diff --git a/.forgejo/PULL_REQUEST_TEMPLATE.md b/.forgejo/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..16941cc494 --- /dev/null +++ b/.forgejo/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Summary + + + +## Changes + + +- `path/to/file` — what changed and why + +## Test Coverage + + +- `tests/Unit/...` — what is covered + +## Quality checklist + +- [ ] `composer check:strict` passes locally (phpcs + phpmd + phpstan + psalm + tests) +- [ ] Hydra gates clean: `/hydra-gates` shows zero `FAIL` lines +- [ ] New `lib/**/*.php` classes have a matching `tests/Unit/**/*Test.php` +- [ ] Burn-down PR? Cite cluster + before/after counts: phpcs __ → __, phpmd __ → __, phpstan __ → __ diff --git a/.forgejo/issue_template/bug-report.yml b/.forgejo/issue_template/bug-report.yml new file mode 100644 index 0000000000..efe108018b --- /dev/null +++ b/.forgejo/issue_template/bug-report.yml @@ -0,0 +1,92 @@ +name: "🐛 Bug Report" +description: "Iets werkt niet zoals verwacht" +title: "[BUG] " +labels: ["bug", "needs-triage"] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## Bug Report + Beschrijf het probleem zo concreet mogelijk zodat het reproduceerbaar is. + + - type: textarea + id: description + attributes: + label: "Beschrijving" + description: "Wat gaat er mis?" + placeholder: "Bij het uploaden van een PDF groter dan 10MB crasht de anonymizer." + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: "Stappen om te reproduceren" + value: | + 1. Ga naar ... + 2. Doe ... + 3. Zie fout ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: "Verwacht gedrag" + placeholder: "Het document wordt anonimiseerd en gedownload." + validations: + required: true + + - type: textarea + id: actual + attributes: + label: "Werkelijk gedrag" + placeholder: "HTTP 500 na ~30 seconden, geen output." + validations: + required: true + + - type: textarea + id: environment + attributes: + label: "Omgeving" + value: | + - Namespace/omgeving: + - Versie/image tag: + - Browser (indien van toepassing): + validations: + required: false + + - type: textarea + id: logs + attributes: + label: "Logs / Screenshots" + description: "Plak relevante logs of voeg screenshots toe" + render: shell + validations: + required: false + + - type: textarea + id: acceptance-criteria + attributes: + label: "Acceptatiecriteria (fix)" + value: | + - [ ] Bug is niet meer reproduceerbaar + - [ ] Regressietest toegevoegd + - [ ] Fix getest in acceptatieomgeving + - [ ] Geen nieuwe security findings + - [ ] Code gereviewd (4-eyes) + validations: + required: true + + - type: dropdown + id: severity + attributes: + label: "Severity" + options: + - "🔴 Critical — productie ligt plat" + - "🟠 High — grote impact, workaround aanwezig" + - "🟡 Medium — beperkte impact" + - "🟢 Low — cosmetic / minor" + validations: + required: true diff --git a/.forgejo/issue_template/feature-request.yml b/.forgejo/issue_template/feature-request.yml new file mode 100644 index 0000000000..41b4c44a49 --- /dev/null +++ b/.forgejo/issue_template/feature-request.yml @@ -0,0 +1,125 @@ +name: "✨ Feature request" +description: "Suggest a feature or improvement. Fields below feed a draft OpenSpec proposal." +title: "[FEATURE] " +labels: ["enhancement", "feature", "needs-triage"] +type: "Feature" +body: + - type: markdown + attributes: + value: | + ## Suggest a feature + + Thanks for telling us what you need. **Triage happens within 24 hours.** + + The fields below feed an OpenSpec proposal directly if the suggestion + is accepted. The more concrete you are, the faster it ships. + + Prefer Dutch? Vul de velden in het Nederlands in — that's fine, we triage in both. + + - type: textarea + id: problem + attributes: + label: "Problem" + description: "What can't you do today? What's the friction? Write it from your perspective — one or two sentences is plenty." + placeholder: "I want to filter contacts by last interaction date but the list view doesn't support it. I end up exporting to CSV and sorting in a spreadsheet." + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: "Proposed solution" + description: "How would you like it to work? Sketches, links, references welcome. \"I'm not sure\" is also a valid answer — we'll figure it out together." + placeholder: "A date-range filter in the contacts list sidebar, defaulting to last 30 days, persisted per user." + validations: + required: true + + - type: textarea + id: who-benefits + attributes: + label: "Who benefits" + description: "Which user role or workflow does this serve? Be specific." + placeholder: "Account managers tracking client engagement, especially before renewal conversations." + validations: + required: true + + - type: dropdown + id: priority-to-you + attributes: + label: "How important is this to you?" + description: "Honest self-assessment. Helps us prioritise." + options: + - "Nice to have" + - "Would use weekly" + - "Would use daily" + - "Blocking me right now" + validations: + required: true + + - type: textarea + id: context + attributes: + label: "Anything else?" + description: "Edge cases, alternatives you've considered, things to avoid, related capabilities, anything that didn't fit in the boxes above." + placeholder: "Out of scope: per-team default filter (could be later). Avoid: hiding the filter behind a settings page — needs to be one click from the list." + + - type: markdown + attributes: + value: | + ### Context + + The fields below are auto-filled when you suggest a feature from inside + the app. They capture where you were when the idea hit so we can scope + the spec without a second round of questions. + + **We show them to you here on purpose**: you can see exactly what we + send and edit or clear any field before you submit. Leave them blank if + you're filing directly from GitHub — we'll still triage it. + + - type: input + id: app + attributes: + label: "App" + description: "Auto-filled by the in-product modal. The Nextcloud app you were using." + placeholder: "pipelinq" + + - type: input + id: page + attributes: + label: "Page" + description: "Auto-filled. The manifest page id + route you were on when you opened the modal." + placeholder: "clients-detail (/clients/abc-123)" + + - type: input + id: surface + attributes: + label: "Modal or widget" + description: "Auto-filled. Any modal, dialog, dashboard widget, or sidebar tab open at the moment the modal launched. Helps us pinpoint UI-attached suggestions." + placeholder: "edit-client-modal · or · dashboard widget: open-leads" + + - type: input + id: object + attributes: + label: "Object in focus" + description: "Auto-filled. The OpenRegister register + schema + UUID the page was viewing, if any. Lets us trace the suggestion to a real data shape." + placeholder: "pipelinq · Client · 2f9d-…-abc" + + - type: input + id: spec-ref + attributes: + label: "Related capability" + description: "Auto-filled if the page or widget declares a `specRef`. Connects the suggestion to the existing OpenSpec for that capability." + placeholder: "client-management" + + - type: markdown + attributes: + value: | + --- + + ### What happens next + + 1. **Within 24 hours**: a maintainer reads this and replies with one of `ready-to-build`, `needs-design`, `parking-lot`, or `wont-build` (with a reason). + 2. **If `ready-to-build`**: an OpenSpec proposal is auto-drafted from these fields. Hydra picks it up and opens a draft PR within days. + 3. **When it ships**: you're credited on the spec, you get a `Co-Authored-By:` trailer on the merge commit, and you appear on the app's contributors page. + + Read the full flow at the [Users are the moat](https://docs.conduction.nl/strategy/users-are-the-moat) strategy doc. diff --git a/.forgejo/issue_template/technical-task.yml b/.forgejo/issue_template/technical-task.yml new file mode 100644 index 0000000000..661b188859 --- /dev/null +++ b/.forgejo/issue_template/technical-task.yml @@ -0,0 +1,92 @@ +name: "⚙️ Technische Taak" +description: "Infra, refactor, technische schuld of ops-werk" +title: "[TECH] " +labels: ["technical", "needs-refinement"] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## Technische Taak + Gebruik dit template voor infra-wijzigingen, refactoring, technische schuld of operationeel werk zonder directe gebruikerswaarde. + + - type: textarea + id: description + attributes: + label: "Beschrijving" + description: "Wat moet er gedaan worden en waarom?" + placeholder: "Migreer de WOO-platform PVC's naar S3 primary storage op Fuga Cloud." + validations: + required: true + + - type: textarea + id: motivation + attributes: + label: "Motivatie / Aanleiding" + description: "Welk probleem lost dit op? Waarom nu?" + placeholder: "Huidige lokale PVC's lopen vol en zijn niet HA. Zie ook issue #123." + validations: + required: false + + - type: textarea + id: approach + attributes: + label: "Aanpak (globaal)" + description: "Hoe gaan we dit oplossen? Welke keuzes zijn al gemaakt?" + placeholder: | + 1. Backup bestaande data + 2. S3 bucket aanmaken op Fuga Cloud + 3. Nextcloud occ storage:update uitvoeren + 4. Smoke test per namespace + validations: + required: false + + - type: textarea + id: acceptance-criteria + attributes: + label: "Acceptatiecriteria" + value: | + - [ ] Taak uitvoerbaar via Ansible/Terraform (geen handmatige stappen) + - [ ] Gedocumenteerd in runbook of ADR + - [ ] Getest in acceptatieomgeving vóór productie + - [ ] Rollback-procedure beschreven + - [ ] Geen downtime buiten afgesproken window + - [ ] Gereviewd (4-eyes) + - [ ] Geen nieuwe security findings + validations: + required: true + + - type: textarea + id: risks + attributes: + label: "Risico's / Afhankelijkheden" + placeholder: "Afhankelijk van beschikbaarheid acceptatieomgeving. Risico: dataverlies bij fout in migratiescript." + validations: + required: false + + - type: dropdown + id: category + attributes: + label: "Categorie" + options: + - "Infra / ops" + - "Refactor" + - "Technische schuld" + - "Security" + - "Performance" + - "CI/CD" + - "Documentatie" + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: "Prioriteit" + options: + - "🔴 Critical" + - "🟠 High" + - "🟡 Medium" + - "🟢 Low" + validations: + required: true diff --git a/.forgejo/issue_template/user-story.yml b/.forgejo/issue_template/user-story.yml new file mode 100644 index 0000000000..0860501a5c --- /dev/null +++ b/.forgejo/issue_template/user-story.yml @@ -0,0 +1,74 @@ +name: "✨ User Story" +description: "Nieuwe functionaliteit vanuit gebruikersperspectief" +title: "Als [rol] wil ik [actie] zodat [waarde]" +labels: ["user-story", "needs-refinement"] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## User Story + Beschrijf de gewenste functionaliteit vanuit het perspectief van de gebruiker. + + - type: textarea + id: story + attributes: + label: "Story" + description: "Als [rol] wil ik [actie] zodat [waarde]" + placeholder: "Als gemeentemedewerker wil ik een document kunnen anonimiseren zodat ik het veilig kan delen." + validations: + required: true + + - type: textarea + id: context + attributes: + label: "Context / Achtergrond" + description: "Waarom is dit nodig? Wat is de aanleiding?" + placeholder: "WOO-verzoeken vereisen anonimisering vóór publicatie..." + validations: + required: false + + - type: textarea + id: acceptance-criteria + attributes: + label: "Acceptatiecriteria" + description: "Definition of Done — vink af wat van toepassing is" + value: | + - [ ] Functionaliteit werkt zoals beschreven in de story + - [ ] Er zijn unit tests aanwezig + - [ ] Er zijn integratietests aanwezig + - [ ] Documentatie is bijgewerkt + - [ ] Code is gereviewd (4-eyes) + - [ ] Geen nieuwe security findings (SAST/Trivy) + - [ ] Getest in acceptatieomgeving + validations: + required: true + + - type: textarea + id: out-of-scope + attributes: + label: "Buiten scope" + description: "Wat doen we expliciet NIET in dit issue?" + placeholder: "Geen bulk-verwerking, geen UI-wijzigingen." + validations: + required: false + + - type: dropdown + id: priority + attributes: + label: "Prioriteit" + options: + - "🔴 Critical" + - "🟠 High" + - "🟡 Medium" + - "🟢 Low" + validations: + required: true + + - type: input + id: story-points + attributes: + label: "Story points (optioneel)" + placeholder: "3" + validations: + required: false diff --git a/.forgejo/workflows/app-tests-live.yml b/.forgejo/workflows/app-tests-live.yml new file mode 100644 index 0000000000..5f280170a8 --- /dev/null +++ b/.forgejo/workflows/app-tests-live.yml @@ -0,0 +1,50 @@ +# app-tests-live.yml — openregister caller for the LIVE-NC reusable workflow. +# +# PHASE-5 NC-in-CI proof: makes openregister's deep e2e (tests/e2e/workflows/) +# AND Newman API-contract suite GATE on every PR — they boot a real, seeded +# Nextcloud (db + NC + OR deployed/enabled) and run against it, so a PR can no +# longer go green while those flows are broken. +# +# This is the Forgejo/Codeberg counterpart to the GitHub-Actions reference rig +# (.github/workflows/api-test-coverage.yml), which already gates Newman on +# GitHub pushes. Both boot .github/docker-compose.ci.yml; this one ALSO runs +# the deep Playwright workflows and is the fragment feature apps replicate. +# Additive — sits alongside app-tests.yml (bare gates) + the release workflows. + +name: app-tests-live + +on: + pull_request: + branches: + - development + - main + - beta + push: + branches: + - development + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + live: + uses: ./.forgejo/workflows/tests-live.yml + with: + app-id: openregister + is-openregister: true + # GATING: both suites run against the live NC and fail the PR on a defect. + run-e2e: true + run-newman: true + # NON-GATING (GAP-5): runs the visual-regression project + uploads + # snapshots/diffs as an artifact. Committed baselines are dev-container + # native, so they will not byte-match the CI Linux runner until + # regenerated in-CI (see tests-live.yml run-visual caveat) — the step is + # continue-on-error so it never blocks a PR. + run-visual: true + # openregister uses the orchestrator (multi-collection), restricted to the + # self-seeding domains that don't assume dev-container fixture IDs — same + # default set the GitHub api-test-coverage.yml rig uses. + newman-entrypoint: tests/newman/run-all.sh + newman-collections: "crud graphql relations auth-matrix error-matrix referential-integrity" diff --git a/.forgejo/workflows/app-tests.yml b/.forgejo/workflows/app-tests.yml new file mode 100644 index 0000000000..4d44fcb28d --- /dev/null +++ b/.forgejo/workflows/app-tests.yml @@ -0,0 +1,49 @@ +# app-tests.yml — openregister caller for the reusable feature-test workflow. +# +# Runs the PHASE-5 testing layers on every PR to the protected branches. +# phpunit-unit + l10n-check HARD-GATE; e2e/newman are scaffolded and opt-in +# (run-e2e / run-newman) until the NC service container is wired (see tests.yml +# TODOs). openregister already has a full Newman-in-CI job +# (.github/workflows/api-test-coverage.yml booting .github/docker-compose.ci.yml) +# — the scaffolded newman job here documents the local entrypoint and stays +# non-gating so the two don't duplicate. Additive — sits alongside +# pre-merge-check-strict.yaml and the release workflows. + +name: app-tests + +on: + pull_request: + branches: + - development + - main + - beta + workflow_dispatch: + inputs: + run-e2e: + type: boolean + default: false + run-newman: + type: boolean + default: false + +permissions: + contents: read + +jobs: + tests: + uses: ./.forgejo/workflows/tests.yml + with: + app-id: openregister + # The reusable tests.yml phpunit-unit job runs in a BARE PHP container + # (no Nextcloud). openregister's unit suite cannot run there: its classes + # touch IQueryBuilder/RootFolder/Hooks, whose OCP stubs reference + # Doctrine\DBAL\* and OC\* internals that only resolve once lib/base.php + # is bootstrapped (~2900 structural "Class not found" errors otherwise). + # So this job stays non-gating; the unit suite is HARD-GATED in-container + # instead — see .github/workflows/api-test-coverage.yml → "PHPUnit unit + # suite (in-container, HARD GATE)". The suite itself is GREEN there + # (0 errors / 0 failures; the prior ~1463-error baseline was harness + # drift — see TESTING-CI-ROLLOUT.md → "openregister unit-suite"). + unit-gating: false + run-e2e: ${{ github.event.inputs.run-e2e == 'true' }} + run-newman: ${{ github.event.inputs.run-newman == 'true' }} diff --git a/.forgejo/workflows/documentation.yml b/.forgejo/workflows/documentation.yml new file mode 100644 index 0000000000..1b99e49db4 --- /dev/null +++ b/.forgejo/workflows/documentation.yml @@ -0,0 +1,25 @@ +name: Publish docs + +# Docs deploy ONLY from the dedicated `documentation` branch — decoupled from main/development +# so doc edits never trigger releases and code releases never trigger doc builds. No cron. +on: + push: + branches: [documentation] + pull_request: + branches: [documentation] + workflow_dispatch: + +jobs: + build: + uses: Conduction/.github/.forgejo/workflows/documentation-build.yml@main + with: + source-folder: docs + secrets: inherit + + deploy: + needs: build + if: github.event_name != 'pull_request' + uses: Conduction/.github/.forgejo/workflows/documentation-deploy.yml@main + with: + cf-project-name: openregister-docs + secrets: inherit diff --git a/.forgejo/workflows/pre-merge-check-strict.yaml b/.forgejo/workflows/pre-merge-check-strict.yaml new file mode 100644 index 0000000000..863f2b03e3 --- /dev/null +++ b/.forgejo/workflows/pre-merge-check-strict.yaml @@ -0,0 +1,70 @@ +# Pre-merge quality gate — enforced lint + phpcs + all Hydra gates on every PR. +# Required status check on protected branches. +# +# Runner/container mirror the proven release-semrel workflow: codeberg-medium + +# official php:8.3-cli + a base-tooling step. The old code.forgejo.org/oci/ci-php:8.3 +# image 404s ("manifest unknown"), which fast-failed every run at container-pull. +# +# The gate runs `composer lint` + `composer phpcs` directly: check:strict's +# psalm/phpstan/phpmd/test:all are wrapped in `|| echo skipping` so they never +# affect pass/fail (ADR-022 parks static analysis), and running them on the medium +# runner OOMs it. lint+phpcs is the identical enforced gate, fast and deterministic. + +name: pre-merge-check-strict + +on: + pull_request: + branches: + - development + - main + - beta + +jobs: + quality-gates: + runs-on: codeberg-medium + container: + image: php:8.3-cli + timeout-minutes: 15 + steps: + - name: Install base tooling + run: | + apt-get update + apt-get install -y --no-install-recommends \ + git curl ca-certificates gnupg jq unzip zip \ + libzip-dev libpng-dev python3 + # Node is required by actions/checkout@v4 (a JS action) which runs + # inside this php:8.3-cli container; the stock image ships no node. + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y --no-install-recommends nodejs + docker-php-ext-install -j"$(nproc)" zip gd + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + + - name: Checkout PR + uses: https://github.com/actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install composer deps + run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-reqs + + - name: Run lint + phpcs (the enforced gate) + run: | + composer lint + composer phpcs + + - name: Clone Hydra (for gate runner) + uses: https://github.com/actions/checkout@v4 + with: + repository: Conduction/hydra + ref: development + path: .hydra + + - name: Run all Hydra gates (diff-scoped per ADR-020) + run: | + git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} || true + bash .hydra/scripts/run-hydra-gates.sh --scope-to-diff --base origin/${{ github.base_ref }} . + + - name: Gate-19 e2e coverage report (informational) + if: always() + run: | + python3 .hydra/scripts/lib/check_e2e_coverage.py . --mode report || true diff --git a/.forgejo/workflows/release-beta.yml b/.forgejo/workflows/release-beta.yml new file mode 100644 index 0000000000..00ee9a9077 --- /dev/null +++ b/.forgejo/workflows/release-beta.yml @@ -0,0 +1,13 @@ +name: Beta Release + +on: + push: + branches: [beta] + workflow_dispatch: + +jobs: + release: + uses: Conduction/.github/.forgejo/workflows/release-semrel-beta.yml@main + with: + app-name: openregister + secrets: inherit diff --git a/.forgejo/workflows/release-stable.yml b/.forgejo/workflows/release-stable.yml new file mode 100644 index 0000000000..a39021fe15 --- /dev/null +++ b/.forgejo/workflows/release-stable.yml @@ -0,0 +1,13 @@ +name: Stable Release + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + release: + uses: Conduction/.github/.forgejo/workflows/release-semrel.yml@main + with: + app-name: openregister + secrets: inherit diff --git a/.forgejo/workflows/tests-live.yml b/.forgejo/workflows/tests-live.yml new file mode 100644 index 0000000000..d1a3ed6372 --- /dev/null +++ b/.forgejo/workflows/tests-live.yml @@ -0,0 +1,327 @@ +# tests-live.yml — reusable LIVE-NC feature-test workflow (PHASE-5 NC-in-CI). +# +# This is the GATING counterpart to tests.yml: where tests.yml runs the bare +# layers (phpunit-unit, l10n) that need no service container, this workflow +# boots a real, seeded Nextcloud and runs the two layers that DO need one — +# the deep Playwright e2e (tests/e2e/workflows/) and the Newman API-contract +# suite — so a PR can no longer go green while those flows are broken. +# +# It generalises openregister's reference rig +# (.github/workflows/api-test-coverage.yml + .github/docker-compose.ci.yml), +# which already boots NC + Postgres, deploys + enables OpenRegister, waits for +# the API and runs Newman. The only per-app knobs are `app-id` and whether the +# app is itself openregister (the data backend) or a feature app that needs +# openregister enabled ALONGSIDE it. +# +# Per-app PREREQUISITE (the common seeding step): +# The deep e2e + Newman suites must be SELF-SEEDING — each spec/collection +# creates the OR register + schema + objects it asserts on in its +# beforeAll/setUp and tears them down in afterAll/teardown. openregister's +# tests/e2e/workflows/object-lifecycle-workflows.spec.ts and the Newman +# collections already do this. A feature app whose suites assume a +# pre-imported OR register-config must EITHER add an `occ` seed step here +# (import the app's register-config, e.g. via its Repair step or +# `occ :import-config`) OR make the suites self-seed. Until one of +# those is true, keep run-e2e/run-newman false for that app. +# +# Runner label `docker` (needs a docker daemon to boot the compose stack) + +# the short `https://code.forgejo.org/actions/...@v4` `uses:` form follow the +# fleet convention. Additive — does not touch tests.yml, pre-merge-check-strict +# or any release workflow. + +name: tests-live + +on: + workflow_call: + inputs: + app-id: + description: "App id (matches package.json name + composer namespace + custom_apps dir)." + required: true + type: string + node-version: + required: false + type: string + default: "20" + php-version: + required: false + type: string + default: "8.3" + is-openregister: + description: "True when app-id IS openregister (the data backend). Feature apps leave this false; openregister is enabled alongside them." + required: false + type: boolean + default: false + run-e2e: + description: "Run the deep Playwright e2e (tests/e2e/workflows/) against the live NC. GATING when true." + required: false + type: boolean + default: false + run-newman: + description: "Run the Newman API-contract suite against the live NC. GATING when true." + required: false + type: boolean + default: false + run-visual: + description: >- + Run the Playwright visual-regression project (tests/e2e/visual/, GAP-5) + against the live NC. NON-GATING by design (continue-on-error). PLATFORM + CAVEAT: PNG baselines are host-font/GPU specific, so committed + dev-container baselines will NOT byte-match a CI Linux runner. On the + first CI run the baselines must be regenerated in-CI (download the + uploaded visual-snapshots artifact and commit it) before this step can + be made gating. Until then it reports diffs as an artifact only. + required: false + type: boolean + default: false + newman-entrypoint: + description: "Path to the Newman runner. openregister uses the orchestrator; feature apps use tests/integration/run-newman.sh." + required: false + type: string + default: "tests/newman/run-all.sh" + newman-collections: + description: "COLLECTIONS subset passed to the orchestrator (self-seeding domains only; excludes fixtures that assume dev-container state)." + required: false + type: string + default: "crud graphql relations auth-matrix error-matrix referential-integrity" + +permissions: + contents: read + +jobs: + # --------------------------------------------------------------------------- + # LIVE GATE — boot seeded NC, deploy OR (+ the app), run deep e2e + Newman. + # + # A single job boots the stack once and runs both suites against it (cheaper + # than two stacks; both are read-mostly + self-seeding so they don't collide + # — e2e prefixes its fixtures with a run-id, Newman teardown-cleans its own). + # --------------------------------------------------------------------------- + live-nc: + name: Live NC e2e + Newman (${{ inputs.app-id }}) + if: ${{ inputs.run-e2e || inputs.run-newman || inputs.run-visual }} + runs-on: docker + timeout-minutes: 40 + permissions: + contents: read + env: + APP_ID: ${{ inputs.app-id }} + COMPOSE_FILE: .github/docker-compose.ci.yml + steps: + - name: Checkout + uses: https://code.forgejo.org/actions/checkout@v4 + + - name: Set up PHP + uses: https://github.com/shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + tools: composer:v2 + coverage: none + + - name: Set up Node.js + uses: https://code.forgejo.org/actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Install composer deps (production) + run: composer install --no-dev --no-interaction --no-progress --prefer-dist + + - name: Install npm deps + Newman + run: | + npm ci --no-audit --no-fund || npm install --no-audit --no-fund + npm install -g newman + + - name: Boot CI stack (db + Nextcloud) + # Reuses openregister's reference compose (db + NC, named volume only). + # A feature app that does not ship its own compose should commit a copy + # of openregister/.github/docker-compose.ci.yml. + run: docker compose -f "$COMPOSE_FILE" up -d + + - name: Wait for Nextcloud to be installed + run: | + for i in $(seq 1 60); do + if docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ status" 2>/dev/null | grep -q "installed: true"; then + echo "Nextcloud installed and ready" + break + fi + echo "Waiting for Nextcloud installation... ($i/60)" + sleep 5 + done + docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ status" + + - name: Deploy OpenRegister (data backend) + # The app under test stores its objects in OpenRegister, so OR must be + # deployed+enabled regardless of which app we're testing. When the app + # UNDER TEST *is* openregister, this single deploy covers it. + run: | + if [ "${{ inputs.is-openregister }}" = "true" ]; then + OR_SRC="." + else + # Feature apps build against a sibling openregister checkout in the + # same apps-extra tree. CI clones only this repo, so fetch OR's + # release build instead. + OR_SRC="$RUNNER_TEMP/openregister" + git clone --depth 1 https://codeberg.org/Conduction/openregister.git "$OR_SRC" + ( cd "$OR_SRC" && composer install --no-dev --no-interaction --no-progress --prefer-dist ) + fi + docker exec nextcloud mkdir -p /var/www/html/custom_apps/openregister + tar --exclude='.git' --exclude='node_modules' --exclude='.claude' \ + --exclude='tests/e2e/playwright-report' --exclude='tests/e2e/test-results' \ + -C "$OR_SRC" -cf - . \ + | docker exec -i nextcloud tar -xf - -C /var/www/html/custom_apps/openregister + docker exec nextcloud chown -R www-data:www-data /var/www/html/custom_apps/openregister + docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:enable openregister" + + - name: Deploy app under test + # Skipped when the app IS openregister (already deployed above). + if: ${{ inputs.is-openregister == false }} + run: | + docker exec nextcloud mkdir -p "/var/www/html/custom_apps/$APP_ID" + tar --exclude='.git' --exclude='node_modules' --exclude='.claude' \ + --exclude='tests/e2e/playwright-report' --exclude='tests/e2e/test-results' \ + -cf - . \ + | docker exec -i nextcloud tar -xf - -C "/var/www/html/custom_apps/$APP_ID" + docker exec nextcloud chown -R www-data:www-data "/var/www/html/custom_apps/$APP_ID" + docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:enable $APP_ID" + docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:list" | grep "$APP_ID" + + - name: Seed OR register-config (per-app prerequisite) + # SELF-SEEDING suites need nothing here. An app whose deep-e2e / Newman + # assumes a pre-imported register-config should import it now, e.g.: + # docker exec nextcloud su -s /bin/bash www-data -c \ + # "php /var/www/html/occ $APP_ID:import-config" # if the app ships one + # or trigger the app's Repair step (most Conduction apps import their + # register via lib/Repair/Initialize*.php on app:enable — already done + # by the enable step above). Left as an explicit, documented hook. + run: | + echo "Seeding: openregister's own suites are self-seeding (fixtures create" + echo "register+schema+objects per run-id, torn down in afterAll/teardown)." + echo "Feature apps: add the register-config import here (see step comment)." + + - name: Wait for the API to respond + # app:enable returns before the repair/magic-mapping step + PHP-FPM + # opcache warm-up finish; poll the OR registers endpoint until it + # returns a real HTTP status (not connection-level 000). + run: | + for i in $(seq 1 60); do + code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \ + -u admin:admin \ + http://localhost:8080/index.php/apps/openregister/api/registers \ + 2>/dev/null | tr -d '\n') + if [ -n "$code" ] && [ ${#code} -eq 3 ] \ + && [ "$code" -ge 100 ] 2>/dev/null && [ "$code" -lt 500 ]; then + echo "OpenRegister API responding (HTTP $code, attempt $i)" + sleep 5 + break + fi + echo "Waiting for API... (attempt $i/60, last='$code')" + sleep 2 + done + + - name: Mint admin storageState + run deep e2e (GATING) + # The app's playwright global-setup logs into NC once and persists the + # cookie jar to tests/e2e/.auth/admin.json (the storageState the + # workflows spec consumes). We run ONLY tests/e2e/workflows here — the + # deep, data-dependent layer — against the live, self-seeded NC. + if: ${{ inputs.run-e2e }} + env: + NEXTCLOUD_URL: http://localhost:8080 + NC_ADMIN_USER: admin + NC_ADMIN_PASS: admin + OR_USER: admin + OR_PASS: admin + CI: "true" + run: | + npx playwright install --with-deps chromium + npx playwright test tests/e2e/workflows + + - name: Run visual-regression project (NON-GATING — GAP-5) + # Visual baselines are rendered against the local dev container; a CI + # Linux runner uses a different font stack + GPU so the committed PNGs + # will not byte-match here. This step is therefore NON-GATING + # (continue-on-error) and exists to (a) surface visual diffs as an + # artifact and (b) let a maintainer regenerate CI-native baselines: + # download the `visual-snapshots-` artifact from a run with + # PLAYWRIGHT_UPDATE=1 and commit it, then drop continue-on-error to + # make the step gating in the CI environment. + if: ${{ inputs.run-visual }} + continue-on-error: true + env: + NEXTCLOUD_URL: http://localhost:8080 + NC_ADMIN_USER: admin + NC_ADMIN_PASS: admin + OR_USER: admin + OR_PASS: admin + CI: "true" + run: | + npx playwright install --with-deps chromium + if [ "${PLAYWRIGHT_UPDATE:-0}" = "1" ]; then + echo "Regenerating CI-native visual baselines (--update-snapshots)…" + npx playwright test --project visual --update-snapshots || true + else + npx playwright test --project visual || true + fi + + - name: Upload visual snapshots + diffs (NON-GATING) + if: ${{ inputs.run-visual }} + continue-on-error: true + uses: https://code.forgejo.org/actions/upload-artifact@v4 + with: + name: visual-snapshots-${{ inputs.app-id }} + path: | + tests/e2e/visual/**/*-snapshots/** + tests/e2e/test-results/** + retention-days: 14 + if-no-files-found: ignore + + - name: Run Newman API-contract suite (GATING) + if: ${{ inputs.run-newman }} + env: + BASE_URL: http://localhost:8080 + ADMIN_USER: admin + ADMIN_PASSWORD: admin + CONTAINER_NAME: nextcloud + NEWMAN_RUNNER: host + FAIL_FAST: "0" + COLLECTIONS: ${{ inputs.newman-collections }} + run: bash "${{ inputs.newman-entrypoint }}" + + - name: Upload Playwright report + if: always() + uses: https://code.forgejo.org/actions/upload-artifact@v4 + with: + name: playwright-report-${{ inputs.app-id }} + path: | + tests/e2e/playwright-report/ + tests/e2e/test-results/ + retention-days: 14 + if-no-files-found: ignore + + - name: Upload Newman reports + if: always() + uses: https://code.forgejo.org/actions/upload-artifact@v4 + with: + name: newman-reports-${{ inputs.app-id }} + path: | + tests/newman/reports/ + newman-*.json + retention-days: 14 + if-no-files-found: ignore + + - name: Collect docker logs on failure + if: failure() + run: | + mkdir -p ci-logs + docker compose -f "$COMPOSE_FILE" logs --no-color > ci-logs/docker-compose.log 2>&1 || true + docker exec nextcloud cat /var/www/html/data/nextcloud.log > ci-logs/nextcloud.log 2>&1 || true + + - name: Upload docker logs + if: failure() + uses: https://code.forgejo.org/actions/upload-artifact@v4 + with: + name: ci-logs-${{ inputs.app-id }} + path: ci-logs/ + retention-days: 14 + if-no-files-found: ignore + + - name: Tear down CI stack + if: always() + run: docker compose -f "$COMPOSE_FILE" down -v diff --git a/.forgejo/workflows/tests.yml b/.forgejo/workflows/tests.yml new file mode 100644 index 0000000000..aa04998ef9 --- /dev/null +++ b/.forgejo/workflows/tests.yml @@ -0,0 +1,281 @@ +# tests.yml — reusable feature-test workflow for Conduction NC apps. +# +# PHASE-5 CI enforcement: runs the testing layers we built so a PR can't go +# green while a feature is broken. This is a `workflow_call` reusable: a thin +# per-app caller (app-tests.yml) invokes it with the app id. Copy the pair +# (this file + app-tests.yml) into any sibling app to roll the pattern out — +# the only per-app knob is `app-id`. +# +# Layers, and what gates today: +# • phpunit-unit — HARD GATE. tests/Unit + tests/unit via phpunit-unit.xml. +# The bootstrap runs standalone (vendor OCP stubs, no NC), +# so this needs no service container. +# • l10n-check — HARD GATE. tests/l10n/check-l10n.js asserts every +# t('', '...') / n(...) source string is present in +# l10n/en.json (the i18n-extraction-drift guard). Pure +# Node, no NC. +# • e2e-deep — SCAFFOLDED (non-gating). Playwright tests/e2e/workflows/ +# need a live, seeded NC. See the TODO in that job. +# • newman — SCAFFOLDED (non-gating). tests/integration Postman +# collections via run-newman.sh need a live NC. See TODO. +# +# Runner labels + reusable `uses:` forms follow the fleet convention +# (codeberg-small / short Conduction/.github@main form). Additive — does not +# touch pre-merge-check-strict.yaml or any release workflow. + +name: tests + +on: + workflow_call: + inputs: + app-id: + description: "App id (matches package.json name + composer namespace)." + required: true + type: string + php-version: + required: false + type: string + default: "8.3" + node-version: + required: false + type: string + default: "20" + unit-gating: + description: "Fail the workflow on unit-test failures. Set false for apps whose tests/Unit suite is not yet green on baseline (e.g. openregister — see TESTING-CI-ROLLOUT.md)." + required: false + type: boolean + default: true + run-e2e: + description: "Run the scaffolded deep-e2e job (needs NC service — see TODO)." + required: false + type: boolean + default: false + run-newman: + description: "Run the scaffolded Newman job (needs NC service — see TODO)." + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + # --------------------------------------------------------------------------- + # HARD GATE 1 — PHPUnit unit suite (no NC needed; vendor OCP stubs). + # --------------------------------------------------------------------------- + phpunit-unit: + name: PHPUnit unit (${{ inputs.app-id }}) + runs-on: codeberg-medium + container: + image: php:8.3-cli + steps: + - name: Install base tooling + run: | + apt-get update + apt-get install -y --no-install-recommends \ + git curl ca-certificates gnupg jq unzip zip \ + libzip-dev libpng-dev python3 + # Node is required by actions/checkout@v4 (a JS action) which runs + # inside this php:8.3-cli container; the stock image ships no node. + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y --no-install-recommends nodejs + docker-php-ext-install -j"$(nproc)" zip gd + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Install composer deps + run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-reqs + + - name: Run unit suite (phpunit-unit.xml) + # unit-gating=false reports failures without failing the job, for apps + # carrying pre-existing tests/Unit debt (see TESTING-CI-ROLLOUT.md). + # Wired to the input so a caller passing `unit-gating: false` (e.g. + # openregister — the suite is GREEN but failOnWarning trips on carried + # PHP-warning debt, and its real hard gate is the in-container run in + # api-test-coverage.yml) actually gets the documented non-blocking + # behaviour instead of a hardcoded hard failure. + continue-on-error: ${{ inputs.unit-gating == false }} + run: ./vendor/bin/phpunit --configuration phpunit-unit.xml --no-coverage --colors=never + + # --------------------------------------------------------------------------- + # HARD GATE 2 — l10n extraction-drift check (no NC needed; pure Node). + # --------------------------------------------------------------------------- + l10n-check: + name: l10n extraction check (${{ inputs.app-id }}) + runs-on: codeberg-medium + container: + image: node:${{ inputs.node-version }} + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Assert every t() source string is in l10n/en.json + run: node tests/l10n/check-l10n.js + + # --------------------------------------------------------------------------- + # HARD GATE 3 — frontend unit suite (Vitest, OFFLINE; no NC needed). + # + # Runs the pure-logic Vitest suite under tests/vitest/** (Pinia store + # state transitions, util/formatter calc, form-validation mappers, and any + # offline component mounts). These need no DOM/NC runtime — @nextcloud/* and + # @conduction/nextcloud-vue are aliased to deterministic stubs in + # vitest.config.js. Always gating. + # --------------------------------------------------------------------------- + frontend-unit: + name: Frontend unit (Vitest — ${{ inputs.app-id }}) + runs-on: codeberg-medium + container: + image: node:${{ inputs.node-version }} + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Install npm deps + run: npm ci --legacy-peer-deps || npm install --legacy-peer-deps + + - name: Run Vitest unit suite + run: npm run test:unit + + # --------------------------------------------------------------------------- + # COVERAGE GATE A — PHPUnit COVERAGE RATCHET (PCOV clover line coverage). + # Fails a PR that drops backend coverage below tests/.coverage-baseline.json + # `phpunit` minus tolerance. Inherits continue-on-error from unit-gating so a + # red unit suite records-but-does-not-block. Seeds on first --update run when + # the baseline is null. See TESTING-CI-ROLLOUT.md "Coverage ratchet". + # --------------------------------------------------------------------------- + phpunit-coverage-ratchet: + name: PHPUnit coverage ratchet (${{ inputs.app-id }}) + runs-on: codeberg-medium + container: + image: php:8.3-cli + continue-on-error: false + steps: + - name: Install base tooling + run: | + apt-get update + apt-get install -y --no-install-recommends \ + git curl ca-certificates gnupg jq unzip zip \ + libzip-dev libpng-dev python3 + # Node is required by actions/checkout@v4 (a JS action) which runs + # inside this php:8.3-cli container; the stock image ships no node. + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y --no-install-recommends nodejs + docker-php-ext-install -j"$(nproc)" zip gd + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Ensure a coverage driver (PCOV) + run: | + if ! php -m | grep -qiE 'pcov|xdebug'; then + (pecl install pcov && docker-php-ext-enable pcov) \ + || echo "WARN: could not install pcov — coverage step may report no driver" + fi + php -m | grep -qiE 'pcov|xdebug' && echo "coverage driver: present" \ + || echo "coverage driver: ABSENT (ratchet will no-op; see TESTING-CI-ROLLOUT.md)" + + - name: Install composer deps + run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-reqs + + - name: Run unit suite WITH coverage (clover) + run: | + php -d pcov.enabled=1 -d pcov.directory=lib \ + ./vendor/bin/phpunit --configuration phpunit-unit.xml \ + --coverage-clover coverage/clover.xml --colors=never || true + test -f coverage/clover.xml || { echo "no clover.xml (driver absent?) — skipping ratchet"; exit 0; } + + - name: Coverage ratchet (fail on drop) + run: | + test -f coverage/clover.xml || exit 0 + bash tests/coverage-ratchet.sh phpunit coverage/clover.xml + + # --------------------------------------------------------------------------- + # COVERAGE GATE B — FRONTEND COVERAGE RATCHET (Vitest v8, src/** line coverage). + # Fails a PR that drops frontend coverage below baseline `vitest` minus + # tolerance. Seeds on first --update run when the baseline is null. + # --------------------------------------------------------------------------- + frontend-coverage-ratchet: + name: Frontend coverage ratchet (${{ inputs.app-id }}) + runs-on: codeberg-medium + container: + image: node:${{ inputs.node-version }} + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Install npm deps (+ coverage-v8) + run: | + npm ci --legacy-peer-deps || npm install --legacy-peer-deps + VITEST_VER="$(node -e "console.log(require('./node_modules/vitest/package.json').version)")" + npm install --no-save --legacy-peer-deps "@vitest/coverage-v8@${VITEST_VER}" + + - name: Run Vitest coverage + ratchet (honors each app's own --coverage.include) + run: | + # Use the app's OWN coverage script so its per-app --coverage.include is + # honored (apps keep frontend JS under src/** OR js/**; hardcoding src/** + # here makes js/**-based apps measure 0 files -> "Unknown" -> ratchet exit 2). + if npm run 2>/dev/null | grep -qE '(^|[[:space:]])test:coverage-ratchet([[:space:]]|$)'; then + npm run test:coverage-ratchet + elif npm run 2>/dev/null | grep -qE '(^|[[:space:]])test:coverage([[:space:]]|$)'; then + npm run test:coverage + test -f coverage-vitest/coverage-summary.json \ + && bash tests/coverage-ratchet.sh vitest coverage-vitest/coverage-summary.json \ + || { echo "no coverage-summary.json — vitest coverage unavailable; skipping ratchet"; exit 0; } + else + echo "no frontend coverage script for this app; skipping ratchet"; exit 0 + fi + + # --------------------------------------------------------------------------- + # SCAFFOLD — deep e2e (Playwright tests/e2e/workflows/). Opt-in via run-e2e. + # + # TODO(nc-in-ci): wire a live Nextcloud before flipping this to gating: + # 1. Boot db + NC (openregister ships .github/docker-compose.ci.yml; add a + # sibling compose for feature apps, or reuse the OR stack + enable this + # app + its OR register/schema fixtures). + # 2. Deploy the working tree into custom_apps/ and `occ app:enable` + # (+ openregister, the data backend). + # 3. Seed the deep-e2e fixtures (tests/e2e/workflows/*fixture*.ts seed the + # OR objects each workflow asserts on). + # 4. `npx playwright install --with-deps chromium` then + # `npm run test:e2e -- tests/e2e/workflows`. + # Until then this job documents the command and is non-gating. + # --------------------------------------------------------------------------- + e2e-deep: + name: Deep e2e (scaffold — needs NC) + if: ${{ inputs.run-e2e }} + runs-on: docker + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: TODO — boot seeded NC, then run deep e2e + run: | + echo "Deep e2e requires a live, seeded Nextcloud (see job header TODO)." + echo "Local: npm ci && npm run test:e2e:install && npm run test:e2e -- tests/e2e/workflows" + echo "Skipping in CI until the NC service container is wired." + + # --------------------------------------------------------------------------- + # SCAFFOLD — Newman API-contract (tests/integration/*.postman_collection.json). + # Opt-in via run-newman. + # + # TODO(nc-in-ci): same live-NC prerequisite as e2e-deep. The runner script + # (tests/integration/run-newman.sh) is collection-self-seeding and runnable + # locally today: `bash tests/integration/run-newman.sh`. openregister uses + # the orchestrator at tests/newman/run-all.sh instead. + # --------------------------------------------------------------------------- + newman: + name: Newman API contract (scaffold — needs NC) + if: ${{ inputs.run-newman }} + runs-on: docker + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: TODO — boot NC, then run Newman + run: | + echo "Newman requires a live Nextcloud serving the app (see job header TODO)." + echo "Local: bash tests/integration/run-newman.sh # OR: bash tests/newman/run-all.sh" + echo "Skipping in CI until the NC service container is wired." diff --git a/.forgejo/workflows/weekly-check-strict.yaml b/.forgejo/workflows/weekly-check-strict.yaml new file mode 100644 index 0000000000..4e7e7b3475 --- /dev/null +++ b/.forgejo/workflows/weekly-check-strict.yaml @@ -0,0 +1,32 @@ +# Weekly smoke run of composer check:strict against `development`. +# +# Catches silent baseline regression — i.e. the case where a PR landed quietly +# without burning down its share of the phpstan-baseline.neon or where PHPMD +# violations crept back in without being noticed. +# +# Spec ref: openspec/changes/openregister-legacy-quality-cleanup/tasks.md#5-3 +name: weekly-check-strict + +on: + schedule: + # Mondays at 06:00 UTC. + - cron: '0 6 * * 1' + workflow_dispatch: + # Allow manual trigger from the Actions UI for ad-hoc baseline-regression checks. + +jobs: + weekly-strict: + runs-on: docker + container: + image: code.forgejo.org/oci/ci-php:8.3 + steps: + - name: Checkout development + uses: https://code.forgejo.org/actions/checkout@v4 + with: + ref: development + + - name: Install composer deps + run: composer install --no-interaction --no-progress --prefer-dist + + - name: Run composer check:strict (catches silent baseline regression) + run: composer check:strict diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f5f4f071b3..78639353c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,36 +1,3 @@ -# CODEOWNERS — auto-request reviewers based on path domain. -# Last-match-wins; broad rules first, specific overrides below. -# Created 2026-05-03 from the OR-abstraction audit follow-up. - -# Default: every named codeowner reviews unmatched paths. -* @rubenvdlinde @rjzondervan @Rem-Dam @remko48 @WilcoLouwerse @bbrands02 @SudoThijn - -# Backend (PHP) — services, controllers, mappers, db, migration, etc. -lib/ @bbrands02 @rjzondervan @WilcoLouwerse -appinfo/ @bbrands02 @rjzondervan @WilcoLouwerse -**/*.php @bbrands02 @rjzondervan @WilcoLouwerse -phpcs.xml @bbrands02 @rjzondervan @WilcoLouwerse -phpmd.xml @bbrands02 @rjzondervan @WilcoLouwerse -phpstan.neon @bbrands02 @rjzondervan @WilcoLouwerse -phpstan-baseline.neon @bbrands02 @rjzondervan @WilcoLouwerse -phpmd.baseline.xml @bbrands02 @rjzondervan @WilcoLouwerse -composer.json @bbrands02 @rjzondervan @WilcoLouwerse -composer.lock @bbrands02 @rjzondervan @WilcoLouwerse - -# Frontend (Vue / TS / JS) — components, stores, pages, build config. -src/ @SudoThijn @remko48 -**/*.vue @SudoThijn @remko48 -**/*.ts @SudoThijn @remko48 -**/*.js @SudoThijn @remko48 -package.json @SudoThijn @remko48 -package-lock.json @SudoThijn @remko48 -jest.config.js @SudoThijn @remko48 -playwright.config.ts @SudoThijn @remko48 -webpack.config.js @SudoThijn @remko48 -babel.config.js @SudoThijn @remko48 - -# Specs / docs / ADRs / openspec. -openspec/ @rubenvdlinde @Rem-Dam -docs/ @rubenvdlinde @Rem-Dam -**/*.md @rubenvdlinde @Rem-Dam -README.md @rubenvdlinde @Rem-Dam +# Coverage baseline — only updated automatically by CI. +# Manual changes require explicit approval from maintainers. +.coverage-baseline @ConductionNL/developers diff --git a/.github/workflows/api-test-coverage.yml b/.github/workflows/api-test-coverage.yml index d9f6da1a3e..39d8186e9f 100644 --- a/.github/workflows/api-test-coverage.yml +++ b/.github/workflows/api-test-coverage.yml @@ -98,6 +98,32 @@ jobs: docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:enable openregister" docker exec nextcloud su -s /bin/bash www-data -c "php /var/www/html/occ app:list" | grep openregister + - name: PHPUnit unit suite (in-container, HARD GATE) + # The unit suite requires the full Nextcloud runtime — the OCP stubs + # reference Doctrine\DBAL\* and OC\* internals that only resolve once + # lib/base.php is bootstrapped, so it CANNOT run in a bare PHP CI + # container (see tests/bootstrap.php + TESTING-CI-ROLLOUT.md). Run it + # here, inside the booted NC stack, after the app is deployed/enabled. + # + # Copy the app OUT of /var/www/html first: when phpunit runs from + # inside custom_apps, NC's autoloader (already loaded by base.php) + # hijacks the bootstrap. A unique COMPOSER_AUTOLOADER_SUFFIX avoids the + # ComposerAutoloaderInit redeclaration between the live and copied vendor. + run: | + docker exec -u www-data nextcloud bash -lc ' + set -e + rm -rf /tmp/ortest + cp -r /var/www/html/custom_apps/openregister /tmp/ortest + cd /tmp/ortest + # The deployed vendor is production-only (--no-dev) so it lacks + # phpunit. Install dev deps + a unique autoloader suffix in the copy + # (the suffix avoids the ComposerAutoloaderInit redeclaration against + # the live app vendor already loaded by base.php). + php -r "\$j=json_decode(file_get_contents(\"composer.json\"),true); \$j[\"config\"][\"autoloader-suffix\"]=\"OrTestCI\"; file_put_contents(\"composer.json\", json_encode(\$j, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));" + php composer.phar install --no-interaction --no-progress --prefer-dist + OPENREGISTER_TEST_NC_ROOT=/var/www/html php vendor/bin/phpunit -c phpunit.xml --no-coverage --colors=never tests/Unit + ' + - name: Wait for openregister API to respond run: | # app:enable returns before the magic-mapping repair step + PHP-FPM diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 76db84b736..17f4142f3a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -27,6 +27,6 @@ jobs: enable-frontend: true enable-eslint: true enable-phpunit: true - enable-newman: false # Newman runs in its own workflow (api-test-coverage.yml) with the orchestrator + docker-compose stack. + enable-newman: true enable-coverage-guard: true enable-sbom: true diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index ccb1515698..774f762851 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -10,4 +10,4 @@ jobs: deploy: uses: ConductionNL/.github/.github/workflows/documentation.yml@main with: - cname: openregister.conduction.nl + cname: openregisters.app diff --git a/.github/workflows/pull-request-lint-check.yaml b/.github/workflows/pull-request-lint-check.yaml deleted file mode 100644 index f2402987af..0000000000 --- a/.github/workflows/pull-request-lint-check.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: Lint Check - -on: - pull_request: - branches: - - development - - main - - beta - -jobs: - lint-check: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Install dependencies - run: npm ci - - - name: Linting - run: npm run lint diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml deleted file mode 100644 index b05a358c8d..0000000000 --- a/.github/workflows/release-beta.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Beta Release - -on: - push: - branches: [beta] - -jobs: - release: - uses: ConductionNL/.github/.github/workflows/release-beta.yml@main - with: - app-name: openregister - verify-vendor-deps: true - vendor-check-paths: "openai-php/client/src,theodo-group/llphant/src" - secrets: inherit diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml deleted file mode 100644 index 660ce407f5..0000000000 --- a/.github/workflows/release-stable.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Stable Release - -on: - push: - branches: [main] - -jobs: - release: - uses: ConductionNL/.github/.github/workflows/release-stable.yml@main - with: - app-name: openregister - verify-vendor-deps: true - vendor-check-paths: "openai-php/client/src,theodo-group/llphant/src" - secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..5c22d6a155 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + branches: [main, beta, development] + +jobs: + unstable: + if: github.ref == 'refs/heads/development' + uses: ConductionNL/.github/.github/workflows/release.yml@main + with: + release-type: unstable + app-name: openregister + verify-vendor-deps: true + vendor-check-paths: 'openai-php/client/src,theodo-group/llphant/src' + secrets: inherit + + beta: + if: github.ref == 'refs/heads/beta' + uses: ConductionNL/.github/.github/workflows/release.yml@main + with: + release-type: beta + app-name: openregister + verify-vendor-deps: true + vendor-check-paths: 'openai-php/client/src,theodo-group/llphant/src' + secrets: inherit + + stable: + if: github.ref == 'refs/heads/main' + uses: ConductionNL/.github/.github/workflows/release.yml@main + with: + release-type: stable + app-name: openregister + verify-vendor-deps: true + vendor-check-paths: 'openai-php/client/src,theodo-group/llphant/src' + secrets: inherit + diff --git a/.gitignore b/.gitignore index a00625164e..6c8596895f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,18 @@ phpqa_output.log **/implements * **/ALL * **/endpoints * +**/*Analysis* +!**/*Analysis*.php +**/*references* +!**/*references*.php +!**/*references*/ +**/*encoding* +!**/*encoding*.php +**/ter +**/clearCache* +**/update*Settings* +**/rebase* +**/setup* # Temporary test files that shouldn't be committed simple-solr-test.php @@ -59,6 +71,7 @@ test-solr-connection.php *.bak2 *.bak3 *.orig +*.backup_* phpqa/ @@ -84,3 +97,5 @@ bom-npm.cdx.json tests/e2e/.auth/ tests/e2e/playwright-report/ tests/e2e/test-results/ +tests/e2e/.mdm-seed.json +.hydra diff --git a/.hydra b/.hydra new file mode 120000 index 0000000000..dfc8e4e7a9 --- /dev/null +++ b/.hydra @@ -0,0 +1 @@ +/home/rubenlinde/nextcloud-docker-dev/workspace/server/apps-extra/hydra \ No newline at end of file diff --git a/.npmrc b/.npmrc index 3942d3489c..e4e8835b72 100644 --- a/.npmrc +++ b/.npmrc @@ -2,4 +2,4 @@ # 24h ago. Compromised first-party-Conduction packages are excluded via # Dependabot cooldown (.github/dependabot.yml); for fresh @conduction/* # releases, override per-install with `npm install --min-release-age=0`. -min-release-age=1 +min-release-age=0 diff --git a/.phpmd.result-cache.php b/.phpmd.result-cache.php index 1127da2c59..656a820920 100644 --- a/.phpmd.result-cache.php +++ b/.phpmd.result-cache.php @@ -53,22 +53,7149 @@ ), 'composer' => array ( - 'composer.json' => '9344ff817088abf4246a02786d2865beefb7e2ca', - 'composer.lock' => '4a07eaacb0bae3aa1b10486cf53a295fec205625', + 'composer.json' => '5f2652f9a902724f05809abd4e74b2b841fac158', + 'composer.lock' => '99144ecba6b7915c933fd00960fbef3e53c0e4cc', ), - 'phpVersion' => 80330, + 'phpVersion' => 80331, ), 'state' => array ( 'files' => array ( + 'lib/Cron/ConfigurationCheckJob.php' => + array ( + 'hash' => '0e599f24945a3f871991108bf34a145ac62d6d9c', + 'violations' => + array ( + ), + ), + 'lib/Cron/TransferCheckJob.php' => + array ( + 'hash' => 'bff7eeb3c3fdd65180bd41d5eb32992717114feb', + 'violations' => + array ( + ), + ), + 'lib/Cron/WebhookRetryJob.php' => + array ( + 'hash' => '335eb5d4b888eb6de37c93e6454ff8d1f8eadf51', + 'violations' => + array ( + ), + ), + 'lib/Cron/LogCleanUpTask.php' => + array ( + 'hash' => 'f2586a245360a7f76bbb7ebbd777acadaca6e632', + 'violations' => + array ( + ), + ), + 'lib/Cron/SyncConfigurationsJob.php' => + array ( + 'hash' => 'ddc83e3ead75a5d14caf01f1edf9f167cdc35027', + 'violations' => + array ( + ), + ), + 'lib/Cron/ArchivalRetentionTask.php' => + array ( + 'hash' => '699bff38761991dabf9d18c97645387a1e41ac9f', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/TenantDeprovisionJob.php' => + array ( + 'hash' => '6c1a836bc2e3e5f05d82f65160af21fa8a840cbe', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/TenantUsageSyncJob.php' => + array ( + 'hash' => '7d4a9278e4e87b9d0f6d11564f2c192e93128cd7', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/DestructionCheckJob.php' => + array ( + 'hash' => '6bd8b26d9f087aa5aa3f4c237f95f24447adc56c', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/DestructionExecutionJob.php' => + array ( + 'hash' => '395e7134e96d44198bb7fd88c5507af7a641b095', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/CacheWarmupJob.php' => + array ( + 'hash' => 'd6a2856d7d8066ac12f945dff99b85e06d3fd117', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/CronFileTextExtractionJob.php' => + array ( + 'hash' => 'edf3c6284d3af5ac547502ba55525a6ad719e484', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/NameCacheWarmupJob.php' => + array ( + 'hash' => '563e3cd970ecfc6162a24d26b250b88ee0756707', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/FileTextExtractionJob.php' => + array ( + 'hash' => 'e78f8fd217d4f7d1df35c91802ec0315e1aff619', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/SolrWarmupJob.php' => + array ( + 'hash' => '7f5918beb1001f376043e7f7f89da8da5cc5690c', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/TenantPurgeJob.php' => + array ( + 'hash' => '4112df99a9c86a562dad6c6b9da4c451c789795a', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/SolrNightlyWarmupJob.php' => + array ( + 'hash' => '9afe78c273817ca8ad27ca9ead27d8eca079935c', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/ExecutionHistoryCleanupJob.php' => + array ( + 'hash' => '6a316e4ed3534890d3555808a2ee7d25ea6e3428', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/ActionScheduleJob.php' => + array ( + 'hash' => '0f4b9be00896dfc9f5302734173c1acc9f8eabbd', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/WebhookDeliveryJob.php' => + array ( + 'hash' => 'e37853962569f1a6f61cc4b1ef34db67618a0a47', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/HookRetryJob.php' => + array ( + 'hash' => '13537583f8a50afd3c971270d31e360ce42e768d', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/ActionRetryJob.php' => + array ( + 'hash' => '31af9dcec7909639d0e45a962c4bcba14bca4691', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/ObjectTextExtractionJob.php' => + array ( + 'hash' => '90b269b8e2a32f5b66aa7c61a1b51cab158262e3', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/BulkLegalHoldJob.php' => + array ( + 'hash' => '132e8c08aa72304b62c3c37cada6ea2ad53763b4', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/ScheduledWorkflowJob.php' => + array ( + 'hash' => 'a953ea0e27037c3a5348a8754911d67a01fd306e', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/BlobMigrationJob.php' => + array ( + 'hash' => '8764748775d9e5c1f8da77ed1a07309ed43d317e', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/TransferExecutionJob.php' => + array ( + 'hash' => '01cd9c85e35f94ee1fff84229720a6da794060a9', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/BatchNotificationJob.php' => + array ( + 'hash' => '3a46cf552c6cdf611efd4341c8c1bd8f6d12b799', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/AvgRetentionJob.php' => + array ( + 'hash' => '58ef2d661f2f031c6a7d32f169ed714d407ee198', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/BackfillCalendarLinksJob.php' => + array ( + 'hash' => '81e1c1285c4894e685005749a54260c1097bef4a', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/RealtimeEventRetentionJob.php' => + array ( + 'hash' => '7e28a110b9f46516f485e30d5ef6c218c341cf7c', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/ScheduledNotificationJob.php' => + array ( + 'hash' => 'ef42803bcab0bade1f6a0171671066d196688788', + 'violations' => + array ( + ), + ), + 'lib/BackgroundJob/ReportRenderJob.php' => + array ( + 'hash' => '96569a4f0eee139285dc86e3e986c0d9c67932f1', + 'violations' => + array ( + ), + ), + 'lib/Controller/HealthController.php' => + array ( + 'hash' => 'b763f47321833d9acd088abcac64f0cda7c41608', + 'violations' => + array ( + ), + ), + 'lib/Controller/SourcesController.php' => + array ( + 'hash' => 'd12292e0bef76e2d379a387c95553ebd5a250eac', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Naming\\ShortVariable', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 149, + 'endLine' => 149, + 'description' => 'Avoid variables with short names like $s. Configured minimum length is 3.', + 'args' => + array ( + 0 => '$s', + 1 => 3, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/EmailsController.php' => + array ( + 'hash' => 'd13d1d3eef7d29a7b2f3b8bc7c515531237fa89e', + 'violations' => + array ( + ), + ), + 'lib/Controller/SettingsController.php' => + array ( + 'hash' => '5e2f17336770dab65039e331645b93a94c4c4512', + 'violations' => + array ( + ), + ), + 'lib/Controller/WorkflowEngineController.php' => + array ( + 'hash' => 'ecb57115108efe49a360d1603adb8d1e266e8b7e', + 'violations' => + array ( + ), + ), + 'lib/Controller/ChatController.php' => + array ( + 'hash' => '33c39bfe9ad8bc48bc6980c5cbd7a8e83468d6b7', + 'violations' => + array ( + ), + ), + 'lib/Controller/FileSearchController.php' => + array ( + 'hash' => 'd61131dad59969a9c337ba0c2c7d919ab90959ea', + 'violations' => + array ( + ), + ), + 'lib/Controller/UserSettingsController.php' => + array ( + 'hash' => 'de37d6260deac8d6d938e75945dba2a98b14ae3b', + 'violations' => + array ( + ), + ), + 'lib/Controller/DashboardController.php' => + array ( + 'hash' => '52cf5ece916bbd497be6f01bfbf7cb62ac6a5eec', + 'violations' => + array ( + ), + ), + 'lib/Controller/ApprovalController.php' => + array ( + 'hash' => 'a7a4b031d47ab0b0865f4c0af2f4ecd9af1d4aa6', + 'violations' => + array ( + ), + ), + 'lib/Controller/WorkflowExecutionController.php' => + array ( + 'hash' => '000e73a45547dc45919c00e50cf803cd88c2cad6', + 'violations' => + array ( + ), + ), + 'lib/Controller/FilesController.php' => + array ( + 'hash' => '679444581487576cd3aeae4979b193afff172c8b', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'FilesController', + 'methodName' => 'show', + 'functionName' => NULL, + 'beginLine' => 268, + 'endLine' => 336, + 'description' => 'The method show() has a Cyclomatic Complexity of 10. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'show', + 2 => 10, + 3 => 10, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/UserController.php' => + array ( + 'hash' => '75309119a01c895b387d5b1ac9f91fe609cd13c2', + 'violations' => + array ( + ), + ), + 'lib/Controller/TagsController.php' => + array ( + 'hash' => '1400db2b0a0644732aa6314e7a0b39e03bf55618', + 'violations' => + array ( + ), + ), + 'lib/Controller/UiController.php' => + array ( + 'hash' => '619b8290f3d1961413e7252fe2f273674ad16895', + 'violations' => + array ( + ), + ), + 'lib/Controller/OrganisationController.php' => + array ( + 'hash' => '6a1f45bfa1ad7a7a9764099734087d1d7eef68cc', + 'violations' => + array ( + ), + ), + 'lib/Controller/CalendarEventsController.php' => + array ( + 'hash' => '28f671460fa8f4112c657aa0724cd391a803a908', + 'violations' => + array ( + ), + ), + 'lib/Controller/RelationsController.php' => + array ( + 'hash' => '5a66ea831113b5701773c49ec8ed5fe26ad94017', + 'violations' => + array ( + ), + ), + 'lib/Controller/TmloController.php' => + array ( + 'hash' => '3b230619e9dcdabcd40395f4e6db860ece6fc93d', + 'violations' => + array ( + ), + ), + 'lib/Controller/ArchivalController.php' => + array ( + 'hash' => '61cc8f336a84bac908a6dbcf52f01e4ef83f8118', + 'violations' => + array ( + ), + ), + 'lib/Controller/TasksController.php' => + array ( + 'hash' => 'd87e7425af18e8eb5cc27b6f23b549883f02ddc9', + 'violations' => + array ( + ), + ), + 'lib/Controller/ConfigurationsController.php' => + array ( + 'hash' => '08259d8dadbdbe6900e7ff405b6fa883cfe7eacb', + 'violations' => + array ( + ), + ), + 'lib/Controller/ConfigurationController.php' => + array ( + 'hash' => 'afc382fd6dcdd2bad59db2b87e0fb9871f271b72', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongParameterList', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'ConfigurationController', + 'methodName' => '__construct', + 'functionName' => NULL, + 'beginLine' => 127, + 'endLine' => 149, + 'description' => 'The method __construct has 11 parameters. Consider reducing the number of parameters to less than 10.', + 'args' => + array ( + 0 => 'method', + 1 => '__construct', + 2 => 11, + 3 => 10, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'ConfigurationController', + 'methodName' => 'fetchConfigFromUrl', + 'functionName' => NULL, + 'beginLine' => 1190, + 'endLine' => 1261, + 'description' => 'The method fetchConfigFromUrl() has a Cyclomatic Complexity of 12. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'fetchConfigFromUrl', + 2 => 12, + 3 => 10, + ), + 'metric' => NULL, + ), + 2 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'ConfigurationController', + 'methodName' => 'fetchConfigFromUrl', + 'functionName' => NULL, + 'beginLine' => 1190, + 'endLine' => 1261, + 'description' => 'The method fetchConfigFromUrl() has an NPath complexity of 544. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'fetchConfigFromUrl', + 2 => '544', + 3 => 200, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/MappingsController.php' => + array ( + 'hash' => '7d8036ede51e148b8a12e850409e3fe2f8b92042', + 'violations' => + array ( + ), + ), + 'lib/Controller/ScheduledWorkflowController.php' => + array ( + 'hash' => '265e94ec7afad65b4799091d82737ddb55882e1e', + 'violations' => + array ( + ), + ), + 'lib/Controller/MigrationController.php' => + array ( + 'hash' => '280e49ac0f84018647ade88f4a59fcfef3ccece4', + 'violations' => + array ( + ), + ), + 'lib/Controller/McpController.php' => + array ( + 'hash' => '356563135d99fe56853d7d9c1884a881acecf3f3', + 'violations' => + array ( + ), + ), + 'lib/Controller/TablesController.php' => + array ( + 'hash' => 'cb9ad6ffd25ea26c88e31e97367a39e7ddbc06f2', + 'violations' => + array ( + ), + ), + 'lib/Controller/EndpointsController.php' => + array ( + 'hash' => 'bda1d2af16443b4adbac68d4d8fd633ff90202ab', + 'violations' => + array ( + ), + ), + 'lib/Controller/ViewsController.php' => + array ( + 'hash' => 'c3f99eca30225378a367278e853ae27232f9f55c', + 'violations' => + array ( + ), + ), 'lib/Controller/AuditTrailController.php' => array ( - 'hash' => 'bf46862ef4e83c7932d879f676f6d3402e28be42', + 'hash' => 'bf46862ef4e83c7932d879f676f6d3402e28be42', + 'violations' => + array ( + ), + ), + 'lib/Controller/NamesController.php' => + array ( + 'hash' => '9c42d95dbe7761b2c018378a26835619e42d7e7b', + 'violations' => + array ( + ), + ), + 'lib/Controller/McpServerController.php' => + array ( + 'hash' => '3a75527a87b5c48da04d726dcef8b7d180e52bc9', + 'violations' => + array ( + ), + ), + 'lib/Controller/TransferController.php' => + array ( + 'hash' => '289e6ac38c23cdf2d532db5c6007f97b16e26302', + 'violations' => + array ( + ), + ), + 'lib/Controller/OasController.php' => + array ( + 'hash' => 'd463381579928ca293288752f42f1bff3e900aaf', + 'violations' => + array ( + ), + ), + 'lib/Controller/HeartbeatController.php' => + array ( + 'hash' => 'fa32c0e759e1cc462e70c38f77d467478fe50aee', + 'violations' => + array ( + ), + ), + 'lib/Controller/SolrController.php' => + array ( + 'hash' => 'fc916f9901849527fe4fc4e894b9c53d9560a03e', + 'violations' => + array ( + ), + ), + 'lib/Controller/SearchTrailController.php' => + array ( + 'hash' => '42ad8f30b05037a79c1bce22d3d3077b4f86514b', + 'violations' => + array ( + ), + ), + 'lib/Controller/BulkController.php' => + array ( + 'hash' => '66dc974eab4724a8448818c5c71c6916003cde1d', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\CouplingBetweenObjects', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'BulkController', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 52, + 'endLine' => 695, + 'description' => 'The class BulkController has a coupling between objects value of 16. Consider to reduce the number of dependencies under 13.', + 'args' => + array ( + 0 => 'BulkController', + 1 => 16, + 2 => 13, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'BulkController', + 'methodName' => 'save', + 'functionName' => NULL, + 'beginLine' => 331, + 'endLine' => 429, + 'description' => 'The method save() has a Cyclomatic Complexity of 10. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'save', + 2 => 10, + 3 => 10, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/GraphQLSubscriptionController.php' => + array ( + 'hash' => '944a57fac75612f7156768e6bf6d4a9deffa15a8', + 'violations' => + array ( + ), + ), + 'lib/Controller/GdprEntitiesController.php' => + array ( + 'hash' => '41d04d2ee2847cb016356ebdde2f4f311bb18ce9', + 'violations' => + array ( + ), + ), + 'lib/Controller/RetentionController.php' => + array ( + 'hash' => '092afa96c8e4f2fdfe687cfeaa1e6e0e277ad0a1', + 'violations' => + array ( + ), + ), + 'lib/Controller/SchemasController.php' => + array ( + 'hash' => '7e966f828473a0bb2fdbddd7a846aad725d2d223', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 850, + 'endLine' => 857, + 'description' => 'The method upload uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'upload', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/ObjectsController.php' => + array ( + 'hash' => '62048c2c52ea38192f085f635e2fb98896aa7c57', + 'violations' => + array ( + ), + ), + 'lib/Controller/DeletedController.php' => + array ( + 'hash' => 'a6b834b950776b2f7f935cfda175d8a0baa015ca', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\WeightedMethodCount', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'DeletedController', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 52, + 'endLine' => 781, + 'description' => 'The class DeletedController has an overall complexity of 59 which is very high. The configured complexity threshold is 50.', + 'args' => + array ( + 0 => 'DeletedController', + 1 => 59, + 2 => 50, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\CouplingBetweenObjects', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'DeletedController', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 52, + 'endLine' => 781, + 'description' => 'The class DeletedController has a coupling between objects value of 14. Consider to reduce the number of dependencies under 13.', + 'args' => + array ( + 0 => 'DeletedController', + 1 => 14, + 2 => 13, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/FileTextController.php' => + array ( + 'hash' => '12d179d2caad31c6fa083543311ab882ddd42779', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\CouplingBetweenObjects', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'FileTextController', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 58, + 'endLine' => 855, + 'description' => 'The class FileTextController has a coupling between objects value of 15. Consider to reduce the number of dependencies under 13.', + 'args' => + array ( + 0 => 'FileTextController', + 1 => 15, + 2 => 13, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongParameterList', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'FileTextController', + 'methodName' => '__construct', + 'functionName' => NULL, + 'beginLine' => 74, + 'endLine' => 87, + 'description' => 'The method __construct has 10 parameters. Consider reducing the number of parameters to less than 10.', + 'args' => + array ( + 0 => 'method', + 1 => '__construct', + 2 => 10, + 3 => 10, + ), + 'metric' => NULL, + ), + 2 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'FileTextController', + 'methodName' => 'addManualEntity', + 'functionName' => NULL, + 'beginLine' => 659, + 'endLine' => 750, + 'description' => 'The method addManualEntity() has a Cyclomatic Complexity of 12. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'addManualEntity', + 2 => 12, + 3 => 10, + ), + 'metric' => NULL, + ), + 3 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'FileTextController', + 'methodName' => 'addManualEntity', + 'functionName' => NULL, + 'beginLine' => 659, + 'endLine' => 750, + 'description' => 'The method addManualEntity() has an NPath complexity of 1152. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'addManualEntity', + 2 => '1152', + 3 => 200, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/ContactsController.php' => + array ( + 'hash' => 'cd39f2a0fd0b890d9ff5c2ca25666e395951c29c', + 'violations' => + array ( + ), + ), + 'lib/Controller/RegistersController.php' => + array ( + 'hash' => 'd452dd2babb30859aa6c068489119075dce75dfa', + 'violations' => + array ( + ), + ), + 'lib/Controller/NotesController.php' => + array ( + 'hash' => '68a43f138076f88cd09a7660264fa492b1ece8d3', + 'violations' => + array ( + ), + ), + 'lib/Controller/ConversationController.php' => + array ( + 'hash' => '34e1bd0cb4df6237e77414bc9711366454c505b6', + 'violations' => + array ( + ), + ), + 'lib/Controller/WebhooksController.php' => + array ( + 'hash' => 'ca2e3894d63227f422537a216b572939c510222e', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'WebhooksController', + 'methodName' => 'test', + 'functionName' => NULL, + 'beginLine' => 599, + 'endLine' => 709, + 'description' => 'The method test() has a Cyclomatic Complexity of 10. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'test', + 2 => 10, + 3 => 10, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/RevertController.php' => + array ( + 'hash' => '9547f9c2de7f703b3d33b5da8d8c4a910e7c48bb', + 'violations' => + array ( + ), + ), + 'lib/Controller/MetricsController.php' => + array ( + 'hash' => '038ddac0364fc4680722165595090d913209894a', + 'violations' => + array ( + ), + ), + 'lib/Controller/FileSidebarController.php' => + array ( + 'hash' => 'f186d9f22661f684c491f31ea6fbd757e51c5eed', + 'violations' => + array ( + ), + ), + 'lib/Controller/AgentsController.php' => + array ( + 'hash' => '87e5681e6ba29f346dd2f6839c65ae8853c982a9', + 'violations' => + array ( + ), + ), + 'lib/Controller/GraphQLController.php' => + array ( + 'hash' => '4bcece855e804f77e4ec8281cdf2cfcb0efd7721', + 'violations' => + array ( + ), + ), + 'lib/Controller/ConsumersController.php' => + array ( + 'hash' => '88f0328d89cecb2931607b81c643f4dc694cc4df', + 'violations' => + array ( + ), + ), + 'lib/Controller/DeckController.php' => + array ( + 'hash' => '8917a8b47ef7d81981f192c2a7ad676172e2c82d', + 'violations' => + array ( + ), + ), + 'lib/Controller/FileExtractionController.php' => + array ( + 'hash' => '9b0506c19a813e8aa0497dc4e1c3b610e89d867a', + 'violations' => + array ( + ), + ), + 'lib/Controller/ActionsController.php' => + array ( + 'hash' => '367d2de0d5523eb1c5afd2dbad8e6f1f8a1259a8', + 'violations' => + array ( + ), + ), + 'lib/Controller/LinkedEntityController.php' => + array ( + 'hash' => 'ccce2da871c19b7e9b049fd8e26f30f831f5dd31', + 'violations' => + array ( + ), + ), + 'lib/Controller/ApplicationsController.php' => + array ( + 'hash' => '940de81c0cd873e3a2dd81bf04aef58938d62b0f', + 'violations' => + array ( + ), + ), + 'lib/Controller/SearchController.php' => + array ( + 'hash' => '2d92cd54f58ba4b4971ac5e12916cc2af417dc42', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/SolrManagementController.php' => + array ( + 'hash' => '482e96d10563f342ceeb43ff762633cd0cca0cc2', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/N8nSettingsController.php' => + array ( + 'hash' => 'adc3a2f98d0dde6a6fb33ef32d97fff81168443f', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/ConfigurationSettingsController.php' => + array ( + 'hash' => '662a67e8243a294c894742e3f3b6b94e886f177e', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/EdepotSettingsController.php' => + array ( + 'hash' => '92c88a6b593db61bbf4883786c165f6520324bd7', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/ValidationSettingsController.php' => + array ( + 'hash' => '376eea1ed6bc24105d7f28fb9669dc374a0c9a14', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/FileSettingsController.php' => + array ( + 'hash' => 'c91f7904636f74b6b9a02a002d858911d0c27613', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/CacheSettingsController.php' => + array ( + 'hash' => '3328ee19c9ec8d2a42e5bfd48c638b9fd9f0590f', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/SolrSettingsController.php' => + array ( + 'hash' => '588975d1eca57ef4c40f0619c67aa6bdd0f7f0c9', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/VectorSettingsController.php' => + array ( + 'hash' => '79dd2482207ec8e64f5b80027c9243b497de0da2', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/SecuritySettingsController.php' => + array ( + 'hash' => '0450ab4436c5fddd0cf3dd14ea5c48ec8c693eda', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/LlmSettingsController.php' => + array ( + 'hash' => '7748ba89f939751f89c642239d5b44f0d75d4524', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/ApiTokenSettingsController.php' => + array ( + 'hash' => '285080992ad6e637a7b52bbfbf343cd3df8f3b98', + 'violations' => + array ( + ), + ), + 'lib/Controller/Settings/SolrOperationsController.php' => + array ( + 'hash' => '48c018d28f5c7254a51e3d28a4f1d0e761c8f598', + 'violations' => + array ( + ), + ), + 'lib/Controller/PollLinksController.php' => + array ( + 'hash' => 'f4dde708c405a39a64006b6d075eb857d789ffb6', + 'violations' => + array ( + ), + ), + 'lib/Controller/TranslationController.php' => + array ( + 'hash' => 'da3f4c29e5f66555668c4b42887fbec74d92e64d', + 'violations' => + array ( + ), + ), + 'lib/Controller/TalkLinksController.php' => + array ( + 'hash' => '70f0ea5f2783025e842c6740b1d79c5608c293f5', + 'violations' => + array ( + ), + ), + 'lib/Controller/AggregationController.php' => + array ( + 'hash' => '0b8faf893863be2933dc89f6c95439dfd0436cdf', + 'violations' => + array ( + ), + ), + 'lib/Controller/NotificationHistoryController.php' => + array ( + 'hash' => 'acbeab2cf3f79ccd895a5b18b58cb94deeba12da', + 'violations' => + array ( + ), + ), + 'lib/Controller/CollectiveLinksController.php' => + array ( + 'hash' => '65dcd1f3003c7103fa51df6f70ba696896daf371', + 'violations' => + array ( + ), + ), + 'lib/Controller/EmailLinksController.php' => + array ( + 'hash' => '4245ad3d9d8c78a2c13e3c98e56cce42a4af8dca', + 'violations' => + array ( + ), + ), + 'lib/Controller/VerwerkingsactiviteitenController.php' => + array ( + 'hash' => 'ddcfb45ac22a5a92b853acc343be901520d4011d', + 'violations' => + array ( + ), + ), + 'lib/Controller/MapLinksController.php' => + array ( + 'hash' => '80540573dd0e023c13133ae522064789e5981b45', + 'violations' => + array ( + ), + ), + 'lib/Controller/BookmarkLinksController.php' => + array ( + 'hash' => '0dc78f9624151bae6f0048921cbff17aac783a2e', + 'violations' => + array ( + ), + ), + 'lib/Controller/TransitionController.php' => + array ( + 'hash' => 'f800f9ed898642230092b7053580506bd22716a2', + 'violations' => + array ( + ), + ), + 'lib/Controller/ReportsController.php' => + array ( + 'hash' => 'e7591f3938de31f9e564b561b2e8edb5c21e323b', + 'violations' => + array ( + ), + ), + 'lib/Controller/PreferencesController.php' => + array ( + 'hash' => '01d3ccb0f50b779d04f1d08df05681b39445d734', + 'violations' => + array ( + ), + ), + 'lib/Controller/PhotoLinksController.php' => + array ( + 'hash' => 'b46baed36b22596ea4d6e3c6d8854a5eee902097', + 'violations' => + array ( + ), + ), + 'lib/Controller/OpenProjectLinksController.php' => + array ( + 'hash' => 'f2c49971ac5ccdf962a2a4ec2f40a5bd3720dee1', + 'violations' => + array ( + ), + ), + 'lib/Controller/ObjectIntegrationsController.php' => + array ( + 'hash' => '8cc452b93b436ddcc7548c13f6f197091da5678a', + 'violations' => + array ( + ), + ), + 'lib/Controller/XwikiLinksController.php' => + array ( + 'hash' => '444148041e523572d13c5c1d0ca6a2f6b17b5b83', + 'violations' => + array ( + ), + ), + 'lib/Controller/ActivityLinksController.php' => + array ( + 'hash' => '9ef1d001eb6da4bde7c9660e888a867dec714019', + 'violations' => + array ( + ), + ), + 'lib/Controller/NotificationPreferencesController.php' => + array ( + 'hash' => '94ff54f0f5cf31708a64d40d60d297bab70da14a', + 'violations' => + array ( + ), + ), + 'lib/Controller/ChatStreamController.php' => + array ( + 'hash' => '338df56099da6b499f8d5a4468af2e072c9b27ef', + 'violations' => + array ( + ), + ), + 'lib/Controller/ChatHealthController.php' => + array ( + 'hash' => '9194d82b8bbdc1d6b39cd9b7b271fcac98d47fe1', + 'violations' => + array ( + ), + ), + 'lib/Controller/TimeTrackerLinksController.php' => + array ( + 'hash' => '1df3cfa4018ccba2be0c8ec8e19ef4c3489fa148', + 'violations' => + array ( + ), + ), + 'lib/Controller/NotificationSubscriptionsController.php' => + array ( + 'hash' => 'b0eb26c46d293c866c75899cb81db870af783e93', + 'violations' => + array ( + ), + ), + 'lib/Controller/ShareLinksController.php' => + array ( + 'hash' => '4c9c7dccb6abf56c3c791f575df06d4e1bf25ff0', + 'violations' => + array ( + ), + ), + 'lib/Controller/FlowLinksController.php' => + array ( + 'hash' => 'b6b003ed74288cf72d4c5b9f27fd406be0fe683b', + 'violations' => + array ( + ), + ), + 'lib/Controller/GitHubIssuesController.php' => + array ( + 'hash' => 'ab210bf4f995cb5efeee831fdba0dbab1e6e200c', + 'violations' => + array ( + ), + ), + 'lib/Controller/UrnController.php' => + array ( + 'hash' => '8680b57c06c3723b3d231c5f58396b487340f349', + 'violations' => + array ( + ), + ), + 'lib/Controller/DeckLinksController.php' => + array ( + 'hash' => '992c686b08148fc616b2382d774f4890c69d7d8c', + 'violations' => + array ( + ), + ), + 'lib/Controller/ScopesController.php' => + array ( + 'hash' => '05d829631263ed3abe4343049344039bf09bde55', + 'violations' => + array ( + ), + ), + 'lib/Controller/RealtimeController.php' => + array ( + 'hash' => '7a813b33078c9d2875ecd1e0d440b24ae92e3f86', + 'violations' => + array ( + ), + ), + 'lib/Controller/IntegrationsController.php' => + array ( + 'hash' => '654900dd7cd0bd1fff944f4ed7689f6749127bcd', + 'violations' => + array ( + ), + ), + 'lib/Controller/EntityRelationsController.php' => + array ( + 'hash' => 'a33c56c3a4b8acbcecf30dc932a114a072119761', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\CouplingBetweenObjects', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'EntityRelationsController', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 63, + 'endLine' => 452, + 'description' => 'The class EntityRelationsController has a coupling between objects value of 17. Consider to reduce the number of dependencies under 13.', + 'args' => + array ( + 0 => 'EntityRelationsController', + 1 => 17, + 2 => 13, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongParameterList', + 'namespaceName' => 'OCA\\OpenRegister\\Controller', + 'className' => 'EntityRelationsController', + 'methodName' => '__construct', + 'functionName' => NULL, + 'beginLine' => 80, + 'endLine' => 94, + 'description' => 'The method __construct has 11 parameters. Consider reducing the number of parameters to less than 10.', + 'args' => + array ( + 0 => 'method', + 1 => '__construct', + 2 => 11, + 3 => 10, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Controller/DsarController.php' => + array ( + 'hash' => '508a2072cfdb375fcc6da820d74e436b00e84b27', + 'violations' => + array ( + ), + ), + 'lib/Controller/ManifestController.php' => + array ( + 'hash' => 'd2b39754f5379fbfa6b352c644c73a55cdefc5f3', + 'violations' => + array ( + ), + ), + 'lib/Controller/AnalyticsLinksController.php' => + array ( + 'hash' => '2a64e82cc9229fe1ed107b401355aa67c8ee440e', + 'violations' => + array ( + ), + ), + 'lib/Controller/CospendLinksController.php' => + array ( + 'hash' => '95712664b612a967034c8e0504bbc59501e0c702', + 'violations' => + array ( + ), + ), + 'lib/Controller/FormLinksController.php' => + array ( + 'hash' => '31f544d45c49aefa6f9c1671df94591f2eb84a0d', + 'violations' => + array ( + ), + ), + 'lib/Middleware/TenantQuotaMiddleware.php' => + array ( + 'hash' => '5357f229d53eae19f44a3c652b19a1b4b1c6ddf3', + 'violations' => + array ( + ), + ), + 'lib/Middleware/TenantStatusException.php' => + array ( + 'hash' => '79639ade9c3a946dc9f42ac6ad3f480edf8fd604', + 'violations' => + array ( + ), + ), + 'lib/Middleware/LanguageMiddleware.php' => + array ( + 'hash' => 'ec31aa9bc744c1be6312f08f6f045716a9f9abff', + 'violations' => + array ( + ), + ), + 'lib/Middleware/TenantQuotaExceededException.php' => + array ( + 'hash' => '2f92929a740013aa8f32b13915c2ed8afae4690e', + 'violations' => + array ( + ), + ), + 'lib/Middleware/AuthRateLimitExceededException.php' => + array ( + 'hash' => '7c31aa89c24b809aaf540057da7c518b378111ca', + 'violations' => + array ( + ), + ), + 'lib/Middleware/OasValidationFailureException.php' => + array ( + 'hash' => 'eca7449960267af9a58286872aac7aefb9ba84aa', + 'violations' => + array ( + ), + ), + 'lib/Middleware/RateLimitMiddleware.php' => + array ( + 'hash' => '98edfa0801bc1523149c415fcf5a1b8b34b43193', + 'violations' => + array ( + ), + ), + 'lib/Middleware/OasValidationMiddleware.php' => + array ( + 'hash' => 'f0790d5decf6fe110f05f937cf14546e88d9e8e6', + 'violations' => + array ( + ), + ), + 'lib/Repair/RegisterRiskLevelMetadata.php' => + array ( + 'hash' => '9e7579333a225f287a2039917939a1ab0f2ed849', + 'violations' => + array ( + ), + ), + 'lib/Repair/MigrateNotificationSubscriptionsToUserConfig.php' => + array ( + 'hash' => 'b3e30d569c86601f93a2359498695749d5c2b20d', + 'violations' => + array ( + ), + ), + 'lib/Repair/LogDanglingLinkedTypes.php' => + array ( + 'hash' => '3f54e8e988cbb83994879256bf18d28edde92512', + 'violations' => + array ( + ), + ), + 'lib/Event/ViewDeletedEvent.php' => + array ( + 'hash' => 'd5e377fa01ff76d75fb355301dcae4b4412502a2', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectLockedEvent.php' => + array ( + 'hash' => '3862cf30598fd2a3d3e8abfbbf2f0500222fab2e', + 'violations' => + array ( + ), + ), + 'lib/Event/ApplicationUpdatedEvent.php' => + array ( + 'hash' => 'ce6bb0d2c2606a31f81bff5a2d81759c06f29ac7', + 'violations' => + array ( + ), + ), + 'lib/Event/OrganisationCreatedEvent.php' => + array ( + 'hash' => '508123c10dd22bb6737f0302e8ae9af19ca7a1df', + 'violations' => + array ( + ), + ), + 'lib/Event/ActionCreatedEvent.php' => + array ( + 'hash' => 'fd076d6d9ec63ec1aed7896e4c76f2cb91bee275', + 'violations' => + array ( + ), + ), + 'lib/Event/OrganisationDeletedEvent.php' => + array ( + 'hash' => '6827a84ed9663b3188a31d9b10cb10a06a4ec39d', + 'violations' => + array ( + ), + ), + 'lib/Event/UserProfileUpdatedEvent.php' => + array ( + 'hash' => 'c2c6f83c907847f6c7619c5539d2e57202e83858', + 'violations' => + array ( + ), + ), + 'lib/Event/FileLockedEvent.php' => + array ( + 'hash' => '9fee05836587da9f4a493739b452caf68a611b24', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectCreatingEvent.php' => + array ( + 'hash' => 'b30ad3d8fe0a07e670c007b591c3524dc24fa445', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectUnlockedEvent.php' => + array ( + 'hash' => '06c3ac1c13ff898125716ffe4cecba5e982b0276', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectDeletedEvent.php' => + array ( + 'hash' => '462815cac98e23c3a872827a5288c1941a64eb8e', + 'violations' => + array ( + ), + ), + 'lib/Event/ToolRegistrationEvent.php' => + array ( + 'hash' => 'a01703b4b460fb4b8b3fbd932d9c9b7fe6a1007f', + 'violations' => + array ( + ), + ), + 'lib/Event/ConversationUpdatedEvent.php' => + array ( + 'hash' => '896c8fd0a88adb62246a0be35cd252e14f5f7e2e', + 'violations' => + array ( + ), + ), + 'lib/Event/SourceCreatedEvent.php' => + array ( + 'hash' => '5347d9407bcc5b73c9d64baa9b6d328820f806bc', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectUpdatingEvent.php' => + array ( + 'hash' => 'cb6db5ea99fed41e418acbaf367680e293cc5008', + 'violations' => + array ( + ), + ), + 'lib/Event/ApplicationCreatedEvent.php' => + array ( + 'hash' => 'bb4be808d6e3724f6152b9d980454e35989dc802', + 'violations' => + array ( + ), + ), + 'lib/Event/SchemaUpdatedEvent.php' => + array ( + 'hash' => '848ccc9ada5414590d9047350af7ac021dd82081', + 'violations' => + array ( + ), + ), + 'lib/Event/ConfigurationDeletedEvent.php' => + array ( + 'hash' => '3918a525bab76b299e59b12da54b58eed8c62b41', + 'violations' => + array ( + ), + ), + 'lib/Event/ConversationDeletedEvent.php' => + array ( + 'hash' => '51dd19078aa02af4e95a41500580e487ac49ffa0', + 'violations' => + array ( + ), + ), + 'lib/Event/SourceDeletedEvent.php' => + array ( + 'hash' => 'ccb8b486a2a01b0aca81c8b5cad6b805763c4a03', + 'violations' => + array ( + ), + ), + 'lib/Event/ApplicationDeletedEvent.php' => + array ( + 'hash' => '2461c054296c1e88de7c8b7cc87972079004ac9e', + 'violations' => + array ( + ), + ), + 'lib/Event/FileRenamedEvent.php' => + array ( + 'hash' => 'dc6c4e94c1d7808453bd877cf1927cc226d19a79', + 'violations' => + array ( + ), + ), + 'lib/Event/ConfigurationCreatedEvent.php' => + array ( + 'hash' => '0c372a5bcccc7627d3ac992467603c37ba5df7ca', + 'violations' => + array ( + ), + ), + 'lib/Event/SchemaCreatedEvent.php' => + array ( + 'hash' => '3b1cde59e1c4a018d08931f587c16ab01a9a0150', + 'violations' => + array ( + ), + ), + 'lib/Event/RegisterCreatedEvent.php' => + array ( + 'hash' => '632bc2a1728ed767cf8aadfc960463a668b9289d', + 'violations' => + array ( + ), + ), + 'lib/Event/ConversationCreatedEvent.php' => + array ( + 'hash' => '2505f415413e96c835d6d59729ba945ebd5ce511', + 'violations' => + array ( + ), + ), + 'lib/Event/FileVersionRestoredEvent.php' => + array ( + 'hash' => '427b0f66dd5999723820038b47686dc4e352acc1', + 'violations' => + array ( + ), + ), + 'lib/Event/AgentCreatedEvent.php' => + array ( + 'hash' => 'aa07e5cbe1f153183899b35e9a373ae9dbff70b5', + 'violations' => + array ( + ), + ), + 'lib/Event/SchemaDeletedEvent.php' => + array ( + 'hash' => '2814b2f5231b0f4a5ebaa53228c5509ee9b63384', + 'violations' => + array ( + ), + ), + 'lib/Event/AgentUpdatedEvent.php' => + array ( + 'hash' => '6f9d893e03add4c2823591fa8d1eb228a1be101a', + 'violations' => + array ( + ), + ), + 'lib/Event/FileUnlockedEvent.php' => + array ( + 'hash' => 'f7e95211172edc323172341cb4254e2e28d8f7ae', + 'violations' => + array ( + ), + ), + 'lib/Event/SourceUpdatedEvent.php' => + array ( + 'hash' => 'df9a90780ba440da9fae8901e263dfbba391c39c', + 'violations' => + array ( + ), + ), + 'lib/Event/ConfigurationUpdatedEvent.php' => + array ( + 'hash' => 'c1f0fc59ee531acaf4d19bf8c74c8a11d38d7027', + 'violations' => + array ( + ), + ), + 'lib/Event/RegisterUpdatedEvent.php' => + array ( + 'hash' => '12b033f6ebe1cc15f12da5987980b6977fa87ccf', + 'violations' => + array ( + ), + ), + 'lib/Event/ViewCreatedEvent.php' => + array ( + 'hash' => '72bac38d8074512ebf66c42836d8575c4a05985e', + 'violations' => + array ( + ), + ), + 'lib/Event/ViewUpdatedEvent.php' => + array ( + 'hash' => '197e6e4e5bc27c7c835e26cd52b786b0334a8e33', + 'violations' => + array ( + ), + ), + 'lib/Event/ActionUpdatedEvent.php' => + array ( + 'hash' => '2ee82698945651f4e8886d8327ecc69658cadf6e', + 'violations' => + array ( + ), + ), + 'lib/Event/AgentDeletedEvent.php' => + array ( + 'hash' => 'd4586329732b0940317b29d67adee7e5354cef06', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectRevertedEvent.php' => + array ( + 'hash' => '9d70e66064cb6c2ce730e36bc10d5a908d32a28e', + 'violations' => + array ( + ), + ), + 'lib/Event/FileCopiedEvent.php' => + array ( + 'hash' => 'a97347f528ed1762a28c15827237306f447eab93', + 'violations' => + array ( + ), + ), + 'lib/Event/DeepLinkRegistrationEvent.php' => + array ( + 'hash' => 'fb5e8582bbb283e404e250f3a8d3dc150bf36363', + 'violations' => + array ( + ), + ), + 'lib/Event/FileMovedEvent.php' => + array ( + 'hash' => '8d1ccbed06da47706f00d63078f557bbe223fe54', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectCreatedEvent.php' => + array ( + 'hash' => '3b6e40734febe12dc5d39d0832880269ed70366b', + 'violations' => + array ( + ), + ), + 'lib/Event/ActionDeletedEvent.php' => + array ( + 'hash' => '06113dfe9902cdaa5ef68d6b8ea6bcabcf585c02', + 'violations' => + array ( + ), + ), + 'lib/Event/OrganisationUpdatedEvent.php' => + array ( + 'hash' => 'd5d287e351ac1eebaa9b1544060a558cc29ddc64', + 'violations' => + array ( + ), + ), + 'lib/Event/RegisterDeletedEvent.php' => + array ( + 'hash' => '4b05b5ab5dad0d9d25dc47647028df123b4c5abb', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectUpdatedEvent.php' => + array ( + 'hash' => '02d0d7ec68d79ef830a77e85ee5ac9755fbce673', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectDeletingEvent.php' => + array ( + 'hash' => '6026beb55bf7bf49f09784e5950cdcdaf99c9118', + 'violations' => + array ( + ), + ), + 'lib/Event/ReferenceValidatedEvent.php' => + array ( + 'hash' => '0b8dd8d929cb3a435fe2c8d8900efb865f3d859f', + 'violations' => + array ( + ), + ), + 'lib/Event/CustomScopeEvaluatingEvent.php' => + array ( + 'hash' => '4537d46b07df7e565ce00fca98bf7758a7d529a2', + 'violations' => + array ( + ), + ), + 'lib/Event/CustomScopeEvaluatedEvent.php' => + array ( + 'hash' => '71ca673c732bc24f47295103c96b3913beb4aaf8', + 'violations' => + array ( + ), + ), + 'lib/Event/ObjectTransitionedEvent.php' => + array ( + 'hash' => 'e9efcfaeb96ac8d8ca4e9f94e3c8356fc37f1221', + 'violations' => + array ( + ), + ), + 'lib/Event/EntityRelationDecisionUpdatedEvent.php' => + array ( + 'hash' => '7f9cbbe396e76293fe216dbb81b487bf7d558cdf', + 'violations' => + array ( + ), + ), + 'lib/Event/ReferenceValidationFailedEvent.php' => + array ( + 'hash' => '1042e8add59ec301d5a85f784242fe07129303a9', + 'violations' => + array ( + ), + ), + 'lib/Formats/BsnFormat.php' => + array ( + 'hash' => '5d9a98cc0dbf8c61ba11c5b31e4719ae65125e1b', + 'violations' => + array ( + ), + ), + 'lib/Formats/SemVerFormat.php' => + array ( + 'hash' => '77a59f6917dce65bdc2bd64a9bd6a1d1604418ac', + 'violations' => + array ( + ), + ), + 'lib/Formats/Iso8601DateTimeFormat.php' => + array ( + 'hash' => '742b41144a682fbcf60c6c6fbe9ae8da017f2a81', + 'violations' => + array ( + ), + ), + 'lib/Formats/ExtendedFieldTypeValidator.php' => + array ( + 'hash' => 'e581cf1369be55e691a909b645ac915019fc5ed9', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 113, + 'endLine' => 115, + 'description' => 'The method validateColor uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'validateColor', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/AppInfo/Application.php' => + array ( + 'hash' => 'f85097de1cddd8be6ea75313877bd53ec9a027fb', + 'violations' => + array ( + ), + ), + 'lib/Dto/DeepLinkRegistration.php' => + array ( + 'hash' => '0705b09b9288dfc2ea672290a1c8c2fa821adcbe', + 'violations' => + array ( + ), + ), + 'lib/Dto/DeletionAnalysis.php' => + array ( + 'hash' => '96ebad4f07293b94b60709996c50e86721f122b0', + 'violations' => + array ( + ), + ), + 'lib/Service/EndpointService.php' => + array ( + 'hash' => 'e86f90a411535d939464122baf248010227d0018', + 'violations' => + array ( + ), + ), + 'lib/Service/RiskLevelService.php' => + array ( + 'hash' => 'b179c43131423ea1b1378181424b17c207e75e9e', + 'violations' => + array ( + ), + ), + 'lib/Service/LinkedEntityService.php' => + array ( + 'hash' => '00e9fca47dc9a747b619869659f9ec94878bb521', + 'violations' => + array ( + ), + ), + 'lib/Service/WebhookService.php' => + array ( + 'hash' => '1e0f8928f5ed97382e4184357d087d6f4e87ed6a', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongClass', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 60, + 'endLine' => 1405, + 'description' => 'The class WebhookService has 1346 lines of code. Current threshold is 1000. Avoid really long classes.', + 'args' => + array ( + 0 => 'WebhookService', + 1 => 1346, + 2 => 1000, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ErrorControlOperator', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'assertSafeWebhookUri', + 'functionName' => NULL, + 'beginLine' => 241, + 'endLine' => 357, + 'description' => 'Remove error control operator \'@\' on line 285.', + 'args' => + array ( + 0 => 285, + ), + 'metric' => NULL, + ), + 2 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'assertSafeWebhookUri', + 'functionName' => NULL, + 'beginLine' => 241, + 'endLine' => 357, + 'description' => 'The method assertSafeWebhookUri() has a Cyclomatic Complexity of 18. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'assertSafeWebhookUri', + 2 => 18, + 3 => 10, + ), + 'metric' => NULL, + ), + 3 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'assertSafeWebhookUri', + 'functionName' => NULL, + 'beginLine' => 241, + 'endLine' => 357, + 'description' => 'The method assertSafeWebhookUri() has an NPath complexity of 18432. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'assertSafeWebhookUri', + 2 => '18432', + 3 => 200, + ), + 'metric' => NULL, + ), + 4 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongMethod', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'assertSafeWebhookUri', + 'functionName' => NULL, + 'beginLine' => 241, + 'endLine' => 357, + 'description' => 'The method assertSafeWebhookUri() has 117 lines of code. Current threshold is set to 100. Avoid really long methods.', + 'args' => + array ( + 0 => 'method', + 1 => 'assertSafeWebhookUri', + 2 => 117, + 3 => 100, + ), + 'metric' => NULL, + ), + 5 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 245, + 'endLine' => 245, + 'description' => 'Missing class import via use statement (line \'245\', column \'23\').', + 'args' => + array ( + 0 => 245, + 1 => 23, + ), + 'metric' => NULL, + ), + 6 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 250, + 'endLine' => 250, + 'description' => 'Missing class import via use statement (line \'250\', column \'23\').', + 'args' => + array ( + 0 => 250, + 1 => 23, + ), + 'metric' => NULL, + ), + 7 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 257, + 'endLine' => 257, + 'description' => 'Missing class import via use statement (line \'257\', column \'23\').', + 'args' => + array ( + 0 => 257, + 1 => 23, + ), + 'metric' => NULL, + ), + 8 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 273, + 'endLine' => 273, + 'description' => 'Missing class import via use statement (line \'273\', column \'27\').', + 'args' => + array ( + 0 => 273, + 1 => 27, + ), + 'metric' => NULL, + ), + 9 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 295, + 'endLine' => 295, + 'description' => 'Missing class import via use statement (line \'295\', column \'31\').', + 'args' => + array ( + 0 => 295, + 1 => 31, + ), + 'metric' => NULL, + ), + 10 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 325, + 'endLine' => 325, + 'description' => 'Missing class import via use statement (line \'325\', column \'23\').', + 'args' => + array ( + 0 => 325, + 1 => 23, + ), + 'metric' => NULL, + ), + 11 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 332, + 'endLine' => 332, + 'description' => 'Missing class import via use statement (line \'332\', column \'23\').', + 'args' => + array ( + 0 => 332, + 1 => 23, + ), + 'metric' => NULL, + ), + 12 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 339, + 'endLine' => 339, + 'description' => 'Missing class import via use statement (line \'339\', column \'23\').', + 'args' => + array ( + 0 => 339, + 1 => 23, + ), + 'metric' => NULL, + ), + 13 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 346, + 'endLine' => 346, + 'description' => 'Missing class import via use statement (line \'346\', column \'23\').', + 'args' => + array ( + 0 => 346, + 1 => 23, + ), + 'metric' => NULL, + ), + 14 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 353, + 'endLine' => 353, + 'description' => 'Missing class import via use statement (line \'353\', column \'23\').', + 'args' => + array ( + 0 => 353, + 1 => 23, + ), + 'metric' => NULL, + ), + 15 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ErrorControlOperator', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'blockedIpv6Reason', + 'functionName' => NULL, + 'beginLine' => 381, + 'endLine' => 450, + 'description' => 'Remove error control operator \'@\' on line 383.', + 'args' => + array ( + 0 => 383, + ), + 'metric' => NULL, + ), + 16 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'blockedIpv6Reason', + 'functionName' => NULL, + 'beginLine' => 381, + 'endLine' => 450, + 'description' => 'The method blockedIpv6Reason() has an NPath complexity of 2880. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'blockedIpv6Reason', + 2 => '2880', + 3 => 200, + ), + 'metric' => NULL, + ), + 17 => + array ( + 'rule' => 'PHPMD\\Rule\\Naming\\ShortVariable', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 381, + 'endLine' => 381, + 'description' => 'Avoid variables with short names like $ip. Configured minimum length is 3.', + 'args' => + array ( + 0 => '$ip', + 1 => 3, + ), + 'metric' => NULL, + ), + 18 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ErrorControlOperator', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'isPrivateHost', + 'functionName' => NULL, + 'beginLine' => 466, + 'endLine' => 515, + 'description' => 'Remove error control operator \'@\' on line 484.', + 'args' => + array ( + 0 => 484, + ), + 'metric' => NULL, + ), + 19 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'isPrivateHost', + 'functionName' => NULL, + 'beginLine' => 466, + 'endLine' => 515, + 'description' => 'The method isPrivateHost() has a Cyclomatic Complexity of 14. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'isPrivateHost', + 2 => 14, + 3 => 10, + ), + 'metric' => NULL, + ), + 20 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'WebhookService', + 'methodName' => 'isPrivateHost', + 'functionName' => NULL, + 'beginLine' => 466, + 'endLine' => 515, + 'description' => 'The method isPrivateHost() has an NPath complexity of 320. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'isPrivateHost', + 2 => '320', + 3 => 200, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/MigrationService.php' => + array ( + 'hash' => 'f7a740c90a0321e4062eece34886f9d734180674', + 'violations' => + array ( + ), + ), + 'lib/Service/UserService.php' => + array ( + 'hash' => '53066096ac4346bd41a1fb9f8a17d3a29efd9c28', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 1172, + 'endLine' => 1174, + 'description' => 'The method setNotificationPreferences uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'setNotificationPreferences', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/Object/MergeHandler.php' => + array ( + 'hash' => 'd4ab0e70d8c440dadfe9e6b8d85d4f8c4731d5ae', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObject.php' => + array ( + 'hash' => 'c0bae7a10e848244420c33e36bb8fdab1d140acc', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/MigrationHandler.php' => + array ( + 'hash' => '925df3af233d629e309af8b3c13deb6d8e495aa4', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObjects/BulkRelationHandler.php' => + array ( + 'hash' => 'a4daff32e58f1ad2e52f5ad85310dc4a5d262502', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObjects/BulkValidationHandler.php' => + array ( + 'hash' => '42c8c39616d6acb39b983de8154e771e905679cd', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObjects/PreparationHandler.php' => + array ( + 'hash' => '0d620a1bbe4c9bf49ba528ce03543e4d32f802a1', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObjects/TransformationHandler.php' => + array ( + 'hash' => 'bd91a70c5f738b20b62493c5f4da7283571aab8e', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObjects/ChunkProcessingHandler.php' => + array ( + 'hash' => '966abfd5b00b8dfe8b36813c3b3c3aa03d6428ab', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/ValidationHandler.php' => + array ( + 'hash' => 'e82869ed6e02ffb2d24c4ba878660085e908634c', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/QueryHandler.php' => + array ( + 'hash' => '135217e008d4607d937dee5219c505d2bd2269ab', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/RelationHandler.php' => + array ( + 'hash' => '3f4d14380e13e0afff8dfeb322f2c5a34c61289d', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/MetadataHandler.php' => + array ( + 'hash' => 'afb2f966ad002cefec2a76367940870e336d7104', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SchemaTypeConverter.php' => + array ( + 'hash' => '6dad8b28f5003c35570b4158658534d22f25faae', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/RenderObject.php' => + array ( + 'hash' => '492b284b222c4d012ede67e2150dac72f8140a4e', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/UtilityHandler.php' => + array ( + 'hash' => '19eef62aa3147aeb741af166ae66fb9ce7c9c379', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SearchQueryHandler.php' => + array ( + 'hash' => '178dab6b7a8eb789c6417de29cf6134ffa8dac0b', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/CascadingHandler.php' => + array ( + 'hash' => 'cb321ce3f6880b49c86da7cf84c664e38712c148', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObject/FilePropertyHandler.php' => + array ( + 'hash' => '1c89bc0b2bed2491c091665858fb7af5ad2d8ce1', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObject/RelationCascadeHandler.php' => + array ( + 'hash' => 'e6db12faa849976f9395022fd9fb41ba57180789', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObject/MetadataHydrationHandler.php' => + array ( + 'hash' => 'b41d627cc62f371f76ee947cb18d709eeabeba85', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObject/ComputedFieldHandler.php' => + array ( + 'hash' => '6646d653f90d432e8157aaac635a939d9a759f11', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObject/LinkedEntityPropertyHandler.php' => + array ( + 'hash' => '2fbba7d9b82a208c33134bcd4c698558fbb90aaf', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/LinkedEntityEnricher.php' => + array ( + 'hash' => '8e928c38ab7f25b63e65b548a9f74d1149790e11', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/FacetHandler.php' => + array ( + 'hash' => '2846a95a2a612ad276a9d17c0d39d18fc5f2df9b', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/VectorizationHandler.php' => + array ( + 'hash' => '776982eff538cebd69b7864de1102c5ebf200fef', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/CrudHandler.php' => + array ( + 'hash' => 'b7569eedda1b67835af276cd7b3f7ae6005e2710', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/ValidateObject.php' => + array ( + 'hash' => '8a68ba085e5d6659de85fd1bed13c90632b26a04', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/RevertHandler.php' => + array ( + 'hash' => 'de00b8569c231003fad35e8b88f0e8751cb4448c', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/SaveObjects.php' => + array ( + 'hash' => '85e6d2f2447d4b437d05487859f39d68450c769c', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/DeleteObject.php' => + array ( + 'hash' => '8f24062751562dec33bd296f58c0a30ff6b47463', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 182, + 'endLine' => 184, + 'description' => 'The method delete uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'delete', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/Object/ExportHandler.php' => + array ( + 'hash' => '3a00e8991eb2ad48b4e77995534fb75833eefb4b', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/RelationshipOptimizationHandler.php' => + array ( + 'hash' => 'b2276f417384564b08c116ab31269f6c75f74450', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/LockHandler.php' => + array ( + 'hash' => '19ff27a0365a4d5d36bd63210a076d6ae0707813', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/TranslationHandler.php' => + array ( + 'hash' => 'c04c0043c593b0e7802580e26aaf045ae7ad9271', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/GetObject.php' => + array ( + 'hash' => '3024f85cd4cc28313216011938a80dedf88ea144', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/PerformanceHandler.php' => + array ( + 'hash' => 'f50258cefd878b93f3597c4319c4c841d18d2047', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/AuditHandler.php' => + array ( + 'hash' => 'd80c0d989b084ac81c41527125d0f9cbb0b1dd4e', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/PermissionHandler.php' => + array ( + 'hash' => '46391319fea466d6c7e0b5787c39b68391ba7253', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/ReferentialIntegrityService.php' => + array ( + 'hash' => 'fbf5f051311f46249077d91e78e97fed552373ef', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/DataManipulationHandler.php' => + array ( + 'hash' => '42601ed57821ebc4d394043a90c52c232ce83856', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/PerformanceOptimizationHandler.php' => + array ( + 'hash' => 'ea1776f009c235098287736ba41cab647d383850', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/CacheHandler.php' => + array ( + 'hash' => 'f4995680c8cf9736b90f7e674617418acac13b6d', + 'violations' => + array ( + ), + ), + 'lib/Service/Object/BatchOperationStatus.php' => + array ( + 'hash' => 'f206d2d890a704ca8c25b8913a0148869dedc2d0', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/UploadHandler.php' => + array ( + 'hash' => '7a3b75e92eefd0190ce347050fd0bb303537e97e', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/GitHubHandler.php' => + array ( + 'hash' => 'ad8d69221167dfa5ae7d765ad89953d17f91f1c4', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/FetchHandler.php' => + array ( + 'hash' => '93dbb05f57082396e6b758a0a1959a469c5382f9', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/GitLabHandler.php' => + array ( + 'hash' => '380500fd3f409ff9015af66f3406b2e5d4739139', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/ImportHandler.php' => + array ( + 'hash' => 'cb0ab2d5b4c1ee3e28caf44e0cc27ea70a059e2b', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/ExportHandler.php' => + array ( + 'hash' => '3899e3317c59b01fe1baea12a75593b021643e27', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/PreviewHandler.php' => + array ( + 'hash' => '3505e541c4c38078b82099780e791151562fe53f', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/CacheHandler.php' => + array ( + 'hash' => '7aa4b437d9004426288c73b153f4b2317838d6d8', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/RateLimiterService.php' => + array ( + 'hash' => '3b15ac8ef2cfc67456b7dc34e0b5ef01a2975be2', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/GitHubGuards.php' => + array ( + 'hash' => '823f1020894213a8d3004daacbd78d1f5b8f9e8a', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/AttributionFormatter.php' => + array ( + 'hash' => 'cfd0e6df58eba71b6ae1069352d307ca396ec2f8', + 'violations' => + array ( + ), + ), + 'lib/Service/Configuration/GitHubRequestValidator.php' => + array ( + 'hash' => '42f22aae8672ec1f5126d61a52ef24cd2fa5801d', + 'violations' => + array ( + ), + ), + 'lib/Service/VectorizationService.php' => + array ( + 'hash' => 'e502d2290c40eb906d406cae2376f637258ba57a', + 'violations' => + array ( + ), + ), + 'lib/Service/UploadService.php' => + array ( + 'hash' => '113ac5713c9258444124cb8dc8885e1005be5aae', + 'violations' => + array ( + ), + ), + 'lib/Service/DeepLinkRegistryService.php' => + array ( + 'hash' => 'e60e0dfb378127656fea0e7594fa92c108fdcbaa', + 'violations' => + array ( + ), + ), + 'lib/Service/Archival/ArchiefactiedatumCalculator.php' => + array ( + 'hash' => '0224f3efa2d5dfba7f9c5a40343c137cc45f1091', + 'violations' => + array ( + ), + ), + 'lib/Service/Archival/DestructionService.php' => + array ( + 'hash' => '0e51dd0dab59a469793b246c0dc82626b8b119cf', + 'violations' => + array ( + ), + ), + 'lib/Service/Archival/LegalHoldService.php' => + array ( + 'hash' => 'e8557d0d521c3320c40f4fc3a49f5fb939d43395', + 'violations' => + array ( + ), + ), + 'lib/Service/Archival/RetentionEvaluator.php' => + array ( + 'hash' => 'ad47baff5a488aa62ec292c37296c6a0b09adfcc', + 'violations' => + array ( + ), + ), + 'lib/Service/Archival/RetentionConditionEvaluator.php' => + array ( + 'hash' => '32ff06532ea75f23a55a48fe74a028ddc4669291', + 'violations' => + array ( + ), + ), + 'lib/Service/Archival/ArchivalAnnotationValidator.php' => + array ( + 'hash' => '9b6a4ca6d1848e67fd7d7e48457e4a737d5675f8', + 'violations' => + array ( + ), + ), + 'lib/Service/RetentionService.php' => + array ( + 'hash' => 'e97353d92d5e6180c7cf8b843cb4aaf3c6e09ccd', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtraction/TextExtractionHandlerInterface.php' => + array ( + 'hash' => 'e5aa889339c0a5c52a9b4d2307318d00294a8afb', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtraction/EntityRecognitionHandler.php' => + array ( + 'hash' => '003f62e341079996d5498737a29553cb36e1edcb', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtraction/FileHandler.php' => + array ( + 'hash' => 'd88cc7b81673e6356fcd1ab3f02137e64ce3fc5f', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtraction/ObjectHandler.php' => + array ( + 'hash' => 'fb08714c1c928e2943ced5755e56627cf83e7067', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtraction/EmlStructure.php' => + array ( + 'hash' => 'e4dacefa93191146a0cef06089e503a7a301e9d3', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtraction/EmlParser.php' => + array ( + 'hash' => '6cce7e781c2069d5fb161c765c737e23c1c23546', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtraction/EmlAttachment.php' => + array ( + 'hash' => 'c460c7f504138c22505777cbc8bd12662a1d5d84', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtraction/EmlBody.php' => + array ( + 'hash' => '5a27d7006ab94f1eec967120a282c57499a83951', + 'violations' => + array ( + ), + ), + 'lib/Service/ContactMatchingService.php' => + array ( + 'hash' => 'a79b0601dada117318169a429d0db9f62ed656bf', + 'violations' => + array ( + ), + ), + 'lib/Service/ToolRegistry.php' => + array ( + 'hash' => '2907e6a79f16a6839f9d837782909f03bfde49d6', + 'violations' => + array ( + ), + ), + 'lib/Service/Mcp/McpProtocolService.php' => + array ( + 'hash' => '35e8a624ef1829121b7501a7423fbe4e0c4c01bb', + 'violations' => + array ( + ), + ), + 'lib/Service/Mcp/McpToolsService.php' => + array ( + 'hash' => '487c4251efdd9265faf607666987691c94c243be', + 'violations' => + array ( + ), + ), + 'lib/Service/Mcp/McpResourcesService.php' => + array ( + 'hash' => '939896ad785c107c83c836f14d26707e3a662068', + 'violations' => + array ( + ), + ), + 'lib/Service/TaskService.php' => + array ( + 'hash' => '441c95fecb6fdad898bc0ca70e07b5a806c82b82', + 'violations' => + array ( + ), + ), + 'lib/Service/LogService.php' => + array ( + 'hash' => '037f2a57242ba052ce9f5fd3ab4c16c687b864dd', + 'violations' => + array ( + ), + ), + 'lib/Service/DateTimeNormalizer.php' => + array ( + 'hash' => 'c3a90c647fbdfe314ea51aff0f5e69752fd48182', + 'violations' => + array ( + ), + ), + 'lib/Service/ContactService.php' => + array ( + 'hash' => 'c7e2b218e0428a9f00a5f784e3162b57b8f3dc5c', + 'violations' => + array ( + ), + ), + 'lib/Service/OasService.php' => + array ( + 'hash' => '2da1fbaa5b46d40a0ac20435cee4f5ef18c345b2', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 866, + 'endLine' => 868, + 'description' => 'The method sanitizePropertyDefinition uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'sanitizePropertyDefinition', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/FileService.php' => + array ( + 'hash' => 'd5332e4a99caa1f3a1e697c348aa0632655f8b14', + 'violations' => + array ( + ), + ), + 'lib/Service/ObjectServiceMapperAdapter.php' => + array ( + 'hash' => '4867e9bd3ef24c9d0bad47ac50a14a518ac998dd', + 'violations' => + array ( + ), + ), + 'lib/Service/RegisterService.php' => + array ( + 'hash' => '2017f82ed5efc868f4be01d0c8f78d98eba744bb', + 'violations' => + array ( + ), + ), + 'lib/Service/RequestScopedCache.php' => + array ( + 'hash' => '3d14c319947d00cc8e31f6e3b7187212f8361032', + 'violations' => + array ( + ), + ), + 'lib/Service/WorkflowEngineRegistry.php' => + array ( + 'hash' => 'ef461b8a5b06a61ced1b8ba060d3b0340a60f92b', + 'violations' => + array ( + ), + ), + 'lib/Service/NotificationService.php' => + array ( + 'hash' => 'cc109ddd1fea1428908170fe54128b139b4874ae', + 'violations' => + array ( + ), + ), + 'lib/Service/ChatService.php' => + array ( + 'hash' => 'a4c5e21213177b800f3bff6f90fbf8bc86e440ac', + 'violations' => + array ( + ), + ), + 'lib/Service/LanguageService.php' => + array ( + 'hash' => '16a95b43aa287c9785d4ad904026872542322e1a', + 'violations' => + array ( + ), + ), + 'lib/Service/ActionService.php' => + array ( + 'hash' => '5fdaca1c80944b4a53a50ff9cd09b78f7e60b723', + 'violations' => + array ( + ), + ), + 'lib/Service/ActionExecutor.php' => + array ( + 'hash' => 'bbd73f3e85111fc59d269d5b6d2f64e3a0f32d0e', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 152, + 'endLine' => 154, + 'description' => 'The method executeSingleAction uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'executeSingleAction', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/MetricsService.php' => + array ( + 'hash' => 'c2d2340f4dbc0655e37c8c308a1b8794ea9a496c', + 'violations' => + array ( + ), + ), + 'lib/Service/EmailService.php' => + array ( + 'hash' => 'e8dc7c01088e558d5fd060845c1fae911d8f500f', + 'violations' => + array ( + ), + ), + 'lib/Service/AuditHashService.php' => + array ( + 'hash' => '500855e74d0da398099f96b7a1adb18ada255dab', + 'violations' => + array ( + ), + ), + 'lib/Service/TextExtractionService.php' => + array ( + 'hash' => 'c89a3f2c88c07d3390592a6a80b08647a1ba03e4', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'TextExtractionService', + 'methodName' => 'walkWordElements', + 'functionName' => NULL, + 'beginLine' => 1589, + 'endLine' => 1649, + 'description' => 'The method walkWordElements() has an NPath complexity of 242. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'walkWordElements', + 2 => '242', + 3 => 200, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/FileSidebarService.php' => + array ( + 'hash' => '4a4073ee39fc9c02d45e2c547fef9f4b235c0610', + 'violations' => + array ( + ), + ), + 'lib/Service/AuthorizationAuditService.php' => + array ( + 'hash' => 'e1c4d0dc3085d1a856c34632abb5154b1c403aae', + 'violations' => + array ( + ), + ), + 'lib/Service/OrganisationService.php' => + array ( + 'hash' => '637361023304fab86e885012c5c8948b39bc2ba5', + 'violations' => + array ( + ), + ), + 'lib/Service/SchemaService.php' => + array ( + 'hash' => '03046cd445e7ef52db7a1a98a3cdb5dc199a5505', + 'violations' => + array ( + ), + ), + 'lib/Service/Vectorization/Handlers/EmbeddingGeneratorHandler.php' => + array ( + 'hash' => 'b01480cdf21ad22fa1a2f912ba7099d19c3aecb7', + 'violations' => + array ( + ), + ), + 'lib/Service/Vectorization/Handlers/VectorSearchHandler.php' => + array ( + 'hash' => '86c5fe424426e0e20111b42b014fb4f48afb5765', + 'violations' => + array ( + ), + ), + 'lib/Service/Vectorization/Handlers/VectorStatsHandler.php' => + array ( + 'hash' => '54d33799c7558d0539a94cea47bf47591dd469c3', + 'violations' => + array ( + ), + ), + 'lib/Service/Vectorization/Handlers/VectorStorageHandler.php' => + array ( + 'hash' => 'fae5f59793987575e454b0d618ae5afd63637f1d', + 'violations' => + array ( + ), + ), + 'lib/Service/Vectorization/VectorEmbeddings.php' => + array ( + 'hash' => '96d53f50ad5f9120252e8f84f3b6bec127a57dc0', + 'violations' => + array ( + ), + ), + 'lib/Service/Vectorization/Strategies/VectorizationStrategyInterface.php' => + array ( + 'hash' => '854edb7f198d51bf7b976614250703f2222c076d', + 'violations' => + array ( + ), + ), + 'lib/Service/Vectorization/Strategies/FileVectorizationStrategy.php' => + array ( + 'hash' => '5c9b8c04d93b873473c6e787a8b6c6f520ee2e40', + 'violations' => + array ( + ), + ), + 'lib/Service/Vectorization/Strategies/ObjectVectorizationStrategy.php' => + array ( + 'hash' => '551896d2a1e7a9c19adfba52dc002124c982027f', + 'violations' => + array ( + ), + ), + 'lib/Service/ArchivalService.php' => + array ( + 'hash' => 'f8e747cf65d7f6f1bf3343b6a4172896cc0defaa', + 'violations' => + array ( + ), + ), + 'lib/Service/ConfigurationService.php' => + array ( + 'hash' => 'f9288230391542fa507decc763ea1367305a7adc', + 'violations' => + array ( + ), + ), + 'lib/Service/ExportService.php' => + array ( + 'hash' => 'c11ccbadf5052f50f9e4933560e86473c319314d', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/FacetBuilder.php' => + array ( + 'hash' => '0b5219f7311d76f34318d4275225ee17788d59fd', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/DocumentBuilder.php' => + array ( + 'hash' => '118262662f5b2c198dc26088bf63eecd21cf8283', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/BulkIndexer.php' => + array ( + 'hash' => '3966449b61acc663e4c2d19aa449aa6dd7d4cfec', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/SchemaHandler.php' => + array ( + 'hash' => 'a2ec3d268de88c7e1879b300adc6db38ffb5834e', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/SchemaMapper.php' => + array ( + 'hash' => 'c096b53485e8e4408eac1c12c47fc85bd264929a', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/SearchBackendInterface.php' => + array ( + 'hash' => 'f6c10e3b004f6f4dd98886b7667267d61d87faf7', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/ConfigurationHandler.php' => + array ( + 'hash' => 'f6b4ec78726c3675613650e878297f92a7aabf81', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/SetupHandler.php' => + array ( + 'hash' => '160c0e610831d6557362b9eee11cd43434bcd87d', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/SolrBackend.php' => + array ( + 'hash' => '0e49f0370f5a223cc5829053f69f397a50ca9860', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Solr/SolrCollectionManager.php' => + array ( + 'hash' => 'fb1f16d6a1618840a03d1fda5a66bf644e718f51', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Solr/SolrHttpClient.php' => + array ( + 'hash' => '6a0901de5c99a413eb8dbdc9decc7d1bb817d12e', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Solr/SolrDocumentIndexer.php' => + array ( + 'hash' => '7b27fc4ff03a87ca85519a372628d9b3808c1a2a', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Solr/SolrQueryExecutor.php' => + array ( + 'hash' => '0adfe8679556a2c68047c8e6f14ebf45d64ec29c', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Solr/SolrFacetProcessor.php' => + array ( + 'hash' => '2306c0486e68da45e5642067eb87de558903af4c', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Solr/SolrSchemaManager.php' => + array ( + 'hash' => '69fcb38692e802f5cf41dd78abb9cb57459cfaf9', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/ElasticsearchBackend.php' => + array ( + 'hash' => 'bdbdbb3e3d4ed290b31abb54fe6ab4755f5a9b2b', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Elasticsearch/ElasticsearchIndexManager.php' => + array ( + 'hash' => '6786b42f71f378271b5e1f3919f9e466b3b5196d', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Elasticsearch/ElasticsearchDocumentIndexer.php' => + array ( + 'hash' => 'f4c278b34ea2c25663aa708e3e454a46d74a9ec4', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Elasticsearch/ElasticsearchHttpClient.php' => + array ( + 'hash' => '4d3038867ad70b6d71482076510f481fa68871e1', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/Backends/Elasticsearch/ElasticsearchQueryExecutor.php' => + array ( + 'hash' => '7ebde85d0db127f559418cbe504883ac01a44c5f', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/FileHandler.php' => + array ( + 'hash' => '42b6f0ed7e6cb96b14fd2640f188a934603bab22', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/WarmupHandler.php' => + array ( + 'hash' => '406cd05615ee238371d604b649385e594d04dbaf', + 'violations' => + array ( + ), + ), + 'lib/Service/Index/ObjectHandler.php' => + array ( + 'hash' => '58bbee672d3ea616fa3db869659fbb8e89df556a', + 'violations' => + array ( + ), + ), + 'lib/Service/DashboardService.php' => + array ( + 'hash' => '9c6df4a9f17780aa98b4272ffd277948dc873096', + 'violations' => + array ( + ), + ), + 'lib/Service/SettingsService.php' => + array ( + 'hash' => '244575a72fbb5acc8daed187e94da5f67dd7fa21', + 'violations' => + array ( + ), + ), + 'lib/Service/DeckCardService.php' => + array ( + 'hash' => '18cf389000b202a89fee2b93a42f7aef9b0efb00', + 'violations' => + array ( + ), + ), + 'lib/Service/OperatorEvaluator.php' => + array ( + 'hash' => 'a27cf7a60b345b498d9f780939589fba7c66444a', + 'violations' => + array ( + ), + ), + 'lib/Service/Schemas/PropertyValidatorHandler.php' => + array ( + 'hash' => '436dc725072ceebc3634ceb4c34173bde0a73ff3', + 'violations' => + array ( + ), + ), + 'lib/Service/Schemas/SchemaCacheHandler.php' => + array ( + 'hash' => 'd942fd1ab99a901533aba49f0c1ef5a4fcc6630d', + 'violations' => + array ( + ), + ), + 'lib/Service/Schemas/FacetCacheHandler.php' => + array ( + 'hash' => 'c0788e6dcf51f49467668dd0a43f18086c0dc5e3', + 'violations' => + array ( + ), + ), + 'lib/Service/Handler/ViewHandler.php' => + array ( + 'hash' => 'e0656b98da82c76725621d385a917dc27daca9ae', + 'violations' => + array ( + ), + ), + 'lib/Service/Handler/AgentHandler.php' => + array ( + 'hash' => '87e35e8145378ddf7f1a5bee00ab4e87ba20e464', + 'violations' => + array ( + ), + ), + 'lib/Service/Handler/ApplicationHandler.php' => + array ( + 'hash' => '3a1a68347d87a5e3b13ddc16738c41aa3b9d2bab', + 'violations' => + array ( + ), + ), + 'lib/Service/Handler/OrganisationHandler.php' => + array ( + 'hash' => '2c0652091d032655287700d586562ac0fde3202e', + 'violations' => + array ( + ), + ), + 'lib/Service/Handler/SourceHandler.php' => + array ( + 'hash' => '92a34d8ed9b8a79a98d89ce43887a642dde608f6', + 'violations' => + array ( + ), + ), + 'lib/Service/NoteService.php' => + array ( + 'hash' => '8da1740f3c9e684e7357cc78a8f5df852c4af87d', + 'violations' => + array ( + ), + ), + 'lib/Service/MappingService.php' => + array ( + 'hash' => '32df5c557e142746ead9dc57f6828541b1a10d75', + 'violations' => + array ( + ), + ), + 'lib/Service/TmloService.php' => + array ( + 'hash' => '7a77b8477c9ffa1d24ff74345ca35b95bf3d6954', + 'violations' => + array ( + ), + ), + 'lib/Service/IndexService.php' => + array ( + 'hash' => '1af757246425bc6deabed0ae41f21d3e412e3960', + 'violations' => + array ( + ), + ), + 'lib/Service/ApprovalService.php' => + array ( + 'hash' => '47e2a92748027c2dbfa05e889780690e794b2253', + 'violations' => + array ( + ), + ), + 'lib/Service/HookExecutor.php' => + array ( + 'hash' => '42256990725735fcf9c2e5b1bb4ecf77ddc5387e', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/Transport/RestApiTransport.php' => + array ( + 'hash' => 'db1c7241b48e4e3ea278b904431627be628aa207', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/Transport/OpenConnectorTransport.php' => + array ( + 'hash' => '5bc6a28d6e7a5ffc9ed37e10055598401bb99bec', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/Transport/SftpTransport.php' => + array ( + 'hash' => '4440a1ce16cf7a703cfd85fef4dfe055c52d897d', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/Transport/TransportResult.php' => + array ( + 'hash' => 'c1f34f011ccfea11102cdf7e7e5f789f799a4dcf', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/Transport/TransportInterface.php' => + array ( + 'hash' => 'd0b7ed744ab86903de0f865844fe2b2b73909350', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/EdepotTransferService.php' => + array ( + 'hash' => '8996f33dc6e3fb99a3d1ed8d8ef63bb221cf7f99', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/MdtoXmlGenerator.php' => + array ( + 'hash' => '95832da585319b4bfdfac71413dacbe83ccade6e', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/SipPackageBuilder.php' => + array ( + 'hash' => '3d691207a7a38b0d148f4bbe55de091f382c0932', + 'violations' => + array ( + ), + ), + 'lib/Service/Edepot/TransferListService.php' => + array ( + 'hash' => '430aa8af2c48ded84d3380731d1be4767a50d789', + 'violations' => + array ( + ), + ), + 'lib/Service/AuthorizationService.php' => + array ( + 'hash' => '87a62c337b87aab7d64407730a8dde361cf0d7b0', + 'violations' => + array ( + ), + ), + 'lib/Service/ViewService.php' => + array ( + 'hash' => '14cb3c8c733dbbb3d30042451b40c60f285d4ce0', + 'violations' => + array ( + ), + ), + 'lib/Service/SearchTrailService.php' => + array ( + 'hash' => '0e9c68a8079c129e607fc7de4baa8416a7190ff6', + 'violations' => + array ( + ), + ), + 'lib/Service/McpDiscoveryService.php' => + array ( + 'hash' => 'eaaec5d222cece856726f9a4bdcb6458140285c3', + 'violations' => + array ( + ), + ), + 'lib/Service/PropertyRbacHandler.php' => + array ( + 'hash' => '9acd85b5884bc508d4d06f65d22adef2542f27cb', + 'violations' => + array ( + ), + ), + 'lib/Service/AuthenticationService.php' => + array ( + 'hash' => '3158c74b06a3e993255a783fdfde3e7ca0de3819', + 'violations' => + array ( + ), + ), + 'lib/Service/ObjectService.php' => + array ( + 'hash' => 'd359e15b7d4a9eb377e0a1ad993e60ed83836176', + 'violations' => + array ( + ), + ), + 'lib/Service/File/TaggingHandler.php' => + array ( + 'hash' => 'f5dc1f5e2cdedca0b980dab5397e93798a365050', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FileLockHandler.php' => + array ( + 'hash' => '925e22aee49a351ae71888ff4f57bc2550888818', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FileVersioningHandler.php' => + array ( + 'hash' => '976672dd43c832124eae54cf013ba6c382cbe713', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FileValidationHandler.php' => + array ( + 'hash' => 'dc8296ae295e9c0f3041b27937290d32095888d5', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FileSharingHandler.php' => + array ( + 'hash' => 'b7438f97add912a7affda9219e641d95fb1587fd', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FilePublishingHandler.php' => + array ( + 'hash' => 'e30ac34fcc3c920e00180375e9baf326802480b1', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FileBatchHandler.php' => + array ( + 'hash' => '3d88d806cb4168a65b95e17912578c96fabda59f', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FileCrudHandler.php' => + array ( + 'hash' => '7767b654d87ae2d403bdd5b86d210b682fd8786e', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FileOwnershipHandler.php' => + array ( + 'hash' => 'a69267b4c118dc8b8e6637b899a3c974cb69a868', + 'violations' => + array ( + ), + ), + 'lib/Service/File/UpdateFileHandler.php' => + array ( + 'hash' => 'a5600e5844a96a02159d1e1ad019eea0d221ac59', + 'violations' => + array ( + ), + ), + 'lib/Service/File/DeleteFileHandler.php' => + array ( + 'hash' => '774849de894ab8aa64ff9030c9a50d6d6915043b', + 'violations' => + array ( + ), + ), + 'lib/Service/File/CreateFileHandler.php' => + array ( + 'hash' => '3b07afc2cd5707183691cf595275ffe07c491779', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FilePreviewHandler.php' => + array ( + 'hash' => '4aa0f789e53ae0c386660eb113e624e3684919d3', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FolderManagementHandler.php' => + array ( + 'hash' => '694b86d9c4dc3b2cc772c3ff8b03766631f7a06e', + 'violations' => + array ( + ), + ), + 'lib/Service/File/DocumentProcessingHandler.php' => + array ( + 'hash' => '07e712c442cc2414d5ecbbbee46630139cd3aae0', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Naming\\LongVariable', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 79, + 'endLine' => 79, + 'description' => 'Avoid excessively long variable names like $lastSanitizationReport. Keep variable name length under 20.', + 'args' => + array ( + 0 => '$lastSanitizationReport', + 1 => 20, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File', + 'className' => 'DocumentProcessingHandler', + 'methodName' => 'anonymizeDocument', + 'functionName' => NULL, + 'beginLine' => 207, + 'endLine' => 350, + 'description' => 'The method anonymizeDocument() has a Cyclomatic Complexity of 16. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'anonymizeDocument', + 2 => 16, + 3 => 10, + ), + 'metric' => NULL, + ), + 2 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File', + 'className' => 'DocumentProcessingHandler', + 'methodName' => 'anonymizeDocument', + 'functionName' => NULL, + 'beginLine' => 207, + 'endLine' => 350, + 'description' => 'The method anonymizeDocument() has an NPath complexity of 2688. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'anonymizeDocument', + 2 => '2688', + 3 => 200, + ), + 'metric' => NULL, + ), + 3 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongMethod', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File', + 'className' => 'DocumentProcessingHandler', + 'methodName' => 'anonymizeDocument', + 'functionName' => NULL, + 'beginLine' => 207, + 'endLine' => 350, + 'description' => 'The method anonymizeDocument() has 144 lines of code. Current threshold is set to 100. Avoid really long methods.', + 'args' => + array ( + 0 => 'method', + 1 => 'anonymizeDocument', + 2 => 144, + 3 => 100, + ), + 'metric' => NULL, + ), + 4 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 264, + 'endLine' => 267, + 'description' => 'The method anonymizeDocument uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'anonymizeDocument', + ), + 'metric' => NULL, + ), + 5 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 318, + 'endLine' => 323, + 'description' => 'The method anonymizeDocument uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'anonymizeDocument', + ), + 'metric' => NULL, + ), + 6 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 445, + 'endLine' => 445, + 'description' => 'Missing class import via use statement (line \'445\', column \'20\').', + 'args' => + array ( + 0 => 445, + 1 => 20, + ), + 'metric' => NULL, + ), + 7 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 538, + 'endLine' => 550, + 'description' => 'The method replaceWordsInWordDocument uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'replaceWordsInWordDocument', + ), + 'metric' => NULL, + ), + 8 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\MissingImport', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 851, + 'endLine' => 851, + 'description' => 'Missing class import via use statement (line \'851\', column \'30\').', + 'args' => + array ( + 0 => 851, + 1 => 30, + ), + 'metric' => NULL, + ), + 9 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\StaticAccess', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 896, + 'endLine' => 896, + 'description' => 'Avoid using static access to class \'\\ddn\\sapp\\PDFDoc\' in method \'replaceWordsInPdfDocument\'.', + 'args' => + array ( + 0 => '\\ddn\\sapp\\PDFDoc', + 1 => 'replaceWordsInPdfDocument', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/File/FileAuditHandler.php' => + array ( + 'hash' => '9c6c72ccbc6c75752e6c84d9de839df330eb7732', + 'violations' => + array ( + ), + ), + 'lib/Service/File/ReadFileHandler.php' => + array ( + 'hash' => '8947acf0a5777fe9e72eb80bf32d9873171c3026', + 'violations' => + array ( + ), + ), + 'lib/Service/File/FileFormattingHandler.php' => + array ( + 'hash' => 'f5d9422980a1c0bb4386902d165ee5ff688cac92', + 'violations' => + array ( + ), + ), + 'lib/Service/File/SanitizationReport.php' => + array ( + 'hash' => '3b7e9c7c3cb08d77988f9ea6dfb97fe464d2d959', + 'violations' => + array ( + ), + ), + 'lib/Service/File/ManualEntityService.php' => + array ( + 'hash' => '726ff00d6c2806349a31dbdc0570018344ff98d2', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\CouplingBetweenObjects', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File', + 'className' => 'ManualEntityService', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 61, + 'endLine' => 526, + 'description' => 'The class ManualEntityService has a coupling between objects value of 21. Consider to reduce the number of dependencies under 13.', + 'args' => + array ( + 0 => 'ManualEntityService', + 1 => 21, + 2 => 13, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongMethod', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File', + 'className' => 'ManualEntityService', + 'methodName' => 'addManualEntity', + 'functionName' => NULL, + 'beginLine' => 134, + 'endLine' => 258, + 'description' => 'The method addManualEntity() has 125 lines of code. Current threshold is set to 100. Avoid really long methods.', + 'args' => + array ( + 0 => 'method', + 1 => 'addManualEntity', + 2 => 125, + 3 => 100, + ), + 'metric' => NULL, + ), + 2 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\StaticAccess', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 376, + 'endLine' => 376, + 'description' => 'Avoid using static access to class \'\\OCA\\OpenRegister\\Service\\TextExtraction\\EntityRecognitionHandler\' in method \'lookupOrCreateEntity\'.', + 'args' => + array ( + 0 => '\\OCA\\OpenRegister\\Service\\TextExtraction\\EntityRecognitionHandler', + 1 => 'lookupOrCreateEntity', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/File/OfficeDocumentSanitizer.php' => + array ( + 'hash' => 'b75bd204ce0f821a9252e6ade7c8c24d4999da5f', + 'violations' => + array ( + ), + ), + 'lib/Service/File/Sanitizer/OdtSanitizer.php' => + array ( + 'hash' => '4f78efb1dbe95869ac2f0ac42c4c34b6fb93a3ae', + 'violations' => + array ( + ), + ), + 'lib/Service/File/Sanitizer/DocxSanitizer.php' => + array ( + 'hash' => '0d42cc517cb9b5a23e1973e8d71f3a5ceeebc029', + 'violations' => + array ( + ), + ), + 'lib/Service/File/Sanitizer/SanitizerInterface.php' => + array ( + 'hash' => 'b219c42d7128cfbce655612506142f73be24effb', + 'violations' => + array ( + ), + ), + 'lib/Service/File/Pdf/PdfTextReplacer.php' => + array ( + 'hash' => '71cae74f11f538d28486dffc1124fdcfb4a91ef0', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\StaticAccess', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 141, + 'endLine' => 141, + 'description' => 'Avoid using static access to class \'\\ddn\\sapp\\PDFDoc\' in method \'replaceInPdf\'.', + 'args' => + array ( + 0 => '\\ddn\\sapp\\PDFDoc', + 1 => 'replaceInPdf', + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File\\Pdf', + 'className' => 'PdfTextReplacer', + 'methodName' => 'validateOutput', + 'functionName' => NULL, + 'beginLine' => 243, + 'endLine' => 392, + 'description' => 'The method validateOutput() has a Cyclomatic Complexity of 13. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'validateOutput', + 2 => 13, + 3 => 10, + ), + 'metric' => NULL, + ), + 2 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File\\Pdf', + 'className' => 'PdfTextReplacer', + 'methodName' => 'validateOutput', + 'functionName' => NULL, + 'beginLine' => 243, + 'endLine' => 392, + 'description' => 'The method validateOutput() has an NPath complexity of 600. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'validateOutput', + 2 => '600', + 3 => 200, + ), + 'metric' => NULL, + ), + 3 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongMethod', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File\\Pdf', + 'className' => 'PdfTextReplacer', + 'methodName' => 'validateOutput', + 'functionName' => NULL, + 'beginLine' => 243, + 'endLine' => 392, + 'description' => 'The method validateOutput() has 150 lines of code. Current threshold is set to 100. Avoid really long methods.', + 'args' => + array ( + 0 => 'method', + 1 => 'validateOutput', + 2 => 150, + 3 => 100, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/File/Pdf/PdfMetadataSanitizer.php' => + array ( + 'hash' => '24535e1348f4fc56559ff9e40f72115b2d7ddd7b', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File\\Pdf', + 'className' => 'PdfMetadataSanitizer', + 'methodName' => 'sanitizeXmp', + 'functionName' => NULL, + 'beginLine' => 239, + 'endLine' => 314, + 'description' => 'The method sanitizeXmp() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'sanitizeXmp', + 2 => 15, + 3 => 10, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File\\Pdf', + 'className' => 'PdfMetadataSanitizer', + 'methodName' => 'sanitizeXmp', + 'functionName' => NULL, + 'beginLine' => 239, + 'endLine' => 314, + 'description' => 'The method sanitizeXmp() has an NPath complexity of 3888. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'sanitizeXmp', + 2 => '3888', + 3 => 200, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/File/ManualEntityResult.php' => + array ( + 'hash' => '4f83c5aaffc978f3a844f0702a3266a699666da9', + 'violations' => + array ( + ), + ), + 'lib/Service/File/ChunkTextMatcher.php' => + array ( + 'hash' => '20bad3950e5914c1ed77218eb4c078f99a2467f1', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ErrorControlOperator', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\File', + 'className' => 'ChunkTextMatcher', + 'methodName' => 'buildPattern', + 'functionName' => NULL, + 'beginLine' => 162, + 'endLine' => 189, + 'description' => 'Remove error control operator \'@\' on line 179.', + 'args' => + array ( + 0 => 179, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/File/SanitizationResult.php' => + array ( + 'hash' => '72c43f09ba19f5d2d790a7f265b6584e49b91daa', + 'violations' => + array ( + ), + ), + 'lib/Service/CalendarEventService.php' => + array ( + 'hash' => '4e83dca5a17b48c4f9c73ccc8c3e1b400f5b3f1c', + 'violations' => + array ( + ), + ), + 'lib/Service/ApplicationService.php' => + array ( + 'hash' => '7d40765b3324a62c15de8f252e03327dd38043ab', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/SchemaGenerator/CompositionHandler.php' => + array ( + 'hash' => '24da08cb873b4b2613b44c9b2f70c89f7b61600f', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/SchemaGenerator/TypeMapperHandler.php' => + array ( + 'hash' => 'c6375b50dd4cba12b2ba02ea373a19068cc2fe25', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/SchemaGenerator.php' => + array ( + 'hash' => '49abaf1db583f80d8be2cb81e6617fd4729a05a5', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/GraphQLErrorFormatter.php' => + array ( + 'hash' => '8a21a0b23267de1db1cc420abbb32e6f59c79a70', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/QueryComplexityAnalyzer.php' => + array ( + 'hash' => 'acb8c09ced3c01ca072e7665b28f4230c6f3e388', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/GraphQLService.php' => + array ( + 'hash' => '0a7dba1500d0f590ed30891455bc29ac2f3525c7', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/Scalar/UuidType.php' => + array ( + 'hash' => '617c1f22334b725ee7a46d94fc44101a80753c9a', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/Scalar/JsonType.php' => + array ( + 'hash' => '477f3f19d6a3a2b9c267deb2294bb7b8f40a2683', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/Scalar/DateTimeType.php' => + array ( + 'hash' => 'dca29a517e875dfb30ce2018aeac6391f3dda9ad', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/Scalar/UploadType.php' => + array ( + 'hash' => 'b9fc606f125a600cd9b403a242bb8ed20e62e0a6', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/Scalar/UriType.php' => + array ( + 'hash' => '8dba7fdce27df7ae9e7abdfaed7cecd996f41f86', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/Scalar/EmailType.php' => + array ( + 'hash' => '3d7adeb3f671b31945373f782e2bd099ee3edb3f', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/SubscriptionService.php' => + array ( + 'hash' => '723de7834391df2e95f65c0437169e1170b99993', + 'violations' => + array ( + ), + ), + 'lib/Service/GraphQL/GraphQLResolver.php' => + array ( + 'hash' => '623acb01a7a7eab6336b907faf654d4c62f306e6', + 'violations' => + array ( + ), + ), + 'lib/Service/Chat/ResponseGenerationHandler.php' => + array ( + 'hash' => '2a128976eafcfa2cceb4c3c7a3d2da9cfdcf0c5f', + 'violations' => + array ( + ), + ), + 'lib/Service/Chat/ConversationManagementHandler.php' => + array ( + 'hash' => 'cb375d6d28385c35b90b68db7986dcfc802dab83', + 'violations' => + array ( + ), + ), + 'lib/Service/Chat/MessageHistoryHandler.php' => + array ( + 'hash' => '73cf30a8c7d21d52656abedccb02bca0789c2786', + 'violations' => + array ( + ), + ), + 'lib/Service/Chat/ToolManagementHandler.php' => + array ( + 'hash' => '81d3774b11d5ea561a3af2468ad76622155465e8', + 'violations' => + array ( + ), + ), + 'lib/Service/Chat/ContextRetrievalHandler.php' => + array ( + 'hash' => '2bf4718f2501ba6b50ec4954fa244670ce0a2d28', + 'violations' => + array ( + ), + ), + 'lib/Service/Chat/StreamYieldChannel.php' => + array ( + 'hash' => '9778d3925a1c038ee68f6daa04e158f64fc9d5e0', + 'violations' => + array ( + ), + ), + 'lib/Service/SecurityService.php' => + array ( + 'hash' => '08fd147fb94a22235a662928a77eefc4e8273a26', + 'violations' => + array ( + ), + ), + 'lib/Service/ActivityService.php' => + array ( + 'hash' => '3860ce31a42f44e313dd8bb5b06a6a5cbe7b16ce', + 'violations' => + array ( + ), + ), + 'lib/Service/Webhook/CloudEventFormatter.php' => + array ( + 'hash' => '5bb2ff9c63b3f8de934b5f7b62c9abb1643f81dd', + 'violations' => + array ( + ), + ), + 'lib/Service/TenantLifecycleService.php' => + array ( + 'hash' => 'ba0e02f6f397f81d789844d2efa367bf360581ba', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'TenantLifecycleService', + 'methodName' => 'provision', + 'functionName' => NULL, + 'beginLine' => 173, + 'endLine' => 272, + 'description' => 'The method provision() has an NPath complexity of 1296. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'provision', + 2 => '1296', + 3 => 200, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongMethod', + 'namespaceName' => 'OCA\\OpenRegister\\Service', + 'className' => 'TenantLifecycleService', + 'methodName' => 'provision', + 'functionName' => NULL, + 'beginLine' => 173, + 'endLine' => 272, + 'description' => 'The method provision() has 100 lines of code. Current threshold is set to 100. Avoid really long methods.', + 'args' => + array ( + 0 => 'method', + 1 => 'provision', + 2 => 100, + 3 => 100, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/ConditionMatcher.php' => + array ( + 'hash' => '0d158d3b0544ab0f55a1f3bfad3c59714705352c', + 'violations' => + array ( + ), + ), + 'lib/Service/ImportService.php' => + array ( + 'hash' => '20d5ec76e2b3a9014d4c160ec533cbbda8a22b23', + 'violations' => + array ( + ), + ), + 'lib/Service/Settings/SolrSettingsHandler.php' => + array ( + 'hash' => '955f7f7e80d1eded6408c58d02ac47b33fe4eff0', + 'violations' => + array ( + ), + ), + 'lib/Service/Settings/ObjectRetentionHandler.php' => + array ( + 'hash' => '54bfc212c4acaa1cf24330a6da9e591724286163', + 'violations' => + array ( + ), + ), + 'lib/Service/Settings/SearchBackendHandler.php' => + array ( + 'hash' => 'a13439039e4bb6e0387a9e95eae5946c586c176a', + 'violations' => + array ( + ), + ), + 'lib/Service/Settings/CacheSettingsHandler.php' => + array ( + 'hash' => 'f3a9f68c84a3b1a2ea93266014701c3d870594d7', + 'violations' => + array ( + ), + ), + 'lib/Service/Settings/ValidationOperationsHandler.php' => + array ( + 'hash' => 'aff4b6500529c583f47084898259fe5c6486dbe2', + 'violations' => + array ( + ), + ), + 'lib/Service/Settings/ConfigurationSettingsHandler.php' => + array ( + 'hash' => 'd9cebb28a1e0f226ccaa5e341b71551d21e5b748', + 'violations' => + array ( + ), + ), + 'lib/Service/Settings/FileSettingsHandler.php' => + array ( + 'hash' => '22d741931b9fa5c82c8d2c4ec79efedc3b8b5ca6', + 'violations' => + array ( + ), + ), + 'lib/Service/Settings/LlmSettingsHandler.php' => + array ( + 'hash' => 'dbc72d76f206247cf5012c3f95f56fba49c280df', + 'violations' => + array ( + ), + ), + 'lib/Service/XwikiLinkService.php' => + array ( + 'hash' => '5ed1e70ab98145d617f4cdf0ed5bdfe388cc9ed0', + 'violations' => + array ( + ), + ), + 'lib/Service/UrnService.php' => + array ( + 'hash' => '123507058d608c00f68516e43211c9787052d91f', + 'violations' => + array ( + ), + ), + 'lib/Service/PollLinkService.php' => + array ( + 'hash' => '25f7414e829995ecb73c4e9fef1e9368bef09481', + 'violations' => + array ( + ), + ), + 'lib/Service/Oas/ProblemDetailsBuilder.php' => + array ( + 'hash' => '51b996e72c065e162206e0987c92553553f7c4a9', + 'violations' => + array ( + ), + ), + 'lib/Service/Oas/OasValidationReport.php' => + array ( + 'hash' => '8289778bda17dc78ceb40388c6eefd83fb1b1302', + 'violations' => + array ( + ), + ), + 'lib/Service/Oas/OasRequestValidator.php' => + array ( + 'hash' => '0e666d9a3063d4b7e6c752429aa749e7ef41082e', + 'violations' => + array ( + ), + ), + 'lib/Service/Oas/OasETagComputer.php' => + array ( + 'hash' => '645cfc6e6ecd8fa07bc4fea0daf9f867f9238adc', + 'violations' => + array ( + ), + ), + 'lib/Service/BulkTranslationService.php' => + array ( + 'hash' => 'aa73d17570ee1fec926e510e3094a775a7b2d9b6', + 'violations' => + array ( + ), + ), + 'lib/Service/EmailLinkService.php' => + array ( + 'hash' => '9ac0175de6a2fd30fe2bbf487ba6dc8ae7b7fd4a', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 279, + 'endLine' => 281, + 'description' => 'The method buildNewLink uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'buildNewLink', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/PhotoLinkService.php' => + array ( + 'hash' => '589a767e0f472d68fc9e7f83962cae86c566d4bb', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/IntegrationRegistry.php' => + array ( + 'hash' => '34f9e78970ca8807142ed4ddf597f7d70c0148c7', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/AbstractIntegrationProvider.php' => + array ( + 'hash' => '8f859a293e529b6f937e6f194ea2e1a7feb3b57c', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/ExternalIntegrationRouter.php' => + array ( + 'hash' => 'd28df51d3382f8a6c2828a03e70558777529020d', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/PropertyReferenceTypeValidator.php' => + array ( + 'hash' => '584fd7e42e3cb820254166cd4d75b3d81e08eff9', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/PaginatedResult.php' => + array ( + 'hash' => 'e0a78fe40c7f4457142b4e881758630f4ef1619c', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/BuiltinProviders/TagsProvider.php' => + array ( + 'hash' => 'b958f73da42f57ede1143aa3ba78690913314fd5', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/BuiltinProviders/TasksProvider.php' => + array ( + 'hash' => '40baec858abdf9cbab23267548f815808b33af08', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/BuiltinProviders/AuditTrailProvider.php' => + array ( + 'hash' => 'aa5c0fce89cc8ca4095f05591c3ea23c00a02568', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/BuiltinProviders/FilesProvider.php' => + array ( + 'hash' => '7c969287dcf489b3b6ebf7085b42cb6ef68921a6', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/BuiltinProviders/NotesProvider.php' => + array ( + 'hash' => 'fd1a3ced03d43a0764645bd6eb29e4a68cccfbd6', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/MarkerLookupTrait.php' => + array ( + 'hash' => 'aa9d2d93095d2f99329efbd81dd7e10e50514819', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/FlowProvider.php' => + array ( + 'hash' => 'd15e255e1be1c7851ef8d5779a391c826e008142', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/PhotosProvider.php' => + array ( + 'hash' => '663231ec2bc72a26486dda567e9039096dd0ae4f', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/ActivityProvider.php' => + array ( + 'hash' => '9ad41b8f55533a0495dccff1e6a3eb9844d9531e', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/ContactsProvider.php' => + array ( + 'hash' => 'c15159876fdaef16a061496061c06ab6004d8c99', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/AnalyticsProvider.php' => + array ( + 'hash' => '558324013f35a381a0631cf753d912889aaec2e0', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/TalkProvider.php' => + array ( + 'hash' => 'd5a7e5147fd46031de57cbde92db69b07a9423fb', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/XwikiProvider.php' => + array ( + 'hash' => 'f6eff99ccc6762f837f8615956a620f9fd3620df', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/EmailProvider.php' => + array ( + 'hash' => 'bd9dfc77e25486a5fc2fe84d47fa2ad9688d7392', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/PollsProvider.php' => + array ( + 'hash' => 'ca906090d27595210a39230fc72cd2850fb129a3', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/TimeProvider.php' => + array ( + 'hash' => '255ea8dbcd388c56a57f30a15f725470c4545a33', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/OpenProjectProvider.php' => + array ( + 'hash' => '86734e0594d7deda85710c61509abff5ea9fb456', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/CalendarProvider.php' => + array ( + 'hash' => '0da631476cb887fd127fa741424f2afed3ac3497', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/MapsProvider.php' => + array ( + 'hash' => '7b239ce9bf35611ebed95fac7c07928c18879ed8', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/BookmarksProvider.php' => + array ( + 'hash' => '439486fb757ed498c3cffaae2e64320842006851', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/CollectivesProvider.php' => + array ( + 'hash' => 'e4c469767757887f602ff56efd8f2016a0ab43ef', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/DeckProvider.php' => + array ( + 'hash' => '7a46bab0cfc11d897d3fd34f5b7f7e21f9b9927b', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/CospendProvider.php' => + array ( + 'hash' => 'e24c0e878d185e0c85a40741fd871202f1738b1f', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/FormsProvider.php' => + array ( + 'hash' => '486adec1d4f38b1aafaae85804a270526d04b188', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/Providers/SharesProvider.php' => + array ( + 'hash' => '73f6efcef6021343ef20b2dd471cf54579e6c94a', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/IntegrationProvider.php' => + array ( + 'hash' => '83aaf04425de1d65c4329aaed4bf73b316b0d2f0', + 'violations' => + array ( + ), + ), + 'lib/Service/Integration/QueryTimeContract.php' => + array ( + 'hash' => '6ac0e5dc41a8b0c36151c2433bcd1f1bd098106a', + 'violations' => + array ( + ), + ), + 'lib/Service/TimeTrackerLinkService.php' => + array ( + 'hash' => '776ee44f8f2c3fb984a032ca8b6b7b78fe05edc8', + 'violations' => + array ( + ), + ), + 'lib/Service/DsarService.php' => + array ( + 'hash' => '30794141b752bc5a8656db3722a54568dd40e3fc', + 'violations' => + array ( + ), + ), + 'lib/Service/AvgRetentionService.php' => + array ( + 'hash' => 'b6782abba07f2bb2f08e95c69a1dbe478d0486c9', + 'violations' => + array ( + ), + ), + 'lib/Service/ShareLinkService.php' => + array ( + 'hash' => '240caddf0e6ecd50cc5594a0081deddf8b087e67', + 'violations' => + array ( + ), + ), + 'lib/Service/Reporting/PdfReportWriter.php' => + array ( + 'hash' => '81d71f4d26ed0426ea3b8332962dec21e979c11b', + 'violations' => + array ( + ), + ), + 'lib/Service/Reporting/SpreadsheetReportWriter.php' => + array ( + 'hash' => 'd14ccaf9062d7dca3d890dcebee6019498a61249', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 244, + 'endLine' => 246, + 'description' => 'The method writeFormat uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'writeFormat', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/Reporting/ReportRenderService.php' => + array ( + 'hash' => 'af32c85cc59e9a7a97f119ea536fba8830d07a57', + 'violations' => + array ( + ), + ), + 'lib/Service/Reporting/HtmlReportWriter.php' => + array ( + 'hash' => '392000068cbbfcb4f8def40a6bacfd775730f666', + 'violations' => + array ( + ), + ), + 'lib/Service/FormLinkService.php' => + array ( + 'hash' => '5355824c2b29b5cdb1866d37e412c5c8599a3eb0', + 'violations' => + array ( + ), + ), + 'lib/Service/TalkLinkService.php' => + array ( + 'hash' => '48c9c19191a0b0fb20aab133f100f11cc261064d', + 'violations' => + array ( + ), + ), + 'lib/Service/Aggregation/SolrAggregationQueryBuilder.php' => + array ( + 'hash' => '627322ba49c72b273eba4b11b201c7fdb9ab2723', + 'violations' => + array ( + ), + ), + 'lib/Service/Aggregation/WidgetAnnotationValidator.php' => + array ( + 'hash' => '540b2aba055145105cfdeab67e5cbf08b28bb62d', + 'violations' => + array ( + ), + ), + 'lib/Service/Aggregation/TimeseriesRequestValidator.php' => + array ( + 'hash' => 'f1310df04fa73da484f8a10ebfbb15206a727dff', + 'violations' => + array ( + ), + ), + 'lib/Service/Aggregation/AggregationRunner.php' => + array ( + 'hash' => 'f0ae13c02ba3f967965c3e0ac6c2c00e78161f6d', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 720, + 'endLine' => 722, + 'description' => 'The method bucketInPhp uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'bucketInPhp', + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 887, + 'endLine' => 889, + 'description' => 'The method computeGrouped uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'computeGrouped', + ), + 'metric' => NULL, + ), + 2 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 932, + 'endLine' => 934, + 'description' => 'The method reduceNumeric uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'reduceNumeric', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/Aggregation/AggregationQuery.php' => + array ( + 'hash' => '9802d4fe3676bdaeb819a6a83de53869532ab96e', + 'violations' => + array ( + ), + ), + 'lib/Service/Aggregation/AggregationAnnotationValidator.php' => + array ( + 'hash' => 'c0530a9e7d76a0a831ebbbb5ec7d25bcc5923f69', + 'violations' => + array ( + ), + ), + 'lib/Service/Aggregation/AggregationCache.php' => + array ( + 'hash' => 'c6da7ecf2e58c7f4c1b3c4859ac11bdc326797b9', + 'violations' => + array ( + ), + ), + 'lib/Service/Aggregation/ElasticsearchAggregationQueryBuilder.php' => + array ( + 'hash' => 'de282bf2e6f12cf0143220559a4801d1d08a88c1', + 'violations' => + array ( + ), + ), + 'lib/Service/Lifecycle/TransitionEngine.php' => + array ( + 'hash' => '454d385c7dbff184d80194a89ee4a8e9f3013482', + 'violations' => + array ( + ), + ), + 'lib/Service/Lifecycle/LifecycleGuardRegistry.php' => + array ( + 'hash' => 'f2da7cf9843cd5dab101865985c11de592b57daf', + 'violations' => + array ( + ), + ), + 'lib/Service/Lifecycle/LifecycleAnnotationValidator.php' => + array ( + 'hash' => '33c2ab70448231b2e124396badbc39d1cc2a73bd', + 'violations' => + array ( + ), + ), + 'lib/Service/MapLinkService.php' => + array ( + 'hash' => 'ff680016597c69bf66bb7b53800d61d3275303cc', + 'violations' => + array ( + ), + ), + 'lib/Service/Registers/RegisterCacheHandler.php' => + array ( + 'hash' => 'e5d9620ffcb7ca79ec028a5d637a8eecf56ab6df', + 'violations' => + array ( + ), + ), + 'lib/Service/Translation/TranslationCsvCodec.php' => + array ( + 'hash' => 'b4ff19ebdbaecfe5501e38518f142e3ceacef6b8', + 'violations' => + array ( + ), + ), + 'lib/Service/Translation/IdentityTranslationProvider.php' => + array ( + 'hash' => 'dc67a8a86c4c99f9342d5924ab52d56d57859d2e', + 'violations' => + array ( + ), + ), + 'lib/Service/Translation/TranslationProviderInterface.php' => + array ( + 'hash' => '570c51118e1ae1d4454cbb382222139a830b2bb0', + 'violations' => + array ( + ), + ), + 'lib/Service/CospendLinkService.php' => + array ( + 'hash' => 'a63bdc2d7d510c4be6275158fae286188b080343', + 'violations' => + array ( + ), + ), + 'lib/Service/ManifestService.php' => + array ( + 'hash' => '281f110faac381b7a08a977e214486b9c5e180ca', + 'violations' => + array ( + ), + ), + 'lib/Service/CollectiveLinkService.php' => + array ( + 'hash' => '2ab1e0d72e46af676f37dc317454c226a9cb5554', + 'violations' => + array ( + ), + ), + 'lib/Service/Search/PlaceholderResolver.php' => + array ( + 'hash' => 'a5ac28dd210937d71788dbf7a05c14925d75574d', + 'violations' => + array ( + ), + ), + 'lib/Service/TranslationStatusService.php' => + array ( + 'hash' => '22e053d7d1ae417a382b5fe2869294756f60062f', + 'violations' => + array ( + ), + ), + 'lib/Service/CalendarLinkService.php' => + array ( + 'hash' => 'de25bcca3daada56bb92d99d942a5e87260669f6', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 503, + 'endLine' => 505, + 'description' => 'The method getEventsForCalendar uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'getEventsForCalendar', + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 509, + 'endLine' => 511, + 'description' => 'The method getEventsForCalendar uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'getEventsForCalendar', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/OpenProjectLinkService.php' => + array ( + 'hash' => '909d7a9c9173779ce11ce493989cde0af63a2f30', + 'violations' => + array ( + ), + ), + 'lib/Service/Geo/GeoFilterParser.php' => + array ( + 'hash' => '837e9e0587e06ae5d420544a03428f4ee61c4d3c', + 'violations' => + array ( + ), + ), + 'lib/Service/Geo/GeoFilterApplier.php' => + array ( + 'hash' => 'a85aa83fe9214810ab7563b411c57cf8f5765c01', + 'violations' => + array ( + ), + ), + 'lib/Service/Geo/GeoFilter.php' => + array ( + 'hash' => '9981874358753cff7fdd018202077661abf3c1b9', + 'violations' => + array ( + ), + ), + 'lib/Service/Geo/GeoSpatialEvaluator.php' => + array ( + 'hash' => '582a58c75dae09439ce74e18ef1307f30e3b9c36', + 'violations' => + array ( + ), + ), + 'lib/Service/AnalyticsLinkService.php' => + array ( + 'hash' => '5dd7f73ac7ce61076f4d9d9c051d27332f8a20c0', + 'violations' => + array ( + ), + ), + 'lib/Service/TenantKeyService.php' => + array ( + 'hash' => '42bbfa4c561bd2d033f8cd28fc9de81e1dc97f74', + 'violations' => + array ( + ), + ), + 'lib/Service/Calculation/CalculationAnnotationValidator.php' => + array ( + 'hash' => '6986f83bc3c2b765978034886e4ce1b9956cccfe', + 'violations' => + array ( + ), + ), + 'lib/Service/Calculation/CalculationEvaluator.php' => + array ( + 'hash' => '3ea7eb2a00a7607255b65d8d8d548945881d1c85', + 'violations' => + array ( + ), + ), + 'lib/Service/Calculation/EvaluationException.php' => + array ( + 'hash' => '2df326b98ee4982004208365c6800ece9e058a17', + 'violations' => + array ( + ), + ), + 'lib/Service/RealtimeService.php' => + array ( + 'hash' => 'd88e18aca9af69de8e4324ca4c8026d8be6d24bc', + 'violations' => + array ( + ), + ), + 'lib/Service/AvgComplianceService.php' => + array ( + 'hash' => '94714ea42f6eb3e9bfe12e8142b57ab60f849c90', + 'violations' => + array ( + ), + ), + 'lib/Service/BookmarkLinkService.php' => + array ( + 'hash' => '468659b013a9bf556ad6e0557031885490752780', + 'violations' => + array ( + ), + ), + 'lib/Service/ActivityFilterService.php' => + array ( + 'hash' => '87cc7b6c8a443fdf41e60bdcdf97d9ffa8506ce9', + 'violations' => + array ( + ), + ), + 'lib/Service/DeckLinkService.php' => + array ( + 'hash' => '369ba05a8c5408de145207c0699a7c12467fee49', + 'violations' => + array ( + ), + ), + 'lib/Service/TranslationProjectionService.php' => + array ( + 'hash' => '897f4e6ff4835ef9ea2700f98bd82ea8382515a9', + 'violations' => + array ( + ), + ), + 'lib/Service/FlowLinkService.php' => + array ( + 'hash' => 'fac7955bca81faa2711c9578158157d87a6b6326', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/AnnotationNotificationDispatcher.php' => + array ( + 'hash' => '79fc1b59984169381e65699cf137eb8148776925', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/VngNotificatiesEnvelope.php' => + array ( + 'hash' => '555919651b409a38249e231c6b004e1af35ba907', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/SystemSchemaRules.php' => + array ( + 'hash' => 'dec099598e69518d336e633f60cbdb069ead3110', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/NotificationReadState.php' => + array ( + 'hash' => 'e849997af224664b1d11b6aff575c7bdadc52c76', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/NotificationDigest.php' => + array ( + 'hash' => '7ae57117eb97afcd7f952a028705369e2cd80931', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/RateLimiter.php' => + array ( + 'hash' => '36881199cbcfd99ca1a295510c9b6b93169dfd13', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/SystemEntityObjectAdapter.php' => + array ( + 'hash' => '2c408fc1186cde35e683ff4d18121c769733a454', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Service\\Notification', + 'className' => 'SystemEntityObjectAdapter', + 'methodName' => '__construct', + 'functionName' => NULL, + 'beginLine' => 58, + 'endLine' => 109, + 'description' => 'The method __construct() has a Cyclomatic Complexity of 10. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => '__construct', + 2 => 10, + 3 => 10, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Service/Notification/NotificationCoalescer.php' => + array ( + 'hash' => '9d32d3aa2f9d0dddec9fadc22183116a5e208370', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/NotificationsAnnotationInstaller.php' => + array ( + 'hash' => 'da77f229a58d619dfe613a39e426ade96cdb96b6', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/RecipientResolverInterface.php' => + array ( + 'hash' => '6045b1116414db59122b17923306b9aea05eed32', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/NotificationPreferenceService.php' => + array ( + 'hash' => '339af9c211823bb0df4b6a21cfcdbf6f79596aeb', + 'violations' => + array ( + ), + ), + 'lib/Service/Notification/NotificationAnnotationValidator.php' => + array ( + 'hash' => '36257aa62cf184ad7ab225f2c61963433952ea95', + 'violations' => + array ( + ), + ), + 'lib/Activity/Provider.php' => + array ( + 'hash' => '9a77e04bec91a74958b83a4207f3ded960e184c3', + 'violations' => + array ( + ), + ), + 'lib/Activity/ProviderSubjectHandler.php' => + array ( + 'hash' => '24b8560c7d9731d5221bc0d5d042f03011f972f6', + 'violations' => + array ( + ), + ), + 'lib/Activity/Filter.php' => + array ( + 'hash' => '6ec3172f94b0adc97ddc80bfe5c4bd14bddd2d04', + 'violations' => + array ( + ), + ), + 'lib/Activity/Setting/RegisterSetting.php' => + array ( + 'hash' => '8a38874572af03b06dfc3e3499d396f3fb83ebc1', + 'violations' => + array ( + ), + ), + 'lib/Activity/Setting/SchemaSetting.php' => + array ( + 'hash' => '41d66ed59541b096ad26a3b9503365294f3c69c1', + 'violations' => + array ( + ), + ), + 'lib/Activity/Setting/ObjectSetting.php' => + array ( + 'hash' => '00f6970362f293769551bfa0b82c5a1c1ef84e09', + 'violations' => + array ( + ), + ), + 'lib/Db/Conversation.php' => + array ( + 'hash' => '975f4819a3d1f92efe5ea415573de29c27e58462', + 'violations' => + array ( + ), + ), + 'lib/Db/Source.php' => + array ( + 'hash' => '19770d73f536daafe0c6a828a5ace282b34ef7fe', + 'violations' => + array ( + ), + ), + 'lib/Db/ScheduledWorkflowMapper.php' => + array ( + 'hash' => '9b785b3f2f5cfe50fe403d5f36c72902bba59e23', + 'violations' => + array ( + ), + ), + 'lib/Db/Organisation.php' => + array ( + 'hash' => 'dec9fed521211d5e0fa1f166b0ba201b7f59412e', + 'violations' => + array ( + ), + ), + 'lib/Db/MappingMapper.php' => + array ( + 'hash' => 'd3638cdca132cadaaa04d741bc7b4a26ec933798', + 'violations' => + array ( + ), + ), + 'lib/Db/MagicMapper.php' => + array ( + 'hash' => 'd2359f9a1d595ce7374d7736ff2f80e3a0ce4016', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 2720, + 'endLine' => 2722, + 'description' => 'The method createTable uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'createTable', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Db/ObjectHandlers/HyperFacetHandler.php' => + array ( + 'hash' => '914c686f8879bd179a5d858c55e5f103cc7ba98a', + 'violations' => + array ( + ), + ), + 'lib/Db/ObjectHandlers/MetaDataFacetHandler.php' => + array ( + 'hash' => '18c66060644288aed9dc2b385c14c8219c05ca15', + 'violations' => + array ( + ), + ), + 'lib/Db/ObjectHandlers/OptimizedFacetHandler.php' => + array ( + 'hash' => 'da7407119e1498aa1e766b7ccaa62014bfed286e', + 'violations' => + array ( + ), + ), + 'lib/Db/ObjectHandlers/MariaDbFacetHandler.php' => + array ( + 'hash' => '29350d24cbbfa7784ff2952c08201d3560d08ae6', + 'violations' => + array ( + ), + ), + 'lib/Db/ObjectHandlers/MariaDbSearchHandler.php' => + array ( + 'hash' => '954444af1ea27a25714011ffd248a42eba43195b', + 'violations' => + array ( + ), + ), + 'lib/Db/TenantUsage.php' => + array ( + 'hash' => 'b8913973ba6015f1b320a82836d82b59fd91ae63', + 'violations' => + array ( + ), + ), + 'lib/Db/DeckLink.php' => + array ( + 'hash' => '0a39669fb1313d212d86b1c5e40881b68073e706', + 'violations' => + array ( + ), + ), + 'lib/Db/Webhook.php' => + array ( + 'hash' => '9513e140bcf2e29c25e6157ae597a20ad9a3d3ed', + 'violations' => + array ( + ), + ), + 'lib/Db/ApprovalStepMapper.php' => + array ( + 'hash' => 'e784d30581a548d925c257b36b6d137427b4eb08', + 'violations' => + array ( + ), + ), + 'lib/Db/ActionLog.php' => + array ( + 'hash' => '95915b0397a6fe52761a6a4a2ce05868f3a35a0b', + 'violations' => + array ( + ), + ), + 'lib/Db/DestructionList.php' => + array ( + 'hash' => 'ba8faa1ea23bd1217a0e85d7410a3646dddf814f', + 'violations' => + array ( + ), + ), + 'lib/Db/WorkflowEngineMapper.php' => + array ( + 'hash' => '29d84f5a45db048f1ea4383080c11aff03160621', + 'violations' => + array ( + ), + ), + 'lib/Db/ConfigurationMapper.php' => + array ( + 'hash' => '4d57ef421049640620c6a3c6e15a7a99c46cd943', + 'violations' => + array ( + ), + ), + 'lib/Db/EndpointMapper.php' => + array ( + 'hash' => 'f421fb3b021a23d06a537d198509869780da4642', + 'violations' => + array ( + ), + ), + 'lib/Db/SelectionListMapper.php' => + array ( + 'hash' => 'fc3d9ede0d9101be643c62d325a7fa8e9ed757d9', + 'violations' => + array ( + ), + ), + 'lib/Db/WebhookLog.php' => + array ( + 'hash' => '3cbb08c8175fe1d6bc91228925063b42ba26cfd9', + 'violations' => + array ( + ), + ), + 'lib/Db/ChunkMapper.php' => + array ( + 'hash' => '94184b57f4b008099c6edf3b5f37700dca971f89', + 'violations' => + array ( + ), + ), + 'lib/Db/SchemaMapper.php' => + array ( + 'hash' => '1ade2bad29652da112eda362839814b50dfdc9c6', + 'violations' => + array ( + ), + ), + 'lib/Db/SelectionList.php' => + array ( + 'hash' => '2b08e6c9bd66ef0b3bd250ad844bed8674a7c868', + 'violations' => + array ( + ), + ), + 'lib/Db/ContactLinkMapper.php' => + array ( + 'hash' => 'e3ce069868403da551b13fe96471ae70251ee4e5', + 'violations' => + array ( + ), + ), + 'lib/Db/ContactLink.php' => + array ( + 'hash' => '013152878dfe2f0120f7473890ec6230b3faee7a', + 'violations' => + array ( + ), + ), + 'lib/Db/AuditTrailMapper.php' => + array ( + 'hash' => '573769fc99e2aa7121e6245c837fecf3dbd874c3', + 'violations' => + array ( + ), + ), + 'lib/Db/Chunk.php' => + array ( + 'hash' => '3f6343dfdd58e46f2887b7ed8d7d4dcff2436583', + 'violations' => + array ( + ), + ), + 'lib/Db/FileMapper.php' => + array ( + 'hash' => '5432e6f321a6f9db49c078828b82e469c2a4b805', + 'violations' => + array ( + ), + ), + 'lib/Db/Configuration.php' => + array ( + 'hash' => '3ebcc4d1086768d3b329f22709f0d274ba3e483b', + 'violations' => + array ( + ), + ), + 'lib/Db/ConsumerMapper.php' => + array ( + 'hash' => '11bcab66ef1ad4b43eaf72673bd4cbcb72eb743f', + 'violations' => + array ( + ), + ), + 'lib/Db/Endpoint.php' => + array ( + 'hash' => 'ea8680356ed91505e74343b9d7bb3de9bcb4654a', + 'violations' => + array ( + ), + ), + 'lib/Db/SearchTrailMapper.php' => + array ( + 'hash' => '351b735d9a8289fd298ed355005a62b3883d2626', + 'violations' => + array ( + ), + ), + 'lib/Db/Action.php' => + array ( + 'hash' => '50d468e2d71de8e0c86d1bfcb375e48597c42cc1', + 'violations' => + array ( + ), + ), + 'lib/Db/RegisterMapper.php' => + array ( + 'hash' => '5c184845a38eda0d8d0bad61de11c9bca69cbe53', + 'violations' => + array ( + ), + ), + 'lib/Db/ConversationMapper.php' => + array ( + 'hash' => '412b8c720df96c6654c97644e01c3d99021de469', + 'violations' => + array ( + ), + ), + 'lib/Db/Consumer.php' => + array ( + 'hash' => 'e12dccd9643d6dc30a77e580045da8ed2e013c0c', + 'violations' => + array ( + ), + ), + 'lib/Db/ScheduledWorkflow.php' => + array ( + 'hash' => 'ba38939cc799abc07c5be9f6e230a7df0dc2ab70', + 'violations' => + array ( + ), + ), + 'lib/Db/WebhookMapper.php' => + array ( + 'hash' => 'faca00ccaff7ea48bf7320231db77c694a7bf8f3', + 'violations' => + array ( + ), + ), + 'lib/Db/DataAccessProfileMapper.php' => + array ( + 'hash' => 'a92661af83b01198ac8362a848497010bbcf2153', + 'violations' => + array ( + ), + ), + 'lib/Db/EntityRelation.php' => + array ( + 'hash' => '8d86974ec7224f7f871d7a77fc17b8f7a4c0694b', + 'violations' => + array ( + ), + ), + 'lib/Db/DeckLinkMapper.php' => + array ( + 'hash' => 'e107a15234a8872eee587fe63caee01ffb882246', + 'violations' => + array ( + ), + ), + 'lib/Db/EndpointLog.php' => + array ( + 'hash' => 'fd085ffe05247794ae026596211f39eb2dcfc6db', + 'violations' => + array ( + ), + ), + 'lib/Db/ApprovalStep.php' => + array ( + 'hash' => 'bcb9880c4549090e20e4fbca55d21da2e6f4e526', + 'violations' => + array ( + ), + ), + 'lib/Db/FeedbackMapper.php' => + array ( + 'hash' => '8e731b406ad4e3d27afabed6711fe676db41c04d', + 'violations' => + array ( + ), + ), + 'lib/Db/MessageMapper.php' => + array ( + 'hash' => '7fe626424470790d94a3eac7af65f49474a66ca6', + 'violations' => + array ( + ), + ), + 'lib/Db/DeployedWorkflow.php' => + array ( + 'hash' => 'b2fb8f57e380a9e2b23b05735ded009fcc2e42f0', + 'violations' => + array ( + ), + ), + 'lib/Db/OrganisationMapper.php' => + array ( + 'hash' => 'b99b09c63c1e2709a1f0e06fa2e77a0b669f9c1e', + 'violations' => + array ( + ), + ), + 'lib/Db/MagicMapper/MagicTableHandler.php' => + array ( + 'hash' => '1b5803d309d29f4d35fa8405994256c91e83b637', + 'violations' => + array ( + ), + ), + 'lib/Db/MagicMapper/MagicStatisticsHandler.php' => + array ( + 'hash' => '7abecfc229a3e4f1ad1808c60f84ee9ee098f603', + 'violations' => + array ( + ), + ), + 'lib/Db/MagicMapper/MagicOrganizationHandler.php' => + array ( + 'hash' => '6e1085c278897c8ea0ec3e8fd8e7f451838ac5d3', + 'violations' => + array ( + ), + ), + 'lib/Db/MagicMapper/MagicSearchHandler.php' => + array ( + 'hash' => 'e24c5298e533b1729d978cd4bd3416e9f180c065', + 'violations' => + array ( + ), + ), + 'lib/Db/MagicMapper/MagicRbacHandler.php' => + array ( + 'hash' => 'd1599961e51ae50e086e0db41c6d22672936cbeb', + 'violations' => + array ( + ), + ), + 'lib/Db/MagicMapper/MagicFacetHandler.php' => + array ( + 'hash' => 'f2d65ec9db2811e76caf6efdbdc5b27ea5b4dcb0', + 'violations' => + array ( + ), + ), + 'lib/Db/MagicMapper/MagicBulkHandler.php' => + array ( + 'hash' => '9abbd58ed5d917228d87f1d0412674dfda6f56f5', + 'violations' => + array ( + ), + ), + 'lib/Db/AbstractObjectMapper.php' => + array ( + 'hash' => '698c17ea42d830394476a5a1b0c031bff711e02b', + 'violations' => + array ( + ), + ), + 'lib/Db/MultiTenancyTrait.php' => + array ( + 'hash' => '67f3be0c4ffeea6b02e88a3ee9fc8793388e3dee', + 'violations' => + array ( + ), + ), + 'lib/Db/AuditTrail.php' => + array ( + 'hash' => '750d9acca7faa5322c5ffebaa92079e55f30104d', + 'violations' => + array ( + ), + ), + 'lib/Db/Application.php' => + array ( + 'hash' => '82f2ec8a5548948a7ed603c8acce595a6f630c7e', + 'violations' => + array ( + ), + ), + 'lib/Db/ApplicationMapper.php' => + array ( + 'hash' => '4a843185281af4b41647fb8b52f24f03de52ee8d', + 'violations' => + array ( + ), + ), + 'lib/Db/EndpointLogMapper.php' => + array ( + 'hash' => '028f84835a7087bd96e1d1c54e9e290679f24a0f', + 'violations' => + array ( + ), + ), + 'lib/Db/SearchTrail.php' => + array ( + 'hash' => '897f1ab51eb6700b333f0a8a9aa3cffde415f7b0', + 'violations' => + array ( + ), + ), + 'lib/Db/WebhookLogMapper.php' => + array ( + 'hash' => '7ce42859eb7d17048583398228466819a9755fb5', + 'violations' => + array ( + ), + ), + 'lib/Db/Agent.php' => + array ( + 'hash' => 'eec2669f0b2ef1ca1e3988da5365b1a91829db85', + 'violations' => + array ( + ), + ), + 'lib/Db/EntityRelationMapper.php' => + array ( + 'hash' => 'aa9396fdd66077edc792938c270cb9f077ae1821', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\TooManyPublicMethods', + 'namespaceName' => 'OCA\\OpenRegister\\Db', + 'className' => 'EntityRelationMapper', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 49, + 'endLine' => 822, + 'description' => 'The class EntityRelationMapper has 13 public methods. Consider refactoring EntityRelationMapper to keep number of public methods under 10.', + 'args' => + array ( + 0 => 'class', + 1 => 'EntityRelationMapper', + 2 => 13, + 3 => 10, + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\WeightedMethodCount', + 'namespaceName' => 'OCA\\OpenRegister\\Db', + 'className' => 'EntityRelationMapper', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 49, + 'endLine' => 822, + 'description' => 'The class EntityRelationMapper has an overall complexity of 69 which is very high. The configured complexity threshold is 50.', + 'args' => + array ( + 0 => 'EntityRelationMapper', + 1 => 69, + 2 => 50, + ), + 'metric' => NULL, + ), + 2 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\CouplingBetweenObjects', + 'namespaceName' => 'OCA\\OpenRegister\\Db', + 'className' => 'EntityRelationMapper', + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 49, + 'endLine' => 822, + 'description' => 'The class EntityRelationMapper has a coupling between objects value of 16. Consider to reduce the number of dependencies under 13.', + 'args' => + array ( + 0 => 'EntityRelationMapper', + 1 => 16, + 2 => 13, + ), + 'metric' => NULL, + ), + 3 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Db', + 'className' => 'EntityRelationMapper', + 'methodName' => 'buildRelationFromRow', + 'functionName' => NULL, + 'beginLine' => 434, + 'endLine' => 500, + 'description' => 'The method buildRelationFromRow() has a Cyclomatic Complexity of 23. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'buildRelationFromRow', + 2 => 23, + 3 => 10, + ), + 'metric' => NULL, + ), + 4 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Db', + 'className' => 'EntityRelationMapper', + 'methodName' => 'buildRelationFromRow', + 'functionName' => NULL, + 'beginLine' => 434, + 'endLine' => 500, + 'description' => 'The method buildRelationFromRow() has an NPath complexity of 419904. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'buildRelationFromRow', + 2 => '419904', + 3 => 200, + ), + 'metric' => NULL, + ), + 5 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 494, + 'endLine' => 496, + 'description' => 'The method buildRelationFromRow uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'buildRelationFromRow', + ), + 'metric' => NULL, + ), + 6 => + array ( + 'rule' => 'PHPMD\\Rule\\CyclomaticComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Db', + 'className' => 'EntityRelationMapper', + 'methodName' => 'updateDecisionMetadata', + 'functionName' => NULL, + 'beginLine' => 610, + 'endLine' => 743, + 'description' => 'The method updateDecisionMetadata() has a Cyclomatic Complexity of 17. The configured cyclomatic complexity threshold is 10.', + 'args' => + array ( + 0 => 'method', + 1 => 'updateDecisionMetadata', + 2 => 17, + 3 => 10, + ), + 'metric' => NULL, + ), + 7 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Db', + 'className' => 'EntityRelationMapper', + 'methodName' => 'updateDecisionMetadata', + 'functionName' => NULL, + 'beginLine' => 610, + 'endLine' => 743, + 'description' => 'The method updateDecisionMetadata() has an NPath complexity of 5616. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'updateDecisionMetadata', + 2 => '5616', + 3 => 200, + ), + 'metric' => NULL, + ), + 8 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\LongMethod', + 'namespaceName' => 'OCA\\OpenRegister\\Db', + 'className' => 'EntityRelationMapper', + 'methodName' => 'updateDecisionMetadata', + 'functionName' => NULL, + 'beginLine' => 610, + 'endLine' => 743, + 'description' => 'The method updateDecisionMetadata() has 134 lines of code. Current threshold is set to 100. Avoid really long methods.', + 'args' => + array ( + 0 => 'method', + 1 => 'updateDecisionMetadata', + 2 => 134, + 3 => 100, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Db/Register.php' => + array ( + 'hash' => '8f454d7094e726d43b7f5f22dd461a3ffdedcb44', + 'violations' => + array ( + ), + ), + 'lib/Db/ApprovalChainMapper.php' => + array ( + 'hash' => '40c5d9873c355e175f387019b821b62ae7155b00', + 'violations' => + array ( + ), + ), + 'lib/Db/EmailLink.php' => + array ( + 'hash' => '19a21231afa060bca52889ce51abf1763e292270', + 'violations' => + array ( + ), + ), + 'lib/Db/View.php' => + array ( + 'hash' => '812bfb17428b4d543a648567d884f0c5c0a3bff3', + 'violations' => + array ( + ), + ), + 'lib/Db/SourceMapper.php' => + array ( + 'hash' => '50f86fd893ff8e5f6961124e42dee89b6bcf50d6', + 'violations' => + array ( + ), + ), + 'lib/Db/ActionMapper.php' => + array ( + 'hash' => 'ee38f2141ddc0eec8630d0568dbfa9375f7c8584', + 'violations' => + array ( + ), + ), + 'lib/Db/AgentMapper.php' => + array ( + 'hash' => '115dc37bef7ee963abb91d4685df7e7469607a4a', + 'violations' => + array ( + ), + ), + 'lib/Db/DestructionListMapper.php' => + array ( + 'hash' => '5a2aa863f52153f1040b3ce94772c456764b1086', + 'violations' => + array ( + ), + ), + 'lib/Db/Message.php' => + array ( + 'hash' => '221d9e9864fc0158bcaedd4fb0bf9f8237697357', + 'violations' => + array ( + ), + ), + 'lib/Db/EmailLinkMapper.php' => + array ( + 'hash' => 'e983c504d3af43fe4e34294edfd5a86dc4283c2d', + 'violations' => + array ( + ), + ), + 'lib/Db/ViewMapper.php' => + array ( + 'hash' => '6e6cb16388f3f28eaf80a4bd26d78cb9a50561c9', + 'violations' => + array ( + ), + ), + 'lib/Db/GdprEntity.php' => + array ( + 'hash' => '261214bca62d1a5d55f479638b7d7a74c6b8b76c', + 'violations' => + array ( + ), + ), + 'lib/Db/WorkflowExecutionMapper.php' => + array ( + 'hash' => 'ac8c85aa5bd973c083475be3bcd4323c20de8bf8', + 'violations' => + array ( + ), + ), + 'lib/Db/Schema.php' => + array ( + 'hash' => '9b02e4275cd07223a463716c8687d397e9672055', + 'violations' => + array ( + ), + ), + 'lib/Db/DataAccessProfile.php' => + array ( + 'hash' => '1a02a13e8f01f83a7971befa3f3c8d1b6cf20737', + 'violations' => + array ( + ), + ), + 'lib/Db/WorkflowEngine.php' => + array ( + 'hash' => '0998860e95f9105133e267f44fc6174464dbd5cc', + 'violations' => + array ( + ), + ), + 'lib/Db/GdprEntityMapper.php' => + array ( + 'hash' => 'e92449fdbff0231fd667dac36f7d291dc340182c', + 'violations' => + array ( + ), + ), + 'lib/Db/ApprovalChain.php' => + array ( + 'hash' => 'a80087e4c99477985c48c184687294cf1d8dad09', + 'violations' => + array ( + ), + ), + 'lib/Db/Feedback.php' => + array ( + 'hash' => '101bba8231abbb6d67b1ffceacce304c9f04d02c', + 'violations' => + array ( + ), + ), + 'lib/Db/DeployedWorkflowMapper.php' => + array ( + 'hash' => '0e8faee204ad42c205f72fe8c4b076e35e2861da', + 'violations' => + array ( + ), + ), + 'lib/Db/ObjectEntity.php' => + array ( + 'hash' => 'b892eb2a75b57b78964917df58fe80ce266f344b', + 'violations' => + array ( + ), + ), + 'lib/Db/WorkflowExecution.php' => + array ( + 'hash' => 'c0abfc9690f3ebe6e10b61d24fc618045b5f35b4', + 'violations' => + array ( + ), + ), + 'lib/Db/TenantUsageMapper.php' => + array ( + 'hash' => 'bb7f1f1654ea4f723d40c093cf1ec25bf9243a5b', + 'violations' => + array ( + ), + ), + 'lib/Db/ActionLogMapper.php' => + array ( + 'hash' => 'c6356bea3f6863b0703ebde90b634c6b8dd53601', + 'violations' => + array ( + ), + ), + 'lib/Db/Mapping.php' => + array ( + 'hash' => '6d5bb8fd4ba8ee443daa841e7af92c727222eb7e', + 'violations' => + array ( + ), + ), + 'lib/Db/TimeTrackerLinkMapper.php' => + array ( + 'hash' => 'f701651a1325aef2c0ac39b47f8dcede187c62d2', + 'violations' => + array ( + ), + ), + 'lib/Db/NotificationReadStateMapper.php' => + array ( + 'hash' => '5800db7afb1f5de60735fefbddb954e823202c16', + 'violations' => + array ( + ), + ), + 'lib/Db/TalkLink.php' => + array ( + 'hash' => '6521a93d4aecd3f01ed85c6fab49d000c303c7e6', + 'violations' => + array ( + ), + ), + 'lib/Db/RealtimeEventMapper.php' => + array ( + 'hash' => '56bbe1126ad63bf98b129c1721a8c76a713e6392', + 'violations' => + array ( + ), + ), + 'lib/Db/FormLinkMapper.php' => + array ( + 'hash' => '553d6e695d6f1c30a382d77b3b4019d8648efd1e', + 'violations' => + array ( + ), + ), + 'lib/Db/AnalyticsLink.php' => + array ( + 'hash' => '0d3171f2f5b0e4c22db9cde9addde8aacde0871c', + 'violations' => + array ( + ), + ), + 'lib/Db/VerwerkingsactiviteitMapper.php' => + array ( + 'hash' => 'ea6c1c57f65f631ed6b012f4dcf23f2341fcd72c', + 'violations' => + array ( + ), + ), + 'lib/Db/Verwerkingsactiviteit.php' => + array ( + 'hash' => '0d8048cece1783a9bc961c6e31ad57f0e79a2c04', + 'violations' => + array ( + ), + ), + 'lib/Db/PhotoLink.php' => + array ( + 'hash' => 'd9d9db231bceaa3cbba7e1ed5f707f0deefd56f1', + 'violations' => + array ( + ), + ), + 'lib/Db/CollectiveLink.php' => + array ( + 'hash' => 'b767a87693a29a3f24f75ccf3078646fa7b3dd56', + 'violations' => + array ( + ), + ), + 'lib/Db/PhotoLinkMapper.php' => + array ( + 'hash' => 'bd914171d1855d0876ae8cdb363b8a62e32f6184', + 'violations' => + array ( + ), + ), + 'lib/Db/PollLink.php' => + array ( + 'hash' => '207c32680240a9243a429381800ac3865c74a293', + 'violations' => + array ( + ), + ), + 'lib/Db/OpenProjectLinkMapper.php' => + array ( + 'hash' => '3635400c031be85fb3d10b127883a702f51e1b84', + 'violations' => + array ( + ), + ), + 'lib/Db/NotificationHistoryMapper.php' => + array ( + 'hash' => '94ffdcee05536638b25d30a0a5b8dfc2299ed39b', + 'violations' => + array ( + ), + ), + 'lib/Db/TranslationMapper.php' => + array ( + 'hash' => '79838fa565a3d470d93a2a730f2b8fc8545deece', + 'violations' => + array ( + ), + ), + 'lib/Db/XwikiLinkMapper.php' => + array ( + 'hash' => 'cc46ed20e369a5349324678609160c36c6d85311', + 'violations' => + array ( + ), + ), + 'lib/Db/RealtimeEvent.php' => + array ( + 'hash' => '21dc133d7d189ce2e027bc71791b11f0a40658c5', + 'violations' => + array ( + ), + ), + 'lib/Db/NotificationDispatchLog.php' => + array ( + 'hash' => 'e685ea073f0a07a108259257dc67befacf48bedf', + 'violations' => + array ( + ), + ), + 'lib/Db/DuplicateDispatchException.php' => + array ( + 'hash' => 'b5f785460584e97df347ecfbe8aa73544173f95b', + 'violations' => + array ( + ), + ), + 'lib/Db/PollLinkMapper.php' => + array ( + 'hash' => '6b18cb966902e5623dd1e53b01b9d7e2d6e25c22', + 'violations' => + array ( + ), + ), + 'lib/Db/CospendLink.php' => + array ( + 'hash' => 'd22974055c9f6217ba974ba649f7d6a4c2ae1c73', + 'violations' => + array ( + ), + ), + 'lib/Db/DetectionMethod.php' => + array ( + 'hash' => '9eee48d81f119216b42ec63f6234aa01317534fa', + 'violations' => + array ( + ), + ), + 'lib/Db/FormLink.php' => + array ( + 'hash' => '217eedc32d6f5561dc1e30e321d9b7abb89dcd18', + 'violations' => + array ( + ), + ), + 'lib/Db/FlowLink.php' => + array ( + 'hash' => '4745074279bcfc12772aa4706e0b165bcd5ac7f4', + 'violations' => + array ( + ), + ), + 'lib/Db/OpenProjectLink.php' => + array ( + 'hash' => 'c93ef986a5b0b5d6f250423a3d965b68da4f86e4', + 'violations' => + array ( + ), + ), + 'lib/Db/BookmarkLinkMapper.php' => + array ( + 'hash' => '7fc77b35136311e04c9187107b9c3356b7b3d11b', + 'violations' => + array ( + ), + ), + 'lib/Db/CollectiveLinkMapper.php' => + array ( + 'hash' => 'bcd817e02072fd7243b358f7bf47307d73b2b83a', + 'violations' => + array ( + ), + ), + 'lib/Db/NotificationSubscription.php' => + array ( + 'hash' => '85a9137d22e64e975dcd59f860feab871367a176', + 'violations' => + array ( + ), + ), + 'lib/Db/NotificationHistory.php' => + array ( + 'hash' => '1e2510892edcef7c4c529e3a5844739ffbb1b253', + 'violations' => + array ( + ), + ), + 'lib/Db/MapLinkMapper.php' => + array ( + 'hash' => 'aede2d263d74b0b2907c1d17c2b56ecfff76e85c', + 'violations' => + array ( + ), + ), + 'lib/Db/BookmarkLink.php' => + array ( + 'hash' => '5aa64073e0be7e66f6cc9bb14cec17961a188ef9', + 'violations' => + array ( + ), + ), + 'lib/Db/MapLink.php' => + array ( + 'hash' => 'f634e9a3e9aaecf563979a19043f606ead4cf44d', + 'violations' => + array ( + ), + ), + 'lib/Db/NotificationSubscriptionMapper.php' => + array ( + 'hash' => '5147d265dd1564babb08b9728b876185b958d2c1', + 'violations' => + array ( + ), + ), + 'lib/Db/NotificationDispatchLogMapper.php' => + array ( + 'hash' => '6bf9c0031d5643ce75610c9cf40cd83d4b116607', + 'violations' => + array ( + ), + ), + 'lib/Db/XwikiLink.php' => + array ( + 'hash' => '5ab0e29fd577744c8eb6524a0acafc9ef24f7d57', + 'violations' => + array ( + ), + ), + 'lib/Db/CalendarLinkMapper.php' => + array ( + 'hash' => '936ad698f30d50bafdfb019d26bc8f96b72224f4', + 'violations' => + array ( + ), + ), + 'lib/Db/TimeTrackerLink.php' => + array ( + 'hash' => 'cb002ae839296c347c3baa72065d0253b89e4694', + 'violations' => + array ( + ), + ), + 'lib/Db/Translation.php' => + array ( + 'hash' => '175de987fccff72f954f6597ac9804e29563d40d', + 'violations' => + array ( + ), + ), + 'lib/Db/TalkLinkMapper.php' => + array ( + 'hash' => '9883d3600df4ddfb028a6e240aebbfafeaa85a07', + 'violations' => + array ( + ), + ), + 'lib/Db/CospendLinkMapper.php' => + array ( + 'hash' => '51e250652640134271e7bf671ffb55bee89bba19', + 'violations' => + array ( + ), + ), + 'lib/Db/File.php' => + array ( + 'hash' => '48ea6b10c1275c1dda4623c7c1038b8ff98e0ef7', + 'violations' => + array ( + ), + ), + 'lib/Db/FlowLinkMapper.php' => + array ( + 'hash' => '95615b31519fa6fbf9fd261da898df19ab2e7281', + 'violations' => + array ( + ), + ), + 'lib/Db/AnalyticsLinkMapper.php' => + array ( + 'hash' => 'c950af5f0626d215c345a5502a32981ce9eb4a63', + 'violations' => + array ( + ), + ), + 'lib/Db/NotificationReadStateEntity.php' => + array ( + 'hash' => 'cda1c15d2eef808e53f86ce0047afbaebfdecc4b', + 'violations' => + array ( + ), + ), + 'lib/Db/CalendarLink.php' => + array ( + 'hash' => 'f3c4ef6a7b4a8c9f9825b24da3fd2c231af7b5b5', + 'violations' => + array ( + ), + ), + 'lib/Listener/ObjectChangeListener.php' => + array ( + 'hash' => 'f71b768092cd8d3e141fc96842113550290df2d5', + 'violations' => + array ( + ), + ), + 'lib/Listener/ActivityEventListener.php' => + array ( + 'hash' => 'effb28521d0d5301c1152740164e6faf75bef5cc', + 'violations' => + array ( + ), + ), + 'lib/Listener/FileChangeListener.php' => + array ( + 'hash' => '64734b4d04520ad9712df6a1550b73ff1be15f13', + 'violations' => + array ( + ), + ), + 'lib/Listener/ActionListener.php' => + array ( + 'hash' => '4cd3ba85c449294e778604e291d3a411d43c64fe', + 'violations' => + array ( + ), + ), + 'lib/Listener/CommentsEntityListener.php' => + array ( + 'hash' => '31db7392d41f40e0b2dd7604c9dd6a730bd5c523', + 'violations' => + array ( + ), + ), + 'lib/Listener/HookListener.php' => + array ( + 'hash' => '768a753a551e265ad8a0922224c95b116ceaf569', + 'violations' => + array ( + ), + ), + 'lib/Listener/GraphQLSubscriptionListener.php' => + array ( + 'hash' => '0dc7d64f403211c12b73c03e0a81db13ecbad5af', + 'violations' => + array ( + ), + ), + 'lib/Listener/MailAppScriptListener.php' => + array ( + 'hash' => '234f732e8b052e8645876460b0c067535deffa82', + 'violations' => + array ( + ), + ), + 'lib/Listener/WebhookEventListener.php' => + array ( + 'hash' => 'a4d71c50e5a0831be5c567cc10e28f2402a926f0', + 'violations' => + array ( + ), + ), + 'lib/Listener/ToolRegistrationListener.php' => + array ( + 'hash' => '1585fe9f855f7cb878f178e819c43fcd183d8988', + 'violations' => + array ( + ), + ), + 'lib/Listener/FilesSidebarListener.php' => + array ( + 'hash' => 'f86363ba089ed5a04192a78cf03af6445f9134ed', + 'violations' => + array ( + ), + ), + 'lib/Listener/ObjectCleanupListener.php' => + array ( + 'hash' => '75bb1a68d69dab40d92c093bd84328b9d4eab85e', + 'violations' => + array ( + ), + ), + 'lib/Listener/AggregationThresholdListener.php' => + array ( + 'hash' => 'b2df113051c9cbf9cd2579c7ac96a8fbc49057ef', + 'violations' => + array ( + ), + ), + 'lib/Listener/CalculationOnSaveListener.php' => + array ( + 'hash' => '9f3697688d09b88508eddcc4c53ebd86a9cf488e', + 'violations' => + array ( + ), + ), + 'lib/Listener/IntegrationGlobalScriptListener.php' => + array ( + 'hash' => '0e5b955a58a017686eeba2641df565c14fb615d1', + 'violations' => + array ( + ), + ), + 'lib/Listener/AggregationCacheInvalidationListener.php' => + array ( + 'hash' => '61c5d92bb6b6c0321c6165a8e9f91cea814c4621', + 'violations' => + array ( + ), + ), + 'lib/Listener/LifecycleInitialStateListener.php' => + array ( + 'hash' => '0b158efb20363ecb5f2360563864829c51c79115', + 'violations' => + array ( + ), + ), + 'lib/Listener/RealtimeEventListener.php' => + array ( + 'hash' => 'a65fa4259903e8304b78be3c923eef42e6501ebb', + 'violations' => + array ( + ), + ), + 'lib/Listener/AnnotationNotificationListener.php' => + array ( + 'hash' => '153aacf677c22476364fb77c2b97a1920cacb048', + 'violations' => + array ( + ), + ), + 'lib/Listener/SystemEntityNotificationListener.php' => + array ( + 'hash' => '19536e26bb393b41233749d915945b523cd84c5f', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\Design\\NpathComplexity', + 'namespaceName' => 'OCA\\OpenRegister\\Listener', + 'className' => 'SystemEntityNotificationListener', + 'methodName' => 'extractEventData', + 'functionName' => NULL, + 'beginLine' => 142, + 'endLine' => 196, + 'description' => 'The method extractEventData() has an NPath complexity of 7776. The configured NPath complexity threshold is 200.', + 'args' => + array ( + 0 => 'method', + 1 => 'extractEventData', + 2 => '7776', + 3 => 200, + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Listener/NotifyPushListener.php' => + array ( + 'hash' => 'def4025c50c2d072d66d2ee9f3d4bc4910dac251', + 'violations' => + array ( + ), + ), + 'lib/Listener/LifecycleValidationListener.php' => + array ( + 'hash' => '34c52351568a4c529a9002f16f5419985b0e5a3d', + 'violations' => + array ( + ), + ), + 'lib/Listener/TranslationProjectionListener.php' => + array ( + 'hash' => '32f6d8670f7847b159a8708c0fe8a74b09ab31ac', + 'violations' => + array ( + ), + ), + 'lib/EventListener/AbstractNodeFolderEventListener.php' => + array ( + 'hash' => '3061f4f97c7864f7b99c41ef33a24f68a4648ffc', + 'violations' => + array ( + ), + ), + 'lib/EventListener/AbstractNodesFolderEventListener.php' => + array ( + 'hash' => 'f58336abdcec8f72775febd2327b6d0dae69c899', + 'violations' => + array ( + ), + ), + 'lib/EventListener/SolrEventListener.php' => + array ( + 'hash' => 'b4dce8e1332089e5ecf343b8ba45040abaf35e5a', + 'violations' => + array ( + ), + ), + 'lib/Search/ObjectsProvider.php' => + array ( + 'hash' => 'a4a462122036d3417b142be918130c388e4f619d', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 469, + 'endLine' => 471, + 'description' => 'The method resolveSchemaName uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'resolveSchemaName', + ), + 'metric' => NULL, + ), + 1 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 498, + 'endLine' => 500, + 'description' => 'The method resolveRegisterName uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => 'resolveRegisterName', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Command/SolrDebugCommand.php' => + array ( + 'hash' => '8576edca850816a1c77d21695b0abdd30db73220', + 'violations' => + array ( + ), + ), + 'lib/Command/SolrManagementCommand.php' => + array ( + 'hash' => 'f28eae34442231050d595592e03e290d2f991895', + 'violations' => + array ( + ), + ), + 'lib/Command/MigrateStorageCommand.php' => + array ( + 'hash' => '58c81f9f7e1e259f1ab9a125d1aa01ab650cad10', + 'violations' => + array ( + ), + ), + 'lib/Command/RematerialiseCalculationsCommand.php' => + array ( + 'hash' => '12b8af191ef18fc7f6e945e872144c6434df92e0', + 'violations' => + array ( + ), + ), + 'lib/Command/BackfillSystemOwnerCommand.php' => + array ( + 'hash' => '47873b51278f57b512aac277dc27e0000a961fac', + 'violations' => + array ( + ), + ), + 'lib/Contacts/ContactsMenuProvider.php' => + array ( + 'hash' => 'dfce7a9e027a2c924bf245478c51061d5c5d6887', + 'violations' => + array ( + ), + ), + 'lib/Sections/OpenRegisterAdmin.php' => + array ( + 'hash' => 'd41ebabd32cb05ac68ae52a4eaa8ca442cf4d2c8', + 'violations' => + array ( + ), + ), + 'lib/Exception/CustomValidationException.php' => + array ( + 'hash' => 'cca7fc330b3521cb38a22580f90f61c871fb182c', + 'violations' => + array ( + ), + ), + 'lib/Exception/RegisterNotFoundException.php' => + array ( + 'hash' => '8eaa04213ab69ca7ac5217190f67386c69a9643c', + 'violations' => + array ( + ), + ), + 'lib/Exception/SchemaNotFoundException.php' => + array ( + 'hash' => '2d78560466f663b6f15d0b98e8ec577412cdef8a', + 'violations' => + array ( + ), + ), + 'lib/Exception/AuthenticationException.php' => + array ( + 'hash' => 'c2c4f5dd7d35905abd314ab186de745a573c8adc', + 'violations' => + array ( + ), + ), + 'lib/Exception/NotAuthorizedException.php' => + array ( + 'hash' => 'dd1107de79d478083125b63a9c3d1807dbefb8e2', + 'violations' => + array ( + ), + ), + 'lib/Exception/ValidationException.php' => + array ( + 'hash' => '6f093b2595b5faed53a40b7cf279fe8fb96129df', + 'violations' => + array ( + ), + ), + 'lib/Exception/LockedException.php' => + array ( + 'hash' => '4ead50d774ddef742de3f3c9b492f12d1ac017d8', + 'violations' => + array ( + ), + ), + 'lib/Exception/ReferentialIntegrityException.php' => + array ( + 'hash' => '40fd3e745c344116f5eeacfa5942c36758120874', + 'violations' => + array ( + ), + ), + 'lib/Exception/NoVtodoCalendarException.php' => + array ( + 'hash' => '1b26a7da5b7d7cc2e0a1e9527cc5caa5c422be92', + 'violations' => + array ( + ), + ), + 'lib/Exception/HookStoppedException.php' => + array ( + 'hash' => 'f28c01b628543f30cfd60547083ec211054d7bd3', + 'violations' => + array ( + ), ), - 'lib/Controller/SearchTrailController.php' => + 'lib/Exception/DatabaseConstraintException.php' => array ( - 'hash' => '42ad8f30b05037a79c1bce22d3d3077b4f86514b', + 'hash' => 'b9911b2ef387d450cabcb369f03b6bb7a18a4cf1', + 'violations' => + array ( + ), + ), + 'lib/Exception/FolderAccessDeniedException.php' => + array ( + 'hash' => '932c210156ec471238a35a315d7bb7202ceaedcc', + 'violations' => + array ( + ), + ), + 'lib/Exception/SanitizationException.php' => + array ( + 'hash' => '5ed1bca0573b045f1d61d7b326a36f6d50251999', + 'violations' => + array ( + ), + ), + 'lib/Exception/ArchivalImmutableException.php' => + array ( + 'hash' => 'f676ae93ad5445f04c9e4b51db43aa1406cab24e', + 'violations' => + array ( + ), + ), + 'lib/Exception/PdfAnonymisationException.php' => + array ( + 'hash' => 'e4163f98a56c6b812871d86d6e9bca97f6ef9587', + 'violations' => + array ( + 0 => + array ( + 'rule' => 'PHPMD\\Rule\\CleanCode\\ElseExpression', + 'namespaceName' => NULL, + 'className' => NULL, + 'methodName' => NULL, + 'functionName' => NULL, + 'beginLine' => 138, + 'endLine' => 140, + 'description' => 'The method __construct uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them.', + 'args' => + array ( + 0 => '__construct', + ), + 'metric' => NULL, + ), + ), + ), + 'lib/Exception/AppendOnlyException.php' => + array ( + 'hash' => 'fe110c753eca84949ee657d517274f0f5a9bcf05', + 'violations' => + array ( + ), + ), + 'lib/Exception/ProviderUnavailableException.php' => + array ( + 'hash' => 'ab44a7c1c5a9ded933c720303e8e1c59b000117d', + 'violations' => + array ( + ), + ), + 'lib/Exception/NotImplementedException.php' => + array ( + 'hash' => '57f1da7359ecb2759ab84609c1b4d79314c7f5f5', + 'violations' => + array ( + ), + ), + 'lib/Exception/ChunkMatcherException.php' => + array ( + 'hash' => 'bad7456d88fd10ce866ed548dcdedf85ac03f4f9', + 'violations' => + array ( + ), + ), + 'lib/Exception/EmlParseException.php' => + array ( + 'hash' => '50c793ece2ab80c74cbf6a8e3e46532ec63895b7', + 'violations' => + array ( + ), + ), + 'lib/Exception/ReferenceValidationException.php' => + array ( + 'hash' => 'f1a9e305ae69a851b63cfabf64a3ca6175f3e988', + 'violations' => + array ( + ), + ), + 'lib/Exception/CircularReferenceException.php' => + array ( + 'hash' => '5e0acd6e3c95f1b951b0314f9813b472df6ba9ef', + 'violations' => + array ( + ), + ), + 'lib/Exception/ManualEntityException.php' => + array ( + 'hash' => '08d346182c884827c612b656693b9092f3cac64f', + 'violations' => + array ( + ), + ), + 'lib/Exception/OasValidationException.php' => + array ( + 'hash' => '056d950c3c99037c126dcf7f6372109fc2de45f8', + 'violations' => + array ( + ), + ), + 'lib/Tool/ObjectsTool.php' => + array ( + 'hash' => '8a71b63391345a006211bcd385a0988937fae3b8', + 'violations' => + array ( + ), + ), + 'lib/Tool/AgentTool.php' => + array ( + 'hash' => '54e23460a3a3e11fd2cf5011da7101ea212a5d4e', + 'violations' => + array ( + ), + ), + 'lib/Tool/AbstractTool.php' => + array ( + 'hash' => '55ac14c154b91c5a913bc4ab57373d9b36ea9610', + 'violations' => + array ( + ), + ), + 'lib/Tool/RegisterTool.php' => + array ( + 'hash' => '811273fdda7e398f94b06c1bf201f977e6f3e7e6', + 'violations' => + array ( + ), + ), + 'lib/Tool/ToolInterface.php' => + array ( + 'hash' => '32658bf81a8271018ab1817b28970ea0b963462c', + 'violations' => + array ( + ), + ), + 'lib/Tool/ApplicationTool.php' => + array ( + 'hash' => '64ca42783b4540fa1461ba6740fb6fbe03c8df5a', + 'violations' => + array ( + ), + ), + 'lib/Tool/SchemaTool.php' => + array ( + 'hash' => 'c7df52b76ed6a09534914dc5c0b3e7f38200c167', + 'violations' => + array ( + ), + ), + 'lib/Tool/StreamingToolInstanceWrapper.php' => + array ( + 'hash' => '16330a974976caedbc377b300f1813734b068e43', + 'violations' => + array ( + ), + ), + 'lib/Tool/McpProviderBridge.php' => + array ( + 'hash' => 'cf4a4b1374b2cfe699cabb5fe291b874d5fe8594', + 'violations' => + array ( + ), + ), + 'lib/Reference/ObjectReferenceProvider.php' => + array ( + 'hash' => '6afc14b5248e1b6b64275a3150f49ce87a1f8b10', + 'violations' => + array ( + ), + ), + 'lib/WorkflowEngine/WorkflowResult.php' => + array ( + 'hash' => '600b6cdfa37ae4cdd3f9ebf5ee5600680f833a56', + 'violations' => + array ( + ), + ), + 'lib/WorkflowEngine/N8nAdapter.php' => + array ( + 'hash' => '330f77a9832f5c624599ee53f4f9ceedc9e38cfd', + 'violations' => + array ( + ), + ), + 'lib/WorkflowEngine/WorkflowEngineInterface.php' => + array ( + 'hash' => 'd0a39f89612326b875ef6e07ab4a908d5d18facb', + 'violations' => + array ( + ), + ), + 'lib/WorkflowEngine/WindmillAdapter.php' => + array ( + 'hash' => '5413b60c1649948ea1b97dac10a218db268d3269', + 'violations' => + array ( + ), + ), + 'lib/Twig/AuthenticationRuntime.php' => + array ( + 'hash' => 'cfda4e87fc0c29c93fe82611f9606d342fcb6933', + 'violations' => + array ( + ), + ), + 'lib/Twig/MappingExtension.php' => + array ( + 'hash' => 'c4675134c46d79fbb61373f39d17499e79197d99', + 'violations' => + array ( + ), + ), + 'lib/Twig/MappingRuntime.php' => + array ( + 'hash' => '520c45845bd32cd8af1e8460766c208cd48ac92c', + 'violations' => + array ( + ), + ), + 'lib/Twig/AuthenticationExtension.php' => + array ( + 'hash' => '6c9fd4c39e06e1872ca532cf4d524989b5beb6de', + 'violations' => + array ( + ), + ), + 'lib/Twig/MappingRuntimeLoader.php' => + array ( + 'hash' => '92734e2a75cf674acab60ecc4cd4bf6928d896b8', + 'violations' => + array ( + ), + ), + 'lib/Notification/Notifier.php' => + array ( + 'hash' => '1e2e0b7f1b28d2ce2c5e922c512af37c9b7e0bd0', + 'violations' => + array ( + ), + ), + 'lib/Notification/AnnotationNotifier.php' => + array ( + 'hash' => 'ae4194fd5376bf4fbdad5755e6570937890bc587', + 'violations' => + array ( + ), + ), + 'lib/Calendar/RegisterCalendar.php' => + array ( + 'hash' => '01875cef48119a531e1c787acfba00b99244d541', + 'violations' => + array ( + ), + ), + 'lib/Calendar/CalendarEventTransformer.php' => + array ( + 'hash' => '7a0f271b7a2de8206ad29a96f77bcc8b27e57615', + 'violations' => + array ( + ), + ), + 'lib/Calendar/RegisterCalendarProvider.php' => + array ( + 'hash' => '98658cc610a40b62e13938567928ab5969113ffe', + 'violations' => + array ( + ), + ), + 'lib/Settings/OpenRegisterAdmin.php' => + array ( + 'hash' => '25ac71a53852a12042b6b8c339bdcaac9b96ec19', + 'violations' => + array ( + ), + ), + 'lib/Settings/IntegrationsAdminSettings.php' => + array ( + 'hash' => '33fa17dc667c6ed1363c6544d4153c6e87b668c5', + 'violations' => + array ( + ), + ), + 'lib/Capabilities/IntegrationsCapability.php' => + array ( + 'hash' => '78e0f865cf72dd36ad51323cd8141b2f7a8efb8f', + 'violations' => + array ( + ), + ), + 'lib/Capabilities/UrnCapability.php' => + array ( + 'hash' => '6610abfef7bf02f7c9972c07d773927593130dbf', + 'violations' => + array ( + ), + ), + 'lib/Lifecycle/LifecycleGuardInterface.php' => + array ( + 'hash' => '2b2f09bd96f50ce8c238bf4fec4149c0e5a5ebc9', + 'violations' => + array ( + ), + ), + 'lib/Lifecycle/GuardResult.php' => + array ( + 'hash' => '64b539d8b76d7a1872567dbc0d635f6a94c84147', + 'violations' => + array ( + ), + ), + 'lib/Mcp/IMcpToolProvider.php' => + array ( + 'hash' => '1c6b78afd9a449e5c89dc3773021c9a058d38f01', + 'violations' => + array ( + ), + ), + 'lib/Mcp/BuiltIn/SchemasToolProvider.php' => + array ( + 'hash' => 'd967d69514b9ea87e9c84623c3b97db1945e6b08', + 'violations' => + array ( + ), + ), + 'lib/Mcp/BuiltIn/IntegrationsToolProvider.php' => + array ( + 'hash' => '2d958d9d956ef07cd00014d0c77706392f0902b4', + 'violations' => + array ( + ), + ), + 'lib/Mcp/BuiltIn/ObjectsToolProvider.php' => + array ( + 'hash' => '69cb6871ed1115679435c4934efbbc5dccd0279c', + 'violations' => + array ( + ), + ), + 'lib/Mcp/BuiltIn/RegistersToolProvider.php' => + array ( + 'hash' => 'db68124be1e93c9eebdd223fce523fe6cb1c31a2', + 'violations' => + array ( + ), + ), + 'lib/Push/PushEvents.php' => + array ( + 'hash' => '7499712195cfc8e5668fc5d6cd3522f8a64e7672', + 'violations' => + array ( + ), + ), + 'lib/Dashboard/IntegrationDashboardWidget.php' => + array ( + 'hash' => 'a286b8d1bc1ab024d57034bb94ac253c921e2d64', 'violations' => array ( ), diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results deleted file mode 100644 index 68c7da0160..0000000000 --- a/.phpunit.cache/test-results +++ /dev/null @@ -1 +0,0 @@ -{"version":2,"defects":{"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testActiveOrganisationAutoSelectionForUserWithNoOrganisations":1,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationCachingTest::testDefaultOrganisationCacheOnFirstTimeCreation":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#simple_name":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#name_with_hyphens":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#name_with_spaces":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#name_with_special_chars":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#numeric_start":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#consecutive_underscores":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#trailing_underscores":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableExistenceCheckingWithCaching":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testPrivateTableExistsMethodWithCaching":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaVersionCalculation":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testGetExistingSchemaTables":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableCreationWorkflow":1,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableCreationErrorHandling":1,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testCreateException":8,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testUpdateException":8,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testFindApplicableExceptions":8,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testFindBySubject":8,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testFindByUuid":8,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testCountByCriteria":8,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testDeactivateException":8,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testActivateException":8,"OCA\\OpenRegister\\Tests\\Db\\AuthorizationExceptionMapperTest::testDeleteByUuid":8,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperTest::testFindAllWithPublishedFilter":8,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperTest::testGetStatisticsPublishedCount":8,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperTest::testRegisterDeleteThrowsIfObjectsAttached":8,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperTest::testGetRegisterCountPerSchemaEmpty":8,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperTest::testGetRegisterCountPerSchemaMultiple":8,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperTest::testGetRegisterCountPerSchemaZeroForUnreferenced":8,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvWithBatchSaving":8,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvWithErrors":8,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvWithEmptyFile":8,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvAsync":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testEnsureRelationIndexBuildsFromSchemas":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexExcludesNoAction":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexExcludesNoOnDelete":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexHandlesArrayRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexResolvesSlugRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testMultipleSchemasReferencingSameTarget":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexIsCached":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteNoIncomingReferences":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteDetectsCascade":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteDetectsRestrict":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetNullNonRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetNullOnRequiredFallsBackToRestrict":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultWithDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultNoDefaultFallsToSetNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultNoDefaultRequiredFallsToRestrict":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteRestrictWithNoDependentsAllowsDeletion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteChainedCascade":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteChainedCascadeIntoRestrict":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSkipsAlreadyDeletedDependents":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteCircularReferenceDetection":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteMixedActionTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testHasIncomingReferencesReturnsFalseForUnreferenced":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testHasIncomingReferencesReturnsTrueForReferenced":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsExecutionOrder":8,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesHs256Token":1,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesHs384Token":1,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesHs512Token":1,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenWithTwigPayloadTemplate":1,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenWithX5tHeader":1,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testPrepareObjectsForSaveSingleSchemaPath":1,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectThrowsWhenNoUser":1,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectThrowsWhenNoCalendar":1,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectReturnsEmptyWhenNoTasks":1,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectReturnsMatchingTasks":1,"Unit\\Service\\TaskServiceTest::testCreateTaskCreatesCalendarObject":1,"Unit\\Service\\TaskServiceTest::testUpdateTaskThrowsWhenNotFound":1,"Unit\\Service\\TaskServiceTest::testUpdateTaskUpdatesFields":1,"Unit\\Service\\TaskServiceTest::testDeleteTaskThrowsWhenNotFound":1,"Unit\\Service\\TaskServiceTest::testDeleteTaskDeletesCalendarObject":1,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayRequiresOcClass":1,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesRequiresOcClass":1,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testSetExpiryDateUpdatesNullExpires":1,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSetExpiryDate":1,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetObjectReturnsCachedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetObjectReturnsNullOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetObjectByUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetObjectWithStringId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsReturnsEmptyForEmptyInput":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsBulkLoadsFromDatabase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsSkipsAlreadyCachedObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsReturnsEmptyOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsWithDuplicateIdentifiers":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsUpdatesStats":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsMixedCachedAndUncached":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsReturnsInitialStats":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsTracksHitsAndMisses":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsTracksNameHitsAndMisses":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsCacheSizeReflectsLoadedObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsNameCacheSizeReflectsSetNames":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsQueryCacheSize":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsNameHitRate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsIncludesDistributedNameCacheSize":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCacheClearsAll":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCacheWithPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCacheHandlesDistributedCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCacheWithoutDistributedCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeWithObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeWithNullObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeOnDelete":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeOnUpdate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeRemovesObjectFromCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteRemovesNameFromDistributedCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteHandlesDistributedCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateUpdatesNameCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeWithNullSchemaAndRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeWithExplicitRegisterAndSchemaIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateWithIndexService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeUpdateWithIndexService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateWithIndexFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateWithIndexUnavailable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteWithIndexService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteWithSolrFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteWithSolrException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateUsesUuidWhenNameNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeNullObjectNullSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeSchemaDistributedCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCachesResetsStats":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCachesClearsNameCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCachesHandlesDistributedQueryCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCachesHandlesDistributedNameCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetAndGetObjectName":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameReturnsNullForUnknownWhenDbFails":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFromDistributedCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameHandlesDistributedCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFindsOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFindsObjectViaFindAcrossAllSources":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameUsesUuidWhenNameIsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetObjectNameWithIntIdentifier":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetObjectNameEnforcesMaxTtl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetObjectNameHandlesDistributedCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetObjectNameWithTtlWithinLimit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFindsOrganisationWithNullName":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFindAcrossAllSourcesNullObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesReturnsEmptyForEmptyInput":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesReturnsCachedNames":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesChecksDistributedCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesFallsBackToOrganisationMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesFallsBackToObjectMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesHandlesDbException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesFiltersToUuidOnlyResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesHandlesDistributedCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesObjectWithNullNameUsesUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesMatchByNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesOrgWithNullNameUsesUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesBatchLoadsMagicTables":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetAllObjectNamesTriggersWarmupWhenEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetAllObjectNamesWithForceWarmup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetAllObjectNamesSkipsWarmupWhenCachePopulated":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetAllObjectNamesFiltersToUuidKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheLoadsOrganisationsAndObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheOrganisationsTakePriority":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheUpdatesStats":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheSkipsObjectsWithNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheSkipsOrganisationsWithNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheWithMagicTables":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheWithMagicMappingEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheWithMagicTableQueryException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheWithMagicTablesOuterException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheSchemaFindException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearNameCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearNameCacheClearsDistributedCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearNameCacheHandlesDistributedCacheException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testConstructorWithoutCacheFactory":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testConstructorWithCacheFactoryException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testConstructorWithNullCacheFactoryAndUserSession":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetDistributedNameCacheCount":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetDistributedNameCacheCountReturnsZeroWhenNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetDistributedNameCacheCountHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetDistributedNameCacheCountWithoutDistributedCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSolrDashboardStatsThrowsWithoutContainer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCommitSolrReturnsErrorWithoutContainer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testOptimizeSolrReturnsErrorWithoutContainer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSolrIndexForDashboardReturnsErrorWithoutContainer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCommitSolrSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCommitSolrFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testOptimizeSolrSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testOptimizeSolrFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testOptimizeSolrException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSolrIndexForDashboardSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSolrIndexForDashboardFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSolrIndexForDashboardException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSolrDashboardStatsWithService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetIndexServiceReturnsNullOnContainerException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCacheEvictionWhenExceedingMaxSize":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCacheObjectWithNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPersistNameCacheToDistributedStoresEntries":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPersistNameCacheToDistributedWithoutCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPersistNameCacheToDistributedHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesNonUuidIdentifiers":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesMappingDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesAllColumnsFail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesSkipsNullAndEmptyNames":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSchemaRelatedCachesWithSchemaId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSchemaRelatedCachesWithoutSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectWithPrefix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectArrayWithMixedValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testIsDateString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testFormatDateForSolr":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testFormatDateForSolrInvalidDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesEmptyUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPersistNameCacheToDistributedMetadataFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesMatchBySlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesMatchByUri":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectStringIsHandledAsString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectIntegerSuffix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectFloatSuffix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsCalculatesQueryHitRate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCachePatternFiltersInMemoryCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesWithValidResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesStopsWhenAllFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesColumnFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesReturnsNameDirectly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesIntegratesBatchResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheStoresNameFromMagicTableRow":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCommitSolrException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetCreatedSubObjectsReturnsEmptyArrayInitially":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testTrackCreatedSubObjectAddsSubObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testTrackMultipleSubObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearCreatedSubObjectsClearsSubObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearAllCachesClearsEverything":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithEmptyData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsFindsUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsFindsUrls":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsFindsNumericIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsSkipsNonStringKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsSkipsPlainTextValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithPrefixedUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithUuidWithoutDashes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithNestedArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithArrayOfUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithPrefix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsSkipsEmptyStrings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithSchemaPropertyTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithTextUuidFormatProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithArrayOfObjectsSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithStandardUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithUuidWithoutDashes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithPrefixedUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithPrefixedUuidNoDashes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithPlainText":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithCommonWord":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithOpenSource":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithIdentifierLikeString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testRemoveQueryParametersWithNoParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testRemoveQueryParametersStripsParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathSimple":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathNested":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathDeeplyNested":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathReturnsNullForMissingKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathReturnsNullForMissingNestedKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathConvertsIntToString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathReturnsNullForNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugBasic":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugWithSpecialChars":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugTrimsHyphens":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugLimitsLength":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugWithNumbers":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithAllEmptyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithNonEmptyValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectSkipsMetadataKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithNestedEmptyObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithNestedNonEmptyObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNonEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNumber":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithZero":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithBoolean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithIndexedArrayAllEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithIndexedArraySomeNonEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultAlwaysBehavior":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultAlwaysBehaviorEvenWithValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultDefaultBehaviorWithMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultDefaultBehaviorWithNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultDefaultBehaviorWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultDefaultBehaviorWithValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueNonTemplate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueSimplePropertyRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueSimpleRefPreservesArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueSimpleRefMissingProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueComplexTemplate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueNonStringValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueNullOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsAuditTrailsEnabledReturnsTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsAuditTrailsEnabledReturnsFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsAuditTrailsEnabledDefaultsToTrueOnMissingSetting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsAuditTrailsEnabledDefaultsToTrueOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsNoAlwaysProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsAppliesAlwaysDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsSkipsNullDefaultValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsWithTemplateValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsReturnsDataWhenNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsReturnsDataOnSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsAppliesDefaultWhenMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsDoesNotOverrideExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsWithFalsyBehavior":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsWithAlwaysBehavior":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsReturnsDataOnSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsReturnsDataWhenNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataDelegatesToHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageFieldString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageFieldFileObjectsDownloadUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageFieldFileObjectsAccessUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithPublishedField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithInvalidPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithDepublishedField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithInvalidDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithEmptyPublishedValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithNumericImageValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataWithSelfMetadata":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataWithExplicitUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataWithIdInData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataNormalizesEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataProcessesUploadedFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataNoUploadedFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithIntIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithNullRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithSlugField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithNoSlugField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithMissingFieldValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithNestedField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceCachedResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceBySlugViaFindAll":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceByPathReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceWithQueryParameters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceNotFoundCachesNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceBySlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceByUrlPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsSlugFromSelfData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsSlugFromData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsPublishedToNullWhenEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataHandlesInvalidPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsDepublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsDepublishedToNullWhenMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsOwner":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetCachedSchemaFetchesAndCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetCachedRegisterFetchesAndCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateObjectRelationsSetsRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectReturnsNullWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectReturnsEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectThrowsOnLockedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectAllowsLockOwner":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForObjectPropertiesWithObjectProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForObjectPropertiesEmptyObjectNonRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForObjectPropertiesEmptyObjectRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForArrayPropertyWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForArrayPropertyWithEmptyStringItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForScalarPropertyNonRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForScalarPropertyRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsReturnsDataOnSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsSkipsMissingProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFillMissingSchemaPropertiesWithNullAddsNullForMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFillMissingSchemaPropertiesDoesNotOverrideExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFillMissingSchemaPropertiesReturnsDataOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameCachesName":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameReturnsEarlyOnNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameFallsBackToNaam":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameHandlesHydrationException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearImageMetadataIfFilePropertyClearsImage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearImageMetadataIfFilePropertyDoesNotClearNonFileProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearImageMetadataIfNoImageFieldConfigured":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesAppliesDefaultWhenMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesDoesNotOverrideExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithConstValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithFalsyBehavior":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithAlwaysBehavior":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithTwigSimpleRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithTwigComplexTemplate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesReturnsDataWhenNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesReturnsDataOnSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesGeneratesSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesSkipsSlugWhenAlreadyPresent":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesNonTemplateValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsWhenEmptyProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsPropertyWithoutValidateReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsNullValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsUnchangedValueOnUpdate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsSoftDeletes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsHandlesDoesNotExist":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsHandlesGenericException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsUsesSystemUserWhenNoUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsReturnsEarlyWhenNoRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsNonUuidRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenNoPropertyConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsNullRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsReturnsEmptyForNonList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsReturnsEmptyForEmptyValidObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsSkipsExistingUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsReturnsEmptyWhenNoRefInItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsCopiesRefFromPropertyLevel":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectReturnsNullWithNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectReturnsNullForEmptyObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectReturnsNullForEmptyIdOnly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectReturnsNullWhenParentHasNoUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectThrowsOnInvalidSchemaRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleObjectUpdateReturnsUnpersistedWhenPersistFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleObjectCreationReturnsUnpersistedWhenPersistFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testProcessFilePropertiesWithRollbackNoFileProps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testProcessFilePropertiesWithRollbackDeletesOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeObjectsReturnsDataOnSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeObjectsSkipsPropertiesNotInData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeObjectsSkipsEmptyPropertyValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeObjectsSkipsWriteBackEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleInverseRelationsWriteBackReturnsDataOnSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleInverseRelationsWriteBackSkipsEmptyPropertyValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleInverseRelationsWriteBackRemovesAfterWriteBack":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectAllowsNonArrayLock":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectAllowsNullLockOwner":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithStringSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterThrowsOnInvalidStringSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsUpdatesRelatedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenAlreadyRelated":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsNoRefInProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsHandlesSchemaResolutionFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsUsesItemsRefForArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceByUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceDirectFindFallsToSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsDepublishedToNullWhenEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataHandlesInvalidDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithArrayOfFileIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataPublishedFieldNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataDepublishedFieldNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithZero":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithArrayOfObjectsAndNestedArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithTextUriFormatProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithTextUrlFormatProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithShortHyphenatedString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithCommonWordClosedSource":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithUnderscoreId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesUsesMergedContextForTwig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesNullTemplateSourceMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithStringRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataPrefersExplicitUuidOverSelfId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataWithNonArraySelf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceCleanedCachedResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceDirectSlugMatchAsLastResort":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceDirectSlugLastResort":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataPreservesPublishedWhenNotInSelfData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageArrayOfNumericIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageFieldNullValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithEmptyDepublishedValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithNullPublishedValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithSystemsoftwareCommonWord":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithClosedSource":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithShortString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsWithTemplateValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsSkipsNullDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsWithNullResolvedValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsReturnsWhenSchemaCannotBeResolved":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsThrowsValidationExceptionWhenObjectNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsPassesWhenObjectFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsLogsWarningOnGenericException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsWithNullRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesCallsValidateForArrayProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsEmptyArrayValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesValidatesSingleProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithBooleanFalseValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithZeroValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithSchemaExceptionReturnsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithArrayOfObjectsContainingNestedArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithIntegerZeroValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithFalseValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithMixedKeysIncludingMetadata":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNonEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNestedEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithUnderscoreIdentifier":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithOnlySpaces":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithTextUrlFormatNonUuidValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsArrayOfObjectsWithNestedArrayItem":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenAlreadyInRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenTargetSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenNoRefInProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsHandlesItemsRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataWithNullPublishedKeepsExistingNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataWithBoolPublishedTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsCopiesInversedByFromPropertyLevel":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsCopiesRegisterFromPropertyLevel":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSaveObjectCreatesNewEntityWithPersistFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSaveObjectUpdatesExistingEntityWithPersistFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceUuidWithDoesNotExist":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithEmptyFieldValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameWithNoNameAndNoFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithEmptyArrayImageValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testScanForRelationsWithSimpleData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testApplyPropertyDefaultsAppliesDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testSaveObjectWithNonExistentUuidCreatesNewObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testSaveObjectWithExistingUuidUpdatesObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testSaveObjectWithoutUuidGeneratesNewUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithInvalidSchemaReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testEmptyCascadingObjectsAreSkipped":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithInversedBySingleObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithoutInversedByStoresIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testMixedCascadingScenarios":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testInversedByWithArrayPropertyAddsToExistingArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithInversedByArrayObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithoutInversedByArrayStoresUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetRegisterWithRegisterEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetRegisterWithNumericIdUsesCachedLookup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetRegisterWithSlugUsesMapperFind":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaWithSchemaEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaWithNumericIdUsesCachedLookup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaWithSlugUsesMapperFind":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaThrowsWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetObjectWithEntitySetsCurrentObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetObjectWithStringIdUsesUnifiedMapperWhenContextSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetObjectFallsBackToUnifiedObjectMapperWithoutContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectReturnsNullInitially":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectReturnsCurrentObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetSchemaThrowsWhenNotSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetSchemaReturnsSchemaId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetRegisterThrowsWhenNotSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetRegisterReturnsRegisterId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindDelegatesToGetHandlerAndRenders":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindReturnsNullWhenObjectNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindSetsRegisterContextWhenProvided":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectWithArrayData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectWithObjectEntityExtractsUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectSetsContextFromParameters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectDelegatesToDeleteHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectWhenNotFoundChecksPermissionIfSchemaSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPublishDelegatesToPublishHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPublishWithCustomDateAndRbac":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDepublishDelegatesToPublishHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testLockObjectDelegatesToLockHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testUnlockObjectDelegatesToLockHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectsDelegatesToBulkOpsHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectsDelegatesToBulkOpsHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountDelegatesToObjectMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountRemovesLimitFromConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetLogsDelegatesToGetHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectWithArrayNoUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectExtractsFromSelfId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectExtractsFromTopLevelId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectWithObjectEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectPreservesProvidedUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectSkipsEmptyId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesConvertDatetimeToDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesLeavesValidDatesAlone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesSkipsNonDateFormats":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesReturnsUnchangedWithoutSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesHandlesSpaceSeparatedDatetime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesLeavesInvalidValuesUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsTrueForValidUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsTrueForUppercaseUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsFalseForNonUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsDelegatesToQueryHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testBuildSearchQueryDelegatesToSearchQueryHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetFacetsForObjectsDelegatesToFacetHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindByRelationsDelegatesToMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountSearchObjectsDelegatesToMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetExtendedObjectsDelegatesToRenderHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetCreatedSubObjectsDelegatesToSaveHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testClearCreatedSubObjectsDelegatesToSaveHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetCacheHandlerReturnsInjectedInstance":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCheckSavePermissionsCreateWhenNoUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCheckSavePermissionsCreateWhenUuidNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCheckSavePermissionsSkipsWhenNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPrepareFindAllConfigConvertsExtendStringToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPrepareFindAllConfigSetsRegisterFromFilters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPrepareFindAllConfigSetsSchemaFromFilters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testRenderEntityDelegatesToRenderHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindSilentDelegatesToGetHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindSilentSetsContextWhenProvided":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testHandleCascadingPreservesParentContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderReturnsNullForNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderCreatesFolderForExistingObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderReturnsNullForNewObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testMethodChainingForContextSetters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountSearchObjectsDelegatesToMapperWithOrgContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountSearchObjectsSkipsOrgWhenMultitenancyDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedUsesDatabaseByDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedSetsRegisterSchemaContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedForcesDbWhenIdsProvided":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedAddsExtendedObjectsWhenExtendSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPublishObjectsDelegatesToBulkOps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDepublishObjectsDelegatesToBulkOps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPublishObjectsBySchemaDelegatesToBulkOps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectsBySchemaDelegatesToBulkOps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectsByRegisterDelegatesToBulkOps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testListObjectsDelegatesToSearchObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCreateObjectCallsSaveObjectInternally":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testBuildObjectSearchQueryDelegatesToBuildSearchQuery":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExportObjectsThrowsDisabledException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testImportObjectsThrowsDisabledException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDownloadObjectFilesThrowsDisabledException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testVectorizeBatchObjectsThrowsDisabledException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetVectorizationStatisticsThrowsDisabledException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetVectorizationCountThrowsDisabledException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testMergeObjectsDelegatesToMergeHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testMigrateObjectsDelegatesToMigrationHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateObjectsBySchemaDelegatesToValidationHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateAndSaveObjectsBySchemaDelegatesToValidationHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectContractsDelegatesToRelationHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectUsesDelegatesToRelationHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectUsedByDelegatesToRelationHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testHandleValidationExceptionDelegatesToValidateHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetDeleteHandlerReturnsInjectedInstance":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectNamesForResultsReturnsEmptyForEmptyResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectNamesForResultsSkipsNonArrayResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsTrueForValid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsFalseForInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromRelationsCollectsDirectUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromRelationsCollectsNestedUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromObjectDataCollectsTopLevel":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromObjectDataStopsAtDepth1":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromObjectDataCollectsFromArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromArrayResultHandlesSelfStructure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromArrayResultHandlesFlatArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectsSetsRegisterSchemaContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectsDelegatesToBulkOps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderExistsHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectReturnsSetObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedHandlesExtendCommaString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetActiveOrganisationReturnsUuidWhenOrgFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetActiveOrganisationReturnsNullWhenNoOrg":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetActiveOrganisationReturnsNullOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateObjectIfRequiredSkipsWhenNotHardValidation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateObjectIfRequiredValidatesWhenHardValidationEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateObjectIfRequiredThrowsOnValidationFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderExistsCallsUpdateWhenNodeIdNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetFacetableFieldsDelegatesToFacetHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetRegisterFallsBackToMapperWhenCacheReturnsWrong":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaFallsBackToMapperWhenCacheReturnsWrong":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountSearchObjectsPassesOrgUuidWhenFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedBypassesMultitenancyForPublicSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedExplicitDatabaseSource":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedForcesDbWhenUsesProvided":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testUpdateObjectSetsIdAndDelegatesToSaveObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPatchObjectMergesExistingDataAndDelegatesToSave":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetContextFromParametersSetsRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetContextFromParametersSetsSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetContextFromParametersDoesNothingWhenBothNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testResolveRegisterAndSchemaReturnsNullsWithoutExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testResolveRegisterAndSchemaUsesCurrentFromFilters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testResolveRegisterAndSchemaLoadsSchemasForSelfSchemaExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testResolveRegisterAndSchemaLoadsRegistersForSelfRegisterExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesReturnsUnchangedWhenNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesConvertsDatetimeToDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesSkipsAlreadyFormattedDates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesLeavesInvalidDatesUntouched":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesSkipsNonStringValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesSkipsNonDateFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderCreatesWhenFolderIsString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderReturnsNullOnGeneralException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderRecreatesWhenFolderIsStringNumeric":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCheckSavePermissionsUpdateWhenUuidExists":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindAllDelegatesToGetHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderExistsCreatesFolder":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonCreatesSeedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedObjectWhenAlreadyExists":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedObjectOnMultipleObjectsException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataPreCreatesMagicMapperTable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataContinuesWhenMagicMapperFails":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataUsesTitleAsSlugFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataUsesProvidedUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataContinuesWhenInsertThrows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataUsesRegisterZeroWhenNoRegisterFound":8,"Unit\\Service\\MigrationServiceTest::testGetStorageStatusReturnsStatusArray":8,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableEmptySource":8,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableDryRun":8,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableSkipsDuplicates":8,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableHandlesFailure":8,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageDryRun":8,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageSkipsDuplicates":8,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableEnsuresTable":8,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableDryRunDoesNotEnsureTable":8,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableActualMigration":8,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageActualMigration":8,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageHandlesFailure":8,"Unit\\Service\\MigrationServiceTest::testGetStorageStatusWithMagicTableExists":8,"Unit\\Service\\MigrationServiceTest::testGetStorageStatusRegisterSchemaIds":8,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableBreaksLoopWhenNoBatchReturned":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceCacheHit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceCleanedCacheHit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceUuidNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferencePathBySlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceFindAllException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceDirectSlugMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceCachesNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceUuidNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceUrlPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceFindAllException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceDirectSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testRemoveQueryParametersNoParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testRemoveQueryParametersStrips":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceStandardUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceUuidNoDashes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferencePrefixedUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferencePrefixedUuidNoDashes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceIdentifierPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceCommonWord":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceShortText":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceWithWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsObjectPropertyType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsTextUuidFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsArrayOfObjectsWithStrings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsNonObjectArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsWithPrefix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsSkipsNumericKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsSchemaCatchesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultAlways":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultFalsyEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultFalsyEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultFalsyNonEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultMissingKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultNullValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultExistingValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueSimpleRefFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueSimpleRefNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValuePreservesArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueComplexTemplate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueNonTemplate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueNumeric":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testCreateSlugSimple":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testCreateSlugSpecialChars":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testCreateSlugLongTextTruncation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testCreateSlugTrimsTrailingHyphens":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGenerateSlugNoConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGenerateSlugWithConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGenerateSlugMissingFieldValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGenerateSlugException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetValueFromPathNested":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetValueFromPathMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetValueFromPathConvertsToString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsEffectivelyEmptyObjectEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsEffectivelyEmptyObjectOnlyMetadata":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsEffectivelyEmptyObjectWithData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsEffectivelyEmptyObjectEmptyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataImageArrayDownloadUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataImageArrayAccessUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataNumericImageValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataStringImageUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataInvalidPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataInvalidDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataImageArrayNumericFileIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testClearAllCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testTrackCreatedSubObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testClearCreatedSubObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetCachedSchemaFetchesAndCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetCachedRegisterFetchesAndCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyAlwaysDefaultsNoAlwaysDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyAlwaysDefaultsAppliesAlwaysDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyAlwaysDefaultsSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyAlwaysDefaultsNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyPropertyDefaultsAppliesForMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyPropertyDefaultsSkipsExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyPropertyDefaultsSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testUpdateObjectRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataPublishedEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataPublishedNotPresent":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataInvalidPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataDepublishedEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataOwner":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataSlugFromData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataDepublishedNotPresent":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSaveObjectWithRbacCreateNewObjectNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSaveObjectWithFilePropertyProcessing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetSelfMetadataWithPublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetSelfMetadataWithNullPublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetSelfMetadataWithDepublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectReturnsNullOnNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectReturnsWhenNotLocked":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectThrowsWhenLockedByDifferentUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectAllowsLockBySameUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectLockNoCurrentUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testDeleteOrphanedRelatedObjectsSoftDeletes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testDeleteOrphanedRelatedObjectsSystemUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testDeleteOrphanedRelatedObjectsHandlesNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testDeleteOrphanedRelatedObjectsHandlesGeneralException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithInvalidDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithEmptyPublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataNoDepublishedInSelfData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testFindAndValidateExistingObjectReturnsExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testFindAndValidateExistingObjectReturnsNullWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testRefactoredSaveObjectIntegration":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testStringPropertyMapsToString":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testIntegerPropertyMapsToInt":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testBooleanPropertyMapsToBoolean":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testDateTimeFormatMapsToDateTimeScalar":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testUuidFormatMapsToUuidScalar":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testEmailFormatMapsToEmailScalar":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testObjectWithoutRefMapsToJsonScalar":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testGeneratesQueryAndMutationTypes":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testGeneratesConnectionType":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testGeneratesMutationFields":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testObjectTypeIncludesMetadataFields":8,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testRefPropertyResolvesToObjectType":8,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testIntrospectionQuery":7,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testCustomScalarsInSchema":7,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testConnectionTypeStructure":7,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testPageInfoTypeFields":7,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testAuditTrailEntryTypeFields":7,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testSchemaGenerationFromRealDatabase":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testSearchConfigurationsReturnsResultsOnSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigExportsMappings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsDataOnSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsErrorWhenNoSourceUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsErrorWhenNullSourceUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsErrorOnFetchFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnUndecodableFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnUrlUnparseableResponse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testUnpublishFileResolvesStringObjectId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeJsonAutoDetect":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeYamlExplicitType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeFallsBackToYamlOnJsonFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeReturnsNullWhenBothParsersFail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeConvertsStdClassToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureConvertsStdClass":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureConvertsNestedObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureLeavesArraysIntact":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureConvertsObjectsInsideArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromURLReturnsArrayOnSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromBodyPassthroughArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromBodyDecodesJsonString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromBodyReturnsErrorForInvalidJson":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromBodyConvertsStdClassObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSetsOwnerAndApplication":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSkipsWhenVersionEqual":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSkipsWhenExistingVersionIsNewer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaUpdatesWhenVersionIsNewer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaThrowsOnMapperError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonThrowsWithoutConfiguration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonStoresVersionInAppConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsOwner":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationReadsTitleFromInfoSection":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationFallsBackToXOpenregisterTitle":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationThrowsOnMapperError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetObjectService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetOpenConnectorConfigurationService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromFilePathThrowsWhenFileNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeYamlExtensionType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeReturnsNullForInvalidJsonWithExplicitType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetObjectMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterDuplicateInfoHandlesFindAllFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterDuplicateInfoOneMatchReturnsGenericMessage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSetsOnlyOwner":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterForceUpdatesExistingWithOwnerAndApp":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesObjectConfigurationRegisterFromDatabase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaRemovesObjectConfigurationRegisterWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesObjectConfigurationSchemaFromMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaNormalisesEmptyObjectConfiguration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesLegacyRegisterProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppSetsOpenregisterVersionRequirement":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppSetsTypeFromXOpenregister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsOpenregisterVersion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedDataWhenAbsent":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedDataWhenSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonUseMagicMapperForSeedData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsVersionCheckWhenNoAppId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonLogsForceImportMessage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsMappingWithoutSlugOrName":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonDoesNotStoreVersionWithoutAppId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonDoesNotStoreVersionWithoutVersion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaForceUpdatesWhenVersionEqual":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaSkipsWhenExistingVersionIsNewer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSetsOnlyApplication":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeReturnsNullForInvalidYamlWithExplicitType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureHandlesMixedArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonContinuesWhenMappingImportFails":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonDoesNotSkipWhenStoredVersionIsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsWorkflowsWhenRegistryNotSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringReturnsEarlyWhenMapperNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaNormalisesItemsObjectConfiguration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaNormalisesItemsFileConfiguration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaConvertsStdClassPropertyToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaConvertsStdClassItemsToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesObjectConfigurationSchemaFromDatabase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaRemovesObjectConfigurationSchemaWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesItemsObjectConfigRegisterFromMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesItemsObjectConfigSchemaFromMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDuplicateSchemaInfoHandlesFindAllFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDuplicateSchemaInfoOneMatchReturnsGenericMessage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationCollectsObjectIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationUsesDataTitleFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationUsesDefaultTitle":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationUsesDataType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeJsonExplicitType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromFileReturnsErrorResponseOnUploadError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromFileReturnsErrorResponseOnDecodeFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromFileReturnsArrayForValidJsonFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromURLReturnsErrorWhenBodyNotDecodable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterCreatesNewRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterUpdatesWhenVersionIsNewer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterForceUpdatesWhenVersionIsOlder":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterStripsIdUuidOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterThrowsOnMapperFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaCreatesNewSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaSetsOwnerAndApplication":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaSkipsWhenVersionEqual":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaDefaultsPropertyTypeToString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaStripsStringFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesRefFromSlugsMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsWhenVersionNotNewer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonImportsSchemas":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonImportsRegisters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonExtractsAppIdAndVersionFromData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsObjectsMissingRegisterOrSchema":1,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsObjectsWithoutSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonForceBypassesVersionCheck":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppCreatesNewConfigurationWhenNoneExists":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppReusesConfigFoundBySourceUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppReusesConfigFoundByApp":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppSetsGithubFieldsFromNestedStructure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppSetsGithubFieldsFromFlatStructure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppWrapsException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationCreatesNew":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationUpdatesExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsSourceUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromFilePathThrowsOnInvalidJson":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromFilePathInjectsSourceMetadata":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonImportsMappings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonCallsOpenConnectorWhenSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonContinuesWhenOpenConnectorThrows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonContinuesWhenSchemaImportFails":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSetsSchemaTitleFromKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetDeployedWorkflowMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterThrowsOnDuplicateRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaThrowsOnDuplicateSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaCreatesOnValidationException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaStripsItemsBinaryFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaSetsMissingPropertyTitle":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesRefFromSchemasMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesItemsRefFromSlugsMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesObjectConfigurationRegisterFromMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesLegacyItemsRegisterFromMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonResolvesRegisterSchemasFromSchemasMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonLogsWarningForUnresolvedRegisterSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonUpdatesExistingObjectWhenVersionIsHigher":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsObjectWithArrayResultWhenVersionNotHigher":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppContinuesWhenConfigVersionUpToDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppForceBypassesConfigVersionCheck":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationFallsBackToXOpenregisterDescription":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsGithubFieldsNested":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsGithubFieldsFlat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationMergesEntityIdsOnUpdate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedObjectWithoutSlugOrTitle":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonUpdatesMappingWhenVersionIsHigher":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsMappingWhenVersionEqual":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSetsMappingNameFromKeyWhenAbsent":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromURLParsesYamlResponse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonResultAlwaysHasAllExpectedKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentFailsOnMissingRequiredFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentFailsWhenNoEngineOfType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentRecordsFailureOnAdapterException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringSkipsEntryWithoutAttachTo":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringAttachesWorkflowToSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringSkipsWhenSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringSkipsIncompleteAttachTo":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaNormalisesEmptyFileConfiguration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaStripsByteFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppUpdatesMetadataOnExistingConfigWithResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromFilePathDoesNotOverwriteExistingSourceMetadata":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonPass2SkipsSchemaNotInMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonPass2CatchesReImportFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonCreatesNewObjectWhenNotExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsObjectWhenVersionNotHigher":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetWorkflowEngineRegistry":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentUnchangedWhenHashMatches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentUpdatesExistingWorkflow":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromURLReturnsErrorOnGuzzleException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentCreatesNewWorkflow":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetMagicMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeReturnsNullForGarbageInput":8,"Unit\\Service\\MigrationServiceTest::testResolveRegisterAndSchemaWithSlugs":8,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageNoMagicTable":8,"Unit\\Service\\MigrationServiceTest::testResolveRegisterAndSchemaThrowsOnMissingRegister":8,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageBreaksLoopWhenNoBatchReturned":8,"Unit\\Service\\MigrationServiceTest::testResolveRegisterAndSchema":8,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageEmptySource":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithStandardUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithUuidWithoutDashes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithPrefixedUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithPrefixedUuidNoDashes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithWhitespaceOnly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithPlainText":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithCommonWordReturnsTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithOpenSourceReturnsTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithIdentifierLikeString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithShortString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithOnlyMetadataKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithNullValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithEmptyStringValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithNonEmptyValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithNestedEmptyObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithNestedNonEmptyObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithNonEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithNumber":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithZero":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithAssociativeArrayAllEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithIndexedArrayAllEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithIndexedArraySomeNotEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testRemoveQueryParametersWithQueryString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testRemoveQueryParametersWithoutQueryString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testRemoveQueryParametersWithMultipleParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGenerateSlugReturnsNullWhenNoSlugField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGenerateSlugReturnsNullWhenFieldValueIsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testCreateSlugConvertsToLowercase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testCreateSlugReplacesSpecialCharacters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testCreateSlugTrimsHyphens":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testCreateSlugTruncatesLongStrings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultAlwaysReturnsTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithMissingKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithNullValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithNonEmptyValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultDefaultBehaviorWithMissingKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultDefaultBehaviorWithNullValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultDefaultBehaviorWithExistingValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueSimpleReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueSimpleReferencePreservesArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueSimpleReferenceMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueComplexTemplate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueNonTemplate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueNonString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueExceptionReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyAlwaysDefaultsNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyAlwaysDefaultsAppliesAlwaysBehavior":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyAlwaysDefaultsSkipsNonAlwaysDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyAlwaysDefaultsWithInvalidSchemaObjectReturnsData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyPropertyDefaultsAppliesDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyPropertyDefaultsSkipsExistingValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyPropertyDefaultsFalsyBehavior":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetValueFromPathSimple":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetValueFromPathNested":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetValueFromPathReturnsNullForMissingKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetValueFromPathConvertIntToString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsObjectPropertyEmptyStringBecomesNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsObjectPropertyEmptyObjectNonRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsObjectPropertyEmptyObjectRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsArrayPropertyEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsArrayItemsWithEmptyStrings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsScalarNonRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsScalarRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsSkipsMissingProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceCachedResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveRegisterReferenceEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveRegisterReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveRegisterReferenceBySlugInUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveRegisterReferenceNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testTrackAndGetCreatedSubObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithImageStringUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithImageArrayFileObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithImageArrayAccessUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithInvalidPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithInvalidDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsWithSchemaObjectProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsWithSchemaTextUuidProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsWithArrayOfObjectStrings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsWithPrefix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsSkipsEmptyKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithConstantValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithAlwaysBehavior":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithFalsyBehaviorEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithTemplateException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetSelfMetadataWithSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetCachedSchemaFetchesAndCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetCachedRegisterFetchesAndCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceBySlugPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceDirectSlugMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceCleanedCacheLookup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsAuditTrailsEnabledReturnsTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsAuditTrailsEnabledReturnsFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsAuditTrailsEnabledDefaultsToTrueWhenKeyMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsAuditTrailsEnabledDefaultsToTrueOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGenerateSlugReturnsNullWhenNoConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGenerateSlugReturnsSlugWithTimestamp":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceNotFoundCachesNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testFillMissingSchemaPropertiesWithNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testClearAllCachesResetsEverything":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithNumericImage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSaveObjectWithRbacUnauthorizedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithSlugGeneration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testUpdateObjectRelationsSetsRelationsOnEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSaveObjectWithAutoGeneratedUuidSkipsLookup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesSkipsNonValidatedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesSkipsNullAndEmptyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesSkipsUnchangedValuesOnUpdate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesSkipsWithoutRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferenceExistsLogsWarningOnGeneralException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferenceExistsReturnsWhenSchemaUnresolvable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsReturnsEmptyForAllStringUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsReturnsEmptyForInvalidItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsReturnsEmptyWhenNoItemsRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsFiltersObjectsWithEmptyId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsRecognizesIdentifierTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testPreCacheParentNameHandlesHydrationException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testClearImageMetadataIfFilePropertySkipsNonFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterWithEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterWithNullRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeSingleObjectReturnsNullWithoutRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeSingleObjectReturnsNullForEmptyObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeSingleObjectReturnsNullWhenParentUuidEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeSingleObjectReturnsNullForObjectWithEmptyId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsWithNullRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveRegisterReferenceWithUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveRegisterReferenceUuidNotFoundFallsToSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaReferenceWithQueryParamsAndCleanedCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaReferenceUuidThrowsDoesNotExist":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testExtractUuidAndSelfDataEmptyUuidToNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testExtractUuidAndSelfDataExtractsIdFromData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithOwner":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesNullProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferenceExistsThrowsOnNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsReturnsEmptyForNonList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testPreCacheParentNameReturnsWhenNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testPreCacheParentNameCachesName":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testPreCacheParentNameFallsBackToNaam":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testClearImageMetadataNoObjectImageField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testClearImageMetadataIfFilePropertyClearsWhenFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterWithIntIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterThrowsForInvalidStringSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterThrowsForInvalidStringRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsReturnsEarlyWhenEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsSkipsNonUuidRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsSkipsNoPropertyConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsSkipsNoTargetSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testExtractUuidAndSelfDataProcessesUploadedFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testExtractUuidAndSelfDataExtractsSelfId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesArrayProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithIdField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithExplicitUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithoutUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithNullRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testClearImageMetadataIfFilePropertyClearsImage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testClearImageMetadataIfFilePropertyPreservesNonFileImage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testClearImageMetadataIfFilePropertyNoConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithSelfMetadata":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithIntegerIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetObjectFallsBackToObjectEntityMapperWithoutContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountDelegatesToObjectEntityMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionValid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionCaseInsensitive":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsEmptyAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsSetNullOnArrayProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexAndSchemaCacheBuiltTogether":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteWithNullSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteDepthLimitingPreventsDeepRecursion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsOnlyCascade":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsSetDefaultUpdatesObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testValidOnDeleteActionsConstant":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsMixedSchemaAllInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsMixedSchemaWithValidObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCreateEmptyResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartDoesNotLogSmallSingleSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartLogsLargeSingleSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartLogsMixedSchemaAboveThreshold":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartDoesNotLogSmallMixedSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testInitializeResultWithNoInvalidObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testInitializeResultWithInvalidObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testMergeChunkResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testMergeChunkResultMultipleChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsBasic":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsWithUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsZeroProcessed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeSmall":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeMedium":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeLarge":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeVeryLarge":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeUltraLarge":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeHuge":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary100":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary1000":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsNoDuplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsWithDuplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsUsesUuidField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsUsesSelfId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsWithoutId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsMixed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLoadSchemaWithCacheLoadsFromDb":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLoadRegisterWithCacheLoadsFromDb":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testGetSchemaAnalysisWithCacheCachesResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSkipsPlainText":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithSchemaObjectType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithArrayOfObjectsInSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithUrlValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithSchemaTextUuidFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithPrefixedUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithPlainText":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithCommonWord":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithIdLikeString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsCommonTextWordMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsCommonTextWordNoMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsWithDeduplicationEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsWithDeduplicationDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsWithEmptyAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSingleObjectRelation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsArrayOfObjectRelation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsNonExistentTarget":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsMissingSchemaAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsDoesNotAddDuplicate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsPropertyNotInObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsConvertsNonArrayExistingValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsArrayWithNonStringValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithPrefix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSkipsNonStringKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithNestedArrayContainingArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaTextUriFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaTextUrlFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSkipsEmptyStringValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSkipsEmptyArrayValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsNonObjectArrayWithStringReferences":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsArrayOfObjectsWithNestedArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithShortString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithHttpUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithFtpUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsCommonTextWordCaseInsensitive":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary5000":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary10000":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary50000":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeJustAbove50000":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsPartialEfficiency":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsWithDeduplication":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testMergeChunkResultWithMissingOptionalKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithSchemaIdInsteadOfObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaNoUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithSelfData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithObjectProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithPublishedDateString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithInvalidPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithAutoPublish":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaAutoPublishWithCsvPublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaEnrichDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsDeduplicationWithDuplicatesLogged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testPrepareObjectsForSaveMixedSchemaPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartExactThresholdSingleSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartExactThresholdMixedSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsPreservesOrder":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsMultipleDuplicateGroups":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaTextNonRelationFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaPropertyTypeArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsNestedObjectWithinNonObjectArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithNullSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaObjectPropertyWithUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithLongIdNoHyphenOrUnderscore":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithSystemsoftwareCommonWord":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithClosedSourceCommonWord":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsArrayRelationWithNonArrayExistingValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsArrayDoesNotAddDuplicate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsObjectWithEmptyId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsObjectWithNullId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsObjectWithNullSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsMultipleObjectsMultipleInverseProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithDepublishedDateString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithInvalidDepublishedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaMultipleObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaPreservesExistingSelfValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithObjectPropertyAndRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsMixedSchemaWithUpdatedAndUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsMixedSchemaWithPartialInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsDeduplicationDisabledPassesAllObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testProcessObjectsInChunksMultipleChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsDeduplicationPercentage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testMergeChunkResultEmptyChunk":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaAutoPublishFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithProvidedIdInSelf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithNoSelfData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaLegacyMetadataRemoval":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeOneObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary101":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary1001":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary5001":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary10001":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testPerformComprehensiveSchemaAnalysisDelegates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithEmptyKeySkipped":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithBothPublishedAndDepublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithWhitespaceId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsCommonTextWordWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsCommonTextWordWithNumericString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLoadSchemaWithCacheStringId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLoadRegisterWithCacheStringId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsWithNoSelfData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithRegisterAsInt":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsDeepNesting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsArrayOfObjectsWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithNonStringPublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithNullSelfPublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsWithValidationEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsWithEventsEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsResultHasAggregateStatisticsKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsReturnsErrorWhenNoObjectsPrepared":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsReturnsEmptyResultForEmptyInput":8,"Unit\\Controller\\BulkControllerTest::testPublishMissingUuids":8,"Unit\\Controller\\BulkControllerTest::testPublishEmptyUuidsArray":8,"Unit\\Controller\\BulkControllerTest::testPublishUuidsNotArray":8,"Unit\\Controller\\BulkControllerTest::testPublishSuccess":8,"Unit\\Controller\\BulkControllerTest::testPublishWithSkippedUuids":8,"Unit\\Controller\\BulkControllerTest::testPublishWithValidDatetime":8,"Unit\\Controller\\BulkControllerTest::testPublishWithDatetimeTrue":8,"Unit\\Controller\\BulkControllerTest::testPublishWithDatetimeFalse":8,"Unit\\Controller\\BulkControllerTest::testPublishWithDatetimeNull":8,"Unit\\Controller\\BulkControllerTest::testPublishInvalidDatetime":8,"Unit\\Controller\\BulkControllerTest::testPublishException":8,"Unit\\Controller\\BulkControllerTest::testPublishDefaultDatetimeWhenNotProvided":8,"Unit\\Controller\\BulkControllerTest::testDepublishSuccess":8,"Unit\\Controller\\BulkControllerTest::testDepublishWithSkippedUuids":8,"Unit\\Controller\\BulkControllerTest::testDepublishMissingUuids":8,"Unit\\Controller\\BulkControllerTest::testDepublishEmptyUuidsArray":8,"Unit\\Controller\\BulkControllerTest::testDepublishUuidsNotArray":8,"Unit\\Controller\\BulkControllerTest::testDepublishWithValidDatetime":8,"Unit\\Controller\\BulkControllerTest::testDepublishWithDatetimeTrue":8,"Unit\\Controller\\BulkControllerTest::testDepublishWithDatetimeFalse":8,"Unit\\Controller\\BulkControllerTest::testDepublishWithDatetimeNull":8,"Unit\\Controller\\BulkControllerTest::testDepublishInvalidDatetime":8,"Unit\\Controller\\BulkControllerTest::testDepublishDefaultDatetimeWhenNotProvided":8,"Unit\\Controller\\BulkControllerTest::testDepublishRegisterNotFound":8,"Unit\\Controller\\BulkControllerTest::testDepublishSchemaNotFound":8,"Unit\\Controller\\BulkControllerTest::testDepublishException":8,"Unit\\Controller\\BulkControllerTest::testPublishSchemaInvalidId":8,"Unit\\Controller\\BulkControllerTest::testPublishSchemaSuccess":8,"Unit\\Controller\\BulkControllerTest::testPublishSchemaWithPublishAllTrue":8,"Unit\\Controller\\BulkControllerTest::testPublishSchemaException":8,"Unit\\Controller\\BulkControllerTest::testPublishReturnsJsonResponse":8,"Unit\\Controller\\BulkControllerTest::testDepublishReturnsJsonResponse":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithAdminUser":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithExplicitOffset":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithUnderscoreOffset":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithSortParams":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithUnderscoreSortParams":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithSearchParam":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithUnderscoreSearchParam":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithCustomFilters":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithPageCalculatesOffset":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithUnderscorePageParam":8,"Unit\\Controller\\DeletedControllerGapTest::testIndexPageCalculation":8,"Unit\\Controller\\DeletedControllerGapTest::testRestoreMultipleWithMixedObjects":8,"Unit\\Controller\\DeletedControllerGapTest::testDestroyMultipleWithMixedObjects":8,"Unit\\Controller\\DeletedControllerGapTest::testRestoreMultipleInnerException":8,"Unit\\Controller\\DeletedControllerGapTest::testDestroyMultipleInnerException":8,"Unit\\Controller\\DeletedControllerGapTest::testRestoreObjectWithEmptyDeletedArray":8,"Unit\\Controller\\DeletedControllerGapTest::testRestoreMultipleMessageWithNoNotFound":8,"Unit\\Controller\\DeletedControllerGapTest::testDestroyMultipleMessageWithNoNotFound":8,"Unit\\Controller\\DeletedControllerTest::testIndexSuccess":8,"Unit\\Controller\\DeletedControllerTest::testIndexException":8,"Unit\\Controller\\DeletedControllerTest::testStatisticsSuccess":8,"Unit\\Controller\\DeletedControllerTest::testStatisticsException":8,"Unit\\Controller\\DeletedControllerTest::testTopDeleters":8,"Unit\\Controller\\DeletedControllerTest::testRestoreObjectNotDeleted":8,"Unit\\Controller\\DeletedControllerTest::testRestoreException":8,"Unit\\Controller\\DeletedControllerTest::testRestoreMultipleNoIds":8,"Unit\\Controller\\DeletedControllerTest::testDestroyObjectNotDeleted":8,"Unit\\Controller\\DeletedControllerTest::testDestroySuccess":8,"Unit\\Controller\\DeletedControllerTest::testDestroyException":8,"Unit\\Controller\\DeletedControllerTest::testDestroyMultipleNoIds":8,"Unit\\Controller\\DeletedControllerTest::testRestoreMultipleSuccess":8,"Unit\\Controller\\DeletedControllerTest::testRestoreMultipleException":8,"Unit\\Controller\\DeletedControllerTest::testDestroyMultipleSuccess":8,"Unit\\Controller\\DeletedControllerTest::testDestroyMultipleException":8,"Unit\\Controller\\DeletedControllerTest::testIndexWithPagination":8,"Unit\\Controller\\DeletedControllerTest::testTopDeletersException":8,"Unit\\Controller\\DeletedControllerTest::testRestoreSuccess":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testCreateWithMultipartNonStringValuesSkipsThem":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testCreateWithMultipartJsonStringDecodesArray":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testContractsWithPageCalculatesOffset":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testContractsWithUnderscorePageParam":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testContractsWithUnderscoreOffsetParam":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexWithMagicMappedSchemaUsesMagicMapper":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithExtendUsesRenderHandler":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithIgnoredFiltersAddsHint":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithFacetsIncludesFacetData":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithFacetErrorLogsFacetError":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithEmptyTruePreservesEmptyValues":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperPageToOffsetConversion":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithZeroLimitSetsPageToOne":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperLargeResultSetsGzipHeaders":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperFiltersSchemaRegisterFromExtend":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithExtendAsStringConvertsToArray":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperStripEmptySerializesObjectEntities":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsWithMagicMappedSchemaUsesMagicMapper":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsWithMagicMapperStripsEmptyValuesByDefault":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsWithMagicMapperEmptyTruePreservesValues":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexCrossTableSearchWithValidPairsReturnsResults":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexCrossTableSearchWithInvalidPairsReturns404":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexCrossTableSearchWithEmptyTruePreservesValues":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testUpdateWithNoUploadedFilesSendsNull":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testPostPatchWithNoFilesPassesNullUploadedFiles":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexNormalPathWithArrayResultsStripsEmpty":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsNormalPathWithArrayResultsStripsEmpty":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testCreateContinuesWhenWebhookFails":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperContinuesWhenOrgServiceFails":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testShowExtendRegisterWithEntityPopulatesMap":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testShowExtendSchemaWithEntityPopulatesMap":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexNormalPathLargeResultsSetsGzipHeaders":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperIgnoredFiltersWithoutControlParams":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexCrossTableSearchWithPagination":8,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsNormalPathEmptyTruePreservesValues":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns204OnSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns500WhenDeleteFails":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns403OnException":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns409OnReferentialIntegrityException":8,"Unit\\Controller\\ObjectsControllerTest::testControllerConstructorSetsProperties":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns422OnHookStoppedException":8,"Unit\\Controller\\ObjectsControllerTest::testLockReturnsLockedObject":8,"Unit\\Controller\\ObjectsControllerTest::testLockReturns404WhenObjectNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testLockReturns500OnError":8,"Unit\\Controller\\ObjectsControllerTest::testUnlockReturnsUnlockedObject":8,"Unit\\Controller\\ObjectsControllerTest::testPublishReturns400OnException":8,"Unit\\Controller\\ObjectsControllerTest::testDepublishReturnsDepublishedObject":8,"Unit\\Controller\\ObjectsControllerTest::testDepublishReturns400OnException":8,"Unit\\Controller\\ObjectsControllerTest::testMergeReturnsMergedObject":8,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns400WhenTargetMissing":8,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns400WhenObjectDataMissing":8,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns404OnDoesNotExistException":8,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns400OnInvalidArgument":8,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns500OnGenericException":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400WhenSourceMissing":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400WhenTargetMissing":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400WhenObjectsMissing":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400WhenMappingMissing":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturnsMigrationResult":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns404OnDoesNotExist":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns500OnGenericException":8,"Unit\\Controller\\ObjectsControllerTest::testVectorizeBatchSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testVectorizeBatchReturns500OnException":8,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationStatsSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationStatsError":8,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationCountSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationCountError":8,"Unit\\Controller\\ObjectsControllerTest::testValidateReturns400WhenParamsMissing":8,"Unit\\Controller\\ObjectsControllerTest::testValidateReturnsSuccessResult":8,"Unit\\Controller\\ObjectsControllerTest::testValidateReturns500OnException":8,"Unit\\Controller\\ObjectsControllerTest::testClearBlobSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testClearBlobReturns500OnException":8,"Unit\\Controller\\ObjectsControllerTest::testContractsReturnsPaginatedResults":8,"Unit\\Controller\\ObjectsControllerTest::testUsesReturnsPaginatedResults":8,"Unit\\Controller\\ObjectsControllerTest::testUsedReturnsPaginatedResults":8,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturns404WhenNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturns403OnException":8,"Unit\\Controller\\ObjectsControllerTest::testImportReturns400WhenNoFile":8,"Unit\\Controller\\ObjectsControllerTest::testImportSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testImportReturns500OnException":8,"Unit\\Controller\\ObjectsControllerTest::testIndexReturns404WhenRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testIndexReturns404WhenSchemaNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404WhenRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404WhenSchemaNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns404WhenRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns404WhenSchemaNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenSchemaNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns404WhenRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns404WhenSchemaNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns404WhenRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns404WhenSchemaNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturns404WhenObjectNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturns500OnGenericException":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsReturnsSearchResults":8,"Unit\\Controller\\ObjectsControllerTest::testContractsThrowsWhenNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testUsesThrowsWhenNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testUsedThrowsWhenNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testImportCatchesRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testLockWithDurationParameter":8,"Unit\\Controller\\ObjectsControllerTest::testLockWithoutOptionalParams":8,"Unit\\Controller\\ObjectsControllerTest::testValidateReturns400WhenSchemaMissing":8,"Unit\\Controller\\ObjectsControllerTest::testValidateReturns400WhenRegisterMissing":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns403WhenNoUser":8,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturns403OnGenericException":8,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns400WhenBothParamsMissing":8,"Unit\\Controller\\ObjectsControllerTest::testPublishWithRegularUser":8,"Unit\\Controller\\ObjectsControllerTest::testDepublishWithRegularUser":8,"Unit\\Controller\\ObjectsControllerTest::testImportWithNoFileReturns400":8,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404OnDoesNotExist":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns404WhenObjectNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns404WhenObjectNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns423WhenObjectLocked":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns500OnGenericExceptionFromFindSilent":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenObjectHasWrongSchemaOnly":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns400OnValidationExceptionAdminUser":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns400OnValidationExceptionAdminUser":8,"Unit\\Controller\\ObjectsControllerTest::testIndexReturnsSearchResults":8,"Unit\\Controller\\ObjectsControllerTest::testIndexReturnsPaginatedData":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithRegisterSchemaParamsResolvesToNormalSearch":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithoutRegisterSchemaParams":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsReturns404WhenRegisterNotFoundViaQueryParam":8,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenRegisterDoesNotMatchWithMessage":8,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenSchemaDoesNotMatchWithMessage":8,"Unit\\Controller\\ObjectsControllerTest::testCreateWithRegularUserReturns201":8,"Unit\\Controller\\ObjectsControllerTest::testCreateWithNoUserSessionReturns201OnSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyWithRegularUserReturns204":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyWithRegularUserReturns500WhenDeleteFails":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithEmptyTruePreservesNullValues":8,"Unit\\Controller\\ObjectsControllerTest::testVectorizeBatchWithViewsParam":8,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturns200WhenDeletionBlocked":8,"Unit\\Controller\\ObjectsControllerTest::testContractsWithPaginationParamsAndOffset":8,"Unit\\Controller\\ObjectsControllerTest::testUsesWithSearchParams":8,"Unit\\Controller\\ObjectsControllerTest::testUsedWithSearchParams":8,"Unit\\Controller\\ObjectsControllerTest::testUnlockReturns404WhenObjectNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns204WhenAdminDeletes":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400OnInvalidArgumentException":8,"Unit\\Controller\\ObjectsControllerTest::testValidateWithLimitAndOffset":8,"Unit\\Controller\\ObjectsControllerTest::testIndexReturnsPaginatedResults":8,"Unit\\Controller\\ObjectsControllerTest::testIndexReturnsEmptyResultsForNoObjects":8,"Unit\\Controller\\ObjectsControllerTest::testIndexStripsEmptyValuesFromResults":8,"Unit\\Controller\\ObjectsControllerTest::testIndexIncludesEmptyValuesWhenRequested":8,"Unit\\Controller\\ObjectsControllerTest::testShowPreservesEmptyValuesWhenEmptyParamTrue":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendRegistersAndSchemas":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns423WhenObjectIsLocked":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns500OnUnexpectedFindSilentException":8,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns400OnCustomValidationException":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns400OnCustomValidationException":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns400OnValidationException":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns400OnCustomValidationException":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns400OnValidationException":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns400OnCustomValidationException":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNames":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithFieldsParameter":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithFilterAndUnsetParameters":8,"Unit\\Controller\\ObjectsControllerTest::testShowAsRegularUserEnablesRbac":8,"Unit\\Controller\\ObjectsControllerTest::testCreateAsRegularUser":8,"Unit\\Controller\\ObjectsControllerTest::testCreateFiltersReservedParams":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateWithFiltersReservedParams":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithFilterParams":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsThrowsWhenSearchFails":8,"Unit\\Controller\\ObjectsControllerTest::testContractsWithPaginationParams":8,"Unit\\Controller\\ObjectsControllerTest::testUsesWithPaginationParams":8,"Unit\\Controller\\ObjectsControllerTest::testUsedWithPaginationParams":8,"Unit\\Controller\\ObjectsControllerTest::testPublishWithDateParameter":8,"Unit\\Controller\\ObjectsControllerTest::testDepublishWithDateParameter":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyAsRegularUserSucceeds":8,"Unit\\Controller\\ObjectsControllerTest::testMergeReturnsFullMergeResult":8,"Unit\\Controller\\ObjectsControllerTest::testVectorizeBatchWithViewsAndBatchSize":8,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationStatsWithViewsParam":8,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationCountWithViews":8,"Unit\\Controller\\ObjectsControllerTest::testLogsThrowsWhenRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testLogsThrowsWhenSchemaNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testCreateWithMultipartFormDataNormalizesJson":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateWithMultipartFormData":8,"Unit\\Controller\\ObjectsControllerTest::testPatchAsRegularUser":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchAsRegularUser":8,"Unit\\Controller\\ObjectsControllerTest::testValidateReturnsResultWithErrors":8,"Unit\\Controller\\ObjectsControllerTest::testIndexAddsGzipHeaderForLargeResults":8,"Unit\\Controller\\ObjectsControllerTest::testShowNormalizesLegacyExtendParameters":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyAsAdminBypassesRbac":8,"Unit\\Controller\\ObjectsControllerTest::testMigrateWithAllValidParams":8,"Unit\\Controller\\ObjectsControllerTest::testClearBlobSuccessWithZeroDeleted":8,"Unit\\Controller\\ObjectsControllerTest::testLockReturns423WhenAlreadyLocked":8,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturnsAnalysisWithBlockers":8,"Unit\\Controller\\ObjectsControllerTest::testShowReturnsObjectWithoutAtSelf":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenObjectInWrongSchema":8,"Unit\\Controller\\ObjectsControllerTest::testCreateContinuesWhenWebhookFails":8,"Unit\\Controller\\ObjectsControllerTest::testValidateReturnsPaginationDetailsWhenLimitAndOffset":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyWithNoUserSessionIsNotAdmin":8,"Unit\\Controller\\ObjectsControllerTest::testPatchUnlockErrorIsSilentlyIgnored":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchUnlockErrorIsSilentlyIgnored":8,"Unit\\Controller\\ObjectsControllerTest::testIndexStripsEmptyValuesFromNestedSequentialArrays":8,"Unit\\Controller\\ObjectsControllerTest::testContractsWithLegacyOffsetAndPageParams":8,"Unit\\Controller\\ObjectsControllerTest::testContractsCalculatesPageFromOffset":8,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAddsNextLink":8,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAddsPrevLink":8,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAutoCorrectsTotalWhenLessThanResults":8,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAddsNextPageToUrlWithoutPageParam":8,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAddsNextPageToUrlWithNoQueryString":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendRegistersIncludesRegisterData":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendSchemasIncludesSchemaData":8,"Unit\\Controller\\ObjectsControllerTest::testCreateUsesWebhookInterceptedData":8,"Unit\\Controller\\ObjectsControllerTest::testCreateWithoutWebhookService":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateUnlockErrorIsSilentlyIgnored":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns500WhenContainerGetThrowsForLockedCheck":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateAsRegularUser":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsReturns404WhenRegisterNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsReturns404WhenSchemaNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithUnderscorePrefixedRegisterAndSchema":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsStripsEmptyValuesFromResults":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsPreservesEmptyValuesWhenRequested":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithSchemaParamOnly":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithRegisterParamOnly":8,"Unit\\Controller\\ObjectsControllerTest::testIndexHandlesObjectEntityResultsInEmptyStripping":8,"Unit\\Controller\\ObjectsControllerTest::testLogsMatchesSchemaBySlug":8,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenSchemaDoesNotMatch":8,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenRegisterDoesNotMatch":8,"Unit\\Controller\\ObjectsControllerTest::testLogsMatchesSchemaAsObject":8,"Unit\\Controller\\ObjectsControllerTest::testLogsMatchesSchemaAsObjectById":8,"Unit\\Controller\\ObjectsControllerTest::testLogsMatchesSchemaAsArrayById":8,"Unit\\Controller\\ObjectsControllerTest::testLogsWithPaginationParams":8,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationCountWithSchemasJsonString":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithEmptyTruePreservesEmptyStrings":8,"Unit\\Controller\\ObjectsControllerTest::testCreateMultipartWithNonJsonStrings":8,"Unit\\Controller\\ObjectsControllerTest::testCreateJsonContentTypeSkipsNormalization":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchWithMultipartFormData":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchFiltersIdFromParams":8,"Unit\\Controller\\ObjectsControllerTest::testCreateAllowsAtSelfParam":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesEmptyCacheHandler":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesCollectsUuidsFromRelations":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithRbacDisabled":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithPublishedAndDeletedParams":8,"Unit\\Controller\\ObjectsControllerTest::testImportWithAllBooleanParams":8,"Unit\\Controller\\ObjectsControllerTest::testPatchWithMultipartFormData":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendRegisterAndSchemaEntities":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendArrayContainingNonStringValues":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendAsIntegerReturnsOk":8,"Unit\\Controller\\ObjectsControllerTest::testCreateFiltersAtPrefixedKeysExceptAtSelf":8,"Unit\\Controller\\ObjectsControllerTest::testContractsWithNonUnderscoreOffsetParam":8,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404OnDbException":8,"Unit\\Controller\\ObjectsControllerTest::testLogsWithLegacyParams":8,"Unit\\Controller\\ObjectsControllerTest::testExportReturnsCsvDownloadResponse":8,"Unit\\Controller\\ObjectsControllerTest::testExportReturnsExcelDownloadResponseByDefault":8,"Unit\\Controller\\ObjectsControllerTest::testExportUsesDefaultSlugsWhenNull":8,"Unit\\Controller\\ObjectsControllerTest::testExportCsvViaTypeParam":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithCommaSeparatedSchemasTriggersCrossTableSearch":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithArraySchemasTriggersCrossTableSearch":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithMultipleRegistersTriggersCrossTableSearch":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithMultipleSchemasTriggersCrossTableSearch":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithMultipleRegistersTriggersCrossTableSearch":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithUnderscorePrefixedMultiSchemas":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesCollectsNestedUuids":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesSkipsMetadataKeys":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesIgnoresNonUuidRelations":8,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturnsZipOnSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturns404WhenObjectDoesNotExistViaException":8,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesUsesCustomFilename":8,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturns500WhenZipCreationFails":8,"Unit\\Controller\\ObjectsControllerTest::testShowStripEmptyValuesPreservesZeroAndFalse":8,"Unit\\Controller\\ObjectsControllerTest::testShowStripsNestedAssociativeArraysWithAllEmptyValues":8,"Unit\\Controller\\ObjectsControllerTest::testShowStripsSequentialArraysPreservingNonEmptyItems":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithEmptyTruePreservesNestedEmptyValues":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendIncludesExtendedObjects":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithFieldsAsCommaSeparatedString":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithUnsetAsString":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithFilterAsString":8,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404WhenFindReturnsNull":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithoutAtSelfKeyDoesNotAddExtendData":8,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns403OnGenericExceptionIncludingDBException":8,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns403OnGenericExceptionIncludingDBException":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns403OnGenericExceptionWithUnlock":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns500OnGenericExceptionWithUnlock":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns500OnGenericExceptionWithUnlock":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithRegistersArrayOverridesRegisterParam":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithDuplicateSchemasDoesNotTriggerCrossTable":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithSingleSchemaParamUsesNormalSearch":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithEmptySchemasParamUsesNormalSearch":8,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithSingleSchemaArrayUsesNormalSearch":8,"Unit\\Controller\\ObjectsControllerTest::testIndexWithBothMultipleSchemasAndRegisters":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendAsCommaSeparatedStringParameterWorks":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesNestedRelationArrays":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesUsesAtSelfObjectForUuids":8,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendRegistersNullEntityCreatesEmptyRegisters":8,"Unit\\Controller\\ObjectsControllerTest::testPublishReturnsPublishedObject":8,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturnsAnalysis":8,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenObjectNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testLogsThrowsWhenFindFails":8,"Unit\\Controller\\ObjectsControllerTest::testImportReturnsSuccessWithValidFile":8,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404WhenObjectNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testShowStripsEmptyValuesByDefault":8,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns201OnSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns400OnValidationException":8,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns422OnHookStoppedException":8,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns403OnGenericException":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturnsObjectOnSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenObjectInWrongRegisterSchema":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns400OnValidationException":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns422OnHookStoppedException":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns403OnGenericException":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenObjectNotFound":8,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns403OnNotAuthorizedException":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturnsObjectOnSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns422OnHookStoppedException":8,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns500OnGenericException":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturnsObjectOnSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns422OnHookStoppedException":8,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns500OnGenericException":8,"Unit\\Controller\\ObjectsControllerTest::testLogsReturnsPaginatedLogsOnSuccess":8,"Unit\\Controller\\ObjectsControllerTest::testImportWithSchemaParameter":8,"Unit\\Controller\\ObjectsControllerTest::testShowReturnsObjectOnSuccess":8,"Unit\\Controller\\RegistersControllerTest::testIndexReturnsRegisters":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithPagination":8,"Unit\\Controller\\RegistersControllerTest::testCreateReturnsCreatedRegister":8,"Unit\\Controller\\RegistersControllerTest::testCreateRemovesInternalParamsAndId":8,"Unit\\Controller\\RegistersControllerTest::testCreateReturns500OnGenericException":8,"Unit\\Controller\\RegistersControllerTest::testUpdateReturnsUpdatedRegister":8,"Unit\\Controller\\RegistersControllerTest::testUpdateRemovesImmutableFields":8,"Unit\\Controller\\RegistersControllerTest::testPatchDelegatesToUpdate":8,"Unit\\Controller\\RegistersControllerTest::testDestroyReturns404WhenNotFound":8,"Unit\\Controller\\RegistersControllerTest::testDestroyReturns409OnValidationException":8,"Unit\\Controller\\RegistersControllerTest::testDestroyReturns500OnGenericException":8,"Unit\\Controller\\RegistersControllerTest::testSchemasReturns404WhenRegisterNotFound":8,"Unit\\Controller\\RegistersControllerTest::testSchemasReturns500OnGenericException":8,"Unit\\Controller\\RegistersControllerTest::testObjectsReturnsSearchResults":8,"Unit\\Controller\\RegistersControllerTest::testUpdateReturnsErrorOnDBException":8,"Unit\\Controller\\RegistersControllerTest::testShowThrowsWhenNotFound":8,"Unit\\Controller\\RegistersControllerTest::testExportReturns400OnException":8,"Unit\\Controller\\RegistersControllerTest::testImportReturns400WhenNoFile":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubReturns400WhenMissingParams":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubReturns404WhenRegisterNotFound":8,"Unit\\Controller\\RegistersControllerTest::testStatsReturnsRegisterStatistics":8,"Unit\\Controller\\RegistersControllerTest::testStatsReturns404WhenNotFound":8,"Unit\\Controller\\RegistersControllerTest::testStatsReturns500OnGenericException":8,"Unit\\Controller\\RegistersControllerTest::testIndexThrowsOnException":8,"Unit\\Controller\\RegistersControllerTest::testUpdateThrowsOnGenericException":8,"Unit\\Controller\\RegistersControllerTest::testCreateReturns409OnDBException":8,"Unit\\Controller\\RegistersControllerTest::testObjectsThrowsOnException":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubReturns500OnException":8,"Unit\\Controller\\RegistersControllerTest::testImportReturns400OnException":8,"Unit\\Controller\\RegistersControllerTest::testPublishReturns404WhenNotFound":8,"Unit\\Controller\\RegistersControllerTest::testPublishReturns400OnException":8,"Unit\\Controller\\RegistersControllerTest::testDepublishReturns404WhenNotFound":8,"Unit\\Controller\\RegistersControllerTest::testDepublishReturns400OnException":8,"Unit\\Controller\\RegistersControllerTest::testPatchThrowsWhenNotFound":8,"Unit\\Controller\\RegistersControllerTest::testExportConfigurationFormatSuccess":8,"Unit\\Controller\\RegistersControllerTest::testExportCsvMissingSchemaReturns400":8,"Unit\\Controller\\RegistersControllerTest::testPublishSuccess":8,"Unit\\Controller\\RegistersControllerTest::testDepublishSuccess":8,"Unit\\Controller\\RegistersControllerTest::testPublishWithCustomDate":8,"Unit\\Controller\\RegistersControllerTest::testDepublishWithCustomDate":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubSuccess":8,"Unit\\Controller\\RegistersControllerTest::testStatsContainsExpectedKeys":8,"Unit\\Controller\\RegistersControllerTest::testCreateWithFullParams":8,"Unit\\Controller\\RegistersControllerTest::testObjectsWithPaginationParams":8,"Unit\\Controller\\RegistersControllerTest::testUpdateReturnsErrorOnDoesNotExist":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithOffsetParam":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithFiltersParam":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithExtendAsString":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithExtendSchemasExpandsSchemaIds":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithExtendSchemasSkipsMissingSchema":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithSelfStatsExtend":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithSchemasAndSelfStatsExtend":8,"Unit\\Controller\\RegistersControllerTest::testIndexWithSchemasAndStatsNoCountForSchema":8,"Unit\\Controller\\RegistersControllerTest::testIndexPageConvertsToOffset":8,"Unit\\Controller\\RegistersControllerTest::testShowWithSelfStatsExtend":8,"Unit\\Controller\\RegistersControllerTest::testShowWithExtendAsArray":8,"Unit\\Controller\\RegistersControllerTest::testShowWithoutStatsExtend":8,"Unit\\Controller\\RegistersControllerTest::testCreateReturns409OnDatabaseConstraintException":8,"Unit\\Controller\\RegistersControllerTest::testUpdateReturns409OnDatabaseConstraintException":8,"Unit\\Controller\\RegistersControllerTest::testExportConfigurationWithIncludeObjects":8,"Unit\\Controller\\RegistersControllerTest::testExportCsvWithSchemaSuccess":8,"Unit\\Controller\\RegistersControllerTest::testExportExcelSuccess":8,"Unit\\Controller\\RegistersControllerTest::testImportExcelSuccess":8,"Unit\\Controller\\RegistersControllerTest::testImportCsvMissingSchemaReturns400":8,"Unit\\Controller\\RegistersControllerTest::testImportCsvWithSchemaSuccess":8,"Unit\\Controller\\RegistersControllerTest::testImportAutoDetectsExcelFromExtension":8,"Unit\\Controller\\RegistersControllerTest::testImportAutoDetectsCsvFromExtension":8,"Unit\\Controller\\RegistersControllerTest::testImportAutoDetectsConfigurationFromJsonExtension":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubUsesDefaultPathWhenEmpty":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubNonDefaultBranchMessage":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubGetRepositoryInfoFails":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubGetFileShaFails":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubStripsLeadingSlashFromPath":8,"Unit\\Controller\\RegistersControllerTest::testCreateRemovesUnderscorePrefixedParams":8,"Unit\\Controller\\RegistersControllerTest::testUpdateRemovesUnderscorePrefixedParams":8,"Unit\\Controller\\RegistersControllerTest::testImportConfigurationWithObjectsInResult":8,"Unit\\Controller\\RegistersControllerTest::testImportConfigurationNoRegistersInResultMergesSchemas":8,"Unit\\Controller\\RegistersControllerTest::testImportConfigurationGetUploadedJsonReturnsJsonResponse":8,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubCustomCommitMessage":8,"Unit\\Controller\\RegistersControllerTest::testSchemasWithStringId":8,"Unit\\Controller\\RegistersControllerTest::testDestroyCallsDeleteOnService":8,"Unit\\Controller\\RegistersControllerTest::testIndexExtendSchemasWithNullSchemasField":8,"Unit\\Controller\\RegistersControllerTest::testExportConfigurationJsonEncodeFails":8,"Unit\\Controller\\RegistersControllerTest::testDestroyReturns500OnDatabaseConstraintException":8,"Unit\\Controller\\RegistersControllerTest::testShowReturnsRegister":8,"Unit\\Controller\\RegistersControllerTest::testDestroyReturnsEmptyOnSuccess":8,"Unit\\Controller\\RegistersControllerTest::testSchemasReturnsSchemasList":8,"Unit\\Controller\\SchemasControllerTest::testIndexWithPagination":8,"Unit\\Controller\\SchemasControllerTest::testShowReturnsSchema":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturnsCreatedSchema":8,"Unit\\Controller\\SchemasControllerTest::testCreateRemovesInternalParams":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns500OnException":8,"Unit\\Controller\\SchemasControllerTest::testUpdateReturnsUpdatedSchema":8,"Unit\\Controller\\SchemasControllerTest::testPatchDelegatesToUpdate":8,"Unit\\Controller\\SchemasControllerTest::testDestroyReturns500WhenNotFound":8,"Unit\\Controller\\SchemasControllerTest::testDownloadReturnsSchema":8,"Unit\\Controller\\SchemasControllerTest::testDownloadReturns404WhenNotFound":8,"Unit\\Controller\\SchemasControllerTest::testRelatedReturnsRelationships":8,"Unit\\Controller\\SchemasControllerTest::testRelatedReturns404WhenSchemaNotFound":8,"Unit\\Controller\\SchemasControllerTest::testRelatedReturns500OnGenericException":8,"Unit\\Controller\\SchemasControllerTest::testStatsReturnsSchemaStatistics":8,"Unit\\Controller\\SchemasControllerTest::testStatsReturns404WhenSchemaNotFound":8,"Unit\\Controller\\SchemasControllerTest::testExploreReturnsExplorationResults":8,"Unit\\Controller\\SchemasControllerTest::testExploreReturns500OnException":8,"Unit\\Controller\\SchemasControllerTest::testUpdateFromExplorationReturns400WhenNoProperties":8,"Unit\\Controller\\SchemasControllerTest::testUpdateFromExplorationSuccess":8,"Unit\\Controller\\SchemasControllerTest::testUpdateFromExplorationReturns500OnException":8,"Unit\\Controller\\SchemasControllerTest::testPublishSetsPublicationDate":8,"Unit\\Controller\\SchemasControllerTest::testPublishReturns404WhenSchemaNotFound":8,"Unit\\Controller\\SchemasControllerTest::testDepublishSetsDepublicationDate":8,"Unit\\Controller\\SchemasControllerTest::testDepublishReturns404WhenSchemaNotFound":8,"Unit\\Controller\\SchemasControllerTest::testUpdateRemovesImmutableFields":8,"Unit\\Controller\\SchemasControllerTest::testUpdateReturns500OnException":8,"Unit\\Controller\\SchemasControllerTest::testIndexWithPageBasedPagination":8,"Unit\\Controller\\SchemasControllerTest::testIndexWithExtendStats":8,"Unit\\Controller\\SchemasControllerTest::testIndexWithExtendStatsDefaultsForMissingSchema":8,"Unit\\Controller\\SchemasControllerTest::testIndexWithFilters":8,"Unit\\Controller\\SchemasControllerTest::testIndexExtendedByPopulated":8,"Unit\\Controller\\SchemasControllerTest::testShowReturns404OnDoesNotExistException":8,"Unit\\Controller\\SchemasControllerTest::testShowReturns404OnValidationException":8,"Unit\\Controller\\SchemasControllerTest::testShowReturns500OnGenericException":8,"Unit\\Controller\\SchemasControllerTest::testShowWithExtendStats":8,"Unit\\Controller\\SchemasControllerTest::testShowWithAllOfAddsPropertyMetadata":8,"Unit\\Controller\\SchemasControllerTest::testShowWithExtendAsString":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturnsErrorOnDBException":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturnsErrorOnDatabaseConstraintException":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnValidationError":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnMustBeError":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnRequiredError":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnFormatError":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnPropertyAtError":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnAuthorizationError":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns409OnConstraintError":8,"Unit\\Controller\\SchemasControllerTest::testCreateReturns409OnDuplicateError":8,"Unit\\Controller\\SchemasControllerTest::testUpdateInvalidatesCaches":8,"Unit\\Controller\\SchemasControllerTest::testUpdateReturnsErrorOnDBException":8,"Unit\\Controller\\SchemasControllerTest::testUpdateReturnsErrorOnDatabaseConstraintException":8,"Unit\\Controller\\SchemasControllerTest::testUpdateReturns400OnValidationError":8,"Unit\\Controller\\SchemasControllerTest::testUpdateReturns409OnConstraintError":8,"Unit\\Controller\\SchemasControllerTest::testUpdateRemovesUnderscoreParams":8,"Unit\\Controller\\SchemasControllerTest::testDestroyInvalidatesCaches":8,"Unit\\Controller\\SchemasControllerTest::testDestroyReturns409OnValidationException":8,"Unit\\Controller\\SchemasControllerTest::testStatsReturns500OnGenericException":8,"Unit\\Controller\\SchemasControllerTest::testPublishWithCustomDate":8,"Unit\\Controller\\SchemasControllerTest::testPublishReturns400OnGenericException":8,"Unit\\Controller\\SchemasControllerTest::testPublishInvalidatesCaches":8,"Unit\\Controller\\SchemasControllerTest::testDepublishWithCustomDate":8,"Unit\\Controller\\SchemasControllerTest::testDepublishReturns400OnGenericException":8,"Unit\\Controller\\SchemasControllerTest::testDepublishInvalidatesCaches":8,"Unit\\Controller\\SchemasControllerTest::testUploadUpdateDelegatesToUpload":8,"Unit\\Controller\\SchemasControllerTest::testUploadNewSchemaWithoutId":8,"Unit\\Controller\\SchemasControllerTest::testUploadReturnsErrorResponseFromUploadService":8,"Unit\\Controller\\SchemasControllerTest::testUploadNewSchemaWithEmptyTitle":8,"Unit\\Controller\\SchemasControllerTest::testUploadExistingSchemaById":8,"Unit\\Controller\\SchemasControllerTest::testUploadReturns500OnGenericException":8,"Unit\\Controller\\SchemasControllerTest::testUploadReturns400OnValidationException":8,"Unit\\Controller\\SchemasControllerTest::testUploadReturns409OnConstraintException":8,"Unit\\Controller\\SchemasControllerTest::testUploadReturnsErrorOnDBException":8,"Unit\\Controller\\SchemasControllerTest::testUploadReturnsErrorOnDatabaseConstraintException":8,"Unit\\Controller\\SchemasControllerTest::testUploadNewSchemaWithOrganisationAlreadySet":8,"Unit\\Controller\\SchemasControllerTest::testRelatedWithOutgoingReferences":8,"Unit\\Controller\\SchemasControllerTest::testIndexReturnsSchemas":8,"Unit\\Controller\\SchemasControllerTest::testDestroyReturnsEmptyOnSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSchemaMappingReturnsResultsOnSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSchemaMappingReturns422WhenMappingThrows":8,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectReturnsSuccess":8,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectWithProvider":8,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectWithEmptyArrayResult":8,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectReturns500OnObjectNotFound":8,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectReturns500OnVectorizationFailure":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsBoundaryLimitOne":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsBoundaryLimitThousand":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturnsEmptyResult":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturnsSuccessWithResults":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsHasMoreWhenFullPage":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsHasMoreFalseWhenPartialPage":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsWithProvider":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsWithNonArrayResult":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsWithOffset":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturns500OnException":8,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsWithNullFilters":8,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsReturnsStats":8,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsWithZeroObjects":8,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsWithAllVectorized":8,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsWithMissingObjectVectorsKey":8,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsProgressRounding":8,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsReturns500OnMapperException":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testConstructorFieldTypes":7,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterMapperTest::testConstructorCreatesInstance":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterMapperTest::testGetTableNameReturnsCorrectValue":8,"Unit\\Db\\SearchTrailTest::testConstructorRegistersFieldTypes":7,"Unit\\Db\\SearchTrailTest::testJsonSerializeAllFieldsPresent":7,"Unit\\Listener\\CommentsEntityListenerTest::testEarlyReturnForNonCommentsEntityEvent":8,"Unit\\Listener\\CommentsEntityListenerTest::testRegistersOpenregisterEntityCollection":8,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenInvalidAlgorithmHeader":7,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtInvalidAlgorithmDetailsContainReason":7,"Unit\\Service\\AuthorizationServiceTest::testGetJwkHmacHs256ReturnsJwkSet":8,"Unit\\Service\\AuthorizationServiceTest::testGetJwkHmacHs384ReturnsJwkSet":8,"Unit\\Service\\AuthorizationServiceTest::testGetJwkHmacHs512ReturnsJwkSet":8,"Unit\\Service\\AuthorizationServiceTest::testGetJwkRsaReturnsJwkSet":8,"Unit\\Service\\AuthorizationServiceTest::testGetJwkPsReturnsJwkSet":8,"Unit\\Service\\AuthorizationServiceTest::testGetJwkThrowsForUnsupportedAlgorithm":7,"Unit\\Service\\AuthorizationServiceTest::testGetJwkUnsupportedAlgorithmDetailsContainAlgorithm":8,"Unit\\Service\\AuthorizationServiceTest::testCheckHeadersWithValidHs256Token":8,"Unit\\Service\\AuthorizationServiceTest::testCheckHeadersWithValidHs384Token":8,"Unit\\Service\\AuthorizationServiceTest::testCheckHeadersWithValidHs512Token":8,"Unit\\Service\\AuthorizationServiceTest::testHmacAlgorithmsConstant":8,"Unit\\Service\\AuthorizationServiceTest::testPkcs1AlgorithmsConstant":8,"Unit\\Service\\AuthorizationServiceTest::testPssAlgorithmsConstant":8,"Unit\\Service\\AuthorizationServiceTest::testAllAlgorithmsAreCovered":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testGracefulHandlingWhenUserSessionIsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testGracefulHandlingWhenOrganisationServiceFails":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testOwnerMetadataSetFromCurrentUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testOrganizationMetadataSetFromOrganisationService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testExistingMetadataIsPreserved":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testBulkOperationsWithMixedMetadataScenarios":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testCachingOptimizationDuringBulkOperations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportMappingCreatesNewWhenFindThrows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportMappingSetsVersionFromParam":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testGetDuplicateRegisterInfoFormatsCreatedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testGetDuplicateRegisterInfoShowsUnknownWhenNoCreatedDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesItemsRefFromSchemasMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesItemsObjConfigRegisterFromDatabase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaUnsetsItemsObjConfigRegisterWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesItemsObjConfigSchemaFromDatabase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaUnsetsItemsObjConfigSchemaWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesLegacyRegisterFromRegistersMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesLegacyItemsRegisterFromRegistersMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaUpdatesWithOwnerAndApp":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonPass2SkipsSchemaNotInMapBySlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonPass2CatchesReImportException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonUpdatesObjectWhenVersionHigher":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonSkipsObjectWhenVersionNotHigher":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testWorkflowHookWiringSkipsWhenDeployedNotInMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testWorkflowHookWiringDeduplicatesExistingHooks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppContinuesWhenVersionNotNewer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppCatchesSourceUrlException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppCatchesFindByAppException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppUpdatesLegacyGithubOnExistingConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppSetsDescriptionFromXOpenregister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataResolvesExternalConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataWarnsOnMissingExternalRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataWarnsOnMissingExternalSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataCatchesExternalRegisterException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataCatchesExternalSchemaException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataFindsSchemaFromDatabase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppSkipsNonSchemaInResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testCreateOrUpdateConfigurationWrapsException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromFilePathThrowsOnMissingFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaStripsBinaryFormatFromItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaConvertsStdClassItemsObjectConfigToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonCallsOpenConnectorIntegration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonCatchesOpenConnectorFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonCreatesNewObjectWhenNoExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataFallsToBlobStorageWithoutUnifiedMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaCatchesValidationExceptionAndCreatesNew":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonResolvesRegisterSchemasFromMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonWarnsWhenRegisterSchemaNotInMap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonContinuesOnSchemaFailureInPass1":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testCreateOrUpdateConfigurationSetsNestedGithub":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testCreateOrUpdateConfigurationSetsLegacyGithub":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetMagicMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetObjectMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetWorkflowEngineRegistry":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetDeployedWorkflowMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetOpenConnectorConfigurationService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesObjConfigRegisterFromDatabase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaUnsetsObjConfigRegisterWhenNotFoundInDB":8,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesProcessesAllObjects":8,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesCountsFailures":8,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesWithRegisterFilter":8,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesWithSchemaFilter":8,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesWithBothFilters":8,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesThrowsOnFindAllError":8,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesCountsFailures":8,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesWithRegisterFilter":8,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesWithSchemaFilter":8,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesWithBothFilters":8,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesThrowsOnError":8,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesAggregatesProcessedAndFailed":8,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesWithFilters":8,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesThrowsWhenObjectRecalcFails":8,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesThrowsWhenLogRecalcFails":8,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasReturnsStructuredData":8,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasFiltersByRegisterId":8,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasFiltersBySchemaId":8,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasExcludesNonMatchingSchemas":8,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasNoRegisters":8,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasThrowsOnError":8,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasStatsErrorReturnsZeroes":8,"Unit\\Service\\DashboardServiceTest::testCalculateReturnsFullResponseNoFilters":8,"Unit\\Service\\DashboardServiceTest::testCalculateWithRegisterId":8,"Unit\\Service\\DashboardServiceTest::testCalculateWithRegisterAndSchemaId":8,"Unit\\Service\\DashboardServiceTest::testCalculateWithSchemaNotBelongingToRegisterThrows":8,"Unit\\Service\\DashboardServiceTest::testCalculateWithNonExistentRegisterThrows":8,"Unit\\Service\\DashboardServiceTest::testCalculateWithNonExistentSchemaThrows":8,"Unit\\Service\\DashboardServiceTest::testCalculateSuccessRateWithProcessed":8,"Unit\\Service\\DashboardServiceTest::testCalculateSuccessRateWithAllSuccessful":8,"Unit\\Service\\DashboardServiceTest::testCalculateWithSchemaIdOnlyNoRegister":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionChartDataReturnsData":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionChartDataWithDateFilters":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionChartDataReturnsEmptyOnError":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsByRegisterChartDataReturnsData":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsByRegisterChartDataWithFilters":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsByRegisterChartDataReturnsEmptyOnError":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySchemaChartDataReturnsData":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySchemaChartDataWithFilters":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySchemaChartDataReturnsEmptyOnError":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySizeChartDataReturnsData":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySizeChartDataWithFilters":8,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySizeChartDataReturnsEmptyOnError":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailStatisticsReturnsData":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailStatisticsWithFilters":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailStatisticsReturnsZeroOnError":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionDistributionReturnsData":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionDistributionWithFilters":8,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionDistributionReturnsEmptyOnError":8,"Unit\\Service\\DashboardServiceTest::testGetMostActiveObjectsReturnsData":8,"Unit\\Service\\DashboardServiceTest::testGetMostActiveObjectsWithFilters":8,"Unit\\Service\\DashboardServiceTest::testGetMostActiveObjectsReturnsEmptyOnError":8,"Unit\\Service\\DashboardServiceTest::testOrphanedStatsWithMultipleRegistersAndSchemas":8,"Unit\\Service\\DashboardServiceTest::testOrphanedStatsErrorFallbackReturnsZeroes":8,"Unit\\Service\\DashboardServiceTest::testGetStatsErrorBranchReturnsZeroStructure":8,"Unit\\Service\\DashboardServiceTest::testGetStatsHandlesWebhookLogsMissingTotal":8,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesCombinesResults":8,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesProcessesAllLogs":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testRegisterAddsRegistration":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testRegisterIgnoresDuplicateKey":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testRegisterWithCustomIcon":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testRegisterWithDefaultIcon":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveReturnsNullWhenNoRegistrations":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveReturnsRegistrationByIds":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveReturnsNullForUnknownIds":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveUrlReturnsNullWhenNoRegistration":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveUrlResolvesTemplate":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveIconReturnsNullWhenNoRegistration":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveIconReturnsIcon":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testHasRegistrationsReturnsFalseWhenEmpty":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testHasRegistrationsReturnsTrueAfterRegister":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResetClearsAllRegistrations":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveHandlesRegisterMapperException":8,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveHandlesSchemaMapperException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testSchemaCreationWithActiveOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testObjectCreationWithActiveOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testEntityAccessWithinSameOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testEntityAccessAcrossOrganisations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testCrossOrganisationObjectCreation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testEntityOrganisationAssignmentValidation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testBulkEntityOperationsWithOrganisationContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testEntityOrganisationInheritance":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testRegisterCreationWithActiveOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsUserAdminNullUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsUserAdminNoAdminGroup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsUserAdminTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsUserAdminFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyUuidFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyWithRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyArrayItemsRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyRegularString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyArrayNoItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsSingleString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsJsonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsNullValuesInArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesSingleString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesSingleStringNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesJsonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringScalar":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringBool":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringObjectWithToString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIdentifyNameCompanionColumns":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testGetObjectValueIdHeader":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testGetObjectValueRegularField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testGetObjectValueMissingField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testExportToCsvMultipleSchemasThrows":8,"Unit\\Service\\ExportServiceGapTest::testExportToCsvThrowsForMultipleSchemas":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithNullRegisterAndSchema":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithSchema":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelSkipsHiddenProperties":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelSkipsInvisibleProperties":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelSkipsRbacRestrictedProperties":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelAddsRelationCompanionColumns":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelAddsRefCompanionColumns":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelAddsArrayUuidCompanionColumns":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithNullUserSkipsMetadata":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithAdminUserAddsMetadata":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithNonAdminGroupNull":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelSkipsDefaultFields":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithRegisterAllSchemas":8,"Unit\\Service\\ExportServiceGapTest::testExportToExcelNoCompanionForNonRelation":8,"Unit\\Service\\ExportServiceTest::testExportToCsvThrowsForMultipleSchemas":8,"Unit\\Service\\ExportServiceTest::testExportToExcelAddsCompanionColumnsForRefRelations":8,"Unit\\Service\\ExportServiceTest::testExportSkipsBuiltInHeaderFields":8,"Unit\\Service\\ExportServiceTest::testExportToExcelNoMetadataForAnonymousUser":8,"Unit\\Service\\ExportServiceTest::testExportToExcelNoMetadataWhenAdminGroupDoesNotExist":8,"Unit\\Service\\ExportServiceTest::testExportPassesSelfMetadataFilters":8,"Unit\\Service\\ExportServiceTest::testExportSkipsNonSelfFilters":8,"Unit\\Service\\ExportServiceTest::testExportWithMultiFilterFalse":8,"Unit\\Service\\ExportServiceTest::testExportWithMultiFilterShortName":8,"Unit\\Service\\ExportServiceTest::testExportMetadataDateTimeFieldFormatsCorrectly":8,"Unit\\Service\\ExportServiceTest::testExportMetadataArrayFieldConvertedToJson":8,"Unit\\Service\\ExportServiceTest::testExportMetadataScalarFieldReturnsString":8,"Unit\\Service\\ExportServiceTest::testExportMetadataNullFieldReturnsNull":8,"Unit\\Service\\ExportServiceTest::testExportCompanionColumnWithNullRelation":8,"Unit\\Service\\ExportServiceTest::testExportResolvesJsonEncodedArrayOfUuids":8,"Unit\\Service\\ExportServiceTest::testExportPreSeedsNameMapFromLoadedObjects":8,"Unit\\Service\\ExportServiceTest::testExportNoCompanionColumnForNonRelationProperty":8,"Unit\\Service\\ExportServiceTest::testExportCompanionColumnForArrayWithRefItems":8,"Unit\\Service\\ExportServiceTest::testExportToExcelWithNoSchemaUsesDataAsSheetTitle":8,"Unit\\Service\\ExportServiceTest::testExportSkipsExplicitlyInvisibleProperties":8,"Unit\\Service\\ExportServiceTest::testExportHandlesIntegerValuesAsStrings":8,"Unit\\Service\\ExportServiceTest::testExportWithRegisterAndSchemaPassesBothIds":8,"Unit\\Service\\ExportServiceTest::testExportFallsBackToUuidWhenNameNotResolved":8,"Unit\\Service\\ExportServiceTest::testExportHandlesJsonArrayWithEmptyStrings":8,"Unit\\Service\\ExportServiceTest::testExportWithMultipleObjectsMixedRelationData":8,"Unit\\Service\\ExportServiceTest::testExportWithNoRelationPropertiesSkipsBulkResolve":8,"Unit\\Service\\ExportServiceTest::testExportResolvesNativeArrayOfUuids":8,"Unit\\Service\\ExportServiceTest::testExportHandlesEmptyStringUuidValue":8,"Unit\\Service\\ExportServiceTest::testExportHandlesMissingFieldInObjectData":8,"Unit\\Service\\ExportServiceTest::testExportToCsvWithRegisterAndSchema":8,"Unit\\Service\\ExportServiceTest::testExportWithMultiFilterTrue":8,"Unit\\Service\\ExportServiceTest::testExportResolvesArrayWithNonStringItems":8,"Unit\\Service\\ExportServiceTest::testExportResolvesJsonArrayWithNonStringItems":8,"Unit\\Service\\ExportServiceTest::testExportDefaultMultitenancy":8,"Unit\\Service\\ExportServiceTest::testExportToExcelWithSingleSchema":8,"Unit\\Service\\ExportServiceTest::testExportToExcelWithRegisterExportsAllSchemas":8,"Unit\\Service\\ExportServiceTest::testExportToExcelWithObjectData":8,"Unit\\Service\\ExportServiceTest::testExportToExcelSkipsHiddenOnCollectionProperties":8,"Unit\\Service\\ExportServiceTest::testExportToExcelSkipsRbacRestrictedProperties":8,"Unit\\Service\\ExportServiceTest::testExportToExcelNoMetadataForNonAdmin":8,"Unit\\Service\\ExportServiceTest::testExportToExcelAddsCompanionColumnsForUuidRelations":8,"Unit\\Service\\ExportServiceTest::testExportToExcelAddsCompanionColumnsForArrayOfUuids":8,"Unit\\Service\\ExportServiceTest::testExportToExcelResolvesUuidNames":8,"Unit\\Service\\ExportServiceTest::testExportHandlesNullValuesGracefully":8,"Unit\\Service\\ExportServiceTest::testExportHandlesArrayValuesAsJson":8,"Unit\\Service\\ExportServiceTest::testExportToCsvWithDataRows":8,"Unit\\Service\\ExportServiceTest::testExportToCsvSkipsHiddenOnCollectionProperties":8,"Unit\\Service\\ExportServiceTest::testExportToExcelHandlesBooleanValues":8,"Unit\\Service\\ExportServiceTest::testExportToExcelHandlesNestedObjectValues":8,"Unit\\Service\\ExportServiceTest::testExportToCsvWithRbacRestrictions":8,"Unit\\Service\\ExportServiceTest::testExportToExcelWithFilters":8,"Unit\\Service\\ExportServiceTest::testExportToCsvWithMultipleObjectsVerifyRowCount":8,"Unit\\Service\\ExportServiceTest::testExportToExcelWithArrayOfUuidsResolvesNames":8,"Unit\\Service\\ExportServiceTest::testExportToCsvHandlesSpecialCharsInValues":8,"Unit\\Service\\ExportServiceTest::testExportToExcelWithRegisterAndSchemaOverrideUsesSchema":8,"Unit\\Service\\ExportServiceTest::testExportToCsvReturnsCsvString":8,"Unit\\Service\\ExportServiceTest::testExportToExcelAddsMetadataColumnsForAdmin":8,"Unit\\Service\\ExportServiceTest::testExportToExcelWithEmptySchemaProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testClearCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeSmall":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeMedium":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeLarge":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeZero":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeBoundary1000":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeBoundary1001":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeBoundary10000":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeBoundary10001":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithNoImportedObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupEmptySummary":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithCustomDelay":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithMultipleSheets":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithNonArrayEntry":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupWithData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupImmediate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupUsesRecommendedMode":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupCapsMaxObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithInvalidPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvIncludesSchemaInfo":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithUpdatedObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithPublishDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithIdColumn":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithAtColumnNotSelfPrefix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithSelfPublishedColumnForAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithMultipleRowsAndMixedResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithNumberProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithObjectProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithRelatedObjectProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithValidationErrorMissingFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithValidFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithSchemaInfo":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelMultiSchemaWithMatchingSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithPublishEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithEmptySheet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithEmptyHeaders":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithTypedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithUnderscoreColumns":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithAtColumnsAsAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithAtColumnsAsNonAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithIdColumn":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithNoRegisterOrSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithValidationErrors":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithDeduplication":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithAtOtherColumnAsAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithNoSchemaSkipsTransform":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValueMySqlFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValueIso8601WithTimezone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValueIso8601WithoutTimezone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValueDateOnly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValuePassThrough":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyPublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyCreated":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyUpdated":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyOrganisationValidUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyOrganisationInvalidUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyOther":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeNumber":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeBoolean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeBooleanAlreadyBool":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeArrayQuotedValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeArrayNonStringNonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeObjectRelatedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeDefaultString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeNullValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeEmptyStringValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeNoTypeKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeObjectInvalidJson":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeArrayInvalidJson":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testAddPublishedDateToObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testIsUserAdminWithNullUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testIsUserAdminWithNoAdminGroup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testIsUserAdminTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testIsUserAdminFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testValidateObjectProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testValidateObjectPropertiesWithBodyAndPayload":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformObjectBySchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformObjectBySchemaWithAllTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImported":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImportedEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImportedWithNonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImportedOnlyUpdated":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImportedMissingKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testBuildColumnMapping":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testBuildColumnMappingWithEmptySheet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testBuildColumnMappingStopsAtEmptyColumn":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testBuildColumnMappingTrimsWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testExtractRowData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testExtractRowDataEmptyRow":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testExtractRowDataPartialRow":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testExtractRowDataTrimsWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetSchemaBySlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetSchemaBySlugThrowsOnNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformCsvRowToObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformCsvRowToObjectWithIdField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformCsvRowToObjectSkipsEmptyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformCsvRowToObjectCachesSchemaProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithoutSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithoutRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithIdField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectSkipsEmptyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithUnderscoreColumns":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithAtColumnsAsAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithAtColumnsAsNonAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithAtOtherColumn":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithWhitespaceOnly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithJsonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithCommaSeparated":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithSingleValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithExistingArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithNonStringNonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithValidJson":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithInvalidJson":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithPlainString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithExistingArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithStdClass":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToBooleanWithVariousInputs":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithEventsAndEnrichDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvPerformanceMetricsComplete":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithSelfCreatedColumnAsAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvRequiresSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithPublishEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithTypedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithUnchangedObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithValidationErrors":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithEmptyDataRows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithUnderscoreColumnsSkipped":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithAtColumnsSkippedForNonAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvPerformanceMetrics":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithValidFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithAtColumnsProcessedForAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithInvalidPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testRbacIntegrationWithMultiTenancy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testSearchFilteringByOrganisation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testAuditTrailOrganisationContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testCrossOrganisationAccessPrevention":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testMultiTenancyWithComplexRelationships":8,"Unit\\Service\\LogServiceTest::testGetAllLogsWithConfig":8,"Unit\\Service\\LogServiceTest::testCountAllLogs":8,"Unit\\Service\\LogServiceTest::testCountAllLogsWithFilters":8,"Unit\\Service\\LogServiceTest::testGetLogsReturnsAuditTrails":8,"Unit\\Service\\LogServiceTest::testGetLogsAllowsAccessWhenRegisterSchemaDeleted":8,"Unit\\Service\\LogServiceTest::testCountReturnsLogCount":8,"Unit\\Service\\LogServiceTest::testCountReturnsZeroWhenNoLogs":8,"Unit\\Service\\LogServiceTest::testGetAllLogsWithDefaults":8,"Unit\\Service\\LogServiceTest::testGetLog":8,"Unit\\Service\\LogServiceTest::testDeleteLogSuccess":8,"Unit\\Service\\LogServiceTest::testDeleteLogThrowsOnFailure":8,"Unit\\Service\\LogServiceTest::testDeleteLogsByIds":8,"Unit\\Service\\LogServiceTest::testDeleteLogsByFilters":8,"Unit\\Service\\LogServiceTest::testExportLogsXml":8,"Unit\\Service\\LogServiceTest::testExportLogsTxt":8,"Unit\\Service\\LogServiceTest::testExportLogsCsvEmpty":8,"Unit\\Service\\LogServiceTest::testExportLogsJson":8,"Unit\\Service\\LogServiceTest::testExportLogsCsv":8,"Unit\\Service\\LogServiceTest::testExportLogsUnsupportedFormat":8,"Unit\\Service\\LogServiceTest::testDeleteLogsByIdsWithFailures":8,"Unit\\Service\\LogServiceTest::testGetLogsWithConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testGetTableNameForRegisterSchema#basic_combination":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testGetTableNameForRegisterSchema#different_ids":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testGetTableNameForRegisterSchema#large_ids":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#enabled_in_schema":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#disabled_in_schema_global_enabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#disabled_in_schema_global_disabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#not_set_in_schema_global_enabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#not_set_in_schema_global_disabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#null_schema_config_global_enabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#simple_name":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#camelcase_name":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#name_with_spaces":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#name_with_special_chars":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#string_property":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#string_with_max_length":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#email_format":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#uuid_format":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#datetime_format":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#integer_property":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#small_integer":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#big_integer":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#number_property":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#boolean_property":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#array_property":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#object_property":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#valid_json_object":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#valid_json_array":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#invalid_json":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#plain_string":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#null_string":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testObjectDataPreparationForTable":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testClearCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testRegisterSchemaVersionCalculation":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testMetadataColumnsGeneration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsDelegatesToSaveObjectsHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsSkipsCacheInvalidationWhenNoObjectsAffected":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsCacheInvalidationFailureDoesNotBreakOperation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsWithRegisterAndSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsReturnsEmptyArrayForEmptyInput":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsWithRbacFiltering":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsWithoutRbacOrMultitenancy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsSkipsCacheWhenNothingDeleted":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsCacheFailureDoesNotBreak":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsReturnsEmptyArrayForEmptyInput":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsWithPermissionFiltering":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsWithDatetime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsSkipsCacheWhenNonePublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDepublishObjectsReturnsEmptyArrayForEmptyInput":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDepublishObjectsWithFiltering":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDepublishObjectsCacheFailureDoesNotBreak":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsBySchemaWithResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsBySchemaSkipsCacheWhenNonePublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsBySchemaUsesBlobStorageWhenNoMagicMapping":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsBySchemaThrowsOnFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsByRegisterWithResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsByRegisterSkipsCacheWhenNoneDeleted":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsByRegisterCacheFailureDoesNotBreak":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsBySchemaUsesMagicTableWhenEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsBySchemaSkipsCacheWhenNoneDeleted":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsBySchemaWithHardDelete":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsBySchemaWithPublishAll":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsBySchemaPublishCacheFailureDoesNotBreak":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDepublishObjectsWithoutRbacOrMultitenancy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDepublishObjectsSkipsCacheWhenNoneDepublished":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsCacheInvalidationCountsUpdates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testGetObjectCacheMissLoadsFromDb":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testGetObjectReturnsNullOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testPreloadObjectsEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testPreloadObjectsAllAlreadyCached":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testPreloadObjectsBulkLoadFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testPreloadObjectsSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testGetStatsInitial":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testGetStatsAfterCacheMiss":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testSetObjectNameStoresInBothCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testSetObjectNameDistributedCacheFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearSearchCacheWithoutPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearSearchCacheWithPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearSearchCacheDistributedFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearAllCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearAllCachesDistributedFailures":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearCacheDelegatesToClearAllCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testInvalidateForObjectChangeCreate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testInvalidateForObjectChangeDelete":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testInvalidateForObjectChangeNullObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testInvalidateForObjectChangeNullSchemaId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testConstructorHandlesCacheFactoryException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testConstructorHandlesCacheFactoryException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetObjectCacheHit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetObjectCacheMissLoadsFromDb":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetObjectCacheMissDbException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetStatsEmptyCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetStatsWithHitsAndMisses":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPreloadObjectsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPreloadObjectsAllCached":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPreloadObjectsFromDb":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPreloadObjectsDbException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCacheObjectEviction":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearSearchCacheWithPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearSearchCacheNoPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearSearchCacheDistributedFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearAllCachesSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearAllCachesDistributedFailures":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearCacheDelegatesToClearAllCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testSetObjectNameSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testSetObjectNameDistributedFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testSetObjectNameEnforcesTtl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSingleObjectNameInMemoryHit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSingleObjectNameDistributedHit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSingleObjectNameDistributedFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetDistributedNameCacheCountNoCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetDistributedNameCacheCountWithValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetDistributedNameCacheCountNullValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetDistributedNameCacheCountException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearNameCacheSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearNameCacheDistributedFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeCreate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeDelete":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeDeleteDistributedFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeNullObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeWithExplicitIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSolrDashboardStatsNoContainer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSolrDashboardStatsNoIndexService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCommitSolrNoIndexService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCommitSolrSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCommitSolrFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCommitSolrException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testExtractDynamicFieldsFromObjectAllTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testExtractDynamicFieldsFromObjectWithPrefix":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIsDateStringWithDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIsDateStringWithNonDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIsDateStringWithNonString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testFormatDateForSolrValid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testFormatDateForSolrInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPersistNameCacheToDistributedNoCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPersistNameCacheToDistributedSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPersistNameCacheToDistributedFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetMultipleObjectNamesEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetAllObjectNamesTriggersWarmup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetAllObjectNamesForceWarmup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIndexObjectInSolrNoService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIndexObjectInSolrServiceNotAvailable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIndexObjectInSolrSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIndexObjectInSolrFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testRemoveObjectFromSolrNoService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testRemoveObjectFromSolrSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testRemoveObjectFromSolrException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testCanDeleteDelegatesToIntegrityService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testCanDeleteReturnsNonDeletableAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteSetsDeletedBySystemWhenNoUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteCreatesAuditTrailWhenEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteSkipsAuditTrailWhenDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteReturnsTrueWhenUpdateSucceeds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectThrowsWhenDeletionIsBlocked":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectCascadesScalarProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectDoesNotCascadeWithoutCascadeFlag":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectReturnsFalseWhenDeleteThrows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testReferentialIntegrityExceptionCarriesAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeletionAnalysisEmptyIsFullyDeletable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeletionAnalysisToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteWithObjectEntityReturnsTrueOnSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteInvalidatesCacheOnSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteReturnsTrueWhenCacheInvalidationThrows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteDefaultsAuditTrailToEnabledOnSettingsException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteWithArrayInputLoadsObjectFromMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectSkipsIntegrityCheckWhenNoIncomingRefs":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectAppliesDeletionActionsWhenDeletable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectSkipsIntegrityOnSubDeletion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectCascadesArrayProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectSkipsCascadeWhenPropertyValueIsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectPassesRbacFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectPassesMultitenancyFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectSkipsIntegrityCheckWhenSchemaIdIsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteConvertsNonNumericIdsToNullForCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteConvertsNumericStringIdsToIntForCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteSetsDeletedByCurrentUser":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionCascade":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionRestrict":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionSetNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionSetDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionNoAction":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionLowercase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testCanDeleteNullSchemaReturnsDeletable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testCanDeleteNoIncomingReferences":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testHasIncomingOnDeleteReferencesNoReferences":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testLogRestrictBlockLogsAuditTrail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testLogRestrictBlockEmptyBlockers":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testApplyDeletionActionsEmptyAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testDeletionAnalysisToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testDeletionAnalysisEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testCanDeleteHandlesSchemaLoadFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionWithValidActions":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionCaseInsensitive":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionWithInvalidActions":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testValidOnDeleteActionsConstant":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractOnDeleteReturnsNullWhenNotSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractOnDeleteReturnsUppercaseValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractOnDeleteWithUppercaseValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractOnDeleteWithMixedCaseValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefReturnsNullWhenNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefReturnsDirectRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefReturnsArrayItemsRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefPrefersDirectRefOverItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefReturnsNullForEmptyItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefById":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefBySlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefByUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefByPathWithBasename":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefReturnsNullWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefMatchesFirstSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefWithEmptySchemaList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsRequiredPropertyReturnsTrueWhenRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsRequiredPropertyReturnsFalseWhenNotRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsRequiredPropertyReturnsFalseWhenSchemaNotCached":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsRequiredPropertyReturnsFalseWithNullSchemaCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsNullWhenNoDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsNullWhenPropertyMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsNullWhenSchemaNotCached":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsNullWhenSchemaIsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsFalseDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsZeroDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testHasIncomingOnDeleteReferencesReturnsTrueWhenExists":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testHasIncomingOnDeleteReferencesReturnsFalseWhenNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testHasIncomingOnDeleteReferencesReturnsFalseForDifferentSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteReturnsEmptyWhenObjectHasNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteReturnsEmptyWhenNoRelationsExist":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteReturnsEmptyWhenSchemaNotInIndex":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteDetectsRestrictBlocker":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteDetectsCascadeTarget":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteDetectsSetNullTargetForNonRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteSetNullFallsBackToRestrictWhenRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteDetectsSetDefaultTargetWithDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultFallsBackToSetNullWhenNoDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultFallsToRestrictWhenNoDefaultAndRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteSkipsSoftDeletedObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteHandlesCycleDetection":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteWithArrayReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteFiltersObjectsBySchemaInFallbackPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteFiltersObjectsByPropertyValueInFallbackPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteFiltersObjectsWithNullObjectData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteArrayRefFiltersMismatchedArrayValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteHandlesFindByRelationException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteWithMultipleDependents":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteWithNoReferencingObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testWalkDeletionGraphRespectsMaxDepth":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockLogsBlockers":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockHandlesEmptyBlockers":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockDeduplicatesSchemas":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockWithNullSchemaId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockHandlesBlockersWithMissingKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogIntegrityActionHandlesInsertException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsAppliesSetNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsAppliesSetNullForArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsSetNullHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsAppliesSetDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsSetDefaultHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsAppliesCascade":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsGroupsCascadeByRegisterSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsCascadeFallbackForNoRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsCascadeHandlesDeleteException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsLogsSetNullAuditTrail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsLogsSetDefaultAuditTrail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsLogsCascadeDeleteAuditTrail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsWithEmptyAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsExecutesInOrder":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplySetNullDefaultsIsArrayToFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsCascadeSeparatesGroups":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsPassesTriggerSchemaSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexHandlesSchemaLoadException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexSkipsNoActionAndNoOnDelete":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexSkipsPropertiesWithoutRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexSkipsUnresolvableRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexHandlesNullProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexOnlyBuildsOnce":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexDetectsArrayType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexDetectsNonArrayType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexHandlesRegisterLoadException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefByIdAfterBasenameClean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefByUuidAfterBasenameClean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteHandlesPropertyMissingFromObjectData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteArrayTypeWithNonArrayValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplySetNullArrayRemovesAllMatchingEntries":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplySetNullNonArrayWhenPropertyNotInData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteCollectsMultipleBlockersFromSameSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteCascadeWithNestedRestrict":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplySetNullArrayWhenPropertyIsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnEmptyWhenSchemaFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnEmptyWhenNoSubFilters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnEmptyWhenPropertyHasNoInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnsNullWhenCallbackFindsNothing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterExtractsUuidFromRelatedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterExtractsUuidFromUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterHandlesNonUuidNonUrlValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterRemovesProcessedFilterKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsReturnsEmptyForNoObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsReturnsEmptyForNoExtendFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsExtractsSingleStringId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsExtractsArrayOfIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsDeduplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsEmptyStringsInArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsEmptyStringScalar":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsLimitsArrayTo10Items":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsCircuitBreakerAt200":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedReturnsEmptyForNoIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedCapsAt200":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedIndexesByUuidAndId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedContinuesOnBatchError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testLoadRelationshipChunkOptimizedReturnsEmptyForNoIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testLoadRelationshipChunkOptimizedReturnsMapperResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testLoadRelationshipChunkOptimizedReturnsEmptyOnMapperException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractRelatedDataDelegatesToPerformanceHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractRelatedDataPassesFlagsCorrectly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsReturnsPaginatedContracts":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsUsesDefaultPagination":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsReturnsEmptyWhenNoContractsProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsReturnsErrorResponseOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsAppliesOffsetCorrectly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesReturnsEmptyWhenObjectHasNoRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesFiltersOutSelfReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesReturnsErrorResponseOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesUsesDefaultPagination":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesExtractsRelationsFromNestedArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByReturnsErrorResponseOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByUsesDefaultPaginationInErrorFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByResponseAlwaysHasRequiredKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsResponseAlwaysHasRequiredKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsLogsDebugWhenArrayTruncated":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsNonStringArrayValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsNonStringScalar":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsExactly200UniqueIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedProcessesMultipleBatches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedAllBatchesFail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedExactly200NoCap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsOffsetBeyondArrayReturnsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsErrorResponseRespectsFilters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesDefaultPaginationWithEmptyQuery":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesExtractsStringRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesResponseStructureOnError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterIntersectsMultipleInversedByProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnsEmptyArrayOnlySimpleKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractRelatedDataWithEmptyResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedBySuccessPathStructure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterPassesRefAsSchemaToCallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterSetsNullRefWhenMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterExtractsUuidFromDeepUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterHandlesMultipleSubKeysForSameProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsCircuitBreakerWithMixedTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsMultipleExtendProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsMissingExtendProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsDeduplicatesAcrossProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedWith300IdsProcessesOnly200":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedSingleId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedMultipleObjectsPerBatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testLoadRelationshipChunkOptimizedPassesMultipleIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsHandlesNonArrayContractsValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsLimitExceedsTotal":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesWithRegisterSchemaIdsFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesIgnoresEmptyRelationValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByWithRegisterSchemaIdsFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByWithRbacDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesHandlesNullRelations":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntitiesRemovesSourceFromSelfInList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntitiesWithNullExtendAndFilter":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveReferencedUuidsSimpleString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveReferencedUuidsObjectValueFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveReferencedUuidsArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveReferencedUuidsMissingField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetInversedPropertiesReturnsPropertiesWithInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetInversedPropertiesDirectInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetInversedPropertiesSkipsEmptyInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testFilterExtendedInversePropertiesWithAll":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testFilterExtendedInversePropertiesSpecific":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testCollectEntityUuidsWithMixedEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testCollectEntityUuidsSkipsNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testExtractInverseConfigValidItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testExtractInverseConfigArrayInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testExtractInverseConfigMissingRefReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testExtractInverseConfigMissingInversedByReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testInitializeInverseCacheEntriesCreatesEmptyArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testInitializeInverseCacheEntriesPreservesExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIndexReferencingObjectsIndexesByUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIndexReferencingObjectsObjectValueFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIndexReferencingObjectsPopulatesObjectsCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesFromCacheArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesFromCacheSingle":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesFromCacheSingleEmptyCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesFromCacheSkipsNoInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRemoveQueryParametersRemovesQueryString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRemoveQueryParametersReturnsUnchangedWhenNoQuery":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceWithQueryParamsNumeric":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceByUuidFallsToSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceSlugResolves":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceJsonSchemaPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceFallsThrough":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testCollectUuidsForExtendWithUrlValueNotUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testCollectUuidsForExtendDeduplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIsUuidLikeValid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIsUuidLikeUppercase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIsUuidLikeInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetValueFromPathDeeplyNested":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetValueFromPathIntermediateNotArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesReturnsWhenSchemaNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesReturnsWhenNoInversedProps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testPreloadInverseRelationshipsEmptyEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testPreloadInverseRelationshipsEmptyExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testPreloadInverseRelationshipsNonEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testPreloadInverseRelationshipsNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntityPropertyRbacInjectsSelf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntityWithEmptyObjectDataNoCrash":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntityNullUuidNoCircularDetection":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithUuidFallsThrough":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithPathReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithPathReferenceException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithQueryParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceSlugMultipleResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRemoveQueryParametersNoParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRemoveQueryParametersWithParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testInitializeInverseCacheEntries":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testInitializeInverseCacheEntriesPreservesExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsBasic":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsWithValueFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsNoDuplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsWithArrayReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsNonMatchingUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testCollectEntityUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testCollectEntityUuidsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testHandleInversedPropertiesFromCacheWithData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testHandleInversedPropertiesFromCacheEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testHandleInversedPropertiesFromCacheSingleProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveReferencedUuidsSimpleString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveReferencedUuidsObjectFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveReferencedUuidsArrayValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveReferencedUuidsMissingField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesEmptyList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesConvertsStringExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesConvertsStringFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesConvertsStringFilter":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesConvertsStringUnset":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesNullExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesProcessesMultipleEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetSchemaCaching":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetRegisterCaching":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetRegisterNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigFromItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigArrayInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigFromTopLevel":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testPreloadInverseRelationshipsEmptyEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testPreloadInverseRelationshipsNoMatchingExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetInversedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetInversedPropertiesEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testSetUltraPreloadCacheAndGetSize":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testSetUltraPreloadCacheReplacesExistingCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testClearCacheResetsAllInternalCaches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectsCacheReturnsEmptyByDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityReturnsEntityWithNoFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithFieldFiltering":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithFilterMatching":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithFilterNotMatching":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithUnsetProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityDetectsCircularReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithPreloadedRegistersAndSchemas":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithStringExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithMultipleEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetRegisterFromCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetRegisterFromDb":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetRegisterNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaFromDb":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaFromCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsUuidLikeValidUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsUuidLikeUpperCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsUuidLikeInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectFromUltraPreloadCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectFromObjectCacheService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectFromLocalCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectCachesResultByUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectsCacheReturnsOnlyUuidKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectsCacheWithArrayEntry":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectsCacheSkipsNonUuidStringKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testClearCacheResetsRegistersAndSchemasAndObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigDirectFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigArrayOfFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigArrayOfStrings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigNoType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigArrayWithNoItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigObjectType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathSimple":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathNested":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathDeepNested":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathPartialNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathReturnsArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendSingleUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendArrayOfUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendSkipsSpecialKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendMissingProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendSkipsNonUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendDeduplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendUsesBaseProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendSkipsNonUuidArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesWithInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesWithItemsInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesEmptyInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesMatchSpecific":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesMatchAll":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesNoMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsSkipsNonObjectEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigWithItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigWithDirectRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMissingRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMissingInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMultipleInversedByFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigItemsRefFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveReferencedUuidsSimpleString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveReferencedUuidsObjectWithValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveReferencedUuidsArrayOfUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveReferencedUuidsMissingField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testInitializeInverseCacheEntries":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testInitializeInverseCacheEntriesDoesNotOverwrite":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityNormalizesExtendShorthands":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityNormalizesRegisterShorthand":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithExtendAll":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendSelfRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendSelfSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRespectsDepthLimit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithPropertyRbac":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithMultipleUnset":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersWithParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersWithoutParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersMultipleParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceNumericId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceNumericWithQueryParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceByUuidFallsToSlugLookup":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceByUuidNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceBySlugPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceBySlugDirect":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceFallthrough":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesWithFileRecords":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesFilterOutObjectTags":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesEmptyRecords":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesNoTags":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesHydratesFileId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesArrayOfFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesSkipsMetadataProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesNoSchemaReturnsUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesInitializesEmptyArrayProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyNonArrayNonNumericReturnsUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArrayPropertyNonArrayValueReturnsUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertySingleFileNumeric":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertySingleFileDigitString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsDataUri":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullForZeroId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullForNegativeId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullWhenFileNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullForEmptyContent":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64WithGenericMimeType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileObjectReturnsFormattedArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileObjectReturnsNullForEmptyResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileObjectReturnsNullOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileObjectReturnsNullForNonNumericInput":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsReturnsTags":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsFiltersObjectTags":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsReturnsEmptyWhenNoTags":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsReturnsEmptyWhenFileNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesSetsImage":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesFallsBackToAccessUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesSetsNullForEmptyField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataNoObjectImageField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringUnset":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithNullExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesClearsSourceFromSelf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadsObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithOnlyValidEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendBothSelfRegisterAndSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithNullUuidNoCircularDetection":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRbacWithExistingSelf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsAvoidsDuplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsObjectValueFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsArrayOfUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheArrayProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheEmptyCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheDirectInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheSkipsNoInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsEmptyEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsEmptyExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNonObjectEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithEmptyObjectData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityFilterWithNonExistentKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityFieldFilterAlwaysIncludesSelfAndId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityUnsetNonExistentKeyNoError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityPreloadedObjectsPopulateCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesMultipleFilesMultipleTags":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesMissingMimetypeFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyBase64Single":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyBase64Array":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyBase64ArraySkipsNulls":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArrayNonBase64":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArraySkipsNullFileObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringFilter":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendWithUrlContainingUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsNoWildcards":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsAtDepthLimit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsWithNumericKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsWithNonIterableRoot":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsWithStringOverrideKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithNonExistentKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotSkipsAtPrefixedKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithNullValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayContainingAlreadyExtendedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayContainingNonArrayNonStringItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithUuidStringNotInCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithUuidStringFoundInCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotSkipsUnderscorePrefixedValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotSkipsAtPrefixedValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithUrlValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayOfUuidsOneNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayFilterNullAndAtValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotCircularReferenceInArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithAllFlagAddsAllToSubExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithStringOverrideKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayStringOverride":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtendObjectWithSelfRegisterAndSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtendObjectPreloadsUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtendObjectWithAllFlag":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendAllPopulatesFromObjectKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendAllSkipsIdAndOriginId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityStringExtendParsedToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRbacTemporarySelfIsRemoved":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityHandlesInversePropertiesWhenExtended":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityNoInversePropsWhenNotExtended":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheDirectArrayType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheEmptyRenderedSingleValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadSkipsNonObjectEntityInUuidCollection":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithRbacAndMultitenancyFlags":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64EmptyMimeTypeUsesFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64WithStringFileId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataDeepNestedPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesExceptionReturnsEntityUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesSkipsUnconfiguredProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathWithFileExtension":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathWithFragment":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesNullExtendDefaultsToEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithNestedKeyExtends":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNoInversedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNoSchemaFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNoExtendMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNoEntityUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadSingleInversePropertyInvalidConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadSingleInversePropertyEmptySchemaId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadSingleInversePropertyTargetSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathNullValueInPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyBase64SingleReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertySingleReturnsNullWhenFileNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataStringValueSetsImageNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataArrayWithoutUrls":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotCircularReferenceForSingleValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotUrlValueResolvesLastSegment":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadSkipsNonArrayObjectData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadStoresObjectsByBothKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathLookupExceptionFallsThrough":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityCommaStringExtendIsParsed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotArrayWithAllFlagAddsAllToSubExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityInverseWithAllExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityShouldHandleInverseMatchesSpecificProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityInverseWithStringExtend":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadSingleInversePropertySchemaNotFoundAfterResolve":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendAllSkipsValuesEqualToId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithNestedDotObjectValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotDeepNestedRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceUuidBranch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathNoMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceSlugMultipleMatches":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetRegisterReturnsCachedRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaReturnsCachedSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaReturnsNullOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersStripsQueryString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersNoQueryStringUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigWithItemsRefAndInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigWithArrayInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMissingRefReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMissingInversedByReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsWithObjectPrefixFiltered":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsWithNoTagsReturnsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsWithEmptyTagIdsReturnsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesDirectlyWithTagsViaPrivateMethod":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesFiltersObjectPrefixTags":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesInitializesArrayFileProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyNonNumericStringUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArrayWithBase64Format":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArrayNonArrayValueUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64WithZeroFileIdReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64WithNegativeFileIdReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64FileNotFoundReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64EmptyContentReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsCorrectDataUri":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesSetsImageFromDownloadUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesFallsBackToAccessUrlViaPrivate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesSetsNullWhenNoUrls":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesNoImageFieldConfigured":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitySchemaNormalization":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRegisterNormalization":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheSinglePropertyEmptyCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsSkipsNonEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsSkipsEntityWithNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithEmptyArrayReturnsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigFromItemsRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigFromDirectRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMultiFieldInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigReturnsNullWhenMissingRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigReturnsNullWhenMissingInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsFromValidEntities":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsEmptyArrayReturnsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesMatchesExactName":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesAllFlagIncludesAll":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesNoMatchReturnsEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testInitializeInverseCacheEntriesSetsEmptyArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testInitializeInverseCacheEntriesPreservesExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsSimpleStringRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsHandlesValueObjectFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsMatchesArrayUuids":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsDeduplicatesMultiField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsPopulatesObjectsCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesFromItemsInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesFromDirectInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesSkipsEmptyInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathRefFindsSchemaBySlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathRefCaseInsensitive":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringExtendToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringFieldsToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringFilterToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringUnsetToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesRemovesSourceFromSelf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadsWhenExtendProvided":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersWithoutQueryStringReturnsAsIs":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersStripsComplexQueryString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesCatchesExceptionGracefully":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesReturnsEarlyWhenSchemaNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesReturnsEarlyWhenNoInversedProps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceReturnsNumericIdAsIs":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceStripsQueryParamsBeforeNumericCheck":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceSlugFilterMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesUsesCacheWhenAvailable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testLegacyUuidArrayBulkResultCountsSaved":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testLegacyUuidArrayDoesNotCountUnmatchedObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testLegacyScalarArrayWithNoMatchingUuidsCountsZeroSaved":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testMultipleInvalidObjectsAreAllAccumulated":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testTransformHandlerIsCalledExactlyOnce":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testSchemaCacheIsForwardedToTransformationHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testEmptyChunkReturnsImmediatelyWithoutCallingBulkSave":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testAllInvalidObjectsSkipsBulkSaveAndPopulatesInvalidList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testBulkSaveWithUpdatedStatusPopulatesUpdatedList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testBulkSaveWithUnchangedStatusPopulatesUnchangedList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testMixedStatusesAggregateCorrectly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testPartialChunkWithInvalidAndValidObjectsMergesCorrectly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testRegisterAndSchemaObjectsPassedDirectlyToUltraFastBulkSave":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testUltraFastBulkSaveIsCalledWithEmptyUpdateObjectsArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testResultAlwaysContainsAllRequiredKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testNullRegisterAndSchemaPropagateToUltraFastBulkSave":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testBooleanFlagVariationsDoNotCauseErrors":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testSavedItemsAreJsonSerializableArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testBulkSaveWithCreatedStatusPopulatesSavedList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testProcessingTimeMsIsAlwaysPresentAndNonNegative":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testUnknownObjectStatusExposesSourceBugInDefaultCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testLargeChunkStatisticsAreTalliedCorrectly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsBranchCoverageTest::testSaveObjectsEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsBranchCoverageTest::testSaveObjectsMixedSchemaNoValidObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsBranchCoverageTest::testSaveObjectsDeduplicatesById":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsBranchCoverageTest::testSaveObjectsWithoutDeduplication":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueFromArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueFromObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueMissingKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsScalar":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsWithObjectConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsWithArrayConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsNoHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingDirect":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingFromItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingFromItemsOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingFromPropertyOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformObjectPropertyNoHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformObjectPropertyRelatedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformObjectPropertyNestedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformToUuidPropertyWithInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformToUuidPropertyWithoutInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformSchemaForValidationNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformOpenRegisterObjectConfigurationsNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformPropertyStripsRefFromStringType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformPropertyInversedByArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformPropertyInversedByObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testRemoveQueryParametersWithQuery":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testRemoveQueryParametersWithoutQuery":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetMixedValueFromArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetMixedValueFromObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetMixedValueReturnsNullForMissingKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetMixedValueReturnsNullForNonArrayNonObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeBool":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeFloat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceWithStringRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceWithObjectRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceWithArrayRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceReturnsFalseForDifferentSlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceReturnsFalseWithNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceWithQueryParameters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformFileType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformDatetimeType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformTypeAsArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformTypeNoTypeSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformStandardTypeUntouched":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsNonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsMovesEnumToItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsMovesOneOfToItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsOneOfAsObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsExistingItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsWithConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsWithArrayConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsNoHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingDirect":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingFromItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingFromItemsOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingFromDirectOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationNonObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationRelatedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationNestedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationWithRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationNoConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToUuidPropertyWithoutInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToUuidPropertyWithInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToUuidPropertyWithInversedByEmptyProps":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToNestedObjectPropertyNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToNestedObjectPropertyWithObjectRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToNestedObjectPropertyWithStringRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToNestedObjectPropertyWithArrayRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformOpenRegisterObjectConfigurationsNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testCleanPropertyForValidationNonObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformPropertyForOpenRegisterInversedByArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformPropertyForOpenRegisterInversedByObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformPropertyForOpenRegisterStripsRefFromString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformPropertyForOpenRegisterArrayItemsInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testResolveSchemaLocalSchemaPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testResolveSchemaFileSchemaPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testResolveSchemaExternalNotAllowed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testHandleCustomValidationException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGenerateErrorMessageForValidResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testPreprocessSchemaReferencesSkipsUuidItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testPreprocessSchemaReferencesProcessesItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationSelfRefRelatedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationSelfRefRelatedObjectWithInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationSelfRefArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationSelfRefArrayItemsWithInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationRemovesSchemaId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testCleanSchemaForValidationRemovesMetadata":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFindSchemaBySlugDirectMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFindSchemaBySlugCaseInsensitive":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFindSchemaBySlugNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFindSchemaBySlugFindAllFails":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithInvalidType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectRemovesExtendAndFiltersFromObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectNullAllowedForOptionalFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageWithValidResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testHandleCustomValidationExceptionReturnsJsonResponse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithSchemaEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidationErrorMessageConstant":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithEmptyRequiredArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationWithSelfReferenceRelatedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationSelfReferenceInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationSelfReferenceArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationRemovesDollarId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationRemovesMetadataProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationWithItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationNonObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationWithNestedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeDatetime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeArrayOfTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeNoType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeStandardType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsNonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsMoveEnumToItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsDoesNotOverrideExistingEnum":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsMoveOneOfToItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceFalseDifferentSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithObjectRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithArrayRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithQueryParameters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testRemoveQueryParametersNoParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testRemoveQueryParametersWithParams":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueFromArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueFromObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueMissingKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueNullData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueScalarData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingDirect":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingFromItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingFromOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsNotIterable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsWithMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsNoMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformOpenRegisterObjectConfigurationsNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToUuidPropertyNoInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToUuidPropertyWithInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertyNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeBoolean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeNumber":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForRequiredSingleField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForRequiredMultipleFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForTypeError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForEnumError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageValidResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectFiltersEmptyStringsForOptionalFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithFileType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithDatetimeType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithMetadataProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesSkipsUuidTransformed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsNonObjectType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectWithRelatedObjectHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectWithNestedObjectHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyStripsRefFromStringType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsNoConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsEmptyConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsWithDuplicateStringConfigThrows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsNoDuplicate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithRefCircular":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithArrayItemsRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithNestedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugDirectMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugCaseInsensitiveFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugNullWhenNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugNullWhenFindAllThrows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugDirectMatchThrowsException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithObjectRefFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithArrayRefFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertySuccessfulResolutionObjectType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertySuccessfulResolutionNonObjectType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithRefQueryParameters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyRefNotComponentsSchemas":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterInversedByArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterInversedByObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterInversedByEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterArrayItemsInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterArrayItemsObjectNoInversedBy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterDirectObjectProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterRecursiveNestedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertySelfReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertyWithObjectRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertyWithArrayRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertyNonSchemasRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToUuidPropertyWithInversedByAndRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToUuidPropertyWithInversedByEmptyProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationSelfReferenceWithObjectConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationSelfReferenceNoHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingFromDirectOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingFromDirectObjectConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingObjectConfigNoHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingItemsConfigNoHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationAsArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationWithNestedItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsOneOfAsObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsDoesNotOverrideExistingOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsEmptyEnum":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsEmptyOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsNoTypeSet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectWithRefNoConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectNoConfigNoRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectConfigWithObjectFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeTime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeEmail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypePhone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeArrayMixed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMinLengthError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMinLengthEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMaxLengthError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMinimumError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMaximumError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForPatternError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMinItemsError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMaxItemsError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForTypeErrorArrayExpected":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForTypeErrorEmptyStringOnRequired":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsWithArrayConfigDuplicate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsWithArrayConfigNoDuplicate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEnumFieldNullNotAllowed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEnumFieldNullAllowed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyArrayFilteredForNonRequiredNoConstraints":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyArrayKeptForMinItemsConstraint":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectNullAllowedForOptionalWithTypeArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesWithItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesSkipsUuidTransformedItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesPropertyWithRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformObjectPropertyForOpenRegisterDefaultHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformObjectPropertyForOpenRegisterNestedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformObjectPropertyForOpenRegisterNullHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForZero":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testHandleValidationExceptionResponseStatus":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testHandleCustomValidationExceptionMultipleErrors":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationTransformsCustomTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationFixesMisplacedEnumOnArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithNonComponentsRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithObjectRefNoId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithArrayRefNoId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectRemovesDollarIdBeforeValidation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectKeepsZeroForOptionalIntegerField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectKeepsFalseForOptionalBooleanField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformOpenRegisterObjectConfigurationsWithProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsObjectConfigNoHandling":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsWithObjectIterable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaLocalApiSchemas":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaFileApiFilesSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaExternalNotAllowedReturnsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaExternalDisallowedReturnsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageTypeErrorExpectsObjectGotEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageTypeErrorExpectsArrayGotEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageTypeErrorExpectsStringGotEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForFormatError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForSemverFormatError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageDefaultKeywordWithSubErrors":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageEnumWithNonArrayValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsArrayConfigDuplicateThrowsTypeError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsArrayConfigNoDuplicate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyArrayKeptForUniqueItemsConstraint":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyArrayKeptForMaxItemsConstraint":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForResource":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithSchemaEntityNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEnumNullRemovedFromObjectForNonRequiredField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyRequiredArrayGetsUnset":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationAsArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationWithOneOf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyArrayItemsSuccessfulResolution":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformOpenRegisterObjectConfigurationsWithArrayProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesWithOneOfRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaReturnsJsonForLocalSchemaUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaHandlesFileApiUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsWithStringConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsThrowsTypeErrorOnArrayConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsReturnsEarlyWhenNoConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeReturnsUnknownForOpenResource":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeReturnsUnknownForClosedResource":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformOpenRegisterObjectConfigurationsNoop":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesWithoutProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesWithAllOfRef":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaHandlesExternalUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithSimpleStringProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithRequiredFieldMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithEnumProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithNestedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithArrayProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithMinItemsConstraint":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithNoProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageWithInvalidResult":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithMultipleTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithBooleanProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithNumberProperty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectKeepsEmptyStringForRequiredField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testHandleValidationExceptionReturnsJsonResponse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithEmptySchemaObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testBase64FileUploadWithDataURI":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testURLFileReference":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testMixedFileTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testArrayOfFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testMultipartFileUploadError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testFileUploadQueuesBackgroundJobForTextExtraction":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testFileUploadIsNonBlocking":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testPDFUploadQueuesBackgroundJob":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testMultipartFileUploadSingleFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCreateEmptyResultInitializesArrays":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCreateEmptyResultInitializesStatistics":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartSmallOperationNoLog":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartLargeOperation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartLargeMixedSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testInitializeResultNoInvalidObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testInitializeResultWithInvalidObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testInitializeResultWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testMergeChunkResultMergesSaved":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testMergeChunkResultMergesErrors":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testMergeChunkResultWithMixedResults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsAddsTimingInfo":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsCalculatesEfficiency":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsWithZeroProcessed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsFormatsValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsWithUnchanged":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCreateEmptyResultStructure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testGetObjectReturnsNullWithoutContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetRegisterWithRegisterObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetRegisterWithSlugString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetRegisterWithNumericIdUsesCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetRegisterWithNumericIdCacheFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetSchemaWithSchemaObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetSchemaWithNumericIdUsesCache":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetSchemaWithSlugString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetSchemaThrowsOnNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetObjectWithEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetObjectWithIdNoContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testPrepareFindAllConfigExtendsStringToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testPrepareFindAllConfigSetsRegister":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testPrepareFindAllConfigSetsSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testExtractUuidSelfId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testExtractUuidIdField":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testExtractUuidExplicitTakesPriority":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testExtractUuidSkipsEmptyId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testCheckSavePermissionsNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testCheckSavePermissionsCreateForNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testCheckSavePermissionsUpdateForExistingUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testCheckSavePermissionsCreateWhenUuidNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testNormalizeDateValuesNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testNormalizeDateValuesConvertsDatetime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testNormalizeDateValuesSkipsValidDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testNormalizeDateValuesSkipsNonDateFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderNullUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderObjectNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderExistsNullFolder":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderExistsEmptyString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderExistsFolderNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderExistsException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testFindByRelationsDelegates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testFindByRelationsExactMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testGetActiveOrganisationForContextReturnsUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testGetActiveOrganisationForContextReturnsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testGetActiveOrganisationForContextException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testSetContextFromParametersWithRegisterObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testSetContextFromParametersWithNullValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testExtractUuidAndNormalizeObjectWithArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testExtractUuidAndNormalizeObjectWithObjectEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testExtractUuidAndNormalizeObjectWithExplicitUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testCheckSavePermissionsWithRbacDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testCheckSavePermissionsCreateScenario":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testCheckSavePermissionsUpdateScenario":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testValidateObjectIfRequiredSkipsWhenHardValidationDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testEnsureObjectFolderReturnsNullWhenNoUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testEnsureObjectFolderCreatesFolderWhenNeeded":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testHandleCascadingWithContextPreservationPreservesContext":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testPrepareFindAllConfigPreservesExistingValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testPrepareFindAllConfigConvertsExtendStringToArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testValidateObjectIfRequiredCallsValidatorWhenEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionCascade":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionRestrict":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionSetNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionSetDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionNoAction":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionCaseInsensitive":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractOnDeletePresent":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractOnDeleteMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractTargetRefDirect":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractTargetRefArrayItems":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractTargetRefNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefById":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefBySlug":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefByUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefByPathBasename":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsRequiredPropertyTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsRequiredPropertyFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsRequiredPropertySchemaNotCached":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValuePresent":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValueMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValueSchemaNotCached":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValuePropertyNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValueNullProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testCanDeleteNoSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testCanDeleteSchemaNotInIndex":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testHasIncomingOnDeleteReferencesTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testHasIncomingOnDeleteReferencesFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testLogRestrictBlock":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testLogRestrictBlockWithEmptyBlockers":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testApplyDeletionActionsEmptyAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testLogIntegrityActionExceptionIsSwallowed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsBlobSchemas":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsSkipsSchemasWithoutId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testApplyInversedByFilterSchemaFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testApplyInversedByFilterNoSubFilters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractRelatedDataDelegates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsStringValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsArrayValue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsArrayLimit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsCircuitBreaker":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsSkipsInvalid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsDeduplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsSkipsMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testBulkLoadRelationshipsBatchedEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testBulkLoadRelationshipsBatchedCap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testBulkLoadRelationshipsBatchedLoads":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testBulkLoadRelationshipsBatchedBatchException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testLoadRelationshipChunkOptimizedEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testLoadRelationshipChunkOptimizedException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testLoadRelationshipChunkOptimizedReturns":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testGetContractsWithContracts":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testGetContractsWithPagination":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testGetContractsEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testGetContractsException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacAdmin":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacNonObjectEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacNullSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacFiltersOnPermission":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacDeniesNoPermission":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsEmptyObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsSingleStringValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsArrayValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsRemovesDuplicates":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsEmptyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsLimitsArrayTo10":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsNonExistentProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testBulkLoadRelationshipsBatchedEmptyInput":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testBulkLoadRelationshipsBatchedIndexesByUuidAndId":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testBulkLoadRelationshipsBatchedCapsAt200":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testBulkLoadRelationshipsBatchedHandlesExceptions":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testLoadRelationshipChunkOptimizedEmptyInput":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testLoadRelationshipChunkOptimizedDelegatesToMapper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testLoadRelationshipChunkOptimizedReturnsEmptyOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testGetContractsReturnsContractsFromObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testGetContractsAppliesPagination":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testGetContractsReturnsEmptyWhenNoContracts":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testGetContractsReturnsEmptyOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractRelatedDataDelegatesToPerformanceHandler":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testApplyInversedByFilterReturnsEmptyWhenSchemaIsFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testApplyInversedByFilterReturnsEmptyWhenNoSubFilters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatDateTime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatRfc3339":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatEmail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatTime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatDuration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatColor":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatHostname":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatIpv4":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatIpv6":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatInvalidDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternIntegerString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternFloatString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternBooleanString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternCamelCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternPascalCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternSnakeCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternScreamingSnakeCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternFilename":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternWindowsPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testConsolidateFormatDetectionNullExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testConsolidateFormatDetectionSameFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testConsolidateFormatDetectionHigherPriorityNew":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testConsolidateFormatDetectionHigherPriorityExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesNullExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesSameType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesIntegerToNumber":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesNumberToInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesIncompatibleTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzezArrayStructureEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzezArrayStructureList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzezArrayStructureAssociative":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeObjectStructureWithObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeObjectStructureWithArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeObjectStructureWithScalar":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeObjectStructures":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testIsInternalPropertyTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testIsInternalPropertyFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatEmail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatUnknown":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromPatternsBoolean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromPatternsInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromPatternsFloat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromPatternsNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeStringWithIntegerPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeStringWithFloatPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeStringWithBooleanPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeDouble":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeFloat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeBoolean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeNumber":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeUnknown":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeStringWithIntegerPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeStringWithFloatPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeStringWithBooleanPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeNonString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeStringPlain":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectEnumLikeTooFewExamples":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectEnumLikeTrue":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectEnumLikeTooManyUnique":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectEnumLikeNonString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testExtractEnumValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateNestedProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateNestedPropertiesNoKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeDouble":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeBoolean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeDefault":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeEmptyTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeNoItemTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareTypeMissing":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareTypeMismatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareTypeMatching":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsMissingMaxLength":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsMaxLengthTooSmall":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsMissingFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsMissingPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsNonStringType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNumericConstraintsNonNumeric":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNumericConstraintsMinimumTooHigh":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNumericConstraintsMaximumTooLow":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNullableConstraintRequiredButNullable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNullableConstraintNotNullable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNullableConstraintSuggestsNullType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintSuggestsEnum":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintDifferentValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintSameValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintTooManyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintNullValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueDouble":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueListArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueAssocArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergePropertyAnalysisTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergePropertyAnalysisExamplesOverflow":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergePropertyAnalysisObjectStructureMerge":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergePropertyAnalysisObjectStructureNewWhenNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testUpdateSchemaFromExplorationSuccess":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testUpdateSchemaFromExplorationFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testRecommendPropertyTypeFromFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testRecommendPropertyTypeFromPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testRecommendPropertyTypeSingleType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testRecommendPropertyTypeMultipleTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareTypeWithMatchingTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareTypeWithMismatchedTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareTypeWithMissingType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareStringConstraintsWithMatchingMaxLength":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareStringConstraintsWithInadequateMaxLength":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareStringConstraintsWithFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareStringConstraintsWithPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNumericConstraintsWithValidRange":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNumericConstraintsWithInadequateMinimum":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNumericConstraintsWithInadequateMaximum":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNumericConstraintsWithMissingConstraints":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNullableConstraintWithNullableData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNullableConstraintWithNonNullableData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNullableConstraintAlreadyNullable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareEnumConstraintWithEnumLikeData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareEnumConstraintWithTooManyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareEnumConstraintAlreadyHasEnum":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithEmail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithDateTime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromPatternsWithBoolean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromPatternsWithInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromPatternsWithFloat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromPatternsWithNoMatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithIntegerString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithFloatString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithBooleanString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithDouble":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypePreservesStandardTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithClearMajority":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithMixedTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithNumericTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithSingleType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithBooleanPatterns":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithFloat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithBoolean":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithListArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithAssociativeArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithEmptyArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatDateTimeRfc3339":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatUrl":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatUuid":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatIpv6":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatTime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatColor":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatDuration":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatReturnsNullForPlainText":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatHostname":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternFloatString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternBooleanString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternSnakeCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternScreamingSnakeCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternCamelCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternPascalCase":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternPath":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternFilenameNotDetected":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testIsInternalPropertyReturnsTrueForInternalNames":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testIsInternalPropertyReturnsFalseForNormalProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testIsInternalPropertyIsCaseInsensitive":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeSingleString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeSingleInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeDoubleReturnsNumber":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeFormatOverridesType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeIntegerStringPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeBooleanStringPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeMultipleTypesUsesDominant":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectEnumLikeReturnsTrueForFewUniqueValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectEnumLikeReturnsFalseForManyUniqueValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectEnumLikeReturnsFalseWithTooFewExamples":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectEnumLikeReturnsFalseForNonStringTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testExtractEnumValuesReturnsUniqueValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesWithNullExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesExpandsRange":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesTypePromotion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesOverlappingRanges":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testConsolidateFormatDetectionNullExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testConsolidateFormatDetectionSameFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testConsolidateFormatDetectionDifferentFormatsHigherPriorityWins":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testConsolidateFormatDetectionKeepsHigherPriorityExisting":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzezArrayStructureEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzezArrayStructureList":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzezArrayStructureAssociative":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeObjectStructureWithStdClass":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeObjectStructureWithScalar":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergePropertyAnalysisMergesTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsSkipsExistingProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsSkipsInternalProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsConfidenceLevels":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testExploreSchemaPropertiesThrowsOnMissingSchema":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testUpdateSchemaFromExplorationThrowsOnFailure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareTypeWithMissingType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareTypeWithMatchingType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareTypeWithMismatchedType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsSkipsNonStringTypes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsDetectsMissingMaxLength":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsDetectsMaxLengthTooSmall":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsDetectsMissingFormat":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsDetectsMissingPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareNullableConstraintWithRequiredAndNullable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareNullableConstraintWithNonRequiredConfig":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareNullableConstraintWithNonNullableAnalysis":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareEnumConstraintSuggestsAddingEnum":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareEnumConstraintDetectsEnumMismatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareEnumConstraintWithNullEnumValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareEnumConstraintSkipsWhenTooManyValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeExistingPropertiesReturnsMismatchImprovements":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeExistingPropertiesSkipsUndiscoveredProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateNestedPropertiesCreatesDefinitions":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateNestedPropertiesWithNullKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateArrayItemTypeReturnsItemType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateArrayItemTypeWithEmptyItemTypesReturnsString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesNullType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesObjectType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesArrayType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesBooleanType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesUnknownType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesFloatType":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGetDominantTypeWithFloatStringPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGetDominantTypeWithBooleanStringPattern":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGetDominantTypeWithIntegerDominant":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesNumberDominatesInteger":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesIncompatibleTypesDefaultsToNumber":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatEmail":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatIpv4":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternIntegerString":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsForNewProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testExploreSchemaPropertiesNoObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testExploreSchemaPropertiesDiscoversProperties":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testUpdateSchemaFromExplorationMergesAndSaves":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsCreatesObjectTypeSuggestion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsCreatesArrayTypeSuggestion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testGetCacheStatsReturnsFullStructure":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheAllTriggersTypeError":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheAllCallsAllClearMethods":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheDistributedTriggersTypeError":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheDefaultTypeTriggersTypeError":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsWithStoredMultitenancyConfig":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetMultitenancySettingsOnlyReturnsDefaults":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetMultitenancySettingsOnlyParsesStoredConfig":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateMultitenancySettingsOnlyStoresAndReturns":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateMultitenancySettingsOnlyDefaultsMissingKeys":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetMultitenancySettingsOnlyPartialConfig":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsReturnsDefaultsWhenNoConfigStored":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStats":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsIncludesSystemInfo":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsHandlesSolrException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsHandlesCacheStatsException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsIncludesTimestamp":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsOuterException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsSerialZeroObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsParallelZeroObjects":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsWithMaxObjectsLimit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsSerialWithObjectsThrowsError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsParallelWithObjectsThrowsTypeError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsBatchSizeOne":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsBatchSizeFiveThousand":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsConfigUsed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsDurationAndMemory":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithSuccessfulDbStats":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithMysqlPlatform":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithMagicMapperTables":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWhenMainQueryReturnsFalse":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithFailingMagicMapperTable":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsMaxObjectsEqualsTotal":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsMaxObjectsGreaterThanTotal":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithDatabaseExceptionFallsBackToZeroedStats":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithCacheExceptionRecordsErrorInCacheKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsSerialWithObjectsAndNullServiceThrowsTypeError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsParallelModeWithObjectsThrowsTypeErrorForNullService":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsSerialCollectErrorsFalseThrowsError":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsMagicMapperOuterCatchWhenGetDatabasePlatformThrows":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsMagicMapperInnerCatchWhenTableQueryFails":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsMagicMapperSuccessPathWithTableData":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithSourcesTableExisting":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceTypeReturnsObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextReturnsExpectedStructure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesUuidAndVersion":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesSchemaTitle":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesSchemaDescription":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextContinuesWhenSchemaNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesRegisterTitle":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesRegisterDescription":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextContinuesWhenRegisterNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesOrganization":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextChecksumIsSha256":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextLengthMatchesTextLength":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextMetadataContainsObjectFields":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextThrowsWhenNoTextExtracted":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextHandlesNestedObjectData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextHandlesBooleanInObjectData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextHandlesNumericValuesInObjectData":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testNeedsExtractionReturnsTrueWhenForced":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testNeedsExtractionReturnsTrueWhenNoChunksExist":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testNeedsExtractionReturnsFalseWhenChunksAreUpToDate":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testNeedsExtractionReturnsTrueWhenChunksAreStale":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceMetadataReturnsExpectedKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceTimestampReturnsObjectUpdateTime":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceTimestampReturnsCurrentTimeWhenObjectNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceTimestampReturnsCurrentTimeWhenUpdatedIsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIgnoresEmptyStringValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIgnoresEmptyArrayValues":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextRespectsMaxRecursionDepth":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkFixedSizeMultipleChunksNoOverlap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkFixedSizeShortTextSingleChunk":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkFixedSizeFiltersTinyChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkRecursiveSplitsByParagraphs":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkRecursiveSplitsBySentences":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRecursiveSplitOversizedSegmentRecursesDeeper":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRecursiveSplitEmptySeparatorsFallsToFixed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRecursiveSplitLastChunkPreserved":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRecursiveSplitWithOverlap":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkDocumentFixedSizeStrategy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkDocumentTruncatesExcessiveChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testCleanTextMixedProblematicCharacters":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSanitizeTextReplacesEmoji":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSanitizeTextRemovesControlChars":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSanitizeTextEmptyForWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testDetectLanguageSignalsDutchHet":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testDetectLanguageSignalsEnglishAnd":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testDetectLanguageSignalsUnknown":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testGetDetectionMethodHeuristic":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testGetDetectionMethodNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsWordDocumentDocx":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsWordDocumentDoc":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsWordDocumentFalseForPdf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSpreadsheetXlsx":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSpreadsheetXls":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSpreadsheetFalseForCsv":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testCalculateAvgChunkSizeEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testCalculateAvgChunkSizeMixed":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testCalculateAvgChunkSizeNullTextKey":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testBuildPositionReferenceObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testBuildPositionReferenceFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testBuildPositionReferenceFileDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSummarizeMetadataPayloadFull":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSummarizeMetadataPayloadEmptyDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testGetStatsTotalFilesCalculation":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testGetTableCountSafeReturnsZeroOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractFileWithEntityRecognitionEnabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractFileEntityRecognitionDisabled":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractFileRiskLevelUpdateFails":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractFileEntityRecognitionThrows":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testDiscoverUntrackedFilesProcessesFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRetryFailedExtractionsWithFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractPendingFilesWithFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSourceUpToDateFalseWhenForced":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSourceUpToDateTrueWhenChunkNewer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSourceUpToDateFalseWhenNoChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSourceUpToDateFalseWhenChunkOlder":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSanitizeTextRemovesNullBytes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSanitizeTextNormalizesWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSanitizeTextTrims":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSanitizeTextEmptyForWhitespaceOnly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDetectLanguageSignalsDutch":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDetectLanguageSignalsEnglish":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDetectLanguageSignalsUnknown":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testGetDetectionMethodHeuristic":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testGetDetectionMethodNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsWordDocumentDocx":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsWordDocumentDoc":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsWordDocumentFalseForPdf":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSpreadsheetXlsx":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSpreadsheetXls":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSpreadsheetFalseForText":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testBuildPositionReferenceObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testBuildPositionReferenceFile":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testBuildPositionReferenceFileDefaults":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkDocumentRecursive":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkDocumentFixedSize":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkDocumentSmallTextSingleChunk":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkDocumentDefaultStrategy":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCleanTextNormalization":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCleanTextReducesNewlines":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCalculateAvgChunkSizeEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCalculateAvgChunkSizeWithArrayChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCalculateAvgChunkSizeWithStringChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCalculateAvgChunkSizeWithNonTextChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSourceUpToDateForceFalse":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSourceUpToDateNoChunks":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSourceUpToDateChunkNewer":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSourceUpToDateChunkOlder":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testHydrateChunkEntity":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSummarizeMetadataPayload":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSummarizeMetadataPayloadEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testTextToChunksMapsCorrectly":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testExtractObjectDeletedObject":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testGetStatsReturnsStructure":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testGetTableCountSafeReturnsZeroOnException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkFixedSizeSmallText":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkFixedSizeExact":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testRecursiveSplitNoSeparators":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDiscoverUntrackedFilesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDiscoverUntrackedFilesEmpty":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testExtractPendingFilesNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testRetryFailedExtractionsNone":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentShortTextSingleChunk":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentWithDefaultOptions":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentRemovesNullBytes":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentNormalizesLineEndings":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentUnknownStrategyFallback":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentReducesWhitespace":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentRecursiveSmallParagraphs":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testGetStatsReturnsExpectedKeys":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testGetStatsWithZeroUntrackedFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testDiscoverUntrackedFilesNoFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testDiscoverUntrackedFilesHandlesException":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testDiscoverUntrackedFilesDefaultLimit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testRetryFailedExtractionsNoFiles":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testRetryFailedExtractionsDefaultLimit":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testExtractPendingFilesNoPending":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testExtractPendingFilesDefaultLimit":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentReturnsChunksForText":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentReturnsEmptyArrayForEmptyText":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentRespectsChunkSizeOption":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithRecursiveStrategy":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithUnknownStrategyFallsBackToRecursive":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentTruncatesExcessiveChunks":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentShortTextReturnsSingleChunk":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentSmallTextBelowMinChunkSizeReturnsOneChunk":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextRemovesNullBytes":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextNormalizesLineEndings":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextReducesExcessiveNewlines":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextCollapsesTabsAndSpaces":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextTrimsWhitespace":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextRemovesNullBytes":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextRemovesControlCharacters":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextNormalizesWhitespace":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextTrims":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextReturnsEmptyForWhitespaceOnly":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextHandlesEmptyString":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsDetectsDutch":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsDetectsEnglish":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsReturnsNullForUnknown":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsDutchTakesPriorityOverEnglish":8,"Unit\\Service\\TextExtractionServiceTest::testGetDetectionMethodReturnsHeuristicForLanguage":8,"Unit\\Service\\TextExtractionServiceTest::testGetDetectionMethodReturnsNoneForNull":8,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsTrueForDocx":8,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsTrueForDoc":8,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsFalseForPdf":8,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsFalseForTextPlain":8,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsTrueForXlsx":8,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsTrueForXls":8,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsFalseForPdf":8,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsFalseForCsv":8,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeReturnsZeroForEmpty":8,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithArrayChunks":8,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithStringChunks":8,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithMixedChunks":8,"Unit\\Service\\TextExtractionServiceTest::testSummarizeMetadataPayloadReturnsExpectedKeys":8,"Unit\\Service\\TextExtractionServiceTest::testSummarizeMetadataPayloadWithMissingKeysDefaultsToNull":8,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForObject":8,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForObjectWithoutPath":8,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForFile":8,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForFileWithoutOffsets":8,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsFalseWhenForceReExtract":8,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsFalseWhenNoChunks":8,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsTrueWhenChunksNewer":8,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsTrueWhenChunksEqual":8,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsFalseWhenChunksOlder":8,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntityCreatesValidChunk":8,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntityHandlesMinimalData":8,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksGeneratesChunkDTOs":8,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksForObjectSource":8,"Unit\\Service\\TextExtractionServiceTest::testGetTableCountSafeReturnsZeroOnException":8,"Unit\\Service\\TextExtractionServiceTest::testGetTableCountSafeReturnsCount":8,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeReturnsOneChunkForShortText":8,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeMultipleChunksWithNoOverlap":8,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeWithLargeText":8,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveReturnsOneChunkForShortText":8,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveSplitsByParagraphs":8,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveSplitsBySentences":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitWithEmptySeparatorsFallsToFixedSize":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitShortTextReturnsSingleChunk":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileThrowsNotFoundWhenFileNotInNextcloud":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileSkipsUpToDateFile":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileProcessesTextFile":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileForceReExtractIgnoresUpToDate":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWithEntityRecognitionEnabled":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileEntityRecognitionFailureDoesNotThrow":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileRiskLevelFailureDoesNotThrow":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileUnsupportedMimeType":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileEmptyNodesThrows":8,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectSkipsDeletedObject":8,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectSkipsUpToDateObject":8,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesReturnsStatsWhenNoFiles":8,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesReturnsErrorOnException":8,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesCountsFailures":8,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesReturnsStatsWhenNoFiles":8,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesCountsFailures":8,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsReturnsStatsWhenNoFiles":8,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsCountsFailures":8,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksRollsBackOnError":8,"Unit\\Service\\TextExtractionServiceTest::testPersistMetadataChunkHandlesJsonEncodingFailure":8,"Unit\\Service\\TextExtractionServiceTest::testDefaultConstantsHaveExpectedValues":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentNormalizesTextBeforeChunking":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithZeroOverlap":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentDefaultOptions":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextThrowsWhenExtractionReturnsNull":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextThrowsWhenResultIsEmpty":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextReturnsPayload":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionTextPlain":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionJsonFile":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionMarkdownFile":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionUnsupportedMimeReturnsNull":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionEmptyNodesThrows":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionNodeNotFileThrows":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionCsvFile":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionYamlFile":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionGenericTextSubtype":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionHtmlFile":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionXmlFile":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionApplicationXmlFile":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionYamlAlternativeMime":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionApplicationYamlMime":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionMissingMimeAndPath":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextReplacesEmojiWithSpace":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextRemovesDeleteCharacter":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextRemovesVerticalTabAndFormFeed":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextPreservesNewlinesAndTabs":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextMissingOwnerAndOrganisationDefaultsToNull":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextMissingMetadataFieldsDefaultToNull":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextWithDutchContent":8,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksDefaultsWhenOptionsEmpty":8,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksShortTextSingleChunk":8,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksWithFixedSizeStrategy":8,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeWordBoundaryBreaking":8,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeFiltersTinyChunks":8,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeGuardPreventsNegativeOffset":8,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeExactChunkSizeText":8,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveWithSentenceBoundaries":8,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveWithClauseBoundaries":8,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveWithWordBoundariesOnly":8,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveExactChunkSizeText":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitWithOverlap":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitLargeSplitRecursesDeeper":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitLastChunkPreserved":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitLastChunkTooSmallIsFiltered":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitWithZeroOverlap":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitSmallOverlapFallsToElse":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileMissingMtimeUsesCurrentTime":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileDefaultEntityRecognitionMethod":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWithOrganisationInMetadata":8,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectWithNullUpdatedTimestamp":8,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectForceReExtractIgnoresUpToDate":8,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceSuccessPath":8,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceMultipleChunks":8,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceWithNullOwnerAndOrg":8,"Unit\\Service\\TextExtractionServiceTest::testPersistMetadataChunkCreatesCorrectChunk":8,"Unit\\Service\\TextExtractionServiceTest::testPersistMetadataChunkWithEmptyPayload":8,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntityUsesTextContentLengthAsEndOffset":8,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntitySetsAllNullableFieldsToNull":8,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntityTimestampSetsCorrectly":8,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesSuccessfulExtraction":8,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesMixedSuccessAndFailure":8,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesSuccessfulExtraction":8,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsSuccessfulRetry":8,"Unit\\Service\\TextExtractionServiceTest::testGetStatsHandlesDbExceptionGracefully":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextEmptyString":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextOnlyNullBytes":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextMixedLineEndings":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsWithEenArticle":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsWithHetArticle":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsWithAndKeyword":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsWithOfKeyword":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsEmptyString":8,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsLanguageLevelAlwaysNull":8,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForUnknownSourceType":8,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithNullTextKey":8,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithIntegerChunks":8,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeSingleChunk":8,"Unit\\Service\\TextExtractionServiceTest::testSummarizeMetadataPayloadWithPartialKeys":8,"Unit\\Service\\TextExtractionServiceTest::testExtractPdfWithInvalidContentThrows":8,"Unit\\Service\\TextExtractionServiceTest::testExtractWordDocxWithInvalidContentThrows":8,"Unit\\Service\\TextExtractionServiceTest::testExtractWordDocWithInvalidContentThrows":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSpreadsheetXlsxReturnsExtractedText":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSpreadsheetXlsReturnsExtractedText":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFilePdfWithInvalidContentPropagates":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWordDocWithInvalidContentPropagates":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileSpreadsheetProcessesContent":8,"Unit\\Service\\TextExtractionServiceTest::testExtractPdfWrapsParseExceptionCorrectly":8,"Unit\\Service\\TextExtractionServiceTest::testExtractWordReturnsNullForEmptyDocx":8,"Unit\\Service\\TextExtractionServiceTest::testExtractWordReturnsTextFromDocx":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSpreadsheetReturnsTextFromXlsx":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSpreadsheetReturnsNullForEmptyXlsx":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextHandlesNonUtf8Input":8,"Unit\\Service\\TextExtractionServiceTest::testPersistMetadataChunkHandlesJsonEncodingFailureDirectly":8,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectEntityExtractionFailureLogsError":8,"Unit\\Service\\TextExtractionServiceTest::testExtractPdfReturnsNullForEmptyPdfText":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextWithOnlyWhitespace":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextWithMultipleTabsAndSpaces":8,"Unit\\Service\\TextExtractionServiceTest::testCleanTextPreservesSingleParagraphBreak":8,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextChecksumMatchesSha256":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitSingleLargeSegmentUsesSubChunks":8,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitSingleSmallSegmentAfterEmptyCurrentChunk":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithOnlyNullBytes":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWithHtmlMimeType":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWithApplicationJsonMimeType":8,"Unit\\Service\\TextExtractionServiceTest::testExtractFileUnsupportedMimeTypeThrowsViaSourceText":8,"Unit\\Service\\TextExtractionServiceTest::testGetStatsWithAllZeroTablesReturnsCorrectStructure":8,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntitySetsPositionReference":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextHandlesHighUnicodeCharacters":8,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextPreservesRegularUnicodeText":8,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionGetByIdExceptionIsRethrown":8,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesWithCustomLimit":8,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksHandlesEndOffsetDefault":8,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksNullLanguageLevelPassedThrough":8,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsFalseForOdt":8,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsFalseForOds":8,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectSkipsWhenObjectNotFound":8,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectSkipsWhenUpToDate":8,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesReturnsCorrectStats":8,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesWithNoFiles":8,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsReturnsCorrectStats":8,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsWithNoFiles":8,"Unit\\Service\\TextExtractionServiceTest::testGetStatsReturnsAllExpectedKeys":8,"Unit\\Service\\TextExtractionServiceTest::testGetTableCountSafeReturnsZeroOnMissingTable":8,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceRollsBackOnException":8,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceCommitsOnSuccess":8,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithFixedSizeStrategy":8,"Unit\\Service\\TextExtractionServiceTest::testGetStatsReturnsExpectedStructure":8,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunWithNoBlobObjectsSetsComplete":8,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunProcessesBatchAndUpdatesCounters":8,"Unit\\BackgroundJob\\BlobMigrationJobTest::testDeleteBlobRowsDeletesByIdArray":8,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunMarksCompleteWhenBlobTableDoesNotExist":8,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunLogsErrorOnException":7,"Unit\\Command\\SolrManagementCommandCoverageTest::testWarmPartialFailure":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testGetCollectionFieldStatusAllPresent":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testGetCollectionFieldStatusEmptyCollection":7,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyAutoPublishFromSchemaConfig":5,"Unit\\BackgroundJob\\ActionScheduleJobTest::testCronExpressionEvaluationConcept":1,"Unit\\BackgroundJob\\ActionScheduleJobTest::testScheduleMatchingLogic":1,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunUsesDefaultMaxObjectsWhenNotConfigured":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunUsesCustomMaxObjectsFromConfig":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunUsesCustomModeFromConfig":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunEnablesCollectErrorsFromConfig":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunLogsSuccessOnCompletedWarmup":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunLogsPerformanceStatsOnSuccess":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunLogsErrorWhenWarmupReturnsFailure":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunDoesNotPropagateException":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunLogsExceptionDetailsOnFailure":8,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunPassesSchemasToWarmupIndex":8,"Unit\\Controller\\AuditTrailControllerTest::testIndexSuccess":8,"Unit\\Controller\\AuditTrailControllerTest::testShowSuccess":8,"Unit\\Controller\\AuditTrailControllerTest::testShowNotFound":8,"Unit\\Controller\\AuditTrailControllerTest::testObjectsSuccess":8,"Unit\\Controller\\AuditTrailControllerTest::testObjectsInvalidArgument":8,"Unit\\Controller\\AuditTrailControllerTest::testObjectsNotFound":8,"Unit\\Controller\\AuditTrailControllerTest::testExportSuccess":8,"Unit\\Controller\\AuditTrailControllerTest::testExportInvalidFormat":8,"Unit\\Controller\\AuditTrailControllerTest::testExportGeneralException":8,"Unit\\Controller\\AuditTrailControllerTest::testUpdateReturns405":8,"Unit\\Controller\\AuditTrailControllerTest::testDestroyReturns405":8,"Unit\\Controller\\AuditTrailControllerTest::testVerifySuccess":8,"Unit\\Controller\\AuditTrailControllerTest::testVerifyWithRange":8,"Unit\\Controller\\AuditTrailControllerTest::testVerifyDetectsTamper":8,"Unit\\Controller\\AuditTrailControllerTest::testVerwerkingsregisterSuccess":8,"Unit\\Controller\\AuditTrailControllerTest::testVerwerkingsregisterEmpty":8,"Unit\\Controller\\AuditTrailControllerTest::testInzageverzoekSuccess":8,"Unit\\Controller\\AuditTrailControllerTest::testInzageverzoekMissingIdentifier":8,"Unit\\Controller\\AuditTrailControllerTest::testInzageverzoekEmptyIdentifier":8,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithUnderscoreLimitAndOffset":8,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithPageCalculatesOffset":8,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithUnderscorePageParam":8,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithSortParam":8,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithUnderscoreSortParam":8,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithSearchParam":8,"Unit\\Controller\\AuditTrailControllerTest::testDestroyMultipleAlwaysReturns405Immutable":8,"Unit\\Controller\\AuditTrailControllerTest::testDestroyMultipleException":8,"Unit\\Controller\\AuditTrailControllerTest::testClearAllSuccess":8,"Unit\\Controller\\AuditTrailControllerTest::testClearAllNoExpired":8,"Unit\\Controller\\AuditTrailControllerTest::testClearAllException":8,"Unit\\Controller\\AuditTrailControllerTest::testDestroyMultipleWithArrayIds":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testGetVectorStatsSuccess":1,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testGetVectorStatsException":1,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testGetVectorStatsResponseContainsExpectedKeys":1,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testGetVectorStatsExceptionIncludesTrace":1,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetVectorStatsContainsTimestamp":1,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetVectorStatsSuccess":1,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetVectorStatsException":1,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testFlagsGetSystemConfig":1,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testFlagsGetDatabaseConnection":1,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testFlagsGetLogger":1,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testIgnoresPsr11Get":1,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testIgnoresThisPropertyAccess":1,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testIgnoresDocblockExample":1,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testIgnoresStringLiteral":1,"Unit\\Integration\\NotifyPushEndToEndTest::testCreateObjectQueuesNotifyCustomEvent":1,"Unit\\Service\\CalendarEventServiceTest::testGetEventsForObjectReturnsMatchingEvents":8,"Unit\\Service\\CalendarEventServiceTest::testGetEventsForObjectSkipsNonMatching":8,"Unit\\Service\\CalendarEventServiceTest::testGetEventsForObjectThrowsWhenNoUser":8,"Unit\\Service\\CalendarEventServiceTest::testCreateEventBuildsVeventWithProperties":8,"Unit\\Service\\CalendarEventServiceTest::testUnlinkEventRemovesProperties":8,"Unit\\Service\\CalendarEventServiceTest::testUnlinkEventThrowsWhenNotFound":8,"Unit\\Service\\ContactServiceTest::testGetContactsForObjectReturnsResults":8,"Unit\\Service\\ContactServiceTest::testGetContactsForObjectEmpty":8,"Unit\\Service\\ContactServiceTest::testLinkContactThrowsWhenContactNotFound":8,"Unit\\Service\\ContactServiceTest::testLinkContactSuccess":8,"Unit\\Service\\ContactServiceTest::testUnlinkContactNotFound":8,"Unit\\Service\\ContactServiceTest::testGetObjectsForContactReturnsLinks":8,"Unit\\Service\\ContactServiceTest::testDeleteLinksForObjectCleansUp":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsMagicTableDoesNotExist":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsHandlesDbException":8,"Unit\\Service\\TaskServiceTest::testCreateTaskWithDueDate":1,"Unit\\Service\\TaskServiceTest::testCreateTaskEscapesSpecialCharacters":1,"Unit\\Service\\TaskServiceTest::testUpdateTaskSetsCompletedTimestamp":1,"Unit\\Service\\TaskServiceTest::testUpdateTaskRemovesDueDate":1,"Unit\\Service\\TaskServiceTest::testUpdateTaskSetsDueDate":1,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectSkipsEmptyCalendarData":1,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectSkipsNonMatchingUuid":1,"Unit\\Service\\TaskServiceTest::testFindUserCalendarWithArrayComponentSet":1,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns404WhenRelationNotFound":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenFileNotWritable":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenFileNotPresentInUserFolder":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns400WhenMapperRaisesValidationException":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns200OnSuccessfulPatch":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns500OnUnexpectedException":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRejectsUnknownField":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRejectsBasesAsNonArray":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRejectsBasesArrayWithNonStringElement":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRejectsSkipAnonymizationAsNonBool":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testSemanticNoOpSkipsAuditEmission":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testEmptyBodyIsNoOp":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testSettingBasesForFirstTimeEmitsAuditEntry":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testFlippingSkipEmitsAuditEntry":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testPartialChangeOnlyAuditsTheChangedField":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testAuditEmissionFailureDoesNotMaskUpdate":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testFallsBackToSessionUserWhenActingUserIsNull":8,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRecordsSystemActorWhenNoUserIsAvailable":8,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testFlatPathFallsBackToHtmlWhenPlainIsAbsent":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenObjectBoundButHasPermissionDeniesUpdate":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns200WhenObjectBoundAndHasPermissionGrantsUpdate":8,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenEmailBoundButEmailLinkHasNoParentObject":8,"Unit\\Db\\FormLinkTest::testJsonSerializeFormLevelRow":8,"Unit\\Db\\FormLinkTest::testJsonSerializeSubmissionLevelRow":8,"Unit\\Db\\FormLinkTest::testJsonSerializeHandlesNullableFields":8,"Unit\\Db\\FormLinkTest::testSettersAndGetters":8,"Unit\\Service\\FormLinkServiceTest::testGetLinkedFormsGroupsSubmissionsUnderParent":8,"Unit\\Service\\FormLinkServiceTest::testGetLinkedFormsSyntheticParentForOrphanSubmission":8,"Unit\\Service\\FormLinkServiceTest::testLinkFormIsIdempotent":8,"Unit\\Service\\FormLinkServiceTest::testLinkFormRequiresUser":8,"Unit\\Service\\FormLinkServiceTest::testLinkFormSubmissionIsIdempotent":8,"Unit\\Service\\FormLinkServiceTest::testUnlinkFormDelegatesToMapper":8,"Unit\\Service\\FormLinkServiceTest::testUnlinkSubmissionReturnsFalseWhenMissing":8,"Unit\\Service\\FormLinkServiceTest::testUnlinkSubmissionDeletesFoundRow":8,"Unit\\Service\\FormLinkServiceTest::testCreateAndLinkFormRequiresFormsApp":8,"Unit\\Service\\FormLinkServiceTest::testGetAvailableFormsReturnsEmptyWhenFormsMissing":8,"Unit\\Service\\FormLinkServiceTest::testGetAvailableFormsReturnsEmptyWhenNoUser":8,"Unit\\Db\\ContactLinkTest::testJsonSerializeReturnsAllFields":8,"Unit\\Db\\ContactLinkTest::testJsonSerializeHandlesNulls":8,"Unit\\Db\\ContactLinkTest::testSettersAndGetters":8,"Unit\\Service\\ContactServiceTest::testGetContactsForObjectEnrichesWithFullVcard":8,"Unit\\Service\\ContactServiceTest::testGetContactsForObjectEnrichesWithInlineBase64Photo":8,"Unit\\Service\\ContactServiceTest::testGetContactsForObjectEnrichesWithPartialVcard":8,"Unit\\Service\\ContactServiceTest::testGetContactsForObjectGracefulWhenVcardMissing":8,"Unit\\Service\\ContactServiceTest::testGetContactsForObjectGracefulWhenCardDavThrows":8,"Unit\\Service\\ContactServiceTest::testUnlinkContactToleratesMissingVcard":8,"Unit\\Service\\ContactServiceTest::testUnlinkContactToleratesThrowableDuringCleanup":8},"times":{"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\FileTextExtractionJobTest::testSuccessfulTextExtraction":0,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\FileTextExtractionJobTest::testExtractionDisabledInConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\FileTextExtractionJobTest::testExtractionWhenConfigKeyMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\FileTextExtractionJobTest::testExceptionHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\FileTextExtractionJobTest::testMissingFileIdArgument":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesAppliesSingleField":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesAppliesMultipleFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesWithEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesWithNullValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesWithBooleanValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesWithArrayValues":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesPreservesDataTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesPerformance":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testDataDrivenApproachReducesComplexity":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesIgnoresUnknownFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerRefactoredMethodsTest::testApplyConfigurationUpdatesWithGitHubFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testValidateAndGetObjectReturnsExistingObject":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testValidateAndGetObjectReturnsNullWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testValidateUploadedFileWithValidFile":0.008,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testValidateUploadedFileWithUploadError":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testValidateUploadedFileWithNonReadableFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testNormalizeSingleFileNormalizesStructure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testNormalizeMultipleFilesWithMultipleFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testNormalizeMultipleFilesWithSingleFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testNormalizeMultipartFilesWithSingleFile":0.011,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testNormalizeMultipartFilesWithMultipleFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testNormalizeMultipartFilesWithEmptyFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FilesControllerRefactoredMethodsTest::testProcessUploadedFilesWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testIndexReturnsValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testIndexHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdateReturnsValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testLoadReturnsValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdatePublishingOptionsReturnsValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testRebaseReturnsValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testStatisticsReturnsValidStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetStatisticsReturnsValidStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetVersionInfoReturnsValidStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetSearchBackendReturnsValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdateSearchBackendReturnsValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdateSearchBackendWithMissingBackend":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testAllEndpointsReturnJsonResponse":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testConstructorRegistersFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testConstructorDefaultValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetName":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetDescription":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetOrganisationNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetStorageQuota":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetBandwidthQuota":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetRequestQuota":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetCreated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetConfigurations":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetConfigurationsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetRegisters":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetRegistersNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetSchemasNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testIsActiveDefaultTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetActiveFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetActiveTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetActiveNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetActiveEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetActiveTruthyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetActiveFalsyStringZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetGroups":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetGroupsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testGetAuthorizationDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAuthorizationArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAuthorizationJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAuthorizationInvalidJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAuthorizationNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testGetJsonFields":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testHydrateBasicFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testHydrateJsonFieldsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testHydrateIgnoresInvalidProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testHydrateWithJsonFieldsPopulated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testJsonSerializeStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testJsonSerializeQuotaStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testJsonSerializeUsageStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testJsonSerializeDatesFormatted":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testJsonSerializeDatesNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testJsonSerializeManagedByConfigurationNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testJsonSerializeManagedByConfigurationSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testToStringReturnsUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testToStringGeneratesUuidWhenNull":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testToStringGeneratesUuidWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testIsValidUuidWithValidUuid":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testIsValidUuidWithInvalidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testIsValidUuidWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testIsManagedByConfigurationTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testIsManagedByConfigurationFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testIsManagedByConfigurationEmptyConfigurations":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testIsManagedByConfigurationNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testGetManagedByConfigurationReturnsConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testGetManagedByConfigurationReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testGetManagedByConfigurationEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetAndGetManagedByConfigurationEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testGetManagedByConfigurationEntityDefaultNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ApplicationTest::testSetManagedByConfigurationEntityNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testConstructorRegistersFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testConstructorDefaultValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetDescription":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetType":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetApp":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetSourceType":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetSourceUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetLocalVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetRemoteVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetLastChecked":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetAutoUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetNotificationGroups":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetGithubRepo":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetGithubBranch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetGithubPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetIsLocal":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetSyncEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetSyncInterval":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetLastSyncDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetSyncStatus":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetOpenregister":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetRegisters":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetViews":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetAgents":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetSources":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetApplications":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetCreated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testSetAndGetUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsValidUuidWithValidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsValidUuidWithInvalidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsValidUuidWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testGetJsonFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHydrateBasicFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHydrateApplicationMapsToApp":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHydrateApplicationDoesNotOverrideApp":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHydrateJsonFieldsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHydrateIgnoresInvalidProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHydrateWithJsonFieldsPopulated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHydrateSyncFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHasUpdateAvailableTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHasUpdateAvailableFalseSameVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHasUpdateAvailableFalseLocalNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHasUpdateAvailableFalseRemoteNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHasUpdateAvailableFalseLocalNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testHasUpdateAvailableFalseBothNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsRemoteSourceGithub":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsRemoteSourceGitlab":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsRemoteSourceUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsRemoteSourceFalseForLocal":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsRemoteSourceFalseForManual":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsRemoteSourceFalseForNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsLocalSourceTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsLocalSourceFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsLocalSourceFalseForNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsManualSourceTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsManualSourceFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testIsManualSourceFalseForNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testJsonSerializeStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testJsonSerializeAppAndApplicationAlias":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testJsonSerializeDatesFormatted":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testJsonSerializeDatesNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testToStringReturnsTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testToStringFallsBackToType":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testToStringFallsBackToId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testToStringFinalFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testToStringTitlePrecedence":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testToStringTypePrecedenceOverId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testToStringEmptyTitleFallsToType":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationTest::testToStringEmptyTypeFallsToId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testConstructorFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testConstructorDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetterReturnsEmptyArrayForNullArrayFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetObjectInjectsUuidAsId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetObjectIdIsFirstKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetObjectWithNullObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetJsonFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testHydrateBasicFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testHydrateConvertsEmptyJsonArraysToNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testHydrateIgnoresInvalidProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testHydrateAddsMetadataIfMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testJsonSerializeStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testJsonSerializeNameFallbackToUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testJsonSerializeOrganisationAtTopLevel":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetObjectArrayContainsAllMetadataFields":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetObjectArraySelfOverrides":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetObjectArrayRelevanceIncluded":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetObjectArrayRelevanceExcluded":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsLockedFalseWhenNotLocked":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsLockedTrueWhenLocked":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsLockedFalseWhenExpired":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsLockedLegacyFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsLockedLegacyExpired":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsLockedPermanentWhenNoExpiration":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetLockInfoWhenLocked":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetLockInfoReturnsNullWhenNotLocked":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetLockedByWhenLocked":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetLockedByReturnsNullWhenNotLocked":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testLockNewLock":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testLockThrowsWithNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testLockByDifferentUserThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testUnlockWhenNotLocked":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testUnlockByOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testUnlockByDifferentUserThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testUnlockThrowsWithNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testDeleteReturnsEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testDeleteThrowsWithNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testLastLogDefaultsToNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testSetAndGetLastLog":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testSetLastLogNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testSourceDefaultsToNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testSetAndGetSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testSetSourceNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testToStringReturnsUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testToStringIdFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testToStringDefaultFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testToStringEmptyUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsManagedByConfigurationTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsManagedByConfigurationFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsManagedByConfigurationEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testIsManagedByConfigurationNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetManagedByConfigurationReturnsConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testGetManagedByConfigurationReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testConstructorRegistersFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testConstructorDefaultValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetName":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetDescription":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetCreated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetStorageQuota":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetBandwidthQuota":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetRequestQuota":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testAddUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testAddUserDoesNotDuplicate":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testAddMultipleUsers":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testAddUserWhenUsersIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testRemoveUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testRemoveUserReindexesArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testRemoveUserNotInList":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testRemoveUserWhenUsersIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testHasUserReturnsTrueForExistingUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testHasUserReturnsFalseForNonExistingUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testHasUserReturnsFalseWhenUsersIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetUserIdsReturnsEmptyArrayByDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetUserIdsReturnsEmptyArrayWhenNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetUserIdsReturnsUserList":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetRoleReturnsNullWhenRolesIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetRoleFindsRoleById":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetRoleFindsRoleByName":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetRoleReturnsNullForNonExistingRole":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetRoleHandlesRoleWithoutIdOrName":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetGroupsDefaultEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetGroups":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetGroupsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetGroupsWhenNullInternally":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testIsActiveDefaultTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetActiveFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetActiveTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetActiveNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetActiveEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetActiveTruthyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetActiveFalsyStringZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testIsActiveWhenInternallyNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetAuthorizationDefault":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAuthorizationArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAuthorizationJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAuthorizationInvalidJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAuthorizationNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testGetParentDefaultNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetAndGetParent":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetParentNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetChildren":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testSetChildrenNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeStructure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeQuotaStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeUsageStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeChildrenPopulated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeChildrenDefaultEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeDatesFormatted":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeDatesNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeAuthorizationDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testJsonSerializeAuthorizationCustom":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testToStringReturnsUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testToStringGeneratesUuidWhenNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testToStringGeneratesUuidWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\OrganisationTest::testToStringPersistsGeneratedUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testConstructorRegistersFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testConstructorDefaultValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetDescription":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetTablePrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetApplication":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetCreated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetAuthorization":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetAndGetGroups":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetSchemasReturnsself":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetSchemasWithJsonStringParses":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetSchemasWithInvalidJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetSchemasViaReflectionAndGet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetSchemasReturnsEmptyArrayWhenNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetSchemasFiltersViaReflection":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetJsonFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testHydrateBasicFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testHydrateJsonFieldsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testHydrateIgnoresInvalidProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testHydrateWithJsonFieldsPopulated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testHydrateAddsMetadataKeyIfMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializeStructure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializeQuotaStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializeUsageStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializeUsageGroupsCountEmptyGroups":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializeDatesFormatted":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializeDatesNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializePublishedDepublishedFormatted":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializeSchemasFiltersNonScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testJsonSerializeSchemasViaReflection":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testToStringReturnsTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testToStringReturnsSlugWhenTitleNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testToStringReturnsSlugWhenTitleEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testToStringReturnsFallbackWithId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testToStringReturnsFallbackUnknownWhenNoId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testToStringPrefersTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testToStringPrefersSlugOverIdFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsManagedByConfigurationTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsManagedByConfigurationFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsManagedByConfigurationEmptyConfigurations":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsManagedByConfigurationNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsManagedByConfigurationMultipleConfigs":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetManagedByConfigurationReturnsConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetManagedByConfigurationReturnsFirstMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetManagedByConfigurationReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetManagedByConfigurationEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetManagedByConfigurationNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetPublishedDefaultNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetPublishedWithDateTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetPublishedWithString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetPublishedWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetDepublishedDefaultNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetDepublishedWithDateTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetDepublishedWithString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetDepublishedWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetConfigurationDefaultEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetConfigurationWithArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetConfigurationWithJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetConfigurationWithInvalidJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetConfigurationWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetConfigurationWithEmptyJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testSetConfigurationWithJsonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetConfigurationReturnsEmptyArrayWhenNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingEnabledNewFormatBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingEnabledNewFormatById":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingEnabledNewFormatByStringId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingDisabledNewFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingEnabledLegacyFormatById":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingEnabledLegacyFormatBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingDisabledLegacyGlobalFlagOff":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingDisabledLegacySchemaNotInList":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingDisabledEmptyConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingSlugCheckedBeforeId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsMagicMappingNullSlugSkipsSlugCheck":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsAutoCreateTableEnabledNewFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsAutoCreateTableDisabledNewFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsAutoCreateTableEnabledNewFormatBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsAutoCreateTableDefaultsFalseWhenMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsAutoCreateTableFallsBackToMagicMappingLegacy":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsAutoCreateTableFalseWhenNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testIsAutoCreateTableByStringId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testEnableMagicMappingForSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testEnableMagicMappingForSchemaWithoutAutoCreate":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testEnableMagicMappingForSchemaWithComment":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testEnableMagicMappingForSchemaWithoutComment":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testEnableMagicMappingPreservesExistingConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testEnableMagicMappingOverwritesExistingSchemaConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testEnableMagicMappingMultipleSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testDisableMagicMappingForSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testDisableMagicMappingForNonExistentSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testDisableMagicMappingPreservesOtherSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testDisableMagicMappingPreservesAutoCreateTable":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetSchemasWithMagicMappingReturnsIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetSchemasWithMagicMappingExcludesDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetSchemasWithMagicMappingEmptyConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetSchemasWithMagicMappingNoSchemasKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetSchemasWithMagicMappingCastsToInt":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterTest::testGetSchemasWithMagicMappingSkipsMissingFlag":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testConstructorFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testConstructorDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetRequiredReturnsEmptyArrayOnNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetRequiredReturnsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetRequiredJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetRequiredInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetRequiredNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetPropertiesReturnsEmptyArrayOnNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPropertyAuthorizationFalseWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPropertyAuthorizationFalseWhenNoAuth":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPropertyAuthorizationTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetPropertyAuthorizationReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetPropertyAuthorizationReturnsRules":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetPropertyAuthorizationReturnsNullForEmptyAuth":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetPropertiesWithAuthorization":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetArchiveReturnsEmptyArrayOnNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetJsonFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testValidatePropertiesEmptyReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testValidatePropertiesDelegatesToValidator":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionAdminGroup":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionAdminUserGroup":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionOwnerMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionEmptyAuthReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionGroupMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionGroupNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionMissingAction":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionComplexEntryWithGroup":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHasPermissionComplexEntryWithMatchNotEvaluated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateBasicFields":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateDefaultRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateDefaultHardValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateExplicitFalseHardValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateEmptyJsonArraysSetToNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateIgnoresInvalidProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateDateTimeStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateDateTimeObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateInvalidDateTimeString":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateConfigurationJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateWithValidator":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testJsonSerializeStructure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testJsonSerializeRequiredEnrichment":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testJsonSerializeDateFormatting":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testJsonSerializeNullDates":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testJsonSerializeHooksDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetSlugPreservesCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetSlugNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetIcon":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetIconNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetConfigurationNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetConfigurationArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetConfigurationJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetConfigurationNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetConfigurationFallbackArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetConfigurationFallbackJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testIsSearchableDefaultTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetSearchableFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testToStringSlugPriority":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testToStringTitleFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testToStringIdFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testToStringUnknownFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetFacetsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetFacetsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetFacetsJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetFacetsInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetFacetsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetAllOfDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetAllOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAllOfNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetAndGetAnyOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetPublishedDateTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetPublishedString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetPublishedNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetDepublishedDateTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetDepublishedString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testSetDepublishedNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testIsManagedByConfigurationTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testIsManagedByConfigurationFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testIsManagedByConfigurationEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testIsManagedByConfigurationNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetManagedByConfigurationReturnsConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testGetManagedByConfigurationReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaTest::testHydrateThenSerialize":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testConstructorRegistersFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testConstructorDefaultValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetName":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetEvents":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetHeaders":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetSecret":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetRetryPolicy":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetMaxRetries":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetTimeout":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetLastTriggeredAt":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetLastSuccessAt":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetLastFailureAt":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetTotalDeliveries":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetSuccessfulDeliveries":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetFailedDeliveries":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetCreated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetAndGetConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetEventsArrayDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetEventsArrayFromJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetEventsArrayInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetEventsArrayNamedArgBug":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetHeadersArrayDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetHeadersArrayFromJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetHeadersArrayInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetHeadersArraySetsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetHeadersArrayNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetFiltersArrayDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetFiltersArrayFromJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetFiltersArrayInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetFiltersArraySetsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetFiltersArrayNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetConfigurationArrayDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetConfigurationArrayFromJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testGetConfigurationArrayInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetConfigurationArraySetsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetConfigurationArrayNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testMatchesEventEmptyEventsMatchesAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testMatchesEventExactMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testMatchesEventNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testMatchesEventWildcard":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testMatchesEventWildcardNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testMatchesEventMultiplePatterns":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testMatchesEventWildcardAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testMatchesEventWildcardPrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testJsonSerializeStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testJsonSerializeSecretMasked":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testJsonSerializeSecretNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testJsonSerializeDatesFormatted":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testJsonSerializeDatesNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testJsonSerializeDefaultArrayFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testJsonSerializeEventsAsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testJsonSerializeConfigurationAsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testHydrateThrowsForNonNullableStringFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testHydrateIdNamedArgBug":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testHydrateSkipsNullValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testHydrateReturnsThis":0,"OCA\\OpenRegister\\Tests\\Unit\\SearchControllerTest::testSearchWithSingleTerm":0,"OCA\\OpenRegister\\Tests\\Unit\\SearchControllerTest::testSearchWithEmptyTerms":0,"OCA\\OpenRegister\\Tests\\Unit\\SearchControllerTest::testSearchWithResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationCachingTest::testActiveOrganisationCacheHit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationCachingTest::testActiveOrganisationCacheMiss":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationCachingTest::testActiveOrganisationCacheExpiration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationCachingTest::testCacheInvalidationOnSetActive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetActiveOrganisationAutoSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testSetActiveOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testActiveOrganisationPersistence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testActiveOrganisationAutoSwitchOnLeave":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testSetNonMemberOrganisationAsActive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testSetNonExistentOrganisationAsActive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetActiveOrganisationViaController":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testActiveOrganisationCacheClearing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testActiveOrganisationSettingWithValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testActiveOrganisationAutoSelectionForUserWithNoOrganisations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testOwnerMetadataSetFromCurrentUser":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testOrganizationMetadataSetFromOrganisationService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testExistingMetadataIsPreserved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testGracefulHandlingWhenUserSessionIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testGracefulHandlingWhenOrganisationServiceFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testBulkOperationsWithMixedMetadataScenarios":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\BulkMetadataHandlingTest::testCachingOptimizationDuringBulkOperations":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testCompareVersionsWithNewerRemote":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testCompareVersionsWithSameVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testCompareVersionsWithOlderRemote":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testCompareVersionsWithNoRemoteVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testIsRemoteSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testIsLocalSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testConfigurationJsonSerializationIncludesNewFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testSchemaManagedByConfigurationDetection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testRegisterManagedByConfigurationDetection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testEntityNotManagedByConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testHasUpdateAvailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ConfigurationServiceTest::testSemanticVersioningComparison":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DataMigrationTest::testExistingDataMigrationToDefaultOrganisation":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DataMigrationTest::testMandatoryOrganisationAndOwnerFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DataMigrationTest::testInvalidOrganisationReferencePrevention":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationCachingTest::testDefaultOrganisationStaticCacheHit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationCachingTest::testDefaultOrganisationCacheExpiration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationCachingTest::testDefaultOrganisationCacheSharedAcrossInstances":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationCachingTest::testDefaultOrganisationCacheInvalidationOnModification":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationCachingTest::testDefaultOrganisationCacheOnFirstTimeCreation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationCachingTest::testDefaultOrganisationPerformanceOptimization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testDefaultOrganisationCreationOnEmptyDatabase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testUserAutoAssignmentToDefaultOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testMultipleDefaultOrganisationsPrevention":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testDatabaseConstraintPreventionOfMultipleDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testActiveOrganisationAutoSettingWithDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testDefaultOrganisationMetadataValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EdgeCasesErrorHandlingTest::testUnauthenticatedRequests":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EdgeCasesErrorHandlingTest::testMalformedJsonRequests":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EdgeCasesErrorHandlingTest::testSqlInjectionAttempts":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EdgeCasesErrorHandlingTest::testVeryLongOrganisationNames":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EdgeCasesErrorHandlingTest::testUnicodeAndSpecialCharacters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EdgeCasesErrorHandlingTest::testNullAndEmptyValueHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EdgeCasesErrorHandlingTest::testExceptionHandlingAndLogging":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EdgeCasesErrorHandlingTest::testRateLimitingSimulation":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testRegisterCreationWithActiveOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testSchemaCreationWithActiveOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testObjectCreationWithActiveOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testEntityAccessWithinSameOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testEntityAccessAcrossOrganisations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testCrossOrganisationObjectCreation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testEntityOrganisationAssignmentValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testBulkEntityOperationsWithOrganisationContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\EntityOrganisationAssignmentTest::testEntityOrganisationInheritance":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Formats\\SemVerFormatTest::testValidSemVerVersions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Formats\\SemVerFormatTest::testInvalidSemVerVersions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Formats\\SemVerFormatTest::testNonStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Formats\\SemVerFormatTest::testSemVerEdgeCases":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testRbacIntegrationWithMultiTenancy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testSearchFilteringByOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testAuditTrailOrganisationContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testCrossOrganisationAccessPrevention":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\IntegrationTest::testMultiTenancyWithComplexRelationships":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testGetTableNameForRegisterSchema#basic_combination":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testGetTableNameForRegisterSchema#different_ids":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testGetTableNameForRegisterSchema#large_ids":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#enabled_in_schema":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#disabled_in_schema":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#not_set_in_schema_global_enabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#not_set_in_schema_global_disabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#null_schema_config_global_enabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#simple_name":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#name_with_hyphens":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#name_with_spaces":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#name_with_special_chars":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#numeric_start":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#consecutive_underscores":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableNameSanitization#trailing_underscores":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#simple_name":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#camelcase_name":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#name_with_spaces":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#name_with_special_chars":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testColumnNameSanitization#numeric_start":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#string_property":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#string_with_max_length":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#email_format":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#uuid_format":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#datetime_format":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#integer_property":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#small_integer":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#big_integer":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#number_property":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#boolean_property":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#array_property":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaPropertyToColumnMapping#object_property":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#valid_json_object":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#valid_json_array":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#invalid_json":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#plain_string":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#empty_string":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testJsonStringDetection#null_string":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableExistenceCheckingWithCaching":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testPrivateTableExistsMethodWithCaching":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testSchemaVersionCalculation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testMetadataColumnsGeneration":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testObjectDataPreparationForTable":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testClearCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testGetExistingSchemaTables":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableCreationWorkflow":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testTableCreationErrorHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testMultipartFileUploadSingleFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testBase64FileUploadWithDataURI":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testURLFileReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testMixedFileTypes":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testArrayOfFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testMultipartFileUploadError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testFileUploadWithInvalidMimeType":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testFileUploadExceedsMaxSize":0.009,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testCorruptedBase64Upload":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testArrayWithValidationError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testFileUploadQueuesBackgroundJobForTextExtraction":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testFileUploadIsNonBlocking":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\IntegratedFileUploadTest::testPDFUploadQueuesBackgroundJob":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithSelfUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithIdField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithExplicitUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataReturnsNullUuidWhenNotProvided":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithRegisterObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithIntegerId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithNullRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithStringThrowsOnInvalidReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterThrowsExceptionForInvalidRegisterString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testFindAndValidateExistingObjectReturnsExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testFindAndValidateExistingObjectReturnsNullWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testFindAndValidateExistingObjectWithRegisterAndSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testClearImageMetadataIfFilePropertyClearsImage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testClearImageMetadataIfFilePropertyPreservesNonFileImage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testClearImageMetadataIfFilePropertyHandlesNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testRefactoredSaveObjectIntegration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testSaveObjectWithNonExistentUuidCreatesNewObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testSaveObjectWithExistingUuidUpdatesObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testSaveObjectWithoutUuidGeneratesNewUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithInversedBySingleObject":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithInversedByArrayObjects":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithoutInversedByStoresIds":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithoutInversedByArrayStoresUuids":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testMixedCascadingScenarios":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testCascadingWithInvalidSchemaReference":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testEmptyCascadingObjectsAreSkipped":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testInversedByWithArrayPropertyAddsToExistingArray":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testScanForRelationsWithSimpleData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectTest::testApplyPropertyDefaultsAppliesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCreateEmptyResultStructure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCreateEmptyResultInitializesArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCreateEmptyResultInitializesStats":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartSingleSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartMixedSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartBelowThreshold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testPrepareObjectsForSaveMixedSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testPrepareObjectsForSaveWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testInitializeResultSetsTotalCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testInitializeResultWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testInitializeResultWithInvalidObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testMergeChunkResultMergesSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testMergeChunkResultMergesFailures":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testMergeChunkResultWithMixedResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsAddsTimingInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsCalculatesEfficiency":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsWithZeroProcessed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsFormatsValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculatePerformanceMetricsWithUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testRefactoredSaveObjectsIntegration":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testRefactoredSaveObjectsWithPartialFailures":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testSetContextFromParametersWithRegisterObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testSetContextFromParametersWithNullValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testExtractUuidAndNormalizeObjectWithArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testExtractUuidAndNormalizeObjectWithObjectEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testExtractUuidAndNormalizeObjectWithExplicitUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testCheckSavePermissionsWithRbacDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testCheckSavePermissionsCreateScenario":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testCheckSavePermissionsUpdateScenario":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testValidateObjectIfRequiredWithValidObject":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testValidateObjectIfRequiredWithInvalidObjectThrowsException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testValidateObjectIfRequiredSkipsWhenHardValidationDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testEnsureObjectFolderReturnsNullWhenNoUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testEnsureObjectFolderCreatesFolderWhenNeeded":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testHandleCascadingWithContextPreservationPreservesContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetRegisterWithRegisterEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetRegisterWithNumericIdUsesCachedLookup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetRegisterWithSlugUsesMapperFind":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaWithSchemaEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaWithNumericIdUsesCachedLookup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaWithSlugUsesMapperFind":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaThrowsWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetObjectWithEntitySetsCurrentObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetObjectWithStringIdUsesUnifiedMapperWhenContextSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetObjectFallsBackToObjectEntityMapperWithoutContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectReturnsNullInitially":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectReturnsCurrentObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetSchemaThrowsWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetSchemaReturnsSchemaId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetRegisterThrowsWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetRegisterReturnsRegisterId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindDelegatesToGetHandlerAndRenders":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindReturnsNullWhenObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindSetsRegisterContextWhenProvided":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindAllDelegatesToGetHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectWithArrayData":0.011,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectWithObjectEntityExtractsUuid":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectSetsContextFromParameters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectDelegatesToDeleteHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectWhenNotFoundChecksPermissionIfSchemaSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPublishDelegatesToPublishHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPublishWithCustomDateAndRbac":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDepublishDelegatesToPublishHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testLockObjectDelegatesToLockHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testUnlockObjectDelegatesToLockHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectsDelegatesToBulkOpsHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectsDelegatesToBulkOpsHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountDelegatesToObjectEntityMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountRemovesLimitFromConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetLogsDelegatesToGetHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectWithArrayNoUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectExtractsFromSelfId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectExtractsFromTopLevelId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectWithObjectEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectPreservesProvidedUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExtractUuidAndNormalizeObjectSkipsEmptyId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesConvertDatetimeToDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesLeavesValidDatesAlone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesSkipsNonDateFormats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesReturnsUnchangedWithoutSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesHandlesSpaceSeparatedDatetime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesLeavesInvalidValuesUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsTrueForValidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsTrueForUppercaseUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsFalseForNonUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsDelegatesToQueryHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testBuildSearchQueryDelegatesToSearchQueryHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetFacetsForObjectsDelegatesToFacetHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindByRelationsDelegatesToMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountSearchObjectsDelegatesToMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetExtendedObjectsDelegatesToRenderHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetCreatedSubObjectsDelegatesToSaveHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testClearCreatedSubObjectsDelegatesToSaveHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetCacheHandlerReturnsInjectedInstance":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCheckSavePermissionsCreateWhenNoUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCheckSavePermissionsUpdateWhenUuidExists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCheckSavePermissionsCreateWhenUuidNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCheckSavePermissionsSkipsWhenNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPrepareFindAllConfigConvertsExtendStringToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPrepareFindAllConfigSetsRegisterFromFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPrepareFindAllConfigSetsSchemaFromFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testRenderEntityDelegatesToRenderHandler":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindSilentDelegatesToGetHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testFindSilentSetsContextWhenProvided":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testHandleCascadingPreservesParentContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderReturnsNullForNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderCreatesFolderForExistingObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderReturnsNullForNewObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testMethodChainingForContextSetters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testCreateNewOrganisation":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetOrganisationDetails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testUpdateOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testSearchOrganisations":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testCreateOrganisationWithEmptyName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testAccessOrganisationWithoutMembership":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testUpdateOrganisationWithoutAccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testOrganisationCreationMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testOrganisationSearchMultipleResults":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testOrganisationNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testOrganisationToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\PerformanceScalabilityTest::testLargeOrganisationWithManyUsers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\PerformanceScalabilityTest::testUserWithManyOrganisations":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\PerformanceScalabilityTest::testConcurrentActiveOrganisationChanges":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\PerformanceScalabilityTest::testDatabaseQueryOptimization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\PerformanceScalabilityTest::testMemoryUsageWithLargeUserLists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\PerformanceScalabilityTest::testCacheEffectivenessUnderLoad":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindByIntegerId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindByStringId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindWithExtensions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindNonExistentThrowsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindWithMultitenancyDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindAllDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindAllWithFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindAllWithMultitenancyDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testFindAllReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testCreateFromArrayMinimalData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testCreateFromArrayWithAllFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testCreateFromArraySetsOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testUpdateFromArrayExistingRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testUpdateFromArrayNonExistentThrowsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testDeleteExistingRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testDeleteReturnsDeletedEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsEmptySchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsBlobSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsSkipsSchemasWithoutId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsMagicTableDoesNotExist":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RegisterServiceTest::testGetSchemaObjectCountsHandlesDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareTypeWithMatchingTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareTypeWithMismatchedTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareTypeWithMissingType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareStringConstraintsWithMatchingMaxLength":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareStringConstraintsWithInadequateMaxLength":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareStringConstraintsWithFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareStringConstraintsWithPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNumericConstraintsWithValidRange":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNumericConstraintsWithInadequateMinimum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNumericConstraintsWithInadequateMaximum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNumericConstraintsWithMissingConstraints":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNullableConstraintWithNullableData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNullableConstraintWithNonNullableData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareNullableConstraintAlreadyNullable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareEnumConstraintWithEnumLikeData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareEnumConstraintWithTooManyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testCompareEnumConstraintAlreadyHasEnum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithEmail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithDateTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromFormatWithEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromPatternsWithBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromPatternsWithInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromPatternsWithFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetTypeFromPatternsWithNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithIntegerString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithFloatString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithBooleanString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithDouble":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypeWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testNormalizeSingleTypePreservesStandardTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithClearMajority":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithMixedTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithNumericTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithSingleType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceRefactoredMethodsTest::testGetDominantTypeWithBooleanPatterns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithListArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithAssociativeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzePropertyValueWithObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatDateTimeRfc3339":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatEmail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatIpv4":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatIpv6":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatColor":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatDuration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatReturnsNullForPlainText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectStringFormatHostname":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternIntegerString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternFloatString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternBooleanString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternSnakeCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternScreamingSnakeCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternCamelCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternPascalCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeStringPatternFilenameNotDetected":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testIsInternalPropertyReturnsTrueForInternalNames":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testIsInternalPropertyReturnsFalseForNormalProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testIsInternalPropertyIsCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeSingleString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeSingleInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeDoubleReturnsNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeFormatOverridesType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeIntegerStringPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeBooleanStringPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testRecommendPropertyTypeMultipleTypesUsesDominant":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectEnumLikeReturnsTrueForFewUniqueValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectEnumLikeReturnsFalseForManyUniqueValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectEnumLikeReturnsFalseWithTooFewExamples":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testDetectEnumLikeReturnsFalseForNonStringTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testExtractEnumValuesReturnsUniqueValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesWithNullExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesExpandsRange":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesTypePromotion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesOverlappingRanges":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testConsolidateFormatDetectionNullExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testConsolidateFormatDetectionSameFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testConsolidateFormatDetectionDifferentFormatsHigherPriorityWins":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testConsolidateFormatDetectionKeepsHigherPriorityExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzezArrayStructureEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzezArrayStructureList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzezArrayStructureAssociative":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeObjectStructureWithStdClass":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeObjectStructureWithScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergePropertyAnalysisMergesTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsForNewProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsSkipsExistingProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsSkipsInternalProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsConfidenceLevels":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testExploreSchemaPropertiesNoObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testExploreSchemaPropertiesThrowsOnMissingSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testExploreSchemaPropertiesDiscoversProperties":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testUpdateSchemaFromExplorationMergesAndSaves":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testUpdateSchemaFromExplorationThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SessionCacheManagementTest::testSessionPersistence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SessionCacheManagementTest::testCachePerformance":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SessionCacheManagementTest::testManualCacheClear":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SessionCacheManagementTest::testCrossUserSessionIsolation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSettings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateSettings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetCacheStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testClearCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testWarmupNamesCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetRbacSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateRbacSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetMultitenancySettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateMultitenancySettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetRetentionSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateRetentionSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSettingsWithException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateSettingsValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesSmall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesOneKB":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesAboveOneKB":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesOneMB":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesOneGB":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesCustomPrecision":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesMegabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesGigabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesKilobytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesPlainNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesUnlimited":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMaskTokenLong":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMaskTokenShort":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMaskTokenExactlyEight":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMaskTokenNineChars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMaskTokenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testIsMultiTenancyEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testIsMultiTenancyEnabledFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetDefaultOrganisationUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetDefaultOrganisationUuidNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testSetDefaultOrganisationUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testSetDefaultOrganisationUuidNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetTenantId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetTenantIdNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetOrganisationId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetOrganisationIdNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetVersionInfoOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetLLMSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateLLMSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetFileSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateFileSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetObjectSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateObjectSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSolrSettings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSolrSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateSolrSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSolrDashboardStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSolrFacetConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateSolrFacetConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetOrganisationSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateOrganisationSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdatePublishingOptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testValidateAllObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSearchBackendConfigStored":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSearchBackendConfigDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSearchBackendConfigException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateSearchBackendConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateSearchBackendConfigWithActiveKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetDatabaseInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetDatabaseInfoEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetDatabaseInfoInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetDatabaseInfoMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testHasPostgresExtensionTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testHasPostgresExtensionFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testHasPostgresExtensionNotPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testHasPostgresExtensionNoCachedData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetPostgresExtensions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetPostgresExtensionsNotPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetPostgresExtensionsNoCachedData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsNoDifferences":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsMissingFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsExtraFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsSkipsSystemFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsTypeMismatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsMultiValuedMismatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsMultipleDifferences":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseAllComponentsTypeError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseSolrOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseSolrComponent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testJoinOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testMultipleOrganisationMembership":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testLeaveOrganisationNonLast":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testJoinNonExistentOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testLeaveLastOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testJoinAlreadyMemberOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testUserMembershipValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testOrganisationStatisticsAfterMembershipChanges":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testConcurrentMembershipOperations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testOrganisationMembershipWithRoleValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testCosineSimilarityIdentical":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testCosineSimilarityOrthogonal":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testCosineSimilarityOpposite":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testCosineSimilarityPartial":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testCosineSimilarityHighDimensional":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testCosineSimilarityNormalization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testReciprocalRankFusionBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testReciprocalRankFusionOnlyVector":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testReciprocalRankFusionOnlySolr":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testReciprocalRankFusionWeights":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testReciprocalRankFusionPreservesMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorEmbeddingServiceTest::testReciprocalRankFusionLargeDataset":0,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperTest::testGetRegisterCountPerSchemaEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperTest::testGetRegisterCountPerSchemaMultiple":0,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperTest::testGetRegisterCountPerSchemaZeroForUnreferenced":0,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvWithBatchSaving":0.003,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvWithErrors":0,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvWithEmptyFile":0,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvWithoutSchema":0.001,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceTest::testImportFromCsvAsync":0,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testDeletableWithEmptyTargets":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testBlockedWithBlockers":0,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testCascadeTargets":0,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testNullifyTargets":0,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testDefaultTargets":0,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testEmptyFactory":0,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testToArrayWithAllFields":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testToArrayEmpty":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testReadonlyProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Dto\\DeletionAnalysisTest::testMixedTargetsNoBLockers":0,"OCA\\OpenRegister\\Tests\\Unit\\Exception\\ReferentialIntegrityExceptionTest::testMessageContainsBlockerCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Exception\\ReferentialIntegrityExceptionTest::testMessageSingleBlocker":0,"OCA\\OpenRegister\\Tests\\Unit\\Exception\\ReferentialIntegrityExceptionTest::testGetAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Exception\\ReferentialIntegrityExceptionTest::testToResponseBody":0,"OCA\\OpenRegister\\Tests\\Unit\\Exception\\ReferentialIntegrityExceptionTest::testExtendsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Exception\\ReferentialIntegrityExceptionTest::testCustomErrorCode":0,"OCA\\OpenRegister\\Tests\\Unit\\Exception\\ReferentialIntegrityExceptionTest::testPreviousExceptionChaining":0,"OCA\\OpenRegister\\Tests\\Unit\\Exception\\ReferentialIntegrityExceptionTest::testResponseBodyWithChainedRestrict":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionValid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testEnsureRelationIndexBuildsFromSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexExcludesNoAction":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexExcludesNoOnDelete":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexHandlesArrayRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexResolvesSlugRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testMultipleSchemasReferencingSameTarget":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexIsCached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteNoIncomingReferences":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteDetectsCascade":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteDetectsRestrict":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetNullNonRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetNullOnRequiredFallsBackToRestrict":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultWithDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultNoDefaultFallsToSetNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultNoDefaultRequiredFallsToRestrict":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteRestrictWithNoDependentsAllowsDeletion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteChainedCascade":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteChainedCascadeIntoRestrict":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteSkipsAlreadyDeletedDependents":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteCircularReferenceDetection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteMixedActionTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testHasIncomingReferencesReturnsFalseForUnreferenced":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testHasIncomingReferencesReturnsTrueForReferenced":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsEmptyAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsExecutionOrder":0,"Unit\\BackgroundJob\\ObjectTextExtractionJobTest::testExtractionDisabledReturnsEarly":0,"Unit\\BackgroundJob\\ObjectTextExtractionJobTest::testMissingObjectIdLogsError":0,"Unit\\BackgroundJob\\ObjectTextExtractionJobTest::testSuccessfulExtraction":0,"Unit\\BackgroundJob\\ObjectTextExtractionJobTest::testExtractionExceptionLogsError":0,"Unit\\BackgroundJob\\ObjectTextExtractionJobTest::testDefaultExtractionModeIsBackground":0,"Unit\\BackgroundJob\\WebhookDeliveryJobTest::testMissingWebhookIdLogsError":0,"Unit\\BackgroundJob\\WebhookDeliveryJobTest::testMissingEventNameLogsError":0,"Unit\\BackgroundJob\\WebhookDeliveryJobTest::testSuccessfulDelivery":0.001,"Unit\\BackgroundJob\\WebhookDeliveryJobTest::testFailedDeliveryLogsWarning":0,"Unit\\BackgroundJob\\WebhookDeliveryJobTest::testMapperExceptionLogsError":0,"Unit\\BackgroundJob\\WebhookDeliveryJobTest::testDefaultAttemptIsOne":0,"Unit\\BackgroundJob\\WebhookDeliveryJobTest::testCustomAttemptNumber":0,"Unit\\Command\\MigrateStorageCommandTest::testCommandName":0,"Unit\\Command\\MigrateStorageCommandTest::testInvalidDirectionReturnsFailure":0.013,"Unit\\Command\\MigrateStorageCommandTest::testResolveExceptionReturnsFailure":0,"Unit\\Command\\MigrateStorageCommandTest::testStatusOnlyReturnsSuccess":0,"Unit\\Command\\MigrateStorageCommandTest::testToMagicMigrationCallsService":0.001,"Unit\\Command\\MigrateStorageCommandTest::testToBlobMigrationCallsService":0.001,"Unit\\Command\\MigrateStorageCommandTest::testMigrationWithFailuresReturnsFailure":0.001,"Unit\\Command\\MigrateStorageCommandTest::testMigrationExceptionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testCommandName":0,"Unit\\Command\\SolrManagementCommandTest::testSolrUnavailableReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testInvalidActionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testClearWithoutForceReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testClearWithForceCallsService":0,"Unit\\Command\\SolrManagementCommandTest::testStatsReturnsSuccess":0,"Unit\\Command\\SolrManagementCommandTest::testStatsUnavailableReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testHealthCheckSuccess":0.002,"Unit\\Command\\SolrManagementCommandTest::testOptimizeSuccess":0.001,"Unit\\Command\\SolrManagementCommandTest::testOptimizeFailureReturnsFailure":0.001,"Unit\\Controller\\AgentsControllerTest::testPage":0.006,"Unit\\Controller\\AgentsControllerTest::testIndexSuccess":0.005,"Unit\\Controller\\AgentsControllerTest::testIndexSuccessNoOrganisation":0,"Unit\\Controller\\AgentsControllerTest::testIndexException":0,"Unit\\Controller\\AgentsControllerTest::testShowSuccess":0,"Unit\\Controller\\AgentsControllerTest::testShowAccessDenied":0,"Unit\\Controller\\AgentsControllerTest::testShowNotFound":0,"Unit\\Controller\\AgentsControllerTest::testCreateSuccess":0,"Unit\\Controller\\AgentsControllerTest::testCreateException":0,"Unit\\Controller\\AgentsControllerTest::testUpdateSuccess":0.003,"Unit\\Controller\\AgentsControllerTest::testUpdateForbidden":0.003,"Unit\\Controller\\AgentsControllerTest::testUpdateException":0.001,"Unit\\Controller\\AgentsControllerTest::testPatchDelegatesToUpdate":0.001,"Unit\\Controller\\AgentsControllerTest::testDestroySuccess":0.001,"Unit\\Controller\\AgentsControllerTest::testDestroyNotAuthenticated":0,"Unit\\Controller\\AgentsControllerTest::testDestroyForbidden":0,"Unit\\Controller\\AgentsControllerTest::testDestroyException":0,"Unit\\Controller\\AgentsControllerTest::testStatsSuccess":0,"Unit\\Controller\\AgentsControllerTest::testStatsException":0.001,"Unit\\Controller\\AgentsControllerTest::testToolsSuccess":0,"Unit\\Controller\\AgentsControllerTest::testToolsException":0,"Unit\\Controller\\ApplicationsControllerTest::testPage":0,"Unit\\Controller\\ApplicationsControllerTest::testIndexSuccess":0,"Unit\\Controller\\ApplicationsControllerTest::testIndexWithPagination":0,"Unit\\Controller\\ApplicationsControllerTest::testIndexWithPage":0,"Unit\\Controller\\ApplicationsControllerTest::testIndexException":0,"Unit\\Controller\\ApplicationsControllerTest::testShowSuccess":0.002,"Unit\\Controller\\ApplicationsControllerTest::testShowNotFound":0.001,"Unit\\Controller\\ApplicationsControllerTest::testCreateSuccess":0,"Unit\\Controller\\ApplicationsControllerTest::testCreateException":0,"Unit\\Controller\\ApplicationsControllerTest::testUpdateSuccess":0,"Unit\\Controller\\ApplicationsControllerTest::testUpdateException":0,"Unit\\Controller\\ApplicationsControllerTest::testPatchDelegatesToUpdate":0,"Unit\\Controller\\ApplicationsControllerTest::testDestroySuccess":0,"Unit\\Controller\\ApplicationsControllerTest::testDestroyException":0,"Unit\\Controller\\AuditTrailControllerTest::testIndexSuccess":0,"Unit\\Controller\\AuditTrailControllerTest::testShowSuccess":0,"Unit\\Controller\\AuditTrailControllerTest::testShowNotFound":0,"Unit\\Controller\\AuditTrailControllerTest::testObjectsSuccess":0,"Unit\\Controller\\AuditTrailControllerTest::testObjectsInvalidArgument":0,"Unit\\Controller\\AuditTrailControllerTest::testObjectsNotFound":0,"Unit\\Controller\\AuditTrailControllerTest::testExportSuccess":0,"Unit\\Controller\\AuditTrailControllerTest::testExportInvalidFormat":0,"Unit\\Controller\\AuditTrailControllerTest::testExportGeneralException":0,"Unit\\Controller\\AuditTrailControllerTest::testDestroySuccess":0,"Unit\\Controller\\AuditTrailControllerTest::testDestroyReturnsFalse":0,"Unit\\Controller\\AuditTrailControllerTest::testDestroyNotFound":0,"Unit\\Controller\\AuditTrailControllerTest::testDestroyMultipleSuccess":0,"Unit\\Controller\\AuditTrailControllerTest::testDestroyMultipleException":0,"Unit\\Controller\\AuditTrailControllerTest::testClearAllSuccess":0,"Unit\\Controller\\AuditTrailControllerTest::testClearAllNoExpired":0,"Unit\\Controller\\AuditTrailControllerTest::testClearAllException":0,"Unit\\Controller\\BulkControllerTest::testDeleteMissingUuids":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteSuccess":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteException":0.001,"Unit\\Controller\\BulkControllerTest::testPublishMissingUuids":0.006,"Unit\\Controller\\BulkControllerTest::testPublishSuccess":0.001,"Unit\\Controller\\BulkControllerTest::testPublishInvalidDatetime":0,"Unit\\Controller\\BulkControllerTest::testDepublishSuccess":0,"Unit\\Controller\\BulkControllerTest::testDepublishMissingUuids":0,"Unit\\Controller\\BulkControllerTest::testSaveSuccess":0.001,"Unit\\Controller\\BulkControllerTest::testSaveMissingObjects":0,"Unit\\Controller\\BulkControllerTest::testPublishSchemaInvalidId":0,"Unit\\Controller\\BulkControllerTest::testPublishSchemaSuccess":0,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaInvalidId":0,"Unit\\Controller\\BulkControllerTest::testDeleteRegisterInvalidId":0,"Unit\\Controller\\BulkControllerTest::testDeleteRegisterSuccess":0,"Unit\\Controller\\BulkControllerTest::testValidateSchemaInvalidId":0,"Unit\\Controller\\BulkControllerTest::testValidateSchemaSuccess":0,"Unit\\Controller\\ChatControllerTest::testPage":0.001,"Unit\\Controller\\ChatControllerTest::testSendMessageEmptyMessage":0,"Unit\\Controller\\ChatControllerTest::testSendMessageMissingConversationAndAgent":0.002,"Unit\\Controller\\ChatControllerTest::testGetHistoryMissingConversationId":0.001,"Unit\\Controller\\ChatControllerTest::testGetHistoryAccessDenied":0.001,"Unit\\Controller\\ChatControllerTest::testGetHistorySuccess":0.006,"Unit\\Controller\\ChatControllerTest::testClearHistoryMissingId":0.001,"Unit\\Controller\\ChatControllerTest::testClearHistoryAccessDenied":0.001,"Unit\\Controller\\ChatControllerTest::testClearHistorySuccess":0.002,"Unit\\Controller\\ChatControllerTest::testSendFeedbackInvalidType":0.001,"Unit\\Controller\\ChatControllerTest::testSendFeedbackAccessDenied":0.001,"Unit\\Controller\\ChatControllerTest::testGetChatStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testIndexSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testIndexException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testShowSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testShowNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testCreateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testCreateException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testUpdateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testUpdateNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testDestroySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testDestroyNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testEnrichDetailsMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testEnrichDetailsGithubSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testCheckVersionNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testCheckVersionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testDiscoverInvalidSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testDiscoverGithubSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitHubBranchesMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitHubBranchesSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testExportSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPreviewNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPreviewSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPreviewException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testShowException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testUpdateException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testDestroyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testEnrichDetailsGitlabReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testEnrichDetailsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testCheckVersionReturnsNullRemoteVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testCheckVersionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testDiscoverGitlabSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testDiscoverException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitHubBranchesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitHubRepositoriesSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitHubRepositoriesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitHubConfigurationsMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitHubConfigurationsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitHubConfigurationsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitLabBranchesMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitLabBranchesSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitLabBranchesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitLabConfigurationsMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitLabConfigurationsSuccess":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testGetGitLabConfigurationsException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testExportNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testExportException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPublishToGitHubNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPublishToGitHubNonLocalConfigReturns400":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPublishToGitHubMissingOwnerRepo":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPublishToGitHubSuccess":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPublishToGitHubException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPublishToGitHubNonDefaultBranch":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testPublishToGitHubAutoGeneratedPath":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromGitHubMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromGitHubSuccess":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromGitHubConflict":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromGitHubException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromGitLabMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromGitLabSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromGitLabException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromUrlMissingUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportFromUrlInvalidUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testEnrichDetailsUnsupportedSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testEnrichDetailsMissingOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testDiscoverMissingSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testExportWithIncludeObjectsParam":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerTest::testImportWithSelectionParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testIndexSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testShowSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testShowNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testCreateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testCreateSetsDefaultSourceType":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testCreateException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testUpdateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testUpdateRemovesImmutableFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testPatchDelegatesToUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testDestroySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testDestroyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testExportSuccess":0.001,"Unit\\Controller\\ConsumersControllerTest::testIndexSuccess":0.001,"Unit\\Controller\\ConsumersControllerTest::testShowSuccess":0,"Unit\\Controller\\ConsumersControllerTest::testShowNotFound":0,"Unit\\Controller\\ConsumersControllerTest::testCreateSuccess":0,"Unit\\Controller\\ConsumersControllerTest::testUpdateSuccess":0,"Unit\\Controller\\ConsumersControllerTest::testUpdateNotFound":0,"Unit\\Controller\\ConsumersControllerTest::testDestroySuccess":0,"Unit\\Controller\\ConsumersControllerTest::testDestroyNotFound":0,"Unit\\Controller\\ConsumersControllerTest::testPatchDelegatesToUpdate":0,"Unit\\Controller\\ConversationControllerTest::testIndexSuccess":0.001,"Unit\\Controller\\ConversationControllerTest::testIndexException":0,"Unit\\Controller\\ConversationControllerTest::testShowSuccess":0,"Unit\\Controller\\ConversationControllerTest::testShowAccessDenied":0,"Unit\\Controller\\ConversationControllerTest::testShowNotFound":0,"Unit\\Controller\\ConversationControllerTest::testMessagesSuccess":0,"Unit\\Controller\\ConversationControllerTest::testMessagesAccessDenied":0,"Unit\\Controller\\ConversationControllerTest::testCreateSuccess":0,"Unit\\Controller\\ConversationControllerTest::testCreateException":0,"Unit\\Controller\\ConversationControllerTest::testUpdateSuccess":0,"Unit\\Controller\\ConversationControllerTest::testUpdateAccessDenied":0,"Unit\\Controller\\ConversationControllerTest::testUpdateNotFound":0,"Unit\\Controller\\ConversationControllerTest::testDestroySoftDelete":0,"Unit\\Controller\\ConversationControllerTest::testDestroyPermanentWhenAlreadyArchived":0,"Unit\\Controller\\ConversationControllerTest::testDestroyAccessDenied":0,"Unit\\Controller\\ConversationControllerTest::testRestoreSuccess":0,"Unit\\Controller\\ConversationControllerTest::testRestoreNotFound":0,"Unit\\Controller\\ConversationControllerTest::testDestroyPermanentSuccess":0,"Unit\\Controller\\ConversationControllerTest::testDestroyPermanentAccessDenied":0,"Unit\\Controller\\DashboardControllerTest::testChartMethodsHandleExceptions#objectsByRegister":0,"Unit\\Controller\\DashboardControllerTest::testChartMethodsHandleExceptions#objectsBySchema":0,"Unit\\Controller\\DashboardControllerTest::testChartMethodsHandleExceptions#objectsBySize":0,"Unit\\Controller\\DashboardControllerTest::testChartMethodsHandleExceptions#auditTrailStats":0,"Unit\\Controller\\DashboardControllerTest::testChartMethodsHandleExceptions#actionDistribution":0,"Unit\\Controller\\DashboardControllerTest::testPage":0,"Unit\\Controller\\DashboardControllerTest::testIndexSuccess":0,"Unit\\Controller\\DashboardControllerTest::testIndexException":0,"Unit\\Controller\\DashboardControllerTest::testCalculateSuccess":0,"Unit\\Controller\\DashboardControllerTest::testCalculateException":0,"Unit\\Controller\\DashboardControllerTest::testGetAuditTrailActionChartSuccess":0,"Unit\\Controller\\DashboardControllerTest::testGetAuditTrailActionChartException":0,"Unit\\Controller\\DashboardControllerTest::testGetObjectsByRegisterChartSuccess":0,"Unit\\Controller\\DashboardControllerTest::testGetObjectsBySchemaChartSuccess":0,"Unit\\Controller\\DashboardControllerTest::testGetObjectsBySizeChartSuccess":0,"Unit\\Controller\\DashboardControllerTest::testGetAuditTrailStatisticsSuccess":0,"Unit\\Controller\\DashboardControllerTest::testGetAuditTrailActionDistributionSuccess":0,"Unit\\Controller\\DashboardControllerTest::testGetMostActiveObjectsSuccess":0,"Unit\\Controller\\DashboardControllerTest::testGetMostActiveObjectsException":0,"Unit\\Controller\\DeletedControllerTest::testIndexSuccess":0.001,"Unit\\Controller\\DeletedControllerTest::testIndexException":0,"Unit\\Controller\\DeletedControllerTest::testStatisticsSuccess":0,"Unit\\Controller\\DeletedControllerTest::testStatisticsException":0,"Unit\\Controller\\DeletedControllerTest::testTopDeleters":0,"Unit\\Controller\\DeletedControllerTest::testRestoreObjectNotDeleted":0,"Unit\\Controller\\DeletedControllerTest::testRestoreException":0,"Unit\\Controller\\DeletedControllerTest::testRestoreMultipleNoIds":0,"Unit\\Controller\\DeletedControllerTest::testDestroyObjectNotDeleted":0,"Unit\\Controller\\DeletedControllerTest::testDestroySuccess":0,"Unit\\Controller\\DeletedControllerTest::testDestroyException":0,"Unit\\Controller\\DeletedControllerTest::testDestroyMultipleNoIds":0,"Unit\\Controller\\DeletedControllerTest::testRestoreSuccess":0.001,"Unit\\Controller\\DeletedControllerTest::testRestoreMultipleSuccess":0.001,"Unit\\Controller\\DeletedControllerTest::testRestoreMultipleException":0,"Unit\\Controller\\DeletedControllerTest::testDestroyMultipleSuccess":0.001,"Unit\\Controller\\DeletedControllerTest::testDestroyMultipleException":0,"Unit\\Controller\\DeletedControllerTest::testIndexWithPagination":0.001,"Unit\\Controller\\DeletedControllerTest::testTopDeletersException":0.002,"Unit\\Controller\\EndpointsControllerTest::testIndexSuccess":0,"Unit\\Controller\\EndpointsControllerTest::testIndexException":0,"Unit\\Controller\\EndpointsControllerTest::testShowSuccess":0,"Unit\\Controller\\EndpointsControllerTest::testShowNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testShowException":0,"Unit\\Controller\\EndpointsControllerTest::testCreateMissingRequiredFields":0,"Unit\\Controller\\EndpointsControllerTest::testCreateSuccess":0,"Unit\\Controller\\EndpointsControllerTest::testCreateException":0,"Unit\\Controller\\EndpointsControllerTest::testUpdateSuccess":0,"Unit\\Controller\\EndpointsControllerTest::testUpdateNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testDestroySuccess":0.001,"Unit\\Controller\\EndpointsControllerTest::testDestroyNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointSuccess":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointFailure":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testLogsSuccess":0,"Unit\\Controller\\EndpointsControllerTest::testLogsNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testLogStatsSuccess":0,"Unit\\Controller\\EndpointsControllerTest::testLogStatsNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testAllLogsSuccess":0,"Unit\\Controller\\EndpointsControllerTest::testAllLogsException":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexFilterNonCompleted":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexException":0,"Unit\\Controller\\FileExtractionControllerTest::testShowNotFound":0,"Unit\\Controller\\FileExtractionControllerTest::testExtractSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testExtractFileNotFound":0,"Unit\\Controller\\FileExtractionControllerTest::testExtractException":0,"Unit\\Controller\\FileExtractionControllerTest::testDiscoverSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testDiscoverException":0,"Unit\\Controller\\FileExtractionControllerTest::testExtractAllSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testRetryFailedSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testStatsSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testStatsException":0,"Unit\\Controller\\FileExtractionControllerTest::testCleanup":0,"Unit\\Controller\\FileExtractionControllerTest::testFileTypes":0,"Unit\\Controller\\FileExtractionControllerTest::testVectorizeBatchSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testVectorizeBatchException":0,"Unit\\Controller\\FileSearchControllerTest::testKeywordSearchEmptyQuery":0,"Unit\\Controller\\FileSearchControllerTest::testKeywordSearchNoCollection":0,"Unit\\Controller\\FileSearchControllerTest::testKeywordSearchException":0,"Unit\\Controller\\FileSearchControllerTest::testSemanticSearchEmptyQuery":0,"Unit\\Controller\\FileSearchControllerTest::testSemanticSearchSuccess":0.001,"Unit\\Controller\\FileSearchControllerTest::testSemanticSearchException":0,"Unit\\Controller\\FileSearchControllerTest::testHybridSearchEmptyQuery":0,"Unit\\Controller\\FileSearchControllerTest::testHybridSearchSuccess":0,"Unit\\Controller\\FileSearchControllerTest::testHybridSearchException":0,"Unit\\Controller\\FileTextControllerTest::testGetFileTextReturnsDeprecated":0,"Unit\\Controller\\FileTextControllerTest::testExtractFileTextDisabled":0,"Unit\\Controller\\FileTextControllerTest::testExtractFileTextSuccess":0.001,"Unit\\Controller\\FileTextControllerTest::testExtractFileTextException":0,"Unit\\Controller\\FileTextControllerTest::testBulkExtractSuccess":0,"Unit\\Controller\\FileTextControllerTest::testBulkExtractException":0,"Unit\\Controller\\FileTextControllerTest::testGetStatsSuccess":0,"Unit\\Controller\\FileTextControllerTest::testGetStatsException":0.001,"Unit\\Controller\\FileTextControllerTest::testDeleteFileTextNotImplemented":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexExtractedSuccess":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexExtractedException":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexFileSuccess":0,"Unit\\Controller\\FileTextControllerTest::testGetChunkingStatsSuccess":0,"Unit\\Controller\\FileTextControllerTest::testGetChunkingStatsException":0,"Unit\\Controller\\FileTextControllerTest::testAnonymizeFileNotFound":0,"Unit\\Controller\\FileTextControllerTest::testAnonymizeFileAlreadyAnonymized":0,"Unit\\Controller\\FileTextControllerTest::testAnonymizeFileNoEntities":0,"Unit\\Controller\\FileTextControllerTest::testAnonymizeFileException":0,"Unit\\Controller\\FilesControllerTest::testPage":0,"Unit\\Controller\\FilesControllerTest::testIndexSuccess":0,"Unit\\Controller\\FilesControllerTest::testIndexObjectNotFound":0.008,"Unit\\Controller\\FilesControllerTest::testIndexFilesFolderNotFound":0.003,"Unit\\Controller\\FilesControllerTest::testCreateMissingFileName":0.001,"Unit\\Controller\\FilesControllerTest::testCreateMissingContent":0,"Unit\\Controller\\FilesControllerTest::testCreateSuccess":0.001,"Unit\\Controller\\FilesControllerTest::testCreateObjectNotFound":0,"Unit\\Controller\\FilesControllerTest::testDeleteSuccess":0.001,"Unit\\Controller\\FilesControllerTest::testDeleteObjectNotFound":0,"Unit\\Controller\\FilesControllerTest::testDownloadByIdSuccess":0.002,"Unit\\Controller\\FilesControllerTest::testDownloadByIdNotFound":0,"Unit\\Controller\\FilesControllerTest::testDownloadByIdNotFoundException":0,"Unit\\Controller\\FilesControllerTest::testSaveMissingName":0.001,"Unit\\Controller\\FilesControllerTest::testPublishSuccess":0,"Unit\\Controller\\FilesControllerTest::testPublishObjectNull":0,"Unit\\Controller\\FilesControllerTest::testDepublishSuccess":0.001,"Unit\\Controller\\FilesControllerTest::testDepublishObjectNull":0,"Unit\\Controller\\FilesControllerTest::testUpdateSuccess":0.001,"Unit\\Controller\\FilesControllerTest::testUpdateObjectNotFound":0.001,"Unit\\Controller\\FilesControllerTest::testShowSuccess":0.001,"Unit\\Controller\\FilesControllerTest::testShowObjectNotFound":0,"Unit\\Controller\\FilesControllerTest::testIndexGeneralException":0,"Unit\\Controller\\FilesControllerTest::testCreateMultipartMissingFile":0,"Unit\\Controller\\FilesControllerTest::testDeleteGeneralException":0,"Unit\\Controller\\FilesControllerTest::testPublishObjectNotFound":0,"Unit\\Controller\\FilesControllerTest::testDepublishObjectNotFound":0,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexException":0,"Unit\\Controller\\GdprEntitiesControllerTest::testShowSuccess":0.003,"Unit\\Controller\\GdprEntitiesControllerTest::testShowNotFound":0,"Unit\\Controller\\GdprEntitiesControllerTest::testShowException":0,"Unit\\Controller\\GdprEntitiesControllerTest::testGetTypesException":0,"Unit\\Controller\\GdprEntitiesControllerTest::testGetCategoriesException":0,"Unit\\Controller\\GdprEntitiesControllerTest::testGetStatsException":0,"Unit\\Controller\\GdprEntitiesControllerTest::testDestroySuccess":0,"Unit\\Controller\\GdprEntitiesControllerTest::testDestroyNotFound":0,"Unit\\Controller\\GdprEntitiesControllerTest::testDestroyException":0,"Unit\\Controller\\HeartbeatControllerTest::testHeartbeatReturnsJsonResponse":0,"Unit\\Controller\\HeartbeatControllerTest::testHeartbeatReturnsAliveStatus":0,"Unit\\Controller\\HeartbeatControllerTest::testHeartbeatReturnsTimestamp":0,"Unit\\Controller\\HeartbeatControllerTest::testHeartbeatReturnsMessage":0,"Unit\\Controller\\HeartbeatControllerTest::testHeartbeatReturnsStatus200":0,"Unit\\Controller\\MappingsControllerTest::testIndexReturnsResults":0.002,"Unit\\Controller\\MappingsControllerTest::testIndexWithPagination":0,"Unit\\Controller\\MappingsControllerTest::testIndexWithPagePagination":0,"Unit\\Controller\\MappingsControllerTest::testShowReturnsMapping":0,"Unit\\Controller\\MappingsControllerTest::testShowReturns404WhenNotFound":0,"Unit\\Controller\\MappingsControllerTest::testShowReturns500OnException":0,"Unit\\Controller\\MappingsControllerTest::testCreateReturnsCreatedMapping":0,"Unit\\Controller\\MappingsControllerTest::testCreateRemovesIdFromData":0,"Unit\\Controller\\MappingsControllerTest::testCreateRemovesInternalParams":0,"Unit\\Controller\\MappingsControllerTest::testCreateReturns500OnException":0.002,"Unit\\Controller\\MappingsControllerTest::testUpdateReturnsUpdatedMapping":0,"Unit\\Controller\\MappingsControllerTest::testUpdateReturns404WhenNotFound":0.002,"Unit\\Controller\\MappingsControllerTest::testUpdateReturns500OnException":0.001,"Unit\\Controller\\MappingsControllerTest::testDestroyReturnsEmptyOnSuccess":0.002,"Unit\\Controller\\MappingsControllerTest::testDestroyReturns404WhenNotFound":0,"Unit\\Controller\\MappingsControllerTest::testDestroyReturns500OnException":0,"Unit\\Controller\\MappingsControllerTest::testTestReturnsBadRequestWhenMissingParams":0,"Unit\\Controller\\MappingsControllerTest::testTestReturnsBadRequestWhenMissingMapping":0,"Unit\\Controller\\MappingsControllerTest::testTestReturnsBadRequestWhenMissingInputObject":0,"Unit\\Controller\\MappingsControllerTest::testTestReturnsResultOnSuccess":0,"Unit\\Controller\\MappingsControllerTest::testTestReturns400OnMappingError":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpControllerTest::testDiscoverSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpControllerTest::testDiscoverException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpControllerTest::testDiscoverCapabilitySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpControllerTest::testDiscoverCapabilityNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpControllerTest::testDiscoverCapabilityException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testControllerInstantiation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleWithInvalidJson":0,"Unit\\Controller\\MigrationControllerTest::testStatusReturnsStorageStatus":0,"Unit\\Controller\\MigrationControllerTest::testStatusReturns500OnException":0,"Unit\\Controller\\MigrationControllerTest::testMigrateReturnsBadRequestWhenMissingParams":0,"Unit\\Controller\\MigrationControllerTest::testMigrateReturnsBadRequestForInvalidDirection":0,"Unit\\Controller\\MigrationControllerTest::testMigrateToMagicSuccess":0,"Unit\\Controller\\MigrationControllerTest::testMigrateToBlobSuccess":0,"Unit\\Controller\\MigrationControllerTest::testMigrateReturns500OnException":0,"Unit\\Controller\\NamesControllerTest::testIndexReturnsAllNames":0.001,"Unit\\Controller\\NamesControllerTest::testIndexWithSpecificIds":0,"Unit\\Controller\\NamesControllerTest::testIndexWithJsonArrayIds":0,"Unit\\Controller\\NamesControllerTest::testIndexReturns500OnException":0,"Unit\\Controller\\NamesControllerTest::testCreateWithValidIds":0.001,"Unit\\Controller\\NamesControllerTest::testCreateReturnsBadRequestWhenIdsNotArray":0,"Unit\\Controller\\NamesControllerTest::testCreateReturnsBadRequestWhenIdsMissing":0,"Unit\\Controller\\NamesControllerTest::testCreateReturnsBadRequestWhenIdsEmpty":0,"Unit\\Controller\\NamesControllerTest::testCreateReturns500OnException":0,"Unit\\Controller\\NamesControllerTest::testShowReturnsNameForExistingId":0,"Unit\\Controller\\NamesControllerTest::testShowReturns404WhenNameNotFound":0.004,"Unit\\Controller\\NamesControllerTest::testShowReturns500OnException":0,"Unit\\Controller\\NamesControllerTest::testStatsReturnsStatistics":0,"Unit\\Controller\\NamesControllerTest::testStatsReturns500OnException":0,"Unit\\Controller\\NamesControllerTest::testWarmupReturnsSuccess":0,"Unit\\Controller\\NamesControllerTest::testWarmupReturns500OnException":0,"Unit\\Controller\\NotesControllerTest::testIndexReturnsNotesForObject":0.001,"Unit\\Controller\\NotesControllerTest::testIndexReturns404WhenObjectNotFound":0,"Unit\\Controller\\NotesControllerTest::testIndexReturns404OnDoesNotExistException":0,"Unit\\Controller\\NotesControllerTest::testIndexReturns500OnException":0,"Unit\\Controller\\NotesControllerTest::testCreateReturnsCreatedNote":0,"Unit\\Controller\\NotesControllerTest::testCreateReturns404WhenObjectNotFound":0,"Unit\\Controller\\NotesControllerTest::testCreateReturns400WhenMessageEmpty":0,"Unit\\Controller\\NotesControllerTest::testCreateReturns400OnException":0,"Unit\\Controller\\NotesControllerTest::testDestroyReturnsSuccess":0,"Unit\\Controller\\NotesControllerTest::testDestroyReturns404WhenObjectNotFound":0,"Unit\\Controller\\NotesControllerTest::testDestroyReturns400OnException":0,"Unit\\Controller\\OasControllerTest::testGenerateAllReturnsOasData":0,"Unit\\Controller\\OasControllerTest::testGenerateAllReturns500OnException":0,"Unit\\Controller\\OasControllerTest::testGenerateReturnsOasForRegister":0,"Unit\\Controller\\OasControllerTest::testGenerateReturns500OnException":0,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns204OnSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns500WhenDeleteFails":0.001,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns403OnException":0,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns409OnReferentialIntegrityException":0.002,"Unit\\Controller\\ObjectsControllerTest::testControllerConstructorSetsProperties":0,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns422OnHookStoppedException":0.001,"Unit\\Controller\\ObjectsControllerTest::testLockReturnsLockedObject":0,"Unit\\Controller\\ObjectsControllerTest::testLockReturns404WhenObjectNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testLockReturns500OnError":0,"Unit\\Controller\\ObjectsControllerTest::testUnlockReturnsUnlockedObject":0,"Unit\\Controller\\ObjectsControllerTest::testPublishReturnsPublishedObject":0.001,"Unit\\Controller\\ObjectsControllerTest::testPublishReturns400OnException":0,"Unit\\Controller\\ObjectsControllerTest::testDepublishReturnsDepublishedObject":0,"Unit\\Controller\\ObjectsControllerTest::testDepublishReturns400OnException":0,"Unit\\Controller\\ObjectsControllerTest::testMergeReturnsMergedObject":0.001,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns400WhenTargetMissing":0,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns400WhenObjectDataMissing":0,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns404OnDoesNotExistException":0,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns400OnInvalidArgument":0,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns500OnGenericException":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400WhenSourceMissing":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400WhenTargetMissing":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400WhenObjectsMissing":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400WhenMappingMissing":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturnsMigrationResult":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns404OnDoesNotExist":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns500OnGenericException":0,"Unit\\Controller\\ObjectsControllerTest::testVectorizeBatchSuccess":0,"Unit\\Controller\\ObjectsControllerTest::testVectorizeBatchReturns500OnException":0,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationStatsSuccess":0,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationStatsError":0,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationCountSuccess":0,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationCountError":0,"Unit\\Controller\\ObjectsControllerTest::testValidateReturns400WhenParamsMissing":0,"Unit\\Controller\\ObjectsControllerTest::testValidateReturnsSuccessResult":0,"Unit\\Controller\\ObjectsControllerTest::testValidateReturns500OnException":0,"Unit\\Controller\\ObjectsControllerTest::testClearBlobSuccess":0,"Unit\\Controller\\ObjectsControllerTest::testClearBlobReturns500OnException":0,"Unit\\Controller\\ObjectsControllerTest::testContractsReturnsPaginatedResults":0.001,"Unit\\Controller\\ObjectsControllerTest::testUsesReturnsPaginatedResults":0,"Unit\\Controller\\ObjectsControllerTest::testUsedReturnsPaginatedResults":0,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturnsAnalysis":0.003,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturns404WhenNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturns403OnException":0,"Unit\\Controller\\ObjectsControllerTest::testImportReturns400WhenNoFile":0,"Unit\\Controller\\ObjectsControllerTest::testImportSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testImportReturns500OnException":0,"Unit\\Controller\\ObjectsControllerTest::testIndexReturns404WhenRegisterNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testIndexReturns404WhenSchemaNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404WhenRegisterNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404WhenSchemaNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns404WhenRegisterNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns404WhenSchemaNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenRegisterNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenSchemaNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns404WhenRegisterNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns404WhenSchemaNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns404WhenRegisterNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns404WhenSchemaNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturns404WhenObjectNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturns500OnGenericException":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsReturnsSearchResults":0,"Unit\\Controller\\ObjectsControllerTest::testContractsThrowsWhenNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testUsesThrowsWhenNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testUsedThrowsWhenNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenObjectNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testLogsThrowsWhenFindFails":0,"Unit\\Controller\\ObjectsControllerTest::testImportReturnsSuccessWithValidFile":0,"Unit\\Controller\\ObjectsControllerTest::testImportCatchesRegisterNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testLockWithDurationParameter":0,"Unit\\Controller\\ObjectsControllerTest::testLockWithoutOptionalParams":0,"Unit\\Controller\\ObjectsControllerTest::testValidateReturns400WhenSchemaMissing":0,"Unit\\Controller\\ObjectsControllerTest::testValidateReturns400WhenRegisterMissing":0,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns403WhenNoUser":0,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturns403OnGenericException":0,"Unit\\Controller\\ObjectsControllerTest::testMergeReturns400WhenBothParamsMissing":0,"Unit\\Controller\\ObjectsControllerTest::testPublishWithRegularUser":0,"Unit\\Controller\\ObjectsControllerTest::testDepublishWithRegularUser":0,"Unit\\Controller\\ObjectsControllerTest::testImportWithNoFileReturns400":0,"Unit\\Controller\\ObjectsControllerTest::testShowReturnsObjectOnSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404WhenObjectNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404OnDoesNotExist":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowStripsEmptyValuesByDefault":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns201OnSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns400OnValidationException":0.002,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns422OnHookStoppedException":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns403OnGenericException":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturnsObjectOnSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenObjectInWrongRegisterSchema":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns400OnValidationException":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns422OnHookStoppedException":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns403OnGenericException":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenObjectNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns403OnNotAuthorizedException":0.002,"Unit\\Controller\\ObjectsControllerTest::testPatchReturnsObjectOnSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns404WhenObjectNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns422OnHookStoppedException":0.001,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns500OnGenericException":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturnsObjectOnSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns404WhenObjectNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns422OnHookStoppedException":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns500OnGenericException":0.001,"Unit\\Controller\\ObjectsControllerTest::testUnlockReturns404WhenObjectNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testLogsReturnsPaginatedLogsOnSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns204WhenAdminDeletes":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateReturns400OnInvalidArgumentException":0,"Unit\\Controller\\ObjectsControllerTest::testValidateWithLimitAndOffset":0.001,"Unit\\Controller\\ObjectsControllerTest::testImportWithSchemaParameter":0.001,"Unit\\Controller\\OrganisationControllerTest::testIndexReturnsUserOrganisations":0,"Unit\\Controller\\OrganisationControllerTest::testIndexReturns500OnException":0,"Unit\\Controller\\OrganisationControllerTest::testSetActiveReturnsSuccess":0.003,"Unit\\Controller\\OrganisationControllerTest::testSetActiveReturnsBadRequestOnFailure":0,"Unit\\Controller\\OrganisationControllerTest::testSetActiveReturnsBadRequestOnException":0,"Unit\\Controller\\OrganisationControllerTest::testGetActiveReturnsOrganisation":0,"Unit\\Controller\\OrganisationControllerTest::testGetActiveReturnsNullWhenNoActive":0,"Unit\\Controller\\OrganisationControllerTest::testGetActiveReturns500OnException":0,"Unit\\Controller\\OrganisationControllerTest::testCreateReturnsCreatedOrganisation":0,"Unit\\Controller\\OrganisationControllerTest::testCreateReturnsBadRequestForEmptyName":0,"Unit\\Controller\\OrganisationControllerTest::testCreateReturnsBadRequestOnException":0,"Unit\\Controller\\OrganisationControllerTest::testJoinReturnsSuccess":0,"Unit\\Controller\\OrganisationControllerTest::testJoinReturnsBadRequestOnFailure":0,"Unit\\Controller\\OrganisationControllerTest::testLeaveReturnsSuccess":0,"Unit\\Controller\\OrganisationControllerTest::testLeaveWithUserIdReturnsDifferentMessage":0.001,"Unit\\Controller\\OrganisationControllerTest::testShowReturnsForbiddenWhenNoAccess":0,"Unit\\Controller\\OrganisationControllerTest::testShowReturnsOrganisation":0,"Unit\\Controller\\OrganisationControllerTest::testShowReturns404OnException":0,"Unit\\Controller\\OrganisationControllerTest::testPatchDelegatesToUpdate":0,"Unit\\Controller\\OrganisationControllerTest::testSearchReturnsOrganisations":0.001,"Unit\\Controller\\OrganisationControllerTest::testSearchReturns500OnException":0,"Unit\\Controller\\OrganisationControllerTest::testClearCacheReturnsSuccess":0,"Unit\\Controller\\OrganisationControllerTest::testClearCacheReturns500OnException":0,"Unit\\Controller\\OrganisationControllerTest::testStatsReturnsStatistics":0,"Unit\\Controller\\OrganisationControllerTest::testStatsReturns500OnException":0,"Unit\\Controller\\RegistersControllerTest::testIndexReturnsRegisters":0.001,"Unit\\Controller\\RegistersControllerTest::testIndexWithPagination":0,"Unit\\Controller\\RegistersControllerTest::testShowReturnsRegister":0,"Unit\\Controller\\RegistersControllerTest::testCreateReturnsCreatedRegister":0,"Unit\\Controller\\RegistersControllerTest::testCreateRemovesInternalParamsAndId":0,"Unit\\Controller\\RegistersControllerTest::testCreateReturns500OnGenericException":0,"Unit\\Controller\\RegistersControllerTest::testUpdateReturnsUpdatedRegister":0,"Unit\\Controller\\RegistersControllerTest::testUpdateRemovesImmutableFields":0,"Unit\\Controller\\RegistersControllerTest::testPatchDelegatesToUpdate":0.001,"Unit\\Controller\\RegistersControllerTest::testDestroyReturnsEmptyOnSuccess":0.004,"Unit\\Controller\\RegistersControllerTest::testDestroyReturns404WhenNotFound":0,"Unit\\Controller\\RegistersControllerTest::testDestroyReturns409OnValidationException":0,"Unit\\Controller\\RegistersControllerTest::testDestroyReturns500OnGenericException":0.001,"Unit\\Controller\\RegistersControllerTest::testSchemasReturnsSchemasList":0.001,"Unit\\Controller\\RegistersControllerTest::testSchemasReturns404WhenRegisterNotFound":0,"Unit\\Controller\\RegistersControllerTest::testSchemasReturns500OnGenericException":0,"Unit\\Controller\\RegistersControllerTest::testObjectsReturnsSearchResults":0,"Unit\\Controller\\RegistersControllerTest::testUpdateReturnsErrorOnDBException":0.002,"Unit\\Controller\\RegistersControllerTest::testShowThrowsWhenNotFound":0.003,"Unit\\Controller\\RegistersControllerTest::testExportReturns400OnException":0.001,"Unit\\Controller\\RegistersControllerTest::testImportReturns400WhenNoFile":0,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubReturns400WhenMissingParams":0,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubReturns404WhenRegisterNotFound":0,"Unit\\Controller\\RegistersControllerTest::testStatsReturnsRegisterStatistics":0.003,"Unit\\Controller\\RegistersControllerTest::testStatsReturns404WhenNotFound":0,"Unit\\Controller\\RegistersControllerTest::testStatsReturns500OnGenericException":0,"Unit\\Controller\\RegistersControllerTest::testIndexThrowsOnException":0,"Unit\\Controller\\RegistersControllerTest::testUpdateThrowsOnGenericException":0,"Unit\\Controller\\RegistersControllerTest::testCreateReturns409OnDBException":0,"Unit\\Controller\\RegistersControllerTest::testObjectsThrowsOnException":0,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubReturns500OnException":0,"Unit\\Controller\\RegistersControllerTest::testImportReturns400OnException":0,"Unit\\Controller\\RegistersControllerTest::testPublishReturns404WhenNotFound":0,"Unit\\Controller\\RegistersControllerTest::testPublishReturns400OnException":0,"Unit\\Controller\\RegistersControllerTest::testDepublishReturns404WhenNotFound":0,"Unit\\Controller\\RegistersControllerTest::testDepublishReturns400OnException":0,"Unit\\Controller\\RegistersControllerTest::testPatchThrowsWhenNotFound":0,"Unit\\Controller\\RegistersControllerTest::testExportConfigurationFormatSuccess":0.001,"Unit\\Controller\\RegistersControllerTest::testExportCsvMissingSchemaReturns400":0,"Unit\\Controller\\RegistersControllerTest::testPublishSuccess":0,"Unit\\Controller\\RegistersControllerTest::testDepublishSuccess":0,"Unit\\Controller\\RegistersControllerTest::testPublishWithCustomDate":0,"Unit\\Controller\\RegistersControllerTest::testDepublishWithCustomDate":0,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubSuccess":0.002,"Unit\\Controller\\RegistersControllerTest::testStatsContainsExpectedKeys":0.001,"Unit\\Controller\\RegistersControllerTest::testCreateWithFullParams":0.001,"Unit\\Controller\\RegistersControllerTest::testDestroyReturns500OnDatabaseConstraintException":0.001,"Unit\\Controller\\RegistersControllerTest::testObjectsWithPaginationParams":0,"Unit\\Controller\\RegistersControllerTest::testUpdateReturnsErrorOnDoesNotExist":0,"Unit\\Controller\\RevertControllerTest::testRevertWithDatetimeReturnsSuccess":0.001,"Unit\\Controller\\RevertControllerTest::testRevertWithAuditTrailIdReturnsSuccess":0.001,"Unit\\Controller\\RevertControllerTest::testRevertWithVersionReturnsSuccess":0.004,"Unit\\Controller\\RevertControllerTest::testRevertReturns400WhenNoCriteriaProvided":0.001,"Unit\\Controller\\RevertControllerTest::testRevertReturns404WhenObjectNotFound":0,"Unit\\Controller\\RevertControllerTest::testRevertReturns403WhenNotAuthorized":0,"Unit\\Controller\\RevertControllerTest::testRevertReturns423WhenLocked":0,"Unit\\Controller\\RevertControllerTest::testRevertReturns500OnGenericException":0,"Unit\\Controller\\SchemasControllerTest::testIndexReturnsSchemas":0.001,"Unit\\Controller\\SchemasControllerTest::testIndexWithPagination":0,"Unit\\Controller\\SchemasControllerTest::testShowReturnsSchema":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturnsCreatedSchema":0,"Unit\\Controller\\SchemasControllerTest::testCreateRemovesInternalParams":0.006,"Unit\\Controller\\SchemasControllerTest::testCreateReturns500OnException":0.002,"Unit\\Controller\\SchemasControllerTest::testUpdateReturnsUpdatedSchema":0.001,"Unit\\Controller\\SchemasControllerTest::testPatchDelegatesToUpdate":0,"Unit\\Controller\\SchemasControllerTest::testDestroyReturnsEmptyOnSuccess":0.005,"Unit\\Controller\\SchemasControllerTest::testDestroyReturns500WhenNotFound":0,"Unit\\Controller\\SchemasControllerTest::testDownloadReturnsSchema":0,"Unit\\Controller\\SchemasControllerTest::testDownloadReturns404WhenNotFound":0,"Unit\\Controller\\SchemasControllerTest::testRelatedReturnsRelationships":0.001,"Unit\\Controller\\SchemasControllerTest::testRelatedReturns404WhenSchemaNotFound":0,"Unit\\Controller\\SchemasControllerTest::testRelatedReturns500OnGenericException":0,"Unit\\Controller\\SchemasControllerTest::testStatsReturnsSchemaStatistics":0.001,"Unit\\Controller\\SchemasControllerTest::testStatsReturns404WhenSchemaNotFound":0,"Unit\\Controller\\SchemasControllerTest::testExploreReturnsExplorationResults":0,"Unit\\Controller\\SchemasControllerTest::testExploreReturns500OnException":0,"Unit\\Controller\\SchemasControllerTest::testUpdateFromExplorationReturns400WhenNoProperties":0,"Unit\\Controller\\SchemasControllerTest::testUpdateFromExplorationSuccess":0,"Unit\\Controller\\SchemasControllerTest::testUpdateFromExplorationReturns500OnException":0,"Unit\\Controller\\SchemasControllerTest::testPublishSetsPublicationDate":0,"Unit\\Controller\\SchemasControllerTest::testPublishReturns404WhenSchemaNotFound":0,"Unit\\Controller\\SchemasControllerTest::testDepublishSetsDepublicationDate":0,"Unit\\Controller\\SchemasControllerTest::testDepublishReturns404WhenSchemaNotFound":0,"Unit\\Controller\\SchemasControllerTest::testUpdateRemovesImmutableFields":0,"Unit\\Controller\\SchemasControllerTest::testUpdateReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchControllerTest::testSearchSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchControllerTest::testSearchEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchControllerTest::testSearchFormatsResultsCorrectly":0,"Unit\\Controller\\SearchTrailControllerTest::testIndexReturnsSearchTrails":0.001,"Unit\\Controller\\SearchTrailControllerTest::testIndexReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testShowReturnsSearchTrail":0,"Unit\\Controller\\SearchTrailControllerTest::testShowReturns404WhenNotFound":0,"Unit\\Controller\\SearchTrailControllerTest::testShowReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testStatisticsReturnsData":0,"Unit\\Controller\\SearchTrailControllerTest::testStatisticsReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testPopularTermsReturnsData":0,"Unit\\Controller\\SearchTrailControllerTest::testActivityReturnsData":0,"Unit\\Controller\\SearchTrailControllerTest::testActivityReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testCleanupReturnsResult":0,"Unit\\Controller\\SearchTrailControllerTest::testCleanupReturns400OnInvalidDate":0,"Unit\\Controller\\SearchTrailControllerTest::testCleanupReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testDestroyReturns404WhenNotFound":0,"Unit\\Controller\\SearchTrailControllerTest::testDestroyReturnsSuccess":0,"Unit\\Controller\\SearchTrailControllerTest::testDestroyMultipleReturnsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testGetApiTokensSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testGetApiTokensEmptyTokens":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testGetApiTokensException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testSaveApiTokensSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testSaveApiTokensSkipsMaskedTokens":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testSaveApiTokensException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testTestGitHubTokenEmptyToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testTestGitLabTokenEmptyToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testGetCacheStatsSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testGetCacheStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearCacheSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearCacheDefaultType":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testWarmupNamesCacheSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testWarmupNamesCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testGetWarmupIntervalSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testGetWarmupIntervalException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testSetWarmupIntervalSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testSetWarmupIntervalDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testSetWarmupIntervalTooLow":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearSpecificCollectionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearSpecificCollectionFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearSpecificCollectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetSettingsSuccess#rbac":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetSettingsSuccess#organisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetSettingsSuccess#multitenancy":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetSettingsSuccess#retention":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetSettingsException#rbac":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetSettingsException#organisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetSettingsException#multitenancy":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetSettingsException#retention":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateSettingsSuccess#rbac":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateSettingsSuccess#organisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateSettingsSuccess#multitenancy":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateSettingsSuccess#retention":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateSettingsException#rbac":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateSettingsException#organisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateSettingsException#multitenancy":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateSettingsException#retention":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetObjectSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetObjectSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateObjectSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateObjectSettingsExtractsProviderId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testUpdateObjectSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testPatchObjectSettingsDelegatesToUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetObjectCollectionFieldsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testGetObjectCollectionFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ConfigurationSettingsControllerTest::testCreateMissingObjectFieldsNoCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsExtractsProviderAndChunkingIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionEmptyParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestPresidioConnectionEmptyEndpoint":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestOpenAnonymiserConnectionEmptyEndpoint":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileCollectionFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testCreateMissingFileFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testWarmupFilesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testIndexFileException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testReindexFilesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileIndexStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileExtractionStatsReturnsZerosOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testCreateMissingFileFieldsNoCollectionConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileSettingsReturnsData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsStripsNonFileKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsHandlesNullProvider":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsHandlesStringProvider":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileCollectionFieldsCallsIndexService":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileIndexStatsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testIndexFileSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testIndexFileReturns422WhenFailed":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionHandlesException":3.826,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestPresidioConnectionHandlesException":3.848,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestOpenAnonymiserConnectionHandlesException":3.956,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetLLMSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetLLMSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testUpdateLLMSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testUpdateLLMSettingsExtractsModelIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testUpdateLLMSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testPatchLLMSettingsDelegatesToUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestEmbeddingMissingProvider":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestEmbeddingInvalidConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestEmbeddingSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestEmbeddingFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestChatMissingProvider":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testCheckEmbeddingModelMismatchSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testCheckEmbeddingModelMismatchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testClearAllEmbeddingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testClearAllEmbeddingsFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testClearAllEmbeddingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetVectorStatsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetVectorStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetN8nSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetN8nSettingsEmptyApiKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetN8nSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testUpdateN8nSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testUpdateN8nSettingsPreservesMaskedApiKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testUpdateN8nSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionSuccess":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nMissingConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsMissingConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearIpRateLimitsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearIpRateLimitsMissingIp":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearIpRateLimitsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearUserRateLimitsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearUserRateLimitsMissingUsername":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearUserRateLimitsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearAllRateLimitsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearAllRateLimitsMissingBoth":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearAllRateLimitsIpOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearAllRateLimitsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testGetSolrFieldsSolrUnavailable":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testGetSolrFieldsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testGetSolrFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsSolrUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testFixMismatchedSolrFieldsSolrUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testFixMismatchedSolrFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testListSolrCollectionsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testListSolrCollectionsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testListSolrConfigSetsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testListSolrConfigSetsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateSolrConfigSetSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateSolrConfigSetException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSolrConfigSetSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSolrConfigSetException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateSolrCollectionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateSolrCollectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCopySolrCollectionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCopySolrCollectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testUpdateSolrCollectionAssignmentsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testUpdateSolrCollectionAssignmentsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testTestSolrConnectionSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testTestSolrConnectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testGetSolrMemoryPredictionSolrUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testGetSolrMemoryPredictionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrCommitSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrCommitFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrOptimizeSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrClearSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrClearFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrUnknownOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrInfoSolrUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrInfoSolrAvailable":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrInfoException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrDashboardStatsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrDashboardStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigurationSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigurationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrFacetConfigurationSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrFacetConfigurationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testDiscoverSolrFacetsSolrUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testDiscoverSolrFacetsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testDiscoverSolrFacetsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoverySolrUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrFacetConfigWithDiscoverySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrFacetConfigWithDiscoveryException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ValidationSettingsControllerTest::testValidateAllObjectsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ValidationSettingsControllerTest::testValidateAllObjectsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ValidationSettingsControllerTest::testMassValidateObjectsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ValidationSettingsControllerTest::testMassValidateObjectsInvalidArgument":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ValidationSettingsControllerTest::testMassValidateObjectsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ValidationSettingsControllerTest::testPredictMassValidationMemorySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ValidationSettingsControllerTest::testPredictMassValidationMemoryException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\VectorSettingsControllerTest::testControllerInstantiation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testRebaseHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetStatisticsHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testStatsHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetVersionInfoHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetSearchBackendHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdateHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdatePublishingOptionsHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testLoadHandlesServiceExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSemanticSearchReturnsResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSemanticSearchReturns400ForEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSemanticSearchHandlesExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchReturns400ForEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetObjectServiceReturnsNullWhenInstalled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetObjectServiceThrowsWhenNotInstalled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetConfigurationServiceReturnsService":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetConfigurationServiceThrowsWhenNotInstalled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSetupHandlerReturnsSolrDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSetupHandlerReturns422OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionInvalidBatchSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionNegativeMaxObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoReturnsCached":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testRefreshDatabaseInfoClearsCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchReturnsResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchHandlesExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSchemaMappingContainerThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSchemaMappingReturns422OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testDebugTypeFilteringReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSemanticSearchWithFiltersAndProvider":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchWithFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWhenCacheEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSetupHandlerWithSolrEnabledButServiceUnavailable":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionValidParams":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchReturnsResults":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchReturns400ForEmptyQuery":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchReturns400ForInvalidLimit":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchReturns400ForLimitTooHigh":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturnsResults":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForEmptyQuery":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForInvalidLimit":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForInvalidWeights":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testGetVectorStatsReturnsStats":0,"Unit\\Controller\\SolrControllerTest::testGetVectorStatsReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns400WhenProviderMissing":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns400ForInvalidProvider":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns400WhenOpenaiMissingApiKey":0,"Unit\\Controller\\SolrControllerTest::testListCollectionsReturnsCollections":0,"Unit\\Controller\\SolrControllerTest::testListCollectionsReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testListConfigSetsReturnsConfigSets":0,"Unit\\Controller\\SolrControllerTest::testCreateCollectionReturnsSuccess":0,"Unit\\Controller\\SolrControllerTest::testCreateCollectionReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testCreateConfigSetReturnsSuccess":0,"Unit\\Controller\\SolrControllerTest::testDeleteConfigSetReturnsSuccess":0,"Unit\\Controller\\SolrControllerTest::testCopyCollectionReturnsSuccess":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturns400ForInvalidLimit":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturns400ForNegativeOffset":0,"Unit\\Controller\\SourcesControllerTest::testIndexReturnsSources":0.002,"Unit\\Controller\\SourcesControllerTest::testIndexWithPagination":0,"Unit\\Controller\\SourcesControllerTest::testIndexWithPagePagination":0,"Unit\\Controller\\SourcesControllerTest::testShowReturnsSource":0,"Unit\\Controller\\SourcesControllerTest::testShowReturns404WhenNotFound":0,"Unit\\Controller\\SourcesControllerTest::testCreateReturnsCreatedSource":0,"Unit\\Controller\\SourcesControllerTest::testCreateRemovesInternalParams":0,"Unit\\Controller\\SourcesControllerTest::testUpdateReturnsUpdatedSource":0,"Unit\\Controller\\SourcesControllerTest::testUpdateRemovesImmutableFields":0,"Unit\\Controller\\SourcesControllerTest::testPatchDelegatesToUpdate":0,"Unit\\Controller\\SourcesControllerTest::testDestroyReturnsEmptyOnSuccess":0.002,"Unit\\Controller\\TablesControllerTest::testSyncReturnsSuccessForNumericIds":0.001,"Unit\\Controller\\TablesControllerTest::testSyncReturns500WhenRegisterNotFound":0,"Unit\\Controller\\TablesControllerTest::testSyncReturns500WhenSchemaNotFound":0,"Unit\\Controller\\TablesControllerTest::testSyncReturns500OnException":0,"Unit\\Controller\\TablesControllerTest::testSyncWithStringNumericIds":0,"Unit\\Controller\\TablesControllerTest::testSyncAllReturnsResults":0,"Unit\\Controller\\TablesControllerTest::testSyncAllHandsIndividualErrors":0,"Unit\\Controller\\TablesControllerTest::testSyncAllReturns500OnGlobalException":0,"Unit\\Controller\\TablesControllerTest::testSyncAllSkipsNonArraySchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TagsControllerTest::testGetAllTagsReturnsJsonResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TagsControllerTest::testGetAllTagsReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testIndexSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testIndexObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testIndexDoesNotExistException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testIndexGeneralException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testCreateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testCreateMissingSummary":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testCreateObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testUpdateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testUpdateTaskNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testDestroySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testDestroyTaskNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#registers":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#registersDetails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#schemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#schemasDetails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#sources":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#organisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#objects":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#tables":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#chat":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#configurations":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#deleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#auditTrail":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#searchTrail":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#webhooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#webhooksLogs":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#entities":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#entitiesDetails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#endpoints":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UiControllerTest::testSpaRoutesReturnTemplateResponse#endpointLogs":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testMeSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testMeNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testMeException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testUpdateMeSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testUpdateMeNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testUpdateMeStripsInternalAndImmutableFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLogoutSuccess":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginInvalidCredentials":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginValidationFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginRateLimited":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginDisabledAccount":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testGetGitHubTokenStatusNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testGetGitHubTokenStatusNoToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testGetGitHubTokenStatusWithValidToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testGetGitHubTokenStatusWithInvalidToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testGetGitHubTokenStatusException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testSetGitHubTokenNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testSetGitHubTokenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testSetGitHubTokenInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testSetGitHubTokenSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testRemoveGitHubTokenNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testRemoveGitHubTokenSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserSettingsControllerTest::testRemoveGitHubTokenException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testIndexNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testIndexSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testShowNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testShowSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testShowNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testCreateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testCreateMissingName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testCreateMissingQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testUpdateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testUpdateNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testDestroySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testDestroyNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testShowSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testShowNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateMissingRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testUpdateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testUpdateNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testDestroySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testDestroyNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testTestWebhookSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testTestWebhookNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testEventsReturnsArray":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testLogsReturnsWebhookLogs":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testLogsReturns404WhenWebhookNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testLogsReturns500OnGenericException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testLogStatsReturnsStatistics":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testLogStatsReturns404WhenWebhookNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testLogStatsReturns500OnGenericException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsReturnsLogs":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryReturns404WhenLogNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateRemovesInternalParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testUpdateReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testDestroyReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testTestWebhookReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testShowReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testIndexSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testShowSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testShowNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testCreateInvalidType":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testCreateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testUpdateSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testUpdateNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testDestroySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testDestroyNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testHealthSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testHealthNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testHealthException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WorkflowEngineControllerTest::testAvailable":0,"Unit\\Cron\\ConfigurationCheckJobTest::testConstructorIntervalValues#default one hour":0,"Unit\\Cron\\ConfigurationCheckJobTest::testConstructorIntervalValues#half hour":0.001,"Unit\\Cron\\ConfigurationCheckJobTest::testConstructorIntervalValues#one day":0,"Unit\\Cron\\ConfigurationCheckJobTest::testConstructorIntervalValues#disabled sets year":0,"Unit\\Cron\\ConfigurationCheckJobTest::testConstructorSetsDefaultInterval":0.001,"Unit\\Cron\\ConfigurationCheckJobTest::testConstructorSetsCustomInterval":0,"Unit\\Cron\\ConfigurationCheckJobTest::testConstructorDisabledIntervalSetsOneYear":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunJobDisabledSkipsExecution":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunNoConfigurations":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunSkipsNonRemoteConfiguration":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunRemoteVersionNull":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunConfigurationUpToDate":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunAutoUpdateEnabled":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunAutoUpdateFails":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunAutoUpdateDisabledSendsNotification":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunNotificationFailureHandled":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunCheckRemoteVersionThrows":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunOuterExceptionHandled":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunMultipleConfigurationsMixed":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunUpdateAvailableWithNullVersions":0,"Unit\\Cron\\ConfigurationCheckJobTest::testRunContinuesAfterFailedCheck":0.001,"Unit\\Cron\\LogCleanUpTaskTest::testConstructorSetsInterval":0,"Unit\\Cron\\LogCleanUpTaskTest::testConstructorSetsTimeSensitivity":0,"Unit\\Cron\\LogCleanUpTaskTest::testConstructorDisablesParallelRuns":0,"Unit\\Cron\\LogCleanUpTaskTest::testRunClearsLogsSuccessfully":0,"Unit\\Cron\\LogCleanUpTaskTest::testRunNoExpiredLogs":0,"Unit\\Cron\\LogCleanUpTaskTest::testRunHandlesException":0,"Unit\\Cron\\LogCleanUpTaskTest::testRunHandlesRuntimeException":0,"Unit\\Cron\\LogCleanUpTaskTest::testRunWithNullArgument":0,"Unit\\Cron\\LogCleanUpTaskTest::testRunWithArrayArgument":0,"Unit\\Cron\\SyncConfigurationsJobTest::testConstructorSetsInterval":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunWithNoConfigurations":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunWithNeverSyncedConfigurationSyncsFromGitHub":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunSkipsConfigurationNotDueForSync":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunSyncsDueConfiguration":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunSyncsFromGitLab":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunGitLabInvalidUrlThrows":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunGitLabEmptySourceUrlThrows":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunSyncsFromUrl":0.003,"Unit\\Cron\\SyncConfigurationsJobTest::testRunUrlEmptySourceUrlThrows":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunUrlInvalidJsonThrows":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunSyncsFromLocal":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunLocalEmptySourceUrlThrows":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunUnsupportedSourceTypeThrows":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunGitHubEmptyRepoThrows":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunGitHubEmptyPathThrows":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunGitHubNullBranchDefaultsToMain":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunGitHubFallbackAppId":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunGitHubFallbackVersion":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunGitHubDefaultVersionAndApp":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunOuterExceptionHandled":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunSyncStatusUpdateFailureHandled":0.001,"Unit\\Cron\\SyncConfigurationsJobTest::testRunLocalFallbackAppUnknown":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunMultipleConfigurationsWithMixedResults":0,"Unit\\Cron\\SyncConfigurationsJobTest::testRunContinuesAfterSingleConfigFailure":0,"Unit\\Cron\\WebhookRetryJobTest::testConstructorSetsInterval":0,"Unit\\Cron\\WebhookRetryJobTest::testRunWithNoFailedLogs":0,"Unit\\Cron\\WebhookRetryJobTest::testRunWithDisabledWebhook":0,"Unit\\Cron\\WebhookRetryJobTest::testRunWithMaxRetriesExceeded":0,"Unit\\Cron\\WebhookRetryJobTest::testRunWithSuccessfulRetry":0,"Unit\\Cron\\WebhookRetryJobTest::testRunWithFailedRetry":0,"Unit\\Cron\\WebhookRetryJobTest::testRunWithExceptionDuringRetry":0,"Unit\\Cron\\WebhookRetryJobTest::testRunWithMultipleLogs":0,"Unit\\Cron\\WebhookRetryJobTest::testRunAttemptEqualsMaxRetriesIsSkipped":0,"Unit\\Cron\\WebhookRetryJobTest::testRunAttemptBelowMaxRetriesIsProcessed":0,"Unit\\Cron\\WebhookRetryJobTest::testRunExceptionDoesNotStopOtherLogs":0,"Unit\\Cron\\WebhookRetryJobTest::testRunPassesCorrectAttemptNumber":0,"Unit\\Cron\\WebhookRetryJobTest::testRunAttemptAboveMaxRetriesIsSkipped":0,"Unit\\Cron\\WebhookRetryJobTest::testRunDeliveryExceptionIsCaught":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testConstructorCreatesInstance":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testGetTableNameReturnsExpectedSuffix":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserAccessAgentNonPrivateReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserAccessAgentNullPrivateReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserAccessAgentPrivateOwnerReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserAccessAgentPrivateInvitedUserReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserAccessAgentPrivateNonOwnerNonInvitedReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserAccessAgentPrivateNoInvitedUsersReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserAccessAgentPrivateEmptyInvitedUsersReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserModifyAgentOwnerReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserModifyAgentNonOwnerReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\AgentMapperTest::testCanUserModifyAgentNullOwnerReturnsFalseForAnyUser":0,"Unit\\Db\\AgentTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\AgentTest::testConstructorDefaultValues":0,"Unit\\Db\\AgentTest::testSetAndGetStringFields":0,"Unit\\Db\\AgentTest::testSetAndGetNumericFields":0,"Unit\\Db\\AgentTest::testSetAndGetBooleanFields":0,"Unit\\Db\\AgentTest::testSetAndGetJsonFields":0,"Unit\\Db\\AgentTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\AgentTest::testHasInvitedUserReturnsTrue":0,"Unit\\Db\\AgentTest::testHasInvitedUserReturnsFalse":0,"Unit\\Db\\AgentTest::testHasInvitedUserReturnsFalseWhenNull":0,"Unit\\Db\\AgentTest::testHydrateFromArray":0.001,"Unit\\Db\\AgentTest::testHydrateWithSnakeCaseFallbacks":0,"Unit\\Db\\AgentTest::testJsonSerialize":0.001,"Unit\\Db\\AgentTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\AgentTest::testJsonSerializeNullDates":0,"Unit\\Db\\AgentTest::testJsonSerializeManagedByConfigurationNull":0,"Unit\\Db\\AuditTrailTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\AuditTrailTest::testConstructorDefaultValues":0,"Unit\\Db\\AuditTrailTest::testSetAndGetStringFields":0,"Unit\\Db\\AuditTrailTest::testSetAndGetIntegerFields":0,"Unit\\Db\\AuditTrailTest::testGetChangedReturnsEmptyArrayWhenNull":0,"Unit\\Db\\AuditTrailTest::testSetAndGetChanged":0,"Unit\\Db\\AuditTrailTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\AuditTrailTest::testGetJsonFields":0,"Unit\\Db\\AuditTrailTest::testHydrate":0,"Unit\\Db\\AuditTrailTest::testJsonSerialize":0.001,"Unit\\Db\\AuditTrailTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\AuditTrailTest::testToStringWithUuid":0,"Unit\\Db\\AuditTrailTest::testToStringWithAction":0,"Unit\\Db\\AuditTrailTest::testToStringFallback":0,"Unit\\Db\\ChunkTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\ChunkTest::testConstructorDefaultValues":0,"Unit\\Db\\ChunkTest::testSetAndGetStringFields":0,"Unit\\Db\\ChunkTest::testSetAndGetNumericFields":0,"Unit\\Db\\ChunkTest::testSetAndGetBooleanFields":0,"Unit\\Db\\ChunkTest::testSetAndGetJsonFields":0,"Unit\\Db\\ChunkTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\ChunkTest::testJsonSerialize":0,"Unit\\Db\\ChunkTest::testJsonSerializeDateTimeFormatting":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationMapperTest::testConstructorCreatesInstance":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ConfigurationMapperTest::testGetTableNameReturnsCorrectValue":0,"Unit\\Db\\ConsumerTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\ConsumerTest::testConstructorDefaultValues":0,"Unit\\Db\\ConsumerTest::testSetAndGetStringFields":0,"Unit\\Db\\ConsumerTest::testSetAndGetJsonFields":0,"Unit\\Db\\ConsumerTest::testGetDomainsReturnsEmptyArrayWhenNull":0,"Unit\\Db\\ConsumerTest::testGetIpsReturnsEmptyArrayWhenNull":0,"Unit\\Db\\ConsumerTest::testGetAuthorizationConfigurationReturnsEmptyArrayWhenNull":0,"Unit\\Db\\ConsumerTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\ConsumerTest::testGetJsonFields":0,"Unit\\Db\\ConsumerTest::testHydrate":0,"Unit\\Db\\ConsumerTest::testJsonSerialize":0,"Unit\\Db\\ConsumerTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\ConsumerTest::testJsonSerializeNullDates":0,"Unit\\Db\\ConversationTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\ConversationTest::testConstructorDefaultValues":0,"Unit\\Db\\ConversationTest::testSetAndGetStringFields":0,"Unit\\Db\\ConversationTest::testSetAndGetAgentId":0,"Unit\\Db\\ConversationTest::testSetAndGetMetadata":0,"Unit\\Db\\ConversationTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\ConversationTest::testSoftDeleteReturnsSelf":0,"Unit\\Db\\ConversationTest::testManualSoftDeleteAndRestore":0,"Unit\\Db\\ConversationTest::testRestoreReturnsSelf":0,"Unit\\Db\\ConversationTest::testJsonSerialize":0,"Unit\\Db\\ConversationTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\ConversationTest::testJsonSerializeNullDates":0,"Unit\\Db\\DataAccessProfileTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\DataAccessProfileTest::testConstructorDefaultValues":0,"Unit\\Db\\DataAccessProfileTest::testSetAndGetStringFields":0,"Unit\\Db\\DataAccessProfileTest::testSetAndGetPermissions":0,"Unit\\Db\\DataAccessProfileTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\DataAccessProfileTest::testJsonSerialize":0,"Unit\\Db\\DataAccessProfileTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\DataAccessProfileTest::testJsonSerializeNullDates":0,"Unit\\Db\\DataAccessProfileTest::testToStringWithName":0,"Unit\\Db\\DataAccessProfileTest::testToStringWithUuid":0,"Unit\\Db\\DataAccessProfileTest::testToStringFallback":0,"Unit\\Db\\DeployedWorkflowTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\DeployedWorkflowTest::testConstructorDefaultValues":0,"Unit\\Db\\DeployedWorkflowTest::testSetAndGetStringFields":0,"Unit\\Db\\DeployedWorkflowTest::testSetAndGetVersion":0,"Unit\\Db\\DeployedWorkflowTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\DeployedWorkflowTest::testHydrate":0,"Unit\\Db\\DeployedWorkflowTest::testJsonSerialize":0,"Unit\\Db\\DeployedWorkflowTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\DeployedWorkflowTest::testJsonSerializeNullDates":0,"Unit\\Db\\EndpointLogTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\EndpointLogTest::testConstructorDefaultValues":0.001,"Unit\\Db\\EndpointLogTest::testSetAndGetStringFields":0,"Unit\\Db\\EndpointLogTest::testSetAndGetIntegerFields":0,"Unit\\Db\\EndpointLogTest::testSetAndGetJsonFields":0,"Unit\\Db\\EndpointLogTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\EndpointLogTest::testSetAndGetSize":0,"Unit\\Db\\EndpointLogTest::testCalculateSize":0,"Unit\\Db\\EndpointLogTest::testCalculateSizeMinimum":0,"Unit\\Db\\EndpointLogTest::testGetJsonFields":0,"Unit\\Db\\EndpointLogTest::testHydrate":0,"Unit\\Db\\EndpointLogTest::testJsonSerialize":0,"Unit\\Db\\EndpointLogTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\EndpointTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\EndpointTest::testConstructorDefaultValues":0,"Unit\\Db\\EndpointTest::testSetAndGetStringFields":0,"Unit\\Db\\EndpointTest::testSetAndGetJsonFields":0,"Unit\\Db\\EndpointTest::testGetSlugGeneratedFromName":0,"Unit\\Db\\EndpointTest::testGetSlugReturnsSetSlug":0,"Unit\\Db\\EndpointTest::testGetJsonFields":0,"Unit\\Db\\EndpointTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\EndpointTest::testHydrate":0,"Unit\\Db\\EndpointTest::testJsonSerialize":0.001,"Unit\\Db\\EndpointTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\EndpointTest::testJsonSerializeNullDates":0,"Unit\\Db\\EntityRelationTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\EntityRelationTest::testConstructorDefaultValues":0.001,"Unit\\Db\\EntityRelationTest::testSetAndGetIntegerFields":0,"Unit\\Db\\EntityRelationTest::testSetAndGetStringFields":0,"Unit\\Db\\EntityRelationTest::testSetAndGetConfidence":0,"Unit\\Db\\EntityRelationTest::testSetAndGetAnonymized":0,"Unit\\Db\\EntityRelationTest::testSetAndGetCreatedAt":0,"Unit\\Db\\EntityRelationTest::testJsonSerialize":0.001,"Unit\\Db\\EntityRelationTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\EntityRelationTest::testJsonSerializeNullCreatedAt":0,"Unit\\Db\\FeedbackTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\FeedbackTest::testConstructorDefaultValues":0,"Unit\\Db\\FeedbackTest::testSetAndGetStringFields":0,"Unit\\Db\\FeedbackTest::testSetAndGetIntegerFields":0,"Unit\\Db\\FeedbackTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\FeedbackTest::testJsonSerialize":0,"Unit\\Db\\FeedbackTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\FeedbackTest::testJsonSerializeNullDates":0,"Unit\\Db\\GdprEntityTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\GdprEntityTest::testConstructorDefaultValues":0,"Unit\\Db\\GdprEntityTest::testSetAndGetStringFields":0,"Unit\\Db\\GdprEntityTest::testSetAndGetBelongsToEntityId":0,"Unit\\Db\\GdprEntityTest::testSetAndGetMetadata":0,"Unit\\Db\\GdprEntityTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\GdprEntityTest::testConstants":0,"Unit\\Db\\GdprEntityTest::testJsonSerialize":0,"Unit\\Db\\GdprEntityTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\GdprEntityTest::testJsonSerializeNullDates":0,"Unit\\Db\\MappingTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\MappingTest::testConstructorDefaultValues":0,"Unit\\Db\\MappingTest::testSetAndGetStringFields":0,"Unit\\Db\\MappingTest::testSetAndGetJsonFields":0,"Unit\\Db\\MappingTest::testGetMappingReturnsEmptyArrayWhenNull":0,"Unit\\Db\\MappingTest::testGetUnsetReturnsEmptyArrayWhenNull":0,"Unit\\Db\\MappingTest::testGetCastReturnsEmptyArrayWhenNull":0,"Unit\\Db\\MappingTest::testGetConfigurationsReturnsEmptyArrayWhenNull":0,"Unit\\Db\\MappingTest::testSetAndGetPassThrough":0,"Unit\\Db\\MappingTest::testSetAndGetDateTimeFields":0,"Unit\\Db\\MappingTest::testGetSlugGeneratedFromName":0.044,"Unit\\Db\\MappingTest::testGetSlugReturnsSetSlug":0,"Unit\\Db\\MappingTest::testGetSlugFallbackWhenEmpty":0,"Unit\\Db\\MappingTest::testGetJsonFields":0,"Unit\\Db\\MappingTest::testHydrate":0,"Unit\\Db\\MappingTest::testJsonSerialize":0,"Unit\\Db\\MappingTest::testJsonSerializeDateTimeFormatting":0,"Unit\\Db\\MappingTest::testJsonSerializeNullDates":0,"Unit\\Db\\MessageTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\MessageTest::testConstructorDefaultValues":0,"Unit\\Db\\MessageTest::testConstants":0,"Unit\\Db\\MessageTest::testSetAndGetUuid":0,"Unit\\Db\\MessageTest::testSetAndGetConversationId":0,"Unit\\Db\\MessageTest::testSetAndGetRole":0,"Unit\\Db\\MessageTest::testSetAndGetContent":0,"Unit\\Db\\MessageTest::testSetAndGetSources":0,"Unit\\Db\\MessageTest::testSetAndGetSourcesNull":0,"Unit\\Db\\MessageTest::testSetAndGetCreated":0,"Unit\\Db\\MessageTest::testJsonSerializeAllFieldsPresent":0,"Unit\\Db\\MessageTest::testJsonSerializeDefaultValues":0,"Unit\\Db\\MessageTest::testJsonSerializeWithValues":0,"Unit\\Db\\MessageTest::testJsonSerializeCreatedFormattedAsIso8601":0,"Unit\\Db\\MessageTest::testJsonSerializeCreatedNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityMapperTest::testConstructorCreatesInstance":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityMapperTest::testGetTableNameReturnsCorrectValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityMapperTest::testHasJsonFiltersReturnsTrueForDotNotation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityMapperTest::testHasJsonFiltersReturnsFalseForSimpleFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityMapperTest::testHasJsonFiltersReturnsFalseForEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityMapperTest::testHasJsonFiltersIgnoresSchemaIdDotNotation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityMapperTest::testHasJsonFiltersDetectsNestedDotNotation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterMapperTest::testConstructorCreatesInstance":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\RegisterMapperTest::testGetTableNameReturnsCorrectValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaMapperTest::testConstructorCreatesInstance":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\SchemaMapperTest::testGetTableNameReturnsCorrectValue":0,"Unit\\Db\\SearchTrailTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\SearchTrailTest::testConstructorDefaultValues":0,"Unit\\Db\\SearchTrailTest::testSetAndGetUuid":0,"Unit\\Db\\SearchTrailTest::testSetAndGetSearchTerm":0,"Unit\\Db\\SearchTrailTest::testSetAndGetRegisterUuid":0,"Unit\\Db\\SearchTrailTest::testSetAndGetSchemaUuid":0,"Unit\\Db\\SearchTrailTest::testSetAndGetUser":0,"Unit\\Db\\SearchTrailTest::testSetAndGetUserName":0,"Unit\\Db\\SearchTrailTest::testSetAndGetSession":0,"Unit\\Db\\SearchTrailTest::testSetAndGetIpAddress":0,"Unit\\Db\\SearchTrailTest::testSetAndGetUserAgent":0,"Unit\\Db\\SearchTrailTest::testSetAndGetRequestUri":0,"Unit\\Db\\SearchTrailTest::testSetAndGetHttpMethod":0,"Unit\\Db\\SearchTrailTest::testSetAndGetExecutionType":0,"Unit\\Db\\SearchTrailTest::testSetAndGetResultCount":0,"Unit\\Db\\SearchTrailTest::testSetAndGetTotalResults":0,"Unit\\Db\\SearchTrailTest::testSetAndGetRegister":0,"Unit\\Db\\SearchTrailTest::testSetAndGetSchema":0,"Unit\\Db\\SearchTrailTest::testSetAndGetResponseTime":0,"Unit\\Db\\SearchTrailTest::testSetAndGetPage":0,"Unit\\Db\\SearchTrailTest::testGetQueryParametersReturnsEmptyArrayWhenNull":0,"Unit\\Db\\SearchTrailTest::testSetAndGetQueryParameters":0,"Unit\\Db\\SearchTrailTest::testGetFiltersReturnsEmptyArrayWhenNull":0,"Unit\\Db\\SearchTrailTest::testSetAndGetFilters":0,"Unit\\Db\\SearchTrailTest::testGetSortParametersReturnsEmptyArrayWhenNull":0,"Unit\\Db\\SearchTrailTest::testSetAndGetSortParameters":0,"Unit\\Db\\SearchTrailTest::testSetAndGetCreated":0,"Unit\\Db\\SearchTrailTest::testSetAndGetRegisterName":0,"Unit\\Db\\SearchTrailTest::testSetAndGetSchemaName":0,"Unit\\Db\\SearchTrailTest::testGetJsonFields":0,"Unit\\Db\\SearchTrailTest::testHydrateSetsFields":0,"Unit\\Db\\SearchTrailTest::testHydrateConvertsEmptyArrayJsonFieldsToNull":0,"Unit\\Db\\SearchTrailTest::testHydrateReturnsThis":0,"Unit\\Db\\SearchTrailTest::testHydrateIgnoresUnknownFields":0,"Unit\\Db\\SearchTrailTest::testToStringReturnsUuidWhenSet":0,"Unit\\Db\\SearchTrailTest::testToStringReturnsSearchTermWhenNoUuid":0,"Unit\\Db\\SearchTrailTest::testToStringFallsBackToFinalDefault":0,"Unit\\Db\\SearchTrailTest::testJsonSerializeAllFieldsPresent":0.001,"Unit\\Db\\SearchTrailTest::testJsonSerializeFormatsDatetimes":0,"Unit\\Db\\SearchTrailTest::testJsonSerializeDatetimesNullWhenNotSet":0,"Unit\\Db\\SearchTrailTest::testJsonSerializeWithValues":0,"Unit\\Db\\SourceTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\SourceTest::testConstructorDefaultValues":0,"Unit\\Db\\SourceTest::testSetAndGetUuid":0,"Unit\\Db\\SourceTest::testSetAndGetTitle":0,"Unit\\Db\\SourceTest::testSetAndGetVersion":0,"Unit\\Db\\SourceTest::testSetAndGetDescription":0,"Unit\\Db\\SourceTest::testSetAndGetDatabaseUrl":0,"Unit\\Db\\SourceTest::testSetAndGetType":0,"Unit\\Db\\SourceTest::testSetAndGetOrganisation":0,"Unit\\Db\\SourceTest::testSetAndGetOrganisationNull":0,"Unit\\Db\\SourceTest::testSetAndGetUpdated":0,"Unit\\Db\\SourceTest::testSetAndGetCreated":0,"Unit\\Db\\SourceTest::testGetJsonFieldsReturnsEmptyForSource":0,"Unit\\Db\\SourceTest::testHydrateSetsFields":0,"Unit\\Db\\SourceTest::testHydrateReturnsThis":0,"Unit\\Db\\SourceTest::testHydrateIgnoresUnknownFields":0,"Unit\\Db\\SourceTest::testManagedByConfigurationEntityDefaultsToNull":0,"Unit\\Db\\SourceTest::testToStringReturnsTitleWhenSet":0,"Unit\\Db\\SourceTest::testToStringReturnsUuidWhenNoTitle":0,"Unit\\Db\\SourceTest::testToStringFallsBackToDefault":0,"Unit\\Db\\SourceTest::testJsonSerializeAllFieldsPresent":0,"Unit\\Db\\SourceTest::testJsonSerializeDefaultValues":0,"Unit\\Db\\SourceTest::testJsonSerializeFormatsDatetimes":0,"Unit\\Db\\SourceTest::testJsonSerializeWithValues":0,"Unit\\Db\\SourceTest::testIsManagedByConfigurationReturnsFalseWithEmptyArray":0,"Unit\\Db\\SourceTest::testIsManagedByConfigurationReturnsFalseWithNoId":0,"Unit\\Db\\ViewTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\ViewTest::testConstructorDefaultValues":0,"Unit\\Db\\ViewTest::testSetAndGetUuid":0,"Unit\\Db\\ViewTest::testSetAndGetName":0,"Unit\\Db\\ViewTest::testSetAndGetDescription":0,"Unit\\Db\\ViewTest::testSetAndGetOwner":0,"Unit\\Db\\ViewTest::testSetAndGetOrganisation":0,"Unit\\Db\\ViewTest::testSetAndGetIsPublic":0,"Unit\\Db\\ViewTest::testSetAndGetIsDefault":0,"Unit\\Db\\ViewTest::testSetAndGetQuery":0,"Unit\\Db\\ViewTest::testSetAndGetFavoredBy":0,"Unit\\Db\\ViewTest::testGetFavoredByReturnsEmptyArrayWhenNull":0,"Unit\\Db\\ViewTest::testSetAndGetCreated":0,"Unit\\Db\\ViewTest::testSetAndGetUpdated":0,"Unit\\Db\\ViewTest::testQueryAcceptsArrayAndReturnsArray":0,"Unit\\Db\\ViewTest::testQueryAcceptsNullValue":0,"Unit\\Db\\ViewTest::testJsonSerializeAllFieldsPresent":0.001,"Unit\\Db\\ViewTest::testJsonSerializeDefaultValues":0,"Unit\\Db\\ViewTest::testJsonSerializeQuotaStructure":0,"Unit\\Db\\ViewTest::testJsonSerializeUsageStructure":0,"Unit\\Db\\ViewTest::testJsonSerializeUsageCountsFavoredByUsers":0,"Unit\\Db\\ViewTest::testJsonSerializeFormatsDatetimes":0,"Unit\\Db\\ViewTest::testJsonSerializeDatetimesNullWhenNotSet":0,"Unit\\Db\\ViewTest::testGetManagedByConfigurationReturnsNullWithEmptyArray":0,"Unit\\Db\\ViewTest::testGetManagedByConfigurationReturnsNullWithNoId":0,"Unit\\Db\\WebhookLogTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\WebhookLogTest::testConstructorDefaultValues":0,"Unit\\Db\\WebhookLogTest::testConstructorInitializesCreatedTimestamp":0,"Unit\\Db\\WebhookLogTest::testSetAndGetWebhook":0,"Unit\\Db\\WebhookLogTest::testSetAndGetEventClass":0,"Unit\\Db\\WebhookLogTest::testSetAndGetPayload":0,"Unit\\Db\\WebhookLogTest::testSetAndGetPayloadNull":0,"Unit\\Db\\WebhookLogTest::testSetAndGetUrl":0,"Unit\\Db\\WebhookLogTest::testSetAndGetMethod":0,"Unit\\Db\\WebhookLogTest::testSetAndGetSuccess":0,"Unit\\Db\\WebhookLogTest::testSetAndGetStatusCode":0,"Unit\\Db\\WebhookLogTest::testSetAndGetStatusCodeNull":0,"Unit\\Db\\WebhookLogTest::testSetAndGetRequestBody":0,"Unit\\Db\\WebhookLogTest::testSetAndGetResponseBody":0,"Unit\\Db\\WebhookLogTest::testSetAndGetErrorMessage":0,"Unit\\Db\\WebhookLogTest::testSetAndGetAttempt":0,"Unit\\Db\\WebhookLogTest::testSetAndGetNextRetryAt":0,"Unit\\Db\\WebhookLogTest::testSetAndGetCreated":0,"Unit\\Db\\WebhookLogTest::testGetPayloadArrayReturnsEmptyArrayWhenNull":0,"Unit\\Db\\WebhookLogTest::testGetPayloadArrayParsesJson":0,"Unit\\Db\\WebhookLogTest::testGetPayloadArrayReturnsEmptyForInvalidJson":0,"Unit\\Db\\WebhookLogTest::testSetPayloadArrayDoesNotStoreValueDueToNamedArgBug":0,"Unit\\Db\\WebhookLogTest::testSetPayloadArrayNullClearsPayload":0,"Unit\\Db\\WebhookLogTest::testJsonSerializeAllFieldsPresent":0,"Unit\\Db\\WebhookLogTest::testJsonSerializeDefaultValues":0,"Unit\\Db\\WebhookLogTest::testJsonSerializePayloadAsArray":0,"Unit\\Db\\WebhookLogTest::testJsonSerializeFormatsDatetimes":0,"Unit\\Db\\WebhookLogTest::testJsonSerializeNextRetryAtNullWhenNotSet":0,"Unit\\Db\\WebhookLogTest::testJsonSerializeWithFullData":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookMapperTest::testConstructorCreatesInstance":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookMapperTest::testGetTableNameReturnsCorrectValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetEventsArrayStoresJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetHeadersArrayStoresJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetFiltersArrayStoresJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testSetConfigurationArrayStoresJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testHydrateSetsStringFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\WebhookTest::testHydrateIdSetsCorrectly":0,"Unit\\Db\\WorkflowEngineTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\WorkflowEngineTest::testConstructorDefaultValues":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetUuid":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetName":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetEngineType":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetBaseUrl":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetAuthType":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetAuthConfig":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetEnabled":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetDefaultTimeout":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetHealthStatus":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetLastHealthCheck":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetCreated":0,"Unit\\Db\\WorkflowEngineTest::testSetAndGetUpdated":0,"Unit\\Db\\WorkflowEngineTest::testHydrateSetsKnownFields":0,"Unit\\Db\\WorkflowEngineTest::testHydrateIgnoresUnknownFields":0,"Unit\\Db\\WorkflowEngineTest::testHydrateReturnsThis":0,"Unit\\Db\\WorkflowEngineTest::testJsonSerializeAllFieldsPresent":0,"Unit\\Db\\WorkflowEngineTest::testJsonSerializeExcludesAuthConfig":0,"Unit\\Db\\WorkflowEngineTest::testJsonSerializeDefaultValues":0,"Unit\\Db\\WorkflowEngineTest::testJsonSerializeFormatsDatetimes":0,"Unit\\Db\\WorkflowEngineTest::testJsonSerializeDatetimesNullWhenNotSet":0,"Unit\\Db\\WorkflowEngineTest::testJsonSerializeWithFullData":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlVariousCombinations#simple uuid":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlVariousCombinations#integer id":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlVariousCombinations#no placeholders":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlVariousCombinations#custom key":0,"Unit\\Dto\\DeepLinkRegistrationTest::testConstructorWithAllParameters":0,"Unit\\Dto\\DeepLinkRegistrationTest::testConstructorDefaultIcon":0,"Unit\\Dto\\DeepLinkRegistrationTest::testPropertiesAreReadonly":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlReplacesUuid":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlReplacesId":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlReplacesRegisterAndSchema":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlReplacesCustomTopLevelKeys":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlIgnoresNonScalarValues":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlMissingPlaceholderLeavesEmpty":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlNoPlaceholders":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlMultiplePlaceholders":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlCastsIntToString":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlBooleanScalar":0,"Unit\\Dto\\DeepLinkRegistrationTest::testResolveUrlEmptyObjectData":0,"Unit\\Dto\\DeletionAnalysisTest::testConstructorWithAllParameters":0,"Unit\\Dto\\DeletionAnalysisTest::testConstructorWithDefaults":0,"Unit\\Dto\\DeletionAnalysisTest::testConstructorNotDeletable":0,"Unit\\Dto\\DeletionAnalysisTest::testReadonlyProperties":0,"Unit\\Dto\\DeletionAnalysisTest::testEmptyReturnsDeletableAnalysis":0,"Unit\\Dto\\DeletionAnalysisTest::testEmptyReturnsEmptyTargets":0,"Unit\\Dto\\DeletionAnalysisTest::testEmptyReturnsNewInstanceEachTime":0,"Unit\\Dto\\DeletionAnalysisTest::testToArrayWithAllData":0,"Unit\\Dto\\DeletionAnalysisTest::testToArrayWithDefaults":0,"Unit\\Dto\\DeletionAnalysisTest::testToArrayFromEmpty":0,"Unit\\Dto\\DeletionAnalysisTest::testToArrayIsJsonSerializable":0,"Unit\\Dto\\DeletionAnalysisTest::testToArrayKeys":0,"Unit\\Dto\\DeletionAnalysisTest::testMixedTargetsNoBlockers":0,"Unit\\Dto\\DeletionAnalysisTest::testBlockedWithBlockers":0,"Unit\\Event\\ObjectSpecialEventsTest::testLockedEventExtendsEvent":0.002,"Unit\\Event\\ObjectSpecialEventsTest::testLockedEventGetObject":0,"Unit\\Event\\ObjectSpecialEventsTest::testUnlockedEventExtendsEvent":0.001,"Unit\\Event\\ObjectSpecialEventsTest::testUnlockedEventGetObject":0,"Unit\\Event\\ObjectSpecialEventsTest::testRevertedEventExtendsEvent":0,"Unit\\Event\\ObjectSpecialEventsTest::testRevertedEventGetObject":0,"Unit\\Event\\ObjectSpecialEventsTest::testRevertedEventUntilNullByDefault":0,"Unit\\Event\\ObjectSpecialEventsTest::testRevertedEventWithDateTimeUntil":0,"Unit\\Event\\ObjectSpecialEventsTest::testRevertedEventWithStringUntil":0,"Unit\\Event\\ObjectSpecialEventsTest::testRevertedEventWithEmptyStringUntil":0,"Unit\\Event\\ObjectStoppableEventsTest::testExtendsEventAndImplementsStoppable#ObjectCreatingEvent":0.001,"Unit\\Event\\ObjectStoppableEventsTest::testExtendsEventAndImplementsStoppable#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testExtendsEventAndImplementsStoppable#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testPropagationNotStoppedByDefault#ObjectCreatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testPropagationNotStoppedByDefault#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testPropagationNotStoppedByDefault#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testStopPropagation#ObjectCreatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testStopPropagation#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testStopPropagation#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testErrorsEmptyByDefault#ObjectCreatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testErrorsEmptyByDefault#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testErrorsEmptyByDefault#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetAndGetErrors#ObjectCreatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetAndGetErrors#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetAndGetErrors#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testModifiedDataEmptyByDefault#ObjectCreatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testModifiedDataEmptyByDefault#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testModifiedDataEmptyByDefault#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetAndGetModifiedData#ObjectCreatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetAndGetModifiedData#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetAndGetModifiedData#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetErrorsOverwritesPrevious#ObjectCreatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetErrorsOverwritesPrevious#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetErrorsOverwritesPrevious#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetModifiedDataOverwritesPrevious#ObjectCreatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetModifiedDataOverwritesPrevious#ObjectUpdatingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testSetModifiedDataOverwritesPrevious#ObjectDeletingEvent":0,"Unit\\Event\\ObjectStoppableEventsTest::testCreatingEventGetObject":0,"Unit\\Event\\ObjectStoppableEventsTest::testUpdatingEventGetNewAndOldObject":0,"Unit\\Event\\ObjectStoppableEventsTest::testUpdatingEventOldObjectNullByDefault":0,"Unit\\Event\\ObjectStoppableEventsTest::testDeletingEventGetObject":0,"Unit\\Event\\RegistrationEventsTest::testToolRegistrationEventExtendsEvent":0,"Unit\\Event\\RegistrationEventsTest::testToolRegistrationEventRegisterToolDelegatesToRegistry":0.001,"Unit\\Event\\RegistrationEventsTest::testDeepLinkRegistrationEventExtendsEvent":0,"Unit\\Event\\RegistrationEventsTest::testDeepLinkRegistrationEventGetRegistry":0,"Unit\\Event\\RegistrationEventsTest::testDeepLinkRegistrationEventRegisterDelegatesToRegistry":0,"Unit\\Event\\RegistrationEventsTest::testDeepLinkRegistrationEventRegisterWithEmptyIcon":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#AgentCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#AgentDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ApplicationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ApplicationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ConfigurationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ConfigurationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ConversationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ConversationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ObjectCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ObjectDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#OrganisationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#OrganisationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#RegisterCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#RegisterDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#SchemaCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#SchemaDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#SourceCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#SourceDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ViewCreatedEvent":0.001,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityExtendsEvent#ViewDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#AgentCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#AgentDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ApplicationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ApplicationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ConfigurationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ConfigurationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ConversationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ConversationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ObjectCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ObjectDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#OrganisationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#OrganisationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#RegisterCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#RegisterDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#SchemaCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#SchemaDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#SourceCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#SourceDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ViewCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityConstructAndGet#ViewDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#AgentCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#AgentDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ApplicationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ApplicationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ConfigurationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ConfigurationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ConversationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ConversationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ObjectCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ObjectDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#OrganisationCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#OrganisationDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#RegisterCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#RegisterDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#SchemaCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#SchemaDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#SourceCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#SourceDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ViewCreatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testSingleEntityGetterReturnsSameInstance#ViewDeletedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventExtendsEvent#AgentUpdatedEvent":0.001,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventExtendsEvent#ApplicationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventExtendsEvent#ConversationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventExtendsEvent#OrganisationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventExtendsEvent#RegisterUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventExtendsEvent#SchemaUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventExtendsEvent#SourceUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetNewEntity#AgentUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetNewEntity#ApplicationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetNewEntity#ConversationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetNewEntity#OrganisationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetNewEntity#RegisterUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetNewEntity#SchemaUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetNewEntity#SourceUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetOldEntity#AgentUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetOldEntity#ApplicationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetOldEntity#ConversationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetOldEntity#OrganisationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetOldEntity#RegisterUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetOldEntity#SchemaUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventGetOldEntity#SourceUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNewAndOldAreDifferentInstances#AgentUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNewAndOldAreDifferentInstances#ApplicationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNewAndOldAreDifferentInstances#ConversationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNewAndOldAreDifferentInstances#OrganisationUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNewAndOldAreDifferentInstances#RegisterUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNewAndOldAreDifferentInstances#SchemaUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNewAndOldAreDifferentInstances#SourceUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNoGettersExtendsEvent#ConfigurationUpdatedEvent":0.001,"Unit\\Event\\SimpleCrudEventsTest::testUpdatedEventNoGettersExtendsEvent#ViewUpdatedEvent":0,"Unit\\Event\\SimpleCrudEventsTest::testObjectUpdatedEventGetObject":0,"Unit\\Event\\SimpleCrudEventsTest::testObjectUpdatedEventOldObjectNullByDefault":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#firstName changed":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#lastName changed":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#middleName changed":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#displayName changed":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#multiple name fields":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#name + other fields":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#only email changed":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#only phone changed":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#no changes":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasNameChanges#non-name fields only":0,"Unit\\Event\\UserProfileUpdatedEventTest::testExtendsEvent":0,"Unit\\Event\\UserProfileUpdatedEventTest::testGetUser":0,"Unit\\Event\\UserProfileUpdatedEventTest::testGetUserId":0,"Unit\\Event\\UserProfileUpdatedEventTest::testGetOldData":0,"Unit\\Event\\UserProfileUpdatedEventTest::testGetNewData":0,"Unit\\Event\\UserProfileUpdatedEventTest::testGetChanges":0,"Unit\\Event\\UserProfileUpdatedEventTest::testGetEmptyData":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasChangedReturnsTrue":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasChangedReturnsFalse":0,"Unit\\Event\\UserProfileUpdatedEventTest::testHasChangedWithEmptyChanges":0,"Unit\\EventListener\\SolrEventListenerTest::testImplementsIEventListener":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleObjectCreatedEvent":0.001,"Unit\\EventListener\\SolrEventListenerTest::testHandleObjectCreatedEventCacheException":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleObjectUpdatedEvent":0.001,"Unit\\EventListener\\SolrEventListenerTest::testHandleObjectUpdatedEventCacheException":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleObjectUpdatedEventNullOldObject":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleObjectDeletedEvent":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleObjectDeletedEventCacheException":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleSchemaCreatedEvent":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleSchemaUpdatedEventFieldsChanged":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleSchemaUpdatedEventFieldsUnchanged":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleSchemaDeletedEvent":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleUnknownEventLogsDebug":0,"Unit\\EventListener\\SolrEventListenerTest::testHandleExceptionLogsErrorAndContinues":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#schema slug duplicate":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#register slug duplicate":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#generic unique duplicate":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#duplicate without unique keyword":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#foreign key constraint":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#foreign key uppercase":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#not null constraint":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#not null uppercase":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#check constraint":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#check constraint uppercase":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#data too long":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#too long lowercase":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#sqlstate error":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseException#unknown error":0,"Unit\\Exception\\ExceptionsTest::testNotAuthorizedExtendsException":0,"Unit\\Exception\\ExceptionsTest::testNotAuthorizedDefaultMessage":0,"Unit\\Exception\\ExceptionsTest::testNotAuthorizedCustomMessage":0,"Unit\\Exception\\ExceptionsTest::testNotAuthorizedCustomCode":0,"Unit\\Exception\\ExceptionsTest::testNotAuthorizedPreviousException":0,"Unit\\Exception\\ExceptionsTest::testLockedExtendsException":0,"Unit\\Exception\\ExceptionsTest::testLockedDefaultMessage":0,"Unit\\Exception\\ExceptionsTest::testLockedCustomMessage":0,"Unit\\Exception\\ExceptionsTest::testLockedPreviousException":0,"Unit\\Exception\\ExceptionsTest::testRegisterNotFoundExtendsException":0,"Unit\\Exception\\ExceptionsTest::testRegisterNotFoundMessage":0,"Unit\\Exception\\ExceptionsTest::testRegisterNotFoundWithId":0,"Unit\\Exception\\ExceptionsTest::testRegisterNotFoundCustomCode":0,"Unit\\Exception\\ExceptionsTest::testRegisterNotFoundPrevious":0,"Unit\\Exception\\ExceptionsTest::testSchemaNotFoundExtendsException":0,"Unit\\Exception\\ExceptionsTest::testSchemaNotFoundMessage":0,"Unit\\Exception\\ExceptionsTest::testSchemaNotFoundPrevious":0,"Unit\\Exception\\ExceptionsTest::testCustomValidationExtendsException":0,"Unit\\Exception\\ExceptionsTest::testCustomValidationGetErrors":0,"Unit\\Exception\\ExceptionsTest::testCustomValidationEmptyErrors":0,"Unit\\Exception\\ExceptionsTest::testValidationExtendsException":0,"Unit\\Exception\\ExceptionsTest::testValidationDefaultErrorsNull":0,"Unit\\Exception\\ExceptionsTest::testValidationCustomCodeAndPrevious":0,"Unit\\Exception\\ExceptionsTest::testAuthenticationExtendsException":0.001,"Unit\\Exception\\ExceptionsTest::testAuthenticationGetDetails":0,"Unit\\Exception\\ExceptionsTest::testAuthenticationEmptyDetails":0,"Unit\\Exception\\ExceptionsTest::testHookStoppedExtendsException":0,"Unit\\Exception\\ExceptionsTest::testHookStoppedDefaultMessage":0,"Unit\\Exception\\ExceptionsTest::testHookStoppedDefaultErrorsEmpty":0,"Unit\\Exception\\ExceptionsTest::testHookStoppedCustomMessageAndErrors":0,"Unit\\Exception\\ExceptionsTest::testHookStoppedCustomCode":0,"Unit\\Exception\\ExceptionsTest::testHookStoppedPreviousException":0,"Unit\\Exception\\ExceptionsTest::testDatabaseConstraintExtendsException":0,"Unit\\Exception\\ExceptionsTest::testDatabaseConstraintDefaultHttpStatus":0,"Unit\\Exception\\ExceptionsTest::testDatabaseConstraintCustomHttpStatus":0,"Unit\\Exception\\ExceptionsTest::testDatabaseConstraintPreviousException":0,"Unit\\Exception\\ExceptionsTest::testFromDatabaseExceptionDefaultEntityType":0,"Unit\\Formats\\BsnFormatTest::testValidBsn#standard 9-digit BSN":0,"Unit\\Formats\\BsnFormatTest::testValidBsn#BSN with leading zeros":0,"Unit\\Formats\\BsnFormatTest::testValidBsn#another valid BSN":0,"Unit\\Formats\\BsnFormatTest::testValidBsn#empty string pads to all zeros (valid checksum)":0,"Unit\\Formats\\BsnFormatTest::testValidBsn#short input padded to valid":0,"Unit\\Formats\\BsnFormatTest::testInvalidBsn#wrong checksum":0,"Unit\\Formats\\BsnFormatTest::testInvalidBsn#non-numeric":0,"Unit\\Formats\\BsnFormatTest::testInvalidBsn#mixed alphanumeric":0,"Unit\\Formats\\BsnFormatTest::testInvalidBsn#single wrong digit":0,"Unit\\Formats\\BsnFormatTest::testInvalidBsn#all ones (invalid checksum)":0,"Unit\\Formats\\BsnFormatTest::testNumericInputCoerced":0,"Unit\\Formats\\BsnFormatTest::testNullCoercedToEmptyString":0,"Unit\\Formats\\BsnFormatTest::testFalseCoercedToEmptyString":0,"Unit\\Formats\\BsnFormatTest::testArrayThrowsTypeError":0,"Unit\\Formats\\BsnFormatTest::testChecksumAlgorithm":0,"Unit\\Listener\\CommentsEntityListenerTest::testEarlyReturnForNonCommentsEntityEvent":0,"Unit\\Listener\\CommentsEntityListenerTest::testRegistersOpenregisterEntityCollection":0.001,"Unit\\Listener\\HookListenerTest::testEarlyReturnForUnrelatedEvent":0,"Unit\\Listener\\HookListenerTest::testEarlyReturnWhenSchemaIdIsNull":0,"Unit\\Listener\\HookListenerTest::testEarlyReturnWhenSchemaHasNoHooks":0,"Unit\\Listener\\HookListenerTest::testExecutesHooksWhenSchemaHasHooks":0,"Unit\\Listener\\HookListenerTest::testSchemaMapperExceptionLogsDebug":0,"Unit\\Listener\\HookListenerTest::testHandlesObjectCreatingEvent":0,"Unit\\Listener\\HookListenerTest::testHandlesObjectUpdatingEvent":0,"Unit\\Listener\\HookListenerTest::testHandlesObjectDeletingEvent":0,"Unit\\Listener\\HookListenerTest::testHandlesObjectUpdatedEvent":0,"Unit\\Listener\\HookListenerTest::testHandlesObjectDeletedEvent":0,"Unit\\Listener\\ObjectChangeListenerTest::testEarlyReturnForUnrelatedEvent":0,"Unit\\Listener\\ObjectChangeListenerTest::testHandlesObjectCreatedEvent":0,"Unit\\Listener\\ObjectChangeListenerTest::testHandlesObjectUpdatedEvent":0,"Unit\\Listener\\ObjectChangeListenerTest::testImmediateExtractionMode":0,"Unit\\Listener\\ObjectChangeListenerTest::testCronModeSkipsProcessing":0,"Unit\\Listener\\ObjectChangeListenerTest::testManualModeSkipsProcessing":0,"Unit\\Listener\\ObjectChangeListenerTest::testNullObjectIdSkipsExtraction":0,"Unit\\Listener\\ObjectChangeListenerTest::testExceptionDuringExtractionLogsError":0,"Unit\\Listener\\ObjectCleanupListenerTest::testEarlyReturnForNonObjectDeletedEvent":0,"Unit\\Listener\\ObjectCleanupListenerTest::testDeletesNotesForObject":0,"Unit\\Listener\\ObjectCleanupListenerTest::testDeletesTasksForObject":0,"Unit\\Listener\\ObjectCleanupListenerTest::testNoteServiceExceptionLogsWarning":0,"Unit\\Listener\\ObjectCleanupListenerTest::testTaskServiceExceptionLogsWarning":0,"Unit\\Listener\\ObjectCleanupListenerTest::testIndividualTaskDeleteFailureLogsWarning":0,"Unit\\Listener\\ToolRegistrationListenerTest::testEarlyReturnForNonToolRegistrationEvent":0,"Unit\\Listener\\ToolRegistrationListenerTest::testRegistersAllFiveTools":0,"Unit\\Listener\\ToolRegistrationListenerTest::testRegistersCorrectToolIds":0,"Unit\\Listener\\WebhookEventListenerTest::testUnknownEventLogsWarningAndReturns":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectLockedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectUnlockedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectRevertedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testRegisterCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testSchemaDeletedEventDispatchesWebhook":0,"Unit\\Repair\\RegisterRiskLevelMetadataTest::testImplementsIRepairStep":0,"Unit\\Repair\\RegisterRiskLevelMetadataTest::testGetNameReturnsDescriptiveString":0,"Unit\\Repair\\RegisterRiskLevelMetadataTest::testRunCallsInitMetadataKey":0,"Unit\\Repair\\RegisterRiskLevelMetadataTest::testRunOutputsInfoMessage":0,"Unit\\Repair\\RegisterRiskLevelMetadataTest::testRunCallsInitBeforeOutput":0,"Unit\\Sections\\OpenRegisterAdminTest::testImplementsIIconSection":0,"Unit\\Sections\\OpenRegisterAdminTest::testGetIdReturnsOpenregister":0,"Unit\\Sections\\OpenRegisterAdminTest::testGetNameUsesTranslation":0,"Unit\\Sections\\OpenRegisterAdminTest::testGetNameReturnsTranslatedString":0,"Unit\\Sections\\OpenRegisterAdminTest::testGetPriorityReturns97":0,"Unit\\Sections\\OpenRegisterAdminTest::testGetIconUsesUrlGenerator":0,"Unit\\Sections\\OpenRegisterAdminTest::testGetIconReturnsUrlGeneratorResult":0,"Unit\\Service\\ApplicationServiceTest::testFindAllReturnsArrayOfApplications":0,"Unit\\Service\\ApplicationServiceTest::testFindAllWithLimitAndOffset":0,"Unit\\Service\\ApplicationServiceTest::testFindAllWithNoResults":0,"Unit\\Service\\ApplicationServiceTest::testFindReturnsApplication":0,"Unit\\Service\\ApplicationServiceTest::testFindThrowsDoesNotExistException":0,"Unit\\Service\\ApplicationServiceTest::testCreateReturnsCreatedApplication":0,"Unit\\Service\\ApplicationServiceTest::testCreateWithEmptyData":0,"Unit\\Service\\ApplicationServiceTest::testUpdateReturnsUpdatedApplication":0,"Unit\\Service\\ApplicationServiceTest::testUpdateThrowsDoesNotExistException":0,"Unit\\Service\\ApplicationServiceTest::testDeleteRemovesApplication":0,"Unit\\Service\\ApplicationServiceTest::testDeleteThrowsDoesNotExistException":0,"Unit\\Service\\ApplicationServiceTest::testCountAllReturnsCount":0,"Unit\\Service\\ApplicationServiceTest::testCountAllReturnsZeroWhenEmpty":0,"Unit\\Service\\AuthenticationServiceTest::testFetchOAuthTokensThrowsWhenGrantTypeMissing":0,"Unit\\Service\\AuthenticationServiceTest::testFetchOAuthTokensThrowsWhenTokenUrlMissing":0,"Unit\\Service\\AuthenticationServiceTest::testFetchOAuthTokensThrowsForUnsupportedGrantType":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenThrowsWhenPayloadMissing":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenThrowsWhenSecretMissing":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenThrowsWhenAlgorithmMissing":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesHs256Token":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesHs384Token":0.001,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesHs512Token":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenThrowsForUnsupportedAlgorithm":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenWithTwigPayloadTemplate":0.001,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenWithX5tHeader":0,"Unit\\Service\\AuthenticationServiceTest::testRequiredParametersClientCredentials":0,"Unit\\Service\\AuthenticationServiceTest::testRequiredParametersPassword":0,"Unit\\Service\\AuthenticationServiceTest::testRequiredParametersJwt":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadSucceedsWithValidToken":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadThrowsWhenMissingIat":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadThrowsWhenExpired":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadUsesDefaultExpiryWhenNoExp":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadDefaultExpiryExpired":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeBasicSucceeds":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeBasicThrowsOnInvalidCredentials":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeOAuthSucceeds":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeOAuthThrowsWithoutBearerPrefix":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeOAuthThrowsWhenNotLoggedIn":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeApiKeySucceeds":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeApiKeyThrowsForInvalidKey":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeApiKeyThrowsWhenUserNotFound":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenNoToken":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenEmptyBearer":0,"Unit\\Service\\AuthorizationServiceTest::testCorsAfterControllerAddsOriginHeader":0.001,"Unit\\Service\\AuthorizationServiceTest::testCorsAfterControllerReturnsResponseWithoutOrigin":0,"Unit\\Service\\AuthorizationServiceTest::testCorsAfterControllerThrowsOnCredentialsTrue":0,"Unit\\Service\\AuthorizationServiceTest::testHmacAlgorithmsConstant":0,"Unit\\Service\\AuthorizationServiceTest::testPkcs1AlgorithmsConstant":0,"Unit\\Service\\AuthorizationServiceTest::testPssAlgorithmsConstant":0,"Unit\\Service\\ChatServiceTest::testProcessMessageSuccess":0.002,"Unit\\Service\\ChatServiceTest::testProcessMessageDeniesAccessToOtherUserConversation":0,"Unit\\Service\\ChatServiceTest::testProcessMessageWithAgentConfigured":0.001,"Unit\\Service\\ChatServiceTest::testProcessMessageGeneratesTitleForNewConversation":0,"Unit\\Service\\ChatServiceTest::testProcessMessageRethrowsExceptions":0,"Unit\\Service\\ChatServiceTest::testGenerateConversationTitleDelegatesToHandler":0,"Unit\\Service\\ChatServiceTest::testEnsureUniqueTitleDelegatesToHandler":0,"Unit\\Service\\ChatServiceTest::testTestChatReturnsSuccess":0,"Unit\\Service\\ChatServiceTest::testTestChatWithCustomMessage":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsWithExactStringMatch":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsFailsOnMismatch":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsWithMultipleConditions":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsFailsWhenOneConditionFails":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsWithEmptyMatch":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsWithMissingProperty":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsWithAtSelfLookup":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsDirectPropertyOverAtSelf":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsWithOperatorArray":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesConditionsWithOperatorArrayFails":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesWithUserIdVariable":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesWithUserVariable":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesWithNullUserReturnsNull":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesWithNullValueConditionAndNullObjectValue":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesWithNullValueConditionAndNonNullObjectValue":0,"Unit\\Service\\ConditionMatcherTest::testFilterOrganisationMatchForCreateRemovesOrgConditions":0,"Unit\\Service\\ConditionMatcherTest::testFilterOrganisationMatchForCreateKeepsNonDynamicOrg":0,"Unit\\Service\\ConditionMatcherTest::testFilterOrganisationMatchForCreateKeepsRegularConditions":0,"Unit\\Service\\ConditionMatcherTest::testFilterOrganisationMatchForCreateWithEmptyMatch":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesWithBooleanValue":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesWithNumericValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\CacheHandlerTest::testGetConfigurationsReturnsEmptyWhenNoOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\CacheHandlerTest::testGetConfigurationsReturnsCachedData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\CacheHandlerTest::testGetConfigurationsFetchesFromDatabaseOnCacheMiss":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeCreate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeSlugLowercased":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testCompareArraysReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testImportConfigurationWithSelectionReturnsEmpty":0,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesProcessesAllObjects":0,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesCountsFailures":0,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesWithFilters":0,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesThrowsOnFindAllError":0,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesProcessesAllLogs":0.001,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesThrowsOnError":0,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesCombinesResults":0,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasReturnsStructuredData":0.001,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasThrowsOnError":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionChartDataReturnsData":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionChartDataReturnsEmptyOnError":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsByRegisterChartDataReturnsEmptyOnError":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySchemaChartDataReturnsEmptyOnError":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySizeChartDataReturnsEmptyOnError":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailStatisticsReturnsData":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailStatisticsReturnsZeroOnError":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionDistributionReturnsEmptyOnError":0,"Unit\\Service\\DashboardServiceTest::testGetMostActiveObjectsReturnsEmptyOnError":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testRegisterAddsRegistration":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testRegisterIgnoresDuplicateKey":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testRegisterWithCustomIcon":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testRegisterWithDefaultIcon":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveReturnsNullWhenNoRegistrations":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveReturnsRegistrationByIds":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveReturnsNullForUnknownIds":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveUrlReturnsNullWhenNoRegistration":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveUrlResolvesTemplate":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveIconReturnsNullWhenNoRegistration":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveIconReturnsIcon":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testHasRegistrationsReturnsFalseWhenEmpty":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testHasRegistrationsReturnsTrueAfterRegister":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResetClearsAllRegistrations":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveHandlesRegisterMapperException":0,"Unit\\Service\\DeepLinkRegistryServiceTest::testResolveHandlesSchemaMapperException":0,"Unit\\Service\\DownloadServiceTest::testCanBeInstantiated":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointDeniedWhenNoUserAndGroupsRequired":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAllowedForAdminUser":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAllowedWhenNoGroupsDefined":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAllowedWhenUserInAllowedGroup":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointDeniedWhenUserNotInGroup":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAllowedForPublicEndpointWithNoUser":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointViewTargetType":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointWebhookTargetType":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointRegisterTargetType":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointSchemaTargetType":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointUnknownTargetType":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointCatchesGroupManagerException":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointLogsCallSuccessfully":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointLoggingErrorDoesNotBreakExecution":0,"Unit\\Service\\ExportServiceTest::testExportToExcelWithSingleSchema":0,"Unit\\Service\\ExportServiceTest::testExportToExcelWithRegisterExportsAllSchemas":0,"Unit\\Service\\ExportServiceTest::testExportToExcelWithObjectData":0.001,"Unit\\Service\\ExportServiceTest::testExportToExcelSkipsHiddenOnCollectionProperties":0,"Unit\\Service\\ExportServiceTest::testExportToExcelSkipsRbacRestrictedProperties":0,"Unit\\Service\\ExportServiceTest::testExportToExcelAddsMetadataColumnsForAdmin":0.001,"Unit\\Service\\ExportServiceTest::testExportToExcelNoMetadataForNonAdmin":0,"Unit\\Service\\ExportServiceTest::testExportToCsvThrowsForMultipleSchemas":0,"Unit\\Service\\ExportServiceTest::testExportToCsvReturnsCsvString":0.003,"Unit\\Service\\ExportServiceTest::testExportToExcelAddsCompanionColumnsForUuidRelations":0.001,"Unit\\Service\\ExportServiceTest::testExportToExcelAddsCompanionColumnsForRefRelations":0,"Unit\\Service\\ExportServiceTest::testExportToExcelAddsCompanionColumnsForArrayOfUuids":0,"Unit\\Service\\ExportServiceTest::testExportToExcelResolvesUuidNames":0.001,"Unit\\Service\\ExportServiceTest::testExportHandlesNullValuesGracefully":0,"Unit\\Service\\ExportServiceTest::testExportHandlesArrayValuesAsJson":0,"Unit\\Service\\ExportServiceTest::testExportSkipsBuiltInHeaderFields":0,"Unit\\Service\\ExportServiceTest::testExportToExcelNoMetadataForAnonymousUser":0,"Unit\\Service\\ExportServiceTest::testExportToCsvWithDataRows":0.001,"Unit\\Service\\ExportServiceTest::testExportToCsvSkipsHiddenOnCollectionProperties":0,"Unit\\Service\\ExportServiceTest::testExportToExcelHandlesBooleanValues":0,"Unit\\Service\\ExportServiceTest::testExportToExcelHandlesNestedObjectValues":0,"Unit\\Service\\ExportServiceTest::testExportToExcelWithEmptySchemaProperties":0,"Unit\\Service\\ExportServiceTest::testExportToCsvWithRbacRestrictions":0,"Unit\\Service\\ExportServiceTest::testExportToExcelWithFilters":0,"Unit\\Service\\ExportServiceTest::testExportToCsvWithMultipleObjectsVerifyRowCount":0.001,"Unit\\Service\\ExportServiceTest::testExportToExcelWithArrayOfUuidsResolvesNames":0.001,"Unit\\Service\\ExportServiceTest::testExportToCsvHandlesSpecialCharsInValues":0.001,"Unit\\Service\\ExportServiceTest::testExportToExcelWithRegisterAndSchemaOverrideUsesSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#exe":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#bat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#sh":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#php":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#py":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#jar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#dll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#ps1":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#bash":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#msi":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#apk":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#deb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#pdf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#jpg":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#png":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#docx":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#xlsx":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#txt":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#csv":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#json":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#xml":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileAllowsSafeExtensions#zip":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesWindowsExe":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesElfExecutable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShellScript":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesBashScript":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesPhpScript":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesJavaClass":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesSafeContent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesSignatureNotAtStart":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileChecksContentAfterExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileEmptyContentAllowed":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksReturnsEarlyForUnknownEventType":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksReturnsEarlyWhenNoHooksMatch":0.001,"Unit\\Service\\HookExecutorTest::testExecuteHooksSkipsDisabledHooks":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksCallsEngineForMatchingHook":0.001,"Unit\\Service\\HookExecutorTest::testExecuteHooksStopsOnPropagationStopped":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksRejectsWhenNoEngineFound":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksHandlesExceptionWithTimeoutFailureMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksRejectedResult":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksModifiedResult":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithFilterConditionNotMet":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithFilterConditionMet":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksQueueFailureMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksSortsHooksByOrder":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithUpdatingEvent":0.001,"Unit\\Service\\HookExecutorTest::testExecuteHooksAsyncMode":0.001,"Unit\\Service\\HookExecutorTest::testExecuteHooksConnectionRefusedUsesEngineDownMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithDeletingEvent":0.001,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithCreatedEvent":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithUpdatedEvent":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithDeletedEvent":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithEmptyHooksArray":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithMultipleMatchingHooks":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithFilterConditionArrayNotMatching":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksEngineDownRejectMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksHookWithNoMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksFlagFailureMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksAllowFailureMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksAsyncFailure":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksErrorStatusResult":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithUnreachableEngineAllowMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithNullHooksArray":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksWithDeletingEventFilterCondition":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testClearCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeSmall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeMedium":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeLarge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeBoundary1000":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeBoundary1001":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeBoundary10000":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetRecommendedWarmupModeBoundary10001":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithNoImportedObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupEmptySummary":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithCustomDelay":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithMultipleSheets":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSolrWarmupWithNonArrayEntry":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupWithData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupImmediate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupUsesRecommendedMode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testScheduleSmartSolrWarmupCapsMaxObjects":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithInvalidPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvRequiresSchema":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithValidFile":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithPublishEnabled":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithEmptyDataRows":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithTypedProperties":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithUnchangedObjects":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithUnderscoreColumnsSkipped":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithAtColumnsSkippedForNonAdmin":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithAtColumnsProcessedForAdmin":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithValidationErrors":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvPerformanceMetrics":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithInvalidPath":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValueMySqlFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValueIso8601WithTimezone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValueIso8601WithoutTimezone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValueDateOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformDateTimeValuePassThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyPublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyCreated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyOrganisationValidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyOrganisationInvalidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyOther":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeBooleanAlreadyBool":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeArrayQuotedValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeArrayNonStringNonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeObjectRelatedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeDefaultString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testAddPublishedDateToObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testIsUserAdminWithNullUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testIsUserAdminWithNoAdminGroup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testIsUserAdminTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testIsUserAdminFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testValidateObjectProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformObjectBySchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImported":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImportedEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImportedWithNonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testIsSolrConfiguredReturnsFalseWhenDisabled":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testIsSolrConfiguredReturnsFalseWhenMissingHost":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testIsSolrConfiguredReturnsFalseWhenMissingCore":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testIsSolrConfiguredReturnsTrueWhenValid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testBuildSolrBaseUrlWithPort":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testBuildSolrBaseUrlWithoutPort":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testBuildSolrBaseUrlWithPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testBuildSolrBaseUrlPortZeroIgnored":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testBuildSolrBaseUrlEmptyPortStringIgnored":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetEndpointUrlDefaultCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetEndpointUrlCustomCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetTenantSpecificCollectionNameReturnsBaseNameAsIs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetConfigStatusConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetConfigStatusNotConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetPortStatusDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetPortStatusCustom":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetCoreStatusDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testGetCoreStatusCustom":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\ConfigurationHandlerTest::testInitializationHandlesSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenRelationsForSolrEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenRelationsForSolrStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenRelationsForSolrNumericValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenRelationsForSolrSkipsArrayValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenRelationsForSolrSingleStringValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenRelationsForSolrSingleNumericValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenFilesForSolrEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenFilesForSolrStringArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenFilesForSolrObjectsWithId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenFilesForSolrSingleString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIdFromObjectWithId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIdFromObjectWithUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIdFromObjectWithIdentifier":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIdFromObjectReturnsNullWhenNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIdFromObjectPrefersIdOverUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractArraysFromRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractArraysFromRelationsSkipsNonDotNotation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIndexableArrayValuesStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIndexableArrayValuesObjectsWithId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIndexableArrayValuesScalars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractIndexableArrayValuesSkipsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testMapFieldToSolrTypeReservedFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testMapFieldToSolrTypeSelfPrefixSkipped":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testMapFieldToSolrTypeNormalField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testTruncateFieldValueShortString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testTruncateFieldValueNonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testTruncateFieldValueLongString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testShouldTruncateFieldFileType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testShouldTruncateFieldLargeContentFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testShouldTruncateFieldBase64InName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testShouldNotTruncateRegularField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testValidateFieldForSolrNoFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testValidateFieldForSolrUnknownField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testValidateFieldForSolrCompatibleType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testValidateFieldForSolrIncompatibleType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testIsValueCompatibleWithSolrTypeNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testIsValueCompatibleWithSolrTypeNumeric":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testIsValueCompatibleWithSolrTypeString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testIsValueCompatibleWithSolrTypeBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testIsValueCompatibleWithSolrTypeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveRegisterToIdEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveRegisterToIdNumeric":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveSchemaToIdEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveSchemaToIdNumeric":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveRegisterToIdWithEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveSchemaToIdWithEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testCreateDocument":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testCreateDocumentSkipsNullValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeAlreadyExists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeCreatesNew":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testGetCollectionFieldStatusAllPresent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testGetCollectionFieldStatusWithMissingFields":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testGetCollectionFieldStatusHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testCreateMissingFieldsDryRun":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testFixMismatchedFieldsDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testFixMismatchedFieldsHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaMapperTest::testMapToBackendSchemaReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaMapperTest::testMapFieldTypeReturnsInputAsIs":0,"Unit\\Service\\LogServiceTest::testGetLogsReturnsAuditTrails":0,"Unit\\Service\\LogServiceTest::testGetLogsWithConfig":0,"Unit\\Service\\LogServiceTest::testGetLogsAllowsAccessWhenRegisterSchemaDeleted":0,"Unit\\Service\\LogServiceTest::testCountReturnsLogCount":0,"Unit\\Service\\LogServiceTest::testCountReturnsZeroWhenNoLogs":0,"Unit\\Service\\LogServiceTest::testGetAllLogsWithDefaults":0,"Unit\\Service\\LogServiceTest::testGetAllLogsWithConfig":0,"Unit\\Service\\LogServiceTest::testCountAllLogs":0,"Unit\\Service\\LogServiceTest::testCountAllLogsWithFilters":0,"Unit\\Service\\LogServiceTest::testGetLog":0,"Unit\\Service\\LogServiceTest::testDeleteLogSuccess":0,"Unit\\Service\\LogServiceTest::testDeleteLogThrowsOnFailure":0,"Unit\\Service\\LogServiceTest::testDeleteLogsByIds":0,"Unit\\Service\\LogServiceTest::testDeleteLogsByIdsWithFailures":0,"Unit\\Service\\LogServiceTest::testDeleteLogsByFilters":0,"Unit\\Service\\LogServiceTest::testExportLogsJson":0,"Unit\\Service\\LogServiceTest::testExportLogsCsv":0,"Unit\\Service\\LogServiceTest::testExportLogsXml":0,"Unit\\Service\\LogServiceTest::testExportLogsTxt":0,"Unit\\Service\\LogServiceTest::testExportLogsUnsupportedFormat":0,"Unit\\Service\\LogServiceTest::testExportLogsCsvEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#disabled_in_schema_global_enabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testIsMagicMappingEnabled#disabled_in_schema_global_disabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicMapperTest::testRegisterSchemaVersionCalculation":0,"Unit\\Service\\MappingServiceTest::testEncodeArrayKeysSimple":0,"Unit\\Service\\MappingServiceTest::testEncodeArrayKeysNested":0,"Unit\\Service\\MappingServiceTest::testEncodeArrayKeysEmptyArray":0,"Unit\\Service\\MappingServiceTest::testEncodeArrayKeysNoMatchingCharacters":0,"Unit\\Service\\MappingServiceTest::testCoordinateStringToArraySinglePoint":0,"Unit\\Service\\MappingServiceTest::testCoordinateStringToArrayMultiplePoints":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingSimpleDotNotation":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithPassThrough":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithoutPassThrough":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithUnset":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithCastToInt":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithCastToBool":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithCastToString":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithCastToFloat":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingListMode":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingRootLevelHash":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingArrayValue":0,"Unit\\Service\\MappingServiceTest::testGetMappingFromCache":0,"Unit\\Service\\MappingServiceTest::testGetMappingFromDatabase":0,"Unit\\Service\\MappingServiceTest::testGetMappings":0,"Unit\\Service\\MappingServiceTest::testInvalidateMappingCache":0,"Unit\\Service\\MappingServiceTest::testInvalidateMappingCacheWithStringId":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithNullableBoolCastNull":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithNullableBoolCastEmpty":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithBase64Cast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithBase64DecodeCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithJsonCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithNullStringToNullCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithUrlEncodeCast":0,"Unit\\Service\\MappingServiceTest::testConstructorHandlesCacheFactoryFailure":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithCastToArray":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithUrlDecodeCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithHtmlCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithHtmlDecodeCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithJsonToArrayCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithJsonToArrayCastAlreadyArray":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithMoneyStringToIntCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithIntToMoneyStringCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithNestedDotNotation":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithNestedOutputDotNotation":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithRootHashResolvesToScalar":0,"Unit\\Service\\MappingServiceTest::testCoordinateStringToArraySingleNumber":0,"Unit\\Service\\MappingServiceTest::testEncodeArrayKeysDeepNesting":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithUnsetOnNestedKey":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingListModeWithExtraValues":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingDefaultCastReturnsValue":0,"Unit\\Service\\MappingServiceTest::testGetMappingCacheNull":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithCastToDatetime":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithEmptyInput":0.001,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithMultipleCasts":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithMultipleUnsets":0,"Unit\\Service\\MappingServiceTest::testGetMappingsReturnsEmptyWhenNone":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithNullableCastNonNull":0,"Unit\\Service\\MappingServiceTest::testCoordinateStringToArrayEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpProtocolServiceTest::testInitializeReturnsServerInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpProtocolServiceTest::testInitializeCapabilities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpProtocolServiceTest::testPingReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpProtocolServiceTest::testCreateSessionReturnsSessionId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpProtocolServiceTest::testValidateSessionWithValidSession":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpProtocolServiceTest::testValidateSessionWithInvalidSession":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListToolsReturnsThreeTools":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListToolsContainsExpectedToolNames":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListToolsHaveRequiredProperties":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCallToolUnknownToolReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCallToolErrorHasContent":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCatalogReturnsCorrectStructure":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCatalogHasAllCapabilities":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCatalogCapabilitiesHaveHref":0.001,"Unit\\Service\\McpDiscoveryServiceTest::testGetCatalogAuthenticationInfo":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityIds":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailRegisters":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailSchemas":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailObjects":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailSearch":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailFiles":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailAudit":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailBulk":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailWebhooks":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailChat":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailViews":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailUnknown":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailRegistersEmpty":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailSchemasEmpty":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailSchemasWithNullProperties":0,"Unit\\Service\\McpDiscoveryServiceTest::testGetCapabilityDetailEndpointsHaveRequiredFields":0,"Unit\\Service\\MetricsServiceTest::testRecordMetricSuccess":0.001,"Unit\\Service\\MetricsServiceTest::testRecordMetricDbFailureDoesNotThrow":0.001,"Unit\\Service\\MetricsServiceTest::testRecordMetricWithNullMetadata":0.001,"Unit\\Service\\MetricsServiceTest::testGetFilesProcessedPerDay":0.001,"Unit\\Service\\MetricsServiceTest::testGetFilesProcessedPerDayEmpty":0,"Unit\\Service\\MetricsServiceTest::testGetEmbeddingStats":0.001,"Unit\\Service\\MetricsServiceTest::testGetEmbeddingStatsZeroTotal":0,"Unit\\Service\\MetricsServiceTest::testGetSearchLatencyStats":0.001,"Unit\\Service\\MetricsServiceTest::testGetSearchLatencyStatsNullAvg":0.001,"Unit\\Service\\MetricsServiceTest::testGetStorageGrowth":0.001,"Unit\\Service\\MetricsServiceTest::testGetStorageGrowthEmpty":0.001,"Unit\\Service\\MetricsServiceTest::testGetDashboardMetrics":0.001,"Unit\\Service\\MetricsServiceTest::testCleanOldMetricsReturnsInt":0,"Unit\\Service\\MetricsServiceTest::testCleanOldMetricsDefaultRetention":0,"Unit\\Service\\MetricsServiceTest::testMetricConstants":0,"Unit\\Service\\MigrationServiceTest::testResolveRegisterAndSchema":0,"Unit\\Service\\MigrationServiceTest::testResolveRegisterAndSchemaWithSlugs":0,"Unit\\Service\\MigrationServiceTest::testGetStorageStatusReturnsStatusArray":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableEmptySource":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableDryRun":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableSkipsDuplicates":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableHandlesFailure":0,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageNoMagicTable":0,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageEmptySource":0.001,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageDryRun":0.001,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageSkipsDuplicates":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableEnsuresTable":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableDryRunDoesNotEnsureTable":0,"Unit\\Service\\NoteServiceTest::testGetNotesForObjectReturnsNotes":0.002,"Unit\\Service\\NoteServiceTest::testGetNotesForObjectEmpty":0,"Unit\\Service\\NoteServiceTest::testGetNotesForObjectWithLimitAndOffset":0,"Unit\\Service\\NoteServiceTest::testGetNotesForObjectIsCurrentUserFalse":0,"Unit\\Service\\NoteServiceTest::testGetNotesForObjectNoCurrentUser":0,"Unit\\Service\\NoteServiceTest::testCreateNoteSuccess":0,"Unit\\Service\\NoteServiceTest::testCreateNoteThrowsWhenNoUser":0,"Unit\\Service\\NoteServiceTest::testDeleteNoteSuccess":0,"Unit\\Service\\NoteServiceTest::testDeleteNoteNotFound":0,"Unit\\Service\\NoteServiceTest::testDeleteNotesForObject":0,"Unit\\Service\\NoteServiceTest::testGetNotesForObjectMultiple":0,"Unit\\Service\\NoteServiceTest::testCreateNoteCallsCreateWithCorrectParams":0,"Unit\\Service\\NotificationServiceTest::testNotifyConfigurationUpdateSendsToAdminGroup":0,"Unit\\Service\\NotificationServiceTest::testNotifyConfigurationUpdateAlwaysIncludesAdmin":0,"Unit\\Service\\NotificationServiceTest::testNotifyConfigurationUpdateDeduplicatesUsers":0,"Unit\\Service\\NotificationServiceTest::testNotifyConfigurationUpdateSkipsNonexistentGroup":0,"Unit\\Service\\NotificationServiceTest::testNotifyConfigurationUpdateHandlesNotificationFailure":0,"Unit\\Service\\NotificationServiceTest::testNotifyConfigurationUpdateNoGroups":0,"Unit\\Service\\NotificationServiceTest::testMarkConfigurationUpdated":0,"Unit\\Service\\NotificationServiceTest::testNotifyConfigurationUpdateMultipleGroups":0,"Unit\\Service\\OasServiceTest::testCreateOasReturnsValidStructure":0.004,"Unit\\Service\\OasServiceTest::testCreateOasForSpecificRegister":0.001,"Unit\\Service\\OasServiceTest::testCreateOasForRegisterWithNoDescription":0.001,"Unit\\Service\\OasServiceTest::testCreateOasWithEmptySchemas":0.001,"Unit\\Service\\OasServiceTest::testCreateOasServersUrl":0,"Unit\\Service\\OasServiceTest::testCreateOasMultipleRegistersDeduplicateSchemas":0.005,"Unit\\Service\\OasServiceTest::testCreateOasSkipsSchemaWithEmptyTitle":0.003,"Unit\\Service\\OasServiceTest::testCreateOasSchemaProperties":0.002,"Unit\\Service\\OasServiceTest::testCreateOasAllRegisters":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsDelegatesToSaveObjectsHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsSkipsCacheInvalidationWhenNoObjectsAffected":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsCacheInvalidationFailureDoesNotBreakOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testSaveObjectsWithRegisterAndSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsReturnsEmptyArrayForEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsWithRbacFiltering":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsWithoutRbacOrMultitenancy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsSkipsCacheWhenNothingDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsCacheFailureDoesNotBreak":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsReturnsEmptyArrayForEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsWithPermissionFiltering":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsWithDatetime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsSkipsCacheWhenNonePublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDepublishObjectsReturnsEmptyArrayForEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDepublishObjectsWithFiltering":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDepublishObjectsCacheFailureDoesNotBreak":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsBySchemaWithResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testPublishObjectsBySchemaSkipsCacheWhenNonePublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsBySchemaUsesBlobStorageWhenNoMagicMapping":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsBySchemaThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsByRegisterWithResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsByRegisterSkipsCacheWhenNoneDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\BulkOperationsHandlerTest::testDeleteObjectsByRegisterCacheFailureDoesNotBreak":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetObjectReturnsCachedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetObjectReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetObjectByUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetObjectWithStringId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsReturnsEmptyForEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsBulkLoadsFromDatabase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsSkipsAlreadyCachedObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsReturnsEmptyOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsWithDuplicateIdentifiers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsUpdatesStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPreloadObjectsMixedCachedAndUncached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsReturnsInitialStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsTracksHitsAndMisses":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsTracksNameHitsAndMisses":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsCacheSizeReflectsLoadedObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsNameCacheSizeReflectsSetNames":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsQueryCacheSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsNameHitRate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCacheClearsAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCacheWithPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCacheHandlesDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeWithObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeWithNullObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeOnDelete":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeOnUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeRemovesObjectFromCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteRemovesNameFromDistributedCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteHandlesDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateUpdatesNameCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeWithNullSchemaAndRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeWithExplicitRegisterAndSchemaIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCachesResetsStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCachesClearsNameCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCachesHandlesDistributedQueryCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearAllCachesHandlesDistributedNameCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetAndGetObjectName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameReturnsNullForUnknownWhenDbFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFromDistributedCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameHandlesDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFindsOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFindsObjectViaFindAcrossAllSources":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameUsesUuidWhenNameIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetObjectNameWithIntIdentifier":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetObjectNameEnforcesMaxTtl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetObjectNameHandlesDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesReturnsEmptyForEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesReturnsCachedNames":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesChecksDistributedCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesFallsBackToOrganisationMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesFallsBackToObjectMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesHandlesDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesFiltersToUuidOnlyResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesHandlesDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetAllObjectNamesTriggersWarmupWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetAllObjectNamesWithForceWarmup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetAllObjectNamesSkipsWarmupWhenCachePopulated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetAllObjectNamesFiltersToUuidKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheLoadsOrganisationsAndObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheOrganisationsTakePriority":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheUpdatesStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheSkipsObjectsWithNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearNameCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearNameCacheClearsDistributedCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearNameCacheHandlesDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testConstructorWithoutCacheFactory":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testConstructorWithCacheFactoryException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetDistributedNameCacheCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetDistributedNameCacheCountReturnsZeroWhenNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetDistributedNameCacheCountHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetDistributedNameCacheCountWithoutDistributedCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSolrDashboardStatsThrowsWithoutContainer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCommitSolrReturnsErrorWithoutContainer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testOptimizeSolrReturnsErrorWithoutContainer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSolrIndexForDashboardReturnsErrorWithoutContainer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCommitSolrSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCommitSolrFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCommitSolrException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testOptimizeSolrSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testOptimizeSolrFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testOptimizeSolrException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSolrIndexForDashboardSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSolrDashboardStatsWithService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetIndexServiceReturnsNullOnContainerException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGetValueFromPathSimpleKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGetValueFromPathNestedKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGetValueFromPathReturnsNullForMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGetValueFromPathDeepMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGetValueFromPathEmptyData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGetValueFromPathReturnsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testCreateSlugHelperBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testCreateSlugHelperSpecialCharacters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testCreateSlugHelperUnicode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testCreateSlugHelperTrimsHyphens":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testCreateSlugHelperEmptyReturnsObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testCreateSlugHelperLowercase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGenerateSlugFromValueReturnsNullForEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGenerateSlugFromValueContainsTimestamp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\MetadataHandlerTest::testGenerateSlugFromValueUniquePerCall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testSetUltraPreloadCacheAndGetSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testSetUltraPreloadCacheReplacesExistingCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testClearCacheResetsAllInternalCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectsCacheReturnsEmptyByDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityReturnsEntityWithNoFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithFieldFiltering":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithFilterMatching":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithFilterNotMatching":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithUnsetProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityDetectsCircularReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithPreloadedRegistersAndSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithStringExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithMultipleEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetRegisterFromCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetRegisterFromDb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetRegisterNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaFromDb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaFromCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsUuidLikeValidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsUuidLikeUpperCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsUuidLikeInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectFromUltraPreloadCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectFromObjectCacheService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectsCacheReturnsOnlyUuidKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectsCacheWithArrayEntry":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testClearCacheResetsRegistersAndSchemasAndObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigDirectFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigArrayOfFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigArrayOfStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigNoType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathSimple":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathDeepNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathPartialNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendSingleUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendArrayOfUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendSkipsSpecialKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendMissingProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendSkipsNonUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendDeduplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesWithInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesWithItemsInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesMatchSpecific":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesMatchAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigWithItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigWithDirectRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMissingRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMissingInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMultipleInversedByFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveReferencedUuidsSimpleString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveReferencedUuidsObjectWithValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveReferencedUuidsArrayOfUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveReferencedUuidsMissingField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testInitializeInverseCacheEntries":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityNormalizesExtendShorthands":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithExtendAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendSelfRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRespectsDepthLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithPropertyRbac":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithMultipleUnset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessUploadedFilesWithValidFile":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessUploadedFilesSkipsUploadErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessUploadedFilesHandlesArrayFieldNames":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessUploadedFilesThrowsOnReadFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithSchemaFileType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithSchemaArrayFileType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithSchemaStringType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithSchemaPropertyNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithUrlAndFileExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithRegularWebUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithBase64String":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithShortString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithArrayOfDataUris":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithNonFileArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFileObjectWithValidFileObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFileObjectWithoutId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFileObjectWithoutTitleOrPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFileObjectWithMinimalFileObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataWithDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataWithPlainBase64":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataThrowsOnInvalidDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataThrowsOnInvalidBase64":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataWithImageDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigPassesForValidFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigRejectsInvalidMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigRejectsOversizedFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigWithArrayIndex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigPassesWithNoRestrictions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksExeExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksBatExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksExecutableMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesAllowsSafeFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesAllowsImages":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsReturnsEmptyResultForEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsMixedSchemaAllInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsMixedSchemaWithValidObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCreateEmptyResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartDoesNotLogSmallSingleSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartLogsLargeSingleSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartLogsMixedSchemaAboveThreshold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLogBulkOperationStartDoesNotLogSmallMixedSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testInitializeResultWithNoInvalidObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testInitializeResultWithInvalidObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testMergeChunkResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testMergeChunkResultMultipleChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsWithUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculatePerformanceMetricsZeroProcessed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeSmall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeMedium":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeLarge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeVeryLarge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeUltraLarge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeHuge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary100":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary1000":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsNoDuplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsWithDuplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsUsesUuidField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsUsesSelfId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsWithoutId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testDeduplicateBatchObjectsMixed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLoadSchemaWithCacheLoadsFromDb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLoadRegisterWithCacheLoadsFromDb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testGetSchemaAnalysisWithCacheCachesResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSkipsPlainText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithSchemaObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithArrayOfObjectsInSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithUrlValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithSchemaTextUuidFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithPrefixedUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithPlainText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithCommonWord":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithIdLikeString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsCommonTextWordMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsCommonTextWordNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsWithDeduplicationEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsWithDeduplicationDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testPrepareObjectsForSaveSingleSchemaPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsWithEmptyAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithEmptySchemaObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithSimpleStringProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithRequiredFieldMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithInvalidType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithEnumProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithNestedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithArrayProperty":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithMinItemsConstraint":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectRemovesExtendAndFiltersFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithNoProperties":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectNullAllowedForOptionalFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageWithValidResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageWithInvalidResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testHandleValidationExceptionReturnsJsonResponse":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testHandleCustomValidationExceptionReturnsJsonResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithSchemaEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidationErrorMessageConstant":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithEmptyRequiredArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithMultipleTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationWithSelfReferenceRelatedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationSelfReferenceInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationSelfReferenceArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationRemovesDollarId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationRemovesMetadataProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationWithItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationNonObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationWithNestedProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeDatetime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeArrayOfTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeNoType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeStandardType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsNonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsMoveEnumToItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsDoesNotOverrideExistingEnum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsMoveOneOfToItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceFalseDifferentSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithObjectRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithArrayRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithQueryParameters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testRemoveQueryParametersNoParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testRemoveQueryParametersWithParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueFromArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueNullData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetMixedValueScalarData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingDirect":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingFromItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingFromOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsNotIterable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsWithMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformOpenRegisterObjectConfigurationsNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToUuidPropertyNoInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToUuidPropertyWithInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertyNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForRequiredSingleField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForRequiredMultipleFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForTypeError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForEnumError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageValidResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectFiltersEmptyStringsForOptionalFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectKeepsEmptyStringForRequiredField":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithFileType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithDatetimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithMetadataProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithBooleanProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithNumberProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesSkipsUuidTransformed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsNonObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectWithRelatedObjectHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectWithNestedObjectHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyStripsRefFromStringType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsEmptyConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsWithDuplicateStringConfigThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsNoDuplicate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithRefCircular":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithArrayItemsRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithNestedProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithoutUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testExtractUuidAndSelfDataWithSelfMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testResolveSchemaAndRegisterWithIntegerIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectRefactoredMethodsTest::testClearImageMetadataIfFilePropertyNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCreateEmptyResultInitializesStatistics":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartSmallOperationNoLog":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartLargeOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testLogBulkOperationStartLargeMixedSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testInitializeResultNoInvalidObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testMergeChunkResultMergesSaved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testMergeChunkResultMergesErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testValidateObjectIfRequiredCallsValidatorWhenEnabled":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testPrepareFindAllConfigPreservesExistingValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceRefactoredMethodsTest::testPrepareFindAllConfigConvertsExtendStringToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountSearchObjectsDelegatesToMapperWithOrgContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountSearchObjectsSkipsOrgWhenMultitenancyDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedUsesDatabaseByDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedSetsRegisterSchemaContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedForcesDbWhenIdsProvided":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedAddsExtendedObjectsWhenExtendSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPublishObjectsDelegatesToBulkOps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDepublishObjectsDelegatesToBulkOps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPublishObjectsBySchemaDelegatesToBulkOps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectsBySchemaDelegatesToBulkOps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectsByRegisterDelegatesToBulkOps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testListObjectsDelegatesToSearchObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCreateObjectCallsSaveObjectInternally":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testBuildObjectSearchQueryDelegatesToBuildSearchQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testExportObjectsThrowsDisabledException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testImportObjectsThrowsDisabledException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDownloadObjectFilesThrowsDisabledException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testVectorizeBatchObjectsThrowsDisabledException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetVectorizationStatisticsThrowsDisabledException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetVectorizationCountThrowsDisabledException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testMergeObjectsDelegatesToMergeHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testMigrateObjectsDelegatesToMigrationHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateObjectsBySchemaDelegatesToValidationHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateAndSaveObjectsBySchemaDelegatesToValidationHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectContractsDelegatesToRelationHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectUsesDelegatesToRelationHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectUsedByDelegatesToRelationHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testHandleValidationExceptionDelegatesToValidateHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetDeleteHandlerReturnsInjectedInstance":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectNamesForResultsReturnsEmptyForEmptyResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectNamesForResultsSkipsNonArrayResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsTrueForValid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testIsUuidFormatReturnsFalseForInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromRelationsCollectsDirectUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromRelationsCollectsNestedUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromObjectDataCollectsTopLevel":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromObjectDataStopsAtDepth1":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromObjectDataCollectsFromArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromArrayResultHandlesSelfStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCollectUuidsFromArrayResultHandlesFlatArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSaveObjectsSetsRegisterSchemaContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testDeleteObjectsDelegatesToBulkOps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderExistsCreatesFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderExistsHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetObjectReturnsSetObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedHandlesExtendCommaString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedExplicitDatabaseSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedForcesDbWhenUsesProvided":0,"Unit\\Service\\OperatorEvaluatorTest::testEqMatchesStrictly":0,"Unit\\Service\\OperatorEvaluatorTest::testEqRejectsLooseMatch":0,"Unit\\Service\\OperatorEvaluatorTest::testEqRejectsDifferentValue":0,"Unit\\Service\\OperatorEvaluatorTest::testNeRejectsSameValue":0,"Unit\\Service\\OperatorEvaluatorTest::testNeMatchesDifferentValue":0,"Unit\\Service\\OperatorEvaluatorTest::testInMatchesValueInArray":0,"Unit\\Service\\OperatorEvaluatorTest::testInRejectsValueNotInArray":0,"Unit\\Service\\OperatorEvaluatorTest::testInReturnsFalseForNonArrayOperand":0,"Unit\\Service\\OperatorEvaluatorTest::testInUsesStrictComparison":0,"Unit\\Service\\OperatorEvaluatorTest::testNinMatchesValueNotInArray":0,"Unit\\Service\\OperatorEvaluatorTest::testNinRejectsValueInArray":0,"Unit\\Service\\OperatorEvaluatorTest::testNinReturnsTrueForNonArrayOperand":0,"Unit\\Service\\OperatorEvaluatorTest::testExistsTrueMatchesNonNull":0,"Unit\\Service\\OperatorEvaluatorTest::testExistsTrueRejectsNull":0,"Unit\\Service\\OperatorEvaluatorTest::testExistsFalseMatchesNull":0,"Unit\\Service\\OperatorEvaluatorTest::testExistsFalseRejectsNonNull":0,"Unit\\Service\\OperatorEvaluatorTest::testGtMatches":0,"Unit\\Service\\OperatorEvaluatorTest::testGtRejectsEqual":0,"Unit\\Service\\OperatorEvaluatorTest::testGteMatchesEqual":0,"Unit\\Service\\OperatorEvaluatorTest::testGteMatchesGreater":0,"Unit\\Service\\OperatorEvaluatorTest::testLtMatches":0,"Unit\\Service\\OperatorEvaluatorTest::testLtRejectsEqual":0,"Unit\\Service\\OperatorEvaluatorTest::testLteMatchesEqual":0,"Unit\\Service\\OperatorEvaluatorTest::testLteMatchesLess":0,"Unit\\Service\\OperatorEvaluatorTest::testMultipleOperatorsMustAllPass":0,"Unit\\Service\\OperatorEvaluatorTest::testMultipleOperatorsFailIfOneDoesNot":0,"Unit\\Service\\OperatorEvaluatorTest::testUnknownOperatorReturnsTrueAndLogsWarning":0,"Unit\\Service\\OperatorEvaluatorTest::testEmptyOperatorsReturnsTrue":0,"Unit\\Service\\PropertyRbacHandlerTest::testIsAdminReturnsTrueForAdminUser":0,"Unit\\Service\\PropertyRbacHandlerTest::testIsAdminReturnsFalseForRegularUser":0,"Unit\\Service\\PropertyRbacHandlerTest::testIsAdminReturnsFalseWhenNoUser":0,"Unit\\Service\\PropertyRbacHandlerTest::testFilterReadablePropertiesReturnsAllForAdmin":0,"Unit\\Service\\PropertyRbacHandlerTest::testFilterReadablePropertiesReturnsAllWhenNoPropertyAuth":0,"Unit\\Service\\PropertyRbacHandlerTest::testCanReadPropertyReturnsTrueWhenNoAuthorizationDefined":0,"Unit\\Service\\PropertyRbacHandlerTest::testCanReadPropertyAllowsPublicGroup":0,"Unit\\Service\\PropertyRbacHandlerTest::testCanReadPropertyAllowsAuthenticatedUserForAuthenticatedGroup":0,"Unit\\Service\\PropertyRbacHandlerTest::testCanReadPropertyDeniedWhenUserNotInGroup":0,"Unit\\Service\\PropertyRbacHandlerTest::testGetUnauthorizedPropertiesReturnsEmptyForAdmin":0,"Unit\\Service\\PropertyRbacHandlerTest::testGetUnauthorizedPropertiesReturnsEmptyWhenNoPropertyAuth":0,"Unit\\Service\\PropertyRbacHandlerTest::testGetUnauthorizedPropertiesSkipsUnchangedFields":0,"Unit\\Service\\PropertyRbacHandlerTest::testGetUnauthorizedPropertiesReturnsUnauthorizedFields":0,"Unit\\Service\\PropertyRbacHandlerTest::testGetUnauthorizedPropertiesSkipsFieldsNotInIncoming":0,"Unit\\Service\\PropertyRbacHandlerTest::testCanUpdatePropertyAllowsAdminGroup":0,"Unit\\Service\\PropertyRbacHandlerTest::testCanReadPropertyWithConditionalMatchPassing":0,"Unit\\Service\\PropertyRbacHandlerTest::testCanReadPropertyWithConditionalMatchFailing":0,"Unit\\Service\\RequestScopedCacheTest::testGetReturnsNullForMissingKey":0,"Unit\\Service\\RequestScopedCacheTest::testSetAndGetReturnsValue":0,"Unit\\Service\\RequestScopedCacheTest::testSetOverwritesPreviousValue":0,"Unit\\Service\\RequestScopedCacheTest::testDifferentNamespacesAreIsolated":0,"Unit\\Service\\RequestScopedCacheTest::testCanStoreNullValue":0,"Unit\\Service\\RequestScopedCacheTest::testHasReturnsFalseForMissingNamespace":0,"Unit\\Service\\RequestScopedCacheTest::testHasReturnsFalseForMissingKey":0,"Unit\\Service\\RequestScopedCacheTest::testHasReturnsTrueForExistingKey":0,"Unit\\Service\\RequestScopedCacheTest::testGetMultipleReturnsFoundEntries":0,"Unit\\Service\\RequestScopedCacheTest::testGetMultipleReturnsEmptyForNoMatches":0,"Unit\\Service\\RequestScopedCacheTest::testGetMultipleWithEmptyKeysArray":0,"Unit\\Service\\RequestScopedCacheTest::testClearNamespaceRemovesOnlyThatNamespace":0,"Unit\\Service\\RequestScopedCacheTest::testClearAllRemovesEverything":0,"Unit\\Service\\RequestScopedCacheTest::testClearNonexistentNamespaceDoesNotError":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelReturnsNoneWhenNoEntities":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelReturnsLowForLocationEntity":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelReturnsMediumForPersonEntity":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelReturnsHighForEmailEntity":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelReturnsVeryHighForSsnEntity":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelTakesHighestRisk":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelEscalatesWhenAboveThreshold":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelDoesNotEscalateVeryHigh":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelDoesNotEscalateAtThreshold":0,"Unit\\Service\\RiskLevelServiceTest::testComputeRiskLevelFallsBackToLowForUnknownEntityType":0,"Unit\\Service\\RiskLevelServiceTest::testUpdateRiskLevelComputesAndPersists":0.003,"Unit\\Service\\RiskLevelServiceTest::testUpdateRiskLevelHandlesMetadataException":0,"Unit\\Service\\RiskLevelServiceTest::testGetRiskLevelReturnsStoredValue":0,"Unit\\Service\\RiskLevelServiceTest::testGetRiskLevelReturnsNoneWhenNoMetadata":0,"Unit\\Service\\RiskLevelServiceTest::testGetRiskLevelReturnsNoneOnException":0,"Unit\\Service\\RiskLevelServiceTest::testInitMetadataKeyCallsManager":0,"Unit\\Service\\RiskLevelServiceTest::testGetAllRiskLevelsReturnsExpectedLevels":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidTypes#string":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidTypes#number":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidTypes#integer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidTypes#boolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidTypes#array":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidTypes#object":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidTypes#null":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidTypes#file":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyRequiresType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyRejectsInvalidType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsValidStringFormats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyRejectsInvalidStringFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyIgnoresFormatOnNonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyNumericMinMax":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyNonNumericMinimumThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyNonNumericMaximumThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyMinimumGreaterThanMaximumThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyAcceptsEnum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyRejectsEmptyEnum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyRejectsNonArrayEnum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyRejectsNonBooleanVisible":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyRejectsNonBooleanHideOnCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyRejectsNonBooleanHideOnForm":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyOnDeleteRequiresRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyOnDeleteAcceptsValidActions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyOnDeleteAcceptsLowercase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyOnDeleteRejectsInvalidAction":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyNestedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyNestedArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyArrayItemsWithRefSkipsItemValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertiesMultiple":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertiesRejectsNonArrayProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertiesReportsPathCorrectly":0,"Unit\\Service\\SearchTrailServiceTest::testCreateSearchTrailReturnsTrail":0,"Unit\\Service\\SearchTrailServiceTest::testCreateSearchTrailThrowsOnMapperException":0,"Unit\\Service\\SearchTrailServiceTest::testCreateSearchTrailWithSelfClearingEnabled":0,"Unit\\Service\\SearchTrailServiceTest::testClearExpiredSearchTrailsReturnsSuccessWithDeletions":0,"Unit\\Service\\SearchTrailServiceTest::testClearExpiredSearchTrailsReturnsSuccessNoDeletions":0,"Unit\\Service\\SearchTrailServiceTest::testClearExpiredSearchTrailsHandlesException":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsReturnsPaginatedResults":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsProcessesPaginationFromPage":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailReturnsEnrichedTrail":0,"Unit\\Service\\SearchTrailServiceTest::testCleanupSearchTrailsReturnsSuccess":0,"Unit\\Service\\SearchTrailServiceTest::testCleanupSearchTrailsHandlesException":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchStatisticsReturnsEnhancedStats":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchStatisticsWithZeroSearches":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchStatisticsWithDateRange":0,"Unit\\Service\\SearchTrailServiceTest::testGetPopularSearchTermsReturnsEnhancedTerms":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchActivityReturnsInsights":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchActivityReturnsNoDataForEmptyActivity":0,"Unit\\Service\\SearchTrailServiceTest::testGetRegisterSchemaStatisticsReturnsEnhancedStats":0,"Unit\\Service\\SearchTrailServiceTest::testConstructorWithCustomRetention":0,"Unit\\Service\\SecurityServiceTest::testCheckLoginRateLimitAllowsWhenNoAttempts":0,"Unit\\Service\\SecurityServiceTest::testCheckLoginRateLimitBlocksLockedOutUser":0,"Unit\\Service\\SecurityServiceTest::testCheckLoginRateLimitBlocksLockedOutIp":0,"Unit\\Service\\SecurityServiceTest::testCheckLoginRateLimitBlocksWhenTooManyAttempts":0,"Unit\\Service\\SecurityServiceTest::testCheckLoginRateLimitAllowsExpiredLockout":0,"Unit\\Service\\SecurityServiceTest::testRecordFailedLoginAttemptIncrementsCounter":0,"Unit\\Service\\SecurityServiceTest::testRecordFailedLoginAttemptLocksOutAfterThreshold":0,"Unit\\Service\\SecurityServiceTest::testRecordSuccessfulLoginClearsRateLimits":0,"Unit\\Service\\SecurityServiceTest::testClearIpRateLimitsRemovesIpKeys":0,"Unit\\Service\\SecurityServiceTest::testClearUserRateLimitsRemovesUserKeys":0,"Unit\\Service\\SecurityServiceTest::testSanitizeInputTrimsStrings":0,"Unit\\Service\\SecurityServiceTest::testSanitizeInputTruncatesLongStrings":0,"Unit\\Service\\SecurityServiceTest::testSanitizeInputRemovesNullBytes":0,"Unit\\Service\\SecurityServiceTest::testSanitizeInputEscapesHtml":0,"Unit\\Service\\SecurityServiceTest::testSanitizeInputReturnsNonStringsUnchanged":0,"Unit\\Service\\SecurityServiceTest::testSanitizeInputProcessesArraysRecursively":0,"Unit\\Service\\SecurityServiceTest::testValidateLoginCredentialsRejectsEmptyUsername":0,"Unit\\Service\\SecurityServiceTest::testValidateLoginCredentialsRejectsEmptyPassword":0,"Unit\\Service\\SecurityServiceTest::testValidateLoginCredentialsRejectsShortUsername":0,"Unit\\Service\\SecurityServiceTest::testValidateLoginCredentialsRejectsInvalidChars":0,"Unit\\Service\\SecurityServiceTest::testValidateLoginCredentialsRejectsTooLongPassword":0,"Unit\\Service\\SecurityServiceTest::testValidateLoginCredentialsAcceptsValidInput":0,"Unit\\Service\\SecurityServiceTest::testAddSecurityHeadersReturnsResponse":0,"Unit\\Service\\SecurityServiceTest::testGetClientIpAddressReturnsRemoteAddress":0,"Unit\\Service\\SecurityServiceTest::testGetClientIpAddressUsesForwardedHeader":0,"Unit\\Service\\SecurityServiceTest::testGetClientIpAddressIgnoresPrivateForwardedIps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testIsMultiTenancyEnabledReturnsFalseWhenNotConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testIsMultiTenancyEnabledReturnsTrueWhenEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testIsMultiTenancyEnabledReturnsFalseWhenDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsReturnsDefaultsWhenNoConfigStored":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsWithStoredRbacConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsWithStoredSolrConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsIncludesAvailableGroups":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsIncludesAvailableUsers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsStoresRbacConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsStoresMultitenancyConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsStoresRetentionConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsStoresSolrConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdatePublishingOptionsStoresValidOptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdatePublishingOptionsIgnoresInvalidKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetRbacSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetRbacSettingsOnlyParsesStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateRbacSettingsOnlyStoresAndReturns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetOrganisationSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetMultitenancySettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetVersionInfoOnly":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetDefaultOrganisationUuidReturnsNullWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetDefaultOrganisationUuidReturnsStoredValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testSetDefaultOrganisationUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetFileSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdatePublishingOptionsThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testGetObjectSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testGetObjectSettingsOnlyParsesStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testUpdateObjectSettingsOnlyStoresConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testUpdateObjectSettingsOnlyAppliesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testGetObjectSettingsOnlyThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testUpdateObjectSettingsOnlyThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseAllComponents":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseDefaultOptionsTriggersAllComponents":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseCacheOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithDbFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsIncludesSystemInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsHandlesSolrException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsHandlesCacheStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsIncludesTimestamp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsInvalidMode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsInvalidBatchSizeTooSmall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsInvalidBatchSizeTooLarge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesTB":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesLowercase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesWithSpaces":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMaskTokenVeryLong":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsDocValuesMismatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsAllDifferenceTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetDatabaseInfoWithNoExtensions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testHasPostgresExtensionNoExtensionsKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testClearCacheNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateSearchBackendConfigDefaultsSolr":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetPostgresExtensionsWithMalformedData":0,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectThrowsWhenNoUser":0,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectThrowsWhenNoCalendar":0,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectReturnsEmptyWhenNoTasks":0,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectReturnsMatchingTasks":0,"Unit\\Service\\TaskServiceTest::testCreateTaskCreatesCalendarObject":0,"Unit\\Service\\TaskServiceTest::testUpdateTaskThrowsWhenNotFound":0,"Unit\\Service\\TaskServiceTest::testUpdateTaskUpdatesFields":0,"Unit\\Service\\TaskServiceTest::testDeleteTaskThrowsWhenNotFound":0,"Unit\\Service\\TaskServiceTest::testDeleteTaskDeletesCalendarObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testEntityTypeConstants":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testMethodConstants":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testCategoryConstants":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkReturnsEmptyForEmptyText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkReturnsEmptyForWhitespaceText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkDetectsEmail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkReturnsEmptyWhenNoEntitiesFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testProcessSourceChunksWithNoChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testProcessSourceChunksFiltersMetadataChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testProcessSourceChunksContinuesOnChunkError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithHybridMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithLlmMethodFallsBackToRegex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithPresidioFallsBackWhenNotConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithOpenAnonymiserFallsBackWhenNotConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkRespectsHighConfidenceThreshold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithEntityTypeFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForPersonType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForEmailType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForIbanType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForOrganizationType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForLocationType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForDateType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForUnknownType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractContextWithinBounds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractContextAtStartOfText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractContextAtEndOfText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexFindsEmails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexFindsIban":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexReturnsEmptyForCleanText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testBuildAnalyzeRequestBodyBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testBuildAnalyzeRequestBodyWithEntityTypes":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentReturnsChunksForText":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentReturnsEmptyArrayForEmptyText":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentRespectsChunkSizeOption":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithFixedSizeStrategy":0,"Unit\\Service\\TextExtractionServiceTest::testGetStatsReturnsExpectedStructure":0,"Unit\\Service\\ToolRegistryTest::testRegisterToolSucceeds":0,"Unit\\Service\\ToolRegistryTest::testRegisterToolThrowsForInvalidIdFormat":0,"Unit\\Service\\ToolRegistryTest::testRegisterToolThrowsForUppercaseId":0,"Unit\\Service\\ToolRegistryTest::testRegisterToolThrowsForDuplicateId":0,"Unit\\Service\\ToolRegistryTest::testRegisterToolThrowsForMissingMetadataName":0,"Unit\\Service\\ToolRegistryTest::testRegisterToolThrowsForMissingMetadataDescription":0,"Unit\\Service\\ToolRegistryTest::testRegisterToolThrowsForMissingMetadataIcon":0,"Unit\\Service\\ToolRegistryTest::testRegisterToolThrowsForMissingMetadataApp":0,"Unit\\Service\\ToolRegistryTest::testGetToolReturnsRegisteredTool":0,"Unit\\Service\\ToolRegistryTest::testGetToolReturnsNullForUnregistered":0,"Unit\\Service\\ToolRegistryTest::testGetAllToolsDispatchesEventOnFirstCall":0,"Unit\\Service\\ToolRegistryTest::testGetAllToolsDoesNotDispatchTwice":0,"Unit\\Service\\ToolRegistryTest::testGetAllToolsReturnsMetadataOnly":0,"Unit\\Service\\ToolRegistryTest::testGetToolsReturnsRequestedToolsOnly":0,"Unit\\Service\\ToolRegistryTest::testGetToolsSkipsMissingToolsAndLogsWarning":0,"Unit\\Service\\ToolRegistryTest::testGetToolsReturnsEmptyForEmptyInput":0,"Unit\\Service\\UploadServiceTest::testGetUploadedJsonReturnsErrorWhenNoValidKeys":0,"Unit\\Service\\UploadServiceTest::testGetUploadedJsonReturnsErrorWhenEmptyData":0,"Unit\\Service\\UploadServiceTest::testGetUploadedJsonRemovesInternalParameters":0,"Unit\\Service\\UploadServiceTest::testGetUploadedJsonHandlesArrayInput":0,"Unit\\Service\\UploadServiceTest::testGetUploadedJsonHandlesJsonString":0,"Unit\\Service\\UploadServiceTest::testGetUploadedJsonReturnsErrorForInvalidJson":0,"Unit\\Service\\UploadServiceTest::testGetUploadedJsonThrowsForFileUpload":0,"Unit\\Service\\UploadServiceTest::testGetUploadedJsonReturnsErrorForNullJson":0,"Unit\\Service\\UserServiceTest::testGetCurrentUserReturnsUserFromSession":0,"Unit\\Service\\UserServiceTest::testGetCurrentUserReturnsNullWhenNotAuthenticated":0,"Unit\\Service\\UserServiceTest::testGetCustomNameFieldsReturnsFieldsFromConfig":0,"Unit\\Service\\UserServiceTest::testGetCustomNameFieldsReturnsNullForEmptyValues":0,"Unit\\Service\\UserServiceTest::testSetCustomNameFieldsSetsAllowedFields":0,"Unit\\Service\\UserServiceTest::testSetCustomNameFieldsIgnoresDisallowedFields":0,"Unit\\Service\\UserServiceTest::testSetCustomNameFieldsSetsAllThreeFields":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayRequiresOcClass":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesRequiresOcClass":0,"Unit\\Service\\VectorizationServiceTest::testRegisterStrategyAddsStrategy":0,"Unit\\Service\\VectorizationServiceTest::testVectorizeBatchThrowsForUnregisteredEntityType":0,"Unit\\Service\\VectorizationServiceTest::testVectorizeBatchReturnsZeroWhenNoEntities":0,"Unit\\Service\\VectorizationServiceTest::testVectorizeBatchProcessesEntitiesSerially":0,"Unit\\Service\\VectorizationServiceTest::testVectorizeBatchHandlesEntityWithNoItems":0,"Unit\\Service\\VectorizationServiceTest::testVectorizeBatchHandlesEmbeddingFailure":0,"Unit\\Service\\VectorizationServiceTest::testVectorizeBatchHandlesEntityProcessingException":0,"Unit\\Service\\VectorizationServiceTest::testVectorizeBatchParallelMode":0,"Unit\\Service\\VectorizationServiceTest::testGenerateEmbeddingDelegatesToVectorService":0,"Unit\\Service\\VectorizationServiceTest::testSemanticSearchDelegatesToVectorService":0,"Unit\\Service\\VectorizationServiceTest::testHybridSearchDelegatesToVectorService":0,"Unit\\Service\\VectorizationServiceTest::testGetVectorStatsDelegatesToVectorService":0,"Unit\\Service\\VectorizationServiceTest::testTestEmbeddingDelegatesToVectorService":0,"Unit\\Service\\VectorizationServiceTest::testCheckEmbeddingModelMismatchDelegatesToVectorService":0,"Unit\\Service\\VectorizationServiceTest::testClearAllEmbeddingsDelegatesToVectorService":0,"Unit\\Service\\ViewServiceTest::testFindReturnsOwnedView":0,"Unit\\Service\\ViewServiceTest::testFindReturnsPublicViewForOtherUser":0,"Unit\\Service\\ViewServiceTest::testFindThrowsForPrivateViewOfOtherUser":0,"Unit\\Service\\ViewServiceTest::testFindAllDelegatesToMapper":0,"Unit\\Service\\ViewServiceTest::testCreateReturnsInsertedView":0,"Unit\\Service\\ViewServiceTest::testCreateClearsDefaultWhenSettingDefault":0.001,"Unit\\Service\\ViewServiceTest::testCreateThrowsAndLogsOnFailure":0,"Unit\\Service\\ViewServiceTest::testUpdateReturnsUpdatedView":0,"Unit\\Service\\ViewServiceTest::testUpdateWithFavoredBy":0,"Unit\\Service\\ViewServiceTest::testUpdateClearsDefaultWhenSwitchingToDefault":0,"Unit\\Service\\ViewServiceTest::testUpdateThrowsForPrivateViewOfOtherUser":0,"Unit\\Service\\ViewServiceTest::testDeleteRemovesView":0,"Unit\\Service\\ViewServiceTest::testDeleteThrowsForPrivateViewOfOtherUser":0,"Unit\\Service\\ViewServiceTest::testDeleteThrowsAndLogsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Webhook\\CloudEventFormatterTest::testFormatAsCloudEventDefaultSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Webhook\\CloudEventFormatterTest::testFormatAsCloudEventCustomSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Webhook\\CloudEventFormatterTest::testFormatAsCloudEventEmptyPayload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Webhook\\CloudEventFormatterTest::testFormatRequestAsCloudEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Webhook\\CloudEventFormatterTest::testFormatRequestAsCloudEventHttpProtocol":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Webhook\\CloudEventFormatterTest::testSubjectExtractionRegisterSchemaOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Webhook\\CloudEventFormatterTest::testContentTypeDefaultsToJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Webhook\\CloudEventFormatterTest::testUniqueIdsGenerated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testBuildPayloadWithMappingTransformsPayload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testBuildPayloadWithMissingMappingFallsBackToStandard":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testBuildPayloadWithMappingErrorFallsBackWithWarning":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testBuildPayloadWithNullMappingUsesStandardFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testBuildPayloadMappingTakesPrecedenceOverCloudEvents":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testApplyMappingTransformationEnrichesInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testApplyMappingTransformationReturnsNullOnMissingMapping":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testApplyMappingTransformationReturnsNullOnExecutionError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGetShortEventNameExtractsClassName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGetShortEventNameWithSimpleName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testBuildPayloadWithNullMappingUsesCloudEventsWhenConfigured":0,"Unit\\Service\\WorkflowEngineRegistryTest::testResolveAdapterReturnsN8nAdapterForN8nType":0,"Unit\\Service\\WorkflowEngineRegistryTest::testResolveAdapterReturnsWindmillAdapterForWindmillType":0,"Unit\\Service\\WorkflowEngineRegistryTest::testResolveAdapterThrowsForUnsupportedType":0,"Unit\\Service\\WorkflowEngineRegistryTest::testResolveAdapterDecryptsAuthConfig":0,"Unit\\Service\\WorkflowEngineRegistryTest::testResolveAdapterHandlesDecryptionFailure":0,"Unit\\Service\\WorkflowEngineRegistryTest::testResolveAdapterHandlesNullAuthConfig":0,"Unit\\Service\\WorkflowEngineRegistryTest::testResolveAdapterByIdFindsAndResolves":0,"Unit\\Service\\WorkflowEngineRegistryTest::testGetEnginesReturnsAll":0,"Unit\\Service\\WorkflowEngineRegistryTest::testGetEnginesByTypeFiltersCorrectly":0,"Unit\\Service\\WorkflowEngineRegistryTest::testGetEngineReturnsEngine":0,"Unit\\Service\\WorkflowEngineRegistryTest::testCreateEngineEncryptsAuthConfig":0,"Unit\\Service\\WorkflowEngineRegistryTest::testCreateEngineSkipsEncryptionForNonArrayAuthConfig":0,"Unit\\Service\\WorkflowEngineRegistryTest::testUpdateEngineEncryptsAuthConfig":0,"Unit\\Service\\WorkflowEngineRegistryTest::testDeleteEngineReturnsDeletedEngine":0,"Unit\\Service\\WorkflowEngineRegistryTest::testHealthCheckReturnsHealthyResult":0,"Unit\\Service\\WorkflowEngineRegistryTest::testHealthCheckReturnsUnhealthyResult":0,"Unit\\Service\\WorkflowEngineRegistryTest::testDiscoverEnginesReturnsEmptyWhenAppApiNotInstalled":0,"Unit\\Service\\WorkflowEngineRegistryTest::testDiscoverEnginesReturnsInstalledEngines":0,"Unit\\Service\\WorkflowEngineRegistryTest::testDiscoverEnginesReturnsBothEnginesWhenInstalled":0,"Unit\\Tool\\AbstractToolTest::testLogLevels#info level":0,"Unit\\Tool\\AbstractToolTest::testLogLevels#error level":0,"Unit\\Tool\\AbstractToolTest::testLogLevels#warning level":0,"Unit\\Tool\\AbstractToolTest::testLogLevels#unknown level":0,"Unit\\Tool\\AbstractToolTest::testGetName":0,"Unit\\Tool\\AbstractToolTest::testGetDescription":0,"Unit\\Tool\\AbstractToolTest::testGetFunctions":0,"Unit\\Tool\\AbstractToolTest::testExecuteFunction":0,"Unit\\Tool\\AbstractToolTest::testSetAgentNull":0,"Unit\\Tool\\AbstractToolTest::testSetAgentWithAgent":0,"Unit\\Tool\\AbstractToolTest::testGetUserIdWithExplicitUserId":0,"Unit\\Tool\\AbstractToolTest::testGetUserIdFromSession":0,"Unit\\Tool\\AbstractToolTest::testGetUserIdFromAgentFallback":0,"Unit\\Tool\\AbstractToolTest::testGetUserIdReturnsNullWhenNoContext":0,"Unit\\Tool\\AbstractToolTest::testGetUserIdAgentWithNullUser":0,"Unit\\Tool\\AbstractToolTest::testHasUserContextTrue":0,"Unit\\Tool\\AbstractToolTest::testHasUserContextFalse":0,"Unit\\Tool\\AbstractToolTest::testApplyViewFiltersNoAgent":0,"Unit\\Tool\\AbstractToolTest::testApplyViewFiltersAgentNullViews":0,"Unit\\Tool\\AbstractToolTest::testApplyViewFiltersAgentEmptyViews":0,"Unit\\Tool\\AbstractToolTest::testFormatSuccessDefault":0,"Unit\\Tool\\AbstractToolTest::testFormatSuccessCustomMessage":0,"Unit\\Tool\\AbstractToolTest::testFormatErrorWithoutDetails":0,"Unit\\Tool\\AbstractToolTest::testFormatErrorWithDetails":0,"Unit\\Tool\\AbstractToolTest::testValidateParametersSuccess":0,"Unit\\Tool\\AbstractToolTest::testValidateParametersMissing":0,"Unit\\Tool\\AbstractToolTest::testValidateParametersNullValue":0,"Unit\\Tool\\AbstractToolTest::testLogInfo":0,"Unit\\Tool\\AbstractToolTest::testLogError":0,"Unit\\Tool\\AbstractToolTest::testLogWarning":0,"Unit\\Tool\\AbstractToolTest::testLogCustomMessage":0,"Unit\\Tool\\AbstractToolTest::testLogDefaultMessage":0,"Unit\\Tool\\AbstractToolTest::testCallSnakeCaseToCamelCase":0,"Unit\\Tool\\AbstractToolTest::testCallWithDefaultValues":0,"Unit\\Tool\\AbstractToolTest::testCallNullStringConvertedToDefault":0,"Unit\\Tool\\AbstractToolTest::testCallTypeCastingInt":0,"Unit\\Tool\\AbstractToolTest::testCallTypeCastingBoolFalse":0,"Unit\\Tool\\AbstractToolTest::testCallAssociativeArguments":0,"Unit\\Tool\\AbstractToolTest::testCallNonExistentMethodThrows":0,"Unit\\Tool\\AbstractToolTest::testCallReturnsNonArrayAsIs":0,"Unit\\Tool\\AgentToolTest::testGetName":0,"Unit\\Tool\\AgentToolTest::testGetDescription":0,"Unit\\Tool\\AgentToolTest::testGetFunctionsContainsAllCrud":0,"Unit\\Tool\\AgentToolTest::testGetFunctionsStructure":0.001,"Unit\\Tool\\AgentToolTest::testExecuteFunctionCallsCorrectMethod":0,"Unit\\Tool\\AgentToolTest::testExecuteFunctionUnknownMethodThrows":0,"Unit\\Tool\\AgentToolTest::testListAgentsSuccess":0,"Unit\\Tool\\AgentToolTest::testListAgentsWithPagination":0,"Unit\\Tool\\AgentToolTest::testListAgentsEmpty":0,"Unit\\Tool\\AgentToolTest::testListAgentsException":0,"Unit\\Tool\\AgentToolTest::testGetAgentSuccess":0,"Unit\\Tool\\AgentToolTest::testGetAgentNotFound":0,"Unit\\Tool\\AgentToolTest::testGetAgentGenericException":0,"Unit\\Tool\\AgentToolTest::testCreateAgentSuccess":0,"Unit\\Tool\\AgentToolTest::testCreateAgentMinimalParams":0,"Unit\\Tool\\AgentToolTest::testCreateAgentWithEmptyStrings":0,"Unit\\Tool\\AgentToolTest::testCreateAgentSetsOwnerFromAgentContext":0,"Unit\\Tool\\AgentToolTest::testCreateAgentException":0,"Unit\\Tool\\AgentToolTest::testUpdateAgentAllFields":0,"Unit\\Tool\\AgentToolTest::testUpdateAgentNoFields":0,"Unit\\Tool\\AgentToolTest::testUpdateAgentNotFound":0,"Unit\\Tool\\AgentToolTest::testUpdateAgentGenericException":0,"Unit\\Tool\\AgentToolTest::testDeleteAgentSuccess":0,"Unit\\Tool\\AgentToolTest::testDeleteAgentNotFound":0,"Unit\\Tool\\AgentToolTest::testDeleteAgentGenericException":0,"Unit\\Tool\\AgentToolTest::testExecuteFunctionGetAgent":0,"Unit\\Tool\\AgentToolTest::testExecuteFunctionCreateAgent":0,"Unit\\Tool\\AgentToolTest::testExecuteFunctionDeleteAgent":0,"Unit\\Tool\\ApplicationToolTest::testGetName":0,"Unit\\Tool\\ApplicationToolTest::testGetDescription":0,"Unit\\Tool\\ApplicationToolTest::testGetFunctionsContainsAllCrud":0,"Unit\\Tool\\ApplicationToolTest::testGetFunctionsStructure":0.001,"Unit\\Tool\\ApplicationToolTest::testExecuteFunctionCallsCorrectMethod":0,"Unit\\Tool\\ApplicationToolTest::testExecuteFunctionUnknownMethodThrows":0,"Unit\\Tool\\ApplicationToolTest::testListApplicationsSuccess":0,"Unit\\Tool\\ApplicationToolTest::testListApplicationsWithPagination":0,"Unit\\Tool\\ApplicationToolTest::testListApplicationsEmpty":0,"Unit\\Tool\\ApplicationToolTest::testListApplicationsException":0,"Unit\\Tool\\ApplicationToolTest::testGetApplicationSuccess":0,"Unit\\Tool\\ApplicationToolTest::testGetApplicationNotFound":0,"Unit\\Tool\\ApplicationToolTest::testGetApplicationGenericException":0,"Unit\\Tool\\ApplicationToolTest::testCreateApplicationSuccess":0,"Unit\\Tool\\ApplicationToolTest::testCreateApplicationMinimalParams":0,"Unit\\Tool\\ApplicationToolTest::testCreateApplicationWithEmptyDescription":0,"Unit\\Tool\\ApplicationToolTest::testCreateApplicationException":0,"Unit\\Tool\\ApplicationToolTest::testUpdateApplicationAllFields":0,"Unit\\Tool\\ApplicationToolTest::testUpdateApplicationNoFields":0,"Unit\\Tool\\ApplicationToolTest::testUpdateApplicationNotFound":0,"Unit\\Tool\\ApplicationToolTest::testUpdateApplicationGenericException":0,"Unit\\Tool\\ApplicationToolTest::testDeleteApplicationSuccess":0,"Unit\\Tool\\ApplicationToolTest::testDeleteApplicationNotFound":0,"Unit\\Tool\\ApplicationToolTest::testDeleteApplicationGenericException":0,"Unit\\Tool\\ApplicationToolTest::testExecuteFunctionGetApplication":0,"Unit\\Tool\\ApplicationToolTest::testExecuteFunctionCreateApplication":0,"Unit\\Tool\\ApplicationToolTest::testExecuteFunctionDeleteApplication":0,"Unit\\Tool\\ObjectsToolTest::testGetName":0,"Unit\\Tool\\ObjectsToolTest::testGetDescription":0,"Unit\\Tool\\ObjectsToolTest::testGetFunctionsContainsAllCrud":0,"Unit\\Tool\\ObjectsToolTest::testGetFunctionsStructure":0.001,"Unit\\Tool\\ObjectsToolTest::testExecuteFunctionNoUserContext":0,"Unit\\Tool\\ObjectsToolTest::testExecuteFunctionUnknownFunction":0,"Unit\\Tool\\ObjectsToolTest::testSearchObjectsSuccess":0,"Unit\\Tool\\ObjectsToolTest::testSearchObjectsWithFilters":0,"Unit\\Tool\\ObjectsToolTest::testSearchObjectsEmptyQuery":0,"Unit\\Tool\\ObjectsToolTest::testSearchObjectsNullResultsKey":0,"Unit\\Tool\\ObjectsToolTest::testSearchObjectsViaExecuteFunction":0,"Unit\\Tool\\ObjectsToolTest::testGetObjectSuccess":0,"Unit\\Tool\\ObjectsToolTest::testGetObjectNotFound":0,"Unit\\Tool\\ObjectsToolTest::testGetObjectNotFoundViaExecuteFunction":0,"Unit\\Tool\\ObjectsToolTest::testCreateObjectSuccess":0,"Unit\\Tool\\ObjectsToolTest::testCreateObjectException":0,"Unit\\Tool\\ObjectsToolTest::testUpdateObjectSuccess":0,"Unit\\Tool\\ObjectsToolTest::testUpdateObjectException":0,"Unit\\Tool\\ObjectsToolTest::testDeleteObjectSuccess":0,"Unit\\Tool\\ObjectsToolTest::testDeleteObjectNotFound":0,"Unit\\Tool\\ObjectsToolTest::testDeleteObjectNotFoundViaExecuteFunction":0,"Unit\\Tool\\ObjectsToolTest::testDeleteObjectUsesIdWhenUuidNull":0,"Unit\\Tool\\ObjectsToolTest::testSetAgentAppliesViewFilters":0,"Unit\\Tool\\RegisterToolTest::testGetName":0,"Unit\\Tool\\RegisterToolTest::testGetDescription":0,"Unit\\Tool\\RegisterToolTest::testGetFunctionsContainsAllCrud":0,"Unit\\Tool\\RegisterToolTest::testGetFunctionsStructure":0,"Unit\\Tool\\RegisterToolTest::testExecuteFunctionNoUserContext":0,"Unit\\Tool\\RegisterToolTest::testExecuteFunctionUnknownFunction":0,"Unit\\Tool\\RegisterToolTest::testListRegistersSuccess":0,"Unit\\Tool\\RegisterToolTest::testListRegistersEmpty":0,"Unit\\Tool\\RegisterToolTest::testListRegistersWithPagination":0,"Unit\\Tool\\RegisterToolTest::testListRegistersViaExecuteFunction":0,"Unit\\Tool\\RegisterToolTest::testGetRegisterSuccess":0,"Unit\\Tool\\RegisterToolTest::testGetRegisterExceptionViaExecuteFunction":0,"Unit\\Tool\\RegisterToolTest::testCreateRegisterSuccess":0,"Unit\\Tool\\RegisterToolTest::testCreateRegisterWithoutSlug":0,"Unit\\Tool\\RegisterToolTest::testCreateRegisterWithSlug":0,"Unit\\Tool\\RegisterToolTest::testCreateRegisterException":0,"Unit\\Tool\\RegisterToolTest::testUpdateRegisterSuccess":0,"Unit\\Tool\\RegisterToolTest::testUpdateRegisterTitleOnly":0,"Unit\\Tool\\RegisterToolTest::testUpdateRegisterNoData":0,"Unit\\Tool\\RegisterToolTest::testUpdateRegisterNoDataViaExecuteFunction":0,"Unit\\Tool\\RegisterToolTest::testUpdateRegisterException":0,"Unit\\Tool\\RegisterToolTest::testDeleteRegisterSuccess":0,"Unit\\Tool\\RegisterToolTest::testDeleteRegisterException":0,"Unit\\Tool\\RegisterToolTest::testExecuteFunctionWithExplicitUserId":0,"Unit\\Tool\\SchemaToolTest::testGetName":0,"Unit\\Tool\\SchemaToolTest::testGetDescription":0,"Unit\\Tool\\SchemaToolTest::testGetFunctionsContainsAllCrud":0,"Unit\\Tool\\SchemaToolTest::testGetFunctionsStructure":0,"Unit\\Tool\\SchemaToolTest::testExecuteFunctionNoUserContext":0,"Unit\\Tool\\SchemaToolTest::testExecuteFunctionUnknownFunction":0,"Unit\\Tool\\SchemaToolTest::testListSchemasSuccess":0,"Unit\\Tool\\SchemaToolTest::testListSchemasEmpty":0,"Unit\\Tool\\SchemaToolTest::testListSchemasWithRegisterFilter":0,"Unit\\Tool\\SchemaToolTest::testListSchemasWithPagination":0,"Unit\\Tool\\SchemaToolTest::testListSchemasViaExecuteFunction":0,"Unit\\Tool\\SchemaToolTest::testGetSchemaSuccess":0,"Unit\\Tool\\SchemaToolTest::testGetSchemaExceptionViaExecuteFunction":0,"Unit\\Tool\\SchemaToolTest::testCreateSchemaSuccess":0,"Unit\\Tool\\SchemaToolTest::testCreateSchemaWithoutRequired":0,"Unit\\Tool\\SchemaToolTest::testCreateSchemaWithRequired":0,"Unit\\Tool\\SchemaToolTest::testCreateSchemaException":0,"Unit\\Tool\\SchemaToolTest::testUpdateSchemaAllFields":0,"Unit\\Tool\\SchemaToolTest::testUpdateSchemaNoFields":0,"Unit\\Tool\\SchemaToolTest::testUpdateSchemaException":0,"Unit\\Tool\\SchemaToolTest::testDeleteSchemaSuccess":0,"Unit\\Tool\\SchemaToolTest::testDeleteSchemaException":0,"Unit\\Tool\\SchemaToolTest::testExecuteFunctionWithExplicitUserId":0,"Unit\\Twig\\AuthenticationExtensionTest::testGetFunctionsReturnsArray":0,"Unit\\Twig\\AuthenticationExtensionTest::testGetFunctionsAreTwigFunctions":0,"Unit\\Twig\\AuthenticationExtensionTest::testGetFunctionsContainsOauthToken":0,"Unit\\Twig\\AuthenticationExtensionTest::testGetFunctionsContainsDecosToken":0,"Unit\\Twig\\AuthenticationExtensionTest::testGetFunctionsContainsJwtToken":0,"Unit\\Twig\\AuthenticationExtensionTest::testFunctionsPointToAuthenticationRuntime":0,"Unit\\Twig\\AuthenticationExtensionTest::testOauthTokenCallable":0,"Unit\\Twig\\AuthenticationExtensionTest::testDecosTokenCallable":0,"Unit\\Twig\\AuthenticationExtensionTest::testJwtTokenCallable":0,"Unit\\Twig\\AuthenticationRuntimeTest::testImplementsRuntimeExtensionInterface":0,"Unit\\Twig\\AuthenticationRuntimeTest::testOauthTokenCallsAuthService":0,"Unit\\Twig\\AuthenticationRuntimeTest::testOauthTokenWithNestedConfig":0,"Unit\\Twig\\AuthenticationRuntimeTest::testDecosTokenCallsAuthService":0,"Unit\\Twig\\AuthenticationRuntimeTest::testJwtTokenCallsAuthService":0,"Unit\\Twig\\AuthenticationRuntimeTest::testOauthTokenWithMissingAuthenticationThrowsTypeError":0,"Unit\\Twig\\AuthenticationRuntimeTest::testDecosTokenWithMissingAuthenticationThrowsTypeError":0,"Unit\\Twig\\AuthenticationRuntimeTest::testJwtTokenWithMissingAuthenticationThrowsTypeError":0,"Unit\\Twig\\AuthenticationRuntimeTest::testOauthTokenWithEmptyAuthentication":0,"Unit\\Twig\\AuthenticationRuntimeTest::testDecosTokenWithEmptyAuthentication":0,"Unit\\Twig\\AuthenticationRuntimeTest::testJwtTokenWithEmptyAuthentication":0,"Unit\\Twig\\MappingExtensionTest::testGetFiltersReturnsArray":0,"Unit\\Twig\\MappingExtensionTest::testGetFiltersAreTwigFilters":0,"Unit\\Twig\\MappingExtensionTest::testGetFiltersNames":0,"Unit\\Twig\\MappingExtensionTest::testFiltersPointToMappingRuntime":0,"Unit\\Twig\\MappingExtensionTest::testFilterCallableMethods":0,"Unit\\Twig\\MappingExtensionTest::testGetFunctionsReturnsArray":0,"Unit\\Twig\\MappingExtensionTest::testGetFunctionsAreTwigFunctions":0,"Unit\\Twig\\MappingExtensionTest::testGetFunctionsNames":0,"Unit\\Twig\\MappingExtensionTest::testFunctionsPointToMappingRuntime":0,"Unit\\Twig\\MappingExtensionTest::testExecuteMappingCallable":0,"Unit\\Twig\\MappingExtensionTest::testGenerateUuidCallable":0,"Unit\\Twig\\MappingRuntimeLoaderTest::testImplementsRuntimeLoaderInterface":0,"Unit\\Twig\\MappingRuntimeLoaderTest::testLoadReturnsMappingRuntimeForCorrectClass":0.001,"Unit\\Twig\\MappingRuntimeLoaderTest::testLoadReturnsNewInstanceEachTime":0,"Unit\\Twig\\MappingRuntimeLoaderTest::testLoadReturnsNullForOtherClass":0,"Unit\\Twig\\MappingRuntimeLoaderTest::testLoadReturnsNullForRandomString":0,"Unit\\Twig\\MappingRuntimeLoaderTest::testLoadReturnsNullForEmptyString":0,"Unit\\Twig\\MappingRuntimeLoaderTest::testLoadReturnsNullForRuntimeExtensionInterface":0,"Unit\\Twig\\MappingRuntimeTest::testImplementsRuntimeExtensionInterface":0,"Unit\\Twig\\MappingRuntimeTest::testB64enc":0,"Unit\\Twig\\MappingRuntimeTest::testB64encEmptyString":0,"Unit\\Twig\\MappingRuntimeTest::testB64encSpecialChars":0,"Unit\\Twig\\MappingRuntimeTest::testB64dec":0,"Unit\\Twig\\MappingRuntimeTest::testB64decEmptyString":0,"Unit\\Twig\\MappingRuntimeTest::testB64encAndB64decRoundTrip":0,"Unit\\Twig\\MappingRuntimeTest::testJsonDecode":0,"Unit\\Twig\\MappingRuntimeTest::testJsonDecodeEmptyObject":0,"Unit\\Twig\\MappingRuntimeTest::testJsonDecodeArray":0,"Unit\\Twig\\MappingRuntimeTest::testJsonDecodeInvalidJsonReturnsEmptyArray":0,"Unit\\Twig\\MappingRuntimeTest::testJsonDecodeEmptyStringReturnsEmptyArray":0,"Unit\\Twig\\MappingRuntimeTest::testGenerateUuidReturnsUuidV4":0,"Unit\\Twig\\MappingRuntimeTest::testGenerateUuidReturnsDifferentValues":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumMapsValue":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumReturnsOriginalWhenNoMapping":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumReturnsOriginalWhenFieldNotInMappings":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumEmptyMappings":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumDefaultEmptyMappings":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumReverseMapsDutchToEnglish":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumReverseReturnsOriginalWhenNoMapping":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumReverseReturnsOriginalWhenFieldMissing":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumReverseEmptyMappings":0,"Unit\\Twig\\MappingRuntimeTest::testZgwEnumReverseDefaultEmptyMappings":0,"Unit\\Twig\\MappingRuntimeTest::testZgwExtractUuidFromUrl":0,"Unit\\Twig\\MappingRuntimeTest::testZgwExtractUuidStripsTrailingSlash":0,"Unit\\Twig\\MappingRuntimeTest::testZgwExtractUuidSimplePath":0,"Unit\\Twig\\MappingRuntimeTest::testZgwExtractUuidJustUuid":0,"Unit\\Twig\\MappingRuntimeTest::testZgwExtractUuidNull":0,"Unit\\Twig\\MappingRuntimeTest::testZgwExtractUuidEmptyString":0,"Unit\\Twig\\MappingRuntimeTest::testExecuteMappingWithMappingObject":0,"Unit\\Twig\\MappingRuntimeTest::testExecuteMappingWithListFlag":0,"Unit\\Twig\\MappingRuntimeTest::testExecuteMappingWithArray":0,"Unit\\Twig\\MappingRuntimeTest::testExecuteMappingWithIntId":0,"Unit\\Twig\\MappingRuntimeTest::testExecuteMappingWithStringId":0,"Unit\\Twig\\MappingRuntimeTest::testExecuteMappingWithUrlReference":0,"Unit\\Twig\\MappingRuntimeTest::testExecuteMappingWithHttpUrlReference":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testConfigureTrimsTrailingSlash":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testDeployWorkflowSuccess":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testDeployWorkflowMissingId":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testUpdateWorkflowReturnsIdFromResponse":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testUpdateWorkflowFallsBackToInputId":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testGetWorkflowReturnsData":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testGetWorkflowReturnsEmptyArrayOnNull":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testDeleteWorkflowCallsDelete":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testActivateWorkflowSendsActiveTrue":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testDeactivateWorkflowSendsActiveFalse":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowApproved":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowRejected":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowModified":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowError":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowNullResponseReturnsApproved":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowDefaultStatusIsApproved":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowExceptionReturnsError":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowTimeoutReturnsTimeoutError":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowTimeoutKeywordAlsoMatches":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowUsesWebhookUrl":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testGetWebhookUrl":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testListWorkflowsSuccess":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testListWorkflowsEmptyData":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testListWorkflowsMissingNameDefaults":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testListWorkflowsExceptionReturnsEmpty":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testHealthCheckReturnsTrue":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testHealthCheckReturnsFalseOnNon200":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testHealthCheckReturnsFalseOnException":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testBearerAuthHeader":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testBasicAuthHeader":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testNoAuthHeader":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testNoAuthConfigAtAll":0,"Unit\\WorkflowEngine\\N8nAdapterTest::testExecuteWorkflowErrorStatusMissingMessageFallback":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testConfigureTrimsTrailingSlash":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testConfigureDefaultWorkspace":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testDeployWorkflowSuccess":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testDeployWorkflowFallsBackToId":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testDeployWorkflowEmptyResponse":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testUpdateWorkflowReturnsPath":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testUpdateWorkflowFallsBackToInputId":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testGetWorkflowSuccess":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testGetWorkflowNullResponse":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testDeleteWorkflowCallsDelete":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testActivateWorkflowIsNoOp":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testDeactivateWorkflowIsNoOp":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowApproved":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowRejected":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowModified":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowErrorStatus":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowNullResponseReturnsApproved":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowDefaultStatusIsApproved":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowExceptionReturnsError":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowTimeoutError":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowTimeoutKeyword":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testExecuteWorkflowErrorMissingMessageFallback":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testGetWebhookUrl":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testListWorkflowsSuccess":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testListWorkflowsNullResponse":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testListWorkflowsExceptionReturnsEmpty":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testHealthCheckReturnsTrue":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testHealthCheckReturnsFalseOnNon200":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testHealthCheckReturnsFalseOnException":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testTokenAuthHeader":0,"Unit\\WorkflowEngine\\WindmillAdapterTest::testNoTokenNoAuthHeader":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testConstructorWithInvalidStatusThrows#empty string":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testConstructorWithInvalidStatusThrows#random string":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testConstructorWithInvalidStatusThrows#uppercase":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testConstructorWithInvalidStatusThrows#partial match":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testStatusChecks#approved":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testStatusChecks#rejected":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testStatusChecks#modified":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testStatusChecks#error":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testConstructorWithValidStatus":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testConstructorWithAllParameters":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testApprovedFactory":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testApprovedFactoryWithMetadata":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testRejectedFactory":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testRejectedFactoryWithoutMetadata":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testModifiedFactory":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testModifiedFactoryWithoutMetadata":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testErrorFactory":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testErrorFactoryWithoutMetadata":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testToArray":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testToArrayWithDefaults":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testJsonSerialize":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testJsonSerializeIsJsonCompatible":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testImplementsJsonSerializable":0,"Unit\\WorkflowEngine\\WorkflowResultTest::testConstants":0,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindById":0.025,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindByIdNonExistent":0.001,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllReturnsArray":27.138,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllWithLimit":0.179,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllWithOffset":0.015,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllWithActionFilter":5.252,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllWithCommaFilter":8.785,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllWithIsNullFilter":21.508,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllWithIsNotNullFilter":20.76,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllWithSearchOnStringColumn":0.008,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testFindAllIgnoresInvalidFilters":22.488,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testUpdateRecalculatesSize":0.009,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetStatisticsReturnsExpectedKeys":0.072,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetStatisticsWithRegisterId":0.137,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetStatisticsWithSchemaId":0.075,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetStatisticsWithExclude":0.091,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetStatisticsGroupedBySchemaEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetStatisticsGroupedBySchema":0.064,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetStatisticsGroupedBySchemaFillsMissing":0.051,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetDetailedStatisticsReturnsExpectedKeys":0.129,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetDetailedStatisticsWithFilters":0.175,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetActionDistributionReturnsExpectedStructure":0.087,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetActionDistributionWithFilters":0.084,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetActionChartDataReturnsExpectedStructure":0.185,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetActionChartDataWithDateRange":0.198,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetActionChartDataWithRegisterSchema":0.073,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetMostActiveObjectsReturnsExpectedStructure":0.163,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testGetMostActiveObjectsWithFilters":0.076,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testClearLogsDeletesExpiredTrails":0.117,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testClearLogsReturnsFalseWhenNoneExpired":0.113,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testSetExpiryDateUpdatesNullExpires":0,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testCreateAuditTrailForNewObject":0.037,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testCreateAuditTrailForUpdatedObject":0.018,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testCreateAuditTrailForDeletedObject":0.018,"OCA\\OpenRegister\\Tests\\Db\\AuditTrailMapperIntegrationTest::testCreateAuditTrailForReadAction":0.018,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testTermsFacetsOnStringProperty":0.17,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testTermsFacetsOnBooleanProperty":0.123,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testTermsFacetsOnIntegerProperty":0.11,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testMultipleFacetsInSingleRequest":0.112,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithPropertyFilterApplied":0.112,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsOnEmptyTableReturnsEmptyBuckets":0.089,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithNoFacetConfigReturnsEmpty":0.084,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithStringConfig":0.11,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithListOfFieldNames":0.107,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testMetadataFacets":0.123,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithCustomTitle":0.106,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsIncludeMetrics":0.101,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetBucketsContainCount":0.109,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsUnionAcrossMultipleTables":0.2,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsUnionWithStringConfig":0.161,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsUnionEmptyPairs":0,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithRbacAndMultitenancyDisabled":0.101,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithManyDistinctValues":0.206,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsOnPropertyWithNullValues":0.144,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testDateHistogramFacetOnMetadata":0.109,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsUnionWithMetadataFacets":0.206,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testDateHistogramFacetWithDayInterval":0.113,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testDateHistogramFacetWithWeekInterval":0.097,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testDateHistogramFacetWithYearInterval":0.106,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithCommaSeparatedFieldNames":0.103,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithCommaSeparatedMetadataFields":0.097,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithCommaSeparatedDateFields":0.1,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsExtendWithFacetableProperties":0.111,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsExtendWithDateFormatProperty":0.113,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithSearchFilter":0.11,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithArrayTypeProperty":0.101,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsUnionWithDateHistogramMetadata":0.202,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsUnionWithFieldMissingInOneTable":0.209,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testMetadataRegisterTermsFacet":0.097,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithNonExistentColumn":0.093,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsUnionWithListOfFieldNames":0.219,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithObjectFieldArrayFilter":0.11,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithFilterOnNonExistentProperty":0.096,"OCA\\OpenRegister\\Tests\\Db\\MagicFacetHandlerIntegrationTest::testFacetsWithCamelCasePropertyName":0.109,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetTableNameForRegisterSchema":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testEnsureTableForRegisterSchema":0.071,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testTableExistsForRegisterSchemaAfterCreation":0.073,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testExistsTableForRegisterSchemaUsesCache":0.071,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testEnsureTableIdempotent":0.089,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testIsMagicMappingEnabledDefault":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testIsMagicMappingEnabledForSchema":0.005,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testInsertAndSearchObjects":0.089,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testCountObjectsInRegisterSchemaTable":0.084,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetSimpleFacetsFromRegisterSchemaTable":0.075,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindInRegisterSchemaTable":0.082,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindInRegisterSchemaTableNotFound":0.073,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAllInRegisterSchemaTable":0.081,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testInsertObjectEntity":0.085,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testUpdateObjectEntity":0.09,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectEntityHardDelete":0.097,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testClearCache":0.078,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testClearCacheAll":0,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetExistingRegisterSchemaTables":0.097,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetAllRegisterSchemaPairs":0.074,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetIgnoredFilters":0,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationReturnsArray":31.768,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSyncTableForRegisterSchema":0.129,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFacetsReturnStructureWithData":0.105,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFacetsWithFilterQuery":0.087,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFacetsOnEmptyTable":0.082,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFacetsUnionAcrossMultipleTables":0.165,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithFilterOnProperty":0.087,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithPagination":0.114,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithSorting":0.087,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testCountObjectsInTable":0.099,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithNonExistentFilterProperty":0.087,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchAcrossMultipleTables":0.226,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchReturnsResultsWithRbacDisabled":0.095,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithRbacEnabledDoesNotCrash":0.092,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testCountWithRbacDisabled":0.084,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFacetsWithRbacDisabled":0.078,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithMultitenancyDisabled":0.08,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectsToRegisterSchemaTableBatch":0.086,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectsToRegisterSchemaTableEmpty":0.102,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAcrossAllMagicTablesByUuid":5.978,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAcrossAllMagicTablesNotFound":3.775,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindMultipleAcrossAllMagicTables":5.306,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindMultipleAcrossAllMagicTablesEmptyInput":0,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationAcrossAllMagicTablesEmptyUuid":0,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationAcrossAllMagicTablesNoMatch":2.95,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationUsingRelationsColumnNoMatch":7.403,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationUsingRelationsColumnEmptyUuid":0,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAllWithLimitAndOffset":0.155,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAllWithSort":0.12,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAllWithFilters":0.087,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectsBySchemaHardDelete":0.099,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectsBySchemaSoftDelete":0.107,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectsBySchemaNoTable":0.011,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testConvertRowToObjectEntityBasic":0.103,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testInsertObjectEntityAutoGeneratesUuid":0.113,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testUpdateObjectEntityPersistsData":0.107,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testHardDeleteRemovesObject":0.107,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithMetadataFilter":0.097,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithFreeTextSearch":0.103,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testCountWithPropertyFilter":0.119,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationBatchInSchemaEmptyInput":0,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchAcrossMultipleTablesWithFilter":0.243,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchWithPaginationAndSorting":0.121,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testTableNameContainsRegisterAndSchemaIds":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSyncTableAfterSchemaPropertyAdded":0.097,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testEnsureTableWithRichSchemaPropertyTypes":0.083,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveAndRetrieveObjectWithAllPropertyTypes":0.088,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectWithBooleanFalseValue":0.092,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectWithNullAndEmptyValues":0.086,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testEnsureTableForRegisterSchemaWithForce":0.079,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaChangeDetectedAfterPropertyChange":0.1,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectWithDatetimeMetadata":0.092,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectWithJsonMetadata":0.106,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectWithInvalidDatetimeString":0.196,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectTwiceTriggersUpdate":0.188,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testConvertRowWithDateFormatProperties":0.167,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testConvertRowWithJsonStringValues":0.169,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testConvertRowWithNumericStringProperty":0.159,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSoftDeleteObjectEntityRequiresUser":0.146,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectsByUuidsHardDelete":0.229,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectsByUuidsSoftDelete":0.254,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectsByUuidsEmptyArray":0.204,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectsBySchemaSoftDeleteBulk":0.282,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testDeleteObjectsBySchemaNoTableExistsReturnsZero":0.036,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testLockObjectEntityRequiresUser":0.291,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchAcrossMultipleTablesWithAggregationsFallsBackToSequential":0.321,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchAcrossMultipleTablesUnionPath":0.352,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchAcrossMultipleTablesWithOrderParam":0.173,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchAcrossMultipleTablesWithNullPairs":0.001,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetSimpleFacetsUnionMultipleSchemas":0.359,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetSimpleFacetsUnionWithRegisterSchemaPairs":0.171,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetSimpleFacetsUnionEmptyConfigs":0,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationWithData":6.021,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationEmptyUuid":0,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationBatchInSchemaWithData":0.181,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationBatchInSchemaWithAdditionalFields":0.167,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationBatchInSchemaTableNotExists":0.002,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationUsingRelationsColumnWithData":4.085,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindMultipleAcrossAllMagicTablesWithData":2.832,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindMultipleAcrossAllMagicTablesDeduplicatesInput":2.055,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindByRelationAcrossAllMagicTablesWithData":1.88,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSyncTableAddsNewColumns":0.18,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSyncTableWhenTableDoesNotExistCreatesIt":0.154,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSyncTableReturnsStatistics":0.171,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testIsMagicMappingEnabledWithSchemaConfiguration":0.025,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testIsMagicMappingEnabledWithoutConfiguration":0.017,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testIsMagicMappingEnabledForSchemaDeprecated":0.011,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testClearCacheWithSpecificRegisterAndSchema":0.15,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindInRegisterSchemaTableBySlug":0.157,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindInRegisterSchemaTableByNumericId":0.159,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindInRegisterSchemaTableIncludeDeleted":0.164,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAllWithLimitOffsetAndSort":0.195,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAllWithPublishedFilterIsHandled":0.18,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithObjectReferenceProperty":0.179,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithArrayOfObjectReferences":0.147,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithSmallIntProperty":0.157,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithBigIntProperty":0.16,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithIntegerDefault":0.156,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithUrlFormatProperty":0.17,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithShortStringProperty":0.168,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithManyPropertiesCreatesTable":0.155,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithFacetableProperty":0.219,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testCountWithSearchTerm":0.193,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testCountOnNonExistentTable":0.021,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSearchOnNonExistentTableReturnsEmpty":0.019,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAcrossAllMagicTablesBySlug":4.011,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testFindAcrossAllMagicTablesIncludeDeletedFlag":3.908,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetExistingRegisterSchemaTablesReturnsCreatedTable":0.259,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testGetIgnoredFiltersAfterSearchWithBadFilter":0.161,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testInsertObjectEntityWithoutUuidGeneratesOne":0.178,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testUpdateObjectEntityWithExplicitOldEntity":0.182,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectWithBase64FilePropertySetsNull":0.19,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSaveObjectWithArrayOfBase64Files":0.159,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithBooleanRequiredFlag":0.151,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testCamelCasePropertiesAreSanitized":0.168,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSpecialCharacterPropertyNames":0.172,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithNumberDefault":0.157,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithBooleanDefaultTrue":0.152,"OCA\\OpenRegister\\Tests\\Db\\MagicMapperIntegrationTest::testSchemaWithStringDefault":0.153,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testGetCurrentUserIdReturnsStringOrNull":0,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testGetCurrentUserGroupsReturnsArray":0,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testIsAdminReturnsBool":0,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionNoAuthorizationReturnsTrue":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionNoAuthorizationForCreate":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionNoAuthorizationForUpdate":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionNoAuthorizationForDelete":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionPublicRuleGrantsAccess":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionPublicRuleForUnconfiguredAction":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionGroupRuleNoMatch":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionAuthenticatedRule":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionOwnerHasAccess":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionConditionalRulePublicGroup":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionConditionalRuleNoMatchBlock":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlNoAuthorization":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlPublicRule":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlUnconfiguredAction":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlGroupRule":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlConditionalRule":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlMultipleRules":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasConditionalRulesBypassingMultitenancyNoAuth":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasConditionalRulesBypassingMultitenancyPublicRule":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasConditionalRulesBypassingMultitenancyConditionalRule":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testSearchWithNoAuthorizationReturnsResults":0.16,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testSearchWithPublicAuthorizationReturnsResults":0.171,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testSearchWithRestrictedAuthorizationDoesNotCrash":0.176,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testSearchWithConditionalAuthorizationDoesNotCrash":0.176,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testCountWithPublicAuthorization":0.182,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testCountWithRestrictedAuthorization":0.162,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testFacetsWithPublicAuthorization":0.182,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionDifferentActions":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlWithOperatorMatch":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlWithInOperator":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testBuildRbacConditionsSqlWithExistsOperator":0.009,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testSearchWithRbacDisabledBypassesRestrictions":0.167,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testCountWithRbacDisabledBypassesRestrictions":0.162,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionWithObjectDataPublicConditional":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testHasPermissionWithObjectDataNotMatching":0.01,"OCA\\OpenRegister\\Tests\\Db\\MagicRbacHandlerIntegrationTest::testSearchAcrossMultipleTablesWithRbac":0.399,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperFindBySource":0.023,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperFindBySourceEmpty":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperDeleteBySource":0.011,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperGetLatestUpdatedTimestamp":0.011,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperGetLatestUpdatedTimestampNonExistent":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperCountAll":0.009,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperCountIndexed":0.01,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperCountUnindexed":0.01,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperCountVectorized":0.009,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperFindUnindexed":0.009,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperFindUnindexedWithLimitAndOffset":0.018,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperCountFileSourceSummaries":0.009,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkMapperCountFileSourceSummariesWithSearch":0.002,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFind":0.013,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFindNonExistent":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFindByWebhook":0.017,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFindByWebhookWithLimit":0.018,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFindByWebhookWithOffset":0.017,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFindAll":0.009,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFindAllWithLimitAndOffset":0.017,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFindFailedForRetry":0.01,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperFindFailedForRetryFutureOnly":0.01,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperInsertSetsCreated":0.01,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperGetStatisticsForAll":0.017,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogMapperGetStatisticsForSpecificWebhook":0.016,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookMapperFindAll":0.015,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookMapperFindAllWithLimit":0.021,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookMapperFindAllWithFilters":0.003,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookMapperFindAllWithIsNullFilter":0.003,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookMapperFind":0.012,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookMapperFindNonExistent":0.003,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookMapperFindEnabled":0.012,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookMapperFindForEvent":0.012,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testAgentMapperCanUserAccessAgentNonPrivate":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testAgentMapperCanUserAccessAgentPrivateOwner":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testAgentMapperCanUserAccessAgentPrivateInvited":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testAgentMapperCanUserModifyAgent":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindById":0.018,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindByIdNonExistent":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindAll":0.582,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindAllWithLimit":0.02,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindAllWithFilter":0.011,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindAllWithIsNullFilter":0.002,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindAllWithIsNotNullFilter":0.402,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindBySourceUrl":0.005,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperFindBySyncEnabled":0.005,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperUpdateSyncStatus":0.013,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperCreateFromArray":0.009,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationMapperUpdateFromArray":0.017,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationEntityHelpers":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationEntityToStringFallbacks":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationEntityIsValidUuid":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationEntityGetJsonFields":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperCountAllFiles":0.021,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperGetTotalFilesSize":0.007,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperGetFilesEmpty":0.008,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperGetFileNonExistent":0.002,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperGetFilesWithIds":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperFindUntrackedFiles":0.004,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperCountUntrackedFiles":0.032,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperGetFilesForObjectWithNoFolder":0.029,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFileMapperDepublishFile":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetStatistics":0.003,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetStatisticsWithRegisterId":0.03,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetStatisticsWithSchemaId":0.027,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetStatisticsWithArrayIds":0.026,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetStatisticsWithExclude":0.002,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetRegisterChartData":0.002,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetRegisterChartDataWithFilters":0.009,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetSchemaChartData":0.002,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetSchemaChartDataWithFilters":0.01,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetSizeDistributionChartData":0.007,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetSizeDistributionChartDataWithFilters":0.031,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetStatisticsGroupedBySchema":0.034,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetStatisticsGroupedBySchemaEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testStatisticsHandlerGetStatisticsGroupedBySchemaFillsMissing":0.002,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerSeparateLargeObjects":0.027,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerSeparateLargeObjectsAllSmall":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerHasJsonFilters":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerApplyCompositeIndexOptimizations":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerApplyCompositeIndexOptimizationsWithOrg":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerOptimizeOrderBy":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerAddQueryHints":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerAddQueryHintsWithRbac":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerProcessLargeObjectsIndividuallyEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testQueryOptimizationHandlerBulkOwnerDeclarationThrowsOnNoArgs":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFacetsHandlerGetSimpleFacetsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFacetsHandlerGetSimpleFacetsNoConfig":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFacetsHandlerGetFacetableFieldsFromSchemas":0.013,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testFacetsHandlerGetFacetableFieldsFromSchemasEmpty":0.003,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookEntityMatchesEvent":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookEntityMatchesEventWildcard":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookEntityArrayAccessors":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookEntityHydrate":0.001,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookEntityJsonSerialize":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogEntityPayloadArray":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testWebhookLogEntityJsonSerialize":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testAgentEntityHasInvitedUser":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testAgentEntityHydrate":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testAgentEntityJsonSerialize":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testChunkEntityJsonSerialize":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationEntityHydrateWithApplicationAlias":0,"OCA\\OpenRegister\\Tests\\Db\\MappersIntegrationTest::testConfigurationEntityHydrateWithEmptyJsonFields":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetQueryBuilder":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetMaxAllowedPacketSize":0.002,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsReturnsExpectedKeys":0.002,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsWithRegisterId":0.128,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsWithSchemaId":0.052,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsGroupedBySchema":0.05,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetRegisterChartData":0.002,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetSchemaChartData":0.002,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetSizeDistributionChartData":0.008,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testInsertAndFind":0.064,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testInsertEntityDirect":0.033,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testUpdateEntity":0.055,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDeleteEntity":0.049,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindDirectBlobStorageByUuid":0.056,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindDirectBlobStorageById":0.05,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindDirectBlobStorageNotFoundThrows":0.004,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindMultiple":0.097,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCountAll":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCountAllWithSchema":0.032,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCountAllWithRegister":0.027,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCountBySchemasEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCountBySchemas":0.03,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindBySchemasEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindBySchemas":0.045,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindBySchemasWithLimit":0.08,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindBySchema":0.046,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testHasJsonFilters":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testApplyCompositeIndexOptimizations":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testOptimizeOrderBy":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testAddQueryHints":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCalculateOptimalChunkSize":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSeparateLargeObjects":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testProcessLargeObjectsIndividually":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllReturnsArray":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllWithLimit":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetSimpleFacets":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetFacetableFieldsFromSchemas":0.009,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDeleteObjectsEmptyReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDeleteObjectsSoftDelete":0.06,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDeleteObjectsHardDelete":0.049,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testPublishObjectsEmptyReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testPublishObjects":0.059,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDepublishObjectsEmptyReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDepublishObjects":0.066,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testProcessInsertChunkReturnsArray":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testProcessUpdateChunkReturnsArray":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCalculateOptimalChunkSizeReturnsPositiveInt":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCalculateOptimalChunkSizeWithData":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSeparateLargeObjectsEmptyInput":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSeparateLargeObjectsWithSmallObjects":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testProcessLargeObjectsIndividuallyEmptyInput":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSetExpiryDate":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDeleteObjectsBySchemaReturnsArray":0.036,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDeleteObjectsByRegisterReturnsArray":0.033,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindByRelationReturnsArray":3.772,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testClearBlobObjectsReturnsArray":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testHasJsonFiltersWithDotNotation":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testHasJsonFiltersWithPlainFilters":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAcrossAllSourcesByUuid":0.05,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAcrossAllSourcesReturnsRegisterSchema":0.03,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAcrossAllSourcesNotFoundThrows":4.004,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindByRelationWithMatchingObject":0.034,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindByRelationEmptySearch":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSearchObjectsReturnsArray":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSearchObjectsWithLimitAndOffset":0.089,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCountSearchObjects":0.046,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCountAllWithSchemaAndRegister":0.026,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testPublishObjectsBySchema":0.025,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDeleteObjectsBySchemaSoftDelete":0.035,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testBulkOwnerDeclaration":0.178,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindDirectBlobStorageIncludeDeleted":0.033,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllWithSchemaFilter":0.047,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllWithRegisterFilter":0.044,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindMultipleMixedIdsAndUuids":0.09,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindMultipleEmptyReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testInsertDirectBlobStorage":0.031,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testUpdateDirectBlobStorage":0.053,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllDirectBlobStorage":0.045,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testLockAndUnlockObject":0.093,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsGroupedBySchemaWithData":0.03,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testUltraFastBulkSaveEmpty":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testLockObjectWithDefaultDuration":0.076,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testUnlockObjectNonExistentUuidReturnsFalse":0.001,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testLockObjectPersistsLockState":0.075,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testDeleteWithEvents":0.057,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testInsertWithEventsCreatesObject":0.048,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testUpdateWithEventsUpdatesObject":0.071,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSearchObjectsWithRegisterFilter":0.062,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSearchObjectsWithSchemaFilter":0.051,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testSearchObjectsWithOrderSort":0.104,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testCountSearchObjectsWithSchemaFilter":0.053,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllWithSortOrder":0.091,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllWithSearchString":0.037,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllWithIdsFilter":0.105,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllIncludeDeleted":0.036,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsWithBothRegisterAndSchema":0.027,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsWithArrayOfRegisterIds":0.043,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsWithArrayOfSchemaIds":0.045,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetRegisterChartDataWithRegisterId":0.026,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetSchemaChartDataWithSchemaId":0.028,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetSizeDistributionChartDataWithFilters":0.03,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindByRelationExcludingMagicTables":0.027,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindByRelationExactMatch":0.029,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetSimpleFacetsWithFacetConfig":0.029,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetFacetableFieldsFromSchemasWithData":0.035,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testProcessInsertChunkWithObjects":0.022,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllWithPublishedFilter":0.035,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindAllWithRegisterAndSchema":0.028,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testHasJsonFiltersWithNestedDotNotation":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testHasJsonFiltersWithMixedFilters":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testHasJsonFiltersWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testBulkOwnerDeclarationWithOwnerAndOrg":0.292,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testFindDirectBlobStorageByNumericString":0.033,"OCA\\OpenRegister\\Tests\\Db\\ObjectEntityMapperIntegrationTest::testGetStatisticsWithExclude":0.031,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testInsertSetsUuid":0.013,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testInsertSetsTimestamps":0.009,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testInsertSetsId":0.009,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindByUuid":0.011,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindByUuidNotFoundThrows":0.001,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindBySlug":0.01,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindBySlugNotFoundThrows":0.001,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindAllReturnsArray":0.015,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindAllRespectsLimit":0.018,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindAllRespectsOffset":0.042,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindByName":0.011,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindByNamePartialMatch":0.012,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindByNameNoResults":0.002,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindMultipleByUuidEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindMultipleByUuid":0.018,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindAllWithUserCount":0.033,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindByUserId":0.01,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindByUserIdNoResults":0.002,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testGetStatistics":0.01,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testUuidExistsTrue":0.009,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testUuidExistsFalse":0.001,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testUuidExistsExcludesId":0.009,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testUpdate":0.114,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testDelete":0.008,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testSaveNewOrganisation":0.008,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testSaveExistingOrganisation":0.014,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testAddUserToOrganisation":0.013,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testRemoveUserFromOrganisation":0.014,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindParentChainNoParent":0.01,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindParentChainWithParent":0.017,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindChildrenChainNoChildren":0.01,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindChildrenChainWithChildren":0.017,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testValidateParentAssignmentNullAllowed":0.009,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testValidateParentAssignmentSelfReferenceThrows":0.008,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testValidateParentAssignmentCircularReferenceThrows":0.019,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testValidateParentAssignmentNonexistentParentThrows":0.009,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testGetOrganisationHierarchyNoParent":0.009,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testGetOrganisationHierarchyWithParent":0.018,"OCA\\OpenRegister\\Tests\\Db\\OrganisationMapperIntegrationTest::testFindByUserIdMultipleOrgs":0.018,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindAllReturnsArray":0.003,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindAllRespectsLimit":0.001,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindAllRespectsOffset":0.006,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindAllWithFilter":0.016,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindAllWithIsNullFilter":0.003,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindAllWithIsNotNullFilter":0.003,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindById":0.01,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindByUuid":0.011,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindBySlug":0.012,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindCachesResult":0.01,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindNonExistentThrowsException":0.002,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindMultiple":0.019,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindMultipleSkipsMissing":0.012,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindMultipleOptimizedEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testFindMultipleOptimized":0.021,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testCreateFromArraySetsUuid":0.012,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testCreateFromArraySetsSlug":0.011,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testCreateFromArraySetsVersion":0.011,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testCreateFromArraySetsSourceDefault":0.011,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testUpdateFromArray":0.017,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testUpdateFromArrayIncrementsVersion":0.017,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testDeleteSucceedsWithNoAttachedObjects":0.01,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testGetIdToSlugMap":0.011,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testGetSlugToIdMap":0.013,"OCA\\OpenRegister\\Tests\\Db\\RegisterMapperIntegrationTest::testGetSchemasByRegisterIdEmptySchemas":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllReturnsArray":0.026,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllRespectsLimit":0.002,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllRespectsOffset":0.06,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllWithFilter":0.03,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllWithIsNullFilter":0.018,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllWithIsNotNullFilter":0.027,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindById":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindByUuid":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindBySlug":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindNonExistentThrowsException":0.002,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindCacheHit":0.011,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindCacheByUuidAfterIdLookup":0.011,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindWithNonNumericId":0.011,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindBySlugMethod":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindBySlugNoResults":0.001,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindBySlugWithPublishedParam":0.014,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindMultiple":0.02,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindMultipleSkipsMissing":0.012,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindMultipleEmptyArray":0,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindMultipleOptimizedEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindMultipleOptimized":0.018,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArraySetsUuid":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArraySetsSlug":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArraySetsVersion":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArraySetsDefaultVersion":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArraySetsDefaultSource":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayPreservesProperties":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithNullRequired":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayAutoPopulatesObjectNameField":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayAutoPopulatesDescriptionField":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayAutoPopulatesDutchFieldNames":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithValidConfigObjectNameField":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithInvalidConfigObjectNameFieldThrows":0,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithTwigTemplateConfigField":0.008,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithInvalidTwigTemplateConfigFieldThrows":0,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithPipeSeparatedConfigField":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithInvalidPipeSeparatedConfigFieldThrows":0,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayBuildsRequiredFromPropertyFlags":0.008,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayBuildsRequiredFromStringTrue":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayPreservesExplicitRequired":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithRefAsArray":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithRefAsInt":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithPropertyLevelNestedRef":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayGeneratesFacetConfigForCommonFields":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayGeneratesFacetForEnumProperty":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayGeneratesFacetForDateField":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayGeneratesFacetForDateFormatProperty":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayGeneratesFacetForExplicitFacetableProperty":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArraySkipsFacetForExplicitFacetableFalse":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayGeneratesFacetForBooleanType":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testUpdateFromArray":0.017,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testUpdateFromArrayIncrementsVersion":0.016,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testUpdateFromArrayWithExplicitVersion":0.016,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testDeleteSucceeds":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetIdToSlugMap":0.011,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetSlugToIdMap":0.011,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetRegisterCountPerSchema":0.002,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetRelatedReturnsArray":0.016,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetRelatedByIdInsteadOfEntity":0.021,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindExtendedByReturnsArray":0.014,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindExtendedByWithKnownUuidAndSlug":0.013,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindExtendedByWithActualExtension":0.023,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllExtendedByReturnsArray":0.003,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllExtendedByWithActualExtension":0.022,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaReturnsBool":0.017,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaByIdTrue":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaByUuidTrue":0.014,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaBySlugTrue":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaByJsonSchemaFormat":0.011,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaInNestedProperties":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaInArrayItems":0.012,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaFalse":0.012,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaSkipsNonArrayProperties":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaByUuidContainedInRef":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetPropertySourceMetadataReturnsArray":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetPropertySourceMetadataWithProperties":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetPropertySourceMetadataWithAllOfComposition":0.022,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testSchemaAllOfResolvesParentProperties":0.021,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testSchemaAllOfMergesRequired":0.021,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testInsertDoesNotAutoSetCreatedTimestamp":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testInsertDoesNotAutoSetUpdatedTimestamp":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testUpdateSetsUpdatedTimestamp":0.116,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllWithSearchConditionsReturnsArray":0.038,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testUpdateFromArrayPreservesExistingProperties":0.017,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateSchemaWithBooleanProperty":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateSchemaWithNumberProperty":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateSchemaWithArrayProperty":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateSchemasWithSameTitleThrowsUniqueConstraint":0.015,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllCountMatchesActualResults":0.043,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindBySlugWithLimit":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindBySlugWithOffset":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetRelatedWithReferencingSchema":0.024,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayEmptyProperties":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithNestedObjectProperties":0.009,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testCreateFromArrayWithSource":0.008,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testUpdateFromArrayReplacesProperties":0.015,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testUpdateFromArrayChangesTitle":0.016,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testHasReferenceToSchemaByIntIdTrue":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindExtendedByEmpty":0.014,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllWithSourceFilter":0.011,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindBySlugCaseInsensitive":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testGetRegisterCountPerSchemaReturnsArray":0.002,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindMultipleOptimizedSkipsMissing":0.01,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllWithPublishedParam":0.028,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindWithPublishedBypass":0.011,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindWithDifferentRbacFlagsCachesSeparately":0.012,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindWithMultitenancyDisabled":0.012,"OCA\\OpenRegister\\Tests\\Db\\SchemaMapperIntegrationTest::testFindAllWithSearchConditionsAndParams":0.011,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindWithRegisterAndSchemaUsesOrmSource":0.179,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindWithoutRegisterSchemaUsesBlobSource":0.027,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testInsertReturnsObjectEntity":0.188,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testInsertAndFindRoundTrip":0.185,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testUpdateObject":0.184,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testUpdateObjectWithoutOldEntity":0.208,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testUpdateObjectWithExplicitOldEntity":0.19,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testUpdateObjectWithAutoResolveRegisterSchema":0.193,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testDeleteObject":0.196,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testDeleteNonObjectEntityThrows":0,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testInsertWithAutoResolveRegisterSchema":0.162,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAllWithRegisterSchemaReturnsArray":0.176,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAllWithoutContextUsesBlob":0.002,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAllWithFilters":0.178,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAllWithNullPublishedFilter":0.181,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindByUuidSetsOrmSource":0.165,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAllBlobSetsSourceOnEntities":0.023,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindMultipleReturnsArray":0.357,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindMultipleWithUuids":3.133,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindMultipleEmptyReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindBySchemaReturnsArray":0.237,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetStatisticsReturnsExpectedKeys":0.002,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetStatisticsWithRegisterFilter":0.191,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetStatisticsWithSchemaFilter":0.236,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetStatisticsWithBothRegisterAndSchemaFilter":0.182,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetRegisterChartData":0.002,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetSchemaChartData":0.002,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetRegisterChartDataWithFilters":0.179,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetSchemaChartDataWithFilters":0.181,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetSimpleFacetsEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetSimpleFacetsWithRegisterSchema":0.186,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetSimpleFacetsViaAtSelfKeys":0.157,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetSimpleFacetsWithMultipleSchemas":0.374,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetFacetableFieldsFromSchemas":0.009,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testCountAllReturnsInt":0.002,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testCountAllWithSchemaFilter":0.159,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testCountAllWithRegisterAndSchemaFilter":0.161,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testCountAllWithFiltersArray":0.154,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsReturnsArray":0.002,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsWithRegisterSchemaRoutesMagic":0.201,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsWithAtSelfKeys":0.184,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsWithRegisterFilter":0.194,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsWithLimit":0.205,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testCountSearchObjectsReturnsInt":0.002,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testCountSearchObjectsWithRegisterSchemaRoutesMagic":0.171,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testCountSearchObjectsWithAtSelfKeys":0.173,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedReturnsExpectedStructure":0.002,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedWithData":0.161,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedWithRegisterSchemaRoutesMagic":0.195,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedWithMultipleSchemas":0.374,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedWithSchemaAsArray":0.377,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedBlobFallback":0.035,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testLockAndUnlockObject":0.05,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testLockObjectWithDefaultDuration":0.056,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetQueryBuilder":0,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetMaxAllowedPacketSize":0.001,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testDeleteObjectsEmptyReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testPublishObjectsEmptyReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testDepublishObjectsEmptyReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testDeleteObjectsWithActualUuids":0.035,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testDeleteObjectsWithHardDelete":0.033,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testPublishObjectsWithActualUuids":0.037,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testDepublishObjectsWithActualUuids":0.047,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAcrossAllSourcesFindsObject":4.178,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAcrossAllSourcesNotFoundThrows":3.794,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAcrossAllSourcesBlobReturnsRegisterAndSchema":0.033,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testInsertNonObjectEntityThrows":0,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testUltraFastBulkSaveWithRegisterSchema":0.147,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testUltraFastBulkSaveWithAutoResolve":0.142,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testUltraFastBulkSaveEmptyReturnsEmpty":0.001,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testCountSearchObjectsWithData":0.024,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testGetSimpleFacetsWithData":0.159,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindAllWithSorting":0.201,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testFindBySchemaAfterInsert":0.161,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testInsertIntoBlobStorageWithRegisterSchema":0.024,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedWithIds":2.178,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedWithSearchInMagicTable":0.174,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedWithRelationsContains":1.781,"OCA\\OpenRegister\\Tests\\Db\\UnifiedObjectMapperIntegrationTest::testSearchObjectsPaginatedWithMultipleRegisters":0.393,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkInsertSingleObject":0.04,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkInsertMultipleObjects":0.092,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkInsertEmptyArrayReturnsEmpty":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkInsertObjectHasCorrectDataInDb":0.037,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkUpdateWithObjectEntityUnifiesFormat":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkMixedInsertAndUpdate":0.07,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueMissingObjectPropertyThrows":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkInsertObjectWithStringObjectThrows":0.015,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testUnifyObjectFormatsAutoGeneratesUuid":0.017,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkInsertObjectExtractsNameFromNaam":0.039,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBulkInsertObjectWithDateTimeFields":0.039,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueWithAtSelfMetadata":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testConvertDateTimeToMySQLFormatIso8601":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testConvertDateTimeToMySQLFormatAlreadyMySQL":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testConvertDateTimeToMySQLFormatFallbackForNonString":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGetJsonColumnsReturnsExpectedColumns":0.016,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testMapObjectColumnsToDatabaseFiltersValidColumns":0.015,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testMapObjectColumnsToDatabaseAddsRequiredColumns":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueForUuid":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueForVersion":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueForObjectColumn":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueForJsonColumns":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueForNameFromNaam":0.015,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueForNameFallback":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueForPublishedDatetime":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractColumnValueForNullPublished":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testCreateEntityFromDataReturnsEntity":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testCreateEntityFromDataReturnsEntityWithMinimalData":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGetValueFromPathSimpleField":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGetValueFromPathNestedField":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGetValueFromPathMissingField":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGetValueFromPathDeepNested":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGetValueFromPathNumericValue":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractMetadataValueSimplePath":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractMetadataValueFallbackChain":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractMetadataValueFallbackChainFirstMatch":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractMetadataValueFallbackChainAllEmpty":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractMetadataValueTwigTemplate":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractMetadataValueTwigTemplateMissingField":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testExtractMetadataValueTwigTemplateAllMissing":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessTwigLikeTemplateWithFallbacks":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessTwigLikeTemplateNoMatches":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessFieldWithFallbacksEmptyFields":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessMapFilter":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessMapFilterNoMatch":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessMapFilterEmptyField":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessIfFilledFilterFieldFilled":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessIfFilledFilterFieldEmpty":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessIfFilledFilterFieldEmptyString":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessIfFilledFilterSingleValue":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessTwigLikeTemplateWithMapFilter":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessTwigLikeTemplateWithIfFilledFilter":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessTwigLikeTemplateWithArrayValueConverted":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testCreateSlug":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testCreateSlugFromValue":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGenerateSlugFromSlugFromConfig":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGenerateSlugFromTitleFieldConfig":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGenerateSlugFromCommonFields":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testGenerateSlugFallbackToSchemaName":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testHydrateObjectMetadataWithConfiguredFields":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testHydrateObjectMetadataWithCustomNameField":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testHydrateObjectMetadataWithSlugField":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testHydrateObjectMetadataWithDescriptionField":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testHydrateObjectMetadataWithSummaryField":0.016,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testHydrateObjectMetadataFallbackFields":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testHydrateObjectMetadataNestedObjectKey":0.015,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyDataUri":0.015,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyUrlWithFileExtension":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyUrlWithoutExtension":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyBase64Long":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyPlainStringNotFile":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyNullValue":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyIntValue":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertySchemaBasedFileType":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertySchemaBasedArrayFileType":0.017,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertySchemaBasedNonFileType":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertySchemaBasedMissingProperty":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyFileObject":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyArrayOfDataUris":0.015,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyArrayOfUrls":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyArrayOfBase64":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyArrayOfFileObjects":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFileObjectValid":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFileObjectMissingId":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFileObjectMissingTitleAndPath":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFileObjectWithPath":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testParseFileDataFromDataUri":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testParseFileDataFromBase64":0.014,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testParseFileDataInvalidBase64Throws":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testParseFileDataInvalidDataUriThrows":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testValidateFileAgainstConfigPassesWithNoRestrictions":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testValidateFileAgainstConfigBlocksMimeType":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testValidateFileAgainstConfigBlocksOversize":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testValidateFileAgainstConfigWithIndex":0.015,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBlockExecutableFilesByExtension":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBlockExecutableFilesByMagicBytes":0.018,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBlockExecutableFilesByMimeType":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBlockExecutableFilesSafeFilePassesThrough":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBlockExecutableFilesShellScript":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBlockExecutableFilesElfBinary":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBlockExecutableFilesShebangInContent":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testProcessUploadedFilesSkipsErrors":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyUrlWithImageExtension":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyUrlWithDocxExtension":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyArrayWithUrlInside":0.013,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testIsFilePropertyArrayWithNonFiles":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testBlockExecutableFilesPhpTag":0.011,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testValidateFileAgainstConfigAllowsCorrectMimeType":0.012,"OCA\\OpenRegister\\Tests\\Service\\BulkAndHandlersIntegrationTest::testValidateFileAgainstConfigAllowsWithinSizeLimit":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersIndex":0.022,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersIndexWithPagination":0.01,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersIndexWithPageParam":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersIndexWithExtendSchemas":0.116,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersIndexWithExtendStats":4.201,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersIndexWithExtendSchemasAndStats":4.556,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersIndexWithExtendAsString":0.042,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersShow":0.01,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersShowWithStats":0.062,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersShowWithExtendString":0.09,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersCreate":0.145,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersUpdate":0.061,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersPatch":0.058,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersSchemas":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersSchemasNotFound":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersObjects":0.01,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersDestroy":0.021,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testRegistersDestroyNotFound":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasIndex":0.145,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasIndexWithPagination":0.014,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasIndexWithPage":0.014,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasIndexWithStats":0.264,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasIndexWithExtendString":0.111,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasShow":0.014,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasShowWithStats":0.062,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasShowNotFound":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasCreate":0.016,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasUpdate":0.024,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasPatch":0.018,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSchemasDestroy":0.023,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsIndex":0.013,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsIndexWithPagination":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsIndexWithPagePagination":0.007,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsIndexUnauthenticated":0.007,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsCreate":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsCreateWithConfiguration":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsCreateMissingName":0.01,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsCreateMissingQuery":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsCreateUnauthenticated":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsShowUnauthenticated":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsShowNotFound":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsUpdateUnauthenticated":0.007,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsUpdateMissingName":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsUpdateMissingQuery":0.007,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsUpdateNotFound":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsPatchUnauthenticated":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsPatchNotFound":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsDestroyUnauthenticated":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsDestroyNotFound":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testViewsCrudCycle":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsIndex":0.05,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsLoad":0.031,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsUpdate":0.031,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsStats":7.164,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsGetStatistics":7.336,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsGetObjectService":0.007,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsGetConfigurationService":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsGetDatabaseInfo":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsGetDatabaseInfoRefresh":0.014,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsRefreshDatabaseInfo":0.016,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsGetVersionInfo":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsGetSearchBackend":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsUpdateSearchBackendEmpty":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsUpdateSearchBackendValid":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsUpdatePublishingOptions":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsRebase":0.01,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsSemanticSearchEmptyQuery":0.007,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsHybridSearchEmptyQuery":0.007,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsTestSetupHandlerSolrDisabled":0.007,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsReindexInvalidBatchSize":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSettingsReindexNegativeMaxObjects":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailIndex":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailIndexWithPagination":0.01,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailIndexWithPage":0.012,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailShowNotFound":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailStatistics":0.389,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailPopularTerms":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailActivity":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailRegisterSchemaStats":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailUserAgentStats":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailCleanupInvalidDate":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailCleanupWithoutDate":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailCleanupWithValidDate":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailDestroyNotFound":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailDestroyMultiple":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailClearAll":0.008,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailExportJson":0.018,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailExportCsv":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailIndexWithDateFilters":0.017,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testSearchTrailIndexWithSort":0.012,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsIndex":0.013,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsShowNotFound":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsCreateMissingFields":0.009,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsCreate":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsUpdateNotFound":0.012,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsDestroyNotFound":0.019,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsCrudCycle":0.01,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsTestNotFound":0.011,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsLogsNotFound":0.014,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsLogStatsNotFound":0.013,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsAllLogs":0.02,"OCA\\OpenRegister\\Tests\\Service\\ControllersIntegrationTest::testEndpointsAllLogsWithFilter":0.01,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testClearCaches":0.01,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testClearCachesIdempotent":0.018,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testGetRecommendedWarmupModeSafe":0.011,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testGetRecommendedWarmupModeBalanced":0.009,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testGetRecommendedWarmupModeFast":0.012,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testGetRecommendedWarmupModeBoundary1000":0.014,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testGetRecommendedWarmupModeBoundary10000":0.011,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testGetRecommendedWarmupModeZero":0.01,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testScheduleSmartSolrWarmupEmpty":0.01,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testScheduleSmartSolrWarmupWithSummary":0.015,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvCreatesObjects":0.165,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvSingleRow":0.16,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvEmptyNoDataRows":0.011,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvIntegerCoercion":0.164,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvBooleanAndArrayTypes":0.181,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvIgnoresUnderscoreColumns":0.163,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvExtraColumnsPreserved":0.156,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvIncludesSchemaInfo":0.172,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvDeduplication":0.196,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvWithPublish":0.171,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvPerformanceMetrics":0.167,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvNonexistentFileThrows":0.012,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvWithoutSchemaThrows":0.013,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvManyRows":0.177,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvSkipsEmptyRows":0.166,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvPartialData":0.171,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvQuotedFieldsWithCommas":0.175,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvNumberType":0.181,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvObjectTypeJson":0.168,"OCA\\OpenRegister\\Tests\\Service\\ImportServiceIntegrationTest::testImportFromCsvWithoutPublish":0.187,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasAll":0.494,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasVersion":0.013,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasInfoSection":0.076,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasServersSection":0.076,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasComponents":0.097,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasForSpecificRegisterWithDescription":0.026,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasForRegisterWithoutDescription":0.026,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasIncludesTags":0.079,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasGeneratesCrudPaths":0.067,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasCollectionPathMethods":0.018,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasItemPathMethods":0.016,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasSecuritySchemes":0.091,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasPaths":0.163,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithVariousPropertyTypes":0.027,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithEnumProperty":0.024,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithAllOfProperty":0.026,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithOneOfProperty":0.027,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithRefProperty":0.03,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithBareRefProperty":0.029,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithEmptyAllOf":0.029,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithInvalidType":0.029,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithBooleanRequired":0.033,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithArrayTypeNoItems":0.031,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithEmptyEnum":0.032,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithNestedProperties":0.038,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithRbacGroups":0.036,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithRbacGroupObjects":0.036,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithPropertyLevelAuth":0.035,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasOperationsHaveOperationId":0.026,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasAllRegisters":0.267,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasCollectionHasQueryParameters":0.024,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasPathsAlwaysArray":0.071,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithMalformedItemsList":0.029,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasPropertyWithNoTypeNoRef":0.025,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasSanitizeSchemaName":0.024,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithAnyOfProperty":0.024,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithEmptyRef":0.025,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasGetOperationResponse":0.018,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasDeleteOperationResponse":0.021,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasPostOperationRequestBody":0.018,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithReadOnlyWriteOnlyProperties":0.031,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasWithPropertyFormats":0.025,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasServerUrlContainsApiPath":0.013,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasStripsInternalProperties":0.026,"OCA\\OpenRegister\\Tests\\Service\\OasServiceIntegrationTest::testCreateOasScopeDescriptions":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateRequiredFieldsWithValidObjects":0.024,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateRequiredFieldsMissingSelf":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateRequiredFieldsMissingRegister":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateRequiredFieldsMissingSchema":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateRequiredFieldsEmptyRegister":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateRequiredFieldsSelfNotArray":0.025,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateRequiredFieldsMultipleObjectsSecondFails":0.027,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateObjectsBySchemaWithValidObjects":16.021,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateObjectsBySchemaWithInvalidCallback":8.365,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateSchemaObjectsWithValidObjects":16.79,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateSchemaObjectsWithFailingCallback":10.364,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testApplyInversedByFilterReturnsEmptyArray":0.067,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateAndSaveObjectsBySchemaInvalidIds":0.134,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateAndSaveObjectsBySchemaWithRealData":17.821,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testValidateAndSaveObjectsBySchemaWithLimitOffset":23.793,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testExtractAllRelationshipIdsEmpty":7.997,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testExtractAllRelationshipIdsSingleRelation":7.923,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testExtractAllRelationshipIdsArrayRelation":8.17,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testExtractAllRelationshipIdsDeduplicates":15.455,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testExtractAllRelationshipIdsCircuitBreaker":7.79,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBulkLoadRelationshipsBatchedEmpty":0.02,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBulkLoadRelationshipsBatchedWithRealObjects":15.592,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBulkLoadRelationshipsBatchedCapsAt200":0.025,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testLoadRelationshipChunkOptimizedEmpty":0.018,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testLoadRelationshipChunkOptimizedWithData":7.857,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetContractsEmptyContracts":9.187,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetContractsNonexistentObject":0.033,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetContractsWithPagination":7.985,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetUsesNoRelations":7.817,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetUsesNonexistentObject":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetUsedByReturnsStructure":25.17,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetUsedByNonexistentObject":0.023,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testExtractRelatedDataEmpty":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetsForObjectsEmptyConfig":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetsForObjectsStringConfig":0.025,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetsForObjectsArrayConfig":16.445,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetableFieldsReturnsStructure":0.027,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetableFieldsNoSchemaFilter":0.054,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetMetadataFacetableFields":0.023,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetCountNoFacets":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetCountWithFacets":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetCountWithNonArrayFacets":0.02,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetFacetsForObjectsWithFallback":0.025,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBuildSearchQueryBasic":0.022,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBuildSearchQueryDotMangling":0.024,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBuildSearchQueryMetadataFields":0.03,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBuildSearchQueryWithIds":0.027,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBuildSearchQueryNormalizesIdsString":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBuildSearchQueryPublishedFilter":0.025,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBuildSearchQueryStripsSystemParams":0.022,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testBuildSearchQueryArrayRegisterSchema":0.023,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testCleanQueryOrdering":0.027,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testCleanQueryOrderingAsc":0.022,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testCleanQuerySuffixOperators":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testCleanQueryInOperator":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testCleanQueryIsNull":0.02,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testCleanQueryNormalizesDoubleUnderscores":0.02,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testIsSolrAvailable":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testIsSearchTrailsEnabled":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testLogSearchTrailDoesNotThrow":0.021,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testApplyViewsToQueryEmptyViews":0.021,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testApplyViewsToQueryNonexistentView":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasPermissionRbacDisabled":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasPermissionAdminUser":0.018,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasPermissionObjectOwner":0.017,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionAdmin":0.018,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionNoAuthorization":0.02,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionEmptyAuthorization":0.018,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionActionNotInAuth":0.021,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionMatchingGroup":0.023,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionNonMatchingGroup":0.021,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionOwnerOverride":0.027,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionComplexEntry":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testHasGroupPermissionComplexEntryNoMatch":0.027,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testEvaluateMatchConditionsAllPass":0.02,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testEvaluateMatchConditionsFails":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testEvaluateMatchConditionsOrganisationVariable":0.02,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testEvaluateMatchConditionsNoActiveOrg":0.024,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testEvaluateMatchConditionsResolvedRelation":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testCheckPermissionThrowsOnDenied":0.037,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetAuthorizedGroupsNoAuth":0.022,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetAuthorizedGroupsWithAuth":0.022,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetAuthorizedGroupsMissingAction":0.021,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testFilterObjectsForPermissionsRbacDisabled":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testGetActiveOrganisationForContext":0.02,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedReturnsStructure":15.297,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedWithPage":0.024,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedWithOffset":15.295,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedIncludesMetrics":0.026,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedWithExtend":7.759,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedLimitZero":7.867,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsReturnsEntities":7.673,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsWithCount":7.652,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testCountSearchObjects":14.876,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedWithFacets":24.776,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedWithFacetable":0.024,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedSourceDatabase":0.023,"OCA\\OpenRegister\\Tests\\Service\\ObjectHandlersIntegrationTest::testSearchObjectsPaginatedStoresQueryMetadata":0.027,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetRegisterWithId":0.007,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetSchemaWithId":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetRegisterWithObject":0.006,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetSchemaWithObject":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetRegisterWithSlug":0.006,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetSchemaWithSlug":0.006,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetRegisterWithUuid":0.007,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetSchemaWithUuid":0.007,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testContextMethodChaining":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetObjectReturnsObjectEntityOrNull":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetRegisterReturnsIntAfterSet":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetSchemaReturnsIntAfterSet":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetSchemaWithNonexistentIdThrows":0.006,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectCreate":0.15,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectWithSpecificUuid":0.125,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectStoresRegisterAndSchema":0.14,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectStoresData":0.136,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectWithArrayData":0.149,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectSilent":0.098,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveAndFindObject":0.165,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testFindByStringId":0.187,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testFindSilent":0.177,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testFindDerivesSchemaFromObject":0.153,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectUpdate":0.241,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testUpdatePreservesUuid":0.18,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectWithObjectEntityInput":0.183,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testDeleteObject":7.988,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testDeleteObjectDerivesSchema":8.188,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testFindAllEmpty":0.01,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testFindAllAfterCreate":0.15,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testFindAllWithLimit":0.285,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSearchObjectsPaginatedWithOffset":0.219,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testFindAllWithExtendString":0.143,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testFindByRelationsEmpty":4.69,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testCountEmpty":0.006,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSearchPaginatedTotalIncreasesAfterCreate":0.172,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSearchObjectsPaginated":0.166,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testCountSearchObjectsReturnsInt":0.008,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testBuildSearchQuery":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testBuildObjectSearchQuery":0.007,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testRenderEntity":0.198,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetExtendedObjects":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetCreatedSubObjects":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testClearCreatedSubObjects":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetCacheHandler":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetDeleteHandler":0.006,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectsBulk":0.104,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testDeleteObjectsBulk":0.262,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testPublishObjectsBulk":0.205,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testDepublishObjectsBulk":0.178,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSetObjectWithEntity":0.148,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testSaveObjectWithHardValidation":0.125,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testDeleteObjectsBySchema":0.176,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testDeleteObjectsByRegister":0.174,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testPublishObjectsBySchema":0.163,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetObjectUsesEmpty":2.996,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetObjectUsedByEmpty":17.409,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testVectorizeBatchObjectsThrowsDisabled":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetVectorizationStatisticsThrowsDisabled":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetVectorizationCountThrowsDisabled":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testExportObjectsThrowsDisabled":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testImportObjectsThrowsDisabled":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testDownloadObjectFilesThrowsDisabled":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testCreateObject":0.106,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetFacetsForObjects":0.163,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testGetFacetableFields":0.006,"OCA\\OpenRegister\\Tests\\Service\\ObjectServiceIntegrationTest::testValidateObjectsBySchema":0.149,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexReturnsEmptyResults":0.017,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexReturnsObjects":0.206,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexWithLimit":0.216,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexWithPage":0.232,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexWithSearch":0.287,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexInvalidRegisterReturns404":0.008,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexInvalidSchemaReturnsError":0.011,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexWithEmptyTrue":0.148,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexWithExtend":0.173,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexWithRegisterSlug":0.179,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testIndexWithOrder":0.227,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowReturnsObject":0.152,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowWithExtendSchema":0.155,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowWithExtendRegister":0.147,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowWithLegacyExtendFormat":0.159,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowWithFields":0.161,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowWithUnset":0.183,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowWithEmptyTrue":0.187,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowNotFoundReturns404":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testShowInvalidRegisterReturns404":0.211,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testCreateReturns201":0.163,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testCreateWithAllPropertyTypes":0.161,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testCreateInvalidRegisterReturns404":0.008,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testCreateInvalidSchemaReturnsError":0.01,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testCreateFiltersReservedParams":0.116,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testCreateWithSlugs":0.362,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testUpdateReturns200":4.192,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testUpdateNotFoundReturns404":0.016,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testUpdateInvalidRegisterReturns404":0.151,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testDestroyReturns204":8.008,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testDestroyWithInvalidRegister":0.007,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testDestroyNonexistentObject":4.123,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPatchReturns200":4.263,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPatchNotFoundReturns404":0.019,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPatchInvalidRegisterReturns404":0.158,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPostPatchReturns200":4.281,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPostPatchInvalidRegisterReturns404":0.008,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPostPatchNotFoundReturns404":0.017,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPublishReturns200":4.469,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPublishWithDate":4.085,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testPublishNotFoundReturnsError":4.078,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testDepublishReturns200":8.244,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testLogsReturnsAuditTrail":0.172,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testLogsNotFoundReturns404":0.012,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testContractsReturnsPaginated":0.183,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testUsesReturnsResult":2.819,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testUsedReturnsResult":17.176,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testCanDeleteReturns200":4.303,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testCanDeleteNotFoundReturns404":4.038,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testMergeWithoutTargetReturns400":0.149,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testMergeWithoutObjectReturns400":0.15,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testMigrateWithoutSourceReturns400":0.005,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testMigrateWithoutTargetReturns400":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testMigrateWithoutObjectsReturns400":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testMigrateWithoutMappingReturns400":0.004,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testObjectsReturnsResults":0.14,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testObjectsWithRegisterSchemaFilter":0.148,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testLockAndUnlock":4.213,"OCA\\OpenRegister\\Tests\\Service\\ObjectsControllerIntegrationTest::testNoUserMeansNotAdmin":0.137,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testEnsureDefaultOrganisation":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testEnsureDefaultOrganisationIdempotent":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testGetOrganisationSettingsOnly":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testGetDefaultOrganisationUuid":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testCreateOrganisation":0.01,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testCreateOrganisationWithUuid":0.006,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testCreateOrganisationInvalidUuid":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testCreateOrganisationGeneratesSlug":0.006,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testHasAccessToOrganisationNonexistent":0.001,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testGetUserOrganisationStats":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testClearDefaultOrganisationCache":0.001,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testClearCache":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testGetOrganisationForNewEntity":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testGetDefaultOrganisationId":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testSetDefaultOrganisationId":0.016,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testGetUserOrganisations":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testGetActiveOrganisation":0,"OCA\\OpenRegister\\Tests\\Service\\OrganisationServiceIntegrationTest::testGetUserActiveOrganisations":0,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testIsValidOnDeleteActionValid":0.007,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testIsValidOnDeleteActionCaseInsensitive":0.007,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testIsValidOnDeleteActionInvalid":0.007,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testCanDeleteNoReferences":7.997,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testHasIncomingOnDeleteReferencesNonexistent":0.008,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testCanDeleteNullSchema":0.007,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testDeletionAnalysisEmpty":0.009,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testDeletionAnalysisToArray":0.009,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testDeletionAnalysisWithBlockers":0.01,"OCA\\OpenRegister\\Tests\\Service\\ReferentialIntegrityServiceIntegrationTest::testLogRestrictBlock":0.011,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testSetUltraPreloadCache":0.007,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testGetUltraCacheSizeEmpty":0.008,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testSetUltraPreloadCacheWithObjects":0.008,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testClearCache":0.008,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testClearCacheResetsObjectsCache":0.008,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testGetObjectsCacheReturnsArray":0.008,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityBasic":7.846,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityPreservesAllProperties":9.031,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityReturnsObjectEntity":7.683,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithFieldsFilter":7.695,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithMultipleFields":7.428,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithFilterMatching":7.869,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithFilterNotMatching":7.515,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithUnset":7.516,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithMultipleUnset":7.335,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithDepthZero":7.572,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithHighDepth":9.475,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityCircularReferenceDetection":10.425,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithEmptyExtend":11.684,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithExtendString":9.844,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithExtendArray":11.235,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendWithRelatedObject":32.954,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendSelfSchema":8.081,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendSelfRegister":8.548,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendShorthandSchema":9.9,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendAll":8.281,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithPreloadedRegisterCache":7.992,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithPreloadedSchemaCache":9.693,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityTwiceReturnsSameResult":8.149,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesEmpty":0.012,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesSingleObject":7.917,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesMultipleObjects":37.994,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesWithStringExtend":7.516,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesWithFieldsFilter":9.692,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesWithUnset":9.265,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesWithExtendRelatedObjects":23.092,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithNullExtend":9.469,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityFilterAndFieldsCombined":7.504,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityUnsetAndFieldsCombined":9.493,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testUltraPreloadCacheWithRealObject":9.459,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendSelfSchemaPopulatesData":7.613,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendSelfRegisterPopulatesData":7.575,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendBothSchemaAndRegister":7.751,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityFilterEmptiesObject":7.49,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityFilterOnNonExistentProperty":7.816,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityUnsetNonExistentProperty":7.658,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendAllWithRelatedUuids":17.554,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendRegisterShorthand":7.917,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityAtDepthLimit":8.013,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithPreloadedObjects":18.492,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesWithStringFilter":7.719,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesWithStringUnset":7.372,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesWithStringFields":7.799,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesExtendAll":16.301,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendArrayOfUuids":33.977,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityPopulatesObjectsCache":27.022,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendWithNonExistentUuid":10.069,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendWithUrlValue":7.666,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityTripleCombination":7.504,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithInversedBySchema":20.892,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesLargeBatch":82.962,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesWithArrayExtend":8.322,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendCommaString":7.71,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithNestedObjectProperty":8.428,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityWithMinimalData":9.318,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntityExtendWithUltraCache":22.59,"OCA\\OpenRegister\\Tests\\Service\\RenderObjectIntegrationTest::testRenderEntitiesCombinedOptions":7.997,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveSchemaReferenceEmpty":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveSchemaReferenceNumericId":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveSchemaReferenceUuid":0.019,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveSchemaReferenceSlug":0.02,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveSchemaReferenceJsonPath":0.019,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveSchemaReferenceUrl":0.022,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveSchemaReferenceWithQueryParams":0.017,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveSchemaReferenceNonexistent":0.02,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveRegisterReferenceEmpty":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveRegisterReferenceNumericId":0.016,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveRegisterReferenceUuid":0.014,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveRegisterReferenceSlug":0.015,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveRegisterReferenceUrl":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveRegisterReferenceWithQueryParams":0.015,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testResolveRegisterReferenceNonexistent":0.111,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsReferenceUuid":0.009,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsReferenceUuidNoDashes":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsReferencePrefixedUuid":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsReferenceObjectsUrl":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsReferenceApiUrl":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsReferenceNumericId":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsReferenceNonReference":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsReferenceZero":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testScanForRelationsEmptyData":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testScanForRelationsSkipsMetadata":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testScanForRelationsDetectsUuids":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testScanForRelationsDetectsArrayOfReferences":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testScanForRelationsNestedData":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testScanForRelationsNestedDataWithSchemaRef":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testUpdateObjectRelationsNoRelations":8.185,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testUpdateObjectRelationsWithUuidReference":7.677,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCascadeObjectsNoInversedBy":7.861,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCascadeObjectsWithInversedBySingle":7.731,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCascadeObjectsWithInversedByArray":7.829,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCascadeObjectsWithEmptyPropData":9.403,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testHandleInverseRelationsWriteBack":7.914,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCascadeSingleObjectReturnsNull":8.535,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsValidObjects":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsGeneratesUuid":0.014,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsPreservesProvidedId":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsMissingRegister":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsMissingSchema":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsInvalidSchemaId":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsWithSelfStructure":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsWithObjectRegisterSchema":0.014,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsScansRelations":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsMixedValidInvalid":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testTransformObjectsWithPresetRelations":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsSimpleRequestBasic":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsSimpleRequestComplexExtend":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsSimpleRequestWithFacets":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsSimpleRequestLargeLimit":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsSimpleRequestManyFilters":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsSimpleRequestStringExtend":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsSimpleRequestTwoExtends":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testIsSimpleRequestFacetableFlag":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testOptimizeRequestForPerformanceFastPath":0.014,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testOptimizeRequestForPerformanceWithExtend":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testOptimizeExtendQueriesString":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testOptimizeExtendQueriesEmptyString":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testOptimizeExtendQueriesArray":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testExtractRelatedDataEmpty":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testExtractRelatedDataWithUuids":17.095,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testExtractRelatedDataNoInclude":7.981,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testExtractRelatedDataSkipsNonEntities":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testGetFacetCountWithFacets":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testGetFacetCountNoFacets":0.016,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testGetFacetCountStringFacets":0.016,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCalculateTotalPages":0.015,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCalculateExtendCountArray":0.016,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCalculateExtendCountString":0.017,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCalculateExtendCountEmptyString":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCalculateExtendCountNull":0.014,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCalculateExtendCountInteger":0.017,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testGetCachedEntitiesUsesFallback":0.015,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testPreloadCriticalEntities":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testDeleteSoftDelete":7.807,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testDeleteObjectByUuid":9.603,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testDeleteObjectCascadeSubDeletion":7.909,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCanDelete":8.113,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testDeleteWithArrayInput":4.171,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testHandlePreValidationCascadingNoInversedBy":0.016,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testHandlePreValidationCascadingGeneratesUuid":0.174,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testHandlePreValidationCascadingEmptyProperty":0.028,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCreateRelatedObjectNullRef":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCreateRelatedObjectEmptyRef":0.017,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCreateRelatedObjectInvalidSchemaRef":0.024,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testCreateRelatedObjectNonPathRef":0.016,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testSaveObjectsEmpty":0.016,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testSaveObjectsSingle":0.145,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testSaveObjectsMultiple":0.135,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testSaveObjectsDeduplication":0.135,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testSaveObjectsNoDeduplication":0.131,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testBulkOperationsSaveObjects":0.136,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testBulkOperationsDeleteObjectsEmpty":0.017,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testBulkOperationsPublishObjectsEmpty":0.019,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testBulkOperationsDepublishObjectsEmpty":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testProcessObjectsChunkEmpty":0.017,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testProcessObjectsChunkValid":0.163,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testProcessObjectsChunkWithIds":0.163,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testProcessObjectsChunkMixed":0.13,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectHandlersIntegrationTest::testProcessObjectsChunkTimingCapture":0.147,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testGetCreatedSubObjectsEmpty":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testClearCreatedSubObjects":0.008,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testClearAllCaches":0.008,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testTrackCreatedSubObject":0.007,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testTrackCreatedSubObjectOverwritesSameUuid":0.007,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsEmpty":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsFlatDataNoRelations":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithUuid":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithUrl":0.009,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithNestedArray":0.009,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithSchema":0.008,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyAlwaysDefaultsNoDefaults":0.007,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyAlwaysDefaultsPreservesExistingData":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyAlwaysDefaultsWithAlwaysBehaviorSchema":0.031,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsNoDefaults":0.011,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsWithSchemaDefaults":0.018,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsPreservesExplicitValues":0.009,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsEmptyData":0.01,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectCreatesNewObject":7.88,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectSetsRegisterAndSchema":7.68,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectMinimalData":8.738,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithStringProperty":8.75,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithIntegerProperty":8.186,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithBooleanProperty":7.9,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithBooleanFalse":7.596,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithArrayProperty":7.983,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithEmptyArray":7.949,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithNumberProperty":7.889,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithObjectProperty":7.729,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithAllPropertyTypes":7.739,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithProvidedUuid":7.754,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithSelfId":8.048,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectSetsCreatedTimestamp":8.115,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectUpdatesExistingByUuid":7.803,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectUpdatePreservesUnchangedFields":9.706,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testUpdateObjectChangesData":10.859,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testUpdateObjectSetsUpdatedTimestamp":9.711,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testUpdateObjectWithSilentMode":10.164,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testUpdateObjectUsingRegisterAndSchemaIds":8.786,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveMultipleObjectsIndependently":37.657,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataDoesNotThrow":10.661,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataWithNameField":10.159,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithSelfMetadata":10.291,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithRegisterAndSchemaIds":8.624,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithPersistFalse":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithValidationDisabled":8.504,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithEmptyStringValues":8.395,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithNullValues":9.235,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithLargeArrayProperty":8.903,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithUnicodeContent":8.772,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithLongStringValue":10.416,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectClearsCachesOnNewSave":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithPrefixedUuid":0.015,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithNumericId":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithUuidWithoutDashes":0.014,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithNestedObjectInArray":0.015,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithSchemaObjectProperty":0.029,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsWithPrefix":0.012,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testScanForRelationsSkipsEmptyKeys":0.013,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsFalsyBehavior":0.027,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsFalsyBehaviorWithEmptyArray":0.026,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsWithTwigTemplate":0.03,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsWithNonExistentSourceProperty":0.027,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyPropertyDefaultsNonTemplateDefault":0.028,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyAlwaysDefaultsWithTwigTemplate":0.032,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testApplyAlwaysDefaultsWithNoSchemaProperties":0.03,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithSchemaSlugResolution":8.554,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithRegisterSlugResolution":8.617,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithConstantValue":8.54,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithSlugGeneration":8.607,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataWithDescriptionField":8.672,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataWithSummaryField":7.792,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataWithImageUrl":8.48,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataWithPublishedField":11.126,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataWithDepublishedField":9.504,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataWithImageFromFileObjectArray":9.438,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testHydrateObjectMetadataWithImageFromAccessUrlFallback":9.065,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithCascadeObjectCreation":17.672,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithCascadeArrayObjects":28.354,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectUpdateMultipleFields":9.082,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithEmptyStringUuid":9.511,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithSelfNestedId":8.843,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithFolderId":8.793,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectInSilentMode":9.028,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectIntegrationTest::testSaveObjectWithDeeplyNestedObject":9.5,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectsIntegrationTest::testSaveObjectsEmpty":0.016,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectsIntegrationTest::testSaveObjectsSingle":0.124,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectsIntegrationTest::testSaveObjectsMultiple":0.128,"OCA\\OpenRegister\\Tests\\Service\\SaveObjectsIntegrationTest::testSaveObjectsWithDeduplication":0.128,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSchemaServiceExplorePropertiesEmpty":0.19,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSchemaServiceExplorePropertiesWithObjects":0.226,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceCreateTrail":0.131,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceGetTrails":0.123,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceGetTrailById":0.122,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceStatistics":0.617,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceStatisticsWithDateRange":0.696,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServicePopularTerms":0.113,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceActivity":0.115,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceRegisterSchemaStats":0.108,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceUserAgentStats":0.116,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceCleanup":0.098,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceClearExpired":0.1,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSearchTrailServiceConfigProcessing":0.114,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceGetRegistersWithSchemas":2.658,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceGetRegistersWithSchemasFiltered":0.445,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceRecalculateSizes":0.138,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceRecalculateLogSizes":0.171,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceRecalculateAllSizes":0.163,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceCalculate":0.187,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceRecalculateSizesNoFilter":0.118,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceAuditTrailChartData":0.179,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceObjectsByRegisterChart":0.105,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceObjectsBySchemaChart":0.12,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceObjectsBySizeChart":0.111,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceAuditTrailStatistics":0.248,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceAuditTrailActionDistribution":0.159,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testDashboardServiceMostActiveObjects":0.16,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceSimpleMapping":0.096,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServicePassThrough":0.099,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceListMapping":0.1,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceCasting":0.111,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceAdvancedCasts":0.096,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceNullableBoolCast":0.103,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceUnset":0.103,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceEncodeArrayKeys":0.105,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceCoordinateStringToArray":0.099,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceInvalidateCache":0.098,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceGetMappings":0.111,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceMoneyCasts":0.106,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceNullStringToNull":0.116,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceJsonToArrayCast":0.123,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMappingServiceListWithExtraValues":0.126,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthenticationServiceJwtHs256":0.18,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthenticationServiceJwtHs384":0.124,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthenticationServiceJwtHs512":0.12,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthenticationServiceJwtMissingParams":0.107,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthenticationServiceOAuthMissingGrantType":0.1,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthenticationServiceOAuthMissingTokenUrl":0.095,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthenticationServiceOAuthUnsupportedGrantType":0.095,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthorizationServiceValidatePayload":0.098,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthorizationServiceValidatePayloadExpired":0.113,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthorizationServiceValidatePayloadMissingIat":0.094,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthorizationServiceValidatePayloadDefaultExp":0.093,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthorizationServiceJwtNoToken":0.096,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthorizationServiceOAuthNonBearer":0.126,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testAuthorizationServiceApiKeyInvalid":0.129,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testRegisterServiceFindAll":0.103,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testRegisterServiceFind":0.111,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testRegisterServiceCreateAndDelete":0.218,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testRegisterServiceUpdate":0.143,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testRegisterServiceSchemaObjectCounts":0.149,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceListTools":0.099,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceListRegisters":0.102,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceListSchemas":0.112,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceGetRegister":0.106,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceGetSchema":0.097,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceUnknownTool":0.095,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceRegisterCrud":0.173,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceSchemaCrud":0.112,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceObjectsMissingParams":0.096,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testMcpToolsServiceMissingRequiredParam":0.111,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorValidString":0.094,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorStringWithFormat":0.097,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorInvalidFormat":0.104,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorInvalidType":0.121,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorMissingType":0.11,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorNumberMinMax":0.152,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorMinGreaterThanMax":0.101,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorNonNumericMinimum":0.099,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorEnum":0.102,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorEmptyEnum":0.097,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorArray":0.114,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorNestedObject":0.099,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorValidateProperties":0.098,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorInvalidPropertyInBatch":0.091,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorBooleanFlags":0.094,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorVisibleNonBoolean":0.093,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorFileType":0.094,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorFileInvalidMime":0.092,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorFileMaxSizeTooLarge":0.095,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorOneOf":0.099,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorOnDelete":0.098,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testPropertyValidatorOnDeleteNonRelation":0.103,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSchemaCacheHandlerInvalidate":0.107,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSchemaCacheHandlerClearSchemaCache":0.102,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testSchemaCacheHandlerInstantiation":0.096,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testFacetCacheHandlerInvalidate":0.097,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceEnsureDefault":0.095,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceGetSettings":0.094,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceGetDefaultUuid":0.094,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceGetUserOrganisations":0.094,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceGetActiveOrganisation":0.09,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceUserStats":0.095,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceGetForNewEntity":0.092,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceClearCache":0.092,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceGetUserActiveOrganisations":0.094,"OCA\\OpenRegister\\Tests\\Service\\ServicesIntegrationTest::testOrganisationServiceHasAccessInvalid":0.1,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetCacheStatsReturnsArrayWithExpectedKeys":0.002,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetCacheStatsOverviewHasNumericValues":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetCacheStatsServicesHasExpectedTypes":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetCacheStatsPerformanceHasExpectedMetrics":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testClearCacheAllTriggersTypeError":0.008,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testClearCacheSchemaReturnsSchemaResult":0.002,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testClearCacheFacetReturnsFacetResult":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testClearCacheDistributedTriggersTypeError":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testClearCacheObjectReturnsResult":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testClearCacheNamesReturnsResult":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testClearCacheInvalidTypeThrowsException":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testClearCacheWithUserIdParameter":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testWarmupNamesCacheReturnsExpectedStructure":0.763,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSettingsReturnsVersion":0.037,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSettingsReturnsRbacSection":0.027,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSettingsReturnsMultitenancySection":0.025,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSettingsReturnsAvailableGroups":0.035,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSettingsReturnsRetentionDefaults":0.032,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSettingsReturnsSolrSection":0.025,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSettingsWithRbacData":0.032,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSettingsWithMultitenancyData":0.036,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSettingsWithRetentionData":0.038,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSettingsWithSolrData":0.03,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testIsMultiTenancyEnabledReturnsBool":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testIsMultiTenancyEnabledAfterUpdate":0.029,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetRbacSettingsOnlyReturnsExpectedStructure":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateRbacSettingsOnlyPersists":0.007,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetMultitenancySettingsOnlyReturnsExpectedStructure":0.025,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateMultitenancySettingsOnlyPersists":0.027,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetOrganisationSettingsOnlyReturnsExpectedStructure":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateOrganisationSettingsOnlyPersists":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetDefaultOrganisationUuidReturnsNullableString":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSetDefaultOrganisationUuidPersists":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetOrganisationIdIsAlias":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetTenantIdReturnsNullableString":0.03,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetLLMSettingsOnlyReturnsDefaults":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateLLMSettingsOnlyPersistsAndMerges":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdatePublishingOptionsPersistsBooleans":0.013,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdatePublishingOptionsIgnoresInvalidKeys":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetN8nSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateN8nSettingsOnlyPersists":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetVersionInfoOnlyReturnsVersionInfo":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testConfigHandlerGetFileSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testConfigHandlerUpdateFileSettingsOnlyPersists":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testFileHandlerGetFileSettingsOnlyReturnsDefaults":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testFileHandlerDefaultValuesAreCorrect":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testFileHandlerUpdateFileSettingsOnlyPersists":0.008,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testFileHandlerEntityRecognitionSettings":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testFileHandlerUpdateThenGetRoundTrip":0.006,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetObjectSettingsOnlyReturnsDefaults":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateObjectSettingsOnlyPersists":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testObjectSettingsRoundTrip":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetRetentionSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateRetentionSettingsOnlyPersists":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testRetentionSettingsRoundTrip":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testRetentionHandlerGetVersionInfoOnly":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSolrSettingsReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSolrSettingsOnlyReturnsComprehensiveDefaults":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSolrSettingsOnlyPersists":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrSettingsRoundTrip":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSolrSettingsOnlyCastsPortToInt":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testWarmupSolrIndexThrowsDeprecatedException":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSolrDashboardStatsReturnsDefaultWhenUnavailable":4.014,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSolrDashboardStatsOverviewHasExpectedKeys":3.869,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSearchBackendConfigReturnsExpectedStructure":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSearchBackendConfigWithValidBackend":0.006,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSearchBackendConfigWithInvalidBackendThrows":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testGetSolrFacetConfigurationReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSolrFacetConfigurationValidatesAndPersists":0.006,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testUpdateSolrFacetConfigurationSkipsInvalidNames":0.006,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testFacetConfigurationRoundTrip":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectCreatingEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectCreatedEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectUpdatingEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectUpdatedEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectDeletingEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectDeletedEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectLockedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectUnlockedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleObjectRevertedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleRegisterCreatedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleRegisterUpdatedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleRegisterDeletedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleSchemaCreatedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleSchemaUpdatedEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleSchemaDeletedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleConfigurationCreatedEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleConfigurationUpdatedEventTriggersError":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleConfigurationDeletedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleViewCreatedEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleConversationCreatedEvent":0.006,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleOrganisationCreatedEvent":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleSourceCreatedEvent":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testHandleUnknownEventReturnsEarly":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandHasCorrectName":0.006,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandHasDescription":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandInvalidActionReturnsFailure":0.006,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandSetupWithoutSolrFails":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandHealthWithoutSolrFails":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandClearWithoutForceFails":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandOptimizeWithoutSolrFails":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandWarmWithoutSolrFails":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandStatsWithoutSolrFails":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testSolrCommandSchemaCheckWithoutSolrFails":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testPreviewRegisterChangeNewRegisterReturnsCreate":0.004,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testPreviewRegisterChangeStructure":0.003,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testCompareArraysReturnsArray":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsHandlersIntegrationTest::testImportConfigurationWithSelectionReturnsArray":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetSearchBackendConfigReturnsArrayWithRequiredKeys":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetSearchBackendConfigActiveIsString":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testIsMultiTenancyEnabledReturnsBool":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetDefaultOrganisationUuidReturnsStringOrNull":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testFormatBytesZero":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testFormatBytesSmall":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testFormatBytesKilobytes":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testFormatBytesMegabytes":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testFormatBytesGigabytes":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testFormatBytesWithPrecision":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testFormatBytesTerabytes":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testFormatBytesWithZeroPrecision":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testConvertToBytesMegabytes":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testConvertToBytesGigabytes":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testConvertToBytesKilobytes":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testConvertToBytesPlain":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testConvertToBytesAndFormatBytesConsistency":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testMaskTokenLong":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testMaskTokenShort":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testMaskTokenExactly8":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testMaskToken9Chars":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testMaskTokenEmpty":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetVersionInfoOnlyReturnsArray":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetDatabaseInfoReturnsArrayOrNull":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testHasPostgresExtensionReturnsBool":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testHasPostgresExtensionNonexistent":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetPostgresExtensionsReturnsArray":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetStatsReturnsStatsArray":7.584,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetStatsContainsWarningsAndTotals":7.459,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetStatsSystemInfo":7.867,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetRbacSettingsOnlyReturnsArray":0.005,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetOrganisationSettingsOnlyStructure":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetMultitenancySettingsOnlyStructure":0.03,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetMultitenancySettingsOnlyContainsAvailableTenants":0.029,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testCompareFieldsMatchingReturnsSummary":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testCompareFieldsDetectsMissing":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testCompareFieldsDetectsExtra":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testCompareFieldsDetectsTypeMismatch":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testCompareFieldsDetectsMultiValuedMismatch":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testCompareFieldsSkipsSystemFields":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testCompareFieldsEmpty":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testCompareFieldsTotalDifferencesIsSum":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testRebaseSolrComponent":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testRebaseCacheComponentHandlesError":0.012,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testRebaseUnknownComponent":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testRebaseReturnsTimestamp":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testClearCacheDelegates":0.002,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testClearCacheWithType":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetSettingsReturnsArray":0.029,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetTenantIdReturnsStringOrNull":0.022,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testGetOrganisationIdReturnsStringOrNull":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testMassValidateObjectsRejectsInvalidMode":0.001,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testMassValidateObjectsRejectsBatchSizeTooSmall":0,"OCA\\OpenRegister\\Tests\\Service\\SettingsServiceIntegrationTest::testMassValidateObjectsRejectsBatchSizeTooLarge":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentShortText":0.003,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentLongText":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentCustomSize":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentFixedSizeStrategy":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentRecursiveStrategy":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentPreservesContent":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentEmptyText":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentWhitespaceOnly":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentSpecialChars":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentNullBytes":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentNormalizesLineEndings":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkOffsetsSequential":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentDefaultStrategy":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentFixedSizeNoOverlap":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentRecursiveWithSentences":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testChunkDocumentRecursiveLargeParagraph":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testGetStats":0.045,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractFileNonexistent":0.003,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractObjectNonexistent":0.002,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testDiscoverUntrackedFiles":0.051,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractPendingFiles":0.043,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testRetryFailedExtractions":0.033,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testSanitizeTextRemovesNullBytes":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testSanitizeTextRemovesControlChars":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testSanitizeTextNormalizesWhitespace":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testSanitizeTextTrims":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testSanitizeTextEmpty":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testSanitizeTextWhitespaceOnly":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testDetectLanguageSignalsDutch":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testDetectLanguageSignalsEnglish":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testDetectLanguageSignalsUnknown":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testGetDetectionMethodWithLanguage":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testGetDetectionMethodNull":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCleanTextRemovesNullBytes":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCleanTextNormalizesLineEndings":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCleanTextCollapsesBlankLines":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCleanTextCollapsesTabs":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testIsWordDocumentDocx":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testIsWordDocumentDoc":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testIsWordDocumentRejectsPdf":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testIsSpreadsheetXlsx":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testIsSpreadsheetXls":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testIsSpreadsheetRejectsText":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCalculateAvgChunkSizeNormal":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCalculateAvgChunkSizeEmpty":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCalculateAvgChunkSizeStringChunks":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCalculateAvgChunkSizeNonTextChunks":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testBuildPositionReferenceObject":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testBuildPositionReferenceFile":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testBuildPositionReferenceFileDefaults":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testSummarizeMetadataPayload":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testSummarizeMetadataPayloadDefaults":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testTextToChunks":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testIsSourceUpToDateForced":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testIsSourceUpToDateNonexistent":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testGetTableCountSafeValidTable":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testGetTableCountSafeInvalidTable":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testRegexDetectsEmails":0.002,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testRegexDetectsIban":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testRegexDetectsPhoneNumbers":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testRegexFiltersByEntityType":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testRegexFiltersByConfidence":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testRegexNoMatches":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testRegexFindsMultipleEmails":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testHybridDetectionFallsToRegex":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testLlmDetectionFallsToRegex":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testPresidioFallsBackToRegex":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testOpenAnonymiserFallsBackToRegex":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testDetectEntitiesRegexMethod":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testDetectEntitiesUnknownMethod":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testGetCategoryForAllTypes":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractContextMiddle":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractContextAtStart":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractContextAtEnd":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractContextZeroWindow":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testMapToPresidioEntityTypes":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testMapToPresidioEntityTypesSkipsUnknown":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testMapFromPresidioEntityTypeKnown":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testMapFromPresidioEntityTypeUnknown":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testBuildAnalyzeRequestBodyBasic":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testBuildAnalyzeRequestBodyWithEntityTypes":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testBuildAnalyzeRequestBodyEmptyEntityTypes":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testConvertApiResultsPresidioStyle":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testConvertApiResultsOpenAnonymiserStyle":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testConvertApiResultsFiltersLowConfidence":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testConvertApiResultsEmpty":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractFromChunkEmptyText":0.002,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractFromChunkWhitespaceText":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testExtractFromChunkNoEntities":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testProcessSourceChunksNoChunks":0.002,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testPostAnalyzeRequestInvalidUrl":0.003,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testEntityTypeConstants":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testMethodConstants":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testCategoryConstants":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testGetRegexPatternsStructure":0.001,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testGetRegexPatternsIncludesTypes":0,"OCA\\OpenRegister\\Tests\\Service\\TextExtractionServiceIntegrationTest::testHydrateChunkEntity":0,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testGetCurrentUserNoSession":0.285,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArray":0.191,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayQuota":0.137,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayBackendCapabilities":0.139,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayOrganisations":0.142,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testGetCustomNameFields":0.146,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testSetCustomNameFields":0.177,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testSetCustomNameFieldsPartial":0.166,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testSetCustomNameFieldsIgnoresUnknown":0.188,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserProperties":0.167,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesDisplayName":0.881,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayGroups":0.188,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayEnabled":0.159,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayLastLogin":0.15,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayBackend":0.144,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArraySubadmin":0.169,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayAvatarScope":0.142,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayEmailVerified":0.165,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesEmail":0.163,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesLanguage":0.174,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesLocale":0.162,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesProfileFields":0.231,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesMiddleName":0.15,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesFunctie":0.208,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesOrganisationSwitch":0.138,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesEmptyData":0.183,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayFunctieFromConfig":0.137,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayCachesOrgStats":0.147,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesDispatchesEvent":0.787,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesFediverse":0.466,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesBiography":0.221,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesHeadline":0.2,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesRole":0.214,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesAddress":0.271,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testGetCustomNameFieldsReturnsNullForUnset":0.181,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayLanguageLocale":0.157,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testUpdateUserPropertiesMultipleProfileFields":0.255,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayWithOrganisationConfig":0.14,"OCA\\OpenRegister\\Tests\\Service\\UserServiceIntegrationTest::testBuildUserDataArrayQuotaRelative":0.138,"OCA\\OpenRegister\\Tests\\Service\\VectorSearchHandlerIntegrationTest::testSemanticSearchEmptyEmbedding":0.005,"OCA\\OpenRegister\\Tests\\Service\\VectorSearchHandlerIntegrationTest::testSemanticSearchPhpBackend":0.001,"OCA\\OpenRegister\\Tests\\Service\\VectorSearchHandlerIntegrationTest::testSemanticSearchDatabaseBackend":0.001,"OCA\\OpenRegister\\Tests\\Service\\VectorSearchHandlerIntegrationTest::testSemanticSearchWithFilters":0.001,"OCA\\OpenRegister\\Tests\\Service\\VectorSearchHandlerIntegrationTest::testSemanticSearchLimitOne":0.001,"OCA\\OpenRegister\\Tests\\Service\\VectorSearchHandlerIntegrationTest::testHybridSearch":0.001,"OCA\\OpenRegister\\Tests\\Service\\VectorSearchHandlerIntegrationTest::testHybridSearchEmptySolr":0.001,"OCA\\OpenRegister\\Tests\\Service\\VectorSearchHandlerIntegrationTest::testHybridSearchWithWeights":0.001,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDispatchEventNoWebhooks":0.003,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDispatchEventEmptyPayload":0.003,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDispatchEventComplexPayload":0.003,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testInterceptRequest":0.003,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookDisabled":0.008,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookUnreachableUrl":2.016,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookFilterMismatch":0.006,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookFilterMatch":2.023,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookArrayFilterMismatch":0.008,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookArrayFilterMatch":2.016,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookNestedFilterMismatch":0.006,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookNestedFilterMatch":2.014,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookNestedFilterMissingKey":0.007,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookWithCloudEventsConfig":2.017,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookWithMissingMapping":2.018,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookGetMethod":2.014,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookWithSecret":2.015,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookWithCustomHeaders":2.016,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookExponentialRetry":2.014,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookLinearRetry":2.017,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookFixedRetry":2.013,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookAtMaxRetry":2.015,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookDefaultRetryPolicy":2.012,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookPutMethod":2.019,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookDeleteMethod":2.015,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDispatchEventWithMatchingWebhook":0.01,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testInterceptRequestWithConfiguredWebhook":0.009,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testInterceptRequestAsyncWebhook":0.01,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDeliverWebhookEmptyFilters":2.015,"OCA\\OpenRegister\\Tests\\Service\\WebhookServiceIntegrationTest::testDispatchEventFullyQualifiedName":0.005,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\BlobMigrationJobTest::testIntervalIsSetToFiveMinutes":0.005,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\BlobMigrationJobTest::testGroupByRegisterSchemaGroupsCorrectly":0.003,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\BlobMigrationJobTest::testGroupByRegisterSchemaHandlesOrphans":0.003,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\BlobMigrationJobTest::testBlobRowToObjectArrayConvertsCorrectly":0.001,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\BlobMigrationJobTest::testBlobRowToObjectArrayHandlesInvalidJson":0.002,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\BlobMigrationJobTest::testBlobRowToObjectArrayHandlesNullObject":0,"OCA\\OpenRegister\\Tests\\Unit\\BackgroundJob\\BlobMigrationJobTest::testBatchSizeIs100":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testCanDeleteDelegatesToIntegrityService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testCanDeleteReturnsNonDeletableAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteWithObjectEntityReturnsTrueOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteSetsDeletedByCurrentUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteSetsDeletedBySystemWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteCreatesAuditTrailWhenEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteSkipsAuditTrailWhenDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteInvalidatesCacheOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteReturnsTrueWhenCacheInvalidationThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteReturnsTrueWhenUpdateSucceeds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteDefaultsAuditTrailToEnabledOnSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteWithArrayInputLoadsObjectFromMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectSkipsIntegrityCheckWhenNoIncomingRefs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectThrowsWhenDeletionIsBlocked":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectAppliesDeletionActionsWhenDeletable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectSkipsIntegrityOnSubDeletion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectCascadesScalarProperty":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectCascadesArrayProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectDoesNotCascadeWithoutCascadeFlag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectSkipsCascadeWhenPropertyValueIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectReturnsFalseWhenDeleteThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectPassesRbacFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectPassesMultitenancyFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteObjectSkipsIntegrityCheckWhenSchemaIdIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testReferentialIntegrityExceptionCarriesAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeletionAnalysisEmptyIsFullyDeletable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeletionAnalysisToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteConvertsNonNumericIdsToNullForCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\DeleteObjectTest::testDeleteConvertsNumericStringIdsToIntForCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCacheFacetableFieldsStoresInMemoryAndDatabase":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCacheFacetableFieldsWithDefaultTtl":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCacheFacetableFieldsWithCustomTtl":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCacheFacetableFieldsLogsFieldCount":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCacheFacetableFieldsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCacheFacetableFieldsUpdatesExistingRecord":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testInvalidateForSchemaChangeRemovesFromMemory":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testInvalidateForSchemaChangeDefaultOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testInvalidateForSchemaChangeWithDeleteOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testInvalidateForSchemaChangeHandlesMissingTable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testInvalidateForSchemaChangeClearsDistributedCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testInvalidateForSchemaChangeLogsExecutionTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testClearAllCachesRemovesEverything":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testClearAllCachesLogsStatistics":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testClearAllCachesClearsDistributedCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCleanExpiredEntriesReturnsDeletedCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCleanExpiredEntriesLogsWhenDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testCleanExpiredEntriesDoesNotLogWhenNoneDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testGetCacheStatisticsReturnsExpectedStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testGetCacheStatisticsAggregatesByType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testGetCacheStatisticsIncludesMemoryCacheSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testClearDistributedFacetCachesFallsBackToLocal":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testClearDistributedFacetCachesHandlesBothFailures":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testSetCachedFacetDataEnforcesMaxTtl":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\FacetCacheHandlerTest::testSetCachedFacetDataWithZeroTtl":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testBuildCacheKeyFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testBuildCacheKeyWithDifferentTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testGetSchemaReturnsFromMemoryCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testGetSchemaReturnsNullWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testGetSchemaLoadsFromMapperOnCacheMiss":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testGetSchemaLogsOnMapperLoad":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCacheSchemaStoresInMemoryAndDatabase":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCacheSchemaWithCustomTtl":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCacheSchemaCallsCacheConfigurationAndProperties":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCacheSchemaConfiguration":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCacheSchemaConfigurationWithNullConfig":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCacheSchemaProperties":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCacheSchemaPropertiesWithEmptyProperties":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testClearSchemaCacheRemovesFromMemory":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testClearSchemaCacheHandlesDatabaseError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testClearSchemaCacheLogsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testInvalidateForSchemaChangeRemovesFromBothCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testInvalidateForSchemaChangeHandlesMissingTable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testInvalidateForSchemaChangeDefaultOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testInvalidateForSchemaChangeLogsDeletedEntries":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testInvalidateForSchemaChangeLogsExecutionTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testClearAllCachesRemovesEverything":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testClearAllCachesLogsStatistics":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCleanExpiredEntriesReturnsDeletedCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCleanExpiredEntriesLogsWhenDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCleanExpiredEntriesDoesNotLogWhenNoneDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testCleanExpiredEntriesReturnsZeroForEmptyTable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testGetCacheStatisticsReturnsExpectedStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testGetCacheStatisticsIncludesMemoryCacheSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testGetCacheStatisticsTimestampIsRecent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testGetCacheStatisticsQueryTimeFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testSerializeSchemaForCacheViaReflection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testSerializeSchemaForCacheWithNullDates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testReconstructSchemaFromCacheReturnsNullDueToInvalidAttributes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testReconstructSchemaFromCacheLogsErrorOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testSetCachedDataEnforcesMaxTtl":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testSetCachedDataUpdatesExistingRecord":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerTest::testSetCachedDataWithZeroTtl":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeReturnsNullForGarbageInput":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeJsonExplicitType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeJsonAutoDetect":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeYamlExplicitType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeFallsBackToYamlOnJsonFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeReturnsNullWhenBothParsersFail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeConvertsStdClassToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureConvertsStdClass":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureConvertsNestedObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureLeavesArraysIntact":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureConvertsObjectsInsideArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromFileReturnsErrorResponseOnUploadError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromFileReturnsErrorResponseOnDecodeFailure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromFileReturnsArrayForValidJsonFile":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromURLReturnsErrorOnGuzzleException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromURLReturnsErrorWhenBodyNotDecodable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromURLReturnsArrayOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromBodyPassthroughArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromBodyDecodesJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromBodyReturnsErrorForInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromBodyConvertsStdClassObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterCreatesNewRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSetsOwnerAndApplication":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSkipsWhenVersionEqual":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSkipsWhenExistingVersionIsNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterUpdatesWhenVersionIsNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterForceUpdatesWhenVersionIsOlder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterStripsIdUuidOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterThrowsOnMapperFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaCreatesNewSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaSetsOwnerAndApplication":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaSkipsWhenVersionEqual":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaUpdatesWhenVersionIsNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaDefaultsPropertyTypeToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaStripsStringFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesRefFromSlugsMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaThrowsOnMapperError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonThrowsWithoutConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsWhenVersionNotNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonImportsSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonImportsRegisters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonStoresVersionInAppConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonExtractsAppIdAndVersionFromData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsObjectsMissingRegisterOrSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsObjectsWithoutSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonForceBypassesVersionCheck":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppCreatesNewConfigurationWhenNoneExists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppReusesConfigFoundBySourceUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppReusesConfigFoundByApp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppSetsGithubFieldsFromNestedStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppSetsGithubFieldsFromFlatStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppWrapsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationCreatesNew":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationUpdatesExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationReadsTitleFromInfoSection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationFallsBackToXOpenregisterTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsSourceUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationThrowsOnMapperError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetObjectService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetOpenConnectorConfigurationService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromFilePathThrowsWhenFileNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromFilePathThrowsOnInvalidJson":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromFilePathInjectsSourceMetadata":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonImportsMappings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonCreatesNewObjectWhenNotExisting":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsObjectWhenVersionNotHigher":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonCallsOpenConnectorWhenSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonContinuesWhenOpenConnectorThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonContinuesWhenSchemaImportFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSetsSchemaTitleFromKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeYamlExtensionType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeReturnsNullForInvalidJsonWithExplicitType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetWorkflowEngineRegistry":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetDeployedWorkflowMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetMagicMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetUnifiedObjectMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterThrowsOnDuplicateRegister":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterDuplicateInfoHandlesFindAllFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterDuplicateInfoOneMatchReturnsGenericMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSetsOnlyOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterForceUpdatesExistingWithOwnerAndApp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaThrowsOnDuplicateSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaCreatesOnValidationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaStripsItemsBinaryFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaSetsMissingPropertyTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesRefFromSchemasMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesItemsRefFromSlugsMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesObjectConfigurationRegisterFromMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesObjectConfigurationRegisterFromDatabase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaRemovesObjectConfigurationRegisterWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesObjectConfigurationSchemaFromMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaNormalisesEmptyObjectConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesLegacyRegisterProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesLegacyItemsRegisterFromMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonResolvesRegisterSchemasFromSchemasMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonLogsWarningForUnresolvedRegisterSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonUpdatesExistingObjectWhenVersionIsHigher":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsObjectWithArrayResultWhenVersionNotHigher":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppContinuesWhenConfigVersionUpToDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppForceBypassesConfigVersionCheck":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppSetsOpenregisterVersionRequirement":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppSetsTypeFromXOpenregister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsOpenregisterVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationFallsBackToXOpenregisterDescription":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsGithubFieldsNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationSetsGithubFieldsFlat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationMergesEntityIdsOnUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedDataWhenAbsent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonCreatesSeedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedObjectWhenAlreadyExists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedObjectWithoutSlugOrTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedDataWhenSchemaNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonUseUnifiedObjectMapperForSeedData":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsSeedObjectOnMultipleObjectsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsVersionCheckWhenNoAppId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonLogsForceImportMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsMappingWithoutSlugOrName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonUpdatesMappingWhenVersionIsHigher":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsMappingWhenVersionEqual":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSetsMappingNameFromKeyWhenAbsent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonDoesNotStoreVersionWithoutAppId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonDoesNotStoreVersionWithoutVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaForceUpdatesWhenVersionEqual":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaSkipsWhenExistingVersionIsNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportRegisterSetsOnlyApplication":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testGetJSONfromURLParsesYamlResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDecodeReturnsNullForInvalidYamlWithExplicitType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testEnsureArrayStructureHandlesMixedArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonResultAlwaysHasAllExpectedKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonContinuesWhenMappingImportFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonDoesNotSkipWhenStoredVersionIsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonSkipsWorkflowsWhenRegistryNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentFailsOnMissingRequiredFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentUnchangedWhenHashMatches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentFailsWhenNoEngineOfType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentCreatesNewWorkflow":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentUpdatesExistingWorkflow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowDeploymentRecordsFailureOnAdapterException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringSkipsEntryWithoutAttachTo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringAttachesWorkflowToSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringSkipsWhenSchemaNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringSkipsIncompleteAttachTo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testWorkflowHookWiringReturnsEarlyWhenMapperNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaNormalisesEmptyFileConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaNormalisesItemsObjectConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaNormalisesItemsFileConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaConvertsStdClassPropertyToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaConvertsStdClassItemsToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesObjectConfigurationSchemaFromDatabase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaRemovesObjectConfigurationSchemaWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesItemsObjectConfigRegisterFromMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaResolvesItemsObjectConfigSchemaFromMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSchemaStripsByteFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDuplicateSchemaInfoHandlesFindAllFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testDuplicateSchemaInfoOneMatchReturnsGenericMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataPreCreatesMagicMapperTable":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataContinuesWhenMagicMapperFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataUsesTitleAsSlugFallback":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataUsesProvidedUuid":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataContinuesWhenInsertThrows":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportSeedDataUsesRegisterZeroWhenNoRegisterFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromAppUpdatesMetadataOnExistingConfigWithResults":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromFilePathDoesNotOverwriteExistingSourceMetadata":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationCollectsObjectIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationUsesDataTitleFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationUsesDefaultTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testCreateOrUpdateConfigurationUsesDataType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonPass2SkipsSchemaNotInMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonPass2CatchesReImportFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#cmd":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#com":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#scr":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#vbs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#vbe":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#js":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#jse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#wsf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#wsh":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#csh":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#ksh":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#zsh":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#run":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#bin":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#app":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#rpm":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#phtml":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#php3":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#php4":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#php5":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#phps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#phar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#pyc":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#pyo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#pyw":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#pl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#pm":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#cgi":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#rb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#rbw":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#war":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#ear":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#class":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#appimage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#snap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#flatpak":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#dmg":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#pkg":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#command":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#elf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#out":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#o":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#so":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileBlocksDangerousExtensions#dylib":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShebangInFirstLines#python shebang line 2":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShebangInFirstLines#perl shebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShebangInFirstLines#ruby shebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShebangInFirstLines#node shebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShebangInFirstLines#php shebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShebangInFirstLines#zsh shebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShebangInFirstLines#ksh shebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesShebangInFirstLines#csh shebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileIsCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileLogsWarningOnBlock":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileSkipsMagicBytesForEmptyContent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesEnvShebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesEmbeddedPhpTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesPhpShortEchoTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesPhpScriptTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesPhpScriptTagSingleQuotes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesLogsWarningOnDetection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipFileAccessible":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipFolderAccessible":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipGenericNodeAccessible":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotFoundExceptionFixesOwnership":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotFoundExceptionNullOwnerFixesOwnership":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotFoundExceptionOwnershipFixFails":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotFoundExceptionCorrectOwnerButNotAccessible":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotFoundExceptionOwnershipCheckThrows":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotFoundExceptionNoUserLoggedIn":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotPermittedExceptionFixesOwnership":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotPermittedExceptionFixFails":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipFolderNotFoundFixesOwnership":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testOwnFileSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testOwnFileReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testOwnFileThrowsOnMapperException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testOwnFileThrowsWhenNoUserLoggedIn":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testOwnFileLogsInfoOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testOwnFileLogsWarningOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testOwnFileLogsErrorOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipNotPermittedNoUserLoggedIn":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileUpperCaseExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileMixedCaseExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileDoubleExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testBlockExecutableFileNoExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectExecutableMagicBytesEmptyContent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testDetectMagicBytesShebangBeyond1024BytesNotDetected":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableActualMigration":0,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageActualMigration":0,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageHandlesFailure":0,"Unit\\Service\\MigrationServiceTest::testGetStorageStatusWithMagicTableExists":0,"Unit\\Service\\MigrationServiceTest::testGetStorageStatusRegisterSchemaIds":0,"Unit\\Service\\MigrationServiceTest::testResolveRegisterAndSchemaThrowsOnMissingRegister":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableBreaksLoopWhenNoBatchReturned":0,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageBreaksLoopWhenNoBatchReturned":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareTypeWithMissingType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareTypeWithMatchingType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareTypeWithMismatchedType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsSkipsNonStringTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsDetectsMissingMaxLength":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsDetectsMaxLengthTooSmall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsDetectsMissingFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareStringConstraintsDetectsMissingPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareNullableConstraintWithRequiredAndNullable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareNullableConstraintWithNonRequiredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareNullableConstraintWithNonNullableAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareEnumConstraintSuggestsAddingEnum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareEnumConstraintDetectsEnumMismatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareEnumConstraintWithNullEnumValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testCompareEnumConstraintSkipsWhenTooManyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeExistingPropertiesReturnsMismatchImprovements":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testAnalyzeExistingPropertiesSkipsUndiscoveredProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateNestedPropertiesCreatesDefinitions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateNestedPropertiesWithNullKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateArrayItemTypeReturnsItemType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateArrayItemTypeWithEmptyItemTypesReturnsString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesNullType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesArrayType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesBooleanType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesUnknownType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testNormalizeSingleTypeHandlesFloatType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGetDominantTypeWithFloatStringPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGetDominantTypeWithBooleanStringPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGetDominantTypeWithIntegerDominant":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesNumberDominatesInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testMergeNumericRangesIncompatibleTypesDefaultsToNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsCreatesObjectTypeSuggestion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceTest::testGenerateSuggestionsCreatesArrayTypeSuggestion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testEmptyChunkReturnsImmediatelyWithoutCallingBulkSave":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testAllInvalidObjectsSkipsBulkSaveAndPopulatesInvalidList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testBulkSaveWithCreatedStatusPopulatesSavedList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testBulkSaveWithUpdatedStatusPopulatesUpdatedList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testBulkSaveWithUnchangedStatusPopulatesUnchangedList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testMixedStatusesAggregateCorrectly":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testPartialChunkWithInvalidAndValidObjectsMergesCorrectly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testLegacyUuidArrayBulkResultCountsSaved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testLegacyUuidArrayDoesNotCountUnmatchedObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testLegacyScalarArrayWithNoMatchingUuidsCountsZeroSaved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testProcessingTimeMsIsAlwaysPresentAndNonNegative":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testRegisterAndSchemaObjectsPassedDirectlyToUltraFastBulkSave":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testUltraFastBulkSaveIsCalledWithEmptyUpdateObjectsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testResultAlwaysContainsAllRequiredKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testMultipleInvalidObjectsAreAllAccumulated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testUnknownObjectStatusExposesSourceBugInDefaultCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testLargeChunkStatisticsAreTalliedCorrectly":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testNullRegisterAndSchemaPropagateToUltraFastBulkSave":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testBooleanFlagVariationsDoNotCauseErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testTransformHandlerIsCalledExactlyOnce":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testSchemaCacheIsForwardedToTransformationHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjects\\ChunkProcessingHandlerTest::testSavedItemsAreJsonSerializableArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithStandardUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithUuidWithoutDashes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithPrefixedUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithPrefixedUuidNoDashes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithWhitespaceOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithPlainText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithCommonWordReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithOpenSourceReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithIdentifierLikeString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsReferenceWithShortString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithOnlyMetadataKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithNullValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithEmptyStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithNonEmptyValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithNestedEmptyObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsEffectivelyEmptyObjectWithNestedNonEmptyObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithNonEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithAssociativeArrayAllEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithIndexedArrayAllEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsValueNotEmptyWithIndexedArraySomeNotEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsAuditTrailsEnabledReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsAuditTrailsEnabledReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsAuditTrailsEnabledDefaultsToTrueWhenKeyMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testIsAuditTrailsEnabledDefaultsToTrueOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testRemoveQueryParametersWithQueryString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testRemoveQueryParametersWithoutQueryString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testRemoveQueryParametersWithMultipleParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGenerateSlugReturnsNullWhenNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGenerateSlugReturnsNullWhenNoSlugField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGenerateSlugReturnsNullWhenFieldValueIsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGenerateSlugReturnsSlugWithTimestamp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testCreateSlugConvertsToLowercase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testCreateSlugReplacesSpecialCharacters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testCreateSlugTrimsHyphens":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testCreateSlugTruncatesLongStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultAlwaysReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultFalsyWithNonEmptyValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultDefaultBehaviorWithMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultDefaultBehaviorWithNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testShouldApplyDefaultDefaultBehaviorWithExistingValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueSimpleReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueSimpleReferencePreservesArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueSimpleReferenceMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueComplexTemplate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueNonTemplate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueNonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveDefaultTemplateValueExceptionReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyAlwaysDefaultsNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyAlwaysDefaultsAppliesAlwaysBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyAlwaysDefaultsSkipsNonAlwaysDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyAlwaysDefaultsWithInvalidSchemaObjectReturnsData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyPropertyDefaultsAppliesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyPropertyDefaultsSkipsExistingValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testApplyPropertyDefaultsFalsyBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetValueFromPathSimple":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetValueFromPathNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetValueFromPathReturnsNullForMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetValueFromPathConvertIntToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsObjectPropertyEmptyStringBecomesNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsObjectPropertyEmptyObjectNonRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsObjectPropertyEmptyObjectRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsArrayPropertyEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsArrayItemsWithEmptyStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsScalarNonRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsScalarRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSanitizeEmptyStringsSkipsMissingProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceCachedResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceNotFoundCachesNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveRegisterReferenceEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveRegisterReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveRegisterReferenceBySlugInUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveRegisterReferenceNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testFillMissingSchemaPropertiesWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testClearAllCachesResetsEverything":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testTrackAndGetCreatedSubObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithImageStringUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithImageArrayFileObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithImageArrayAccessUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithInvalidPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithInvalidDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testHydrateObjectMetadataWithNumericImage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsWithSchemaObjectProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsWithSchemaTextUuidProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsWithArrayOfObjectStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsWithPrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testScanForRelationsSkipsEmptyKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSaveObjectWithRbacUnauthorizedProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSaveObjectWithRbacCreateNewObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSaveObjectWithAutoGeneratedUuidSkipsLookup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSaveObjectWithFilePropertyProcessing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithConstantValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithAlwaysBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithFalsyBehaviorEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithTemplateException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetDefaultValuesWithSlugGeneration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testUpdateObjectRelationsSetsRelationsOnEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetSelfMetadataWithSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetSelfMetadataWithPublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetSelfMetadataWithNullPublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testSetSelfMetadataWithDepublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetCachedSchemaFetchesAndCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testGetCachedRegisterFetchesAndCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceBySlugPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceDirectSlugMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectAdditionalTest::testResolveSchemaReferenceCleanedCacheLookup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesNullProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesSkipsNonValidatedProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesSkipsNullAndEmptyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesSkipsUnchangedValuesOnUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesSkipsWithoutRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferencesArrayProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferenceExistsThrowsOnNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferenceExistsLogsWarningOnGeneralException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testValidateReferenceExistsReturnsWhenSchemaUnresolvable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectReturnsNullOnNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectReturnsWhenNotLocked":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectThrowsWhenLockedByDifferentUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectAllowsLockBySameUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testFindAndValidateExistingObjectLockNoCurrentUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsReturnsEmptyForNonList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsReturnsEmptyForAllStringUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsReturnsEmptyForInvalidItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsReturnsEmptyWhenNoItemsRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsFiltersObjectsWithEmptyId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeMultipleObjectsRecognizesIdentifierTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testDeleteOrphanedRelatedObjectsSoftDeletes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testDeleteOrphanedRelatedObjectsSystemUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testDeleteOrphanedRelatedObjectsHandlesNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testDeleteOrphanedRelatedObjectsHandlesGeneralException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testPreCacheParentNameReturnsWhenNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testPreCacheParentNameCachesName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testPreCacheParentNameFallsBackToNaam":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testPreCacheParentNameHandlesHydrationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testClearImageMetadataNoObjectImageField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testClearImageMetadataIfFilePropertyClearsWhenFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testClearImageMetadataIfFilePropertySkipsNonFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterWithEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterWithIntIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterWithNullRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterThrowsForInvalidStringSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaAndRegisterThrowsForInvalidStringRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeSingleObjectReturnsNullWithoutRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeSingleObjectReturnsNullForEmptyObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeSingleObjectReturnsNullWhenParentUuidEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testCascadeSingleObjectReturnsNullForObjectWithEmptyId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsReturnsEarlyWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsSkipsNonUuidRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsSkipsNoPropertyConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsSkipsNoTargetSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testUpdateInverseRelationsWithNullRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveRegisterReferenceWithUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveRegisterReferenceUuidNotFoundFallsToSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaReferenceWithQueryParamsAndCleanedCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testResolveSchemaReferenceUuidThrowsDoesNotExist":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testExtractUuidAndSelfDataEmptyUuidToNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testExtractUuidAndSelfDataProcessesUploadedFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testExtractUuidAndSelfDataExtractsIdFromData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testExtractUuidAndSelfDataExtractsSelfId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithInvalidDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataWithEmptyPublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectCoverageTest::testSetSelfMetadataNoDepublishedInSelfData":0,"Unit\\Service\\GraphQL\\GraphQLErrorFormatterTest::testFormatsBasicError":0.002,"Unit\\Service\\GraphQL\\GraphQLErrorFormatterTest::testFormatsNotAuthorizedError":0,"Unit\\Service\\GraphQL\\GraphQLErrorFormatterTest::testFormatsErrorWithExtensionCode":0,"Unit\\Service\\GraphQL\\GraphQLErrorFormatterTest::testFieldForbiddenCreatesCorrectError":0,"Unit\\Service\\GraphQL\\GraphQLErrorFormatterTest::testNotFoundCreatesCorrectError":0,"Unit\\Service\\GraphQL\\QueryComplexityAnalyzerTest::testSimpleQueryPassesComplexity":0.008,"Unit\\Service\\GraphQL\\QueryComplexityAnalyzerTest::testDepthLimitingRejectsDeepQuery":0,"Unit\\Service\\GraphQL\\QueryComplexityAnalyzerTest::testCostBudgetRejectsExpensiveQuery":0.001,"Unit\\Service\\GraphQL\\QueryComplexityAnalyzerTest::testListMultiplierIncreasessCost":0,"Unit\\Service\\GraphQL\\QueryComplexityAnalyzerTest::testIntrospectionFieldsSkipped":0,"Unit\\Service\\GraphQL\\QueryComplexityAnalyzerTest::testComplexityReturnedInResult":0,"Unit\\Service\\GraphQL\\QueryComplexityAnalyzerTest::testPerSchemaCostOverride":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testDateTimeSerializesDateTimeInterface":0.005,"Unit\\Service\\GraphQL\\ScalarTypesTest::testDateTimeSerializesString":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testDateTimeRejectsInteger":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testDateTimeParsesIso8601":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testDateTimeParsesDateOnly":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testDateTimeRejectsInvalidString":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testDateTimeRejectsNonString":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUuidSerializesValidUuid":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUuidRejectsNonStringSerialize":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUuidParsesValidUuid":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUuidRejectsInvalidFormat":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUuidRejectsNonString":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testEmailSerializesString":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testEmailParsesValidEmail":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testEmailRejectsInvalidEmail":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUriSerializesString":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUriParsesValidUri":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUriRejectsInvalidUri":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testJsonSerializesAnything":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testJsonParsesAnything":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUploadSerializesValue":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUploadParsesArray":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUploadParsesString":0,"Unit\\Service\\GraphQL\\ScalarTypesTest::testUploadRejectsInteger":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testStringPropertyMapsToString":0.017,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testIntegerPropertyMapsToInt":0.001,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testBooleanPropertyMapsToBoolean":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testDateTimeFormatMapsToDateTimeScalar":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testUuidFormatMapsToUuidScalar":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testEmailFormatMapsToEmailScalar":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testObjectWithoutRefMapsToJsonScalar":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testGeneratesQueryAndMutationTypes":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testGeneratesConnectionType":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testGeneratesMutationFields":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testObjectTypeIncludesMetadataFields":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testGeneratesRegisterQuery":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testRefPropertyResolvesToObjectType":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testStringPropertyMapsToStringType":0.008,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testIntegerPropertyPresent":0,"Unit\\Service\\GraphQL\\SchemaGeneratorTest::testEmptySchemaGeneratesValidGraphQL":0,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testSchemaGenerationFromRealDatabase":0.052,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testIntrospectionQuery":0.054,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testComplexityInExtensions":0.048,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testInvalidQueryReturnsError":0.042,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testSyntaxErrorReturnsError":0.012,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testCustomScalarsInSchema":0.062,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testConnectionTypeStructure":0.101,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testPageInfoTypeFields":0.051,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testAuditTrailEntryTypeFields":0.096,"OCA\\OpenRegister\\Tests\\Service\\GraphQLIntegrationTest::testDepthLimitingRejectsDeepQuery":0.011,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testSearchConfigurationsReturnsResultsOnSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testSearchConfigurationsWithSearchTerm":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testSearchConfigurationsThrowsOnApiError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testEnrichConfigurationDetailsReturnsMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testEnrichConfigurationDetailsReturnsNullOnInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testEnrichConfigurationDetailsReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testEnrichConfigurationDetailsWithCustomBranch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetBranchesReturnsBranchList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetBranchesThrowsOnApiFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetFileContentReturnsDecodedJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetFileContentThrowsOnInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetFileContentThrowsOnNoContent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetRepositoriesReturnsEmptyWhenNoToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetRepositoriesReturnsMappedData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetRepositoryInfoReturnsFormattedData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetRepositoryInfoThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetUserTokenReturnsTokenWhenSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetUserTokenReturnsNullWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testSetUserTokenStoresToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testSetUserTokenDeletesWhenNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testValidateTokenReturnsTrueOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testValidateTokenReturnsFalseOnEmptyToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testValidateTokenReturnsFalseOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testValidateTokenWithUserIdUsesUserToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetFileShaReturnsSha":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testGetFileShaReturnsNullOn404":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testPublishConfigurationSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testPublishConfigurationThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testAuthTokenIncludedInSearchRequests":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\GitHubHandlerTest::testNoAuthTokenWhenNotConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigWithRegisterInput":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigWithRegisterSetsTypeMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigWithConfigurationInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigWithArrayInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigWithIncludeObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigWithNoRegistersReturnsEmptyComponents":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigExportsMappings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testExportConfigHandlesMappingNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testSetWorkflowEngineRegistry":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ExportHandlerTest::testSetDeployedWorkflowMapper":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlHandlesVariousContentTypes#json content-type":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlHandlesVariousContentTypes#json with charset":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlHandlesVariousContentTypes#yaml content-type":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlHandlesVariousContentTypes#yml content-type":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlHandlesVariousContentTypes#empty content-type with json":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlReturnsArrayOnValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlParsesYamlWhenContentTypeIsYaml":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlReturnsErrorOnConnectionFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testGetJsonFromUrlReturnsErrorOnUnparseableResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsDataOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsErrorWhenNotRemote":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsErrorWhenNoSourceUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsErrorWhenNullSourceUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\FetchHandlerTest::testFetchRemoteConfigurationReturnsErrorOnFetchFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnNoInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnEmptyDataAndEmptyFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnMultipleFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonParsesJsonFile":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnUploadError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnUndecodableFile":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonFetchesFromUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnUrlFetchFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnUrlUnparseableResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonParsesJsonBody":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonParsesJsonStringBody":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnInvalidJsonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonPrioritizesFileOverUrl":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonPrioritizesUrlOverJsonBody":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonParsesYamlFile":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testSetFileServiceStoresReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testPublishFileByIdSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testPublishFileByIdThrowsWhenFileNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testPublishFileByPathSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testPublishFileByPathThrowsWhenObjectFolderNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testPublishFileByPathThrowsWhenFileNotInFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testPublishFileThrowsWhenNodeIsNotFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testPublishFileResolvesStringObjectId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testPublishFileThrowsWhenShareCreationFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testUnpublishFileResolvesStringObjectId":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testCreateObjectFilesZipResolvesStringObjectId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderNameVariations#plain name":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderNameVariations#already has register":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderNameVariations#lowercase register":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderNameVariations#has Register mid-word":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderNameAppendsSuffix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderNameDoesNotDuplicateSuffix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderNameHandlesCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderNameWithNullTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetObjectFolderNameReturnsUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetObjectFolderNameReturnsIdWhenNoUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetObjectFolderNameReturnsStringWhenStringInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetNodeTypeFromFolderReturnsFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetNodeTypeFromFolderReturnsFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetNodeTypeFromFolderReturnsUnknown":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetOpenRegisterUserFolderReturnsFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetOpenRegisterUserFolderThrowsWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetNodeByIdReturnsNodeFromUserFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetNodeByIdFallsBackToRootFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetNodeByIdReturnsNullWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetNodeByIdReturnsNullOnExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testCreateEntityFolderWithRegisterDelegatesToCreateRegisterFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testCreateEntityFolderReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderByIdReturnsExistingFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderByIdCreatesNewWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testGetRegisterFolderByIdCreatesNewWhenNonNumeric":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testCreateFolderReturnsExistingFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FolderManagementHandlerTest::testSetFileServiceDoesNotThrow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnBinaryFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\UploadHandlerTest::testGetUploadedJsonReturnsErrorOnUrlWithBinaryResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FilePublishingHandlerTest::testUnpublishFileByIdSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetActiveOrganisationNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testSetActiveOrganisationNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetUserOrganisationStatsNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetUserOrganisationsNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetUserOrganisationsUsesCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testClearCacheNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testClearCacheWithPersistent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetUserActiveOrganisationsNoActiveOrg":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetUserActiveOrganisationsWithParentChain":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetUserActiveOrganisationsNoParents":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationManagementTest::testGetOrganisationForNewEntityFallbackToDefault":0,"Unit\\Service\\AuthenticationServiceTest::testCreateClientCredentialConfigThrowsWhenMissingParams":0,"Unit\\Service\\AuthenticationServiceTest::testCreateClientCredentialConfigThrowsListsMissingParams":0,"Unit\\Service\\AuthenticationServiceTest::testCreateClientCredentialConfigBodyAuth":0,"Unit\\Service\\AuthenticationServiceTest::testCreateClientCredentialConfigBasicAuth":0,"Unit\\Service\\AuthenticationServiceTest::testCreateClientCredentialConfigOtherAuthType":0,"Unit\\Service\\AuthenticationServiceTest::testCreatePasswordConfigThrowsWhenMissingParams":0,"Unit\\Service\\AuthenticationServiceTest::testCreatePasswordConfigThrowsListsMissingParams":0,"Unit\\Service\\AuthenticationServiceTest::testCreatePasswordConfigBodyAuth":0,"Unit\\Service\\AuthenticationServiceTest::testCreatePasswordConfigBasicAuth":0,"Unit\\Service\\AuthenticationServiceTest::testCreatePasswordConfigOtherAuthType":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenThrowsWhenAllParamsMissing":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenThrowsMissingParamsList":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesRs256Token":1.134,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesRs384Token":1.202,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesRs512Token":1.268,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenGeneratesPs256Token":68.434,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenWithComplexTwigTemplate":0.001,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenWithoutX5tHasNoX5tInHeader":0,"Unit\\Service\\AuthenticationServiceTest::testFetchJWTTokenWithX5tAndRsaKey":0.965,"Unit\\Service\\AuthenticationServiceTest::testGetJwkReturnsHsKeyForHs256":0,"Unit\\Service\\AuthenticationServiceTest::testGetJwkReturnsHsKeyForHs384":0,"Unit\\Service\\AuthenticationServiceTest::testGetJwkReturnsHsKeyForHs512":0,"Unit\\Service\\AuthenticationServiceTest::testGetJwkReturnsRsKeyForRs256":0.056,"Unit\\Service\\AuthenticationServiceTest::testGetJwkReturnsRsKeyForPs256":0.082,"Unit\\Service\\AuthenticationServiceTest::testGetJwkThrowsForUnsupportedAlgorithm":0,"Unit\\Service\\AuthenticationServiceTest::testGetJwkThrowsForEdDsaAlgorithm":0,"Unit\\Service\\AuthenticationServiceTest::testGetHsJwkReturnsOctKey":0,"Unit\\Service\\AuthenticationServiceTest::testGetHsJwkEncodesSecretWithSpecialChars":0,"Unit\\Service\\AuthenticationServiceTest::testGetRsJwkReturnsRsaKey":0.063,"Unit\\Service\\AuthenticationServiceTest::testGetRsJwkCleansUpTempFile":0.042,"Unit\\Service\\AuthenticationServiceTest::testGetJwtPayloadParsesJsonPayload":0.001,"Unit\\Service\\AuthenticationServiceTest::testGetJwtPayloadRendersTwigVariables":0.001,"Unit\\Service\\AuthenticationServiceTest::testGetJwtPayloadWithNumericValues":0,"Unit\\Service\\AuthenticationServiceTest::testGenerateJwtProducesValidCompactSerialization":0,"Unit\\Service\\AuthenticationServiceTest::testGenerateJwtWithX5tAddsToHeader":0,"Unit\\Service\\AuthenticationServiceTest::testGenerateJwtPayloadMatchesInput":0,"Unit\\Service\\AuthenticationServiceTest::testCreateClientCredentialConfigWithJwtBearerAssertionThrowsOnInvalidKey":0.001,"Unit\\Service\\AuthenticationServiceTest::testCreateClientCredentialConfigWithoutJwtBearerAssertionType":0,"Unit\\Service\\TaskServiceTest::testCreateTaskWithDueDate":0,"Unit\\Service\\TaskServiceTest::testCreateTaskEscapesSpecialCharacters":0,"Unit\\Service\\TaskServiceTest::testUpdateTaskSetsCompletedTimestamp":0,"Unit\\Service\\TaskServiceTest::testUpdateTaskRemovesDueDate":0,"Unit\\Service\\TaskServiceTest::testUpdateTaskSetsDueDate":0,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectSkipsEmptyCalendarData":0,"Unit\\Service\\TaskServiceTest::testGetTasksForObjectSkipsNonMatchingUuid":0,"Unit\\Service\\TaskServiceTest::testFindUserCalendarWithArrayComponentSet":0,"Unit\\BackgroundJob\\CacheWarmupJobTest::testDefaultIntervalIsSet":0.001,"Unit\\BackgroundJob\\CacheWarmupJobTest::testZeroIntervalDisablesJobBySettingYearlyInterval":0,"Unit\\BackgroundJob\\CacheWarmupJobTest::testCustomIntervalIsRespected":0,"Unit\\BackgroundJob\\CacheWarmupJobTest::testRunSkipsWhenIntervalIsZero":0,"Unit\\BackgroundJob\\CacheWarmupJobTest::testRunCallsWarmupNameCacheOnHappyPath":0,"Unit\\BackgroundJob\\CacheWarmupJobTest::testRunLogsNamesLoaded":0,"Unit\\BackgroundJob\\CacheWarmupJobTest::testRunLogsErrorOnException":0,"Unit\\BackgroundJob\\CacheWarmupJobTest::testRunDoesNotRethrowException":0,"Unit\\BackgroundJob\\CacheWarmupJobTest::testRunStoresLastRunTimestamp":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testIntervalIsSetToFifteenMinutes":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunSkipsWhenExtractionModeIsBackground":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunSkipsWhenExtractionModeIsNone":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunSkipsWhenExtractionModeIsNotSet":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunReturnsEarlyWhenNoPendingFiles":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunProcessesPendingFiles":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunUsesDefaultBatchSizeWhenNotConfigured":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunSkipsFilesWithZeroFileId":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunContinuesProcessingAfterPerFileException":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunDoesNotPropagateOuterException":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunDoesNotPropagateFileMapperException":0,"Unit\\BackgroundJob\\CronFileTextExtractionJobTest::testRunLogsCompletionWithProcessedAndFailedCounts":0,"Unit\\BackgroundJob\\NameCacheWarmupJobTest::testIntervalIsSetToTwentyFourHours":0,"Unit\\BackgroundJob\\NameCacheWarmupJobTest::testRunCallsWarmupNameCache":0,"Unit\\BackgroundJob\\NameCacheWarmupJobTest::testRunLogsStartAndCompletion":0,"Unit\\BackgroundJob\\NameCacheWarmupJobTest::testRunLogsNamesLoadedInCompletionContext":0,"Unit\\BackgroundJob\\NameCacheWarmupJobTest::testRunWithZeroNamesLoadedCompletesNormally":0,"Unit\\BackgroundJob\\NameCacheWarmupJobTest::testRunLogsErrorWhenCacheHandlerThrows":0,"Unit\\BackgroundJob\\NameCacheWarmupJobTest::testRunDoesNotPropagateException":0,"Unit\\BackgroundJob\\NameCacheWarmupJobTest::testRunLogsExceptionMessageInErrorContext":0,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testIntervalIsSetToTwentyFourHours":0,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunSkipsWhenSolrDisabledInSettings":0.001,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunSkipsWhenSolrEnabledKeyMissing":0,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunSkipsWhenSolrServiceNotAvailable":0,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunUsesDefaultMaxObjectsWhenNotConfigured":0.016,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunUsesCustomMaxObjectsFromConfig":0,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunUsesCustomModeFromConfig":0,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunEnablesCollectErrorsFromConfig":0.001,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunLogsSuccessOnCompletedWarmup":0.001,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunLogsPerformanceStatsOnSuccess":0.001,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunLogsErrorWhenWarmupReturnsFailure":0.001,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunDoesNotPropagateException":0.001,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunLogsExceptionDetailsOnFailure":0.001,"Unit\\BackgroundJob\\SolrNightlyWarmupJobTest::testRunPassesSchemasToWarmupIndex":0.001,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunSkipsWhenSolrNotAvailable":0,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunUsesDefaultArguments":0,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunRespectsCustomArguments":0.001,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunLogsSuccessWhenWarmupSucceeds":0.001,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunLogsErrorWhenWarmupReturnsFailure":0,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunRethrowsExceptionAndLogsError":0.002,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunPassesSchemasToWarmupIndex":0.001,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunHandlesEmptySchemaList":0.001,"Unit\\BackgroundJob\\SolrWarmupJobTest::testRunIncludesTriggeredByInStartLog":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testSetupConnectionFailsWithDetails":0.004,"Unit\\Command\\SolrManagementCommandCoverageTest::testSetupExceptionPath":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testOptimizeWithCommitSuccess":0.002,"Unit\\Command\\SolrManagementCommandCoverageTest::testOptimizeWithCommitFailure":0.003,"Unit\\Command\\SolrManagementCommandCoverageTest::testOptimizeExceptionPath":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testOptimizeFailureReturn":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testWarmPartialFailure":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testWarmExceptionPath":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testHealthAllPass":0.002,"Unit\\Command\\SolrManagementCommandCoverageTest::testHealthExceptionPath":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testStatsWithTimingMetrics":0.005,"Unit\\Command\\SolrManagementCommandCoverageTest::testStatsWithFileAndChunkSections":0.002,"Unit\\Command\\SolrManagementCommandCoverageTest::testClearSuccessWithNullSuccessKey":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testSchemaCheckWithExtraFields":0.001,"Unit\\Command\\SolrManagementCommandCoverageTest::testSchemaCheckExceptionPath":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testExecuteWithSolrUnavailable":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testHandleInvalidAction":0,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testHandleOptimizeSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testHandleOptimizeWithCommit":0,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testHandleOptimizeFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testHandleClearWithoutForce":0,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testHandleClearWithForceSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testHandleClearWithForceFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Command\\SolrManagementCommandDeepTest::testHandleStatsUnavailable":0.001,"Unit\\Command\\SolrManagementCommandTest::testCommandHasActionArgument":0,"Unit\\Command\\SolrManagementCommandTest::testCommandHasForceOption":0,"Unit\\Command\\SolrManagementCommandTest::testCommandHasCommitOption":0,"Unit\\Command\\SolrManagementCommandTest::testCommandHasTenantCollectionOption":0,"Unit\\Command\\SolrManagementCommandTest::testSolrUnavailableOutputsSuggestion":0.001,"Unit\\Command\\SolrManagementCommandTest::testInvalidActionOutputsAvailableActions":0.001,"Unit\\Command\\SolrManagementCommandTest::testSetupConnectionFailureReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testSetupConnectionFailureOutputsError":0.001,"Unit\\Command\\SolrManagementCommandTest::testSetupExceptionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testOptimizeWithCommitCallsCommit":0.001,"Unit\\Command\\SolrManagementCommandTest::testOptimizeWithCommitFailedCommitStillSucceeds":0.001,"Unit\\Command\\SolrManagementCommandTest::testOptimizeWithoutCommitDoesNotCallCommit":0.001,"Unit\\Command\\SolrManagementCommandTest::testOptimizeExceptionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testOptimizeOutputsExecutionTime":0.001,"Unit\\Command\\SolrManagementCommandTest::testWarmAllQueriesSucceedReturnsSuccess":0.001,"Unit\\Command\\SolrManagementCommandTest::testWarmOneQueryFailsReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testWarmExceptionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testWarmOutputsQueryDescriptions":0.001,"Unit\\Command\\SolrManagementCommandTest::testHealthCheckWithConnectionFailureReturnsFailure":0.002,"Unit\\Command\\SolrManagementCommandTest::testHealthCheckWithTenantCollectionFailureReturnsFailure":0.002,"Unit\\Command\\SolrManagementCommandTest::testHealthCheckWithSearchFailureReturnsFailure":0.002,"Unit\\Command\\SolrManagementCommandTest::testHealthCheckExceptionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testHealthCheckOutputsStats":0.002,"Unit\\Command\\SolrManagementCommandTest::testSchemaCheckWithDocumentsReturnsSuccess":0.002,"Unit\\Command\\SolrManagementCommandTest::testSchemaCheckWithNoDocumentsReturnsSuccess":0.001,"Unit\\Command\\SolrManagementCommandTest::testSchemaCheckOutputsMissingFieldsWarning":0.001,"Unit\\Command\\SolrManagementCommandTest::testSchemaCheckWithSearchFailurePrintsWarning":0.001,"Unit\\Command\\SolrManagementCommandTest::testSchemaCheckExceptionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testClearWithoutForceOutputsSafetyMessage":0.001,"Unit\\Command\\SolrManagementCommandTest::testClearWithForceCallsServiceAndSucceeds":0.001,"Unit\\Command\\SolrManagementCommandTest::testClearWithForceServiceReturnsFalseGivesFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testClearWithForceServiceReturnsNoSuccessKeyGivesFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testClearExceptionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testStatsAvailableReturnsSuccess":0.001,"Unit\\Command\\SolrManagementCommandTest::testStatsOutputsBackendNumbers":0.001,"Unit\\Command\\SolrManagementCommandTest::testStatsWithFileSectionOutputsFileStats":0.001,"Unit\\Command\\SolrManagementCommandTest::testStatsWithChunkSectionOutputsChunkStats":0.001,"Unit\\Command\\SolrManagementCommandTest::testStatsExceptionReturnsFailure":0.001,"Unit\\Command\\SolrManagementCommandTest::testStatsWithSearchTimePrintsMilliseconds":0.001,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithUnderscoreLimitAndOffset":0,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithPageCalculatesOffset":0,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithUnderscorePageParam":0,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithSortParam":0,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithUnderscoreSortParam":0,"Unit\\Controller\\AuditTrailControllerTest::testIndexWithSearchParam":0,"Unit\\Controller\\AuditTrailControllerTest::testDestroyGeneralException":0,"Unit\\Controller\\AuditTrailControllerTest::testDestroyMultipleWithArrayIds":0,"Unit\\Controller\\BulkControllerTest::testDeleteEmptyUuidsArray":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteUuidsNotArray":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteAllSuccessful":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteRegisterNotFound":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaNotFound":0.001,"Unit\\Controller\\BulkControllerTest::testPublishEmptyUuidsArray":0,"Unit\\Controller\\BulkControllerTest::testPublishUuidsNotArray":0,"Unit\\Controller\\BulkControllerTest::testPublishWithSkippedUuids":0,"Unit\\Controller\\BulkControllerTest::testPublishWithValidDatetime":0,"Unit\\Controller\\BulkControllerTest::testPublishWithDatetimeTrue":0,"Unit\\Controller\\BulkControllerTest::testPublishWithDatetimeFalse":0,"Unit\\Controller\\BulkControllerTest::testPublishWithDatetimeNull":0,"Unit\\Controller\\BulkControllerTest::testPublishException":0,"Unit\\Controller\\BulkControllerTest::testPublishDefaultDatetimeWhenNotProvided":0,"Unit\\Controller\\BulkControllerTest::testDepublishWithSkippedUuids":0,"Unit\\Controller\\BulkControllerTest::testDepublishEmptyUuidsArray":0,"Unit\\Controller\\BulkControllerTest::testDepublishUuidsNotArray":0,"Unit\\Controller\\BulkControllerTest::testDepublishWithValidDatetime":0,"Unit\\Controller\\BulkControllerTest::testDepublishWithDatetimeTrue":0,"Unit\\Controller\\BulkControllerTest::testDepublishWithDatetimeFalse":0,"Unit\\Controller\\BulkControllerTest::testDepublishWithDatetimeNull":0,"Unit\\Controller\\BulkControllerTest::testDepublishInvalidDatetime":0,"Unit\\Controller\\BulkControllerTest::testDepublishDefaultDatetimeWhenNotProvided":0,"Unit\\Controller\\BulkControllerTest::testDepublishRegisterNotFound":0.001,"Unit\\Controller\\BulkControllerTest::testDepublishSchemaNotFound":0,"Unit\\Controller\\BulkControllerTest::testDepublishException":0,"Unit\\Controller\\BulkControllerTest::testSaveEmptyObjectsArray":0,"Unit\\Controller\\BulkControllerTest::testSaveObjectsNotArray":0,"Unit\\Controller\\BulkControllerTest::testSaveRegisterNotFound":0,"Unit\\Controller\\BulkControllerTest::testSaveSchemaNotFound":0,"Unit\\Controller\\BulkControllerTest::testSaveException":0.001,"Unit\\Controller\\BulkControllerTest::testSaveMixedSchemaMode":0.001,"Unit\\Controller\\BulkControllerTest::testSaveWithStatisticsMissingSavedKey":0.001,"Unit\\Controller\\BulkControllerTest::testSaveWithStatisticsMissingUpdatedKey":0.001,"Unit\\Controller\\BulkControllerTest::testSaveWithEmptyStatistics":0.001,"Unit\\Controller\\BulkControllerTest::testPublishSchemaWithPublishAllTrue":0,"Unit\\Controller\\BulkControllerTest::testPublishSchemaException":0,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaSuccess":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaWithHardDelete":0,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaException":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaObjectsSuccess":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaObjectsWithHardDelete":0.002,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaObjectsRegisterNotFound":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaObjectsSchemaNotFound":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteSchemaObjectsException":0.001,"Unit\\Controller\\BulkControllerTest::testDeleteRegisterException":0.001,"Unit\\Controller\\BulkControllerTest::testValidateSchemaException":0,"Unit\\Controller\\BulkControllerTest::testDeleteReturnsJsonResponse":0.001,"Unit\\Controller\\BulkControllerTest::testPublishReturnsJsonResponse":0,"Unit\\Controller\\BulkControllerTest::testDepublishReturnsJsonResponse":0,"Unit\\Controller\\BulkControllerTest::testSaveReturnsJsonResponse":0,"Unit\\Controller\\ChatControllerTest::testSendMessageException":0.001,"Unit\\Controller\\ChatControllerTest::testSendMessageAccessDeniedException":0.001,"Unit\\Controller\\ChatControllerTest::testGetHistoryException":0,"Unit\\Controller\\ChatControllerTest::testClearHistoryException":0,"Unit\\Controller\\ChatControllerTest::testSendFeedbackSuccess":0.001,"Unit\\Controller\\ChatControllerTest::testSendFeedbackUpdateExisting":0.001,"Unit\\Controller\\ChatControllerTest::testSendFeedbackMessageNotInConversation":0.001,"Unit\\Controller\\ChatControllerTest::testSendFeedbackException":0.001,"Unit\\Controller\\ChatControllerTest::testGetChatStatsSuccess":0.001,"Unit\\Controller\\ChatControllerTest::testSendMessageCreatesNewConversation":0.001,"Unit\\Controller\\ChatControllerTest::testSendMessageAgentNotFound":0,"Unit\\Controller\\ChatControllerTest::testSendMessageConversationNotFound":0,"Unit\\Controller\\ChatControllerTest::testSendMessageWithViewsAndTools":0.001,"Unit\\Controller\\ConfigurationControllerCoverageTest::testCheckVersionRemoteVersionNull":0.003,"Unit\\Controller\\ConfigurationControllerCoverageTest::testCheckVersionGenericException":0.001,"Unit\\Controller\\ConfigurationControllerCoverageTest::testPreviewReturnsJsonResponseDirectly":0.001,"Unit\\Controller\\ConfigurationControllerCoverageTest::testPreviewReturnsArray":0.001,"Unit\\Controller\\ConfigurationControllerCoverageTest::testExportWithIncludeObjectsTrue":0.001,"Unit\\Controller\\ConfigurationControllerCoverageTest::testExportWithIncludeObjectsFalse":0.001,"Unit\\Controller\\ConfigurationControllerCoverageTest::testDiscoverGitLabPath":0,"Unit\\Controller\\ConfigurationControllerCoverageTest::testGetGitHubRepositoriesWithPagination":0,"Unit\\Controller\\ConfigurationControllerCoverageTest::testGetGitHubRepositoriesDefaultPagination":0,"Unit\\Controller\\ConfigurationControllerCoverageTest::testGetGitHubConfigurationsMissingOwnerAndRepo":0,"Unit\\Controller\\ConfigurationControllerCoverageTest::testGetGitHubConfigurationsWithBranch":0,"Unit\\Controller\\ConfigurationControllerCoverageTest::testGetGitLabBranchesMissingParams":0,"Unit\\Controller\\ConfigurationControllerCoverageTest::testGetGitLabConfigurationsMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerDeepTest::testIndexReturnsConfigurations":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerDeepTest::testIndexException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationControllerDeepTest::testShowException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testCreateWithGithubSourceTypeSetsIsLocalFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testCreateWithManualSourceTypeSetsIsLocalTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testCreateWithUnknownSourceTypeDefaultsIsLocalTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testCreateStripsInternalAndDataParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testUpdateWithUrlSourceTypeSetsIsLocalFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testUpdateWithLocalSourceTypeSetsIsLocalTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testUpdateException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testExportException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testImportFromUploadedFile":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testImportReturnsJsonResponseFromGetUploadedJson":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ConfigurationsControllerTest::testImportException":0,"Unit\\Controller\\ConversationControllerTest::testIndexWithDeletedFilter":0,"Unit\\Controller\\ConversationControllerTest::testIndexWithPagination":0,"Unit\\Controller\\ConversationControllerTest::testShowReturns500OnException":0,"Unit\\Controller\\ConversationControllerTest::testMessagesNotFound":0,"Unit\\Controller\\ConversationControllerTest::testMessagesReturns500OnException":0,"Unit\\Controller\\ConversationControllerTest::testMessagesWithPagination":0.001,"Unit\\Controller\\ConversationControllerTest::testCreateWithAgentUuid":0.001,"Unit\\Controller\\ConversationControllerTest::testCreateWithAgentUuidNotFound":0.001,"Unit\\Controller\\ConversationControllerTest::testCreateWithAgentIdAutoTitle":0,"Unit\\Controller\\ConversationControllerTest::testUpdateWithMetadata":0.001,"Unit\\Controller\\ConversationControllerTest::testUpdateReturns500OnException":0,"Unit\\Controller\\ConversationControllerTest::testDestroyNotFound":0,"Unit\\Controller\\ConversationControllerTest::testDestroyReturns500OnException":0,"Unit\\Controller\\ConversationControllerTest::testRestoreAccessDenied":0,"Unit\\Controller\\ConversationControllerTest::testRestoreReturns500OnException":0,"Unit\\Controller\\ConversationControllerTest::testDestroyPermanentNotFound":0,"Unit\\Controller\\ConversationControllerTest::testDestroyPermanentReturns500OnException":0,"Unit\\Controller\\EndpointsControllerTest::testIndexReturnsEndpointsAndTotal":0,"Unit\\Controller\\EndpointsControllerTest::testIndexReturnsEmptyListWhenNoEndpoints":0,"Unit\\Controller\\EndpointsControllerTest::testIndexReturns500OnException":0,"Unit\\Controller\\EndpointsControllerTest::testShowReturnsEndpoint":0,"Unit\\Controller\\EndpointsControllerTest::testShowReturns404WhenNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testShowReturns500OnGenericException":0,"Unit\\Controller\\EndpointsControllerTest::testCreateReturns400WhenNameMissing":0,"Unit\\Controller\\EndpointsControllerTest::testCreateReturns400WhenEndpointMissing":0,"Unit\\Controller\\EndpointsControllerTest::testCreateReturns400WhenBothFieldsMissing":0,"Unit\\Controller\\EndpointsControllerTest::testCreateReturns400WhenFieldsAreEmpty":0,"Unit\\Controller\\EndpointsControllerTest::testCreateReturns201OnSuccess":0.001,"Unit\\Controller\\EndpointsControllerTest::testCreateReturns500OnException":0.001,"Unit\\Controller\\EndpointsControllerTest::testUpdateReturnsUpdatedEndpoint":0,"Unit\\Controller\\EndpointsControllerTest::testUpdateStripsIdFromRequestData":0.002,"Unit\\Controller\\EndpointsControllerTest::testUpdateReturns404WhenNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testUpdateReturns500OnGenericException":0,"Unit\\Controller\\EndpointsControllerTest::testDestroyReturns204OnSuccess":0.002,"Unit\\Controller\\EndpointsControllerTest::testDestroyReturns404WhenNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testDestroyReturns500OnGenericException":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointReturnsSuccessResult":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointWithTestData":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointReturnsFailureWithErrorMessage":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointReturnsFailureWithoutErrorKey":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointReturns404WhenNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testTestEndpointReturns500OnGenericException":0.001,"Unit\\Controller\\EndpointsControllerTest::testLogsReturnsLogsForEndpoint":0.002,"Unit\\Controller\\EndpointsControllerTest::testLogsWithCustomPagination":0.001,"Unit\\Controller\\EndpointsControllerTest::testLogsReturns404WhenEndpointNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testLogsReturns500OnGenericException":0.001,"Unit\\Controller\\EndpointsControllerTest::testLogStatsReturnsStatistics":0,"Unit\\Controller\\EndpointsControllerTest::testLogStatsReturns404WhenEndpointNotFound":0,"Unit\\Controller\\EndpointsControllerTest::testLogStatsReturns500OnGenericException":0,"Unit\\Controller\\EndpointsControllerTest::testAllLogsReturnsAllLogsWithoutFilter":0,"Unit\\Controller\\EndpointsControllerTest::testAllLogsWithEndpointIdFilter":0.001,"Unit\\Controller\\EndpointsControllerTest::testAllLogsWithEmptyEndpointIdFallsBackToAll":0,"Unit\\Controller\\EndpointsControllerTest::testAllLogsWithZeroEndpointIdFallsBackToAll":0,"Unit\\Controller\\EndpointsControllerTest::testAllLogsReturns500OnException":0,"Unit\\Controller\\EndpointsControllerTest::testAllLogsWithEndpointIdFilterException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testExtractFileNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testExtractException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testDiscoverException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testExtractAllException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testRetryFailedException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testShowNoChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testShowException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testCleanupReturnsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testFileTypesReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testVectorizeBatchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileExtractionControllerDeepTest::testIndexFilterByNonCompletedStatus":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexSuccessEmpty":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexWithSummaries":0.001,"Unit\\Controller\\FileExtractionControllerTest::testIndexFilterNonCompletedStatus":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexFilterStatusCompleted":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexFilterStatusEmpty":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexWithSearchTerm":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexWithEmptySearchTerm":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexWithRiskLevelFilter":0.001,"Unit\\Controller\\FileExtractionControllerTest::testIndexWithEmptyRiskLevelFilter":0.001,"Unit\\Controller\\FileExtractionControllerTest::testIndexSortByRiskLevelAsc":0.001,"Unit\\Controller\\FileExtractionControllerTest::testIndexSortByRiskLevelDesc":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexSortByEntityCountAsc":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexSortByEntityCountDesc":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexPhpSortWithPagination":0.001,"Unit\\Controller\\FileExtractionControllerTest::testIndexSortByRiskLevelWithUnknownRisk":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexSortByDbColumn":0,"Unit\\Controller\\FileExtractionControllerTest::testIndexRiskLevelFilterNoMatch":0,"Unit\\Controller\\FileExtractionControllerTest::testShowSuccess":0.003,"Unit\\Controller\\FileExtractionControllerTest::testShowNotFoundEmpty":0,"Unit\\Controller\\FileExtractionControllerTest::testShowException":0,"Unit\\Controller\\FileExtractionControllerTest::testExtractWithForceReExtract":0,"Unit\\Controller\\FileExtractionControllerTest::testExtractGeneralException":0,"Unit\\Controller\\FileExtractionControllerTest::testDiscoverWithCustomLimit":0,"Unit\\Controller\\FileExtractionControllerTest::testExtractAllWithCustomLimit":0,"Unit\\Controller\\FileExtractionControllerTest::testExtractAllException":0,"Unit\\Controller\\FileExtractionControllerTest::testRetryFailedWithCustomLimit":0,"Unit\\Controller\\FileExtractionControllerTest::testRetryFailedException":0,"Unit\\Controller\\FileExtractionControllerTest::testCleanupSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testFileTypesSuccess":0,"Unit\\Controller\\FileExtractionControllerTest::testVectorizeBatchSuccessDefaults":0,"Unit\\Controller\\FileExtractionControllerTest::testVectorizeBatchWithCustomParams":0,"Unit\\Controller\\FileExtractionControllerTest::testVectorizeBatchPartialParams":0,"Unit\\Controller\\FileSearchControllerCoverageTest::testKeywordSearchWithAuthCredentials":0.024,"Unit\\Controller\\FileSearchControllerCoverageTest::testSemanticSearchWithCustomLimit":0,"Unit\\Controller\\FileSearchControllerCoverageTest::testHybridSearchCustomWeights":0,"Unit\\Controller\\FileSearchControllerCoverageTest::testKeywordSearchGroupsResultsByFileId":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSearchControllerDeepTest::testSemanticSearchEmptyQuery":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSearchControllerDeepTest::testSemanticSearchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSearchControllerDeepTest::testHybridSearchEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSearchControllerDeepTest::testHybridSearchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSearchControllerDeepTest::testKeywordSearchEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSearchControllerDeepTest::testKeywordSearchNoFileCollection":0,"Unit\\Controller\\FileSearchControllerTest::testKeywordSearchWithFileTypes":0.001,"Unit\\Controller\\FileSearchControllerTest::testKeywordSearchWithMissingFileCollectionKey":0,"Unit\\Controller\\FileSearchControllerTest::testKeywordSearchWithEmptyFileCollectionString":0,"Unit\\Controller\\FileSearchControllerTest::testSemanticSearchReturnsEmptyResults":0,"Unit\\Controller\\FileSearchControllerTest::testHybridSearchReturnsWeightsInResponse":0,"Unit\\Controller\\FileTextControllerCoverageTest::testExtractFileTextEnabledWithValidScope":0.001,"Unit\\Controller\\FileTextControllerCoverageTest::testExtractFileTextEnabledWithNullScope":0.001,"Unit\\Controller\\FileTextControllerCoverageTest::testProcessAndIndexExtractedWithBothOptions":0.002,"Unit\\Controller\\FileTextControllerCoverageTest::testProcessAndIndexExtractedWithNullLimit":0,"Unit\\Controller\\FileTextControllerCoverageTest::testProcessAndIndexFileWithOptions":0,"Unit\\Controller\\FileTextControllerCoverageTest::testBulkExtractWithExactMaxLimit":0,"Unit\\Controller\\FileTextControllerCoverageTest::testBulkExtractWithOverMaxLimit":0,"Unit\\Controller\\FileTextControllerCoverageTest::testAnonymizeFileNotFoundReturns404":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testExtractFileTextWhenDisabled":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testExtractFileTextWhenScopeNone":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testExtractFileTextSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testExtractFileTextException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testBulkExtractException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testGetStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testProcessAndIndexExtractedException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testProcessAndIndexFileException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testGetChunkingStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testAnonymizeFileNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testAnonymizeFileAlreadyAnonymized":0.007,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testAnonymizeFileNoEntities":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileTextControllerDeepTest::testAnonymizeFileException":0,"Unit\\Controller\\FileTextControllerTest::testGetFileTextReturnsDeprecatedWithDifferentFileId":0,"Unit\\Controller\\FileTextControllerTest::testExtractFileTextDisabledWhenNoConfig":0,"Unit\\Controller\\FileTextControllerTest::testExtractFileTextDisabledWhenScopeIsNone":0,"Unit\\Controller\\FileTextControllerTest::testExtractFileTextSuccessWithDifferentFileId":0,"Unit\\Controller\\FileTextControllerTest::testExtractFileTextWithNullExtractionScope":0,"Unit\\Controller\\FileTextControllerTest::testBulkExtractCapsLimitAt500":0,"Unit\\Controller\\FileTextControllerTest::testBulkExtractUsesDefaultLimit":0,"Unit\\Controller\\FileTextControllerTest::testDeleteFileTextNotImplementedWithDifferentId":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexExtractedWithLimit":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexExtractedWithChunkSize":0.001,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexExtractedWithChunkOverlap":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexExtractedWithAllParams":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexFileWithChunkSize":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexFileWithChunkOverlap":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexFileWithAllParams":0,"Unit\\Controller\\FileTextControllerTest::testProcessAndIndexFileException":0,"Unit\\Controller\\FileTextControllerTest::testAnonymizeFileAlreadyAnonymizedMidName":0,"Unit\\Controller\\FileTextControllerTest::testAnonymizeFileSuccess":0.001,"Unit\\Controller\\FileTextControllerTest::testAnonymizeFileDeduplicatesEntities":0.001,"Unit\\Controller\\FileTextControllerTest::testAnonymizeFileExceptionDuringAnonymization":0,"Unit\\Controller\\FilesControllerTest::testIndexWithFilesReturnsFormattedData":0.001,"Unit\\Controller\\FilesControllerTest::testShowSuccessReturnsStreamResponse":0.004,"Unit\\Controller\\FilesControllerTest::testShowFileNotFoundReturns404":0.004,"Unit\\Controller\\FilesControllerTest::testShowFileNotFoundFallbackViaOwner":0.009,"Unit\\Controller\\FilesControllerTest::testShowFileNotFoundFallbackViaSystemUser":0.001,"Unit\\Controller\\FilesControllerTest::testShowFallbackUserFolderThrowsException":0.001,"Unit\\Controller\\FilesControllerTest::testShowFallbackEmptyNodesReturnsNotFound":0.001,"Unit\\Controller\\FilesControllerTest::testShowGeneralException":0,"Unit\\Controller\\FilesControllerTest::testShowFallbackNodeNotFileInstance":0.003,"Unit\\Controller\\FilesControllerTest::testCreateWithFilenameKey":0,"Unit\\Controller\\FilesControllerTest::testCreateGeneralException":0.001,"Unit\\Controller\\FilesControllerTest::testCreateWithStringShareTrue":0.001,"Unit\\Controller\\FilesControllerTest::testCreateWithStringShareYes":0.001,"Unit\\Controller\\FilesControllerTest::testCreateWithNumericShare":0,"Unit\\Controller\\FilesControllerTest::testCreateWithNullShare":0,"Unit\\Controller\\FilesControllerTest::testCreateWithNullTags":0,"Unit\\Controller\\FilesControllerTest::testCreateObjectNull":0.003,"Unit\\Controller\\FilesControllerTest::testSaveSuccess":0.001,"Unit\\Controller\\FilesControllerTest::testSaveMissingContent":0,"Unit\\Controller\\FilesControllerTest::testSaveEmptyContent":0,"Unit\\Controller\\FilesControllerTest::testSaveObjectNull":0,"Unit\\Controller\\FilesControllerTest::testSaveObjectNotFoundViaException":0,"Unit\\Controller\\FilesControllerTest::testSaveGeneralException":0.001,"Unit\\Controller\\FilesControllerTest::testSaveWithStringTags":0.001,"Unit\\Controller\\FilesControllerTest::testSaveWithShareTrue":0,"Unit\\Controller\\FilesControllerTest::testCreateMultipartObjectNull":0,"Unit\\Controller\\FilesControllerTest::testCreateMultipartSingleFileUpload":0.008,"Unit\\Controller\\FilesControllerTest::testCreateMultipartMultipleFilesUpload":0.017,"Unit\\Controller\\FilesControllerTest::testCreateMultipartGeneralException":0.001,"Unit\\Controller\\FilesControllerTest::testUpdateMetadataOnly":0,"Unit\\Controller\\FilesControllerTest::testUpdateGeneralException":0.001,"Unit\\Controller\\FilesControllerTest::testUpdateNoTagsProvided":0.003,"Unit\\Controller\\FilesControllerTest::testDeleteReturnsFalse":0,"Unit\\Controller\\FilesControllerTest::testPublishGeneralException":0.014,"Unit\\Controller\\FilesControllerTest::testDepublishGeneralException":0,"Unit\\Controller\\FilesControllerTest::testDownloadByIdGeneralException":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithBoolTrue":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithBoolFalse":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithStringTrue":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithString1":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithStringOn":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithStringYes":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithStringFalse":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithStringNo":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithString0":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithNumeric1":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithNumeric0":0,"Unit\\Controller\\FilesControllerTest::testParseBoolWithNull":0.004,"Unit\\Controller\\FilesControllerTest::testParseBoolWithArray":0.002,"Unit\\Controller\\FilesControllerTest::testNormalizeTagsWithArray":0,"Unit\\Controller\\FilesControllerTest::testNormalizeTagsWithString":0,"Unit\\Controller\\FilesControllerTest::testNormalizeTagsWithEmptyString":0,"Unit\\Controller\\FilesControllerTest::testNormalizeTagsWithNull":0,"Unit\\Controller\\FilesControllerTest::testNormalizeTagsWithInteger":0,"Unit\\Controller\\FilesControllerTest::testGetUploadErrorMessageIniSize":0,"Unit\\Controller\\FilesControllerTest::testGetUploadErrorMessageFormSize":0,"Unit\\Controller\\FilesControllerTest::testGetUploadErrorMessagePartial":0,"Unit\\Controller\\FilesControllerTest::testGetUploadErrorMessageNoFile":0,"Unit\\Controller\\FilesControllerTest::testGetUploadErrorMessageNoTmpDir":0,"Unit\\Controller\\FilesControllerTest::testGetUploadErrorMessageCantWrite":0,"Unit\\Controller\\FilesControllerTest::testGetUploadErrorMessageExtension":0,"Unit\\Controller\\FilesControllerTest::testGetUploadErrorMessageUnknown":0,"Unit\\Controller\\FilesControllerTest::testGetFileViaKnownUsersWithOwner":0.001,"Unit\\Controller\\FilesControllerTest::testGetFileViaKnownUsersNoUserFound":0,"Unit\\Controller\\FilesControllerTest::testGetFileViaKnownUsersAllExceptions":0,"Unit\\Controller\\FilesControllerTest::testValidateUploadedFileSuccess":0.001,"Unit\\Controller\\FilesControllerTest::testValidateUploadedFileWithError":0.002,"Unit\\Controller\\FilesControllerTest::testValidateUploadedFileNonReadable":0.001,"Unit\\Controller\\FilesControllerTest::testValidateUploadedFileNullError":0.001,"Unit\\Controller\\FilesControllerTest::testNormalizeMultipartFilesEmpty":0,"Unit\\Controller\\FilesControllerTest::testNormalizeMultipartFilesSingle":0.001,"Unit\\Controller\\FilesControllerTest::testNormalizeMultipartFilesMultiple":0,"Unit\\Controller\\FilesControllerTest::testNormalizeMultipleFilesWithScalarErrorAndSize":0,"Unit\\Controller\\FilesControllerTest::testNormalizeMultipleFilesWithMissingFields":0,"Unit\\Controller\\FilesControllerTest::testNormalizeSingleFileWithArrayTags":0,"Unit\\Controller\\FilesControllerTest::testNormalizeSingleFileWithMissingFields":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesThrowsWhenNoFiles":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesSingleFile":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesMultipart":0,"Unit\\Controller\\FilesControllerTest::testValidateAndGetObjectReturnsObject":0,"Unit\\Controller\\FilesControllerTest::testValidateAndGetObjectReturnsNull":0,"Unit\\Controller\\FilesControllerTest::testProcessUploadedFilesEmpty":0,"Unit\\Controller\\FilesControllerTest::testProcessUploadedFilesSuccess":0.004,"Unit\\Controller\\FilesControllerTest::testProcessUploadedFilesFailedRead":0.001,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexSuccessNoFilters":0.006,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexSuccessWithSearchFilter":0.003,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexSuccessWithTypeFilter":0.014,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexSuccessWithCategoryFilter":0.003,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexSuccessWithAllFilters":0.003,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexSuccessMultipleRows":0.004,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexSuccessEmptyResults":0.007,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexExceptionMessageContent":0,"Unit\\Controller\\GdprEntitiesControllerTest::testIndexDefaultLimitAndOffset":0.003,"Unit\\Controller\\GdprEntitiesControllerTest::testShowSuccessWithRelations":0.002,"Unit\\Controller\\GdprEntitiesControllerTest::testShowSuccessWithMultipleRelations":0,"Unit\\Controller\\GdprEntitiesControllerTest::testShowNotFoundMessage":0,"Unit\\Controller\\GdprEntitiesControllerTest::testShowExceptionMessageContent":0,"Unit\\Controller\\GdprEntitiesControllerTest::testShowSuccessEntityDataPassedThrough":0,"Unit\\Controller\\GdprEntitiesControllerTest::testGetTypesSuccess":0.002,"Unit\\Controller\\GdprEntitiesControllerTest::testGetTypesSuccessEmpty":0.001,"Unit\\Controller\\GdprEntitiesControllerTest::testGetTypesSingleType":0.001,"Unit\\Controller\\GdprEntitiesControllerTest::testGetTypesExceptionLogsError":0,"Unit\\Controller\\GdprEntitiesControllerTest::testGetCategoriesSuccess":0.001,"Unit\\Controller\\GdprEntitiesControllerTest::testGetCategoriesSuccessEmpty":0.002,"Unit\\Controller\\GdprEntitiesControllerTest::testGetCategoriesSingleCategory":0.001,"Unit\\Controller\\GdprEntitiesControllerTest::testGetCategoriesExceptionLogsError":0,"Unit\\Controller\\GdprEntitiesControllerTest::testGetStatsSuccess":0.008,"Unit\\Controller\\GdprEntitiesControllerTest::testGetStatsSuccessEmpty":0.003,"Unit\\Controller\\GdprEntitiesControllerTest::testGetStatsExceptionLogsError":0,"Unit\\Controller\\GdprEntitiesControllerTest::testGetStatsResponseStructure":0.003,"Unit\\Controller\\GdprEntitiesControllerTest::testDestroyNotFoundMessage":0.001,"Unit\\Controller\\GdprEntitiesControllerTest::testDestroyExceptionOnDelete":0.005,"Unit\\Controller\\GdprEntitiesControllerTest::testDestroyExceptionLogsError":0,"Unit\\Controller\\GdprEntitiesControllerTest::testDestroyCallsDeleteOnMapper":0.001,"Unit\\Controller\\HeartbeatControllerTest::testConstructorCreatesInstance":0,"Unit\\Controller\\HeartbeatControllerTest::testControllerExtendsBaseController":0,"Unit\\Controller\\HeartbeatControllerTest::testHeartbeatResponseContainsExactlyThreeKeys":0,"Unit\\Controller\\HeartbeatControllerTest::testHeartbeatTimestampIsPositiveInteger":0,"Unit\\Controller\\HeartbeatControllerTest::testMultipleHeartbeatCallsReturnConsistentStructure":0,"Unit\\Controller\\HeartbeatControllerTest::testConstructorWithDifferentAppName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleWithEmptyBody":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleMissingJsonrpcVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleWrongJsonrpcVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleMissingMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleInvalidRequestPreservesId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleInvalidRequestWithoutIdReturnsNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleNotificationReturns202":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleNotificationWithNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleInitializeSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleInitializeWithNoParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleInitializeException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleRequiresSessionForNonInitializeMethods":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleInvalidSessionReturnsError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandlePing":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleToolsList":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleToolCallSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleToolCallWithoutArguments":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleToolCallMissingName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourcesList":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourceReadSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourceReadMissingUri":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourcesTemplatesList":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleUnknownMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleDispatchInternalError":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleToolCallInvalidArgumentException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourceReadBadMethodCallException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourcesListInternalError":0.007,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleToolsListInternalError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourcesTemplatesListInternalError":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testSuccessResponseStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testErrorResponseStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleWithStringId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleWithIntegerId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleToolCallWithEmptyParams":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourceReadWithEmptyParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleInitializeDoesNotRequireSession":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleNotificationWithDifferentMethods":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleResourceReadInvalidArgument":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\McpServerControllerTest::testHandleToolCallGeneralException":0,"Unit\\Controller\\OrganisationControllerTest::testSetActiveSuccessWithNullActiveOrg":0,"Unit\\Controller\\OrganisationControllerTest::testCreateWithUuidFromRequestBody":0,"Unit\\Controller\\OrganisationControllerTest::testCreateReturnsBadRequestForEmptyStringName":0,"Unit\\Controller\\OrganisationControllerTest::testJoinWithUserIdPassesUserIdToService":0,"Unit\\Controller\\OrganisationControllerTest::testJoinReturnsBadRequestOnException":0,"Unit\\Controller\\OrganisationControllerTest::testJoinExceptionWithUserId":0,"Unit\\Controller\\OrganisationControllerTest::testLeaveReturnsBadRequestOnFailure":0,"Unit\\Controller\\OrganisationControllerTest::testLeaveReturnsBadRequestOnException":0,"Unit\\Controller\\OrganisationControllerTest::testLeaveExceptionWithUserId":0,"Unit\\Controller\\OrganisationControllerTest::testShowReturnsOrganisationWithChildren":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateReturnsForbiddenWhenNoAccess":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithNameOnly":0.003,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithNameAndSlug":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithDescription":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithActiveFieldTrue":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithActiveFieldEmptyString":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithActiveFieldFalse":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithQuotaFields":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithArrayFields":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateIgnoresNonArrayGroups":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateIgnoresNonArrayAuthorization":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithParentSet":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithParentNull":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateSuccessWithParentEmptyString":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateReturnsBadRequestOnCircularParent":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNoParentKeyDoesNotCallValidation":0.002,"Unit\\Controller\\OrganisationControllerTest::testUpdateReturnsBadRequestOnException":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateStripsRouteFromRequestData":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithEmptyNameDoesNotSetName":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithEmptySlugDoesNotOverride":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithAllFields":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNameButNullSlugAutoGenerates":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNullNameDoesNotSetName":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNullDescriptionDoesNotSet":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithEmptyDescription":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNullActiveDoesNotSetActive":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithOnlySingleQuotaField":0.003,"Unit\\Controller\\OrganisationControllerTest::testUpdateSaveThrowsException":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNameSpecialCharactersGeneratesSlug":0.006,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithEmptyRequestDataNoChanges":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNameEmptySlugAutoGenerates":0.001,"Unit\\Controller\\OrganisationControllerTest::testPatchSuccessfulUpdate":0,"Unit\\Controller\\OrganisationControllerTest::testPatchReturnsForbiddenWhenNoAccess":0,"Unit\\Controller\\OrganisationControllerTest::testSearchWithQueryUsesFindByName":0,"Unit\\Controller\\OrganisationControllerTest::testSearchWithWhitespaceOnlyQueryUsesFindAll":0,"Unit\\Controller\\OrganisationControllerTest::testSearchClampsLimitToMax100":0,"Unit\\Controller\\OrganisationControllerTest::testSearchClampsLimitToMin1":0,"Unit\\Controller\\OrganisationControllerTest::testSearchClampsNegativeOffsetToZero":0,"Unit\\Controller\\OrganisationControllerTest::testSearchWithCustomPagination":0,"Unit\\Controller\\OrganisationControllerTest::testSearchWithEmptyResultsReturnsEmptyArray":0,"Unit\\Controller\\OrganisationControllerTest::testSearchByNameException":0,"Unit\\Controller\\OrganisationControllerTest::testCreateWithDefaultDescription":0,"Unit\\Controller\\OrganisationControllerTest::testCreateWithTabOnlyName":0,"Unit\\Controller\\OrganisationControllerTest::testJoinWithNullUserIdInParams":0,"Unit\\Controller\\OrganisationControllerTest::testJoinExceptionWithoutUserIdInParams":0,"Unit\\Controller\\OrganisationControllerTest::testLeaveWithNullUserIdReturnsLeftMessage":0,"Unit\\Controller\\OrganisationControllerTest::testLeaveExceptionWithNullUserId":0,"Unit\\Controller\\OrganisationControllerTest::testShowExceptionFromFindByUuid":0,"Unit\\Controller\\OrganisationControllerTest::testShowExceptionFromFindChildrenChain":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNameAndWhitespaceOnlySlugAutoGenerates":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNullGroupsDoesNotSet":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNullAuthorizationDoesNotSet":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithBandwidthQuotaOnly":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithRequestQuotaOnly":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNullQuotaFieldsDoesNotSet":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNameContainingLeadingTrailingSpaces":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithDescriptionLeadingTrailingSpaces":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithSlugLeadingTrailingSpaces":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateFindByUuidThrowsException":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithVeryLongNameTruncatesSlug":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithNameContainingUnicodeChars":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateParentValidationSuccessWithNewParent":0.001,"Unit\\Controller\\OrganisationControllerTest::testSearchWithMultipleResults":0.001,"Unit\\Controller\\OrganisationControllerTest::testSearchByNameWithCustomPagination":0,"Unit\\Controller\\OrganisationControllerTest::testSearchWithNegativeLimitClampsToOne":0,"Unit\\Controller\\OrganisationControllerTest::testPatchWithNameAndDescription":0.001,"Unit\\Controller\\OrganisationControllerTest::testPatchExceptionReturnsBadRequest":0,"Unit\\Controller\\OrganisationControllerTest::testIndexReturnsComplexStats":0,"Unit\\Controller\\OrganisationControllerTest::testIndexExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testSetActiveExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testGetActiveExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testCreateExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testShowExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testSearchExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testClearCacheExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testStatsExceptionLogsError":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithActiveFieldTruthyString":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithActiveFieldZeroString":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithEmptyGroupsArray":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithEmptyAuthorizationArray":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateWithMultipleRouteAndRegularParams":0.001,"Unit\\Controller\\OrganisationControllerTest::testStatsReturnsDetailedStatistics":0,"Unit\\Controller\\OrganisationControllerTest::testClearCacheCallsService":0,"Unit\\Controller\\OrganisationControllerTest::testUpdateParentCircularReferenceLogsWarning":0.001,"Unit\\Controller\\OrganisationControllerTest::testUpdateReturnsSavedOrganisationData":0.001,"Unit\\Controller\\OrganisationControllerTest::testCreateWithUuidNotInParams":0.001,"Unit\\Controller\\OrganisationControllerTest::testSearchQueryTrimsWhitespace":0,"Unit\\Controller\\SearchTrailControllerCoverageTest::testClearAllThrowsErrorDueToUndefinedMethod":0.003,"Unit\\Controller\\SearchTrailControllerCoverageTest::testCleanupWithNullBeforeDate":0,"Unit\\Controller\\SearchTrailControllerCoverageTest::testDestroyMultipleReturnsNotImplemented":0,"Unit\\Controller\\SearchTrailControllerCoverageTest::testDestroyWithExistingTrail":0.003,"Unit\\Controller\\SearchTrailControllerCoverageTest::testDestroyWithGeneralException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testShowNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testShowException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testDestroyNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testDestroyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testDestroyMultiple":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testStatisticsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testCleanupInvalidDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testCleanupException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testArrayToCsvEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testArrayToCsvWithData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testPaginateWithOffsetAndPage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SearchTrailControllerDeepTest::testExtractRequestParametersWithDates":0,"Unit\\Controller\\SearchTrailControllerTest::testIndexStripsRouteAndIdParams":0,"Unit\\Controller\\SearchTrailControllerTest::testIndexWithMissingServiceResultKeys":0,"Unit\\Controller\\SearchTrailControllerTest::testIndexPaginationWithMultiplePages":0,"Unit\\Controller\\SearchTrailControllerTest::testIndexPaginationMiddlePage":0.001,"Unit\\Controller\\SearchTrailControllerTest::testIndexPaginationLastPage":0,"Unit\\Controller\\SearchTrailControllerTest::testStatisticsWithDateFilters":0,"Unit\\Controller\\SearchTrailControllerTest::testStatisticsWithInvalidFromDate":0,"Unit\\Controller\\SearchTrailControllerTest::testStatisticsWithInvalidToDate":0,"Unit\\Controller\\SearchTrailControllerTest::testPopularTermsReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testPopularTermsWithDateFilters":0,"Unit\\Controller\\SearchTrailControllerTest::testActivityWithCustomInterval":0,"Unit\\Controller\\SearchTrailControllerTest::testRegisterSchemaStatsReturnsData":0.001,"Unit\\Controller\\SearchTrailControllerTest::testRegisterSchemaStatsReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testRegisterSchemaStatsWithMissingKeys":0,"Unit\\Controller\\SearchTrailControllerTest::testUserAgentStatsReturnsStructuredData":0.001,"Unit\\Controller\\SearchTrailControllerTest::testUserAgentStatsReturnsSimpleArray":0,"Unit\\Controller\\SearchTrailControllerTest::testUserAgentStatsReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testUserAgentStatsWithEmptyBrowserDistribution":0,"Unit\\Controller\\SearchTrailControllerTest::testUserAgentStatsWithNullBrowserDistribution":0,"Unit\\Controller\\SearchTrailControllerTest::testCleanupWithValidDate":0,"Unit\\Controller\\SearchTrailControllerTest::testDestroyReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testExportJsonFormat":0.003,"Unit\\Controller\\SearchTrailControllerTest::testExportJsonFormatWithMetadata":0.001,"Unit\\Controller\\SearchTrailControllerTest::testExportCsvFormat":0.001,"Unit\\Controller\\SearchTrailControllerTest::testExportCsvFormatWithMetadata":0.001,"Unit\\Controller\\SearchTrailControllerTest::testExportEmptyResultsJson":0,"Unit\\Controller\\SearchTrailControllerTest::testExportEmptyResultsCsv":0,"Unit\\Controller\\SearchTrailControllerTest::testExportReturns500OnException":0,"Unit\\Controller\\SearchTrailControllerTest::testExportMultipleTrails":0.001,"Unit\\Controller\\SearchTrailControllerTest::testExportWithFiltersAndSearch":0,"Unit\\Controller\\SearchTrailControllerTest::testClearAllCatchesServerError":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithLimitParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithUnderscoreLimitParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithOffsetParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithUnderscoreOffsetParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithPageParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithUnderscorePageParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersPageCalculatesOffset":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithSearchParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithUnderscoreSearchParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithSortParams":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersWithUnderscoreSortParams":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersFiltersOutSystemParams":0,"Unit\\Controller\\SearchTrailControllerTest::testPaginateWithOffsetButPageOne":0,"Unit\\Controller\\SearchTrailControllerTest::testPaginateWithTotalLessThanResultCount":0,"Unit\\Controller\\SearchTrailControllerTest::testPaginateNextUrlWithLegacyPageParam":0.001,"Unit\\Controller\\SearchTrailControllerTest::testPaginatePrevUrlWithLegacyPageParam":0,"Unit\\Controller\\SearchTrailControllerTest::testPaginateNextUrlWithNoPageParam":0,"Unit\\Controller\\SearchTrailControllerTest::testPaginatePrevUrlWithNoPageParam":0,"Unit\\Controller\\SearchTrailControllerTest::testPaginateWithNullValues":0,"Unit\\Controller\\SearchTrailControllerTest::testPaginateNextUrlWithNoQueryString":0.001,"Unit\\Controller\\SearchTrailControllerTest::testPaginatePrevUrlWithNoQueryString":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersBothLimitAndUnderscoreLimit":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersBothOffsetAndUnderscoreOffset":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersBothPageAndUnderscorePage":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersSortWithUnderscorePrefix":0,"Unit\\Controller\\SearchTrailControllerTest::testExtractRequestParametersSortWithoutOrderParam":0,"Unit\\Controller\\SearchTrailControllerTest::testExportWithDefaultFormatIsCsv":0.002,"Unit\\Controller\\SearchTrailControllerTest::testPopularTermsWithPageAndOffset":0.001,"Unit\\Controller\\SearchTrailControllerTest::testActivityWithDateFilters":0.001,"Unit\\Controller\\SearchTrailControllerTest::testRegisterSchemaStatsWithDateFilters":0,"Unit\\Controller\\SearchTrailControllerTest::testUserAgentStatsWithDateFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testSaveApiTokensOnlySavesGitLabUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testSaveApiTokensWithNoParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testGetApiTokensMasksOnlyNonEmptyTokens":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testTestGitHubTokenUsesTokenFromRequest":0.422,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testTestGitLabTokenUsesUrlFromRequest":0.11,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testTestGitLabTokenDefaultsToGitLabDotCom":0.3,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testSaveApiTokensMaskedGithubTokenNotSaved":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\ApiTokenSettingsControllerTest::testSaveApiTokensMaskedGitlabTokenNotSaved":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testGetWarmupIntervalWithEmptyLastRun":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testSetWarmupIntervalDisabledSetsZeroMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testSetWarmupIntervalWithExactlyMinimumAllowed":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testSetWarmupIntervalException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testSetWarmupIntervalLogsMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testSetWarmupIntervalDefaultWhenNotProvided":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearCacheWithSpecificType":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearSpecificCollectionReturnsCollectionName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearSpecificCollectionFailureContainsMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheSuccess":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheAllType":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheFileNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheInvalidJsonFormat":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheAppstoreFolderNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheGeneralException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheDefaultTypeIsApps":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheDiscoverType":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\CacheSettingsControllerTest::testClearAppStoreCacheGenericFileException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testGetFileSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testGetFileSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testUpdateFileSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testUpdateFileSettingsExtractsProviderIdFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testUpdateFileSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testTestDolphinConnectionEmptyInputs":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testTestPresidioConnectionEmptyEndpoint":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testTestOpenAnonymiserConnectionEmptyEndpoint":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testGetFileCollectionFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testCreateMissingFileFieldsNoCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testCreateMissingFileFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testIndexFileException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testGetFileIndexStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testGetFileExtractionStatsReturnsZerosOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerBranchTest::testReindexFilesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testGetFileSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testGetFileSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testUpdateFileSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testUpdateFileSettingsExtractsProviderIdFromArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testUpdateFileSettingsExtractsChunkingStrategyIdFromArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testUpdateFileSettingsWithStringProviderPassesThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testUpdateFileSettingsWithNullProviderPassesThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testUpdateFileSettingsArrayProviderMissingId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testUpdateFileSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testGetFileCollectionFieldsSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testGetFileCollectionFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testCreateMissingFileFieldsNoFileCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testCreateMissingFileFieldsNullFileCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testCreateMissingFileFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testIndexFileSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testIndexFileFailure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testIndexFileFailureNoErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testIndexFileException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testGetFileIndexStatsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testGetFileIndexStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testGetFileExtractionStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testReindexFilesNoFilesToReindex":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testReindexFilesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testWarmupFilesNoFilesToProcess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testWarmupFilesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerCoverageTest::testWarmupFilesWithSkipIndexedFalse":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileSettingsReturnsFullData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsExtractsProviderIdFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsExtractsChunkingStrategyIdFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsExtractsBothProviderAndChunkingIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsProviderObjectWithoutId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsChunkingObjectWithoutId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsHandlesNullChunkingStrategy":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testUpdateFileSettingsHandlesStringChunkingStrategy":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionEmptyEndpoint":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionEmptyKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionBothEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionHealthCheckFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionWithRealCurlFail":10.034,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionRealWithValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestPresidioConnectionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestPresidioConnectionHealthCheckFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestPresidioConnectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestPresidioConnectionWithRealCurlFail":10.014,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestOpenAnonymiserConnectionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestOpenAnonymiserConnectionHealthCheckFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestOpenAnonymiserConnectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestOpenAnonymiserConnectionWithRealCurlFail":10.015,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileCollectionFieldsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testCreateMissingFileFieldsNoCollectionConfiguredEmpty":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testCreateMissingFileFieldsNoCollectionNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testCreateMissingFileFieldsNoCollectionKeyMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testCreateMissingFileFieldsReflectionFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testWarmupFilesNoFilesToProcess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testWarmupFilesWithSkipIndexedTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testWarmupFilesWithSkipIndexedFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testWarmupFilesWithMultipleBatches":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testWarmupFilesWithErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testWarmupFilesErrorsTruncatedTo20":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testWarmupFilesMaxFilesCapAt5000":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testIndexFileReturns422WithDefaultMessageWhenNoErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testIndexFilePassesCorrectFileId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testReindexFilesNoFilesToReindex":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testReindexFilesSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testReindexFilesWithMultipleBatchesAndErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testReindexFilesErrorsTruncatedTo20":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileIndexStatsReturnsEmptyStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileExtractionStatsSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileExtractionStatsUntrackedFilesClampedToZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestDolphinConnectionRealExceptionPath":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestPresidioConnectionRealSuccessPath":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testTestOpenAnonymiserConnectionRealPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testCreateMissingFileFieldsSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testCreateMissingFileFieldsReturnsFalseFromEnsure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\FileSettingsControllerTest::testGetFileExtractionStatsMissingSolrTotalChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testGetLLMSettingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testGetLLMSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testUpdateLLMSettingsExtractsModelIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testUpdateLLMSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testPatchLLMSettingsDelegatesToUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testTestEmbeddingMissingProvider":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testTestEmbeddingInvalidConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testTestEmbeddingSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testTestEmbeddingFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testTestEmbeddingException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testTestChatMissingProvider":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testTestChatInvalidConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testCheckEmbeddingModelMismatchSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testCheckEmbeddingModelMismatchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testClearAllEmbeddingsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testClearAllEmbeddingsFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testClearAllEmbeddingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testGetVectorStatsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerBranchTest::testGetVectorStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testTestChatSuccessPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testUpdateLLMSettingsNoConfigKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testUpdateLLMSettingsPartialConfigs":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testCheckEmbeddingModelMismatchNoVectors":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testGetVectorStatsResponseContainsExpectedKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testGetVectorStatsExceptionIncludesTrace":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerCoverageTest::testClearAllEmbeddingsSuccessContainsDeletedCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestEmbeddingException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestChatInvalidConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestChatNamedParamBugHitsExceptionPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestChatException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testUpdateLLMSettingsWithStringModelValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testUpdateLLMSettingsWithNullModelValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetOllamaModelsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testUpdateLLMSettingsWithArrayModelMissingId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testUpdateLLMSettingsResponseContainsMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestEmbeddingWithDefaultTestText":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testTestChatWithDefaultMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testCheckEmbeddingModelMismatchReturnsMismatchData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testClearAllEmbeddingsReturns500WhenSuccessFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetVectorStatsContainsTimestamp":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testPatchLLMSettingsWithAllModelConfigs":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetOllamaModelsCurlErrorPath":5.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\LlmSettingsControllerTest::testGetOllamaModelsHttpNon200":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetN8nSettingsNullApiKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testUpdateN8nSettingsEmptyApiKeyInResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testUpdateN8nSettingsNoApiKeyInParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionMissingUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionMissingApiKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionSuccessWithTrailingSlash":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionSuccessEmptyDataField":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionNon2xxStatus":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testTestN8nConnectionStatus300":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nMissingUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nMissingApiKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nExistingProject":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nCreatesNewProject":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nCreatesNewProjectWithCustomName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nProjectCreationReturnsNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nEmptyProjectsList":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nInnerException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nOuterException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testInitializeN8nWorkflowsWithNoDataKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsMissingUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsMissingApiKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsWithTrailingSlashUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsProjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsProjectNotFoundEmptyList":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsDefaultProjectName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\N8nSettingsControllerTest::testGetWorkflowsNoDataKeyInResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SecuritySettingsControllerTest::testClearAllRateLimitsUsernameOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testGetSolrFieldsWithMissingAndExtraFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testGetSolrFieldsOnlyObjectMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsSuccessNoMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsSuccessWithMissingFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsDryRun":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsWithErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsObjectExceptionFileSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsFileException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsBothExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateMissingSolrFieldsWithNullCounts":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testFixMismatchedSolrFieldsFieldsConfigFailed":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testFixMismatchedSolrFieldsFieldsConfigFailedNoMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testFixMismatchedSolrFieldsNoMismatches":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testFixMismatchedSolrFieldsWithMismatches":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testFixMismatchedSolrFieldsWithFieldsConfigNoFieldsKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSolrFieldProtectedField":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSolrFieldSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSolrFieldFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSolrFieldFailureNoError":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSolrFieldException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSpecificSolrCollectionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSpecificSolrCollectionFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSpecificSolrCollectionFailureNoErrorCode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testDeleteSpecificSolrCollectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testListSolrCollectionsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testListSolrConfigSetsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateSolrConfigSetWithCustomBase":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCreateSolrCollectionWithCustomParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testCopySolrCollectionWithCopyData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testUpdateSolrCollectionAssignmentsOnlyObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testUpdateSolrCollectionAssignmentsOnlyFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testUpdateSolrCollectionAssignmentsBothNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrManagementControllerTest::testUpdateSolrCollectionAssignmentsSaveException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexValidModes#serial mode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexValidModes#parallel mode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexValidModes#hyper mode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexInvalidModes#batch mode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexInvalidModes#async mode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexInvalidModes#fast mode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexInvalidModes#empty string":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testTestSolrConnectionReturnsFailureData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexInvalidMode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexParallelModeHitsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexHyperModeHitsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexSerialModeHitsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexWithZeroMaxObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexStringCollectErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexStringCollectErrorsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexContainerException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexSuccessWithCustomQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexRowsClamped":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexRowsMinimum":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexNegativeStart":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexFailureWithoutErrorDetails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testGetSolrMemoryPredictionReflectionException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testGetSolrMemoryPredictionSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testGetSolrMemoryPredictionSuccessZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testGetSolrMemoryPredictionZeroMaxObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrOptimizeFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrClearFailureWithoutError":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrEmptyOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrOptimizeException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrClearException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrDeleteOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrReindexOperation":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexNegativeRows":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexExactly100Rows":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexExactly1Row":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testTestSolrConnectionEmptyResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrCommitTimestampFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrOptimizeTimestampFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrClearWithFullErrorDetails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrClearSuccessNoErrorKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testGetSolrMemoryPredictionExceptionMessageFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testTestSolrConnectionExceptionMessageFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testSetupSolrExceptionBeforeSetupCreated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testSetupSolrExceptionReturns422WithErrorStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testSetupSolrSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testSetupSolrExceptionResponseHasTimestamp":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testSetupSolrExceptionWithNullPort":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testSetupSolrContainerExceptionAfterSettingsLoad":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexParallelModeSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testWarmupSolrIndexHyperModeSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testGetSolrMemoryPredictionUnavailableZeroObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrClearWithErrorDetailsKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrCommitSuccessHasAllKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testManageSolrOptimizeSuccessHasAllKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testInspectSolrIndexLargeStartValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrOperationsControllerTest::testTestSolrConnectionReturnsAllServiceFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrInfoCollectionListingFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrInfoCollectionWithDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrInfoMultipleCollections":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoverySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryMergesExistingConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryNoDefaultSettings":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryEmptyDiscoveredFacets":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryMultipleSelfFacets":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryMultipleObjectFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryExistingConfigWithUnderscoreKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryFacetWithNoSuggestions":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrSettingsReturnsExactData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrSettingsExceptionContainsMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrSettingsPassesParamsToService":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrSettingsExceptionContainsMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigurationReturnsData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrFacetConfigurationPassesParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testDiscoverSolrFacetsExceptionMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testUpdateSolrFacetConfigWithDiscoveryPassesParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrInfoStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrInfoAvailableStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\Settings\\SolrSettingsControllerTest::testGetSolrFacetConfigWithDiscoveryGlobalSettingsFromExisting":0,"Unit\\Controller\\SettingsControllerCoverageTest::testGetConfigurationServiceReturnsServiceWhenInstalled":0,"Unit\\Controller\\SettingsControllerCoverageTest::testUpdateSearchBackendSuccess":0,"Unit\\Controller\\SettingsControllerCoverageTest::testUpdateSearchBackendUsingActiveKey":0,"Unit\\Controller\\SettingsControllerCoverageTest::testUpdateSearchBackendMissingBackend":0,"Unit\\Controller\\SettingsControllerCoverageTest::testUpdateSearchBackendException":0,"Unit\\Controller\\SettingsControllerCoverageTest::testSetupHandlerSolrDisabled":0,"Unit\\Controller\\SettingsControllerCoverageTest::testSetupHandlerException":0,"Unit\\Controller\\SettingsControllerCoverageTest::testReindexSpecificCollectionSuccess":0,"Unit\\Controller\\SettingsControllerCoverageTest::testReindexSpecificCollectionServiceFailure":0,"Unit\\Controller\\SettingsControllerCoverageTest::testReindexSpecificCollectionBatchSizeZero":0,"Unit\\Controller\\SettingsControllerCoverageTest::testSchemaMappingException":0,"Unit\\Controller\\SettingsControllerCoverageTest::testGetDatabaseInfoWithSqlitePlatform":0,"Unit\\Controller\\SettingsControllerCoverageTest::testGetDatabaseInfoWithMysqlPlatform":0,"Unit\\Controller\\SettingsControllerCoverageTest::testGetDatabaseInfoWithInvalidCachedJson":0,"Unit\\Controller\\SettingsControllerCoverageTest::testRefreshDatabaseInfoDeletesCache":0,"Unit\\Controller\\SettingsControllerCoverageTest::testHybridSearchWithNonArrayResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testGetObjectServiceWhenInstalled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testGetObjectServiceWhenNotInstalled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testGetConfigurationServiceWhenInstalled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testGetConfigurationServiceWhenNotInstalled":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testUpdateException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testLoadException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testRebaseException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testGetStatisticsCallsStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testUpdatePublishingOptionsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testGetSearchBackendException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testUpdateSearchBackendEmptyBackend":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testSemanticSearchEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testSemanticSearchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testHybridSearchEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testHybridSearchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerDeepTest::testGetVersionInfoException":0,"Unit\\Controller\\SettingsControllerGapTest::testGetObjectServiceReturnsNullWhenInstalled":0,"Unit\\Controller\\SettingsControllerGapTest::testGetObjectServiceThrowsWhenNotInstalled":0,"Unit\\Controller\\SettingsControllerGapTest::testGetConfigurationServiceThrowsWhenNotInstalled":0,"Unit\\Controller\\SettingsControllerGapTest::testLoadReturnsSettings":0,"Unit\\Controller\\SettingsControllerGapTest::testLoadException":0,"Unit\\Controller\\SettingsControllerGapTest::testUpdatePublishingOptions":0,"Unit\\Controller\\SettingsControllerGapTest::testUpdatePublishingOptionsException":0,"Unit\\Controller\\SettingsControllerGapTest::testRebaseSuccess":0,"Unit\\Controller\\SettingsControllerGapTest::testRebaseException":0,"Unit\\Controller\\SettingsControllerGapTest::testGetStatisticsIsAliasForStats":0,"Unit\\Controller\\SettingsControllerGapTest::testGetVersionInfo":0,"Unit\\Controller\\SettingsControllerGapTest::testGetVersionInfoException":0,"Unit\\Controller\\SettingsControllerGapTest::testGetSearchBackend":0,"Unit\\Controller\\SettingsControllerGapTest::testGetSearchBackendException":0,"Unit\\Controller\\SettingsControllerGapTest::testUpdateSearchBackendEmptyBackend":0,"Unit\\Controller\\SettingsControllerGapTest::testSemanticSearchEmptyQuery":0,"Unit\\Controller\\SettingsControllerGapTest::testSemanticSearchSuccess":0,"Unit\\Controller\\SettingsControllerGapTest::testSemanticSearchException":0,"Unit\\Controller\\SettingsControllerGapTest::testHybridSearchEmptyQuery":0,"Unit\\Controller\\SettingsControllerGapTest::testHybridSearchSuccess":0,"Unit\\Controller\\SettingsControllerGapTest::testHybridSearchException":0,"Unit\\Controller\\SettingsControllerGapTest::testGetDatabaseInfoWithCachedData":0,"Unit\\Controller\\SettingsControllerGapTest::testGetDatabaseInfoException":0,"Unit\\Controller\\SettingsControllerGapTest::testReindexSpecificCollectionInvalidBatchSize":0,"Unit\\Controller\\SettingsControllerGapTest::testReindexSpecificCollectionNegativeMaxObjects":0,"Unit\\Controller\\SettingsControllerGapTest::testReindexSpecificCollectionException":0,"Unit\\Controller\\SettingsControllerGapTest::testSemanticSearchWithFiltersAndProvider":0,"Unit\\Controller\\SettingsControllerGapTest::testHybridSearchWithCustomWeights":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionReturns422WhenReindexFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionReturns422WithDefaultMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionBatchSizeZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionBatchSizeMinBoundary":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionBatchSizeMaxBoundary":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionBatchSizeAboveMax":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionSuccessIncludesStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionSuccessWithoutStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdateSearchBackendWithActiveKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithInvalidCachedJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithCachedJsonMissingDatabaseKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithMysqlPlatform":0.016,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithMariadbPlatform":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoMysqlVersionQueryFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithPostgresPlatformNoPgvector":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithPostgresPlatformWithPgvector":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoPostgresVersionQueryFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithSqlitePlatform":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithUnknownPlatform":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoStoresInCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchWithCustomWeightsAndFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSemanticSearchWithEmptyStringReturns400":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchWithEmptyStringReturns400":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSemanticSearchResponseIncludesTimestamp":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSemanticSearchResponseIncludesFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSemanticSearchExceptionIncludesTrace":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchExceptionIncludesTrace":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdateSearchBackendHandlesServiceException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoPostgresExtensionQueryFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoWithMariadbPlatformName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testGetDatabaseInfoIncludesLastUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testStatsReturns422StatusOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testIndexReturns500StatusOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdateReturns500StatusOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testLoadReturns500StatusOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testRebaseReturns500StatusOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testReindexSpecificCollectionExceptionMessageInResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSetupHandlerExceptionMessageFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSchemaMappingReturnsResultsOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSchemaMappingReturns422WhenMappingThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testSchemaMappingReturns422WhenObjectMapperFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testDebugTypeFilteringReturnsResults":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testDebugTypeFilteringWithResults":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testDebugTypeFilteringWithMissingType":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testDebugTypeFilteringWhenSetRegisterThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testDebugTypeFilteringWhenSearchThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testDebugTypeFilteringWhenQueryBuilderFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchResponseIncludesTimestamp":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testHybridSearchMergesResultKeysIntoResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\SettingsControllerTest::testUpdateSearchBackendMissingBothKeysReturns400":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchReturns400ForBlankQuery":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchWithFiltersAndProvider":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchWithEmptyResults":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchWithBoundaryLimitOne":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchWithBoundaryLimitHundred":0,"Unit\\Controller\\SolrControllerTest::testSemanticSearchWithNegativeLimit":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForWhitespaceOnlyQuery":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForZeroLimit":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForNegativeLimit":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchWithBoundaryLimitOne":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchWithBoundaryLimitTwoHundred":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForNegativeSolrWeight":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForNegativeVectorWeight":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchReturns400ForVectorWeightTooHigh":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchWithValidWeights":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchWithDefaultWeights":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchWithSolrFilters":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchWithProvider":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchWithEmptyArrayResult":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchSpreadsResultArray":0,"Unit\\Controller\\SolrControllerTest::testHybridSearchWithMissingWeightKeysUsesDefaults":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns400WhenProviderEmpty":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns400WhenOpenaiApiKeyEmpty":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns400WhenFireworksMissingApiKey":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns400WhenFireworksApiKeyEmpty":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingSuccessWithOpenai":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingSuccessWithOpenaiDefaultModel":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingSuccessWithOllama":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingSuccessWithOllamaDefaults":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingSuccessWithFireworks":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingSuccessWithFireworksDefaults":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingWithDefaultTestText":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns500WhenEmbeddingEmpty":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testTestVectorEmbeddingFirstValuesLimitedToFive":0,"Unit\\Controller\\SolrControllerTest::testListCollectionsReturnsEmptyList":0,"Unit\\Controller\\SolrControllerTest::testListConfigSetsReturnsEmptyList":0,"Unit\\Controller\\SolrControllerTest::testListConfigSetsReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testCreateCollectionWithCustomShardConfig":0,"Unit\\Controller\\SolrControllerTest::testCreateConfigSetWithCustomBase":0,"Unit\\Controller\\SolrControllerTest::testCreateConfigSetReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testDeleteConfigSetReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testCopyCollectionReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectReturnsSuccess":0,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectWithProvider":0,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectWithEmptyArrayResult":0,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectReturns500OnObjectNotFound":0,"Unit\\Controller\\SolrControllerTest::testVectorizeObjectReturns500OnVectorizationFailure":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturns400ForLimitTooHigh":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturns400ForNegativeLimit":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsBoundaryLimitOne":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsBoundaryLimitThousand":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturnsEmptyResult":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturnsSuccessWithResults":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsHasMoreWhenFullPage":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsHasMoreFalseWhenPartialPage":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsWithProvider":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsWithNonArrayResult":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsWithOffset":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testBulkVectorizeObjectsWithNullFilters":0,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsReturnsStats":0,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsWithZeroObjects":0,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsWithAllVectorized":0,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsWithMissingObjectVectorsKey":0,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsProgressRounding":0,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsReturns500OnException":0,"Unit\\Controller\\SolrControllerTest::testGetVectorizationStatsReturns500OnMapperException":0,"Unit\\Controller\\SourcesControllerTest::testIndexReturnsSourcesWithEmptyParams":0,"Unit\\Controller\\SourcesControllerTest::testIndexReturnsEmptyResults":0,"Unit\\Controller\\SourcesControllerTest::testIndexWithLimitAndOffset":0,"Unit\\Controller\\SourcesControllerTest::testIndexWithPageConvertsToOffset":0,"Unit\\Controller\\SourcesControllerTest::testIndexWithPageOneConvertsToOffsetZero":0,"Unit\\Controller\\SourcesControllerTest::testIndexPageWithoutLimitDoesNotConvert":0,"Unit\\Controller\\SourcesControllerTest::testIndexFiltersOutSpecialParams":0,"Unit\\Controller\\SourcesControllerTest::testIndexWithOnlyLimitParam":0,"Unit\\Controller\\SourcesControllerTest::testIndexWithOnlyOffsetParam":0,"Unit\\Controller\\SourcesControllerTest::testIndexWithMultipleSources":0,"Unit\\Controller\\SourcesControllerTest::testShowCastsStringIdToInt":0,"Unit\\Controller\\SourcesControllerTest::testShowWithNonNumericIdCastsToZero":0,"Unit\\Controller\\SourcesControllerTest::testCreateRemovesIdIfPresent":0,"Unit\\Controller\\SourcesControllerTest::testCreateDoesNotRemoveIdIfNull":0,"Unit\\Controller\\SourcesControllerTest::testCreateWithEmptyParams":0,"Unit\\Controller\\SourcesControllerTest::testCreateWithMultipleInternalParams":0,"Unit\\Controller\\SourcesControllerTest::testUpdateRemovesInternalParams":0,"Unit\\Controller\\SourcesControllerTest::testUpdateWithEmptyParams":0,"Unit\\Controller\\SourcesControllerTest::testUpdatePassesCorrectId":0,"Unit\\Controller\\SourcesControllerTest::testPatchRemovesImmutableFieldsLikeUpdate":0,"Unit\\Controller\\SourcesControllerTest::testDestroyCallsFindWithCorrectId":0,"Unit\\Controller\\SourcesControllerTest::testDestroyCallsDeleteWithFoundSource":0,"Unit\\Controller\\TasksControllerGapTest::testUpdateWithCalendarIdInRequest":0,"Unit\\Controller\\TasksControllerGapTest::testUpdateFindsCalendarIdFromExistingTasks":0,"Unit\\Controller\\TasksControllerGapTest::testUpdateTaskNotFound":0,"Unit\\Controller\\TasksControllerGapTest::testUpdateObjectNotFound":0,"Unit\\Controller\\TasksControllerGapTest::testUpdateDoesNotExistException":0,"Unit\\Controller\\TasksControllerGapTest::testUpdateGenericException":0,"Unit\\Controller\\TasksControllerGapTest::testDestroyTaskNotFoundInList":0,"Unit\\Controller\\TasksControllerGapTest::testDestroySuccess":0,"Unit\\Controller\\TasksControllerGapTest::testDestroyObjectNotFound":0,"Unit\\Controller\\TasksControllerGapTest::testDestroyDoesNotExistException":0,"Unit\\Controller\\TasksControllerGapTest::testDestroyGenericException":0,"Unit\\Controller\\TasksControllerGapTest::testCreateEmptySummary":0,"Unit\\Controller\\TasksControllerGapTest::testCreateWithNullObjectName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testMeNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testMeException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testUpdateMeNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testUpdateMeException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testUpdateMeStripsInternalAndImmutableParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testConvertToBytesUnlimited":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testConvertToBytesGigabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testConvertToBytesMegabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testConvertToBytesKilobytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerDeepTest::testLogout":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testUpdateMeException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginRateLimitedWithDelay":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginRecordsFailedAttemptOnInvalidCredentials":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginRecordsFailedAttemptForDisabledAccount":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testLoginSuccessCallsRecordSuccessfulLogin":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testIndexWithLimitAndOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testIndexWithLimitAndPage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testIndexWithLimitOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testIndexException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testShowException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testCreateNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testCreateWithConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testCreateWithConfigurationDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testCreateException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testCreateWithEmptyName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testUpdateNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testUpdateMissingName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testUpdateMissingQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testUpdateWithConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testUpdateException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testUpdateWithEmptyName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchWithConfiguration":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchWithDirectQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchWithIsPublicAndIsDefaultOverrides":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchWithFavoredBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testPatchNoFieldsUpdatesWithExistingValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testDestroyNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\ViewsControllerTest::testDestroyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexWithLimitAndOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexWithPageBasedPagination":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexWithStringExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexWithArrayExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexWithFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexWithPageWithoutLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testIndexReturnsEmptyResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateMissingName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateMissingBothNameAndUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateRemovesOrganisationParam":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateWithEmptyNameString":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateWithEmptyUrlString":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateWithMultipleUnderscoreParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testCreateWithIdNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testUpdateRemovesIdFromData":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testDestroyCallsDeleteOnMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testTestWebhookDeliveryFailedWithLogDetails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testTestWebhookDeliveryFailedWithNoLogs":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testTestWebhookDeliveryFailedWithLogNoErrorMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testTestWebhookDeliveryFailedWithLogErrorMessageButNoStatusCode":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testTestWebhookGuzzleException":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testEventsContainsExpectedStructure":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testEventsContainsAllCategories":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testLogsWithCustomLimitAndOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testLogStatsWithPendingRetries":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithWebhookIdFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithSuccessTrueFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithSuccessFalseFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithSuccess1Filter":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithSuccess0Filter":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithWebhookIdAndSuccessFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithInvalidSuccessFilterIsIgnored":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithCustomLimitAndOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithWebhookIdEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithWebhookIdZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testAllLogsWithSuccessEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryReturns400WhenLogIsSuccessful":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetrySuccessWithRequestBody":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetrySuccessWithPayloadArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryReturns400WhenNoPayloadAvailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryFailedDeliveryWithLogDetails":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryFailedDeliveryWithNoLogDetails":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryFailedDeliveryWithLogNoErrorMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryReturns500OnGenericException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryWithRequestBodyContainingInvalidJson":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryUsesDataKeyFromPayload":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryWithPayloadWithoutDataKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\WebhooksControllerTest::testRetryFailedWithLogErrorMessageAndStatusCode":0,"Unit\\Listener\\ObjectChangeListenerGapTest::testUnknownExtractionModeFallsBackToBackground":0,"Unit\\Listener\\ObjectChangeListenerGapTest::testImmediateExtractionFailureLogs":0,"Unit\\Listener\\ObjectChangeListenerGapTest::testBackgroundJobQueueFailureLogs":0,"Unit\\Listener\\ObjectChangeListenerGapTest::testDefaultExtractionModeIsBackground":0,"Unit\\Listener\\ObjectChangeListenerGapTest::testObjectUpdatedEventWithImmediateMode":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectCreatingEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectUpdatingEventDispatchesWebhookWithOldObject":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectUpdatingEventDispatchesWebhookWithNullOldObject":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectDeletingEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testObjectRevertedEventIncludesRevertPoint":0,"Unit\\Listener\\WebhookEventListenerTest::testRegisterUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testRegisterDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testSchemaCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testSchemaUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testApplicationCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testApplicationUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testApplicationDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testAgentCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testAgentUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testAgentDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testSourceCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testSourceUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testSourceDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testConfigurationCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testConfigurationUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testConfigurationDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testViewCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testViewUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testViewDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testConversationCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testConversationUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testConversationDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testOrganisationCreatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testOrganisationUpdatedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testOrganisationDeletedEventDispatchesWebhook":0,"Unit\\Listener\\WebhookEventListenerTest::testSuccessfulEventLogsDebug":0,"Unit\\Listener\\WebhookEventListenerTest::testUnknownEventWarningContainsEventClass":0,"OCA\\OpenRegister\\Tests\\Unit\\Notification\\NotifierTest::testGetId":0,"OCA\\OpenRegister\\Tests\\Unit\\Notification\\NotifierTest::testGetName":0,"OCA\\OpenRegister\\Tests\\Unit\\Notification\\NotifierTest::testPrepareThrowsForWrongApp":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Notification\\NotifierTest::testPrepareThrowsForUnknownSubject":0,"OCA\\OpenRegister\\Tests\\Unit\\Notification\\NotifierTest::testPrepareConfigurationUpdateAvailable":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Notification\\NotifierTest::testPrepareConfigurationUpdateWithoutConfigId":0,"OCA\\OpenRegister\\Tests\\Unit\\Notification\\NotifierTest::testPrepareConfigurationUpdateWithDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testGetId":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testGetName":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testGetOrder":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testGetSupportedFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testGetAlternateIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testGetCustomFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchWithNoResults":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchWithTermFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchWithRegisterAndSchemaFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchWithDateFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchWithUntilFilterOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchWithResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchWithObjectEntityResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchResultWithNameFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchResultWithUuidFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchResultWithDescription":0,"OCA\\OpenRegister\\Tests\\Unit\\Search\\ObjectsProviderTest::testSearchResultWithSummary":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationCachingTest::testCacheReconstructionWithDateTimeObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationCachingTest::testActiveOrgClearedWhenUserNoLongerMember":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActiveOrganisationCachingTest::testActiveOrgClearedWhenOrgDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testFetchOAuthTokensMissingGrantType":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testFetchOAuthTokensMissingTokenUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testFetchOAuthTokensUnsupportedGrantType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testCreateClientCredentialConfigMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testCreateClientCredentialConfigBody":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testCreateClientCredentialConfigBasicAuth":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testCreatePasswordConfigMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testCreatePasswordConfigBody":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testCreatePasswordConfigBasicAuth":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testFetchJWTTokenMissingParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testGetHSJWK":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testGetJWKUnsupportedAlgorithm":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\AuthenticationServiceDeepTest::testFetchJWTTokenWithHS256":0.031,"Unit\\Service\\AuthenticationServiceGapTest::testFetchJWTTokenMissingParams":0,"Unit\\Service\\AuthenticationServiceGapTest::testFetchOAuthTokensMissingGrantType":0,"Unit\\Service\\AuthenticationServiceGapTest::testFetchOAuthTokensMissingTokenUrl":0,"Unit\\Service\\AuthenticationServiceGapTest::testFetchOAuthTokensUnsupportedGrantType":0,"Unit\\Service\\AuthenticationServiceGapTest::testFetchJWTTokenWithHS256":0.001,"Unit\\Service\\AuthenticationServiceGapTest::testFetchJWTTokenWithHS384":0,"Unit\\Service\\AuthenticationServiceGapTest::testFetchJWTTokenWithHS512":0,"Unit\\Service\\AuthenticationServiceGapTest::testFetchJWTTokenWithX5tHeader":0,"Unit\\Service\\AuthenticationServiceGapTest::testFetchJWTTokenUnsupportedAlgorithm":0,"Unit\\Service\\AuthenticationServiceGapTest::testFetchJWTTokenWithTwigPayload":0.006,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadThrowsWhenMissingIatWithDetails":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadExpiredDetailsContainTimestamps":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadWithFutureIat":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadWithExactlyNowIat":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadWithZeroIat":0,"Unit\\Service\\AuthorizationServiceTest::testValidatePayloadWithExtraClaimsDoesNotThrow":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeBasicWithUsersAndGroupsParams":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeBasicWithPasswordContainingColon":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeBasicEmptyDetailsOnFailure":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeOAuthThrowsWithBasicPrefix":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeOAuthThrowsWithEmptyString":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeOAuthInvalidMethodDetailsContainReason":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeOAuthNotAuthorizedDetailsContainReason":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeOAuthSucceedsWithBearerNoSpace":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeApiKeyWithMultipleKeys":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeApiKeyWithEmptyKeysMap":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeApiKeyEmptyDetailsOnInvalidKey":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeApiKeyEmptyDetailsOnNullUser":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtEmptyTokenDetails":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenNoIssuer":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenEmptyIssuer":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtNoIssuerDetailsContainReason":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenInvalidAlgorithmHeader":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtInvalidAlgorithmDetailsContainReason":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenIssuerNotFound":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtSucceedsWithHs256":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtSucceedsWithHs384":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtSucceedsWithHs512":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenSignatureInvalid":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtSignatureInvalidDetailsContainReason":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenTokenExpired":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenTokenMissingIat":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtSucceedsWithDefaultExpiry":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtThrowsWhenUnsupportedAlgorithm":0,"Unit\\Service\\AuthorizationServiceTest::testAuthorizeJwtWithMultipleConsumersReturnsFirst":0,"Unit\\Service\\AuthorizationServiceTest::testFindIssuerReturnsConsumer":0,"Unit\\Service\\AuthorizationServiceTest::testFindIssuerThrowsWhenNotFound":0,"Unit\\Service\\AuthorizationServiceTest::testFindIssuerDetailsContainIss":0,"Unit\\Service\\AuthorizationServiceTest::testFindIssuerReturnsFirstConsumerWhenMultiple":0,"Unit\\Service\\AuthorizationServiceTest::testGetJwkHmacHs256ReturnsJwkSet":0,"Unit\\Service\\AuthorizationServiceTest::testGetJwkHmacHs384ReturnsJwkSet":0,"Unit\\Service\\AuthorizationServiceTest::testGetJwkHmacHs512ReturnsJwkSet":0,"Unit\\Service\\AuthorizationServiceTest::testGetJwkRsaReturnsJwkSet":0.013,"Unit\\Service\\AuthorizationServiceTest::testGetJwkPsReturnsJwkSet":0.015,"Unit\\Service\\AuthorizationServiceTest::testGetJwkThrowsForUnsupportedAlgorithm":0.001,"Unit\\Service\\AuthorizationServiceTest::testGetJwkUnsupportedAlgorithmDetailsContainAlgorithm":0,"Unit\\Service\\AuthorizationServiceTest::testCheckHeadersWithValidHs256Token":0.003,"Unit\\Service\\AuthorizationServiceTest::testCheckHeadersWithValidHs384Token":0,"Unit\\Service\\AuthorizationServiceTest::testCheckHeadersWithValidHs512Token":0,"Unit\\Service\\AuthorizationServiceTest::testCorsAfterControllerAllowsCredentialsFalse":0,"Unit\\Service\\AuthorizationServiceTest::testCorsAfterControllerHandlesMultipleHeaders":0,"Unit\\Service\\AuthorizationServiceTest::testCorsAfterControllerCredentialsCaseInsensitive":0,"Unit\\Service\\AuthorizationServiceTest::testCorsAfterControllerWithEmptyHeaders":0,"Unit\\Service\\AuthorizationServiceTest::testCorsAfterControllerWithNullServerKey":0,"Unit\\Service\\AuthorizationServiceTest::testAllAlgorithmsAreCovered":0,"Unit\\Service\\ConditionMatcherGapTest::testOrganisationVariableResolvesViaService":0,"Unit\\Service\\ConditionMatcherGapTest::testActiveOrganisationVariableAlias":0,"Unit\\Service\\ConditionMatcherGapTest::testOrganisationVariableReturnsNullWhenNoActiveOrg":0,"Unit\\Service\\ConditionMatcherGapTest::testOrganisationVariableExceptionReturnsNull":0,"Unit\\Service\\ConditionMatcherGapTest::testFilterOrganisationMatchForCreateNonStringValue":0,"Unit\\Service\\ConditionMatcherGapTest::testNonUnderscorePropertyMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeCreateWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeTitleFallbackToSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeProposedDataPreserved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeFindCalledWithLowercasedSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeUpdateWithNewerVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeSkipSameVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeSkipOlderVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeNullVersionDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeNullCurrentVersionNewerProposed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewRegisterChangeCurrentDataFromExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesReturnsJsonResponseOnFetchError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesEmptyComponents":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesNoComponentsKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesMetadataInfoVersionFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesMetadataNoVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesPreviewedAtFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesVersionPrecedence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesWithRegisters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesWithSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesSchemaUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesSchemaSkip":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesSchemaNoVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesSchemaTitleFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesWithObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesObjectsNullSlugRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesMultipleObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesNullSections":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesNonArraySections":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testPreviewConfigurationChangesTotalChanges":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testCompareArraysWithPrefixReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testCompareArraysEmptyInputs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\PreviewHandlerTest::testImportConfigurationWithSelectionNonEmptySelection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testEnsureDefaultOrganisationFetchesFromDb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testEnsureDefaultOrganisationCreatesWhenUuidNotFound":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testEnsureDefaultOrganisationCreatesWhenNoUuidConfigured":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DefaultOrganisationManagementTest::testGetUserOrganisationsAutoAssignsToDefault":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointNoUserPublicEndpoint":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointNoUserGroupsRequired":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointAdminAlwaysAllowed":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointNoGroupsAllowsAuthenticated":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointUserInAllowedGroup":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointUserInSecondAllowedGroup":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointUserNotInAnyAllowedGroup":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointUserInMultipleGroupsOneMatches":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointViewType":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointWebhookType":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointRegisterType":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointSchemaType":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointUnknownType":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointAgentTypeFailsGracefullyWhenAgentNotFound":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointAgentNotFound":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointEmptyMessage":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointEmptyMessageString":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointMessageInTopLevelRequest":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointUnsupportedProvider":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointNoToolsConfigured":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointNullToolsConfigured":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointWithToolsLoaded":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointToolReturnsNull":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointToolThrowsException":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointOllamaProviderThrowsError":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointOllamaWithPromptThrowsError":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointOllamaWithoutPromptThrowsError":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointOllamaEmptyPromptThrowsError":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointOllamaDefaultUrlThrowsError":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointOllamaNoLlmConfigThrowsError":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointOllamaWithToolsAndPromptThrowsError":0,"Unit\\Service\\EndpointServiceTest::testExecuteAgentEndpointMixedToolsSuccessAndFailure":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAgentTargetTypeReturnsErrorWhenAgentNotFound":0.001,"Unit\\Service\\EndpointServiceTest::testTestEndpointAgentNotFoundViaTestEndpoint":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAgentEmptyMessageViaTestEndpoint":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAgentWithMessageInTestData":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointPassesTestDataThrough":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointUsesMethodFromEndpoint":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointNullMethodDefaultsToGet":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointCatchesExceptionAndReturnsErrorDetails":0,"Unit\\Service\\EndpointServiceTest::testLogEndpointCallWithAuthenticatedUser":0,"Unit\\Service\\EndpointServiceTest::testLogEndpointCallWithoutUser":0,"Unit\\Service\\EndpointServiceTest::testLogEndpointCallWithErrorResult":0,"Unit\\Service\\EndpointServiceTest::testLogEndpointCallWithSuccessNoErrorKey":0,"Unit\\Service\\EndpointServiceTest::testLogEndpointCallInsertFailureDoesNotThrow":0,"Unit\\Service\\EndpointServiceTest::testLogEndpointCallVerifiesLogProperties":0,"Unit\\Service\\EndpointServiceTest::testLogEndpointCallSuccessMessageDefault":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointWithEmptyTestData":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointWithDifferentEndpointIds":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointResponseStructureForSuccess":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointResponseStructureForDenied":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointResponseStructureForUnknownType":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointWithEmptyTargetType":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointWithEmptyGroupsArrayAndNoUser":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointAdminBypassesGroupRestriction":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointUserWithNoGroupsAndEndpointHasGroups":0,"Unit\\Service\\EndpointServiceTest::testLogEndpointCallWithLargeRequestData":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointDifferentEndpointPaths":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAllPlaceholderTargetTypesReturnCorrectMessage":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAgentUnsupportedProviderLogsResult":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAgentNotFoundLogsError":0,"Unit\\Service\\EndpointServiceTest::testTestEndpointAgentWithToolsAndMessage":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointCaseInsensitiveTargetType":0,"Unit\\Service\\EndpointServiceTest::testExecuteEndpointWithSpecialCharTargetType":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointUserInAllGroupsNotJustOne":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointNonAdminUserInAllAllowedGroups":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointSingleGroup":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointManyGroupsNoneMatch":0,"Unit\\Service\\EndpointServiceTest::testCanExecuteEndpointLastGroupMatches":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksModifiedResultOnUpdatingEvent":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksModifiedResultOnDeletingEvent":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksModifiedResultWithNullData":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksRejectedOnUpdatingEvent":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksRejectedOnDeletingEvent":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksUnknownFailureMode":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksGeneralExceptionUsesOnFailure":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksFilterConditionNonArray":0,"Unit\\Service\\HookExecutorTest::testExecuteHooksFlagModeWithEmptyErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrDateNonParseable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrDateNonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrArrayWrapsScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrIntegerNonNumericReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testConvertValueForSolrFloatNonNumericReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testIsValueCompatibleWithSolrTypePdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testIsValueCompatibleWithSolrTypeUnknownAllowsAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveRegisterToIdNonResolvableReturnsZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveSchemaToIdNonResolvableReturnsZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveRegisterToIdNoMapperReturnsZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testResolveSchemaToIdNoMapperReturnsZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenFilesForSolrSkipsArraysWithoutIdOrUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testFlattenFilesForSolrNonStringNonArrayReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testExtractArraysFromRelationsReindexesSequentially":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testShouldTruncateFieldImageAndDocumentFormats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\DocumentBuilderTest::testCreateDocumentIncludesCreatedAndUpdatedDates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testEnsureVectorFieldTypeAlreadyExists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testEnsureVectorFieldTypeCreatesNew":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testEnsureVectorFieldTypeException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testGetCollectionFieldStatusSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testGetCollectionFieldStatusException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testCreateMissingFieldsDryRun":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testCreateMissingFieldsActual":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testFixMismatchedFieldsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testDetermineSolrFieldType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testIsMultiValued":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testGetMostPermissiveType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerDeepTest::testGenerateSolrFieldName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeAlreadyExistsDoesNotCallAddFieldType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeCreatesNewWithDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeCreatesNewWithCustomDimensionsAndSimilarity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeReturnsFalseWhenAddFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeHandlesGetFieldTypesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testEnsureVectorFieldTypeHandlesAddFieldTypeException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasWithNoSchemasSucceeds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasProcessesSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasWithForceFlag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasCountsCreatedAndUpdatedFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasCountsUpdatedSchemaFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasReturnsResolvedConflicts":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasErrorInOneSchemaDoesNotAbort":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasPropagatesTopLevelException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testMirrorSchemasIncludesExecutionTime":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testGetCollectionFieldStatusEmptyCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testGetCollectionFieldStatusWithExtraFieldsInBackend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testGetCollectionFieldStatusPassesCollectionName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testCreateMissingFieldsDryRunReturnsFieldsToAdd":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testCreateMissingFieldsDryRunDoesNotCallBackend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testCreateMissingFieldsActuallyCreatesFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testCreateMissingFieldsCountsFailuresWhenBackendSkips":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testCreateMissingFieldsEmptyArraySucceeds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testFixMismatchedFieldsDelegatesToBackend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testFixMismatchedFieldsPassesDryRunFlag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testFixMismatchedFieldsEmptyArraySucceeds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testConflictResolutionPrefersStringOverInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testConflictResolutionPrefersStringOverBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testConflictResolutionPrefersTextOverFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testConflictResolutionPrefersFloatOverInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testNoConflictWhenFieldUsedSameTypeAcrossSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testIntegerTypeMappedToSolrInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testNumberTypeMappedToSolrFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testBooleanTypeMappedToSolrBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testDateTypeMappedToSolrDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testUnknownTypeMapsToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testFieldNameSanitizationReplacesSpecialChars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testArrayTypeFieldIsMultiValued":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testFieldWithMaxItemsGreaterThanOneIsMultiValued":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testScalarFieldIsNotMultiValued":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testAllFieldsAreBothIndexedAndStored":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Index\\SchemaHandlerTest::testSchemaWithNoPropertiesProcessedWithoutError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testEncodeArrayKeysFlat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testEncodeArrayKeysNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testEncodeArrayKeysEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCoordinateStringToArraySinglePoint":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCoordinateStringToArrayMultiplePoints":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testExecuteMappingSimpleDotNotation":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testExecuteMappingWithPassThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testExecuteMappingArrayValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testExecuteMappingWithUnset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToInt":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToBoolTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToBoolFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToNullableBoolNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToNullableBoolTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastToJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastJsonToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastJsonToArrayAlreadyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastNullStringToNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastNullStringToNullNotNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastMoneyStringToInt":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastIntToMoneyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastBase64":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastBase64Decode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastUrlDecode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastHtml":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastHtmlDecode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastUnsetIfValueMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastUnsetIfValueEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastSetNullIfValueMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastSetNullIfValueEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastDefaultUnknownType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testCastMultipleCastsAsCsv":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testExecuteMappingListMode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testExecuteMappingListModeWithListInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testExecuteMappingRootHashArrayValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testExecuteMappingRootHashNullValue":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testAreAllArrayKeysNullEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testAreAllArrayKeysNullAllNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testAreAllArrayKeysNullNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testAreAllArrayKeysNullHasValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testInvalidateMappingCacheCallsRemove":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testInvalidateMappingCacheNoCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testGetMappingCacheHit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testGetMappingCacheMiss":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\MappingServiceCoverageTest::testGetMappings":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithRawUrlEncodeCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithRawUrlDecodeCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithKeyCantBeValueCastWhenMatch":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithKeyCantBeValueCastWhenNoMatch":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithUnsetIfValueMatchingString":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithUnsetIfValueNotMatching":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithUnsetIfValueEmptyString":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithSetNullIfValueMatchingString":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithSetNullIfValueEmptyValue":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithSetNullIfValueNotMatching":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithCountValueCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithCountValueCastKeyNotFound":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithBoolCastFromYes":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithBoolCastFromOne":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithBoolCastFalse":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithBooleanAliasCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithIntegerAliasCast":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithNullStringToNullCastNonNull":0,"Unit\\Service\\MappingServiceTest::testExecuteMappingWithTwigTemplate":0.001,"Unit\\Service\\MappingServiceTest::testExecuteMappingTwigTemplateThrowsOnError":0.002,"Unit\\Service\\MappingServiceTest::testCoordinateStringToArrayThreePoints":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListToolsHavePropertiesAndRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testRegistersToolHasCorrectProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testSchemasToolHasCorrectProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testObjectsToolHasCorrectProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCallToolErrorContentContainsMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCallToolLogsErrorOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCallToolSuccessStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCallToolLogsDebugOnEveryCall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListRegisters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListRegistersWithLimitAndOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListRegistersDefaultsToNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testGetRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testGetRegisterMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCreateRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCreateRegisterMissingDataReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateRegisterMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateRegisterMissingDataReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testDeleteRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testDeleteRegisterMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testRegistersUnknownActionReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListSchemasWithLimitAndOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListSchemasDefaultsToNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testGetSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testGetSchemaMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCreateSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCreateSchemaMissingDataReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateSchemaMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateSchemaMissingDataReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testDeleteSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testDeleteSchemaMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testSchemasUnknownActionReturnsError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListObjectsWithLimitAndOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListObjectsWithOnlyLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListObjectsWithOnlyOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListObjectsSetsRegisterAndSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testObjectsMissingRegisterReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testObjectsMissingSchemaReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testObjectsMissingBothRegisterAndSchemaReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testGetObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testGetObjectMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCreateObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testCreateObjectMissingDataReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateObjectMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testUpdateObjectMissingDataReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testDeleteObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testDeleteObjectMissingIdReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testObjectsUnknownActionReturnsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testServiceExceptionReturnedAsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testSchemaMapperExceptionReturnedAsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testObjectServiceExceptionReturnedAsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListRegistersReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListSchemasReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Mcp\\McpToolsServiceTest::testListObjectsReturnsEmptyArray":0,"Unit\\Service\\MetricsServiceTest::testRecordMetricWithErrorMessage":0,"Unit\\Service\\MetricsServiceTest::testGetEmbeddingStatsWithNullRow":0,"Unit\\Service\\MetricsServiceTest::testGetSearchLatencyStatsWithStringAvg":0.001,"Unit\\Service\\MetricsServiceTest::testGetStorageGrowthNullBytes":0.001,"Unit\\Service\\MetricsServiceTest::testCleanOldMetricsCustomRetention":0,"Unit\\Service\\MetricsServiceTest::testRecordMetricWithInvalidUtf8Metadata":0,"Unit\\Service\\MetricsServiceTest::testGetSearchLatencyStatsAllTypesHaveData":0.001,"Unit\\Service\\MetricsServiceTest::testGetEmbeddingStatsEstimatedCost":0,"Unit\\Service\\MetricsServiceTest::testGetStorageGrowthMultipleDays":0.001,"Unit\\Service\\MetricsServiceTest::testRecordMetricMinimalParams":0,"Unit\\Service\\OasServiceTest::testCreateOasGeneratesPathsForSchema":0.003,"Unit\\Service\\OasServiceTest::testCreateOasGeneratesTagsForSchemas":0.001,"Unit\\Service\\OasServiceTest::testCreateOasMultipleRegistersWithPrefixes":0.005,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithArrayProperties":0.002,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithObjectProperty":0.002,"Unit\\Service\\OasServiceTest::testCreateOasSecuritySchemes":0.001,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithDescription":0,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithRequiredProperties":0.001,"Unit\\Service\\OasServiceTest::testCreateOasWithRbacProperties":0.001,"Unit\\Service\\OasServiceTest::testSlugifySimpleString":0,"Unit\\Service\\OasServiceTest::testSlugifySpecialCharacters":0,"Unit\\Service\\OasServiceTest::testSlugifyAlreadySlugified":0,"Unit\\Service\\OasServiceTest::testSlugifyUpperCase":0,"Unit\\Service\\OasServiceTest::testSlugifyTrimsHyphens":0,"Unit\\Service\\OasServiceTest::testPascalCaseSimple":0,"Unit\\Service\\OasServiceTest::testPascalCaseSingleWord":0,"Unit\\Service\\OasServiceTest::testPascalCaseWithHyphens":0,"Unit\\Service\\OasServiceTest::testPascalCaseWithSpecialChars":0,"Unit\\Service\\OasServiceTest::testSanitizeSchemaNameSimple":0,"Unit\\Service\\OasServiceTest::testSanitizeSchemaNameWithSpaces":0,"Unit\\Service\\OasServiceTest::testSanitizeSchemaNameNull":0,"Unit\\Service\\OasServiceTest::testSanitizeSchemaNameEmpty":0,"Unit\\Service\\OasServiceTest::testSanitizeSchemaNameWithSpecialChars":0,"Unit\\Service\\OasServiceTest::testSanitizeSchemaNameStartingWithNumber":0,"Unit\\Service\\OasServiceTest::testSanitizeSchemaNameWithDotsAndDashes":0,"Unit\\Service\\OasServiceTest::testSanitizeSchemaNameAllSpecialChars":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionNonArray":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionInteger":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionBoolean":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionStripsInternalFields":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionKeepsAllowedKeywords":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionInvalidType":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionArrayTypeGetsItemsDefault":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionNestedItems":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionItemsAsList":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionEmptyItemsList":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionBooleanRequired":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionArrayRequired":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionEmptyOneOf":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionValidOneOf":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionEmptyAnyOf":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionEmptyAllOf":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionAllOfNonArray":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionAllOfWithEmptyItems":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionAllOfWithValidItems":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionEmptyRef":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionRefNotString":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionBareRefNormalized":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionFullRefKept":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionEmptyEnum":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionEnumNotArray":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionValidEnum":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionNoTypeOrRef":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionNoDescription":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionRefNoDefaultDescription":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionNestedProperties":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionCompositionRecursive":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionNullableProperty":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionWriteOnlyProperty":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionMinMaxValidation":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionConstKeyword":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionExamplesKeyword":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionUniqueItems":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionMaxMinProperties":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionAdditionalProperties":0,"Unit\\Service\\OasServiceTest::testSanitizePropertyDefinitionNotKeyword":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromArray":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromArrayInvalid":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromArrayNoType":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromStringInt":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromStringFloat":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromStringBool":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromStringString":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromStringArray":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromStringObject":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromStringUnknown":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromNull":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeFromInteger":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeBoolean":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeNumber":0,"Unit\\Service\\OasServiceTest::testGetPropertyTypeNull":0,"Unit\\Service\\OasServiceTest::testExtractGroupFromRuleString":0,"Unit\\Service\\OasServiceTest::testExtractGroupFromRuleArrayWithGroup":0,"Unit\\Service\\OasServiceTest::testExtractGroupFromRuleArrayWithoutGroup":0,"Unit\\Service\\OasServiceTest::testExtractGroupFromRuleNull":0,"Unit\\Service\\OasServiceTest::testExtractGroupFromRuleInteger":0,"Unit\\Service\\OasServiceTest::testGetScopeDescriptionAdmin":0,"Unit\\Service\\OasServiceTest::testGetScopeDescriptionPublic":0,"Unit\\Service\\OasServiceTest::testGetScopeDescriptionCustomGroup":0,"Unit\\Service\\OasServiceTest::testEnrichSchemaBasic":0,"Unit\\Service\\OasServiceTest::testEnrichSchemaEmptyProperties":0,"Unit\\Service\\OasServiceTest::testEnrichSchemaHasSelfRef":0,"Unit\\Service\\OasServiceTest::testEnrichSchemaHasIdUuidFormat":0,"Unit\\Service\\OasServiceTest::testExtractSchemaGroupsNoAuth":0,"Unit\\Service\\OasServiceTest::testExtractSchemaGroupsWithSchemaLevelAuth":0,"Unit\\Service\\OasServiceTest::testExtractSchemaGroupsWithPropertyLevelAuth":0,"Unit\\Service\\OasServiceTest::testExtractSchemaGroupsWithArrayRules":0,"Unit\\Service\\OasServiceTest::testExtractSchemaGroupsDeduplicates":0,"Unit\\Service\\OasServiceTest::testExtractSchemaGroupsNonArrayProperty":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationAddsGroups":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationAlwaysIncludesAdmin":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationAdminAlreadyInGroups":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationAdds403Response":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationEmptyGroups":0,"Unit\\Service\\OasServiceTest::testCreateCommonQueryParametersNonCollection":0,"Unit\\Service\\OasServiceTest::testCreateCommonQueryParametersCollection":0,"Unit\\Service\\OasServiceTest::testCreateCommonQueryParametersCollectionSkipsMetadataProps":0,"Unit\\Service\\OasServiceTest::testCreateCommonQueryParametersCollectionSkipsIdProp":0,"Unit\\Service\\OasServiceTest::testCreateCommonQueryParametersCollectionArrayType":0,"Unit\\Service\\OasServiceTest::testCreateCommonQueryParametersCollectionNoSchema":0,"Unit\\Service\\OasServiceTest::testCreateOasPathsWithSchemaSlug":0.002,"Unit\\Service\\OasServiceTest::testCreateOasPathsWithoutSlug":0.001,"Unit\\Service\\OasServiceTest::testCreateOasCollectionPathHasGetAndPost":0.001,"Unit\\Service\\OasServiceTest::testCreateOasItemPathHasGetPutDelete":0.001,"Unit\\Service\\OasServiceTest::testCreateOasOperationIdNoPrefixForSingleRegister":0,"Unit\\Service\\OasServiceTest::testCreateOasOperationIdWithPrefixForMultipleRegisters":0,"Unit\\Service\\OasServiceTest::testCreateOasOperationTags":0,"Unit\\Service\\OasServiceTest::testCreateOasGetCollectionHasPaginatedResponse":0,"Unit\\Service\\OasServiceTest::testCreateOasGetSingleHas404Response":0,"Unit\\Service\\OasServiceTest::testCreateOasPostHas201Response":0,"Unit\\Service\\OasServiceTest::testCreateOasPutHasRequestBody":0,"Unit\\Service\\OasServiceTest::testCreateOasDeleteHas204And404Response":0,"Unit\\Service\\OasServiceTest::testCreateOasGetSingleHasIdPathParam":0,"Unit\\Service\\OasServiceTest::testCreateOasPathsWithSchemaLevelRbac":0,"Unit\\Service\\OasServiceTest::testCreateOasScopeDescriptionsCorrect":0,"Unit\\Service\\OasServiceTest::testCreateOasWithBrokenRefGetsCleaned":0.001,"Unit\\Service\\OasServiceTest::testCreateOasWithCaseInsensitiveRefMatch":0.002,"Unit\\Service\\OasServiceTest::testCreateOasWithBareRefGetsNormalized":0,"Unit\\Service\\OasServiceTest::testCreateOasSchemaComponentHasCorrectStructure":0.001,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithFormatProperty":0,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithSpecialCharactersInTitle":0,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithNullDescription":0,"Unit\\Service\\OasServiceTest::testCreateOasHasBaseComponentSchemas":0.001,"Unit\\Service\\OasServiceTest::testCreateOasHasBasicAuth":0,"Unit\\Service\\OasServiceTest::testCreateOasHasOAuth2Config":0,"Unit\\Service\\OasServiceTest::testCreateOasOpenApiVersion":0,"Unit\\Service\\OasServiceTest::testCreateOasInfoHasContactAndLicense":0,"Unit\\Service\\OasServiceTest::testCreateOasWithManySchemas":0.006,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithAllPropertyTypes":0.001,"Unit\\Service\\OasServiceTest::testCreateOasSchemaWithInternalFieldsStripped":0,"Unit\\Service\\OasServiceTest::testCreateOasGetCollectionParametersIncludeSchemaFilters":0,"Unit\\Service\\OasServiceTest::testCreateOasSpecificRegisterInfoVersion":0,"Unit\\Service\\OasServiceTest::testCreateOasDefaultInfoPreservedForAllRegisters":0,"Unit\\Service\\OasServiceTest::testCreateOasSchemaReferenceInResponseBody":0,"Unit\\Service\\OasServiceTest::testCreateOasDeleteHasNoRequestBody":0,"Unit\\Service\\OasServiceTest::testCreateOasSchemaNotAddedToPathsIfNotInRegister":0.001,"Unit\\Service\\OasServiceTest::testCreateOasGetCollectionHas400Response":0,"Unit\\Service\\OasServiceTest::testCreateOasPostHas400Response":0,"Unit\\Service\\OasServiceTest::testCreateLogsOperation":0,"Unit\\Service\\OasServiceTest::testCreateGetFilesOperation":0,"Unit\\Service\\OasServiceTest::testCreatePostFileOperation":0,"Unit\\Service\\OasServiceTest::testCreateLockOperation":0,"Unit\\Service\\OasServiceTest::testCreateUnlockOperation":0,"Unit\\Service\\OasServiceTest::testCreateGetCollectionOperationStructure":0,"Unit\\Service\\OasServiceTest::testCreateGetCollectionOperationEmptyTitleFallback":0,"Unit\\Service\\OasServiceTest::testCreateGetOperationStructure":0,"Unit\\Service\\OasServiceTest::testCreatePutOperationStructure":0,"Unit\\Service\\OasServiceTest::testCreatePostOperationStructure":0,"Unit\\Service\\OasServiceTest::testCreateDeleteOperationStructure":0,"Unit\\Service\\OasServiceTest::testAddExtendedPathsEmptyWhitelist":0,"Unit\\Service\\OasServiceTest::testAddCrudPathsCreatesCollectionAndItemPaths":0.002,"Unit\\Service\\OasServiceTest::testAddCrudPathsWithOperationIdPrefix":0,"Unit\\Service\\OasServiceTest::testAddCrudPathsFallsBackToSlugifiedTitle":0.001,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesRemovesEmptyAllOf":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesRemovesEmptyRef":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesRemovesNonStringRef":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesCaseInsensitiveMatch":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesBrokenRefFallsBack":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesValidRefKept":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesRecursiveProperties":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesRecursiveItems":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesAllOfValidation":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesAllOfAllInvalid":0,"Unit\\Service\\OasServiceTest::testValidateSchemaReferencesCompositionKeywords":0,"Unit\\Service\\OasServiceTest::testValidateOasIntegrityFixesPathSchemaRefs":0.003,"Unit\\Service\\OasServiceTest::testGetBaseOasReturnsValidOas":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessUploadedFilesWithEmptyBracketArrayField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessUploadedFilesAppendsToExistingArrayField":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessUploadedFilesWithMissingType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessUploadedFilesOverridesExistingDataField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithFileObjectValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithArrayOfFileObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithArrayOfUrls":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithArrayOfBase64":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithSchemaArrayNonFileItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithIntegerValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithUrlNoPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithUrlPathNoExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithArrayOfNonFileUrls":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithShortBase64String":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFilePropertyWithSchemaNoType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFileObjectWithIdAndPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFileObjectWithAccessUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFileObjectWithDownloadUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testIsFileObjectEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataWithApplicationPdfDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataWithInvalidBase64InDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataPlainBase64ReturnsExtension":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigCallsBlockExecutableFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigAllowExecutablesFlag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigWithEmptyAllowedTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testValidateFileAgainstConfigMaxSizeZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksPhpExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksShExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksPyExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksMsiExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksDllExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksWindowsExeMagicBytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksElfMagicBytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksShellScript":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksBashScript":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksEnvShebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksPhpTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksPhpShortTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksJavaClassMagicBytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksShellscriptMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksDosexecMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksPhpMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksPythonMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksJarMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksShebangInContent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesBlocksPhpScriptTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesNoFilenameNoContent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testBlockExecutableFilesNullMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyDeletionSingleFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyDeletionArrayFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyDeletionWithDeleteException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyDeletionArrayWithDeleteException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyDeletionNoExistingFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyDeletionEmptyStringExistingId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyThrowsIfPropertyNotInSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyThrowsIfNotFileProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyArrayNotFileThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyArrayReceivesNonArrayThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertySingleFileUpload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyArrayFileUpload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyAutoPublishFromSchemaConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyAutoPublishAtPropertyLevel":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyWithAutoTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertySingleValueNotFileProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyArrayWithNonFileItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessSingleFilePropertyWithDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessSingleFilePropertyWithBase64":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessSingleFilePropertyWithFileObjectExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessSingleFilePropertyWithUnsupportedType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessSingleFilePropertyWithFileObjectNoExistingFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessSingleFilePropertyWithFileObjectNonNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessSingleFilePropertyWithFileObjectGetFileReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testProcessSingleFilePropertyWithIndex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testGenerateFileNameWithoutIndex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testGenerateFileNameWithIndex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testPrepareAutoTagsBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testPrepareAutoTagsWithConfiguredTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testPrepareAutoTagsDeduplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testPrepareAutoTagsNonArrayAutoTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testGetExtensionFromMimeTypeCommonTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testGetExtensionFromMimeTypeUnknownTypeReturnsBin":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testGetExtensionFromMimeTypeOfficeDocuments":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataFromUrlWithExtension":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testParseFileDataFromUrlWithoutExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testGetCommonFileExtensionsContainsExpectedTypes":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testGetDangerousExecutableExtensionsContainsExpected":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testGetExecutableMimeTypesContainsExpected":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesSafeContent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesWindowsExe":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesElf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesPerlShebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesRubyShebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesNodeShebang":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesEmbeddedPhpTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesPhpShortEchoTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testDetectExecutableMagicBytesZipSignatureSkipped":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyDeletionArrayWithNonNumericIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyArrayMixedFileAndNonFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\FilePropertyHandlerTest::testHandleFilePropertyPropertyAutoPublishOverridesSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetOrganisationSettingsOnlyEmptyConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetOrganisationSettingsOnlyWithStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetDefaultOrganisationUuidDirectKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetDefaultOrganisationUuidFallbackToNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetDefaultOrganisationUuidReturnsNullWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetDefaultOrganisationIdReturnsUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetDefaultOrganisationIdReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testSetDefaultOrganisationId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testCreateOrganisationWithInvalidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testCreateOrganisationWithoutUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testCreateOrganisationSetsAsDefaultWhenNoneExists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testCreateOrganisationWithValidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testCreateOrganisationAddsAdminGroupUsers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetOrganisationSettingsOnlyThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetDefaultOrganisationUuidReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetUserOrganisationsReturnsEmptyWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetUserOrganisationsReturnsOrganisationsForUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testHasAccessToOrganisationReturnsTrueForAdmin":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testHasAccessToOrganisationReturnsFalseForNonMember":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testHasAccessToOrganisationReturnsFalseWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testHasAccessToOrganisationReturnsFalseWhenOrgNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testSetActiveOrganisationThrowsWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testSetActiveOrganisationThrowsWhenOrgNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testSetActiveOrganisationThrowsWhenUserNotMember":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testSetActiveOrganisationSucceedsForMember":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testJoinOrganisationThrowsWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testJoinOrganisationThrowsWhenOrgNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testJoinOrganisationSucceedsForCurrentUser":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testJoinOrganisationThrowsWhenTargetUserDoesNotExist":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testLeaveOrganisationThrowsWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testLeaveOrganisationThrowsWhenLastOrg":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testClearCacheReturnsFalseWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testClearCacheReturnsTrueForLoggedInUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testClearCacheWithPersistentDeletesUserValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testGetUserOrganisationStatsReturnsEmptyWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationCrudTest::testClearDefaultOrganisationCacheResetsStaticCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testGetOrganisationSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testGetOrganisationSettingsOnlyWithStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testHasAccessToOrganisationNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testHasAccessToOrganisationAdminUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testGetUserOrganisationStatsNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testClearCacheNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testClearCacheWithPersistent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testClearCacheWithoutPersistent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testClearDefaultOrganisationCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testGetUserOrganisationsNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testGetActiveOrganisationNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testSetActiveOrganisationNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testJoinOrganisationNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testLeaveOrganisationNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceBranchCoverageTest::testCreateOrganisationWithInvalidUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testGetOrganisationSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testGetOrganisationSettingsOnlyReturnsStoredValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testGetDefaultOrganisationUuidReturnsDirectConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testGetDefaultOrganisationUuidReturnsNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testGetDefaultOrganisationUuidHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testGetUserOrganisationsReturnsEmptyWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testGetActiveOrganisationReturnsNullWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testSetActiveOrganisationThrowsWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testSetActiveOrganisationThrowsWhenOrgNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testSetActiveOrganisationThrowsWhenUserNotMember":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testJoinOrganisationThrowsWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testJoinOrganisationThrowsWhenTargetUserNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testLeaveOrganisationThrowsWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testHasAccessToOrganisationReturnsFalseWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testHasAccessToOrganisationReturnsFalseWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testHasAccessToOrganisationReturnsTrueForAdmin":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testHasAccessToOrganisationChecksUserMembership":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testGetUserOrganisationStatsReturnsEmptyWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testClearDefaultOrganisationCacheClearsStaticCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testClearCacheReturnsFalseWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testClearCacheReturnsTrueWhenUserExists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testClearCacheWithClearPersistentDeletesUserConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\OrganisationServiceGapTest::testSetDefaultOrganisationIdStoresValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyWithValidAllowedTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsNonArrayAllowedTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsNonStringMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsInvalidMimeFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyWithValidMaxSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsNonNumericMaxSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsNegativeMaxSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsExcessiveMaxSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyWithValidAllowedTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsNonArrayAllowedTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsNonStringTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsEmptyTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsLongTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyWithValidAutoTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsNonArrayAutoTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsNonStringAutoTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsEmptyAutoTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidateFilePropertyRejectsLongAutoTag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\PropertyValidatorHandlerTest::testValidatePropertyOnDeleteWithItemsRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testGetSchemaMemoryCacheHit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testGetSchemaNotFoundReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testClearSchemaCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testClearSchemaCacheDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testInvalidateForSchemaChange":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testInvalidateForSchemaChangeDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testClearAllCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testCleanExpiredEntriesRemovesSome":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testCleanExpiredEntriesNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testGetCacheStatistics":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerBranchCoverageTest::testCacheSchemaConfigurationAndProperties":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerDeepTest::testGetSchemaNotFoundInDbReturnsNull":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerDeepTest::testClearSchemaCacheWithDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerDeepTest::testClearAllCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerDeepTest::testCleanExpiredEntries":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerDeepTest::testInvalidateForSchemaChangeWithDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Schemas\\SchemaCacheHandlerDeepTest::testGetCacheStatistics":0,"Unit\\Service\\SearchTrailServiceTest::testGetUserAgentStatisticsReturnsEnhancedStats":0,"Unit\\Service\\SearchTrailServiceTest::testGetUserAgentStatisticsWithDateRange":0,"Unit\\Service\\SearchTrailServiceTest::testGetUserAgentStatisticsWithEdgeBrowser":0,"Unit\\Service\\SearchTrailServiceTest::testGetUserAgentStatisticsWithOperaBrowser":0,"Unit\\Service\\SearchTrailServiceTest::testGetUserAgentStatisticsWithUnknownBrowser":0,"Unit\\Service\\SearchTrailServiceTest::testGetUserAgentStatisticsEmptyResults":0,"Unit\\Service\\SearchTrailServiceTest::testGetUserAgentStatisticsBrowserDistribution":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithUnderscorePaginationParams":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithUnderscorePageParam":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsCalculatesPageFromOffset":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithUnderscoreSearchParam":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithSortParams":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithUnderscoreSortParams":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithDateFilters":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithInvalidDateFilters":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithCustomFilterParams":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithLimitLessThanOne":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithPageLessThanOne":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchActivityWithSinglePeriod":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchActivityWithDecreasingTrend":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchActivityWithStableTrend":0,"Unit\\Service\\SearchTrailServiceTest::testGetRegisterSchemaStatisticsGoodRating":0,"Unit\\Service\\SearchTrailServiceTest::testGetRegisterSchemaStatisticsAverageRating":0,"Unit\\Service\\SearchTrailServiceTest::testGetRegisterSchemaStatisticsPoorRating":0,"Unit\\Service\\SearchTrailServiceTest::testGetRegisterSchemaStatisticsSortsByPercentage":0,"Unit\\Service\\SearchTrailServiceTest::testGetRegisterSchemaStatisticsWithDateRange":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailEnrichesWithRegisterAndSchemaNames":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailHandlesRegisterDoesNotExist":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailHandlesSchemaDoesNotExist":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailHandlesRegisterGenericException":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailHandlesSchemaGenericException":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsEnrichesMultipleTrails":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailWithNullRegisterAndSchema":0,"Unit\\Service\\SearchTrailServiceTest::testCleanupSearchTrailsNoExpiredEntries":0,"Unit\\Service\\SearchTrailServiceTest::testCleanupSearchTrailsSuccessMessage":0,"Unit\\Service\\SearchTrailServiceTest::testGetPopularSearchTermsWithDateRange":0,"Unit\\Service\\SearchTrailServiceTest::testGetPopularSearchTermsEmpty":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchActivityWithDateRange":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsWithZeroTotalReturnsZeroPages":0,"Unit\\Service\\SearchTrailServiceTest::testGetSearchTrailsPageCalculation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearCacheObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearCacheSchemaType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearCacheFacetType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearCacheDistributedType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearCacheNamesType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearCacheAllType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearCacheInvalidTypeThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testWarmupNamesCacheSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testWarmupNamesCacheWhenNoCacheHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testWarmupNamesCacheWithContainerFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testGetCacheStatsSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testGetCacheStatsWithDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testGetCacheStatsZeroRequests":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearObjectCacheWhenContainerThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearSchemaWhenNoEntriesKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearDistributedCacheWhenException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerBranchCoverageTest::testClearCacheWithUserId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testCalculateHitRateWithZeroRequests":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testCalculateHitRateWithRequests":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testCalculateHitRateWithMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testGetDistributedCacheStatsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testGetDistributedCacheStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testGetCachePerformanceMetrics":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearCacheWithObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearCacheWithSchemaType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearCacheWithFacetType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearCacheWithDistributedType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearCacheWithNamesType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearCacheWithInvalidType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearCacheAllTypeIndividualServices":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearObjectCacheWithNoHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearObjectCacheLazyLoadFromContainer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearObjectCacheContainerThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testWarmupNamesCacheSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testWarmupNamesCacheNoHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testWarmupNamesCacheLazyLoadFromContainer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearSchemaCacheWithEntriesKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearSchemaCacheWithoutEntriesKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearSchemaCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearFacetCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerCoverageTest::testClearDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testGetCacheStatsReturnsFullStructure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testGetCacheStatsStructureOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testGetCacheStatsPerformanceMetrics":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testGetCacheStatsDistributedCacheFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testGetCacheStatsDistributedCacheSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testGetCacheStatsServicesSchemaAndFacetDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testGetCacheStatsLastUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testHitRateWithPositiveRequests":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheAllTriggersTypeError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheAllCallsAllClearMethods":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheFacet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheDistributedTriggersTypeError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheNames":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheInvalidType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheWithUserId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheObjectNoCacheHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheNamesNoCacheHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheSchemaOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheFacetOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheDistributedOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheTotalClearedNonDistributed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheDefaultTypeTriggersTypeError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearObjectCacheContainerFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearObjectCacheContainerFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testWarmupNamesCacheSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testWarmupNamesCacheNoCacheHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testWarmupNamesCacheContainerFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testWarmupNamesCacheContainerFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testWarmupNamesCacheWarmupThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearSchemaCacheClearedCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearSchemaCacheNoEntriesKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearFacetCacheClearedCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearNamesCacheBeforeAfterStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testConstructorMinimalParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearNamesCacheContainerFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearNamesCacheContainerFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheTimestampFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheErrorsArrayEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testIsMultiTenancyEnabledReturnsFalseWhenKeyMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsWithStoredMultitenancyConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsWithStoredRetentionConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsWithAllConfigsStored":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsIncludesAvailableTenants":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsUserWithNullDisplayNameFallsBackToUid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsUserWithEmptyDisplayNameFallsBackToUid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsOrganisationMapperExceptionReturnsEmptyTenants":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsWithEmptyDataReturnsGetSettings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsWithAllSections":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdatePublishingOptionsAcceptsStringTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdatePublishingOptionsWithOldStylePublishingView":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdatePublishingOptionsStoresFalseValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetRbacSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateRbacSettingsOnlyWithAllFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateRbacSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetOrganisationSettingsOnlyParsesStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetOrganisationSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateOrganisationSettingsOnlyStoresAndReturns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateOrganisationSettingsOnlyDefaultsMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateOrganisationSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetMultitenancySettingsOnlyParsesStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetMultitenancySettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateMultitenancySettingsOnlyStoresAndReturns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateMultitenancySettingsOnlyDefaultsMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateMultitenancySettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetDefaultOrganisationUuidReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testSetDefaultOrganisationUuidWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testSetDefaultOrganisationUuidLogsErrorOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetTenantIdReturnsEmptyStringWhenDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetTenantIdReturnsStoredValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetTenantIdReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetOrganisationIdDelegatesToGetDefaultOrganisationUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetOrganisationIdReturnsNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyParsesStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyAddsEnabledWhenMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyAddsVectorConfigWhenMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyFillsMissingVectorConfigFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyFillsMissingBackendOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyFillsMissingSolrFieldOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyRemovesDeprecatedSolrCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetLLMSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateLLMSettingsOnlyStoresAndReturns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateLLMSettingsOnlyMergesWithExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateLLMSettingsOnlyWithFireworksConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateLLMSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetFileSettingsOnlyParsesStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetFileSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateFileSettingsOnlyStoresAndReturns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateFileSettingsOnlyDefaultsMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateFileSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetN8nSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetN8nSettingsOnlyParsesStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetN8nSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateN8nSettingsOnlyStoresAndReturns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateN8nSettingsOnlyDefaultsMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateN8nSettingsOnlyThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testConstructorWithCustomAppName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsRetentionPartialConfigUsesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetSettingsSolrPartialConfigUsesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsRetentionDefaultsMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsSolrCastsNumericValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetMultitenancySettingsOnlyPartialConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetRbacSettingsOnlyPartialConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testGetOrganisationSettingsOnlyPartialConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateSettingsSolrWithCollectionFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateLLMSettingsOnlyFullConfigFromEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ConfigurationSettingsHandlerTest::testUpdateFileSettingsOnlyWithCustomFileTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\FileSettingsHandlerTest::testGetFileSettingsReturnsDefaultWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\FileSettingsHandlerTest::testGetFileSettingsReturnsDecodedConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\FileSettingsHandlerTest::testGetFileSettingsThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\FileSettingsHandlerTest::testUpdateFileSettingsWithFullData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\FileSettingsHandlerTest::testUpdateFileSettingsWithEmptyDataUsesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\FileSettingsHandlerTest::testUpdateFileSettingsWithPartialData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\FileSettingsHandlerTest::testUpdateFileSettingsThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\FileSettingsHandlerTest::testUpdateFileSettingsStoresCorrectJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerCoverageTest::testGetLLMSettingsOnlyReturnsDefaultsWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerCoverageTest::testGetLLMSettingsOnlyAddsEnabledIfMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerCoverageTest::testGetLLMSettingsOnlyAddsVectorConfigIfMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerCoverageTest::testGetLLMSettingsOnlyFillsMissingVectorConfigFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerCoverageTest::testGetLLMSettingsOnlyPreservesExistingVectorConfigFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerCoverageTest::testUpdateLLMSettingsOnlyMergesWithExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerCoverageTest::testUpdateLLMSettingsOnlyWithAllProviders":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testGetLlmSettingsReturnsDefaultWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testGetLlmSettingsReturnsDecodedConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testGetLlmSettingsAddsEnabledFieldIfMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testGetLlmSettingsAddsVectorConfigIfMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testGetLlmSettingsFillsMissingVectorSubFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testGetLlmSettingsRemovesDeprecatedSolrCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testGetLlmSettingsThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testUpdateLlmSettingsWithFullData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testUpdateLlmSettingsPatchBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testUpdateLlmSettingsWithEmptyDataUsesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\LlmSettingsHandlerTest::testUpdateLlmSettingsThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testGetRetentionSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testGetRetentionSettingsOnlyParsesStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testGetRetentionSettingsOnlyThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testUpdateRetentionSettingsOnlyStoresConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testUpdateRetentionSettingsOnlyAppliesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testUpdateRetentionSettingsOnlyThrowsOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testGetVersionInfoOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testConvertToBooleanWithNumericValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testConvertToBooleanWithStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ObjectRetentionHandlerTest::testGetObjectSettingsOnlyMigratesLegacyFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testGetSearchBackendConfigReturnsDefaultWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testGetSearchBackendConfigReturnsDecodedConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testGetSearchBackendConfigThrowsRuntimeExceptionOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testUpdateSearchBackendConfigWithSolr":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testUpdateSearchBackendConfigWithElasticsearch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testUpdateSearchBackendConfigWithInvalidBackendThrowsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testUpdateSearchBackendConfigSetsTimestamp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testUpdateSearchBackendConfigThrowsRuntimeExceptionOnWriteError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testUpdateSearchBackendConfigLogsCorrectContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SearchBackendHandlerTest::testUpdateSearchBackendConfigAvailableBackendsComplete":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrSettingsReturnsDefaultsWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrSettingsReturnsStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrSettingsThrowsOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testWarmupSolrIndexThrowsDeprecatedException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrDashboardStatsWithAvailableCacheHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrDashboardStatsReturnsDefaultsOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrDashboardStatsUsesContainerFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrDashboardStatsNoCacheHandlerOrContainer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrDashboardStatsContainerResolutionFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testTransformSolrStatsToDashboardWhenUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testTransformStatsWithZeroOperations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testTransformStatsWithOperations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testTransformStatsFormatsIndexSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testTransformStatsWithMissingServiceStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testFormatBytesZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testFormatBytesLargeValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrSettingsOnlyReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrSettingsOnlyReturnsStoredWithDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrSettingsOnlyThrowsOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSolrSettingsOnlySavesConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSolrSettingsOnlyAppliesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSolrSettingsOnlyThrowsOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSearchBackendConfigReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSearchBackendConfigReturnsStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSearchBackendConfigThrowsOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSearchBackendConfigSolr":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSearchBackendConfigElasticsearch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSearchBackendConfigInvalidBackend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSearchBackendConfigThrowsOnSaveError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrFacetConfigurationReturnsDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrFacetConfigurationReturnsStored":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testGetSolrFacetConfigurationThrowsOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSolrFacetConfigurationSaves":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSolrFacetConfigurationEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSolrFacetConfigurationSkipsInvalidKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSolrFacetConfigurationFiltersGlobalOrder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testUpdateSolrFacetConfigurationThrowsOnSaveError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testConstructorMinimalParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testConstructorCustomAppName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\SolrSettingsHandlerTest::testTransformAvailableStatsHasLastCommit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ValidationOperationsHandlerTest::testValidateAllObjectsReturnsEmptyWhenObjectServiceUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ValidationOperationsHandlerTest::testValidateAllObjectsHasCorrectStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ValidationOperationsHandlerTest::testValidateAllObjectsValidationErrorsIsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ValidationOperationsHandlerTest::testValidateAllObjectsSummaryHasCorrectTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ValidationOperationsHandlerTest::testValidateAllObjectsReturns100PercentForZeroObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ValidationOperationsHandlerTest::testConstructorStoresDependencies":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ValidationOperationsHandlerTest::testValidateAllObjectsCanBeCalledMultipleTimes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\ValidationOperationsHandlerTest::testValidateAllObjectsInvalidCountMatchesErrorCount":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testFormatBytesBytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testFormatBytesKilobytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testFormatBytesMegabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testFormatBytesGigabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testFormatBytesZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testFormatBytesCustomPrecision":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testConvertToBytesGigabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testConvertToBytesMegabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testConvertToBytesKilobytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testConvertToBytesPlainNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testMaskTokenShort":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testMaskTokenEightChars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testMaskTokenLong":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetSearchBackendConfigEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetSearchBackendConfigValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetSearchBackendConfigException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testUpdateSearchBackendConfigWithActiveKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testUpdateSearchBackendConfigWithBackendKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetDatabaseInfoEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetDatabaseInfoInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetDatabaseInfoValid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetDatabaseInfoMissingDatabaseKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testHasPostgresExtensionNotPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testHasPostgresExtensionFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testHasPostgresExtensionNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testHasPostgresExtensionNoDbInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetPostgresExtensionsNotPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetPostgresExtensionsReturns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testMassValidateObjectsInvalidMode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testMassValidateObjectsBatchSizeTooSmall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testMassValidateObjectsBatchSizeTooLarge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCreateBatchJobs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCreateBatchJobsZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCreateBatchJobsExact":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCompareFieldsNoDifferences":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCompareFieldsMissingFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCompareFieldsExtraFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCompareFieldsMismatchedType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCompareFieldsMismatchedMultiValued":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testCompareFieldsSkipsSystemFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetLLMSettingsOnlyDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetFileSettingsOnlyDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetObjectSettingsOnlyDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetRetentionSettingsOnlyDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetCacheStatsDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testClearCacheDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testClearCacheWithTypeDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testIsMultiTenancyEnabledDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetTenantIdDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceDeepTest::testGetOrganisationIdDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testFormatBytesWithBytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testFormatBytesWithKilobytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testFormatBytesWithMegabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testFormatBytesWithGigabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testFormatBytesWithZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testFormatBytesWithCustomPrecision":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testConvertToBytesWithMegabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testConvertToBytesWithGigabytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testConvertToBytesWithKilobytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testConvertToBytesWithPlainNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testMaskTokenWithLongToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testMaskTokenWithShortToken":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testMaskTokenWith8Chars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testMaskTokenWith9Chars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testCompareFieldsFindsMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testCompareFieldsFindsExtra":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testCompareFieldsFindsMismatched":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testCompareFieldsAllMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testCompareFieldsSkipsSystemFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testCompareFieldsDetectsMultiValuedMismatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetDatabaseInfoReturnsNullWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetDatabaseInfoReturnsNullWhenInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetDatabaseInfoReturnsNullWhenNoDatabaseKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetDatabaseInfoReturnsDatabaseData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testHasPostgresExtensionReturnsFalseWhenNoDbInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testHasPostgresExtensionReturnsFalseForNonPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testHasPostgresExtensionReturnsTrueWhenFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetPostgresExtensionsReturnsEmptyForNonPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetPostgresExtensionsReturnsList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetSearchBackendConfigReturnsDefaultsWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetSearchBackendConfigReturnsStoredConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetSearchBackendConfigHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetLLMSettingsOnlyDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetFileSettingsOnlyDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetObjectSettingsOnlyDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetRetentionSettingsOnlyDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testIsMultiTenancyEnabledDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceGapTest::testGetDefaultOrganisationUuidDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetExpectedSchemaFieldsWithSetupHandler":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetExpectedSchemaFieldsWithoutSetupHandlerOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetExpectedSchemaFieldsFallbackToCoreFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseExceptionPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseUnknownComponent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseSolrAndCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsNegativeBatchSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsModeValidatedFirst":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testClearCacheSpecificType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsMissingTypeKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsActualMissingType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsExtraFieldType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsExtraFieldNoType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsMissingFieldNoType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesOneByte":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesPrecisionZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testFormatBytesLargeTB":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesLowercaseG":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesLowercaseM":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConvertToBytesZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testHasPostgresExtensionMissingTypeKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetPostgresExtensionsNoExtensionsKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testUpdateSearchBackendConfigBackendPrecedence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsOuterException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testWarmupNamesCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testValidateAllObjectsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetLLMSettingsOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetFileSettingsOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetObjectSettingsOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSolrSettingsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetRetentionSettingsOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetRbacSettingsOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetMultitenancySettingsOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetOrganisationSettingsOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetVersionInfoOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetCacheStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSolrSettingsOnlyException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSolrFacetConfigurationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetSolrDashboardStatsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConstructorWithAllHandlers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testConstructorWithCustomAppName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMaskTokenBoundaryTenChars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMaskTokenSingleChar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsMismatchEntryStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testCompareFieldsSummaryStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetDatabaseInfoJsonDecodesToScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testWarmupSolrIndexDelegatesToHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testWarmupSolrIndexWithParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsSerialZeroObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsParallelZeroObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsWithMaxObjectsLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsSerialWithObjectsThrowsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsParallelWithObjectsThrowsTypeError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsBatchSizeOne":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsBatchSizeFiveThousand":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsConfigUsed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsDurationAndMemory":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithSuccessfulDbStats":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithMysqlPlatform":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithMagicMapperTables":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWhenMainQueryReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithFailingMagicMapperTable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsMaxObjectsEqualsTotal":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsMaxObjectsGreaterThanTotal":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithDatabaseExceptionFallsBackToZeroedStats":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithCacheExceptionRecordsErrorInCacheKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseWithAllComponentRebasesBothSolrAndCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseWithSolrOnlyComponentDoesNotRebaseCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseWithCacheOnlyComponentDoesNotRebaseSolr":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseWithDefaultOptionsUsesAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsSerialWithObjectsAndNullServiceThrowsTypeError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsParallelModeWithObjectsThrowsTypeErrorForNullService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testMassValidateObjectsSerialCollectErrorsFalseThrowsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithSuccessfulDbQueriesReturnsSystemInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testRebaseReturnsErrorOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsMagicMapperOuterCatchWhenGetDatabasePlatformThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsMagicMapperInnerCatchWhenTableQueryFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsMagicMapperSuccessPathWithTableData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithPostgresPlatformUsesPostgresQuery":0.009,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsWithSourcesTableExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SettingsServiceTest::testGetStatsOuterCatchWhenFinalQueryReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testExtractFromChunkWithEmptyText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testExtractFromChunkWithWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testExtractFromChunkLLMFallsBackToRegex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testExtractFromChunkHybridMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testExtractFromChunkUnknownMethodThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testProcessSourceChunksWithNoChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testProcessSourceChunksFiltersMetadataChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testProcessSourceChunksHandlesChunkException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testHighConfidenceThresholdFiltersLowConfidence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testPresidioFallsBackToRegexWhenNotConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testOpenAnonymiserFallsBackToRegex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testEntityTypeFilterInRegex":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testExtractEmailAndCreateEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerBranchTest::testExtractIbanViaRegex":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testStoreDetectedEntitiesSetsObjectIdForObjectSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testStoreDetectedEntitiesCatchesExceptionAndContinues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testStoreDetectedEntitiesUsesCategoryFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testProcessSourceChunksChunkExceptionIsCaughtAndContinues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testDetectWithPresidioConfiguredEndpointCurlFails":10.02,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testDetectWithOpenAnonymiserConfiguredEndpointCurlFails":10.012,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testPostAnalyzeRequestReturnsNullOnCurlError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testBuildAnalyzeRequestBodyWithoutEntityTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testBuildAnalyzeRequestBodyWithMappedEntityTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testBuildAnalyzeRequestBodyWithEmptyEntityTypesArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testConvertApiResultsMissingScoreUsesDefaultConfidence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testConvertApiResultsFiltersLowDefaultConfidence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testConvertApiResultsExtractsValueFromSourceText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testConvertApiResultsMapsEntityTypesCorrectly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testMapToPresidioEntityTypesAllKnownTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testMapFromPresidioEntityTypeCreditCard":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testMapFromPresidioEntityTypeCrypto":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testMapFromPresidioEntityTypeUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testMapFromPresidioEntityTypeNrp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testMapFromPresidioEntityTypeCompletelyUnknown":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testExtractContextNegativeClampedStart":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testExtractContextSmallWindow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testGetCategoryForAllEntityTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testDetectWithRegexHighConfidenceFiltersPhones":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testDetectWithRegexEntityTypesFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testExtractFromChunkOtherSourceTypeNoFileOrObjectId":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testFindOrCreateEntityUpdatesExistingEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testDetectEntitiesUnknownMethodThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerCoverageTest::testGetRegexPatternsReturnsThreePatterns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkReturnsEmptyForNullText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkDetectsMultipleEmails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkSetsFileIdForFileSourceType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkSetsObjectIdForObjectSourceType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkSetsNeitherFileNorObjectForOtherSourceType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkUsesDefaultMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkUsesDefaultConfidenceThreshold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testProcessSourceChunksHandlesExceptionInExtraction":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testProcessSourceChunksAccumulatesResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testProcessSourceChunksWithOnlyMetadataChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithUnknownMethodThrowsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkRespectsLowConfidenceThreshold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithPhoneTypeFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithCustomContextWindow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testStoreDetectedEntitiesHandlesInsertException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testStoreDetectedEntitiesHandlesRelationInsertException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testFindOrCreateEntityReusesExistingEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForPhoneType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForAddressType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForSsnType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForIpAddressType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractContextWithZeroWindow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractContextWithLargeWindow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractContextClampsStartToZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractContextClampsEndToTextLength":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexFindsPhoneNumbers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexFiltersEntityTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexFiltersEntityTypesExcludesUnrequested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexFiltersByConfidenceThreshold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexIncludesPositionInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexFindsMultipleEntitiesOfSameType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectEntitiesThrowsOnUnknownMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectEntitiesCallsRegexMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectEntitiesCallsHybridMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectEntitiesCallsLlmMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testBuildAnalyzeRequestBodyWithEmptyEntityTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testBuildAnalyzeRequestBodyWithUnmappableEntityTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testMapToPresidioEntityTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testMapToPresidioEntityTypesWithOrganization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testMapToPresidioEntityTypesWithUnknownType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testMapToPresidioEntityTypesWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testMapFromPresidioEntityTypeKnownTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testMapFromPresidioEntityTypePassthroughTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testMapFromPresidioEntityTypeUnknown":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsToEntitiesBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsFiltersLowConfidence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsUsesDefaultConfidence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsDefaultConfidenceBelowThreshold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsUsesTextField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsExtractsFromTextWhenNoTextField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsHandlesMissingStartEnd":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsHandlesUnknownEntityType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsHandlesMissingEntityType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsMultipleEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testConvertApiResultsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetRegexPatternsReturnsExpectedStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetRegexPatternsContainsEmailPhoneIban":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithPresidioFallsBackOnException":10.014,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithOpenAnonymiserFallsBackOnException":10.026,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithLlmLogsFallbackMessage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithHybridReturnsRegexResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithHybridRespectsEntityTypeFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testFindOrCreateEntityCreatesNewEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testFindOrCreateEntityReturnsExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testRelationHasCorrectDetectionMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testRelationHasCorrectPositions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testRelationHasCreatedAt":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testRelationHasConfidence":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkReturnsCorrectEntityStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexIbanHasSensitivePiiCategory":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithPresidioEmptyEndpointFallsBackToRegex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithOpenAnonymiserEmptyEndpointFallsBackToRegex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectsEmailAndIbanTogether":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectsPhoneAndEmailTogether":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testProcessSourceChunksReturnsSummedCounts":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkDetectsIban":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithOpenAnonymiserHandlesPiiEntitiesWrapper":10.015,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testGetCategoryForIpAddressReturnsContextual":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testExtractFromChunkWithPresidioMethodFallsBackWhenNoEndpoint":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testStoreDetectedEntitiesSetsFileIdForFileSource":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testDetectWithRegexIbanCategoryIsSensitivePii":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EntityRecognitionHandlerTest::testProcessSourceChunksReturnsZerosWhenNoNonMetadataChunks":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonMissingSource":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonRemovesInternalParams":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonWithArrayInput":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonWithStringInput":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonWithInvalidJsonString":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonWithFileThrowsException":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonWithNullJsonValue":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonWithFalseJsonValue":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonOnlyInternalParamsRemoved":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonWithEmptyData":0,"Unit\\Service\\UploadServiceGapTest::testGetUploadedJsonAllInternalParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testJoinOrganisationNoUserLoggedIn":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testJoinOrganisationTargetUserNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testJoinOrganisationWithTargetUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testLeaveOrganisationNoUserLoggedIn":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testLeaveOrganisationWithTargetUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testLeaveOrganisationNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testHasAccessToOrganisationAdminUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserOrganisationRelationshipTest::testHasAccessToOrganisationNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceBranchCoverageTest::testGetCurrentUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceBranchCoverageTest::testGetCurrentUserNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceBranchCoverageTest::testGetCustomNameFieldsReturnsNames":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceBranchCoverageTest::testGetCustomNameFieldsAllEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceBranchCoverageTest::testSetCustomNameFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceBranchCoverageTest::testBuildUserDataArrayHandlesOrganisationException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceBranchCoverageTest::testUpdateUserPropertiesWithActiveOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceBranchCoverageTest::testUpdateUserPropertiesWithFailedOrganisationSwitch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetCurrentUserReturnsUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetCurrentUserReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testBuildQuotaWithNumericQuota":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testBuildQuotaWithNoneQuota":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testBuildQuotaWithUnlimitedQuota":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testBuildQuotaWithZeroTotalBytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetLanguageAndLocaleWithEnglish":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetLanguageAndLocaleWithNonEnglish":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetLanguageAndLocaleWithExplicitLocale":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetLanguageAndLocaleNoMethodsAvailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetAdditionalProfileInfoFallbackOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetAdditionalProfileInfoWithOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetCustomNameFieldsEmptyValuesReturnNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetCustomNameFieldsWithValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testSetCustomNameFieldsOnlyAllowedFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetAccountManagerPropertiesSelectivelyWithException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetAccountManagerPropertiesSelectivelyWithValues":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetAccountManagerPropertiesSelectivelyEmptyValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetDefaultPropertyScopeReturnsPrivateForUnknown":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetDefaultPropertyScopeReturnsPublishedForWebsite":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetDefaultPropertyScopeReturnsLocalForOrg":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testGetDefaultPropertyScopeReturnsPrivateForPhone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testDetermineChangedFieldsDetectsChanges":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testDetermineChangedFieldsHandlesMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testDetermineChangedFieldsNoChanges":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceCoverageTest::testDetermineChangedFieldsNullToValue":0,"Unit\\Service\\UserServiceTest::testGetCustomNameFieldsPartialValues":0,"Unit\\Service\\UserServiceTest::testSetCustomNameFieldsCastsToString":0,"Unit\\Service\\UserServiceTest::testSetCustomNameFieldsEmptyArrayDoesNothing":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayReturnsBasicUserInfo":0.001,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayHandlesOrganisationException":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayCachesOrgStats":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayBackendCapabilities":0.001,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayNameFields":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayWithAccountProperties":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArraySkipsEmptyAccountProperties":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayOrganisationTransformAddsNaam":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayOrganisationNullActive":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayFunctieFromRole":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayFunctieFallbackFromConfig":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayCustomNameFieldsFromConfig":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayOrganisationFromConfig":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayAccountManagerExceptionFallsBackToConfig":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayMultipleGroups":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayQuotaDefaults":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayDefaultSubadminEmpty":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayWithExtendedUserMethods":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayEmailVerifiedFalse":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayWithLanguageAndLocale":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayLocaleFromLanguageNonEnglish":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayLocaleFromLanguageEnglish":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayLanguageAlreadySetSkipsL10NFactory":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayQuotaNumericRelativeCalculation":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayQuotaUnlimited":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayQuotaNone":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayQuotaExceptionFallback":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayOrgWithoutNameKey":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesBasicUpdate":0.001,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesWithOrganisationSwitch":0.001,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesWithFailedOrganisationSwitch":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesWithNameFields":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesNoChangesNoEvent":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesDispatchesEventOnChange":0.001,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesStoresFunctieInConfig":0.001,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesOrgSwitchInvalidatesCacheAndRefetches":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesHandlesProfileUpdateException":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesUpdatesExistingAccountProperty":0.001,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesDoesNotUpdateUnchangedProperty":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesResultStructure":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesWithNonStringActiveOrganisation":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesWithMultipleProfileFields":0.001,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesStandardFieldsWithExtendedUser":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesDisplayNameNotChangedWhenNotAllowed":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesDefaultScopeForUnknownProperty":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesDetectsMultipleChanges":0.001,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesFunctieMapsTopropertyRole":0.001,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayAccountManagerFallbackEmptyValues":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayNoGroups":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayFunctieNullWhenNoRoleOrFunctie":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayDefaultEmailVerifiedAndAvatarScope":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesSetsLanguageAndLocale":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayCanChangeMailAddressFalse":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayQuotaOuterException":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayQuotaZeroTotalBytes":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayOrganisationsHasAvailableTrue":0,"Unit\\Service\\UserServiceTest::testSetCustomNameFieldsWithIntegerValue":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesResultContainsOrganisationUpdatedFalseByDefault":0,"Unit\\Service\\UserServiceTest::testGetCurrentUserDelegatesToUserSession":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesActiveOrganisationNonStringSkipsSwitch":0,"Unit\\Service\\UserServiceTest::testGetCustomNameFieldsAllNull":0,"Unit\\Service\\UserServiceTest::testBuildUserDataArrayOrganisationNaamMatchesName":0,"Unit\\Service\\UserServiceTest::testUpdateUserPropertiesOrgMessageOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendReturnsEmptyWhenNoVectors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendRanksHigherSimilarityFirst":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendRespectsLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendSkipsInvalidEmbeddings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendDecodesJsonMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendPropagatesDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendWithEntityTypeStringFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendWithEntityTypeArrayFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchCombinesBothSources":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchNormalisesWeights":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchSourceBreakdownCategorisesResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchWithZeroVectorWeightSkipsSemanticSearch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchReturnsTopNResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchVectorSearchFailsGracefully":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchRrfOrdersByDescendingCombinedScore":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testCosineSimilarityIdenticalVectorsReturnsOne":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testCosineSimilarityOrthogonalVectorsReturnsZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchSolrBackendThrowsWhenSolrUnavailable":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchSolrBackendThrowsWhenNoCollections":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendUsesDefaultMaxVectors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendUsesCustomMaxVectors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchPhpBackendResultHasExpectedKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchResultHasAllExpectedTopLevelKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testCosineSimilarityDimensionMismatchThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testCosineSimilarityZeroMagnitudeReturnsZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testCosineSimilarityAntiParallelVectors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testCosineSimilarityHighDimensionalVectors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testExtractEntityIdForFileType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testExtractEntityIdForFilesType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testExtractEntityIdForObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testExtractEntityIdForObjectTypeFallbackToSelfObjectId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testExtractEntityIdForObjectTypeFallbackToId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testExtractEntityIdForFileTypeReturnsEmptyWhenNoFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetSolrCollectionForFileType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetSolrCollectionForFilesType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetSolrCollectionForObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetSolrCollectionForObjectTypeFallbackToCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetSolrCollectionReturnsNullWhenNotConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetSolrCollectionIsCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetCollectionsToSearchWithEntityTypeFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetCollectionsToSearchWithArrayEntityType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetCollectionsToSearchWithoutEntityTypeReturnsAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetCollectionsToSearchWithoutEntityTypeOnlyObjectCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testGetCollectionsToSearchSkipsUnconfiguredEntityType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testFetchVectorsWithEntityIdStringFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testFetchVectorsWithEntityIdArrayFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testReciprocalRankFusionVectorOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testReciprocalRankFusionSolrOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testReciprocalRankFusionMergesOverlappingEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testReciprocalRankFusionEmptyInputs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchDatabaseBackendUsesPHPPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchNullMetadataReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testSemanticSearchEmptyMetadataReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchZeroSolrWeightVectorOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchBothWeightsZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchMissingWeightKeysDefaultsToHalf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\Handlers\\VectorSearchHandlerTest::testHybridSearchSearchTimeIsPositive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testSemanticSearchPhpBackendNoVectors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testSemanticSearchPhpBackendWithVectors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testSemanticSearchPhpBackendWithBadSerializedEmbedding":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testSemanticSearchWithEntityTypeFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testSemanticSearchWithEntityTypeArrayFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testSemanticSearchWithEntityIdStringFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testHybridSearchCombinesVectorAndSolrResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testHybridSearchWithZeroVectorWeight":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Vectorization\\VectorSearchHandlerCoverageTest::testHybridSearchWithBothVectorAndSolr":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testRegisterStrategy":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testGetStrategyThrowsForUnknownType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeBatchEmptyEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeBatchEntityException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeBatchFetchEntitiesThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeEntitySerialSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeEntitySerialException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeEntityParallelMode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeEntityParallelWithNullEmbedding":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeEntityParallelBatchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testVectorizeEntityEmptyItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testGenerateEmbeddingDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testSemanticSearchDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testGetVectorStatsDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testTestEmbeddingDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testCheckEmbeddingModelMismatchDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testClearAllEmbeddingsDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\VectorizationServiceCoverageTest::testHybridSearchDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDispatchEventWithNoMatchingWebhooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDispatchEventWithExceptionOnFindForEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookReturnsFalseWhenDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookReturnsFalseWhenFiltersDontMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testPassesFiltersReturnsTrueWithNoFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testPassesFiltersReturnsTrueWhenAllFiltersMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testPassesFiltersReturnsFalseWhenFilterDoesNotMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testPassesFiltersWithArrayFilterValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGetNestedValueSimpleKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGetNestedValueDotNotation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGetNestedValueReturnsNullForMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGetNestedValueDeepNesting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testCalculateRetryDelayExponential":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testCalculateRetryDelayLinear":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testCalculateRetryDelayFixed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testCalculateRetryDelayDefaultPolicy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testCalculateNextRetryTimeIsInFuture":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testEventTypeToEventClass":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testEventTypeToEventClassWithSinglePart":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testShouldProcessResponseReturnsFalseByDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testShouldProcessResponseReturnsTrueWhenConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testShouldProcessResponseReturnsFalseWhenAsync":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGenerateSignatureReturnsHmac":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGenerateSignatureDifferentSecretsProduceDifferentResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testInterceptRequestReturnsParamsWhenNoWebhooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testSendRequestPostWithJsonBody":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testSendRequestGetUsesQueryParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testSendRequestAddsSignatureWhenSecretSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testSendRequestNoSignatureWithoutSecret":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testSendRequestMergesCustomHeaders":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testSendRequestUsesWebhookTimeout":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookReturnsTrueOnSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookReturnsFalseOnRequestExceptionWithResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookReturnsFalseOnConnectionError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookReturnsFalseOnUnexpectedException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookSchedulesRetryOnFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookDoesNotRetryAtMaxAttempts":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookExtractsJsonMessageFromErrorResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookExtractsJsonErrorKeyFromResponse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testScheduleRetryLogsInfo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testFindWebhooksForInterceptionReturnsEmptyWhenNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testFindWebhooksForInterceptionFiltersNonInterceptWebhooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testFindWebhooksForInterceptionReturnsMatchingWebhooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testFindWebhooksForInterceptionFiltersByEventType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDispatchEventDeliversToMatchingWebhooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDispatchEventLogsDebugForNoWebhooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDispatchEventLogsInfoWhenWebhooksFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testInterceptRequestWithCloudEventFormatter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testInterceptRequestWithMatchingWebhooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testInterceptRequestContinuesOnDeliveryException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testInterceptRequestFallbackFormatWithoutFormatter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testApplyMappingTransformationReturnsNullOnGenericFindException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testBuildPayloadCloudEventsConfiguredButNoFormatter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testBuildPayloadStandardFormatWithHigherAttempt":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testPassesFiltersReturnsFalseWhenKeyMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testPassesFiltersWithDotNotationKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testPassesFiltersArrayFilterWithDotNotation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testEventTypeToEventClassUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testEventTypeToEventClassSchemaDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testEventTypeToEventClassRegisterUpdating":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testCalculateRetryDelayExponentialAttemptZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testCalculateRetryDelayLinearAttemptOne":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testCalculateNextRetryTimeExponentialDelay":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGenerateSignatureWithEmptyPayload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGenerateSignatureWithNestedPayload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testShouldProcessResponseFalseInConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testShouldProcessResponseWithoutAsyncKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookPassesAttemptToBuildPayload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGetNestedValueWithNonArrayIntermediate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testGetNestedValueWithEmptyStringKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testInterceptRequestUsesCloudEventFormatterWhenAvailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\WebhookServiceTest::testDeliverWebhookWithMappingTransformation":0,"OCA\\OpenRegister\\Tests\\Unit\\Settings\\OpenRegisterAdminTest::testGetForm":0,"OCA\\OpenRegister\\Tests\\Unit\\Settings\\OpenRegisterAdminTest::testGetSection":0,"OCA\\OpenRegister\\Tests\\Unit\\Settings\\OpenRegisterAdminTest::testGetPriority":0,"OCA\\OpenRegister\\Tests\\Unit\\Settings\\OpenRegisterAdminTest::testGetFormWithFalseSetting":0,"Unit\\Service\\MigrationServiceTest::testGetStorageStatusWithNoMagicTable":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableReturnsStubReport":0,"Unit\\Service\\MigrationServiceTest::testMigrateToMagicTableDryRunReturnsStubReport":0,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageReturnsStubReport":0,"Unit\\Service\\MigrationServiceTest::testMigrateToBlobStorageDryRunReturnsStubReport":0,"Unit\\BackgroundJob\\BlobMigrationJobTest::testIntervalIsSetToFiveMinutes":0.001,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunWithNoBlobObjectsSetsComplete":0.027,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunSkipsWhenAlreadyComplete":0,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunProcessesBatchAndUpdatesCounters":0.01,"Unit\\BackgroundJob\\BlobMigrationJobTest::testGroupByRegisterSchemaGroupsOrphanedObjects":0.001,"Unit\\BackgroundJob\\BlobMigrationJobTest::testGroupByRegisterSchemaWithAllValidObjects":0.002,"Unit\\BackgroundJob\\BlobMigrationJobTest::testGroupByRegisterSchemaWithEmptyInput":0.001,"Unit\\BackgroundJob\\BlobMigrationJobTest::testDeleteBlobRowsDeletesByIdArray":0,"Unit\\BackgroundJob\\BlobMigrationJobTest::testDeleteBlobRowsSkipsWhenNoIds":0,"Unit\\BackgroundJob\\BlobMigrationJobTest::testDeleteBlobRowsWithEmptyArray":0,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunMarksCompleteWhenBlobTableDoesNotExist":0.001,"Unit\\BackgroundJob\\BlobMigrationJobTest::testRunLogsErrorOnException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\FilterTest::testGetIdentifier":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\FilterTest::testGetName":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\FilterTest::testGetPriority":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\FilterTest::testGetIcon":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\FilterTest::testFilterTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\FilterTest::testAllowedApps":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderSubjectHandlerTest::testApplySubjectTextObjectCreated":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderSubjectHandlerTest::testApplySubjectTextRegisterDeleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderSubjectHandlerTest::testApplySubjectTextEmptyTitle":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderSubjectHandlerTest::testApplySubjectTextBuildRichParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesValidEvent":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseThrowsForForeignApp":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseThrowsForUnknownSubject":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#object_created":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#object_updated":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#object_deleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#register_created":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#register_updated":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#register_deleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#schema_created":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#schema_updated":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\ProviderTest::testParseHandlesAllNineSubjects#schema_deleted":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\Setting\\ObjectSettingTest::testObjectSettingIdentifierAndDefaults":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\Setting\\ObjectSettingTest::testRegisterSettingIdentifier":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\Setting\\ObjectSettingTest::testSchemaSettingIdentifier":0,"OCA\\OpenRegister\\Tests\\Unit\\Activity\\Setting\\ObjectSettingTest::testAllSettingsShareGroup":0,"Unit\\BackgroundJob\\ActionRetryJobTest::testCalculateDelayExponential":0.001,"Unit\\BackgroundJob\\ActionRetryJobTest::testCalculateDelayLinear":0,"Unit\\BackgroundJob\\ActionRetryJobTest::testCalculateDelayFixed":0,"Unit\\BackgroundJob\\ActionRetryJobTest::testCalculateDelayUnknownPolicyUsesDefault":0,"Unit\\BackgroundJob\\ActionScheduleJobTest::testCronExpressionEvaluationConcept":0.001,"Unit\\BackgroundJob\\ActionScheduleJobTest::testScheduleMatchingLogic":0,"Unit\\BackgroundJob\\DestructionCheckJobTest::testConstructor":0.001,"Unit\\BackgroundJob\\DestructionCheckJobTest::testDefaultInterval":0,"Unit\\BackgroundJob\\DestructionExecutionJobTest::testIsQueuedJob":0,"Unit\\BackgroundJob\\DestructionExecutionJobTest::testDefaultBatchSize":0.001,"Unit\\BackgroundJob\\ExecutionHistoryCleanupJobTest::testDeletesOlderThanRetentionPeriod":0,"Unit\\BackgroundJob\\ExecutionHistoryCleanupJobTest::testUsesDefaultRetentionWhenNotConfigured":0,"Unit\\BackgroundJob\\ExecutionHistoryCleanupJobTest::testHandlesZeroRetentionGracefully":0,"Unit\\BackgroundJob\\HookRetryJobTest::testRunWithMissingArgumentsLogsError":0,"Unit\\BackgroundJob\\HookRetryJobTest::testRunWithMissingObjectIdLogsError":0,"Unit\\BackgroundJob\\HookRetryJobTest::testRunWithObjectLoadFailureLogsError":0,"Unit\\BackgroundJob\\HookRetryJobTest::testRunMaxRetriesReachedLogsErrorAndStops":0.001,"Unit\\BackgroundJob\\HookRetryJobTest::testRunReQueuesOnFailureBelowMaxRetries":0,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testFiresWhenIntervalElapsed":0.002,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testDoesNotFireWhenIntervalNotElapsed":0,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testFiresAfterIntervalEvenWithCachedFireTime":0,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testFilterRestrictsToMatchingObjects":0,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testEmptyFilterMatchesEveryObject":0,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testNonScheduledTriggersAreIgnored":0.001,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testIntervalBelowMinimumIsSkipped":0,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testPerObjectDispatchFailureDoesNotBlockOthers":0.001,"Unit\\BackgroundJob\\ScheduledNotificationJobTest::testRecordsFireTimeOnCacheSet":0.001,"Unit\\BackgroundJob\\ScheduledWorkflowJobTest::testSkipsScheduleNotYetDue":0.001,"Unit\\BackgroundJob\\ScheduledWorkflowJobTest::testHandlesNoEngineFoundGracefully":0.003,"Unit\\Calendar\\CalendarEventTransformerTest::testAllDayEventTransformation":0.001,"Unit\\Calendar\\CalendarEventTransformerTest::testDateTimeEventTransformation":0,"Unit\\Calendar\\CalendarEventTransformerTest::testTitleTemplateInterpolation":0,"Unit\\Calendar\\CalendarEventTransformerTest::testTitleTemplateWithMissingFields":0,"Unit\\Calendar\\CalendarEventTransformerTest::testDescriptionTemplateInterpolation":0,"Unit\\Calendar\\CalendarEventTransformerTest::testLocationFieldMapping":0,"Unit\\Calendar\\CalendarEventTransformerTest::testStatusMappingWithConfiguredMapping":0,"Unit\\Calendar\\CalendarEventTransformerTest::testDefaultStatusWhenNoMappingConfigured":0,"Unit\\Calendar\\CalendarEventTransformerTest::testTranspIsAlwaysTransparent":0,"Unit\\Calendar\\CalendarEventTransformerTest::testUrlGeneration":0,"Unit\\Calendar\\CalendarEventTransformerTest::testCategoriesIncludeOpenRegisterAndSchemaName":0,"Unit\\Calendar\\CalendarEventTransformerTest::testUidStability":0,"Unit\\Calendar\\CalendarEventTransformerTest::testAutoDetectionOfAllDayFromPropertyFormat":0,"Unit\\Calendar\\CalendarEventTransformerTest::testExplicitAllDayOverride":0,"Unit\\Calendar\\CalendarEventTransformerTest::testReturnsNullWhenDtstartFieldEmpty":0,"Unit\\Calendar\\CalendarEventTransformerTest::testDefaultDtendForAllDayEvent":0,"Unit\\Calendar\\CalendarEventTransformerTest::testConfiguredDtendField":0,"Unit\\Calendar\\RegisterCalendarProviderTest::testGetCalendarsReturnsCalendarsForEnabledSchemas":0.001,"Unit\\Calendar\\RegisterCalendarProviderTest::testGetCalendarsWithUriFilterReturnsOnlyMatchingCalendars":0,"Unit\\Calendar\\RegisterCalendarProviderTest::testGetCalendarsReturnsEmptyWhenNoSchemasEnabled":0,"Unit\\Calendar\\RegisterCalendarProviderTest::testGetCalendarsReturnsEmptyForAnonymousPrincipal":0,"Unit\\Calendar\\RegisterCalendarProviderTest::testGetCalendarsHandlesExceptionGracefully":0,"Unit\\Calendar\\RegisterCalendarProviderTest::testGetCalendarsCachesEnabledSchemas":0,"Unit\\Calendar\\RegisterCalendarTest::testGetKeyReturnsSchemaBasedKey":0,"Unit\\Calendar\\RegisterCalendarTest::testGetUriReturnsSchemaBasedUri":0,"Unit\\Calendar\\RegisterCalendarTest::testGetDisplayNameReturnsConfiguredName":0,"Unit\\Calendar\\RegisterCalendarTest::testGetDisplayNameFallsBackToSchemaTitle":0,"Unit\\Calendar\\RegisterCalendarTest::testGetDisplayColorReturnsConfiguredColor":0,"Unit\\Calendar\\RegisterCalendarTest::testGetDisplayColorDefaultsWhenNotConfigured":0,"Unit\\Calendar\\RegisterCalendarTest::testGetPermissionsReturnsReadOnly":0,"Unit\\Calendar\\RegisterCalendarTest::testIsDeletedReturnsFalse":0,"Unit\\Calendar\\RegisterCalendarTest::testSearchReturnsEmptyForInvalidPrincipal":0,"Unit\\Calendar\\RegisterCalendarTest::testSearchReturnsTransformedEvents":0,"Unit\\Calendar\\RegisterCalendarTest::testSearchSkipsNullTransformResults":0,"Unit\\Calendar\\RegisterCalendarTest::testSearchFiltersEventsByPattern":0,"Unit\\Calendar\\RegisterCalendarTest::testSearchReturnsEmptyWhenNoRegistersContainSchema":0,"Unit\\Contacts\\ContactsMenuProviderTest::testProcessAddsActionsAndCountBadgeWhenMatchesFound":0.003,"Unit\\Contacts\\ContactsMenuProviderTest::testProcessAddsNoActionsWhenNoMatchesFound":0,"Unit\\Contacts\\ContactsMenuProviderTest::testProcessCatchesExceptionsAndLogs":0,"Unit\\Contacts\\ContactsMenuProviderTest::testProcessFallsBackToDefaultActionWhenNoDeepLink":0,"Unit\\Contacts\\ContactsMenuProviderTest::testProcessSkipsWhenNoEmailAndNoName":0,"Unit\\Controller\\AggregationControllerTest::testHeaderMissOnFreshComputation":0.001,"Unit\\Controller\\AggregationControllerTest::testHeaderHitOnCacheReplay":0,"Unit\\Controller\\AggregationControllerTest::testNoHeaderOn404":0,"Unit\\Controller\\ApprovalControllerTest::testIndexReturnsChains":0.001,"Unit\\Controller\\ApprovalControllerTest::testShowReturns404ForMissing":0.001,"Unit\\Controller\\ApprovalControllerTest::testApproveReturns403ForUnauthorised":0.005,"Unit\\Controller\\ApprovalControllerTest::testApproveReturns401WhenNotAuthenticated":0,"Unit\\Controller\\ApprovalControllerTest::testRejectReturns403ForUnauthorised":0.001,"Unit\\Controller\\ArchivalControllerTest::testUnauthenticatedReturns401":0.001,"Unit\\Controller\\ArchivalControllerTest::testRegularUserReturns403":0.001,"Unit\\Controller\\ArchivalControllerTest::testAdminIsAuthorized":0.001,"Unit\\Controller\\ArchivalControllerTest::testListDestructionListsOk":0.001,"Unit\\Controller\\ArchivalControllerTest::testListDestructionListsWithStatusFilter":0.001,"Unit\\Controller\\ArchivalControllerTest::testGetDestructionListOk":0.001,"Unit\\Controller\\ArchivalControllerTest::testGetDestructionListNotFound":0.001,"Unit\\Controller\\ArchivalControllerTest::testApproveDestructionListOk":0.001,"Unit\\Controller\\ArchivalControllerTest::testApproveDestructionListPartial":0.001,"Unit\\Controller\\ArchivalControllerTest::testApproveDestructionListDualApprovalConflict":0.003,"Unit\\Controller\\ArchivalControllerTest::testApproveDestructionListException":0.001,"Unit\\Controller\\ArchivalControllerTest::testRejectDestructionListOk":0.001,"Unit\\Controller\\ArchivalControllerTest::testRejectDestructionListMissingReason":0.005,"Unit\\Controller\\ArchivalControllerTest::testRejectDestructionListEmptyReason":0.001,"Unit\\Controller\\ArchivalControllerTest::testRejectDestructionListException":0.001,"Unit\\Controller\\ArchivalControllerTest::testCreateLegalHoldSingleObjectOk":0.001,"Unit\\Controller\\ArchivalControllerTest::testCreateLegalHoldBulkOk":0.001,"Unit\\Controller\\ArchivalControllerTest::testCreateLegalHoldBulkMissingRegisterId":0.001,"Unit\\Controller\\ArchivalControllerTest::testCreateLegalHoldMissingReason":0.001,"Unit\\Controller\\ArchivalControllerTest::testCreateLegalHoldMissingIds":0.003,"Unit\\Controller\\ArchivalControllerTest::testCreateLegalHoldException":0.001,"Unit\\Controller\\ArchivalControllerTest::testReleaseLegalHoldOk":0.001,"Unit\\Controller\\ArchivalControllerTest::testReleaseLegalHoldMissingReason":0.001,"Unit\\Controller\\ArchivalControllerTest::testReleaseLegalHoldException":0.001,"Unit\\Controller\\ArchivalControllerTest::testListLegalHoldsOk":0.001,"Unit\\Controller\\ArchivalControllerTest::testListCertificatesOk":0.001,"Unit\\Controller\\ContactsControllerTest::testMatchReturns200WithCorrectJsonStructure":0.001,"Unit\\Controller\\ContactsControllerTest::testMatchReturns400WhenNoEmailOrNameProvided":0,"Unit\\Controller\\ContactsControllerTest::testMatchReturns500OnInternalError":0,"Unit\\Controller\\ContactsControllerTest::testMatchWorksWithEmailOnly":0,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithAdminUser":0.001,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithExplicitOffset":0.001,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithUnderscoreOffset":0,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithSortParams":0,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithUnderscoreSortParams":0,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithSearchParam":0,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithUnderscoreSearchParam":0.001,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithCustomFilters":0,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithPageCalculatesOffset":0,"Unit\\Controller\\DeletedControllerGapTest::testIndexWithUnderscorePageParam":0,"Unit\\Controller\\DeletedControllerGapTest::testIndexPageCalculation":0,"Unit\\Controller\\DeletedControllerGapTest::testRestoreMultipleWithMixedObjects":0.001,"Unit\\Controller\\DeletedControllerGapTest::testDestroyMultipleWithMixedObjects":0.001,"Unit\\Controller\\DeletedControllerGapTest::testRestoreMultipleInnerException":0.001,"Unit\\Controller\\DeletedControllerGapTest::testDestroyMultipleInnerException":0.001,"Unit\\Controller\\DeletedControllerGapTest::testRestoreObjectWithEmptyDeletedArray":0,"Unit\\Controller\\DeletedControllerGapTest::testRestoreMultipleMessageWithNoNotFound":0.001,"Unit\\Controller\\DeletedControllerGapTest::testDestroyMultipleMessageWithNoNotFound":0.001,"Unit\\Controller\\EmailsControllerTest::testIndexReturnsEmailLinks":0.001,"Unit\\Controller\\EmailsControllerTest::testIndexReturns501WhenMailNotAvailable":0,"Unit\\Controller\\EmailsControllerTest::testIndexReturns404WhenObjectNotFound":0,"Unit\\Controller\\EmailsControllerTest::testCreateReturns201OnSuccess":0.002,"Unit\\Controller\\EmailsControllerTest::testCreateReturnsBadRequestWhenMissingFields":0.001,"Unit\\Controller\\EmailsControllerTest::testDestroyReturnsSuccess":0.001,"Unit\\Controller\\EmailsControllerTest::testSearchReturnsBadRequestWithoutSender":0,"Unit\\Controller\\EmailsControllerTest::testSearchReturnsResults":0,"Unit\\Controller\\EmailsControllerTest::testBySenderReturnsDiscoveredObjects":0,"Unit\\Controller\\EmailsControllerTest::testBySenderReturnsBadRequestWithoutSender":0,"Unit\\Controller\\EmailsControllerTest::testBySenderReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSidebarControllerTest::testGetObjectsForFileReturnsSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSidebarControllerTest::testGetObjectsForFileReturns500OnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSidebarControllerTest::testGetExtractionStatusReturnsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\FileSidebarControllerTest::testGetExtractionStatusReturns500OnException":0,"Unit\\Controller\\FilesControllerFileActionsTest::testRenameSuccess":0.002,"Unit\\Controller\\FilesControllerFileActionsTest::testRenameDuplicate":0.002,"Unit\\Controller\\FilesControllerFileActionsTest::testRenameInvalidChars":0.001,"Unit\\Controller\\FilesControllerFileActionsTest::testLockSuccess":0.003,"Unit\\Controller\\FilesControllerFileActionsTest::testLockConflict":0.001,"Unit\\Controller\\FilesControllerFileActionsTest::testBatchSuccess":0.006,"Unit\\Controller\\FilesControllerFileActionsTest::testBatchPartialFailure":0.001,"Unit\\Controller\\FilesControllerFileActionsTest::testBatchInvalidAction":0.001,"Unit\\Controller\\FilesControllerFileActionsTest::testUnlockNonOwner":0.002,"Unit\\Controller\\FilesControllerFileActionsTest::testPreviewUnsupported":0.002,"Unit\\Controller\\FilesControllerFileActionsTest::testPreviewAnonymousOnUnpublishedFileReturns403":0,"Unit\\Controller\\FilesControllerFileActionsTest::testPreviewAnonymousOnPublishedFileFallsThrough":0.001,"Unit\\Controller\\FilesControllerFileActionsTest::testPreviewAuthenticatedBypassesPublishedGate":0.001,"Unit\\Controller\\FilesControllerFileActionsTest::testCopyWithinSameRegister":0.011,"Unit\\Controller\\FilesControllerFileActionsTest::testCopyAcrossRegisters":0.002,"Unit\\Controller\\FilesControllerFileActionsTest::testCopyToNonexistentTarget":0.002,"Unit\\Controller\\FilesControllerFileActionsTest::testMoveWithSourceCleanup":0.002,"Unit\\Controller\\FilesControllerFileActionsTest::testMoveBlockedWhenSourceLocked":0.001,"Unit\\Controller\\FilesControllerFileActionsTest::testRestoreVersionResponseShape":0.007,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#string true":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#string TRUE":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#string 1":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#string yes":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#string on":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#bool true":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#string false":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#string 0":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#bool false":0,"Unit\\Controller\\FilesControllerTest::testExtractUploadedFilesShareParsing#empty string":0,"Unit\\Controller\\ObjectsControllerCoverageTest::testCreateWithMultipartNonStringValuesSkipsThem":0.004,"Unit\\Controller\\ObjectsControllerCoverageTest::testCreateWithMultipartJsonStringDecodesArray":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testContractsWithPageCalculatesOffset":0.005,"Unit\\Controller\\ObjectsControllerCoverageTest::testContractsWithUnderscorePageParam":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testContractsWithUnderscoreOffsetParam":0,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexWithMagicMappedSchemaUsesMagicMapper":0.01,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithExtendUsesRenderHandler":0.014,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithIgnoredFiltersAddsHint":0.002,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithFacetsIncludesFacetData":0.006,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithFacetErrorLogsFacetError":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithEmptyTruePreservesEmptyValues":0.006,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperPageToOffsetConversion":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithZeroLimitSetsPageToOne":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperLargeResultSetsGzipHeaders":0.007,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperFiltersSchemaRegisterFromExtend":0.002,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperWithExtendAsStringConvertsToArray":0.002,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperStripEmptySerializesObjectEntities":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsWithMagicMappedSchemaUsesMagicMapper":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsWithMagicMapperStripsEmptyValuesByDefault":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsWithMagicMapperEmptyTruePreservesValues":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexCrossTableSearchWithValidPairsReturnsResults":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexCrossTableSearchWithInvalidPairsReturns404":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexCrossTableSearchWithEmptyTruePreservesValues":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testUpdateWithNoUploadedFilesSendsNull":0.002,"Unit\\Controller\\ObjectsControllerCoverageTest::testPostPatchWithNoFilesPassesNullUploadedFiles":0.002,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexNormalPathWithArrayResultsStripsEmpty":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsNormalPathWithArrayResultsStripsEmpty":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testCreateContinuesWhenWebhookFails":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperContinuesWhenOrgServiceFails":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testShowExtendRegisterWithEntityPopulatesMap":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testShowExtendSchemaWithEntityPopulatesMap":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexNormalPathLargeResultsSetsGzipHeaders":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexMagicMapperIgnoredFiltersWithoutControlParams":0.002,"Unit\\Controller\\ObjectsControllerCoverageTest::testIndexCrossTableSearchWithPagination":0.001,"Unit\\Controller\\ObjectsControllerCoverageTest::testObjectsNormalPathEmptyTruePreservesValues":0,"Unit\\Controller\\ObjectsControllerTest::testClearBlobReturnsRetiredMessage":0,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns423WhenObjectLocked":0.002,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns500OnGenericExceptionFromFindSilent":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenObjectHasWrongSchemaOnly":0.002,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns400OnValidationExceptionAdminUser":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns400OnValidationExceptionAdminUser":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexReturnsSearchResults":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexReturnsPaginatedData":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithRegisterSchemaParamsResolvesToNormalSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithoutRegisterSchemaParams":0,"Unit\\Controller\\ObjectsControllerTest::testObjectsReturns404WhenRegisterNotFoundViaQueryParam":0,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenRegisterDoesNotMatchWithMessage":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenSchemaDoesNotMatchWithMessage":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateWithRegularUserReturns201":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateWithNoUserSessionReturns201OnSuccess":0.001,"Unit\\Controller\\ObjectsControllerTest::testDestroyWithRegularUserReturns204":0,"Unit\\Controller\\ObjectsControllerTest::testDestroyWithRegularUserReturns500WhenDeleteFails":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithEmptyTruePreservesNullValues":0.001,"Unit\\Controller\\ObjectsControllerTest::testVectorizeBatchWithViewsParam":0.001,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturns200WhenDeletionBlocked":0.001,"Unit\\Controller\\ObjectsControllerTest::testContractsWithPaginationParamsAndOffset":0.001,"Unit\\Controller\\ObjectsControllerTest::testUsesWithSearchParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testUsedWithSearchParams":0,"Unit\\Controller\\ObjectsControllerTest::testIndexReturnsPaginatedResults":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexReturnsEmptyResultsForNoObjects":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexStripsEmptyValuesFromResults":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexIncludesEmptyValuesWhenRequested":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowPreservesEmptyValuesWhenEmptyParamTrue":0.002,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendRegistersAndSchemas":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns423WhenObjectIsLocked":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns500OnUnexpectedFindSilentException":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns400OnCustomValidationException":0.004,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns400OnCustomValidationException":0.001,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns400OnValidationException":0.001,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns400OnCustomValidationException":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns400OnValidationException":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns400OnCustomValidationException":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNames":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithFieldsParameter":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithFilterAndUnsetParameters":0.01,"Unit\\Controller\\ObjectsControllerTest::testShowAsRegularUserEnablesRbac":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateAsRegularUser":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateFiltersReservedParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateWithFiltersReservedParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithFilterParams":0,"Unit\\Controller\\ObjectsControllerTest::testObjectsThrowsWhenSearchFails":0,"Unit\\Controller\\ObjectsControllerTest::testContractsWithPaginationParams":0,"Unit\\Controller\\ObjectsControllerTest::testUsesWithPaginationParams":0,"Unit\\Controller\\ObjectsControllerTest::testUsedWithPaginationParams":0,"Unit\\Controller\\ObjectsControllerTest::testDestroyAsRegularUserSucceeds":0.001,"Unit\\Controller\\ObjectsControllerTest::testMergeReturnsFullMergeResult":0.006,"Unit\\Controller\\ObjectsControllerTest::testVectorizeBatchWithViewsAndBatchSize":0.001,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationStatsWithViewsParam":0,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationCountWithViews":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsThrowsWhenRegisterNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testLogsThrowsWhenSchemaNotFound":0,"Unit\\Controller\\ObjectsControllerTest::testCreateWithMultipartFormDataNormalizesJson":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateWithMultipartFormData":0.002,"Unit\\Controller\\ObjectsControllerTest::testPatchAsRegularUser":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchAsRegularUser":0.001,"Unit\\Controller\\ObjectsControllerTest::testValidateReturnsResultWithErrors":0,"Unit\\Controller\\ObjectsControllerTest::testIndexAddsGzipHeaderForLargeResults":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowNormalizesLegacyExtendParameters":0.001,"Unit\\Controller\\ObjectsControllerTest::testDestroyAsAdminBypassesRbac":0,"Unit\\Controller\\ObjectsControllerTest::testMigrateWithAllValidParams":0,"Unit\\Controller\\ObjectsControllerTest::testLockReturns423WhenAlreadyLocked":0.001,"Unit\\Controller\\ObjectsControllerTest::testCanDeleteReturnsAnalysisWithBlockers":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowReturnsObjectWithoutAtSelf":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns404WhenObjectInWrongSchema":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateContinuesWhenWebhookFails":0.006,"Unit\\Controller\\ObjectsControllerTest::testValidateReturnsPaginationDetailsWhenLimitAndOffset":0.001,"Unit\\Controller\\ObjectsControllerTest::testDestroyWithNoUserSessionIsNotAdmin":0.003,"Unit\\Controller\\ObjectsControllerTest::testPatchUnlockErrorIsSilentlyIgnored":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchUnlockErrorIsSilentlyIgnored":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexStripsEmptyValuesFromNestedSequentialArrays":0.001,"Unit\\Controller\\ObjectsControllerTest::testContractsWithLegacyOffsetAndPageParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testContractsCalculatesPageFromOffset":0.001,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAddsNextLink":0.001,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAddsPrevLink":0.001,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAutoCorrectsTotalWhenLessThanResults":0.001,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAddsNextPageToUrlWithoutPageParam":0.001,"Unit\\Controller\\ObjectsControllerTest::testContractsPaginateAddsNextPageToUrlWithNoQueryString":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendRegistersIncludesRegisterData":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendSchemasIncludesSchemaData":0.002,"Unit\\Controller\\ObjectsControllerTest::testCreateUsesWebhookInterceptedData":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateWithoutWebhookService":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateUnlockErrorIsSilentlyIgnored":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns500WhenContainerGetThrowsForLockedCheck":0.001,"Unit\\Controller\\ObjectsControllerTest::testUpdateAsRegularUser":0.003,"Unit\\Controller\\ObjectsControllerTest::testObjectsReturns404WhenRegisterNotFound":0.002,"Unit\\Controller\\ObjectsControllerTest::testObjectsReturns404WhenSchemaNotFound":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithUnderscorePrefixedRegisterAndSchema":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsStripsEmptyValuesFromResults":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsPreservesEmptyValuesWhenRequested":0,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithSchemaParamOnly":0,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithRegisterParamOnly":0,"Unit\\Controller\\ObjectsControllerTest::testIndexHandlesObjectEntityResultsInEmptyStripping":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsMatchesSchemaBySlug":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenSchemaDoesNotMatch":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404WhenRegisterDoesNotMatch":0.003,"Unit\\Controller\\ObjectsControllerTest::testLogsMatchesSchemaAsObject":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsMatchesSchemaAsObjectById":0.07,"Unit\\Controller\\ObjectsControllerTest::testLogsMatchesSchemaAsArrayById":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsWithPaginationParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testGetObjectVectorizationCountWithSchemasJsonString":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexWithEmptyTruePreservesEmptyStrings":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateMultipartWithNonJsonStrings":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateJsonContentTypeSkipsNormalization":0.002,"Unit\\Controller\\ObjectsControllerTest::testPostPatchWithMultipartFormData":0.001,"Unit\\Controller\\ObjectsControllerTest::testPostPatchFiltersIdFromParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateAllowsAtSelfParam":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesEmptyCacheHandler":0.002,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesCollectsUuidsFromRelations":0.002,"Unit\\Controller\\ObjectsControllerTest::testIndexWithRbacDisabled":0.003,"Unit\\Controller\\ObjectsControllerTest::testIndexWithPublishedAndDeletedParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testImportWithAllBooleanParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testPatchWithMultipartFormData":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendRegisterAndSchemaEntities":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendArrayContainingNonStringValues":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendAsIntegerReturnsOk":0.002,"Unit\\Controller\\ObjectsControllerTest::testCreateFiltersAtPrefixedKeysExceptAtSelf":0.002,"Unit\\Controller\\ObjectsControllerTest::testContractsWithNonUnderscoreOffsetParam":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsReturns404OnDbException":0.001,"Unit\\Controller\\ObjectsControllerTest::testLogsWithLegacyParams":0.001,"Unit\\Controller\\ObjectsControllerTest::testExportReturnsCsvDownloadResponse":0.001,"Unit\\Controller\\ObjectsControllerTest::testExportReturnsExcelDownloadResponseByDefault":0.221,"Unit\\Controller\\ObjectsControllerTest::testExportUsesDefaultSlugsWhenNull":0.001,"Unit\\Controller\\ObjectsControllerTest::testExportCsvViaTypeParam":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexWithCommaSeparatedSchemasTriggersCrossTableSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexWithArraySchemasTriggersCrossTableSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexWithMultipleRegistersTriggersCrossTableSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithMultipleSchemasTriggersCrossTableSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithMultipleRegistersTriggersCrossTableSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithUnderscorePrefixedMultiSchemas":0,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesCollectsNestedUuids":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesSkipsMetadataKeys":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesIgnoresNonUuidRelations":0.001,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturnsZipOnSuccess":0.003,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturns404WhenObjectDoesNotExistViaException":0,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesUsesCustomFilename":0.001,"Unit\\Controller\\ObjectsControllerTest::testDownloadFilesReturns500WhenZipCreationFails":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowStripEmptyValuesPreservesZeroAndFalse":0.002,"Unit\\Controller\\ObjectsControllerTest::testShowStripsNestedAssociativeArraysWithAllEmptyValues":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowStripsSequentialArraysPreservingNonEmptyItems":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithEmptyTruePreservesNestedEmptyValues":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendIncludesExtendedObjects":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithFieldsAsCommaSeparatedString":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithUnsetAsString":0.002,"Unit\\Controller\\ObjectsControllerTest::testShowWithFilterAsString":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowReturns404WhenFindReturnsNull":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithoutAtSelfKeyDoesNotAddExtendData":0.001,"Unit\\Controller\\ObjectsControllerTest::testCreateReturns403OnGenericExceptionIncludingDBException":0.001,"Unit\\Controller\\ObjectsControllerTest::testDestroyReturns403OnGenericExceptionIncludingDBException":0,"Unit\\Controller\\ObjectsControllerTest::testUpdateReturns403OnGenericExceptionWithUnlock":0.001,"Unit\\Controller\\ObjectsControllerTest::testPatchReturns500OnGenericExceptionWithUnlock":0.006,"Unit\\Controller\\ObjectsControllerTest::testPostPatchReturns500OnGenericExceptionWithUnlock":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithRegistersArrayOverridesRegisterParam":0.002,"Unit\\Controller\\ObjectsControllerTest::testIndexWithDuplicateSchemasDoesNotTriggerCrossTable":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexWithSingleSchemaParamUsesNormalSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexWithEmptySchemasParamUsesNormalSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testObjectsWithSingleSchemaArrayUsesNormalSearch":0.001,"Unit\\Controller\\ObjectsControllerTest::testIndexWithBothMultipleSchemasAndRegisters":0,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendAsCommaSeparatedStringParameterWorks":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesNestedRelationArrays":0.002,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendNamesUsesAtSelfObjectForUuids":0.001,"Unit\\Controller\\ObjectsControllerTest::testShowWithExtendRegistersNullEntityCreatesEmptyRegisters":0.001,"Unit\\Controller\\RegistersControllerTest::testIndexWithOffsetParam":0,"Unit\\Controller\\RegistersControllerTest::testIndexWithFiltersParam":0,"Unit\\Controller\\RegistersControllerTest::testIndexWithExtendAsString":0,"Unit\\Controller\\RegistersControllerTest::testIndexWithExtendSchemasExpandsSchemaIds":0.001,"Unit\\Controller\\RegistersControllerTest::testIndexWithExtendSchemasSkipsMissingSchema":0,"Unit\\Controller\\RegistersControllerTest::testIndexWithSelfStatsExtend":0.001,"Unit\\Controller\\RegistersControllerTest::testIndexWithSchemasAndSelfStatsExtend":0.001,"Unit\\Controller\\RegistersControllerTest::testIndexWithSchemasAndStatsNoCountForSchema":0.001,"Unit\\Controller\\RegistersControllerTest::testIndexPageConvertsToOffset":0,"Unit\\Controller\\RegistersControllerTest::testShowWithSelfStatsExtend":0.001,"Unit\\Controller\\RegistersControllerTest::testShowWithExtendAsArray":0.001,"Unit\\Controller\\RegistersControllerTest::testShowWithoutStatsExtend":0,"Unit\\Controller\\RegistersControllerTest::testCreateReturns409OnDatabaseConstraintException":0,"Unit\\Controller\\RegistersControllerTest::testUpdateReturns409OnDatabaseConstraintException":0.001,"Unit\\Controller\\RegistersControllerTest::testExportConfigurationWithIncludeObjects":0.001,"Unit\\Controller\\RegistersControllerTest::testExportCsvWithSchemaSuccess":0.001,"Unit\\Controller\\RegistersControllerTest::testExportExcelSuccess":0.022,"Unit\\Controller\\RegistersControllerTest::testImportExcelSuccess":0.001,"Unit\\Controller\\RegistersControllerTest::testImportCsvMissingSchemaReturns400":0.001,"Unit\\Controller\\RegistersControllerTest::testImportCsvWithSchemaSuccess":0.001,"Unit\\Controller\\RegistersControllerTest::testImportAutoDetectsExcelFromExtension":0,"Unit\\Controller\\RegistersControllerTest::testImportAutoDetectsCsvFromExtension":0,"Unit\\Controller\\RegistersControllerTest::testImportAutoDetectsConfigurationFromJsonExtension":0.001,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubUsesDefaultPathWhenEmpty":0.011,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubNonDefaultBranchMessage":0.001,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubGetRepositoryInfoFails":0.001,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubGetFileShaFails":0.001,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubStripsLeadingSlashFromPath":0.001,"Unit\\Controller\\RegistersControllerTest::testCreateRemovesUnderscorePrefixedParams":0,"Unit\\Controller\\RegistersControllerTest::testUpdateRemovesUnderscorePrefixedParams":0,"Unit\\Controller\\RegistersControllerTest::testImportConfigurationWithObjectsInResult":0.001,"Unit\\Controller\\RegistersControllerTest::testImportConfigurationNoRegistersInResultMergesSchemas":0.007,"Unit\\Controller\\RegistersControllerTest::testImportConfigurationGetUploadedJsonReturnsJsonResponse":0.001,"Unit\\Controller\\RegistersControllerTest::testPublishToGitHubCustomCommitMessage":0.001,"Unit\\Controller\\RegistersControllerTest::testSchemasWithStringId":0,"Unit\\Controller\\RegistersControllerTest::testDestroyCallsDeleteOnService":0,"Unit\\Controller\\RegistersControllerTest::testIndexExtendSchemasWithNullSchemasField":0,"Unit\\Controller\\RegistersControllerTest::testExportConfigurationJsonEncodeFails":0,"Unit\\Controller\\RelationsControllerTest::testIndexReturnsAllRelationTypes":0.002,"Unit\\Controller\\RelationsControllerTest::testIndexOmitsMailWhenNotAvailable":0.001,"Unit\\Controller\\RelationsControllerTest::testIndexFiltersTypes":0.001,"Unit\\Controller\\RelationsControllerTest::testIndexReturns404WhenObjectNotFound":0,"Unit\\Controller\\ScheduledWorkflowControllerTest::testIndexReturnsAllWorkflows":0.001,"Unit\\Controller\\ScheduledWorkflowControllerTest::testShowReturns404ForMissing":0,"Unit\\Controller\\ScheduledWorkflowControllerTest::testCreateReturns201":0,"Unit\\Controller\\SchemasControllerTest::testIndexWithPageBasedPagination":0,"Unit\\Controller\\SchemasControllerTest::testIndexWithExtendStats":0.001,"Unit\\Controller\\SchemasControllerTest::testIndexWithExtendStatsDefaultsForMissingSchema":0.001,"Unit\\Controller\\SchemasControllerTest::testIndexWithFilters":0,"Unit\\Controller\\SchemasControllerTest::testIndexExtendedByPopulated":0,"Unit\\Controller\\SchemasControllerTest::testShowReturns404OnDoesNotExistException":0,"Unit\\Controller\\SchemasControllerTest::testShowReturns404OnValidationException":0,"Unit\\Controller\\SchemasControllerTest::testShowReturns500OnGenericException":0,"Unit\\Controller\\SchemasControllerTest::testShowWithExtendStats":0.001,"Unit\\Controller\\SchemasControllerTest::testShowWithAllOfAddsPropertyMetadata":0,"Unit\\Controller\\SchemasControllerTest::testShowWithExtendAsString":0.001,"Unit\\Controller\\SchemasControllerTest::testCreateReturnsErrorOnDBException":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturnsErrorOnDatabaseConstraintException":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnValidationError":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnMustBeError":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnRequiredError":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnFormatError":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnPropertyAtError":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturns400OnAuthorizationError":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturns409OnConstraintError":0,"Unit\\Controller\\SchemasControllerTest::testCreateReturns409OnDuplicateError":0,"Unit\\Controller\\SchemasControllerTest::testUpdateInvalidatesCaches":0,"Unit\\Controller\\SchemasControllerTest::testUpdateReturnsErrorOnDBException":0,"Unit\\Controller\\SchemasControllerTest::testUpdateReturnsErrorOnDatabaseConstraintException":0,"Unit\\Controller\\SchemasControllerTest::testUpdateReturns400OnValidationError":0,"Unit\\Controller\\SchemasControllerTest::testUpdateReturns409OnConstraintError":0.001,"Unit\\Controller\\SchemasControllerTest::testUpdateRemovesUnderscoreParams":0,"Unit\\Controller\\SchemasControllerTest::testDestroyInvalidatesCaches":0,"Unit\\Controller\\SchemasControllerTest::testDestroyReturns409OnValidationException":0,"Unit\\Controller\\SchemasControllerTest::testStatsReturns500OnGenericException":0,"Unit\\Controller\\SchemasControllerTest::testPublishWithCustomDate":0,"Unit\\Controller\\SchemasControllerTest::testPublishReturns400OnGenericException":0,"Unit\\Controller\\SchemasControllerTest::testPublishInvalidatesCaches":0,"Unit\\Controller\\SchemasControllerTest::testDepublishWithCustomDate":0,"Unit\\Controller\\SchemasControllerTest::testDepublishReturns400OnGenericException":0,"Unit\\Controller\\SchemasControllerTest::testDepublishInvalidatesCaches":0,"Unit\\Controller\\SchemasControllerTest::testUploadUpdateDelegatesToUpload":0.001,"Unit\\Controller\\SchemasControllerTest::testUploadNewSchemaWithoutId":0,"Unit\\Controller\\SchemasControllerTest::testUploadReturnsErrorResponseFromUploadService":0,"Unit\\Controller\\SchemasControllerTest::testUploadNewSchemaWithEmptyTitle":0.001,"Unit\\Controller\\SchemasControllerTest::testUploadExistingSchemaById":0,"Unit\\Controller\\SchemasControllerTest::testUploadReturns500OnGenericException":0,"Unit\\Controller\\SchemasControllerTest::testUploadReturns400OnValidationException":0,"Unit\\Controller\\SchemasControllerTest::testUploadReturns409OnConstraintException":0,"Unit\\Controller\\SchemasControllerTest::testUploadReturnsErrorOnDBException":0,"Unit\\Controller\\SchemasControllerTest::testUploadReturnsErrorOnDatabaseConstraintException":0,"Unit\\Controller\\SchemasControllerTest::testUploadNewSchemaWithOrganisationAlreadySet":0.001,"Unit\\Controller\\SchemasControllerTest::testRelatedWithOutgoingReferences":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\TasksControllerTest::testIndexReturnsEmptyWhenNoVtodoCalendar":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testChangePasswordNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testChangePasswordSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testChangePasswordIncorrectCurrent":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testChangePasswordRateLimited":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testGetNotificationPreferencesNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testGetNotificationPreferencesSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testUpdateNotificationPreferencesSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testUpdateNotificationPreferencesInvalidDigest":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testGetActivityNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testGetActivitySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testListTokensNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testListTokensSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testCreateTokenSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testCreateTokenMissingName":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testRevokeTokenSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testRevokeTokenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testRequestDeactivationNotAuthenticated":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testRequestDeactivationSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testGetDeactivationStatusSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testCancelDeactivationSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\UserControllerTest::testCancelDeactivationNoPending":0,"Unit\\Controller\\WorkflowExecutionControllerTest::testIndexReturnsResultsWithPagination":0,"Unit\\Controller\\WorkflowExecutionControllerTest::testShowReturnsExecution":0,"Unit\\Controller\\WorkflowExecutionControllerTest::testShowReturns404ForMissing":0,"Unit\\Controller\\WorkflowExecutionControllerTest::testDestroyDeletesRecord":0,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testFlagsGetSystemConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testFlagsGetDatabaseConnection":0,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testFlagsGetLogger":0,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testIgnoresPsr11Get":0,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testIgnoresThisPropertyAccess":0,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testIgnoresDocblockExample":0,"OCA\\OpenRegister\\Tests\\Unit\\CustomSniffs\\NoLegacyServerAccessorsSniffTest::testIgnoresStringLiteral":0,"Unit\\Db\\ActionLogTest::testConstructorRegistersFieldTypes":0.006,"Unit\\Db\\ActionLogTest::testConstructorDefaultValues":0.001,"Unit\\Db\\ActionLogTest::testJsonSerialize":0,"Unit\\Db\\ActionLogTest::testGetRequestPayloadArrayNull":0,"Unit\\Db\\ActionLogTest::testGetRequestPayloadArrayValid":0,"Unit\\Db\\ActionLogTest::testGetResponsePayloadArrayNull":0,"Unit\\Db\\ActionLogTest::testGetResponsePayloadArrayValid":0,"Unit\\Db\\ActionTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\ActionTest::testConstructorDefaultValues":0,"Unit\\Db\\ActionTest::testJsonSerializeReturnsAllFields":0,"Unit\\Db\\ActionTest::testMatchesEventExactMatch":0,"Unit\\Db\\ActionTest::testMatchesEventWildcardMatch":0,"Unit\\Db\\ActionTest::testMatchesEventJsonArrayMatch":0,"Unit\\Db\\ActionTest::testMatchesSchemaEmptyMatchesAll":0,"Unit\\Db\\ActionTest::testMatchesSchemaSpecificBinding":0,"Unit\\Db\\ActionTest::testMatchesRegisterEmptyMatchesAll":0,"Unit\\Db\\ActionTest::testMatchesRegisterSpecificBinding":0,"Unit\\Db\\ActionTest::testHydrate":0,"Unit\\Db\\ActionTest::testGetEventTypeArraySingleString":0,"Unit\\Db\\ActionTest::testGetEventTypeArrayJsonArray":0,"Unit\\Db\\ActionTest::testGetFilterConditionArrayNull":0,"Unit\\Db\\ActionTest::testGetConfigurationArrayNull":0,"Unit\\Db\\ApprovalChainTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\ApprovalChainTest::testDefaultValues":0,"Unit\\Db\\ApprovalChainTest::testHydrateEncodesStepsArray":0.001,"Unit\\Db\\ApprovalChainTest::testJsonSerializeReturnsStepsAsArray":0,"Unit\\Db\\ApprovalChainTest::testGetStepsArrayReturnsEmptyForNull":0,"Unit\\Db\\ApprovalStepTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\ApprovalStepTest::testDefaultValues":0,"Unit\\Db\\ApprovalStepTest::testHydrate":0,"Unit\\Db\\ApprovalStepTest::testJsonSerialize":0,"Unit\\Db\\AuditTrailTest::testSetAndGetHashFields":0,"Unit\\Db\\AuditTrailTest::testHashFieldsDefaultToNull":0,"Unit\\Db\\AuditTrailTest::testJsonSerializeIncludesHashFields":0,"Unit\\Db\\AuditTrailTest::testJsonSerializeHashFieldsNullWhenNotSet":0,"Unit\\Db\\ContactLinkTest::testJsonSerializeReturnsAllFields":0.005,"Unit\\Db\\ContactLinkTest::testJsonSerializeHandlesNulls":0,"Unit\\Db\\ContactLinkTest::testSettersAndGetters":0,"Unit\\Db\\DeckLinkTest::testJsonSerializeReturnsAllFields":0,"Unit\\Db\\DeckLinkTest::testJsonSerializeHandlesNulls":0,"Unit\\Db\\DeckLinkTest::testSettersAndGetters":0,"Unit\\Db\\DestructionListTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\DestructionListTest::testConstructorDefaultValues":0,"Unit\\Db\\DestructionListTest::testGettersAndSetters":0,"Unit\\Db\\DestructionListTest::testJsonSerialize":0,"Unit\\Db\\DestructionListTest::testHydrate":0,"Unit\\Db\\DestructionListTest::testStatusConstants":0,"Unit\\Db\\EmailLinkTest::testJsonSerializeReturnsAllFields":0,"Unit\\Db\\EmailLinkTest::testJsonSerializeHandlesNulls":0,"Unit\\Db\\EmailLinkTest::testSettersAndGetters":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\FileMapperGetFileIdsForObjectsTest::testEmptyInputReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\FileMapperGetFileIdsForObjectsTest::testInputOfOnlyInvalidUuidsReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testPostgresYearUsesToCharWithYYYY":0.027,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testPostgresMonthUsesToCharWithYYYYMM":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testPostgresWeekUsesIsoToChar":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testPostgresQuarterUsesToChar":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testPostgresDayUsesToCharWithYYYYMMDD":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testMariadbYearUsesDateFormatWithPercentY":0.012,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testMariadbMonthUsesDateFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testMariadbDayUsesDateFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testMariadbWeekUsesIsoDateFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testMariadbQuarterUsesConcat":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testMariadbUnknownIntervalFallsBackToMonth":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testQualifiedFieldPassesThroughOnPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testQualifiedFieldPassesThroughOnMariadb":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testQualifiedFieldInQuarterConcatOnMariadb":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testStringWithDateFormatReturnDateHistogram":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testStringWithDateTimeFormatReturnsDateHistogram":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testPlainStringDefaultsToTerms":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testExplicitFacetableTypeOverrideIsRespected":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testWeekBoundsUseIsoWeekNotMonth":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testWeekBoundsForFirstIsoWeekOfYear":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testWeekBoundsForIsoWeek52OfPreviousYearAtBoundary":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicFacetHandlerTest::testWeekBoundsRejectMalformedKey":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testAdminBypassShortCircuitsBeforeConditionMatcher":0.002,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testOwnerBypassShortCircuitsBeforeConditionMatcher":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testNoAuthorizationGrantsOpenAccess":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testActionNotConfiguredGrantsOpenAccess":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testSimplePublicRuleGrantsAnonymousAccessWithoutDelegation":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testSimpleGroupRuleGrantsMemberWithoutDelegation":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testSimpleGroupRuleDeniesNonMemberWithoutDelegation":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testConditionalPublicRuleWithMatchDelegatesToConditionMatcher":0.001,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testConditionalRuleReturnsFalseWhenConditionMatcherReturnsFalse":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testConditionalRuleSkippedWhenUserDoesNotQualifyForGroup":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testConditionalRuleGrantsAccessWhenNoObjectDataSupplied":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testConditionalRuleWithEmptyMatchActsAsPlainGroupMatch":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testInOperatorDelegation":0,"Unit\\Db\\MagicMapper\\MagicRbacHandlerTest::testUserIdVariableDelegation":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testGteProducesGreaterThanOrEqualCondition":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testLteProducesLessThanOrEqualCondition":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testGteAndLteTogetherProduceTwoRangeConditions":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testGtProducesStrictGreaterThanCondition":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testLtProducesStrictLessThanCondition":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testInOperatorKeyProducesInClause":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testInOperatorKeyWithSingleStringValueProducesInClause":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testPlainArrayValueStillProducesInClause":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testScalarValueProducesEqualityCondition":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testUnknownPropertyProducesImpossibleCondition":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testBuildSearchConditionSqlQuotesReservedWordOnPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testBuildSearchConditionSqlQuotesReservedWordOnMySql":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicSearchHandlerTest::testBuildSearchConditionSqlQuotesEveryStringPropertyOnPostgres":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicStatisticsHandlerTest::testEmptyStringDateTimePropertyRendersAsNull":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicStatisticsHandlerTest::testEmptyStringDatePropertyRendersAsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicStatisticsHandlerTest::testValidDateTimePropertyRendersAsIso8601":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicStatisticsHandlerTest::testWhitespaceOnlyDateTimePropertyRendersAsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicTableHandlerTest::testNoSyncWhenTableExistsAndColumnsMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicTableHandlerTest::testSyncCalledWhenColumnMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\MagicTableHandlerTest::testFastPathSkipsInfoSchemaQueryWhenVerified":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\NotificationHistoryTest::testAllFieldsRoundTrip":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\NotificationHistoryTest::testJsonSerializeReturnsDocumentedShape":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\NotificationHistoryTest::testJsonSerializeWithNullDispatchedAt":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTest::testJsonSerializeOrganisationLivesInSelfBlock":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTmloTest::testTmloGetterDefaultsToEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTmloTest::testSetAndGetTmlo":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTmloTest::testTmloInGetObjectArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTmloTest::testTmloInJsonSerialize":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTmloTest::testHydrateSetsTmlo":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTmloTest::testSetTmloNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\ObjectEntityTmloTest::testFullTmloFieldSet":0,"Unit\\Db\\ScheduledWorkflowTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\ScheduledWorkflowTest::testDefaultValues":0,"Unit\\Db\\ScheduledWorkflowTest::testHydrate":0,"Unit\\Db\\ScheduledWorkflowTest::testJsonSerializeDecodesPayload":0,"Unit\\Db\\SelectionListTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\SelectionListTest::testConstructorDefaultValues":0,"Unit\\Db\\SelectionListTest::testGettersAndSetters":0,"Unit\\Db\\SelectionListTest::testJsonSerialize":0,"Unit\\Db\\SelectionListTest::testHydrate":0,"Unit\\Db\\SelectionListTest::testValidActionsConstant":0,"Unit\\Db\\WorkflowExecutionTest::testConstructorRegistersFieldTypes":0,"Unit\\Db\\WorkflowExecutionTest::testDefaultValues":0,"Unit\\Db\\WorkflowExecutionTest::testHydrate":0,"Unit\\Db\\WorkflowExecutionTest::testJsonSerializeDecodesJsonFields":0,"Unit\\Db\\WorkflowExecutionTest::testJsonSerializeNullJsonFieldsReturnNull":0,"Unit\\Dto\\DeepLinkRegistrationTest::testContactEmailPlaceholderIsUrlEncoded":0.001,"Unit\\Dto\\DeepLinkRegistrationTest::testContactNamePlaceholderIsUrlEncoded":0,"Unit\\Dto\\DeepLinkRegistrationTest::testEntityIdPlaceholderIsReplacedWithUuid":0,"Unit\\Dto\\DeepLinkRegistrationTest::testContactIdPlaceholderIsUrlEncoded":0,"Unit\\Dto\\DeepLinkRegistrationTest::testBothObjectAndContactPlaceholdersCoexist":0,"Unit\\Dto\\DeepLinkRegistrationTest::testMissingContactContextLeavesPlaceholdersAsIs":0,"Unit\\Dto\\DeepLinkRegistrationTest::testOriginalObjectPlaceholdersStillWork":0,"Unit\\Event\\ReferenceValidationEventsTest::testValidatedEventExtendsEvent":0.001,"Unit\\Event\\ReferenceValidationEventsTest::testValidatedEventExposesAllFields":0,"Unit\\Event\\ReferenceValidationEventsTest::testValidatedEventDefaultsTargetRegisterToNull":0,"Unit\\Event\\ReferenceValidationEventsTest::testFailedEventExtendsEvent":0,"Unit\\Event\\ReferenceValidationEventsTest::testFailedEventExposesAllFields":0,"Unit\\Event\\ReferenceValidationEventsTest::testFailedEventDefaultsTargetRegisterToNull":0,"Unit\\Event\\ReferenceValidationEventsTest::testValidatedAndFailedEventsAreDistinctTypes":0,"Unit\\Integration\\NotifyPushEndToEndTest::testCreateObjectQueuesNotifyCustomEvent":0,"Unit\\Listener\\ActionListenerTest::testHandleSkipsWhenPropagationStopped":0,"Unit\\Listener\\ActionListenerTest::testHandleSkipsWhenNoMatchingActions":0,"Unit\\Listener\\ActionListenerTest::testHandleDelegatesMatchingActionsToExecutor":0,"Unit\\Listener\\ActionListenerTest::testHandleCatchesExceptionsGracefully":0,"Unit\\Listener\\ActionListenerTest::testHandleFiltersOutActionsByFilterCondition":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleObjectCreatedEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleObjectUpdatedEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleObjectDeletedEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleRegisterCreatedEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleRegisterUpdatedEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleRegisterDeletedEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleSchemaCreatedEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleSchemaDeletedEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Listener\\ActivityEventListenerTest::testHandleUnknownEventIsIgnored":0,"Unit\\Listener\\AggregationCacheInvalidationListenerTest::testCreatedEventEvictsByRegisterAndSchemaSlug":0,"Unit\\Listener\\AggregationCacheInvalidationListenerTest::testUpdatedEventEvicts":0,"Unit\\Listener\\AggregationCacheInvalidationListenerTest::testDeletedEventEvicts":0,"Unit\\Listener\\AggregationCacheInvalidationListenerTest::testTransitionedEventEvicts":0,"Unit\\Listener\\AggregationCacheInvalidationListenerTest::testUnrelatedEventIsIgnored":0,"Unit\\Listener\\AggregationThresholdListenerTest::testDispatchesOnTransitionBelowToAbove":0.001,"Unit\\Listener\\AggregationThresholdListenerTest::testDoesNotRefireWhenStillAbove":0,"Unit\\Listener\\AggregationThresholdListenerTest::testDoesNotFireBelowThreshold":0,"Unit\\Listener\\AggregationThresholdListenerTest::testFiresAgainAfterDip":0,"Unit\\Listener\\AggregationThresholdListenerTest::testIgnoresNotificationsWithDifferentTriggerType":0,"Unit\\Listener\\FileChangeListenerTest::testHandleIgnoresNonFileEvents":0,"Unit\\Listener\\FileChangeListenerTest::testHandleIgnoresFolderNodes":0.001,"Unit\\Listener\\FileChangeListenerTest::testHandleSkipsWhenScopeIsNone":0,"Unit\\Listener\\FileChangeListenerTest::testHandleSkipsAnonymizedFiles":0,"Unit\\Listener\\FileChangeListenerTest::testHandleQueuesBackgroundJobForOpenRegisterFile":0,"Unit\\Listener\\FileChangeListenerTest::testHandleImmediateModeSynchronousExtraction":0,"Unit\\Listener\\FileChangeListenerTest::testHandleScopeObjectsSkipsNonOpenRegisterFiles":0,"Unit\\Listener\\GraphQLSubscriptionListenerTest::testHandleObjectCreatedEvent":0,"Unit\\Listener\\GraphQLSubscriptionListenerTest::testHandleObjectUpdatedEvent":0,"Unit\\Listener\\GraphQLSubscriptionListenerTest::testHandleObjectDeletedEvent":0,"Unit\\Listener\\GraphQLSubscriptionListenerTest::testHandleNonObjectEventIgnored":0,"Unit\\Listener\\GraphQLSubscriptionListenerTest::testHandleExceptionLogsWarning":0,"Unit\\Listener\\MailAppScriptListenerTest::testIgnoresNonMailEvents":0,"Unit\\Listener\\MailAppScriptListenerTest::testIgnoresWhenNoUserIsLoggedIn":0.002,"Unit\\Listener\\MailAppScriptListenerTest::testIgnoresWhenMailAppNotEnabled":0,"Unit\\Listener\\MailAppScriptListenerTest::testIgnoresWhenUserHasNoRegisters":0,"Unit\\Listener\\NotifyPushListenerTest::testSoftFailWhenQueueNotResolvable":0.104,"Unit\\Listener\\NotifyPushListenerTest::testEmitsObjectEventOnUpdate":0.008,"Unit\\Listener\\NotifyPushListenerTest::testEmitsCollectionEventOnCreate":0.001,"Unit\\Listener\\NotifyPushListenerTest::testEmitsCollectionEventOnDelete":0.001,"Unit\\Listener\\NotifyPushListenerTest::testCollectionEventUsesSlugsNotIds":0.008,"Unit\\Listener\\NotifyPushListenerTest::testDedupPreventsDoubleEmit":0.006,"Unit\\Listener\\NotifyPushListenerTest::testBatchModeSuppressesPerObjectPush":0.001,"Unit\\Listener\\NotifyPushListenerTest::testFlushBatchEmitsOneCollectionEvent":0.001,"Unit\\Listener\\NotifyPushListenerTest::testPushAvailableFlagSetOnFirstSuccess":0.001,"Unit\\Listener\\ObjectCleanupListenerTest::testHandleCallsAllCleanupMethods":0,"Unit\\Listener\\ObjectCleanupListenerTest::testHandleIgnoresNonObjectDeletedEvents":0,"Unit\\Listener\\ObjectCleanupListenerTest::testHandleContinuesWhenOneCleanupFails":0,"Unit\\Middleware\\TenantQuotaMiddlewareTest::testSkipsForUnauthenticatedRequests":0,"Unit\\Middleware\\TenantQuotaMiddlewareTest::testSkipsWhenNoActiveOrganisation":0,"Unit\\Middleware\\TenantQuotaMiddlewareTest::testBlocksSuspendedOrganisation":0.001,"Unit\\Middleware\\TenantQuotaMiddlewareTest::testBlocksDeprovisioningOrganisation":0,"Unit\\Middleware\\TenantQuotaMiddlewareTest::testAllowsActiveOrganisation":0,"Unit\\Middleware\\TenantQuotaMiddlewareTest::testProvisioningAllowsAdminOnly":0,"Unit\\Middleware\\TenantQuotaMiddlewareTest::testProvisioningAllowsAdmin":0,"OCA\\OpenRegister\\Tests\\Unit\\Migration\\Version1Date20260313130000Test::testMigrationIdempotentWithoutColumns":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Migration\\Version1Date20260313130000Test::testMigrationDropsColumnsFromMagicTables":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Migration\\Version1Date20260313130000Test::testMigrationSkipsMagicTablesWithoutColumns":0,"OCA\\OpenRegister\\Tests\\Unit\\Migration\\Version1Date20260313130000Test::testMigrationSkipsNonMagicTables":0,"Unit\\Push\\PushEventsTest::testOrObjectConstantValue":0,"Unit\\Push\\PushEventsTest::testOrCollectionConstantValue":0,"Unit\\Push\\PushEventsTest::testOrObjectIsNonEmptyString":0,"Unit\\Push\\PushEventsTest::testOrCollectionIsNonEmptyString":0,"Unit\\Push\\PushEventsTest::testConstantsAreDistinct":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetIdReturnsCorrectIdentifier":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetTitleReturnsTranslatedString":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetOrderReturns10":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetSupportedSearchProviderIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testMatchReferenceHashRoutedUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testMatchReferenceHashRoutedUrlWithIndexPhp":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testMatchReferenceApiUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testMatchReferenceApiUrlWithIndexPhp":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testMatchReferenceDirectUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testMatchReferenceDirectUrlWithIndexPhp":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testMatchReferenceNonMatchingUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testResolveReferenceSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testResolveReferenceObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testResolveReferenceAuthorizationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testResolveReferenceNonMatchingUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetCachePrefixReturnsCorrectFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetCachePrefixFallsBackToUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetCacheKeyReturnsUserId":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetCacheKeyReturnsEmptyForAnonymous":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testGetIconUrlUsesUrlGenerator":0,"OCA\\OpenRegister\\Tests\\Unit\\Reference\\ObjectReferenceProviderTest::testParseReferenceApiUrl":0,"Unit\\Resources\\RapportageTemplatesTest::testTemplateParsesAndCarriesOneDashboardObject#woo":0.001,"Unit\\Resources\\RapportageTemplatesTest::testTemplateParsesAndCarriesOneDashboardObject#audit-trail":0.001,"Unit\\Resources\\RapportageTemplatesTest::testEveryWidgetPassesShapeValidation#woo":0.001,"Unit\\Resources\\RapportageTemplatesTest::testEveryWidgetPassesShapeValidation#audit-trail":0,"Unit\\Resources\\RapportageTemplatesTest::testWooTemplateCarriesTheDocumentedWidgets":0,"Unit\\Resources\\RapportageTemplatesTest::testAuditTrailTemplateMixesAggregationAndStatisticsModes":0,"Unit\\Resources\\RapportageTemplatesTest::testTemplatesDeclareTransparencyAndAuditCategories":0,"Unit\\Service\\ActionExecutorTest::testBuildCloudEventPayloadStructure":0,"Unit\\Service\\ActionExecutorTest::testExecuteActionsStopsOnPropagationStopped":0,"Unit\\Service\\ActionExecutorTest::testExecuteActionsEngineNotAvailableLogsFailure":0,"Unit\\Service\\ActionServiceTest::testCreateActionSuccess":0,"Unit\\Service\\ActionServiceTest::testCreateActionMissingNameThrows":0,"Unit\\Service\\ActionServiceTest::testCreateActionMissingEventTypeThrows":0,"Unit\\Service\\ActionServiceTest::testCreateActionMissingEngineThrows":0,"Unit\\Service\\ActionServiceTest::testDeleteActionSoftDeletes":0.001,"Unit\\Service\\ActionServiceTest::testUpdateActionDispatchesEvent":0.001,"Unit\\Service\\ActionServiceTest::testTestActionMatchReturnsTrue":0,"Unit\\Service\\ActionServiceTest::testTestActionFilterMismatch":0,"Unit\\Service\\ActionServiceTest::testMigrateFromHooksCreatesActions":0,"Unit\\Service\\ActionServiceTest::testUpdateStatisticsIncrementsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActivityServiceTest::testPublishObjectCreatedPublishesEvent":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActivityServiceTest::testPublishRegisterCreatedPublishesEvent":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActivityServiceTest::testPublishSchemaDeletedPublishesEventWithEmptyLink":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActivityServiceTest::testDualNotificationWhenOwnerDiffersFromAuthor":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActivityServiceTest::testExceptionIsCaughtAndLogged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActivityServiceTest::testSystemContextFallsBackToOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActivityServiceTest::testSystemContextNoOwnerSkipsPublishing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ActivityServiceTest::testAllNinePublishMethodsExist":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testNoAnnotationIsValid":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testEmptyMapIsRejected":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testCountMetricNeedsNoField":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testSumMetricRequiresField":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testFieldMustExistOnSchema":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testFilterFieldMustExist":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testGroupByFieldMustExist":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testUnknownMetricIsRejected":0,"Unit\\Service\\Aggregation\\AggregationAnnotationValidatorTest::testValidAnnotationProducesNoErrors":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testGetReturnsNullOnMiss":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testGetReturnsDecodedArrayOnHit":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testGetReturnsNullOnMalformedBlob":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testGetReturnsNullOnNonArrayJson":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testSetWritesEncodedJsonWithTtl":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testEvictForSchemaCallsClear":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testKeyIncludesAllInputs":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testKeyIsStableAcrossFilterOrder":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testDifferentFiltersProduceDifferentKeys":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testRbacScopeIsolatesUsers":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testAnonymousUserHasStableScope":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testGetSwallowsBackendException":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testSetSwallowsBackendException":0,"Unit\\Service\\Aggregation\\AggregationCacheTest::testCacheBackendUnavailableFailsClosed":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testCountQueryDoesNotRequireField":0.001,"Unit\\Service\\Aggregation\\AggregationQueryTest::testNonCountMetricsRequireField":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testRejectsUnknownMetric":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testGroupedQueryExposesGroupByField":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testGroupByMustHaveField":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testFilterIsCarriedThrough":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testDateBucketIsExposedThroughHasDateBucket":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testDateBucketRequiresAllFourFields":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testDateBucketGapMustBeKnownVocabulary":0,"Unit\\Service\\Aggregation\\AggregationQueryTest::testGroupByAndDateBucketAreMutuallyExclusive":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testCountUngroupedReturnsSizeZeroWithTrackTotalHits":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testCountGroupedAddsTermsBucket":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testSumUngroupedUsesMetricAggregation":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testAvgGroupedNestsMetricInsideTermsBucket":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testFiltersScalarEqualityBecomesTerm":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testFiltersInOperatorBecomesTerms":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testFiltersGteAndLtBecomeRange":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testFiltersNeBecomesMustNot":0,"Unit\\Service\\Aggregation\\ElasticsearchAggregationQueryBuilderTest::testDateBucketEmitsDateHistogramAgg":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testCountUngroupedReturnsRowsZero":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testCountGroupedAddsFacetField":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testSumUngroupedUsesStatsComponent":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testAvgGroupedUsesJsonFacetApi":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testFiltersScalarEqualityToFq":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testFiltersInOperatorToOrClause":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testFiltersGteAndLtToRangeClauses":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testFiltersNeUsesNegation":0,"Unit\\Service\\Aggregation\\SolrAggregationQueryBuilderTest::testDateBucketEmitsFacetRangeParams":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testNoAnnotationIsValid":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testEmptyArrayIsRejected":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testValidAggregationWidget":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testUnknownTypeIsRejected":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testMissingTitleIsRejected":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testMissingDataSourceIsRejected":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testBadModeIsRejected":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testAggregationModeRequiresAllRefs":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testGraphqlModeRequiresQuery":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testGraphqlModeWithQueryIsValid":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testOptionsMustBeObject":0,"Unit\\Service\\Aggregation\\WidgetAnnotationValidatorTest::testNonObjectWidgetIsRejected":0,"Unit\\Service\\ApprovalServiceTest::testInitializeChainCreatesStepsWithCorrectStatuses":0,"Unit\\Service\\ApprovalServiceTest::testApproveStepThrowsIfNotPending":0,"Unit\\Service\\ApprovalServiceTest::testApproveStepThrowsIfUserNotInRole":0,"Unit\\Service\\ApprovalServiceTest::testRejectStepThrowsIfNotPending":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateAfgehandeld":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateAfgehandeldNoClosure":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateEigenschap":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateEigenschapMissingProperty":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateTermijn":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateMissingAfleidingswijze":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateMissingBewaartermijn":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateInvalidBewaartermijn":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testCalculateUnknownAfleidingswijze":0,"Unit\\Service\\Archival\\ArchiefactiedatumCalculatorTest::testRecalculateOnPropertyChange":0,"Unit\\Service\\Archival\\DestructionServiceTest::testCreateDestructionList":0,"Unit\\Service\\Archival\\DestructionServiceTest::testCreateDestructionListEmpty":0,"Unit\\Service\\Archival\\DestructionServiceTest::testApproveListFull":0,"Unit\\Service\\Archival\\DestructionServiceTest::testApproveListPartial":0.001,"Unit\\Service\\Archival\\DestructionServiceTest::testRejectList":0,"Unit\\Service\\Archival\\DestructionServiceTest::testDualApprovalFirstStep":0,"Unit\\Service\\Archival\\DestructionServiceTest::testGenerateCertificate":0,"Unit\\Service\\Archival\\DestructionServiceTest::testGenerateCertificatePartial":0,"Unit\\Service\\Archival\\LegalHoldServiceTest::testPlaceHold":0.001,"Unit\\Service\\Archival\\LegalHoldServiceTest::testReleaseHold":0,"Unit\\Service\\Archival\\LegalHoldServiceTest::testHasActiveHoldTrue":0,"Unit\\Service\\Archival\\LegalHoldServiceTest::testHasActiveHoldFalse":0,"Unit\\Service\\Archival\\LegalHoldServiceTest::testHasActiveHoldReleased":0,"Unit\\Service\\Archival\\LegalHoldServiceTest::testHasActiveHoldFromRetention":0,"Unit\\Service\\Archival\\LegalHoldServiceTest::testBulkPlaceHold":0,"Unit\\Service\\Archival\\LegalHoldServiceTest::testPlaceHoldSystemUser":0,"Unit\\Service\\ArchivalServiceTest::testSetRetentionMetadataValidFull":0,"Unit\\Service\\ArchivalServiceTest::testSetRetentionMetadataDefaults":0,"Unit\\Service\\ArchivalServiceTest::testSetRetentionMetadataInvalidNominatie":0,"Unit\\Service\\ArchivalServiceTest::testSetRetentionMetadataInvalidStatus":0,"Unit\\Service\\ArchivalServiceTest::testSetRetentionMetadataInvalidDateFormat":0,"Unit\\Service\\ArchivalServiceTest::testSetRetentionMetadataMergesExisting":0,"Unit\\Service\\ArchivalServiceTest::testCalculateArchivalDateStandard":0,"Unit\\Service\\ArchivalServiceTest::testCalculateArchivalDateWithSchemaOverride":0,"Unit\\Service\\ArchivalServiceTest::testCalculateArchivalDateNoMatchingOverride":0,"Unit\\Service\\ArchivalServiceTest::testCalculateArchivalDateZeroYears":0,"Unit\\Service\\ArchivalServiceTest::testGenerateDestructionListEmpty":0,"Unit\\Service\\ArchivalServiceTest::testGenerateDestructionListWithObjects":0.001,"Unit\\Service\\ArchivalServiceTest::testApproveDestructionListInvalidStatus":0,"Unit\\Service\\ArchivalServiceTest::testApproveDestructionListSuccess":0.001,"Unit\\Service\\ArchivalServiceTest::testRejectFromDestructionListInvalidStatus":0,"Unit\\Service\\ArchivalServiceTest::testRejectFromDestructionListRemovesObjects":0,"Unit\\Service\\ArchivalServiceTest::testRejectAllObjectsCancelsList":0,"Unit\\Service\\AuditHashServiceTest::testGetGenesisHash":0,"Unit\\Service\\AuditHashServiceTest::testGetGenesisHashIsConsistent":0,"Unit\\Service\\AuditHashServiceTest::testGetCanonicalJsonExcludesHashFields":0,"Unit\\Service\\AuditHashServiceTest::testGetCanonicalJsonHasSortedKeys":0,"Unit\\Service\\AuditHashServiceTest::testComputeHashReturns64CharHex":0,"Unit\\Service\\AuditHashServiceTest::testComputeHashIsDeterministic":0,"Unit\\Service\\AuditHashServiceTest::testComputeHashDiffersWithDifferentPreviousHash":0,"Unit\\Service\\AuditHashServiceTest::testComputeHashDiffersWithDifferentEntryData":0,"Unit\\Service\\AuditHashServiceTest::testGetLastHashReturnsGenesisWhenEmpty":0,"Unit\\Service\\AuditHashServiceTest::testGetLastHashReturnsStoredHash":0,"Unit\\Service\\AuthorizationAuditServiceTest::testLogSchemaAuthorizationChangeCreatesInfoEntry":0,"Unit\\Service\\AuthorizationAuditServiceTest::testLogRegisterAuthorizationChangeIncludesAffectedCount":0,"Unit\\Service\\AuthorizationAuditServiceTest::testLogRoleDefinitionChangeCreatesInfoEntry":0,"Unit\\Service\\AuthorizationAuditServiceTest::testLogWithNoUserSessionUsesSystemFallback":0,"Unit\\Service\\Calculation\\CalculationAnnotationValidatorTest::testNoAnnotationIsValid":0,"Unit\\Service\\Calculation\\CalculationAnnotationValidatorTest::testEmptyMapIsRejected":0,"Unit\\Service\\Calculation\\CalculationAnnotationValidatorTest::testRefToUnknownProp":0,"Unit\\Service\\Calculation\\CalculationAnnotationValidatorTest::testSelfFieldAllowed":0,"Unit\\Service\\Calculation\\CalculationAnnotationValidatorTest::testSelfUnknownFieldRejected":0,"Unit\\Service\\Calculation\\CalculationAnnotationValidatorTest::testCycleDetected":0,"Unit\\Service\\Calculation\\CalculationAnnotationValidatorTest::testUnknownOperatorRejected":0,"Unit\\Service\\Calculation\\CalculationAnnotationValidatorTest::testValidNoErrors":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testBareScalar":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testProp":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testDottedProp":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testConcat":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testIfTrue":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testArithmetic":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testComparison":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testDateComparison":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testLogical":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testDiffDays":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testFormatDate":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testUnknownOperatorThrows":0.001,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testNonNumericArithmeticThrows":0,"Unit\\Service\\Calculation\\CalculationEvaluatorTest::testDivisionByZeroThrows":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesResolvedRelationViaIdKey":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesResolvedRelationMismatch":0,"Unit\\Service\\ConditionMatcherTest::testObjectMatchesPlainArrayValueWithoutIdKeyStaysArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportMappingCreatesNewWhenFindThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportMappingSetsVersionFromParam":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testGetDuplicateRegisterInfoFormatsCreatedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testGetDuplicateRegisterInfoShowsUnknownWhenNoCreatedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesItemsRefFromSchemasMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesItemsObjConfigRegisterFromDatabase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaUnsetsItemsObjConfigRegisterWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesItemsObjConfigSchemaFromDatabase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaUnsetsItemsObjConfigSchemaWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesLegacyRegisterFromRegistersMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesLegacyItemsRegisterFromRegistersMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaUpdatesWithOwnerAndApp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonPass2SkipsSchemaNotInMapBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonPass2CatchesReImportException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonUpdatesObjectWhenVersionHigher":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonSkipsObjectWhenVersionNotHigher":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testWorkflowHookWiringSkipsWhenDeployedNotInMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testWorkflowHookWiringDeduplicatesExistingHooks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppContinuesWhenVersionNotNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppCatchesSourceUrlException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppCatchesFindByAppException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppUpdatesLegacyGithubOnExistingConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppSetsDescriptionFromXOpenregister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataResolvesExternalConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataWarnsOnMissingExternalRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataWarnsOnMissingExternalSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataCatchesExternalRegisterException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataCatchesExternalSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataFindsSchemaFromDatabase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromAppSkipsNonSchemaInResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testCreateOrUpdateConfigurationWrapsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromFilePathThrowsOnMissingFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaStripsBinaryFormatFromItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaConvertsStdClassItemsObjectConfigToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonCallsOpenConnectorIntegration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonCatchesOpenConnectorFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonCreatesNewObjectWhenNoExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSeedDataFallsToBlobStorageWithoutUnifiedMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaCatchesValidationExceptionAndCreatesNew":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonResolvesRegisterSchemasFromMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonWarnsWhenRegisterSchemaNotInMap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportFromJsonContinuesOnSchemaFailureInPass1":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testCreateOrUpdateConfigurationSetsNestedGithub":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testCreateOrUpdateConfigurationSetsLegacyGithub":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetMagicMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetObjectMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetWorkflowEngineRegistry":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetDeployedWorkflowMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testSetOpenConnectorConfigurationService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaResolvesObjConfigRegisterFromDatabase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerCoverageTest::testImportSchemaUnsetsObjConfigRegisterWhenNotFoundInDB":0,"Unit\\Service\\Configuration\\ImportHandlerSeedRelatedItemsTest::testFilesAreCreatedWithBase64Decoded":0,"Unit\\Service\\Configuration\\ImportHandlerSeedRelatedItemsTest::testNotesAreCreatedThroughNoteService":0,"Unit\\Service\\Configuration\\ImportHandlerSeedRelatedItemsTest::testTasksAreCreatedWithFullData":0,"Unit\\Service\\Configuration\\ImportHandlerSeedRelatedItemsTest::testTaskFailureDoesNotBlockOtherTypes":0,"Unit\\Service\\Configuration\\ImportHandlerSeedRelatedItemsTest::testTasksAndNotesSkippedWhenNoUserContextButFilesRun":0,"Unit\\Service\\Configuration\\ImportHandlerSeedRelatedItemsTest::testServiceNotInjectedSkipsTypeSilently":0,"Unit\\Service\\Configuration\\ImportHandlerSeedRelatedItemsTest::testInvalidFileEntriesAreSkipped":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testSetObjectMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Configuration\\ImportHandlerTest::testImportFromJsonUseMagicMapperForSeedData":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByEmailReturnsResultsWithConfidenceOne":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByEmailIsCaseInsensitive":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByEmailReturnsEmptyArrayForNoMatch":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByEmailReturnsCachedResultsWithoutDbQuery":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByEmailWithEmptyStringReturnsEmpty":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByNameFullMatchReturnsConfidencePointSeven":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByNamePartialMatchReturnsConfidencePointFour":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByNameNoMatchReturnsEmptyArray":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByOrganizationExactMatchReturnsConfidencePointFive":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByOrganizationNoMatchReturnsEmptyArray":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchByOrganizationFiltersToOrgTypedSchemasOnly":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchContactDeduplicatesByUuidKeepingHighestConfidence":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchContactEmptyEmailWithNameOnly":0,"Unit\\Service\\ContactMatchingServiceTest::testMatchContactAllThreeParametersProvided":0,"Unit\\Service\\ContactMatchingServiceTest::testGetRelatedObjectCountsGroupsBySchemaTitle":0,"Unit\\Service\\ContactMatchingServiceTest::testInvalidateCacheRemovesCacheEntry":0,"Unit\\Service\\ContactMatchingServiceTest::testInvalidateCacheForObjectExtractsEmailProperties":0.001,"Unit\\Service\\ContactMatchingServiceTest::testInvalidateCacheForObjectIgnoresNonEmailProperties":0,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesWithRegisterFilter":0,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesWithSchemaFilter":0,"Unit\\Service\\DashboardServiceTest::testRecalculateSizesWithBothFilters":0,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesCountsFailures":0,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesWithRegisterFilter":0,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesWithSchemaFilter":0,"Unit\\Service\\DashboardServiceTest::testRecalculateLogSizesWithBothFilters":0,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesAggregatesProcessedAndFailed":0,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesWithFilters":0,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesThrowsWhenObjectRecalcFails":0,"Unit\\Service\\DashboardServiceTest::testRecalculateAllSizesThrowsWhenLogRecalcFails":0,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasFiltersByRegisterId":0,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasFiltersBySchemaId":0,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasExcludesNonMatchingSchemas":0,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasNoRegisters":0,"Unit\\Service\\DashboardServiceTest::testGetRegistersWithSchemasStatsErrorReturnsZeroes":0,"Unit\\Service\\DashboardServiceTest::testCalculateReturnsFullResponseNoFilters":0,"Unit\\Service\\DashboardServiceTest::testCalculateWithRegisterId":0,"Unit\\Service\\DashboardServiceTest::testCalculateWithRegisterAndSchemaId":0,"Unit\\Service\\DashboardServiceTest::testCalculateWithSchemaNotBelongingToRegisterThrows":0,"Unit\\Service\\DashboardServiceTest::testCalculateWithNonExistentRegisterThrows":0,"Unit\\Service\\DashboardServiceTest::testCalculateWithNonExistentSchemaThrows":0,"Unit\\Service\\DashboardServiceTest::testCalculateSuccessRateWithProcessed":0,"Unit\\Service\\DashboardServiceTest::testCalculateSuccessRateWithAllSuccessful":0,"Unit\\Service\\DashboardServiceTest::testCalculateWithSchemaIdOnlyNoRegister":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionChartDataWithDateFilters":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsByRegisterChartDataReturnsData":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsByRegisterChartDataWithFilters":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySchemaChartDataReturnsData":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySchemaChartDataWithFilters":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySizeChartDataReturnsData":0,"Unit\\Service\\DashboardServiceTest::testGetObjectsBySizeChartDataWithFilters":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailStatisticsWithFilters":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionDistributionReturnsData":0,"Unit\\Service\\DashboardServiceTest::testGetAuditTrailActionDistributionWithFilters":0,"Unit\\Service\\DashboardServiceTest::testGetMostActiveObjectsReturnsData":0,"Unit\\Service\\DashboardServiceTest::testGetMostActiveObjectsWithFilters":0,"Unit\\Service\\DashboardServiceTest::testOrphanedStatsWithMultipleRegistersAndSchemas":0.001,"Unit\\Service\\DashboardServiceTest::testOrphanedStatsErrorFallbackReturnsZeroes":0,"Unit\\Service\\DashboardServiceTest::testGetStatsErrorBranchReturnsZeroStructure":0,"Unit\\Service\\DashboardServiceTest::testGetStatsHandlesWebhookLogsMissingTotal":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testNullInputReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testEmptyStringReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testWhitespaceOnlyReturnsNull#spaces":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testWhitespaceOnlyReturnsNull#tab":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testWhitespaceOnlyReturnsNull#newline":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testWhitespaceOnlyReturnsNull#mixed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testIso8601WithOffsetReturnsImmutable":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testIso8601ZuluReturnsImmutable":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testDatabaseFormatReturnsImmutable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testDateOnlyReturnsImmutableAtMidnight":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testExistingDateTimeImmutablePassesThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testMutableDateTimeConvertsToImmutable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testGarbledStringReturnsNullAndLogs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testNonStringNonDateTimeReturnsNullAndLogs#integer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testNonStringNonDateTimeReturnsNullAndLogs#float":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testNonStringNonDateTimeReturnsNullAndLogs#bool":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testNonStringNonDateTimeReturnsNullAndLogs#array":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testNonStringNonDateTimeReturnsNullAndLogs#object":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testFormatForDatabaseOnEmptyReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testFormatForDatabaseOnValidProducesYMDHIS":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testFormatForDatabaseOnGarbledReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testFormatForIso8601OnEmptyReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testFormatForIso8601OnValidProducesIso8601WithOffset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\DateTimeNormalizerTest::testFormatForIso8601OnGarbledReturnsNull":0,"Unit\\Service\\DeckCardServiceTest::testIsDeckAvailableTrue":0,"Unit\\Service\\DeckCardServiceTest::testIsDeckAvailableFalse":0,"Unit\\Service\\DeckCardServiceTest::testGetCardsForObjectReturnsResults":0,"Unit\\Service\\DeckCardServiceTest::testGetCardsForObjectEmpty":0,"Unit\\Service\\DeckCardServiceTest::testLinkOrCreateCardThrowsWhenNoUser":0,"Unit\\Service\\DeckCardServiceTest::testLinkOrCreateCardThrowsMissingParams":0,"Unit\\Service\\DeckCardServiceTest::testUnlinkCardSuccess":0,"Unit\\Service\\DeckCardServiceTest::testUnlinkCardNotFound":0,"Unit\\Service\\DeckCardServiceTest::testGetObjectsForBoardReturnsLinks":0,"Unit\\Service\\DeckCardServiceTest::testDeleteLinksForObject":0,"Unit\\Service\\Edepot\\MdtoXmlGeneratorTest::testGenerateCompleteObject":0,"Unit\\Service\\Edepot\\MdtoXmlGeneratorTest::testGenerateWithFiles":0,"Unit\\Service\\Edepot\\MdtoXmlGeneratorTest::testGenerateWithMissingOptionalFields":0,"Unit\\Service\\Edepot\\MdtoXmlGeneratorTest::testGenerateMissingRequiredFieldsThrows":0,"Unit\\Service\\Edepot\\MdtoXmlGeneratorTest::testGenerateMissingOrganisationThrows":0,"Unit\\Service\\Edepot\\SipPackageBuilderTest::testBuildEmptyObjectsThrows":0,"Unit\\Service\\Edepot\\SipPackageBuilderTest::testBuildReturnsSipFilePaths":0.003,"Unit\\Service\\Edepot\\SipPackageBuilderTest::testBuildContainsExpectedEntries":0.002,"Unit\\Service\\Edepot\\SipPackageBuilderTest::testBuildSplitsLargePackages":0.004,"Unit\\Service\\Edepot\\TransferListServiceTest::testCreateTransferList":0.001,"Unit\\Service\\Edepot\\TransferListServiceTest::testCreateTransferListEmptyThrows":0,"Unit\\Service\\Edepot\\TransferListServiceTest::testApproveTransferList":0,"Unit\\Service\\Edepot\\TransferListServiceTest::testApproveNonReviewListThrows":0,"Unit\\Service\\Edepot\\TransferListServiceTest::testRejectTransferList":0,"Unit\\Service\\Edepot\\TransferListServiceTest::testRejectWithoutReasonThrows":0,"Unit\\Service\\Edepot\\TransferListServiceTest::testExcludeObjects":0,"Unit\\Service\\Edepot\\TransferListServiceTest::testGetObjectsOnActiveTransferLists":0,"Unit\\Service\\Edepot\\TransferListServiceTest::testNotifyArchivists":0,"Unit\\Service\\Edepot\\TransportTest::testTransportResultSuccess":0.001,"Unit\\Service\\Edepot\\TransportTest::testTransportResultPartialSuccess":0,"Unit\\Service\\Edepot\\TransportTest::testTransportResultToArray":0,"Unit\\Service\\Edepot\\TransportTest::testSftpTransportName":0.002,"Unit\\Service\\Edepot\\TransportTest::testSftpTransportSendMissingFile":0,"Unit\\Service\\Edepot\\TransportTest::testSftpTransportSendMissingConfig":0,"Unit\\Service\\Edepot\\TransportTest::testRestApiTransportName":0.001,"Unit\\Service\\Edepot\\TransportTest::testRestApiTransportSendMissingFile":0,"Unit\\Service\\Edepot\\TransportTest::testRestApiTransportSendMissingConfig":0,"Unit\\Service\\Edepot\\TransportTest::testOpenConnectorTransportName":0.001,"Unit\\Service\\Edepot\\TransportTest::testOpenConnectorTransportSendMissingConfig":0,"Unit\\Service\\EmailServiceTest::testIsMailAvailableReturnsTrueWhenEnabled":0,"Unit\\Service\\EmailServiceTest::testIsMailAvailableReturnsFalseWhenDisabled":0,"Unit\\Service\\EmailServiceTest::testGetEmailsForObjectReturnsResults":0,"Unit\\Service\\EmailServiceTest::testGetEmailsForObjectReturnsEmpty":0,"Unit\\Service\\EmailServiceTest::testLinkEmailThrowsOnDuplicate":0,"Unit\\Service\\EmailServiceTest::testUnlinkEmailSuccess":0,"Unit\\Service\\EmailServiceTest::testUnlinkEmailNotFound":0,"Unit\\Service\\EmailServiceTest::testSearchBySenderReturnLinks":0,"Unit\\Service\\EmailServiceTest::testDeleteLinksForObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsUserAdminNullUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsUserAdminNoAdminGroup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsUserAdminTrue":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsUserAdminFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyUuidFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyWithRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyArrayItemsRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyRegularString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIsRelationPropertyArrayNoItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsSingleString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsJsonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testCollectUuidsNullValuesInArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesSingleString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesSingleStringNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesJsonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testResolveUuidsToNamesArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringBool":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testConvertValueToStringObjectWithToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testIdentifyNameCompanionColumns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testGetObjectValueIdHeader":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testGetObjectValueRegularField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testGetObjectValueMissingField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ExportServiceCoverageTest::testExportToCsvMultipleSchemasThrows":0,"Unit\\Service\\ExportServiceGapTest::testExportToCsvThrowsForMultipleSchemas":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithNullRegisterAndSchema":0.003,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithSchema":0.001,"Unit\\Service\\ExportServiceGapTest::testExportToExcelSkipsHiddenProperties":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelSkipsInvisibleProperties":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelSkipsRbacRestrictedProperties":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelAddsRelationCompanionColumns":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelAddsRefCompanionColumns":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelAddsArrayUuidCompanionColumns":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithNullUserSkipsMetadata":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithAdminUserAddsMetadata":0.001,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithNonAdminGroupNull":0.001,"Unit\\Service\\ExportServiceGapTest::testExportToExcelSkipsDefaultFields":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelWithRegisterAllSchemas":0,"Unit\\Service\\ExportServiceGapTest::testExportToExcelNoCompanionForNonRelation":0,"Unit\\Service\\ExportServiceTest::testExportToExcelNoMetadataWhenAdminGroupDoesNotExist":0,"Unit\\Service\\ExportServiceTest::testExportPassesSelfMetadataFilters":0,"Unit\\Service\\ExportServiceTest::testExportSkipsNonSelfFilters":0,"Unit\\Service\\ExportServiceTest::testExportWithMultiFilterFalse":0,"Unit\\Service\\ExportServiceTest::testExportWithMultiFilterShortName":0,"Unit\\Service\\ExportServiceTest::testExportMetadataDateTimeFieldFormatsCorrectly":0.002,"Unit\\Service\\ExportServiceTest::testExportMetadataArrayFieldConvertedToJson":0.002,"Unit\\Service\\ExportServiceTest::testExportMetadataScalarFieldReturnsString":0.002,"Unit\\Service\\ExportServiceTest::testExportMetadataNullFieldReturnsNull":0.002,"Unit\\Service\\ExportServiceTest::testExportCompanionColumnWithNullRelation":0.001,"Unit\\Service\\ExportServiceTest::testExportResolvesJsonEncodedArrayOfUuids":0.001,"Unit\\Service\\ExportServiceTest::testExportPreSeedsNameMapFromLoadedObjects":0.001,"Unit\\Service\\ExportServiceTest::testExportNoCompanionColumnForNonRelationProperty":0.001,"Unit\\Service\\ExportServiceTest::testExportCompanionColumnForArrayWithRefItems":0.032,"Unit\\Service\\ExportServiceTest::testExportToExcelWithNoSchemaUsesDataAsSheetTitle":0,"Unit\\Service\\ExportServiceTest::testExportSkipsExplicitlyInvisibleProperties":0,"Unit\\Service\\ExportServiceTest::testExportHandlesIntegerValuesAsStrings":0,"Unit\\Service\\ExportServiceTest::testExportWithRegisterAndSchemaPassesBothIds":0,"Unit\\Service\\ExportServiceTest::testExportFallsBackToUuidWhenNameNotResolved":0.001,"Unit\\Service\\ExportServiceTest::testExportHandlesJsonArrayWithEmptyStrings":0,"Unit\\Service\\ExportServiceTest::testExportWithMultipleObjectsMixedRelationData":0.001,"Unit\\Service\\ExportServiceTest::testExportWithNoRelationPropertiesSkipsBulkResolve":0,"Unit\\Service\\ExportServiceTest::testExportResolvesNativeArrayOfUuids":0,"Unit\\Service\\ExportServiceTest::testExportHandlesEmptyStringUuidValue":0,"Unit\\Service\\ExportServiceTest::testExportHandlesMissingFieldInObjectData":0,"Unit\\Service\\ExportServiceTest::testExportToCsvWithRegisterAndSchema":0,"Unit\\Service\\ExportServiceTest::testExportWithMultiFilterTrue":0,"Unit\\Service\\ExportServiceTest::testExportResolvesArrayWithNonStringItems":0.001,"Unit\\Service\\ExportServiceTest::testExportResolvesJsonArrayWithNonStringItems":0.001,"Unit\\Service\\ExportServiceTest::testExportDefaultMultitenancy":0,"Unit\\Service\\File\\FileAuditHandlerTest::testLogDownloadAuthenticated":0,"Unit\\Service\\File\\FileAuditHandlerTest::testLogDownloadAnonymous":0,"Unit\\Service\\File\\FileAuditHandlerTest::testLogBulkDownload":0,"Unit\\Service\\File\\FileAuditHandlerTest::testLogBulkDownloadAnonymous":0,"Unit\\Service\\File\\FileAuditHandlerTest::testLogDownloadDoesNotThrow":0,"Unit\\Service\\File\\FileAuditHandlerTest::testLogFileActionPersistsAuditTrail":0,"Unit\\Service\\File\\FileAuditHandlerTest::testLogFileActionDoesNotThrowOnInsertFailure":0,"Unit\\Service\\File\\FileAuditHandlerTest::testLogFileActionFallsBackToSystemUser":0,"Unit\\Service\\File\\FileBatchHandlerTest::testBatchPublishSuccess":0.002,"Unit\\Service\\File\\FileBatchHandlerTest::testBatchPartialFailure":0,"Unit\\Service\\File\\FileBatchHandlerTest::testBatchSizeLimit":0,"Unit\\Service\\File\\FileBatchHandlerTest::testBatchInvalidAction":0,"Unit\\Service\\File\\FileBatchHandlerTest::testBatchEmptyFileIds":0,"Unit\\Service\\File\\FileFormattingHandlerTest::testFormatFileAuthenticatedNoLocksReturnsLockedFalse":0.002,"Unit\\Service\\File\\FileFormattingHandlerTest::testFormatFileAuthenticatedWithUserLockReturnsLockEnvelope":0.001,"Unit\\Service\\File\\FileFormattingHandlerTest::testFormatFileAuthenticatedAppLockWithTimeoutComputesExpiresAt":0,"Unit\\Service\\File\\FileFormattingHandlerTest::testFormatFileAuthenticatedLockProviderUnavailableReturnsLockedFalse":0,"Unit\\Service\\File\\FileFormattingHandlerTest::testFormatFileAnonymousOmitsLockedAndLock":0,"Unit\\Service\\File\\FileFormattingHandlerTest::testFormatFilesResilientToLockedExceptionAuthenticated":0.001,"Unit\\Service\\File\\FileFormattingHandlerTest::testFormatFilesResilientToLockedExceptionAnonymousOmitsLockFields":0.001,"Unit\\Service\\File\\FileFormattingHandlerTest::testLimitHonoursLargeValueWithoutCeiling":0,"Unit\\Service\\File\\FileFormattingHandlerTest::testLimitZeroAndNegativeValuesClampToFloorOfOne":0,"Unit\\Service\\File\\FileFormattingHandlerTest::testLimitDefaultsToThirtyWhenMissing":0,"Unit\\Service\\File\\FileLockHandlerCachePersistenceTest::testLockSurvivesAcrossHandlerInstancesViaSharedCache":0,"Unit\\Service\\File\\FileLockHandlerCachePersistenceTest::testCacheTtlElidesExpiredLockOnRead":0,"Unit\\Service\\File\\FileLockHandlerCachePersistenceTest::testDefensiveTtlRecheckClearsStaleEntry":0,"Unit\\Service\\File\\FileLockHandlerCachePersistenceTest::testUnlockRemovesEntryFromSharedCache":0,"Unit\\Service\\File\\FileLockHandlerTest::testLockFileSuccess":0,"Unit\\Service\\File\\FileLockHandlerTest::testLockFileConflict":0,"Unit\\Service\\File\\FileLockHandlerTest::testUnlockByOwner":0,"Unit\\Service\\File\\FileLockHandlerTest::testUnlockAlreadyUnlocked":0,"Unit\\Service\\File\\FileLockHandlerTest::testAssertCanModifyUnlockedFile":0,"Unit\\Service\\File\\FileLockHandlerTest::testAssertCanModifyByLockOwner":0,"Unit\\Service\\File\\FileLockHandlerTest::testGetLockInfoUnlocked":0,"Unit\\Service\\File\\FileLockHandlerTest::testGetLockInfoLocked":0,"Unit\\Service\\File\\FileLockHandlerTest::testUnlockByNonOwnerThrows":0,"Unit\\Service\\File\\FileLockHandlerTest::testAdminForceUnlockSucceeds":0,"Unit\\Service\\File\\FileLockHandlerTest::testTtlExpiryAutoClears":0,"Unit\\Service\\File\\FileLockHandlerTest::testAssertCanModifyByNonOwnerThrows":0,"Unit\\Service\\File\\FilePreviewHandlerTest::testGetPreviewSuccess":0,"Unit\\Service\\File\\FilePreviewHandlerTest::testGetPreviewUnsupportedType":0,"Unit\\Service\\File\\FilePreviewHandlerTest::testIsPreviewAvailableTrue":0,"Unit\\Service\\File\\FilePreviewHandlerTest::testIsPreviewAvailableFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipReadableFileWithCorrectOwnerIsNoOp":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipUnreadableFileThrowsNotPermitted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipReadableFileWithDriftedOwnerTriggersRepair":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipReadableFileWithNullOwnerTriggersRepair":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\File\\FileValidationHandlerTest::testCheckOwnershipRepairFailureIsSwallowed":0,"Unit\\Service\\File\\FileVersioningHandlerTest::testListVersionsDisabled":0,"Unit\\Service\\File\\FileVersioningHandlerTest::testListVersionsEnabled":0,"Unit\\Service\\File\\FileVersioningHandlerTest::testRestoreVersionDisabled":0,"Unit\\Service\\File\\FileVersioningHandlerTest::testIsVersioningEnabled":0,"Unit\\Service\\File\\FileVersioningHandlerTest::testIsVersioningNotEnabled":0,"Unit\\Service\\File\\FileVersioningHandlerTest::testRestoreVersionRejectsMalformedId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\FileSidebarServiceTest::testGetObjectsForFileReturnsEmptyWhenNoRegisters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\FileSidebarServiceTest::testGetObjectsForFileReturnsEmptyOnRegisterException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\FileSidebarServiceTest::testGetObjectsForFileSkipsRegistersWithNoSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\FileSidebarServiceTest::testGetExtractionStatusReturnsNoneWhenNoChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\FileSidebarServiceTest::testGetExtractionStatusReturnsCompletedWithEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\FileSidebarServiceTest::testGetExtractionStatusDetectsAnonymization":0,"Unit\\Service\\Geo\\GeoFilterApplierTest::testEmptyFilterListPassesEverythingThrough":0,"Unit\\Service\\Geo\\GeoFilterApplierTest::testBboxFilterKeepsOnlyMatchingRows":0.001,"Unit\\Service\\Geo\\GeoFilterApplierTest::testAndComposition_BboxIntersectsNear":0,"Unit\\Service\\Geo\\GeoFilterApplierTest::testRowWithoutGeometryIsExcluded":0,"Unit\\Service\\Geo\\GeoFilterApplierTest::testPropertyHintTargetsNamedProperty":0,"Unit\\Service\\Geo\\GeoFilterApplierTest::testCoercionRejectsNonGeoJsonShapes":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testQueryWithoutGeoParamsReturnsEmpty":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testQueryParsesBbox":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testQueryParsesNearAndRadius":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testQueryRejectsNearWithoutRadius":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testQueryRejectsRadiusWithoutNear":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testQueryComposesBboxAndNear":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testQueryHonoursPropertyHint":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testGeoSearchBodyParsesWithinPolygon":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testGeoSearchBodyParsesIntersects":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testGeoSearchBodyComposesWithinAndIntersects":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testGeoSearchBodyRejectsMissingGeometry":0,"Unit\\Service\\Geo\\GeoFilterParserTest::testGeoSearchBodyRejectsEmptyGeometry":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testBboxIncludesPointInside":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testBboxExcludesPointOutside":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testBboxIncludesPointOnEdge":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testBboxRejectsMalformed":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testBboxRejectsWestGreaterThanEast":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testBboxOnPolygonUsesCentroid":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testBboxNullGeometryFails":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testNearIncludesPointInsideRadius":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testNearExcludesPointOutsideRadius":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testNearRejectsZeroOrNegativeRadius":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testHaversineKnownDistance":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testWithinIncludesPointInside":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testWithinExcludesPointOutside":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testWithinRejectsNonPolygonPredicate":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testWithinRejectsMissingCoordinates":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testIntersectsPointInsidePredicate":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testIntersectsOverlappingPolygons":0,"Unit\\Service\\Geo\\GeoSpatialEvaluatorTest::testIntersectsDisjointPolygons":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceErrorsCsvTest::testReturnsEmptyStringWhenNoErrorsPresent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceErrorsCsvTest::testIncludesUtf8BomPrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceErrorsCsvTest::testEmitsExpectedHeaderRow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceErrorsCsvTest::testValidationErrorShapeRendersCorrectly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceErrorsCsvTest::testRowParseErrorUsesSheetKeyAsFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceErrorsCsvTest::testCombinesErrorsAcrossSheets":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceErrorsCsvTest::testSkipsSheetsWithoutErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServicePublishDeprecationTest::testAddPublishedDateToObjectsMethodRemoved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServicePublishDeprecationTest::testImportFromExcelStillAcceptsPublishParameter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServicePublishDeprecationTest::testImportFromCsvStillAcceptsPublishParameter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServicePublishDeprecationTest::testNoMethodInjectsPublishedMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvIncludesSchemaInfo":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithUpdatedObjects":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithPublishDisabled":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithIdColumn":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithAtColumnNotSelfPrefix":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithSelfPublishedColumnForAdmin":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithMultipleRowsAndMixedResults":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithNumberProperty":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithObjectProperty":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithRelatedObjectProperty":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithValidationErrorMissingFields":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithValidFile":0.01,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithSchemaInfo":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithNoSchema":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelMultiSchemaWithMatchingSchema":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithPublishEnabled":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithEmptySheet":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithEmptyHeaders":0.007,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithTypedProperties":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithUnderscoreColumns":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithAtColumnsAsAdmin":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithAtColumnsAsNonAdmin":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithIdColumn":0.007,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithNoRegisterOrSchema":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithValidationErrors":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithDeduplication":0.007,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithAtOtherColumnAsAdmin":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithNoSchemaSkipsTransform":0.006,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformSelfPropertyUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeEmptyStringValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeNoTypeKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeObjectInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformValueByTypeArrayInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testValidateObjectPropertiesWithBodyAndPayload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformObjectBySchemaWithAllTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImportedOnlyUpdated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testCalculateTotalImportedMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testBuildColumnMapping":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testBuildColumnMappingWithEmptySheet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testBuildColumnMappingStopsAtEmptyColumn":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testBuildColumnMappingTrimsWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testExtractRowData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testExtractRowDataEmptyRow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testExtractRowDataPartialRow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testExtractRowDataTrimsWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetSchemaBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testGetSchemaBySlugThrowsOnNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformCsvRowToObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformCsvRowToObjectWithIdField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformCsvRowToObjectSkipsEmptyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformCsvRowToObjectCachesSchemaProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithoutSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithoutRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithIdField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectSkipsEmptyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithUnderscoreColumns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithAtColumnsAsAdmin":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithAtColumnsAsNonAdmin":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testTransformExcelRowToObjectWithAtOtherColumn":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithWhitespaceOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithJsonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithCommaSeparated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithSingleValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithExistingArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToArrayWithNonStringNonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithValidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithInvalidJson":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithPlainString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithExistingArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToObjectWithStdClass":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testStringToBooleanWithVariousInputs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvWithEventsAndEnrichDisabled":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromCsvPerformanceMetricsComplete":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ImportServiceTest::testImportFromExcelWithSelfCreatedColumnAsAdmin":0.005,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testNoAnnotationIsValid":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testMissingFieldIsRejected":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testFieldNotInPropertiesIsRejected":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testFieldNotStringIsRejected":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testInitialNotInEnumIsRejected":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testFinalNotInEnumIsRejected":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testTransitionFromNotInEnumIsRejected":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testTransitionToNotInEnumIsRejected":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testRequiresMustBeNonEmptyString":0,"Unit\\Service\\Lifecycle\\LifecycleAnnotationValidatorTest::testValidAnnotationProducesNoErrors":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testInlinePostSkippedWhenWebhookPersistent":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testTalkChannelPostsOnceWithToken":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testTalkChannelSilentWhenTokenMissing":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testObjectAclManageReturnsOwnerOnly":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testObjectAclReadIncludesOwnerAndGroupMembers":0.001,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testExpressionResolverReceivesObjectAndContext":0.001,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testExpressionResolverFailsClosedOnInterfaceMismatch":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testExpressionResolverFailsClosedOnMissingService":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testInlinePostFiresWhenWebhookNotPersistent":0.001,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testPerLocaleSubjectRendersDutchForDutchUser":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testPerLocaleSubjectRendersEnglishForEnglishUser":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testPerLocaleSubjectFallsBackToDefaultLocaleWhenUserHasNoPreference":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testLegacyStringSubjectStillRenders":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testWebhookBroadcastUsesDefaultLocaleNotRecipientLocale":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testOrganisationGateBlocksWhenOrgsDoNotMatch":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testOrganisationGateAllowsWhenOrgsMatch":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testOrganisationGateAllowsWhenObjectOrgInDeclaredArray":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testOrganisationGateBlocksUntenantedObjectsByDefault":0,"Unit\\Service\\Notification\\AnnotationNotificationDispatcherTest::testNoOrganisationGateLeavesDispatchUnchanged":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testNoAnnotationIsValid":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testEmptyMapIsRejected":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testBadTriggerIsRejected":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testBadChannelIsRejected":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testRecipientFieldMustExist":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testValidNoErrors":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testGroupsRecipientAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testRelationRecipientAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testWebhookChannelRequiresUrl":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testWebhookChannelWithUrlAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testObjectAclRecipientRequiresPermission":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testObjectAclWithReadPermissionAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testExpressionRecipientRequiresResolver":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testExpressionRecipientWithResolverAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testTalkChannelRequiresToken":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testTalkChannelWithTokenAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testEmailAndActivityChannelsAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testScheduledTriggerRequiresIntervalSec":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testScheduledTriggerRejectsTooShortInterval":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testScheduledTriggerWithIntervalAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testThresholdTriggerRequiresAggregationOpAndValue":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testThresholdTriggerWithFullSpecAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testPerLocaleSubjectMapAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testPerLocaleSubjectMapWithDefaultLocaleAccepted":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testPerLocaleSubjectMapWithEmptyLocaleRejected":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testPerLocaleSubjectMapMissingDefaultLocaleRejected":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testPerLocaleSubjectMapWithOnlyDefaultLocaleKeyRejected":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testOrganisationGateAcceptsSingleStringUuid":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testOrganisationGateAcceptsArrayOfStrings":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testOrganisationGateRejectsEmptyString":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testOrganisationGateRejectsEmptyArray":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testOrganisationGateRejectsArrayWithEmptyEntry":0,"Unit\\Service\\Notification\\NotificationAnnotationValidatorTest::testOrganisationGateRejectsNonStringNonArray":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testFirstEventOpensWindowAndDispatches":0.001,"Unit\\Service\\Notification\\NotificationCoalescerTest::testSubsequentEventsInWindowAreSilenced":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testNextEventAfterWindowOpensFreshWindow":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testMaxEventsForcesEarlyFlush":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testIsolatesPerRuleAndPerRecipient":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testNoConfigBlockMeansNoCoalescing":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testKillSwitchAlwaysAllows":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testEmptyRuleOrRecipientFailsOpen":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testInspectReturnsCurrentWindowState":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testInspectReturnsNullWhenNoState":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testCacheReadFailureFailsOpen":0,"Unit\\Service\\Notification\\NotificationCoalescerTest::testSilencedDispatchEmitsInfoNotWarning":0,"Unit\\Service\\Notification\\NotificationDigestTest::testEmptyDigestHasZeroCounts":0.001,"Unit\\Service\\Notification\\NotificationDigestTest::testEnqueueAccumulatesPerRecipient":0,"Unit\\Service\\Notification\\NotificationDigestTest::testFlushReturnsBucketsAndClears":0,"Unit\\Service\\Notification\\NotificationDigestTest::testFlushPreservesEnqueueOrderPerRecipient":0,"Unit\\Service\\Notification\\NotificationReadStateTest::testNewNotificationsStartUnread":0,"Unit\\Service\\Notification\\NotificationReadStateTest::testMarkReadFlipsTheState":0,"Unit\\Service\\Notification\\NotificationReadStateTest::testMarkReadIsIdempotent":0,"Unit\\Service\\Notification\\NotificationReadStateTest::testMarkUnreadRestoresUnreadState":0,"Unit\\Service\\Notification\\NotificationReadStateTest::testReadStateIsScopedPerUserPerNotification":0,"Unit\\Service\\Notification\\NotificationsAnnotationInstallerTest::testCreatesWebhookForPersistentNotification":0.001,"Unit\\Service\\Notification\\NotificationsAnnotationInstallerTest::testUpdatesExistingWebhookByName":0,"Unit\\Service\\Notification\\NotificationsAnnotationInstallerTest::testSkipsWhenWebhookChannelNotDeclared":0,"Unit\\Service\\Notification\\NotificationsAnnotationInstallerTest::testSkipsWhenWebhookNotPersistent":0,"Unit\\Service\\Notification\\NotificationsAnnotationInstallerTest::testSkipsWhenUrlMissingOrInvalid":0,"Unit\\Service\\Notification\\NotificationsAnnotationInstallerTest::testIdempotentOnRepeatedInstall":0,"Unit\\Service\\Notification\\NotificationsAnnotationInstallerTest::testFiltersOutFalsePositivesInFindByName":0,"Unit\\Service\\Notification\\NotificationsAnnotationInstallerTest::testSwallowsCreateFailure":0,"Unit\\Service\\Notification\\RateLimiterTest::testAllowsUpToBucketSizeThenDrops":0.001,"Unit\\Service\\Notification\\RateLimiterTest::testRefillsOneTokenAfterRefillInterval":0,"Unit\\Service\\Notification\\RateLimiterTest::testIsolatesPerRuleAndPerRecipient":0,"Unit\\Service\\Notification\\RateLimiterTest::testKillSwitchAlwaysAllows":0,"Unit\\Service\\Notification\\RateLimiterTest::testAppConfigDefaultsApply":0,"Unit\\Service\\Notification\\RateLimiterTest::testEmptyRuleOrRecipientFailsOpen":0,"Unit\\Service\\Notification\\RateLimiterTest::testCacheReadFailureFailsOpen":0,"Unit\\Service\\Notification\\RateLimiterTest::testDropEmitsInfoNotWarning":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testEnvelopeForCreateActionIsVngCompliant":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testEnvelopeStripsTrailingSlashFromBaseUrl":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testEnvelopeIncludesKenmerkenWhenProvided":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#create":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#created (past tense)":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#update":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#updated (past tense)":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#partial_update":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#patched alias":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#destroy":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#delete alias":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#deleted alias":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testActionAliasesMapToVngActieValues#case-insensitive":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testRejectsUnknownAction":0,"Unit\\Service\\Notification\\VngNotificatiesEnvelopeTest::testTimestampDefaultsToNowInUtcWhenOmitted":0,"Unit\\Service\\Oas\\OasETagComputerTest::testETagIsQuotedAndDeterministic":0,"Unit\\Service\\Oas\\OasETagComputerTest::testStructurallyEquivalentSpecsHashIdentically":0,"Unit\\Service\\Oas\\OasETagComputerTest::testListOrderIsPreserved":0,"Unit\\Service\\Oas\\OasETagComputerTest::testIfNoneMatchWildcardMatchesAnyETag":0,"Unit\\Service\\Oas\\OasETagComputerTest::testIfNoneMatchSingleETagMatch":0,"Unit\\Service\\Oas\\OasETagComputerTest::testIfNoneMatchListMatch":0,"Unit\\Service\\Oas\\OasETagComputerTest::testIfNoneMatchAcceptsWeakETagPrefix":0,"Unit\\Service\\Oas\\OasETagComputerTest::testIfNoneMatchEmptyDoesNotMatch":0,"Unit\\Service\\Oas\\OasRequestValidatorTest::testValidBodyAgainstObjectSchemaReturnsEmptyErrors":0.035,"Unit\\Service\\Oas\\OasRequestValidatorTest::testMissingRequiredFieldYieldsError":0.001,"Unit\\Service\\Oas\\OasRequestValidatorTest::testWrongTypeYieldsError":0,"Unit\\Service\\Oas\\OasRequestValidatorTest::testEnumViolationYieldsError":0.001,"Unit\\Service\\Oas\\OasRequestValidatorTest::testEachErrorHasPathAndMessage":0,"Unit\\Service\\Oas\\ProblemDetailsBuilderTest::testMinimalProblemHasTypeTitleStatus":0,"Unit\\Service\\Oas\\ProblemDetailsBuilderTest::testProblemIncludesOptionalFields":0,"Unit\\Service\\Oas\\ProblemDetailsBuilderTest::testExtensionsAreIncludedButCannotOverwriteStandardFields":0,"Unit\\Service\\Oas\\ProblemDetailsBuilderTest::testValidationFailedHelperEmits422WithErrorsExtension":0,"Unit\\Service\\Oas\\ProblemDetailsBuilderTest::testNotFoundHelperEmits404":0,"Unit\\Service\\Oas\\ProblemDetailsBuilderTest::testConflictHelperEmits409":0,"Unit\\Service\\Oas\\ProblemDetailsBuilderTest::testContentTypeConstantIsRfc7807":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationEmitsOauth2SecurityBlock":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationSecurityIncludesAdminWhenGroupsEmpty":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationSecurityDeduplicatesAdmin":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationSecurityAdminFirst":0,"Unit\\Service\\OasServiceTest::testApplyRbacToOperationDoesNotMutateUnrelatedKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testGetObjectCacheMissLoadsFromDb":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testGetObjectReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testPreloadObjectsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testPreloadObjectsAllAlreadyCached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testPreloadObjectsBulkLoadFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testPreloadObjectsSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testGetStatsInitial":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testGetStatsAfterCacheMiss":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testSetObjectNameStoresInBothCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testSetObjectNameDistributedCacheFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearSearchCacheWithoutPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearSearchCacheWithPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearSearchCacheDistributedFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearAllCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearAllCachesDistributedFailures":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testClearCacheDelegatesToClearAllCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testInvalidateForObjectChangeCreate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testInvalidateForObjectChangeDelete":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testInvalidateForObjectChangeNullObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testInvalidateForObjectChangeNullSchemaId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerBranchCoverageTest::testConstructorHandlesCacheFactoryException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testConstructorHandlesCacheFactoryException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetObjectCacheHit":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetObjectCacheMissLoadsFromDb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetObjectCacheMissDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetStatsEmptyCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetStatsWithHitsAndMisses":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPreloadObjectsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPreloadObjectsAllCached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPreloadObjectsFromDb":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPreloadObjectsDbException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCacheObjectEviction":0.087,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearSearchCacheWithPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearSearchCacheNoPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearSearchCacheDistributedFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearAllCachesSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearAllCachesDistributedFailures":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearCacheDelegatesToClearAllCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testSetObjectNameSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testSetObjectNameDistributedFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testSetObjectNameEnforcesTtl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSingleObjectNameInMemoryHit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSingleObjectNameDistributedHit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSingleObjectNameDistributedFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetDistributedNameCacheCountNoCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetDistributedNameCacheCountWithValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetDistributedNameCacheCountNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetDistributedNameCacheCountException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearNameCacheSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testClearNameCacheDistributedFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeCreate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeDelete":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeDeleteDistributedFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeNullObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testInvalidateForObjectChangeWithExplicitIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSolrDashboardStatsNoContainer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetSolrDashboardStatsNoIndexService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCommitSolrNoIndexService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCommitSolrSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCommitSolrFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testCommitSolrException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testExtractDynamicFieldsFromObjectAllTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testExtractDynamicFieldsFromObjectWithPrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIsDateStringWithDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIsDateStringWithNonDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIsDateStringWithNonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testFormatDateForSolrValid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testFormatDateForSolrInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPersistNameCacheToDistributedNoCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPersistNameCacheToDistributedSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testPersistNameCacheToDistributedFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetMultipleObjectNamesEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetAllObjectNamesTriggersWarmup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testGetAllObjectNamesForceWarmup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIndexObjectInSolrNoService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIndexObjectInSolrServiceNotAvailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIndexObjectInSolrSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testIndexObjectInSolrFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testRemoveObjectFromSolrNoService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testRemoveObjectFromSolrSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerCoverageTest::testRemoveObjectFromSolrException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsIncludesDistributedNameCacheSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCacheWithoutDistributedCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateWithIndexService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeUpdateWithIndexService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateWithIndexFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateWithIndexUnavailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteWithIndexService":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteWithSolrFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeDeleteWithSolrException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeCreateUsesUuidWhenNameNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeNullObjectNullSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testInvalidateForObjectChangeSchemaDistributedCacheException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testSetObjectNameWithTtlWithinLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFindsOrganisationWithNullName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetSingleObjectNameFindAcrossAllSourcesNullObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesObjectWithNullNameUsesUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesMatchByNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesOrgWithNullNameUsesUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesBatchLoadsMagicTables":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheSkipsOrganisationsWithNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheWithMagicTables":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheWithMagicMappingEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheWithMagicTableQueryException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheWithMagicTablesOuterException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheSchemaFindException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testConstructorWithNullCacheFactoryAndUserSession":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSolrIndexForDashboardFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSolrIndexForDashboardException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCacheEvictionWhenExceedingMaxSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testCacheObjectWithNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPersistNameCacheToDistributedStoresEntries":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPersistNameCacheToDistributedWithoutCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPersistNameCacheToDistributedHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesNonUuidIdentifiers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesSchemaNotFound":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesMappingDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesAllColumnsFail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesSkipsNullAndEmptyNames":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSchemaRelatedCachesWithSchemaId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSchemaRelatedCachesWithoutSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectWithPrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectArrayWithMixedValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testIsDateString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testFormatDateForSolr":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testFormatDateForSolrInvalidDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesEmptyUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testPersistNameCacheToDistributedMetadataFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesMatchBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesMatchByUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectStringIsHandledAsString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectIntegerSuffix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testExtractDynamicFieldsFromObjectFloatSuffix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetStatsCalculatesQueryHitRate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testClearSearchCachePatternFiltersInMemoryCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesWithValidResults":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testBatchLoadNamesFromMagicTablesStopsWhenAllFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesColumnFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testQueryTableForNamesReturnsNameDirectly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testGetMultipleObjectNamesIntegratesBatchResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\CacheHandlerTest::testWarmupNameCacheStoresNameFromMagicTableRow":0,"Unit\\Service\\Object\\PermissionHandlerCacheTest::testRepeatedHasPermissionCallsHitCacheAndAvoidGroupLookup":0,"Unit\\Service\\Object\\PermissionHandlerCacheTest::testDifferentActionsAreCachedSeparately":0,"Unit\\Service\\Object\\PermissionHandlerCacheTest::testDifferentUsersAreCachedSeparately":0,"Unit\\Service\\Object\\PermissionHandlerCacheTest::testConditionalRulesEvaluatePerObjectUuid":0,"Unit\\Service\\Object\\PermissionHandlerCacheTest::testObjectOwnerIsPartOfCacheKey":0,"Unit\\Service\\Object\\PermissionHandlerCacheTest::testClearPermissionCacheInvalidatesEntries":0,"Unit\\Service\\Object\\PermissionHandlerCacheTest::testRbacBypassShortCircuitsBeforeCache":0,"Unit\\Service\\Object\\PermissionHandlerCustomScopeTest::testListenerVotingAllowGrantsCustomAction":0.001,"Unit\\Service\\Object\\PermissionHandlerCustomScopeTest::testListenerVotingDenyRejectsCustomAction":0,"Unit\\Service\\Object\\PermissionHandlerCustomScopeTest::testFirstVerdictWinsRegardlessOfRegistrationOrder":0,"Unit\\Service\\Object\\PermissionHandlerCustomScopeTest::testNoListenerVoteFallsThroughToStandardChain":0,"Unit\\Service\\Object\\PermissionHandlerCustomScopeTest::testCanonicalActionsSkipEventDispatch":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testSchemaAuthorizationOverridesRegister":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testRegisterFallbackWhenSchemaHasNoAuth":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testNeitherSchemaNorRegisterHasAuth":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testRoleExpansionViewerRole":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testRoleExpansionEditorRole":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testMixedRoleAndDirectAuth":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testUnknownRoleNameIsIgnored":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testManageActionEvaluated":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testManageActionDenied":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testAdminBypassesManageCheck":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testConditionalPublicRuleDelegatesToConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testConditionalRuleReturnsFalseWhenConditionMatcherReturnsFalse":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testUserIdVariableRuleDelegatesToConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testInOperatorRuleDelegatesToConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testOrganisationVariableFoldsIntoEnvelopeViaSelf":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testAdminBypassSkipsConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testOwnerBypassSkipsConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testSimpleStringRuleDoesNotInvokeConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testConditionalRuleWithoutMatchClauseDoesNotInvokeConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testAnonymousCallerAgainstNonPublicRuleReturnsFalseWithoutDelegation":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testPublicLteNowRuleMatchesPastPublishedAt":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testPublicLteNowRuleRejectsNullPublishedAt":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testPublicLteNowRuleRejectsExplicitNullPublishedAt":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testPublicLteNowRuleRejectsFuturePublishedAt":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testNowResolvesToSqlNativeFormat":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testNowAlignsWithSqlPathForIsoStoredDates":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testNowAlignsWithSqlPathForDateOnlyStored":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testCompositePublishedAndNotDepublishedRule":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testResolvedRelationUnwrappingViaRealConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRbacTest::testUnknownOperatorFailsClosedViaRealConditionMatcher":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testFlatRoleExpansion":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testMultipleRolesMergeGroups":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testRoleExtendsInheritsActions":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testMultiLevelRoleExtension":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testExtendsArraySupportsMultipleInheritance":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testCyclicExtendsLoggedAndContained":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testUnknownExtendsIgnoredButOwnActionsSurvive":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testAuthorizationWithoutRolesReturnedUntouched":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testUnknownRoleAssignmentLoggedAndSkipped":0,"Unit\\Service\\Object\\PermissionHandlerRoleHierarchyTest::testRegisterWithoutRoleDefinitionsLoggedAndSkipped":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionCascade":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionRestrict":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionSetNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionSetDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionNoAction":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionLowercase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testIsValidOnDeleteActionEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testCanDeleteNullSchemaReturnsDeletable":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testCanDeleteNoIncomingReferences":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testHasIncomingOnDeleteReferencesNoReferences":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testLogRestrictBlockLogsAuditTrail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testLogRestrictBlockEmptyBlockers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testApplyDeletionActionsEmptyAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testDeletionAnalysisToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testDeletionAnalysisEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceBranchTest::testCanDeleteHandlesSchemaLoadFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionWithValidActions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsValidOnDeleteActionWithInvalidActions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testValidOnDeleteActionsConstant":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractOnDeleteReturnsNullWhenNotSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractOnDeleteReturnsUppercaseValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractOnDeleteWithUppercaseValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractOnDeleteWithMixedCaseValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefReturnsNullWhenNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefReturnsDirectRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefReturnsArrayItemsRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefPrefersDirectRefOverItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testExtractTargetRefReturnsNullForEmptyItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefById":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefByUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefByPathWithBasename":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefReturnsNullWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefMatchesFirstSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefWithEmptySchemaList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsRequiredPropertyReturnsTrueWhenRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsRequiredPropertyReturnsFalseWhenNotRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsRequiredPropertyReturnsFalseWhenSchemaNotCached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testIsRequiredPropertyReturnsFalseWithNullSchemaCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsNullWhenNoDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsNullWhenPropertyMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsNullWhenSchemaNotCached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsNullWhenSchemaIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsFalseDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testGetDefaultValueReturnsZeroDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testHasIncomingOnDeleteReferencesReturnsTrueWhenExists":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testHasIncomingOnDeleteReferencesReturnsFalseWhenNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testHasIncomingOnDeleteReferencesReturnsFalseForDifferentSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteReturnsEmptyWhenObjectHasNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteReturnsEmptyWhenNoRelationsExist":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteReturnsEmptyWhenSchemaNotInIndex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteDetectsRestrictBlocker":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteDetectsCascadeTarget":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteDetectsSetNullTargetForNonRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteSetNullFallsBackToRestrictWhenRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteDetectsSetDefaultTargetWithDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultFallsBackToSetNullWhenNoDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteSetDefaultFallsToRestrictWhenNoDefaultAndRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteSkipsSoftDeletedObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteHandlesCycleDetection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteWithArrayReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteFiltersObjectsBySchemaInFallbackPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteFiltersObjectsByPropertyValueInFallbackPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteFiltersObjectsWithNullObjectData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteArrayRefFiltersMismatchedArrayValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteHandlesFindByRelationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteWithMultipleDependents":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteWithNoReferencingObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testWalkDeletionGraphRespectsMaxDepth":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockLogsBlockers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockHandlesEmptyBlockers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockDeduplicatesSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockWithNullSchemaId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogRestrictBlockHandlesBlockersWithMissingKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testLogIntegrityActionHandlesInsertException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsAppliesSetNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsAppliesSetNullForArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsSetNullHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsAppliesSetDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsSetDefaultHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsAppliesCascade":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsGroupsCascadeByRegisterSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsCascadeFallbackForNoRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsCascadeHandlesDeleteException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsLogsSetNullAuditTrail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsLogsSetDefaultAuditTrail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsLogsCascadeDeleteAuditTrail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsWithEmptyAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsExecutesInOrder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplySetNullDefaultsIsArrayToFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsCascadeSeparatesGroups":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplyDeletionActionsPassesTriggerSchemaSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexHandlesSchemaLoadException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexSkipsNoActionAndNoOnDelete":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexSkipsPropertiesWithoutRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexSkipsUnresolvableRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexHandlesNullProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexOnlyBuildsOnce":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexDetectsArrayType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexDetectsNonArrayType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testEnsureRelationIndexHandlesRegisterLoadException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefByIdAfterBasenameClean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testResolveSchemaRefByUuidAfterBasenameClean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteHandlesPropertyMissingFromObjectData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteArrayTypeWithNonArrayValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplySetNullArrayRemovesAllMatchingEntries":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplySetNullNonArrayWhenPropertyNotInData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteCollectsMultipleBlockersFromSameSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testCanDeleteCascadeWithNestedRestrict":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ReferentialIntegrityServiceTest::testApplySetNullArrayWhenPropertyIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnEmptyWhenSchemaFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnEmptyWhenNoSubFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnEmptyWhenPropertyHasNoInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnsNullWhenCallbackFindsNothing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterExtractsUuidFromRelatedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterExtractsUuidFromUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterHandlesNonUuidNonUrlValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterRemovesProcessedFilterKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsReturnsEmptyForNoObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsReturnsEmptyForNoExtendFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsExtractsSingleStringId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsExtractsArrayOfIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsDeduplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsEmptyStringsInArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsEmptyStringScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsLimitsArrayTo10Items":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsCircuitBreakerAt200":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedReturnsEmptyForNoIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedCapsAt200":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedIndexesByUuidAndId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedContinuesOnBatchError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testLoadRelationshipChunkOptimizedReturnsEmptyForNoIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testLoadRelationshipChunkOptimizedReturnsMapperResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testLoadRelationshipChunkOptimizedReturnsEmptyOnMapperException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractRelatedDataDelegatesToPerformanceHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractRelatedDataPassesFlagsCorrectly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsReturnsPaginatedContracts":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsUsesDefaultPagination":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsReturnsEmptyWhenNoContractsProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsReturnsErrorResponseOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsAppliesOffsetCorrectly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesReturnsEmptyWhenObjectHasNoRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesFiltersOutSelfReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesReturnsErrorResponseOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesUsesDefaultPagination":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesExtractsRelationsFromNestedArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByReturnsErrorResponseOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByUsesDefaultPaginationInErrorFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByResponseAlwaysHasRequiredKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsResponseAlwaysHasRequiredKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsLogsDebugWhenArrayTruncated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsNonStringArrayValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsNonStringScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsExactly200UniqueIds":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedProcessesMultipleBatches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedAllBatchesFail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedExactly200NoCap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsOffsetBeyondArrayReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsErrorResponseRespectsFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesDefaultPaginationWithEmptyQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesExtractsStringRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesResponseStructureOnError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterIntersectsMultipleInversedByProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterReturnsEmptyArrayOnlySimpleKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractRelatedDataWithEmptyResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedBySuccessPathStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterPassesRefAsSchemaToCallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterSetsNullRefWhenMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterExtractsUuidFromDeepUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testApplyInversedByFilterHandlesMultipleSubKeysForSameProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsCircuitBreakerWithMixedTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsMultipleExtendProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsMissingExtendProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testExtractAllRelationshipIdsDeduplicatesAcrossProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedWith300IdsProcessesOnly200":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedSingleId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testBulkLoadRelationshipsBatchedMultipleObjectsPerBatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testLoadRelationshipChunkOptimizedPassesMultipleIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsHandlesNonArrayContractsValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetContractsLimitExceedsTotal":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesWithRegisterSchemaIdsFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesIgnoresEmptyRelationValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByWithRegisterSchemaIdsFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsedByWithRbacDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RelationHandlerTest::testGetUsesHandlesNullRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntitiesRemovesSourceFromSelfInList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntitiesWithNullExtendAndFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveReferencedUuidsSimpleString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveReferencedUuidsObjectValueFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveReferencedUuidsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveReferencedUuidsMissingField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetInversedPropertiesReturnsPropertiesWithInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetInversedPropertiesDirectInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetInversedPropertiesSkipsEmptyInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testFilterExtendedInversePropertiesWithAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testFilterExtendedInversePropertiesSpecific":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testCollectEntityUuidsWithMixedEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testCollectEntityUuidsSkipsNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testExtractInverseConfigValidItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testExtractInverseConfigArrayInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testExtractInverseConfigMissingRefReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testExtractInverseConfigMissingInversedByReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testInitializeInverseCacheEntriesCreatesEmptyArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testInitializeInverseCacheEntriesPreservesExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIndexReferencingObjectsIndexesByUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIndexReferencingObjectsObjectValueFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIndexReferencingObjectsPopulatesObjectsCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesFromCacheArray":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesFromCacheSingle":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesFromCacheSingleEmptyCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesFromCacheSkipsNoInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRemoveQueryParametersRemovesQueryString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRemoveQueryParametersReturnsUnchangedWhenNoQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceWithQueryParamsNumeric":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceByUuidFallsToSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceSlugResolves":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceJsonSchemaPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testResolveSchemaReferenceFallsThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testCollectUuidsForExtendWithUrlValueNotUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testCollectUuidsForExtendDeduplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIsUuidLikeValid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIsUuidLikeUppercase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testIsUuidLikeInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetValueFromPathDeeplyNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testGetValueFromPathIntermediateNotArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesReturnsWhenSchemaNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testHandleInversedPropertiesReturnsWhenNoInversedProps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testPreloadInverseRelationshipsEmptyEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testPreloadInverseRelationshipsEmptyExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testPreloadInverseRelationshipsNonEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testPreloadInverseRelationshipsNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntityPropertyRbacInjectsSelf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntityWithEmptyObjectDataNoCrash":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectCoverageTest::testRenderEntityNullUuidNoCircularDetection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithUuidFallsThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithPathReference":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithPathReferenceException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceWithQueryParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveSchemaReferenceSlugMultipleResults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRemoveQueryParametersNoParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRemoveQueryParametersWithParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testInitializeInverseCacheEntries":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testInitializeInverseCacheEntriesPreservesExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsWithValueFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsNoDuplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsWithArrayReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testIndexReferencingObjectsNonMatchingUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testCollectEntityUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testCollectEntityUuidsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testHandleInversedPropertiesFromCacheWithData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testHandleInversedPropertiesFromCacheEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testHandleInversedPropertiesFromCacheSingleProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveReferencedUuidsSimpleString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveReferencedUuidsObjectFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveReferencedUuidsArrayValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testResolveReferencedUuidsMissingField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesEmptyList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesConvertsStringExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesConvertsStringFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesConvertsStringFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesConvertsStringUnset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesNullExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testRenderEntitiesProcessesMultipleEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetSchemaCaching":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetSchemaNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetRegisterCaching":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetRegisterNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigFromItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigArrayInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigFromTopLevel":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testExtractInverseConfigNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testPreloadInverseRelationshipsEmptyEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testPreloadInverseRelationshipsNoMatchingExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetInversedProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectDeepTest::testGetInversedPropertiesEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectFromLocalCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectCachesResultByUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetObjectsCacheSkipsNonUuidStringKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigArrayWithNoItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIsFilePropertyConfigObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathReturnsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendUsesBaseProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendSkipsNonUuidArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesEmptyInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsSkipsNonObjectEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigItemsRefFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testInitializeInverseCacheEntriesDoesNotOverwrite":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityNormalizesRegisterShorthand":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendSelfSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersWithParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersWithoutParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersMultipleParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceNumericWithQueryParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceByUuidFallsToSlugLookup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceByUuidNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceBySlugPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceBySlugDirect":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceFallthrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesWithFileRecords":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesFilterOutObjectTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesEmptyRecords":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesNoTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesHydratesFileId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesArrayOfFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesSkipsMetadataProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesNoSchemaReturnsUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesInitializesEmptyArrayProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyNonArrayNonNumericReturnsUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArrayPropertyNonArrayValueReturnsUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertySingleFileNumeric":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertySingleFileDigitString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullForZeroId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullForNegativeId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullWhenFileNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullForEmptyContent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64WithGenericMimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileObjectReturnsFormattedArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileObjectReturnsNullForEmptyResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileObjectReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileObjectReturnsNullForNonNumericInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsReturnsTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsFiltersObjectTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsReturnsEmptyWhenNoTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsReturnsEmptyWhenFileNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesSetsImage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesFallsBackToAccessUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesSetsNullForEmptyField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataNoObjectImageField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringUnset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithNullExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesClearsSourceFromSelf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadsObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithOnlyValidEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendBothSelfRegisterAndSchema":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithNullUuidNoCircularDetection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRbacWithExistingSelf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsAvoidsDuplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsObjectValueFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsArrayOfUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheArrayProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheEmptyCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheDirectInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheSkipsNoInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsEmptyEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsEmptyExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNonObjectEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityWithEmptyObjectData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityFilterWithNonExistentKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityFieldFilterAlwaysIncludesSelfAndId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityUnsetNonExistentKeyNoError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityPreloadedObjectsPopulateCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesMultipleFilesMultipleTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesMissingMimetypeFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyBase64Single":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyBase64Array":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyBase64ArraySkipsNulls":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArrayNonBase64":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArraySkipsNullFileObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringFilter":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectUuidsForExtendWithUrlContainingUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsNoWildcards":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsAtDepthLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsWithNumericKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsWithNonIterableRoot":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleWildcardExtendsWithStringOverrideKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithNonExistentKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotSkipsAtPrefixedKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayContainingAlreadyExtendedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayContainingNonArrayNonStringItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithUuidStringNotInCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithUuidStringFoundInCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotSkipsUnderscorePrefixedValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotSkipsAtPrefixedValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithUrlValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayOfUuidsOneNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayFilterNullAndAtValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotCircularReferenceInArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithAllFlagAddsAllToSubExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithStringOverrideKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithArrayStringOverride":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtendObjectWithSelfRegisterAndSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtendObjectPreloadsUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtendObjectWithAllFlag":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendAllPopulatesFromObjectKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendAllSkipsIdAndOriginId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityStringExtendParsedToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRbacTemporarySelfIsRemoved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityHandlesInversePropertiesWhenExtended":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityNoInversePropsWhenNotExtended":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheDirectArrayType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheEmptyRenderedSingleValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadSkipsNonObjectEntityInUuidCollection":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithRbacAndMultitenancyFlags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64EmptyMimeTypeUsesFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64WithStringFileId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataDeepNestedPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesExceptionReturnsEntityUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesSkipsUnconfiguredProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathWithFileExtension":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathWithFragment":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesNullExtendDefaultsToEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithNestedKeyExtends":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNoInversedProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNoSchemaFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNoExtendMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadInverseRelationshipsNoEntityUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadSingleInversePropertyInvalidConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadSingleInversePropertyEmptySchemaId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadSingleInversePropertyTargetSchemaNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetValueFromPathNullValueInPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyBase64SingleReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertySingleReturnsNullWhenFileNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataStringValueSetsImageNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataArrayWithoutUrls":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotCircularReferenceForSingleValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotUrlValueResolvesLastSegment":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadSkipsNonArrayObjectData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadStoresObjectsByBothKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathLookupExceptionFallsThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityCommaStringExtendIsParsed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotArrayWithAllFlagAddsAllToSubExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityInverseWithAllExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityShouldHandleInverseMatchesSpecificProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityInverseWithStringExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testPreloadSingleInversePropertySchemaNotFoundAfterResolve":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityExtendAllSkipsValuesEqualToId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotWithNestedDotObjectValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleExtendDotDeepNestedRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceUuidBranch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathNoMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceSlugMultipleMatches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetRegisterReturnsCachedRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaReturnsCachedSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetSchemaReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersStripsQueryString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersNoQueryStringUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigWithItemsRefAndInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigWithArrayInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMissingRefReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMissingInversedByReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsWithObjectPrefixFiltered":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsWithNoTagsReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileTagsWithEmptyTagIdsReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesDirectlyWithTagsViaPrivateMethod":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilesFiltersObjectPrefixTags":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderFilePropertiesInitializesArrayFileProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyNonNumericStringUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArrayWithBase64Format":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateFilePropertyArrayNonArrayValueUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64WithZeroFileIdReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64WithNegativeFileIdReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64FileNotFoundReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64EmptyContentReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetFileAsBase64ReturnsCorrectDataUri":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesSetsImageFromDownloadUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesFallsBackToAccessUrlViaPrivate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesSetsNullWhenNoUrls":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesNoImageFieldConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitySchemaNormalization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRegisterNormalization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesFromCacheSinglePropertyEmptyCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsSkipsNonEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsSkipsEntityWithNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesWithEmptyArrayReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigFromItemsRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigFromDirectRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigMultiFieldInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigReturnsNullWhenMissingRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testExtractInverseConfigReturnsNullWhenMissingInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsFromValidEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testCollectEntityUuidsEmptyArrayReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesMatchesExactName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesAllFlagIncludesAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testFilterExtendedInversePropertiesNoMatchReturnsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testInitializeInverseCacheEntriesSetsEmptyArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testInitializeInverseCacheEntriesPreservesExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsSimpleStringRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsHandlesValueObjectFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsMatchesArrayUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsDeduplicatesMultiField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testIndexReferencingObjectsPopulatesObjectsCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesFromItemsInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesFromDirectInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testGetInversedPropertiesSkipsEmptyInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathRefFindsSchemaBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferencePathRefCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringExtendToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringFieldsToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringFilterToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesConvertsStringUnsetToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesRemovesSourceFromSelf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntitiesBatchPreloadsWhenExtendProvided":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersWithoutQueryStringReturnsAsIs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRemoveQueryParametersStripsComplexQueryString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHydrateMetadataFromFilePropertiesCatchesExceptionGracefully":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesReturnsEarlyWhenSchemaNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesReturnsEarlyWhenNoInversedProps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceReturnsNumericIdAsIs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceStripsQueryParamsBeforeNumericCheck":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testResolveSchemaReferenceSlugFilterMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testHandleInversedPropertiesUsesCacheWhenAvailable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testShouldExtendFilesRecognizesCanonicalAndShorthand":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityEmitsLightweightFileIdsWhenExtendOmitsFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testRenderEntityRoutesThroughRenderFilesWhenExtendCoversFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testSetLightweightFileIdsCacheMissFallsThroughToSingleLookup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\RenderObjectTest::testSetLightweightFileIdsCacheHitReturnsCachedValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\MetadataHydrationHandlerDeprecationTest::testObjectPublishedFieldTriggersDeprecationWarning":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\MetadataHydrationHandlerDeprecationTest::testObjectDepublishedFieldTriggersDeprecationWarning":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\MetadataHydrationHandlerDeprecationTest::testAutoPublishTriggersDeprecationWarning":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\MetadataHydrationHandlerDeprecationTest::testMultipleDeprecatedKeysEachTriggerWarning":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\MetadataHydrationHandlerDeprecationTest::testNonDeprecatedKeysDoNotTriggerWarning":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObject\\MetadataHydrationHandlerDeprecationTest::testDeprecationWarningSuggestsRbacMigration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectCircularReferenceTest::testPushAndPopMaintainsBalancedStack":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectCircularReferenceTest::testEmptyUuidPushReturnsNull":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectCircularReferenceTest::testDuplicatePushIsRejectedWithoutCorruption":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectCircularReferenceTest::testDetectCircularReferenceReturnsCycleWhenUuidOnStack":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectCircularReferenceTest::testValidateReferenceExistsThrowsCircularReferenceException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectCircularReferenceTest::testCircularExceptionIsValidationButNotReferenceValidationSubclass":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceCacheHit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceCleanedCacheHit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceUuidNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferencePathBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceFindAllException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceDirectSlugMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveSchemaReferenceCachesNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceUuidNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceUrlPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceFindAllException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveRegisterReferenceDirectSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testRemoveQueryParametersNoParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testRemoveQueryParametersStrips":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceStandardUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceUuidNoDashes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferencePrefixedUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferencePrefixedUuidNoDashes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceIdentifierPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceCommonWord":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceShortText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsReferenceWithWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsObjectPropertyType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsTextUuidFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsArrayOfObjectsWithStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsNonObjectArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsWithPrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsSkipsNumericKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testScanForRelationsSchemaCatchesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultAlways":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultFalsyEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultFalsyEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultFalsyNonEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testShouldApplyDefaultExistingValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueSimpleRefFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueSimpleRefNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValuePreservesArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueComplexTemplate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueNonTemplate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueNumeric":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testResolveDefaultTemplateValueException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testCreateSlugSimple":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testCreateSlugSpecialChars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testCreateSlugLongTextTruncation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testCreateSlugTrimsTrailingHyphens":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGenerateSlugNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGenerateSlugWithConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGenerateSlugMissingFieldValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGenerateSlugException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetValueFromPathNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetValueFromPathMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetValueFromPathConvertsToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsEffectivelyEmptyObjectEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsEffectivelyEmptyObjectOnlyMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsEffectivelyEmptyObjectWithData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testIsEffectivelyEmptyObjectEmptyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataImageArrayDownloadUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataImageArrayAccessUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataNumericImageValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataStringImageUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataInvalidPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataInvalidDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testHydrateObjectMetadataImageArrayNumericFileIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testClearAllCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testTrackCreatedSubObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testClearCreatedSubObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetCachedSchemaFetchesAndCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testGetCachedRegisterFetchesAndCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyAlwaysDefaultsNoAlwaysDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyAlwaysDefaultsAppliesAlwaysDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyAlwaysDefaultsSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyAlwaysDefaultsNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyPropertyDefaultsAppliesForMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyPropertyDefaultsSkipsExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testApplyPropertyDefaultsSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testUpdateObjectRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataPublishedEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataPublishedNotPresent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataInvalidPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataDepublishedEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataSlugFromData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectDeepTest::testSetSelfMetadataDepublishedNotPresent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testAdminUserBypassesReferenceValidationWhenFlagDefaultOn":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testAdminUserDoesNotBypassWhenFlagDisabled":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testNonAdminUserNeverBypassesEvenWhenFlagOn":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testBypassDisabledWhenGroupManagerMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testReferenceValidationFailedEventDispatchedOnMissingTarget":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testReferenceValidatedEventDispatchedOnSuccessfulLookup":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testNoEventsDispatchedWhenDispatcherMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testValidateReferenceExistsPassesIncludeDeletedFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testRequestScopedCachePreventsDuplicateLookups":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testRequestScopedCacheReplaysNegativeVerdictWithoutDuplicateEvents":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testClearReferenceValidationCacheForcesRevalidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testStrictnessWarnDoesNotThrowOnMissingReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testStrictnessWarnStillDispatchesFailureEvent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testStrictnessErrorRejectsOnMissingReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testStrictnessBlockRejectsOnMissingReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testStrictnessFieldDowngradesBooleanTrueToWarn":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testStrictnessFieldOffDisablesValidationEvenWhenBooleanTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectReferenceValidationTest::testStrictnessFalseDisablesValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetCreatedSubObjectsReturnsEmptyArrayInitially":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testTrackCreatedSubObjectAddsSubObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testTrackMultipleSubObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearCreatedSubObjectsClearsSubObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearAllCachesClearsEverything":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithEmptyData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsFindsUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsFindsUrls":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsFindsNumericIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsSkipsNonStringKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsSkipsPlainTextValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithPrefixedUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithUuidWithoutDashes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithNestedArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithArrayOfUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithPrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsSkipsEmptyStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithSchemaPropertyTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithTextUuidFormatProperty":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithArrayOfObjectsSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithStandardUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithUuidWithoutDashes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithPrefixedUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithPrefixedUuidNoDashes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithPlainText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithCommonWord":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithOpenSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithIdentifierLikeString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testRemoveQueryParametersWithNoParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testRemoveQueryParametersStripsParams":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathSimple":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathDeeplyNested":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathReturnsNullForMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathReturnsNullForMissingNestedKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathConvertsIntToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetValueFromPathReturnsNullForNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugBasic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugWithSpecialChars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugTrimsHyphens":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugLimitsLength":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCreateSlugWithNumbers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithAllEmptyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithNonEmptyValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectSkipsMetadataKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithNestedEmptyObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithNestedNonEmptyObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNonEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithIndexedArrayAllEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithIndexedArraySomeNonEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultAlwaysBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultAlwaysBehaviorEvenWithValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultFalsyBehaviorWithValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultDefaultBehaviorWithMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultDefaultBehaviorWithNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultDefaultBehaviorWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testShouldApplyDefaultDefaultBehaviorWithValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueNonTemplate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueSimplePropertyRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueSimpleRefPreservesArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueSimpleRefMissingProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueComplexTemplate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueNonStringValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveDefaultTemplateValueNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsAuditTrailsEnabledReturnsTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsAuditTrailsEnabledReturnsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsAuditTrailsEnabledDefaultsToTrueOnMissingSetting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsAuditTrailsEnabledDefaultsToTrueOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsNoAlwaysProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsAppliesAlwaysDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsSkipsNullDefaultValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsWithTemplateValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsReturnsDataWhenNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyAlwaysDefaultsReturnsDataOnSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsAppliesDefaultWhenMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsDoesNotOverrideExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsWithFalsyBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsWithAlwaysBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsReturnsDataOnSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsReturnsDataWhenNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataDelegatesToHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageFieldString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageFieldFileObjectsDownloadUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageFieldFileObjectsAccessUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithPublishedField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithInvalidPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithDepublishedField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithInvalidDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithEmptyPublishedValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithNumericImageValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataWithSelfMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataWithExplicitUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataWithIdInData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataNormalizesEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataProcessesUploadedFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataNoUploadedFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithEntities":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithIntIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithNullRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithSlugField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithNoSlugField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithMissingFieldValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithNestedField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceCachedResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceBySlugViaFindAll":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceByPathReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceWithQueryParameters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceNotFoundCachesNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceNumericId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceByUrlPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsSlugFromSelfData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsSlugFromData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsPublishedToNullWhenEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataHandlesInvalidPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsDepublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsDepublishedToNullWhenMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsOrganisation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetCachedSchemaFetchesAndCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGetCachedRegisterFetchesAndCaches":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateObjectRelationsSetsRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectReturnsNullWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectReturnsEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectThrowsOnLockedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectAllowsLockOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForObjectPropertiesWithObjectProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForObjectPropertiesEmptyObjectNonRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForObjectPropertiesEmptyObjectRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForArrayPropertyWithEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForArrayPropertyWithEmptyStringItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForScalarPropertyNonRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsForScalarPropertyRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsReturnsDataOnSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSanitizeEmptyStringsSkipsMissingProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFillMissingSchemaPropertiesWithNullAddsNullForMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFillMissingSchemaPropertiesDoesNotOverrideExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFillMissingSchemaPropertiesReturnsDataOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameCachesName":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameReturnsEarlyOnNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameFallsBackToNaam":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameHandlesHydrationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearImageMetadataIfFilePropertyClearsImage":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearImageMetadataIfFilePropertyDoesNotClearNonFileProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testClearImageMetadataIfNoImageFieldConfigured":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesAppliesDefaultWhenMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesDoesNotOverrideExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithConstValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithFalsyBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithAlwaysBehavior":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithTwigSimpleRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesWithTwigComplexTemplate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesReturnsDataWhenNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesReturnsDataOnSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesGeneratesSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesSkipsSlugWhenAlreadyPresent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesNonTemplateValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsWhenEmptyProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsPropertyWithoutValidateReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsUnchangedValueOnUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsSoftDeletes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsHandlesDoesNotExist":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsHandlesGenericException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsUsesSystemUserWhenNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsReturnsEarlyWhenNoRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsNonUuidRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenNoPropertyConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsNullRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsReturnsEmptyForNonList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsReturnsEmptyForEmptyValidObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsSkipsExistingUuids":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsReturnsEmptyWhenNoRefInItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsCopiesRefFromPropertyLevel":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectReturnsNullWithNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectReturnsNullForEmptyObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectReturnsNullForEmptyIdOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectReturnsNullWhenParentHasNoUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeSingleObjectThrowsOnInvalidSchemaRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleObjectUpdateReturnsUnpersistedWhenPersistFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleObjectCreationReturnsUnpersistedWhenPersistFalse":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testProcessFilePropertiesWithRollbackNoFileProps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testProcessFilePropertiesWithRollbackDeletesOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeObjectsReturnsDataOnSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeObjectsSkipsPropertiesNotInData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeObjectsSkipsEmptyPropertyValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeObjectsSkipsWriteBackEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleInverseRelationsWriteBackReturnsDataOnSchemaException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleInverseRelationsWriteBackSkipsEmptyPropertyValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHandleInverseRelationsWriteBackRemovesAfterWriteBack":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectAllowsNonArrayLock":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testFindAndValidateExistingObjectAllowsNullLockOwner":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithStringSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterThrowsOnInvalidStringSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsUpdatesRelatedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenAlreadyRelated":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsNoRefInProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsHandlesSchemaResolutionFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsUsesItemsRefForArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceByUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceDirectFindFallsToSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataSetsDepublishedToNullWhenEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataHandlesInvalidDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithArrayOfFileIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataPublishedFieldNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataDepublishedFieldNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithArrayOfObjectsAndNestedArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithTextUriFormatProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithTextUrlFormatProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithShortHyphenatedString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithCommonWordClosedSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithUnderscoreId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesUsesMergedContextForTwig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetDefaultValuesNullTemplateSourceMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaAndRegisterWithStringRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataPrefersExplicitUuidOverSelfId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testExtractUuidAndSelfDataWithNonArraySelf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceCleanedCachedResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveSchemaReferenceDirectSlugMatchAsLastResort":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceDirectSlugLastResort":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataPreservesPublishedWhenNotInSelfData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageArrayOfNumericIds":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithImageFieldNullValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithEmptyDepublishedValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithNullPublishedValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithSystemsoftwareCommonWord":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithClosedSource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithShortString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsWithTemplateValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsSkipsNullDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testApplyPropertyDefaultsWithNullResolvedValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsReturnsWhenSchemaCannotBeResolved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsThrowsValidationExceptionWhenObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsPassesWhenObjectFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsLogsWarningOnGenericException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferenceExistsWithNullRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesCallsValidateForArrayProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesSkipsEmptyArrayValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testValidateReferencesValidatesSingleProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithBooleanFalseValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithZeroValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithSchemaExceptionReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithArrayOfObjectsContainingNestedArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithIntegerZeroValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithFalseValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsEffectivelyEmptyObjectWithMixedKeysIncludingMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNonEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsValueNotEmptyWithNestedEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithUnderscoreIdentifier":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testIsReferenceWithOnlySpaces":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsWithTextUrlFormatNonUuidValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testScanForRelationsArrayOfObjectsWithNestedArrayItem":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenAlreadyInRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenTargetSchemaNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsSkipsWhenNoRefInProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testUpdateInverseRelationsHandlesItemsRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataWithNullPublishedKeepsExistingNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSetSelfMetadataWithBoolPublishedTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsCopiesInversedByFromPropertyLevel":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testCascadeMultipleObjectsCopiesRegisterFromPropertyLevel":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSaveObjectCreatesNewEntityWithPersistFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testSaveObjectUpdatesExistingEntityWithPersistFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testResolveRegisterReferenceUuidWithDoesNotExist":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testGenerateSlugWithEmptyFieldValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testPreCacheParentNameWithNoNameAndNoFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testDeleteOrphanedRelatedObjectsWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectTest::testHydrateObjectMetadataWithEmptyArrayImageValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsBranchCoverageTest::testSaveObjectsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsBranchCoverageTest::testSaveObjectsMixedSchemaNoValidObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsBranchCoverageTest::testSaveObjectsDeduplicatesById":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsBranchCoverageTest::testSaveObjectsWithoutDeduplication":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testInitializeSaveResultCreatesCorrectStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSingleObjectRelation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsArrayOfObjectRelation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsNonExistentTarget":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsMissingSchemaAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsDoesNotAddDuplicate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsPropertyNotInObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsConvertsNonArrayExistingValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsArrayWithNonStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithPrefix":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSkipsNonStringKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithNestedArrayContainingArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaTextUriFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaTextUrlFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSkipsEmptyStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSkipsEmptyArrayValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsNonObjectArrayWithStringReferences":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsArrayOfObjectsWithNestedArrays":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithShortString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithHttpUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithFtpUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary5000":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary10000":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary50000":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeJustAbove50000":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithSchemaIdInsteadOfObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaNoUser":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithSelfData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithObjectProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithPublishedDateString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithInvalidPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithAutoPublish":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaAutoPublishWithCsvPublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaEnrichDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaTextNonRelationFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaPropertyTypeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsNestedObjectWithinNonObjectArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithNullSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsSchemaObjectPropertyWithUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithLongIdNoHyphenOrUnderscore":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithSystemsoftwareCommonWord":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testIsReferenceWithClosedSourceCommonWord":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsArrayRelationWithNonArrayExistingValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsArrayDoesNotAddDuplicate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsObjectWithEmptyId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsObjectWithNullId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsSkipsObjectWithNullSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsMultipleObjectsMultipleInverseProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithDepublishedDateString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithInvalidDepublishedDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaMultipleObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaPreservesExistingSelfValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithObjectPropertyAndRelations":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsMixedSchemaWithUpdatedAndUnchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsMixedSchemaWithPartialInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsDeduplicationDisabledPassesAllObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaAutoPublishFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithProvidedIdInSelf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithNoSelfData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaLegacyMetadataRemoval":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeOneObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary101":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary1001":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary5001":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testCalculateOptimalChunkSizeBoundary10001":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testPerformComprehensiveSchemaAnalysisReturnsCorrectStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsWithEmptyKeySkipped":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithBothPublishedAndDepublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithWhitespaceId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLoadSchemaWithCacheStringId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testLoadRegisterWithCacheStringId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testHandleBulkInverseRelationsWithNoSelfData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithRegisterAsInt":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsDeepNesting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testScanForRelationsArrayOfObjectsWithEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithNonStringPublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsSingleSchemaWithNullSelfPublished":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsWithValidationEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsWithEventsEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsResultHasAggregateStatisticsKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SaveObjectsTest::testSaveObjectsReturnsErrorWhenNoObjectsPrepared":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#numeric string passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#integer DB value coerces to string":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#float DB value coerces to string":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#boolean-literal \"true\" stays string":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#boolean-literal \"false\" stays string":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#null-literal \"null\" stays string":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#numeric-shaped string stays string":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#quoted-string JSON keeps its quotes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#array-shaped string DECODED for compat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#object-shaped string DECODED for compat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testStringCoercion#plain text passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#native true passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#native false passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#MariaDB int 1 coerces to true":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#MariaDB int 0 coerces to false":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#string \"1\" coerces to true":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#string \"0\" coerces to false":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#string \"true\" coerces to true":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#string \"TRUE\" (upper) is truthy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#string \"yes\" coerces to true":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#string \"no\" coerces to false":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#random string coerces to false":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#empty string coerces to false":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#string \"on\" is FALSE (per design D7)":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#float 0.0 coerces to false":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testBooleanCoercion#float 1.5 coerces to true":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testIntegerCoercion#numeric string coerces to int":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testIntegerCoercion#integer passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testIntegerCoercion#float string truncates to int":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testIntegerCoercion#non-numeric stays unchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNumberCoercion#integer DB value coerces to float":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNumberCoercion#decimal string coerces to float":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNumberCoercion#integer string coerces to float":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNumberCoercion#non-numeric stays unchanged":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testArrayCoercion#JSON-string array decodes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testArrayCoercion#JSON-string object decodes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testArrayCoercion#already-array passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testArrayCoercion#already-list passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testArrayCoercion#invalid JSON returns string for validator":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testObjectCoercion#JSON-string array decodes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testObjectCoercion#JSON-string object decodes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testObjectCoercion#already-array passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testObjectCoercion#already-list passes through":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testObjectCoercion#invalid JSON returns string for validator":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNullPreserved#string":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNullPreserved#boolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNullPreserved#integer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNullPreserved#number":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNullPreserved#array":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNullPreserved#object":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testNullPreserved#mystery":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testUnknownSchemaTypePassesPlainStringThrough":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testUnknownSchemaTypeDecodesArrayShapedStringForCompat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testUnknownSchemaTypeCastsNumericInputToString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\SchemaTypeConverterTest::testEmptySchemaTypeUsesStringFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueFromArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testGetMixedValueString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsWithObjectConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsWithArrayConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractHandlingFromOneOfItemsNoHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingDirect":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingFromItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingFromItemsOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingFromPropertyOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testExtractObjectConfigurationHandlingNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformObjectPropertyNoHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformObjectPropertyRelatedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformObjectPropertyNestedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformToUuidPropertyWithInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformToUuidPropertyWithoutInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformSchemaForValidationNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformOpenRegisterObjectConfigurationsNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformPropertyStripsRefFromStringType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformPropertyInversedByArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectBranchCoverageTest::testTransformPropertyInversedByObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testRemoveQueryParametersWithQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testRemoveQueryParametersWithoutQuery":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetMixedValueFromArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetMixedValueFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetMixedValueReturnsNullForMissingKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetMixedValueReturnsNullForNonArrayNonObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeBool":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGetValueTypeObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceWithStringRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceWithObjectRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceWithArrayRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceReturnsFalseForDifferentSlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceReturnsFalseWithNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testIsSelfReferenceWithQueryParameters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformFileType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformDatetimeType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformTypeAsArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformTypeNoTypeSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformStandardTypeUntouched":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsNonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsMovesEnumToItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsMovesOneOfToItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsOneOfAsObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFixMisplacedArrayConstraintsExistingItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsWithConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsWithArrayConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractHandlingFromOneOfItemsNoHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingDirect":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingFromItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingFromItemsOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingFromDirectOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testExtractObjectConfigurationHandlingReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationNonObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationRelatedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationNestedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationWithRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformArrayItemsForValidationNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToUuidPropertyWithoutInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToUuidPropertyWithInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToUuidPropertyWithInversedByEmptyProps":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToNestedObjectPropertyNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToNestedObjectPropertyWithObjectRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToNestedObjectPropertyWithStringRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformToNestedObjectPropertyWithArrayRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformOpenRegisterObjectConfigurationsNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testCleanPropertyForValidationNonObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformPropertyForOpenRegisterInversedByArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformPropertyForOpenRegisterInversedByObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformPropertyForOpenRegisterStripsRefFromString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformPropertyForOpenRegisterArrayItemsInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testResolveSchemaLocalSchemaPath":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testResolveSchemaFileSchemaPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testResolveSchemaExternalNotAllowed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testHandleCustomValidationException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testGenerateErrorMessageForValidResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testPreprocessSchemaReferencesSkipsUuidItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testPreprocessSchemaReferencesProcessesItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationSelfRefRelatedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationSelfRefRelatedObjectWithInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationSelfRefArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationSelfRefArrayItemsWithInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testTransformSchemaForValidationRemovesSchemaId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testCleanSchemaForValidationRemovesMetadata":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFindSchemaBySlugDirectMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFindSchemaBySlugCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFindSchemaBySlugNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectCoverageTest::testFindSchemaBySlugFindAllFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugDirectMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugCaseInsensitiveFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugNullWhenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugNullWhenFindAllThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFindSchemaBySlugDirectMatchThrowsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithObjectRefFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithArrayRefFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertySuccessfulResolutionObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertySuccessfulResolutionNonObjectType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyWithRefQueryParameters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyRefNotComponentsSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterInversedByArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterInversedByObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterInversedByEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterArrayItemsInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterArrayItemsObjectNoInversedBy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterDirectObjectProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformPropertyForOpenRegisterRecursiveNestedProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertySelfReference":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertyWithObjectRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertyWithArrayRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToNestedObjectPropertyNonSchemasRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToUuidPropertyWithInversedByAndRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformToUuidPropertyWithInversedByEmptyProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationSelfReferenceWithObjectConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformSchemaForValidationSelfReferenceNoHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingFromDirectOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingFromDirectObjectConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingObjectConfigNoHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractObjectConfigurationHandlingItemsConfigNoHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationAsArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationWithNestedItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsOneOfAsObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsDoesNotOverrideExistingOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsEmptyEnum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testFixMisplacedArrayConstraintsEmptyOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsNoTypeSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectWithRefNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectNoConfigNoRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformArrayItemsObjectConfigWithObjectFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeEmail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypePhone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformCustomTypeArrayMixed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMinLengthError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMinLengthEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMaxLengthError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMinimumError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMaximumError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForPatternError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMinItemsError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForMaxItemsError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForTypeErrorArrayExpected":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForTypeErrorEmptyStringOnRequired":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsWithArrayConfigDuplicate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsWithArrayConfigNoDuplicate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEnumFieldNullNotAllowed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEnumFieldNullAllowed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyArrayFilteredForNonRequiredNoConstraints":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyArrayKeptForMinItemsConstraint":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectNullAllowedForOptionalWithTypeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesWithItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesSkipsUuidTransformedItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesPropertyWithRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformObjectPropertyForOpenRegisterDefaultHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformObjectPropertyForOpenRegisterNestedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformObjectPropertyForOpenRegisterNullHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForZero":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testHandleValidationExceptionResponseStatus":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testHandleCustomValidationExceptionMultipleErrors":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationTransformsCustomTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationFixesMisplacedEnumOnArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithNonComponentsRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithObjectRefNoId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testIsSelfReferenceWithArrayRefNoId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectRemovesDollarIdBeforeValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectKeepsZeroForOptionalIntegerField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectKeepsFalseForOptionalBooleanField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformOpenRegisterObjectConfigurationsWithProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsObjectConfigNoHandling":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testExtractHandlingFromOneOfItemsWithObjectIterable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaLocalApiSchemas":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaFileApiFilesSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaExternalNotAllowedReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaExternalDisallowedReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageTypeErrorExpectsObjectGotEmptyArray":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageTypeErrorExpectsArrayGotEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageTypeErrorExpectsStringGotEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForFormatError":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageForSemverFormatError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageDefaultKeywordWithSubErrors":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGenerateErrorMessageEnumWithNonArrayValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsArrayConfigDuplicateThrowsTypeError":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsArrayConfigNoDuplicate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyArrayKeptForUniqueItemsConstraint":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyArrayKeptForMaxItemsConstraint":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeForResource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectWithSchemaEntityNoProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEnumNullRemovedFromObjectForNonRequiredField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateObjectEmptyRequiredArrayGetsUnset":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanSchemaForValidationAsArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testCleanPropertyForValidationWithOneOf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaPropertyArrayItemsSuccessfulResolution":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformOpenRegisterObjectConfigurationsWithArrayProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesWithOneOfRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaReturnsJsonForLocalSchemaUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaHandlesFileApiUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsWithStringConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsThrowsTypeErrorOnArrayConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testValidateUniqueFieldsReturnsEarlyWhenNoConfig":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeReturnsUnknownForOpenResource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testGetValueTypeReturnsUnknownForClosedResource":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testTransformOpenRegisterObjectConfigurationsNoop":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesWithoutProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testPreprocessSchemaReferencesWithAllOfRef":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Object\\ValidateObjectTest::testResolveSchemaHandlesExternalUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testSaveObjectsEmptyInputReturnsEmptyResult":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testInitializeSaveResultStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectHandlers\\SaveObjectsRefactoredMethodsTest::testCalculateOptimalChunkSizeSmall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testGetObjectReturnsNullWithoutContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetRegisterWithRegisterObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetRegisterWithSlugString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetRegisterWithNumericIdUsesCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetRegisterWithNumericIdCacheFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetSchemaWithSchemaObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetSchemaWithNumericIdUsesCache":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetSchemaWithSlugString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetSchemaThrowsOnNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetObjectWithEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testSetObjectWithIdNoContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testPrepareFindAllConfigExtendsStringToArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testPrepareFindAllConfigSetsRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testPrepareFindAllConfigSetsSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testExtractUuidSelfId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testExtractUuidIdField":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testExtractUuidExplicitTakesPriority":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testExtractUuidSkipsEmptyId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testCheckSavePermissionsNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testCheckSavePermissionsCreateForNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testCheckSavePermissionsUpdateForExistingUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testCheckSavePermissionsCreateWhenUuidNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testNormalizeDateValuesNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testNormalizeDateValuesConvertsDatetime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testNormalizeDateValuesSkipsValidDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testNormalizeDateValuesSkipsNonDateFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderNullUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderExistsNullFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderExistsEmptyString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderExistsFolderNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testEnsureObjectFolderExistsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testFindByRelationsDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testFindByRelationsExactMatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testGetActiveOrganisationForContextReturnsUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testGetActiveOrganisationForContextReturnsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceDeepTest::testGetActiveOrganisationForContextException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetObjectFallsBackToMagicMapperWithoutContext":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountDelegatesToMagicMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetActiveOrganisationReturnsUuidWhenOrgFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetActiveOrganisationReturnsNullWhenNoOrg":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetActiveOrganisationReturnsNullOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateObjectIfRequiredSkipsWhenNotHardValidation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateObjectIfRequiredValidatesWhenHardValidationEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testValidateObjectIfRequiredThrowsOnValidationFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderExistsCallsUpdateWhenNodeIdNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testGetFacetableFieldsDelegatesToFacetHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetRegisterFallsBackToMapperWhenCacheReturnsWrong":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetSchemaFallsBackToMapperWhenCacheReturnsWrong":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testCountSearchObjectsPassesOrgUuidWhenFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSearchObjectsPaginatedBypassesMultitenancyForPublicSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testUpdateObjectSetsIdAndDelegatesToSaveObject":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testPatchObjectMergesExistingDataAndDelegatesToSave":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetContextFromParametersSetsRegister":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetContextFromParametersSetsSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testSetContextFromParametersDoesNothingWhenBothNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testResolveRegisterAndSchemaReturnsNullsWithoutExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testResolveRegisterAndSchemaUsesCurrentFromFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testResolveRegisterAndSchemaLoadsSchemasForSelfSchemaExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testResolveRegisterAndSchemaLoadsRegistersForSelfRegisterExtend":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesReturnsUnchangedWhenNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesConvertsDatetimeToDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesSkipsAlreadyFormattedDates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesLeavesInvalidDatesUntouched":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesSkipsNonStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testNormalizeDateValuesSkipsNonDateFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderCreatesWhenFolderIsString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderReturnsNullOnGeneralException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ObjectServiceTest::testEnsureObjectFolderRecreatesWhenFolderIsStringNumeric":0,"Unit\\Service\\OperatorEvaluatorTest::testUnknownOperatorReturnsFalseAndLogsWarning":0,"Unit\\Service\\OperatorEvaluatorTest::testGtAgainstNullValueReturnsFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testGteAgainstNullValueReturnsFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testLtAgainstNullValueReturnsFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testLteAgainstNullValueReturnsFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testLteAgainstNullValueReturnsFalseForStringOperand":0,"Unit\\Service\\OperatorEvaluatorTest::testComparisonOperatorsAgainstNullOperandReturnFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testInAgainstNullValueReturnsFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testInAgainstNullValueWithNullInOperandArrayStillReturnsFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testNinAgainstNullValueReturnsFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testNeAgainstNullValueReturnsFalse":0,"Unit\\Service\\OperatorEvaluatorTest::testEqWithNullOperandAndNullValueStillMatchesForBackwardCompat":0,"Unit\\Service\\OperatorEvaluatorTest::testExistsStillHonoursExplicitNullCheck":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionCascade":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionRestrict":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionSetNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionSetDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionNoAction":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionCaseInsensitive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsValidOnDeleteActionInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractOnDeletePresent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractOnDeleteMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractTargetRefDirect":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractTargetRefArrayItems":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testExtractTargetRefNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefById":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefBySlug":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefByUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefByPathBasename":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testResolveSchemaRefNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsRequiredPropertyTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsRequiredPropertyFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testIsRequiredPropertySchemaNotCached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValuePresent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValueMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValueSchemaNotCached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValuePropertyNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testGetDefaultValueNullProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testCanDeleteNoSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testCanDeleteSchemaNotInIndex":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testHasIncomingOnDeleteReferencesTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testHasIncomingOnDeleteReferencesFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testLogRestrictBlock":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testLogRestrictBlockWithEmptyBlockers":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testApplyDeletionActionsEmptyAnalysis":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceCoverageTest::testLogIntegrityActionExceptionIsSwallowed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsSetNullOnArrayProperty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testRelationIndexAndSchemaCacheBuiltTogether":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteWithNullSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testCanDeleteDepthLimitingPreventsDeepRecursion":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsOnlyCascade":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testApplyDeletionActionsSetDefaultUpdatesObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\ReferentialIntegrityServiceTest::testValidOnDeleteActionsConstant":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testApplyInversedByFilterSchemaFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testApplyInversedByFilterNoSubFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractRelatedDataDelegates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsStringValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsArrayValue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsArrayLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsCircuitBreaker":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsSkipsInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsDeduplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testExtractAllRelationshipIdsSkipsMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testBulkLoadRelationshipsBatchedEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testBulkLoadRelationshipsBatchedCap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testBulkLoadRelationshipsBatchedLoads":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testBulkLoadRelationshipsBatchedBatchException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testLoadRelationshipChunkOptimizedEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testLoadRelationshipChunkOptimizedException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testLoadRelationshipChunkOptimizedReturns":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testGetContractsWithContracts":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testGetContractsWithPagination":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testGetContractsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testGetContractsException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacAdmin":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacNonObjectEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacNullSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacSchemaNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacFiltersOnPermission":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerDeepTest::testFilterByRbacDeniesNoPermission":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsEmptyObjects":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsSingleStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsArrayValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsRemovesDuplicates":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsEmptyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsLimitsArrayTo10":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractAllRelationshipIdsSkipsNonExistentProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testBulkLoadRelationshipsBatchedEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testBulkLoadRelationshipsBatchedIndexesByUuidAndId":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testBulkLoadRelationshipsBatchedCapsAt200":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testBulkLoadRelationshipsBatchedHandlesExceptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testLoadRelationshipChunkOptimizedEmptyInput":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testLoadRelationshipChunkOptimizedDelegatesToMapper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testLoadRelationshipChunkOptimizedReturnsEmptyOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testGetContractsReturnsContractsFromObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testGetContractsAppliesPagination":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testGetContractsReturnsEmptyWhenNoContracts":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testGetContractsReturnsEmptyOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testExtractRelatedDataDelegatesToPerformanceHandler":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testApplyInversedByFilterReturnsEmptyWhenSchemaIsFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RelationHandlerTest::testApplyInversedByFilterReturnsEmptyWhenNoSubFilters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testApplyArchivalMetadataWithEnabledSchema":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testApplyArchivalMetadataSkipsWhenDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testValidateNotImmutableReturnsDestroyedCode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testValidateNotImmutableReturnsTransferredCode":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testValidateNotImmutableReturnsNullForMutable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testPlaceLegalHold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testReleaseLegalHold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testHasActiveLegalHoldTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testHasActiveLegalHoldFalseWhenNoHold":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testExtendArchiefactiedatum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\RetentionServiceTest::testGenerateDestructionCertificate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatDateTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatRfc3339":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatUuid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatEmail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatUrl":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatDuration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatColor":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatHostname":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatIpv4":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatIpv6":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectStringFormatInvalidDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternIntegerString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternFloatString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternBooleanString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternCamelCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternPascalCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternSnakeCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternScreamingSnakeCase":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternFilename":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeStringPatternWindowsPath":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testConsolidateFormatDetectionNullExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testConsolidateFormatDetectionSameFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testConsolidateFormatDetectionHigherPriorityNew":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testConsolidateFormatDetectionHigherPriorityExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesNullExisting":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesSameType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesIntegerToNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesNumberToInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeNumericRangesIncompatibleTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzezArrayStructureEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzezArrayStructureList":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzezArrayStructureAssociative":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeObjectStructureWithObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeObjectStructureWithArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzeObjectStructureWithScalar":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergeObjectStructures":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testIsInternalPropertyTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testIsInternalPropertyFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatEmail":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromFormatUnknown":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromPatternsBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromPatternsInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromPatternsFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetTypeFromPatternsNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeStringWithIntegerPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeStringWithFloatPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeStringWithBooleanPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeDouble":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeFloat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeNumber":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testNormalizeSingleTypeUnknown":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeStringWithIntegerPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeStringWithFloatPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeStringWithBooleanPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeNonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGetDominantTypeStringPlain":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectEnumLikeTooFewExamples":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectEnumLikeTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectEnumLikeTooManyUnique":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testDetectEnumLikeNonString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testExtractEnumValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateNestedProperties":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateNestedPropertiesNoKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeDouble":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeBoolean":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeDefault":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeEmptyTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testGenerateArrayItemTypeNoItemTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareTypeMissing":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareTypeMismatch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareTypeMatching":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsMissingMaxLength":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsMaxLengthTooSmall":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsMissingFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsMissingPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareStringConstraintsNonStringType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNumericConstraintsNonNumeric":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNumericConstraintsMinimumTooHigh":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNumericConstraintsMaximumTooLow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNullableConstraintRequiredButNullable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNullableConstraintNotNullable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareNullableConstraintSuggestsNullType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintSuggestsEnum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintDifferentValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintSameValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintTooManyValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testCompareEnumConstraintNullValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueString":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueInteger":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueDouble":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueEmptyArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueListArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueAssocArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testAnalyzePropertyValueObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergePropertyAnalysisTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergePropertyAnalysisExamplesOverflow":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergePropertyAnalysisObjectStructureMerge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testMergePropertyAnalysisObjectStructureNewWhenNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testUpdateSchemaFromExplorationSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testUpdateSchemaFromExplorationFailure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testRecommendPropertyTypeFromFormat":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testRecommendPropertyTypeFromPattern":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testRecommendPropertyTypeSingleType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\SchemaServiceCoverageTest::testRecommendPropertyTypeMultipleTypes":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testNonStringPassesThrough":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testNonPlaceholderStringPassesThrough":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testNowReturnsDateTime":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testStartOfMonthIsFirstOfMonth":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testStartOfYearIsJanuaryFirst":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testNowMinus7Days":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testStartOfMonthMinus1Month":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testCurrentUserUnauthenticated":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testCurrentUserAuthenticated":0,"Unit\\Service\\Search\\PlaceholderResolverTest::testResolveArrayRecurses":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheAllSkipsNonIntCleared":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheDistributedSkipsNonIntCleared":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\Settings\\CacheSettingsHandlerTest::testClearCacheDefaultTypeClearsAll":0,"Unit\\Service\\TenantLifecycleServiceTest::testValidTransitions#provisioning to active":0,"Unit\\Service\\TenantLifecycleServiceTest::testValidTransitions#active to suspended":0,"Unit\\Service\\TenantLifecycleServiceTest::testValidTransitions#active to deprovisioning":0,"Unit\\Service\\TenantLifecycleServiceTest::testValidTransitions#suspended to active":0,"Unit\\Service\\TenantLifecycleServiceTest::testValidTransitions#suspended to deprovisioning":0,"Unit\\Service\\TenantLifecycleServiceTest::testValidTransitions#deprovisioning to archived":0,"Unit\\Service\\TenantLifecycleServiceTest::testInvalidTransitions#archived to active":0.002,"Unit\\Service\\TenantLifecycleServiceTest::testInvalidTransitions#archived to provisioning":0,"Unit\\Service\\TenantLifecycleServiceTest::testInvalidTransitions#provisioning to suspended":0,"Unit\\Service\\TenantLifecycleServiceTest::testInvalidTransitions#active to provisioning":0,"Unit\\Service\\TenantLifecycleServiceTest::testInvalidTransitions#deprovisioning to active":0,"Unit\\Service\\TenantLifecycleServiceTest::testInvalidTransitions#active to archived":0,"Unit\\Service\\TenantLifecycleServiceTest::testSuspendSetsStatusAndTimestamp":0,"Unit\\Service\\TenantLifecycleServiceTest::testReactivateClearsSuspendedAt":0,"Unit\\Service\\TenantLifecycleServiceTest::testDeprovisionSetsStatusAndTimestamp":0,"Unit\\Service\\TenantLifecycleServiceTest::testSuspendFromArchivedThrowsException":0,"Unit\\Service\\TenantLifecycleServiceTest::testGetValidTransitions":0,"Unit\\Service\\TenantLifecycleServiceTest::testIsValidEnvironment":0,"Unit\\Service\\TenantLifecycleServiceTest::testIsValidPromotionOrder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceTypeReturnsObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextReturnsExpectedStructure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesUuidAndVersion":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesSchemaTitle":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesSchemaDescription":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextContinuesWhenSchemaNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesRegisterTitle":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesRegisterDescription":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextContinuesWhenRegisterNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIncludesOrganization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextChecksumIsSha256":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextLengthMatchesTextLength":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextMetadataContainsObjectFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextThrowsWhenNoTextExtracted":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextHandlesNestedObjectData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextHandlesBooleanInObjectData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextHandlesNumericValuesInObjectData":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testNeedsExtractionReturnsTrueWhenForced":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testNeedsExtractionReturnsTrueWhenNoChunksExist":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testNeedsExtractionReturnsFalseWhenChunksAreUpToDate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testNeedsExtractionReturnsTrueWhenChunksAreStale":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceMetadataReturnsExpectedKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceTimestampReturnsObjectUpdateTime":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceTimestampReturnsCurrentTimeWhenObjectNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testGetSourceTimestampReturnsCurrentTimeWhenUpdatedIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIgnoresEmptyStringValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextIgnoresEmptyArrayValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\ObjectHandlerTest::testExtractTextRespectsMaxRecursionDepth":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkFixedSizeMultipleChunksNoOverlap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkFixedSizeShortTextSingleChunk":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkFixedSizeFiltersTinyChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkRecursiveSplitsByParagraphs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkRecursiveSplitsBySentences":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRecursiveSplitOversizedSegmentRecursesDeeper":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRecursiveSplitEmptySeparatorsFallsToFixed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRecursiveSplitLastChunkPreserved":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRecursiveSplitWithOverlap":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkDocumentFixedSizeStrategy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testChunkDocumentTruncatesExcessiveChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testCleanTextMixedProblematicCharacters":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSanitizeTextReplacesEmoji":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSanitizeTextRemovesControlChars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSanitizeTextEmptyForWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testDetectLanguageSignalsDutchHet":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testDetectLanguageSignalsEnglishAnd":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testDetectLanguageSignalsUnknown":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testGetDetectionMethodHeuristic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testGetDetectionMethodNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsWordDocumentDocx":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsWordDocumentDoc":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsWordDocumentFalseForPdf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSpreadsheetXlsx":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSpreadsheetXls":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSpreadsheetFalseForCsv":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testCalculateAvgChunkSizeEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testCalculateAvgChunkSizeMixed":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testCalculateAvgChunkSizeNullTextKey":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testBuildPositionReferenceObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testBuildPositionReferenceFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testBuildPositionReferenceFileDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSummarizeMetadataPayloadFull":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testSummarizeMetadataPayloadEmptyDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testGetStatsTotalFilesCalculation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testGetTableCountSafeReturnsZeroOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractFileWithEntityRecognitionEnabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractFileEntityRecognitionDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractFileRiskLevelUpdateFails":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractFileEntityRecognitionThrows":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testDiscoverUntrackedFilesProcessesFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testRetryFailedExtractionsWithFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testExtractPendingFilesWithFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSourceUpToDateFalseWhenForced":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSourceUpToDateTrueWhenChunkNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSourceUpToDateFalseWhenNoChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceCoverageTest::testIsSourceUpToDateFalseWhenChunkOlder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSanitizeTextRemovesNullBytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSanitizeTextNormalizesWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSanitizeTextTrims":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSanitizeTextEmptyForWhitespaceOnly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDetectLanguageSignalsDutch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDetectLanguageSignalsEnglish":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDetectLanguageSignalsUnknown":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testGetDetectionMethodHeuristic":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testGetDetectionMethodNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsWordDocumentDocx":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsWordDocumentDoc":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsWordDocumentFalseForPdf":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSpreadsheetXlsx":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSpreadsheetXls":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSpreadsheetFalseForText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testBuildPositionReferenceObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testBuildPositionReferenceFile":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testBuildPositionReferenceFileDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkDocumentRecursive":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkDocumentFixedSize":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkDocumentSmallTextSingleChunk":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkDocumentDefaultStrategy":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCleanTextNormalization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCleanTextReducesNewlines":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCalculateAvgChunkSizeEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCalculateAvgChunkSizeWithArrayChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCalculateAvgChunkSizeWithStringChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testCalculateAvgChunkSizeWithNonTextChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSourceUpToDateForceFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSourceUpToDateNoChunks":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSourceUpToDateChunkNewer":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testIsSourceUpToDateChunkOlder":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testHydrateChunkEntity":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSummarizeMetadataPayload":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testSummarizeMetadataPayloadEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testTextToChunksMapsCorrectly":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testExtractObjectDeletedObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testGetStatsReturnsStructure":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testGetTableCountSafeReturnsZeroOnException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkFixedSizeSmallText":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testChunkFixedSizeExact":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testRecursiveSplitNoSeparators":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDiscoverUntrackedFilesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testDiscoverUntrackedFilesEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testExtractPendingFilesNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceDeepTest::testRetryFailedExtractionsNone":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentShortTextSingleChunk":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentWithDefaultOptions":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentRemovesNullBytes":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentNormalizesLineEndings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentUnknownStrategyFallback":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentReducesWhitespace":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testChunkDocumentRecursiveSmallParagraphs":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testGetStatsReturnsExpectedKeys":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testGetStatsWithZeroUntrackedFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testDiscoverUntrackedFilesNoFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testDiscoverUntrackedFilesHandlesException":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testDiscoverUntrackedFilesDefaultLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testRetryFailedExtractionsNoFiles":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testRetryFailedExtractionsDefaultLimit":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testExtractPendingFilesNoPending":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtractionServiceGapTest::testExtractPendingFilesDefaultLimit":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithRecursiveStrategy":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithUnknownStrategyFallsBackToRecursive":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentTruncatesExcessiveChunks":0.002,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentShortTextReturnsSingleChunk":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentSmallTextBelowMinChunkSizeReturnsOneChunk":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextRemovesNullBytes":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextNormalizesLineEndings":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextReducesExcessiveNewlines":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextCollapsesTabsAndSpaces":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextTrimsWhitespace":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextRemovesNullBytes":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextRemovesControlCharacters":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextNormalizesWhitespace":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextTrims":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextReturnsEmptyForWhitespaceOnly":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextHandlesEmptyString":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsDetectsDutch":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsDetectsEnglish":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsReturnsNullForUnknown":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsDutchTakesPriorityOverEnglish":0,"Unit\\Service\\TextExtractionServiceTest::testGetDetectionMethodReturnsHeuristicForLanguage":0,"Unit\\Service\\TextExtractionServiceTest::testGetDetectionMethodReturnsNoneForNull":0,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsTrueForDocx":0,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsTrueForDoc":0,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsFalseForPdf":0,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsFalseForTextPlain":0,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsTrueForXlsx":0,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsTrueForXls":0,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsFalseForPdf":0,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsFalseForCsv":0,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeReturnsZeroForEmpty":0,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithArrayChunks":0,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithStringChunks":0,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithMixedChunks":0,"Unit\\Service\\TextExtractionServiceTest::testSummarizeMetadataPayloadReturnsExpectedKeys":0,"Unit\\Service\\TextExtractionServiceTest::testSummarizeMetadataPayloadWithMissingKeysDefaultsToNull":0,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForObject":0,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForObjectWithoutPath":0,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForFile":0,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForFileWithoutOffsets":0,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsFalseWhenForceReExtract":0,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsFalseWhenNoChunks":0,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsTrueWhenChunksNewer":0,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsTrueWhenChunksEqual":0,"Unit\\Service\\TextExtractionServiceTest::testIsSourceUpToDateReturnsFalseWhenChunksOlder":0,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntityCreatesValidChunk":0,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntityHandlesMinimalData":0,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksGeneratesChunkDTOs":0.001,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksForObjectSource":0,"Unit\\Service\\TextExtractionServiceTest::testGetTableCountSafeReturnsZeroOnException":0,"Unit\\Service\\TextExtractionServiceTest::testGetTableCountSafeReturnsCount":0,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeReturnsOneChunkForShortText":0,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeMultipleChunksWithNoOverlap":0,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeWithLargeText":0,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveReturnsOneChunkForShortText":0,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveSplitsByParagraphs":0,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveSplitsBySentences":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitWithEmptySeparatorsFallsToFixedSize":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitShortTextReturnsSingleChunk":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileThrowsNotFoundWhenFileNotInNextcloud":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileSkipsUpToDateFile":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileProcessesTextFile":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileForceReExtractIgnoresUpToDate":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWithEntityRecognitionEnabled":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileEntityRecognitionFailureDoesNotThrow":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileRiskLevelFailureDoesNotThrow":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileUnsupportedMimeType":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileEmptyNodesThrows":0,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectSkipsDeletedObject":0,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectSkipsUpToDateObject":0,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesReturnsStatsWhenNoFiles":0,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesReturnsErrorOnException":0,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesCountsFailures":0,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesReturnsStatsWhenNoFiles":0,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesCountsFailures":0,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsReturnsStatsWhenNoFiles":0,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsCountsFailures":0,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksRollsBackOnError":0,"Unit\\Service\\TextExtractionServiceTest::testPersistMetadataChunkHandlesJsonEncodingFailure":0,"Unit\\Service\\TextExtractionServiceTest::testDefaultConstantsHaveExpectedValues":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentNormalizesTextBeforeChunking":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithZeroOverlap":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentDefaultOptions":0,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextThrowsWhenExtractionReturnsNull":0,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextThrowsWhenResultIsEmpty":0,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextReturnsPayload":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionTextPlain":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionJsonFile":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionMarkdownFile":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionUnsupportedMimeReturnsNull":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionEmptyNodesThrows":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionNodeNotFileThrows":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionCsvFile":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionYamlFile":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionGenericTextSubtype":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionHtmlFile":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionXmlFile":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionApplicationXmlFile":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionYamlAlternativeMime":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionApplicationYamlMime":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionMissingMimeAndPath":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextReplacesEmojiWithSpace":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextRemovesDeleteCharacter":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextRemovesVerticalTabAndFormFeed":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextPreservesNewlinesAndTabs":0,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextMissingOwnerAndOrganisationDefaultsToNull":0,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextMissingMetadataFieldsDefaultToNull":0,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextWithDutchContent":0,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksDefaultsWhenOptionsEmpty":0,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksShortTextSingleChunk":0,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksWithFixedSizeStrategy":0.001,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeWordBoundaryBreaking":0,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeFiltersTinyChunks":0,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeGuardPreventsNegativeOffset":0,"Unit\\Service\\TextExtractionServiceTest::testChunkFixedSizeExactChunkSizeText":0,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveWithSentenceBoundaries":0,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveWithClauseBoundaries":0,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveWithWordBoundariesOnly":0,"Unit\\Service\\TextExtractionServiceTest::testChunkRecursiveExactChunkSizeText":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitWithOverlap":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitLargeSplitRecursesDeeper":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitLastChunkPreserved":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitLastChunkTooSmallIsFiltered":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitWithZeroOverlap":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitSmallOverlapFallsToElse":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileMissingMtimeUsesCurrentTime":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileDefaultEntityRecognitionMethod":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWithOrganisationInMetadata":0.001,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectWithNullUpdatedTimestamp":0,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectForceReExtractIgnoresUpToDate":0,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceSuccessPath":0,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceMultipleChunks":0,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceWithNullOwnerAndOrg":0,"Unit\\Service\\TextExtractionServiceTest::testPersistMetadataChunkCreatesCorrectChunk":0,"Unit\\Service\\TextExtractionServiceTest::testPersistMetadataChunkWithEmptyPayload":0,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntityUsesTextContentLengthAsEndOffset":0,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntitySetsAllNullableFieldsToNull":0,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntityTimestampSetsCorrectly":0,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesSuccessfulExtraction":0.001,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesMixedSuccessAndFailure":0,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesSuccessfulExtraction":0,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsSuccessfulRetry":0,"Unit\\Service\\TextExtractionServiceTest::testGetStatsHandlesDbExceptionGracefully":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextEmptyString":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextOnlyNullBytes":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextMixedLineEndings":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsWithEenArticle":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsWithHetArticle":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsWithAndKeyword":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsWithOfKeyword":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsEmptyString":0,"Unit\\Service\\TextExtractionServiceTest::testDetectLanguageSignalsLanguageLevelAlwaysNull":0,"Unit\\Service\\TextExtractionServiceTest::testBuildPositionReferenceForUnknownSourceType":0,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithNullTextKey":0,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeWithIntegerChunks":0,"Unit\\Service\\TextExtractionServiceTest::testCalculateAvgChunkSizeSingleChunk":0,"Unit\\Service\\TextExtractionServiceTest::testSummarizeMetadataPayloadWithPartialKeys":0,"Unit\\Service\\TextExtractionServiceTest::testExtractPdfWithInvalidContentThrows":0.003,"Unit\\Service\\TextExtractionServiceTest::testExtractWordDocxWithInvalidContentThrows":0.007,"Unit\\Service\\TextExtractionServiceTest::testExtractWordDocWithInvalidContentThrows":0,"Unit\\Service\\TextExtractionServiceTest::testExtractSpreadsheetXlsxReturnsExtractedText":0.012,"Unit\\Service\\TextExtractionServiceTest::testExtractSpreadsheetXlsReturnsExtractedText":0.001,"Unit\\Service\\TextExtractionServiceTest::testExtractFilePdfWithInvalidContentPropagates":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWordDocWithInvalidContentPropagates":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileSpreadsheetProcessesContent":0.002,"Unit\\Service\\TextExtractionServiceTest::testExtractPdfWrapsParseExceptionCorrectly":0,"Unit\\Service\\TextExtractionServiceTest::testExtractWordReturnsNullForEmptyDocx":0.008,"Unit\\Service\\TextExtractionServiceTest::testExtractWordReturnsTextFromDocx":0.008,"Unit\\Service\\TextExtractionServiceTest::testExtractSpreadsheetReturnsTextFromXlsx":0.008,"Unit\\Service\\TextExtractionServiceTest::testExtractSpreadsheetReturnsNullForEmptyXlsx":0.006,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextHandlesNonUtf8Input":0,"Unit\\Service\\TextExtractionServiceTest::testPersistMetadataChunkHandlesJsonEncodingFailureDirectly":0,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectEntityExtractionFailureLogsError":0,"Unit\\Service\\TextExtractionServiceTest::testExtractPdfReturnsNullForEmptyPdfText":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextWithOnlyWhitespace":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextWithMultipleTabsAndSpaces":0,"Unit\\Service\\TextExtractionServiceTest::testCleanTextPreservesSingleParagraphBreak":0,"Unit\\Service\\TextExtractionServiceTest::testExtractSourceTextChecksumMatchesSha256":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitSingleLargeSegmentUsesSubChunks":0,"Unit\\Service\\TextExtractionServiceTest::testRecursiveSplitSingleSmallSegmentAfterEmptyCurrentChunk":0,"Unit\\Service\\TextExtractionServiceTest::testChunkDocumentWithOnlyNullBytes":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWithHtmlMimeType":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileWithApplicationJsonMimeType":0,"Unit\\Service\\TextExtractionServiceTest::testExtractFileUnsupportedMimeTypeThrowsViaSourceText":0,"Unit\\Service\\TextExtractionServiceTest::testGetStatsWithAllZeroTablesReturnsCorrectStructure":0,"Unit\\Service\\TextExtractionServiceTest::testHydrateChunkEntitySetsPositionReference":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextHandlesHighUnicodeCharacters":0,"Unit\\Service\\TextExtractionServiceTest::testSanitizeTextPreservesRegularUnicodeText":0,"Unit\\Service\\TextExtractionServiceTest::testPerformTextExtractionGetByIdExceptionIsRethrown":0,"Unit\\Service\\TextExtractionServiceTest::testDiscoverUntrackedFilesWithCustomLimit":0,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksHandlesEndOffsetDefault":0,"Unit\\Service\\TextExtractionServiceTest::testTextToChunksNullLanguageLevelPassedThrough":0,"Unit\\Service\\TextExtractionServiceTest::testIsWordDocumentReturnsFalseForOdt":0,"Unit\\Service\\TextExtractionServiceTest::testIsSpreadsheetReturnsFalseForOds":0,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectSkipsWhenObjectNotFound":0,"Unit\\Service\\TextExtractionServiceTest::testExtractObjectSkipsWhenUpToDate":0.001,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesReturnsCorrectStats":0,"Unit\\Service\\TextExtractionServiceTest::testExtractPendingFilesWithNoFiles":0,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsReturnsCorrectStats":0,"Unit\\Service\\TextExtractionServiceTest::testRetryFailedExtractionsWithNoFiles":0,"Unit\\Service\\TextExtractionServiceTest::testGetStatsReturnsAllExpectedKeys":0.001,"Unit\\Service\\TextExtractionServiceTest::testGetTableCountSafeReturnsZeroOnMissingTable":0,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceRollsBackOnException":0,"Unit\\Service\\TextExtractionServiceTest::testPersistChunksForSourceCommitsOnSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloExportTest::testGenerateMdtoXmlFullObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloExportTest::testGenerateMdtoXmlThrowsForMissingTmlo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloExportTest::testGenerateMdtoXmlMapsArchiefnominatie":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloExportTest::testGenerateBatchMdtoXml":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloExportTest::testGenerateBatchMdtoXmlSkipsNoTmlo":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloExportTest::testGenerateBatchMdtoXmlEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloExportTest::testGenerateMdtoXmlEscapesSpecialChars":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testIsTmloEnabledTrue":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testIsTmloEnabledFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testIsTmloEnabledExplicitlyFalse":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testPopulateDefaultsSetsArchiefstatusActief":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testPopulateDefaultsMergesSchemaDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testPopulateDefaultsDoesNotOverrideExplicitValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testPopulateDefaultsSkipsWhenDisabled":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testPopulateDefaultsCalculatesArchiefactiedatum":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateFieldValuesAcceptsValidArchiefnominatie":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateFieldValuesRejectsInvalidArchiefnominatie":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateFieldValuesRejectsInvalidDuration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateFieldValuesAcceptsValidDuration":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateTransitionActiefToSemiStatisch":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateTransitionActiefToOvergebrachtRejected":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateTransitionToOvergebrachtRequiresFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateTransitionToVernietigdRequiresVernietiginNominatie":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidateTransitionNoChangeReturnsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testValidTransitionToOvergebracht":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testCalculateArchiefactiedatumValid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testCalculateArchiefactiedatumInvalid":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testGetSchemaDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TmloServiceTest::testGetSchemaDefaultsEmpty":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testChangePasswordSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testChangePasswordBackendUnsupported":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testChangePasswordIncorrectCurrent":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testChangePasswordPolicyViolation":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testUploadAvatarSuccess":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testUploadAvatarUnsupportedType":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testUploadAvatarTooLarge":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testUploadAvatarBackendUnsupported":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testDeleteAvatarSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testGetNotificationPreferencesDefaults":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testGetNotificationPreferencesStored":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testSetNotificationPreferencesSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testSetNotificationPreferencesInvalidDigest":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testGetUserActivitySuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testCreateApiTokenSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testCreateApiTokenMaxReached":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testListApiTokensMasked":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testRevokeApiTokenSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testRevokeApiTokenNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testRequestDeactivationSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testRequestDeactivationDuplicate":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testGetDeactivationStatusActive":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testGetDeactivationStatusPending":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testCancelDeactivationSuccess":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testCancelDeactivationNoPending":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\UserServiceProfileActionsTest::testExportPersonalDataRateLimited":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns401WhenNotAuthenticated":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns404WhenRelationNotFound":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenFileNotWritable":0.007,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenFileNotPresentInUserFolder":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns400WhenMapperRaisesValidationException":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns200OnSuccessfulPatch":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns500OnUnexpectedException":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperTest::testConstructsWithInjectedDependencies":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRejectsUnknownField":0.007,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRejectsBasesAsNonArray":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRejectsBasesArrayWithNonStringElement":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRejectsSkipAnonymizationAsNonBool":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testSemanticNoOpSkipsAuditEmission":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testEmptyBodyIsNoOp":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testSettingBasesForFirstTimeEmitsAuditEntry":0.004,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testFlippingSkipEmitsAuditEntry":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testPartialChangeOnlyAuditsTheChangedField":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testAuditEmissionFailureDoesNotMaskUpdate":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testFallsBackToSessionUserWhenActingUserIsNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testRecordsSystemActorWhenNoUserIsAvailable":0.001,"Unit\\Db\\EntityRelationTest::testSetAndGetBasesRoundTrip":0,"Unit\\Db\\EntityRelationTest::testBasesEmptyArrayDistinctFromNull":0,"Unit\\Db\\EntityRelationTest::testSetAndGetSkipAnonymization":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testStructureExposesCanonicalHeaders":0.069,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testMultipartAlternativePrefersTextPlainOnFlatPath":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testFlatPathFallsBackToHtmlWhenPlainIsAbsent":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testAttachmentContentIsDecodedBytesNotBase64":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testMissingFilenameFallsBackToPositionalAttachmentName":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testInlineAttachmentExposesContentIdWithoutAngleBrackets":0.003,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testEmptyBodyDoesNotInterfereWithStructure":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testFlattenIncludesAttachmentMarker":0.002,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testSanitisePiiForLoggingRedactsEmailsAndAngleBrackets":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testSanitisePiiForLoggingRedactsQuotedStrings":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testEmlBodyValueObjectIsImmutable":0,"OCA\\OpenRegister\\Tests\\Unit\\Service\\TextExtraction\\EmlParserTest::testEmlAttachmentJsonSerialisesContentAsBase64":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testDispatchesEventOnSkipAnonymizationFlip":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testDispatchesEventOnBasesChange":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testNoEventDispatchedOnSemanticNoOp":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testDispatchFailureDoesNotMaskUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenObjectBoundButHasPermissionDeniesUpdate":0.005,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns200WhenObjectBoundAndHasPermissionGrantsUpdate":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenEmailBoundButEmailLinkHasNoParentObject":0,"OCA\\OpenRegister\\Tests\\Unit\\Controller\\EntityRelationsControllerTest::testReturns403WhenRelationHasNoSubject":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testAuditEmissionFailureRollsBackUpdateAndThrows":0.001,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testReorderedBasesAreSemanticNoOpAndSkipAudit":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testDuplicateBasesAreSemanticNoOpAgainstUniqueSet":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationMapperUpdateDecisionMetadataTest::testNullVsEmptyArrayBasesAreNotEqual":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testConstructorRegistersFieldTypes":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testConstructorDefaultValues":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testSetAndGetBasesRoundTrip":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testBasesEmptyArrayDistinctFromNull":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testSetAndGetSkipAnonymization":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testSetAndGetIntegerFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testSetAndGetStringFields":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testSetAndGetConfidence":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testSetAndGetAnonymized":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testSetAndGetCreatedAt":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testJsonSerialize":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testJsonSerializeDateTimeFormatting":0,"OCA\\OpenRegister\\Tests\\Unit\\Db\\EntityRelationTest::testJsonSerializeNullCreatedAt":0,"Unit\\Db\\FormLinkTest::testJsonSerializeFormLevelRow":0.006,"Unit\\Db\\FormLinkTest::testJsonSerializeSubmissionLevelRow":0,"Unit\\Db\\FormLinkTest::testJsonSerializeHandlesNullableFields":0,"Unit\\Db\\FormLinkTest::testSettersAndGetters":0}} \ No newline at end of file diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000000..3fa19a7cff --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":2,"defects":{"Unit\\Controller\\ProcessingLogControllerTest::testPrivacyOfficerIsScopedAndSeesConfidential":8,"Unit\\Controller\\ProcessingLogControllerTest::testExtractRequiresSubjectIdentifiers":8,"Unit\\Controller\\ProcessingLogControllerTest::testExtractRangeTooWideIs422":8},"times":{"Unit\\Service\\ProcessingLogServiceTest::testReadOnOptedInSchemaIsBuffered":0.011,"Unit\\Service\\ProcessingLogServiceTest::testNoLoggingWithoutOptIn":0.001,"Unit\\Service\\ProcessingLogServiceTest::testNoAnnotationNoEntry":0,"Unit\\Service\\ProcessingLogServiceTest::testUnresolvedAttributionFallsBackAndIsNotDropped":0.001,"Unit\\Service\\ProcessingLogServiceTest::testRetiredActivityFallsBack":0.001,"Unit\\Service\\ProcessingLogServiceTest::testPerOperationExportAttribution":0.001,"Unit\\Service\\ProcessingLogServiceTest::testListReadCollapsesToOneEntry":0.001,"Unit\\Service\\ProcessingLogServiceTest::testFlushIsFailSoft":0.001,"Unit\\Service\\ProcessingLogServiceTest::testCaptureIsFailSoftOnSchemaError":0,"Unit\\Service\\ProcessingLogServiceTest::testLegacyAnnotationDoesNotEnableReads":0,"Unit\\Controller\\ProcessingLogControllerTest::testAnonymousIsUnauthorized":0.001,"Unit\\Controller\\ProcessingLogControllerTest::testNonPrivilegedUserIsForbidden":0,"Unit\\Controller\\ProcessingLogControllerTest::testPrivacyOfficerIsScopedAndSeesConfidential":0.001,"Unit\\Controller\\ProcessingLogControllerTest::testAdminIsUnscoped":0,"Unit\\Controller\\ProcessingLogControllerTest::testExtractRequiresSubjectIdentifiers":0,"Unit\\Controller\\ProcessingLogControllerTest::testExtractRangeTooWideIs422":0.001,"Unit\\Controller\\ProcessingLogControllerTest::testControllerHasNoMutationEndpoints":0.001}} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d97efad326..1c4b287fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,17 +3,61 @@ ## Unreleased ### Added +- **Field-level object encryption at rest (`field-level-encryption`).** Schema property flag `x-openregister-encrypted: true` encrypts flagged object field values at rest, transparently, for every app built on OpenRegister — reusing `OCP\Security\ICrypto` (the same primitive `TenantKeyService`/`SourcesController`/`WorkflowEngineRegistry` already use for credentials, no new crypto). New `lib/Service/FieldEncryptionHandler.php` encrypts on save (`SaveObject::prepareObjectData()`, last step before persistence) and decrypts on read (`RenderObject::renderEntity()`/`redactWriteOnlyFromRows()`) — decryption runs strictly *after* the existing `writeOnly`/property-authorization redaction and only touches properties still present, so an unauthorized caller never receives ciphertext or plaintext, only the same absence every other redacted field gets. Stored values use a versioned envelope (`openregister:enc:v1:`) so mixed plaintext/ciphertext during rollout is unambiguous. An encrypted property gets no dedicated magic-table column (`MagicMapper::buildTableColumnsFromSchema()`), is excluded from full-text search and facets, and filtering on it is rejected with HTTP 400 (`EncryptedFieldFilterException` via new `EncryptedFieldFilterMiddleware`) rather than silently returning zero rows. New `occ openregister:encrypt-field --property=` command migrates existing plaintext values idempotently, with `--dry-run`/`--register`/`--schema` scoping. Decryption failure never fails silently — a structured `@openregister_decryption_error` marker + ERROR log by default, or a thrown `FieldDecryptionException` for strict callers (the migration command). No behaviour change for any schema that does not use the new flag. (`field-level-object-encryption`) +- **Scheduled report jobs — recurring `ExportService` deliveries to Files + notification (`scheduled-report-jobs`).** Users can now schedule a recurring export of any register/schema + filter combination (the same shape the export endpoints already accept) instead of clicking "export" every time. New `ScheduledReport` (`lib/Db/ScheduledReport.php`, `openregister_scheduled_reports` table, migration `Version1Date20260713000000`) is infrastructure DB state — not an OR object/register (ADR-001, same reasoning as `PushSubscription`) — owned by one user: register/schema, opaque `@self.*` filter JSON, format (`csv`/`excel`/`pdf`, validated against `ExportService`'s supported set; `csv` requires a `schemaId`), a `daily`/`weekly`/`monthly` + hour schedule (no cron expressions), delivery folder (default `Reports/`), enabled flag, and `lastRunAt`/`lastStatus`/`lastError`. `ScheduledReportService::runOne()` impersonates the report owner (`IUserSession::setUser()` + try/finally restore, the established `HandoffService`/`BackfillCalendarLinksJob` pattern — never a privilege-escalating `SystemOperationContext` bypass) so `ExportService` applies exactly that owner's RBAC/multi-tenancy scope, writes the export into the owner's Nextcloud Files (path-traversal-hardened, mirroring `ReportRenderJob::writeToFiles()`), and sends an `INotifier` notification (`scheduled_report_delivered` / `scheduled_report_failed`) linking to the file. The hourly `ScheduledReportJob` (`TimedJob`) loads enabled reports and runs those whose elapsed time since `lastRunAt` is at least a full schedule period — catch-up-safe (a report overdue by days still fires on the next tick) — with per-report isolation (one report's failure, including `ExportTooLargeException`, marks it `failed` with a reason and notifies the owner, without retry-looping or aborting the rest of the batch). `ScheduledReportsController` exposes owner-scoped REST CRUD (admin sees all via `?all=true`) plus `POST /api/scheduled-reports/{id}/run-now`, which queues `ScheduledReportRunNowJob` via `IJobList::add()` (mirrors `WebhookDeliveryJob`'s dispatch-not-inline pattern) and returns 202 — the export never runs inline in the HTTP request. This is the generic counterpart to the still-unimplemented "scheduled report generation" requirement `rapportage-bi-export`'s spec reserved the `ScheduledReportJob` class name for; the already-shipped `ReportRenderJob` (BI-dashboard-template rendering) is a separate, narrower mechanism and is unaffected. Email delivery remains out of scope (documented as future work; Files + notification only). (`scheduled-report-jobs`) +- **Property-level read authorization — objects can now strip sensitive fields on read (closes #380).** Two opt-in, fail-safe, backward-compatible mechanisms on the object read path: (1) a property marked `writeOnly: true` (standard JSON Schema / OpenAPI keyword) is stripped from every read response for everyone including admin, while remaining writable — the correct semantic for secrets/tokens; (2) a property carrying `authorization.read` is stripped unless the caller's groups match (already shipped, now hardened). Both apply on single get, list, and nested/related expansion, and run server-side AFTER any caller `fields`/`extend`/`unset` selection so a caller can never re-surface a stripped property. Stripping bypasses for trusted internal reads (`_rbac === false`) and system operations (`SystemOperationContext::isActive()`), mirroring `PermissionHandler::hasPermission()`, so the app's own service/repair reads still get the full object; internal reads via `ObjectEntity::getObject()` never reach the render path and are unaffected. `Schema` gains `hasWriteOnlyProperties()`/`getWriteOnlyProperties()`; `PropertyRbacHandler::filterReadableProperties()` strips `writeOnly` before its admin bypass; `RenderObject::renderEntity()` gates on writeOnly OR property-authorization and applies the `_rbac`/system-context bypass. Because ADR-063 MCP tools read through OpenRegister, they inherit the redaction automatically without per-dialect field projection. A property with neither mechanism is returned exactly as before. (`property-level-read-rbac`) +- **`#[McpTool]` optional hint/scope params — attribute-derived service tools now carry the same MCP 2025-11-25 annotation hints and `scope` schema-derived tools have had since PR #373 (closes #374).** `#[McpTool]` gains four optional constructor params, `readOnlyHint`/`destructiveHint`/`idempotentHint` (bool) and `scope` (string), reusing `McpAnnotationValidator::HINT_KEYS`/`SCOPES` verbatim rather than a parallel vocabulary; all four default to `null` and are never inferred or defaulted when omitted. `AttributeToolScanner::buildDescriptor()` forwards whatever the author set, additively, into the tool descriptor, and rejects (logs + skips) a method whose `scope` is not a recognised value. `AttributeToolProvider::getTools()` — previously an explicit key whitelist that would have **silently dropped** these fields — now forwards them through unchanged; `McpProviderBridge::getFunctions()` needed no change, since it already forwards any descriptor key in that vocabulary regardless of which provider produced it. Hints/scope remain advisory UX metadata only — OpenRegister RBAC and the owning method's own authorization stay the sole authoritative invoke-time gate (ADR-063). Closes the governance gap where a downstream consumer (e.g. Hermiq's `ToolGrantResolver`) could classify a schema-derived 3-segment tool's destructiveness but was blind to it on a 2-segment attributed service tool — exactly where the genuinely dangerous operations tend to live. (`or-mcp-attribute-hints`) +- **`#[McpTool]` service-method attribute + reflection scanner — annotated tools on both serving surfaces (ADR-063 chain 3/3, chain now COMPLETE).** New net-new PHP attribute `OCA\OpenRegister\Mcp\Attribute\McpTool` (`lib/Mcp/Attribute/McpTool.php`, php-mcp/server style, `Attribute::TARGET_METHOD`) marks a public service method for MCP exposure: optional `name` (default: method name) and `description` (default: docblock summary), `inputSchema` **inferred** from parameter type hints + docblock `@param` tags (required = no default value; nullable/defaulted → optional JSON-Schema `[type, 'null']`), `outputSchema` best-effort from the return type / `@return` (omitted for untyped `array`/`mixed`/`void`). New reflection scanner `lib/Mcp/AttributeToolScanner.php` (pure, no DI dependency) finds attributed methods on an app's declared scannable service classes and builds one descriptor per method, id `{appId}.{toolName}`; non-public, static, and abstract attributed methods are ignored with a logged warning. **Discovery scope is opt-in** (provisional resolution of design.md's deferred question): an app registers a `list` of its own scannable service classes via the container alias `OCA\OpenRegister\Mcp\IMcpScannableServices::` (new `lib/Mcp/IMcpScannableServices.php`, mirrors the existing per-app `IMcpToolProvider::` convention) — nothing is scanned by default. New thin `IMcpToolProvider`-shaped adapter `lib/Mcp/BuiltIn/AttributeToolProvider.php` (one instance per owning app, mirrors `SchemaDerivedToolProvider`) wires attributed tools into the **same catalog and both existing serving surfaces** with no new serving code: `Application::collectAttributeMcpProviders()` runs **last**, after both hand-written (`collectPerAppMcpProviders()`) and schema-derived (`collectSchemaDerivedMcpProviders()`) collection, so its collision policy sees every existing id — an attributed id colliding with a **hand-written** id is silently self-suppressed (hand-written wins, same precedence as the derived provider), while an attributed id colliding with a **derived** id is a discovery-time error (rejected, logged) since that ambiguity is a developer mistake, not an intentional override. **In-process invocation only (ADR-041)** — `invokeTool()` calls `$instance->{$method}(...$namedArguments)` directly on the already-DI-resolved owning service instance, in the caller's ambient Nextcloud session; there is no HTTP call, message bus, or OpenRegister-side re-implementation, and the owning method alone is responsible for its own authorization/IDOR (no impersonation, no elevation). **Audited identically to the derived provider**: every invocation — success or failure — writes exactly one immutable, hash-chained audit record via the existing `AuditTrailMapper::createToolInvocationEntry()` (acting identity, `toolId`, SHA-256 params digest, structured result summary), chained into the same tamper-evident trail (EU AI Act art.12/14). Ships **no** production seed data — only a test fixture service (`tests/Unit/Mcp/Fixtures/`); the real first consumers are leaf apps annotating their own services (pipelinq `createLead`/`logContactmoment`/`pipelineForecast`) in their own migration changes. (`or-mcp-tool-attribute`) +- **Schema-derived MCP tools — `{appId}.{schema}.{verb}` on both serving surfaces (ADR-063 chain 2/3).** The `x-openregister-mcp` dialect now actually emits tools: the new `SchemaDerivedToolProvider` (`lib/Mcp/BuiltIn/SchemaDerivedToolProvider.php`) reads every schema whose validated annotation has `enabled: true` and derives one MCP tool per declared verb, reusing the schema itself as `inputSchema` (and as `outputSchema`/`structuredContent` element shape for read verbs, MCP 2025-06-18) and passing the dialect's `readOnlyHint`/`destructiveHint`/`idempotentHint` through as untrusted UX metadata. One provider instance is registered **per owning app** (resolved schema `application` → owning register `application`), so every id satisfies the existing `IMcpToolProvider` "id prefix == `getAppId()`" invariant with **no ABI change**, and both serving surfaces are fed from the single derivation: the JSON-RPC `tools/list`/`tools/call` surface (`Application::registerMcpToolProviders()` appends the derived providers **after** all hand-written per-app providers — first-wins precedence) and the chat/facade surface via the existing `ToolRegistrationListener`/`McpProviderBridge` path (the `ToolRegistry` id regex is widened to dot-separated segments, `^[a-z0-9_]+(\.[a-zA-Z0-9_]+)+$`, so three-segment derived ids register). **Hand-written > derived precedence** is doubly enforced: registration ordering plus self-suppression — a derived tool whose id a hand-written provider for the same app already claims is absent from the catalog, not merely shadowed, so apps migrate schema-by-schema without breakage. The derived `search` honours **only** the dialect-declared `filters` (an undeclared filter key is rejected with a tool error), paginates with a bounded default (20) and hard-clamped max (100) page size, supports optional `fields` projection, truncates long string values, and returns `total`/`hasMore` paging metadata to keep agent token cost sane. `create`/`update`/`delete` write through `ObjectService` in the caller's ambient Nextcloud session — no impersonation, no system account, RBAC + IDOR identical to the REST path (the dialect `scope`/hints never gate execution). **Every invocation — read, write, or failed — writes exactly one immutable, hash-chained audit record** (EU AI Act art.12/14): new `AuditTrailMapper::createToolInvocationEntry()` records the acting identity, the full `toolId` (action `mcp.{verb}`), a SHA-256 **digest** of the canonicalised arguments (never raw values, no PII at rest), and a structured result summary (count/ids/isError/error class), sealed into the existing `AuditHashService` chain; migration `Version1Date20260712130000` adds the nullable `tool_id`/`params_digest`/`result_summary` columns. Ships **no** production seed data — OR's own registers stay default-OFF; leaf apps opt in per schema. (`or-mcp-derived-tool-provider`) +- **`x-openregister-mcp` schema dialect — declarative, opt-in MCP tool exposure (ADR-063).** A schema can now declare which coarse CRUD MCP tools it exposes via a top-level `x-openregister-mcp` annotation in `lib/Settings/{app}_register.json`, joining the existing `x-openregister-*` dialect family (lifecycle, calculations, notifications, handoff, etc.). The key is registered in `Schema::ANNOTATION_VOCABULARY` so it folds into the schema's `configuration` on import instead of being dropped as unknown; a new `McpAnnotationValidator` (`lib/Service/Mcp/McpAnnotationValidator.php`), invoked from `SchemaMapper::cleanObject()`, validates the block's shape at save time — `enabled` (boolean, the opt-in gate, default OFF), an optional `tools` object restricted to the fixed verb set `{search, get, create, update, delete}`, each with `description`/`scope`/the MCP 2025-11-25 hints `readOnlyHint`/`destructiveHint`/`idempotentHint`, and `search`-only `filters` cross-checked against real schema properties. Unknown verbs, misplaced `filters`, bad `scope` values, and non-boolean hints all fail the schema save loudly, consistent with every sibling dialect validator. This is the first of a three-change arc delivering OpenRegister's half of ADR-063 (MCP as Platform Abstraction) — this change defines and validates the declaration only; **no MCP tool is emitted and no serving surface changes** until the follow-up `or-mcp-derived-tool-provider` change ships. (`or-mcp-schema-dialect`) +- **True CRUD on database-backed virtual registers — opt-in write-through to external databases (dbal-virtual-registers-crud).** Read-only stays the default; an admin enables "Allow writes" on a `type: database` source (`authConfig.writable`, UI switch on the source modal with a least-privilege warning). Create/update/delete on that source's table schemas are then delegated to the new `WritableObjectSourceProvider` seam — an interface only `DbalObjectSourceProvider` implements, so the Nextcloud-native providers can never be written. The dispatch in `SaveObject`/`DeleteObject` requires BOTH the schema annotation `readOnly: false` (stamped by introspection; views always read-only) AND the Source's writable flag re-verified live at write time (fail closed — flipping it off re-locks instantly, without re-introspection); RBAC and JSON-Schema validation run before the external system is reached, and external writes record audit-trail rows. Provider writes are single parameterized statements on the introspected scalar-column allowlist (unknown property → 400, never dropped): INSERT returns generated keys via PostgreSQL `RETURNING` / `lastInsertId` and re-reads the row so DB-applied defaults appear in the response; UPDATE/DELETE reconstruct single or composite-PK predicates from the object id (PK columns immutable; part-count mismatch → 400; zero affected rows → uniform 404); no-PK tables are append-only. External constraint violations map to sanitized 4xx via Doctrine's typed exceptions (unique/FK → 409, not-null/check/type → 422 — SQLite's generic SQLSTATE 23000 included) and controller catches no longer flatten them to 403. Also fixed: primary-key properties introspect as `type: string` (the id the read path returns previously failed write validation as `integer`); admins get the NON-SECRET `authConfig` back on database sources so the edit modal rehydrates (every UI source-save previously wiped the connection settings); the frontend `Source` entity no longer drops `authConfig`. Full CRUD live-verified (API + Playwright UI) against a real external PostgreSQL database. Amends hydra ADR-064 Rule 4. (`dbal-virtual-registers-crud`) +- **Nextcloud Tables tables as read-only virtual registers (tables-object-source-provider).** When the Tables app is installed, OpenRegister auto-seeds a `tables` virtual register with one read-only Schema per Tables table (deterministic slug; repair step on upgrade, `occ openregister:tables:sync` on demand, live retirement on `TableDeletedEvent`). Rows are served live through the object-source-provider seam (`x-openregister-object-source {provider: 'tables'}`) as the acting user, so Tables' ownership/share/context permissions decide visibility (denied == absent, no enumeration oracle). Tables column types map to JSON Schema (text/number/datetime/selection/usergroup; check cells coerce `'true'/'false'` strings to booleans); relation columns resolve to the referenced virtual object's deterministic UUIDv5 for cross-schema deep-linking; native limit/offset pushdown with optional Tables **View** binding for server-side filter/sort. Soft dependency — no composer requirement, guarded class lookups, fail-closed (empty + logged warning) when Tables is absent; writes are rejected as a read-only projection. Live-verified against Tables 2.2.0 (call-shape fixes for `ColumnService::findAllByTable`, `RowService::find`/`getRowsCount`, View-entity counts, and `createTutorial: false` on reconcile). (`tables-object-source-provider`) +- **External databases as read-only virtual registers over Doctrine DBAL (dbal-virtual-registers).** An admin configures a `Source` of `type: database` (drivers `pdo_mysql` / `pdo_pgsql` / `pdo_sqlite`; password custodied behind the `CredentialStore` seam via `CredentialStoreResolver` — Doriath leaf, NC-vault fallback — referenced by credential UUID and never persisted in plaintext; `SourcesController` strips submitted `authConfig.password`/`secret`). `DatabaseIntrospectionService` introspects the database through the DBAL schema manager into a Register + one Schema per table/view: column types → JSON-Schema `type`/`format` (`SqlTypeMapper`), `NOT NULL` (non-PK, non-defaulted) → `required`, length → `maxLength`, single-column FKs → `$ref` + `objectConfiguration.handling: related-object` with inverse `inversedBy` (so `_extend` and validation work unchanged), composite PKs → deterministic joined ids, no-PK tables list-only. Each schema is bound via `x-openregister-object-source {provider: 'dbal-source'}` and served LIVE by the new `DbalObjectSourceProvider` (parameterized SQL, platform-quoted identifiers, introspected-column allowlist, LIMIT/OFFSET pushdown, real `COUNT(*)`). `ObjectService::paginateObjectSource()` now consults provider `count()` for true page metadata for ALL object-source providers (in-memory fallback preserves the 8 native providers' behaviour) and enforces schema-level read RBAC BEFORE the provider — and therefore the external database — is consulted (denied list reads → 404 mirroring `show()`, no enumeration oracle). New `ObjectSourceErrorMiddleware` maps `DbalObjectSourceException` to 503 (unreachable) / 502 (upstream error) — never a bare 500. `DbalIntrospectionJob` (TimedJob) re-introspects each database source on a schedule, applying drift via `SchemaDiffService` with per-source failure isolation; admin routes `sources/{id}/test-connection` + `sources/{id}/introspect` (contract-tested; introspection failures return a fixed non-sensitive message). Minimal add-database-source UI in the Source modal (`generateUrl`-based). SQLite permits demo fixture + 376 change-scoped unit tests. (`dbal-virtual-registers`) +- **DSAR deadline escalation + GDPR art-35 DPIA pattern detection (dsar-escalation-and-dpia).** Closes the two OpenRegister gaps from pipelinq's `consume-or-dsar` deletion audit. **Deadline escalation:** a generic hourly `TemporalCalculationSweepJob` + `TemporalCalculationSweepService` re-materialises calculated fields whose expressions reference the evaluation clock (`now`) — the DSAR `escalationTier` — for objects in non-terminal lifecycle states, persisting only changed values through the normal `ObjectService` write path so the register's already-declared `deadlineAdvanceReminder` / `deadlineEscalation` / `deadlineBreach` `calculatedChange` rules finally dispatch on tier crossings (they were dead letters — `calculatedChange` only fires on a write, and nothing re-computed a time-dependent tier for an untouched case). A write-once `breachedAt` stamp (a calculation) records the breach crossing, and the `deadlineBreach` rule now also reaches a privacy-officer group resolved from the active `dsarPolicyPack` via the new `PrivacyOfficerRecipientResolver` (`kind: expression` recipient). `daysRemaining` / `isOverdue` / `escalationTier` are now declared as schema properties so their materialised values persist to magic-table columns. **DPIA detection:** a daily `DsarDpiaDetectionJob` + pure `DpiaPatternDetectionService` group DSAR cases received inside a rolling window by pack-configured characteristics (default type + normalised scope) and set `dpiaRequired = true` on every case of a group reaching the pack threshold through an audited `dpia.detected` write — idempotent (flagged cases never re-written), one-way ratchet (never clears a manual flag), fail-safe (no pack / no `dpiaDetection` block ⇒ no-op), and dispatching the officer notification through the declared `dpiaFlagged` rule (no imperative dispatch, gate-18). Threshold / window / grouping / officer group are `dsarPolicyPack` data (config as data). The new `ImportDsarRegisters` repair step imports/upgrades the DSAR case + policy-pack registers from `lib/Settings` (ADR-037). Both jobs carry `IAppConfig` enable/interval toggles. Also fixed: `ImportHandler::importSeedData` now targets the register imported in the same run (first-import seeds previously failed with "Cannot insert object without register and schema context"). (`dsar-escalation-and-dpia`) +- **Semantic object handoff engine (ADR-051).** Cross-app workflows can now hand objects over via canonical semantic kinds (`https://openregister.app/ns#Case`, `ns#Quote`, `ns#Contract`, `ns#Invoice`) instead of point-to-point bridges. A source schema declares handoffs via the new `x-openregister-handoff` dialect (validated at schema save: typed `handoff-bad-target-type` / `handoff-bad-mapping-expression` / `handoff-bad-success-update` errors); an implementing schema binds each kind-contract field to its own properties via the new `handoffContract` block (`handoff-contract-incomplete` on missing mandatory fields). `HandoffService` executes conversions on top of the shipped `SemanticTypeResolver` under the CALLER's RBAC (create pre-checked, never escalated), writes `handoff::handed-off-to` / `:originated-from` provenance relations on both objects plus one immutable `handoff.executed` audit row per side, applies the declared `onSuccess.set` source update through the lifecycle-aware write path, and dispatches a typed ADR-041 `HandoffExecutedEvent` (provenance + correlationId + deferred flag) after completion. Degradation when no installed schema implements the kind: `hide` → machine-readable `handoff-provider-unavailable` (409, never 5xx); `queue` → durable park in the new `oc_openregister_handoff_queue` table (WebhookLog pattern), drained by schema-save/app-enable listeners + a 5-minute fallback TimedJob AS the recorded requester with RBAC re-evaluated at drain time (`failed-permission` entries notify the requester). New REST surface: `GET/POST /api/objects/{register}/{schema}/{id}/handoffs[/{handoffId}]` (availability with `available`/`unavailable(reason)`/`queued` states; execute returning 200 executed / 202 parked / 404 not-declared / 409 unavailable). Lifecycle-triggered handoffs (`trigger: "lifecycle:"`) fire from `ObjectTransitionedEvent` for real actors only. Fixed alongside: PostgreSQL-fatal MySQL-only `SHOW VARIABLES` probes in `MagicMapper`/`MagicBulkHandler` are platform-guarded (a swallowed failing statement aborts caller-managed postgres transactions, SQLSTATE 25P02); `SchemasController` validator failures surface as HTTP 400 with the typed message instead of a generic 500. (`semantic-object-handoff-engine`) +- **Search-trail recording wired up, restoring the dashboard "Popular Search Terms" widget and "Searches" KPI.** `SearchQueryHandler::logSearchTrail()` (previously a commented-out `// TODO` stub) is now implemented and invoked from `ObjectService::searchObjectsPaginated()` for both the SOLR/index and database paths — response time is measured from a `microtime(true)` captured at method entry, and each entry records the search term, page result count, total matching results, response time, and execution type. A new admin retention setting `searchTrailRecordingMode` (`all` | `_search` | `none`, default `_search`) gates what is recorded: `_search` records only free-text searches, `all` records every paginated call, `none` records nothing. It combines with the existing `searchTrailsEnabled` master switch into an effective mode (switch off ⇒ `none`). The setting persists in the existing `retention` appConfig blob — added to the defaults, read path, and update path of `ObjectRetentionHandler` (with a `normaliseRecordingMode` validator) — and round-trips through `GET`/`PATCH /api/settings/retention`; a 3-option `NcSelect` in the retention admin page (`RetentionConfiguration.vue`) lets the mode change without a code deploy. Recording is best-effort: a write failure logs a warning and the search returns normally. No schema changes, migrations, or seed data. (`search-trail-recording`) +- **Anonymisation log table `openregister_anonymisation_log`.** Every successful and failed anonymisation run is now persisted as one row capturing the input file id, originating object UUID / register / schema (when applicable), observed MIME type, engine class label (`OfficeDocumentSanitizer` / `PdfTextReplacer` / `TextReplacer`), `status` (`success` | `failure`), structured `reason` (failure only — e.g. `encrypted`, `validation_failed`), the count of distinct replacements applied, wall-clock duration, and — for Office documents — the JSON-serialised `SanitizationReport` on the nullable `sanitization` column. The column is `null` for non-Office runs (PDF, plain text) and pre-sanitisation legacy runs, satisfying the spec invariant that consumers (DocuDesk's grondslagen-summary renderer) treat `null` as "no sanitisation data; not applicable" rather than an error. New entity `OCA\OpenRegister\Db\AnonymisationLog` with `STATUS_SUCCESS` / `STATUS_FAILURE` constants and a `getSanitizationArray()` decode helper; new mapper `AnonymisationLogMapper` with `findByFileId()` / `findByObjectUuid()` / `findLatestSuccessForFile()`; new idempotent migration `Version1Date20260611000000`. `DocumentProcessingHandler::anonymizeDocument()` writes one row at the end of every run; persistence failures are best-effort (logged PII-free, swallowed) so a DB-side error never masks a successful redaction. (`office-document-sanitization`) +- **Dormant Path B PDF anonymisation fallback scaffold.** A new `PdfOdtFallbackOrchestrator` ships behind the `pdf-anonymisation.path-b-enabled` feature flag (default `false`) — the production seam for the future NC Office (Collabora) PDF→ODT→PDF round-trip rescue path described in `openspec/changes/pdf-anonymisation-odt-fallback`. In the dormant state shipping with this change the orchestrator is wireable but inert: `isEnabled()` returns false, `attempt()` immediately re-raises the original Path A `PdfAnonymisationException(reason: 'validation_failed')` unchanged. The seam enforces the spec's reason guards centrally — `REASON_ENCRYPTED_PDF` and `REASON_TEXT_LAYER_MISSING` never trigger Path B even when the flag is on (encrypted-in stays terminal; image-only defers to OCR). Activation lands in a follow-up PR once telemetry justifies it: an operator installs NC Office, overrides the `NcOfficeConverterInterface` DI binding (default `NullNcOfficeConverter`) with a Collabora-talking implementation, and flips the flag. On Path B sub-step failure the orchestrator raises a new `PdfAnonymisationException::REASON_VALIDATION_FAILED_AFTER_FALLBACK` with a PII-free diagnostic carrying the failed `pathB_stage` (`pdf_to_odt`, `odt_to_pdf`, `rerun_replace`). The new reason code mirrors `validation_failed`'s HTTP 500 mapping so the response surface stays consistent for v1 callers. (`pdf-anonymisation-odt-fallback`) +- **PDF anonymisation fixture-checkin guard + Newman counterpart.** Both `tests/Integration/Pdf/AnonymisationFlowTest.php` and `tests/postman/openregister-anonymisation-tests.postman_collection.json` now ship with a fixture-checkin guard: each test enumerates `tests/fixtures/pdf-anonymisation/*.pdf` (PHPUnit) or checks `pm.collectionVariables.get('fixtureFileId')` (Newman) at run-time and skips cleanly when no fixture is bound. Dropping a fixture into the directory (or binding the collection variable) auto-promotes the deferred test to a live assertion on the next CI run — no code change required. Each fixture pairs with a `*.expected.json` sidecar listing the substitution map and the must-be-absent assertion list; both layers re-extract the anonymised output via smalot and fail the run when any banned needle survives. Contract documented at `tests/fixtures/pdf-anonymisation/README.md`. (`pdf-anonymisation`) +- **PDF anonymisation via SAPP byte-replace (Path A).** `POST /api/files/{fileId}/anonymize` now produces a clean anonymised PDF for `application/pdf` inputs instead of the corrupt byte-level `str_ireplace` result the previous code path emitted. A new `PdfTextReplacer` wraps the `ddn/sapp` fork's `replace_text_in_document()` API with the OpenRegister anonymisation conventions — `[: ]` placeholders rendered via a Helvetica font switch (one of the 14 PDF base fonts, guaranteed to render even when the original font is a subset), longest-needle-first ordering so overlapping entities cannot clobber each other, adjacent-duplicate placeholder collapse, and a mode-dependent validation gate (re-extract via `smalot/pdfparser`, case-sensitive whitespace-normalised substring probe). A new `PdfMetadataSanitizer` strips PII-bearing fields from the `/Info` dictionary (`Title`/`Author`/`Subject`/`Keywords`/`Creator` → `[REDACTED]`; `Producer`/`CreationDate`/`ModDate` preserved) and the `/Metadata` XMP stream (`dc:*`/`xmp:*`/`pdf:*` element bodies sentinel-replaced; custom namespaces preserved). The pipeline probes for encryption (`/Encrypt` trailer ref + smalot "secured" message) and an empty text layer up front, raising structured `PdfAnonymisationException` reasons that `FileTextController::anonymizeFile` maps to HTTP statuses (`encrypted_pdf` → 422, `text_layer_missing` → 422 with defer-to-OCR semantics, `validation_failed` → 500, `internal_error` → 500). The response body shape is `{ "error": "pdf_anonymisation_failed", "reason": "", "details": { ...PII-free diagnostic counters... } }`. Per ADR-005 the operator-supplied entity text never appears in HTTP responses or error logs — only structural counts (`streams_scanned`, `streams_modified`, `tj_arrays_modified`, `subset_font_fallbacks_used`, `font_encoding_misses`, `cid_split_mismatch`, etc.) are surfaced. The substitution-map convention from `entity-relation-grondslagen` is preserved unchanged — every variant of one entity resolves to the same `[: ]` placeholder. DOCX / ODT / plain-text branches are untouched. No new HTTP endpoints. (`pdf-anonymisation`) +- **`RegisterResolverService` resolves `_register` / `_schema` IAppConfig keys to entities.** A new public DI-friendly service (`OCA\OpenRegister\Service\RegisterResolverService`) replaces the duplicated `getValueString(...) + manual mapper-lookup` pattern observed in 13 audited call sites across opencatalogi (5 controllers), pipelinq (8 services / jobs), and docudesk (`OpenRegisterResolver`). Five public methods (`resolveRegisterId`, `resolveSchemaId`, `resolveRegister`, `resolveSchema`, `resolvePair`) return either slug/UUID strings or hydrated `Register` / `Schema` entities; `enumerateAppConfigs()` powers admin diagnostics. The service is multi-tenant aware (entities resolved through the mappers' `applyOrganisationFilter` path; explicit `$organisationUuid` override for cross-tenant admin tooling), caches resolved entities at the request scope, and ships typed exceptions (`MissingConfigException`, `RegisterNotFoundException`, `SchemaNotFoundException`) under `OCA\OpenRegister\Service\Resolver\Exception`. A companion `occ openregister:resolver:list ` console command prints every resolver-shaped IAppConfig key for an app. Naming convention documented in `docs/services/register-resolver.md` (prefer `_register` / `_schema`; bare `register` / `schema` grandfathered as `default_register` / `default_schema` in the enumeration). (`register-resolver-service`) +- **`RegisterSerializer` honours `_extend` for DI consumers, not just HTTP.** A new entity serializer (`OCA\OpenRegister\Service\Serializer\RegisterSerializer`) owns the `schemas` and `@self.stats` expansion logic that used to live inline in `RegistersController::index()`. Two companion methods on `RegisterService` — `findSerialized(id, _extend)` and `findAllSerialized(..., _extend)` — give DI consumers (DocuDesk's `RegisterDiscoveryService`, opencatalogi, softwarecatalog) the same expanded payload the HTTP endpoint produces. `Register::jsonSerialize()` stays ID-only by contract (no behavioural change); expansion is an opt-in serializer step. The controller still drives the HTTP wire format unchanged on the happy path — the refactor is structural — with **one deliberate divergence on the edge case**: when a register references a schema ID that has been deleted, the response now retains the orphan ID in its original array position instead of silently dropping it. This produces a heterogeneous `schemas` array (objects + bare IDs) on the affected edge case — a wire-format change that JSON consumers in statically-typed clients (Go/Java/Kotlin) MUST be prepared for. The orphan ID is logged as a warning. Mapper-layer `_extend` plumbing is removed (`RegisterMapper::find` / `::findAll` no longer accept `_extend` — it was never honoured there). (`register-service-extensions`) +- **Office document sanitiser (DOCX + ODT) strips PII-bearing non-text structures during anonymisation.** A new `OfficeDocumentSanitizer` (strategy pattern: `DocxSanitizer` / `OdtSanitizer` behind `SanitizerInterface`) runs ahead of the entity-anonymisation walker for `.docx` and `.odt` inputs. It performs XML-level surgery on a temp copy of the ZIP container (the original Nextcloud file is never mutated) to: remove comments (`word/comments*.xml` + inline markers / `office:annotation`), accept tracked changes (unwrap ``/`text:change-start..end`, drop ``/`text:change`), strip revision (`w:rsid*`) attributes, remove all custom XML parts (`customXml/item*.xml`) while unwrapping data-bound content controls to preserve visible text, scrub document metadata (`docProps/core.xml`/`app.xml`/`custom.xml`, ODT `meta.xml`) to the sentinel `DocuDesk Anonymisation`, strip person-identity field codes (`AUTHOR`/`USERNAME`/`USERINITIALS`/`LASTSAVEDBY`, both simple `` and complex `` forms; ODT `text:author-name`/`-initials`/`initial-creator`), and flatten hyperlinks (drop the URL + relationship, keep the visible text). Removed parts are reconciled out of `[Content_Types].xml` and `_rels/*.rels` so the output opens cleanly in Word/LibreOffice. ODT is now a supported anonymisation format (previously fell through to a raw-string replacer that corrupted the container). A `SanitizationReport` value object captures per-category counts (PII-free, `JsonSerializable`) and is retained on `DocumentProcessingHandler::getLastSanitizationReport()` for audit; logging is counts-only per ADR-005; XML parsing is XXE-safe. (`office-document-sanitization`) +- **Session-less writes are attributed to a system identifier so service-written rows stay visible.** When an OpenRegister object is created without an active `IUserSession` user (cron jobs, queue workers, internal service calls such as openconnector's `CallService::call()`), `SaveObject` now sets `_owner` to a configured system identifier (default `__system__`, read from the `openregister.systemUserId` app-config key) instead of persisting an empty `_owner` — which the REST list endpoint's RBAC filter previously hid from every user including admins (chain-E `total: 0` bug, #1617). An explicit non-empty owner set by the caller is preserved. The read side carves out visibility for those rows: `MagicRbacHandler::applyRbacFilters()` and its raw-SQL twin `buildRbacConditionsSql()` make `_owner = ` rows visible to admins (already covered by the existing full RBAC bypass) and to any group listed in the `openregister.systemReaderGroups` app-config key (comma-separated, default empty). The default `__system__` cannot collide with a real user because Nextcloud's user-ID validator rejects double-underscore prefixes. Organisation/multitenancy isolation is untouched — system rows still carry an `_organisation` and tenant boundaries hold independently. No DB migration runs; operators backfill pre-existing `_owner=''` rows manually if desired. (`system-context-owner-attribution`) +- **`POST /api/files/{fileId}/manual-entities` — operator-supplied "add this exact text to the file's anonymisation list" endpoint.** Performs chunk-aware exact-string matching against the file's already-extracted text (`wholeWord` + `caseSensitive` defaults to `true`), creates a `GdprEntity` catalogue entry for `(value, type)` (or reuses an existing one — the catalogue row is shared across files; the response's `entity.reused` boolean signals which path was taken), and inserts one `EntityRelation` per occurrence with `detection_method = manual`, `role = anonymisable`, `confidence = 1.0`. Idempotent on retry — each match position is probed via `EntityRelationMapper::existsForFileAtPosition(fileId, entityId, chunkId, positionStart, positionEnd)` before insert; already-present rows bump `matchesSkipped` and don't re-insert. Atomic per call: catalogue write + relation inserts + audit-trail entries all happen inside one `IDBConnection::beginTransaction()` / `commit()` / `rollBack()` window. Zero-match calls return HTTP 200 (not 201) with a `message` field; the catalogue entry is still created/reused so the value is available for use on other files. Audit-trail: `entity_create` (only when a new catalogue row is inserted) and `entity_relations_batch_create` (every call, including zero-match — operator intent is always recorded); per ADR-022 the `value` is allowed in the audit-trail's `changed.fields` payload only — never in HTTP logs or error responses. RBAC: caller must have file-write access (`isUpdateable() === true` on the resolved node in the actor's user-folder) — same model as `PATCH /api/entity-relations/{id}`. ADR-005 PII redaction: the request log captures `valueLength` only (no `value`); error responses never echo the operator-supplied value. Manual-method relations are picked up by the existing `POST /api/files/{fileId}/anonymize` flow unchanged — `EntityRelationMapper::findEntitiesForAnonymization` does not filter on `detection_method`. Status codes: 415 (non-JSON), 401 (no session), 400 (missing `value`/`type` or value longer than the chunk overlap), 403 (file-write denied), 422 (file not extracted — operator must trigger extraction first), 201 (matches found), 200 (zero matches), 500 (unexpected). Endpoint is `@NoAdminRequired`. ([#1593](https://github.com/ConductionNL/openregister/issues/1593)) - **Register & schema read endpoints are reachable when OpenRegister is restricted to a user group.** `RegistersController::index`/`show` are now `#[PublicPage]` (schema/object reads already were), so the read surface survives Nextcloud's *"Limit app to groups"* restriction — which otherwise blocks every non-public route for users outside the group, including consuming apps. Write/management endpoints stay non-public, so the restriction keeps limiting management to the group. A "logged-in unless published" guard on register/schema `index`/`show` means anonymous callers see only published resources (list filtered; `show` → 401 on unpublished), while authenticated users are unaffected (RBAC-scoped as before); the gate derives from server-side `published`/`depublished` state, never client params. (`register-schema-read-accessibility`) - **Schema-declared in-app/push notifications now render, with override-only per-user preferences.** Object-lifecycle notifications declared in a schema's `x-openregister-notifications` (`object_created` / `object_updated` / `object_transitioned`) now render in the Nextcloud bell via `AnnotationNotifier` — localised nl/en, with a primary action deep-linking to the object (a schema's custom per-locale `subject` wins, else a canonical string); `Notifier` keeps `configuration_update_available` and the two are mutually exclusive by subject so the manager never double-renders. Push needs no extra code (`notify_push` intercepts the same `IManager::notify()`). Notification **rules** live ONLY in the schema annotation (no rule table) and are evaluated at dispatch from the always-loaded schema. Per-user **preferences** are override-only Nextcloud user-config values (`notification_pref//`) exposed via `GET`/`PUT /api/notification-preferences`: a stored override only flips the schema default (on/off, optionally narrowing channels) for one `(schema, notification)` pair, and absence falls through to the default — so adding a schema or a new notification works with **zero migration**. The dispatcher resolves `schema-default ⊕ user-override` **per recipient** before delivering the in-app/push channel, skipping recipients who opted out without affecting others. The legacy per-`(register, schema)` `NotificationSubscription` table + controller are **deprecated**, with a one-shot repair step migrating existing rows to user-config overrides. (`notification-schema-rules-and-userconfig-prefs`) - **EML (`message/rfc822`) support in `TextExtractionService`.** Two output paths share an underlying `zbateson/mail-mime-parser` invocation: (1) a flat plain-text path used by `extractFile` for entity detection — header block (`From` / `To` / `Cc` / `Subject` / `Date`), blank line, body (`text/plain` preferred over `text/html`-stripped-to-text), attachments listed under `--- Attachment: ---` markers; nested EML attachments are inlined via recursive flattening; (2) a new public `TextExtractionService::parseEmlStructured(File): EmlStructure` that returns headers + `EmlBody` (`plainText`, `html`) + array of `EmlAttachment` (decoded binary bytes — not the base64 transport string — plus filename / MIME / inline / contentId / `nestedEml`). Recursion is capped at depth 3 (root = depth 0; deeper `message/rfc822` attachments expose an `EmlAttachment` shell with `nestedEml = null`). `parseEmlStructured` MUST throw `EmlParseException` on irrecoverable malformed input — consumers (notably DocuDesk's `eml-pdf-assembly`) drive their fallback paths via exception propagation. Non-UTF-8 body parts are transcoded via `mb_detect_encoding` + `mb_convert_encoding`. Filename resolution: `Content-Disposition` `filename` → `Content-Type` `name` → generated `attachment-` (1-indexed). Per ADR-005, parser-failure log lines are PII-sanitised — addresses, quoted strings, and angle-bracketed values are replaced with `` before logging. New dependency: `zbateson/mail-mime-parser:^3.0`. (`text-extraction-eml`) +- **`PATCH /api/entity-relations/{id}` — operator decision-metadata endpoint.** The `EntityRelation` row now carries two operator-decision fields: `bases` (nullable JSON array of UUIDs referencing legal grondslagen) and `skip_anonymization` (boolean, default false). The new endpoint accepts a JSON body with a strict whitelist of `{bases?, skipAnonymization?}` — any other key returns HTTP 400. A parallel DI method `EntityRelationMapper::updateDecisionMetadata(EntityRelation $relation, array $fields, ?IUser $actingUser = null): EntityRelation` provides the same contract for in-process callers (e.g. DocuDesk's anonymise flow). The row UPDATE and audit-trail INSERT run inside a single DB transaction so an audit-INSERT failure rolls back the UPDATE and surfaces as HTTP 500 — clients never observe a persisted decision-metadata change without a matching audit entry. The `bases` diff is multiset-equal (order-insensitive, duplicates collapsed) so cosmetic reorderings do not produce spurious audit entries; `null` vs `[]` remains distinct. Successful writes that produce a non-empty diff emit one immutable audit-trail entry (ADR-022) capturing the acting user UID (per ADR-005, the UID — not the display name), timestamp, row identifier, and per-field previous → new values; semantic no-ops (PATCH with values identical to current state, or empty body) succeed with HTTP 200 and write no audit entry. The endpoint requires write-access to the relation's parent file/object — the same implicit check that `markAsAnonymized` inherits today — and is `@NoAdminRequired`. The post-hoc system fields `anonymized` and `anonymizedValue` are intentionally NOT in the whitelist; those record what the redaction code path actually did and remain writable only by `markAsAnonymized`. (`entity-relation-grondslagen`) +- **Word-family text extraction now captures all content and supports legacy `.doc` and `.odt`.** `TextExtractionService::extractWord()` was rewritten around a recursive PhpWord element walker (`walkWordElements`, depth-capped at `MAX_WORD_DEPTH = 50`) that descends tables (rows → cells, including nested tables and in-cell text-runs / list items), section headers and footers, and document-level footnotes/endnotes — content the previous 2-level traversal silently dropped (notably entire tables, because a PhpWord `Table` exposes `getRows()`, not `getElements()`, so the old loop skipped it). The PhpWord reader is now selected from the file MIME / extension (`resolveWordReader`): `.docx` → `Word2007`, legacy `.doc` → `MsDoc`, `.odt` → `ODText` — previously the default `Word2007` reader was always used, so `.doc` and `.odt` failed to load even when routed. `application/vnd.oasis.opendocument.text` is added to the Word routing in `isWordDocument()`. (`text-extraction-word-completeness`) ### Behaviour changes +- **Dashboard right sidebar is now collapsed by default on initial load.** The dashboard view (route `/`) renders with its right-hand app sidebar closed so the dashboard's primary content is unobstructed on arrival. The sidebar remains fully functional — the standard Nextcloud sidebar toggle opens it, and its filters, system totals, and orphaned-items sections behave identically once open. No other view's sidebar default open/closed state changes. (`dashboard-sidebar-default-state`) +- **File access is permission-based and ownership-agnostic; objects can link files owned by other users.** `FileValidationHandler::checkOwnership()` now grants access on `Node::isReadable()` alone instead of denying when the file's owner UID differs from the session user — fixing `File is not owned by the current session` errors when uploading, viewing, updating, or deleting a file reached via a Nextcloud share or owned by a user other than the `openregister` system user. The update and delete paths now assert `Node::isUpdateable()` / `Node::isDeletable()` before mutating (failing fast with `NotPermittedException` in addition to Nextcloud's native enforcement), and `FileOwnershipHandler::transferFileOwnershipIfNeeded()` no longer re-owns a file to the system user when the current user already has write rights — ownership follows the folder's mount owner (the system user inside the OpenRegister folder; the original owner for a folder linked from outside it), with re-owning retained only as a fallback. (`file-permission-share-access`) +- **`POST /api/files/{fileId}/anonymize` on PDF inputs now produces a clean output instead of a corrupted file.** Tenants that relied on the previous (broken) behaviour — where the byte-level `str_ireplace` path returned an unopenable PDF — need to verify nothing in their downstream pipeline assumed a corrupt PDF (e.g. mailers that retried on parse failure, viewer dashboards that rendered placeholders for unopenable attachments). The endpoint URL, auth model, and request shape are unchanged. PDF-specific failure responses are now structured (`{ "error": "pdf_anonymisation_failed", "reason": "", "details": { ... } }`) with reason-specific HTTP statuses — see the `### Added` entry for the full reason-to-status table. Encrypted PDFs are explicitly rejected (HTTP 422); image-only scans defer to the OCR capability (HTTP 422 with `text_layer_missing` reason); the strict-mode validation gate fails closed (HTTP 500 with `validation_failed`) when any substitution-map entry still appears in the re-extracted output. (`pdf-anonymisation`) +- **Anonymisation of Office documents now produces a sanitised derivative.** Reviewer comments, tracked-change author metadata, document metadata (Author/Last-Modified-By/Title/Subject/Keywords), person-identity field codes, custom XML data bindings, and hyperlink URLs are removed from the anonymised `.docx`/`.odt` output (tracked changes are resolved with Word's "accept all" semantics — inserts kept, deletions dropped). Original files are preserved unchanged. ODT is now anonymised in-place as a valid container rather than via the legacy raw-string path that corrupted the ZIP. Encrypted Office documents raise a caller-correctable "cannot anonymise an encrypted document" error. (`office-document-sanitization`) - **EML files that previously produced null / empty extracted-text now produce populated text.** Files with `mimetype: message/rfc822` are now extracted via the new EML pipeline rather than being silently skipped. The flat output is suitable for the existing entity-detection pipeline. Tenants that relied on EMLs being skipped (unlikely) need to revisit their downstream flows. Non-EML extractable attachments (PDF, DOCX, text within an EML) are listed by filename + MIME type only in v1 — inline text extraction for those types is deferred (the consumer-side `eml-pdf-assembly` handles rich rendering separately). (`text-extraction-eml`) +- **Anonymise flow now honours `skip_anonymization`.** Rows where `skip_anonymization = true` are excluded from the redaction pass: `EntityRelationMapper::markAsAnonymized` no longer flips `anonymized = true` on skipped rows (added `AND skip_anonymization = 0` predicate); `FileTextController::anonymizeFile` reads relations through the new skip-aware `EntityRelationMapper::findEntitiesForAnonymization` method; `DocumentProcessingHandler::anonymizeDocument` defensively filters out skipped occurrences server-side before text-replacement so the OR contract ("skipped relations are never redacted, full stop") holds regardless of caller behaviour. Skipped rows retain `anonymized = false` after the file's anonymise pass — the operator decision is preserved and queryable via the flag. Files with no skipped rows see behaviour identical to pre-change. The `skip_anonymization` flag is forward-looking: flipping it to `true` on an already-anonymised row does not retroactively un-redact the file. (`entity-relation-grondslagen`) +- **DOCX extracted-text now includes previously-dropped table, header/footer and footnote/endnote content**, so extracted text — and the downstream chunking / entity-detection that consumes it — is longer and more complete for documents that use tables or notes. A per-document Word parse failure (e.g. a legacy `.doc` the limited `MsDoc` reader cannot read) now returns null — logged with structural detail only (file ID / MIME / reader / exception class; no document content, per ADR-005) — instead of throwing, so a single un-parseable file no longer aborts a batch extraction run. The "PhpWord library not installed" deployment guard still throws. (`text-extraction-word-completeness`) +- **`inheritFromPublic` flag on schema and register authorization.** Schemas (and registers, via cascade) can now opt out of the implicit "logged-in users inherit `public` group rights" behaviour. When `inheritFromPublic: false` is set on the authorization block, authenticated users no longer qualify for `public` rules — they qualify only via their own group memberships. Anonymous users are unaffected. The cascade is `schema → register → IAppConfig openregister.rbac.inherit_from_public_default → hard-coded true`; `null` is treated as "unset". Default stays `true`, so existing schemas behave identically to before. Both the PHP-side check (`PermissionHandler::hasPermission`) and the SQL-side filter (`MagicRbacHandler::applyRbacFilters` / `buildRbacConditionsSql`) honour the flag identically. Useful for tiered visibility flows (public catalogue with date-windowed visibility plus a separate authenticated curated view) and privacy-strict schemas where authentication is meant to be a strict gate, not a superset of public access. ([#1439](https://github.com/ConductionNL/openregister/issues/1439)) +- **`inheritFromPublic` defaults to `true` everywhere.** Setting `inheritFromPublic: false` on a schema, on its parent register, or via the tenant-wide `IAppConfig` key `openregister.rbac.inherit_from_public_default` is a deliberate opt-in. Tenants that flip the global default MUST audit existing schemas with public-conditional rules to confirm authenticated users were not relying on those grants. ([#1439](https://github.com/ConductionNL/openregister/issues/1439)) + +### Dependencies +- **`ddn/sapp` consumed from the `Conduction/sapp` fork (Codeberg) for the PDF anonymisation byte-replace pipeline.** A `repositories` entry in `composer.json` points at `https://codeberg.org/Conduction/sapp`, and the `ddn/sapp` constraint pins to a specific work-branch commit SHA on the fork for build reproducibility. SAPP today supports only `/FlateDecode`; the fork extends it with additional stream-filter decoders (LZW / ASCII85 / ASCIIHex / RunLength + filter chaining), a ToUnicode CMap parser + per-font encoding resolver, a TJ-kerning-array flattening pre-pass, the `replace_text_in_document()` flagship API with Helvetica base-font-fallback, and the cross-line matching that pairs vertically adjacent same-font blocks for line-wrapped entities. The upstream-PR series tracking the fork → `dealfonso/sapp` transition lives at [`Conduction/sapp:docs/upstream-prs/`](https://codeberg.org/Conduction/sapp/tree/work/text-replacement/docs/upstream-prs); when upstream merges and tags a release, the `repositories` entry is removed and the constraint becomes a normal version range. (`pdf-anonymisation`) + +### Removed +- **Legacy linked-type constants removed from `LinkedEntityService` and `Schema`.** The deprecated internal map (linked-type id → column-name) on `LinkedEntityService` and the legacy public allow-list constant on `Schema` are gone. Validation now flows through `IntegrationRegistry::isValidIntegrationId()` (the authoritative source post `pluggable-integration-registry`), with a small private legacy-id allow-list (`legacyLinkedTypeIds()`, mirrored in `Schema` and `LinkedEntityService`) preserving compat for `linkedTypes: ['mail', 'todos', 'calendar', 'talk', 'deck', ...]` declarations that pre-date the matching `IntegrationProvider` renames (`mail` → `email`, `todos` → `tasks`). The allow-list MUST NOT grow — new linked-types belong in an `IntegrationProvider`. `LinkedEntityService` now depends on `IntegrationRegistry` (constructor signature change; auto-wired through OR's DI container — no app-info wiring needed). Internal API change only; no public-API behavioural regression for any documented caller. (`cleanup-linked-entity-type-map`) + +### Breaking Changes (cleanup-linked-entity-type-map) +- **`LinkedEntityService::TYPE_COLUMN_MAP` and `Schema::VALID_LINKED_TYPES` constants removed.** These private constants were deprecated in `pluggable-integration-registry` and have now been deleted. There is no public API change — neither constant was documented or exported. Any code that accessed these constants via reflection must migrate to `IntegrationRegistry::listIds()` to enumerate valid linked-entity integration ids. (`cleanup-linked-entity-type-map`) ### Breaking Changes +- **Pre-existing folder bindings are re-validated on every subsequent save.** `ObjectService::ensureObjectFolder()` now calls `FileService::assertObjectFolderAccessible($existingObject)` on the existing-folder branch (defense in depth — PR #1431 review concern). Without this, cross-tenant bindings established before this PR shipped would continue to function on update/PATCH calls that don't touch `@self.folder` — the `setSelfMetadata` access check only fires when the write payload includes `@self.folder`. The re-check throws `FolderAccessDeniedException` (HTTP 403) for legacy bindings the acting user cannot read; operators can use the audit log (`action: "folder_access_denied"`) to identify affected rows and migrate them via the planned `occ openregister:folder-audit` command. (#1342) +- **`@self.folder` writes now require read access to the target folder.** Callers that POST/PUT/PATCH an object with a numeric `@self.folder` value (e.g. `"@self": {"folder": "42"}`) now receive HTTP 403 with body `{"error": "folder_access_denied"}` if the acting user cannot read the referenced Nextcloud folder. Previously the binding was unchecked and silently created a cross-tenant link: an authenticated caller could attach an object — and all of its child file writes — to *any* user's folder by guessing or harvesting node IDs. The new check uses the user's user-folder mount and `Folder::isReadable()`, deliberately avoiding the root-folder fallback (which exists only for anonymous public file reads and is preserved on the general-purpose `getNodeById()` helper). Empty and legacy non-numeric folder values continue through the existing auto-create path unchanged. Every denial writes a forensic audit-trail entry (`action: "folder_access_denied"`) before propagating the exception. **Internal callers that legitimately need to bind to a folder outside the session user's tree must now pass an explicit `IUser $currentUser` to `FolderManagementHandler::createObjectFolderById()` or `SaveObject::setSelfMetadata()`.** The 403 response body does NOT echo the attempted folder ID: doing so would add an enumeration oracle (a caller probing sequential IDs could distinguish "exists but inaccessible" from "does not exist" purely by response shape); the caller already knows which ID they sent, and the attempted ID is recorded server-side in the audit trail. The HTTP status code is exposed as `FolderAccessDeniedException::HTTP_STATUS` (controllers MUST map via this constant, not via `$exception->getCode()`). No internal caller in the OpenRegister codebase regresses (cron jobs and import paths don't set `@self.folder`); downstream apps that already use accessible folder IDs are unaffected. See `docs/api/objects.md#self-folder-access-control-contract` for the full contract. (#1342) - **`@self.files` on rendered objects is now opt-in for full file metadata.** By default, `@self.files` is a lightweight list of integer file IDs (`[123, 456, 789]`). Consumers that need full file metadata (`id`, `path`, `title`, `accessUrl`, `downloadUrl`, `type`, `extension`, `size`, `hash`, `published`, `modified`, `labels`) MUST add `_extend[]=@self.files` (or the equivalent shorthand `_extend[]=_files`) to their request. The change applies to **every** consumer of OpenRegister's render output, including `show` endpoints in dependent apps (e.g. opencatalogi `/publications/{catalogSlug}/{id}`). Migration is a one-line query parameter addition. The previous behavior — full metadata always served on show, no metadata on list — caused asymmetric responses across endpoints and paid the file-lookup cost on every show response regardless of need. The new contract is symmetric across show and list endpoints (both emit `@self.files` as IDs by default; both accept `_extend[]=@self.files` for full metadata) and is documented under the `files-render-extension` capability. **Note:** Using `_extend[]=@self.files` (or `_files`) on **list** endpoints is heavily discouraged because it triggers per-row file/tag lookups (N+1 queries scaling with page size) and will result in degraded performance. Use it only when full file metadata is genuinely required for every row. **SOLR limitation:** on SOLR/index-backed list endpoints, `_extend[]=@self.files` is not yet supported; the lightweight ID list is always returned and the response carries `@self.extend_unsupported: ["@self.files"]` so consumers can detect the mismatch programmatically. Use the database-backed path when full file metadata is required on lists. ### Fixed +- **Strict PDF anonymisation no longer fails on case-variant text and now redacts line-wrapped entities.** Three related fixes diagnosed on a Dutch government letter fixture: (1) `DocumentProcessingHandler::anonymizeDocument` orders the substitution map longest-needle-first so overlapping entities cannot clobber each other (a bare `Amsterdam` LOCATION no longer rewrites `De gemeente Amsterdam` before the longer `gemeente Amsterdam` needle matches, which left the longer entity unmatched and mis-typed). (2) `PdfTextReplacer::validateOutput` is now case-SENSITIVE (`mb_strpos`, mirroring the replacement engine's exact-case guarantee — previously a lowercase URL fragment like `www.amsterdam.nl`, never a detected entity, tripped the case-insensitive probe and failed fully-anonymised documents closed with `REASON_VALIDATION_FAILED`) and whitespace-normalised (both the re-extracted text and each needle are collapsed to single spaces, so entity text the PDF splits across a line break — `14 mei` / `2026` — is detected as residual instead of silently leaking). (3) The `ddn/sapp` pin is bumped to the cross-line-matching commit (Phase 4, Conduction/sapp PR #1) so wrapped entities are actually replaced: vertically adjacent same-font blocks are paired and matched across the wrap, giving the wrapped date its own placeholder. The dev-branch pin is temporary — re-pinning to a tagged sapp release is tracked in #69. On invalid UTF-8 from the re-extraction (the encoding-edge SAPP runs tracked by `font_encoding_misses`/`cid_split_mismatch`), strict mode now fails CLOSED (`validate.normalise`) instead of silently passing unaudited output; lenient mode falls back to un-normalised probing. Verified end-to-end through the DocuDesk anonymise flow: all 34 entities replaced, `unmatchedEntities: []`, strict validation passes. (#65) - **Magic-table read path now coerces every property to its schema-declared PHP type.** Previously only `string` properties were coerced (and even then over-eagerly JSON-decoded scalar JSON like `"true"` / `"123"`); `boolean`, `integer`, `number`, `array`, and `object` properties were returned with whatever type the database driver produced — most visibly, booleans came back as `int 0`/`1` on MariaDB. A new shared `Service\Object\SchemaTypeConverter` is now the single source of truth for both `MagicStatisticsHandler::convertRowToObjectEntity` (single-object / list / POST / PUT response paths) and `MagicSearchHandler::convertRowToObjectEntity` (search path). Every endpoint that returns an `ObjectEntity` (`GET /api/objects/`, `GET /api/objects`, `GET /api/search`, `POST /api/objects`, `PUT /api/objects/`) now produces consistently schema-typed JSON. **Consumer-impact note:** consumers that depended on the broken behaviour (e.g. JS `value === 1` for "true") must switch to native truthy checks; OpenConnector register-backed sync flows and frontend widgets now receive correctly-typed values automatically. (`fix-magic-table-type-coercion`) - **`date_histogram` facets on schema-backed magic tables now work correctly on MariaDB/MySQL.** Previously `MagicFacetHandler` emitted PostgreSQL-only `TO_CHAR(..., '')` SQL unconditionally — on MariaDB this either returned empty buckets (MariaDB < 10.6, no `TO_CHAR`) or wrong keys for week/quarter (MariaDB ≥ 10.6, no ISO output, no quoted-literal patterns). A new `buildDateKeyExpr()` helper now branches on `getDatabasePlatform()` and emits native `DATE_FORMAT(..., '')` (with `CONCAT(YEAR(...), '-Q', QUARTER(...))` for the quarter interval) on MariaDB while leaving the PostgreSQL path unchanged. Weekly bucket keys are now ISO 8601 (`%x-%v` on MariaDB, `IYYY-IW` on PostgreSQL) for cross-DB parity at year boundaries — the legacy `MariaDbFacetHandler` and `MetaDataFacetHandler` paths are aligned to the same ISO format. The `getDateBoundsForBucket()` week branch on `MagicFacetHandler` is fixed to use `DateTime::setISODate()` instead of `strtotime($dateKey)` — the latter parsed `'2025-12'` as "December 2025" instead of ISO week 12 of 2025, producing wrong `from`/`to` bounds. **Breaking-change note:** MariaDB consumers that already cached `%Y-%u` week keys will see different values around Jan 1 of some years (e.g. `2024-00` becomes `2023-52`); flagged for transparency, real-world impact is expected to be zero given the broader path was non-functional. (`fix-date-histogram-mariadb`) - Schema-level RBAC now honours the full operator set (`$eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$exists`) and dynamic variables (`$organisation/$userId/$now`) for conditional `match` clauses, matching the behaviour of the SQL row-level filter. Previously `PermissionHandler::evaluateMatchConditions()` was an equality-only reimplementation, so any `find()`/single-object fetch against a schema using, for example, `{ "read": [{ "group": "public", "match": { "publishDate": { "$lte": "$now" } } }] }` would throw "User 'Anonymous' does not have permission" even though the list endpoint correctly returned the same object. `PermissionHandler` and `MagicRbacHandler::hasPermission()` now delegate conditional match evaluation to the shared `ConditionMatcher` service (the same matcher already used by `PropertyRbacHandler`). The SQL emitter in `MagicRbacHandler::applyRbacFilters()` is unchanged. Fixes OpenCatalogi `PublicationsController::attachments` returning 500 on publications whose schema uses operator-based public rules. ([#1336](https://github.com/ConductionNL/openregister/issues/1336)) @@ -27,6 +71,8 @@ ### Security - **Object writes now fail closed for anonymous callers.** Creating or updating an object as an **anonymous** caller (no resolved Nextcloud user) is now denied with HTTP 403 unless the target schema's `authorization` explicitly grants the `public` group the requested write action. Previously `PermissionHandler::hasGroupPermission()` returned `true` for everyone when a schema had no `authorization` block (or no entry for the action), so an anonymous `POST /api/objects/{register}/{schema}` against a no-rule schema returned 201 (object created) — runtime-confirmed on `decidesk/meeting`. The new denial is scoped strictly to anonymous principals in `PermissionHandler::evaluatePermission()` (via a `publicGroupExplicitlyGranted()` opt-in check over `create`/`update`/`delete`); **authenticated-user write behaviour is unchanged**, and schemas that declare a `public` create/update rule still accept anonymous submissions. ([#1955](https://github.com/ConductionNL/openregister/issues/1955)) - **SQL/list RBAC `match` evaluation now fails closed on null-resolved dynamic variables.** When a `match` rule references a dynamic variable (`$organisation`/`$userId`/`$now`) that resolves to `null` for the current principal, the SQL/list path (`MagicRbacHandler::buildPropertyCondition` / `buildPropertyConditionSql` / comparison-operator conditions) now emits an impossible predicate (`1 = 0`) for that property instead of silently dropping it from the AND. Previously a dropped predicate degraded a multi-condition `match` to its surviving static predicate, so a `public` read rule like `{ "match": { "status": "published", "organisation": "$organisation" } }` made the LIST endpoint leak objects to an anonymous caller (null `$organisation`) that the single-object FIND path correctly denied — runtime-confirmed cross-method bypass. The list path now produces identical verdicts to the PHP/find path (`ConditionMatcher`); rules whose variables resolve are unaffected (no new denials). ([#1953](https://github.com/ConductionNL/openregister/issues/1953)) +- **Multitenancy bypass now honours `inheritFromPublic`.** The list-path multitenancy short-circuit (`MagicRbacHandler::hasConditionalRulesBypassingMultitenancy` → `simpleRuleBypassesMultitenancy` / `conditionalRuleBypassesMultitenancy`) treated a `public` rule as an unconditional cross-tenant bypass. With `inheritFromPublic: false` and a schema listing `read: ['public', 'authenticated']`, an authenticated user's query skipped the per-tenant filter and the surviving `authenticated` rule then returned rows from **all** tenants — a cross-tenant leak. The bypass decision is now gated by the resolved `inheritFromPublic` value via `qualifiesForPublic($userId, $inheritFromPublic)`, identical to the three already-gated RBAC paths (`applyRbacFilters`, `buildRbacConditionsSql`, `hasPermission`). Anonymous users still bypass for `public` (unchanged); authenticated users only when public inheritance is enabled. ([#1439](https://github.com/ConductionNL/openregister/issues/1439)) +- **`inheritFromPublic` register cascade is now conservative across multiple registers.** When a schema belongs to more than one register, the cascade previously resolved against an arbitrary "first register wins" scan order (`getFirstRegisterWithSchema`), making the security verdict non-deterministic across nodes/restores. It now consults every register containing the schema (`RegisterMapper::getAllRegisterIdsWithSchema`) and the most-restrictive explicit value wins: a single register with `inheritFromPublic: false` disables inheritance even if another sets `true`. Resolution also fails to the configured tenant default (not a hard-coded grant) on error, symmetric across both RBAC callers. ([#1439](https://github.com/ConductionNL/openregister/issues/1439)) ## 0.2.13-unstable.78 – 2026-04-03 diff --git a/CODE-REVIEW-IMPROVEMENT-PLAN.md b/CODE-REVIEW-IMPROVEMENT-PLAN.md new file mode 100644 index 0000000000..c9ac744411 --- /dev/null +++ b/CODE-REVIEW-IMPROVEMENT-PLAN.md @@ -0,0 +1,340 @@ +# OpenRegister Code Review & Improvement Plan + +> **Status:** Draft 1 — generated 2026-06-11 from a full multi-agent code review of `lib/` (~371k LOC PHP, 953 files) and `src/` (~115k LOC Vue/JS). +> **Audience:** developers and AI coding agents picking up individual TODOs. Every item is self-contained: it names the exact file, the change to make, and how to verify it. +> **How findings were produced:** eight parallel review agents (security ×2, bugs ×3, performance, refactor, frontend, ops) each read the real code and cited `file:line`. Every CRITICAL and HIGH item below was then **adversarially re-verified** by a second agent against the source. Items marked _verified_ were confirmed by that second pass; items marked _unverified_ came from a single agent and should be re-checked before large changes. + +--- + +## How to use this document + +1. Work **top-down by priority**: P0 (security/data-loss) → P1 (correctness bugs) → P2 (performance) → P3 (refactors) → P4 (frontend/ops hygiene). +2. Each task has a stable ID (e.g. `SEC-CTRL-1`). Reference it in commit messages and PR titles. +3. **Project rules that override your defaults** (from `.claude/CLAUDE.md` and team memory): + - Use the **Edit/Write tools only** for code changes — never `sed`/`awk`/scripts (high risk of breaking files). + - PHP must pass `composer check:strict` (PHPCS, PHPMD, Psalm, PHPStan). Run `bash scripts/run-hydra-gates.sh` (or the `/hydra-gates` skill) before pushing. + - **Fix pre-existing quality issues** you touch — don't leave them. + - **Bump `appinfo/info.xml` ``** on any change that affects the JS bundle or app behaviour (NC serves bundles `immutable` for ~6 months). + - i18n keys are **English source strings** — never Dutch. + - Don't add `Co-Authored-By` trailers to commits. + - The local OCP/PHPStan stubs are stale — validate auth/PHPStan reasoning by `php -l` + logic, defer deep PHPStan to CI. + - OR's real `ObjectService` API is `find / findAll / saveObject / createObject / updateObject / deleteObject` — do not invent methods. +4. **Add a characterization test BEFORE refactoring a giant class** (see `REF-15`). The giants already have test files — extend them. +5. Verify against the running dev container: `http://localhost:8080` (admin:admin), app at `/apps/openregister/`. Reset brute-force: `docker exec nextcloud php occ security:bruteforce:reset 127.0.1`. + +--- + +## Executive summary + +OpenRegister is structurally healthy for its age: the worst god-classes already have extraction sub-directories and ~585 unit tests, all `appinfo/routes.php` targets resolve, and the dominant RBAC pattern (derive `$rbac` from `isCurrentUserAdmin()`, enforce in `ObjectService`/`PermissionHandler`) is sound. The problems are at the **edges where that discipline lapses**, and they cluster into five themes: + +1. **Request-controlled security switches.** A *public* listing endpoint and the bulk/import paths let the caller pass `?rbac=false&_multi=false` to turn off RBAC **and** multitenancy. (`SEC-CTRL-1`, `SEC-CTRL-6`) +2. **The bulk write path is a second-class citizen.** `SaveObjects` accepts `_validation`/`_events`/`_rbac` flags and **ignores all three** — no per-object authorization, no schema validation, no events, no audit trail, and it lets clients **forge `@self.owner`/`@self.organisation`** (cross-tenant injection). Single-object save does all of this correctly. (`BUG-OBJ-1`, `BUG-OBJ-15`) +3. **Server fetches user-controlled URLs without SSRF protection** in three places; only one ad-hoc guard exists and it isn't on the user-reachable paths. Two Twig environments compile user-authored templates **without a sandbox** (SSTI). (`SEC-SVC-1/2/3/4`) +4. **Silent failures hide data problems:** webhooks record 4xx/5xx as success, GDPR erasure reports success while skipping failed objects, a file-property update drops previously-stored fields, and object/file deletes never clean up Nextcloud files. (`BUG-SVC-1`, `BUG-SVC-2`, `BUG-OBJ-8`, `BUG-OBJ-2`) +5. **Dead registrations:** the entire event-driven Actions feature, all non-create webhooks, and several background jobs are never wired up, so they silently never run. (`OPS-1/2/3/4`) + +Performance work is dominated by N+1 query patterns in the render path and per-row redundant work in the magic mapper. + +### Priority counts + +| Priority | Theme | Count | +|---|---|---| +| **P0** | Security + data-loss (critical/high, verified) | 14 | +| **P1** | Correctness bugs (medium) | ~18 | +| **P2** | Performance | 14 | +| **P3** | Refactor / architecture | 15 | +| **P4** | Frontend + ops hygiene | ~20 | + +--- + +# P0 — Security & data-loss (do first) + +> All P0 items were adversarially verified against source unless noted. + +## SEC-CTRL-1 — Public listing endpoint lets anyone disable RBAC + multitenancy _(critical, verified)_ +- **File:** `lib/Controller/ObjectsController.php` — `index()` (method ~`:970`, annotations `@NoAdminRequired @NoCSRFRequired @PublicPage` at `:956-960`); flags read at `:1021` and `:1023-1024`; forwarded at `:1052-1053`, `:1092-1093`, `:1270-1274`. Enforcement bypass: `lib/Service/Object/PermissionHandler.php:216-219` (`if ($_rbac === false) { return true; }`). +- **Problem:** `index()` is reachable anonymously and derives `rbac`/`multi` from **request params** instead of admin status. Any caller can request `?rbac=false&_multi=false` to list every object across all organisations and ACLs. `geoSearch()` re-invokes `index()` and inherits this. +- **Fix:** + 1. In `index()`, delete the two lines that read `rbac`/`multi` from `$params` (~`:1021-1024`). + 2. Replace with the admin-derived posture already used by `show()` (see `:1754-1757`): + ```php + $isAdmin = $this->isCurrentUserAdmin(); + $rbac = ($isAdmin === false); + $multi = ($isAdmin === false); + ``` + 3. Search the file for `'rbac'`, `'_multi'`, `'multi'` and ensure no surviving path takes them from `$params`/`getParam` (also check `objects()`, `crossTableSearch()`, `geoSearch()`). +- **Verify:** As a non-admin/anonymous user, `curl 'http://localhost:8080/index.php/apps/openregister/api/objects//?rbac=false&_multi=false'` must return only authorized objects. Add a Newman test asserting the `rbac=false` result count equals the default for a non-admin. + +## SEC-CTRL-6 — Bulk import still forwards request-controlled `rbac`/`multi` _(medium→high, verified pattern)_ +- **File:** `lib/Controller/RegistersController.php:1299-1300` (Excel branch) + the CSV branch just below; forwarded to `importService->importFromExcel(_rbac:..., _multitenancy:...)`. +- **Problem:** Even though *who* may import is gated by `checkRegisterManagePermission()`, the dangerous flags are still read from the request, so a manager can pass `?multi=false` to write objects **across organisation boundaries**. +- **Fix:** Stop reading `rbac`/`multi` from the request here. Derive `$rbac = !$this->isCurrentUserAdmin();` and **hardcode `_multitenancy: true`** so imports stay in the caller's active organisation. Apply to both Excel and CSV branches. +- **Verify:** Import with `?multi=false` as a non-super-admin manager; confirm objects land only in the caller's active organisation. + +## SEC-CTRL-3 — IDOR: any authenticated user can read/export any configuration _(high, verified)_ +- **File:** `lib/Controller/ConfigurationController.php` — `show()` `:216`, `preview()` `:613`, `export()` `:718` (all `@NoAdminRequired`, no guard). Contrast `create()` `:333`, `update()` `:406`, `destroy()` `:511`, `import()` `:654` which all start with `if ($this->isCurrentUserAdmin() === false) { return 403; }`. +- **Problem:** `export()` with `?includeObjects=true` serialises a configuration's objects — bulk exfiltration by incrementing the numeric `id`. +- **Fix:** Add the same admin guard to the top of `show()`, `preview()`, `export()`: + ```php + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + ``` + (If non-admins are meant to read configs they own, use an owner check instead of leaving it open.) +- **Verify:** As a non-admin, `curl '.../api/configurations/1/export?includeObjects=true'` → 403. Add an integration test. + +## SEC-CTRL-2 — Public `names` endpoint leaks object/organisation names _(high, partially verified)_ +- **File:** `lib/Controller/NamesController.php` — `index()` `:114-117` (`#[NoAdminRequired]`/`#[PublicPage]`, no auth check, calls `getAllObjectNames()` at `:173`). Backing: `lib/Service/Object/CacheHandler.php` `warmupNameCache()` — `findAllWithUserCount()` `:1460` (all orgs) + `getObjectMapper()->findAll()` `:1472` (no RBAC). +- **Verification nuance:** `index()` + `warmupNameCache` confirmed unauthenticated and RBAC-blind. **But** `create()` requires an `ids[]` body (resolves via `getMultipleObjectNames`, not the full dump), and `MagicMapper::findAll()` returns `[]` when no register/schema is supplied (`MagicMapper.php:7032-7038`), so the *full* unbounded object-name dump may not materialise via that exact path — the **organisation** name leak and the per-`ids` resolution leak are the concrete risks. +- **Fix:** + 1. Require authentication: inject `IUserSession`, return 401 when `getUser() === null`, and drop `#[PublicPage]` from `index()` and `create()`. + 2. Make name resolution RBAC/tenant-aware: filter `getMultipleObjectNames()`/`getAllObjectNames()` by the caller's read permissions + active organisation. Never expose an unbounded global name map over HTTP. + 3. Apply the same to organisation names in `warmupNameCache()`. +- **Verify:** `curl .../api/names` logged-out → 401. As a low-priv user, `POST /api/names {"ids":[""]}` must omit that UUID. + +## BUG-OBJ-1 — Bulk save bypasses validation, RBAC, events, and audit _(critical, verified)_ +- **File:** `lib/Service/Object/SaveObjects.php` — `processObjectsChunk()` `:963-1017` declares `bool $_rbac, $_multitenancy, $_validation, $_events` (`:966-969`) but the body only calls `transformChunk → persistChunk → buildChunkResults` and **references none of them**. Caller: `lib/Controller/BulkController.php:345-398` — the comment at `:349-351` falsely claims per-object RBAC runs; mixed-schema (`schema=0`) skips even the schema-level gate at `:354-370`. +- **Problem:** A user POSTing bulk with `schema=0` can mass-insert into schemas/registers they cannot write individually, write data that fails `hardValidation`, fire no lifecycle events, and leave no audit trail. This is a wholesale regression of every guarantee single-object `saveObject` enforces. +- **Fix (in `processObjectsChunk`, before `persistChunk`):** + 1. When `$_rbac === true`, inject `PermissionHandler` and check `create`/`update` permission per object; move failures into `$result['invalid']`. + 2. When `$_validation === true`, run `ValidateObject::validateObject()` per object against the resolved schema; reject invalid ones. + 3. When `$_events === true`, dispatch `ObjectCreatedEvent`/`ObjectUpdatedEvent` after persist. + 4. Always write the audit trail (unless an explicit `silent` flag is set). + 5. In `BulkController`, add an explicit per-object RBAC gate for the mixed-schema path and delete the false comment. +- **Verify:** As a non-admin without manage rights, `POST` bulk with `schema=0` + objects in a restricted schema → all rejected. Bulk into a `hardValidation=true` schema with an invalid object → it lands in `invalid`, not `saved`. Assert an audit row exists per created object. + +## BUG-OBJ-15 — Bulk save lets clients forge `@self.owner` / `@self.organisation` _(high, verified)_ +- **File:** `lib/Service/Object/SaveObjects.php` — `hydrateObjectMetadataFields()` `:1734-1752` only fills owner/organisation **when absent** (`:1737`, `:1746`), keeping client-supplied values. Contrast single-save `lib/Service/Object/SaveObject.php` `setSelfMetadata()` `:3633-3664`: owner is never accepted from the client; organisation only when `isAdmin || hasAccessToOrganisation()`. +- **Problem:** Through the bulk endpoint a user can plant objects owned by someone else and inside another tenant — the exact cross-tenant injection the single-save "wave-7/wave-11" fixes prevent. +- **Fix:** In `hydrateObjectMetadataFields`, mirror the single-save policy: always stamp `owner` to the session user (ignore client input); accept `organisation` only when `isAdmin` or `hasAccessToOrganisation($value)`, else fall back to `getOrganisationForNewEntity()`. Add a regression test pinning parity with single-save. +- **Verify:** As a non-admin in org A, bulk-save an object with `@self.owner="victim"` + `@self.organisation=""` → persisted owner is the caller, organisation is org A. + +## BUG-OBJ-8 — File-property update overwrites the object with raw partial input (data loss) _(high, verified)_ +- **File:** `lib/Service/Object/SaveObject.php` — `updateObject()` `:4887`; merged data built by `prepareObjectForUpdate()` `:4928` and persisted at `:4968`; then on the file-property path `:5030` does `$updatedEntity->setObject($data)` using the **raw incoming `$data`** (only `@self`/`id` removed at `:4912`) and re-persists at `:5050`. +- **Problem:** A PATCH-style update that includes any file property discards the merged/prepared body (computed fields, defaults, cascaded sub-objects, null-fills, **and previously-stored fields not present in the request**). Without a file property the merge is kept; with one it's lost — data loss that depends on payload shape. +- **Fix:** Build the post-file body from the merged data: start from `$preparedObject->getObject()` (or `$updatedEntity->getObject()`), overlay only the file-id replacements computed by the file handler, then `setObject(...)`. Never use raw `$data` as the full body. +- **Verify:** Create object `{a, b, fileX}`; PATCH with only `{fileX:newUpload}`; re-fetch and assert `a` and `b` survive. + +## BUG-OBJ-2 — Object delete never cleans up Nextcloud files/folders _(high, verified by reviewer; re-confirm before fix)_ +- **File:** `lib/Service/Object/DeleteObject.php` — `delete()` `:175-372`. Permanent delete (`:210-246`) calls only `deleteObjectEntity(hardDelete:true)`; soft delete (`:248-330`) sets the `deleted` metadata. No `FileService`/folder cleanup anywhere (the class header claims it does). +- **Problem:** Every object with an attached folder/file leaks its NC folder forever after permanent delete — storage bloat and **undestroyed file contents** despite the archival/`vernietigd` destruction workflow (a compliance issue). +- **Fix:** Inject `FileService`. In the permanent-delete branch, resolve `$objectEntity->getFolder()` and delete it via `FileService` (guard null/legacy). Decide with product whether soft delete should trash (recoverable) the folder too. +- **Verify:** Create an object with a file, permanently delete it, assert `$rootFolder->getById(...)` is empty. Add a regression test. + +## SEC-SVC-1 / SEC-SVC-2 — SSRF: server fetches user-supplied URLs with no IP/scheme allowlist _(high, verified)_ +- **Files:** + - `lib/Service/UploadService.php` — `getJSONfromURL()` `:247-285` (`$this->client->request('GET', $url)` at `:251`, no validation). Entry: `SchemasController::upload()` `:816-818` is `@NoAdminRequired`; for an existing schema only *manage* permission is required. + - `lib/Service/Object/SaveObject/FilePropertyHandler.php` — `fetchFileFromUrl()` `:916-938` (`file_get_contents` with `follow_location:true, max_redirects:5`, no IP check). Triggered whenever an object is written with a file-typed property whose value is an `http(s)://` URL. + - Contrast: `ConfigurationController::fetchConfigFromUrl()` `:1206-1242` **does** have an https-only + `gethostbyname` private-IP guard — but the two paths above don't use it, and there is **no shared helper**. +- **Problem:** Any authenticated user can make the server fetch `http://169.254.169.254/...` (cloud metadata), `http://localhost:*`, or RFC-1918 hosts and read the result back (full read SSRF). `follow_location:true` defeats naive allowlists via redirect. +- **Fix:** + 1. Create one shared `assertSafeFetchUrl(string $url): void` (put it in `lib/Service/SecurityService.php`) that: rejects non-`http(s)` schemes; resolves **all** A **and** AAAA records (`dns_get_record`/`gethostbynamel`) and rejects if **any** is loopback/RFC-1918/link-local/ULA/IPv6-loopback (`filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE) === false`); optionally enforces a config-gated allowlist `openregister/import_url_allowlist` (mirror opencatalogi's `local_federation_hosts` pattern). + 2. Call it at the top of `getJSONfromURL`, `fetchFileFromUrl`, **and** refactor `fetchConfigFromUrl` to use it (`SEC-SVC-6`). + 3. Disable redirects (`allow_redirects:false` / `max_redirects:0`); if redirects are required, re-validate each `Location`. Prefer the injected `IClientService` Guzzle client over raw `file_get_contents`. +- **Verify:** POST a schema/object with `url` = `http://127.0.0.1/`, `http://169.254.169.254/`, `http://10.0.0.1/`, and a public URL that 302-redirects to `10.0.0.1` → all rejected; a public allowlisted URL → 200. + +## SEC-SVC-6 — `fetchConfigFromUrl` SSRF guard is DNS-rebinding/IPv6-bypassable _(medium, verified guard exists)_ +- **File:** `lib/Controller/ConfigurationController.php:1206-1242`. +- **Problem:** TOCTOU — `gethostbyname` resolves once for validation, Guzzle re-resolves at fetch (DNS rebinding); `gethostbyname` returns only the first A record (no AAAA, no multi-record); redirects unconstrained. +- **Fix:** Replace with the shared `assertSafeFetchUrl()` from `SEC-SVC-1`; pass `allow_redirects:false`; to fully close rebinding, pin the connection to the validated IP (Guzzle `CURLOPT_RESOLVE` / `force_ip_resolve`). +- **Verify:** A DNS name alternating public↔`127.0.0.1` and an `[::1]` URL are both rejected. + +## SEC-SVC-3 / SEC-SVC-4 — SSTI: user-authored Twig templates compiled without a sandbox _(high/medium, verified)_ +- **Files:** + - `lib/Service/MappingService.php` `:110-111` (`new Environment(new ArrayLoader([]))`, no sandbox) + `:553` (`createTemplate($templateString)` on user mapping strings). + - `lib/Service/AuthenticationService.php` `:98` + `:337` (`createTemplate($configuration['payload'])->render($configuration)`, no sandbox, full config — incl. secrets — in context). + - **Reference implementation to copy:** `lib/Service/Object/SaveObject/ComputedFieldHandler.php:124-162` already builds a `Twig\Sandbox\SecurityPolicy` + `new SandboxExtension($policy, sandboxed: true)`. +- **Problem:** A malicious mapping/payload template can reach Twig internals (`_self`, attribute/method access) for info disclosure and, depending on exposed objects/callables, code execution. `AuthenticationService` additionally leaks adjacent secrets via the full `$configuration` context. +- **Fix:** + 1. In `MappingService`, build a `SecurityPolicy` allowlisting only needed tags (`if`, `for`, `set`, `apply`), the app's filters (`b64enc`/`b64dec`/`json_decode`/`zgw_*`), functions (`executeMapping`, `generateUuid`), no methods/properties; `addExtension(new SandboxExtension($policy, true))`. Copy the `ComputedFieldHandler` pattern verbatim and adjust the allowlist. + 2. In `AuthenticationService`, add the same sandbox **and** pass a filtered context (only the intended claim fields, not the whole `$configuration` with secrets). +- **Verify:** Templates with `{{ _self }}` / `{{ object.getClass() }}` / a non-allowlisted filter throw `SecurityError`; legitimate `{{ value|b64enc }}` still renders. + +## BUG-DB-2 — SQL injection in the `@self` terms-facet UNION path _(high, verified)_ +- **File:** `lib/Db/MagicMapper/MagicFacetHandler.php` — `getTermsFacetUnion()` `:568-621` concatenates `$field` raw into `CAST({$field} AS CHAR)`/`{$field}::text` (`:589-591`), `WHERE {$field} IS NOT NULL` (`:600`), `GROUP BY {$castField}` (`:619`). The `@self` caller (`:515` `$columnName = self::METADATA_PREFIX.$field`, `:518-524`) builds the column straight from request facet keys with **no** `sanitizeColumnName`/`columnExists` — unlike the object-field branch (`:454`/`:460`) and `getDateHistogramFacetUnion` (`:828`). +- **Problem:** A multi-table facet request with a crafted `_facets[@self][...]` key injects arbitrary SQL. +- **Fix:** In `getTermsFacetUnion`, before building SQL, run `$field` through `sanitizeColumnName` and validate against the metadata-column allowlist (`array_keys($this->getMetadataColumns())`); skip the facet if it fails. Mirror the `columnExists` guard the other two paths already use. +- **Verify:** Issue a multi-register search with `_facets[@self][_created) UNION SELECT ...]`; confirm the facet is skipped (empty buckets), not executed. + +## BUG-DB-1 — Schema default values interpolated into DDL without escaping _(high, verified pattern)_ +- **File:** `lib/Db/MagicMapper.php` — `formatDefaultValueForSQL()` `:4346-4348` (`return "'".$default."'";`) and the same pattern in `createTable` `:2654-2655`. +- **Problem:** An admin-authored property `default` containing a single quote (`O'Brien`) breaks the `ALTER TABLE ... DEFAULT '...'` DDL and aborts table sync; a crafted default is stored-DDL injection. Value writes use `connection->quote()`; the DDL path does not. +- **Fix:** Escape via the platform: `$this->db->getDatabasePlatform()->quoteStringLiteral($default)` (or `$this->db->quote($default)`); apply in both spots. Never hand-build `"'".$x."'"`. +- **Verify:** Create a property with default `a'b`, enable magic mapping, confirm the table syncs and the column default is literally `a'b`. + +## SEC-CTRL-5 — File read path re-owns files instead of denying _(medium, single-agent; re-verify)_ +- **File:** `lib/Controller/FilesController.php` — `downloadById()` `:1038`, `show()` `:268`; `lib/Service/File/FileValidationHandler.php` — `checkOwnership()` `:278-312` calls `ownFile()` (reassigns ownership) on owner mismatch instead of denying. +- **Problem:** Authenticated file access has no object-level RBAC (only NC mount visibility), and a GET **mutates file ownership** — a state-changing side effect on a read path; any mount-visibility drift becomes a cross-user read. +- **Fix:** In `checkOwnership()`, throw `NotPermittedException` on owner mismatch when the file isn't shared/published to the caller — never call `ownFile()` on a read. In `downloadById()`/`show()`, resolve the parent object and run a `PermissionHandler` read check for authenticated callers too. Move any legitimate ownership repair to an explicit admin job. +- **Verify:** As user B, request a file in user A's object that B can't read → 403/404, and the file's owner is unchanged afterward. + +--- + +# P1 — Correctness bugs + +## Object pipeline + +- **BUG-OBJ-3 — Magic-mapped objects can never set `@self.published`** _(RESOLVED — WON'T-FIX, retired 2026-06-15. **DO NOT IMPLEMENT.**)_ + - **Re-classification:** This is **not** a bug to fix — it is **intended behaviour**. Object-level publish metadata (`@self.published` / `published` / `depublished`, `setPublished()` / `setDepublished()`, the `autoPublish` schema key and `ImportService::addPublishedDateToObjects()`) was **deliberately removed** as a **breaking change** by the `deprecate-published-metadata` OpenSpec change. The columns were dropped by migration `lib/Migration/Version1Date20260313130000.php` and the removal is locked by the regression test `tests/Unit/Service/ImportServicePublishDeprecationTest.php`. The original finding is preserved below for history but must **not** be acted on. + - **Why the proposed fix is wrong:** Re-adding `setPublished()` / `setDepublished()` and a `publish` permission to `setSelfMetadata()` / the bulk path would **revert a shipped breaking change** — re-introducing columns the migration dropped and breaking the regression test. The "federation gap" framing is stale: anonymous publication visibility is **not** an object-metadata concern. + - **The actual model (live):** Anonymous / public visibility is governed by **RBAC**, not object metadata — a public-group read scope with a date-window match, e.g. `{group: public, match: {publicatiedatum: {$lte: $now}}}`. This is owned by the **downstream consuming apps** (OpenCatalogi, Softwarecatalogus, decidesk), not by OpenRegister core, and is already the live publication model. OR core's job is only to evaluate the RBAC match; the publication-date semantics belong to the leaf apps. + - **Original finding (for history, superseded):** `lib/Service/Object/SaveObject.php` `setSelfMetadata()` and `SaveObjects.php` never read/set `published`/`depublished` (grep confirms no `setPublished`). _The proposed fix below has been rejected; do not implement._ ~~In `setSelfMetadata`, after the organisation block, accept `published`/`depublished` from `$selfData` (with a `publish` permission check) and call `setPublished()`/`setDepublished()`; mirror in the bulk path's metadata hydration.~~ +- **BUG-OBJ-4 — Null-schema save throws raw `TypeError` 500** _(medium)._ `lib/Service/ObjectService.php:1215` calls `applyAlwaysDefaults(schema: $this->currentSchema, ...)` (non-nullable param) and `:1443` `$this->currentSchema->getHardValidation()` with no null guard, while `checkSavePermissions` returns early on null schema (`:1352`). **Fix:** throw a structured 400 "Schema could not be resolved" when `currentSchema === null` early in the save flow; add a null guard to `validateObjectIfRequired`. **Verify:** `POST /api/objects` with no resolvable schema → 400, not 500. +- **BUG-OBJ-5 — Bulk-delete cache invalidation passes null register/schema** _(medium)._ `lib/Service/ObjectService.php:3185-3201` calls `invalidateForObjectChange(... registerId:null, schemaId:null)`; `CacheHandler::clearSchemaRelatedCaches` only clears the distributed query cache when `schemaId !== null` (`CacheHandler.php:767`). **Fix:** collect distinct `(register, schema)` pairs from deleted objects in the loop, invalidate per pair. **Verify:** cache a collection query, bulk-delete one object, re-run query → deleted object gone immediately. +- **BUG-OBJ-6 — `CacheHandler::getObject()` caches across tenants with a bare-id key** _(medium)._ `CacheHandler.php:289-312` — `$key=(string)$identifier`, `find()` with no `_rbac`/`_multitenancy`/tenant discriminator. **Fix:** route reads through `ObjectService::find` (which post-fetch `checkPermission`s), or include the active organisation in the key and re-apply tenant filtering; audit all callers. **Verify:** tenant B can't retrieve tenant A's object via any path touching `getObject()`. +- **BUG-OBJ-7 — Object-name cache keyed by UUID only, never invalidated on rename** _(low)._ `CacheHandler.php:1071-1290`. **Fix:** add name-cache invalidation to `invalidateForObjectChange`; include organisation in the key if names are tenant-sensitive. +- **BUG-OBJ-9 — Contact-match cache invalidated with pre-save input array** _(low)._ `ObjectService.php:1251-1264` passes `$object` (no final UUID/defaults) not `$savedObject`; whole block swallows `\Throwable`. **Fix:** pass `$savedObject`; narrow the catch + log. +- **BUG-OBJ-10 — Bulk cascade count uses shared mutable `getLastCascadeCount()`** _(low)._ `ObjectService.php:3146-3158`. **Fix:** return the cascade count from `deleteObject` instead of via instance state, or reset it at the start of every delete. +- **BUG-OBJ-11 — `DeleteObject::delete()` reads `$object['id']` without existence check** _(low)._ `DeleteObject.php:190-192`. **Fix:** `$identifier = $object['id'] ?? $object['@self']['id'] ?? $object['uuid'] ?? null;` and throw if still null. +- **BUG-OBJ-13 — `find()` mutates shared `currentRegister`/`currentSchema` as a read side-effect** _(low, but caused openregister#1520-class bugs)._ `ObjectService.php:604-699` (esp. `:656-665`). **Fix:** resolve the render context into locals and pass explicitly; or snapshot-and-restore in a `finally`. +- **BUG-OBJ-14 — Empty/silent `catch` blocks hide cache-invalidation failures** _(low)._ `ObjectService.php:1260-1264, 3030-3038, 3193-3201`; `DeleteObject.php:319-329` (empty body). **Fix:** log a warning with object/register/schema context in each. + +## Db layer + +- **BUG-DB-3 — Hardcoded `oc_` prefix breaks every raw-SQL path on a custom `dbtableprefix`** _(high, verified)._ `lib/Db/MagicMapper.php` (~21 sites incl. `:1324, :1715, :2820, :4388, :4669`), `MagicBulkHandler.php:491`, `MagicFacetHandler.php:583,821` — all `'oc_'.$tableName`, while `createTable` `:2621` correctly uses `config->getSystemValue('dbtableprefix','oc_')`. **Fix:** add one helper `getFullTableName(string $tableName): string` reading `dbtableprefix` once; replace every `'oc_'.$tableName` and `$prefix='oc_'`. Also fix the two `str_replace('oc_', '', ...)` calls (`:5075, :5139`) → `substr($fullTableName, strlen($prefix))`. **Verify:** install with `dbtableprefix=nc_`, confirm cross-register search/facets/bulk import work. +- **BUG-DB-4 — Non-deterministic pagination: LIMIT/OFFSET without a default ORDER BY** _(medium-high)._ `MagicSearchHandler.php:248-253`. **Fix:** when `$order` is empty, add a stable default (`addOrderBy('t._id','ASC')` or `t._created, t._id`) before `setMaxResults`; add a tiebreaker to the UNION path too. **Verify:** page through 50 rows at `_limit=10` with no `_order`; union of pages = full set, no dupes across runs. +- **BUG-DB-5 — Bulk UPSERT has no transaction → partial writes on failure** _(medium-high)._ `MagicBulkHandler.php:374-383`. **Fix:** wrap the chunk loop in `beginTransaction()/commit()/rollBack()`. **Verify:** import a batch where one row violates a constraint mid-stream → no rows persist. +- **BUG-DB-6 — Bulk UPSERT silently drops objects lacking `_uuid`** _(medium)._ `MagicBulkHandler.php:451-466`. **Fix:** generate a UUID for uuid-less rows, or collect them into an error bucket; don't drop silently. +- **BUG-DB-9 — Inverse-relation lookup uses invalid MySQL `CAST(table AS CHAR)`, failure swallowed** _(medium)._ `MagicMapper.php:6588-6624` — always throws on MariaDB/MySQL, caught at *debug* and returns `[]`, so inverse relations silently resolve to nothing. **Fix:** replace with a valid per-column `CONCAT_WS(...) LIKE ?` / `JSON_SEARCH`; raise the catch to warning. **Verify:** on MariaDB, A references B by UUID, inverse lookup for B returns A. +- **BUG-DB-7 — Identifier quoting doesn't escape embedded quote chars** _(medium, defense-in-depth)._ `MagicMapper.php:3956-3963` + raw backtick concat at `:2630-2690`. **Fix:** double the quote char (`str_replace('"','""',$name)` / `` str_replace('`','``',$name) ``); route `createTable`'s raw `$column['name']` through `quoteIdentifier`. +- **BUG-DB-8 — `sanitizeColumnName` is non-injective → property collisions / lossy round-trip** _(medium)._ `MagicMapper.php:3388-3410`. **Fix:** detect collisions when building the column map and append a deterministic disambiguator (or reject the schema); store the original property name in a sidecar map rather than relying on a lossy inverse. +- **BUG-DB-10 — Schema `find()` cache key omits `$published`; numeric-slug vs id ambiguity** _(medium)._ `SchemaMapper.php:249, 269-281, 331`. **Fix:** include `published` in the read+write cache keys (`:249`/`:328`); prefer exact-id match when `is_numeric($id)`; guard `getUuid() !== null` before `strtolower`. (Same UUID-null pattern in `RegisterMapper`.) +- **BUG-DB-12 — Naive semver bump drops pre-release suffix** _(low)._ `RegisterMapper.php:691-694` — `1.0.0-beta` → `1.0.1`. **Fix:** parse the numeric patch with a regex preserving any `-suffix`; pad missing segments to three. +- **BUG-DB-11 — Leftover `logger->error` debug block in the hot write path** _(low)._ `MagicMapper.php:3222-3235` (hardcoded `element`/`gemmaType`). **Fix:** delete (or gate behind a debug flag at `debug` level). +- **BUG-DB-13 — Request-scoped schema-version cache can mask intra-request schema edits** _(low)._ `MagicMapper.php:3512-3534`. **Fix:** include the schema `version`/`updated` in the cache key, or invalidate on schema mutation. + +## Other services + +- **BUG-SVC-1 — Webhooks record HTTP 4xx/5xx as successful deliveries** _(high, verified)._ `lib/Service/WebhookService.php:189` (`http_errors:false`) + `deliverWebhook()` `:689-713` sets `setSuccess(true)` + `updateStatistics(success:true)` unconditionally; only network errors hit the catch. **Fix:** after `sendRequest`, branch on `$response['status_code']` 2xx; on non-2xx take the failure path (`setSuccess(false)`, log, `updateStatistics(false)`, schedule retry, return false). **Verify:** point a webhook at a 500 endpoint → log row `success=false` + retry queued. +- **BUG-SVC-2 — DSAR right-to-erasure reports success while skipping failed objects** _(high, GDPR)._ `lib/Service/DsarService.php:294-339` — no `failed` bucket; a failed soft-delete is logged and dropped. **Fix:** add `'failed' => []`, append `{object, error}` in the catch, have the caller treat non-empty `failed` as partial failure (207/error). **Verify:** force one update to throw → summary's `failed` populated, not reported complete. +- **BUG-SVC-3 — Solr search applies neither RBAC nor multitenancy** _(high, verified)._ `lib/Service/Index/Backends/Solr/SolrQueryExecutor.php:161-227` accepts `$_rbac`/`$_multitenancy` but only ever adds `-deleted:true`; no org/owner `fq`. Contrast `AggregationRunner.php:1169-1171` (`_organisation = ?`, fail-closed). **Fix:** when `$_multitenancy`, add `_organisation:` (fail closed when no active org); when `$_rbac`, add the owner/group predicate mirroring `MagicRbacHandler`; ensure the indexer writes `_organisation`/ACL fields. **Verify:** index docs for two orgs, search as org-A user, org-B docs absent. +- **BUG-SVC-4 — Postgres date-bucket labels shift by the server UTC offset** _(medium)._ `AggregationRunner.php:1385-1404` (`coerceBucketKey`) — `strtotime` parses offset-less Postgres text in the server TZ, then `gmdate` re-expresses as UTC. **Fix:** when `$raw` has no TZ designator, parse as UTC explicitly (`DateTime::createFromFormat('Y-m-d H:i:s', $raw, new DateTimeZone('UTC'))`); only `strtotime` for offset-bearing shapes. **Verify:** on a CET server, daily aggregation buckets = local date as UTC midnight, not shifted. +- **BUG-SVC-5 — Import drops timezone offset instead of normalizing** _(medium)._ `ImportService.php:1193-1229` — `format('Y-m-d H:i:s')` discards the offset. **Fix:** `setTimezone(new DateTimeZone('UTC'))` before formatting; document the assumed zone for offset-less inputs. **Verify:** `...T00:00:00+05:00` and the equivalent `Z` instant persist to the same value. +- **BUG-SVC-6 — `getSearchBackendConfig` fatals on corrupt JSON (`TypeError` not caught)** _(medium)._ `SettingsService.php:417-442` — `json_decode` can return null into an `: array` return; `catch (\Exception)` misses `TypeError`. **Fix:** capture the decode, `if (is_array($decoded) === false) return ;`. **Verify:** set `search_backend` to `"not-json"` → getter returns default. +- **BUG-SVC-7 — Native aggregation builds malformed SQL for value metrics with null field** _(low)._ `AggregationRunner.php:1121-1123, 1228-1249`. **Fix:** short-circuit to fallback when `metric ∈ {sum,avg,min,max}` and `field` is null/empty. +- **BUG-SVC-8 — Notification numeric `eq`/`ne` compares as strings (`1.0 != 1`)** _(low)._ `AnnotationNotificationDispatcher.php:1180-1200`. **Fix:** when both sides numeric, compare as floats; keep string fallback for non-numeric. +- **BUG-SVC-9 — PHP-fallback aggregation equality uses strict `!==`, dropping rows on type mismatch** _(low)._ `AggregationRunner.php:980-1010` — magic-table values are strings, criteria may be int/bool → Postgres path and fallback diverge. **Fix:** compare with `(string)` cast (guarding non-scalars). +- **BUG-SVC-10 — Solr `q` not Lucene-escaped** _(low)._ `SolrQueryExecutor.php:108, 205-211`. **Fix:** add `escapeSolrQuery()` backslash-escaping the Lucene special set, apply to the user term. +- **BUG-SVC-11 — External-aggregation backend failures swallowed with no log** _(low)._ `AggregationRunner.php:321-324`. **Fix:** `logger->warning(...)` in the catch before falling through. + +## Security (lower-severity hardening) + +- **SEC-SVC-5 — GitHub token prefix + length logged** _(medium)._ `Configuration/GitHubHandler.php:148-156` logs `token_prefix`/`token_length`. **Fix:** log only `has_token => true`; sweep `GitHubHandler`/`GitLabHandler` for any `substr($token...)`/`strlen($token)` in log context. +- **SEC-SVC-7 — Path traversal in `importFromFilePath` (no `..` rejection)** _(medium)._ `Configuration/ImportHandler.php:2865-2890` — `'/var/www/html/'.$filePath` fallback allows `../../etc/passwd`. **Fix:** after `realpath`, assert containment under an allowed base (`str_starts_with($fullPath, $base.'/')`); reject `..`/leading `/`; remove or contain the unbounded fallback. +- **SEC-SVC-9 — `unserialize()` on stored vector blob** _(low, object-injection)._ `Vectorization/Handlers/VectorSearchHandler.php:134`. **Fix:** store/read embeddings as JSON, or `unserialize($x, ['allowed_classes' => false])`. +- **SEC-SVC-10 — `unserialize()` of session-cached config without `allowed_classes`** _(low)._ `Configuration/CacheHandler.php:133`. **Fix:** `unserialize($x, ['allowed_classes' => [\OCA\OpenRegister\Db\Configuration::class]])` or switch to JSON. +- **SEC-SVC-11 — Forwarded-IP headers trusted for client-IP attribution** _(low)._ `SecurityService.php:687-716` parses `X-Forwarded-For`/`X-Real-IP`/`CF-Connecting-IP` unconditionally → lockout bypass. **Fix:** prefer `IRequest::getRemoteAddress()` (honors NC `trusted_proxies`) or gate the header loop behind a trusted-proxy check. +- **SEC-SVC-8 — Path-injection into raw.githubusercontent.com URL** _(low)._ `Configuration/GitHubHandler.php:528-547`. **Fix:** `rawurlencode()` each path segment; reject `/`/`..`/control chars; `allow_redirects:max=0`. +- **SEC-SVC-12 — Downloaded URL file stored without content-type allowlist** _(low, with SEC-SVC-2)._ `FilePropertyHandler.php:759-800`. **Fix:** validate resolved MIME/extension against the property's accepted types before `addFile`; serve with `Content-Disposition: attachment` + restrictive CSP. +- **SEC-CTRL-7 — Pervasive exception-message disclosure on 500s** _(medium)._ ~160 sites return `$e->getMessage()` (e.g. `ObjectsController` ~45, `SchemasController` ~55, `ConfigurationController` ~34, `FilesController` ~29). **Fix:** add a helper that logs the exception and returns a generic `['error' => 'Internal server error']` for 500s; keep specific messages only for 4xx validation. (Pairs well with `REF-5`.) +- **SEC-CTRL-8 — `@NoCSRFRequired` on authenticated state-changing endpoints** _(low)._ e.g. `LinkedEntityController.php:81-191`, many `ConfigurationController`/`ObjectsController` writes. **Fix:** remove `NoCSRFRequired` from SPA-called writes (axios sends the token); keep it only on truly public/bearer routes. Audit each one. +- **SEC-CTRL-9 — Unsanitized filename in `Content-Disposition`** _(low)._ `FilesController.php:324`, `FileService.php:1648`. **Fix:** RFC 6266 encode (`filename*=UTF-8''` + ASCII fallback), strip control chars/quotes. +- **SEC-CTRL-4 — Linked-entity write only checks read RBAC** _(medium)._ `LinkedEntityController.php:82-192` + `LinkedEntityService.php:102-118` — `MagicMapper::find()` (read check) then `update()` (no write check), so read-only users can mutate link columns. **Fix:** add a write-permission `PermissionHandler` check before `update()` in each `LinkedEntityService` write method; return 403 in the controller. +- **SEC-CTRL-10 — Dead `ObjectsController::import()` retains the param-RBAC bypass** _(low)._ `:3150-3173` (route disabled at `routes.php:303`). **Fix:** delete the dead method (and the retired `clearBlob()` no-op at `:3834`). + +--- + +# P2 — Performance + +> Top 3 wins: **PERF-1** (N+1 file/tag loading in list render), **PERF-4** (per-row regex map rebuild), **PERF-2** (information_schema + all-tables UNION on every extend preload). + +- **PERF-1 — File-property hydration is N+1 (2+ queries per file per row)** _(critical)._ `lib/Service/Object/RenderObject.php:1262` (`renderFileProperties` per entity) → `getFileObject` `:966/982` → `fileMapper->getFile` `:1143` (1 query) + `getFileTags` `:756-773` (2 queries). A 20-row page × 3 files ≈ 120 queries. **Fix:** in `renderEntities` (before the per-entity loop ~`:3234`) collect all file IDs across the page, batch-load via a new `FileMapper::getFilesByIds(array)` (single `WHERE fileid IN (...)`) + a batch `getTagIdsForObjects($allFileIds, 'files')`; cache in request-scoped `$this->fileObjectCache`/`$this->fileTagsCache`; have `getFileObject`/`getFileTags` read from it. **Verify:** query log on a 20-row 3-file page: ~120 → ~3. +- **PERF-4 — `convertRowToObjectEntity` rebuilds the column map + runs `sanitizeColumnName` per row** _(high)._ `MagicSearchHandler.php:1581-1654` (`:1628-1632`, `:1898`). 100 rows × 50 props ≈ 25k regex ops per page. **Fix:** compute `$propertyTypes`/`$columnToPropertyMap` once in `executeSearchQuery` and pass into `convertRowToObjectEntity`, or memoize per `$schema->getId()`. **Verify:** `sanitizeColumnName` call count drops from 5000 → 50 for a 100×50 page. +- **PERF-2 — `findMultipleAcrossAllMagicTables` scans `information_schema` + UNION-queries every magic table on every extend preload** _(high)._ `MagicMapper.php:4828-4915`, via `CacheHandler::preloadObjects` `:577`. **Fix:** cache the magic-table list in a static keyed by DB name (invalidate on table create), eliminating the `information_schema` round-trip; where the caller knows target schema(s), narrow the UNION to those tables. **Verify:** branch count N → distinct target schemas; `information_schema` queries 1+ → 0 after warm cache. +- **PERF-3 — `ScheduledNotificationJob` loads whole schema tables into PHP and filters in-memory** _(high)._ `BackgroundJob/ScheduledNotificationJob.php:253, 267-274` → `MagicMapper::findBySchema` (no limit). **Fix:** push the trigger `filter` into SQL (`searchObjectsInRegisterSchemaTable` with `_filter`), add `_limit`/cursor paging, persist a per-schema watermark for delta scans. **Verify:** 50k-object schema with 10 matches → SQL returns ~10 rows; measure peak memory. +- **PERF-13 — `findBySchema` is an unbounded full-table read by design** _(medium)._ `MagicMapper.php:7094-7129`. **Fix:** add optional `$limit`/`$offset`/`$filter` pushed to SQL; audit callers (`ScheduledNotificationJob`, reports) to pass bounds. +- **PERF-5 — Magic tables have no `_deleted` index though every query filters `_deleted IS NULL`** _(medium)._ `MagicMapper.php:2822-2858` (index list) vs filter at `MagicSearchHandler.php:498, 909`. **Fix:** add a partial index in `createTableForRegisterSchema` (Postgres `CREATE INDEX ... ON {table}(_uuid) WHERE _deleted IS NULL`) + a one-shot migration for existing tables; consider partial composites on hot filter columns. **Verify:** `EXPLAIN ANALYZE` on `WHERE _owner=? AND _deleted IS NULL` switches to the partial index. +- **PERF-6 — Cross-table search fetches all rows, counts in PHP, no global limit/offset** _(medium)._ `ObjectsController.php:821-845` + `MagicMapper.php:1604-1627` — returns up to `limit × tableCount` rows; `total = count($fetched)`. **Fix:** `array_slice` to the real page window after merge/sort; compute `total` via per-table `COUNT(*)` sum. **Verify:** `schemas=1,2,3&_limit=20` against 1000-row tables → 20 rows, `total=3000`. +- **PERF-11 — Base64 file embedding reads whole file into memory per file per row** _(medium)._ `RenderObject.php:998-1043` (`getFileAsBase64`, `$file->getContent()` `:1014`) inside `renderFileProperties`. **Fix:** refuse `format: base64` on list/collection renders (return the file reference); on single-object GET, cap by size and stream/encode lazily. **Verify:** 10-row page × 5 MB base64 prop → peak memory bounded after the list-path guard. +- **PERF-10 — Separate `COUNT(*)` on every list request even when total isn't needed** _(low)._ `ObjectsController.php:1115-1124`. **Fix:** support `_count=false`/`_noTotal` to skip the count (return `total:null`); or fetch `limit+1` rows to infer `hasMore`. +- **PERF-7 — `INFO`-level logging of full extend params on every list render** _(low)._ `RenderObject.php:3162-3202`; also `MagicMapper.php:1610, 1644, 1049`. **Fix:** downgrade the `[BATCH_PRELOAD]` traces to `logger->debug`. +- **PERF-8 — Per-entity re-preload duplicates page-level batch preload (CPU)** _(low)._ `RenderObject.php:2164-2182` vs `:3204-3211`. **Fix:** set a "rendering a page" flag so `extendObject` skips the per-entity `preloadObjects` and relies on the warm cache. +- **PERF-9 — `cacheObject` eviction does O(n) `array_slice` of the whole cache per insert** _(low)._ `CacheHandler.php:613-629`. **Fix:** `unset()` the oldest keys in a loop (or use a ring buffer) instead of reallocating via `array_slice`. +- **PERF-12 — `rowToObjectEntity` resolves+decodes schema per row in fallback hydration** _(low)._ `MagicMapper.php:6717-6764`. **Fix:** memoize `_schema → columnToPropertyMap` per schema id (same fix family as PERF-4). +- **PERF-14 — Per-pair `find()` re-resolution in `crossTableSearch` build loop** _(low)._ `ObjectsController.php:756-792`. **Fix:** resolve each register once (outer loop) + pre-resolve schemas into a map. + +--- + +# P3 — Refactor / architecture + +> **Sequencing: do `REF-15` (characterization tests) before touching the giants.** Then Phase-1 mechanical wins, then the giants. Run `composer check:strict` + `/hydra-gates` between phases. + +### Phase 0 — safety net +- **REF-15 — Characterization tests for the riskiest SQL (do FIRST)** _(high, prerequisite)._ The giants have test files, but the MagicMapper **DDL/column-mapping** methods (`REF-1` targets) and the **union-search** cluster (`REF-2`) are pinned only by the integration test. **Action:** add `tests/Unit/Db/MagicMapper/SchemaColumnMappingTest.php` (pin `mapSchemaPropertyToColumn`/`map*Property`), `TableEvolutionTest.php` (pin `updateTableStructure`/`addMissingColumns`/`findJsonbColumnsNeedingRetype`), `UnionSearchTest.php` (pin `buildUnionSelectPart`/`collectAllPropertyColumns`). They must pass against current code first. + +### Phase 1 — high-value, low-risk, independent +- **REF-4 — Extract `AbstractLinkController` for 17 near-identical link controllers** _(high)._ `lib/Controller/*LinksController.php` (17 files, ~5895 lines) share byte-identical `validateObject()` + `mapException()`. **Action:** create `lib/Controller/AbstractLinkController.php extends Controller` with the shared helpers + protected `$objectService`; convert one controller, delete its copies, run its test, roll across the other 16 one at a time. +- **REF-9 — Converge controllers on attribute-based auth** _(medium)._ 88 files use docblock `@NoAdminRequired`, 15 use `#[NoAdminRequired]` (no file mixes both). **Action:** mechanical per-method swap to `#[...]` attributes in batches of ~10. **Preserve posture exactly** — controllers without `@NoAdminRequired` are admin-only by NC default; don't add/remove gates. Run `route-auth`/`semantic-auth` gates after each batch. + +### Phase 2 — the giants (gated by Phase 0) +- **REF-1 — Finish MagicMapper decomposition: extract the DDL/schema-evolution engine** _(high)._ `lib/Db/MagicMapper.php` (8662 lines). ~20 DDL/column-map methods span `:1784-4435` and are still inline despite 7 existing handlers. **Action:** (1) `MagicSchemaColumnMapper.php` ← the pure mapping functions (`mapSchemaPropertyToColumn`, `map*Property`, `mapColumnTypeToSQL`, `getMetadataColumns`, `buildTableColumnsFromSchema`, `sanitizeColumnName`, `quoteIdentifier`, `formatDefaultValueForSQL`); (2) `MagicTableDdlHandler.php` ← connection-bound DDL (`createTable`, `*Indexes`, `updateTableStructure`, `addMissingColumns`, `deRequire/reRequireColumns`, `dropDuplicateCamelCaseColumns`, `makeObsoleteColumnsNullable`, `findJsonbColumnsNeedingRetype`, `migrateJsonbToJson*`); (3) move the create/update/sync orchestration. Add delegating wrappers; run unit + integration tests after each step. +- **REF-2 — Extract the MagicMapper cross-table UNION-search cluster** _(high)._ `MagicMapper.php:1047-1696` (~650 lines) → move into `MagicSearchHandler` (keep a public delegator). Also move `searchObjectsPaginatedMultiSchema` `:8271`, `getGlobalSearchResult` `:8466`, `searchObjectsGloballyBySearch` `:8544`. +- **REF-3 — Finish SaveObject decomposition** _(high)._ `lib/Service/Object/SaveObject.php` (5277 lines). **Action:** `DefaultValueHandler.php` ← defaults/slug (`:1220-1608`); `ReferenceValidationHandler.php` ← reference + circular-ref (`:3831-4597`, move the existing dedicated tests to target it); consolidate the 4 cascade methods (`:1655-2212`) into the existing `RelationCascadeHandler`. +- **REF-7 — Split ImportHandler** _(medium)._ `lib/Service/Configuration/ImportHandler.php` (3958 lines, 11 setter-injected collaborators). **Action:** `SeedDataImporter.php` ← `importSeedData`/`processRelatedItems`/`ensureDependenciesForSeedData`/`handleNextcloudAppDependencies`; `WorkflowDeploymentImporter.php` ← `processWorkflowDeployment`/`processWorkflowHookWiring`; ImportHandler keeps config-definition import. + +### Phase 3 — facade / boot cleanup +- **REF-8 — Move Application.php registration groups into registrar classes** _(medium)._ `lib/AppInfo/Application.php` (2228 lines). **Action:** `Registration/EventListenerRegistrar.php` ← the 76 `registerEventListener` calls (`:1715-1869`); `Registration/IntegrationProviderRegistrar.php` ← `registerBuiltinIntegrationProviders`/`bootBuiltinIntegrationProviders`; move MCP discovery (`collectPerAppMcpProviders` etc. `:1909-2060`) into `McpDiscoveryService`. **Note:** do this _with_ the OPS registration fixes below (you'll be editing the same block). +- **REF-6 — Extract ObjectsController request-parsing / file-upload concerns** _(medium)._ `lib/Controller/ObjectsController.php` (3952 lines). **Action:** `Support/ObjectRequestParser.php`, `Support/MultipartFileExtractor.php`, `Support/GeoQueryParser.php`; delete the duplicated `isUuid`/`collectUuids*` (see REF-10). +- **REF-10 — De-duplicate UUID/name-collection helpers** _(medium)._ `ObjectsController.php:3729-3834` vs `ObjectService.php:2656-2844`. **Action:** create `Service/Object/UuidCollector.php` from the safer depth-guarded version; replace both call-sites. +- **REF-5 — Controller exception→JSONResponse middleware + identifier trait** _(medium)._ ~1801 `new JSONResponse`, 47 hand-rolled 500s, 41 copies of UUID detection. **Action:** `Middleware/ControllerExceptionMiddleware.php` (`afterException` → canonical `['error'=>...]` envelope, mapped per domain exception), register in Application.php; `Controller/Trait/ResolvesIdentifiersTrait.php` for the one canonical `isUuid()`. Migrate controllers to *throw* incrementally. (Closes `SEC-CTRL-7` at the same time.) + +### Phase 4 — consolidation +- **REF-13 — Consolidate SaveObject ↔ SaveObjects shared logic** _(low, after REF-3)._ Point `SaveObjects/BulkValidationHandler` at the same `DefaultValueHandler`/`ReferenceValidationHandler`. (Also the structural fix that makes `BUG-OBJ-1`/`BUG-OBJ-15` parity natural.) +- **REF-14 — Split ObjectService search/facet surface + collapse `find`/`findSilent`** _(low)._ `lib/Service/ObjectService.php` (3658 lines). Extract `ObjectSearchService.php`; make `find`/`findSilent` wrap a private `doFind(..., bool $throw)`. + +### Phase 5 — layering hygiene (opportunistic, audit-gated) +- **REF-11 — Controllers reaching into mappers instead of ObjectService** _(low)._ 40 controllers inject `*Mapper` directly. **Do NOT blind-sweep:** definition-CRUD controllers (Registers/Schemas/Scopes managing their own entity) are legitimate; only object-data controllers (Deleted/Bulk/EntityRelations) should route through `ObjectService` for RBAC. Classify first, convert the misclassified, run each test. +- **REF-12 — HTTP import in the Db trait** _(low)._ `lib/Db/MultiTenancyTrait.php:35` imports `Http\JSONResponse`. **Fix:** remove the import / move any response-building out to a controller; trait should throw a domain exception. + +--- + +# P4 — Frontend & ops hygiene + +## Frontend (`src/`) — state is uniformly Pinia (no legacy Vuex) +- **FE-1 — Stored XSS in ViewObject editability-warning toast** _(critical)._ `src/modals/object/ViewObject.vue:2311` assigns `notification.innerHTML` with an interpolated user object value (`:2122-2131`). **Fix:** replace the hand-rolled toast with `import { showWarning } from '@nextcloud/dialogs'` → `showWarning(warning)` (text-escapes); delete `showWarningNotification` (`:2290-2328`, this is also `FE-10`). **Verify:** immutable property set to `` → no alert, literal text shown. +- **FE-2 — XSS in chat via unsanitized `marked.parse` → `v-html`** _(critical)._ `src/views/chat/ChatIndex.vue:81, 876`. `marked` v16 has no built-in sanitizer; `dompurify>=3.4` is already a dependency. **Fix:** `import DOMPurify from 'dompurify'` → `return DOMPurify.sanitize(marked.parse(content || ''))`. **Verify:** chat message with `` / `[x](javascript:...)` doesn't execute. +- **FE-3 — Unvalidated `href` bound to imported configuration URLs** _(high)._ `src/components/cards/ConfigurationCard.vue:149` (org url), `:138` (repo url). **Fix:** computed `safeOrgUrl` returning the URL only if `/^https?:\/\//i.test(url)` else null; bind `:href` + `v-if`. **Verify:** imported config with `url:"javascript:alert(1)"` → link inert. +- **FE-4 — Solr setup spinner/interval runs forever on failure** _(high)._ `src/views/settings/sections/SolrConfiguration.vue:1600-1609` — no try/finally around `setupSolr()`. **Fix:** wrap in `try/catch(showError)/finally(stopGameLoading)`. +- **FE-5 — Solr loading `setInterval` leaks on unmount** _(medium)._ same file `:1626`, no `beforeDestroy`. **Fix:** add `beforeDestroy() { this.stopGameLoading() }`. +- **FE-14 — N8n editor `window.open` of unvalidated configured URL** _(low)._ `src/views/settings/sections/N8nConfiguration.vue:587-590`. **Fix:** `if (/^https?:\/\//i.test(this.n8nUrl)) window.open(this.n8nUrl,'_blank','noopener')`. +- **FE-16 — `SettingsSection.sanitizeHtml` strips all formatting (silent UX bug)** _(low)._ `src/components/shared/SettingsSection.vue:214-218` uses `textContent`. **Fix:** `DOMPurify.sanitize(html, { ALLOWED_TAGS:['a','strong','em','code','br','p'], ALLOWED_ATTR:['href','target','rel'] })`. +- **FE-6 — Dutch strings used as i18n keys** _(medium)._ `src/dialogs/avg/EditActivityDialog.vue:40,50,55,60,65,143,144`. **Fix:** English keys (`'Technical measures'`, `'Edit processing activity'`, …) + Dutch values in `l10n/nl.*`. +- **FE-7 — Hardcoded English loading strings not in `t()`** _(low)._ `SolrConfiguration.vue:1207-1220, 1620`. **Fix:** wrap each tip/message in `t('openregister', ...)`. +- **FE-8 — `console.log` inside a computed (fires every re-render)** _(medium, perf)._ `src/components/FacetComponent.vue:397-413` in `metadataDateFields`. **Fix:** delete all logs in the computed. +- **FE-9 — ~470 `console.log` calls across stores/components** _(low)._ Every store setter logs. **Fix:** remove non-error logs from `src/store/modules/*` + components; keep `console.error` in catches; drop the file-level `eslint-disable no-console`. +- **FE-11 — `SolrConfiguration.vue` is a 4646-line god-component** _(medium, refactor)._ **Fix:** extract `components/LoadingTips.vue`, `dialogs/settings/SolrFacetConfigDialog.vue`, a connection-form child; keep the section a thin orchestrator. One extraction per PR. +- **FE-12 — Inconsistent data-fetch: adapter-store vs hand-rolled fetch** _(medium)._ `object.js` uses `@conduction/nextcloud-vue`'s store factory; `register/schema/configuration.js` hand-roll `fetch`. Only `register.js` has single-flight/abort. **Fix:** converge on the shared store factory (ADR-022) or factor a `useApiFetch()` composable with abort + single-flight. +- **FE-13 — `ViewObject.vue` is 2706 lines with inline selection-step markup** _(low)._ **Fix:** extract `components/object/RegisterSchemaSelector.vue` + a property-table child. +- **FE-15 — `settings.js` is a 1756-line monolith with broad refetch** _(low)._ **Fix:** split into `settings/solr.js`, `settings/llm.js`, …; have save actions patch the changed slice from the response instead of full refetch. + +## Ops / background jobs / migrations +- **OPS-1 — Entire event-driven Actions feature is dead (`ActionListener` never registered)** _(high, verified)._ `lib/Listener/ActionListener.php` is referenced only in its own file; absent from `Application.php:1715-1856`. **Fix:** in `registerEventListeners()`, `registerEventListener(ObjectCreatedEvent::class, ActionListener::class)` + Updated/Deleted/Transitioned as the executor matches. **Verify:** configure an Action, save an object, confirm `ActionExecutor` ran. +- **OPS-2 — Webhooks only fire on object *create*** _(high, verified)._ `Application.php:1805` registers `WebhookEventListener` for `ObjectCreatedEvent` only, but the listener handles Updated/Deleted/Locked/Unlocked/Reverted/Register/Schema events. **Fix:** register the listener for every event class it handles (loop over the list). **Verify:** webhook on `object.updated` delivers on edit. +- **OPS-3 — `SyncConfigurationsJob` never scheduled** _(high, verified)._ `lib/Cron/SyncConfigurationsJob.php` (TimedJob) absent from `info.xml` `` and never `IJobList::add`'d. **Fix:** add `OCA\OpenRegister\Cron\SyncConfigurationsJob` to ``; bump ``. **Verify:** `occ background-job:list | grep SyncConfigurations`. +- **OPS-4 — Three orphaned BackgroundJobs (dead code)** _(medium)._ `WebhookDeliveryJob`, `CacheWarmupJob`, `BackfillCalendarLinksJob` — zero references, not in info.xml. **Fix:** per-job decide: wire up (info.xml or `IJobList::add`) if needed (look closely at `WebhookDeliveryJob` given OPS-2), else delete the file. +- **OPS-6 — `ScheduledNotificationJob` loads all objects per fire** — see `PERF-3`. +- **OPS-7 — `DestructionCheckJob` full-table scan + unbounded appconfig "notified" list** _(medium)._ `lib/BackgroundJob/DestructionCheckJob.php:193-265` — `fetchAll()` all retention rows; appends UUIDs to a JSON appconfig blob that only ever grows. **Fix:** batch the retention query (`setMaxResults`+offset); prune notified UUIDs after destruction or replace the appconfig blob with a per-object flag/table. **Verify:** appconfig value size stays bounded over repeated runs. +- **OPS-8 — Notification-subscription repair re-runs every upgrade, re-enabling overrides** _(medium)._ `appinfo/info.xml:76` (``) + `lib/Repair/MigrateNotificationSubscriptionsToUserConfig.php:104-165` forces `enabled:true` each run, never marks done. **Fix:** make it run-once (delete/flag legacy rows or set an appconfig marker + early-return); consider moving to a one-shot migration. **Verify:** `occ upgrade` twice — a manually-disabled override stays disabled. +- **OPS-9 — `DedupeRegistersCommand` deletes by default (no confirmation)** _(medium)._ `lib/Command/DedupeRegistersCommand.php:103-170` — dry-run is opt-in. **Fix:** default to dry-run (require `--apply`), or add an interactive confirmation with `--no-interaction`/`--force` bypass. **Verify:** no-flag invocation reports without deleting. +- **OPS-11 — Unbatched `fetchAll()` in data migrations (OOM risk on upgrade)** _(medium)._ `lib/Migration/Version1Date20250829120000.php:110,129` + `Version1Date20260521120000.php:131,217`. **Fix:** chunk with `setFirstResult`/`setMaxResults` paging; free each batch. **Verify:** `occ upgrade` against a large magic table — bounded peak memory. +- **OPS-14 — `FileChangeListener` synchronous extraction on the instance-wide `NodeWritten` hot path** _(low)._ `lib/Listener/FileChangeListener.php:100-215` — in `immediate`+`all` mode calls `extractFile()` synchronously (`:185`); `info`-level log per matching write (`:154`). **Fix:** always enqueue `FileTextExtractionJob` instead of extracting inline; demote the per-event log to `debug`. +- **OPS-13 — "Nightly" warmup jobs use a fixed 24h interval, not a night window** _(low)._ `SolrNightlyWarmupJob.php:80`, `NameCacheWarmupJob.php:67` — no `setTimeSensitivity(TIME_INSENSITIVE)` / time-of-day gate (cf. `LogCleanUpTask.php:81`). **Fix:** add `setTimeSensitivity(IJob::TIME_INSENSITIVE)`; gate `run()` to a configured hour window if true night-running is required. +- **OPS-15 — `TransferExecutionJob` (a QueuedJob) declared in ``** _(low)._ `info.xml:94` — NC adds it with a null argument → spurious "missing transferList" error on every fresh install. **Fix:** remove it from ``; rely on dynamic `IJobList::add(...)` (confirm `TransferCheckJob` enqueues it). +- **OPS-10 — Solr / migrate-storage occ commands disabled by circular DI** _(low)._ `info.xml:126-131` (commented out). **Fix:** lazy-resolve the heavy services inside the commands (factory closure / `ContainerInterface`) instead of constructor-injecting `SettingsService`→`IndexService`; re-enable. **Verify:** `occ openregister:migrate-storage --help` without a DI fatal. +- **OPS-5 — Stale `.backup_*` files shipped in `lib/Cron/`** _(low)._ `ConfigurationCheckJob.php.backup_20251230_001404`, `WebhookRetryJob.php.backup_20251230_001130`. **Fix:** delete both; add `*.backup_*` to `.gitignore` + build excludes. +- **OPS-12 — Migration version-number scheme mixes `Version1Date…` and `Version002003000Date…`** _(low, latent ordering hazard)._ The four `Version002…` files sort *after* all `Version1…` by `version_compare`. **Fix:** standardise going forward (rename the four to `Version1Date…` in a coordinated change — do **not** rename already-applied migrations on existing installs carelessly; NC tracks applied names in `oc_migrations`). Add a lint enforcing the convention. + +--- + +## Appendix — suggested execution order for an autonomous agent + +1. **P0 security batch A (no behaviour change for legit users):** `SEC-CTRL-1`, `SEC-CTRL-6`, `SEC-CTRL-3`, `SEC-CTRL-2`, `BUG-DB-2`, `BUG-DB-1`. Small, surgical, high impact. +2. **P0 SSRF/SSTI batch:** build the shared `assertSafeFetchUrl()` once (`SEC-SVC-1`), wire `SEC-SVC-2`/`SEC-SVC-6`; build the sandboxed-Twig pattern once, wire `SEC-SVC-3`/`SEC-SVC-4`. +3. **P0 data-loss/bulk batch:** `BUG-OBJ-1`, `BUG-OBJ-15`, `BUG-OBJ-8`, `BUG-OBJ-2` (add tests first — these are the highest-risk changes). +4. **OPS registration fixes** (`OPS-1/2/3/4`) — they're one-line wires but restore whole features; do them while editing `Application.php`/`info.xml` (combine with `REF-8`). +5. **P1 correctness** by area, then **P2 performance** (PERF-1/4/2 first). +6. **P3 refactors** only after `REF-15` tests exist; mechanical wins (`REF-4`, `REF-9`) before giants. +7. **P4 frontend** XSS first (`FE-1`, `FE-2`, `FE-3`), then the rest. + +Each batch: branch → implement → `composer check:strict` + `bash scripts/run-hydra-gates.sh` → bump `appinfo/info.xml` `` if behaviour/bundle changed → verify against `localhost:8080` → PR (no `Co-Authored-By`). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..ca0f0d896b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,35 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders at **info@conduction.nl**. + +All complaints will be reviewed and investigated promptly and fairly. Community leaders are obligated to respect the privacy and security of the reporter. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..05d9c98874 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,270 @@ +# Contributing to Conduction Nextcloud Apps + +Thank you for considering contributing to our projects! It's people like you that make open source such a great community. + +## Code of Conduct + +This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. + +## How Can I Contribute? + +### Reporting Bugs + +Before creating bug reports, please check the issue list as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible: + +- Use a clear and descriptive title +- Describe the exact steps which reproduce the problem +- Provide specific examples to demonstrate the steps +- Describe the behavior you observed after following the steps +- Explain which behavior you expected to see instead and why +- Include screenshots if possible + +### Suggesting Enhancements + +Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include: + +- Use a clear and descriptive title +- Provide a step-by-step description of the suggested enhancement +- Describe the current behavior and explain which behavior you expected to see instead +- Explain why this enhancement would be useful + +### Pull Requests + +- Fork the repo and create your branch from `development` +- If you've added code that should be tested, add tests +- If you've changed APIs, update the documentation +- Ensure the test suite passes +- Make sure your code lints (`composer cs:check`) +- Create a pull request! + +### PR Size + +Prefer **one PR per logically-coherent finding or feature**. Each PR's commit message, checkbox, and inline-comment chain should map to a single change unit — reviewers hold a clearer mental model on focused PRs than on large ones. + +- When a PR's scope grows past **~10 commits or ~30 files**, consider splitting it before requesting review. The per-finding commits stay; the PR boundary moves. +- **Exception:** release-promotion PRs (`development → beta`, `beta → main`) aggregate every change since the last cut and are expected to be larger. +- PRs touching many files across unrelated subsystems tend to get reviewed paragraph-by-paragraph rather than holistically — that's a signal to split, not to push through. + +## Branch Protection & Git Flow + +We use a structured branching model to ensure stability across environments. All branches are protected via **organization-wide rulesets** on the ConductionNL GitHub organization — direct pushes are not allowed. Every change flows through a pull request with peer review and CI checks. + +```mermaid +graph LR + F["feature/*\nbugfix/*"] -->|"PR + 1 review\n+ Quality CI ✓"| D[development] + D -->|"PR + 1 review\n+ Quality CI ✓"| B[beta] + B -->|"PR + 2 reviews\n+ Branch CI ✓"| M[main] + H["hotfix/*"] -->|"PR + 1 review\n+ Quality CI ✓"| B + H -->|"PR + 2 reviews\n+ Branch CI ✓"| M + + style F fill:#e1f5fe + style D fill:#fff9c4 + style B fill:#ffe0b2 + style M fill:#c8e6c9 + style H fill:#ffcdd2 +``` + +### Branch Rules + +These rules are enforced organization-wide across all ConductionNL repositories. They cannot be overridden at the repository level. + +| Target | Allowed Sources | Reviews | Required CI Checks | +| ------------- | -------------------------------------------- | ------------------- | --------------------------------------------------- | +| `development` | `feature/*`, `bugfix/*` | 1 approving review | Quality CI (`lint-check`) | +| `beta` | `development`, `hotfix/*`, `main` (backport) | 1 approving review | Quality CI (`lint-check`) | +| `main` | `beta`, `hotfix/*` | 2 approving reviews | Branch Protection CI (`check-branch`, `lint-check`) | + +### Organization-Wide Rulesets + +Branch protection is managed at the **organization level**, not per-repository. This ensures consistent enforcement across all Conduction apps. The three rulesets are: + +1. **Development Branch Protection** — Enforces peer review and Quality CI for all feature work entering `development` +2. **Beta Branch Protection** — Same requirements as development, gates the path to beta releases +3. **Main Branch Protection** — Stricter: requires 2 reviewers and branch-source validation before stable release + +All rulesets also enforce: + +- No force pushes +- No branch deletion +- Stale reviews dismissed on new pushes +- All review threads must be resolved before merge + +### How It Works + +1. **Feature work** happens on `feature/*` or `bugfix/*` branches created from `development` +2. **PRs to `development`** require 1 approving peer review and the Quality CI workflow must pass +3. **When ready for beta release**, a developer creates a PR from `development` to `beta` — same review + CI requirements +4. **Merging to `beta`** triggers an automatic beta release to the Nextcloud App Store +5. **When ready for stable release**, a developer creates a PR from `beta` to `main` — requires 2 approving reviews and Branch Protection CI +6. **Merging to `main`** triggers an automatic stable release to the Nextcloud App Store +7. **Hotfixes** can target both `beta` and `main` directly for urgent patches via PR (same review requirements apply) +8. **Branches are automatically deleted** after their PR is merged + +> **Important:** There are no automatic merges or auto-created PRs between branches. Every promotion (development -> beta -> main) requires a deliberate pull request created by a developer, with peer review and CI passing before merge is allowed. + +## Quality Workflow + +Every pull request triggers our automated quality pipeline. **All checks must pass before a PR can be merged.** This ensures that no code enters `development`, `beta`, or `main` without meeting our quality standards. + +### PHP Quality Checks + +| Check | Tool | What It Does | +| ------------------- | ----------------- | ------------------------------------------------------ | +| **Lint** | `php -l` | Syntax validation — catches parse errors | +| **Code Style** | PHPCS | Enforces coding standards (PSR-12 + custom rules) | +| **Static Analysis** | PHPStan (level 5) | Type checking, undefined methods, dead code | +| **Static Analysis** | Psalm | Additional type inference and security analysis | +| **Mess Detection** | PHPMD | Complexity, naming, unused code, design problems | +| **Metrics** | phpmetrics | Maintainability index, coupling, cyclomatic complexity | + +### Frontend Quality Checks + +| Check | Tool | What It Does | +| -------------- | --------- | ---------------------------------- | +| **JavaScript** | ESLint | Enforces JS/Vue coding standards | +| **CSS** | Stylelint | Enforces CSS/SCSS coding standards | + +### Dependency Checks + +| Check | What It Does | +| ----------------------------- | ---------------------------------------------------------- | +| **License (npm + composer)** | Ensures all dependencies use approved open-source licenses | +| **Security (npm + composer)** | Checks for known vulnerabilities in dependencies | + +### Running Quality Checks Locally + +```bash +# PHP +composer cs:check # PHPCS code style +composer cs:fix # Auto-fix code style +composer phpstan # PHPStan static analysis +composer psalm # Psalm static analysis +composer phpmd # PHPMD mess detection + +# Frontend +npm run lint # ESLint +npx stylelint "src/**/*.{css,scss,vue}" # Stylelint +``` + +## App Store Release Process + +Releases to the Nextcloud App Store are fully automated via GitHub Actions. They are triggered by merging PRs into `beta` or `main`. Version numbers are calculated automatically from PR labels. + +```mermaid +graph TD + subgraph "Beta Release" + D[development] -->|"Developer creates PR"| BP1{"Quality CI\npasses?"} + BP1 -->|"Yes"| BM["Merge PR to beta"] + BP1 -->|"No"| BF["Fix issues\nre-push"] + BF --> BP1 + BM --> BT{Version Bump\nfrom PR label} + BT -->|"label: major"| BV1["v2.0.0-beta.20260319"] + BT -->|"label: minor"| BV2["v1.1.0-beta.20260319"] + BT -->|"label: patch\n(default)"| BV3["v1.0.1-beta.20260319"] + BV1 & BV2 & BV3 --> BB["Build & Package"] + BB --> BU["Upload to App Store\n(nightly channel)"] + BB --> BG["Create GitHub\npre-release"] + end + + subgraph "Stable Release" + B2[beta] -->|"Developer creates PR"| SP1{"Branch Protection\nCI passes?"} + SP1 -->|"Yes"| SM["Merge PR to main"] + SP1 -->|"No"| SF["Fix issues"] + SF --> SP1 + SM --> ST{Version Bump\nfrom PR label} + ST -->|"from PR labels"| SV["v1.1.0"] + SV --> SB["Build & Package"] + SB --> SU["Upload to App Store\n(stable channel)"] + SB --> SG["Create GitHub release\nwith changelog"] + end + + style D fill:#fff9c4 + style BM fill:#ffe0b2 + style SM fill:#c8e6c9 + style BU fill:#e1bee7 + style SU fill:#e1bee7 + style BF fill:#ffcdd2 + style SF fill:#ffcdd2 +``` + +### Version Labeling + +Add a label to your PR to control the version bump: + +| Label | Version Change | When to Use | +| ----------------- | ----------------- | ------------------------------------ | +| `major` | `1.0.0` → `2.0.0` | Breaking changes, major redesigns | +| `minor` | `1.0.0` → `1.1.0` | New features, non-breaking additions | +| `patch` (default) | `1.0.0` → `1.0.1` | Bug fixes, small improvements | + +### Release Artifacts + +Each release automatically: + +1. Bumps the version in `appinfo/info.xml` +2. Builds the app (composer install, npm build) +3. Creates a signed tarball +4. Uploads to the [Nextcloud App Store](https://apps.nextcloud.com) +5. Creates a GitHub release with auto-generated changelog + +## Documentation Release Process + +Documentation is built with [Docusaurus](https://docusaurus.io/) and deployed to GitHub Pages. + +1. Documentation source lives in the `docs/` (or `docusaurus/`) folder on any branch +2. Push or merge to the `documentation` branch triggers the build +3. Docusaurus builds the static site +4. The site is deployed to GitHub Pages with a custom domain (e.g., `openregister.app`) + +Each app has its own documentation site — see the app's README for its URL. + +## Development Process + +1. Create a feature request issue describing your proposed changes +2. Fork the repository +3. Create a new branch: `git checkout -b feature/[issue-number]/[feature-name]` +4. Make your changes +5. Run quality checks: `composer cs:check` and `composer phpstan` +6. Push to your fork and open a Pull Request +7. Wait for Quality CI to pass, address any failures +8. Request review from a team member + +### Git Commit Messages + +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): + +- `feat:` for new features +- `fix:` for bug fixes +- `chore:` for maintenance tasks +- `docs:` for documentation changes +- `refactor:` for code refactoring +- Use the present tense and imperative mood +- Limit the first line to 72 characters + +### PR Labels for Changelogs + +Add labels to categorize your PR in the automated changelog: + +- **`feature`** / **`enhancement`** — New features (appears under "Added") +- **`bug`** / **`fix`** — Bug fixes (appears under "Fixed") +- **`docs`** — Documentation updates +- **`refactor`** / **`chore`** — Code improvements (appears under "Changed") +- **`skip-changelog`** — Exclude from changelog + +## Development Setup + +1. Install PHP 8.1+ and Node.js 20+ +2. Install Composer +3. Clone the repository +4. Run `composer install` and `npm install` +5. Configure your [Nextcloud development environment](https://github.com/ConductionNL/nextcloud-docker-dev) + +## Community + +- Join the [Common Ground Slack](https://commonground.nl) +- Follow us on [X](https://x.com/conduction_nl) +- Read our updates on [LinkedIn](https://www.linkedin.com/company/conduction/) + +## License + +By contributing, you agree that your contributions will be licensed under the same license as the project (EUPL-1.2 unless stated otherwise). diff --git a/README.md b/README.md index 8a00a4d66b..4f11e86694 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -

+> [!IMPORTANT] +> ## 🚚 This repository has moved to Codeberg +> +> Active development now happens at **https://codeberg.org/Conduction/openregister**. +> This GitHub mirror is read-only — issues, pull requests, and new commits should go to Codeberg. +> Update your remote with: `git remote set-url origin https://codeberg.org/Conduction/openregister`

OpenRegister logo

@@ -70,9 +75,9 @@ It is the shared data backbone for apps like [OpenCatalogi](https://github.com/C - **Pluggable integration registry** — Object surfaces (sidebar tabs, dashboard widgets, detail pages, reference properties) are driven by a registry of `IntegrationProvider`s. Five built-ins ship out of the box (Files, Notes, Tags, Tasks, Audit Trail); apps add their own — including OpenConnector-backed external integrations like xWiki — without touching OpenRegister core. See [docs/Integrations/pluggable-integration-registry.md](docs/Integrations/pluggable-integration-registry.md) for the "how to add an integration" walkthrough. - **SOLR Integration** — Optional Apache Solr for advanced search scenarios - **Source Synchronization** — Keep registers in sync with external data sources -- **Schema Import** — Import schemas from Schema.org, OpenAPI, and GGM standards +- **Schema Import** — Import schemas from Schema.org types and GGM (Gemeentelijk Gegevensmodel) objecttypes from bundled, versioned snapshots: datatype mapping, Dutch metadata preserved, pre-filled JSON-LD vocabulary, import provenance, and a guarded update-from-source flow. The upload path also detects (or is told) its dialect (`json-schema` / `openapi` / `schema.org` / `ggm`) and rejects unidentifiable input with HTTP 422. OpenAPI/JSON Schema ingestion is unchanged. See [docs/Features/schema-import.md](docs/Features/schema-import.md). - **CalDAV Tasks** — Attach Nextcloud tasks and comments directly to data objects -- **JSON-LD and Linked Data** — Standards-compliant output for the open data ecosystem +- **JSON-LD and Linked Data** — Opt-in read-side JSON-LD output via content negotiation (`Accept: application/ld+json`) on the object read endpoints, with `@id` set to the canonical object URI, a `@context` derived from the schema definition, and dereferenceable `/api/contexts/*` documents. Schema.org alignment is per-schema mapping (opt-in via the schema's `configuration.jsonld` block); zero-config schemas still emit valid JSON-LD with OpenRegister-local terms. Read-side only — no JSON-LD ingest. See [docs/Features/json-ld.md](docs/Features/json-ld.md). ## Architecture @@ -171,10 +176,14 @@ npm run build # Production build ### Code quality ```bash -# PHP -composer phpcs # Check coding standards -composer cs:fix # Auto-fix issues -composer phpmd # Mess detection +# Authoritative gate (same as CI) — must pass before pushing +composer check:strict # phpcs + phpmd + phpstan + psalm + all tests + +# Individual tools +composer phpcs # Check coding standards (0 errors required) +composer cs:fix # Auto-fix coding-standard issues +composer phpmd # Mess detection (52 violations tracked in openspec/changes/openregister-legacy-quality-cleanup/) +composer phpstan # Static analysis (baseline: 1371 entries, tracked in phpstan-baseline.neon) composer phpmetrics # HTML metrics report # Frontend @@ -209,7 +218,7 @@ Full documentation is available at **[openregisters.app](https://openregisters.a ## Standards & Compliance -- **Data standard:** JSON Schema, JSON-LD, Schema.org +- **Data standard:** JSON Schema; JSON-LD read-side output (opt-in via content negotiation); Schema.org alignment per-schema mapping (opt-in) — see [docs/Features/json-ld.md](docs/Features/json-ld.md) - **API standard:** NLGov REST API Design Rules (Logius) - **Dutch interoperability:** Common Ground principles, VNG standards - **Accessibility:** WCAG AA (Dutch government requirement) @@ -232,4 +241,4 @@ EUPL-1.2 — see [LICENSE](LICENSE) for details. ## Authors -Built by [Conduction](https://conduction.nl) — open-source software for Dutch government and public sector organizations. +Built by [Conduction](https://conduction.nl) — open-source software for Dutch government and public sector organizations. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..bbbfa1354c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,88 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in any Conduction Nextcloud app, please report it responsibly. + +**Do NOT open a public GitHub issue for security vulnerabilities.** + +Instead, please email us at: **security@conduction.nl** + +Include the following in your report: + +- Description of the vulnerability +- Steps to reproduce the issue +- Potential impact +- Suggested fix (if any) + +## Response Timeline + +- **Acknowledgement:** Within 48 hours of receiving your report +- **Initial assessment:** Within 1 week +- **Fix and disclosure:** We aim to resolve critical vulnerabilities within 30 days + +## Supported Versions + +We provide security updates for the latest stable release of each app. Older versions may not receive security patches. + +## Scope + +This security policy applies to all repositories under the [ConductionNL](https://github.com/ConductionNL) organization. + +## Recognition + +We appreciate responsible disclosure and will credit reporters (with permission) in our release notes. + +## Software Bill of Materials (SBOM) + +We publish a [CycloneDX](https://cyclonedx.org/) 1.5 JSON SBOM for every release of every Conduction Nextcloud app. The SBOM lists every production dependency (Composer + npm, merged, dev-dependencies excluded) with name, version, license, and PURL. Each SBOM is CVE-scanned with [Grype](https://github.com/anchore/grype) at build time and the release fails if any **critical** vulnerability is detected. + +### Stable URLs + +For every app `` under [ConductionNL](https://github.com/ConductionNL), two URLs always work: + +| Use case | URL pattern | +| ------------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| **Always-latest released SBOM** (auto-redirects to newest release) | `https://github.com/ConductionNL//releases/latest/download/sbom.cdx.json` | +| **Specific release SBOM** (pinned, for compliance archives) | `https://github.com/ConductionNL//releases/download//sbom.cdx.json` | + +Example — fetch the latest openregister SBOM: + +```bash +curl -sL https://github.com/ConductionNL/openregister/releases/latest/download/sbom.cdx.json | jq . +``` + +Example — fetch the SBOM for a specific historical release: + +```bash +curl -sL https://github.com/ConductionNL/openregister/releases/download/v1.0.0/sbom.cdx.json | jq . +``` + +### Update cadence + +A new SBOM is generated and attached on every release tag. We do not commit SBOMs into the repository tree — they are published exclusively as release assets to keep main-branch history clean and to guarantee every SBOM corresponds to an immutable release artifact. + +### Format + +- **Specification:** CycloneDX 1.5 +- **Encoding:** JSON +- **Filename:** `sbom.cdx.json` (consistent across all apps) +- **Scope:** Production dependencies only — `--omit=dev` for both Composer (`composer CycloneDX:make-sbom`) and npm (`@cyclonedx/cyclonedx-npm`). Composer plugins are also omitted. + +### Verification before publication + +Each release SBOM passes through these gates before it ships: + +1. **Grype CVE scan** — `--fail-on critical` against the SBOM itself. +2. **`composer audit`** — informational, captured in CI logs. +3. **`npm audit --audit-level=critical`** — informational, captured in CI logs. + +If any of these block, the release is held until the underlying issue is patched. + +### Workflow artifact (CI-only) + +A 90-day workflow artifact named `sbom-` is also produced on every successful CI run on `main` / `beta` / `development`. This is for internal audit / replay only — external consumers should always use the release-asset URLs above for stable, version-pinned access. + +### Reporting SBOM-related issues + +If you spot a missing dependency, an incorrect version, or a CVE we should be alerted to, email `security@conduction.nl` per the disclosure process at the top of this document. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000000..bdf64e6565 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,25 @@ +# Support + +## Getting Help + +- **GitHub Issues** — For bug reports and feature requests, use the issue tracker on the relevant repository +- **GitHub Discussions** — For questions and community discussions +- **Documentation** — Each app has documentation in its `docs/` folder or on its Docusaurus website + +## Commercial Support + +For commercial support, SLA agreements, or custom development: + +- **Email:** info@conduction.nl +- **Website:** [conduction.nl](https://conduction.nl) + +## Community + +- [Common Ground](https://commonground.nl) — The Dutch government open source community +- [LinkedIn](https://www.linkedin.com/company/conduction/) — Follow us for updates +- [X / Twitter](https://x.com/conduction_nl) — Quick updates and announcements + +## Useful Links + +- [Nextcloud Docker Dev Environment](https://github.com/ConductionNL/nextcloud-docker-dev) — Development setup +- [OpenRegister](https://github.com/ConductionNL/openregister) — Foundation repository for all Conduction apps diff --git a/appinfo/info.xml b/appinfo/info.xml index a5539dc08d..f103356b0e 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -12,11 +12,11 @@ - **Schema-based registers** — Define data structures with JSON Schema and organize them into registers - **Full CRUD API** — Create, read, update, and delete objects through a powerful REST API -- **Full-text search with faceting** — Search across registers with filters, powered by magic tables or Solr +- **Full-text search with faceting** — Search across registers with filters, powered by magic tables - **Role-based access control** — Fine-grained permissions with organization-level multitenancy - **Event-driven architecture** — React to data changes with events for tasks, notifications, and integrations - **CalDAV tasks and notes** — Attach Nextcloud tasks and comments directly to your data objects -- **JSON-LD and Linked Data** — Standards-compliant data that connects to the broader open data ecosystem +- **JSON-LD and Linked Data** — Opt-in read-side JSON-LD output (via `Accept: application/ld+json` content negotiation) with canonical `@id`, schema-derived `@context`, and dereferenceable context documents; Schema.org alignment is per-schema opt-in mapping. Read-side only (no JSON-LD ingest) - **Source synchronization** — Keep your registers in sync with external data sources Open Register powers apps like OpenCatalogi, Procest, Pipelinq, and Software Catalogus, giving them a shared, federated data backbone. @@ -29,31 +29,31 @@ Free and open source under the EUPL license. - **Schema-gebaseerde registers** — Definieer datastructuren met JSON Schema en organiseer ze in registers - **Volledige CRUD API** — Maak, lees, wijzig en verwijder objecten via een krachtige REST API -- **Full-text zoeken met facetten** — Zoek dwars door registers met filters, aangedreven door magic tables of Solr +- **Full-text zoeken met facetten** — Zoek dwars door registers met filters, aangedreven door magic tables - **Rolgebaseerde toegangscontrole** — Fijnmazige rechten met multitenancy op organisatieniveau - **Event-driven architectuur** — Reageer op datawijzigingen met events voor taken, notificaties en integraties - **CalDAV taken en notities** — Koppel Nextcloud-taken en opmerkingen direct aan je dataobjecten -- **JSON-LD en Linked Data** — Data conform standaarden die aansluit op het bredere open data ecosysteem +- **JSON-LD en Linked Data** — Optionele read-side JSON-LD-uitvoer (via `Accept: application/ld+json` content-negotiation) met canonieke `@id`, een uit het schema afgeleide `@context` en dereferenceerbare context-documenten; Schema.org-uitlijning is een per-schema opt-in mapping. Alleen lezen (geen JSON-LD-import) - **Bronsynchronisatie** — Houd je registers in sync met externe databronnen Open Register drijft apps zoals OpenCatalogi, Procest, Pipelinq en Software Catalogus aan en geeft ze een gedeelde, gefedereerde data-backbone. Vrij en open source onder de EUPL-licentie. ]]> - 0.2.13-unstable.84 - agpl + 0.2.17-unstable.14 + EUPL-1.2 Conduction OpenRegister organization tools search - https://github.com/ConductionNL/OpenRegister - https://github.com/ConductionNL/OpenRegister/discussions - https://github.com/ConductionNL/OpenRegister/issues - https://github.com/ConductionNL/OpenRegister + https://codeberg.org/Conduction/openregister + https://codeberg.org/Conduction/openregister/issues + https://codeberg.org/Conduction/openregister - https://raw.githubusercontent.com/ConductionNL/OpenRegister/main/img/app-store.svg - https://raw.githubusercontent.com/ConductionNL/openregister/main/img/screenshot.png + https://codeberg.org/Conduction/openregister/raw/branch/main/img/screenshot-dashboard.png + https://codeberg.org/Conduction/openregister/raw/branch/main/img/screenshot-registers.png + https://codeberg.org/Conduction/openregister/raw/branch/main/img/screenshot-objects.png https://conduction.gitbook.io/openregister-nextcloud/ @@ -74,24 +74,43 @@ Vrij en open source onder de EUPL-licentie. OCA\OpenRegister\Repair\RegisterRiskLevelMetadata OCA\OpenRegister\Repair\LogDanglingLinkedTypes OCA\OpenRegister\Repair\MigrateNotificationSubscriptionsToUserConfig + OCA\OpenRegister\Repair\SeedDirectoryVirtualSchemas + OCA\OpenRegister\Repair\SeedAppVirtualSchemas + OCA\OpenRegister\Repair\SeedTablesVirtualSchemas + OCA\OpenRegister\Repair\ImportCredentialBrokerRegister + OCA\OpenRegister\Repair\ImportDsarRegisters + OCA\OpenRegister\Repair\ImportEdepotTransferRegister + OCA\OpenRegister\Repair\ImportTrustConfigurationRegister + OCA\OpenRegister\Repair\RegisterOpenRegisterWithDoriath + OCA\OpenRegister\Repair\SeedZgwZakenMigrationPack
OCA\OpenRegister\Repair\RegisterRiskLevelMetadata OCA\OpenRegister\Repair\LogDanglingLinkedTypes + OCA\OpenRegister\Repair\SeedDirectoryVirtualSchemas + OCA\OpenRegister\Repair\SeedAppVirtualSchemas + OCA\OpenRegister\Repair\SeedTablesVirtualSchemas + OCA\OpenRegister\Repair\ImportCredentialBrokerRegister + OCA\OpenRegister\Repair\ImportDsarRegisters + OCA\OpenRegister\Repair\ImportEdepotTransferRegister + OCA\OpenRegister\Repair\ImportTrustConfigurationRegister + OCA\OpenRegister\Repair\RegisterOpenRegisterWithDoriath + OCA\OpenRegister\Repair\SeedZgwZakenMigrationPack OCA\OpenRegister\Cron\LogCleanUpTask OCA\OpenRegister\Cron\ConfigurationCheckJob - OCA\OpenRegister\BackgroundJob\SolrNightlyWarmupJob OCA\OpenRegister\BackgroundJob\NameCacheWarmupJob OCA\OpenRegister\BackgroundJob\CronFileTextExtractionJob + OCA\OpenRegister\BackgroundJob\ChunkVectorizationJob OCA\OpenRegister\Cron\WebhookRetryJob OCA\OpenRegister\BackgroundJob\BlobMigrationJob OCA\OpenRegister\BackgroundJob\DestructionCheckJob OCA\OpenRegister\Cron\TransferCheckJob - OCA\OpenRegister\BackgroundJob\TransferExecutionJob + OCA\OpenRegister\Cron\SyncConfigurationsJob + OCA\OpenRegister\Cron\SyncDataJob OCA\OpenRegister\BackgroundJob\ScheduledWorkflowJob OCA\OpenRegister\BackgroundJob\ScheduledNotificationJob OCA\OpenRegister\BackgroundJob\ActionScheduleJob @@ -99,11 +118,17 @@ Vrij en open source onder de EUPL-licentie. OCA\OpenRegister\BackgroundJob\TenantPurgeJob OCA\OpenRegister\BackgroundJob\TenantUsageSyncJob OCA\OpenRegister\BackgroundJob\ExecutionHistoryCleanupJob - OCA\OpenRegister\BackgroundJob\RealtimeEventRetentionJob OCA\OpenRegister\BackgroundJob\AvgRetentionJob + OCA\OpenRegister\BackgroundJob\DsarRetentionSweepJob OCA\OpenRegister\BackgroundJob\ReportRenderJob - OCA\OpenRegister\BackgroundJob\BatchNotificationJob + OCA\OpenRegister\BackgroundJob\NotificationQueueFlushJob OCA\OpenRegister\Cron\ArchivalRetentionTask + OCA\OpenRegister\Cron\HandoffQueueDrainJob + OCA\OpenRegister\BackgroundJob\TemporalCalculationSweepJob + OCA\OpenRegister\BackgroundJob\DsarDpiaDetectionJob + OCA\OpenRegister\BackgroundJob\ScheduleReconcilerJob + OCA\OpenRegister\Cron\DbalIntrospectionJob + OCA\OpenRegister\BackgroundJob\ScheduledReportJob @@ -120,18 +145,26 @@ Vrij en open source onder de EUPL-licentie. OCA\OpenRegister\Settings\OpenRegisterAdmin OCA\OpenRegister\Settings\IntegrationsAdminSettings OCA\OpenRegister\Sections\OpenRegisterAdmin + OCA\OpenRegister\Settings\OpenRegisterPersonal - - - OCA\OpenRegister\Command\RematerialiseCalculationsCommand OCA\OpenRegister\Command\BackfillSystemOwnerCommand + OCA\OpenRegister\Command\BackfillTranslationSourceLanguageCommand + + OCA\OpenRegister\Command\EncryptFieldCommand + OCA\OpenRegister\Command\DedupeRegistersCommand + OCA\OpenRegister\Command\ResolverListCommand + OCA\OpenRegister\Command\TimeReconcileCommand + + OCA\OpenRegister\Command\GenerateVapidKeys + + OCA\OpenRegister\Command\TablesSyncCommand diff --git a/appinfo/routes.php b/appinfo/routes.php index 71a269467e..6dad1b2b24 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -13,6 +13,46 @@ 'Consumers' => ['url' => 'api/consumers'], ], 'routes' => [ + // Federation (cross-instance OCM sharing) — token-scoped serving endpoints. + // #[PublicPage]: the caller is a remote instance authenticated by the + // bearer share token in the URL, not a local session. + ['name' => 'federation#objects', 'url' => '/api/federation/{shareToken}/objects', 'verb' => 'GET', 'requirements' => ['shareToken' => '[^/]+']], + ['name' => 'federation#object', 'url' => '/api/federation/{shareToken}/objects/{id}', 'verb' => 'GET', 'requirements' => ['shareToken' => '[^/]+', 'id' => '[^/]+']], + ['name' => 'federation#meta', 'url' => '/api/federation/{shareToken}/meta', 'verb' => 'GET', 'requirements' => ['shareToken' => '[^/]+']], + // Federation write-through (read-write shares only, token-scoped). + ['name' => 'federation#createObject', 'url' => '/api/federation/{shareToken}/objects', 'verb' => 'POST', 'requirements' => ['shareToken' => '[^/]+']], + ['name' => 'federation#updateObject', 'url' => '/api/federation/{shareToken}/objects/{id}', 'verb' => 'PUT', 'requirements' => ['shareToken' => '[^/]+', 'id' => '[^/]+']], + ['name' => 'federation#deleteObject', 'url' => '/api/federation/{shareToken}/objects/{id}', 'verb' => 'DELETE', 'requirements' => ['shareToken' => '[^/]+', 'id' => '[^/]+']], + // Federation share management (authenticated, organisation-scoped). + ['name' => 'federation#shares', 'url' => '/api/federation/shares', 'verb' => 'GET'], + ['name' => 'federation#createShare', 'url' => '/api/federation/shares', 'verb' => 'POST'], + ['name' => 'federation#revokeShare', 'url' => '/api/federation/shares/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '\d+']], + + // Credential broker (credential-broker-service) — owner-scoped credential + // metadata CRUD + per-app signing-secret registration + the guarded broker + // call. The token broker call (`/request`) reads the app id from the verified + // X-Credential-Token header (never the body); the session broker call + // (`/session-request`) authenticates via the NC session + CSRF requesttoken + // and reads the app id from the body, with the owner guard still enforced + // against the session user. All owner-scoped, static errors, no secret leak. + ['name' => 'credential#index', 'url' => '/api/credentials', 'verb' => 'GET'], + ['name' => 'credential#providers', 'url' => '/api/credentials/providers', 'verb' => 'GET'], + ['name' => 'credential#create', 'url' => '/api/credentials', 'verb' => 'POST'], + ['name' => 'credential#update', 'url' => '/api/credentials/{id}', 'verb' => 'PUT', 'requirements' => ['id' => '[^/]+']], + ['name' => 'credential#destroy', 'url' => '/api/credentials/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+']], + ['name' => 'credential#registerApp', 'url' => '/api/credentials/apps/{appId}/register', 'verb' => 'POST', 'requirements' => ['appId' => '[a-z0-9_-]+']], + ['name' => 'credential#brokerRequest', 'url' => '/api/credentials/{id}/request', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'credential#sessionBrokerRequest', 'url' => '/api/credentials/{id}/session-request', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + + // Web Push channel (openregister-web-push-engine). + // VAPID public key (browser subscribe key) + current-user subscription CRUD + // (owner-scoped, no IDOR) + per-originApp cobalt-hex notification icon/badge. + ['name' => 'webPush#vapidPublicKey', 'url' => '/webpush/vapid-public-key', 'verb' => 'GET'], + ['name' => 'webPush#subscribe', 'url' => '/webpush/subscription', 'verb' => 'POST'], + ['name' => 'webPush#unsubscribe', 'url' => '/webpush/subscription', 'verb' => 'DELETE'], + ['name' => 'webPush#hexIcon', 'url' => '/webpush/icon/{app}', 'verb' => 'GET', 'requirements' => ['app' => '[a-z0-9_-]+']], + ['name' => 'webPush#hexBadge', 'url' => '/webpush/badge/{app}', 'verb' => 'GET', 'requirements' => ['app' => '[a-z0-9_-]+']], + // Integration registry (read-only discovery API) — // pluggable-integration-registry task 4.3 / tasks.md#task-20. ['name' => 'integrations#index', 'url' => '/api/integrations', 'verb' => 'GET'], @@ -30,6 +70,18 @@ ['name' => 'registers#patch', 'url' => '/api/registers/{id}', 'verb' => 'PATCH', 'requirements' => ['id' => '[^/]+']], ['name' => 'schemas#patch', 'url' => '/api/schemas/{id}', 'verb' => 'PATCH', 'requirements' => ['id' => '[^/]+']], ['name' => 'sources#patch', 'url' => '/api/sources/{id}', 'verb' => 'PATCH', 'requirements' => ['id' => '[^/]+']], + + // Curated MDI glyph as an SVG image (used to render a schema's icon in unified search). + ['name' => 'icon#mdi', 'url' => '/api/icon/mdi/{name}', 'verb' => 'GET', 'requirements' => ['name' => '[A-Za-z0-9-]+']], + + // Data sync / harvesting — manual trigger + status (data-sync-harvesting spec). + ['name' => 'sources#syncNow', 'url' => '/api/sources/{id}/sync', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'sources#syncStatus', 'url' => '/api/sources/{id}/sync-status', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], + + // Virtual registers over DBAL — connection test + introspection (dbal-virtual-registers spec). + ['name' => 'sources#testConnection', 'url' => '/api/sources/{id}/test-connection', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'sources#introspect', 'url' => '/api/sources/{id}/introspect', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'configurations#patch', 'url' => '/api/configurations/{id}', 'verb' => 'PATCH', 'requirements' => ['id' => '[^/]+']], ['name' => 'applications#patch', 'url' => '/api/applications/{id}', 'verb' => 'PATCH', 'requirements' => ['id' => '[^/]+']], ['name' => 'agents#patch', 'url' => '/api/agents/{id}', 'verb' => 'PATCH', 'requirements' => ['id' => '[^/]+']], @@ -58,66 +110,11 @@ // Migration - Move objects between blob storage and magic tables. ['name' => 'migration#status', 'url' => '/api/migration/status/{register}/{schema}', 'verb' => 'GET', 'requirements' => ['register' => '[^/]+', 'schema' => '[^/]+']], ['name' => 'migration#migrate', 'url' => '/api/migration/migrate', 'verb' => 'POST'], - + // Settings - Focused endpoints for better performance. ['name' => 'settings#getSearchBackend', 'url' => '/api/settings/search-backend', 'verb' => 'GET'], ['name' => 'settings#updateSearchBackend', 'url' => '/api/settings/search-backend', 'verb' => 'PUT'], ['name' => 'settings#updateSearchBackend', 'url' => '/api/settings/search-backend', 'verb' => 'PATCH'], - ['name' => 'Settings\SolrSettings#getSolrSettings', 'url' => '/api/settings/solr', 'verb' => 'GET'], - ['name' => 'Settings\SolrSettings#updateSolrSettings', 'url' => '/api/settings/solr', 'verb' => 'PATCH'], - ['name' => 'Settings\SolrSettings#updateSolrSettings', 'url' => '/api/settings/solr', 'verb' => 'PUT'], - ['name' => 'Settings\SolrOperations#testSolrConnection', 'url' => '/api/settings/solr/test', 'verb' => 'POST'], - ['name' => 'Settings\SolrOperations#warmupSolrIndex', 'url' => '/api/settings/solr/warmup', 'verb' => 'POST'], - ['name' => 'Settings\SolrOperations#getSolrMemoryPrediction', 'url' => '/api/settings/solr/memory-prediction', 'verb' => 'POST'], - ['name' => 'Settings\SolrOperations#testSchemaMapping', 'url' => '/api/settings/solr/test-schema-mapping', 'verb' => 'POST'], - ['name' => 'Settings\SolrSettings#getSolrFacetConfiguration', 'url' => '/api/settings/solr-facet-config', 'verb' => 'GET'], - ['name' => 'Settings\SolrSettings#updateSolrFacetConfiguration', 'url' => '/api/settings/solr-facet-config', 'verb' => 'POST'], - ['name' => 'Settings\SolrSettings#discoverSolrFacets', 'url' => '/api/solr/discover-facets', 'verb' => 'GET'], - ['name' => 'Settings\SolrSettings#getSolrFacetConfigWithDiscovery', 'url' => '/api/solr/facet-config', 'verb' => 'GET'], - ['name' => 'Settings\SolrSettings#updateSolrFacetConfigWithDiscovery', 'url' => '/api/solr/facet-config', 'verb' => 'POST'], - ['name' => 'Settings\SolrManagement#getSolrFields', 'url' => '/api/solr/fields', 'verb' => 'GET'], - ['name' => 'Settings\SolrManagement#createMissingSolrFields', 'url' => '/api/solr/fields/create-missing', 'verb' => 'POST'], - ['name' => 'Settings\SolrManagement#fixMismatchedSolrFields', 'url' => '/api/solr/fields/fix-mismatches', 'verb' => 'POST'], - ['name' => 'Settings\SolrManagement#deleteSolrField', 'url' => '/api/solr/fields/{fieldName}', 'verb' => 'DELETE', 'requirements' => ['fieldName' => '[^/]+']], - - // Collection-specific field management. - ['name' => 'Settings\ConfigurationSettings#getObjectCollectionFields', 'url' => '/api/solr/collections/objects/fields', 'verb' => 'GET'], - ['name' => 'Settings\FileSettings#getFileCollectionFields', 'url' => '/api/solr/collections/files/fields', 'verb' => 'GET'], - ['name' => 'Settings\ConfigurationSettings#createMissingObjectFields', 'url' => '/api/solr/collections/objects/fields/create-missing', 'verb' => 'POST'], - ['name' => 'Settings\FileSettings#createMissingFileFields', 'url' => '/api/solr/collections/files/fields/create-missing', 'verb' => 'POST'], - - // SOLR Dashboard Management endpoints. - ['name' => 'Settings\SolrSettings#getSolrDashboardStats', 'url' => '/api/solr/dashboard/stats', 'verb' => 'GET'], - ['name' => 'Settings\SolrOperations#inspectSolrIndex', 'url' => '/api/settings/solr/inspect', 'verb' => 'POST'], - ['name' => 'Settings\SolrOperations#manageSolr', 'url' => '/api/solr/manage/{operation}', 'verb' => 'POST'], - ['name' => 'Settings\SolrOperations#setupSolr', 'url' => '/api/solr/setup', 'verb' => 'POST'], - ['name' => 'Settings\SolrOperations#testSetupHandler', 'url' => '/api/solr/test-setup', 'verb' => 'POST'], - - // Collection-specific operations (with collection name parameter). - ['name' => 'Settings\SolrManagement#deleteSpecificSolrCollection', 'url' => '/api/solr/collections/{name}', 'verb' => 'DELETE', 'requirements' => ['name' => '[^/]+']], - ['name' => 'Settings\SolrManagement#clearSpecificCollection', 'url' => '/api/solr/collections/{name}/clear', 'verb' => 'POST', 'requirements' => ['name' => '[^/]+']], - ['name' => 'Settings\SolrManagement#reindexSpecificCollection', 'url' => '/api/solr/collections/{name}/reindex', 'verb' => 'POST', 'requirements' => ['name' => '[^/]+']], - - // SOLR Collection and ConfigSet Management endpoints (SolrController). - ['name' => 'solr#listCollections', 'url' => '/api/solr/collections', 'verb' => 'GET'], - ['name' => 'solr#createCollection', 'url' => '/api/solr/collections', 'verb' => 'POST'], - ['name' => 'solr#listConfigSets', 'url' => '/api/solr/configsets', 'verb' => 'GET'], - ['name' => 'solr#createConfigSet', 'url' => '/api/solr/configsets', 'verb' => 'POST'], - ['name' => 'solr#deleteConfigSet', 'url' => '/api/solr/configsets/{name}', 'verb' => 'DELETE'], - ['name' => 'solr#copyCollection', 'url' => '/api/solr/collections/copy', 'verb' => 'POST'], - ['name' => 'Settings\SolrManagement#updateSolrCollectionAssignments', 'url' => '/api/solr/collections/assignments', 'verb' => 'PUT'], - - // Vector Search endpoints (Semantic and Hybrid Search) - SolrController. - ['name' => 'solr#semanticSearch', 'url' => '/api/search/semantic', 'verb' => 'POST'], - ['name' => 'solr#hybridSearch', 'url' => '/api/search/hybrid', 'verb' => 'POST'], - ['name' => 'solr#getVectorStats', 'url' => '/api/vectors/stats', 'verb' => 'GET'], - ['name' => 'solr#testVectorEmbedding', 'url' => '/api/vectors/test', 'verb' => 'POST'], - - // Object Vectorization endpoints - SolrController. - ['name' => 'solr#vectorizeObject', 'url' => '/api/objects/{objectId}/vectorize', 'verb' => 'POST'], - ['name' => 'solr#bulkVectorizeObjects', 'url' => '/api/objects/vectorize/bulk', 'verb' => 'POST'], - ['name' => 'solr#getVectorizationStats', 'url' => '/api/solr/vectorize/stats', 'verb' => 'GET'], - // Magic Table Sync endpoints. ['name' => 'tables#sync', 'url' => '/api/tables/sync/{registerId}/{schemaId}', 'verb' => 'POST', 'requirements' => ['registerId' => '[^/]+', 'schemaId' => '[^/]+']], ['name' => 'tables#syncAll', 'url' => '/api/tables/sync', 'verb' => 'POST'], @@ -125,19 +122,18 @@ ['name' => 'Settings\ConfigurationSettings#getRbacSettings', 'url' => '/api/settings/rbac', 'verb' => 'GET'], ['name' => 'Settings\ConfigurationSettings#updateRbacSettings', 'url' => '/api/settings/rbac', 'verb' => 'PATCH'], ['name' => 'Settings\ConfigurationSettings#updateRbacSettings', 'url' => '/api/settings/rbac', 'verb' => 'PUT'], - + ['name' => 'Settings\ConfigurationSettings#getMultitenancySettings', 'url' => '/api/settings/multitenancy', 'verb' => 'GET'], ['name' => 'Settings\ConfigurationSettings#updateMultitenancySettings', 'url' => '/api/settings/multitenancy', 'verb' => 'PATCH'], ['name' => 'Settings\ConfigurationSettings#updateMultitenancySettings', 'url' => '/api/settings/multitenancy', 'verb' => 'PUT'], - + ['name' => 'Settings\ConfigurationSettings#getOrganisationSettings', 'url' => '/api/settings/organisation', 'verb' => 'GET'], ['name' => 'Settings\ConfigurationSettings#updateOrganisationSettings', 'url' => '/api/settings/organisation', 'verb' => 'PATCH'], ['name' => 'Settings\ConfigurationSettings#updateOrganisationSettings', 'url' => '/api/settings/organisation', 'verb' => 'PUT'], - + ['name' => 'Settings\LlmSettings#getLLMSettings', 'url' => '/api/settings/llm', 'verb' => 'GET'], ['name' => 'settings#getDatabaseInfo', 'url' => '/api/settings/database', 'verb' => 'GET'], ['name' => 'settings#refreshDatabaseInfo', 'url' => '/api/settings/database/refresh', 'verb' => 'POST'], - ['name' => 'Settings\SolrSettings#getSolrInfo', 'url' => '/api/settings/solr-info', 'verb' => 'GET'], ['name' => 'Settings\LlmSettings#updateLLMSettings', 'url' => '/api/settings/llm', 'verb' => 'POST'], ['name' => 'Settings\LlmSettings#patchLLMSettings', 'url' => '/api/settings/llm', 'verb' => 'PATCH'], ['name' => 'Settings\LlmSettings#updateLLMSettings', 'url' => '/api/settings/llm', 'verb' => 'PUT'], @@ -153,20 +149,28 @@ ['name' => 'Settings\FileSettings#testDolphinConnection', 'url' => '/api/settings/files/test-dolphin', 'verb' => 'POST'], ['name' => 'Settings\FileSettings#testPresidioConnection', 'url' => '/api/settings/files/test-presidio', 'verb' => 'POST'], ['name' => 'Settings\FileSettings#testOpenAnonymiserConnection', 'url' => '/api/settings/files/test-openanonymiser', 'verb' => 'POST'], + + // Anonymisation backend selection (admin-only). + ['name' => 'anonymisationBackend#getBackendState', 'url' => '/api/admin/anonymisation/backend-state', 'verb' => 'GET'], + ['name' => 'anonymisationBackend#testConnection', 'url' => '/api/admin/anonymisation/test-connection', 'verb' => 'POST'], + ['name' => 'Settings\ConfigurationSettings#getObjectSettings', 'url' => '/api/settings/objects/vectorize', 'verb' => 'GET'], ['name' => 'Settings\ConfigurationSettings#getObjectSettings', 'url' => '/api/settings/objects', 'verb' => 'GET'], ['name' => 'Settings\ConfigurationSettings#updateObjectSettings', 'url' => '/api/settings/objects/vectorize', 'verb' => 'POST'], ['name' => 'Settings\ConfigurationSettings#patchObjectSettings', 'url' => '/api/settings/objects/vectorize', 'verb' => 'PATCH'], ['name' => 'Settings\ConfigurationSettings#updateObjectSettings', 'url' => '/api/settings/objects/vectorize', 'verb' => 'PUT'], - + // Object vectorization endpoints. ['name' => 'objects#vectorizeBatch', 'url' => '/api/objects/vectorize/batch', 'verb' => 'POST'], ['name' => 'objects#getObjectVectorizationCount', 'url' => '/api/objects/vectorize/count', 'verb' => 'GET'], ['name' => 'objects#getObjectVectorizationStats', 'url' => '/api/objects/vectorize/stats', 'verb' => 'GET'], - + // Object validation endpoint. ['name' => 'objects#validate', 'url' => '/api/objects/validate', 'verb' => 'POST'], - + + // Batched object-count endpoint (one round-trip for many register/schema pairs). + ['name' => 'objects#counts', 'url' => '/api/objects/counts', 'verb' => 'POST'], + // Core file extraction endpoints (use fileExtraction controller to avoid conflict with files controller). // NOTE: Specific routes MUST come before parameterized routes like {id} ['name' => 'fileExtraction#index', 'url' => '/api/files', 'verb' => 'GET'], @@ -179,30 +183,26 @@ ['name' => 'fileExtraction#cleanup', 'url' => '/api/files/cleanup', 'verb' => 'POST'], ['name' => 'fileExtraction#show', 'url' => '/api/files/{id}', 'verb' => 'GET'], ['name' => 'fileExtraction#extract', 'url' => '/api/files/{id}/extract', 'verb' => 'POST'], - + ['name' => 'Settings\ConfigurationSettings#getRetentionSettings', 'url' => '/api/settings/retention', 'verb' => 'GET'], - + // Settings — additional endpoints. ['name' => 'settings#load', 'url' => '/api/settings/load', 'verb' => 'GET'], - ['name' => 'settings#updatePublishingOptions', 'url' => '/api/settings/publishing-options', 'verb' => 'PUT'], - ['name' => 'settings#testSetupHandler', 'url' => '/api/settings/test-setup', 'verb' => 'POST'], - ['name' => 'settings#reindexSpecificCollection','url' => '/api/settings/reindex/{name}', 'verb' => 'POST', 'requirements' => ['name' => '[^/]+']], - ['name' => 'settings#testSchemaMapping', 'url' => '/api/settings/test-schema-mapping', 'verb' => 'POST'], ['name' => 'settings#semanticSearch', 'url' => '/api/settings/search/semantic', 'verb' => 'GET'], ['name' => 'settings#hybridSearch', 'url' => '/api/settings/search/hybrid', 'verb' => 'GET'], // Debug endpoints for type filtering issue. ['name' => 'settings#debugTypeFiltering', 'url' => '/api/debug/type-filtering', 'verb' => 'GET'], ['name' => 'Settings\ConfigurationSettings#updateRetentionSettings', 'url' => '/api/settings/retention', 'verb' => 'PATCH'], ['name' => 'Settings\ConfigurationSettings#updateRetentionSettings', 'url' => '/api/settings/retention', 'verb' => 'PUT'], - + ['name' => 'settings#getVersionInfo', 'url' => '/api/settings/version', 'verb' => 'GET'], - + // API Tokens for GitHub and GitLab. ['name' => 'Settings\ApiTokenSettings#getApiTokens', 'url' => '/api/settings/api-tokens', 'verb' => 'GET'], ['name' => 'Settings\ApiTokenSettings#saveApiTokens', 'url' => '/api/settings/api-tokens', 'verb' => 'POST'], ['name' => 'Settings\ApiTokenSettings#testGitHubToken', 'url' => '/api/settings/api-tokens/test/github', 'verb' => 'POST'], ['name' => 'Settings\ApiTokenSettings#testGitLabToken', 'url' => '/api/settings/api-tokens/test/gitlab', 'verb' => 'POST'], - + // n8n workflow integration. ['name' => 'Settings\N8nSettings#getN8nSettings', 'url' => '/api/settings/n8n', 'verb' => 'GET'], ['name' => 'Settings\N8nSettings#updateN8nSettings', 'url' => '/api/settings/n8n', 'verb' => 'POST'], @@ -211,10 +211,10 @@ ['name' => 'Settings\N8nSettings#testN8nConnection', 'url' => '/api/settings/n8n/test', 'verb' => 'POST'], ['name' => 'Settings\N8nSettings#initializeN8n', 'url' => '/api/settings/n8n/initialize', 'verb' => 'POST'], ['name' => 'Settings\N8nSettings#getWorkflows', 'url' => '/api/settings/n8n/workflows', 'verb' => 'GET'], - - // Statistics endpoint. + + // Statistics endpoint. ['name' => 'settings#getStatistics', 'url' => '/api/settings/statistics', 'verb' => 'GET'], - + // Cache management. ['name' => 'Settings\CacheSettings#getCacheStats', 'url' => '/api/settings/cache', 'verb' => 'GET'], ['name' => 'Settings\CacheSettings#clearCache', 'url' => '/api/settings/cache', 'verb' => 'DELETE'], @@ -234,14 +234,26 @@ ['name' => 'manifest#index', 'url' => '/api/manifest/{appId}', 'verb' => 'GET', 'requirements' => ['appId' => '[^/]+']], // Heartbeat - Keep-alive endpoint for long-running operations. ['name' => 'heartbeat#heartbeat', 'url' => '/api/heartbeat', 'verb' => 'GET'], - // Prometheus metrics endpoint. - ['name' => 'metrics#index', 'url' => '/api/metrics', 'verb' => 'GET'], - // Health check endpoint. - ['name' => 'health#index', 'url' => '/api/health', 'verb' => 'GET'], + // Prometheus metrics endpoint — served by OpenRegister's own AppHost + // declarative observability engine (ADR-040). OR dogfoods the engine: + // the canonical /api/metrics URL is aliased at GenericMetricsController, + // which reads the `observability.metrics` block of src/manifest.json. + // URL + output contract are unchanged from the deleted MetricsController + // (parity-verified); $appName resolves to "openregister". + ['name' => 'AppHost\Controller\GenericMetrics#index', 'url' => '/api/metrics', 'verb' => 'GET'], + // Health check endpoint — served by the AppHost engine from the + // `observability.health` block. The engine adds #[PublicPage] (ADR-006 + // anon health, an intentional improvement over the login-gated bespoke + // controller) and the standard {status, app, version, checks} shape. + ['name' => 'AppHost\Controller\GenericHealth#index', 'url' => '/api/health', 'verb' => 'GET'], // URN resolution endpoints (RFC 8141 system-independent identifiers). ['name' => 'urn#resolve', 'url' => '/api/urn/resolve', 'verb' => 'GET'], ['name' => 'urn#lookup', 'url' => '/api/urn/lookup', 'verb' => 'GET'], ['name' => 'urn#bulk', 'url' => '/api/urn/bulk', 'verb' => 'POST'], + // JSON-LD context document endpoints (json-ld-output). Dereferenceable + // @context documents referenced by JSON-LD object serializations. + ['name' => 'contexts#register', 'url' => '/api/contexts/{register}', 'verb' => 'GET'], + ['name' => 'contexts#schema', 'url' => '/api/contexts/{register}/{schema}', 'verb' => 'GET'], // RBAC scope discovery endpoint — clients query effective (register, // schema, action) scopes for the authenticated user without probing // every endpoint individually. @@ -259,9 +271,33 @@ ['name' => 'dsar#vergetelheid', 'url' => '/api/avg/vergetelheid', 'verb' => 'POST'], ['name' => 'dsar#rectificatie', 'url' => '/api/avg/rectificatie', 'verb' => 'POST'], ['name' => 'dsar#compliance', 'url' => '/api/avg/compliance', 'verb' => 'GET'], - // Realtime cursor-based polling endpoints. - ['name' => 'realtime#events', 'url' => '/api/realtime/events', 'verb' => 'GET'], - ['name' => 'realtime#cursor', 'url' => '/api/realtime/cursor', 'verb' => 'GET'], + // Generic, RBAC + tenant scoped GDPR data-subject-rights endpoints + // (consumable by leaf apps; NOT admin-only, distinct from dsar#*). + ['name' => 'dataSubjectRequest#subjectData', 'url' => '/api/gdpr/subject-data', 'verb' => 'GET'], + ['name' => 'dataSubjectRequest#accessExport', 'url' => '/api/gdpr/access-export', 'verb' => 'GET'], + ['name' => 'dataSubjectRequest#rectify', 'url' => '/api/gdpr/rectify', 'verb' => 'POST'], + ['name' => 'dataSubjectRequest#erase', 'url' => '/api/gdpr/erase', 'verb' => 'POST'], + ['name' => 'dataSubjectRequest#restrict', 'url' => '/api/gdpr/restrict', 'verb' => 'POST'], + ['name' => 'dataSubjectRequest#objection', 'url' => '/api/gdpr/object', 'verb' => 'POST'], + // DSAR case-management engine (dsar-case-engine): stateful case workflow. + // All @NoAdminRequired (never @PublicPage); @NoCSRFRequired only on the + // one-time download (browser navigation). Case-level access control + // (handler-scopes-own + officer-override, fail-closed) enforced in-body. + ['name' => 'dsarCase#create', 'url' => '/api/gdpr/cases', 'verb' => 'POST'], + ['name' => 'dsarCase#transition', 'url' => '/api/gdpr/cases/{id}/transition', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'dsarCase#evidence', 'url' => '/api/gdpr/cases/{id}/evidence', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'dsarCase#redact', 'url' => '/api/gdpr/cases/{id}/redactions', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'dsarCase#generateBundle', 'url' => '/api/gdpr/cases/{id}/bundle', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'dsarCase#downloadBundle', 'url' => '/api/gdpr/cases/{id}/bundle/download', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], + ['name' => 'dsarCase#dossier', 'url' => '/api/gdpr/cases/{id}/dossier', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], + // DSAR integration seams (dsar-integration-seams): pack-selector-driven, + // fail-closed identity-verify + regulator-escalate call-outs. + ['name' => 'dsarCase#identityVerify', 'url' => '/api/gdpr/cases/{id}/verify-identity', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + ['name' => 'dsarCase#escalate', 'url' => '/api/gdpr/cases/{id}/escalate', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + // AVG / GDPR per-access processing log (verwerkingenlogging) — read-only, + // admin-default + FG-delegated, append-only by surface (no write routes). + ['name' => 'processingLog#index', 'url' => '/api/avg/verwerkingen', 'verb' => 'GET'], + ['name' => 'processingLog#betrokkene', 'url' => '/api/avg/verwerkingen/betrokkene', 'verb' => 'GET'], // Translation sidecar — search, per-object slots + completeness, status updates. ['name' => 'translation#search', 'url' => '/api/translations/search', 'verb' => 'GET'], ['name' => 'translation#showByObject', 'url' => '/api/translations/object/{uuid}', 'verb' => 'GET'], @@ -288,19 +324,20 @@ // Linked entities (mail sidebar, contacts sidebar, etc.). // Must be before objects/{register}/{schema} routes to avoid wildcard matching. ['name' => 'linked_entity#addObjectLink', 'url' => '/api/objects/{uuid}/_linked/{type}', 'verb' => 'POST', 'requirements' => ['uuid' => '[^/]+', 'type' => '[^/]+']], - ['name' => 'linked_entity#removeObjectLink', 'url' => '/api/objects/{uuid}/_linked/{type}/{entityId}', 'verb' => 'DELETE', 'requirements' => ['uuid' => '[^/]+', 'type' => '[^/]+', 'entityId' => '[^/]+']], + ['name' => 'linked_entity#removeObjectLink', 'url' => '/api/objects/{uuid}/_linked/{type}/{entityId}', 'verb' => 'DELETE', 'requirements' => ['uuid' => '[^/]+', 'type' => '[^/]+', 'entityId' => '.+']], ['name' => 'linked_entity#addRegisterLink', 'url' => '/api/registers/{uuid}/_linked/{type}', 'verb' => 'POST', 'requirements' => ['uuid' => '[^/]+', 'type' => '[^/]+']], ['name' => 'linked_entity#addSchemaLink', 'url' => '/api/schemas/{uuid}/_linked/{type}', 'verb' => 'POST', 'requirements' => ['uuid' => '[^/]+', 'type' => '[^/]+']], - // Note: entityId uses [^/]+ (not .+) to prevent slashes in captured values. Entity IDs are - // opaque (UUIDs / external IDs) and should never contain path separators. The legacy underscore- + // Note: entityId uses .+ (not [^/]+) because mail entity refs are `{accountId}/{messageId}` + // with a RAW slash — the sidebar deliberately does not URL-encode it (Apache rejects %2F in + // paths unless AllowEncodedSlashes is on; see commit d8acb45f0). The legacy underscore- // prefixed variant below (linkedEntity#reverseLookup on /api/linked/_{type}/{entityId}) coexists // for backwards compatibility with older mail-sidebar clients; deduplicate in a future cleanup. - ['name' => 'linked_entity#reverseLookup', 'url' => '/api/linked/{type}/{entityId}', 'verb' => 'GET', 'requirements' => ['type' => '[^/]+', 'entityId' => '[^/]+']], + ['name' => 'linked_entity#reverseLookup', 'url' => '/api/linked/{type}/{entityId}', 'verb' => 'GET', 'requirements' => ['type' => '[^/]+', 'entityId' => '.+']], // Objects. ['name' => 'objects#objects', 'url' => '/api/objects', 'verb' => 'GET'], - ['name' => 'objects#clearBlob', 'url' => '/api/objects/clear-blob', 'verb' => 'DELETE'], - // ['name' => 'objects#import', 'url' => '/api/objects/{register}/import', 'verb' => 'POST'], // DISABLED: Use registers import endpoint instead + // SEC-CTRL-10: the clearBlob route was removed — blob storage retired; the controller method was a no-op. + // The objects import route was also removed — use the registers import endpoint instead. // Lifecycle transitions — MUST precede the wildcard {register}/{schema} routes // so /api/objects/{id}/transition isn't grabbed as register=id, schema=transition. ['name' => 'transition#transition', 'url' => '/api/objects/{id}/transition', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], @@ -309,9 +346,32 @@ // Aggregations — ad-hoc time-bucket primitive (must be ordered // BEFORE the {name} wildcard so /timeseries literal matches first). ['name' => 'aggregation#timeseries', 'url' => '/api/objects/aggregations/{register}/{schema}/timeseries', 'verb' => 'GET'], + // Aggregations — ad-hoc single-value + categorical group-by primitives (literal before {name}). + ['name' => 'aggregation#value', 'url' => '/api/objects/aggregations/{register}/{schema}/value', 'verb' => 'GET'], + ['name' => 'aggregation#grouped', 'url' => '/api/objects/aggregations/{register}/{schema}/grouped', 'verb' => 'GET'], // Aggregations sugar endpoint — named annotation surface. ['name' => 'aggregation#aggregate', 'url' => '/api/objects/aggregations/{register}/{schema}/{name}', 'verb' => 'GET'], + // MDM read-only surface — quality statistics + lowest-quality listing + // (must be ordered BEFORE the bare {register}/{schema} listing so the + // literal /stats segment matches first). + ['name' => 'quality#stats', 'url' => '/api/objects/quality/{register}/{schema}/stats', 'verb' => 'GET'], + ['name' => 'quality#index', 'url' => '/api/objects/quality/{register}/{schema}', 'verb' => 'GET'], + // MDM read-only surface — duplicate-candidate listing. + ['name' => 'duplicate#index', 'url' => '/api/objects/duplicates/{register}/{schema}', 'verb' => 'GET'], + // MDM reversible merge surface (ADR-045 follow-on #B) — preview / execute / reverse. + ['name' => 'merge#preview', 'url' => '/api/objects/merge/preview', 'verb' => 'POST'], + ['name' => 'merge#execute', 'url' => '/api/objects/merge/execute', 'verb' => 'POST'], + ['name' => 'merge#reverse', 'url' => '/api/objects/merge/{id}/reverse', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + + // MDM per-object attribute-override primitive (ADR-045 follow-on #E) — + // sets/clears one attribute override on a master object and recomputes + // its golden record. + ['name' => 'survivorship#override', 'url' => '/api/objects/survivorship/{id}/override', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + // Resolve a master's competing source records (embedded or reverse-FK) + // for the conflict-resolution UI. + ['name' => 'survivorship#sources', 'url' => '/api/objects/survivorship/{id}/sources', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], + // Contacts matching API — used by ContactsMenuProvider + mail-sidebar. ['name' => 'contacts#match', 'url' => '/api/contacts/match', 'verb' => 'GET'], @@ -363,6 +423,9 @@ ['name' => 'deckLinks#destroy', 'url' => '/api/objects/{register}/{schema}/{id}/deck/{cardId}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+', 'cardId' => '[0-9]+']], ['name' => 'deckLinks#boards', 'url' => '/api/integrations/deck/boards', 'verb' => 'GET'], ['name' => 'deckLinks#stacks', 'url' => '/api/integrations/deck/boards/{boardId}/stacks', 'verb' => 'GET', 'requirements' => ['boardId' => '[0-9]+']], + // Schema-level sticky default board+stack (per-schema config, not object data). + ['name' => 'deckLinks#getDefault', 'url' => '/api/integrations/deck/default/{schema}', 'verb' => 'GET'], + ['name' => 'deckLinks#setDefault', 'url' => '/api/integrations/deck/default/{schema}', 'verb' => 'PUT'], // Tier-1 legacy endpoints (superseded by deckLinks; kept for back-compat). ['name' => 'deck#index', 'url' => '/api/objects/{register}/{schema}/{id}/deck/cards', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'deck#create', 'url' => '/api/objects/{register}/{schema}/{id}/deck/cards', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], @@ -448,10 +511,40 @@ // pageRef is a url-encoded canonical page reference (`%2F` not `/`), // so the `[^/]+` requirement matches the whole segment. ['name' => 'xwikiLinks#available', 'url' => '/api/integrations/xwiki/available', 'verb' => 'GET'], + ['name' => 'xwikiLinks#search', 'url' => '/api/integrations/xwiki/search', 'verb' => 'GET'], ['name' => 'xwikiLinks#index', 'url' => '/api/objects/{register}/{schema}/{id}/xwiki', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'xwikiLinks#createAndLink','url' => '/api/objects/{register}/{schema}/{id}/xwiki/new', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], ['name' => 'xwikiLinks#link', 'url' => '/api/objects/{register}/{schema}/{id}/xwiki', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], ['name' => 'xwikiLinks#destroy', 'url' => '/api/objects/{register}/{schema}/{id}/xwiki/{pageRef}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+', 'pageRef' => '[^/]+']], + + // KvK + OpenCorporates company lookup (external, OpenConnector-routed) + // — read-only, object-independent company-lookup leaves. No NC app + // gate; the OpenConnector `kvk` / `opencorporates` sources carry the + // base URL + API key. Unconfigured/down → 503 with details.cause. + // @spec openspec/changes/integration-kvk-opencorporates/specs/integration-company-lookup/spec.md. + ['name' => 'companyLookup#kvkCompany', 'url' => '/api/integrations/kvk/company', 'verb' => 'GET'], + ['name' => 'companyLookup#kvkSearch', 'url' => '/api/integrations/kvk/search', 'verb' => 'GET'], + ['name' => 'companyLookup#openCorporatesSearch', 'url' => '/api/integrations/opencorporates/search', 'verb' => 'GET'], + // BRP HaalCentraal person lookup (external, OpenConnector-routed) — + // read-only, object-independent person-lookup leaf. No NC app gate; the + // OpenConnector `brp-haalcentraal` source carries the base URL + OAuth2 + // client_credentials secret + PKIoverheid mutual-TLS client certificate + // (both applied natively by CallService). Unconfigured/down → 503 with + // details.cause. The BSN travels in the request body only, never logged. + // @spec openspec/changes/integration-brp-haalcentraal/specs/integration-person-lookup/spec.md. + ['name' => 'personLookup#brpPerson', 'url' => '/api/integrations/brp/person', 'verb' => 'GET'], + // Outbound-messaging dispatch (external, OpenConnector-routed) — + // side-effecting send leaf. No NC app gate; the OpenConnector + // cmcom-sms / messagebird-sms / twilio-sms (SMS) and + // whatsapp-cloud-api / whatsapp-bsp (WhatsApp) sources carry the base + // URL + provider credential. The consuming app (pipelinq) composes the + // vendor-shaped body + path and owns all orchestration (provider + // selection, STOP opt-out, template-approval, 24h session, dedupe, + // delivery-status); this leaf only POSTs the message. Unconfigured/down + // → 503 with details.cause. + // @spec openspec/changes/messaging-dispatch-leaf/specs/integration-message-dispatch/spec.md. + ['name' => 'messageDispatch#smsSend', 'url' => '/api/integrations/sms/send', 'verb' => 'POST'], + ['name' => 'messageDispatch#whatsappSend', 'url' => '/api/integrations/whatsapp/send', 'verb' => 'POST'], // Cospend (NC Costs) — Tier-2 link-table API. User-scoped (no // admin gate). The specific `/cospend/new` (create + link) route // MUST precede the wildcard `/cospend/{entryId}` unlink route, and @@ -510,6 +603,29 @@ ['name' => 'analyticsLinks#link', 'url' => '/api/objects/{register}/{schema}/{id}/analytics', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], ['name' => 'analyticsLinks#destroy', 'url' => '/api/objects/{register}/{schema}/{id}/analytics/{reportId}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+', 'reportId' => '[0-9]+']], + // Analytics page-level series — leaf-foundation render surface. + // A leaf (procest SLA dashboard) registers a pre-computed series + // (labels + datasets); the render layer fetches it as a chart + // widget. RBAC-scoped inside AnalyticsSeriesService. + // @spec openspec/changes/integration-leaf-foundation-shares-analytics/specs/integration-leaf-foundation/spec.md. + ['name' => 'analyticsSeries#register', 'url' => '/api/integrations/analytics/series', 'verb' => 'POST'], + ['name' => 'analyticsSeries#fetch', 'url' => '/api/integrations/analytics/series/{seriesKey}', 'verb' => 'GET', 'requirements' => ['seriesKey' => '[^/]+']], + + // Maps page-level overview — multi-object "cases on map" render + // surface (procest issue #112). register declares a `map` page + // widget; points queries the RBAC-scoped marker set for a + // register/schema. RBAC enforced inside MapsOverviewService via the + // canonical OR read path (_rbac:true for non-admins, fail-closed). + // @spec openspec/changes/integration-maps-overview-page-surface/specs/integration-maps-overview/spec.md. + ['name' => 'mapsOverview#register', 'url' => '/api/integrations/maps/overviews', 'verb' => 'POST'], + ['name' => 'mapsOverview#points', 'url' => '/api/integrations/maps/overviews/{register}/{schema}/points', 'verb' => 'GET', 'requirements' => ['register' => '[^/]+', 'schema' => '[^/]+']], + + // Public "track your case" token resolve — anonymous, RBAC-scoped + // public-safe object view minted via the Shares integration + // provider. Fails closed (404) on unknown/revoked/expired tokens. + // @spec openspec/changes/integration-leaf-foundation-shares-analytics/specs/integration-leaf-foundation/spec.md. + ['name' => 'caseToken#resolve', 'url' => '/api/public/case-tokens/{token}', 'verb' => 'GET', 'requirements' => ['token' => '[^/]+']], + // Activity — Tier-2 read-only API. NC Activity entries are // core-generated (no link/create/delete verbs); this surface // only filters + cursor-paginates the entries linked to an OR @@ -571,10 +687,10 @@ // Linked-entity-types — generic per-{type} link API (mail / event / contact / deck). ['name' => 'linkedEntity#addObjectLink', 'url' => '/api/objects/{uuid}/_{type}', 'verb' => 'POST', 'requirements' => ['uuid' => '[^/]+', 'type' => '[a-z]+']], - ['name' => 'linkedEntity#removeObjectLink', 'url' => '/api/objects/{uuid}/_{type}/{entityId}','verb' => 'DELETE', 'requirements' => ['uuid' => '[^/]+', 'type' => '[a-z]+', 'entityId' => '[^/]+']], + ['name' => 'linkedEntity#removeObjectLink', 'url' => '/api/objects/{uuid}/_{type}/{entityId}','verb' => 'DELETE', 'requirements' => ['uuid' => '[^/]+', 'type' => '[a-z]+', 'entityId' => '.+']], ['name' => 'linkedEntity#addRegisterLink', 'url' => '/api/registers/{uuid}/_{type}', 'verb' => 'POST', 'requirements' => ['uuid' => '[^/]+', 'type' => '[a-z]+']], ['name' => 'linkedEntity#addSchemaLink', 'url' => '/api/schemas/{uuid}/_{type}', 'verb' => 'POST', 'requirements' => ['uuid' => '[^/]+', 'type' => '[a-z]+']], - ['name' => 'linkedEntity#reverseLookup', 'url' => '/api/linked/_{type}/{entityId}', 'verb' => 'GET', 'requirements' => ['type' => '[a-z]+', 'entityId' => '[^/]+']], + ['name' => 'linkedEntity#reverseLookup', 'url' => '/api/linked/_{type}/{entityId}', 'verb' => 'GET', 'requirements' => ['type' => '[a-z]+', 'entityId' => '.+']], // TMLO metadata export endpoints (declarative archival metadata per Dutch TMLO standard). ['name' => 'tmlo#summary', 'url' => '/api/tmlo/{register}/{schema}/summary', 'verb' => 'GET'], @@ -599,6 +715,9 @@ ['name' => 'objects#index', 'url' => '/api/objects/{register}/{schema}', 'verb' => 'GET'], ['name' => 'objects#geoSearch', 'url' => '/api/objects/{register}/{schema}/geo-search', 'verb' => 'POST'], + ['name' => 'objects#geoJson', 'url' => '/api/geo/{register}/{schema}/geojson', 'verb' => 'GET'], + ['name' => 'objects#wfs', 'url' => '/api/geo/{register}/{schema}/wfs', 'verb' => 'GET'], + ['name' => 'objects#geocode', 'url' => '/api/geo/geocode', 'verb' => 'GET'], ['name' => 'objects#create', 'url' => '/api/objects/{register}/{schema}', 'verb' => 'POST'], ['name' => 'objects#export', 'url' => '/api/objects/{register}/{schema}/export', 'verb' => 'GET'], @@ -637,6 +756,15 @@ ['name' => 'auditTrail#update', 'url' => '/api/audit-trails/{id}', 'verb' => 'PUT', 'requirements' => ['id' => '[^/]+']], ['name' => 'auditTrail#destroy', 'url' => '/api/audit-trails/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+']], ['name' => 'auditTrail#destroyMultiple', 'url' => '/api/audit-trails', 'verb' => 'DELETE'], + // Audit Query (v2) — unified, cross-app query/export of audit-entry + // objects (e.g. procest's aiAuditEntry, parafering's paraferingAuditEntry). + // Distinct from Audit Trails above (OR's own object-mutation log); + // this queries app-defined audit-entry OBJECTS via ObjectService. + // /export MUST come before the (paramless) query route is irrelevant + // here since both are static paths, but kept in the same specific-first + // order as the audit-trails block above for consistency. + ['name' => 'auditQuery#export', 'url' => '/api/v2/audit/export', 'verb' => 'GET'], + ['name' => 'auditQuery#query', 'url' => '/api/v2/audit', 'verb' => 'GET'], // Notification History — read-only audit trail of every dispatch. ['name' => 'notificationHistory#index', 'url' => '/api/notification-history', 'verb' => 'GET'], // Notification Subscriptions — DEPRECATED per-user (register, schema) opt-in surface. @@ -647,6 +775,9 @@ // Notification Preferences — override-only, per-(schema, notification) user preferences. ['name' => 'notificationPreferences#index', 'url' => '/api/notification-preferences', 'verb' => 'GET'], ['name' => 'notificationPreferences#update', 'url' => '/api/notification-preferences', 'verb' => 'PUT'], + // Notification Delivery Window — override-only, per-user quiet-hours preference. + ['name' => 'notificationDeliveryWindow#index', 'url' => '/api/notification-delivery-window', 'verb' => 'GET'], + ['name' => 'notificationDeliveryWindow#update', 'url' => '/api/notification-delivery-window', 'verb' => 'PUT'], // Search Trails - specific routes first, then general ones. ['name' => 'searchTrail#index', 'url' => '/api/search-trails', 'verb' => 'GET'], ['name' => 'searchTrail#statistics', 'url' => '/api/search-trails/statistics', 'verb' => 'GET'], @@ -670,7 +801,7 @@ ['name' => 'deleted#destroyMultiple', 'url' => '/api/deleted', 'verb' => 'DELETE'], // Revert. ['name' => 'revert#revert', 'url' => '/api/objects/{register}/{schema}/{id}/revert', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], - + // Files operations under objects. ['name' => 'files#create', 'url' => '/api/objects/{register}/{schema}/{id}/files', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], ['name' => 'files#save', 'url' => '/api/objects/{register}/{schema}/{id}/files/save', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], @@ -709,8 +840,18 @@ ['name' => 'notes#create', 'url' => '/api/objects/{register}/{schema}/{id}/notes', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], ['name' => 'notes#update', 'url' => '/api/objects/{register}/{schema}/{id}/notes/{noteId}', 'verb' => 'PUT', 'requirements' => ['id' => '[^/]+', 'noteId' => '[^/]+']], ['name' => 'notes#destroy', 'url' => '/api/objects/{register}/{schema}/{id}/notes/{noteId}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+', 'noteId' => '[^/]+']], - + + // Semantic-object handoff engine (ADR-051): availability + execute. + // Both #[NoAdminRequired] with a per-object RBAC guard in the method + // body (ADR-005/016/029); CSRF stays enabled on the POST. + ['name' => 'handoff#availability', 'url' => '/api/objects/{register}/{schema}/{id}/handoffs', 'verb' => 'GET', 'requirements' => ['register' => '[^/]+', 'schema' => '[^/]+', 'id' => '[^/]+']], + ['name' => 'handoff#execute', 'url' => '/api/objects/{register}/{schema}/{id}/handoffs/{handoffId}', 'verb' => 'POST', 'requirements' => ['register' => '[^/]+', 'schema' => '[^/]+', 'id' => '[^/]+', 'handoffId' => '[^/]+']], + // Schemas. + // Cross-app semantic reference discovery (ADR-048): resolve a canonical + // semantic-type URI to the installed provider schema. Static path, + // registered before the `{id}` schema routes so it is not shadowed. + ['name' => 'schemas#resolveByImplements', 'url' => '/api/schemas/resolve-by-implements', 'verb' => 'GET'], ['name' => 'schemas#upload', 'url' => '/api/schemas/upload', 'verb' => 'POST'], ['name' => 'schemas#uploadUpdate', 'url' => '/api/schemas/{id}/upload', 'verb' => 'PUT', 'requirements' => ['id' => '[^/]+']], ['name' => 'schemas#download', 'url' => '/api/schemas/{id}/download', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], @@ -718,6 +859,19 @@ ['name' => 'schemas#stats', 'url' => '/api/schemas/{id}/stats', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'schemas#explore', 'url' => '/api/schemas/{id}/explore', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'schemas#updateFromExploration', 'url' => '/api/schemas/{id}/update-from-exploration', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], + // Schema versioning & object migration (schema-versioning-and-object-migration). + ['name' => 'schemaMigration#changelog', 'url' => '/api/schemas/{id}/changelog', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], + ['name' => 'schemaMigration#revalidate', 'url' => '/api/schemas/{id}/revalidate', 'verb' => 'POST', 'requirements' => ['id' => '\d+']], + ['name' => 'schemaMigration#runs', 'url' => '/api/schemas/{id}/runs', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], + ['name' => 'schemaMigration#run', 'url' => '/api/schemas/{id}/runs/{run}', 'verb' => 'GET', 'requirements' => ['id' => '\d+', 'run' => '\d+']], + ['name' => 'schemaMigration#previewMigration', 'url' => '/api/schemas/{id}/migrations/preview', 'verb' => 'POST', 'requirements' => ['id' => '\d+']], + ['name' => 'schemaMigration#migrate', 'url' => '/api/schemas/{id}/migrations', 'verb' => 'POST', 'requirements' => ['id' => '\d+']], + ['name' => 'schemaMigration#rollback', 'url' => '/api/schemas/{id}/runs/{run}/rollback', 'verb' => 'POST', 'requirements' => ['id' => '\d+', 'run' => '\d+']], + // Schema import from external standards (schema-import-standards). Admin-gated by NC framework default. + ['name' => 'schemaImport#types', 'url' => '/api/schema-import/{dialect}/types', 'verb' => 'GET', 'requirements' => ['dialect' => '[^/]+']], + ['name' => 'schemaImport#snapshot', 'url' => '/api/schema-import/{dialect}/snapshot', 'verb' => 'GET', 'requirements' => ['dialect' => '[^/]+']], + ['name' => 'schemaImport#import', 'url' => '/api/schema-import/{dialect}', 'verb' => 'POST', 'requirements' => ['dialect' => '[^/]+']], + ['name' => 'schemaImport#reimport', 'url' => '/api/schemas/{id}/reimport', 'verb' => 'POST', 'requirements' => ['id' => '\d+']], // Registers ['name' => 'registers#export', 'url' => '/api/registers/{id}/export', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'registers#import', 'url' => '/api/registers/{id}/import', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], @@ -744,7 +898,7 @@ ['name' => 'configuration#preview', 'url' => '/api/configurations/{id}/preview', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], ['name' => 'configuration#import', 'url' => '/api/configurations/{id}/import', 'verb' => 'POST', 'requirements' => ['id' => '\d+']], ['name' => 'configuration#export', 'url' => '/api/configurations/{id}/export', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], - + // Configuration discovery endpoints. ['name' => 'configuration#discover', 'url' => '/api/configurations/discover', 'verb' => 'GET'], ['name' => 'configuration#enrichDetails', 'url' => '/api/configurations/enrich', 'verb' => 'GET'], @@ -753,16 +907,16 @@ ['name' => 'configuration#getGitHubConfigurations', 'url' => '/api/configurations/github/files', 'verb' => 'GET'], ['name' => 'configuration#getGitLabBranches', 'url' => '/api/configurations/gitlab/branches', 'verb' => 'GET'], ['name' => 'configuration#getGitLabConfigurations', 'url' => '/api/configurations/gitlab/files', 'verb' => 'GET'], - + // Configuration import endpoints. ['name' => 'configurations#import', 'url' => '/api/configurations/import', 'verb' => 'POST'], ['name' => 'configuration#importFromGitHub', 'url' => '/api/configurations/import/github', 'verb' => 'POST'], ['name' => 'configuration#importFromGitLab', 'url' => '/api/configurations/import/gitlab', 'verb' => 'POST'], ['name' => 'configuration#importFromUrl', 'url' => '/api/configurations/import/url', 'verb' => 'POST'], - + // Configuration publish endpoints. ['name' => 'configuration#publishToGitHub', 'url' => '/api/configurations/{id}/publish/github', 'verb' => 'POST'], - + // User Settings - GitHub Integration. ['name' => 'userSettings#getGitHubTokenStatus', 'url' => '/api/user-settings/github/status', 'verb' => 'GET'], ['name' => 'userSettings#setGitHubToken', 'url' => '/api/user-settings/github/token', 'verb' => 'POST'], @@ -771,8 +925,8 @@ ['name' => 'applications#page', 'url' => '/applications', 'verb' => 'GET'], // SPA detail route — see ConductionNL/openregister#1962. ['name' => 'ui#applicationDetails', 'url' => '/applications/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], - // Agents. - ['name' => 'agents#page', 'url' => '/agents', 'verb' => 'GET'], + // Agents. The SPA page moved to hermiq (or-chat-engine-decommission); + // only the API surface remains, answered via the compat proxy. ['name' => 'agents#stats', 'url' => '/api/agents/stats', 'verb' => 'GET'], ['name' => 'agents#tools', 'url' => '/api/agents/tools', 'verb' => 'GET'], // Search. @@ -806,7 +960,7 @@ ['name' => 'tags#index', 'url' => '/api/objects/{register}/{schema}/{id}/tags', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'tags#add', 'url' => '/api/objects/{register}/{schema}/{id}/tags', 'verb' => 'POST', 'requirements' => ['id' => '[^/]+']], ['name' => 'tags#remove', 'url' => '/api/objects/{register}/{schema}/{id}/tags/{tag}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+', 'tag' => '[^/]+']], - + // Views - Saved search configurations. ['name' => 'views#index', 'url' => '/api/views', 'verb' => 'GET'], ['name' => 'views#show', 'url' => '/api/views/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], @@ -814,7 +968,7 @@ ['name' => 'views#update', 'url' => '/api/views/{id}', 'verb' => 'PUT', 'requirements' => ['id' => '[^/]+']], ['name' => 'views#patch', 'url' => '/api/views/{id}', 'verb' => 'PATCH', 'requirements' => ['id' => '[^/]+']], ['name' => 'views#destroy', 'url' => '/api/views/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '[^/]+']], - + // Chat - AI Assistant endpoints. ['name' => 'chat#sendMessage', 'url' => '/api/chat/send', 'verb' => 'POST'], ['name' => 'chat#getHistory', 'url' => '/api/chat/history', 'verb' => 'GET'], @@ -827,7 +981,7 @@ // Chat - SSE streaming endpoint (authenticated). ['name' => 'chatStream#stream', 'url' => '/api/chat/stream', 'verb' => 'POST'], - + // Conversations - AI Conversation management. ['name' => 'conversation#index', 'url' => '/api/conversations', 'verb' => 'GET'], ['name' => 'conversation#show', 'url' => '/api/conversations/{uuid}', 'verb' => 'GET', 'requirements' => ['uuid' => '[^/]+']], @@ -837,22 +991,25 @@ ['name' => 'conversation#destroy', 'url' => '/api/conversations/{uuid}', 'verb' => 'DELETE', 'requirements' => ['uuid' => '[^/]+']], ['name' => 'conversation#restore', 'url' => '/api/conversations/{uuid}/restore', 'verb' => 'POST', 'requirements' => ['uuid' => '[^/]+']], ['name' => 'conversation#destroyPermanent', 'url' => '/api/conversations/{uuid}/permanent', 'verb' => 'DELETE', 'requirements' => ['uuid' => '[^/]+']], - + // File Text Management - Extract and manage text from files. ['name' => 'fileText#getFileText', 'url' => '/api/files/{fileId}/text', 'verb' => 'GET', 'requirements' => ['fileId' => '\\d+']], ['name' => 'fileText#extractFileText', 'url' => '/api/files/{fileId}/extract', 'verb' => 'POST', 'requirements' => ['fileId' => '\\d+']], ['name' => 'fileText#bulkExtract', 'url' => '/api/files/extract/bulk', 'verb' => 'POST'], ['name' => 'fileText#getStats', 'url' => '/api/files/extraction/stats', 'verb' => 'GET'], ['name' => 'fileText#deleteFileText', 'url' => '/api/files/{fileId}/text', 'verb' => 'DELETE', 'requirements' => ['fileId' => '\\d+']], - + // File Chunking & Indexing - Process extracted files and index chunks in SOLR. - ['name' => 'fileText#processAndIndexExtracted', 'url' => '/api/files/chunks/process', 'verb' => 'POST'], - ['name' => 'fileText#processAndIndexFile', 'url' => '/api/files/{fileId}/chunks/process', 'verb' => 'POST', 'requirements' => ['fileId' => '\\d+']], - ['name' => 'fileText#getChunkingStats', 'url' => '/api/files/chunks/stats', 'verb' => 'GET'], // File Anonymization - Replace detected entities with placeholders. ['name' => 'fileText#anonymizeFile', 'url' => '/api/files/{fileId}/anonymize', 'verb' => 'POST', 'requirements' => ['fileId' => '\\d+']], + // Manual entity addition - operator-supplied value, chunk-aware string matching, persists catalogue + relations. + ['name' => 'fileText#addManualEntity', 'url' => '/api/files/{fileId}/manual-entities', 'verb' => 'POST', 'requirements' => ['fileId' => '\\d+']], + + // Entity Relations - Decision-metadata PATCH (bases + skipAnonymization). See `entity-relation-grondslagen`. + ['name' => 'entityRelations#update', 'url' => '/api/entity-relations/{id}', 'verb' => 'PATCH', 'requirements' => ['id' => '\\d+']], + // GDPR Entities - Manage detected PII entities. ['name' => 'gdprEntities#index', 'url' => '/api/entities', 'verb' => 'GET'], ['name' => 'gdprEntities#show', 'url' => '/api/entities/{id}', 'verb' => 'GET', 'requirements' => ['id' => '\\d+']], @@ -861,14 +1018,7 @@ ['name' => 'gdprEntities#getCategories', 'url' => '/api/entities/categories', 'verb' => 'GET'], ['name' => 'gdprEntities#getStats', 'url' => '/api/entities/stats', 'verb' => 'GET'], - // File Warmup & Indexing - Bulk process and index files in SOLR. - ['name' => 'Settings\FileSettings#warmupFiles', 'url' => '/api/solr/warmup/files', 'verb' => 'POST'], - ['name' => 'Settings\FileSettings#indexFile', 'url' => '/api/solr/files/{fileId}/index', 'verb' => 'POST', 'requirements' => ['fileId' => '\\d+']], - ['name' => 'Settings\FileSettings#reindexFiles', 'url' => '/api/solr/files/reindex', 'verb' => 'POST'], - ['name' => 'Settings\FileSettings#getFileIndexStats', 'url' => '/api/solr/files/stats', 'verb' => 'GET'], - - // File Search - Keyword, semantic, and hybrid search over file contents. - ['name' => 'fileSearch#keywordSearch', 'url' => '/api/search/files/keyword', 'verb' => 'POST'], + // File Search - Semantic and hybrid search over file contents. ['name' => 'fileSearch#semanticSearch', 'url' => '/api/search/files/semantic', 'verb' => 'POST'], ['name' => 'fileSearch#hybridSearch', 'url' => '/api/search/files/hybrid', 'verb' => 'POST'], @@ -897,7 +1047,6 @@ // guard in OC's Router doesn't reject it. ['name' => 'ui#integrationsView', 'url' => '/integrations/{register}/{schema}/{objectId}', 'verb' => 'GET', 'requirements' => ['register' => '[^/]+', 'schema' => '[^/]+', 'objectId' => '[^/]+']], ['name' => 'ui#tables', 'url' => '/tables', 'verb' => 'GET'], - ['name' => 'ui#chat', 'url' => '/chat', 'verb' => 'GET'], ['name' => 'ui#configurations', 'url' => '/configurations', 'verb' => 'GET'], ['name' => 'ui#deleted', 'url' => '/deleted', 'verb' => 'GET'], ['name' => 'ui#auditTrail', 'url' => '/audit-trails', 'verb' => 'GET'], @@ -954,6 +1103,31 @@ ['name' => 'webhooks#allLogs', 'url' => '/api/webhooks/logs', 'verb' => 'GET'], ['name' => 'webhooks#retry', 'url' => '/api/webhooks/logs/{logId}/retry', 'verb' => 'POST', 'requirements' => ['logId' => '\d+']], + // Scheduled reports (scheduled-report-jobs): owner-scoped recurring + // ExportService exports, delivered to Files + notification. Admin may + // list all via ?all=true. run-now queues ScheduledReportRunNowJob and + // never runs the export inline in the request. + ['name' => 'scheduledReports#index', 'url' => '/api/scheduled-reports', 'verb' => 'GET'], + ['name' => 'scheduledReports#show', 'url' => '/api/scheduled-reports/{id}', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], + ['name' => 'scheduledReports#create', 'url' => '/api/scheduled-reports', 'verb' => 'POST'], + ['name' => 'scheduledReports#update', 'url' => '/api/scheduled-reports/{id}', 'verb' => 'PUT', 'requirements' => ['id' => '\d+']], + ['name' => 'scheduledReports#destroy', 'url' => '/api/scheduled-reports/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '\d+']], + ['name' => 'scheduledReports#runNow', 'url' => '/api/scheduled-reports/{id}/run-now', 'verb' => 'POST', 'requirements' => ['id' => '\d+']], + + // Migration mapping packs (migration-mapping-packs): declarative + // source-format-to-schema import mappings. Reads (index/show/export) + // are available to any authenticated user so the import flow can + // browse packs; create/update/destroy/import are admin-gated. The + // `packId` request param on registers#import (below) resolves a + // pack by its `packSlug` and runs each row through it before save. + ['name' => 'migrationPacks#index', 'url' => '/api/migration-packs', 'verb' => 'GET'], + ['name' => 'migrationPacks#create', 'url' => '/api/migration-packs', 'verb' => 'POST'], + ['name' => 'migrationPacks#import', 'url' => '/api/migration-packs/import', 'verb' => 'POST'], + ['name' => 'migrationPacks#show', 'url' => '/api/migration-packs/{id}', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], + ['name' => 'migrationPacks#update', 'url' => '/api/migration-packs/{id}', 'verb' => 'PUT', 'requirements' => ['id' => '\d+']], + ['name' => 'migrationPacks#destroy', 'url' => '/api/migration-packs/{id}', 'verb' => 'DELETE', 'requirements' => ['id' => '\d+']], + ['name' => 'migrationPacks#export', 'url' => '/api/migration-packs/{id}/export', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], + // Workflow Engines - CRUD and health check. ['name' => 'workflowEngine#available', 'url' => '/api/engines/available', 'verb' => 'GET'], ['name' => 'workflowEngine#index', 'url' => '/api/engines', 'verb' => 'GET'], diff --git a/composer.json b/composer.json index ba5c5a4f52..cc58a996fb 100644 --- a/composer.json +++ b/composer.json @@ -11,15 +11,17 @@ ], "autoload": { "psr-4": { - "OCA\\OpenRegister\\": "lib/", - "OCP\\": "vendor/nextcloud/ocp/OCP/", - "NCU\\": "vendor/nextcloud/ocp/NCU/" + "OCA\\OpenRegister\\": "lib/" } }, "repositories": [ { "type": "vcs", "url": "https://github.com/theodo-group/LLPhant" + }, + { + "type": "vcs", + "url": "https://codeberg.org/Conduction/sapp" } ], "scripts": { @@ -39,7 +41,7 @@ "psalm:fix": "./vendor/bin/psalm --threads=1 --no-cache --alter || echo 'Psalm not installed, skipping...'", "psalm:output": "./vendor/bin/psalm --threads=1 --no-cache --output-format=json 2>/dev/null | tail -1 > psalm-output.json || echo 'Psalm not installed, skipping...'", "phpstan": "./vendor/bin/phpstan analyse --memory-limit=1G || echo 'PHPStan not installed, skipping...'", - "phpmd": "phpmd lib text phpmd.xml --cache || echo 'PHPMD not installed, skipping...'", + "phpmd": "./vendor/bin/phpmd lib text phpmd.xml --baseline-file phpmd.baseline.xml || echo 'PHPMD not installed, skipping...'", "phpmetrics": "./vendor/bin/phpmetrics --report-html=phpmetrics lib/", "phpmetrics:json": "./vendor/bin/phpmetrics --report-json=phpmetrics/report.json lib/", "phpmetrics:csv": "./vendor/bin/phpmetrics --report-csv=phpmetrics/report.csv lib/", @@ -94,9 +96,12 @@ "adbario/php-dot-notation": "^3.3.0", "bamarni/composer-bin-plugin": "^1.8", "cweagans/composer-patches": "^1.7", + "ddn/sapp": "dev-feat/chained-filter-text-replace#5c406e91254d6936f44372db35f1cc15e5a06c56", "dompdf/dompdf": "^3.1", + "dragonmantank/cron-expression": "^3.3", "elasticsearch/elasticsearch": "^v8.14.0", "guzzlehttp/guzzle": "^7.0", + "minishlink/web-push": "^9.0", "opis/json-schema": "^2.3", "phpoffice/phpspreadsheet": "^5.0", "phpoffice/phpword": "^1.2", @@ -118,6 +123,7 @@ }, "require-dev": { "cyclonedx/cyclonedx-php-composer": "^6.2", + "doctrine/dbal": "^3.8", "edgedesign/phpqa": "^1.27", "nextcloud/coding-standard": "^1.4", "nextcloud/ocp": "^31.0", @@ -149,7 +155,11 @@ "composer-exit-on-patch-failure": true, "patches": { "theodo-group/llphant": { - "Forward think:false + keep_alive:-1 to the Ollama API (qwen3 ~5x speedup, no idle-unload wedge) \u2014 drop when LLPhant adds a per-call keep_alive/think model option": "patches/llphant-ollama-think-keepalive.patch" + "Forward think:false + keep_alive:-1 to the Ollama API (qwen3 ~5x speedup, no idle-unload wedge) \u2014 drop when LLPhant adds a per-call keep_alive/think model option": "patches/llphant-ollama-think-keepalive.patch", + "Capture Ollama token/latency usage on OllamaChat::$lastUsage so ChatService can record per-run cost (run-analytics) \u2014 drop when LLPhant exposes response usage": "patches/llphant-ollama-usage-capture.patch" + }, + "phpoffice/phpspreadsheet": { + "Prefer ZipStream3 when zipstream-php v3 is installed \u2014 a co-installed app's leaked zipstream v2 Option\\Archive otherwise mis-selects the v2 shim and 500s every .xlsx export on the shared Nextcloud autoloader": "patches/phpspreadsheet-zipstream3-prefer.patch" } } } diff --git a/composer.lock b/composer.lock index c97561c41d..be29d81d63 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "74fbef263382676f4d61f6f55f0270ef", + "content-hash": "f2ba0fc14c0f30c58ec359432476209a", "packages": [ { "name": "adbario/php-dot-notation", @@ -304,6 +304,43 @@ }, "time": "2022-12-20T22:53:13+00:00" }, + { + "name": "ddn/sapp", + "version": "dev-feat/chained-filter-text-replace", + "source": { + "type": "git", + "url": "https://codeberg.org/Conduction/sapp", + "reference": "5c406e91254d6936f44372db35f1cc15e5a06c56" + }, + "require": { + "ext-mbstring": "*", + "php": ">=7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ddn\\sapp\\": "src/" + } + }, + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Carlos A.", + "email": "caralla@upv.es" + } + ], + "description": "Simple and Agnostic PDF Parser (SAPP): Edit, manipulate and sign PDF documents usign PHP", + "keywords": [ + "electronic signature", + "pdf", + "php", + "sign", + "signature" + ], + "time": "2026-06-16T13:52:30+00:00" + }, { "name": "dompdf/dompdf", "version": "v3.1.5", @@ -459,6 +496,70 @@ }, "time": "2026-01-02T16:01:13+00:00" }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, { "name": "elastic/transport", "version": "v8.11.0", @@ -576,25 +677,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.4", + "version": "7.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "aec528da477062d3af11f51e6b33402be233b21f" + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aec528da477062d3af11f51e6b33402be233b21f", - "reference": "aec528da477062d3af11f51e6b33402be233b21f", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9aa17bcdd777ee31df9fc83c337ca4ca2340def3", + "reference": "9aa17bcdd777ee31df9fc83c337ca4ca2340def3", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -603,7 +705,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.3.2", + "guzzlehttp/test-server": "^0.5.1", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -683,7 +785,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.4" + "source": "https://github.com/guzzle/guzzle/tree/7.12.3" }, "funding": [ { @@ -699,24 +801,25 @@ "type": "tidelift" } ], - "time": "2026-05-22T19:00:53+00:00" + "time": "2026-06-23T15:29:02+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.4.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -766,7 +869,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.4.1" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -782,27 +885,29 @@ "type": "tidelift" } ], - "time": "2026-05-20T22:57:30+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.10.1", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "73ab136360b5dfd858006eae9795e8fe43c80361" + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/73ab136360b5dfd858006eae9795e8fe43c80361", - "reference": "73ab136360b5dfd858006eae9795e8fe43c80361", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -883,7 +988,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.10.1" + "source": "https://github.com/guzzle/psr7/tree/2.12.3" }, "funding": [ { @@ -899,7 +1004,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T09:27:36+00:00" + "time": "2026-06-23T15:21:08+00:00" }, { "name": "laravel/serializable-closure", @@ -1214,6 +1319,73 @@ }, "time": "2025-07-25T09:04:22+00:00" }, + { + "name": "minishlink/web-push", + "version": "v9.0.4", + "source": { + "type": "git", + "url": "https://github.com/web-push-libs/web-push-php.git", + "reference": "f979f40b0017d2f86d82b9f21edbc515d031cc23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/f979f40b0017d2f86d82b9f21edbc515d031cc23", + "reference": "f979f40b0017d2f86d82b9f21edbc515d031cc23", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^7.9.2", + "php": ">=8.1", + "spomky-labs/base64url": "^2.0.4", + "symfony/polyfill-php82": "^v1.31.0", + "web-token/jwt-library": "^3.3.0|^4.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.91.3", + "phpstan/phpstan": "^2.1.2", + "phpunit/phpunit": "^10.5.44|^11.5.6", + "symfony/polyfill-iconv": "^1.33" + }, + "suggest": { + "ext-bcmath": "Optional for performance.", + "ext-gmp": "Optional for performance." + }, + "type": "library", + "autoload": { + "psr-4": { + "Minishlink\\WebPush\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Louis Lagrange", + "email": "lagrange.louis@gmail.com", + "homepage": "https://github.com/Minishlink" + } + ], + "description": "Web Push library for PHP", + "homepage": "https://github.com/web-push-libs/web-push-php", + "keywords": [ + "Push API", + "WebPush", + "notifications", + "push", + "web" + ], + "support": { + "issues": "https://github.com/web-push-libs/web-push-php/issues", + "source": "https://github.com/web-push-libs/web-push-php/tree/v9.0.4" + }, + "time": "2025-12-10T14:00:12+00:00" + }, { "name": "open-telemetry/api", "version": "1.9.0", @@ -3362,6 +3534,71 @@ ], "time": "2021-12-08T20:36:59+00:00" }, + { + "name": "spomky-labs/base64url", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/base64url.git", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.11|^0.12", + "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", + "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", + "phpstan/phpstan-phpunit": "^0.11|^0.12", + "phpstan/phpstan-strict-rules": "^0.11|^0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Base64Url\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky-Labs/base64url/contributors" + } + ], + "description": "Base 64 URL Safe Encoding/Decoding PHP Library", + "homepage": "https://github.com/Spomky-Labs/base64url", + "keywords": [ + "base64", + "rfc4648", + "safe", + "url" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/base64url/issues", + "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2020-11-03T09:10:25+00:00" + }, { "name": "spomky-labs/pki-framework", "version": "1.4.2", @@ -3651,16 +3888,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v7.4.10", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d" + "reference": "f299e20ce983be6c0744952533c6dfeaaa1448e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", - "reference": "4eb0d9dfa9d4f7c59216baf49b3ed6b1fb72293d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f299e20ce983be6c0744952533c6dfeaaa1448e2", + "reference": "f299e20ce983be6c0744952533c6dfeaaa1448e2", "shasum": "" }, "require": { @@ -3711,7 +3948,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.10" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.13" }, "funding": [ { @@ -3731,7 +3968,7 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:55:30+00:00" + "time": "2026-05-20T14:07:29+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4123,16 +4360,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.9", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6" + "reference": "e8a112b8415707265a7e614278136a9d92989a6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6", - "reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6", + "url": "https://api.github.com/repos/symfony/http-client/zipball/e8a112b8415707265a7e614278136a9d92989a6a", + "reference": "e8a112b8415707265a7e614278136a9d92989a6a", "shasum": "" }, "require": { @@ -4200,7 +4437,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.9" + "source": "https://github.com/symfony/http-client/tree/v7.4.13" }, "funding": [ { @@ -4220,7 +4457,7 @@ "type": "tidelift" } ], - "time": "2026-04-29T13:25:15+00:00" + "time": "2026-05-24T09:57:54+00:00" }, { "name": "symfony/http-client-contracts", @@ -4839,16 +5076,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -4900,7 +5137,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -4920,7 +5157,91 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php82", @@ -5004,16 +5325,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.37.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", "shasum": "" }, "require": { @@ -5060,7 +5381,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" }, "funding": [ { @@ -5080,7 +5401,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-27T06:51:48+00:00" }, { "name": "symfony/polyfill-php85", @@ -5736,16 +6057,16 @@ }, { "name": "symfony/yaml", - "version": "v7.4.12", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "8b6952b56ca6417f25f7a65758cadd0ce02edc51" + "reference": "e8fdf3408c85806198d5826e604ffc6830d33152" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/8b6952b56ca6417f25f7a65758cadd0ce02edc51", - "reference": "8b6952b56ca6417f25f7a65758cadd0ce02edc51", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e8fdf3408c85806198d5826e604ffc6830d33152", + "reference": "e8fdf3408c85806198d5826e604ffc6830d33152", "shasum": "" }, "require": { @@ -5788,7 +6109,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.12" + "source": "https://github.com/symfony/yaml/tree/v6.4.41" }, "funding": [ { @@ -5808,7 +6129,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-05-25T06:03:23+00:00" }, { "name": "thecodingmachine/safe", @@ -6085,16 +6406,16 @@ }, { "name": "twig/twig", - "version": "v3.27.0", + "version": "v3.27.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "04ae1bfe9463c816cf72ca0abe7eae2c77a9a9ed" + "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/04ae1bfe9463c816cf72ca0abe7eae2c77a9a9ed", - "reference": "04ae1bfe9463c816cf72ca0abe7eae2c77a9a9ed", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/ae2071bffb38f04847fc0864d730c94b9cb8ab74", + "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74", "shasum": "" }, "require": { @@ -6149,7 +6470,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.27.0" + "source": "https://github.com/twigphp/Twig/tree/v3.27.1" }, "funding": [ { @@ -6161,20 +6482,20 @@ "type": "tidelift" } ], - "time": "2026-05-27T13:05:51+00:00" + "time": "2026-05-30T17:09:26+00:00" }, { "name": "web-token/jwt-framework", - "version": "3.4.9", + "version": "3.4.10", "source": { "type": "git", "url": "https://github.com/web-token/jwt-framework.git", - "reference": "112045db98dc78f10611647cab49e0b3d8a59057" + "reference": "1e231cb4c2630c154be38e6010ff8c0b965bd487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-token/jwt-framework/zipball/112045db98dc78f10611647cab49e0b3d8a59057", - "reference": "112045db98dc78f10611647cab49e0b3d8a59057", + "url": "https://api.github.com/repos/web-token/jwt-framework/zipball/1e231cb4c2630c154be38e6010ff8c0b965bd487", + "reference": "1e231cb4c2630c154be38e6010ff8c0b965bd487", "shasum": "" }, "require": { @@ -6318,7 +6639,7 @@ ], "support": { "issues": "https://github.com/web-token/jwt-framework/issues", - "source": "https://github.com/web-token/jwt-framework/tree/3.4.9" + "source": "https://github.com/web-token/jwt-framework/tree/3.4.10" }, "funding": [ { @@ -6330,7 +6651,7 @@ "type": "patreon" } ], - "time": "2025-11-17T20:20:37+00:00" + "time": "2026-06-06T16:30:13+00:00" }, { "name": "webonyx/graphql-php", @@ -7672,6 +7993,120 @@ }, "time": "2019-12-04T15:06:13+00:00" }, + { + "name": "doctrine/dbal", + "version": "3.10.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "95d84866bf3c04b2ddca1df7c049714660959aef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/95d84866bf3c04b2ddca1df7c049714660959aef", + "reference": "95d84866bf3c04b2ddca1df7c049714660959aef", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/cache": "< 1.11" + }, + "require-dev": { + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.34", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^5.4|^6.0|^7.0|^8.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.10.5" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2026-02-24T08:03:57+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.6", @@ -7720,6 +8155,97 @@ }, "time": "2026-02-07T07:09:04+00:00" }, + { + "name": "doctrine/event-manager", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2026-01-29T07:11:08+00:00" + }, { "name": "edgedesign/phpqa", "version": "v1.27.2", @@ -12377,16 +12903,16 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", "shasum": "" }, "require": { @@ -12433,7 +12959,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.1" }, "funding": [ { @@ -12453,7 +12979,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:45:58+00:00" }, { "name": "theseer/tokenizer", @@ -12685,6 +13211,7 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { + "ddn/sapp": 20, "roave/security-advisories": 20 }, "prefer-stable": false, diff --git a/css/mail-sidebar.css b/css/mail-sidebar.css index d437cf46b6..82341b34ac 100644 --- a/css/mail-sidebar.css +++ b/css/mail-sidebar.css @@ -85,6 +85,9 @@ aside.or-mail-sidebar.app-sidebar, } .or-mail-object-card__title { + display: flex; + align-items: center; + gap: 8px; font-size: 14px; font-weight: 600; margin: 0; @@ -92,6 +95,11 @@ aside.or-mail-sidebar.app-sidebar, min-width: 0; } +.or-mail-object-card__icon { + flex-shrink: 0; + color: var(--color-text-maxcontrast); +} + .or-mail-object-card__title a { color: var(--color-primary-element); text-decoration: none; @@ -199,6 +207,42 @@ aside.or-mail-sidebar.app-sidebar, padding: 4px 0; } +.or-action-create { + display: block; + width: 100%; + margin-top: 6px; + padding: 6px 10px; + border: 1px solid var(--color-border-dark); + border-radius: var(--border-radius); + background-color: var(--color-primary-element-light); + color: var(--color-primary-element-light-text); + font-size: 13px; + cursor: pointer; +} + +.or-action-create:hover:not(:disabled) { + background-color: var(--color-primary-element-light-hover); +} + +.or-action-create:disabled { + opacity: 0.6; + cursor: default; +} + +/* Spinner shown while a connection is being made / a record created */ +.or-action-connecting { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 2px; + font-size: 13px; + color: var(--color-text-maxcontrast); +} + +.or-action-block--busy { + opacity: 0.85; +} + /* Empty / loading states */ .or-tab-empty { color: var(--color-text-maxcontrast); diff --git a/docker-compose.yml b/docker-compose.yml index 2800b81d72..b40aea5249 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -596,7 +596,16 @@ services: user: root container_name: nextcloud # entrypoint: occ app:enable openregister - image: nextcloud + # PINNED Nextcloud version. Do NOT use the floating `nextcloud` / `:latest` + # tag here: when `latest` drifts to a newer NC (e.g. 33→34) and the env is + # recreated, the container boots a newer NC over the existing data volume → + # needsDbUpgrade → maintenance mode → 503 instance-wide, and bind-mounted + # apps (shillinq/pipelinq/procest/openregister/…) get disabled or their + # OpenRegister API fails until someone runs `occ upgrade`. Pinning a single + # patch tag makes every `docker compose up` reproducible — bump this + # deliberately (and run `occ upgrade` once) when you actually want to move + # NC versions, instead of being surprised on a reset. + image: nextcloud:34.0.0-apache restart: always ports: - 8080:80 @@ -614,7 +623,7 @@ services: - ../opencatalogi:/var/www/html/custom_apps/opencatalogi - ../softwarecatalog:/var/www/html/custom_apps/softwarecatalog - ../nldesign:/var/www/html/custom_apps/nldesign - - ../mydash:/var/www/html/custom_apps/mydash + - ../launchpad:/var/www/html/custom_apps/launchpad - ../opentalk:/var/www/html/custom_apps/opentalk - ../valtimo:/var/www/html/custom_apps/valtimo - ../openzaak:/var/www/html/custom_apps/openzaak @@ -625,6 +634,7 @@ services: - ../decidesk:/var/www/html/custom_apps/decidesk - ../zaakafhandelapp:/var/www/html/custom_apps/zaakafhandelapp - ../larpingapp:/var/www/html/custom_apps/larpingapp + - ../shillinq:/var/www/html/custom_apps/shillinq environment: # Database configuration (PostgreSQL) - POSTGRES_DB=nextcloud @@ -713,7 +723,9 @@ services: - mariadb user: root container_name: nextcloud-mariadb - image: nextcloud + # PINNED — same reason as the main nextcloud service above (avoid latest-drift + # upgrade churn that 503s the env on reset). + image: nextcloud:34.0.0-apache restart: always ports: - 8080:80 @@ -1473,23 +1485,3 @@ services: timeout: 10s retries: 5 start_period: 120s - - # GreenMail — SMTP/IMAP test mail server (use --profile mail) - greenmail: - profiles: - - mail - image: greenmail/standalone:2.0.1 - container_name: openregister-greenmail - restart: unless-stopped - ports: - - "3025:3025" - - "3143:3143" - - "8085:8080" - environment: - - GREENMAIL_OPTS=-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.users=testadmin:password1@test.local -Dgreenmail.verbose - healthcheck: - test: ["CMD", "sh", "-c", "nc -z 127.0.0.1 3025"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s diff --git a/docs/Features/credential-broker.md b/docs/Features/credential-broker.md new file mode 100644 index 0000000000..06820c977c --- /dev/null +++ b/docs/Features/credential-broker.md @@ -0,0 +1,117 @@ +# Credential Broker + +The credential broker lets a user store a long-lived external secret (a GitHub +token, a Doffin subscription key, …) ONCE in OpenRegister and lets allowed apps +*use* it without ever holding it. Apps never receive the secret: the broker +performs the outbound call itself and injects the secret server-side. + +## How a brokered call works + +Given a `credential` id, a calling app id, and a method + path, the broker +(`lib/Service/Credential/CredentialBrokerService.php`) enforces **four ordered +guards, failing closed** with a static 403: + +1. **Owner** — the acting user owns the credential object (per-object IDOR guard). +2. **Allowed apps** — the calling app id is in the credential's `allowedApps[]`. +3. **Allow-rules** — method + query-stripped path match one of the provider's + `allowRules[]` (exact method, glob path). +4. **Host-lock** — the resolved URL host equals the provider `baseUrl` host; + callers supply only a path, never a URL. + +Only then is the secret read from the store, injected per the provider's +`authScheme` (the broker owns that header — caller-supplied values for it are +discarded), and the single outbound call performed. The secret never appears in +a response, log line, or error. + +## Provider catalogue (runtime-immutable) + +`lib/Settings/credential-providers.json` is a **read-only security control** +shipped with the app: it defines every reachable provider, its host-locked +`baseUrl`, auth header template, and allow-rules. There is NO API that writes +it — new providers/rules ship only through a reviewed release. Current entries: +`github`, `gitlab`, and `doffin` (Doffin Norway public-procurement Public API — +GET-only, `/notices` search, `Ocp-Apim-Subscription-Key` header carrying the +bare key). + +## Secret custody: resolved store leaves + +The secret itself lives behind the `CredentialStore` interface (`put`/`get`/ +`delete`, keyed by the credential object's UUID). Which leaf holds it is +decided at runtime by **`CredentialStoreResolver`** (DI factory in +`lib/AppInfo/Application.php`): + +- **Doriath leaf** (`DoriathCredentialStore`) — preferred when the `doriath` + secrets app is enabled, its service classes and application-scoped seam + methods (`SecretService::getByNameForApplication` / `deleteByApplication`, + landing via Doriath's `application-secret-delete` change) exist, AND + OpenRegister has self-registered with Doriath. Secrets are stored as + **application-owned ciphertext** in OpenRegister's single Doriath vault + (secret name = credential UUID, root folder), envelope-encrypted with + Doriath's `rsa-oaep-sha256-chunked-v1` scheme against OpenRegister's own + public key. Only OpenRegister's system-scoped private key can decrypt them; + Doriath adds at-rest encryption, audit events, and rotation metadata. +- **Nextcloud-vault leaf** (`NextcloudVaultCredentialStore`) — the fallback: + per-user rows in Nextcloud's encrypted credentials vault. Used whenever any + Doriath condition fails, so instances without Doriath behave exactly as + before. + +**Lazy migration:** with the Doriath leaf active, a read that misses in Doriath +but hits in the legacy per-user vault re-puts the secret into Doriath, deletes +the vault row, and returns it. This only happens in a user-session context (the +vault is session-scoped); a background read of an un-migrated secret fails +closed until its owner uses it once interactively. Deletes always clear both +stores. + +**Operator note:** disabling Doriath after secrets have migrated makes those +secrets unreachable (broker fails closed with 403) until Doriath is re-enabled; +nothing is lost or exposed. If OpenRegister's private key is ever lost, affected +credentials fail closed and owners simply re-enter the secret. + +## Doriath self-registration (repair step) + +`lib/Repair/RegisterOpenRegisterWithDoriath.php` (install + post-migration) +makes OpenRegister a first-class Doriath application, idempotently: + +1. Skips fast when the persisted application UUID still matches a live Doriath + row; warns and completes (never throws) when Doriath is unavailable. +2. Otherwise: generates an RSA-4096 keypair, stores the private key + SYSTEM-scoped in `ICredentialsManager` (`openregister/doriath/private-key`), + self-generates a PKCS#10 CSR (CN `openregister`), and calls Doriath's + `ApplicationService::register(..., isAdmin: true)` in-process — the + registration auto-approves and provisions the EncryptionSuite from the CSR. +3. Persists the Doriath-assigned application UUID and OpenRegister's public key + PEM (public material only) in `IAppConfig`. + +## App onboarding and identity + +- **HTTP / cross-runtime callers** authenticate per app with a short-lived + HMAC token (`X-Credential-Token`, minted from a per-app signing secret + registered via `POST /api/credentials/apps/{appId}/register`, admin-only). + The app id checked against `allowedApps[]` comes ONLY from verified token + claims. +- **Trusted in-process callers** (same-instance PHP: openconnector, background + jobs) call `CredentialBrokerService::request` directly, passing their own + `appId` — no token. The token authenticates claims across the HTTP trust + boundary, which an in-process call does not cross; all four guards run + identically. +- **No-code onboarding:** when an AppHost leaf (or generated virtual app) + initialises and its bundled `src/manifest.json` declares a non-empty + `credentials[]`, `GenericInitializeSettings` auto-registers the app with the + broker — guarded by `isRegistered()` so an auto-run never rotates an existing + signing secret. Rotation stays an explicit admin action. + +## Background jobs: `actingUserId` + +`CredentialBrokerService::request` accepts an optional `actingUserId` for +**sessionless in-process callers only** (e.g. a cron job running a scheduled +ingestion for the user who configured it). Semantics: + +- Honored ONLY when no user session exists; the owner guard then evaluates + against it. All other guards are unchanged. +- When a session exists, the **session identity wins unconditionally** — a + session caller cannot impersonate anyone by passing `actingUserId`. +- The HTTP controller **never forwards** an acting user: on the HTTP path + identity is session-only, and any request-supplied acting-user value is + ignored entirely. +- The value is an assertion by trusted same-instance code — derive it from + durable job context (the configuring user), never from request input. diff --git a/docs/Features/entity-relation-decision-metadata.md b/docs/Features/entity-relation-decision-metadata.md new file mode 100644 index 0000000000..ebe5eed90d --- /dev/null +++ b/docs/Features/entity-relation-decision-metadata.md @@ -0,0 +1,88 @@ +# Entity-Relation Decision Metadata + +OpenRegister exposes an audited `PATCH /api/entity-relations/{id}` endpoint (and a parallel DI mapper method `EntityRelationMapper::updateDecisionMetadata`) for setting operator decisions on detected-entity occurrences. The two decision fields are: + +- **`bases`** — `?array` — UUIDs referencing legal grondslagen (Woo Art. 5 / equivalent) that justify redacting the occurrence. OpenRegister persists the UUIDs verbatim and does not validate that they resolve — the vocabulary is owned by the consumer app (DocuDesk's `dossier` register is the first consumer). +- **`skipAnonymization`** — `bool` (default `false`) — when `true`, the anonymise pass excludes this occurrence: text-replacement skips it, and `EntityRelationMapper::markAsAnonymized` leaves `anonymized = false` on the row. + +These are **decision-only** fields. The post-hoc system fields `anonymized` and `anonymizedValue` (which record what the redaction code path actually did) are intentionally NOT in the PATCH whitelist; only `EntityRelationMapper::markAsAnonymized` writes them. + +## Endpoint contract + +``` +PATCH /api/entity-relations/{id} +Content-Type: application/json +Body: { "bases"?: null | string[], "skipAnonymization"?: boolean } +``` + +- **200**: returned on success, body is the updated `EntityRelation` (`jsonSerialize` shape). +- **400**: shape or whitelist violation. Body: `{"error": "", "details": {"field": "", "reason"?: ""}}`. Triggered by: + - Any non-whitelisted top-level key (e.g. `anonymized`, `entityId`). + - `bases` not `null` or `array`. + - `skipAnonymization` not boolean. +- **401**: no authenticated session. +- **403**: acting user lacks write-access to the relation's parent file (or object/email). For file-bound relations the check resolves the file through the user-folder and requires `isUpdateable()` to be `true`. Object- and email-bound relations are accepted with a warning log in v1; tightening tracked as a follow-up. +- **404**: `{id}` does not resolve to an existing relation. +- **500**: unexpected failure during the write. + +The endpoint is `@NoAdminRequired` — non-admins can PATCH relations they have write-access to. + +## Semantics + +- **Single audited write path.** Both the HTTP controller and in-process DI callers go through `EntityRelationMapper::updateDecisionMetadata`. There is no parallel write path that bypasses validation or the audit trail. +- **Diff-aware.** Only fields whose new value differs from the current row state contribute to the update and the audit entry. A PATCH where every supplied value matches the current state, or an empty body `{}`, returns 200 with the unchanged row and writes NO audit entry. +- **Three-way `bases` semantics.** Field absent → unchanged; `"bases": null` → cleared; `"bases": []` → set to empty array (distinct from null per the spec); `"bases": ["..."]` → set to the array. +- **Audit-trail entry** (per successful change): + ``` + action = "entity_relation_decision_updated" + user = acting user UID (ADR-005 — NEVER the display name in the structured changed-fields payload) + created = now (UTC) + changed.subjectType = "openregister_entity_relations" + changed.subjectId = + changed.fields = { "": { "previous": , "new": } } — only fields that actually changed + ``` + Reads of `EntityRelation` rows produce no audit entries. + +## How callers use it + +**HTTP** (DocuDesk frontend, batch tools, scripts): + +```http +PATCH /api/entity-relations/123 +{ "bases": ["b8a3-..."], "skipAnonymization": false } +``` + +**PHP DI** (DocuDesk's `AnonymizationService`, OpenConnector pipelines, anywhere in OR's process): + +```php +$mapper = $this->getOpenRegisterService('OCA\OpenRegister\Db\EntityRelationMapper'); +$mapper->updateDecisionMetadata( + id: 123, + fields: ['bases' => ['b8a3-...'], 'skipAnonymization' => false], + actingUser: $this->userSession->getUser() +); +``` + +DocuDesk specifically uses the DI path for its prohibition-override flow: when an operator acknowledges an override, DocuDesk writes its own audit entry (capturing the operator's reason) and then PATCHes the relation with `skipAnonymization=true` via this DI method — so OR's anonymise pass automatically excludes the released entity. See [DocuDesk `anonymisation-grondslagen-and-prohibition-gate`](https://github.com/ConductionNL/docudesk/pull/135). + +## Anonymise-flow interaction + +The new field changes the behaviour of two existing code paths: + +1. **`POST /api/files/:fileId/anonymize` (HTTP)** — `FileTextController::anonymizeFile` reads relations through `EntityRelationMapper::findEntitiesForAnonymization`, which adds `AND skip_anonymization = 0` to the existing `findEntitiesForFile` query. Skipped relations are not in the replacements list and are not flipped by `markAsAnonymized`. +2. **`FileService::anonymizeDocument(Node, entities[])` (DI)** — the underlying `DocumentProcessingHandler::anonymizeDocument` defensively filters the caller-supplied `entities[]` against `EntityRelationMapper::findSkippedEntityValuesForFile($fileId)`. Even if the caller includes skipped occurrences in the array, OR drops them server-side before text-replacement. Contract: "skipped relations are never redacted, full stop." + +After the anonymise call: + +- Non-skipped relations: `anonymized = true`, `anonymizedValue = ` (existing behaviour). +- Skipped relations: `anonymized = false`, the operator's `skipAnonymization = true` flag is preserved. + +`skipAnonymization` is **forward-looking**: flipping it to `true` on an already-anonymised row does not retroactively un-redact the file. The redaction has already happened in the file content; only future re-runs honour the flag. + +## Spec references + +- Capability: [`openspec/changes/entity-relation-grondslagen/specs/entity-relation-grondslagen/spec.md`](../../openspec/changes/entity-relation-grondslagen/specs/entity-relation-grondslagen/spec.md) +- Design (anonymise flow, audit, authz, two-column migration): [`openspec/changes/entity-relation-grondslagen/design.md`](../../openspec/changes/entity-relation-grondslagen/design.md) +- ADR-022 (audit-trail for OR-owned mutations). +- ADR-005 (no PII in logs; UID not display name in audit payloads). +- ADR-023 (action-level authorization — opt-in; not introduced here). diff --git a/docs/Features/federation.md b/docs/Features/federation.md new file mode 100644 index 0000000000..1464b22098 --- /dev/null +++ b/docs/Features/federation.md @@ -0,0 +1,102 @@ +--- +title: Federation +sidebar_position: 12 +description: Share OpenRegister objects (registers, schemas, or individual objects) with an organisation on another Nextcloud instance, and read or edit them there. +keywords: + - Open Register + - Federation + - OCM + - Open Cloud Mesh + - Cross-instance sharing +--- + +# Federation + +Federation lets an organisation on one Nextcloud instance **share OpenRegister data with an organisation on another instance** — the way Nextcloud already federates files between servers. Share a whole register, a schema, a single object, or everything matching a rule; the receiving organisation reads (and, on read-write shares, edits) the shared objects as if they were local. + +It is built on Nextcloud's **Open Cloud Mesh (OCM)** for trust and share exchange, and on OpenRegister's existing object-source machinery for the data path — so shared objects appear as native, filterable objects across the API, index pages, detail pages, map and relations. + +## Concepts + +- **FederatedShare** — one cross-instance sharing relationship (outgoing or incoming). It carries the scope, the target organisation (`slug@host`), the permissions, and a scoped bearer **share token**. +- **Scope** — what is shared: `register` (everything in a register), `schema` (e.g. all WOO publications), `object` (a single case), or `query` (everything matching a filter — used by rule-based sharing). +- **Permissions** — `read` or `read-write`. +- **Confidentiality** — register/schema/query shares never serve an object marked confidential; only `object` shares serve exactly what the sharer picked. + +## How it works + +**Trust** reuses Nextcloud's federation (trusted servers + Federated Cloud IDs) and OCM. OpenRegister registers a cloud-federation provider for the `openregister` resource type, so its instances advertise (`/ocm-provider`) that they accept `openregister` shares and route incoming ones to the provider. + +**Data access** is *live proxy* by default: the receiving instance binds a shadow schema to the `federated` object-source provider, which proxies `find/findAll/count` to the sharing instance's token-scoped serving endpoint. No copy is stored; reads are always fresh. (A sync/copy mode reusing the Source engine is available for offline/performance.) + +**Write-back** — on a `read-write` share the receiver's edits proxy to the serving endpoint, which mutates the source object (full schema validation applies). + +## API + +### Share management (authenticated, organisation-scoped) + +| Method | URL | Purpose | +|---|---|---| +| `GET` | `/apps/openregister/api/federation/shares` | List the org's shares (`?direction=outgoing\|incoming`). | +| `POST` | `/apps/openregister/api/federation/shares` | Create an outgoing share. Body: `scope`, `register`, `schema`, `objectUri?`, `queryFilter?`, `permissions`, `sharedWith`. Returns the share incl. its token. | +| `DELETE` | `/apps/openregister/api/federation/shares/{id}` | Revoke a share. | + +### Serving (token-scoped, called by the remote instance) + +| Method | URL | Purpose | +|---|---|---| +| `GET` | `/apps/openregister/api/federation/{shareToken}/objects` | List the shared objects (paginated: `_limit`, `_offset`, `_search`). | +| `GET` | `/apps/openregister/api/federation/{shareToken}/objects/{id}` | Read one shared object. | +| `GET` | `/apps/openregister/api/federation/{shareToken}/meta` | Describe the share (scope, register/schema, permissions). | +| `POST` | `/apps/openregister/api/federation/{shareToken}/objects` | Create (read-write shares only). | +| `PUT` | `/apps/openregister/api/federation/{shareToken}/objects/{id}` | Update (read-write shares only). | +| `DELETE` | `/apps/openregister/api/federation/{shareToken}/objects/{id}` | Delete (read-write shares only). | + +The serving endpoints are public by design — the caller is another server authenticated by the bearer share token, not a local session — and every request is re-scoped to exactly what the share grants (register/schema/object/query, the sharing organisation, and confidentiality). + +## Consuming a share (shadow schema) + +Bind a local schema to the `federated` provider so the remote objects appear as native objects: + +```json +{ + "configuration": { + "x-openregister-object-source": { + "provider": "federated", + "readOnly": true, + "config": { "remoteUrl": "https://remote.example", "shareToken": "" } + } + } +} +``` + +## Rule-based sharing (flows) + +Instead of sharing by hand, a schema **flow** can share objects automatically. Add a `federate-share` action to the schema's `x-openregister-flows`; it fires on the matching lifecycle trigger and creates an idempotent object-scope share to the target organisation: + +```json +{ + "x-openregister-flows": [ + { + "name": "federate-published", + "trigger": "updated", + "actions": [ + { "type": "federate-share", "sharedWith": "partner-org@remote.example", "permissions": "read" } + ] + } + ] +} +``` + +"Every case meeting condition X goes to organisation Y" becomes declarative configuration, not code. + +## Security + +- Share tokens are per-share 48-character secrets scoped to a register/schema/object/query, a single organisation, and a permission level. +- Register/schema/query shares filter out confidential objects at serve time; a schema share can never leak a case marked confidential. +- Federated writers can only write into the sharing organisation — an edit can never plant an object into another organisation. +- OCM trust means shares are only exchanged with instances you have federated with. + +## Notes + +The live-proxy path requires the sharing instance to be reachable. A localhost self-loop (single-instance testing) additionally needs `allow_local_remote_servers`; real federated hostnames do not. diff --git a/docs/Features/json-ld.md b/docs/Features/json-ld.md new file mode 100644 index 0000000000..c8233ec6d0 --- /dev/null +++ b/docs/Features/json-ld.md @@ -0,0 +1,169 @@ +--- +title: Linked Data / JSON-LD +sidebar_position: 30 +description: Opt-in read-side JSON-LD serialization of register objects, with schema-derived contexts, canonical @id, and dereferenceable context documents. +keywords: + - Open Register + - JSON-LD + - Linked Data + - Schema.org + - content negotiation +--- + +# Linked Data / JSON-LD + +Open Register can serve register objects as +[JSON-LD 1.1](https://www.w3.org/TR/json-ld11/) — the W3C standard for +expressing Linked Data in JSON. JSON-LD output is **opt-in** and +**read-side only**: the default `application/json` representation is +never changed, and write endpoints are untouched. + +## Content negotiation + +Add `Accept: application/ld+json` to a request on either object read +endpoint to receive a JSON-LD representation: + +| Endpoint | Route | +|---|---| +| Single object | `GET /api/objects/{register}/{schema}/{id}` | +| Object collection | `GET /api/objects/{register}/{schema}` | + +JSON-LD responses carry `Content-Type: application/ld+json` and +`Vary: Accept`. When the `Accept` header is absent, is +`application/json`, or matches only via a wildcard (`*/*`, +`application/*`), the response is the existing plain-JSON +representation, byte-for-byte (including the `@self` envelope). + +Access control is identical across both representations — the JSON-LD +serializer wraps the *already-rendered* object, so RBAC, multitenancy, +the published predicate, and field-level security all keep applying. +There is no second data path. + +### Single object + +```http +GET /api/objects/personen/persoon/550e8400-… HTTP/1.1 +Accept: application/ld+json +``` + +```jsonc +{ + "@context": "https://nc.example.org/index.php/apps/openregister/api/contexts/personen/persoon", + "@id": "https://nc.example.org/index.php/apps/openregister/api/objects/personen/persoon/550e8400-…", + "@type": "persoon", + "name": "Jansen", + "birthDate": "1980-04-01", + "or:register": "personen", + "or:schema": "persoon", + "or:urn": "urn:gemeente-a:openregister:personen:persoon:550e8400-…" +} +``` + +### Collection + +A collection response is a single document with one top-level +`@context` and the page's objects as nodes in `@graph` (each with its +own `@id`/`@type`, no repeated `@context`). Pagination metadata is +expressed with `or:` terms so the document stays valid JSON-LD: + +```jsonc +{ + "@context": "https://nc.example.org/.../api/contexts/personen/persoon", + "@graph": [ { "@id": "…", "@type": "persoon", "name": "Jansen" }, … ], + "or:total": 42, + "or:page": 1, + "or:next": "https://nc.example.org/.../api/objects/personen/persoon?_page=2" +} +``` + +## `@id` and URN identity + +Each object's `@id` is its canonical, dereferenceable URI +(`ObjectEntity.uri`). When an object has no stored `uri`, `@id` falls +back to the absolute URL of its `objects#show` route. The object's +[URN](./urn-resource-addressing) is exposed as the `or:urn` term — not +as `@id`, because `@id` is meant to be dereferenceable. + +The `@self` envelope is **lifted** into `or:`-prefixed terms +(`or:register`, `or:schema`, `or:urn`, …) and the `@self` key itself +never appears in JSON-LD output. Any object data key that begins with +`@` is escaped under the `or:raw#` prefix to avoid colliding with +JSON-LD keywords. + +## The `@context` and the mapping block + +The `@context` value references a per-schema context document. The +context is derived mechanically from the schema's `properties` +(JSON Schema): + +- Every property gets a term. +- JSON-Schema `format` hints become `@type` coercions: + `date` → `xsd:date`, `date-time` → `xsd:dateTime`, `uri` → `@id`. +- Relation properties (referencing other objects) are declared + `"@type": "@id"` so related UUIDs/URIs read as node references. +- The `or:` prefix (`https://openregister.app/ns#`) is always defined + for OpenRegister metadata terms. + +Every schema therefore produces **valid JSON-LD with zero +configuration** — unmapped properties resolve to fragment terms in the +schema's own context document. + +### Opting into Schema.org (or any vocabulary) + +To align a schema with an external vocabulary such as +[Schema.org](https://schema.org/), add a `jsonld` block to the schema's +existing `configuration` JSON (round-tripped by the schemas API — no +migration needed): + +```jsonc +{ + "jsonld": { + "@vocab": "https://schema.org/", + "type": "https://schema.org/Person", + "properties": { + "name": "https://schema.org/name", + "birthDate": "https://schema.org/birthDate" + } + } +} +``` + +- `@vocab` — the vocabulary base IRI. +- `type` — the class IRI used as the object's `@type`. +- `properties` — a map of property name → term IRI. + +Mapped properties use the declared IRIs; unmapped properties keep their +zero-config defaults; without a `type`, `@type` defaults to the schema +slug. The mapping is **validated on schema save**: term values must be +absolute IRIs or compact terms resolvable against the declared +`@vocab`. An invalid mapping is rejected with `400` and the stored +configuration is left unchanged. + +## Context document endpoints + +The `@context` URLs are real, dereferenceable documents: + +| Endpoint | Returns | +|---|---| +| `GET /api/contexts/{register}` | The register-wide context document | +| `GET /api/contexts/{register}/{schema}` | The per-schema context document | + +Both respond with `Content-Type: application/ld+json`, a body of the +form `{"@context": { … }}`, an `ETag` derived from the underlying +`updated` timestamps (so conditional `If-None-Match` GETs return `304`), +and `Cache-Control` headers. Context documents contain structure only +(terms, IRIs, coercions) — never object data — so they are publicly +dereferenceable for registers with published schemas, letting external +linked-data consumers resolve the contexts they find in +published-object serializations. Unknown register/schema slugs return +`404`. + +## Scope + +JSON-LD support is **read-side only**: + +- Write endpoints (`POST`/`PUT`/`PATCH`/`DELETE`) do not accept or + interpret `application/ld+json` request bodies, and are unaffected by + an `Accept: application/ld+json` header. +- JSON-LD ingest, RDF stores, SPARQL, framing, signed credentials, and + non-JSON-LD RDF serializations are out of scope. diff --git a/docs/Features/manual-entity-anonymisation.md b/docs/Features/manual-entity-anonymisation.md new file mode 100644 index 0000000000..e96e744d1c --- /dev/null +++ b/docs/Features/manual-entity-anonymisation.md @@ -0,0 +1,133 @@ +# Manual Entity Anonymisation + +OpenRegister exposes `POST /api/files/{fileId}/manual-entities` for the operator-supplied "add this exact text to the anonymisation list for this file" flow. The endpoint performs **chunk-aware exact-string matching** against the file's previously extracted text, creates (or reuses) a catalogue entry for the value, and creates one `EntityRelation` row per occurrence found with `detectionMethod = manual`. + +The endpoint complements presidio/openanonymiser/pattern detection by giving operators a clean way to catch values the detectors missed without polluting the model with edge cases. It feeds into the same anonymise flow as auto-detected entities — `POST /api/files/{fileId}/anonymize` picks up the new relations on its next run. + +## Endpoint contract + +``` +POST /api/files/{fileId}/manual-entities +Content-Type: application/json +Body: { + "value": string, // operator-supplied text (REQUIRED, PII) + "type": string, // entity type tag, e.g. "PERSON" (REQUIRED) + "wholeWord": boolean (default true), + "caseSensitive": boolean (default true) +} +``` + +> **Note on `category`:** the `oc_openregister_entities.category` column is `NOT NULL` and is populated server-side from `type` via the same `EntityRecognitionHandler::getCategoryForType()` mapping the detector flow uses (PERSON / EMAIL / PHONE / ADDRESS → `personal_data`; IBAN / SSN → `sensitive_pii`; ORGANIZATION → `business_data`; LOCATION → `contextual_data`; DATE → `temporal_data`; everything else → `contextual_data`). The endpoint intentionally does NOT accept a `category` field in v1 — operator-override on category is a follow-up if a concrete use case emerges. + +**Response shape (201 / 200):** + +```json +{ + "entity": { + "id": 7, + "uuid": "01HMX...", + "value": "Jan Jansen", + "type": "PERSON", + "reused": false + }, + "relations": [ + { + "id": 200, + "chunkId": 100, + "positionStart": 13, + "positionEnd": 23, + "context": "... Jan Jansen woont in ..." + } + ], + "matchCount": 1, + "matchesSkipped": 0 +} +``` + +**Status codes:** + +- **201** — one or more matches found; the catalogue entry (new or reused) plus the inserted relations are in the body. +- **200** — zero matches found in the file. The catalogue entry is still created/reused and is available for use on other files. Body adds a `message` field: `"Text not found in file. Catalogue entry created (or reused) and is available for use on other files."` +- **400** — `{ "error": "invalid_request", "field": "value"|"type" }` for missing required body fields, or `{ "error": "regex_compile_failure" }` for malformed Unicode in the needle or a value longer than the chunk overlap (200 chars). +- **401** — no authenticated session: `{ "error": "unauthenticated" }`. +- **403** — acting user lacks write-access to the file: `{ "error": "forbidden", "reason": "write access to file required" }`. Same RBAC check as `PATCH /api/entity-relations/{id}` — the file MUST be reachable in the user-folder and `isUpdateable()` MUST return true. +- **415** — non-JSON Content-Type: `{ "error": "unsupported_media_type", "reason": "..." }`. +- **422** — `{ "error": "file_not_extracted" }` — the file has no extracted chunks. Operator must trigger text extraction first. +- **500** — unexpected failure: `{ "error": "internal_error" }`. Body never echoes the operator-supplied value (ADR-005). + +The endpoint is `@NoAdminRequired` — non-admins can add manual entities to files they can write. + +## Semantics + +- **Atomic per call.** The catalogue write, the relation inserts, and the audit-trail entries all happen inside one `IDBConnection::beginTransaction()` / `commit()` / `rollBack()` window. Either everything in the call lands or nothing does. +- **Idempotent on retry.** Each match position is probed via `EntityRelationMapper::existsForFileAtPosition($fileId, $entityId, $chunkId, $positionStart, $positionEnd)` before insert. Already-present rows bump `matchesSkipped` and don't insert. Re-running the call for the same value on the same file is a no-op DB-wise (the audit-trail entry is still written so operator intent is recorded). +- **Catalogue lookup-or-create.** `GdprEntityMapper::findOneByValueAndType(value, type)` resolves an existing row; otherwise a fresh `GdprEntity` is inserted. `entity.reused = (existing !== null)` in the response signals which path was taken. **Match flags do not key the catalogue lookup** — the catalogue row is the canonical truth for that (value, type) and is shared across all files that reference it. +- **Chunk-aware matching.** OR's text extractor splits long files into 1000-character chunks with 200-character overlap. The matcher runs `preg_match_all` per chunk, computes absolute positions as `chunk.startOffset + chunkRelativeOffset`, and dedups by absolute (start, end) across chunks. When the same match appears in two chunks' overlap region the entry from the **lower `chunkIndex`** wins, so re-runs select the same canonical chunk (idempotency precondition). +- **Match flag defaults.** Both `wholeWord` and `caseSensitive` default to **true**. The defaults reflect "operator means exactly what they typed"; loosen them only when the use case demands it. +- **Value-too-long.** Values longer than the chunk overlap (200 chars by default) are rejected with `regex_compile_failure` because they cannot reliably be matched per-chunk. The error message never contains the value (ADR-005). + +## Audit trail + +Two action types are written by every successful call: + +1. **`entity_create`** — written ONLY when a new catalogue row was inserted: + ``` + action = "entity_create" + user = acting user UID (NEVER the display name — ADR-005) + created = now (UTC) + changed.subjectType = "openregister_entities" + changed.subjectId = + changed.fields = { "value": , "type": , "category": } + ``` + ADR-022 forensic exception: `value` IS allowed in the audit payload (and only here — never in HTTP logs or error responses). + +2. **`entity_relations_batch_create`** — written on EVERY call, even on zero-match calls: + ``` + action = "entity_relations_batch_create" + user = acting user UID + created = now (UTC) + changed.subjectType = "openregister_files" + changed.subjectId = + changed.fields = { + "value": , + "type": , + "fileId": , + "detectionMethod": "manual", + "matchCount": , + "matchesSkipped": , + "relationIds": [, , ...] // only the inserted ones + } + ``` + Zero-match calls are still audited so operator intent is recorded even when the value isn't found in the file (e.g. a typo). + +## Anonymise-flow interaction + +Manual-method relations carry `detectionMethod = "manual"` but are otherwise identical to detector-produced rows. The downstream pass picks them up unchanged: + +- **`POST /api/files/{fileId}/anonymize`** — `EntityRelationMapper::findEntitiesForAnonymization` does NOT filter on `detection_method`; all relations on the file (minus `skip_anonymization = true` ones) are included. Manual-method rows are anonymised on the next run. +- **Caveat: value-keyed substitution.** OR's `DocumentProcessingHandler::anonymizeDocument` collapses multiple distinct catalogue entries with the same `value` into one substitution token. If an operator manually adds a value that already exists in the catalogue (under a different type), the auto-detected and manually-added occurrences will all map to the same placeholder. This is a feature, not a bug — operators don't see two placeholders for "Jan Jansen" just because one occurrence was auto-detected and another was operator-flagged. + +## PII redaction (ADR-005) + +- **Request log** — controller logs `valueLength` only, never `value`. Permitted log payload: `fileId`, `type`, `wholeWord`, `caseSensitive`, `valueLength`, `actor` (UID — UID is not PII per ADR-005). +- **Error responses** — never include the operator-supplied `value`. Reason codes are stable strings; messages are operator-readable but PII-clean. +- **Logger warnings on the catalogue dedup-invariant violation** — `GdprEntityMapper::findOneByValueAndType` logs the type + the colliding ids if two rows match the same (value, type), but NOT the value itself (the value can be re-derived from the row in the catalogue audit log if a forensic step is needed). +- **Audit trail (`entity_create`, `entity_relations_batch_create`)** — explicit forensic exception per ADR-022. `value` IS persisted here. + +## RBAC + +Same model as the rest of OR's file-bound write endpoints: + +1. The file MUST resolve through the actor's `IRootFolder::getUserFolder($uid)->getById($fileId)` lookup (i.e. the file must be visible to the actor). +2. The resolved node MUST be a file (not a folder) and `isUpdateable()` MUST return true. + +Either check failing produces a 403 with `{ "error": "forbidden", "reason": "write access to file required" }`. There is no oracle between "file does not exist" and "file is not writable" — both produce 403/422 depending on the path (file-not-extracted → 422, write-denied → 403). This is the spec's no-oracle rule. + +## Spec references + +- Capability: [`openspec/changes/manual-entity-anonymisation/specs/entity-relation-grondslagen/spec.md`](../../openspec/changes/manual-entity-anonymisation/specs/entity-relation-grondslagen/spec.md) +- Design (matcher algorithm, audit, RBAC, idempotency invariants): [`openspec/changes/manual-entity-anonymisation/design.md`](../../openspec/changes/manual-entity-anonymisation/design.md) +- Tracking issue: [`#1593`](https://github.com/ConductionNL/openregister/issues/1593) +- ADR-005 (no PII in logs / error responses; UID not display name in audit payloads) +- ADR-022 (audit-trail for OR-owned mutations; forensic exception for `value` in audit payload only) +- Related: [Entity-Relation Decision Metadata](./entity-relation-decision-metadata.md) — operator decisions (`bases`, `skipAnonymization`) on individual relations. diff --git a/docs/Features/pdf-anonymisation.md b/docs/Features/pdf-anonymisation.md new file mode 100644 index 0000000000..2ac166af4d --- /dev/null +++ b/docs/Features/pdf-anonymisation.md @@ -0,0 +1,180 @@ +# PDF Anonymisation + +OpenRegister anonymises PDF inputs through the same `POST /api/files/{fileId}/anonymize` endpoint that handles DOCX / ODT / plain-text files. Previously the PDF branch fell through to a binary `str_ireplace` path that corrupted the output container; v1 of the `pdf-anonymisation` capability replaces that branch with a SAPP-driven byte-level replacement pipeline (Path A from the discovery doc) plus a smalot/pdfparser validation gate. + +This document describes the runtime behaviour. The architectural rationale (eight-approach evaluation matrix, hard constraints, SAPP fork strategy) lives in `openspec/changes/pdf-anonymisation/{proposal,design}.md` and `openspec/changes/pdf-anonymisation-discovery/discovery.md`. + +## Endpoint contract + +No new HTTP endpoints. PDF inputs route through the existing anonymise endpoint unchanged: + +``` +POST /api/files/{fileId}/anonymize +``` + +### Success response (HTTP 200) + +```json +{ + "success": true, + "message": "File anonymized successfully", + "original_file_id": 123, + "anonymized_file_id": 456, + "anonymized_path": "/files//Documents/Letter_anonymized.pdf", + "entities_replaced": 34 +} +``` + +### Failure response (PDF-specific, structured) + +For PDF inputs the controller maps `PdfAnonymisationException` reasons to HTTP statuses with a structured body: + +| Reason | HTTP | Meaning | +|-------------------------|------|------------------------------------------------------------------| +| `encrypted_pdf` | 422 | PDF is password-protected; out of scope for v1. | +| `text_layer_missing` | 422 | Image-only scan; defer to the OCR capability before retrying. | +| `validation_failed` | 500 | Pipeline ran but the output still contains entity text (strict). | +| `internal_error` | 500 | Unexpected SAPP / parser failure. | + +```json +{ + "success": false, + "error": "pdf_anonymisation_failed", + "reason": "validation_failed", + "details": { + "stage": "validate.assert", + "residual_count": 1, + "streams_scanned": 42, + "streams_modified": 18, + "tj_arrays_modified": 7, + "subset_font_fallbacks_used": 3, + "font_encoding_misses": 0, + "cid_split_mismatch": 0, + "encoding_dict_unhandled": 0, + "contents_array_pages": 0, + "rejected_substitutions": 0 + } +} +``` + +Per ADR-005 the `details` surface is PII-free by construction — counts and structural counters only. The operator-supplied entity text never appears in the response body or in error logs. + +## Hard constraints (locked from the discovery doc) + +The implementation MUST satisfy all five — alternative approaches (visual overlay, render-from-text, commercial libraries, LibreOffice CLI sidecar, flatten-and-OCR) were ruled out because no combination satisfies all five together. + +1. **No original PII in the output.** Every layer of the output PDF — visible text streams (Tj / TJ), hidden text, `/Info` dictionary, `/Metadata` XMP stream, outlines, annotations — MUST be free of the entity values from the substitution map. +2. **Identifiable placeholders.** Replacements take the form `[: ]` (the `entity-relation-grondslagen` convention). Pure black-bar redaction is ruled out — the placeholder cross-references the grondslagen summary. +3. **Layout preservation.** Tables stay structurally recognisable. Overflow within a cell, paragraph wrapping changes at the operator level, and minor visual shifts within a cell are acceptable; loss of table structure is not. +4. **FOSS only.** No commercial dependencies. +5. **No sidecars.** PHP-side processing only. + +## Pipeline shape + +``` +PDF input + │ + │ Encryption probe (trailer /Encrypt ref OR smalot "secured" message) + │ └── → REASON_ENCRYPTED_PDF (HTTP 422) + │ + │ Text-layer probe via smalot/pdfparser + │ └── no extractable text → REASON_TEXT_LAYER_MISSING (HTTP 422, + │ defer to OCR) + │ + │ SAPP loads via PDFDoc::from_string() + │ + │ replace_text_in_document(substitutions: longest-first map): + │ for each content-stream object, + │ decode FlateDecode (+ LZW / ASCII85 / ASCIIHex / RunLength as + │ upstream PRs land on the SAPP fork), + │ resolve the active font's encoding (WinAnsi / MacRoman / Standard + │ / Differences / Identity-H/V via ToUnicode CMap), + │ flatten TJ kerning arrays into single-Tj strings, + │ match per-font byte sequences against the substitution map, + │ on match emit /F-Replacement Tf () Tj + │ /F Tf + │ so the placeholder renders via Helvetica (PDF base font) even + │ when the original font is a subset. + │ + │ PdfMetadataSanitizer.sanitize(doc): + │ /Info → strip Title / Author / Subject / Keywords / Creator + │ (Producer / CreationDate / ModDate preserved). + │ /Metadata XMP → sentinel-replace dc:* / xmp:* / pdf:* element + │ bodies; preserve custom namespaces. + │ + │ SAPP serialises with rebuild=true (incremental mode produces an + │ unopenable PDF once _pdf_objects is populated by the replace API). + │ + │ VALIDATION GATE (PdfTextReplacer::validateOutput) + │ re-extract via smalot/pdfparser, + │ whitespace-normalise both haystack and each needle, + │ case-SENSITIVE mb_strpos for every substitution-map key, + │ on any residual: + │ emit PII-redacted warning (counts + structural counters only), + │ strict=true → fail closed (REASON_VALIDATION_FAILED, HTTP 500), + │ strict=false → return the partial output (docx parity, lenient + │ behaviour for ad-hoc replaceWords callers). +``` + +## Validation-gate semantics + +The validation gate is the single most important safety mechanism: it catches every silent-failure mode of byte-replace (encoding mismatches, missed splits, font edge cases) by detecting residual entity text in the output. + +Mode is selected by the caller (`PdfTextReplacer::validateOutput(..., bool $strict)`): + +- **Strict (`$strict = true`)** — the entity-anonymisation flow `anonymizeDocument` → `replaceWords(strict: true)`. A GDPR-anonymised file marked `_anonymized` MUST NOT be written while it still contains the original entity text. Fail-closed. +- **Lenient (`$strict = false`)** — ad-hoc `replaceWords`. Preserves parity with the DOCX path (PHPWord + `str_ireplace` returns a partial result silently when a needle is split across `` runs). Warning is logged structurally; the partial output is returned. + +Per the design's D6-amendment the REQ:no-residual-PII Requirement is mode-conditional: GDPR-critical anonymisation retains fail-closed semantics, ad-hoc replace stays lenient until DOCX receives a matching gate. Otherwise the asymmetry returns for ad-hoc callers. + +The validation gate uses **Choice α**: smalot re-extract + substring search. Re-extracted text matches what an automated downstream consumer would see. Choice β (full Presidio / openanonymiser round-trip) is reserved for a follow-up if α's blind spots surface in practice. + +## Encrypted-PDF rejection + +Encrypted PDFs raise `PdfAnonymisationException(reason: 'encrypted_pdf')` → HTTP 422. Detection runs twice: + +1. **Trailer probe.** A `/Encrypt N G R` indirect reference in the trailer dictionary anchors a regex that avoids false positives from the literal string appearing inside a content stream. +2. **smalot probe.** When the trailer probe missed but smalot rejects the input with a `secured` / `encrypt` message, the same `REASON_ENCRYPTED_PDF` is raised. + +Anonymising a password-protected PDF requires the password; v1 does not accept passwords. + +## Image-only PDF → defer to OCR + +PDFs whose text layer extracts to an empty string after `trim()` raise `PdfAnonymisationException(reason: 'text_layer_missing')`. The caller is expected to route to the `ocr-document-scanning` capability before retrying the anonymise call. v1 does NOT auto-redirect — the controller surfaces a structured 422 so the caller can choose its own dispatch. + +## Metadata sanitisation + +Strips PII-bearing fields in parity with the sister `office-document-sanitization` change: + +- `/Info` fields `/Title`, `/Author`, `/Subject`, `/Keywords`, `/Creator` → sentinel `[REDACTED]`. +- `/Info` fields `/Producer`, `/CreationDate`, `/ModDate` → preserved (provenance, no PII). +- `/Metadata` XMP stream → element bodies for `dc:*`, `xmp:*`, `pdf:*` namespaces sentinel-replaced; custom namespaces (Conduction-internal workflow metadata, government-template tags) preserved. + +## Operator escalation flow on validation failure + +When the strict-mode gate fails: + +1. The output PDF is **discarded** — never persisted, never returned. The original file is untouched. +2. A PII-redacted warning log line is emitted with the structural counters (`residual_count`, `streams_scanned`, `streams_modified`, `tj_arrays_modified`, `subset_font_fallbacks_used`, `font_encoding_misses`, `cid_split_mismatch`, `encoding_dict_unhandled`, `contents_array_pages`, `rejected_substitutions`). +3. The controller returns HTTP 500 with `{ "error": "pdf_anonymisation_failed", "reason": "validation_failed", "details": { ... PII-free counters ... } }`. +4. Operator escalates: the diagnostic counters tell ops which SAPP stage missed (font encoding, CID split, unhandled encoding dict, contents array page count, rejected substitution count). The follow-up `pdf-anonymisation-odt-fallback` (Path B) will slot in here once Path A's fall-through rate is measurable from real Woo PDFs. + +## Cross-app coordination + +- **DocuDesk `anonymise-output-as-pdf-by-default`.** PDF outputs from DocuDesk's anonymise flow are no longer wrapped around a corrupted byte-replace result. No DocuDesk-side change required. +- **`ocr-document-scanning`.** Image-only PDFs defer to the OCR capability — see the failure-mode table above. +- **Sister changes `office-document-sanitization` and `text-extraction-office-completeness`.** Orthogonal — they cover DOCX / ODT branches. + +## Composer dependency + +The PDF byte-replace pipeline uses the `ddn/sapp` fork hosted at `https://codeberg.org/Conduction/sapp`. While upstream PRs land, OpenRegister pins to a specific work-branch commit SHA via the VCS repository entry in `composer.json` for build reproducibility. When upstream `dealfonso/sapp` merges the work and tags a release, the `repositories` entry is removed and the constraint becomes a normal version range. + +The upstream-PR series tracking the fork→upstream transition lives at [`Conduction/sapp:docs/upstream-prs/`](https://codeberg.org/Conduction/sapp/tree/work/text-replacement/docs/upstream-prs). + +## Spec references + +- `openspec/changes/pdf-anonymisation/specs/pdf-anonymisation/spec.md` — canonical Requirement set. +- `openspec/changes/pdf-anonymisation/proposal.md` — what changes. +- `openspec/changes/pdf-anonymisation/design.md` — decisions (PoC scope, validation gate semantics, failure handling, XMP handling, upstream contribution policy). +- `openspec/changes/pdf-anonymisation-discovery/discovery.md` — eight-approach evaluation matrix. +- `openspec/changes/pdf-anonymisation-odt-fallback/` (follow-up, scaffolded) — Path B (NC Office ODT round-trip) reserved for once Path A's fall-through rate is measured. diff --git a/docs/Features/schema-import.md b/docs/Features/schema-import.md new file mode 100644 index 0000000000..9dadb8b928 --- /dev/null +++ b/docs/Features/schema-import.md @@ -0,0 +1,136 @@ +--- +title: Schema Import from Standards +sidebar_position: 31 +description: Import register schemas from external standards — Schema.org types and GGM objecttypes — with datatype mapping, pre-filled JSON-LD vocabulary, import provenance, and guarded update-from-source. +keywords: + - Open Register + - Schema Import + - Schema.org + - GGM + - Gemeentelijk Gegevensmodel + - JSON-LD +--- + +# Schema Import from Standards + +Open Register can build a register schema **from an external standard** +instead of hand-writing JSON Schema. Two dialects ship today — +[Schema.org](https://schema.org/) types and **GGM** (the VNG +*Gemeentelijk Gegevensmodel*) objecttypes — and the importer interface is +pluggable, so DCAT/SKOS/ZGW dialects are follow-up additions. + +Both vocabularies ship as **bundled, versioned snapshots** inside the app +(`lib/Resources/schemaorg/`, `lib/Resources/ggm/`). Imports are therefore +deterministic and require **no network egress** at import time. + +## Schema.org import + +Reference a type by IRI (`https://schema.org/Person`) or bare name +(`Person`). The importer: + +- maps each Schema.org property to a JSON Schema property, carrying its + description over; +- maps Schema.org datatypes to JSON Schema types/formats (see the table + below); +- imports the type's **direct** properties by default; ancestor + (inherited) properties are added only when `includeAncestors` is set; +- accepts an optional `propertySubset` (Schema.org types are large — + `Person` alone has dozens of properties) and reports any requested + property that does not exist on the type; +- **pre-fills** the schema's `configuration.jsonld` block (`@vocab`, + class IRI, per-property term map) per the + [JSON-LD output](./json-ld.md) contract — so JSON-LD output of the + schema's objects is Schema.org-conformant with zero manual mapping. + +| Schema.org range | JSON Schema | +|---|---| +| `Text` | `string` | +| `Number` / `Float` | `number` | +| `Integer` | `integer` | +| `Boolean` | `boolean` | +| `Date` | `string` / `format: date` | +| `DateTime` | `string` / `format: date-time` | +| `Time` | `string` / `format: time` | +| `URL` | `string` / `format: uri` | +| object-typed range (a class) | `string` / `format: uri` (a reference, never a recursive import) | +| multi-type range | the most permissive member (string wins) | + +```bash +# Discover importable types +GET /api/schema-import/schema.org/types?q=person + +# Import Person with a property subset into a register +POST /api/schema-import/schema.org +{ "reference": "Person", + "propertySubset": ["givenName", "familyName", "email", "birthDate"], + "targetRegister": 42 } +``` + +An unknown type reference returns **404** naming the reference. + +## GGM import + +Reference a GGM objecttype by id or Dutch name. The importer preserves +the Dutch metadata verbatim (objecttype name/definition → schema +title/description; each attribuutsoort name/definition → property +title/description), maps attribute types per the table below, turns a +referentielijst (with values present) into `enum`, and renders relations +as **single reference properties** (never recursive imports). + +| GGM type | JSON Schema | +|---|---| +| tekst | `string` | +| geheel getal | `integer` | +| decimaal | `number` | +| boolean | `boolean` | +| datum | `string` / `format: date` | +| datumtijd | `string` / `format: date-time` | +| relatie | `string` / `format: uri` (reference) | + +GGM can also be imported from an **uploaded GGM export** (admin-only), +normalised through the same intermediate that the bundled snapshot uses; +provenance then points at the upload. The bundled snapshot is produced by +`tools/generate-ggm-snapshot.php` from a published GGM release. + +## Dialect detection on upload + +The existing `POST /api/schemas/upload` (and `PUT /api/schemas/{id}/upload`) +ingestion path accepts an optional `dialect` parameter +(`json-schema` | `openapi` | `schema.org` | `ggm`). When absent, the +dialect is **detected** from unambiguous structural markers: + +- `$schema` / a JSON-Schema shape → `json-schema` +- `openapi` + `components` → `openapi` +- `@context` referencing schema.org → `schema.org` +- GGM export markers → `ggm` + +An explicit `dialect` always wins. **Input matching no dialect now fails +with HTTP 422** (listing the supported dialects) instead of being +silently mis-ingested as JSON Schema. Existing JSON Schema and OpenAPI +uploads behave exactly as before. + +## Provenance and update-from-source + +Every standards-imported schema records, in +`configuration.importSource`, the dialect, source identifier, snapshot +version, import timestamp, and the **imported baseline** property +definitions. Re-importing the same source updates the schema with a +three-way merge that never silently destroys local work: + +- `POST /api/schemas/{id}/reimport` returns a **diff preview** + (added / removed / changed / kept-local / conflicts) by default; +- pass `apply: true` to persist; +- **locally added** properties are kept; **locally modified** properties + whose source is unchanged are kept; a property changed **both** locally + and in the source is reported as a **conflict** and is not overwritten + until confirmed per-property via `resolveConflicts: ["name", ...]`; +- the apply flows through the standard schema-update path, so the + [schema versioning](../../openspec/specs/schema-migration/spec.md) + version bump / changelog / breaking-change gate apply as for any other + update. + +## Authorization + +All import endpoints are **admin-gated** (the same authority as creating a +schema): the Nextcloud SecurityMiddleware rejects non-admins before the +controller runs. diff --git a/docs/Integrations/Deck.md b/docs/Integrations/Deck.md deleted file mode 100644 index 3cfdcb5a53..0000000000 --- a/docs/Integrations/Deck.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: Deck Integration -sidebar_position: 2 -description: How OpenRegister integrates with the Nextcloud Deck app, including push events and card–object linking -keywords: - - OpenRegister - - Deck - - Kanban - - notify_push - - real-time ---- - -# Deck Integration - -OpenRegister integrates with the Nextcloud Deck app to allow Kanban-style task boards to be linked directly to register objects. Deck cards represent tasks or action items; by linking them to objects you get traceable work attached to your data. - -## Overview - -- Deck cards can be linked to any OpenRegister object via a link table managed by `DeckCardService`. -- When a Deck board or card changes, Deck fires `notify_custom` push events via `notify_push`. Frontend consumers can listen for these events to refresh linked-object views in real time. -- OpenRegister's own push events (`or-object-*` and `or-collection-*`) complement Deck events, providing object-level invalidation alongside card-level updates. - -## Backend integration - -| Class | Responsibility | -|---|---| -| `OCA\OpenRegister\Service\DeckCardService` | Creates, retrieves, and deletes Deck cards linked to OR objects; wraps the Deck REST API. | -| `OCA\OpenRegister\Controller\DeckController` | REST controller exposing card link endpoints under `/api/objects/{register}/{schema}/{uuid}/deck`. | - -The integration uses the Deck REST API (`/api/v1/boards`, `/api/v1/boards/{id}/stacks`, `/api/v1/boards/{id}/stacks/{stackId}/cards`) rather than the Deck PHP service layer, so it does not require Deck to be installed on the same Nextcloud instance — it can target a remote Deck instance. - -## Linking cards to objects - -A Deck card is linked to an OpenRegister object through the `deck` property on the `ObjectEntity`. The property holds an array of card-link descriptors: - -```json -{ - "@self": { "id": "550e8400-e29b-41d4-a716-446655440000" }, - "deck": [ - { - "boardId": 5, - "stackId": 12, - "cardId": 99, - "title": "Review permit application", - "url": "https://nextcloud.example.com/apps/deck#/board/5" - } - ] -} -``` - -`DeckCardService` is responsible for hydrating this array and for creating new cards via the API. - -The link is stored on the object itself (as part of the JSONB blob). There is no separate join table — the `deck` property is the canonical record of which cards are attached to which object. - -## Push events - -Deck emits `notify_custom` events via the Nextcloud `notify_push` app. Both constants live in `OCA\Deck\NotifyPushEvents`: - -| Event string | Fired when | Payload fields | -|---|---|---| -| `deck_board_update` | A board is updated, a member is added/removed, or ACL changes | `id` (board ID) | -| `deck_card_update` | A card is created, updated, moved, or deleted | `boardId`, `cardId` | - -These events are fired server-side by `OCA\Deck\Listeners\LiveUpdateListener`. Connected browser clients receive them via the `notify_push` WebSocket or SSE channel. - -**OpenRegister contrast:** OR emits the following event strings (see `OCA\OpenRegister\Push\PushEvents`): - -| Event string pattern | Fired when | -|---|---| -| `or-object-{uuid}` | Any object lifecycle event (create, update, delete) | -| `or-collection-{register-slug}-{schema-slug}` | Object created or deleted (collection invalidation) | - -Deck and OR events are independent streams. A frontend widget that shows an object with linked Deck cards should subscribe to both. - -## Subscribing from the frontend - -`@conduction/nextcloud-vue` provides composables for subscribing to `notify_push` events. Use the `useNotifyPush` composable (available from the upcoming `add-live-updates-plugin` change): - -```js -import { useNotifyPush } from '@conduction/nextcloud-vue' - -const { subscribe } = useNotifyPush() - -// Subscribe to object-level OR events -subscribe(`or-object-${objectUuid}`, ({ data }) => { - const payload = JSON.parse(data) - // refresh object view -}) - -// Subscribe to Deck card updates for the board that owns the linked card -subscribe('deck_card_update', ({ data }) => { - const { boardId, cardId } = JSON.parse(data) - // refresh card list for the affected board -}) -``` - -Cross-reference: the `add-live-updates-plugin` change in `nextcloud-vue` provides the full composable implementation including reconnection handling and per-user event routing. - -## Configuration - -The Deck integration requires: - -1. **notify_push installed and running** — check the admin settings Push Notifications section in OpenRegister for status. -2. **Deck app installed** — the integration uses Deck's REST API, which must be reachable. -3. **Admin credentials or app password** — `DeckCardService` uses the current user's session; board access follows Deck's own RBAC. - -## Related documentation - -- [n8n Integration](./n8n.md) — trigger n8n workflows on OR object events -- [Custom Webhooks](./custom-webhooks.md) — push events to arbitrary HTTP endpoints -- [Deck app documentation](https://github.com/nextcloud/deck) -- [notify_push configuration](https://github.com/nextcloud/notify_push#configuration) diff --git a/docs/Integrations/openproject-openconnector-source.yaml b/docs/Integrations/openproject-openconnector-source.yaml new file mode 100644 index 0000000000..73ac6c459b --- /dev/null +++ b/docs/Integrations/openproject-openconnector-source.yaml @@ -0,0 +1,153 @@ +# OpenConnector source template — OpenProject +# +# Import this into OpenConnector (Settings → OpenConnector → Sources → +# Import) and fill in the `location` + auth fields for your OpenProject +# instance. OpenRegister's `OpenProjectProvider` (id: `openproject`, +# group: `external`) routes all of its CRUD through this source via +# ExternalIntegrationRouter; it never holds credentials itself. +# +# (The repo's `config/` directory is gitignored, so this template +# lives under docs/Integrations/ — copy it to wherever your +# OpenConnector instance imports source definitions from.) +# +# OpenProject version notes: +# - OpenProject 12.x+ exposes a REST API v3 at `/api/v3`. Earlier +# versions used v2 (`/api/v2`) which is not supported — upgrade. +# - Authentication is OAuth2 in modern OpenProject deployments; +# long-lived API keys (Bearer token) remain available for +# server-to-server integrations on most instances. Pick ONE of the +# `auth` blocks below and delete the other. +# +# Spec: openspec/changes/integration-openproject/design.md +# - AD-1: OpenConnector source name is conventional (`openproject`). +# OpenProjectProvider::getOpenConnectorSource() returns 'openproject' +# — re-aliasing on the OpenConnector side is supported, but the +# provider doesn't probe for alternative names. +# - AD-2: No local caching beyond request-scope. OpenConnector may +# add its own caching layer; the provider doesn't duplicate it. +# - AD-3: Auth expiry surfaces an explicit "Reconnect" banner in the +# tab — the health probe must return `authStatus: 'expired'` when +# the OAuth token can't be refreshed; do not silently 401. + +source: + # Stable id — MUST be `openproject` (matches OpenProjectProvider::getOpenConnectorSource()). + reference: openproject + name: OpenProject + description: >- + OpenProject work packages linked to OpenRegister objects. Surfaced + as the "Projects" integration (sidebar tab + dashboard/detail + widgets) with status, type, priority, assignee, and project columns. + type: api + + # Base URL of the OpenProject REST API v3. Example for a community + # instance at https://op.example.org: + # https://op.example.org/api/v3 + location: "https://op.example.org/api/v3" + + # --------------------------------------------------------------- + # Authentication — keep exactly ONE of the two blocks below. + # --------------------------------------------------------------- + + # Option A — OAuth2 (preferred; AD-3 expiry banner requires this). + auth: + type: oauth2 + grantType: authorization_code + tokenUrl: "https://op.example.org/oauth/token" + authorizationUrl: "https://op.example.org/oauth/authorize" + clientId: "${OPENPROJECT_OAUTH_CLIENT_ID}" + clientSecret: "${OPENPROJECT_OAUTH_CLIENT_SECRET}" + scope: "api_v3" + + # Option B — API key / Bearer token (works for server-to-server). + # auth: + # type: bearer + # token: "${OPENPROJECT_API_KEY}" + + headers: + Accept: application/hal+json + + # --------------------------------------------------------------- + # Endpoint mapping consumed by OpenProjectProvider. The provider + # sends the object context as query params (register / schema / + # object) plus optional _search / _limit / _page; OpenConnector + # resolves those to the appropriate OpenProject REST calls and + # normalises the response rows. OpenProjectProvider::normalizeRow() + # additionally flattens hAL `_links` / `_embedded` envelopes onto + # top-level `type`, `priority`, `assignee`, `project` so the + # CnOpenprojectTab columns render without hand-walking nested data. + # --------------------------------------------------------------- + # + # OpenProject hAL+JSON shape — verified against OpenProject 13.x: + # GET /api/v3/work_packages/{id} → + # { id, subject, description: { raw, html }, lockVersion, + # _links: { self, status, type, priority, assignee, project, … }, + # _embedded: { status: { name, isClosed, … }, + # type: { name, color, … }, + # priority: { name, color, … }, + # assignee: { name, … }, + # project: { name, identifier, … }, … }, … } + # GET /api/v3/work_packages?filters=… → + # { _embedded: { elements: [WorkPackage, …] }, + # total, count, pageSize, offset, _links } + endpoints: + # GET — list linked work packages for an object. + # + # The pairing store — which OpenProject work packages are linked + # to which OR object — lives in OpenConnector's pairing model, + # keyed by the {register, schema, object} context. For instances + # without a pairing model, scope `list` to a hard-coded project + # filter and let `link-by-id` carry the rest. + list: + method: GET + path: "work_packages" + response: + rowsPath: "$._embedded.elements" + map: + id: "id" + reference: "id" + subject: "subject" + # _links titles are populated by OpenProject for every linked + # resource; safe to map directly. OpenProjectProvider::pickHalLabel + # also falls through to _embedded..name when a label is + # blank, so either side of the envelope works. + status: "_links.status.title" + type: "_links.type.title" + priority: "_links.priority.title" + assignee: "_links.assignee.title" + project: "_links.project.title" + url: "_links.self.href" + + # GET {id} — one linked work package. + get: + method: GET + path: "work_packages/{id}" + + # POST — link a work package. The body carries either `reference` + # (a numeric WP id or a full OpenProject URL — OpenConnector + # resolves both to canonical) or a complete WP payload for the + # create-and-link flow. + create: + method: POST + path: "work_packages" + + # PUT {id} — update a pairing. + update: + method: PUT + path: "work_packages/{id}" + + # DELETE {id} — unlink (removes the pairing; does NOT delete the + # work package in OpenProject). + delete: + method: DELETE + path: "work_packages/{id}" + + # Health probe used by OpenRegister's admin UI / OCS capabilities. + # A cheap authenticated GET that returns 200 when the instance is + # reachable and the credentials are valid; OpenProject's `/api/v3` + # root endpoint returns the API description without requiring any + # particular permission. Per AD-3, the health probe MUST surface + # `authStatus: 'expired'` when the OAuth token cannot be refreshed — + # do not silently 401. + healthCheck: + method: GET + path: "" diff --git a/docs/Patterns/collaborative-editing.md b/docs/Patterns/collaborative-editing.md index fbbc16d50f..3bf454c2ff 100644 --- a/docs/Patterns/collaborative-editing.md +++ b/docs/Patterns/collaborative-editing.md @@ -108,7 +108,7 @@ User A clicks Edit │ |---|---|---| | `notify_push` unreachable | Plugin falls back to polling | Subscriptions still work, latency increases | | Lock POST 401/403 | `useObjectLock.acquire()` rejects with `PermissionError` | Toast: "Concurrent edits are not blocked on this schema." Edit allowed without lock. | -| Lock POST 409/423 (conflict) | rejects with `LockConflictError` | Banner: "Locked by X until ." Edit disabled. | +| Lock POST 409/423 (conflict) | rejects with `LockConflictError` | Banner: "Locked by X until ``." Edit disabled. | | Network failure on release | `beforeunload` falls back to `navigator.sendBeacon`; OR's TTL expires the lock automatically | No UX impact. | | Lock holder inactive | Renew skipped while document hidden | Lock TTL elapses; on next focus, `acquire()` runs again. | diff --git a/docs/Technical/building-an-app-on-apphost.md b/docs/Technical/building-an-app-on-apphost.md new file mode 100644 index 0000000000..13eaa2354e --- /dev/null +++ b/docs/Technical/building-an-app-on-apphost.md @@ -0,0 +1,241 @@ +# Building an app on the AppHost + +The **AppHost** is OpenRegister's shared application runtime (ADR-040). It lets a +Conduction leaf app delete ~10 near-identical plumbing classes (dashboard/SPA +serving, settings API, per-user preferences, install repair steps, admin +settings panel, deep-link registration, route table) and replace them with a +single `Bootstrap::register()` call plus one `Routes::standard()` statement. + +This page covers the minimal leaf-app layout, the `Bootstrap` options, the +override cookbook, and the small stub floor that Nextcloud's class-name-based +instantiation forces to remain. + +> The observability half of the AppHost (health + metrics) is documented in +> [Declarative Observability](./declarative-observability.md). Both halves share +> the `OCA\OpenRegister\AppHost\` namespace and the same `Bootstrap`. + +## The complete generic set + +`Bootstrap::register()` aliases a leaf app's conventional class names to the +following engine-owned generics. Every class below ships under +`lib/AppHost/` — a `Bootstrap::register()` call with all standard options +enabled resolves each one (asserted by +`tests/Unit/AppHost/BootstrapFactoryChainTest::testAllFactoriesResolveToRealClassesWithFullOptions`), +so a leaf app can fully adopt the engine without leaving any plumbing class behind. + +| Generic class | Leaf alias | Role | +|---|---|---| +| `Controller\GenericDashboardController` | `Controller\DashboardController` | Serve the SPA page + history-mode catch-all. | +| `Controller\GenericPreferencesController` | `Controller\PreferencesController` | Per-user key/value UI preferences (`#[NoAdminRequired]`, user-scoped, `pref_` namespace). | +| `Controller\GenericSettingsController` | `Controller\SettingsController` | App settings API (admin-write, register binding stripped for non-admins). | +| `Controller\GenericHealthController` | `Controller\HealthController` | `GET /api/health` (observability; opt-out via `observability: false`). | +| `Controller\GenericMetricsController` | `Controller\MetricsController` | `GET /api/metrics` (Prometheus; observability). | +| `Service\AppHostSettingsService` | `Service\SettingsService` | App-scoped settings backed by `IAppConfig`. | +| `Service\GenericActionAuthService` | `Service\ActionAuthService` | Action authorization matrix (fails closed, admin-only default). | +| `Repair\GenericInitializeSettings` | `Repair\InitializeSettings` | Install repair step — seed default settings. | +| `Repair\GenericInitializeActions` | `Repair\InitializeActions` | Install repair step — seed default action matrix. | +| `Settings\GenericAdminSettings` | `Settings\AdminSettings` | `IDelegatedSettings` admin panel (#299). | +| `Settings\GenericSettingsSection` | `Sections\SettingsSection` | `IIconSection` admin section. | +| `Listener\GenericDeepLinkRegistrationListener` | `Listener\DeepLinkRegistrationListener` | Manifest-driven deep-link registration (opt-out via `deepLinks: false`). | + +## The minimal leaf app + +After adoption, a shell-only app contains: + +``` +appinfo/info.xml # id, version, navigation, , — NC requires it +appinfo/routes.php # return \OCA\OpenRegister\AppHost\Routes::standard(); +lib/AppInfo/Application.php # ~20 lines: APP_ID const + Bootstrap::register(...) +src/manifest.json # UI shell + observability + deepLinks blocks +lib/Settings/{app}_register.json (or register.d/ fragments) # data model +lib/actions.seed.json # optional ADR-023 action matrix seed +img/*.svg +templates/index.php # chunk loader (shared-vendor → shared-nc-vue → main) +``` + +### `lib/AppInfo/Application.php` + +```php + 'OCA\\PetStore', + 'sectionName' => 'Pet Store', + 'dashboardWidgets' => [\OCA\PetStore\Dashboard\ExampleWidget::class], + 'mcpProvider' => \OCA\PetStore\Mcp\ExampleToolProvider::class, + ]); + } + + public function boot(IBootContext $context): void {} +} +``` + +### `appinfo/routes.php` + +```php + 'pets#index', 'url' => '/api/pets', 'verb' => 'GET'], +]); +``` + +`$extra` routes are inserted **before** the SPA catch-all so they keep priority +over the `/{path}` fallback. A duplicate route name **within** `$extra` throws; +an `$extra` route whose name matches a canonical one **overrides** the canonical +entry (the documented way to re-point a single endpoint at a local controller). + +## `Bootstrap::register()` options + +| Option | Default | Purpose | +|---|---|---| +| `namespace` | StudlyCase guess from app id | The app's `OCA\X` base namespace. **Pass it explicitly** — the guess is only a fallback. Drives all sub-namespaces below. | +| `controllerNamespace` / `repairNamespace` / `settingsNamespace` / `sectionsNamespace` / `listenerNamespace` / `serviceNamespace` | derived from `namespace` | Override a single sub-namespace if the app deviates from convention. | +| `sectionId` | `$appId` | Admin settings section id. | +| `sectionName` | StudlyCase id | Admin section display name. | +| `sectionIcon` | `app-dark.svg` | Icon file in the app's `img/`. | +| `sectionPriority` / `adminPriority` | `75` / `10` | Settings ordering. | +| `dashboardWidgets` | `[]` | Widget classes to register. | +| `mcpProvider` | — | MCP tool provider class (ADR-034/035). | +| `deepLinks` | `true` | Register the manifest-driven deep-link listener. | +| `observability` | `true` | Alias the health + metrics controllers. | + +### Manifest `deepLinks` block + +The generic deep-link listener reads patterns from `src/manifest.json` instead +of hardcoded PHP: + +```json +{ + "deepLinks": [ + { + "registerSlug": "petstore", + "schemaSlug": "pet", + "urlTemplate": "/apps/petstore/#/pets/{uuid}", + "displayName": "Pet" + } + ] +} +``` + +## Why a disabled OpenRegister does not fatal Nextcloud + +Every registration in `Bootstrap` is a `registerService(name, Closure)`. The +closure body — which references `OCA\OpenRegister\AppHost\…` classes — runs only +when the app's DI container resolves that service, i.e. when a route is +dispatched. No OR class is referenced outside a closure in `Bootstrap` or +`Routes`. + +Consequently, with OpenRegister disabled or absent: + +- `Application::register()` completes without loading a single OR class → + **Nextcloud boots normally**. +- The first request to an aliased route triggers the closure, which cannot + autoload the generic class and surfaces as a **5xx JSON error** — the correct + degraded behaviour. The `/api/health` endpoint reports `orAvailable: failed`. + +## Override cookbook (extension-first) + +No AppHost generic is `final`; every app-specific behaviour sits behind a +`protected` hook. To replace one endpoint while keeping the rest generic, +subclass the generic in the leaf app and let `Bootstrap` alias your conventional +class name — or alias it yourself after calling `Bootstrap::register()`: + +```php +// lib/Controller/DashboardController.php +namespace OCA\PetStore\Controller; + +use OCA\OpenRegister\AppHost\Controller\GenericDashboardController; +use OCP\AppFramework\Http\TemplateResponse; + +class DashboardController extends GenericDashboardController +{ + protected function renderIndex(): TemplateResponse + { + // Add extra initial state, then defer to the generic template. + return parent::renderIndex(); + } +} +``` + +Then, in `Application::register()`, register your subclass *after* +`Bootstrap::register()` so it wins: + +```php +Bootstrap::register($context, self::APP_ID, ['namespace' => 'OCA\\PetStore']); +$context->registerServiceAlias( + \OCA\PetStore\Controller\DashboardController::class, + \OCA\PetStore\Controller\DashboardController::class // your concrete subclass +); +``` + +The same pattern applies to the settings service (`AppHostSettingsService::configKeys()`), +the action-auth service, the repair steps, the admin settings, and the deep-link +listener. + +## The stub floor (what cannot be deleted) + +Nextcloud instantiates a few classes **by class name** read from `info.xml`, +so those classes must physically exist in the app's namespace. `Bootstrap` +binds them to the generics via factory closures, so the stubs carry **no +logic** — they are one line each: + +```php +// lib/Repair/InitializeSettings.php +namespace OCA\PetStore\Repair; +use OCA\OpenRegister\AppHost\Repair\GenericInitializeSettings; +class InitializeSettings extends GenericInitializeSettings {} +``` + +The complete stub floor: + +| Stub | Referenced by | Why it must exist | +|---|---|---| +| `Repair\InitializeSettings` | `info.xml` `` | NC `RepairStep` loaded by class name. | +| `Repair\InitializeActions` | `info.xml` `` | same. | +| `Settings\AdminSettings` | `info.xml` `` + `#[AuthorizedAdminSetting(AdminSettings::class)]` | NC `ISettings` + attribute target. | +| `Sections\SettingsSection` | `info.xml` `` | NC `IIconSection` loaded by class name. | +| `Application.php` | NC app loader | resolves controllers via the app's DI container; hosts the `Bootstrap::register()` call. | +| `appinfo/routes.php` | NC router | only read from the app dir; one `return` statement. | + +Everything else — `DashboardController`, `PreferencesController`, +`SettingsController`, `HealthController`, `MetricsController`, `SettingsService`, +`ActionAuthService`, `DeepLinkRegistrationListener` — is provided by the generics +and needs **no** file in the leaf app unless you are overriding it. + +## Why not a Composer package? + +Shipping the AppHost as a `vendor/` Composer dependency was rejected: Nextcloud +loads every app into one shared PHP process, so two apps vendoring different +versions of the same `OCA\OpenRegister\AppHost\…` class would collide at the +class-loader level. Owning the classes in OpenRegister (a hard dependency of +every Conduction app already, per ADR-022) avoids the collision entirely. diff --git a/docs/Technical/declarative-observability.md b/docs/Technical/declarative-observability.md new file mode 100644 index 0000000000..a8cef4e1a7 --- /dev/null +++ b/docs/Technical/declarative-observability.md @@ -0,0 +1,206 @@ +--- +title: Declarative Observability (AppHost) +sidebar_position: 3 +description: Declare health checks and Prometheus metrics as JSON in your app manifest; OpenRegister's AppHost engine serves the endpoints. +keywords: + - Open Register + - AppHost + - Observability + - Prometheus + - Health check + - ADR-040 +--- + +# Declarative Observability (AppHost) + +OpenRegister's **AppHost observability engine** lets a Conduction app declare its +health checks and Prometheus metrics as a JSON `observability` block in +`src/manifest.json`. OpenRegister executes the descriptors and serves the +responses through generic, reusable controllers — so an adopting app ships +**zero hand-written observability PHP** while keeping its existing +`/apps/{appid}/api/health` and `/apps/{appid}/api/metrics` URLs and the ADR-006 +contract. + +This is the implementation of **ADR-040 (declarative observability)**. It +specialises ADR-006 (the endpoint contract, now enforced centrally) and reuses +the ADR-035 provider-alias discovery pattern for the escape hatch. + +## The manifest `observability` block + +```jsonc +"observability": { + "health": { + "statusCodePolicy": "adr006", // "adr006" (default): 503 when any critical check fails, 200 otherwise + // "always200": always HTTP 200, status field carries ok|degraded|error + "cors": false, // true adds Access-Control-Allow-* headers (reverse-proxy probe contract) + "checks": [ + { "id": "database", "type": "database" }, + { "id": "filesystem", "type": "filesystem", "severity": "degraded" }, + { "id": "openregister", "type": "appEnabled", "app": "openregister", "severity": "critical" }, + { "id": "config", "type": "appConfig", "key": "token_set", "assert": "nonEmpty", "severity": "degraded" }, + { "id": "or", "type": "orAvailable" } + ] + }, + "metrics": [ + // {app}_info and {app}_up are implicit — never declared. + { "name": "cases_total", "type": "gauge", "help": "Cases by status", "cacheTtl": 30, + "source": { "kind": "objectCount", "register": "procest", "schema": "zaak", "groupBy": ["status"] } }, + { "name": "cases_overdue_total", "type": "gauge", + "source": { "kind": "objectCount", "schema": "zaak", "filter": { "deadline": { "lt": "now" } } } }, + { "name": "leads_value_total", "type": "gauge", + "source": { "kind": "objectSum", "schema": "lead", "field": "value", "groupBy": ["pipeline"] } }, + { "name": "widgets_total", "type": "gauge", + "source": { "kind": "tableCount", "table": "launchpad_widget_placements" } }, + { "name": "dashboards_total", "type": "gauge", + "source": { "kind": "tableCount", "table": "launchpad_dashboards", + "groupBy": ["type"], "labelDefaults": { "type": "personal" }, + "labelMap": { "type": "kind" }, "filter": { "status": { "eq": "active" } } } }, + { "name": "pdf_generations_total", "type": "counter", + "source": { "kind": "appConfig", "key": "pdf_generations_total" } }, + { "name": "customer_bridge", "source": { "kind": "provider" } } + ] +} +``` + +If the block is **absent**, the app still gets compliant endpoints: a `database` +health check (plus `orAvailable` when the manifest declares OpenRegister +registers) and the implicit `{app}_info` / `{app}_up` metrics. + +## Health check types (closed set) + +| `type` | Params | Semantics | +|---|---|---| +| `database` | — | `SELECT 1` via `IDBConnection` | +| `filesystem` | — | write + unlink a temp file | +| `appEnabled` | `app` | `IAppManager::isInstalled` | +| `appConfig` | `key`, `assert` (`present` \| `nonEmpty`) | `IAppConfig` read | +| `orAvailable` | — | resolve OpenRegister's `ObjectService`, non-null | + +Each check carries a `severity`: `critical` (default — failure drives status +`error` and HTTP 503 under `adr006`) or `degraded` (failure drives status +`degraded`, still HTTP 200). + +Response shape: + +```json +{ "status": "ok|degraded|error", "app": "myapp", "version": "1.2.3", + "checks": { "database": "ok", "config": "failed: config key empty" } } +``` + +Check results are only ever `ok` or `failed[: ]`; exception +messages are logged, never returned. + +## Metric source kinds (closed set + escape hatch) + +| `kind` | Params | Executes as | +|---|---|---| +| `objectCount` | `register`, `schema` (slugs), `groupBy[]`, `filter` | OpenRegister portable aggregation (no raw JSON SQL) | +| `objectSum` | as above + `field` (numeric JSON field) | OpenRegister aggregation SUM | +| `tableCount` | `table`, `groupBy[]`, `filter`, `labelDefaults`, `labelMap` | QueryBuilder COUNT / GROUP BY (aggregate-only) | +| `appConfig` | `key` | `IAppConfig::getValueInt` | +| `provider` | — | merge the app's registered `IMetricsProvider` output | +| *(implicit)* | — | `{app}_info`, `{app}_up` | + +`filter` operators: `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `like`, with the +server-side date tokens `now` and `today`. + +`cacheTtl` (seconds, default `0`): the engine memoises the rendered sample set +per metric via the distributed cache (key = appId + metric name). + +### `tableCount` security + +`tableCount.table` MUST match `^[a-z0-9_]+$` — the allowlist regex is enforced +both at manifest-validation time and (defence in depth) inside the source. The +table name is passed through the QueryBuilder table API (never string +concatenation), all `groupBy`/`filter` columns are validated identifiers, and +the engine only ever emits aggregate counts — never row data. + +## Engine-owned contract + +- The **health endpoint is public** (`#[PublicPage]` + `#[NoCSRFRequired]`); the + **metrics endpoint is admin-only**. Leaf apps cannot drift this through the + manifest. +- The **exposition format** is Prometheus text 0.0.4 (`# HELP` / `# TYPE`, + `{app}_` prefix sanitised, label escaping), `Content-Type: + text/plain; version=0.0.4`. +- Unknown check types, source kinds, or filter operators are reported as + manifest diagnostics and fall back to defaults — never a runtime 500. + +## Escape hatch + +Anything that cannot be expressed by a descriptor goes through a provider: + +- `OCA\OpenRegister\AppHost\IHealthCheckProvider` — `check(): array` of + `{id => {ok, severity?, message?}}`. +- `OCA\OpenRegister\AppHost\IMetricsProvider` — `metrics(): MetricSample[]`. + +Register the implementation on your app's container under the alias +`OCA\OpenRegister\AppHost\IMetricsProvider::{yourAppId}` (the ADR-035 pattern); +a `{"kind":"provider"}` metric descriptor then merges its output into the +generic response. + +## Adopting in a leaf app + +1. Add the `observability` block to `src/manifest.json`. +2. Point your existing routes at the generic controllers via container service + aliases in your `Application.php` (URLs do not change): + + ```php + $context->registerServiceAlias( + 'OCA\\YourApp\\Controller\\HealthController', + \OCA\OpenRegister\AppHost\Controller\GenericHealthController::class + ); + $context->registerServiceAlias( + 'OCA\\YourApp\\Controller\\MetricsController', + \OCA\OpenRegister\AppHost\Controller\GenericMetricsController::class + ); + ``` + +3. Delete the hand-written `HealthController` / `MetricsController`. + +The controller's `$appName` (your app id) is what the engine uses to load your +manifest, so the same OpenRegister engine serves every app correctly. + +## Migration table + +| Old hand-written pattern | Declarative replacement | +|---|---| +| `SELECT 1` in `HealthController` | `{"type":"database"}` | +| temp-file write check | `{"type":"filesystem"}` | +| `isEnabledForUser` check | `{"type":"appEnabled","app":"..."}` | +| OR DI lookup health check | `{"type":"orAvailable"}` | +| PHP-side object JSON aggregation | `{"kind":"objectCount"/"objectSum", ...}` | +| `LIKE '%pattern%'` schema match | `schema` slug in the descriptor | +| own-table COUNT queries | `{"kind":"tableCount","table":"..."}` | +| appconfig counter | `{"kind":"appConfig","key":"..."}` | +| JSON metrics output | engine renders Prometheus 0.0.4 | +| public metrics / auth'd health drift | engine-owned auth posture | + +## OpenRegister runs on its own engine (the living example) + +OpenRegister is the **first adopter** of the AppHost observability engine — it +dogfoods its own engine. The ~520 lines of hand-written `HealthController` + +`MetricsController` have been deleted; OR's `/apps/openregister/api/health` and +`/apps/openregister/api/metrics` routes are aliased at +`AppHost\Controller\GenericHealth#index` / `GenericMetrics#index` in +`appinfo/routes.php`, and the descriptors live in the `observability` block of +[`src/manifest.json`](https://github.com/ConductionNL/openregister/blob/development/src/manifest.json). +That block is the canonical, working reference for every other Conduction app +adopting the engine: two health checks (`database` critical, `filesystem` +degraded) and nine metric descriptors (registers/schemas/objects counts, the +four audit-trail CRUD counters via `tableCount` + `action` filter, the +search-request counter, and the webhook-delivery counter). + +The endpoint contract is **unchanged** from the deleted controllers — the same +metric names, types and label keys, verified by byte-comparing the Prometheus +output and the health JSON before vs after adoption on a seeded instance. Three +intentional improvements ride along with the engine: + +- the health JSON envelope now carries an `app` field (`{status, app, version, checks}`); +- the implicit `{app}_info` gauge gains a `nextcloud_version` label alongside `version` + `php_version`; +- `/api/health` is now public (`#[PublicPage]`, ADR-006 anonymous health) instead of login-gated. + +One label-semantics note for `objects_total`: the engine's `tableCount` +`groupBy: ["register","schema"]` emits the numeric register/schema ids as label +values (the bespoke controller joined to the title columns). Use the +`objectCount` source kind if you need slug/title labels in your own app. diff --git a/docs/api/objects.md b/docs/api/objects.md index 84cceda5a5..15a6189b4e 100644 --- a/docs/api/objects.md +++ b/docs/api/objects.md @@ -606,6 +606,7 @@ When creating or updating objects, you can explicitly set certain @self metadata - **`organisation`**: Organization UUID - **`published`**: Publication timestamp - **`depublished`**: Depublication timestamp +- **`folder`**: Numeric Nextcloud folder ID to bind the object to (see access-control contract below) Example: ```json @@ -621,6 +622,65 @@ Example: For detailed information about @self metadata handling, see [Self Metadata Handling](../development/self-metadata-handling.md). +### `@self.folder` access-control contract + +The `@self.folder` metadata field binds an object to an existing Nextcloud folder +by node ID. The bind is governed by an access-control check on every save: + +- **Empty / absent** — the system creates a new folder under the register's root + folder and stores the new node ID on the object. (Default behaviour, unchanged.) +- **Legacy non-numeric** (path-style strings from older installs) — auto-create + proceeds as before; no access check runs. +- **Non-empty numeric** (the format produced by current `@self.folder` writes) — + the acting user MUST be able to read the folder. The check uses the user's + user-folder mount and `Folder::isReadable()`. If either fails — folder doesn't + exist in the user's mount, the resolved node is a file, the folder is trashed, + or the user has no read permission — the save is rejected. + +#### Denial response shape + +When `@self.folder` is rejected, the endpoint returns **HTTP 403** with body: + +```json +{ + "error": "folder_access_denied" +} +``` + +The attempted node ID is intentionally **not** echoed in the response body — +echoing it would turn the endpoint into a folder-ID enumeration oracle for an +attacker. The attempted ID is recorded server-side in the audit trail +(`AuditTrail` table, action `folder_access_denied`) for forensic review by +operators with audit-log access. The check applies uniformly across `POST` +(create), `PUT` (update), and `PATCH` (partial update) on object endpoints. + +#### Acting user resolution ("self") + +The check resolves the acting user in this order: + +1. The `IUser` explicitly passed to the underlying service helper (DI / cron path). +2. The session user (`IUserSession::getUser()`). +3. If neither resolves, the bind is **denied** by default — there is no fail-open + path on `@self.folder` writes. + +#### Audit trail + +Every denial writes a forensic audit-trail entry with `action: "folder_access_denied"`, +the actor (UID or `"system"`), the attempted folder ID, and a reason code. The +entry is written **before** the exception is thrown, so even a caller that +catches the exception has a record. Audit-write failures are logged at warning +level and do **not** swallow the denial — denial is authoritative. + +For cleanup of stale `@self.folder` references on existing objects (folders the +owner can no longer access), an `occ openregister:folder-audit` command is +tracked separately as a follow-up. + +#### See also + +- Capability spec (post-archive): `openspec/specs/self-folder-access-control/spec.md` +- Architectural context: ADR-007 (Security and Auth), ADR-008 (Backend Layering) +- Downstream consumer benefiting from this hardening: DocuDesk's `add-dossier-schema` change. + ## Security - **RBAC**: Respects role-based access control diff --git a/docs/development-notes/custom-share-tokens-exploration.md b/docs/development-notes/custom-share-tokens-exploration.md new file mode 100644 index 0000000000..0439473a33 --- /dev/null +++ b/docs/development-notes/custom-share-tokens-exploration.md @@ -0,0 +1,137 @@ +# Exploration: custom tokens for public share links + +**Date:** 2026-06-17 +**Status:** Exploration only — nothing implemented. Captures findings so the +approach is ready when we pick it up. + +## Question + +Nextcloud can let you set a **custom token** for a public file-share link (the +`…/s/` part of the URL), gated by a Share-API admin setting. Can we +(a) detect from the PHP backend whether that capability is available/enabled, +and (b) set a custom token when creating a share? + +**Answer: yes to both.** Detection is a one-line OCP call; setting the token +needs a two-step create→update because the share manager overwrites the token +at create time. + +All findings verified against the Nextcloud server checkout in this workspace +(OCP ^31). + +## Detection + +`OCP\Share\IManager::allowCustomTokens(): bool` (public OCP method). + +It reads the app-config value (app `core`): + +| Key | `shareapi_allow_custom_tokens` | +|-----|--------------------------------| +| Store | `oc_appconfig`, app `core` (NOT system config / config.php) | +| Type | bool | +| Default | `false` (true only on the FAMILY / PRIVATE install presets) | +| Admin UI | Sharing settings → *"Allow users to customize share URL"* | +| occ | `occ config:app:set core shareapi_allow_custom_tokens --value yes` | + +- It is also surfaced to clients via the `files_sharing` capability + `public.custom_tokens` (`apps/files_sharing/lib/Capabilities.php` → + `$shareManager->allowCustomTokens()`). +- For older-NC safety, guard with `method_exists($shareManager, 'allowCustomTokens')` — + the method only exists on versions shipping the feature (we're on OCP ^31, so + it's present). + +## How setting a custom token works + +### The gotcha: create-time tokens are overwritten + +`OC\Share20\Manager::createShare()` **unconditionally** assigns a freshly +generated token for link/email shares: + +```php +// lib/private/Share20/Manager.php (~line 673, inside createShare) +$token = $this->generateToken(); +$share->setToken($token); +``` + +So calling `$share->setToken($custom)` *before* `createShare()` does nothing — +it gets overwritten. `updateShare()` does NOT regenerate the token, so the +custom token has to be applied as a **second step after create**. + +This mirrors core itself: the OCS `ShareAPIController` applies the custom token +on the **update** path, not create +(`apps/files_sharing/lib/Controller/ShareAPIController.php` ~line 1358). + +### Validation (enforced by the OCS controller, NOT the manager) + +Core's `ShareAPIController::validateToken()`: +- non-empty (`mb_strlen($token) > 0`) +- charset `^[a-z0-9-]+$/i` → letters, numbers, hyphen only + +Plus it checks `allowCustomTokens()` first (403 if disabled). Uniqueness is +**not** auto-checked on the custom path — a collision would hit the DB token +index. The manager's `updateShare()` just persists whatever token is set, so a +backend calling the manager directly must do all of this itself. + +### Recommended backend pattern + +```php +use OCP\Share\IManager; +use OCP\Share\IShare; +use OCP\Constants; +use OCP\Share\Exceptions\ShareNotFound; + +// 1. Detect (guard for older NC). +if (!method_exists($this->shareManager, 'allowCustomTokens') + || !$this->shareManager->allowCustomTokens()) { + // Feature off/unavailable — fall back to the default random token. +} + +// 2. Validate the desired token (mirror core's rules). +if (preg_match('/^[A-Za-z0-9-]+$/', $custom) !== 1) { + throw new \InvalidArgumentException('Invalid share token'); +} + +// 3. Uniqueness pre-check (core does NOT auto-check the custom path). +try { + $this->shareManager->getShareByToken($custom); + // token already taken +} catch (ShareNotFound $e) { + // free +} + +// 4. Create the link share — gets a random token. +$share = $this->shareManager->newShare(); +$share->setNode($node) + ->setShareType(IShare::TYPE_LINK) + ->setSharedBy($uid) + ->setPermissions(Constants::PERMISSION_READ); +$share = $this->shareManager->createShare($share); + +// 5. Swap in the custom token. +$share->setToken($custom); +$share = $this->shareManager->updateShare($share); +``` + +## Relevant OCP surface (all public on `OCP\Share\IManager`) + +- `allowCustomTokens(): bool` — detection +- `getShareByToken(string $token): IShare` — uniqueness check (throws `ShareNotFound`) +- `generateToken(): string` — the default random generator +- `OCP\Share\IShare::setToken($token)` / `getToken()` + +## Source references (Nextcloud server, OCP ^31) + +- `lib/public/Share/IManager.php` — `allowCustomTokens` (464), `getShareByToken` + (179), `generateToken` (532) +- `lib/private/Share20/Manager.php` — `createShare` token overwrite (~673), + `allowCustomTokens` impl (~1847) +- `core/AppInfo/ConfigLexicon.php` — `SHARE_CUSTOM_TOKEN = 'shareapi_allow_custom_tokens'` +- `apps/files_sharing/lib/Controller/ShareAPIController.php` — custom-token + update gate + `validateToken()` (~1358, ~1401) +- `apps/files_sharing/lib/Capabilities.php` — `public.custom_tokens` (~147) + +## Not decided / next steps + +- Where it would live (OpenRegister `FileService` vs DocuDesk) and the use case + (e.g. predictable links for anonymised documents) — not chosen yet. +- A small reusable helper (detect → validate → create-then-update) was offered + but not built. diff --git a/docs/development-notes/integration-registry-vs-reference-provider.md b/docs/development-notes/integration-registry-vs-reference-provider.md new file mode 100644 index 0000000000..6f2ca2b9aa --- /dev/null +++ b/docs/development-notes/integration-registry-vs-reference-provider.md @@ -0,0 +1,114 @@ +# Decision Record: Integration Registry ↔ IReferenceProvider Convergence + +**Status:** Decided · **Date:** 2026-06-15 · **Operationalizes:** ADR-041 Decision #3 +(the deferred open question) · **Type:** Investigation / spike — no production code changed. + +> ADR-041 §3 recommends that cross-app *rendering* of linked things SHOULD align with NC's +> native `OCP\Collaboration\Reference\IReferenceProvider`, while the OpenRegister integration +> registry keeps its bespoke layer only for the CRUD it genuinely adds beyond read-only +> references — and defers "whether and how it converges" to a dedicated investigation. This is +> that investigation. + +## Headline recommendation + +**KEEP-SEPARATE-BUT-ALIGN.** Do **not** converge the OpenRegister integration registry +(ADR-019 leaf system) onto `IReferenceProvider`. Keep the two mechanisms separate — each is the +correct shape for its job — and align the seam between them. + +- **Cross-app *render-a-link* → `IReferenceProvider`.** OpenRegister already does this with + `OCA\OpenRegister\Reference\ObjectReferenceProvider` (registered via + `registerReferenceProvider`). Future "show app X's record as a rich card in app Y" SHOULD be a + reference provider in the *owning* app, not a leaf in OR's registry. +- **Object-scoped *CRUD-over-linked-things* → OR integration registry.** The write verbs + (`create/update/delete`), the `openregister_*_links` tables, the `(register, schema, objectId)` + scoping, and the OpenConnector transport are genuine added value with no `IReferenceProvider` + equivalent. Bound it (ADR-041 §2); do not replace it. + +## Why (responsibilities matrix) + +| OR registry responsibility | Bucket | Covered by `IReferenceProvider`? | +|---|---|---| +| `list(register,schema,objectId)` | READ, object-scoped | No — no "list the X of object Y" verb | +| `get(...entityId)` | READ/RENDER | Partial — returns full entity, not a preview card | +| `create / update / delete` | **VALUE-ADD** | No — reference providers are read-only | +| `openregister_*_links` tables + magic-column | **VALUE-ADD** | No — references are stateless | +| `(register, schema, objectId)` scoping | **VALUE-ADD** | No — no object-graph binding exists | +| `getStorageStrategy` + `ExternalIntegrationRouter` (OpenConnector) | **VALUE-ADD** | No — no transport abstraction | +| `getId/getLabel/getIcon/getGroup` discovery metadata | READ | Yes → `getId/getTitle/getIconUrl/getOrder` | +| render a link to an OR object in another app | READ/RENDER | **Yes — already done** by `ObjectReferenceProvider` | + +The overlap is the narrow *render-a-link* edge, and it is **already served** by an +`IReferenceProvider`. Everything that makes the registry distinct has no reference-provider +analogue. Forcing the value-adding 90% (CRUD, link tables, scoping) through a read-only contract +would be a semantic abuse analogous to the `GetTaskProcessingProvidersEvent` abuse ADR-041 +rejects. + +## Contract comparison (the shape mismatch) + +- `IReferenceProvider`: `matchReference` / `resolveReference → IReference` + `getCachePrefix` / + `getCacheKey`. Cross-app by design (`registerReferenceProvider`), **read-only**, first-class + caching via `IReferenceManager`. `IReference` is a *preview* (title/description/image/url/ + richObject), not a mutable entity. +- OR `IntegrationProvider`: `list/get/create/update/delete` + metadata, `(register,schema, + objectId)`-scoped, registered **only** from OR's own boot (no sibling-app contribution hook). + Storage strategies observed across the 22 built-ins: `link-table` ×19, `external` ×4, + `query-time` ×3, `magic-column` ×1. CRUD-capable leaves include Notes, Tasks, Contacts, Deck, + Email, OpenProject, Xwiki, Files, Tags, Calendar, Shares. + +One line: `IReferenceProvider` = "render a link to a thing identified by URL/text" (cross-app, +cached, read-only); OR registry = "manage the things linked **to this object**" (object-scoped, +CRUD, persisted links). + +## Migration blast radius (why full convergence is rejected) + +| Dependent | Full-convergence | Keep-separate-but-align | +|---|---|---| +| Manifest `referenceType` markers (`single-entity` widgets) | **BREAK** — expect entity `get()`, not a preview | Transparent | +| 22 built-in providers (5 builtin + 17 providers) | **BREAK/rewrite** — split read vs write per leaf | Preserve | +| Frontend `useIntegrationRegistry` / `CnObjectSidebar` / `CnIntegrationCard` / `CnFormDialog` | **BREAK** — object-scoped CRUD has no reference model | Transparent | +| ADR-019 (registry pattern, Tier-2 parity, 3-stage filter) | **CONTRADICT** accepted ADR | Preserve + clarify | +| ADR-036 (universal widget manifest v2, `single-entity` surface) | **BREAK** | Transparent | +| `openregister_*_links` (×17) + migrations | **ORPHAN/migrate** | Preserve | + +Full convergence is a fleet-wide breaking change for a benefit already delivered. Cost/benefit +favors keep-separate-but-align decisively. + +## Phased follow-up (alignment — small, optional, additive) + +- **Phase A (this record):** declare the boundary — references for cross-app render, registry for + object-scoped CRUD. +- **Phase B (future, additive):** cross-app render needs (e.g. decidesk/docudesk cards in a + sibling app) ship an `IReferenceProvider` in the **owning** app — not a leaf. No OR change. +- **Phase C (future, optional):** a leaf's read path MAY delegate preview rendering to + `IReferenceManager::resolveReference()` instead of bespoke preview code. Per-leaf, reversible. +- **Phase D (guard):** a gate flagging any new leaf whose sole purpose is cross-app *rendering* + (should be a reference provider), complementing the ADR-041 anti-RPC gate. + +## Risks + +- **R1** Two-mechanisms confusion — mitigated by the one-line rule + Phase-D guard. +- **R2** Preview duplication drift if Phase C is skipped — cosmetic, low severity. +- **R3** Temptation to re-add cross-app contribution to the registry — explicitly forbidden by + ADR-041 §2; reaffirmed here. + +## Read-only PoC (illustration only, not wired) + +```php +// ILLUSTRATIVE — not added to any boot path. A leaf read path could reuse NC's +// cached, cross-app reference machinery for previews without any registry change: +$ref = $this->referenceManager->resolveReference($url); // IReferenceManager +return $ref === null ? null : [ + 'title' => $ref->getTitle(), + 'description' => $ref->getDescription(), + 'imageUrl' => $ref->getImageUrl(), + 'richObject' => $ref->getRichObject(), +]; +``` + +## Decision + +**KEEP-SEPARATE-BUT-ALIGN.** The ADR-041 deferred open question is resolved: do not converge. +`IReferenceProvider` is the cross-app render-a-link mechanism (already in use); +the OR integration registry is the object-scoped CRUD-over-linked-things mechanism (bounded by +ADR-041, not replaced). Align the seam via the optional additive phases above. No registry code +is changed by this investigation. diff --git a/docs/development-notes/object-slugs-and-slug-routing-exploration.md b/docs/development-notes/object-slugs-and-slug-routing-exploration.md new file mode 100644 index 0000000000..7bdfcfc978 --- /dev/null +++ b/docs/development-notes/object-slugs-and-slug-routing-exploration.md @@ -0,0 +1,87 @@ +# Exploration: object slugs & slug-based routing (OpenRegister + OpenCatalogi) + +**Date:** 2026-06-17 +**Status:** Exploration only — nothing implemented. Captures findings + the two +changes that would make object-slug URLs work, so they're ready to pick up. + +## Questions + +1. Does OpenRegister auto-generate `slug`s on objects, or do we generate them? +2. Can an object be fetched via `/{register-slug}/{schema-slug}/{object-slug}` + (OpenRegister) and `/{catalogue-slug}/{object-slug}` (OpenCatalogi)? + +## Findings + +### 1. Object slugs are NOT auto-generated + +- `ObjectEntity` has a nullable `slug` field (`@self.slug`), documented as + "unique within register+schema". +- **Nothing populates it.** `->setSlug(` is called **nowhere** in OpenRegister + `lib/`, and the save path (`Service/Object/SaveObject.php`) has no slug logic. + There's no slugify helper and no auto-fill from `name`/`title`. +- It CAN be set manually: `ObjectEntity::hydrate()` maps any input key to its + setter, so including `'slug' => '…'` in the saved object data calls + `setSlug()` and stores it. Or call `$object->setSlug($slug)` before persisting. +- Uniqueness is documented intent only — not generated, not enforced. A caller + that sets slugs must ensure uniqueness (and collision handling) itself. + +### 2. Slug routing + +**OpenRegister — slug-aware end to end (but see caveat).** +- Route `objects#show`: `GET /index.php/apps/openregister/api/objects/{register}/{schema}/{id}` + (note the `/api/objects/` prefix — it is NOT a bare `/{register}/{schema}/{id}`). +- `{register}` / `{schema}` → resolved by **slug** (or id/uuid) via + `resolveRegisterSchemaIds`. +- `{id}` → resolved via `ObjectService::find()`, which matches **id OR uuid OR + slug** (`orX(id, uuid, slug)`). +- So `…/api/objects/{register-slug}/{schema-slug}/{object-slug}` **resolves** — + **but only for objects that actually have a slug set** (see finding 1). + Register/schema slugs are seeded, so those segments work today regardless. + +**OpenCatalogi — catalog by slug, object by UUID only.** +- Route `publications#show`: `GET /index.php/apps/opencatalogi/api/{catalogSlug}/{id}`. +- `{catalogSlug}` → by **slug** (`[a-z0-9-]+`, `getCatalogBySlug`). +- `{id}` → resolved **strictly by UUID**: it does `searchObjects` with a + `@self.uuid` filter (deliberately avoids `find()` because of a `deleted IS + NULL` condition that trips on objects with `deleted: []`). **Slug is not + queried.** +- So `/{catalogue-slug}/{object-slug}` does **NOT** work — only + `/{catalogue-slug}/{object-uuid}`. + +## What it takes to support object-slug URLs + +1. **OpenRegister — add slug generation** (the lookup already supports slugs): + - In the `SaveObject` path, when no slug is supplied, slugify a source field + (e.g. `name`/`title`) and ensure uniqueness **within register+schema** + (suffix `-2`, `-3`, … on collision). Then + `/api/objects/{register-slug}/{schema-slug}/{object-slug}` works. + - Decide: generate always, or only when the schema opts in + (`configuration.objectNameField` already exists as a related concept). + +2. **OpenCatalogi — make `PublicationsController::show()` slug-aware**: + - Currently filters `@self.uuid = {id}`. Add a slug branch (also filter/try + `@self.slug`), or switch to the slug-aware `ObjectService::find()` — while + handling the `deleted IS NULL` quirk the current code is avoiding. + - Depends on (1): an object needs a slug before it can be looked up by one. + +## Source references + +OpenRegister: +- `lib/Db/ObjectEntity.php` — `slug` field (~169), `hydrate()` (~818) +- `lib/Service/Object/SaveObject.php` — no slug logic (confirms not generated) +- `lib/Service/ObjectService.php` — "find() supports slugs via orX(id, uuid, + slug)" (~444, ~504) +- `lib/Controller/ObjectsController.php` — `show()` (~2093): + `resolveRegisterSchemaIds` + `objectService->find(id: $id, …)` +- `appinfo/routes.php` — `objects#show` `/api/objects/{register}/{schema}/{id}` (~630) + +OpenCatalogi: +- `appinfo/routes.php` — `publications#show` `/api/{catalogSlug}/{id}` (~106) +- `lib/Controller/PublicationsController.php` — `show()` (~473) resolves `{id}` + via `searchObjects` with `@self.uuid` (UUID-only; comment on avoiding `find()`) + +## Not decided + +- Whether/where to add object-slug generation (always vs schema opt-in). +- Whether OpenCatalogi public URLs should accept slugs at all (SEO-friendly + publication URLs) — product decision. diff --git a/docs/development/Services/RegisterSerializer.md b/docs/development/Services/RegisterSerializer.md new file mode 100644 index 0000000000..5acd3661ee --- /dev/null +++ b/docs/development/Services/RegisterSerializer.md @@ -0,0 +1,97 @@ +# RegisterSerializer + +`OCA\OpenRegister\Service\Serializer\RegisterSerializer` + +**Filesystem path:** `lib/Service/Serializer/RegisterSerializer.php` + +## Purpose + +Serializes `Register` entities to arrays with optional `_extend` post-processing. Any caller — HTTP controller or DI consumer — that needs fully expanded registers (schema objects instead of bare IDs, per-schema object counts) uses this class. + +Prior to this serializer, schema expansion only happened inside `RegistersController::index()`. Any app calling `RegisterService::findAll(_extend: ['schemas'])` via DI received ID-only schemas because `_extend` was silently dropped at the mapper layer. + +## Methods + +### `serialize(Register $register, array $extend = [], ?array $schemaStats = null): array` + +Serializes a single register entity. Calls `$register->jsonSerialize()` for the base shape, then applies each recognized `_extend` key. + +**Parameters** + +| Parameter | Type | Description | +|---|---|---| +| `$register` | `Register` | The register entity to serialize. | +| `$extend` | `string[]` | Extension keys (`'schemas'`, `'@self.stats'`). | +| `$schemaStats` | `array\|null` | Pre-computed schema object counts (keyed by schema ID). Pass the result of `RegisterService::getSchemaObjectCounts()`. Required when `'@self.stats'` is in `$extend`; ignored otherwise. | + +**Returns** `array` — serialized register array with extensions applied. + +### `serializeMany(array $registers, array $extend = [], ?array $schemaStatsByRegisterId = null): array` + +Serializes multiple register entities. Delegates each to `serialize()`, routing the per-register stats slice via `$schemaStatsByRegisterId[$register->getId()]`. + +## Recognized `_extend` keys + +| Key | Effect | +|---|---| +| `'schemas'` | Each schema ID in the `schemas` field is replaced by the full schema object (`SchemaMapper::find($id, _multitenancy: false)->jsonSerialize()`). On `DoesNotExistException` the original ID is **kept in its original array position** — not dropped. A warning is logged. | +| `'@self.stats'` | Only effective when `'schemas'` is also in `$extend`. Attaches `stats.objects.total` to each successfully expanded schema object. Defaults to `0` when the ID is absent from `$schemaStats`. Orphan bare IDs are not augmented. | + +Unknown keys are silently ignored. No exception, no log entry. + +## Usage example (DI consumer) + +```php +// Inject via constructor (Nextcloud auto-wires by type): +public function __construct( + private readonly RegisterService $registerService, +) {} + +// In your method: +$registers = $this->registerService->findAllSerialized( + _extend: ['schemas', '@self.stats'], +); + +foreach ($registers as $register) { + foreach ($register['schemas'] as $schemaOrId) { + if (is_array($schemaOrId)) { + // Fully expanded schema object. + echo $schemaOrId['title']; + } + // else: orphan ID — the schema was deleted. + } +} +``` + +## Using the service methods directly + +`RegisterService` exposes two convenience methods that pre-compute stats and delegate to the serializer: + +```php +// Serialize a single register by ID: +$registerArr = $this->registerService->findSerialized( + id: $id, + _extend: ['schemas'], + _multitenancy: false +); + +// Serialize all registers with pagination: +$registersArr = $this->registerService->findAllSerialized( + limit: 25, + offset: 0, + _extend: ['schemas', '@self.stats'], + _multitenancy: false +); +``` + +The entity-returning `findAll()` / `find()` methods keep their current signatures and entity return types. The `_extend` parameter on those methods is declared for signature compatibility only and has no effect. + +## Namespace conventions + +`lib/Service/Serializer/` follows the existing subfolder convention in `lib/Service/` (`Archival/`, `Chat/`, `Configuration/`, `Edepot/`, `File/`). Future entity serializers (`SchemaSerializer`, `ObjectSerializer`) should be placed alongside `RegisterSerializer` in this namespace. + +## Related + +- `RegisterService::findAllSerialized()` / `::findSerialized()` — orchestrate DB queries and delegate to this serializer. +- `RegistersController::index()` — uses `findAllSerialized()` since the refactor; no longer contains inline expansion logic. +- `Register::jsonSerialize()` — unchanged; always returns schemas as an array of IDs. Expansion is a serializer concern. diff --git a/docs/development/import-flow.md b/docs/development/import-flow.md index 298b1dd704..b3858ac622 100644 --- a/docs/development/import-flow.md +++ b/docs/development/import-flow.md @@ -154,11 +154,13 @@ Both paths now include metadata hydration using temporary ObjectEntity instances ### Published Date Handling -When `publish=1` is set during import: +> **DEPRECATED / REMOVED (2026-06-15).** Object-level publish metadata was removed as a breaking change by the `deprecate-published-metadata` OpenSpec change: the `published`/`depublished` columns were dropped by migration `Version1Date20260313130000`, `ImportService::addPublishedDateToObjects()` was removed, and passing `$publish=true` to import now only logs a deprecation warning (locked by `tests/Unit/Service/ImportServicePublishDeprecationTest.php`). Publication/anonymous visibility is now governed by **RBAC** (a public-group read scope with a date-window match such as `publicatiedatum <= $now`), owned by the downstream consuming apps — not by import-time object metadata. The historical behaviour below no longer applies. -1. **ImportService** adds published date to all objects in `@self.published` -2. **SaveObjects** preserves published dates during processing -3. **Database** stores published dates in the `published` column +The legacy behaviour, when `publish=1` was set during import (now a no-op + deprecation warning): + +1. **ImportService** added a published date to all objects in `@self.published` +2. **SaveObjects** preserved published dates during processing +3. **Database** stored published dates in the `published` column ## Performance Characteristics diff --git a/docs/development/magic-mapper-indexing.md b/docs/development/magic-mapper-indexing.md index 39f1efdca1..aa666ddc45 100644 --- a/docs/development/magic-mapper-indexing.md +++ b/docs/development/magic-mapper-indexing.md @@ -15,7 +15,7 @@ Alle magic mapper tables hebben deze metadata columns met automatische indexes: | `_id` | BIGINT | PRIMARY KEY | Row identifier | | `_uuid` | VARCHAR(36) | UNIQUE INDEX | Object UUID lookups | | `_slug` | VARCHAR(255) | INDEX | URL routing en lookups | -| `_name` | VARCHAR(255) | **INDEX** | **_search queries!** | +| `_name` | VARCHAR(255) | **INDEX + pg_trgm GIN** | **_search / _fuzzy queries!** | | `_description` | TEXT | **GEEN INDEX** | _search queries (full scan) | | `_summary` | TEXT | **GEEN INDEX** | _search queries (full scan) | | `_register` | VARCHAR(255) | INDEX | Filter op register | @@ -48,6 +48,33 @@ Properties die `facetable: true` hebben krijgen automatisch een SQL INDEX: } ``` +### 3. Schema Properties met `searchable: true` + +Properties die `searchable: true` hebben krijgen automatisch een **`pg_trgm` GIN index** op hun kolom, zodat fuzzy/substring zoeken (`_search`, `_fuzzy=true`, `ILIKE`, `similarity()`) op dat veld *index-backed* is in plaats van een sequential scan. De vlag is structureel identiek aan `facetable` (een boolean op de property), maar mikt op tekstzoeken in plaats van facet-tellingen. + +```json +{ + "properties": { + "title": { + "type": "string", + "searchable": true // ✅ pg_trgm GIN index voor snelle fuzzy/substring search + }, + "amount": { + "type": "number" // ❌ searchable heeft geen zin op niet-tekst velden + } + } +} +``` + +**Richtlijnen:** + +- **Alleen zinvol op `string`-getypeerde properties** — `gin_trgm_ops` vereist een tekst-castbare kolom. Een niet-string property die per ongeluk `searchable: true` krijgt, wordt getolereerd: de index-poging staat in een try/catch, faalt zachtjes met een waarschuwing in de log, en breekt het aanmaken/synchroniseren van de tabel niet af. +- **PostgreSQL-only, portable no-op elders** — de index wordt alleen aangemaakt op PostgreSQL met de `pg_trgm` extensie beschikbaar. Op MariaDB/MySQL/SQLite (of PostgreSQL zonder `pg_trgm`) wordt de vlag stil geaccepteerd: geen index, geen fout. Schema-definities blijven dus portable over database-platforms; `_search` blijft daar correct werken via het bestaande ongeïndexeerde `ILIKE`/`CAST` pad. +- **Baseline `_name` is al gedekt** — je hoeft `searchable` niet op je titelveld te zetten om `_name` snel te maken: elke magic table krijgt automatisch een `pg_trgm` GIN index op `_name` (zie de metadata-tabel hierboven). Gebruik `searchable: true` voor *extra* tekstvelden voorbij `_name` (bijv. de volledige `description`/`body` van een document). +- **Retrofit bij schema-wijziging** — wordt `searchable: true` later aan een bestaande tabel toegevoegd, dan maakt de sync-route (`updateTableIndexes()` → `createTableIndexes()`) de ontbrekende index alsnog aan (`CREATE INDEX IF NOT EXISTS` is idempotent), net zoals een nieuw `facetable`-veld zijn btree-index retrofit krijgt. + +> **Let op — Doctrine-veiligheid:** zowel de baseline `_name`-index als de per-property `searchable`-index zijn GIN-indexen op een **bestaande** `varchar`/`text`-kolom. Er wordt géén nieuw kolomtype toegevoegd. Anders dan `vector`- of `tsvector`-*getypeerde* kolommen (die Doctrine's `introspectSchema()` over `oc_`-getablesprefixte tabellen laten crashen met "Unknown database type"), zijn functionele/kolom-GIN-indexen onzichtbaar voor Doctrine's type-systeem en dus veilig — dezelfde reden waarom de `hybrid-document-search` functionele `to_tsvector` GIN-index veilig is. + ## _search Parameter Werking De `_search` parameter zoekt in **3 metadata velden**: @@ -90,9 +117,9 @@ ALTER TABLE oc_openregister_table_X_Y ADD FULLTEXT INDEX idx_search_fields (_name, _description, _summary); ``` -**Optie 3: Separate Search Service (Huidige Aanpak)** +**Optie 3: Property-level `searchable: true` (Huidige Aanpak)** -Gebruik SOLR/Elasticsearch voor full-text search via `searchable: true` op schema niveau: +> SOLR/Elasticsearch is verwijderd (zie de `remove-solr-and-publishing` change). Zoeken is nu volledig database-native. Zet `searchable: true` op een **string-property** om een `pg_trgm` GIN index op die kolom te krijgen (zie [Schema Properties met `searchable: true`](#3-schema-properties-met-searchable-true)): ```json { @@ -100,8 +127,12 @@ Gebruik SOLR/Elasticsearch voor full-text search via `searchable: true` op schem "schemas": { "publication": { "slug": "publication", - "searchable": true, // ✅ Geïndexeerd in SOLR - "properties": { ... } + "properties": { + "title": { + "type": "string", + "searchable": true // ✅ pg_trgm GIN index (PostgreSQL) + } + } } } } @@ -191,24 +222,11 @@ Deze velden zijn **TEXT** type zonder index omdat: ## Aanbeveling voor _search Performance -### Korte Termijn (Huidige Code): - -Blijf SOLR/Elasticsearch gebruiken voor `_search` queries: - -```json -{ - "slug": "publication", - "searchable": true // ✅ Index in SOLR voor snelle full-text search -} -``` - -### Lange Termijn (Optimalisatie): - -Implementeer database-native full-text search als fallback wanneer SOLR niet beschikbaar is: +Zoeken is database-native (SOLR is verwijderd). Voor snelle `_search`/`_fuzzy` queries: -1. **PostgreSQL**: GIN index met `to_tsvector()` -2. **MySQL**: FULLTEXT index op `_name`, `_description`, `_summary` -3. **Automatische detectie**: Gebruik SOLR als beschikbaar, anders database FTS +1. **`_name` is automatisch snel** — elke magic table krijgt een `pg_trgm` GIN index op `_name` op PostgreSQL met de `pg_trgm` extensie. Dit maakt zowel het altijd-aan `ILIKE` pad als het `_fuzzy=true` `similarity()` pad index-backed (gemeten: 268ms → ~1.5ms op een tabel van ~82k rijen). +2. **Extra tekstvelden** — zet `searchable: true` op een string-property om diezelfde `pg_trgm` GIN index op die kolom te krijgen (bijv. een volledige `title`/`description` body). +3. **Portable** — op MariaDB/MySQL/SQLite (of PostgreSQL zonder `pg_trgm`) vallen queries terug op het correcte, ongeïndexeerde `ILIKE`/`CAST` pad; er verandert alleen de performance, niet de correctheid. ## Index Overhead diff --git a/docs/features.json b/docs/features.json index 27018cae56..0bfae12b6a 100644 --- a/docs/features.json +++ b/docs/features.json @@ -1,272 +1,36 @@ -[ - { - "slug": "activity-provider", - "title": "Activity Provider", - "summary": "Integrate OpenRegister with Nextcloud's Activity app so that all CRUD operations on Objects, Registers, and Schemas are visible in the standard Nextcloud activity stream, dashboard activity widget, and (optionally) email notifications. This gives users and administrators a clear, auditable timeline of who changed what and when, using the standard `OCP\\Activity` API (IManager, IProvider, IFilter, ActivitySettings).", - "docsUrl": "openspec/specs/activity-provider/spec.md" - }, - { - "slug": "ai-mcp", - "title": "AI MCP — LLPhant Tool Bridge", - "summary": "OpenRegister exposes Model Context Protocol (MCP) tools to LLPhant-driven chat agents through two cooperating mechanisms: (1) an event-driven `ToolRegistry` that lets every installed Nextcloud app contribute LLPhant `ToolInterface` instances to the chat agent's tool-loop, and (2) an `McpProviderBridge` adapter that lifts `IMcpToolProvider` implementations (the per-app MCP plugin contract) into LLPhant function descriptors. The bridge handles the impedance mismatch between MCP's dot-namespaced tool ids and LLPhant/OpenAI/Ollama function-name constraints (no dots), and collapses JSON-Schema nullable types into the scalar strings LLPhant's `Parameter` accepts.", - "docsUrl": "openspec/specs/ai-mcp/spec.md" - }, - { - "slug": "approval-workflow", - "title": "Approval Workflow", - "summary": "Approval Workflow provides multi-step, role-gated approval chains for OpenRegister objects. Administrators configure named chains with ordered steps, each bound to a Nextcloud group (the \"role\"). When an object enters a chain, one `ApprovalStep` record per chain step is created — step 1 starts as `pending`, all others as `waiting`. Authorised users approve or reject the pending step; on approval the next waiting step is automatically advanced to `pending`. Each decision is persisted to the workflow execution history.", - "docsUrl": "openspec/specs/approval-workflow/spec.md" - }, - { - "slug": "archival-destruction-workflow", - "title": "Archival Destruction Workflow", - "summary": "Implement a NEN 15489 compliant destruction workflow for register objects, providing automated destruction scheduling via background jobs, multi-step approval workflows with destruction lists, legal hold management, destruction certificate generation, and archiefactiedatum calculation using configurable afleidingswijzen. This capability builds on the archivering-vernietiging spec and integrates with the immutable audit trail and deletion audit trail for legally required evidence trails.", - "docsUrl": "openspec/specs/archival-destruction-workflow/spec.md" - }, - { - "slug": "auth-system", - "title": "Authentication and Authorization System", - "summary": "Define the authentication and authorization system for OpenRegister, supporting Nextcloud session auth, Basic Auth for API consumers, JWT bearer tokens for external systems, API key auth for MCP and service-to-service integration, and SSO integration via SAML/OIDC. The auth system MUST map all external identities to Nextcloud users via the Consumer entity and enforce consistent RBAC across every access method (REST, GraphQL, MCP, public endpoints), ensuring that a single identity model drives schema-level, property-level, and row-level security decisions.", - "docsUrl": "openspec/specs/auth-system/spec.md" - }, - { - "slug": "avg-verwerkingsregister", - "title": "AVG Verwerkingsregister", - "summary": "Implement GDPR Article 30 processing activity registration integrated with OpenRegister's existing person and organisation entity system. Processing activities link to schemas that contain personal data, and data subject rights (access, rectification, erasure, portability) operate through the existing ObjectService CRUD operations, filtered by the person/organisation identifiers already tracked via the MultiTenancyTrait. The verwerkingsregister itself is modeled as an OpenRegister register and schema — not a separate system — leveraging the same RBAC, audit trail, and multi-tenancy infrastructure used by all other registers. PII detection builds on the existing EntityRecognitionHandler and GdprEntity infrastructure. Retention enforcement integrates with the existing ObjectRetentionHandler and archival metadata. The system MUST maintain a structured register of verwerkingsactiviteiten with mandatory fields (purpose limitation, legal basis, data categories, data subjects, retention periods, security measures, and processor information), enforce purpose-bound access control (doelbinding) on schemas containing personal data, and provide end-to-end workflows for data subject rights including inzageverzoeken (Art 15), rectificatie (Art 16), recht op vergetelheid (Art 17), and dataportabiliteit (Art 20). Additionally, the system MUST support Data Protection Impact Assessments (DPIA, Art 35), automated PII detection and anonymization, consent tracking for processing activities, and structured export of the complete Art 30 register for the Autoriteit Persoonsgegevens (AP).", - "docsUrl": "openspec/specs/avg-verwerkingsregister/spec.md" - }, - { - "slug": "calendar-provider", - "title": "Calendar Provider", - "summary": "OpenRegister SHALL implement `OCP\\Calendar\\ICalendarProvider` to surface objects with date properties as read-only calendar events in the Nextcloud Calendar app. This enables users to see case deadlines, publication dates, hearing schedules, and other time-based data directly in their calendar without manual event creation.", - "docsUrl": "openspec/specs/calendar-provider/spec.md" - }, - { - "slug": "computed-fields", - "title": "Computed Fields", - "summary": "Computed fields enable schema properties whose values are derived automatically from expressions evaluated against object data, cross-referenced objects, and aggregation functions. This capability eliminates redundant data entry, ensures consistency of derived values (full names, totals, expiry dates), and brings spreadsheet-like formula power to OpenRegister without requiring external workflow engines for simple calculations. Computed fields use Twig expressions evaluated server-side, leveraging the existing Twig infrastructure already integrated into OpenRegister for mapping and transformation.", - "docsUrl": "openspec/specs/computed-fields/spec.md" - }, - { - "slug": "content-versioning", - "title": "Content Versioning", - "summary": "Content versioning provides a complete lifecycle for register objects, enabling users to track every change as a numbered version, create named draft versions for work-in-progress edits, compare any two versions with field-level diffs, and roll back to any previous state. This capability is essential for government compliance (WOO, Archiefwet), editorial workflows where changes require review before publication, and multi-user collaboration where concurrent edits must be managed safely.", - "docsUrl": "openspec/specs/content-versioning/spec.md" - }, - { - "slug": "data-import-export", - "title": "Data Import and Export", - "summary": "Document and extend OpenRegister's existing import/export infrastructure. The core pipeline is already implemented: ImportService with ChunkProcessingHandler for bulk ingest, ExportService/ExportHandler for CSV/JSON/XML output, and Configuration/ImportHandler for register template loading. This spec validates the existing implementation and defines extensions for additional formats, progress tracking, and schema validation. The existing pipeline already handles CSV and Excel import via PhpSpreadsheet, CSV and Excel export with RBAC-aware header generation and relation name resolution, configuration import/export in OpenAPI 3.0.0 format, bulk operations via SaveObjects with BulkRelationHandler and BulkValidationHandler, deduplication efficiency reporting, multi-sheet Excel import, two-pass UUID-to-name resolution, and property-level RBAC enforcement on export columns. This spec extends that foundation with JSON/XML/ODS format support, interactive column mapping, progress tracking UI, downloadable error reports, import templates, streaming for large datasets, scheduled imports, and i18n for headers and templates.", - "docsUrl": "openspec/specs/data-import-export/spec.md" - }, - { - "slug": "deep-link-registry", - "title": "Deep Link Registry", - "summary": "The Deep Link Registry enables consuming Nextcloud apps (Procest, Pipelinq, OpenCatalogi, etc.) to claim ownership of specific OpenRegister (register, schema) combinations by registering URL templates at boot time. When Nextcloud's unified search returns objects belonging to a claimed combination, results link directly to the consuming app's detail view instead of OpenRegister's generic object view. This decouples object storage (OpenRegister) from object presentation (consuming apps), allowing each app to own its user experience while sharing a common data layer.", - "docsUrl": "openspec/specs/deep-link-registry/spec.md" - }, - { - "slug": "deletion-audit-trail", - "title": "Deletion Audit Trail", - "summary": "Provide a comprehensive audit and lifecycle management system for all deletion operations in OpenRegister, encompassing soft delete (marking objects as deleted without physical removal), configurable retention before permanent purge, restore from soft delete, cascade delete tracking, and full GDPR-compliant audit trail entries. The spec ensures that every deletion -- whether user-initiated, cascade-triggered, or system-scheduled -- is recorded with sufficient context to reconstruct what happened, why, and by whom, satisfying Dutch government compliance requirements (BIO, AVG/GDPR Article 30, Archiefwet 1995, NEN-ISO 16175-1:2020).", - "docsUrl": "openspec/specs/deletion-audit-trail/spec.md" - }, - { - "slug": "deprecate-published-metadata", - "title": "Deprecate Published/Depublished Object Metadata", - "summary": "Remove the dedicated `published`/`depublished` object metadata system from OpenRegister. The RBAC `$now` dynamic variable replaces this functionality, allowing publication control via authorization rules rather than dedicated metadata columns. This eliminates a parallel publication-state mechanism that overlapped with — and frequently conflicted with — the existing RBAC time-based access controls.", - "docsUrl": "openspec/specs/deprecate-published-metadata/spec.md" - }, - { - "slug": "entity-management-modals", - "title": "Entity Management Modals", - "summary": "Describes the user-facing modal dialog components that mediate create / read / update / delete and bulk operations on first-class register entities (registers, schemas, objects, agents, applications, organisations, configurations, endpoints, sources, views, webhooks, soft-deleted records, audit-trail entries, files). Every register entity in the OpenRegister UI is mutated through a small family of modal Vue components (`Edit{Entity}.vue`, `Delete{Entity}.vue`, `View{Entity}.vue`, plus per-entity bulk variants) that share a consistent open / load / submit / close / error-handling lifecycle driven by the `navigationStore` dialog state and the corresponding entity store.", - "docsUrl": "openspec/specs/entity-management-modals/spec.md" - }, - { - "slug": "event-driven-architecture", - "title": "Event-Driven Architecture", - "summary": "OpenRegister implements a comprehensive event-driven architecture built on Nextcloud's `IEventDispatcher` (OCP\\EventDispatcher\\IEventDispatcher) that enables loose coupling between internal components and external systems. Every mutation across all entity types -- Objects, Registers, Schemas, Sources, Configurations, Views, Agents, Applications, Conversations, and Organisations -- dispatches a typed PHP event that can be consumed by any Nextcloud app, delivered to external systems via webhooks in CloudEvents v1.0 format, or pushed to real-time subscribers via GraphQL SSE. The architecture distinguishes between pre-mutation events (ObjectCreatingEvent, ObjectUpdatingEvent, ObjectDeletingEvent) that implement `StoppableEventInterface` to allow hooks to reject or modify operations, and post-mutation events (ObjectCreatedEvent, ObjectUpdatedEvent, ObjectDeletedEvent) that notify downstream systems after persistence is complete.", - "docsUrl": "openspec/specs/event-driven-architecture/spec.md" - }, - { - "slug": "files-sidebar-tabs", - "title": "Filter Sidebar Tabs", - "summary": "Provide a consistent filter-sidebar UX across OpenRegister's main list views (Entities, Webhooks, Dashboard, Deleted). Each filter sidebar is a Vue single-file component that owns a small set of filter controls (search input, register/schema pickers, status pickers, date ranges) and communicates its state either through `update:*` events to a parent list view (Entities, Webhooks) or through the global Pinia register/schema/deleted stores plus the router query string (Dashboard, Deleted).", - "docsUrl": "openspec/specs/files-sidebar-tabs/spec.md" - }, - { - "slug": "graphql-api", - "title": "GraphQL API", - "summary": "Provide an auto-generated GraphQL API alongside the existing REST API for register data, enabling clients to request exactly the fields they need in a single round-trip and resolve nested relationships without over-fetching. The GraphQL schema MUST be derived dynamically from register schema definitions at runtime, supporting queries with nested object resolution, mutations for CRUD operations, and subscriptions for real-time updates via Server-Sent Events (SSE).", - "docsUrl": "openspec/specs/graphql-api/spec.md" - }, - { - "slug": "linked-entity-types", - "title": "Linked Entity Types", - "summary": "Unified system for linking Nextcloud entities (mail, contacts, calendar events, notes, todos, Talk conversations, Deck cards) to OpenRegister objects and entities. Provides schema-level `configuration.linkedTypes` declarations, Nc\\* property types for typed field-level references, lean `_` metadata columns on both magic and entity tables, a generic API for ad-hoc linking and reverse lookups, read-time enrichment via `_extend`, and sidebar injection based on linkedTypes.", - "docsUrl": "openspec/specs/linked-entity-types/spec.md" - }, - { - "slug": "mail-sidebar", - "title": "Mail Sidebar", - "summary": "Provide a sidebar panel inside the Nextcloud Mail app that displays OpenRegister objects related to the currently viewed email. This enables case handlers to see at a glance which cases, applications, or records are associated with an email -- and to create new associations -- without leaving the Mail app. The integration builds on the `openregister_email_links` table and `EmailService` established by the nextcloud-entity-relations spec.", - "docsUrl": "openspec/specs/mail-sidebar/spec.md" - }, - { - "slug": "mail-smart-picker", - "title": "Mail Smart Picker", - "summary": "Enable OpenRegister objects to be discovered, searched, and inserted as rich references via Nextcloud's Smart Picker in any app that supports the `@nextcloud/vue-richtext` component (Mail, Text, Talk, Collectives, etc.). When a user pastes or picks an OpenRegister object URL, it SHALL render as an inline rich preview card showing the object's title, schema, register, key properties, and a direct link. This integration uses the standard `OCP\\Collaboration\\Reference` API and the existing `ObjectsProvider` search provider.", - "docsUrl": "openspec/specs/mail-smart-picker/spec.md" - }, - { - "slug": "mariadb-ci-matrix", - "title": "MariaDB Support & Dual-Database CI Matrix", - "summary": "OpenRegister SHALL be fully tested on both PostgreSQL and MariaDB through a cost-efficient 2-line CI matrix that piggybacks the database difference onto the PHP version split, ensuring that database-specific code paths (JSONB vs JSON, GIN indexes vs B-tree, pg_trgm vs LIKE, PostgreSQL containment operators vs JSON_CONTAINS) are exercised in CI rather than only discovered in production. Blob storage (Normal mode) is removed — only MagicMapper (dedicated SQL tables per schema) is supported.", - "docsUrl": "openspec/specs/mariadb-ci-matrix/spec.md" - }, - { - "slug": "mcp-discovery", - "title": "MCP Discovery", - "summary": "Provides AI agents and MCP-compatible clients with two complementary interfaces to the OpenRegister platform: a tiered REST-based discovery API for token-efficient API exploration, and a full MCP standard protocol endpoint implementing JSON-RPC 2.0 over Streamable HTTP for native tool and resource access. Together these interfaces allow any LLM or MCP client to discover capabilities, establish sessions, and perform CRUD operations on registers, schemas, and objects without prior knowledge of the API surface.", - "docsUrl": "openspec/specs/mcp-discovery/spec.md" - }, - { - "slug": "mock-registers", - "title": "Mock Registers", - "summary": "Provide self-contained mock registers for the five Dutch base registries -- BRP (persons), KVK (businesses), BAG (addresses/buildings), DSO (environmental permits), and ORI (council information) -- so that Procest, Pipelinq, and other consuming apps can develop and demonstrate integrations without external API credentials, government certificates, or network access. Each register ships as a `*_register.json` file in `lib/Settings/` following the OpenAPI 3.0.0 + `x-openregister` extension pattern, with seed data in the `components.objects[]` array using the `@self` envelope format, imported via the `ConfigurationService -> ImportHandler` pipeline.", - "docsUrl": "openspec/specs/mock-registers/spec.md" - }, - { - "slug": "oas-generation", - "title": "OAS Generation", - "summary": "OAS Generation provides on-demand OpenAPI Specification (OAS 3.x) documents derived from the live register and schema configuration. Callers can retrieve a combined specification covering all registers, or a scoped specification for a single register. The endpoint is publicly accessible without authentication — OAS is treated as self-describing public API documentation.", - "docsUrl": "openspec/specs/oas-generation/spec.md" - }, - { - "slug": "object-interactions", - "title": "Object Interactions", - "summary": "OpenRegister objects require rich interaction capabilities — notes, tasks, file attachments, tags, and audit trails — that allow users to collaborate on and track the lifecycle of register data. Rather than building custom interaction systems, this spec defines a convenience API layer that wraps Nextcloud's native subsystems (CalDAV for tasks, ICommentsManager for notes, IRootFolder for files, Nextcloud tags) and links them to OpenRegister objects via standardized properties. Any consuming app (Procest, Pipelinq, OpenCatalogi, ZaakAfhandelApp) can use these unified sub-resource endpoints without knowledge of the underlying Nextcloud internals.", - "docsUrl": "openspec/specs/object-interactions/spec.md" - }, - { - "slug": "openapi-generation", - "title": "OpenAPI Generation", - "summary": "Auto-generate OpenAPI 3.1.0 specifications from register and schema definitions stored in OpenRegister, producing complete API documentation that covers every CRUD endpoint, query parameter, authentication scheme, and response model. The generated spec MUST be downloadable in JSON and YAML formats, serveable via an interactive Swagger UI, and MUST regenerate automatically when schemas change so that documentation never drifts from the live API surface. The generation pipeline MUST also support NL API Design Rules compliance markers for Dutch government API interoperability.", - "docsUrl": "openspec/specs/openapi-generation/spec.md" - }, - { - "slug": "platform-administration-modals", - "title": "Platform Administration Modals", - "summary": "Describes the administrator-facing modal dialogs that configure OpenRegister's platform-level infrastructure (SOLR search backend, LLM provider wiring, configuration sets, collection assignments, vectorization, faceting, file management) and that drive long-running operational tasks (cache clear, index warmup, mass validation, index inspection, SOLR setup). Unlike the entity-management modals — which mutate user data via per-entity stores — these modals read and write platform settings via `/api/settings/*` REST endpoints and dispatch background operational jobs.", - "docsUrl": "openspec/specs/platform-administration-modals/spec.md" - }, - { - "slug": "production-observability", - "title": "Production Observability", - "summary": "Provide production-grade observability for OpenRegister deployments through Prometheus metrics, structured logging, health/readiness endpoints, and audit-compliant monitoring. This capability enables operations teams to monitor application health, track SLA compliance, detect anomalies in real-time, and satisfy BIO (Baseline Informatiebeveiliging Overheid) audit logging requirements for Dutch government deployments.", - "docsUrl": "openspec/specs/production-observability/spec.md" - }, - { - "slug": "profile-actions", - "title": "Profile Actions", - "summary": "Extend the OpenRegister user profile system with self-service account management actions. The current profile API (`/api/user/me`) supports reading and updating basic profile fields, but lacks password change, avatar management, personal data export (GDPR Article 20 data portability), notification preferences, personal activity history, API token management, and account deactivation requests. This spec defines REST endpoints and frontend UI for each action, all respecting Nextcloud backend capabilities and organisation-level policies. Every action is scoped to the authenticated user (no admin elevation required) and integrates with existing `UserService`, `SecurityService`, and `OrganisationService`.", - "docsUrl": "openspec/specs/profile-actions/spec.md" - }, - { - "slug": "rapportage-bi-export", - "title": "Rapportage en BI Export", - "summary": "Provide a comprehensive reporting and business intelligence export layer for OpenRegister that enables government organisations to generate management reports, perform data aggregation queries, connect external BI tools, and satisfy Dutch public accountability requirements (WOO, jaarverslag, verantwoording). The system exposes a general-purpose aggregation API (count, sum, avg, min, max, count_distinct, group by) on top of the existing `MagicMapper` and `MagicStatisticsHandler` infrastructure, supports scheduled report generation via the `ReportRenderJob` daily TimedJob, produces exports in CSV, XLSX, ODS, HTML, and PDF formats via `ReportRenderService` (PhpSpreadsheet for spreadsheets, Dompdf for PDF), and provides GraphQL as the primary external-BI integration surface (Power BI, Tableau, Looker, Metabase all speak it via existing connectors). All reporting operations enforce RBAC via `PermissionHandler`, `MagicRbacHandler`, and `PropertyRbacHandler`, and respect multi-tenancy boundaries to guarantee data isolation between organisations.", - "docsUrl": "openspec/specs/rapportage-bi-export/spec.md" - }, - { - "slug": "realtime-updates", - "title": "Realtime Updates", - "summary": "Provide live data synchronization to connected clients so that register object mutations (create, update, delete) are pushed immediately without manual page refresh. The system MUST offer Server-Sent Events (SSE) as the primary transport, with Nextcloud's notify_push integration as a complementary channel, and graceful fallback to polling. All realtime channels MUST be authorization-aware, meaning users only receive events for objects their RBAC permissions allow them to see, and MUST support topic-based subscriptions at the register, schema, and individual object level.", - "docsUrl": "openspec/specs/realtime-updates/spec.md" - }, - { - "slug": "referential-integrity", - "title": "Referential Integrity", - "summary": "Enforce referential integrity between register objects connected via `$ref` schema properties so that modifications or deletions of referenced objects propagate correctly according to configurable integrity actions (CASCADE, SET_NULL, SET_DEFAULT, RESTRICT, NO_ACTION). The system MUST maintain data consistency across schemas, detect circular reference chains, support cross-register references, and provide auditable, transactional enforcement that prevents orphaned references while respecting performance constraints on deep reference graphs.", - "docsUrl": "openspec/specs/referential-integrity/spec.md" - }, - { - "slug": "register-i18n", - "title": "Register Internationalization", - "summary": "Implement multi-language content management for register objects so that translatable properties store per-language variants, APIs negotiate content language via Accept-Language headers, and the UI provides language-aware editing with completeness tracking. The system MUST support at minimum Dutch (NL, required) and English (EN, optional) to comply with Single Digital Gateway (SDG) Regulation (EU) 2018/1724 for cross-border EU service access, while the architecture MUST allow registers to configure any number of BCP 47 languages including RTL scripts. This spec covers data-level i18n for register object content -- it is distinct from the app UI string translations governed by `i18n-infrastructure`, `i18n-string-extraction`, `i18n-backend-messages`, and `i18n-dutch-translations` specs, which handle Nextcloud `IL10N` / `t()` / `$l->t()` for interface labels.", - "docsUrl": "openspec/specs/register-i18n/spec.md" - }, - { - "slug": "retention-management", - "title": "Retention Management", - "summary": "Implement retention lifecycle management for register objects: MDTO-compliant archival metadata, selectielijsten, archiefactiedatum calculation, destruction scheduling with approval workflows, legal holds, and notifications per Archiefwet 1995.", - "docsUrl": "openspec/specs/retention-management/spec.md" - }, - { - "slug": "row-field-level-security", - "title": "Row and Field Level Security", - "summary": "Implement dynamic per-record access rules based on field values (row-level security / RLS) and per-field visibility and editability rules based on user roles (field-level security / FLS). Beyond schema-level RBAC that controls access to entire object types, the system MUST support row-level security where access to individual objects depends on the object's own properties (e.g., department, classification level, owner), and field-level security where different users see different fields of the same object. Both security layers MUST be enforced consistently across REST, GraphQL, search, export, and MCP access methods, evaluated at the database query level where possible for performance, and composable with schema-level RBAC and multi-tenancy isolation.", - "docsUrl": "openspec/specs/row-field-level-security/spec.md" - }, - { - "slug": "saved-search-views", - "title": "Saved Search Views", - "summary": "Lets OpenRegister users save the configuration of an object search — selected registers and schemas, free-text search terms, facet filters, and enabled facets — as a reusable, named **view** backed by `/api/views`. Views can be marked public or default, favorited per user, and re-applied to the live search from the search sidebar. This capability describes the observed frontend contract of `src/sidebars/search/SearchSideBar.vue` and the `viewsStore` it drives. It was retrofitted under ADR-003 on 2026-05-25 (cluster `fe-sidebars`); requirements capture observed behavior rather than original intent.", - "docsUrl": "openspec/specs/saved-search-views/spec.md" - }, - { - "slug": "schema-hooks", - "title": "Schema Hooks", - "summary": "Schema hooks enable per-schema configuration of workflow callbacks that fire on object lifecycle events, allowing external systems to validate, enrich, transform, or reject data before or after persistence. Hooks use CloudEvents 1.0 structured content mode for payloads, support synchronous (request-response) and asynchronous (fire-and-forget) delivery modes, and provide configurable failure behavior (reject, allow, flag, queue) so administrators can balance data integrity against availability. The hook system is engine-agnostic through the `WorkflowEngineInterface` abstraction, currently supporting n8n and Windmill adapters, and integrates deeply with Nextcloud's PSR-14 event dispatcher via `StoppableEventInterface` for pre-mutation rejection.", - "docsUrl": "openspec/specs/schema-hooks/spec.md" - }, - { - "slug": "search-index", - "title": "Search Index", - "summary": "OpenRegister provides full-text search, faceted browsing, and bulk re-indexing over its object store via a pluggable search backend (Solr today, Elasticsearch in parallel, with the `SearchBackendInterface` keeping the contract backend-agnostic). The `search-index` capability covers everything from the high-level `IndexService` facade down through the Solr-specific primitives (`SolrCollectionManager`, `SolrDocumentIndexer`, `SolrQueryExecutor`, `SolrFacetProcessor`, `SolrHttpClient`), the schema/document builders (`SchemaHandler`, `DocumentBuilder`, `SchemaMapper`), the bulk-indexing driver (`BulkIndexer`), the configuration plumbing (`ConfigurationHandler`), and the tenant-collection setup orchestrator (`SetupHandler`).", - "docsUrl": "openspec/specs/search-index/spec.md" - }, - { - "slug": "tmlo-metadata", - "title": "tmlo-metadata", - "summary": "Foundation capability for TMLO (Toepassingsprofiel Metadatastandaard Lokale Overheden) archival metadata on OpenRegister objects. Owns the cross-cutting contract that the sibling specs (`tmlo-metadata-schema`, `tmlo-auto-populate`, `tmlo-export`, `tmlo-query-api`) compose against:", - "docsUrl": "openspec/specs/tmlo-metadata/spec.md" - }, - { - "slug": "urn-resource-addressing", - "title": "URN Resource Addressing", - "summary": "Implement bidirectional URN-URL mapping for system-independent resource identification, enabling Dutch government organisations to address register objects across multi-vendor environments without coupling to specific system URLs or database identifiers. Every register object MUST support a URN identifier following RFC 8141 syntax that can be resolved to an API URL and vice versa, ensuring stable addressing across system migrations, domain changes, and federated deployments. This spec covers URN format definition, resolution APIs, cross-instance federation, NL government identifier mapping, event integration, and human-readable aliases.", - "docsUrl": "openspec/specs/urn-resource-addressing/spec.md" - }, - { - "slug": "webhook-payload-mapping", - "title": "Webhook Payload Mapping", - "summary": "Extend OpenRegister's existing CloudEvent-based event and webhook infrastructure with configurable payload mapping. The core webhook delivery (WebhookService, WebhookDeliveryJob, CloudEventFormatter) is already implemented. This spec focuses on the Mapping entity integration for payload transformation, advanced filtering, and delivery management. It documents the complete webhook lifecycle as already implemented: registration with URL/events/secret, payload format selection (standard, CloudEvents, Twig-mapped), delivery retry with exponential backoff, delivery logging, HMAC authentication, event filtering by register/schema/conditions, webhook management API, testing/dry-run, async delivery via background jobs, health monitoring through statistics, multi-tenant webhook isolation via organisation scoping, and request interception for pre-event webhooks. The Mapping entity reference allows any subscriber to receive events in whatever format they require (ZGW notifications, FHIR events, CloudEvents, VNG Notificaties API, custom formats) without any hardcoded format knowledge in OpenRegister.", - "docsUrl": "openspec/specs/webhook-payload-mapping/spec.md" - }, - { - "slug": "workflow-engine-abstraction", - "title": "Workflow Engine Abstraction", - "summary": "Provides an engine-agnostic interface for OpenRegister to interact with workflow engines (n8n, Windmill, and future engines), enabling the system to deploy, execute, monitor, and manage workflows without coupling to any specific engine's API. This is the foundation layer that other specs (Schema Hooks, Workflow-in-Import, Workflow Integration) build upon: every hook execution, import-time workflow deployment, and event-driven automation flows through the `WorkflowEngineInterface` and `WorkflowEngineRegistry` defined here. By abstracting engine specifics behind adapters, OpenRegister can support multiple simultaneous engines, allow engine migration without data loss, and extend to new engines via a single interface implementation.", - "docsUrl": "openspec/specs/workflow-engine-abstraction/spec.md" - }, - { - "slug": "workflow-in-import", - "title": "Workflow in Import", - "summary": "Extends the OpenRegister JSON configuration import pipeline to deploy workflow definitions to external engines (n8n, Windmill), wire them as schema hooks, track them for versioning and idempotent re-import, and include them in configuration exports -- all from a single import file. This specification bridges the `workflow-engine-abstraction` layer (engine adapters, `WorkflowEngineInterface`) with the `data-import-export` pipeline (`ImportHandler`, `ExportHandler`), enabling portable, self-contained register configurations that include both data structures and automation logic. It also ensures that workflows imported alongside schemas and objects participate in the `schema-hooks` lifecycle so that hooks are active before any objects in the same import are created.", - "docsUrl": "openspec/specs/workflow-in-import/spec.md" - }, - { - "slug": "workflow-integration", - "title": "Workflow Integration", - "summary": "Integrate BPMN-style workflow automation with register operations via n8n (primary) and other pluggable workflow engines (Windmill, future). Register events (create, update, delete, status change) MUST trigger configurable workflows for process automation, enrichment, validation, escalation, approval chains, and scheduled tasks. The integration MUST support zero-coding workflow configuration for functional administrators and provide full observability into workflow executions via logging, status tracking, and audit trails.", - "docsUrl": "openspec/specs/workflow-integration/spec.md" - }, - { - "slug": "zoeken-filteren", - "title": "Zoeken en Filteren", - "summary": "Provide a comprehensive, backend-agnostic search and filtering system for register objects that supports full-text search with relevance ranking, field-level filtering with comparison operators, faceted drill-down navigation, multi-field sorting, cursor and offset pagination, and saved search trails. The system MUST transparently operate against PostgreSQL (with optional pg_trgm fuzzy matching), Apache Solr, or Elasticsearch as interchangeable backends, while exposing a single unified API surface through `ObjectService.searchObjectsPaginated()` and `SearchBackendInterface`.", - "docsUrl": "openspec/specs/zoeken-filteren/spec.md" - } -] +{ + "schemaVersion": 1, + "generatedAt": "2026-07-12T13:34:59.662Z", + "features": [ + { + "slug": "avg-verwerkingsregister", + "title": "AVG Verwerkingsregister", + "summary": "**Tender demand**: 58% of analyzed government tenders require AVG processing register capabilities. Cross-referencing with archivering-vernietiging (77%), audit-trail-immutable (56%), and auth-system (67%) shows that GDPR compliance is a prerequisite capability for nearly all Dutch government tender participation.", + "docsUrl": "https://codeberg.org/Conduction/openregister/blob/main/openspec/specs/avg-verwerkingsregister/spec.md" + }, + { + "slug": "deprecate-published-metadata", + "title": "Deprecate Published/Depublished Object Metadata", + "summary": "Remove the dedicated `published`/`depublished` object metadata system from OpenRegister. The RBAC `$now` dynamic variable replaces this functionality, allowing publication control via authorization rules rather than dedicated metadata columns. This eliminates a parallel publication-state mechanism that overlapped with — and frequently conflicted with — the existing RBAC time-based access controls.", + "docsUrl": "https://codeberg.org/Conduction/openregister/blob/main/openspec/specs/deprecate-published-metadata/spec.md" + }, + { + "slug": "graphql-api", + "title": "GraphQL API", + "summary": "Provide an auto-generated GraphQL API alongside the existing REST API for register data, enabling clients to request exactly the fields they need in a single round-trip and resolve nested relationships without over-fetching. The GraphQL schema MUST be derived dynamically from register schema definitions at runtime, supporting queries with nested object resolution, mutations for CRUD operations, and subscriptions for real-time updates via Server-Sent Events (SSE).", + "docsUrl": "https://codeberg.org/Conduction/openregister/blob/main/openspec/specs/graphql-api/spec.md" + }, + { + "slug": "rapportage-bi-export", + "title": "Rapportage en BI Export", + "summary": "**Tender demand**: 89% of analyzed government tenders require reporting and BI export capabilities. Key recurring requirements include management dashboards, KPI tracking, periodic status reports (wekelijkse voortgangsrapportage), WOO transparency reporting, and integration with existing BI tooling (Power BI, Tableau, QlikView).", + "docsUrl": "https://codeberg.org/Conduction/openregister/blob/main/openspec/specs/rapportage-bi-export/spec.md" + }, + { + "slug": "saved-search-views", + "title": "Saved Search Views", + "summary": "Lets OpenRegister users save the configuration of an object search — selected registers and schemas, free-text search terms, facet filters, and enabled facets — as a reusable, named **view** backed by `/api/views`. Views can be marked public or default, favorited per user, and re-applied to the live search from the search sidebar. This capability describes the observed frontend contract of `src/sidebars/search/SearchSideBar.vue` and the `viewsStore` it drives. It was retrofitted under ADR-003 on 2026-05-25 (cluster `fe-sidebars`); requirements capture observed behavior rather than original intent.", + "docsUrl": "https://codeberg.org/Conduction/openregister/blob/main/openspec/specs/saved-search-views/spec.md" + } + ] +} diff --git a/docs/features/README.md b/docs/features/README.md index 02d31b9390..d80cbf004c 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -46,6 +46,8 @@ OpenRegister implements or supports the following GEMMA (Gemeentelijke Model Arc | Feature | Doc | Category | Status | Key Standards | |---------|-----|----------|--------|---------------| | Registers & Schemas | [registers-and-schemas.md](registers-and-schemas.md) | Core | Implemented | JSON Schema, Schema.org, GGM | +| External Databases as Virtual Registers | [dbal-virtual-registers.md](dbal-virtual-registers.md) | Core | Implemented | Doctrine DBAL, JSON Schema, Common Ground (data at the source) | +| Nextcloud Tables as Virtual Registers | [tables-virtual-register.md](tables-virtual-register.md) | Core | Implemented | Nextcloud Tables, JSON Schema, Common Ground (data at the source) | | Object Storage & Lifecycle | [object-storage.md](object-storage.md) | Core | Implemented | UUID, soft delete, versioning | | Search, Filtering & Faceting | [search-and-faceting.md](search-and-faceting.md) | Core | Implemented | NL API Design Rules, PostgreSQL, Solr, Elasticsearch | | Access Control (RBAC) | [access-control.md](access-control.md) | Security | Implemented | OAuth2 scopes, ZGW Autorisaties, BIO | @@ -84,6 +86,7 @@ Meeting Dutch government regulatory requirements. - [Content Versioning & Audit Trail](versioning-and-audit.md) — Immutable hash-chained audit log, semantic versioning, rollback - [Archiving & Records Management](archiving.md) — Retention schedules, destruction workflows, MDTO XML, e-Depot transfer (SIP) +- [DSAR Deadline Escalation & DPIA Detection](dsar-escalation-and-dpia.md) — Temporal deadline-reminder/escalation/breach sweep + GDPR art-35 DPIA pattern detection, policy-pack-driven (ADR-047) ### Integration Connecting OpenRegister to external systems and workflows. @@ -94,6 +97,7 @@ Connecting OpenRegister to external systems and workflows. - [Workflow Automation](workflow-automation.md) — Schema hooks, n8n/Windmill integration, import-time workflow triggers - [OpenAPI & GraphQL APIs](api-generation.md) — Auto-generated specs and GraphQL schema per register - [Real-Time Updates](realtime-updates.md) — SSE subscriptions, RBAC-filtered events, reconnection with replay +- [Semantic Object Handoff](semantic-object-handoff.md) — Cross-app object conversion via canonical kinds (ns#Case, ns#Quote, …), declarative dialect, queue-mode deferral, provenance both ways (ADR-051) ### AI & Agent Interfaces Enabling AI systems and LLMs to access register data. diff --git a/docs/features/ai-and-mcp.md b/docs/features/ai-and-mcp.md index ad24b69cbd..8fb7194b0b 100644 --- a/docs/features/ai-and-mcp.md +++ b/docs/features/ai-and-mcp.md @@ -104,6 +104,83 @@ GET /api/mcp/v1/discover/{capability}/data Returns live data samples from the capability (e.g., the first 5 objects from a schema). Enables few-shot context injection. +## Schema-Declared MCP Tools (`x-openregister-mcp` dialect) + +Per ADR-063 (MCP as Platform Abstraction) and ADR-031 (schema-declarative +business logic), a schema can declare which coarse CRUD MCP tools it exposes +via a top-level `x-openregister-mcp` annotation in +`lib/Settings/{app}_register.json`, a member of the `x-openregister-*` +dialect family alongside `x-openregister-lifecycle`, +`x-openregister-calculations`, `x-openregister-notifications`, etc. This is +the **declaration** step of a three-change arc — this change (validation + +storage only) is followed by a derived tool provider that emits the actual +MCP tools, then a `#[McpTool]` service attribute for non-CRUD behaviour. All +three chain changes have since shipped: schema-derived tools go live the +moment `enabled: true` is saved, and `#[McpTool]` now accepts the same +`readOnlyHint`/`destructiveHint`/`idempotentHint`/`scope` vocabulary shown +below, forwarded to both serving surfaces when a service author sets them +(`or-mcp-attribute-hints`). + +### Shape + +```jsonc +"x-openregister-mcp": { + "enabled": true, // REQUIRED. Default OFF: absent or false = no tools. + "tools": { // OPTIONAL. Absent => all five verbs with defaults. + "search": { + "description": "Search cases by status, assignee and free text.", + "filters": ["status", "assignee", "createdAt"], // search only; each MUST be a schema property + "scope": "read", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + }, + "get": { "description": "...", "scope": "read", "readOnlyHint": true }, + "create": { "description": "...", "scope": "create", "destructiveHint": false, "idempotentHint": false }, + "update": { "description": "...", "scope": "update", "destructiveHint": false, "idempotentHint": true }, + "delete": { "description": "...", "scope": "delete", "destructiveHint": true, "idempotentHint": true } + } +} +``` + +- **`enabled`** (boolean, required when the block is present) is the opt-in + gate — the dialect is **default OFF** fleet-wide. A schema with no block, + or `enabled:false`, exposes no MCP tools. +- **`tools`** (object, optional) keys are restricted to the closed verb set + `{search, get, create, update, delete}` — this is a **coarse CRUD + template**, not a mechanism for declaring arbitrary per-REST-endpoint + tools. Naive OpenAPI→MCP (one tool per endpoint) measurably degrades LLM + tool-selection accuracy and burns tokens on large surfaces; the dialect + reuses the schema itself as the tool's input/output schema instead. + Non-CRUD, behaviour-specific tools belong to the `#[McpTool]` service + attribute, not this dialect. +- Per-verb `description` / `scope` (`read|create|update|delete`) / the MCP + 2025-11-25 annotation hints `readOnlyHint` / `destructiveHint` / + `idempotentHint` are validated for **type and shape only**. `search` also + accepts `filters` — property names the search tool accepts as query + filters; every entry must name a real property on the schema. + +### Untrusted hints vs. authoritative RBAC + +The `readOnlyHint` / `destructiveHint` / `idempotentHint` values are +**untrusted UX hints** passed through to the tool descriptor for client-side +display — they are never treated as a security decision. The authoritative +gate at invoke time is always OpenRegister RBAC via `ObjectService`, +identical to every other access path (UI, REST, GraphQL). A schema author +declaring `destructiveHint: false` on `delete` does not weaken RBAC in any +way; it only affects how an MCP client chooses to surface the tool to a +human-in-the-loop. + +### Validation + +A malformed `x-openregister-mcp` block fails the schema save loudly — the +same failure mode as every sibling `x-openregister-*` dialect — via +`McpAnnotationValidator` (`lib/Service/Mcp/McpAnnotationValidator.php`), +invoked from `SchemaMapper::cleanObject()`. Unknown verbs, unknown `filters` +property references, `filters` on a non-`search` verb, invalid `scope` +values, and non-boolean hints are all rejected with a schema-identifying +error rather than silently mis-exposing data. + ## Agent Use Cases OpenRegister is designed to serve as a data backend for AI-driven applications: diff --git a/docs/features/dbal-virtual-registers.md b/docs/features/dbal-virtual-registers.md new file mode 100644 index 0000000000..761113921b --- /dev/null +++ b/docs/features/dbal-virtual-registers.md @@ -0,0 +1,24 @@ +# External Databases as Virtual Registers (Doctrine DBAL) + +Connect an external relational database and use it as a read-only **virtual register**: the database becomes a Register, its tables and views become Schemas, and objects are served live over Doctrine DBAL — no copy, no sync job, no duplicate storage. + +## Standards & architecture references + +- GEMMA Gegevensmagazijncomponent — [GEMMA](https://gemmaonline.nl/index.php/GEMMA/id-06321658-50d1-4153-b007-6630ffabcd80) (data warehouse over existing sources) +- Common Ground principle: data stays at the source; OpenRegister projects it. +- Hydra ADR-049 (virtual schemas via object-source providers), OpenRegister ADR-004 (credential custody). +- Spec: [`openspec/specs/dbal-virtual-registers/spec.md`](../../openspec/specs/dbal-virtual-registers/spec.md) + +## Overview + +An administrator creates a `Source` of type `database` (driver, host, port, database, user) with the password custodied in the credential vault (referenced by credential UUID — never stored in plaintext). A test-connection action verifies reachability. Introspection then reads the database structure through the DBAL schema manager and produces a Register plus one Schema per table/view, each bound to the `dbal-source` object-source provider via `x-openregister-object-source`. From that point the standard objects API lists, reads, filters, sorts, and paginates the external rows live — with schema-level RBAC parity and relation expansion via `_extend`. + +## Key capabilities + +- **Drivers**: `pdo_mysql`, `pdo_pgsql`, `pdo_sqlite`; views supported (read-only by nature). +- **Credential custody (ADR-004)**: password lives behind the `CredentialStore` seam (Doriath leaf, Nextcloud vault fallback); submitted plaintext passwords are stripped before persistence; fail-closed on unresolvable credentials. +- **SQL → JSON Schema introspection**: column types map to JSON-Schema `type`/`format`; `NOT NULL` → `required`; length → `maxLength`; single-column foreign keys become `$ref` relations with `objectConfiguration.handling: related-object` (+ inverse `inversedBy`) so `_extend` resolution and validation work unchanged; composite primary keys get a deterministic joined id; tables without a primary key are list-only. +- **Live reads, real pagination**: filters/search/sort/pagination push down to parameterized SQL (platform-quoted identifiers, column allowlist); true totals via `COUNT(*)` — the pagination fix benefits all object-source providers. +- **Scheduled drift detection**: a background job re-introspects each database source on an interval and applies schema diffs; a manual introspect action also exists. +- **Failure semantics**: unreachable database → 503, upstream error → 502 — never a bare 500; denied list reads return 404 with no enumeration oracle. +- **Read-only by default, opt-in writes (v2)**: an admin can enable "Allow writes" on a database source; create/update/delete then push through to the external database as parameterized, column-allowlisted statements with RBAC before external contact, audit-trail rows, sanitized constraint errors (unique/FK → 409, not-null/check/type → 422), and generated keys via PostgreSQL `RETURNING` / `lastInsertId`. The flag is re-verified live at write time (disabling re-locks instantly); views are never writable, no-PK tables are append-only, and writes to external rows are hard deletes / last-write-wins (no OpenRegister locking). diff --git a/docs/features/dsar-escalation-and-dpia.md b/docs/features/dsar-escalation-and-dpia.md new file mode 100644 index 0000000000..fe223e965a --- /dev/null +++ b/docs/features/dsar-escalation-and-dpia.md @@ -0,0 +1,39 @@ +# DSAR Deadline Escalation & DPIA Pattern Detection + +Two proactive GDPR-safety capabilities layered on the shipped DSAR (data-subject-request) register: the temporal deadline-escalation sweep and art-35 DPIA pattern detection. Both are cron-driven and configured entirely through the `dsarPolicyPack` (config as data, ADR-047/ADR-031); consuming apps (pipelinq, procest, zaakafhandelapp) inherit them with zero app code. + +## Standards & architecture references + +- GDPR art-12(3) — response deadlines; art-35 — Data Protection Impact Assessment +- ADR-047 — OpenRegister owns the AVG/DSAR workflow +- ADR-031 — Schema-declarative business logic (calculations + notification rules) +- ADR-051 §4 — OR-owned capabilities are exclusive (generic art-35 tooling belongs in OR, not a leaf) + +## Deadline escalation + +The DSAR register declares an `escalationTier` calculation (on-track / reminder / escalation / breached, boundaries from the active policy pack) and three `calculatedChange` notification rules (reminder, escalation, breach). Those rules only fire on an object write — so a case nobody touches never re-computes its tier and the notifications never dispatch. + +`TemporalCalculationSweepJob` (hourly `TimedJob`) closes the gap generically: it detects schemas whose materialised calculations reference the evaluation clock (`now`), selects objects in non-terminal lifecycle states, recomputes via the calculation engine against the shared payload builder, and persists **only changed values** through the normal `ObjectService` write path — which emits the standard updated event with old+new data, so the declared `calculatedChange` rules fire, boundary-guarded (one dispatch per tier crossing). Any future `now`-dependent schema inherits deadline-watch behaviour for free. + +Breach visibility: `breachedAt` is a write-once stamp (a calculation evaluated after `escalationTier`), and the `deadlineBreach` rule reaches both the case handler and a **privacy-officer group** resolved from the pack (`privacyOfficerGroup`, via `PrivacyOfficerRecipientResolver`). `daysRemaining` / `isOverdue` / `escalationTier` are declared as schema properties so their materialised values persist to magic-table columns. + +Operator toggles: `temporal_calculation_sweep_enabled` (default true), `temporal_calculation_sweep_interval` (default 3600 s, min 300). + +## DPIA pattern detection + +`DsarDpiaDetectionJob` (daily `TimedJob`) partitions DSAR cases by jurisdiction, resolves each partition's active pack, and feeds the pack's `dpiaDetection` block (threshold, window, grouping fields) into the pure `DpiaPatternDetectionService`. That service groups cases received inside the rolling window by the configured characteristics (default request `type` + normalised scope) and reports every group reaching the threshold. The job sets `dpiaRequired = true` on each **unflagged** member through an audited write (`dpia.detected`, recording rule / group key / window / count). + +- **Idempotent** — already-flagged cases count toward their group but are never re-written or re-audited. +- **One-way ratchet** — a manually-set flag is never cleared by the job. +- **No imperative dispatch** — the declared `dpiaFlagged` notification rule fires on the `dpiaRequired` false→true write, so detection and manual flagging share one path (gate-18 posture). The officer recipient resolves from the pack. +- **Fail-safe** — no resolvable pack, or a pack with no `dpiaDetection` block, is a no-op (no false DPIA flags). + +Defaults (seeded default pack, pipelinq parity): threshold 10, window 30 days, group by type + scope. Operator toggle: `dsar_dpia_detection_enabled` (default true). + +## Key capabilities + +- Generic temporal re-materialisation of `now`-dependent calculations (deadline clockwork for any schema) +- Declared reminder / escalation / breach notifications made live without object writes +- Write-once `breachedAt` breach stamp + privacy-officer breach visibility (both declared, not hard-coded) +- Policy-pack-driven art-35 DPIA pattern detection with audited flagging and idempotent, ratchet-safe re-runs +- `ImportDsarRegisters` repair step imports/upgrades the DSAR case + policy-pack registers from `lib/Settings` (ADR-037) diff --git a/docs/features/file-actions.md b/docs/features/file-actions.md index 153d760f6c..24ed5f17d4 100644 --- a/docs/features/file-actions.md +++ b/docs/features/file-actions.md @@ -18,6 +18,7 @@ Extended file operations for register objects beyond basic CRUD. Provides versio - **Thumbnail Preview** -- Generate and serve thumbnail previews for supported file types. Returns a stream response for direct embedding. - **Download Audit** -- All file downloads (via `downloadById`) are tracked for compliance and audit trail purposes. - **Label Management** -- Attach, update, or remove classification labels on files (`updateLabels`). Supports arbitrary key-value label sets. +- **Permission-based Access** -- File access is authorized by Nextcloud node permissions, not ownership: read permission to list/view, write permission to update, delete permission to delete. Objects may link files owned by any user (including files reached via a share); a file is not force-transferred to the OpenRegister system user when the current user already has write access. Ownership follows the containing folder's owner. ## API Endpoints diff --git a/docs/features/office-document-sanitization.md b/docs/features/office-document-sanitization.md new file mode 100644 index 0000000000..e917fa7151 --- /dev/null +++ b/docs/features/office-document-sanitization.md @@ -0,0 +1,59 @@ +--- +title: Office Document Sanitisation +sidebar_position: 16 +--- + +# Office Document Sanitisation + +## Overview + +When OpenRegister anonymises an Office document (`.docx` or `.odt`), the entity walker can only reach the visible text runs. Office documents also carry personally-identifying information (PII) in **non-text structures** that the walker is structurally blind to — reviewer comments, tracked-change author attributes, document metadata, person-identifying field codes, custom XML data bindings, and hyperlink URLs. These regularly leak names and case numbers into published Woo/dossier documents. + +The **document sanitiser** runs *ahead* of the anonymisation walker. It produces a cleaned derivative of the input by performing XML-level surgery on the ZIP container; the cleaned file is then passed to the walker for entity detection and replacement. **The original file is never modified.** + +## What it strips + +| Category | DOCX | ODT | +| --- | --- | --- | +| Comments | `word/comments*.xml` + `people.xml` + inline `commentRange*`/`commentReference` markers | `office:annotation` / `office:annotation-end` | +| Tracked changes | accept `` (keep text), drop `` (remove text), strip `w:rsid*` revision attributes | accept `text:change-start..end`, drop `text:change`, remove the `text:tracked-changes` container | +| Document metadata | `docProps/core.xml`, `app.xml`, `custom.xml` (Author, Last-Modified-By, Title, Subject, Keywords, Description, Category, Content-Status, Company, Manager, string-typed custom props) | `meta.xml` (creator, initial-creator, title, subject, keyword, description, string-typed user-defined) | +| Person field codes | `AUTHOR`, `USERNAME`, `USERINITIALS`, `LASTSAVEDBY` (simple `` + complex `` forms) | `text:author-name`, `text:author-initials`, `text:initial-creator` | +| Custom XML | all `customXml/item*.xml` parts; data-bound `` content controls are unwrapped (visible text preserved) | — | +| Hyperlinks | `` flattened to plain text; URL + relationship dropped | `text:a` flattened to plain text | + +Timestamps (`dcterms:created`/`modified`) and non-person field codes (`DATE`, `PAGE`, etc.) are preserved. Removed parts are reconciled out of `[Content_Types].xml` and `_rels/*.rels` so the output opens cleanly in Microsoft Word and LibreOffice (no "found unreadable content" recovery). + +## Metadata sentinel (design D5) + +Scrubbed metadata fields are replaced with the sentinel string **`DocuDesk Anonymisation`** rather than deleted. Keeping the element with a recognisable value (a) signals in-file that the document was processed, and (b) defends against Word's "fill missing metadata on save" behaviour, which would otherwise re-populate `` with the current user on the next save — re-leaking PII. The sentinel is a single constant and can be changed to a generic value (e.g. `Anonymous`) without touching the surgery logic. + +## Hyperlink flattening (design D7) + +Hyperlinks are flattened to their visible text; the URL (which often carries PII such as `mailto:p.jansen@…` or query strings with case numbers) and its relationship entry are dropped. The visible link text remains and is then walker-anonymised in the entity pass. + +## Audit report + +Each sanitisation produces a `SanitizationReport` with per-category counts (comments removed, tracked changes accepted/dropped, revision attributes stripped, hyperlinks flattened, metadata fields scrubbed, custom XML parts dropped, field codes stripped, sentinel applied). The report is PII-free (counts only — never document content) and is retained on `DocumentProcessingHandler::getLastSanitizationReport()`. Logging follows ADR-005: only file ID, MIME type, strategy class and counts are logged. + +### Anonymisation log persistence + +Per-run rows land in the new `openregister_anonymisation_log` table. Each row captures the file id, originating object UUID / register / schema (when applicable), MIME type, engine label, status, structured failure reason, replacement count, wall-clock duration, and — for Office documents — the JSON-serialised `SanitizationReport` on the nullable `sanitization` column. The column is `null` for non-Office runs (PDF, plain text) and pre-sanitisation legacy rows. Consumers (DocuDesk's grondslagen-summary renderer) treat `null` as "no sanitisation data; not applicable" and do not raise an error. + +The entity / mapper live at: + +- `lib/Db/AnonymisationLog.php` — entity with `STATUS_SUCCESS` / `STATUS_FAILURE` constants and a `getSanitizationArray()` decode helper. +- `lib/Db/AnonymisationLogMapper.php` — `findByFileId()`, `findByObjectUuid()`, `findLatestSuccessForFile()`. +- `lib/Migration/Version1Date20260611000000.php` — idempotent create-if-missing migration. + +Persistence is best-effort — a DB-side failure is logged PII-free and swallowed so a successful redaction is never masked. + +## Encrypted documents + +Password-protected `.docx`/`.odt` cannot be sanitised without the password. The sanitiser raises a typed `SanitizationException` (reason `encrypted`); the anonymisation flow surfaces this as a caller-correctable "Cannot anonymise an encrypted document" error. + +## Scope + +- One-way only — the original file is the source of truth for the un-sanitised content; there is no reverse operation. +- v1 is always-on, all categories (no per-tenant or per-category toggles). +- Internal-document anchor hyperlinks (`#bookmark1`) are flattened along with external ones in v1. diff --git a/docs/features/realtime-updates.md b/docs/features/realtime-updates.md index 5f7907d12f..fb79652b1e 100644 --- a/docs/features/realtime-updates.md +++ b/docs/features/realtime-updates.md @@ -6,10 +6,10 @@ OpenRegister provides live data synchronization to connected clients via Server- ## Server-Sent Events (SSE) -SSE is the primary real-time transport. Clients open a persistent HTTP connection and receive a stream of events: +SSE is the primary real-time transport. Today SSE is served by the GraphQL subscription endpoint (see [GraphQL Subscriptions](#graphql-subscriptions) below); a dedicated REST-native SSE endpoint is planned: ``` -GET /api/realtime/subscribe?topic=register/meldingen-register/schema/meldingen +GET /api/sse/{register}/{schema} (planned — see the realtime-updates spec) ``` ### Topics @@ -24,7 +24,7 @@ GET /api/realtime/subscribe?topic=register/meldingen-register/schema/meldingen Multiple topics can be subscribed in one connection: ``` -GET /api/realtime/subscribe?topic[]=register/meldingen&topic[]=register/besluiten +GET /api/sse?topics=zaken/meldingen,zaken/besluiten (planned) ``` ### Event Format @@ -51,10 +51,10 @@ This ensures users only receive events for data they are authorized to see, even ### Reconnection with Event Replay -Clients that disconnect and reconnect can request missed events: +Clients that disconnect and reconnect can request missed events via the standard SSE `Last-Event-ID` header: ``` -GET /api/realtime/subscribe?topic=...&lastEventId=evt-099 +Last-Event-ID: evt-099 ``` The SSE service replays events since `lastEventId` from an in-memory ring buffer (configurable size, default 1000 events). Events older than the buffer are not replayed — clients should poll for a full resync. @@ -98,11 +98,12 @@ The `_updatedSince` parameter returns only objects updated after the given times ## API ``` -GET /api/realtime/subscribe SSE subscription endpoint -GET /api/realtime/topics List available topics -GET /api/realtime/status Connection statistics +POST /api/graphql GraphQL subscriptions over SSE (current SSE transport) +notify_push `notify_custom` events or-object-{uuid} / or-collection-{register-slug}-{schema-slug} ``` +> **Removed:** the DB-backed cursor-polling endpoints (`GET /api/realtime/events`, `GET /api/realtime/cursor`) and their append-only event table writer were removed in the `complete-live-updates` change — the write path ran on every object change but had zero consumers. Use notify_push events with an RBAC-filtered REST refetch, or GraphQL SSE subscriptions. + ## Standards | Standard | Role | diff --git a/docs/features/search-and-faceting.md b/docs/features/search-and-faceting.md index af301c02d3..2af93b2d77 100644 --- a/docs/features/search-and-faceting.md +++ b/docs/features/search-and-faceting.md @@ -158,6 +158,75 @@ POST /api/views Create a saved view GET /api/views/{id} Execute a saved view ``` +## Nextcloud Unified Search + +OpenRegister exposes a single, fleet-wide Nextcloud **unified search** provider +(the top-bar magnifier) over register objects, implemented by +`OCA\OpenRegister\Search\ObjectsProvider` (id `openregister_objects`). This is +the one place register objects surface in unified search for the entire fleet — +consuming apps (Pipelinq, Procest, …) do **not** register their own search +provider. They participate by claiming `(register, schema)` pairs through the +deep-link registry, which supplies each result's URL, icon, and display label. + +Key behaviours: + +- **Central access control** — the provider delegates to + `ObjectService::searchObjectsPaginated(query, _rbac: true, _multitenancy: true)` + and applies no second access filter of its own. Results contain only objects + the searching user may read: RBAC-granted objects plus objects readable + through the published predicate. Soft-deleted objects never appear, and tenant + isolation is enforced. The provider can only narrow this set further (it never + widens it). +- **`searchable` flag governs unified-search exposure** — the schema-level + `searchable` boolean (default `true`, editable via the schemas API and schema + edit modal) now controls whether a schema's objects appear in Nextcloud + unified search. Setting `searchable = false` removes a schema from the + magnifier; the exclusion is applied inside the search query (not by + post-filtering a page). An explicit `schema` filter that targets a + non-searchable schema returns an empty result set (opt-out wins). +- **Per-app labeling** — each result entry is labeled + `{App} · {Register} · {Schema}` and carries the owning app's rounded icon for + claimed pairs; unclaimed pairs keep the `Open Register · …` label and the + OpenRegister icon. +- **Excerpts** — the result subline ends with an excerpt around the first match + of the search term, falling back to the object's `summary`/`description`. + Excerpts come from the rendered object the user may read, so field-level + security applies to excerpt content. +- **Pagination** — results paginate with a cursor (integer offset), 25 per page, + so "load more" works for registers with thousands of objects. + +Apps declare their result URLs, icons, and display names via the boot-time +deep-link registry (`DeepLinkRegistrationEvent`); the registry's optional +`displayName` is what labels an app's unified-search results. + +## Search Trail Recording + +OpenRegister records a **search trail** for paginated searches so the dashboard's +"Popular Search Terms" widget and "Searches" KPI have data to display. Each trail +entry captures the search term, the result count on the returned page, the total +matching results, the response time, and the execution backend (database vs. +SOLR/index). Recording happens in `ObjectService::searchObjectsPaginated()`, so it +covers both backends transparently. + +What gets recorded is governed by two retention settings, combined into an +**effective mode**: + +- **`searchTrailsEnabled`** (boolean master switch) — when `false`, nothing is + recorded regardless of the mode below. When the setting cannot be read, + recording fails safe to enabled. +- **`searchTrailRecordingMode`** (`all` | `_search` | `none`, default `_search`) — + configurable from the **Retention** admin page without a code deploy: + - `_search` (default) — record only free-text searches (non-empty `_search`); + plain list and pagination calls record nothing. + - `all` — record every paginated search call. + - `none` — record nothing. + +Recording is best-effort: a write failure logs a warning and the search returns +normally, so analytics recording never degrades search availability. The settings +round-trip through `GET`/`PATCH /api/settings/retention`. + +**Standards**: BIO (audit logging), AVG/GDPR Article 30 (processing register context). + ## Related Features - [Registers & Schemas](registers-and-schemas.md) — facet configuration lives on schema properties diff --git a/docs/features/semantic-object-handoff.md b/docs/features/semantic-object-handoff.md new file mode 100644 index 0000000000..092e9f6047 --- /dev/null +++ b/docs/features/semantic-object-handoff.md @@ -0,0 +1,48 @@ +# Semantic Object Handoff + +Cross-app workflows hand objects over via **canonical semantic kinds** (`https://openregister.app/ns#Case`, `ns#Quote`, `ns#Contract`, `ns#Invoice`) — never via point-to-point bridges. OpenRegister owns the handoff/conversion engine (ADR-051, ADR-022 exclusivity): the emitting app never names a target app or schema slug; whichever installed schema *implements* the kind (ADR-048 markers) receives the object. + +## Standards & architecture references + +- ADR-051 — Cross-app workflows hand off via canonical semantic primitives (hydra) +- ADR-048 — Cross-app semantic references (the read direction; this feature is the write/convert direction) +- ADR-041 — Cross-app commands via events (the handoff-executed event shape) +- ADR-031 — Schema-declarative business logic (the `x-openregister-handoff` dialect) + +## Overview + +A **source schema** declares its handoffs in an `x-openregister-handoff` array (configuration block, ADR-031 dialect family). Each entry names a target kind URI, a trigger (`manual` or `lifecycle:`), a mapping onto the kind's **contract fields** using five expression kinds (`from`, `const`, `template`, `semanticRef`, `provenance`), an optional degradation mode (`whenUnavailable: hide | queue`), and an optional `onSuccess.set` update applied to the source after a successful handoff. + +An **implementing schema** claims a kind via its existing `implements[]` / `jsonld.type` / `x-schema-org` markers and binds each contract field to one of its own properties in a `handoffContract` block. Save-time validators reject malformed declarations (`handoff-bad-target-type`, `handoff-bad-mapping-expression`, `handoff-bad-success-update`) and incomplete bindings (`handoff-contract-incomplete`). + +Executing a handoff (HandoffService): + +1. Resolves the provider via `SemanticTypeResolver` (null-safe, disabled apps excluded, deterministic tie-break), filtered to schemas with a **complete** binding. +2. Creates the target object through the normal `ObjectService` write path **under the caller's RBAC** (create permission pre-checked — never escalated). +3. Links `handoff::handed-off-to` / `handoff::originated-from` provenance relations on both objects. +4. Writes one immutable audit row per side (`handoff.executed`, correlation id). +5. Applies `onSuccess.set` through the lifecycle-aware write path. +6. Dispatches a typed `HandoffExecutedEvent` (ADR-041 provenance + correlation id) so the providing app runs its intake logic in its own DI context. + +Failure at any step leaves no partial state (compensation removes the created target and restores the source). + +**Degradation** when no installed schema implements the kind: `hide` returns the machine-readable `handoff-provider-unavailable` response (409, never a 5xx) and the UI omits the action; `queue` parks the request durably in `oc_openregister_handoff_queue` and drains it automatically when a provider appears (schema-save / app-enable listeners + a 5-minute fallback job), re-evaluating the original requester's RBAC at drain time. + +## API + +| Method | Path | Behaviour | +|---|---|---| +| GET | `/api/objects/{register}/{schema}/{id}/handoffs` | Availability: every declared handoff with state `available` (naming the resolved provider schema), `unavailable` (+ machine-readable reason), or `queued` (+ queue entry). | +| POST | `/api/objects/{register}/{schema}/{id}/handoffs/{handoffId}` | Execute: `200 {status: executed, target: {register, schema, uuid}, correlationId}`, `202 {status: parked, queueEntry}` (queue mode), `404 handoff-not-declared`, `409 handoff-provider-unavailable`, `403` on RBAC refusal, `400` on target validation failure. | + +Both endpoints require authentication and enforce per-object RBAC (read for availability; write-on-source + create-on-target for execute). + +## Key capabilities + +- Declarative handoff dialect on the source schema — pure data, validated at save, versioned with the register +- Kind-contract binding on the provider — emitters never name a concrete target property +- Seed kind contracts: `ns#Case` (title, summary, channel, source + requester?, priority?) and the order chain `ns#Quote` / `ns#Contract` / `ns#Invoice` +- Semantic references carried as references (UUID), never copied blobs +- Engine-filled provenance pointer + bidirectional provenance relations + immutable audit +- Durable queue-mode deferral with drain-time RBAC re-evaluation and requester notifications +- Lifecycle-triggered handoffs for real actors (no system-user privilege lane) diff --git a/docs/features/tables-virtual-register.md b/docs/features/tables-virtual-register.md new file mode 100644 index 0000000000..c35768d717 --- /dev/null +++ b/docs/features/tables-virtual-register.md @@ -0,0 +1,22 @@ +# Nextcloud Tables as Virtual Registers + +Use any Nextcloud Tables table as a read-only **virtual register**: each table (or Tables View) becomes an auto-seeded Schema under a `tables` Register, and rows are served live through the object-source-provider seam — no copy, no sync job, no duplicate storage. + +## Standards & architecture references + +- Common Ground principle: data stays at the source; OpenRegister projects it. +- Hydra ADR-049 (virtual schemas via object-source providers). +- Spec: [`openspec/specs/tables-virtual-register/spec.md`](../../openspec/specs/tables-virtual-register/spec.md) + +## Overview + +When the Nextcloud Tables app is installed, OpenRegister reconciles a `tables` virtual register: one read-only Schema per Tables table, with a deterministic slug, seeded on app upgrade (repair step), on demand (`occ openregister:tables:sync`), and retired live when a table is deleted (Tables event listener). Tables column definitions are translated to JSON-Schema properties, and the standard objects API then lists, reads, counts, and paginates the table's rows live — as the acting user, so Tables' own ownership/share/context permissions decide visibility (denied reads are indistinguishable from absent objects). + +## Key capabilities + +- **Soft dependency**: no composer or install-time requirement on Tables; the provider registers via guarded class lookups and fails closed (empty results, logged warning) when the app is missing or disabled. +- **Column-type mapping**: text (line/long/rich/link) → string; number → number/integer with min/max (progress 0–100, stars 0–5); datetime → date-time/date/time formats; selection → enum, check → boolean, multi → enum array; usergroup → id/type object array. +- **Relation columns deep-link**: a Tables relation cell resolves to the referenced row's deterministic UUIDv5 in the referenced table's auto-seeded schema, so OR-level cross-schema linking works; falls back to the raw row id when the target schema is missing. +- **RBAC parity**: every read is performed as the acting user through Tables' own permission checks (ownership, shares, contexts); there is no enumeration oracle. +- **Pagination and View binding**: native limit/offset pushdown; other filters/sorts are applied provider-side; a schema can optionally bind a Tables **View** to inherit its server-side filter/sort. +- **Read-only by design**: writes to a Tables-bound schema are rejected by the object-source dispatch with an explicit read-only error. diff --git a/docs/features/text-extraction-sources.md b/docs/features/text-extraction-sources.md index ca5b04cd78..465555e3b6 100644 --- a/docs/features/text-extraction-sources.md +++ b/docs/features/text-extraction-sources.md @@ -303,7 +303,7 @@ flowchart TD Local --> Parse{File Format?} Parse -->|TXT/HTML| Direct[Direct Read] Parse -->|PDF| PDFLib[PDF Parser Library] - Parse -->|DOCX| DOCXLib[PhpOffice Library] + Parse -->|DOCX/DOC/ODT| DOCXLib[PhpOffice Library] Parse -->|Image| NotSupported[Not Supported] API --> DolphinProcess[AI Processing + OCR] @@ -328,6 +328,19 @@ flowchart TD style Error fill:#f44336 ``` +### Word-family extraction (DOCX / DOC / ODT) + +Word documents are parsed with PhpOffice/PhpWord. The reader is selected from the +file's MIME type / extension — `.docx` → `Word2007`, legacy `.doc` → `MsDoc`, +`.odt` → `ODText` — so all three formats load (previously only `.docx` did). + +Extraction is **content-complete**: a recursive element walker captures the full +document body **including tables** (and nested tables, in-cell text-runs and list +items), plus section **headers and footers** and document-level **footnotes and +endnotes**. A per-document parse failure (e.g. a legacy `.doc` the limited `MsDoc` +reader cannot read) is logged with structural detail only — no document content, +per ADR-005 — and yields an empty result rather than aborting the batch. + ### File Metadata Preserved When files are processed, the following metadata is maintained: diff --git a/docs/features/web-push-enable-toggle.png b/docs/features/web-push-enable-toggle.png new file mode 100644 index 0000000000..1033b9adae Binary files /dev/null and b/docs/features/web-push-enable-toggle.png differ diff --git a/docs/features/webhooks-and-notifications.md b/docs/features/webhooks-and-notifications.md index 5a5eb5315a..48773f0f9d 100644 --- a/docs/features/webhooks-and-notifications.md +++ b/docs/features/webhooks-and-notifications.md @@ -126,7 +126,7 @@ Rules live on the schema under `configuration['x-openregister-notifications']`. | `created` | Object created. | | `updated` | Object updated. | | `transition` | Object transitioned via the lifecycle state machine. Optional `trigger.action` filters to a specific action. | -| `scheduled` | Periodic. Requires `trigger.intervalSec >= 60`. The 60s `ScheduledNotificationJob` iterates the schema, optionally narrowed by `trigger.filter` (flat equality match on object data), and dispatches once per interval. | +| `scheduled` | Periodic. Requires `trigger.intervalSec >= 60`. The 60s `ScheduledNotificationJob` iterates the schema, optionally narrowed by `trigger.filter` (flat equality, relative-date and inequality operators — see below), and dispatches **at most once per object per dedup fingerprint** until the watched fields change (see *Scheduled trigger filters & per-object dedup* below). | | `threshold` | Aggregation crossed a threshold. Requires `trigger.aggregation` (declared on the same schema), `trigger.op` ∈ `[gt, gte, lt, lte, eq, ne]`, and `trigger.value`. `AggregationThresholdListener` re-runs the aggregation on object-write events and dispatches once per below→above transition. | #### Recipient kinds @@ -174,6 +174,61 @@ Rules live on the schema under `configuration['x-openregister-notifications']`. } ``` +#### Scheduled trigger filters & per-object dedup + +The `scheduled` trigger's `filter` is a flat `field => spec` map ANDed together. Each entry accepts either a **scalar** (strict equality, v1 behaviour) or an **operator object**: + +| Operator | Value form | Meaning | +|----------|------------|---------| +| `equals` | scalar | Strict equality. Same as the scalar shorthand. | +| `notEquals` | scalar | Strict inequality. A missing/`null` field satisfies `notEquals` for any non-null value. | +| `withinNext` | ISO-8601 duration (e.g. `PT24H`, `P7D`) | Field is a date or date-time in the half-open window `(now, now + duration]`. | +| `olderThan` | ISO-8601 duration | Field is a date or date-time strictly before `now − duration`. | + +Relative-date operators **fail closed**: unparsable values do not match, and the engine logs at debug only. All entries must hold for the object to match (AND semantics). The annotation validator rejects unknown operators, missing values, and malformed durations with HTTP 422. + +**Per-object dedup.** A scheduled rule dispatches at most once per `(schema, rule key, object, watched-field fingerprint)`. The fingerprint is a SHA-1 over the object's current values of the rule's **watched fields**: + +- By default, the watched fields are the filter fields that use a relative-date operator (`withinNext` / `olderThan`). +- Override with `trigger.dedupeFields: ["field1", "field2", …]` — a non-empty array of field names. Validated at save time on the same 422 contract as the operator grammar. +- When there are neither relative-date operators nor `dedupeFields`, the fingerprint is constant — the rule fires exactly once per object until pruned. + +Dedup state lives in `oc_openregister_notification_dedupe` (durable across cache flushes and worker restarts) and is pruned automatically when: + +- the object is deleted (`ObjectDeletedEvent` → `NotificationDedupePruneListener`); +- a rule key is removed/renamed in the schema annotation (`SchemaCreatedEvent`/`SchemaUpdatedEvent` → `NotificationDedupeAnnotationSyncListener`); +- a state row has not been re-evaluated for `notification_dedupe_retention_days` (default 90), reclaimed by the retention sweep that runs inside the scheduled job. + +The per-rule `intervalSec` throttle is unchanged: it bounds **scan frequency**, not delivery count. + +##### Worked example — `taskDueSoon` + +```json +{ + "x-openregister-notifications": { + "taskDueSoon": { + "trigger": { + "type": "scheduled", + "intervalSec": 3600, + "filter": { + "dueDate": {"operator": "withinNext", "value": "PT24H"}, + "status": {"operator": "notEquals", "value": "done"} + } + }, + "recipients": [{"kind": "field", "field": "assignee"}], + "channels": ["nc-notification", "email"], + "subject": "Task '{{title}}' is due within 24h" + } + } +} +``` + +- The job re-evaluates every hour. Tasks whose `dueDate` is within the next 24h **and** `status` is anything other than `done` match. +- The watched-field set is `["dueDate"]` (the only relative-date field). Each matching task dispatches exactly once for a given `dueDate`. Reschedule a task → new fingerprint → exactly one new reminder. Mark it `done` → no more matches; sweep eventually reclaims the dedup row. +- To re-arm whenever the assignee changes too, add `"dedupeFields": ["dueDate", "assignee"]` to the `trigger`. + +The `updated` trigger's field-change `only_if_changed` filter — documented earlier in this page — is independent of scheduled dedup; both pipelines stay decoupled. + #### Rendering In-app subjects are rendered by `AnnotationNotifier` (`object_created` / `object_updated` / `object_transitioned`), localised nl/en, with a primary action linking to the object detail view. A schema's per-locale `subject` wins; otherwise a canonical localised string is used. `Notifier` keeps `configuration_update_available` — the two notifiers are mutually exclusive by subject. Push needs no extra code: `notify_push` auto-intercepts the same `INotificationManager::notify()` call. @@ -191,10 +246,154 @@ Before delivering the in-app/push channel, the dispatcher resolves `schema-defau > The earlier per-`(register, schema)` `NotificationSubscription` table + controller are **deprecated**; existing rows are migrated to user-config overrides by a one-shot repair step and the table is scheduled for removal. +### Delivery Windows (Quiet Hours) & Fixed-Time Digest Schedules + +Independently of the per-`(schema, notification)` on/off override above, a user MAY configure a global **delivery window** (quiet hours) via `NotificationDeliveryWindowService` — stored the same override-only way, under a distinct per-user app-config key (`notification_delivery_window`), so a user who never configures one keeps today's immediate-delivery behaviour (zero-migration). + +| Endpoint | Description | +|----------|-------------| +| `GET /api/notification-delivery-window` | The current user's stored window, or `{enabled: false}` when none is configured. | +| `PUT /api/notification-delivery-window` | Store (`{enabled: true, start: "HH:MM", end: "HH:MM", timezone?, days?: [0-6]}`) or clear (`{enabled: false}`) the window for the current user only. `timezone` is an IANA name (e.g. `Europe/Amsterdam`); when absent it defaults to the Nextcloud server timezone. `start`/`end` wrap past midnight (e.g. `18:00`-`08:00`). | + +Two new dialect keys on a notification rule work with the delivery window: + +| Key | Shape | Behaviour | +|-----|-------|-----------| +| `critical` | `bool`, default `false` | When `true`, the rule bypasses quiet-hours queuing and always dispatches immediately (still subject to the preference-off / rate-limit / coalesce gates). Does NOT bypass a recipient's per-`(schema, notification)` opt-out. | +| `digest` | `{schedule: "daily"\|"weekly", at: "HH:MM", timezone?, weekday?: 0-6}` | A FIXED time-of-day batching schedule, distinct from the rolling `coalesce` window. A rule may declare at most one of `coalesce` or `digest` (mutually exclusive, HTTP 422). `weekday` (0=Sunday) is required for `schedule: "weekly"`, forbidden for `"daily"`. | + +**Dispatcher behaviour**: before delivering a per-recipient channel (`nc-notification`, `email`, `activity`), the dispatcher checks whether the recipient is inside their configured delivery window, or the rule's `digest` schedule has not yet reached its next fire time. When either applies and the rule is not `critical: true`, the event is **persisted as a `QueuedNotification` row — never dropped** — and notification history records `queued-quiet-hours` or `queued-digest` instead of `dispatched`. Broadcast channels (`webhook`, `talk`) are unaffected — they still fire once per dispatch. + +`NotificationQueueFlushJob` (a 60s `TimedJob`) scans the queue every tick and **re-evaluates each row's condition live** against the current wall clock in the window's/schedule's declared timezone — never a precomputed instant — so DST transitions are handled correctly by PHP's tz database. Rows sharing `(schema, rule, recipient)` are grouped into one summary notification per flush. This replaces the earlier `NotificationDigest`/`BatchNotificationJob` in-memory primitives, which were never wired to the dispatcher and could not survive the process boundary between the enqueuing request and the flush tick. + ### VNG Notificaties API Compliance For Dutch government interoperability, webhook payloads can be formatted according to the VNG Notificaties API standard via a Twig mapping configuration. This enables OpenRegister to act as a notificatiecomponent in a ZGW API landscape. +## Browser Web Push (rich background notifications) + +The `web-push` channel of the `x-openregister-notifications` dialect delivers a +rich OS-level notification — app icon, body, and up to two action buttons — +**even when the browser or all Nextcloud tabs are closed**. It complements the +`nc-notification` channel (which only nudges already-open tabs via `notify_push`). + +A rule opts in by listing `web-push` in `channels` and may declare `actions[]` + +`originApp` (see the dialect reference in ADR-031). Example — an incoming-call +notification with an "Open client" button: + +```jsonc +"incomingCall": { + "trigger": { "type": "created", "filter": { "field": "channel", "operator": "equals", "value": "telefoon" } }, + "originApp": "pipelinq", + "channels": ["nc-notification", "web-push"], + "recipients": [ { "kind": "field", "field": "agent" } ], + "subject": { "nl": "Inkomende oproep van {{client}}", "en": "Incoming call from {{client}}" }, + "actions": [ + { "label": { "nl": "Klant openen", "en": "Open client" }, "primary": true, + "target": { "kind": "object-detail", "object": { "kind": "relation", "field": "client" } } } + ] +} +``` + +### Setup (admin) + +Web Push needs a one-time VAPID keypair, generated and stored in app config: + +```bash +occ openregister:web-push:generate-vapid +``` + +The public key is exposed to browsers via `GET /apps/openregister/webpush/vapid-public-key`; +the private key never leaves the server. Status and the public key are shown in +**Admin settings → OpenRegister → Push notifications**. + +**Key rotation:** re-running the command replaces the keypair. Existing browser +subscriptions are bound to the old key and stop receiving pushes after a rotation — +they self-heal when the user re-enables the toggle (which re-subscribes against the +new key); the server prunes stale subscriptions on the first `404`/`410` response +from the push service. + +### Enabling browser notifications (user guide) + +Browser push is **opt-in** — OpenRegister never prompts for notification +permission on page load. To turn it on: + +1. Click your avatar (top-right) → **Settings**. +2. Open the **Notifications** section under *Personal*. +3. Find **Browser notifications** and switch on **"Enable browser notifications"**. +4. Your browser asks to show notifications — click **Allow**. + +![The "Enable browser notifications" toggle on the personal Notifications settings page](web-push-enable-toggle.png) + +When it is on, the line under the toggle reads **"Notification permission: +granted"**. From then on OpenRegister sends you a native notification — with the +originating app's icon and any action buttons (for example *Open client*) — +whenever a notification rule you are a recipient of fires, **even when no +Nextcloud tab is open**. + +To stop receiving them, switch the toggle **off** (this removes the browser +subscription). + +> Behind the scenes the toggle requests permission, registers the Service Worker +> (`js/openregister-push-sw.js`), and subscribes +> (`pushManager.subscribe({ userVisibleOnly: true, … })`). The subscription is +> stored per user + browser in the `openregister_push_subscriptions` table +> (infrastructure state — not an OpenRegister object). + +### Browser support / degradation + +| Browser | Background Web Push | +|---------|---------------------| +| Chrome / Edge | Supported — delivered while the browser keeps a background process alive (see below) | +| Firefox | Supported — delivered while the browser keeps a background process alive (see below) | +| Safari (macOS/iOS) | Only when the site is an **installed PWA**; otherwise no background push — degrades to the foreground `nc-notification` popup | + +### Receiving notifications when the browser is closed + +Browser push is delivered over your browser's **background connection** to its +push service (Chrome → FCM, Firefox → Mozilla autopush). The notification arrives +**without any Nextcloud tab open**, but a **browser process must be running**: + +| State | Result | +|-------|--------| +| A browser window is open (any site — no Nextcloud tab needed) | Arrives immediately | +| Browser running in the background (window closed, tray process alive) | Arrives immediately — *if* the browser is actually maintaining its push connection (see the reality check) | +| Browser fully closed / process asleep | The push is **queued** by the push service and delivered the moment you next open the browser — nothing is lost | + +This is standard browser/OS behaviour shared by every web-push site (Slack, +Gmail, …) — it is not specific to OpenRegister. For notifications that arrive +with **no browser running at all**, use the native Nextcloud desktop or mobile +apps, which have their own push channel. + +#### Keep Chrome receiving with all windows closed + +For Chrome to receive a push with no window open, it has to keep a background +process running. On Windows: + +1. Open Chrome **Settings → System** (or paste `chrome://settings/system` into the address bar). +2. Turn on **"Continue running background apps when Google Chrome is closed."** + +Microsoft Edge has the same option at `edge://settings/system`. + +> **Reality check.** This setting is *necessary but not sufficient*. Even with it +> on, whether a push is delivered while every window is closed depends on whether +> Chrome is actually keeping its background connection alive — Windows power +> management, how you closed Chrome, and Chrome's own throttling can still let the +> process sleep. When that happens the push is simply **queued and delivered the +> instant you reopen the browser** (no Nextcloud tab required, nothing lost). The +> only reliably-instant states are *a browser window open* or *the browser +> actively running in the background*. For guaranteed always-on delivery, use the +> native Nextcloud desktop/mobile apps. + +### Duplicate suppression + +When `web-push` is active and a Nextcloud tab is also open, the stock +notifications app would otherwise show its own plain popup for the same event. +Each notification carries a stable `tag` (`openregister--`) so +the rich Service-Worker notification and the stock popup collapse, and the +foreground client suppresses the stock popup while web-push is active. With the +browser closed there is only one source, so no suppression is needed. + ## Standards | Standard | Role | @@ -203,6 +402,8 @@ For Dutch government interoperability, webhook payloads can be formatted accordi | HMAC-SHA256 | Webhook request signing | | VNG Notificaties API | Dutch government notification format (via Mapping) | | Nextcloud INotificationManager | In-app notification delivery | +| Web Push Protocol (RFC 8030) + VAPID (RFC 8292) | Background browser notifications | +| aes128gcm (RFC 8291) | Web Push payload encryption (via `minishlink/web-push`) | ## Related Features diff --git a/docs/features/workflow-automation.md b/docs/features/workflow-automation.md index 7406b7491c..325034eb1f 100644 --- a/docs/features/workflow-automation.md +++ b/docs/features/workflow-automation.md @@ -160,6 +160,86 @@ GET /api/engines/{id}/workflows/{workflowId} Get a workflow DELETE /api/engines/{id}/workflows/{workflowId} Delete a workflow from an engine ``` +## Declarative Lifecycle Transitions (`x-openregister-lifecycle`) + +Beyond hook-driven automation, a schema can declare a **state machine** on a +single field via the `x-openregister-lifecycle` annotation (schema +`configuration`). The shared `TransitionEngine` interprets it centrally, so apps +get `/available-actions` and `/transition` without writing a controller per +schema. + +``` +GET /api/objects/{id}/available-actions List actions allowed from the current state +POST /api/objects/{id}/transition Apply a named action ({ "action": "" }) +``` + +### Static mode (`transitions`) + +`transitions` is a fixed `action → { from: [states], to: state }` map compared +against the object's current **literal** field value. The field must be a +`string` with an `enum` of the allowed states. + +### Graph mode (`graph`) + +For status graphs that are **data**, not a fixed enum — e.g. a `case.status` +that is a `$ref` UUID to a `statusType` object whose valid set differs per +parent `caseType` — declare a `graph` block instead. The engine derives the +available and target transitions **at runtime** from FK-scoped sibling objects. + +```json +{ + "field": "status", + "initial": { "from": "caseType", "field": "initialStatus" }, + "graph": { + "schema": "statustype", + "parentField": "caseType", + "parentFrom": "caseType", + "orderField": "order", + "finalField": "isFinal", + "allowedMoves": "forward" + } +} +``` + +| `graph` field | Description | +|---------------|-------------| +| `schema` | Sibling schema slug that holds the status objects | +| `parentField` | FK property on the sibling that references the parent | +| `parentFrom` | Property on the transitioning object holding the parent reference | +| `orderField` | Numeric ordering property on the sibling (UUID tiebreak) | +| `finalField` | Boolean terminal-state property on the sibling | +| `allowedMoves` | `forward` (next only), `adjacent` (previous + next), or `any` (every other sibling) | + +Derivation: read the parent reference from `object.data[parentFrom]`, fetch the +sibling objects of `schema` where `parentField` equals it (ordered by +`orderField`), locate the object's current state, and offer candidate targets +per `allowedMoves`. Each derived action has a stable id `move-to-`, +a `to` equal to the target UUID, and a `label` equal to the target's display +name. A **terminal** state (`finalField` true) is a sink under `forward` / +`adjacent`; `any` treats terminality as advisory and still offers the other +siblings. An **orphaned** current value (not among the siblings) recovers to the +first sibling. An object with an empty `parentFrom` yields no actions. + +`availableActions()` and the validation inside `transition()` share the SAME +derivation, so a client can only apply a `move-to-` the graph currently +allows; a non-candidate is rejected without mutating the object. + +**Static precedence:** when a schema declares BOTH a non-empty `transitions` map +and a `graph` block, the static `transitions` map is used and the `graph` block +is ignored (no sibling fetch). Static-only schemas are unaffected. + +### Auto-seed on create (object-form `initial`) + +When `initial` is the object form `{ "from": "", "field": "" }`, +the create pipeline seeds the lifecycle field from the parent BEFORE schema +validation — so a `required` lifecycle `$ref` field passes on a seeded create. +It runs only on the create path (never on update), only when the lifecycle field +is absent/null/empty (a client-supplied value is never overwritten), and is a +fail-soft no-op when the parent reference is empty, the parent cannot be loaded, +or the parent's `field` value is empty. Seeding dispatches no +`ObjectTransitionedEvent` — it is an initialisation, not a transition. The legacy +literal-string `initial` keeps its static-mode semantics and is not auto-seeded. + ## Standards | Standard | Role | diff --git a/docs/i18n.md b/docs/i18n.md new file mode 100644 index 0000000000..85c1a16dbb --- /dev/null +++ b/docs/i18n.md @@ -0,0 +1,241 @@ +# Internationalisation (i18n) + +OpenRegister ships first-class internationalisation across the object model, +the REST API, and the rendered HTTP responses. This document describes the +two pillars introduced by hydra ADR-025: + +1. **Source-of-truth** — declaring which language is canonical for a + translatable property, projecting that into the sidecar, and flipping + derived translations to `outdated` whenever the source value changes. +2. **API language negotiation** — explicit per-request language overrides + via query parameters and the write-side + `X-Translation-Target-Language` header. + +See also: `openspec/specs/register-i18n/spec.md`, +`openspec/changes/i18n-source-of-truth/`, +`openspec/changes/i18n-api-language-negotiation/`. + +## Schema declaration + +Translatable properties carry `translatable: true`. They may also declare +a canonical source language via the `sourceLanguage` modifier: + +```json +{ + "type": "object", + "properties": { + "title": { + "type": "string", + "translatable": true, + "sourceLanguage": "nl" + }, + "categoryCode": { + "type": "string" + } + } +} +``` + +- `sourceLanguage` is OPTIONAL. If omitted, OR uses + `Register.defaultLanguage` (the first element of + `Register.languages`, falling back to `'nl'`). +- `sourceLanguage` MUST NOT appear on properties without + `translatable: true`; the validator rejects this with a 400. +- The value MUST match basic BCP-47 syntax (`/^[a-z]{2,3}(-[a-zA-Z0-9]{2,8})*$/`). + +## Object-level source override + +Individual objects may override the schema's source-language for a single +property by writing to `_translationMeta..sourceLanguage`: + +```json +{ + "id": "obj-uuid", + "title": {"nl": "Welkom", "en": "Welcome"}, + "_translationMeta": { + "title": { + "sourceLanguage": "en" + } + } +} +``` + +Override precedence (highest first): + +1. object body `_translationMeta..sourceLanguage` +2. schema property `sourceLanguage` +3. `Register.defaultLanguage` +4. `'nl'` (hardcoded final fallback) + +## Translation projection + status + +The `openregister_translations` sidecar carries one row per +`(object_uuid, property, language)` tuple and now also tracks +`source_language`. The projection service writes that column on every +upsert; the `Translation` entity exposes it via +`getSourceLanguage()` and embeds it (alongside an `isSource` flag) in +`jsonSerialize()`. + +When the canonical source-language value for a property changes, +`SaveObject` invokes +`TranslationStatusService::markDerivedTranslationsOutdated($uuid, $property, $sourceLanguage)`, +which flips every other-language row for that property from +`approved` / `human_reviewed` / `machine_translated` to `outdated`. +Already-outdated and already-draft rows are not re-flipped. + +To back-fill `source_language` on data created before the migration ran: + +```bash +docker exec nextcloud php occ openregister:translations:backfill-source-language +# optional flags +docker exec nextcloud php occ openregister:translations:backfill-source-language \ + --batch-size=500 --dry-run +``` + +The command is idempotent: re-running it once every row has a non-empty +`source_language` reports `0 rows updated`. + +## Request-side language negotiation + +The middleware honours, in priority order: + +| Priority | Source | Notes | +| -------- | --------------------------------------- | ------------------------------------------------ | +| 1 | `?_lang=` | Canonical per-request override | +| 2 | `?language=` | Alias for `?_lang=` | +| 3 | `Accept-Language: ` | Existing browser-default fallback | +| 4 | `Register.defaultLanguage` | Resolved against the register's `languages` list | +| 5 | `'nl'` | Hardcoded final fallback | + +Invalid BCP-47 tags at any step are logged and fall through; no `400` +is ever emitted for a malformed language parameter. + +`LanguageService::getRequestedLanguageSource()` returns +`'query' | 'header' | 'default'`, useful for telemetry and the +`X-Source-Language` response setter. + +## Write-side `X-Translation-Target-Language` + +Translators can edit a single language without sending the full +language-keyed body by adding the header on POST/PUT/PATCH: + +```http +PATCH /api/objects/r/s/uuid HTTP/1.1 +Content-Type: application/json +X-Translation-Target-Language: en + +{"title": "Welcome"} +``` + +The handler wraps the scalar value under the target language, persisting +`title.en = "Welcome"` without touching `title.nl`. Sending a full +language-keyed body PLUS the header is a conflict and returns +`400 Bad Request` with body: + +```json +{ + "error": { + "code": "TRANSLATION_TARGET_CONFLICT", + "property": "title", + "targetLanguage": "en" + } +} +``` + +## Translation metadata envelope + +When `?_translationMeta=true` is on the request, every translatable +property in the response gets a sibling entry under +`_meta.languageMeta.`: + +```json +{ + "id": "obj-uuid", + "title": "Welcome", + "_meta": { + "languageMeta": { + "title": { + "served": "en", + "sourceLanguage": "nl", + "isSource": false, + "status": "approved" + } + } + } +} +``` + +Use cases: + +- A translation UI rendering "Original (Dutch)" vs "Translation + (out of date)" badges. +- A reviewer dashboard summarising freshness ratios. + +The envelope is OFF by default (no payload bloat for clients that +don't need it). + +## Response headers + +| Header | When | Example | +| ---------------------------- | -------------------------- | -------- | +| `Content-Language` | Always | `en` | +| `X-Content-Language-Fallback`| Fallback chain used | `true` | +| `X-Source-Language` | Single-object content | `nl` | + +`X-Source-Language` reflects the dominant source language across the +object's translatable properties (or the only source language when there +is exactly one). + +## Translation search filters + +`GET /api/translations/search` accepts the standard +`?query=`, `?language=`, `?status=`, `?objectUuid=` filters plus the new: + +- `?sourceLanguage=` — restrict to rows whose canonical source + language matches the given tag. +- `?isOutOfDate=true` — restrict to rows in `outdated` status. +- `?compareToSource=true` — return both `value` and `sourceValue` + side-by-side per matched row. + +## Client snippets + +### curl + +```bash +# Read in English; declare the explicit override even though the browser sends Dutch. +curl -H "Accept-Language: nl" \ + "https://or.example/api/objects/r/s/uuid?_lang=en" + +# Edit only the German translation of `title` on a single object. +curl -X PATCH \ + -H "Content-Type: application/json" \ + -H "X-Translation-Target-Language: de" \ + -d '{"title": "Willkommen"}' \ + "https://or.example/api/objects/r/s/uuid" +``` + +### axios + +```ts +// Read. +await axios.get(`/api/objects/${r}/${s}/${id}`, { params: { _lang: 'en' } }); + +// Write the French translation. +await axios.patch( + `/api/objects/${r}/${s}/${id}`, + { title: 'Bienvenue' }, + { headers: { 'X-Translation-Target-Language': 'fr' } } +); +``` + +### Postman + +Add `_lang` to the **Params** tab for reads. For writes, add a header +`X-Translation-Target-Language: ` and send a scalar body. + +## Hydra references + +- ADR-025 (parent decision) — + `hydra/openspec/architecture/adr-025-i18n-source-of-truth.md`. +- `openspec/changes/i18n-source-of-truth/` — source-of-truth half. +- `openspec/changes/i18n-api-language-negotiation/` — API negotiation half. diff --git a/docs/package-lock.json b/docs/package-lock.json index 96b3d8c008..2ee034476b 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,7 +8,7 @@ "name": "openregister-docs", "version": "0.0.0", "dependencies": { - "@conduction/docusaurus-preset": "^3.5.0", + "@conduction/docusaurus-preset": "^3.24.0", "@docusaurus/core": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0", "@docusaurus/theme-mermaid": "^3.7.0", @@ -1912,9 +1912,9 @@ } }, "node_modules/@conduction/docusaurus-preset": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-3.9.0.tgz", - "integrity": "sha512-WYzKUiF0VyRzhy0ChReAx/w/Dn0HfIKemtr0R+Skt+Q+2+ekA4yffZLJr4zCXexi6wW17XDqh+m9sqsXQqh3RQ==", + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-3.24.0.tgz", + "integrity": "sha512-T6LwvArwaF6QZfnc36zSTiOGEKmCIXaIBjiLAEFR7mcC1EfCUrZI6Dsf1z9BtC971jruTX3cE3+l4NxMo3LgzQ==", "license": "EUPL-1.2", "bin": { "validate-ai-baseline": "bin/validate-ai-baseline.mjs" @@ -20182,9 +20182,9 @@ "optional": true }, "@conduction/docusaurus-preset": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-3.9.0.tgz", - "integrity": "sha512-WYzKUiF0VyRzhy0ChReAx/w/Dn0HfIKemtr0R+Skt+Q+2+ekA4yffZLJr4zCXexi6wW17XDqh+m9sqsXQqh3RQ==" + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-3.24.0.tgz", + "integrity": "sha512-T6LwvArwaF6QZfnc36zSTiOGEKmCIXaIBjiLAEFR7mcC1EfCUrZI6Dsf1z9BtC971jruTX3cE3+l4NxMo3LgzQ==" }, "@csstools/cascade-layer-name-parser": { "version": "2.0.4", diff --git a/docs/package.json b/docs/package.json index 10ae3a472b..2cf2db5405 100644 --- a/docs/package.json +++ b/docs/package.json @@ -17,7 +17,7 @@ "ci": "npm ci --legacy-peer-deps && npm run build" }, "dependencies": { - "@conduction/docusaurus-preset": "^3.5.0", + "@conduction/docusaurus-preset": "^3.24.0", "@docusaurus/core": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0", "@docusaurus/theme-mermaid": "^3.7.0", diff --git a/docs/quality-assurance.md b/docs/quality-assurance.md index b623e4e1ed..021be27e4d 100644 --- a/docs/quality-assurance.md +++ b/docs/quality-assurance.md @@ -2,6 +2,34 @@ This document describes the quality assurance tools and processes used in the OpenRegister application and all Conduction Nextcloud apps. +## CI Gate — `composer check:strict` + +The authoritative pre-merge gate is: + +```bash +composer check:strict +``` + +This runs `phpcs + phpmd + phpstan + psalm + test:all` in sequence. The same command runs in CI via `.forgejo/workflows/pre-merge-check-strict.yaml` on every PR targeting `development`, `main`, or `beta`. **All commands must exit 0 before pushing.** + +### Current baseline state (2026-06-04) + +| Gate | Status | +|---|---| +| PHPCS | ✅ 0 errors — gate clean | +| PHPMD | ⚠️ 52 violations — tracked in `openspec/changes/openregister-legacy-quality-cleanup/` | +| PHPStan | ⚠️ 1,371 baseline entries — tracked in `phpstan-baseline.neon` | +| Psalm | ✅ baseline managed | +| PHPUnit | ✅ passing | + +Burn-down progress is tracked in issue #25 and spec +`openspec/changes/openregister-legacy-quality-cleanup/` (audit report: +`quality-inventory-2026-06-04.md`). + +**Note:** The `composer phpmd` script uses a `|| echo 'PHPMD not installed, skipping...'` +fallback that causes PHPMD to exit 0 even when violations exist. This will be fixed once +all violations are cleared (see task 5.2 in the burn-down spec). + ## Overview We use two main tools for quality assurance: diff --git a/docs/services/entity-relation-grondslagen.md b/docs/services/entity-relation-grondslagen.md new file mode 100644 index 0000000000..988af79d9d --- /dev/null +++ b/docs/services/entity-relation-grondslagen.md @@ -0,0 +1,153 @@ +# Entity-Relation Grondslagen — Decision-metadata PATCH contract + +Reviewers of an anonymised document need a way to record their +operator decision *per entity occurrence*: which legal grondslagen +justify the redaction (or non-redaction), and whether a specific +occurrence should be left untouched. The +`entity-relation-grondslagen` change adds two fields to the +`oc_openregister_entity_relations` row and one PATCH endpoint to +write them, alongside a defensive read-side filter so a row marked +`skip_anonymization = true` is never redacted regardless of caller +behaviour. + +## Row-shape additions + +The `EntityRelation` entity now carries two operator-decision fields: + +| Column | Type | Default | Semantics | +|-----------------------|-------------------|---------|---------------------------------------------------------------------------| +| `bases` | JSON (nullable) | `NULL` | Optional array of UUIDs referencing legal grondslagen. | +| `skip_anonymization` | BOOLEAN | `false` | Operator decision NOT to redact this occurrence. | + +Both columns are added by the idempotent migration +`Version1Date20260512120000.php` (guarded with `hasColumn` so a re-run +is a no-op). Existing rows read back as `bases = null` and +`skipAnonymization = false`. + +`bases` is multiset-equal on diff (order-insensitive, duplicates +collapsed) so cosmetic reorderings do not produce spurious audit +entries. The distinction between `null` and `[]` is preserved. + +## PATCH endpoint + +``` +PATCH /api/entity-relations/{id} +Content-Type: application/json +Authorization: + +{ + "bases": ["", ...], // optional + "skipAnonymization": true | false // optional +} +``` + +Behaviour: + +- **Whitelist enforcement.** Body keys MUST be a strict subset of + `{bases, skipAnonymization}`. Any other key (e.g. `anonymized`, + `anonymizedValue`) returns HTTP 400 with body + `{ "error": "field_not_allowed", "field": "" }`. The + post-hoc system fields `anonymized` / `anonymizedValue` are + intentionally NOT writable here — those record what the redaction + code path actually did and remain owned by `markAsAnonymized`. +- **Shape validation.** `bases` MUST be `null` or `array`; + `skipAnonymization` MUST be a `bool`. Anything else → HTTP 400. +- **Authorisation.** Resolution order: + 1. If the row has `fileId`: caller MUST be able to write the file + (the same implicit check `markAsAnonymized` inherits today). + 2. Else if the row has `objectId` (with `registerId` + `schemaId`): + caller MUST be able to update the object. + 3. Else if the row has `emailId`: caller MUST be able to access + the email. + 4. Otherwise deny. + + Unauthenticated session → HTTP 401 (the NC framework rejects + before the controller runs; `@NoAdminRequired` does not bypass + session auth). +- **Semantic no-op.** A PATCH whose values match the current state, + or an empty body, returns HTTP 200 with the unchanged row and + writes NO audit entry. +- **Diff-aware audit.** A non-empty diff persists the change AND + emits exactly one immutable audit-trail entry (ADR-022). The + entry payload captures: + - `actor`: the acting user UID (per ADR-005 — the UID, NOT the + display name). + - `subjectType`: `openregister_entity_relations`. + - `subjectId`: the row id. + - `timestamp`: ISO-8601. + - `changedFields`: object whose keys are ONLY the fields that + actually changed; each value `{ previous: , new: }`. +- **Transactional integrity.** The row UPDATE and the audit-trail + INSERT run inside a single DB transaction. An audit-INSERT + failure rolls back the UPDATE and surfaces as HTTP 500 — clients + never observe a persisted decision-metadata change without a + matching audit entry. +- **Post-commit event.** After commit, the mapper dispatches one + `EntityRelationDecisionUpdatedEvent` (Symfony event, isolated in + its own try/catch so a listener failure does not mask the + persisted state change). Listeners can subscribe to the + `isSkipAnonymizationActivated()` convenience for the common + `false → true` trigger. + +Successful response shape (200 OK): + +```json +{ "...row fields...", "bases": [...], "skipAnonymization": true } +``` + +Validation-error response shape (400): + +```json +{ "error": "", "field": "" } +``` + +## Skip-aware anonymise flow + +Three independent enforcement layers ensure that a row with +`skip_anonymization = true` is never redacted regardless of +caller behaviour: + +1. **`EntityRelationMapper::markAsAnonymized($fileId, $value)`** — + added `AND skip_anonymization = 0` predicate. Skipped rows + retain `anonymized = false` after the file's anonymise pass. +2. **`FileTextController::anonymizeFile`** — now reads relations + through the new mapper method `findEntitiesForAnonymization` + which filters `skip_anonymization = 0` at the SQL layer. The + replacements list never contains skipped occurrences. +3. **`DocumentProcessingHandler::anonymizeDocument`** — + defensively filters the caller-supplied entity array against + `findSkippedEntityValuesForFile` before delegating to the + text-replace pipeline. Even a caller that includes a skipped + row in its payload will see it filtered out at OR. + +The `skip_anonymization` flag is forward-looking: flipping it to +`true` on an already-anonymised row does NOT retroactively +un-redact the file. Re-running the anonymise pass on the same +file with the updated flag is safe and idempotent — already-redacted +occurrences stay redacted; newly-skipped occurrences are left +untouched in any future pass. + +## Retry-by-omission pattern + +A reviewer who PATCHes only `{bases: [...]}` leaves +`skipAnonymization` unchanged. Omitted fields retain their stored +value. Two consecutive PATCHes setting `bases` to the same array +produce one audit entry (the first); the second is a semantic +no-op. + +## Cross-references + +- `entity-relation-grondslagen` openspec change (this contract). +- ADR-005 — PII-free logging / actor UID (not display name) in + audit entries. +- ADR-022 — immutable audit-trail conventions. +- `lib/Db/EntityRelationMapper.php` — `updateDecisionMetadata`, + `markAsAnonymized`, `findEntitiesForAnonymization`, + `findSkippedEntityValuesForFile`. +- `lib/Controller/EntityRelationsController.php` — PATCH handler. +- `lib/Event/EntityRelationDecisionUpdatedEvent.php` — post-commit + Symfony event. +- `tests/Unit/Db/EntityRelationMapperUpdateDecisionMetadataTest.php` + — diff-aware audit, whitelist, no-op semantics (19 tests). +- `tests/Unit/Controller/EntityRelationsControllerTest.php` — + 200 / 400 / 401 / 403 / 404 / 500 paths (11 tests). diff --git a/docs/services/register-resolver.md b/docs/services/register-resolver.md new file mode 100644 index 0000000000..0e131fa6f8 --- /dev/null +++ b/docs/services/register-resolver.md @@ -0,0 +1,106 @@ +# RegisterResolverService + +`OCA\OpenRegister\Service\RegisterResolverService` is a small, DI-friendly +public service that consumer apps use to resolve `_register` and +`_schema` `IAppConfig` keys into either bare slug/UUID strings or +hydrated `Register` / `Schema` entities. It replaces the duplicated +`getValueString(...)` + manual mapper-lookup pattern observed across +opencatalogi (5 controllers), pipelinq (8 services / jobs), and docudesk +(`OpenRegisterResolver` helper). + +The service is **multi-tenant aware** — it goes through the same +`RegisterMapper` / `SchemaMapper` `find()` calls every other OR code path +takes, so `MultiTenancyTrait::applyOrganisationFilter` is honoured for +free. An explicit `$organisationUuid` override on every resolve method is +available for cross-tenant admin tooling. Resolved entities are cached at +the request scope so a single PHP request only pays one mapper hit per +`(appId, configKey, organisationUuid)` tuple. + +## Naming convention + +| Form | When to use | Example | +|---------------------------|----------------------------------------------------------|--------------------------| +| `_register` | Preferred. New code MUST use this shape. | `theme_register` | +| `_schema` | Preferred. New code MUST use this shape. | `listing_schema` | +| Bare `register` / `schema`| Grandfathered for legacy consumers (pipelinq). | `register` | + +`enumerateAppConfigs()` surfaces the bare keys as `default_register` / +`default_schema` so admin UIs can flag them as legacy convention. + +## Consuming-app migration + +Before — every consumer ships some variant of this: + +```php +$registerSlug = $this->appConfig->getValueString( + Application::APP_ID, + 'theme_register', + '' +); +if ($registerSlug === '') { + return new JSONResponse(['error' => 'Theme register not configured'], 500); +} +$register = $this->registerMapper->findBySlug($registerSlug); +``` + +After — the resolver returns a hydrated entity or throws a typed exception: + +```php +use OCA\OpenRegister\Service\RegisterResolverService; +use OCA\OpenRegister\Service\Resolver\Exception\MissingConfigException; +use OCA\OpenRegister\Service\Resolver\Exception\RegisterNotFoundException; + +public function __construct(private readonly RegisterResolverService $resolver) {} + +public function showTheme(): JSONResponse { + try { + $register = $this->resolver->resolveRegister( + Application::APP_ID, + 'theme_register' + ); + } catch (MissingConfigException $e) { + return new JSONResponse([ + 'error' => 'Theme register not configured', + 'configKey' => $e->getConfigKey(), + ], 500); + } catch (RegisterNotFoundException $e) { + return new JSONResponse([ + 'error' => 'Theme register not found', + 'resolvedValue' => $e->getResolvedValue(), + ], 500); + } + + return new JSONResponse($register->jsonSerialize()); +} +``` + +## Diagnostic CLI + +A new console command lists every resolver-shaped config key currently +set for an app, mirroring `enumerateAppConfigs()`: + +```bash +$ occ openregister:resolver:list opencatalogi +Resolver keys for app "opencatalogi": + listing_register = cms + listing_schema = listing-v2 + theme_register = theme-2026 + theme_schema = theme-v1 +``` + +## Public API + +| Method | Returns | Throws | +|----------------------------------------------------------------------------------------------|------------------------|----------------------------------------------------------------------------------------------------------| +| `resolveRegisterId(appId, configKey, default?, organisationUuid?)` | `string` | `MissingConfigException` | +| `resolveSchemaId(appId, configKey, default?, organisationUuid?)` | `string` | `MissingConfigException` | +| `resolveRegister(appId, configKey, default?, organisationUuid?)` | `Register` | `MissingConfigException`, `RegisterNotFoundException` | +| `resolveSchema(appId, configKey, default?, organisationUuid?)` | `Schema` | `MissingConfigException`, `SchemaNotFoundException` | +| `resolvePair(appId, registerKey, schemaKey, organisationUuid?)` | `RegisterSchemaPair` | `MissingConfigException`, `RegisterNotFoundException`, `SchemaNotFoundException` | +| `enumerateAppConfigs(appId)` | `array` | - | +| `clearCache()` | `void` | - | + +Exceptions live under `OCA\OpenRegister\Service\Resolver\Exception\` to +avoid name collision with the generic +`OCA\OpenRegister\Exception\RegisterNotFoundException` / +`SchemaNotFoundException` used by the entity layer. diff --git a/docs/services/runtime-schema-api.md b/docs/services/runtime-schema-api.md new file mode 100644 index 0000000000..38fd07b107 --- /dev/null +++ b/docs/services/runtime-schema-api.md @@ -0,0 +1,84 @@ +# Runtime Schema API + +OpenRegister exposes runtime CRUD on `/api/schemas` and `/api/registers` +so a non-admin citizen developer (driven by an in-Nextcloud builder UI +such as OpenBuild's schema editor) can author a full data model without +shipping a PHP `_register.json` PR. This document captures the contract +that makes those routes correct for runtime use: cache invalidation, +DELETE safety, auto-Register provisioning on import, and the +slug-aware search helper. + +## Cache invalidation on mutation + +Every successful `POST` / `PUT` / `PATCH` / `DELETE` on a schema or a +register calls into the cache handler before the controller returns: + +- `SchemaCacheHandler::invalidate(int $schemaId)` clears the + in-process schema cache + the request-scoped `SchemaMapper::findCache` + entry. The next read in the same PHP worker re-fetches from the DB. +- `RegisterCacheHandler::invalidate(int $registerId)` does the same + for registers (registers don't have a persistent cache table; the + request-scoped mapper cache is the relevant clearance target). + +This closes the "create schema → immediately use it" loop that +runtime authoring requires. + +## DELETE safety + +``` +DELETE /api/schemas/{id} +DELETE /api/registers/{id} +``` + +Without `?force=true`, the controller refuses to delete an entity that +still owns objects: returns HTTP 409 with body +`{ "error": "schema-has-objects", "objectCount": N }` (or +`register-has-objects` on registers). With `?force=true`, the delete +proceeds and a WARNING is logged with the calling user, the entity +slug, and the orphan-object count for audit-trail review. + +## `importFromApp` auto-Register + +When a configuration imported via `ImportHandler::importFromApp` +carries `x-openregister.type=application`, the handler now derives a +Register entity from `x-openregister.app` (slug), `info.title` +(title), and `info.description` (description). Lookup is idempotent +on `(slug, organisationId)` so re-imports update rather than +duplicate; schemas[] is unioned so already-attached schemas are +preserved across re-imports. Configurations of any other `x-openregister.type` +or without the marker are NOT auto-provisioned — the previous +behaviour is preserved. + +## Slug-aware search helper + +```php +$results = $objectService->searchObjectsBySlug( + 'openbuild', // register slug + 'application', // schema slug + ['type' => 'service'] +); +``` + +`ObjectService::searchObjects` requires numeric `@self.register` and +`@self.schema` for the fast path; passing slugs silently downgraded +the search before this spec landed. `searchObjectsBySlug()` resolves +both slugs through the mappers' standard organisation-filtered +`find()` and delegates to `searchObjects()`. An unknown slug +(either side) throws `OCP\AppFramework\Db\DoesNotExistException` +identifying which side failed; foreign-organisation slugs throw +the same exception (the resolver never crosses tenants). + +## Smoke test reference + +The `bootstrap-openbuild` smoke test (commit 3138e4c, openbuild repo) +documents the manual flow this spec was driven from. Re-running it +against this branch confirms: + +1. `POST /api/configurations/import-from-app` of an `application`-type + OAS produces a complete `(Configuration, Schemas, Register)` triple + in one call — no manual `POST /api/registers` needed. +2. `searchObjectsBySlug('openbuild', 'application', [])` returns the + seeded objects (pre-spec: zero results). +3. Editing a schema via `PUT /api/schemas/{id}` then re-listing + immediately surfaces the new state in the same PHP worker (cache + invalidation works). diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index fe27d6be52..46a7dae442 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -235,7 +235,6 @@ export default function Home() { ['notIn' => ['cancelled', 'draft']], + 'createdAt' => ['gte' => '$startOfMonth'], // placeholders are resolved + ], + groupBy: ['field' => 'costCenter'], // optional — omit for a single scalar +); + +$result = $this->aggregationRunner->runAdhocByRef( + registerRef: 'finance', // register slug / uuid / id + schemaRef: 'invoice', // schema slug / uuid / id + query: $query, +); +``` + +### Method signature + +```php +AggregationRunner::runAdhocByRef( + string $registerRef, + string $schemaRef, + AggregationQuery $query +): array +``` + +Throws `NotAuthorizedException` (→ HTTP 403) when the caller lacks `list` +permission on the schema, and `RuntimeException` (→ 404) when the register +or schema ref can't be resolved. + +### Return shape + +Ungrouped (no `groupBy`): + +```php +['value' => 1234.5, 'backend' => 'postgres', 'cached' => false] +``` + +Grouped (`groupBy` set): one bucket per distinct group value. + +```php +[ + 'groups' => [ + ['key' => 'cc-100', 'value' => 800.0], + ['key' => 'cc-200', 'value' => 434.5], + ], + 'backend' => 'postgres', + 'cached' => false, +] +``` + +`value` is an `int` for `count`, a `float` for `sum`/`avg`/`min`/`max`, and +`null` when the metric has no matching rows (empty set). `backend` reports +the engine that served the request (see the Backend matrix below). + +### Filter operator vocabulary + +Per field, the filter map accepts a bare scalar (equality) or an operator +sub-map. Supported operators: + +| Operator | Operand | Meaning | +|---|---|---| +| (scalar) | scalar | `field = value` | +| `in` | array | `field IN (...)` | +| `notIn` | array | `field NOT IN (...)` — empty array excludes nothing (retains all rows) | +| `ne` | scalar | `field <> value` | +| `gt` / `gte` / `lt` / `lte` | scalar | range comparisons | + +The same `in` / `notIn` / `ne` / `gt` / `gte` / `lt` / `lte` operator +vocabulary is also reachable through the standard `ObjectService::findAll(array $config)` +/ `count(array $config)` config path (`config['filters']`), so a consuming +app can exclude a set of values (`'status' => ['notIn' => [...]]`) on an +ordinary object query without aggregating. + ## Backend matrix The runner picks the matching native bucketing primitive for the active database engine and falls back to PHP only on engines OpenRegister doesn't natively target. diff --git a/docs/technical/removing-solr-and-publishing-migration.md b/docs/technical/removing-solr-and-publishing-migration.md new file mode 100644 index 0000000000..b9610c378b --- /dev/null +++ b/docs/technical/removing-solr-and-publishing-migration.md @@ -0,0 +1,99 @@ +# Migration: removing SOLR and Register/Schema publishing + +This release removes two deprecated subsystems from OpenRegister: + +1. **The SOLR search index and the entire external search backend abstraction** + (including the Elasticsearch backend). Full-text search, faceting, and + aggregation now run exclusively on the built-in database (Magic-Tables / + PostgreSQL) path. +2. **Register/Schema publishing** — the `published`/`depublished` columns and the + publish/depublish UI. Anonymous visibility of registers and schemas is now + decided entirely by RBAC. + +Both removals are **BREAKING**. This guide describes the operator actions +required so nothing silently breaks after upgrading. + +## 1. SOLR / external search backend + +No data migration is required: the SOLR index was a derived copy of data that +already lives in the database. After upgrading: + +- Remove the `solr` (and `zookeeper`) services from your deployment — they are no + longer used. The docker compose `solr` profile and `resources/solr/` + configsets have been removed from the app. +- The `search_backend` configuration value is ignored; the active backend is + always `database`. +- The following HTTP endpoints have been **removed** and now return `404`: + `/api/settings/solr*`, `/api/solr/*` (collections, configsets, fields, + dashboard, setup, manage), `/api/settings/solr-info`, + `/api/settings/solr-facet-config`, and the SOLR-backed file endpoints + (`/api/solr/warmup/files`, `/api/solr/files/*`, `/api/search/files/keyword`, + `/api/files/chunks/process`, `/api/files/chunks/stats`). +- In-process semantic/hybrid vector search (PostgreSQL `pgvector`) is unchanged; + its endpoints (`/api/search/files/semantic`, `/api/search/files/hybrid`, + `/api/settings/search/semantic|hybrid`, `/api/objects/vectorize/*`, + `/api/vectors/test-embedding`, …) keep working. + +> Note: SOLR's relevance ranking (BM25/TF-IDF) is gone. Database search uses +> `ILIKE`/`pg_trgm` matching without relevance scoring. + +## 2. Register / Schema publishing → RBAC + +The `published`/`depublished` columns on the registers and schemas tables are +dropped by migration `Version1Date20260624000000`. Previously, a register or +schema was visible to **anonymous** callers when it had a `published` timestamp +(and was not depublished). + +Anonymous visibility is now expressed as an **RBAC rule** on the entity's +`authorization` block: a register or schema is anonymously readable when its +`authorization.read` grants access to the `public` group. + +### Required operator action + +For **every register or schema that was previously published** and must stay +publicly readable, add `public` to its `read` authorization **before or right +after** upgrading. Minimal form: + +```json +{ + "authorization": { + "read": ["public"] + } +} +``` + +To express a time-based publication window (the equivalent of a future +`published` date), use the RBAC `$now` dynamic variable against a date property +on the object — the same pattern the object-level deprecation established: + +```json +{ + "authorization": { + "read": [ + { "group": "public", "match": { "publicatiedatum": { "$lte": "$now" } } } + ] + } +} +``` + +There is **no automated data migration** of publication state into RBAC rules: +the intended audience of a previously-published resource cannot be inferred +safely, so operators add the rule explicitly. + +> Rollback: the column-drop migration is destructive. It is sequenced as the +> last deploy step so application code can be reverted independently before the +> columns are dropped. After the columns are dropped, restoring them requires a +> database backup. + +## 3. File auto-share: `autoPublish` → `autoShare` + +The schema/property config key `autoPublish` conflated object publishing (now +removed) with Nextcloud **file sharing**. The file auto-share behaviour is +preserved under a new, file-scoped key: **`autoShare`**. + +- Rename any schema or property file-config `"autoPublish": true` to + `"autoShare": true` to keep auto-creating a public share for uploaded files. +- `autoPublish` is **no longer read for any purpose**. A schema still carrying + `autoPublish` simply stops auto-sharing and logs a one-time deprecation + warning naming `autoShare`. +- This is a manual, documented rename — there is no automated config rewrite. diff --git a/img/app.svg b/img/app.svg index a63d33293d..a61e2721af 100644 --- a/img/app.svg +++ b/img/app.svg @@ -1,16 +1,9 @@ - - - - + - + - \ No newline at end of file + diff --git a/js/openregister-push-client.js b/js/openregister-push-client.js new file mode 100644 index 0000000000..5e911a1ccb --- /dev/null +++ b/js/openregister-push-client.js @@ -0,0 +1,198 @@ +/** + * OpenRegister Web Push subscribe client. + * + * Loaded on EVERY full-page Nextcloud render by PushClientScriptListener + * (openregister-web-push-engine). It is OPT-IN ONLY: it NEVER calls + * Notification.requestPermission() or subscribes on load. It exposes + * window.OCA.OpenRegister.WebPush.{ enablePush, disablePush, isSupported, + * permission } for the settings UI to call on an explicit user gesture, per + * Chrome's permission-abuse rules. + * + * Plain static asset (NOT bundled through webpack); uses the OC.* globals and + * fetch so it can run standalone on any page. + * + * @license EUPL-1.2 + * @copyright 2026 Conduction B.V. + */ + +(function() { + 'use strict' + + var SW_URL = OC.filePath('openregister', 'js', 'openregister-push-sw.js') + // A Service Worker can only claim a scope at or below its own served path. + // The SW is served from .../custom_apps/openregister/js/, so derive the scope + // from SW_URL's own directory rather than the /apps/ route (a different path, + // which makes register() throw a SecurityError → subscription never created). + var SW_SCOPE = SW_URL.replace(/[^/]*$/, '') + + function isSupported() { + return (typeof navigator !== 'undefined' + && 'serviceWorker' in navigator + && typeof window !== 'undefined' + && 'PushManager' in window + && 'Notification' in window) + } + + /** Decode a base64url VAPID public key into the Uint8Array applicationServerKey expects. */ + function urlBase64ToUint8Array(base64String) { + var padding = '='.repeat((4 - (base64String.length % 4)) % 4) + var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') + var raw = window.atob(base64) + var output = new Uint8Array(raw.length) + for (var i = 0; i < raw.length; ++i) { + output[i] = raw.charCodeAt(i) + } + return output + } + + function apiUrl(path) { + return OC.generateUrl('/apps/openregister/webpush' + path) + } + + function fetchVapidKey() { + return fetch(apiUrl('/vapid-public-key'), { + headers: { requesttoken: OC.requestToken }, + }).then(function(r) { return r.json() }) + } + + function postSubscription(subscription) { + var json = subscription.toJSON() + return fetch(apiUrl('/subscription'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + requesttoken: OC.requestToken, + }, + body: JSON.stringify({ endpoint: json.endpoint, keys: json.keys }), + }) + } + + function deleteSubscription(endpoint) { + return fetch(apiUrl('/subscription'), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + requesttoken: OC.requestToken, + }, + body: JSON.stringify({ endpoint: endpoint }), + }) + } + + /** + * Enable browser push for the current user. MUST be called from a user + * gesture (button/toggle) — it requests notification permission and + * registers the Service Worker only then. + * + * @return {Promise} true when subscribed. + */ + function enablePush() { + if (!isSupported()) { + return Promise.reject(new Error('Web Push is not supported in this browser')) + } + return Notification.requestPermission().then(function(permission) { + if (permission !== 'granted') { + throw new Error('Notification permission not granted') + } + return Promise.all([ + navigator.serviceWorker.register(SW_URL, { scope: SW_SCOPE }), + fetchVapidKey(), + ]) + }).then(function(results) { + var registration = results[0] + var vapid = results[1] + if (!vapid || !vapid.configured || !vapid.publicKey) { + throw new Error('Server VAPID key is not configured (run occ openregister:web-push:generate-vapid)') + } + return registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(vapid.publicKey), + }) + }).then(function(subscription) { + return postSubscription(subscription).then(function() { + suppressStockOpenRegisterPopups() + return true + }) + }) + } + + /** + * Disable browser push: unsubscribe in the browser and delete the + * server-side subscription. + * + * @return {Promise} true when unsubscribed. + */ + function disablePush() { + if (!isSupported()) { + return Promise.resolve(false) + } + return navigator.serviceWorker.getRegistration(SW_SCOPE).then(function(registration) { + if (!registration) { + return false + } + return registration.pushManager.getSubscription().then(function(subscription) { + if (!subscription) { + return false + } + var endpoint = subscription.endpoint + return subscription.unsubscribe().then(function() { + return deleteSubscription(endpoint).then(function() { return true }) + }) + }) + }) + } + + var stockPopupsSuppressed = false + + /** + * Suppress the stock notifications-app foreground popup for OpenRegister + * notifications once web-push is active. The stock app calls + * `new Notification()` for every nc-notification while a tab is open; for our + * notifications the Service Worker already shows a richer version (with the + * "Open client" action), so the stock one is a duplicate. We identify ours by + * the hex notification icon URL and drop only those — every other app's + * notifications and the Notification permission API are left untouched. + * + * @return {void} + */ + function suppressStockOpenRegisterPopups() { + if (stockPopupsSuppressed || typeof window.Notification !== 'function') { + return + } + var Native = window.Notification + var Patched = function(title, options) { + options = options || {} + if (String(options.icon || '').indexOf('/openregister/webpush/icon') !== -1) { + // Our Service Worker shows this one — swallow the stock duplicate. + return { close: function() {}, addEventListener: function() {}, removeEventListener: function() {} } + } + return new Native(title, options) + } + // Preserve the static Notification API the rest of Nextcloud relies on. + Object.defineProperty(Patched, 'permission', { get: function() { return Native.permission } }) + Patched.requestPermission = function() { return Native.requestPermission.apply(Native, arguments) } + Patched.maxActions = Native.maxActions + try { Patched.prototype = Native.prototype } catch (e) { /* non-fatal */ } + window.Notification = Patched + stockPopupsSuppressed = true + } + + window.OCA = window.OCA || {} + window.OCA.OpenRegister = window.OCA.OpenRegister || {} + window.OCA.OpenRegister.WebPush = { + isSupported: isSupported, + enablePush: enablePush, + disablePush: disablePush, + permission: function() { + return ('Notification' in window) ? Notification.permission : 'unsupported' + }, + } + + // On every page load, if a push subscription is already active, suppress the + // stock foreground duplicate so the user only sees the rich SW notification. + if (isSupported()) { + navigator.serviceWorker.getRegistration(SW_SCOPE) + .then(function(reg) { return reg ? reg.pushManager.getSubscription() : null }) + .then(function(sub) { if (sub) { suppressStockOpenRegisterPopups() } }) + .catch(function() {}) + } +}()) diff --git a/js/openregister-push-sw.js b/js/openregister-push-sw.js new file mode 100644 index 0000000000..50deb616e1 --- /dev/null +++ b/js/openregister-push-sw.js @@ -0,0 +1,85 @@ +/** + * OpenRegister Web Push Service Worker. + * + * Renders rich background OS notifications pushed by the OpenRegister web-push + * engine (openregister-web-push-engine) and routes clicks (top-level + per + * action button) to the resolved deeplink. This file is a plain static asset + * served at /apps/openregister/js/openregister-push-sw.js and registered by + * js/openregister-push-client.js; it is NOT bundled through webpack. + * + * Payload shape (JSON, produced by WebPushService): + * { title, body, icon, badge, tag, actions: [{action, title}], data: { url, actions: { : } } } + * + * @license EUPL-1.2 + * @copyright 2026 Conduction B.V. + */ + +'use strict' + +self.addEventListener('install', () => { + // Activate immediately so the first subscription starts receiving pushes. + self.skipWaiting() +}) + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('push', (event) => { + let payload = {} + try { + payload = event.data ? event.data.json() : {} + } catch (e) { + // Non-JSON payloads degrade to a plain text body. + payload = { title: 'OpenRegister', body: event.data ? event.data.text() : '' } + } + + const title = payload.title || 'OpenRegister' + const options = { + body: payload.body || '', + icon: payload.icon || undefined, + badge: payload.badge || undefined, + // At most two actions render on desktop (Web Notification API limit). + actions: Array.isArray(payload.actions) ? payload.actions.slice(0, 2) : [], + // A stable tag collapses duplicates of the same logical notification + // (and lets the foreground client suppress the stock popup). + tag: payload.tag || undefined, + renotify: !!payload.tag, + data: payload.data || {}, + } + + event.waitUntil(self.registration.showNotification(title, options)) +}) + +self.addEventListener('notificationclick', (event) => { + event.notification.close() + + const data = event.notification.data || {} + // A clicked action button resolves to its own deeplink; a click on the + // notification body falls back to the top-level url. + let targetUrl = data.url || '/' + if (event.action && data.actions && data.actions[event.action]) { + targetUrl = data.actions[event.action] + } + + event.waitUntil( + self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { + // Focus an already-open tab on the same origin instead of opening a new one. + for (const client of clientList) { + try { + const sameOrigin = new URL(client.url).origin === new URL(targetUrl, self.location.origin).origin + if (sameOrigin && 'focus' in client) { + client.navigate(targetUrl) + return client.focus() + } + } catch (e) { + // Ignore malformed client URLs and fall through to openWindow. + } + } + if (self.clients.openWindow) { + return self.clients.openWindow(targetUrl) + } + return undefined + }) + ) +}) diff --git a/l10n/be.js b/l10n/be.js new file mode 100644 index 0000000000..0562dd2c94 --- /dev/null +++ b/l10n/be.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(без тэмы)", + "(no title)" : "(без назвы)", + "(unnamed)" : "(без імя)", + "(untitled card)" : "(картка без назвы)", + "Activities" : "Дзейнасць", + "Add Endpoint" : "Дадаць канчатковую кропку", + "Add Source" : "Дадаць крыніцу", + "Add card" : "Дадаць картку", + "Add contact" : "Дадаць кантакт", + "All clear" : "Усё ў парадку", + "Articles" : "Артыкулы", + "Code" : "Код", + "Compliance" : "Адпаведнасць", + "Contacts" : "Кантакты", + "Create Agent" : "Стварыць агента", + "Create Application" : "Стварыць праграму", + "Create event" : "Стварыць падзею", + "Erasure complete" : "Сціранне завершана", + "Erasure preview" : "Папярэдні прагляд сцірання", + "Expand details" : "Разгарнуць падрабязнасці", + "Save changes" : "Захаваць змены", + "📄 Object Serialization" : "📄 Серыялізацыя аб'екта", + "🔢 Vectorization Settings" : "🔢 Налады вектарызацыі", + "💰 View Selection (Cost Optimization)" : "💰 Выбар прагляду (аптымізацыя выдаткаў)", + "AI Assistant" : "ШІ-памочнік", + "AI Chat" : "ШІ-чат", + "API Key" : "Ключ API", + "API tokens saved successfully" : "Токены API паспяхова захаваны", + "About ConfigSets" : "Пра ConfigSet", + "Action" : "Дзеянне", + "Action Distribution" : "Размеркаванне дзеянняў", + "Action:" : "Дзеянне:", + "Actions" : "Дзеянні", + "Active" : "Актыўны", + "Active Collections" : "Актыўныя калекцыі", + "Active Filters" : "Актыўныя фільтры", + "Active Organisations" : "Актыўныя арганізацыі", + "Active filters:" : "Актыўныя фільтры:", + "Active organisation set successfully" : "Актыўная арганізацыя паспяхова ўсталявана", + "Activity Period" : "Перыяд дзейнасці", + "Add Groups" : "Дадаць групы", + "Add Schema" : "Дадаць схему", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Дадайце назвы схем, апісанні і звесткі рэестра, каб забяспечыць больш багаты кантэкст для пошуку", + "Add to favorites" : "Дадаць у абраныя", + "Added" : "Дададзена", + "Added to favorites" : "Дададзена ў абраныя", + "Additional Information" : "Дадатковая інфармацыя", + "Additional feedback saved. Thank you!" : "Дадатковы водгук захаваны. Дзякуй!", + "Admin" : "Адміністратар", + "Advanced" : "Дадаткова", + "Advanced Filters" : "Дадатковыя фільтры", + "Advanced Options" : "Дадатковыя параметры", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Дадатковыя фільтры недаступныя пры выкарыстанні крыніцы базы дадзеных. Пераключыцеся на Auto або SOLR Index для параметраў фільтрацыі.", + "After" : "Пасля", + "Agent deleted successfully" : "Агент паспяхова выдалены", + "Agents" : "Агенты", + "All" : "Усе", + "All Categories" : "Усе катэгорыі", + "All Collections" : "Усе калекцыі", + "All Confidence Levels" : "Усе ўзроўні ўпэўненасці", + "All ConfigSets" : "Усе ConfigSet", + "All Files" : "Усе файлы", + "All Levels" : "Усе ўзроўні", + "All Types" : "Усе тыпы", + "All Webhooks" : "Усе вэбхукі", + "All actions" : "Усе дзеянні", + "All audit trails cleared successfully" : "Усе журналы аўдыту паспяхова ачышчаны", + "All registers" : "Усе рэестры", + "All schemas" : "Усе схемы", + "All searches" : "Усе пошукі", + "All users" : "Усе карыстальнікі", + "An error occurred while clearing audit trails" : "Адбылася памылка пры ачыстцы журналаў аўдыту", + "An error occurred while deleting the audit trail" : "Адбылася памылка пры выдаленні журнала аўдыту", + "An error occurred while deleting the view" : "Адбылася памылка пры выдаленні прагляду", + "An error occurred while permanently deleting the objects" : "Адбылася памылка пры канчатковым выдаленні аб'ектаў", + "Analysis completed:" : "Аналіз завершаны:", + "Analysis steps:" : "Крокі аналізу:", + "Analytics" : "Аналітыка", + "Analyze Objects" : "Аналізаваць аб'екты", + "Analyze Properties" : "Аналізаваць уласцівасці", + "Analyze existing properties for improvement opportunities" : "Аналізуйце існуючыя ўласцівасці на прадмет магчымасцей паляпшэння", + "Analyzing..." : "Аналіз...", + "Any favorites and sharing settings for this view" : "Любыя абраныя і налады супольнага доступу для гэтага прагляду", + "Any user" : "Любы карыстальнік", + "App store cache invalidated successfully" : "Кэш крамы праграм паспяхова інвалідаваны", + "Application deleted successfully" : "Праграма паспяхова выдалена", + "Applications" : "Праграмы", + "Apply Changes" : "Прымяніць змены", + "Archive" : "Архіў", + "Archive conversation" : "Архіваваць размову", + "Archived conversations are hidden from your active list" : "Архіваваныя размовы схаваны з вашага актыўнага спісу", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Вы ўпэўнены, што хочаце ачысціць старыя журналы пошуку? Гэта выдаліць запісы старэйшыя за 30 дзён.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Вы ўпэўнены, што хочаце выдаліць абраныя журналы аўдыту? Гэта дзеянне нельга адмяніць.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Вы ўпэўнены, што хочаце выдаліць абраныя журналы пошуку? Гэта дзеянне нельга адмяніць.", + "Are you sure you want to delete this ConfigSet?" : "Вы ўпэўнены, што хочаце выдаліць гэты ConfigSet?", + "Are you sure you want to permanently delete" : "Вы ўпэўнены, што хочаце канчаткова выдаліць", + "Ask a question..." : "Задайце пытанне...", + "Ask questions about your data using natural language" : "Задавайце пытанні пра вашы дадзеныя з дапамогай натуральнай мовы", + "Attempt" : "Спроба", + "Audit Trail Changes" : "Змены журнала аўдыту", + "Audit Trail Details" : "Падрабязнасці журнала аўдыту", + "Audit Trail Management" : "Кіраванне журналам аўдыту", + "Audit Trail Statistics" : "Статыстыка журнала аўдыту", + "Audit Trails" : "Журналы аўдыту", + "Audit trail data copied to clipboard" : "Дадзеныя журнала аўдыту скапіяваны ў буфер абмену", + "Audit trail deleted successfully" : "Журнал аўдыту паспяхова выдалены", + "Audit trail successfully deleted" : "Журнал аўдыту паспяхова выдалены", + "Authentication" : "Аўтэнтыфікацыя", + "Auto-commit disabled" : "Аўтаматычная фіксацыя адключана", + "Auto-commit enabled" : "Аўтаматычная фіксацыя ўключана", + "Auto-create Default Organisation" : "Аўтаматычна ствараць арганізацыю па змаўчанні", + "Auto-retry failed vectorizations" : "Аўтаматычна паўтараць няўдалыя вектарызацыі", + "Automatically commit changes to SOLR index" : "Аўтаматычна фіксаваць змены ў індэксе SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Аўтаматычна ствараць арганізацыю па змаўчанні, калі ніводнай не існуе пры ініцыялізацыі праграмы", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Аўтаматычна генераваць вектарныя ўбудаванні з тэкставых фрагментаў пры запампоўцы і апрацоўцы файлаў", + "Automatically generate vector embeddings when objects are created or updated" : "Аўтаматычна генераваць вектарныя ўбудаванні пры стварэнні або абнаўленні аб'ектаў", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Аўтаматычна паўтараць няўдалыя спробы вектарызацыі (максімум 3 паўторы)", + "Available Filters" : "Даступныя фільтры", + "Available Workflows" : "Даступныя працэсы", + "Avg Execution Time" : "Сярэдні час выканання", + "Avg Members/Org" : "Сярэдняя колькасць удзельнікаў/арг.", + "Avg Object Views/Session" : "Сярэдняя колькасць праглядаў аб'ектаў/сеанс", + "Avg Response Time" : "Сярэдні час адказу", + "Avg Results/Search" : "Сярэдняя колькасць вынікаў/пошук", + "Avg Searches/Session" : "Сярэдняя колькасць пошукаў/сеанс", + "Back" : "Назад", + "Back to Entities" : "Назад да сутнасцей", + "Back to Registers" : "Назад да рэестраў", + "Back to Webhooks" : "Назад да вэбхукаў", + "Back to applications" : "Назад да праграм", + "Back to entities" : "Назад да сутнасцей", + "Backend updated successfully. Please reload the application." : "Бэкэнд паспяхова абноўлены. Калі ласка, перазагрузіце праграму.", + "Base URL (Optional)" : "Базавы URL (неабавязкова)", + "Basic Information" : "Асноўная інфармацыя", + "Batch Size" : "Памер пакета", + "Batch extraction completed" : "Пакетнае выманне завершана", + "Before" : "Да", + "Before object vectorization can work:" : "Перш чым вектарызацыя аб'ектаў зможа працаваць:", + "Behavior Issue" : "Праблема паводзін", + "Bookmarks" : "Закладкі", + "Blob storage has been retired. All objects now use magic tables." : "Сховішча blob выведзена з эксплуатацыі. Усе аб'екты цяпер выкарыстоўваюць магічныя табліцы.", + "Branch" : "Галіна", + "Bulk delete operation completed successfully" : "Аперацыя масавага выдалення паспяхова завершана", + "Bulk save operation completed successfully" : "Аперацыя масавага захавання паспяхова завершана", + "Business Data" : "Дзелавыя дадзеныя", + "Cache cleared successfully" : "Кэш паспяхова ачышчаны", + "Calculate Sizes" : "Разлічыць памеры", + "Cancel" : "Адмяніць", + "Cannot delete: objects are still attached" : "Немагчыма выдаліць: аб'екты ўсё яшчэ далучаны", + "Category" : "Катэгорыя", + "Change Type" : "Тып змены", + "Changes" : "Змены", + "Changes copied to clipboard" : "Змены скапіяваны ў буфер абмену", + "Chat Model" : "Мадэль чату", + "Chat Provider" : "Пастаўшчык чату", + "Chat Provider (RAG)" : "Пастаўшчык чату (RAG)", + "Chat Settings" : "Налады чату", + "Chat provider connection successful!" : "Падключэнне да пастаўшчыка чату паспяховае!", + "Chat settings" : "Налады чату", + "Choose a register" : "Выберыце рэестр", + "Choose a schema" : "Выберыце схему", + "Choose how vector similarity calculations are performed for semantic search" : "Выберыце, як выконваюцца разлікі вектарнага падабенства для семантычнага пошуку", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Выберыце, якія тыпы файлаў уключыць у працэс вектарызацыі. Будуць апрацаваны толькі файлы з вынятым тэкстам і фрагментамі.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Выберыце, якія прагляды ўключыць у працэс вектарызацыі. Пакіньце пустым, каб апрацаваць усе прагляды на падставе вашай канфігурацыі.", + "Chunk" : "Фрагмент", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Выдаленне фрагментаў яшчэ не рэалізавана. Выкарыстоўвайце канчатковыя кропкі на аснове фрагментаў.", + "Chunks" : "Фрагменты", + "Chunks to Vectorize:" : "Фрагменты для вектарызацыі:", + "Cleanup Old Trails" : "Ачысціць старыя журналы", + "Cleanup completed" : "Ачыстка завершана", + "Clear All Embeddings" : "Ачысціць усе ўбудаванні", + "Clear Entries" : "Ачысціць запісы", + "Clear Filtered Audit Trails" : "Ачысціць адфільтраваныя журналы аўдыту", + "Clear Filters" : "Ачысціць фільтры", + "Clear Index" : "Ачысціць індэкс", + "Clear Selection" : "Ачысціць выбар", + "Clear all" : "Ачысціць усё", + "Clear all filters" : "Ачысціць усе фільтры", + "Clear date range" : "Ачысціць дыяпазон дат", + "Clearing..." : "Ачыстка...", + "Close" : "Закрыць", + "Collection Management" : "Кіраванне калекцыямі", + "Collection Name" : "Назва калекцыі", + "Collection assignments updated successfully" : "Прызначэнні калекцый паспяхова абноўлены", + "Collection cleared successfully" : "Калекцыя паспяхова ачышчана", + "Collection copied successfully" : "Калекцыя паспяхова скапіявана", + "Collection created successfully" : "Калекцыя паспяхова створана", + "Collection deleted successfully" : "Калекцыя паспяхова выдалена", + "Collection used to store and index file metadata and content" : "Калекцыя, якая выкарыстоўваецца для захоўвання і індэксацыі метададзеных і зместу файлаў", + "Collection used to store and index object data" : "Калекцыя, якая выкарыстоўваецца для захоўвання і індэксацыі дадзеных аб'ектаў", + "Collections:" : "Калекцыі:", + "Commit Message" : "Паведамленне фіксацыі", + "Commit Within (ms)" : "Фіксаваць у межах (мс)", + "Commit message" : "Паведамленне фіксацыі", + "Compare current schema with real object data" : "Параўнаць бягучую схему з рэальнымі дадзенымі аб'екта", + "Completed" : "Завершана", + "Complex" : "Складаны", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Складаныя запыты: пашыраныя пошукі з некалькімі фільтрамі, аператарамі і складанымі камбінацыямі параметраў", + "Confidence Level" : "Узровень упэўненасці", + "Confidence Score" : "Адзнака ўпэўненасці", + "Config must be provided as an object" : "Канфігурацыя павінна быць пададзена як аб'ект", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Кіраванне ConfigSet", + "ConfigSet Name" : "Назва ConfigSet", + "ConfigSet created successfully" : "ConfigSet паспяхова створаны", + "ConfigSet deleted successfully" : "ConfigSet паспяхова выдалены", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSet вызначаюць схему і канфігурацыю для вашых калекцый SOLR. Яны ўтрымліваюць вызначэнні палёў, аналізатары і іншыя налады пошуку.", + "Configuration" : "Канфігурацыя", + "Configuration Data" : "Дадзеныя канфігурацыі", + "Configuration saved" : "Канфігурацыя захавана", + "Configurations" : "Канфігурацыі", + "Configure Facets" : "Наладзіць фасеты", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Наладзьце пастаўшчыкоў вялікіх моўных мадэляў (LLM) для функцый на аснове ШІ, уключаючы семантычны пошук, убудаванні і чат.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Наладзьце асноўныя налады падключэння для вашага сервера SOLR, уключаючы аўтэнтыфікацыю і сеткавыя параметры. Выкарыстоўвайце асобныя дыялогі кіравання ConfigSet і калекцыямі для кіравання ядрамі і калекцыямі.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Наладзьце, як аб'екты базы дадзеных пераўтвараюцца ў вектарныя ўбудаванні для семантычнага пошуку. Аб'екты вектарызуюцца напрамую без неабходнасці вымання тэксту.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Наладзьце, як аб'екты пераўтвараюцца ў тэкст перад вектарызацыяй. Гэтыя налады ўплываюць на якасць пошуку і кантэкст.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Наладзьце параметры для вектарызацыі файлаў. Гэты працэс згенеруе вектарныя ўбудаванні для ўсіх вынятых фрагментаў файлаў.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Наладзьце параметры для вектарызацыі аб'ектаў. Гэты працэс згенеруе вектарныя ўбудаванні для ўсіх аб'ектаў, якія адпавядаюць вашым фільтрам прагляду.", + "Configure which types of data to search and how many sources to retrieve" : "Наладзьце, якія тыпы дадзеных шукаць і колькі крыніц атрымліваць", + "Connection Failed" : "Падключэнне не ўдалося", + "Connection Settings" : "Налады падключэння", + "Connection Successful!" : "Падключэнне паспяховае!", + "Connection failed" : "Падключэнне не ўдалося", + "Connection protocol" : "Пратакол падключэння", + "Connection successful" : "Падключэнне паспяховае", + "Connection timeout in seconds" : "Тайм-аўт падключэння ў секундах", + "Constant delay between retries (5 minutes)" : "Сталая затрымка паміж паўторамі (5 хвілін)", + "Constraint Issue" : "Праблема абмежавання", + "Content Filters" : "Фільтры зместу", + "Continue to Properties" : "Перайсці да ўласцівасцей", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Кіруйце тым, якія прагляды аб'ектаў павінны быць вектарызаваны для зніжэння выдаткаў API. Вектарызуйце толькі прагляды, якія выйграюць ад семантычнага пошуку.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Кіруйце тым, якія прагляды і інструменты ШІ можа выкарыстоўваць у гэтай размове. Па змаўчанні ўсе магчымасці агента ўключаны.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Кіруе выпадковасцю (0 = дэтэрмінаваны, 2 = вельмі творчы)", + "Conversation ID is required" : "Патрабуецца ідэнтыфікатар размовы", + "Conversation archived" : "Размова архівавана", + "Conversation archived successfully" : "Размова паспяхова архівавана", + "Conversation cleared successfully" : "Размова паспяхова ачышчана", + "Conversation deleted" : "Размова выдалена", + "Conversation permanently deleted" : "Размова канчаткова выдалена", + "Conversation renamed" : "Размова перайменавана", + "Conversation restored" : "Размова адноўлена", + "Conversation title" : "Назва размовы", + "Copied!" : "Скапіявана!", + "Copy" : "Капіяваць", + "Copy Changes" : "Капіяваць змены", + "Copy Collection" : "Капіяваць калекцыю", + "Copy Data" : "Капіяваць дадзеныя", + "Copy Full Data" : "Капіяваць поўныя дадзеныя", + "Copying..." : "Капіяванне...", + "Count" : "Колькасць", + "Counting objects..." : "Падлік аб'ектаў...", + "Costs" : "Выдаткі", + "Create" : "Стварыць", + "Create Collection" : "Стварыць калекцыю", + "Create ConfigSet" : "Стварыць ConfigSet", + "Create New Collection" : "Стварыць новую калекцыю", + "Create New ConfigSet" : "Стварыць новы ConfigSet", + "Create Operations" : "Аперацыі стварэння", + "Create Webhook" : "Стварыць вэбхук", + "Create a copy of collection:" : "Стварыць копію калекцыі:", + "Create a new ConfigSet based on the _default template" : "Стварыць новы ConfigSet на аснове шаблону _default", + "Create a new SOLR collection from an existing ConfigSet" : "Стварыць новую калекцыю SOLR з існуючага ConfigSet", + "Create your first AI agent to get started." : "Стварыце свайго першага ШІ-агента, каб пачаць.", + "Created" : "Створана", + "Created:" : "Створана:", + "Creating..." : "Стварэнне...", + "Current Filters" : "Бягучыя фільтры", + "Current Type:" : "Бягучы тып:", + "Custom API endpoint if using a different region" : "Карыстальніцкая канчатковая кропка API пры выкарыстанні іншага рэгіёна", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Карыстальніцкія загалоўкі HTTP (па адным на радок, фармат: Header-Name: value)", + "Daily" : "Штодзённа", + "Dashboard" : "Панэль кіравання", + "Data Source" : "Крыніца дадзеных", + "Data sources" : "Крыніцы дадзеных", + "Data type does not match observed values" : "Тып дадзеных не адпавядае назіраным значэнням", + "Database URL" : "URL базы дадзеных", + "Database information refreshed" : "Інфармацыя базы дадзеных абноўлена", + "Date Range" : "Дыяпазон дат", + "Default" : "Па змаўчанні", + "Default Organisation" : "Арганізацыя па змаўчанні", + "Default: 5" : "Па змаўчанні: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Затрымкі падвойваюцца з кожнай спробай (2, 4, 8 хвілін...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Затрымкі павялічваюцца лінейна (5, 10, 15 хвілін...)", + "Delete" : "Выдаліць", + "Delete Application" : "Выдаліць праграму", + "Delete Audit Trail" : "Выдаліць журнал аўдыту", + "Delete Collection" : "Выдаліць калекцыю", + "Delete ConfigSet" : "Выдаліць ConfigSet", + "Delete Objects" : "Выдаліць аб'екты", + "Delete Operations" : "Аперацыі выдалення", + "Delete View" : "Выдаліць прагляд", + "Delete all objects in this schema" : "Выдаліць усе аб'екты ў гэтай схеме", + "Delete permanently" : "Выдаліць канчаткова", + "Delete this conversation?" : "Выдаліць гэту размову?", + "Delete view" : "Выдаліць прагляд", + "Deleted" : "Выдалена", + "Deleted By" : "Выдалена кім", + "Deleted Date" : "Дата выдалення", + "Deleted Items Management" : "Кіраванне выдаленымі элементамі", + "Deleted This Week" : "Выдалена на гэтым тыдні", + "Deleted Today" : "Выдалена сёння", + "Deleted:" : "Выдалена:", + "Deleting..." : "Выдаленне...", + "Deletion Date Range" : "Дыяпазон дат выдалення", + "Deletion Statistics" : "Статыстыка выдалення", + "Deprecated" : "Састарэлы", + "Description" : "Апісанне", + "Description:" : "Апісанне:", + "Detect data types and patterns" : "Выяўляць тыпы дадзеных і шаблоны", + "Detected At" : "Выяўлена ў", + "Detected Format:" : "Выяўлены фармат:", + "Detected Issues:" : "Выяўленыя праблемы:", + "Disable" : "Адключыць", + "Disabled" : "Адключана", + "Discover Files" : "Выявіць файлы", + "Discovered Properties" : "Выяўленыя ўласцівасці", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Вы хочаце канчаткова выдаліць усе адфільтраваныя запісы журнала аўдыту? Гэта дзеянне нельга адмяніць.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Вы хочаце канчаткова выдаліць гэты запіс журнала аўдыту? Гэта дзеянне нельга адмяніць.", + "Documents" : "Дакументы", + "Download API Spec" : "Спампаваць спецыфікацыю API", + "Edit" : "Рэдагаваць", + "Edit Register" : "Рэдагаваць рэестр", + "Edit Schema" : "Рэдагаваць схему", + "Edit View" : "Рэдагаваць прагляд", + "Edit Webhook" : "Рэдагаваць вэбхук", + "Edit view" : "Рэдагаваць прагляд", + "Edit view details" : "Рэдагаваць падрабязнасці прагляду", + "Effectiveness" : "Эфектыўнасць", + "Email" : "Электронная пошта", + "Emails" : "Электронныя лісты", + "Embedding Model" : "Мадэль убудавання", + "Embedding Provider" : "Пастаўшчык убудавання", + "Embedding generated successfully" : "Убудаванне паспяхова згенеравана", + "Embedding provider connection successful!" : "Падключэнне да пастаўшчыка ўбудавання паспяховае!", + "Enable" : "Уключыць", + "Enable auto-creation" : "Уключыць аўтаматычнае стварэнне", + "Enable automatic file vectorization" : "Уключыць аўтаматычную вектарызацыю файлаў", + "Enable automatic object vectorization" : "Уключыць аўтаматычную вектарызацыю аб'ектаў", + "Enable automatic synchronization" : "Уключыць аўтаматычную сінхранізацыю", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Уключыць падрабязнае журналяванне для аперацый SOLR (рэкамендуецца для адладкі)", + "Enable faceting" : "Уключыць фасетаванне", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Уключыце або адключыце функцыі LLM. Наладзьце пастаўшчыкоў і мадэлі з дапамогай кнопкі канфігурацыі LLM вышэй.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Уключыце або адключыце інтэграцыю пошуку SOLR. Наладзьце налады падключэння з дапамогай кнопкі налад падключэння вышэй.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Уключыце або адключыце інтэграцыю працэсаў n8n. Наладзьце налады падключэння ніжэй.", + "Enable or disable this webhook" : "Уключыце або адключыце гэты вэбхук", + "Enable vectorization for all existing and future views (may increase costs)" : "Уключыць вектарызацыю для ўсіх існуючых і будучых праглядаў (можа павялічыць выдаткі)", + "Enabled" : "Уключана", + "Endpoints" : "Канчатковыя кропкі", + "Enter ConfigSet name" : "Увядзіце назву ConfigSet", + "Enter collection name" : "Увядзіце назву калекцыі", + "Enter description (optional)..." : "Увядзіце апісанне (неабавязкова)...", + "Enter new collection name" : "Увядзіце новую назву калекцыі", + "Enter object ID" : "Увядзіце ідэнтыфікатар аб'екта", + "Enter search term" : "Увядзіце пошукавы тэрмін", + "Enter view name..." : "Увядзіце назву прагляду...", + "Enter webhook name" : "Увядзіце назву вэбхука", + "Entities" : "Сутнасці", + "Entity Information" : "Інфармацыя пра сутнасць", + "Entity deleted successfully" : "Сутнасць паспяхова выдалена", + "Entity not found" : "Сутнасць не знойдзена", + "Entries to be deleted:" : "Запісы для выдалення:", + "Enum Issue" : "Праблема пераліку", + "Enum constraint is missing" : "Адсутнічае абмежаванне пераліку", + "Error" : "Памылка", + "Error Details" : "Падрабязнасці памылкі", + "Error Information" : "Інфармацыя пра памылку", + "Error Message" : "Паведамленне пра памылку", + "Error loading application" : "Памылка загрузкі праграмы", + "Error loading audit trails" : "Памылка загрузкі журналаў аўдыту", + "Error loading entity" : "Памылка загрузкі сутнасці", + "Error loading search trails" : "Памылка загрузкі журналаў пошуку", + "Estimated Batches:" : "Прыблізная колькасць пакетаў:", + "Estimated Cost:" : "Прыблізныя выдаткі:", + "Estimated Duration:" : "Прыблізная працягласць:", + "Event" : "Падзея", + "Event Property for Payload" : "Уласцівасць падзеі для карыснай нагрузкі", + "Events" : "Падзеі", + "Example Value" : "Прыклад значэння", + "Exclusive Maximum" : "Выключны максімум", + "Exclusive Minimum" : "Выключны мінімум", + "Execution Mode" : "Рэжым выканання", + "Execution Time" : "Час выканання", + "Execution Time Range" : "Дыяпазон часу выканання", + "Existing Improvements" : "Існуючыя паляпшэнні", + "Exponential" : "Экспаненцыяльны", + "Export" : "Экспарт", + "Export completed successfully" : "Экспарт паспяхова завершаны", + "Export, view, or delete audit trails" : "Экспартуйце, праглядайце або выдаляйце журналы аўдыту", + "Extend Schema" : "Пашырыць схему", + "Extended" : "Пашыраны", + "Extensions" : "Пашырэнні", + "Extract Pending Files" : "Выняць файлы ў чаканні", + "Extract properties from each object" : "Вымаць уласцівасці з кожнага аб'екта", + "Extracted At" : "Вынята ў", + "Extraction Status" : "Статус вымання", + "Facet configuration updated successfully" : "Канфігурацыя фасетаў паспяхова абноўлена", + "Facets discovered and configured successfully" : "Фасеты паспяхова выяўлены і наладжаны", + "Facets discovered successfully" : "Фасеты паспяхова выяўлены", + "Failed" : "Не ўдалося", + "Failed to analyze schema properties" : "Не ўдалося прааналізаваць уласцівасці схемы", + "Failed to archive conversation" : "Не ўдалося архіваваць размову", + "Failed to calculate sizes" : "Не ўдалося разлічыць памеры", + "Failed to clear collection" : "Не ўдалося ачысціць калекцыю", + "Failed to copy changes" : "Не ўдалося скапіяваць змены", + "Failed to copy data" : "Не ўдалося скапіяваць дадзеныя", + "Failed to copy data to clipboard" : "Не ўдалося скапіяваць дадзеныя ў буфер абмену", + "Failed to create conversation" : "Не ўдалося стварыць размову", + "Failed to create or find project" : "Не ўдалося стварыць або знайсці праект", + "Failed to delete collection" : "Не ўдалося выдаліць калекцыю", + "Failed to delete conversation" : "Не ўдалося выдаліць размову", + "Failed to delete webhook" : "Не ўдалося выдаліць вэбхук", + "Failed to download API specification" : "Не ўдалося спампаваць спецыфікацыю API", + "Failed to get SOLR field configuration" : "Не ўдалося атрымаць канфігурацыю палёў SOLR", + "Failed to get entity categories" : "Не ўдалося атрымаць катэгорыі сутнасцей", + "Failed to get entity statistics" : "Не ўдалося атрымаць статыстыку сутнасцей", + "Failed to get entity types" : "Не ўдалося атрымаць тыпы сутнасцей", + "Failed to load LLM configuration" : "Не ўдалося загрузіць канфігурацыю LLM", + "Failed to load Nextcloud groups" : "Не ўдалося загрузіць групы Nextcloud", + "Failed to load conversation" : "Не ўдалося загрузіць размову", + "Failed to load entities" : "Не ўдалося загрузіць сутнасці", + "Failed to load entity" : "Не ўдалося загрузіць сутнасць", + "Failed to load files" : "Не ўдалося загрузіць файлы", + "Failed to load organisations" : "Не ўдалося загрузіць арганізацыі", + "Failed to load templates" : "Не ўдалося загрузіць шаблоны", + "Failed to load webhooks" : "Не ўдалося загрузіць вэбхукі", + "Failed to load workflows" : "Не ўдалося загрузіць працэсы", + "Failed to refresh database information" : "Не ўдалося абнавіць інфармацыю базы дадзеных", + "Failed to reindex collection" : "Не ўдалося пераіндэксаваць калекцыю", + "Failed to rename conversation" : "Не ўдалося перайменаваць размову", + "Failed to restore conversation" : "Не ўдалося аднавіць размову", + "Failed to retry extraction" : "Не ўдалося паўтарыць выманне", + "Failed to retry webhook" : "Не ўдалося паўтарыць вэбхук", + "Failed to save GitHub token" : "Не ўдалося захаваць токен GitHub", + "Failed to save GitLab URL" : "Не ўдалося захаваць URL GitLab", + "Failed to save GitLab token" : "Не ўдалося захаваць токен GitLab", + "Failed to save additional feedback" : "Не ўдалося захаваць дадатковы водгук", + "Failed to save n8n configuration" : "Не ўдалося захаваць канфігурацыю n8n", + "Failed to save roles" : "Не ўдалося захаваць ролі", + "Failed to save settings" : "Не ўдалося захаваць налады", + "Failed to save webhook" : "Не ўдалося захаваць вэбхук", + "Failed to send feedback" : "Не ўдалося адправіць водгук", + "Failed to test webhook" : "Не ўдалося праверыць вэбхук", + "Failed to update favorite status" : "Не ўдалося абнавіць статус абранага", + "Failed to update schema properties" : "Не ўдалося абнавіць уласцівасці схемы", + "Failed to update view" : "Не ўдалося абнавіць прагляд", + "Failed to update webhook" : "Не ўдалося абнавіць вэбхук", + "Feedback recorded" : "Водгук запісаны", + "Fewer sources (1-3):" : "Менш крыніц (1-3):", + "Field" : "Поле", + "File" : "Файл", + "File Chunk Prediction" : "Прагноз фрагментаў файла", + "File Collection" : "Калекцыя файлаў", + "File Management" : "Кіраванне файламі", + "File Name" : "Назва файла", + "File Path" : "Шлях да файла", + "File Type Selection" : "Выбар тыпу файла", + "File Vectorization" : "Вектарызацыя файлаў", + "File Warmup" : "Разагрэў файлаў", + "File actions menu" : "Меню дзеянняў з файлам", + "File anonymized successfully" : "Файл паспяхова ананімізаваны", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Вектарызацыя фрагментаў файлаў яшчэ не рэалізавана. Фрагменты гатовы і захаваны, але служба вектарызацыі знаходзіцца ў распрацоўцы.", + "File collection not configured" : "Калекцыя файлаў не наладжана", + "File discovery completed" : "Выяўленне файлаў завершана", + "File extraction completed" : "Выманне файлаў завершана", + "File extraction queued" : "Выманне файлаў пастаўлена ў чаргу", + "File indexed successfully" : "Файл паспяхова праіндэксаваны", + "File is already anonymized" : "Файл ужо ананімізаваны", + "File not found" : "Файл не знойдзены", + "File settings updated successfully" : "Налады файла паспяхова абноўлены", + "File sources" : "Крыніцы файлаў", + "File vectorization configuration saved successfully" : "Канфігурацыя вектарызацыі файлаў паспяхова захавана", + "File vectorization started. Check the statistics section for progress." : "Вектарызацыя файлаў пачата. Праверце раздзел статыстыкі для прагрэсу.", + "File warmup completed" : "Разагрэў файлаў завершаны", + "Files" : "Файлы", + "Files with Completed Extraction:" : "Файлы з завершаным выманнем:", + "Files → fileCollection, Objects → objectCollection" : "Файлы → fileCollection, Аб'екты → objectCollection", + "Filter Audit Trails" : "Фільтраваць журналы аўдыту", + "Filter Deleted Items" : "Фільтраваць выдаленыя элементы", + "Filter Objects" : "Фільтраваць аб'екты", + "Filter Properties" : "Фільтраваць уласцівасці", + "Filter Search Trails" : "Фільтраваць журналы пошуку", + "Filter Statistics" : "Статыстыка фільтрацыі", + "Filter and analyze search trail entries" : "Фільтруйце і аналізуйце запісы журнала пошуку", + "Filter and manage audit trail entries" : "Фільтруйце і кіруйце запісамі журнала аўдыту", + "Filter and manage soft deleted items" : "Фільтруйце і кіруйце мякка выдаленымі элементамі", + "Filter by object ID" : "Фільтраваць па ідэнтыфікатары аб'екта", + "Filter by search term" : "Фільтраваць па пошукавым тэрміне", + "Filter by webhook" : "Фільтраваць па вэбхуку", + "Filter data loaded automatically. Use the filters below to refine your search." : "Дадзеныя фільтра загружаны аўтаматычна. Выкарыстоўвайце фільтры ніжэй, каб удакладніць пошук.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Фільтруйце трыгеры вэбхука па ўласцівасцях карыснай нагрузкі (па адным на радок, фармат: key: value)", + "Filtered" : "Адфільтравана", + "Filters" : "Фільтры", + "Fireworks AI Chat Settings" : "Налады чату Fireworks AI", + "Fireworks AI Embedding Configuration" : "Канфігурацыя ўбудавання Fireworks AI", + "First" : "Першы", + "Fixed" : "Выпраўлена", + "For chat and retrieval-augmented generation" : "Для чату і генерацыі з пашырэннем пошуку", + "For vector embeddings and semantic search" : "Для вектарных убудаванняў і семантычнага пошуку", + "Format" : "Фармат", + "Format Issue" : "Праблема фармату", + "Format constraint is missing" : "Адсутнічае абмежаванне фармату", + "From Date" : "З даты", + "From date" : "З даты", + "Full data copied to clipboard" : "Поўныя дадзеныя скапіяваны ў буфер абмену", + "General Issue" : "Агульная праблема", + "Generate recommendations and confidence scores" : "Генераваць рэкамендацыі і адзнакі ўпэўненасці", + "Generate vectors immediately when new objects are created" : "Генераваць вектары неадкладна пры стварэнні новых аб'ектаў", + "GitHub token is valid" : "Токен GitHub дзейсны", + "GitHub token removed successfully" : "Токен GitHub паспяхова выдалены", + "GitHub token saved successfully" : "Токен GitHub паспяхова захаваны", + "GitLab URL saved successfully" : "URL GitLab паспяхова захаваны", + "GitLab token is valid" : "Токен GitLab дзейсны", + "GitLab token saved successfully" : "Токен GitLab паспяхова захаваны", + "HTTP Method" : "Метад HTTP", + "HTTP method used to send webhook requests" : "Метад HTTP, які выкарыстоўваецца для адпраўкі запытаў вэбхука", + "Headers" : "Загалоўкі", + "Health" : "Стан", + "Heartbeat successful - connection kept alive" : "Сігнал актыўнасці паспяховы — падключэнне захавана", + "Helpful" : "Карысна", + "Hide Filters" : "Схаваць фільтры", + "Hide in forms" : "Схаваць у формах", + "Hide in list view" : "Схаваць у праглядзе спісу", + "High" : "Высокі", + "High Confidence" : "Высокая ўпэўненасць", + "Host" : "Хост", + "Hourly" : "Штогадзіннае", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Наколькі глыбока абыходзіць укладзеныя ўласцівасці аб'екта (1-20). Большыя значэнні захопліваюць больш дэталяў, але павялічваюць памер вектара.", + "How often to check for updates (1-168 hours)" : "Як часта правяраць абнаўленні (1-168 гадзін)", + "How to handle retries for failed webhook deliveries" : "Як апрацоўваць паўторы для няўдалых дастаўкаў вэбхука", + "ID" : "Ідэнтыфікатар", + "ID:" : "Ідэнтыфікатар:", + "IP rate limits cleared successfully" : "Абмежаванні хуткасці IP паспяхова ачышчаны", + "Identify properties not in the schema" : "Вызначыць уласцівасці, якіх няма ў схеме", + "Immutable" : "Нязменны", + "Import" : "Імпарт", + "Import successful" : "Імпарт паспяховы", + "Improved Property" : "Палепшаная ўласцівасць", + "In use" : "Выкарыстоўваецца", + "Inactive" : "Неактыўны", + "Include IDs and names of related objects for better contextual search" : "Уключыце ідэнтыфікатары і назвы звязаных аб'ектаў для лепшага кантэкстнага пошуку", + "Include related object references" : "Уключыць спасылкі на звязаныя аб'екты", + "Include schema and register metadata" : "Уключыць метададзеныя схемы і рэестра", + "Initialization failed" : "Ініцыялізацыя не ўдалася", + "Initialize Project" : "Ініцыялізаваць праект", + "Inspect Fields" : "Праверыць палі", + "Inspect Index" : "Праверыць індэкс", + "Invalid" : "Несапраўдны", + "Invalid batch size. Must be between 1 and 5000" : "Несапраўдны памер пакета. Павінен быць паміж 1 і 5000", + "Invalid field name provided" : "Пададзена несапраўдная назва поля", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Несапраўдны maxObjects. Павінен быць 0 (усе) або станоўчы лік", + "Invalid:" : "Несапраўдны:", + "Issue" : "Праблема", + "Items per page:" : "Элементаў на старонку:", + "Just now" : "Толькі што", + "LLM Configuration" : "Канфігурацыя LLM", + "LLM actions menu" : "Меню дзеянняў LLM", + "LLM configuration saved successfully" : "Канфігурацыя LLM паспяхова захавана", + "LLM features disabled" : "Функцыі LLM адключаны", + "LLM features enabled" : "Функцыі LLM уключаны", + "LLM must be enabled with an embedding provider configured" : "LLM павінен быць уключаны з наладжаным пастаўшчыком убудавання", + "LLM settings updated successfully" : "Налады LLM паспяхова абноўлены", + "Last" : "Апошні", + "Last Triggered" : "Апошні раз спрацавала", + "Length Range:" : "Дыяпазон даўжыні:", + "Linear" : "Лінейны", + "Load Advanced Filters" : "Загрузіць дадатковыя фільтры", + "Load advanced filters with live data from your search index" : "Загрузіце дадатковыя фільтры з жывымі дадзенымі з вашага пошукавага індэкса", + "Load view" : "Загрузіць прагляд", + "Loading ConfigSets..." : "Загрузка ConfigSet...", + "Loading advanced filters..." : "Загрузка дадатковых фільтраў...", + "Loading agents..." : "Загрузка агентаў...", + "Loading application details..." : "Загрузка падрабязнасцей праграмы...", + "Loading applications..." : "Загрузка праграм...", + "Loading archived conversations..." : "Загрузка архіваваных размоў...", + "Loading audit trails..." : "Загрузка журналаў аўдыту...", + "Loading collections..." : "Загрузка калекцый...", + "Loading configurations..." : "Загрузка канфігурацый...", + "Loading conversation..." : "Загрузка размовы...", + "Loading conversations..." : "Загрузка размоў...", + "Loading deleted items..." : "Загрузка выдаленых элементаў...", + "Loading events..." : "Загрузка падзей...", + "Loading filter data..." : "Загрузка дадзеных фільтра...", + "Loading groups..." : "Загрузка груп...", + "Loading log details..." : "Загрузка падрабязнасцей журнала...", + "Loading registers..." : "Загрузка рэестраў...", + "Loading schemas..." : "Загрузка схем...", + "Loading search trails..." : "Загрузка журналаў пошуку...", + "Loading sources..." : "Загрузка крыніц...", + "Loading statistics..." : "Загрузка статыстыкі...", + "Loading users..." : "Загрузка карыстальнікаў...", + "Loading views..." : "Загрузка праглядаў...", + "Loading..." : "Загрузка...", + "Local" : "Лакальны", + "Local Version" : "Лакальная версія", + "Locked" : "Заблакіравана", + "Locked:" : "Заблакіравана:", + "Login successful" : "Уваход паспяховы", + "Logs" : "Журналы", + "Low" : "Нізкі", + "Low Confidence" : "Нізкая ўпэўненасць", + "Lucene Version:" : "Версія Lucene:", + "Magic table synchronized successfully" : "Магічная табліца паспяхова сінхранізавана", + "Manage Organisation Roles" : "Кіраваць ролямі арганізацыі", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Кіруйце калекцыямі SOLR (сховішчамі дадзеных) і прызначайце іх для аб'ектаў і файлаў.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Кіруйце ConfigSet SOLR (шаблонамі канфігурацыі) для вашых калекцый.", + "Manage and configure agents for automated tasks" : "Кіруйце і наладжвайце агентаў для аўтаматызаваных задач", + "Manage and monitor file text extraction status" : "Кіруйце і кантралюйце статус вымання тэксту файлаў", + "Manage and restore soft deleted items from your registers" : "Кіруйце і аднаўляйце мякка выдаленыя элементы з вашых рэестраў", + "Manage and view detected entities from files and objects" : "Кіруйце і праглядайце выяўленыя сутнасці з файлаў і аб'ектаў", + "Manage document templates and themes" : "Кіруйце шаблонамі дакументаў і тэмамі", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Кіруйце працэсамі n8n для аўтаматызацыі OpenRegister. Працэсы будуць захоўвацца ў наладжаным праекце.", + "Manage webhooks for event-driven integrations" : "Кіруйце вэбхукамі для інтэграцый, кіраваных падзеямі", + "Manage your applications and modules" : "Кіруйце вашымі праграмамі і модулямі", + "Manage your data registers and their configurations" : "Кіруйце вашымі рэестрамі дадзеных і іх канфігурацыямі", + "Manage your data schemas and their properties" : "Кіруйце вашымі схемамі дадзеных і іх уласцівасцямі", + "Manage your data sources and their configurations" : "Кіруйце вашымі крыніцамі дадзеных і іх канфігурацыямі", + "Manage your saved search configurations" : "Кіруйце вашымі захаванымі канфігурацыямі пошуку", + "Manage your system configurations and settings" : "Кіруйце вашымі сістэмнымі канфігурацыямі і наладамі", + "Managed" : "Кіруемы", + "Max Files (0 = all)" : "Макс. файлаў (0 = усе)", + "Max Length" : "Макс. даўжыня", + "Max Length:" : "Макс. даўжыня:", + "Max Objects (0 = all)" : "Макс. аб'ектаў (0 = усе)", + "Max Retries" : "Макс. паўтораў", + "Max Shards/Node" : "Макс. шардаў/вузел", + "Max execution time (ms)" : "Макс. час выканання (мс)", + "Max ms" : "Макс. мс", + "Max result count" : "Макс. колькасць вынікаў", + "Max results" : "Макс. вынікаў", + "Maximum" : "Максімум", + "Maximum Nesting Depth" : "Максімальная глыбіня ўкладання", + "Maximum length constraint is missing" : "Адсутнічае абмежаванне максімальнай даўжыні", + "Maximum length is too restrictive" : "Максімальная даўжыня занадта абмежавальная", + "Maximum number of files to process. Set to 0 to process all files." : "Максімальная колькасць файлаў для апрацоўкі. Усталюйце 0, каб апрацаваць усе файлы.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Максімальная колькасць аб'ектаў для апрацоўкі. Усталюйце 0, каб апрацаваць усе аб'екты.", + "Maximum number of retry attempts for failed deliveries" : "Максімальная колькасць спроб паўтору для няўдалых дастаўкаў", + "Maximum time to wait before committing changes" : "Максімальны час чакання перад фіксацыяй змен", + "Maximum tokens to generate" : "Максімальная колькасць токенаў для генерацыі", + "Maximum value constraint is missing" : "Адсутнічае абмежаванне максімальнага значэння", + "Maximum value is too restrictive" : "Максімальнае значэнне занадта абмежавальнае", + "Medium" : "Сярэдні", + "Medium Confidence" : "Сярэдняя ўпэўненасць", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Сярэднія запыты: пошукі з некаторай фільтрацыяй або некалькімі параметрамі (напрыклад, дыяпазоны дат, пэўныя рэестры/схемы)", + "Members of selected groups can access this view" : "Удзельнікі абраных груп могуць атрымаць доступ да гэтага прагляду", + "Members:" : "Удзельнікі:", + "Memory prediction calculated successfully" : "Прагноз памяці паспяхова разлічаны", + "Message content is required" : "Патрабуецца змест паведамлення", + "Message does not belong to this conversation" : "Паведамленне не належыць гэтай размове", + "Metadata Filters" : "Фільтры метададзеных", + "Method" : "Метад", + "Min Length" : "Мін. даўжыня", + "Min execution time (ms)" : "Мін. час выканання (мс)", + "Min ms" : "Мін. мс", + "Min result count" : "Мін. колькасць вынікаў", + "Min results" : "Мін. вынікаў", + "Minimum" : "Мінімум", + "Minimum value constraint is missing" : "Адсутнічае абмежаванне мінімальнага значэння", + "Minimum value is too restrictive" : "Мінімальнае значэнне занадта абмежавальнае", + "Missing object fields created successfully" : "Адсутныя палі аб'екта паспяхова створаны", + "Mode:" : "Рэжым:", + "Model" : "Мадэль", + "Model Name" : "Назва мадэлі", + "Modified" : "Зменена", + "Monthly" : "Штомесячна", + "More sources (10-20):" : "Больш крыніц (10-20):", + "Most Active Objects" : "Найбольш актыўныя аб'екты", + "Multiple Of" : "Кратнае", + "Name" : "Назва", + "Names cache warmup completed" : "Разагрэў кэша назваў завершаны", + "New Collection Name" : "Новая назва калекцыі", + "New Conversation" : "Новая размова", + "New Properties" : "Новыя ўласцівасці", + "New Property" : "Новая ўласцівасць", + "New Value" : "Новае значэнне", + "New users without specific organisation membership will be automatically added to this organisation" : "Новыя карыстальнікі без пэўнага сяброўства ў арганізацыі будуць аўтаматычна дададзены ў гэту арганізацыю", + "Next" : "Далей", + "No" : "Не", + "No ConfigSets found" : "ConfigSet не знойдзены", + "No GitHub token configured" : "Токен GitHub не наладжаны", + "No GitHub token provided" : "Токен GitHub не пададзены", + "No GitLab token provided" : "Токен GitLab не пададзены", + "No Organisation" : "Няма арганізацыі", + "No active filters" : "Няма актыўных фільтраў", + "No activity data available" : "Дадзеныя дзейнасці недаступныя", + "No agents available" : "Няма даступных агентаў", + "No agents found" : "Агенты не знойдзены", + "No app store cache exists yet" : "Кэш крамы праграм яшчэ не існуе", + "No applications are available." : "Праграмы недаступныя.", + "No applications found" : "Праграмы не знойдзены", + "No archived conversations" : "Няма архіваваных размоў", + "No audit trail entries found" : "Запісы журнала аўдыту не знойдзены", + "No changes recorded" : "Змены не запісаны", + "No chunks to vectorize" : "Няма фрагментаў для вектарызацыі", + "No collections found" : "Калекцыі не знойдзены", + "No configuration data" : "Няма дадзеных канфігурацыі", + "No configurations are available." : "Канфігурацыі недаступныя.", + "No configurations found" : "Канфігурацыі не знойдзены", + "No configurations found for this application." : "Канфігурацыі для гэтай праграмы не знойдзены.", + "No conversations yet. Create a new one to get started!" : "Размоў яшчэ няма. Стварыце новую, каб пачаць!", + "No data available" : "Дадзеныя недаступныя", + "No data available for chart" : "Дадзеныя для дыяграмы недаступныя", + "No deleted items found" : "Выдаленыя элементы не знойдзены", + "No deletion data available" : "Дадзеныя выдалення недаступныя", + "No description found" : "Апісанне не знойдзена", + "No description provided" : "Апісанне не пададзена", + "No entities detected in this file. Run text extraction first." : "У гэтым файле не выяўлена сутнасцей. Спачатку запусціце выманне тэксту.", + "No entities found" : "Сутнасці не знойдзены", + "No entities have been detected yet" : "Сутнасці яшчэ не выяўлены", + "No events found" : "Падзеі не знойдзены", + "No expired audit trails found to clear" : "Не знойдзена пратэрмінаваных журналаў аўдыту для ачысткі", + "No facetable fields available. Select a register and schema to see available filters." : "Няма даступных палёў для фасетавання. Выберыце рэестр і схему, каб убачыць даступныя фільтры.", + "No files found" : "Файлы не знойдзены", + "No files have been extracted yet" : "Файлы яшчэ не вынятыя", + "No files to process" : "Няма файлаў для апрацоўкі", + "No files to reindex" : "Няма файлаў для пераіндэксацыі", + "No filters are currently active. This will delete ALL audit trail entries!" : "Цяпер няма актыўных фільтраў. Гэта выдаліць УСЕ запісы журнала аўдыту!", + "No log entries found" : "Запісы журнала не знойдзены", + "No logs are available for this configuration." : "Журналы для гэтай канфігурацыі недаступныя.", + "No logs are available for this source." : "Журналы для гэтай крыніцы недаступныя.", + "No logs found" : "Журналы не знойдзены", + "No mismatched fields found - SOLR schema is properly configured" : "Не знойдзена неадпаведных палёў — схема SOLR наладжана правільна", + "No objects found" : "Аб'екты не знойдзены", + "No objects found to vectorize" : "Не знойдзена аб'ектаў для вектарызацыі", + "No objects selected for deletion" : "Не выбрана аб'ектаў для выдалення", + "No objects to delete" : "Няма аб'ектаў для выдалення", + "No objects were permanently deleted" : "Ніводны аб'ект не быў канчаткова выдалены", + "No properties found" : "Уласцівасці не знойдзены", + "No properties match your filters." : "Ніводная ўласцівасць не адпавядае вашым фільтрам.", + "No purge date set" : "Дата ачысткі не ўсталявана", + "No register data available" : "Дадзеныя рэестра недаступныя", + "No registers found" : "Рэестры не знойдзены", + "No registers found for this application." : "Рэестры для гэтай праграмы не знойдзены.", + "No relations found" : "Сувязі не знойдзены", + "No request body available" : "Цела запыту недаступнае", + "No response body available" : "Цела адказу недаступнае", + "No saved views yet. Create one in the Search tab!" : "Захаваных праглядаў яшчэ няма. Стварыце адзін на ўкладцы пошуку!", + "No schema data available" : "Дадзеныя схемы недаступныя", + "No schema selected for exploration" : "Не выбрана схема для даследавання", + "No schemas found" : "Схемы не знойдзены", + "No schemas found for this application." : "Схемы для гэтай праграмы не знойдзены.", + "No search terms data available" : "Дадзеныя пошукавых тэрмінаў недаступныя", + "No search trail entries found" : "Запісы журнала пошуку не знойдзены", + "No sources are available." : "Крыніцы недаступныя.", + "No sources found" : "Крыніцы не знойдзены", + "No templates found" : "Шаблоны не знойдзены", + "No templates have been created yet" : "Шаблоны яшчэ не створаны", + "No valid magic-mapped register+schema combinations found" : "Не знойдзена дзейсных камбінацый рэестр+схема з магічным супастаўленнем", + "No views found. Create views first before configuring vectorization." : "Прагляды не знойдзены. Спачатку стварыце прагляды перад наладжваннем вектарызацыі.", + "No views match your search" : "Ніводны прагляд не адпавядае вашаму пошуку", + "No webhooks found" : "Вэбхукі не знойдзены", + "No webhooks have been configured yet" : "Вэбхукі яшчэ не наладжаны", + "No workflows found in this project. Create workflows in the n8n editor." : "У гэтым праекце не знойдзена працэсаў. Стварыце працэсы ў рэдактары n8n.", + "None" : "Няма", + "Not helpful" : "Не карысна", + "Not in use" : "Не выкарыстоўваецца", + "Number Constraints" : "Лічбавыя абмежаванні", + "Number of chunks to vectorize in one API call (1-100)." : "Колькасць фрагментаў для вектарызацыі за адзін выклік API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Колькасць фрагментаў для вектарызацыі за адзін выклік API. Больш = хутчэй, але больш памяці. Рэкамендуецца: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Колькасць аб'ектаў для апрацоўкі ў кожным пакеце (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Колькасць аб'ектаў для вектарызацыі за адзін выклік API. Больш = хутчэй, але больш памяці. Рэкамендуецца: 10-50.", + "Numeric Range:" : "Лічбавы дыяпазон:", + "Object" : "Аб'ект", + "Object Collection" : "Калекцыя аб'ектаў", + "Object Count Prediction" : "Прагноз колькасці аб'ектаў", + "Object ID" : "Ідэнтыфікатар аб'екта", + "Object Management" : "Кіраванне аб'ектамі", + "Object Vectorization" : "Вектарызацыя аб'ектаў", + "Object Warmup" : "Разагрэў аб'ектаў", + "Object collection not configured" : "Калекцыя аб'ектаў не наладжана", + "Object not found" : "Аб'ект не знойдзены", + "Object permanently deleted" : "Аб'ект канчаткова выдалены", + "Object restored successfully" : "Аб'ект паспяхова адноўлены", + "Object settings updated successfully" : "Налады аб'екта паспяхова абноўлены", + "Object sources" : "Крыніцы аб'ектаў", + "Object unlocked successfully" : "Аб'ект паспяхова разблакіраваны", + "Object vectorization configuration saved successfully" : "Канфігурацыя вектарызацыі аб'ектаў паспяхова захавана", + "Object vectorized successfully" : "Аб'ект паспяхова вектарызаваны", + "Object:" : "Аб'ект:", + "Objects" : "Аб'екты", + "Objects Analyzed" : "Прааналізаваныя аб'екты", + "Objects Distribution" : "Размеркаванне аб'ектаў", + "Objects being analyzed" : "Аб'екты, якія аналізуюцца", + "Objects by Register" : "Аб'екты па рэестры", + "Objects by Schema" : "Аб'екты па схеме", + "Objects deletion completed successfully" : "Выдаленне аб'ектаў паспяхова завершана", + "Objects in schema" : "Аб'екты ў схеме", + "Objects to Process:" : "Аб'екты для апрацоўкі:", + "Objects to be analyzed" : "Аб'екты для аналізу", + "Objects to be deleted" : "Аб'екты для выдалення", + "Objects to be validated" : "Аб'екты для праверкі", + "Objects will be serialized as JSON text before vectorization" : "Аб'екты будуць серыялізаваны як JSON-тэкст перад вектарызацыяй", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Аб'екты будуць мякка выдалены (пазначаны як выдаленыя, але захаваны ў базе дадзеных). Іх можна аднавіць пазней пры неабходнасці.", + "Old Value" : "Старое значэнне", + "Oldest Item (days)" : "Найстарэйшы элемент (дзён)", + "Ollama Chat Settings" : "Налады чату Ollama", + "Ollama Embedding Configuration" : "Канфігурацыя ўбудавання Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Толькі са зменамі", + "Open Register Objects" : "Аб'екты Open Register", + "Open n8n Editor" : "Адкрыць рэдактар n8n", + "OpenAI Chat Settings" : "Налады чату OpenAI", + "OpenAI Embedding Configuration" : "Канфігурацыя ўбудавання OpenAI", + "OpenRegister Settings" : "Налады OpenRegister", + "Optional webhook secret for signature verification" : "Неабавязковы сакрэт вэбхука для праверкі подпісу", + "Order" : "Парадак", + "Organisation" : "Арганізацыя", + "Organisation ID" : "Ідэнтыфікатар арганізацыі", + "Organisation Members" : "Удзельнікі арганізацыі", + "Organisation Statistics" : "Статыстыка арганізацыі", + "Organisation created successfully" : "Арганізацыя паспяхова створана", + "Organisation settings saved successfully" : "Налады арганізацыі паспяхова захаваны", + "Organisations" : "Арганізацыі", + "Organization" : "Арганізацыя", + "Organization ID (Optional)" : "Ідэнтыфікатар арганізацыі (неабавязкова)", + "Orphaned Items" : "Асірацелыя элементы", + "Owner" : "Уладальнік", + "Owner:" : "Уладальнік:", + "Parallel Mode (Faster, more resource intensive)" : "Паралельны рэжым (хутчэй, больш рэсурсаёмісты)", + "Parallel:" : "Паралельна:", + "Password" : "Пароль", + "Password for SOLR authentication (optional)" : "Пароль для аўтэнтыфікацыі SOLR (неабавязкова)", + "Path" : "Шлях", + "Path in repository" : "Шлях у рэпазіторыі", + "Path where the configuration file will be saved in the repository" : "Шлях, дзе файл канфігурацыі будзе захаваны ў рэпазіторыі", + "Path where the register OAS file will be saved in the repository" : "Шлях, дзе файл OAS рэестра будзе захаваны ў рэпазіторыі", + "Pattern (regex)" : "Шаблон (рэгулярны выраз)", + "Pattern Issue" : "Праблема шаблону", + "Pattern constraint is missing" : "Адсутнічае абмежаванне шаблону", + "Patterns:" : "Шаблоны:", + "Pending" : "У чаканні", + "Permanent Delete Mode" : "Рэжым канчатковага выдалення", + "Permanently Delete" : "Выдаліць канчаткова", + "Permanently delete already soft-deleted objects" : "Канчаткова выдаліць ужо мякка выдаленыя аб'екты", + "Permanently delete this conversation?" : "Канчаткова выдаліць гэту размову?", + "Person" : "Асоба", + "Personal Data" : "Асабістыя дадзеныя", + "Phone" : "Тэлефон", + "Photos" : "Фатаграфіі", + "Please create an agent in the" : "Калі ласка, стварыце агента ў", + "Please select an agent to continue" : "Калі ласка, выберыце агента, каб працягнуць", + "Please select which register and schema to use for the new object" : "Калі ласка, выберыце, які рэестр і схему выкарыстоўваць для новага аб'екта", + "Please try again later." : "Калі ласка, паспрабуйце пазней.", + "Please wait while we fetch your agents." : "Калі ласка, пачакайце, пакуль мы атрымліваем вашых агентаў.", + "Please wait while we fetch your applications." : "Калі ласка, пачакайце, пакуль мы атрымліваем вашы праграмы.", + "Please wait while we fetch your configurations." : "Калі ласка, пачакайце, пакуль мы атрымліваем вашы канфігурацыі.", + "Please wait while we fetch your deleted items." : "Калі ласка, пачакайце, пакуль мы атрымліваем вашы выдаленыя элементы.", + "Please wait while we fetch your sources." : "Калі ласка, пачакайце, пакуль мы атрымліваем вашы крыніцы.", + "Polls" : "Апытанні", + "Popular Search Terms" : "Папулярныя пошукавыя тэрміны", + "Port" : "Порт", + "Prerequisites" : "Перадумовы", + "Press Enter to send, Shift+Enter for new line" : "Націсніце Enter, каб адправіць, Shift+Enter для новага радка", + "Previous" : "Папярэдні", + "Private" : "Прыватны", + "Processes chunks in batches with simulated parallelism." : "Апрацоўвае фрагменты пакетамі з імітаваным паралелізмам.", + "Processes file chunks sequentially (safest)." : "Апрацоўвае фрагменты файлаў паслядоўна (найбяспечней).", + "Processes objects in chunks with simulated parallelism." : "Апрацоўвае аб'екты фрагментамі з імітаваным паралелізмам.", + "Processes objects sequentially (safest)." : "Апрацоўвае аб'екты паслядоўна (найбяспечней).", + "Processing" : "Апрацоўка", + "Processing Limits" : "Абмежаванні апрацоўкі", + "Processing..." : "Апрацоўка...", + "Project initialized successfully" : "Праект паспяхова ініцыялізаваны", + "Project not found. Please initialize first." : "Праект не знойдзены. Калі ласка, спачатку ініцыялізуйце.", + "Properties" : "Уласцівасці", + "Property Behaviors" : "Паводзіны ўласцівасцей", + "Property Configuration:" : "Канфігурацыя ўласцівасці:", + "Property Filters" : "Фільтры ўласцівасцей", + "Property Title" : "Назва ўласцівасці", + "Property Type" : "Тып уласцівасці", + "Property can be improved" : "Уласцівасць можна палепшыць", + "Protected" : "Абаронены", + "Provider" : "Пастаўшчык", + "Provider is required for testing" : "Пастаўшчык патрабуецца для тэставання", + "Public" : "Публічны", + "Public View" : "Публічны прагляд", + "Public views can be accessed by anyone in the system" : "Публічныя прагляды даступныя любому ў сістэме", + "Publish" : "Апублікаваць", + "Publish Objects" : "Апублікаваць аб'екты", + "Publish all objects in this schema" : "Апублікаваць усе аб'екты ў гэтай схеме", + "Published" : "Апублікавана", + "Published:" : "Апублікавана:", + "Publishing..." : "Публікацыя...", + "Purge" : "Ачысціць", + "Purge Date" : "Дата ачысткі", + "Query Complexity Distribution" : "Размеркаванне складанасці запытаў", + "Query parameter is required" : "Патрабуецца параметр запыту", + "RAG" : "RAG", + "RAG Configuration" : "Канфігурацыя RAG", + "Rate limits cleared successfully" : "Абмежаванні хуткасці паспяхова ачышчаны", + "Raw Changes Data" : "Сырыя дадзеныя змен", + "Re-vectorize on object update" : "Паўторна вектарызаваць пры абнаўленні аб'екта", + "Read" : "Чытаць", + "Recommendations:" : "Рэкамендацыі:", + "Recommended Type:" : "Рэкамендаваны тып:", + "Recommended: 5 sources" : "Рэкамендуецца: 5 крыніц", + "Refresh" : "Абнавіць", + "Refresh Data" : "Абнавіць дадзеныя", + "Refresh Stats" : "Абнавіць статыстыку", + "Refresh Workflows" : "Абнавіць працэсы", + "Refresh dashboard" : "Абнавіць панэль кіравання", + "Refresh database info" : "Абнавіць інфармацыю базы дадзеных", + "Register" : "Рэестр", + "Register ID" : "Ідэнтыфікатар рэестра", + "Register Statistics" : "Статыстыка рэестра", + "Register Totals" : "Вынікі рэестра", + "Register depublished successfully" : "Рэестр паспяхова зняты з публікацыі", + "Register not found" : "Рэестр не знойдзены", + "Register objects deletion completed successfully" : "Выдаленне аб'ектаў рэестра паспяхова завершана", + "Register published successfully" : "Рэестр паспяхова апублікаваны", + "Register/Schema Usage" : "Выкарыстанне рэестра/схемы", + "Registers" : "Рэестры", + "Reindex" : "Пераіндэксаваць", + "Reindex completed" : "Пераіндэксацыя завершана", + "Reindex completed successfully" : "Пераіндэксацыя паспяхова завершана", + "Reindex failed" : "Пераіндэксацыя не ўдалася", + "Relations" : "Сувязі", + "Remote Version" : "Аддаленая версія", + "Remove" : "Выдаліць", + "Remove filter" : "Выдаліць фільтр", + "Remove from favorites" : "Выдаліць з абраных", + "Remove group" : "Выдаліць групу", + "Removed" : "Выдалена", + "Removed from favorites" : "Выдалена з абраных", + "Rename Conversation" : "Перайменаваць размову", + "Rename conversation" : "Перайменаваць размову", + "Replicas" : "Рэплікі", + "Repository" : "Рэпазіторый", + "Request Body" : "Цела запыту", + "Request Data" : "Дадзеныя запыту", + "Request Details" : "Падрабязнасці запыту", + "Request body must be JSON with a \"query\" field" : "Цела запыту павінна быць JSON з полем \"query\"", + "Request timeout in seconds" : "Тайм-аўт запыту ў секундах", + "Required field" : "Абавязковае поле", + "Required status is inconsistent" : "Абавязковы статус непаслядоўны", + "Rerun Search" : "Перазапусціць пошук", + "Reset Changes" : "Скінуць змены", + "Reset Filters" : "Скінуць фільтры", + "Response Body" : "Цела адказу", + "Response Details" : "Падрабязнасці адказу", + "Response Time:" : "Час адказу:", + "Restore" : "Аднавіць", + "Restore conversation" : "Аднавіць размову", + "Restore or permanently delete items" : "Аднавіце або канчаткова выдаліце элементы", + "Result Count Range" : "Дыяпазон колькасці вынікаў", + "Results" : "Вынікі", + "Retrieve all objects for this schema" : "Атрымаць усе аб'екты для гэтай схемы", + "Retry" : "Паўтарыць", + "Retry Failed Extractions" : "Паўтарыць няўдалыя выманні", + "Retry Policy" : "Палітыка паўтораў", + "Retry completed" : "Паўтор завершаны", + "Retrying..." : "Паўтор...", + "Risk Level" : "Узровень рызыкі", + "Roles updated successfully" : "Ролі паспяхова абноўлены", + "SOLR Connection Settings" : "Налады падключэння SOLR", + "SOLR Version:" : "Версія SOLR:", + "SOLR actions menu" : "Меню дзеянняў SOLR", + "SOLR base path (usually /solr)" : "Базавы шлях SOLR (звычайна /solr)", + "SOLR is disabled" : "SOLR адключаны", + "SOLR is not available or not configured" : "SOLR недаступны або не наладжаны", + "SOLR logging disabled" : "Журналяванне SOLR адключана", + "SOLR logging enabled" : "Журналяванне SOLR уключана", + "SOLR search disabled" : "Пошук SOLR адключаны", + "SOLR search enabled" : "Пошук SOLR уключаны", + "SOLR server hostname or IP address" : "Імя хоста або IP-адрас сервера SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Нумар порта сервера SOLR (неабавязкова, па змаўчанні 8983)", + "SOLR setup completed successfully" : "Наладка SOLR паспяхова завершана", + "SOLR setup failed" : "Наладка SOLR не ўдалася", + "SOLR setup failed - check logs" : "Наладка SOLR не ўдалася — праверце журналы", + "Sample Values:" : "Прыклады значэнняў:", + "Save" : "Захаваць", + "Save Configuration" : "Захаваць канфігурацыю", + "Save Connection Settings" : "Захаваць налады падключэння", + "Save Roles" : "Захаваць ролі", + "Save Settings" : "Захаваць налады", + "Save current search as view" : "Захаваць бягучы пошук як прагляд", + "Saved Views" : "Захаваныя прагляды", + "Saving..." : "Захаванне...", + "Schema" : "Схема", + "Schema ID" : "Ідэнтыфікатар схемы", + "Schema Name" : "Назва схемы", + "Schema Statistics" : "Статыстыка схемы", + "Schema depublished successfully" : "Схема паспяхова знята з публікацыі", + "Schema objects deletion completed successfully" : "Выдаленне аб'ектаў схемы паспяхова завершана", + "Schema published successfully" : "Схема паспяхова апублікавана", + "Schema successfully created" : "Схема паспяхова створана", + "Schema successfully updated" : "Схема паспяхова абноўлена", + "Schemas" : "Схемы", + "Scheme" : "Схема", + "Search / Views" : "Пошук / Прагляды", + "Search Activity" : "Дзейнасць пошуку", + "Search Analytics" : "Аналітыка пошуку", + "Search Entities" : "Пошук сутнасцей", + "Search Files" : "Пошук файлаў", + "Search GitHub" : "Пошук GitHub", + "Search GitLab" : "Пошук GitLab", + "Search Method" : "Метад пошуку", + "Search Term" : "Пошукавы тэрмін", + "Search Trail Details" : "Падрабязнасці журнала пошуку", + "Search Trail Management" : "Кіраванне журналам пошуку", + "Search Trail Statistics" : "Статыстыка журнала пошуку", + "Search Trails" : "Журналы пошуку", + "Search Views" : "Пошук праглядаў", + "Search Webhooks" : "Пошук вэбхукаў", + "Search by file name or path" : "Пошук па назве файла або шляху", + "Search by name or URL" : "Пошук па назве або URL", + "Search by value" : "Пошук па значэнні", + "Search in Files" : "Пошук у файлах", + "Search in Objects" : "Пошук у аб'ектах", + "Search property names..." : "Пошук назваў уласцівасцей...", + "Search trail deletion not implemented yet" : "Выдаленне журнала пошуку яшчэ не рэалізавана", + "Search views..." : "Пошук праглядаў...", + "Secret" : "Сакрэт", + "Secret key for HMAC signature generation (optional)" : "Сакрэтны ключ для генерацыі подпісу HMAC (неабавязкова)", + "Select AI Agent" : "Выберыце ШІ-агента", + "Select All" : "Выбраць усё", + "Select ConfigSet" : "Выберыце ConfigSet", + "Select File Types to Vectorize:" : "Выберыце тыпы файлаў для вектарызацыі:", + "Select HTTP method" : "Выберыце метад HTTP", + "Select Register and Schema" : "Выберыце рэестр і схему", + "Select Views to Vectorize:" : "Выберыце прагляды для вектарызацыі:", + "Select a Nextcloud group to add" : "Выберыце групу Nextcloud для дадання", + "Select a branch" : "Выберыце галіну", + "Select a model or type a custom model name" : "Выберыце мадэль або ўвядзіце карыстальніцкую назву мадэлі", + "Select a repository" : "Выберыце рэпазіторый", + "Select a repository you have write access to" : "Выберыце рэпазіторый, да якога ў вас ёсць доступ на запіс", + "Select an AI Agent" : "Выберыце ШІ-агента", + "Select an AI agent to begin chatting with your data." : "Выберыце ШІ-агента, каб пачаць чат з вашымі дадзенымі.", + "Select backend" : "Выберыце бэкэнд", + "Select chat model" : "Выберыце мадэль чату", + "Select collection for files" : "Выберыце калекцыю для файлаў", + "Select collection for objects" : "Выберыце калекцыю для аб'ектаў", + "Select default organisation" : "Выберыце арганізацыю па змаўчанні", + "Select event to listen to..." : "Выберыце падзею для праслухоўвання...", + "Select model" : "Выберыце мадэль", + "Select options..." : "Выберыце параметры...", + "Select period" : "Выберыце перыяд", + "Select property to send as payload" : "Выберыце ўласцівасць для адпраўкі як карысную нагрузку", + "Select provider" : "Выберыце пастаўшчыка", + "Select registers and schemas to save a view" : "Выберыце рэестры і схемы, каб захаваць прагляд", + "Select retry policy" : "Выберыце палітыку паўтораў", + "Select the branch to publish to" : "Выберыце галіну для публікацыі", + "Select the event this webhook should listen to" : "Выберыце падзею, якую гэты вэбхук павінен праслухоўваць", + "Select views to vectorize:" : "Выберыце прагляды для вектарызацыі:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Выберыце, якія групы Nextcloud даступныя для гэтай арганізацыі. Карыстальнікі ў гэтых групах будуць мець доступ да рэсурсаў арганізацыі.", + "Select which data views the AI can search" : "Выберыце, якія прагляды дадзеных ШІ можа шукаць", + "Select which property from the event should be used as the webhook payload data" : "Выберыце, якая ўласцівасць з падзеі павінна выкарыстоўвацца як дадзеныя карыснай нагрузкі вэбхука", + "Select which tools the AI can use to perform actions" : "Выберыце, якія інструменты ШІ можа выкарыстоўваць для выканання дзеянняў", + "Selected" : "Выбрана", + "Selected Groups" : "Выбраныя групы", + "Selected audit trails deleted successfully" : "Выбраныя журналы аўдыту паспяхова выдалены", + "Selected search trails deleted successfully" : "Выбраныя журналы пошуку паспяхова выдалены", + "Selected users can access this view" : "Выбраныя карыстальнікі могуць атрымаць доступ да гэтага прагляду", + "Send additional feedback" : "Адправіць дадатковы водгук", + "Send as CloudEvent" : "Адправіць як CloudEvent", + "Sensitive PII" : "Канфідэнцыяльныя асабістыя дадзеныя", + "Serial Mode (Safer, slower)" : "Паслядоўны рэжым (бяспечней, павольней)", + "Serial:" : "Паслядоўна:", + "Server Configuration" : "Канфігурацыя сервера", + "Server Information" : "Інфармацыя пра сервер", + "Settings" : "Налады", + "Shards" : "Шарды", + "Share" : "Падзяліцца", + "Share with Groups" : "Падзяліцца з групамі", + "Share with Users" : "Падзяліцца з карыстальнікамі", + "Show Filters" : "Паказаць фільтры", + "Show less" : "Паказаць менш", + "Show only entries with changes" : "Паказаць толькі запісы са зменамі", + "Simple" : "Просты", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Простыя запыты: асноўныя тэкставыя пошукі з мінімальнымі параметрамі (напрыклад, адзін пошукавы тэрмін, без дадатковых фільтраў)", + "Size" : "Памер", + "Slug" : "Слаг", + "Soft Delete Mode" : "Рэжым мяккага выдалення", + "Soft Deleted Items" : "Мякка выдаленыя элементы", + "SolrCloud mode enabled" : "Рэжым SolrCloud уключаны", + "Source" : "Крыніца", + "Sources" : "Крыніцы", + "Sources:" : "Крыніцы:", + "Standalone SOLR mode" : "Аўтаномны рэжым SOLR", + "Start Conversation" : "Пачаць размову", + "Start Vectorization" : "Пачаць вектарызацыю", + "Start a conversation" : "Пачаць размову", + "Starting names cache warmup..." : "Пачатак разагрэву кэша назваў...", + "Starting..." : "Запуск...", + "Statistics" : "Статыстыка", + "Stats calculation not yet implemented" : "Разлік статыстыкі яшчэ не рэалізаваны", + "Status" : "Статус", + "Status Code" : "Код статусу", + "String Constraints" : "Радковыя абмежаванні", + "Success" : "Поспех", + "Success Rate" : "Каэфіцыент поспеху", + "Success Status" : "Статус поспеху", + "Successful" : "Паспяхова", + "Successfully joined organisation" : "Паспяхова далучыліся да арганізацыі", + "Sync Table" : "Сінхранізаваць табліцу", + "Synchronization Settings" : "Налады сінхранізацыі", + "System Default" : "Сістэмнае значэнне па змаўчанні", + "Technical Description" : "Тэхнічнае апісанне", + "Technical description for developers and administrators" : "Тэхнічнае апісанне для распрацоўшчыкаў і адміністратараў", + "Temperature" : "Тэмпература", + "Templates" : "Шаблоны", + "Test" : "Тэст", + "Test Chat" : "Тэставаць чат", + "Test Connection" : "Тэставаць падключэнне", + "Test Embedding" : "Тэставаць убудаванне", + "Test endpoint executed successfully" : "Тэставая канчатковая кропка паспяхова выканана", + "Test webhook delivered successfully" : "Тэставы вэбхук паспяхова дастаўлены", + "Test webhook delivery failed" : "Дастаўка тэставага вэбхука не ўдалася", + "Test webhook sent successfully" : "Тэставы вэбхук паспяхова адпраўлены", + "Testing..." : "Тэставанне...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Тэкставыя фрагменты генеруюцца падчас вымання файлаў і захоўваюцца ў базе дадзеных. Вектарызацыя чытае гэтыя папярэдне фрагментаваныя файлы і пераўтварае іх ва ўбудаванні.", + "Text extracted successfully" : "Тэкст паспяхова выняты", + "Text extraction disabled" : "Выманне тэксту адключана", + "The URL where webhook events will be sent" : "URL, куды будуць адпраўляцца падзеі вэбхука", + "The requested conversation does not exist" : "Запытаная размова не існуе", + "The saved view and all its search configuration" : "Захаваны прагляд і ўся яго канфігурацыя пошуку", + "There are no audit trail entries matching your current filters." : "Няма запісаў журнала аўдыту, якія адпавядаюць вашым бягучым фільтрам.", + "There are no deleted items matching your current filters." : "Няма выдаленых элементаў, якія адпавядаюць вашым бягучым фільтрам.", + "There are no search trail entries matching your current filters." : "Няма запісаў журнала пошуку, якія адпавядаюць вашым бягучым фільтрам.", + "There are no webhook log entries matching your filters." : "Няма запісаў журнала вэбхука, якія адпавядаюць вашым фільтрам.", + "This action cannot be undone." : "Гэта дзеянне нельга адмяніць.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Гэта дзеянне нельга адмяніць. Пераканайцеся, што ніводная калекцыя не выкарыстоўвае гэты ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "У гэтага агента няма наладжвальных праглядаў або інструментаў, але вы ўсё яшчэ можаце наладзіць налады RAG ніжэй.", + "This analysis may take some time" : "Гэты аналіз можа заняць некаторы час", + "This audit trail entry does not contain any change information." : "Гэты запіс журнала аўдыту не ўтрымлівае ніякай інфармацыі пра змены.", + "This configuration has no data defined." : "У гэтай канфігурацыі не вызначана дадзеных.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Гэта канчатковая кропка састарэла. Замест яе выкарыстоўвайце канчатковыя кропкі на аснове фрагментаў.", + "This entity has no relations to objects or files" : "У гэтай сутнасці няма сувязей з аб'ектамі або файламі", + "This is a test webhook from OpenRegister" : "Гэта тэставы вэбхук ад OpenRegister", + "This schema must use Magic Table configuration to sync" : "Гэта схема павінна выкарыстоўваць канфігурацыю Magic Table для сінхранізацыі", + "This source has no associated registers." : "У гэтай крыніцы няма звязаных рэестраў.", + "This will copy the _default ConfigSet with the new name" : "Гэта скапіруе ConfigSet _default з новай назвай", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Гэта канчаткова выдаліць УСЕ ўбудаванні (вектары) з базы дадзеных. Вам трэба будзе паўторна вектарызаваць усе аб'екты і файлы. Гэта дзеянне нельга адмяніць.\\n\\nВы ўпэўнены, што хочаце працягнуць?", + "This will permanently delete:" : "Гэта канчаткова выдаліць:", + "Timeout (seconds)" : "Тайм-аўт (секунды)", + "Timestamp" : "Адзнака часу", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Парада: уключайце толькі прагляды, якія патрабуюць семантычнага пошуку, каб мінімізаваць выдаткі на ўбудаванні. Простыя табліцы пошуку рэдка патрабуюць вектарызацыі.", + "Title" : "Назва", + "To Date" : "Да даты", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Каб наладзіць памер фрагмента або стратэгію, перайдзіце ў Канфігурацыю файлаў → Абмежаванні апрацоўкі.", + "To change the embedding provider or model, go to LLM Configuration." : "Каб змяніць пастаўшчыка ўбудавання або мадэль, перайдзіце ў Канфігурацыю LLM.", + "To date" : "Да даты", + "Toggle search sidebar" : "Пераключыць бакавую панэль пошуку", + "Tools" : "Інструменты", + "Top Deleters" : "Найбольшыя выдаляльнікі", + "Total" : "Усяго", + "Total Audit Trails" : "Усяго журналаў аўдыту", + "Total Chunks Available:" : "Усяго даступных фрагментаў:", + "Total Deleted Items" : "Усяго выдаленых элементаў", + "Total Members" : "Усяго ўдзельнікаў", + "Total Objects" : "Усяго аб'ектаў", + "Total Objects in Database:" : "Усяго аб'ектаў у базе дадзеных:", + "Total Organisations" : "Усяго арганізацый", + "Total Results" : "Усяго вынікаў", + "Total Searches" : "Усяго пошукаў", + "Total Size" : "Агульны памер", + "Total Storage" : "Агульнае сховішча", + "Total:" : "Усяго:", + "Totals" : "Вынікі", + "Type" : "Тып", + "Type Issue" : "Праблема тыпу", + "Type Variations:" : "Варыяцыі тыпу:", + "Type must be \"positive\" or \"negative\"" : "Тып павінен быць \"positive\" або \"negative\"", + "Type to search for groups" : "Увядзіце для пошуку груп", + "Type to search for users" : "Увядзіце для пошуку карыстальнікаў", + "Type to search groups..." : "Увядзіце для пошуку груп...", + "Type to search users..." : "Увядзіце для пошуку карыстальнікаў...", + "URL" : "URL", + "URL where Ollama is running" : "URL, дзе працуе Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Без змен", + "Unique Search Terms" : "Унікальныя пошукавыя тэрміны", + "Unique Terms" : "Унікальныя тэрміны", + "Unknown" : "Невядома", + "Unknown Register" : "Невядомы рэестр", + "Unknown Schema" : "Невядомая схема", + "Untitled View" : "Прагляд без назвы", + "Update" : "Абнавіць", + "Update Operations" : "Аперацыі абнаўлення", + "Update configuration: ..." : "Абнавіць канфігурацыю: ...", + "Update register OAS: ..." : "Абнавіць OAS рэестра: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Абнаўляць вектары пры змене дадзеных аб'екта (рэкамендуецца для дакладнага пошуку)", + "Updated" : "Абноўлена", + "Updated At" : "Абноўлена ў", + "Updated:" : "Абноўлена:", + "Uptime:" : "Час бесперапыннай работы:", + "Usage Count" : "Колькасць выкарыстанняў", + "Use SolrCloud with Zookeeper for distributed search" : "Выкарыстоўвайце SolrCloud з Zookeeper для размеркаванага пошуку", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Выкарыстоўвайце фільтры, каб звузіць запісы журнала аўдыту па рэестры, схеме, тыпе дзеяння, карыстальніку, дыяпазоне дат або ідэнтыфікатары аб'екта.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Выкарыстоўвайце фільтры, каб звузіць выдаленыя элементы па рэестры, схеме, даце выдалення або карыстальніку, які іх выдаліў.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Выкарыстоўвайце фільтры, каб звузіць запісы журнала пошуку па рэестры, схеме, статусе поспеху, карыстальніку, дыяпазоне дат, пошукавых тэрмінах або метрыках прадукцыйнасці.", + "Used By Collections" : "Выкарыстоўваецца калекцыямі", + "User" : "Карыстальнік", + "User Agent Statistics" : "Статыстыка User Agent", + "User Description" : "Апісанне карыстальніка", + "User rate limits cleared successfully" : "Абмежаванні хуткасці карыстальніка паспяхова ачышчаны", + "User removed successfully" : "Карыстальнік паспяхова выдалены", + "User-friendly description shown in forms and help text" : "Зручнае для карыстальніка апісанне, якое паказваецца ў формах і даведачным тэксце", + "Username" : "Імя карыстальніка", + "Username for SOLR authentication (optional)" : "Імя карыстальніка для аўтэнтыфікацыі SOLR (неабавязкова)", + "Users" : "Карыстальнікі", + "Using Pre-Generated Chunks" : "Выкарыстанне папярэдне згенераваных фрагментаў", + "Validate" : "Праверыць", + "Validate Objects" : "Праверыць аб'екты", + "Validation completed successfully" : "Праверка паспяхова завершана", + "Validation failed" : "Праверка не ўдалася", + "Value" : "Значэнне", + "Vector Dimensions" : "Вектарныя вымярэнні", + "Vector Search Backend" : "Бэкэнд вектарнага пошуку", + "Vector field: _embedding_" : "Вектарнае поле: _embedding_", + "Vectorization Triggers" : "Трыгеры вектарызацыі", + "Vectorize All Files" : "Вектарызаваць усе файлы", + "Vectorize All Objects" : "Вектарызаваць усе аб'екты", + "Vectorize all file types" : "Вектарызаваць усе тыпы файлаў", + "Vectorize all views" : "Вектарызаваць усе прагляды", + "Vectorize on object creation" : "Вектарызаваць пры стварэнні аб'екта", + "Vectors will be stored in your existing object and file collections" : "Вектары будуць захоўвацца ў вашых існуючых калекцыях аб'ектаў і файлаў", + "Version" : "Версія", + "Very High" : "Вельмі высокі", + "View API Docs" : "Прагляд дакументацыі API", + "View Changes" : "Прагляд змен", + "View Details" : "Прагляд падрабязнасцей", + "View Error" : "Прагляд памылкі", + "View File" : "Прагляд файла", + "View Full Details" : "Прагляд поўных падрабязнасцей", + "View Logs" : "Прагляд журналаў", + "View Name" : "Назва прагляду", + "View Object" : "Прагляд аб'екта", + "View Parameters" : "Прагляд параметраў", + "View Selection" : "Выбар прагляду", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Праглядайце і аналізуйце журналы пошуку з дадатковымі магчымасцямі фільтрацыі і аналітыкі", + "View and analyze system audit trails with advanced filtering capabilities" : "Праглядайце і аналізуйце сістэмныя журналы аўдыту з дадатковымі магчымасцямі фільтрацыі", + "View deleted successfully" : "Прагляд паспяхова выдалены", + "View entity details and manage relations" : "Праглядайце падрабязнасці сутнасці і кіруйце сувязямі", + "View name" : "Назва прагляду", + "View name is required" : "Патрабуецца назва прагляду", + "View saved successfully!" : "Прагляд паспяхова захаваны!", + "View search analytics and manage search logs" : "Праглядайце аналітыку пошуку і кіруйце журналамі пошуку", + "View successfully deleted" : "Прагляд паспяхова выдалены", + "View successfully updated" : "Прагляд паспяхова абноўлены", + "View updated successfully!" : "Прагляд паспяхова абноўлены!", + "View webhook delivery logs and filter by webhook" : "Праглядайце журналы дастаўкі вэбхукаў і фільтруйце па вэбхуку", + "Views" : "Прагляды", + "Visible to users" : "Бачна карыстальнікам", + "Wait for Response" : "Чакаць адказу", + "Wait for webhook response before continuing (required for request/response flows)" : "Чакаць адказу вэбхука перад працягам (патрабуецца для патокаў запыт/адказ)", + "Warmup Names Cache" : "Разагрэць кэш назваў", + "Warning" : "Папярэджанне", + "Warning:" : "Папярэджанне:", + "Webhook" : "Вэбхук", + "Webhook Log Details" : "Падрабязнасці журнала вэбхука", + "Webhook Logs" : "Журналы вэбхука", + "Webhook created successfully" : "Вэбхук паспяхова створаны", + "Webhook deleted" : "Вэбхук выдалены", + "Webhook retry delivered successfully" : "Паўтор вэбхука паспяхова дастаўлены", + "Webhook retry delivery failed" : "Дастаўка паўтору вэбхука не ўдалася", + "Webhook updated" : "Вэбхук абноўлены", + "Webhook updated successfully" : "Вэбхук паспяхова абноўлены", + "Webhooks" : "Вэбхукі", + "Weekly" : "Штотыднёва", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Абгарнуць карысную нагрузку вэбхука ў фармат CloudEvents для лепшай сумяшчальнасці", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Так", + "You" : "Вы", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Вы можаце ствараць новыя ConfigSet на аснове шаблону _default або запампоўваць карыстальніцкія непасрэдна на ваш сервер SOLR.", + "You do not have access to this conversation" : "У вас няма доступу да гэтай размовы", + "You do not have permission to delete this conversation" : "У вас няма дазволу на выдаленне гэтай размовы", + "You do not have permission to modify this conversation" : "У вас няма дазволу на змяненне гэтай размовы", + "You do not have permission to restore this conversation" : "У вас няма дазволу на аднаўленне гэтай размовы", + "You must be logged in to favorite views" : "Вы павінны быць увайшоўшы ў сістэму, каб дадаваць прагляды ў абраныя", + "You need an AI agent to start a conversation." : "Вам патрэбны ШІ-агент, каб пачаць размову.", + "Your Fireworks AI API key. Get one at" : "Ваш ключ API Fireworks AI. Атрымайце яго на", + "Your OpenAI API key. Get one at" : "Ваш ключ API OpenAI. Атрымайце яго на", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Ваш водгук запісаны. Пры жаданні вы можаце падаць дадатковыя падрабязнасці тут...", + "Zookeeper Hosts" : "Хосты Zookeeper", + "Zookeeper Port" : "Порт Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Налады Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Радок падключэння Zookeeper для SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Нумар порта Zookeeper (неабавязкова, па змаўчанні 2181)", + "chunk" : "фрагмент", + "chunks" : "фрагменты", + "chunks processed" : "фрагментаў апрацавана", + "configuration(s)" : "канфігурацыя(і)", + "e.g., lib/Settings/config.json" : "напр., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "напр., lib/Settings/register.json", + "failed" : "не ўдалося", + "file" : "файл", + "files" : "файлы", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "удзельнікі", + "menu or contact someone with permission to create agents." : "меню або звярніцеся да кагосьці з дазволам на стварэнне агентаў.", + "messages" : "паведамленні", + "n8n URL and API key are required" : "Патрабуюцца URL і ключ API n8n", + "n8n configuration saved successfully" : "Канфігурацыя n8n паспяхова захавана", + "n8n connection not configured" : "Падключэнне n8n не наладжана", + "n8n connection successful" : "Падключэнне n8n паспяховае", + "n8n connection test successful" : "Тэст падключэння n8n паспяховы", + "n8n integration disabled" : "Інтэграцыя n8n адключана", + "n8n integration enabled" : "Інтэграцыя n8n уключана", + "n8n project initialized successfully" : "Праект n8n паспяхова ініцыялізаваны", + "n8n settings saved successfully" : "Налады n8n паспяхова захаваны", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "аб'екты", + "objects processed" : "аб'ектаў апрацавана", + "or pick a range" : "або выберыце дыяпазон", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "забяспечвае добры баланс паміж хуткасцю і дакладнасцю.", + "register(s)" : "рэестр(ы)", + "register(s) selected" : "рэестр(аў) выбрана", + "required" : "абавязкова", + "results" : "вынікі", + "schema(s)" : "схема(ы)", + "schema(s) selected" : "схем(аў) выбрана", + "sk-..." : "sk-...", + "this application" : "гэта праграма", + "to" : "да", + "vectorized" : "вектарызавана", + "views selected" : "праглядаў выбрана", + "ℹ️ Current Configuration" : "ℹ️ Бягучая канфігурацыя", + "⚡ Batch Processing" : "⚡ Пакетная апрацоўка", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Поўны кантэкст ✓ Менш верагоднасць прапусціць дэталі ✗ Павольнейшыя адказы ✗ Можа ўключаць менш рэлевантную інфармацыю", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Хутчэйшыя адказы ✓ Больш сфакусаваныя адказы ✗ Можа прапусціць рэлевантную інфармацыю", + "✨ AI Features" : "✨ Функцыі ШІ", + "All search trails cleared successfully" : "Усе журналы пошуку паспяхова ачышчаны", + "Multiple search trail deletion not implemented yet" : "Множнае выдаленне журналаў пошуку яшчэ не рэалізавана", + "No expired search trails found to clear" : "Не знойдзена пратэрмінаваных журналаў пошуку для ачысткі", + "Missing message" : "Адсутнічае паведамленне", + "message content is required" : "патрабуецца змест паведамлення", + "Missing conversation or agentUuid" : "Адсутнічае размова або agentUuid", + "Access denied" : "Доступ забаронены", + "Conversation not found" : "Размова не знойдзена", + "AI service not configured" : "Служба ШІ не наладжана", + "Failed to process message" : "Не ўдалося апрацаваць паведамленне", + "Missing conversationId" : "Адсутнічае conversationId", + "conversationId is required" : "патрабуецца conversationId", + "Failed to fetch conversation history" : "Не ўдалося атрымаць гісторыю размовы", + "Failed to clear conversation" : "Не ўдалося ачысціць размову", + "Invalid feedback type" : "Несапраўдны тып водгуку", + "type must be \"positive\" or \"negative\"" : "тып павінен быць \"positive\" або \"negative\"", + "Message not found" : "Паведамленне не знойдзена", + "Failed to save feedback" : "Не ўдалося захаваць водгук", + "Failed to get chat statistics" : "Не ўдалося атрымаць статыстыку чату", + "Not Found" : "Не знойдзена", + "Engine not found" : "Рухавік не знойдзены", + "Consumer not found" : "Спажывец не знойдзены", + "Backend parameter is required" : "Патрабуецца параметр бэкэнда", + "Failed to get database information: %s" : "Не ўдалося атрымаць інфармацыю базы дадзеных: %s", + "SOLR setup error: %s" : "Памылка наладкі SOLR: %s", + "Reindex failed: %s" : "Пераіндэксацыя не ўдалася: %s", + "Anonymized" : "Ананімізавана", + "Entities detected" : "Сутнасці выяўлены", + "Extract Now" : "Выняць зараз", + "Extracted at" : "Вынята ў", + "Extraction" : "Выманне", + "Failed to load extraction data" : "Не ўдалося загрузіць дадзеныя вымання", + "Failed to load register data" : "Не ўдалося загрузіць дадзеныя рэестра", + "Failed to retrieve extraction status." : "Не ўдалося атрымаць статус вымання.", + "Failed to retrieve objects for file." : "Не ўдалося атрымаць аб'екты для файла.", + "No extraction data available for this file" : "Дадзеныя вымання для гэтага файла недаступныя", + "No register objects reference this file" : "Ніводны аб'ект рэестра не спасылаецца на гэты файл", + "Not extracted" : "Не вынята", + "Register Objects" : "Аб'екты рэестра", + "Risk level" : "Узровень рызыкі", + "Text chunks" : "Тэкставыя фрагменты", + "Unknown error" : "Невядомая памылка", + "Very high" : "Вельмі высокі", + "{title} in {register} / {schema}" : "{title} у {register} / {schema}", + "Push Notifications" : "Push-апавяшчэнні", + "Real-time push notification status via notify_push" : "Статус push-апавяшчэнняў у рэальным часе праз notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push у рэальным часе недаступны — праграма notify_push не ўсталявана", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Усталюйце праграму notify_push з крамы праграм Nextcloud, каб уключыць абнаўленні ў рэальным часе.", + "Open Nextcloud App Store" : "Адкрыць краму праграм Nextcloud", + "notify_push is installed but not yet active" : "notify_push усталявана, але яшчэ не актыўна", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push усталявана, але OpenRegister яшчэ не пацвердзіў паспяховы push. Запусціце захаванне аб'екта для актывацыі або праверце вашу канфігурацыю notify_push.", + "notify_push configuration guide" : "кіраўніцтва па канфігурацыі notify_push", + "Realtime push active" : "Push у рэальным часе актыўны", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push-апавяшчэнні ў рэальным часе актыўны. Падключаныя кліенты атрымліваюць імгненныя абнаўленні пры стварэнні, абнаўленні або выдаленні аб'ектаў.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +); diff --git a/l10n/be.json b/l10n/be.json new file mode 100644 index 0000000000..4bd78821bf --- /dev/null +++ b/l10n/be.json @@ -0,0 +1,2670 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(требуется Dolphin)", + "(no subject)": "(без темы)", + "(no title)": "(без названия)", + "(unnamed)": "(без имени)", + "(untitled card)": "(карточка без названия)", + "({count} selected)": "({count} выбрано)", + ", {failed} failed": ", {failed} неудачно", + "0 = unlimited": "0 = неограниченно", + "A deactivation request is pending.": "Запрос на деактивацию ожидает обработки.", + "AI Assistant": "ИИ-помощник", + "AI Chat": "ИИ-чат", + "AI service not configured": "Сервис ИИ не настроен", + "API Key": "API-ключ", + "API Request Quota (requests/day)": "Квота запросов API (запросов/день)", + "API Token Configuration": "налада API-токенов", + "API Tokens": "API-токены", + "API tokens saved successfully": "API-токены паспяхова сохранены", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Ст. 30 §4: в этом отчёте каждая операция обработки связывается с общим количеством записей журнала аудита. Используется аудиторами и Autoriteit Persoonsgegevens для надзорной проверки.", + "About ConfigSets": "О наборах конфигурации (ConfigSets)", + "Access denied": "Доступ запрещён", + "Access organisation dashboard and analytics": "Доступ к дашборду и аналитике организации", + "Account": "Аккаунт", + "Action": "Действие", + "Action Distribution": "Распределение действий", + "Action:": "Действие:", + "Actions": "Действия", + "Active": "Активный", + "Active Collections": "Активные коллекции", + "Active Filters": "Активные фильтры", + "Active Organisation:": "Активная организация:", + "Active Organisations": "Активные организации", + "Active filters:": "Активные фильтры:", + "Active organisation changed successfully": "Активная организация паспяхова изменена", + "Active organisation set successfully": "Активная организация паспяхова установлена", + "Activities": "Действия", + "Activity": "Активность", + "Activity Period": "Период активности", + "Add": "дадаць", + "Add Attachment": "дадаць вложение", + "Add Endpoint": "дадаць конечную точку", + "Add Groups": "дадаць группы", + "Add Property to '{title}'": "дадаць свойство к '{title}'", + "Add Register": "дадаць рэестр", + "Add Schema": "дадаць схему", + "Add Source": "дадаць источник", + "Add User": "дадаць карыстальніка", + "Add User to Organisation": "дадаць карыстальніка в организацию", + "Add a contact from any of your address books to associate it with this object.": "Добавьте контакт из любой адресной книги для связи с этим объектом.", + "Add a file or files": "дадаць файл или файлы", + "Add card": "дадаць карточку", + "Add contact": "дадаць контакт", + "Add endpoint": "дадаць конечную точку", + "Add oneOf entry": "дадаць запись oneOf", + "Add range": "дадаць диапазон", + "Add schema titles, descriptions, and register information to provide richer context for search": "Добавьте заголовки, описания схем и информацию реестра для более богатого контекста поиска", + "Add to favorites": "дадаць в избранное", + "Added": "Добавлено", + "Added to favorites": "Добавлено в избранное", + "Additional Information": "Дополнительная информация", + "Additional feedback saved. Thank you!": "Дополнительный отзыв захаваны. Спасибо!", + "Addresses": "Адреса", + "Admin": "Администратор", + "Advanced": "Дополнительно", + "Advanced Filters": "Расширенные фильтры", + "Advanced Options": "Расширенные параметры", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Расширенные фильтры недоступны при использовании источника базы данных. Переключитесь на Auto или SOLR Index для использования фильтров.", + "After": "После", + "Agent Type": "Тип агента", + "Agent deleted successfully": "Агент паспяхова выдалены", + "Agent successfully created": "Агент паспяхова створаны", + "Agent successfully updated": "Агент паспяхова обновлён", + "Agents": "Агенты", + "Aggregated across schemas": "Агрегировано по схемам", + "All": "Все", + "All Categories": "Все категории", + "All Collections": "Все коллекции", + "All Confidence Levels": "Все уровни достоверности", + "All ConfigSets": "Все наборы конфигурации", + "All Files": "Все файлы", + "All Levels": "Все уровни", + "All Types": "Все типы", + "All Webhooks": "Все веб-хуки", + "All actions": "Все действия", + "All audit trails cleared successfully": "Все журналы аудита паспяхова очищены", + "All clear": "Всё в порядке", + "All file types": "Все типы файлов", + "All registers": "Все реестры", + "All schemas": "Все схемы", + "All search trails cleared successfully": "Все журналы поиска паспяхова очищены", + "All searches": "Все поисковые запросы", + "All users": "Все карыстальнікі", + "All-day events": "События на весь день", + "Allowed MIME Types": "Разрешённые типы MIME", + "Already linked": "Уже связано", + "An error occurred while clearing audit trails": "Произошла памылка при очистке журналов аудита", + "An error occurred while deleting the audit trail": "Произошла памылка при удалении записи журнала аудита", + "An error occurred while deleting the object": "Произошла памылка при удалении аб'екта", + "An error occurred while deleting the view": "Произошла памылка при удалении представления", + "An error occurred while permanently deleting the objects": "Произошла памылка при безвозвратном удалении аб'ектаў", + "An error occurred while restoring the objects": "Произошла памылка при восстановлении аб'ектаў", + "An error occurred while saving the organisation": "Произошла памылка при сохранении организации", + "Analysis completed:": "Анализ завершён:", + "Analysis steps:": "Шаги анализа:", + "Analytics": "Аналитика", + "Analyze Objects": "Анализировать аб'екты", + "Analyze Properties": "Анализировать свойства", + "Analyze existing properties for improvement opportunities": "Анализировать существующие свойства для поиска возможностей улучшения", + "Analyzing...": "Анализ...", + "Anonymized": "Анонимизировано", + "Anonymous Group": "Анонимная группа", + "Any favorites and sharing settings for this view": "Все налады избранного и совместного доступа для этого представления", + "Any user": "Любой карыстальнік", + "App ID": "ID приложения", + "App store cache invalidated successfully": "Кэш магазина приложений паспяхова аннулирован", + "App store cache invalidated: {invalidated}": "Кэш магазина приложений аннулирован: {invalidated}", + "Application deleted successfully": "Приложение паспяхова удалено", + "Application identifier for this configuration (optional)": "Идентификатор приложения для этой конфигурации (необязательно)", + "Application settings": "налады приложения", + "Application successfully created": "Приложение паспяхова создано", + "Application successfully updated": "Приложение паспяхова обновлено", + "Applications": "Приложения", + "Apply": "Применить", + "Apply Changes": "Применить изменения", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Применить роль ко всем схемам в этом реестре, у которых нет явных переопределений авторизации.", + "Approval Progress": "Ход согласования", + "Approve": "Одобрить", + "Approved": "Одобрено", + "Archive": "Архив", + "Archive conversation": "Архивировать переписку", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Архивировать эту verwerkingsactiviteit? Строки журнала аудита будут по-прежнему ссылаться на неё.", + "Archived conversations are hidden from your active list": "Архивированные переписки скрыты из активного списка", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Вы уверены, что хотите выдаліць коллекцию \"{name}\"?\n\nЭто действие:\n• Безвозвратно удалит коллекцию и все её дадзеныя\n• Удалит все проиндексированные документы\n• Нельзя адмяніць", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Вы уверены, что хотите очистить старые журналы поиска? Это удалит записи старше 30 дней.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Вы уверены, что хотите очистить все дадзеныя из коллекции \"{name}\"?\n\nЭто действие:\n• Удалит все проиндексированные документы\n• Сохранит структуру коллекции\n• Нельзя адмяніць", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Вы уверены, что хотите выдаліць приложение \"{name}\"? Это действие нельзя адмяніць.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Вы уверены, что хотите выдаліць выбранные записи журнала аудита? Это действие нельзя адмяніць.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Вы уверены, что хотите выдаліць выбранные записи журнала поиска? Это действие нельзя адмяніць.", + "Are you sure you want to delete this ConfigSet?": "Вы уверены, что хотите выдаліць этот набор конфигурации?", + "Are you sure you want to delete this configset?": "Вы уверены, что хотите выдаліць этот набор конфигурации?", + "Are you sure you want to leave '{name}'?": "Вы уверены, что хотите покинуть '{name}'?", + "Are you sure you want to permanently delete": "Вы уверены, что хотите безвозвратно выдаліць", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Вы уверены, что хотите переиндексировать коллекцию \"{name}\"?\n\nЭто действие:\n• Перестроит индекс со всеми объектами\n• Займёт несколько минут\n• Может повлиять на производительность поиска во время переиндексации", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Вы уверены, что хотите выдаліць схему \"{schema}\" из реестра \"{register}\"? Это действие нельзя адмяніць.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Вы уверены, что хотите программно выдаліць {count} активных аб'ектаў схемы \"{schema}\"?\n\nОни будут помечены как удалённые, но могут быть безвозвратно удалены позже.", + "Array Object Configuration:": "канфігурацыя аб'екта массива:", + "Articles": "Статьи", + "Ask a question...": "Задайте вопрос...", + "Ask questions about your data using natural language": "Задавайте вопросы о ваших данных на естественном языке", + "Assigned collaborative tags": "Назначенные совместные теги", + "Assignment notifications": "Уведомления о назначениях", + "Attachment added to {name}": "Вложение добавлено к {name}", + "Attempt": "Попытка", + "Audit Trail Changes": "Изменения в журнале аудита", + "Audit Trail Details": "Детали записи журнала аудита", + "Audit Trail Management": "Управление журналом аудита", + "Audit Trail Statistics": "Статистика журнала аудита", + "Audit Trails": "Журналы аудита", + "Audit trail data copied to clipboard": "дадзеныя журнала аудита скопированы в буфер обмена", + "Audit trail deleted successfully": "Запись журнала аудита паспяхова удалена", + "Audit trail successfully deleted": "Запись журнала аудита паспяхова удалена", + "Authentication": "аўтэнтыфікацыя", + "Auto (e.g. Y for year, F Y for month)": "Авто (например, Y для года, F Y для месяца)", + "Auto share": "Автоматический общий доступ", + "Auto-commit disabled": "Автоматическая фиксация отключена", + "Auto-commit enabled": "Автоматическая фиксация включена", + "Auto-create Default Organisation": "Автоматически создавать организацию по умолчанию", + "Auto-retry failed vectorizations": "Автоматически повторять неудачные векторизации", + "Automatically commit changes to SOLR index": "Автоматически фиксировать изменения в индексе SOLR", + "Automatically connect to the federation network.": "Автоматически подключаться к сети федерации.", + "Automatically create a default organisation if none exists when the app is initialized": "Автоматически создавать организацию по умолчанию, если она отсутствует при инициализации приложения", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Автоматически создавать векторные представления из текстовых фрагментов при загрузке и обработке файлов", + "Automatically generate vector embeddings when objects are created or updated": "Автоматически создавать векторные представления при создании или обновлении аб'ектаў", + "Automatically retry failed vectorization attempts (max 3 retries)": "Автоматически повторять неудачные попытки векторизации (максимум 3 попытки)", + "Automatically update catalogues.": "Автоматически обновлять каталоги.", + "Available Filters": "Доступные фильтры", + "Available Workflows": "Доступные рабочие процессы", + "Available placeholders:": "Доступные заполнители:", + "Avatar": "Аватар", + "Avatar changes are not supported by your authentication provider.": "Ваш провайдер аутентификации не поддерживает изменение аватара.", + "Avatar removed": "Аватар выдалены", + "Avatar updated successfully": "Аватар паспяхова обновлён", + "Avg Execution Time": "Среднее время выполнения", + "Avg Members/Org": "Среднее число участников/организация", + "Avg Object Views/Session": "Среднее количество просмотров аб'ектаў/сессия", + "Avg Response Time": "Среднее время ответа", + "Avg Results/Search": "Среднее число результатов/пошук", + "Avg Searches/Session": "Среднее число поисков/сессия", + "Back": "Назад", + "Back to Entities": "Назад к объектам", + "Back to Registers": "Назад к реестрам", + "Back to Webhooks": "Назад к веб-хукам", + "Back to applications": "Назад к приложениям", + "Back to entities": "Назад к объектам", + "Backend parameter is required": "Параметр backend обязателен", + "Backend updated successfully. Please reload the application.": "Backend паспяхова обновлён. Пожалуйста, перезагрузите приложение.", + "Bandwidth Quota (MB/month)": "Квота пропускной способности (МБ/месяц)", + "Base URL (Optional)": "Базовый URL (необязательно)", + "Basic Information": "Основная информация", + "Batch Size": "Размер пакета", + "Batch extraction completed": "Пакетное извлечение завершено", + "Before": "До", + "Before object vectorization can work:": "Перед тем как начать векторизацию аб'ектаў:", + "Behavior": "Поведение", + "Behavior Issue": "Проблема поведения", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (длительность ISO-8601, например P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Хранилище Blob устарело. Все аб'екты теперь используют magic tables.", + "Bookmarks": "Закладки", + "Branch": "Ветка", + "Bulk Role Assignment: {title}": "Массовое назначение ролей: {title}", + "Bulk delete operation completed successfully": "Операция массового удаления паспяхова завершена", + "Bulk save operation completed successfully": "Операция массового сохранения паспяхова завершена", + "Business Data": "Бизнес-дадзеныя", + "CSV": "CSV", + "Cache cleared successfully": "Кэш паспяхова очищен", + "Cache grew from {old} to {new} entries.": "Кэш вырос с {old} до {new} записей.", + "Cache shrunk from {old} to {new} entries.": "Кэш сократился с {old} до {new} записей.", + "Cache stayed the same at {size} entries.": "Кэш остался без изменений: {size} записей.", + "Calculate Sizes": "Вычислить размеры", + "Calendar": "Календарь", + "Calendar Provider Configuration": "налада провайдера календаря", + "Calendar integration is not available": "Интеграция с календарём недоступна", + "Calendar name": "назва календаря", + "Cancel": "адмена", + "Cancel deactivation request": "адмяніць запрос на деактивацию", + "Cannot delete: objects are still attached": "Невозможно выдаліць: аб'екты всё ещё прикреплены", + "Cannot edit: This register is managed by external configuration {title}": "Невозможно рэдагаваць: этот рэестр управляется внешней конфигурацией {title}", + "Cannot remove schema with existing objects ({count} objects)": "Невозможно выдаліць схему с существующими объектами ({count} аб'ектаў)", + "Cannot remove schema {schema} because it contains {count} objects": "Невозможно выдаліць схему {schema}, так как она содержит {count} аб'ектаў", + "Cards": "Карточки", + "Cascade delete": "Каскадное выдаленне", + "Categories of data subjects (one per line)": "Категории субъектов данных (по одной на строку)", + "Categories of personal data (one per line)": "Категории персональных данных (по одной на строку)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Категория", + "Change Type": "Тип изменения", + "Change password": "Изменить пароль", + "Changes": "Изменения", + "Changes copied to clipboard": "Изменения скопированы в буфер обмена", + "Changes for Audit Trail #{id}": "Изменения для записи журнала аудита #{id}", + "Chat Model": "Модель чата", + "Chat Provider": "Провайдер чата", + "Chat Provider (RAG)": "Провайдер чата (RAG)", + "Chat Settings": "налады чата", + "Chat provider connection successful!": "Подключение к провайдеру чата паспяхова!", + "Chat settings": "налады чата", + "Chat test failed: {error}": "Тест чата не удался: {error}", + "Choose a register": "Выберите рэестр", + "Choose a schema": "Выберите схему", + "Choose how vector similarity calculations are performed for semantic search": "Выберите метод вычисления векторного сходства для семантического поиска", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Выберите типы файлов для включения в процесс векторизации. Будут обработаны только файлы с извлечённым текстом и фрагментами.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Выберите представления для включения в процесс векторизации. Оставьте пустым для обработки всех представлений согласно вашей конфигурации.", + "Chunk": "Фрагмент", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "выдаленне фрагментов ещё не реализовано. Используйте конечные точки на основе фрагментов.", + "Chunks": "Фрагменты", + "Chunks to Vectorize:": "Фрагменты для векторизации:", + "Cleanup Old Trails": "Очистить старые журналы", + "Cleanup completed": "Очистка завершена", + "Cleanup completed successfully. Deleted {count} entries.": "Очистка паспяхова завершена. Удалено {count} записей.", + "Cleanup failed: {error}": "памылка очистки: {error}", + "Clear All Embeddings": "Очистить все векторные представления", + "Clear Entries": "Очистить записи", + "Clear Filtered Audit Trails": "Очистить отфильтрованные журналы аудита", + "Clear Filters": "Сбросить фильтры", + "Clear Index": "Очистить индекс", + "Clear Selection": "Снять выделение", + "Clear all": "Очистить всё", + "Clear all filters": "Сбросить все фильтры", + "Clear date range": "Очистить диапазон дат", + "Clear filters": "Сбросить фильтры", + "Clearing...": "Очистка...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Нажмите «Запустить проверку соответствия», чтобы найти схемы, в которых обнаружены персональные дадзеныя, но нет аннотации об операции обработки.", + "Click to subscribe to notifications": "Нажмите, чтобы подписаться на уведомления", + "Click to unsubscribe from notifications": "Нажмите, чтобы отписаться от уведомлений", + "Close": "зачыніць", + "Cluster name": "Имя кластера", + "Code": "Код", + "Code (short readable key, e.g. v-2026-001)": "Код (короткий читаемый ключ, например v-2026-001)", + "Collapse details": "Свернуть детали", + "Collection Management": "Управление коллекциями", + "Collection Name": "назва коллекции", + "Collection assignments updated successfully": "Назначения коллекции паспяхова обновлены", + "Collection cleared successfully": "Коллекция паспяхова очищена", + "Collection copied successfully": "Коллекция паспяхова скопирована", + "Collection created successfully": "Коллекция паспяхова создана", + "Collection deleted successfully": "Коллекция паспяхова удалена", + "Collection used to store and index file metadata and content": "Коллекция для хранения и индексации метаданных и содержимого файлов", + "Collection used to store and index object data": "Коллекция для хранения и индексации данных аб'ектаў", + "Collections:": "Коллекции:", + "Color": "Цвет", + "Comment...": "Комментарий...", + "Commit Message": "Сообщение коммита", + "Commit Within (ms)": "Фиксация в течение (мс)", + "Commit message": "Сообщение коммита", + "Compare current schema with real object data": "Сравнить текущую схему с реальными данными аб'ектаў", + "Completed": "Завершено", + "Complex": "Сложный", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Сложные запросы: расширенный пошук с несколькими фильтрами, операторами и сложными комбинациями параметров", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Сложные запросы: расширенный пошук с несколькими фильтрами, операторами и сложными комбинациями параметров", + "Compliance": "Соответствие требованиям", + "Confidence Level": "Уровень достоверности", + "Confidence Score": "Оценка достоверности", + "Confidence: {confidence}%": "Достоверность: {confidence}%", + "Config must be provided as an object": "канфігурацыя должна быть предоставлена как аб'ект", + "ConfigSet": "Набор конфигурации", + "ConfigSet Management": "Управление наборами конфигурации", + "ConfigSet Name": "назва набора конфигурации", + "ConfigSet created successfully": "Набор конфигурации паспяхова створаны", + "ConfigSet deleted successfully": "Набор конфигурации паспяхова выдалены", + "ConfigSet:": "Набор конфигурации:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Наборы конфигурации определяют схему и налады для ваших коллекций SOLR. Они содержат определения полей, анализаторы и другие налады поиска.", + "Configuration": "канфігурацыя", + "Configuration Data": "дадзеныя конфигурации", + "Configuration is up to date": "канфігурацыя актуальна", + "Configuration saved": "канфігурацыя сохранена", + "Configuration saved successfully.": "канфігурацыя паспяхова сохранена.", + "Configuration type (default, application, etc.)": "Тип конфигурации (по умолчанию, приложение и т.д.)", + "Configurations": "Конфигурации", + "Configure API tokens for external service integrations": "Настроить API-токены для интеграций с внешними сервисами", + "Configure Apache SOLR search engine for advanced search capabilities": "Настроить поисковой движок Apache SOLR для расширенных возможностей поиска", + "Configure Facets": "Настроить фасеты", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Настроить провайдеров больших языковых моделей (LLM) для функций на основе ИИ, включая семантический пошук, векторные представления и чат.", + "Configure SOLR Facets": "Настроить фасеты SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Настроить основные параметры подключения к серверу SOLR, включая аутентификацию и сетевые параметры. Используйте отдельные диалоги управления наборами конфигурации и коллекциями для управления ядрами и коллекциями.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Настроить основные параметры подключения к серверу SOLR, включая аутентификацию и сетевые параметры. Используйте отдельные диалоги управления наборами конфигурации и коллекциями для управления ядрами и коллекциями.", + "Configure default organisation and organisation-related settings": "Настроить организацию по умолчанию и связанные налады", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Настроить преобразование аб'ектаў базы данных в векторные представления для семантического поиска. аб'екты векторизуются напрямую без необходимости извлечения текста.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Настроить преобразование аб'ектаў в текст перед векторизацией. Эти налады влияют на качество поиска и контекст.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Настроить провайдеров больших языковых моделей (LLM) для функций на основе ИИ, включая семантический пошук, векторные представления и чат.", + "Configure multi-organization support and tenant isolation": "Настроить поддержку нескольких организаций и изоляцию арендаторов", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Настроить параметры векторизации файлов. Этот процесс создаст векторные представления для всех извлечённых фрагментов файлов.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Настроить параметры векторизации аб'ектаў. Этот процесс создаст векторные представления для всех аб'ектаў, соответствующих фильтрам представления.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Настроить эту схему для отображения аб'ектаў как событий в приложении Nextcloud Календарь.", + "Configure which types of data to search and how many sources to retrieve": "Настроить типы данных для поиска и количество извлекаемых источников", + "Configuring field mappings...": "налада сопоставления полей...", + "Confirm Account Deactivation": "пацвердзіць деактивацию аккаунта", + "Confirm Object Deletion": "пацвердзіць выдаленне аб'екта", + "Confirm Object Restoration": "пацвердзіць восстановление аб'екта", + "Confirm Permanent Object Deletion": "пацвердзіць безвозвратное выдаленне аб'екта", + "Confirm deactivation": "пацвердзіць деактивацию", + "Connecting to SOLR cluster...": "Подключение к кластеру SOLR...", + "Connection Failed": "Подключение не удалось", + "Connection Settings": "налады подключения", + "Connection Successful!": "Подключение паспяхова!", + "Connection failed": "Подключение не удалось", + "Connection protocol": "Протокол подключения", + "Connection successful": "Подключение паспяхова", + "Connection timeout in seconds": "Тайм-аут подключения в секундах", + "Constant delay between retries (5 minutes)": "Постоянная задержка между повторными попытками (5 минут)", + "Constraint Issue": "Проблема ограничения", + "Consumer not found": "Потребитель не найден", + "Contacts": "Контакты", + "Contacts integration is not available": "Интеграция с контактами недоступна", + "Content Filters": "Фильтры содержимого", + "Continue to Properties": "Перейти к свойствам", + "Contracts": "Контракты", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Управляйте тем, какие представления аб'ектаў следует векторизовать, чтобы снизить расходы на API. Векторизуйте только те представления, которые выигрывают от семантического поиска.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Управляйте тем, какие представления и инструменты ИИ может использовать в этой переписке. По умолчанию все возможности агента включены.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Управляет случайностью (0 = детерминировано, 2 = очень творчески)", + "Conversation ID is required": "Требуется ID переписки", + "Conversation archived": "Переписка архивирована", + "Conversation archived successfully": "Переписка паспяхова архивирована", + "Conversation cleared successfully": "Переписка паспяхова очищена", + "Conversation deleted": "Переписка удалена", + "Conversation not found": "Переписка не найдена", + "Conversation permanently deleted": "Переписка безвозвратно удалена", + "Conversation renamed": "Переписка переименована", + "Conversation restored": "Переписка восстановлена", + "Conversation started with {agent}": "Переписка начата с {agent}", + "Conversation title": "назва переписки", + "Conversations": "Переписки", + "Converting {schema} to blob storage...": "Конвертация {schema} в blob-хранилище...", + "Converting {schema} to magic table...": "Конвертация {schema} в magic table...", + "Copied!": "Скопировано!", + "Copy": "Копировать", + "Copy Changes": "Копировать изменения", + "Copy Collection": "Копировать коллекцию", + "Copy Data": "Копировать дадзеныя", + "Copy Full Data": "Копировать все дадзеныя", + "Copy of {name}": "Копия {name}", + "Copy to clipboard": "Копировать в буфер обмена", + "Copying...": "Копирование...", + "Costs": "Затраты", + "Count": "Количество", + "Counting objects...": "Подсчёт аб'ектаў...", + "Create": "стварыць", + "Create API Token": "стварыць API-токен", + "Create Agent": "стварыць агента", + "Create Application": "стварыць приложение", + "Create Collection": "стварыць коллекцию", + "Create ConfigSet": "стварыць набор конфигурации", + "Create New Collection": "стварыць новую коллекцию", + "Create New ConfigSet": "стварыць новый набор конфигурации", + "Create Operations": "Операции создания", + "Create Organisation": "стварыць организацию", + "Create Webhook": "стварыць веб-хук", + "Create a copy of collection:": "стварыць копию коллекции:", + "Create a new ConfigSet based on the _default template": "стварыць новый набор конфигурации на основе шаблона _default", + "Create a new SOLR collection from an existing ConfigSet": "стварыць новую коллекцию SOLR из существующего набора конфигурации", + "Create a new SOLR collection from an existing configset": "стварыць новую коллекцию SOLR из существующего набора конфигурации", + "Create a new configset based on the _default template": "стварыць новый набор конфигурации на основе шаблона _default", + "Create a new event or link an existing one from any of your calendars.": "Создайте новое событие или свяжите существующее из любого из ваших календарей.", + "Create another": "стварыць ещё", + "Create copies of": "стварыць копии", + "Create event": "стварыць событие", + "Create new token": "стварыць новый токен", + "Create or link a Deck card to track work on this object.": "Создайте или свяжите карточку Deck для отслеживания работы с этим объектом.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Создайте первую verwerkingsactiviteit, чтобы начать помечать строки журнала аудита их атрибуцией по AVG Ст. 30.", + "Create your first AI agent to get started.": "Создайте своего первого ИИ-агента для начала работы.", + "Created": "Создано", + "Created:": "Создано:", + "Creating search collections...": "стварэнне коллекций для поиска...", + "Creating...": "стварэнне...", + "Current": "Текущий", + "Current Filters": "Текущие фильтры", + "Current Type:": "Текущий тип:", + "Current password": "Текущий пароль", + "Custom API endpoint if using a different region": "Пользовательская конечная точка API для другого региона", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Пользовательские HTTP-заголовки (по одному на строку, формат: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Пользовательские HTTP-заголовки (по одному на строку, формат: header-name: value)", + "Custom Ranges:": "Пользовательские диапазоны:", + "Custom display title for this facet": "Пользовательский заголовок для этого фасета", + "Custom pattern:": "Пользовательский шаблон:", + "DELETE COLLECTION": "выдаліць КОЛЛЕКЦИЮ", + "DSAR": "DSAR", + "Daily": "Ежедневно", + "Dashboard": "Дашборд", + "Dashboard not found": "Дашборд не найден", + "Data": "дадзеныя", + "Data Source": "Источник данных", + "Data Sources": "Источники данных", + "Data Views": "Представления данных", + "Data sources": "Источники данных", + "Data type does not match observed values": "Тип данных не соответствует наблюдаемым значениям", + "Data-subject access request": "Запрос на доступ субъекта данных", + "Database URL": "URL базы данных", + "Database information refreshed": "Информация о базе данных обновлена", + "Date Faceting:": "Фасетирование по дате:", + "Date Range": "Диапазон дат", + "Dates": "Даты", + "Deactivation request cancelled": "Запрос на деактивацию отменён", + "Deactivation request submitted": "Запрос на деактивацию отправлен", + "Deck": "Deck", + "Deck integration is not available": "Интеграция с Deck недоступна", + "Default": "По умолчанию", + "Default Object Tenant": "Арендатор аб'екта по умолчанию", + "Default Organisation": "Организация по умолчанию", + "Default Owner": "Владелец по умолчанию", + "Default User (for cron/background jobs)": "карыстальнік по умолчанию (для cron/фоновых задач)", + "Default User Tenant": "Арендатор карыстальніка по умолчанию", + "Default value": "значэнне по умолчанию", + "Default value (Email)": "значэнне по умолчанию (Email)", + "Default value (Password)": "значэнне по умолчанию (Пароль)", + "Default value (Phone number)": "значэнне по умолчанию (Номер телефона)", + "Default value (Regex)": "значэнне по умолчанию (Regex)", + "Default: 5": "По умолчанию: 5", + "Defaults to current user. Select a different user if needed.": "По умолчанию — текущий карыстальнік. При необходимости выберите другого.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Задержка удваивается с каждой попыткой (2, 4, 8 минут...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Задержка увеличивается линейно (5, 10, 15 минут...)", + "Delete": "выдаліць", + "Delete ({count})": "выдаліць ({count})", + "Delete Application": "выдаліць приложение", + "Delete Audit Trail": "выдаліць запись журнала аудита", + "Delete Collection": "выдаліць коллекцию", + "Delete ConfigSet": "выдаліць набор конфигурации", + "Delete Objects": "выдаліць аб'екты", + "Delete Operations": "Операции удаления", + "Delete SOLR Collection": "выдаліць коллекцию SOLR", + "Delete View": "выдаліць представление", + "Delete all objects in this schema": "выдаліць все аб'екты в этой схеме", + "Delete permanently": "выдаліць навсегда", + "Delete this conversation?": "выдаліць эту переписку?", + "Delete view": "выдаліць представление", + "Delete {count} object": "выдаліць {count} аб'ект", + "Delete {count} objects": "выдаліць {count} аб'ектаў", + "Deleted": "Удалено", + "Deleted By": "Удалено пользователем", + "Deleted Date": "Дата удаления", + "Deleted Items Management": "Управление удалёнными элементами", + "Deleted This Week": "Удалено на этой неделе", + "Deleted Today": "Удалено сегодня", + "Deleted:": "Удалено:", + "Deleting SOLR Collection...": "выдаленне коллекции SOLR...", + "Deleting...": "выдаленне...", + "Deletion Date Range": "Диапазон дат удаления", + "Deletion Statistics": "Статистика удалений", + "Deprecated": "Устаревший", + "Depublish": "Снять с публикации", + "Description": "апісанне", + "Description Template": "Шаблон описания", + "Description shown as tooltip": "апісанне, отображаемое как подсказка", + "Description:": "апісанне:", + "Detect data types and patterns": "Определить типы данных и шаблоны", + "Detected At": "Обнаружено в", + "Detected Format:": "Обнаруженный формат:", + "Detected Issues:": "Обнаруженные проблемы:", + "Detected: {date}": "Обнаружено: {date}", + "Direct permission": "Прямое разрешение", + "Disable": "Отключить", + "Disable this to make the agent": "Отключите это, чтобы агент", + "Disabled": "Отключено", + "Discover Files": "Обнаружить файлы", + "Discovered Properties": "Обнаруженные свойства", + "Display Format": "Формат отображения", + "Display Name": "Отображаемое имя", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Вы хотите безвозвратно выдаліць все отфильтрованные записи журнала аудита? Это действие нельзя адмяніць.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Вы хотите безвозвратно выдаліць эту запись журнала аудита? Это действие нельзя адмяніць.", + "Documents": "Документы", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR включён", + "Download API Spec": "Скачать спецификацию API", + "Download API Specification": "Скачать спецификацию API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Скачать копию всех ваших персональных данных, хранящихся в OpenRegister (GDPR Статья 20).", + "Drag a file or files here": "Перетащите файл или файлы сюда", + "Dry run completed: {count} fields would be created": "Тестовый запуск завершён: будет создано {count} полей", + "Dry run completed: {count} fields would be fixed": "Тестовый запуск завершён: будет исправлено {count} полей", + "Duration in seconds (optional)": "Длительность в секундах (необязательно)", + "Edit": "рэдагаваць", + "Edit Agent": "рэдагаваць агента", + "Edit Application": "рэдагаваць приложение", + "Edit Configuration": "рэдагаваць конфигурацию", + "Edit Endpoint": "рэдагаваць конечную точку", + "Edit Organisation": "рэдагаваць организацию", + "Edit Property '{key}' of '{title}'": "рэдагаваць свойство '{key}' аб'екта '{title}'", + "Edit Register": "рэдагаваць рэестр", + "Edit Schema": "рэдагаваць схему", + "Edit Source": "рэдагаваць источник", + "Edit View": "рэдагаваць представление", + "Edit Webhook": "рэдагаваць веб-хук", + "Edit labels": "рэдагаваць метки", + "Edit processing activity": "рэдагаваць операцию обработки", + "Edit tags for {name}": "рэдагаваць теги для {name}", + "Edit verwerkingsactiviteit": "рэдагаваць verwerkingsactiviteit", + "Edit view": "рэдагаваць представление", + "Edit view details": "рэдагаваць детали представления", + "Effectiveness": "Эффективность", + "Email": "Электронная почта", + "Email Addresses": "Адреса электронной почты", + "Email digest frequency": "Частота дайджеста по почте", + "Emails": "Письма", + "Embedding Model": "Модель векторных представлений", + "Embedding Provider": "Провайдер векторных представлений", + "Embedding generated successfully": "Векторное представление паспяхова создано", + "Embedding provider connection successful!": "Подключение к провайдеру векторных представлений паспяхова!", + "Embedding test failed: {error}": "Тест векторных представлений не удался: {error}", + "Enable": "уключыць", + "Enable RAG": "уключыць RAG", + "Enable auto-creation": "уключыць автосоздание", + "Enable automatic file vectorization": "уключыць автоматическую векторизацию файлов", + "Enable automatic object vectorization": "уключыць автоматическую векторизацию аб'ектаў", + "Enable automatic synchronization": "уключыць автоматическую синхронизацию", + "Enable calendar provider": "уключыць провайдер календаря", + "Enable detailed logging for SOLR operations (recommended for debugging)": "уключыць подробное логирование операций SOLR (рекомендуется для отладки)", + "Enable faceting": "уключыць фасетирование", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "уключыць или отключить функции LLM. Настройте провайдеров и модели с помощью кнопки «налада LLM» выше.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "уключыць или отключить функции LLM. Настройте провайдеров и модели с помощью кнопки налады LLM выше.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "уключыць или отключить интеграцию поиска SOLR. Настройте параметры подключения с помощью кнопки «налады подключения» выше.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "уключыць или отключить интеграцию поиска SOLR. Настройте параметры подключения с помощью кнопки настроек подключения выше.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "уключыць или отключить интеграцию с рабочими процессами n8n. Настройте параметры подключения ниже.", + "Enable or disable this webhook": "уключыць или отключить этот веб-хук", + "Enable tools that allow the agent to interact with data through function calling.": "уключыць инструменты, позволяющие агенту взаимодействовать с данными через вызов функций.", + "Enable vectorization for all existing and future views (may increase costs)": "уключыць векторизацию для всех существующих и будущих представлений (может увеличить затраты)", + "Enable write-back to target objects": "уключыць обратную запись в целевые аб'екты", + "Enabled": "Включено", + "End Date Field": "Поле даты окончания", + "Endpoint Path*": "Путь конечной точки*", + "Endpoints": "Конечные точки", + "Engine not found": "Движок не найден", + "Enter ConfigSet name": "Введите назва набора конфигурации", + "Enter Nextcloud usernames to grant access to this private agent": "Введите имена карыстальнікаў Nextcloud для предоставления доступа к этому частному агенту", + "Enter agent description (optional)": "Введите апісанне агента (необязательно)", + "Enter agent name": "Введите имя агента", + "Enter application description (optional)": "Введите апісанне приложения (необязательно)", + "Enter application name": "Введите назва приложения", + "Enter collection name": "Введите назва коллекции", + "Enter configuration description (optional)": "Введите апісанне конфигурации (необязательно)", + "Enter configuration title": "Введите заголовок конфигурации", + "Enter description (optional)...": "Введите апісанне (необязательно)...", + "Enter new collection name": "Введите новое назва коллекции", + "Enter object ID": "Введите ID аб'екта", + "Enter organisation description (optional)": "Введите апісанне организации (необязательно)", + "Enter organisation name": "Введите назва организации", + "Enter search term": "Введите поисковый запрос", + "Enter search terms or leave empty to browse all": "Введите поисковые запросы или оставьте пустым для просмотра всех", + "Enter system prompt for the agent": "Введите системный промпт для агента", + "Enter username and press Enter": "Введите имя карыстальніка и нажмите Enter", + "Enter view name...": "Введите назва представления...", + "Enter webhook name": "Введите назва веб-хука", + "Enter your API key": "Введите ваш API-ключ", + "Enter your object here...": "Введите ваш аб'ект здесь...", + "Enter your schema here...": "Введите вашу схему здесь...", + "Entities": "аб'екты", + "Entities detected": "аб'екты обнаружены", + "Entity ID: {id}": "ID аб'екта: {id}", + "Entity Information": "Информация об объекте", + "Entity Recognition": "Распознавание аб'ектаў", + "Entity Recognition Method": "Метод распознавания аб'ектаў", + "Entity deleted successfully": "аб'ект паспяхова выдалены", + "Entity not found": "аб'ект не найден", + "Entries to be deleted:": "Записи для удаления:", + "Enum Issue": "Проблема с перечислением", + "Enum constraint is missing": "Ограничение перечисления отсутствует", + "Erase (Art 17)": "Стереть (Ст. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Стереть {count} аб'ект(ов) для этого субъекта? Это действие будет зафиксировано в журнале аудита.", + "Erasure complete": "Стирание завершено", + "Erasure preview": "Предпросмотр стирания", + "Error": "памылка", + "Error Details": "Детали памылкі", + "Error Information": "Информация об ошибке", + "Error Message": "Сообщение об ошибке", + "Error deleting audit trails: {error}": "памылка при удалении журналов аудита: {error}", + "Error deleting search trails: {error}": "памылка при удалении журналов поиска: {error}", + "Error loading application": "памылка при загрузке приложения", + "Error loading audit trails": "памылка при загрузке журналов аудита", + "Error loading entity": "памылка при загрузке аб'екта", + "Error loading search trails": "памылка при загрузке журналов поиска", + "Estimated Batches:": "Расчётное количество пакетов:", + "Estimated Cost:": "Расчётная стоимость:", + "Estimated Duration:": "Расчётная длительность:", + "Event": "Событие", + "Event Property for Payload": "Свойство события для полезной нагрузки", + "Events": "События", + "Every schema with detected PII has a processing-activity annotation.": "Каждая схема с обнаруженными персональными данными имеет аннотацию об операции обработки.", + "Example": "Пример", + "Example Value": "Пример значения", + "Example value for this property": "Пример значения для этого свойства", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Строгий максимум", + "Exclusive Minimum": "Строгий минимум", + "Exclusive maximum": "Строгий максимум", + "Exclusive minimum": "Строгий минимум", + "Execution Mode": "Режим выполнения", + "Execution Time": "Время выполнения", + "Execution Time Range": "Диапазон времени выполнения", + "Existing Improvements": "Существующие улучшения", + "Expand details": "Развернуть детали", + "Expiration": "Срок действия", + "Expires": "Истекает", + "Expires in (e.g., 90d)": "Истекает через (например, 90d)", + "Exponential": "Экспоненциальный", + "Export": "экспарт", + "Export \"{schema}\" objects from \"{register}\"": "экспарт аб'ектаў \"{schema}\" из \"{register}\"", + "Export Configuration": "Экспортировать конфигурацию", + "Export Format:": "Формат экспорта:", + "Export Objects": "Экспортировать аб'екты", + "Export completed successfully": "экспарт паспяхова завершён", + "Export downloaded successfully": "экспарт паспяхова загружен", + "Export failed: {error}": "памылка экспорта: {error}", + "Export is rate limited. Please try again later.": "экспарт ограничен по частоте запросов. Повторите попытку позже.", + "Export my data": "Экспортировать мои дадзеныя", + "Export, view, or delete audit trails": "Экспортировать, просматривать или удалять журналы аудита", + "Exporting...": "экспарт...", + "Extend Schema": "Расширить схему", + "Extended": "Расширенный", + "Extensions": "Расширения", + "Extra Query Parameters": "Дополнительные параметры запроса", + "Extract Now": "Извлечь сейчас", + "Extract Pending Files": "Извлечь ожидающие файлы", + "Extract now": "Извлечь сейчас", + "Extract properties from each object": "Извлечь свойства из каждого аб'екта", + "Extracted At": "Извлечено в", + "Extracted at": "Извлечено в", + "Extraction": "Извлечение", + "Extraction Mode": "Режим извлечения", + "Extraction Scope": "Область извлечения", + "Extraction Status": "Статус извлечения", + "Extraction error for {file}: {error}": "памылка извлечения для {file}: {error}", + "Facet Description": "апісанне фасета", + "Facet Order": "Порядок фасетов", + "Facet Title": "Заголовок фасета", + "Facet Type": "Тип фасета", + "Facet configuration updated successfully": "канфігурацыя фасетов паспяхова обновлена", + "Facetable": "Поддерживает фасетирование", + "Faceting Configuration:": "канфігурацыя фасетирования:", + "Facets discovered and configured successfully": "Фасеты паспяхова обнаружены и настроены", + "Facets discovered successfully": "Фасеты паспяхова обнаружены", + "Failed": "Не удалось", + "Failed to add attachment to object": "Не удалось дадаць вложение к объекту", + "Failed to add user to organisation": "Не удалось дадаць карыстальніка в организацию", + "Failed to analyze schema properties": "Не удалось проанализировать свойства схемы", + "Failed to archive conversation": "Не удалось архивировать переписку", + "Failed to calculate sizes": "Не удалось вычислить размеры", + "Failed to cancel deactivation": "Не удалось адмяніць деактивацию", + "Failed to change active organisation: {error}": "Не удалось изменить активную организацию: {error}", + "Failed to change password": "Не удалось изменить пароль", + "Failed to check version: {error}": "Не удалось проверить версию: {error}", + "Failed to clear audit trails: {error}": "Не удалось очистить журналы аудита: {error}", + "Failed to clear blob objects: {error}": "Не удалось очистить аб'екты blob: {error}", + "Failed to clear cache: {error}": "Не удалось очистить кэш: {error}", + "Failed to clear collection": "Не удалось очистить коллекцию", + "Failed to clear conversation": "Не удалось очистить переписку", + "Failed to clear embeddings: {error}": "Не удалось очистить векторные представления: {error}", + "Failed to clear search trails: {error}": "Не удалось очистить журналы поиска: {error}", + "Failed to clear {type} cache: {error}": "Не удалось очистить кэш {type}: {error}", + "Failed to copy changes": "Не удалось скопировать изменения", + "Failed to copy data": "Не удалось скопировать дадзеныя", + "Failed to copy data to clipboard": "Не удалось скопировать дадзеныя в буфер обмена", + "Failed to copy to clipboard": "Не удалось скопировать в буфер обмена", + "Failed to copy token": "Не удалось скопировать токен", + "Failed to create conversation": "Не удалось стварыць переписку", + "Failed to create missing SOLR fields: {error}": "Не удалось стварыць отсутствующие поля SOLR: {error}", + "Failed to create or find project": "Не удалось стварыць или найти проект", + "Failed to create token": "Не удалось стварыць токен", + "Failed to delete SOLR collection: {error}": "Не удалось выдаліць коллекцию SOLR: {error}", + "Failed to delete collection": "Не удалось выдаліць коллекцию", + "Failed to delete conversation": "Не удалось выдаліць переписку", + "Failed to delete objects for {schema}: {error}": "Не удалось выдаліць аб'екты для {schema}: {error}", + "Failed to delete webhook": "Не удалось выдаліць веб-хук", + "Failed to depublish register: {error}": "Не удалось снять рэестр с публикации: {error}", + "Failed to depublish schema: {error}": "Не удалось снять схему с публикации: {error}", + "Failed to depublish: {error}": "Не удалось снять с публикации: {error}", + "Failed to discover files: {error}": "Не удалось обнаружить файлы: {error}", + "Failed to download API specification": "Не удалось скачать спецификацию API", + "Failed to export data": "Не удалось экспортировать дадзеныя", + "Failed to export objects": "Не удалось экспортировать аб'екты", + "Failed to fetch conversation history": "Не удалось получить историю переписки", + "Failed to fix mismatched SOLR fields: {error}": "Не удалось исправить несоответствующие поля SOLR: {error}", + "Failed to get SOLR field configuration": "Не удалось получить конфигурацию полей SOLR", + "Failed to get chat statistics": "Не удалось получить статистику чата", + "Failed to get database information: %s": "Не удалось получить информацию о базе данных: %s", + "Failed to get entity categories": "Не удалось получить категории аб'ектаў", + "Failed to get entity statistics": "Не удалось получить статистику аб'ектаў", + "Failed to get entity types": "Не удалось получить типы аб'ектаў", + "Failed to get response: {error}": "Не удалось получить ответ: {error}", + "Failed to invalidate app store cache: {error}": "Не удалось аннулировать кэш магазина приложений: {error}", + "Failed to leave organisation: {error}": "Не удалось покинуть организацию: {error}", + "Failed to link object": "Не удалось связать аб'ект", + "Failed to load Deck cards": "Не удалось загрузіць карточки Deck", + "Failed to load LLM configuration": "Не удалось загрузіць конфигурацию LLM", + "Failed to load Nextcloud groups": "Не удалось загрузіць группы Nextcloud", + "Failed to load cache statistics: {error}": "Не удалось загрузіць статистику кэша: {error}", + "Failed to load conversation": "Не удалось загрузіць переписку", + "Failed to load entities": "Не удалось загрузіць аб'екты", + "Failed to load entity": "Не удалось загрузіць аб'ект", + "Failed to load extraction data": "Не удалось загрузіць дадзеныя извлечения", + "Failed to load files": "Не удалось загрузіць файлы", + "Failed to load linked contacts": "Не удалось загрузіць связанные контакты", + "Failed to load linked emails": "Не удалось загрузіць связанные письма", + "Failed to load linked events": "Не удалось загрузіць связанные события", + "Failed to load organisations": "Не удалось загрузіць организации", + "Failed to load register data": "Не удалось загрузіць дадзеныя реестра", + "Failed to load related entities": "Не удалось загрузіць связанные аб'екты", + "Failed to load settings: {error}": "Не удалось загрузіць налады: {error}", + "Failed to load statistics: {error}": "Не удалось загрузіць статистику: {error}", + "Failed to load templates": "Не удалось загрузіць шаблоны", + "Failed to load view: {error}": "Не удалось загрузіць представление: {error}", + "Failed to load webhooks": "Не удалось загрузіць веб-хуки", + "Failed to load workflows": "Не удалось загрузіць рабочие процессы", + "Failed to process message": "Не удалось обработать сообщение", + "Failed to publish register: {error}": "Не удалось опубликовать рэестр: {error}", + "Failed to publish schema: {error}": "Не удалось опубликовать схему: {error}", + "Failed to publish: {error}": "Не удалось опубликовать: {error}", + "Failed to refresh database information": "Не удалось абнавіць информацию о базе данных", + "Failed to reindex collection": "Не удалось переиндексировать коллекцию", + "Failed to remove avatar": "Не удалось выдаліць аватар", + "Failed to remove link": "Не удалось выдаліць ссылку", + "Failed to remove member: {error}": "Не удалось выдаліць участника: {error}", + "Failed to remove schema {schema}: {error}": "Не удалось выдаліць схему {schema}: {error}", + "Failed to remove user from organisation": "Не удалось выдаліць карыстальніка из организации", + "Failed to rename conversation": "Не удалось переименовать переписку", + "Failed to request deactivation": "Не удалось запросить деактивацию", + "Failed to restore conversation": "Не удалось восстановить переписку", + "Failed to retrieve extraction status.": "Не удалось получить статус извлечения.", + "Failed to retrieve objects for file.": "Не удалось получить аб'екты для файла.", + "Failed to retry extraction": "Не удалось повторить извлечение", + "Failed to retry webhook": "Не удалось повторить веб-хук", + "Failed to revoke token": "Не удалось отозвать токен", + "Failed to save GitHub token": "Не удалось захаваць токен GitHub", + "Failed to save GitLab URL": "Не удалось захаваць URL GitLab", + "Failed to save GitLab token": "Не удалось захаваць токен GitLab", + "Failed to save LLM settings: {error}": "Не удалось захаваць налады LLM: {error}", + "Failed to save additional feedback": "Не удалось захаваць дополнительный отзыв", + "Failed to save configuration.": "Не удалось захаваць конфигурацию.", + "Failed to save configuration: {error}": "Не удалось захаваць конфигурацию: {error}", + "Failed to save facet configuration": "Не удалось захаваць конфигурацию фасетов", + "Failed to save feedback": "Не удалось захаваць отзыв", + "Failed to save file settings: {error}": "Не удалось захаваць налады файлов: {error}", + "Failed to save n8n configuration": "Не удалось захаваць конфигурацию n8n", + "Failed to save preferences": "Не удалось захаваць предпочтения", + "Failed to save roles": "Не удалось захаваць роли", + "Failed to save settings": "Не удалось захаваць налады", + "Failed to save settings: {error}": "Не удалось захаваць налады: {error}", + "Failed to save view: {error}": "Не удалось захаваць представление: {error}", + "Failed to save warmup interval: {error}": "Не удалось захаваць интервал прогрева: {error}", + "Failed to save webhook": "Не удалось захаваць веб-хук", + "Failed to send feedback": "Не удалось отправить отзыв", + "Failed to set active organisation: {error}": "Не удалось установить активную организацию: {error}", + "Failed to setup SOLR: {error}": "Не удалось настроить SOLR: {error}", + "Failed to start file warmup: {error}": "Не удалось запустить прогрев файлов: {error}", + "Failed to start processing {type} files: {error}": "Не удалось начать обработку файлов {type}: {error}", + "Failed to start vectorization: {error}": "Не удалось запустить векторизацию: {error}", + "Failed to switch organisation: {error}": "Не удалось переключить организацию: {error}", + "Failed to sync magic table for {schema}: {error}": "Не удалось синхронизировать magic table для {schema}: {error}", + "Failed to test SOLR connection: {error}": "Не удалось проверить подключение SOLR: {error}", + "Failed to test webhook": "Не удалось проверить веб-хук", + "Failed to update LLM settings: {error}": "Не удалось абнавіць налады LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Не удалось абнавіць налады мультиарендности: {error}", + "Failed to update RBAC settings: {error}": "Не удалось абнавіць налады RBAC: {error}", + "Failed to update Retention settings: {error}": "Не удалось абнавіць налады хранения: {error}", + "Failed to update SOLR settings: {error}": "Не удалось абнавіць налады SOLR: {error}", + "Failed to update favorite status": "Не удалось абнавіць статус избранного", + "Failed to update schema configuration for {schema}: {error}": "Не удалось абнавіць конфигурацию схемы {schema}: {error}", + "Failed to update schema properties": "Не удалось абнавіць свойства схемы", + "Failed to update view": "Не удалось абнавіць представление", + "Failed to update view: {error}": "Не удалось абнавіць представление: {error}", + "Failed to update webhook": "Не удалось абнавіць веб-хук", + "Failed to upload avatar": "Не удалось загрузіць аватар", + "Failed to validate {schema}: {error}": "Не удалось проверить {schema}: {error}", + "Failed to vectorize files: {error}": "Не удалось векторизовать файлы: {error}", + "Failed to warmup names cache: {error}": "Не удалось прогреть кэш имён: {error}", + "Federation": "Федерация", + "Feedback recorded": "Отзыв записан", + "Fewer sources (1-3):": "Меньше источников (1-3):", + "Field": "Поле", + "Fields to Display": "Поля для отображения", + "File": "Файл", + "File #{id}": "Файл #{id}", + "File Chunk Prediction": "Прогноз фрагментов файла", + "File Collection": "Коллекция файлов", + "File Handling": "Обработка файлов", + "File Location": "Расположение файла", + "File Management": "Управление файлами", + "File Name": "Имя файла", + "File Path": "Путь к файлу", + "File Type Selection": "Выбор типа файла", + "File Types to Process": "Типы файлов для обработки", + "File Vectorization": "Векторизация файлов", + "File Warmup": "Прогрев файлов", + "File actions menu": "Меню действий с файлом", + "File anonymized successfully": "Файл паспяхова анонимизирован", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Векторизация фрагментов файлов ещё не реализована. Фрагменты готовы и сохранены, но сервис векторизации находится в разработке.", + "File collection not configured": "Коллекция файлов не настроена", + "File discovery completed": "Обнаружение файлов завершено", + "File extraction completed": "Извлечение файлов завершено", + "File extraction queued": "Извлечение файлов поставлено в очередь", + "File indexed successfully": "Файл паспяхова проиндексирован", + "File is already anonymized": "Файл уже анонимизирован", + "File name": "Имя файла", + "File not found": "Файл не найден", + "File settings saved successfully": "налады файлов паспяхова сохранены", + "File settings updated successfully": "налады файлов паспяхова обновлены", + "File sources": "Источники файлов", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Векторизация файлов завершена! Векторизовано {vectorized} фрагментов из {files} файлов. {failed} не удалось.", + "File vectorization configuration saved successfully": "канфігурацыя векторизации файлов паспяхова сохранена", + "File vectorization started. Check the statistics section for progress.": "Векторизация файлов запущена. Прогресс см. в разделе статистики.", + "File warmup completed": "Прогрев файлов завершён", + "Files": "Файлы", + "Files added successfully": "Файлы паспяхова добавлены", + "Files with Completed Extraction:": "Файлы с завершённым извлечением:", + "Files → fileCollection, Objects → objectCollection": "Файлы → fileCollection, аб'екты → objectCollection", + "Files → filecollection, objects → objectcollection": "Файлы → filecollection, аб'екты → objectcollection", + "Filter Audit Trails": "Фильтровать журналы аудита", + "Filter Deleted Items": "Фильтровать удалённые элементы", + "Filter Objects": "Фильтровать аб'екты", + "Filter Properties": "Фильтровать свойства", + "Filter Search Trails": "Фильтровать журналы поиска", + "Filter Statistics": "Статистика фильтров", + "Filter and analyze search trail entries": "Фильтровать и анализировать записи журнала поиска", + "Filter and manage audit trail entries": "Фильтровать и управлять записями журнала аудита", + "Filter and manage soft deleted items": "Фильтровать и управлять программно удалёнными элементами", + "Filter and search entities": "Фильтровать и искать аб'екты", + "Filter by object ID": "Фильтровать по ID аб'екта", + "Filter by search term": "Фильтровать по поисковому запросу", + "Filter by type": "Фильтровать по типу", + "Filter by webhook": "Фильтровать по веб-хуку", + "Filter data loaded automatically. Use the filters below to refine your search.": "дадзеныя фильтров загружены автоматически. Используйте фильтры ниже для уточнения поиска.", + "Filter fields...": "Фильтровать поля...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Фильтровать триггеры веб-хука по свойствам полезной нагрузки (по одному на строку, формат: key: value)", + "Filtered": "Отфильтровано", + "Filters": "Фильтры", + "Finalizing setup...": "Завершение налады...", + "Fireworks AI Chat Settings": "налады чата Fireworks AI", + "Fireworks AI Embedding Configuration": "канфігурацыя векторных представлений Fireworks AI", + "First": "Первый", + "Fixed": "Исправлено", + "For chat and retrieval-augmented generation": "Для чата и генерации с расширенным поиском", + "For vector embeddings and semantic search": "Для векторных представлений и семантического поиска", + "Format": "Формат", + "Format Issue": "Проблема формата", + "Format constraint is missing": "Ограничение формата отсутствует", + "From (e.g. -7 days or 2025-01-01)": "От (например, -7 days или 2025-01-01)", + "From Date": "Дата начала", + "From date": "Дата начала", + "Full data copied to clipboard": "Полные дадзеныя скопированы в буфер обмена", + "Function Tools": "Функциональные инструменты", + "General Issue": "Общая проблема", + "Generate recommendations and confidence scores": "Сгенерировать рекомендации и оценки достоверности", + "Generate report": "Сгенерировать отчёт", + "Generate the verantwoordingsdocument": "Сгенерировать verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Генерировать векторы сразу при создании новых аб'ектаў", + "Generated: {time}": "Сгенерировано: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Получайте дадзеныя в реальном времени о состоянии данных вашей организации, уделяя внимание реестрам, определениям схем, хранению и использованию аб'ектаў.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Получайте дадзеныя в реальном времени о состоянии данных вашей организации, тенденциях роста и показателях качества.", + "GitHub Personal Access Token": "Персональный токен доступа GitHub", + "GitHub token is valid": "Токен GitHub действителен", + "GitHub token is valid! Username: {username}": "Токен GitHub действителен! Имя карыстальніка: {username}", + "GitHub token removed successfully": "Токен GitHub паспяхова выдалены", + "GitHub token saved successfully": "Токен GitHub паспяхова захаваны", + "GitHub token test failed: {message}": "Тест токена GitHub не удался: {message}", + "GitLab Personal Access Token": "Персональный токен доступа GitLab", + "GitLab URL saved successfully": "URL GitLab паспяхова захаваны", + "GitLab token is valid": "Токен GitLab действителен", + "GitLab token is valid! Username: {username}": "Токен GitLab действителен! Имя карыстальніка: {username}", + "GitLab token saved successfully": "Токен GitLab паспяхова захаваны", + "GitLab token test failed: {message}": "Тест токена GitLab не удался: {message}", + "Go to linked Register": "Перейти к связанному реестру", + "Go to linked Schema": "Перейти к связанной схеме", + "Go to organisation": "Перейти к организации", + "Grant additional permissions beyond standard CRUD operations": "Предоставить дополнительные разрешения помимо стандартных CRUD-операций", + "Group Quota": "Квота группы", + "Groups": "Группы", + "HTML preview": "Предпросмотр HTML", + "HTTP Method": "Метод HTTP", + "HTTP method used to send webhook requests": "Метод HTTP для отправки запросов веб-хука", + "Headers": "Заголовки", + "Health": "Состояние", + "Heartbeat successful - connection kept alive": "Сигнал жизнеспособности успешен — соединение поддерживается", + "Helpful": "Полезно", + "Here you can configure the details for various connections.": "Здесь вы можете настроить параметры различных подключений.", + "Here you can configure the details for your organisation.": "Здесь вы можете настроить параметры вашей организации.", + "Hide": "Скрыть", + "Hide Filters": "Скрыть фильтры", + "Hide in collection view": "Скрыть в представлении коллекции", + "Hide in forms": "Скрыть в формах", + "Hide in list view": "Скрыть в списковом представлении", + "High": "Высокий", + "High Confidence": "Высокая достоверность", + "Host": "Хост", + "Hourly": "Каждый час", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Глубина обхода вложенных свойств аб'екта (1-20). Большие значения захватывают больше деталей, но увеличивают размер вектора.", + "How often to check for updates (1-168 hours)": "Как часто проверять обновления (1-168 часов)", + "How to handle retries for failed webhook deliveries": "Как обрабатывать повторные попытки при неудачной доставке веб-хука", + "IBANs": "Номера IBAN", + "ID": "ID", + "ID of input mapping (optional)": "ID входного маппинга (необязательно)", + "ID of output mapping (optional)": "ID выходного маппинга (необязательно)", + "ID of the target resource": "ID целевого ресурса", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ограничения частоты запросов IP паспяхова очищены", + "Identify properties not in the schema": "Определить свойства, отсутствующие в схеме", + "Immutable": "Неизменяемый", + "Import": "імпарт", + "Import Configuration": "Импортировать конфигурацию", + "Import Data into Register": "Импортировать дадзеныя в рэестр", + "Import successful": "імпарт паспяхова выполнен", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Импортируйте шаблон report-bundle.json, чтобы получить рэестр `reports`, затем создайте первый дашборд через стандартный интерфейс аб'ектаў. Дашборды объявляют свои виджеты в JSON, а рендерер передаёт каждому виджету живые агрегированные дадзеныя.", + "Improved Property": "Улучшенное свойство", + "In use": "Используется", + "Inactive": "Неактивный", + "Inactive organisations cannot be used": "Неактивные организации нельзя использовать", + "Include IDs and names of related objects for better contextual search": "уключыць идентификаторы и имена связанных аб'ектаў для улучшения контекстного поиска", + "Include ids and names of related objects for better contextual search": "уключыць идентификаторы и имена связанных аб'ектаў для улучшения контекстного поиска", + "Include related object references": "уключыць ссылки на связанные аб'екты", + "Include schema and register metadata": "уключыць метаданные схемы и реестра", + "Index": "Индекс", + "Information about the current OpenRegister installation": "Информация о текущей установке OpenRegister", + "Inherited from register": "Унаследовано от реестра", + "Inherits permissions from register": "Наследует разрешения от реестра", + "Initialization failed": "Инициализация не удалась", + "Initialize Project": "Инициализировать проект", + "Initializing SOLR setup...": "Инициализация налады SOLR...", + "Input Mapping": "Входной маппинг", + "Inspect Fields": "Проверить поля", + "Inspect Index": "Проверить индекс", + "Inspect SOLR Index": "Проверить индекс SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Установите приложение notify_push из магазина приложений Nextcloud для включения обновлений в реальном времени.", + "Integrations": "Интеграции", + "Internet location (URL) of this installation": "Интернет-адрес (URL) этой установки", + "Interval": "Интервал", + "Invalid": "Недействительный", + "Invalid batch size. Must be between 1 and 5000": "Неверный размер пакета. Должен быть от 1 до 5000", + "Invalid feedback type": "Недопустимый тип отзыва", + "Invalid field name provided": "Указано недействительное имя поля", + "Invalid maxObjects. Must be 0 (all) or positive number": "Неверное значэнне maxObjects. Должно быть 0 (все) или положительное число", + "Invalid:": "Недействительный:", + "Invite Users": "Пригласить карыстальнікаў", + "Invited Users": "Приглашённые карыстальнікі", + "Inzage (Art 15)": "Inzage (Ст. 15)", + "Inzage results": "Результаты Inzage", + "Issue": "Проблема", + "Items per page:": "Элементов на странице:", + "Just now": "Только что", + "Key": "Ключ", + "LLM Configuration": "налада LLM", + "LLM actions menu": "Меню действий LLM", + "LLM configuration saved successfully": "канфігурацыя LLM паспяхова сохранена", + "LLM features disabled": "Функции LLM отключены", + "LLM features enabled": "Функции LLM включены", + "LLM must be enabled with an embedding provider configured": "LLM должен быть включён с настроенным провайдером векторных представлений", + "LLM settings saved successfully": "налады LLM паспяхова сохранены", + "LLM settings updated successfully": "налады LLM паспяхова обновлены", + "Label": "Метка", + "Labels": "Метки", + "Languages": "Языки", + "Last": "Последний", + "Last Triggered": "Последнее срабатывание", + "Leave empty to allow all users access": "Оставьте пустым для доступа всех карыстальнікаў", + "Leave off for auto-detection from property format.": "Оставьте выключенным для автоопределения из формата свойства.", + "Left organisation successfully": "Организация паспяхова покинута", + "Legal basis *": "Правовое основание *", + "Length Range:": "Диапазон длины:", + "Linear": "Линейный", + "Link": "Ссылка", + "Link an email from the Mail app sidebar to associate it with this object.": "Свяжите письмо из боковой панели приложения Mail с этим объектом.", + "Link an object to see it here.": "Свяжите аб'ект, чтобы увидеть его здесь.", + "Link another object": "Связать другой аб'ект", + "Link existing event": "Связать существующее событие", + "Link removed": "Ссылка удалена", + "Link to Object": "Ссылка на аб'ект", + "Link to {name}": "Ссылка на {name}", + "Linked Objects": "Связанные аб'екты", + "Linked by {user}": "Связано пользователем {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Здесь будут отображаться связанные письма, события календаря, контакты и карточки Deck.", + "Linked to {name}": "Связано с {name}", + "Load Advanced Filters": "загрузіць расширенные фильтры", + "Load advanced filters with live data from your search index": "загрузіць расширенные фильтры с живыми данными из вашего поискового индекса", + "Load more": "загрузіць ещё", + "Load view": "загрузіць представление", + "Loading API tokens...": "загрузка API-токенов...", + "Loading ConfigSets...": "загрузка наборов конфигурации...", + "Loading LLM configuration...": "загрузка конфигурации LLM...", + "Loading RBAC settings...": "загрузка настроек RBAC...", + "Loading activity...": "загрузка активности...", + "Loading advanced filters...": "загрузка расширенных фильтров...", + "Loading agents...": "загрузка агентов...", + "Loading application details...": "загрузка сведений о приложении...", + "Loading applications...": "загрузка приложений...", + "Loading archived conversations...": "загрузка архивированных переписок...", + "Loading audit trails...": "загрузка журналов аудита...", + "Loading available tools...": "загрузка доступных инструментов...", + "Loading cache statistics...": "загрузка статистики кэша...", + "Loading collections...": "загрузка коллекций...", + "Loading configurations...": "загрузка конфигураций...", + "Loading conversation...": "загрузка переписки...", + "Loading conversations...": "загрузка переписок...", + "Loading deleted items...": "загрузка удалённых элементов...", + "Loading endpoints": "загрузка конечных точек", + "Loading entities...": "загрузка аб'ектаў...", + "Loading events...": "загрузка событий...", + "Loading filter data...": "загрузка данных фильтров...", + "Loading groups...": "загрузка групп...", + "Loading linked objects...": "загрузка связанных аб'ектаў...", + "Loading log details...": "загрузка деталей журнала...", + "Loading multitenancy settings...": "загрузка настроек мультиарендности...", + "Loading n8n configuration...": "загрузка конфигурации n8n...", + "Loading permission matrix...": "загрузка матрицы разрешений...", + "Loading preferences...": "загрузка предпочтений...", + "Loading register data...": "загрузка данных реестра...", + "Loading registers...": "загрузка реестров...", + "Loading retention settings...": "загрузка настроек хранения...", + "Loading schemas...": "загрузка схем...", + "Loading search configuration...": "загрузка конфигурации поиска...", + "Loading search trails...": "загрузка журналов поиска...", + "Loading sources...": "загрузка источников...", + "Loading statistics...": "загрузка статистики...", + "Loading text extraction settings...": "загрузка настроек извлечения текста...", + "Loading tokens...": "загрузка токенов...", + "Loading user groups...": "загрузка групп карыстальнікаў...", + "Loading users...": "загрузка карыстальнікаў...", + "Loading views...": "загрузка представлений...", + "Loading...": "загрузка...", + "Local": "Локальный", + "Local Version": "Локальная версия", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Найдите каждый аб'ект, ссылающийся на субъекта данных (Ст. 15 inzage), предпросмотрите стирание (Ст. 17 vergetelheid) или экспортируйте их дадзеныя (Ст. 20 portabiliteit).", + "Location": "Расположение", + "Location Field": "Поле расположения", + "Locations": "Местоположения", + "Locked": "Заблокировано", + "Locked:": "Заблокировано:", + "Login successful": "Вход выполнен паспяхова", + "Logs": "Журналы", + "Low": "Низкий", + "Low Confidence": "Низкая достоверность", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Меньшие числа отображаются первыми в боковой панели фильтров. Оставьте пустым для автоматической сортировки.", + "Lucene Version:": "Версия Lucene:", + "Magic table sync completed for {schema}": "сінхранізацыя magic table для {schema} завершена", + "Magic table synchronized successfully": "Magic table паспяхова синхронизирована", + "Mail Integration": "Интеграция с почтой", + "Mail integration is not available": "Интеграция с почтой недоступна", + "Make this installation discoverable within the federation network.": "Сделать эту установку доступной для обнаружения в сети федерации.", + "Manage Organisation Roles": "Управление ролями организации", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Управление коллекциями SOLR (хранилищами данных) и их назначение для аб'ектаў и файлов.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Управление наборами конфигурации SOLR (шаблонами конфигурации) для ваших коллекций.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Управление коллекциями SOLR (хранилищами данных) и их назначение для аб'ектаў и файлов.", + "Manage SOLR configsets (configuration templates) for your collections.": "Управление наборами конфигурации SOLR (шаблонами конфигурации) для ваших коллекций.", + "Manage and configure agents for automated tasks": "Управление и налада агентов для автоматизированных задач", + "Manage and monitor file text extraction status": "Управление и мониторинг статуса извлечения текста из файлов", + "Manage and restore soft deleted items from your registers": "Управление и восстановление программно удалённых элементов из ваших реестров", + "Manage and view detected entities from files and objects": "Управление и просмотр обнаруженных аб'ектаў в файлах и данных", + "Manage document templates and themes": "Управление шаблонами и темами документов", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Управление рабочими процессами n8n для автоматизации OpenRegister. Рабочие процессы будут сохранены в настроенном проекте.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Управление операциями обработки, выполнение запросов на доступ субъектов данных и аудит соответствия требованиям EU GDPR / нидерландского AVG.", + "Manage webhooks for event-driven integrations": "Управление веб-хуками для интеграций на основе событий", + "Manage your account settings, security, and personal data.": "Управляйте настройками аккаунта, безопасностью и персональными данными.", + "Manage your applications and modules": "Управляйте своими приложениями и модулями", + "Manage your chat conversations": "Управляйте своими переписками", + "Manage your data registers and their configurations": "Управляйте реестрами данных и их конфигурациями", + "Manage your data schemas and their properties": "Управляйте схемами данных и их свойствами", + "Manage your data sources and their configurations": "Управляйте источниками данных и их конфигурациями", + "Manage your organisations and switch between them": "Управляйте своими организациями и переключайтесь между ними", + "Manage your saved search configurations": "Управляйте сохранёнными конфигурациями поиска", + "Manage your system configurations and settings": "Управляйте системными конфигурациями и настройками", + "Managed": "Управляемый", + "Mappings": "Маппинги", + "Mass validation completed successfully": "Массовая проверка паспяхова завершена", + "Mass validation failed: {error}": "Массовая проверка не удалась: {error}", + "Max Files (0 = all)": "Максимальное количество файлов (0 = все)", + "Max Length": "Максимальная длина", + "Max Length:": "Максимальная длина:", + "Max Objects (0 = all)": "Максимальное количество аб'ектаў (0 = все)", + "Max Retries": "Максимальное число повторных попыток", + "Max Shards/Node": "Максимальное количество шардов/узел", + "Max Tokens": "Максимальное количество токенов", + "Max execution time (ms)": "Максимальное время выполнения (мс)", + "Max ms": "Макс. мс", + "Max result count": "Максимальное число результатов", + "Max results": "Максимальное число результатов", + "Maximum": "Максимум", + "Maximum File Size (MB)": "Максимальный размер файла (МБ)", + "Maximum Nesting Depth": "Максимальная глубина вложенности", + "Maximum length": "Максимальная длина", + "Maximum length constraint is missing": "Ограничение максимальной длины отсутствует", + "Maximum length is too restrictive": "Максимальная длина слишком ограничительна", + "Maximum number of files to process. Set to 0 to process all files.": "Максимальное количество файлов для обработки. Установите 0 для обработки всех файлов.", + "Maximum number of items": "Максимальное количество элементов", + "Maximum number of objects to process. Set to 0 to process all objects.": "Максимальное количество аб'ектаў для обработки. Установите 0 для обработки всех аб'ектаў.", + "Maximum number of retry attempts for failed deliveries": "Максимальное количество попыток повтора при неудачной доставке", + "Maximum time to wait before committing changes": "Максимальное время ожидания перед фиксацией изменений", + "Maximum tokens to generate": "Максимальное количество генерируемых токенов", + "Maximum value": "Максимальное значэнне", + "Maximum value constraint is missing": "Ограничение максимального значения отсутствует", + "Maximum value is too restrictive": "Максимальное значэнне слишком ограничительно", + "Medium": "Средний", + "Medium Confidence": "Средняя достоверность", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средние запросы: пошук с некоторой фильтрацией или несколькими параметрами (например, диапазоны дат, конкретные реестры/схемы)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средние запросы: пошук с некоторой фильтрацией или несколькими параметрами (например, диапазоны дат, конкретные реестры/схемы)", + "Member removed successfully": "Участник паспяхова выдалены", + "Members": "Участники", + "Members ({count})": "Участники ({count})", + "Members of selected groups can access this view": "Члены выбранных групп могут получить доступ к этому представлению", + "Members:": "Участники:", + "Memory prediction calculated successfully": "Прогноз памяти паспяхова рассчитан", + "Message content is required": "Содержимое сообщения обязательно", + "Message does not belong to this conversation": "Сообщение не принадлежит этой переписке", + "Message not found": "Сообщение не найдено", + "Metadata": "Метаданные", + "Metadata Filters": "Фильтры метаданных", + "Method": "Метод", + "Method*": "Метод*", + "Method: {method}": "Метод: {method}", + "Min Length": "Минимальная длина", + "Min execution time (ms)": "Минимальное время выполнения (мс)", + "Min ms": "Мин. мс", + "Min result count": "Минимальное число результатов", + "Min results": "Минимальное число результатов", + "Minimum": "Минимум", + "Minimum length": "Минимальная длина", + "Minimum number of items": "Минимальное количество элементов", + "Minimum value": "Минимальное значэнне", + "Minimum value constraint is missing": "Ограничение минимального значения отсутствует", + "Minimum value is too restrictive": "Минимальное значэнне слишком ограничительно", + "Missing conversation or agentUuid": "Отсутствует переписка или agentUuid", + "Missing conversationId": "Отсутствует conversationId", + "Missing message": "Отсутствует сообщение", + "Missing object fields created successfully": "Отсутствующие поля аб'екта паспяхова созданы", + "Mode:": "Режим:", + "Model": "Модель", + "Model Name": "назва модели", + "Modified": "Изменено", + "Monthly": "Ежемесячно", + "More sources (10-20):": "Больше источников (10-20):", + "Most Active Objects": "Наиболее активные аб'екты", + "Multiple Of": "Кратно", + "Multiple of": "Кратно", + "Multiple search trail deletion not implemented yet": "Множественное выдаленне журналов поиска ещё не реализовано", + "Multitenancy": "Мультиарендность", + "Multitenancy settings updated successfully": "налады мультиарендности паспяхова обновлены", + "My Account": "Мой аккаунт", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "назва", + "Name *": "назва *", + "Name*": "назва*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Кэш имён паспяхова прогрет: загружено {count} имён за {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Кэш имён паспяхова прогрет: загружено {count} имён за {time}. {message}", + "Names cache warmup completed": "Прогрев кэша имён завершён", + "Naming pattern for copies:": "Шаблон именования копий:", + "New Collection Name": "Новое назва коллекции", + "New Configuration": "Новая канфігурацыя", + "New Conversation": "Новая переписка", + "New Properties": "Новые свойства", + "New Property": "Новое свойство", + "New User Group": "Новая группа карыстальнікаў", + "New Value": "Новое значэнне", + "New activity": "Новая активность", + "New password": "Новый пароль", + "New processing activity": "Новая операция обработки", + "New users without specific organisation membership will be automatically added to this organisation": "Новые карыстальнікі без конкретного членства в организации будут автоматически добавлены в эту организацию", + "New verwerkingsactiviteit": "Новая verwerkingsactiviteit", + "Next": "Далее", + "Nextcloud Groups": "Группы Nextcloud", + "No": "Нет", + "No API tokens.": "API-токенов нет.", + "No ConfigSets found": "Наборы конфигурации не найдены", + "No Deck cards linked to this object": "К этому объекту не привязаны карточки Deck", + "No GitHub token configured": "Токен GitHub не настроен", + "No GitHub token provided": "Токен GitHub не предоставлен", + "No GitLab token provided": "Токен GitLab не предоставлен", + "No Organisation": "Без организации", + "No active filters": "Активных фильтров нет", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Нет активных аб'ектаў для программного удаления в схеме {schema}. Используйте «выдаліць навсегда» для удаления программно удалённых аб'ектаў.", + "No activity data available": "дадзеныя об активности недоступны", + "No activity found.": "Активность не найдена.", + "No agents available": "Агенты недоступны", + "No agents found": "Агенты не найдены", + "No app store cache exists yet": "Кэш магазина приложений ещё не существует", + "No applications are available.": "Приложения недоступны.", + "No applications found": "Приложения не найдены", + "No approval steps for this object.": "Шаги согласования для этого аб'екта отсутствуют.", + "No archived conversations": "Архивированных переписок нет", + "No audit trail entries found": "Записи журнала аудита не найдены", + "No changes recorded": "Изменения не зафиксированы", + "No chunks to vectorize": "Фрагментов для векторизации нет", + "No collections found": "Коллекции не найдены", + "No configuration data": "Данных конфигурации нет", + "No configuration selected": "канфігурацыя не выбрана", + "No configurations are available.": "Конфигурации недоступны.", + "No configurations found": "Конфигурации не найдены", + "No configurations found for this application.": "Конфигурации для этого приложения не найдены.", + "No contacts linked to this object": "К этому объекту не привязаны контакты", + "No conversations yet. Create a new one to get started!": "Переписок ещё нет. Создайте новую, чтобы начать!", + "No dashboards yet": "Дашбордов ещё нет", + "No data available": "дадзеныя недоступны", + "No data available for chart": "дадзеныя для графика недоступны", + "No deleted items found": "Удалённые элементы не найдены", + "No deletion data available": "дадзеныя об удалении недоступны", + "No description found": "апісанне не найдено", + "No description provided": "апісанне не указано", + "No emails linked to this object": "К этому объекту не привязаны письма", + "No endpoints defined": "Конечные точки не определены", + "No entities detected for this email.": "аб'екты для этого письма не обнаружены.", + "No entities detected in this file. Run text extraction first.": "аб'екты в этом файле не обнаружены. Сначала выполните извлечение текста.", + "No entities found": "аб'екты не найдены", + "No entities have been detected yet": "аб'екты ещё не обнаружены", + "No events found": "События не найдены", + "No events linked to this object": "К этому объекту не привязаны события", + "No expired audit trails found to clear": "Истёкшие записи журнала аудита для очистки не найдены", + "No expired search trails found to clear": "Истёкшие записи журнала поиска для очистки не найдены", + "No extraction data available for this file": "дадзеныя извлечения для этого файла недоступны", + "No facetable fields available. Select a register and schema to see available filters.": "Поля для фасетирования недоступны. Выберите рэестр и схему для просмотра доступных фильтров.", + "No files found": "Файлы не найдены", + "No files have been extracted yet": "Файлы ещё не извлекались", + "No files selected": "Файлы не выбраны", + "No files to process": "Файлов для обработки нет", + "No files to reindex": "Файлов для переиндексации нет", + "No filters are currently active. This will delete ALL audit trail entries!": "Активных фильтров нет. Это удалит ВСЕ записи журнала аудита!", + "No groups found. Try a different search.": "Группы не найдены. Попробуйте другой поисковый запрос.", + "No label": "Без метки", + "No labels": "Без меток", + "No linked objects": "Связанных аб'ектаў нет", + "No log entries found": "Записи журнала не найдены", + "No logs are available for this configuration.": "Журналы для этой конфигурации недоступны.", + "No logs are available for this source.": "Журналы для этого источника недоступны.", + "No logs found": "Журналы не найдены", + "No matches": "Совпадений нет", + "No mismatched fields found - SOLR schema is properly configured": "Несоответствующие поля не найдены — схема SOLR настроена правильно", + "No objects are currently selected for deletion.": "аб'екты для удаления не выбраны.", + "No objects are currently selected for permanent deletion.": "аб'екты для безвозвратного удаления не выбраны.", + "No objects are currently selected for restoration.": "аб'екты для восстановления не выбраны.", + "No objects found": "аб'екты не найдены", + "No objects found to vectorize": "аб'екты для векторизации не найдены", + "No objects linked to this email": "К этому письму не привязаны аб'екты", + "No objects selected": "аб'екты не выбраны", + "No objects selected for deletion": "аб'екты для удаления не выбраны", + "No objects selected for restoration": "аб'екты для восстановления не выбраны", + "No objects to delete": "аб'ектаў для удаления нет", + "No objects to delete for schema {schema}": "аб'ектаў для удаления в схеме {schema} нет", + "No objects were permanently deleted": "аб'екты не были безвозвратно удалены", + "No organisations found": "Организации не найдены", + "No personal data was found for this subject identifier.": "Персональные дадзеныя для этого идентификатора субъекта не найдены.", + "No processing activities yet": "Операций обработки ещё нет", + "No properties found": "Свойства не найдены", + "No properties match your filters.": "Свойства не соответствуют вашим фильтрам.", + "No purge date set": "Дата очистки не задана", + "No register data available": "дадзеныя реестра недоступны", + "No register objects reference this file": "Ни один аб'ект реестра не ссылается на этот файл", + "No registers found": "Реестры не найдены", + "No registers found for this application.": "Реестры для этого приложения не найдены.", + "No registers found. Create a register to configure permissions.": "Реестры не найдены. Создайте рэестр для налады разрешений.", + "No related cases found for this sender": "Связанных дел для этого отправителя не найдено", + "No related entities": "Связанных аб'ектаў нет", + "No relations found": "Связи не найдены", + "No request body available": "Тело запроса недоступно", + "No response body available": "Тело ответа недоступно", + "No saved views yet. Create one in the Search tab!": "Сохранённых представлений ещё нет. Создайте одно на вкладке пошук!", + "No saved views yet. create one in the search tab!": "Сохранённых представлений ещё нет. Создайте одно на вкладке поиска!", + "No scan run yet": "Проверка ещё не запускалась", + "No schema data available": "дадзеныя схемы недоступны", + "No schema selected for exploration": "Схема для исследования не выбрана", + "No schemas configured for mail linking.": "Схемы для привязки почты не настроены.", + "No schemas found": "Схемы не найдены", + "No schemas found for this application.": "Схемы для этого приложения не найдены.", + "No search terms data available": "дадзеныя о поисковых запросах недоступны", + "No search trail entries found": "Записи журнала поиска не найдены", + "No sources are available.": "Источники недоступны.", + "No sources found": "Источники не найдены", + "No synchronizations found": "Синхронизации не найдены", + "No templates found": "Шаблоны не найдены", + "No templates have been created yet": "Шаблоны ещё не созданы", + "No tools available. Tools can be registered by installed apps.": "Инструменты недоступны. Инструменты могут быть зарегистрированы установленными приложениями.", + "No users in this organisation.": "В этой организации нет карыстальнікаў.", + "No valid magic-mapped register+schema combinations found": "Допустимые комбинации рэестр+схема с magic mapping не найдены", + "No views found. Create views first before configuring vectorization.": "Представления не найдены. Сначала создайте представления перед настройкой векторизации.", + "No views match your search": "Представления не соответствуют вашему поиску", + "No webhooks found": "Веб-хуки не найдены", + "No webhooks have been configured yet": "Веб-хуки ещё не настроены", + "No workflows found in this project. Create workflows in the n8n editor.": "В этом проекте рабочие процессы не найдены. Создайте рабочие процессы в редакторе n8n.", + "None": "Нет", + "Not Found": "Не найдено", + "Not extracted": "Не извлечено", + "Not helpful": "Бесполезно", + "Not in use": "Не используется", + "Note:": "Примечание:", + "Notifications": "Уведомления", + "Number Constraints": "Числовые ограничения", + "Number of Sources": "Количество источников", + "Number of chunks to vectorize in one API call (1-100).": "Количество фрагментов для векторизации в одном API-вызове (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Количество фрагментов для векторизации в одном API-вызове. Больше = быстрее, но больше памяти. Рекомендуется: 10-50.", + "Number of objects to process in each batch (1-5000).": "Количество аб'ектаў для обработки в каждом пакете (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Количество аб'ектаў для векторизации в одном API-вызове. Больше = быстрее, но больше памяти. Рекомендуется: 10-50.", + "Numeric Range:": "Числовой диапазон:", + "Object": "аб'ект", + "Object #{id}": "аб'ект #{id}", + "Object Collection": "Коллекция аб'ектаў", + "Object Configuration:": "канфігурацыя аб'екта:", + "Object Count Prediction": "Прогноз количества аб'ектаў", + "Object Extraction Mode": "Режим извлечения аб'ектаў", + "Object ID": "ID аб'екта", + "Object Management": "Управление объектами", + "Object Text Extraction": "Извлечение текста из аб'ектаў", + "Object Vectorization": "Векторизация аб'ектаў", + "Object Warmup": "Прогрев аб'ектаў", + "Object changes in owned objects": "Изменения аб'ектаў в принадлежащих объектах", + "Object collection not configured": "Коллекция аб'ектаў не настроена", + "Object metadata incomplete for file upload": "Метаданные аб'екта неполны для загрузки файла", + "Object not found": "аб'ект не найден", + "Object permanently deleted": "аб'ект безвозвратно выдалены", + "Object restored successfully": "аб'ект паспяхова восстановлен", + "Object settings updated successfully": "налады аб'екта паспяхова обновлены", + "Object sources": "Источники аб'ектаў", + "Object successfully deleted": "аб'ект паспяхова выдалены", + "Object successfully modified": "аб'ект паспяхова изменён", + "Object unlocked successfully": "аб'ект паспяхова разблокирован", + "Object vectorization configuration saved successfully": "канфігурацыя векторизации аб'ектаў паспяхова сохранена", + "Object vectorized successfully": "аб'ект паспяхова векторизован", + "Object:": "аб'ект:", + "Objects": "аб'екты", + "Objects Analyzed": "Проанализированных аб'ектаў", + "Objects Distribution": "Распределение аб'ектаў", + "Objects being analyzed": "Анализируемые аб'екты", + "Objects by Register": "аб'екты по реестрам", + "Objects by Schema": "аб'екты по схемам", + "Objects deletion completed for {schema}": "выдаленне аб'ектаў для {schema} завершено", + "Objects deletion completed successfully": "выдаленне аб'ектаў паспяхова завершено", + "Objects in schema": "аб'екты в схеме", + "Objects successfully deleted": "аб'екты паспяхова удалены", + "Objects to Process:": "аб'ектаў для обработки:", + "Objects to be analyzed": "аб'екты для анализа", + "Objects to be deleted": "аб'екты для удаления", + "Objects to be validated": "аб'екты для проверки", + "Objects will be serialized as JSON text before vectorization": "аб'екты будут сериализованы в формат JSON перед векторизацией", + "Objects will be soft deleted and moved to the": "аб'екты будут программно удалены и перемещены в", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "аб'екты будут программно удалены (помечены как удалённые, но сохранены в базе данных). При необходимости их можно восстановить позже.", + "Old Value": "Старое значэнне", + "Oldest Item (days)": "Старейший элемент (дней)", + "Ollama Chat Settings": "налады чата Ollama", + "Ollama Embedding Configuration": "канфігурацыя векторных представлений Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Только с изменениями", + "Only members of selected groups can access this organisation": "Только члены выбранных групп могут получить доступ к этой организации", + "Open Nextcloud App Store": "адкрыць магазин приложений Nextcloud", + "Open OpenRegister sidebar": "адкрыць боковую панель OpenRegister", + "Open Register Objects": "адкрыць аб'екты реестра", + "Open folder": "адкрыць папку", + "Open in OpenRegister": "адкрыць в OpenRegister", + "Open n8n Editor": "адкрыць редактор n8n", + "OpenAI Chat Settings": "налады чата OpenAI", + "OpenAI Embedding Configuration": "канфігурацыя векторных представлений OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "налады OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Операторские дашборды и запланированные отчёты. Каждый дашборд является первоклассным объектом в реестре `reports`; виджеты объявляются в массиве `widgets` дашборда и отображаются в реальном времени из агрегаций / GraphQL.", + "Optimizing search performance...": "Оптимизация производительности поиска...", + "Optional URL-friendly identifier": "Необязательный удобный для URL идентификатор", + "Optional description for this facet": "Необязательное апісанне для этого фасета", + "Optional end date property": "Необязательное свойство даты окончания", + "Optional event description template": "Необязательный шаблон описания события", + "Optional location property": "Необязательное свойство расположения", + "Optional webhook secret for signature verification": "Необязательный секрет веб-хука для проверки подписи", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Необязательно: добавьте параметры запроса для фильтрации ссылочной схемы (например, status=active&type=public)", + "Or": "Или", + "Order": "Порядок", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Упорядоченные теги языка BCP 47. Первый язык является языком реестра по умолчанию и управляет резервным вариантом Accept-Language для переводимых свойств.", + "Organisation": "Организация", + "Organisation Configuration": "канфігурацыя организации", + "Organisation ID": "ID организации", + "Organisation Members": "Участники организации", + "Organisation Statistics": "Статистика организации", + "Organisation created successfully": "Организация паспяхова создана", + "Organisation membership changes": "Изменения членства в организации", + "Organisation name is required": "назва организации обязательно", + "Organisation settings saved successfully": "налады организации паспяхова сохранены", + "Organisation successfully created": "Организация паспяхова создана", + "Organisation successfully updated": "Организация паспяхова обновлена", + "Organisational measures": "Организационные меры", + "Organisations": "Организации", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Организация", + "Organization ID (Optional)": "ID организации (необязательно)", + "Organizations": "Организации", + "Orphaned Items": "Осиротевшие элементы", + "Other": "Другое", + "Output Mapping": "Выходной маппинг", + "Overview": "Обзор", + "Owner": "Владелец", + "Owner:": "Владелец:", + "PDF": "PDF", + "PII hits": "Попадания PII", + "PII rows": "Строки PII", + "Page {current} of {total}": "Страница {current} из {total}", + "Parallel Mode (Faster, more resource intensive)": "Параллельный режим (быстрее, но более ресурсоёмкий)", + "Parallel:": "Параллельно:", + "Password": "Пароль", + "Password changes are not supported by your authentication provider.": "Ваш провайдер аутентификации не поддерживает изменение пароля.", + "Password for SOLR authentication (optional)": "Пароль для аутентификации SOLR (необязательно)", + "Password updated successfully": "Пароль паспяхова обновлён", + "Path": "Путь", + "Path in repository": "Путь в репозитории", + "Path where the configuration file will be saved in the repository": "Путь сохранения файла конфигурации в репозитории", + "Path where the register OAS file will be saved in the repository": "Путь сохранения файла OAS реестра в репозитории", + "Pattern (regex)": "Шаблон (regex)", + "Pattern Issue": "Проблема шаблона", + "Pattern constraint is missing": "Ограничение шаблона отсутствует", + "Patterns:": "Шаблоны:", + "Pending": "Ожидает", + "Permanent Delete Mode": "Режим безвозвратного удаления", + "Permanently Delete": "выдаліць навсегда", + "Permanently Delete ({count})": "выдаліць навсегда ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Безвозвратно выдаліць все {count} программно удалённых аб'екта. Это действие нельзя адмяніць!", + "Permanently delete already soft-deleted objects": "Безвозвратно выдаліць уже программно удалённые аб'екты", + "Permanently delete this conversation?": "Безвозвратно выдаліць эту переписку?", + "Permission Matrix": "Матрица разрешений", + "Person": "Персона", + "Personal Data": "Персональные дадзеныя", + "Personal Data Export": "экспарт персональных данных", + "Persons": "Персоны", + "Phone": "Телефон", + "Phone Numbers": "Номера телефонов", + "Photos": "Фотографии", + "Please create an agent in the": "Пожалуйста, создайте агента в", + "Please select a user": "Пожалуйста, выберите карыстальніка", + "Please select an agent to continue": "Пожалуйста, выберите агента для продолжения", + "Please select an organisation": "Пожалуйста, выберите организацию", + "Please select at least one item to import": "Пожалуйста, выберите хотя бы один элемент для импорта", + "Please select registers or schemas first": "Пожалуйста, сначала выберите реестры или схемы", + "Please select which register and schema to use for the new object": "Пожалуйста, выберите рэестр и схему для нового аб'екта", + "Please try again later.": "Пожалуйста, повторите попытку позже.", + "Please wait while we fetch your agents.": "Пожалуйста, подождите, пока загружаются ваши агенты.", + "Please wait while we fetch your applications.": "Пожалуйста, подождите, пока загружаются ваши приложения.", + "Please wait while we fetch your configurations.": "Пожалуйста, подождите, пока загружаются ваши конфигурации.", + "Please wait while we fetch your deleted items.": "Пожалуйста, подождите, пока загружаются ваши удалённые элементы.", + "Please wait while we fetch your sources.": "Пожалуйста, подождите, пока загружаются ваши источники.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Пожалуйста, подождите, пока коллекция SOLR безвозвратно удаляется. Это может занять некоторое время.", + "Polls": "Опросы", + "Popular Search Terms": "Популярные поисковые запросы", + "Port": "Порт", + "Portabiliteit (Art 20)": "Portabiliteit (Ст. 20)", + "Preferences saved": "Предпочтения сохранены", + "Prerequisites": "Предварительные условия", + "Press Enter to send, Shift+Enter for new line": "Нажмите Enter для отправки, Shift+Enter для новой строки", + "Press enter to send, shift+enter for new line": "Нажмите Enter для отправки, Shift+Enter для новой строки", + "Prettify": "Форматировать", + "Preview erasure": "Предпросмотр стирания", + "Preview:": "Предпросмотр:", + "Previous": "Предыдущий", + "Private": "Частный", + "Private Agent (Default)": "Частный агент (по умолчанию)", + "Private agents": "Частные агенты", + "Process Name (optional)": "назва процесса (необязательно)", + "Processed {indexed} files, {failed} failed": "Обработано {indexed} файлов, {failed} не удалось", + "Processes chunks in batches with simulated parallelism.": "Обрабатывает фрагменты пакетами с симулированным параллелизмом.", + "Processes file chunks sequentially (safest).": "Обрабатывает фрагменты файлов последовательно (самый безопасный режим).", + "Processes objects in chunks with simulated parallelism.": "Обрабатывает аб'екты фрагментами с симулированным параллелизмом.", + "Processes objects sequentially (safest).": "Обрабатывает аб'екты последовательно (самый безопасный режим).", + "Processing": "Обработка", + "Processing Limits": "Ограничения обработки", + "Processing...": "Обработка...", + "Project initialized successfully": "Проект паспяхова инициализирован", + "Project not found. Please initialize first.": "Проект не найден. Пожалуйста, сначала инициализируйте.", + "Properties": "Свойства", + "Property Behaviors": "Поведение свойств", + "Property Configuration:": "канфігурацыя свойства:", + "Property Filters": "Фильтры свойств", + "Property Title": "Заголовок свойства", + "Property Type": "Тип свойства", + "Property can be improved": "Свойство можно улучшить", + "Property could not be added": "Свойство не удалось дадаць", + "Property could not be updated": "Свойство не удалось абнавіць", + "Property name of inversed relation": "Имя свойства обратной связи", + "Property successfully added": "Свойство паспяхова добавлено", + "Property successfully updated": "Свойство паспяхова обновлено", + "Protected": "Защищённый", + "Provider": "Провайдер", + "Provider is required for testing": "Для тестирования необходим провайдер", + "Public": "Публичный", + "Public View": "Публичное представление", + "Public views can be accessed by anyone in the system": "Публичные представления доступны всем пользователям системы", + "Publish": "Опубликовать", + "Publish Configuration to GitHub": "Опубликовать конфигурацию на GitHub", + "Publish OAS": "Опубликовать OAS", + "Publish Objects": "Опубликовать аб'екты", + "Publish Register OAS to GitHub": "Опубликовать OAS реестра на GitHub", + "Publish all objects in this schema": "Опубликовать все аб'екты в этой схеме", + "Publish objects to make them publicly available": "Опубликовать аб'екты для открытого доступа", + "Published": "Опубликовано", + "Published:": "Опубликовано:", + "Publishing...": "Публикация...", + "Purge": "Очистить", + "Purge Date": "Дата очистки", + "Purge {count} object from database": "Очистить {count} аб'ект из базы данных", + "Purge {count} objects from database": "Очистить {count} аб'ектаў из базы данных", + "Purpose limitation *": "Ограничение цели *", + "Push Notifications": "Push-уведомления", + "Query Complexity Distribution": "Распределение сложности запросов", + "Query Help": "Справка по запросам", + "Query parameter is required": "Параметр запроса обязателен", + "Quota": "Квота", + "RAG": "RAG", + "RAG Configuration": "канфігурацыя RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "налады RBAC паспяхова обновлены", + "Rate limits cleared successfully": "Ограничения частоты запросов паспяхова очищены", + "Raw Changes Data": "Необработанные дадзеныя изменений", + "Re-vectorize on object update": "Повторно векторизовать при обновлении аб'екта", + "Read": "Чтение", + "Real-time push notification status via notify_push": "Статус push-уведомлений в реальном времени через notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push-уведомления в реальном времени активны. Подключённые клиенты получают мгновенные обновления при создании, обновлении или удалении аб'ектаў.", + "Realtime push active": "Push в реальном времени активен", + "Realtime push not available — the notify_push app is not installed": "Push в реальном времени недоступен — приложение notify_push не установлено", + "Reason": "Причина", + "Reason (optional)": "Причина (необязательно)", + "Rebase operation completed successfully": "Операция перебазирования паспяхова завершена", + "Rebase operation failed: {error}": "Операция перебазирования не удалась: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Рекомендации:", + "Recommended Type:": "Рекомендуемый тип:", + "Recommended: 5 sources": "Рекомендуется: 5 источников", + "Refresh": "абнавіць", + "Refresh Data": "абнавіць дадзеныя", + "Refresh Stats": "абнавіць статистику", + "Refresh Workflows": "абнавіць рабочие процессы", + "Refresh dashboard": "абнавіць дашборд", + "Refresh database info": "абнавіць информацию о базе данных", + "Register": "рэестр", + "Register #{id}": "рэестр #{id}", + "Register / Schema": "рэестр / Схема", + "Register ID": "ID реестра", + "Register Objects": "аб'екты реестра", + "Register Overview": "Обзор реестра", + "Register Statistics": "Статистика реестра", + "Register Totals": "Итоги реестра", + "Register and schema are required": "рэестр и схема обязательны", + "Register annotation": "Аннотация реестра", + "Register depublished successfully": "рэестр паспяхова снят с публикации", + "Register not found": "рэестр не найден", + "Register objects": "аб'екты реестра", + "Register objects deletion completed successfully": "выдаленне аб'ектаў реестра паспяхова завершено", + "Register published successfully": "рэестр паспяхова опубликован", + "Register/Schema Usage": "Использование реестра/схемы", + "Register:": "рэестр:", + "Registers": "Реестры", + "Reindex": "Переиндексировать", + "Reindex completed": "Переиндексация завершена", + "Reindex completed successfully": "Переиндексация паспяхова завершена", + "Reindex completed! Processed {count} objects in {duration}s": "Переиндексация завершена! Обработано {count} аб'ектаў за {duration}с", + "Reindex failed": "Переиндексация не удалась", + "Reindex failed: %s": "Переиндексация не удалась: %s", + "Reject": "Отклонить", + "Rejected": "Отклонено", + "Related Cases": "Связанные дела", + "Relations": "Связи", + "Remote Version": "Удалённая версия", + "Remove": "выдаліць", + "Remove Deck card": "выдаліць карточку Deck", + "Remove avatar": "выдаліць аватар", + "Remove contact": "выдаліць контакт", + "Remove filter": "выдаліць фильтр", + "Remove from favorites": "выдаліць из избранного", + "Remove from list": "выдаліць из списка", + "Remove group": "выдаліць группу", + "Remove link": "выдаліць ссылку", + "Remove link to {name}": "выдаліць ссылку на {name}", + "Remove link to {name}?": "выдаліць ссылку на {name}?", + "Remove link to {title}": "выдаліць ссылку на {title}", + "Remove oneOf entry": "выдаліць запись oneOf", + "Remove property after write-back": "выдаліць свойство после обратной записи", + "Remove {title}": "выдаліць {title}", + "Remove {userId} from this organisation?": "выдаліць {userId} из этой организации?", + "Removed": "Удалено", + "Removed from favorites": "Удалено из избранного", + "Removing schema {schema}...": "выдаленне схемы {schema}...", + "Rename Conversation": "Переименовать переписку", + "Rename conversation": "Переименовать переписку", + "Replicas": "Реплики", + "Reports": "Отчёты", + "Repository": "Репозиторий", + "Request Body": "Тело запроса", + "Request Data": "дадзеныя запроса", + "Request Details": "Детали запроса", + "Request Quota (per day)": "Квота запросов (в день)", + "Request account deactivation": "Запросить деактивацию аккаунта", + "Request account deactivation. This will notify administrators for review.": "Запросить деактивацию аккаунта. Это уведомит администраторов для проверки.", + "Request body must be JSON with a \"query\" field": "Тело запроса должно быть JSON с полем \"query\"", + "Request timeout in seconds": "Тайм-аут запроса в секундах", + "Requested at": "Запрошено в", + "Required": "Обязательно", + "Required Token Scopes": "Обязательные области токена", + "Required field": "Обязательное поле", + "Required status is inconsistent": "Статус обязательности несогласован", + "Requires Dolphin AI for OCR text extraction": "Требуется Dolphin AI для извлечения текста OCR", + "Rerun Search": "Повторить пошук", + "Rerunning search: {searchTerm}": "Повтор поиска: {searchTerm}", + "Reset Changes": "Сбросить изменения", + "Reset Filters": "Сбросить фильтры", + "Resource Quotas": "Квоты ресурсов", + "Response Body": "Тело ответа", + "Response Details": "Детали ответа", + "Response Time:": "Время ответа:", + "Restore": "Восстановить", + "Restore conversation": "Восстановить переписку", + "Restore or permanently delete items": "Восстановить или безвозвратно выдаліць элементы", + "Restore {count} object": "Восстановить {count} аб'ект", + "Restore {count} objects": "Восстановить {count} аб'ектаў", + "Result Count Range": "Диапазон количества результатов", + "Results": "Результаты", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Период хранения (длительность ISO-8601, например P10Y, P30D)", + "Retention settings updated successfully": "налады хранения паспяхова обновлены", + "Retrieve all objects for this schema": "Получить все аб'екты для этой схемы", + "Retry": "Повторить", + "Retry Failed Extractions": "Повторить неудачные извлечения", + "Retry Policy": "Политика повтора", + "Retry completed": "Повтор завершён", + "Retry upload": "Повторить загрузку", + "Retrying...": "Повтор...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Просмотрите выбранные аб'екты ниже. Вы можете убрать любые аб'екты, которые не хотите удалять, нажав кнопку удаления.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Просмотрите выбранные аб'екты ниже. Вы можете убрать любые аб'екты, которые не хотите включать.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Просмотрите выбранные аб'екты ниже. Вы можете убрать любые аб'екты, которые не хотите безвозвратно удалять, нажав кнопку удаления. Это действие нельзя адмяніць.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Просмотрите выбранные аб'екты ниже. Вы можете убрать любые аб'екты, которые не хотите восстанавливать, нажав кнопку удаления. аб'екты будут восстановлены в исходное расположение.", + "Revoke": "Отозвать", + "Right": "Право", + "Risk Level": "Уровень риска", + "Risk level": "Уровень риска", + "Roles and Permissions": "Роли и разрешения", + "Roles updated successfully": "Роли паспяхова обновлены", + "Run compliance scan": "Запустить проверку соответствия", + "SOLR Connection Settings": "налады подключения SOLR", + "SOLR Index Warmup": "Прогрев индекса SOLR", + "SOLR Version:": "Версия SOLR:", + "SOLR actions menu": "Меню действий SOLR", + "SOLR base path (usually /solr)": "Базовый путь SOLR (обычно /solr)", + "SOLR collection deleted successfully": "Коллекция SOLR паспяхова удалена", + "SOLR connection test failed: {error}": "Тест подключения SOLR не удался: {error}", + "SOLR connection test successful!": "Тест подключения SOLR успешен!", + "SOLR index warmup completed successfully": "Прогрев индекса SOLR паспяхова завершён", + "SOLR is disabled": "SOLR отключён", + "SOLR is not available or not configured": "SOLR недоступен или не настроен", + "SOLR logging disabled": "Логирование SOLR отключено", + "SOLR logging enabled": "Логирование SOLR включено", + "SOLR search disabled": "пошук SOLR отключён", + "SOLR search enabled": "пошук SOLR включён", + "SOLR server hostname or IP address": "Имя хоста или IP-адрес сервера SOLR", + "SOLR server port number (optional, defaults to 8983)": "Номер порта сервера SOLR (необязательно, по умолчанию 8983)", + "SOLR settings updated successfully": "налады SOLR паспяхова обновлены", + "SOLR setup completed successfully": "налада SOLR паспяхова завершена", + "SOLR setup completed successfully!": "налада SOLR паспяхова завершена!", + "SOLR setup error: %s": "памылка налады SOLR: %s", + "SOLR setup failed": "налада SOLR не удалась", + "SOLR setup failed - check logs": "налада SOLR не удалась — проверьте журналы", + "SOLR setup failed. Please check the configuration and try again.": "налада SOLR не удалась. Проверьте конфигурацию и повторите попытку.", + "SOLR setup failed: {error}": "налада SOLR не удалась: {error}", + "SOLR warmup failed: {error}": "Прогрев SOLR не удался: {error}", + "Sample Values:": "Примеры значений:", + "Save": "захаваць", + "Save Configuration": "захаваць конфигурацию", + "Save Connection Settings": "захаваць налады подключения", + "Save Roles": "захаваць роли", + "Save Settings": "захаваць налады", + "Save changes": "захаваць изменения", + "Save current search as view": "захаваць текущий пошук как представление", + "Save labels": "захаваць метки", + "Save tags for {name}": "захаваць теги для {name}", + "Save the organisation first to manage users.": "Сначала сохраните организацию для управления пользователями.", + "Saved Views": "Сохранённые представления", + "Saving...": "захаванне...", + "Scheduled": "Запланировано", + "Schema": "Схема", + "Schema ID": "ID схемы", + "Schema Name": "назва схемы", + "Schema Statistics": "Статистика схемы", + "Schema annotation": "Аннотация схемы", + "Schema configuration updated successfully for {schema}": "канфігурацыя схемы для {schema} паспяхова обновлена", + "Schema depublished successfully": "Схема паспяхова снята с публикации", + "Schema objects deletion completed successfully": "выдаленне аб'ектаў схемы паспяхова завершено", + "Schema published successfully": "Схема паспяхова опубликована", + "Schema reference ($ref)": "Ссылка на схему ($ref)", + "Schema successfully created": "Схема паспяхова создана", + "Schema successfully updated": "Схема паспяхова обновлена", + "Schema {schema} removed successfully": "Схема {schema} паспяхова удалена", + "Schema:": "Схема:", + "Schemas": "Схемы", + "Scheme": "Схема", + "Search": "пошук", + "Search / Views": "пошук / Представления", + "Search / views": "пошук / представления", + "Search Activity": "Поисковая активность", + "Search Analytics": "Аналитика поиска", + "Search Configuration": "канфігурацыя поиска", + "Search Entities": "пошук аб'ектаў", + "Search Files": "пошук файлов", + "Search GitHub": "пошук на GitHub", + "Search GitLab": "пошук на GitLab", + "Search Method": "Метод поиска", + "Search Mode": "Режим поиска", + "Search Query": "Поисковый запрос", + "Search Results": "Результаты поиска", + "Search Term": "Поисковый запрос", + "Search Trail Details": "Детали журнала поиска", + "Search Trail Management": "Управление журналом поиска", + "Search Trail Statistics": "Статистика журнала поиска", + "Search Trails": "Журналы поиска", + "Search Views": "пошук представлений", + "Search Webhooks": "пошук веб-хуков", + "Search agents...": "пошук агентов...", + "Search applications...": "пошук приложений...", + "Search by file name or path": "пошук по имени файла или пути", + "Search by name or URL": "пошук по имени или URL", + "Search by title or UUID...": "пошук по заголовку или UUID...", + "Search by value": "пошук по значению", + "Search configurations": "пошук конфигураций", + "Search data sources...": "пошук источников данных...", + "Search groups...": "пошук групп...", + "Search in Database Objects": "пошук в объектах базы данных", + "Search in Files": "пошук в файлах", + "Search in Objects": "пошук в объектах", + "Search objects": "пошук аб'ектаў", + "Search objects...": "пошук аб'ектаў...", + "Search property names...": "пошук имён свойств...", + "Search registers...": "пошук реестров...", + "Search schemas...": "пошук схем...", + "Search trail deletion not implemented yet": "выдаленне журнала поиска ещё не реализовано", + "Search views...": "пошук представлений...", + "Search {name}...": "пошук {name}...", + "Searching...": "пошук...", + "Secret": "Секрет", + "Secret key for HMAC signature generation (optional)": "Секретный ключ для генерации подписи HMAC (необязательно)", + "Security": "Безопасность", + "See organisations as a table": "Просматривать организации в виде таблицы", + "See organisations as cards": "Просматривать организации в виде карточек", + "Select AI Agent": "выбраць ИИ-агента", + "Select Active Organisation": "выбраць активную организацию", + "Select All": "выбраць всё", + "Select ConfigSet": "выбраць набор конфигурации", + "Select File Types to Vectorize:": "выбраць типы файлов для векторизации:", + "Select HTTP method": "выбраць метод HTTP", + "Select Register and Schema": "выбраць рэестр и схему", + "Select Views to Vectorize:": "выбраць представления для векторизации:", + "Select a Nextcloud group to add": "выбраць группу Nextcloud для добавления", + "Select a branch": "выбраць ветку", + "Select a date property": "выбраць свойство даты", + "Select a model or type a custom model name": "Выберите модель или введите пользовательское имя модели", + "Select a register": "выбраць рэестр", + "Select a register...": "выбраць рэестр...", + "Select a repository": "выбраць репозиторий", + "Select a repository you have write access to": "Выберите репозиторий, к которому у вас есть доступ на запись", + "Select a schema": "выбраць схему", + "Select a schema...": "выбраць схему...", + "Select a user": "выбраць карыстальніка", + "Select agent type": "выбраць тип агента", + "Select all entities": "выбраць все аб'екты", + "Select an AI Agent": "выбраць ИИ-агента", + "Select an AI agent to begin chatting with your data.": "Выберите ИИ-агента для начала общения с вашими данными.", + "Select an email to see linked objects": "Выберите письмо для просмотра связанных аб'ектаў", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Выберите организацию и карыстальніка для добавления в качестве участника. пошук организаций по имени.", + "Select backend": "выбраць backend", + "Select chat model": "выбраць модель чата", + "Select collection for files": "выбраць коллекцию для файлов", + "Select collection for objects": "выбраць коллекцию для аб'ектаў", + "Select configuration type...": "выбраць тип конфигурации...", + "Select default organisation": "выбраць организацию по умолчанию", + "Select entity {value}": "выбраць аб'ект {value}", + "Select event to listen to...": "выбраць событие для прослушивания...", + "Select fields to display": "выбраць поля для отображения", + "Select files with the correct extension": "выбраць файлы с правильным расширением", + "Select group": "выбраць группу", + "Select groups (optional)": "выбраць группы (необязательно)", + "Select groups to notify...": "выбраць группы для уведомления...", + "Select groups with access to this agent": "выбраць группы с доступом к этому агенту", + "Select groups...": "выбраць группы...", + "Select model": "выбраць модель", + "Select one or more registers": "выбраць один или несколько реестров", + "Select one or more schemas": "выбраць одну или несколько схем", + "Select options...": "выбраць параметры...", + "Select or create labels, or select \"No label\" to add files": "Выберите или создайте метки, или выберите «Без метки», чтобы дадаць файлы", + "Select owner application (optional)...": "выбраць приложение-владелец (необязательно)...", + "Select period": "выбраць период", + "Select property to send as payload": "выбраць свойство для отправки в качестве полезной нагрузки", + "Select provider": "выбраць провайдера", + "Select registers and schemas to save a view": "Выберите реестры и схемы для сохранения представления", + "Select retry policy": "выбраць политику повтора", + "Select role": "выбраць роль", + "Select schemas (empty = all schemas)": "выбраць схемы (пусто = все схемы)", + "Select schemas to warm up": "выбраць схемы для прогрева", + "Select search mode": "выбраць режим поиска", + "Select source type...": "выбраць тип источника...", + "Select target type": "выбраць тип цели", + "Select the branch to publish to": "выбраць ветку для публикации", + "Select the event this webhook should listen to": "выбраць событие, которое должен прослушивать этот веб-хук", + "Select views to filter data (optional)": "выбраць представления для фильтрации данных (необязательно)", + "Select views to limit which data the agent can access": "выбраць представления для ограничения доступных агенту данных", + "Select views to vectorize:": "выбраць представления для векторизации:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Выберите группы Nextcloud, доступные для этой организации. карыстальнікі в этих группах получат доступ к ресурсам организации.", + "Select which data views the AI can search": "выбраць представления данных, которые ИИ может искать", + "Select which property from the event should be used as the webhook payload data": "выбраць свойство события для использования в качестве данных полезной нагрузки веб-хука", + "Select which tools the AI can use to perform actions": "выбраць инструменты, которые ИИ может использовать для выполнения действий", + "Select {fieldName} values": "выбраць значения {fieldName}", + "Selected": "Выбрано", + "Selected Groups": "Выбранные группы", + "Selected Objects ({count})": "Выбранные аб'екты ({count})", + "Selected audit trails deleted successfully": "Выбранные записи журнала аудита паспяхова удалены", + "Selected search trails deleted successfully": "Выбранные записи журнала поиска паспяхова удалены", + "Selected users can access this view": "Выбранные карыстальнікі могут получить доступ к этому представлению", + "Send additional feedback": "Отправить дополнительный отзыв", + "Send as CloudEvent": "Отправить как CloudEvent", + "Send as cloudevent": "Отправить как CloudEvent", + "Sensitive PII": "Конфиденциальные персональные дадзеныя", + "Serial Mode (Safer, slower)": "Последовательный режим (безопаснее, медленнее)", + "Serial:": "Последовательно:", + "Server Configuration": "канфігурацыя сервера", + "Server Information": "Информация о сервере", + "Set as Active": "Установить как активный", + "Set as active organisation": "Установить как активную организацию", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Установите ограничения для использования API и потребления токенов. Используйте 0 для неограниченных ресурсов.", + "Settings": "налады", + "Settings updated successfully": "налады паспяхова обновлены", + "Shards": "Шарды", + "Share": "Поделиться", + "Share with Groups": "Поделиться с группами", + "Share with Users": "Поделиться с пользователями", + "Show": "Показать", + "Show Filters": "Показать фильтры", + "Show less": "Показать меньше", + "Show only entries with changes": "Показывать только записи с изменениями", + "Showing {count} dashboard(s)": "Показано {count} дашборд(ов)", + "Showing {count} processing activities": "Показано {count} операций обработки", + "Showing {showing} of {total} agents": "Показано {showing} из {total} агентов", + "Showing {showing} of {total} applications": "Показано {showing} из {total} приложений", + "Showing {showing} of {total} audit trail entries": "Показано {showing} из {total} записей журнала аудита", + "Showing {showing} of {total} configurations": "Показано {showing} из {total} конфигураций", + "Showing {showing} of {total} deleted items": "Показано {showing} из {total} удалённых элементов", + "Showing {showing} of {total} entities": "Показано {showing} из {total} аб'ектаў", + "Showing {showing} of {total} files": "Показано {showing} из {total} файлов", + "Showing {showing} of {total} log entries": "Показано {showing} из {total} записей журнала", + "Showing {showing} of {total} search trail entries": "Показано {showing} из {total} записей журнала поиска", + "Showing {showing} of {total} sources": "Показано {showing} из {total} источников", + "Showing {showing} of {total} templates": "Показано {showing} из {total} шаблонов", + "Showing {showing} of {total} webhooks": "Показано {showing} из {total} веб-хуков", + "Showing {shown} of {total} organisations": "Показано {shown} из {total} организаций", + "Simple": "Простой", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Простые запросы: базовый текстовый пошук с минимальными параметрами (например, один поисковый запрос без расширенных фильтров)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Простые запросы: базовый текстовый пошук с минимальными параметрами (например, один поисковый запрос без расширенных фильтров)", + "Size": "Размер", + "Slug": "Slug", + "Soft Delete Mode": "Режим программного удаления", + "Soft Deleted Items": "Программно удалённые элементы", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Программно выдаліць все аб'екты этой схемы ({active} активных, {deleted} уже удалённых)", + "SolrCloud mode enabled": "Режим SolrCloud включён", + "Something went wrong while adding files": "При добавлении файлов что-то пошло не так", + "Source": "Источник", + "Source successfully updated": "Источник паспяхова обновлён", + "Sources": "Источники", + "Sources:": "Источники:", + "Special Rights": "Особые права", + "Standalone SOLR mode": "Автономный режим SOLR", + "Start Conversation": "Начать переписку", + "Start Date Field": "Поле даты начала", + "Start Vectorization": "Начать векторизацию", + "Start a conversation": "Начать переписку", + "Starting names cache warmup...": "Начало прогрева кэша имён...", + "Starting validation for {schema}...": "Начало проверки {schema}...", + "Starting {action} for {schema}...": "Начало {action} для {schema}...", + "Starting...": "Запуск...", + "Statistics": "Статистика", + "Statistics and Metrics": "Статистика и метрики", + "Statistics refreshed": "Статистика обновлена", + "Stats calculation not yet implemented": "Расчёт статистики ещё не реализован", + "Status": "Статус", + "Status Code": "Код статуса", + "Step": "Шаг", + "Storage": "Хранилище", + "Storage Quota (MB)": "Квота хранилища (МБ)", + "String Constraints": "Строковые ограничения", + "Subject identifier (email, BSN, name, etc.)": "Идентификатор субъекта (email, BSN, имя и т.д.)", + "Subscribe": "Подписаться", + "Subscribed": "Подписан", + "Success": "Успех", + "Success Rate": "Процент успеха", + "Success Status": "Статус успеха", + "Successful": "паспяхова", + "Successfully added user to organisation: {name}": "карыстальнік паспяхова добавлен в организацию: {name}", + "Successfully cleared {count} audit trails": "паспяхова очищено {count} записей журнала аудита", + "Successfully cleared {count} blob storage objects": "паспяхова очищено {count} аб'ектаў blob-хранилища", + "Successfully cleared {count} search trails": "паспяхова очищено {count} записей журнала поиска", + "Successfully created {count} SOLR fields": "паспяхова создано {count} полей SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "паспяхова удалено {count} векторных представлений. Пожалуйста, повторно векторизуйте ваши дадзеныя.", + "Successfully deleted {count} objects for {schema}": "паспяхова удалено {count} аб'ектаў для {schema}", + "Successfully fixed {count} SOLR field configurations": "паспяхова исправлено {count} конфигураций полей SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "паспяхова импортировано: {registers} реестров, {schemas} схем, {objects} аб'ектаў", + "Successfully joined organisation": "Вы паспяхова присоединились к организации", + "Successfully permanently deleted {count} objects": "паспяхова безвозвратно удалено {count} аб'ектаў", + "Successfully processed {count} files!": "паспяхова обработано {count} файлов!", + "Successfully restored {count} object": "паспяхова восстановлен {count} аб'ект", + "Successfully restored {count} objects": "паспяхова восстановлено {count} аб'ектаў", + "Successfully saved configuration for {count} facets!": "канфігурацыя для {count} фасетов паспяхова сохранена!", + "Supported File Types": "Поддерживаемые типы файлов", + "Switch Active Organisation": "Переключить активную организацию", + "Switch Organisation": "Переключить организацию", + "Sync Table": "Синхронизировать таблицу", + "Synchronization Settings": "налады синхронизации", + "Syncing magic table for {schema}...": "сінхранізацыя magic table для {schema}...", + "Syncs": "Синхронизации", + "System Default": "Системное значэнне по умолчанию", + "System Prompt": "Системный промпт", + "System announcements": "Системные объявления", + "Table": "Таблица", + "Target ID": "ID цели", + "Target Type*": "Тип цели*", + "Technical Description": "Техническое апісанне", + "Technical Details": "Технические детали", + "Technical description for developers and administrators": "Техническое апісанне для разработчиков и администраторов", + "Technical measures": "Технические меры", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Температура", + "Templates": "Шаблоны", + "Test": "Тест", + "Test Chat": "Тест чата", + "Test Connection": "Проверить подключение", + "Test Embedding": "Тест векторных представлений", + "Test endpoint executed successfully": "Тест конечной точки выполнен паспяхова", + "Test webhook delivered successfully": "Тестовый веб-хук паспяхова доставлен", + "Test webhook delivery failed": "Доставка тестового веб-хука не удалась", + "Test webhook sent successfully": "Тестовый веб-хук паспяхова отправлен", + "Testing...": "Тестирование...", + "Text Chunk #{id}": "Текстовый фрагмент #{id}", + "Text Extraction": "Извлечение текста", + "Text Extraction Engine": "Движок извлечения текста", + "Text chunks": "Текстовые фрагменты", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Текстовые фрагменты создаются в процессе извлечения файлов и сохраняются в базе данных. Векторизация читает эти предварительно разбитые файлы и преобразует их в векторные представления.", + "Text extracted successfully": "Текст паспяхова извлечён", + "Text extraction disabled": "Извлечение текста отключено", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Приложение Nextcloud Календарь не установлено или не включено на этом сервере.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Приложение Nextcloud Контакты не установлено или не включено на этом сервере.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Приложение Nextcloud Deck не установлено или не включено на этом сервере.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Приложение Nextcloud Почта не установлено или не включено на этом сервере.", + "The URL where webhook events will be sent": "URL, на который будут отправляться события веб-хука", + "The requested conversation does not exist": "Запрошенная переписка не существует", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Запрошенный дашборд не удалось загрузіць. Проверьте URL или выберите другой дашборд из списка.", + "The saved view and all its search configuration": "Сохранённое представление и вся его канфігурацыя поиска", + "There are no audit trail entries matching your current filters.": "Нет записей журнала аудита, соответствующих вашим текущим фильтрам.", + "There are no deleted items matching your current filters.": "Нет удалённых элементов, соответствующих вашим текущим фильтрам.", + "There are no search trail entries matching your current filters.": "Нет записей журнала поиска, соответствующих вашим текущим фильтрам.", + "There are no webhook log entries matching your filters.": "Нет записей журнала веб-хука, соответствующих вашим фильтрам.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Они будут храниться в соответствии с настроенным периодом хранения схемы и автоматически безвозвратно удалены по истечении периода хранения. Период хранения настраивается для каждой схемы и находится в настройках схемы.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Они будут храниться в соответствии с настроенным периодом хранения схемы, а затем безвозвратно удалены.", + "This action cannot be undone.": "Это действие нельзя адмяніць.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Это действие нельзя адмяніць. Убедитесь, что ни одна коллекция не использует этот набор конфигурации.", + "This action cannot be undone. Make sure no collections are using this configset.": "Это действие нельзя адмяніць. Убедитесь, что ни одна коллекция не использует этот набор конфигурации.", + "This action will submit a deactivation request to your administrators.": "Это действие отправит запрос на деактивацию вашим администраторам.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "У этого агента нет настраиваемых представлений или инструментов, но вы можете изменить налады RAG ниже.", + "This analysis may take some time": "Этот анализ может занять некоторое время", + "This audit trail entry does not contain any change information.": "Эта запись журнала аудита не содержит информации об изменениях.", + "This configuration has no data defined.": "Для этой конфигурации не определены дадзеныя.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Эта конечная точка устарела. Вместо неё используйте конечные точки на основе фрагментов.", + "This entity has no relations to objects or files": "Этот аб'ект не имеет связей с объектами или файлами", + "This is a test webhook from OpenRegister": "Это тестовый веб-хук от OpenRegister", + "This is not valid JSON": "Это не является допустимым JSON", + "This key already exists on this schema": "Этот ключ уже существует в этой схеме", + "This schema must use Magic Table configuration to sync": "Эта схема должна использовать конфигурацию Magic Table для синхронизации", + "This schema must use magic table configuration to sync": "Эта схема должна использовать конфигурацию magic table для синхронизации", + "This source has no associated registers.": "Этот источник не имеет связанных реестров.", + "This token will only be shown once. Copy it now.": "Этот токен будет показан только один раз. Скопируйте его сейчас.", + "This will copy the _default ConfigSet with the new name": "Это скопирует набор конфигурации _default с новым именем", + "This will copy the _default configset with the new name": "Это скопирует набор конфигурации _default с новым именем", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Это безвозвратно удалит ВСЕ векторные представления из базы данных. Вам потребуется повторно векторизовать все аб'екты и файлы. Это действие нельзя адмяніць.\n\nВы уверены, что хотите продолжить?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Это безвозвратно удалит ВСЕ векторные представления из базы данных. Вам потребуется повторно векторизовать все аб'екты и файлы. Это действие нельзя адмяніць.\\n\\nВы уверены, что хотите продолжить?", + "This will permanently delete:": "Это безвозвратно удалит:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Это безвозвратно удалит {count} уже программно удалённых аб'ектаў из базы данных. Это действие нельзя адмяніць.", + "Timeout (seconds)": "Тайм-аут (секунды)", + "Timestamp": "Временная метка", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Совет: включайте только те представления, которым нужен семантический пошук, чтобы снизить затраты на векторные представления. Простые таблицы поиска редко нуждаются в векторизации.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Совет: включайте только те представления, которым нужен семантический пошук, чтобы снизить затраты на векторные представления. Простые таблицы поиска редко нуждаются в векторизации.", + "Title": "Заголовок", + "Title *": "Заголовок *", + "Title Template": "Шаблон заголовка", + "Title*": "Заголовок*", + "To (e.g. -1 year or 2025-12-31)": "По (например, -1 year или 2025-12-31)", + "To Date": "Дата окончания", + "To add files larger than or equal to 512MB, go to the": "Для добавления файлов размером 512 МБ и более перейдите в", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Для изменения размера фрагмента или стратегии перейдите в канфігурацыя файлов → Ограничения обработки.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Для изменения размера фрагмента или стратегии перейдите в канфігурацыя файлов → ограничения обработки.", + "To change the embedding provider or model, go to LLM Configuration.": "Для изменения провайдера или модели векторных представлений перейдите в налада LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Для изменения провайдера или модели векторных представлений перейдите в настройку LLM.", + "To date": "По дату", + "Toggle search sidebar": "Переключить боковую панель поиска", + "Token Created": "Токен створаны", + "Token Quota (per request)": "Квота токенов (на запрос)", + "Token copied to clipboard": "Токен скопирован в буфер обмена", + "Token name": "Имя токена", + "Token revoked": "Токен отозван", + "Tools": "Инструменты", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Инструменты выполняются с правами карыстальніка агента по умолчанию, когда пользовательский сеанс неактивен (например, задания cron). Настройте карыстальніка по умолчанию на вкладке налады.", + "Tools respect the agent's own role and organization boundaries.": "Инструменты соблюдают собственную роль агента и границы организации.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Инструменты соблюдают представления, разрешения и границы организации агента.", + "Top Deleters": "Наибольшее число удалений", + "Total": "Итого", + "Total Audit Trails": "Всего журналов аудита", + "Total Chunks Available:": "Доступно фрагментов:", + "Total Deleted Items": "Всего удалённых элементов", + "Total Members": "Всего участников", + "Total Objects": "Всего аб'ектаў", + "Total Objects in Database:": "Всего аб'ектаў в базе данных:", + "Total Organisations": "Всего организаций", + "Total Results": "Всего результатов", + "Total Searches": "Всего поисков", + "Total Size": "Общий размер", + "Total Storage": "Общее хранилище", + "Total events": "Всего событий", + "Total:": "Итого:", + "Totals": "Итоги", + "Try searching by UUID or with different keywords": "Попробуйте пошук по UUID или с другими ключевыми словами", + "Type": "Тип", + "Type \"DELETE COLLECTION\" to confirm:": "Введите «DELETE COLLECTION» для подтверждения:", + "Type Issue": "Проблема типа", + "Type Variations:": "Варианты типа:", + "Type filter (optional, e.g. email)": "Фильтр типа (необязательно, например email)", + "Type must be \"positive\" or \"negative\"": "Тип должен быть «positive» или «negative»", + "Type to search for groups": "Введите для поиска групп", + "Type to search for objects...": "Введите для поиска аб'ектаў...", + "Type to search for organisations": "Введите для поиска организаций", + "Type to search for users": "Введите для поиска карыстальнікаў", + "Type to search groups...": "Введите для поиска групп...", + "Type to search users...": "Введите для поиска карыстальнікаў...", + "Type to search...": "Введите для поиска...", + "Type your username to confirm": "Введите имя карыстальніка для подтверждения", + "URL": "URL", + "URL where Ollama is running": "URL, по которому работает Ollama", + "UUID copied to clipboard": "UUID скопирован в буфер обмена", + "UUID:": "UUID:", + "Unchanged": "Без изменений", + "Unique Search Terms": "Уникальные поисковые запросы", + "Unique Terms": "Уникальные термины", + "Unknown": "Неизвестно", + "Unknown Object": "Неизвестный аб'ект", + "Unknown Register": "Неизвестный рэестр", + "Unknown Schema": "Неизвестная схема", + "Unknown error": "Неизвестная памылка", + "Unknown widget type: {type}": "Неизвестный тип виджета: {type}", + "Unlink email": "Отвязать письмо", + "Unlink event": "Отвязать событие", + "Unnamed Object": "аб'ект без имени", + "Untitled": "Без названия", + "Untitled View": "Представление без названия", + "Update": "абнавіць", + "Update Operations": "Операции обновления", + "Update available: {local} → {remote}": "Доступно абнаўленне: {local} → {remote}", + "Update configuration: ...": "абнавіць конфигурацию: ...", + "Update register OAS: ...": "абнавіць OAS реестра: ...", + "Update vectors when object data changes (recommended for accurate search)": "Обновлять векторы при изменении данных аб'екта (рекомендуется для точного поиска)", + "Updated": "Обновлено", + "Updated At": "Обновлено в", + "Updated:": "Обновлено:", + "Upload new avatar": "загрузіць новый аватар", + "Uploading configuration sets...": "загрузка наборов конфигурации...", + "Uptime:": "Время работы:", + "Url": "Url", + "Usage Count": "Количество использований", + "Use AI agents for processing and analysis": "Использовать ИИ-агентов для обработки и анализа", + "Use Large Language Model features": "Использовать функции больших языковых моделей", + "Use SolrCloud with Zookeeper for distributed search": "Использовать SolrCloud с Zookeeper для распределённого поиска", + "Use SolrCloud with zookeeper for distributed search": "Использовать SolrCloud с Zookeeper для распределённого поиска", + "Use VNG APIs instead of MongoDB.": "Использовать VNG API вместо MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Использовать диапазоны по умолчанию (последние 7/30/90 дней, последний год, старее)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Использовать внешнее хранилище (например, MongoDB) вместо внутреннего хранилища Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Используйте фильтры для сужения записей журнала аудита по реестру, схеме, типу действия, пользователю, диапазону дат или ID аб'екта.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Используйте фильтры для сужения удалённых элементов по реестру, схеме, дате удаления или пользователю, который их удалил.", + "Use filters to narrow down entities by type or category.": "Используйте фильтры для сужения аб'ектаў по типу или категории.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Используйте фильтры для сужения записей журнала поиска по реестру, схеме, статусу успеха, пользователю, диапазону дат, поисковым запросам или метрикам производительности.", + "Used By Collections": "Используется коллекциями", + "Used by": "Используется", + "User": "карыстальнік", + "User Agent Statistics": "Статистика агентов карыстальнікаў", + "User Description": "апісанне карыстальніка", + "User Quota": "Квота карыстальніка", + "User rate limits cleared successfully": "Ограничения частоты запросов карыстальніка паспяхова очищены", + "User removed successfully": "карыстальнік паспяхова выдалены", + "User-friendly description shown in forms and help text": "Понятное апісанне, отображаемое в формах и текстах справки", + "Username": "Имя карыстальніка", + "Username for SOLR authentication (optional)": "Имя карыстальніка для аутентификации SOLR (необязательно)", + "Users": "карыстальнікі", + "Uses": "Использует", + "Using Pre-Generated Chunks": "Использование предварительно сгенерированных фрагментов", + "Validate": "Проверить", + "Validate Objects": "Проверить аб'екты", + "Validation completed for {schema}": "Проверка {schema} завершена", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Проверка {schema} завершена: обработано {processed}, обновлено {updated}, не удалось {failed}", + "Validation completed successfully": "Проверка паспяхова завершена", + "Validation failed": "Проверка не удалась", + "Value": "значэнне", + "Value list (split on ,)": "Список значений (разделять запятой)", + "Vector Dimensions": "Размерность вектора", + "Vector Search Backend": "Backend векторного поиска", + "Vector field: _embedding_": "Векторное поле: _embedding_", + "Vectorization Triggers": "Триггеры векторизации", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Векторизация завершена: векторизовано {vectorized} аб'ектаў, {failed} не удалось", + "Vectorization failed: {error}": "Векторизация не удалась: {error}", + "Vectorize All Files": "Векторизовать все файлы", + "Vectorize All Objects": "Векторизовать все аб'екты", + "Vectorize all file types": "Векторизовать все типы файлов", + "Vectorize all views": "Векторизовать все представления", + "Vectorize on object creation": "Векторизовать при создании аб'екта", + "Vectors will be stored in your existing object and file collections": "Векторы будут сохранены в существующих коллекциях аб'ектаў и файлов", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Проверка подключения к серверу...", + "Version": "Версия", + "Version Information": "Информация о версии", + "Very High": "Очень высокий", + "Very high": "Очень высокий", + "View": "Представление", + "View API Docs": "Просмотр документации API", + "View API Documentation": "Просмотр документации API", + "View APIs": "Просмотр API", + "View Changes": "Просмотр изменений", + "View Details": "Детали представления", + "View Error": "памылка представления", + "View File": "Просмотр файла", + "View Full Details": "Просмотреть полные детали", + "View Logs": "Просмотр журналов", + "View Name": "назва представления", + "View Object": "Просмотр аб'екта", + "View Parameters": "Параметры представления", + "View Selection": "Выбор представления", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Просматривать и анализировать журналы поиска с расширенными возможностями фильтрации и аналитики", + "View and analyze system audit trails with advanced filtering capabilities": "Просматривать и анализировать системные журналы аудита с расширенными возможностями фильтрации", + "View and manage authorization across registers and schemas": "Просматривать и управлять авторизацией в реестрах и схемах", + "View deleted successfully": "Представление паспяхова удалено", + "View entity details and manage relations": "Просматривать детали аб'екта и управлять связями", + "View name": "назва представления", + "View name is required": "назва представления обязательно", + "View object": "Просмотр аб'екта", + "View saved successfully!": "Представление паспяхова сохранено!", + "View search analytics and manage search logs": "Просматривать аналитику поиска и управлять журналами поиска", + "View successfully deleted": "Представление паспяхова удалено", + "View successfully updated": "Представление паспяхова обновлено", + "View updated successfully!": "Представление паспяхова обновлено!", + "View webhook delivery logs and filter by webhook": "Просматривать журналы доставки веб-хуков и фильтровать по веб-хуку", + "View {count} more": "Просмотреть ещё {count}", + "Views": "Представления", + "Visible to end users": "Видно конечным пользователям", + "Visible to users": "Видно пользователям", + "Wait for Response": "Ожидать ответа", + "Wait for webhook response before continuing (required for request/response flows)": "Ожидать ответ веб-хука перед продолжением (обязательно для потоков запрос/ответ)", + "Waiting for cluster synchronization...": "Ожидание синхронизации кластера...", + "Warmup Names Cache": "Прогреть кэш имён", + "Warmup failed": "Прогрев не удался", + "Warning": "Предупреждение", + "Warning:": "Предупреждение:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Предупреждение: это свойство имеет несогласованные типы: {types}. Рассмотрите, должны ли все аб'екты иметь одинаковый тип.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Мы просканируем все аб'екты, принадлежащие этой схеме, чтобы обнаружить новые свойства и проанализировать существующие свойства для потенциальных улучшений. Процесс включает изучение структуры данных каждого аб'екта, выявление свойств, не определённых в текущей схеме, и пошук возможностей улучшения существующих определений свойств с лучшими ограничениями, форматами и правилами проверки.", + "Webhook": "Веб-хук", + "Webhook Log Details": "Детали журнала веб-хука", + "Webhook Logs": "Журналы веб-хука", + "Webhook created successfully": "Веб-хук паспяхова створаны", + "Webhook deleted": "Веб-хук выдалены", + "Webhook retry delivered successfully": "Повтор веб-хука паспяхова доставлен", + "Webhook retry delivery failed": "Доставка повтора веб-хука не удалась", + "Webhook updated": "Веб-хук обновлён", + "Webhook updated successfully": "Веб-хук паспяхова обновлён", + "Webhooks": "Веб-хуки", + "Weekly": "Еженедельно", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Когда агент работает без пользовательского сеанса (например, запланированные задачи), будет использоваться контекст этого карыстальніка", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Когда агент работает без пользовательского сеанса (например, запланированные задачи), будет использоваться контекст этого карыстальніка.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "При отключении этот фасет будет показывать только значения из этой схемы и включать фильтр схемы при выборе.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "При включении захаванне этого аб'екта также обновит целевые аб'екты для включения обратной ссылки на этот аб'ект.", + "When enabled, this property will be removed from the source object after updating the target objects.": "При включении это свойство будет удалено из исходного аб'екта после обновления целевых аб'ектаў.", + "Workflow Management": "Управление рабочими процессами", + "Workflows": "Рабочие процессы", + "Wrap webhook payload in CloudEvents format for better interoperability": "Обернуть полезную нагрузку веб-хука в формат CloudEvents для лучшей совместимости", + "Wrap webhook payload in cloudevents format for better interoperability": "Обернуть полезную нагрузку веб-хука в формат CloudEvents для лучшей совместимости", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Да", + "You": "Вы", + "You are not yet a member of any organisations.": "Вы ещё не являетесь участником ни одной организации.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Вы можете создавать новые наборы конфигурации на основе шаблона _default или загружать пользовательские непосредственно на сервер SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Вы можете создавать новые наборы конфигурации на основе шаблона _default или загружать пользовательские непосредственно на сервер SOLR.", + "You do not have access to this conversation": "У вас нет доступа к этой переписке", + "You do not have permission to delete this conversation": "У вас нет разрешения на выдаленне этой переписки", + "You do not have permission to modify this conversation": "У вас нет разрешения на изменение этой переписки", + "You do not have permission to restore this conversation": "У вас нет разрешения на восстановление этой переписки", + "You do not have permission to view the permission matrix. Admin access is required.": "У вас нет разрешения для просмотра матрицы разрешений. Требуется доступ администратора.", + "You have selected {count} properties to add to the schema.": "Вы выбрали {count} свойств для добавления в схему.", + "You must be logged in to favorite views": "Для добавления представлений в избранное необходимо войти в систему", + "You need an AI agent to start a conversation.": "Для начала переписки вам нужен ИИ-агент.", + "Your Fireworks AI API key. Get one at": "Ваш API-ключ Fireworks AI. Получить по адресу", + "Your OpenAI API key. Get one at": "Ваш API-ключ OpenAI. Получить по адресу", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Ваш отзыв записан. При желании вы можете предоставить дополнительные сведения здесь...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Ваш отзыв записан. При желании вы можете предоставить дополнительные сведения здесь...", + "Zookeeper Hosts": "Хосты Zookeeper", + "Zookeeper Port": "Порт Zookeeper", + "Zookeeper Settings (SolrCloud)": "налады Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Строка подключения Zookeeper для SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Номер порта Zookeeper (необязательно, по умолчанию 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ПРЕДУПРЕЖДЕНИЕ О БЕЗВОЗВРАТНОМ УДАЛЕНИИ \\u26A0\\uFE0F\n\nВы собираетесь БЕЗВОЗВРАТНО выдаліць ВСЕ аб'екты схемы \"{schema}\":\n\n\\u2022 Активных аб'ектаў: {active}\n\\u2022 Программно удалённых аб'ектаў: {deleted}\n\\u2022 Итого: {total}\n\nЭти аб'екты будут полностью удалены из базы данных и НЕ МОГУТ быть восстановлены.\n\nВы абсолютно уверены?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ПРЕДУПРЕЖДЕНИЕ О БЕЗВОЗВРАТНОМ УДАЛЕНИИ \\u26A0\\uFE0F\n\nВы собираетесь БЕЗВОЗВРАТНО выдаліць {count} программно удалённых аб'ектаў схемы \"{schema}\".\n\nЭти аб'екты будут полностью удалены из базы данных и НЕ МОГУТ быть восстановлены.\n\nВы абсолютно уверены?", + "and accessible to all users in selected groups (or all users if no groups selected).": "и доступно всем пользователям в выбранных группах (или всем пользователям, если группы не выбраны).", + "and add the files there.": "и добавьте файлы туда.", + "approved": "одобрено", + "are only accessible to invited users.": "доступны только приглашённым пользователям.", + "by": "от", + "chunk": "фрагмент", + "chunks": "фрагменты", + "chunks processed": "фрагментов обработано", + "completed": "завершено", + "configuration(s)": "канфігурацыя(ии)", + "conversationId is required": "conversationId обязателен", + "create": "стварыць", + "delete": "выдаліць", + "deleted objects section": "раздел удалённых аб'ектаў", + "e.g., lib/Settings/config.json": "например, lib/Settings/config.json", + "e.g., lib/Settings/register.json": "например, lib/Settings/register.json", + "e.g., lib/settings/config.json": "например, lib/settings/config.json", + "e.g., lib/settings/register.json": "например, lib/settings/register.json", + "execute": "выполнить", + "failed": "не удалось", + "file": "файл", + "files": "файлы", + "file{plural}": "файл(ов)", + "folder": "папка", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "в {register}", + "in_progress": "в процессе", + "inherited": "унаследовано", + "list": "список", + "log{plural}": "журнал(ов)", + "manage": "управлять", + "members": "участники", + "menu or contact someone with permission to create agents.": "меню или обратитесь к кому-то с разрешением на стварэнне агентов.", + "message content is required": "содержимое сообщения обязательно", + "messages": "сообщения", + "missing": "отсутствует", + "myapp": "myapp", + "n8n URL and API key are required": "URL n8n и API-ключ обязательны", + "n8n configuration saved successfully": "канфігурацыя n8n паспяхова сохранена", + "n8n connection not configured": "Подключение n8n не настроено", + "n8n connection successful": "Подключение n8n паспяхова", + "n8n connection test failed: {message}": "Тест подключения n8n не удался: {message}", + "n8n connection test successful": "Тест подключения n8n успешен", + "n8n initialization failed: {message}": "Инициализация n8n не удалась: {message}", + "n8n integration disabled": "Интеграция n8n отключена", + "n8n integration enabled": "Интеграция n8n включена", + "n8n project initialized successfully": "Проект n8n паспяхова инициализирован", + "n8n settings saved successfully": "налады n8n паспяхова сохранены", + "notify_push configuration guide": "Руководство по настройке notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push установлен, но OpenRegister ещё не подтвердил паспяховы push. Активируйте захаванне аб'екта или проверьте конфигурацию notify_push.", + "notify_push is installed but not yet active": "notify_push установлен, но ещё не активен", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "аб'екты", + "objects processed": "аб'ектаў обработано", + "object{plural}": "аб'ект(ов)", + "or pick a range": "или выберите диапазон", + "org-...": "org-...", + "pending": "ожидает", + "provides a good balance between speed and accuracy.": "обеспечивает хороший баланс между скоростью и точностью.", + "public": "публичный", + "read": "чтение", + "register(s)": "рэестр(ов)", + "register(s) selected": "рэестр(ов) выбрано", + "register{plural}": "рэестр(ов)", + "rejected": "отклонено", + "required": "обязательно", + "results": "результаты", + "schema(s)": "схема(ем)", + "schema(s) selected": "схема(ем) выбрано", + "schema{plural}": "схема(ем)", + "selected objects": "выбранные аб'екты", + "set": "установить", + "sk-...": "sk-...", + "this application": "это приложение", + "to": "в", + "type must be \"positive\" or \"negative\"": "тип должен быть «positive» или «negative»", + "update": "абнавіць", + "vectorized": "векторизовано", + "view": "представление", + "views selected": "представлений выбрано", + "waiting": "ожидание", + "{active} active, {deleted} deleted": "{active} активных, {deleted} удалённых", + "{count} audit trails cleared successfully": "{count} записей журнала аудита паспяхова очищено", + "{count} compliance issue(s) detected": "Обнаружено {count} проблема(ем) соответствия", + "{count} deletions": "{count} удалений", + "{count} documents found": "Найдено {count} документов", + "{count} email": "{count} письмо", + "{count} emails": "{count} писем", + "{count} entries": "{count} записей", + "{count} issues": "{count} проблем", + "{count} members": "{count} участников", + "{count} searches": "{count} поисков", + "{count} selected": "{count} выбрано", + "{count} widget(s)": "{count} виджет(ов)", + "{matched} object(s) matched. {erased} erased.": "{matched} аб'ект(ов) найдено. {erased} удалено.", + "{minutes} minutes ago": "{minutes} минут назад", + "{percentage}% of objects": "{percentage}% аб'ектаў", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} в {register} / {schema}", + "{type} cache cleared successfully": "Кэш {type} паспяхова очищен", + "ℹ️ Current Configuration": "ℹ️ Текущая канфігурацыя", + "ℹ️ current configuration": "ℹ️ текущая канфігурацыя", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ ОПАСНОСТЬ: Все {total} аб'ектаў будут БЕЗВОЗВРАТНО УДАЛЕНЫ из базы данных. Это действие НЕОБРАТИМО и не может быть отменено!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕЖДЕНИЕ О БЕЗВОЗВРАТНОМ УДАЛЕНИИ ⚠️\n\nВы собираетесь БЕЗВОЗВРАТНО выдаліць ВСЕ аб'екты схемы \"{schema}\":\n\n• Активных аб'ектаў: {active}\n• Программно удалённых аб'ектаў: {deleted}\n• Итого: {total}\n\nЭти аб'екты будут полностью удалены из базы данных и НЕ МОГУТ быть восстановлены.\n\nВы абсолютно уверены?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕЖДЕНИЕ О БЕЗВОЗВРАТНОМ УДАЛЕНИИ ⚠️\n\nВы собираетесь БЕЗВОЗВРАТНО выдаліць {count} программно удалённых аб'ектаў схемы \"{schema}\".\n\nЭти аб'екты будут полностью удалены из базы данных и НЕ МОГУТ быть восстановлены.\n\nВы абсолютно уверены?", + "⚡ Batch Processing": "⚡ Пакетная обработка", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR может обрабатывать миллионы документов со временем ответа поиска менее секунды...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Полный контекст ✓ Меньше шансов пропустить детали ✗ Медленные ответы ✗ Может включать менее релевантную информацию", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Быстрые ответы ✓ Более сфокусированные ответы ✗ Может упустить релевантную информацию", + "✨ AI Features": "✨ Функции ИИ", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 В распределённом режиме SOLR использует ZooKeeper для координации кластера...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Фасетный пошук позволяет пользователям углубляться в результаты по категориям...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Знали ли вы? SOLR обеспечивает пошук для Netflix, Apple и многих других крупных сайтов!", + "💰 View Selection (Cost Optimization)": "💰 Выбор представлений (оптимизация затрат)", + "📄 Object Serialization": "📄 Сериализация аб'ектаў", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR предоставляет богатую аналитику и статистику производительности поиска...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Наборы конфигурации содержат схему и файлы конфигурации для вашего поискового индекса...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Репликация гарантирует доступность поискового индекса даже при отказе узлов...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR — мощная корпоративная поисковая платформа, построенная на Apache Lucene...", + "🔢 Vectorization Settings": "🔢 налады векторизации", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Автомасштабирование может динамически добавлять или удалять узлы в зависимости от нагрузки...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Функции безопасности включают аутентификацию, авторизацию и шифрование SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} письмо", + "{count} письма", + "{count} писем" + ], + "Purge {count} object from database": [ + "Очистить {count} аб'ект из базы данных", + "Очистить {count} аб'екта из базы данных", + "Очистить {count} аб'ектаў из базы данных" + ], + "Restore {count} object": [ + "Восстановить {count} аб'ект", + "Восстановить {count} аб'екта", + "Восстановить {count} аб'ектаў" + ], + "Successfully restored {count} object": [ + "паспяхова восстановлен {count} аб'ект", + "паспяхова восстановлено {count} аб'екта", + "паспяхова восстановлено {count} аб'ектаў" + ], + "Delete {count} object": [ + "выдаліць {count} аб'ект", + "выдаліць {count} аб'екта", + "выдаліць {count} аб'ектаў" + ], + "Object successfully deleted": [ + "аб'ект паспяхова выдалены", + "аб'екта паспяхова удалены", + "аб'ектаў паспяхова удалено" + ] + } +} diff --git a/l10n/bg.js b/l10n/bg.js new file mode 100644 index 0000000000..75463f278c --- /dev/null +++ b/l10n/bg.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(без тема)", + "(no title)" : "(без заглавие)", + "(unnamed)" : "(без име)", + "(untitled card)" : "(карта без заглавие)", + "Activities" : "Дейности", + "Add Endpoint" : "Добавяне на крайна точка", + "Add Source" : "Добавяне на източник", + "Add card" : "Добавяне на карта", + "Add contact" : "Добавяне на контакт", + "All clear" : "Всичко е наред", + "Articles" : "Статии", + "Code" : "Код", + "Compliance" : "Съответствие", + "Contacts" : "Контакти", + "Create Agent" : "Създаване на агент", + "Create Application" : "Създаване на приложение", + "Create event" : "Създаване на събитие", + "Erasure complete" : "Изтриването завърши", + "Erasure preview" : "Преглед на изтриването", + "Expand details" : "Разгъване на детайлите", + "Save changes" : "Запазване на промените", + "📄 Object Serialization" : "📄 Сериализация на обекти", + "🔢 Vectorization Settings" : "🔢 Настройки за векторизация", + "💰 View Selection (Cost Optimization)" : "💰 Избор на изгледи (оптимизация на разходите)", + "AI Assistant" : "AI асистент", + "AI Chat" : "AI чат", + "API Key" : "API ключ", + "API tokens saved successfully" : "API токените са запазени успешно", + "About ConfigSets" : "Относно ConfigSets", + "Action" : "Действие", + "Action Distribution" : "Разпределение на действията", + "Action:" : "Действие:", + "Actions" : "Действия", + "Active" : "Активен", + "Active Collections" : "Активни колекции", + "Active Filters" : "Активни филтри", + "Active Organisations" : "Активни организации", + "Active filters:" : "Активни филтри:", + "Active organisation set successfully" : "Активната организация е зададена успешно", + "Activity Period" : "Период на активност", + "Add Groups" : "Добавяне на групи", + "Add Schema" : "Добавяне на Схема", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Добавете заглавия на схеми, описания и информация за регистри, за да осигурите по-богат контекст при търсене", + "Add to favorites" : "Добавяне към любими", + "Added" : "Добавено", + "Added to favorites" : "Добавено към любими", + "Additional Information" : "Допълнителна информация", + "Additional feedback saved. Thank you!" : "Допълнителната обратна връзка е запазена. Благодарим Ви!", + "Admin" : "Администратор", + "Advanced" : "Разширени", + "Advanced Filters" : "Разширени филтри", + "Advanced Options" : "Разширени опции", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Разширените филтри не са налични при използване на източник база данни. Превключете на Auto или SOLR индекс за опции за филтриране.", + "After" : "След", + "Agent deleted successfully" : "Агентът е изтрит успешно", + "Agents" : "Агенти", + "All" : "Всички", + "All Categories" : "Всички категории", + "All Collections" : "Всички колекции", + "All Confidence Levels" : "Всички нива на достоверност", + "All ConfigSets" : "Всички ConfigSets", + "All Files" : "Всички Файлове", + "All Levels" : "Всички нива", + "All Types" : "Всички типове", + "All Webhooks" : "Всички уебхукове", + "All actions" : "Всички действия", + "All audit trails cleared successfully" : "Всички одитни следи са изчистени успешно", + "All registers" : "Всички регистри", + "All schemas" : "Всички схеми", + "All searches" : "Всички търсения", + "All users" : "Всички потребители", + "An error occurred while clearing audit trails" : "Възникна грешка при изчистването на одитните следи", + "An error occurred while deleting the audit trail" : "Възникна грешка при изтриването на одитната следа", + "An error occurred while deleting the view" : "Възникна грешка при изтриването на изгледа", + "An error occurred while permanently deleting the objects" : "Възникна грешка при окончателното изтриване на обектите", + "Analysis completed:" : "Анализът завърши:", + "Analysis steps:" : "Стъпки на анализа:", + "Analytics" : "Анализи", + "Analyze Objects" : "Анализиране на обекти", + "Analyze Properties" : "Анализиране на свойства", + "Analyze existing properties for improvement opportunities" : "Анализиране на съществуващите свойства за възможности за подобрение", + "Analyzing..." : "Анализиране...", + "Any favorites and sharing settings for this view" : "Всички любими и настройки за споделяне за този изглед", + "Any user" : "Всеки потребител", + "App store cache invalidated successfully" : "Кешът на магазина за приложения е инвалидиран успешно", + "Application deleted successfully" : "Приложението е изтрито успешно", + "Applications" : "Приложения", + "Apply Changes" : "Прилагане на промените", + "Archive" : "Архивиране", + "Archive conversation" : "Архивиране на разговора", + "Archived conversations are hidden from your active list" : "Архивираните разговори са скрити от Вашия активен списък", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Сигурни ли сте, че искате да изчистите старите следи на търсене? Това ще изтрие записи, по-стари от 30 дни.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Сигурни ли сте, че искате да изтриете избраните одитни следи? Това действие не може да бъде отменено.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Сигурни ли сте, че искате да изтриете избраните следи на търсене? Това действие не може да бъде отменено.", + "Are you sure you want to delete this ConfigSet?" : "Сигурни ли сте, че искате да изтриете този ConfigSet?", + "Are you sure you want to permanently delete" : "Сигурни ли сте, че искате окончателно да изтриете", + "Ask a question..." : "Задайте въпрос...", + "Ask questions about your data using natural language" : "Задавайте въпроси относно Вашите данни, използвайки естествен език", + "Attempt" : "Опит", + "Audit Trail Changes" : "Промени в одитната следа", + "Audit Trail Details" : "Детайли на одитната следа", + "Audit Trail Management" : "Управление на одитни следи", + "Audit Trail Statistics" : "Статистика на одитните следи", + "Audit Trails" : "Одитни следи", + "Audit trail data copied to clipboard" : "Данните от одитната следа са копирани в клипборда", + "Audit trail deleted successfully" : "Одитната следа е изтрита успешно", + "Audit trail successfully deleted" : "Одитната следа е изтрита успешно", + "Authentication" : "Удостоверяване", + "Auto-commit disabled" : "Автоматичното подаване е изключено", + "Auto-commit enabled" : "Автоматичното подаване е включено", + "Auto-create Default Organisation" : "Автоматично създаване на организация по подразбиране", + "Auto-retry failed vectorizations" : "Автоматично повторение на неуспешните векторизации", + "Automatically commit changes to SOLR index" : "Автоматично подаване на промените към SOLR индекса", + "Automatically create a default organisation if none exists when the app is initialized" : "Автоматично създаване на организация по подразбиране, ако не съществува такава при инициализиране на приложението", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Автоматично генериране на векторни вграждания от текстови фрагменти при качване и обработка на файлове", + "Automatically generate vector embeddings when objects are created or updated" : "Автоматично генериране на векторни вграждания при създаване или актуализиране на обекти", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Автоматично повторение на неуспешните опити за векторизация (максимум 3 повторения)", + "Available Filters" : "Налични филтри", + "Available Workflows" : "Налични работни потоци", + "Avg Execution Time" : "Средно време за изпълнение", + "Avg Members/Org" : "Средно членове/орг.", + "Avg Object Views/Session" : "Средно преглеждания на обекти/сесия", + "Avg Response Time" : "Средно време за отговор", + "Avg Results/Search" : "Средно резултати/търсене", + "Avg Searches/Session" : "Средно търсения/сесия", + "Back" : "Назад", + "Back to Entities" : "Обратно към субектите", + "Back to Registers" : "Обратно към регистрите", + "Back to Webhooks" : "Обратно към уебхуковете", + "Back to applications" : "Обратно към приложенията", + "Back to entities" : "Обратно към субектите", + "Backend updated successfully. Please reload the application." : "Бекендът е актуализиран успешно. Моля, презаредете приложението.", + "Base URL (Optional)" : "Базов URL (по избор)", + "Basic Information" : "Основна информация", + "Batch Size" : "Размер на партидата", + "Batch extraction completed" : "Партидното извличане завърши", + "Before" : "Преди", + "Before object vectorization can work:" : "Преди векторизацията на обекти да може да работи:", + "Behavior Issue" : "Проблем с поведението", + "Bookmarks" : "Отметки", + "Blob storage has been retired. All objects now use magic tables." : "Blob съхранението е изведено от употреба. Всички обекти вече използват магически таблици.", + "Branch" : "Клон", + "Bulk delete operation completed successfully" : "Операцията за групово изтриване завърши успешно", + "Bulk save operation completed successfully" : "Операцията за групово запазване завърши успешно", + "Business Data" : "Бизнес данни", + "Cache cleared successfully" : "Кешът е изчистен успешно", + "Calculate Sizes" : "Изчисляване на размери", + "Cancel" : "Отказ", + "Cannot delete: objects are still attached" : "Не може да се изтрие: все още има прикачени обекти", + "Category" : "Категория", + "Change Type" : "Тип на промяната", + "Changes" : "Промени", + "Changes copied to clipboard" : "Промените са копирани в клипборда", + "Chat Model" : "Чат модел", + "Chat Provider" : "Чат доставчик", + "Chat Provider (RAG)" : "Чат доставчик (RAG)", + "Chat Settings" : "Настройки на чата", + "Chat provider connection successful!" : "Връзката с чат доставчика е успешна!", + "Chat settings" : "Настройки на чата", + "Choose a register" : "Изберете регистър", + "Choose a schema" : "Изберете схема", + "Choose how vector similarity calculations are performed for semantic search" : "Изберете как се извършват изчисленията за векторно сходство при семантично търсене", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Изберете кои типове файлове да бъдат включени в процеса на векторизация. Ще бъдат обработени само файлове с извлечен текст и фрагменти.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Изберете кои изгледи да бъдат включени в процеса на векторизация. Оставете празно, за да обработите всички изгледи според Вашата конфигурация.", + "Chunk" : "Фрагмент", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Изтриването на фрагменти все още не е реализирано. Използвайте крайни точки, базирани на фрагменти.", + "Chunks" : "Фрагменти", + "Chunks to Vectorize:" : "Фрагменти за векторизация:", + "Cleanup Old Trails" : "Изчистване на старите следи", + "Cleanup completed" : "Изчистването завърши", + "Clear All Embeddings" : "Изчистване на всички вграждания", + "Clear Entries" : "Изчистване на записите", + "Clear Filtered Audit Trails" : "Изчистване на филтрираните одитни следи", + "Clear Filters" : "Изчистване на филтрите", + "Clear Index" : "Изчистване на индекса", + "Clear Selection" : "Изчистване на избора", + "Clear all" : "Изчистване на всички", + "Clear all filters" : "Изчистване на всички филтри", + "Clear date range" : "Изчистване на диапазона от дати", + "Clearing..." : "Изчистване...", + "Close" : "Затваряне", + "Collection Management" : "Управление на колекции", + "Collection Name" : "Име на колекцията", + "Collection assignments updated successfully" : "Назначенията на колекции са актуализирани успешно", + "Collection cleared successfully" : "Колекцията е изчистена успешно", + "Collection copied successfully" : "Колекцията е копирана успешно", + "Collection created successfully" : "Колекцията е създадена успешно", + "Collection deleted successfully" : "Колекцията е изтрита успешно", + "Collection used to store and index file metadata and content" : "Колекция, използвана за съхранение и индексиране на метаданни и съдържание на файлове", + "Collection used to store and index object data" : "Колекция, използвана за съхранение и индексиране на данни за обекти", + "Collections:" : "Колекции:", + "Commit Message" : "Съобщение за подаване", + "Commit Within (ms)" : "Подаване в рамките на (мс)", + "Commit message" : "Съобщение за подаване", + "Compare current schema with real object data" : "Сравняване на текущата схема с реални данни за обекти", + "Completed" : "Завършено", + "Complex" : "Сложно", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Сложни заявки: разширени търсения с множество филтри, оператори и сложни комбинации от параметри", + "Confidence Level" : "Ниво на достоверност", + "Confidence Score" : "Оценка на достоверност", + "Config must be provided as an object" : "Конфигурацията трябва да бъде предоставена като обект", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Управление на ConfigSet", + "ConfigSet Name" : "Име на ConfigSet", + "ConfigSet created successfully" : "ConfigSet е създаден успешно", + "ConfigSet deleted successfully" : "ConfigSet е изтрит успешно", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets дефинират схемата и конфигурацията за Вашите SOLR колекции. Те съдържат дефиниции на полета, анализатори и други настройки за търсене.", + "Configuration" : "Конфигурация", + "Configuration Data" : "Данни за конфигурацията", + "Configuration saved" : "Конфигурацията е запазена", + "Configurations" : "Конфигурации", + "Configure Facets" : "Конфигуриране на фасети", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Конфигурирайте доставчици на големи езикови модели (LLM) за функции, задвижвани от AI, включително семантично търсене, вграждания и чат.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Конфигурирайте основните настройки за връзка за Вашия SOLR сървър, включително удостоверяване и мрежови опции. Използвайте отделните диалогови прозорци за управление на ConfigSet и колекции, за да управлявате ядра и колекции.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Конфигурирайте как обектите от базата данни се преобразуват във векторни вграждания за семантично търсене. Обектите се векторизират директно, без да е необходимо извличане на текст.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Конфигурирайте как обектите се преобразуват в текст преди векторизация. Тези настройки влияят върху качеството и контекста на търсенето.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Конфигурирайте параметри за векторизация на файлове. Този процес ще генерира векторни вграждания за всички извлечени фрагменти от файлове.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Конфигурирайте параметри за векторизация на обекти. Този процес ще генерира векторни вграждания за всички обекти, отговарящи на филтрите на Вашия изглед.", + "Configure which types of data to search and how many sources to retrieve" : "Конфигурирайте кои типове данни да се търсят и колко източника да се извличат", + "Connection Failed" : "Връзката е неуспешна", + "Connection Settings" : "Настройки за връзка", + "Connection Successful!" : "Връзката е успешна!", + "Connection failed" : "Връзката е неуспешна", + "Connection protocol" : "Протокол за връзка", + "Connection successful" : "Връзката е успешна", + "Connection timeout in seconds" : "Таймаут на връзката в секунди", + "Constant delay between retries (5 minutes)" : "Постоянно забавяне между повторенията (5 минути)", + "Constraint Issue" : "Проблем с ограничение", + "Content Filters" : "Филтри за съдържание", + "Continue to Properties" : "Продължаване към свойствата", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Контролирайте кои изгледи на обекти да бъдат векторизирани, за да намалите разходите за API. Векторизирайте само изгледи, които се възползват от семантично търсене.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Контролирайте кои изгледи и инструменти може да използва AI в този разговор. По подразбиране всички възможности на агента са активирани.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Контролира случайността (0 = детерминистично, 2 = много творческо)", + "Conversation ID is required" : "Идентификаторът на разговора е задължителен", + "Conversation archived" : "Разговорът е архивиран", + "Conversation archived successfully" : "Разговорът е архивиран успешно", + "Conversation cleared successfully" : "Разговорът е изчистен успешно", + "Conversation deleted" : "Разговорът е изтрит", + "Conversation permanently deleted" : "Разговорът е окончателно изтрит", + "Conversation renamed" : "Разговорът е преименуван", + "Conversation restored" : "Разговорът е възстановен", + "Conversation title" : "Заглавие на разговора", + "Copied!" : "Копирано!", + "Copy" : "Копиране", + "Copy Changes" : "Копиране на промените", + "Copy Collection" : "Копиране на колекция", + "Copy Data" : "Копиране на данни", + "Copy Full Data" : "Копиране на пълните данни", + "Copying..." : "Копиране...", + "Count" : "Брой", + "Counting objects..." : "Преброяване на обекти...", + "Costs" : "Разходи", + "Create" : "Създаване", + "Create Collection" : "Създаване на колекция", + "Create ConfigSet" : "Създаване на ConfigSet", + "Create New Collection" : "Създаване на нова колекция", + "Create New ConfigSet" : "Създаване на нов ConfigSet", + "Create Operations" : "Операции за създаване", + "Create Webhook" : "Създаване на уебхук", + "Create a copy of collection:" : "Създаване на копие на колекцията:", + "Create a new ConfigSet based on the _default template" : "Създаване на нов ConfigSet въз основа на шаблона _default", + "Create a new SOLR collection from an existing ConfigSet" : "Създаване на нова SOLR колекция от съществуващ ConfigSet", + "Create your first AI agent to get started." : "Създайте първия си AI агент, за да започнете.", + "Created" : "Създадено", + "Created:" : "Създадено:", + "Creating..." : "Създаване...", + "Current Filters" : "Текущи филтри", + "Current Type:" : "Текущ тип:", + "Custom API endpoint if using a different region" : "Персонализирана крайна точка на API при използване на различен регион", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Персонализирани HTTP заглавки (по една на ред, формат: Header-Name: value)", + "Daily" : "Ежедневно", + "Dashboard" : "Табло", + "Data Source" : "Източник на данни", + "Data sources" : "Източници на данни", + "Data type does not match observed values" : "Типът на данните не съответства на наблюдаваните стойности", + "Database URL" : "URL на базата данни", + "Database information refreshed" : "Информацията за базата данни е обновена", + "Date Range" : "Диапазон от дати", + "Default" : "По подразбиране", + "Default Organisation" : "Организация по подразбиране", + "Default: 5" : "По подразбиране: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Забавянията се удвояват с всеки опит (2, 4, 8 минути...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Забавянията нарастват линейно (5, 10, 15 минути...)", + "Delete" : "Изтриване", + "Delete Application" : "Изтриване на приложение", + "Delete Audit Trail" : "Изтриване на одитна следа", + "Delete Collection" : "Изтриване на колекция", + "Delete ConfigSet" : "Изтриване на ConfigSet", + "Delete Objects" : "Изтриване на обекти", + "Delete Operations" : "Операции за изтриване", + "Delete View" : "Изтриване на изглед", + "Delete all objects in this schema" : "Изтриване на всички обекти в тази схема", + "Delete permanently" : "Окончателно изтриване", + "Delete this conversation?" : "Изтриване на този разговор?", + "Delete view" : "Изтриване на изглед", + "Deleted" : "Изтрито", + "Deleted By" : "Изтрито от", + "Deleted Date" : "Дата на изтриване", + "Deleted Items Management" : "Управление на изтрити елементи", + "Deleted This Week" : "Изтрити тази седмица", + "Deleted Today" : "Изтрити днес", + "Deleted:" : "Изтрито:", + "Deleting..." : "Изтриване...", + "Deletion Date Range" : "Диапазон от дати на изтриване", + "Deletion Statistics" : "Статистика за изтриванията", + "Deprecated" : "Остаряло", + "Description" : "Описание", + "Description:" : "Описание:", + "Detect data types and patterns" : "Откриване на типове данни и модели", + "Detected At" : "Открито на", + "Detected Format:" : "Открит формат:", + "Detected Issues:" : "Открити проблеми:", + "Disable" : "Деактивиране", + "Disabled" : "Деактивирано", + "Discover Files" : "Откриване на файлове", + "Discovered Properties" : "Открити свойства", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Искате ли да изтриете окончателно всички филтрирани записи на одитни следи? Това действие не може да бъде отменено.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Искате ли да изтриете окончателно този запис на одитна следа? Това действие не може да бъде отменено.", + "Documents" : "Документи", + "Download API Spec" : "Изтегляне на API спецификация", + "Edit" : "Редактиране", + "Edit Register" : "Редактиране на Регистър", + "Edit Schema" : "Редактиране на Схема", + "Edit View" : "Редактиране на изглед", + "Edit Webhook" : "Редактиране на уебхук", + "Edit view" : "Редактиране на изглед", + "Edit view details" : "Редактиране на детайлите на изгледа", + "Effectiveness" : "Ефективност", + "Email" : "Имейл", + "Emails" : "Имейли", + "Embedding Model" : "Модел за вграждане", + "Embedding Provider" : "Доставчик на вграждания", + "Embedding generated successfully" : "Вграждането е генерирано успешно", + "Embedding provider connection successful!" : "Връзката с доставчика на вграждания е успешна!", + "Enable" : "Активиране", + "Enable auto-creation" : "Активиране на автоматично създаване", + "Enable automatic file vectorization" : "Активиране на автоматична векторизация на файлове", + "Enable automatic object vectorization" : "Активиране на автоматична векторизация на обекти", + "Enable automatic synchronization" : "Активиране на автоматична синхронизация", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Активиране на подробно логване за SOLR операции (препоръчително за отстраняване на грешки)", + "Enable faceting" : "Активиране на фасетиране", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Активирайте или деактивирайте LLM функциите. Конфигурирайте доставчиците и моделите, използвайки бутона LLM конфигурация по-горе.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Активирайте или деактивирайте интеграцията със SOLR търсене. Конфигурирайте настройките за връзка, използвайки бутона Настройки за връзка по-горе.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Активирайте или деактивирайте интеграцията с работни потоци на n8n. Конфигурирайте настройките за връзка по-долу.", + "Enable or disable this webhook" : "Активиране или деактивиране на този уебхук", + "Enable vectorization for all existing and future views (may increase costs)" : "Активиране на векторизация за всички съществуващи и бъдещи изгледи (може да увеличи разходите)", + "Enabled" : "Активирано", + "Endpoints" : "Крайни точки", + "Enter ConfigSet name" : "Въведете име на ConfigSet", + "Enter collection name" : "Въведете име на колекция", + "Enter description (optional)..." : "Въведете описание (по избор)...", + "Enter new collection name" : "Въведете ново име на колекция", + "Enter object ID" : "Въведете идентификатор на обект", + "Enter search term" : "Въведете термин за търсене", + "Enter view name..." : "Въведете име на изглед...", + "Enter webhook name" : "Въведете име на уебхук", + "Entities" : "Субекти", + "Entity Information" : "Информация за субекта", + "Entity deleted successfully" : "Субектът е изтрит успешно", + "Entity not found" : "Субектът не е намерен", + "Entries to be deleted:" : "Записи за изтриване:", + "Enum Issue" : "Проблем с изброяване", + "Enum constraint is missing" : "Липсва ограничение за изброяване", + "Error" : "Грешка", + "Error Details" : "Детайли за грешката", + "Error Information" : "Информация за грешката", + "Error Message" : "Съобщение за грешка", + "Error loading application" : "Грешка при зареждане на приложението", + "Error loading audit trails" : "Грешка при зареждане на одитните следи", + "Error loading entity" : "Грешка при зареждане на субекта", + "Error loading search trails" : "Грешка при зареждане на следите на търсене", + "Estimated Batches:" : "Прогнозни партиди:", + "Estimated Cost:" : "Прогнозен разход:", + "Estimated Duration:" : "Прогнозна продължителност:", + "Event" : "Събитие", + "Event Property for Payload" : "Свойство на събитие за полезен товар", + "Events" : "Събития", + "Example Value" : "Примерна стойност", + "Exclusive Maximum" : "Изключителен максимум", + "Exclusive Minimum" : "Изключителен минимум", + "Execution Mode" : "Режим на изпълнение", + "Execution Time" : "Време за изпълнение", + "Execution Time Range" : "Диапазон на времето за изпълнение", + "Existing Improvements" : "Съществуващи подобрения", + "Exponential" : "Експоненциално", + "Export" : "Експортиране", + "Export completed successfully" : "Експортирането завърши успешно", + "Export, view, or delete audit trails" : "Експортиране, преглед или изтриване на одитни следи", + "Extend Schema" : "Разширяване на Схема", + "Extended" : "Разширено", + "Extensions" : "Разширения", + "Extract Pending Files" : "Извличане на чакащи файлове", + "Extract properties from each object" : "Извличане на свойства от всеки обект", + "Extracted At" : "Извлечено на", + "Extraction Status" : "Статус на извличане", + "Facet configuration updated successfully" : "Конфигурацията на фасетите е актуализирана успешно", + "Facets discovered and configured successfully" : "Фасетите са открити и конфигурирани успешно", + "Facets discovered successfully" : "Фасетите са открити успешно", + "Failed" : "Неуспешно", + "Failed to analyze schema properties" : "Неуспешно анализиране на свойствата на схемата", + "Failed to archive conversation" : "Неуспешно архивиране на разговора", + "Failed to calculate sizes" : "Неуспешно изчисляване на размерите", + "Failed to clear collection" : "Неуспешно изчистване на колекцията", + "Failed to copy changes" : "Неуспешно копиране на промените", + "Failed to copy data" : "Неуспешно копиране на данните", + "Failed to copy data to clipboard" : "Неуспешно копиране на данните в клипборда", + "Failed to create conversation" : "Неуспешно създаване на разговор", + "Failed to create or find project" : "Неуспешно създаване или намиране на проект", + "Failed to delete collection" : "Неуспешно изтриване на колекцията", + "Failed to delete conversation" : "Неуспешно изтриване на разговора", + "Failed to delete webhook" : "Неуспешно изтриване на уебхука", + "Failed to download API specification" : "Неуспешно изтегляне на API спецификацията", + "Failed to get SOLR field configuration" : "Неуспешно получаване на конфигурацията на SOLR полето", + "Failed to get entity categories" : "Неуспешно получаване на категориите на субектите", + "Failed to get entity statistics" : "Неуспешно получаване на статистиката на субектите", + "Failed to get entity types" : "Неуспешно получаване на типовете субекти", + "Failed to load LLM configuration" : "Неуспешно зареждане на LLM конфигурацията", + "Failed to load Nextcloud groups" : "Неуспешно зареждане на групите в Nextcloud", + "Failed to load conversation" : "Неуспешно зареждане на разговора", + "Failed to load entities" : "Неуспешно зареждане на субектите", + "Failed to load entity" : "Неуспешно зареждане на субекта", + "Failed to load files" : "Неуспешно зареждане на файловете", + "Failed to load organisations" : "Неуспешно зареждане на организациите", + "Failed to load templates" : "Неуспешно зареждане на шаблоните", + "Failed to load webhooks" : "Неуспешно зареждане на уебхуковете", + "Failed to load workflows" : "Неуспешно зареждане на работните потоци", + "Failed to refresh database information" : "Неуспешно обновяване на информацията за базата данни", + "Failed to reindex collection" : "Неуспешно повторно индексиране на колекцията", + "Failed to rename conversation" : "Неуспешно преименуване на разговора", + "Failed to restore conversation" : "Неуспешно възстановяване на разговора", + "Failed to retry extraction" : "Неуспешно повторение на извличането", + "Failed to retry webhook" : "Неуспешно повторение на уебхука", + "Failed to save GitHub token" : "Неуспешно запазване на GitHub токена", + "Failed to save GitLab URL" : "Неуспешно запазване на GitLab URL", + "Failed to save GitLab token" : "Неуспешно запазване на GitLab токена", + "Failed to save additional feedback" : "Неуспешно запазване на допълнителната обратна връзка", + "Failed to save n8n configuration" : "Неуспешно запазване на n8n конфигурацията", + "Failed to save roles" : "Неуспешно запазване на ролите", + "Failed to save settings" : "Неуспешно запазване на настройките", + "Failed to save webhook" : "Неуспешно запазване на уебхука", + "Failed to send feedback" : "Неуспешно изпращане на обратна връзка", + "Failed to test webhook" : "Неуспешно тестване на уебхука", + "Failed to update favorite status" : "Неуспешно актуализиране на статуса на любими", + "Failed to update schema properties" : "Неуспешно актуализиране на свойствата на схемата", + "Failed to update view" : "Неуспешно актуализиране на изгледа", + "Failed to update webhook" : "Неуспешно актуализиране на уебхука", + "Feedback recorded" : "Обратната връзка е записана", + "Fewer sources (1-3):" : "По-малко източници (1-3):", + "Field" : "Поле", + "File" : "Файл", + "File Chunk Prediction" : "Прогноза за фрагменти на файла", + "File Collection" : "Колекция от файлове", + "File Management" : "Управление на файлове", + "File Name" : "Име на файла", + "File Path" : "Път до файла", + "File Type Selection" : "Избор на тип файл", + "File Vectorization" : "Векторизация на файлове", + "File Warmup" : "Подгряване на файлове", + "File actions menu" : "Меню за действия с файлове", + "File anonymized successfully" : "Файлът е анонимизиран успешно", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Векторизацията на фрагменти на файла все още не е реализирана. Фрагментите са готови и съхранени, но услугата за векторизация е в процес на разработка.", + "File collection not configured" : "Колекцията от файлове не е конфигурирана", + "File discovery completed" : "Откриването на файлове завърши", + "File extraction completed" : "Извличането на файла завърши", + "File extraction queued" : "Извличането на файла е поставено на опашка", + "File indexed successfully" : "Файлът е индексиран успешно", + "File is already anonymized" : "Файлът вече е анонимизиран", + "File not found" : "Файлът не е намерен", + "File settings updated successfully" : "Настройките на файла са актуализирани успешно", + "File sources" : "Източници на файлове", + "File vectorization configuration saved successfully" : "Конфигурацията за векторизация на файлове е запазена успешно", + "File vectorization started. Check the statistics section for progress." : "Векторизацията на файлове започна. Проверете секцията със статистика за напредъка.", + "File warmup completed" : "Подгряването на файла завърши", + "Files" : "Файлове", + "Files with Completed Extraction:" : "Файлове със завършено извличане:", + "Files → fileCollection, Objects → objectCollection" : "Файлове → fileCollection, обекти → objectCollection", + "Filter Audit Trails" : "Филтриране на одитни следи", + "Filter Deleted Items" : "Филтриране на изтрити елементи", + "Filter Objects" : "Филтриране на обекти", + "Filter Properties" : "Филтриране на свойства", + "Filter Search Trails" : "Филтриране на следи на търсене", + "Filter Statistics" : "Статистика на филтрите", + "Filter and analyze search trail entries" : "Филтриране и анализиране на записи на следи на търсене", + "Filter and manage audit trail entries" : "Филтриране и управление на записи на одитни следи", + "Filter and manage soft deleted items" : "Филтриране и управление на меко изтрити елементи", + "Filter by object ID" : "Филтриране по идентификатор на обект", + "Filter by search term" : "Филтриране по термин за търсене", + "Filter by webhook" : "Филтриране по уебхук", + "Filter data loaded automatically. Use the filters below to refine your search." : "Данните за филтриране са заредени автоматично. Използвайте филтрите по-долу, за да прецизирате търсенето си.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Филтриране на тригерите на уебхук по свойства на полезния товар (по едно на ред, формат: key: value)", + "Filtered" : "Филтрирано", + "Filters" : "Филтри", + "Fireworks AI Chat Settings" : "Настройки за чат на Fireworks AI", + "Fireworks AI Embedding Configuration" : "Конфигурация за вграждания на Fireworks AI", + "First" : "Първи", + "Fixed" : "Фиксирано", + "For chat and retrieval-augmented generation" : "За чат и генериране, подсилено с извличане", + "For vector embeddings and semantic search" : "За векторни вграждания и семантично търсене", + "Format" : "Формат", + "Format Issue" : "Проблем с формата", + "Format constraint is missing" : "Липсва ограничение за формат", + "From Date" : "От дата", + "From date" : "От дата", + "Full data copied to clipboard" : "Пълните данни са копирани в клипборда", + "General Issue" : "Общ проблем", + "Generate recommendations and confidence scores" : "Генериране на препоръки и оценки на достоверност", + "Generate vectors immediately when new objects are created" : "Генериране на вектори незабавно при създаване на нови обекти", + "GitHub token is valid" : "GitHub токенът е валиден", + "GitHub token removed successfully" : "GitHub токенът е премахнат успешно", + "GitHub token saved successfully" : "GitHub токенът е запазен успешно", + "GitLab URL saved successfully" : "GitLab URL е запазен успешно", + "GitLab token is valid" : "GitLab токенът е валиден", + "GitLab token saved successfully" : "GitLab токенът е запазен успешно", + "HTTP Method" : "HTTP метод", + "HTTP method used to send webhook requests" : "HTTP метод, използван за изпращане на заявки за уебхук", + "Headers" : "Заглавки", + "Health" : "Състояние", + "Heartbeat successful - connection kept alive" : "Сърдечният ритъм е успешен - връзката е поддържана", + "Helpful" : "Полезно", + "Hide Filters" : "Скриване на филтрите", + "Hide in forms" : "Скриване във формуляри", + "Hide in list view" : "Скриване в изглед на списък", + "High" : "Високо", + "High Confidence" : "Висока достоверност", + "Host" : "Хост", + "Hourly" : "Ежечасно", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Колко дълбоко да се обхождат вложените свойства на обекта (1-20). По-високите стойности улавят повече детайли, но увеличават размера на вектора.", + "How often to check for updates (1-168 hours)" : "Колко често да се проверява за актуализации (1-168 часа)", + "How to handle retries for failed webhook deliveries" : "Как да се обработват повторенията за неуспешни доставки на уебхук", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Ограниченията на честотата по IP са изчистени успешно", + "Identify properties not in the schema" : "Идентифициране на свойства, които не са в схемата", + "Immutable" : "Неизменяемо", + "Import" : "Импортиране", + "Import successful" : "Импортирането е успешно", + "Improved Property" : "Подобрено свойство", + "In use" : "В употреба", + "Inactive" : "Неактивен", + "Include IDs and names of related objects for better contextual search" : "Включване на идентификатори и имена на свързани обекти за по-добро контекстуално търсене", + "Include related object references" : "Включване на референции към свързани обекти", + "Include schema and register metadata" : "Включване на метаданни за схема и регистър", + "Initialization failed" : "Инициализацията е неуспешна", + "Initialize Project" : "Инициализиране на проект", + "Inspect Fields" : "Инспектиране на полета", + "Inspect Index" : "Инспектиране на индекс", + "Invalid" : "Невалиден", + "Invalid batch size. Must be between 1 and 5000" : "Невалиден размер на партидата. Трябва да бъде между 1 и 5000", + "Invalid field name provided" : "Предоставено е невалидно име на поле", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Невалиден maxObjects. Трябва да бъде 0 (всички) или положително число", + "Invalid:" : "Невалиден:", + "Issue" : "Проблем", + "Items per page:" : "Елементи на страница:", + "Just now" : "Току-що", + "LLM Configuration" : "LLM конфигурация", + "LLM actions menu" : "Меню за LLM действия", + "LLM configuration saved successfully" : "LLM конфигурацията е запазена успешно", + "LLM features disabled" : "LLM функциите са деактивирани", + "LLM features enabled" : "LLM функциите са активирани", + "LLM must be enabled with an embedding provider configured" : "LLM трябва да бъде активиран с конфигуриран доставчик на вграждания", + "LLM settings updated successfully" : "LLM настройките са актуализирани успешно", + "Last" : "Последен", + "Last Triggered" : "Последно задействане", + "Length Range:" : "Диапазон на дължина:", + "Linear" : "Линейно", + "Load Advanced Filters" : "Зареждане на разширени филтри", + "Load advanced filters with live data from your search index" : "Зареждане на разширени филтри с данни на живо от Вашия индекс за търсене", + "Load view" : "Зареждане на изглед", + "Loading ConfigSets..." : "Зареждане на ConfigSets...", + "Loading advanced filters..." : "Зареждане на разширени филтри...", + "Loading agents..." : "Зареждане на агенти...", + "Loading application details..." : "Зареждане на детайли за приложението...", + "Loading applications..." : "Зареждане на приложения...", + "Loading archived conversations..." : "Зареждане на архивирани разговори...", + "Loading audit trails..." : "Зареждане на одитни следи...", + "Loading collections..." : "Зареждане на колекции...", + "Loading configurations..." : "Зареждане на конфигурации...", + "Loading conversation..." : "Зареждане на разговор...", + "Loading conversations..." : "Зареждане на разговори...", + "Loading deleted items..." : "Зареждане на изтрити елементи...", + "Loading events..." : "Зареждане на събития...", + "Loading filter data..." : "Зареждане на данни за филтриране...", + "Loading groups..." : "Зареждане на групи...", + "Loading log details..." : "Зареждане на детайли за дневника...", + "Loading registers..." : "Зареждане на регистри...", + "Loading schemas..." : "Зареждане на схеми...", + "Loading search trails..." : "Зареждане на следи на търсене...", + "Loading sources..." : "Зареждане на източници...", + "Loading statistics..." : "Зареждане на статистика...", + "Loading users..." : "Зареждане на потребители...", + "Loading views..." : "Зареждане на изгледи...", + "Loading..." : "Зареждане...", + "Local" : "Локален", + "Local Version" : "Локална версия", + "Locked" : "Заключено", + "Locked:" : "Заключено:", + "Login successful" : "Влизането е успешно", + "Logs" : "Дневници", + "Low" : "Ниско", + "Low Confidence" : "Ниска достоверност", + "Lucene Version:" : "Версия на Lucene:", + "Magic table synchronized successfully" : "Магическата таблица е синхронизирана успешно", + "Manage Organisation Roles" : "Управление на ролите в организацията", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Управлявайте SOLR колекции (хранилища на данни) и ги назначавайте за обекти и файлове.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Управлявайте SOLR ConfigSets (конфигурационни шаблони) за Вашите колекции.", + "Manage and configure agents for automated tasks" : "Управление и конфигуриране на агенти за автоматизирани задачи", + "Manage and monitor file text extraction status" : "Управление и наблюдение на статуса на извличане на текст от файлове", + "Manage and restore soft deleted items from your registers" : "Управление и възстановяване на меко изтрити елементи от Вашите регистри", + "Manage and view detected entities from files and objects" : "Управление и преглед на открити субекти от файлове и обекти", + "Manage document templates and themes" : "Управление на шаблони и теми за документи", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Управлявайте работни потоци на n8n за автоматизация на OpenRegister. Работните потоци ще се съхраняват в конфигурирания проект.", + "Manage webhooks for event-driven integrations" : "Управление на уебхукове за интеграции, задвижвани от събития", + "Manage your applications and modules" : "Управлявайте Вашите приложения и модули", + "Manage your data registers and their configurations" : "Управлявайте Вашите регистри с данни и техните конфигурации", + "Manage your data schemas and their properties" : "Управлявайте Вашите схеми с данни и техните свойства", + "Manage your data sources and their configurations" : "Управлявайте Вашите източници на данни и техните конфигурации", + "Manage your saved search configurations" : "Управлявайте Вашите запазени конфигурации за търсене", + "Manage your system configurations and settings" : "Управлявайте Вашите системни конфигурации и настройки", + "Managed" : "Управляван", + "Max Files (0 = all)" : "Максимум файлове (0 = всички)", + "Max Length" : "Максимална дължина", + "Max Length:" : "Максимална дължина:", + "Max Objects (0 = all)" : "Максимум обекти (0 = всички)", + "Max Retries" : "Максимум повторения", + "Max Shards/Node" : "Максимум фрагменти/възел", + "Max execution time (ms)" : "Максимално време за изпълнение (мс)", + "Max ms" : "Макс. мс", + "Max result count" : "Максимален брой резултати", + "Max results" : "Максимум резултати", + "Maximum" : "Максимум", + "Maximum Nesting Depth" : "Максимална дълбочина на влагане", + "Maximum length constraint is missing" : "Липсва ограничение за максимална дължина", + "Maximum length is too restrictive" : "Максималната дължина е твърде ограничителна", + "Maximum number of files to process. Set to 0 to process all files." : "Максимален брой файлове за обработка. Задайте 0, за да обработите всички файлове.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Максимален брой обекти за обработка. Задайте 0, за да обработите всички обекти.", + "Maximum number of retry attempts for failed deliveries" : "Максимален брой опити за повторение при неуспешни доставки", + "Maximum time to wait before committing changes" : "Максимално време за изчакване преди подаване на промените", + "Maximum tokens to generate" : "Максимален брой токени за генериране", + "Maximum value constraint is missing" : "Липсва ограничение за максимална стойност", + "Maximum value is too restrictive" : "Максималната стойност е твърде ограничителна", + "Medium" : "Средно", + "Medium Confidence" : "Средна достоверност", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Средни заявки: търсения с известно филтриране или множество параметри (напр. диапазони от дати, специфични регистри/схеми)", + "Members of selected groups can access this view" : "Членовете на избраните групи могат да получат достъп до този изглед", + "Members:" : "Членове:", + "Memory prediction calculated successfully" : "Прогнозата за памет е изчислена успешно", + "Message content is required" : "Съдържанието на съобщението е задължително", + "Message does not belong to this conversation" : "Съобщението не принадлежи на този разговор", + "Metadata Filters" : "Филтри за метаданни", + "Method" : "Метод", + "Min Length" : "Минимална дължина", + "Min execution time (ms)" : "Минимално време за изпълнение (мс)", + "Min ms" : "Мин. мс", + "Min result count" : "Минимален брой резултати", + "Min results" : "Минимум резултати", + "Minimum" : "Минимум", + "Minimum value constraint is missing" : "Липсва ограничение за минимална стойност", + "Minimum value is too restrictive" : "Минималната стойност е твърде ограничителна", + "Missing object fields created successfully" : "Липсващите полета на обекта са създадени успешно", + "Mode:" : "Режим:", + "Model" : "Модел", + "Model Name" : "Име на модела", + "Modified" : "Променено", + "Monthly" : "Месечно", + "More sources (10-20):" : "Повече източници (10-20):", + "Most Active Objects" : "Най-активни обекти", + "Multiple Of" : "Кратно на", + "Name" : "Име", + "Names cache warmup completed" : "Подгряването на кеша с имена завърши", + "New Collection Name" : "Име на нова колекция", + "New Conversation" : "Нов разговор", + "New Properties" : "Нови свойства", + "New Property" : "Ново свойство", + "New Value" : "Нова стойност", + "New users without specific organisation membership will be automatically added to this organisation" : "Новите потребители без специфично членство в организация ще бъдат автоматично добавени към тази организация", + "Next" : "Напред", + "No" : "Не", + "No ConfigSets found" : "Не са намерени ConfigSets", + "No GitHub token configured" : "Не е конфигуриран GitHub токен", + "No GitHub token provided" : "Не е предоставен GitHub токен", + "No GitLab token provided" : "Не е предоставен GitLab токен", + "No Organisation" : "Без организация", + "No active filters" : "Няма активни филтри", + "No activity data available" : "Няма налични данни за активност", + "No agents available" : "Няма налични агенти", + "No agents found" : "Не са намерени агенти", + "No app store cache exists yet" : "Все още не съществува кеш на магазина за приложения", + "No applications are available." : "Няма налични приложения.", + "No applications found" : "Не са намерени приложения", + "No archived conversations" : "Няма архивирани разговори", + "No audit trail entries found" : "Не са намерени записи на одитни следи", + "No changes recorded" : "Не са записани промени", + "No chunks to vectorize" : "Няма фрагменти за векторизация", + "No collections found" : "Не са намерени колекции", + "No configuration data" : "Няма данни за конфигурацията", + "No configurations are available." : "Няма налични конфигурации.", + "No configurations found" : "Не са намерени конфигурации", + "No configurations found for this application." : "Не са намерени конфигурации за това приложение.", + "No conversations yet. Create a new one to get started!" : "Все още няма разговори. Създайте нов, за да започнете!", + "No data available" : "Няма налични данни", + "No data available for chart" : "Няма налични данни за диаграмата", + "No deleted items found" : "Не са намерени изтрити елементи", + "No deletion data available" : "Няма налични данни за изтриване", + "No description found" : "Не е намерено описание", + "No description provided" : "Не е предоставено описание", + "No entities detected in this file. Run text extraction first." : "Не са открити субекти в този файл. Първо стартирайте извличане на текст.", + "No entities found" : "Не са намерени субекти", + "No entities have been detected yet" : "Все още не са открити субекти", + "No events found" : "Не са намерени събития", + "No expired audit trails found to clear" : "Не са намерени изтекли одитни следи за изчистване", + "No facetable fields available. Select a register and schema to see available filters." : "Няма налични полета за фасетиране. Изберете регистър и схема, за да видите наличните филтри.", + "No files found" : "Не са намерени файлове", + "No files have been extracted yet" : "Все още не са извлечени файлове", + "No files to process" : "Няма файлове за обработка", + "No files to reindex" : "Няма файлове за повторно индексиране", + "No filters are currently active. This will delete ALL audit trail entries!" : "В момента няма активни филтри. Това ще изтрие ВСИЧКИ записи на одитни следи!", + "No log entries found" : "Не са намерени записи в дневника", + "No logs are available for this configuration." : "Няма налични дневници за тази конфигурация.", + "No logs are available for this source." : "Няма налични дневници за този източник.", + "No logs found" : "Не са намерени дневници", + "No mismatched fields found - SOLR schema is properly configured" : "Не са намерени несъответстващи полета - SOLR схемата е правилно конфигурирана", + "No objects found" : "Не са намерени обекти", + "No objects found to vectorize" : "Не са намерени обекти за векторизация", + "No objects selected for deletion" : "Не са избрани обекти за изтриване", + "No objects to delete" : "Няма обекти за изтриване", + "No objects were permanently deleted" : "Не бяха окончателно изтрити обекти", + "No properties found" : "Не са намерени свойства", + "No properties match your filters." : "Няма свойства, отговарящи на Вашите филтри.", + "No purge date set" : "Не е зададена дата за изчистване", + "No register data available" : "Няма налични данни за регистъра", + "No registers found" : "Не са намерени регистри", + "No registers found for this application." : "Не са намерени регистри за това приложение.", + "No relations found" : "Не са намерени връзки", + "No request body available" : "Няма налично тяло на заявката", + "No response body available" : "Няма налично тяло на отговора", + "No saved views yet. Create one in the Search tab!" : "Все още няма запазени изгледи. Създайте един в раздела Търсене!", + "No schema data available" : "Няма налични данни за схемата", + "No schema selected for exploration" : "Не е избрана схема за проучване", + "No schemas found" : "Не са намерени схеми", + "No schemas found for this application." : "Не са намерени схеми за това приложение.", + "No search terms data available" : "Няма налични данни за термини за търсене", + "No search trail entries found" : "Не са намерени записи на следи на търсене", + "No sources are available." : "Няма налични източници.", + "No sources found" : "Не са намерени източници", + "No templates found" : "Не са намерени шаблони", + "No templates have been created yet" : "Все още не са създадени шаблони", + "No valid magic-mapped register+schema combinations found" : "Не са намерени валидни комбинации регистър+схема с магическо съпоставяне", + "No views found. Create views first before configuring vectorization." : "Не са намерени изгледи. Първо създайте изгледи, преди да конфигурирате векторизация.", + "No views match your search" : "Няма изгледи, отговарящи на Вашето търсене", + "No webhooks found" : "Не са намерени уебхукове", + "No webhooks have been configured yet" : "Все още не са конфигурирани уебхукове", + "No workflows found in this project. Create workflows in the n8n editor." : "Не са намерени работни потоци в този проект. Създайте работни потоци в редактора на n8n.", + "None" : "Няма", + "Not helpful" : "Не е полезно", + "Not in use" : "Не е в употреба", + "Number Constraints" : "Числови ограничения", + "Number of chunks to vectorize in one API call (1-100)." : "Брой фрагменти за векторизация в едно API повикване (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Брой фрагменти за векторизация в едно API повикване. По-висок = по-бързо, но повече памет. Препоръчително: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Брой обекти за обработка във всяка партида (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Брой обекти за векторизация в едно API повикване. По-висок = по-бързо, но повече памет. Препоръчително: 10-50.", + "Numeric Range:" : "Числов диапазон:", + "Object" : "Обект", + "Object Collection" : "Колекция от обекти", + "Object Count Prediction" : "Прогноза за брой обекти", + "Object ID" : "Идентификатор на обект", + "Object Management" : "Управление на обекти", + "Object Vectorization" : "Векторизация на обекти", + "Object Warmup" : "Подгряване на обекти", + "Object collection not configured" : "Колекцията от обекти не е конфигурирана", + "Object not found" : "Обектът не е намерен", + "Object permanently deleted" : "Обектът е окончателно изтрит", + "Object restored successfully" : "Обектът е възстановен успешно", + "Object settings updated successfully" : "Настройките на обекта са актуализирани успешно", + "Object sources" : "Източници на обекти", + "Object unlocked successfully" : "Обектът е отключен успешно", + "Object vectorization configuration saved successfully" : "Конфигурацията за векторизация на обекти е запазена успешно", + "Object vectorized successfully" : "Обектът е векторизиран успешно", + "Object:" : "Обект:", + "Objects" : "Обекти", + "Objects Analyzed" : "Анализирани обекти", + "Objects Distribution" : "Разпределение на обектите", + "Objects being analyzed" : "Обекти, които се анализират", + "Objects by Register" : "Обекти по регистър", + "Objects by Schema" : "Обекти по схема", + "Objects deletion completed successfully" : "Изтриването на обектите завърши успешно", + "Objects in schema" : "Обекти в схемата", + "Objects to Process:" : "Обекти за обработка:", + "Objects to be analyzed" : "Обекти за анализиране", + "Objects to be deleted" : "Обекти за изтриване", + "Objects to be validated" : "Обекти за валидиране", + "Objects will be serialized as JSON text before vectorization" : "Обектите ще бъдат сериализирани като JSON текст преди векторизация", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Обектите ще бъдат меко изтрити (маркирани като изтрити, но запазени в базата данни). Те могат да бъдат възстановени по-късно, ако е необходимо.", + "Old Value" : "Стара стойност", + "Oldest Item (days)" : "Най-стар елемент (дни)", + "Ollama Chat Settings" : "Настройки за чат на Ollama", + "Ollama Embedding Configuration" : "Конфигурация за вграждания на Ollama", + "Ollama URL" : "URL на Ollama", + "Only With Changes" : "Само с промени", + "Open Register Objects" : "Обекти на Open Register", + "Open n8n Editor" : "Отваряне на редактора на n8n", + "OpenAI Chat Settings" : "Настройки за чат на OpenAI", + "OpenAI Embedding Configuration" : "Конфигурация за вграждания на OpenAI", + "OpenRegister Settings" : "Настройки на OpenRegister", + "Optional webhook secret for signature verification" : "Незадължителна тайна за уебхук за проверка на подпис", + "Order" : "Ред", + "Organisation" : "Организация", + "Organisation ID" : "Идентификатор на организация", + "Organisation Members" : "Членове на организацията", + "Organisation Statistics" : "Статистика на организацията", + "Organisation created successfully" : "Организацията е създадена успешно", + "Organisation settings saved successfully" : "Настройките на организацията са запазени успешно", + "Organisations" : "Организации", + "Organization" : "Организация", + "Organization ID (Optional)" : "Идентификатор на организация (по избор)", + "Orphaned Items" : "Осиротели елементи", + "Owner" : "Собственик", + "Owner:" : "Собственик:", + "Parallel Mode (Faster, more resource intensive)" : "Паралелен режим (по-бърз, по-ресурсоемък)", + "Parallel:" : "Паралелно:", + "Password" : "Парола", + "Password for SOLR authentication (optional)" : "Парола за SOLR удостоверяване (по избор)", + "Path" : "Път", + "Path in repository" : "Път в хранилището", + "Path where the configuration file will be saved in the repository" : "Път, където конфигурационният файл ще бъде запазен в хранилището", + "Path where the register OAS file will be saved in the repository" : "Път, където OAS файлът на регистъра ще бъде запазен в хранилището", + "Pattern (regex)" : "Шаблон (regex)", + "Pattern Issue" : "Проблем с шаблон", + "Pattern constraint is missing" : "Липсва ограничение за шаблон", + "Patterns:" : "Шаблони:", + "Pending" : "В очакване", + "Permanent Delete Mode" : "Режим на окончателно изтриване", + "Permanently Delete" : "Окончателно изтриване", + "Permanently delete already soft-deleted objects" : "Окончателно изтриване на вече меко изтрити обекти", + "Permanently delete this conversation?" : "Окончателно изтриване на този разговор?", + "Person" : "Лице", + "Personal Data" : "Лични данни", + "Phone" : "Телефон", + "Photos" : "Снимки", + "Please create an agent in the" : "Моля, създайте агент в", + "Please select an agent to continue" : "Моля, изберете агент, за да продължите", + "Please select which register and schema to use for the new object" : "Моля, изберете кои регистър и схема да използвате за новия обект", + "Please try again later." : "Моля, опитайте отново по-късно.", + "Please wait while we fetch your agents." : "Моля, изчакайте, докато извличаме Вашите агенти.", + "Please wait while we fetch your applications." : "Моля, изчакайте, докато извличаме Вашите приложения.", + "Please wait while we fetch your configurations." : "Моля, изчакайте, докато извличаме Вашите конфигурации.", + "Please wait while we fetch your deleted items." : "Моля, изчакайте, докато извличаме Вашите изтрити елементи.", + "Please wait while we fetch your sources." : "Моля, изчакайте, докато извличаме Вашите източници.", + "Polls" : "Анкети", + "Popular Search Terms" : "Популярни термини за търсене", + "Port" : "Порт", + "Prerequisites" : "Предпоставки", + "Press Enter to send, Shift+Enter for new line" : "Натиснете Enter за изпращане, Shift+Enter за нов ред", + "Previous" : "Предишен", + "Private" : "Личен", + "Processes chunks in batches with simulated parallelism." : "Обработва фрагменти на партиди със симулиран паралелизъм.", + "Processes file chunks sequentially (safest)." : "Обработва фрагментите на файла последователно (най-безопасно).", + "Processes objects in chunks with simulated parallelism." : "Обработва обектите на фрагменти със симулиран паралелизъм.", + "Processes objects sequentially (safest)." : "Обработва обектите последователно (най-безопасно).", + "Processing" : "Обработка", + "Processing Limits" : "Лимити за обработка", + "Processing..." : "Обработка...", + "Project initialized successfully" : "Проектът е инициализиран успешно", + "Project not found. Please initialize first." : "Проектът не е намерен. Моля, първо инициализирайте.", + "Properties" : "Свойства", + "Property Behaviors" : "Поведения на свойствата", + "Property Configuration:" : "Конфигурация на свойството:", + "Property Filters" : "Филтри за свойства", + "Property Title" : "Заглавие на свойството", + "Property Type" : "Тип на свойството", + "Property can be improved" : "Свойството може да бъде подобрено", + "Protected" : "Защитено", + "Provider" : "Доставчик", + "Provider is required for testing" : "Доставчикът е задължителен за тестване", + "Public" : "Публичен", + "Public View" : "Публичен изглед", + "Public views can be accessed by anyone in the system" : "До публичните изгледи може да получи достъп всеки в системата", + "Publish" : "Публикуване", + "Publish Objects" : "Публикуване на обекти", + "Publish all objects in this schema" : "Публикуване на всички обекти в тази схема", + "Published" : "Публикувано", + "Published:" : "Публикувано:", + "Publishing..." : "Публикуване...", + "Purge" : "Изчистване", + "Purge Date" : "Дата на изчистване", + "Query Complexity Distribution" : "Разпределение на сложността на заявките", + "Query parameter is required" : "Параметърът на заявката е задължителен", + "RAG" : "RAG", + "RAG Configuration" : "RAG конфигурация", + "Rate limits cleared successfully" : "Ограниченията на честотата са изчистени успешно", + "Raw Changes Data" : "Сурови данни за промените", + "Re-vectorize on object update" : "Повторна векторизация при актуализиране на обект", + "Read" : "Четене", + "Recommendations:" : "Препоръки:", + "Recommended Type:" : "Препоръчителен тип:", + "Recommended: 5 sources" : "Препоръчително: 5 източника", + "Refresh" : "Опресняване", + "Refresh Data" : "Опресняване на данните", + "Refresh Stats" : "Опресняване на статистиката", + "Refresh Workflows" : "Опресняване на работните потоци", + "Refresh dashboard" : "Опресняване на таблото", + "Refresh database info" : "Опресняване на информацията за базата данни", + "Register" : "Регистър", + "Register ID" : "Идентификатор на регистър", + "Register Statistics" : "Статистика на регистъра", + "Register Totals" : "Общи стойности на регистъра", + "Register depublished successfully" : "Регистърът е депубликуван успешно", + "Register not found" : "Регистърът не е намерен", + "Register objects deletion completed successfully" : "Изтриването на обектите на регистъра завърши успешно", + "Register published successfully" : "Регистърът е публикуван успешно", + "Register/Schema Usage" : "Употреба на регистър/схема", + "Registers" : "Регистри", + "Reindex" : "Повторно индексиране", + "Reindex completed" : "Повторното индексиране завърши", + "Reindex completed successfully" : "Повторното индексиране завърши успешно", + "Reindex failed" : "Повторното индексиране е неуспешно", + "Relations" : "Връзки", + "Remote Version" : "Отдалечена версия", + "Remove" : "Премахване", + "Remove filter" : "Премахване на филтър", + "Remove from favorites" : "Премахване от любими", + "Remove group" : "Премахване на група", + "Removed" : "Премахнато", + "Removed from favorites" : "Премахнато от любими", + "Rename Conversation" : "Преименуване на разговор", + "Rename conversation" : "Преименуване на разговор", + "Replicas" : "Реплики", + "Repository" : "Хранилище", + "Request Body" : "Тяло на заявката", + "Request Data" : "Данни на заявката", + "Request Details" : "Детайли на заявката", + "Request body must be JSON with a \"query\" field" : "Тялото на заявката трябва да бъде JSON с поле \"query\"", + "Request timeout in seconds" : "Таймаут на заявката в секунди", + "Required field" : "Задължително поле", + "Required status is inconsistent" : "Задължителният статус е непоследователен", + "Rerun Search" : "Повторно изпълнение на търсенето", + "Reset Changes" : "Нулиране на промените", + "Reset Filters" : "Нулиране на филтрите", + "Response Body" : "Тяло на отговора", + "Response Details" : "Детайли на отговора", + "Response Time:" : "Време за отговор:", + "Restore" : "Възстановяване", + "Restore conversation" : "Възстановяване на разговор", + "Restore or permanently delete items" : "Възстановяване или окончателно изтриване на елементи", + "Result Count Range" : "Диапазон на броя резултати", + "Results" : "Резултати", + "Retrieve all objects for this schema" : "Извличане на всички обекти за тази схема", + "Retry" : "Повторение", + "Retry Failed Extractions" : "Повторение на неуспешните извличания", + "Retry Policy" : "Политика за повторение", + "Retry completed" : "Повторението завърши", + "Retrying..." : "Повторение...", + "Risk Level" : "Ниво на риск", + "Roles updated successfully" : "Ролите са актуализирани успешно", + "SOLR Connection Settings" : "Настройки за SOLR връзка", + "SOLR Version:" : "Версия на SOLR:", + "SOLR actions menu" : "Меню за SOLR действия", + "SOLR base path (usually /solr)" : "Базов път на SOLR (обикновено /solr)", + "SOLR is disabled" : "SOLR е деактивиран", + "SOLR is not available or not configured" : "SOLR не е наличен или не е конфигуриран", + "SOLR logging disabled" : "SOLR логването е деактивирано", + "SOLR logging enabled" : "SOLR логването е активирано", + "SOLR search disabled" : "SOLR търсенето е деактивирано", + "SOLR search enabled" : "SOLR търсенето е активирано", + "SOLR server hostname or IP address" : "Име на хост или IP адрес на SOLR сървъра", + "SOLR server port number (optional, defaults to 8983)" : "Номер на порт на SOLR сървъра (по избор, по подразбиране 8983)", + "SOLR setup completed successfully" : "Настройката на SOLR завърши успешно", + "SOLR setup failed" : "Настройката на SOLR е неуспешна", + "SOLR setup failed - check logs" : "Настройката на SOLR е неуспешна - проверете дневниците", + "Sample Values:" : "Примерни стойности:", + "Save" : "Запазване", + "Save Configuration" : "Запазване на конфигурацията", + "Save Connection Settings" : "Запазване на настройките за връзка", + "Save Roles" : "Запазване на ролите", + "Save Settings" : "Запазване на настройките", + "Save current search as view" : "Запазване на текущото търсене като изглед", + "Saved Views" : "Запазени изгледи", + "Saving..." : "Запазване...", + "Schema" : "Схема", + "Schema ID" : "Идентификатор на схема", + "Schema Name" : "Име на схемата", + "Schema Statistics" : "Статистика на схемата", + "Schema depublished successfully" : "Схемата е депубликувана успешно", + "Schema objects deletion completed successfully" : "Изтриването на обектите на схемата завърши успешно", + "Schema published successfully" : "Схемата е публикувана успешно", + "Schema successfully created" : "Схемата е създадена успешно", + "Schema successfully updated" : "Схемата е актуализирана успешно", + "Schemas" : "Схеми", + "Scheme" : "Схема", + "Search / Views" : "Търсене / Изгледи", + "Search Activity" : "Активност на търсене", + "Search Analytics" : "Анализи на търсене", + "Search Entities" : "Търсене на субекти", + "Search Files" : "Търсене на файлове", + "Search GitHub" : "Търсене в GitHub", + "Search GitLab" : "Търсене в GitLab", + "Search Method" : "Метод за търсене", + "Search Term" : "Термин за търсене", + "Search Trail Details" : "Детайли на следата на търсене", + "Search Trail Management" : "Управление на следите на търсене", + "Search Trail Statistics" : "Статистика на следите на търсене", + "Search Trails" : "Следи на търсене", + "Search Views" : "Търсене на изгледи", + "Search Webhooks" : "Търсене на уебхукове", + "Search by file name or path" : "Търсене по име на файл или път", + "Search by name or URL" : "Търсене по име или URL", + "Search by value" : "Търсене по стойност", + "Search in Files" : "Търсене във файлове", + "Search in Objects" : "Търсене в обекти", + "Search property names..." : "Търсене на имена на свойства...", + "Search trail deletion not implemented yet" : "Изтриването на следи на търсене все още не е реализирано", + "Search views..." : "Търсене на изгледи...", + "Secret" : "Тайна", + "Secret key for HMAC signature generation (optional)" : "Таен ключ за генериране на HMAC подпис (по избор)", + "Select AI Agent" : "Изберете AI агент", + "Select All" : "Избор на всички", + "Select ConfigSet" : "Изберете ConfigSet", + "Select File Types to Vectorize:" : "Изберете типове файлове за векторизация:", + "Select HTTP method" : "Изберете HTTP метод", + "Select Register and Schema" : "Изберете Регистър и Схема", + "Select Views to Vectorize:" : "Изберете изгледи за векторизация:", + "Select a Nextcloud group to add" : "Изберете група в Nextcloud за добавяне", + "Select a branch" : "Изберете клон", + "Select a model or type a custom model name" : "Изберете модел или въведете персонализирано име на модел", + "Select a repository" : "Изберете хранилище", + "Select a repository you have write access to" : "Изберете хранилище, до което имате достъп за запис", + "Select an AI Agent" : "Изберете AI агент", + "Select an AI agent to begin chatting with your data." : "Изберете AI агент, за да започнете да чатите с Вашите данни.", + "Select backend" : "Изберете бекенд", + "Select chat model" : "Изберете чат модел", + "Select collection for files" : "Изберете колекция за файлове", + "Select collection for objects" : "Изберете колекция за обекти", + "Select default organisation" : "Изберете организация по подразбиране", + "Select event to listen to..." : "Изберете събитие за прослушване...", + "Select model" : "Изберете модел", + "Select options..." : "Изберете опции...", + "Select period" : "Изберете период", + "Select property to send as payload" : "Изберете свойство за изпращане като полезен товар", + "Select provider" : "Изберете доставчик", + "Select registers and schemas to save a view" : "Изберете регистри и схеми, за да запазите изглед", + "Select retry policy" : "Изберете политика за повторение", + "Select the branch to publish to" : "Изберете клона, в който да публикувате", + "Select the event this webhook should listen to" : "Изберете събитието, което този уебхук трябва да прослушва", + "Select views to vectorize:" : "Изберете изгледи за векторизация:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Изберете кои групи в Nextcloud са налични за тази организация. Потребителите в тези групи ще имат достъп до ресурсите на организацията.", + "Select which data views the AI can search" : "Изберете кои изгледи на данни може да търси AI", + "Select which property from the event should be used as the webhook payload data" : "Изберете кое свойство от събитието да се използва като данни за полезния товар на уебхука", + "Select which tools the AI can use to perform actions" : "Изберете кои инструменти може да използва AI за извършване на действия", + "Selected" : "Избрано", + "Selected Groups" : "Избрани групи", + "Selected audit trails deleted successfully" : "Избраните одитни следи са изтрити успешно", + "Selected search trails deleted successfully" : "Избраните следи на търсене са изтрити успешно", + "Selected users can access this view" : "Избраните потребители могат да получат достъп до този изглед", + "Send additional feedback" : "Изпращане на допълнителна обратна връзка", + "Send as CloudEvent" : "Изпращане като CloudEvent", + "Sensitive PII" : "Чувствителни лични данни", + "Serial Mode (Safer, slower)" : "Сериен режим (по-безопасен, по-бавен)", + "Serial:" : "Серийно:", + "Server Configuration" : "Конфигурация на сървъра", + "Server Information" : "Информация за сървъра", + "Settings" : "Настройки", + "Shards" : "Фрагменти", + "Share" : "Споделяне", + "Share with Groups" : "Споделяне с групи", + "Share with Users" : "Споделяне с потребители", + "Show Filters" : "Показване на филтрите", + "Show less" : "Показване на по-малко", + "Show only entries with changes" : "Показване само на записи с промени", + "Simple" : "Просто", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Прости заявки: основни текстови търсения с минимални параметри (напр. единичен термин за търсене, без разширени филтри)", + "Size" : "Размер", + "Slug" : "Slug", + "Soft Delete Mode" : "Режим на меко изтриване", + "Soft Deleted Items" : "Меко изтрити елементи", + "SolrCloud mode enabled" : "Режимът SolrCloud е активиран", + "Source" : "Източник", + "Sources" : "Източници", + "Sources:" : "Източници:", + "Standalone SOLR mode" : "Самостоятелен режим на SOLR", + "Start Conversation" : "Започване на разговор", + "Start Vectorization" : "Започване на векторизация", + "Start a conversation" : "Започнете разговор", + "Starting names cache warmup..." : "Започване на подгряване на кеша с имена...", + "Starting..." : "Започване...", + "Statistics" : "Статистика", + "Stats calculation not yet implemented" : "Изчисляването на статистиката все още не е реализирано", + "Status" : "Статус", + "Status Code" : "Код на статуса", + "String Constraints" : "Ограничения за низове", + "Success" : "Успех", + "Success Rate" : "Процент на успех", + "Success Status" : "Статус на успех", + "Successful" : "Успешно", + "Successfully joined organisation" : "Успешно присъединяване към организация", + "Sync Table" : "Синхронизиране на таблица", + "Synchronization Settings" : "Настройки за синхронизация", + "System Default" : "Системно по подразбиране", + "Technical Description" : "Техническо описание", + "Technical description for developers and administrators" : "Техническо описание за разработчици и администратори", + "Temperature" : "Температура", + "Templates" : "Шаблони", + "Test" : "Тест", + "Test Chat" : "Тестване на чат", + "Test Connection" : "Тестване на връзката", + "Test Embedding" : "Тестване на вграждане", + "Test endpoint executed successfully" : "Тестовата крайна точка е изпълнена успешно", + "Test webhook delivered successfully" : "Тестовият уебхук е доставен успешно", + "Test webhook delivery failed" : "Доставката на тестовия уебхук е неуспешна", + "Test webhook sent successfully" : "Тестовият уебхук е изпратен успешно", + "Testing..." : "Тестване...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Текстовите фрагменти се генерират по време на извличането на файла и се съхраняват в базата данни. Векторизацията чете тези предварително фрагментирани файлове и ги преобразува във вграждания.", + "Text extracted successfully" : "Текстът е извлечен успешно", + "Text extraction disabled" : "Извличането на текст е деактивирано", + "The URL where webhook events will be sent" : "URL адресът, на който ще бъдат изпращани събитията на уебхука", + "The requested conversation does not exist" : "Заявеният разговор не съществува", + "The saved view and all its search configuration" : "Запазеният изглед и цялата му конфигурация за търсене", + "There are no audit trail entries matching your current filters." : "Няма записи на одитни следи, отговарящи на текущите Ви филтри.", + "There are no deleted items matching your current filters." : "Няма изтрити елементи, отговарящи на текущите Ви филтри.", + "There are no search trail entries matching your current filters." : "Няма записи на следи на търсене, отговарящи на текущите Ви филтри.", + "There are no webhook log entries matching your filters." : "Няма записи в дневника на уебхука, отговарящи на Вашите филтри.", + "This action cannot be undone." : "Това действие не може да бъде отменено.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Това действие не може да бъде отменено. Уверете се, че никакви колекции не използват този ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Този агент няма конфигурируеми изгледи или инструменти, но все пак можете да коригирате настройките на RAG по-долу.", + "This analysis may take some time" : "Този анализ може да отнеме известно време", + "This audit trail entry does not contain any change information." : "Този запис на одитна следа не съдържа информация за промени.", + "This configuration has no data defined." : "Тази конфигурация няма дефинирани данни.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Тази крайна точка е остаряла. Вместо това използвайте крайни точки, базирани на фрагменти.", + "This entity has no relations to objects or files" : "Този субект няма връзки с обекти или файлове", + "This is a test webhook from OpenRegister" : "Това е тестов уебхук от OpenRegister", + "This schema must use Magic Table configuration to sync" : "Тази схема трябва да използва конфигурация Magic Table за синхронизиране", + "This source has no associated registers." : "Този източник няма свързани регистри.", + "This will copy the _default ConfigSet with the new name" : "Това ще копира ConfigSet _default с новото име", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Това ще изтрие окончателно ВСИЧКИ вграждания (вектори) от базата данни. Ще трябва да векторизирате повторно всички обекти и файлове. Това действие не може да бъде отменено.\\n\\nСигурни ли сте, че искате да продължите?", + "This will permanently delete:" : "Това ще изтрие окончателно:", + "Timeout (seconds)" : "Таймаут (секунди)", + "Timestamp" : "Времеви маркер", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Съвет: Активирайте само изгледи, които се нуждаят от семантично търсене, за да минимизирате разходите за вграждания. Простите справочни таблици рядко се нуждаят от векторизация.", + "Title" : "Заглавие", + "To Date" : "До дата", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "За да коригирате размера или стратегията на фрагментите, отидете на Конфигурация на файлове → Лимити за обработка.", + "To change the embedding provider or model, go to LLM Configuration." : "За да промените доставчика на вграждания или модела, отидете на LLM конфигурация.", + "To date" : "До дата", + "Toggle search sidebar" : "Превключване на страничната лента за търсене", + "Tools" : "Инструменти", + "Top Deleters" : "Най-активни изтриващи", + "Total" : "Общо", + "Total Audit Trails" : "Общо одитни следи", + "Total Chunks Available:" : "Общо налични фрагменти:", + "Total Deleted Items" : "Общо изтрити елементи", + "Total Members" : "Общо членове", + "Total Objects" : "Общо обекти", + "Total Objects in Database:" : "Общо обекти в базата данни:", + "Total Organisations" : "Общо организации", + "Total Results" : "Общо резултати", + "Total Searches" : "Общо търсения", + "Total Size" : "Общ размер", + "Total Storage" : "Общо съхранение", + "Total:" : "Общо:", + "Totals" : "Общи стойности", + "Type" : "Тип", + "Type Issue" : "Проблем с типа", + "Type Variations:" : "Вариации на типа:", + "Type must be \"positive\" or \"negative\"" : "Типът трябва да бъде \"positive\" или \"negative\"", + "Type to search for groups" : "Въведете, за да търсите групи", + "Type to search for users" : "Въведете, за да търсите потребители", + "Type to search groups..." : "Въведете, за да търсите групи...", + "Type to search users..." : "Въведете, за да търсите потребители...", + "URL" : "URL", + "URL where Ollama is running" : "URL, на който работи Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Непроменено", + "Unique Search Terms" : "Уникални термини за търсене", + "Unique Terms" : "Уникални термини", + "Unknown" : "Неизвестно", + "Unknown Register" : "Неизвестен регистър", + "Unknown Schema" : "Неизвестна схема", + "Untitled View" : "Изглед без заглавие", + "Update" : "Актуализиране", + "Update Operations" : "Операции за актуализиране", + "Update configuration: ..." : "Актуализиране на конфигурацията: ...", + "Update register OAS: ..." : "Актуализиране на OAS на регистъра: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Актуализиране на векторите при промяна на данните на обекта (препоръчително за точно търсене)", + "Updated" : "Актуализирано", + "Updated At" : "Актуализирано на", + "Updated:" : "Актуализирано:", + "Uptime:" : "Време на работа:", + "Usage Count" : "Брой употреби", + "Use SolrCloud with Zookeeper for distributed search" : "Използвайте SolrCloud със Zookeeper за разпределено търсене", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Използвайте филтри, за да стесните записите на одитни следи по регистър, схема, тип действие, потребител, диапазон от дати или идентификатор на обект.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Използвайте филтри, за да стесните изтритите елементи по регистър, схема, дата на изтриване или потребителя, който ги е изтрил.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Използвайте филтри, за да стесните записите на следи на търсене по регистър, схема, статус на успех, потребител, диапазон от дати, термини за търсене или показатели за производителност.", + "Used By Collections" : "Използвано от колекции", + "User" : "Потребител", + "User Agent Statistics" : "Статистика на потребителския агент", + "User Description" : "Потребителско описание", + "User rate limits cleared successfully" : "Ограниченията на честотата на потребителя са изчистени успешно", + "User removed successfully" : "Потребителят е премахнат успешно", + "User-friendly description shown in forms and help text" : "Удобно за потребителя описание, показвано във формуляри и помощен текст", + "Username" : "Потребителско име", + "Username for SOLR authentication (optional)" : "Потребителско име за SOLR удостоверяване (по избор)", + "Users" : "Потребители", + "Using Pre-Generated Chunks" : "Използване на предварително генерирани фрагменти", + "Validate" : "Валидиране", + "Validate Objects" : "Валидиране на обекти", + "Validation completed successfully" : "Валидирането завърши успешно", + "Validation failed" : "Валидирането е неуспешно", + "Value" : "Стойност", + "Vector Dimensions" : "Векторни измерения", + "Vector Search Backend" : "Бекенд за векторно търсене", + "Vector field: _embedding_" : "Векторно поле: _embedding_", + "Vectorization Triggers" : "Тригери за векторизация", + "Vectorize All Files" : "Векторизиране на всички файлове", + "Vectorize All Objects" : "Векторизиране на всички обекти", + "Vectorize all file types" : "Векторизиране на всички типове файлове", + "Vectorize all views" : "Векторизиране на всички изгледи", + "Vectorize on object creation" : "Векторизиране при създаване на обект", + "Vectors will be stored in your existing object and file collections" : "Векторите ще бъдат съхранени във Вашите съществуващи колекции от обекти и файлове", + "Version" : "Версия", + "Very High" : "Много високо", + "View API Docs" : "Преглед на API документацията", + "View Changes" : "Преглед на промените", + "View Details" : "Преглед на детайлите", + "View Error" : "Преглед на грешката", + "View File" : "Преглед на файла", + "View Full Details" : "Преглед на пълните детайли", + "View Logs" : "Преглед на дневниците", + "View Name" : "Име на изгледа", + "View Object" : "Преглед на обекта", + "View Parameters" : "Преглед на параметрите", + "View Selection" : "Избор на изглед", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Преглеждайте и анализирайте дневниците на следите на търсене с разширени възможности за филтриране и анализ", + "View and analyze system audit trails with advanced filtering capabilities" : "Преглеждайте и анализирайте системните одитни следи с разширени възможности за филтриране", + "View deleted successfully" : "Изгледът е изтрит успешно", + "View entity details and manage relations" : "Преглед на детайлите на субекта и управление на връзките", + "View name" : "Име на изгледа", + "View name is required" : "Името на изгледа е задължително", + "View saved successfully!" : "Изгледът е запазен успешно!", + "View search analytics and manage search logs" : "Преглед на анализите на търсене и управление на дневниците на търсене", + "View successfully deleted" : "Изгледът е изтрит успешно", + "View successfully updated" : "Изгледът е актуализиран успешно", + "View updated successfully!" : "Изгледът е актуализиран успешно!", + "View webhook delivery logs and filter by webhook" : "Преглед на дневниците за доставка на уебхук и филтриране по уебхук", + "Views" : "Изгледи", + "Visible to users" : "Видимо за потребителите", + "Wait for Response" : "Изчакване на отговор", + "Wait for webhook response before continuing (required for request/response flows)" : "Изчакване на отговора на уебхука преди продължаване (задължително за потоци заявка/отговор)", + "Warmup Names Cache" : "Подгряване на кеша с имена", + "Warning" : "Предупреждение", + "Warning:" : "Предупреждение:", + "Webhook" : "Уебхук", + "Webhook Log Details" : "Детайли на дневника на уебхука", + "Webhook Logs" : "Дневници на уебхука", + "Webhook created successfully" : "Уебхукът е създаден успешно", + "Webhook deleted" : "Уебхукът е изтрит", + "Webhook retry delivered successfully" : "Повторението на уебхука е доставено успешно", + "Webhook retry delivery failed" : "Доставката на повторението на уебхука е неуспешна", + "Webhook updated" : "Уебхукът е актуализиран", + "Webhook updated successfully" : "Уебхукът е актуализиран успешно", + "Webhooks" : "Уебхукове", + "Weekly" : "Седмично", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Обвиване на полезния товар на уебхука във формат CloudEvents за по-добра оперативна съвместимост", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Да", + "You" : "Вие", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Можете да създавате нови ConfigSets въз основа на шаблона _default или да качвате персонализирани директно на Вашия SOLR сървър.", + "You do not have access to this conversation" : "Нямате достъп до този разговор", + "You do not have permission to delete this conversation" : "Нямате разрешение да изтриете този разговор", + "You do not have permission to modify this conversation" : "Нямате разрешение да променяте този разговор", + "You do not have permission to restore this conversation" : "Нямате разрешение да възстановите този разговор", + "You must be logged in to favorite views" : "Трябва да сте влезли, за да добавяте изгледи към любими", + "You need an AI agent to start a conversation." : "Нуждаете се от AI агент, за да започнете разговор.", + "Your Fireworks AI API key. Get one at" : "Вашият Fireworks AI API ключ. Вземете такъв на", + "Your OpenAI API key. Get one at" : "Вашият OpenAI API ключ. Вземете такъв на", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Вашата обратна връзка е записана. По избор можете да предоставите допълнителни детайли тук...", + "Zookeeper Hosts" : "Хостове на Zookeeper", + "Zookeeper Port" : "Порт на Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Настройки на Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Низ за връзка със Zookeeper за SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Номер на порт на Zookeeper (по избор, по подразбиране 2181)", + "chunk" : "фрагмент", + "chunks" : "фрагменти", + "chunks processed" : "обработени фрагменти", + "configuration(s)" : "конфигурация(и)", + "e.g., lib/Settings/config.json" : "напр. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "напр. lib/Settings/register.json", + "failed" : "неуспешно", + "file" : "файл", + "files" : "файлове", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "членове", + "menu or contact someone with permission to create agents." : "меню или се свържете с някого, който има разрешение да създава агенти.", + "messages" : "съобщения", + "n8n URL and API key are required" : "URL на n8n и API ключ са задължителни", + "n8n configuration saved successfully" : "Конфигурацията на n8n е запазена успешно", + "n8n connection not configured" : "Връзката с n8n не е конфигурирана", + "n8n connection successful" : "Връзката с n8n е успешна", + "n8n connection test successful" : "Тестът на връзката с n8n е успешен", + "n8n integration disabled" : "Интеграцията с n8n е деактивирана", + "n8n integration enabled" : "Интеграцията с n8n е активирана", + "n8n project initialized successfully" : "Проектът на n8n е инициализиран успешно", + "n8n settings saved successfully" : "Настройките на n8n са запазени успешно", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "обекти", + "objects processed" : "обработени обекти", + "or pick a range" : "или изберете диапазон", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "осигурява добър баланс между скорост и точност.", + "register(s)" : "регистър(и)", + "register(s) selected" : "регистър(и) избрани", + "required" : "задължително", + "results" : "резултати", + "schema(s)" : "схема(и)", + "schema(s) selected" : "схема(и) избрани", + "sk-..." : "sk-...", + "this application" : "това приложение", + "to" : "до", + "vectorized" : "векторизирано", + "views selected" : "избрани изгледи", + "ℹ️ Current Configuration" : "ℹ️ Текуща конфигурация", + "⚡ Batch Processing" : "⚡ Партидна обработка", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Изчерпателен контекст ✓ По-малка вероятност да пропусне детайли ✗ По-бавни отговори ✗ Може да включва по-малко релевантна информация", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ По-бързи отговори ✓ По-фокусирани отговори ✗ Може да пропусне релевантна информация", + "✨ AI Features" : "✨ AI функции", + "All search trails cleared successfully" : "Всички следи на търсене са изчистени успешно", + "Multiple search trail deletion not implemented yet" : "Изтриването на множество следи на търсене все още не е реализирано", + "No expired search trails found to clear" : "Не са намерени изтекли следи на търсене за изчистване", + "Missing message" : "Липсва съобщение", + "message content is required" : "съдържанието на съобщението е задължително", + "Missing conversation or agentUuid" : "Липсва разговор или agentUuid", + "Access denied" : "Достъпът е отказан", + "Conversation not found" : "Разговорът не е намерен", + "AI service not configured" : "AI услугата не е конфигурирана", + "Failed to process message" : "Неуспешна обработка на съобщението", + "Missing conversationId" : "Липсва conversationId", + "conversationId is required" : "conversationId е задължителен", + "Failed to fetch conversation history" : "Неуспешно извличане на историята на разговора", + "Failed to clear conversation" : "Неуспешно изчистване на разговора", + "Invalid feedback type" : "Невалиден тип обратна връзка", + "type must be \"positive\" or \"negative\"" : "типът трябва да бъде \"positive\" или \"negative\"", + "Message not found" : "Съобщението не е намерено", + "Failed to save feedback" : "Неуспешно запазване на обратната връзка", + "Failed to get chat statistics" : "Неуспешно получаване на статистиката на чата", + "Not Found" : "Не е намерено", + "Engine not found" : "Двигателят не е намерен", + "Consumer not found" : "Потребителят не е намерен", + "Backend parameter is required" : "Параметърът на бекенда е задължителен", + "Failed to get database information: %s" : "Неуспешно получаване на информация за базата данни: %s", + "SOLR setup error: %s" : "Грешка при настройка на SOLR: %s", + "Reindex failed: %s" : "Повторното индексиране е неуспешно: %s", + "Anonymized" : "Анонимизирано", + "Entities detected" : "Открити субекти", + "Extract Now" : "Извличане сега", + "Extracted at" : "Извлечено на", + "Extraction" : "Извличане", + "Failed to load extraction data" : "Неуспешно зареждане на данните за извличане", + "Failed to load register data" : "Неуспешно зареждане на данните за регистъра", + "Failed to retrieve extraction status." : "Неуспешно извличане на статуса на извличане.", + "Failed to retrieve objects for file." : "Неуспешно извличане на обектите за файла.", + "No extraction data available for this file" : "Няма налични данни за извличане за този файл", + "No register objects reference this file" : "Никакви обекти на регистъра не препращат към този файл", + "Not extracted" : "Не е извлечено", + "Register Objects" : "Обекти на регистъра", + "Risk level" : "Ниво на риск", + "Text chunks" : "Текстови фрагменти", + "Unknown error" : "Неизвестна грешка", + "Very high" : "Много високо", + "{title} in {register} / {schema}" : "{title} в {register} / {schema}", + "Push Notifications" : "Push известия", + "Real-time push notification status via notify_push" : "Статус на push известия в реално време чрез notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push в реално време не е наличен — приложението notify_push не е инсталирано", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Инсталирайте приложението notify_push от App Store на Nextcloud, за да активирате актуализации в реално време.", + "Open Nextcloud App Store" : "Отваряне на App Store на Nextcloud", + "notify_push is installed but not yet active" : "notify_push е инсталиран, но все още не е активен", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push е инсталиран, но OpenRegister все още не е потвърдил успешен push. Задействайте запазване на обект, за да активирате, или проверете конфигурацията на notify_push.", + "notify_push configuration guide" : "ръководство за конфигуриране на notify_push", + "Realtime push active" : "Push в реално време е активен", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push известията в реално време са активни. Свързаните клиенти получават незабавни актуализации при създаване, актуализиране или изтриване на обекти.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/bg.json b/l10n/bg.json new file mode 100644 index 0000000000..56cd659cbd --- /dev/null +++ b/l10n/bg.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(изисква се Dolphin)", + "(no subject)": "(без тема)", + "(no title)": "(без заглавие)", + "(unnamed)": "(без име)", + "(untitled card)": "(карта без заглавие)", + "({count} selected)": "({count} избрани)", + ", {failed} failed": ", {failed} неуспешни", + "0 = unlimited": "0 = неограничено", + "A deactivation request is pending.": "Заявка за деактивиране е в очакване.", + "AI Assistant": "AI асистент", + "AI Chat": "AI чат", + "AI service not configured": "AI услугата не е конфигурирана", + "API Key": "API ключ", + "API Request Quota (requests/day)": "Квота за API заявки (заявки/ден)", + "API Token Configuration": "Конфигурация на API токен", + "API Tokens": "API токени", + "API tokens saved successfully": "API токените са запазени успешно", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: този отчет свързва всяка дейност по обработка с общия брой записи в одитната следа, приписани към нея. Одиторите и Autoriteit Persoonsgegevens използват това за надзорна проверка.", + "About ConfigSets": "Относно ConfigSets", + "Access denied": "Достъпът е отказан", + "Access organisation dashboard and analytics": "Достъп до табло и анализи на организацията", + "Account": "Акаунт", + "Action": "Действие", + "Action Distribution": "Разпределение на действията", + "Action:": "Действие:", + "Actions": "Действия", + "Active": "Активен", + "Active Collections": "Активни колекции", + "Active Filters": "Активни филтри", + "Active Organisation:": "Активна организация:", + "Active Organisations": "Активни организации", + "Active filters:": "Активни филтри:", + "Active organisation changed successfully": "Активната организация е променена успешно", + "Active organisation set successfully": "Активната организация е зададена успешно", + "Activities": "Дейности", + "Activity": "Дейност", + "Activity Period": "Период на активност", + "Add": "Добавяне", + "Add Attachment": "Добавяне на прикачен файл", + "Add Endpoint": "Добавяне на крайна точка", + "Add Groups": "Добавяне на групи", + "Add Property to '{title}'": "Добавяне на свойство към „{title}“", + "Add Register": "Добавяне на регистър", + "Add Schema": "Добавяне на Схема", + "Add Source": "Добавяне на източник", + "Add User": "Добавяне на потребител", + "Add User to Organisation": "Добавяне на потребител към организация", + "Add a contact from any of your address books to associate it with this object.": "Добавете контакт от която и да е от Вашите адресни книги, за да го свържете с този обект.", + "Add a file or files": "Добавяне на файл или файлове", + "Add card": "Добавяне на карта", + "Add contact": "Добавяне на контакт", + "Add endpoint": "Добавяне на крайна точка", + "Add oneOf entry": "Добавяне на запис oneOf", + "Add range": "Добавяне на диапазон", + "Add schema titles, descriptions, and register information to provide richer context for search": "Добавете заглавия на схеми, описания и информация за регистри, за да осигурите по-богат контекст при търсене", + "Add to favorites": "Добавяне към любими", + "Added": "Добавено", + "Added to favorites": "Добавено към любими", + "Additional Information": "Допълнителна информация", + "Additional feedback saved. Thank you!": "Допълнителната обратна връзка е запазена. Благодарим Ви!", + "Addresses": "Адреси", + "Admin": "Администратор", + "Advanced": "Разширени", + "Advanced Filters": "Разширени филтри", + "Advanced Options": "Разширени опции", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Разширените филтри не са налични при използване на източник база данни. Превключете на Auto или SOLR индекс за опции за филтриране.", + "After": "След", + "Agent Type": "Тип агент", + "Agent deleted successfully": "Агентът е изтрит успешно", + "Agent successfully created": "Агентът е създаден успешно", + "Agent successfully updated": "Агентът е актуализиран успешно", + "Agents": "Агенти", + "Aggregated across schemas": "Агрегирано по схеми", + "All": "Всички", + "All Categories": "Всички категории", + "All Collections": "Всички колекции", + "All Confidence Levels": "Всички нива на достоверност", + "All ConfigSets": "Всички ConfigSets", + "All Files": "Всички Файлове", + "All Levels": "Всички нива", + "All Types": "Всички типове", + "All Webhooks": "Всички уебхукове", + "All actions": "Всички действия", + "All audit trails cleared successfully": "Всички одитни следи са изчистени успешно", + "All clear": "Всичко е наред", + "All file types": "Всички типове файлове", + "All registers": "Всички регистри", + "All schemas": "Всички схеми", + "All search trails cleared successfully": "Всички следи на търсене са изчистени успешно", + "All searches": "Всички търсения", + "All users": "Всички потребители", + "All-day events": "Целодневни събития", + "Allowed MIME Types": "Разрешени MIME типове", + "Already linked": "Вече е свързано", + "An error occurred while clearing audit trails": "Възникна грешка при изчистването на одитните следи", + "An error occurred while deleting the audit trail": "Възникна грешка при изтриването на одитната следа", + "An error occurred while deleting the object": "Възникна грешка при изтриването на обекта", + "An error occurred while deleting the view": "Възникна грешка при изтриването на изгледа", + "An error occurred while permanently deleting the objects": "Възникна грешка при окончателното изтриване на обектите", + "An error occurred while restoring the objects": "Възникна грешка при възстановяването на обектите", + "An error occurred while saving the organisation": "Възникна грешка при запазването на организацията", + "Analysis completed:": "Анализът завърши:", + "Analysis steps:": "Стъпки на анализа:", + "Analytics": "Анализи", + "Analyze Objects": "Анализиране на обекти", + "Analyze Properties": "Анализиране на свойства", + "Analyze existing properties for improvement opportunities": "Анализиране на съществуващите свойства за възможности за подобрение", + "Analyzing...": "Анализиране...", + "Anonymized": "Анонимизирано", + "Anonymous Group": "Анонимна група", + "Any favorites and sharing settings for this view": "Всички любими и настройки за споделяне за този изглед", + "Any user": "Всеки потребител", + "App ID": "ID на приложение", + "App store cache invalidated successfully": "Кешът на магазина за приложения е инвалидиран успешно", + "App store cache invalidated: {invalidated}": "Кешът на App Store е анулиран: {invalidated}", + "Application deleted successfully": "Приложението е изтрито успешно", + "Application identifier for this configuration (optional)": "Идентификатор на приложението за тази конфигурация (по избор)", + "Application settings": "Настройки на приложението", + "Application successfully created": "Приложението е създадено успешно", + "Application successfully updated": "Приложението е актуализирано успешно", + "Applications": "Приложения", + "Apply": "Прилагане", + "Apply Changes": "Прилагане на промените", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Прилагане на роля към всички схеми в този регистър, които нямат изрични замествания на оторизацията.", + "Approval Progress": "Напредък на одобрението", + "Approve": "Одобряване", + "Approved": "Одобрено", + "Archive": "Архивиране", + "Archive conversation": "Архивиране на разговора", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Архивиране на тази verwerkingsactiviteit? Редовете на одитната следа ще продължат да я препращат.", + "Archived conversations are hidden from your active list": "Архивираните разговори са скрити от Вашия активен списък", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Сигурни ли сте, че искате да ИЗТРИЕТЕ колекцията „{name}“?\n\nТова ще:\n• Изтрие за постоянно колекцията и всичките ѝ данни\n• Премахне всички индексирани документи\n• Това действие не може да бъде отменено", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Сигурни ли сте, че искате да изчистите старите следи на търсене? Това ще изтрие записи, по-стари от 30 дни.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Сигурни ли сте, че искате да изчистите всички данни от колекцията „{name}“?\n\nТова ще:\n• Изтрие всички индексирани документи\n• Запази структурата на колекцията непокътната\n• Това действие не може да бъде отменено", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Сигурни ли сте, че искате да изтриете приложението „{name}“? Това действие не може да бъде отменено.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Сигурни ли сте, че искате да изтриете избраните одитни следи? Това действие не може да бъде отменено.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Сигурни ли сте, че искате да изтриете избраните следи на търсене? Това действие не може да бъде отменено.", + "Are you sure you want to delete this ConfigSet?": "Сигурни ли сте, че искате да изтриете този ConfigSet?", + "Are you sure you want to delete this configset?": "Сигурни ли сте, че искате да изтриете този ConfigSet?", + "Are you sure you want to leave '{name}'?": "Сигурни ли сте, че искате да напуснете „{name}“?", + "Are you sure you want to permanently delete": "Сигурни ли сте, че искате окончателно да изтриете", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Сигурни ли сте, че искате да преиндексирате колекцията „{name}“?\n\nТова ще:\n• Възстанови индекса с всички обекти\n• Отнеме няколко минути за завършване\n• Може да повлияе на производителността на търсенето по време на преиндексирането", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Сигурни ли сте, че искате да премахнете схемата „{schema}“ от регистъра „{register}“? Това действие не може да бъде отменено.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Сигурни ли сте, че искате да изтриете меко {count} активни обекта за схемата „{schema}“?\n\nТе ще бъдат маркирани като изтрити, но могат да бъдат премахнати за постоянно по-късно.", + "Array Object Configuration:": "Конфигурация на обект-масив:", + "Articles": "Статии", + "Ask a question...": "Задайте въпрос...", + "Ask questions about your data using natural language": "Задавайте въпроси относно Вашите данни, използвайки естествен език", + "Assigned collaborative tags": "Присвоени съвместни етикети", + "Assignment notifications": "Известия за назначаване", + "Attachment added to {name}": "Прикаченият файл е добавен към {name}", + "Attempt": "Опит", + "Audit Trail Changes": "Промени в одитната следа", + "Audit Trail Details": "Детайли на одитната следа", + "Audit Trail Management": "Управление на одитни следи", + "Audit Trail Statistics": "Статистика на одитните следи", + "Audit Trails": "Одитни следи", + "Audit trail data copied to clipboard": "Данните от одитната следа са копирани в клипборда", + "Audit trail deleted successfully": "Одитната следа е изтрита успешно", + "Audit trail successfully deleted": "Одитната следа е изтрита успешно", + "Authentication": "Удостоверяване", + "Auto (e.g. Y for year, F Y for month)": "Авто (напр. Y за година, F Y за месец)", + "Auto share": "Автоматично споделяне", + "Auto-commit disabled": "Автоматичното подаване е изключено", + "Auto-commit enabled": "Автоматичното подаване е включено", + "Auto-create Default Organisation": "Автоматично създаване на организация по подразбиране", + "Auto-retry failed vectorizations": "Автоматично повторение на неуспешните векторизации", + "Automatically commit changes to SOLR index": "Автоматично подаване на промените към SOLR индекса", + "Automatically connect to the federation network.": "Автоматично свързване с мрежата за федерация.", + "Automatically create a default organisation if none exists when the app is initialized": "Автоматично създаване на организация по подразбиране, ако не съществува такава при инициализиране на приложението", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Автоматично генериране на векторни вграждания от текстови фрагменти при качване и обработка на файлове", + "Automatically generate vector embeddings when objects are created or updated": "Автоматично генериране на векторни вграждания при създаване или актуализиране на обекти", + "Automatically retry failed vectorization attempts (max 3 retries)": "Автоматично повторение на неуспешните опити за векторизация (максимум 3 повторения)", + "Automatically update catalogues.": "Автоматично актуализиране на каталозите.", + "Available Filters": "Налични филтри", + "Available Workflows": "Налични работни потоци", + "Available placeholders:": "Налични контейнери за заместване:", + "Avatar": "Аватар", + "Avatar changes are not supported by your authentication provider.": "Промените на аватара не се поддържат от Вашия доставчик на удостоверяване.", + "Avatar removed": "Аватарът е премахнат", + "Avatar updated successfully": "Аватарът е актуализиран успешно", + "Avg Execution Time": "Средно време за изпълнение", + "Avg Members/Org": "Средно членове/орг.", + "Avg Object Views/Session": "Средно преглеждания на обекти/сесия", + "Avg Response Time": "Средно време за отговор", + "Avg Results/Search": "Средно резултати/търсене", + "Avg Searches/Session": "Средно търсения/сесия", + "Back": "Назад", + "Back to Entities": "Обратно към субектите", + "Back to Registers": "Обратно към регистрите", + "Back to Webhooks": "Обратно към уебхуковете", + "Back to applications": "Обратно към приложенията", + "Back to entities": "Обратно към субектите", + "Backend parameter is required": "Параметърът на бекенда е задължителен", + "Backend updated successfully. Please reload the application.": "Бекендът е актуализиран успешно. Моля, презаредете приложението.", + "Bandwidth Quota (MB/month)": "Квота за честотна лента (MB/месец)", + "Base URL (Optional)": "Базов URL (по избор)", + "Basic Information": "Основна информация", + "Batch Size": "Размер на партидата", + "Batch extraction completed": "Партидното извличане завърши", + "Before": "Преди", + "Before object vectorization can work:": "Преди векторизацията на обекти да може да работи:", + "Behavior": "Поведение", + "Behavior Issue": "Проблем с поведението", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (продължителност по ISO-8601, напр. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob съхранението е изведено от употреба. Всички обекти вече използват магически таблици.", + "Bookmarks": "Отметки", + "Branch": "Клон", + "Bulk Role Assignment: {title}": "Групово присвояване на роли: {title}", + "Bulk delete operation completed successfully": "Операцията за групово изтриване завърши успешно", + "Bulk save operation completed successfully": "Операцията за групово запазване завърши успешно", + "Business Data": "Бизнес данни", + "CSV": "CSV", + "Cache cleared successfully": "Кешът е изчистен успешно", + "Cache grew from {old} to {new} entries.": "Кешът нарасна от {old} до {new} записа.", + "Cache shrunk from {old} to {new} entries.": "Кешът се сви от {old} до {new} записа.", + "Cache stayed the same at {size} entries.": "Кешът остана същият при {size} записа.", + "Calculate Sizes": "Изчисляване на размери", + "Calendar": "Календар", + "Calendar Provider Configuration": "Конфигурация на доставчик на календар", + "Calendar integration is not available": "Интеграцията с календара не е налична", + "Calendar name": "Име на календара", + "Cancel": "Отказ", + "Cancel deactivation request": "Отказване на заявката за деактивиране", + "Cannot delete: objects are still attached": "Не може да се изтрие: все още има прикачени обекти", + "Cannot edit: This register is managed by external configuration {title}": "Не може да се редактира: Този регистър се управлява от външна конфигурация {title}", + "Cannot remove schema with existing objects ({count} objects)": "Не може да се премахне схема със съществуващи обекти ({count} обекта)", + "Cannot remove schema {schema} because it contains {count} objects": "Не може да се премахне схема {schema}, защото съдържа {count} обекта", + "Cards": "Карти", + "Cascade delete": "Каскадно изтриване", + "Categories of data subjects (one per line)": "Категории субекти на данни (по една на ред)", + "Categories of personal data (one per line)": "Категории лични данни (по една на ред)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Категория", + "Change Type": "Тип на промяната", + "Change password": "Промяна на паролата", + "Changes": "Промени", + "Changes copied to clipboard": "Промените са копирани в клипборда", + "Changes for Audit Trail #{id}": "Промени за одитна следа #{id}", + "Chat Model": "Чат модел", + "Chat Provider": "Чат доставчик", + "Chat Provider (RAG)": "Чат доставчик (RAG)", + "Chat Settings": "Настройки на чата", + "Chat provider connection successful!": "Връзката с чат доставчика е успешна!", + "Chat settings": "Настройки на чата", + "Chat test failed: {error}": "Тестът на чата е неуспешен: {error}", + "Choose a register": "Изберете регистър", + "Choose a schema": "Изберете схема", + "Choose how vector similarity calculations are performed for semantic search": "Изберете как се извършват изчисленията за векторно сходство при семантично търсене", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Изберете кои типове файлове да бъдат включени в процеса на векторизация. Ще бъдат обработени само файлове с извлечен текст и фрагменти.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Изберете кои изгледи да бъдат включени в процеса на векторизация. Оставете празно, за да обработите всички изгледи според Вашата конфигурация.", + "Chunk": "Фрагмент", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Изтриването на фрагменти все още не е реализирано. Използвайте крайни точки, базирани на фрагменти.", + "Chunks": "Фрагменти", + "Chunks to Vectorize:": "Фрагменти за векторизация:", + "Cleanup Old Trails": "Изчистване на старите следи", + "Cleanup completed": "Изчистването завърши", + "Cleanup completed successfully. Deleted {count} entries.": "Почистването завърши успешно. Изтрити са {count} записа.", + "Cleanup failed: {error}": "Почистването е неуспешно: {error}", + "Clear All Embeddings": "Изчистване на всички вграждания", + "Clear Entries": "Изчистване на записите", + "Clear Filtered Audit Trails": "Изчистване на филтрираните одитни следи", + "Clear Filters": "Изчистване на филтрите", + "Clear Index": "Изчистване на индекса", + "Clear Selection": "Изчистване на избора", + "Clear all": "Изчистване на всички", + "Clear all filters": "Изчистване на всички филтри", + "Clear date range": "Изчистване на диапазона от дати", + "Clear filters": "Изчистване на филтрите", + "Clearing...": "Изчистване...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Щракнете върху Изпълни сканиране за съответствие, за да намерите схеми, при които е открита PII, но не съществува анотация за дейност по обработка.", + "Click to subscribe to notifications": "Щракнете, за да се абонирате за известия", + "Click to unsubscribe from notifications": "Щракнете, за да се отпишете от известия", + "Close": "Затваряне", + "Cluster name": "Име на клъстера", + "Code": "Код", + "Code (short readable key, e.g. v-2026-001)": "Код (кратък четим ключ, напр. v-2026-001)", + "Collapse details": "Свиване на подробностите", + "Collection Management": "Управление на колекции", + "Collection Name": "Име на колекцията", + "Collection assignments updated successfully": "Назначенията на колекции са актуализирани успешно", + "Collection cleared successfully": "Колекцията е изчистена успешно", + "Collection copied successfully": "Колекцията е копирана успешно", + "Collection created successfully": "Колекцията е създадена успешно", + "Collection deleted successfully": "Колекцията е изтрита успешно", + "Collection used to store and index file metadata and content": "Колекция, използвана за съхранение и индексиране на метаданни и съдържание на файлове", + "Collection used to store and index object data": "Колекция, използвана за съхранение и индексиране на данни за обекти", + "Collections:": "Колекции:", + "Color": "Цвят", + "Comment...": "Коментар...", + "Commit Message": "Съобщение за подаване", + "Commit Within (ms)": "Подаване в рамките на (мс)", + "Commit message": "Съобщение за подаване", + "Compare current schema with real object data": "Сравняване на текущата схема с реални данни за обекти", + "Completed": "Завършено", + "Complex": "Сложно", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Сложни заявки: разширени търсения с множество филтри, оператори и сложни комбинации от параметри", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Сложни заявки: разширени търсения с множество филтри, оператори и сложни комбинации от параметри", + "Compliance": "Съответствие", + "Confidence Level": "Ниво на достоверност", + "Confidence Score": "Оценка на достоверност", + "Confidence: {confidence}%": "Увереност: {confidence}%", + "Config must be provided as an object": "Конфигурацията трябва да бъде предоставена като обект", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Управление на ConfigSet", + "ConfigSet Name": "Име на ConfigSet", + "ConfigSet created successfully": "ConfigSet е създаден успешно", + "ConfigSet deleted successfully": "ConfigSet е изтрит успешно", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets дефинират схемата и конфигурацията за Вашите SOLR колекции. Те съдържат дефиниции на полета, анализатори и други настройки за търсене.", + "Configuration": "Конфигурация", + "Configuration Data": "Данни за конфигурацията", + "Configuration is up to date": "Конфигурацията е актуална", + "Configuration saved": "Конфигурацията е запазена", + "Configuration saved successfully.": "Конфигурацията е запазена успешно.", + "Configuration type (default, application, etc.)": "Тип конфигурация (по подразбиране, приложение и др.)", + "Configurations": "Конфигурации", + "Configure API tokens for external service integrations": "Конфигуриране на API токени за интеграции с външни услуги", + "Configure Apache SOLR search engine for advanced search capabilities": "Конфигуриране на търсачката Apache SOLR за разширени възможности за търсене", + "Configure Facets": "Конфигуриране на фасети", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Конфигурирайте доставчици на големи езикови модели (LLM) за функции, задвижвани от AI, включително семантично търсене, вграждания и чат.", + "Configure SOLR Facets": "Конфигуриране на SOLR фасети", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Конфигурирайте основните настройки за връзка за Вашия SOLR сървър, включително удостоверяване и мрежови опции. Използвайте отделните диалогови прозорци за управление на ConfigSet и колекции, за да управлявате ядра и колекции.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Конфигурирайте основните настройки за връзка за Вашия SOLR сървър, включително удостоверяване и мрежови опции. Използвайте отделните диалогови прозорци за управление на ConfigSet и колекции, за да управлявате ядрата и колекциите.", + "Configure default organisation and organisation-related settings": "Конфигуриране на организация по подразбиране и настройки, свързани с организацията", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Конфигурирайте как обектите от базата данни се преобразуват във векторни вграждания за семантично търсене. Обектите се векторизират директно, без да е необходимо извличане на текст.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Конфигурирайте как обектите се преобразуват в текст преди векторизация. Тези настройки влияят върху качеството и контекста на търсенето.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Конфигурирайте доставчици на големи езикови модели (LLM) за функции с изкуствен интелект, включително семантично търсене, вграждания и чат.", + "Configure multi-organization support and tenant isolation": "Конфигуриране на поддръжка на множество организации и изолация на наематели", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Конфигурирайте параметри за векторизация на файлове. Този процес ще генерира векторни вграждания за всички извлечени фрагменти от файлове.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Конфигурирайте параметри за векторизация на обекти. Този процес ще генерира векторни вграждания за всички обекти, отговарящи на филтрите на Вашия изглед.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Конфигурирайте тази схема, за да показва обекти като събития в приложението Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Конфигурирайте кои типове данни да се търсят и колко източника да се извличат", + "Configuring field mappings...": "Конфигуриране на съответствия на полета...", + "Confirm Account Deactivation": "Потвърждаване на деактивирането на акаунта", + "Confirm Object Deletion": "Потвърждаване на изтриването на обект", + "Confirm Object Restoration": "Потвърждаване на възстановяването на обект", + "Confirm Permanent Object Deletion": "Потвърждаване на постоянното изтриване на обект", + "Confirm deactivation": "Потвърждаване на деактивирането", + "Connecting to SOLR cluster...": "Свързване със SOLR клъстер...", + "Connection Failed": "Връзката е неуспешна", + "Connection Settings": "Настройки за връзка", + "Connection Successful!": "Връзката е успешна!", + "Connection failed": "Връзката е неуспешна", + "Connection protocol": "Протокол за връзка", + "Connection successful": "Връзката е успешна", + "Connection timeout in seconds": "Таймаут на връзката в секунди", + "Constant delay between retries (5 minutes)": "Постоянно забавяне между повторенията (5 минути)", + "Constraint Issue": "Проблем с ограничение", + "Consumer not found": "Потребителят не е намерен", + "Contacts": "Контакти", + "Contacts integration is not available": "Интеграцията с контактите не е налична", + "Content Filters": "Филтри за съдържание", + "Continue to Properties": "Продължаване към свойствата", + "Contracts": "Договори", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Контролирайте кои изгледи на обекти да бъдат векторизирани, за да намалите разходите за API. Векторизирайте само изгледи, които се възползват от семантично търсене.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Контролирайте кои изгледи и инструменти може да използва AI в този разговор. По подразбиране всички възможности на агента са активирани.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Контролира случайността (0 = детерминистично, 2 = много творческо)", + "Conversation ID is required": "Идентификаторът на разговора е задължителен", + "Conversation archived": "Разговорът е архивиран", + "Conversation archived successfully": "Разговорът е архивиран успешно", + "Conversation cleared successfully": "Разговорът е изчистен успешно", + "Conversation deleted": "Разговорът е изтрит", + "Conversation not found": "Разговорът не е намерен", + "Conversation permanently deleted": "Разговорът е окончателно изтрит", + "Conversation renamed": "Разговорът е преименуван", + "Conversation restored": "Разговорът е възстановен", + "Conversation started with {agent}": "Разговорът е започнат с {agent}", + "Conversation title": "Заглавие на разговора", + "Conversations": "Разговори", + "Converting {schema} to blob storage...": "Преобразуване на {schema} към съхранение на блобове...", + "Converting {schema} to magic table...": "Преобразуване на {schema} към магическа таблица...", + "Copied!": "Копирано!", + "Copy": "Копиране", + "Copy Changes": "Копиране на промените", + "Copy Collection": "Копиране на колекция", + "Copy Data": "Копиране на данни", + "Copy Full Data": "Копиране на пълните данни", + "Copy of {name}": "Копие на {name}", + "Copy to clipboard": "Копиране в клипборда", + "Copying...": "Копиране...", + "Costs": "Разходи", + "Count": "Брой", + "Counting objects...": "Преброяване на обекти...", + "Create": "Създаване", + "Create API Token": "Създаване на API токен", + "Create Agent": "Създаване на агент", + "Create Application": "Създаване на приложение", + "Create Collection": "Създаване на колекция", + "Create ConfigSet": "Създаване на ConfigSet", + "Create New Collection": "Създаване на нова колекция", + "Create New ConfigSet": "Създаване на нов ConfigSet", + "Create Operations": "Операции за създаване", + "Create Organisation": "Създаване на организация", + "Create Webhook": "Създаване на уебхук", + "Create a copy of collection:": "Създаване на копие на колекцията:", + "Create a new ConfigSet based on the _default template": "Създаване на нов ConfigSet въз основа на шаблона _default", + "Create a new SOLR collection from an existing ConfigSet": "Създаване на нова SOLR колекция от съществуващ ConfigSet", + "Create a new SOLR collection from an existing configset": "Създаване на нова SOLR колекция от съществуващ ConfigSet", + "Create a new configset based on the _default template": "Създаване на нов ConfigSet въз основа на шаблона _default", + "Create a new event or link an existing one from any of your calendars.": "Създайте ново събитие или свържете съществуващо от който и да е от Вашите календари.", + "Create another": "Създаване на друг", + "Create copies of": "Създаване на копия на", + "Create event": "Създаване на събитие", + "Create new token": "Създаване на нов токен", + "Create or link a Deck card to track work on this object.": "Създайте или свържете Deck карта, за да проследявате работата по този обект.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Създайте първата verwerkingsactiviteit, за да започнете да маркирате редовете на одитната следа с тяхното приписване по AVG Art 30.", + "Create your first AI agent to get started.": "Създайте първия си AI агент, за да започнете.", + "Created": "Създадено", + "Created:": "Създадено:", + "Creating search collections...": "Създаване на колекции за търсене...", + "Creating...": "Създаване...", + "Current": "Текущ", + "Current Filters": "Текущи филтри", + "Current Type:": "Текущ тип:", + "Current password": "Текуща парола", + "Custom API endpoint if using a different region": "Персонализирана крайна точка на API при използване на различен регион", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Персонализирани HTTP заглавки (по една на ред, формат: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Персонализирани HTTP хедъри (по един на ред, формат: header-name: value)", + "Custom Ranges:": "Персонализирани диапазони:", + "Custom display title for this facet": "Персонализирано заглавие за показване на този фасет", + "Custom pattern:": "Персонализиран шаблон:", + "DELETE COLLECTION": "ИЗТРИВАНЕ НА КОЛЕКЦИЯ", + "DSAR": "DSAR", + "Daily": "Ежедневно", + "Dashboard": "Табло", + "Dashboard not found": "Таблото не е намерено", + "Data": "Данни", + "Data Source": "Източник на данни", + "Data Sources": "Източници на данни", + "Data Views": "Изгледи на данни", + "Data sources": "Източници на данни", + "Data type does not match observed values": "Типът на данните не съответства на наблюдаваните стойности", + "Data-subject access request": "Заявка за достъп на субект на данни", + "Database URL": "URL на базата данни", + "Database information refreshed": "Информацията за базата данни е обновена", + "Date Faceting:": "Фасетиране по дата:", + "Date Range": "Диапазон от дати", + "Dates": "Дати", + "Deactivation request cancelled": "Заявката за деактивиране е отказана", + "Deactivation request submitted": "Заявката за деактивиране е подадена", + "Deck": "Deck", + "Deck integration is not available": "Интеграцията с Deck не е налична", + "Default": "По подразбиране", + "Default Object Tenant": "Наемател на обект по подразбиране", + "Default Organisation": "Организация по подразбиране", + "Default Owner": "Собственик по подразбиране", + "Default User (for cron/background jobs)": "Потребител по подразбиране (за cron/фонови задачи)", + "Default User Tenant": "Наемател на потребител по подразбиране", + "Default value": "Стойност по подразбиране", + "Default value (Email)": "Стойност по подразбиране (имейл)", + "Default value (Password)": "Стойност по подразбиране (парола)", + "Default value (Phone number)": "Стойност по подразбиране (телефонен номер)", + "Default value (Regex)": "Стойност по подразбиране (регулярен израз)", + "Default: 5": "По подразбиране: 5", + "Defaults to current user. Select a different user if needed.": "По подразбиране е текущият потребител. Изберете друг потребител, ако е необходимо.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Забавянията се удвояват с всеки опит (2, 4, 8 минути...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Забавянията нарастват линейно (5, 10, 15 минути...)", + "Delete": "Изтриване", + "Delete ({count})": "Изтриване ({count})", + "Delete Application": "Изтриване на приложение", + "Delete Audit Trail": "Изтриване на одитна следа", + "Delete Collection": "Изтриване на колекция", + "Delete ConfigSet": "Изтриване на ConfigSet", + "Delete Objects": "Изтриване на обекти", + "Delete Operations": "Операции за изтриване", + "Delete SOLR Collection": "Изтриване на SOLR колекция", + "Delete View": "Изтриване на изглед", + "Delete all objects in this schema": "Изтриване на всички обекти в тази схема", + "Delete permanently": "Окончателно изтриване", + "Delete this conversation?": "Изтриване на този разговор?", + "Delete view": "Изтриване на изглед", + "Delete {count} object": "Изтриване на {count} обект", + "Delete {count} objects": "Изтриване на {count} обекта", + "Deleted": "Изтрито", + "Deleted By": "Изтрито от", + "Deleted Date": "Дата на изтриване", + "Deleted Items Management": "Управление на изтрити елементи", + "Deleted This Week": "Изтрити тази седмица", + "Deleted Today": "Изтрити днес", + "Deleted:": "Изтрито:", + "Deleting SOLR Collection...": "Изтриване на SOLR колекция...", + "Deleting...": "Изтриване...", + "Deletion Date Range": "Диапазон от дати на изтриване", + "Deletion Statistics": "Статистика за изтриванията", + "Deprecated": "Остаряло", + "Depublish": "Отпубликуване", + "Description": "Описание", + "Description Template": "Шаблон на описание", + "Description shown as tooltip": "Описание, показано като подсказка", + "Description:": "Описание:", + "Detect data types and patterns": "Откриване на типове данни и модели", + "Detected At": "Открито на", + "Detected Format:": "Открит формат:", + "Detected Issues:": "Открити проблеми:", + "Detected: {date}": "Открито: {date}", + "Direct permission": "Пряко разрешение", + "Disable": "Деактивиране", + "Disable this to make the agent": "Деактивирайте това, за да направите агента", + "Disabled": "Деактивирано", + "Discover Files": "Откриване на файлове", + "Discovered Properties": "Открити свойства", + "Display Format": "Формат на показване", + "Display Name": "Показвано име", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Искате ли да изтриете окончателно всички филтрирани записи на одитни следи? Това действие не може да бъде отменено.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Искате ли да изтриете окончателно този запис на одитна следа? Това действие не може да бъде отменено.", + "Documents": "Документи", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR е активиран", + "Download API Spec": "Изтегляне на API спецификация", + "Download API Specification": "Изтегляне на API спецификацията", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Изтеглете копие на всичките Ви лични данни, съхранявани в OpenRegister (GDPR член 20).", + "Drag a file or files here": "Плъзнете файл или файлове тук", + "Dry run completed: {count} fields would be created": "Пробното изпълнение завърши: {count} полета биха били създадени", + "Dry run completed: {count} fields would be fixed": "Пробното изпълнение завърши: {count} полета биха били коригирани", + "Duration in seconds (optional)": "Продължителност в секунди (по избор)", + "Edit": "Редактиране", + "Edit Agent": "Редактиране на агент", + "Edit Application": "Редактиране на приложение", + "Edit Configuration": "Редактиране на конфигурация", + "Edit Endpoint": "Редактиране на крайна точка", + "Edit Organisation": "Редактиране на организация", + "Edit Property '{key}' of '{title}'": "Редактиране на свойство „{key}“ на „{title}“", + "Edit Register": "Редактиране на Регистър", + "Edit Schema": "Редактиране на Схема", + "Edit Source": "Редактиране на източник", + "Edit View": "Редактиране на изглед", + "Edit Webhook": "Редактиране на уебхук", + "Edit labels": "Редактиране на етикети", + "Edit processing activity": "Редактиране на дейност по обработка", + "Edit tags for {name}": "Редактиране на етикети за {name}", + "Edit verwerkingsactiviteit": "Редактиране на verwerkingsactiviteit", + "Edit view": "Редактиране на изглед", + "Edit view details": "Редактиране на детайлите на изгледа", + "Effectiveness": "Ефективност", + "Email": "Имейл", + "Email Addresses": "Имейл адреси", + "Email digest frequency": "Честота на имейл обобщенията", + "Emails": "Имейли", + "Embedding Model": "Модел за вграждане", + "Embedding Provider": "Доставчик на вграждания", + "Embedding generated successfully": "Вграждането е генерирано успешно", + "Embedding provider connection successful!": "Връзката с доставчика на вграждания е успешна!", + "Embedding test failed: {error}": "Тестът на вграждането е неуспешен: {error}", + "Enable": "Активиране", + "Enable RAG": "Активиране на RAG", + "Enable auto-creation": "Активиране на автоматично създаване", + "Enable automatic file vectorization": "Активиране на автоматична векторизация на файлове", + "Enable automatic object vectorization": "Активиране на автоматична векторизация на обекти", + "Enable automatic synchronization": "Активиране на автоматична синхронизация", + "Enable calendar provider": "Активиране на доставчик на календар", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Активиране на подробно логване за SOLR операции (препоръчително за отстраняване на грешки)", + "Enable faceting": "Активиране на фасетиране", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Активирайте или деактивирайте LLM функциите. Конфигурирайте доставчиците и моделите, използвайки бутона LLM конфигурация по-горе.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Активирайте или деактивирайте LLM функциите. Конфигурирайте доставчиците и моделите чрез бутона за конфигурация на LLM по-горе.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Активирайте или деактивирайте интеграцията със SOLR търсене. Конфигурирайте настройките за връзка, използвайки бутона Настройки за връзка по-горе.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Активирайте или деактивирайте интеграцията за търсене със SOLR. Конфигурирайте настройките за връзка чрез бутона за настройки на връзката по-горе.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Активирайте или деактивирайте интеграцията с работни потоци на n8n. Конфигурирайте настройките за връзка по-долу.", + "Enable or disable this webhook": "Активиране или деактивиране на този уебхук", + "Enable tools that allow the agent to interact with data through function calling.": "Активирайте инструменти, които позволяват на агента да взаимодейства с данни чрез извикване на функции.", + "Enable vectorization for all existing and future views (may increase costs)": "Активиране на векторизация за всички съществуващи и бъдещи изгледи (може да увеличи разходите)", + "Enable write-back to target objects": "Активиране на обратно записване в целевите обекти", + "Enabled": "Активирано", + "End Date Field": "Поле за крайна дата", + "Endpoint Path*": "Път на крайната точка*", + "Endpoints": "Крайни точки", + "Engine not found": "Двигателят не е намерен", + "Enter ConfigSet name": "Въведете име на ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Въведете потребителски имена в Nextcloud, за да предоставите достъп до този частен агент", + "Enter agent description (optional)": "Въведете описание на агента (по избор)", + "Enter agent name": "Въведете име на агента", + "Enter application description (optional)": "Въведете описание на приложението (по избор)", + "Enter application name": "Въведете име на приложението", + "Enter collection name": "Въведете име на колекция", + "Enter configuration description (optional)": "Въведете описание на конфигурацията (по избор)", + "Enter configuration title": "Въведете заглавие на конфигурацията", + "Enter description (optional)...": "Въведете описание (по избор)...", + "Enter new collection name": "Въведете ново име на колекция", + "Enter object ID": "Въведете идентификатор на обект", + "Enter organisation description (optional)": "Въведете описание на организацията (по избор)", + "Enter organisation name": "Въведете име на организацията", + "Enter search term": "Въведете термин за търсене", + "Enter search terms or leave empty to browse all": "Въведете термини за търсене или оставете празно, за да прегледате всички", + "Enter system prompt for the agent": "Въведете системна подкана за агента", + "Enter username and press Enter": "Въведете потребителско име и натиснете Enter", + "Enter view name...": "Въведете име на изглед...", + "Enter webhook name": "Въведете име на уебхук", + "Enter your API key": "Въведете Вашия API ключ", + "Enter your object here...": "Въведете Вашия обект тук...", + "Enter your schema here...": "Въведете Вашата схема тук...", + "Entities": "Субекти", + "Entities detected": "Открити субекти", + "Entity ID: {id}": "ID на субект: {id}", + "Entity Information": "Информация за субекта", + "Entity Recognition": "Разпознаване на субекти", + "Entity Recognition Method": "Метод за разпознаване на субекти", + "Entity deleted successfully": "Субектът е изтрит успешно", + "Entity not found": "Субектът не е намерен", + "Entries to be deleted:": "Записи за изтриване:", + "Enum Issue": "Проблем с изброяване", + "Enum constraint is missing": "Липсва ограничение за изброяване", + "Erase (Art 17)": "Изтриване (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Да се изтрият ли {count} обект(а) за този субект? Това действие се записва в одитната следа.", + "Erasure complete": "Изтриването завърши", + "Erasure preview": "Преглед на изтриването", + "Error": "Грешка", + "Error Details": "Детайли за грешката", + "Error Information": "Информация за грешката", + "Error Message": "Съобщение за грешка", + "Error deleting audit trails: {error}": "Грешка при изтриването на одитни следи: {error}", + "Error deleting search trails: {error}": "Грешка при изтриването на следи на търсене: {error}", + "Error loading application": "Грешка при зареждане на приложението", + "Error loading audit trails": "Грешка при зареждане на одитните следи", + "Error loading entity": "Грешка при зареждане на субекта", + "Error loading search trails": "Грешка при зареждане на следите на търсене", + "Estimated Batches:": "Прогнозни партиди:", + "Estimated Cost:": "Прогнозен разход:", + "Estimated Duration:": "Прогнозна продължителност:", + "Event": "Събитие", + "Event Property for Payload": "Свойство на събитие за полезен товар", + "Events": "Събития", + "Every schema with detected PII has a processing-activity annotation.": "Всяка схема с открита PII има анотация за дейност по обработка.", + "Example": "Пример", + "Example Value": "Примерна стойност", + "Example value for this property": "Примерна стойност за това свойство", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Изключителен максимум", + "Exclusive Minimum": "Изключителен минимум", + "Exclusive maximum": "Изключителен максимум", + "Exclusive minimum": "Изключителен минимум", + "Execution Mode": "Режим на изпълнение", + "Execution Time": "Време за изпълнение", + "Execution Time Range": "Диапазон на времето за изпълнение", + "Existing Improvements": "Съществуващи подобрения", + "Expand details": "Разгъване на детайлите", + "Expiration": "Изтичане", + "Expires": "Изтича", + "Expires in (e.g., 90d)": "Изтича след (напр. 90d)", + "Exponential": "Експоненциално", + "Export": "Експортиране", + "Export \"{schema}\" objects from \"{register}\"": "Експортиране на обекти „{schema}“ от „{register}“", + "Export Configuration": "Експортиране на конфигурация", + "Export Format:": "Формат на експортиране:", + "Export Objects": "Експортиране на обекти", + "Export completed successfully": "Експортирането завърши успешно", + "Export downloaded successfully": "Експортирането е изтеглено успешно", + "Export failed: {error}": "Експортирането е неуспешно: {error}", + "Export is rate limited. Please try again later.": "Експортирането е ограничено по честота. Моля, опитайте отново по-късно.", + "Export my data": "Експортиране на моите данни", + "Export, view, or delete audit trails": "Експортиране, преглед или изтриване на одитни следи", + "Exporting...": "Експортиране...", + "Extend Schema": "Разширяване на Схема", + "Extended": "Разширено", + "Extensions": "Разширения", + "Extra Query Parameters": "Допълнителни параметри на заявката", + "Extract Now": "Извличане сега", + "Extract Pending Files": "Извличане на чакащи файлове", + "Extract now": "Извличане сега", + "Extract properties from each object": "Извличане на свойства от всеки обект", + "Extracted At": "Извлечено на", + "Extracted at": "Извлечено на", + "Extraction": "Извличане", + "Extraction Mode": "Режим на извличане", + "Extraction Scope": "Обхват на извличане", + "Extraction Status": "Статус на извличане", + "Extraction error for {file}: {error}": "Грешка при извличане за {file}: {error}", + "Facet Description": "Описание на фасет", + "Facet Order": "Ред на фасета", + "Facet Title": "Заглавие на фасет", + "Facet Type": "Тип фасет", + "Facet configuration updated successfully": "Конфигурацията на фасетите е актуализирана успешно", + "Facetable": "Подлежи на фасетиране", + "Faceting Configuration:": "Конфигурация на фасетиране:", + "Facets discovered and configured successfully": "Фасетите са открити и конфигурирани успешно", + "Facets discovered successfully": "Фасетите са открити успешно", + "Failed": "Неуспешно", + "Failed to add attachment to object": "Неуспешно добавяне на прикачен файл към обекта", + "Failed to add user to organisation": "Неуспешно добавяне на потребител към организацията", + "Failed to analyze schema properties": "Неуспешно анализиране на свойствата на схемата", + "Failed to archive conversation": "Неуспешно архивиране на разговора", + "Failed to calculate sizes": "Неуспешно изчисляване на размерите", + "Failed to cancel deactivation": "Неуспешно отказване на деактивирането", + "Failed to change active organisation: {error}": "Неуспешна промяна на активната организация: {error}", + "Failed to change password": "Неуспешна промяна на паролата", + "Failed to check version: {error}": "Неуспешна проверка на версията: {error}", + "Failed to clear audit trails: {error}": "Неуспешно изчистване на одитни следи: {error}", + "Failed to clear blob objects: {error}": "Неуспешно изчистване на блоб обекти: {error}", + "Failed to clear cache: {error}": "Неуспешно изчистване на кеша: {error}", + "Failed to clear collection": "Неуспешно изчистване на колекцията", + "Failed to clear conversation": "Неуспешно изчистване на разговора", + "Failed to clear embeddings: {error}": "Неуспешно изчистване на вграждания: {error}", + "Failed to clear search trails: {error}": "Неуспешно изчистване на следи на търсене: {error}", + "Failed to clear {type} cache: {error}": "Неуспешно изчистване на {type} кеш: {error}", + "Failed to copy changes": "Неуспешно копиране на промените", + "Failed to copy data": "Неуспешно копиране на данните", + "Failed to copy data to clipboard": "Неуспешно копиране на данните в клипборда", + "Failed to copy to clipboard": "Неуспешно копиране в клипборда", + "Failed to copy token": "Неуспешно копиране на токена", + "Failed to create conversation": "Неуспешно създаване на разговор", + "Failed to create missing SOLR fields: {error}": "Неуспешно създаване на липсващи SOLR полета: {error}", + "Failed to create or find project": "Неуспешно създаване или намиране на проект", + "Failed to create token": "Неуспешно създаване на токен", + "Failed to delete SOLR collection: {error}": "Неуспешно изтриване на SOLR колекция: {error}", + "Failed to delete collection": "Неуспешно изтриване на колекцията", + "Failed to delete conversation": "Неуспешно изтриване на разговора", + "Failed to delete objects for {schema}: {error}": "Неуспешно изтриване на обекти за {schema}: {error}", + "Failed to delete webhook": "Неуспешно изтриване на уебхука", + "Failed to depublish register: {error}": "Неуспешно отпубликуване на регистър: {error}", + "Failed to depublish schema: {error}": "Неуспешно отпубликуване на схема: {error}", + "Failed to depublish: {error}": "Неуспешно отпубликуване: {error}", + "Failed to discover files: {error}": "Неуспешно откриване на файлове: {error}", + "Failed to download API specification": "Неуспешно изтегляне на API спецификацията", + "Failed to export data": "Неуспешно експортиране на данни", + "Failed to export objects": "Неуспешно експортиране на обекти", + "Failed to fetch conversation history": "Неуспешно извличане на историята на разговора", + "Failed to fix mismatched SOLR fields: {error}": "Неуспешно коригиране на несъответстващи SOLR полета: {error}", + "Failed to get SOLR field configuration": "Неуспешно получаване на конфигурацията на SOLR полето", + "Failed to get chat statistics": "Неуспешно получаване на статистиката на чата", + "Failed to get database information: %s": "Неуспешно получаване на информация за базата данни: %s", + "Failed to get entity categories": "Неуспешно получаване на категориите на субектите", + "Failed to get entity statistics": "Неуспешно получаване на статистиката на субектите", + "Failed to get entity types": "Неуспешно получаване на типовете субекти", + "Failed to get response: {error}": "Неуспешно получаване на отговор: {error}", + "Failed to invalidate app store cache: {error}": "Неуспешно анулиране на кеша на App Store: {error}", + "Failed to leave organisation: {error}": "Неуспешно напускане на организацията: {error}", + "Failed to link object": "Неуспешно свързване на обект", + "Failed to load Deck cards": "Неуспешно зареждане на Deck карти", + "Failed to load LLM configuration": "Неуспешно зареждане на LLM конфигурацията", + "Failed to load Nextcloud groups": "Неуспешно зареждане на групите в Nextcloud", + "Failed to load cache statistics: {error}": "Неуспешно зареждане на статистики за кеша: {error}", + "Failed to load conversation": "Неуспешно зареждане на разговора", + "Failed to load entities": "Неуспешно зареждане на субектите", + "Failed to load entity": "Неуспешно зареждане на субекта", + "Failed to load extraction data": "Неуспешно зареждане на данните за извличане", + "Failed to load files": "Неуспешно зареждане на файловете", + "Failed to load linked contacts": "Неуспешно зареждане на свързани контакти", + "Failed to load linked emails": "Неуспешно зареждане на свързани имейли", + "Failed to load linked events": "Неуспешно зареждане на свързани събития", + "Failed to load organisations": "Неуспешно зареждане на организациите", + "Failed to load register data": "Неуспешно зареждане на данните за регистъра", + "Failed to load related entities": "Неуспешно зареждане на свързани субекти", + "Failed to load settings: {error}": "Неуспешно зареждане на настройки: {error}", + "Failed to load statistics: {error}": "Неуспешно зареждане на статистики: {error}", + "Failed to load templates": "Неуспешно зареждане на шаблоните", + "Failed to load view: {error}": "Неуспешно зареждане на изглед: {error}", + "Failed to load webhooks": "Неуспешно зареждане на уебхуковете", + "Failed to load workflows": "Неуспешно зареждане на работните потоци", + "Failed to process message": "Неуспешна обработка на съобщението", + "Failed to publish register: {error}": "Неуспешно публикуване на регистър: {error}", + "Failed to publish schema: {error}": "Неуспешно публикуване на схема: {error}", + "Failed to publish: {error}": "Неуспешно публикуване: {error}", + "Failed to refresh database information": "Неуспешно обновяване на информацията за базата данни", + "Failed to reindex collection": "Неуспешно повторно индексиране на колекцията", + "Failed to remove avatar": "Неуспешно премахване на аватара", + "Failed to remove link": "Неуспешно премахване на връзка", + "Failed to remove member: {error}": "Неуспешно премахване на член: {error}", + "Failed to remove schema {schema}: {error}": "Неуспешно премахване на схема {schema}: {error}", + "Failed to remove user from organisation": "Неуспешно премахване на потребител от организацията", + "Failed to rename conversation": "Неуспешно преименуване на разговора", + "Failed to request deactivation": "Неуспешна заявка за деактивиране", + "Failed to restore conversation": "Неуспешно възстановяване на разговора", + "Failed to retrieve extraction status.": "Неуспешно извличане на статуса на извличане.", + "Failed to retrieve objects for file.": "Неуспешно извличане на обектите за файла.", + "Failed to retry extraction": "Неуспешно повторение на извличането", + "Failed to retry webhook": "Неуспешно повторение на уебхука", + "Failed to revoke token": "Неуспешно отнемане на токен", + "Failed to save GitHub token": "Неуспешно запазване на GitHub токена", + "Failed to save GitLab URL": "Неуспешно запазване на GitLab URL", + "Failed to save GitLab token": "Неуспешно запазване на GitLab токена", + "Failed to save LLM settings: {error}": "Неуспешно запазване на настройките на LLM: {error}", + "Failed to save additional feedback": "Неуспешно запазване на допълнителната обратна връзка", + "Failed to save configuration.": "Неуспешно запазване на конфигурацията.", + "Failed to save configuration: {error}": "Неуспешно запазване на конфигурацията: {error}", + "Failed to save facet configuration": "Неуспешно запазване на конфигурацията на фасета", + "Failed to save feedback": "Неуспешно запазване на обратната връзка", + "Failed to save file settings: {error}": "Неуспешно запазване на настройките на файла: {error}", + "Failed to save n8n configuration": "Неуспешно запазване на n8n конфигурацията", + "Failed to save preferences": "Неуспешно запазване на предпочитанията", + "Failed to save roles": "Неуспешно запазване на ролите", + "Failed to save settings": "Неуспешно запазване на настройките", + "Failed to save settings: {error}": "Неуспешно запазване на настройки: {error}", + "Failed to save view: {error}": "Неуспешно запазване на изглед: {error}", + "Failed to save warmup interval: {error}": "Неуспешно запазване на интервала за подгряване: {error}", + "Failed to save webhook": "Неуспешно запазване на уебхука", + "Failed to send feedback": "Неуспешно изпращане на обратна връзка", + "Failed to set active organisation: {error}": "Неуспешно задаване на активна организация: {error}", + "Failed to setup SOLR: {error}": "Неуспешна настройка на SOLR: {error}", + "Failed to start file warmup: {error}": "Неуспешно стартиране на подгряване на файлове: {error}", + "Failed to start processing {type} files: {error}": "Неуспешно стартиране на обработката на {type} файлове: {error}", + "Failed to start vectorization: {error}": "Неуспешно стартиране на векторизация: {error}", + "Failed to switch organisation: {error}": "Неуспешно превключване на организация: {error}", + "Failed to sync magic table for {schema}: {error}": "Неуспешно синхронизиране на магическата таблица за {schema}: {error}", + "Failed to test SOLR connection: {error}": "Неуспешно тестване на връзката със SOLR: {error}", + "Failed to test webhook": "Неуспешно тестване на уебхука", + "Failed to update LLM settings: {error}": "Неуспешно актуализиране на настройките за LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Неуспешно актуализиране на настройките за многопотребителство: {error}", + "Failed to update RBAC settings: {error}": "Неуспешно актуализиране на настройките за RBAC: {error}", + "Failed to update Retention settings: {error}": "Неуспешно актуализиране на настройките за съхранение: {error}", + "Failed to update SOLR settings: {error}": "Неуспешно актуализиране на настройките за SOLR: {error}", + "Failed to update favorite status": "Неуспешно актуализиране на статуса на любими", + "Failed to update schema configuration for {schema}: {error}": "Неуспешно актуализиране на конфигурацията на схемата за {schema}: {error}", + "Failed to update schema properties": "Неуспешно актуализиране на свойствата на схемата", + "Failed to update view": "Неуспешно актуализиране на изгледа", + "Failed to update view: {error}": "Неуспешно актуализиране на изгледа: {error}", + "Failed to update webhook": "Неуспешно актуализиране на уебхука", + "Failed to upload avatar": "Неуспешно качване на аватар", + "Failed to validate {schema}: {error}": "Неуспешна валидация на {schema}: {error}", + "Failed to vectorize files: {error}": "Неуспешна векторизация на файлове: {error}", + "Failed to warmup names cache: {error}": "Неуспешно подгряване на кеша с имена: {error}", + "Federation": "Федерация", + "Feedback recorded": "Обратната връзка е записана", + "Fewer sources (1-3):": "По-малко източници (1-3):", + "Field": "Поле", + "Fields to Display": "Полета за показване", + "File": "Файл", + "File #{id}": "Файл #{id}", + "File Chunk Prediction": "Прогноза за фрагменти на файла", + "File Collection": "Колекция от файлове", + "File Handling": "Обработка на файлове", + "File Location": "Местоположение на файла", + "File Management": "Управление на файлове", + "File Name": "Име на файла", + "File Path": "Път до файла", + "File Type Selection": "Избор на тип файл", + "File Types to Process": "Типове файлове за обработка", + "File Vectorization": "Векторизация на файлове", + "File Warmup": "Подгряване на файлове", + "File actions menu": "Меню за действия с файлове", + "File anonymized successfully": "Файлът е анонимизиран успешно", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Векторизацията на фрагменти на файла все още не е реализирана. Фрагментите са готови и съхранени, но услугата за векторизация е в процес на разработка.", + "File collection not configured": "Колекцията от файлове не е конфигурирана", + "File discovery completed": "Откриването на файлове завърши", + "File extraction completed": "Извличането на файла завърши", + "File extraction queued": "Извличането на файла е поставено на опашка", + "File indexed successfully": "Файлът е индексиран успешно", + "File is already anonymized": "Файлът вече е анонимизиран", + "File name": "Име на файл", + "File not found": "Файлът не е намерен", + "File settings saved successfully": "Настройките за файлове са запазени успешно", + "File settings updated successfully": "Настройките на файла са актуализирани успешно", + "File sources": "Източници на файлове", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Векторизацията на файлове завърши! {vectorized} фрагмента са векторизирани от {files} файла. {failed} неуспешни.", + "File vectorization configuration saved successfully": "Конфигурацията за векторизация на файлове е запазена успешно", + "File vectorization started. Check the statistics section for progress.": "Векторизацията на файлове започна. Проверете секцията със статистика за напредъка.", + "File warmup completed": "Подгряването на файла завърши", + "Files": "Файлове", + "Files added successfully": "Файловете са добавени успешно", + "Files with Completed Extraction:": "Файлове със завършено извличане:", + "Files → fileCollection, Objects → objectCollection": "Файлове → fileCollection, обекти → objectCollection", + "Files → filecollection, objects → objectcollection": "Файлове → filecollection, обекти → objectcollection", + "Filter Audit Trails": "Филтриране на одитни следи", + "Filter Deleted Items": "Филтриране на изтрити елементи", + "Filter Objects": "Филтриране на обекти", + "Filter Properties": "Филтриране на свойства", + "Filter Search Trails": "Филтриране на следи на търсене", + "Filter Statistics": "Статистика на филтрите", + "Filter and analyze search trail entries": "Филтриране и анализиране на записи на следи на търсене", + "Filter and manage audit trail entries": "Филтриране и управление на записи на одитни следи", + "Filter and manage soft deleted items": "Филтриране и управление на меко изтрити елементи", + "Filter and search entities": "Филтриране и търсене на субекти", + "Filter by object ID": "Филтриране по идентификатор на обект", + "Filter by search term": "Филтриране по термин за търсене", + "Filter by type": "Филтриране по тип", + "Filter by webhook": "Филтриране по уебхук", + "Filter data loaded automatically. Use the filters below to refine your search.": "Данните за филтриране са заредени автоматично. Използвайте филтрите по-долу, за да прецизирате търсенето си.", + "Filter fields...": "Филтриране на полета...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Филтриране на тригерите на уебхук по свойства на полезния товар (по едно на ред, формат: key: value)", + "Filtered": "Филтрирано", + "Filters": "Филтри", + "Finalizing setup...": "Финализиране на настройката...", + "Fireworks AI Chat Settings": "Настройки за чат на Fireworks AI", + "Fireworks AI Embedding Configuration": "Конфигурация за вграждания на Fireworks AI", + "First": "Първи", + "Fixed": "Фиксирано", + "For chat and retrieval-augmented generation": "За чат и генериране, подсилено с извличане", + "For vector embeddings and semantic search": "За векторни вграждания и семантично търсене", + "Format": "Формат", + "Format Issue": "Проблем с формата", + "Format constraint is missing": "Липсва ограничение за формат", + "From (e.g. -7 days or 2025-01-01)": "От (напр. -7 дни или 2025-01-01)", + "From Date": "От дата", + "From date": "От дата", + "Full data copied to clipboard": "Пълните данни са копирани в клипборда", + "Function Tools": "Функционални инструменти", + "General Issue": "Общ проблем", + "Generate recommendations and confidence scores": "Генериране на препоръки и оценки на достоверност", + "Generate report": "Генериране на отчет", + "Generate the verantwoordingsdocument": "Генериране на verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Генериране на вектори незабавно при създаване на нови обекти", + "Generated: {time}": "Генериран: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Получавайте в реално време информация за състоянието на данните на Вашата организация, като се фокусирате върху регистри, дефиниции на схеми и съхранение и използване на обекти.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Получавайте в реално време информация за състоянието на данните на Вашата организация, тенденциите на растеж и показателите за качество.", + "GitHub Personal Access Token": "Личен токен за достъп до GitHub", + "GitHub token is valid": "GitHub токенът е валиден", + "GitHub token is valid! Username: {username}": "Токенът за GitHub е валиден! Потребителско име: {username}", + "GitHub token removed successfully": "GitHub токенът е премахнат успешно", + "GitHub token saved successfully": "GitHub токенът е запазен успешно", + "GitHub token test failed: {message}": "Тестът на токена за GitHub е неуспешен: {message}", + "GitLab Personal Access Token": "Личен токен за достъп до GitLab", + "GitLab URL saved successfully": "GitLab URL е запазен успешно", + "GitLab token is valid": "GitLab токенът е валиден", + "GitLab token is valid! Username: {username}": "Токенът за GitLab е валиден! Потребителско име: {username}", + "GitLab token saved successfully": "GitLab токенът е запазен успешно", + "GitLab token test failed: {message}": "Тестът на токена за GitLab е неуспешен: {message}", + "Go to linked Register": "Към свързания Регистър", + "Go to linked Schema": "Към свързаната Схема", + "Go to organisation": "Към организацията", + "Grant additional permissions beyond standard CRUD operations": "Предоставяне на допълнителни разрешения извън стандартните CRUD операции", + "Group Quota": "Квота на групата", + "Groups": "Групи", + "HTML preview": "Преглед на HTML", + "HTTP Method": "HTTP метод", + "HTTP method used to send webhook requests": "HTTP метод, използван за изпращане на заявки за уебхук", + "Headers": "Заглавки", + "Health": "Състояние", + "Heartbeat successful - connection kept alive": "Сърдечният ритъм е успешен - връзката е поддържана", + "Helpful": "Полезно", + "Here you can configure the details for various connections.": "Тук можете да конфигурирате детайлите за различни връзки.", + "Here you can configure the details for your organisation.": "Тук можете да конфигурирате детайлите за Вашата организация.", + "Hide": "Скриване", + "Hide Filters": "Скриване на филтрите", + "Hide in collection view": "Скриване в изгледа на колекция", + "Hide in forms": "Скриване във формуляри", + "Hide in list view": "Скриване в изглед на списък", + "High": "Високо", + "High Confidence": "Висока достоверност", + "Host": "Хост", + "Hourly": "Ежечасно", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Колко дълбоко да се обхождат вложените свойства на обекта (1-20). По-високите стойности улавят повече детайли, но увеличават размера на вектора.", + "How often to check for updates (1-168 hours)": "Колко често да се проверява за актуализации (1-168 часа)", + "How to handle retries for failed webhook deliveries": "Как да се обработват повторенията за неуспешни доставки на уебхук", + "IBANs": "IBAN-и", + "ID": "ID", + "ID of input mapping (optional)": "ID на входното съпоставяне (по избор)", + "ID of output mapping (optional)": "ID на изходното съпоставяне (по избор)", + "ID of the target resource": "ID на целевия ресурс", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ограниченията на честотата по IP са изчистени успешно", + "Identify properties not in the schema": "Идентифициране на свойства, които не са в схемата", + "Immutable": "Неизменяемо", + "Import": "Импортиране", + "Import Configuration": "Импортиране на конфигурация", + "Import Data into Register": "Импортиране на данни в Регистър", + "Import successful": "Импортирането е успешно", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Импортирайте шаблона report-bundle.json, за да получите регистъра `reports`, след което създайте първото си табло чрез стандартния потребителски интерфейс за обекти. Таблата декларират своите джаджи в JSON, а рендерът захранва всяка джаджа с данни от агрегации в реално време.", + "Improved Property": "Подобрено свойство", + "In use": "В употреба", + "Inactive": "Неактивен", + "Inactive organisations cannot be used": "Неактивните организации не могат да бъдат използвани", + "Include IDs and names of related objects for better contextual search": "Включване на идентификатори и имена на свързани обекти за по-добро контекстуално търсене", + "Include ids and names of related objects for better contextual search": "Включване на идентификатори и имена на свързани обекти за по-добро контекстуално търсене", + "Include related object references": "Включване на референции към свързани обекти", + "Include schema and register metadata": "Включване на метаданни за схема и регистър", + "Index": "Индекс", + "Information about the current OpenRegister installation": "Информация за текущата инсталация на OpenRegister", + "Inherited from register": "Наследено от регистър", + "Inherits permissions from register": "Наследява разрешенията от регистър", + "Initialization failed": "Инициализацията е неуспешна", + "Initialize Project": "Инициализиране на проект", + "Initializing SOLR setup...": "Инициализиране на настройката на SOLR...", + "Input Mapping": "Входно съпоставяне", + "Inspect Fields": "Инспектиране на полета", + "Inspect Index": "Инспектиране на индекс", + "Inspect SOLR Index": "Инспектиране на SOLR индекса", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Инсталирайте приложението notify_push от App Store на Nextcloud, за да активирате актуализации в реално време.", + "Integrations": "Интеграции", + "Internet location (URL) of this installation": "Интернет местоположение (URL) на тази инсталация", + "Interval": "Интервал", + "Invalid": "Невалиден", + "Invalid batch size. Must be between 1 and 5000": "Невалиден размер на партидата. Трябва да бъде между 1 и 5000", + "Invalid feedback type": "Невалиден тип обратна връзка", + "Invalid field name provided": "Предоставено е невалидно име на поле", + "Invalid maxObjects. Must be 0 (all) or positive number": "Невалиден maxObjects. Трябва да бъде 0 (всички) или положително число", + "Invalid:": "Невалиден:", + "Invite Users": "Покана на потребители", + "Invited Users": "Поканени потребители", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Резултати от Inzage", + "Issue": "Проблем", + "Items per page:": "Елементи на страница:", + "Just now": "Току-що", + "Key": "Ключ", + "LLM Configuration": "LLM конфигурация", + "LLM actions menu": "Меню за LLM действия", + "LLM configuration saved successfully": "LLM конфигурацията е запазена успешно", + "LLM features disabled": "LLM функциите са деактивирани", + "LLM features enabled": "LLM функциите са активирани", + "LLM must be enabled with an embedding provider configured": "LLM трябва да бъде активиран с конфигуриран доставчик на вграждания", + "LLM settings saved successfully": "Настройките за LLM са запазени успешно", + "LLM settings updated successfully": "LLM настройките са актуализирани успешно", + "Label": "Етикет", + "Labels": "Етикети", + "Languages": "Езици", + "Last": "Последен", + "Last Triggered": "Последно задействане", + "Leave empty to allow all users access": "Оставете празно, за да разрешите достъп на всички потребители", + "Leave off for auto-detection from property format.": "Оставете изключено за автоматично откриване от формата на свойството.", + "Left organisation successfully": "Излизането от организацията е успешно", + "Legal basis *": "Правно основание *", + "Length Range:": "Диапазон на дължина:", + "Linear": "Линейно", + "Link": "Връзка", + "Link an email from the Mail app sidebar to associate it with this object.": "Свържете имейл от страничната лента на приложението Mail, за да го асоциирате с този обект.", + "Link an object to see it here.": "Свържете обект, за да го видите тук.", + "Link another object": "Свързване на друг обект", + "Link existing event": "Свързване на съществуващо събитие", + "Link removed": "Връзката е премахната", + "Link to Object": "Връзка към Обект", + "Link to {name}": "Връзка към {name}", + "Linked Objects": "Свързани обекти", + "Linked by {user}": "Свързано от {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Свързаните имейли, събития в календара, контакти и карти от Deck ще се показват тук.", + "Linked to {name}": "Свързано с {name}", + "Load Advanced Filters": "Зареждане на разширени филтри", + "Load advanced filters with live data from your search index": "Зареждане на разширени филтри с данни на живо от Вашия индекс за търсене", + "Load more": "Зареждане на още", + "Load view": "Зареждане на изглед", + "Loading API tokens...": "Зареждане на API токени...", + "Loading ConfigSets...": "Зареждане на ConfigSets...", + "Loading LLM configuration...": "Зареждане на конфигурацията за LLM...", + "Loading RBAC settings...": "Зареждане на настройките за RBAC...", + "Loading activity...": "Зареждане на активност...", + "Loading advanced filters...": "Зареждане на разширени филтри...", + "Loading agents...": "Зареждане на агенти...", + "Loading application details...": "Зареждане на детайли за приложението...", + "Loading applications...": "Зареждане на приложения...", + "Loading archived conversations...": "Зареждане на архивирани разговори...", + "Loading audit trails...": "Зареждане на одитни следи...", + "Loading available tools...": "Зареждане на наличните инструменти...", + "Loading cache statistics...": "Зареждане на статистиката за кеша...", + "Loading collections...": "Зареждане на колекции...", + "Loading configurations...": "Зареждане на конфигурации...", + "Loading conversation...": "Зареждане на разговор...", + "Loading conversations...": "Зареждане на разговори...", + "Loading deleted items...": "Зареждане на изтрити елементи...", + "Loading endpoints": "Зареждане на крайни точки", + "Loading entities...": "Зареждане на субекти...", + "Loading events...": "Зареждане на събития...", + "Loading filter data...": "Зареждане на данни за филтриране...", + "Loading groups...": "Зареждане на групи...", + "Loading linked objects...": "Зареждане на свързани обекти...", + "Loading log details...": "Зареждане на детайли за дневника...", + "Loading multitenancy settings...": "Зареждане на настройките за многопотребителство...", + "Loading n8n configuration...": "Зареждане на конфигурацията за n8n...", + "Loading permission matrix...": "Зареждане на матрицата с разрешения...", + "Loading preferences...": "Зареждане на предпочитания...", + "Loading register data...": "Зареждане на данните за регистъра...", + "Loading registers...": "Зареждане на регистри...", + "Loading retention settings...": "Зареждане на настройките за съхранение...", + "Loading schemas...": "Зареждане на схеми...", + "Loading search configuration...": "Зареждане на конфигурацията за търсене...", + "Loading search trails...": "Зареждане на следи на търсене...", + "Loading sources...": "Зареждане на източници...", + "Loading statistics...": "Зареждане на статистика...", + "Loading text extraction settings...": "Зареждане на настройките за извличане на текст...", + "Loading tokens...": "Зареждане на токени...", + "Loading user groups...": "Зареждане на потребителските групи...", + "Loading users...": "Зареждане на потребители...", + "Loading views...": "Зареждане на изгледи...", + "Loading...": "Зареждане...", + "Local": "Локален", + "Local Version": "Локална версия", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Намерете всеки обект, който се отнася до субект на данни (Art 15 inzage), визуализирайте изтриване (Art 17 vergetelheid) или експортирайте техните данни (Art 20 portabiliteit).", + "Location": "Местоположение", + "Location Field": "Поле за местоположение", + "Locations": "Местоположения", + "Locked": "Заключено", + "Locked:": "Заключено:", + "Login successful": "Влизането е успешно", + "Logs": "Дневници", + "Low": "Ниско", + "Low Confidence": "Ниска достоверност", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "По-ниските числа се показват първи в страничната лента за филтриране. Оставете празно за автоматично подреждане.", + "Lucene Version:": "Версия на Lucene:", + "Magic table sync completed for {schema}": "Синхронизирането на магическата таблица завърши за {schema}", + "Magic table synchronized successfully": "Магическата таблица е синхронизирана успешно", + "Mail Integration": "Интеграция с Mail", + "Mail integration is not available": "Интеграцията с Mail не е налична", + "Make this installation discoverable within the federation network.": "Направете тази инсталация откриваема в мрежата на федерацията.", + "Manage Organisation Roles": "Управление на ролите в организацията", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Управлявайте SOLR колекции (хранилища на данни) и ги назначавайте за обекти и файлове.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Управлявайте SOLR ConfigSets (конфигурационни шаблони) за Вашите колекции.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Управлявайте SOLR колекции (хранилища на данни) и ги назначавайте за обекти и файлове.", + "Manage SOLR configsets (configuration templates) for your collections.": "Управлявайте SOLR ConfigSet-ове (шаблони за конфигурация) за Вашите колекции.", + "Manage and configure agents for automated tasks": "Управление и конфигуриране на агенти за автоматизирани задачи", + "Manage and monitor file text extraction status": "Управление и наблюдение на статуса на извличане на текст от файлове", + "Manage and restore soft deleted items from your registers": "Управление и възстановяване на меко изтрити елементи от Вашите регистри", + "Manage and view detected entities from files and objects": "Управление и преглед на открити субекти от файлове и обекти", + "Manage document templates and themes": "Управление на шаблони и теми за документи", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Управлявайте работни потоци на n8n за автоматизация на OpenRegister. Работните потоци ще се съхраняват в конфигурирания проект.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Управлявайте дейностите по обработка, изпълнявайте заявки за достъп от субекти на данни и одитирайте съответствието съгласно GDPR на ЕС / нидерландския AVG.", + "Manage webhooks for event-driven integrations": "Управление на уебхукове за интеграции, задвижвани от събития", + "Manage your account settings, security, and personal data.": "Управлявайте настройките на акаунта си, сигурността и личните данни.", + "Manage your applications and modules": "Управлявайте Вашите приложения и модули", + "Manage your chat conversations": "Управлявайте Вашите разговори в чата", + "Manage your data registers and their configurations": "Управлявайте Вашите регистри с данни и техните конфигурации", + "Manage your data schemas and their properties": "Управлявайте Вашите схеми с данни и техните свойства", + "Manage your data sources and their configurations": "Управлявайте Вашите източници на данни и техните конфигурации", + "Manage your organisations and switch between them": "Управлявайте Вашите организации и превключвайте между тях", + "Manage your saved search configurations": "Управлявайте Вашите запазени конфигурации за търсене", + "Manage your system configurations and settings": "Управлявайте Вашите системни конфигурации и настройки", + "Managed": "Управляван", + "Mappings": "Съпоставяния", + "Mass validation completed successfully": "Масовата валидация завърши успешно", + "Mass validation failed: {error}": "Масовата валидация е неуспешна: {error}", + "Max Files (0 = all)": "Максимум файлове (0 = всички)", + "Max Length": "Максимална дължина", + "Max Length:": "Максимална дължина:", + "Max Objects (0 = all)": "Максимум обекти (0 = всички)", + "Max Retries": "Максимум повторения", + "Max Shards/Node": "Максимум фрагменти/възел", + "Max Tokens": "Максимален брой токени", + "Max execution time (ms)": "Максимално време за изпълнение (мс)", + "Max ms": "Макс. мс", + "Max result count": "Максимален брой резултати", + "Max results": "Максимум резултати", + "Maximum": "Максимум", + "Maximum File Size (MB)": "Максимален размер на файл (MB)", + "Maximum Nesting Depth": "Максимална дълбочина на влагане", + "Maximum length": "Максимална дължина", + "Maximum length constraint is missing": "Липсва ограничение за максимална дължина", + "Maximum length is too restrictive": "Максималната дължина е твърде ограничителна", + "Maximum number of files to process. Set to 0 to process all files.": "Максимален брой файлове за обработка. Задайте 0, за да обработите всички файлове.", + "Maximum number of items": "Максимален брой елементи", + "Maximum number of objects to process. Set to 0 to process all objects.": "Максимален брой обекти за обработка. Задайте 0, за да обработите всички обекти.", + "Maximum number of retry attempts for failed deliveries": "Максимален брой опити за повторение при неуспешни доставки", + "Maximum time to wait before committing changes": "Максимално време за изчакване преди подаване на промените", + "Maximum tokens to generate": "Максимален брой токени за генериране", + "Maximum value": "Максимална стойност", + "Maximum value constraint is missing": "Липсва ограничение за максимална стойност", + "Maximum value is too restrictive": "Максималната стойност е твърде ограничителна", + "Medium": "Средно", + "Medium Confidence": "Средна достоверност", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средни заявки: търсения с известно филтриране или множество параметри (напр. диапазони от дати, специфични регистри/схеми)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средни заявки: търсения с известно филтриране или множество параметри (напр. диапазони от дати, конкретни регистри/схеми)", + "Member removed successfully": "Членът е премахнат успешно", + "Members": "Членове", + "Members ({count})": "Членове ({count})", + "Members of selected groups can access this view": "Членовете на избраните групи могат да получат достъп до този изглед", + "Members:": "Членове:", + "Memory prediction calculated successfully": "Прогнозата за памет е изчислена успешно", + "Message content is required": "Съдържанието на съобщението е задължително", + "Message does not belong to this conversation": "Съобщението не принадлежи на този разговор", + "Message not found": "Съобщението не е намерено", + "Metadata": "Метаданни", + "Metadata Filters": "Филтри за метаданни", + "Method": "Метод", + "Method*": "Метод*", + "Method: {method}": "Метод: {method}", + "Min Length": "Минимална дължина", + "Min execution time (ms)": "Минимално време за изпълнение (мс)", + "Min ms": "Мин. мс", + "Min result count": "Минимален брой резултати", + "Min results": "Минимум резултати", + "Minimum": "Минимум", + "Minimum length": "Минимална дължина", + "Minimum number of items": "Минимален брой елементи", + "Minimum value": "Минимална стойност", + "Minimum value constraint is missing": "Липсва ограничение за минимална стойност", + "Minimum value is too restrictive": "Минималната стойност е твърде ограничителна", + "Missing conversation or agentUuid": "Липсва разговор или agentUuid", + "Missing conversationId": "Липсва conversationId", + "Missing message": "Липсва съобщение", + "Missing object fields created successfully": "Липсващите полета на обекта са създадени успешно", + "Mode:": "Режим:", + "Model": "Модел", + "Model Name": "Име на модела", + "Modified": "Променено", + "Monthly": "Месечно", + "More sources (10-20):": "Повече източници (10-20):", + "Most Active Objects": "Най-активни обекти", + "Multiple Of": "Кратно на", + "Multiple of": "Кратно на", + "Multiple search trail deletion not implemented yet": "Изтриването на множество следи на търсене все още не е реализирано", + "Multitenancy": "Многопотребителство", + "Multitenancy settings updated successfully": "Настройките за многопотребителство са актуализирани успешно", + "My Account": "Моят акаунт", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Име", + "Name *": "Име *", + "Name*": "Име*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Кешът с имена е подгрят успешно: {count} имена са заредени за {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Кешът с имена е подгрят успешно: {count} имена са заредени за {time}. {message}", + "Names cache warmup completed": "Подгряването на кеша с имена завърши", + "Naming pattern for copies:": "Шаблон за наименуване на копията:", + "New Collection Name": "Име на нова колекция", + "New Configuration": "Нова конфигурация", + "New Conversation": "Нов разговор", + "New Properties": "Нови свойства", + "New Property": "Ново свойство", + "New User Group": "Нова потребителска група", + "New Value": "Нова стойност", + "New activity": "Нова дейност", + "New password": "Нова парола", + "New processing activity": "Нова дейност по обработка", + "New users without specific organisation membership will be automatically added to this organisation": "Новите потребители без специфично членство в организация ще бъдат автоматично добавени към тази организация", + "New verwerkingsactiviteit": "Нова verwerkingsactiviteit", + "Next": "Напред", + "Nextcloud Groups": "Nextcloud групи", + "No": "Не", + "No API tokens.": "Няма API токени.", + "No ConfigSets found": "Не са намерени ConfigSets", + "No Deck cards linked to this object": "Няма карти от Deck, свързани с този обект", + "No GitHub token configured": "Не е конфигуриран GitHub токен", + "No GitHub token provided": "Не е предоставен GitHub токен", + "No GitLab token provided": "Не е предоставен GitLab токен", + "No Organisation": "Без организация", + "No active filters": "Няма активни филтри", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Няма активни обекти за меко изтриване за схема {schema}. Използвайте „Окончателно изтриване“, за да премахнете меко изтритите обекти.", + "No activity data available": "Няма налични данни за активност", + "No activity found.": "Не е намерена активност.", + "No agents available": "Няма налични агенти", + "No agents found": "Не са намерени агенти", + "No app store cache exists yet": "Все още не съществува кеш на магазина за приложения", + "No applications are available.": "Няма налични приложения.", + "No applications found": "Не са намерени приложения", + "No approval steps for this object.": "Няма стъпки за одобрение за този обект.", + "No archived conversations": "Няма архивирани разговори", + "No audit trail entries found": "Не са намерени записи на одитни следи", + "No changes recorded": "Не са записани промени", + "No chunks to vectorize": "Няма фрагменти за векторизация", + "No collections found": "Не са намерени колекции", + "No configuration data": "Няма данни за конфигурацията", + "No configuration selected": "Не е избрана конфигурация", + "No configurations are available.": "Няма налични конфигурации.", + "No configurations found": "Не са намерени конфигурации", + "No configurations found for this application.": "Не са намерени конфигурации за това приложение.", + "No contacts linked to this object": "Няма контакти, свързани с този обект", + "No conversations yet. Create a new one to get started!": "Все още няма разговори. Създайте нов, за да започнете!", + "No dashboards yet": "Все още няма табла", + "No data available": "Няма налични данни", + "No data available for chart": "Няма налични данни за диаграмата", + "No deleted items found": "Не са намерени изтрити елементи", + "No deletion data available": "Няма налични данни за изтриване", + "No description found": "Не е намерено описание", + "No description provided": "Не е предоставено описание", + "No emails linked to this object": "Няма имейли, свързани с този обект", + "No endpoints defined": "Няма дефинирани крайни точки", + "No entities detected for this email.": "Не са открити субекти за този имейл.", + "No entities detected in this file. Run text extraction first.": "Не са открити субекти в този файл. Първо стартирайте извличане на текст.", + "No entities found": "Не са намерени субекти", + "No entities have been detected yet": "Все още не са открити субекти", + "No events found": "Не са намерени събития", + "No events linked to this object": "Няма събития, свързани с този обект", + "No expired audit trails found to clear": "Не са намерени изтекли одитни следи за изчистване", + "No expired search trails found to clear": "Не са намерени изтекли следи на търсене за изчистване", + "No extraction data available for this file": "Няма налични данни за извличане за този файл", + "No facetable fields available. Select a register and schema to see available filters.": "Няма налични полета за фасетиране. Изберете регистър и схема, за да видите наличните филтри.", + "No files found": "Не са намерени файлове", + "No files have been extracted yet": "Все още не са извлечени файлове", + "No files selected": "Няма избрани файлове", + "No files to process": "Няма файлове за обработка", + "No files to reindex": "Няма файлове за повторно индексиране", + "No filters are currently active. This will delete ALL audit trail entries!": "В момента няма активни филтри. Това ще изтрие ВСИЧКИ записи на одитни следи!", + "No groups found. Try a different search.": "Не са намерени групи. Опитайте с различно търсене.", + "No label": "Без етикет", + "No labels": "Без етикети", + "No linked objects": "Няма свързани обекти", + "No log entries found": "Не са намерени записи в дневника", + "No logs are available for this configuration.": "Няма налични дневници за тази конфигурация.", + "No logs are available for this source.": "Няма налични дневници за този източник.", + "No logs found": "Не са намерени дневници", + "No matches": "Няма съвпадения", + "No mismatched fields found - SOLR schema is properly configured": "Не са намерени несъответстващи полета - SOLR схемата е правилно конфигурирана", + "No objects are currently selected for deletion.": "В момента няма избрани обекти за изтриване.", + "No objects are currently selected for permanent deletion.": "В момента няма избрани обекти за окончателно изтриване.", + "No objects are currently selected for restoration.": "В момента няма избрани обекти за възстановяване.", + "No objects found": "Не са намерени обекти", + "No objects found to vectorize": "Не са намерени обекти за векторизация", + "No objects linked to this email": "Няма обекти, свързани с този имейл", + "No objects selected": "Няма избрани обекти", + "No objects selected for deletion": "Не са избрани обекти за изтриване", + "No objects selected for restoration": "Няма избрани обекти за възстановяване", + "No objects to delete": "Няма обекти за изтриване", + "No objects to delete for schema {schema}": "Няма обекти за изтриване за схема {schema}", + "No objects were permanently deleted": "Не бяха окончателно изтрити обекти", + "No organisations found": "Не са намерени организации", + "No personal data was found for this subject identifier.": "Не са намерени лични данни за този идентификатор на субект.", + "No processing activities yet": "Все още няма дейности по обработка", + "No properties found": "Не са намерени свойства", + "No properties match your filters.": "Няма свойства, отговарящи на Вашите филтри.", + "No purge date set": "Не е зададена дата за изчистване", + "No register data available": "Няма налични данни за регистъра", + "No register objects reference this file": "Никакви обекти на регистъра не препращат към този файл", + "No registers found": "Не са намерени регистри", + "No registers found for this application.": "Не са намерени регистри за това приложение.", + "No registers found. Create a register to configure permissions.": "Не са намерени регистри. Създайте регистър, за да конфигурирате разрешения.", + "No related cases found for this sender": "Не са намерени свързани казуси за този подател", + "No related entities": "Няма свързани субекти", + "No relations found": "Не са намерени връзки", + "No request body available": "Няма налично тяло на заявката", + "No response body available": "Няма налично тяло на отговора", + "No saved views yet. Create one in the Search tab!": "Все още няма запазени изгледи. Създайте един в раздела Търсене!", + "No saved views yet. create one in the search tab!": "Все още няма запазени изгледи. създайте такъв в раздела за търсене!", + "No scan run yet": "Все още не е изпълнено сканиране", + "No schema data available": "Няма налични данни за схемата", + "No schema selected for exploration": "Не е избрана схема за проучване", + "No schemas configured for mail linking.": "Няма конфигурирани схеми за свързване на поща.", + "No schemas found": "Не са намерени схеми", + "No schemas found for this application.": "Не са намерени схеми за това приложение.", + "No search terms data available": "Няма налични данни за термини за търсене", + "No search trail entries found": "Не са намерени записи на следи на търсене", + "No sources are available.": "Няма налични източници.", + "No sources found": "Не са намерени източници", + "No synchronizations found": "Не са намерени синхронизации", + "No templates found": "Не са намерени шаблони", + "No templates have been created yet": "Все още не са създадени шаблони", + "No tools available. Tools can be registered by installed apps.": "Няма налични инструменти. Инструментите могат да бъдат регистрирани от инсталирани приложения.", + "No users in this organisation.": "Няма потребители в тази организация.", + "No valid magic-mapped register+schema combinations found": "Не са намерени валидни комбинации регистър+схема с магическо съпоставяне", + "No views found. Create views first before configuring vectorization.": "Не са намерени изгледи. Първо създайте изгледи, преди да конфигурирате векторизация.", + "No views match your search": "Няма изгледи, отговарящи на Вашето търсене", + "No webhooks found": "Не са намерени уебхукове", + "No webhooks have been configured yet": "Все още не са конфигурирани уебхукове", + "No workflows found in this project. Create workflows in the n8n editor.": "Не са намерени работни потоци в този проект. Създайте работни потоци в редактора на n8n.", + "None": "Няма", + "Not Found": "Не е намерено", + "Not extracted": "Не е извлечено", + "Not helpful": "Не е полезно", + "Not in use": "Не е в употреба", + "Note:": "Бележка:", + "Notifications": "Известия", + "Number Constraints": "Числови ограничения", + "Number of Sources": "Брой източници", + "Number of chunks to vectorize in one API call (1-100).": "Брой фрагменти за векторизация в едно API повикване (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Брой фрагменти за векторизация в едно API повикване. По-висок = по-бързо, но повече памет. Препоръчително: 10-50.", + "Number of objects to process in each batch (1-5000).": "Брой обекти за обработка във всяка партида (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Брой обекти за векторизация в едно API повикване. По-висок = по-бързо, но повече памет. Препоръчително: 10-50.", + "Numeric Range:": "Числов диапазон:", + "Object": "Обект", + "Object #{id}": "Обект #{id}", + "Object Collection": "Колекция от обекти", + "Object Configuration:": "Конфигурация на обект:", + "Object Count Prediction": "Прогноза за брой обекти", + "Object Extraction Mode": "Режим на извличане на обекти", + "Object ID": "Идентификатор на обект", + "Object Management": "Управление на обекти", + "Object Text Extraction": "Извличане на текст от обекти", + "Object Vectorization": "Векторизация на обекти", + "Object Warmup": "Подгряване на обекти", + "Object changes in owned objects": "Промени в обекти в притежавани обекти", + "Object collection not configured": "Колекцията от обекти не е конфигурирана", + "Object metadata incomplete for file upload": "Метаданните на обекта са непълни за качване на файл", + "Object not found": "Обектът не е намерен", + "Object permanently deleted": "Обектът е окончателно изтрит", + "Object restored successfully": "Обектът е възстановен успешно", + "Object settings updated successfully": "Настройките на обекта са актуализирани успешно", + "Object sources": "Източници на обекти", + "Object successfully deleted": "Обектът е изтрит успешно", + "Object successfully modified": "Обектът е променен успешно", + "Object unlocked successfully": "Обектът е отключен успешно", + "Object vectorization configuration saved successfully": "Конфигурацията за векторизация на обекти е запазена успешно", + "Object vectorized successfully": "Обектът е векторизиран успешно", + "Object:": "Обект:", + "Objects": "Обекти", + "Objects Analyzed": "Анализирани обекти", + "Objects Distribution": "Разпределение на обектите", + "Objects being analyzed": "Обекти, които се анализират", + "Objects by Register": "Обекти по регистър", + "Objects by Schema": "Обекти по схема", + "Objects deletion completed for {schema}": "Изтриването на обекти завърши за {schema}", + "Objects deletion completed successfully": "Изтриването на обектите завърши успешно", + "Objects in schema": "Обекти в схемата", + "Objects successfully deleted": "Обектите са изтрити успешно", + "Objects to Process:": "Обекти за обработка:", + "Objects to be analyzed": "Обекти за анализиране", + "Objects to be deleted": "Обекти за изтриване", + "Objects to be validated": "Обекти за валидиране", + "Objects will be serialized as JSON text before vectorization": "Обектите ще бъдат сериализирани като JSON текст преди векторизация", + "Objects will be soft deleted and moved to the": "Обектите ще бъдат меко изтрити и преместени в", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Обектите ще бъдат меко изтрити (маркирани като изтрити, но запазени в базата данни). Те могат да бъдат възстановени по-късно, ако е необходимо.", + "Old Value": "Стара стойност", + "Oldest Item (days)": "Най-стар елемент (дни)", + "Ollama Chat Settings": "Настройки за чат на Ollama", + "Ollama Embedding Configuration": "Конфигурация за вграждания на Ollama", + "Ollama URL": "URL на Ollama", + "Only With Changes": "Само с промени", + "Only members of selected groups can access this organisation": "Само членове на избраните групи могат да получат достъп до тази организация", + "Open Nextcloud App Store": "Отваряне на App Store на Nextcloud", + "Open OpenRegister sidebar": "Отваряне на страничната лента на OpenRegister", + "Open Register Objects": "Обекти на Open Register", + "Open folder": "Отваряне на папка", + "Open in OpenRegister": "Отваряне в OpenRegister", + "Open n8n Editor": "Отваряне на редактора на n8n", + "OpenAI Chat Settings": "Настройки за чат на OpenAI", + "OpenAI Embedding Configuration": "Конфигурация за вграждания на OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Настройки на OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Дефинирани от оператора табла и планирани отчети. Всяко табло е първокласен обект в регистъра `reports`; джаджите се декларират в масива `widgets` на таблото и се рендират в реално време от агрегации / GraphQL.", + "Optimizing search performance...": "Оптимизиране на производителността на търсенето...", + "Optional URL-friendly identifier": "Незадължителен URL-съвместим идентификатор", + "Optional description for this facet": "Незадължително описание за този аспект", + "Optional end date property": "Незадължително свойство за крайна дата", + "Optional event description template": "Незадължителен шаблон за описание на събитие", + "Optional location property": "Незадължително свойство за местоположение", + "Optional webhook secret for signature verification": "Незадължителна тайна за уебхук за проверка на подпис", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "По избор: Добавете параметри за заявка, за да филтрирате посочената схема (напр. status=active&type=public)", + "Or": "Или", + "Order": "Ред", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Подредени езикови тагове по BCP 47. Първият език е стойността по подразбиране на регистъра и управлява резервния Accept-Language за преводими свойства.", + "Organisation": "Организация", + "Organisation Configuration": "Конфигурация на организацията", + "Organisation ID": "Идентификатор на организация", + "Organisation Members": "Членове на организацията", + "Organisation Statistics": "Статистика на организацията", + "Organisation created successfully": "Организацията е създадена успешно", + "Organisation membership changes": "Промени в членството в организацията", + "Organisation name is required": "Името на организацията е задължително", + "Organisation settings saved successfully": "Настройките на организацията са запазени успешно", + "Organisation successfully created": "Организацията е създадена успешно", + "Organisation successfully updated": "Организацията е актуализирана успешно", + "Organisational measures": "Организационни мерки", + "Organisations": "Организации", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Организация", + "Organization ID (Optional)": "Идентификатор на организация (по избор)", + "Organizations": "Организации", + "Orphaned Items": "Осиротели елементи", + "Other": "Друго", + "Output Mapping": "Изходно съпоставяне", + "Overview": "Преглед", + "Owner": "Собственик", + "Owner:": "Собственик:", + "PDF": "PDF", + "PII hits": "Попадения на PII", + "PII rows": "Редове с PII", + "Page {current} of {total}": "Страница {current} от {total}", + "Parallel Mode (Faster, more resource intensive)": "Паралелен режим (по-бърз, по-ресурсоемък)", + "Parallel:": "Паралелно:", + "Password": "Парола", + "Password changes are not supported by your authentication provider.": "Промените на паролата не се поддържат от Вашия доставчик на удостоверяване.", + "Password for SOLR authentication (optional)": "Парола за SOLR удостоверяване (по избор)", + "Password updated successfully": "Паролата е актуализирана успешно", + "Path": "Път", + "Path in repository": "Път в хранилището", + "Path where the configuration file will be saved in the repository": "Път, където конфигурационният файл ще бъде запазен в хранилището", + "Path where the register OAS file will be saved in the repository": "Път, където OAS файлът на регистъра ще бъде запазен в хранилището", + "Pattern (regex)": "Шаблон (regex)", + "Pattern Issue": "Проблем с шаблон", + "Pattern constraint is missing": "Липсва ограничение за шаблон", + "Patterns:": "Шаблони:", + "Pending": "В очакване", + "Permanent Delete Mode": "Режим на окончателно изтриване", + "Permanently Delete": "Окончателно изтриване", + "Permanently Delete ({count})": "Окончателно изтриване ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Окончателно изтриване на всички {count} меко изтрити обекта. Това не може да бъде отменено!", + "Permanently delete already soft-deleted objects": "Окончателно изтриване на вече меко изтрити обекти", + "Permanently delete this conversation?": "Окончателно изтриване на този разговор?", + "Permission Matrix": "Матрица с разрешения", + "Person": "Лице", + "Personal Data": "Лични данни", + "Personal Data Export": "Експортиране на лични данни", + "Persons": "Лица", + "Phone": "Телефон", + "Phone Numbers": "Телефонни номера", + "Photos": "Снимки", + "Please create an agent in the": "Моля, създайте агент в", + "Please select a user": "Моля, изберете потребител", + "Please select an agent to continue": "Моля, изберете агент, за да продължите", + "Please select an organisation": "Моля, изберете организация", + "Please select at least one item to import": "Моля, изберете поне един елемент за импортиране", + "Please select registers or schemas first": "Моля, първо изберете регистри или схеми", + "Please select which register and schema to use for the new object": "Моля, изберете кои регистър и схема да използвате за новия обект", + "Please try again later.": "Моля, опитайте отново по-късно.", + "Please wait while we fetch your agents.": "Моля, изчакайте, докато извличаме Вашите агенти.", + "Please wait while we fetch your applications.": "Моля, изчакайте, докато извличаме Вашите приложения.", + "Please wait while we fetch your configurations.": "Моля, изчакайте, докато извличаме Вашите конфигурации.", + "Please wait while we fetch your deleted items.": "Моля, изчакайте, докато извличаме Вашите изтрити елементи.", + "Please wait while we fetch your sources.": "Моля, изчакайте, докато извличаме Вашите източници.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Моля, изчакайте, докато окончателно изтрием SOLR колекцията. Това може да отнеме няколко момента.", + "Polls": "Анкети", + "Popular Search Terms": "Популярни термини за търсене", + "Port": "Порт", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Предпочитанията са запазени", + "Prerequisites": "Предпоставки", + "Press Enter to send, Shift+Enter for new line": "Натиснете Enter за изпращане, Shift+Enter за нов ред", + "Press enter to send, shift+enter for new line": "Натиснете enter за изпращане, shift+enter за нов ред", + "Prettify": "Форматиране", + "Preview erasure": "Визуализиране на изтриване", + "Preview:": "Визуализация:", + "Previous": "Предишен", + "Private": "Личен", + "Private Agent (Default)": "Частен Агент (По подразбиране)", + "Private agents": "Частни агенти", + "Process Name (optional)": "Име на процеса (по избор)", + "Processed {indexed} files, {failed} failed": "Обработени {indexed} файла, {failed} неуспешни", + "Processes chunks in batches with simulated parallelism.": "Обработва фрагменти на партиди със симулиран паралелизъм.", + "Processes file chunks sequentially (safest).": "Обработва фрагментите на файла последователно (най-безопасно).", + "Processes objects in chunks with simulated parallelism.": "Обработва обектите на фрагменти със симулиран паралелизъм.", + "Processes objects sequentially (safest).": "Обработва обектите последователно (най-безопасно).", + "Processing": "Обработка", + "Processing Limits": "Лимити за обработка", + "Processing...": "Обработка...", + "Project initialized successfully": "Проектът е инициализиран успешно", + "Project not found. Please initialize first.": "Проектът не е намерен. Моля, първо инициализирайте.", + "Properties": "Свойства", + "Property Behaviors": "Поведения на свойствата", + "Property Configuration:": "Конфигурация на свойството:", + "Property Filters": "Филтри за свойства", + "Property Title": "Заглавие на свойството", + "Property Type": "Тип на свойството", + "Property can be improved": "Свойството може да бъде подобрено", + "Property could not be added": "Свойството не можа да бъде добавено", + "Property could not be updated": "Свойството не можа да бъде актуализирано", + "Property name of inversed relation": "Име на свойството на обърнатата релация", + "Property successfully added": "Свойството е добавено успешно", + "Property successfully updated": "Свойството е актуализирано успешно", + "Protected": "Защитено", + "Provider": "Доставчик", + "Provider is required for testing": "Доставчикът е задължителен за тестване", + "Public": "Публичен", + "Public View": "Публичен изглед", + "Public views can be accessed by anyone in the system": "До публичните изгледи може да получи достъп всеки в системата", + "Publish": "Публикуване", + "Publish Configuration to GitHub": "Публикуване на конфигурацията в GitHub", + "Publish OAS": "Публикуване на OAS", + "Publish Objects": "Публикуване на обекти", + "Publish Register OAS to GitHub": "Публикуване на OAS на Регистър в GitHub", + "Publish all objects in this schema": "Публикуване на всички обекти в тази схема", + "Publish objects to make them publicly available": "Публикувайте обекти, за да ги направите публично достъпни", + "Published": "Публикувано", + "Published:": "Публикувано:", + "Publishing...": "Публикуване...", + "Purge": "Изчистване", + "Purge Date": "Дата на изчистване", + "Purge {count} object from database": "Прочистване на {count} обект от базата данни", + "Purge {count} objects from database": "Прочистване на {count} обекта от базата данни", + "Purpose limitation *": "Ограничение на целта *", + "Push Notifications": "Push известия", + "Query Complexity Distribution": "Разпределение на сложността на заявките", + "Query Help": "Помощ за заявки", + "Query parameter is required": "Параметърът на заявката е задължителен", + "Quota": "Квота", + "RAG": "RAG", + "RAG Configuration": "RAG конфигурация", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Настройките за RBAC са актуализирани успешно", + "Rate limits cleared successfully": "Ограниченията на честотата са изчистени успешно", + "Raw Changes Data": "Сурови данни за промените", + "Re-vectorize on object update": "Повторна векторизация при актуализиране на обект", + "Read": "Четене", + "Real-time push notification status via notify_push": "Статус на push известия в реално време чрез notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push известията в реално време са активни. Свързаните клиенти получават незабавни актуализации при създаване, актуализиране или изтриване на обекти.", + "Realtime push active": "Push в реално време е активен", + "Realtime push not available — the notify_push app is not installed": "Push в реално време не е наличен — приложението notify_push не е инсталирано", + "Reason": "Причина", + "Reason (optional)": "Причина (по избор)", + "Rebase operation completed successfully": "Операцията по rebase завърши успешно", + "Rebase operation failed: {error}": "Операцията по rebase е неуспешна: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Препоръки:", + "Recommended Type:": "Препоръчителен тип:", + "Recommended: 5 sources": "Препоръчително: 5 източника", + "Refresh": "Опресняване", + "Refresh Data": "Опресняване на данните", + "Refresh Stats": "Опресняване на статистиката", + "Refresh Workflows": "Опресняване на работните потоци", + "Refresh dashboard": "Опресняване на таблото", + "Refresh database info": "Опресняване на информацията за базата данни", + "Register": "Регистър", + "Register #{id}": "Регистър #{id}", + "Register / Schema": "Регистър / Схема", + "Register ID": "Идентификатор на регистър", + "Register Objects": "Обекти на регистъра", + "Register Overview": "Преглед на регистъра", + "Register Statistics": "Статистика на регистъра", + "Register Totals": "Общи стойности на регистъра", + "Register and schema are required": "Регистърът и схемата са задължителни", + "Register annotation": "Анотация на регистъра", + "Register depublished successfully": "Регистърът е депубликуван успешно", + "Register not found": "Регистърът не е намерен", + "Register objects": "Обекти на регистъра", + "Register objects deletion completed successfully": "Изтриването на обектите на регистъра завърши успешно", + "Register published successfully": "Регистърът е публикуван успешно", + "Register/Schema Usage": "Употреба на регистър/схема", + "Register:": "Регистър:", + "Registers": "Регистри", + "Reindex": "Повторно индексиране", + "Reindex completed": "Повторното индексиране завърши", + "Reindex completed successfully": "Повторното индексиране завърши успешно", + "Reindex completed! Processed {count} objects in {duration}s": "Преиндексирането завърши! Обработени {count} обекта за {duration}с", + "Reindex failed": "Повторното индексиране е неуспешно", + "Reindex failed: %s": "Повторното индексиране е неуспешно: %s", + "Reject": "Отхвърляне", + "Rejected": "Отхвърлено", + "Related Cases": "Свързани казуси", + "Relations": "Връзки", + "Remote Version": "Отдалечена версия", + "Remove": "Премахване", + "Remove Deck card": "Премахване на карта от Deck", + "Remove avatar": "Премахване на аватар", + "Remove contact": "Премахване на контакт", + "Remove filter": "Премахване на филтър", + "Remove from favorites": "Премахване от любими", + "Remove from list": "Премахване от списъка", + "Remove group": "Премахване на група", + "Remove link": "Премахване на връзка", + "Remove link to {name}": "Премахване на връзката към {name}", + "Remove link to {name}?": "Премахване на връзката към {name}?", + "Remove link to {title}": "Премахване на връзката към {title}", + "Remove oneOf entry": "Премахване на oneOf запис", + "Remove property after write-back": "Премахване на свойството след обратно записване", + "Remove {title}": "Премахване на {title}", + "Remove {userId} from this organisation?": "Премахване на {userId} от тази организация?", + "Removed": "Премахнато", + "Removed from favorites": "Премахнато от любими", + "Removing schema {schema}...": "Премахване на схема {schema}...", + "Rename Conversation": "Преименуване на разговор", + "Rename conversation": "Преименуване на разговор", + "Replicas": "Реплики", + "Reports": "Отчети", + "Repository": "Хранилище", + "Request Body": "Тяло на заявката", + "Request Data": "Данни на заявката", + "Request Details": "Детайли на заявката", + "Request Quota (per day)": "Квота за заявки (на ден)", + "Request account deactivation": "Заявка за деактивиране на акаунт", + "Request account deactivation. This will notify administrators for review.": "Заявете деактивиране на акаунта. Това ще уведоми администраторите за преглед.", + "Request body must be JSON with a \"query\" field": "Тялото на заявката трябва да бъде JSON с поле \"query\"", + "Request timeout in seconds": "Таймаут на заявката в секунди", + "Requested at": "Заявено на", + "Required": "Задължително", + "Required Token Scopes": "Задължителни обхвати на токена", + "Required field": "Задължително поле", + "Required status is inconsistent": "Задължителният статус е непоследователен", + "Requires Dolphin AI for OCR text extraction": "Изисква Dolphin AI за извличане на текст чрез OCR", + "Rerun Search": "Повторно изпълнение на търсенето", + "Rerunning search: {searchTerm}": "Повторно изпълнение на търсенето: {searchTerm}", + "Reset Changes": "Нулиране на промените", + "Reset Filters": "Нулиране на филтрите", + "Resource Quotas": "Квоти за ресурси", + "Response Body": "Тяло на отговора", + "Response Details": "Детайли на отговора", + "Response Time:": "Време за отговор:", + "Restore": "Възстановяване", + "Restore conversation": "Възстановяване на разговор", + "Restore or permanently delete items": "Възстановяване или окончателно изтриване на елементи", + "Restore {count} object": "Възстановяване на {count} обект", + "Restore {count} objects": "Възстановяване на {count} обекта", + "Result Count Range": "Диапазон на броя резултати", + "Results": "Резултати", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Период на запазване (продължителност по ISO-8601, напр. P10Y, P30D)", + "Retention settings updated successfully": "Настройките за запазване бяха актуализирани успешно", + "Retrieve all objects for this schema": "Извличане на всички обекти за тази схема", + "Retry": "Повторение", + "Retry Failed Extractions": "Повторение на неуспешните извличания", + "Retry Policy": "Политика за повторение", + "Retry completed": "Повторението завърши", + "Retry upload": "Повторно качване", + "Retrying...": "Повторение...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Прегледайте избраните обекти по-долу. Можете да премахнете всеки обект, който не желаете да изтриете, като щракнете върху бутона за премахване.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Прегледайте избраните обекти по-долу. Можете да премахнете всеки обект, който не желаете да включите.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Прегледайте избраните обекти по-долу. Можете да премахнете всеки обект, който не желаете да изтриете окончателно, като щракнете върху бутона за премахване. Това действие не може да бъде отменено.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Прегледайте избраните обекти по-долу. Можете да премахнете всеки обект, който не желаете да възстановите, като щракнете върху бутона за премахване. Обектите ще бъдат възстановени на първоначалното им местоположение.", + "Revoke": "Отнемане", + "Right": "Дясно", + "Risk Level": "Ниво на риск", + "Risk level": "Ниво на риск", + "Roles and Permissions": "Роли и разрешения", + "Roles updated successfully": "Ролите са актуализирани успешно", + "Run compliance scan": "Стартиране на сканиране за съответствие", + "SOLR Connection Settings": "Настройки за SOLR връзка", + "SOLR Index Warmup": "Загряване на индекса на SOLR", + "SOLR Version:": "Версия на SOLR:", + "SOLR actions menu": "Меню за SOLR действия", + "SOLR base path (usually /solr)": "Базов път на SOLR (обикновено /solr)", + "SOLR collection deleted successfully": "Колекцията на SOLR беше изтрита успешно", + "SOLR connection test failed: {error}": "Тестът за връзка със SOLR беше неуспешен: {error}", + "SOLR connection test successful!": "Тестът за връзка със SOLR беше успешен!", + "SOLR index warmup completed successfully": "Загряването на индекса на SOLR завърши успешно", + "SOLR is disabled": "SOLR е деактивиран", + "SOLR is not available or not configured": "SOLR не е наличен или не е конфигуриран", + "SOLR logging disabled": "SOLR логването е деактивирано", + "SOLR logging enabled": "SOLR логването е активирано", + "SOLR search disabled": "SOLR търсенето е деактивирано", + "SOLR search enabled": "SOLR търсенето е активирано", + "SOLR server hostname or IP address": "Име на хост или IP адрес на SOLR сървъра", + "SOLR server port number (optional, defaults to 8983)": "Номер на порт на SOLR сървъра (по избор, по подразбиране 8983)", + "SOLR settings updated successfully": "Настройките на SOLR бяха актуализирани успешно", + "SOLR setup completed successfully": "Настройката на SOLR завърши успешно", + "SOLR setup completed successfully!": "Настройката на SOLR завърши успешно!", + "SOLR setup error: %s": "Грешка при настройка на SOLR: %s", + "SOLR setup failed": "Настройката на SOLR е неуспешна", + "SOLR setup failed - check logs": "Настройката на SOLR е неуспешна - проверете дневниците", + "SOLR setup failed. Please check the configuration and try again.": "Настройката на SOLR беше неуспешна. Моля, проверете конфигурацията и опитайте отново.", + "SOLR setup failed: {error}": "Настройката на SOLR беше неуспешна: {error}", + "SOLR warmup failed: {error}": "Загряването на SOLR беше неуспешно: {error}", + "Sample Values:": "Примерни стойности:", + "Save": "Запазване", + "Save Configuration": "Запазване на конфигурацията", + "Save Connection Settings": "Запазване на настройките за връзка", + "Save Roles": "Запазване на ролите", + "Save Settings": "Запазване на настройките", + "Save changes": "Запазване на промените", + "Save current search as view": "Запазване на текущото търсене като изглед", + "Save labels": "Запазване на етикетите", + "Save tags for {name}": "Запазване на тагове за {name}", + "Save the organisation first to manage users.": "Първо запазете организацията, за да управлявате потребителите.", + "Saved Views": "Запазени изгледи", + "Saving...": "Запазване...", + "Scheduled": "Планирано", + "Schema": "Схема", + "Schema ID": "Идентификатор на схема", + "Schema Name": "Име на схемата", + "Schema Statistics": "Статистика на схемата", + "Schema annotation": "Анотация на схема", + "Schema configuration updated successfully for {schema}": "Конфигурацията на схемата беше актуализирана успешно за {schema}", + "Schema depublished successfully": "Схемата е депубликувана успешно", + "Schema objects deletion completed successfully": "Изтриването на обектите на схемата завърши успешно", + "Schema published successfully": "Схемата е публикувана успешно", + "Schema reference ($ref)": "Препратка към схема ($ref)", + "Schema successfully created": "Схемата е създадена успешно", + "Schema successfully updated": "Схемата е актуализирана успешно", + "Schema {schema} removed successfully": "Схемата {schema} беше премахната успешно", + "Schema:": "Схема:", + "Schemas": "Схеми", + "Scheme": "Схема", + "Search": "Търсене", + "Search / Views": "Търсене / Изгледи", + "Search / views": "Търсене / изгледи", + "Search Activity": "Активност на търсене", + "Search Analytics": "Анализи на търсене", + "Search Configuration": "Конфигурация на търсенето", + "Search Entities": "Търсене на субекти", + "Search Files": "Търсене на файлове", + "Search GitHub": "Търсене в GitHub", + "Search GitLab": "Търсене в GitLab", + "Search Method": "Метод за търсене", + "Search Mode": "Режим на търсене", + "Search Query": "Заявка за търсене", + "Search Results": "Резултати от търсенето", + "Search Term": "Термин за търсене", + "Search Trail Details": "Детайли на следата на търсене", + "Search Trail Management": "Управление на следите на търсене", + "Search Trail Statistics": "Статистика на следите на търсене", + "Search Trails": "Следи на търсене", + "Search Views": "Търсене на изгледи", + "Search Webhooks": "Търсене на уебхукове", + "Search agents...": "Търсене на агенти...", + "Search applications...": "Търсене на приложения...", + "Search by file name or path": "Търсене по име на файл или път", + "Search by name or URL": "Търсене по име или URL", + "Search by title or UUID...": "Търсене по заглавие или UUID...", + "Search by value": "Търсене по стойност", + "Search configurations": "Търсене на конфигурации", + "Search data sources...": "Търсене на източници на данни...", + "Search groups...": "Търсене на групи...", + "Search in Database Objects": "Търсене в обектите на базата данни", + "Search in Files": "Търсене във файлове", + "Search in Objects": "Търсене в обекти", + "Search objects": "Търсене на обекти", + "Search objects...": "Търсене на обекти...", + "Search property names...": "Търсене на имена на свойства...", + "Search registers...": "Търсене на регистри...", + "Search schemas...": "Търсене на схеми...", + "Search trail deletion not implemented yet": "Изтриването на следи на търсене все още не е реализирано", + "Search views...": "Търсене на изгледи...", + "Search {name}...": "Търсене на {name}...", + "Searching...": "Търсене...", + "Secret": "Тайна", + "Secret key for HMAC signature generation (optional)": "Таен ключ за генериране на HMAC подпис (по избор)", + "Security": "Сигурност", + "See organisations as a table": "Преглед на организациите като таблица", + "See organisations as cards": "Преглед на организациите като карти", + "Select AI Agent": "Изберете AI агент", + "Select Active Organisation": "Изберете активна организация", + "Select All": "Избор на всички", + "Select ConfigSet": "Изберете ConfigSet", + "Select File Types to Vectorize:": "Изберете типове файлове за векторизация:", + "Select HTTP method": "Изберете HTTP метод", + "Select Register and Schema": "Изберете Регистър и Схема", + "Select Views to Vectorize:": "Изберете изгледи за векторизация:", + "Select a Nextcloud group to add": "Изберете група в Nextcloud за добавяне", + "Select a branch": "Изберете клон", + "Select a date property": "Изберете свойство за дата", + "Select a model or type a custom model name": "Изберете модел или въведете персонализирано име на модел", + "Select a register": "Изберете регистър", + "Select a register...": "Изберете регистър...", + "Select a repository": "Изберете хранилище", + "Select a repository you have write access to": "Изберете хранилище, до което имате достъп за запис", + "Select a schema": "Изберете схема", + "Select a schema...": "Изберете схема...", + "Select a user": "Изберете потребител", + "Select agent type": "Изберете тип агент", + "Select all entities": "Изберете всички субекти", + "Select an AI Agent": "Изберете AI агент", + "Select an AI agent to begin chatting with your data.": "Изберете AI агент, за да започнете да чатите с Вашите данни.", + "Select an email to see linked objects": "Изберете имейл, за да видите свързаните обекти", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Изберете организация и потребител, за да ги добавите като член. Търсете организации по име.", + "Select backend": "Изберете бекенд", + "Select chat model": "Изберете чат модел", + "Select collection for files": "Изберете колекция за файлове", + "Select collection for objects": "Изберете колекция за обекти", + "Select configuration type...": "Изберете тип конфигурация...", + "Select default organisation": "Изберете организация по подразбиране", + "Select entity {value}": "Изберете субект {value}", + "Select event to listen to...": "Изберете събитие за прослушване...", + "Select fields to display": "Изберете полета за показване", + "Select files with the correct extension": "Изберете файлове с правилното разширение", + "Select group": "Изберете група", + "Select groups (optional)": "Изберете групи (по избор)", + "Select groups to notify...": "Изберете групи за уведомяване...", + "Select groups with access to this agent": "Изберете групи с достъп до този агент", + "Select groups...": "Изберете групи...", + "Select model": "Изберете модел", + "Select one or more registers": "Изберете един или повече регистри", + "Select one or more schemas": "Изберете една или повече схеми", + "Select options...": "Изберете опции...", + "Select or create labels, or select \"No label\" to add files": "Изберете или създайте етикети, или изберете „Без етикет“, за да добавите файлове", + "Select owner application (optional)...": "Изберете приложение собственик (по избор)...", + "Select period": "Изберете период", + "Select property to send as payload": "Изберете свойство за изпращане като полезен товар", + "Select provider": "Изберете доставчик", + "Select registers and schemas to save a view": "Изберете регистри и схеми, за да запазите изглед", + "Select retry policy": "Изберете политика за повторение", + "Select role": "Изберете роля", + "Select schemas (empty = all schemas)": "Изберете схеми (празно = всички схеми)", + "Select schemas to warm up": "Изберете схеми за загряване", + "Select search mode": "Изберете режим на търсене", + "Select source type...": "Изберете тип източник...", + "Select target type": "Изберете целеви тип", + "Select the branch to publish to": "Изберете клона, в който да публикувате", + "Select the event this webhook should listen to": "Изберете събитието, което този уебхук трябва да прослушва", + "Select views to filter data (optional)": "Изберете изгледи за филтриране на данните (по избор)", + "Select views to limit which data the agent can access": "Изберете изгледи, за да ограничите до кои данни има достъп агентът", + "Select views to vectorize:": "Изберете изгледи за векторизация:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Изберете кои групи в Nextcloud са налични за тази организация. Потребителите в тези групи ще имат достъп до ресурсите на организацията.", + "Select which data views the AI can search": "Изберете кои изгледи на данни може да търси AI", + "Select which property from the event should be used as the webhook payload data": "Изберете кое свойство от събитието да се използва като данни за полезния товар на уебхука", + "Select which tools the AI can use to perform actions": "Изберете кои инструменти може да използва AI за извършване на действия", + "Select {fieldName} values": "Изберете стойности за {fieldName}", + "Selected": "Избрано", + "Selected Groups": "Избрани групи", + "Selected Objects ({count})": "Избрани обекти ({count})", + "Selected audit trails deleted successfully": "Избраните одитни следи са изтрити успешно", + "Selected search trails deleted successfully": "Избраните следи на търсене са изтрити успешно", + "Selected users can access this view": "Избраните потребители могат да получат достъп до този изглед", + "Send additional feedback": "Изпращане на допълнителна обратна връзка", + "Send as CloudEvent": "Изпращане като CloudEvent", + "Send as cloudevent": "Изпращане като cloudevent", + "Sensitive PII": "Чувствителни лични данни", + "Serial Mode (Safer, slower)": "Сериен режим (по-безопасен, по-бавен)", + "Serial:": "Серийно:", + "Server Configuration": "Конфигурация на сървъра", + "Server Information": "Информация за сървъра", + "Set as Active": "Задаване като активна", + "Set as active organisation": "Задаване като активна организация", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Задайте ограничения за използването на API и потреблението на токени. Използвайте 0 за неограничени ресурси.", + "Settings": "Настройки", + "Settings updated successfully": "Настройките бяха актуализирани успешно", + "Shards": "Фрагменти", + "Share": "Споделяне", + "Share with Groups": "Споделяне с групи", + "Share with Users": "Споделяне с потребители", + "Show": "Показване", + "Show Filters": "Показване на филтрите", + "Show less": "Показване на по-малко", + "Show only entries with changes": "Показване само на записи с промени", + "Showing {count} dashboard(s)": "Показани са {count} табла", + "Showing {count} processing activities": "Показани са {count} дейности по обработка", + "Showing {showing} of {total} agents": "Показани са {showing} от {total} агенти", + "Showing {showing} of {total} applications": "Показани са {showing} от {total} приложения", + "Showing {showing} of {total} audit trail entries": "Показани са {showing} от {total} записа в одитната следа", + "Showing {showing} of {total} configurations": "Показани са {showing} от {total} конфигурации", + "Showing {showing} of {total} deleted items": "Показани са {showing} от {total} изтрити елемента", + "Showing {showing} of {total} entities": "Показани са {showing} от {total} субекта", + "Showing {showing} of {total} files": "Показани са {showing} от {total} файла", + "Showing {showing} of {total} log entries": "Показани са {showing} от {total} записа в дневника", + "Showing {showing} of {total} search trail entries": "Показани са {showing} от {total} записа в следата на търсене", + "Showing {showing} of {total} sources": "Показани са {showing} от {total} източника", + "Showing {showing} of {total} templates": "Показани са {showing} от {total} шаблона", + "Showing {showing} of {total} webhooks": "Показани са {showing} от {total} уебхука", + "Showing {shown} of {total} organisations": "Показани са {shown} от {total} организации", + "Simple": "Просто", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Прости заявки: основни текстови търсения с минимални параметри (напр. единичен термин за търсене, без разширени филтри)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Прости заявки: основни текстови търсения с минимални параметри (напр. единичен термин за търсене, без разширени филтри)", + "Size": "Размер", + "Slug": "Slug", + "Soft Delete Mode": "Режим на меко изтриване", + "Soft Deleted Items": "Меко изтрити елементи", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Меко изтриване на всички обекти за тази схема ({active} активни, {deleted} вече изтрити)", + "SolrCloud mode enabled": "Режимът SolrCloud е активиран", + "Something went wrong while adding files": "Възникна грешка при добавянето на файлове", + "Source": "Източник", + "Source successfully updated": "Източникът беше актуализиран успешно", + "Sources": "Източници", + "Sources:": "Източници:", + "Special Rights": "Специални права", + "Standalone SOLR mode": "Самостоятелен режим на SOLR", + "Start Conversation": "Започване на разговор", + "Start Date Field": "Поле за начална дата", + "Start Vectorization": "Започване на векторизация", + "Start a conversation": "Започнете разговор", + "Starting names cache warmup...": "Започване на подгряване на кеша с имена...", + "Starting validation for {schema}...": "Стартиране на валидация за {schema}...", + "Starting {action} for {schema}...": "Стартиране на {action} за {schema}...", + "Starting...": "Започване...", + "Statistics": "Статистика", + "Statistics and Metrics": "Статистика и показатели", + "Statistics refreshed": "Статистиката беше обновена", + "Stats calculation not yet implemented": "Изчисляването на статистиката все още не е реализирано", + "Status": "Статус", + "Status Code": "Код на статуса", + "Step": "Стъпка", + "Storage": "Хранилище", + "Storage Quota (MB)": "Квота за хранилище (MB)", + "String Constraints": "Ограничения за низове", + "Subject identifier (email, BSN, name, etc.)": "Идентификатор на субекта (имейл, BSN, име и т.н.)", + "Subscribe": "Абониране", + "Subscribed": "Абониран", + "Success": "Успех", + "Success Rate": "Процент на успех", + "Success Status": "Статус на успех", + "Successful": "Успешно", + "Successfully added user to organisation: {name}": "Потребителят беше успешно добавен към организацията: {name}", + "Successfully cleared {count} audit trails": "Успешно изчистени {count} одитни следи", + "Successfully cleared {count} blob storage objects": "Успешно изчистени {count} обекта в blob хранилището", + "Successfully cleared {count} search trails": "Успешно изчистени {count} следи на търсене", + "Successfully created {count} SOLR fields": "Успешно създадени {count} полета в SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Успешно изтрити {count} вграждания. Моля, повторно векторизирайте Вашите данни.", + "Successfully deleted {count} objects for {schema}": "Успешно изтрити {count} обекта за {schema}", + "Successfully fixed {count} SOLR field configurations": "Успешно поправени {count} конфигурации на полета в SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Успешно импортирани: {registers} регистри, {schemas} схеми, {objects} обекта", + "Successfully joined organisation": "Успешно присъединяване към организация", + "Successfully permanently deleted {count} objects": "Успешно окончателно изтрити {count} обекта", + "Successfully processed {count} files!": "Успешно обработени {count} файла!", + "Successfully restored {count} object": "Успешно възстановен {count} обект", + "Successfully restored {count} objects": "Успешно възстановени {count} обекта", + "Successfully saved configuration for {count} facets!": "Успешно запазена конфигурация за {count} фасета!", + "Supported File Types": "Поддържани типове файлове", + "Switch Active Organisation": "Превключване на активната организация", + "Switch Organisation": "Превключване на организация", + "Sync Table": "Синхронизиране на таблица", + "Synchronization Settings": "Настройки за синхронизация", + "Syncing magic table for {schema}...": "Синхронизиране на магическата таблица за {schema}...", + "Syncs": "Синхронизации", + "System Default": "Системно по подразбиране", + "System Prompt": "Системна подкана", + "System announcements": "Системни съобщения", + "Table": "Таблица", + "Target ID": "Целеви идентификатор", + "Target Type*": "Целеви тип*", + "Technical Description": "Техническо описание", + "Technical Details": "Технически подробности", + "Technical description for developers and administrators": "Техническо описание за разработчици и администратори", + "Technical measures": "Технически мерки", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Температура", + "Templates": "Шаблони", + "Test": "Тест", + "Test Chat": "Тестване на чат", + "Test Connection": "Тестване на връзката", + "Test Embedding": "Тестване на вграждане", + "Test endpoint executed successfully": "Тестовата крайна точка е изпълнена успешно", + "Test webhook delivered successfully": "Тестовият уебхук е доставен успешно", + "Test webhook delivery failed": "Доставката на тестовия уебхук е неуспешна", + "Test webhook sent successfully": "Тестовият уебхук е изпратен успешно", + "Testing...": "Тестване...", + "Text Chunk #{id}": "Текстов фрагмент №{id}", + "Text Extraction": "Извличане на текст", + "Text Extraction Engine": "Машина за извличане на текст", + "Text chunks": "Текстови фрагменти", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Текстовите фрагменти се генерират по време на извличането на файла и се съхраняват в базата данни. Векторизацията чете тези предварително фрагментирани файлове и ги преобразува във вграждания.", + "Text extracted successfully": "Текстът е извлечен успешно", + "Text extraction disabled": "Извличането на текст е деактивирано", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Приложението Nextcloud Calendar не е инсталирано или активирано на този сървър.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Приложението Nextcloud Contacts не е инсталирано или активирано на този сървър.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Приложението Nextcloud Deck не е инсталирано или активирано на този сървър.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Приложението Nextcloud Mail не е инсталирано или активирано на този сървър.", + "The URL where webhook events will be sent": "URL адресът, на който ще бъдат изпращани събитията на уебхука", + "The requested conversation does not exist": "Заявеният разговор не съществува", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Заявеното табло не можа да бъде заредено. Проверете URL адреса или изберете друго табло от списъка.", + "The saved view and all its search configuration": "Запазеният изглед и цялата му конфигурация за търсене", + "There are no audit trail entries matching your current filters.": "Няма записи на одитни следи, отговарящи на текущите Ви филтри.", + "There are no deleted items matching your current filters.": "Няма изтрити елементи, отговарящи на текущите Ви филтри.", + "There are no search trail entries matching your current filters.": "Няма записи на следи на търсене, отговарящи на текущите Ви филтри.", + "There are no webhook log entries matching your filters.": "Няма записи в дневника на уебхука, отговарящи на Вашите филтри.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Те ще бъдат запазени според конфигурирания период на запазване на тяхната схема и автоматично окончателно изтрити при изтичане на периода на запазване. Периодът на запазване е конфигурируем за всяка схема и може да бъде намерен в настройките на схемата.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Те ще бъдат запазени според конфигурирания период на запазване на тяхната схема и след това окончателно премахнати.", + "This action cannot be undone.": "Това действие не може да бъде отменено.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Това действие не може да бъде отменено. Уверете се, че никакви колекции не използват този ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Това действие не може да бъде отменено. Уверете се, че никоя колекция не използва този ConfigSet.", + "This action will submit a deactivation request to your administrators.": "Това действие ще изпрати заявка за деактивиране до Вашите администратори.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Този агент няма конфигурируеми изгледи или инструменти, но все пак можете да коригирате настройките на RAG по-долу.", + "This analysis may take some time": "Този анализ може да отнеме известно време", + "This audit trail entry does not contain any change information.": "Този запис на одитна следа не съдържа информация за промени.", + "This configuration has no data defined.": "Тази конфигурация няма дефинирани данни.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Тази крайна точка е остаряла. Вместо това използвайте крайни точки, базирани на фрагменти.", + "This entity has no relations to objects or files": "Този субект няма връзки с обекти или файлове", + "This is a test webhook from OpenRegister": "Това е тестов уебхук от OpenRegister", + "This is not valid JSON": "Това не е валиден JSON", + "This key already exists on this schema": "Този ключ вече съществува в тази схема", + "This schema must use Magic Table configuration to sync": "Тази схема трябва да използва конфигурация Magic Table за синхронизиране", + "This schema must use magic table configuration to sync": "Тази схема трябва да използва конфигурация на магическа таблица, за да се синхронизира", + "This source has no associated registers.": "Този източник няма свързани регистри.", + "This token will only be shown once. Copy it now.": "Този токен ще бъде показан само веднъж. Копирайте го сега.", + "This will copy the _default ConfigSet with the new name": "Това ще копира ConfigSet _default с новото име", + "This will copy the _default configset with the new name": "Това ще копира ConfigSet _default с новото име", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Това ще изтрие окончателно ВСИЧКИ вграждания (вектори) от базата данни. Ще трябва да векторизирате повторно всички обекти и файлове. Това действие не може да бъде отменено.\n\nСигурни ли сте, че искате да продължите?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Това ще изтрие окончателно ВСИЧКИ вграждания (вектори) от базата данни. Ще трябва да векторизирате повторно всички обекти и файлове. Това действие не може да бъде отменено.\\n\\nСигурни ли сте, че искате да продължите?", + "This will permanently delete:": "Това ще изтрие окончателно:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Това ще премахне окончателно {count} вече меко изтрити обекта от базата данни. Това действие не може да бъде отменено.", + "Timeout (seconds)": "Таймаут (секунди)", + "Timestamp": "Времеви маркер", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Съвет: Активирайте само изгледи, които се нуждаят от семантично търсене, за да минимизирате разходите за вграждания. Простите справочни таблици рядко се нуждаят от векторизация.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Съвет: активирайте само изгледите, които се нуждаят от семантично търсене, за да сведете до минимум разходите за вграждане. Простите справочни таблици рядко се нуждаят от векторизация.", + "Title": "Заглавие", + "Title *": "Заглавие *", + "Title Template": "Шаблон за заглавие", + "Title*": "Заглавие*", + "To (e.g. -1 year or 2025-12-31)": "До (напр. -1 година или 2025-12-31)", + "To Date": "До дата", + "To add files larger than or equal to 512MB, go to the": "За да добавите файлове, по-големи или равни на 512MB, отидете на", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "За да коригирате размера или стратегията на фрагментите, отидете на Конфигурация на файлове → Лимити за обработка.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "За да коригирате размера или стратегията на фрагментите, отидете на конфигурация на файловете → ограничения за обработка.", + "To change the embedding provider or model, go to LLM Configuration.": "За да промените доставчика на вграждания или модела, отидете на LLM конфигурация.", + "To change the embedding provider or model, go to LLM configuration.": "За да промените доставчика или модела за вграждане, отидете на конфигурация на LLM.", + "To date": "До дата", + "Toggle search sidebar": "Превключване на страничната лента за търсене", + "Token Created": "Токенът е създаден", + "Token Quota (per request)": "Квота за токени (на заявка)", + "Token copied to clipboard": "Токенът е копиран в клипборда", + "Token name": "Име на токена", + "Token revoked": "Токенът е отнет", + "Tools": "Инструменти", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Инструментите се изпълняват с разрешенията на потребителя по подразбиране на агента, когато не е активна потребителска сесия (напр. cron задачи). Конфигурирайте потребителя по подразбиране в раздела Настройки.", + "Tools respect the agent's own role and organization boundaries.": "Инструментите спазват собствената роля на агента и границите на организацията.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Инструментите спазват изгледите, разрешенията и границите на организацията на агента.", + "Top Deleters": "Най-активни изтриващи", + "Total": "Общо", + "Total Audit Trails": "Общо одитни следи", + "Total Chunks Available:": "Общо налични фрагменти:", + "Total Deleted Items": "Общо изтрити елементи", + "Total Members": "Общо членове", + "Total Objects": "Общо обекти", + "Total Objects in Database:": "Общо обекти в базата данни:", + "Total Organisations": "Общо организации", + "Total Results": "Общо резултати", + "Total Searches": "Общо търсения", + "Total Size": "Общ размер", + "Total Storage": "Общо съхранение", + "Total events": "Общо събития", + "Total:": "Общо:", + "Totals": "Общи стойности", + "Try searching by UUID or with different keywords": "Опитайте да търсите по UUID или с различни ключови думи", + "Type": "Тип", + "Type \"DELETE COLLECTION\" to confirm:": "Въведете „DELETE COLLECTION“, за да потвърдите:", + "Type Issue": "Проблем с типа", + "Type Variations:": "Вариации на типа:", + "Type filter (optional, e.g. email)": "Филтър по тип (по избор, напр. имейл)", + "Type must be \"positive\" or \"negative\"": "Типът трябва да бъде \"positive\" или \"negative\"", + "Type to search for groups": "Въведете, за да търсите групи", + "Type to search for objects...": "Въведете, за да търсите обекти...", + "Type to search for organisations": "Въведете, за да търсите организации", + "Type to search for users": "Въведете, за да търсите потребители", + "Type to search groups...": "Въведете, за да търсите групи...", + "Type to search users...": "Въведете, за да търсите потребители...", + "Type to search...": "Въведете, за да търсите...", + "Type your username to confirm": "Въведете Вашето потребителско име, за да потвърдите", + "URL": "URL", + "URL where Ollama is running": "URL, на който работи Ollama", + "UUID copied to clipboard": "UUID е копиран в клипборда", + "UUID:": "UUID:", + "Unchanged": "Непроменено", + "Unique Search Terms": "Уникални термини за търсене", + "Unique Terms": "Уникални термини", + "Unknown": "Неизвестно", + "Unknown Object": "Неизвестен обект", + "Unknown Register": "Неизвестен регистър", + "Unknown Schema": "Неизвестна схема", + "Unknown error": "Неизвестна грешка", + "Unknown widget type: {type}": "Неизвестен тип джаджа: {type}", + "Unlink email": "Премахване на връзката с имейл", + "Unlink event": "Премахване на връзката със събитие", + "Unnamed Object": "Неименуван обект", + "Untitled": "Без заглавие", + "Untitled View": "Изглед без заглавие", + "Update": "Актуализиране", + "Update Operations": "Операции за актуализиране", + "Update available: {local} → {remote}": "Налична е актуализация: {local} → {remote}", + "Update configuration: ...": "Актуализиране на конфигурацията: ...", + "Update register OAS: ...": "Актуализиране на OAS на регистъра: ...", + "Update vectors when object data changes (recommended for accurate search)": "Актуализиране на векторите при промяна на данните на обекта (препоръчително за точно търсене)", + "Updated": "Актуализирано", + "Updated At": "Актуализирано на", + "Updated:": "Актуализирано:", + "Upload new avatar": "Качване на нов аватар", + "Uploading configuration sets...": "Качване на набори от конфигурации...", + "Uptime:": "Време на работа:", + "Url": "URL", + "Usage Count": "Брой употреби", + "Use AI agents for processing and analysis": "Използване на AI агенти за обработка и анализ", + "Use Large Language Model features": "Използване на функции за големи езикови модели", + "Use SolrCloud with Zookeeper for distributed search": "Използвайте SolrCloud със Zookeeper за разпределено търсене", + "Use SolrCloud with zookeeper for distributed search": "Използване на SolrCloud със Zookeeper за разпределено търсене", + "Use VNG APIs instead of MongoDB.": "Използване на VNG API вместо MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Използване на диапазони по подразбиране (Последните 7/30/90 дни, Последната година, По-стари)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Използване на външно хранилище (напр. MongoDB) вместо вътрешното хранилище на Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Използвайте филтри, за да стесните записите на одитни следи по регистър, схема, тип действие, потребител, диапазон от дати или идентификатор на обект.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Използвайте филтри, за да стесните изтритите елементи по регистър, схема, дата на изтриване или потребителя, който ги е изтрил.", + "Use filters to narrow down entities by type or category.": "Използвайте филтри, за да стесните субектите по тип или категория.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Използвайте филтри, за да стесните записите на следи на търсене по регистър, схема, статус на успех, потребител, диапазон от дати, термини за търсене или показатели за производителност.", + "Used By Collections": "Използвано от колекции", + "Used by": "Използван от", + "User": "Потребител", + "User Agent Statistics": "Статистика на потребителския агент", + "User Description": "Потребителско описание", + "User Quota": "Потребителска квота", + "User rate limits cleared successfully": "Ограниченията на честотата на потребителя са изчистени успешно", + "User removed successfully": "Потребителят е премахнат успешно", + "User-friendly description shown in forms and help text": "Удобно за потребителя описание, показвано във формуляри и помощен текст", + "Username": "Потребителско име", + "Username for SOLR authentication (optional)": "Потребителско име за SOLR удостоверяване (по избор)", + "Users": "Потребители", + "Uses": "Употреби", + "Using Pre-Generated Chunks": "Използване на предварително генерирани фрагменти", + "Validate": "Валидиране", + "Validate Objects": "Валидиране на обекти", + "Validation completed for {schema}": "Валидацията завърши за {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Валидацията завърши за {schema}: {processed} обработени, {updated} актуализирани, {failed} неуспешни", + "Validation completed successfully": "Валидирането завърши успешно", + "Validation failed": "Валидирането е неуспешно", + "Value": "Стойност", + "Value list (split on ,)": "Списък със стойности (разделени с ,)", + "Vector Dimensions": "Векторни измерения", + "Vector Search Backend": "Бекенд за векторно търсене", + "Vector field: _embedding_": "Векторно поле: _embedding_", + "Vectorization Triggers": "Тригери за векторизация", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Векторизацията завърши: {vectorized} обекта векторизирани, {failed} неуспешни", + "Vectorization failed: {error}": "Векторизацията беше неуспешна: {error}", + "Vectorize All Files": "Векторизиране на всички файлове", + "Vectorize All Objects": "Векторизиране на всички обекти", + "Vectorize all file types": "Векторизиране на всички типове файлове", + "Vectorize all views": "Векторизиране на всички изгледи", + "Vectorize on object creation": "Векторизиране при създаване на обект", + "Vectors will be stored in your existing object and file collections": "Векторите ще бъдат съхранени във Вашите съществуващи колекции от обекти и файлове", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Проверка на свързаността със сървъра...", + "Version": "Версия", + "Version Information": "Информация за версията", + "Very High": "Много високо", + "Very high": "Много високо", + "View": "Изглед", + "View API Docs": "Преглед на API документацията", + "View API Documentation": "Преглед на документацията на API", + "View APIs": "Преглед на API", + "View Changes": "Преглед на промените", + "View Details": "Преглед на детайлите", + "View Error": "Преглед на грешката", + "View File": "Преглед на файла", + "View Full Details": "Преглед на пълните детайли", + "View Logs": "Преглед на дневниците", + "View Name": "Име на изгледа", + "View Object": "Преглед на обекта", + "View Parameters": "Преглед на параметрите", + "View Selection": "Избор на изглед", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Преглеждайте и анализирайте дневниците на следите на търсене с разширени възможности за филтриране и анализ", + "View and analyze system audit trails with advanced filtering capabilities": "Преглеждайте и анализирайте системните одитни следи с разширени възможности за филтриране", + "View and manage authorization across registers and schemas": "Преглед и управление на оторизацията през регистри и схеми", + "View deleted successfully": "Изгледът е изтрит успешно", + "View entity details and manage relations": "Преглед на детайлите на субекта и управление на връзките", + "View name": "Име на изгледа", + "View name is required": "Името на изгледа е задължително", + "View object": "Преглед на обект", + "View saved successfully!": "Изгледът е запазен успешно!", + "View search analytics and manage search logs": "Преглед на анализите на търсене и управление на дневниците на търсене", + "View successfully deleted": "Изгледът е изтрит успешно", + "View successfully updated": "Изгледът е актуализиран успешно", + "View updated successfully!": "Изгледът е актуализиран успешно!", + "View webhook delivery logs and filter by webhook": "Преглед на дневниците за доставка на уебхук и филтриране по уебхук", + "View {count} more": "Преглед на още {count}", + "Views": "Изгледи", + "Visible to end users": "Видимо за крайните потребители", + "Visible to users": "Видимо за потребителите", + "Wait for Response": "Изчакване на отговор", + "Wait for webhook response before continuing (required for request/response flows)": "Изчакване на отговора на уебхука преди продължаване (задължително за потоци заявка/отговор)", + "Waiting for cluster synchronization...": "Изчакване на синхронизация на клъстера...", + "Warmup Names Cache": "Подгряване на кеша с имена", + "Warmup failed": "Загряването беше неуспешно", + "Warning": "Предупреждение", + "Warning:": "Предупреждение:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Предупреждение: Това свойство има несъответстващи типове: {types}. Преценете дали всички обекти трябва да имат един и същ тип.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Ще сканираме всички обекти, принадлежащи към тази схема, за да открием нови свойства и да анализираме съществуващите свойства за потенциални подобрения. Процесът включва изследване на структурата на данните на всеки обект, идентифициране на свойства, които не са дефинирани в текущата схема, и намиране на възможности за подобряване на съществуващите дефиниции на свойства с по-добри ограничения, формати и правила за валидация.", + "Webhook": "Уебхук", + "Webhook Log Details": "Детайли на дневника на уебхука", + "Webhook Logs": "Дневници на уебхука", + "Webhook created successfully": "Уебхукът е създаден успешно", + "Webhook deleted": "Уебхукът е изтрит", + "Webhook retry delivered successfully": "Повторението на уебхука е доставено успешно", + "Webhook retry delivery failed": "Доставката на повторението на уебхука е неуспешна", + "Webhook updated": "Уебхукът е актуализиран", + "Webhook updated successfully": "Уебхукът е актуализиран успешно", + "Webhooks": "Уебхукове", + "Weekly": "Седмично", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Когато агентът се изпълнява без потребителска сесия (напр. планирани задачи), ще се използва контекстът на този потребител", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Когато агентът се изпълнява без потребителска сесия (напр. планирани задачи), ще се използва контекстът на този потребител.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Когато е деактивиран, този фасет ще показва само стойности от тази схема и ще включва филтър за схема, когато е избран.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Когато е активирано, запазването на този обект ще актуализира и целевите обекти, за да включи препратка обратно към този обект.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Когато е активирано, това свойство ще бъде премахнато от обекта източник след актуализирането на целевите обекти.", + "Workflow Management": "Управление на работни процеси", + "Workflows": "Работни процеси", + "Wrap webhook payload in CloudEvents format for better interoperability": "Обвиване на полезния товар на уебхука във формат CloudEvents за по-добра оперативна съвместимост", + "Wrap webhook payload in cloudevents format for better interoperability": "Обвиване на полезния товар на уебхука във формат cloudevents за по-добра оперативна съвместимост", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Да", + "You": "Вие", + "You are not yet a member of any organisations.": "Все още не сте член на никоя организация.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Можете да създавате нови ConfigSets въз основа на шаблона _default или да качвате персонализирани директно на Вашия SOLR сървър.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Можете да създавате нови ConfigSets въз основа на шаблона _default или да качвате персонализирани директно на Вашия SOLR сървър.", + "You do not have access to this conversation": "Нямате достъп до този разговор", + "You do not have permission to delete this conversation": "Нямате разрешение да изтриете този разговор", + "You do not have permission to modify this conversation": "Нямате разрешение да променяте този разговор", + "You do not have permission to restore this conversation": "Нямате разрешение да възстановите този разговор", + "You do not have permission to view the permission matrix. Admin access is required.": "Нямате разрешение да преглеждате матрицата с разрешения. Изисква се администраторски достъп.", + "You have selected {count} properties to add to the schema.": "Избрали сте {count} свойства за добавяне към схемата.", + "You must be logged in to favorite views": "Трябва да сте влезли, за да добавяте изгледи към любими", + "You need an AI agent to start a conversation.": "Нуждаете се от AI агент, за да започнете разговор.", + "Your Fireworks AI API key. Get one at": "Вашият Fireworks AI API ключ. Вземете такъв на", + "Your OpenAI API key. Get one at": "Вашият OpenAI API ключ. Вземете такъв на", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Вашата обратна връзка е записана. По избор можете да предоставите допълнителни детайли тук...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Вашата обратна връзка беше записана. по желание можете да предоставите допълнителни подробности тук...", + "Zookeeper Hosts": "Хостове на Zookeeper", + "Zookeeper Port": "Порт на Zookeeper", + "Zookeeper Settings (SolrCloud)": "Настройки на Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Низ за връзка със Zookeeper за SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Номер на порт на Zookeeper (по избор, по подразбиране 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ПРЕДУПРЕЖДЕНИЕ ЗА ОКОНЧАТЕЛНО ИЗТРИВАНЕ \\u26A0\\uFE0F\n\nНа път сте да изтриете ОКОНЧАТЕЛНО ВСИЧКИ обекти за схема \"{schema}\":\n\n\\u2022 Активни обекти: {active}\n\\u2022 Меко изтрити обекти: {deleted}\n\\u2022 Общо: {total}\n\nТези обекти ще бъдат напълно премахнати от базата данни и НЕ МОГАТ да бъдат възстановени.\n\nНапълно ли сте сигурни?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ПРЕДУПРЕЖДЕНИЕ ЗА ОКОНЧАТЕЛНО ИЗТРИВАНЕ \\u26A0\\uFE0F\n\nНа път сте да изтриете ОКОНЧАТЕЛНО {count} меко изтрити обекта за схема \"{schema}\".\n\nТези обекти ще бъдат напълно премахнати от базата данни и НЕ МОГАТ да бъдат възстановени.\n\nНапълно ли сте сигурни?", + "and accessible to all users in selected groups (or all users if no groups selected).": "и достъпни за всички потребители в избраните групи (или всички потребители, ако не са избрани групи).", + "and add the files there.": "и добавете файловете там.", + "approved": "одобрено", + "are only accessible to invited users.": "са достъпни само за поканени потребители.", + "by": "от", + "chunk": "фрагмент", + "chunks": "фрагменти", + "chunks processed": "обработени фрагменти", + "completed": "завършено", + "configuration(s)": "конфигурация(и)", + "conversationId is required": "conversationId е задължителен", + "create": "създаване", + "delete": "изтриване", + "deleted objects section": "секция за изтрити обекти", + "e.g., lib/Settings/config.json": "напр. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "напр. lib/Settings/register.json", + "e.g., lib/settings/config.json": "напр. lib/settings/config.json", + "e.g., lib/settings/register.json": "напр. lib/settings/register.json", + "execute": "изпълнение", + "failed": "неуспешно", + "file": "файл", + "files": "файлове", + "file{plural}": "файл{plural}", + "folder": "папка", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "в {register}", + "in_progress": "в процес", + "inherited": "наследено", + "list": "списък", + "log{plural}": "дневник{plural}", + "manage": "управление", + "members": "членове", + "menu or contact someone with permission to create agents.": "меню или се свържете с някого, който има разрешение да създава агенти.", + "message content is required": "съдържанието на съобщението е задължително", + "messages": "съобщения", + "missing": "липсва", + "myapp": "myapp", + "n8n URL and API key are required": "URL на n8n и API ключ са задължителни", + "n8n configuration saved successfully": "Конфигурацията на n8n е запазена успешно", + "n8n connection not configured": "Връзката с n8n не е конфигурирана", + "n8n connection successful": "Връзката с n8n е успешна", + "n8n connection test failed: {message}": "Тестът за връзка с n8n беше неуспешен: {message}", + "n8n connection test successful": "Тестът на връзката с n8n е успешен", + "n8n initialization failed: {message}": "Инициализацията на n8n беше неуспешна: {message}", + "n8n integration disabled": "Интеграцията с n8n е деактивирана", + "n8n integration enabled": "Интеграцията с n8n е активирана", + "n8n project initialized successfully": "Проектът на n8n е инициализиран успешно", + "n8n settings saved successfully": "Настройките на n8n са запазени успешно", + "notify_push configuration guide": "ръководство за конфигуриране на notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push е инсталиран, но OpenRegister все още не е потвърдил успешен push. Задействайте запазване на обект, за да активирате, или проверете конфигурацията на notify_push.", + "notify_push is installed but not yet active": "notify_push е инсталиран, но все още не е активен", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "обекти", + "objects processed": "обработени обекти", + "object{plural}": "обект{plural}", + "or pick a range": "или изберете диапазон", + "org-...": "org-...", + "pending": "в очакване", + "provides a good balance between speed and accuracy.": "осигурява добър баланс между скорост и точност.", + "public": "публичен", + "read": "четене", + "register(s)": "регистър(и)", + "register(s) selected": "регистър(и) избрани", + "register{plural}": "регистър{plural}", + "rejected": "отхвърлено", + "required": "задължително", + "results": "резултати", + "schema(s)": "схема(и)", + "schema(s) selected": "схема(и) избрани", + "schema{plural}": "схема{plural}", + "selected objects": "избрани обекти", + "set": "задаване", + "sk-...": "sk-...", + "this application": "това приложение", + "to": "до", + "type must be \"positive\" or \"negative\"": "типът трябва да бъде \"positive\" или \"negative\"", + "update": "актуализиране", + "vectorized": "векторизирано", + "view": "преглед", + "views selected": "избрани изгледи", + "waiting": "изчакване", + "{active} active, {deleted} deleted": "{active} активни, {deleted} изтрити", + "{count} audit trails cleared successfully": "{count} одитни следи бяха изчистени успешно", + "{count} compliance issue(s) detected": "Открити са {count} проблема със съответствието", + "{count} deletions": "{count} изтривания", + "{count} documents found": "Намерени са {count} документа", + "{count} email": "{count} имейл", + "{count} emails": "{count} имейла", + "{count} entries": "{count} записа", + "{count} issues": "{count} проблема", + "{count} members": "{count} членове", + "{count} searches": "{count} търсения", + "{count} selected": "{count} избрани", + "{count} widget(s)": "{count} джаджи", + "{matched} object(s) matched. {erased} erased.": "{matched} обекта съвпаднаха. {erased} изтрити.", + "{minutes} minutes ago": "преди {minutes} минути", + "{percentage}% of objects": "{percentage}% от обектите", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} в {register} / {schema}", + "{type} cache cleared successfully": "Кешът {type} беше изчистен успешно", + "ℹ️ Current Configuration": "ℹ️ Текуща конфигурация", + "ℹ️ current configuration": "ℹ️ текуща конфигурация", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ ОПАСНОСТ: Всички {total} обекта ще бъдат ОКОНЧАТЕЛНО ИЗТРИТИ от базата данни. Това действие е НЕОБРАТИМО и не може да бъде отменено!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕЖДЕНИЕ ЗА ОКОНЧАТЕЛНО ИЗТРИВАНЕ ⚠️\n\nНа път сте да изтриете ОКОНЧАТЕЛНО ВСИЧКИ обекти за схема \"{schema}\":\n\n• Активни обекти: {active}\n• Меко изтрити обекти: {deleted}\n• Общо: {total}\n\nТези обекти ще бъдат напълно премахнати от базата данни и НЕ МОГАТ да бъдат възстановени.\n\nНапълно ли сте сигурни?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕЖДЕНИЕ ЗА ОКОНЧАТЕЛНО ИЗТРИВАНЕ ⚠️\n\nНа път сте да изтриете ОКОНЧАТЕЛНО {count} меко изтрити обекта за схема \"{schema}\".\n\nТези обекти ще бъдат напълно премахнати от базата данни и НЕ МОГАТ да бъдат възстановени.\n\nНапълно ли сте сигурни?", + "⚡ Batch Processing": "⚡ Партидна обработка", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR може да обработва милиони документи с време за отговор на търсенето под една секунда...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Изчерпателен контекст ✓ По-малка вероятност да пропусне детайли ✗ По-бавни отговори ✗ Може да включва по-малко релевантна информация", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ По-бързи отговори ✓ По-фокусирани отговори ✗ Може да пропусне релевантна информация", + "✨ AI Features": "✨ AI функции", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 В разпределен режим SOLR използва ZooKeeper за координация на клъстера...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Фасетното търсене позволява на потребителите да детайлизират резултатите по категории...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Знаете ли? SOLR захранва търсенето за Netflix, Apple и много други големи сайтове!", + "💰 View Selection (Cost Optimization)": "💰 Избор на изгледи (оптимизация на разходите)", + "📄 Object Serialization": "📄 Сериализация на обекти", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR предоставя богата аналитика и статистика за производителността на търсенето...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets съдържат схемата и конфигурационните файлове за Вашия индекс за търсене...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Репликацията гарантира, че Вашият индекс за търсене е наличен дори ако възли се повредят...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR е мощна корпоративна платформа за търсене, изградена върху Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Настройки за векторизация", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Автоматичното мащабиране може динамично да добавя или премахва възли в зависимост от натоварването...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Функциите за сигурност включват удостоверяване, оторизация и SSL криптиране...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} имейл", + "{count} имейла" + ], + "Purge {count} object from database": [ + "Изчистване на {count} обект от базата данни", + "Изчистване на {count} обекта от базата данни" + ], + "Restore {count} object": [ + "Възстановяване на {count} обект", + "Възстановяване на {count} обекта" + ], + "Successfully restored {count} object": [ + "Успешно възстановен {count} обект", + "Успешно възстановени {count} обекта" + ], + "Delete {count} object": [ + "Изтриване на {count} обект", + "Изтриване на {count} обекта" + ], + "Object successfully deleted": [ + "Обектът е изтрит успешно", + "Обектите са изтрити успешно" + ] + } +} diff --git a/l10n/bs.js b/l10n/bs.js new file mode 100644 index 0000000000..ff0a7d7068 --- /dev/null +++ b/l10n/bs.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(bez predmeta)", + "(no title)" : "(bez naslova)", + "(unnamed)" : "(bez naziva)", + "(untitled card)" : "(kartica bez naslova)", + "Activities" : "Aktivnosti", + "Add Endpoint" : "Dodaj krajnju tačku", + "Add Source" : "Dodaj izvor", + "Add card" : "Dodaj karticu", + "Add contact" : "Dodaj kontakt", + "All clear" : "Sve je u redu", + "Articles" : "Članci", + "Code" : "Kôd", + "Compliance" : "Usklađenost", + "Contacts" : "Kontakti", + "Create Agent" : "Kreiraj agenta", + "Create Application" : "Kreiraj aplikaciju", + "Create event" : "Kreiraj događaj", + "Erasure complete" : "Brisanje završeno", + "Erasure preview" : "Pregled brisanja", + "Expand details" : "Proširi detalje", + "Save changes" : "Sačuvaj izmjene", + "📄 Object Serialization" : "📄 Serijalizacija objekata", + "🔢 Vectorization Settings" : "🔢 Postavke vektorizacije", + "💰 View Selection (Cost Optimization)" : "💰 Odabir prikaza (optimizacija troškova)", + "AI Assistant" : "AI asistent", + "AI Chat" : "AI razgovor", + "API Key" : "API ključ", + "API tokens saved successfully" : "API tokeni su uspješno sačuvani", + "About ConfigSets" : "O ConfigSet-ovima", + "Action" : "Radnja", + "Action Distribution" : "Raspodjela radnji", + "Action:" : "Radnja:", + "Actions" : "Radnje", + "Active" : "Aktivno", + "Active Collections" : "Aktivne kolekcije", + "Active Filters" : "Aktivni filteri", + "Active Organisations" : "Aktivne organizacije", + "Active filters:" : "Aktivni filteri:", + "Active organisation set successfully" : "Aktivna organizacija je uspješno postavljena", + "Activity Period" : "Period aktivnosti", + "Add Groups" : "Dodaj grupe", + "Add Schema" : "Dodaj Šemu", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Dodajte naslove Šema, opise i informacije o Registru kako biste pružili bogatiji kontekst za pretragu", + "Add to favorites" : "Dodaj u omiljene", + "Added" : "Dodano", + "Added to favorites" : "Dodano u omiljene", + "Additional Information" : "Dodatne informacije", + "Additional feedback saved. Thank you!" : "Dodatne povratne informacije su sačuvane. Hvala vam!", + "Admin" : "Administrator", + "Advanced" : "Napredno", + "Advanced Filters" : "Napredni filteri", + "Advanced Options" : "Napredne opcije", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Napredni filteri nisu dostupni kada se koristi izvor baze podataka. Prebacite na Auto ili SOLR indeks za opcije filtriranja.", + "After" : "Nakon", + "Agent deleted successfully" : "Agent je uspješno obrisan", + "Agents" : "Agenti", + "All" : "Sve", + "All Categories" : "Sve kategorije", + "All Collections" : "Sve kolekcije", + "All Confidence Levels" : "Svi nivoi pouzdanosti", + "All ConfigSets" : "Svi ConfigSet-ovi", + "All Files" : "Sve Datoteke", + "All Levels" : "Svi nivoi", + "All Types" : "Svi tipovi", + "All Webhooks" : "Svi webhook-ovi", + "All actions" : "Sve radnje", + "All audit trails cleared successfully" : "Svi tragovi revizije su uspješno obrisani", + "All registers" : "Svi Registri", + "All schemas" : "Sve Šeme", + "All searches" : "Sve pretrage", + "All users" : "Svi korisnici", + "An error occurred while clearing audit trails" : "Došlo je do greške prilikom brisanja tragova revizije", + "An error occurred while deleting the audit trail" : "Došlo je do greške prilikom brisanja traga revizije", + "An error occurred while deleting the view" : "Došlo je do greške prilikom brisanja prikaza", + "An error occurred while permanently deleting the objects" : "Došlo je do greške prilikom trajnog brisanja Objekata", + "Analysis completed:" : "Analiza je završena:", + "Analysis steps:" : "Koraci analize:", + "Analytics" : "Analitika", + "Analyze Objects" : "Analiziraj Objekte", + "Analyze Properties" : "Analiziraj Svojstva", + "Analyze existing properties for improvement opportunities" : "Analizirajte postojeća Svojstva radi mogućnosti poboljšanja", + "Analyzing..." : "Analiziranje...", + "Any favorites and sharing settings for this view" : "Sve postavke omiljenih i dijeljenja za ovaj prikaz", + "Any user" : "Bilo koji korisnik", + "App store cache invalidated successfully" : "Keš trgovine aplikacija je uspješno poništen", + "Application deleted successfully" : "Aplikacija je uspješno obrisana", + "Applications" : "Aplikacije", + "Apply Changes" : "Primijeni izmjene", + "Archive" : "Arhiviraj", + "Archive conversation" : "Arhiviraj razgovor", + "Archived conversations are hidden from your active list" : "Arhivirani razgovori su skriveni s vaše aktivne liste", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Jeste li sigurni da želite očistiti stare tragove pretrage? Ovo će obrisati unose starije od 30 dana.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Jeste li sigurni da želite obrisati odabrane tragove revizije? Ova radnja se ne može poništiti.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Jeste li sigurni da želite obrisati odabrane tragove pretrage? Ova radnja se ne može poništiti.", + "Are you sure you want to delete this ConfigSet?" : "Jeste li sigurni da želite obrisati ovaj ConfigSet?", + "Are you sure you want to permanently delete" : "Jeste li sigurni da želite trajno obrisati", + "Ask a question..." : "Postavite pitanje...", + "Ask questions about your data using natural language" : "Postavljajte pitanja o svojim podacima koristeći prirodni jezik", + "Attempt" : "Pokušaj", + "Audit Trail Changes" : "Izmjene traga revizije", + "Audit Trail Details" : "Detalji traga revizije", + "Audit Trail Management" : "Upravljanje tragom revizije", + "Audit Trail Statistics" : "Statistika traga revizije", + "Audit Trails" : "Tragovi revizije", + "Audit trail data copied to clipboard" : "Podaci traga revizije su kopirani u međuspremnik", + "Audit trail deleted successfully" : "Trag revizije je uspješno obrisan", + "Audit trail successfully deleted" : "Trag revizije je uspješno obrisan", + "Authentication" : "Autentifikacija", + "Auto-commit disabled" : "Automatsko potvrđivanje onemogućeno", + "Auto-commit enabled" : "Automatsko potvrđivanje omogućeno", + "Auto-create Default Organisation" : "Automatski kreiraj zadanu organizaciju", + "Auto-retry failed vectorizations" : "Automatski ponovi neuspjele vektorizacije", + "Automatically commit changes to SOLR index" : "Automatski potvrdi izmjene u SOLR indeksu", + "Automatically create a default organisation if none exists when the app is initialized" : "Automatski kreiraj zadanu organizaciju ako nijedna ne postoji kada se aplikacija inicijalizuje", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Automatski generiši vektorska ugrađivanja iz tekstualnih dijelova kada se Datoteke otpreme i obrade", + "Automatically generate vector embeddings when objects are created or updated" : "Automatski generiši vektorska ugrađivanja kada se Objekti kreiraju ili ažuriraju", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Automatski ponovi neuspjele pokušaje vektorizacije (najviše 3 pokušaja)", + "Available Filters" : "Dostupni filteri", + "Available Workflows" : "Dostupni tokovi rada", + "Avg Execution Time" : "Prosječno vrijeme izvršavanja", + "Avg Members/Org" : "Prosjek članova/org", + "Avg Object Views/Session" : "Prosjek pregleda Objekata/sesija", + "Avg Response Time" : "Prosječno vrijeme odgovora", + "Avg Results/Search" : "Prosjek rezultata/pretraga", + "Avg Searches/Session" : "Prosjek pretraga/sesija", + "Back" : "Nazad", + "Back to Entities" : "Nazad na entitete", + "Back to Registers" : "Nazad na Registre", + "Back to Webhooks" : "Nazad na webhook-ove", + "Back to applications" : "Nazad na aplikacije", + "Back to entities" : "Nazad na entitete", + "Backend updated successfully. Please reload the application." : "Pozadinski sistem je uspješno ažuriran. Molimo ponovo učitajte aplikaciju.", + "Base URL (Optional)" : "Osnovni URL (opcionalno)", + "Basic Information" : "Osnovne informacije", + "Batch Size" : "Veličina serije", + "Batch extraction completed" : "Serijska ekstrakcija je završena", + "Before" : "Prije", + "Before object vectorization can work:" : "Prije nego što vektorizacija Objekata može funkcionisati:", + "Behavior Issue" : "Problem ponašanja", + "Bookmarks" : "Oznake", + "Blob storage has been retired. All objects now use magic tables." : "Blob pohrana je ukinuta. Svi Objekti sada koriste magične tabele.", + "Branch" : "Grana", + "Bulk delete operation completed successfully" : "Operacija masovnog brisanja je uspješno završena", + "Bulk save operation completed successfully" : "Operacija masovnog čuvanja je uspješno završena", + "Business Data" : "Poslovni podaci", + "Cache cleared successfully" : "Keš je uspješno obrisan", + "Calculate Sizes" : "Izračunaj veličine", + "Cancel" : "Otkaži", + "Cannot delete: objects are still attached" : "Nije moguće obrisati: Objekti su i dalje priloženi", + "Category" : "Kategorija", + "Change Type" : "Tip izmjene", + "Changes" : "Izmjene", + "Changes copied to clipboard" : "Izmjene su kopirane u međuspremnik", + "Chat Model" : "Model razgovora", + "Chat Provider" : "Pružatelj razgovora", + "Chat Provider (RAG)" : "Pružatelj razgovora (RAG)", + "Chat Settings" : "Postavke razgovora", + "Chat provider connection successful!" : "Veza s pružateljem razgovora je uspješna!", + "Chat settings" : "Postavke razgovora", + "Choose a register" : "Odaberite Registar", + "Choose a schema" : "Odaberite Šemu", + "Choose how vector similarity calculations are performed for semantic search" : "Odaberite kako se izračunavanja vektorske sličnosti izvode za semantičku pretragu", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Odaberite koje tipove Datoteka uključiti u proces vektorizacije. Samo Datoteke s ekstrahovanim tekstom i dijelovima bit će obrađene.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Odaberite koje prikaze uključiti u proces vektorizacije. Ostavite prazno za obradu svih prikaza na osnovu vaše konfiguracije.", + "Chunk" : "Dio", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Brisanje dijelova još nije implementirano. Koristite krajnje tačke zasnovane na dijelovima.", + "Chunks" : "Dijelovi", + "Chunks to Vectorize:" : "Dijelovi za vektorizaciju:", + "Cleanup Old Trails" : "Očisti stare tragove", + "Cleanup completed" : "Čišćenje je završeno", + "Clear All Embeddings" : "Obriši sva ugrađivanja", + "Clear Entries" : "Obriši unose", + "Clear Filtered Audit Trails" : "Obriši filtrirane tragove revizije", + "Clear Filters" : "Obriši filtere", + "Clear Index" : "Obriši indeks", + "Clear Selection" : "Obriši odabir", + "Clear all" : "Obriši sve", + "Clear all filters" : "Obriši sve filtere", + "Clear date range" : "Obriši raspon datuma", + "Clearing..." : "Brisanje...", + "Close" : "Zatvori", + "Collection Management" : "Upravljanje kolekcijama", + "Collection Name" : "Naziv kolekcije", + "Collection assignments updated successfully" : "Dodjele kolekcija su uspješno ažurirane", + "Collection cleared successfully" : "Kolekcija je uspješno obrisana", + "Collection copied successfully" : "Kolekcija je uspješno kopirana", + "Collection created successfully" : "Kolekcija je uspješno kreirana", + "Collection deleted successfully" : "Kolekcija je uspješno obrisana", + "Collection used to store and index file metadata and content" : "Kolekcija koja se koristi za pohranu i indeksiranje metapodataka i sadržaja Datoteka", + "Collection used to store and index object data" : "Kolekcija koja se koristi za pohranu i indeksiranje podataka Objekata", + "Collections:" : "Kolekcije:", + "Commit Message" : "Poruka potvrde", + "Commit Within (ms)" : "Potvrdi unutar (ms)", + "Commit message" : "Poruka potvrde", + "Compare current schema with real object data" : "Uporedite trenutnu Šemu sa stvarnim podacima Objekta", + "Completed" : "Završeno", + "Complex" : "Složeno", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Složeni upiti: napredne pretrage s više filtera, operatora i složenim kombinacijama parametara", + "Confidence Level" : "Nivo pouzdanosti", + "Confidence Score" : "Rezultat pouzdanosti", + "Config must be provided as an object" : "Konfiguracija mora biti pružena kao objekat", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Upravljanje ConfigSet-om", + "ConfigSet Name" : "Naziv ConfigSet-a", + "ConfigSet created successfully" : "ConfigSet je uspješno kreiran", + "ConfigSet deleted successfully" : "ConfigSet je uspješno obrisan", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSet-ovi definišu Šemu i konfiguraciju za vaše SOLR kolekcije. Oni sadrže definicije polja, analizatore i druge postavke pretrage.", + "Configuration" : "Konfiguracija", + "Configuration Data" : "Podaci konfiguracije", + "Configuration saved" : "Konfiguracija je sačuvana", + "Configurations" : "Konfiguracije", + "Configure Facets" : "Konfiguriši fasete", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurišite pružatelje velikih jezičkih modela (LLM) za funkcije pokretane AI-jem, uključujući semantičku pretragu, ugrađivanja i razgovor.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurišite osnovne postavke veze za vaš SOLR server, uključujući autentifikaciju i mrežne opcije. Koristite zasebne dijaloge za upravljanje ConfigSet-om i kolekcijama za upravljanje jezgrama i kolekcijama.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurišite kako se Objekti baze podataka pretvaraju u vektorska ugrađivanja za semantičku pretragu. Objekti se direktno vektorizuju bez potrebe za ekstrakcijom teksta.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurišite kako se Objekti pretvaraju u tekst prije vektorizacije. Ove postavke utiču na kvalitet pretrage i kontekst.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurišite parametre za vektorizaciju Datoteka. Ovaj proces će generisati vektorska ugrađivanja za sve ekstrahovane dijelove Datoteka.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurišite parametre za vektorizaciju Objekata. Ovaj proces će generisati vektorska ugrađivanja za sve Objekte koji odgovaraju filterima vašeg prikaza.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurišite koje tipove podataka pretraživati i koliko izvora dohvatiti", + "Connection Failed" : "Veza nije uspjela", + "Connection Settings" : "Postavke veze", + "Connection Successful!" : "Veza je uspješna!", + "Connection failed" : "Veza nije uspjela", + "Connection protocol" : "Protokol veze", + "Connection successful" : "Veza je uspješna", + "Connection timeout in seconds" : "Istek veze u sekundama", + "Constant delay between retries (5 minutes)" : "Konstantno kašnjenje između pokušaja (5 minuta)", + "Constraint Issue" : "Problem ograničenja", + "Content Filters" : "Filteri sadržaja", + "Continue to Properties" : "Nastavi na Svojstva", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontrolišite koji prikazi Objekata trebaju biti vektorizovani kako biste smanjili API troškove. Vektorizujte samo prikaze koji imaju koristi od semantičke pretrage.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontrolišite koje prikaze i alate AI može koristiti u ovom razgovoru. Prema zadanim postavkama, sve mogućnosti agenta su omogućene.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Kontroliše nasumičnost (0 = determinističko, 2 = vrlo kreativno)", + "Conversation ID is required" : "ID razgovora je obavezan", + "Conversation archived" : "Razgovor je arhiviran", + "Conversation archived successfully" : "Razgovor je uspješno arhiviran", + "Conversation cleared successfully" : "Razgovor je uspješno obrisan", + "Conversation deleted" : "Razgovor je obrisan", + "Conversation permanently deleted" : "Razgovor je trajno obrisan", + "Conversation renamed" : "Razgovor je preimenovan", + "Conversation restored" : "Razgovor je vraćen", + "Conversation title" : "Naslov razgovora", + "Copied!" : "Kopirano!", + "Copy" : "Kopiraj", + "Copy Changes" : "Kopiraj izmjene", + "Copy Collection" : "Kopiraj kolekciju", + "Copy Data" : "Kopiraj podatke", + "Copy Full Data" : "Kopiraj sve podatke", + "Copying..." : "Kopiranje...", + "Count" : "Broj", + "Counting objects..." : "Brojanje Objekata...", + "Costs" : "Troškovi", + "Create" : "Kreiraj", + "Create Collection" : "Kreiraj kolekciju", + "Create ConfigSet" : "Kreiraj ConfigSet", + "Create New Collection" : "Kreiraj novu kolekciju", + "Create New ConfigSet" : "Kreiraj novi ConfigSet", + "Create Operations" : "Operacije kreiranja", + "Create Webhook" : "Kreiraj webhook", + "Create a copy of collection:" : "Kreiraj kopiju kolekcije:", + "Create a new ConfigSet based on the _default template" : "Kreiraj novi ConfigSet na osnovu predloška _default", + "Create a new SOLR collection from an existing ConfigSet" : "Kreiraj novu SOLR kolekciju iz postojećeg ConfigSet-a", + "Create your first AI agent to get started." : "Kreirajte svog prvog AI agenta da biste započeli.", + "Created" : "Kreirano", + "Created:" : "Kreirano:", + "Creating..." : "Kreiranje...", + "Current Filters" : "Trenutni filteri", + "Current Type:" : "Trenutni tip:", + "Custom API endpoint if using a different region" : "Prilagođena API krajnja tačka ako koristite drugu regiju", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Prilagođena HTTP zaglavlja (jedno po redu, format: Naziv-zaglavlja: vrijednost)", + "Daily" : "Dnevno", + "Dashboard" : "Kontrolna ploča", + "Data Source" : "Izvor podataka", + "Data sources" : "Izvori podataka", + "Data type does not match observed values" : "Tip podataka ne odgovara posmatranim vrijednostima", + "Database URL" : "URL baze podataka", + "Database information refreshed" : "Informacije o bazi podataka su osvježene", + "Date Range" : "Raspon datuma", + "Default" : "Zadano", + "Default Organisation" : "Zadana organizacija", + "Default: 5" : "Zadano: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Kašnjenja se udvostručuju sa svakim pokušajem (2, 4, 8 minuta...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Kašnjenja se linearno povećavaju (5, 10, 15 minuta...)", + "Delete" : "Obriši", + "Delete Application" : "Obriši aplikaciju", + "Delete Audit Trail" : "Obriši trag revizije", + "Delete Collection" : "Obriši kolekciju", + "Delete ConfigSet" : "Obriši ConfigSet", + "Delete Objects" : "Obriši Objekte", + "Delete Operations" : "Operacije brisanja", + "Delete View" : "Obriši prikaz", + "Delete all objects in this schema" : "Obriši sve Objekte u ovoj Šemi", + "Delete permanently" : "Obriši trajno", + "Delete this conversation?" : "Obrisati ovaj razgovor?", + "Delete view" : "Obriši prikaz", + "Deleted" : "Obrisano", + "Deleted By" : "Obrisao", + "Deleted Date" : "Datum brisanja", + "Deleted Items Management" : "Upravljanje obrisanim stavkama", + "Deleted This Week" : "Obrisano ove sedmice", + "Deleted Today" : "Obrisano danas", + "Deleted:" : "Obrisano:", + "Deleting..." : "Brisanje...", + "Deletion Date Range" : "Raspon datuma brisanja", + "Deletion Statistics" : "Statistika brisanja", + "Deprecated" : "Zastarjelo", + "Description" : "Opis", + "Description:" : "Opis:", + "Detect data types and patterns" : "Otkrij tipove podataka i obrasce", + "Detected At" : "Otkriveno u", + "Detected Format:" : "Otkriveni format:", + "Detected Issues:" : "Otkriveni problemi:", + "Disable" : "Onemogući", + "Disabled" : "Onemogućeno", + "Discover Files" : "Otkrij Datoteke", + "Discovered Properties" : "Otkrivena Svojstva", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Želite li trajno obrisati sve filtrirane unose traga revizije? Ova radnja se ne može poništiti.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Želite li trajno obrisati ovaj unos traga revizije? Ova radnja se ne može poništiti.", + "Documents" : "Dokumenti", + "Download API Spec" : "Preuzmi API specifikaciju", + "Edit" : "Uredi", + "Edit Register" : "Uredi Registar", + "Edit Schema" : "Uredi Šemu", + "Edit View" : "Uredi prikaz", + "Edit Webhook" : "Uredi webhook", + "Edit view" : "Uredi prikaz", + "Edit view details" : "Uredi detalje prikaza", + "Effectiveness" : "Učinkovitost", + "Email" : "E-pošta", + "Emails" : "E-pošte", + "Embedding Model" : "Model ugrađivanja", + "Embedding Provider" : "Pružatelj ugrađivanja", + "Embedding generated successfully" : "Ugrađivanje je uspješno generisano", + "Embedding provider connection successful!" : "Veza s pružateljem ugrađivanja je uspješna!", + "Enable" : "Omogući", + "Enable auto-creation" : "Omogući automatsko kreiranje", + "Enable automatic file vectorization" : "Omogući automatsku vektorizaciju Datoteka", + "Enable automatic object vectorization" : "Omogući automatsku vektorizaciju Objekata", + "Enable automatic synchronization" : "Omogući automatsku sinhronizaciju", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Omogući detaljno bilježenje za SOLR operacije (preporučeno za otklanjanje grešaka)", + "Enable faceting" : "Omogući fasetiranje", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Omogući ili onemogući LLM funkcije. Konfigurišite pružatelje i modele koristeći dugme LLM konfiguracija iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Omogući ili onemogući integraciju SOLR pretrage. Konfigurišite postavke veze koristeći dugme Postavke veze iznad.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Omogući ili onemogući integraciju n8n toka rada. Konfigurišite postavke veze ispod.", + "Enable or disable this webhook" : "Omogući ili onemogući ovaj webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Omogući vektorizaciju za sve postojeće i buduće prikaze (može povećati troškove)", + "Enabled" : "Omogućeno", + "Endpoints" : "Krajnje tačke", + "Enter ConfigSet name" : "Unesite naziv ConfigSet-a", + "Enter collection name" : "Unesite naziv kolekcije", + "Enter description (optional)..." : "Unesite opis (opcionalno)...", + "Enter new collection name" : "Unesite novi naziv kolekcije", + "Enter object ID" : "Unesite ID Objekta", + "Enter search term" : "Unesite pojam za pretragu", + "Enter view name..." : "Unesite naziv prikaza...", + "Enter webhook name" : "Unesite naziv webhook-a", + "Entities" : "Entiteti", + "Entity Information" : "Informacije o entitetu", + "Entity deleted successfully" : "Entitet je uspješno obrisan", + "Entity not found" : "Entitet nije pronađen", + "Entries to be deleted:" : "Unosi za brisanje:", + "Enum Issue" : "Problem enumeracije", + "Enum constraint is missing" : "Nedostaje ograničenje enumeracije", + "Error" : "Greška", + "Error Details" : "Detalji greške", + "Error Information" : "Informacije o grešci", + "Error Message" : "Poruka o grešci", + "Error loading application" : "Greška pri učitavanju aplikacije", + "Error loading audit trails" : "Greška pri učitavanju tragova revizije", + "Error loading entity" : "Greška pri učitavanju entiteta", + "Error loading search trails" : "Greška pri učitavanju tragova pretrage", + "Estimated Batches:" : "Procijenjene serije:", + "Estimated Cost:" : "Procijenjeni trošak:", + "Estimated Duration:" : "Procijenjeno trajanje:", + "Event" : "Događaj", + "Event Property for Payload" : "Svojstvo događaja za korisni teret", + "Events" : "Događaji", + "Example Value" : "Primjer vrijednosti", + "Exclusive Maximum" : "Isključivi maksimum", + "Exclusive Minimum" : "Isključivi minimum", + "Execution Mode" : "Način izvršavanja", + "Execution Time" : "Vrijeme izvršavanja", + "Execution Time Range" : "Raspon vremena izvršavanja", + "Existing Improvements" : "Postojeća poboljšanja", + "Exponential" : "Eksponencijalno", + "Export" : "Izvoz", + "Export completed successfully" : "Izvoz je uspješno završen", + "Export, view, or delete audit trails" : "Izvezite, pregledajte ili obrišite tragove revizije", + "Extend Schema" : "Proširi Šemu", + "Extended" : "Prošireno", + "Extensions" : "Ekstenzije", + "Extract Pending Files" : "Ekstrahuj Datoteke na čekanju", + "Extract properties from each object" : "Ekstrahuj Svojstva iz svakog Objekta", + "Extracted At" : "Ekstrahovano u", + "Extraction Status" : "Status ekstrakcije", + "Facet configuration updated successfully" : "Konfiguracija faseta je uspješno ažurirana", + "Facets discovered and configured successfully" : "Fasete su uspješno otkrivene i konfigurisane", + "Facets discovered successfully" : "Fasete su uspješno otkrivene", + "Failed" : "Nije uspjelo", + "Failed to analyze schema properties" : "Nije uspjela analiza Svojstava Šeme", + "Failed to archive conversation" : "Nije uspjelo arhiviranje razgovora", + "Failed to calculate sizes" : "Nije uspjelo izračunavanje veličina", + "Failed to clear collection" : "Nije uspjelo brisanje kolekcije", + "Failed to copy changes" : "Nije uspjelo kopiranje izmjena", + "Failed to copy data" : "Nije uspjelo kopiranje podataka", + "Failed to copy data to clipboard" : "Nije uspjelo kopiranje podataka u međuspremnik", + "Failed to create conversation" : "Nije uspjelo kreiranje razgovora", + "Failed to create or find project" : "Nije uspjelo kreiranje ili pronalaženje projekta", + "Failed to delete collection" : "Nije uspjelo brisanje kolekcije", + "Failed to delete conversation" : "Nije uspjelo brisanje razgovora", + "Failed to delete webhook" : "Nije uspjelo brisanje webhook-a", + "Failed to download API specification" : "Nije uspjelo preuzimanje API specifikacije", + "Failed to get SOLR field configuration" : "Nije uspjelo dobijanje konfiguracije SOLR polja", + "Failed to get entity categories" : "Nije uspjelo dobijanje kategorija entiteta", + "Failed to get entity statistics" : "Nije uspjelo dobijanje statistike entiteta", + "Failed to get entity types" : "Nije uspjelo dobijanje tipova entiteta", + "Failed to load LLM configuration" : "Nije uspjelo učitavanje LLM konfiguracije", + "Failed to load Nextcloud groups" : "Nije uspjelo učitavanje Nextcloud grupa", + "Failed to load conversation" : "Nije uspjelo učitavanje razgovora", + "Failed to load entities" : "Nije uspjelo učitavanje entiteta", + "Failed to load entity" : "Nije uspjelo učitavanje entiteta", + "Failed to load files" : "Nije uspjelo učitavanje Datoteka", + "Failed to load organisations" : "Nije uspjelo učitavanje organizacija", + "Failed to load templates" : "Nije uspjelo učitavanje predložaka", + "Failed to load webhooks" : "Nije uspjelo učitavanje webhook-ova", + "Failed to load workflows" : "Nije uspjelo učitavanje tokova rada", + "Failed to refresh database information" : "Nije uspjelo osvježavanje informacija o bazi podataka", + "Failed to reindex collection" : "Nije uspjelo ponovno indeksiranje kolekcije", + "Failed to rename conversation" : "Nije uspjelo preimenovanje razgovora", + "Failed to restore conversation" : "Nije uspjelo vraćanje razgovora", + "Failed to retry extraction" : "Nije uspjelo ponovno pokretanje ekstrakcije", + "Failed to retry webhook" : "Nije uspjelo ponovno pokretanje webhook-a", + "Failed to save GitHub token" : "Nije uspjelo čuvanje GitHub tokena", + "Failed to save GitLab URL" : "Nije uspjelo čuvanje GitLab URL-a", + "Failed to save GitLab token" : "Nije uspjelo čuvanje GitLab tokena", + "Failed to save additional feedback" : "Nije uspjelo čuvanje dodatnih povratnih informacija", + "Failed to save n8n configuration" : "Nije uspjelo čuvanje n8n konfiguracije", + "Failed to save roles" : "Nije uspjelo čuvanje uloga", + "Failed to save settings" : "Nije uspjelo čuvanje postavki", + "Failed to save webhook" : "Nije uspjelo čuvanje webhook-a", + "Failed to send feedback" : "Nije uspjelo slanje povratnih informacija", + "Failed to test webhook" : "Nije uspjelo testiranje webhook-a", + "Failed to update favorite status" : "Nije uspjelo ažuriranje statusa omiljenog", + "Failed to update schema properties" : "Nije uspjelo ažuriranje Svojstava Šeme", + "Failed to update view" : "Nije uspjelo ažuriranje prikaza", + "Failed to update webhook" : "Nije uspjelo ažuriranje webhook-a", + "Feedback recorded" : "Povratne informacije su zabilježene", + "Fewer sources (1-3):" : "Manje izvora (1-3):", + "Field" : "Polje", + "File" : "Datoteka", + "File Chunk Prediction" : "Predviđanje dijelova Datoteke", + "File Collection" : "Kolekcija Datoteka", + "File Management" : "Upravljanje Datotekama", + "File Name" : "Naziv Datoteke", + "File Path" : "Putanja Datoteke", + "File Type Selection" : "Odabir tipa Datoteke", + "File Vectorization" : "Vektorizacija Datoteka", + "File Warmup" : "Zagrijavanje Datoteka", + "File actions menu" : "Izbornik radnji nad Datotekama", + "File anonymized successfully" : "Datoteka je uspješno anonimizirana", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vektorizacija dijelova Datoteke još nije implementirana. Dijelovi su spremni i pohranjeni, ali usluga vektorizacije je u razvoju.", + "File collection not configured" : "Kolekcija Datoteka nije konfigurisana", + "File discovery completed" : "Otkrivanje Datoteka je završeno", + "File extraction completed" : "Ekstrakcija Datoteka je završena", + "File extraction queued" : "Ekstrakcija Datoteka je u redu čekanja", + "File indexed successfully" : "Datoteka je uspješno indeksirana", + "File is already anonymized" : "Datoteka je već anonimizirana", + "File not found" : "Datoteka nije pronađena", + "File settings updated successfully" : "Postavke Datoteke su uspješno ažurirane", + "File sources" : "Izvori Datoteka", + "File vectorization configuration saved successfully" : "Konfiguracija vektorizacije Datoteka je uspješno sačuvana", + "File vectorization started. Check the statistics section for progress." : "Vektorizacija Datoteka je započeta. Provjerite odjeljak statistike za napredak.", + "File warmup completed" : "Zagrijavanje Datoteka je završeno", + "Files" : "Datoteke", + "Files with Completed Extraction:" : "Datoteke sa završenom ekstrakcijom:", + "Files → fileCollection, Objects → objectCollection" : "Datoteke → fileCollection, Objekti → objectCollection", + "Filter Audit Trails" : "Filtriraj tragove revizije", + "Filter Deleted Items" : "Filtriraj obrisane stavke", + "Filter Objects" : "Filtriraj Objekte", + "Filter Properties" : "Filtriraj Svojstva", + "Filter Search Trails" : "Filtriraj tragove pretrage", + "Filter Statistics" : "Filtriraj statistiku", + "Filter and analyze search trail entries" : "Filtrirajte i analizirajte unose traga pretrage", + "Filter and manage audit trail entries" : "Filtrirajte i upravljajte unosima traga revizije", + "Filter and manage soft deleted items" : "Filtrirajte i upravljajte mekano obrisanim stavkama", + "Filter by object ID" : "Filtriraj po ID-u Objekta", + "Filter by search term" : "Filtriraj po pojmu za pretragu", + "Filter by webhook" : "Filtriraj po webhook-u", + "Filter data loaded automatically. Use the filters below to refine your search." : "Podaci filtera su učitani automatski. Koristite filtere ispod da poboljšate svoju pretragu.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtriraj okidače webhook-a po Svojstvima korisnog tereta (jedno po redu, format: ključ: vrijednost)", + "Filtered" : "Filtrirano", + "Filters" : "Filteri", + "Fireworks AI Chat Settings" : "Postavke Fireworks AI razgovora", + "Fireworks AI Embedding Configuration" : "Fireworks AI konfiguracija ugrađivanja", + "First" : "Prvo", + "Fixed" : "Fiksno", + "For chat and retrieval-augmented generation" : "Za razgovor i generisanje proširenо dohvatom", + "For vector embeddings and semantic search" : "Za vektorska ugrađivanja i semantičku pretragu", + "Format" : "Format", + "Format Issue" : "Problem formata", + "Format constraint is missing" : "Nedostaje ograničenje formata", + "From Date" : "Od datuma", + "From date" : "Od datuma", + "Full data copied to clipboard" : "Svi podaci su kopirani u međuspremnik", + "General Issue" : "Opšti problem", + "Generate recommendations and confidence scores" : "Generiši preporuke i rezultate pouzdanosti", + "Generate vectors immediately when new objects are created" : "Generiši vektore odmah kada se kreiraju novi Objekti", + "GitHub token is valid" : "GitHub token je valjan", + "GitHub token removed successfully" : "GitHub token je uspješno uklonjen", + "GitHub token saved successfully" : "GitHub token je uspješno sačuvan", + "GitLab URL saved successfully" : "GitLab URL je uspješno sačuvan", + "GitLab token is valid" : "GitLab token je valjan", + "GitLab token saved successfully" : "GitLab token je uspješno sačuvan", + "HTTP Method" : "HTTP metoda", + "HTTP method used to send webhook requests" : "HTTP metoda koja se koristi za slanje webhook zahtjeva", + "Headers" : "Zaglavlja", + "Health" : "Zdravlje", + "Heartbeat successful - connection kept alive" : "Otkucaj uspješan - veza održana aktivnom", + "Helpful" : "Korisno", + "Hide Filters" : "Sakrij filtere", + "Hide in forms" : "Sakrij u obrascima", + "Hide in list view" : "Sakrij u prikazu liste", + "High" : "Visoko", + "High Confidence" : "Visoka pouzdanost", + "Host" : "Domaćin", + "Hourly" : "Po satu", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Koliko duboko prelaziti ugniježdena Svojstva Objekta (1-20). Veće vrijednosti hvataju više detalja ali povećavaju veličinu vektora.", + "How often to check for updates (1-168 hours)" : "Koliko često provjeravati ažuriranja (1-168 sati)", + "How to handle retries for failed webhook deliveries" : "Kako rukovati ponovnim pokušajima za neuspjele isporuke webhook-a", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Ograničenja brzine IP-a su uspješno obrisana", + "Identify properties not in the schema" : "Identifikuj Svojstva koja nisu u Šemi", + "Immutable" : "Nepromjenjivo", + "Import" : "Uvoz", + "Import successful" : "Uvoz je uspješan", + "Improved Property" : "Poboljšano Svojstvo", + "In use" : "U upotrebi", + "Inactive" : "Neaktivno", + "Include IDs and names of related objects for better contextual search" : "Uključi ID-jeve i nazive povezanih Objekata za bolju kontekstualnu pretragu", + "Include related object references" : "Uključi reference povezanih Objekata", + "Include schema and register metadata" : "Uključi metapodatke Šeme i Registra", + "Initialization failed" : "Inicijalizacija nije uspjela", + "Initialize Project" : "Inicijalizuj projekat", + "Inspect Fields" : "Pregledaj polja", + "Inspect Index" : "Pregledaj indeks", + "Invalid" : "Nevažeće", + "Invalid batch size. Must be between 1 and 5000" : "Nevažeća veličina serije. Mora biti između 1 i 5000", + "Invalid field name provided" : "Pružen je nevažeći naziv polja", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Nevažeći maxObjects. Mora biti 0 (svi) ili pozitivan broj", + "Invalid:" : "Nevažeće:", + "Issue" : "Problem", + "Items per page:" : "Stavki po stranici:", + "Just now" : "Upravo sada", + "LLM Configuration" : "LLM konfiguracija", + "LLM actions menu" : "Izbornik LLM radnji", + "LLM configuration saved successfully" : "LLM konfiguracija je uspješno sačuvana", + "LLM features disabled" : "LLM funkcije su onemogućene", + "LLM features enabled" : "LLM funkcije su omogućene", + "LLM must be enabled with an embedding provider configured" : "LLM mora biti omogućen s konfigurisanim pružateljem ugrađivanja", + "LLM settings updated successfully" : "LLM postavke su uspješno ažurirane", + "Last" : "Zadnje", + "Last Triggered" : "Zadnje pokrenuto", + "Length Range:" : "Raspon dužine:", + "Linear" : "Linearno", + "Load Advanced Filters" : "Učitaj napredne filtere", + "Load advanced filters with live data from your search index" : "Učitaj napredne filtere sa živim podacima iz vašeg indeksa pretrage", + "Load view" : "Učitaj prikaz", + "Loading ConfigSets..." : "Učitavanje ConfigSet-ova...", + "Loading advanced filters..." : "Učitavanje naprednih filtera...", + "Loading agents..." : "Učitavanje agenata...", + "Loading application details..." : "Učitavanje detalja aplikacije...", + "Loading applications..." : "Učitavanje aplikacija...", + "Loading archived conversations..." : "Učitavanje arhiviranih razgovora...", + "Loading audit trails..." : "Učitavanje tragova revizije...", + "Loading collections..." : "Učitavanje kolekcija...", + "Loading configurations..." : "Učitavanje konfiguracija...", + "Loading conversation..." : "Učitavanje razgovora...", + "Loading conversations..." : "Učitavanje razgovora...", + "Loading deleted items..." : "Učitavanje obrisanih stavki...", + "Loading events..." : "Učitavanje događaja...", + "Loading filter data..." : "Učitavanje podataka filtera...", + "Loading groups..." : "Učitavanje grupa...", + "Loading log details..." : "Učitavanje detalja zapisa...", + "Loading registers..." : "Učitavanje Registara...", + "Loading schemas..." : "Učitavanje Šema...", + "Loading search trails..." : "Učitavanje tragova pretrage...", + "Loading sources..." : "Učitavanje Izvora...", + "Loading statistics..." : "Učitavanje statistike...", + "Loading users..." : "Učitavanje korisnika...", + "Loading views..." : "Učitavanje prikaza...", + "Loading..." : "Učitavanje...", + "Local" : "Lokalno", + "Local Version" : "Lokalna verzija", + "Locked" : "Zaključano", + "Locked:" : "Zaključano:", + "Login successful" : "Prijava uspješna", + "Logs" : "Zapisi", + "Low" : "Nisko", + "Low Confidence" : "Niska pouzdanost", + "Lucene Version:" : "Lucene verzija:", + "Magic table synchronized successfully" : "Magična tabela je uspješno sinhronizovana", + "Manage Organisation Roles" : "Upravljaj ulogama organizacije", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Upravljaj SOLR kolekcijama (spremišta podataka) i dodijeli ih za Objekte i Datoteke.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Upravljaj SOLR ConfigSet-ovima (predlošci konfiguracije) za vaše kolekcije.", + "Manage and configure agents for automated tasks" : "Upravljaj i konfiguriši agente za automatizovane zadatke", + "Manage and monitor file text extraction status" : "Upravljaj i prati status ekstrakcije teksta iz Datoteka", + "Manage and restore soft deleted items from your registers" : "Upravljaj i vrati mekano obrisane stavke iz vaših Registara", + "Manage and view detected entities from files and objects" : "Upravljaj i pregledaj otkrivene entitete iz Datoteka i Objekata", + "Manage document templates and themes" : "Upravljaj predlošcima dokumenata i temama", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Upravljaj n8n tokovima rada za OpenRegister automatizaciju. Tokovi rada bit će pohranjeni u konfigurisanom projektu.", + "Manage webhooks for event-driven integrations" : "Upravljaj webhook-ovima za integracije vođene događajima", + "Manage your applications and modules" : "Upravljaj svojim aplikacijama i modulima", + "Manage your data registers and their configurations" : "Upravljaj svojim Registrima podataka i njihovim konfiguracijama", + "Manage your data schemas and their properties" : "Upravljaj svojim Šemama podataka i njihovim Svojstvima", + "Manage your data sources and their configurations" : "Upravljaj svojim Izvorima podataka i njihovim konfiguracijama", + "Manage your saved search configurations" : "Upravljaj svojim sačuvanim konfiguracijama pretrage", + "Manage your system configurations and settings" : "Upravljaj svojim sistemskim konfiguracijama i postavkama", + "Managed" : "Upravljano", + "Max Files (0 = all)" : "Maks. Datoteka (0 = sve)", + "Max Length" : "Maks. dužina", + "Max Length:" : "Maks. dužina:", + "Max Objects (0 = all)" : "Maks. Objekata (0 = svi)", + "Max Retries" : "Maks. pokušaja", + "Max Shards/Node" : "Maks. fragmenata/čvor", + "Max execution time (ms)" : "Maks. vrijeme izvršavanja (ms)", + "Max ms" : "Maks. ms", + "Max result count" : "Maks. broj rezultata", + "Max results" : "Maks. rezultata", + "Maximum" : "Maksimum", + "Maximum Nesting Depth" : "Maksimalna dubina ugniježdenja", + "Maximum length constraint is missing" : "Nedostaje ograničenje maksimalne dužine", + "Maximum length is too restrictive" : "Maksimalna dužina je previše restriktivna", + "Maximum number of files to process. Set to 0 to process all files." : "Maksimalan broj Datoteka za obradu. Postavite na 0 za obradu svih Datoteka.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maksimalan broj Objekata za obradu. Postavite na 0 za obradu svih Objekata.", + "Maximum number of retry attempts for failed deliveries" : "Maksimalan broj pokušaja ponavljanja za neuspjele isporuke", + "Maximum time to wait before committing changes" : "Maksimalno vrijeme čekanja prije potvrde izmjena", + "Maximum tokens to generate" : "Maksimalan broj tokena za generisanje", + "Maximum value constraint is missing" : "Nedostaje ograničenje maksimalne vrijednosti", + "Maximum value is too restrictive" : "Maksimalna vrijednost je previše restriktivna", + "Medium" : "Srednje", + "Medium Confidence" : "Srednja pouzdanost", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Srednji upiti: pretrage s nekim filtriranjem ili više parametara (npr. rasponi datuma, specifični Registri/Šeme)", + "Members of selected groups can access this view" : "Članovi odabranih grupa mogu pristupiti ovom prikazu", + "Members:" : "Članovi:", + "Memory prediction calculated successfully" : "Predviđanje memorije je uspješno izračunato", + "Message content is required" : "Sadržaj poruke je obavezan", + "Message does not belong to this conversation" : "Poruka ne pripada ovom razgovoru", + "Metadata Filters" : "Filteri metapodataka", + "Method" : "Metoda", + "Min Length" : "Min. dužina", + "Min execution time (ms)" : "Min. vrijeme izvršavanja (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. broj rezultata", + "Min results" : "Min. rezultata", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Nedostaje ograničenje minimalne vrijednosti", + "Minimum value is too restrictive" : "Minimalna vrijednost je previše restriktivna", + "Missing object fields created successfully" : "Nedostajuća polja Objekta su uspješno kreirana", + "Mode:" : "Način:", + "Model" : "Model", + "Model Name" : "Naziv modela", + "Modified" : "Izmijenjeno", + "Monthly" : "Mjesečno", + "More sources (10-20):" : "Više izvora (10-20):", + "Most Active Objects" : "Najaktivniji Objekti", + "Multiple Of" : "Višekratnik od", + "Name" : "Naziv", + "Names cache warmup completed" : "Zagrijavanje keša naziva je završeno", + "New Collection Name" : "Naziv nove kolekcije", + "New Conversation" : "Novi razgovor", + "New Properties" : "Nova Svojstva", + "New Property" : "Novo Svojstvo", + "New Value" : "Nova vrijednost", + "New users without specific organisation membership will be automatically added to this organisation" : "Novi korisnici bez specifičnog članstva u organizaciji bit će automatski dodani ovoj organizaciji", + "Next" : "Sljedeće", + "No" : "Ne", + "No ConfigSets found" : "Nije pronađen nijedan ConfigSet", + "No GitHub token configured" : "Nijedan GitHub token nije konfigurisan", + "No GitHub token provided" : "Nijedan GitHub token nije pružen", + "No GitLab token provided" : "Nijedan GitLab token nije pružen", + "No Organisation" : "Nema organizacije", + "No active filters" : "Nema aktivnih filtera", + "No activity data available" : "Nema dostupnih podataka o aktivnosti", + "No agents available" : "Nema dostupnih agenata", + "No agents found" : "Nije pronađen nijedan agent", + "No app store cache exists yet" : "Keš trgovine aplikacija još ne postoji", + "No applications are available." : "Nema dostupnih aplikacija.", + "No applications found" : "Nije pronađena nijedna aplikacija", + "No archived conversations" : "Nema arhiviranih razgovora", + "No audit trail entries found" : "Nije pronađen nijedan unos traga revizije", + "No changes recorded" : "Nije zabilježena nijedna izmjena", + "No chunks to vectorize" : "Nema dijelova za vektorizaciju", + "No collections found" : "Nije pronađena nijedna kolekcija", + "No configuration data" : "Nema podataka konfiguracije", + "No configurations are available." : "Nema dostupnih konfiguracija.", + "No configurations found" : "Nije pronađena nijedna konfiguracija", + "No configurations found for this application." : "Nije pronađena nijedna konfiguracija za ovu aplikaciju.", + "No conversations yet. Create a new one to get started!" : "Još nema razgovora. Kreirajte novi da biste započeli!", + "No data available" : "Nema dostupnih podataka", + "No data available for chart" : "Nema dostupnih podataka za grafikon", + "No deleted items found" : "Nije pronađena nijedna obrisana stavka", + "No deletion data available" : "Nema dostupnih podataka o brisanju", + "No description found" : "Nije pronađen nijedan opis", + "No description provided" : "Nije pružen nijedan opis", + "No entities detected in this file. Run text extraction first." : "Nije otkriven nijedan entitet u ovoj Datoteci. Prvo pokrenite ekstrakciju teksta.", + "No entities found" : "Nije pronađen nijedan entitet", + "No entities have been detected yet" : "Još nije otkriven nijedan entitet", + "No events found" : "Nije pronađen nijedan događaj", + "No expired audit trails found to clear" : "Nije pronađen nijedan istekli trag revizije za brisanje", + "No facetable fields available. Select a register and schema to see available filters." : "Nema dostupnih polja za fasetiranje. Odaberite Registar i Šemu da biste vidjeli dostupne filtere.", + "No files found" : "Nije pronađena nijedna Datoteka", + "No files have been extracted yet" : "Još nije ekstrahovana nijedna Datoteka", + "No files to process" : "Nema Datoteka za obradu", + "No files to reindex" : "Nema Datoteka za ponovno indeksiranje", + "No filters are currently active. This will delete ALL audit trail entries!" : "Nijedan filter trenutno nije aktivan. Ovo će obrisati SVE unose traga revizije!", + "No log entries found" : "Nije pronađen nijedan unos zapisa", + "No logs are available for this configuration." : "Nema dostupnih zapisa za ovu konfiguraciju.", + "No logs are available for this source." : "Nema dostupnih zapisa za ovaj Izvor.", + "No logs found" : "Nije pronađen nijedan zapis", + "No mismatched fields found - SOLR schema is properly configured" : "Nije pronađeno nijedno neusklađeno polje - SOLR Šema je ispravno konfigurisana", + "No objects found" : "Nije pronađen nijedan Objekat", + "No objects found to vectorize" : "Nije pronađen nijedan Objekat za vektorizaciju", + "No objects selected for deletion" : "Nijedan Objekat nije odabran za brisanje", + "No objects to delete" : "Nema Objekata za brisanje", + "No objects were permanently deleted" : "Nijedan Objekat nije trajno obrisan", + "No properties found" : "Nije pronađeno nijedno Svojstvo", + "No properties match your filters." : "Nijedno Svojstvo ne odgovara vašim filterima.", + "No purge date set" : "Nije postavljen datum čišćenja", + "No register data available" : "Nema dostupnih podataka o Registru", + "No registers found" : "Nije pronađen nijedan Registar", + "No registers found for this application." : "Nije pronađen nijedan Registar za ovu aplikaciju.", + "No relations found" : "Nije pronađena nijedna relacija", + "No request body available" : "Nema dostupnog tijela zahtjeva", + "No response body available" : "Nema dostupnog tijela odgovora", + "No saved views yet. Create one in the Search tab!" : "Još nema sačuvanih prikaza. Kreirajte jedan na kartici Pretraga!", + "No schema data available" : "Nema dostupnih podataka o Šemi", + "No schema selected for exploration" : "Nijedna Šema nije odabrana za istraživanje", + "No schemas found" : "Nije pronađena nijedna Šema", + "No schemas found for this application." : "Nije pronađena nijedna Šema za ovu aplikaciju.", + "No search terms data available" : "Nema dostupnih podataka o pojmovima za pretragu", + "No search trail entries found" : "Nije pronađen nijedan unos traga pretrage", + "No sources are available." : "Nema dostupnih Izvora.", + "No sources found" : "Nije pronađen nijedan Izvor", + "No templates found" : "Nije pronađen nijedan predložak", + "No templates have been created yet" : "Još nije kreiran nijedan predložak", + "No valid magic-mapped register+schema combinations found" : "Nije pronađena nijedna valjana magično mapirana kombinacija Registar+Šema", + "No views found. Create views first before configuring vectorization." : "Nije pronađen nijedan prikaz. Prvo kreirajte prikaze prije konfigurisanja vektorizacije.", + "No views match your search" : "Nijedan prikaz ne odgovara vašoj pretrazi", + "No webhooks found" : "Nije pronađen nijedan webhook", + "No webhooks have been configured yet" : "Još nije konfigurisan nijedan webhook", + "No workflows found in this project. Create workflows in the n8n editor." : "Nije pronađen nijedan tok rada u ovom projektu. Kreirajte tokove rada u n8n uređivaču.", + "None" : "Nijedan", + "Not helpful" : "Nije korisno", + "Not in use" : "Nije u upotrebi", + "Number Constraints" : "Ograničenja broja", + "Number of chunks to vectorize in one API call (1-100)." : "Broj dijelova za vektorizaciju u jednom API pozivu (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Broj dijelova za vektorizaciju u jednom API pozivu. Veći = brže ali više memorije. Preporučeno: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Broj Objekata za obradu u svakoj seriji (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Broj Objekata za vektorizaciju u jednom API pozivu. Veći = brže ali više memorije. Preporučeno: 10-50.", + "Numeric Range:" : "Numerički raspon:", + "Object" : "Objekat", + "Object Collection" : "Kolekcija Objekata", + "Object Count Prediction" : "Predviđanje broja Objekata", + "Object ID" : "ID Objekta", + "Object Management" : "Upravljanje Objektima", + "Object Vectorization" : "Vektorizacija Objekata", + "Object Warmup" : "Zagrijavanje Objekata", + "Object collection not configured" : "Kolekcija Objekata nije konfigurisana", + "Object not found" : "Objekat nije pronađen", + "Object permanently deleted" : "Objekat je trajno obrisan", + "Object restored successfully" : "Objekat je uspješno vraćen", + "Object settings updated successfully" : "Postavke Objekta su uspješno ažurirane", + "Object sources" : "Izvori Objekata", + "Object unlocked successfully" : "Objekat je uspješno otključan", + "Object vectorization configuration saved successfully" : "Konfiguracija vektorizacije Objekata je uspješno sačuvana", + "Object vectorized successfully" : "Objekat je uspješno vektorizovan", + "Object:" : "Objekat:", + "Objects" : "Objekti", + "Objects Analyzed" : "Analizirani Objekti", + "Objects Distribution" : "Raspodjela Objekata", + "Objects being analyzed" : "Objekti koji se analiziraju", + "Objects by Register" : "Objekti po Registru", + "Objects by Schema" : "Objekti po Šemi", + "Objects deletion completed successfully" : "Brisanje Objekata je uspješno završeno", + "Objects in schema" : "Objekti u Šemi", + "Objects to Process:" : "Objekti za obradu:", + "Objects to be analyzed" : "Objekti za analizu", + "Objects to be deleted" : "Objekti za brisanje", + "Objects to be validated" : "Objekti za validaciju", + "Objects will be serialized as JSON text before vectorization" : "Objekti će biti serijalizovani kao JSON tekst prije vektorizacije", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekti će biti mekano obrisani (označeni kao obrisani ali zadržani u bazi podataka). Mogu se kasnije povratiti ako je potrebno.", + "Old Value" : "Stara vrijednost", + "Oldest Item (days)" : "Najstarija stavka (dani)", + "Ollama Chat Settings" : "Postavke Ollama razgovora", + "Ollama Embedding Configuration" : "Ollama konfiguracija ugrađivanja", + "Ollama URL" : "Ollama URL", + "Only With Changes" : "Samo s izmjenama", + "Open Register Objects" : "Open Register Objekti", + "Open n8n Editor" : "Otvori n8n uređivač", + "OpenAI Chat Settings" : "Postavke OpenAI razgovora", + "OpenAI Embedding Configuration" : "OpenAI konfiguracija ugrađivanja", + "OpenRegister Settings" : "OpenRegister postavke", + "Optional webhook secret for signature verification" : "Opcionalna tajna webhook-a za provjeru potpisa", + "Order" : "Redoslijed", + "Organisation" : "Organizacija", + "Organisation ID" : "ID organizacije", + "Organisation Members" : "Članovi organizacije", + "Organisation Statistics" : "Statistika organizacije", + "Organisation created successfully" : "Organizacija je uspješno kreirana", + "Organisation settings saved successfully" : "Postavke organizacije su uspješno sačuvane", + "Organisations" : "Organizacije", + "Organization" : "Organizacija", + "Organization ID (Optional)" : "ID organizacije (opcionalno)", + "Orphaned Items" : "Stavke bez roditelja", + "Owner" : "Vlasnik", + "Owner:" : "Vlasnik:", + "Parallel Mode (Faster, more resource intensive)" : "Paralelni način (brže, zahtijeva više resursa)", + "Parallel:" : "Paralelno:", + "Password" : "Lozinka", + "Password for SOLR authentication (optional)" : "Lozinka za SOLR autentifikaciju (opcionalno)", + "Path" : "Putanja", + "Path in repository" : "Putanja u repozitoriju", + "Path where the configuration file will be saved in the repository" : "Putanja gdje će konfiguracijska Datoteka biti sačuvana u repozitoriju", + "Path where the register OAS file will be saved in the repository" : "Putanja gdje će OAS Datoteka Registra biti sačuvana u repozitoriju", + "Pattern (regex)" : "Obrazac (regex)", + "Pattern Issue" : "Problem obrasca", + "Pattern constraint is missing" : "Nedostaje ograničenje obrasca", + "Patterns:" : "Obrasci:", + "Pending" : "Na čekanju", + "Permanent Delete Mode" : "Način trajnog brisanja", + "Permanently Delete" : "Trajno obriši", + "Permanently delete already soft-deleted objects" : "Trajno obriši već mekano obrisane Objekte", + "Permanently delete this conversation?" : "Trajno obrisati ovaj razgovor?", + "Person" : "Osoba", + "Personal Data" : "Lični podaci", + "Phone" : "Telefon", + "Photos" : "Fotografije", + "Please create an agent in the" : "Molimo kreirajte agenta u", + "Please select an agent to continue" : "Molimo odaberite agenta za nastavak", + "Please select which register and schema to use for the new object" : "Molimo odaberite koji Registar i Šemu koristiti za novi Objekat", + "Please try again later." : "Molimo pokušajte ponovo kasnije.", + "Please wait while we fetch your agents." : "Molimo pričekajte dok dohvatimo vaše agente.", + "Please wait while we fetch your applications." : "Molimo pričekajte dok dohvatimo vaše aplikacije.", + "Please wait while we fetch your configurations." : "Molimo pričekajte dok dohvatimo vaše konfiguracije.", + "Please wait while we fetch your deleted items." : "Molimo pričekajte dok dohvatimo vaše obrisane stavke.", + "Please wait while we fetch your sources." : "Molimo pričekajte dok dohvatimo vaše Izvore.", + "Polls" : "Ankete", + "Popular Search Terms" : "Popularni pojmovi za pretragu", + "Port" : "Port", + "Prerequisites" : "Preduslovi", + "Press Enter to send, Shift+Enter for new line" : "Pritisnite Enter za slanje, Shift+Enter za novi red", + "Previous" : "Prethodno", + "Private" : "Privatno", + "Processes chunks in batches with simulated parallelism." : "Obrađuje dijelove u serijama sa simuliranom paralelizacijom.", + "Processes file chunks sequentially (safest)." : "Obrađuje dijelove Datoteke sekvencijalno (najsigurnije).", + "Processes objects in chunks with simulated parallelism." : "Obrađuje Objekte u dijelovima sa simuliranom paralelizacijom.", + "Processes objects sequentially (safest)." : "Obrađuje Objekte sekvencijalno (najsigurnije).", + "Processing" : "Obrada", + "Processing Limits" : "Ograničenja obrade", + "Processing..." : "Obrada...", + "Project initialized successfully" : "Projekat je uspješno inicijalizovan", + "Project not found. Please initialize first." : "Projekat nije pronađen. Molimo prvo inicijalizujte.", + "Properties" : "Svojstva", + "Property Behaviors" : "Ponašanja Svojstva", + "Property Configuration:" : "Konfiguracija Svojstva:", + "Property Filters" : "Filteri Svojstava", + "Property Title" : "Naslov Svojstva", + "Property Type" : "Tip Svojstva", + "Property can be improved" : "Svojstvo se može poboljšati", + "Protected" : "Zaštićeno", + "Provider" : "Pružatelj", + "Provider is required for testing" : "Pružatelj je obavezan za testiranje", + "Public" : "Javno", + "Public View" : "Javni prikaz", + "Public views can be accessed by anyone in the system" : "Javnim prikazima može pristupiti svako u sistemu", + "Publish" : "Objavi", + "Publish Objects" : "Objavi Objekte", + "Publish all objects in this schema" : "Objavi sve Objekte u ovoj Šemi", + "Published" : "Objavljeno", + "Published:" : "Objavljeno:", + "Publishing..." : "Objavljivanje...", + "Purge" : "Očisti", + "Purge Date" : "Datum čišćenja", + "Query Complexity Distribution" : "Raspodjela složenosti upita", + "Query parameter is required" : "Parametar upita je obavezan", + "RAG" : "RAG", + "RAG Configuration" : "RAG konfiguracija", + "Rate limits cleared successfully" : "Ograničenja brzine su uspješno obrisana", + "Raw Changes Data" : "Sirovi podaci izmjena", + "Re-vectorize on object update" : "Ponovo vektorizuj pri ažuriranju Objekta", + "Read" : "Čitaj", + "Recommendations:" : "Preporuke:", + "Recommended Type:" : "Preporučeni tip:", + "Recommended: 5 sources" : "Preporučeno: 5 izvora", + "Refresh" : "Osvježi", + "Refresh Data" : "Osvježi podatke", + "Refresh Stats" : "Osvježi statistiku", + "Refresh Workflows" : "Osvježi tokove rada", + "Refresh dashboard" : "Osvježi kontrolnu ploču", + "Refresh database info" : "Osvježi informacije o bazi podataka", + "Register" : "Registar", + "Register ID" : "ID Registra", + "Register Statistics" : "Statistika Registra", + "Register Totals" : "Ukupni iznosi Registra", + "Register depublished successfully" : "Registar je uspješno depubliciran", + "Register not found" : "Registar nije pronađen", + "Register objects deletion completed successfully" : "Brisanje Objekata Registra je uspješno završeno", + "Register published successfully" : "Registar je uspješno objavljen", + "Register/Schema Usage" : "Upotreba Registra/Šeme", + "Registers" : "Registri", + "Reindex" : "Ponovo indeksiraj", + "Reindex completed" : "Ponovno indeksiranje je završeno", + "Reindex completed successfully" : "Ponovno indeksiranje je uspješno završeno", + "Reindex failed" : "Ponovno indeksiranje nije uspjelo", + "Relations" : "Relacije", + "Remote Version" : "Udaljena verzija", + "Remove" : "Ukloni", + "Remove filter" : "Ukloni filter", + "Remove from favorites" : "Ukloni iz omiljenih", + "Remove group" : "Ukloni grupu", + "Removed" : "Uklonjeno", + "Removed from favorites" : "Uklonjeno iz omiljenih", + "Rename Conversation" : "Preimenuj razgovor", + "Rename conversation" : "Preimenuj razgovor", + "Replicas" : "Replike", + "Repository" : "Repozitorij", + "Request Body" : "Tijelo zahtjeva", + "Request Data" : "Podaci zahtjeva", + "Request Details" : "Detalji zahtjeva", + "Request body must be JSON with a \"query\" field" : "Tijelo zahtjeva mora biti JSON s poljem \"query\"", + "Request timeout in seconds" : "Istek zahtjeva u sekundama", + "Required field" : "Obavezno polje", + "Required status is inconsistent" : "Obavezni status je nedosljedan", + "Rerun Search" : "Ponovo pokreni pretragu", + "Reset Changes" : "Poništi izmjene", + "Reset Filters" : "Poništi filtere", + "Response Body" : "Tijelo odgovora", + "Response Details" : "Detalji odgovora", + "Response Time:" : "Vrijeme odgovora:", + "Restore" : "Vrati", + "Restore conversation" : "Vrati razgovor", + "Restore or permanently delete items" : "Vrati ili trajno obriši stavke", + "Result Count Range" : "Raspon broja rezultata", + "Results" : "Rezultati", + "Retrieve all objects for this schema" : "Dohvati sve Objekte za ovu Šemu", + "Retry" : "Pokušaj ponovo", + "Retry Failed Extractions" : "Ponovo pokušaj neuspjele ekstrakcije", + "Retry Policy" : "Politika ponovnih pokušaja", + "Retry completed" : "Ponovni pokušaj je završen", + "Retrying..." : "Ponovno pokušavanje...", + "Risk Level" : "Nivo rizika", + "Roles updated successfully" : "Uloge su uspješno ažurirane", + "SOLR Connection Settings" : "Postavke SOLR veze", + "SOLR Version:" : "SOLR verzija:", + "SOLR actions menu" : "Izbornik SOLR radnji", + "SOLR base path (usually /solr)" : "SOLR osnovna putanja (obično /solr)", + "SOLR is disabled" : "SOLR je onemogućen", + "SOLR is not available or not configured" : "SOLR nije dostupan ili nije konfigurisan", + "SOLR logging disabled" : "SOLR bilježenje je onemogućeno", + "SOLR logging enabled" : "SOLR bilježenje je omogućeno", + "SOLR search disabled" : "SOLR pretraga je onemogućena", + "SOLR search enabled" : "SOLR pretraga je omogućena", + "SOLR server hostname or IP address" : "SOLR naziv domaćina servera ili IP adresa", + "SOLR server port number (optional, defaults to 8983)" : "Broj porta SOLR servera (opcionalno, zadano je 8983)", + "SOLR setup completed successfully" : "SOLR postavljanje je uspješno završeno", + "SOLR setup failed" : "SOLR postavljanje nije uspjelo", + "SOLR setup failed - check logs" : "SOLR postavljanje nije uspjelo - provjerite zapise", + "Sample Values:" : "Uzorci vrijednosti:", + "Save" : "Sačuvaj", + "Save Configuration" : "Sačuvaj konfiguraciju", + "Save Connection Settings" : "Sačuvaj postavke veze", + "Save Roles" : "Sačuvaj uloge", + "Save Settings" : "Sačuvaj postavke", + "Save current search as view" : "Sačuvaj trenutnu pretragu kao prikaz", + "Saved Views" : "Sačuvani prikazi", + "Saving..." : "Čuvanje...", + "Schema" : "Šema", + "Schema ID" : "ID Šeme", + "Schema Name" : "Naziv Šeme", + "Schema Statistics" : "Statistika Šeme", + "Schema depublished successfully" : "Šema je uspješno depublicirana", + "Schema objects deletion completed successfully" : "Brisanje Objekata Šeme je uspješno završeno", + "Schema published successfully" : "Šema je uspješno objavljena", + "Schema successfully created" : "Šema je uspješno kreirana", + "Schema successfully updated" : "Šema je uspješno ažurirana", + "Schemas" : "Šeme", + "Scheme" : "Šema", + "Search / Views" : "Pretraga / Prikazi", + "Search Activity" : "Aktivnost pretrage", + "Search Analytics" : "Analitika pretrage", + "Search Entities" : "Pretraži entitete", + "Search Files" : "Pretraži Datoteke", + "Search GitHub" : "Pretraži GitHub", + "Search GitLab" : "Pretraži GitLab", + "Search Method" : "Metoda pretrage", + "Search Term" : "Pojam za pretragu", + "Search Trail Details" : "Detalji traga pretrage", + "Search Trail Management" : "Upravljanje tragom pretrage", + "Search Trail Statistics" : "Statistika traga pretrage", + "Search Trails" : "Tragovi pretrage", + "Search Views" : "Pretraži prikaze", + "Search Webhooks" : "Pretraži webhook-ove", + "Search by file name or path" : "Pretraži po nazivu Datoteke ili putanji", + "Search by name or URL" : "Pretraži po nazivu ili URL-u", + "Search by value" : "Pretraži po vrijednosti", + "Search in Files" : "Pretraži u Datotekama", + "Search in Objects" : "Pretraži u Objektima", + "Search property names..." : "Pretraži nazive Svojstava...", + "Search trail deletion not implemented yet" : "Brisanje traga pretrage još nije implementirano", + "Search views..." : "Pretraži prikaze...", + "Secret" : "Tajna", + "Secret key for HMAC signature generation (optional)" : "Tajni ključ za generisanje HMAC potpisa (opcionalno)", + "Select AI Agent" : "Odaberi AI agenta", + "Select All" : "Odaberi sve", + "Select ConfigSet" : "Odaberi ConfigSet", + "Select File Types to Vectorize:" : "Odaberi tipove Datoteka za vektorizaciju:", + "Select HTTP method" : "Odaberi HTTP metodu", + "Select Register and Schema" : "Odaberi Registar i Šemu", + "Select Views to Vectorize:" : "Odaberi prikaze za vektorizaciju:", + "Select a Nextcloud group to add" : "Odaberi Nextcloud grupu za dodavanje", + "Select a branch" : "Odaberi granu", + "Select a model or type a custom model name" : "Odaberi model ili upiši prilagođeni naziv modela", + "Select a repository" : "Odaberi repozitorij", + "Select a repository you have write access to" : "Odaberi repozitorij za koji imate pristup pisanju", + "Select an AI Agent" : "Odaberi AI agenta", + "Select an AI agent to begin chatting with your data." : "Odaberi AI agenta da biste započeli razgovor sa svojim podacima.", + "Select backend" : "Odaberi pozadinski sistem", + "Select chat model" : "Odaberi model razgovora", + "Select collection for files" : "Odaberi kolekciju za Datoteke", + "Select collection for objects" : "Odaberi kolekciju za Objekte", + "Select default organisation" : "Odaberi zadanu organizaciju", + "Select event to listen to..." : "Odaberi događaj za osluškivanje...", + "Select model" : "Odaberi model", + "Select options..." : "Odaberi opcije...", + "Select period" : "Odaberi period", + "Select property to send as payload" : "Odaberi Svojstvo za slanje kao korisni teret", + "Select provider" : "Odaberi pružatelja", + "Select registers and schemas to save a view" : "Odaberi Registre i Šeme za čuvanje prikaza", + "Select retry policy" : "Odaberi politiku ponovnih pokušaja", + "Select the branch to publish to" : "Odaberi granu za objavljivanje", + "Select the event this webhook should listen to" : "Odaberi događaj koji ovaj webhook treba osluškivati", + "Select views to vectorize:" : "Odaberi prikaze za vektorizaciju:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Odaberi koje su Nextcloud grupe dostupne za ovu organizaciju. Korisnici u ovim grupama imat će pristup resursima organizacije.", + "Select which data views the AI can search" : "Odaberi koje prikaze podataka AI može pretraživati", + "Select which property from the event should be used as the webhook payload data" : "Odaberi koje Svojstvo iz događaja treba koristiti kao podatke korisnog tereta webhook-a", + "Select which tools the AI can use to perform actions" : "Odaberi koje alate AI može koristiti za izvođenje radnji", + "Selected" : "Odabrano", + "Selected Groups" : "Odabrane grupe", + "Selected audit trails deleted successfully" : "Odabrani tragovi revizije su uspješno obrisani", + "Selected search trails deleted successfully" : "Odabrani tragovi pretrage su uspješno obrisani", + "Selected users can access this view" : "Odabrani korisnici mogu pristupiti ovom prikazu", + "Send additional feedback" : "Pošalji dodatne povratne informacije", + "Send as CloudEvent" : "Pošalji kao CloudEvent", + "Sensitive PII" : "Osjetljivi lični podaci (PII)", + "Serial Mode (Safer, slower)" : "Serijski način (sigurnije, sporije)", + "Serial:" : "Serijski:", + "Server Configuration" : "Konfiguracija servera", + "Server Information" : "Informacije o serveru", + "Settings" : "Postavke", + "Shards" : "Fragmenti", + "Share" : "Dijeli", + "Share with Groups" : "Dijeli s grupama", + "Share with Users" : "Dijeli s korisnicima", + "Show Filters" : "Prikaži filtere", + "Show less" : "Prikaži manje", + "Show only entries with changes" : "Prikaži samo unose s izmjenama", + "Simple" : "Jednostavno", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Jednostavni upiti: osnovne tekstualne pretrage s minimalnim parametrima (npr. jedan pojam za pretragu, bez naprednih filtera)", + "Size" : "Veličina", + "Slug" : "Slug", + "Soft Delete Mode" : "Način mekanog brisanja", + "Soft Deleted Items" : "Mekano obrisane stavke", + "SolrCloud mode enabled" : "SolrCloud način je omogućen", + "Source" : "Izvor", + "Sources" : "Izvori", + "Sources:" : "Izvori:", + "Standalone SOLR mode" : "Samostalni SOLR način", + "Start Conversation" : "Započni razgovor", + "Start Vectorization" : "Započni vektorizaciju", + "Start a conversation" : "Započni razgovor", + "Starting names cache warmup..." : "Pokretanje zagrijavanja keša naziva...", + "Starting..." : "Pokretanje...", + "Statistics" : "Statistika", + "Stats calculation not yet implemented" : "Izračunavanje statistike još nije implementirano", + "Status" : "Status", + "Status Code" : "Statusni kôd", + "String Constraints" : "Ograničenja stringa", + "Success" : "Uspjeh", + "Success Rate" : "Stopa uspjeha", + "Success Status" : "Status uspjeha", + "Successful" : "Uspješno", + "Successfully joined organisation" : "Uspješno ste se pridružili organizaciji", + "Sync Table" : "Sinhronizuj tabelu", + "Synchronization Settings" : "Postavke sinhronizacije", + "System Default" : "Sistemski zadano", + "Technical Description" : "Tehnički opis", + "Technical description for developers and administrators" : "Tehnički opis za razvojne programere i administratore", + "Temperature" : "Temperatura", + "Templates" : "Predlošci", + "Test" : "Testiraj", + "Test Chat" : "Testiraj razgovor", + "Test Connection" : "Testiraj vezu", + "Test Embedding" : "Testiraj ugrađivanje", + "Test endpoint executed successfully" : "Testna krajnja tačka je uspješno izvršena", + "Test webhook delivered successfully" : "Testni webhook je uspješno isporučen", + "Test webhook delivery failed" : "Isporuka testnog webhook-a nije uspjela", + "Test webhook sent successfully" : "Testni webhook je uspješno poslan", + "Testing..." : "Testiranje...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Tekstualni dijelovi se generišu tokom ekstrakcije Datoteka i pohranjuju u bazu podataka. Vektorizacija čita ove unaprijed podijeljene Datoteke i pretvara ih u ugrađivanja.", + "Text extracted successfully" : "Tekst je uspješno ekstrahovan", + "Text extraction disabled" : "Ekstrakcija teksta je onemogućena", + "The URL where webhook events will be sent" : "URL na koji će se slati webhook događaji", + "The requested conversation does not exist" : "Traženi razgovor ne postoji", + "The saved view and all its search configuration" : "Sačuvani prikaz i sva njegova konfiguracija pretrage", + "There are no audit trail entries matching your current filters." : "Nema unosa traga revizije koji odgovaraju vašim trenutnim filterima.", + "There are no deleted items matching your current filters." : "Nema obrisanih stavki koje odgovaraju vašim trenutnim filterima.", + "There are no search trail entries matching your current filters." : "Nema unosa traga pretrage koji odgovaraju vašim trenutnim filterima.", + "There are no webhook log entries matching your filters." : "Nema unosa zapisa webhook-a koji odgovaraju vašim filterima.", + "This action cannot be undone." : "Ova radnja se ne može poništiti.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Ova radnja se ne može poništiti. Provjerite da nijedna kolekcija ne koristi ovaj ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Ovaj agent nema konfigurabilne prikaze ili alate, ali i dalje možete prilagoditi RAG postavke ispod.", + "This analysis may take some time" : "Ova analiza može potrajati neko vrijeme", + "This audit trail entry does not contain any change information." : "Ovaj unos traga revizije ne sadrži nikakve informacije o izmjenama.", + "This configuration has no data defined." : "Ova konfiguracija nema definisanih podataka.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ova krajnja tačka je zastarjela. Umjesto toga koristite krajnje tačke zasnovane na dijelovima.", + "This entity has no relations to objects or files" : "Ovaj entitet nema relacija s Objektima ili Datotekama", + "This is a test webhook from OpenRegister" : "Ovo je testni webhook iz OpenRegister-a", + "This schema must use Magic Table configuration to sync" : "Ova Šema mora koristiti konfiguraciju Magične tabele za sinhronizaciju", + "This source has no associated registers." : "Ovaj Izvor nema povezanih Registara.", + "This will copy the _default ConfigSet with the new name" : "Ovo će kopirati _default ConfigSet s novim nazivom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Ovo će trajno obrisati SVA ugrađivanja (vektore) iz baze podataka. Morat ćete ponovo vektorizovati sve Objekte i Datoteke. Ova radnja se ne može poništiti.\\n\\nJeste li sigurni da želite nastaviti?", + "This will permanently delete:" : "Ovo će trajno obrisati:", + "Timeout (seconds)" : "Istek (sekunde)", + "Timestamp" : "Vremenska oznaka", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Savjet: Omogućite samo prikaze kojima je potrebna semantička pretraga kako biste smanjili troškove ugrađivanja. Jednostavne tabele za pretraživanje rijetko trebaju vektorizaciju.", + "Title" : "Naslov", + "To Date" : "Do datuma", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Za prilagođavanje veličine dijela ili strategije, idite na Konfiguracija Datoteka → Ograničenja obrade.", + "To change the embedding provider or model, go to LLM Configuration." : "Za promjenu pružatelja ugrađivanja ili modela, idite na LLM konfiguraciju.", + "To date" : "Do datuma", + "Toggle search sidebar" : "Uključi/isključi bočnu traku pretrage", + "Tools" : "Alati", + "Top Deleters" : "Najčešći brisači", + "Total" : "Ukupno", + "Total Audit Trails" : "Ukupno tragova revizije", + "Total Chunks Available:" : "Ukupno dostupnih dijelova:", + "Total Deleted Items" : "Ukupno obrisanih stavki", + "Total Members" : "Ukupno članova", + "Total Objects" : "Ukupno Objekata", + "Total Objects in Database:" : "Ukupno Objekata u bazi podataka:", + "Total Organisations" : "Ukupno organizacija", + "Total Results" : "Ukupno rezultata", + "Total Searches" : "Ukupno pretraga", + "Total Size" : "Ukupna veličina", + "Total Storage" : "Ukupna pohrana", + "Total:" : "Ukupno:", + "Totals" : "Ukupni iznosi", + "Type" : "Tip", + "Type Issue" : "Problem tipa", + "Type Variations:" : "Varijacije tipa:", + "Type must be \"positive\" or \"negative\"" : "Tip mora biti \"positive\" ili \"negative\"", + "Type to search for groups" : "Upišite za pretragu grupa", + "Type to search for users" : "Upišite za pretragu korisnika", + "Type to search groups..." : "Upišite za pretragu grupa...", + "Type to search users..." : "Upišite za pretragu korisnika...", + "URL" : "URL", + "URL where Ollama is running" : "URL na kojem Ollama radi", + "UUID:" : "UUID:", + "Unchanged" : "Nepromijenjeno", + "Unique Search Terms" : "Jedinstveni pojmovi za pretragu", + "Unique Terms" : "Jedinstveni pojmovi", + "Unknown" : "Nepoznato", + "Unknown Register" : "Nepoznat Registar", + "Unknown Schema" : "Nepoznata Šema", + "Untitled View" : "Prikaz bez naslova", + "Update" : "Ažuriraj", + "Update Operations" : "Operacije ažuriranja", + "Update configuration: ..." : "Ažuriraj konfiguraciju: ...", + "Update register OAS: ..." : "Ažuriraj OAS Registra: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Ažuriraj vektore kada se podaci Objekta promijene (preporučeno za tačnu pretragu)", + "Updated" : "Ažurirano", + "Updated At" : "Ažurirano u", + "Updated:" : "Ažurirano:", + "Uptime:" : "Vrijeme rada:", + "Usage Count" : "Broj upotreba", + "Use SolrCloud with Zookeeper for distributed search" : "Koristi SolrCloud sa Zookeeper-om za distribuiranu pretragu", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Koristite filtere da suzite unose traga revizije po Registru, Šemi, tipu radnje, korisniku, rasponu datuma ili ID-u Objekta.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Koristite filtere da suzite obrisane stavke po Registru, Šemi, datumu brisanja ili korisniku koji ih je obrisao.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Koristite filtere da suzite unose traga pretrage po Registru, Šemi, statusu uspjeha, korisniku, rasponu datuma, pojmovima za pretragu ili metrikama performansi.", + "Used By Collections" : "Korišteno od strane kolekcija", + "User" : "Korisnik", + "User Agent Statistics" : "Statistika korisničkog agenta", + "User Description" : "Korisnički opis", + "User rate limits cleared successfully" : "Korisnička ograničenja brzine su uspješno obrisana", + "User removed successfully" : "Korisnik je uspješno uklonjen", + "User-friendly description shown in forms and help text" : "Korisniku prilagođen opis prikazan u obrascima i tekstu pomoći", + "Username" : "Korisničko ime", + "Username for SOLR authentication (optional)" : "Korisničko ime za SOLR autentifikaciju (opcionalno)", + "Users" : "Korisnici", + "Using Pre-Generated Chunks" : "Korištenje unaprijed generisanih dijelova", + "Validate" : "Validiraj", + "Validate Objects" : "Validiraj Objekte", + "Validation completed successfully" : "Validacija je uspješno završena", + "Validation failed" : "Validacija nije uspjela", + "Value" : "Vrijednost", + "Vector Dimensions" : "Dimenzije vektora", + "Vector Search Backend" : "Pozadinski sistem vektorske pretrage", + "Vector field: _embedding_" : "Polje vektora: _embedding_", + "Vectorization Triggers" : "Okidači vektorizacije", + "Vectorize All Files" : "Vektorizuj sve Datoteke", + "Vectorize All Objects" : "Vektorizuj sve Objekte", + "Vectorize all file types" : "Vektorizuj sve tipove Datoteka", + "Vectorize all views" : "Vektorizuj sve prikaze", + "Vectorize on object creation" : "Vektorizuj pri kreiranju Objekta", + "Vectors will be stored in your existing object and file collections" : "Vektori će biti pohranjeni u vašim postojećim kolekcijama Objekata i Datoteka", + "Version" : "Verzija", + "Very High" : "Vrlo visoko", + "View API Docs" : "Pregledaj API dokumentaciju", + "View Changes" : "Pregledaj izmjene", + "View Details" : "Pregledaj detalje", + "View Error" : "Pregledaj grešku", + "View File" : "Pregledaj Datoteku", + "View Full Details" : "Pregledaj sve detalje", + "View Logs" : "Pregledaj zapise", + "View Name" : "Naziv prikaza", + "View Object" : "Pregledaj Objekat", + "View Parameters" : "Parametri prikaza", + "View Selection" : "Odabir prikaza", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Pregledajte i analizirajte zapise traga pretrage s naprednim mogućnostima filtriranja i analitike", + "View and analyze system audit trails with advanced filtering capabilities" : "Pregledajte i analizirajte sistemske tragove revizije s naprednim mogućnostima filtriranja", + "View deleted successfully" : "Prikaz je uspješno obrisan", + "View entity details and manage relations" : "Pregledajte detalje entiteta i upravljajte relacijama", + "View name" : "Naziv prikaza", + "View name is required" : "Naziv prikaza je obavezan", + "View saved successfully!" : "Prikaz je uspješno sačuvan!", + "View search analytics and manage search logs" : "Pregledajte analitiku pretrage i upravljajte zapisima pretrage", + "View successfully deleted" : "Prikaz je uspješno obrisan", + "View successfully updated" : "Prikaz je uspješno ažuriran", + "View updated successfully!" : "Prikaz je uspješno ažuriran!", + "View webhook delivery logs and filter by webhook" : "Pregledajte zapise isporuke webhook-a i filtrirajte po webhook-u", + "Views" : "Prikazi", + "Visible to users" : "Vidljivo korisnicima", + "Wait for Response" : "Čekaj odgovor", + "Wait for webhook response before continuing (required for request/response flows)" : "Čekaj odgovor webhook-a prije nastavka (obavezno za tokove zahtjev/odgovor)", + "Warmup Names Cache" : "Zagrij keš naziva", + "Warning" : "Upozorenje", + "Warning:" : "Upozorenje:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detalji zapisa webhook-a", + "Webhook Logs" : "Zapisi webhook-a", + "Webhook created successfully" : "Webhook je uspješno kreiran", + "Webhook deleted" : "Webhook je obrisan", + "Webhook retry delivered successfully" : "Ponovni pokušaj webhook-a je uspješno isporučen", + "Webhook retry delivery failed" : "Isporuka ponovnog pokušaja webhook-a nije uspjela", + "Webhook updated" : "Webhook je ažuriran", + "Webhook updated successfully" : "Webhook je uspješno ažuriran", + "Webhooks" : "Webhook-ovi", + "Weekly" : "Sedmično", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Umotaj korisni teret webhook-a u CloudEvents format radi bolje interoperabilnosti", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Da", + "You" : "Vi", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Možete kreirati nove ConfigSet-ove na osnovu predloška _default ili otpremiti prilagođene direktno na vaš SOLR server.", + "You do not have access to this conversation" : "Nemate pristup ovom razgovoru", + "You do not have permission to delete this conversation" : "Nemate dozvolu za brisanje ovog razgovora", + "You do not have permission to modify this conversation" : "Nemate dozvolu za izmjenu ovog razgovora", + "You do not have permission to restore this conversation" : "Nemate dozvolu za vraćanje ovog razgovora", + "You must be logged in to favorite views" : "Morate biti prijavljeni da biste označili prikaze kao omiljene", + "You need an AI agent to start a conversation." : "Potreban vam je AI agent da biste započeli razgovor.", + "Your Fireworks AI API key. Get one at" : "Vaš Fireworks AI API ključ. Nabavite ga na", + "Your OpenAI API key. Get one at" : "Vaš OpenAI API ključ. Nabavite ga na", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Vaše povratne informacije su zabilježene. Opcionalno, ovdje možete pružiti dodatne detalje...", + "Zookeeper Hosts" : "Zookeeper domaćini", + "Zookeeper Port" : "Zookeeper port", + "Zookeeper Settings (SolrCloud)" : "Zookeeper postavke (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper string veze za SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Broj Zookeeper porta (opcionalno, zadano je 2181)", + "chunk" : "dio", + "chunks" : "dijelovi", + "chunks processed" : "dijelova obrađeno", + "configuration(s)" : "konfiguracija/e", + "e.g., lib/Settings/config.json" : "npr. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "npr. lib/Settings/register.json", + "failed" : "nije uspjelo", + "file" : "datoteka", + "files" : "datoteke", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "članovi", + "menu or contact someone with permission to create agents." : "izborniku ili kontaktirajte nekoga s dozvolom za kreiranje agenata.", + "messages" : "poruke", + "n8n URL and API key are required" : "n8n URL i API ključ su obavezni", + "n8n configuration saved successfully" : "n8n konfiguracija je uspješno sačuvana", + "n8n connection not configured" : "n8n veza nije konfigurisana", + "n8n connection successful" : "n8n veza je uspješna", + "n8n connection test successful" : "n8n test veze je uspješan", + "n8n integration disabled" : "n8n integracija je onemogućena", + "n8n integration enabled" : "n8n integracija je omogućena", + "n8n project initialized successfully" : "n8n projekat je uspješno inicijalizovan", + "n8n settings saved successfully" : "n8n postavke su uspješno sačuvane", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekti", + "objects processed" : "objekata obrađeno", + "or pick a range" : "ili odaberite raspon", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "pruža dobru ravnotežu između brzine i tačnosti.", + "register(s)" : "registar/i", + "register(s) selected" : "registar/i odabran/o", + "required" : "obavezno", + "results" : "rezultati", + "schema(s)" : "šema/e", + "schema(s) selected" : "šema/e odabrana/o", + "sk-..." : "sk-...", + "this application" : "ova aplikacija", + "to" : "do", + "vectorized" : "vektorizovano", + "views selected" : "prikaza odabrano", + "ℹ️ Current Configuration" : "ℹ️ Trenutna konfiguracija", + "⚡ Batch Processing" : "⚡ Serijska obrada", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Sveobuhvatan kontekst ✓ Manja vjerovatnoća propuštanja detalja ✗ Sporiji odgovori ✗ Može uključiti manje relevantne informacije", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Brži odgovori ✓ Fokusiraniji odgovori ✗ Može propustiti relevantne informacije", + "✨ AI Features" : "✨ AI funkcije", + "All search trails cleared successfully" : "Svi tragovi pretrage su uspješno obrisani", + "Multiple search trail deletion not implemented yet" : "Brisanje više tragova pretrage još nije implementirano", + "No expired search trails found to clear" : "Nije pronađen nijedan istekli trag pretrage za brisanje", + "Missing message" : "Nedostaje poruka", + "message content is required" : "sadržaj poruke je obavezan", + "Missing conversation or agentUuid" : "Nedostaje razgovor ili agentUuid", + "Access denied" : "Pristup odbijen", + "Conversation not found" : "Razgovor nije pronađen", + "AI service not configured" : "AI usluga nije konfigurisana", + "Failed to process message" : "Nije uspjela obrada poruke", + "Missing conversationId" : "Nedostaje conversationId", + "conversationId is required" : "conversationId je obavezan", + "Failed to fetch conversation history" : "Nije uspjelo dohvatanje historije razgovora", + "Failed to clear conversation" : "Nije uspjelo brisanje razgovora", + "Invalid feedback type" : "Nevažeći tip povratnih informacija", + "type must be \"positive\" or \"negative\"" : "tip mora biti \"positive\" ili \"negative\"", + "Message not found" : "Poruka nije pronađena", + "Failed to save feedback" : "Nije uspjelo čuvanje povratnih informacija", + "Failed to get chat statistics" : "Nije uspjelo dobijanje statistike razgovora", + "Not Found" : "Nije pronađeno", + "Engine not found" : "Mašina nije pronađena", + "Consumer not found" : "Potrošač nije pronađen", + "Backend parameter is required" : "Parametar pozadinskog sistema je obavezan", + "Failed to get database information: %s" : "Nije uspjelo dobijanje informacija o bazi podataka: %s", + "SOLR setup error: %s" : "Greška postavljanja SOLR-a: %s", + "Reindex failed: %s" : "Ponovno indeksiranje nije uspjelo: %s", + "Anonymized" : "Anonimizirano", + "Entities detected" : "Otkriveni entiteti", + "Extract Now" : "Ekstrahuj sada", + "Extracted at" : "Ekstrahovano u", + "Extraction" : "Ekstrakcija", + "Failed to load extraction data" : "Nije uspjelo učitavanje podataka ekstrakcije", + "Failed to load register data" : "Nije uspjelo učitavanje podataka Registra", + "Failed to retrieve extraction status." : "Nije uspjelo dohvatanje statusa ekstrakcije.", + "Failed to retrieve objects for file." : "Nije uspjelo dohvatanje Objekata za Datoteku.", + "No extraction data available for this file" : "Nema dostupnih podataka ekstrakcije za ovu Datoteku", + "No register objects reference this file" : "Nijedan Objekat Registra ne referencira ovu Datoteku", + "Not extracted" : "Nije ekstrahovano", + "Register Objects" : "Objekti Registra", + "Risk level" : "Nivo rizika", + "Text chunks" : "Tekstualni dijelovi", + "Unknown error" : "Nepoznata greška", + "Very high" : "Vrlo visoko", + "{title} in {register} / {schema}" : "{title} u {register} / {schema}", + "Push Notifications" : "Push obavještenja", + "Real-time push notification status via notify_push" : "Status push obavještenja u stvarnom vremenu putem notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push u stvarnom vremenu nije dostupan — aplikacija notify_push nije instalirana", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Instalirajte aplikaciju notify_push iz Nextcloud trgovine aplikacija da omogućite ažuriranja u stvarnom vremenu.", + "Open Nextcloud App Store" : "Otvori Nextcloud trgovinu aplikacija", + "notify_push is installed but not yet active" : "notify_push je instaliran ali još nije aktivan", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push je instaliran ali OpenRegister još nije potvrdio uspješan push. Pokrenite čuvanje Objekta da aktivirate ili provjerite svoju notify_push konfiguraciju.", + "notify_push configuration guide" : "vodič za konfiguraciju notify_push", + "Realtime push active" : "Push u stvarnom vremenu aktivan", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push obavještenja u stvarnom vremenu su aktivna. Povezani klijenti primaju trenutna ažuriranja kada se Objekti kreiraju, ažuriraju ili brišu.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +); diff --git a/l10n/bs.json b/l10n/bs.json new file mode 100644 index 0000000000..bcdcfc7658 --- /dev/null +++ b/l10n/bs.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Potreban Dolphin)", + "(no subject)": "(bez predmeta)", + "(no title)": "(bez naslova)", + "(unnamed)": "(bez naziva)", + "(untitled card)": "(kartica bez naslova)", + "({count} selected)": "({count} odabrano)", + ", {failed} failed": ", {failed} neuspješno", + "0 = unlimited": "0 = neograničeno", + "A deactivation request is pending.": "Zahtjev za deaktivaciju je na čekanju.", + "AI Assistant": "AI asistent", + "AI Chat": "AI razgovor", + "AI service not configured": "AI usluga nije konfigurirana", + "API Key": "API ključ", + "API Request Quota (requests/day)": "Kvota API zahtjeva (zahtjeva/dan)", + "API Token Configuration": "Konfiguracija API tokena", + "API Tokens": "API tokeni", + "API tokens saved successfully": "API tokeni uspješno spremljeni", + "AVG / Verwerkingsregister": "AVG / Registar obrade", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG čl. 30 §4: ovo izvješće povezuje svaku aktivnost obrade s ukupnim brojem zapisa revizijskog traga koji joj se pripisuju. Revizori i Autoriteit Persoonsgegevens to koriste za nadzorni pregled.", + "About ConfigSets": "O ConfigSet stavkama", + "Access denied": "Pristup odbijen", + "Access organisation dashboard and analytics": "Pristup nadzornoj ploči i analitici organizacije", + "Account": "Račun", + "Action": "Radnja", + "Action Distribution": "Raspodjela radnji", + "Action:": "Radnja:", + "Actions": "Radnje", + "Active": "Aktivno", + "Active Collections": "Aktivne zbirke", + "Active Filters": "Aktivni filtri", + "Active Organisation:": "Aktivna organizacija:", + "Active Organisations": "Aktivne organizacije", + "Active filters:": "Aktivni filtri:", + "Active organisation changed successfully": "Aktivna organizacija uspješno promijenjena", + "Active organisation set successfully": "Aktivna organizacija uspješno postavljena", + "Activities": "Aktivnosti", + "Activity": "Aktivnost", + "Activity Period": "Razdoblje aktivnosti", + "Add": "Dodaj", + "Add Attachment": "Dodaj privitak", + "Add Endpoint": "Dodaj krajnju točku", + "Add Groups": "Dodaj grupe", + "Add Property to '{title}'": "Dodaj svojstvo u '{title}'", + "Add Register": "Dodaj registar", + "Add Schema": "Dodaj shemu", + "Add Source": "Dodaj izvor", + "Add User": "Dodaj korisnika", + "Add User to Organisation": "Dodaj korisnika u organizaciju", + "Add a contact from any of your address books to associate it with this object.": "Dodajte kontakt iz bilo kojeg svojeg adresara kako biste ga povezali s ovim objektom.", + "Add a file or files": "Dodajte datoteku ili datoteke", + "Add card": "Dodaj karticu", + "Add contact": "Dodaj kontakt", + "Add endpoint": "Dodaj krajnju točku", + "Add oneOf entry": "Dodaj oneOf unos", + "Add range": "Dodaj raspon", + "Add schema titles, descriptions, and register information to provide richer context for search": "Dodajte naslove shema, opise i informacije o registru kako biste pružili bogatiji kontekst za pretraživanje", + "Add to favorites": "Dodaj u favorite", + "Added": "Dodano", + "Added to favorites": "Dodano u favorite", + "Additional Information": "Dodatne informacije", + "Additional feedback saved. Thank you!": "Dodatne povratne informacije spremljene. Hvala vam!", + "Addresses": "Adrese", + "Admin": "Administrator", + "Advanced": "Napredno", + "Advanced Filters": "Napredni filtri", + "Advanced Options": "Napredne mogućnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Napredni filtri nisu dostupni pri korištenju izvora baze podataka. Prebacite na Auto ili SOLR indeks za mogućnosti filtriranja.", + "After": "Nakon", + "Agent Type": "Vrsta agenta", + "Agent deleted successfully": "Agent uspješno izbrisan", + "Agent successfully created": "Agent uspješno stvoren", + "Agent successfully updated": "Agent uspješno ažuriran", + "Agents": "Agenti", + "Aggregated across schemas": "Agregirano kroz sheme", + "All": "Sve", + "All Categories": "Sve kategorije", + "All Collections": "Sve zbirke", + "All Confidence Levels": "Sve razine pouzdanosti", + "All ConfigSets": "Svi ConfigSet-ovi", + "All Files": "Sve datoteke", + "All Levels": "Sve razine", + "All Types": "Sve vrste", + "All Webhooks": "Svi webhookovi", + "All actions": "Sve radnje", + "All audit trails cleared successfully": "Svi revizijski tragovi uspješno obrisani", + "All clear": "Sve čisto", + "All file types": "Sve vrste datoteka", + "All registers": "Svi registri", + "All schemas": "Sve sheme", + "All search trails cleared successfully": "Svi tragovi pretraživanja uspješno obrisani", + "All searches": "Sva pretraživanja", + "All users": "Svi korisnici", + "All-day events": "Cjelodnevni događaji", + "Allowed MIME Types": "Dopuštene MIME vrste", + "Already linked": "Već povezano", + "An error occurred while clearing audit trails": "Došlo je do pogreške prilikom brisanja revizijskih tragova", + "An error occurred while deleting the audit trail": "Došlo je do pogreške prilikom brisanja revizijskog traga", + "An error occurred while deleting the object": "Došlo je do pogreške prilikom brisanja objekta", + "An error occurred while deleting the view": "Došlo je do pogreške prilikom brisanja prikaza", + "An error occurred while permanently deleting the objects": "Došlo je do pogreške prilikom trajnog brisanja objekata", + "An error occurred while restoring the objects": "Došlo je do pogreške prilikom vraćanja objekata", + "An error occurred while saving the organisation": "Došlo je do pogreške prilikom spremanja organizacije", + "Analysis completed:": "Analiza dovršena:", + "Analysis steps:": "Koraci analize:", + "Analytics": "Analitika", + "Analyze Objects": "Analiziraj objekte", + "Analyze Properties": "Analiziraj svojstva", + "Analyze existing properties for improvement opportunities": "Analizirajte postojeća svojstva za mogućnosti poboljšanja", + "Analyzing...": "Analiziranje...", + "Anonymized": "Anonimizirano", + "Anonymous Group": "Anonimna grupa", + "Any favorites and sharing settings for this view": "Sve postavke favorita i dijeljenja za ovaj prikaz", + "Any user": "Bilo koji korisnik", + "App ID": "ID aplikacije", + "App store cache invalidated successfully": "Predmemorija trgovine aplikacija uspješno poništena", + "App store cache invalidated: {invalidated}": "Predmemorija trgovine aplikacija poništena: {invalidated}", + "Application deleted successfully": "Aplikacija uspješno izbrisana", + "Application identifier for this configuration (optional)": "Identifikator aplikacije za ovu konfiguraciju (neobavezno)", + "Application settings": "Postavke aplikacije", + "Application successfully created": "Aplikacija uspješno stvorena", + "Application successfully updated": "Aplikacija uspješno ažurirana", + "Applications": "Aplikacije", + "Apply": "Primijeni", + "Apply Changes": "Primijeni promjene", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Primijenite ulogu na sve sheme u ovom registru koje nemaju eksplicitna nadjačavanja autorizacije.", + "Approval Progress": "Napredak odobravanja", + "Approve": "Odobri", + "Approved": "Odobreno", + "Archive": "Arhiviraj", + "Archive conversation": "Arhiviraj razgovor", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arhivirati ovu aktivnost obrade? Reci revizijskog traga nastavit će je referencirati.", + "Archived conversations are hidden from your active list": "Arhivirani razgovori skriveni su s vašeg aktivnog popisa", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Jeste li sigurni da želite IZBRISATI zbirku \"{name}\"?\n\nOvo će:\n• Trajno izbrisati zbirku i sve njezine podatke\n• Ukloniti sve indeksirane dokumente\n• Ova se radnja ne može poništiti", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Jeste li sigurni da želite očistiti stare tragove pretraživanja? Ovo će izbrisati unose starije od 30 dana.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Jeste li sigurni da želite obrisati sve podatke iz zbirke \"{name}\"?\n\nOvo će:\n• Izbrisati sve indeksirane dokumente\n• Zadržati strukturu zbirke netaknutom\n• Ova se radnja ne može poništiti", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Jeste li sigurni da želite izbrisati aplikaciju \"{name}\"? Ova se radnja ne može poništiti.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Jeste li sigurni da želite izbrisati odabrane revizijske tragove? Ova se radnja ne može poništiti.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Jeste li sigurni da želite izbrisati odabrane tragove pretraživanja? Ova se radnja ne može poništiti.", + "Are you sure you want to delete this ConfigSet?": "Jeste li sigurni da želite izbrisati ovaj ConfigSet?", + "Are you sure you want to delete this configset?": "Jeste li sigurni da želite izbrisati ovaj configset?", + "Are you sure you want to leave '{name}'?": "Jeste li sigurni da želite napustiti '{name}'?", + "Are you sure you want to permanently delete": "Jeste li sigurni da želite trajno izbrisati", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Jeste li sigurni da želite ponovno indeksirati zbirku \"{name}\"?\n\nOvo će:\n• Ponovno izgraditi indeks sa svim objektima\n• Potrajati nekoliko minuta do dovršetka\n• Možda utjecati na performanse pretraživanja tijekom ponovnog indeksiranja", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Jeste li sigurni da želite ukloniti shemu \"{schema}\" iz registra \"{register}\"? Ova se radnja ne može poništiti.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Jeste li sigurni da želite meko izbrisati {count} aktivnih objekata za shemu \"{schema}\"?\n\nBit će označeni kao izbrisani, ali se mogu trajno ukloniti kasnije.", + "Array Object Configuration:": "Konfiguracija objekta polja:", + "Articles": "Članci", + "Ask a question...": "Postavite pitanje...", + "Ask questions about your data using natural language": "Postavljajte pitanja o svojim podacima koristeći prirodni jezik", + "Assigned collaborative tags": "Dodijeljene suradničke oznake", + "Assignment notifications": "Obavijesti o dodjeli", + "Attachment added to {name}": "Privitak dodan u {name}", + "Attempt": "Pokušaj", + "Audit Trail Changes": "Promjene revizijskog traga", + "Audit Trail Details": "Pojedinosti revizijskog traga", + "Audit Trail Management": "Upravljanje revizijskim tragom", + "Audit Trail Statistics": "Statistika revizijskog traga", + "Audit Trails": "Revizijski tragovi", + "Audit trail data copied to clipboard": "Podaci revizijskog traga kopirani u međuspremnik", + "Audit trail deleted successfully": "Revizijski trag uspješno izbrisan", + "Audit trail successfully deleted": "Revizijski trag uspješno izbrisan", + "Authentication": "Autentifikacija", + "Auto (e.g. Y for year, F Y for month)": "Auto (npr. Y za godinu, F Y za mjesec)", + "Auto share": "Automatsko dijeljenje", + "Auto-commit disabled": "Automatska potvrda onemogućena", + "Auto-commit enabled": "Automatska potvrda omogućena", + "Auto-create Default Organisation": "Automatski stvori zadanu organizaciju", + "Auto-retry failed vectorizations": "Automatski ponovno pokušaj neuspjele vektorizacije", + "Automatically commit changes to SOLR index": "Automatski potvrdi promjene u SOLR indeks", + "Automatically connect to the federation network.": "Automatski se poveži na federacijsku mrežu.", + "Automatically create a default organisation if none exists when the app is initialized": "Automatski stvori zadanu organizaciju ako nijedna ne postoji kada se aplikacija inicijalizira", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automatski generiraj vektorska ugrađivanja iz tekstualnih dijelova kada se datoteke prenesu i obrade", + "Automatically generate vector embeddings when objects are created or updated": "Automatski generiraj vektorska ugrađivanja kada se objekti stvore ili ažuriraju", + "Automatically retry failed vectorization attempts (max 3 retries)": "Automatski ponovno pokušaj neuspjele pokušaje vektorizacije (najviše 3 ponovna pokušaja)", + "Automatically update catalogues.": "Automatski ažuriraj kataloge.", + "Available Filters": "Dostupni filtri", + "Available Workflows": "Dostupni tijekovi rada", + "Available placeholders:": "Dostupni rezervirani znakovi:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Vaš pružatelj autentifikacije ne podržava promjene avatara.", + "Avatar removed": "Avatar uklonjen", + "Avatar updated successfully": "Avatar uspješno ažuriran", + "Avg Execution Time": "Prosj. vrijeme izvršavanja", + "Avg Members/Org": "Prosj. članova/org.", + "Avg Object Views/Session": "Prosj. pregleda objekata/sesiji", + "Avg Response Time": "Prosj. vrijeme odgovora", + "Avg Results/Search": "Prosj. rezultata/pretraživanju", + "Avg Searches/Session": "Prosj. pretraživanja/sesiji", + "Back": "Natrag", + "Back to Entities": "Natrag na entitete", + "Back to Registers": "Natrag na registre", + "Back to Webhooks": "Natrag na webhookove", + "Back to applications": "Natrag na aplikacije", + "Back to entities": "Natrag na entitete", + "Backend parameter is required": "Parametar pozadinskog sustava je obavezan", + "Backend updated successfully. Please reload the application.": "Pozadinski sustav uspješno ažuriran. Molimo ponovno učitajte aplikaciju.", + "Bandwidth Quota (MB/month)": "Kvota propusnosti (MB/mjesec)", + "Base URL (Optional)": "Osnovni URL (neobavezno)", + "Basic Information": "Osnovne informacije", + "Batch Size": "Veličina serije", + "Batch extraction completed": "Serijska ekstrakcija dovršena", + "Before": "Prije", + "Before object vectorization can work:": "Prije nego što vektorizacija objekata može raditi:", + "Behavior": "Ponašanje", + "Behavior Issue": "Problem s ponašanjem", + "Beschrijving": "Opis", + "Bewaartermijn": "Rok čuvanja", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Rok čuvanja (ISO-8601 trajanje, npr. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob pohrana je povučena. Svi objekti sada koriste magične tablice.", + "Bookmarks": "Knjižne oznake", + "Branch": "Grana", + "Bulk Role Assignment: {title}": "Skupna dodjela uloga: {title}", + "Bulk delete operation completed successfully": "Operacija skupnog brisanja uspješno dovršena", + "Bulk save operation completed successfully": "Operacija skupnog spremanja uspješno dovršena", + "Business Data": "Poslovni podaci", + "CSV": "CSV", + "Cache cleared successfully": "Predmemorija uspješno obrisana", + "Cache grew from {old} to {new} entries.": "Predmemorija je narasla s {old} na {new} unosa.", + "Cache shrunk from {old} to {new} entries.": "Predmemorija se smanjila s {old} na {new} unosa.", + "Cache stayed the same at {size} entries.": "Predmemorija je ostala ista na {size} unosa.", + "Calculate Sizes": "Izračunaj veličine", + "Calendar": "Kalendar", + "Calendar Provider Configuration": "Konfiguracija pružatelja kalendara", + "Calendar integration is not available": "Integracija kalendara nije dostupna", + "Calendar name": "Naziv kalendara", + "Cancel": "Odustani", + "Cancel deactivation request": "Otkaži zahtjev za deaktivaciju", + "Cannot delete: objects are still attached": "Nije moguće izbrisati: objekti su još uvijek priloženi", + "Cannot edit: This register is managed by external configuration {title}": "Nije moguće uređivati: ovim registrom upravlja vanjska konfiguracija {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nije moguće ukloniti shemu s postojećim objektima ({count} objekata)", + "Cannot remove schema {schema} because it contains {count} objects": "Nije moguće ukloniti shemu {schema} jer sadrži {count} objekata", + "Cards": "Kartice", + "Cascade delete": "Kaskadno brisanje", + "Categories of data subjects (one per line)": "Kategorije ispitanika (jedna po retku)", + "Categories of personal data (one per line)": "Kategorije osobnih podataka (jedna po retku)", + "Categorieën betrokkenen (één per regel)": "Kategorije ispitanika (jedna po retku)", + "Categorieën persoonsgegevens (één per regel)": "Kategorije osobnih podataka (jedna po retku)", + "Category": "Kategorija", + "Change Type": "Vrsta promjene", + "Change password": "Promijeni lozinku", + "Changes": "Promjene", + "Changes copied to clipboard": "Promjene kopirane u međuspremnik", + "Changes for Audit Trail #{id}": "Promjene za revizijski trag #{id}", + "Chat Model": "Model razgovora", + "Chat Provider": "Pružatelj razgovora", + "Chat Provider (RAG)": "Pružatelj razgovora (RAG)", + "Chat Settings": "Postavke razgovora", + "Chat provider connection successful!": "Veza s pružateljem razgovora uspješna!", + "Chat settings": "Postavke razgovora", + "Chat test failed: {error}": "Test razgovora neuspješan: {error}", + "Choose a register": "Odaberite registar", + "Choose a schema": "Odaberite shemu", + "Choose how vector similarity calculations are performed for semantic search": "Odaberite kako se izračuni vektorske sličnosti izvode za semantičko pretraživanje", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Odaberite koje vrste datoteka uključiti u proces vektorizacije. Obradit će se samo datoteke s izvučenim tekstom i dijelovima.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Odaberite koje prikaze uključiti u proces vektorizacije. Ostavite prazno za obradu svih prikaza na temelju vaše konfiguracije.", + "Chunk": "Dio", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Brisanje dijelova još nije implementirano. Koristite krajnje točke temeljene na dijelovima.", + "Chunks": "Dijelovi", + "Chunks to Vectorize:": "Dijelovi za vektorizaciju:", + "Cleanup Old Trails": "Očisti stare tragove", + "Cleanup completed": "Čišćenje dovršeno", + "Cleanup completed successfully. Deleted {count} entries.": "Čišćenje uspješno dovršeno. Izbrisano {count} unosa.", + "Cleanup failed: {error}": "Čišćenje neuspješno: {error}", + "Clear All Embeddings": "Obriši sva ugrađivanja", + "Clear Entries": "Obriši unose", + "Clear Filtered Audit Trails": "Obriši filtrirane revizijske tragove", + "Clear Filters": "Obriši filtre", + "Clear Index": "Obriši indeks", + "Clear Selection": "Obriši odabir", + "Clear all": "Obriši sve", + "Clear all filters": "Obriši sve filtre", + "Clear date range": "Obriši raspon datuma", + "Clear filters": "Obriši filtre", + "Clearing...": "Brisanje...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Kliknite Pokreni provjeru usklađenosti kako biste pronašli sheme u kojima je otkriven PII, ali ne postoji bilješka o aktivnosti obrade.", + "Click to subscribe to notifications": "Kliknite za pretplatu na obavijesti", + "Click to unsubscribe from notifications": "Kliknite za odjavu od obavijesti", + "Close": "Zatvori", + "Cluster name": "Naziv klastera", + "Code": "Kod", + "Code (short readable key, e.g. v-2026-001)": "Kod (kratki čitljivi ključ, npr. v-2026-001)", + "Collapse details": "Sažmi pojedinosti", + "Collection Management": "Upravljanje zbirkama", + "Collection Name": "Naziv zbirke", + "Collection assignments updated successfully": "Dodjele zbirki uspješno ažurirane", + "Collection cleared successfully": "Zbirka uspješno obrisana", + "Collection copied successfully": "Zbirka uspješno kopirana", + "Collection created successfully": "Zbirka uspješno stvorena", + "Collection deleted successfully": "Zbirka uspješno izbrisana", + "Collection used to store and index file metadata and content": "Zbirka koja se koristi za pohranu i indeksiranje metapodataka i sadržaja datoteka", + "Collection used to store and index object data": "Zbirka koja se koristi za pohranu i indeksiranje podataka objekata", + "Collections:": "Zbirke:", + "Color": "Boja", + "Comment...": "Komentar...", + "Commit Message": "Poruka potvrde", + "Commit Within (ms)": "Potvrdi unutar (ms)", + "Commit message": "Poruka potvrde", + "Compare current schema with real object data": "Usporedi trenutnu shemu sa stvarnim podacima objekata", + "Completed": "Dovršeno", + "Complex": "Složeno", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Složeni upiti: napredna pretraživanja s više filtara, operatora i složenih kombinacija parametara", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Složeni upiti: napredna pretraživanja s više filtara, operatora i složenih kombinacija parametara", + "Compliance": "Usklađenost", + "Confidence Level": "Razina pouzdanosti", + "Confidence Score": "Rezultat pouzdanosti", + "Confidence: {confidence}%": "Pouzdanost: {confidence}%", + "Config must be provided as an object": "Konfiguracija mora biti navedena kao objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Upravljanje ConfigSet stavkama", + "ConfigSet Name": "Naziv ConfigSet stavke", + "ConfigSet created successfully": "ConfigSet uspješno stvoren", + "ConfigSet deleted successfully": "ConfigSet uspješno izbrisan", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSet stavke definiraju shemu i konfiguraciju za vaše SOLR zbirke. Sadrže definicije polja, analizatore i druge postavke pretraživanja.", + "Configuration": "Konfiguracija", + "Configuration Data": "Podaci konfiguracije", + "Configuration is up to date": "Konfiguracija je ažurna", + "Configuration saved": "Konfiguracija spremljena", + "Configuration saved successfully.": "Konfiguracija uspješno spremljena.", + "Configuration type (default, application, etc.)": "Vrsta konfiguracije (zadana, aplikacija itd.)", + "Configurations": "Konfiguracije", + "Configure API tokens for external service integrations": "Konfigurirajte API tokene za integracije vanjskih usluga", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurirajte Apache SOLR tražilicu za napredne mogućnosti pretraživanja", + "Configure Facets": "Konfiguriraj fasete", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurirajte pružatelje velikih jezičnih modela (LLM) za značajke pokretane AI-jem uključujući semantičko pretraživanje, ugrađivanja i razgovor.", + "Configure SOLR Facets": "Konfiguriraj SOLR fasete", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurirajte osnovne postavke veze za vaš SOLR poslužitelj uključujući autentifikaciju i mrežne mogućnosti. Koristite zasebne dijaloge za upravljanje ConfigSet stavkama i zbirkama za upravljanje jezgrama i zbirkama.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurirajte osnovne postavke veze za vaš SOLR poslužitelj uključujući autentifikaciju i mrežne mogućnosti. Koristite zasebne dijaloge za upravljanje configset stavkama i zbirkama za upravljanje jezgrama i zbirkama.", + "Configure default organisation and organisation-related settings": "Konfigurirajte zadanu organizaciju i postavke vezane uz organizaciju", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurirajte kako se objekti baze podataka pretvaraju u vektorska ugrađivanja za semantičko pretraživanje. Objekti se izravno vektoriziraju bez potrebe za ekstrakcijom teksta.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurirajte kako se objekti pretvaraju u tekst prije vektorizacije. Ove postavke utječu na kvalitetu i kontekst pretraživanja.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurirajte pružatelje velikih jezičnih modela (LLM) za značajke pokretane AI-jem uključujući semantičko pretraživanje, ugrađivanja i razgovor.", + "Configure multi-organization support and tenant isolation": "Konfigurirajte podršku za više organizacija i izolaciju zakupaca", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurirajte parametre za vektorizaciju datoteka. Ovaj će proces generirati vektorska ugrađivanja za sve izvučene dijelove datoteka.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurirajte parametre za vektorizaciju objekata. Ovaj će proces generirati vektorska ugrađivanja za sve objekte koji odgovaraju filtrima vašeg prikaza.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurirajte ovu shemu da prikazuje objekte kao događaje u aplikaciji Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurirajte koje vrste podataka pretraživati i koliko izvora dohvatiti", + "Configuring field mappings...": "Konfiguriranje mapiranja polja...", + "Confirm Account Deactivation": "Potvrdi deaktivaciju računa", + "Confirm Object Deletion": "Potvrdi brisanje objekta", + "Confirm Object Restoration": "Potvrdi vraćanje objekta", + "Confirm Permanent Object Deletion": "Potvrdi trajno brisanje objekta", + "Confirm deactivation": "Potvrdi deaktivaciju", + "Connecting to SOLR cluster...": "Povezivanje s SOLR klasterom...", + "Connection Failed": "Veza neuspješna", + "Connection Settings": "Postavke veze", + "Connection Successful!": "Veza uspješna!", + "Connection failed": "Veza neuspješna", + "Connection protocol": "Protokol veze", + "Connection successful": "Veza uspješna", + "Connection timeout in seconds": "Istek veze u sekundama", + "Constant delay between retries (5 minutes)": "Stalni odgoda između ponovnih pokušaja (5 minuta)", + "Constraint Issue": "Problem s ograničenjem", + "Consumer not found": "Potrošač nije pronađen", + "Contacts": "Kontakti", + "Contacts integration is not available": "Integracija kontakata nije dostupna", + "Content Filters": "Filtri sadržaja", + "Continue to Properties": "Nastavi na svojstva", + "Contracts": "Ugovori", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontrolirajte koji se prikazi objekata trebaju vektorizirati radi smanjenja troškova API-ja. Vektorizirajte samo prikaze koji imaju koristi od semantičkog pretraživanja.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontrolirajte koje prikaze i alate AI može koristiti u ovom razgovoru. Prema zadanim postavkama, sve su mogućnosti agenta omogućene.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontrolira nasumičnost (0 = determinističko, 2 = vrlo kreativno)", + "Conversation ID is required": "ID razgovora je obavezan", + "Conversation archived": "Razgovor arhiviran", + "Conversation archived successfully": "Razgovor uspješno arhiviran", + "Conversation cleared successfully": "Razgovor uspješno obrisan", + "Conversation deleted": "Razgovor izbrisan", + "Conversation not found": "Razgovor nije pronađen", + "Conversation permanently deleted": "Razgovor trajno izbrisan", + "Conversation renamed": "Razgovor preimenovan", + "Conversation restored": "Razgovor vraćen", + "Conversation started with {agent}": "Razgovor započet s {agent}", + "Conversation title": "Naslov razgovora", + "Conversations": "Razgovori", + "Converting {schema} to blob storage...": "Pretvaranje {schema} u blob pohranu...", + "Converting {schema} to magic table...": "Pretvaranje {schema} u magičnu tablicu...", + "Copied!": "Kopirano!", + "Copy": "Kopiraj", + "Copy Changes": "Kopiraj promjene", + "Copy Collection": "Kopiraj zbirku", + "Copy Data": "Kopiraj podatke", + "Copy Full Data": "Kopiraj sve podatke", + "Copy of {name}": "Kopija od {name}", + "Copy to clipboard": "Kopiraj u međuspremnik", + "Copying...": "Kopiranje...", + "Costs": "Troškovi", + "Count": "Broj", + "Counting objects...": "Brojanje objekata...", + "Create": "Stvori", + "Create API Token": "Stvori API token", + "Create Agent": "Stvori agenta", + "Create Application": "Stvori aplikaciju", + "Create Collection": "Stvori zbirku", + "Create ConfigSet": "Stvori ConfigSet", + "Create New Collection": "Stvori novu zbirku", + "Create New ConfigSet": "Stvori novi ConfigSet", + "Create Operations": "Stvori operacije", + "Create Organisation": "Stvori organizaciju", + "Create Webhook": "Stvori webhook", + "Create a copy of collection:": "Stvori kopiju zbirke:", + "Create a new ConfigSet based on the _default template": "Stvori novi ConfigSet na temelju predloška _default", + "Create a new SOLR collection from an existing ConfigSet": "Stvori novu SOLR zbirku iz postojećeg ConfigSet-a", + "Create a new SOLR collection from an existing configset": "Stvori novu SOLR zbirku iz postojećeg configset-a", + "Create a new configset based on the _default template": "Stvori novi configset na temelju predloška _default", + "Create a new event or link an existing one from any of your calendars.": "Stvorite novi događaj ili povežite postojeći iz bilo kojeg svojeg kalendara.", + "Create another": "Stvori još jedan", + "Create copies of": "Stvori kopije od", + "Create event": "Stvori događaj", + "Create new token": "Stvori novi token", + "Create or link a Deck card to track work on this object.": "Stvorite ili povežite Deck karticu za praćenje rada na ovom objektu.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Stvorite prvu aktivnost obrade kako biste počeli označavati retke revizijskog traga njihovom AVG čl. 30 atribucijom.", + "Create your first AI agent to get started.": "Stvorite svojeg prvog AI agenta za početak.", + "Created": "Stvoreno", + "Created:": "Stvoreno:", + "Creating search collections...": "Stvaranje zbirki pretraživanja...", + "Creating...": "Stvaranje...", + "Current": "Trenutno", + "Current Filters": "Trenutni filtri", + "Current Type:": "Trenutna vrsta:", + "Current password": "Trenutna lozinka", + "Custom API endpoint if using a different region": "Prilagođena API krajnja točka ako koristite drugu regiju", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Prilagođena HTTP zaglavlja (jedno po retku, format: Header-Name: vrijednost)", + "Custom HTTP headers (one per line, format: header-name: value)": "Prilagođena HTTP zaglavlja (jedno po retku, format: header-name: vrijednost)", + "Custom Ranges:": "Prilagođeni rasponi:", + "Custom display title for this facet": "Prilagođeni prikazni naslov za ovu fasetu", + "Custom pattern:": "Prilagođeni uzorak:", + "DELETE COLLECTION": "IZBRIŠI ZBIRKU", + "DSAR": "DSAR", + "Daily": "Dnevno", + "Dashboard": "Nadzorna ploča", + "Dashboard not found": "Nadzorna ploča nije pronađena", + "Data": "Podaci", + "Data Source": "Izvor podataka", + "Data Sources": "Izvori podataka", + "Data Views": "Prikazi podataka", + "Data sources": "Izvori podataka", + "Data type does not match observed values": "Vrsta podataka ne odgovara promatranim vrijednostima", + "Data-subject access request": "Zahtjev ispitanika za pristup", + "Database URL": "URL baze podataka", + "Database information refreshed": "Informacije o bazi podataka osvježene", + "Date Faceting:": "Fasetiranje datuma:", + "Date Range": "Raspon datuma", + "Dates": "Datumi", + "Deactivation request cancelled": "Zahtjev za deaktivaciju otkazan", + "Deactivation request submitted": "Zahtjev za deaktivaciju podnesen", + "Deck": "Deck", + "Deck integration is not available": "Deck integracija nije dostupna", + "Default": "Zadano", + "Default Object Tenant": "Zadani zakupac objekta", + "Default Organisation": "Zadana organizacija", + "Default Owner": "Zadani vlasnik", + "Default User (for cron/background jobs)": "Zadani korisnik (za cron/pozadinske poslove)", + "Default User Tenant": "Zadani zakupac korisnika", + "Default value": "Zadana vrijednost", + "Default value (Email)": "Zadana vrijednost (e-pošta)", + "Default value (Password)": "Zadana vrijednost (lozinka)", + "Default value (Phone number)": "Zadana vrijednost (telefonski broj)", + "Default value (Regex)": "Zadana vrijednost (regularni izraz)", + "Default: 5": "Zadano: 5", + "Defaults to current user. Select a different user if needed.": "Zadano na trenutnog korisnika. Odaberite drugog korisnika ako je potrebno.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Odgode se udvostručuju sa svakim pokušajem (2, 4, 8 minuta...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Odgode se povećavaju linearno (5, 10, 15 minuta...)", + "Delete": "Izbriši", + "Delete ({count})": "Izbriši ({count})", + "Delete Application": "Izbriši aplikaciju", + "Delete Audit Trail": "Izbriši revizijski trag", + "Delete Collection": "Izbriši zbirku", + "Delete ConfigSet": "Izbriši ConfigSet", + "Delete Objects": "Izbriši objekte", + "Delete Operations": "Izbriši operacije", + "Delete SOLR Collection": "Izbriši SOLR zbirku", + "Delete View": "Izbriši prikaz", + "Delete all objects in this schema": "Izbriši sve objekte u ovoj shemi", + "Delete permanently": "Izbriši trajno", + "Delete this conversation?": "Izbrisati ovaj razgovor?", + "Delete view": "Izbriši prikaz", + "Delete {count} object": "Izbriši {count} objekt", + "Delete {count} objects": "Izbriši {count} objekata", + "Deleted": "Izbrisano", + "Deleted By": "Izbrisao", + "Deleted Date": "Datum brisanja", + "Deleted Items Management": "Upravljanje izbrisanim stavkama", + "Deleted This Week": "Izbrisano ovaj tjedan", + "Deleted Today": "Izbrisano danas", + "Deleted:": "Izbrisano:", + "Deleting SOLR Collection...": "Brisanje SOLR zbirke...", + "Deleting...": "Brisanje...", + "Deletion Date Range": "Raspon datuma brisanja", + "Deletion Statistics": "Statistika brisanja", + "Deprecated": "Zastarjelo", + "Depublish": "Poništi objavu", + "Description": "Opis", + "Description Template": "Predložak opisa", + "Description shown as tooltip": "Opis prikazan kao opis alata", + "Description:": "Opis:", + "Detect data types and patterns": "Otkrij vrste i uzorke podataka", + "Detected At": "Otkriveno u", + "Detected Format:": "Otkriveni format:", + "Detected Issues:": "Otkriveni problemi:", + "Detected: {date}": "Otkriveno: {date}", + "Direct permission": "Izravno dopuštenje", + "Disable": "Onemogući", + "Disable this to make the agent": "Onemogućite ovo kako bi agent", + "Disabled": "Onemogućeno", + "Discover Files": "Otkrij datoteke", + "Discovered Properties": "Otkrivena svojstva", + "Display Format": "Format prikaza", + "Display Name": "Prikazni naziv", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Želite li trajno izbrisati sve filtrirane unose revizijskog traga? Ova se radnja ne može poništiti.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Želite li trajno izbrisati ovaj unos revizijskog traga? Ova se radnja ne može poništiti.", + "Documents": "Dokumenti", + "Doelbinding *": "Vezanost svrhe *", + "Dolphin OCR enabled": "Dolphin OCR omogućen", + "Download API Spec": "Preuzmi API specifikaciju", + "Download API Specification": "Preuzmi API specifikaciju", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Preuzmite kopiju svih svojih osobnih podataka pohranjenih u OpenRegister (GDPR članak 20.).", + "Drag a file or files here": "Povucite datoteku ili datoteke ovdje", + "Dry run completed: {count} fields would be created": "Probno izvođenje dovršeno: {count} polja bilo bi stvoreno", + "Dry run completed: {count} fields would be fixed": "Probno izvođenje dovršeno: {count} polja bilo bi ispravljeno", + "Duration in seconds (optional)": "Trajanje u sekundama (neobavezno)", + "Edit": "Uredi", + "Edit Agent": "Uredi agenta", + "Edit Application": "Uredi aplikaciju", + "Edit Configuration": "Uredi konfiguraciju", + "Edit Endpoint": "Uredi krajnju točku", + "Edit Organisation": "Uredi organizaciju", + "Edit Property '{key}' of '{title}'": "Uredi svojstvo '{key}' od '{title}'", + "Edit Register": "Uredi registar", + "Edit Schema": "Uredi shemu", + "Edit Source": "Uredi izvor", + "Edit View": "Uredi prikaz", + "Edit Webhook": "Uredi webhook", + "Edit labels": "Uredi oznake", + "Edit processing activity": "Uredi aktivnost obrade", + "Edit tags for {name}": "Uredi oznake za {name}", + "Edit verwerkingsactiviteit": "Uredi aktivnost obrade", + "Edit view": "Uredi prikaz", + "Edit view details": "Uredi pojedinosti prikaza", + "Effectiveness": "Učinkovitost", + "Email": "E-pošta", + "Email Addresses": "Adrese e-pošte", + "Email digest frequency": "Učestalost sažetka e-pošte", + "Emails": "E-pošte", + "Embedding Model": "Model ugrađivanja", + "Embedding Provider": "Pružatelj ugrađivanja", + "Embedding generated successfully": "Ugrađivanje uspješno generirano", + "Embedding provider connection successful!": "Veza s pružateljem ugrađivanja uspješna!", + "Embedding test failed: {error}": "Test ugrađivanja neuspješan: {error}", + "Enable": "Omogući", + "Enable RAG": "Omogući RAG", + "Enable auto-creation": "Omogući automatsko stvaranje", + "Enable automatic file vectorization": "Omogući automatsku vektorizaciju datoteka", + "Enable automatic object vectorization": "Omogući automatsku vektorizaciju objekata", + "Enable automatic synchronization": "Omogući automatsku sinkronizaciju", + "Enable calendar provider": "Omogući pružatelja kalendara", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Omogući detaljno bilježenje za SOLR operacije (preporučeno za otklanjanje pogrešaka)", + "Enable faceting": "Omogući fasetiranje", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Omogućite ili onemogućite LLM značajke. Konfigurirajte pružatelje i modele koristeći gumb LLM konfiguracija iznad.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Omogućite ili onemogućite LLM značajke. Konfigurirajte pružatelje i modele koristeći gumb LLM konfiguracija iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Omogućite ili onemogućite integraciju SOLR pretraživanja. Konfigurirajte postavke veze koristeći gumb Postavke veze iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Omogućite ili onemogućite integraciju SOLR pretraživanja. Konfigurirajte postavke veze koristeći gumb postavke veze iznad.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Omogućite ili onemogućite integraciju n8n tijeka rada. Konfigurirajte postavke veze ispod.", + "Enable or disable this webhook": "Omogući ili onemogući ovaj webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Omogućite alate koji agentu omogućuju interakciju s podacima putem pozivanja funkcija.", + "Enable vectorization for all existing and future views (may increase costs)": "Omogući vektorizaciju za sve postojeće i buduće prikaze (može povećati troškove)", + "Enable write-back to target objects": "Omogući upisivanje natrag u ciljne objekte", + "Enabled": "Omogućeno", + "End Date Field": "Polje datuma završetka", + "Endpoint Path*": "Putanja krajnje točke*", + "Endpoints": "Krajnje točke", + "Engine not found": "Mehanizam nije pronađen", + "Enter ConfigSet name": "Unesite naziv ConfigSet stavke", + "Enter Nextcloud usernames to grant access to this private agent": "Unesite Nextcloud korisnička imena za dodjelu pristupa ovom privatnom agentu", + "Enter agent description (optional)": "Unesite opis agenta (neobavezno)", + "Enter agent name": "Unesite naziv agenta", + "Enter application description (optional)": "Unesite opis aplikacije (neobavezno)", + "Enter application name": "Unesite naziv aplikacije", + "Enter collection name": "Unesite naziv zbirke", + "Enter configuration description (optional)": "Unesite opis konfiguracije (neobavezno)", + "Enter configuration title": "Unesite naslov konfiguracije", + "Enter description (optional)...": "Unesite opis (neobavezno)...", + "Enter new collection name": "Unesite novi naziv zbirke", + "Enter object ID": "Unesite ID objekta", + "Enter organisation description (optional)": "Unesite opis organizacije (neobavezno)", + "Enter organisation name": "Unesite naziv organizacije", + "Enter search term": "Unesite pojam za pretraživanje", + "Enter search terms or leave empty to browse all": "Unesite pojmove za pretraživanje ili ostavite prazno za pregledavanje svega", + "Enter system prompt for the agent": "Unesite sistemski upit za agenta", + "Enter username and press Enter": "Unesite korisničko ime i pritisnite Enter", + "Enter view name...": "Unesite naziv prikaza...", + "Enter webhook name": "Unesite naziv webhooka", + "Enter your API key": "Unesite svoj API ključ", + "Enter your object here...": "Unesite svoj objekt ovdje...", + "Enter your schema here...": "Unesite svoju shemu ovdje...", + "Entities": "Entiteti", + "Entities detected": "Otkriveni entiteti", + "Entity ID: {id}": "ID entiteta: {id}", + "Entity Information": "Informacije o entitetu", + "Entity Recognition": "Prepoznavanje entiteta", + "Entity Recognition Method": "Metoda prepoznavanja entiteta", + "Entity deleted successfully": "Entitet je uspješno izbrisan", + "Entity not found": "Entitet nije pronađen", + "Entries to be deleted:": "Unosi koji će biti izbrisani:", + "Enum Issue": "Problem s enumeracijom", + "Enum constraint is missing": "Nedostaje ograničenje enumeracije", + "Erase (Art 17)": "Brisanje (čl. 17.)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Izbrisati {count} objekt(a) za ovaj subjekt? Ova radnja bilježi se u revizijskom tragu.", + "Erasure complete": "Brisanje dovršeno", + "Erasure preview": "Pregled brisanja", + "Error": "Pogreška", + "Error Details": "Pojedinosti o pogrešci", + "Error Information": "Informacije o pogrešci", + "Error Message": "Poruka o pogrešci", + "Error deleting audit trails: {error}": "Pogreška pri brisanju revizijskih tragova: {error}", + "Error deleting search trails: {error}": "Pogreška pri brisanju tragova pretraživanja: {error}", + "Error loading application": "Pogreška pri učitavanju aplikacije", + "Error loading audit trails": "Pogreška pri učitavanju revizijskih tragova", + "Error loading entity": "Pogreška pri učitavanju entiteta", + "Error loading search trails": "Pogreška pri učitavanju tragova pretraživanja", + "Estimated Batches:": "Procijenjeni paketi:", + "Estimated Cost:": "Procijenjeni trošak:", + "Estimated Duration:": "Procijenjeno trajanje:", + "Event": "Događaj", + "Event Property for Payload": "Svojstvo događaja za korisni teret", + "Events": "Događaji", + "Every schema with detected PII has a processing-activity annotation.": "Svaka shema s otkrivenim PII ima napomenu o aktivnosti obrade.", + "Example": "Primjer", + "Example Value": "Primjer vrijednosti", + "Example value for this property": "Primjer vrijednosti za ovo svojstvo", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Isključivi maksimum", + "Exclusive Minimum": "Isključivi minimum", + "Exclusive maximum": "Isključivi maksimum", + "Exclusive minimum": "Isključivi minimum", + "Execution Mode": "Način izvođenja", + "Execution Time": "Vrijeme izvođenja", + "Execution Time Range": "Raspon vremena izvođenja", + "Existing Improvements": "Postojeća poboljšanja", + "Expand details": "Proširi pojedinosti", + "Expiration": "Istek", + "Expires": "Istječe", + "Expires in (e.g., 90d)": "Istječe za (npr. 90d)", + "Exponential": "Eksponencijalno", + "Export": "Izvoz", + "Export \"{schema}\" objects from \"{register}\"": "Izvezi objekte \"{schema}\" iz \"{register}\"", + "Export Configuration": "Konfiguracija izvoza", + "Export Format:": "Format izvoza:", + "Export Objects": "Izvoz objekata", + "Export completed successfully": "Izvoz je uspješno dovršen", + "Export downloaded successfully": "Izvoz je uspješno preuzet", + "Export failed: {error}": "Izvoz nije uspio: {error}", + "Export is rate limited. Please try again later.": "Izvoz je ograničen po stopi. Pokušajte ponovno kasnije.", + "Export my data": "Izvezi moje podatke", + "Export, view, or delete audit trails": "Izvezite, pregledajte ili izbrišite revizijske tragove", + "Exporting...": "Izvoz...", + "Extend Schema": "Proširi shemu", + "Extended": "Prošireno", + "Extensions": "Proširenja", + "Extra Query Parameters": "Dodatni parametri upita", + "Extract Now": "Izdvoji sada", + "Extract Pending Files": "Izdvoji datoteke na čekanju", + "Extract now": "Izdvoji sada", + "Extract properties from each object": "Izdvoji svojstva iz svakog objekta", + "Extracted At": "Izdvojeno", + "Extracted at": "Izdvojeno", + "Extraction": "Izdvajanje", + "Extraction Mode": "Način izdvajanja", + "Extraction Scope": "Opseg izdvajanja", + "Extraction Status": "Status izdvajanja", + "Extraction error for {file}: {error}": "Pogreška izdvajanja za {file}: {error}", + "Facet Description": "Opis fasete", + "Facet Order": "Redoslijed fasete", + "Facet Title": "Naslov fasete", + "Facet Type": "Vrsta fasete", + "Facet configuration updated successfully": "Konfiguracija fasete uspješno ažurirana", + "Facetable": "Fasetabilno", + "Faceting Configuration:": "Konfiguracija faseta:", + "Facets discovered and configured successfully": "Fasete su uspješno otkrivene i konfigurirane", + "Facets discovered successfully": "Fasete su uspješno otkrivene", + "Failed": "Neuspjelo", + "Failed to add attachment to object": "Dodavanje privitka objektu nije uspjelo", + "Failed to add user to organisation": "Dodavanje korisnika u organizaciju nije uspjelo", + "Failed to analyze schema properties": "Analiza svojstava sheme nije uspjela", + "Failed to archive conversation": "Arhiviranje razgovora nije uspjelo", + "Failed to calculate sizes": "Izračun veličina nije uspio", + "Failed to cancel deactivation": "Otkazivanje deaktivacije nije uspjelo", + "Failed to change active organisation: {error}": "Promjena aktivne organizacije nije uspjela: {error}", + "Failed to change password": "Promjena lozinke nije uspjela", + "Failed to check version: {error}": "Provjera verzije nije uspjela: {error}", + "Failed to clear audit trails: {error}": "Brisanje revizijskih tragova nije uspjelo: {error}", + "Failed to clear blob objects: {error}": "Brisanje blob objekata nije uspjelo: {error}", + "Failed to clear cache: {error}": "Brisanje predmemorije nije uspjelo: {error}", + "Failed to clear collection": "Brisanje kolekcije nije uspjelo", + "Failed to clear conversation": "Brisanje razgovora nije uspjelo", + "Failed to clear embeddings: {error}": "Brisanje ugradbi nije uspjelo: {error}", + "Failed to clear search trails: {error}": "Brisanje tragova pretraživanja nije uspjelo: {error}", + "Failed to clear {type} cache: {error}": "Brisanje predmemorije {type} nije uspjelo: {error}", + "Failed to copy changes": "Kopiranje promjena nije uspjelo", + "Failed to copy data": "Kopiranje podataka nije uspjelo", + "Failed to copy data to clipboard": "Kopiranje podataka u međuspremnik nije uspjelo", + "Failed to copy to clipboard": "Kopiranje u međuspremnik nije uspjelo", + "Failed to copy token": "Kopiranje tokena nije uspjelo", + "Failed to create conversation": "Stvaranje razgovora nije uspjelo", + "Failed to create missing SOLR fields: {error}": "Stvaranje nedostajućih SOLR polja nije uspjelo: {error}", + "Failed to create or find project": "Stvaranje ili pronalaženje projekta nije uspjelo", + "Failed to create token": "Stvaranje tokena nije uspjelo", + "Failed to delete SOLR collection: {error}": "Brisanje SOLR kolekcije nije uspjelo: {error}", + "Failed to delete collection": "Brisanje kolekcije nije uspjelo", + "Failed to delete conversation": "Brisanje razgovora nije uspjelo", + "Failed to delete objects for {schema}: {error}": "Brisanje objekata za {schema} nije uspjelo: {error}", + "Failed to delete webhook": "Brisanje webhooka nije uspjelo", + "Failed to depublish register: {error}": "Depubliciranje registra nije uspjelo: {error}", + "Failed to depublish schema: {error}": "Depubliciranje sheme nije uspjelo: {error}", + "Failed to depublish: {error}": "Depubliciranje nije uspjelo: {error}", + "Failed to discover files: {error}": "Otkrivanje datoteka nije uspjelo: {error}", + "Failed to download API specification": "Preuzimanje API specifikacije nije uspjelo", + "Failed to export data": "Izvoz podataka nije uspio", + "Failed to export objects": "Izvoz objekata nije uspio", + "Failed to fetch conversation history": "Dohvaćanje povijesti razgovora nije uspjelo", + "Failed to fix mismatched SOLR fields: {error}": "Ispravljanje neusklađenih SOLR polja nije uspjelo: {error}", + "Failed to get SOLR field configuration": "Dohvaćanje konfiguracije SOLR polja nije uspjelo", + "Failed to get chat statistics": "Dohvaćanje statistike razgovora nije uspjelo", + "Failed to get database information: %s": "Dohvaćanje informacija o bazi podataka nije uspjelo: %s", + "Failed to get entity categories": "Dohvaćanje kategorija entiteta nije uspjelo", + "Failed to get entity statistics": "Dohvaćanje statistike entiteta nije uspjelo", + "Failed to get entity types": "Dohvaćanje vrsta entiteta nije uspjelo", + "Failed to get response: {error}": "Dohvaćanje odgovora nije uspjelo: {error}", + "Failed to invalidate app store cache: {error}": "Poništavanje predmemorije trgovine aplikacija nije uspjelo: {error}", + "Failed to leave organisation: {error}": "Napuštanje organizacije nije uspjelo: {error}", + "Failed to link object": "Povezivanje objekta nije uspjelo", + "Failed to load Deck cards": "Učitavanje Deck kartica nije uspjelo", + "Failed to load LLM configuration": "Učitavanje LLM konfiguracije nije uspjelo", + "Failed to load Nextcloud groups": "Učitavanje Nextcloud grupa nije uspjelo", + "Failed to load cache statistics: {error}": "Učitavanje statistike predmemorije nije uspjelo: {error}", + "Failed to load conversation": "Učitavanje razgovora nije uspjelo", + "Failed to load entities": "Učitavanje entiteta nije uspjelo", + "Failed to load entity": "Učitavanje entiteta nije uspjelo", + "Failed to load extraction data": "Učitavanje podataka o izdvajanju nije uspjelo", + "Failed to load files": "Učitavanje datoteka nije uspjelo", + "Failed to load linked contacts": "Učitavanje povezanih kontakata nije uspjelo", + "Failed to load linked emails": "Učitavanje povezanih e-pošta nije uspjelo", + "Failed to load linked events": "Učitavanje povezanih događaja nije uspjelo", + "Failed to load organisations": "Učitavanje organizacija nije uspjelo", + "Failed to load register data": "Učitavanje podataka registra nije uspjelo", + "Failed to load related entities": "Učitavanje povezanih entiteta nije uspjelo", + "Failed to load settings: {error}": "Učitavanje postavki nije uspjelo: {error}", + "Failed to load statistics: {error}": "Učitavanje statistike nije uspjelo: {error}", + "Failed to load templates": "Učitavanje predložaka nije uspjelo", + "Failed to load view: {error}": "Učitavanje prikaza nije uspjelo: {error}", + "Failed to load webhooks": "Učitavanje webhookova nije uspjelo", + "Failed to load workflows": "Učitavanje tijekova rada nije uspjelo", + "Failed to process message": "Obrada poruke nije uspjela", + "Failed to publish register: {error}": "Objavljivanje registra nije uspjelo: {error}", + "Failed to publish schema: {error}": "Objavljivanje sheme nije uspjelo: {error}", + "Failed to publish: {error}": "Objavljivanje nije uspjelo: {error}", + "Failed to refresh database information": "Osvježavanje informacija o bazi podataka nije uspjelo", + "Failed to reindex collection": "Ponovno indeksiranje kolekcije nije uspjelo", + "Failed to remove avatar": "Uklanjanje avatara nije uspjelo", + "Failed to remove link": "Uklanjanje veze nije uspjelo", + "Failed to remove member: {error}": "Uklanjanje člana nije uspjelo: {error}", + "Failed to remove schema {schema}: {error}": "Uklanjanje sheme {schema} nije uspjelo: {error}", + "Failed to remove user from organisation": "Uklanjanje korisnika iz organizacije nije uspjelo", + "Failed to rename conversation": "Preimenovanje razgovora nije uspjelo", + "Failed to request deactivation": "Zahtjev za deaktivaciju nije uspio", + "Failed to restore conversation": "Vraćanje razgovora nije uspjelo", + "Failed to retrieve extraction status.": "Dohvaćanje statusa izdvajanja nije uspjelo.", + "Failed to retrieve objects for file.": "Dohvaćanje objekata za datoteku nije uspjelo.", + "Failed to retry extraction": "Ponovni pokušaj izdvajanja nije uspio", + "Failed to retry webhook": "Ponovni pokušaj webhooka nije uspio", + "Failed to revoke token": "Opoziv tokena nije uspio", + "Failed to save GitHub token": "Spremanje GitHub tokena nije uspjelo", + "Failed to save GitLab URL": "Spremanje GitLab URL-a nije uspjelo", + "Failed to save GitLab token": "Spremanje GitLab tokena nije uspjelo", + "Failed to save LLM settings: {error}": "Spremanje LLM postavki nije uspjelo: {error}", + "Failed to save additional feedback": "Spremanje dodatnih povratnih informacija nije uspjelo", + "Failed to save configuration.": "Spremanje konfiguracije nije uspjelo.", + "Failed to save configuration: {error}": "Spremanje konfiguracije nije uspjelo: {error}", + "Failed to save facet configuration": "Spremanje konfiguracije fasete nije uspjelo", + "Failed to save feedback": "Spremanje povratnih informacija nije uspjelo", + "Failed to save file settings: {error}": "Spremanje postavki datoteke nije uspjelo: {error}", + "Failed to save n8n configuration": "Spremanje n8n konfiguracije nije uspjelo", + "Failed to save preferences": "Spremanje preferencija nije uspjelo", + "Failed to save roles": "Spremanje uloga nije uspjelo", + "Failed to save settings": "Spremanje postavki nije uspjelo", + "Failed to save settings: {error}": "Spremanje postavki nije uspjelo: {error}", + "Failed to save view: {error}": "Spremanje prikaza nije uspjelo: {error}", + "Failed to save warmup interval: {error}": "Spremanje intervala zagrijavanja nije uspjelo: {error}", + "Failed to save webhook": "Spremanje webhooka nije uspjelo", + "Failed to send feedback": "Slanje povratnih informacija nije uspjelo", + "Failed to set active organisation: {error}": "Postavljanje aktivne organizacije nije uspjelo: {error}", + "Failed to setup SOLR: {error}": "Postavljanje SOLR-a nije uspjelo: {error}", + "Failed to start file warmup: {error}": "Pokretanje zagrijavanja datoteka nije uspjelo: {error}", + "Failed to start processing {type} files: {error}": "Pokretanje obrade {type} datoteka nije uspjelo: {error}", + "Failed to start vectorization: {error}": "Pokretanje vektorizacije nije uspjelo: {error}", + "Failed to switch organisation: {error}": "Prebacivanje organizacije nije uspjelo: {error}", + "Failed to sync magic table for {schema}: {error}": "Sinkronizacija čarobne tablice za {schema} nije uspjela: {error}", + "Failed to test SOLR connection: {error}": "Testiranje SOLR veze nije uspjelo: {error}", + "Failed to test webhook": "Testiranje webhooka nije uspjelo", + "Failed to update LLM settings: {error}": "Ažuriranje LLM postavki nije uspjelo: {error}", + "Failed to update Multitenancy settings: {error}": "Ažuriranje postavki višestruke zakupništva nije uspjelo: {error}", + "Failed to update RBAC settings: {error}": "Ažuriranje RBAC postavki nije uspjelo: {error}", + "Failed to update Retention settings: {error}": "Ažuriranje postavki čuvanja nije uspjelo: {error}", + "Failed to update SOLR settings: {error}": "Ažuriranje SOLR postavki nije uspjelo: {error}", + "Failed to update favorite status": "Ažuriranje statusa favorita nije uspjelo", + "Failed to update schema configuration for {schema}: {error}": "Ažuriranje konfiguracije sheme za {schema} nije uspjelo: {error}", + "Failed to update schema properties": "Ažuriranje svojstava sheme nije uspjelo", + "Failed to update view": "Ažuriranje prikaza nije uspjelo", + "Failed to update view: {error}": "Ažuriranje prikaza nije uspjelo: {error}", + "Failed to update webhook": "Ažuriranje webhooka nije uspjelo", + "Failed to upload avatar": "Učitavanje avatara nije uspjelo", + "Failed to validate {schema}: {error}": "Provjera valjanosti {schema} nije uspjela: {error}", + "Failed to vectorize files: {error}": "Vektorizacija datoteka nije uspjela: {error}", + "Failed to warmup names cache: {error}": "Zagrijavanje predmemorije naziva nije uspjelo: {error}", + "Federation": "Federacija", + "Feedback recorded": "Povratne informacije zabilježene", + "Fewer sources (1-3):": "Manje izvora (1-3):", + "Field": "Polje", + "Fields to Display": "Polja za prikaz", + "File": "Datoteka", + "File #{id}": "Datoteka #{id}", + "File Chunk Prediction": "Predviđanje fragmenta datoteke", + "File Collection": "Kolekcija datoteka", + "File Handling": "Rukovanje datotekama", + "File Location": "Lokacija datoteke", + "File Management": "Upravljanje datotekama", + "File Name": "Naziv datoteke", + "File Path": "Putanja datoteke", + "File Type Selection": "Odabir vrste datoteke", + "File Types to Process": "Vrste datoteka za obradu", + "File Vectorization": "Vektorizacija datoteka", + "File Warmup": "Zagrijavanje datoteka", + "File actions menu": "Izbornik radnji datoteke", + "File anonymized successfully": "Datoteka je uspješno anonimizirana", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vektorizacija fragmenata datoteke još nije implementirana. Fragmenti su spremni i pohranjeni, ali je usluga vektorizacije u razvoju.", + "File collection not configured": "Kolekcija datoteka nije konfigurirana", + "File discovery completed": "Otkrivanje datoteka dovršeno", + "File extraction completed": "Izdvajanje datoteke dovršeno", + "File extraction queued": "Izdvajanje datoteke stavljeno u red čekanja", + "File indexed successfully": "Datoteka je uspješno indeksirana", + "File is already anonymized": "Datoteka je već anonimizirana", + "File name": "Naziv datoteke", + "File not found": "Datoteka nije pronađena", + "File settings saved successfully": "Postavke datoteke uspješno spremljene", + "File settings updated successfully": "Postavke datoteke uspješno ažurirane", + "File sources": "Izvori datoteka", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vektorizacija datoteka dovršena! {vectorized} fragmenata vektorizirano iz {files} datoteka. {failed} neuspjelo.", + "File vectorization configuration saved successfully": "Konfiguracija vektorizacije datoteka uspješno spremljena", + "File vectorization started. Check the statistics section for progress.": "Vektorizacija datoteka pokrenuta. Provjerite odjeljak statistike za napredak.", + "File warmup completed": "Zagrijavanje datoteka dovršeno", + "Files": "Datoteke", + "Files added successfully": "Datoteke uspješno dodane", + "Files with Completed Extraction:": "Datoteke s dovršenim izdvajanjem:", + "Files → fileCollection, Objects → objectCollection": "Datoteke → fileCollection, Objekti → objectCollection", + "Files → filecollection, objects → objectcollection": "Datoteke → filecollection, objekti → objectcollection", + "Filter Audit Trails": "Filtriraj revizijske tragove", + "Filter Deleted Items": "Filtriraj izbrisane stavke", + "Filter Objects": "Filtriraj objekte", + "Filter Properties": "Filtriraj svojstva", + "Filter Search Trails": "Filtriraj tragove pretraživanja", + "Filter Statistics": "Filtriraj statistiku", + "Filter and analyze search trail entries": "Filtrirajte i analizirajte unose tragova pretraživanja", + "Filter and manage audit trail entries": "Filtrirajte i upravljajte unosima revizijskog traga", + "Filter and manage soft deleted items": "Filtrirajte i upravljajte meko izbrisanim stavkama", + "Filter and search entities": "Filtrirajte i pretražujte entitete", + "Filter by object ID": "Filtriraj po ID-u objekta", + "Filter by search term": "Filtriraj po pojmu za pretraživanje", + "Filter by type": "Filtriraj po vrsti", + "Filter by webhook": "Filtriraj po webhooku", + "Filter data loaded automatically. Use the filters below to refine your search.": "Podaci filtera učitani automatski. Upotrijebite filtere u nastavku za pročišćavanje pretraživanja.", + "Filter fields...": "Filtriraj polja...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrirajte okidače webhooka po svojstvima korisnog tereta (jedno po retku, format: key: value)", + "Filtered": "Filtrirano", + "Filters": "Filteri", + "Finalizing setup...": "Dovršavanje postavljanja...", + "Fireworks AI Chat Settings": "Postavke razgovora Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfiguracija ugradbi Fireworks AI", + "First": "Prvo", + "Fixed": "Fiksno", + "For chat and retrieval-augmented generation": "Za razgovor i generiranje uz potporu dohvaćanja", + "For vector embeddings and semantic search": "Za vektorske ugradbe i semantičko pretraživanje", + "Format": "Format", + "Format Issue": "Problem s formatom", + "Format constraint is missing": "Nedostaje ograničenje formata", + "From (e.g. -7 days or 2025-01-01)": "Od (npr. -7 dana ili 2025-01-01)", + "From Date": "Od datuma", + "From date": "Od datuma", + "Full data copied to clipboard": "Potpuni podaci kopirani u međuspremnik", + "Function Tools": "Funkcijski alati", + "General Issue": "Općeniti problem", + "Generate recommendations and confidence scores": "Generiraj preporuke i ocjene pouzdanosti", + "Generate report": "Generiraj izvješće", + "Generate the verantwoordingsdocument": "Generiraj dokument o odgovornosti", + "Generate vectors immediately when new objects are created": "Generiraj vektore odmah kada se stvore novi objekti", + "Generated: {time}": "Generirano: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Dobijte uvide u stanje podataka vaše organizacije u stvarnom vremenu fokusiranjem na registre, definicije shema te pohranu i upotrebu objekata.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Dobijte uvide u stanje podataka vaše organizacije, trendove rasta i metrike kvalitete u stvarnom vremenu.", + "GitHub Personal Access Token": "GitHub osobni pristupni token", + "GitHub token is valid": "GitHub token je valjan", + "GitHub token is valid! Username: {username}": "GitHub token je valjan! Korisničko ime: {username}", + "GitHub token removed successfully": "GitHub token uspješno uklonjen", + "GitHub token saved successfully": "GitHub token uspješno spremljen", + "GitHub token test failed: {message}": "Testiranje GitHub tokena nije uspjelo: {message}", + "GitLab Personal Access Token": "GitLab osobni pristupni token", + "GitLab URL saved successfully": "GitLab URL uspješno spremljen", + "GitLab token is valid": "GitLab token je valjan", + "GitLab token is valid! Username: {username}": "GitLab token je valjan! Korisničko ime: {username}", + "GitLab token saved successfully": "GitLab token uspješno spremljen", + "GitLab token test failed: {message}": "Testiranje GitLab tokena nije uspjelo: {message}", + "Go to linked Register": "Idi na povezani registar", + "Go to linked Schema": "Idi na povezanu shemu", + "Go to organisation": "Idi na organizaciju", + "Grant additional permissions beyond standard CRUD operations": "Dodijeli dodatna dopuštenja izvan standardnih CRUD operacija", + "Group Quota": "Kvota grupe", + "Groups": "Grupe", + "HTML preview": "HTML pretpregled", + "HTTP Method": "HTTP metoda", + "HTTP method used to send webhook requests": "HTTP metoda koja se koristi za slanje zahtjeva webhooka", + "Headers": "Zaglavlja", + "Health": "Stanje", + "Heartbeat successful - connection kept alive": "Otkucaj uspješan - veza održana aktivnom", + "Helpful": "Korisno", + "Here you can configure the details for various connections.": "Ovdje možete konfigurirati pojedinosti za razne veze.", + "Here you can configure the details for your organisation.": "Ovdje možete konfigurirati pojedinosti za svoju organizaciju.", + "Hide": "Sakrij", + "Hide Filters": "Sakrij filtere", + "Hide in collection view": "Sakrij u prikazu kolekcije", + "Hide in forms": "Sakrij u obrascima", + "Hide in list view": "Sakrij u prikazu popisa", + "High": "Visoko", + "High Confidence": "Visoka pouzdanost", + "Host": "Poslužitelj", + "Hourly": "Svaki sat", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Koliko duboko prolaziti kroz ugniježđena svojstva objekta (1-20). Više vrijednosti obuhvaćaju više pojedinosti, ali povećavaju veličinu vektora.", + "How often to check for updates (1-168 hours)": "Koliko često provjeravati ažuriranja (1-168 sati)", + "How to handle retries for failed webhook deliveries": "Kako rukovati ponovnim pokušajima za neuspjele isporuke webhooka", + "IBANs": "IBAN-ovi", + "ID": "ID", + "ID of input mapping (optional)": "ID ulaznog mapiranja (neobavezno)", + "ID of output mapping (optional)": "ID izlaznog mapiranja (neobavezno)", + "ID of the target resource": "ID ciljnog resursa", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ograničenja IP stope uspješno izbrisana", + "Identify properties not in the schema": "Identificiraj svojstva koja nisu u shemi", + "Immutable": "Nepromjenjivo", + "Import": "Uvoz", + "Import Configuration": "Konfiguracija uvoza", + "Import Data into Register": "Uvezi podatke u registar", + "Import successful": "Uvoz uspješan", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Uvezite predložak report-bundle.json da biste dobili registar `reports`, zatim stvorite svoju prvu nadzornu ploču putem standardnog korisničkog sučelja za objekte. Nadzorne ploče deklariraju svoje widgete u JSON-u, a prikazivač svakom widgetu dostavlja podatke agregacije uživo.", + "Improved Property": "Poboljšano svojstvo", + "In use": "U upotrebi", + "Inactive": "Neaktivno", + "Inactive organisations cannot be used": "Neaktivne organizacije ne mogu se koristiti", + "Include IDs and names of related objects for better contextual search": "Uključi ID-ove i nazive povezanih objekata za bolje kontekstualno pretraživanje", + "Include ids and names of related objects for better contextual search": "Uključi id-ove i nazive povezanih objekata za bolje kontekstualno pretraživanje", + "Include related object references": "Uključi reference povezanih objekata", + "Include schema and register metadata": "Uključi metapodatke sheme i registra", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Informacije o trenutnoj OpenRegister instalaciji", + "Inherited from register": "Naslijeđeno od registra", + "Inherits permissions from register": "Nasljeđuje dopuštenja od registra", + "Initialization failed": "Inicijalizacija nije uspjela", + "Initialize Project": "Inicijaliziraj projekt", + "Initializing SOLR setup...": "Inicijalizacija SOLR postavljanja...", + "Input Mapping": "Ulazno mapiranje", + "Inspect Fields": "Pregledaj polja", + "Inspect Index": "Pregledaj indeks", + "Inspect SOLR Index": "Pregledaj SOLR indeks", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instalirajte aplikaciju notify_push iz trgovine aplikacija Nextcloud kako biste omogućili ažuriranja u stvarnom vremenu.", + "Integrations": "Integracije", + "Internet location (URL) of this installation": "Internetska lokacija (URL) ove instalacije", + "Interval": "Interval", + "Invalid": "Nevaljano", + "Invalid batch size. Must be between 1 and 5000": "Nevaljana veličina paketa. Mora biti između 1 i 5000", + "Invalid feedback type": "Nevaljana vrsta povratne informacije", + "Invalid field name provided": "Naveden je nevaljan naziv polja", + "Invalid maxObjects. Must be 0 (all) or positive number": "Nevaljan maxObjects. Mora biti 0 (sve) ili pozitivan broj", + "Invalid:": "Nevaljano:", + "Invite Users": "Pozovi korisnike", + "Invited Users": "Pozvani korisnici", + "Inzage (Art 15)": "Uvid (čl. 15.)", + "Inzage results": "Rezultati uvida", + "Issue": "Problem", + "Items per page:": "Stavki po stranici:", + "Just now": "Upravo sada", + "Key": "Ključ", + "LLM Configuration": "LLM konfiguracija", + "LLM actions menu": "Izbornik LLM radnji", + "LLM configuration saved successfully": "LLM konfiguracija uspješno spremljena", + "LLM features disabled": "LLM značajke onemogućene", + "LLM features enabled": "LLM značajke omogućene", + "LLM must be enabled with an embedding provider configured": "LLM mora biti omogućen uz konfiguriranog pružatelja ugradbi", + "LLM settings saved successfully": "LLM postavke uspješno spremljene", + "LLM settings updated successfully": "LLM postavke uspješno ažurirane", + "Label": "Oznaka", + "Labels": "Oznake", + "Languages": "Jezici", + "Last": "Zadnje", + "Last Triggered": "Zadnje aktivirano", + "Leave empty to allow all users access": "Ostavite prazno da biste dopustili pristup svim korisnicima", + "Leave off for auto-detection from property format.": "Ostavite isključeno za automatsko otkrivanje iz formata svojstva.", + "Left organisation successfully": "Uspješno napuštena organizacija", + "Legal basis *": "Pravna osnova *", + "Length Range:": "Raspon duljine:", + "Linear": "Linearno", + "Link": "Veza", + "Link an email from the Mail app sidebar to associate it with this object.": "Povežite e-poštu iz bočne trake aplikacije Mail kako biste je pridružili ovom objektu.", + "Link an object to see it here.": "Povežite objekt da biste ga ovdje vidjeli.", + "Link another object": "Poveži drugi objekt", + "Link existing event": "Poveži postojeći događaj", + "Link removed": "Veza uklonjena", + "Link to Object": "Veza na objekt", + "Link to {name}": "Veza na {name}", + "Linked Objects": "Povezani objekti", + "Linked by {user}": "Povezao {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Povezane e-pošte, kalendarski događaji, kontakti i Deck kartice pojavit će se ovdje.", + "Linked to {name}": "Povezano s {name}", + "Load Advanced Filters": "Učitaj napredne filtere", + "Load advanced filters with live data from your search index": "Učitaj napredne filtere s podacima uživo iz vašeg indeksa pretraživanja", + "Load more": "Učitaj više", + "Load view": "Učitaj prikaz", + "Loading API tokens...": "Učitavanje API tokena...", + "Loading ConfigSets...": "Učitavanje ConfigSetova...", + "Loading LLM configuration...": "Učitavanje LLM konfiguracije...", + "Loading RBAC settings...": "Učitavanje RBAC postavki...", + "Loading activity...": "Učitavanje aktivnosti...", + "Loading advanced filters...": "Učitavanje naprednih filtera...", + "Loading agents...": "Učitavanje agenata...", + "Loading application details...": "Učitavanje pojedinosti aplikacije...", + "Loading applications...": "Učitavanje aplikacija...", + "Loading archived conversations...": "Učitavanje arhiviranih razgovora...", + "Loading audit trails...": "Učitavanje revizijskih tragova...", + "Loading available tools...": "Učitavanje dostupnih alata...", + "Loading cache statistics...": "Učitavanje statistike predmemorije...", + "Loading collections...": "Učitavanje kolekcija...", + "Loading configurations...": "Učitavanje konfiguracija...", + "Loading conversation...": "Učitavanje razgovora...", + "Loading conversations...": "Učitavanje razgovora...", + "Loading deleted items...": "Učitavanje izbrisanih stavki...", + "Loading endpoints": "Učitavanje krajnjih točaka", + "Loading entities...": "Učitavanje entiteta...", + "Loading events...": "Učitavanje događaja...", + "Loading filter data...": "Učitavanje podataka filtera...", + "Loading groups...": "Učitavanje grupa...", + "Loading linked objects...": "Učitavanje povezanih objekata...", + "Loading log details...": "Učitavanje pojedinosti zapisnika...", + "Loading multitenancy settings...": "Učitavanje postavki višestruke zakupništva...", + "Loading n8n configuration...": "Učitavanje n8n konfiguracije...", + "Loading permission matrix...": "Učitavanje matrice dopuštenja...", + "Loading preferences...": "Učitavanje preferencija...", + "Loading register data...": "Učitavanje podataka registra...", + "Loading registers...": "Učitavanje registara...", + "Loading retention settings...": "Učitavanje postavki čuvanja...", + "Loading schemas...": "Učitavanje shema...", + "Loading search configuration...": "Učitavanje konfiguracije pretraživanja...", + "Loading search trails...": "Učitavanje tragova pretraživanja...", + "Loading sources...": "Učitavanje izvora...", + "Loading statistics...": "Učitavanje statistike...", + "Loading text extraction settings...": "Učitavanje postavki izdvajanja teksta...", + "Loading tokens...": "Učitavanje tokena...", + "Loading user groups...": "Učitavanje korisničkih grupa...", + "Loading users...": "Učitavanje korisnika...", + "Loading views...": "Učitavanje prikaza...", + "Loading...": "Učitavanje...", + "Local": "Lokalno", + "Local Version": "Lokalna verzija", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Pronađite svaki objekt koji referencira ispitanika (čl. 15. uvid), pregledajte brisanje (čl. 17. pravo na zaborav) ili izvezite njihove podatke (čl. 20. prenosivost).", + "Location": "Lokacija", + "Location Field": "Polje lokacije", + "Locations": "Lokacije", + "Locked": "Zaključano", + "Locked:": "Zaključano:", + "Login successful": "Prijava uspješna", + "Logs": "Zapisnici", + "Low": "Nisko", + "Low Confidence": "Niska pouzdanost", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Niži brojevi pojavljuju se prvi u bočnoj traci filtera. Ostavite prazno za automatski redoslijed.", + "Lucene Version:": "Lucene verzija:", + "Magic table sync completed for {schema}": "Sinkronizacija čarobne tablice dovršena za {schema}", + "Magic table synchronized successfully": "Čarobna tablica uspješno sinkronizirana", + "Mail Integration": "Integracija e-pošte", + "Mail integration is not available": "Integracija e-pošte nije dostupna", + "Make this installation discoverable within the federation network.": "Učinite ovu instalaciju pronalažljivom unutar mreže federacije.", + "Manage Organisation Roles": "Upravljaj ulogama organizacije", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Upravljajte SOLR kolekcijama (spremišta podataka) i dodijelite ih za objekte i datoteke.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Upravljajte SOLR ConfigSetovima (predlošci konfiguracije) za svoje kolekcije.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Upravljajte SOLR kolekcijama (spremišta podataka) i dodijelite ih za objekte i datoteke.", + "Manage SOLR configsets (configuration templates) for your collections.": "Upravljajte SOLR configsetovima (predlošci konfiguracije) za svoje kolekcije.", + "Manage and configure agents for automated tasks": "Upravljajte i konfigurirajte agente za automatizirane zadatke", + "Manage and monitor file text extraction status": "Upravljajte i nadzirite status izdvajanja teksta iz datoteka", + "Manage and restore soft deleted items from your registers": "Upravljajte i vraćajte meko izbrisane stavke iz svojih registara", + "Manage and view detected entities from files and objects": "Upravljajte i pregledajte otkrivene entitete iz datoteka i objekata", + "Manage document templates and themes": "Upravljajte predlošcima i temama dokumenata", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Upravljajte n8n tijekovima rada za OpenRegister automatizaciju. Tijekovi rada bit će pohranjeni u konfiguriranom projektu.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Upravljajte aktivnostima obrade, izvršavajte zahtjeve za pristup ispitanika i revidirajte usklađenost prema EU GDPR / nizozemskom AVG.", + "Manage webhooks for event-driven integrations": "Upravljajte webhookovima za integracije vođene događajima", + "Manage your account settings, security, and personal data.": "Upravljajte postavkama računa, sigurnošću i osobnim podacima.", + "Manage your applications and modules": "Upravljajte svojim aplikacijama i modulima", + "Manage your chat conversations": "Upravljajte svojim razgovorima", + "Manage your data registers and their configurations": "Upravljajte svojim registrima podataka i njihovim konfiguracijama", + "Manage your data schemas and their properties": "Upravljajte svojim shemama podataka i njihovim svojstvima", + "Manage your data sources and their configurations": "Upravljajte svojim izvorima podataka i njihovim konfiguracijama", + "Manage your organisations and switch between them": "Upravljajte svojim organizacijama i prebacujte se između njih", + "Manage your saved search configurations": "Upravljajte svojim spremljenim konfiguracijama pretraživanja", + "Manage your system configurations and settings": "Upravljajte svojim konfiguracijama i postavkama sustava", + "Managed": "Upravljano", + "Mappings": "Mapiranja", + "Mass validation completed successfully": "Masovna provjera valjanosti uspješno dovršena", + "Mass validation failed: {error}": "Masovna provjera valjanosti nije uspjela: {error}", + "Max Files (0 = all)": "Maks. datoteka (0 = sve)", + "Max Length": "Maks. duljina", + "Max Length:": "Maks. duljina:", + "Max Objects (0 = all)": "Maks. objekata (0 = sve)", + "Max Retries": "Maks. ponovnih pokušaja", + "Max Shards/Node": "Maks. shardova/čvor", + "Max Tokens": "Maks. tokena", + "Max execution time (ms)": "Maks. vrijeme izvođenja (ms)", + "Max ms": "Maks. ms", + "Max result count": "Maks. broj rezultata", + "Max results": "Maks. rezultata", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksimalna veličina datoteke (MB)", + "Maximum Nesting Depth": "Maksimalna dubina ugniježđivanja", + "Maximum length": "Maksimalna duljina", + "Maximum length constraint is missing": "Nedostaje ograničenje maksimalne duljine", + "Maximum length is too restrictive": "Maksimalna duljina previše je restriktivna", + "Maximum number of files to process. Set to 0 to process all files.": "Maksimalan broj datoteka za obradu. Postavite na 0 za obradu svih datoteka.", + "Maximum number of items": "Maksimalan broj stavki", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksimalan broj objekata za obradu. Postavite na 0 za obradu svih objekata.", + "Maximum number of retry attempts for failed deliveries": "Maksimalan broj ponovnih pokušaja za neuspjele isporuke", + "Maximum time to wait before committing changes": "Maksimalno vrijeme čekanja prije potvrđivanja promjena", + "Maximum tokens to generate": "Maksimalan broj tokena za generiranje", + "Maximum value": "Maksimalna vrijednost", + "Maximum value constraint is missing": "Nedostaje ograničenje maksimalne vrijednosti", + "Maximum value is too restrictive": "Maksimalna vrijednost previše je restriktivna", + "Medium": "Srednje", + "Medium Confidence": "Srednja pouzdanost", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Srednji upiti: Pretraživanja s nekim filtriranjem ili više parametara (npr. rasponi datuma, određeni registri/sheme)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Srednji upiti: pretraživanja s nekim filtriranjem ili više parametara (npr. rasponi datuma, određeni registri/sheme)", + "Member removed successfully": "Član uspješno uklonjen", + "Members": "Članovi", + "Members ({count})": "Članovi ({count})", + "Members of selected groups can access this view": "Članovi odabranih grupa mogu pristupiti ovom prikazu", + "Members:": "Članovi:", + "Memory prediction calculated successfully": "Predviđanje memorije uspješno izračunato", + "Message content is required": "Sadržaj poruke je obavezan", + "Message does not belong to this conversation": "Poruka ne pripada ovom razgovoru", + "Message not found": "Poruka nije pronađena", + "Metadata": "Metapodaci", + "Metadata Filters": "Filteri metapodataka", + "Method": "Metoda", + "Method*": "Metoda*", + "Method: {method}": "Metoda: {method}", + "Min Length": "Min. duljina", + "Min execution time (ms)": "Min. vrijeme izvođenja (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. broj rezultata", + "Min results": "Min. rezultata", + "Minimum": "Minimum", + "Minimum length": "Minimalna duljina", + "Minimum number of items": "Minimalan broj stavki", + "Minimum value": "Minimalna vrijednost", + "Minimum value constraint is missing": "Nedostaje ograničenje minimalne vrijednosti", + "Minimum value is too restrictive": "Minimalna vrijednost previše je restriktivna", + "Missing conversation or agentUuid": "Nedostaje razgovor ili agentUuid", + "Missing conversationId": "Nedostaje conversationId", + "Missing message": "Nedostaje poruka", + "Missing object fields created successfully": "Nedostajuća polja objekta uspješno stvorena", + "Mode:": "Način:", + "Model": "Model", + "Model Name": "Naziv modela", + "Modified": "Izmijenjeno", + "Monthly": "Mjesečno", + "More sources (10-20):": "Više izvora (10-20):", + "Most Active Objects": "Najaktivniji objekti", + "Multiple Of": "Višekratnik od", + "Multiple of": "Višekratnik od", + "Multiple search trail deletion not implemented yet": "Brisanje više tragova pretraživanja još nije implementirano", + "Multitenancy": "Višestruka zakupništvo", + "Multitenancy settings updated successfully": "Postavke višestruke zakupništva uspješno ažurirane", + "My Account": "Moj račun", + "Naam": "Naziv", + "Naam *": "Naziv *", + "Name": "Naziv", + "Name *": "Naziv *", + "Name*": "Naziv*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Predmemorija naziva uspješno je zagrijana: {count} naziva učitano za {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Predmemorija naziva uspješno je zagrijana: {count} naziva učitano za {time}. {message}", + "Names cache warmup completed": "Zagrijavanje predmemorije naziva dovršeno", + "Naming pattern for copies:": "Obrazac imenovanja za kopije:", + "New Collection Name": "Naziv nove zbirke", + "New Configuration": "Nova konfiguracija", + "New Conversation": "Novi razgovor", + "New Properties": "Nova svojstva", + "New Property": "Novo svojstvo", + "New User Group": "Nova korisnička grupa", + "New Value": "Nova vrijednost", + "New activity": "Nova aktivnost", + "New password": "Nova lozinka", + "New processing activity": "Nova aktivnost obrade", + "New users without specific organisation membership will be automatically added to this organisation": "Novi korisnici bez određenog članstva u organizaciji bit će automatski dodani u ovu organizaciju", + "New verwerkingsactiviteit": "Nova aktivnost obrade", + "Next": "Sljedeće", + "Nextcloud Groups": "Nextcloud grupe", + "No": "Ne", + "No API tokens.": "Nema API tokena.", + "No ConfigSets found": "Nisu pronađeni ConfigSet-ovi", + "No Deck cards linked to this object": "Nijedna Deck kartica nije povezana s ovim objektom", + "No GitHub token configured": "GitHub token nije konfiguriran", + "No GitHub token provided": "GitHub token nije naveden", + "No GitLab token provided": "GitLab token nije naveden", + "No Organisation": "Nema organizacije", + "No active filters": "Nema aktivnih filtara", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Nema aktivnih objekata za meko brisanje za shemu {schema}. Upotrijebite \"Trajno izbriši\" za uklanjanje meko izbrisanih objekata.", + "No activity data available": "Nema dostupnih podataka o aktivnosti", + "No activity found.": "Nije pronađena aktivnost.", + "No agents available": "Nema dostupnih agenata", + "No agents found": "Nisu pronađeni agenti", + "No app store cache exists yet": "Predmemorija trgovine aplikacijama još ne postoji", + "No applications are available.": "Nema dostupnih aplikacija.", + "No applications found": "Nisu pronađene aplikacije", + "No approval steps for this object.": "Nema koraka odobravanja za ovaj objekt.", + "No archived conversations": "Nema arhiviranih razgovora", + "No audit trail entries found": "Nisu pronađeni unosi revizijskog traga", + "No changes recorded": "Nisu zabilježene promjene", + "No chunks to vectorize": "Nema dijelova za vektorizaciju", + "No collections found": "Nisu pronađene zbirke", + "No configuration data": "Nema podataka o konfiguraciji", + "No configuration selected": "Nije odabrana konfiguracija", + "No configurations are available.": "Nema dostupnih konfiguracija.", + "No configurations found": "Nisu pronađene konfiguracije", + "No configurations found for this application.": "Nisu pronađene konfiguracije za ovu aplikaciju.", + "No contacts linked to this object": "Nijedan kontakt nije povezan s ovim objektom", + "No conversations yet. Create a new one to get started!": "Još nema razgovora. Stvorite novi za početak!", + "No dashboards yet": "Još nema nadzornih ploča", + "No data available": "Nema dostupnih podataka", + "No data available for chart": "Nema dostupnih podataka za grafikon", + "No deleted items found": "Nisu pronađene izbrisane stavke", + "No deletion data available": "Nema dostupnih podataka o brisanju", + "No description found": "Nije pronađen opis", + "No description provided": "Nije naveden opis", + "No emails linked to this object": "Nijedna e-pošta nije povezana s ovim objektom", + "No endpoints defined": "Nisu definirane krajnje točke", + "No entities detected for this email.": "Nisu otkriveni entiteti za ovu e-poštu.", + "No entities detected in this file. Run text extraction first.": "Nisu otkriveni entiteti u ovoj datoteci. Prvo pokrenite izvlačenje teksta.", + "No entities found": "Nisu pronađeni entiteti", + "No entities have been detected yet": "Još nisu otkriveni entiteti", + "No events found": "Nisu pronađeni događaji", + "No events linked to this object": "Nijedan događaj nije povezan s ovim objektom", + "No expired audit trails found to clear": "Nisu pronađeni istekli revizijski tragovi za brisanje", + "No expired search trails found to clear": "Nisu pronađeni istekli tragovi pretraživanja za brisanje", + "No extraction data available for this file": "Nema dostupnih podataka o izvlačenju za ovu datoteku", + "No facetable fields available. Select a register and schema to see available filters.": "Nema dostupnih polja za fasete. Odaberite registar i shemu da biste vidjeli dostupne filtre.", + "No files found": "Nisu pronađene datoteke", + "No files have been extracted yet": "Još nisu izvučene datoteke", + "No files selected": "Nisu odabrane datoteke", + "No files to process": "Nema datoteka za obradu", + "No files to reindex": "Nema datoteka za ponovno indeksiranje", + "No filters are currently active. This will delete ALL audit trail entries!": "Trenutno nije aktivan nijedan filtar. Time će se izbrisati SVI unosi revizijskog traga!", + "No groups found. Try a different search.": "Nisu pronađene grupe. Pokušajte s drugačijim pretraživanjem.", + "No label": "Nema oznake", + "No labels": "Nema oznaka", + "No linked objects": "Nema povezanih objekata", + "No log entries found": "Nisu pronađeni unosi zapisnika", + "No logs are available for this configuration.": "Nema dostupnih zapisnika za ovu konfiguraciju.", + "No logs are available for this source.": "Nema dostupnih zapisnika za ovaj izvor.", + "No logs found": "Nisu pronađeni zapisnici", + "No matches": "Nema podudaranja", + "No mismatched fields found - SOLR schema is properly configured": "Nisu pronađena nepodudarna polja - SOLR shema je ispravno konfigurirana", + "No objects are currently selected for deletion.": "Trenutno nije odabran nijedan objekt za brisanje.", + "No objects are currently selected for permanent deletion.": "Trenutno nije odabran nijedan objekt za trajno brisanje.", + "No objects are currently selected for restoration.": "Trenutno nije odabran nijedan objekt za vraćanje.", + "No objects found": "Nisu pronađeni objekti", + "No objects found to vectorize": "Nisu pronađeni objekti za vektorizaciju", + "No objects linked to this email": "Nijedan objekt nije povezan s ovom e-poštom", + "No objects selected": "Nije odabran nijedan objekt", + "No objects selected for deletion": "Nije odabran nijedan objekt za brisanje", + "No objects selected for restoration": "Nije odabran nijedan objekt za vraćanje", + "No objects to delete": "Nema objekata za brisanje", + "No objects to delete for schema {schema}": "Nema objekata za brisanje za shemu {schema}", + "No objects were permanently deleted": "Nijedan objekt nije trajno izbrisan", + "No organisations found": "Nisu pronađene organizacije", + "No personal data was found for this subject identifier.": "Nisu pronađeni osobni podaci za ovaj identifikator subjekta.", + "No processing activities yet": "Još nema aktivnosti obrade", + "No properties found": "Nisu pronađena svojstva", + "No properties match your filters.": "Nijedno svojstvo ne odgovara vašim filtrima.", + "No purge date set": "Datum čišćenja nije postavljen", + "No register data available": "Nema dostupnih podataka o registru", + "No register objects reference this file": "Nijedan objekt registra ne referencira ovu datoteku", + "No registers found": "Nisu pronađeni registri", + "No registers found for this application.": "Nisu pronađeni registri za ovu aplikaciju.", + "No registers found. Create a register to configure permissions.": "Nisu pronađeni registri. Stvorite registar za konfiguriranje dozvola.", + "No related cases found for this sender": "Nisu pronađeni povezani predmeti za ovog pošiljatelja", + "No related entities": "Nema povezanih entiteta", + "No relations found": "Nisu pronađene veze", + "No request body available": "Nema dostupnog tijela zahtjeva", + "No response body available": "Nema dostupnog tijela odgovora", + "No saved views yet. Create one in the Search tab!": "Još nema spremljenih prikaza. Stvorite jedan na kartici Pretraživanje!", + "No saved views yet. create one in the search tab!": "Još nema spremljenih prikaza. stvorite jedan na kartici pretraživanja!", + "No scan run yet": "Skeniranje još nije pokrenuto", + "No schema data available": "Nema dostupnih podataka o shemi", + "No schema selected for exploration": "Nije odabrana shema za istraživanje", + "No schemas configured for mail linking.": "Nijedna shema nije konfigurirana za povezivanje pošte.", + "No schemas found": "Nisu pronađene sheme", + "No schemas found for this application.": "Nisu pronađene sheme za ovu aplikaciju.", + "No search terms data available": "Nema dostupnih podataka o pojmovima pretraživanja", + "No search trail entries found": "Nisu pronađeni unosi traga pretraživanja", + "No sources are available.": "Nema dostupnih izvora.", + "No sources found": "Nisu pronađeni izvori", + "No synchronizations found": "Nisu pronađene sinkronizacije", + "No templates found": "Nisu pronađeni predlošci", + "No templates have been created yet": "Još nisu stvoreni predlošci", + "No tools available. Tools can be registered by installed apps.": "Nema dostupnih alata. Alate mogu registrirati instalirane aplikacije.", + "No users in this organisation.": "Nema korisnika u ovoj organizaciji.", + "No valid magic-mapped register+schema combinations found": "Nisu pronađene valjane magic-mapped kombinacije registra+sheme", + "No views found. Create views first before configuring vectorization.": "Nisu pronađeni prikazi. Prvo stvorite prikaze prije konfiguriranja vektorizacije.", + "No views match your search": "Nijedan prikaz ne odgovara vašem pretraživanju", + "No webhooks found": "Nisu pronađeni webhookovi", + "No webhooks have been configured yet": "Još nisu konfigurirani webhookovi", + "No workflows found in this project. Create workflows in the n8n editor.": "U ovom projektu nisu pronađeni tijekovi rada. Stvorite tijekove rada u n8n uređivaču.", + "None": "Nijedan", + "Not Found": "Nije pronađeno", + "Not extracted": "Nije izvučeno", + "Not helpful": "Nije korisno", + "Not in use": "Nije u upotrebi", + "Note:": "Napomena:", + "Notifications": "Obavijesti", + "Number Constraints": "Ograničenja broja", + "Number of Sources": "Broj izvora", + "Number of chunks to vectorize in one API call (1-100).": "Broj dijelova za vektorizaciju u jednom API pozivu (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Broj dijelova za vektorizaciju u jednom API pozivu. Više = brže, ali više memorije. Preporučeno: 10-50.", + "Number of objects to process in each batch (1-5000).": "Broj objekata za obradu u svakoj seriji (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Broj objekata za vektorizaciju u jednom API pozivu. Više = brže, ali više memorije. Preporučeno: 10-50.", + "Numeric Range:": "Brojčani raspon:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Zbirka objekata", + "Object Configuration:": "Konfiguracija objekta:", + "Object Count Prediction": "Predviđanje broja objekata", + "Object Extraction Mode": "Način izvlačenja objekata", + "Object ID": "ID objekta", + "Object Management": "Upravljanje objektima", + "Object Text Extraction": "Izvlačenje teksta objekta", + "Object Vectorization": "Vektorizacija objekata", + "Object Warmup": "Zagrijavanje objekata", + "Object changes in owned objects": "Promjene objekta u vlastitim objektima", + "Object collection not configured": "Zbirka objekata nije konfigurirana", + "Object metadata incomplete for file upload": "Metapodaci objekta nepotpuni za prijenos datoteke", + "Object not found": "Objekt nije pronađen", + "Object permanently deleted": "Objekt trajno izbrisan", + "Object restored successfully": "Objekt uspješno vraćen", + "Object settings updated successfully": "Postavke objekta uspješno ažurirane", + "Object sources": "Izvori objekata", + "Object successfully deleted": "Objekt uspješno izbrisan", + "Object successfully modified": "Objekt uspješno izmijenjen", + "Object unlocked successfully": "Objekt uspješno otključan", + "Object vectorization configuration saved successfully": "Konfiguracija vektorizacije objekata uspješno spremljena", + "Object vectorized successfully": "Objekt uspješno vektoriziran", + "Object:": "Objekt:", + "Objects": "Objekti", + "Objects Analyzed": "Analizirani objekti", + "Objects Distribution": "Raspodjela objekata", + "Objects being analyzed": "Objekti koji se analiziraju", + "Objects by Register": "Objekti po registru", + "Objects by Schema": "Objekti po shemi", + "Objects deletion completed for {schema}": "Brisanje objekata dovršeno za {schema}", + "Objects deletion completed successfully": "Brisanje objekata uspješno dovršeno", + "Objects in schema": "Objekti u shemi", + "Objects successfully deleted": "Objekti uspješno izbrisani", + "Objects to Process:": "Objekti za obradu:", + "Objects to be analyzed": "Objekti koji će biti analizirani", + "Objects to be deleted": "Objekti koji će biti izbrisani", + "Objects to be validated": "Objekti koji će biti provjereni", + "Objects will be serialized as JSON text before vectorization": "Objekti će biti serijalizirani kao JSON tekst prije vektorizacije", + "Objects will be soft deleted and moved to the": "Objekti će biti meko izbrisani i premješteni u", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekti će biti meko izbrisani (označeni kao izbrisani, ali zadržani u bazi podataka). Mogu se kasnije vratiti ako je potrebno.", + "Old Value": "Stara vrijednost", + "Oldest Item (days)": "Najstarija stavka (dani)", + "Ollama Chat Settings": "Postavke Ollama razgovora", + "Ollama Embedding Configuration": "Konfiguracija Ollama ugrađivanja", + "Ollama URL": "Ollama URL", + "Only With Changes": "Samo s promjenama", + "Only members of selected groups can access this organisation": "Samo članovi odabranih grupa mogu pristupiti ovoj organizaciji", + "Open Nextcloud App Store": "Otvori Nextcloud trgovinu aplikacija", + "Open OpenRegister sidebar": "Otvori OpenRegister bočnu traku", + "Open Register Objects": "Otvori objekte registra", + "Open folder": "Otvori mapu", + "Open in OpenRegister": "Otvori u OpenRegister", + "Open n8n Editor": "Otvori n8n uređivač", + "OpenAI Chat Settings": "Postavke OpenAI razgovora", + "OpenAI Embedding Configuration": "Konfiguracija OpenAI ugrađivanja", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister postavke", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Nadzorne ploče i zakazani izvještaji koje definira operater. Svaka nadzorna ploča je prvorazredni objekt u `reports` registru; widgeti se deklariraju u `widgets` polju nadzorne ploče i prikazuju uživo iz agregacija / GraphQL.", + "Optimizing search performance...": "Optimiziranje izvedbe pretraživanja...", + "Optional URL-friendly identifier": "Neobavezni identifikator prilagođen URL-u", + "Optional description for this facet": "Neobavezni opis za ovu fasetu", + "Optional end date property": "Neobavezno svojstvo datuma završetka", + "Optional event description template": "Neobavezni predložak opisa događaja", + "Optional location property": "Neobavezno svojstvo lokacije", + "Optional webhook secret for signature verification": "Neobavezna webhook tajna za provjeru potpisa", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Neobavezno: Dodajte parametre upita za filtriranje referencirane sheme (npr. status=active&type=public)", + "Or": "Ili", + "Order": "Redoslijed", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Poredane BCP 47 jezične oznake. Prvi jezik je zadani za registar i pokreće Accept-Language zamjenu za prevodiva svojstva.", + "Organisation": "Organizacija", + "Organisation Configuration": "Konfiguracija organizacije", + "Organisation ID": "ID organizacije", + "Organisation Members": "Članovi organizacije", + "Organisation Statistics": "Statistika organizacije", + "Organisation created successfully": "Organizacija uspješno stvorena", + "Organisation membership changes": "Promjene članstva u organizaciji", + "Organisation name is required": "Naziv organizacije je obavezan", + "Organisation settings saved successfully": "Postavke organizacije uspješno spremljene", + "Organisation successfully created": "Organizacija uspješno stvorena", + "Organisation successfully updated": "Organizacija uspješno ažurirana", + "Organisational measures": "Organizacijske mjere", + "Organisations": "Organizacije", + "Organisatorische maatregelen": "Organizacijske mjere", + "Organization": "Organizacija", + "Organization ID (Optional)": "ID organizacije (neobavezno)", + "Organizations": "Organizacije", + "Orphaned Items": "Stavke bez roditelja", + "Other": "Ostalo", + "Output Mapping": "Mapiranje izlaza", + "Overview": "Pregled", + "Owner": "Vlasnik", + "Owner:": "Vlasnik:", + "PDF": "PDF", + "PII hits": "PII pogodci", + "PII rows": "PII redci", + "Page {current} of {total}": "Stranica {current} od {total}", + "Parallel Mode (Faster, more resource intensive)": "Paralelni način (brže, zahtjevnije za resurse)", + "Parallel:": "Paralelno:", + "Password": "Lozinka", + "Password changes are not supported by your authentication provider.": "Vaš davatelj autentifikacije ne podržava promjene lozinke.", + "Password for SOLR authentication (optional)": "Lozinka za SOLR autentifikaciju (neobavezno)", + "Password updated successfully": "Lozinka uspješno ažurirana", + "Path": "Putanja", + "Path in repository": "Putanja u repozitoriju", + "Path where the configuration file will be saved in the repository": "Putanja na kojoj će konfiguracijska datoteka biti spremljena u repozitoriju", + "Path where the register OAS file will be saved in the repository": "Putanja na kojoj će OAS datoteka registra biti spremljena u repozitoriju", + "Pattern (regex)": "Obrazac (regex)", + "Pattern Issue": "Problem s obrascem", + "Pattern constraint is missing": "Nedostaje ograničenje obrasca", + "Patterns:": "Obrasci:", + "Pending": "Na čekanju", + "Permanent Delete Mode": "Način trajnog brisanja", + "Permanently Delete": "Trajno izbriši", + "Permanently Delete ({count})": "Trajno izbriši ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Trajno izbriši svih {count} meko izbrisanih objekata. To se ne može poništiti!", + "Permanently delete already soft-deleted objects": "Trajno izbriši već meko izbrisane objekte", + "Permanently delete this conversation?": "Trajno izbrisati ovaj razgovor?", + "Permission Matrix": "Matrica dozvola", + "Person": "Osoba", + "Personal Data": "Osobni podaci", + "Personal Data Export": "Izvoz osobnih podataka", + "Persons": "Osobe", + "Phone": "Telefon", + "Phone Numbers": "Telefonski brojevi", + "Photos": "Fotografije", + "Please create an agent in the": "Stvorite agenta u", + "Please select a user": "Odaberite korisnika", + "Please select an agent to continue": "Odaberite agenta za nastavak", + "Please select an organisation": "Odaberite organizaciju", + "Please select at least one item to import": "Odaberite barem jednu stavku za uvoz", + "Please select registers or schemas first": "Prvo odaberite registre ili sheme", + "Please select which register and schema to use for the new object": "Odaberite koji registar i shemu koristiti za novi objekt", + "Please try again later.": "Pokušajte ponovno kasnije.", + "Please wait while we fetch your agents.": "Pričekajte dok dohvaćamo vaše agente.", + "Please wait while we fetch your applications.": "Pričekajte dok dohvaćamo vaše aplikacije.", + "Please wait while we fetch your configurations.": "Pričekajte dok dohvaćamo vaše konfiguracije.", + "Please wait while we fetch your deleted items.": "Pričekajte dok dohvaćamo vaše izbrisane stavke.", + "Please wait while we fetch your sources.": "Pričekajte dok dohvaćamo vaše izvore.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Pričekajte dok trajno brišemo SOLR zbirku. To može potrajati nekoliko trenutaka.", + "Polls": "Ankete", + "Popular Search Terms": "Popularni pojmovi pretraživanja", + "Port": "Priključak", + "Portabiliteit (Art 20)": "Prenosivost (čl. 20)", + "Preferences saved": "Preferencije spremljene", + "Prerequisites": "Preduvjeti", + "Press Enter to send, Shift+Enter for new line": "Pritisnite Enter za slanje, Shift+Enter za novi redak", + "Press enter to send, shift+enter for new line": "Pritisnite enter za slanje, shift+enter za novi redak", + "Prettify": "Uljepšaj", + "Preview erasure": "Pretpregled brisanja", + "Preview:": "Pretpregled:", + "Previous": "Prethodno", + "Private": "Privatno", + "Private Agent (Default)": "Privatni agent (zadano)", + "Private agents": "Privatni agenti", + "Process Name (optional)": "Naziv procesa (neobavezno)", + "Processed {indexed} files, {failed} failed": "Obrađeno {indexed} datoteka, {failed} neuspješno", + "Processes chunks in batches with simulated parallelism.": "Obrađuje dijelove u serijama sa simuliranim paralelizmom.", + "Processes file chunks sequentially (safest).": "Obrađuje dijelove datoteke sekvencijalno (najsigurnije).", + "Processes objects in chunks with simulated parallelism.": "Obrađuje objekte u dijelovima sa simuliranim paralelizmom.", + "Processes objects sequentially (safest).": "Obrađuje objekte sekvencijalno (najsigurnije).", + "Processing": "Obrada", + "Processing Limits": "Ograničenja obrade", + "Processing...": "Obrada...", + "Project initialized successfully": "Projekt uspješno inicijaliziran", + "Project not found. Please initialize first.": "Projekt nije pronađen. Prvo inicijalizirajte.", + "Properties": "Svojstva", + "Property Behaviors": "Ponašanja svojstva", + "Property Configuration:": "Konfiguracija svojstva:", + "Property Filters": "Filtri svojstva", + "Property Title": "Naslov svojstva", + "Property Type": "Vrsta svojstva", + "Property can be improved": "Svojstvo se može poboljšati", + "Property could not be added": "Svojstvo se nije moglo dodati", + "Property could not be updated": "Svojstvo se nije moglo ažurirati", + "Property name of inversed relation": "Naziv svojstva obrnute veze", + "Property successfully added": "Svojstvo uspješno dodano", + "Property successfully updated": "Svojstvo uspješno ažurirano", + "Protected": "Zaštićeno", + "Provider": "Davatelj", + "Provider is required for testing": "Davatelj je obavezan za testiranje", + "Public": "Javno", + "Public View": "Javni prikaz", + "Public views can be accessed by anyone in the system": "Javnim prikazima može pristupiti bilo tko u sustavu", + "Publish": "Objavi", + "Publish Configuration to GitHub": "Objavi konfiguraciju na GitHub", + "Publish OAS": "Objavi OAS", + "Publish Objects": "Objavi objekte", + "Publish Register OAS to GitHub": "Objavi OAS registra na GitHub", + "Publish all objects in this schema": "Objavi sve objekte u ovoj shemi", + "Publish objects to make them publicly available": "Objavi objekte kako bi bili javno dostupni", + "Published": "Objavljeno", + "Published:": "Objavljeno:", + "Publishing...": "Objavljivanje...", + "Purge": "Očisti", + "Purge Date": "Datum čišćenja", + "Purge {count} object from database": "Očisti {count} objekt iz baze podataka", + "Purge {count} objects from database": "Očisti {count} objekata iz baze podataka", + "Purpose limitation *": "Ograničenje svrhe *", + "Push Notifications": "Push obavijesti", + "Query Complexity Distribution": "Raspodjela složenosti upita", + "Query Help": "Pomoć za upite", + "Query parameter is required": "Parametar upita je obavezan", + "Quota": "Kvota", + "RAG": "RAG", + "RAG Configuration": "RAG konfiguracija", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC postavke uspješno ažurirane", + "Rate limits cleared successfully": "Ograničenja brzine uspješno obrisana", + "Raw Changes Data": "Neobrađeni podaci o promjenama", + "Re-vectorize on object update": "Ponovno vektoriziraj pri ažuriranju objekta", + "Read": "Čitanje", + "Real-time push notification status via notify_push": "Status push obavijesti u stvarnom vremenu putem notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push obavijesti u stvarnom vremenu su aktivne. Povezani klijenti primaju trenutna ažuriranja kada se objekti stvore, ažuriraju ili izbrišu.", + "Realtime push active": "Push u stvarnom vremenu aktivan", + "Realtime push not available — the notify_push app is not installed": "Push u stvarnom vremenu nije dostupan — aplikacija notify_push nije instalirana", + "Reason": "Razlog", + "Reason (optional)": "Razlog (neobavezno)", + "Rebase operation completed successfully": "Operacija rebase uspješno dovršena", + "Rebase operation failed: {error}": "Operacija rebase nije uspjela: {error}", + "Rechtsgrond": "Pravna osnova", + "Recommendations:": "Preporuke:", + "Recommended Type:": "Preporučena vrsta:", + "Recommended: 5 sources": "Preporučeno: 5 izvora", + "Refresh": "Osvježi", + "Refresh Data": "Osvježi podatke", + "Refresh Stats": "Osvježi statistiku", + "Refresh Workflows": "Osvježi tijekove rada", + "Refresh dashboard": "Osvježi nadzornu ploču", + "Refresh database info": "Osvježi informacije o bazi podataka", + "Register": "Registar", + "Register #{id}": "Registar #{id}", + "Register / Schema": "Registar / Shema", + "Register ID": "ID registra", + "Register Objects": "Objekti registra", + "Register Overview": "Pregled registra", + "Register Statistics": "Statistika registra", + "Register Totals": "Ukupni iznosi registra", + "Register and schema are required": "Registar i shema su obavezni", + "Register annotation": "Anotacija registra", + "Register depublished successfully": "Registar uspješno povučen iz objave", + "Register not found": "Registar nije pronađen", + "Register objects": "Objekti registra", + "Register objects deletion completed successfully": "Brisanje objekata registra uspješno dovršeno", + "Register published successfully": "Registar uspješno objavljen", + "Register/Schema Usage": "Upotreba registra/sheme", + "Register:": "Registar:", + "Registers": "Registri", + "Reindex": "Ponovno indeksiraj", + "Reindex completed": "Ponovno indeksiranje dovršeno", + "Reindex completed successfully": "Ponovno indeksiranje uspješno dovršeno", + "Reindex completed! Processed {count} objects in {duration}s": "Ponovno indeksiranje dovršeno! Obrađeno {count} objekata za {duration}s", + "Reindex failed": "Ponovno indeksiranje nije uspjelo", + "Reindex failed: %s": "Ponovno indeksiranje nije uspjelo: %s", + "Reject": "Odbij", + "Rejected": "Odbijeno", + "Related Cases": "Povezani predmeti", + "Relations": "Veze", + "Remote Version": "Udaljena verzija", + "Remove": "Ukloni", + "Remove Deck card": "Ukloni Deck karticu", + "Remove avatar": "Ukloni avatar", + "Remove contact": "Ukloni kontakt", + "Remove filter": "Ukloni filtar", + "Remove from favorites": "Ukloni iz favorita", + "Remove from list": "Ukloni s popisa", + "Remove group": "Ukloni grupu", + "Remove link": "Ukloni vezu", + "Remove link to {name}": "Ukloni vezu na {name}", + "Remove link to {name}?": "Ukloniti vezu na {name}?", + "Remove link to {title}": "Ukloni vezu na {title}", + "Remove oneOf entry": "Ukloni oneOf unos", + "Remove property after write-back": "Ukloni svojstvo nakon povratnog upisa", + "Remove {title}": "Ukloni {title}", + "Remove {userId} from this organisation?": "Ukloniti {userId} iz ove organizacije?", + "Removed": "Uklonjeno", + "Removed from favorites": "Uklonjeno iz favorita", + "Removing schema {schema}...": "Uklanjanje sheme {schema}...", + "Rename Conversation": "Preimenuj razgovor", + "Rename conversation": "Preimenuj razgovor", + "Replicas": "Replike", + "Reports": "Izvještaji", + "Repository": "Repozitorij", + "Request Body": "Tijelo zahtjeva", + "Request Data": "Podaci zahtjeva", + "Request Details": "Pojedinosti zahtjeva", + "Request Quota (per day)": "Kvota zahtjeva (po danu)", + "Request account deactivation": "Zatraži deaktivaciju računa", + "Request account deactivation. This will notify administrators for review.": "Zatraži deaktivaciju računa. Time će administratori biti obaviješteni radi pregleda.", + "Request body must be JSON with a \"query\" field": "Tijelo zahtjeva mora biti JSON s poljem \"query\"", + "Request timeout in seconds": "Istek zahtjeva u sekundama", + "Requested at": "Zatraženo u", + "Required": "Obavezno", + "Required Token Scopes": "Obavezni opsezi tokena", + "Required field": "Obavezno polje", + "Required status is inconsistent": "Status obaveznosti je nedosljedan", + "Requires Dolphin AI for OCR text extraction": "Zahtijeva Dolphin AI za OCR izvlačenje teksta", + "Rerun Search": "Ponovno pokreni pretraživanje", + "Rerunning search: {searchTerm}": "Ponovno pokretanje pretraživanja: {searchTerm}", + "Reset Changes": "Poništi promjene", + "Reset Filters": "Poništi filtre", + "Resource Quotas": "Kvote resursa", + "Response Body": "Tijelo odgovora", + "Response Details": "Pojedinosti odgovora", + "Response Time:": "Vrijeme odgovora:", + "Restore": "Vrati", + "Restore conversation": "Vrati razgovor", + "Restore or permanently delete items": "Vrati ili trajno izbriši stavke", + "Restore {count} object": "Vrati {count} objekt", + "Restore {count} objects": "Vrati {count} objekata", + "Result Count Range": "Raspon broja rezultata", + "Results": "Rezultati", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Rok čuvanja (ISO-8601 trajanje, npr. P10Y, P30D)", + "Retention settings updated successfully": "Postavke čuvanja uspješno ažurirane", + "Retrieve all objects for this schema": "Dohvati sve objekte za ovu shemu", + "Retry": "Pokušaj ponovno", + "Retry Failed Extractions": "Ponovi neuspjela izvlačenja", + "Retry Policy": "Pravila ponovnog pokušaja", + "Retry completed": "Ponovni pokušaj dovršen", + "Retry upload": "Ponovi prijenos", + "Retrying...": "Ponovni pokušaj...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite izbrisati klikom na gumb za uklanjanje.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite uključiti.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite trajno izbrisati klikom na gumb za uklanjanje. Ova se radnja ne može poništiti.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite vratiti klikom na gumb za uklanjanje. Objekti će biti vraćeni na svoju izvornu lokaciju.", + "Revoke": "Opozovi", + "Right": "Desno", + "Risk Level": "Razina rizika", + "Risk level": "Razina rizika", + "Roles and Permissions": "Uloge i dozvole", + "Roles updated successfully": "Uloge uspješno ažurirane", + "Run compliance scan": "Pokreni skeniranje usklađenosti", + "SOLR Connection Settings": "Postavke SOLR veze", + "SOLR Index Warmup": "Zagrijavanje SOLR indeksa", + "SOLR Version:": "SOLR verzija:", + "SOLR actions menu": "Izbornik SOLR radnji", + "SOLR base path (usually /solr)": "SOLR osnovna putanja (obično /solr)", + "SOLR collection deleted successfully": "SOLR zbirka uspješno izbrisana", + "SOLR connection test failed: {error}": "Test SOLR veze nije uspio: {error}", + "SOLR connection test successful!": "Test SOLR veze uspješan!", + "SOLR index warmup completed successfully": "Zagrijavanje SOLR indeksa uspješno dovršeno", + "SOLR is disabled": "SOLR je onemogućen", + "SOLR is not available or not configured": "SOLR nije dostupan ili nije konfiguriran", + "SOLR logging disabled": "SOLR zapisivanje onemogućeno", + "SOLR logging enabled": "SOLR zapisivanje omogućeno", + "SOLR search disabled": "SOLR pretraživanje onemogućeno", + "SOLR search enabled": "SOLR pretraživanje omogućeno", + "SOLR server hostname or IP address": "SOLR naziv poslužitelja ili IP adresa", + "SOLR server port number (optional, defaults to 8983)": "Broj priključka SOLR poslužitelja (neobavezno, zadano 8983)", + "SOLR settings updated successfully": "SOLR postavke uspješno ažurirane", + "SOLR setup completed successfully": "SOLR postavljanje uspješno dovršeno", + "SOLR setup completed successfully!": "SOLR postavljanje uspješno dovršeno!", + "SOLR setup error: %s": "Pogreška SOLR postavljanja: %s", + "SOLR setup failed": "SOLR postavljanje nije uspjelo", + "SOLR setup failed - check logs": "SOLR postavljanje nije uspjelo - provjerite zapisnike", + "SOLR setup failed. Please check the configuration and try again.": "SOLR postavljanje nije uspjelo. Provjerite konfiguraciju i pokušajte ponovno.", + "SOLR setup failed: {error}": "SOLR postavljanje nije uspjelo: {error}", + "SOLR warmup failed: {error}": "Zagrijavanje SOLR nije uspjelo: {error}", + "Sample Values:": "Uzorci vrijednosti:", + "Save": "Spremi", + "Save Configuration": "Spremi konfiguraciju", + "Save Connection Settings": "Spremi postavke veze", + "Save Roles": "Spremi uloge", + "Save Settings": "Spremi postavke", + "Save changes": "Spremi promjene", + "Save current search as view": "Spremi trenutno pretraživanje kao prikaz", + "Save labels": "Spremi oznake", + "Save tags for {name}": "Spremi oznake za {name}", + "Save the organisation first to manage users.": "Prvo spremite organizaciju za upravljanje korisnicima.", + "Saved Views": "Spremljeni prikazi", + "Saving...": "Spremanje...", + "Scheduled": "Zakazano", + "Schema": "Shema", + "Schema ID": "ID sheme", + "Schema Name": "Naziv sheme", + "Schema Statistics": "Statistika sheme", + "Schema annotation": "Anotacija sheme", + "Schema configuration updated successfully for {schema}": "Konfiguracija sheme uspješno ažurirana za {schema}", + "Schema depublished successfully": "Shema uspješno povučena iz objave", + "Schema objects deletion completed successfully": "Brisanje objekata sheme uspješno dovršeno", + "Schema published successfully": "Shema uspješno objavljena", + "Schema reference ($ref)": "Referenca sheme ($ref)", + "Schema successfully created": "Shema uspješno stvorena", + "Schema successfully updated": "Shema uspješno ažurirana", + "Schema {schema} removed successfully": "Shema {schema} uspješno uklonjena", + "Schema:": "Shema:", + "Schemas": "Sheme", + "Scheme": "Shema", + "Search": "Pretraži", + "Search / Views": "Pretraživanje / Prikazi", + "Search / views": "Pretraživanje / prikazi", + "Search Activity": "Aktivnost pretraživanja", + "Search Analytics": "Analitika pretraživanja", + "Search Configuration": "Konfiguracija pretraživanja", + "Search Entities": "Pretraži entitete", + "Search Files": "Pretraži datoteke", + "Search GitHub": "Pretraži GitHub", + "Search GitLab": "Pretraži GitLab", + "Search Method": "Metoda pretraživanja", + "Search Mode": "Način pretraživanja", + "Search Query": "Upit pretraživanja", + "Search Results": "Rezultati pretraživanja", + "Search Term": "Pojam pretraživanja", + "Search Trail Details": "Pojedinosti traga pretraživanja", + "Search Trail Management": "Upravljanje tragom pretraživanja", + "Search Trail Statistics": "Statistika traga pretraživanja", + "Search Trails": "Tragovi pretraživanja", + "Search Views": "Prikazi pretraživanja", + "Search Webhooks": "Pretraži webhookove", + "Search agents...": "Pretraži agente...", + "Search applications...": "Pretraži aplikacije...", + "Search by file name or path": "Pretraži po nazivu datoteke ili putanji", + "Search by name or URL": "Pretraži po nazivu ili URL-u", + "Search by title or UUID...": "Pretraži po naslovu ili UUID...", + "Search by value": "Pretraži po vrijednosti", + "Search configurations": "Pretraži konfiguracije", + "Search data sources...": "Pretraži izvore podataka...", + "Search groups...": "Pretraži grupe...", + "Search in Database Objects": "Pretraži objekte u bazi podataka", + "Search in Files": "Pretraži datoteke", + "Search in Objects": "Pretraži objekte", + "Search objects": "Pretraži objekte", + "Search objects...": "Pretraži objekte...", + "Search property names...": "Pretraži nazive svojstava...", + "Search registers...": "Pretraži registre...", + "Search schemas...": "Pretraži sheme...", + "Search trail deletion not implemented yet": "Brisanje traga pretraživanja još nije implementirano", + "Search views...": "Pretraži prikaze...", + "Search {name}...": "Pretraži {name}...", + "Searching...": "Pretraživanje...", + "Secret": "Tajna", + "Secret key for HMAC signature generation (optional)": "Tajni ključ za generiranje HMAC potpisa (neobavezno)", + "Security": "Sigurnost", + "See organisations as a table": "Prikaži organizacije kao tablicu", + "See organisations as cards": "Prikaži organizacije kao kartice", + "Select AI Agent": "Odaberi AI agenta", + "Select Active Organisation": "Odaberi aktivnu organizaciju", + "Select All": "Odaberi sve", + "Select ConfigSet": "Odaberite ConfigSet", + "Select File Types to Vectorize:": "Odaberite vrste datoteka za vektorizaciju:", + "Select HTTP method": "Odaberite HTTP metodu", + "Select Register and Schema": "Odaberite Registar i Shemu", + "Select Views to Vectorize:": "Odaberite prikaze za vektorizaciju:", + "Select a Nextcloud group to add": "Odaberite Nextcloud grupu za dodavanje", + "Select a branch": "Odaberite granu", + "Select a date property": "Odaberite svojstvo datuma", + "Select a model or type a custom model name": "Odaberite model ili upišite prilagođeni naziv modela", + "Select a register": "Odaberite registar", + "Select a register...": "Odaberite registar...", + "Select a repository": "Odaberite repozitorij", + "Select a repository you have write access to": "Odaberite repozitorij na koji imate pravo pisanja", + "Select a schema": "Odaberite shemu", + "Select a schema...": "Odaberite shemu...", + "Select a user": "Odaberite korisnika", + "Select agent type": "Odaberite vrstu agenta", + "Select all entities": "Odaberite sve entitete", + "Select an AI Agent": "Odaberite AI agenta", + "Select an AI agent to begin chatting with your data.": "Odaberite AI agenta za početak razgovora s vašim podacima.", + "Select an email to see linked objects": "Odaberite e-poštu za prikaz povezanih objekata", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Odaberite organizaciju i korisnika da biste ih dodali kao člana. Pretražite organizacije po nazivu.", + "Select backend": "Odaberite pozadinski sustav", + "Select chat model": "Odaberite model razgovora", + "Select collection for files": "Odaberite kolekciju za datoteke", + "Select collection for objects": "Odaberite kolekciju za objekte", + "Select configuration type...": "Odaberite vrstu konfiguracije...", + "Select default organisation": "Odaberite zadanu organizaciju", + "Select entity {value}": "Odaberite entitet {value}", + "Select event to listen to...": "Odaberite događaj koji će se osluškivati...", + "Select fields to display": "Odaberite polja za prikaz", + "Select files with the correct extension": "Odaberite datoteke s ispravnom ekstenzijom", + "Select group": "Odaberite grupu", + "Select groups (optional)": "Odaberite grupe (neobavezno)", + "Select groups to notify...": "Odaberite grupe za obavještavanje...", + "Select groups with access to this agent": "Odaberite grupe s pristupom ovom agentu", + "Select groups...": "Odaberite grupe...", + "Select model": "Odaberite model", + "Select one or more registers": "Odaberite jedan ili više registara", + "Select one or more schemas": "Odaberite jednu ili više shema", + "Select options...": "Odaberite opcije...", + "Select or create labels, or select \"No label\" to add files": "Odaberite ili stvorite oznake ili odaberite \"Bez oznake\" za dodavanje datoteka", + "Select owner application (optional)...": "Odaberite vlasničku aplikaciju (neobavezno)...", + "Select period": "Odaberite razdoblje", + "Select property to send as payload": "Odaberite svojstvo za slanje kao korisni teret", + "Select provider": "Odaberite pružatelja usluge", + "Select registers and schemas to save a view": "Odaberite registre i sheme za spremanje prikaza", + "Select retry policy": "Odaberite pravilo ponovnog pokušaja", + "Select role": "Odaberite ulogu", + "Select schemas (empty = all schemas)": "Odaberite sheme (prazno = sve sheme)", + "Select schemas to warm up": "Odaberite sheme za zagrijavanje", + "Select search mode": "Odaberite način pretraživanja", + "Select source type...": "Odaberite vrstu izvora...", + "Select target type": "Odaberite ciljnu vrstu", + "Select the branch to publish to": "Odaberite granu za objavljivanje", + "Select the event this webhook should listen to": "Odaberite događaj koji bi ovaj webhook trebao osluškivati", + "Select views to filter data (optional)": "Odaberite prikaze za filtriranje podataka (neobavezno)", + "Select views to limit which data the agent can access": "Odaberite prikaze za ograničavanje podataka kojima agent može pristupiti", + "Select views to vectorize:": "Odaberite prikaze za vektorizaciju:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Odaberite koje su Nextcloud grupe dostupne za ovu organizaciju. Korisnici u tim grupama imat će pristup resursima organizacije.", + "Select which data views the AI can search": "Odaberite koje prikaze podataka AI može pretraživati", + "Select which property from the event should be used as the webhook payload data": "Odaberite koje svojstvo iz događaja treba koristiti kao podatke korisnog tereta webhooka", + "Select which tools the AI can use to perform actions": "Odaberite koje alate AI može koristiti za izvođenje radnji", + "Select {fieldName} values": "Odaberite vrijednosti {fieldName}", + "Selected": "Odabrano", + "Selected Groups": "Odabrane grupe", + "Selected Objects ({count})": "Odabrani objekti ({count})", + "Selected audit trails deleted successfully": "Odabrani revizijski tragovi uspješno su izbrisani", + "Selected search trails deleted successfully": "Odabrani tragovi pretraživanja uspješno su izbrisani", + "Selected users can access this view": "Odabrani korisnici mogu pristupiti ovom prikazu", + "Send additional feedback": "Pošaljite dodatne povratne informacije", + "Send as CloudEvent": "Pošalji kao CloudEvent", + "Send as cloudevent": "Pošalji kao cloudevent", + "Sensitive PII": "Osjetljivi PII", + "Serial Mode (Safer, slower)": "Serijski način rada (sigurniji, sporiji)", + "Serial:": "Serijski:", + "Server Configuration": "Konfiguracija poslužitelja", + "Server Information": "Informacije o poslužitelju", + "Set as Active": "Postavi kao aktivno", + "Set as active organisation": "Postavi kao aktivnu organizaciju", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Postavite ograničenja za korištenje API-ja i potrošnju tokena. Koristite 0 za neograničene resurse.", + "Settings": "Postavke", + "Settings updated successfully": "Postavke su uspješno ažurirane", + "Shards": "Krhotine", + "Share": "Dijeli", + "Share with Groups": "Dijeli s grupama", + "Share with Users": "Dijeli s korisnicima", + "Show": "Prikaži", + "Show Filters": "Prikaži filtre", + "Show less": "Prikaži manje", + "Show only entries with changes": "Prikaži samo unose s promjenama", + "Showing {count} dashboard(s)": "Prikazuje se {count} nadzornih ploča", + "Showing {count} processing activities": "Prikazuje se {count} aktivnosti obrade", + "Showing {showing} of {total} agents": "Prikazuje se {showing} od {total} agenata", + "Showing {showing} of {total} applications": "Prikazuje se {showing} od {total} aplikacija", + "Showing {showing} of {total} audit trail entries": "Prikazuje se {showing} od {total} unosa revizijskog traga", + "Showing {showing} of {total} configurations": "Prikazuje se {showing} od {total} konfiguracija", + "Showing {showing} of {total} deleted items": "Prikazuje se {showing} od {total} izbrisanih stavki", + "Showing {showing} of {total} entities": "Prikazuje se {showing} od {total} entiteta", + "Showing {showing} of {total} files": "Prikazuje se {showing} od {total} datoteka", + "Showing {showing} of {total} log entries": "Prikazuje se {showing} od {total} unosa zapisnika", + "Showing {showing} of {total} search trail entries": "Prikazuje se {showing} od {total} unosa traga pretraživanja", + "Showing {showing} of {total} sources": "Prikazuje se {showing} od {total} izvora", + "Showing {showing} of {total} templates": "Prikazuje se {showing} od {total} predložaka", + "Showing {showing} of {total} webhooks": "Prikazuje se {showing} od {total} webhookova", + "Showing {shown} of {total} organisations": "Prikazuje se {shown} od {total} organizacija", + "Simple": "Jednostavno", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednostavni upiti: osnovna pretraživanja teksta s minimalnim parametrima (npr. jedan pojam za pretraživanje, bez naprednih filtara)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednostavni upiti: osnovna pretraživanja teksta s minimalnim parametrima (npr. jedan pojam za pretraživanje, bez naprednih filtara)", + "Size": "Veličina", + "Slug": "Slug", + "Soft Delete Mode": "Način meke brisanja", + "Soft Deleted Items": "Meko izbrisane stavke", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Meko izbriši sve objekte za ovu shemu ({active} aktivnih, {deleted} već izbrisanih)", + "SolrCloud mode enabled": "SolrCloud način rada omogućen", + "Something went wrong while adding files": "Nešto je pošlo po zlu prilikom dodavanja datoteka", + "Source": "Izvor", + "Source successfully updated": "Izvor je uspješno ažuriran", + "Sources": "Izvori", + "Sources:": "Izvori:", + "Special Rights": "Posebna prava", + "Standalone SOLR mode": "Samostalni SOLR način rada", + "Start Conversation": "Započni razgovor", + "Start Date Field": "Polje datuma početka", + "Start Vectorization": "Pokreni vektorizaciju", + "Start a conversation": "Započni razgovor", + "Starting names cache warmup...": "Pokretanje zagrijavanja predmemorije naziva...", + "Starting validation for {schema}...": "Pokretanje provjere valjanosti za {schema}...", + "Starting {action} for {schema}...": "Pokretanje {action} za {schema}...", + "Starting...": "Pokretanje...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika i mjerni podaci", + "Statistics refreshed": "Statistika je osvježena", + "Stats calculation not yet implemented": "Izračun statistike još nije implementiran", + "Status": "Status", + "Status Code": "Statusni kod", + "Step": "Korak", + "Storage": "Pohrana", + "Storage Quota (MB)": "Kvota pohrane (MB)", + "String Constraints": "Ograničenja niza", + "Subject identifier (email, BSN, name, etc.)": "Identifikator subjekta (e-pošta, BSN, ime itd.)", + "Subscribe": "Pretplati se", + "Subscribed": "Pretplaćeno", + "Success": "Uspjeh", + "Success Rate": "Stopa uspjeha", + "Success Status": "Status uspjeha", + "Successful": "Uspješno", + "Successfully added user to organisation: {name}": "Korisnik je uspješno dodan u organizaciju: {name}", + "Successfully cleared {count} audit trails": "Uspješno je obrisano {count} revizijskih tragova", + "Successfully cleared {count} blob storage objects": "Uspješno je obrisano {count} objekata blob pohrane", + "Successfully cleared {count} search trails": "Uspješno je obrisano {count} tragova pretraživanja", + "Successfully created {count} SOLR fields": "Uspješno je stvoreno {count} SOLR polja", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Uspješno je izbrisano {count} ugrađivanja. Ponovno vektorizirajte svoje podatke.", + "Successfully deleted {count} objects for {schema}": "Uspješno je izbrisano {count} objekata za {schema}", + "Successfully fixed {count} SOLR field configurations": "Uspješno je popravljeno {count} konfiguracija SOLR polja", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Uspješno uvezeno: {registers} registara, {schemas} shema, {objects} objekata", + "Successfully joined organisation": "Uspješno ste se pridružili organizaciji", + "Successfully permanently deleted {count} objects": "Uspješno je trajno izbrisano {count} objekata", + "Successfully processed {count} files!": "Uspješno je obrađeno {count} datoteka!", + "Successfully restored {count} object": "Uspješno je obnovljen {count} objekt", + "Successfully restored {count} objects": "Uspješno je obnovljeno {count} objekata", + "Successfully saved configuration for {count} facets!": "Uspješno je spremljena konfiguracija za {count} faseta!", + "Supported File Types": "Podržane vrste datoteka", + "Switch Active Organisation": "Promijeni aktivnu organizaciju", + "Switch Organisation": "Promijeni organizaciju", + "Sync Table": "Sinkroniziraj tablicu", + "Synchronization Settings": "Postavke sinkronizacije", + "Syncing magic table for {schema}...": "Sinkronizacija magične tablice za {schema}...", + "Syncs": "Sinkronizacije", + "System Default": "Zadano sustavom", + "System Prompt": "Sistemski upit", + "System announcements": "Obavijesti sustava", + "Table": "Tablica", + "Target ID": "Ciljni ID", + "Target Type*": "Ciljna vrsta*", + "Technical Description": "Tehnički opis", + "Technical Details": "Tehnički detalji", + "Technical description for developers and administrators": "Tehnički opis za razvojne programere i administratore", + "Technical measures": "Tehničke mjere", + "Technische maatregelen": "Tehničke mjere", + "Temperature": "Temperatura", + "Templates": "Predlošci", + "Test": "Test", + "Test Chat": "Testni razgovor", + "Test Connection": "Testiraj vezu", + "Test Embedding": "Testiraj ugrađivanje", + "Test endpoint executed successfully": "Testna krajnja točka uspješno je izvršena", + "Test webhook delivered successfully": "Testni webhook uspješno je isporučen", + "Test webhook delivery failed": "Isporuka testnog webhooka nije uspjela", + "Test webhook sent successfully": "Testni webhook uspješno je poslan", + "Testing...": "Testiranje...", + "Text Chunk #{id}": "Tekstualni isječak #{id}", + "Text Extraction": "Izvlačenje teksta", + "Text Extraction Engine": "Mehanizam za izvlačenje teksta", + "Text chunks": "Tekstualni isječci", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Tekstualni isječci generiraju se tijekom izvlačenja datoteka i pohranjuju u bazu podataka. Vektorizacija čita te unaprijed isječene datoteke i pretvara ih u ugrađivanja.", + "Text extracted successfully": "Tekst je uspješno izvučen", + "Text extraction disabled": "Izvlačenje teksta onemogućeno", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplikacija Nextcloud Calendar nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplikacija Nextcloud Contacts nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplikacija Nextcloud Deck nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplikacija Nextcloud Mail nije instalirana ili omogućena na ovom poslužitelju.", + "The URL where webhook events will be sent": "URL na koji će se slati webhook događaji", + "The requested conversation does not exist": "Traženi razgovor ne postoji", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Tražena nadzorna ploča nije se mogla učitati. Provjerite URL ili odaberite drugu nadzornu ploču s popisa.", + "The saved view and all its search configuration": "Spremljeni prikaz i sva njegova konfiguracija pretraživanja", + "There are no audit trail entries matching your current filters.": "Nema unosa revizijskog traga koji odgovaraju vašim trenutnim filtrima.", + "There are no deleted items matching your current filters.": "Nema izbrisanih stavki koje odgovaraju vašim trenutnim filtrima.", + "There are no search trail entries matching your current filters.": "Nema unosa traga pretraživanja koji odgovaraju vašim trenutnim filtrima.", + "There are no webhook log entries matching your filters.": "Nema unosa zapisnika webhooka koji odgovaraju vašim filtrima.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Bit će zadržani u skladu s konfiguriranim rokom čuvanja njihove sheme i automatski trajno izbrisani kada rok čuvanja istekne. Rok čuvanja moguće je konfigurirati po shemi i nalazi se u postavkama sheme.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Bit će zadržani u skladu s konfiguriranim rokom čuvanja njihove sheme, a zatim trajno uklonjeni.", + "This action cannot be undone.": "Ova se radnja ne može poništiti.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Ova se radnja ne može poništiti. Provjerite da nijedna kolekcija ne koristi ovaj ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Ova se radnja ne može poništiti. Provjerite da nijedna kolekcija ne koristi ovaj configset.", + "This action will submit a deactivation request to your administrators.": "Ova će radnja poslati zahtjev za deaktivaciju vašim administratorima.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Ovaj agent nema konfigurabilne prikaze ili alate, ali još uvijek možete prilagoditi RAG postavke u nastavku.", + "This analysis may take some time": "Ova analiza može potrajati", + "This audit trail entry does not contain any change information.": "Ovaj unos revizijskog traga ne sadrži nikakve informacije o promjenama.", + "This configuration has no data defined.": "Ova konfiguracija nema definirane podatke.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ova je krajnja točka zastarjela. Umjesto toga koristite krajnje točke temeljene na isječcima.", + "This entity has no relations to objects or files": "Ovaj entitet nema veza s objektima ili datotekama", + "This is a test webhook from OpenRegister": "Ovo je testni webhook iz OpenRegister", + "This is not valid JSON": "Ovo nije valjani JSON", + "This key already exists on this schema": "Ovaj ključ već postoji u ovoj shemi", + "This schema must use Magic Table configuration to sync": "Ova shema mora koristiti konfiguraciju Magične tablice za sinkronizaciju", + "This schema must use magic table configuration to sync": "Ova shema mora koristiti konfiguraciju magične tablice za sinkronizaciju", + "This source has no associated registers.": "Ovaj izvor nema povezanih registara.", + "This token will only be shown once. Copy it now.": "Ovaj će se token prikazati samo jednom. Kopirajte ga sada.", + "This will copy the _default ConfigSet with the new name": "Ovo će kopirati _default ConfigSet s novim nazivom", + "This will copy the _default configset with the new name": "Ovo će kopirati _default configset s novim nazivom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Ovo će trajno izbrisati SVA ugrađivanja (vektore) iz baze podataka. Morat ćete ponovno vektorizirati sve objekte i datoteke. Ova se radnja ne može poništiti.\n\nJeste li sigurni da želite nastaviti?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Ovo će trajno izbrisati SVA ugrađivanja (vektore) iz baze podataka. Morat ćete ponovno vektorizirati sve objekte i datoteke. Ova se radnja ne može poništiti.\\n\\nJeste li sigurni da želite nastaviti?", + "This will permanently delete:": "Ovo će trajno izbrisati:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Ovo će trajno ukloniti {count} već meko izbrisanih objekata iz baze podataka. Ova se radnja ne može poništiti.", + "Timeout (seconds)": "Vremensko ograničenje (sekunde)", + "Timestamp": "Vremenska oznaka", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Savjet: Omogućite samo prikaze kojima je potrebno semantičko pretraživanje kako biste minimizirali troškove ugrađivanja. Jednostavne tablice za pretraživanje rijetko trebaju vektorizaciju.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Savjet: omogućite samo prikaze kojima je potrebno semantičko pretraživanje kako biste minimizirali troškove ugrađivanja. Jednostavne tablice za pretraživanje rijetko trebaju vektorizaciju.", + "Title": "Naslov", + "Title *": "Naslov *", + "Title Template": "Predložak naslova", + "Title*": "Naslov*", + "To (e.g. -1 year or 2025-12-31)": "Do (npr. -1 godina ili 2025-12-31)", + "To Date": "Do datuma", + "To add files larger than or equal to 512MB, go to the": "Za dodavanje datoteka većih ili jednakih 512 MB, idite na", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Za prilagodbu veličine isječka ili strategije, idite na Konfiguracija datoteke → Ograničenja obrade.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Za prilagodbu veličine isječka ili strategije, idite na konfiguracija datoteke → ograničenja obrade.", + "To change the embedding provider or model, go to LLM Configuration.": "Za promjenu pružatelja usluge ugrađivanja ili modela, idite na LLM konfiguraciju.", + "To change the embedding provider or model, go to LLM configuration.": "Za promjenu pružatelja usluge ugrađivanja ili modela, idite na LLM konfiguraciju.", + "To date": "Do datuma", + "Toggle search sidebar": "Uključi/isključi bočnu traku pretraživanja", + "Token Created": "Token stvoren", + "Token Quota (per request)": "Kvota tokena (po zahtjevu)", + "Token copied to clipboard": "Token kopiran u međuspremnik", + "Token name": "Naziv tokena", + "Token revoked": "Token opozvan", + "Tools": "Alati", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Alati se izvršavaju sa zadanim korisničkim ovlastima agenta kada nijedna korisnička sesija nije aktivna (npr. cron poslovi). Konfigurirajte zadanog korisnika na kartici Postavke.", + "Tools respect the agent's own role and organization boundaries.": "Alati poštuju vlastitu ulogu agenta i granice organizacije.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Alati poštuju prikaze, ovlasti i granice organizacije agenta.", + "Top Deleters": "Najčešći brisači", + "Total": "Ukupno", + "Total Audit Trails": "Ukupno revizijskih tragova", + "Total Chunks Available:": "Ukupno dostupnih isječaka:", + "Total Deleted Items": "Ukupno izbrisanih stavki", + "Total Members": "Ukupno članova", + "Total Objects": "Ukupno objekata", + "Total Objects in Database:": "Ukupno objekata u bazi podataka:", + "Total Organisations": "Ukupno organizacija", + "Total Results": "Ukupno rezultata", + "Total Searches": "Ukupno pretraživanja", + "Total Size": "Ukupna veličina", + "Total Storage": "Ukupna pohrana", + "Total events": "Ukupno događaja", + "Total:": "Ukupno:", + "Totals": "Ukupni iznosi", + "Try searching by UUID or with different keywords": "Pokušajte pretraživati po UUID-u ili s drugačijim ključnim riječima", + "Type": "Vrsta", + "Type \"DELETE COLLECTION\" to confirm:": "Upišite \"DELETE COLLECTION\" za potvrdu:", + "Type Issue": "Vrsta problema", + "Type Variations:": "Varijacije vrste:", + "Type filter (optional, e.g. email)": "Filtar vrste (neobavezno, npr. e-pošta)", + "Type must be \"positive\" or \"negative\"": "Vrsta mora biti \"positive\" ili \"negative\"", + "Type to search for groups": "Upišite za pretraživanje grupa", + "Type to search for objects...": "Upišite za pretraživanje objekata...", + "Type to search for organisations": "Upišite za pretraživanje organizacija", + "Type to search for users": "Upišite za pretraživanje korisnika", + "Type to search groups...": "Upišite za pretraživanje grupa...", + "Type to search users...": "Upišite za pretraživanje korisnika...", + "Type to search...": "Upišite za pretraživanje...", + "Type your username to confirm": "Upišite svoje korisničko ime za potvrdu", + "URL": "URL", + "URL where Ollama is running": "URL na kojem se izvodi Ollama", + "UUID copied to clipboard": "UUID kopiran u međuspremnik", + "UUID:": "UUID:", + "Unchanged": "Nepromijenjeno", + "Unique Search Terms": "Jedinstveni pojmovi za pretraživanje", + "Unique Terms": "Jedinstveni pojmovi", + "Unknown": "Nepoznato", + "Unknown Object": "Nepoznati objekt", + "Unknown Register": "Nepoznati registar", + "Unknown Schema": "Nepoznata shema", + "Unknown error": "Nepoznata pogreška", + "Unknown widget type: {type}": "Nepoznata vrsta widgeta: {type}", + "Unlink email": "Odveži e-poštu", + "Unlink event": "Odveži događaj", + "Unnamed Object": "Neimenovani objekt", + "Untitled": "Bez naslova", + "Untitled View": "Prikaz bez naslova", + "Update": "Ažuriraj", + "Update Operations": "Operacije ažuriranja", + "Update available: {local} → {remote}": "Ažuriranje dostupno: {local} → {remote}", + "Update configuration: ...": "Ažuriraj konfiguraciju: ...", + "Update register OAS: ...": "Ažuriraj OAS registra: ...", + "Update vectors when object data changes (recommended for accurate search)": "Ažuriraj vektore kada se podaci objekta promijene (preporučeno za točno pretraživanje)", + "Updated": "Ažurirano", + "Updated At": "Ažurirano u", + "Updated:": "Ažurirano:", + "Upload new avatar": "Prenesi novi avatar", + "Uploading configuration sets...": "Prenošenje skupova konfiguracije...", + "Uptime:": "Vrijeme rada:", + "Url": "Url", + "Usage Count": "Broj korištenja", + "Use AI agents for processing and analysis": "Koristite AI agente za obradu i analizu", + "Use Large Language Model features": "Koristite značajke velikog jezičnog modela", + "Use SolrCloud with Zookeeper for distributed search": "Koristite SolrCloud sa Zookeeperom za distribuirano pretraživanje", + "Use SolrCloud with zookeeper for distributed search": "Koristite SolrCloud sa zookeeperom za distribuirano pretraživanje", + "Use VNG APIs instead of MongoDB.": "Koristite VNG API-je umjesto MongoDB-a.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Koristite zadane raspone (posljednjih 7/30/90 dana, prošla godina, starije)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Koristite vanjsku pohranu (npr. MongoDB) umjesto interne Nextcloud pohrane.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Koristite filtre za sužavanje unosa revizijskog traga prema registru, shemi, vrsti radnje, korisniku, rasponu datuma ili ID-u objekta.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Koristite filtre za sužavanje izbrisanih stavki prema registru, shemi, datumu brisanja ili korisniku koji ih je izbrisao.", + "Use filters to narrow down entities by type or category.": "Koristite filtre za sužavanje entiteta prema vrsti ili kategoriji.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Koristite filtre za sužavanje unosa traga pretraživanja prema registru, shemi, statusu uspjeha, korisniku, rasponu datuma, pojmovima za pretraživanje ili mjernim podacima o izvedbi.", + "Used By Collections": "Korišteno od strane kolekcija", + "Used by": "Korišteno od strane", + "User": "Korisnik", + "User Agent Statistics": "Statistika korisničkog agenta", + "User Description": "Korisnički opis", + "User Quota": "Korisnička kvota", + "User rate limits cleared successfully": "Ograničenja brzine korisnika uspješno su obrisana", + "User removed successfully": "Korisnik je uspješno uklonjen", + "User-friendly description shown in forms and help text": "Korisniku prilagođen opis prikazan u obrascima i tekstu pomoći", + "Username": "Korisničko ime", + "Username for SOLR authentication (optional)": "Korisničko ime za SOLR autentifikaciju (neobavezno)", + "Users": "Korisnici", + "Uses": "Korištenja", + "Using Pre-Generated Chunks": "Korištenje unaprijed generiranih isječaka", + "Validate": "Provjeri valjanost", + "Validate Objects": "Provjeri valjanost objekata", + "Validation completed for {schema}": "Provjera valjanosti dovršena za {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Provjera valjanosti dovršena za {schema}: {processed} obrađeno, {updated} ažurirano, {failed} neuspješno", + "Validation completed successfully": "Provjera valjanosti uspješno je dovršena", + "Validation failed": "Provjera valjanosti nije uspjela", + "Value": "Vrijednost", + "Value list (split on ,)": "Popis vrijednosti (razdvojeno s ,)", + "Vector Dimensions": "Dimenzije vektora", + "Vector Search Backend": "Pozadinski sustav za vektorsko pretraživanje", + "Vector field: _embedding_": "Vektorsko polje: _embedding_", + "Vectorization Triggers": "Okidači vektorizacije", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorizacija dovršena: {vectorized} objekata vektorizirano, {failed} neuspješno", + "Vectorization failed: {error}": "Vektorizacija nije uspjela: {error}", + "Vectorize All Files": "Vektoriziraj sve datoteke", + "Vectorize All Objects": "Vektoriziraj sve objekte", + "Vectorize all file types": "Vektoriziraj sve vrste datoteka", + "Vectorize all views": "Vektoriziraj sve prikaze", + "Vectorize on object creation": "Vektoriziraj pri stvaranju objekta", + "Vectors will be stored in your existing object and file collections": "Vektori će biti pohranjeni u vašim postojećim kolekcijama objekata i datoteka", + "Verantwoording": "Obrazloženje", + "Verifying server connectivity...": "Provjera povezivosti poslužitelja...", + "Version": "Verzija", + "Version Information": "Informacije o verziji", + "Very High": "Vrlo visoko", + "Very high": "Vrlo visoko", + "View": "Prikaz", + "View API Docs": "Pregledaj API dokumentaciju", + "View API Documentation": "Pregledaj API dokumentaciju", + "View APIs": "Pregledaj API-je", + "View Changes": "Pregledaj promjene", + "View Details": "Pregledaj pojedinosti", + "View Error": "Pregledaj pogrešku", + "View File": "Pregledaj datoteku", + "View Full Details": "Pregledaj sve pojedinosti", + "View Logs": "Pregledaj zapisnike", + "View Name": "Naziv prikaza", + "View Object": "Pregledaj objekt", + "View Parameters": "Pregledaj parametre", + "View Selection": "Odabir prikaza", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Pregledajte i analizirajte zapisnike traga pretraživanja s naprednim mogućnostima filtriranja i analitike", + "View and analyze system audit trails with advanced filtering capabilities": "Pregledajte i analizirajte revizijske tragove sustava s naprednim mogućnostima filtriranja", + "View and manage authorization across registers and schemas": "Pregledajte i upravljajte ovlaštenjima u registrima i shemama", + "View deleted successfully": "Prikaz je uspješno izbrisan", + "View entity details and manage relations": "Pregledajte pojedinosti entiteta i upravljajte vezama", + "View name": "Naziv prikaza", + "View name is required": "Naziv prikaza je obavezan", + "View object": "Pregledaj objekt", + "View saved successfully!": "Prikaz je uspješno spremljen!", + "View search analytics and manage search logs": "Pregledajte analitiku pretraživanja i upravljajte zapisnicima pretraživanja", + "View successfully deleted": "Prikaz je uspješno izbrisan", + "View successfully updated": "Prikaz je uspješno ažuriran", + "View updated successfully!": "Prikaz je uspješno ažuriran!", + "View webhook delivery logs and filter by webhook": "Pregledajte zapisnike isporuke webhooka i filtrirajte po webhooku", + "View {count} more": "Pregledaj još {count}", + "Views": "Prikazi", + "Visible to end users": "Vidljivo krajnjim korisnicima", + "Visible to users": "Vidljivo korisnicima", + "Wait for Response": "Čekaj odgovor", + "Wait for webhook response before continuing (required for request/response flows)": "Čekaj odgovor webhooka prije nastavka (obavezno za tokove zahtjev/odgovor)", + "Waiting for cluster synchronization...": "Čekanje sinkronizacije klastera...", + "Warmup Names Cache": "Zagrij predmemoriju naziva", + "Warmup failed": "Zagrijavanje nije uspjelo", + "Warning": "Upozorenje", + "Warning:": "Upozorenje:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Upozorenje: Ovo svojstvo ima nedosljedne vrste: {types}. Razmislite trebaju li svi objekti imati istu vrstu.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Pregledat ćemo sve objekte koji pripadaju ovoj shemi kako bismo otkrili nova svojstva i analizirali postojeća svojstva za potencijalna poboljšanja. Proces uključuje ispitivanje strukture podataka svakog objekta, prepoznavanje svojstava koja nisu definirana u trenutnoj shemi i pronalaženje prilika za poboljšanje postojećih definicija svojstava boljim ograničenjima, formatima i pravilima provjere valjanosti.", + "Webhook": "Webhook", + "Webhook Log Details": "Pojedinosti zapisnika webhooka", + "Webhook Logs": "Zapisnici webhooka", + "Webhook created successfully": "Webhook je uspješno stvoren", + "Webhook deleted": "Webhook izbrisan", + "Webhook retry delivered successfully": "Ponovni pokušaj webhooka uspješno je isporučen", + "Webhook retry delivery failed": "Isporuka ponovnog pokušaja webhooka nije uspjela", + "Webhook updated": "Webhook ažuriran", + "Webhook updated successfully": "Webhook je uspješno ažuriran", + "Webhooks": "Webhookovi", + "Weekly": "Tjedno", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kada se agent izvodi bez korisničke sesije (npr. zakazani zadaci), koristit će se kontekst ovog korisnika", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kada se agent izvodi bez korisničke sesije (npr. zakazani zadaci), koristit će se kontekst ovog korisnika.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Kada je onemogućeno, ova će faseta prikazivati samo vrijednosti iz ove sheme i uključivat će filtar sheme kada je odabrana.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Kada je omogućeno, spremanje ovog objekta također će ažurirati ciljne objekte kako bi uključili referencu natrag na ovaj objekt.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Kada je omogućeno, ovo će se svojstvo ukloniti iz izvornog objekta nakon ažuriranja ciljnih objekata.", + "Workflow Management": "Upravljanje током rada", + "Workflows": "Tijekovi rada", + "Wrap webhook payload in CloudEvents format for better interoperability": "Omotaj korisni teret webhooka u CloudEvents format za bolju interoperabilnost", + "Wrap webhook payload in cloudevents format for better interoperability": "Omotaj korisni teret webhooka u cloudevents format za bolju interoperabilnost", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Da", + "You": "Vi", + "You are not yet a member of any organisations.": "Još niste član nijedne organizacije.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Možete stvoriti nove ConfigSetove na temelju _default predloška ili izravno prenijeti prilagođene na svoj SOLR poslužitelj.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Možete stvoriti nove configsetove na temelju _default predloška ili izravno prenijeti prilagođene na svoj SOLR poslužitelj.", + "You do not have access to this conversation": "Nemate pristup ovom razgovoru", + "You do not have permission to delete this conversation": "Nemate ovlasti za brisanje ovog razgovora", + "You do not have permission to modify this conversation": "Nemate ovlasti za izmjenu ovog razgovora", + "You do not have permission to restore this conversation": "Nemate ovlasti za obnovu ovog razgovora", + "You do not have permission to view the permission matrix. Admin access is required.": "Nemate ovlasti za pregled matrice ovlasti. Potreban je administratorski pristup.", + "You have selected {count} properties to add to the schema.": "Odabrali ste {count} svojstava za dodavanje u shemu.", + "You must be logged in to favorite views": "Morate biti prijavljeni da biste prikaze označili kao omiljene", + "You need an AI agent to start a conversation.": "Potreban vam je AI agent za početak razgovora.", + "Your Fireworks AI API key. Get one at": "Vaš Fireworks AI API ključ. Nabavite ga na", + "Your OpenAI API key. Get one at": "Vaš OpenAI API ključ. Nabavite ga na", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Vaše povratne informacije su zabilježene. Po želji ovdje možete navesti dodatne pojedinosti...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Vaše povratne informacije su zabilježene. po želji ovdje možete navesti dodatne pojedinosti...", + "Zookeeper Hosts": "Zookeeper poslužitelji", + "Zookeeper Port": "Zookeeper priključak", + "Zookeeper Settings (SolrCloud)": "Zookeeper postavke (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper niz za povezivanje za SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Broj Zookeeper priključka (neobavezno, zadano 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORENJE O TRAJNOM BRISANJU \\u26A0\\uFE0F\n\nUpravo ćete TRAJNO izbrisati SVE objekte za shemu \"{schema}\":\n\n\\u2022 Aktivni objekti: {active}\n\\u2022 Meko izbrisani objekti: {deleted}\n\\u2022 Ukupno: {total}\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORENJE O TRAJNOM BRISANJU \\u26A0\\uFE0F\n\nUpravo ćete TRAJNO izbrisati {count} meko izbrisanih objekata za shemu \"{schema}\".\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "and accessible to all users in selected groups (or all users if no groups selected).": "i dostupno svim korisnicima u odabranim grupama (ili svim korisnicima ako nije odabrana nijedna grupa).", + "and add the files there.": "i ondje dodajte datoteke.", + "approved": "odobreno", + "are only accessible to invited users.": "dostupni su samo pozvanim korisnicima.", + "by": "od strane", + "chunk": "isječak", + "chunks": "isječci", + "chunks processed": "isječaka obrađeno", + "completed": "dovršeno", + "configuration(s)": "konfiguracija", + "conversationId is required": "conversationId je obavezan", + "create": "stvori", + "delete": "izbriši", + "deleted objects section": "odjeljak izbrisanih objekata", + "e.g., lib/Settings/config.json": "npr. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "npr. lib/Settings/register.json", + "e.g., lib/settings/config.json": "npr. lib/settings/config.json", + "e.g., lib/settings/register.json": "npr. lib/settings/register.json", + "execute": "izvrši", + "failed": "neuspješno", + "file": "datoteka", + "files": "datoteke", + "file{plural}": "datotek{plural}", + "folder": "mapa", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "u {register}", + "in_progress": "u tijeku", + "inherited": "naslijeđeno", + "list": "popis", + "log{plural}": "zapisni{plural}", + "manage": "upravljaj", + "members": "članovi", + "menu or contact someone with permission to create agents.": "izborniku ili se obratite nekome tko ima ovlasti za stvaranje agenata.", + "message content is required": "sadržaj poruke je obavezan", + "messages": "poruke", + "missing": "nedostaje", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL i API ključ su obavezni", + "n8n configuration saved successfully": "n8n konfiguracija uspješno je spremljena", + "n8n connection not configured": "n8n veza nije konfigurirana", + "n8n connection successful": "n8n veza uspješna", + "n8n connection test failed: {message}": "n8n test veze nije uspio: {message}", + "n8n connection test successful": "n8n test veze uspješan", + "n8n initialization failed: {message}": "n8n inicijalizacija nije uspjela: {message}", + "n8n integration disabled": "n8n integracija onemogućena", + "n8n integration enabled": "n8n integracija omogućena", + "n8n project initialized successfully": "n8n projekt uspješno je inicijaliziran", + "n8n settings saved successfully": "n8n postavke uspješno su spremljene", + "notify_push configuration guide": "vodič za konfiguraciju notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push je instaliran, ali OpenRegister još nije potvrdio uspješno guranje. Pokrenite spremanje objekta za aktivaciju ili provjerite svoju notify_push konfiguraciju.", + "notify_push is installed but not yet active": "notify_push je instaliran, ali još nije aktivan", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekti", + "objects processed": "objekata obrađeno", + "object{plural}": "objek{plural}", + "or pick a range": "ili odaberite raspon", + "org-...": "org-...", + "pending": "na čekanju", + "provides a good balance between speed and accuracy.": "pruža dobru ravnotežu između brzine i točnosti.", + "public": "javno", + "read": "čitaj", + "register(s)": "registar/registri", + "register(s) selected": "registar/registri odabran", + "register{plural}": "registar{plural}", + "rejected": "odbijeno", + "required": "obavezno", + "results": "rezultati", + "schema(s)": "shema/sheme", + "schema(s) selected": "shema/sheme odabrana", + "schema{plural}": "shem{plural}", + "selected objects": "odabrani objekti", + "set": "postavi", + "sk-...": "sk-...", + "this application": "ova aplikacija", + "to": "do", + "type must be \"positive\" or \"negative\"": "vrsta mora biti \"positive\" ili \"negative\"", + "update": "ažuriraj", + "vectorized": "vektorizirano", + "view": "prikaz", + "views selected": "prikaza odabrano", + "waiting": "čeka se", + "{active} active, {deleted} deleted": "{active} aktivnih, {deleted} izbrisanih", + "{count} audit trails cleared successfully": "{count} revizijskih tragova uspješno obrisano", + "{count} compliance issue(s) detected": "Otkriveno {count} problema usklađenosti", + "{count} deletions": "{count} brisanja", + "{count} documents found": "Pronađeno {count} dokumenata", + "{count} email": "{count} e-pošta", + "{count} emails": "{count} e-pošta", + "{count} entries": "{count} unosa", + "{count} issues": "{count} problema", + "{count} members": "{count} članova", + "{count} searches": "{count} pretraživanja", + "{count} selected": "{count} odabrano", + "{count} widget(s)": "{count} widgeta", + "{matched} object(s) matched. {erased} erased.": "{matched} objekata podudara se. {erased} izbrisano.", + "{minutes} minutes ago": "prije {minutes} minuta", + "{percentage}% of objects": "{percentage}% objekata", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} u {register} / {schema}", + "{type} cache cleared successfully": "{type} predmemorija uspješno obrisana", + "ℹ️ Current Configuration": "ℹ️ Trenutna konfiguracija", + "ℹ️ current configuration": "ℹ️ trenutna konfiguracija", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ OPASNOST: Svih {total} objekata bit će TRAJNO IZBRISANO iz baze podataka. Ova je radnja NEPOVRATNA i ne može se poništiti!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORENJE O TRAJNOM BRISANJU ⚠️\n\nUpravo ćete TRAJNO izbrisati SVE objekte za shemu \"{schema}\":\n\n• Aktivni objekti: {active}\n• Meko izbrisani objekti: {deleted}\n• Ukupno: {total}\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORENJE O TRAJNOM BRISANJU ⚠️\n\nUpravo ćete TRAJNO izbrisati {count} meko izbrisanih objekata za shemu \"{schema}\".\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "⚡ Batch Processing": "⚡ Skupna obrada", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR može obraditi milijune dokumenata s vremenom odziva pretraživanja kraćim od sekunde...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Sveobuhvatan kontekst ✓ Manja vjerojatnost izostavljanja pojedinosti ✗ Sporiji odgovori ✗ Može uključivati manje relevantne informacije", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Brži odgovori ✓ Usredotočeniji odgovori ✗ Može izostaviti relevantne informacije", + "✨ AI Features": "✨ AI značajke", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 U distribuiranom načinu rada, SOLR koristi ZooKeeper za koordinaciju klastera...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasetno pretraživanje omogućuje korisnicima dublje istraživanje rezultata po kategorijama...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Jeste li znali? SOLR pokreće pretraživanje za Netflix, Apple i mnoge druge velike stranice!", + "💰 View Selection (Cost Optimization)": "💰 Odabir prikaza (optimizacija troškova)", + "📄 Object Serialization": "📄 Serijalizacija objekta", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR pruža bogatu analitiku i statistiku o izvedbi pretraživanja...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSetovi sadrže shemu i konfiguracijske datoteke za vaš indeks pretraživanja...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikacija osigurava dostupnost vašeg indeksa pretraživanja čak i ako čvorovi zakažu...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR je moćna platforma za pretraživanje na razini poduzeća izgrađena na Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Postavke vektorizacije", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automatsko skaliranje može dinamički dodavati ili uklanjati čvorove na temelju opterećenja...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Sigurnosne značajke uključuju autentifikaciju, autorizaciju i SSL enkripciju...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-pošta", + "{count} e-pošte" + ], + "Purge {count} object from database": [ + "Trajno ukloni {count} objekt iz baze podataka", + "Trajno ukloni {count} objekata iz baze podataka" + ], + "Restore {count} object": [ + "Vrati {count} objekt", + "Vrati {count} objekata" + ], + "Successfully restored {count} object": [ + "Uspješno vraćen {count} objekt", + "Uspješno vraćeno {count} objekata" + ], + "Delete {count} object": [ + "Izbriši {count} objekt", + "Izbriši {count} objekata" + ], + "Object successfully deleted": [ + "Objekt je uspješno izbrisan", + "Objekti su uspješno izbrisani" + ] + } +} diff --git a/l10n/ca.js b/l10n/ca.js new file mode 100644 index 0000000000..9f23dd3d4d --- /dev/null +++ b/l10n/ca.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(sense assumpte)", + "(no title)" : "(sense títol)", + "(unnamed)" : "(sense nom)", + "(untitled card)" : "(targeta sense títol)", + "Activities" : "Activitats", + "Add Endpoint" : "Afegeix un punt final", + "Add Source" : "Afegeix una font", + "Add card" : "Afegeix una targeta", + "Add contact" : "Afegeix un contacte", + "All clear" : "Tot correcte", + "Articles" : "Articles", + "Code" : "Codi", + "Compliance" : "Compliment", + "Contacts" : "Contactes", + "Create Agent" : "Crea un agent", + "Create Application" : "Crea una aplicació", + "Create event" : "Crea un esdeveniment", + "Erasure complete" : "Esborrat complet", + "Erasure preview" : "Vista prèvia de l'esborrat", + "Expand details" : "Amplia els detalls", + "Save changes" : "Desa els canvis", + "📄 Object Serialization" : "📄 Serialització d'objectes", + "🔢 Vectorization Settings" : "🔢 Configuració de la vectorització", + "💰 View Selection (Cost Optimization)" : "💰 Selecció de vistes (optimització de costos)", + "AI Assistant" : "Assistent d'IA", + "AI Chat" : "Xat d'IA", + "API Key" : "Clau de l'API", + "API tokens saved successfully" : "Els tokens de l'API s'han desat correctament", + "About ConfigSets" : "Quant als ConfigSets", + "Action" : "Acció", + "Action Distribution" : "Distribució d'accions", + "Action:" : "Acció:", + "Actions" : "Accions", + "Active" : "Actiu", + "Active Collections" : "Col·leccions actives", + "Active Filters" : "Filtres actius", + "Active Organisations" : "Organitzacions actives", + "Active filters:" : "Filtres actius:", + "Active organisation set successfully" : "L'organització activa s'ha establert correctament", + "Activity Period" : "Període d'activitat", + "Add Groups" : "Afegeix grups", + "Add Schema" : "Afegeix un esquema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Afegiu títols d'esquema, descripcions i informació del registre per proporcionar un context més ric per a la cerca", + "Add to favorites" : "Afegeix als preferits", + "Added" : "Afegit", + "Added to favorites" : "Afegit als preferits", + "Additional Information" : "Informació addicional", + "Additional feedback saved. Thank you!" : "S'han desat els comentaris addicionals. Gràcies!", + "Admin" : "Administrador", + "Advanced" : "Avançat", + "Advanced Filters" : "Filtres avançats", + "Advanced Options" : "Opcions avançades", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Els filtres avançats no estan disponibles quan s'utilitza la font de base de dades. Canvieu a Automàtic o a l'índex SOLR per a les opcions de filtratge.", + "After" : "Després", + "Agent deleted successfully" : "L'agent s'ha suprimit correctament", + "Agents" : "Agents", + "All" : "Tot", + "All Categories" : "Totes les categories", + "All Collections" : "Totes les col·leccions", + "All Confidence Levels" : "Tots els nivells de confiança", + "All ConfigSets" : "Tots els ConfigSets", + "All Files" : "Tots els fitxers", + "All Levels" : "Tots els nivells", + "All Types" : "Tots els tipus", + "All Webhooks" : "Tots els webhooks", + "All actions" : "Totes les accions", + "All audit trails cleared successfully" : "Tots els rastres d'auditoria s'han esborrat correctament", + "All registers" : "Tots els registres", + "All schemas" : "Tots els esquemes", + "All searches" : "Totes les cerques", + "All users" : "Tots els usuaris", + "An error occurred while clearing audit trails" : "S'ha produït un error en esborrar els rastres d'auditoria", + "An error occurred while deleting the audit trail" : "S'ha produït un error en suprimir el rastre d'auditoria", + "An error occurred while deleting the view" : "S'ha produït un error en suprimir la vista", + "An error occurred while permanently deleting the objects" : "S'ha produït un error en suprimir permanentment els objectes", + "Analysis completed:" : "Anàlisi completada:", + "Analysis steps:" : "Passos de l'anàlisi:", + "Analytics" : "Analítiques", + "Analyze Objects" : "Analitza els objectes", + "Analyze Properties" : "Analitza les propietats", + "Analyze existing properties for improvement opportunities" : "Analitza les propietats existents per a oportunitats de millora", + "Analyzing..." : "S'està analitzant...", + "Any favorites and sharing settings for this view" : "Qualsevol configuració de preferits i de compartició per a aquesta vista", + "Any user" : "Qualsevol usuari", + "App store cache invalidated successfully" : "La memòria cau de la botiga d'aplicacions s'ha invalidat correctament", + "Application deleted successfully" : "L'aplicació s'ha suprimit correctament", + "Applications" : "Aplicacions", + "Apply Changes" : "Aplica els canvis", + "Archive" : "Arxiva", + "Archive conversation" : "Arxiva la conversa", + "Archived conversations are hidden from your active list" : "Les converses arxivades s'oculten de la vostra llista activa", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Esteu segur que voleu netejar els rastres de cerca antics? Això suprimirà les entrades de més de 30 dies.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Esteu segur que voleu suprimir els rastres d'auditoria seleccionats? Aquesta acció no es pot desfer.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Esteu segur que voleu suprimir els rastres de cerca seleccionats? Aquesta acció no es pot desfer.", + "Are you sure you want to delete this ConfigSet?" : "Esteu segur que voleu suprimir aquest ConfigSet?", + "Are you sure you want to permanently delete" : "Esteu segur que voleu suprimir permanentment", + "Ask a question..." : "Feu una pregunta...", + "Ask questions about your data using natural language" : "Feu preguntes sobre les vostres dades utilitzant llenguatge natural", + "Attempt" : "Intent", + "Audit Trail Changes" : "Canvis del rastre d'auditoria", + "Audit Trail Details" : "Detalls del rastre d'auditoria", + "Audit Trail Management" : "Gestió del rastre d'auditoria", + "Audit Trail Statistics" : "Estadístiques del rastre d'auditoria", + "Audit Trails" : "Rastres d'auditoria", + "Audit trail data copied to clipboard" : "Les dades del rastre d'auditoria s'han copiat al porta-retalls", + "Audit trail deleted successfully" : "El rastre d'auditoria s'ha suprimit correctament", + "Audit trail successfully deleted" : "El rastre d'auditoria s'ha suprimit correctament", + "Authentication" : "Autenticació", + "Auto-commit disabled" : "Confirmació automàtica desactivada", + "Auto-commit enabled" : "Confirmació automàtica activada", + "Auto-create Default Organisation" : "Crea automàticament l'organització per defecte", + "Auto-retry failed vectorizations" : "Reintenta automàticament les vectoritzacions fallides", + "Automatically commit changes to SOLR index" : "Confirma automàticament els canvis a l'índex SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Crea automàticament una organització per defecte si no n'existeix cap quan s'inicialitza l'aplicació", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Genera automàticament incrustacions vectorials a partir de fragments de text quan els fitxers es pugen i es processen", + "Automatically generate vector embeddings when objects are created or updated" : "Genera automàticament incrustacions vectorials quan es creen o s'actualitzen els objectes", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Reintenta automàticament els intents de vectorització fallits (màxim 3 reintents)", + "Available Filters" : "Filtres disponibles", + "Available Workflows" : "Fluxos de treball disponibles", + "Avg Execution Time" : "Temps d'execució mitjà", + "Avg Members/Org" : "Mitjana de membres/org.", + "Avg Object Views/Session" : "Mitjana de visualitzacions d'objectes/sessió", + "Avg Response Time" : "Temps de resposta mitjà", + "Avg Results/Search" : "Mitjana de resultats/cerca", + "Avg Searches/Session" : "Mitjana de cerques/sessió", + "Back" : "Enrere", + "Back to Entities" : "Torna a les entitats", + "Back to Registers" : "Torna als registres", + "Back to Webhooks" : "Torna als webhooks", + "Back to applications" : "Torna a les aplicacions", + "Back to entities" : "Torna a les entitats", + "Backend updated successfully. Please reload the application." : "El backend s'ha actualitzat correctament. Torneu a carregar l'aplicació.", + "Base URL (Optional)" : "URL base (opcional)", + "Basic Information" : "Informació bàsica", + "Batch Size" : "Mida del lot", + "Batch extraction completed" : "L'extracció per lots s'ha completat", + "Before" : "Abans", + "Before object vectorization can work:" : "Abans que la vectorització d'objectes pugui funcionar:", + "Behavior Issue" : "Problema de comportament", + "Bookmarks" : "Adreces d'interès", + "Blob storage has been retired. All objects now use magic tables." : "L'emmagatzematge de blobs s'ha retirat. Tots els objectes utilitzen ara taules màgiques.", + "Branch" : "Branca", + "Bulk delete operation completed successfully" : "L'operació de supressió massiva s'ha completat correctament", + "Bulk save operation completed successfully" : "L'operació de desament massiu s'ha completat correctament", + "Business Data" : "Dades empresarials", + "Cache cleared successfully" : "La memòria cau s'ha esborrat correctament", + "Calculate Sizes" : "Calcula les mides", + "Cancel" : "Cancel·la", + "Cannot delete: objects are still attached" : "No es pot suprimir: encara hi ha objectes adjunts", + "Category" : "Categoria", + "Change Type" : "Tipus de canvi", + "Changes" : "Canvis", + "Changes copied to clipboard" : "Els canvis s'han copiat al porta-retalls", + "Chat Model" : "Model de xat", + "Chat Provider" : "Proveïdor de xat", + "Chat Provider (RAG)" : "Proveïdor de xat (RAG)", + "Chat Settings" : "Configuració del xat", + "Chat provider connection successful!" : "Connexió amb el proveïdor de xat correcta!", + "Chat settings" : "Configuració del xat", + "Choose a register" : "Trieu un registre", + "Choose a schema" : "Trieu un esquema", + "Choose how vector similarity calculations are performed for semantic search" : "Trieu com es fan els càlculs de similitud vectorial per a la cerca semàntica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Trieu quins tipus de fitxer s'inclouen en el procés de vectorització. Només es processaran els fitxers amb text extret i fragments.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Trieu quines vistes s'inclouen en el procés de vectorització. Deixeu-ho buit per processar totes les vistes segons la vostra configuració.", + "Chunk" : "Fragment", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "La supressió de fragments encara no està implementada. Utilitzeu els punts finals basats en fragments.", + "Chunks" : "Fragments", + "Chunks to Vectorize:" : "Fragments a vectoritzar:", + "Cleanup Old Trails" : "Neteja els rastres antics", + "Cleanup completed" : "La neteja s'ha completat", + "Clear All Embeddings" : "Esborra totes les incrustacions", + "Clear Entries" : "Esborra les entrades", + "Clear Filtered Audit Trails" : "Esborra els rastres d'auditoria filtrats", + "Clear Filters" : "Esborra els filtres", + "Clear Index" : "Esborra l'índex", + "Clear Selection" : "Esborra la selecció", + "Clear all" : "Esborra-ho tot", + "Clear all filters" : "Esborra tots els filtres", + "Clear date range" : "Esborra l'interval de dates", + "Clearing..." : "S'està esborrant...", + "Close" : "Tanca", + "Collection Management" : "Gestió de col·leccions", + "Collection Name" : "Nom de la col·lecció", + "Collection assignments updated successfully" : "Les assignacions de col·lecció s'han actualitzat correctament", + "Collection cleared successfully" : "La col·lecció s'ha esborrat correctament", + "Collection copied successfully" : "La col·lecció s'ha copiat correctament", + "Collection created successfully" : "La col·lecció s'ha creat correctament", + "Collection deleted successfully" : "La col·lecció s'ha suprimit correctament", + "Collection used to store and index file metadata and content" : "Col·lecció utilitzada per emmagatzemar i indexar metadades i contingut de fitxers", + "Collection used to store and index object data" : "Col·lecció utilitzada per emmagatzemar i indexar dades d'objectes", + "Collections:" : "Col·leccions:", + "Commit Message" : "Missatge de confirmació", + "Commit Within (ms)" : "Confirma en (ms)", + "Commit message" : "Missatge de confirmació", + "Compare current schema with real object data" : "Compara l'esquema actual amb les dades reals dels objectes", + "Completed" : "Completat", + "Complex" : "Complex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Consultes complexes: cerques avançades amb múltiples filtres, operadors i combinacions de paràmetres complexes", + "Confidence Level" : "Nivell de confiança", + "Confidence Score" : "Puntuació de confiança", + "Config must be provided as an object" : "La configuració s'ha de proporcionar com a objecte", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Gestió de ConfigSets", + "ConfigSet Name" : "Nom del ConfigSet", + "ConfigSet created successfully" : "El ConfigSet s'ha creat correctament", + "ConfigSet deleted successfully" : "El ConfigSet s'ha suprimit correctament", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Els ConfigSets defineixen l'esquema i la configuració de les vostres col·leccions SOLR. Contenen definicions de camps, analitzadors i altres paràmetres de cerca.", + "Configuration" : "Configuració", + "Configuration Data" : "Dades de configuració", + "Configuration saved" : "Configuració desada", + "Configurations" : "Configuracions", + "Configure Facets" : "Configura les facetes", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Configureu els proveïdors de models de llenguatge gran (LLM) per a funcions impulsades per IA, com ara la cerca semàntica, les incrustacions i el xat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Configureu els paràmetres bàsics de connexió per al vostre servidor SOLR, incloent-hi l'autenticació i les opcions de xarxa. Utilitzeu els diàlegs separats de gestió de ConfigSets i col·leccions per gestionar els nuclis i les col·leccions.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Configureu com els objectes de la base de dades es converteixen en incrustacions vectorials per a la cerca semàntica. Els objectes es vectoritzen directament sense necessitat d'extracció de text.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Configureu com els objectes es converteixen en text abans de la vectorització. Aquests paràmetres afecten la qualitat i el context de la cerca.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Configureu els paràmetres per a la vectorització de fitxers. Aquest procés generarà incrustacions vectorials per a tots els fragments de fitxer extrets.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Configureu els paràmetres per a la vectorització d'objectes. Aquest procés generarà incrustacions vectorials per a tots els objectes que coincideixin amb els filtres de la vostra vista.", + "Configure which types of data to search and how many sources to retrieve" : "Configureu quins tipus de dades cercar i quantes fonts recuperar", + "Connection Failed" : "La connexió ha fallat", + "Connection Settings" : "Configuració de la connexió", + "Connection Successful!" : "Connexió correcta!", + "Connection failed" : "La connexió ha fallat", + "Connection protocol" : "Protocol de connexió", + "Connection successful" : "Connexió correcta", + "Connection timeout in seconds" : "Temps d'espera de connexió en segons", + "Constant delay between retries (5 minutes)" : "Retard constant entre reintents (5 minuts)", + "Constraint Issue" : "Problema de restricció", + "Content Filters" : "Filtres de contingut", + "Continue to Properties" : "Continua a les propietats", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Controleu quines vistes d'objectes s'han de vectoritzar per reduir els costos de l'API. Vectoritzeu només les vistes que es beneficien de la cerca semàntica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Controleu quines vistes i eines pot utilitzar la IA en aquesta conversa. Per defecte, totes les capacitats de l'agent estan activades.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Controla l'aleatorietat (0 = determinista, 2 = molt creatiu)", + "Conversation ID is required" : "L'identificador de la conversa és obligatori", + "Conversation archived" : "Conversa arxivada", + "Conversation archived successfully" : "La conversa s'ha arxivat correctament", + "Conversation cleared successfully" : "La conversa s'ha esborrat correctament", + "Conversation deleted" : "Conversa suprimida", + "Conversation permanently deleted" : "Conversa suprimida permanentment", + "Conversation renamed" : "Conversa reanomenada", + "Conversation restored" : "Conversa restaurada", + "Conversation title" : "Títol de la conversa", + "Copied!" : "Copiat!", + "Copy" : "Copia", + "Copy Changes" : "Copia els canvis", + "Copy Collection" : "Copia la col·lecció", + "Copy Data" : "Copia les dades", + "Copy Full Data" : "Copia totes les dades", + "Copying..." : "S'està copiant...", + "Count" : "Recompte", + "Counting objects..." : "S'estan comptant els objectes...", + "Costs" : "Costos", + "Create" : "Crea", + "Create Collection" : "Crea una col·lecció", + "Create ConfigSet" : "Crea un ConfigSet", + "Create New Collection" : "Crea una col·lecció nova", + "Create New ConfigSet" : "Crea un ConfigSet nou", + "Create Operations" : "Operacions de creació", + "Create Webhook" : "Crea un webhook", + "Create a copy of collection:" : "Crea una còpia de la col·lecció:", + "Create a new ConfigSet based on the _default template" : "Crea un ConfigSet nou basat en la plantilla _default", + "Create a new SOLR collection from an existing ConfigSet" : "Crea una col·lecció SOLR nova a partir d'un ConfigSet existent", + "Create your first AI agent to get started." : "Creeu el vostre primer agent d'IA per començar.", + "Created" : "Creat", + "Created:" : "Creat:", + "Creating..." : "S'està creant...", + "Current Filters" : "Filtres actuals", + "Current Type:" : "Tipus actual:", + "Custom API endpoint if using a different region" : "Punt final de l'API personalitzat si s'utilitza una regió diferent", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Capçaleres HTTP personalitzades (una per línia, format: Nom-Capçalera: valor)", + "Daily" : "Diàriament", + "Dashboard" : "Tauler", + "Data Source" : "Font de dades", + "Data sources" : "Fonts de dades", + "Data type does not match observed values" : "El tipus de dades no coincideix amb els valors observats", + "Database URL" : "URL de la base de dades", + "Database information refreshed" : "Informació de la base de dades actualitzada", + "Date Range" : "Interval de dates", + "Default" : "Per defecte", + "Default Organisation" : "Organització per defecte", + "Default: 5" : "Per defecte: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Els retards es dupliquen amb cada intent (2, 4, 8 minuts...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Els retards augmenten linealment (5, 10, 15 minuts...)", + "Delete" : "Suprimeix", + "Delete Application" : "Suprimeix l'aplicació", + "Delete Audit Trail" : "Suprimeix el rastre d'auditoria", + "Delete Collection" : "Suprimeix la col·lecció", + "Delete ConfigSet" : "Suprimeix el ConfigSet", + "Delete Objects" : "Suprimeix els objectes", + "Delete Operations" : "Operacions de supressió", + "Delete View" : "Suprimeix la vista", + "Delete all objects in this schema" : "Suprimeix tots els objectes d'aquest esquema", + "Delete permanently" : "Suprimeix permanentment", + "Delete this conversation?" : "Voleu suprimir aquesta conversa?", + "Delete view" : "Suprimeix la vista", + "Deleted" : "Suprimit", + "Deleted By" : "Suprimit per", + "Deleted Date" : "Data de supressió", + "Deleted Items Management" : "Gestió dels elements suprimits", + "Deleted This Week" : "Suprimits aquesta setmana", + "Deleted Today" : "Suprimits avui", + "Deleted:" : "Suprimit:", + "Deleting..." : "S'està suprimint...", + "Deletion Date Range" : "Interval de dates de supressió", + "Deletion Statistics" : "Estadístiques de supressió", + "Deprecated" : "Obsolet", + "Description" : "Descripció", + "Description:" : "Descripció:", + "Detect data types and patterns" : "Detecta els tipus de dades i els patrons", + "Detected At" : "Detectat a", + "Detected Format:" : "Format detectat:", + "Detected Issues:" : "Problemes detectats:", + "Disable" : "Desactiva", + "Disabled" : "Desactivat", + "Discover Files" : "Descobreix els fitxers", + "Discovered Properties" : "Propietats descobertes", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Voleu suprimir permanentment totes les entrades del rastre d'auditoria filtrades? Aquesta acció no es pot desfer.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Voleu suprimir permanentment aquesta entrada del rastre d'auditoria? Aquesta acció no es pot desfer.", + "Documents" : "Documents", + "Download API Spec" : "Baixa l'especificació de l'API", + "Edit" : "Edita", + "Edit Register" : "Edita el registre", + "Edit Schema" : "Edita l'esquema", + "Edit View" : "Edita la vista", + "Edit Webhook" : "Edita el webhook", + "Edit view" : "Edita la vista", + "Edit view details" : "Edita els detalls de la vista", + "Effectiveness" : "Eficàcia", + "Email" : "Correu electrònic", + "Emails" : "Correus electrònics", + "Embedding Model" : "Model d'incrustació", + "Embedding Provider" : "Proveïdor d'incrustacions", + "Embedding generated successfully" : "La incrustació s'ha generat correctament", + "Embedding provider connection successful!" : "Connexió amb el proveïdor d'incrustacions correcta!", + "Enable" : "Activa", + "Enable auto-creation" : "Activa la creació automàtica", + "Enable automatic file vectorization" : "Activa la vectorització automàtica de fitxers", + "Enable automatic object vectorization" : "Activa la vectorització automàtica d'objectes", + "Enable automatic synchronization" : "Activa la sincronització automàtica", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Activa el registre detallat per a les operacions de SOLR (recomanat per a la depuració)", + "Enable faceting" : "Activa el facetatge", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Activa o desactiva les funcions de LLM. Configureu els proveïdors i els models utilitzant el botó de configuració de LLM de dalt.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Activa o desactiva la integració de cerca de SOLR. Configureu els paràmetres de connexió utilitzant el botó de configuració de la connexió de dalt.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Activa o desactiva la integració del flux de treball n8n. Configureu els paràmetres de connexió a continuació.", + "Enable or disable this webhook" : "Activa o desactiva aquest webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Activa la vectorització per a totes les vistes existents i futures (pot augmentar els costos)", + "Enabled" : "Activat", + "Endpoints" : "Punts finals", + "Enter ConfigSet name" : "Introduïu el nom del ConfigSet", + "Enter collection name" : "Introduïu el nom de la col·lecció", + "Enter description (optional)..." : "Introduïu una descripció (opcional)...", + "Enter new collection name" : "Introduïu el nom nou de la col·lecció", + "Enter object ID" : "Introduïu l'identificador de l'objecte", + "Enter search term" : "Introduïu el terme de cerca", + "Enter view name..." : "Introduïu el nom de la vista...", + "Enter webhook name" : "Introduïu el nom del webhook", + "Entities" : "Entitats", + "Entity Information" : "Informació de l'entitat", + "Entity deleted successfully" : "L'entitat s'ha suprimit correctament", + "Entity not found" : "No s'ha trobat l'entitat", + "Entries to be deleted:" : "Entrades a suprimir:", + "Enum Issue" : "Problema d'enumeració", + "Enum constraint is missing" : "Falta la restricció d'enumeració", + "Error" : "Error", + "Error Details" : "Detalls de l'error", + "Error Information" : "Informació de l'error", + "Error Message" : "Missatge d'error", + "Error loading application" : "S'ha produït un error en carregar l'aplicació", + "Error loading audit trails" : "S'ha produït un error en carregar els rastres d'auditoria", + "Error loading entity" : "S'ha produït un error en carregar l'entitat", + "Error loading search trails" : "S'ha produït un error en carregar els rastres de cerca", + "Estimated Batches:" : "Lots estimats:", + "Estimated Cost:" : "Cost estimat:", + "Estimated Duration:" : "Durada estimada:", + "Event" : "Esdeveniment", + "Event Property for Payload" : "Propietat de l'esdeveniment per a la càrrega útil", + "Events" : "Esdeveniments", + "Example Value" : "Valor d'exemple", + "Exclusive Maximum" : "Màxim exclusiu", + "Exclusive Minimum" : "Mínim exclusiu", + "Execution Mode" : "Mode d'execució", + "Execution Time" : "Temps d'execució", + "Execution Time Range" : "Interval de temps d'execució", + "Existing Improvements" : "Millores existents", + "Exponential" : "Exponencial", + "Export" : "Exporta", + "Export completed successfully" : "L'exportació s'ha completat correctament", + "Export, view, or delete audit trails" : "Exporta, visualitza o suprimeix rastres d'auditoria", + "Extend Schema" : "Amplia l'esquema", + "Extended" : "Ampliat", + "Extensions" : "Extensions", + "Extract Pending Files" : "Extreu els fitxers pendents", + "Extract properties from each object" : "Extreu les propietats de cada objecte", + "Extracted At" : "Extret a", + "Extraction Status" : "Estat de l'extracció", + "Facet configuration updated successfully" : "La configuració de les facetes s'ha actualitzat correctament", + "Facets discovered and configured successfully" : "Les facetes s'han descobert i configurat correctament", + "Facets discovered successfully" : "Les facetes s'han descobert correctament", + "Failed" : "Fallit", + "Failed to analyze schema properties" : "No s'han pogut analitzar les propietats de l'esquema", + "Failed to archive conversation" : "No s'ha pogut arxivar la conversa", + "Failed to calculate sizes" : "No s'han pogut calcular les mides", + "Failed to clear collection" : "No s'ha pogut esborrar la col·lecció", + "Failed to copy changes" : "No s'han pogut copiar els canvis", + "Failed to copy data" : "No s'han pogut copiar les dades", + "Failed to copy data to clipboard" : "No s'han pogut copiar les dades al porta-retalls", + "Failed to create conversation" : "No s'ha pogut crear la conversa", + "Failed to create or find project" : "No s'ha pogut crear o trobar el projecte", + "Failed to delete collection" : "No s'ha pogut suprimir la col·lecció", + "Failed to delete conversation" : "No s'ha pogut suprimir la conversa", + "Failed to delete webhook" : "No s'ha pogut suprimir el webhook", + "Failed to download API specification" : "No s'ha pogut baixar l'especificació de l'API", + "Failed to get SOLR field configuration" : "No s'ha pogut obtenir la configuració del camp de SOLR", + "Failed to get entity categories" : "No s'han pogut obtenir les categories de l'entitat", + "Failed to get entity statistics" : "No s'han pogut obtenir les estadístiques de l'entitat", + "Failed to get entity types" : "No s'han pogut obtenir els tipus d'entitat", + "Failed to load LLM configuration" : "No s'ha pogut carregar la configuració de LLM", + "Failed to load Nextcloud groups" : "No s'han pogut carregar els grups de Nextcloud", + "Failed to load conversation" : "No s'ha pogut carregar la conversa", + "Failed to load entities" : "No s'han pogut carregar les entitats", + "Failed to load entity" : "No s'ha pogut carregar l'entitat", + "Failed to load files" : "No s'han pogut carregar els fitxers", + "Failed to load organisations" : "No s'han pogut carregar les organitzacions", + "Failed to load templates" : "No s'han pogut carregar les plantilles", + "Failed to load webhooks" : "No s'han pogut carregar els webhooks", + "Failed to load workflows" : "No s'han pogut carregar els fluxos de treball", + "Failed to refresh database information" : "No s'ha pogut actualitzar la informació de la base de dades", + "Failed to reindex collection" : "No s'ha pogut reindexar la col·lecció", + "Failed to rename conversation" : "No s'ha pogut reanomenar la conversa", + "Failed to restore conversation" : "No s'ha pogut restaurar la conversa", + "Failed to retry extraction" : "No s'ha pogut reintentar l'extracció", + "Failed to retry webhook" : "No s'ha pogut reintentar el webhook", + "Failed to save GitHub token" : "No s'ha pogut desar el token de GitHub", + "Failed to save GitLab URL" : "No s'ha pogut desar l'URL de GitLab", + "Failed to save GitLab token" : "No s'ha pogut desar el token de GitLab", + "Failed to save additional feedback" : "No s'han pogut desar els comentaris addicionals", + "Failed to save n8n configuration" : "No s'ha pogut desar la configuració de n8n", + "Failed to save roles" : "No s'han pogut desar els rols", + "Failed to save settings" : "No s'ha pogut desar la configuració", + "Failed to save webhook" : "No s'ha pogut desar el webhook", + "Failed to send feedback" : "No s'han pogut enviar els comentaris", + "Failed to test webhook" : "No s'ha pogut provar el webhook", + "Failed to update favorite status" : "No s'ha pogut actualitzar l'estat de preferit", + "Failed to update schema properties" : "No s'han pogut actualitzar les propietats de l'esquema", + "Failed to update view" : "No s'ha pogut actualitzar la vista", + "Failed to update webhook" : "No s'ha pogut actualitzar el webhook", + "Feedback recorded" : "Comentaris registrats", + "Fewer sources (1-3):" : "Menys fonts (1-3):", + "Field" : "Camp", + "File" : "Fitxer", + "File Chunk Prediction" : "Predicció de fragments de fitxer", + "File Collection" : "Col·lecció de fitxers", + "File Management" : "Gestió de fitxers", + "File Name" : "Nom del fitxer", + "File Path" : "Camí del fitxer", + "File Type Selection" : "Selecció del tipus de fitxer", + "File Vectorization" : "Vectorització de fitxers", + "File Warmup" : "Escalfament de fitxers", + "File actions menu" : "Menú d'accions del fitxer", + "File anonymized successfully" : "El fitxer s'ha anonimitzat correctament", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "La vectorització de fragments de fitxer encara no està implementada. Els fragments estan preparats i emmagatzemats, però el servei de vectorització està en desenvolupament.", + "File collection not configured" : "La col·lecció de fitxers no està configurada", + "File discovery completed" : "El descobriment de fitxers s'ha completat", + "File extraction completed" : "L'extracció de fitxers s'ha completat", + "File extraction queued" : "L'extracció de fitxers s'ha posat a la cua", + "File indexed successfully" : "El fitxer s'ha indexat correctament", + "File is already anonymized" : "El fitxer ja està anonimitzat", + "File not found" : "No s'ha trobat el fitxer", + "File settings updated successfully" : "La configuració del fitxer s'ha actualitzat correctament", + "File sources" : "Fonts de fitxers", + "File vectorization configuration saved successfully" : "La configuració de la vectorització de fitxers s'ha desat correctament", + "File vectorization started. Check the statistics section for progress." : "La vectorització de fitxers s'ha iniciat. Consulteu la secció d'estadístiques per veure el progrés.", + "File warmup completed" : "L'escalfament de fitxers s'ha completat", + "Files" : "Fitxers", + "Files with Completed Extraction:" : "Fitxers amb extracció completada:", + "Files → fileCollection, Objects → objectCollection" : "Fitxers → fileCollection, Objectes → objectCollection", + "Filter Audit Trails" : "Filtra els rastres d'auditoria", + "Filter Deleted Items" : "Filtra els elements suprimits", + "Filter Objects" : "Filtra els objectes", + "Filter Properties" : "Filtra les propietats", + "Filter Search Trails" : "Filtra els rastres de cerca", + "Filter Statistics" : "Filtra les estadístiques", + "Filter and analyze search trail entries" : "Filtra i analitza les entrades del rastre de cerca", + "Filter and manage audit trail entries" : "Filtra i gestiona les entrades del rastre d'auditoria", + "Filter and manage soft deleted items" : "Filtra i gestiona els elements suprimits temporalment", + "Filter by object ID" : "Filtra per identificador d'objecte", + "Filter by search term" : "Filtra per terme de cerca", + "Filter by webhook" : "Filtra per webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Dades de filtre carregades automàticament. Utilitzeu els filtres de sota per refinar la cerca.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtra els activadors del webhook per propietats de la càrrega útil (un per línia, format: clau: valor)", + "Filtered" : "Filtrat", + "Filters" : "Filtres", + "Fireworks AI Chat Settings" : "Configuració del xat de Fireworks AI", + "Fireworks AI Embedding Configuration" : "Configuració de les incrustacions de Fireworks AI", + "First" : "Primer", + "Fixed" : "Fix", + "For chat and retrieval-augmented generation" : "Per al xat i la generació augmentada amb recuperació", + "For vector embeddings and semantic search" : "Per a les incrustacions vectorials i la cerca semàntica", + "Format" : "Format", + "Format Issue" : "Problema de format", + "Format constraint is missing" : "Falta la restricció de format", + "From Date" : "Des de la data", + "From date" : "Des de la data", + "Full data copied to clipboard" : "Totes les dades s'han copiat al porta-retalls", + "General Issue" : "Problema general", + "Generate recommendations and confidence scores" : "Genera recomanacions i puntuacions de confiança", + "Generate vectors immediately when new objects are created" : "Genera vectors immediatament quan es creen objectes nous", + "GitHub token is valid" : "El token de GitHub és vàlid", + "GitHub token removed successfully" : "El token de GitHub s'ha eliminat correctament", + "GitHub token saved successfully" : "El token de GitHub s'ha desat correctament", + "GitLab URL saved successfully" : "L'URL de GitLab s'ha desat correctament", + "GitLab token is valid" : "El token de GitLab és vàlid", + "GitLab token saved successfully" : "El token de GitLab s'ha desat correctament", + "HTTP Method" : "Mètode HTTP", + "HTTP method used to send webhook requests" : "Mètode HTTP utilitzat per enviar les sol·licituds del webhook", + "Headers" : "Capçaleres", + "Health" : "Estat", + "Heartbeat successful - connection kept alive" : "Batec correcte - connexió mantinguda activa", + "Helpful" : "Útil", + "Hide Filters" : "Oculta els filtres", + "Hide in forms" : "Oculta als formularis", + "Hide in list view" : "Oculta a la vista de llista", + "High" : "Alt", + "High Confidence" : "Confiança alta", + "Host" : "Amfitrió", + "Hourly" : "Cada hora", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Quina profunditat recórrer en les propietats d'objectes imbricades (1-20). Els valors més alts capturen més detall però augmenten la mida del vector.", + "How often to check for updates (1-168 hours)" : "Amb quina freqüència comprovar si hi ha actualitzacions (1-168 hores)", + "How to handle retries for failed webhook deliveries" : "Com gestionar els reintents per a les entregues de webhook fallides", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Els límits de freqüència d'IP s'han esborrat correctament", + "Identify properties not in the schema" : "Identifica les propietats que no són a l'esquema", + "Immutable" : "Immutable", + "Import" : "Importa", + "Import successful" : "Importació correcta", + "Improved Property" : "Propietat millorada", + "In use" : "En ús", + "Inactive" : "Inactiu", + "Include IDs and names of related objects for better contextual search" : "Inclou els identificadors i els noms dels objectes relacionats per a una millor cerca contextual", + "Include related object references" : "Inclou les referències d'objectes relacionats", + "Include schema and register metadata" : "Inclou les metadades de l'esquema i del registre", + "Initialization failed" : "La inicialització ha fallat", + "Initialize Project" : "Inicialitza el projecte", + "Inspect Fields" : "Inspecciona els camps", + "Inspect Index" : "Inspecciona l'índex", + "Invalid" : "No vàlid", + "Invalid batch size. Must be between 1 and 5000" : "Mida del lot no vàlida. Ha d'estar entre 1 i 5000", + "Invalid field name provided" : "S'ha proporcionat un nom de camp no vàlid", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects no vàlid. Ha de ser 0 (tot) o un nombre positiu", + "Invalid:" : "No vàlid:", + "Issue" : "Problema", + "Items per page:" : "Elements per pàgina:", + "Just now" : "Ara mateix", + "LLM Configuration" : "Configuració de LLM", + "LLM actions menu" : "Menú d'accions de LLM", + "LLM configuration saved successfully" : "La configuració de LLM s'ha desat correctament", + "LLM features disabled" : "Funcions de LLM desactivades", + "LLM features enabled" : "Funcions de LLM activades", + "LLM must be enabled with an embedding provider configured" : "El LLM ha d'estar activat amb un proveïdor d'incrustacions configurat", + "LLM settings updated successfully" : "La configuració de LLM s'ha actualitzat correctament", + "Last" : "Últim", + "Last Triggered" : "Última activació", + "Length Range:" : "Interval de longitud:", + "Linear" : "Lineal", + "Load Advanced Filters" : "Carrega els filtres avançats", + "Load advanced filters with live data from your search index" : "Carrega els filtres avançats amb dades en directe del vostre índex de cerca", + "Load view" : "Carrega la vista", + "Loading ConfigSets..." : "S'estan carregant els ConfigSets...", + "Loading advanced filters..." : "S'estan carregant els filtres avançats...", + "Loading agents..." : "S'estan carregant els agents...", + "Loading application details..." : "S'estan carregant els detalls de l'aplicació...", + "Loading applications..." : "S'estan carregant les aplicacions...", + "Loading archived conversations..." : "S'estan carregant les converses arxivades...", + "Loading audit trails..." : "S'estan carregant els rastres d'auditoria...", + "Loading collections..." : "S'estan carregant les col·leccions...", + "Loading configurations..." : "S'estan carregant les configuracions...", + "Loading conversation..." : "S'està carregant la conversa...", + "Loading conversations..." : "S'estan carregant les converses...", + "Loading deleted items..." : "S'estan carregant els elements suprimits...", + "Loading events..." : "S'estan carregant els esdeveniments...", + "Loading filter data..." : "S'estan carregant les dades del filtre...", + "Loading groups..." : "S'estan carregant els grups...", + "Loading log details..." : "S'estan carregant els detalls del registre...", + "Loading registers..." : "S'estan carregant els registres...", + "Loading schemas..." : "S'estan carregant els esquemes...", + "Loading search trails..." : "S'estan carregant els rastres de cerca...", + "Loading sources..." : "S'estan carregant les fonts...", + "Loading statistics..." : "S'estan carregant les estadístiques...", + "Loading users..." : "S'estan carregant els usuaris...", + "Loading views..." : "S'estan carregant les vistes...", + "Loading..." : "S'està carregant...", + "Local" : "Local", + "Local Version" : "Versió local", + "Locked" : "Bloquejat", + "Locked:" : "Bloquejat:", + "Login successful" : "Inici de sessió correcte", + "Logs" : "Registres", + "Low" : "Baix", + "Low Confidence" : "Confiança baixa", + "Lucene Version:" : "Versió de Lucene:", + "Magic table synchronized successfully" : "La taula màgica s'ha sincronitzat correctament", + "Manage Organisation Roles" : "Gestiona els rols de l'organització", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Gestiona les col·leccions SOLR (magatzems de dades) i assigna-les per a objectes i fitxers.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Gestiona els ConfigSets de SOLR (plantilles de configuració) per a les vostres col·leccions.", + "Manage and configure agents for automated tasks" : "Gestiona i configura agents per a tasques automatitzades", + "Manage and monitor file text extraction status" : "Gestiona i supervisa l'estat de l'extracció de text dels fitxers", + "Manage and restore soft deleted items from your registers" : "Gestiona i restaura els elements suprimits temporalment dels vostres registres", + "Manage and view detected entities from files and objects" : "Gestiona i visualitza les entitats detectades de fitxers i objectes", + "Manage document templates and themes" : "Gestiona les plantilles i els temes de documents", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Gestiona els fluxos de treball de n8n per a l'automatització d'OpenRegister. Els fluxos de treball s'emmagatzemaran al projecte configurat.", + "Manage webhooks for event-driven integrations" : "Gestiona els webhooks per a integracions impulsades per esdeveniments", + "Manage your applications and modules" : "Gestioneu les vostres aplicacions i mòduls", + "Manage your data registers and their configurations" : "Gestioneu els vostres registres de dades i les seves configuracions", + "Manage your data schemas and their properties" : "Gestioneu els vostres esquemes de dades i les seves propietats", + "Manage your data sources and their configurations" : "Gestioneu les vostres fonts de dades i les seves configuracions", + "Manage your saved search configurations" : "Gestioneu les vostres configuracions de cerca desades", + "Manage your system configurations and settings" : "Gestioneu les vostres configuracions i paràmetres del sistema", + "Managed" : "Gestionat", + "Max Files (0 = all)" : "Fitxers màxims (0 = tots)", + "Max Length" : "Longitud màxima", + "Max Length:" : "Longitud màxima:", + "Max Objects (0 = all)" : "Objectes màxims (0 = tots)", + "Max Retries" : "Reintents màxims", + "Max Shards/Node" : "Fragments màxims/node", + "Max execution time (ms)" : "Temps d'execució màxim (ms)", + "Max ms" : "Màx ms", + "Max result count" : "Recompte màxim de resultats", + "Max results" : "Resultats màxims", + "Maximum" : "Màxim", + "Maximum Nesting Depth" : "Profunditat màxima d'imbricació", + "Maximum length constraint is missing" : "Falta la restricció de longitud màxima", + "Maximum length is too restrictive" : "La longitud màxima és massa restrictiva", + "Maximum number of files to process. Set to 0 to process all files." : "Nombre màxim de fitxers a processar. Establiu-ho a 0 per processar tots els fitxers.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Nombre màxim d'objectes a processar. Establiu-ho a 0 per processar tots els objectes.", + "Maximum number of retry attempts for failed deliveries" : "Nombre màxim d'intents de reintent per a les entregues fallides", + "Maximum time to wait before committing changes" : "Temps màxim d'espera abans de confirmar els canvis", + "Maximum tokens to generate" : "Tokens màxims a generar", + "Maximum value constraint is missing" : "Falta la restricció de valor màxim", + "Maximum value is too restrictive" : "El valor màxim és massa restrictiu", + "Medium" : "Mitjà", + "Medium Confidence" : "Confiança mitjana", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Consultes mitjanes: cerques amb algun filtratge o múltiples paràmetres (p. ex., intervals de dates, registres/esquemes específics)", + "Members of selected groups can access this view" : "Els membres dels grups seleccionats poden accedir a aquesta vista", + "Members:" : "Membres:", + "Memory prediction calculated successfully" : "La predicció de memòria s'ha calculat correctament", + "Message content is required" : "El contingut del missatge és obligatori", + "Message does not belong to this conversation" : "El missatge no pertany a aquesta conversa", + "Metadata Filters" : "Filtres de metadades", + "Method" : "Mètode", + "Min Length" : "Longitud mínima", + "Min execution time (ms)" : "Temps d'execució mínim (ms)", + "Min ms" : "Mín ms", + "Min result count" : "Recompte mínim de resultats", + "Min results" : "Resultats mínims", + "Minimum" : "Mínim", + "Minimum value constraint is missing" : "Falta la restricció de valor mínim", + "Minimum value is too restrictive" : "El valor mínim és massa restrictiu", + "Missing object fields created successfully" : "Els camps d'objecte que faltaven s'han creat correctament", + "Mode:" : "Mode:", + "Model" : "Model", + "Model Name" : "Nom del model", + "Modified" : "Modificat", + "Monthly" : "Mensualment", + "More sources (10-20):" : "Més fonts (10-20):", + "Most Active Objects" : "Objectes més actius", + "Multiple Of" : "Múltiple de", + "Name" : "Nom", + "Names cache warmup completed" : "L'escalfament de la memòria cau de noms s'ha completat", + "New Collection Name" : "Nom de la col·lecció nova", + "New Conversation" : "Conversa nova", + "New Properties" : "Propietats noves", + "New Property" : "Propietat nova", + "New Value" : "Valor nou", + "New users without specific organisation membership will be automatically added to this organisation" : "Els usuaris nous sense pertinença específica a una organització s'afegiran automàticament a aquesta organització", + "Next" : "Següent", + "No" : "No", + "No ConfigSets found" : "No s'ha trobat cap ConfigSet", + "No GitHub token configured" : "No hi ha cap token de GitHub configurat", + "No GitHub token provided" : "No s'ha proporcionat cap token de GitHub", + "No GitLab token provided" : "No s'ha proporcionat cap token de GitLab", + "No Organisation" : "Sense organització", + "No active filters" : "No hi ha filtres actius", + "No activity data available" : "No hi ha dades d'activitat disponibles", + "No agents available" : "No hi ha agents disponibles", + "No agents found" : "No s'ha trobat cap agent", + "No app store cache exists yet" : "Encara no existeix cap memòria cau de la botiga d'aplicacions", + "No applications are available." : "No hi ha aplicacions disponibles.", + "No applications found" : "No s'ha trobat cap aplicació", + "No archived conversations" : "No hi ha converses arxivades", + "No audit trail entries found" : "No s'ha trobat cap entrada del rastre d'auditoria", + "No changes recorded" : "No s'ha registrat cap canvi", + "No chunks to vectorize" : "No hi ha fragments a vectoritzar", + "No collections found" : "No s'ha trobat cap col·lecció", + "No configuration data" : "No hi ha dades de configuració", + "No configurations are available." : "No hi ha configuracions disponibles.", + "No configurations found" : "No s'ha trobat cap configuració", + "No configurations found for this application." : "No s'ha trobat cap configuració per a aquesta aplicació.", + "No conversations yet. Create a new one to get started!" : "Encara no hi ha converses. Creeu-ne una de nova per començar!", + "No data available" : "No hi ha dades disponibles", + "No data available for chart" : "No hi ha dades disponibles per al gràfic", + "No deleted items found" : "No s'ha trobat cap element suprimit", + "No deletion data available" : "No hi ha dades de supressió disponibles", + "No description found" : "No s'ha trobat cap descripció", + "No description provided" : "No s'ha proporcionat cap descripció", + "No entities detected in this file. Run text extraction first." : "No s'ha detectat cap entitat en aquest fitxer. Executeu primer l'extracció de text.", + "No entities found" : "No s'ha trobat cap entitat", + "No entities have been detected yet" : "Encara no s'ha detectat cap entitat", + "No events found" : "No s'ha trobat cap esdeveniment", + "No expired audit trails found to clear" : "No s'ha trobat cap rastre d'auditoria caducat per esborrar", + "No facetable fields available. Select a register and schema to see available filters." : "No hi ha camps facetables disponibles. Seleccioneu un registre i un esquema per veure els filtres disponibles.", + "No files found" : "No s'ha trobat cap fitxer", + "No files have been extracted yet" : "Encara no s'ha extret cap fitxer", + "No files to process" : "No hi ha fitxers a processar", + "No files to reindex" : "No hi ha fitxers a reindexar", + "No filters are currently active. This will delete ALL audit trail entries!" : "Actualment no hi ha cap filtre actiu. Això suprimirà TOTES les entrades del rastre d'auditoria!", + "No log entries found" : "No s'ha trobat cap entrada de registre", + "No logs are available for this configuration." : "No hi ha registres disponibles per a aquesta configuració.", + "No logs are available for this source." : "No hi ha registres disponibles per a aquesta font.", + "No logs found" : "No s'ha trobat cap registre", + "No mismatched fields found - SOLR schema is properly configured" : "No s'ha trobat cap camp no coincident; l'esquema de SOLR està configurat correctament", + "No objects found" : "No s'ha trobat cap objecte", + "No objects found to vectorize" : "No s'ha trobat cap objecte per vectoritzar", + "No objects selected for deletion" : "No s'ha seleccionat cap objecte per suprimir", + "No objects to delete" : "No hi ha objectes a suprimir", + "No objects were permanently deleted" : "No s'ha suprimit permanentment cap objecte", + "No properties found" : "No s'ha trobat cap propietat", + "No properties match your filters." : "No hi ha cap propietat que coincideixi amb els vostres filtres.", + "No purge date set" : "No s'ha establert cap data de purga", + "No register data available" : "No hi ha dades de registre disponibles", + "No registers found" : "No s'ha trobat cap registre", + "No registers found for this application." : "No s'ha trobat cap registre per a aquesta aplicació.", + "No relations found" : "No s'ha trobat cap relació", + "No request body available" : "No hi ha cap cos de sol·licitud disponible", + "No response body available" : "No hi ha cap cos de resposta disponible", + "No saved views yet. Create one in the Search tab!" : "Encara no hi ha vistes desades. Creeu-ne una a la pestanya de cerca!", + "No schema data available" : "No hi ha dades d'esquema disponibles", + "No schema selected for exploration" : "No s'ha seleccionat cap esquema per a l'exploració", + "No schemas found" : "No s'ha trobat cap esquema", + "No schemas found for this application." : "No s'ha trobat cap esquema per a aquesta aplicació.", + "No search terms data available" : "No hi ha dades de termes de cerca disponibles", + "No search trail entries found" : "No s'ha trobat cap entrada del rastre de cerca", + "No sources are available." : "No hi ha fonts disponibles.", + "No sources found" : "No s'ha trobat cap font", + "No templates found" : "No s'ha trobat cap plantilla", + "No templates have been created yet" : "Encara no s'ha creat cap plantilla", + "No valid magic-mapped register+schema combinations found" : "No s'ha trobat cap combinació vàlida de registre+esquema amb mapatge màgic", + "No views found. Create views first before configuring vectorization." : "No s'ha trobat cap vista. Creeu vistes abans de configurar la vectorització.", + "No views match your search" : "No hi ha cap vista que coincideixi amb la vostra cerca", + "No webhooks found" : "No s'ha trobat cap webhook", + "No webhooks have been configured yet" : "Encara no s'ha configurat cap webhook", + "No workflows found in this project. Create workflows in the n8n editor." : "No s'ha trobat cap flux de treball en aquest projecte. Creeu fluxos de treball a l'editor de n8n.", + "None" : "Cap", + "Not helpful" : "No útil", + "Not in use" : "No està en ús", + "Number Constraints" : "Restriccions numèriques", + "Number of chunks to vectorize in one API call (1-100)." : "Nombre de fragments a vectoritzar en una crida a l'API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Nombre de fragments a vectoritzar en una crida a l'API. Més alt = més ràpid però més memòria. Recomanat: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Nombre d'objectes a processar en cada lot (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Nombre d'objectes a vectoritzar en una crida a l'API. Més alt = més ràpid però més memòria. Recomanat: 10-50.", + "Numeric Range:" : "Interval numèric:", + "Object" : "Objecte", + "Object Collection" : "Col·lecció d'objectes", + "Object Count Prediction" : "Predicció del recompte d'objectes", + "Object ID" : "Identificador de l'objecte", + "Object Management" : "Gestió d'objectes", + "Object Vectorization" : "Vectorització d'objectes", + "Object Warmup" : "Escalfament d'objectes", + "Object collection not configured" : "La col·lecció d'objectes no està configurada", + "Object not found" : "No s'ha trobat l'objecte", + "Object permanently deleted" : "Objecte suprimit permanentment", + "Object restored successfully" : "L'objecte s'ha restaurat correctament", + "Object settings updated successfully" : "La configuració de l'objecte s'ha actualitzat correctament", + "Object sources" : "Fonts d'objectes", + "Object unlocked successfully" : "L'objecte s'ha desbloquejat correctament", + "Object vectorization configuration saved successfully" : "La configuració de la vectorització d'objectes s'ha desat correctament", + "Object vectorized successfully" : "L'objecte s'ha vectoritzat correctament", + "Object:" : "Objecte:", + "Objects" : "Objectes", + "Objects Analyzed" : "Objectes analitzats", + "Objects Distribution" : "Distribució d'objectes", + "Objects being analyzed" : "Objectes en anàlisi", + "Objects by Register" : "Objectes per registre", + "Objects by Schema" : "Objectes per esquema", + "Objects deletion completed successfully" : "La supressió d'objectes s'ha completat correctament", + "Objects in schema" : "Objectes a l'esquema", + "Objects to Process:" : "Objectes a processar:", + "Objects to be analyzed" : "Objectes a analitzar", + "Objects to be deleted" : "Objectes a suprimir", + "Objects to be validated" : "Objectes a validar", + "Objects will be serialized as JSON text before vectorization" : "Els objectes se serialitzaran com a text JSON abans de la vectorització", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Els objectes se suprimiran temporalment (marcats com a suprimits però conservats a la base de dades). Es poden recuperar més endavant si cal.", + "Old Value" : "Valor antic", + "Oldest Item (days)" : "Element més antic (dies)", + "Ollama Chat Settings" : "Configuració del xat d'Ollama", + "Ollama Embedding Configuration" : "Configuració de les incrustacions d'Ollama", + "Ollama URL" : "URL d'Ollama", + "Only With Changes" : "Només amb canvis", + "Open Register Objects" : "Objectes d'Open Register", + "Open n8n Editor" : "Obre l'editor de n8n", + "OpenAI Chat Settings" : "Configuració del xat d'OpenAI", + "OpenAI Embedding Configuration" : "Configuració de les incrustacions d'OpenAI", + "OpenRegister Settings" : "Configuració d'OpenRegister", + "Optional webhook secret for signature verification" : "Secret opcional del webhook per a la verificació de la signatura", + "Order" : "Ordre", + "Organisation" : "Organització", + "Organisation ID" : "Identificador de l'organització", + "Organisation Members" : "Membres de l'organització", + "Organisation Statistics" : "Estadístiques de l'organització", + "Organisation created successfully" : "L'organització s'ha creat correctament", + "Organisation settings saved successfully" : "La configuració de l'organització s'ha desat correctament", + "Organisations" : "Organitzacions", + "Organization" : "Organització", + "Organization ID (Optional)" : "Identificador de l'organització (opcional)", + "Orphaned Items" : "Elements orfes", + "Owner" : "Propietari", + "Owner:" : "Propietari:", + "Parallel Mode (Faster, more resource intensive)" : "Mode paral·lel (més ràpid, més intensiu en recursos)", + "Parallel:" : "Paral·lel:", + "Password" : "Contrasenya", + "Password for SOLR authentication (optional)" : "Contrasenya per a l'autenticació de SOLR (opcional)", + "Path" : "Camí", + "Path in repository" : "Camí al repositori", + "Path where the configuration file will be saved in the repository" : "Camí on es desarà el fitxer de configuració al repositori", + "Path where the register OAS file will be saved in the repository" : "Camí on es desarà el fitxer OAS del registre al repositori", + "Pattern (regex)" : "Patró (regex)", + "Pattern Issue" : "Problema de patró", + "Pattern constraint is missing" : "Falta la restricció de patró", + "Patterns:" : "Patrons:", + "Pending" : "Pendent", + "Permanent Delete Mode" : "Mode de supressió permanent", + "Permanently Delete" : "Suprimeix permanentment", + "Permanently delete already soft-deleted objects" : "Suprimeix permanentment els objectes ja suprimits temporalment", + "Permanently delete this conversation?" : "Voleu suprimir permanentment aquesta conversa?", + "Person" : "Persona", + "Personal Data" : "Dades personals", + "Phone" : "Telèfon", + "Photos" : "Fotos", + "Please create an agent in the" : "Creeu un agent al", + "Please select an agent to continue" : "Seleccioneu un agent per continuar", + "Please select which register and schema to use for the new object" : "Seleccioneu quin registre i esquema utilitzar per a l'objecte nou", + "Please try again later." : "Torneu-ho a provar més tard.", + "Please wait while we fetch your agents." : "Espereu mentre recuperem els vostres agents.", + "Please wait while we fetch your applications." : "Espereu mentre recuperem les vostres aplicacions.", + "Please wait while we fetch your configurations." : "Espereu mentre recuperem les vostres configuracions.", + "Please wait while we fetch your deleted items." : "Espereu mentre recuperem els vostres elements suprimits.", + "Please wait while we fetch your sources." : "Espereu mentre recuperem les vostres fonts.", + "Polls" : "Enquestes", + "Popular Search Terms" : "Termes de cerca populars", + "Port" : "Port", + "Prerequisites" : "Prerequisits", + "Press Enter to send, Shift+Enter for new line" : "Premeu Retorn per enviar, Maj+Retorn per a una línia nova", + "Previous" : "Anterior", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Processa els fragments per lots amb paral·lelisme simulat.", + "Processes file chunks sequentially (safest)." : "Processa els fragments de fitxer seqüencialment (el més segur).", + "Processes objects in chunks with simulated parallelism." : "Processa els objectes en fragments amb paral·lelisme simulat.", + "Processes objects sequentially (safest)." : "Processa els objectes seqüencialment (el més segur).", + "Processing" : "S'està processant", + "Processing Limits" : "Límits de processament", + "Processing..." : "S'està processant...", + "Project initialized successfully" : "El projecte s'ha inicialitzat correctament", + "Project not found. Please initialize first." : "No s'ha trobat el projecte. Inicialitzeu-lo primer.", + "Properties" : "Propietats", + "Property Behaviors" : "Comportaments de la propietat", + "Property Configuration:" : "Configuració de la propietat:", + "Property Filters" : "Filtres de propietats", + "Property Title" : "Títol de la propietat", + "Property Type" : "Tipus de propietat", + "Property can be improved" : "La propietat es pot millorar", + "Protected" : "Protegit", + "Provider" : "Proveïdor", + "Provider is required for testing" : "El proveïdor és obligatori per provar", + "Public" : "Públic", + "Public View" : "Vista pública", + "Public views can be accessed by anyone in the system" : "Qualsevol persona del sistema pot accedir a les vistes públiques", + "Publish" : "Publica", + "Publish Objects" : "Publica els objectes", + "Publish all objects in this schema" : "Publica tots els objectes d'aquest esquema", + "Published" : "Publicat", + "Published:" : "Publicat:", + "Publishing..." : "S'està publicant...", + "Purge" : "Purga", + "Purge Date" : "Data de purga", + "Query Complexity Distribution" : "Distribució de la complexitat de les consultes", + "Query parameter is required" : "El paràmetre de consulta és obligatori", + "RAG" : "RAG", + "RAG Configuration" : "Configuració de RAG", + "Rate limits cleared successfully" : "Els límits de freqüència s'han esborrat correctament", + "Raw Changes Data" : "Dades de canvis en brut", + "Re-vectorize on object update" : "Torna a vectoritzar en actualitzar l'objecte", + "Read" : "Llegeix", + "Recommendations:" : "Recomanacions:", + "Recommended Type:" : "Tipus recomanat:", + "Recommended: 5 sources" : "Recomanat: 5 fonts", + "Refresh" : "Actualitza", + "Refresh Data" : "Actualitza les dades", + "Refresh Stats" : "Actualitza les estadístiques", + "Refresh Workflows" : "Actualitza els fluxos de treball", + "Refresh dashboard" : "Actualitza el tauler", + "Refresh database info" : "Actualitza la informació de la base de dades", + "Register" : "Registre", + "Register ID" : "Identificador del registre", + "Register Statistics" : "Estadístiques del registre", + "Register Totals" : "Totals del registre", + "Register depublished successfully" : "El registre s'ha despublicat correctament", + "Register not found" : "No s'ha trobat el registre", + "Register objects deletion completed successfully" : "La supressió dels objectes del registre s'ha completat correctament", + "Register published successfully" : "El registre s'ha publicat correctament", + "Register/Schema Usage" : "Ús del registre/esquema", + "Registers" : "Registres", + "Reindex" : "Reindexa", + "Reindex completed" : "La reindexació s'ha completat", + "Reindex completed successfully" : "La reindexació s'ha completat correctament", + "Reindex failed" : "La reindexació ha fallat", + "Relations" : "Relacions", + "Remote Version" : "Versió remota", + "Remove" : "Elimina", + "Remove filter" : "Elimina el filtre", + "Remove from favorites" : "Elimina dels preferits", + "Remove group" : "Elimina el grup", + "Removed" : "Eliminat", + "Removed from favorites" : "Eliminat dels preferits", + "Rename Conversation" : "Reanomena la conversa", + "Rename conversation" : "Reanomena la conversa", + "Replicas" : "Rèpliques", + "Repository" : "Repositori", + "Request Body" : "Cos de la sol·licitud", + "Request Data" : "Dades de la sol·licitud", + "Request Details" : "Detalls de la sol·licitud", + "Request body must be JSON with a \"query\" field" : "El cos de la sol·licitud ha de ser JSON amb un camp \"query\"", + "Request timeout in seconds" : "Temps d'espera de la sol·licitud en segons", + "Required field" : "Camp obligatori", + "Required status is inconsistent" : "L'estat d'obligatori és inconsistent", + "Rerun Search" : "Torna a executar la cerca", + "Reset Changes" : "Restableix els canvis", + "Reset Filters" : "Restableix els filtres", + "Response Body" : "Cos de la resposta", + "Response Details" : "Detalls de la resposta", + "Response Time:" : "Temps de resposta:", + "Restore" : "Restaura", + "Restore conversation" : "Restaura la conversa", + "Restore or permanently delete items" : "Restaura o suprimeix permanentment els elements", + "Result Count Range" : "Interval del recompte de resultats", + "Results" : "Resultats", + "Retrieve all objects for this schema" : "Recupera tots els objectes d'aquest esquema", + "Retry" : "Reintenta", + "Retry Failed Extractions" : "Reintenta les extraccions fallides", + "Retry Policy" : "Política de reintents", + "Retry completed" : "El reintent s'ha completat", + "Retrying..." : "S'està reintentant...", + "Risk Level" : "Nivell de risc", + "Roles updated successfully" : "Els rols s'han actualitzat correctament", + "SOLR Connection Settings" : "Configuració de la connexió de SOLR", + "SOLR Version:" : "Versió de SOLR:", + "SOLR actions menu" : "Menú d'accions de SOLR", + "SOLR base path (usually /solr)" : "Camí base de SOLR (normalment /solr)", + "SOLR is disabled" : "SOLR està desactivat", + "SOLR is not available or not configured" : "SOLR no està disponible o no està configurat", + "SOLR logging disabled" : "Registre de SOLR desactivat", + "SOLR logging enabled" : "Registre de SOLR activat", + "SOLR search disabled" : "Cerca de SOLR desactivada", + "SOLR search enabled" : "Cerca de SOLR activada", + "SOLR server hostname or IP address" : "Nom d'amfitrió o adreça IP del servidor SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Número de port del servidor SOLR (opcional, per defecte 8983)", + "SOLR setup completed successfully" : "La configuració de SOLR s'ha completat correctament", + "SOLR setup failed" : "La configuració de SOLR ha fallat", + "SOLR setup failed - check logs" : "La configuració de SOLR ha fallat: comproveu els registres", + "Sample Values:" : "Valors de mostra:", + "Save" : "Desa", + "Save Configuration" : "Desa la configuració", + "Save Connection Settings" : "Desa la configuració de la connexió", + "Save Roles" : "Desa els rols", + "Save Settings" : "Desa la configuració", + "Save current search as view" : "Desa la cerca actual com a vista", + "Saved Views" : "Vistes desades", + "Saving..." : "S'està desant...", + "Schema" : "Esquema", + "Schema ID" : "Identificador de l'esquema", + "Schema Name" : "Nom de l'esquema", + "Schema Statistics" : "Estadístiques de l'esquema", + "Schema depublished successfully" : "L'esquema s'ha despublicat correctament", + "Schema objects deletion completed successfully" : "La supressió dels objectes de l'esquema s'ha completat correctament", + "Schema published successfully" : "L'esquema s'ha publicat correctament", + "Schema successfully created" : "L'esquema s'ha creat correctament", + "Schema successfully updated" : "L'esquema s'ha actualitzat correctament", + "Schemas" : "Esquemes", + "Scheme" : "Esquema", + "Search / Views" : "Cerca / Vistes", + "Search Activity" : "Activitat de cerca", + "Search Analytics" : "Analítiques de cerca", + "Search Entities" : "Cerca entitats", + "Search Files" : "Cerca fitxers", + "Search GitHub" : "Cerca a GitHub", + "Search GitLab" : "Cerca a GitLab", + "Search Method" : "Mètode de cerca", + "Search Term" : "Terme de cerca", + "Search Trail Details" : "Detalls del rastre de cerca", + "Search Trail Management" : "Gestió del rastre de cerca", + "Search Trail Statistics" : "Estadístiques del rastre de cerca", + "Search Trails" : "Rastres de cerca", + "Search Views" : "Cerca vistes", + "Search Webhooks" : "Cerca webhooks", + "Search by file name or path" : "Cerca per nom de fitxer o camí", + "Search by name or URL" : "Cerca per nom o URL", + "Search by value" : "Cerca per valor", + "Search in Files" : "Cerca als fitxers", + "Search in Objects" : "Cerca als objectes", + "Search property names..." : "Cerca noms de propietats...", + "Search trail deletion not implemented yet" : "La supressió del rastre de cerca encara no està implementada", + "Search views..." : "Cerca vistes...", + "Secret" : "Secret", + "Secret key for HMAC signature generation (optional)" : "Clau secreta per a la generació de la signatura HMAC (opcional)", + "Select AI Agent" : "Selecciona un agent d'IA", + "Select All" : "Selecciona-ho tot", + "Select ConfigSet" : "Selecciona un ConfigSet", + "Select File Types to Vectorize:" : "Selecciona els tipus de fitxer a vectoritzar:", + "Select HTTP method" : "Selecciona el mètode HTTP", + "Select Register and Schema" : "Selecciona el registre i l'esquema", + "Select Views to Vectorize:" : "Selecciona les vistes a vectoritzar:", + "Select a Nextcloud group to add" : "Selecciona un grup de Nextcloud per afegir", + "Select a branch" : "Selecciona una branca", + "Select a model or type a custom model name" : "Selecciona un model o escriu un nom de model personalitzat", + "Select a repository" : "Selecciona un repositori", + "Select a repository you have write access to" : "Selecciona un repositori al qual tinguis accés d'escriptura", + "Select an AI Agent" : "Selecciona un agent d'IA", + "Select an AI agent to begin chatting with your data." : "Seleccioneu un agent d'IA per començar a xatejar amb les vostres dades.", + "Select backend" : "Selecciona el backend", + "Select chat model" : "Selecciona el model de xat", + "Select collection for files" : "Selecciona la col·lecció per als fitxers", + "Select collection for objects" : "Selecciona la col·lecció per als objectes", + "Select default organisation" : "Selecciona l'organització per defecte", + "Select event to listen to..." : "Selecciona l'esdeveniment a escoltar...", + "Select model" : "Selecciona el model", + "Select options..." : "Selecciona opcions...", + "Select period" : "Selecciona el període", + "Select property to send as payload" : "Selecciona la propietat a enviar com a càrrega útil", + "Select provider" : "Selecciona el proveïdor", + "Select registers and schemas to save a view" : "Selecciona registres i esquemes per desar una vista", + "Select retry policy" : "Selecciona la política de reintents", + "Select the branch to publish to" : "Selecciona la branca on publicar", + "Select the event this webhook should listen to" : "Selecciona l'esdeveniment que aquest webhook ha d'escoltar", + "Select views to vectorize:" : "Selecciona les vistes a vectoritzar:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Seleccioneu quins grups de Nextcloud estan disponibles per a aquesta organització. Els usuaris d'aquests grups tindran accés als recursos de l'organització.", + "Select which data views the AI can search" : "Selecciona quines vistes de dades pot cercar la IA", + "Select which property from the event should be used as the webhook payload data" : "Selecciona quina propietat de l'esdeveniment s'ha d'utilitzar com a dades de la càrrega útil del webhook", + "Select which tools the AI can use to perform actions" : "Selecciona quines eines pot utilitzar la IA per dur a terme accions", + "Selected" : "Seleccionat", + "Selected Groups" : "Grups seleccionats", + "Selected audit trails deleted successfully" : "Els rastres d'auditoria seleccionats s'han suprimit correctament", + "Selected search trails deleted successfully" : "Els rastres de cerca seleccionats s'han suprimit correctament", + "Selected users can access this view" : "Els usuaris seleccionats poden accedir a aquesta vista", + "Send additional feedback" : "Envia comentaris addicionals", + "Send as CloudEvent" : "Envia com a CloudEvent", + "Sensitive PII" : "PII sensible", + "Serial Mode (Safer, slower)" : "Mode serial (més segur, més lent)", + "Serial:" : "Serial:", + "Server Configuration" : "Configuració del servidor", + "Server Information" : "Informació del servidor", + "Settings" : "Configuració", + "Shards" : "Fragments", + "Share" : "Comparteix", + "Share with Groups" : "Comparteix amb grups", + "Share with Users" : "Comparteix amb usuaris", + "Show Filters" : "Mostra els filtres", + "Show less" : "Mostra menys", + "Show only entries with changes" : "Mostra només les entrades amb canvis", + "Simple" : "Simple", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Consultes simples: cerques de text bàsiques amb paràmetres mínims (p. ex., un sol terme de cerca, sense filtres avançats)", + "Size" : "Mida", + "Slug" : "Slug", + "Soft Delete Mode" : "Mode de supressió temporal", + "Soft Deleted Items" : "Elements suprimits temporalment", + "SolrCloud mode enabled" : "Mode SolrCloud activat", + "Source" : "Font", + "Sources" : "Fonts", + "Sources:" : "Fonts:", + "Standalone SOLR mode" : "Mode SOLR autònom", + "Start Conversation" : "Inicia la conversa", + "Start Vectorization" : "Inicia la vectorització", + "Start a conversation" : "Inicia una conversa", + "Starting names cache warmup..." : "S'està iniciant l'escalfament de la memòria cau de noms...", + "Starting..." : "S'està iniciant...", + "Statistics" : "Estadístiques", + "Stats calculation not yet implemented" : "El càlcul d'estadístiques encara no està implementat", + "Status" : "Estat", + "Status Code" : "Codi d'estat", + "String Constraints" : "Restriccions de cadena", + "Success" : "Èxit", + "Success Rate" : "Taxa d'èxit", + "Success Status" : "Estat d'èxit", + "Successful" : "Correcte", + "Successfully joined organisation" : "Us heu unit a l'organització correctament", + "Sync Table" : "Sincronitza la taula", + "Synchronization Settings" : "Configuració de la sincronització", + "System Default" : "Per defecte del sistema", + "Technical Description" : "Descripció tècnica", + "Technical description for developers and administrators" : "Descripció tècnica per a desenvolupadors i administradors", + "Temperature" : "Temperatura", + "Templates" : "Plantilles", + "Test" : "Prova", + "Test Chat" : "Prova el xat", + "Test Connection" : "Prova la connexió", + "Test Embedding" : "Prova la incrustació", + "Test endpoint executed successfully" : "El punt final de prova s'ha executat correctament", + "Test webhook delivered successfully" : "El webhook de prova s'ha entregat correctament", + "Test webhook delivery failed" : "L'entrega del webhook de prova ha fallat", + "Test webhook sent successfully" : "El webhook de prova s'ha enviat correctament", + "Testing..." : "S'està provant...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Els fragments de text es generen durant l'extracció de fitxers i s'emmagatzemen a la base de dades. La vectorització llegeix aquests fitxers prefragmentats i els converteix en incrustacions.", + "Text extracted successfully" : "El text s'ha extret correctament", + "Text extraction disabled" : "Extracció de text desactivada", + "The URL where webhook events will be sent" : "L'URL on s'enviaran els esdeveniments del webhook", + "The requested conversation does not exist" : "La conversa sol·licitada no existeix", + "The saved view and all its search configuration" : "La vista desada i tota la seva configuració de cerca", + "There are no audit trail entries matching your current filters." : "No hi ha cap entrada del rastre d'auditoria que coincideixi amb els vostres filtres actuals.", + "There are no deleted items matching your current filters." : "No hi ha cap element suprimit que coincideixi amb els vostres filtres actuals.", + "There are no search trail entries matching your current filters." : "No hi ha cap entrada del rastre de cerca que coincideixi amb els vostres filtres actuals.", + "There are no webhook log entries matching your filters." : "No hi ha cap entrada de registre de webhook que coincideixi amb els vostres filtres.", + "This action cannot be undone." : "Aquesta acció no es pot desfer.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Aquesta acció no es pot desfer. Assegureu-vos que cap col·lecció utilitza aquest ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Aquest agent no té vistes ni eines configurables, però encara podeu ajustar la configuració de RAG a continuació.", + "This analysis may take some time" : "Aquesta anàlisi pot trigar una estona", + "This audit trail entry does not contain any change information." : "Aquesta entrada del rastre d'auditoria no conté cap informació de canvis.", + "This configuration has no data defined." : "Aquesta configuració no té cap dada definida.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Aquest punt final és obsolet. Utilitzeu els punts finals basats en fragments en lloc seu.", + "This entity has no relations to objects or files" : "Aquesta entitat no té relacions amb objectes o fitxers", + "This is a test webhook from OpenRegister" : "Aquest és un webhook de prova d'OpenRegister", + "This schema must use Magic Table configuration to sync" : "Aquest esquema ha d'utilitzar la configuració de Magic Table per sincronitzar-se", + "This source has no associated registers." : "Aquesta font no té registres associats.", + "This will copy the _default ConfigSet with the new name" : "Això copiarà el ConfigSet _default amb el nom nou", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Això suprimirà permanentment TOTES les incrustacions (vectors) de la base de dades. Haureu de tornar a vectoritzar tots els objectes i fitxers. Aquesta acció no es pot desfer.\\n\\nEsteu segur que voleu continuar?", + "This will permanently delete:" : "Això suprimirà permanentment:", + "Timeout (seconds)" : "Temps d'espera (segons)", + "Timestamp" : "Marca de temps", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Consell: activeu només les vistes que necessiten cerca semàntica per minimitzar els costos d'incrustació. Les taules de consulta simples rarament necessiten vectorització.", + "Title" : "Títol", + "To Date" : "Fins a la data", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Per ajustar la mida o l'estratègia dels fragments, aneu a Configuració de fitxers → Límits de processament.", + "To change the embedding provider or model, go to LLM Configuration." : "Per canviar el proveïdor o el model d'incrustacions, aneu a Configuració de LLM.", + "To date" : "Fins a la data", + "Toggle search sidebar" : "Commuta la barra lateral de cerca", + "Tools" : "Eines", + "Top Deleters" : "Principals supressors", + "Total" : "Total", + "Total Audit Trails" : "Total de rastres d'auditoria", + "Total Chunks Available:" : "Total de fragments disponibles:", + "Total Deleted Items" : "Total d'elements suprimits", + "Total Members" : "Total de membres", + "Total Objects" : "Total d'objectes", + "Total Objects in Database:" : "Total d'objectes a la base de dades:", + "Total Organisations" : "Total d'organitzacions", + "Total Results" : "Total de resultats", + "Total Searches" : "Total de cerques", + "Total Size" : "Mida total", + "Total Storage" : "Emmagatzematge total", + "Total:" : "Total:", + "Totals" : "Totals", + "Type" : "Tipus", + "Type Issue" : "Problema de tipus", + "Type Variations:" : "Variacions de tipus:", + "Type must be \"positive\" or \"negative\"" : "El tipus ha de ser \"positive\" o \"negative\"", + "Type to search for groups" : "Escriviu per cercar grups", + "Type to search for users" : "Escriviu per cercar usuaris", + "Type to search groups..." : "Escriviu per cercar grups...", + "Type to search users..." : "Escriviu per cercar usuaris...", + "URL" : "URL", + "URL where Ollama is running" : "URL on s'està executant Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Sense canvis", + "Unique Search Terms" : "Termes de cerca únics", + "Unique Terms" : "Termes únics", + "Unknown" : "Desconegut", + "Unknown Register" : "Registre desconegut", + "Unknown Schema" : "Esquema desconegut", + "Untitled View" : "Vista sense títol", + "Update" : "Actualitza", + "Update Operations" : "Operacions d'actualització", + "Update configuration: ..." : "Actualitza la configuració: ...", + "Update register OAS: ..." : "Actualitza l'OAS del registre: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Actualitza els vectors quan canvien les dades de l'objecte (recomanat per a una cerca precisa)", + "Updated" : "Actualitzat", + "Updated At" : "Actualitzat a", + "Updated:" : "Actualitzat:", + "Uptime:" : "Temps de funcionament:", + "Usage Count" : "Recompte d'ús", + "Use SolrCloud with Zookeeper for distributed search" : "Utilitza SolrCloud amb Zookeeper per a la cerca distribuïda", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Utilitzeu els filtres per acotar les entrades del rastre d'auditoria per registre, esquema, tipus d'acció, usuari, interval de dates o identificador d'objecte.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Utilitzeu els filtres per acotar els elements suprimits per registre, esquema, data de supressió o usuari que els ha suprimit.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Utilitzeu els filtres per acotar les entrades del rastre de cerca per registre, esquema, estat d'èxit, usuari, interval de dates, termes de cerca o mètriques de rendiment.", + "Used By Collections" : "Utilitzat per col·leccions", + "User" : "Usuari", + "User Agent Statistics" : "Estadístiques de l'agent d'usuari", + "User Description" : "Descripció de l'usuari", + "User rate limits cleared successfully" : "Els límits de freqüència de l'usuari s'han esborrat correctament", + "User removed successfully" : "L'usuari s'ha eliminat correctament", + "User-friendly description shown in forms and help text" : "Descripció amigable mostrada als formularis i al text d'ajuda", + "Username" : "Nom d'usuari", + "Username for SOLR authentication (optional)" : "Nom d'usuari per a l'autenticació de SOLR (opcional)", + "Users" : "Usuaris", + "Using Pre-Generated Chunks" : "S'estan utilitzant fragments pregenerats", + "Validate" : "Valida", + "Validate Objects" : "Valida els objectes", + "Validation completed successfully" : "La validació s'ha completat correctament", + "Validation failed" : "La validació ha fallat", + "Value" : "Valor", + "Vector Dimensions" : "Dimensions del vector", + "Vector Search Backend" : "Backend de cerca vectorial", + "Vector field: _embedding_" : "Camp vectorial: _embedding_", + "Vectorization Triggers" : "Activadors de la vectorització", + "Vectorize All Files" : "Vectoritza tots els fitxers", + "Vectorize All Objects" : "Vectoritza tots els objectes", + "Vectorize all file types" : "Vectoritza tots els tipus de fitxer", + "Vectorize all views" : "Vectoritza totes les vistes", + "Vectorize on object creation" : "Vectoritza en crear l'objecte", + "Vectors will be stored in your existing object and file collections" : "Els vectors s'emmagatzemaran a les vostres col·leccions d'objectes i fitxers existents", + "Version" : "Versió", + "Very High" : "Molt alt", + "View API Docs" : "Visualitza la documentació de l'API", + "View Changes" : "Visualitza els canvis", + "View Details" : "Visualitza els detalls", + "View Error" : "Visualitza l'error", + "View File" : "Visualitza el fitxer", + "View Full Details" : "Visualitza tots els detalls", + "View Logs" : "Visualitza els registres", + "View Name" : "Nom de la vista", + "View Object" : "Visualitza l'objecte", + "View Parameters" : "Visualitza els paràmetres", + "View Selection" : "Selecció de vistes", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Visualitza i analitza els registres del rastre de cerca amb capacitats avançades de filtratge i analítiques", + "View and analyze system audit trails with advanced filtering capabilities" : "Visualitza i analitza els rastres d'auditoria del sistema amb capacitats avançades de filtratge", + "View deleted successfully" : "La vista s'ha suprimit correctament", + "View entity details and manage relations" : "Visualitza els detalls de l'entitat i gestiona les relacions", + "View name" : "Nom de la vista", + "View name is required" : "El nom de la vista és obligatori", + "View saved successfully!" : "La vista s'ha desat correctament!", + "View search analytics and manage search logs" : "Visualitza les analítiques de cerca i gestiona els registres de cerca", + "View successfully deleted" : "La vista s'ha suprimit correctament", + "View successfully updated" : "La vista s'ha actualitzat correctament", + "View updated successfully!" : "La vista s'ha actualitzat correctament!", + "View webhook delivery logs and filter by webhook" : "Visualitza els registres d'entrega del webhook i filtra per webhook", + "Views" : "Vistes", + "Visible to users" : "Visible per als usuaris", + "Wait for Response" : "Espera la resposta", + "Wait for webhook response before continuing (required for request/response flows)" : "Espera la resposta del webhook abans de continuar (obligatori per als fluxos de sol·licitud/resposta)", + "Warmup Names Cache" : "Escalfa la memòria cau de noms", + "Warning" : "Advertiment", + "Warning:" : "Advertiment:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detalls del registre del webhook", + "Webhook Logs" : "Registres del webhook", + "Webhook created successfully" : "El webhook s'ha creat correctament", + "Webhook deleted" : "Webhook suprimit", + "Webhook retry delivered successfully" : "El reintent del webhook s'ha entregat correctament", + "Webhook retry delivery failed" : "L'entrega del reintent del webhook ha fallat", + "Webhook updated" : "Webhook actualitzat", + "Webhook updated successfully" : "El webhook s'ha actualitzat correctament", + "Webhooks" : "Webhooks", + "Weekly" : "Setmanalment", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Embolcalla la càrrega útil del webhook en format CloudEvents per a una millor interoperabilitat", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Sí", + "You" : "Vós", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Podeu crear ConfigSets nous basats en la plantilla _default, o pujar-ne de personalitzats directament al vostre servidor SOLR.", + "You do not have access to this conversation" : "No teniu accés a aquesta conversa", + "You do not have permission to delete this conversation" : "No teniu permís per suprimir aquesta conversa", + "You do not have permission to modify this conversation" : "No teniu permís per modificar aquesta conversa", + "You do not have permission to restore this conversation" : "No teniu permís per restaurar aquesta conversa", + "You must be logged in to favorite views" : "Heu d'haver iniciat sessió per marcar vistes com a preferides", + "You need an AI agent to start a conversation." : "Necessiteu un agent d'IA per iniciar una conversa.", + "Your Fireworks AI API key. Get one at" : "La vostra clau de l'API de Fireworks AI. Obteniu-ne una a", + "Your OpenAI API key. Get one at" : "La vostra clau de l'API d'OpenAI. Obteniu-ne una a", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Els vostres comentaris s'han registrat. Opcionalment, podeu proporcionar detalls addicionals aquí...", + "Zookeeper Hosts" : "Amfitrions de Zookeeper", + "Zookeeper Port" : "Port de Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Configuració de Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Cadena de connexió de Zookeeper per a SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Número de port de Zookeeper (opcional, per defecte 2181)", + "chunk" : "fragment", + "chunks" : "fragments", + "chunks processed" : "fragments processats", + "configuration(s)" : "configuració/ons", + "e.g., lib/Settings/config.json" : "p. ex., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "p. ex., lib/Settings/register.json", + "failed" : "fallit", + "file" : "fitxer", + "files" : "fitxers", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "membres", + "menu or contact someone with permission to create agents." : "menú o contacteu amb algú que tingui permís per crear agents.", + "messages" : "missatges", + "n8n URL and API key are required" : "L'URL i la clau de l'API de n8n són obligatòries", + "n8n configuration saved successfully" : "La configuració de n8n s'ha desat correctament", + "n8n connection not configured" : "La connexió de n8n no està configurada", + "n8n connection successful" : "Connexió de n8n correcta", + "n8n connection test successful" : "La prova de connexió de n8n s'ha completat correctament", + "n8n integration disabled" : "Integració de n8n desactivada", + "n8n integration enabled" : "Integració de n8n activada", + "n8n project initialized successfully" : "El projecte de n8n s'ha inicialitzat correctament", + "n8n settings saved successfully" : "La configuració de n8n s'ha desat correctament", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objectes", + "objects processed" : "objectes processats", + "or pick a range" : "o trieu un interval", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "proporciona un bon equilibri entre velocitat i precisió.", + "register(s)" : "registre/s", + "register(s) selected" : "registre/s seleccionat/s", + "required" : "obligatori", + "results" : "resultats", + "schema(s)" : "esquema/es", + "schema(s) selected" : "esquema/es seleccionat/s", + "sk-..." : "sk-...", + "this application" : "aquesta aplicació", + "to" : "fins a", + "vectorized" : "vectoritzat", + "views selected" : "vistes seleccionades", + "ℹ️ Current Configuration" : "ℹ️ Configuració actual", + "⚡ Batch Processing" : "⚡ Processament per lots", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Context complet ✓ Menys probable que es perdin detalls ✗ Respostes més lentes ✗ Pot incloure informació menys rellevant", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Respostes més ràpides ✓ Respostes més enfocades ✗ Pot perdre informació rellevant", + "✨ AI Features" : "✨ Funcions d'IA", + "All search trails cleared successfully" : "Tots els rastres de cerca s'han esborrat correctament", + "Multiple search trail deletion not implemented yet" : "La supressió de múltiples rastres de cerca encara no està implementada", + "No expired search trails found to clear" : "No s'ha trobat cap rastre de cerca caducat per esborrar", + "Missing message" : "Falta el missatge", + "message content is required" : "el contingut del missatge és obligatori", + "Missing conversation or agentUuid" : "Falta la conversa o agentUuid", + "Access denied" : "Accés denegat", + "Conversation not found" : "No s'ha trobat la conversa", + "AI service not configured" : "El servei d'IA no està configurat", + "Failed to process message" : "No s'ha pogut processar el missatge", + "Missing conversationId" : "Falta conversationId", + "conversationId is required" : "conversationId és obligatori", + "Failed to fetch conversation history" : "No s'ha pogut recuperar l'historial de la conversa", + "Failed to clear conversation" : "No s'ha pogut esborrar la conversa", + "Invalid feedback type" : "Tipus de comentari no vàlid", + "type must be \"positive\" or \"negative\"" : "el tipus ha de ser \"positive\" o \"negative\"", + "Message not found" : "No s'ha trobat el missatge", + "Failed to save feedback" : "No s'han pogut desar els comentaris", + "Failed to get chat statistics" : "No s'han pogut obtenir les estadístiques del xat", + "Not Found" : "No s'ha trobat", + "Engine not found" : "No s'ha trobat el motor", + "Consumer not found" : "No s'ha trobat el consumidor", + "Backend parameter is required" : "El paràmetre del backend és obligatori", + "Failed to get database information: %s" : "No s'ha pogut obtenir la informació de la base de dades: %s", + "SOLR setup error: %s" : "Error de configuració de SOLR: %s", + "Reindex failed: %s" : "La reindexació ha fallat: %s", + "Anonymized" : "Anonimitzat", + "Entities detected" : "Entitats detectades", + "Extract Now" : "Extreu ara", + "Extracted at" : "Extret a", + "Extraction" : "Extracció", + "Failed to load extraction data" : "No s'han pogut carregar les dades d'extracció", + "Failed to load register data" : "No s'han pogut carregar les dades del registre", + "Failed to retrieve extraction status." : "No s'ha pogut recuperar l'estat de l'extracció.", + "Failed to retrieve objects for file." : "No s'han pogut recuperar els objectes del fitxer.", + "No extraction data available for this file" : "No hi ha dades d'extracció disponibles per a aquest fitxer", + "No register objects reference this file" : "Cap objecte del registre fa referència a aquest fitxer", + "Not extracted" : "No extret", + "Register Objects" : "Objectes del registre", + "Risk level" : "Nivell de risc", + "Text chunks" : "Fragments de text", + "Unknown error" : "Error desconegut", + "Very high" : "Molt alt", + "{title} in {register} / {schema}" : "{title} a {register} / {schema}", + "Push Notifications" : "Notificacions push", + "Real-time push notification status via notify_push" : "Estat de les notificacions push en temps real mitjançant notify_push", + "Realtime push not available — the notify_push app is not installed" : "Les notificacions push en temps real no estan disponibles: l'aplicació notify_push no està instal·lada", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Instal·leu l'aplicació notify_push des de la botiga d'aplicacions de Nextcloud per activar les actualitzacions en temps real.", + "Open Nextcloud App Store" : "Obre la botiga d'aplicacions de Nextcloud", + "notify_push is installed but not yet active" : "notify_push està instal·lat però encara no està actiu", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push està instal·lat però OpenRegister encara no ha confirmat un push correcte. Activeu un desament d'objecte per activar-lo, o comproveu la vostra configuració de notify_push.", + "notify_push configuration guide" : "guia de configuració de notify_push", + "Realtime push active" : "Notificacions push en temps real actives", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Les notificacions push en temps real estan actives. Els clients connectats reben actualitzacions instantànies quan es creen, s'actualitzen o se suprimeixen objectes.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/ca.json b/l10n/ca.json new file mode 100644 index 0000000000..59f609fa18 --- /dev/null +++ b/l10n/ca.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Es requereix Dolphin)", + "(no subject)": "(sense assumpte)", + "(no title)": "(sense títol)", + "(unnamed)": "(sense nom)", + "(untitled card)": "(targeta sense títol)", + "({count} selected)": "({count} seleccionats)", + ", {failed} failed": ", {failed} fallits", + "0 = unlimited": "0 = il·limitat", + "A deactivation request is pending.": "Hi ha una sol·licitud de desactivació pendent.", + "AI Assistant": "Assistent d'IA", + "AI Chat": "Xat d'IA", + "AI service not configured": "El servei d'IA no està configurat", + "API Key": "Clau d'API", + "API Request Quota (requests/day)": "Quota de sol·licituds d'API (sol·licituds/dia)", + "API Token Configuration": "Configuració de tokens d'API", + "API Tokens": "Tokens d'API", + "API tokens saved successfully": "Els tokens d'API s'han desat correctament", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: aquest informe uneix cada activitat de tractament amb els comptadors de rastres d'auditoria associats. Els auditors i l'Autoriteit Persoonsgegevens l'utilitzen per a la revisió supervisora.", + "About ConfigSets": "Sobre els ConfigSets", + "Access denied": "Accés denegat", + "Access organisation dashboard and analytics": "Accedir al tauler i les analítiques de l'organització", + "Account": "Compte", + "Action": "Acció", + "Action Distribution": "Distribució d'accions", + "Action:": "Acció:", + "Actions": "Accions", + "Active": "Actiu", + "Active Collections": "Col·leccions actives", + "Active Filters": "Filtres actius", + "Active Organisation:": "Organització activa:", + "Active Organisations": "Organitzacions actives", + "Active filters:": "Filtres actius:", + "Active organisation changed successfully": "L'organització activa s'ha canviat correctament", + "Active organisation set successfully": "L'organització activa s'ha establert correctament", + "Activities": "Activitats", + "Activity": "Activitat", + "Activity Period": "Període d'activitat", + "Add": "Afegir", + "Add Attachment": "Afegir adjunt", + "Add Endpoint": "Afegir punt d'accés", + "Add Groups": "Afegir grups", + "Add Property to '{title}'": "Afegir propietat a '{title}'", + "Add Register": "Afegir registre", + "Add Schema": "Afegir esquema", + "Add Source": "Afegir font", + "Add User": "Afegir usuari", + "Add User to Organisation": "Afegir usuari a l'organització", + "Add a contact from any of your address books to associate it with this object.": "Afegiu un contacte de qualsevol dels vostres llibres d'adreces per associar-lo a aquest objecte.", + "Add a file or files": "Afegir un fitxer o fitxers", + "Add card": "Afegir targeta", + "Add contact": "Afegir contacte", + "Add endpoint": "Afegir punt d'accés", + "Add oneOf entry": "Afegir entrada oneOf", + "Add range": "Afegir rang", + "Add schema titles, descriptions, and register information to provide richer context for search": "Afegiu títols d'esquema, descripcions i informació del registre per proporcionar un context més ric a la cerca", + "Add to favorites": "Afegir als preferits", + "Added": "Afegit", + "Added to favorites": "Afegit als preferits", + "Additional Information": "Informació addicional", + "Additional feedback saved. Thank you!": "Comentaris addicionals desats. Gràcies!", + "Addresses": "Adreces", + "Admin": "Administrador", + "Advanced": "Avançat", + "Advanced Filters": "Filtres avançats", + "Advanced Options": "Opcions avançades", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Els filtres avançats no estan disponibles quan s'utilitza la font de base de dades. Canvieu a Auto o Índex SOLR per a les opcions de filtratge.", + "After": "Després", + "Agent Type": "Tipus d'agent", + "Agent deleted successfully": "L'agent s'ha eliminat correctament", + "Agent successfully created": "L'agent s'ha creat correctament", + "Agent successfully updated": "L'agent s'ha actualitzat correctament", + "Agents": "Agents", + "Aggregated across schemas": "Agregat entre esquemes", + "All": "Tot", + "All Categories": "Totes les categories", + "All Collections": "Totes les col·leccions", + "All Confidence Levels": "Tots els nivells de confiança", + "All ConfigSets": "Tots els ConfigSets", + "All Files": "Tots els fitxers", + "All Levels": "Tots els nivells", + "All Types": "Tots els tipus", + "All Webhooks": "Tots els webhooks", + "All actions": "Totes les accions", + "All audit trails cleared successfully": "Tots els rastres d'auditoria s'han esborrat correctament", + "All clear": "Tot net", + "All file types": "Tots els tipus de fitxers", + "All registers": "Tots els registres", + "All schemas": "Tots els esquemes", + "All search trails cleared successfully": "Tots els rastres de cerca s'han esborrat correctament", + "All searches": "Totes les cerques", + "All users": "Tots els usuaris", + "All-day events": "Esdeveniments de tot el dia", + "Allowed MIME Types": "Tipus MIME permesos", + "Already linked": "Ja enllaçat", + "An error occurred while clearing audit trails": "S'ha produït un error en esborrar els rastres d'auditoria", + "An error occurred while deleting the audit trail": "S'ha produït un error en eliminar el rastre d'auditoria", + "An error occurred while deleting the object": "S'ha produït un error en eliminar l'objecte", + "An error occurred while deleting the view": "S'ha produït un error en eliminar la vista", + "An error occurred while permanently deleting the objects": "S'ha produït un error en eliminar permanentment els objectes", + "An error occurred while restoring the objects": "S'ha produït un error en restaurar els objectes", + "An error occurred while saving the organisation": "S'ha produït un error en desar l'organització", + "Analysis completed:": "Anàlisi completada:", + "Analysis steps:": "Passos d'anàlisi:", + "Analytics": "Analítiques", + "Analyze Objects": "Analitzar objectes", + "Analyze Properties": "Analitzar propietats", + "Analyze existing properties for improvement opportunities": "Analitzar les propietats existents per trobar oportunitats de millora", + "Analyzing...": "Analitzant...", + "Anonymized": "Anonimitzat", + "Anonymous Group": "Grup anònim", + "Any favorites and sharing settings for this view": "Qualsevol preferit i configuració de compartició per a aquesta vista", + "Any user": "Qualsevol usuari", + "App ID": "ID d'aplicació", + "App store cache invalidated successfully": "La memòria cau de la botiga d'aplicacions s'ha invalidat correctament", + "App store cache invalidated: {invalidated}": "Memòria cau de la botiga d'aplicacions invalidada: {invalidated}", + "Application deleted successfully": "L'aplicació s'ha eliminat correctament", + "Application identifier for this configuration (optional)": "Identificador d'aplicació per a aquesta configuració (opcional)", + "Application settings": "Configuració de l'aplicació", + "Application successfully created": "L'aplicació s'ha creat correctament", + "Application successfully updated": "L'aplicació s'ha actualitzat correctament", + "Applications": "Aplicacions", + "Apply": "Aplicar", + "Apply Changes": "Aplicar canvis", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Apliqueu un rol a tots els esquemes d'aquest registre que no tinguin substitucions d'autorització explícites.", + "Approval Progress": "Progrés d'aprovació", + "Approve": "Aprovar", + "Approved": "Aprovat", + "Archive": "Arxiu", + "Archive conversation": "Arxivar conversa", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arxiveu aquesta verwerkingsactiviteit? Les files del rastre d'auditoria continuaran fent referència a ella.", + "Archived conversations are hidden from your active list": "Les converses arxivades estan ocultes de la llista activa", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Esteu segur que voleu ELIMINAR la col·lecció \"{name}\"?\n\nAixò:\n• Eliminarà permanentment la col·lecció i totes les seves dades\n• Eliminarà tots els documents indexats\n• Aquesta acció no es pot desfer", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Esteu segur que voleu netejar els rastres de cerca antics? Això eliminarà les entrades de més de 30 dies.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Esteu segur que voleu esborrar totes les dades de la col·lecció \"{name}\"?\n\nAixò:\n• Eliminarà tots els documents indexats\n• Mantindrà intacta l'estructura de la col·lecció\n• Aquesta acció no es pot desfer", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Esteu segur que voleu eliminar l'aplicació \"{name}\"? Aquesta acció no es pot desfer.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Esteu segur que voleu eliminar els rastres d'auditoria seleccionats? Aquesta acció no es pot desfer.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Esteu segur que voleu eliminar els rastres de cerca seleccionats? Aquesta acció no es pot desfer.", + "Are you sure you want to delete this ConfigSet?": "Esteu segur que voleu eliminar aquest ConfigSet?", + "Are you sure you want to delete this configset?": "Esteu segur que voleu eliminar aquest configset?", + "Are you sure you want to leave '{name}'?": "Esteu segur que voleu sortir de '{name}'?", + "Are you sure you want to permanently delete": "Esteu segur que voleu eliminar permanentment", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Esteu segur que voleu reindexar la col·lecció \"{name}\"?\n\nAixò:\n• Reconstruirà l'índex amb tots els objectes\n• Pot tardar uns quants minuts\n• Pot afectar el rendiment de la cerca durant la reindexació", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Esteu segur que voleu eliminar l'esquema \"{schema}\" del registre \"{register}\"? Aquesta acció no es pot desfer.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Esteu segur que voleu fer una supressió suau de {count} objectes actius per a l'esquema \"{schema}\"?\n\nEs marcaran com a eliminats però podran eliminar-se permanentment més endavant.", + "Array Object Configuration:": "Configuració d'objecte matriu:", + "Articles": "Articles", + "Ask a question...": "Feu una pregunta...", + "Ask questions about your data using natural language": "Feu preguntes sobre les vostres dades en llenguatge natural", + "Assigned collaborative tags": "Etiquetes col·laboratives assignades", + "Assignment notifications": "Notificacions d'assignació", + "Attachment added to {name}": "Adjunt afegit a {name}", + "Attempt": "Intent", + "Audit Trail Changes": "Canvis del rastre d'auditoria", + "Audit Trail Details": "Detalls del rastre d'auditoria", + "Audit Trail Management": "Gestió del rastre d'auditoria", + "Audit Trail Statistics": "Estadístiques del rastre d'auditoria", + "Audit Trails": "Rastres d'auditoria", + "Audit trail data copied to clipboard": "Les dades del rastre d'auditoria s'han copiat al porta-retalls", + "Audit trail deleted successfully": "El rastre d'auditoria s'ha eliminat correctament", + "Audit trail successfully deleted": "El rastre d'auditoria s'ha eliminat correctament", + "Authentication": "Autenticació", + "Auto (e.g. Y for year, F Y for month)": "Auto (p. ex. Y per a any, F Y per a mes)", + "Auto share": "Compartició automàtica", + "Auto-commit disabled": "Confirmació automàtica desactivada", + "Auto-commit enabled": "Confirmació automàtica activada", + "Auto-create Default Organisation": "Crear automàticament l'organització per defecte", + "Auto-retry failed vectorizations": "Reintentar automàticament les vectoritzacions fallides", + "Automatically commit changes to SOLR index": "Confirmar automàticament els canvis a l'índex SOLR", + "Automatically connect to the federation network.": "Connectar automàticament a la xarxa de federació.", + "Automatically create a default organisation if none exists when the app is initialized": "Crear automàticament una organització per defecte si no n'hi ha cap quan s'inicialitza l'aplicació", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Generar automàticament incrustacions vectorials a partir de fragments de text quan es pugen i processen fitxers", + "Automatically generate vector embeddings when objects are created or updated": "Generar automàticament incrustacions vectorials quan es creen o actualitzen objectes", + "Automatically retry failed vectorization attempts (max 3 retries)": "Reintentar automàticament els intents de vectorització fallits (màx. 3 reintents)", + "Automatically update catalogues.": "Actualitzar catàlegs automàticament.", + "Available Filters": "Filtres disponibles", + "Available Workflows": "Fluxos de treball disponibles", + "Available placeholders:": "Marcadors de posició disponibles:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "El vostre proveïdor d'autenticació no admet canvis d'avatar.", + "Avatar removed": "Avatar eliminat", + "Avatar updated successfully": "L'avatar s'ha actualitzat correctament", + "Avg Execution Time": "Temps d'execució mitjà", + "Avg Members/Org": "Membres mitjans/Org", + "Avg Object Views/Session": "Visualitzacions d'objectes mitjanes/Sessió", + "Avg Response Time": "Temps de resposta mitjà", + "Avg Results/Search": "Resultats mitjans/Cerca", + "Avg Searches/Session": "Cerques mitjanes/Sessió", + "Back": "Enrere", + "Back to Entities": "Tornar a entitats", + "Back to Registers": "Tornar als registres", + "Back to Webhooks": "Tornar als webhooks", + "Back to applications": "Tornar a les aplicacions", + "Back to entities": "Tornar a entitats", + "Backend parameter is required": "El paràmetre de backend és obligatori", + "Backend updated successfully. Please reload the application.": "El backend s'ha actualitzat correctament. Recarregueu l'aplicació.", + "Bandwidth Quota (MB/month)": "Quota d'ample de banda (MB/mes)", + "Base URL (Optional)": "URL base (opcional)", + "Basic Information": "Informació bàsica", + "Batch Size": "Mida del lot", + "Batch extraction completed": "L'extracció per lots s'ha completat", + "Before": "Abans", + "Before object vectorization can work:": "Abans que la vectorització d'objectes pugui funcionar:", + "Behavior": "Comportament", + "Behavior Issue": "Problema de comportament", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (durada ISO-8601, p. ex. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "L'emmagatzematge blob ha quedat obsolet. Tots els objectes utilitzen ara taules màgiques.", + "Bookmarks": "Adreces d'interès", + "Branch": "Branca", + "Bulk Role Assignment: {title}": "Assignació massiva de rols: {title}", + "Bulk delete operation completed successfully": "L'operació d'eliminació massiva s'ha completat correctament", + "Bulk save operation completed successfully": "L'operació de desat massiu s'ha completat correctament", + "Business Data": "Dades empresarials", + "CSV": "CSV", + "Cache cleared successfully": "La memòria cau s'ha esborrat correctament", + "Cache grew from {old} to {new} entries.": "La memòria cau ha crescut de {old} a {new} entrades.", + "Cache shrunk from {old} to {new} entries.": "La memòria cau ha disminuït de {old} a {new} entrades.", + "Cache stayed the same at {size} entries.": "La memòria cau s'ha mantingut igual a {size} entrades.", + "Calculate Sizes": "Calcular mides", + "Calendar": "Calendari", + "Calendar Provider Configuration": "Configuració del proveïdor de calendari", + "Calendar integration is not available": "La integració de calendari no està disponible", + "Calendar name": "Nom del calendari", + "Cancel": "Cancel·lar", + "Cancel deactivation request": "Cancel·lar la sol·licitud de desactivació", + "Cannot delete: objects are still attached": "No es pot eliminar: els objectes encara estan adjunts", + "Cannot edit: This register is managed by external configuration {title}": "No es pot editar: aquest registre és gestionat per la configuració externa {title}", + "Cannot remove schema with existing objects ({count} objects)": "No es pot eliminar l'esquema amb objectes existents ({count} objectes)", + "Cannot remove schema {schema} because it contains {count} objects": "No es pot eliminar l'esquema {schema} perquè conté {count} objectes", + "Cards": "Targetes", + "Cascade delete": "Eliminació en cascada", + "Categories of data subjects (one per line)": "Categories de persones interessades (una per línia)", + "Categories of personal data (one per line)": "Categories de dades personals (una per línia)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Categoria", + "Change Type": "Canviar tipus", + "Change password": "Canviar la contrasenya", + "Changes": "Canvis", + "Changes copied to clipboard": "Els canvis s'han copiat al porta-retalls", + "Changes for Audit Trail #{id}": "Canvis per al rastre d'auditoria #{id}", + "Chat Model": "Model de xat", + "Chat Provider": "Proveïdor de xat", + "Chat Provider (RAG)": "Proveïdor de xat (RAG)", + "Chat Settings": "Configuració del xat", + "Chat provider connection successful!": "La connexió amb el proveïdor de xat ha estat correcta!", + "Chat settings": "Configuració del xat", + "Chat test failed: {error}": "La prova de xat ha fallat: {error}", + "Choose a register": "Trieu un registre", + "Choose a schema": "Trieu un esquema", + "Choose how vector similarity calculations are performed for semantic search": "Trieu com es realitzen els càlculs de similitud vectorial per a la cerca semàntica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Trieu quins tipus de fitxers incloure en el procés de vectorització. Només es processaran els fitxers amb text extret i fragments.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Trieu quines vistes incloure en el procés de vectorització. Deixeu-ho buit per processar totes les vistes segons la vostra configuració.", + "Chunk": "Fragment", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "L'eliminació de fragments encara no s'ha implementat. Utilitzeu punts d'accés basats en fragments.", + "Chunks": "Fragments", + "Chunks to Vectorize:": "Fragments per vectoritzar:", + "Cleanup Old Trails": "Netejar rastres antics", + "Cleanup completed": "Neteja completada", + "Cleanup completed successfully. Deleted {count} entries.": "Neteja completada correctament. S'han eliminat {count} entrades.", + "Cleanup failed: {error}": "La neteja ha fallat: {error}", + "Clear All Embeddings": "Esborrar totes les incrustacions", + "Clear Entries": "Esborrar entrades", + "Clear Filtered Audit Trails": "Esborrar rastres d'auditoria filtrats", + "Clear Filters": "Esborrar filtres", + "Clear Index": "Esborrar índex", + "Clear Selection": "Esborrar selecció", + "Clear all": "Esborrar tot", + "Clear all filters": "Esborrar tots els filtres", + "Clear date range": "Esborrar rang de dates", + "Clear filters": "Esborrar filtres", + "Clearing...": "Esborrant...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Feu clic a Executar escaneig de compliment per trobar esquemes on s'ha detectat PII però no hi ha cap anotació d'activitat de processament.", + "Click to subscribe to notifications": "Feu clic per subscriure-us a les notificacions", + "Click to unsubscribe from notifications": "Feu clic per cancel·lar la subscripció a les notificacions", + "Close": "Tancar", + "Cluster name": "Nom del clúster", + "Code": "Codi", + "Code (short readable key, e.g. v-2026-001)": "Codi (clau llegible curta, p. ex. v-2026-001)", + "Collapse details": "Col·lapsar detalls", + "Collection Management": "Gestió de col·leccions", + "Collection Name": "Nom de la col·lecció", + "Collection assignments updated successfully": "Les assignacions de la col·lecció s'han actualitzat correctament", + "Collection cleared successfully": "La col·lecció s'ha esborrat correctament", + "Collection copied successfully": "La col·lecció s'ha copiat correctament", + "Collection created successfully": "La col·lecció s'ha creat correctament", + "Collection deleted successfully": "La col·lecció s'ha eliminat correctament", + "Collection used to store and index file metadata and content": "Col·lecció utilitzada per emmagatzemar i indexar metadades i contingut de fitxers", + "Collection used to store and index object data": "Col·lecció utilitzada per emmagatzemar i indexar dades d'objectes", + "Collections:": "Col·leccions:", + "Color": "Color", + "Comment...": "Comentari...", + "Commit Message": "Missatge de confirmació", + "Commit Within (ms)": "Confirmar dins de (ms)", + "Commit message": "Missatge de confirmació", + "Compare current schema with real object data": "Comparar l'esquema actual amb les dades reals dels objectes", + "Completed": "Completat", + "Complex": "Complex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Consultes complexes: cerques avançades amb múltiples filtres, operadors i combinacions de paràmetres complexos", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Consultes complexes: cerques avançades amb múltiples filtres, operadors i combinacions de paràmetres complexos", + "Compliance": "Compliment", + "Confidence Level": "Nivell de confiança", + "Confidence Score": "Puntuació de confiança", + "Confidence: {confidence}%": "Confiança: {confidence}%", + "Config must be provided as an object": "La configuració s'ha de proporcionar com un objecte", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Gestió de ConfigSets", + "ConfigSet Name": "Nom del ConfigSet", + "ConfigSet created successfully": "El ConfigSet s'ha creat correctament", + "ConfigSet deleted successfully": "El ConfigSet s'ha eliminat correctament", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Els ConfigSets defineixen l'esquema i la configuració de les vostres col·leccions SOLR. Contenen definicions de camps, analitzadors i altres configuracions de cerca.", + "Configuration": "Configuració", + "Configuration Data": "Dades de configuració", + "Configuration is up to date": "La configuració està actualitzada", + "Configuration saved": "Configuració desada", + "Configuration saved successfully.": "La configuració s'ha desat correctament.", + "Configuration type (default, application, etc.)": "Tipus de configuració (per defecte, aplicació, etc.)", + "Configurations": "Configuracions", + "Configure API tokens for external service integrations": "Configurar tokens d'API per a integracions de serveis externs", + "Configure Apache SOLR search engine for advanced search capabilities": "Configurar el motor de cerca Apache SOLR per a capacitats de cerca avançades", + "Configure Facets": "Configurar facetes", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Configurar proveïdors de models de llenguatge gran (LLM) per a funcions amb IA, inclosa la cerca semàntica, les incrustacions i el xat.", + "Configure SOLR Facets": "Configurar facetes SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Configurar els paràmetres de connexió bàsics del servidor SOLR, inclosa l'autenticació i les opcions de xarxa. Utilitzeu els diàlegs separats de Gestió de ConfigSet i Col·lecció per gestionar nuclis i col·leccions.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configurar els paràmetres de connexió bàsics del servidor SOLR, inclosa l'autenticació i les opcions de xarxa. Utilitzeu els diàlegs separats de gestió de configset i col·lecció per gestionar nuclis i col·leccions.", + "Configure default organisation and organisation-related settings": "Configurar l'organització per defecte i la configuració relacionada amb l'organització", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Configurar com es converteixen els objectes de la base de dades en incrustacions vectorials per a la cerca semàntica. Els objectes es vectoritzen directament sense necessitat d'extracció de text.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Configurar com es converteixen els objectes a text abans de la vectorització. Aquests paràmetres afecten la qualitat de la cerca i el context.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configurar proveïdors de models de llenguatge gran (LLM) per a funcions amb IA, inclosa la cerca semàntica, les incrustacions i el xat.", + "Configure multi-organization support and tenant isolation": "Configurar el suport multi-organització i l'aïllament d'inquilins", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Configurar els paràmetres per a la vectorització de fitxers. Aquest procés generarà incrustacions vectorials per a tots els fragments de fitxers extrets.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Configurar els paràmetres per a la vectorització d'objectes. Aquest procés generarà incrustacions vectorials per a tots els objectes que coincideixin amb els filtres de la vostra vista.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configurar aquest esquema per mostrar objectes com a esdeveniments a l'aplicació de Calendari de Nextcloud.", + "Configure which types of data to search and how many sources to retrieve": "Configurar quins tipus de dades cercar i quantes fonts recuperar", + "Configuring field mappings...": "Configurant mapatges de camps...", + "Confirm Account Deactivation": "Confirmar la desactivació del compte", + "Confirm Object Deletion": "Confirmar l'eliminació de l'objecte", + "Confirm Object Restoration": "Confirmar la restauració de l'objecte", + "Confirm Permanent Object Deletion": "Confirmar l'eliminació permanent de l'objecte", + "Confirm deactivation": "Confirmar la desactivació", + "Connecting to SOLR cluster...": "Connectant al clúster SOLR...", + "Connection Failed": "La connexió ha fallat", + "Connection Settings": "Configuració de la connexió", + "Connection Successful!": "La connexió ha estat correcta!", + "Connection failed": "La connexió ha fallat", + "Connection protocol": "Protocol de connexió", + "Connection successful": "La connexió ha estat correcta", + "Connection timeout in seconds": "Temps d'espera de la connexió en segons", + "Constant delay between retries (5 minutes)": "Retard constant entre reintents (5 minuts)", + "Constraint Issue": "Problema de restricció", + "Consumer not found": "No s'ha trobat el consumidor", + "Contacts": "Contactes", + "Contacts integration is not available": "La integració de contactes no està disponible", + "Content Filters": "Filtres de contingut", + "Continue to Properties": "Continuar a propietats", + "Contracts": "Contractes", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Controleu quines vistes d'objectes s'han de vectoritzar per reduir els costos d'API. Vectoritzeu només les vistes que es beneficiïn de la cerca semàntica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Controleu quines vistes i eines pot utilitzar la IA en aquesta conversa. Per defecte, totes les capacitats de l'agent estan activades.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Controla l'aleatorietat (0 = determinista, 2 = molt creatiu)", + "Conversation ID is required": "L'ID de conversa és obligatori", + "Conversation archived": "Conversa arxivada", + "Conversation archived successfully": "La conversa s'ha arxivat correctament", + "Conversation cleared successfully": "La conversa s'ha esborrat correctament", + "Conversation deleted": "Conversa eliminada", + "Conversation not found": "No s'ha trobat la conversa", + "Conversation permanently deleted": "Conversa eliminada permanentment", + "Conversation renamed": "Conversa reanomenada", + "Conversation restored": "Conversa restaurada", + "Conversation started with {agent}": "Conversa iniciada amb {agent}", + "Conversation title": "Títol de la conversa", + "Conversations": "Converses", + "Converting {schema} to blob storage...": "Convertint {schema} a emmagatzematge blob...", + "Converting {schema} to magic table...": "Convertint {schema} a taula màgica...", + "Copied!": "Copiat!", + "Copy": "Copiar", + "Copy Changes": "Copiar canvis", + "Copy Collection": "Copiar col·lecció", + "Copy Data": "Copiar dades", + "Copy Full Data": "Copiar dades completes", + "Copy of {name}": "Còpia de {name}", + "Copy to clipboard": "Copiar al porta-retalls", + "Copying...": "Copiant...", + "Costs": "Costos", + "Count": "Recompte", + "Counting objects...": "Comptant objectes...", + "Create": "Crear", + "Create API Token": "Crear token d'API", + "Create Agent": "Crear agent", + "Create Application": "Crear aplicació", + "Create Collection": "Crear col·lecció", + "Create ConfigSet": "Crear ConfigSet", + "Create New Collection": "Crear nova col·lecció", + "Create New ConfigSet": "Crear nou ConfigSet", + "Create Operations": "Operacions de creació", + "Create Organisation": "Crear organització", + "Create Webhook": "Crear webhook", + "Create a copy of collection:": "Crear una còpia de la col·lecció:", + "Create a new ConfigSet based on the _default template": "Crear un nou ConfigSet basat en la plantilla _default", + "Create a new SOLR collection from an existing ConfigSet": "Crear una nova col·lecció SOLR des d'un ConfigSet existent", + "Create a new SOLR collection from an existing configset": "Crear una nova col·lecció SOLR des d'un configset existent", + "Create a new configset based on the _default template": "Crear un nou configset basat en la plantilla _default", + "Create a new event or link an existing one from any of your calendars.": "Crear un nou esdeveniment o enllaçar-ne un d'existent de qualsevol dels vostres calendaris.", + "Create another": "Crear un altre", + "Create copies of": "Crear còpies de", + "Create event": "Crear esdeveniment", + "Create new token": "Crear nou token", + "Create or link a Deck card to track work on this object.": "Crear o enllaçar una targeta Deck per fer el seguiment del treball en aquest objecte.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Creeu la primera verwerkingsactiviteit per començar a etiquetar les files del rastre d'auditoria amb la seva atribució AVG Art 30.", + "Create your first AI agent to get started.": "Creeu el vostre primer agent d'IA per començar.", + "Created": "Creat", + "Created:": "Creat:", + "Creating search collections...": "Creant col·leccions de cerca...", + "Creating...": "Creant...", + "Current": "Actual", + "Current Filters": "Filtres actuals", + "Current Type:": "Tipus actual:", + "Current password": "Contrasenya actual", + "Custom API endpoint if using a different region": "Punt d'accés d'API personalitzat si s'utilitza una regió diferent", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Capçaleres HTTP personalitzades (una per línia, format: Nom-Capçalera: valor)", + "Custom HTTP headers (one per line, format: header-name: value)": "Capçaleres HTTP personalitzades (una per línia, format: nom-capçalera: valor)", + "Custom Ranges:": "Rangs personalitzats:", + "Custom display title for this facet": "Títol de visualització personalitzat per a aquesta faceta", + "Custom pattern:": "Patró personalitzat:", + "DELETE COLLECTION": "ELIMINAR COL·LECCIÓ", + "DSAR": "DSAR", + "Daily": "Diari", + "Dashboard": "Tauler", + "Dashboard not found": "No s'ha trobat el tauler", + "Data": "Dades", + "Data Source": "Font de dades", + "Data Sources": "Fonts de dades", + "Data Views": "Vistes de dades", + "Data sources": "Fonts de dades", + "Data type does not match observed values": "El tipus de dades no coincideix amb els valors observats", + "Data-subject access request": "Sol·licitud d'accés de la persona interessada", + "Database URL": "URL de la base de dades", + "Database information refreshed": "Informació de la base de dades actualitzada", + "Date Faceting:": "Facetació de dates:", + "Date Range": "Rang de dates", + "Dates": "Dates", + "Deactivation request cancelled": "Sol·licitud de desactivació cancel·lada", + "Deactivation request submitted": "Sol·licitud de desactivació enviada", + "Deck": "Deck", + "Deck integration is not available": "La integració amb Deck no està disponible", + "Default": "Per defecte", + "Default Object Tenant": "Inquilí d'objecte per defecte", + "Default Organisation": "Organització per defecte", + "Default Owner": "Propietari per defecte", + "Default User (for cron/background jobs)": "Usuari per defecte (per a tasques cron/en segon pla)", + "Default User Tenant": "Inquilí d'usuari per defecte", + "Default value": "Valor per defecte", + "Default value (Email)": "Valor per defecte (Correu electrònic)", + "Default value (Password)": "Valor per defecte (Contrasenya)", + "Default value (Phone number)": "Valor per defecte (Número de telèfon)", + "Default value (Regex)": "Valor per defecte (Regex)", + "Default: 5": "Per defecte: 5", + "Defaults to current user. Select a different user if needed.": "Per defecte és l'usuari actual. Seleccioneu un usuari diferent si cal.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Els retards es doblen amb cada intent (2, 4, 8 minuts...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Els retards augmenten linealment (5, 10, 15 minuts...)", + "Delete": "Eliminar", + "Delete ({count})": "Eliminar ({count})", + "Delete Application": "Eliminar aplicació", + "Delete Audit Trail": "Eliminar rastre d'auditoria", + "Delete Collection": "Eliminar col·lecció", + "Delete ConfigSet": "Eliminar ConfigSet", + "Delete Objects": "Eliminar objectes", + "Delete Operations": "Operacions d'eliminació", + "Delete SOLR Collection": "Eliminar col·lecció SOLR", + "Delete View": "Eliminar vista", + "Delete all objects in this schema": "Eliminar tots els objectes d'aquest esquema", + "Delete permanently": "Eliminar permanentment", + "Delete this conversation?": "Eliminar aquesta conversa?", + "Delete view": "Eliminar vista", + "Delete {count} object": "Eliminar {count} objecte", + "Delete {count} objects": "Eliminar {count} objectes", + "Deleted": "Eliminat", + "Deleted By": "Eliminat per", + "Deleted Date": "Data d'eliminació", + "Deleted Items Management": "Gestió d'elements eliminats", + "Deleted This Week": "Eliminat aquesta setmana", + "Deleted Today": "Eliminat avui", + "Deleted:": "Eliminat:", + "Deleting SOLR Collection...": "Eliminant col·lecció SOLR...", + "Deleting...": "Eliminant...", + "Deletion Date Range": "Rang de dates d'eliminació", + "Deletion Statistics": "Estadístiques d'eliminació", + "Deprecated": "Obsolet", + "Depublish": "Despublicar", + "Description": "Descripció", + "Description Template": "Plantilla de descripció", + "Description shown as tooltip": "Descripció mostrada com a informació sobre l'eina", + "Description:": "Descripció:", + "Detect data types and patterns": "Detectar tipus de dades i patrons", + "Detected At": "Detectat a", + "Detected Format:": "Format detectat:", + "Detected Issues:": "Problemes detectats:", + "Detected: {date}": "Detectat: {date}", + "Direct permission": "Permís directe", + "Disable": "Desactivar", + "Disable this to make the agent": "Desactiveu-ho per fer que l'agent", + "Disabled": "Desactivat", + "Discover Files": "Descobrir fitxers", + "Discovered Properties": "Propietats descobertes", + "Display Format": "Format de visualització", + "Display Name": "Nom de visualització", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Voleu eliminar permanentment totes les entrades del rastre d'auditoria filtrades? Aquesta acció no es pot desfer.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Voleu eliminar permanentment aquesta entrada del rastre d'auditoria? Aquesta acció no es pot desfer.", + "Documents": "Documents", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR activat", + "Download API Spec": "Descarregar especificació d'API", + "Download API Specification": "Descarregar especificació d'API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Descarregueu una còpia de totes les vostres dades personals emmagatzemades a OpenRegister (Article 20 del RGPD).", + "Drag a file or files here": "Arrossegueu un fitxer o fitxers aquí", + "Dry run completed: {count} fields would be created": "Execució de prova completada: es crearien {count} camps", + "Dry run completed: {count} fields would be fixed": "Execució de prova completada: es corregirien {count} camps", + "Duration in seconds (optional)": "Durada en segons (opcional)", + "Edit": "Editar", + "Edit Agent": "Editar agent", + "Edit Application": "Editar aplicació", + "Edit Configuration": "Editar configuració", + "Edit Endpoint": "Editar punt d'accés", + "Edit Organisation": "Editar organització", + "Edit Property '{key}' of '{title}'": "Editar la propietat '{key}' de '{title}'", + "Edit Register": "Editar registre", + "Edit Schema": "Editar esquema", + "Edit Source": "Editar font", + "Edit View": "Editar vista", + "Edit Webhook": "Editar webhook", + "Edit labels": "Editar etiquetes", + "Edit processing activity": "Editar activitat de processament", + "Edit tags for {name}": "Editar etiquetes per a {name}", + "Edit verwerkingsactiviteit": "Editar verwerkingsactiviteit", + "Edit view": "Editar vista", + "Edit view details": "Editar detalls de la vista", + "Effectiveness": "Eficàcia", + "Email": "Correu electrònic", + "Email Addresses": "Adreces de correu electrònic", + "Email digest frequency": "Freqüència del resum de correu electrònic", + "Emails": "Correus electrònics", + "Embedding Model": "Model d'incrustació", + "Embedding Provider": "Proveïdor d'incrustació", + "Embedding generated successfully": "La incrustació s'ha generat correctament", + "Embedding provider connection successful!": "La connexió amb el proveïdor d'incrustació ha estat correcta!", + "Embedding test failed: {error}": "La prova d'incrustació ha fallat: {error}", + "Enable": "Activar", + "Enable RAG": "Activar RAG", + "Enable auto-creation": "Activar la creació automàtica", + "Enable automatic file vectorization": "Activar la vectorització automàtica de fitxers", + "Enable automatic object vectorization": "Activar la vectorització automàtica d'objectes", + "Enable automatic synchronization": "Activar la sincronització automàtica", + "Enable calendar provider": "Activar el proveïdor de calendari", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Activar el registre detallat per a les operacions SOLR (recomanat per a la depuració)", + "Enable faceting": "Activar la facetació", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Activar o desactivar les funcions LLM. Configureu els proveïdors i models usant el botó Configuració LLM de dalt.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Activar o desactivar les funcions LLM. Configureu els proveïdors i models usant el botó de configuració LLM de dalt.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Activar o desactivar la integració de cerca SOLR. Configureu els paràmetres de connexió usant el botó Configuració de la connexió de dalt.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Activar o desactivar la integració de cerca SOLR. Configureu els paràmetres de connexió usant el botó de configuració de la connexió de dalt.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Activar o desactivar la integració de flux de treball n8n. Configureu els paràmetres de connexió a continuació.", + "Enable or disable this webhook": "Activar o desactivar aquest webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Activar les eines que permeten a l'agent interactuar amb les dades mitjançant l'invocació de funcions.", + "Enable vectorization for all existing and future views (may increase costs)": "Activar la vectorització per a totes les vistes existents i futures (pot augmentar els costos)", + "Enable write-back to target objects": "Activar la retroescriptura als objectes de destinació", + "Enabled": "Activat", + "End Date Field": "Camp de data de finalització", + "Endpoint Path*": "Ruta del punt d'accés*", + "Endpoints": "Punts d'accés", + "Engine not found": "No s'ha trobat el motor", + "Enter ConfigSet name": "Introduïu el nom del ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Introduïu noms d'usuari de Nextcloud per concedir accés a aquest agent privat", + "Enter agent description (optional)": "Introduïu la descripció de l'agent (opcional)", + "Enter agent name": "Introduïu el nom de l'agent", + "Enter application description (optional)": "Introduïu la descripció de l'aplicació (opcional)", + "Enter application name": "Introduïu el nom de l'aplicació", + "Enter collection name": "Introduïu el nom de la col·lecció", + "Enter configuration description (optional)": "Introduïu la descripció de la configuració (opcional)", + "Enter configuration title": "Introduïu el títol de la configuració", + "Enter description (optional)...": "Introduïu la descripció (opcional)...", + "Enter new collection name": "Introduïu el nou nom de la col·lecció", + "Enter object ID": "Introduïu l'ID de l'objecte", + "Enter organisation description (optional)": "Introduïu la descripció de l'organització (opcional)", + "Enter organisation name": "Introduïu el nom de l'organització", + "Enter search term": "Introduïu el terme de cerca", + "Enter search terms or leave empty to browse all": "Introduïu termes de cerca o deixeu-ho buit per navegar per tot", + "Enter system prompt for the agent": "Introduïu la instrucció del sistema per a l'agent", + "Enter username and press Enter": "Introduïu el nom d'usuari i premeu Retorn", + "Enter view name...": "Introduïu el nom de la vista...", + "Enter webhook name": "Introduïu el nom del webhook", + "Enter your API key": "Introduïu la vostra clau d'API", + "Enter your object here...": "Introduïu el vostre objecte aquí...", + "Enter your schema here...": "Introduïu el vostre esquema aquí...", + "Entities": "Entitats", + "Entities detected": "Entitats detectades", + "Entity ID: {id}": "ID d'entitat: {id}", + "Entity Information": "Informació de l'entitat", + "Entity Recognition": "Reconeixement d'entitats", + "Entity Recognition Method": "Mètode de reconeixement d'entitats", + "Entity deleted successfully": "L'entitat s'ha eliminat correctament", + "Entity not found": "No s'ha trobat l'entitat", + "Entries to be deleted:": "Entrades que s'eliminaran:", + "Enum Issue": "Problema d'enumeració", + "Enum constraint is missing": "Falta la restricció d'enumeració", + "Erase (Art 17)": "Suprimir (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Suprimiu {count} objecte(s) per a aquest subjecte? Aquesta acció es registra al rastre d'auditoria.", + "Erasure complete": "Supressió completada", + "Erasure preview": "Previsualització de la supressió", + "Error": "Error", + "Error Details": "Detalls de l'error", + "Error Information": "Informació de l'error", + "Error Message": "Missatge d'error", + "Error deleting audit trails: {error}": "Error en eliminar els rastres d'auditoria: {error}", + "Error deleting search trails: {error}": "Error en eliminar els rastres de cerca: {error}", + "Error loading application": "Error en carregar l'aplicació", + "Error loading audit trails": "Error en carregar els rastres d'auditoria", + "Error loading entity": "Error en carregar l'entitat", + "Error loading search trails": "Error en carregar els rastres de cerca", + "Estimated Batches:": "Lots estimats:", + "Estimated Cost:": "Cost estimat:", + "Estimated Duration:": "Durada estimada:", + "Event": "Esdeveniment", + "Event Property for Payload": "Propietat d'esdeveniment per a la càrrega útil", + "Events": "Esdeveniments", + "Every schema with detected PII has a processing-activity annotation.": "Cada esquema amb PII detectada té una anotació d'activitat de processament.", + "Example": "Exemple", + "Example Value": "Valor d'exemple", + "Example value for this property": "Valor d'exemple per a aquesta propietat", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Màxim exclusiu", + "Exclusive Minimum": "Mínim exclusiu", + "Exclusive maximum": "Màxim exclusiu", + "Exclusive minimum": "Mínim exclusiu", + "Execution Mode": "Mode d'execució", + "Execution Time": "Temps d'execució", + "Execution Time Range": "Rang de temps d'execució", + "Existing Improvements": "Millores existents", + "Expand details": "Expandir detalls", + "Expiration": "Caducitat", + "Expires": "Caduca", + "Expires in (e.g., 90d)": "Caduca en (p. ex., 90d)", + "Exponential": "Exponencial", + "Export": "Exportar", + "Export \"{schema}\" objects from \"{register}\"": "Exportar objectes \"{schema}\" de \"{register}\"", + "Export Configuration": "Exportar configuració", + "Export Format:": "Format d'exportació:", + "Export Objects": "Exportar objectes", + "Export completed successfully": "L'exportació s'ha completat correctament", + "Export downloaded successfully": "L'exportació s'ha descarregat correctament", + "Export failed: {error}": "L'exportació ha fallat: {error}", + "Export is rate limited. Please try again later.": "L'exportació té un límit de velocitat. Torneu-ho a provar més tard.", + "Export my data": "Exportar les meves dades", + "Export, view, or delete audit trails": "Exportar, visualitzar o eliminar rastres d'auditoria", + "Exporting...": "Exportant...", + "Extend Schema": "Estendre esquema", + "Extended": "Estès", + "Extensions": "Extensions", + "Extra Query Parameters": "Paràmetres de consulta addicionals", + "Extract Now": "Extreure ara", + "Extract Pending Files": "Extreure fitxers pendents", + "Extract now": "Extreure ara", + "Extract properties from each object": "Extreure propietats de cada objecte", + "Extracted At": "Extret a", + "Extracted at": "Extret a", + "Extraction": "Extracció", + "Extraction Mode": "Mode d'extracció", + "Extraction Scope": "Àmbit d'extracció", + "Extraction Status": "Estat de l'extracció", + "Extraction error for {file}: {error}": "Error d'extracció per a {file}: {error}", + "Facet Description": "Descripció de la faceta", + "Facet Order": "Ordre de la faceta", + "Facet Title": "Títol de la faceta", + "Facet Type": "Tipus de faceta", + "Facet configuration updated successfully": "La configuració de la faceta s'ha actualitzat correctament", + "Facetable": "Facetable", + "Faceting Configuration:": "Configuració de la facetació:", + "Facets discovered and configured successfully": "Les facetes s'han descobert i configurat correctament", + "Facets discovered successfully": "Les facetes s'han descobert correctament", + "Failed": "Fallat", + "Failed to add attachment to object": "No s'ha pogut afegir l'adjunt a l'objecte", + "Failed to add user to organisation": "No s'ha pogut afegir l'usuari a l'organització", + "Failed to analyze schema properties": "No s'han pogut analitzar les propietats de l'esquema", + "Failed to archive conversation": "No s'ha pogut arxivar la conversa", + "Failed to calculate sizes": "No s'han pogut calcular les mides", + "Failed to cancel deactivation": "No s'ha pogut cancel·lar la desactivació", + "Failed to change active organisation: {error}": "No s'ha pogut canviar l'organització activa: {error}", + "Failed to change password": "No s'ha pogut canviar la contrasenya", + "Failed to check version: {error}": "No s'ha pogut comprovar la versió: {error}", + "Failed to clear audit trails: {error}": "No s'han pogut esborrar els rastres d'auditoria: {error}", + "Failed to clear blob objects: {error}": "No s'han pogut esborrar els objectes blob: {error}", + "Failed to clear cache: {error}": "No s'ha pogut esborrar la memòria cau: {error}", + "Failed to clear collection": "No s'ha pogut esborrar la col·lecció", + "Failed to clear conversation": "No s'ha pogut esborrar la conversa", + "Failed to clear embeddings: {error}": "No s'han pogut esborrar les incrustacions: {error}", + "Failed to clear search trails: {error}": "No s'han pogut esborrar els rastres de cerca: {error}", + "Failed to clear {type} cache: {error}": "No s'ha pogut esborrar la memòria cau {type}: {error}", + "Failed to copy changes": "No s'han pogut copiar els canvis", + "Failed to copy data": "No s'han pogut copiar les dades", + "Failed to copy data to clipboard": "No s'han pogut copiar les dades al porta-retalls", + "Failed to copy to clipboard": "No s'ha pogut copiar al porta-retalls", + "Failed to copy token": "No s'ha pogut copiar el token", + "Failed to create conversation": "No s'ha pogut crear la conversa", + "Failed to create missing SOLR fields: {error}": "No s'han pogut crear els camps SOLR que falten: {error}", + "Failed to create or find project": "No s'ha pogut crear o trobar el projecte", + "Failed to create token": "No s'ha pogut crear el token", + "Failed to delete SOLR collection: {error}": "No s'ha pogut eliminar la col·lecció SOLR: {error}", + "Failed to delete collection": "No s'ha pogut eliminar la col·lecció", + "Failed to delete conversation": "No s'ha pogut eliminar la conversa", + "Failed to delete objects for {schema}: {error}": "No s'han pogut eliminar els objectes de {schema}: {error}", + "Failed to delete webhook": "No s'ha pogut eliminar el webhook", + "Failed to depublish register: {error}": "No s'ha pogut despublicar el registre: {error}", + "Failed to depublish schema: {error}": "No s'ha pogut despublicar l'esquema: {error}", + "Failed to depublish: {error}": "No s'ha pogut despublicar: {error}", + "Failed to discover files: {error}": "No s'han pogut descobrir els fitxers: {error}", + "Failed to download API specification": "No s'ha pogut descarregar l'especificació d'API", + "Failed to export data": "No s'han pogut exportar les dades", + "Failed to export objects": "No s'han pogut exportar els objectes", + "Failed to fetch conversation history": "No s'ha pogut obtenir l'historial de la conversa", + "Failed to fix mismatched SOLR fields: {error}": "No s'han pogut corregir els camps SOLR no coincidents: {error}", + "Failed to get SOLR field configuration": "No s'ha pogut obtenir la configuració dels camps SOLR", + "Failed to get chat statistics": "No s'han pogut obtenir les estadístiques del xat", + "Failed to get database information: %s": "No s'ha pogut obtenir la informació de la base de dades: %s", + "Failed to get entity categories": "No s'han pogut obtenir les categories de l'entitat", + "Failed to get entity statistics": "No s'han pogut obtenir les estadístiques de l'entitat", + "Failed to get entity types": "No s'han pogut obtenir els tipus d'entitat", + "Failed to get response: {error}": "No s'ha pogut obtenir la resposta: {error}", + "Failed to invalidate app store cache: {error}": "No s'ha pogut invalidar la memòria cau de la botiga d'aplicacions: {error}", + "Failed to leave organisation: {error}": "No s'ha pogut sortir de l'organització: {error}", + "Failed to link object": "No s'ha pogut enllaçar l'objecte", + "Failed to load Deck cards": "No s'han pogut carregar les targetes Deck", + "Failed to load LLM configuration": "No s'ha pogut carregar la configuració LLM", + "Failed to load Nextcloud groups": "No s'han pogut carregar els grups de Nextcloud", + "Failed to load cache statistics: {error}": "No s'han pogut carregar les estadístiques de la memòria cau: {error}", + "Failed to load conversation": "No s'ha pogut carregar la conversa", + "Failed to load entities": "No s'han pogut carregar les entitats", + "Failed to load entity": "No s'ha pogut carregar l'entitat", + "Failed to load extraction data": "No s'han pogut carregar les dades d'extracció", + "Failed to load files": "No s'han pogut carregar els fitxers", + "Failed to load linked contacts": "No s'han pogut carregar els contactes enllaçats", + "Failed to load linked emails": "No s'han pogut carregar els correus electrònics enllaçats", + "Failed to load linked events": "No s'han pogut carregar els esdeveniments enllaçats", + "Failed to load organisations": "No s'han pogut carregar les organitzacions", + "Failed to load register data": "No s'han pogut carregar les dades del registre", + "Failed to load related entities": "No s'han pogut carregar les entitats relacionades", + "Failed to load settings: {error}": "No s'ha pogut carregar la configuració: {error}", + "Failed to load statistics: {error}": "No s'han pogut carregar les estadístiques: {error}", + "Failed to load templates": "No s'han pogut carregar les plantilles", + "Failed to load view: {error}": "No s'ha pogut carregar la vista: {error}", + "Failed to load webhooks": "No s'han pogut carregar els webhooks", + "Failed to load workflows": "No s'han pogut carregar els fluxos de treball", + "Failed to process message": "No s'ha pogut processar el missatge", + "Failed to publish register: {error}": "No s'ha pogut publicar el registre: {error}", + "Failed to publish schema: {error}": "No s'ha pogut publicar l'esquema: {error}", + "Failed to publish: {error}": "No s'ha pogut publicar: {error}", + "Failed to refresh database information": "No s'ha pogut actualitzar la informació de la base de dades", + "Failed to reindex collection": "No s'ha pogut reindexar la col·lecció", + "Failed to remove avatar": "No s'ha pogut eliminar l'avatar", + "Failed to remove link": "No s'ha pogut eliminar l'enllaç", + "Failed to remove member: {error}": "No s'ha pogut eliminar el membre: {error}", + "Failed to remove schema {schema}: {error}": "No s'ha pogut eliminar l'esquema {schema}: {error}", + "Failed to remove user from organisation": "No s'ha pogut eliminar l'usuari de l'organització", + "Failed to rename conversation": "No s'ha pogut reanomenar la conversa", + "Failed to request deactivation": "No s'ha pogut sol·licitar la desactivació", + "Failed to restore conversation": "No s'ha pogut restaurar la conversa", + "Failed to retrieve extraction status.": "No s'ha pogut recuperar l'estat de l'extracció.", + "Failed to retrieve objects for file.": "No s'han pogut recuperar els objectes del fitxer.", + "Failed to retry extraction": "No s'ha pogut reintentar l'extracció", + "Failed to retry webhook": "No s'ha pogut reintentar el webhook", + "Failed to revoke token": "No s'ha pogut revocar el token", + "Failed to save GitHub token": "No s'ha pogut desar el token de GitHub", + "Failed to save GitLab URL": "No s'ha pogut desar la URL de GitLab", + "Failed to save GitLab token": "No s'ha pogut desar el token de GitLab", + "Failed to save LLM settings: {error}": "No s'ha pogut desar la configuració LLM: {error}", + "Failed to save additional feedback": "No s'han pogut desar els comentaris addicionals", + "Failed to save configuration.": "No s'ha pogut desar la configuració.", + "Failed to save configuration: {error}": "No s'ha pogut desar la configuració: {error}", + "Failed to save facet configuration": "No s'ha pogut desar la configuració de la faceta", + "Failed to save feedback": "No s'han pogut desar els comentaris", + "Failed to save file settings: {error}": "No s'ha pogut desar la configuració del fitxer: {error}", + "Failed to save n8n configuration": "No s'ha pogut desar la configuració n8n", + "Failed to save preferences": "No s'han pogut desar les preferències", + "Failed to save roles": "No s'han pogut desar els rols", + "Failed to save settings": "No s'ha pogut desar la configuració", + "Failed to save settings: {error}": "No s'ha pogut desar la configuració: {error}", + "Failed to save view: {error}": "No s'ha pogut desar la vista: {error}", + "Failed to save warmup interval: {error}": "No s'ha pogut desar l'interval d'escalfament: {error}", + "Failed to save webhook": "No s'ha pogut desar el webhook", + "Failed to send feedback": "No s'han pogut enviar els comentaris", + "Failed to set active organisation: {error}": "No s'ha pogut establir l'organització activa: {error}", + "Failed to setup SOLR: {error}": "No s'ha pogut configurar SOLR: {error}", + "Failed to start file warmup: {error}": "No s'ha pogut iniciar l'escalfament de fitxers: {error}", + "Failed to start processing {type} files: {error}": "No s'ha pogut iniciar el processament de fitxers {type}: {error}", + "Failed to start vectorization: {error}": "No s'ha pogut iniciar la vectorització: {error}", + "Failed to switch organisation: {error}": "No s'ha pogut canviar d'organització: {error}", + "Failed to sync magic table for {schema}: {error}": "No s'ha pogut sincronitzar la taula màgica per a {schema}: {error}", + "Failed to test SOLR connection: {error}": "No s'ha pogut provar la connexió SOLR: {error}", + "Failed to test webhook": "No s'ha pogut provar el webhook", + "Failed to update LLM settings: {error}": "No s'ha pogut actualitzar la configuració LLM: {error}", + "Failed to update Multitenancy settings: {error}": "No s'ha pogut actualitzar la configuració de multitinença: {error}", + "Failed to update RBAC settings: {error}": "No s'ha pogut actualitzar la configuració RBAC: {error}", + "Failed to update Retention settings: {error}": "No s'ha pogut actualitzar la configuració de retenció: {error}", + "Failed to update SOLR settings: {error}": "No s'ha pogut actualitzar la configuració SOLR: {error}", + "Failed to update favorite status": "No s'ha pogut actualitzar l'estat de preferit", + "Failed to update schema configuration for {schema}: {error}": "No s'ha pogut actualitzar la configuració de l'esquema per a {schema}: {error}", + "Failed to update schema properties": "No s'han pogut actualitzar les propietats de l'esquema", + "Failed to update view": "No s'ha pogut actualitzar la vista", + "Failed to update view: {error}": "No s'ha pogut actualitzar la vista: {error}", + "Failed to update webhook": "No s'ha pogut actualitzar el webhook", + "Failed to upload avatar": "No s'ha pogut pujar l'avatar", + "Failed to validate {schema}: {error}": "No s'ha pogut validar {schema}: {error}", + "Failed to vectorize files: {error}": "No s'han pogut vectoritzar els fitxers: {error}", + "Failed to warmup names cache: {error}": "No s'ha pogut escalfar la memòria cau de noms: {error}", + "Federation": "Federació", + "Feedback recorded": "Comentaris registrats", + "Fewer sources (1-3):": "Menys fonts (1-3):", + "Field": "Camp", + "Fields to Display": "Camps a mostrar", + "File": "Fitxer", + "File #{id}": "Fitxer #{id}", + "File Chunk Prediction": "Predicció de fragments de fitxer", + "File Collection": "Col·lecció de fitxers", + "File Handling": "Gestió de fitxers", + "File Location": "Ubicació del fitxer", + "File Management": "Gestió de fitxers", + "File Name": "Nom del fitxer", + "File Path": "Ruta del fitxer", + "File Type Selection": "Selecció del tipus de fitxer", + "File Types to Process": "Tipus de fitxers a processar", + "File Vectorization": "Vectorització de fitxers", + "File Warmup": "Escalfament de fitxers", + "File actions menu": "Menú d'accions del fitxer", + "File anonymized successfully": "El fitxer s'ha anonimitzat correctament", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "La vectorització de fragments de fitxer encara no s'ha implementat. Els fragments estan preparats i emmagatzemats, però el servei de vectorització està en desenvolupament.", + "File collection not configured": "La col·lecció de fitxers no està configurada", + "File discovery completed": "El descobriment de fitxers s'ha completat", + "File extraction completed": "L'extracció de fitxers s'ha completat", + "File extraction queued": "L'extracció de fitxers s'ha posat a la cua", + "File indexed successfully": "El fitxer s'ha indexat correctament", + "File is already anonymized": "El fitxer ja està anonimitzat", + "File name": "Nom del fitxer", + "File not found": "No s'ha trobat el fitxer", + "File settings saved successfully": "La configuració del fitxer s'ha desat correctament", + "File settings updated successfully": "La configuració del fitxer s'ha actualitzat correctament", + "File sources": "Fonts de fitxers", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "La vectorització de fitxers s'ha completat! {vectorized} fragments vectoritzats de {files} fitxers. {failed} fallits.", + "File vectorization configuration saved successfully": "La configuració de vectorització de fitxers s'ha desat correctament", + "File vectorization started. Check the statistics section for progress.": "La vectorització de fitxers ha començat. Comproveu la secció d'estadístiques per al progrés.", + "File warmup completed": "L'escalfament de fitxers s'ha completat", + "Files": "Fitxers", + "Files added successfully": "Els fitxers s'han afegit correctament", + "Files with Completed Extraction:": "Fitxers amb extracció completada:", + "Files → fileCollection, Objects → objectCollection": "Fitxers → fileCollection, Objectes → objectCollection", + "Files → filecollection, objects → objectcollection": "Fitxers → filecollection, objectes → objectcollection", + "Filter Audit Trails": "Filtrar rastres d'auditoria", + "Filter Deleted Items": "Filtrar elements eliminats", + "Filter Objects": "Filtrar objectes", + "Filter Properties": "Filtrar propietats", + "Filter Search Trails": "Filtrar rastres de cerca", + "Filter Statistics": "Filtrar estadístiques", + "Filter and analyze search trail entries": "Filtrar i analitzar entrades del rastre de cerca", + "Filter and manage audit trail entries": "Filtrar i gestionar entrades del rastre d'auditoria", + "Filter and manage soft deleted items": "Filtrar i gestionar elements amb supressió suau", + "Filter and search entities": "Filtrar i cercar entitats", + "Filter by object ID": "Filtrar per ID d'objecte", + "Filter by search term": "Filtrar per terme de cerca", + "Filter by type": "Filtrar per tipus", + "Filter by webhook": "Filtrar per webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Les dades del filtre s'han carregat automàticament. Utilitzeu els filtres de baix per refinar la cerca.", + "Filter fields...": "Filtrar camps...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrar els desencadenadors del webhook per propietats de la càrrega útil (un per línia, format: clau: valor)", + "Filtered": "Filtrat", + "Filters": "Filtres", + "Finalizing setup...": "Finalitzant la configuració...", + "Fireworks AI Chat Settings": "Configuració del xat d'IA Fireworks", + "Fireworks AI Embedding Configuration": "Configuració d'incrustació de Fireworks AI", + "First": "Primer", + "Fixed": "Corregit", + "For chat and retrieval-augmented generation": "Per al xat i la generació augmentada per recuperació", + "For vector embeddings and semantic search": "Per a incrustacions vectorials i cerca semàntica", + "Format": "Format", + "Format Issue": "Problema de format", + "Format constraint is missing": "Falta la restricció de format", + "From (e.g. -7 days or 2025-01-01)": "Des de (p. ex. -7 dies o 2025-01-01)", + "From Date": "Des de la data", + "From date": "Des de la data", + "Full data copied to clipboard": "Les dades completes s'han copiat al porta-retalls", + "Function Tools": "Eines de funció", + "General Issue": "Problema general", + "Generate recommendations and confidence scores": "Generar recomanacions i puntuacions de confiança", + "Generate report": "Generar informe", + "Generate the verantwoordingsdocument": "Generar el verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generar vectors immediatament quan es creen nous objectes", + "Generated: {time}": "Generat: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Obteniu informació en temps real sobre la salut de les dades de la vostra organització centrant-vos en els registres, les definicions d'esquema i l'emmagatzematge i l'ús dels objectes.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Obteniu informació en temps real sobre la salut de les dades de la vostra organització, les tendències de creixement i les mètriques de qualitat.", + "GitHub Personal Access Token": "Token d'accés personal de GitHub", + "GitHub token is valid": "El token de GitHub és vàlid", + "GitHub token is valid! Username: {username}": "El token de GitHub és vàlid! Nom d'usuari: {username}", + "GitHub token removed successfully": "El token de GitHub s'ha eliminat correctament", + "GitHub token saved successfully": "El token de GitHub s'ha desat correctament", + "GitHub token test failed: {message}": "La prova del token de GitHub ha fallat: {message}", + "GitLab Personal Access Token": "Token d'accés personal de GitLab", + "GitLab URL saved successfully": "La URL de GitLab s'ha desat correctament", + "GitLab token is valid": "El token de GitLab és vàlid", + "GitLab token is valid! Username: {username}": "El token de GitLab és vàlid! Nom d'usuari: {username}", + "GitLab token saved successfully": "El token de GitLab s'ha desat correctament", + "GitLab token test failed: {message}": "La prova del token de GitLab ha fallat: {message}", + "Go to linked Register": "Anar al registre enllaçat", + "Go to linked Schema": "Anar a l'esquema enllaçat", + "Go to organisation": "Anar a l'organització", + "Grant additional permissions beyond standard CRUD operations": "Concedir permisos addicionals més enllà de les operacions CRUD estàndard", + "Group Quota": "Quota del grup", + "Groups": "Grups", + "HTML preview": "Previsualització HTML", + "HTTP Method": "Mètode HTTP", + "HTTP method used to send webhook requests": "Mètode HTTP usat per enviar sol·licituds de webhook", + "Headers": "Capçaleres", + "Health": "Salut", + "Heartbeat successful - connection kept alive": "Latit correcte: la connexió s'ha mantingut activa", + "Helpful": "Útil", + "Here you can configure the details for various connections.": "Aquí podeu configurar els detalls de diverses connexions.", + "Here you can configure the details for your organisation.": "Aquí podeu configurar els detalls de la vostra organització.", + "Hide": "Amagar", + "Hide Filters": "Amagar filtres", + "Hide in collection view": "Amagar a la vista de col·lecció", + "Hide in forms": "Amagar als formularis", + "Hide in list view": "Amagar a la vista de llista", + "High": "Alt", + "High Confidence": "Alta confiança", + "Host": "Host", + "Hourly": "Cada hora", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Quant de profund s'han de recórrer les propietats d'objectes niats (1-20). Els valors més alts capturen més detall però augmenten la mida del vector.", + "How often to check for updates (1-168 hours)": "Amb quina freqüència comprovar les actualitzacions (1-168 hores)", + "How to handle retries for failed webhook deliveries": "Com gestionar els reintents per als lliuraments de webhooks fallits", + "IBANs": "IBANs", + "ID": "ID", + "ID of input mapping (optional)": "ID del mapatge d'entrada (opcional)", + "ID of output mapping (optional)": "ID del mapatge de sortida (opcional)", + "ID of the target resource": "ID del recurs de destinació", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Els límits de velocitat d'IP s'han esborrat correctament", + "Identify properties not in the schema": "Identificar propietats que no es troben a l'esquema", + "Immutable": "Immutable", + "Import": "Importar", + "Import Configuration": "Importar configuració", + "Import Data into Register": "Importar dades al registre", + "Import successful": "Importació correcta", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importeu la plantilla report-bundle.json per obtenir el registre `reports`, després creeu el vostre primer tauler a través de la interfície d'objecte estàndard. Els taulers declaren els seus ginys en JSON i el renderitzador alimenta cada giny amb dades d'agregació en viu.", + "Improved Property": "Propietat millorada", + "In use": "En ús", + "Inactive": "Inactiu", + "Inactive organisations cannot be used": "Les organitzacions inactives no es poden usar", + "Include IDs and names of related objects for better contextual search": "Incloure ID i noms dels objectes relacionats per a una cerca contextual millor", + "Include ids and names of related objects for better contextual search": "Incloure ID i noms dels objectes relacionats per a una cerca contextual millor", + "Include related object references": "Incloure referències d'objectes relacionats", + "Include schema and register metadata": "Incloure metadades d'esquema i registre", + "Index": "Índex", + "Information about the current OpenRegister installation": "Informació sobre la instal·lació actual d'OpenRegister", + "Inherited from register": "Heretat del registre", + "Inherits permissions from register": "Hereta els permisos del registre", + "Initialization failed": "La inicialització ha fallat", + "Initialize Project": "Inicialitzar el projecte", + "Initializing SOLR setup...": "Inicialitzant la configuració de SOLR...", + "Input Mapping": "Mapatge d'entrada", + "Inspect Fields": "Inspeccionar camps", + "Inspect Index": "Inspeccionar índex", + "Inspect SOLR Index": "Inspeccionar l'índex SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instal·leu l'aplicació notify_push de la Botiga d'aplicacions de Nextcloud per activar les actualitzacions en temps real.", + "Integrations": "Integracions", + "Internet location (URL) of this installation": "Ubicació a Internet (URL) d'aquesta instal·lació", + "Interval": "Interval", + "Invalid": "No vàlid", + "Invalid batch size. Must be between 1 and 5000": "Mida del lot no vàlida. Ha d'estar entre 1 i 5000", + "Invalid feedback type": "Tipus de comentari no vàlid", + "Invalid field name provided": "S'ha proporcionat un nom de camp no vàlid", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects no vàlid. Ha de ser 0 (tots) o un nombre positiu", + "Invalid:": "No vàlid:", + "Invite Users": "Convidar usuaris", + "Invited Users": "Usuaris convidats", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Resultats d'inzage", + "Issue": "Problema", + "Items per page:": "Elements per pàgina:", + "Just now": "Ara mateix", + "Key": "Clau", + "LLM Configuration": "Configuració LLM", + "LLM actions menu": "Menú d'accions LLM", + "LLM configuration saved successfully": "La configuració LLM s'ha desat correctament", + "LLM features disabled": "Les funcions LLM estan desactivades", + "LLM features enabled": "Les funcions LLM estan activades", + "LLM must be enabled with an embedding provider configured": "El LLM ha d'estar activat amb un proveïdor d'incrustació configurat", + "LLM settings saved successfully": "La configuració LLM s'ha desat correctament", + "LLM settings updated successfully": "La configuració LLM s'ha actualitzat correctament", + "Label": "Etiqueta", + "Labels": "Etiquetes", + "Languages": "Idiomes", + "Last": "Últim", + "Last Triggered": "Últim activat", + "Leave empty to allow all users access": "Deixeu-ho buit per permetre l'accés a tots els usuaris", + "Leave off for auto-detection from property format.": "Deixeu-ho desactivat per a la detecció automàtica a partir del format de la propietat.", + "Left organisation successfully": "Heu sortit de l'organització correctament", + "Legal basis *": "Base jurídica *", + "Length Range:": "Rang de longitud:", + "Linear": "Lineal", + "Link": "Enllaç", + "Link an email from the Mail app sidebar to associate it with this object.": "Enllaçar un correu electrònic de la barra lateral de l'aplicació de correu per associar-lo a aquest objecte.", + "Link an object to see it here.": "Enllaçar un objecte per veure'l aquí.", + "Link another object": "Enllaçar un altre objecte", + "Link existing event": "Enllaçar un esdeveniment existent", + "Link removed": "Enllaç eliminat", + "Link to Object": "Enllaçar a l'objecte", + "Link to {name}": "Enllaçar a {name}", + "Linked Objects": "Objectes enllaçats", + "Linked by {user}": "Enllaçat per {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Els correus electrònics, els esdeveniments de calendari, els contactes i les targetes Deck enllaçats apareixeran aquí.", + "Linked to {name}": "Enllaçat a {name}", + "Load Advanced Filters": "Carregar filtres avançats", + "Load advanced filters with live data from your search index": "Carregar filtres avançats amb dades en viu de l'índex de cerca", + "Load more": "Carregar més", + "Load view": "Carregar vista", + "Loading API tokens...": "Carregant tokens d'API...", + "Loading ConfigSets...": "Carregant ConfigSets...", + "Loading LLM configuration...": "Carregant la configuració LLM...", + "Loading RBAC settings...": "Carregant la configuració RBAC...", + "Loading activity...": "Carregant activitat...", + "Loading advanced filters...": "Carregant filtres avançats...", + "Loading agents...": "Carregant agents...", + "Loading application details...": "Carregant detalls de l'aplicació...", + "Loading applications...": "Carregant aplicacions...", + "Loading archived conversations...": "Carregant converses arxivades...", + "Loading audit trails...": "Carregant rastres d'auditoria...", + "Loading available tools...": "Carregant eines disponibles...", + "Loading cache statistics...": "Carregant estadístiques de la memòria cau...", + "Loading collections...": "Carregant col·leccions...", + "Loading configurations...": "Carregant configuracions...", + "Loading conversation...": "Carregant conversa...", + "Loading conversations...": "Carregant converses...", + "Loading deleted items...": "Carregant elements eliminats...", + "Loading endpoints": "Carregant punts d'accés", + "Loading entities...": "Carregant entitats...", + "Loading events...": "Carregant esdeveniments...", + "Loading filter data...": "Carregant dades del filtre...", + "Loading groups...": "Carregant grups...", + "Loading linked objects...": "Carregant objectes enllaçats...", + "Loading log details...": "Carregant detalls del registre...", + "Loading multitenancy settings...": "Carregant la configuració de multitinença...", + "Loading n8n configuration...": "Carregant la configuració n8n...", + "Loading permission matrix...": "Carregant la matriu de permisos...", + "Loading preferences...": "Carregant preferències...", + "Loading register data...": "Carregant dades del registre...", + "Loading registers...": "Carregant registres...", + "Loading retention settings...": "Carregant la configuració de retenció...", + "Loading schemas...": "Carregant esquemes...", + "Loading search configuration...": "Carregant la configuració de cerca...", + "Loading search trails...": "Carregant rastres de cerca...", + "Loading sources...": "Carregant fonts...", + "Loading statistics...": "Carregant estadístiques...", + "Loading text extraction settings...": "Carregant la configuració d'extracció de text...", + "Loading tokens...": "Carregant tokens...", + "Loading user groups...": "Carregant grups d'usuaris...", + "Loading users...": "Carregant usuaris...", + "Loading views...": "Carregant vistes...", + "Loading...": "Carregant...", + "Local": "Local", + "Local Version": "Versió local", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Localitzar tots els objectes que fan referència a una persona interessada (Art 15 inzage), previsualitzar una supressió (Art 17 vergetelheid) o exportar les seves dades (Art 20 portabiliteit).", + "Location": "Ubicació", + "Location Field": "Camp d'ubicació", + "Locations": "Ubicacions", + "Locked": "Bloquejat", + "Locked:": "Bloquejat:", + "Login successful": "Inici de sessió correcte", + "Logs": "Registres", + "Low": "Baix", + "Low Confidence": "Baixa confiança", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Els números més baixos apareixen primer a la barra lateral del filtre. Deixeu-ho buit per a l'ordenació automàtica.", + "Lucene Version:": "Versió de Lucene:", + "Magic table sync completed for {schema}": "La sincronització de la taula màgica s'ha completat per a {schema}", + "Magic table synchronized successfully": "La taula màgica s'ha sincronitzat correctament", + "Mail Integration": "Integració del correu", + "Mail integration is not available": "La integració del correu no està disponible", + "Make this installation discoverable within the federation network.": "Fer que aquesta instal·lació sigui descobrible dins de la xarxa de federació.", + "Manage Organisation Roles": "Gestionar els rols de l'organització", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Gestionar les col·leccions SOLR (magatzems de dades) i assignar-les per a objectes i fitxers.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Gestionar els ConfigSets SOLR (plantilles de configuració) per a les vostres col·leccions.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Gestionar les col·leccions SOLR (magatzems de dades) i assignar-les per a objectes i fitxers.", + "Manage SOLR configsets (configuration templates) for your collections.": "Gestionar els configsets SOLR (plantilles de configuració) per a les vostres col·leccions.", + "Manage and configure agents for automated tasks": "Gestionar i configurar agents per a tasques automatitzades", + "Manage and monitor file text extraction status": "Gestionar i monitorar l'estat d'extracció de text dels fitxers", + "Manage and restore soft deleted items from your registers": "Gestionar i restaurar elements amb supressió suau dels vostres registres", + "Manage and view detected entities from files and objects": "Gestionar i visualitzar les entitats detectades de fitxers i objectes", + "Manage document templates and themes": "Gestionar les plantilles i temes de documents", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Gestionar els fluxos de treball n8n per a l'automatització d'OpenRegister. Els fluxos de treball s'emmagatzemaran al projecte configurat.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Gestionar les activitats de processament, executar sol·licituds d'accés de la persona interessada i auditar el compliment del RGPD de la UE / AVG holandès.", + "Manage webhooks for event-driven integrations": "Gestionar els webhooks per a integracions basades en esdeveniments", + "Manage your account settings, security, and personal data.": "Gestionar la configuració del compte, la seguretat i les dades personals.", + "Manage your applications and modules": "Gestionar les vostres aplicacions i mòduls", + "Manage your chat conversations": "Gestionar les vostres converses de xat", + "Manage your data registers and their configurations": "Gestionar els vostres registres de dades i les seves configuracions", + "Manage your data schemas and their properties": "Gestionar els vostres esquemes de dades i les seves propietats", + "Manage your data sources and their configurations": "Gestionar les vostres fonts de dades i les seves configuracions", + "Manage your organisations and switch between them": "Gestionar les vostres organitzacions i canviar entre elles", + "Manage your saved search configurations": "Gestionar les vostres configuracions de cerca desades", + "Manage your system configurations and settings": "Gestionar les vostres configuracions i paràmetres del sistema", + "Managed": "Gestionat", + "Mappings": "Mapatges", + "Mass validation completed successfully": "La validació massiva s'ha completat correctament", + "Mass validation failed: {error}": "La validació massiva ha fallat: {error}", + "Max Files (0 = all)": "Màx. fitxers (0 = tots)", + "Max Length": "Longitud màxima", + "Max Length:": "Longitud màxima:", + "Max Objects (0 = all)": "Màx. objectes (0 = tots)", + "Max Retries": "Màx. reintents", + "Max Shards/Node": "Màx. fragments/node", + "Max Tokens": "Màx. tokens", + "Max execution time (ms)": "Temps d'execució màx. (ms)", + "Max ms": "Màx. ms", + "Max result count": "Recompte màx. de resultats", + "Max results": "Màx. resultats", + "Maximum": "Màxim", + "Maximum File Size (MB)": "Mida màxima del fitxer (MB)", + "Maximum Nesting Depth": "Profunditat màxima d'anidament", + "Maximum length": "Longitud màxima", + "Maximum length constraint is missing": "Falta la restricció de longitud màxima", + "Maximum length is too restrictive": "La longitud màxima és massa restrictiva", + "Maximum number of files to process. Set to 0 to process all files.": "Nombre màxim de fitxers a processar. Establiu-lo a 0 per processar tots els fitxers.", + "Maximum number of items": "Nombre màxim d'elements", + "Maximum number of objects to process. Set to 0 to process all objects.": "Nombre màxim d'objectes a processar. Establiu-lo a 0 per processar tots els objectes.", + "Maximum number of retry attempts for failed deliveries": "Nombre màxim d'intents de reintent per als lliuraments fallits", + "Maximum time to wait before committing changes": "Temps màxim d'espera abans de confirmar els canvis", + "Maximum tokens to generate": "Nombre màxim de tokens a generar", + "Maximum value": "Valor màxim", + "Maximum value constraint is missing": "Falta la restricció del valor màxim", + "Maximum value is too restrictive": "El valor màxim és massa restrictiu", + "Medium": "Mitjà", + "Medium Confidence": "Confiança mitjana", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Consultes mitjanes: cerques amb algun filtratge o múltiples paràmetres (p. ex., rangs de dates, registres/esquemes específics)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Consultes mitjanes: cerques amb algun filtratge o múltiples paràmetres (p. ex., rangs de dates, registres/esquemes específics)", + "Member removed successfully": "El membre s'ha eliminat correctament", + "Members": "Membres", + "Members ({count})": "Membres ({count})", + "Members of selected groups can access this view": "Els membres dels grups seleccionats poden accedir a aquesta vista", + "Members:": "Membres:", + "Memory prediction calculated successfully": "La predicció de memòria s'ha calculat correctament", + "Message content is required": "El contingut del missatge és obligatori", + "Message does not belong to this conversation": "El missatge no pertany a aquesta conversa", + "Message not found": "No s'ha trobat el missatge", + "Metadata": "Metadades", + "Metadata Filters": "Filtres de metadades", + "Method": "Mètode", + "Method*": "Mètode*", + "Method: {method}": "Mètode: {method}", + "Min Length": "Longitud mínima", + "Min execution time (ms)": "Temps d'execució mín. (ms)", + "Min ms": "Mín. ms", + "Min result count": "Recompte mín. de resultats", + "Min results": "Mín. resultats", + "Minimum": "Mínim", + "Minimum length": "Longitud mínima", + "Minimum number of items": "Nombre mínim d'elements", + "Minimum value": "Valor mínim", + "Minimum value constraint is missing": "Falta la restricció del valor mínim", + "Minimum value is too restrictive": "El valor mínim és massa restrictiu", + "Missing conversation or agentUuid": "Falta la conversa o agentUuid", + "Missing conversationId": "Falta el conversationId", + "Missing message": "Falta el missatge", + "Missing object fields created successfully": "Els camps d'objecte que faltaven s'han creat correctament", + "Mode:": "Mode:", + "Model": "Model", + "Model Name": "Nom del model", + "Modified": "Modificat", + "Monthly": "Mensual", + "More sources (10-20):": "Més fonts (10-20):", + "Most Active Objects": "Objectes més actius", + "Multiple Of": "Múltiple de", + "Multiple of": "Múltiple de", + "Multiple search trail deletion not implemented yet": "L'eliminació de múltiples rastres de cerca encara no s'ha implementat", + "Multitenancy": "Multitinença", + "Multitenancy settings updated successfully": "La configuració de multitinença s'ha actualitzat correctament", + "My Account": "El meu compte", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Nom", + "Name *": "Nom *", + "Name*": "Nom*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "La memòria cau de noms s'ha escalfat correctament: {count} noms carregats en {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "La memòria cau de noms s'ha escalfat correctament: {count} noms carregats en {time}. {message}", + "Names cache warmup completed": "L'escalfament de la memòria cau de noms s'ha completat", + "Naming pattern for copies:": "Patró de nomenclatura per a les còpies:", + "New Collection Name": "Nou nom de la col·lecció", + "New Configuration": "Nova configuració", + "New Conversation": "Nova conversa", + "New Properties": "Noves propietats", + "New Property": "Nova propietat", + "New User Group": "Nou grup d'usuaris", + "New Value": "Nou valor", + "New activity": "Nova activitat", + "New password": "Nova contrasenya", + "New processing activity": "Nova activitat de processament", + "New users without specific organisation membership will be automatically added to this organisation": "Els nous usuaris sense una pertinença específica a l'organització s'afegiran automàticament a aquesta organització", + "New verwerkingsactiviteit": "Nova verwerkingsactiviteit", + "Next": "Següent", + "Nextcloud Groups": "Grups de Nextcloud", + "No": "No", + "No API tokens.": "No hi ha tokens d'API.", + "No ConfigSets found": "No s'han trobat ConfigSets", + "No Deck cards linked to this object": "No hi ha targetes Deck enllaçades a aquest objecte", + "No GitHub token configured": "No hi ha cap token de GitHub configurat", + "No GitHub token provided": "No s'ha proporcionat cap token de GitHub", + "No GitLab token provided": "No s'ha proporcionat cap token de GitLab", + "No Organisation": "Sense organització", + "No active filters": "No hi ha filtres actius", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "No hi ha objectes actius per fer la supressió suau per a l'esquema {schema}. Useu \"Eliminar permanentment\" per eliminar els objectes amb supressió suau.", + "No activity data available": "No hi ha dades d'activitat disponibles", + "No activity found.": "No s'ha trobat cap activitat.", + "No agents available": "No hi ha agents disponibles", + "No agents found": "No s'han trobat agents", + "No app store cache exists yet": "La memòria cau de la botiga d'aplicacions encara no existeix", + "No applications are available.": "No hi ha cap aplicació disponible.", + "No applications found": "No s'han trobat aplicacions", + "No approval steps for this object.": "No hi ha passos d'aprovació per a aquest objecte.", + "No archived conversations": "No hi ha converses arxivades", + "No audit trail entries found": "No s'han trobat entrades del rastre d'auditoria", + "No changes recorded": "No s'han registrat canvis", + "No chunks to vectorize": "No hi ha fragments per vectoritzar", + "No collections found": "No s'han trobat col·leccions", + "No configuration data": "No hi ha dades de configuració", + "No configuration selected": "No s'ha seleccionat cap configuració", + "No configurations are available.": "No hi ha cap configuració disponible.", + "No configurations found": "No s'han trobat configuracions", + "No configurations found for this application.": "No s'han trobat configuracions per a aquesta aplicació.", + "No contacts linked to this object": "No hi ha contactes enllaçats a aquest objecte", + "No conversations yet. Create a new one to get started!": "Encara no hi ha converses. Creeu-ne una de nova per començar!", + "No dashboards yet": "Encara no hi ha taulers", + "No data available": "No hi ha dades disponibles", + "No data available for chart": "No hi ha dades disponibles per al gràfic", + "No deleted items found": "No s'han trobat elements eliminats", + "No deletion data available": "No hi ha dades d'eliminació disponibles", + "No description found": "No s'ha trobat cap descripció", + "No description provided": "No s'ha proporcionat cap descripció", + "No emails linked to this object": "No hi ha correus electrònics enllaçats a aquest objecte", + "No endpoints defined": "No s'han definit punts d'accés", + "No entities detected for this email.": "No s'han detectat entitats per a aquest correu electrònic.", + "No entities detected in this file. Run text extraction first.": "No s'han detectat entitats en aquest fitxer. Executeu primer l'extracció de text.", + "No entities found": "No s'han trobat entitats", + "No entities have been detected yet": "Encara no s'han detectat entitats", + "No events found": "No s'han trobat esdeveniments", + "No events linked to this object": "No hi ha esdeveniments enllaçats a aquest objecte", + "No expired audit trails found to clear": "No s'han trobat rastres d'auditoria caducats per esborrar", + "No expired search trails found to clear": "No s'han trobat rastres de cerca caducats per esborrar", + "No extraction data available for this file": "No hi ha dades d'extracció disponibles per a aquest fitxer", + "No facetable fields available. Select a register and schema to see available filters.": "No hi ha camps facetables disponibles. Seleccioneu un registre i un esquema per veure els filtres disponibles.", + "No files found": "No s'han trobat fitxers", + "No files have been extracted yet": "Encara no s'han extret fitxers", + "No files selected": "No s'ha seleccionat cap fitxer", + "No files to process": "No hi ha fitxers per processar", + "No files to reindex": "No hi ha fitxers per reindexar", + "No filters are currently active. This will delete ALL audit trail entries!": "No hi ha cap filtre actiu actualment. Això eliminarà TOTES les entrades del rastre d'auditoria!", + "No groups found. Try a different search.": "No s'han trobat grups. Proveu una cerca diferent.", + "No label": "Sense etiqueta", + "No labels": "Sense etiquetes", + "No linked objects": "No hi ha objectes enllaçats", + "No log entries found": "No s'han trobat entrades del registre", + "No logs are available for this configuration.": "No hi ha registres disponibles per a aquesta configuració.", + "No logs are available for this source.": "No hi ha registres disponibles per a aquesta font.", + "No logs found": "No s'han trobat registres", + "No matches": "Cap coincidència", + "No mismatched fields found - SOLR schema is properly configured": "No s'han trobat camps no coincidents: l'esquema SOLR està configurat correctament", + "No objects are currently selected for deletion.": "Actualment no hi ha cap objecte seleccionat per eliminar.", + "No objects are currently selected for permanent deletion.": "Actualment no hi ha cap objecte seleccionat per a l'eliminació permanent.", + "No objects are currently selected for restoration.": "Actualment no hi ha cap objecte seleccionat per restaurar.", + "No objects found": "No s'han trobat objectes", + "No objects found to vectorize": "No s'han trobat objectes per vectoritzar", + "No objects linked to this email": "No hi ha objectes enllaçats a aquest correu electrònic", + "No objects selected": "No s'ha seleccionat cap objecte", + "No objects selected for deletion": "No s'ha seleccionat cap objecte per eliminar", + "No objects selected for restoration": "No s'ha seleccionat cap objecte per restaurar", + "No objects to delete": "No hi ha objectes per eliminar", + "No objects to delete for schema {schema}": "No hi ha objectes per eliminar per a l'esquema {schema}", + "No objects were permanently deleted": "No s'ha eliminat permanentment cap objecte", + "No organisations found": "No s'han trobat organitzacions", + "No personal data was found for this subject identifier.": "No s'han trobat dades personals per a aquest identificador de subjecte.", + "No processing activities yet": "Encara no hi ha activitats de processament", + "No properties found": "No s'han trobat propietats", + "No properties match your filters.": "Cap propietat coincideix amb els vostres filtres.", + "No purge date set": "No s'ha establert cap data de purga", + "No register data available": "No hi ha dades del registre disponibles", + "No register objects reference this file": "Cap objecte de registre fa referència a aquest fitxer", + "No registers found": "No s'han trobat registres", + "No registers found for this application.": "No s'han trobat registres per a aquesta aplicació.", + "No registers found. Create a register to configure permissions.": "No s'han trobat registres. Creeu un registre per configurar els permisos.", + "No related cases found for this sender": "No s'han trobat casos relacionats per a aquest remitent", + "No related entities": "No hi ha entitats relacionades", + "No relations found": "No s'han trobat relacions", + "No request body available": "No hi ha cos de sol·licitud disponible", + "No response body available": "No hi ha cos de resposta disponible", + "No saved views yet. Create one in the Search tab!": "Encara no hi ha vistes desades. Creeu-ne una a la pestanya Cerca!", + "No saved views yet. create one in the search tab!": "Encara no hi ha vistes desades. Creeu-ne una a la pestanya de cerca!", + "No scan run yet": "Encara no s'ha executat cap escaneig", + "No schema data available": "No hi ha dades d'esquema disponibles", + "No schema selected for exploration": "No s'ha seleccionat cap esquema per explorar", + "No schemas configured for mail linking.": "No hi ha esquemes configurats per a l'enllaç de correu.", + "No schemas found": "No s'han trobat esquemes", + "No schemas found for this application.": "No s'han trobat esquemes per a aquesta aplicació.", + "No search terms data available": "No hi ha dades de termes de cerca disponibles", + "No search trail entries found": "No s'han trobat entrades del rastre de cerca", + "No sources are available.": "No hi ha fonts disponibles.", + "No sources found": "No s'han trobat fonts", + "No synchronizations found": "No s'han trobat sincronitzacions", + "No templates found": "No s'han trobat plantilles", + "No templates have been created yet": "Encara no s'ha creat cap plantilla", + "No tools available. Tools can be registered by installed apps.": "No hi ha eines disponibles. Les eines es poden registrar per les aplicacions instal·lades.", + "No users in this organisation.": "No hi ha usuaris en aquesta organització.", + "No valid magic-mapped register+schema combinations found": "No s'han trobat combinacions vàlides de registre+esquema mapades màgicament", + "No views found. Create views first before configuring vectorization.": "No s'han trobat vistes. Creeu vistes primer abans de configurar la vectorització.", + "No views match your search": "Cap vista coincideix amb la vostra cerca", + "No webhooks found": "No s'han trobat webhooks", + "No webhooks have been configured yet": "Encara no s'han configurat webhooks", + "No workflows found in this project. Create workflows in the n8n editor.": "No s'han trobat fluxos de treball en aquest projecte. Creeu fluxos de treball a l'editor n8n.", + "None": "Cap", + "Not Found": "No s'ha trobat", + "Not extracted": "No extret", + "Not helpful": "No és útil", + "Not in use": "No en ús", + "Note:": "Nota:", + "Notifications": "Notificacions", + "Number Constraints": "Restriccions numèriques", + "Number of Sources": "Nombre de fonts", + "Number of chunks to vectorize in one API call (1-100).": "Nombre de fragments per vectoritzar en una sola crida d'API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Nombre de fragments per vectoritzar en una sola crida d'API. Més alt = més ràpid però més memòria. Recomanat: 10-50.", + "Number of objects to process in each batch (1-5000).": "Nombre d'objectes a processar en cada lot (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Nombre d'objectes per vectoritzar en una sola crida d'API. Més alt = més ràpid però més memòria. Recomanat: 10-50.", + "Numeric Range:": "Rang numèric:", + "Object": "Objecte", + "Object #{id}": "Objecte #{id}", + "Object Collection": "Col·lecció d'objectes", + "Object Configuration:": "Configuració de l'objecte:", + "Object Count Prediction": "Predicció del recompte d'objectes", + "Object Extraction Mode": "Mode d'extracció d'objectes", + "Object ID": "ID de l'objecte", + "Object Management": "Gestió d'objectes", + "Object Text Extraction": "Extracció de text d'objectes", + "Object Vectorization": "Vectorització d'objectes", + "Object Warmup": "Escalfament d'objectes", + "Object changes in owned objects": "Canvis d'objecte en objectes propis", + "Object collection not configured": "La col·lecció d'objectes no està configurada", + "Object metadata incomplete for file upload": "Les metadades de l'objecte no estan completes per a la pujada de fitxers", + "Object not found": "No s'ha trobat l'objecte", + "Object permanently deleted": "Objecte eliminat permanentment", + "Object restored successfully": "L'objecte s'ha restaurat correctament", + "Object settings updated successfully": "La configuració de l'objecte s'ha actualitzat correctament", + "Object sources": "Fonts de l'objecte", + "Object successfully deleted": "L'objecte s'ha eliminat correctament", + "Object successfully modified": "L'objecte s'ha modificat correctament", + "Object unlocked successfully": "L'objecte s'ha desbloquejat correctament", + "Object vectorization configuration saved successfully": "La configuració de vectorització de l'objecte s'ha desat correctament", + "Object vectorized successfully": "L'objecte s'ha vectoritzat correctament", + "Object:": "Objecte:", + "Objects": "Objectes", + "Objects Analyzed": "Objectes analitzats", + "Objects Distribution": "Distribució d'objectes", + "Objects being analyzed": "Objectes en anàlisi", + "Objects by Register": "Objectes per registre", + "Objects by Schema": "Objectes per esquema", + "Objects deletion completed for {schema}": "L'eliminació d'objectes s'ha completat per a {schema}", + "Objects deletion completed successfully": "L'eliminació d'objectes s'ha completat correctament", + "Objects in schema": "Objectes a l'esquema", + "Objects successfully deleted": "Els objectes s'han eliminat correctament", + "Objects to Process:": "Objectes a processar:", + "Objects to be analyzed": "Objectes per analitzar", + "Objects to be deleted": "Objectes per eliminar", + "Objects to be validated": "Objectes per validar", + "Objects will be serialized as JSON text before vectorization": "Els objectes es serialitzaran com a text JSON abans de la vectorització", + "Objects will be soft deleted and moved to the": "Els objectes seran eliminats suaument i traslladats a la", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Els objectes seran eliminats suaument (marcats com a eliminats però conservats a la base de dades). Es poden recuperar més endavant si cal.", + "Old Value": "Valor anterior", + "Oldest Item (days)": "Element més antic (dies)", + "Ollama Chat Settings": "Configuració del xat Ollama", + "Ollama Embedding Configuration": "Configuració d'incrustació d'Ollama", + "Ollama URL": "URL d'Ollama", + "Only With Changes": "Només amb canvis", + "Only members of selected groups can access this organisation": "Només els membres dels grups seleccionats poden accedir a aquesta organització", + "Open Nextcloud App Store": "Obrir la Botiga d'aplicacions de Nextcloud", + "Open OpenRegister sidebar": "Obrir la barra lateral d'OpenRegister", + "Open Register Objects": "Obrir objectes del registre", + "Open folder": "Obrir la carpeta", + "Open in OpenRegister": "Obrir a OpenRegister", + "Open n8n Editor": "Obrir l'editor n8n", + "OpenAI Chat Settings": "Configuració del xat d'OpenAI", + "OpenAI Embedding Configuration": "Configuració d'incrustació d'OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Configuració d'OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Taulers definits per l'operador i informes programats. Cada tauler és un objecte de primer nivell al registre `reports`; els ginys es declaren a la matriu `widgets` del tauler i es representen en viu a partir d'agregacions / GraphQL.", + "Optimizing search performance...": "Optimitzant el rendiment de la cerca...", + "Optional URL-friendly identifier": "Identificador opcional compatible amb URL", + "Optional description for this facet": "Descripció opcional per a aquesta faceta", + "Optional end date property": "Propietat de data de finalització opcional", + "Optional event description template": "Plantilla de descripció d'esdeveniment opcional", + "Optional location property": "Propietat d'ubicació opcional", + "Optional webhook secret for signature verification": "Secret de webhook opcional per a la verificació de signatures", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Opcional: Afegiu paràmetres de consulta per filtrar l'esquema referenciat (p. ex., status=active&type=public)", + "Or": "O", + "Order": "Ordre", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Etiquetes de llengua BCP 47 ordenades. El primer idioma és el valor per defecte del registre i impulsa la tornada Accept-Language per a les propietats traduïbles.", + "Organisation": "Organització", + "Organisation Configuration": "Configuració de l'organització", + "Organisation ID": "ID de l'organització", + "Organisation Members": "Membres de l'organització", + "Organisation Statistics": "Estadístiques de l'organització", + "Organisation created successfully": "L'organització s'ha creat correctament", + "Organisation membership changes": "Canvis de pertinença a l'organització", + "Organisation name is required": "El nom de l'organització és obligatori", + "Organisation settings saved successfully": "La configuració de l'organització s'ha desat correctament", + "Organisation successfully created": "L'organització s'ha creat correctament", + "Organisation successfully updated": "L'organització s'ha actualitzat correctament", + "Organisational measures": "Mesures organitzatives", + "Organisations": "Organitzacions", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organització", + "Organization ID (Optional)": "ID de l'organització (opcional)", + "Organizations": "Organitzacions", + "Orphaned Items": "Elements orfes", + "Other": "Altres", + "Output Mapping": "Mapatge de sortida", + "Overview": "Resum", + "Owner": "Propietari", + "Owner:": "Propietari:", + "PDF": "PDF", + "PII hits": "Coincidències de PII", + "PII rows": "Files de PII", + "Page {current} of {total}": "Pàgina {current} de {total}", + "Parallel Mode (Faster, more resource intensive)": "Mode paral·lel (més ràpid, més intensiu en recursos)", + "Parallel:": "Paral·lel:", + "Password": "Contrasenya", + "Password changes are not supported by your authentication provider.": "El vostre proveïdor d'autenticació no admet canvis de contrasenya.", + "Password for SOLR authentication (optional)": "Contrasenya per a l'autenticació SOLR (opcional)", + "Password updated successfully": "La contrasenya s'ha actualitzat correctament", + "Path": "Ruta", + "Path in repository": "Ruta al repositori", + "Path where the configuration file will be saved in the repository": "Ruta on es desarà el fitxer de configuració al repositori", + "Path where the register OAS file will be saved in the repository": "Ruta on es desarà el fitxer OAS del registre al repositori", + "Pattern (regex)": "Patró (regex)", + "Pattern Issue": "Problema de patró", + "Pattern constraint is missing": "Falta la restricció de patró", + "Patterns:": "Patrons:", + "Pending": "Pendent", + "Permanent Delete Mode": "Mode d'eliminació permanent", + "Permanently Delete": "Eliminar permanentment", + "Permanently Delete ({count})": "Eliminar permanentment ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Eliminar permanentment tots els {count} objectes amb supressió suau. Aquesta acció no es pot desfer!", + "Permanently delete already soft-deleted objects": "Eliminar permanentment els objectes ja eliminats suaument", + "Permanently delete this conversation?": "Eliminar permanentment aquesta conversa?", + "Permission Matrix": "Matriu de permisos", + "Person": "Persona", + "Personal Data": "Dades personals", + "Personal Data Export": "Exportació de dades personals", + "Persons": "Persones", + "Phone": "Telèfon", + "Phone Numbers": "Números de telèfon", + "Photos": "Fotos", + "Please create an agent in the": "Creeu un agent a la", + "Please select a user": "Seleccioneu un usuari", + "Please select an agent to continue": "Seleccioneu un agent per continuar", + "Please select an organisation": "Seleccioneu una organització", + "Please select at least one item to import": "Seleccioneu almenys un element per importar", + "Please select registers or schemas first": "Seleccioneu primer registres o esquemes", + "Please select which register and schema to use for the new object": "Seleccioneu quin registre i esquema usar per al nou objecte", + "Please try again later.": "Torneu-ho a provar més tard.", + "Please wait while we fetch your agents.": "Espereu mentre recuperem els vostres agents.", + "Please wait while we fetch your applications.": "Espereu mentre recuperem les vostres aplicacions.", + "Please wait while we fetch your configurations.": "Espereu mentre recuperem les vostres configuracions.", + "Please wait while we fetch your deleted items.": "Espereu mentre recuperem els vostres elements eliminats.", + "Please wait while we fetch your sources.": "Espereu mentre recuperem les vostres fonts.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Espereu mentre eliminem permanentment la col·lecció SOLR. Això pot trigar uns moments.", + "Polls": "Enquestes", + "Popular Search Terms": "Termes de cerca populars", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Preferències desades", + "Prerequisites": "Requisits previs", + "Press Enter to send, Shift+Enter for new line": "Premeu Retorn per enviar, Maj+Retorn per a una línia nova", + "Press enter to send, shift+enter for new line": "Premeu retorn per enviar, maj+retorn per a una línia nova", + "Prettify": "Embellir", + "Preview erasure": "Previsualitzar la supressió", + "Preview:": "Previsualització:", + "Previous": "Anterior", + "Private": "Privat", + "Private Agent (Default)": "Agent privat (per defecte)", + "Private agents": "Agents privats", + "Process Name (optional)": "Nom del procés (opcional)", + "Processed {indexed} files, {failed} failed": "S'han processat {indexed} fitxers, {failed} fallits", + "Processes chunks in batches with simulated parallelism.": "Processa fragments en lots amb paral·lelisme simulat.", + "Processes file chunks sequentially (safest).": "Processa els fragments de fitxer seqüencialment (el més segur).", + "Processes objects in chunks with simulated parallelism.": "Processa objectes en fragments amb paral·lelisme simulat.", + "Processes objects sequentially (safest).": "Processa objectes seqüencialment (el més segur).", + "Processing": "Processant", + "Processing Limits": "Límits de processament", + "Processing...": "Processant...", + "Project initialized successfully": "El projecte s'ha inicialitzat correctament", + "Project not found. Please initialize first.": "No s'ha trobat el projecte. Inicialitzeu-lo primer.", + "Properties": "Propietats", + "Property Behaviors": "Comportaments de la propietat", + "Property Configuration:": "Configuració de la propietat:", + "Property Filters": "Filtres de propietats", + "Property Title": "Títol de la propietat", + "Property Type": "Tipus de propietat", + "Property can be improved": "La propietat es pot millorar", + "Property could not be added": "No s'ha pogut afegir la propietat", + "Property could not be updated": "No s'ha pogut actualitzar la propietat", + "Property name of inversed relation": "Nom de la propietat de la relació inversa", + "Property successfully added": "La propietat s'ha afegit correctament", + "Property successfully updated": "La propietat s'ha actualitzat correctament", + "Protected": "Protegit", + "Provider": "Proveïdor", + "Provider is required for testing": "El proveïdor és obligatori per a les proves", + "Public": "Públic", + "Public View": "Vista pública", + "Public views can be accessed by anyone in the system": "Les vistes públiques són accessibles per a qualsevol persona del sistema", + "Publish": "Publicar", + "Publish Configuration to GitHub": "Publicar la configuració a GitHub", + "Publish OAS": "Publicar OAS", + "Publish Objects": "Publicar objectes", + "Publish Register OAS to GitHub": "Publicar l'OAS del registre a GitHub", + "Publish all objects in this schema": "Publicar tots els objectes d'aquest esquema", + "Publish objects to make them publicly available": "Publicar objectes per fer-los disponibles públicament", + "Published": "Publicat", + "Published:": "Publicat:", + "Publishing...": "Publicant...", + "Purge": "Purgar", + "Purge Date": "Data de purga", + "Purge {count} object from database": "Purga {count} objecte de la base de dades", + "Purge {count} objects from database": "Purga {count} objectes de la base de dades", + "Purpose limitation *": "Limitació de la finalitat *", + "Push Notifications": "Notificacions push", + "Query Complexity Distribution": "Distribució de la complexitat de les consultes", + "Query Help": "Ajuda de consultes", + "Query parameter is required": "El paràmetre de consulta és obligatori", + "Quota": "Quota", + "RAG": "RAG", + "RAG Configuration": "Configuració RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "La configuració RBAC s'ha actualitzat correctament", + "Rate limits cleared successfully": "Els límits de velocitat s'han esborrat correctament", + "Raw Changes Data": "Dades de canvis en brut", + "Re-vectorize on object update": "Revectoritzar en l'actualització de l'objecte", + "Read": "Llegir", + "Real-time push notification status via notify_push": "Estat de les notificacions push en temps real a través de notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Les notificacions push en temps real estan actives. Els clients connectats reben actualitzacions instantànies quan els objectes es creen, s'actualitzen o s'eliminen.", + "Realtime push active": "Push en temps real actiu", + "Realtime push not available — the notify_push app is not installed": "El push en temps real no està disponible: l'aplicació notify_push no està instal·lada", + "Reason": "Motiu", + "Reason (optional)": "Motiu (opcional)", + "Rebase operation completed successfully": "L'operació de rebase s'ha completat correctament", + "Rebase operation failed: {error}": "L'operació de rebase ha fallat: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Recomanacions:", + "Recommended Type:": "Tipus recomanat:", + "Recommended: 5 sources": "Recomanat: 5 fonts", + "Refresh": "Actualitzar", + "Refresh Data": "Actualitzar les dades", + "Refresh Stats": "Actualitzar estadístiques", + "Refresh Workflows": "Actualitzar els fluxos de treball", + "Refresh dashboard": "Actualitzar el tauler", + "Refresh database info": "Actualitzar la informació de la base de dades", + "Register": "Registre", + "Register #{id}": "Registre #{id}", + "Register / Schema": "Registre / Esquema", + "Register ID": "ID del registre", + "Register Objects": "Objectes del registre", + "Register Overview": "Resum del registre", + "Register Statistics": "Estadístiques del registre", + "Register Totals": "Totals del registre", + "Register and schema are required": "El registre i l'esquema són obligatoris", + "Register annotation": "Anotació del registre", + "Register depublished successfully": "El registre s'ha despublicat correctament", + "Register not found": "No s'ha trobat el registre", + "Register objects": "Objectes del registre", + "Register objects deletion completed successfully": "L'eliminació dels objectes del registre s'ha completat correctament", + "Register published successfully": "El registre s'ha publicat correctament", + "Register/Schema Usage": "Ús del registre/esquema", + "Register:": "Registre:", + "Registers": "Registres", + "Reindex": "Reindexar", + "Reindex completed": "Reindexació completada", + "Reindex completed successfully": "La reindexació s'ha completat correctament", + "Reindex completed! Processed {count} objects in {duration}s": "Reindexació completada! S'han processat {count} objectes en {duration}s", + "Reindex failed": "La reindexació ha fallat", + "Reindex failed: %s": "La reindexació ha fallat: %s", + "Reject": "Rebutjar", + "Rejected": "Rebutjat", + "Related Cases": "Casos relacionats", + "Relations": "Relacions", + "Remote Version": "Versió remota", + "Remove": "Eliminar", + "Remove Deck card": "Eliminar targeta Deck", + "Remove avatar": "Eliminar avatar", + "Remove contact": "Eliminar contacte", + "Remove filter": "Eliminar filtre", + "Remove from favorites": "Eliminar dels preferits", + "Remove from list": "Eliminar de la llista", + "Remove group": "Eliminar grup", + "Remove link": "Eliminar l'enllaç", + "Remove link to {name}": "Eliminar l'enllaç a {name}", + "Remove link to {name}?": "Eliminar l'enllaç a {name}?", + "Remove link to {title}": "Eliminar l'enllaç a {title}", + "Remove oneOf entry": "Eliminar l'entrada oneOf", + "Remove property after write-back": "Eliminar la propietat després de la retroescriptura", + "Remove {title}": "Eliminar {title}", + "Remove {userId} from this organisation?": "Eliminar {userId} d'aquesta organització?", + "Removed": "Eliminat", + "Removed from favorites": "Eliminat dels preferits", + "Removing schema {schema}...": "Eliminant l'esquema {schema}...", + "Rename Conversation": "Reanomenar la conversa", + "Rename conversation": "Reanomenar la conversa", + "Replicas": "Rèpliques", + "Reports": "Informes", + "Repository": "Repositori", + "Request Body": "Cos de la sol·licitud", + "Request Data": "Dades de la sol·licitud", + "Request Details": "Detalls de la sol·licitud", + "Request Quota (per day)": "Quota de sol·licituds (per dia)", + "Request account deactivation": "Sol·licitar la desactivació del compte", + "Request account deactivation. This will notify administrators for review.": "Sol·licitar la desactivació del compte. Això notificarà els administradors per a la seva revisió.", + "Request body must be JSON with a \"query\" field": "El cos de la sol·licitud ha de ser JSON amb un camp \"query\"", + "Request timeout in seconds": "Temps d'espera de la sol·licitud en segons", + "Requested at": "Sol·licitat a", + "Required": "Obligatori", + "Required Token Scopes": "Àmbits de token obligatoris", + "Required field": "Camp obligatori", + "Required status is inconsistent": "L'estat obligatori és inconsistent", + "Requires Dolphin AI for OCR text extraction": "Requereix Dolphin AI per a l'extracció de text OCR", + "Rerun Search": "Tornar a executar la cerca", + "Rerunning search: {searchTerm}": "Tornant a executar la cerca: {searchTerm}", + "Reset Changes": "Restablir els canvis", + "Reset Filters": "Restablir els filtres", + "Resource Quotas": "Quotes de recursos", + "Response Body": "Cos de la resposta", + "Response Details": "Detalls de la resposta", + "Response Time:": "Temps de resposta:", + "Restore": "Restaurar", + "Restore conversation": "Restaurar la conversa", + "Restore or permanently delete items": "Restaurar o eliminar permanentment els elements", + "Restore {count} object": "Restaura {count} objecte", + "Restore {count} objects": "Restaura {count} objectes", + "Result Count Range": "Rang del recompte de resultats", + "Results": "Resultats", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Període de retenció (durada ISO-8601, p. ex. P10Y, P30D)", + "Retention settings updated successfully": "La configuració de retenció s'ha actualitzat correctament", + "Retrieve all objects for this schema": "Recuperar tots els objectes d'aquest esquema", + "Retry": "Reintentar", + "Retry Failed Extractions": "Reintentar les extraccions fallides", + "Retry Policy": "Política de reintents", + "Retry completed": "Reintent completat", + "Retry upload": "Reintentar la pujada", + "Retrying...": "Reintentant...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Reviseu els objectes seleccionats a continuació. Podeu eliminar qualsevol objecte que no vulgueu eliminar fent clic al botó d'eliminació.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Reviseu els objectes seleccionats a continuació. Podeu eliminar qualsevol objecte que no vulgueu incloure.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Reviseu els objectes seleccionats a continuació. Podeu eliminar qualsevol objecte que no vulgueu eliminar permanentment fent clic al botó d'eliminació. Aquesta acció no es pot desfer.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Reviseu els objectes seleccionats a continuació. Podeu eliminar qualsevol objecte que no vulgueu restaurar fent clic al botó d'eliminació. Els objectes es restauraran a la seva ubicació original.", + "Revoke": "Revocar", + "Right": "Dreta", + "Risk Level": "Nivell de risc", + "Risk level": "Nivell de risc", + "Roles and Permissions": "Rols i permisos", + "Roles updated successfully": "Els rols s'han actualitzat correctament", + "Run compliance scan": "Executar l'escaneig de compliment", + "SOLR Connection Settings": "Configuració de la connexió SOLR", + "SOLR Index Warmup": "Escalfament de l'índex SOLR", + "SOLR Version:": "Versió de SOLR:", + "SOLR actions menu": "Menú d'accions SOLR", + "SOLR base path (usually /solr)": "Ruta base de SOLR (normalment /solr)", + "SOLR collection deleted successfully": "La col·lecció SOLR s'ha eliminat correctament", + "SOLR connection test failed: {error}": "La prova de connexió SOLR ha fallat: {error}", + "SOLR connection test successful!": "La prova de connexió SOLR ha estat correcta!", + "SOLR index warmup completed successfully": "L'escalfament de l'índex SOLR s'ha completat correctament", + "SOLR is disabled": "SOLR està desactivat", + "SOLR is not available or not configured": "SOLR no està disponible o no està configurat", + "SOLR logging disabled": "El registre de SOLR està desactivat", + "SOLR logging enabled": "El registre de SOLR està activat", + "SOLR search disabled": "La cerca SOLR està desactivada", + "SOLR search enabled": "La cerca SOLR està activada", + "SOLR server hostname or IP address": "Nom d'host o adreça IP del servidor SOLR", + "SOLR server port number (optional, defaults to 8983)": "Número de port del servidor SOLR (opcional, per defecte 8983)", + "SOLR settings updated successfully": "La configuració de SOLR s'ha actualitzat correctament", + "SOLR setup completed successfully": "La configuració de SOLR s'ha completat correctament", + "SOLR setup completed successfully!": "La configuració de SOLR s'ha completat correctament!", + "SOLR setup error: %s": "Error de configuració de SOLR: %s", + "SOLR setup failed": "La configuració de SOLR ha fallat", + "SOLR setup failed - check logs": "La configuració de SOLR ha fallat: comproveu els registres", + "SOLR setup failed. Please check the configuration and try again.": "La configuració de SOLR ha fallat. Comproveu la configuració i torneu-ho a provar.", + "SOLR setup failed: {error}": "La configuració de SOLR ha fallat: {error}", + "SOLR warmup failed: {error}": "L'escalfament de SOLR ha fallat: {error}", + "Sample Values:": "Valors de mostra:", + "Save": "Desar", + "Save Configuration": "Desar la configuració", + "Save Connection Settings": "Desar la configuració de la connexió", + "Save Roles": "Desar els rols", + "Save Settings": "Desar la configuració", + "Save changes": "Desar els canvis", + "Save current search as view": "Desar la cerca actual com a vista", + "Save labels": "Desar les etiquetes", + "Save tags for {name}": "Desar les etiquetes per a {name}", + "Save the organisation first to manage users.": "Deseu primer l'organització per gestionar els usuaris.", + "Saved Views": "Vistes desades", + "Saving...": "Desant...", + "Scheduled": "Programat", + "Schema": "Esquema", + "Schema ID": "ID de l'esquema", + "Schema Name": "Nom de l'esquema", + "Schema Statistics": "Estadístiques de l'esquema", + "Schema annotation": "Anotació de l'esquema", + "Schema configuration updated successfully for {schema}": "La configuració de l'esquema s'ha actualitzat correctament per a {schema}", + "Schema depublished successfully": "L'esquema s'ha despublicat correctament", + "Schema objects deletion completed successfully": "L'eliminació dels objectes de l'esquema s'ha completat correctament", + "Schema published successfully": "L'esquema s'ha publicat correctament", + "Schema reference ($ref)": "Referència de l'esquema ($ref)", + "Schema successfully created": "L'esquema s'ha creat correctament", + "Schema successfully updated": "L'esquema s'ha actualitzat correctament", + "Schema {schema} removed successfully": "L'esquema {schema} s'ha eliminat correctament", + "Schema:": "Esquema:", + "Schemas": "Esquemes", + "Scheme": "Esquema", + "Search": "Cerca", + "Search / Views": "Cerca / Vistes", + "Search / views": "Cerca / vistes", + "Search Activity": "Activitat de cerca", + "Search Analytics": "Analítiques de cerca", + "Search Configuration": "Configuració de cerca", + "Search Entities": "Cercar entitats", + "Search Files": "Cercar fitxers", + "Search GitHub": "Cercar a GitHub", + "Search GitLab": "Cercar a GitLab", + "Search Method": "Mètode de cerca", + "Search Mode": "Mode de cerca", + "Search Query": "Consulta de cerca", + "Search Results": "Resultats de la cerca", + "Search Term": "Terme de cerca", + "Search Trail Details": "Detalls del rastre de cerca", + "Search Trail Management": "Gestió del rastre de cerca", + "Search Trail Statistics": "Estadístiques del rastre de cerca", + "Search Trails": "Rastres de cerca", + "Search Views": "Cercar vistes", + "Search Webhooks": "Cercar webhooks", + "Search agents...": "Cercar agents...", + "Search applications...": "Cercar aplicacions...", + "Search by file name or path": "Cercar per nom de fitxer o ruta", + "Search by name or URL": "Cercar per nom o URL", + "Search by title or UUID...": "Cercar per títol o UUID...", + "Search by value": "Cercar per valor", + "Search configurations": "Configuracions de cerca", + "Search data sources...": "Cercar fonts de dades...", + "Search groups...": "Cercar grups...", + "Search in Database Objects": "Cercar en objectes de la base de dades", + "Search in Files": "Cercar en fitxers", + "Search in Objects": "Cercar en objectes", + "Search objects": "Cercar objectes", + "Search objects...": "Cercar objectes...", + "Search property names...": "Cercar noms de propietats...", + "Search registers...": "Cercar registres...", + "Search schemas...": "Cercar esquemes...", + "Search trail deletion not implemented yet": "L'eliminació del rastre de cerca encara no s'ha implementat", + "Search views...": "Cercar vistes...", + "Search {name}...": "Cercar {name}...", + "Searching...": "Cercant...", + "Secret": "Secret", + "Secret key for HMAC signature generation (optional)": "Clau secreta per a la generació de signatures HMAC (opcional)", + "Security": "Seguretat", + "See organisations as a table": "Veure les organitzacions com una taula", + "See organisations as cards": "Veure les organitzacions com targetes", + "Select AI Agent": "Seleccionar l'agent d'IA", + "Select Active Organisation": "Seleccionar l'organització activa", + "Select All": "Seleccionar tot", + "Select ConfigSet": "Seleccionar el ConfigSet", + "Select File Types to Vectorize:": "Seleccionar els tipus de fitxers per vectoritzar:", + "Select HTTP method": "Seleccionar el mètode HTTP", + "Select Register and Schema": "Seleccionar el registre i l'esquema", + "Select Views to Vectorize:": "Seleccionar les vistes per vectoritzar:", + "Select a Nextcloud group to add": "Seleccionar un grup de Nextcloud per afegir", + "Select a branch": "Seleccionar una branca", + "Select a date property": "Seleccionar una propietat de data", + "Select a model or type a custom model name": "Seleccionar un model o escriure un nom de model personalitzat", + "Select a register": "Seleccionar un registre", + "Select a register...": "Seleccionar un registre...", + "Select a repository": "Seleccionar un repositori", + "Select a repository you have write access to": "Seleccionar un repositori amb accés d'escriptura", + "Select a schema": "Seleccionar un esquema", + "Select a schema...": "Seleccionar un esquema...", + "Select a user": "Seleccionar un usuari", + "Select agent type": "Seleccionar el tipus d'agent", + "Select all entities": "Seleccionar totes les entitats", + "Select an AI Agent": "Seleccionar un agent d'IA", + "Select an AI agent to begin chatting with your data.": "Seleccioneu un agent d'IA per començar a xatejar amb les vostres dades.", + "Select an email to see linked objects": "Seleccioneu un correu electrònic per veure els objectes enllaçats", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Seleccioneu una organització i un usuari per afegir-los com a membres. Cerqueu organitzacions per nom.", + "Select backend": "Seleccionar el backend", + "Select chat model": "Seleccionar el model de xat", + "Select collection for files": "Seleccionar la col·lecció per als fitxers", + "Select collection for objects": "Seleccionar la col·lecció per als objectes", + "Select configuration type...": "Seleccionar el tipus de configuració...", + "Select default organisation": "Seleccionar l'organització per defecte", + "Select entity {value}": "Seleccionar l'entitat {value}", + "Select event to listen to...": "Seleccionar l'esdeveniment per escoltar...", + "Select fields to display": "Seleccionar els camps a mostrar", + "Select files with the correct extension": "Seleccionar fitxers amb l'extensió correcta", + "Select group": "Seleccionar el grup", + "Select groups (optional)": "Seleccionar grups (opcional)", + "Select groups to notify...": "Seleccionar grups per notificar...", + "Select groups with access to this agent": "Seleccionar grups amb accés a aquest agent", + "Select groups...": "Seleccionar grups...", + "Select model": "Seleccionar el model", + "Select one or more registers": "Seleccionar un o més registres", + "Select one or more schemas": "Seleccionar un o més esquemes", + "Select options...": "Seleccionar opcions...", + "Select or create labels, or select \"No label\" to add files": "Seleccioneu o creeu etiquetes, o seleccioneu \"Sense etiqueta\" per afegir fitxers", + "Select owner application (optional)...": "Seleccionar l'aplicació propietària (opcional)...", + "Select period": "Seleccionar el període", + "Select property to send as payload": "Seleccionar la propietat per enviar com a càrrega útil", + "Select provider": "Seleccionar el proveïdor", + "Select registers and schemas to save a view": "Seleccionar registres i esquemes per desar una vista", + "Select retry policy": "Seleccionar la política de reintents", + "Select role": "Seleccionar el rol", + "Select schemas (empty = all schemas)": "Seleccionar esquemes (buit = tots els esquemes)", + "Select schemas to warm up": "Seleccionar els esquemes per escalfar", + "Select search mode": "Seleccionar el mode de cerca", + "Select source type...": "Seleccionar el tipus de font...", + "Select target type": "Seleccionar el tipus de destinació", + "Select the branch to publish to": "Seleccionar la branca on publicar", + "Select the event this webhook should listen to": "Seleccionar l'esdeveniment que ha d'escoltar aquest webhook", + "Select views to filter data (optional)": "Seleccionar vistes per filtrar dades (opcional)", + "Select views to limit which data the agent can access": "Seleccionar vistes per limitar a quines dades pot accedir l'agent", + "Select views to vectorize:": "Seleccionar vistes per vectoritzar:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Seleccioneu quins grups de Nextcloud estan disponibles per a aquesta organització. Els usuaris d'aquests grups tindran accés als recursos de l'organització.", + "Select which data views the AI can search": "Seleccionar quines vistes de dades pot cercar la IA", + "Select which property from the event should be used as the webhook payload data": "Seleccionar quina propietat de l'esdeveniment s'ha d'usar com a dades de la càrrega útil del webhook", + "Select which tools the AI can use to perform actions": "Seleccionar quines eines pot usar la IA per realitzar accions", + "Select {fieldName} values": "Seleccionar els valors de {fieldName}", + "Selected": "Seleccionat", + "Selected Groups": "Grups seleccionats", + "Selected Objects ({count})": "Objectes seleccionats ({count})", + "Selected audit trails deleted successfully": "Els rastres d'auditoria seleccionats s'han eliminat correctament", + "Selected search trails deleted successfully": "Els rastres de cerca seleccionats s'han eliminat correctament", + "Selected users can access this view": "Els usuaris seleccionats poden accedir a aquesta vista", + "Send additional feedback": "Enviar comentaris addicionals", + "Send as CloudEvent": "Enviar com a CloudEvent", + "Send as cloudevent": "Enviar com a cloudevent", + "Sensitive PII": "PII sensible", + "Serial Mode (Safer, slower)": "Mode serial (més segur, més lent)", + "Serial:": "Serial:", + "Server Configuration": "Configuració del servidor", + "Server Information": "Informació del servidor", + "Set as Active": "Establir com a actiu", + "Set as active organisation": "Establir com a organització activa", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Establir límits per a l'ús d'API i el consum de tokens. Useu 0 per a recursos il·limitats.", + "Settings": "Configuració", + "Settings updated successfully": "La configuració s'ha actualitzat correctament", + "Shards": "Fragments", + "Share": "Compartir", + "Share with Groups": "Compartir amb grups", + "Share with Users": "Compartir amb usuaris", + "Show": "Mostrar", + "Show Filters": "Mostrar filtres", + "Show less": "Mostrar menys", + "Show only entries with changes": "Mostrar només les entrades amb canvis", + "Showing {count} dashboard(s)": "S'estan mostrant {count} tauler(s)", + "Showing {count} processing activities": "S'estan mostrant {count} activitats de processament", + "Showing {showing} of {total} agents": "S'estan mostrant {showing} de {total} agents", + "Showing {showing} of {total} applications": "S'estan mostrant {showing} de {total} aplicacions", + "Showing {showing} of {total} audit trail entries": "S'estan mostrant {showing} de {total} entrades del rastre d'auditoria", + "Showing {showing} of {total} configurations": "S'estan mostrant {showing} de {total} configuracions", + "Showing {showing} of {total} deleted items": "S'estan mostrant {showing} de {total} elements eliminats", + "Showing {showing} of {total} entities": "S'estan mostrant {showing} de {total} entitats", + "Showing {showing} of {total} files": "S'estan mostrant {showing} de {total} fitxers", + "Showing {showing} of {total} log entries": "S'estan mostrant {showing} de {total} entrades del registre", + "Showing {showing} of {total} search trail entries": "S'estan mostrant {showing} de {total} entrades del rastre de cerca", + "Showing {showing} of {total} sources": "S'estan mostrant {showing} de {total} fonts", + "Showing {showing} of {total} templates": "S'estan mostrant {showing} de {total} plantilles", + "Showing {showing} of {total} webhooks": "S'estan mostrant {showing} de {total} webhooks", + "Showing {shown} of {total} organisations": "S'estan mostrant {shown} de {total} organitzacions", + "Simple": "Simple", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Consultes simples: cerques de text bàsiques amb paràmetres mínims (p. ex., un sol terme de cerca, sense filtres avançats)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Consultes simples: cerques de text bàsiques amb paràmetres mínims (p. ex., un sol terme de cerca, sense filtres avançats)", + "Size": "Mida", + "Slug": "Slug", + "Soft Delete Mode": "Mode de supressió suau", + "Soft Deleted Items": "Elements amb supressió suau", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Fer la supressió suau de tots els objectes d'aquest esquema ({active} actius, {deleted} ja eliminats)", + "SolrCloud mode enabled": "Mode SolrCloud activat", + "Something went wrong while adding files": "S'ha produït un error en afegir fitxers", + "Source": "Font", + "Source successfully updated": "La font s'ha actualitzat correctament", + "Sources": "Fonts", + "Sources:": "Fonts:", + "Special Rights": "Drets especials", + "Standalone SOLR mode": "Mode SOLR autònom", + "Start Conversation": "Iniciar la conversa", + "Start Date Field": "Camp de data d'inici", + "Start Vectorization": "Iniciar la vectorització", + "Start a conversation": "Iniciar una conversa", + "Starting names cache warmup...": "Iniciant l'escalfament de la memòria cau de noms...", + "Starting validation for {schema}...": "Iniciant la validació per a {schema}...", + "Starting {action} for {schema}...": "Iniciant {action} per a {schema}...", + "Starting...": "Iniciant...", + "Statistics": "Estadístiques", + "Statistics and Metrics": "Estadístiques i mètriques", + "Statistics refreshed": "Estadístiques actualitzades", + "Stats calculation not yet implemented": "El càlcul d'estadístiques encara no s'ha implementat", + "Status": "Estat", + "Status Code": "Codi d'estat", + "Step": "Pas", + "Storage": "Emmagatzematge", + "Storage Quota (MB)": "Quota d'emmagatzematge (MB)", + "String Constraints": "Restriccions de cadena", + "Subject identifier (email, BSN, name, etc.)": "Identificador de subjecte (correu electrònic, BSN, nom, etc.)", + "Subscribe": "Subscriure", + "Subscribed": "Subscrit", + "Success": "Correcte", + "Success Rate": "Taxa d'èxit", + "Success Status": "Estat d'èxit", + "Successful": "Correcte", + "Successfully added user to organisation: {name}": "L'usuari s'ha afegit correctament a l'organització: {name}", + "Successfully cleared {count} audit trails": "S'han esborrat correctament {count} rastres d'auditoria", + "Successfully cleared {count} blob storage objects": "S'han esborrat correctament {count} objectes d'emmagatzematge blob", + "Successfully cleared {count} search trails": "S'han esborrat correctament {count} rastres de cerca", + "Successfully created {count} SOLR fields": "S'han creat correctament {count} camps SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "S'han eliminat correctament {count} incrustacions. Revectoritzeu les vostres dades.", + "Successfully deleted {count} objects for {schema}": "S'han eliminat correctament {count} objectes per a {schema}", + "Successfully fixed {count} SOLR field configurations": "S'han corregit correctament {count} configuracions de camps SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "S'ha importat correctament: {registers} registres, {schemas} esquemes, {objects} objectes", + "Successfully joined organisation": "Heu entrat correctament a l'organització", + "Successfully permanently deleted {count} objects": "S'han eliminat permanentment correctament {count} objectes", + "Successfully processed {count} files!": "S'han processat correctament {count} fitxers!", + "Successfully restored {count} object": "{count} objecte restaurat correctament", + "Successfully restored {count} objects": "{count} objectes restaurats correctament", + "Successfully saved configuration for {count} facets!": "La configuració s'ha desat correctament per a {count} facetes!", + "Supported File Types": "Tipus de fitxers admesos", + "Switch Active Organisation": "Canviar l'organització activa", + "Switch Organisation": "Canviar d'organització", + "Sync Table": "Sincronitzar la taula", + "Synchronization Settings": "Configuració de sincronització", + "Syncing magic table for {schema}...": "Sincronitzant la taula màgica per a {schema}...", + "Syncs": "Sincronitzacions", + "System Default": "Per defecte del sistema", + "System Prompt": "Instrucció del sistema", + "System announcements": "Anuncis del sistema", + "Table": "Taula", + "Target ID": "ID de la destinació", + "Target Type*": "Tipus de destinació*", + "Technical Description": "Descripció tècnica", + "Technical Details": "Detalls tècnics", + "Technical description for developers and administrators": "Descripció tècnica per a desenvolupadors i administradors", + "Technical measures": "Mesures tècniques", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatura", + "Templates": "Plantilles", + "Test": "Prova", + "Test Chat": "Provar el xat", + "Test Connection": "Provar la connexió", + "Test Embedding": "Provar la incrustació", + "Test endpoint executed successfully": "El punt d'accés de prova s'ha executat correctament", + "Test webhook delivered successfully": "El webhook de prova s'ha lliurat correctament", + "Test webhook delivery failed": "El lliurament del webhook de prova ha fallat", + "Test webhook sent successfully": "El webhook de prova s'ha enviat correctament", + "Testing...": "Provant...", + "Text Chunk #{id}": "Fragment de text #{id}", + "Text Extraction": "Extracció de text", + "Text Extraction Engine": "Motor d'extracció de text", + "Text chunks": "Fragments de text", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Els fragments de text es generen durant l'extracció de fitxers i s'emmagatzemen a la base de dades. La vectorització llegeix aquests fitxers pre-fragmentats i els converteix en incrustacions.", + "Text extracted successfully": "El text s'ha extret correctament", + "Text extraction disabled": "L'extracció de text està desactivada", + "The Nextcloud Calendar app is not installed or enabled on this server.": "L'aplicació de Calendari de Nextcloud no està instal·lada o activada en aquest servidor.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "L'aplicació de Contactes de Nextcloud no està instal·lada o activada en aquest servidor.", + "The Nextcloud Deck app is not installed or enabled on this server.": "L'aplicació Deck de Nextcloud no està instal·lada o activada en aquest servidor.", + "The Nextcloud Mail app is not installed or enabled on this server.": "L'aplicació de Correu de Nextcloud no està instal·lada o activada en aquest servidor.", + "The URL where webhook events will be sent": "La URL on s'enviaran els esdeveniments del webhook", + "The requested conversation does not exist": "La conversa sol·licitada no existeix", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "No s'ha pogut carregar el tauler sol·licitat. Verifiqueu la URL o trieu un altre tauler de la llista.", + "The saved view and all its search configuration": "La vista desada i tota la seva configuració de cerca", + "There are no audit trail entries matching your current filters.": "No hi ha entrades del rastre d'auditoria que coincideixin amb els vostres filtres actuals.", + "There are no deleted items matching your current filters.": "No hi ha elements eliminats que coincideixin amb els vostres filtres actuals.", + "There are no search trail entries matching your current filters.": "No hi ha entrades del rastre de cerca que coincideixin amb els vostres filtres actuals.", + "There are no webhook log entries matching your filters.": "No hi ha entrades del registre de webhook que coincideixin amb els vostres filtres.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Es conservaran d'acord amb el període de retenció configurat del seu esquema i s'eliminaran permanentment de manera automàtica quan el període de retenció caduqui. El període de retenció és configurable per esquema i es pot trobar a la configuració de l'esquema.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Es conservaran d'acord amb el període de retenció configurat del seu esquema i després s'eliminaran permanentment.", + "This action cannot be undone.": "Aquesta acció no es pot desfer.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Aquesta acció no es pot desfer. Assegureu-vos que cap col·lecció estigui usant aquest ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Aquesta acció no es pot desfer. Assegureu-vos que cap col·lecció estigui usant aquest configset.", + "This action will submit a deactivation request to your administrators.": "Aquesta acció enviarà una sol·licitud de desactivació als vostres administradors.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Aquest agent no té vistes ni eines configurables, però podeu ajustar la configuració RAG a continuació.", + "This analysis may take some time": "Aquesta anàlisi pot trigar una estona", + "This audit trail entry does not contain any change information.": "Aquesta entrada del rastre d'auditoria no conté informació de canvis.", + "This configuration has no data defined.": "Aquesta configuració no té dades definides.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Aquest punt d'accés és obsolet. Useu punts d'accés basats en fragments en el seu lloc.", + "This entity has no relations to objects or files": "Aquesta entitat no té relacions amb objectes o fitxers", + "This is a test webhook from OpenRegister": "Aquest és un webhook de prova d'OpenRegister", + "This is not valid JSON": "Això no és JSON vàlid", + "This key already exists on this schema": "Aquesta clau ja existeix en aquest esquema", + "This schema must use Magic Table configuration to sync": "Aquest esquema ha d'usar la configuració de Taula màgica per sincronitzar", + "This schema must use magic table configuration to sync": "Aquest esquema ha d'usar la configuració de taula màgica per sincronitzar", + "This source has no associated registers.": "Aquesta font no té registres associats.", + "This token will only be shown once. Copy it now.": "Aquest token es mostrarà una sola vegada. Copieu-lo ara.", + "This will copy the _default ConfigSet with the new name": "Això copiarà el ConfigSet _default amb el nou nom", + "This will copy the _default configset with the new name": "Això copiarà el configset _default amb el nou nom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Això eliminarà permanentment TOTES les incrustacions (vectors) de la base de dades. Haureu de revectoritzar tots els objectes i fitxers. Aquesta acció no es pot desfer.\n\nEsteu segur que voleu continuar?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Això eliminarà permanentment TOTES les incrustacions (vectors) de la base de dades. Haureu de revectoritzar tots els objectes i fitxers. Aquesta acció no es pot desfer.\\n\\nEsteu segur que voleu continuar?", + "This will permanently delete:": "Això eliminarà permanentment:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Això eliminarà permanentment {count} objectes ja eliminats suaument de la base de dades. Aquesta acció no es pot desfer.", + "Timeout (seconds)": "Temps d'espera (segons)", + "Timestamp": "Marca de temps", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Consell: Activeu només les vistes que necessitin cerca semàntica per minimitzar els costos d'incrustació. Les taules de cerca simples rarament necessiten vectorització.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Consell: Activeu només les vistes que necessitin cerca semàntica per minimitzar els costos d'incrustació. Les taules de cerca simples rarament necessiten vectorització.", + "Title": "Títol", + "Title *": "Títol *", + "Title Template": "Plantilla de títol", + "Title*": "Títol*", + "To (e.g. -1 year or 2025-12-31)": "Fins a (p. ex. -1 any o 2025-12-31)", + "To Date": "Fins a la data", + "To add files larger than or equal to 512MB, go to the": "Per afegir fitxers de 512 MB o més, aneu a", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Per ajustar la mida o estratègia dels fragments, aneu a Configuració del fitxer → Límits de processament.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Per ajustar la mida o estratègia dels fragments, aneu a configuració del fitxer → límits de processament.", + "To change the embedding provider or model, go to LLM Configuration.": "Per canviar el proveïdor o model d'incrustació, aneu a Configuració LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Per canviar el proveïdor o model d'incrustació, aneu a configuració LLM.", + "To date": "Fins a la data", + "Toggle search sidebar": "Alternar la barra lateral de cerca", + "Token Created": "Token creat", + "Token Quota (per request)": "Quota de tokens (per sol·licitud)", + "Token copied to clipboard": "El token s'ha copiat al porta-retalls", + "Token name": "Nom del token", + "Token revoked": "Token revocat", + "Tools": "Eines", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Les eines s'executen amb els permisos d'usuari per defecte de l'agent quan no hi ha cap sessió d'usuari activa (p. ex., tasques cron). Configureu l'usuari per defecte a la pestanya Configuració.", + "Tools respect the agent's own role and organization boundaries.": "Les eines respecten el propi rol de l'agent i els límits de l'organització.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Les eines respecten les vistes, els permisos i els límits de l'organització de l'agent.", + "Top Deleters": "Principals eliminadors", + "Total": "Total", + "Total Audit Trails": "Total de rastres d'auditoria", + "Total Chunks Available:": "Total de fragments disponibles:", + "Total Deleted Items": "Total d'elements eliminats", + "Total Members": "Total de membres", + "Total Objects": "Total d'objectes", + "Total Objects in Database:": "Total d'objectes a la base de dades:", + "Total Organisations": "Total d'organitzacions", + "Total Results": "Total de resultats", + "Total Searches": "Total de cerques", + "Total Size": "Mida total", + "Total Storage": "Emmagatzematge total", + "Total events": "Total d'esdeveniments", + "Total:": "Total:", + "Totals": "Totals", + "Try searching by UUID or with different keywords": "Proveu de cercar per UUID o amb paraules clau diferents", + "Type": "Tipus", + "Type \"DELETE COLLECTION\" to confirm:": "Escriviu \"DELETE COLLECTION\" per confirmar:", + "Type Issue": "Problema de tipus", + "Type Variations:": "Variacions de tipus:", + "Type filter (optional, e.g. email)": "Filtre de tipus (opcional, p. ex. email)", + "Type must be \"positive\" or \"negative\"": "El tipus ha de ser \"positive\" o \"negative\"", + "Type to search for groups": "Escriviu per cercar grups", + "Type to search for objects...": "Escriviu per cercar objectes...", + "Type to search for organisations": "Escriviu per cercar organitzacions", + "Type to search for users": "Escriviu per cercar usuaris", + "Type to search groups...": "Escriviu per cercar grups...", + "Type to search users...": "Escriviu per cercar usuaris...", + "Type to search...": "Escriviu per cercar...", + "Type your username to confirm": "Escriviu el vostre nom d'usuari per confirmar", + "URL": "URL", + "URL where Ollama is running": "URL on s'executa Ollama", + "UUID copied to clipboard": "L'UUID s'ha copiat al porta-retalls", + "UUID:": "UUID:", + "Unchanged": "Sense canvis", + "Unique Search Terms": "Termes de cerca únics", + "Unique Terms": "Termes únics", + "Unknown": "Desconegut", + "Unknown Object": "Objecte desconegut", + "Unknown Register": "Registre desconegut", + "Unknown Schema": "Esquema desconegut", + "Unknown error": "Error desconegut", + "Unknown widget type: {type}": "Tipus de giny desconegut: {type}", + "Unlink email": "Desenllaçar el correu electrònic", + "Unlink event": "Desenllaçar l'esdeveniment", + "Unnamed Object": "Objecte sense nom", + "Untitled": "Sense títol", + "Untitled View": "Vista sense títol", + "Update": "Actualitzar", + "Update Operations": "Operacions d'actualització", + "Update available: {local} → {remote}": "Actualització disponible: {local} → {remote}", + "Update configuration: ...": "Actualitzar la configuració: ...", + "Update register OAS: ...": "Actualitzar el OAS del registre: ...", + "Update vectors when object data changes (recommended for accurate search)": "Actualitzar els vectors quan canviïn les dades de l'objecte (recomanat per a una cerca precisa)", + "Updated": "Actualitzat", + "Updated At": "Actualitzat a", + "Updated:": "Actualitzat:", + "Upload new avatar": "Pujar un nou avatar", + "Uploading configuration sets...": "Pujant conjunts de configuració...", + "Uptime:": "Temps de funcionament:", + "Url": "Url", + "Usage Count": "Recompte d'ús", + "Use AI agents for processing and analysis": "Usar agents d'IA per al processament i l'anàlisi", + "Use Large Language Model features": "Usar les funcions del model de llenguatge gran", + "Use SolrCloud with Zookeeper for distributed search": "Usar SolrCloud amb Zookeeper per a la cerca distribuïda", + "Use SolrCloud with zookeeper for distributed search": "Usar SolrCloud amb zookeeper per a la cerca distribuïda", + "Use VNG APIs instead of MongoDB.": "Usar les API de VNG en lloc de MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Usar els rangs per defecte (Últims 7/30/90 dies, Últim any, Més antic)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Usar emmagatzematge extern (p. ex. MongoDB) en lloc de l'emmagatzematge intern de Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Useu els filtres per restringir les entrades del rastre d'auditoria per registre, esquema, tipus d'acció, usuari, rang de dates o ID d'objecte.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Useu els filtres per restringir els elements eliminats per registre, esquema, data d'eliminació o usuari que els ha eliminat.", + "Use filters to narrow down entities by type or category.": "Useu els filtres per restringir les entitats per tipus o categoria.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Useu els filtres per restringir les entrades del rastre de cerca per registre, esquema, estat d'èxit, usuari, rang de dates, termes de cerca o mètriques de rendiment.", + "Used By Collections": "Usat per les col·leccions", + "Used by": "Usat per", + "User": "Usuari", + "User Agent Statistics": "Estadístiques de l'agent d'usuari", + "User Description": "Descripció de l'usuari", + "User Quota": "Quota de l'usuari", + "User rate limits cleared successfully": "Els límits de velocitat de l'usuari s'han esborrat correctament", + "User removed successfully": "L'usuari s'ha eliminat correctament", + "User-friendly description shown in forms and help text": "Descripció amigable per a l'usuari que es mostra als formularis i al text d'ajuda", + "Username": "Nom d'usuari", + "Username for SOLR authentication (optional)": "Nom d'usuari per a l'autenticació SOLR (opcional)", + "Users": "Usuaris", + "Uses": "Usos", + "Using Pre-Generated Chunks": "Usant fragments pre-generats", + "Validate": "Validar", + "Validate Objects": "Validar objectes", + "Validation completed for {schema}": "La validació s'ha completat per a {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "La validació s'ha completat per a {schema}: {processed} processats, {updated} actualitzats, {failed} fallits", + "Validation completed successfully": "La validació s'ha completat correctament", + "Validation failed": "La validació ha fallat", + "Value": "Valor", + "Value list (split on ,)": "Llista de valors (separada per ,)", + "Vector Dimensions": "Dimensions del vector", + "Vector Search Backend": "Backend de cerca vectorial", + "Vector field: _embedding_": "Camp vectorial: _embedding_", + "Vectorization Triggers": "Desencadenadors de vectorització", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vectorització completada: {vectorized} objectes vectoritzats, {failed} fallits", + "Vectorization failed: {error}": "La vectorització ha fallat: {error}", + "Vectorize All Files": "Vectoritzar tots els fitxers", + "Vectorize All Objects": "Vectoritzar tots els objectes", + "Vectorize all file types": "Vectoritzar tots els tipus de fitxers", + "Vectorize all views": "Vectoritzar totes les vistes", + "Vectorize on object creation": "Vectoritzar en la creació de l'objecte", + "Vectors will be stored in your existing object and file collections": "Els vectors s'emmagatzemaran a les vostres col·leccions d'objectes i fitxers existents", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Verificant la connectivitat del servidor...", + "Version": "Versió", + "Version Information": "Informació de la versió", + "Very High": "Molt alt", + "Very high": "Molt alt", + "View": "Vista", + "View API Docs": "Veure la documentació de l'API", + "View API Documentation": "Veure la documentació de l'API", + "View APIs": "Veure les API", + "View Changes": "Veure els canvis", + "View Details": "Veure els detalls", + "View Error": "Error de la vista", + "View File": "Veure el fitxer", + "View Full Details": "Veure els detalls complets", + "View Logs": "Veure els registres", + "View Name": "Nom de la vista", + "View Object": "Veure l'objecte", + "View Parameters": "Paràmetres de la vista", + "View Selection": "Selecció de la vista", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Veure i analitzar els registres del rastre de cerca amb capacitats avançades de filtratge i analítiques", + "View and analyze system audit trails with advanced filtering capabilities": "Veure i analitzar els rastres d'auditoria del sistema amb capacitats avançades de filtratge", + "View and manage authorization across registers and schemas": "Veure i gestionar l'autorització en tots els registres i esquemes", + "View deleted successfully": "La vista s'ha eliminat correctament", + "View entity details and manage relations": "Veure els detalls de l'entitat i gestionar les relacions", + "View name": "Nom de la vista", + "View name is required": "El nom de la vista és obligatori", + "View object": "Veure l'objecte", + "View saved successfully!": "La vista s'ha desat correctament!", + "View search analytics and manage search logs": "Veure les analítiques de cerca i gestionar els registres de cerca", + "View successfully deleted": "La vista s'ha eliminat correctament", + "View successfully updated": "La vista s'ha actualitzat correctament", + "View updated successfully!": "La vista s'ha actualitzat correctament!", + "View webhook delivery logs and filter by webhook": "Veure els registres de lliurament de webhooks i filtrar per webhook", + "View {count} more": "Veure {count} més", + "Views": "Vistes", + "Visible to end users": "Visible per als usuaris finals", + "Visible to users": "Visible per als usuaris", + "Wait for Response": "Esperar la resposta", + "Wait for webhook response before continuing (required for request/response flows)": "Esperar la resposta del webhook abans de continuar (obligatori per als fluxos de sol·licitud/resposta)", + "Waiting for cluster synchronization...": "Esperant la sincronització del clúster...", + "Warmup Names Cache": "Escalfar la memòria cau de noms", + "Warmup failed": "L'escalfament ha fallat", + "Warning": "Avís", + "Warning:": "Avís:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Avís: Aquesta propietat té tipus inconsistents: {types}. Considereu si tots els objectes haurien de tenir el mateix tipus.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Escanearem tots els objectes que pertanyen a aquest esquema per descobrir noves propietats i analitzar les propietats existents per trobar millores potencials. El procés implica examinar l'estructura de dades de cada objecte, identificar les propietats no definides a l'esquema actual i trobar oportunitats per millorar les definicions de propietats existents amb millors restriccions, formats i regles de validació.", + "Webhook": "Webhook", + "Webhook Log Details": "Detalls del registre de webhook", + "Webhook Logs": "Registres de webhook", + "Webhook created successfully": "El webhook s'ha creat correctament", + "Webhook deleted": "Webhook eliminat", + "Webhook retry delivered successfully": "El reintent del webhook s'ha lliurat correctament", + "Webhook retry delivery failed": "El lliurament del reintent del webhook ha fallat", + "Webhook updated": "Webhook actualitzat", + "Webhook updated successfully": "El webhook s'ha actualitzat correctament", + "Webhooks": "Webhooks", + "Weekly": "Setmanal", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Quan l'agent s'executa sense una sessió d'usuari (p. ex., tasques programades), s'usarà el context d'aquest usuari", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Quan l'agent s'executa sense una sessió d'usuari (p. ex., tasques programades), s'usarà el context d'aquest usuari.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Quan estigui desactivat, aquesta faceta mostrarà només els valors d'aquest esquema i inclourà un filtre d'esquema quan estigui seleccionada.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Quan estigui activat, desar aquest objecte també actualitzarà els objectes de destinació per incloure una referència de tornada a aquest objecte.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Quan estigui activat, aquesta propietat s'eliminarà de l'objecte font després d'actualitzar els objectes de destinació.", + "Workflow Management": "Gestió del flux de treball", + "Workflows": "Fluxos de treball", + "Wrap webhook payload in CloudEvents format for better interoperability": "Embolicar la càrrega útil del webhook en format CloudEvents per a una millor interoperabilitat", + "Wrap webhook payload in cloudevents format for better interoperability": "Embolicar la càrrega útil del webhook en format cloudevents per a una millor interoperabilitat", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: valor\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: valor\\nAuthorization: Bearer token", + "Yes": "Sí", + "You": "Vosaltres", + "You are not yet a member of any organisations.": "Encara no sou membre de cap organització.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Podeu crear nous ConfigSets basats en la plantilla _default, o pujar-ne de personalitzats directament al vostre servidor SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Podeu crear nous configsets basats en la plantilla _default, o pujar-ne de personalitzats directament al vostre servidor SOLR.", + "You do not have access to this conversation": "No teniu accés a aquesta conversa", + "You do not have permission to delete this conversation": "No teniu permís per eliminar aquesta conversa", + "You do not have permission to modify this conversation": "No teniu permís per modificar aquesta conversa", + "You do not have permission to restore this conversation": "No teniu permís per restaurar aquesta conversa", + "You do not have permission to view the permission matrix. Admin access is required.": "No teniu permís per veure la matriu de permisos. Es requereix accés d'administrador.", + "You have selected {count} properties to add to the schema.": "Heu seleccionat {count} propietats per afegir a l'esquema.", + "You must be logged in to favorite views": "Heu d'iniciar sessió per marcar les vistes com a preferides", + "You need an AI agent to start a conversation.": "Necessiteu un agent d'IA per iniciar una conversa.", + "Your Fireworks AI API key. Get one at": "La vostra clau d'API de Fireworks AI. Obteniu-ne una a", + "Your OpenAI API key. Get one at": "La vostra clau d'API d'OpenAI. Obteniu-ne una a", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Els vostres comentaris s'han registrat. Opcionalment, podeu proporcionar detalls addicionals aquí...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Els vostres comentaris s'han registrat. Opcionalment, podeu proporcionar detalls addicionals aquí...", + "Zookeeper Hosts": "Hosts de Zookeeper", + "Zookeeper Port": "Port de Zookeeper", + "Zookeeper Settings (SolrCloud)": "Configuració de Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Cadena de connexió de Zookeeper per a SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Número de port de Zookeeper (opcional, per defecte 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVÍS D'ELIMINACIÓ PERMANENT \\u26A0\\uFE0F\n\nEsteu a punt d'ELIMINAR PERMANENTMENT TOTS els objectes per a l'esquema \"{schema}\":\n\n\\u2022 Objectes actius: {active}\n\\u2022 Objectes eliminats suaument: {deleted}\n\\u2022 Total: {total}\n\nAquests objectes s'eliminaran completament de la base de dades i NO ES PODEN recuperar.\n\nEsteu absolutament segur?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVÍS D'ELIMINACIÓ PERMANENT \\u26A0\\uFE0F\n\nEsteu a punt d'ELIMINAR PERMANENTMENT {count} objectes eliminats suaument per a l'esquema \"{schema}\".\n\nAquests objectes s'eliminaran completament de la base de dades i NO ES PODEN recuperar.\n\nEsteu absolutament segur?", + "and accessible to all users in selected groups (or all users if no groups selected).": "i accessible per a tots els usuaris dels grups seleccionats (o tots els usuaris si no s'ha seleccionat cap grup).", + "and add the files there.": "i afegiu els fitxers allà.", + "approved": "aprovat", + "are only accessible to invited users.": "només són accessibles per als usuaris convidats.", + "by": "per", + "chunk": "fragment", + "chunks": "fragments", + "chunks processed": "fragments processats", + "completed": "completat", + "configuration(s)": "configuració(ns)", + "conversationId is required": "el conversationId és obligatori", + "create": "crear", + "delete": "eliminar", + "deleted objects section": "secció d'objectes eliminats", + "e.g., lib/Settings/config.json": "p. ex., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "p. ex., lib/Settings/register.json", + "e.g., lib/settings/config.json": "p. ex., lib/settings/config.json", + "e.g., lib/settings/register.json": "p. ex., lib/settings/register.json", + "execute": "executar", + "failed": "fallat", + "file": "fitxer", + "files": "fitxers", + "file{plural}": "fitxer{plural}", + "folder": "carpeta", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "a {register}", + "in_progress": "en curs", + "inherited": "heretat", + "list": "llista", + "log{plural}": "registre{plural}", + "manage": "gestionar", + "members": "membres", + "menu or contact someone with permission to create agents.": "menú o contacteu amb algú amb permís per crear agents.", + "message content is required": "el contingut del missatge és obligatori", + "messages": "missatges", + "missing": "que falta", + "myapp": "myapp", + "n8n URL and API key are required": "Es requereix la URL i la clau d'API de n8n", + "n8n configuration saved successfully": "La configuració n8n s'ha desat correctament", + "n8n connection not configured": "La connexió n8n no està configurada", + "n8n connection successful": "La connexió n8n ha estat correcta", + "n8n connection test failed: {message}": "La prova de connexió n8n ha fallat: {message}", + "n8n connection test successful": "La prova de connexió n8n ha estat correcta", + "n8n initialization failed: {message}": "La inicialització n8n ha fallat: {message}", + "n8n integration disabled": "La integració n8n està desactivada", + "n8n integration enabled": "La integració n8n està activada", + "n8n project initialized successfully": "El projecte n8n s'ha inicialitzat correctament", + "n8n settings saved successfully": "La configuració n8n s'ha desat correctament", + "notify_push configuration guide": "Guia de configuració de notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push està instal·lat però OpenRegister encara no ha confirmat un push correcte. Activeu el desat d'un objecte per activar-lo, o comproveu la vostra configuració de notify_push.", + "notify_push is installed but not yet active": "notify_push està instal·lat però encara no està actiu", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objectes", + "objects processed": "objectes processats", + "object{plural}": "objecte{plural}", + "or pick a range": "o trieu un rang", + "org-...": "org-...", + "pending": "pendent", + "provides a good balance between speed and accuracy.": "proporciona un bon equilibri entre velocitat i precisió.", + "public": "públic", + "read": "llegir", + "register(s)": "registre(s)", + "register(s) selected": "registre(s) seleccionat(s)", + "register{plural}": "registre{plural}", + "rejected": "rebutjat", + "required": "obligatori", + "results": "resultats", + "schema(s)": "esquema(es)", + "schema(s) selected": "esquema(es) seleccionat(s)", + "schema{plural}": "esquema{plural}", + "selected objects": "objectes seleccionats", + "set": "establir", + "sk-...": "sk-...", + "this application": "aquesta aplicació", + "to": "a", + "type must be \"positive\" or \"negative\"": "el tipus ha de ser \"positive\" o \"negative\"", + "update": "actualitzar", + "vectorized": "vectoritzat", + "view": "vista", + "views selected": "vistes seleccionades", + "waiting": "esperant", + "{active} active, {deleted} deleted": "{active} actiu(s), {deleted} eliminat(s)", + "{count} audit trails cleared successfully": "{count} rastres d'auditoria esborrats correctament", + "{count} compliance issue(s) detected": "{count} problema(es) de compliment detectat(s)", + "{count} deletions": "{count} eliminacions", + "{count} documents found": "S'han trobat {count} documents", + "{count} email": "{count} correu electrònic", + "{count} emails": "{count} correus electrònics", + "{count} entries": "{count} entrades", + "{count} issues": "{count} problemes", + "{count} members": "{count} membres", + "{count} searches": "{count} cerques", + "{count} selected": "{count} seleccionat(s)", + "{count} widget(s)": "{count} giny(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} objecte(s) coincidint(s). {erased} suprimit(s).", + "{minutes} minutes ago": "fa {minutes} minuts", + "{percentage}% of objects": "{percentage}% dels objectes", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} a {register} / {schema}", + "{type} cache cleared successfully": "La memòria cau de {type} s'ha esborrat correctament", + "ℹ️ Current Configuration": "ℹ️ Configuració actual", + "ℹ️ current configuration": "ℹ️ configuració actual", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ PERILL: Tots {total} objectes seran ELIMINATS PERMANENTMENT de la base de dades. Aquesta acció és IRRECUPERABLE i no es pot desfer!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVÍS D'ELIMINACIÓ PERMANENT ⚠️\n\nEsteu a punt d'ELIMINAR PERMANENTMENT TOTS els objectes per a l'esquema \"{schema}\":\n\n• Objectes actius: {active}\n• Objectes eliminats suaument: {deleted}\n• Total: {total}\n\nAquests objectes s'eliminaran completament de la base de dades i NO ES PODEN recuperar.\n\nEsteu absolutament segur?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVÍS D'ELIMINACIÓ PERMANENT ⚠️\n\nEsteu a punt d'ELIMINAR PERMANENTMENT {count} objectes eliminats suaument per a l'esquema \"{schema}\".\n\nAquests objectes s'eliminaran completament de la base de dades i NO ES PODEN recuperar.\n\nEsteu absolutament segur?", + "⚡ Batch Processing": "⚡ Processament per lots", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR pot gestionar milions de documents amb temps de resposta de cerca inferiors a un segon...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Context complet ✓ Menys probabilitat de perdre detalls ✗ Respostes més lentes ✗ Pot incloure informació menys rellevant", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Respostes més ràpides ✓ Respostes més enfocades ✗ Pot perdre informació rellevant", + "✨ AI Features": "✨ Funcions d'IA", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 En mode distribuït, SOLR usa ZooKeeper per a la coordinació del clúster...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 La cerca per facetes permet als usuaris aprofundir en els resultats per categories...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Sabíeu que? SOLR alimenta la cerca de Netflix, Apple i molts altres llocs importants!", + "💰 View Selection (Cost Optimization)": "💰 Selecció de vista (Optimització de costos)", + "📄 Object Serialization": "📄 Serialització d'objectes", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR proporciona analítiques i estadístiques riques sobre el rendiment de la cerca...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Els ConfigSets contenen l'esquema i els fitxers de configuració per al vostre índex de cerca...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 La replicació garanteix que el vostre índex de cerca estigui disponible fins i tot si els nodes fallen...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR és una potent plataforma de cerca empresarial construïda sobre Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Configuració de vectorització", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 L'escalat automàtic pot afegir o eliminar nodes dinàmicament en funció de la càrrega...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Les funcions de seguretat inclouen autenticació, autorització i xifratge SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} correu electrònic", + "{count} correus electrònics" + ], + "Purge {count} object from database": [ + "Purga {count} objecte de la base de dades", + "Purga {count} objectes de la base de dades" + ], + "Restore {count} object": [ + "Restaura {count} objecte", + "Restaura {count} objectes" + ], + "Successfully restored {count} object": [ + "{count} objecte restaurat correctament", + "{count} objectes restaurats correctament" + ], + "Delete {count} object": [ + "Elimina {count} objecte", + "Elimina {count} objectes" + ], + "Object successfully deleted": [ + "Objecte eliminat correctament", + "Objectes eliminats correctament" + ] + } +} diff --git a/l10n/cs.js b/l10n/cs.js new file mode 100644 index 0000000000..0a32df0858 --- /dev/null +++ b/l10n/cs.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(bez předmětu)", + "(no title)" : "(bez názvu)", + "(unnamed)" : "(bez názvu)", + "(untitled card)" : "(karta bez názvu)", + "Activities" : "Aktivity", + "Add Endpoint" : "Přidat koncový bod", + "Add Source" : "Přidat zdroj", + "Add card" : "Přidat kartu", + "Add contact" : "Přidat kontakt", + "All clear" : "Vše v pořádku", + "Articles" : "Články", + "Code" : "Kód", + "Compliance" : "Soulad s předpisy", + "Contacts" : "Kontakty", + "Create Agent" : "Vytvořit agenta", + "Create Application" : "Vytvořit aplikaci", + "Create event" : "Vytvořit událost", + "Erasure complete" : "Vymazání dokončeno", + "Erasure preview" : "Náhled vymazání", + "Expand details" : "Rozbalit podrobnosti", + "Save changes" : "Uložit změny", + "📄 Object Serialization" : "📄 Serializace objektů", + "🔢 Vectorization Settings" : "🔢 Nastavení vektorizace", + "💰 View Selection (Cost Optimization)" : "💰 Výběr pohledů (optimalizace nákladů)", + "AI Assistant" : "AI asistent", + "AI Chat" : "AI chat", + "API Key" : "API klíč", + "API tokens saved successfully" : "API tokeny byly úspěšně uloženy", + "About ConfigSets" : "O ConfigSetech", + "Action" : "Akce", + "Action Distribution" : "Rozložení akcí", + "Action:" : "Akce:", + "Actions" : "Akce", + "Active" : "Aktivní", + "Active Collections" : "Aktivní kolekce", + "Active Filters" : "Aktivní filtry", + "Active Organisations" : "Aktivní organizace", + "Active filters:" : "Aktivní filtry:", + "Active organisation set successfully" : "Aktivní organizace byla úspěšně nastavena", + "Activity Period" : "Období aktivity", + "Add Groups" : "Přidat skupiny", + "Add Schema" : "Přidat schéma", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Přidejte názvy schémat, popisy a informace o registru pro poskytnutí bohatšího kontextu pro vyhledávání", + "Add to favorites" : "Přidat k oblíbeným", + "Added" : "Přidáno", + "Added to favorites" : "Přidáno k oblíbeným", + "Additional Information" : "Další informace", + "Additional feedback saved. Thank you!" : "Další zpětná vazba byla uložena. Děkujeme!", + "Admin" : "Administrátor", + "Advanced" : "Pokročilé", + "Advanced Filters" : "Pokročilé filtry", + "Advanced Options" : "Pokročilé možnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Pokročilé filtry nejsou dostupné při použití databázového zdroje. Přepněte na Auto nebo SOLR index pro možnosti filtrování.", + "After" : "Po", + "Agent deleted successfully" : "Agent byl úspěšně smazán", + "Agents" : "Agenti", + "All" : "Vše", + "All Categories" : "Všechny kategorie", + "All Collections" : "Všechny kolekce", + "All Confidence Levels" : "Všechny úrovně spolehlivosti", + "All ConfigSets" : "Všechny ConfigSety", + "All Files" : "Všechny soubory", + "All Levels" : "Všechny úrovně", + "All Types" : "Všechny typy", + "All Webhooks" : "Všechny webhooky", + "All actions" : "Všechny akce", + "All audit trails cleared successfully" : "Všechny auditní záznamy byly úspěšně vymazány", + "All registers" : "Všechny registry", + "All schemas" : "Všechna schémata", + "All searches" : "Všechna vyhledávání", + "All users" : "Všichni uživatelé", + "An error occurred while clearing audit trails" : "Při mazání auditních záznamů došlo k chybě", + "An error occurred while deleting the audit trail" : "Při mazání auditního záznamu došlo k chybě", + "An error occurred while deleting the view" : "Při mazání pohledu došlo k chybě", + "An error occurred while permanently deleting the objects" : "Při trvalém mazání objektů došlo k chybě", + "Analysis completed:" : "Analýza dokončena:", + "Analysis steps:" : "Kroky analýzy:", + "Analytics" : "Analytika", + "Analyze Objects" : "Analyzovat objekty", + "Analyze Properties" : "Analyzovat vlastnosti", + "Analyze existing properties for improvement opportunities" : "Analyzovat stávající vlastnosti za účelem nalezení možností zlepšení", + "Analyzing..." : "Analyzuje se...", + "Any favorites and sharing settings for this view" : "Veškerá oblíbená a nastavení sdílení pro tento pohled", + "Any user" : "Jakýkoli uživatel", + "App store cache invalidated successfully" : "Mezipaměť App store byla úspěšně zneplatněna", + "Application deleted successfully" : "Aplikace byla úspěšně smazána", + "Applications" : "Aplikace", + "Apply Changes" : "Použít změny", + "Archive" : "Archivovat", + "Archive conversation" : "Archivovat konverzaci", + "Archived conversations are hidden from your active list" : "Archivované konverzace jsou skryty z vašeho aktivního seznamu", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Opravdu chcete vyčistit staré záznamy vyhledávání? Tím se smažou položky starší než 30 dní.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Opravdu chcete smazat vybrané auditní záznamy? Tuto akci nelze vrátit zpět.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Opravdu chcete smazat vybrané záznamy vyhledávání? Tuto akci nelze vrátit zpět.", + "Are you sure you want to delete this ConfigSet?" : "Opravdu chcete smazat tento ConfigSet?", + "Are you sure you want to permanently delete" : "Opravdu chcete trvale smazat", + "Ask a question..." : "Položte otázku...", + "Ask questions about your data using natural language" : "Pokládejte otázky o svých datech pomocí přirozeného jazyka", + "Attempt" : "Pokus", + "Audit Trail Changes" : "Změny auditního záznamu", + "Audit Trail Details" : "Podrobnosti auditního záznamu", + "Audit Trail Management" : "Správa auditních záznamů", + "Audit Trail Statistics" : "Statistiky auditních záznamů", + "Audit Trails" : "Auditní záznamy", + "Audit trail data copied to clipboard" : "Data auditního záznamu byla zkopírována do schránky", + "Audit trail deleted successfully" : "Auditní záznam byl úspěšně smazán", + "Audit trail successfully deleted" : "Auditní záznam byl úspěšně smazán", + "Authentication" : "Ověření", + "Auto-commit disabled" : "Automatické potvrzení zakázáno", + "Auto-commit enabled" : "Automatické potvrzení povoleno", + "Auto-create Default Organisation" : "Automaticky vytvořit výchozí organizaci", + "Auto-retry failed vectorizations" : "Automaticky opakovat neúspěšné vektorizace", + "Automatically commit changes to SOLR index" : "Automaticky potvrzovat změny do SOLR indexu", + "Automatically create a default organisation if none exists when the app is initialized" : "Automaticky vytvořit výchozí organizaci, pokud žádná neexistuje při inicializaci aplikace", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Automaticky generovat vektorová vnoření z textových úseků při nahrávání a zpracování souborů", + "Automatically generate vector embeddings when objects are created or updated" : "Automaticky generovat vektorová vnoření při vytvoření nebo aktualizaci objektů", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Automaticky opakovat neúspěšné pokusy o vektorizaci (max. 3 opakování)", + "Available Filters" : "Dostupné filtry", + "Available Workflows" : "Dostupné pracovní postupy", + "Avg Execution Time" : "Průměrná doba provedení", + "Avg Members/Org" : "Průměrný počet členů/org.", + "Avg Object Views/Session" : "Průměrný počet zobrazení objektů/relaci", + "Avg Response Time" : "Průměrná doba odezvy", + "Avg Results/Search" : "Průměrný počet výsledků/vyhledávání", + "Avg Searches/Session" : "Průměrný počet vyhledávání/relaci", + "Back" : "Zpět", + "Back to Entities" : "Zpět na entity", + "Back to Registers" : "Zpět na registry", + "Back to Webhooks" : "Zpět na webhooky", + "Back to applications" : "Zpět na aplikace", + "Back to entities" : "Zpět na entity", + "Backend updated successfully. Please reload the application." : "Backend byl úspěšně aktualizován. Načtěte aplikaci znovu.", + "Base URL (Optional)" : "Základní URL (volitelné)", + "Basic Information" : "Základní informace", + "Batch Size" : "Velikost dávky", + "Batch extraction completed" : "Dávková extrakce dokončena", + "Before" : "Před", + "Before object vectorization can work:" : "Než může vektorizace objektů fungovat:", + "Behavior Issue" : "Problém s chováním", + "Bookmarks" : "Záložky", + "Blob storage has been retired. All objects now use magic tables." : "Úložiště blob bylo vyřazeno. Všechny objekty nyní používají magic tabulky.", + "Branch" : "Větev", + "Bulk delete operation completed successfully" : "Hromadná operace mazání byla úspěšně dokončena", + "Bulk save operation completed successfully" : "Hromadná operace ukládání byla úspěšně dokončena", + "Business Data" : "Obchodní data", + "Cache cleared successfully" : "Mezipaměť byla úspěšně vymazána", + "Calculate Sizes" : "Vypočítat velikosti", + "Cancel" : "Zrušit", + "Cannot delete: objects are still attached" : "Nelze smazat: objekty jsou stále připojeny", + "Category" : "Kategorie", + "Change Type" : "Typ změny", + "Changes" : "Změny", + "Changes copied to clipboard" : "Změny byly zkopírovány do schránky", + "Chat Model" : "Model chatu", + "Chat Provider" : "Poskytovatel chatu", + "Chat Provider (RAG)" : "Poskytovatel chatu (RAG)", + "Chat Settings" : "Nastavení chatu", + "Chat provider connection successful!" : "Připojení k poskytovateli chatu bylo úspěšné!", + "Chat settings" : "Nastavení chatu", + "Choose a register" : "Vyberte registr", + "Choose a schema" : "Vyberte schéma", + "Choose how vector similarity calculations are performed for semantic search" : "Zvolte způsob výpočtu vektorové podobnosti pro sémantické vyhledávání", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Zvolte, které typy souborů zahrnout do procesu vektorizace. Zpracovány budou pouze soubory s extrahovaným textem a úseky.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Zvolte, které pohledy zahrnout do procesu vektorizace. Ponechte prázdné pro zpracování všech pohledů na základě vaší konfigurace.", + "Chunk" : "Úsek", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Mazání úseků zatím není implementováno. Použijte koncové body založené na úsecích.", + "Chunks" : "Úseky", + "Chunks to Vectorize:" : "Úseky k vektorizaci:", + "Cleanup Old Trails" : "Vyčistit staré záznamy", + "Cleanup completed" : "Čištění dokončeno", + "Clear All Embeddings" : "Vymazat všechna vnoření", + "Clear Entries" : "Vymazat položky", + "Clear Filtered Audit Trails" : "Vymazat filtrované auditní záznamy", + "Clear Filters" : "Vymazat filtry", + "Clear Index" : "Vymazat index", + "Clear Selection" : "Vymazat výběr", + "Clear all" : "Vymazat vše", + "Clear all filters" : "Vymazat všechny filtry", + "Clear date range" : "Vymazat rozsah dat", + "Clearing..." : "Maže se...", + "Close" : "Zavřít", + "Collection Management" : "Správa kolekcí", + "Collection Name" : "Název kolekce", + "Collection assignments updated successfully" : "Přiřazení kolekcí byla úspěšně aktualizována", + "Collection cleared successfully" : "Kolekce byla úspěšně vymazána", + "Collection copied successfully" : "Kolekce byla úspěšně zkopírována", + "Collection created successfully" : "Kolekce byla úspěšně vytvořena", + "Collection deleted successfully" : "Kolekce byla úspěšně smazána", + "Collection used to store and index file metadata and content" : "Kolekce používaná k ukládání a indexování metadat a obsahu souborů", + "Collection used to store and index object data" : "Kolekce používaná k ukládání a indexování dat objektů", + "Collections:" : "Kolekce:", + "Commit Message" : "Zpráva potvrzení", + "Commit Within (ms)" : "Potvrdit do (ms)", + "Commit message" : "Zpráva potvrzení", + "Compare current schema with real object data" : "Porovnat aktuální schéma se skutečnými daty objektů", + "Completed" : "Dokončeno", + "Complex" : "Komplexní", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Komplexní dotazy: Pokročilá vyhledávání s více filtry, operátory a komplexními kombinacemi parametrů", + "Confidence Level" : "Úroveň spolehlivosti", + "Confidence Score" : "Skóre spolehlivosti", + "Config must be provided as an object" : "Konfigurace musí být poskytnuta jako objekt", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Správa ConfigSetů", + "ConfigSet Name" : "Název ConfigSetu", + "ConfigSet created successfully" : "ConfigSet byl úspěšně vytvořen", + "ConfigSet deleted successfully" : "ConfigSet byl úspěšně smazán", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSety definují schéma a konfiguraci pro vaše SOLR kolekce. Obsahují definice polí, analyzátory a další nastavení vyhledávání.", + "Configuration" : "Konfigurace", + "Configuration Data" : "Konfigurační data", + "Configuration saved" : "Konfigurace uložena", + "Configurations" : "Konfigurace", + "Configure Facets" : "Konfigurovat fazety", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurujte poskytovatele velkých jazykových modelů (LLM) pro funkce poháněné AI včetně sémantického vyhledávání, vnoření a chatu.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurujte základní nastavení připojení k vašemu SOLR serveru včetně ověření a síťových možností. Použijte oddělené dialogy správy ConfigSetů a kolekcí pro správu jader a kolekcí.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurujte způsob převodu databázových objektů na vektorová vnoření pro sémantické vyhledávání. Objekty jsou přímo vektorizovány bez nutnosti extrakce textu.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurujte způsob převodu objektů na text před vektorizací. Tato nastavení ovlivňují kvalitu vyhledávání a kontext.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurujte parametry pro vektorizaci souborů. Tento proces vygeneruje vektorová vnoření pro všechny extrahované úseky souborů.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurujte parametry pro vektorizaci objektů. Tento proces vygeneruje vektorová vnoření pro všechny objekty odpovídající filtrům vašeho pohledu.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurujte, které typy dat prohledávat a kolik zdrojů načíst", + "Connection Failed" : "Připojení selhalo", + "Connection Settings" : "Nastavení připojení", + "Connection Successful!" : "Připojení úspěšné!", + "Connection failed" : "Připojení selhalo", + "Connection protocol" : "Protokol připojení", + "Connection successful" : "Připojení úspěšné", + "Connection timeout in seconds" : "Časový limit připojení v sekundách", + "Constant delay between retries (5 minutes)" : "Konstantní prodleva mezi opakováními (5 minut)", + "Constraint Issue" : "Problém s omezením", + "Content Filters" : "Filtry obsahu", + "Continue to Properties" : "Pokračovat na vlastnosti", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Určete, které pohledy objektů by měly být vektorizovány za účelem snížení nákladů na API. Vektorizujte pouze pohledy, které těží ze sémantického vyhledávání.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Určete, které pohledy a nástroje může AI v této konverzaci použít. Ve výchozím nastavení jsou povoleny všechny schopnosti agenta.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Řídí náhodnost (0 = deterministické, 2 = velmi kreativní)", + "Conversation ID is required" : "ID konverzace je vyžadováno", + "Conversation archived" : "Konverzace archivována", + "Conversation archived successfully" : "Konverzace byla úspěšně archivována", + "Conversation cleared successfully" : "Konverzace byla úspěšně vymazána", + "Conversation deleted" : "Konverzace smazána", + "Conversation permanently deleted" : "Konverzace trvale smazána", + "Conversation renamed" : "Konverzace přejmenována", + "Conversation restored" : "Konverzace obnovena", + "Conversation title" : "Název konverzace", + "Copied!" : "Zkopírováno!", + "Copy" : "Kopírovat", + "Copy Changes" : "Kopírovat změny", + "Copy Collection" : "Kopírovat kolekci", + "Copy Data" : "Kopírovat data", + "Copy Full Data" : "Kopírovat všechna data", + "Copying..." : "Kopíruje se...", + "Count" : "Počet", + "Counting objects..." : "Počítají se objekty...", + "Costs" : "Náklady", + "Create" : "Vytvořit", + "Create Collection" : "Vytvořit kolekci", + "Create ConfigSet" : "Vytvořit ConfigSet", + "Create New Collection" : "Vytvořit novou kolekci", + "Create New ConfigSet" : "Vytvořit nový ConfigSet", + "Create Operations" : "Operace vytvoření", + "Create Webhook" : "Vytvořit webhook", + "Create a copy of collection:" : "Vytvořit kopii kolekce:", + "Create a new ConfigSet based on the _default template" : "Vytvořit nový ConfigSet na základě šablony _default", + "Create a new SOLR collection from an existing ConfigSet" : "Vytvořit novou SOLR kolekci ze stávajícího ConfigSetu", + "Create your first AI agent to get started." : "Vytvořte svého prvního AI agenta pro začátek.", + "Created" : "Vytvořeno", + "Created:" : "Vytvořeno:", + "Creating..." : "Vytváří se...", + "Current Filters" : "Aktuální filtry", + "Current Type:" : "Aktuální typ:", + "Custom API endpoint if using a different region" : "Vlastní koncový bod API při použití jiného regionu", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Vlastní HTTP hlavičky (jedna na řádek, formát: Header-Name: value)", + "Daily" : "Denně", + "Dashboard" : "Nástěnka", + "Data Source" : "Zdroj dat", + "Data sources" : "Zdroje dat", + "Data type does not match observed values" : "Datový typ neodpovídá pozorovaným hodnotám", + "Database URL" : "URL databáze", + "Database information refreshed" : "Informace o databázi obnoveny", + "Date Range" : "Rozsah dat", + "Default" : "Výchozí", + "Default Organisation" : "Výchozí organizace", + "Default: 5" : "Výchozí: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Prodlevy se s každým pokusem zdvojnásobí (2, 4, 8 minut...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Prodlevy se lineárně zvyšují (5, 10, 15 minut...)", + "Delete" : "Smazat", + "Delete Application" : "Smazat aplikaci", + "Delete Audit Trail" : "Smazat auditní záznam", + "Delete Collection" : "Smazat kolekci", + "Delete ConfigSet" : "Smazat ConfigSet", + "Delete Objects" : "Smazat objekty", + "Delete Operations" : "Operace mazání", + "Delete View" : "Smazat pohled", + "Delete all objects in this schema" : "Smazat všechny objekty v tomto schématu", + "Delete permanently" : "Smazat trvale", + "Delete this conversation?" : "Smazat tuto konverzaci?", + "Delete view" : "Smazat pohled", + "Deleted" : "Smazáno", + "Deleted By" : "Smazal", + "Deleted Date" : "Datum smazání", + "Deleted Items Management" : "Správa smazaných položek", + "Deleted This Week" : "Smazáno tento týden", + "Deleted Today" : "Smazáno dnes", + "Deleted:" : "Smazáno:", + "Deleting..." : "Maže se...", + "Deletion Date Range" : "Rozsah data smazání", + "Deletion Statistics" : "Statistiky mazání", + "Deprecated" : "Zastaralé", + "Description" : "Popis", + "Description:" : "Popis:", + "Detect data types and patterns" : "Detekovat datové typy a vzory", + "Detected At" : "Detekováno v", + "Detected Format:" : "Detekovaný formát:", + "Detected Issues:" : "Detekované problémy:", + "Disable" : "Zakázat", + "Disabled" : "Zakázáno", + "Discover Files" : "Objevit soubory", + "Discovered Properties" : "Objevené vlastnosti", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Chcete trvale smazat všechny filtrované položky auditních záznamů? Tuto akci nelze vrátit zpět.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Chcete trvale smazat tuto položku auditního záznamu? Tuto akci nelze vrátit zpět.", + "Documents" : "Dokumenty", + "Download API Spec" : "Stáhnout specifikaci API", + "Edit" : "Upravit", + "Edit Register" : "Upravit registr", + "Edit Schema" : "Upravit schéma", + "Edit View" : "Upravit pohled", + "Edit Webhook" : "Upravit webhook", + "Edit view" : "Upravit pohled", + "Edit view details" : "Upravit podrobnosti pohledu", + "Effectiveness" : "Účinnost", + "Email" : "E-mail", + "Emails" : "E-maily", + "Embedding Model" : "Model vnoření", + "Embedding Provider" : "Poskytovatel vnoření", + "Embedding generated successfully" : "Vnoření bylo úspěšně vygenerováno", + "Embedding provider connection successful!" : "Připojení k poskytovateli vnoření bylo úspěšné!", + "Enable" : "Povolit", + "Enable auto-creation" : "Povolit automatické vytváření", + "Enable automatic file vectorization" : "Povolit automatickou vektorizaci souborů", + "Enable automatic object vectorization" : "Povolit automatickou vektorizaci objektů", + "Enable automatic synchronization" : "Povolit automatickou synchronizaci", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Povolit podrobné protokolování pro SOLR operace (doporučeno pro ladění)", + "Enable faceting" : "Povolit fazetování", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Povolte nebo zakažte funkce LLM. Konfigurujte poskytovatele a modely pomocí tlačítka Konfigurace LLM výše.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Povolte nebo zakažte integraci vyhledávání SOLR. Konfigurujte nastavení připojení pomocí tlačítka Nastavení připojení výše.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Povolte nebo zakažte integraci pracovních postupů n8n. Konfigurujte nastavení připojení níže.", + "Enable or disable this webhook" : "Povolit nebo zakázat tento webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Povolit vektorizaci pro všechny stávající a budoucí pohledy (může zvýšit náklady)", + "Enabled" : "Povoleno", + "Endpoints" : "Koncové body", + "Enter ConfigSet name" : "Zadejte název ConfigSetu", + "Enter collection name" : "Zadejte název kolekce", + "Enter description (optional)..." : "Zadejte popis (volitelné)...", + "Enter new collection name" : "Zadejte název nové kolekce", + "Enter object ID" : "Zadejte ID objektu", + "Enter search term" : "Zadejte hledaný výraz", + "Enter view name..." : "Zadejte název pohledu...", + "Enter webhook name" : "Zadejte název webhooku", + "Entities" : "Entity", + "Entity Information" : "Informace o entitě", + "Entity deleted successfully" : "Entita byla úspěšně smazána", + "Entity not found" : "Entita nenalezena", + "Entries to be deleted:" : "Položky ke smazání:", + "Enum Issue" : "Problém s výčtem", + "Enum constraint is missing" : "Chybí omezení výčtu", + "Error" : "Chyba", + "Error Details" : "Podrobnosti chyby", + "Error Information" : "Informace o chybě", + "Error Message" : "Chybová zpráva", + "Error loading application" : "Chyba při načítání aplikace", + "Error loading audit trails" : "Chyba při načítání auditních záznamů", + "Error loading entity" : "Chyba při načítání entity", + "Error loading search trails" : "Chyba při načítání záznamů vyhledávání", + "Estimated Batches:" : "Odhadované dávky:", + "Estimated Cost:" : "Odhadované náklady:", + "Estimated Duration:" : "Odhadovaná doba trvání:", + "Event" : "Událost", + "Event Property for Payload" : "Vlastnost události pro datovou část", + "Events" : "Události", + "Example Value" : "Příklad hodnoty", + "Exclusive Maximum" : "Vylučující maximum", + "Exclusive Minimum" : "Vylučující minimum", + "Execution Mode" : "Režim provádění", + "Execution Time" : "Doba provedení", + "Execution Time Range" : "Rozsah doby provedení", + "Existing Improvements" : "Stávající vylepšení", + "Exponential" : "Exponenciální", + "Export" : "Exportovat", + "Export completed successfully" : "Export byl úspěšně dokončen", + "Export, view, or delete audit trails" : "Exportovat, zobrazit nebo smazat auditní záznamy", + "Extend Schema" : "Rozšířit schéma", + "Extended" : "Rozšířené", + "Extensions" : "Rozšíření", + "Extract Pending Files" : "Extrahovat čekající soubory", + "Extract properties from each object" : "Extrahovat vlastnosti z každého objektu", + "Extracted At" : "Extrahováno v", + "Extraction Status" : "Stav extrakce", + "Facet configuration updated successfully" : "Konfigurace fazet byla úspěšně aktualizována", + "Facets discovered and configured successfully" : "Fazety byly úspěšně objeveny a nakonfigurovány", + "Facets discovered successfully" : "Fazety byly úspěšně objeveny", + "Failed" : "Selhalo", + "Failed to analyze schema properties" : "Nepodařilo se analyzovat vlastnosti schématu", + "Failed to archive conversation" : "Nepodařilo se archivovat konverzaci", + "Failed to calculate sizes" : "Nepodařilo se vypočítat velikosti", + "Failed to clear collection" : "Nepodařilo se vymazat kolekci", + "Failed to copy changes" : "Nepodařilo se zkopírovat změny", + "Failed to copy data" : "Nepodařilo se zkopírovat data", + "Failed to copy data to clipboard" : "Nepodařilo se zkopírovat data do schránky", + "Failed to create conversation" : "Nepodařilo se vytvořit konverzaci", + "Failed to create or find project" : "Nepodařilo se vytvořit nebo najít projekt", + "Failed to delete collection" : "Nepodařilo se smazat kolekci", + "Failed to delete conversation" : "Nepodařilo se smazat konverzaci", + "Failed to delete webhook" : "Nepodařilo se smazat webhook", + "Failed to download API specification" : "Nepodařilo se stáhnout specifikaci API", + "Failed to get SOLR field configuration" : "Nepodařilo se získat konfiguraci SOLR polí", + "Failed to get entity categories" : "Nepodařilo se získat kategorie entit", + "Failed to get entity statistics" : "Nepodařilo se získat statistiky entit", + "Failed to get entity types" : "Nepodařilo se získat typy entit", + "Failed to load LLM configuration" : "Nepodařilo se načíst konfiguraci LLM", + "Failed to load Nextcloud groups" : "Nepodařilo se načíst skupiny Nextcloud", + "Failed to load conversation" : "Nepodařilo se načíst konverzaci", + "Failed to load entities" : "Nepodařilo se načíst entity", + "Failed to load entity" : "Nepodařilo se načíst entitu", + "Failed to load files" : "Nepodařilo se načíst soubory", + "Failed to load organisations" : "Nepodařilo se načíst organizace", + "Failed to load templates" : "Nepodařilo se načíst šablony", + "Failed to load webhooks" : "Nepodařilo se načíst webhooky", + "Failed to load workflows" : "Nepodařilo se načíst pracovní postupy", + "Failed to refresh database information" : "Nepodařilo se obnovit informace o databázi", + "Failed to reindex collection" : "Nepodařilo se přeindexovat kolekci", + "Failed to rename conversation" : "Nepodařilo se přejmenovat konverzaci", + "Failed to restore conversation" : "Nepodařilo se obnovit konverzaci", + "Failed to retry extraction" : "Nepodařilo se opakovat extrakci", + "Failed to retry webhook" : "Nepodařilo se opakovat webhook", + "Failed to save GitHub token" : "Nepodařilo se uložit GitHub token", + "Failed to save GitLab URL" : "Nepodařilo se uložit GitLab URL", + "Failed to save GitLab token" : "Nepodařilo se uložit GitLab token", + "Failed to save additional feedback" : "Nepodařilo se uložit další zpětnou vazbu", + "Failed to save n8n configuration" : "Nepodařilo se uložit konfiguraci n8n", + "Failed to save roles" : "Nepodařilo se uložit role", + "Failed to save settings" : "Nepodařilo se uložit nastavení", + "Failed to save webhook" : "Nepodařilo se uložit webhook", + "Failed to send feedback" : "Nepodařilo se odeslat zpětnou vazbu", + "Failed to test webhook" : "Nepodařilo se otestovat webhook", + "Failed to update favorite status" : "Nepodařilo se aktualizovat stav oblíbené", + "Failed to update schema properties" : "Nepodařilo se aktualizovat vlastnosti schématu", + "Failed to update view" : "Nepodařilo se aktualizovat pohled", + "Failed to update webhook" : "Nepodařilo se aktualizovat webhook", + "Feedback recorded" : "Zpětná vazba zaznamenána", + "Fewer sources (1-3):" : "Méně zdrojů (1-3):", + "Field" : "Pole", + "File" : "Soubor", + "File Chunk Prediction" : "Predikce úseků souboru", + "File Collection" : "Kolekce souborů", + "File Management" : "Správa souborů", + "File Name" : "Název souboru", + "File Path" : "Cesta k souboru", + "File Type Selection" : "Výběr typu souboru", + "File Vectorization" : "Vektorizace souborů", + "File Warmup" : "Zahřátí souborů", + "File actions menu" : "Nabídka akcí souboru", + "File anonymized successfully" : "Soubor byl úspěšně anonymizován", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vektorizace úseků souborů zatím není implementována. Úseky jsou připraveny a uloženy, ale služba vektorizace je ve vývoji.", + "File collection not configured" : "Kolekce souborů není nakonfigurována", + "File discovery completed" : "Objevování souborů dokončeno", + "File extraction completed" : "Extrakce souboru dokončena", + "File extraction queued" : "Extrakce souboru zařazena do fronty", + "File indexed successfully" : "Soubor byl úspěšně indexován", + "File is already anonymized" : "Soubor je již anonymizován", + "File not found" : "Soubor nenalezen", + "File settings updated successfully" : "Nastavení souborů bylo úspěšně aktualizováno", + "File sources" : "Zdroje souborů", + "File vectorization configuration saved successfully" : "Konfigurace vektorizace souborů byla úspěšně uložena", + "File vectorization started. Check the statistics section for progress." : "Vektorizace souborů byla zahájena. Zkontrolujte sekci statistik pro průběh.", + "File warmup completed" : "Zahřátí souborů dokončeno", + "Files" : "Soubory", + "Files with Completed Extraction:" : "Soubory s dokončenou extrakcí:", + "Files → fileCollection, Objects → objectCollection" : "Soubory → fileCollection, Objekty → objectCollection", + "Filter Audit Trails" : "Filtrovat auditní záznamy", + "Filter Deleted Items" : "Filtrovat smazané položky", + "Filter Objects" : "Filtrovat objekty", + "Filter Properties" : "Filtrovat vlastnosti", + "Filter Search Trails" : "Filtrovat záznamy vyhledávání", + "Filter Statistics" : "Statistiky filtru", + "Filter and analyze search trail entries" : "Filtrovat a analyzovat položky záznamů vyhledávání", + "Filter and manage audit trail entries" : "Filtrovat a spravovat položky auditních záznamů", + "Filter and manage soft deleted items" : "Filtrovat a spravovat měkce smazané položky", + "Filter by object ID" : "Filtrovat podle ID objektu", + "Filter by search term" : "Filtrovat podle hledaného výrazu", + "Filter by webhook" : "Filtrovat podle webhooku", + "Filter data loaded automatically. Use the filters below to refine your search." : "Data filtru byla načtena automaticky. Použijte filtry níže pro upřesnění vašeho vyhledávání.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrovat spouštěče webhooku podle vlastností datové části (jedna na řádek, formát: key: value)", + "Filtered" : "Filtrováno", + "Filters" : "Filtry", + "Fireworks AI Chat Settings" : "Nastavení chatu Fireworks AI", + "Fireworks AI Embedding Configuration" : "Konfigurace vnoření Fireworks AI", + "First" : "První", + "Fixed" : "Opraveno", + "For chat and retrieval-augmented generation" : "Pro chat a generování rozšířené o vyhledávání", + "For vector embeddings and semantic search" : "Pro vektorová vnoření a sémantické vyhledávání", + "Format" : "Formát", + "Format Issue" : "Problém s formátem", + "Format constraint is missing" : "Chybí omezení formátu", + "From Date" : "Od data", + "From date" : "Od data", + "Full data copied to clipboard" : "Všechna data byla zkopírována do schránky", + "General Issue" : "Obecný problém", + "Generate recommendations and confidence scores" : "Generovat doporučení a skóre spolehlivosti", + "Generate vectors immediately when new objects are created" : "Generovat vektory okamžitě při vytvoření nových objektů", + "GitHub token is valid" : "GitHub token je platný", + "GitHub token removed successfully" : "GitHub token byl úspěšně odstraněn", + "GitHub token saved successfully" : "GitHub token byl úspěšně uložen", + "GitLab URL saved successfully" : "GitLab URL byla úspěšně uložena", + "GitLab token is valid" : "GitLab token je platný", + "GitLab token saved successfully" : "GitLab token byl úspěšně uložen", + "HTTP Method" : "HTTP metoda", + "HTTP method used to send webhook requests" : "HTTP metoda použitá k odeslání požadavků webhooku", + "Headers" : "Hlavičky", + "Health" : "Stav", + "Heartbeat successful - connection kept alive" : "Heartbeat úspěšný – připojení udržováno aktivní", + "Helpful" : "Užitečné", + "Hide Filters" : "Skrýt filtry", + "Hide in forms" : "Skrýt ve formulářích", + "Hide in list view" : "Skrýt v seznamovém zobrazení", + "High" : "Vysoká", + "High Confidence" : "Vysoká spolehlivost", + "Host" : "Hostitel", + "Hourly" : "Hodinově", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Jak hluboko procházet vnořené vlastnosti objektu (1-20). Vyšší hodnoty zachytí více detailů, ale zvyšují velikost vektoru.", + "How often to check for updates (1-168 hours)" : "Jak často kontrolovat aktualizace (1-168 hodin)", + "How to handle retries for failed webhook deliveries" : "Jak řešit opakování neúspěšných doručení webhooku", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Omezení rychlosti pro IP byla úspěšně vymazána", + "Identify properties not in the schema" : "Identifikovat vlastnosti, které nejsou ve schématu", + "Immutable" : "Neměnné", + "Import" : "Importovat", + "Import successful" : "Import úspěšný", + "Improved Property" : "Vylepšená vlastnost", + "In use" : "Používá se", + "Inactive" : "Neaktivní", + "Include IDs and names of related objects for better contextual search" : "Zahrnout ID a názvy souvisejících objektů pro lepší kontextové vyhledávání", + "Include related object references" : "Zahrnout odkazy na související objekty", + "Include schema and register metadata" : "Zahrnout metadata schématu a registru", + "Initialization failed" : "Inicializace selhala", + "Initialize Project" : "Inicializovat projekt", + "Inspect Fields" : "Prozkoumat pole", + "Inspect Index" : "Prozkoumat index", + "Invalid" : "Neplatné", + "Invalid batch size. Must be between 1 and 5000" : "Neplatná velikost dávky. Musí být mezi 1 a 5000", + "Invalid field name provided" : "Byl poskytnut neplatný název pole", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Neplatné maxObjects. Musí být 0 (vše) nebo kladné číslo", + "Invalid:" : "Neplatné:", + "Issue" : "Problém", + "Items per page:" : "Položek na stránku:", + "Just now" : "Právě teď", + "LLM Configuration" : "Konfigurace LLM", + "LLM actions menu" : "Nabídka akcí LLM", + "LLM configuration saved successfully" : "Konfigurace LLM byla úspěšně uložena", + "LLM features disabled" : "Funkce LLM zakázány", + "LLM features enabled" : "Funkce LLM povoleny", + "LLM must be enabled with an embedding provider configured" : "LLM musí být povoleno s nakonfigurovaným poskytovatelem vnoření", + "LLM settings updated successfully" : "Nastavení LLM bylo úspěšně aktualizováno", + "Last" : "Poslední", + "Last Triggered" : "Naposledy spuštěno", + "Length Range:" : "Rozsah délky:", + "Linear" : "Lineární", + "Load Advanced Filters" : "Načíst pokročilé filtry", + "Load advanced filters with live data from your search index" : "Načíst pokročilé filtry s živými daty z vašeho vyhledávacího indexu", + "Load view" : "Načíst pohled", + "Loading ConfigSets..." : "Načítají se ConfigSety...", + "Loading advanced filters..." : "Načítají se pokročilé filtry...", + "Loading agents..." : "Načítají se agenti...", + "Loading application details..." : "Načítají se podrobnosti aplikace...", + "Loading applications..." : "Načítají se aplikace...", + "Loading archived conversations..." : "Načítají se archivované konverzace...", + "Loading audit trails..." : "Načítají se auditní záznamy...", + "Loading collections..." : "Načítají se kolekce...", + "Loading configurations..." : "Načítají se konfigurace...", + "Loading conversation..." : "Načítá se konverzace...", + "Loading conversations..." : "Načítají se konverzace...", + "Loading deleted items..." : "Načítají se smazané položky...", + "Loading events..." : "Načítají se události...", + "Loading filter data..." : "Načítají se data filtru...", + "Loading groups..." : "Načítají se skupiny...", + "Loading log details..." : "Načítají se podrobnosti protokolu...", + "Loading registers..." : "Načítají se registry...", + "Loading schemas..." : "Načítají se schémata...", + "Loading search trails..." : "Načítají se záznamy vyhledávání...", + "Loading sources..." : "Načítají se zdroje...", + "Loading statistics..." : "Načítají se statistiky...", + "Loading users..." : "Načítají se uživatelé...", + "Loading views..." : "Načítají se pohledy...", + "Loading..." : "Načítá se...", + "Local" : "Místní", + "Local Version" : "Místní verze", + "Locked" : "Uzamčeno", + "Locked:" : "Uzamčeno:", + "Login successful" : "Přihlášení úspěšné", + "Logs" : "Protokoly", + "Low" : "Nízká", + "Low Confidence" : "Nízká spolehlivost", + "Lucene Version:" : "Verze Lucene:", + "Magic table synchronized successfully" : "Magic tabulka byla úspěšně synchronizována", + "Manage Organisation Roles" : "Spravovat role organizace", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Spravujte SOLR kolekce (datová úložiště) a přiřazujte je pro objekty a soubory.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Spravujte SOLR ConfigSety (konfigurační šablony) pro vaše kolekce.", + "Manage and configure agents for automated tasks" : "Spravujte a konfigurujte agenty pro automatizované úlohy", + "Manage and monitor file text extraction status" : "Spravujte a sledujte stav extrakce textu ze souborů", + "Manage and restore soft deleted items from your registers" : "Spravujte a obnovujte měkce smazané položky z vašich registrů", + "Manage and view detected entities from files and objects" : "Spravujte a prohlížejte detekované entity ze souborů a objektů", + "Manage document templates and themes" : "Spravujte šablony dokumentů a motivy", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Spravujte pracovní postupy n8n pro automatizaci OpenRegister. Pracovní postupy budou uloženy v nakonfigurovaném projektu.", + "Manage webhooks for event-driven integrations" : "Spravujte webhooky pro integrace řízené událostmi", + "Manage your applications and modules" : "Spravujte své aplikace a moduly", + "Manage your data registers and their configurations" : "Spravujte své datové registry a jejich konfigurace", + "Manage your data schemas and their properties" : "Spravujte svá datová schémata a jejich vlastnosti", + "Manage your data sources and their configurations" : "Spravujte své datové zdroje a jejich konfigurace", + "Manage your saved search configurations" : "Spravujte své uložené konfigurace vyhledávání", + "Manage your system configurations and settings" : "Spravujte své systémové konfigurace a nastavení", + "Managed" : "Spravované", + "Max Files (0 = all)" : "Max. souborů (0 = vše)", + "Max Length" : "Max. délka", + "Max Length:" : "Max. délka:", + "Max Objects (0 = all)" : "Max. objektů (0 = vše)", + "Max Retries" : "Max. opakování", + "Max Shards/Node" : "Max. shardů/uzel", + "Max execution time (ms)" : "Max. doba provedení (ms)", + "Max ms" : "Max. ms", + "Max result count" : "Max. počet výsledků", + "Max results" : "Max. výsledků", + "Maximum" : "Maximum", + "Maximum Nesting Depth" : "Maximální hloubka vnoření", + "Maximum length constraint is missing" : "Chybí omezení maximální délky", + "Maximum length is too restrictive" : "Maximální délka je příliš omezující", + "Maximum number of files to process. Set to 0 to process all files." : "Maximální počet souborů ke zpracování. Nastavte na 0 pro zpracování všech souborů.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maximální počet objektů ke zpracování. Nastavte na 0 pro zpracování všech objektů.", + "Maximum number of retry attempts for failed deliveries" : "Maximální počet pokusů o opakování pro neúspěšná doručení", + "Maximum time to wait before committing changes" : "Maximální doba čekání před potvrzením změn", + "Maximum tokens to generate" : "Maximální počet tokenů ke generování", + "Maximum value constraint is missing" : "Chybí omezení maximální hodnoty", + "Maximum value is too restrictive" : "Maximální hodnota je příliš omezující", + "Medium" : "Střední", + "Medium Confidence" : "Střední spolehlivost", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Středně složité dotazy: Vyhledávání s určitým filtrováním nebo více parametry (např. rozsahy dat, konkrétní registry/schémata)", + "Members of selected groups can access this view" : "Členové vybraných skupin mají přístup k tomuto pohledu", + "Members:" : "Členové:", + "Memory prediction calculated successfully" : "Predikce paměti byla úspěšně vypočtena", + "Message content is required" : "Obsah zprávy je vyžadován", + "Message does not belong to this conversation" : "Zpráva nepatří do této konverzace", + "Metadata Filters" : "Filtry metadat", + "Method" : "Metoda", + "Min Length" : "Min. délka", + "Min execution time (ms)" : "Min. doba provedení (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. počet výsledků", + "Min results" : "Min. výsledků", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Chybí omezení minimální hodnoty", + "Minimum value is too restrictive" : "Minimální hodnota je příliš omezující", + "Missing object fields created successfully" : "Chybějící pole objektu byla úspěšně vytvořena", + "Mode:" : "Režim:", + "Model" : "Model", + "Model Name" : "Název modelu", + "Modified" : "Upraveno", + "Monthly" : "Měsíčně", + "More sources (10-20):" : "Více zdrojů (10-20):", + "Most Active Objects" : "Nejaktivnější objekty", + "Multiple Of" : "Násobek", + "Name" : "Název", + "Names cache warmup completed" : "Zahřátí mezipaměti názvů dokončeno", + "New Collection Name" : "Název nové kolekce", + "New Conversation" : "Nová konverzace", + "New Properties" : "Nové vlastnosti", + "New Property" : "Nová vlastnost", + "New Value" : "Nová hodnota", + "New users without specific organisation membership will be automatically added to this organisation" : "Noví uživatelé bez konkrétního členství v organizaci budou automaticky přidáni do této organizace", + "Next" : "Další", + "No" : "Ne", + "No ConfigSets found" : "Nebyly nalezeny žádné ConfigSety", + "No GitHub token configured" : "Není nakonfigurován žádný GitHub token", + "No GitHub token provided" : "Nebyl poskytnut žádný GitHub token", + "No GitLab token provided" : "Nebyl poskytnut žádný GitLab token", + "No Organisation" : "Žádná organizace", + "No active filters" : "Žádné aktivní filtry", + "No activity data available" : "Nejsou dostupná žádná data o aktivitě", + "No agents available" : "Nejsou dostupní žádní agenti", + "No agents found" : "Nebyli nalezeni žádní agenti", + "No app store cache exists yet" : "Zatím neexistuje žádná mezipaměť App store", + "No applications are available." : "Nejsou dostupné žádné aplikace.", + "No applications found" : "Nebyly nalezeny žádné aplikace", + "No archived conversations" : "Žádné archivované konverzace", + "No audit trail entries found" : "Nebyly nalezeny žádné položky auditních záznamů", + "No changes recorded" : "Nebyly zaznamenány žádné změny", + "No chunks to vectorize" : "Žádné úseky k vektorizaci", + "No collections found" : "Nebyly nalezeny žádné kolekce", + "No configuration data" : "Žádná konfigurační data", + "No configurations are available." : "Nejsou dostupné žádné konfigurace.", + "No configurations found" : "Nebyly nalezeny žádné konfigurace", + "No configurations found for this application." : "Pro tuto aplikaci nebyly nalezeny žádné konfigurace.", + "No conversations yet. Create a new one to get started!" : "Zatím žádné konverzace. Vytvořte novou pro začátek!", + "No data available" : "Nejsou dostupná žádná data", + "No data available for chart" : "Nejsou dostupná žádná data pro graf", + "No deleted items found" : "Nebyly nalezeny žádné smazané položky", + "No deletion data available" : "Nejsou dostupná žádná data o mazání", + "No description found" : "Nebyl nalezen žádný popis", + "No description provided" : "Nebyl poskytnut žádný popis", + "No entities detected in this file. Run text extraction first." : "V tomto souboru nebyly detekovány žádné entity. Nejprve spusťte extrakci textu.", + "No entities found" : "Nebyly nalezeny žádné entity", + "No entities have been detected yet" : "Zatím nebyly detekovány žádné entity", + "No events found" : "Nebyly nalezeny žádné události", + "No expired audit trails found to clear" : "Nebyly nalezeny žádné expirované auditní záznamy k vymazání", + "No facetable fields available. Select a register and schema to see available filters." : "Nejsou dostupná žádná fazetovatelná pole. Vyberte registr a schéma pro zobrazení dostupných filtrů.", + "No files found" : "Nebyly nalezeny žádné soubory", + "No files have been extracted yet" : "Zatím nebyly extrahovány žádné soubory", + "No files to process" : "Žádné soubory ke zpracování", + "No files to reindex" : "Žádné soubory k přeindexování", + "No filters are currently active. This will delete ALL audit trail entries!" : "Žádné filtry nejsou aktuálně aktivní. Tím se smažou VŠECHNY položky auditních záznamů!", + "No log entries found" : "Nebyly nalezeny žádné položky protokolu", + "No logs are available for this configuration." : "Pro tuto konfiguraci nejsou dostupné žádné protokoly.", + "No logs are available for this source." : "Pro tento zdroj nejsou dostupné žádné protokoly.", + "No logs found" : "Nebyly nalezeny žádné protokoly", + "No mismatched fields found - SOLR schema is properly configured" : "Nebyla nalezena žádná neodpovídající pole – SOLR schéma je správně nakonfigurováno", + "No objects found" : "Nebyly nalezeny žádné objekty", + "No objects found to vectorize" : "Nebyly nalezeny žádné objekty k vektorizaci", + "No objects selected for deletion" : "Nebyly vybrány žádné objekty ke smazání", + "No objects to delete" : "Žádné objekty ke smazání", + "No objects were permanently deleted" : "Žádné objekty nebyly trvale smazány", + "No properties found" : "Nebyly nalezeny žádné vlastnosti", + "No properties match your filters." : "Žádné vlastnosti neodpovídají vašim filtrům.", + "No purge date set" : "Není nastaveno žádné datum vymazání", + "No register data available" : "Nejsou dostupná žádná data registru", + "No registers found" : "Nebyly nalezeny žádné registry", + "No registers found for this application." : "Pro tuto aplikaci nebyly nalezeny žádné registry.", + "No relations found" : "Nebyly nalezeny žádné vztahy", + "No request body available" : "Není dostupné žádné tělo požadavku", + "No response body available" : "Není dostupné žádné tělo odpovědi", + "No saved views yet. Create one in the Search tab!" : "Zatím žádné uložené pohledy. Vytvořte jeden v záložce Vyhledávání!", + "No schema data available" : "Nejsou dostupná žádná data schématu", + "No schema selected for exploration" : "Nebylo vybráno žádné schéma k prozkoumání", + "No schemas found" : "Nebyla nalezena žádná schémata", + "No schemas found for this application." : "Pro tuto aplikaci nebyla nalezena žádná schémata.", + "No search terms data available" : "Nejsou dostupná žádná data o hledaných výrazech", + "No search trail entries found" : "Nebyly nalezeny žádné položky záznamů vyhledávání", + "No sources are available." : "Nejsou dostupné žádné zdroje.", + "No sources found" : "Nebyly nalezeny žádné zdroje", + "No templates found" : "Nebyly nalezeny žádné šablony", + "No templates have been created yet" : "Zatím nebyly vytvořeny žádné šablony", + "No valid magic-mapped register+schema combinations found" : "Nebyly nalezeny žádné platné kombinace registr+schéma s magic mapováním", + "No views found. Create views first before configuring vectorization." : "Nebyly nalezeny žádné pohledy. Nejprve vytvořte pohledy před konfigurací vektorizace.", + "No views match your search" : "Žádné pohledy neodpovídají vašemu vyhledávání", + "No webhooks found" : "Nebyly nalezeny žádné webhooky", + "No webhooks have been configured yet" : "Zatím nebyly nakonfigurovány žádné webhooky", + "No workflows found in this project. Create workflows in the n8n editor." : "V tomto projektu nebyly nalezeny žádné pracovní postupy. Vytvořte pracovní postupy v editoru n8n.", + "None" : "Žádné", + "Not helpful" : "Neužitečné", + "Not in use" : "Nepoužívá se", + "Number Constraints" : "Číselná omezení", + "Number of chunks to vectorize in one API call (1-100)." : "Počet úseků k vektorizaci v jednom volání API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Počet úseků k vektorizaci v jednom volání API. Vyšší = rychlejší, ale více paměti. Doporučeno: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Počet objektů ke zpracování v každé dávce (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Počet objektů k vektorizaci v jednom volání API. Vyšší = rychlejší, ale více paměti. Doporučeno: 10-50.", + "Numeric Range:" : "Číselný rozsah:", + "Object" : "Objekt", + "Object Collection" : "Kolekce objektů", + "Object Count Prediction" : "Predikce počtu objektů", + "Object ID" : "ID objektu", + "Object Management" : "Správa objektů", + "Object Vectorization" : "Vektorizace objektů", + "Object Warmup" : "Zahřátí objektů", + "Object collection not configured" : "Kolekce objektů není nakonfigurována", + "Object not found" : "Objekt nenalezen", + "Object permanently deleted" : "Objekt trvale smazán", + "Object restored successfully" : "Objekt byl úspěšně obnoven", + "Object settings updated successfully" : "Nastavení objektu bylo úspěšně aktualizováno", + "Object sources" : "Zdroje objektů", + "Object unlocked successfully" : "Objekt byl úspěšně odemčen", + "Object vectorization configuration saved successfully" : "Konfigurace vektorizace objektů byla úspěšně uložena", + "Object vectorized successfully" : "Objekt byl úspěšně vektorizován", + "Object:" : "Objekt:", + "Objects" : "Objekty", + "Objects Analyzed" : "Analyzované objekty", + "Objects Distribution" : "Rozložení objektů", + "Objects being analyzed" : "Analyzované objekty", + "Objects by Register" : "Objekty podle registru", + "Objects by Schema" : "Objekty podle schématu", + "Objects deletion completed successfully" : "Mazání objektů bylo úspěšně dokončeno", + "Objects in schema" : "Objekty ve schématu", + "Objects to Process:" : "Objekty ke zpracování:", + "Objects to be analyzed" : "Objekty k analýze", + "Objects to be deleted" : "Objekty ke smazání", + "Objects to be validated" : "Objekty k ověření", + "Objects will be serialized as JSON text before vectorization" : "Objekty budou před vektorizací serializovány jako JSON text", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekty budou měkce smazány (označeny jako smazané, ale ponechány v databázi). V případě potřeby je lze později obnovit.", + "Old Value" : "Stará hodnota", + "Oldest Item (days)" : "Nejstarší položka (dny)", + "Ollama Chat Settings" : "Nastavení chatu Ollama", + "Ollama Embedding Configuration" : "Konfigurace vnoření Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Pouze se změnami", + "Open Register Objects" : "Objekty Open Register", + "Open n8n Editor" : "Otevřít editor n8n", + "OpenAI Chat Settings" : "Nastavení chatu OpenAI", + "OpenAI Embedding Configuration" : "Konfigurace vnoření OpenAI", + "OpenRegister Settings" : "Nastavení OpenRegister", + "Optional webhook secret for signature verification" : "Volitelný tajný klíč webhooku pro ověření podpisu", + "Order" : "Pořadí", + "Organisation" : "Organizace", + "Organisation ID" : "ID organizace", + "Organisation Members" : "Členové organizace", + "Organisation Statistics" : "Statistiky organizace", + "Organisation created successfully" : "Organizace byla úspěšně vytvořena", + "Organisation settings saved successfully" : "Nastavení organizace bylo úspěšně uloženo", + "Organisations" : "Organizace", + "Organization" : "Organizace", + "Organization ID (Optional)" : "ID organizace (volitelné)", + "Orphaned Items" : "Osiřelé položky", + "Owner" : "Vlastník", + "Owner:" : "Vlastník:", + "Parallel Mode (Faster, more resource intensive)" : "Paralelní režim (rychlejší, náročnější na zdroje)", + "Parallel:" : "Paralelní:", + "Password" : "Heslo", + "Password for SOLR authentication (optional)" : "Heslo pro SOLR ověření (volitelné)", + "Path" : "Cesta", + "Path in repository" : "Cesta v repozitáři", + "Path where the configuration file will be saved in the repository" : "Cesta, kam bude konfigurační soubor uložen v repozitáři", + "Path where the register OAS file will be saved in the repository" : "Cesta, kam bude OAS soubor registru uložen v repozitáři", + "Pattern (regex)" : "Vzor (regulární výraz)", + "Pattern Issue" : "Problém se vzorem", + "Pattern constraint is missing" : "Chybí omezení vzoru", + "Patterns:" : "Vzory:", + "Pending" : "Čeká", + "Permanent Delete Mode" : "Režim trvalého mazání", + "Permanently Delete" : "Trvale smazat", + "Permanently delete already soft-deleted objects" : "Trvale smazat již měkce smazané objekty", + "Permanently delete this conversation?" : "Trvale smazat tuto konverzaci?", + "Person" : "Osoba", + "Personal Data" : "Osobní údaje", + "Phone" : "Telefon", + "Photos" : "Fotografie", + "Please create an agent in the" : "Vytvořte prosím agenta v", + "Please select an agent to continue" : "Pro pokračování vyberte prosím agenta", + "Please select which register and schema to use for the new object" : "Vyberte prosím, který registr a schéma použít pro nový objekt", + "Please try again later." : "Zkuste to prosím znovu později.", + "Please wait while we fetch your agents." : "Počkejte prosím, než načteme vaše agenty.", + "Please wait while we fetch your applications." : "Počkejte prosím, než načteme vaše aplikace.", + "Please wait while we fetch your configurations." : "Počkejte prosím, než načteme vaše konfigurace.", + "Please wait while we fetch your deleted items." : "Počkejte prosím, než načteme vaše smazané položky.", + "Please wait while we fetch your sources." : "Počkejte prosím, než načteme vaše zdroje.", + "Polls" : "Ankety", + "Popular Search Terms" : "Oblíbené hledané výrazy", + "Port" : "Port", + "Prerequisites" : "Předpoklady", + "Press Enter to send, Shift+Enter for new line" : "Stiskněte Enter pro odeslání, Shift+Enter pro nový řádek", + "Previous" : "Předchozí", + "Private" : "Soukromé", + "Processes chunks in batches with simulated parallelism." : "Zpracovává úseky v dávkách se simulovaným paralelismem.", + "Processes file chunks sequentially (safest)." : "Zpracovává úseky souborů sekvenčně (nejbezpečnější).", + "Processes objects in chunks with simulated parallelism." : "Zpracovává objekty v úsecích se simulovaným paralelismem.", + "Processes objects sequentially (safest)." : "Zpracovává objekty sekvenčně (nejbezpečnější).", + "Processing" : "Zpracování", + "Processing Limits" : "Limity zpracování", + "Processing..." : "Zpracovává se...", + "Project initialized successfully" : "Projekt byl úspěšně inicializován", + "Project not found. Please initialize first." : "Projekt nenalezen. Nejprve jej prosím inicializujte.", + "Properties" : "Vlastnosti", + "Property Behaviors" : "Chování vlastností", + "Property Configuration:" : "Konfigurace vlastnosti:", + "Property Filters" : "Filtry vlastností", + "Property Title" : "Název vlastnosti", + "Property Type" : "Typ vlastnosti", + "Property can be improved" : "Vlastnost lze vylepšit", + "Protected" : "Chráněné", + "Provider" : "Poskytovatel", + "Provider is required for testing" : "Poskytovatel je vyžadován pro testování", + "Public" : "Veřejné", + "Public View" : "Veřejný pohled", + "Public views can be accessed by anyone in the system" : "Veřejné pohledy mohou zobrazit všichni v systému", + "Publish" : "Publikovat", + "Publish Objects" : "Publikovat objekty", + "Publish all objects in this schema" : "Publikovat všechny objekty v tomto schématu", + "Published" : "Publikováno", + "Published:" : "Publikováno:", + "Publishing..." : "Publikuje se...", + "Purge" : "Vymazat", + "Purge Date" : "Datum vymazání", + "Query Complexity Distribution" : "Rozložení složitosti dotazů", + "Query parameter is required" : "Parametr dotazu je vyžadován", + "RAG" : "RAG", + "RAG Configuration" : "Konfigurace RAG", + "Rate limits cleared successfully" : "Omezení rychlosti byla úspěšně vymazána", + "Raw Changes Data" : "Surová data změn", + "Re-vectorize on object update" : "Znovu vektorizovat při aktualizaci objektu", + "Read" : "Číst", + "Recommendations:" : "Doporučení:", + "Recommended Type:" : "Doporučený typ:", + "Recommended: 5 sources" : "Doporučeno: 5 zdrojů", + "Refresh" : "Obnovit", + "Refresh Data" : "Obnovit data", + "Refresh Stats" : "Obnovit statistiky", + "Refresh Workflows" : "Obnovit pracovní postupy", + "Refresh dashboard" : "Obnovit nástěnku", + "Refresh database info" : "Obnovit informace o databázi", + "Register" : "Registr", + "Register ID" : "ID registru", + "Register Statistics" : "Statistiky registru", + "Register Totals" : "Souhrny registru", + "Register depublished successfully" : "Publikování registru bylo úspěšně zrušeno", + "Register not found" : "Registr nenalezen", + "Register objects deletion completed successfully" : "Mazání objektů registru bylo úspěšně dokončeno", + "Register published successfully" : "Registr byl úspěšně publikován", + "Register/Schema Usage" : "Využití registru/schématu", + "Registers" : "Registry", + "Reindex" : "Přeindexovat", + "Reindex completed" : "Přeindexování dokončeno", + "Reindex completed successfully" : "Přeindexování bylo úspěšně dokončeno", + "Reindex failed" : "Přeindexování selhalo", + "Relations" : "Vztahy", + "Remote Version" : "Vzdálená verze", + "Remove" : "Odebrat", + "Remove filter" : "Odebrat filtr", + "Remove from favorites" : "Odebrat z oblíbených", + "Remove group" : "Odebrat skupinu", + "Removed" : "Odebráno", + "Removed from favorites" : "Odebráno z oblíbených", + "Rename Conversation" : "Přejmenovat konverzaci", + "Rename conversation" : "Přejmenovat konverzaci", + "Replicas" : "Repliky", + "Repository" : "Repozitář", + "Request Body" : "Tělo požadavku", + "Request Data" : "Data požadavku", + "Request Details" : "Podrobnosti požadavku", + "Request body must be JSON with a \"query\" field" : "Tělo požadavku musí být JSON s polem \"query\"", + "Request timeout in seconds" : "Časový limit požadavku v sekundách", + "Required field" : "Povinné pole", + "Required status is inconsistent" : "Stav vyžadovanosti je nekonzistentní", + "Rerun Search" : "Znovu spustit vyhledávání", + "Reset Changes" : "Obnovit změny", + "Reset Filters" : "Obnovit filtry", + "Response Body" : "Tělo odpovědi", + "Response Details" : "Podrobnosti odpovědi", + "Response Time:" : "Doba odezvy:", + "Restore" : "Obnovit", + "Restore conversation" : "Obnovit konverzaci", + "Restore or permanently delete items" : "Obnovit nebo trvale smazat položky", + "Result Count Range" : "Rozsah počtu výsledků", + "Results" : "Výsledky", + "Retrieve all objects for this schema" : "Načíst všechny objekty pro toto schéma", + "Retry" : "Opakovat", + "Retry Failed Extractions" : "Opakovat neúspěšné extrakce", + "Retry Policy" : "Zásady opakování", + "Retry completed" : "Opakování dokončeno", + "Retrying..." : "Opakuje se...", + "Risk Level" : "Úroveň rizika", + "Roles updated successfully" : "Role byly úspěšně aktualizovány", + "SOLR Connection Settings" : "Nastavení připojení SOLR", + "SOLR Version:" : "Verze SOLR:", + "SOLR actions menu" : "Nabídka akcí SOLR", + "SOLR base path (usually /solr)" : "Základní cesta SOLR (obvykle /solr)", + "SOLR is disabled" : "SOLR je zakázán", + "SOLR is not available or not configured" : "SOLR není dostupný nebo není nakonfigurován", + "SOLR logging disabled" : "Protokolování SOLR zakázáno", + "SOLR logging enabled" : "Protokolování SOLR povoleno", + "SOLR search disabled" : "Vyhledávání SOLR zakázáno", + "SOLR search enabled" : "Vyhledávání SOLR povoleno", + "SOLR server hostname or IP address" : "Název hostitele nebo IP adresa SOLR serveru", + "SOLR server port number (optional, defaults to 8983)" : "Číslo portu SOLR serveru (volitelné, výchozí 8983)", + "SOLR setup completed successfully" : "Nastavení SOLR bylo úspěšně dokončeno", + "SOLR setup failed" : "Nastavení SOLR selhalo", + "SOLR setup failed - check logs" : "Nastavení SOLR selhalo – zkontrolujte protokoly", + "Sample Values:" : "Ukázkové hodnoty:", + "Save" : "Uložit", + "Save Configuration" : "Uložit konfiguraci", + "Save Connection Settings" : "Uložit nastavení připojení", + "Save Roles" : "Uložit role", + "Save Settings" : "Uložit nastavení", + "Save current search as view" : "Uložit aktuální vyhledávání jako pohled", + "Saved Views" : "Uložené pohledy", + "Saving..." : "Ukládá se...", + "Schema" : "Schéma", + "Schema ID" : "ID schématu", + "Schema Name" : "Název schématu", + "Schema Statistics" : "Statistiky schématu", + "Schema depublished successfully" : "Publikování schématu bylo úspěšně zrušeno", + "Schema objects deletion completed successfully" : "Mazání objektů schématu bylo úspěšně dokončeno", + "Schema published successfully" : "Schéma bylo úspěšně publikováno", + "Schema successfully created" : "Schéma bylo úspěšně vytvořeno", + "Schema successfully updated" : "Schéma bylo úspěšně aktualizováno", + "Schemas" : "Schémata", + "Scheme" : "Schéma", + "Search / Views" : "Vyhledávání / Pohledy", + "Search Activity" : "Aktivita vyhledávání", + "Search Analytics" : "Analytika vyhledávání", + "Search Entities" : "Hledat entity", + "Search Files" : "Hledat soubory", + "Search GitHub" : "Hledat na GitHub", + "Search GitLab" : "Hledat na GitLab", + "Search Method" : "Metoda vyhledávání", + "Search Term" : "Hledaný výraz", + "Search Trail Details" : "Podrobnosti záznamu vyhledávání", + "Search Trail Management" : "Správa záznamů vyhledávání", + "Search Trail Statistics" : "Statistiky záznamů vyhledávání", + "Search Trails" : "Záznamy vyhledávání", + "Search Views" : "Hledat pohledy", + "Search Webhooks" : "Hledat webhooky", + "Search by file name or path" : "Hledat podle názvu souboru nebo cesty", + "Search by name or URL" : "Hledat podle názvu nebo URL", + "Search by value" : "Hledat podle hodnoty", + "Search in Files" : "Hledat v souborech", + "Search in Objects" : "Hledat v objektech", + "Search property names..." : "Hledat názvy vlastností...", + "Search trail deletion not implemented yet" : "Mazání záznamů vyhledávání zatím není implementováno", + "Search views..." : "Hledat pohledy...", + "Secret" : "Tajný klíč", + "Secret key for HMAC signature generation (optional)" : "Tajný klíč pro generování HMAC podpisu (volitelné)", + "Select AI Agent" : "Vybrat AI agenta", + "Select All" : "Vybrat vše", + "Select ConfigSet" : "Vybrat ConfigSet", + "Select File Types to Vectorize:" : "Vyberte typy souborů k vektorizaci:", + "Select HTTP method" : "Vyberte HTTP metodu", + "Select Register and Schema" : "Vyberte registr a schéma", + "Select Views to Vectorize:" : "Vyberte pohledy k vektorizaci:", + "Select a Nextcloud group to add" : "Vyberte skupinu Nextcloud k přidání", + "Select a branch" : "Vyberte větev", + "Select a model or type a custom model name" : "Vyberte model nebo zadejte vlastní název modelu", + "Select a repository" : "Vyberte repozitář", + "Select a repository you have write access to" : "Vyberte repozitář, ke kterému máte přístup pro zápis", + "Select an AI Agent" : "Vyberte AI agenta", + "Select an AI agent to begin chatting with your data." : "Vyberte AI agenta pro zahájení chatu s vašimi daty.", + "Select backend" : "Vyberte backend", + "Select chat model" : "Vyberte model chatu", + "Select collection for files" : "Vyberte kolekci pro soubory", + "Select collection for objects" : "Vyberte kolekci pro objekty", + "Select default organisation" : "Vyberte výchozí organizaci", + "Select event to listen to..." : "Vyberte událost, kterou naslouchat...", + "Select model" : "Vyberte model", + "Select options..." : "Vyberte možnosti...", + "Select period" : "Vyberte období", + "Select property to send as payload" : "Vyberte vlastnost k odeslání jako datovou část", + "Select provider" : "Vyberte poskytovatele", + "Select registers and schemas to save a view" : "Vyberte registry a schémata pro uložení pohledu", + "Select retry policy" : "Vyberte zásady opakování", + "Select the branch to publish to" : "Vyberte větev, do které publikovat", + "Select the event this webhook should listen to" : "Vyberte událost, kterou má tento webhook naslouchat", + "Select views to vectorize:" : "Vyberte pohledy k vektorizaci:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Vyberte, které skupiny Nextcloud jsou dostupné pro tuto organizaci. Uživatelé v těchto skupinách budou mít přístup ke zdrojům organizace.", + "Select which data views the AI can search" : "Vyberte, které datové pohledy může AI prohledávat", + "Select which property from the event should be used as the webhook payload data" : "Vyberte, která vlastnost z události má být použita jako data datové části webhooku", + "Select which tools the AI can use to perform actions" : "Vyberte, které nástroje může AI použít k provádění akcí", + "Selected" : "Vybráno", + "Selected Groups" : "Vybrané skupiny", + "Selected audit trails deleted successfully" : "Vybrané auditní záznamy byly úspěšně smazány", + "Selected search trails deleted successfully" : "Vybrané záznamy vyhledávání byly úspěšně smazány", + "Selected users can access this view" : "Vybraní uživatelé mají přístup k tomuto pohledu", + "Send additional feedback" : "Odeslat další zpětnou vazbu", + "Send as CloudEvent" : "Odeslat jako CloudEvent", + "Sensitive PII" : "Citlivé osobní údaje", + "Serial Mode (Safer, slower)" : "Sériový režim (bezpečnější, pomalejší)", + "Serial:" : "Sériový:", + "Server Configuration" : "Konfigurace serveru", + "Server Information" : "Informace o serveru", + "Settings" : "Nastavení", + "Shards" : "Shardy", + "Share" : "Sdílet", + "Share with Groups" : "Sdílet se skupinami", + "Share with Users" : "Sdílet s uživateli", + "Show Filters" : "Zobrazit filtry", + "Show less" : "Zobrazit méně", + "Show only entries with changes" : "Zobrazit pouze položky se změnami", + "Simple" : "Jednoduchý", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Jednoduché dotazy: Základní textová vyhledávání s minimem parametrů (např. jeden hledaný výraz, žádné pokročilé filtry)", + "Size" : "Velikost", + "Slug" : "Slug", + "Soft Delete Mode" : "Režim měkkého mazání", + "Soft Deleted Items" : "Měkce smazané položky", + "SolrCloud mode enabled" : "Režim SolrCloud povolen", + "Source" : "Zdroj", + "Sources" : "Zdroje", + "Sources:" : "Zdroje:", + "Standalone SOLR mode" : "Samostatný režim SOLR", + "Start Conversation" : "Zahájit konverzaci", + "Start Vectorization" : "Zahájit vektorizaci", + "Start a conversation" : "Zahájit konverzaci", + "Starting names cache warmup..." : "Zahajuje se zahřátí mezipaměti názvů...", + "Starting..." : "Zahajuje se...", + "Statistics" : "Statistiky", + "Stats calculation not yet implemented" : "Výpočet statistik zatím není implementován", + "Status" : "Stav", + "Status Code" : "Stavový kód", + "String Constraints" : "Omezení řetězce", + "Success" : "Úspěch", + "Success Rate" : "Míra úspěšnosti", + "Success Status" : "Stav úspěšnosti", + "Successful" : "Úspěšné", + "Successfully joined organisation" : "Úspěšné připojení k organizaci", + "Sync Table" : "Synchronizovat tabulku", + "Synchronization Settings" : "Nastavení synchronizace", + "System Default" : "Výchozí systému", + "Technical Description" : "Technický popis", + "Technical description for developers and administrators" : "Technický popis pro vývojáře a administrátory", + "Temperature" : "Teplota", + "Templates" : "Šablony", + "Test" : "Test", + "Test Chat" : "Otestovat chat", + "Test Connection" : "Otestovat připojení", + "Test Embedding" : "Otestovat vnoření", + "Test endpoint executed successfully" : "Testovací koncový bod byl úspěšně proveden", + "Test webhook delivered successfully" : "Testovací webhook byl úspěšně doručen", + "Test webhook delivery failed" : "Doručení testovacího webhooku selhalo", + "Test webhook sent successfully" : "Testovací webhook byl úspěšně odeslán", + "Testing..." : "Testuje se...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Textové úseky jsou generovány během extrakce souborů a uloženy v databázi. Vektorizace načítá tyto předem rozdělené soubory a převádí je na vnoření.", + "Text extracted successfully" : "Text byl úspěšně extrahován", + "Text extraction disabled" : "Extrakce textu zakázána", + "The URL where webhook events will be sent" : "URL, kam budou odesílány události webhooku", + "The requested conversation does not exist" : "Požadovaná konverzace neexistuje", + "The saved view and all its search configuration" : "Uložený pohled a celá jeho konfigurace vyhledávání", + "There are no audit trail entries matching your current filters." : "Neexistují žádné položky auditních záznamů odpovídající vašim aktuálním filtrům.", + "There are no deleted items matching your current filters." : "Neexistují žádné smazané položky odpovídající vašim aktuálním filtrům.", + "There are no search trail entries matching your current filters." : "Neexistují žádné položky záznamů vyhledávání odpovídající vašim aktuálním filtrům.", + "There are no webhook log entries matching your filters." : "Neexistují žádné položky protokolu webhooku odpovídající vašim filtrům.", + "This action cannot be undone." : "Tuto akci nelze vrátit zpět.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Tuto akci nelze vrátit zpět. Ujistěte se, že tento ConfigSet nepoužívají žádné kolekce.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Tento agent nemá žádné konfigurovatelné pohledy ani nástroje, ale stále můžete upravit nastavení RAG níže.", + "This analysis may take some time" : "Tato analýza může nějakou dobu trvat", + "This audit trail entry does not contain any change information." : "Tato položka auditního záznamu neobsahuje žádné informace o změnách.", + "This configuration has no data defined." : "Tato konfigurace nemá definována žádná data.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Tento koncový bod je zastaralý. Použijte místo něj koncové body založené na úsecích.", + "This entity has no relations to objects or files" : "Tato entita nemá žádné vztahy k objektům nebo souborům", + "This is a test webhook from OpenRegister" : "Toto je testovací webhook z OpenRegister", + "This schema must use Magic Table configuration to sync" : "Toto schéma musí pro synchronizaci používat konfiguraci Magic Table", + "This source has no associated registers." : "Tento zdroj nemá žádné přidružené registry.", + "This will copy the _default ConfigSet with the new name" : "Tím se zkopíruje ConfigSet _default s novým názvem", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Tím se trvale smažou VŠECHNA vnoření (vektory) z databáze. Budete muset znovu vektorizovat všechny objekty a soubory. Tuto akci nelze vrátit zpět.\\n\\nOpravdu chcete pokračovat?", + "This will permanently delete:" : "Tím se trvale smaže:", + "Timeout (seconds)" : "Časový limit (sekundy)", + "Timestamp" : "Časové razítko", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tip: Povolte pouze pohledy, které potřebují sémantické vyhledávání, abyste minimalizovali náklady na vnoření. Jednoduché vyhledávací tabulky vektorizaci zřídka potřebují.", + "Title" : "Název", + "To Date" : "Do data", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Pro úpravu velikosti úseku nebo strategie přejděte na Konfigurace souborů → Limity zpracování.", + "To change the embedding provider or model, go to LLM Configuration." : "Pro změnu poskytovatele vnoření nebo modelu přejděte na Konfigurace LLM.", + "To date" : "Do data", + "Toggle search sidebar" : "Přepnout postranní panel vyhledávání", + "Tools" : "Nástroje", + "Top Deleters" : "Nejaktivnější mazači", + "Total" : "Celkem", + "Total Audit Trails" : "Celkem auditních záznamů", + "Total Chunks Available:" : "Celkem dostupných úseků:", + "Total Deleted Items" : "Celkem smazaných položek", + "Total Members" : "Celkem členů", + "Total Objects" : "Celkem objektů", + "Total Objects in Database:" : "Celkem objektů v databázi:", + "Total Organisations" : "Celkem organizací", + "Total Results" : "Celkem výsledků", + "Total Searches" : "Celkem vyhledávání", + "Total Size" : "Celková velikost", + "Total Storage" : "Celkové úložiště", + "Total:" : "Celkem:", + "Totals" : "Souhrny", + "Type" : "Typ", + "Type Issue" : "Problém s typem", + "Type Variations:" : "Variace typu:", + "Type must be \"positive\" or \"negative\"" : "Typ musí být \"positive\" nebo \"negative\"", + "Type to search for groups" : "Pište pro vyhledání skupin", + "Type to search for users" : "Pište pro vyhledání uživatelů", + "Type to search groups..." : "Pište pro vyhledání skupin...", + "Type to search users..." : "Pište pro vyhledání uživatelů...", + "URL" : "URL", + "URL where Ollama is running" : "URL, kde běží Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Beze změny", + "Unique Search Terms" : "Jedinečné hledané výrazy", + "Unique Terms" : "Jedinečné výrazy", + "Unknown" : "Neznámé", + "Unknown Register" : "Neznámý registr", + "Unknown Schema" : "Neznámé schéma", + "Untitled View" : "Pohled bez názvu", + "Update" : "Aktualizovat", + "Update Operations" : "Operace aktualizace", + "Update configuration: ..." : "Aktualizovat konfiguraci: ...", + "Update register OAS: ..." : "Aktualizovat OAS registru: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Aktualizovat vektory při změně dat objektu (doporučeno pro přesné vyhledávání)", + "Updated" : "Aktualizováno", + "Updated At" : "Aktualizováno v", + "Updated:" : "Aktualizováno:", + "Uptime:" : "Doba provozu:", + "Usage Count" : "Počet použití", + "Use SolrCloud with Zookeeper for distributed search" : "Použít SolrCloud se Zookeeperem pro distribuované vyhledávání", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Použijte filtry pro zúžení položek auditních záznamů podle registru, schématu, typu akce, uživatele, rozsahu dat nebo ID objektu.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Použijte filtry pro zúžení smazaných položek podle registru, schématu, data smazání nebo uživatele, který je smazal.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Použijte filtry pro zúžení položek záznamů vyhledávání podle registru, schématu, stavu úspěšnosti, uživatele, rozsahu dat, hledaných výrazů nebo metrik výkonu.", + "Used By Collections" : "Používáno kolekcemi", + "User" : "Uživatel", + "User Agent Statistics" : "Statistiky uživatelských agentů", + "User Description" : "Popis pro uživatele", + "User rate limits cleared successfully" : "Omezení rychlosti pro uživatele byla úspěšně vymazána", + "User removed successfully" : "Uživatel byl úspěšně odebrán", + "User-friendly description shown in forms and help text" : "Popis srozumitelný pro uživatele zobrazený ve formulářích a nápovědě", + "Username" : "Uživatelské jméno", + "Username for SOLR authentication (optional)" : "Uživatelské jméno pro SOLR ověření (volitelné)", + "Users" : "Uživatelé", + "Using Pre-Generated Chunks" : "Použití předem vygenerovaných úseků", + "Validate" : "Ověřit", + "Validate Objects" : "Ověřit objekty", + "Validation completed successfully" : "Ověření bylo úspěšně dokončeno", + "Validation failed" : "Ověření selhalo", + "Value" : "Hodnota", + "Vector Dimensions" : "Rozměry vektoru", + "Vector Search Backend" : "Backend vektorového vyhledávání", + "Vector field: _embedding_" : "Vektorové pole: _embedding_", + "Vectorization Triggers" : "Spouštěče vektorizace", + "Vectorize All Files" : "Vektorizovat všechny soubory", + "Vectorize All Objects" : "Vektorizovat všechny objekty", + "Vectorize all file types" : "Vektorizovat všechny typy souborů", + "Vectorize all views" : "Vektorizovat všechny pohledy", + "Vectorize on object creation" : "Vektorizovat při vytvoření objektu", + "Vectors will be stored in your existing object and file collections" : "Vektory budou uloženy ve vašich stávajících kolekcích objektů a souborů", + "Version" : "Verze", + "Very High" : "Velmi vysoká", + "View API Docs" : "Zobrazit dokumentaci API", + "View Changes" : "Zobrazit změny", + "View Details" : "Zobrazit podrobnosti", + "View Error" : "Zobrazit chybu", + "View File" : "Zobrazit soubor", + "View Full Details" : "Zobrazit všechny podrobnosti", + "View Logs" : "Zobrazit protokoly", + "View Name" : "Název pohledu", + "View Object" : "Zobrazit objekt", + "View Parameters" : "Zobrazit parametry", + "View Selection" : "Výběr pohledů", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Zobrazit a analyzovat protokoly záznamů vyhledávání s pokročilými možnostmi filtrování a analytiky", + "View and analyze system audit trails with advanced filtering capabilities" : "Zobrazit a analyzovat systémové auditní záznamy s pokročilými možnostmi filtrování", + "View deleted successfully" : "Pohled byl úspěšně smazán", + "View entity details and manage relations" : "Zobrazit podrobnosti entity a spravovat vztahy", + "View name" : "Název pohledu", + "View name is required" : "Název pohledu je vyžadován", + "View saved successfully!" : "Pohled byl úspěšně uložen!", + "View search analytics and manage search logs" : "Zobrazit analytiku vyhledávání a spravovat protokoly vyhledávání", + "View successfully deleted" : "Pohled byl úspěšně smazán", + "View successfully updated" : "Pohled byl úspěšně aktualizován", + "View updated successfully!" : "Pohled byl úspěšně aktualizován!", + "View webhook delivery logs and filter by webhook" : "Zobrazit protokoly doručení webhooku a filtrovat podle webhooku", + "Views" : "Pohledy", + "Visible to users" : "Viditelné pro uživatele", + "Wait for Response" : "Čekat na odpověď", + "Wait for webhook response before continuing (required for request/response flows)" : "Čekat na odpověď webhooku před pokračováním (vyžadováno pro toky požadavek/odpověď)", + "Warmup Names Cache" : "Zahřát mezipaměť názvů", + "Warning" : "Upozornění", + "Warning:" : "Upozornění:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Podrobnosti protokolu webhooku", + "Webhook Logs" : "Protokoly webhooku", + "Webhook created successfully" : "Webhook byl úspěšně vytvořen", + "Webhook deleted" : "Webhook smazán", + "Webhook retry delivered successfully" : "Opakování webhooku bylo úspěšně doručeno", + "Webhook retry delivery failed" : "Doručení opakování webhooku selhalo", + "Webhook updated" : "Webhook aktualizován", + "Webhook updated successfully" : "Webhook byl úspěšně aktualizován", + "Webhooks" : "Webhooky", + "Weekly" : "Týdně", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Zabalit datovou část webhooku do formátu CloudEvents pro lepší interoperabilitu", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Ano", + "You" : "Vy", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Můžete vytvořit nové ConfigSety na základě šablony _default nebo nahrát vlastní přímo na váš SOLR server.", + "You do not have access to this conversation" : "Nemáte přístup k této konverzaci", + "You do not have permission to delete this conversation" : "Nemáte oprávnění smazat tuto konverzaci", + "You do not have permission to modify this conversation" : "Nemáte oprávnění upravit tuto konverzaci", + "You do not have permission to restore this conversation" : "Nemáte oprávnění obnovit tuto konverzaci", + "You must be logged in to favorite views" : "Pro označení pohledů jako oblíbených musíte být přihlášeni", + "You need an AI agent to start a conversation." : "Pro zahájení konverzace potřebujete AI agenta.", + "Your Fireworks AI API key. Get one at" : "Váš API klíč Fireworks AI. Získejte jej na", + "Your OpenAI API key. Get one at" : "Váš API klíč OpenAI. Získejte jej na", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Vaše zpětná vazba byla zaznamenána. Volitelně zde můžete poskytnout další podrobnosti...", + "Zookeeper Hosts" : "Hostitelé Zookeeper", + "Zookeeper Port" : "Port Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Nastavení Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Připojovací řetězec Zookeeper pro SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Číslo portu Zookeeper (volitelné, výchozí 2181)", + "chunk" : "úsek", + "chunks" : "úseky", + "chunks processed" : "úseků zpracováno", + "configuration(s)" : "konfigurace", + "e.g., lib/Settings/config.json" : "např. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "např. lib/Settings/register.json", + "failed" : "selhalo", + "file" : "soubor", + "files" : "soubory", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "členové", + "menu or contact someone with permission to create agents." : "nabídce nebo kontaktujte někoho s oprávněním k vytváření agentů.", + "messages" : "zprávy", + "n8n URL and API key are required" : "URL a API klíč n8n jsou vyžadovány", + "n8n configuration saved successfully" : "Konfigurace n8n byla úspěšně uložena", + "n8n connection not configured" : "Připojení n8n není nakonfigurováno", + "n8n connection successful" : "Připojení n8n úspěšné", + "n8n connection test successful" : "Test připojení n8n úspěšný", + "n8n integration disabled" : "Integrace n8n zakázána", + "n8n integration enabled" : "Integrace n8n povolena", + "n8n project initialized successfully" : "Projekt n8n byl úspěšně inicializován", + "n8n settings saved successfully" : "Nastavení n8n bylo úspěšně uloženo", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekty", + "objects processed" : "objektů zpracováno", + "or pick a range" : "nebo vyberte rozsah", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "poskytuje dobrou rovnováhu mezi rychlostí a přesností.", + "register(s)" : "registr(y)", + "register(s) selected" : "registr(y) vybráno", + "required" : "vyžadováno", + "results" : "výsledky", + "schema(s)" : "schéma(ta)", + "schema(s) selected" : "schéma(ta) vybráno", + "sk-..." : "sk-...", + "this application" : "této aplikaci", + "to" : "do", + "vectorized" : "vektorizováno", + "views selected" : "pohledů vybráno", + "ℹ️ Current Configuration" : "ℹ️ Aktuální konfigurace", + "⚡ Batch Processing" : "⚡ Dávkové zpracování", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Komplexní kontext ✓ Menší pravděpodobnost přehlédnutí detailů ✗ Pomalejší odpovědi ✗ Může zahrnovat méně relevantní informace", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Rychlejší odpovědi ✓ Více zaměřené odpovědi ✗ Může přehlédnout relevantní informace", + "✨ AI Features" : "✨ AI funkce", + "All search trails cleared successfully" : "Všechny záznamy vyhledávání byly úspěšně vymazány", + "Multiple search trail deletion not implemented yet" : "Hromadné mazání záznamů vyhledávání zatím není implementováno", + "No expired search trails found to clear" : "Nebyly nalezeny žádné expirované záznamy vyhledávání k vymazání", + "Missing message" : "Chybí zpráva", + "message content is required" : "obsah zprávy je vyžadován", + "Missing conversation or agentUuid" : "Chybí konverzace nebo agentUuid", + "Access denied" : "Přístup odepřen", + "Conversation not found" : "Konverzace nenalezena", + "AI service not configured" : "AI služba není nakonfigurována", + "Failed to process message" : "Nepodařilo se zpracovat zprávu", + "Missing conversationId" : "Chybí conversationId", + "conversationId is required" : "conversationId je vyžadováno", + "Failed to fetch conversation history" : "Nepodařilo se načíst historii konverzace", + "Failed to clear conversation" : "Nepodařilo se vymazat konverzaci", + "Invalid feedback type" : "Neplatný typ zpětné vazby", + "type must be \"positive\" or \"negative\"" : "typ musí být \"positive\" nebo \"negative\"", + "Message not found" : "Zpráva nenalezena", + "Failed to save feedback" : "Nepodařilo se uložit zpětnou vazbu", + "Failed to get chat statistics" : "Nepodařilo se získat statistiky chatu", + "Not Found" : "Nenalezeno", + "Engine not found" : "Engine nenalezen", + "Consumer not found" : "Konzument nenalezen", + "Backend parameter is required" : "Parametr backend je vyžadován", + "Failed to get database information: %s" : "Nepodařilo se získat informace o databázi: %s", + "SOLR setup error: %s" : "Chyba nastavení SOLR: %s", + "Reindex failed: %s" : "Přeindexování selhalo: %s", + "Anonymized" : "Anonymizováno", + "Entities detected" : "Detekované entity", + "Extract Now" : "Extrahovat nyní", + "Extracted at" : "Extrahováno v", + "Extraction" : "Extrakce", + "Failed to load extraction data" : "Nepodařilo se načíst data extrakce", + "Failed to load register data" : "Nepodařilo se načíst data registru", + "Failed to retrieve extraction status." : "Nepodařilo se získat stav extrakce.", + "Failed to retrieve objects for file." : "Nepodařilo se získat objekty pro soubor.", + "No extraction data available for this file" : "Pro tento soubor nejsou dostupná žádná data extrakce", + "No register objects reference this file" : "Tento soubor neodkazují žádné objekty registru", + "Not extracted" : "Neextrahováno", + "Register Objects" : "Objekty registru", + "Risk level" : "Úroveň rizika", + "Text chunks" : "Textové úseky", + "Unknown error" : "Neznámá chyba", + "Very high" : "Velmi vysoká", + "{title} in {register} / {schema}" : "{title} v {register} / {schema}", + "Push Notifications" : "Push notifikace", + "Real-time push notification status via notify_push" : "Stav push notifikací v reálném čase prostřednictvím notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push v reálném čase není dostupný — aplikace notify_push není nainstalována", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Nainstalujte aplikaci notify_push z Nextcloud App Store pro povolení aktualizací v reálném čase.", + "Open Nextcloud App Store" : "Otevřít Nextcloud App Store", + "notify_push is installed but not yet active" : "notify_push je nainstalován, ale zatím není aktivní", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push je nainstalován, ale OpenRegister zatím nepotvrdil úspěšný push. Pro aktivaci spusťte uložení objektu nebo zkontrolujte svou konfiguraci notify_push.", + "notify_push configuration guide" : "průvodce konfigurací notify_push", + "Realtime push active" : "Push v reálném čase aktivní", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push notifikace v reálném čase jsou aktivní. Připojení klienti dostávají okamžité aktualizace při vytvoření, aktualizaci nebo smazání objektů.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" +); diff --git a/l10n/cs.json b/l10n/cs.json new file mode 100644 index 0000000000..17f1a9c311 --- /dev/null +++ b/l10n/cs.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(vyžaduje Dolphin)", + "(no subject)": "(bez předmětu)", + "(no title)": "(bez názvu)", + "(unnamed)": "(bez názvu)", + "(untitled card)": "(karta bez názvu)", + "({count} selected)": "({count} vybráno)", + ", {failed} failed": ", {failed} selhalo", + "0 = unlimited": "0 = neomezeno", + "A deactivation request is pending.": "Žádost o deaktivaci čeká na vyřízení.", + "AI Assistant": "AI asistent", + "AI Chat": "AI chat", + "AI service not configured": "AI služba není nakonfigurována", + "API Key": "API klíč", + "API Request Quota (requests/day)": "Kvóta API požadavků (požadavků/den)", + "API Token Configuration": "Konfigurace API tokenu", + "API Tokens": "API tokeny", + "API tokens saved successfully": "API tokeny byly úspěšně uloženy", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: tato zpráva spojuje každou činnost zpracování s počty záznamů auditní stopy za celou dobu, které jí jsou připsány. Auditoři a Autoriteit Persoonsgegevens to využívají pro dozorové přezkoumání.", + "About ConfigSets": "O sadách ConfigSet", + "Access denied": "Přístup odepřen", + "Access organisation dashboard and analytics": "Přístup k nástěnce a analytice organizace", + "Account": "Účet", + "Action": "Akce", + "Action Distribution": "Rozložení akcí", + "Action:": "Akce:", + "Actions": "Akce", + "Active": "Aktivní", + "Active Collections": "Aktivní kolekce", + "Active Filters": "Aktivní filtry", + "Active Organisation:": "Aktivní organizace:", + "Active Organisations": "Aktivní organizace", + "Active filters:": "Aktivní filtry:", + "Active organisation changed successfully": "Aktivní organizace byla úspěšně změněna", + "Active organisation set successfully": "Aktivní organizace byla úspěšně nastavena", + "Activities": "Činnosti", + "Activity": "Činnost", + "Activity Period": "Období činnosti", + "Add": "Přidat", + "Add Attachment": "Přidat přílohu", + "Add Endpoint": "Přidat koncový bod", + "Add Groups": "Přidat skupiny", + "Add Property to '{title}'": "Přidat vlastnost do '{title}'", + "Add Register": "Přidat registr", + "Add Schema": "Přidat schéma", + "Add Source": "Přidat zdroj", + "Add User": "Přidat uživatele", + "Add User to Organisation": "Přidat uživatele do organizace", + "Add a contact from any of your address books to associate it with this object.": "Přidejte kontakt z kteréhokoli z vašich adresářů a propojte jej s tímto objektem.", + "Add a file or files": "Přidat soubor nebo soubory", + "Add card": "Přidat kartu", + "Add contact": "Přidat kontakt", + "Add endpoint": "Přidat koncový bod", + "Add oneOf entry": "Přidat položku oneOf", + "Add range": "Přidat rozsah", + "Add schema titles, descriptions, and register information to provide richer context for search": "Přidejte názvy schémat, popisy a informace o registru pro bohatší kontext při vyhledávání", + "Add to favorites": "Přidat do oblíbených", + "Added": "Přidáno", + "Added to favorites": "Přidáno do oblíbených", + "Additional Information": "Další informace", + "Additional feedback saved. Thank you!": "Další zpětná vazba uložena. Děkujeme!", + "Addresses": "Adresy", + "Admin": "Správce", + "Advanced": "Pokročilé", + "Advanced Filters": "Pokročilé filtry", + "Advanced Options": "Pokročilé možnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Pokročilé filtry nejsou dostupné při použití databázového zdroje. Pro možnosti filtrování přepněte na Auto nebo SOLR Index.", + "After": "Po", + "Agent Type": "Typ agenta", + "Agent deleted successfully": "Agent byl úspěšně smazán", + "Agent successfully created": "Agent byl úspěšně vytvořen", + "Agent successfully updated": "Agent byl úspěšně aktualizován", + "Agents": "Agenti", + "Aggregated across schemas": "Agregováno napříč schématy", + "All": "Vše", + "All Categories": "Všechny kategorie", + "All Collections": "Všechny kolekce", + "All Confidence Levels": "Všechny úrovně spolehlivosti", + "All ConfigSets": "Všechny sady ConfigSet", + "All Files": "Všechny soubory", + "All Levels": "Všechny úrovně", + "All Types": "Všechny typy", + "All Webhooks": "Všechny webhooky", + "All actions": "Všechny akce", + "All audit trails cleared successfully": "Všechny auditní stopy byly úspěšně vymazány", + "All clear": "Vše v pořádku", + "All file types": "Všechny typy souborů", + "All registers": "Všechny registry", + "All schemas": "Všechna schémata", + "All search trails cleared successfully": "Všechny stopy vyhledávání byly úspěšně vymazány", + "All searches": "Všechna vyhledávání", + "All users": "Všichni uživatelé", + "All-day events": "Celodenní události", + "Allowed MIME Types": "Povolené typy MIME", + "Already linked": "Již propojeno", + "An error occurred while clearing audit trails": "Při mazání auditních stop došlo k chybě", + "An error occurred while deleting the audit trail": "Při mazání auditní stopy došlo k chybě", + "An error occurred while deleting the object": "Při mazání objektu došlo k chybě", + "An error occurred while deleting the view": "Při mazání pohledu došlo k chybě", + "An error occurred while permanently deleting the objects": "Při trvalém mazání objektů došlo k chybě", + "An error occurred while restoring the objects": "Při obnovování objektů došlo k chybě", + "An error occurred while saving the organisation": "Při ukládání organizace došlo k chybě", + "Analysis completed:": "Analýza dokončena:", + "Analysis steps:": "Kroky analýzy:", + "Analytics": "Analytika", + "Analyze Objects": "Analyzovat objekty", + "Analyze Properties": "Analyzovat vlastnosti", + "Analyze existing properties for improvement opportunities": "Analyzovat stávající vlastnosti a hledat příležitosti ke zlepšení", + "Analyzing...": "Analyzuji...", + "Anonymized": "Anonymizováno", + "Anonymous Group": "Anonymní skupina", + "Any favorites and sharing settings for this view": "Veškerá nastavení oblíbených a sdílení pro tento pohled", + "Any user": "Kterýkoli uživatel", + "App ID": "ID aplikace", + "App store cache invalidated successfully": "Mezipaměť App Store byla úspěšně zneplatněna", + "App store cache invalidated: {invalidated}": "Mezipaměť App Store zneplatněna: {invalidated}", + "Application deleted successfully": "Aplikace byla úspěšně smazána", + "Application identifier for this configuration (optional)": "Identifikátor aplikace pro tuto konfiguraci (volitelné)", + "Application settings": "Nastavení aplikace", + "Application successfully created": "Aplikace byla úspěšně vytvořena", + "Application successfully updated": "Aplikace byla úspěšně aktualizována", + "Applications": "Aplikace", + "Apply": "Použít", + "Apply Changes": "Použít změny", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Použít roli na všechna schémata v tomto registru, která nemají explicitní přepsání autorizace.", + "Approval Progress": "Průběh schvalování", + "Approve": "Schválit", + "Approved": "Schváleno", + "Archive": "Archivovat", + "Archive conversation": "Archivovat konverzaci", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Archivovat tuto verwerkingsactiviteit? Řádky auditní stopy na ni budou nadále odkazovat.", + "Archived conversations are hidden from your active list": "Archivované konverzace jsou skryty z vašeho aktivního seznamu", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Opravdu chcete SMAZAT kolekci \"{name}\"?\n\nTímto:\n• trvale smažete kolekci a všechna její data\n• odstraníte všechny indexované dokumenty\n• Tuto akci nelze vrátit zpět", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Opravdu chcete vyčistit staré stopy vyhledávání? Tímto smažete položky starší než 30 dnů.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Opravdu chcete vymazat všechna data z kolekce \"{name}\"?\n\nTímto:\n• smažete všechny indexované dokumenty\n• zachováte strukturu kolekce nedotčenou\n• Tuto akci nelze vrátit zpět", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Opravdu chcete smazat aplikaci \"{name}\"? Tuto akci nelze vrátit zpět.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Opravdu chcete smazat vybrané auditní stopy? Tuto akci nelze vrátit zpět.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Opravdu chcete smazat vybrané stopy vyhledávání? Tuto akci nelze vrátit zpět.", + "Are you sure you want to delete this ConfigSet?": "Opravdu chcete smazat tuto sadu ConfigSet?", + "Are you sure you want to delete this configset?": "Opravdu chcete smazat tuto sadu configset?", + "Are you sure you want to leave '{name}'?": "Opravdu chcete opustit '{name}'?", + "Are you sure you want to permanently delete": "Opravdu chcete trvale smazat", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Opravdu chcete přeindexovat kolekci \"{name}\"?\n\nTímto:\n• přebudujete index se všemi objekty\n• Dokončení potrvá několik minut\n• Během přeindexování může být ovlivněn výkon vyhledávání", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Opravdu chcete odebrat schéma \"{schema}\" z registru \"{register}\"? Tuto akci nelze vrátit zpět.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Opravdu chcete dočasně smazat {count} aktivních objektů pro schéma \"{schema}\"?\n\nBudou označeny jako smazané, ale lze je trvale odstranit později.", + "Array Object Configuration:": "Konfigurace objektu pole:", + "Articles": "Články", + "Ask a question...": "Položte otázku...", + "Ask questions about your data using natural language": "Pokládejte otázky o svých datech v přirozeném jazyce", + "Assigned collaborative tags": "Přiřazené týmové štítky", + "Assignment notifications": "Oznámení o přiřazení", + "Attachment added to {name}": "Příloha přidána do {name}", + "Attempt": "Pokus", + "Audit Trail Changes": "Změny auditní stopy", + "Audit Trail Details": "Detaily auditní stopy", + "Audit Trail Management": "Správa auditní stopy", + "Audit Trail Statistics": "Statistiky auditní stopy", + "Audit Trails": "Auditní stopy", + "Audit trail data copied to clipboard": "Data auditní stopy zkopírována do schránky", + "Audit trail deleted successfully": "Auditní stopa byla úspěšně smazána", + "Audit trail successfully deleted": "Auditní stopa byla úspěšně smazána", + "Authentication": "Ověření", + "Auto (e.g. Y for year, F Y for month)": "Auto (např. Y pro rok, F Y pro měsíc)", + "Auto share": "Automatické sdílení", + "Auto-commit disabled": "Automatické potvrzování zakázáno", + "Auto-commit enabled": "Automatické potvrzování povoleno", + "Auto-create Default Organisation": "Automaticky vytvořit výchozí organizaci", + "Auto-retry failed vectorizations": "Automaticky opakovat neúspěšné vektorizace", + "Automatically commit changes to SOLR index": "Automaticky potvrzovat změny do indexu SOLR", + "Automatically connect to the federation network.": "Automaticky se připojit k federační síti.", + "Automatically create a default organisation if none exists when the app is initialized": "Automaticky vytvořit výchozí organizaci, pokud žádná neexistuje při inicializaci aplikace", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automaticky generovat vektorová vložení z textových úseků, když jsou soubory nahrány a zpracovány", + "Automatically generate vector embeddings when objects are created or updated": "Automaticky generovat vektorová vložení, když jsou objekty vytvořeny nebo aktualizovány", + "Automatically retry failed vectorization attempts (max 3 retries)": "Automaticky opakovat neúspěšné pokusy o vektorizaci (max. 3 opakování)", + "Automatically update catalogues.": "Automaticky aktualizovat katalogy.", + "Available Filters": "Dostupné filtry", + "Available Workflows": "Dostupné pracovní postupy", + "Available placeholders:": "Dostupné zástupné symboly:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Změny avataru nejsou podporovány vaším poskytovatelem ověření.", + "Avatar removed": "Avatar odstraněn", + "Avatar updated successfully": "Avatar byl úspěšně aktualizován", + "Avg Execution Time": "Prům. doba provedení", + "Avg Members/Org": "Prům. členů/org.", + "Avg Object Views/Session": "Prům. zobrazení objektů/relace", + "Avg Response Time": "Prům. doba odezvy", + "Avg Results/Search": "Prům. výsledků/vyhledávání", + "Avg Searches/Session": "Prům. vyhledávání/relace", + "Back": "Zpět", + "Back to Entities": "Zpět na entity", + "Back to Registers": "Zpět na registry", + "Back to Webhooks": "Zpět na webhooky", + "Back to applications": "Zpět na aplikace", + "Back to entities": "Zpět na entity", + "Backend parameter is required": "Parametr backend je povinný", + "Backend updated successfully. Please reload the application.": "Backend byl úspěšně aktualizován. Znovu načtěte aplikaci.", + "Bandwidth Quota (MB/month)": "Kvóta přenosu dat (MB/měsíc)", + "Base URL (Optional)": "Základní URL (volitelné)", + "Basic Information": "Základní informace", + "Batch Size": "Velikost dávky", + "Batch extraction completed": "Dávková extrakce dokončena", + "Before": "Před", + "Before object vectorization can work:": "Než může vektorizace objektů fungovat:", + "Behavior": "Chování", + "Behavior Issue": "Problém s chováním", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (doba trvání ISO-8601, např. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Úložiště blob bylo vyřazeno. Všechny objekty nyní používají magické tabulky.", + "Bookmarks": "Záložky", + "Branch": "Větev", + "Bulk Role Assignment: {title}": "Hromadné přiřazení rolí: {title}", + "Bulk delete operation completed successfully": "Operace hromadného mazání byla úspěšně dokončena", + "Bulk save operation completed successfully": "Operace hromadného ukládání byla úspěšně dokončena", + "Business Data": "Obchodní data", + "CSV": "CSV", + "Cache cleared successfully": "Mezipaměť byla úspěšně vymazána", + "Cache grew from {old} to {new} entries.": "Mezipaměť vzrostla z {old} na {new} položek.", + "Cache shrunk from {old} to {new} entries.": "Mezipaměť se zmenšila z {old} na {new} položek.", + "Cache stayed the same at {size} entries.": "Mezipaměť zůstala stejná na {size} položkách.", + "Calculate Sizes": "Vypočítat velikosti", + "Calendar": "Kalendář", + "Calendar Provider Configuration": "Konfigurace poskytovatele kalendáře", + "Calendar integration is not available": "Integrace kalendáře není dostupná", + "Calendar name": "Název kalendáře", + "Cancel": "Zrušit", + "Cancel deactivation request": "Zrušit žádost o deaktivaci", + "Cannot delete: objects are still attached": "Nelze smazat: objekty jsou stále připojeny", + "Cannot edit: This register is managed by external configuration {title}": "Nelze upravit: Tento registr je spravován externí konfigurací {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nelze odebrat schéma s existujícími objekty ({count} objektů)", + "Cannot remove schema {schema} because it contains {count} objects": "Nelze odebrat schéma {schema}, protože obsahuje {count} objektů", + "Cards": "Karty", + "Cascade delete": "Kaskádové smazání", + "Categories of data subjects (one per line)": "Kategorie subjektů údajů (jedna na řádek)", + "Categories of personal data (one per line)": "Kategorie osobních údajů (jedna na řádek)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategorie", + "Change Type": "Změnit typ", + "Change password": "Změnit heslo", + "Changes": "Změny", + "Changes copied to clipboard": "Změny zkopírovány do schránky", + "Changes for Audit Trail #{id}": "Změny pro auditní stopu #{id}", + "Chat Model": "Chat model", + "Chat Provider": "Poskytovatel chatu", + "Chat Provider (RAG)": "Poskytovatel chatu (RAG)", + "Chat Settings": "Nastavení chatu", + "Chat provider connection successful!": "Připojení k poskytovateli chatu úspěšné!", + "Chat settings": "Nastavení chatu", + "Chat test failed: {error}": "Test chatu selhal: {error}", + "Choose a register": "Vyberte registr", + "Choose a schema": "Vyberte schéma", + "Choose how vector similarity calculations are performed for semantic search": "Vyberte, jak se provádějí výpočty podobnosti vektorů pro sémantické vyhledávání", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Vyberte, které typy souborů zahrnout do procesu vektorizace. Zpracovány budou pouze soubory s extrahovaným textem a úseky.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Vyberte, které pohledy zahrnout do procesu vektorizace. Ponechte prázdné pro zpracování všech pohledů podle vaší konfigurace.", + "Chunk": "Úsek", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Mazání úseků zatím není implementováno. Použijte koncové body založené na úsecích.", + "Chunks": "Úseky", + "Chunks to Vectorize:": "Úseky k vektorizaci:", + "Cleanup Old Trails": "Vyčistit staré stopy", + "Cleanup completed": "Čištění dokončeno", + "Cleanup completed successfully. Deleted {count} entries.": "Čištění úspěšně dokončeno. Smazáno {count} položek.", + "Cleanup failed: {error}": "Čištění selhalo: {error}", + "Clear All Embeddings": "Vymazat všechna vložení", + "Clear Entries": "Vymazat položky", + "Clear Filtered Audit Trails": "Vymazat filtrované auditní stopy", + "Clear Filters": "Vymazat filtry", + "Clear Index": "Vymazat index", + "Clear Selection": "Zrušit výběr", + "Clear all": "Vymazat vše", + "Clear all filters": "Vymazat všechny filtry", + "Clear date range": "Vymazat rozsah dat", + "Clear filters": "Vymazat filtry", + "Clearing...": "Mazání...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Klikněte na Spustit kontrolu shody pro nalezení schémat, kde byly detekovány osobní údaje (PII), ale neexistuje žádná anotace činnosti zpracování.", + "Click to subscribe to notifications": "Klikněte pro odběr oznámení", + "Click to unsubscribe from notifications": "Klikněte pro zrušení odběru oznámení", + "Close": "Zavřít", + "Cluster name": "Název clusteru", + "Code": "Kód", + "Code (short readable key, e.g. v-2026-001)": "Kód (krátký čitelný klíč, např. v-2026-001)", + "Collapse details": "Sbalit detaily", + "Collection Management": "Správa kolekcí", + "Collection Name": "Název kolekce", + "Collection assignments updated successfully": "Přiřazení kolekcí byla úspěšně aktualizována", + "Collection cleared successfully": "Kolekce byla úspěšně vymazána", + "Collection copied successfully": "Kolekce byla úspěšně zkopírována", + "Collection created successfully": "Kolekce byla úspěšně vytvořena", + "Collection deleted successfully": "Kolekce byla úspěšně smazána", + "Collection used to store and index file metadata and content": "Kolekce používaná k ukládání a indexování metadat a obsahu souborů", + "Collection used to store and index object data": "Kolekce používaná k ukládání a indexování dat objektů", + "Collections:": "Kolekce:", + "Color": "Barva", + "Comment...": "Komentář...", + "Commit Message": "Zpráva commitu", + "Commit Within (ms)": "Potvrdit během (ms)", + "Commit message": "Zpráva commitu", + "Compare current schema with real object data": "Porovnat aktuální schéma se skutečnými daty objektů", + "Completed": "Dokončeno", + "Complex": "Složité", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Složité dotazy: Pokročilá vyhledávání s více filtry, operátory a složitými kombinacemi parametrů", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Složité dotazy: pokročilá vyhledávání s více filtry, operátory a složitými kombinacemi parametrů", + "Compliance": "Shoda", + "Confidence Level": "Úroveň spolehlivosti", + "Confidence Score": "Skóre spolehlivosti", + "Confidence: {confidence}%": "Spolehlivost: {confidence}%", + "Config must be provided as an object": "Konfigurace musí být poskytnuta jako objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Správa sad ConfigSet", + "ConfigSet Name": "Název sady ConfigSet", + "ConfigSet created successfully": "Sada ConfigSet byla úspěšně vytvořena", + "ConfigSet deleted successfully": "Sada ConfigSet byla úspěšně smazána", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Sady ConfigSet definují schéma a konfiguraci pro vaše kolekce SOLR. Obsahují definice polí, analyzátory a další nastavení vyhledávání.", + "Configuration": "Konfigurace", + "Configuration Data": "Konfigurační data", + "Configuration is up to date": "Konfigurace je aktuální", + "Configuration saved": "Konfigurace uložena", + "Configuration saved successfully.": "Konfigurace byla úspěšně uložena.", + "Configuration type (default, application, etc.)": "Typ konfigurace (výchozí, aplikace atd.)", + "Configurations": "Konfigurace", + "Configure API tokens for external service integrations": "Konfigurace API tokenů pro integrace externích služeb", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurace vyhledávacího nástroje Apache SOLR pro pokročilé možnosti vyhledávání", + "Configure Facets": "Konfigurovat fasety", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurace poskytovatelů velkých jazykových modelů (LLM) pro funkce poháněné AI včetně sémantického vyhledávání, vložení a chatu.", + "Configure SOLR Facets": "Konfigurovat fasety SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Nakonfigurujte základní nastavení připojení pro váš server SOLR včetně ověření a síťových možností. Pro správu jader a kolekcí použijte samostatné dialogy Správa sad ConfigSet a Správa kolekcí.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Nakonfigurujte základní nastavení připojení pro váš server SOLR včetně ověření a síťových možností. Pro správu jader a kolekcí použijte samostatné dialogy správy sad configset a kolekcí.", + "Configure default organisation and organisation-related settings": "Konfigurace výchozí organizace a nastavení souvisejících s organizací", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Nakonfigurujte, jak jsou databázové objekty převáděny na vektorová vložení pro sémantické vyhledávání. Objekty jsou vektorizovány přímo bez potřeby extrakce textu.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Nakonfigurujte, jak jsou objekty převáděny na text před vektorizací. Tato nastavení ovlivňují kvalitu a kontext vyhledávání.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurace poskytovatelů velkých jazykových modelů (LLM) pro funkce poháněné AI včetně sémantického vyhledávání, vložení a chatu.", + "Configure multi-organization support and tenant isolation": "Konfigurace podpory více organizací a izolace nájemců", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Nakonfigurujte parametry pro vektorizaci souborů. Tento proces vygeneruje vektorová vložení pro všechny extrahované úseky souborů.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Nakonfigurujte parametry pro vektorizaci objektů. Tento proces vygeneruje vektorová vložení pro všechny objekty odpovídající filtrům vašeho pohledu.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Nakonfigurujte toto schéma, aby zobrazovalo objekty jako události v aplikaci Nextcloud Kalendář.", + "Configure which types of data to search and how many sources to retrieve": "Nakonfigurujte, které typy dat prohledávat a kolik zdrojů načíst", + "Configuring field mappings...": "Konfiguruji mapování polí...", + "Confirm Account Deactivation": "Potvrdit deaktivaci účtu", + "Confirm Object Deletion": "Potvrdit smazání objektu", + "Confirm Object Restoration": "Potvrdit obnovení objektu", + "Confirm Permanent Object Deletion": "Potvrdit trvalé smazání objektu", + "Confirm deactivation": "Potvrdit deaktivaci", + "Connecting to SOLR cluster...": "Připojování ke clusteru SOLR...", + "Connection Failed": "Připojení selhalo", + "Connection Settings": "Nastavení připojení", + "Connection Successful!": "Připojení úspěšné!", + "Connection failed": "Připojení selhalo", + "Connection protocol": "Protokol připojení", + "Connection successful": "Připojení úspěšné", + "Connection timeout in seconds": "Časový limit připojení v sekundách", + "Constant delay between retries (5 minutes)": "Konstantní prodleva mezi opakováními (5 minut)", + "Constraint Issue": "Problém s omezením", + "Consumer not found": "Spotřebitel nenalezen", + "Contacts": "Kontakty", + "Contacts integration is not available": "Integrace kontaktů není dostupná", + "Content Filters": "Filtry obsahu", + "Continue to Properties": "Pokračovat na vlastnosti", + "Contracts": "Smlouvy", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Určete, které pohledy objektů by měly být vektorizovány pro snížení nákladů na API. Vektorizujte pouze pohledy, které těží ze sémantického vyhledávání.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Určete, které pohledy a nástroje může AI v této konverzaci používat. Ve výchozím nastavení jsou povoleny všechny schopnosti agenta.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Řídí náhodnost (0 = deterministické, 2 = velmi kreativní)", + "Conversation ID is required": "ID konverzace je povinné", + "Conversation archived": "Konverzace archivována", + "Conversation archived successfully": "Konverzace byla úspěšně archivována", + "Conversation cleared successfully": "Konverzace byla úspěšně vymazána", + "Conversation deleted": "Konverzace smazána", + "Conversation not found": "Konverzace nenalezena", + "Conversation permanently deleted": "Konverzace trvale smazána", + "Conversation renamed": "Konverzace přejmenována", + "Conversation restored": "Konverzace obnovena", + "Conversation started with {agent}": "Konverzace zahájena s {agent}", + "Conversation title": "Název konverzace", + "Conversations": "Konverzace", + "Converting {schema} to blob storage...": "Převádím {schema} na úložiště blob...", + "Converting {schema} to magic table...": "Převádím {schema} na magickou tabulku...", + "Copied!": "Zkopírováno!", + "Copy": "Kopírovat", + "Copy Changes": "Kopírovat změny", + "Copy Collection": "Kopírovat kolekci", + "Copy Data": "Kopírovat data", + "Copy Full Data": "Kopírovat všechna data", + "Copy of {name}": "Kopie {name}", + "Copy to clipboard": "Kopírovat do schránky", + "Copying...": "Kopírování...", + "Costs": "Náklady", + "Count": "Počet", + "Counting objects...": "Počítání objektů...", + "Create": "Vytvořit", + "Create API Token": "Vytvořit API token", + "Create Agent": "Vytvořit agenta", + "Create Application": "Vytvořit aplikaci", + "Create Collection": "Vytvořit kolekci", + "Create ConfigSet": "Vytvořit sadu ConfigSet", + "Create New Collection": "Vytvořit novou kolekci", + "Create New ConfigSet": "Vytvořit novou sadu ConfigSet", + "Create Operations": "Operace vytvoření", + "Create Organisation": "Vytvořit organizaci", + "Create Webhook": "Vytvořit webhook", + "Create a copy of collection:": "Vytvořit kopii kolekce:", + "Create a new ConfigSet based on the _default template": "Vytvořit novou sadu ConfigSet na základě šablony _default", + "Create a new SOLR collection from an existing ConfigSet": "Vytvořit novou kolekci SOLR z existující sady ConfigSet", + "Create a new SOLR collection from an existing configset": "Vytvořit novou kolekci SOLR z existující sady configset", + "Create a new configset based on the _default template": "Vytvořit novou sadu configset na základě šablony _default", + "Create a new event or link an existing one from any of your calendars.": "Vytvořte novou událost nebo propojte existující z kteréhokoli z vašich kalendářů.", + "Create another": "Vytvořit další", + "Create copies of": "Vytvořit kopie", + "Create event": "Vytvořit událost", + "Create new token": "Vytvořit nový token", + "Create or link a Deck card to track work on this object.": "Vytvořte nebo propojte kartu Deck pro sledování práce na tomto objektu.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Vytvořte první verwerkingsactiviteit a začněte označovat řádky auditní stopy jejich přiřazením podle AVG Art 30.", + "Create your first AI agent to get started.": "Vytvořte svého prvního AI agenta a začněte.", + "Created": "Vytvořeno", + "Created:": "Vytvořeno:", + "Creating search collections...": "Vytváření vyhledávacích kolekcí...", + "Creating...": "Vytváření...", + "Current": "Aktuální", + "Current Filters": "Aktuální filtry", + "Current Type:": "Aktuální typ:", + "Current password": "Aktuální heslo", + "Custom API endpoint if using a different region": "Vlastní koncový bod API při použití jiného regionu", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Vlastní hlavičky HTTP (jedna na řádek, formát: Header-Name: hodnota)", + "Custom HTTP headers (one per line, format: header-name: value)": "Vlastní hlavičky HTTP (jedna na řádek, formát: header-name: hodnota)", + "Custom Ranges:": "Vlastní rozsahy:", + "Custom display title for this facet": "Vlastní zobrazovaný název pro tuto fasetu", + "Custom pattern:": "Vlastní vzor:", + "DELETE COLLECTION": "DELETE COLLECTION", + "DSAR": "DSAR", + "Daily": "Denně", + "Dashboard": "Nástěnka", + "Dashboard not found": "Nástěnka nenalezena", + "Data": "Data", + "Data Source": "Zdroj dat", + "Data Sources": "Zdroje dat", + "Data Views": "Datové pohledy", + "Data sources": "Zdroje dat", + "Data type does not match observed values": "Datový typ neodpovídá pozorovaným hodnotám", + "Data-subject access request": "Žádost subjektu údajů o přístup", + "Database URL": "URL databáze", + "Database information refreshed": "Informace o databázi obnoveny", + "Date Faceting:": "Fasetování podle data:", + "Date Range": "Rozsah dat", + "Dates": "Data", + "Deactivation request cancelled": "Žádost o deaktivaci zrušena", + "Deactivation request submitted": "Žádost o deaktivaci odeslána", + "Deck": "Deck", + "Deck integration is not available": "Integrace Deck není dostupná", + "Default": "Výchozí", + "Default Object Tenant": "Výchozí nájemce objektu", + "Default Organisation": "Výchozí organizace", + "Default Owner": "Výchozí vlastník", + "Default User (for cron/background jobs)": "Výchozí uživatel (pro cron/úlohy na pozadí)", + "Default User Tenant": "Výchozí nájemce uživatele", + "Default value": "Výchozí hodnota", + "Default value (Email)": "Výchozí hodnota (e-mail)", + "Default value (Password)": "Výchozí hodnota (heslo)", + "Default value (Phone number)": "Výchozí hodnota (telefonní číslo)", + "Default value (Regex)": "Výchozí hodnota (regulární výraz)", + "Default: 5": "Výchozí: 5", + "Defaults to current user. Select a different user if needed.": "Výchozí je aktuální uživatel. V případě potřeby vyberte jiného uživatele.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Prodlevy se s každým pokusem zdvojnásobují (2, 4, 8 minut...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Prodlevy se zvyšují lineárně (5, 10, 15 minut...)", + "Delete": "Smazat", + "Delete ({count})": "Smazat ({count})", + "Delete Application": "Smazat aplikaci", + "Delete Audit Trail": "Smazat auditní stopu", + "Delete Collection": "Smazat kolekci", + "Delete ConfigSet": "Smazat sadu ConfigSet", + "Delete Objects": "Smazat objekty", + "Delete Operations": "Operace smazání", + "Delete SOLR Collection": "Smazat kolekci SOLR", + "Delete View": "Smazat pohled", + "Delete all objects in this schema": "Smazat všechny objekty v tomto schématu", + "Delete permanently": "Smazat trvale", + "Delete this conversation?": "Smazat tuto konverzaci?", + "Delete view": "Smazat pohled", + "Delete {count} object": "Smazat {count} objekt", + "Delete {count} objects": "Smazat {count} objektů", + "Deleted": "Smazáno", + "Deleted By": "Smazáno uživatelem", + "Deleted Date": "Datum smazání", + "Deleted Items Management": "Správa smazaných položek", + "Deleted This Week": "Smazáno tento týden", + "Deleted Today": "Smazáno dnes", + "Deleted:": "Smazáno:", + "Deleting SOLR Collection...": "Mazání kolekce SOLR...", + "Deleting...": "Mazání...", + "Deletion Date Range": "Rozsah dat smazání", + "Deletion Statistics": "Statistiky mazání", + "Deprecated": "Zastaralé", + "Depublish": "Zrušit publikování", + "Description": "Popis", + "Description Template": "Šablona popisu", + "Description shown as tooltip": "Popis zobrazený jako popisek", + "Description:": "Popis:", + "Detect data types and patterns": "Detekovat datové typy a vzory", + "Detected At": "Detekováno v", + "Detected Format:": "Detekovaný formát:", + "Detected Issues:": "Detekované problémy:", + "Detected: {date}": "Detekováno: {date}", + "Direct permission": "Přímé oprávnění", + "Disable": "Zakázat", + "Disable this to make the agent": "Zakázáním tohoto učiníte agenta", + "Disabled": "Zakázáno", + "Discover Files": "Objevit soubory", + "Discovered Properties": "Objevené vlastnosti", + "Display Format": "Formát zobrazení", + "Display Name": "Zobrazované jméno", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Chcete trvale smazat všechny filtrované položky auditní stopy? Tuto akci nelze vrátit zpět.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Chcete trvale smazat tuto položku auditní stopy? Tuto akci nelze vrátit zpět.", + "Documents": "Dokumenty", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR povoleno", + "Download API Spec": "Stáhnout specifikaci API", + "Download API Specification": "Stáhnout specifikaci API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Stáhněte si kopii všech svých osobních údajů uložených v OpenRegister (GDPR článek 20).", + "Drag a file or files here": "Přetáhněte sem soubor nebo soubory", + "Dry run completed: {count} fields would be created": "Zkušební běh dokončen: bylo by vytvořeno {count} polí", + "Dry run completed: {count} fields would be fixed": "Zkušební běh dokončen: bylo by opraveno {count} polí", + "Duration in seconds (optional)": "Doba trvání v sekundách (volitelné)", + "Edit": "Upravit", + "Edit Agent": "Upravit agenta", + "Edit Application": "Upravit aplikaci", + "Edit Configuration": "Upravit konfiguraci", + "Edit Endpoint": "Upravit koncový bod", + "Edit Organisation": "Upravit organizaci", + "Edit Property '{key}' of '{title}'": "Upravit vlastnost '{key}' v '{title}'", + "Edit Register": "Upravit registr", + "Edit Schema": "Upravit schéma", + "Edit Source": "Upravit zdroj", + "Edit View": "Upravit pohled", + "Edit Webhook": "Upravit webhook", + "Edit labels": "Upravit štítky", + "Edit processing activity": "Upravit činnost zpracování", + "Edit tags for {name}": "Upravit štítky pro {name}", + "Edit verwerkingsactiviteit": "Upravit verwerkingsactiviteit", + "Edit view": "Upravit pohled", + "Edit view details": "Upravit detaily pohledu", + "Effectiveness": "Účinnost", + "Email": "E-mail", + "Email Addresses": "E-mailové adresy", + "Email digest frequency": "Frekvence e-mailového souhrnu", + "Emails": "E-maily", + "Embedding Model": "Model vložení", + "Embedding Provider": "Poskytovatel vložení", + "Embedding generated successfully": "Vložení bylo úspěšně vygenerováno", + "Embedding provider connection successful!": "Připojení k poskytovateli vložení úspěšné!", + "Embedding test failed: {error}": "Test vložení selhal: {error}", + "Enable": "Povolit", + "Enable RAG": "Povolit RAG", + "Enable auto-creation": "Povolit automatické vytváření", + "Enable automatic file vectorization": "Povolit automatickou vektorizaci souborů", + "Enable automatic object vectorization": "Povolit automatickou vektorizaci objektů", + "Enable automatic synchronization": "Povolit automatickou synchronizaci", + "Enable calendar provider": "Povolit poskytovatele kalendáře", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Povolit podrobné protokolování pro operace SOLR (doporučeno pro ladění)", + "Enable faceting": "Povolit fasetování", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Povolit nebo zakázat funkce LLM. Poskytovatele a modely nakonfigurujte pomocí tlačítka Konfigurace LLM výše.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Povolit nebo zakázat funkce LLM. Poskytovatele a modely nakonfigurujte pomocí tlačítka konfigurace LLM výše.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Povolit nebo zakázat integraci vyhledávání SOLR. Nastavení připojení nakonfigurujte pomocí tlačítka Nastavení připojení výše.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Povolit nebo zakázat integraci vyhledávání SOLR. Nastavení připojení nakonfigurujte pomocí tlačítka nastavení připojení výše.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Povolit nebo zakázat integraci pracovních postupů n8n. Nastavení připojení nakonfigurujte níže.", + "Enable or disable this webhook": "Povolit nebo zakázat tento webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Povolte nástroje, které agentovi umožní interakci s daty prostřednictvím volání funkcí.", + "Enable vectorization for all existing and future views (may increase costs)": "Povolit vektorizaci pro všechny stávající i budoucí pohledy (může zvýšit náklady)", + "Enable write-back to target objects": "Povolit zpětný zápis do cílových objektů", + "Enabled": "Povoleno", + "End Date Field": "Pole koncového data", + "Endpoint Path*": "Cesta koncového bodu*", + "Endpoints": "Koncové body", + "Engine not found": "Nástroj nenalezen", + "Enter ConfigSet name": "Zadejte název sady ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Zadejte uživatelská jména Nextcloud pro udělení přístupu k tomuto soukromému agentovi", + "Enter agent description (optional)": "Zadejte popis agenta (volitelné)", + "Enter agent name": "Zadejte název agenta", + "Enter application description (optional)": "Zadejte popis aplikace (volitelné)", + "Enter application name": "Zadejte název aplikace", + "Enter collection name": "Zadejte název kolekce", + "Enter configuration description (optional)": "Zadejte popis konfigurace (volitelné)", + "Enter configuration title": "Zadejte název konfigurace", + "Enter description (optional)...": "Zadejte popis (volitelné)...", + "Enter new collection name": "Zadejte název nové kolekce", + "Enter object ID": "Zadejte ID objektu", + "Enter organisation description (optional)": "Zadejte popis organizace (volitelné)", + "Enter organisation name": "Zadejte název organizace", + "Enter search term": "Zadejte hledaný výraz", + "Enter search terms or leave empty to browse all": "Zadejte hledané výrazy nebo ponechte prázdné pro procházení všeho", + "Enter system prompt for the agent": "Zadejte systémový prompt pro agenta", + "Enter username and press Enter": "Zadejte uživatelské jméno a stiskněte Enter", + "Enter view name...": "Zadejte název pohledu...", + "Enter webhook name": "Zadejte název webhooku", + "Enter your API key": "Zadejte svůj API klíč", + "Enter your object here...": "Zadejte zde svůj objekt...", + "Enter your schema here...": "Zadejte zde své schéma...", + "Entities": "Entity", + "Entities detected": "Detekované entity", + "Entity ID: {id}": "ID entity: {id}", + "Entity Information": "Informace o entitě", + "Entity Recognition": "Rozpoznávání entit", + "Entity Recognition Method": "Metoda rozpoznávání entit", + "Entity deleted successfully": "Entita byla úspěšně smazána", + "Entity not found": "Entita nenalezena", + "Entries to be deleted:": "Položky ke smazání:", + "Enum Issue": "Problém s výčtem", + "Enum constraint is missing": "Chybí omezení výčtu", + "Erase (Art 17)": "Vymazat (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Vymazat {count} objekt(ů) pro tento subjekt? Tato akce je zaznamenána v auditní stopě.", + "Erasure complete": "Vymazání dokončeno", + "Erasure preview": "Náhled vymazání", + "Error": "Chyba", + "Error Details": "Detaily chyby", + "Error Information": "Informace o chybě", + "Error Message": "Chybová zpráva", + "Error deleting audit trails: {error}": "Chyba při mazání auditních stop: {error}", + "Error deleting search trails: {error}": "Chyba při mazání stop vyhledávání: {error}", + "Error loading application": "Chyba při načítání aplikace", + "Error loading audit trails": "Chyba při načítání auditních stop", + "Error loading entity": "Chyba při načítání entity", + "Error loading search trails": "Chyba při načítání stop vyhledávání", + "Estimated Batches:": "Odhadované dávky:", + "Estimated Cost:": "Odhadované náklady:", + "Estimated Duration:": "Odhadovaná doba trvání:", + "Event": "Událost", + "Event Property for Payload": "Vlastnost události pro užitečné zatížení", + "Events": "Události", + "Every schema with detected PII has a processing-activity annotation.": "Každé schéma s detekovanými osobními údaji (PII) má anotaci činnosti zpracování.", + "Example": "Příklad", + "Example Value": "Příkladová hodnota", + "Example value for this property": "Příkladová hodnota pro tuto vlastnost", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Výhradní maximum", + "Exclusive Minimum": "Výhradní minimum", + "Exclusive maximum": "Výhradní maximum", + "Exclusive minimum": "Výhradní minimum", + "Execution Mode": "Režim provádění", + "Execution Time": "Doba provedení", + "Execution Time Range": "Rozsah doby provedení", + "Existing Improvements": "Stávající vylepšení", + "Expand details": "Rozbalit detaily", + "Expiration": "Vypršení platnosti", + "Expires": "Vyprší", + "Expires in (e.g., 90d)": "Vyprší za (např. 90d)", + "Exponential": "Exponenciální", + "Export": "Export", + "Export \"{schema}\" objects from \"{register}\"": "Exportovat objekty \"{schema}\" z \"{register}\"", + "Export Configuration": "Exportovat konfiguraci", + "Export Format:": "Formát exportu:", + "Export Objects": "Exportovat objekty", + "Export completed successfully": "Export byl úspěšně dokončen", + "Export downloaded successfully": "Export byl úspěšně stažen", + "Export failed: {error}": "Export selhal: {error}", + "Export is rate limited. Please try again later.": "Export je omezen rychlostí. Zkuste to prosím později.", + "Export my data": "Exportovat moje data", + "Export, view, or delete audit trails": "Exportovat, zobrazit nebo smazat auditní stopy", + "Exporting...": "Export...", + "Extend Schema": "Rozšířit schéma", + "Extended": "Rozšířeno", + "Extensions": "Rozšíření", + "Extra Query Parameters": "Další parametry dotazu", + "Extract Now": "Extrahovat nyní", + "Extract Pending Files": "Extrahovat čekající soubory", + "Extract now": "Extrahovat nyní", + "Extract properties from each object": "Extrahovat vlastnosti z každého objektu", + "Extracted At": "Extrahováno v", + "Extracted at": "Extrahováno v", + "Extraction": "Extrakce", + "Extraction Mode": "Režim extrakce", + "Extraction Scope": "Rozsah extrakce", + "Extraction Status": "Stav extrakce", + "Extraction error for {file}: {error}": "Chyba extrakce pro {file}: {error}", + "Facet Description": "Popis fasety", + "Facet Order": "Pořadí fasety", + "Facet Title": "Název fasety", + "Facet Type": "Typ fasety", + "Facet configuration updated successfully": "Konfigurace fasety byla úspěšně aktualizována", + "Facetable": "Fasetovatelné", + "Faceting Configuration:": "Konfigurace fasetování:", + "Facets discovered and configured successfully": "Fasety byly úspěšně objeveny a nakonfigurovány", + "Facets discovered successfully": "Fasety byly úspěšně objeveny", + "Failed": "Selhalo", + "Failed to add attachment to object": "Nepodařilo se přidat přílohu k objektu", + "Failed to add user to organisation": "Nepodařilo se přidat uživatele do organizace", + "Failed to analyze schema properties": "Nepodařilo se analyzovat vlastnosti schématu", + "Failed to archive conversation": "Nepodařilo se archivovat konverzaci", + "Failed to calculate sizes": "Nepodařilo se vypočítat velikosti", + "Failed to cancel deactivation": "Nepodařilo se zrušit deaktivaci", + "Failed to change active organisation: {error}": "Nepodařilo se změnit aktivní organizaci: {error}", + "Failed to change password": "Nepodařilo se změnit heslo", + "Failed to check version: {error}": "Nepodařilo se zkontrolovat verzi: {error}", + "Failed to clear audit trails: {error}": "Nepodařilo se vymazat auditní stopy: {error}", + "Failed to clear blob objects: {error}": "Nepodařilo se vymazat objekty blob: {error}", + "Failed to clear cache: {error}": "Nepodařilo se vymazat mezipaměť: {error}", + "Failed to clear collection": "Nepodařilo se vymazat kolekci", + "Failed to clear conversation": "Nepodařilo se vymazat konverzaci", + "Failed to clear embeddings: {error}": "Nepodařilo se vymazat vložení: {error}", + "Failed to clear search trails: {error}": "Nepodařilo se vymazat stopy vyhledávání: {error}", + "Failed to clear {type} cache: {error}": "Nepodařilo se vymazat mezipaměť {type}: {error}", + "Failed to copy changes": "Nepodařilo se zkopírovat změny", + "Failed to copy data": "Nepodařilo se zkopírovat data", + "Failed to copy data to clipboard": "Nepodařilo se zkopírovat data do schránky", + "Failed to copy to clipboard": "Nepodařilo se zkopírovat do schránky", + "Failed to copy token": "Nepodařilo se zkopírovat token", + "Failed to create conversation": "Nepodařilo se vytvořit konverzaci", + "Failed to create missing SOLR fields: {error}": "Nepodařilo se vytvořit chybějící pole SOLR: {error}", + "Failed to create or find project": "Nepodařilo se vytvořit nebo najít projekt", + "Failed to create token": "Nepodařilo se vytvořit token", + "Failed to delete SOLR collection: {error}": "Nepodařilo se smazat kolekci SOLR: {error}", + "Failed to delete collection": "Nepodařilo se smazat kolekci", + "Failed to delete conversation": "Nepodařilo se smazat konverzaci", + "Failed to delete objects for {schema}: {error}": "Nepodařilo se smazat objekty pro {schema}: {error}", + "Failed to delete webhook": "Nepodařilo se smazat webhook", + "Failed to depublish register: {error}": "Nepodařilo se zrušit publikování registru: {error}", + "Failed to depublish schema: {error}": "Nepodařilo se zrušit publikování schématu: {error}", + "Failed to depublish: {error}": "Nepodařilo se zrušit publikování: {error}", + "Failed to discover files: {error}": "Nepodařilo se objevit soubory: {error}", + "Failed to download API specification": "Nepodařilo se stáhnout specifikaci API", + "Failed to export data": "Nepodařilo se exportovat data", + "Failed to export objects": "Nepodařilo se exportovat objekty", + "Failed to fetch conversation history": "Nepodařilo se načíst historii konverzace", + "Failed to fix mismatched SOLR fields: {error}": "Nepodařilo se opravit neodpovídající pole SOLR: {error}", + "Failed to get SOLR field configuration": "Nepodařilo se získat konfiguraci pole SOLR", + "Failed to get chat statistics": "Nepodařilo se získat statistiky chatu", + "Failed to get database information: %s": "Nepodařilo se získat informace o databázi: %s", + "Failed to get entity categories": "Nepodařilo se získat kategorie entit", + "Failed to get entity statistics": "Nepodařilo se získat statistiky entit", + "Failed to get entity types": "Nepodařilo se získat typy entit", + "Failed to get response: {error}": "Nepodařilo se získat odpověď: {error}", + "Failed to invalidate app store cache: {error}": "Nepodařilo se zneplatnit mezipaměť App Store: {error}", + "Failed to leave organisation: {error}": "Nepodařilo se opustit organizaci: {error}", + "Failed to link object": "Nepodařilo se propojit objekt", + "Failed to load Deck cards": "Nepodařilo se načíst karty Deck", + "Failed to load LLM configuration": "Nepodařilo se načíst konfiguraci LLM", + "Failed to load Nextcloud groups": "Nepodařilo se načíst skupiny Nextcloud", + "Failed to load cache statistics: {error}": "Nepodařilo se načíst statistiky mezipaměti: {error}", + "Failed to load conversation": "Nepodařilo se načíst konverzaci", + "Failed to load entities": "Nepodařilo se načíst entity", + "Failed to load entity": "Nepodařilo se načíst entitu", + "Failed to load extraction data": "Nepodařilo se načíst data extrakce", + "Failed to load files": "Nepodařilo se načíst soubory", + "Failed to load linked contacts": "Nepodařilo se načíst propojené kontakty", + "Failed to load linked emails": "Nepodařilo se načíst propojené e-maily", + "Failed to load linked events": "Nepodařilo se načíst propojené události", + "Failed to load organisations": "Nepodařilo se načíst organizace", + "Failed to load register data": "Nepodařilo se načíst data registru", + "Failed to load related entities": "Nepodařilo se načíst související entity", + "Failed to load settings: {error}": "Nepodařilo se načíst nastavení: {error}", + "Failed to load statistics: {error}": "Nepodařilo se načíst statistiky: {error}", + "Failed to load templates": "Nepodařilo se načíst šablony", + "Failed to load view: {error}": "Nepodařilo se načíst pohled: {error}", + "Failed to load webhooks": "Nepodařilo se načíst webhooky", + "Failed to load workflows": "Nepodařilo se načíst pracovní postupy", + "Failed to process message": "Nepodařilo se zpracovat zprávu", + "Failed to publish register: {error}": "Nepodařilo se publikovat registr: {error}", + "Failed to publish schema: {error}": "Nepodařilo se publikovat schéma: {error}", + "Failed to publish: {error}": "Nepodařilo se publikovat: {error}", + "Failed to refresh database information": "Nepodařilo se obnovit informace o databázi", + "Failed to reindex collection": "Nepodařilo se přeindexovat kolekci", + "Failed to remove avatar": "Nepodařilo se odstranit avatar", + "Failed to remove link": "Nepodařilo se odstranit propojení", + "Failed to remove member: {error}": "Nepodařilo se odstranit člena: {error}", + "Failed to remove schema {schema}: {error}": "Nepodařilo se odebrat schéma {schema}: {error}", + "Failed to remove user from organisation": "Nepodařilo se odstranit uživatele z organizace", + "Failed to rename conversation": "Nepodařilo se přejmenovat konverzaci", + "Failed to request deactivation": "Nepodařilo se požádat o deaktivaci", + "Failed to restore conversation": "Nepodařilo se obnovit konverzaci", + "Failed to retrieve extraction status.": "Nepodařilo se získat stav extrakce.", + "Failed to retrieve objects for file.": "Nepodařilo se získat objekty pro soubor.", + "Failed to retry extraction": "Nepodařilo se opakovat extrakci", + "Failed to retry webhook": "Nepodařilo se opakovat webhook", + "Failed to revoke token": "Nepodařilo se odvolat token", + "Failed to save GitHub token": "Nepodařilo se uložit token GitHub", + "Failed to save GitLab URL": "Nepodařilo se uložit URL GitLab", + "Failed to save GitLab token": "Nepodařilo se uložit token GitLab", + "Failed to save LLM settings: {error}": "Nepodařilo se uložit nastavení LLM: {error}", + "Failed to save additional feedback": "Nepodařilo se uložit další zpětnou vazbu", + "Failed to save configuration.": "Nepodařilo se uložit konfiguraci.", + "Failed to save configuration: {error}": "Nepodařilo se uložit konfiguraci: {error}", + "Failed to save facet configuration": "Nepodařilo se uložit konfiguraci fasety", + "Failed to save feedback": "Nepodařilo se uložit zpětnou vazbu", + "Failed to save file settings: {error}": "Nepodařilo se uložit nastavení souborů: {error}", + "Failed to save n8n configuration": "Nepodařilo se uložit konfiguraci n8n", + "Failed to save preferences": "Nepodařilo se uložit předvolby", + "Failed to save roles": "Nepodařilo se uložit role", + "Failed to save settings": "Nepodařilo se uložit nastavení", + "Failed to save settings: {error}": "Nepodařilo se uložit nastavení: {error}", + "Failed to save view: {error}": "Nepodařilo se uložit pohled: {error}", + "Failed to save warmup interval: {error}": "Nepodařilo se uložit interval zahřívání: {error}", + "Failed to save webhook": "Nepodařilo se uložit webhook", + "Failed to send feedback": "Nepodařilo se odeslat zpětnou vazbu", + "Failed to set active organisation: {error}": "Nepodařilo se nastavit aktivní organizaci: {error}", + "Failed to setup SOLR: {error}": "Nepodařilo se nastavit SOLR: {error}", + "Failed to start file warmup: {error}": "Nepodařilo se zahájit zahřívání souborů: {error}", + "Failed to start processing {type} files: {error}": "Nepodařilo se zahájit zpracování souborů {type}: {error}", + "Failed to start vectorization: {error}": "Nepodařilo se zahájit vektorizaci: {error}", + "Failed to switch organisation: {error}": "Nepodařilo se přepnout organizaci: {error}", + "Failed to sync magic table for {schema}: {error}": "Nepodařilo se synchronizovat magickou tabulku pro {schema}: {error}", + "Failed to test SOLR connection: {error}": "Nepodařilo se otestovat připojení SOLR: {error}", + "Failed to test webhook": "Nepodařilo se otestovat webhook", + "Failed to update LLM settings: {error}": "Nepodařilo se aktualizovat nastavení LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Nepodařilo se aktualizovat nastavení Multitenancy: {error}", + "Failed to update RBAC settings: {error}": "Nepodařilo se aktualizovat nastavení RBAC: {error}", + "Failed to update Retention settings: {error}": "Nepodařilo se aktualizovat nastavení uchovávání: {error}", + "Failed to update SOLR settings: {error}": "Nepodařilo se aktualizovat nastavení SOLR: {error}", + "Failed to update favorite status": "Nepodařilo se aktualizovat stav oblíbenosti", + "Failed to update schema configuration for {schema}: {error}": "Nepodařilo se aktualizovat konfiguraci schématu pro {schema}: {error}", + "Failed to update schema properties": "Nepodařilo se aktualizovat vlastnosti schématu", + "Failed to update view": "Nepodařilo se aktualizovat pohled", + "Failed to update view: {error}": "Nepodařilo se aktualizovat pohled: {error}", + "Failed to update webhook": "Nepodařilo se aktualizovat webhook", + "Failed to upload avatar": "Nepodařilo se nahrát avatar", + "Failed to validate {schema}: {error}": "Nepodařilo se ověřit {schema}: {error}", + "Failed to vectorize files: {error}": "Nepodařilo se vektorizovat soubory: {error}", + "Failed to warmup names cache: {error}": "Nepodařilo se zahřát mezipaměť názvů: {error}", + "Federation": "Federace", + "Feedback recorded": "Zpětná vazba zaznamenána", + "Fewer sources (1-3):": "Méně zdrojů (1-3):", + "Field": "Pole", + "Fields to Display": "Pole k zobrazení", + "File": "Soubor", + "File #{id}": "Soubor #{id}", + "File Chunk Prediction": "Predikce úseků souboru", + "File Collection": "Kolekce souborů", + "File Handling": "Zpracování souborů", + "File Location": "Umístění souboru", + "File Management": "Správa souborů", + "File Name": "Název souboru", + "File Path": "Cesta k souboru", + "File Type Selection": "Výběr typu souboru", + "File Types to Process": "Typy souborů ke zpracování", + "File Vectorization": "Vektorizace souborů", + "File Warmup": "Zahřívání souborů", + "File actions menu": "Nabídka akcí souboru", + "File anonymized successfully": "Soubor byl úspěšně anonymizován", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vektorizace úseků souborů zatím není implementována. Úseky jsou připraveny a uloženy, ale služba vektorizace je ve vývoji.", + "File collection not configured": "Kolekce souborů není nakonfigurována", + "File discovery completed": "Objevování souborů dokončeno", + "File extraction completed": "Extrakce souborů dokončena", + "File extraction queued": "Extrakce souborů zařazena do fronty", + "File indexed successfully": "Soubor byl úspěšně indexován", + "File is already anonymized": "Soubor je již anonymizován", + "File name": "Název souboru", + "File not found": "Soubor nenalezen", + "File settings saved successfully": "Nastavení souborů byla úspěšně uložena", + "File settings updated successfully": "Nastavení souborů byla úspěšně aktualizována", + "File sources": "Zdroje souborů", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vektorizace souborů dokončena! {vectorized} úseků vektorizováno z {files} souborů. {failed} selhalo.", + "File vectorization configuration saved successfully": "Konfigurace vektorizace souborů byla úspěšně uložena", + "File vectorization started. Check the statistics section for progress.": "Vektorizace souborů zahájena. Postup zkontrolujte v sekci statistik.", + "File warmup completed": "Zahřívání souborů dokončeno", + "Files": "Soubory", + "Files added successfully": "Soubory byly úspěšně přidány", + "Files with Completed Extraction:": "Soubory s dokončenou extrakcí:", + "Files → fileCollection, Objects → objectCollection": "Soubory → fileCollection, Objekty → objectCollection", + "Files → filecollection, objects → objectcollection": "Soubory → filecollection, objekty → objectcollection", + "Filter Audit Trails": "Filtrovat auditní stopy", + "Filter Deleted Items": "Filtrovat smazané položky", + "Filter Objects": "Filtrovat objekty", + "Filter Properties": "Filtrovat vlastnosti", + "Filter Search Trails": "Filtrovat stopy vyhledávání", + "Filter Statistics": "Filtrovat statistiky", + "Filter and analyze search trail entries": "Filtrovat a analyzovat položky stop vyhledávání", + "Filter and manage audit trail entries": "Filtrovat a spravovat položky auditní stopy", + "Filter and manage soft deleted items": "Filtrovat a spravovat dočasně smazané položky", + "Filter and search entities": "Filtrovat a vyhledávat entity", + "Filter by object ID": "Filtrovat podle ID objektu", + "Filter by search term": "Filtrovat podle hledaného výrazu", + "Filter by type": "Filtrovat podle typu", + "Filter by webhook": "Filtrovat podle webhooku", + "Filter data loaded automatically. Use the filters below to refine your search.": "Data filtru načtena automaticky. Pro upřesnění vyhledávání použijte filtry níže.", + "Filter fields...": "Filtrovat pole...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrovat spouštěče webhooků podle vlastností užitečného zatížení (jedna na řádek, formát: klíč: hodnota)", + "Filtered": "Filtrováno", + "Filters": "Filtry", + "Finalizing setup...": "Dokončování nastavení...", + "Fireworks AI Chat Settings": "Nastavení chatu Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfigurace vložení Fireworks AI", + "First": "První", + "Fixed": "Opraveno", + "For chat and retrieval-augmented generation": "Pro chat a generování rozšířené o vyhledávání (RAG)", + "For vector embeddings and semantic search": "Pro vektorová vložení a sémantické vyhledávání", + "Format": "Formát", + "Format Issue": "Problém s formátem", + "Format constraint is missing": "Chybí omezení formátu", + "From (e.g. -7 days or 2025-01-01)": "Od (např. -7 days nebo 2025-01-01)", + "From Date": "Od data", + "From date": "Od data", + "Full data copied to clipboard": "Všechna data zkopírována do schránky", + "Function Tools": "Funkční nástroje", + "General Issue": "Obecný problém", + "Generate recommendations and confidence scores": "Generovat doporučení a skóre spolehlivosti", + "Generate report": "Generovat zprávu", + "Generate the verantwoordingsdocument": "Generovat verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generovat vektory ihned po vytvoření nových objektů", + "Generated: {time}": "Vygenerováno: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Získejte přehled v reálném čase o zdraví dat vaší organizace se zaměřením na registry, definice schémat a ukládání a využití objektů.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Získejte přehled v reálném čase o zdraví dat vaší organizace, trendech růstu a metrikách kvality.", + "GitHub Personal Access Token": "Osobní přístupový token GitHub", + "GitHub token is valid": "Token GitHub je platný", + "GitHub token is valid! Username: {username}": "Token GitHub je platný! Uživatelské jméno: {username}", + "GitHub token removed successfully": "Token GitHub byl úspěšně odstraněn", + "GitHub token saved successfully": "Token GitHub byl úspěšně uložen", + "GitHub token test failed: {message}": "Test tokenu GitHub selhal: {message}", + "GitLab Personal Access Token": "Osobní přístupový token GitLab", + "GitLab URL saved successfully": "URL GitLab byla úspěšně uložena", + "GitLab token is valid": "Token GitLab je platný", + "GitLab token is valid! Username: {username}": "Token GitLab je platný! Uživatelské jméno: {username}", + "GitLab token saved successfully": "Token GitLab byl úspěšně uložen", + "GitLab token test failed: {message}": "Test tokenu GitLab selhal: {message}", + "Go to linked Register": "Přejít na propojený registr", + "Go to linked Schema": "Přejít na propojené schéma", + "Go to organisation": "Přejít na organizaci", + "Grant additional permissions beyond standard CRUD operations": "Udělit další oprávnění nad rámec standardních operací CRUD", + "Group Quota": "Kvóta skupiny", + "Groups": "Skupiny", + "HTML preview": "Náhled HTML", + "HTTP Method": "Metoda HTTP", + "HTTP method used to send webhook requests": "Metoda HTTP použitá k odesílání požadavků webhooku", + "Headers": "Hlavičky", + "Health": "Zdraví", + "Heartbeat successful - connection kept alive": "Heartbeat úspěšný – připojení udrženo naživu", + "Helpful": "Užitečné", + "Here you can configure the details for various connections.": "Zde můžete nakonfigurovat detaily pro různá připojení.", + "Here you can configure the details for your organisation.": "Zde můžete nakonfigurovat detaily pro vaši organizaci.", + "Hide": "Skrýt", + "Hide Filters": "Skrýt filtry", + "Hide in collection view": "Skrýt v zobrazení kolekce", + "Hide in forms": "Skrýt ve formulářích", + "Hide in list view": "Skrýt v zobrazení seznamu", + "High": "Vysoká", + "High Confidence": "Vysoká spolehlivost", + "Host": "Hostitel", + "Hourly": "Každou hodinu", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Jak hluboko procházet vnořené vlastnosti objektů (1-20). Vyšší hodnoty zachytí více detailů, ale zvětší velikost vektoru.", + "How often to check for updates (1-168 hours)": "Jak často kontrolovat aktualizace (1-168 hodin)", + "How to handle retries for failed webhook deliveries": "Jak zpracovat opakování pro neúspěšná doručení webhooků", + "IBANs": "IBANy", + "ID": "ID", + "ID of input mapping (optional)": "ID vstupního mapování (volitelné)", + "ID of output mapping (optional)": "ID výstupního mapování (volitelné)", + "ID of the target resource": "ID cílového zdroje", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Limity rychlosti IP byly úspěšně vymazány", + "Identify properties not in the schema": "Identifikovat vlastnosti, které nejsou ve schématu", + "Immutable": "Neměnné", + "Import": "Import", + "Import Configuration": "Importovat konfiguraci", + "Import Data into Register": "Importovat data do registru", + "Import successful": "Import úspěšný", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importujte šablonu report-bundle.json a získejte registr `reports`, poté vytvořte svou první nástěnku prostřednictvím standardního UI objektů. Nástěnky deklarují své widgety v JSON a renderer přivádí každému widgetu živá agregační data.", + "Improved Property": "Vylepšená vlastnost", + "In use": "Používá se", + "Inactive": "Neaktivní", + "Inactive organisations cannot be used": "Neaktivní organizace nelze použít", + "Include IDs and names of related objects for better contextual search": "Zahrnout ID a názvy souvisejících objektů pro lepší kontextové vyhledávání", + "Include ids and names of related objects for better contextual search": "Zahrnout ID a názvy souvisejících objektů pro lepší kontextové vyhledávání", + "Include related object references": "Zahrnout odkazy na související objekty", + "Include schema and register metadata": "Zahrnout metadata schématu a registru", + "Index": "Index", + "Information about the current OpenRegister installation": "Informace o aktuální instalaci OpenRegister", + "Inherited from register": "Zděděno z registru", + "Inherits permissions from register": "Dědí oprávnění z registru", + "Initialization failed": "Inicializace selhala", + "Initialize Project": "Inicializovat projekt", + "Initializing SOLR setup...": "Inicializace nastavení SOLR...", + "Input Mapping": "Vstupní mapování", + "Inspect Fields": "Prozkoumat pole", + "Inspect Index": "Prozkoumat index", + "Inspect SOLR Index": "Prozkoumat index SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Nainstalujte aplikaci notify_push z Nextcloud App Store pro povolení aktualizací v reálném čase.", + "Integrations": "Integrace", + "Internet location (URL) of this installation": "Internetová adresa (URL) této instalace", + "Interval": "Interval", + "Invalid": "Neplatné", + "Invalid batch size. Must be between 1 and 5000": "Neplatná velikost dávky. Musí být mezi 1 a 5000", + "Invalid feedback type": "Neplatný typ zpětné vazby", + "Invalid field name provided": "Zadán neplatný název pole", + "Invalid maxObjects. Must be 0 (all) or positive number": "Neplatné maxObjects. Musí být 0 (vše) nebo kladné číslo", + "Invalid:": "Neplatné:", + "Invite Users": "Pozvat uživatele", + "Invited Users": "Pozvaní uživatelé", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Výsledky inzage", + "Issue": "Problém", + "Items per page:": "Položek na stránku:", + "Just now": "Právě teď", + "Key": "Klíč", + "LLM Configuration": "Konfigurace LLM", + "LLM actions menu": "Nabídka akcí LLM", + "LLM configuration saved successfully": "Konfigurace LLM byla úspěšně uložena", + "LLM features disabled": "Funkce LLM zakázány", + "LLM features enabled": "Funkce LLM povoleny", + "LLM must be enabled with an embedding provider configured": "LLM musí být povoleno s nakonfigurovaným poskytovatelem vložení", + "LLM settings saved successfully": "Nastavení LLM byla úspěšně uložena", + "LLM settings updated successfully": "Nastavení LLM byla úspěšně aktualizována", + "Label": "Štítek", + "Labels": "Štítky", + "Languages": "Jazyky", + "Last": "Poslední", + "Last Triggered": "Naposledy spuštěno", + "Leave empty to allow all users access": "Ponechte prázdné pro povolení přístupu všem uživatelům", + "Leave off for auto-detection from property format.": "Ponechte vypnuté pro automatickou detekci z formátu vlastnosti.", + "Left organisation successfully": "Organizace byla úspěšně opuštěna", + "Legal basis *": "Právní základ *", + "Length Range:": "Rozsah délky:", + "Linear": "Lineární", + "Link": "Propojit", + "Link an email from the Mail app sidebar to associate it with this object.": "Propojte e-mail z postranního panelu aplikace Mail a přiřaďte jej k tomuto objektu.", + "Link an object to see it here.": "Propojte objekt, abyste jej zde viděli.", + "Link another object": "Propojit další objekt", + "Link existing event": "Propojit existující událost", + "Link removed": "Propojení odstraněno", + "Link to Object": "Propojit s objektem", + "Link to {name}": "Propojit s {name}", + "Linked Objects": "Propojené objekty", + "Linked by {user}": "Propojeno uživatelem {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Propojené e-maily, události kalendáře, kontakty a karty Deck se zobrazí zde.", + "Linked to {name}": "Propojeno s {name}", + "Load Advanced Filters": "Načíst pokročilé filtry", + "Load advanced filters with live data from your search index": "Načíst pokročilé filtry s živými daty z vašeho vyhledávacího indexu", + "Load more": "Načíst více", + "Load view": "Načíst pohled", + "Loading API tokens...": "Načítání API tokenů...", + "Loading ConfigSets...": "Načítání sad ConfigSet...", + "Loading LLM configuration...": "Načítání konfigurace LLM...", + "Loading RBAC settings...": "Načítání nastavení RBAC...", + "Loading activity...": "Načítání činnosti...", + "Loading advanced filters...": "Načítání pokročilých filtrů...", + "Loading agents...": "Načítání agentů...", + "Loading application details...": "Načítání detailů aplikace...", + "Loading applications...": "Načítání aplikací...", + "Loading archived conversations...": "Načítání archivovaných konverzací...", + "Loading audit trails...": "Načítání auditních stop...", + "Loading available tools...": "Načítání dostupných nástrojů...", + "Loading cache statistics...": "Načítání statistik mezipaměti...", + "Loading collections...": "Načítání kolekcí...", + "Loading configurations...": "Načítání konfigurací...", + "Loading conversation...": "Načítání konverzace...", + "Loading conversations...": "Načítání konverzací...", + "Loading deleted items...": "Načítání smazaných položek...", + "Loading endpoints": "Načítání koncových bodů", + "Loading entities...": "Načítání entit...", + "Loading events...": "Načítání událostí...", + "Loading filter data...": "Načítání dat filtru...", + "Loading groups...": "Načítání skupin...", + "Loading linked objects...": "Načítání propojených objektů...", + "Loading log details...": "Načítání detailů protokolu...", + "Loading multitenancy settings...": "Načítání nastavení multitenancy...", + "Loading n8n configuration...": "Načítání konfigurace n8n...", + "Loading permission matrix...": "Načítání matice oprávnění...", + "Loading preferences...": "Načítání předvoleb...", + "Loading register data...": "Načítání dat registru...", + "Loading registers...": "Načítání registrů...", + "Loading retention settings...": "Načítání nastavení uchovávání...", + "Loading schemas...": "Načítání schémat...", + "Loading search configuration...": "Načítání konfigurace vyhledávání...", + "Loading search trails...": "Načítání stop vyhledávání...", + "Loading sources...": "Načítání zdrojů...", + "Loading statistics...": "Načítání statistik...", + "Loading text extraction settings...": "Načítání nastavení extrakce textu...", + "Loading tokens...": "Načítání tokenů...", + "Loading user groups...": "Načítání uživatelských skupin...", + "Loading users...": "Načítání uživatelů...", + "Loading views...": "Načítání pohledů...", + "Loading...": "Načítání...", + "Local": "Místní", + "Local Version": "Místní verze", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Najděte každý objekt odkazující na subjekt údajů (Art 15 inzage), zobrazte náhled vymazání (Art 17 vergetelheid) nebo exportujte jeho data (Art 20 portabiliteit).", + "Location": "Umístění", + "Location Field": "Pole umístění", + "Locations": "Umístění", + "Locked": "Uzamčeno", + "Locked:": "Uzamčeno:", + "Login successful": "Přihlášení úspěšné", + "Logs": "Protokoly", + "Low": "Nízká", + "Low Confidence": "Nízká spolehlivost", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Nižší čísla se zobrazí jako první v postranním panelu filtrů. Ponechte prázdné pro automatické řazení.", + "Lucene Version:": "Verze Lucene:", + "Magic table sync completed for {schema}": "Synchronizace magické tabulky dokončena pro {schema}", + "Magic table synchronized successfully": "Magická tabulka byla úspěšně synchronizována", + "Mail Integration": "Integrace Mail", + "Mail integration is not available": "Integrace Mail není dostupná", + "Make this installation discoverable within the federation network.": "Zpřístupnit tuto instalaci k nalezení ve federační síti.", + "Manage Organisation Roles": "Spravovat role organizace", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Spravujte kolekce SOLR (úložiště dat) a přiřaďte je objektům a souborům.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Spravujte sady ConfigSet SOLR (konfigurační šablony) pro vaše kolekce.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Spravujte kolekce SOLR (úložiště dat) a přiřaďte je objektům a souborům.", + "Manage SOLR configsets (configuration templates) for your collections.": "Spravujte sady configset SOLR (konfigurační šablony) pro vaše kolekce.", + "Manage and configure agents for automated tasks": "Spravovat a konfigurovat agenty pro automatizované úlohy", + "Manage and monitor file text extraction status": "Spravovat a monitorovat stav extrakce textu ze souborů", + "Manage and restore soft deleted items from your registers": "Spravovat a obnovovat dočasně smazané položky z vašich registrů", + "Manage and view detected entities from files and objects": "Spravovat a zobrazovat detekované entity ze souborů a objektů", + "Manage document templates and themes": "Spravovat šablony dokumentů a motivy", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Spravujte pracovní postupy n8n pro automatizaci OpenRegister. Pracovní postupy budou uloženy v nakonfigurovaném projektu.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Spravujte činnosti zpracování, provádějte žádosti subjektů údajů o přístup a auditujte shodu podle EU GDPR / nizozemské AVG.", + "Manage webhooks for event-driven integrations": "Spravovat webhooky pro integrace řízené událostmi", + "Manage your account settings, security, and personal data.": "Spravujte nastavení svého účtu, zabezpečení a osobní údaje.", + "Manage your applications and modules": "Spravujte své aplikace a moduly", + "Manage your chat conversations": "Spravujte své chatovací konverzace", + "Manage your data registers and their configurations": "Spravujte své datové registry a jejich konfigurace", + "Manage your data schemas and their properties": "Spravujte svá datová schémata a jejich vlastnosti", + "Manage your data sources and their configurations": "Spravujte své zdroje dat a jejich konfigurace", + "Manage your organisations and switch between them": "Spravujte své organizace a přepínejte mezi nimi", + "Manage your saved search configurations": "Spravujte své uložené konfigurace vyhledávání", + "Manage your system configurations and settings": "Spravujte své systémové konfigurace a nastavení", + "Managed": "Spravováno", + "Mappings": "Mapování", + "Mass validation completed successfully": "Hromadné ověření bylo úspěšně dokončeno", + "Mass validation failed: {error}": "Hromadné ověření selhalo: {error}", + "Max Files (0 = all)": "Max. souborů (0 = vše)", + "Max Length": "Max. délka", + "Max Length:": "Max. délka:", + "Max Objects (0 = all)": "Max. objektů (0 = vše)", + "Max Retries": "Max. opakování", + "Max Shards/Node": "Max. shardů/uzel", + "Max Tokens": "Max. tokenů", + "Max execution time (ms)": "Max. doba provedení (ms)", + "Max ms": "Max. ms", + "Max result count": "Max. počet výsledků", + "Max results": "Max. výsledků", + "Maximum": "Maximum", + "Maximum File Size (MB)": "Maximální velikost souboru (MB)", + "Maximum Nesting Depth": "Maximální hloubka vnoření", + "Maximum length": "Maximální délka", + "Maximum length constraint is missing": "Chybí omezení maximální délky", + "Maximum length is too restrictive": "Maximální délka je příliš omezující", + "Maximum number of files to process. Set to 0 to process all files.": "Maximální počet souborů ke zpracování. Nastavte na 0 pro zpracování všech souborů.", + "Maximum number of items": "Maximální počet položek", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maximální počet objektů ke zpracování. Nastavte na 0 pro zpracování všech objektů.", + "Maximum number of retry attempts for failed deliveries": "Maximální počet pokusů o opakování pro neúspěšná doručení", + "Maximum time to wait before committing changes": "Maximální doba čekání před potvrzením změn", + "Maximum tokens to generate": "Maximální počet tokenů ke generování", + "Maximum value": "Maximální hodnota", + "Maximum value constraint is missing": "Chybí omezení maximální hodnoty", + "Maximum value is too restrictive": "Maximální hodnota je příliš omezující", + "Medium": "Střední", + "Medium Confidence": "Střední spolehlivost", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Středně složité dotazy: Vyhledávání s určitým filtrováním nebo více parametry (např. rozsahy dat, konkrétní registry/schémata)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Středně složité dotazy: vyhledávání s určitým filtrováním nebo více parametry (např. rozsahy dat, konkrétní registry/schémata)", + "Member removed successfully": "Člen byl úspěšně odstraněn", + "Members": "Členové", + "Members ({count})": "Členové ({count})", + "Members of selected groups can access this view": "Členové vybraných skupin mají přístup k tomuto pohledu", + "Members:": "Členové:", + "Memory prediction calculated successfully": "Predikce paměti byla úspěšně vypočítána", + "Message content is required": "Obsah zprávy je povinný", + "Message does not belong to this conversation": "Zpráva nepatří do této konverzace", + "Message not found": "Zpráva nenalezena", + "Metadata": "Metadata", + "Metadata Filters": "Filtry metadat", + "Method": "Metoda", + "Method*": "Metoda*", + "Method: {method}": "Metoda: {method}", + "Min Length": "Min. délka", + "Min execution time (ms)": "Min. doba provedení (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. počet výsledků", + "Min results": "Min. výsledků", + "Minimum": "Minimum", + "Minimum length": "Minimální délka", + "Minimum number of items": "Minimální počet položek", + "Minimum value": "Minimální hodnota", + "Minimum value constraint is missing": "Chybí omezení minimální hodnoty", + "Minimum value is too restrictive": "Minimální hodnota je příliš omezující", + "Missing conversation or agentUuid": "Chybí konverzace nebo agentUuid", + "Missing conversationId": "Chybí conversationId", + "Missing message": "Chybí zpráva", + "Missing object fields created successfully": "Chybějící pole objektu byla úspěšně vytvořena", + "Mode:": "Režim:", + "Model": "Model", + "Model Name": "Název modelu", + "Modified": "Upraveno", + "Monthly": "Měsíčně", + "More sources (10-20):": "Více zdrojů (10-20):", + "Most Active Objects": "Nejaktivnější objekty", + "Multiple Of": "Násobek", + "Multiple of": "Násobek", + "Multiple search trail deletion not implemented yet": "Mazání více stop vyhledávání zatím není implementováno", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Nastavení multitenancy byla úspěšně aktualizována", + "My Account": "Můj účet", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Název", + "Name *": "Název *", + "Name*": "Název*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Mezipaměť názvů byla úspěšně zahřáta: {count} názvů načteno za {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Mezipaměť názvů byla úspěšně zahřáta: {count} názvů načteno za {time}. {message}", + "Names cache warmup completed": "Zahřívání mezipaměti názvů dokončeno", + "Naming pattern for copies:": "Vzor pojmenování pro kopie:", + "New Collection Name": "Název nové kolekce", + "New Configuration": "Nová konfigurace", + "New Conversation": "Nová konverzace", + "New Properties": "Nové vlastnosti", + "New Property": "Nová vlastnost", + "New User Group": "Nová uživatelská skupina", + "New Value": "Nová hodnota", + "New activity": "Nová činnost", + "New password": "Nové heslo", + "New processing activity": "Nová činnost zpracování", + "New users without specific organisation membership will be automatically added to this organisation": "Noví uživatelé bez konkrétního členství v organizaci budou automaticky přidáni do této organizace", + "New verwerkingsactiviteit": "Nová verwerkingsactiviteit", + "Next": "Další", + "Nextcloud Groups": "Skupiny Nextcloud", + "No": "Ne", + "No API tokens.": "Žádné API tokeny.", + "No ConfigSets found": "Nenalezeny žádné sady ConfigSet", + "No Deck cards linked to this object": "K tomuto objektu nejsou propojeny žádné karty Deck", + "No GitHub token configured": "Není nakonfigurován žádný token GitHub", + "No GitHub token provided": "Nebyl poskytnut žádný token GitHub", + "No GitLab token provided": "Nebyl poskytnut žádný token GitLab", + "No Organisation": "Žádná organizace", + "No active filters": "Žádné aktivní filtry", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Žádné aktivní objekty k dočasnému smazání pro schéma {schema}. Pro odstranění dočasně smazaných objektů použijte \"Trvale smazat\".", + "No activity data available": "Nejsou dostupná žádná data o činnosti", + "No activity found.": "Nebyla nalezena žádná činnost.", + "No agents available": "Nejsou dostupní žádní agenti", + "No agents found": "Nenalezeni žádní agenti", + "No app store cache exists yet": "Zatím neexistuje žádná mezipaměť App Store", + "No applications are available.": "Nejsou dostupné žádné aplikace.", + "No applications found": "Nenalezeny žádné aplikace", + "No approval steps for this object.": "Žádné kroky schvalování pro tento objekt.", + "No archived conversations": "Žádné archivované konverzace", + "No audit trail entries found": "Nenalezeny žádné položky auditní stopy", + "No changes recorded": "Nebyly zaznamenány žádné změny", + "No chunks to vectorize": "Žádné úseky k vektorizaci", + "No collections found": "Nenalezeny žádné kolekce", + "No configuration data": "Žádná konfigurační data", + "No configuration selected": "Není vybrána žádná konfigurace", + "No configurations are available.": "Nejsou dostupné žádné konfigurace.", + "No configurations found": "Nenalezeny žádné konfigurace", + "No configurations found for this application.": "Pro tuto aplikaci nebyly nalezeny žádné konfigurace.", + "No contacts linked to this object": "K tomuto objektu nejsou propojeny žádné kontakty", + "No conversations yet. Create a new one to get started!": "Zatím žádné konverzace. Vytvořte novou a začněte!", + "No dashboards yet": "Zatím žádné nástěnky", + "No data available": "Nejsou dostupná žádná data", + "No data available for chart": "Nejsou dostupná žádná data pro graf", + "No deleted items found": "Nenalezeny žádné smazané položky", + "No deletion data available": "Nejsou dostupná žádná data o mazání", + "No description found": "Nenalezen žádný popis", + "No description provided": "Nebyl poskytnut žádný popis", + "No emails linked to this object": "K tomuto objektu nejsou propojeny žádné e-maily", + "No endpoints defined": "Nejsou definovány žádné koncové body", + "No entities detected for this email.": "Pro tento e-mail nebyly detekovány žádné entity.", + "No entities detected in this file. Run text extraction first.": "V tomto souboru nebyly detekovány žádné entity. Nejprve spusťte extrakci textu.", + "No entities found": "Nenalezeny žádné entity", + "No entities have been detected yet": "Zatím nebyly detekovány žádné entity", + "No events found": "Nenalezeny žádné události", + "No events linked to this object": "K tomuto objektu nejsou propojeny žádné události", + "No expired audit trails found to clear": "Nenalezeny žádné prošlé auditní stopy k vymazání", + "No expired search trails found to clear": "Nenalezeny žádné prošlé stopy vyhledávání k vymazání", + "No extraction data available for this file": "Pro tento soubor nejsou dostupná žádná data extrakce", + "No facetable fields available. Select a register and schema to see available filters.": "Nejsou dostupná žádná fasetovatelná pole. Pro zobrazení dostupných filtrů vyberte registr a schéma.", + "No files found": "Nenalezeny žádné soubory", + "No files have been extracted yet": "Zatím nebyly extrahovány žádné soubory", + "No files selected": "Nejsou vybrány žádné soubory", + "No files to process": "Žádné soubory ke zpracování", + "No files to reindex": "Žádné soubory k přeindexování", + "No filters are currently active. This will delete ALL audit trail entries!": "Momentálně nejsou aktivní žádné filtry. Tímto smažete VŠECHNY položky auditní stopy!", + "No groups found. Try a different search.": "Nenalezeny žádné skupiny. Zkuste jiné vyhledávání.", + "No label": "Žádný štítek", + "No labels": "Žádné štítky", + "No linked objects": "Žádné propojené objekty", + "No log entries found": "Nenalezeny žádné položky protokolu", + "No logs are available for this configuration.": "Pro tuto konfiguraci nejsou dostupné žádné protokoly.", + "No logs are available for this source.": "Pro tento zdroj nejsou dostupné žádné protokoly.", + "No logs found": "Nenalezeny žádné protokoly", + "No matches": "Žádné shody", + "No mismatched fields found - SOLR schema is properly configured": "Nenalezena žádná neodpovídající pole – schéma SOLR je správně nakonfigurováno", + "No objects are currently selected for deletion.": "Momentálně nejsou vybrány žádné objekty ke smazání.", + "No objects are currently selected for permanent deletion.": "Momentálně nejsou vybrány žádné objekty k trvalému smazání.", + "No objects are currently selected for restoration.": "Momentálně nejsou vybrány žádné objekty k obnovení.", + "No objects found": "Nenalezeny žádné objekty", + "No objects found to vectorize": "Nenalezeny žádné objekty k vektorizaci", + "No objects linked to this email": "K tomuto e-mailu nejsou propojeny žádné objekty", + "No objects selected": "Nejsou vybrány žádné objekty", + "No objects selected for deletion": "Nejsou vybrány žádné objekty ke smazání", + "No objects selected for restoration": "Nejsou vybrány žádné objekty k obnovení", + "No objects to delete": "Žádné objekty ke smazání", + "No objects to delete for schema {schema}": "Žádné objekty ke smazání pro schéma {schema}", + "No objects were permanently deleted": "Žádné objekty nebyly trvale smazány", + "No organisations found": "Nenalezeny žádné organizace", + "No personal data was found for this subject identifier.": "Pro tento identifikátor subjektu nebyly nalezeny žádné osobní údaje.", + "No processing activities yet": "Zatím žádné činnosti zpracování", + "No properties found": "Nenalezeny žádné vlastnosti", + "No properties match your filters.": "Žádné vlastnosti neodpovídají vašim filtrům.", + "No purge date set": "Není nastaveno datum vyčištění", + "No register data available": "Nejsou dostupná žádná data registru", + "No register objects reference this file": "Tento soubor neodkazuje žádný objekt registru", + "No registers found": "Nenalezeny žádné registry", + "No registers found for this application.": "Pro tuto aplikaci nebyly nalezeny žádné registry.", + "No registers found. Create a register to configure permissions.": "Nenalezeny žádné registry. Pro konfiguraci oprávnění vytvořte registr.", + "No related cases found for this sender": "Pro tohoto odesílatele nebyly nalezeny žádné související případy", + "No related entities": "Žádné související entity", + "No relations found": "Nenalezeny žádné vztahy", + "No request body available": "Není dostupné žádné tělo požadavku", + "No response body available": "Není dostupné žádné tělo odpovědi", + "No saved views yet. Create one in the Search tab!": "Zatím žádné uložené pohledy. Vytvořte jeden na kartě Vyhledávání!", + "No saved views yet. create one in the search tab!": "Zatím žádné uložené pohledy. vytvořte jeden na kartě vyhledávání!", + "No scan run yet": "Zatím nebyla spuštěna žádná kontrola", + "No schema data available": "Nejsou dostupná žádná data schématu", + "No schema selected for exploration": "Není vybráno žádné schéma k prozkoumání", + "No schemas configured for mail linking.": "Nejsou nakonfigurována žádná schémata pro propojení e-mailů.", + "No schemas found": "Nenalezena žádná schémata", + "No schemas found for this application.": "Pro tuto aplikaci nebyla nalezena žádná schémata.", + "No search terms data available": "Nejsou dostupná žádná data hledaných výrazů", + "No search trail entries found": "Nenalezeny žádné položky stop vyhledávání", + "No sources are available.": "Nejsou dostupné žádné zdroje.", + "No sources found": "Nenalezeny žádné zdroje", + "No synchronizations found": "Nenalezeny žádné synchronizace", + "No templates found": "Nenalezeny žádné šablony", + "No templates have been created yet": "Zatím nebyly vytvořeny žádné šablony", + "No tools available. Tools can be registered by installed apps.": "Nejsou dostupné žádné nástroje. Nástroje mohou registrovat nainstalované aplikace.", + "No users in this organisation.": "Žádní uživatelé v této organizaci.", + "No valid magic-mapped register+schema combinations found": "Nenalezeny žádné platné kombinace registru+schématu mapované magicky", + "No views found. Create views first before configuring vectorization.": "Nenalezeny žádné pohledy. Před konfigurací vektorizace nejprve vytvořte pohledy.", + "No views match your search": "Žádné pohledy neodpovídají vašemu vyhledávání", + "No webhooks found": "Nenalezeny žádné webhooky", + "No webhooks have been configured yet": "Zatím nebyly nakonfigurovány žádné webhooky", + "No workflows found in this project. Create workflows in the n8n editor.": "V tomto projektu nebyly nalezeny žádné pracovní postupy. Vytvořte pracovní postupy v editoru n8n.", + "None": "Žádné", + "Not Found": "Nenalezeno", + "Not extracted": "Neextrahováno", + "Not helpful": "Neužitečné", + "Not in use": "Nepoužívá se", + "Note:": "Poznámka:", + "Notifications": "Oznámení", + "Number Constraints": "Omezení čísel", + "Number of Sources": "Počet zdrojů", + "Number of chunks to vectorize in one API call (1-100).": "Počet úseků k vektorizaci v jednom volání API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Počet úseků k vektorizaci v jednom volání API. Vyšší = rychlejší, ale více paměti. Doporučeno: 10-50.", + "Number of objects to process in each batch (1-5000).": "Počet objektů ke zpracování v každé dávce (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Počet objektů k vektorizaci v jednom volání API. Vyšší = rychlejší, ale více paměti. Doporučeno: 10-50.", + "Numeric Range:": "Číselný rozsah:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Kolekce objektů", + "Object Configuration:": "Konfigurace objektu:", + "Object Count Prediction": "Predikce počtu objektů", + "Object Extraction Mode": "Režim extrakce objektů", + "Object ID": "ID objektu", + "Object Management": "Správa objektů", + "Object Text Extraction": "Extrakce textu z objektů", + "Object Vectorization": "Vektorizace objektů", + "Object Warmup": "Zahřívání objektů", + "Object changes in owned objects": "Změny objektů ve vlastněných objektech", + "Object collection not configured": "Kolekce objektů není nakonfigurována", + "Object metadata incomplete for file upload": "Metadata objektu jsou neúplná pro nahrání souboru", + "Object not found": "Objekt nenalezen", + "Object permanently deleted": "Objekt trvale smazán", + "Object restored successfully": "Objekt byl úspěšně obnoven", + "Object settings updated successfully": "Nastavení objektu byla úspěšně aktualizována", + "Object sources": "Zdroje objektů", + "Object successfully deleted": "Objekt byl úspěšně smazán", + "Object successfully modified": "Objekt byl úspěšně upraven", + "Object unlocked successfully": "Objekt byl úspěšně odemčen", + "Object vectorization configuration saved successfully": "Konfigurace vektorizace objektů byla úspěšně uložena", + "Object vectorized successfully": "Objekt byl úspěšně vektorizován", + "Object:": "Objekt:", + "Objects": "Objekty", + "Objects Analyzed": "Analyzované objekty", + "Objects Distribution": "Rozložení objektů", + "Objects being analyzed": "Analyzované objekty", + "Objects by Register": "Objekty podle registru", + "Objects by Schema": "Objekty podle schématu", + "Objects deletion completed for {schema}": "Mazání objektů dokončeno pro {schema}", + "Objects deletion completed successfully": "Mazání objektů bylo úspěšně dokončeno", + "Objects in schema": "Objekty ve schématu", + "Objects successfully deleted": "Objekty byly úspěšně smazány", + "Objects to Process:": "Objekty ke zpracování:", + "Objects to be analyzed": "Objekty k analýze", + "Objects to be deleted": "Objekty ke smazání", + "Objects to be validated": "Objekty k ověření", + "Objects will be serialized as JSON text before vectorization": "Objekty budou před vektorizací serializovány jako text JSON", + "Objects will be soft deleted and moved to the": "Objekty budou dočasně smazány a přesunuty do", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekty budou dočasně smazány (označeny jako smazané, ale ponechány v databázi). V případě potřeby je lze později obnovit.", + "Old Value": "Stará hodnota", + "Oldest Item (days)": "Nejstarší položka (dny)", + "Ollama Chat Settings": "Nastavení chatu Ollama", + "Ollama Embedding Configuration": "Konfigurace vložení Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Pouze se změnami", + "Only members of selected groups can access this organisation": "Pouze členové vybraných skupin mají přístup k této organizaci", + "Open Nextcloud App Store": "Otevřít Nextcloud App Store", + "Open OpenRegister sidebar": "Otevřít postranní panel OpenRegister", + "Open Register Objects": "Otevřít objekty registru", + "Open folder": "Otevřít složku", + "Open in OpenRegister": "Otevřít v OpenRegister", + "Open n8n Editor": "Otevřít editor n8n", + "OpenAI Chat Settings": "Nastavení chatu OpenAI", + "OpenAI Embedding Configuration": "Konfigurace vložení OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Nastavení OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Nástěnky definované operátorem a naplánované zprávy. Každá nástěnka je plnohodnotný objekt v registru `reports`; widgety jsou deklarovány v poli `widgets` nástěnky a vykreslovány živě z agregací / GraphQL.", + "Optimizing search performance...": "Optimalizace výkonu vyhledávání...", + "Optional URL-friendly identifier": "Volitelný identifikátor vhodný pro URL", + "Optional description for this facet": "Volitelný popis pro tuto fasetu", + "Optional end date property": "Volitelná vlastnost koncového data", + "Optional event description template": "Volitelná šablona popisu události", + "Optional location property": "Volitelná vlastnost umístění", + "Optional webhook secret for signature verification": "Volitelný tajný klíč webhooku pro ověření podpisu", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Volitelné: Přidejte parametry dotazu pro filtrování odkazovaného schématu (např. status=active&type=public)", + "Or": "Nebo", + "Order": "Pořadí", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Seřazené jazykové značky BCP 47. První jazyk je výchozí pro registr a řídí záložní mechanismus Accept-Language pro přeložitelné vlastnosti.", + "Organisation": "Organizace", + "Organisation Configuration": "Konfigurace organizace", + "Organisation ID": "ID organizace", + "Organisation Members": "Členové organizace", + "Organisation Statistics": "Statistiky organizace", + "Organisation created successfully": "Organizace byla úspěšně vytvořena", + "Organisation membership changes": "Změny členství v organizaci", + "Organisation name is required": "Název organizace je povinný", + "Organisation settings saved successfully": "Nastavení organizace byla úspěšně uložena", + "Organisation successfully created": "Organizace byla úspěšně vytvořena", + "Organisation successfully updated": "Organizace byla úspěšně aktualizována", + "Organisational measures": "Organizační opatření", + "Organisations": "Organizace", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organizace", + "Organization ID (Optional)": "ID organizace (volitelné)", + "Organizations": "Organizace", + "Orphaned Items": "Osiřelé položky", + "Other": "Ostatní", + "Output Mapping": "Výstupní mapování", + "Overview": "Přehled", + "Owner": "Vlastník", + "Owner:": "Vlastník:", + "PDF": "PDF", + "PII hits": "Zásahy PII", + "PII rows": "Řádky PII", + "Page {current} of {total}": "Stránka {current} z {total}", + "Parallel Mode (Faster, more resource intensive)": "Paralelní režim (rychlejší, náročnější na zdroje)", + "Parallel:": "Paralelně:", + "Password": "Heslo", + "Password changes are not supported by your authentication provider.": "Změny hesla nejsou podporovány vaším poskytovatelem ověření.", + "Password for SOLR authentication (optional)": "Heslo pro ověření SOLR (volitelné)", + "Password updated successfully": "Heslo bylo úspěšně aktualizováno", + "Path": "Cesta", + "Path in repository": "Cesta v úložišti", + "Path where the configuration file will be saved in the repository": "Cesta, kam bude konfigurační soubor uložen v úložišti", + "Path where the register OAS file will be saved in the repository": "Cesta, kam bude soubor OAS registru uložen v úložišti", + "Pattern (regex)": "Vzor (regulární výraz)", + "Pattern Issue": "Problém se vzorem", + "Pattern constraint is missing": "Chybí omezení vzoru", + "Patterns:": "Vzory:", + "Pending": "Čeká", + "Permanent Delete Mode": "Režim trvalého mazání", + "Permanently Delete": "Trvale smazat", + "Permanently Delete ({count})": "Trvale smazat ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Trvale smazat všech {count} dočasně smazaných objektů. Toto nelze vrátit zpět!", + "Permanently delete already soft-deleted objects": "Trvale smazat již dočasně smazané objekty", + "Permanently delete this conversation?": "Trvale smazat tuto konverzaci?", + "Permission Matrix": "Matice oprávnění", + "Person": "Osoba", + "Personal Data": "Osobní údaje", + "Personal Data Export": "Export osobních údajů", + "Persons": "Osoby", + "Phone": "Telefon", + "Phone Numbers": "Telefonní čísla", + "Photos": "Fotografie", + "Please create an agent in the": "Vytvořte prosím agenta v", + "Please select a user": "Vyberte prosím uživatele", + "Please select an agent to continue": "Pro pokračování vyberte prosím agenta", + "Please select an organisation": "Vyberte prosím organizaci", + "Please select at least one item to import": "Vyberte prosím alespoň jednu položku k importu", + "Please select registers or schemas first": "Nejprve prosím vyberte registry nebo schémata", + "Please select which register and schema to use for the new object": "Vyberte prosím, který registr a schéma použít pro nový objekt", + "Please try again later.": "Zkuste to prosím později.", + "Please wait while we fetch your agents.": "Počkejte prosím, než načteme vaše agenty.", + "Please wait while we fetch your applications.": "Počkejte prosím, než načteme vaše aplikace.", + "Please wait while we fetch your configurations.": "Počkejte prosím, než načteme vaše konfigurace.", + "Please wait while we fetch your deleted items.": "Počkejte prosím, než načteme vaše smazané položky.", + "Please wait while we fetch your sources.": "Počkejte prosím, než načteme vaše zdroje.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Počkejte prosím, než trvale smažeme kolekci SOLR. Může to chvíli trvat.", + "Polls": "Ankety", + "Popular Search Terms": "Oblíbené hledané výrazy", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Předvolby uloženy", + "Prerequisites": "Předpoklady", + "Press Enter to send, Shift+Enter for new line": "Stiskněte Enter pro odeslání, Shift+Enter pro nový řádek", + "Press enter to send, shift+enter for new line": "Stiskněte enter pro odeslání, shift+enter pro nový řádek", + "Prettify": "Zkrášlit", + "Preview erasure": "Náhled vymazání", + "Preview:": "Náhled:", + "Previous": "Předchozí", + "Private": "Soukromé", + "Private Agent (Default)": "Soukromý agent (výchozí)", + "Private agents": "Soukromí agenti", + "Process Name (optional)": "Název procesu (volitelné)", + "Processed {indexed} files, {failed} failed": "Zpracováno {indexed} souborů, {failed} selhalo", + "Processes chunks in batches with simulated parallelism.": "Zpracovává úseky v dávkách se simulovaným paralelismem.", + "Processes file chunks sequentially (safest).": "Zpracovává úseky souborů sekvenčně (nejbezpečnější).", + "Processes objects in chunks with simulated parallelism.": "Zpracovává objekty po úsecích se simulovaným paralelismem.", + "Processes objects sequentially (safest).": "Zpracovává objekty sekvenčně (nejbezpečnější).", + "Processing": "Zpracování", + "Processing Limits": "Limity zpracování", + "Processing...": "Zpracování...", + "Project initialized successfully": "Projekt byl úspěšně inicializován", + "Project not found. Please initialize first.": "Projekt nenalezen. Nejprve prosím inicializujte.", + "Properties": "Vlastnosti", + "Property Behaviors": "Chování vlastnosti", + "Property Configuration:": "Konfigurace vlastnosti:", + "Property Filters": "Filtry vlastností", + "Property Title": "Název vlastnosti", + "Property Type": "Typ vlastnosti", + "Property can be improved": "Vlastnost lze vylepšit", + "Property could not be added": "Vlastnost nelze přidat", + "Property could not be updated": "Vlastnost nelze aktualizovat", + "Property name of inversed relation": "Název vlastnosti obráceného vztahu", + "Property successfully added": "Vlastnost byla úspěšně přidána", + "Property successfully updated": "Vlastnost byla úspěšně aktualizována", + "Protected": "Chráněno", + "Provider": "Poskytovatel", + "Provider is required for testing": "Pro testování je vyžadován poskytovatel", + "Public": "Veřejné", + "Public View": "Veřejný pohled", + "Public views can be accessed by anyone in the system": "K veřejným pohledům má přístup kdokoli v systému", + "Publish": "Publikovat", + "Publish Configuration to GitHub": "Publikovat konfiguraci na GitHub", + "Publish OAS": "Publikovat OAS", + "Publish Objects": "Publikovat objekty", + "Publish Register OAS to GitHub": "Publikovat OAS registru na GitHub", + "Publish all objects in this schema": "Publikovat všechny objekty v tomto schématu", + "Publish objects to make them publicly available": "Publikujte objekty, aby byly veřejně dostupné", + "Published": "Publikováno", + "Published:": "Publikováno:", + "Publishing...": "Publikování...", + "Purge": "Vyčistit", + "Purge Date": "Datum vyčištění", + "Purge {count} object from database": "Vyčistit {count} objekt z databáze", + "Purge {count} objects from database": "Vyčistit {count} objektů z databáze", + "Purpose limitation *": "Omezení účelu *", + "Push Notifications": "Push oznámení", + "Query Complexity Distribution": "Rozložení složitosti dotazů", + "Query Help": "Nápověda k dotazům", + "Query parameter is required": "Parametr dotazu je povinný", + "Quota": "Kvóta", + "RAG": "RAG", + "RAG Configuration": "Konfigurace RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Nastavení RBAC byla úspěšně aktualizována", + "Rate limits cleared successfully": "Limity rychlosti byly úspěšně vymazány", + "Raw Changes Data": "Surová data změn", + "Re-vectorize on object update": "Znovu vektorizovat při aktualizaci objektu", + "Read": "Číst", + "Real-time push notification status via notify_push": "Stav push oznámení v reálném čase prostřednictvím notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push oznámení v reálném čase jsou aktivní. Připojení klienti dostávají okamžité aktualizace, když jsou objekty vytvořeny, aktualizovány nebo smazány.", + "Realtime push active": "Push v reálném čase aktivní", + "Realtime push not available — the notify_push app is not installed": "Push v reálném čase není dostupný — aplikace notify_push není nainstalována", + "Reason": "Důvod", + "Reason (optional)": "Důvod (volitelné)", + "Rebase operation completed successfully": "Operace rebase byla úspěšně dokončena", + "Rebase operation failed: {error}": "Operace rebase selhala: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Doporučení:", + "Recommended Type:": "Doporučený typ:", + "Recommended: 5 sources": "Doporučeno: 5 zdrojů", + "Refresh": "Obnovit", + "Refresh Data": "Obnovit data", + "Refresh Stats": "Obnovit statistiky", + "Refresh Workflows": "Obnovit pracovní postupy", + "Refresh dashboard": "Obnovit nástěnku", + "Refresh database info": "Obnovit informace o databázi", + "Register": "Registr", + "Register #{id}": "Registr #{id}", + "Register / Schema": "Registr / Schéma", + "Register ID": "ID registru", + "Register Objects": "Objekty registru", + "Register Overview": "Přehled registru", + "Register Statistics": "Statistiky registru", + "Register Totals": "Souhrny registru", + "Register and schema are required": "Registr a schéma jsou povinné", + "Register annotation": "Anotace registru", + "Register depublished successfully": "Publikování registru bylo úspěšně zrušeno", + "Register not found": "Registr nenalezen", + "Register objects": "Objekty registru", + "Register objects deletion completed successfully": "Mazání objektů registru bylo úspěšně dokončeno", + "Register published successfully": "Registr byl úspěšně publikován", + "Register/Schema Usage": "Využití registru/schématu", + "Register:": "Registr:", + "Registers": "Registry", + "Reindex": "Přeindexovat", + "Reindex completed": "Přeindexování dokončeno", + "Reindex completed successfully": "Přeindexování bylo úspěšně dokončeno", + "Reindex completed! Processed {count} objects in {duration}s": "Přeindexování dokončeno! Zpracováno {count} objektů za {duration}s", + "Reindex failed": "Přeindexování selhalo", + "Reindex failed: %s": "Přeindexování selhalo: %s", + "Reject": "Odmítnout", + "Rejected": "Odmítnuto", + "Related Cases": "Související případy", + "Relations": "Vztahy", + "Remote Version": "Vzdálená verze", + "Remove": "Odebrat", + "Remove Deck card": "Odebrat kartu Deck", + "Remove avatar": "Odebrat avatar", + "Remove contact": "Odebrat kontakt", + "Remove filter": "Odebrat filtr", + "Remove from favorites": "Odebrat z oblíbených", + "Remove from list": "Odebrat ze seznamu", + "Remove group": "Odebrat skupinu", + "Remove link": "Odebrat propojení", + "Remove link to {name}": "Odebrat propojení s {name}", + "Remove link to {name}?": "Odebrat propojení s {name}?", + "Remove link to {title}": "Odebrat propojení s {title}", + "Remove oneOf entry": "Odebrat položku oneOf", + "Remove property after write-back": "Odebrat vlastnost po zpětném zápisu", + "Remove {title}": "Odebrat {title}", + "Remove {userId} from this organisation?": "Odebrat {userId} z této organizace?", + "Removed": "Odebráno", + "Removed from favorites": "Odebráno z oblíbených", + "Removing schema {schema}...": "Odebírání schématu {schema}...", + "Rename Conversation": "Přejmenovat konverzaci", + "Rename conversation": "Přejmenovat konverzaci", + "Replicas": "Repliky", + "Reports": "Zprávy", + "Repository": "Úložiště", + "Request Body": "Tělo požadavku", + "Request Data": "Data požadavku", + "Request Details": "Detaily požadavku", + "Request Quota (per day)": "Kvóta požadavků (za den)", + "Request account deactivation": "Požádat o deaktivaci účtu", + "Request account deactivation. This will notify administrators for review.": "Požádejte o deaktivaci účtu. Tím budou upozorněni správci k přezkoumání.", + "Request body must be JSON with a \"query\" field": "Tělo požadavku musí být JSON s polem \"query\"", + "Request timeout in seconds": "Časový limit požadavku v sekundách", + "Requested at": "Požádáno v", + "Required": "Povinné", + "Required Token Scopes": "Požadované rozsahy tokenu", + "Required field": "Povinné pole", + "Required status is inconsistent": "Stav povinnosti je nekonzistentní", + "Requires Dolphin AI for OCR text extraction": "Vyžaduje Dolphin AI pro extrakci textu pomocí OCR", + "Rerun Search": "Znovu spustit vyhledávání", + "Rerunning search: {searchTerm}": "Opětovné spuštění vyhledávání: {searchTerm}", + "Reset Changes": "Resetovat změny", + "Reset Filters": "Resetovat filtry", + "Resource Quotas": "Kvóty zdrojů", + "Response Body": "Tělo odpovědi", + "Response Details": "Detaily odpovědi", + "Response Time:": "Doba odezvy:", + "Restore": "Obnovit", + "Restore conversation": "Obnovit konverzaci", + "Restore or permanently delete items": "Obnovit nebo trvale smazat položky", + "Restore {count} object": "Obnovit {count} objekt", + "Restore {count} objects": "Obnovit {count} objektů", + "Result Count Range": "Rozsah počtu výsledků", + "Results": "Výsledky", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Doba uchovávání (doba trvání ISO-8601, např. P10Y, P30D)", + "Retention settings updated successfully": "Nastavení uchovávání byla úspěšně aktualizována", + "Retrieve all objects for this schema": "Načíst všechny objekty pro toto schéma", + "Retry": "Opakovat", + "Retry Failed Extractions": "Opakovat neúspěšné extrakce", + "Retry Policy": "Zásada opakování", + "Retry completed": "Opakování dokončeno", + "Retry upload": "Opakovat nahrání", + "Retrying...": "Opakování...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Zkontrolujte vybrané objekty níže. Kliknutím na tlačítko odebrat můžete odebrat všechny objekty, které nechcete smazat.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Zkontrolujte vybrané objekty níže. Můžete odebrat všechny objekty, které nechcete zahrnout.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Zkontrolujte vybrané objekty níže. Kliknutím na tlačítko odebrat můžete odebrat všechny objekty, které nechcete trvale smazat. Tuto akci nelze vrátit zpět.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Zkontrolujte vybrané objekty níže. Kliknutím na tlačítko odebrat můžete odebrat všechny objekty, které nechcete obnovit. Objekty budou obnoveny do svého původního umístění.", + "Revoke": "Odvolat", + "Right": "Právo", + "Risk Level": "Úroveň rizika", + "Risk level": "Úroveň rizika", + "Roles and Permissions": "Role a oprávnění", + "Roles updated successfully": "Role byly úspěšně aktualizovány", + "Run compliance scan": "Spustit kontrolu shody", + "SOLR Connection Settings": "Nastavení připojení SOLR", + "SOLR Index Warmup": "Zahřívání indexu SOLR", + "SOLR Version:": "Verze SOLR:", + "SOLR actions menu": "Nabídka akcí SOLR", + "SOLR base path (usually /solr)": "Základní cesta SOLR (obvykle /solr)", + "SOLR collection deleted successfully": "Kolekce SOLR byla úspěšně smazána", + "SOLR connection test failed: {error}": "Test připojení SOLR selhal: {error}", + "SOLR connection test successful!": "Test připojení SOLR úspěšný!", + "SOLR index warmup completed successfully": "Zahřívání indexu SOLR bylo úspěšně dokončeno", + "SOLR is disabled": "SOLR je zakázáno", + "SOLR is not available or not configured": "SOLR není dostupné nebo nakonfigurováno", + "SOLR logging disabled": "Protokolování SOLR zakázáno", + "SOLR logging enabled": "Protokolování SOLR povoleno", + "SOLR search disabled": "Vyhledávání SOLR zakázáno", + "SOLR search enabled": "Vyhledávání SOLR povoleno", + "SOLR server hostname or IP address": "Název hostitele nebo IP adresa serveru SOLR", + "SOLR server port number (optional, defaults to 8983)": "Číslo portu serveru SOLR (volitelné, výchozí je 8983)", + "SOLR settings updated successfully": "Nastavení SOLR byla úspěšně aktualizována", + "SOLR setup completed successfully": "Nastavení SOLR bylo úspěšně dokončeno", + "SOLR setup completed successfully!": "Nastavení SOLR bylo úspěšně dokončeno!", + "SOLR setup error: %s": "Chyba nastavení SOLR: %s", + "SOLR setup failed": "Nastavení SOLR selhalo", + "SOLR setup failed - check logs": "Nastavení SOLR selhalo – zkontrolujte protokoly", + "SOLR setup failed. Please check the configuration and try again.": "Nastavení SOLR selhalo. Zkontrolujte prosím konfiguraci a zkuste to znovu.", + "SOLR setup failed: {error}": "Nastavení SOLR selhalo: {error}", + "SOLR warmup failed: {error}": "Zahřívání SOLR selhalo: {error}", + "Sample Values:": "Ukázkové hodnoty:", + "Save": "Uložit", + "Save Configuration": "Uložit konfiguraci", + "Save Connection Settings": "Uložit nastavení připojení", + "Save Roles": "Uložit role", + "Save Settings": "Uložit nastavení", + "Save changes": "Uložit změny", + "Save current search as view": "Uložit aktuální vyhledávání jako pohled", + "Save labels": "Uložit štítky", + "Save tags for {name}": "Uložit štítky pro {name}", + "Save the organisation first to manage users.": "Pro správu uživatelů nejprve uložte organizaci.", + "Saved Views": "Uložené pohledy", + "Saving...": "Ukládání...", + "Scheduled": "Naplánováno", + "Schema": "Schéma", + "Schema ID": "ID schématu", + "Schema Name": "Název schématu", + "Schema Statistics": "Statistiky schématu", + "Schema annotation": "Anotace schématu", + "Schema configuration updated successfully for {schema}": "Konfigurace schématu byla úspěšně aktualizována pro {schema}", + "Schema depublished successfully": "Publikování schématu bylo úspěšně zrušeno", + "Schema objects deletion completed successfully": "Mazání objektů schématu bylo úspěšně dokončeno", + "Schema published successfully": "Schéma bylo úspěšně publikováno", + "Schema reference ($ref)": "Odkaz na schéma ($ref)", + "Schema successfully created": "Schéma bylo úspěšně vytvořeno", + "Schema successfully updated": "Schéma bylo úspěšně aktualizováno", + "Schema {schema} removed successfully": "Schéma {schema} bylo úspěšně odebráno", + "Schema:": "Schéma:", + "Schemas": "Schémata", + "Scheme": "Schéma", + "Search": "Vyhledávání", + "Search / Views": "Vyhledávání / Pohledy", + "Search / views": "Vyhledávání / pohledy", + "Search Activity": "Aktivita vyhledávání", + "Search Analytics": "Analytika vyhledávání", + "Search Configuration": "Konfigurace vyhledávání", + "Search Entities": "Vyhledávat entity", + "Search Files": "Vyhledávat soubory", + "Search GitHub": "Vyhledávat na GitHubu", + "Search GitLab": "Vyhledávat na GitLabu", + "Search Method": "Metoda vyhledávání", + "Search Mode": "Režim vyhledávání", + "Search Query": "Vyhledávací dotaz", + "Search Results": "Výsledky vyhledávání", + "Search Term": "Hledaný výraz", + "Search Trail Details": "Detaily stopy vyhledávání", + "Search Trail Management": "Správa stop vyhledávání", + "Search Trail Statistics": "Statistiky stop vyhledávání", + "Search Trails": "Stopy vyhledávání", + "Search Views": "Vyhledávat pohledy", + "Search Webhooks": "Vyhledávat webhooky", + "Search agents...": "Vyhledávat agenty...", + "Search applications...": "Vyhledávat aplikace...", + "Search by file name or path": "Vyhledávat podle názvu nebo cesty souboru", + "Search by name or URL": "Vyhledávat podle názvu nebo URL", + "Search by title or UUID...": "Vyhledávat podle názvu nebo UUID...", + "Search by value": "Vyhledávat podle hodnoty", + "Search configurations": "Vyhledávat konfigurace", + "Search data sources...": "Vyhledávat zdroje dat...", + "Search groups...": "Vyhledávat skupiny...", + "Search in Database Objects": "Vyhledávat v databázových objektech", + "Search in Files": "Vyhledávat v souborech", + "Search in Objects": "Vyhledávat v objektech", + "Search objects": "Vyhledávat objekty", + "Search objects...": "Vyhledávat objekty...", + "Search property names...": "Vyhledávat názvy vlastností...", + "Search registers...": "Vyhledávat registry...", + "Search schemas...": "Vyhledávat schémata...", + "Search trail deletion not implemented yet": "Mazání stop vyhledávání zatím není implementováno", + "Search views...": "Vyhledávat pohledy...", + "Search {name}...": "Vyhledávat {name}...", + "Searching...": "Vyhledávání...", + "Secret": "Tajný klíč", + "Secret key for HMAC signature generation (optional)": "Tajný klíč pro generování podpisu HMAC (volitelné)", + "Security": "Zabezpečení", + "See organisations as a table": "Zobrazit organizace jako tabulku", + "See organisations as cards": "Zobrazit organizace jako karty", + "Select AI Agent": "Vybrat AI agenta", + "Select Active Organisation": "Vybrat aktivní organizaci", + "Select All": "Vybrat vše", + "Select ConfigSet": "Vybrat sadu ConfigSet", + "Select File Types to Vectorize:": "Vyberte typy souborů k vektorizaci:", + "Select HTTP method": "Vyberte metodu HTTP", + "Select Register and Schema": "Vybrat registr a schéma", + "Select Views to Vectorize:": "Vyberte pohledy k vektorizaci:", + "Select a Nextcloud group to add": "Vyberte skupinu Nextcloud k přidání", + "Select a branch": "Vyberte větev", + "Select a date property": "Vyberte vlastnost data", + "Select a model or type a custom model name": "Vyberte model nebo zadejte vlastní název modelu", + "Select a register": "Vyberte registr", + "Select a register...": "Vyberte registr...", + "Select a repository": "Vyberte úložiště", + "Select a repository you have write access to": "Vyberte úložiště, ke kterému máte přístup pro zápis", + "Select a schema": "Vyberte schéma", + "Select a schema...": "Vyberte schéma...", + "Select a user": "Vyberte uživatele", + "Select agent type": "Vyberte typ agenta", + "Select all entities": "Vybrat všechny entity", + "Select an AI Agent": "Vyberte AI agenta", + "Select an AI agent to begin chatting with your data.": "Vyberte AI agenta a začněte chatovat se svými daty.", + "Select an email to see linked objects": "Vyberte e-mail pro zobrazení propojených objektů", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Vyberte organizaci a uživatele a přidejte jej jako člena. Organizace vyhledávejte podle názvu.", + "Select backend": "Vyberte backend", + "Select chat model": "Vyberte chat model", + "Select collection for files": "Vyberte kolekci pro soubory", + "Select collection for objects": "Vyberte kolekci pro objekty", + "Select configuration type...": "Vyberte typ konfigurace...", + "Select default organisation": "Vyberte výchozí organizaci", + "Select entity {value}": "Vybrat entitu {value}", + "Select event to listen to...": "Vyberte událost k naslouchání...", + "Select fields to display": "Vyberte pole k zobrazení", + "Select files with the correct extension": "Vyberte soubory se správnou příponou", + "Select group": "Vyberte skupinu", + "Select groups (optional)": "Vyberte skupiny (volitelné)", + "Select groups to notify...": "Vyberte skupiny k oznámení...", + "Select groups with access to this agent": "Vyberte skupiny s přístupem k tomuto agentovi", + "Select groups...": "Vyberte skupiny...", + "Select model": "Vyberte model", + "Select one or more registers": "Vyberte jeden nebo více registrů", + "Select one or more schemas": "Vyberte jedno nebo více schémat", + "Select options...": "Vyberte možnosti...", + "Select or create labels, or select \"No label\" to add files": "Vyberte nebo vytvořte štítky, nebo vyberte \"Žádný štítek\" pro přidání souborů", + "Select owner application (optional)...": "Vyberte vlastnickou aplikaci (volitelné)...", + "Select period": "Vyberte období", + "Select property to send as payload": "Vyberte vlastnost k odeslání jako užitečné zatížení", + "Select provider": "Vyberte poskytovatele", + "Select registers and schemas to save a view": "Vyberte registry a schémata pro uložení pohledu", + "Select retry policy": "Vyberte zásadu opakování", + "Select role": "Vyberte roli", + "Select schemas (empty = all schemas)": "Vyberte schémata (prázdné = všechna schémata)", + "Select schemas to warm up": "Vyberte schémata k zahřátí", + "Select search mode": "Vyberte režim vyhledávání", + "Select source type...": "Vyberte typ zdroje...", + "Select target type": "Vyberte cílový typ", + "Select the branch to publish to": "Vyberte větev pro publikování", + "Select the event this webhook should listen to": "Vyberte událost, které má tento webhook naslouchat", + "Select views to filter data (optional)": "Vyberte pohledy pro filtrování dat (volitelné)", + "Select views to limit which data the agent can access": "Vyberte pohledy pro omezení dat, ke kterým má agent přístup", + "Select views to vectorize:": "Vyberte pohledy k vektorizaci:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Vyberte, které skupiny Nextcloud jsou dostupné pro tuto organizaci. Uživatelé v těchto skupinách budou mít přístup ke zdrojům organizace.", + "Select which data views the AI can search": "Vyberte, které datové pohledy může AI prohledávat", + "Select which property from the event should be used as the webhook payload data": "Vyberte, která vlastnost z události by měla být použita jako data užitečného zatížení webhooku", + "Select which tools the AI can use to perform actions": "Vyberte, které nástroje může AI použít k provádění akcí", + "Select {fieldName} values": "Vyberte hodnoty {fieldName}", + "Selected": "Vybráno", + "Selected Groups": "Vybrané skupiny", + "Selected Objects ({count})": "Vybrané objekty ({count})", + "Selected audit trails deleted successfully": "Vybrané auditní stopy byly úspěšně smazány", + "Selected search trails deleted successfully": "Vybrané stopy vyhledávání byly úspěšně smazány", + "Selected users can access this view": "Vybraní uživatelé mají přístup k tomuto pohledu", + "Send additional feedback": "Odeslat další zpětnou vazbu", + "Send as CloudEvent": "Odeslat jako CloudEvent", + "Send as cloudevent": "Odeslat jako cloudevent", + "Sensitive PII": "Citlivé osobní údaje (PII)", + "Serial Mode (Safer, slower)": "Sériový režim (bezpečnější, pomalejší)", + "Serial:": "Sériově:", + "Server Configuration": "Konfigurace serveru", + "Server Information": "Informace o serveru", + "Set as Active": "Nastavit jako aktivní", + "Set as active organisation": "Nastavit jako aktivní organizaci", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Nastavte limity pro využití API a spotřebu tokenů. Pro neomezené zdroje použijte 0.", + "Settings": "Nastavení", + "Settings updated successfully": "Nastavení byla úspěšně aktualizována", + "Shards": "Shardy", + "Share": "Sdílet", + "Share with Groups": "Sdílet se skupinami", + "Share with Users": "Sdílet s uživateli", + "Show": "Zobrazit", + "Show Filters": "Zobrazit filtry", + "Show less": "Zobrazit méně", + "Show only entries with changes": "Zobrazit pouze položky se změnami", + "Showing {count} dashboard(s)": "Zobrazeno {count} nástěnek", + "Showing {count} processing activities": "Zobrazeno {count} činností zpracování", + "Showing {showing} of {total} agents": "Zobrazeno {showing} z {total} agentů", + "Showing {showing} of {total} applications": "Zobrazeno {showing} z {total} aplikací", + "Showing {showing} of {total} audit trail entries": "Zobrazeno {showing} z {total} položek auditní stopy", + "Showing {showing} of {total} configurations": "Zobrazeno {showing} z {total} konfigurací", + "Showing {showing} of {total} deleted items": "Zobrazeno {showing} z {total} smazaných položek", + "Showing {showing} of {total} entities": "Zobrazeno {showing} z {total} entit", + "Showing {showing} of {total} files": "Zobrazeno {showing} z {total} souborů", + "Showing {showing} of {total} log entries": "Zobrazeno {showing} z {total} položek protokolu", + "Showing {showing} of {total} search trail entries": "Zobrazeno {showing} z {total} položek stop vyhledávání", + "Showing {showing} of {total} sources": "Zobrazeno {showing} z {total} zdrojů", + "Showing {showing} of {total} templates": "Zobrazeno {showing} z {total} šablon", + "Showing {showing} of {total} webhooks": "Zobrazeno {showing} z {total} webhooků", + "Showing {shown} of {total} organisations": "Zobrazeno {shown} z {total} organizací", + "Simple": "Jednoduché", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednoduché dotazy: Základní textová vyhledávání s minimem parametrů (např. jeden hledaný výraz, žádné pokročilé filtry)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednoduché dotazy: základní textová vyhledávání s minimem parametrů (např. jeden hledaný výraz, žádné pokročilé filtry)", + "Size": "Velikost", + "Slug": "Slug", + "Soft Delete Mode": "Režim dočasného mazání", + "Soft Deleted Items": "Dočasně smazané položky", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Dočasně smazat všechny objekty pro toto schéma ({active} aktivních, {deleted} již smazaných)", + "SolrCloud mode enabled": "Režim SolrCloud povolen", + "Something went wrong while adding files": "Při přidávání souborů se něco pokazilo", + "Source": "Zdroj", + "Source successfully updated": "Zdroj byl úspěšně aktualizován", + "Sources": "Zdroje", + "Sources:": "Zdroje:", + "Special Rights": "Zvláštní práva", + "Standalone SOLR mode": "Samostatný režim SOLR", + "Start Conversation": "Zahájit konverzaci", + "Start Date Field": "Pole počátečního data", + "Start Vectorization": "Zahájit vektorizaci", + "Start a conversation": "Zahájit konverzaci", + "Starting names cache warmup...": "Zahájení zahřívání mezipaměti názvů...", + "Starting validation for {schema}...": "Zahájení ověřování pro {schema}...", + "Starting {action} for {schema}...": "Zahájení {action} pro {schema}...", + "Starting...": "Zahajování...", + "Statistics": "Statistiky", + "Statistics and Metrics": "Statistiky a metriky", + "Statistics refreshed": "Statistiky obnoveny", + "Stats calculation not yet implemented": "Výpočet statistik zatím není implementován", + "Status": "Stav", + "Status Code": "Stavový kód", + "Step": "Krok", + "Storage": "Úložiště", + "Storage Quota (MB)": "Kvóta úložiště (MB)", + "String Constraints": "Omezení řetězců", + "Subject identifier (email, BSN, name, etc.)": "Identifikátor subjektu (e-mail, BSN, jméno atd.)", + "Subscribe": "Odebírat", + "Subscribed": "Odebíráno", + "Success": "Úspěch", + "Success Rate": "Míra úspěšnosti", + "Success Status": "Stav úspěchu", + "Successful": "Úspěšné", + "Successfully added user to organisation: {name}": "Uživatel byl úspěšně přidán do organizace: {name}", + "Successfully cleared {count} audit trails": "Úspěšně vymazáno {count} auditních stop", + "Successfully cleared {count} blob storage objects": "Úspěšně vymazáno {count} objektů úložiště blob", + "Successfully cleared {count} search trails": "Úspěšně vymazáno {count} stop vyhledávání", + "Successfully created {count} SOLR fields": "Úspěšně vytvořeno {count} polí SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Úspěšně smazáno {count} vložení. Znovu prosím vektorizujte svá data.", + "Successfully deleted {count} objects for {schema}": "Úspěšně smazáno {count} objektů pro {schema}", + "Successfully fixed {count} SOLR field configurations": "Úspěšně opraveno {count} konfigurací polí SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Úspěšně importováno: {registers} registrů, {schemas} schémat, {objects} objektů", + "Successfully joined organisation": "Úspěšné připojení k organizaci", + "Successfully permanently deleted {count} objects": "Úspěšně trvale smazáno {count} objektů", + "Successfully processed {count} files!": "Úspěšně zpracováno {count} souborů!", + "Successfully restored {count} object": "Úspěšně obnoven {count} objekt", + "Successfully restored {count} objects": "Úspěšně obnoveno {count} objektů", + "Successfully saved configuration for {count} facets!": "Úspěšně uložena konfigurace pro {count} faset!", + "Supported File Types": "Podporované typy souborů", + "Switch Active Organisation": "Přepnout aktivní organizaci", + "Switch Organisation": "Přepnout organizaci", + "Sync Table": "Synchronizovat tabulku", + "Synchronization Settings": "Nastavení synchronizace", + "Syncing magic table for {schema}...": "Synchronizace magické tabulky pro {schema}...", + "Syncs": "Synchronizace", + "System Default": "Výchozí systémové", + "System Prompt": "Systémový prompt", + "System announcements": "Systémová oznámení", + "Table": "Tabulka", + "Target ID": "ID cíle", + "Target Type*": "Cílový typ*", + "Technical Description": "Technický popis", + "Technical Details": "Technické detaily", + "Technical description for developers and administrators": "Technický popis pro vývojáře a správce", + "Technical measures": "Technická opatření", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Teplota", + "Templates": "Šablony", + "Test": "Test", + "Test Chat": "Otestovat chat", + "Test Connection": "Otestovat připojení", + "Test Embedding": "Otestovat vložení", + "Test endpoint executed successfully": "Testovací koncový bod byl úspěšně proveden", + "Test webhook delivered successfully": "Testovací webhook byl úspěšně doručen", + "Test webhook delivery failed": "Doručení testovacího webhooku selhalo", + "Test webhook sent successfully": "Testovací webhook byl úspěšně odeslán", + "Testing...": "Testování...", + "Text Chunk #{id}": "Textový úsek #{id}", + "Text Extraction": "Extrakce textu", + "Text Extraction Engine": "Nástroj pro extrakci textu", + "Text chunks": "Textové úseky", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Textové úseky jsou generovány během extrakce souborů a ukládány do databáze. Vektorizace čte tyto předem rozdělené soubory a převádí je na vložení.", + "Text extracted successfully": "Text byl úspěšně extrahován", + "Text extraction disabled": "Extrakce textu zakázána", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplikace Nextcloud Kalendář není na tomto serveru nainstalována nebo povolena.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplikace Nextcloud Kontakty není na tomto serveru nainstalována nebo povolena.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplikace Nextcloud Deck není na tomto serveru nainstalována nebo povolena.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplikace Nextcloud Mail není na tomto serveru nainstalována nebo povolena.", + "The URL where webhook events will be sent": "URL, kam budou odesílány události webhooku", + "The requested conversation does not exist": "Požadovaná konverzace neexistuje", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Požadovanou nástěnku nelze načíst. Ověřte URL nebo vyberte jinou nástěnku ze seznamu.", + "The saved view and all its search configuration": "Uložený pohled a celá jeho konfigurace vyhledávání", + "There are no audit trail entries matching your current filters.": "Vašim aktuálním filtrům neodpovídají žádné položky auditní stopy.", + "There are no deleted items matching your current filters.": "Vašim aktuálním filtrům neodpovídají žádné smazané položky.", + "There are no search trail entries matching your current filters.": "Vašim aktuálním filtrům neodpovídají žádné položky stop vyhledávání.", + "There are no webhook log entries matching your filters.": "Vašim filtrům neodpovídají žádné položky protokolu webhooku.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Budou uchovány podle nakonfigurované doby uchovávání jejich schématu a automaticky trvale smazány po vypršení doby uchovávání. Doba uchovávání je konfigurovatelná pro každé schéma a lze ji nalézt v nastavení schématu.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Budou uchovány podle nakonfigurované doby uchovávání jejich schématu a poté trvale odstraněny.", + "This action cannot be undone.": "Tuto akci nelze vrátit zpět.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Tuto akci nelze vrátit zpět. Ujistěte se, že tuto sadu ConfigSet nepoužívají žádné kolekce.", + "This action cannot be undone. Make sure no collections are using this configset.": "Tuto akci nelze vrátit zpět. Ujistěte se, že tuto sadu configset nepoužívají žádné kolekce.", + "This action will submit a deactivation request to your administrators.": "Tato akce odešle žádost o deaktivaci vašim správcům.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Tento agent nemá žádné konfigurovatelné pohledy ani nástroje, ale níže můžete přesto upravit nastavení RAG.", + "This analysis may take some time": "Tato analýza může chvíli trvat", + "This audit trail entry does not contain any change information.": "Tato položka auditní stopy neobsahuje žádné informace o změnách.", + "This configuration has no data defined.": "Tato konfigurace nemá definovaná žádná data.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Tento koncový bod je zastaralý. Místo toho použijte koncové body založené na úsecích.", + "This entity has no relations to objects or files": "Tato entita nemá žádné vztahy k objektům ani souborům", + "This is a test webhook from OpenRegister": "Toto je testovací webhook z OpenRegister", + "This is not valid JSON": "Toto není platný JSON", + "This key already exists on this schema": "Tento klíč již v tomto schématu existuje", + "This schema must use Magic Table configuration to sync": "Toto schéma musí pro synchronizaci používat konfiguraci magické tabulky", + "This schema must use magic table configuration to sync": "Toto schéma musí pro synchronizaci používat konfiguraci magické tabulky", + "This source has no associated registers.": "Tento zdroj nemá žádné přidružené registry.", + "This token will only be shown once. Copy it now.": "Tento token bude zobrazen pouze jednou. Zkopírujte jej nyní.", + "This will copy the _default ConfigSet with the new name": "Tímto se zkopíruje sada ConfigSet _default s novým názvem", + "This will copy the _default configset with the new name": "Tímto se zkopíruje sada configset _default s novým názvem", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Tímto trvale smažete VŠECHNA vložení (vektory) z databáze. Bude nutné znovu vektorizovat všechny objekty a soubory. Tuto akci nelze vrátit zpět.\n\nOpravdu chcete pokračovat?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Tímto trvale smažete VŠECHNA vložení (vektory) z databáze. Bude nutné znovu vektorizovat všechny objekty a soubory. Tuto akci nelze vrátit zpět.\\n\\nOpravdu chcete pokračovat?", + "This will permanently delete:": "Tímto trvale smažete:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Tímto trvale odstraníte {count} již dočasně smazaných objektů z databáze. Tuto akci nelze vrátit zpět.", + "Timeout (seconds)": "Časový limit (sekundy)", + "Timestamp": "Časové razítko", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: Povolte pouze pohledy, které potřebují sémantické vyhledávání, abyste minimalizovali náklady na vložení. Jednoduché vyhledávací tabulky vektorizaci málokdy potřebují.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: povolte pouze pohledy, které potřebují sémantické vyhledávání, abyste minimalizovali náklady na vložení. Jednoduché vyhledávací tabulky vektorizaci málokdy potřebují.", + "Title": "Název", + "Title *": "Název *", + "Title Template": "Šablona názvu", + "Title*": "Název*", + "To (e.g. -1 year or 2025-12-31)": "Do (např. -1 year nebo 2025-12-31)", + "To Date": "Do data", + "To add files larger than or equal to 512MB, go to the": "Pro přidání souborů o velikosti 512 MB nebo větší přejděte do", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Pro úpravu velikosti úseku nebo strategie přejděte do Konfigurace souborů → Limity zpracování.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Pro úpravu velikosti úseku nebo strategie přejděte do konfigurace souborů → limity zpracování.", + "To change the embedding provider or model, go to LLM Configuration.": "Pro změnu poskytovatele nebo modelu vložení přejděte do Konfigurace LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Pro změnu poskytovatele nebo modelu vložení přejděte do konfigurace LLM.", + "To date": "Do data", + "Toggle search sidebar": "Přepnout postranní panel vyhledávání", + "Token Created": "Token vytvořen", + "Token Quota (per request)": "Kvóta tokenů (na požadavek)", + "Token copied to clipboard": "Token zkopírován do schránky", + "Token name": "Název tokenu", + "Token revoked": "Token odvolán", + "Tools": "Nástroje", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Nástroje se provádějí s výchozími uživatelskými oprávněními agenta, pokud není aktivní žádná uživatelská relace (např. úlohy cron). Výchozího uživatele nakonfigurujte na kartě Nastavení.", + "Tools respect the agent's own role and organization boundaries.": "Nástroje respektují vlastní roli agenta a hranice organizace.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Nástroje respektují pohledy, oprávnění a hranice organizace agenta.", + "Top Deleters": "Nejčastější mazatelé", + "Total": "Celkem", + "Total Audit Trails": "Celkem auditních stop", + "Total Chunks Available:": "Celkem dostupných úseků:", + "Total Deleted Items": "Celkem smazaných položek", + "Total Members": "Celkem členů", + "Total Objects": "Celkem objektů", + "Total Objects in Database:": "Celkem objektů v databázi:", + "Total Organisations": "Celkem organizací", + "Total Results": "Celkem výsledků", + "Total Searches": "Celkem vyhledávání", + "Total Size": "Celková velikost", + "Total Storage": "Celkové úložiště", + "Total events": "Celkem událostí", + "Total:": "Celkem:", + "Totals": "Souhrny", + "Try searching by UUID or with different keywords": "Zkuste vyhledávat podle UUID nebo s jinými klíčovými slovy", + "Type": "Typ", + "Type \"DELETE COLLECTION\" to confirm:": "Pro potvrzení napište \"DELETE COLLECTION\":", + "Type Issue": "Problém s typem", + "Type Variations:": "Variace typu:", + "Type filter (optional, e.g. email)": "Filtr typu (volitelné, např. email)", + "Type must be \"positive\" or \"negative\"": "Typ musí být \"positive\" nebo \"negative\"", + "Type to search for groups": "Pište pro vyhledávání skupin", + "Type to search for objects...": "Pište pro vyhledávání objektů...", + "Type to search for organisations": "Pište pro vyhledávání organizací", + "Type to search for users": "Pište pro vyhledávání uživatelů", + "Type to search groups...": "Pište pro vyhledávání skupin...", + "Type to search users...": "Pište pro vyhledávání uživatelů...", + "Type to search...": "Pište pro vyhledávání...", + "Type your username to confirm": "Pro potvrzení napište své uživatelské jméno", + "URL": "URL", + "URL where Ollama is running": "URL, kde běží Ollama", + "UUID copied to clipboard": "UUID zkopírováno do schránky", + "UUID:": "UUID:", + "Unchanged": "Beze změny", + "Unique Search Terms": "Jedinečné hledané výrazy", + "Unique Terms": "Jedinečné výrazy", + "Unknown": "Neznámé", + "Unknown Object": "Neznámý objekt", + "Unknown Register": "Neznámý registr", + "Unknown Schema": "Neznámé schéma", + "Unknown error": "Neznámá chyba", + "Unknown widget type: {type}": "Neznámý typ widgetu: {type}", + "Unlink email": "Zrušit propojení e-mailu", + "Unlink event": "Zrušit propojení události", + "Unnamed Object": "Nepojmenovaný objekt", + "Untitled": "Bez názvu", + "Untitled View": "Pohled bez názvu", + "Update": "Aktualizovat", + "Update Operations": "Operace aktualizace", + "Update available: {local} → {remote}": "Dostupná aktualizace: {local} → {remote}", + "Update configuration: ...": "Aktualizovat konfiguraci: ...", + "Update register OAS: ...": "Aktualizovat OAS registru: ...", + "Update vectors when object data changes (recommended for accurate search)": "Aktualizovat vektory při změně dat objektu (doporučeno pro přesné vyhledávání)", + "Updated": "Aktualizováno", + "Updated At": "Aktualizováno v", + "Updated:": "Aktualizováno:", + "Upload new avatar": "Nahrát nový avatar", + "Uploading configuration sets...": "Nahrávání konfiguračních sad...", + "Uptime:": "Doba provozu:", + "Url": "Url", + "Usage Count": "Počet použití", + "Use AI agents for processing and analysis": "Použít AI agenty pro zpracování a analýzu", + "Use Large Language Model features": "Použít funkce velkých jazykových modelů", + "Use SolrCloud with Zookeeper for distributed search": "Použít SolrCloud se Zookeeperem pro distribuované vyhledávání", + "Use SolrCloud with zookeeper for distributed search": "Použít SolrCloud se zookeeperem pro distribuované vyhledávání", + "Use VNG APIs instead of MongoDB.": "Použít VNG API místo MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Použít výchozí rozsahy (Posledních 7/30/90 dní, Poslední rok, Starší)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Použít externí úložiště (např. MongoDB) místo interního úložiště Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Pomocí filtrů zúžíte položky auditní stopy podle registru, schématu, typu akce, uživatele, rozsahu dat nebo ID objektu.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Pomocí filtrů zúžíte smazané položky podle registru, schématu, data smazání nebo uživatele, který je smazal.", + "Use filters to narrow down entities by type or category.": "Pomocí filtrů zúžíte entity podle typu nebo kategorie.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Pomocí filtrů zúžíte položky stop vyhledávání podle registru, schématu, stavu úspěchu, uživatele, rozsahu dat, hledaných výrazů nebo metrik výkonu.", + "Used By Collections": "Používáno kolekcemi", + "Used by": "Používáno", + "User": "Uživatel", + "User Agent Statistics": "Statistiky user agentů", + "User Description": "Popis uživatele", + "User Quota": "Kvóta uživatele", + "User rate limits cleared successfully": "Limity rychlosti uživatele byly úspěšně vymazány", + "User removed successfully": "Uživatel byl úspěšně odstraněn", + "User-friendly description shown in forms and help text": "Uživatelsky přívětivý popis zobrazený ve formulářích a textu nápovědy", + "Username": "Uživatelské jméno", + "Username for SOLR authentication (optional)": "Uživatelské jméno pro ověření SOLR (volitelné)", + "Users": "Uživatelé", + "Uses": "Použití", + "Using Pre-Generated Chunks": "Používání předem vygenerovaných úseků", + "Validate": "Ověřit", + "Validate Objects": "Ověřit objekty", + "Validation completed for {schema}": "Ověření dokončeno pro {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Ověření dokončeno pro {schema}: {processed} zpracováno, {updated} aktualizováno, {failed} selhalo", + "Validation completed successfully": "Ověření bylo úspěšně dokončeno", + "Validation failed": "Ověření selhalo", + "Value": "Hodnota", + "Value list (split on ,)": "Seznam hodnot (rozdělené čárkou)", + "Vector Dimensions": "Rozměry vektoru", + "Vector Search Backend": "Backend vektorového vyhledávání", + "Vector field: _embedding_": "Pole vektoru: _embedding_", + "Vectorization Triggers": "Spouštěče vektorizace", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorizace dokončena: {vectorized} objektů vektorizováno, {failed} selhalo", + "Vectorization failed: {error}": "Vektorizace selhala: {error}", + "Vectorize All Files": "Vektorizovat všechny soubory", + "Vectorize All Objects": "Vektorizovat všechny objekty", + "Vectorize all file types": "Vektorizovat všechny typy souborů", + "Vectorize all views": "Vektorizovat všechny pohledy", + "Vectorize on object creation": "Vektorizovat při vytvoření objektu", + "Vectors will be stored in your existing object and file collections": "Vektory budou uloženy ve vašich stávajících kolekcích objektů a souborů", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Ověřování konektivity serveru...", + "Version": "Verze", + "Version Information": "Informace o verzi", + "Very High": "Velmi vysoká", + "Very high": "Velmi vysoká", + "View": "Pohled", + "View API Docs": "Zobrazit dokumentaci API", + "View API Documentation": "Zobrazit dokumentaci API", + "View APIs": "Zobrazit API", + "View Changes": "Zobrazit změny", + "View Details": "Zobrazit detaily", + "View Error": "Zobrazit chybu", + "View File": "Zobrazit soubor", + "View Full Details": "Zobrazit všechny detaily", + "View Logs": "Zobrazit protokoly", + "View Name": "Název pohledu", + "View Object": "Zobrazit objekt", + "View Parameters": "Parametry pohledu", + "View Selection": "Výběr pohledu", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Zobrazit a analyzovat protokoly stop vyhledávání s pokročilými možnostmi filtrování a analytiky", + "View and analyze system audit trails with advanced filtering capabilities": "Zobrazit a analyzovat systémové auditní stopy s pokročilými možnostmi filtrování", + "View and manage authorization across registers and schemas": "Zobrazit a spravovat autorizaci napříč registry a schématy", + "View deleted successfully": "Pohled byl úspěšně smazán", + "View entity details and manage relations": "Zobrazit detaily entity a spravovat vztahy", + "View name": "Název pohledu", + "View name is required": "Název pohledu je povinný", + "View object": "Zobrazit objekt", + "View saved successfully!": "Pohled byl úspěšně uložen!", + "View search analytics and manage search logs": "Zobrazit analytiku vyhledávání a spravovat protokoly vyhledávání", + "View successfully deleted": "Pohled byl úspěšně smazán", + "View successfully updated": "Pohled byl úspěšně aktualizován", + "View updated successfully!": "Pohled byl úspěšně aktualizován!", + "View webhook delivery logs and filter by webhook": "Zobrazit protokoly doručení webhooků a filtrovat podle webhooku", + "View {count} more": "Zobrazit dalších {count}", + "Views": "Pohledy", + "Visible to end users": "Viditelné koncovým uživatelům", + "Visible to users": "Viditelné uživatelům", + "Wait for Response": "Čekat na odpověď", + "Wait for webhook response before continuing (required for request/response flows)": "Před pokračováním počkat na odpověď webhooku (vyžadováno pro toky požadavek/odpověď)", + "Waiting for cluster synchronization...": "Čekání na synchronizaci clusteru...", + "Warmup Names Cache": "Zahřát mezipaměť názvů", + "Warmup failed": "Zahřívání selhalo", + "Warning": "Varování", + "Warning:": "Varování:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Varování: Tato vlastnost má nekonzistentní typy: {types}. Zvažte, zda by všechny objekty měly mít stejný typ.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Prohledáme všechny objekty patřící tomuto schématu, abychom objevili nové vlastnosti a analyzovali stávající vlastnosti pro potenciální vylepšení. Proces zahrnuje zkoumání datové struktury každého objektu, identifikaci vlastností nedefinovaných v aktuálním schématu a hledání příležitostí ke zlepšení stávajících definic vlastností pomocí lepších omezení, formátů a ověřovacích pravidel.", + "Webhook": "Webhook", + "Webhook Log Details": "Detaily protokolu webhooku", + "Webhook Logs": "Protokoly webhooku", + "Webhook created successfully": "Webhook byl úspěšně vytvořen", + "Webhook deleted": "Webhook smazán", + "Webhook retry delivered successfully": "Opakování webhooku bylo úspěšně doručeno", + "Webhook retry delivery failed": "Doručení opakování webhooku selhalo", + "Webhook updated": "Webhook aktualizován", + "Webhook updated successfully": "Webhook byl úspěšně aktualizován", + "Webhooks": "Webhooky", + "Weekly": "Týdně", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Když agent běží bez uživatelské relace (např. naplánované úlohy), použije se kontext tohoto uživatele", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Když agent běží bez uživatelské relace (např. naplánované úlohy), použije se kontext tohoto uživatele.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Když je zakázáno, tato faseta zobrazí pouze hodnoty z tohoto schématu a při výběru zahrne filtr schématu.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Když je povoleno, uložení tohoto objektu také aktualizuje cílové objekty tak, aby zahrnovaly zpětný odkaz na tento objekt.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Když je povoleno, tato vlastnost bude po aktualizaci cílových objektů odstraněna ze zdrojového objektu.", + "Workflow Management": "Správa pracovních postupů", + "Workflows": "Pracovní postupy", + "Wrap webhook payload in CloudEvents format for better interoperability": "Zabalit užitečné zatížení webhooku do formátu CloudEvents pro lepší interoperabilitu", + "Wrap webhook payload in cloudevents format for better interoperability": "Zabalit užitečné zatížení webhooku do formátu cloudevents pro lepší interoperabilitu", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Ano", + "You": "Vy", + "You are not yet a member of any organisations.": "Zatím nejste členem žádné organizace.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Můžete vytvářet nové sady ConfigSet na základě šablony _default nebo nahrát vlastní přímo na váš server SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Můžete vytvářet nové sady configset na základě šablony _default nebo nahrát vlastní přímo na váš server SOLR.", + "You do not have access to this conversation": "K této konverzaci nemáte přístup", + "You do not have permission to delete this conversation": "Nemáte oprávnění smazat tuto konverzaci", + "You do not have permission to modify this conversation": "Nemáte oprávnění upravit tuto konverzaci", + "You do not have permission to restore this conversation": "Nemáte oprávnění obnovit tuto konverzaci", + "You do not have permission to view the permission matrix. Admin access is required.": "Nemáte oprávnění zobrazit matici oprávnění. Je vyžadován přístup správce.", + "You have selected {count} properties to add to the schema.": "Vybrali jste {count} vlastností k přidání do schématu.", + "You must be logged in to favorite views": "Pro označení pohledů jako oblíbených musíte být přihlášeni", + "You need an AI agent to start a conversation.": "K zahájení konverzace potřebujete AI agenta.", + "Your Fireworks AI API key. Get one at": "Váš API klíč Fireworks AI. Získejte jej na", + "Your OpenAI API key. Get one at": "Váš API klíč OpenAI. Získejte jej na", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Vaše zpětná vazba byla zaznamenána. Volitelně zde můžete poskytnout další detaily...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Vaše zpětná vazba byla zaznamenána. volitelně zde můžete poskytnout další detaily...", + "Zookeeper Hosts": "Hostitelé Zookeeper", + "Zookeeper Port": "Port Zookeeper", + "Zookeeper Settings (SolrCloud)": "Nastavení Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Připojovací řetězec Zookeeper pro SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Číslo portu Zookeeper (volitelné, výchozí je 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F VAROVÁNÍ O TRVALÉM SMAZÁNÍ \\u26A0\\uFE0F\n\nChystáte se TRVALE smazat VŠECHNY objekty pro schéma \"{schema}\":\n\n\\u2022 Aktivní objekty: {active}\n\\u2022 Dočasně smazané objekty: {deleted}\n\\u2022 Celkem: {total}\n\nTyto objekty budou zcela odstraněny z databáze a NELZE je obnovit.\n\nJste si naprosto jisti?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F VAROVÁNÍ O TRVALÉM SMAZÁNÍ \\u26A0\\uFE0F\n\nChystáte se TRVALE smazat {count} dočasně smazaných objektů pro schéma \"{schema}\".\n\nTyto objekty budou zcela odstraněny z databáze a NELZE je obnovit.\n\nJste si naprosto jisti?", + "and accessible to all users in selected groups (or all users if no groups selected).": "a přístupné všem uživatelům ve vybraných skupinách (nebo všem uživatelům, pokud nejsou vybrány žádné skupiny).", + "and add the files there.": "a přidejte soubory tam.", + "approved": "schváleno", + "are only accessible to invited users.": "jsou přístupné pouze pozvaným uživatelům.", + "by": "od", + "chunk": "úsek", + "chunks": "úseky", + "chunks processed": "úseků zpracováno", + "completed": "dokončeno", + "configuration(s)": "konfigurace", + "conversationId is required": "conversationId je povinné", + "create": "vytvořit", + "delete": "smazat", + "deleted objects section": "sekce smazaných objektů", + "e.g., lib/Settings/config.json": "např. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "např. lib/Settings/register.json", + "e.g., lib/settings/config.json": "např. lib/settings/config.json", + "e.g., lib/settings/register.json": "např. lib/settings/register.json", + "execute": "provést", + "failed": "selhalo", + "file": "soubor", + "files": "soubory", + "file{plural}": "soubor{plural}", + "folder": "složka", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "v {register}", + "in_progress": "probíhá", + "inherited": "zděděno", + "list": "seznam", + "log{plural}": "protokol{plural}", + "manage": "spravovat", + "members": "členů", + "menu or contact someone with permission to create agents.": "nabídce nebo kontaktujte někoho s oprávněním vytvářet agenty.", + "message content is required": "obsah zprávy je povinný", + "messages": "zprávy", + "missing": "chybí", + "myapp": "myapp", + "n8n URL and API key are required": "URL n8n a API klíč jsou povinné", + "n8n configuration saved successfully": "Konfigurace n8n byla úspěšně uložena", + "n8n connection not configured": "Připojení n8n není nakonfigurováno", + "n8n connection successful": "Připojení n8n úspěšné", + "n8n connection test failed: {message}": "Test připojení n8n selhal: {message}", + "n8n connection test successful": "Test připojení n8n úspěšný", + "n8n initialization failed: {message}": "Inicializace n8n selhala: {message}", + "n8n integration disabled": "Integrace n8n zakázána", + "n8n integration enabled": "Integrace n8n povolena", + "n8n project initialized successfully": "Projekt n8n byl úspěšně inicializován", + "n8n settings saved successfully": "Nastavení n8n byla úspěšně uložena", + "notify_push configuration guide": "průvodce konfigurací notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push je nainstalováno, ale OpenRegister zatím nepotvrdil úspěšný push. Aktivujte spuštěním uložení objektu nebo zkontrolujte konfiguraci notify_push.", + "notify_push is installed but not yet active": "notify_push je nainstalováno, ale zatím není aktivní", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekty", + "objects processed": "objektů zpracováno", + "object{plural}": "objekt{plural}", + "or pick a range": "nebo vyberte rozsah", + "org-...": "org-...", + "pending": "čeká", + "provides a good balance between speed and accuracy.": "poskytuje dobrou rovnováhu mezi rychlostí a přesností.", + "public": "veřejné", + "read": "číst", + "register(s)": "registr(y)", + "register(s) selected": "registr(ů) vybráno", + "register{plural}": "registr{plural}", + "rejected": "odmítnuto", + "required": "povinné", + "results": "výsledky", + "schema(s)": "schéma(ta)", + "schema(s) selected": "schémat vybráno", + "schema{plural}": "schéma{plural}", + "selected objects": "vybrané objekty", + "set": "nastavit", + "sk-...": "sk-...", + "this application": "tato aplikace", + "to": "do", + "type must be \"positive\" or \"negative\"": "typ musí být \"positive\" nebo \"negative\"", + "update": "aktualizovat", + "vectorized": "vektorizováno", + "view": "pohled", + "views selected": "pohledů vybráno", + "waiting": "čeká", + "{active} active, {deleted} deleted": "{active} aktivních, {deleted} smazaných", + "{count} audit trails cleared successfully": "{count} auditních stop bylo úspěšně vymazáno", + "{count} compliance issue(s) detected": "Detekováno {count} problémů se shodou", + "{count} deletions": "{count} smazání", + "{count} documents found": "Nalezeno {count} dokumentů", + "{count} email": "{count} e-mail", + "{count} emails": "{count} e-mailů", + "{count} entries": "{count} položek", + "{count} issues": "{count} problémů", + "{count} members": "{count} členů", + "{count} searches": "{count} vyhledávání", + "{count} selected": "{count} vybráno", + "{count} widget(s)": "{count} widgetů", + "{matched} object(s) matched. {erased} erased.": "Odpovídá {matched} objektů. Vymazáno {erased}.", + "{minutes} minutes ago": "před {minutes} minutami", + "{percentage}% of objects": "{percentage} % objektů", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} v {register} / {schema}", + "{type} cache cleared successfully": "Mezipaměť {type} byla úspěšně vymazána", + "ℹ️ Current Configuration": "ℹ️ Aktuální konfigurace", + "ℹ️ current configuration": "ℹ️ aktuální konfigurace", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ NEBEZPEČÍ: Všech {total} objektů bude TRVALE SMAZÁNO z databáze. Tato akce je NEVRATNÁ a nelze ji vrátit zpět!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ VAROVÁNÍ O TRVALÉM SMAZÁNÍ ⚠️\n\nChystáte se TRVALE smazat VŠECHNY objekty pro schéma \"{schema}\":\n\n• Aktivní objekty: {active}\n• Dočasně smazané objekty: {deleted}\n• Celkem: {total}\n\nTyto objekty budou zcela odstraněny z databáze a NELZE je obnovit.\n\nJste si naprosto jisti?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ VAROVÁNÍ O TRVALÉM SMAZÁNÍ ⚠️\n\nChystáte se TRVALE smazat {count} dočasně smazaných objektů pro schéma \"{schema}\".\n\nTyto objekty budou zcela odstraněny z databáze a NELZE je obnovit.\n\nJste si naprosto jisti?", + "⚡ Batch Processing": "⚡ Dávkové zpracování", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR zvládne miliony dokumentů s dobou odezvy vyhledávání pod jednu sekundu...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Komplexní kontext ✓ Menší pravděpodobnost vynechání detailů ✗ Pomalejší odpovědi ✗ Může zahrnovat méně relevantní informace", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Rychlejší odpovědi ✓ Cílenější odpovědi ✗ Může vynechat relevantní informace", + "✨ AI Features": "✨ Funkce AI", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 V distribuovaném režimu používá SOLR ZooKeeper pro koordinaci clusteru...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasetové vyhledávání umožňuje uživatelům podrobně procházet výsledky podle kategorií...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Věděli jste? SOLR pohání vyhledávání pro Netflix, Apple a mnoho dalších velkých webů!", + "💰 View Selection (Cost Optimization)": "💰 Výběr pohledů (optimalizace nákladů)", + "📄 Object Serialization": "📄 Serializace objektů", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR poskytuje bohatou analytiku a statistiky o výkonu vyhledávání...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Sady ConfigSet obsahují schéma a konfigurační soubory pro váš vyhledávací index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikace zajišťuje dostupnost vašeho vyhledávacího indexu i v případě selhání uzlů...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR je výkonná podniková vyhledávací platforma postavená na Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Nastavení vektorizace", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automatické škálování může dynamicky přidávat nebo odebírat uzly podle zatížení...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Funkce zabezpečení zahrnují ověřování, autorizaci a šifrování SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-mailů" + ], + "Purge {count} object from database": [ + "Vyčistit {count} objekt z databáze", + "Vyčistit {count} objektů z databáze" + ], + "Restore {count} object": [ + "Obnovit {count} objekt", + "Obnovit {count} objektů" + ], + "Successfully restored {count} object": [ + "Úspěšně obnoven {count} objekt", + "Úspěšně obnoveno {count} objektů" + ], + "Delete {count} object": [ + "Smazat {count} objekt", + "Smazat {count} objektů" + ], + "Object successfully deleted": [ + "Objekt byl úspěšně smazán", + "Objekty byly úspěšně smazány" + ] + } +} diff --git a/l10n/da.js b/l10n/da.js new file mode 100644 index 0000000000..3e36c3ef95 --- /dev/null +++ b/l10n/da.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(intet emne)", + "(no title)" : "(ingen titel)", + "(unnamed)" : "(uden navn)", + "(untitled card)" : "(kort uden titel)", + "Activities" : "Aktiviteter", + "Add Endpoint" : "Tilføj endpoint", + "Add Source" : "Tilføj kilde", + "Add card" : "Tilføj kort", + "Add contact" : "Tilføj kontakt", + "All clear" : "Alt i orden", + "Articles" : "Artikler", + "Code" : "Kode", + "Compliance" : "Overholdelse", + "Contacts" : "Kontakter", + "Create Agent" : "Opret agent", + "Create Application" : "Opret applikation", + "Create event" : "Opret begivenhed", + "Erasure complete" : "Sletning fuldført", + "Erasure preview" : "Forhåndsvisning af sletning", + "Expand details" : "Udvid detaljer", + "Save changes" : "Gem ændringer", + "📄 Object Serialization" : "📄 Objektserialisering", + "🔢 Vectorization Settings" : "🔢 Vektoriseringsindstillinger", + "💰 View Selection (Cost Optimization)" : "💰 Visningsvalg (omkostningsoptimering)", + "AI Assistant" : "AI-assistent", + "AI Chat" : "AI-chat", + "API Key" : "API-nøgle", + "API tokens saved successfully" : "API-tokens gemt", + "About ConfigSets" : "Om ConfigSets", + "Action" : "Handling", + "Action Distribution" : "Handlingsfordeling", + "Action:" : "Handling:", + "Actions" : "Handlinger", + "Active" : "Aktiv", + "Active Collections" : "Aktive samlinger", + "Active Filters" : "Aktive filtre", + "Active Organisations" : "Aktive organisationer", + "Active filters:" : "Aktive filtre:", + "Active organisation set successfully" : "Aktiv organisation angivet", + "Activity Period" : "Aktivitetsperiode", + "Add Groups" : "Tilføj grupper", + "Add Schema" : "Tilføj skema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Tilføj skematitler, beskrivelser og registeroplysninger for at give en rigere kontekst til søgning", + "Add to favorites" : "Tilføj til favoritter", + "Added" : "Tilføjet", + "Added to favorites" : "Tilføjet til favoritter", + "Additional Information" : "Yderligere oplysninger", + "Additional feedback saved. Thank you!" : "Yderligere feedback gemt. Tak!", + "Admin" : "Administrator", + "Advanced" : "Avanceret", + "Advanced Filters" : "Avancerede filtre", + "Advanced Options" : "Avancerede indstillinger", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Avancerede filtre er ikke tilgængelige, når der bruges databasekilde. Skift til Auto eller SOLR-indeks for filtreringsmuligheder.", + "After" : "Efter", + "Agent deleted successfully" : "Agent slettet", + "Agents" : "Agenter", + "All" : "Alle", + "All Categories" : "Alle kategorier", + "All Collections" : "Alle samlinger", + "All Confidence Levels" : "Alle konfidensniveauer", + "All ConfigSets" : "Alle ConfigSets", + "All Files" : "Alle filer", + "All Levels" : "Alle niveauer", + "All Types" : "Alle typer", + "All Webhooks" : "Alle webhooks", + "All actions" : "Alle handlinger", + "All audit trails cleared successfully" : "Alle revisionsspor ryddet", + "All registers" : "Alle registre", + "All schemas" : "Alle skemaer", + "All searches" : "Alle søgninger", + "All users" : "Alle brugere", + "An error occurred while clearing audit trails" : "Der opstod en fejl under rydning af revisionsspor", + "An error occurred while deleting the audit trail" : "Der opstod en fejl under sletning af revisionssporet", + "An error occurred while deleting the view" : "Der opstod en fejl under sletning af visningen", + "An error occurred while permanently deleting the objects" : "Der opstod en fejl under permanent sletning af objekterne", + "Analysis completed:" : "Analyse fuldført:", + "Analysis steps:" : "Analysetrin:", + "Analytics" : "Analyser", + "Analyze Objects" : "Analyser objekter", + "Analyze Properties" : "Analyser egenskaber", + "Analyze existing properties for improvement opportunities" : "Analyser eksisterende egenskaber for forbedringsmuligheder", + "Analyzing..." : "Analyserer...", + "Any favorites and sharing settings for this view" : "Eventuelle favoritter og delingsindstillinger for denne visning", + "Any user" : "Enhver bruger", + "App store cache invalidated successfully" : "App store-cache ugyldiggjort", + "Application deleted successfully" : "Applikation slettet", + "Applications" : "Applikationer", + "Apply Changes" : "Anvend ændringer", + "Archive" : "Arkivér", + "Archive conversation" : "Arkivér samtale", + "Archived conversations are hidden from your active list" : "Arkiverede samtaler er skjult fra din aktive liste", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Er du sikker på, at du vil rydde gamle søgespor op? Dette sletter poster, der er ældre end 30 dage.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Er du sikker på, at du vil slette de valgte revisionsspor? Denne handling kan ikke fortrydes.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Er du sikker på, at du vil slette de valgte søgespor? Denne handling kan ikke fortrydes.", + "Are you sure you want to delete this ConfigSet?" : "Er du sikker på, at du vil slette dette ConfigSet?", + "Are you sure you want to permanently delete" : "Er du sikker på, at du vil slette permanent", + "Ask a question..." : "Stil et spørgsmål...", + "Ask questions about your data using natural language" : "Stil spørgsmål om dine data ved hjælp af naturligt sprog", + "Attempt" : "Forsøg", + "Audit Trail Changes" : "Ændringer i revisionsspor", + "Audit Trail Details" : "Detaljer for revisionsspor", + "Audit Trail Management" : "Administration af revisionsspor", + "Audit Trail Statistics" : "Statistik for revisionsspor", + "Audit Trails" : "Revisionsspor", + "Audit trail data copied to clipboard" : "Revisionssporsdata kopieret til udklipsholderen", + "Audit trail deleted successfully" : "Revisionsspor slettet", + "Audit trail successfully deleted" : "Revisionsspor slettet", + "Authentication" : "Godkendelse", + "Auto-commit disabled" : "Auto-commit deaktiveret", + "Auto-commit enabled" : "Auto-commit aktiveret", + "Auto-create Default Organisation" : "Auto-opret standardorganisation", + "Auto-retry failed vectorizations" : "Forsøg automatisk mislykkede vektoriseringer igen", + "Automatically commit changes to SOLR index" : "Bekræft automatisk ændringer til SOLR-indeks", + "Automatically create a default organisation if none exists when the app is initialized" : "Opret automatisk en standardorganisation, hvis ingen findes, når appen initialiseres", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Generér automatisk vektorindlejringer fra tekstbidder, når filer uploades og behandles", + "Automatically generate vector embeddings when objects are created or updated" : "Generér automatisk vektorindlejringer, når objekter oprettes eller opdateres", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Forsøg automatisk mislykkede vektoriseringsforsøg igen (maks. 3 forsøg)", + "Available Filters" : "Tilgængelige filtre", + "Available Workflows" : "Tilgængelige arbejdsgange", + "Avg Execution Time" : "Gns. udførelsestid", + "Avg Members/Org" : "Gns. medlemmer/org.", + "Avg Object Views/Session" : "Gns. objektvisninger/session", + "Avg Response Time" : "Gns. svartid", + "Avg Results/Search" : "Gns. resultater/søgning", + "Avg Searches/Session" : "Gns. søgninger/session", + "Back" : "Tilbage", + "Back to Entities" : "Tilbage til entiteter", + "Back to Registers" : "Tilbage til registre", + "Back to Webhooks" : "Tilbage til webhooks", + "Back to applications" : "Tilbage til applikationer", + "Back to entities" : "Tilbage til entiteter", + "Backend updated successfully. Please reload the application." : "Backend opdateret. Genindlæs venligst applikationen.", + "Base URL (Optional)" : "Basis-URL (valgfri)", + "Basic Information" : "Grundlæggende oplysninger", + "Batch Size" : "Batchstørrelse", + "Batch extraction completed" : "Batchudtrækning fuldført", + "Before" : "Før", + "Before object vectorization can work:" : "Før objektvektorisering kan fungere:", + "Behavior Issue" : "Adfærdsproblem", + "Bookmarks" : "Bogmærker", + "Blob storage has been retired. All objects now use magic tables." : "Blob-lagring er udfaset. Alle objekter bruger nu magiske tabeller.", + "Branch" : "Gren", + "Bulk delete operation completed successfully" : "Massesletningshandling fuldført", + "Bulk save operation completed successfully" : "Massegemningshandling fuldført", + "Business Data" : "Forretningsdata", + "Cache cleared successfully" : "Cache ryddet", + "Calculate Sizes" : "Beregn størrelser", + "Cancel" : "Annullér", + "Cannot delete: objects are still attached" : "Kan ikke slette: objekter er stadig tilknyttet", + "Category" : "Kategori", + "Change Type" : "Ændringstype", + "Changes" : "Ændringer", + "Changes copied to clipboard" : "Ændringer kopieret til udklipsholderen", + "Chat Model" : "Chatmodel", + "Chat Provider" : "Chatudbyder", + "Chat Provider (RAG)" : "Chatudbyder (RAG)", + "Chat Settings" : "Chatindstillinger", + "Chat provider connection successful!" : "Forbindelse til chatudbyder lykkedes!", + "Chat settings" : "Chatindstillinger", + "Choose a register" : "Vælg et register", + "Choose a schema" : "Vælg et skema", + "Choose how vector similarity calculations are performed for semantic search" : "Vælg, hvordan beregninger af vektorlighed udføres til semantisk søgning", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Vælg, hvilke filtyper der skal inkluderes i vektoriseringsprocessen. Kun filer med udtrukket tekst og bidder behandles.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Vælg, hvilke visninger der skal inkluderes i vektoriseringsprocessen. Lad feltet stå tomt for at behandle alle visninger baseret på din konfiguration.", + "Chunk" : "Bidde", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Sletning af bidder er endnu ikke implementeret. Brug bidde-baserede endpoints.", + "Chunks" : "Bidder", + "Chunks to Vectorize:" : "Bidder til vektorisering:", + "Cleanup Old Trails" : "Ryd gamle spor op", + "Cleanup completed" : "Oprydning fuldført", + "Clear All Embeddings" : "Ryd alle indlejringer", + "Clear Entries" : "Ryd poster", + "Clear Filtered Audit Trails" : "Ryd filtrerede revisionsspor", + "Clear Filters" : "Ryd filtre", + "Clear Index" : "Ryd indeks", + "Clear Selection" : "Ryd valg", + "Clear all" : "Ryd alle", + "Clear all filters" : "Ryd alle filtre", + "Clear date range" : "Ryd datointerval", + "Clearing..." : "Rydder...", + "Close" : "Luk", + "Collection Management" : "Administration af samlinger", + "Collection Name" : "Samlingsnavn", + "Collection assignments updated successfully" : "Samlingstildelinger opdateret", + "Collection cleared successfully" : "Samling ryddet", + "Collection copied successfully" : "Samling kopieret", + "Collection created successfully" : "Samling oprettet", + "Collection deleted successfully" : "Samling slettet", + "Collection used to store and index file metadata and content" : "Samling brugt til at lagre og indeksere filmetadata og -indhold", + "Collection used to store and index object data" : "Samling brugt til at lagre og indeksere objektdata", + "Collections:" : "Samlinger:", + "Commit Message" : "Commit-besked", + "Commit Within (ms)" : "Commit inden for (ms)", + "Commit message" : "Commit-besked", + "Compare current schema with real object data" : "Sammenlign nuværende skema med rigtige objektdata", + "Completed" : "Fuldført", + "Complex" : "Kompleks", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Komplekse forespørgsler: Avancerede søgninger med flere filtre, operatorer og komplekse parameterkombinationer", + "Confidence Level" : "Konfidensniveau", + "Confidence Score" : "Konfidensscore", + "Config must be provided as an object" : "Konfiguration skal angives som et objekt", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Administration af ConfigSet", + "ConfigSet Name" : "ConfigSet-navn", + "ConfigSet created successfully" : "ConfigSet oprettet", + "ConfigSet deleted successfully" : "ConfigSet slettet", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets definerer skemaet og konfigurationen for dine SOLR-samlinger. De indeholder feltdefinitioner, analysatorer og andre søgeindstillinger.", + "Configuration" : "Konfiguration", + "Configuration Data" : "Konfigurationsdata", + "Configuration saved" : "Konfiguration gemt", + "Configurations" : "Konfigurationer", + "Configure Facets" : "Konfigurér facetter", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurér udbydere af store sprogmodeller (LLM) til AI-drevne funktioner, herunder semantisk søgning, indlejringer og chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurér grundlæggende forbindelsesindstillinger for din SOLR-server, herunder godkendelse og netværksindstillinger. Brug de separate dialoger til administration af ConfigSet og samlinger til at administrere kerner og samlinger.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurér, hvordan databaseobjekter konverteres til vektorindlejringer til semantisk søgning. Objekter vektoriseres direkte uden behov for tekstudtrækning.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurér, hvordan objekter konverteres til tekst før vektorisering. Disse indstillinger påvirker søgekvalitet og kontekst.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurér parametre for filvektorisering. Denne proces genererer vektorindlejringer for alle udtrukne filbidder.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurér parametre for objektvektorisering. Denne proces genererer vektorindlejringer for alle objekter, der matcher dine visningsfiltre.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurér, hvilke datatyper der skal søges i, og hvor mange kilder der skal hentes", + "Connection Failed" : "Forbindelse mislykkedes", + "Connection Settings" : "Forbindelsesindstillinger", + "Connection Successful!" : "Forbindelse lykkedes!", + "Connection failed" : "Forbindelse mislykkedes", + "Connection protocol" : "Forbindelsesprotokol", + "Connection successful" : "Forbindelse lykkedes", + "Connection timeout in seconds" : "Forbindelsestimeout i sekunder", + "Constant delay between retries (5 minutes)" : "Konstant forsinkelse mellem forsøg (5 minutter)", + "Constraint Issue" : "Begrænsningsproblem", + "Content Filters" : "Indholdsfiltre", + "Continue to Properties" : "Fortsæt til egenskaber", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontrollér, hvilke objektvisninger der skal vektoriseres for at reducere API-omkostninger. Vektorisér kun visninger, der drager fordel af semantisk søgning.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontrollér, hvilke visninger og værktøjer AI'en kan bruge i denne samtale. Som standard er alle agentfunktioner aktiveret.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Styrer tilfældighed (0 = deterministisk, 2 = meget kreativ)", + "Conversation ID is required" : "Samtale-ID er påkrævet", + "Conversation archived" : "Samtale arkiveret", + "Conversation archived successfully" : "Samtale arkiveret", + "Conversation cleared successfully" : "Samtale ryddet", + "Conversation deleted" : "Samtale slettet", + "Conversation permanently deleted" : "Samtale slettet permanent", + "Conversation renamed" : "Samtale omdøbt", + "Conversation restored" : "Samtale gendannet", + "Conversation title" : "Samtaletitel", + "Copied!" : "Kopieret!", + "Copy" : "Kopiér", + "Copy Changes" : "Kopiér ændringer", + "Copy Collection" : "Kopiér samling", + "Copy Data" : "Kopiér data", + "Copy Full Data" : "Kopiér fulde data", + "Copying..." : "Kopierer...", + "Count" : "Antal", + "Counting objects..." : "Tæller objekter...", + "Costs" : "Omkostninger", + "Create" : "Opret", + "Create Collection" : "Opret samling", + "Create ConfigSet" : "Opret ConfigSet", + "Create New Collection" : "Opret ny samling", + "Create New ConfigSet" : "Opret nyt ConfigSet", + "Create Operations" : "Opret-handlinger", + "Create Webhook" : "Opret webhook", + "Create a copy of collection:" : "Opret en kopi af samling:", + "Create a new ConfigSet based on the _default template" : "Opret et nyt ConfigSet baseret på _default-skabelonen", + "Create a new SOLR collection from an existing ConfigSet" : "Opret en ny SOLR-samling fra et eksisterende ConfigSet", + "Create your first AI agent to get started." : "Opret din første AI-agent for at komme i gang.", + "Created" : "Oprettet", + "Created:" : "Oprettet:", + "Creating..." : "Opretter...", + "Current Filters" : "Nuværende filtre", + "Current Type:" : "Nuværende type:", + "Custom API endpoint if using a different region" : "Brugerdefineret API-endpoint, hvis du bruger en anden region", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Brugerdefinerede HTTP-headere (én pr. linje, format: Header-Navn: værdi)", + "Daily" : "Dagligt", + "Dashboard" : "Oversigt", + "Data Source" : "Datakilde", + "Data sources" : "Datakilder", + "Data type does not match observed values" : "Datatype matcher ikke observerede værdier", + "Database URL" : "Database-URL", + "Database information refreshed" : "Databaseoplysninger opdateret", + "Date Range" : "Datointerval", + "Default" : "Standard", + "Default Organisation" : "Standardorganisation", + "Default: 5" : "Standard: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Forsinkelser fordobles ved hvert forsøg (2, 4, 8 minutter...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Forsinkelser øges lineært (5, 10, 15 minutter...)", + "Delete" : "Slet", + "Delete Application" : "Slet applikation", + "Delete Audit Trail" : "Slet revisionsspor", + "Delete Collection" : "Slet samling", + "Delete ConfigSet" : "Slet ConfigSet", + "Delete Objects" : "Slet objekter", + "Delete Operations" : "Slet-handlinger", + "Delete View" : "Slet visning", + "Delete all objects in this schema" : "Slet alle objekter i dette skema", + "Delete permanently" : "Slet permanent", + "Delete this conversation?" : "Slet denne samtale?", + "Delete view" : "Slet visning", + "Deleted" : "Slettet", + "Deleted By" : "Slettet af", + "Deleted Date" : "Slettedato", + "Deleted Items Management" : "Administration af slettede elementer", + "Deleted This Week" : "Slettet denne uge", + "Deleted Today" : "Slettet i dag", + "Deleted:" : "Slettet:", + "Deleting..." : "Sletter...", + "Deletion Date Range" : "Datointerval for sletning", + "Deletion Statistics" : "Slettestatistik", + "Deprecated" : "Forældet", + "Description" : "Beskrivelse", + "Description:" : "Beskrivelse:", + "Detect data types and patterns" : "Registrér datatyper og mønstre", + "Detected At" : "Registreret den", + "Detected Format:" : "Registreret format:", + "Detected Issues:" : "Registrerede problemer:", + "Disable" : "Deaktivér", + "Disabled" : "Deaktiveret", + "Discover Files" : "Find filer", + "Discovered Properties" : "Fundne egenskaber", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Vil du slette alle filtrerede revisionssporsposter permanent? Denne handling kan ikke fortrydes.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Vil du slette denne revisionssporspost permanent? Denne handling kan ikke fortrydes.", + "Documents" : "Dokumenter", + "Download API Spec" : "Download API-specifikation", + "Edit" : "Redigér", + "Edit Register" : "Redigér register", + "Edit Schema" : "Redigér skema", + "Edit View" : "Redigér visning", + "Edit Webhook" : "Redigér webhook", + "Edit view" : "Redigér visning", + "Edit view details" : "Redigér visningsdetaljer", + "Effectiveness" : "Effektivitet", + "Email" : "E-mail", + "Emails" : "E-mails", + "Embedding Model" : "Indlejringsmodel", + "Embedding Provider" : "Indlejringsudbyder", + "Embedding generated successfully" : "Indlejring genereret", + "Embedding provider connection successful!" : "Forbindelse til indlejringsudbyder lykkedes!", + "Enable" : "Aktivér", + "Enable auto-creation" : "Aktivér automatisk oprettelse", + "Enable automatic file vectorization" : "Aktivér automatisk filvektorisering", + "Enable automatic object vectorization" : "Aktivér automatisk objektvektorisering", + "Enable automatic synchronization" : "Aktivér automatisk synkronisering", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Aktivér detaljeret logning for SOLR-handlinger (anbefales til fejlfinding)", + "Enable faceting" : "Aktivér facettering", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Aktivér eller deaktivér LLM-funktioner. Konfigurér udbydere og modeller via knappen LLM-konfiguration ovenfor.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Aktivér eller deaktivér integration af SOLR-søgning. Konfigurér forbindelsesindstillinger via knappen Forbindelsesindstillinger ovenfor.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Aktivér eller deaktivér integration af n8n-arbejdsgange. Konfigurér forbindelsesindstillinger nedenfor.", + "Enable or disable this webhook" : "Aktivér eller deaktivér denne webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Aktivér vektorisering for alle eksisterende og fremtidige visninger (kan øge omkostningerne)", + "Enabled" : "Aktiveret", + "Endpoints" : "Endpoints", + "Enter ConfigSet name" : "Indtast ConfigSet-navn", + "Enter collection name" : "Indtast samlingsnavn", + "Enter description (optional)..." : "Indtast beskrivelse (valgfri)...", + "Enter new collection name" : "Indtast nyt samlingsnavn", + "Enter object ID" : "Indtast objekt-ID", + "Enter search term" : "Indtast søgeord", + "Enter view name..." : "Indtast visningsnavn...", + "Enter webhook name" : "Indtast webhook-navn", + "Entities" : "Entiteter", + "Entity Information" : "Entitetsoplysninger", + "Entity deleted successfully" : "Entitet slettet", + "Entity not found" : "Entitet ikke fundet", + "Entries to be deleted:" : "Poster, der skal slettes:", + "Enum Issue" : "Enum-problem", + "Enum constraint is missing" : "Enum-begrænsning mangler", + "Error" : "Fejl", + "Error Details" : "Fejldetaljer", + "Error Information" : "Fejloplysninger", + "Error Message" : "Fejlmeddelelse", + "Error loading application" : "Fejl ved indlæsning af applikation", + "Error loading audit trails" : "Fejl ved indlæsning af revisionsspor", + "Error loading entity" : "Fejl ved indlæsning af entitet", + "Error loading search trails" : "Fejl ved indlæsning af søgespor", + "Estimated Batches:" : "Anslåede batches:", + "Estimated Cost:" : "Anslåede omkostninger:", + "Estimated Duration:" : "Anslået varighed:", + "Event" : "Begivenhed", + "Event Property for Payload" : "Begivenhedsegenskab til nyttelast", + "Events" : "Begivenheder", + "Example Value" : "Eksempelværdi", + "Exclusive Maximum" : "Eksklusivt maksimum", + "Exclusive Minimum" : "Eksklusivt minimum", + "Execution Mode" : "Udførelsestilstand", + "Execution Time" : "Udførelsestid", + "Execution Time Range" : "Interval for udførelsestid", + "Existing Improvements" : "Eksisterende forbedringer", + "Exponential" : "Eksponentiel", + "Export" : "Eksportér", + "Export completed successfully" : "Eksport fuldført", + "Export, view, or delete audit trails" : "Eksportér, vis eller slet revisionsspor", + "Extend Schema" : "Udvid skema", + "Extended" : "Udvidet", + "Extensions" : "Udvidelser", + "Extract Pending Files" : "Udtræk ventende filer", + "Extract properties from each object" : "Udtræk egenskaber fra hvert objekt", + "Extracted At" : "Udtrukket den", + "Extraction Status" : "Udtrækningsstatus", + "Facet configuration updated successfully" : "Facetkonfiguration opdateret", + "Facets discovered and configured successfully" : "Facetter fundet og konfigureret", + "Facets discovered successfully" : "Facetter fundet", + "Failed" : "Mislykkedes", + "Failed to analyze schema properties" : "Kunne ikke analysere skemaegenskaber", + "Failed to archive conversation" : "Kunne ikke arkivere samtale", + "Failed to calculate sizes" : "Kunne ikke beregne størrelser", + "Failed to clear collection" : "Kunne ikke rydde samling", + "Failed to copy changes" : "Kunne ikke kopiere ændringer", + "Failed to copy data" : "Kunne ikke kopiere data", + "Failed to copy data to clipboard" : "Kunne ikke kopiere data til udklipsholderen", + "Failed to create conversation" : "Kunne ikke oprette samtale", + "Failed to create or find project" : "Kunne ikke oprette eller finde projekt", + "Failed to delete collection" : "Kunne ikke slette samling", + "Failed to delete conversation" : "Kunne ikke slette samtale", + "Failed to delete webhook" : "Kunne ikke slette webhook", + "Failed to download API specification" : "Kunne ikke downloade API-specifikation", + "Failed to get SOLR field configuration" : "Kunne ikke hente SOLR-feltkonfiguration", + "Failed to get entity categories" : "Kunne ikke hente entitetskategorier", + "Failed to get entity statistics" : "Kunne ikke hente entitetsstatistik", + "Failed to get entity types" : "Kunne ikke hente entitetstyper", + "Failed to load LLM configuration" : "Kunne ikke indlæse LLM-konfiguration", + "Failed to load Nextcloud groups" : "Kunne ikke indlæse Nextcloud-grupper", + "Failed to load conversation" : "Kunne ikke indlæse samtale", + "Failed to load entities" : "Kunne ikke indlæse entiteter", + "Failed to load entity" : "Kunne ikke indlæse entitet", + "Failed to load files" : "Kunne ikke indlæse filer", + "Failed to load organisations" : "Kunne ikke indlæse organisationer", + "Failed to load templates" : "Kunne ikke indlæse skabeloner", + "Failed to load webhooks" : "Kunne ikke indlæse webhooks", + "Failed to load workflows" : "Kunne ikke indlæse arbejdsgange", + "Failed to refresh database information" : "Kunne ikke opdatere databaseoplysninger", + "Failed to reindex collection" : "Kunne ikke genindeksere samling", + "Failed to rename conversation" : "Kunne ikke omdøbe samtale", + "Failed to restore conversation" : "Kunne ikke gendanne samtale", + "Failed to retry extraction" : "Kunne ikke forsøge udtrækning igen", + "Failed to retry webhook" : "Kunne ikke forsøge webhook igen", + "Failed to save GitHub token" : "Kunne ikke gemme GitHub-token", + "Failed to save GitLab URL" : "Kunne ikke gemme GitLab-URL", + "Failed to save GitLab token" : "Kunne ikke gemme GitLab-token", + "Failed to save additional feedback" : "Kunne ikke gemme yderligere feedback", + "Failed to save n8n configuration" : "Kunne ikke gemme n8n-konfiguration", + "Failed to save roles" : "Kunne ikke gemme roller", + "Failed to save settings" : "Kunne ikke gemme indstillinger", + "Failed to save webhook" : "Kunne ikke gemme webhook", + "Failed to send feedback" : "Kunne ikke sende feedback", + "Failed to test webhook" : "Kunne ikke teste webhook", + "Failed to update favorite status" : "Kunne ikke opdatere favoritstatus", + "Failed to update schema properties" : "Kunne ikke opdatere skemaegenskaber", + "Failed to update view" : "Kunne ikke opdatere visning", + "Failed to update webhook" : "Kunne ikke opdatere webhook", + "Feedback recorded" : "Feedback registreret", + "Fewer sources (1-3):" : "Færre kilder (1-3):", + "Field" : "Felt", + "File" : "Fil", + "File Chunk Prediction" : "Forudsigelse af filbidder", + "File Collection" : "Filsamling", + "File Management" : "Filadministration", + "File Name" : "Filnavn", + "File Path" : "Filsti", + "File Type Selection" : "Valg af filtype", + "File Vectorization" : "Filvektorisering", + "File Warmup" : "Filopvarmning", + "File actions menu" : "Menu for filhandlinger", + "File anonymized successfully" : "Fil anonymiseret", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vektorisering af filbidder er endnu ikke implementeret. Bidderne er klar og lagret, men vektoriseringstjenesten er under udvikling.", + "File collection not configured" : "Filsamling ikke konfigureret", + "File discovery completed" : "Filsøgning fuldført", + "File extraction completed" : "Filudtrækning fuldført", + "File extraction queued" : "Filudtrækning sat i kø", + "File indexed successfully" : "Fil indekseret", + "File is already anonymized" : "Fil er allerede anonymiseret", + "File not found" : "Fil ikke fundet", + "File settings updated successfully" : "Filindstillinger opdateret", + "File sources" : "Filkilder", + "File vectorization configuration saved successfully" : "Konfiguration af filvektorisering gemt", + "File vectorization started. Check the statistics section for progress." : "Filvektorisering startet. Tjek statistiksektionen for fremskridt.", + "File warmup completed" : "Filopvarmning fuldført", + "Files" : "Filer", + "Files with Completed Extraction:" : "Filer med fuldført udtrækning:", + "Files → fileCollection, Objects → objectCollection" : "Filer → fileCollection, Objekter → objectCollection", + "Filter Audit Trails" : "Filtrér revisionsspor", + "Filter Deleted Items" : "Filtrér slettede elementer", + "Filter Objects" : "Filtrér objekter", + "Filter Properties" : "Filtrér egenskaber", + "Filter Search Trails" : "Filtrér søgespor", + "Filter Statistics" : "Filtrér statistik", + "Filter and analyze search trail entries" : "Filtrér og analyser søgesporsposter", + "Filter and manage audit trail entries" : "Filtrér og administrér revisionssporsposter", + "Filter and manage soft deleted items" : "Filtrér og administrér blødt slettede elementer", + "Filter by object ID" : "Filtrér efter objekt-ID", + "Filter by search term" : "Filtrér efter søgeord", + "Filter by webhook" : "Filtrér efter webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filterdata indlæst automatisk. Brug filtrene nedenfor til at forfine din søgning.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrér webhook-udløsere efter nyttelastegenskaber (én pr. linje, format: nøgle: værdi)", + "Filtered" : "Filtreret", + "Filters" : "Filtre", + "Fireworks AI Chat Settings" : "Fireworks AI-chatindstillinger", + "Fireworks AI Embedding Configuration" : "Fireworks AI-indlejringskonfiguration", + "First" : "Første", + "Fixed" : "Rettet", + "For chat and retrieval-augmented generation" : "Til chat og søgningsforstærket generering", + "For vector embeddings and semantic search" : "Til vektorindlejringer og semantisk søgning", + "Format" : "Format", + "Format Issue" : "Formatproblem", + "Format constraint is missing" : "Formatbegrænsning mangler", + "From Date" : "Fra dato", + "From date" : "Fra dato", + "Full data copied to clipboard" : "Fulde data kopieret til udklipsholderen", + "General Issue" : "Generelt problem", + "Generate recommendations and confidence scores" : "Generér anbefalinger og konfidensscorer", + "Generate vectors immediately when new objects are created" : "Generér vektorer øjeblikkeligt, når nye objekter oprettes", + "GitHub token is valid" : "GitHub-token er gyldig", + "GitHub token removed successfully" : "GitHub-token fjernet", + "GitHub token saved successfully" : "GitHub-token gemt", + "GitLab URL saved successfully" : "GitLab-URL gemt", + "GitLab token is valid" : "GitLab-token er gyldig", + "GitLab token saved successfully" : "GitLab-token gemt", + "HTTP Method" : "HTTP-metode", + "HTTP method used to send webhook requests" : "HTTP-metode brugt til at sende webhook-anmodninger", + "Headers" : "Headere", + "Health" : "Tilstand", + "Heartbeat successful - connection kept alive" : "Heartbeat lykkedes - forbindelse holdt i live", + "Helpful" : "Nyttig", + "Hide Filters" : "Skjul filtre", + "Hide in forms" : "Skjul i formularer", + "Hide in list view" : "Skjul i listevisning", + "High" : "Høj", + "High Confidence" : "Høj konfidens", + "Host" : "Vært", + "Hourly" : "Hver time", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Hvor dybt indlejrede objektegenskaber skal gennemløbes (1-20). Højere værdier fanger flere detaljer, men øger vektorstørrelsen.", + "How often to check for updates (1-168 hours)" : "Hvor ofte der skal tjekkes for opdateringer (1-168 timer)", + "How to handle retries for failed webhook deliveries" : "Sådan håndteres genforsøg for mislykkede webhook-leveringer", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "IP-hastighedsgrænser ryddet", + "Identify properties not in the schema" : "Identificér egenskaber, der ikke er i skemaet", + "Immutable" : "Uforanderlig", + "Import" : "Importér", + "Import successful" : "Import lykkedes", + "Improved Property" : "Forbedret egenskab", + "In use" : "I brug", + "Inactive" : "Inaktiv", + "Include IDs and names of related objects for better contextual search" : "Inkludér ID'er og navne på relaterede objekter for bedre kontekstuel søgning", + "Include related object references" : "Inkludér relaterede objektreferencer", + "Include schema and register metadata" : "Inkludér skema- og registermetadata", + "Initialization failed" : "Initialisering mislykkedes", + "Initialize Project" : "Initialisér projekt", + "Inspect Fields" : "Inspicér felter", + "Inspect Index" : "Inspicér indeks", + "Invalid" : "Ugyldig", + "Invalid batch size. Must be between 1 and 5000" : "Ugyldig batchstørrelse. Skal være mellem 1 og 5000", + "Invalid field name provided" : "Ugyldigt feltnavn angivet", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Ugyldig maxObjects. Skal være 0 (alle) eller et positivt tal", + "Invalid:" : "Ugyldig:", + "Issue" : "Problem", + "Items per page:" : "Elementer pr. side:", + "Just now" : "Lige nu", + "LLM Configuration" : "LLM-konfiguration", + "LLM actions menu" : "Menu for LLM-handlinger", + "LLM configuration saved successfully" : "LLM-konfiguration gemt", + "LLM features disabled" : "LLM-funktioner deaktiveret", + "LLM features enabled" : "LLM-funktioner aktiveret", + "LLM must be enabled with an embedding provider configured" : "LLM skal være aktiveret med en indlejringsudbyder konfigureret", + "LLM settings updated successfully" : "LLM-indstillinger opdateret", + "Last" : "Sidste", + "Last Triggered" : "Senest udløst", + "Length Range:" : "Længdeinterval:", + "Linear" : "Lineær", + "Load Advanced Filters" : "Indlæs avancerede filtre", + "Load advanced filters with live data from your search index" : "Indlæs avancerede filtre med live-data fra dit søgeindeks", + "Load view" : "Indlæs visning", + "Loading ConfigSets..." : "Indlæser ConfigSets...", + "Loading advanced filters..." : "Indlæser avancerede filtre...", + "Loading agents..." : "Indlæser agenter...", + "Loading application details..." : "Indlæser applikationsdetaljer...", + "Loading applications..." : "Indlæser applikationer...", + "Loading archived conversations..." : "Indlæser arkiverede samtaler...", + "Loading audit trails..." : "Indlæser revisionsspor...", + "Loading collections..." : "Indlæser samlinger...", + "Loading configurations..." : "Indlæser konfigurationer...", + "Loading conversation..." : "Indlæser samtale...", + "Loading conversations..." : "Indlæser samtaler...", + "Loading deleted items..." : "Indlæser slettede elementer...", + "Loading events..." : "Indlæser begivenheder...", + "Loading filter data..." : "Indlæser filterdata...", + "Loading groups..." : "Indlæser grupper...", + "Loading log details..." : "Indlæser logdetaljer...", + "Loading registers..." : "Indlæser registre...", + "Loading schemas..." : "Indlæser skemaer...", + "Loading search trails..." : "Indlæser søgespor...", + "Loading sources..." : "Indlæser kilder...", + "Loading statistics..." : "Indlæser statistik...", + "Loading users..." : "Indlæser brugere...", + "Loading views..." : "Indlæser visninger...", + "Loading..." : "Indlæser...", + "Local" : "Lokal", + "Local Version" : "Lokal version", + "Locked" : "Låst", + "Locked:" : "Låst:", + "Login successful" : "Login lykkedes", + "Logs" : "Logge", + "Low" : "Lav", + "Low Confidence" : "Lav konfidens", + "Lucene Version:" : "Lucene-version:", + "Magic table synchronized successfully" : "Magisk tabel synkroniseret", + "Manage Organisation Roles" : "Administrér organisationsroller", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Administrér SOLR-samlinger (datalagre) og tildel dem til objekter og filer.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Administrér SOLR ConfigSets (konfigurationsskabeloner) til dine samlinger.", + "Manage and configure agents for automated tasks" : "Administrér og konfigurér agenter til automatiserede opgaver", + "Manage and monitor file text extraction status" : "Administrér og overvåg status for filtekstudtrækning", + "Manage and restore soft deleted items from your registers" : "Administrér og gendan blødt slettede elementer fra dine registre", + "Manage and view detected entities from files and objects" : "Administrér og vis registrerede entiteter fra filer og objekter", + "Manage document templates and themes" : "Administrér dokumentskabeloner og temaer", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Administrér n8n-arbejdsgange til OpenRegister-automatisering. Arbejdsgange lagres i det konfigurerede projekt.", + "Manage webhooks for event-driven integrations" : "Administrér webhooks til begivenhedsdrevne integrationer", + "Manage your applications and modules" : "Administrér dine applikationer og moduler", + "Manage your data registers and their configurations" : "Administrér dine dataregistre og deres konfigurationer", + "Manage your data schemas and their properties" : "Administrér dine dataskemaer og deres egenskaber", + "Manage your data sources and their configurations" : "Administrér dine datakilder og deres konfigurationer", + "Manage your saved search configurations" : "Administrér dine gemte søgekonfigurationer", + "Manage your system configurations and settings" : "Administrér dine systemkonfigurationer og indstillinger", + "Managed" : "Administreret", + "Max Files (0 = all)" : "Maks. filer (0 = alle)", + "Max Length" : "Maks. længde", + "Max Length:" : "Maks. længde:", + "Max Objects (0 = all)" : "Maks. objekter (0 = alle)", + "Max Retries" : "Maks. genforsøg", + "Max Shards/Node" : "Maks. shards/node", + "Max execution time (ms)" : "Maks. udførelsestid (ms)", + "Max ms" : "Maks. ms", + "Max result count" : "Maks. resultatantal", + "Max results" : "Maks. resultater", + "Maximum" : "Maksimum", + "Maximum Nesting Depth" : "Maksimal indlejringsdybde", + "Maximum length constraint is missing" : "Begrænsning for maksimal længde mangler", + "Maximum length is too restrictive" : "Maksimal længde er for restriktiv", + "Maximum number of files to process. Set to 0 to process all files." : "Maksimalt antal filer, der skal behandles. Sæt til 0 for at behandle alle filer.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maksimalt antal objekter, der skal behandles. Sæt til 0 for at behandle alle objekter.", + "Maximum number of retry attempts for failed deliveries" : "Maksimalt antal genforsøg for mislykkede leveringer", + "Maximum time to wait before committing changes" : "Maksimal tid at vente, før ændringer bekræftes", + "Maximum tokens to generate" : "Maksimalt antal tokens, der skal genereres", + "Maximum value constraint is missing" : "Begrænsning for maksimal værdi mangler", + "Maximum value is too restrictive" : "Maksimal værdi er for restriktiv", + "Medium" : "Mellem", + "Medium Confidence" : "Mellem konfidens", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Mellemforespørgsler: Søgninger med en vis filtrering eller flere parametre (f.eks. datointervaller, specifikke registre/skemaer)", + "Members of selected groups can access this view" : "Medlemmer af de valgte grupper kan få adgang til denne visning", + "Members:" : "Medlemmer:", + "Memory prediction calculated successfully" : "Hukommelsesforudsigelse beregnet", + "Message content is required" : "Beskedindhold er påkrævet", + "Message does not belong to this conversation" : "Beskeden tilhører ikke denne samtale", + "Metadata Filters" : "Metadatafiltre", + "Method" : "Metode", + "Min Length" : "Min. længde", + "Min execution time (ms)" : "Min. udførelsestid (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. resultatantal", + "Min results" : "Min. resultater", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Begrænsning for minimal værdi mangler", + "Minimum value is too restrictive" : "Minimal værdi er for restriktiv", + "Missing object fields created successfully" : "Manglende objektfelter oprettet", + "Mode:" : "Tilstand:", + "Model" : "Model", + "Model Name" : "Modelnavn", + "Modified" : "Ændret", + "Monthly" : "Månedligt", + "More sources (10-20):" : "Flere kilder (10-20):", + "Most Active Objects" : "Mest aktive objekter", + "Multiple Of" : "Multiplum af", + "Name" : "Navn", + "Names cache warmup completed" : "Opvarmning af navnecache fuldført", + "New Collection Name" : "Nyt samlingsnavn", + "New Conversation" : "Ny samtale", + "New Properties" : "Nye egenskaber", + "New Property" : "Ny egenskab", + "New Value" : "Ny værdi", + "New users without specific organisation membership will be automatically added to this organisation" : "Nye brugere uden specifikt organisationsmedlemskab tilføjes automatisk til denne organisation", + "Next" : "Næste", + "No" : "Nej", + "No ConfigSets found" : "Ingen ConfigSets fundet", + "No GitHub token configured" : "Ingen GitHub-token konfigureret", + "No GitHub token provided" : "Ingen GitHub-token angivet", + "No GitLab token provided" : "Ingen GitLab-token angivet", + "No Organisation" : "Ingen organisation", + "No active filters" : "Ingen aktive filtre", + "No activity data available" : "Ingen aktivitetsdata tilgængelige", + "No agents available" : "Ingen agenter tilgængelige", + "No agents found" : "Ingen agenter fundet", + "No app store cache exists yet" : "Der findes endnu ingen app store-cache", + "No applications are available." : "Der er ingen applikationer tilgængelige.", + "No applications found" : "Ingen applikationer fundet", + "No archived conversations" : "Ingen arkiverede samtaler", + "No audit trail entries found" : "Ingen revisionssporsposter fundet", + "No changes recorded" : "Ingen ændringer registreret", + "No chunks to vectorize" : "Ingen bidder at vektorisere", + "No collections found" : "Ingen samlinger fundet", + "No configuration data" : "Ingen konfigurationsdata", + "No configurations are available." : "Der er ingen konfigurationer tilgængelige.", + "No configurations found" : "Ingen konfigurationer fundet", + "No configurations found for this application." : "Ingen konfigurationer fundet for denne applikation.", + "No conversations yet. Create a new one to get started!" : "Ingen samtaler endnu. Opret en ny for at komme i gang!", + "No data available" : "Ingen data tilgængelige", + "No data available for chart" : "Ingen data tilgængelige til diagram", + "No deleted items found" : "Ingen slettede elementer fundet", + "No deletion data available" : "Ingen slettedata tilgængelige", + "No description found" : "Ingen beskrivelse fundet", + "No description provided" : "Ingen beskrivelse angivet", + "No entities detected in this file. Run text extraction first." : "Ingen entiteter registreret i denne fil. Kør tekstudtrækning først.", + "No entities found" : "Ingen entiteter fundet", + "No entities have been detected yet" : "Der er endnu ikke registreret nogen entiteter", + "No events found" : "Ingen begivenheder fundet", + "No expired audit trails found to clear" : "Ingen udløbne revisionsspor fundet at rydde", + "No facetable fields available. Select a register and schema to see available filters." : "Ingen facetterbare felter tilgængelige. Vælg et register og skema for at se tilgængelige filtre.", + "No files found" : "Ingen filer fundet", + "No files have been extracted yet" : "Der er endnu ikke udtrukket nogen filer", + "No files to process" : "Ingen filer at behandle", + "No files to reindex" : "Ingen filer at genindeksere", + "No filters are currently active. This will delete ALL audit trail entries!" : "Der er i øjeblikket ingen aktive filtre. Dette sletter ALLE revisionssporsposter!", + "No log entries found" : "Ingen logposter fundet", + "No logs are available for this configuration." : "Der er ingen logge tilgængelige for denne konfiguration.", + "No logs are available for this source." : "Der er ingen logge tilgængelige for denne kilde.", + "No logs found" : "Ingen logge fundet", + "No mismatched fields found - SOLR schema is properly configured" : "Ingen uoverensstemmende felter fundet - SOLR-skema er korrekt konfigureret", + "No objects found" : "Ingen objekter fundet", + "No objects found to vectorize" : "Ingen objekter fundet at vektorisere", + "No objects selected for deletion" : "Ingen objekter valgt til sletning", + "No objects to delete" : "Ingen objekter at slette", + "No objects were permanently deleted" : "Ingen objekter blev slettet permanent", + "No properties found" : "Ingen egenskaber fundet", + "No properties match your filters." : "Ingen egenskaber matcher dine filtre.", + "No purge date set" : "Ingen udrensningsdato angivet", + "No register data available" : "Ingen registerdata tilgængelige", + "No registers found" : "Ingen registre fundet", + "No registers found for this application." : "Ingen registre fundet for denne applikation.", + "No relations found" : "Ingen relationer fundet", + "No request body available" : "Ingen anmodningstekst tilgængelig", + "No response body available" : "Ingen svartekst tilgængelig", + "No saved views yet. Create one in the Search tab!" : "Ingen gemte visninger endnu. Opret en i søgefanen!", + "No schema data available" : "Ingen skemadata tilgængelige", + "No schema selected for exploration" : "Intet skema valgt til udforskning", + "No schemas found" : "Ingen skemaer fundet", + "No schemas found for this application." : "Ingen skemaer fundet for denne applikation.", + "No search terms data available" : "Ingen søgeordsdata tilgængelige", + "No search trail entries found" : "Ingen søgesporsposter fundet", + "No sources are available." : "Der er ingen kilder tilgængelige.", + "No sources found" : "Ingen kilder fundet", + "No templates found" : "Ingen skabeloner fundet", + "No templates have been created yet" : "Der er endnu ikke oprettet nogen skabeloner", + "No valid magic-mapped register+schema combinations found" : "Ingen gyldige magisk-tilknyttede register+skema-kombinationer fundet", + "No views found. Create views first before configuring vectorization." : "Ingen visninger fundet. Opret visninger først, før du konfigurerer vektorisering.", + "No views match your search" : "Ingen visninger matcher din søgning", + "No webhooks found" : "Ingen webhooks fundet", + "No webhooks have been configured yet" : "Der er endnu ikke konfigureret nogen webhooks", + "No workflows found in this project. Create workflows in the n8n editor." : "Ingen arbejdsgange fundet i dette projekt. Opret arbejdsgange i n8n-editoren.", + "None" : "Ingen", + "Not helpful" : "Ikke nyttig", + "Not in use" : "Ikke i brug", + "Number Constraints" : "Talbegrænsninger", + "Number of chunks to vectorize in one API call (1-100)." : "Antal bidder, der skal vektoriseres i ét API-kald (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Antal bidder, der skal vektoriseres i ét API-kald. Højere = hurtigere, men mere hukommelse. Anbefalet: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Antal objekter, der skal behandles i hver batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Antal objekter, der skal vektoriseres i ét API-kald. Højere = hurtigere, men mere hukommelse. Anbefalet: 10-50.", + "Numeric Range:" : "Numerisk interval:", + "Object" : "Objekt", + "Object Collection" : "Objektsamling", + "Object Count Prediction" : "Forudsigelse af objektantal", + "Object ID" : "Objekt-ID", + "Object Management" : "Objektadministration", + "Object Vectorization" : "Objektvektorisering", + "Object Warmup" : "Objektopvarmning", + "Object collection not configured" : "Objektsamling ikke konfigureret", + "Object not found" : "Objekt ikke fundet", + "Object permanently deleted" : "Objekt slettet permanent", + "Object restored successfully" : "Objekt gendannet", + "Object settings updated successfully" : "Objektindstillinger opdateret", + "Object sources" : "Objektkilder", + "Object unlocked successfully" : "Objekt låst op", + "Object vectorization configuration saved successfully" : "Konfiguration af objektvektorisering gemt", + "Object vectorized successfully" : "Objekt vektoriseret", + "Object:" : "Objekt:", + "Objects" : "Objekter", + "Objects Analyzed" : "Analyserede objekter", + "Objects Distribution" : "Objektfordeling", + "Objects being analyzed" : "Objekter, der analyseres", + "Objects by Register" : "Objekter efter register", + "Objects by Schema" : "Objekter efter skema", + "Objects deletion completed successfully" : "Sletning af objekter fuldført", + "Objects in schema" : "Objekter i skema", + "Objects to Process:" : "Objekter, der skal behandles:", + "Objects to be analyzed" : "Objekter, der skal analyseres", + "Objects to be deleted" : "Objekter, der skal slettes", + "Objects to be validated" : "Objekter, der skal valideres", + "Objects will be serialized as JSON text before vectorization" : "Objekter serialiseres som JSON-tekst før vektorisering", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekter bløde slettes (markeres som slettede, men beholdes i databasen). De kan gendannes senere, hvis det er nødvendigt.", + "Old Value" : "Gammel værdi", + "Oldest Item (days)" : "Ældste element (dage)", + "Ollama Chat Settings" : "Ollama-chatindstillinger", + "Ollama Embedding Configuration" : "Ollama-indlejringskonfiguration", + "Ollama URL" : "Ollama-URL", + "Only With Changes" : "Kun med ændringer", + "Open Register Objects" : "Open Register-objekter", + "Open n8n Editor" : "Åbn n8n-editor", + "OpenAI Chat Settings" : "OpenAI-chatindstillinger", + "OpenAI Embedding Configuration" : "OpenAI-indlejringskonfiguration", + "OpenRegister Settings" : "OpenRegister-indstillinger", + "Optional webhook secret for signature verification" : "Valgfri webhook-hemmelighed til signaturverifikation", + "Order" : "Rækkefølge", + "Organisation" : "Organisation", + "Organisation ID" : "Organisations-ID", + "Organisation Members" : "Organisationsmedlemmer", + "Organisation Statistics" : "Organisationsstatistik", + "Organisation created successfully" : "Organisation oprettet", + "Organisation settings saved successfully" : "Organisationsindstillinger gemt", + "Organisations" : "Organisationer", + "Organization" : "Organisation", + "Organization ID (Optional)" : "Organisations-ID (valgfri)", + "Orphaned Items" : "Forældreløse elementer", + "Owner" : "Ejer", + "Owner:" : "Ejer:", + "Parallel Mode (Faster, more resource intensive)" : "Parallel tilstand (hurtigere, mere ressourcekrævende)", + "Parallel:" : "Parallel:", + "Password" : "Adgangskode", + "Password for SOLR authentication (optional)" : "Adgangskode til SOLR-godkendelse (valgfri)", + "Path" : "Sti", + "Path in repository" : "Sti i repository", + "Path where the configuration file will be saved in the repository" : "Sti, hvor konfigurationsfilen gemmes i repository", + "Path where the register OAS file will be saved in the repository" : "Sti, hvor register-OAS-filen gemmes i repository", + "Pattern (regex)" : "Mønster (regex)", + "Pattern Issue" : "Mønsterproblem", + "Pattern constraint is missing" : "Mønsterbegrænsning mangler", + "Patterns:" : "Mønstre:", + "Pending" : "Afventer", + "Permanent Delete Mode" : "Tilstand for permanent sletning", + "Permanently Delete" : "Slet permanent", + "Permanently delete already soft-deleted objects" : "Slet allerede blødt slettede objekter permanent", + "Permanently delete this conversation?" : "Slet denne samtale permanent?", + "Person" : "Person", + "Personal Data" : "Personoplysninger", + "Phone" : "Telefon", + "Photos" : "Fotos", + "Please create an agent in the" : "Opret venligst en agent i", + "Please select an agent to continue" : "Vælg venligst en agent for at fortsætte", + "Please select which register and schema to use for the new object" : "Vælg venligst hvilket register og skema der skal bruges til det nye objekt", + "Please try again later." : "Prøv venligst igen senere.", + "Please wait while we fetch your agents." : "Vent venligst, mens vi henter dine agenter.", + "Please wait while we fetch your applications." : "Vent venligst, mens vi henter dine applikationer.", + "Please wait while we fetch your configurations." : "Vent venligst, mens vi henter dine konfigurationer.", + "Please wait while we fetch your deleted items." : "Vent venligst, mens vi henter dine slettede elementer.", + "Please wait while we fetch your sources." : "Vent venligst, mens vi henter dine kilder.", + "Polls" : "Afstemninger", + "Popular Search Terms" : "Populære søgeord", + "Port" : "Port", + "Prerequisites" : "Forudsætninger", + "Press Enter to send, Shift+Enter for new line" : "Tryk Enter for at sende, Shift+Enter for ny linje", + "Previous" : "Forrige", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Behandler bidder i batches med simuleret parallelisme.", + "Processes file chunks sequentially (safest)." : "Behandler filbidder sekventielt (sikrest).", + "Processes objects in chunks with simulated parallelism." : "Behandler objekter i bidder med simuleret parallelisme.", + "Processes objects sequentially (safest)." : "Behandler objekter sekventielt (sikrest).", + "Processing" : "Behandler", + "Processing Limits" : "Behandlingsgrænser", + "Processing..." : "Behandler...", + "Project initialized successfully" : "Projekt initialiseret", + "Project not found. Please initialize first." : "Projekt ikke fundet. Initialisér venligst først.", + "Properties" : "Egenskaber", + "Property Behaviors" : "Egenskabsadfærd", + "Property Configuration:" : "Egenskabskonfiguration:", + "Property Filters" : "Egenskabsfiltre", + "Property Title" : "Egenskabstitel", + "Property Type" : "Egenskabstype", + "Property can be improved" : "Egenskab kan forbedres", + "Protected" : "Beskyttet", + "Provider" : "Udbyder", + "Provider is required for testing" : "Udbyder er påkrævet til test", + "Public" : "Offentlig", + "Public View" : "Offentlig visning", + "Public views can be accessed by anyone in the system" : "Offentlige visninger kan tilgås af alle i systemet", + "Publish" : "Udgiv", + "Publish Objects" : "Udgiv objekter", + "Publish all objects in this schema" : "Udgiv alle objekter i dette skema", + "Published" : "Udgivet", + "Published:" : "Udgivet:", + "Publishing..." : "Udgiver...", + "Purge" : "Udrens", + "Purge Date" : "Udrensningsdato", + "Query Complexity Distribution" : "Fordeling af forespørgselskompleksitet", + "Query parameter is required" : "Forespørgselsparameter er påkrævet", + "RAG" : "RAG", + "RAG Configuration" : "RAG-konfiguration", + "Rate limits cleared successfully" : "Hastighedsgrænser ryddet", + "Raw Changes Data" : "Rå ændringsdata", + "Re-vectorize on object update" : "Genvektorisér ved objektopdatering", + "Read" : "Læs", + "Recommendations:" : "Anbefalinger:", + "Recommended Type:" : "Anbefalet type:", + "Recommended: 5 sources" : "Anbefalet: 5 kilder", + "Refresh" : "Opdatér", + "Refresh Data" : "Opdatér data", + "Refresh Stats" : "Opdatér statistik", + "Refresh Workflows" : "Opdatér arbejdsgange", + "Refresh dashboard" : "Opdatér oversigt", + "Refresh database info" : "Opdatér databaseoplysninger", + "Register" : "Register", + "Register ID" : "Register-ID", + "Register Statistics" : "Registerstatistik", + "Register Totals" : "Registertotaler", + "Register depublished successfully" : "Register afpubliceret", + "Register not found" : "Register ikke fundet", + "Register objects deletion completed successfully" : "Sletning af registerobjekter fuldført", + "Register published successfully" : "Register publiceret", + "Register/Schema Usage" : "Register/skema-brug", + "Registers" : "Registre", + "Reindex" : "Genindeksér", + "Reindex completed" : "Genindeksering fuldført", + "Reindex completed successfully" : "Genindeksering fuldført", + "Reindex failed" : "Genindeksering mislykkedes", + "Relations" : "Relationer", + "Remote Version" : "Fjernversion", + "Remove" : "Fjern", + "Remove filter" : "Fjern filter", + "Remove from favorites" : "Fjern fra favoritter", + "Remove group" : "Fjern gruppe", + "Removed" : "Fjernet", + "Removed from favorites" : "Fjernet fra favoritter", + "Rename Conversation" : "Omdøb samtale", + "Rename conversation" : "Omdøb samtale", + "Replicas" : "Replikaer", + "Repository" : "Repository", + "Request Body" : "Anmodningstekst", + "Request Data" : "Anmodningsdata", + "Request Details" : "Anmodningsdetaljer", + "Request body must be JSON with a \"query\" field" : "Anmodningstekst skal være JSON med et \"query\"-felt", + "Request timeout in seconds" : "Anmodningstimeout i sekunder", + "Required field" : "Påkrævet felt", + "Required status is inconsistent" : "Påkrævet status er inkonsistent", + "Rerun Search" : "Kør søgning igen", + "Reset Changes" : "Nulstil ændringer", + "Reset Filters" : "Nulstil filtre", + "Response Body" : "Svartekst", + "Response Details" : "Svardetaljer", + "Response Time:" : "Svartid:", + "Restore" : "Gendan", + "Restore conversation" : "Gendan samtale", + "Restore or permanently delete items" : "Gendan eller slet elementer permanent", + "Result Count Range" : "Interval for resultatantal", + "Results" : "Resultater", + "Retrieve all objects for this schema" : "Hent alle objekter for dette skema", + "Retry" : "Prøv igen", + "Retry Failed Extractions" : "Prøv mislykkede udtrækninger igen", + "Retry Policy" : "Genforsøgspolitik", + "Retry completed" : "Genforsøg fuldført", + "Retrying..." : "Prøver igen...", + "Risk Level" : "Risikoniveau", + "Roles updated successfully" : "Roller opdateret", + "SOLR Connection Settings" : "SOLR-forbindelsesindstillinger", + "SOLR Version:" : "SOLR-version:", + "SOLR actions menu" : "Menu for SOLR-handlinger", + "SOLR base path (usually /solr)" : "SOLR-basissti (normalt /solr)", + "SOLR is disabled" : "SOLR er deaktiveret", + "SOLR is not available or not configured" : "SOLR er ikke tilgængelig eller ikke konfigureret", + "SOLR logging disabled" : "SOLR-logning deaktiveret", + "SOLR logging enabled" : "SOLR-logning aktiveret", + "SOLR search disabled" : "SOLR-søgning deaktiveret", + "SOLR search enabled" : "SOLR-søgning aktiveret", + "SOLR server hostname or IP address" : "SOLR-serverens værtsnavn eller IP-adresse", + "SOLR server port number (optional, defaults to 8983)" : "SOLR-serverens portnummer (valgfri, standard er 8983)", + "SOLR setup completed successfully" : "SOLR-opsætning fuldført", + "SOLR setup failed" : "SOLR-opsætning mislykkedes", + "SOLR setup failed - check logs" : "SOLR-opsætning mislykkedes - tjek logge", + "Sample Values:" : "Eksempelværdier:", + "Save" : "Gem", + "Save Configuration" : "Gem konfiguration", + "Save Connection Settings" : "Gem forbindelsesindstillinger", + "Save Roles" : "Gem roller", + "Save Settings" : "Gem indstillinger", + "Save current search as view" : "Gem nuværende søgning som visning", + "Saved Views" : "Gemte visninger", + "Saving..." : "Gemmer...", + "Schema" : "Skema", + "Schema ID" : "Skema-ID", + "Schema Name" : "Skemanavn", + "Schema Statistics" : "Skemastatistik", + "Schema depublished successfully" : "Skema afpubliceret", + "Schema objects deletion completed successfully" : "Sletning af skemaobjekter fuldført", + "Schema published successfully" : "Skema publiceret", + "Schema successfully created" : "Skema oprettet", + "Schema successfully updated" : "Skema opdateret", + "Schemas" : "Skemaer", + "Scheme" : "Skema", + "Search / Views" : "Søg / visninger", + "Search Activity" : "Søgeaktivitet", + "Search Analytics" : "Søgeanalyser", + "Search Entities" : "Søg i entiteter", + "Search Files" : "Søg i filer", + "Search GitHub" : "Søg på GitHub", + "Search GitLab" : "Søg på GitLab", + "Search Method" : "Søgemetode", + "Search Term" : "Søgeord", + "Search Trail Details" : "Detaljer for søgespor", + "Search Trail Management" : "Administration af søgespor", + "Search Trail Statistics" : "Statistik for søgespor", + "Search Trails" : "Søgespor", + "Search Views" : "Søg i visninger", + "Search Webhooks" : "Søg i webhooks", + "Search by file name or path" : "Søg efter filnavn eller sti", + "Search by name or URL" : "Søg efter navn eller URL", + "Search by value" : "Søg efter værdi", + "Search in Files" : "Søg i filer", + "Search in Objects" : "Søg i objekter", + "Search property names..." : "Søg i egenskabsnavne...", + "Search trail deletion not implemented yet" : "Sletning af søgespor er endnu ikke implementeret", + "Search views..." : "Søg i visninger...", + "Secret" : "Hemmelighed", + "Secret key for HMAC signature generation (optional)" : "Hemmelig nøgle til generering af HMAC-signatur (valgfri)", + "Select AI Agent" : "Vælg AI-agent", + "Select All" : "Vælg alle", + "Select ConfigSet" : "Vælg ConfigSet", + "Select File Types to Vectorize:" : "Vælg filtyper til vektorisering:", + "Select HTTP method" : "Vælg HTTP-metode", + "Select Register and Schema" : "Vælg register og skema", + "Select Views to Vectorize:" : "Vælg visninger til vektorisering:", + "Select a Nextcloud group to add" : "Vælg en Nextcloud-gruppe at tilføje", + "Select a branch" : "Vælg en gren", + "Select a model or type a custom model name" : "Vælg en model eller indtast et brugerdefineret modelnavn", + "Select a repository" : "Vælg et repository", + "Select a repository you have write access to" : "Vælg et repository, du har skriveadgang til", + "Select an AI Agent" : "Vælg en AI-agent", + "Select an AI agent to begin chatting with your data." : "Vælg en AI-agent for at begynde at chatte med dine data.", + "Select backend" : "Vælg backend", + "Select chat model" : "Vælg chatmodel", + "Select collection for files" : "Vælg samling til filer", + "Select collection for objects" : "Vælg samling til objekter", + "Select default organisation" : "Vælg standardorganisation", + "Select event to listen to..." : "Vælg begivenhed at lytte til...", + "Select model" : "Vælg model", + "Select options..." : "Vælg indstillinger...", + "Select period" : "Vælg periode", + "Select property to send as payload" : "Vælg egenskab, der skal sendes som nyttelast", + "Select provider" : "Vælg udbyder", + "Select registers and schemas to save a view" : "Vælg registre og skemaer for at gemme en visning", + "Select retry policy" : "Vælg genforsøgspolitik", + "Select the branch to publish to" : "Vælg den gren, der skal udgives til", + "Select the event this webhook should listen to" : "Vælg den begivenhed, denne webhook skal lytte til", + "Select views to vectorize:" : "Vælg visninger til vektorisering:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Vælg hvilke Nextcloud-grupper der er tilgængelige for denne organisation. Brugere i disse grupper får adgang til organisationsressourcer.", + "Select which data views the AI can search" : "Vælg hvilke datavisninger AI'en kan søge i", + "Select which property from the event should be used as the webhook payload data" : "Vælg hvilken egenskab fra begivenheden der skal bruges som webhook-nyttelastdata", + "Select which tools the AI can use to perform actions" : "Vælg hvilke værktøjer AI'en kan bruge til at udføre handlinger", + "Selected" : "Valgt", + "Selected Groups" : "Valgte grupper", + "Selected audit trails deleted successfully" : "Valgte revisionsspor slettet", + "Selected search trails deleted successfully" : "Valgte søgespor slettet", + "Selected users can access this view" : "Valgte brugere kan få adgang til denne visning", + "Send additional feedback" : "Send yderligere feedback", + "Send as CloudEvent" : "Send som CloudEvent", + "Sensitive PII" : "Følsomme personoplysninger", + "Serial Mode (Safer, slower)" : "Seriel tilstand (sikrere, langsommere)", + "Serial:" : "Seriel:", + "Server Configuration" : "Serverkonfiguration", + "Server Information" : "Serveroplysninger", + "Settings" : "Indstillinger", + "Shards" : "Shards", + "Share" : "Del", + "Share with Groups" : "Del med grupper", + "Share with Users" : "Del med brugere", + "Show Filters" : "Vis filtre", + "Show less" : "Vis mindre", + "Show only entries with changes" : "Vis kun poster med ændringer", + "Simple" : "Enkel", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Enkle forespørgsler: Grundlæggende tekstsøgninger med minimale parametre (f.eks. enkelt søgeord, ingen avancerede filtre)", + "Size" : "Størrelse", + "Slug" : "Slug", + "Soft Delete Mode" : "Tilstand for blød sletning", + "Soft Deleted Items" : "Blødt slettede elementer", + "SolrCloud mode enabled" : "SolrCloud-tilstand aktiveret", + "Source" : "Kilde", + "Sources" : "Kilder", + "Sources:" : "Kilder:", + "Standalone SOLR mode" : "Selvstændig SOLR-tilstand", + "Start Conversation" : "Start samtale", + "Start Vectorization" : "Start vektorisering", + "Start a conversation" : "Start en samtale", + "Starting names cache warmup..." : "Starter opvarmning af navnecache...", + "Starting..." : "Starter...", + "Statistics" : "Statistik", + "Stats calculation not yet implemented" : "Statistikberegning er endnu ikke implementeret", + "Status" : "Status", + "Status Code" : "Statuskode", + "String Constraints" : "Strengbegrænsninger", + "Success" : "Succes", + "Success Rate" : "Succesrate", + "Success Status" : "Successtatus", + "Successful" : "Vellykket", + "Successfully joined organisation" : "Tilsluttet organisation", + "Sync Table" : "Synkronisér tabel", + "Synchronization Settings" : "Synkroniseringsindstillinger", + "System Default" : "Systemstandard", + "Technical Description" : "Teknisk beskrivelse", + "Technical description for developers and administrators" : "Teknisk beskrivelse til udviklere og administratorer", + "Temperature" : "Temperatur", + "Templates" : "Skabeloner", + "Test" : "Test", + "Test Chat" : "Test chat", + "Test Connection" : "Test forbindelse", + "Test Embedding" : "Test indlejring", + "Test endpoint executed successfully" : "Test-endpoint udført", + "Test webhook delivered successfully" : "Test-webhook leveret", + "Test webhook delivery failed" : "Levering af test-webhook mislykkedes", + "Test webhook sent successfully" : "Test-webhook sendt", + "Testing..." : "Tester...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Tekstbidder genereres under filudtrækning og lagres i databasen. Vektorisering læser disse forudbiddede filer og konverterer dem til indlejringer.", + "Text extracted successfully" : "Tekst udtrukket", + "Text extraction disabled" : "Tekstudtrækning deaktiveret", + "The URL where webhook events will be sent" : "Den URL, hvor webhook-begivenheder sendes til", + "The requested conversation does not exist" : "Den anmodede samtale findes ikke", + "The saved view and all its search configuration" : "Den gemte visning og hele dens søgekonfiguration", + "There are no audit trail entries matching your current filters." : "Der er ingen revisionssporsposter, der matcher dine nuværende filtre.", + "There are no deleted items matching your current filters." : "Der er ingen slettede elementer, der matcher dine nuværende filtre.", + "There are no search trail entries matching your current filters." : "Der er ingen søgesporsposter, der matcher dine nuværende filtre.", + "There are no webhook log entries matching your filters." : "Der er ingen webhook-logposter, der matcher dine filtre.", + "This action cannot be undone." : "Denne handling kan ikke fortrydes.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Denne handling kan ikke fortrydes. Sørg for, at ingen samlinger bruger dette ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Denne agent har ingen konfigurerbare visninger eller værktøjer, men du kan stadig justere RAG-indstillingerne nedenfor.", + "This analysis may take some time" : "Denne analyse kan tage noget tid", + "This audit trail entry does not contain any change information." : "Denne revisionssporspost indeholder ingen ændringsoplysninger.", + "This configuration has no data defined." : "Denne konfiguration har ingen data defineret.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Dette endpoint er forældet. Brug bidde-baserede endpoints i stedet.", + "This entity has no relations to objects or files" : "Denne entitet har ingen relationer til objekter eller filer", + "This is a test webhook from OpenRegister" : "Dette er en test-webhook fra OpenRegister", + "This schema must use Magic Table configuration to sync" : "Dette skema skal bruge Magic Table-konfiguration for at synkronisere", + "This source has no associated registers." : "Denne kilde har ingen tilknyttede registre.", + "This will copy the _default ConfigSet with the new name" : "Dette kopierer _default-ConfigSet med det nye navn", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Dette sletter permanent ALLE indlejringer (vektorer) fra databasen. Du bliver nødt til at genvektorisere alle objekter og filer. Denne handling kan ikke fortrydes.\\n\\nEr du sikker på, at du vil fortsætte?", + "This will permanently delete:" : "Dette sletter permanent:", + "Timeout (seconds)" : "Timeout (sekunder)", + "Timestamp" : "Tidsstempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tip: Aktivér kun visninger, der har brug for semantisk søgning, for at minimere indlejringsomkostninger. Enkle opslagstabeller har sjældent brug for vektorisering.", + "Title" : "Titel", + "To Date" : "Til dato", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "For at justere biddestørrelse eller -strategi skal du gå til Filkonfiguration → Behandlingsgrænser.", + "To change the embedding provider or model, go to LLM Configuration." : "For at ændre indlejringsudbyder eller -model skal du gå til LLM-konfiguration.", + "To date" : "Til dato", + "Toggle search sidebar" : "Skift søgesidebjælke", + "Tools" : "Værktøjer", + "Top Deleters" : "Top-slettere", + "Total" : "I alt", + "Total Audit Trails" : "Revisionsspor i alt", + "Total Chunks Available:" : "Tilgængelige bidder i alt:", + "Total Deleted Items" : "Slettede elementer i alt", + "Total Members" : "Medlemmer i alt", + "Total Objects" : "Objekter i alt", + "Total Objects in Database:" : "Objekter i database i alt:", + "Total Organisations" : "Organisationer i alt", + "Total Results" : "Resultater i alt", + "Total Searches" : "Søgninger i alt", + "Total Size" : "Samlet størrelse", + "Total Storage" : "Samlet lagerplads", + "Total:" : "I alt:", + "Totals" : "Totaler", + "Type" : "Type", + "Type Issue" : "Typeproblem", + "Type Variations:" : "Typevariationer:", + "Type must be \"positive\" or \"negative\"" : "Type skal være \"positive\" eller \"negative\"", + "Type to search for groups" : "Skriv for at søge efter grupper", + "Type to search for users" : "Skriv for at søge efter brugere", + "Type to search groups..." : "Skriv for at søge i grupper...", + "Type to search users..." : "Skriv for at søge i brugere...", + "URL" : "URL", + "URL where Ollama is running" : "URL, hvor Ollama kører", + "UUID:" : "UUID:", + "Unchanged" : "Uændret", + "Unique Search Terms" : "Unikke søgeord", + "Unique Terms" : "Unikke ord", + "Unknown" : "Ukendt", + "Unknown Register" : "Ukendt register", + "Unknown Schema" : "Ukendt skema", + "Untitled View" : "Visning uden titel", + "Update" : "Opdatér", + "Update Operations" : "Opdatér-handlinger", + "Update configuration: ..." : "Opdatér konfiguration: ...", + "Update register OAS: ..." : "Opdatér register-OAS: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Opdatér vektorer, når objektdata ændres (anbefales til nøjagtig søgning)", + "Updated" : "Opdateret", + "Updated At" : "Opdateret den", + "Updated:" : "Opdateret:", + "Uptime:" : "Oppetid:", + "Usage Count" : "Brugsantal", + "Use SolrCloud with Zookeeper for distributed search" : "Brug SolrCloud med Zookeeper til distribueret søgning", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Brug filtre til at indsnævre revisionssporsposter efter register, skema, handlingstype, bruger, datointerval eller objekt-ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Brug filtre til at indsnævre slettede elementer efter register, skema, slettedato eller den bruger, der slettede dem.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Brug filtre til at indsnævre søgesporsposter efter register, skema, successtatus, bruger, datointerval, søgeord eller ydeevnemålinger.", + "Used By Collections" : "Brugt af samlinger", + "User" : "Bruger", + "User Agent Statistics" : "User Agent-statistik", + "User Description" : "Brugerbeskrivelse", + "User rate limits cleared successfully" : "Brugerhastighedsgrænser ryddet", + "User removed successfully" : "Bruger fjernet", + "User-friendly description shown in forms and help text" : "Brugervenlig beskrivelse vist i formularer og hjælpetekst", + "Username" : "Brugernavn", + "Username for SOLR authentication (optional)" : "Brugernavn til SOLR-godkendelse (valgfri)", + "Users" : "Brugere", + "Using Pre-Generated Chunks" : "Bruger forudgenererede bidder", + "Validate" : "Validér", + "Validate Objects" : "Validér objekter", + "Validation completed successfully" : "Validering fuldført", + "Validation failed" : "Validering mislykkedes", + "Value" : "Værdi", + "Vector Dimensions" : "Vektordimensioner", + "Vector Search Backend" : "Vektorsøgningsbackend", + "Vector field: _embedding_" : "Vektorfelt: _embedding_", + "Vectorization Triggers" : "Vektoriseringsudløsere", + "Vectorize All Files" : "Vektorisér alle filer", + "Vectorize All Objects" : "Vektorisér alle objekter", + "Vectorize all file types" : "Vektorisér alle filtyper", + "Vectorize all views" : "Vektorisér alle visninger", + "Vectorize on object creation" : "Vektorisér ved objektoprettelse", + "Vectors will be stored in your existing object and file collections" : "Vektorer lagres i dine eksisterende objekt- og filsamlinger", + "Version" : "Version", + "Very High" : "Meget høj", + "View API Docs" : "Vis API-dokumentation", + "View Changes" : "Vis ændringer", + "View Details" : "Vis detaljer", + "View Error" : "Vis fejl", + "View File" : "Vis fil", + "View Full Details" : "Vis fulde detaljer", + "View Logs" : "Vis logge", + "View Name" : "Visningsnavn", + "View Object" : "Vis objekt", + "View Parameters" : "Vis parametre", + "View Selection" : "Visningsvalg", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Vis og analyser søgesporslogge med avancerede filtrerings- og analysefunktioner", + "View and analyze system audit trails with advanced filtering capabilities" : "Vis og analyser systemrevisionsspor med avancerede filtreringsfunktioner", + "View deleted successfully" : "Visning slettet", + "View entity details and manage relations" : "Vis entitetsdetaljer og administrér relationer", + "View name" : "Visningsnavn", + "View name is required" : "Visningsnavn er påkrævet", + "View saved successfully!" : "Visning gemt!", + "View search analytics and manage search logs" : "Vis søgeanalyser og administrér søgelogge", + "View successfully deleted" : "Visning slettet", + "View successfully updated" : "Visning opdateret", + "View updated successfully!" : "Visning opdateret!", + "View webhook delivery logs and filter by webhook" : "Vis webhook-leveringslogge og filtrér efter webhook", + "Views" : "Visninger", + "Visible to users" : "Synlig for brugere", + "Wait for Response" : "Vent på svar", + "Wait for webhook response before continuing (required for request/response flows)" : "Vent på webhook-svar før fortsættelse (påkrævet for anmodnings-/svar-flows)", + "Warmup Names Cache" : "Opvarm navnecache", + "Warning" : "Advarsel", + "Warning:" : "Advarsel:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detaljer for webhook-log", + "Webhook Logs" : "Webhook-logge", + "Webhook created successfully" : "Webhook oprettet", + "Webhook deleted" : "Webhook slettet", + "Webhook retry delivered successfully" : "Webhook-genforsøg leveret", + "Webhook retry delivery failed" : "Levering af webhook-genforsøg mislykkedes", + "Webhook updated" : "Webhook opdateret", + "Webhook updated successfully" : "Webhook opdateret", + "Webhooks" : "Webhooks", + "Weekly" : "Ugentligt", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Indpak webhook-nyttelast i CloudEvents-format for bedre interoperabilitet", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Ja", + "You" : "Dig", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Du kan oprette nye ConfigSets baseret på _default-skabelonen eller uploade brugerdefinerede direkte til din SOLR-server.", + "You do not have access to this conversation" : "Du har ikke adgang til denne samtale", + "You do not have permission to delete this conversation" : "Du har ikke tilladelse til at slette denne samtale", + "You do not have permission to modify this conversation" : "Du har ikke tilladelse til at ændre denne samtale", + "You do not have permission to restore this conversation" : "Du har ikke tilladelse til at gendanne denne samtale", + "You must be logged in to favorite views" : "Du skal være logget ind for at gøre visninger til favoritter", + "You need an AI agent to start a conversation." : "Du har brug for en AI-agent for at starte en samtale.", + "Your Fireworks AI API key. Get one at" : "Din Fireworks AI API-nøgle. Få en på", + "Your OpenAI API key. Get one at" : "Din OpenAI API-nøgle. Få en på", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Din feedback er blevet registreret. Du kan eventuelt angive yderligere detaljer her...", + "Zookeeper Hosts" : "Zookeeper-værter", + "Zookeeper Port" : "Zookeeper-port", + "Zookeeper Settings (SolrCloud)" : "Zookeeper-indstillinger (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper-forbindelsesstreng til SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper-portnummer (valgfri, standard er 2181)", + "chunk" : "bidde", + "chunks" : "bidder", + "chunks processed" : "bidder behandlet", + "configuration(s)" : "konfiguration(er)", + "e.g., lib/Settings/config.json" : "f.eks. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "f.eks. lib/Settings/register.json", + "failed" : "mislykkedes", + "file" : "fil", + "files" : "filer", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "medlemmer", + "menu or contact someone with permission to create agents." : "menuen eller kontakt en med tilladelse til at oprette agenter.", + "messages" : "beskeder", + "n8n URL and API key are required" : "n8n-URL og API-nøgle er påkrævet", + "n8n configuration saved successfully" : "n8n-konfiguration gemt", + "n8n connection not configured" : "n8n-forbindelse ikke konfigureret", + "n8n connection successful" : "n8n-forbindelse lykkedes", + "n8n connection test successful" : "n8n-forbindelsestest lykkedes", + "n8n integration disabled" : "n8n-integration deaktiveret", + "n8n integration enabled" : "n8n-integration aktiveret", + "n8n project initialized successfully" : "n8n-projekt initialiseret", + "n8n settings saved successfully" : "n8n-indstillinger gemt", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekter", + "objects processed" : "objekter behandlet", + "or pick a range" : "eller vælg et interval", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "giver en god balance mellem hastighed og nøjagtighed.", + "register(s)" : "register(re)", + "register(s) selected" : "register(re) valgt", + "required" : "påkrævet", + "results" : "resultater", + "schema(s)" : "skema(er)", + "schema(s) selected" : "skema(er) valgt", + "sk-..." : "sk-...", + "this application" : "denne applikation", + "to" : "til", + "vectorized" : "vektoriseret", + "views selected" : "visninger valgt", + "ℹ️ Current Configuration" : "ℹ️ Nuværende konfiguration", + "⚡ Batch Processing" : "⚡ Batchbehandling", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Omfattende kontekst ✓ Mindre sandsynligt at gå glip af detaljer ✗ Langsommere svar ✗ Kan inkludere mindre relevant information", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Hurtigere svar ✓ Mere fokuserede svar ✗ Kan gå glip af relevant information", + "✨ AI Features" : "✨ AI-funktioner", + "All search trails cleared successfully" : "Alle søgespor ryddet", + "Multiple search trail deletion not implemented yet" : "Sletning af flere søgespor er endnu ikke implementeret", + "No expired search trails found to clear" : "Ingen udløbne søgespor fundet at rydde", + "Missing message" : "Manglende besked", + "message content is required" : "beskedindhold er påkrævet", + "Missing conversation or agentUuid" : "Manglende samtale eller agentUuid", + "Access denied" : "Adgang nægtet", + "Conversation not found" : "Samtale ikke fundet", + "AI service not configured" : "AI-tjeneste ikke konfigureret", + "Failed to process message" : "Kunne ikke behandle besked", + "Missing conversationId" : "Manglende conversationId", + "conversationId is required" : "conversationId er påkrævet", + "Failed to fetch conversation history" : "Kunne ikke hente samtalehistorik", + "Failed to clear conversation" : "Kunne ikke rydde samtale", + "Invalid feedback type" : "Ugyldig feedbacktype", + "type must be \"positive\" or \"negative\"" : "type skal være \"positive\" eller \"negative\"", + "Message not found" : "Besked ikke fundet", + "Failed to save feedback" : "Kunne ikke gemme feedback", + "Failed to get chat statistics" : "Kunne ikke hente chatstatistik", + "Not Found" : "Ikke fundet", + "Engine not found" : "Motor ikke fundet", + "Consumer not found" : "Forbruger ikke fundet", + "Backend parameter is required" : "Backend-parameter er påkrævet", + "Failed to get database information: %s" : "Kunne ikke hente databaseoplysninger: %s", + "SOLR setup error: %s" : "SOLR-opsætningsfejl: %s", + "Reindex failed: %s" : "Genindeksering mislykkedes: %s", + "Anonymized" : "Anonymiseret", + "Entities detected" : "Entiteter registreret", + "Extract Now" : "Udtræk nu", + "Extracted at" : "Udtrukket den", + "Extraction" : "Udtrækning", + "Failed to load extraction data" : "Kunne ikke indlæse udtrækningsdata", + "Failed to load register data" : "Kunne ikke indlæse registerdata", + "Failed to retrieve extraction status." : "Kunne ikke hente udtrækningsstatus.", + "Failed to retrieve objects for file." : "Kunne ikke hente objekter for fil.", + "No extraction data available for this file" : "Ingen udtrækningsdata tilgængelige for denne fil", + "No register objects reference this file" : "Ingen registerobjekter refererer til denne fil", + "Not extracted" : "Ikke udtrukket", + "Register Objects" : "Registerobjekter", + "Risk level" : "Risikoniveau", + "Text chunks" : "Tekstbidder", + "Unknown error" : "Ukendt fejl", + "Very high" : "Meget høj", + "{title} in {register} / {schema}" : "{title} i {register} / {schema}", + "Push Notifications" : "Push-notifikationer", + "Real-time push notification status via notify_push" : "Status for realtids push-notifikationer via notify_push", + "Realtime push not available — the notify_push app is not installed" : "Realtids push ikke tilgængelig — appen notify_push er ikke installeret", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installér appen notify_push fra Nextcloud App Store for at aktivere realtidsopdateringer.", + "Open Nextcloud App Store" : "Åbn Nextcloud App Store", + "notify_push is installed but not yet active" : "notify_push er installeret, men endnu ikke aktiv", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push er installeret, men OpenRegister har endnu ikke bekræftet en vellykket push. Udløs en objektgemning for at aktivere, eller tjek din notify_push-konfiguration.", + "notify_push configuration guide" : "notify_push-konfigurationsvejledning", + "Realtime push active" : "Realtids push aktiv", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Realtids push-notifikationer er aktive. Tilsluttede klienter modtager øjeblikkelige opdateringer, når objekter oprettes, opdateres eller slettes.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/da.json b/l10n/da.json new file mode 100644 index 0000000000..a056d528ae --- /dev/null +++ b/l10n/da.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin påkrævet)", + "(no subject)": "(intet emne)", + "(no title)": "(ingen titel)", + "(unnamed)": "(uden navn)", + "(untitled card)": "(kort uden titel)", + "({count} selected)": "({count} valgt)", + ", {failed} failed": ", {failed} mislykkedes", + "0 = unlimited": "0 = ubegrænset", + "A deactivation request is pending.": "En anmodning om deaktivering afventer.", + "AI Assistant": "AI-assistent", + "AI Chat": "AI-chat", + "AI service not configured": "AI-tjeneste er ikke konfigureret", + "API Key": "API-nøgle", + "API Request Quota (requests/day)": "API-anmodningskvote (anmodninger/dag)", + "API Token Configuration": "Konfiguration af API-token", + "API Tokens": "API-tokens", + "API tokens saved successfully": "API-tokens blev gemt", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: denne rapport sammenkæder hver verwerkingsactiviteit med de samlede revisionssporstællinger, der er knyttet til den. Revisorer og Autoriteit Persoonsgegevens anvender dette til tilsynsmæssig gennemgang.", + "About ConfigSets": "Om ConfigSets", + "Access denied": "Adgang nægtet", + "Access organisation dashboard and analytics": "Få adgang til organisationens dashboard og analyser", + "Account": "Konto", + "Action": "Handling", + "Action Distribution": "Handlingsfordeling", + "Action:": "Handling:", + "Actions": "Handlinger", + "Active": "Aktiv", + "Active Collections": "Aktive samlinger", + "Active Filters": "Aktive filtre", + "Active Organisation:": "Aktiv organisation:", + "Active Organisations": "Aktive organisationer", + "Active filters:": "Aktive filtre:", + "Active organisation changed successfully": "Den aktive organisation blev ændret", + "Active organisation set successfully": "Den aktive organisation blev angivet", + "Activities": "Aktiviteter", + "Activity": "Aktivitet", + "Activity Period": "Aktivitetsperiode", + "Add": "Tilføj", + "Add Attachment": "Tilføj vedhæftet fil", + "Add Endpoint": "Tilføj slutpunkt", + "Add Groups": "Tilføj grupper", + "Add Property to '{title}'": "Tilføj egenskab til '{title}'", + "Add Register": "Tilføj register", + "Add Schema": "Tilføj skema", + "Add Source": "Tilføj kilde", + "Add User": "Tilføj bruger", + "Add User to Organisation": "Tilføj bruger til organisation", + "Add a contact from any of your address books to associate it with this object.": "Tilføj en kontakt fra en af dine adressebøger for at knytte den til dette objekt.", + "Add a file or files": "Tilføj en fil eller filer", + "Add card": "Tilføj kort", + "Add contact": "Tilføj kontakt", + "Add endpoint": "Tilføj slutpunkt", + "Add oneOf entry": "Tilføj oneOf-post", + "Add range": "Tilføj interval", + "Add schema titles, descriptions, and register information to provide richer context for search": "Tilføj skematitler, beskrivelser og registeroplysninger for at give en rigere kontekst til søgning", + "Add to favorites": "Føj til favoritter", + "Added": "Tilføjet", + "Added to favorites": "Føjet til favoritter", + "Additional Information": "Yderligere oplysninger", + "Additional feedback saved. Thank you!": "Yderligere feedback blev gemt. Tak!", + "Addresses": "Adresser", + "Admin": "Administrator", + "Advanced": "Avanceret", + "Advanced Filters": "Avancerede filtre", + "Advanced Options": "Avancerede indstillinger", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Avancerede filtre er ikke tilgængelige ved brug af databasekilde. Skift til Auto eller SOLR-indeks for filtreringsmuligheder.", + "After": "Efter", + "Agent Type": "Agenttype", + "Agent deleted successfully": "Agenten blev slettet", + "Agent successfully created": "Agenten blev oprettet", + "Agent successfully updated": "Agenten blev opdateret", + "Agents": "Agenter", + "Aggregated across schemas": "Aggregeret på tværs af skemaer", + "All": "Alle", + "All Categories": "Alle kategorier", + "All Collections": "Alle samlinger", + "All Confidence Levels": "Alle konfidensniveauer", + "All ConfigSets": "Alle ConfigSets", + "All Files": "Alle filer", + "All Levels": "Alle niveauer", + "All Types": "Alle typer", + "All Webhooks": "Alle webhooks", + "All actions": "Alle handlinger", + "All audit trails cleared successfully": "Alle revisionsspor blev ryddet", + "All clear": "Alt i orden", + "All file types": "Alle filtyper", + "All registers": "Alle registre", + "All schemas": "Alle skemaer", + "All search trails cleared successfully": "Alle søgespor blev ryddet", + "All searches": "Alle søgninger", + "All users": "Alle brugere", + "All-day events": "Heldagsbegivenheder", + "Allowed MIME Types": "Tilladte MIME-typer", + "Already linked": "Allerede tilknyttet", + "An error occurred while clearing audit trails": "Der opstod en fejl under rydning af revisionsspor", + "An error occurred while deleting the audit trail": "Der opstod en fejl under sletning af revisionssporet", + "An error occurred while deleting the object": "Der opstod en fejl under sletning af objektet", + "An error occurred while deleting the view": "Der opstod en fejl under sletning af visningen", + "An error occurred while permanently deleting the objects": "Der opstod en fejl under permanent sletning af objekterne", + "An error occurred while restoring the objects": "Der opstod en fejl under gendannelse af objekterne", + "An error occurred while saving the organisation": "Der opstod en fejl under lagring af organisationen", + "Analysis completed:": "Analyse fuldført:", + "Analysis steps:": "Analysetrin:", + "Analytics": "Analyser", + "Analyze Objects": "Analysér objekter", + "Analyze Properties": "Analysér egenskaber", + "Analyze existing properties for improvement opportunities": "Analysér eksisterende egenskaber for forbedringsmuligheder", + "Analyzing...": "Analyserer...", + "Anonymized": "Anonymiseret", + "Anonymous Group": "Anonym gruppe", + "Any favorites and sharing settings for this view": "Alle favoritter og delingsindstillinger for denne visning", + "Any user": "Enhver bruger", + "App ID": "App-id", + "App store cache invalidated successfully": "App store-cachen blev invalideret", + "App store cache invalidated: {invalidated}": "App store-cache invalideret: {invalidated}", + "Application deleted successfully": "Applikationen blev slettet", + "Application identifier for this configuration (optional)": "Applikationsidentifikator for denne konfiguration (valgfri)", + "Application settings": "Applikationsindstillinger", + "Application successfully created": "Applikationen blev oprettet", + "Application successfully updated": "Applikationen blev opdateret", + "Applications": "Applikationer", + "Apply": "Anvend", + "Apply Changes": "Anvend ændringer", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Anvend en rolle på alle skemaer i dette register, der ikke har eksplicitte autorisationstilsidesættelser.", + "Approval Progress": "Godkendelsesforløb", + "Approve": "Godkend", + "Approved": "Godkendt", + "Archive": "Arkivér", + "Archive conversation": "Arkivér samtale", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arkivér denne verwerkingsactiviteit? Revisionssporrækker vil fortsat henvise til den.", + "Archived conversations are hidden from your active list": "Arkiverede samtaler skjules fra din aktive liste", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Er du sikker på, at du vil SLETTE samlingen \"{name}\"?\n\nDette vil:\n• Slette samlingen og alle dens data permanent\n• Fjerne alle indekserede dokumenter\n• Denne handling kan ikke fortrydes", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Er du sikker på, at du vil rydde gamle søgespor op? Dette vil slette poster, der er ældre end 30 dage.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Er du sikker på, at du vil rydde alle data fra samlingen \"{name}\"?\n\nDette vil:\n• Slette alle indekserede dokumenter\n• Bevare samlingsstrukturen intakt\n• Denne handling kan ikke fortrydes", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Er du sikker på, at du vil slette applikationen \"{name}\"? Denne handling kan ikke fortrydes.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Er du sikker på, at du vil slette de valgte revisionsspor? Denne handling kan ikke fortrydes.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Er du sikker på, at du vil slette de valgte søgespor? Denne handling kan ikke fortrydes.", + "Are you sure you want to delete this ConfigSet?": "Er du sikker på, at du vil slette denne ConfigSet?", + "Are you sure you want to delete this configset?": "Er du sikker på, at du vil slette denne configset?", + "Are you sure you want to leave '{name}'?": "Er du sikker på, at du vil forlade '{name}'?", + "Are you sure you want to permanently delete": "Er du sikker på, at du vil slette permanent", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Er du sikker på, at du vil genindeksere samlingen \"{name}\"?\n\nDette vil:\n• Genopbygge indekset med alle objekter\n• Tage flere minutter at fuldføre\n• Kan påvirke søgeydelsen under genindekseringen", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Er du sikker på, at du vil fjerne skemaet \"{schema}\" fra registret \"{register}\"? Denne handling kan ikke fortrydes.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Er du sikker på, at du vil blødt slette {count} aktive objekter for skemaet \"{schema}\"?\n\nDe vil blive markeret som slettede, men kan fjernes permanent senere.", + "Array Object Configuration:": "Konfiguration af array-objekt:", + "Articles": "Artikler", + "Ask a question...": "Stil et spørgsmål...", + "Ask questions about your data using natural language": "Stil spørgsmål om dine data ved hjælp af naturligt sprog", + "Assigned collaborative tags": "Tildelte samarbejdsmærker", + "Assignment notifications": "Tildelingsnotifikationer", + "Attachment added to {name}": "Vedhæftet fil tilføjet til {name}", + "Attempt": "Forsøg", + "Audit Trail Changes": "Ændringer i revisionsspor", + "Audit Trail Details": "Detaljer om revisionsspor", + "Audit Trail Management": "Administration af revisionsspor", + "Audit Trail Statistics": "Statistik for revisionsspor", + "Audit Trails": "Revisionsspor", + "Audit trail data copied to clipboard": "Revisionssporsdata kopieret til udklipsholderen", + "Audit trail deleted successfully": "Revisionssporet blev slettet", + "Audit trail successfully deleted": "Revisionssporet blev slettet", + "Authentication": "Godkendelse", + "Auto (e.g. Y for year, F Y for month)": "Auto (f.eks. Y for år, F Y for måned)", + "Auto share": "Automatisk deling", + "Auto-commit disabled": "Automatisk commit deaktiveret", + "Auto-commit enabled": "Automatisk commit aktiveret", + "Auto-create Default Organisation": "Opret standardorganisation automatisk", + "Auto-retry failed vectorizations": "Forsøg automatisk mislykkede vektoriseringer igen", + "Automatically commit changes to SOLR index": "Commit ændringer til SOLR-indekset automatisk", + "Automatically connect to the federation network.": "Opret automatisk forbindelse til føderationsnetværket.", + "Automatically create a default organisation if none exists when the app is initialized": "Opret automatisk en standardorganisation, hvis ingen findes, når appen initialiseres", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Generér automatisk vektorindlejringer fra tekststykker, når filer uploades og behandles", + "Automatically generate vector embeddings when objects are created or updated": "Generér automatisk vektorindlejringer, når objekter oprettes eller opdateres", + "Automatically retry failed vectorization attempts (max 3 retries)": "Forsøg automatisk mislykkede vektoriseringsforsøg igen (maks. 3 forsøg)", + "Automatically update catalogues.": "Opdater kataloger automatisk.", + "Available Filters": "Tilgængelige filtre", + "Available Workflows": "Tilgængelige arbejdsgange", + "Available placeholders:": "Tilgængelige pladsholdere:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Ændringer af avatar understøttes ikke af din godkendelsesudbyder.", + "Avatar removed": "Avatar fjernet", + "Avatar updated successfully": "Avataren blev opdateret", + "Avg Execution Time": "Gns. udførelsestid", + "Avg Members/Org": "Gns. medlemmer/org.", + "Avg Object Views/Session": "Gns. objektvisninger/session", + "Avg Response Time": "Gns. svartid", + "Avg Results/Search": "Gns. resultater/søgning", + "Avg Searches/Session": "Gns. søgninger/session", + "Back": "Tilbage", + "Back to Entities": "Tilbage til entiteter", + "Back to Registers": "Tilbage til registre", + "Back to Webhooks": "Tilbage til webhooks", + "Back to applications": "Tilbage til applikationer", + "Back to entities": "Tilbage til entiteter", + "Backend parameter is required": "Backend-parameter er påkrævet", + "Backend updated successfully. Please reload the application.": "Backend blev opdateret. Genindlæs venligst applikationen.", + "Bandwidth Quota (MB/month)": "Båndbreddekvote (MB/måned)", + "Base URL (Optional)": "Basis-URL (valgfri)", + "Basic Information": "Grundlæggende oplysninger", + "Batch Size": "Batchstørrelse", + "Batch extraction completed": "Batch-udtræk fuldført", + "Before": "Før", + "Before object vectorization can work:": "Før objektvektorisering kan fungere:", + "Behavior": "Adfærd", + "Behavior Issue": "Adfærdsproblem", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601-varighed, f.eks. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob-lagring er udfaset. Alle objekter bruger nu magic tables.", + "Bookmarks": "Bogmærker", + "Branch": "Gren", + "Bulk Role Assignment: {title}": "Masse-rolletildeling: {title}", + "Bulk delete operation completed successfully": "Massesletning blev fuldført", + "Bulk save operation completed successfully": "Masselagring blev fuldført", + "Business Data": "Forretningsdata", + "CSV": "CSV", + "Cache cleared successfully": "Cachen blev ryddet", + "Cache grew from {old} to {new} entries.": "Cachen voksede fra {old} til {new} poster.", + "Cache shrunk from {old} to {new} entries.": "Cachen blev reduceret fra {old} til {new} poster.", + "Cache stayed the same at {size} entries.": "Cachen forblev uændret på {size} poster.", + "Calculate Sizes": "Beregn størrelser", + "Calendar": "Kalender", + "Calendar Provider Configuration": "Konfiguration af kalenderudbyder", + "Calendar integration is not available": "Kalenderintegration er ikke tilgængelig", + "Calendar name": "Kalendernavn", + "Cancel": "Annuller", + "Cancel deactivation request": "Annuller anmodning om deaktivering", + "Cannot delete: objects are still attached": "Kan ikke slette: objekter er stadig tilknyttet", + "Cannot edit: This register is managed by external configuration {title}": "Kan ikke redigere: Dette register administreres af ekstern konfiguration {title}", + "Cannot remove schema with existing objects ({count} objects)": "Kan ikke fjerne skema med eksisterende objekter ({count} objekter)", + "Cannot remove schema {schema} because it contains {count} objects": "Kan ikke fjerne skemaet {schema}, fordi det indeholder {count} objekter", + "Cards": "Kort", + "Cascade delete": "Kaskadesletning", + "Categories of data subjects (one per line)": "Kategorier af registrerede (én pr. linje)", + "Categories of personal data (one per line)": "Kategorier af personoplysninger (én pr. linje)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategori", + "Change Type": "Ændringstype", + "Change password": "Skift adgangskode", + "Changes": "Ændringer", + "Changes copied to clipboard": "Ændringer kopieret til udklipsholderen", + "Changes for Audit Trail #{id}": "Ændringer for revisionsspor #{id}", + "Chat Model": "Chatmodel", + "Chat Provider": "Chatudbyder", + "Chat Provider (RAG)": "Chatudbyder (RAG)", + "Chat Settings": "Chatindstillinger", + "Chat provider connection successful!": "Forbindelsen til chatudbyder lykkedes!", + "Chat settings": "Chatindstillinger", + "Chat test failed: {error}": "Chattest mislykkedes: {error}", + "Choose a register": "Vælg et register", + "Choose a schema": "Vælg et skema", + "Choose how vector similarity calculations are performed for semantic search": "Vælg, hvordan beregninger af vektorlighed udføres til semantisk søgning", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Vælg, hvilke filtyper der skal inkluderes i vektoriseringsprocessen. Kun filer med udtrukket tekst og stykker behandles.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Vælg, hvilke visninger der skal inkluderes i vektoriseringsprocessen. Lad feltet være tomt for at behandle alle visninger baseret på din konfiguration.", + "Chunk": "Stykke", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Sletning af stykker er endnu ikke implementeret. Brug stykbaserede slutpunkter.", + "Chunks": "Stykker", + "Chunks to Vectorize:": "Stykker, der skal vektoriseres:", + "Cleanup Old Trails": "Ryd gamle spor op", + "Cleanup completed": "Oprydning fuldført", + "Cleanup completed successfully. Deleted {count} entries.": "Oprydningen blev fuldført. {count} poster blev slettet.", + "Cleanup failed: {error}": "Oprydning mislykkedes: {error}", + "Clear All Embeddings": "Ryd alle indlejringer", + "Clear Entries": "Ryd poster", + "Clear Filtered Audit Trails": "Ryd filtrerede revisionsspor", + "Clear Filters": "Ryd filtre", + "Clear Index": "Ryd indeks", + "Clear Selection": "Ryd markering", + "Clear all": "Ryd alle", + "Clear all filters": "Ryd alle filtre", + "Clear date range": "Ryd datointerval", + "Clear filters": "Ryd filtre", + "Clearing...": "Rydder...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Klik på Kør overholdelsesscanning for at finde skemaer, hvor PII er registreret, men hvor der ikke findes nogen annotering af verwerkingsactiviteit.", + "Click to subscribe to notifications": "Klik for at abonnere på notifikationer", + "Click to unsubscribe from notifications": "Klik for at afmelde notifikationer", + "Close": "Luk", + "Cluster name": "Klyngenavn", + "Code": "Kode", + "Code (short readable key, e.g. v-2026-001)": "Kode (kort læsbar nøgle, f.eks. v-2026-001)", + "Collapse details": "Skjul detaljer", + "Collection Management": "Administration af samlinger", + "Collection Name": "Samlingsnavn", + "Collection assignments updated successfully": "Samlingstildelinger blev opdateret", + "Collection cleared successfully": "Samlingen blev ryddet", + "Collection copied successfully": "Samlingen blev kopieret", + "Collection created successfully": "Samlingen blev oprettet", + "Collection deleted successfully": "Samlingen blev slettet", + "Collection used to store and index file metadata and content": "Samling, der bruges til at lagre og indeksere filmetadata og -indhold", + "Collection used to store and index object data": "Samling, der bruges til at lagre og indeksere objektdata", + "Collections:": "Samlinger:", + "Color": "Farve", + "Comment...": "Kommentar...", + "Commit Message": "Commit-besked", + "Commit Within (ms)": "Commit inden for (ms)", + "Commit message": "Commit-besked", + "Compare current schema with real object data": "Sammenlign det aktuelle skema med rigtige objektdata", + "Completed": "Fuldført", + "Complex": "Kompleks", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Komplekse forespørgsler: Avancerede søgninger med flere filtre, operatorer og komplekse parameterkombinationer", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Komplekse forespørgsler: avancerede søgninger med flere filtre, operatorer og komplekse parameterkombinationer", + "Compliance": "Overholdelse", + "Confidence Level": "Konfidensniveau", + "Confidence Score": "Konfidensscore", + "Confidence: {confidence}%": "Konfidens: {confidence}%", + "Config must be provided as an object": "Konfiguration skal angives som et objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Administration af ConfigSet", + "ConfigSet Name": "ConfigSet-navn", + "ConfigSet created successfully": "ConfigSet blev oprettet", + "ConfigSet deleted successfully": "ConfigSet blev slettet", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets definerer skemaet og konfigurationen for dine SOLR-samlinger. De indeholder feltdefinitioner, analysatorer og andre søgeindstillinger.", + "Configuration": "Konfiguration", + "Configuration Data": "Konfigurationsdata", + "Configuration is up to date": "Konfigurationen er opdateret", + "Configuration saved": "Konfiguration gemt", + "Configuration saved successfully.": "Konfigurationen blev gemt.", + "Configuration type (default, application, etc.)": "Konfigurationstype (standard, applikation osv.)", + "Configurations": "Konfigurationer", + "Configure API tokens for external service integrations": "Konfigurer API-tokens til integrationer med eksterne tjenester", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurer Apache SOLR-søgemaskinen til avancerede søgefunktioner", + "Configure Facets": "Konfigurer facetter", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurer udbydere af store sprogmodeller (LLM) til AI-drevne funktioner, herunder semantisk søgning, indlejringer og chat.", + "Configure SOLR Facets": "Konfigurer SOLR-facetter", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurer grundlæggende forbindelsesindstillinger for din SOLR-server, herunder godkendelse og netværksindstillinger. Brug de separate dialoger til administration af ConfigSet og samlinger til at administrere kerner og samlinger.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurer grundlæggende forbindelsesindstillinger for din SOLR-server, herunder godkendelse og netværksindstillinger. Brug de separate dialoger til administration af configset og samlinger til at administrere kerner og samlinger.", + "Configure default organisation and organisation-related settings": "Konfigurer standardorganisation og organisationsrelaterede indstillinger", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurer, hvordan databaseobjekter konverteres til vektorindlejringer til semantisk søgning. Objekter vektoriseres direkte uden behov for tekstudtræk.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurer, hvordan objekter konverteres til tekst før vektorisering. Disse indstillinger påvirker søgekvalitet og kontekst.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurer udbydere af store sprogmodeller (LLM) til AI-drevne funktioner, herunder semantisk søgning, indlejringer og chat.", + "Configure multi-organization support and tenant isolation": "Konfigurer understøttelse af flere organisationer og lejerisolering", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurer parametre for filvektorisering. Denne proces genererer vektorindlejringer for alle udtrukne filstykker.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurer parametre for objektvektorisering. Denne proces genererer vektorindlejringer for alle objekter, der matcher dine visningsfiltre.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurer dette skema til at vise objekter som begivenheder i Nextcloud Kalender-appen.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurer, hvilke datatyper der skal søges i, og hvor mange kilder der skal hentes", + "Configuring field mappings...": "Konfigurerer feltmapninger...", + "Confirm Account Deactivation": "Bekræft deaktivering af konto", + "Confirm Object Deletion": "Bekræft sletning af objekt", + "Confirm Object Restoration": "Bekræft gendannelse af objekt", + "Confirm Permanent Object Deletion": "Bekræft permanent sletning af objekt", + "Confirm deactivation": "Bekræft deaktivering", + "Connecting to SOLR cluster...": "Opretter forbindelse til SOLR-klynge...", + "Connection Failed": "Forbindelsen mislykkedes", + "Connection Settings": "Forbindelsesindstillinger", + "Connection Successful!": "Forbindelsen lykkedes!", + "Connection failed": "Forbindelsen mislykkedes", + "Connection protocol": "Forbindelsesprotokol", + "Connection successful": "Forbindelsen lykkedes", + "Connection timeout in seconds": "Forbindelsestimeout i sekunder", + "Constant delay between retries (5 minutes)": "Konstant forsinkelse mellem forsøg (5 minutter)", + "Constraint Issue": "Begrænsningsproblem", + "Consumer not found": "Forbruger ikke fundet", + "Contacts": "Kontakter", + "Contacts integration is not available": "Kontaktintegration er ikke tilgængelig", + "Content Filters": "Indholdsfiltre", + "Continue to Properties": "Fortsæt til egenskaber", + "Contracts": "Kontrakter", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Styr, hvilke objektvisninger der skal vektoriseres for at reducere API-omkostninger. Vektorisér kun visninger, der drager fordel af semantisk søgning.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Styr, hvilke visninger og værktøjer AI'en kan bruge i denne samtale. Som standard er alle agentfunktioner aktiveret.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Styrer tilfældighed (0 = deterministisk, 2 = meget kreativ)", + "Conversation ID is required": "Samtale-id er påkrævet", + "Conversation archived": "Samtale arkiveret", + "Conversation archived successfully": "Samtalen blev arkiveret", + "Conversation cleared successfully": "Samtalen blev ryddet", + "Conversation deleted": "Samtale slettet", + "Conversation not found": "Samtale ikke fundet", + "Conversation permanently deleted": "Samtale slettet permanent", + "Conversation renamed": "Samtale omdøbt", + "Conversation restored": "Samtale gendannet", + "Conversation started with {agent}": "Samtale startet med {agent}", + "Conversation title": "Samtaletitel", + "Conversations": "Samtaler", + "Converting {schema} to blob storage...": "Konverterer {schema} til blob-lagring...", + "Converting {schema} to magic table...": "Konverterer {schema} til magic table...", + "Copied!": "Kopieret!", + "Copy": "Kopiér", + "Copy Changes": "Kopiér ændringer", + "Copy Collection": "Kopiér samling", + "Copy Data": "Kopiér data", + "Copy Full Data": "Kopiér alle data", + "Copy of {name}": "Kopi af {name}", + "Copy to clipboard": "Kopiér til udklipsholder", + "Copying...": "Kopierer...", + "Costs": "Omkostninger", + "Count": "Antal", + "Counting objects...": "Tæller objekter...", + "Create": "Opret", + "Create API Token": "Opret API-token", + "Create Agent": "Opret agent", + "Create Application": "Opret applikation", + "Create Collection": "Opret samling", + "Create ConfigSet": "Opret ConfigSet", + "Create New Collection": "Opret ny samling", + "Create New ConfigSet": "Opret nyt ConfigSet", + "Create Operations": "Opret handlinger", + "Create Organisation": "Opret organisation", + "Create Webhook": "Opret webhook", + "Create a copy of collection:": "Opret en kopi af samlingen:", + "Create a new ConfigSet based on the _default template": "Opret et nyt ConfigSet baseret på _default-skabelonen", + "Create a new SOLR collection from an existing ConfigSet": "Opret en ny SOLR-samling fra et eksisterende ConfigSet", + "Create a new SOLR collection from an existing configset": "Opret en ny SOLR-samling fra et eksisterende configset", + "Create a new configset based on the _default template": "Opret et nyt configset baseret på _default-skabelonen", + "Create a new event or link an existing one from any of your calendars.": "Opret en ny begivenhed, eller tilknyt en eksisterende fra en af dine kalendere.", + "Create another": "Opret en ny", + "Create copies of": "Opret kopier af", + "Create event": "Opret begivenhed", + "Create new token": "Opret nyt token", + "Create or link a Deck card to track work on this object.": "Opret eller tilknyt et Deck-kort for at spore arbejdet med dette objekt.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Opret den første verwerkingsactiviteit for at begynde at tagge revisionsspor-rækker med deres AVG Art 30-tilskrivning.", + "Create your first AI agent to get started.": "Opret din første AI-agent for at komme i gang.", + "Created": "Oprettet", + "Created:": "Oprettet:", + "Creating search collections...": "Opretter søgesamlinger...", + "Creating...": "Opretter...", + "Current": "Aktuel", + "Current Filters": "Aktuelle filtre", + "Current Type:": "Aktuel type:", + "Current password": "Nuværende adgangskode", + "Custom API endpoint if using a different region": "Brugerdefineret API-endpoint, hvis du bruger en anden region", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Brugerdefinerede HTTP-headers (en pr. linje, format: Header-Name: værdi)", + "Custom HTTP headers (one per line, format: header-name: value)": "Brugerdefinerede HTTP-headers (en pr. linje, format: header-name: værdi)", + "Custom Ranges:": "Brugerdefinerede intervaller:", + "Custom display title for this facet": "Brugerdefineret visningstitel for denne facet", + "Custom pattern:": "Brugerdefineret mønster:", + "DELETE COLLECTION": "SLET SAMLING", + "DSAR": "DSAR", + "Daily": "Dagligt", + "Dashboard": "Dashboard", + "Dashboard not found": "Dashboard ikke fundet", + "Data": "Data", + "Data Source": "Datakilde", + "Data Sources": "Datakilder", + "Data Views": "Datavisninger", + "Data sources": "Datakilder", + "Data type does not match observed values": "Datatypen stemmer ikke overens med de observerede værdier", + "Data-subject access request": "Anmodning om indsigt fra den registrerede", + "Database URL": "Database-URL", + "Database information refreshed": "Databaseoplysninger opdateret", + "Date Faceting:": "Datofacettering:", + "Date Range": "Datointerval", + "Dates": "Datoer", + "Deactivation request cancelled": "Anmodning om deaktivering annulleret", + "Deactivation request submitted": "Anmodning om deaktivering indsendt", + "Deck": "Deck", + "Deck integration is not available": "Deck-integration er ikke tilgængelig", + "Default": "Standard", + "Default Object Tenant": "Standard objekt-tenant", + "Default Organisation": "Standardorganisation", + "Default Owner": "Standardejer", + "Default User (for cron/background jobs)": "Standardbruger (til cron-/baggrundsjob)", + "Default User Tenant": "Standard bruger-tenant", + "Default value": "Standardværdi", + "Default value (Email)": "Standardværdi (e-mail)", + "Default value (Password)": "Standardværdi (adgangskode)", + "Default value (Phone number)": "Standardværdi (telefonnummer)", + "Default value (Regex)": "Standardværdi (regex)", + "Default: 5": "Standard: 5", + "Defaults to current user. Select a different user if needed.": "Standard er den aktuelle bruger. Vælg en anden bruger om nødvendigt.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Forsinkelserne fordobles ved hvert forsøg (2, 4, 8 minutter...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Forsinkelserne stiger lineært (5, 10, 15 minutter...)", + "Delete": "Slet", + "Delete ({count})": "Slet ({count})", + "Delete Application": "Slet applikation", + "Delete Audit Trail": "Slet revisionsspor", + "Delete Collection": "Slet samling", + "Delete ConfigSet": "Slet ConfigSet", + "Delete Objects": "Slet objekter", + "Delete Operations": "Slet handlinger", + "Delete SOLR Collection": "Slet SOLR-samling", + "Delete View": "Slet visning", + "Delete all objects in this schema": "Slet alle objekter i dette skema", + "Delete permanently": "Slet permanent", + "Delete this conversation?": "Slet denne samtale?", + "Delete view": "Slet visning", + "Delete {count} object": "Slet {count} objekt", + "Delete {count} objects": "Slet {count} objekter", + "Deleted": "Slettet", + "Deleted By": "Slettet af", + "Deleted Date": "Slettedato", + "Deleted Items Management": "Administration af slettede elementer", + "Deleted This Week": "Slettet denne uge", + "Deleted Today": "Slettet i dag", + "Deleted:": "Slettet:", + "Deleting SOLR Collection...": "Sletter SOLR-samling...", + "Deleting...": "Sletter...", + "Deletion Date Range": "Datointerval for sletning", + "Deletion Statistics": "Sletningsstatistik", + "Deprecated": "Forældet", + "Depublish": "Afpublicer", + "Description": "Beskrivelse", + "Description Template": "Beskrivelsesskabelon", + "Description shown as tooltip": "Beskrivelse vist som værktøjstip", + "Description:": "Beskrivelse:", + "Detect data types and patterns": "Registrer datatyper og mønstre", + "Detected At": "Registreret kl.", + "Detected Format:": "Registreret format:", + "Detected Issues:": "Registrerede problemer:", + "Detected: {date}": "Registreret: {date}", + "Direct permission": "Direkte tilladelse", + "Disable": "Deaktiver", + "Disable this to make the agent": "Deaktiver dette for at gøre agenten", + "Disabled": "Deaktiveret", + "Discover Files": "Find filer", + "Discovered Properties": "Fundne egenskaber", + "Display Format": "Visningsformat", + "Display Name": "Visningsnavn", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Vil du permanent slette alle filtrerede revisionsspor-poster? Denne handling kan ikke fortrydes.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Vil du permanent slette denne revisionsspor-post? Denne handling kan ikke fortrydes.", + "Documents": "Dokumenter", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR aktiveret", + "Download API Spec": "Download API-spec", + "Download API Specification": "Download API-specifikation", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Download en kopi af alle dine personoplysninger, der er lagret i OpenRegister (GDPR artikel 20).", + "Drag a file or files here": "Træk en eller flere filer hertil", + "Dry run completed: {count} fields would be created": "Prøvekørsel afsluttet: {count} felter ville blive oprettet", + "Dry run completed: {count} fields would be fixed": "Prøvekørsel afsluttet: {count} felter ville blive rettet", + "Duration in seconds (optional)": "Varighed i sekunder (valgfrit)", + "Edit": "Rediger", + "Edit Agent": "Rediger agent", + "Edit Application": "Rediger applikation", + "Edit Configuration": "Rediger konfiguration", + "Edit Endpoint": "Rediger endpoint", + "Edit Organisation": "Rediger organisation", + "Edit Property '{key}' of '{title}'": "Rediger egenskaben '{key}' for '{title}'", + "Edit Register": "Rediger register", + "Edit Schema": "Rediger skema", + "Edit Source": "Rediger kilde", + "Edit View": "Rediger visning", + "Edit Webhook": "Rediger webhook", + "Edit labels": "Rediger etiketter", + "Edit processing activity": "Rediger behandlingsaktivitet", + "Edit tags for {name}": "Rediger tags for {name}", + "Edit verwerkingsactiviteit": "Rediger verwerkingsactiviteit", + "Edit view": "Rediger visning", + "Edit view details": "Rediger visningsoplysninger", + "Effectiveness": "Effektivitet", + "Email": "E-mail", + "Email Addresses": "E-mailadresser", + "Email digest frequency": "Hyppighed for e-mailoversigt", + "Emails": "E-mails", + "Embedding Model": "Embedding-model", + "Embedding Provider": "Embedding-udbyder", + "Embedding generated successfully": "Embedding genereret", + "Embedding provider connection successful!": "Forbindelse til embedding-udbyder lykkedes!", + "Embedding test failed: {error}": "Embedding-test mislykkedes: {error}", + "Enable": "Aktiver", + "Enable RAG": "Aktiver RAG", + "Enable auto-creation": "Aktiver automatisk oprettelse", + "Enable automatic file vectorization": "Aktiver automatisk filvektorisering", + "Enable automatic object vectorization": "Aktiver automatisk objektvektorisering", + "Enable automatic synchronization": "Aktiver automatisk synkronisering", + "Enable calendar provider": "Aktiver kalenderudbyder", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Aktiver detaljeret logning for SOLR-handlinger (anbefales til fejlfinding)", + "Enable faceting": "Aktiver facettering", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Aktiver eller deaktiver LLM-funktioner. Konfigurer udbydere og modeller med LLM-konfigurationsknappen ovenfor.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Aktiver eller deaktiver LLM-funktioner. Konfigurer udbydere og modeller med LLM-konfigurationsknappen ovenfor.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Aktiver eller deaktiver SOLR-søgeintegration. Konfigurer forbindelsesindstillinger med knappen Forbindelsesindstillinger ovenfor.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Aktiver eller deaktiver SOLR-søgeintegration. Konfigurer forbindelsesindstillinger med knappen til forbindelsesindstillinger ovenfor.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Aktiver eller deaktiver n8n-workflowintegration. Konfigurer forbindelsesindstillinger nedenfor.", + "Enable or disable this webhook": "Aktiver eller deaktiver denne webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Aktiver værktøjer, der gør det muligt for agenten at interagere med data via funktionskald.", + "Enable vectorization for all existing and future views (may increase costs)": "Aktiver vektorisering for alle eksisterende og fremtidige visninger (kan øge omkostningerne)", + "Enable write-back to target objects": "Aktiver tilbageskrivning til målobjekter", + "Enabled": "Aktiveret", + "End Date Field": "Slutdatofelt", + "Endpoint Path*": "Endpoint-sti*", + "Endpoints": "Endpoints", + "Engine not found": "Engine ikke fundet", + "Enter ConfigSet name": "Indtast ConfigSet-navn", + "Enter Nextcloud usernames to grant access to this private agent": "Indtast Nextcloud-brugernavne for at give adgang til denne private agent", + "Enter agent description (optional)": "Indtast agentbeskrivelse (valgfrit)", + "Enter agent name": "Indtast agentnavn", + "Enter application description (optional)": "Indtast applikationsbeskrivelse (valgfrit)", + "Enter application name": "Indtast applikationsnavn", + "Enter collection name": "Indtast samlingsnavn", + "Enter configuration description (optional)": "Indtast konfigurationsbeskrivelse (valgfrit)", + "Enter configuration title": "Indtast konfigurationstitel", + "Enter description (optional)...": "Indtast beskrivelse (valgfrit)...", + "Enter new collection name": "Indtast nyt samlingsnavn", + "Enter object ID": "Indtast objekt-ID", + "Enter organisation description (optional)": "Indtast organisationsbeskrivelse (valgfrit)", + "Enter organisation name": "Indtast organisationsnavn", + "Enter search term": "Indtast søgeord", + "Enter search terms or leave empty to browse all": "Indtast søgeord, eller lad feltet stå tomt for at gennemse alle", + "Enter system prompt for the agent": "Indtast systemprompt til agenten", + "Enter username and press Enter": "Indtast brugernavn, og tryk på Enter", + "Enter view name...": "Indtast visningsnavn...", + "Enter webhook name": "Indtast webhook-navn", + "Enter your API key": "Indtast din API-nøgle", + "Enter your object here...": "Indtast dit objekt her...", + "Enter your schema here...": "Indtast dit skema her...", + "Entities": "Entiteter", + "Entities detected": "Entiteter registreret", + "Entity ID: {id}": "Entitets-ID: {id}", + "Entity Information": "Entitetsoplysninger", + "Entity Recognition": "Entitetsgenkendelse", + "Entity Recognition Method": "Metode til entitetsgenkendelse", + "Entity deleted successfully": "Entitet slettet", + "Entity not found": "Entitet ikke fundet", + "Entries to be deleted:": "Poster, der skal slettes:", + "Enum Issue": "Enum-problem", + "Enum constraint is missing": "Enum-begrænsning mangler", + "Erase (Art 17)": "Slet (art. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Slet {count} objekt(er) for denne registrerede? Denne handling logges i revisionssporet.", + "Erasure complete": "Sletning fuldført", + "Erasure preview": "Forhåndsvisning af sletning", + "Error": "Fejl", + "Error Details": "Fejldetaljer", + "Error Information": "Fejloplysninger", + "Error Message": "Fejlmeddelelse", + "Error deleting audit trails: {error}": "Fejl ved sletning af revisionsspor: {error}", + "Error deleting search trails: {error}": "Fejl ved sletning af søgespor: {error}", + "Error loading application": "Fejl ved indlæsning af applikation", + "Error loading audit trails": "Fejl ved indlæsning af revisionsspor", + "Error loading entity": "Fejl ved indlæsning af entitet", + "Error loading search trails": "Fejl ved indlæsning af søgespor", + "Estimated Batches:": "Anslåede batches:", + "Estimated Cost:": "Anslået omkostning:", + "Estimated Duration:": "Anslået varighed:", + "Event": "Begivenhed", + "Event Property for Payload": "Begivenhedsegenskab for payload", + "Events": "Begivenheder", + "Every schema with detected PII has a processing-activity annotation.": "Alle skemaer med registrerede PII har en behandlingsaktivitets-annotering.", + "Example": "Eksempel", + "Example Value": "Eksempelværdi", + "Example value for this property": "Eksempelværdi for denne egenskab", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Eksklusivt maksimum", + "Exclusive Minimum": "Eksklusivt minimum", + "Exclusive maximum": "Eksklusivt maksimum", + "Exclusive minimum": "Eksklusivt minimum", + "Execution Mode": "Udførelsestilstand", + "Execution Time": "Udførelsestidspunkt", + "Execution Time Range": "Interval for udførelsestidspunkt", + "Existing Improvements": "Eksisterende forbedringer", + "Expand details": "Udvid detaljer", + "Expiration": "Udløb", + "Expires": "Udløber", + "Expires in (e.g., 90d)": "Udløber om (f.eks. 90d)", + "Exponential": "Eksponentiel", + "Export": "Eksporter", + "Export \"{schema}\" objects from \"{register}\"": "Eksporter \"{schema}\"-objekter fra \"{register}\"", + "Export Configuration": "Eksportkonfiguration", + "Export Format:": "Eksportformat:", + "Export Objects": "Eksporter objekter", + "Export completed successfully": "Eksport fuldført", + "Export downloaded successfully": "Eksport downloadet", + "Export failed: {error}": "Eksport mislykkedes: {error}", + "Export is rate limited. Please try again later.": "Eksport er hastighedsbegrænset. Prøv igen senere.", + "Export my data": "Eksporter mine data", + "Export, view, or delete audit trails": "Eksporter, vis eller slet revisionsspor", + "Exporting...": "Eksporterer...", + "Extend Schema": "Udvid skema", + "Extended": "Udvidet", + "Extensions": "Udvidelser", + "Extra Query Parameters": "Ekstra forespørgselsparametre", + "Extract Now": "Udtræk nu", + "Extract Pending Files": "Udtræk afventende filer", + "Extract now": "Udtræk nu", + "Extract properties from each object": "Udtræk egenskaber fra hvert objekt", + "Extracted At": "Udtrukket kl.", + "Extracted at": "Udtrukket kl.", + "Extraction": "Udtrækning", + "Extraction Mode": "Udtrækningstilstand", + "Extraction Scope": "Udtrækningsomfang", + "Extraction Status": "Udtrækningsstatus", + "Extraction error for {file}: {error}": "Udtrækningsfejl for {file}: {error}", + "Facet Description": "Facetbeskrivelse", + "Facet Order": "Facetrækkefølge", + "Facet Title": "Facettitel", + "Facet Type": "Facettype", + "Facet configuration updated successfully": "Facetkonfiguration opdateret", + "Facetable": "Facetbar", + "Faceting Configuration:": "Facetteringskonfiguration:", + "Facets discovered and configured successfully": "Facetter fundet og konfigureret", + "Facets discovered successfully": "Facetter fundet", + "Failed": "Mislykkedes", + "Failed to add attachment to object": "Kunne ikke tilføje vedhæftning til objektet", + "Failed to add user to organisation": "Kunne ikke tilføje bruger til organisationen", + "Failed to analyze schema properties": "Kunne ikke analysere skemaegenskaber", + "Failed to archive conversation": "Kunne ikke arkivere samtalen", + "Failed to calculate sizes": "Kunne ikke beregne størrelser", + "Failed to cancel deactivation": "Kunne ikke annullere deaktivering", + "Failed to change active organisation: {error}": "Kunne ikke skifte aktiv organisation: {error}", + "Failed to change password": "Kunne ikke ændre adgangskode", + "Failed to check version: {error}": "Kunne ikke kontrollere version: {error}", + "Failed to clear audit trails: {error}": "Kunne ikke rydde revisionsspor: {error}", + "Failed to clear blob objects: {error}": "Kunne ikke rydde blob-objekter: {error}", + "Failed to clear cache: {error}": "Kunne ikke rydde cache: {error}", + "Failed to clear collection": "Kunne ikke rydde samlingen", + "Failed to clear conversation": "Kunne ikke rydde samtalen", + "Failed to clear embeddings: {error}": "Kunne ikke rydde embeddings: {error}", + "Failed to clear search trails: {error}": "Kunne ikke rydde søgespor: {error}", + "Failed to clear {type} cache: {error}": "Kunne ikke rydde {type}-cache: {error}", + "Failed to copy changes": "Kunne ikke kopiere ændringer", + "Failed to copy data": "Kunne ikke kopiere data", + "Failed to copy data to clipboard": "Kunne ikke kopiere data til udklipsholderen", + "Failed to copy to clipboard": "Kunne ikke kopiere til udklipsholderen", + "Failed to copy token": "Kunne ikke kopiere token", + "Failed to create conversation": "Kunne ikke oprette samtalen", + "Failed to create missing SOLR fields: {error}": "Kunne ikke oprette manglende SOLR-felter: {error}", + "Failed to create or find project": "Kunne ikke oprette eller finde projektet", + "Failed to create token": "Kunne ikke oprette token", + "Failed to delete SOLR collection: {error}": "Kunne ikke slette SOLR-samlingen: {error}", + "Failed to delete collection": "Kunne ikke slette samlingen", + "Failed to delete conversation": "Kunne ikke slette samtalen", + "Failed to delete objects for {schema}: {error}": "Kunne ikke slette objekter for {schema}: {error}", + "Failed to delete webhook": "Kunne ikke slette webhook", + "Failed to depublish register: {error}": "Kunne ikke afpublicere register: {error}", + "Failed to depublish schema: {error}": "Kunne ikke afpublicere skema: {error}", + "Failed to depublish: {error}": "Kunne ikke afpublicere: {error}", + "Failed to discover files: {error}": "Kunne ikke finde filer: {error}", + "Failed to download API specification": "Kunne ikke downloade API-specifikationen", + "Failed to export data": "Kunne ikke eksportere data", + "Failed to export objects": "Kunne ikke eksportere objekter", + "Failed to fetch conversation history": "Kunne ikke hente samtalehistorik", + "Failed to fix mismatched SOLR fields: {error}": "Kunne ikke rette uoverensstemmende SOLR-felter: {error}", + "Failed to get SOLR field configuration": "Kunne ikke hente SOLR-feltkonfiguration", + "Failed to get chat statistics": "Kunne ikke hente chatstatistik", + "Failed to get database information: %s": "Kunne ikke hente databaseoplysninger: %s", + "Failed to get entity categories": "Kunne ikke hente entitetskategorier", + "Failed to get entity statistics": "Kunne ikke hente entitetsstatistik", + "Failed to get entity types": "Kunne ikke hente entitetstyper", + "Failed to get response: {error}": "Kunne ikke hente svar: {error}", + "Failed to invalidate app store cache: {error}": "Kunne ikke invalidere app store-cache: {error}", + "Failed to leave organisation: {error}": "Kunne ikke forlade organisationen: {error}", + "Failed to link object": "Kunne ikke tilknytte objektet", + "Failed to load Deck cards": "Kunne ikke indlæse Deck-kort", + "Failed to load LLM configuration": "Kunne ikke indlæse LLM-konfiguration", + "Failed to load Nextcloud groups": "Kunne ikke indlæse Nextcloud-grupper", + "Failed to load cache statistics: {error}": "Kunne ikke indlæse cachestatistik: {error}", + "Failed to load conversation": "Kunne ikke indlæse samtalen", + "Failed to load entities": "Kunne ikke indlæse entiteter", + "Failed to load entity": "Kunne ikke indlæse entitet", + "Failed to load extraction data": "Kunne ikke indlæse udtrækningsdata", + "Failed to load files": "Kunne ikke indlæse filer", + "Failed to load linked contacts": "Kunne ikke indlæse tilknyttede kontakter", + "Failed to load linked emails": "Kunne ikke indlæse tilknyttede e-mails", + "Failed to load linked events": "Kunne ikke indlæse tilknyttede begivenheder", + "Failed to load organisations": "Kunne ikke indlæse organisationer", + "Failed to load register data": "Kunne ikke indlæse registerdata", + "Failed to load related entities": "Kunne ikke indlæse relaterede entiteter", + "Failed to load settings: {error}": "Kunne ikke indlæse indstillinger: {error}", + "Failed to load statistics: {error}": "Kunne ikke indlæse statistik: {error}", + "Failed to load templates": "Kunne ikke indlæse skabeloner", + "Failed to load view: {error}": "Kunne ikke indlæse visning: {error}", + "Failed to load webhooks": "Kunne ikke indlæse webhooks", + "Failed to load workflows": "Kunne ikke indlæse workflows", + "Failed to process message": "Kunne ikke behandle meddelelsen", + "Failed to publish register: {error}": "Kunne ikke publicere register: {error}", + "Failed to publish schema: {error}": "Kunne ikke publicere skema: {error}", + "Failed to publish: {error}": "Kunne ikke publicere: {error}", + "Failed to refresh database information": "Kunne ikke opdatere databaseoplysninger", + "Failed to reindex collection": "Kunne ikke genindeksere samlingen", + "Failed to remove avatar": "Kunne ikke fjerne avatar", + "Failed to remove link": "Kunne ikke fjerne tilknytning", + "Failed to remove member: {error}": "Kunne ikke fjerne medlem: {error}", + "Failed to remove schema {schema}: {error}": "Kunne ikke fjerne skema {schema}: {error}", + "Failed to remove user from organisation": "Kunne ikke fjerne bruger fra organisationen", + "Failed to rename conversation": "Kunne ikke omdøbe samtalen", + "Failed to request deactivation": "Kunne ikke anmode om deaktivering", + "Failed to restore conversation": "Kunne ikke gendanne samtalen", + "Failed to retrieve extraction status.": "Kunne ikke hente udtrækningsstatus.", + "Failed to retrieve objects for file.": "Kunne ikke hente objekter for filen.", + "Failed to retry extraction": "Kunne ikke forsøge udtrækning igen", + "Failed to retry webhook": "Kunne ikke forsøge webhook igen", + "Failed to revoke token": "Kunne ikke tilbagekalde token", + "Failed to save GitHub token": "Kunne ikke gemme GitHub-token", + "Failed to save GitLab URL": "Kunne ikke gemme GitLab-URL", + "Failed to save GitLab token": "Kunne ikke gemme GitLab-token", + "Failed to save LLM settings: {error}": "Kunne ikke gemme LLM-indstillinger: {error}", + "Failed to save additional feedback": "Kunne ikke gemme yderligere feedback", + "Failed to save configuration.": "Kunne ikke gemme konfigurationen.", + "Failed to save configuration: {error}": "Kunne ikke gemme konfigurationen: {error}", + "Failed to save facet configuration": "Kunne ikke gemme facetkonfiguration", + "Failed to save feedback": "Kunne ikke gemme feedback", + "Failed to save file settings: {error}": "Kunne ikke gemme filindstillinger: {error}", + "Failed to save n8n configuration": "Kunne ikke gemme n8n-konfiguration", + "Failed to save preferences": "Kunne ikke gemme præferencer", + "Failed to save roles": "Kunne ikke gemme roller", + "Failed to save settings": "Kunne ikke gemme indstillinger", + "Failed to save settings: {error}": "Kunne ikke gemme indstillinger: {error}", + "Failed to save view: {error}": "Kunne ikke gemme visning: {error}", + "Failed to save warmup interval: {error}": "Kunne ikke gemme warmup-interval: {error}", + "Failed to save webhook": "Kunne ikke gemme webhook", + "Failed to send feedback": "Kunne ikke sende feedback", + "Failed to set active organisation: {error}": "Kunne ikke angive aktiv organisation: {error}", + "Failed to setup SOLR: {error}": "Kunne ikke opsætte SOLR: {error}", + "Failed to start file warmup: {error}": "Kunne ikke starte filopvarmning: {error}", + "Failed to start processing {type} files: {error}": "Kunne ikke starte behandling af {type}-filer: {error}", + "Failed to start vectorization: {error}": "Kunne ikke starte vektorisering: {error}", + "Failed to switch organisation: {error}": "Kunne ikke skifte organisation: {error}", + "Failed to sync magic table for {schema}: {error}": "Kunne ikke synkronisere magic-tabel for {schema}: {error}", + "Failed to test SOLR connection: {error}": "Kunne ikke teste SOLR-forbindelsen: {error}", + "Failed to test webhook": "Kunne ikke teste webhook", + "Failed to update LLM settings: {error}": "Kunne ikke opdatere LLM-indstillinger: {error}", + "Failed to update Multitenancy settings: {error}": "Kunne ikke opdatere Multitenancy-indstillinger: {error}", + "Failed to update RBAC settings: {error}": "Kunne ikke opdatere RBAC-indstillinger: {error}", + "Failed to update Retention settings: {error}": "Kunne ikke opdatere opbevaringsindstillinger: {error}", + "Failed to update SOLR settings: {error}": "Kunne ikke opdatere SOLR-indstillinger: {error}", + "Failed to update favorite status": "Kunne ikke opdatere favoritstatus", + "Failed to update schema configuration for {schema}: {error}": "Kunne ikke opdatere skemakonfiguration for {schema}: {error}", + "Failed to update schema properties": "Kunne ikke opdatere skemaegenskaber", + "Failed to update view": "Kunne ikke opdatere visning", + "Failed to update view: {error}": "Kunne ikke opdatere visning: {error}", + "Failed to update webhook": "Kunne ikke opdatere webhook", + "Failed to upload avatar": "Kunne ikke uploade avatar", + "Failed to validate {schema}: {error}": "Kunne ikke validere {schema}: {error}", + "Failed to vectorize files: {error}": "Kunne ikke vektorisere filer: {error}", + "Failed to warmup names cache: {error}": "Kunne ikke opvarme navnecache: {error}", + "Federation": "Federation", + "Feedback recorded": "Feedback registreret", + "Fewer sources (1-3):": "Færre kilder (1-3):", + "Field": "Felt", + "Fields to Display": "Felter, der skal vises", + "File": "Fil", + "File #{id}": "Fil #{id}", + "File Chunk Prediction": "Forudsigelse af filsegment", + "File Collection": "Filsamling", + "File Handling": "Filhåndtering", + "File Location": "Filplacering", + "File Management": "Filhåndtering", + "File Name": "Filnavn", + "File Path": "Filsti", + "File Type Selection": "Valg af filtype", + "File Types to Process": "Filtyper, der skal behandles", + "File Vectorization": "Filvektorisering", + "File Warmup": "Filopvarmning", + "File actions menu": "Menu med filhandlinger", + "File anonymized successfully": "Filen blev anonymiseret", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vektorisering af filsegmenter er endnu ikke implementeret. Segmenterne er klar og gemt, men vektoriseringstjenesten er under udvikling.", + "File collection not configured": "Filsamling er ikke konfigureret", + "File discovery completed": "Filsøgning fuldført", + "File extraction completed": "Filudtræk fuldført", + "File extraction queued": "Filudtræk sat i kø", + "File indexed successfully": "Filen blev indekseret", + "File is already anonymized": "Filen er allerede anonymiseret", + "File name": "Filnavn", + "File not found": "Filen blev ikke fundet", + "File settings saved successfully": "Filindstillinger blev gemt", + "File settings updated successfully": "Filindstillinger blev opdateret", + "File sources": "Filkilder", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Filvektorisering fuldført! {vectorized} segmenter vektoriseret fra {files} filer. {failed} mislykkedes.", + "File vectorization configuration saved successfully": "Konfiguration for filvektorisering blev gemt", + "File vectorization started. Check the statistics section for progress.": "Filvektorisering startet. Tjek statistikafsnittet for fremdrift.", + "File warmup completed": "Filopvarmning fuldført", + "Files": "Filer", + "Files added successfully": "Filerne blev tilføjet", + "Files with Completed Extraction:": "Filer med fuldført udtræk:", + "Files → fileCollection, Objects → objectCollection": "Filer → fileCollection, Objekter → objectCollection", + "Files → filecollection, objects → objectcollection": "Filer → filecollection, objekter → objectcollection", + "Filter Audit Trails": "Filtrér revisionsspor", + "Filter Deleted Items": "Filtrér slettede elementer", + "Filter Objects": "Filtrér objekter", + "Filter Properties": "Filtrér egenskaber", + "Filter Search Trails": "Filtrér søgespor", + "Filter Statistics": "Filtrér statistik", + "Filter and analyze search trail entries": "Filtrér og analysér søgesporsposter", + "Filter and manage audit trail entries": "Filtrér og administrér revisionssporsposter", + "Filter and manage soft deleted items": "Filtrér og administrér blødt slettede elementer", + "Filter and search entities": "Filtrér og søg i entiteter", + "Filter by object ID": "Filtrér efter objekt-ID", + "Filter by search term": "Filtrér efter søgeterm", + "Filter by type": "Filtrér efter type", + "Filter by webhook": "Filtrér efter webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filterdata blev indlæst automatisk. Brug filtrene nedenfor til at forfine din søgning.", + "Filter fields...": "Filtrér felter ...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrér webhook-udløsere efter payload-egenskaber (én pr. linje, format: nøgle: værdi)", + "Filtered": "Filtreret", + "Filters": "Filtre", + "Finalizing setup...": "Afslutter opsætning ...", + "Fireworks AI Chat Settings": "Fireworks AI-chatindstillinger", + "Fireworks AI Embedding Configuration": "Fireworks AI-indlejringskonfiguration", + "First": "Første", + "Fixed": "Rettet", + "For chat and retrieval-augmented generation": "Til chat og retrieval-augmented generation", + "For vector embeddings and semantic search": "Til vektorindlejringer og semantisk søgning", + "Format": "Format", + "Format Issue": "Formatproblem", + "Format constraint is missing": "Formatbegrænsning mangler", + "From (e.g. -7 days or 2025-01-01)": "Fra (f.eks. -7 days eller 2025-01-01)", + "From Date": "Fra dato", + "From date": "Fra dato", + "Full data copied to clipboard": "Alle data blev kopieret til udklipsholderen", + "Function Tools": "Funktionsværktøjer", + "General Issue": "Generelt problem", + "Generate recommendations and confidence scores": "Generér anbefalinger og konfidensscorer", + "Generate report": "Generér rapport", + "Generate the verantwoordingsdocument": "Generér verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generér vektorer med det samme, når nye objekter oprettes", + "Generated: {time}": "Genereret: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Få realtidsindsigt i din organisations datasundhed ved at fokusere på registre, skemadefinitioner samt lagring og brug af objekter.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Få realtidsindsigt i din organisations datasundhed, væksttendenser og kvalitetsmålinger.", + "GitHub Personal Access Token": "GitHub personlig adgangstoken", + "GitHub token is valid": "GitHub-token er gyldig", + "GitHub token is valid! Username: {username}": "GitHub-token er gyldig! Brugernavn: {username}", + "GitHub token removed successfully": "GitHub-token blev fjernet", + "GitHub token saved successfully": "GitHub-token blev gemt", + "GitHub token test failed: {message}": "Test af GitHub-token mislykkedes: {message}", + "GitLab Personal Access Token": "GitLab personlig adgangstoken", + "GitLab URL saved successfully": "GitLab-URL blev gemt", + "GitLab token is valid": "GitLab-token er gyldig", + "GitLab token is valid! Username: {username}": "GitLab-token er gyldig! Brugernavn: {username}", + "GitLab token saved successfully": "GitLab-token blev gemt", + "GitLab token test failed: {message}": "Test af GitLab-token mislykkedes: {message}", + "Go to linked Register": "Gå til tilknyttet register", + "Go to linked Schema": "Gå til tilknyttet skema", + "Go to organisation": "Gå til organisation", + "Grant additional permissions beyond standard CRUD operations": "Tildel yderligere tilladelser ud over almindelige CRUD-operationer", + "Group Quota": "Gruppekvote", + "Groups": "Grupper", + "HTML preview": "HTML-forhåndsvisning", + "HTTP Method": "HTTP-metode", + "HTTP method used to send webhook requests": "HTTP-metode, der bruges til at sende webhook-anmodninger", + "Headers": "Headere", + "Health": "Sundhed", + "Heartbeat successful - connection kept alive": "Heartbeat lykkedes – forbindelsen holdes i live", + "Helpful": "Nyttig", + "Here you can configure the details for various connections.": "Her kan du konfigurere detaljerne for forskellige forbindelser.", + "Here you can configure the details for your organisation.": "Her kan du konfigurere detaljerne for din organisation.", + "Hide": "Skjul", + "Hide Filters": "Skjul filtre", + "Hide in collection view": "Skjul i samlingsvisning", + "Hide in forms": "Skjul i formularer", + "Hide in list view": "Skjul i listevisning", + "High": "Høj", + "High Confidence": "Høj konfidens", + "Host": "Vært", + "Hourly": "Hver time", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Hvor dybt indlejrede objektegenskaber skal gennemløbes (1-20). Højere værdier indfanger flere detaljer, men øger vektorstørrelsen.", + "How often to check for updates (1-168 hours)": "Hvor ofte der skal tjekkes for opdateringer (1-168 timer)", + "How to handle retries for failed webhook deliveries": "Hvordan genforsøg for mislykkede webhook-leveringer skal håndteres", + "IBANs": "IBAN-numre", + "ID": "ID", + "ID of input mapping (optional)": "ID for inputmapping (valgfrit)", + "ID of output mapping (optional)": "ID for outputmapping (valgfrit)", + "ID of the target resource": "ID for målressourcen", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "IP-hastighedsgrænser blev ryddet", + "Identify properties not in the schema": "Identificér egenskaber, der ikke findes i skemaet", + "Immutable": "Uforanderlig", + "Import": "Importér", + "Import Configuration": "Importér konfiguration", + "Import Data into Register": "Importér data til register", + "Import successful": "Import lykkedes", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importér skabelonen report-bundle.json for at få `reports`-registret, og opret derefter dit første dashboard via den almindelige objektgrænseflade. Dashboards angiver deres widgets i JSON, og rendereren forsyner hver widget med live aggregeringsdata.", + "Improved Property": "Forbedret egenskab", + "In use": "I brug", + "Inactive": "Inaktiv", + "Inactive organisations cannot be used": "Inaktive organisationer kan ikke bruges", + "Include IDs and names of related objects for better contextual search": "Inkludér ID'er og navne på relaterede objekter for bedre kontekstuel søgning", + "Include ids and names of related objects for better contextual search": "Inkludér id'er og navne på relaterede objekter for bedre kontekstuel søgning", + "Include related object references": "Inkludér referencer til relaterede objekter", + "Include schema and register metadata": "Inkludér skema- og registermetadata", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Oplysninger om den aktuelle OpenRegister-installation", + "Inherited from register": "Nedarvet fra register", + "Inherits permissions from register": "Nedarver tilladelser fra register", + "Initialization failed": "Initialisering mislykkedes", + "Initialize Project": "Initialisér projekt", + "Initializing SOLR setup...": "Initialiserer SOLR-opsætning ...", + "Input Mapping": "Inputmapping", + "Inspect Fields": "Inspicér felter", + "Inspect Index": "Inspicér indeks", + "Inspect SOLR Index": "Inspicér SOLR-indeks", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installér notify_push-appen fra Nextcloud App Store for at aktivere realtidsopdateringer.", + "Integrations": "Integrationer", + "Internet location (URL) of this installation": "Internetplacering (URL) for denne installation", + "Interval": "Interval", + "Invalid": "Ugyldig", + "Invalid batch size. Must be between 1 and 5000": "Ugyldig batchstørrelse. Skal være mellem 1 og 5000", + "Invalid feedback type": "Ugyldig feedbacktype", + "Invalid field name provided": "Ugyldigt feltnavn angivet", + "Invalid maxObjects. Must be 0 (all) or positive number": "Ugyldig maxObjects. Skal være 0 (alle) eller et positivt tal", + "Invalid:": "Ugyldig:", + "Invite Users": "Invitér brugere", + "Invited Users": "Inviterede brugere", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Inzage-resultater", + "Issue": "Problem", + "Items per page:": "Elementer pr. side:", + "Just now": "Lige nu", + "Key": "Nøgle", + "LLM Configuration": "LLM-konfiguration", + "LLM actions menu": "Menu med LLM-handlinger", + "LLM configuration saved successfully": "LLM-konfiguration blev gemt", + "LLM features disabled": "LLM-funktioner deaktiveret", + "LLM features enabled": "LLM-funktioner aktiveret", + "LLM must be enabled with an embedding provider configured": "LLM skal være aktiveret med en konfigureret indlejringsudbyder", + "LLM settings saved successfully": "LLM-indstillinger blev gemt", + "LLM settings updated successfully": "LLM-indstillinger blev opdateret", + "Label": "Etiket", + "Labels": "Etiketter", + "Languages": "Sprog", + "Last": "Sidste", + "Last Triggered": "Senest udløst", + "Leave empty to allow all users access": "Lad feltet stå tomt for at give alle brugere adgang", + "Leave off for auto-detection from property format.": "Lad være med at angive for automatisk registrering ud fra egenskabsformatet.", + "Left organisation successfully": "Forlod organisationen", + "Legal basis *": "Retsgrundlag *", + "Length Range:": "Længdeinterval:", + "Linear": "Lineær", + "Link": "Link", + "Link an email from the Mail app sidebar to associate it with this object.": "Tilknyt en e-mail fra Mail-appens sidepanel for at knytte den til dette objekt.", + "Link an object to see it here.": "Tilknyt et objekt for at se det her.", + "Link another object": "Tilknyt et andet objekt", + "Link existing event": "Tilknyt eksisterende begivenhed", + "Link removed": "Link fjernet", + "Link to Object": "Link til objekt", + "Link to {name}": "Link til {name}", + "Linked Objects": "Tilknyttede objekter", + "Linked by {user}": "Tilknyttet af {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Tilknyttede e-mails, kalenderbegivenheder, kontakter og Deck-kort vises her.", + "Linked to {name}": "Tilknyttet {name}", + "Load Advanced Filters": "Indlæs avancerede filtre", + "Load advanced filters with live data from your search index": "Indlæs avancerede filtre med live-data fra dit søgeindeks", + "Load more": "Indlæs flere", + "Load view": "Indlæs visning", + "Loading API tokens...": "Indlæser API-tokens ...", + "Loading ConfigSets...": "Indlæser ConfigSets ...", + "Loading LLM configuration...": "Indlæser LLM-konfiguration ...", + "Loading RBAC settings...": "Indlæser RBAC-indstillinger ...", + "Loading activity...": "Indlæser aktivitet ...", + "Loading advanced filters...": "Indlæser avancerede filtre ...", + "Loading agents...": "Indlæser agenter ...", + "Loading application details...": "Indlæser applikationsdetaljer ...", + "Loading applications...": "Indlæser applikationer ...", + "Loading archived conversations...": "Indlæser arkiverede samtaler ...", + "Loading audit trails...": "Indlæser revisionsspor ...", + "Loading available tools...": "Indlæser tilgængelige værktøjer ...", + "Loading cache statistics...": "Indlæser cachestatistik ...", + "Loading collections...": "Indlæser samlinger ...", + "Loading configurations...": "Indlæser konfigurationer ...", + "Loading conversation...": "Indlæser samtale ...", + "Loading conversations...": "Indlæser samtaler ...", + "Loading deleted items...": "Indlæser slettede elementer ...", + "Loading endpoints": "Indlæser endpoints", + "Loading entities...": "Indlæser entiteter ...", + "Loading events...": "Indlæser begivenheder ...", + "Loading filter data...": "Indlæser filterdata ...", + "Loading groups...": "Indlæser grupper ...", + "Loading linked objects...": "Indlæser tilknyttede objekter ...", + "Loading log details...": "Indlæser logdetaljer ...", + "Loading multitenancy settings...": "Indlæser multitenancy-indstillinger ...", + "Loading n8n configuration...": "Indlæser n8n-konfiguration ...", + "Loading permission matrix...": "Indlæser tilladelsesmatrix ...", + "Loading preferences...": "Indlæser præferencer ...", + "Loading register data...": "Indlæser registerdata ...", + "Loading registers...": "Indlæser registre ...", + "Loading retention settings...": "Indlæser opbevaringsindstillinger ...", + "Loading schemas...": "Indlæser skemaer ...", + "Loading search configuration...": "Indlæser søgekonfiguration ...", + "Loading search trails...": "Indlæser søgespor ...", + "Loading sources...": "Indlæser kilder ...", + "Loading statistics...": "Indlæser statistik ...", + "Loading text extraction settings...": "Indlæser indstillinger for tekstudtræk ...", + "Loading tokens...": "Indlæser tokens ...", + "Loading user groups...": "Indlæser brugergrupper ...", + "Loading users...": "Indlæser brugere ...", + "Loading views...": "Indlæser visninger ...", + "Loading...": "Indlæser ...", + "Local": "Lokal", + "Local Version": "Lokal version", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Find alle objekter, der refererer til en registreret person (Art 15 inzage), forhåndsvis en sletning (Art 17 vergetelheid), eller eksportér deres data (Art 20 portabiliteit).", + "Location": "Placering", + "Location Field": "Placeringsfelt", + "Locations": "Placeringer", + "Locked": "Låst", + "Locked:": "Låst:", + "Login successful": "Login lykkedes", + "Logs": "Logge", + "Low": "Lav", + "Low Confidence": "Lav konfidens", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Lavere tal vises først i filtersidepanelet. Lad feltet stå tomt for automatisk rækkefølge.", + "Lucene Version:": "Lucene-version:", + "Magic table sync completed for {schema}": "Magic-tabelsynkronisering fuldført for {schema}", + "Magic table synchronized successfully": "Magic-tabellen blev synkroniseret", + "Mail Integration": "Mail-integration", + "Mail integration is not available": "Mail-integration er ikke tilgængelig", + "Make this installation discoverable within the federation network.": "Gør denne installation synlig i federationsnetværket.", + "Manage Organisation Roles": "Administrér organisationsroller", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Administrér SOLR-samlinger (datalagre) og tildel dem til objekter og filer.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Administrér SOLR-ConfigSets (konfigurationsskabeloner) til dine samlinger.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Administrér SOLR-samlinger (datalagre) og tildel dem til objekter og filer.", + "Manage SOLR configsets (configuration templates) for your collections.": "Administrér SOLR-configsets (konfigurationsskabeloner) til dine samlinger.", + "Manage and configure agents for automated tasks": "Administrér og konfigurér agenter til automatiserede opgaver", + "Manage and monitor file text extraction status": "Administrér og overvåg status for tekstudtræk fra filer", + "Manage and restore soft deleted items from your registers": "Administrér og gendan blødt slettede elementer fra dine registre", + "Manage and view detected entities from files and objects": "Administrér og se registrerede entiteter fra filer og objekter", + "Manage document templates and themes": "Administrér dokumentskabeloner og temaer", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Administrér n8n-arbejdsgange til OpenRegister-automatisering. Arbejdsgangene gemmes i det konfigurerede projekt.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Administrér behandlingsaktiviteter, kør indsigtsanmodninger fra registrerede, og revidér overholdelse i henhold til EU's GDPR / nederlandsk AVG.", + "Manage webhooks for event-driven integrations": "Administrér webhooks til hændelsesdrevne integrationer", + "Manage your account settings, security, and personal data.": "Administrér dine kontoindstillinger, sikkerhed og personlige data.", + "Manage your applications and modules": "Administrér dine applikationer og moduler", + "Manage your chat conversations": "Administrér dine chatsamtaler", + "Manage your data registers and their configurations": "Administrér dine dataregistre og deres konfigurationer", + "Manage your data schemas and their properties": "Administrér dine dataskemaer og deres egenskaber", + "Manage your data sources and their configurations": "Administrér dine datakilder og deres konfigurationer", + "Manage your organisations and switch between them": "Administrér dine organisationer og skift mellem dem", + "Manage your saved search configurations": "Administrér dine gemte søgekonfigurationer", + "Manage your system configurations and settings": "Administrér dine systemkonfigurationer og -indstillinger", + "Managed": "Administreret", + "Mappings": "Mappinger", + "Mass validation completed successfully": "Massevalidering blev fuldført", + "Mass validation failed: {error}": "Massevalidering mislykkedes: {error}", + "Max Files (0 = all)": "Maks. filer (0 = alle)", + "Max Length": "Maks. længde", + "Max Length:": "Maks. længde:", + "Max Objects (0 = all)": "Maks. objekter (0 = alle)", + "Max Retries": "Maks. genforsøg", + "Max Shards/Node": "Maks. shards/node", + "Max Tokens": "Maks. tokens", + "Max execution time (ms)": "Maks. udførelsestid (ms)", + "Max ms": "Maks. ms", + "Max result count": "Maks. antal resultater", + "Max results": "Maks. resultater", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksimal filstørrelse (MB)", + "Maximum Nesting Depth": "Maksimal indlejringsdybde", + "Maximum length": "Maksimal længde", + "Maximum length constraint is missing": "Begrænsning for maksimal længde mangler", + "Maximum length is too restrictive": "Maksimal længde er for restriktiv", + "Maximum number of files to process. Set to 0 to process all files.": "Maksimalt antal filer, der skal behandles. Angiv 0 for at behandle alle filer.", + "Maximum number of items": "Maksimalt antal elementer", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksimalt antal objekter, der skal behandles. Angiv 0 for at behandle alle objekter.", + "Maximum number of retry attempts for failed deliveries": "Maksimalt antal genforsøg ved mislykkede leveringer", + "Maximum time to wait before committing changes": "Maksimal ventetid, før ændringer commits", + "Maximum tokens to generate": "Maksimalt antal tokens, der skal genereres", + "Maximum value": "Maksimal værdi", + "Maximum value constraint is missing": "Begrænsning for maksimal værdi mangler", + "Maximum value is too restrictive": "Maksimal værdi er for restriktiv", + "Medium": "Middel", + "Medium Confidence": "Middel konfidens", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Mellemstore forespørgsler: Søgninger med en vis filtrering eller flere parametre (f.eks. datointervaller, bestemte registre/skemaer)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Mellemstore forespørgsler: søgninger med en vis filtrering eller flere parametre (f.eks. datointervaller, bestemte registre/skemaer)", + "Member removed successfully": "Medlemmet blev fjernet", + "Members": "Medlemmer", + "Members ({count})": "Medlemmer ({count})", + "Members of selected groups can access this view": "Medlemmer af de valgte grupper kan få adgang til denne visning", + "Members:": "Medlemmer:", + "Memory prediction calculated successfully": "Hukommelsesforudsigelse blev beregnet", + "Message content is required": "Beskedindhold er påkrævet", + "Message does not belong to this conversation": "Beskeden tilhører ikke denne samtale", + "Message not found": "Beskeden blev ikke fundet", + "Metadata": "Metadata", + "Metadata Filters": "Metadatafiltre", + "Method": "Metode", + "Method*": "Metode*", + "Method: {method}": "Metode: {method}", + "Min Length": "Min. længde", + "Min execution time (ms)": "Min. udførelsestid (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. antal resultater", + "Min results": "Min. resultater", + "Minimum": "Minimum", + "Minimum length": "Minimal længde", + "Minimum number of items": "Minimalt antal elementer", + "Minimum value": "Minimal værdi", + "Minimum value constraint is missing": "Begrænsning for minimal værdi mangler", + "Minimum value is too restrictive": "Minimal værdi er for restriktiv", + "Missing conversation or agentUuid": "Manglende samtale eller agentUuid", + "Missing conversationId": "Manglende conversationId", + "Missing message": "Manglende besked", + "Missing object fields created successfully": "Manglende objektfelter blev oprettet", + "Mode:": "Tilstand:", + "Model": "Model", + "Model Name": "Modelnavn", + "Modified": "Ændret", + "Monthly": "Månedligt", + "More sources (10-20):": "Flere kilder (10-20):", + "Most Active Objects": "Mest aktive objekter", + "Multiple Of": "Multiplum af", + "Multiple of": "Multiplum af", + "Multiple search trail deletion not implemented yet": "Sletning af flere søgespor er endnu ikke implementeret", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Multitenancy-indstillinger blev opdateret", + "My Account": "Min konto", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Navn", + "Name *": "Navn *", + "Name*": "Navn*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Navnecache opvarmet: {count} navne indlæst på {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Navnecache opvarmet: {count} navne indlæst på {time}. {message}", + "Names cache warmup completed": "Opvarmning af navnecache fuldført", + "Naming pattern for copies:": "Navngivningsmønster for kopier:", + "New Collection Name": "Nyt samlingsnavn", + "New Configuration": "Ny konfiguration", + "New Conversation": "Ny samtale", + "New Properties": "Nye egenskaber", + "New Property": "Ny egenskab", + "New User Group": "Ny brugergruppe", + "New Value": "Ny værdi", + "New activity": "Ny aktivitet", + "New password": "Ny adgangskode", + "New processing activity": "Ny behandlingsaktivitet", + "New users without specific organisation membership will be automatically added to this organisation": "Nye brugere uden specifikt organisationsmedlemskab tilføjes automatisk til denne organisation", + "New verwerkingsactiviteit": "Ny verwerkingsactiviteit", + "Next": "Næste", + "Nextcloud Groups": "Nextcloud-grupper", + "No": "Nej", + "No API tokens.": "Ingen API-tokens.", + "No ConfigSets found": "Ingen ConfigSets fundet", + "No Deck cards linked to this object": "Ingen Deck-kort er knyttet til dette objekt", + "No GitHub token configured": "Ingen GitHub-token konfigureret", + "No GitHub token provided": "Ingen GitHub-token angivet", + "No GitLab token provided": "Ingen GitLab-token angivet", + "No Organisation": "Ingen organisation", + "No active filters": "Ingen aktive filtre", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Ingen aktive objekter at slette midlertidigt for skema {schema}. Brug \"Slet permanent\" for at fjerne midlertidigt slettede objekter.", + "No activity data available": "Ingen aktivitetsdata tilgængelige", + "No activity found.": "Ingen aktivitet fundet.", + "No agents available": "Ingen agenter tilgængelige", + "No agents found": "Ingen agenter fundet", + "No app store cache exists yet": "Der findes endnu ingen app store-cache", + "No applications are available.": "Ingen applikationer er tilgængelige.", + "No applications found": "Ingen applikationer fundet", + "No approval steps for this object.": "Ingen godkendelsestrin for dette objekt.", + "No archived conversations": "Ingen arkiverede samtaler", + "No audit trail entries found": "Ingen revisionsspor-poster fundet", + "No changes recorded": "Ingen ændringer registreret", + "No chunks to vectorize": "Ingen blokke at vektorisere", + "No collections found": "Ingen samlinger fundet", + "No configuration data": "Ingen konfigurationsdata", + "No configuration selected": "Ingen konfiguration valgt", + "No configurations are available.": "Ingen konfigurationer er tilgængelige.", + "No configurations found": "Ingen konfigurationer fundet", + "No configurations found for this application.": "Ingen konfigurationer fundet for denne applikation.", + "No contacts linked to this object": "Ingen kontakter er knyttet til dette objekt", + "No conversations yet. Create a new one to get started!": "Ingen samtaler endnu. Opret en ny for at komme i gang!", + "No dashboards yet": "Ingen dashboards endnu", + "No data available": "Ingen data tilgængelige", + "No data available for chart": "Ingen data tilgængelige for diagram", + "No deleted items found": "Ingen slettede elementer fundet", + "No deletion data available": "Ingen sletningsdata tilgængelige", + "No description found": "Ingen beskrivelse fundet", + "No description provided": "Ingen beskrivelse angivet", + "No emails linked to this object": "Ingen e-mails er knyttet til dette objekt", + "No endpoints defined": "Ingen endepunkter defineret", + "No entities detected for this email.": "Ingen entiteter registreret for denne e-mail.", + "No entities detected in this file. Run text extraction first.": "Ingen entiteter registreret i denne fil. Kør tekstudtrækning først.", + "No entities found": "Ingen entiteter fundet", + "No entities have been detected yet": "Der er endnu ikke registreret nogen entiteter", + "No events found": "Ingen begivenheder fundet", + "No events linked to this object": "Ingen begivenheder er knyttet til dette objekt", + "No expired audit trails found to clear": "Ingen udløbne revisionsspor fundet at rydde", + "No expired search trails found to clear": "Ingen udløbne søgespor fundet at rydde", + "No extraction data available for this file": "Ingen udtrækningsdata tilgængelige for denne fil", + "No facetable fields available. Select a register and schema to see available filters.": "Ingen facetbare felter tilgængelige. Vælg et register og skema for at se tilgængelige filtre.", + "No files found": "Ingen filer fundet", + "No files have been extracted yet": "Der er endnu ikke udtrukket nogen filer", + "No files selected": "Ingen filer valgt", + "No files to process": "Ingen filer at behandle", + "No files to reindex": "Ingen filer at genindeksere", + "No filters are currently active. This will delete ALL audit trail entries!": "Der er i øjeblikket ingen aktive filtre. Dette vil slette ALLE revisionsspor-poster!", + "No groups found. Try a different search.": "Ingen grupper fundet. Prøv en anden søgning.", + "No label": "Ingen etiket", + "No labels": "Ingen etiketter", + "No linked objects": "Ingen tilknyttede objekter", + "No log entries found": "Ingen logposter fundet", + "No logs are available for this configuration.": "Der er ingen logfiler tilgængelige for denne konfiguration.", + "No logs are available for this source.": "Der er ingen logfiler tilgængelige for denne kilde.", + "No logs found": "Ingen logfiler fundet", + "No matches": "Ingen match", + "No mismatched fields found - SOLR schema is properly configured": "Ingen uoverensstemmende felter fundet - SOLR-skemaet er korrekt konfigureret", + "No objects are currently selected for deletion.": "Der er i øjeblikket ingen objekter valgt til sletning.", + "No objects are currently selected for permanent deletion.": "Der er i øjeblikket ingen objekter valgt til permanent sletning.", + "No objects are currently selected for restoration.": "Der er i øjeblikket ingen objekter valgt til gendannelse.", + "No objects found": "Ingen objekter fundet", + "No objects found to vectorize": "Ingen objekter fundet at vektorisere", + "No objects linked to this email": "Ingen objekter er knyttet til denne e-mail", + "No objects selected": "Ingen objekter valgt", + "No objects selected for deletion": "Ingen objekter valgt til sletning", + "No objects selected for restoration": "Ingen objekter valgt til gendannelse", + "No objects to delete": "Ingen objekter at slette", + "No objects to delete for schema {schema}": "Ingen objekter at slette for skema {schema}", + "No objects were permanently deleted": "Ingen objekter blev slettet permanent", + "No organisations found": "Ingen organisationer fundet", + "No personal data was found for this subject identifier.": "Der blev ikke fundet nogen personoplysninger for denne emneidentifikator.", + "No processing activities yet": "Ingen behandlingsaktiviteter endnu", + "No properties found": "Ingen egenskaber fundet", + "No properties match your filters.": "Ingen egenskaber matcher dine filtre.", + "No purge date set": "Ingen tømningsdato angivet", + "No register data available": "Ingen registerdata tilgængelige", + "No register objects reference this file": "Ingen registerobjekter refererer til denne fil", + "No registers found": "Ingen registre fundet", + "No registers found for this application.": "Ingen registre fundet for denne applikation.", + "No registers found. Create a register to configure permissions.": "Ingen registre fundet. Opret et register for at konfigurere tilladelser.", + "No related cases found for this sender": "Ingen relaterede sager fundet for denne afsender", + "No related entities": "Ingen relaterede entiteter", + "No relations found": "Ingen relationer fundet", + "No request body available": "Ingen anmodningstekst tilgængelig", + "No response body available": "Ingen svartekst tilgængelig", + "No saved views yet. Create one in the Search tab!": "Ingen gemte visninger endnu. Opret en i fanen Søg!", + "No saved views yet. create one in the search tab!": "Ingen gemte visninger endnu. opret en i søgefanen!", + "No scan run yet": "Ingen scanning kørt endnu", + "No schema data available": "Ingen skemadata tilgængelige", + "No schema selected for exploration": "Intet skema valgt til udforskning", + "No schemas configured for mail linking.": "Ingen skemaer konfigureret til e-mailtilknytning.", + "No schemas found": "Ingen skemaer fundet", + "No schemas found for this application.": "Ingen skemaer fundet for denne applikation.", + "No search terms data available": "Ingen søgeordsdata tilgængelige", + "No search trail entries found": "Ingen søgespor-poster fundet", + "No sources are available.": "Ingen kilder er tilgængelige.", + "No sources found": "Ingen kilder fundet", + "No synchronizations found": "Ingen synkroniseringer fundet", + "No templates found": "Ingen skabeloner fundet", + "No templates have been created yet": "Der er endnu ikke oprettet nogen skabeloner", + "No tools available. Tools can be registered by installed apps.": "Ingen værktøjer tilgængelige. Værktøjer kan registreres af installerede apps.", + "No users in this organisation.": "Ingen brugere i denne organisation.", + "No valid magic-mapped register+schema combinations found": "Ingen gyldige magic-mappede register+skema-kombinationer fundet", + "No views found. Create views first before configuring vectorization.": "Ingen visninger fundet. Opret visninger først, før du konfigurerer vektorisering.", + "No views match your search": "Ingen visninger matcher din søgning", + "No webhooks found": "Ingen webhooks fundet", + "No webhooks have been configured yet": "Der er endnu ikke konfigureret nogen webhooks", + "No workflows found in this project. Create workflows in the n8n editor.": "Ingen arbejdsgange fundet i dette projekt. Opret arbejdsgange i n8n-editoren.", + "None": "Ingen", + "Not Found": "Ikke fundet", + "Not extracted": "Ikke udtrukket", + "Not helpful": "Ikke nyttig", + "Not in use": "Ikke i brug", + "Note:": "Bemærk:", + "Notifications": "Notifikationer", + "Number Constraints": "Talbegrænsninger", + "Number of Sources": "Antal kilder", + "Number of chunks to vectorize in one API call (1-100).": "Antal blokke at vektorisere i ét API-kald (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Antal blokke at vektorisere i ét API-kald. Højere = hurtigere, men mere hukommelse. Anbefalet: 10-50.", + "Number of objects to process in each batch (1-5000).": "Antal objekter at behandle i hver batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Antal objekter at vektorisere i ét API-kald. Højere = hurtigere, men mere hukommelse. Anbefalet: 10-50.", + "Numeric Range:": "Talinterval:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Objektsamling", + "Object Configuration:": "Objektkonfiguration:", + "Object Count Prediction": "Forudsigelse af objektantal", + "Object Extraction Mode": "Objektudtrækningstilstand", + "Object ID": "Objekt-ID", + "Object Management": "Objekthåndtering", + "Object Text Extraction": "Tekstudtrækning fra objekt", + "Object Vectorization": "Objektvektorisering", + "Object Warmup": "Objektopvarmning", + "Object changes in owned objects": "Objektændringer i ejede objekter", + "Object collection not configured": "Objektsamling ikke konfigureret", + "Object metadata incomplete for file upload": "Objektmetadata er ufuldstændige til filoverførsel", + "Object not found": "Objekt ikke fundet", + "Object permanently deleted": "Objekt slettet permanent", + "Object restored successfully": "Objekt gendannet", + "Object settings updated successfully": "Objektindstillinger opdateret", + "Object sources": "Objektkilder", + "Object successfully deleted": "Objekt slettet", + "Object successfully modified": "Objekt ændret", + "Object unlocked successfully": "Objekt låst op", + "Object vectorization configuration saved successfully": "Konfiguration af objektvektorisering gemt", + "Object vectorized successfully": "Objekt vektoriseret", + "Object:": "Objekt:", + "Objects": "Objekter", + "Objects Analyzed": "Analyserede objekter", + "Objects Distribution": "Objektfordeling", + "Objects being analyzed": "Objekter, der analyseres", + "Objects by Register": "Objekter efter register", + "Objects by Schema": "Objekter efter skema", + "Objects deletion completed for {schema}": "Sletning af objekter fuldført for {schema}", + "Objects deletion completed successfully": "Sletning af objekter fuldført", + "Objects in schema": "Objekter i skema", + "Objects successfully deleted": "Objekter slettet", + "Objects to Process:": "Objekter at behandle:", + "Objects to be analyzed": "Objekter, der skal analyseres", + "Objects to be deleted": "Objekter, der skal slettes", + "Objects to be validated": "Objekter, der skal valideres", + "Objects will be serialized as JSON text before vectorization": "Objekter serialiseres som JSON-tekst før vektorisering", + "Objects will be soft deleted and moved to the": "Objekter slettes midlertidigt og flyttes til", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekter slettes midlertidigt (markeret som slettet, men beholdt i databasen). De kan gendannes senere efter behov.", + "Old Value": "Gammel værdi", + "Oldest Item (days)": "Ældste element (dage)", + "Ollama Chat Settings": "Ollama chatindstillinger", + "Ollama Embedding Configuration": "Ollama embedding-konfiguration", + "Ollama URL": "Ollama-URL", + "Only With Changes": "Kun med ændringer", + "Only members of selected groups can access this organisation": "Kun medlemmer af de valgte grupper kan få adgang til denne organisation", + "Open Nextcloud App Store": "Åbn Nextcloud App Store", + "Open OpenRegister sidebar": "Åbn OpenRegister-sidepanel", + "Open Register Objects": "Åbn registerobjekter", + "Open folder": "Åbn mappe", + "Open in OpenRegister": "Åbn i OpenRegister", + "Open n8n Editor": "Åbn n8n-editor", + "OpenAI Chat Settings": "OpenAI chatindstillinger", + "OpenAI Embedding Configuration": "OpenAI embedding-konfiguration", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister-indstillinger", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operatørdefinerede dashboards og planlagte rapporter. Hvert dashboard er et førsteklasses objekt i `reports`-registret; widgets erklæres i dashboardets `widgets`-array og gengives live fra aggregeringer / GraphQL.", + "Optimizing search performance...": "Optimerer søgeydeevne...", + "Optional URL-friendly identifier": "Valgfri URL-venlig identifikator", + "Optional description for this facet": "Valgfri beskrivelse af denne facet", + "Optional end date property": "Valgfri slutdato-egenskab", + "Optional event description template": "Valgfri skabelon for begivenhedsbeskrivelse", + "Optional location property": "Valgfri placerings-egenskab", + "Optional webhook secret for signature verification": "Valgfri webhook-hemmelighed til signaturverifikation", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Valgfrit: Tilføj forespørgselsparametre for at filtrere det refererede skema (f.eks. status=active&type=public)", + "Or": "Eller", + "Order": "Rækkefølge", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Ordnede BCP 47-sprogkoder. Det første sprog er registrets standard og styrer Accept-Language-fallback for oversættelige egenskaber.", + "Organisation": "Organisation", + "Organisation Configuration": "Organisationskonfiguration", + "Organisation ID": "Organisations-ID", + "Organisation Members": "Organisationsmedlemmer", + "Organisation Statistics": "Organisationsstatistik", + "Organisation created successfully": "Organisation oprettet", + "Organisation membership changes": "Ændringer i organisationsmedlemskab", + "Organisation name is required": "Organisationsnavn er påkrævet", + "Organisation settings saved successfully": "Organisationsindstillinger gemt", + "Organisation successfully created": "Organisation oprettet", + "Organisation successfully updated": "Organisation opdateret", + "Organisational measures": "Organisatoriske foranstaltninger", + "Organisations": "Organisationer", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organisation", + "Organization ID (Optional)": "Organisations-ID (valgfrit)", + "Organizations": "Organisationer", + "Orphaned Items": "Forældreløse elementer", + "Other": "Andet", + "Output Mapping": "Outputmapping", + "Overview": "Oversigt", + "Owner": "Ejer", + "Owner:": "Ejer:", + "PDF": "PDF", + "PII hits": "PII-træffere", + "PII rows": "PII-rækker", + "Page {current} of {total}": "Side {current} af {total}", + "Parallel Mode (Faster, more resource intensive)": "Parallel tilstand (hurtigere, mere ressourcekrævende)", + "Parallel:": "Parallel:", + "Password": "Adgangskode", + "Password changes are not supported by your authentication provider.": "Ændring af adgangskode understøttes ikke af din godkendelsesudbyder.", + "Password for SOLR authentication (optional)": "Adgangskode til SOLR-godkendelse (valgfri)", + "Password updated successfully": "Adgangskode opdateret", + "Path": "Sti", + "Path in repository": "Sti i lager", + "Path where the configuration file will be saved in the repository": "Sti, hvor konfigurationsfilen gemmes i lageret", + "Path where the register OAS file will be saved in the repository": "Sti, hvor registrets OAS-fil gemmes i lageret", + "Pattern (regex)": "Mønster (regex)", + "Pattern Issue": "Mønsterproblem", + "Pattern constraint is missing": "Mønsterbegrænsning mangler", + "Patterns:": "Mønstre:", + "Pending": "Afventer", + "Permanent Delete Mode": "Permanent sletningstilstand", + "Permanently Delete": "Slet permanent", + "Permanently Delete ({count})": "Slet permanent ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Slet alle {count} midlertidigt slettede objekter permanent. Dette kan ikke fortrydes!", + "Permanently delete already soft-deleted objects": "Slet allerede midlertidigt slettede objekter permanent", + "Permanently delete this conversation?": "Slet denne samtale permanent?", + "Permission Matrix": "Tilladelsesmatrix", + "Person": "Person", + "Personal Data": "Personoplysninger", + "Personal Data Export": "Eksport af personoplysninger", + "Persons": "Personer", + "Phone": "Telefon", + "Phone Numbers": "Telefonnumre", + "Photos": "Fotos", + "Please create an agent in the": "Opret en agent i", + "Please select a user": "Vælg en bruger", + "Please select an agent to continue": "Vælg en agent for at fortsætte", + "Please select an organisation": "Vælg en organisation", + "Please select at least one item to import": "Vælg mindst ét element at importere", + "Please select registers or schemas first": "Vælg registre eller skemaer først", + "Please select which register and schema to use for the new object": "Vælg hvilket register og skema der skal bruges til det nye objekt", + "Please try again later.": "Prøv igen senere.", + "Please wait while we fetch your agents.": "Vent, mens vi henter dine agenter.", + "Please wait while we fetch your applications.": "Vent, mens vi henter dine applikationer.", + "Please wait while we fetch your configurations.": "Vent, mens vi henter dine konfigurationer.", + "Please wait while we fetch your deleted items.": "Vent, mens vi henter dine slettede elementer.", + "Please wait while we fetch your sources.": "Vent, mens vi henter dine kilder.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Vent, mens vi sletter SOLR-samlingen permanent. Dette kan tage et øjeblik.", + "Polls": "Afstemninger", + "Popular Search Terms": "Populære søgeord", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Præferencer gemt", + "Prerequisites": "Forudsætninger", + "Press Enter to send, Shift+Enter for new line": "Tryk Enter for at sende, Shift+Enter for ny linje", + "Press enter to send, shift+enter for new line": "Tryk enter for at sende, shift+enter for ny linje", + "Prettify": "Forskøn", + "Preview erasure": "Forhåndsvis sletning", + "Preview:": "Forhåndsvisning:", + "Previous": "Forrige", + "Private": "Privat", + "Private Agent (Default)": "Privat agent (standard)", + "Private agents": "Private agenter", + "Process Name (optional)": "Procesnavn (valgfrit)", + "Processed {indexed} files, {failed} failed": "Behandlede {indexed} filer, {failed} mislykkedes", + "Processes chunks in batches with simulated parallelism.": "Behandler blokke i batches med simuleret parallelitet.", + "Processes file chunks sequentially (safest).": "Behandler filblokke sekventielt (sikrest).", + "Processes objects in chunks with simulated parallelism.": "Behandler objekter i blokke med simuleret parallelitet.", + "Processes objects sequentially (safest).": "Behandler objekter sekventielt (sikrest).", + "Processing": "Behandler", + "Processing Limits": "Behandlingsgrænser", + "Processing...": "Behandler...", + "Project initialized successfully": "Projekt initialiseret", + "Project not found. Please initialize first.": "Projekt ikke fundet. Initialiser først.", + "Properties": "Egenskaber", + "Property Behaviors": "Egenskabsadfærd", + "Property Configuration:": "Egenskabskonfiguration:", + "Property Filters": "Egenskabsfiltre", + "Property Title": "Egenskabstitel", + "Property Type": "Egenskabstype", + "Property can be improved": "Egenskaben kan forbedres", + "Property could not be added": "Egenskaben kunne ikke tilføjes", + "Property could not be updated": "Egenskaben kunne ikke opdateres", + "Property name of inversed relation": "Egenskabsnavn for omvendt relation", + "Property successfully added": "Egenskab tilføjet", + "Property successfully updated": "Egenskab opdateret", + "Protected": "Beskyttet", + "Provider": "Udbyder", + "Provider is required for testing": "Udbyder er påkrævet til test", + "Public": "Offentlig", + "Public View": "Offentlig visning", + "Public views can be accessed by anyone in the system": "Offentlige visninger kan tilgås af alle i systemet", + "Publish": "Udgiv", + "Publish Configuration to GitHub": "Udgiv konfiguration til GitHub", + "Publish OAS": "Udgiv OAS", + "Publish Objects": "Udgiv objekter", + "Publish Register OAS to GitHub": "Udgiv registrets OAS til GitHub", + "Publish all objects in this schema": "Udgiv alle objekter i dette skema", + "Publish objects to make them publicly available": "Udgiv objekter for at gøre dem offentligt tilgængelige", + "Published": "Udgivet", + "Published:": "Udgivet:", + "Publishing...": "Udgiver...", + "Purge": "Tøm", + "Purge Date": "Tømningsdato", + "Purge {count} object from database": "Tøm {count} objekt fra databasen", + "Purge {count} objects from database": "Tøm {count} objekter fra databasen", + "Purpose limitation *": "Formålsbegrænsning *", + "Push Notifications": "Push-notifikationer", + "Query Complexity Distribution": "Fordeling af forespørgselskompleksitet", + "Query Help": "Hjælp til forespørgsler", + "Query parameter is required": "Forespørgselsparameter er påkrævet", + "Quota": "Kvote", + "RAG": "RAG", + "RAG Configuration": "RAG-konfiguration", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC-indstillinger opdateret", + "Rate limits cleared successfully": "Hastighedsgrænser ryddet", + "Raw Changes Data": "Rå ændringsdata", + "Re-vectorize on object update": "Revektoriser ved objektopdatering", + "Read": "Læs", + "Real-time push notification status via notify_push": "Status for push-notifikationer i realtid via notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push-notifikationer i realtid er aktive. Forbundne klienter modtager øjeblikkelige opdateringer, når objekter oprettes, opdateres eller slettes.", + "Realtime push active": "Push i realtid aktiv", + "Realtime push not available — the notify_push app is not installed": "Push i realtid er ikke tilgængelig — notify_push-appen er ikke installeret", + "Reason": "Årsag", + "Reason (optional)": "Årsag (valgfri)", + "Rebase operation completed successfully": "Rebase-handling fuldført", + "Rebase operation failed: {error}": "Rebase-handling mislykkedes: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Anbefalinger:", + "Recommended Type:": "Anbefalet type:", + "Recommended: 5 sources": "Anbefalet: 5 kilder", + "Refresh": "Opdater", + "Refresh Data": "Opdater data", + "Refresh Stats": "Opdater statistik", + "Refresh Workflows": "Opdater arbejdsgange", + "Refresh dashboard": "Opdater dashboard", + "Refresh database info": "Opdater databaseoplysninger", + "Register": "Register", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Skema", + "Register ID": "Register-ID", + "Register Objects": "Registerobjekter", + "Register Overview": "Registeroversigt", + "Register Statistics": "Registerstatistik", + "Register Totals": "Registertotaler", + "Register and schema are required": "Register og skema er påkrævet", + "Register annotation": "Registerannotation", + "Register depublished successfully": "Register afpubliceret", + "Register not found": "Register ikke fundet", + "Register objects": "Registerobjekter", + "Register objects deletion completed successfully": "Sletning af registerobjekter blev gennemført", + "Register published successfully": "Registeret blev udgivet", + "Register/Schema Usage": "Register-/skemaforbrug", + "Register:": "Register:", + "Registers": "Registre", + "Reindex": "Geninddekser", + "Reindex completed": "Genindeksering fuldført", + "Reindex completed successfully": "Genindeksering blev gennemført", + "Reindex completed! Processed {count} objects in {duration}s": "Genindeksering fuldført! Behandlede {count} objekter på {duration}s", + "Reindex failed": "Genindeksering mislykkedes", + "Reindex failed: %s": "Genindeksering mislykkedes: %s", + "Reject": "Afvis", + "Rejected": "Afvist", + "Related Cases": "Relaterede sager", + "Relations": "Relationer", + "Remote Version": "Fjernversion", + "Remove": "Fjern", + "Remove Deck card": "Fjern Deck-kort", + "Remove avatar": "Fjern avatar", + "Remove contact": "Fjern kontakt", + "Remove filter": "Fjern filter", + "Remove from favorites": "Fjern fra favoritter", + "Remove from list": "Fjern fra liste", + "Remove group": "Fjern gruppe", + "Remove link": "Fjern link", + "Remove link to {name}": "Fjern link til {name}", + "Remove link to {name}?": "Fjern link til {name}?", + "Remove link to {title}": "Fjern link til {title}", + "Remove oneOf entry": "Fjern oneOf-post", + "Remove property after write-back": "Fjern egenskab efter tilbageskrivning", + "Remove {title}": "Fjern {title}", + "Remove {userId} from this organisation?": "Fjern {userId} fra denne organisation?", + "Removed": "Fjernet", + "Removed from favorites": "Fjernet fra favoritter", + "Removing schema {schema}...": "Fjerner skema {schema}...", + "Rename Conversation": "Omdøb samtale", + "Rename conversation": "Omdøb samtale", + "Replicas": "Replikaer", + "Reports": "Rapporter", + "Repository": "Lager", + "Request Body": "Anmodningstekst", + "Request Data": "Anmodningsdata", + "Request Details": "Anmodningsdetaljer", + "Request Quota (per day)": "Anmodningskvote (pr. dag)", + "Request account deactivation": "Anmod om deaktivering af konto", + "Request account deactivation. This will notify administrators for review.": "Anmod om deaktivering af konto. Dette vil underrette administratorer med henblik på gennemgang.", + "Request body must be JSON with a \"query\" field": "Anmodningsteksten skal være JSON med et \"query\"-felt", + "Request timeout in seconds": "Anmodningstimeout i sekunder", + "Requested at": "Anmodet den", + "Required": "Påkrævet", + "Required Token Scopes": "Påkrævede token-scopes", + "Required field": "Påkrævet felt", + "Required status is inconsistent": "Påkrævet-status er inkonsistent", + "Requires Dolphin AI for OCR text extraction": "Kræver Dolphin AI til OCR-tekstudtræk", + "Rerun Search": "Kør søgning igen", + "Rerunning search: {searchTerm}": "Kører søgning igen: {searchTerm}", + "Reset Changes": "Nulstil ændringer", + "Reset Filters": "Nulstil filtre", + "Resource Quotas": "Ressourcekvoter", + "Response Body": "Svartekst", + "Response Details": "Svardetaljer", + "Response Time:": "Svartid:", + "Restore": "Gendan", + "Restore conversation": "Gendan samtale", + "Restore or permanently delete items": "Gendan eller slet elementer permanent", + "Restore {count} object": "Gendan {count} objekt", + "Restore {count} objects": "Gendan {count} objekter", + "Result Count Range": "Interval for antal resultater", + "Results": "Resultater", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Opbevaringsperiode (ISO-8601-varighed, f.eks. P10Y, P30D)", + "Retention settings updated successfully": "Opbevaringsindstillinger blev opdateret", + "Retrieve all objects for this schema": "Hent alle objekter for dette skema", + "Retry": "Prøv igen", + "Retry Failed Extractions": "Prøv mislykkede udtræk igen", + "Retry Policy": "Genforsøgspolitik", + "Retry completed": "Genforsøg fuldført", + "Retry upload": "Prøv upload igen", + "Retrying...": "Prøver igen...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Gennemgå de valgte objekter nedenfor. Du kan fjerne objekter, du ikke ønsker at slette, ved at klikke på Fjern-knappen.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Gennemgå de valgte objekter nedenfor. Du kan fjerne objekter, du ikke ønsker at medtage.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Gennemgå de valgte objekter nedenfor. Du kan fjerne objekter, du ikke ønsker at slette permanent, ved at klikke på Fjern-knappen. Denne handling kan ikke fortrydes.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Gennemgå de valgte objekter nedenfor. Du kan fjerne objekter, du ikke ønsker at gendanne, ved at klikke på Fjern-knappen. Objekter gendannes til deres oprindelige placering.", + "Revoke": "Tilbagekald", + "Right": "Højre", + "Risk Level": "Risikoniveau", + "Risk level": "Risikoniveau", + "Roles and Permissions": "Roller og tilladelser", + "Roles updated successfully": "Roller blev opdateret", + "Run compliance scan": "Kør compliance-scanning", + "SOLR Connection Settings": "SOLR-forbindelsesindstillinger", + "SOLR Index Warmup": "SOLR-indeksopvarmning", + "SOLR Version:": "SOLR-version:", + "SOLR actions menu": "SOLR-handlingsmenu", + "SOLR base path (usually /solr)": "SOLR-basissti (normalt /solr)", + "SOLR collection deleted successfully": "SOLR-samling blev slettet", + "SOLR connection test failed: {error}": "SOLR-forbindelsestest mislykkedes: {error}", + "SOLR connection test successful!": "SOLR-forbindelsestest lykkedes!", + "SOLR index warmup completed successfully": "SOLR-indeksopvarmning blev gennemført", + "SOLR is disabled": "SOLR er deaktiveret", + "SOLR is not available or not configured": "SOLR er ikke tilgængelig eller ikke konfigureret", + "SOLR logging disabled": "SOLR-logning deaktiveret", + "SOLR logging enabled": "SOLR-logning aktiveret", + "SOLR search disabled": "SOLR-søgning deaktiveret", + "SOLR search enabled": "SOLR-søgning aktiveret", + "SOLR server hostname or IP address": "SOLR-serverens værtsnavn eller IP-adresse", + "SOLR server port number (optional, defaults to 8983)": "SOLR-serverens portnummer (valgfrit, standard er 8983)", + "SOLR settings updated successfully": "SOLR-indstillinger blev opdateret", + "SOLR setup completed successfully": "SOLR-opsætning blev gennemført", + "SOLR setup completed successfully!": "SOLR-opsætning blev gennemført!", + "SOLR setup error: %s": "SOLR-opsætningsfejl: %s", + "SOLR setup failed": "SOLR-opsætning mislykkedes", + "SOLR setup failed - check logs": "SOLR-opsætning mislykkedes - kontrollér logfiler", + "SOLR setup failed. Please check the configuration and try again.": "SOLR-opsætning mislykkedes. Kontrollér konfigurationen, og prøv igen.", + "SOLR setup failed: {error}": "SOLR-opsætning mislykkedes: {error}", + "SOLR warmup failed: {error}": "SOLR-opvarmning mislykkedes: {error}", + "Sample Values:": "Eksempelværdier:", + "Save": "Gem", + "Save Configuration": "Gem konfiguration", + "Save Connection Settings": "Gem forbindelsesindstillinger", + "Save Roles": "Gem roller", + "Save Settings": "Gem indstillinger", + "Save changes": "Gem ændringer", + "Save current search as view": "Gem den aktuelle søgning som visning", + "Save labels": "Gem etiketter", + "Save tags for {name}": "Gem mærker for {name}", + "Save the organisation first to manage users.": "Gem organisationen først for at administrere brugere.", + "Saved Views": "Gemte visninger", + "Saving...": "Gemmer...", + "Scheduled": "Planlagt", + "Schema": "Skema", + "Schema ID": "Skema-id", + "Schema Name": "Skemanavn", + "Schema Statistics": "Skemastatistik", + "Schema annotation": "Skemaannotation", + "Schema configuration updated successfully for {schema}": "Skemakonfiguration blev opdateret for {schema}", + "Schema depublished successfully": "Skema blev afpubliceret", + "Schema objects deletion completed successfully": "Sletning af skemaobjekter blev gennemført", + "Schema published successfully": "Skema blev udgivet", + "Schema reference ($ref)": "Skemareference ($ref)", + "Schema successfully created": "Skema blev oprettet", + "Schema successfully updated": "Skema blev opdateret", + "Schema {schema} removed successfully": "Skema {schema} blev fjernet", + "Schema:": "Skema:", + "Schemas": "Skemaer", + "Scheme": "Skema", + "Search": "Søg", + "Search / Views": "Søgning / visninger", + "Search / views": "Søgning / visninger", + "Search Activity": "Søgeaktivitet", + "Search Analytics": "Søgeanalyse", + "Search Configuration": "Søgekonfiguration", + "Search Entities": "Søg i entiteter", + "Search Files": "Søg i filer", + "Search GitHub": "Søg i GitHub", + "Search GitLab": "Søg i GitLab", + "Search Method": "Søgemetode", + "Search Mode": "Søgetilstand", + "Search Query": "Søgeforespørgsel", + "Search Results": "Søgeresultater", + "Search Term": "Søgeterm", + "Search Trail Details": "Detaljer for søgespor", + "Search Trail Management": "Administration af søgespor", + "Search Trail Statistics": "Statistik for søgespor", + "Search Trails": "Søgespor", + "Search Views": "Søgevisninger", + "Search Webhooks": "Søg i webhooks", + "Search agents...": "Søg i agenter...", + "Search applications...": "Søg i applikationer...", + "Search by file name or path": "Søg efter filnavn eller sti", + "Search by name or URL": "Søg efter navn eller URL", + "Search by title or UUID...": "Søg efter titel eller UUID...", + "Search by value": "Søg efter værdi", + "Search configurations": "Søg i konfigurationer", + "Search data sources...": "Søg i datakilder...", + "Search groups...": "Søg i grupper...", + "Search in Database Objects": "Søg i databaseobjekter", + "Search in Files": "Søg i filer", + "Search in Objects": "Søg i objekter", + "Search objects": "Søg i objekter", + "Search objects...": "Søg i objekter...", + "Search property names...": "Søg efter egenskabsnavne...", + "Search registers...": "Søg i registre...", + "Search schemas...": "Søg i skemaer...", + "Search trail deletion not implemented yet": "Sletning af søgespor er endnu ikke implementeret", + "Search views...": "Søg i visninger...", + "Search {name}...": "Søg i {name}...", + "Searching...": "Søger...", + "Secret": "Hemmelighed", + "Secret key for HMAC signature generation (optional)": "Hemmelig nøgle til generering af HMAC-signatur (valgfrit)", + "Security": "Sikkerhed", + "See organisations as a table": "Se organisationer som en tabel", + "See organisations as cards": "Se organisationer som kort", + "Select AI Agent": "Vælg AI-agent", + "Select Active Organisation": "Vælg aktiv organisation", + "Select All": "Vælg alle", + "Select ConfigSet": "Vælg ConfigSet", + "Select File Types to Vectorize:": "Vælg filtyper til vektorisering:", + "Select HTTP method": "Vælg HTTP-metode", + "Select Register and Schema": "Vælg register og skema", + "Select Views to Vectorize:": "Vælg visninger til vektorisering:", + "Select a Nextcloud group to add": "Vælg en Nextcloud-gruppe, der skal tilføjes", + "Select a branch": "Vælg en gren", + "Select a date property": "Vælg en datoegenskab", + "Select a model or type a custom model name": "Vælg en model, eller indtast et brugerdefineret modelnavn", + "Select a register": "Vælg et register", + "Select a register...": "Vælg et register...", + "Select a repository": "Vælg et lager", + "Select a repository you have write access to": "Vælg et lager, du har skriveadgang til", + "Select a schema": "Vælg et skema", + "Select a schema...": "Vælg et skema...", + "Select a user": "Vælg en bruger", + "Select agent type": "Vælg agenttype", + "Select all entities": "Vælg alle entiteter", + "Select an AI Agent": "Vælg en AI-agent", + "Select an AI agent to begin chatting with your data.": "Vælg en AI-agent for at begynde at chatte med dine data.", + "Select an email to see linked objects": "Vælg en e-mail for at se sammenkædede objekter", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Vælg en organisation og en bruger for at tilføje dem som medlem. Søg efter organisationer ved navn.", + "Select backend": "Vælg backend", + "Select chat model": "Vælg chatmodel", + "Select collection for files": "Vælg samling til filer", + "Select collection for objects": "Vælg samling til objekter", + "Select configuration type...": "Vælg konfigurationstype...", + "Select default organisation": "Vælg standardorganisation", + "Select entity {value}": "Vælg entitet {value}", + "Select event to listen to...": "Vælg hændelse at lytte efter...", + "Select fields to display": "Vælg felter, der skal vises", + "Select files with the correct extension": "Vælg filer med den korrekte filtype", + "Select group": "Vælg gruppe", + "Select groups (optional)": "Vælg grupper (valgfrit)", + "Select groups to notify...": "Vælg grupper, der skal underrettes...", + "Select groups with access to this agent": "Vælg grupper med adgang til denne agent", + "Select groups...": "Vælg grupper...", + "Select model": "Vælg model", + "Select one or more registers": "Vælg et eller flere registre", + "Select one or more schemas": "Vælg et eller flere skemaer", + "Select options...": "Vælg indstillinger...", + "Select or create labels, or select \"No label\" to add files": "Vælg eller opret etiketter, eller vælg \"Ingen etiket\" for at tilføje filer", + "Select owner application (optional)...": "Vælg ejerapplikation (valgfrit)...", + "Select period": "Vælg periode", + "Select property to send as payload": "Vælg egenskab, der skal sendes som nyttelast", + "Select provider": "Vælg udbyder", + "Select registers and schemas to save a view": "Vælg registre og skemaer for at gemme en visning", + "Select retry policy": "Vælg genforsøgspolitik", + "Select role": "Vælg rolle", + "Select schemas (empty = all schemas)": "Vælg skemaer (tom = alle skemaer)", + "Select schemas to warm up": "Vælg skemaer, der skal varmes op", + "Select search mode": "Vælg søgetilstand", + "Select source type...": "Vælg kildetype...", + "Select target type": "Vælg måltype", + "Select the branch to publish to": "Vælg den gren, der skal udgives til", + "Select the event this webhook should listen to": "Vælg den hændelse, denne webhook skal lytte efter", + "Select views to filter data (optional)": "Vælg visninger for at filtrere data (valgfrit)", + "Select views to limit which data the agent can access": "Vælg visninger for at begrænse, hvilke data agenten kan tilgå", + "Select views to vectorize:": "Vælg visninger til vektorisering:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Vælg hvilke Nextcloud-grupper, der er tilgængelige for denne organisation. Brugere i disse grupper får adgang til organisationens ressourcer.", + "Select which data views the AI can search": "Vælg hvilke datavisninger AI'en kan søge i", + "Select which property from the event should be used as the webhook payload data": "Vælg hvilken egenskab fra hændelsen, der skal bruges som webhookens nyttelastdata", + "Select which tools the AI can use to perform actions": "Vælg hvilke værktøjer AI'en kan bruge til at udføre handlinger", + "Select {fieldName} values": "Vælg {fieldName}-værdier", + "Selected": "Valgt", + "Selected Groups": "Valgte grupper", + "Selected Objects ({count})": "Valgte objekter ({count})", + "Selected audit trails deleted successfully": "Valgte revisionsspor blev slettet", + "Selected search trails deleted successfully": "Valgte søgespor blev slettet", + "Selected users can access this view": "Valgte brugere kan tilgå denne visning", + "Send additional feedback": "Send yderligere feedback", + "Send as CloudEvent": "Send som CloudEvent", + "Send as cloudevent": "Send som cloudevent", + "Sensitive PII": "Følsomme personoplysninger", + "Serial Mode (Safer, slower)": "Seriel tilstand (sikrere, langsommere)", + "Serial:": "Seriel:", + "Server Configuration": "Serverkonfiguration", + "Server Information": "Serveroplysninger", + "Set as Active": "Angiv som aktiv", + "Set as active organisation": "Angiv som aktiv organisation", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Angiv grænser for API-forbrug og tokenforbrug. Brug 0 for ubegrænsede ressourcer.", + "Settings": "Indstillinger", + "Settings updated successfully": "Indstillinger blev opdateret", + "Shards": "Shards", + "Share": "Del", + "Share with Groups": "Del med grupper", + "Share with Users": "Del med brugere", + "Show": "Vis", + "Show Filters": "Vis filtre", + "Show less": "Vis mindre", + "Show only entries with changes": "Vis kun poster med ændringer", + "Showing {count} dashboard(s)": "Viser {count} dashboard(s)", + "Showing {count} processing activities": "Viser {count} behandlingsaktiviteter", + "Showing {showing} of {total} agents": "Viser {showing} af {total} agenter", + "Showing {showing} of {total} applications": "Viser {showing} af {total} applikationer", + "Showing {showing} of {total} audit trail entries": "Viser {showing} af {total} revisionssporposter", + "Showing {showing} of {total} configurations": "Viser {showing} af {total} konfigurationer", + "Showing {showing} of {total} deleted items": "Viser {showing} af {total} slettede elementer", + "Showing {showing} of {total} entities": "Viser {showing} af {total} entiteter", + "Showing {showing} of {total} files": "Viser {showing} af {total} filer", + "Showing {showing} of {total} log entries": "Viser {showing} af {total} logposter", + "Showing {showing} of {total} search trail entries": "Viser {showing} af {total} søgesporposter", + "Showing {showing} of {total} sources": "Viser {showing} af {total} kilder", + "Showing {showing} of {total} templates": "Viser {showing} af {total} skabeloner", + "Showing {showing} of {total} webhooks": "Viser {showing} af {total} webhooks", + "Showing {shown} of {total} organisations": "Viser {shown} af {total} organisationer", + "Simple": "Enkel", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Enkle forespørgsler: Grundlæggende tekstsøgninger med minimale parametre (f.eks. én søgeterm, ingen avancerede filtre)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Enkle forespørgsler: grundlæggende tekstsøgninger med minimale parametre (f.eks. én søgeterm, ingen avancerede filtre)", + "Size": "Størrelse", + "Slug": "Slug", + "Soft Delete Mode": "Tilstand for blød sletning", + "Soft Deleted Items": "Blødt slettede elementer", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Blød sletning af alle objekter for dette skema ({active} aktive, {deleted} allerede slettet)", + "SolrCloud mode enabled": "SolrCloud-tilstand aktiveret", + "Something went wrong while adding files": "Noget gik galt under tilføjelse af filer", + "Source": "Kilde", + "Source successfully updated": "Kilden blev opdateret", + "Sources": "Kilder", + "Sources:": "Kilder:", + "Special Rights": "Særlige rettigheder", + "Standalone SOLR mode": "Selvstændig SOLR-tilstand", + "Start Conversation": "Start samtale", + "Start Date Field": "Startdatofelt", + "Start Vectorization": "Start vektorisering", + "Start a conversation": "Start en samtale", + "Starting names cache warmup...": "Starter opvarmning af navnecache...", + "Starting validation for {schema}...": "Starter validering for {schema}...", + "Starting {action} for {schema}...": "Starter {action} for {schema}...", + "Starting...": "Starter...", + "Statistics": "Statistik", + "Statistics and Metrics": "Statistik og metrikker", + "Statistics refreshed": "Statistik opdateret", + "Stats calculation not yet implemented": "Beregning af statistik er endnu ikke implementeret", + "Status": "Status", + "Status Code": "Statuskode", + "Step": "Trin", + "Storage": "Lager", + "Storage Quota (MB)": "Lagerkvote (MB)", + "String Constraints": "Strengbegrænsninger", + "Subject identifier (email, BSN, name, etc.)": "Subjektidentifikator (e-mail, BSN, navn osv.)", + "Subscribe": "Abonnér", + "Subscribed": "Abonneret", + "Success": "Succes", + "Success Rate": "Succesrate", + "Success Status": "Successtatus", + "Successful": "Lykkedes", + "Successfully added user to organisation: {name}": "Brugeren blev tilføjet til organisationen: {name}", + "Successfully cleared {count} audit trails": "{count} revisionsspor blev ryddet", + "Successfully cleared {count} blob storage objects": "{count} blob-lagerobjekter blev ryddet", + "Successfully cleared {count} search trails": "{count} søgespor blev ryddet", + "Successfully created {count} SOLR fields": "{count} SOLR-felter blev oprettet", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "{count} embeddings blev slettet. Vektorisér dine data igen.", + "Successfully deleted {count} objects for {schema}": "{count} objekter blev slettet for {schema}", + "Successfully fixed {count} SOLR field configurations": "{count} SOLR-feltkonfigurationer blev rettet", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importeret: {registers} registre, {schemas} skemaer, {objects} objekter", + "Successfully joined organisation": "Du blev medlem af organisationen", + "Successfully permanently deleted {count} objects": "{count} objekter blev slettet permanent", + "Successfully processed {count} files!": "{count} filer blev behandlet!", + "Successfully restored {count} object": "{count} objekt blev gendannet", + "Successfully restored {count} objects": "{count} objekter blev gendannet", + "Successfully saved configuration for {count} facets!": "Konfiguration for {count} facetter blev gemt!", + "Supported File Types": "Understøttede filtyper", + "Switch Active Organisation": "Skift aktiv organisation", + "Switch Organisation": "Skift organisation", + "Sync Table": "Synkroniser tabel", + "Synchronization Settings": "Synkroniseringsindstillinger", + "Syncing magic table for {schema}...": "Synkroniserer magisk tabel for {schema}...", + "Syncs": "Synkroniseringer", + "System Default": "Systemstandard", + "System Prompt": "Systemprompt", + "System announcements": "Systemmeddelelser", + "Table": "Tabel", + "Target ID": "Mål-id", + "Target Type*": "Måltype*", + "Technical Description": "Teknisk beskrivelse", + "Technical Details": "Tekniske detaljer", + "Technical description for developers and administrators": "Teknisk beskrivelse til udviklere og administratorer", + "Technical measures": "Tekniske foranstaltninger", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatur", + "Templates": "Skabeloner", + "Test": "Test", + "Test Chat": "Testchat", + "Test Connection": "Test forbindelse", + "Test Embedding": "Test embedding", + "Test endpoint executed successfully": "Testendepunkt blev udført", + "Test webhook delivered successfully": "Testwebhook blev leveret", + "Test webhook delivery failed": "Levering af testwebhook mislykkedes", + "Test webhook sent successfully": "Testwebhook blev sendt", + "Testing...": "Tester...", + "Text Chunk #{id}": "Tekstblok #{id}", + "Text Extraction": "Tekstudtræk", + "Text Extraction Engine": "Tekstudtræksmotor", + "Text chunks": "Tekstblokke", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Tekstblokke genereres under filudtræk og lagres i databasen. Vektorisering læser disse forhåndsopdelte filer og konverterer dem til embeddings.", + "Text extracted successfully": "Tekst blev udtrukket", + "Text extraction disabled": "Tekstudtræk deaktiveret", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloud Calendar-appen er ikke installeret eller aktiveret på denne server.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloud Contacts-appen er ikke installeret eller aktiveret på denne server.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloud Deck-appen er ikke installeret eller aktiveret på denne server.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloud Mail-appen er ikke installeret eller aktiveret på denne server.", + "The URL where webhook events will be sent": "URL'en som webhook-hændelser vil blive sendt til", + "The requested conversation does not exist": "Den ønskede samtale findes ikke", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Det ønskede dashboard kunne ikke indlæses. Kontrollér URL'en eller vælg et andet dashboard fra listen.", + "The saved view and all its search configuration": "Den gemte visning og hele dens søgekonfiguration", + "There are no audit trail entries matching your current filters.": "Der er ingen revisionsspor-poster, der matcher dine nuværende filtre.", + "There are no deleted items matching your current filters.": "Der er ingen slettede elementer, der matcher dine nuværende filtre.", + "There are no search trail entries matching your current filters.": "Der er ingen søgespor-poster, der matcher dine nuværende filtre.", + "There are no webhook log entries matching your filters.": "Der er ingen webhook-logposter, der matcher dine filtre.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "De vil blive bevaret i henhold til deres skemas konfigurerede opbevaringsperiode og automatisk slettet permanent, når opbevaringsperioden udløber. Opbevaringsperioden kan konfigureres pr. skema og kan findes i skemaets indstillinger.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "De vil blive bevaret i henhold til deres skemas konfigurerede opbevaringsperiode og derefter fjernet permanent.", + "This action cannot be undone.": "Denne handling kan ikke fortrydes.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Denne handling kan ikke fortrydes. Sørg for, at ingen samlinger bruger dette ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Denne handling kan ikke fortrydes. Sørg for, at ingen samlinger bruger dette configset.", + "This action will submit a deactivation request to your administrators.": "Denne handling sender en anmodning om deaktivering til dine administratorer.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Denne agent har ingen konfigurerbare visninger eller værktøjer, men du kan stadig justere RAG-indstillingerne nedenfor.", + "This analysis may take some time": "Denne analyse kan tage noget tid", + "This audit trail entry does not contain any change information.": "Denne revisionsspor-post indeholder ingen ændringsoplysninger.", + "This configuration has no data defined.": "Denne konfiguration har ingen data defineret.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Dette endpoint er forældet. Brug chunk-baserede endpoints i stedet.", + "This entity has no relations to objects or files": "Denne entitet har ingen relationer til objekter eller filer", + "This is a test webhook from OpenRegister": "Dette er en test-webhook fra OpenRegister", + "This is not valid JSON": "Dette er ikke gyldig JSON", + "This key already exists on this schema": "Denne nøgle findes allerede på dette skema", + "This schema must use Magic Table configuration to sync": "Dette skema skal bruge Magic Table-konfiguration for at synkronisere", + "This schema must use magic table configuration to sync": "Dette skema skal bruge magic table-konfiguration for at synkronisere", + "This source has no associated registers.": "Denne kilde har ingen tilknyttede registre.", + "This token will only be shown once. Copy it now.": "Dette token vises kun én gang. Kopiér det nu.", + "This will copy the _default ConfigSet with the new name": "Dette vil kopiere _default-ConfigSet'et med det nye navn", + "This will copy the _default configset with the new name": "Dette vil kopiere _default-configset'et med det nye navn", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Dette vil permanent slette ALLE indlejringer (vektorer) fra databasen. Du bliver nødt til at revektorisere alle objekter og filer. Denne handling kan ikke fortrydes.\n\nEr du sikker på, at du vil fortsætte?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Dette vil permanent slette ALLE indlejringer (vektorer) fra databasen. Du bliver nødt til at revektorisere alle objekter og filer. Denne handling kan ikke fortrydes.\\n\\nEr du sikker på, at du vil fortsætte?", + "This will permanently delete:": "Dette vil permanent slette:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Dette vil permanent fjerne {count} allerede blødt slettede objekter fra databasen. Denne handling kan ikke fortrydes.", + "Timeout (seconds)": "Timeout (sekunder)", + "Timestamp": "Tidsstempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: Aktivér kun visninger, der har brug for semantisk søgning, for at minimere omkostningerne til indlejring. Simple opslagstabeller har sjældent brug for vektorisering.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: aktivér kun visninger, der har brug for semantisk søgning, for at minimere omkostningerne til indlejring. Simple opslagstabeller har sjældent brug for vektorisering.", + "Title": "Titel", + "Title *": "Titel *", + "Title Template": "Titelskabelon", + "Title*": "Titel*", + "To (e.g. -1 year or 2025-12-31)": "Til (f.eks. -1 year eller 2025-12-31)", + "To Date": "Til-dato", + "To add files larger than or equal to 512MB, go to the": "For at tilføje filer på 512 MB eller derover skal du gå til", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "For at justere chunk-størrelse eller -strategi skal du gå til Filkonfiguration → Behandlingsgrænser.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "For at justere chunk-størrelse eller -strategi skal du gå til filkonfiguration → behandlingsgrænser.", + "To change the embedding provider or model, go to LLM Configuration.": "For at ændre indlejringsudbyderen eller -modellen skal du gå til LLM-konfiguration.", + "To change the embedding provider or model, go to LLM configuration.": "For at ændre indlejringsudbyderen eller -modellen skal du gå til LLM-konfiguration.", + "To date": "Til-dato", + "Toggle search sidebar": "Slå søgesidebjælke til/fra", + "Token Created": "Token oprettet", + "Token Quota (per request)": "Token-kvote (pr. anmodning)", + "Token copied to clipboard": "Token kopieret til udklipsholder", + "Token name": "Token-navn", + "Token revoked": "Token tilbagekaldt", + "Tools": "Værktøjer", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Værktøjer udføres med agentens standardbrugertilladelser, når der ikke er en aktiv brugersession (f.eks. cron-jobs). Konfigurer standardbrugeren under fanen Indstillinger.", + "Tools respect the agent's own role and organization boundaries.": "Værktøjer respekterer agentens egen rolle og organisationsgrænser.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Værktøjer respekterer agentens visninger, tilladelser og organisationsgrænser.", + "Top Deleters": "Hyppigste slettere", + "Total": "I alt", + "Total Audit Trails": "Revisionsspor i alt", + "Total Chunks Available:": "Tilgængelige chunks i alt:", + "Total Deleted Items": "Slettede elementer i alt", + "Total Members": "Medlemmer i alt", + "Total Objects": "Objekter i alt", + "Total Objects in Database:": "Objekter i databasen i alt:", + "Total Organisations": "Organisationer i alt", + "Total Results": "Resultater i alt", + "Total Searches": "Søgninger i alt", + "Total Size": "Samlet størrelse", + "Total Storage": "Samlet lagerplads", + "Total events": "Hændelser i alt", + "Total:": "I alt:", + "Totals": "Totaler", + "Try searching by UUID or with different keywords": "Prøv at søge efter UUID eller med andre nøgleord", + "Type": "Type", + "Type \"DELETE COLLECTION\" to confirm:": "Skriv \"DELETE COLLECTION\" for at bekræfte:", + "Type Issue": "Typeproblem", + "Type Variations:": "Typevariationer:", + "Type filter (optional, e.g. email)": "Typefilter (valgfrit, f.eks. e-mail)", + "Type must be \"positive\" or \"negative\"": "Type skal være \"positive\" eller \"negative\"", + "Type to search for groups": "Skriv for at søge efter grupper", + "Type to search for objects...": "Skriv for at søge efter objekter...", + "Type to search for organisations": "Skriv for at søge efter organisationer", + "Type to search for users": "Skriv for at søge efter brugere", + "Type to search groups...": "Skriv for at søge i grupper...", + "Type to search users...": "Skriv for at søge i brugere...", + "Type to search...": "Skriv for at søge...", + "Type your username to confirm": "Skriv dit brugernavn for at bekræfte", + "URL": "URL", + "URL where Ollama is running": "URL hvor Ollama kører", + "UUID copied to clipboard": "UUID kopieret til udklipsholder", + "UUID:": "UUID:", + "Unchanged": "Uændret", + "Unique Search Terms": "Unikke søgetermer", + "Unique Terms": "Unikke termer", + "Unknown": "Ukendt", + "Unknown Object": "Ukendt objekt", + "Unknown Register": "Ukendt register", + "Unknown Schema": "Ukendt skema", + "Unknown error": "Ukendt fejl", + "Unknown widget type: {type}": "Ukendt widget-type: {type}", + "Unlink email": "Fjern tilknytning af e-mail", + "Unlink event": "Fjern tilknytning af hændelse", + "Unnamed Object": "Unavngivet objekt", + "Untitled": "Uden titel", + "Untitled View": "Visning uden titel", + "Update": "Opdatér", + "Update Operations": "Opdateringshandlinger", + "Update available: {local} → {remote}": "Opdatering tilgængelig: {local} → {remote}", + "Update configuration: ...": "Opdatér konfiguration: ...", + "Update register OAS: ...": "Opdatér register-OAS: ...", + "Update vectors when object data changes (recommended for accurate search)": "Opdatér vektorer, når objektdata ændres (anbefales for nøjagtig søgning)", + "Updated": "Opdateret", + "Updated At": "Opdateret den", + "Updated:": "Opdateret:", + "Upload new avatar": "Upload ny avatar", + "Uploading configuration sets...": "Uploader konfigurationssæt...", + "Uptime:": "Oppetid:", + "Url": "Url", + "Usage Count": "Antal anvendelser", + "Use AI agents for processing and analysis": "Brug AI-agenter til behandling og analyse", + "Use Large Language Model features": "Brug funktioner til store sprogmodeller", + "Use SolrCloud with Zookeeper for distributed search": "Brug SolrCloud med Zookeeper til distribueret søgning", + "Use SolrCloud with zookeeper for distributed search": "Brug SolrCloud med zookeeper til distribueret søgning", + "Use VNG APIs instead of MongoDB.": "Brug VNG-API'er i stedet for MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Brug standardintervaller (Sidste 7/30/90 dage, Sidste år, Ældre)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Brug ekstern lagring (f.eks. MongoDB) i stedet for den interne Nextcloud-lagring.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Brug filtre til at indsnævre revisionsspor-poster efter register, skema, handlingstype, bruger, datointerval eller objekt-ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Brug filtre til at indsnævre slettede elementer efter register, skema, sletningsdato eller den bruger, der slettede dem.", + "Use filters to narrow down entities by type or category.": "Brug filtre til at indsnævre entiteter efter type eller kategori.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Brug filtre til at indsnævre søgespor-poster efter register, skema, successtatus, bruger, datointerval, søgetermer eller ydelsesmålinger.", + "Used By Collections": "Brugt af samlinger", + "Used by": "Brugt af", + "User": "Bruger", + "User Agent Statistics": "User Agent-statistik", + "User Description": "Brugerbeskrivelse", + "User Quota": "Brugerkvote", + "User rate limits cleared successfully": "Brugerens hastighedsgrænser blev ryddet", + "User removed successfully": "Brugeren blev fjernet", + "User-friendly description shown in forms and help text": "Brugervenlig beskrivelse, der vises i formularer og hjælpetekst", + "Username": "Brugernavn", + "Username for SOLR authentication (optional)": "Brugernavn til SOLR-godkendelse (valgfrit)", + "Users": "Brugere", + "Uses": "Anvendelser", + "Using Pre-Generated Chunks": "Bruger forhåndsgenererede chunks", + "Validate": "Validér", + "Validate Objects": "Validér objekter", + "Validation completed for {schema}": "Validering fuldført for {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validering fuldført for {schema}: {processed} behandlet, {updated} opdateret, {failed} mislykkedes", + "Validation completed successfully": "Validering blev fuldført", + "Validation failed": "Validering mislykkedes", + "Value": "Værdi", + "Value list (split on ,)": "Værdiliste (opdelt ved ,)", + "Vector Dimensions": "Vektordimensioner", + "Vector Search Backend": "Backend til vektorsøgning", + "Vector field: _embedding_": "Vektorfelt: _embedding_", + "Vectorization Triggers": "Vektoriseringsudløsere", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorisering fuldført: {vectorized} objekter vektoriseret, {failed} mislykkedes", + "Vectorization failed: {error}": "Vektorisering mislykkedes: {error}", + "Vectorize All Files": "Vektorisér alle filer", + "Vectorize All Objects": "Vektorisér alle objekter", + "Vectorize all file types": "Vektorisér alle filtyper", + "Vectorize all views": "Vektorisér alle visninger", + "Vectorize on object creation": "Vektorisér ved oprettelse af objekt", + "Vectors will be stored in your existing object and file collections": "Vektorer vil blive gemt i dine eksisterende objekt- og filsamlinger", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Verificerer serverforbindelse...", + "Version": "Version", + "Version Information": "Versionsoplysninger", + "Very High": "Meget høj", + "Very high": "Meget høj", + "View": "Visning", + "View API Docs": "Vis API-dokumentation", + "View API Documentation": "Vis API-dokumentation", + "View APIs": "Vis API'er", + "View Changes": "Vis ændringer", + "View Details": "Vis detaljer", + "View Error": "Vis fejl", + "View File": "Vis fil", + "View Full Details": "Vis fulde detaljer", + "View Logs": "Vis logge", + "View Name": "Visningsnavn", + "View Object": "Vis objekt", + "View Parameters": "Vis parametre", + "View Selection": "Valg af visning", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Vis og analysér søgespor-logge med avancerede filtrerings- og analysefunktioner", + "View and analyze system audit trails with advanced filtering capabilities": "Vis og analysér systemets revisionsspor med avancerede filtreringsfunktioner", + "View and manage authorization across registers and schemas": "Vis og administrér autorisation på tværs af registre og skemaer", + "View deleted successfully": "Visningen blev slettet", + "View entity details and manage relations": "Vis entitetsdetaljer og administrér relationer", + "View name": "Visningsnavn", + "View name is required": "Visningsnavn er påkrævet", + "View object": "Vis objekt", + "View saved successfully!": "Visningen blev gemt!", + "View search analytics and manage search logs": "Vis søgeanalyser og administrér søgelogge", + "View successfully deleted": "Visningen blev slettet", + "View successfully updated": "Visningen blev opdateret", + "View updated successfully!": "Visningen blev opdateret!", + "View webhook delivery logs and filter by webhook": "Vis logge over webhook-leveringer og filtrér efter webhook", + "View {count} more": "Vis {count} flere", + "Views": "Visninger", + "Visible to end users": "Synlig for slutbrugere", + "Visible to users": "Synlig for brugere", + "Wait for Response": "Vent på svar", + "Wait for webhook response before continuing (required for request/response flows)": "Vent på webhook-svar, før der fortsættes (påkrævet for anmodnings-/svarforløb)", + "Waiting for cluster synchronization...": "Venter på klyngesynkronisering...", + "Warmup Names Cache": "Opvarm navnecache", + "Warmup failed": "Opvarmning mislykkedes", + "Warning": "Advarsel", + "Warning:": "Advarsel:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Advarsel: Denne egenskab har inkonsistente typer: {types}. Overvej, om alle objekter bør have samme type.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Vi scanner alle objekter, der tilhører dette skema, for at finde nye egenskaber og analysere eksisterende egenskaber for mulige forbedringer. Processen omfatter undersøgelse af hvert objekts datastruktur, identifikation af egenskaber, der ikke er defineret i det nuværende skema, og søgning efter muligheder for at forbedre eksisterende egenskabsdefinitioner med bedre begrænsninger, formater og valideringsregler.", + "Webhook": "Webhook", + "Webhook Log Details": "Webhook-logdetaljer", + "Webhook Logs": "Webhook-logge", + "Webhook created successfully": "Webhook blev oprettet", + "Webhook deleted": "Webhook slettet", + "Webhook retry delivered successfully": "Genforsøg af webhook blev leveret", + "Webhook retry delivery failed": "Genforsøg af webhook-levering mislykkedes", + "Webhook updated": "Webhook opdateret", + "Webhook updated successfully": "Webhook blev opdateret", + "Webhooks": "Webhooks", + "Weekly": "Ugentlig", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Når agenten kører uden en brugersession (f.eks. planlagte opgaver), bruges denne brugers kontekst", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Når agenten kører uden en brugersession (f.eks. planlagte opgaver), bruges denne brugers kontekst.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Når dette er deaktiveret, viser denne facet kun værdier fra dette skema og inkluderer et skemafilter, når det vælges.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Når dette er aktiveret, vil lagring af dette objekt også opdatere målobjekterne til at indeholde en reference tilbage til dette objekt.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Når dette er aktiveret, vil denne egenskab blive fjernet fra kildeobjektet efter opdatering af målobjekterne.", + "Workflow Management": "Workflow-administration", + "Workflows": "Workflows", + "Wrap webhook payload in CloudEvents format for better interoperability": "Indpak webhook-payload i CloudEvents-format for bedre interoperabilitet", + "Wrap webhook payload in cloudevents format for better interoperability": "Indpak webhook-payload i cloudevents-format for bedre interoperabilitet", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Ja", + "You": "Dig", + "You are not yet a member of any organisations.": "Du er endnu ikke medlem af nogen organisationer.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Du kan oprette nye ConfigSets baseret på _default-skabelonen eller uploade brugerdefinerede direkte til din SOLR-server.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Du kan oprette nye configsets baseret på _default-skabelonen eller uploade brugerdefinerede direkte til din SOLR-server.", + "You do not have access to this conversation": "Du har ikke adgang til denne samtale", + "You do not have permission to delete this conversation": "Du har ikke tilladelse til at slette denne samtale", + "You do not have permission to modify this conversation": "Du har ikke tilladelse til at ændre denne samtale", + "You do not have permission to restore this conversation": "Du har ikke tilladelse til at gendanne denne samtale", + "You do not have permission to view the permission matrix. Admin access is required.": "Du har ikke tilladelse til at se tilladelsesmatrixen. Administratoradgang er påkrævet.", + "You have selected {count} properties to add to the schema.": "Du har valgt {count} egenskaber, der skal tilføjes til skemaet.", + "You must be logged in to favorite views": "Du skal være logget ind for at markere visninger som favoritter", + "You need an AI agent to start a conversation.": "Du skal bruge en AI-agent for at starte en samtale.", + "Your Fireworks AI API key. Get one at": "Din Fireworks AI API-nøgle. Få en på", + "Your OpenAI API key. Get one at": "Din OpenAI API-nøgle. Få en på", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Din feedback er blevet registreret. Du kan eventuelt angive yderligere detaljer her...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Din feedback er blevet registreret. du kan eventuelt angive yderligere detaljer her...", + "Zookeeper Hosts": "Zookeeper-værter", + "Zookeeper Port": "Zookeeper-port", + "Zookeeper Settings (SolrCloud)": "Zookeeper-indstillinger (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper-forbindelsesstreng til SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper-portnummer (valgfrit, standard er 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ADVARSEL OM PERMANENT SLETNING \\u26A0\\uFE0F\n\nDu er ved PERMANENT at slette ALLE objekter for skemaet \"{schema}\":\n\n\\u2022 Aktive objekter: {active}\n\\u2022 Blødt slettede objekter: {deleted}\n\\u2022 I alt: {total}\n\nDisse objekter vil blive fjernet helt fra databasen og KAN IKKE gendannes.\n\nEr du helt sikker?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ADVARSEL OM PERMANENT SLETNING \\u26A0\\uFE0F\n\nDu er ved PERMANENT at slette {count} blødt slettede objekter for skemaet \"{schema}\".\n\nDisse objekter vil blive fjernet helt fra databasen og KAN IKKE gendannes.\n\nEr du helt sikker?", + "and accessible to all users in selected groups (or all users if no groups selected).": "og tilgængelig for alle brugere i de valgte grupper (eller alle brugere, hvis ingen grupper er valgt).", + "and add the files there.": "og tilføj filerne der.", + "approved": "godkendt", + "are only accessible to invited users.": "er kun tilgængelige for inviterede brugere.", + "by": "af", + "chunk": "chunk", + "chunks": "chunks", + "chunks processed": "chunks behandlet", + "completed": "fuldført", + "configuration(s)": "konfiguration(er)", + "conversationId is required": "conversationId er påkrævet", + "create": "opret", + "delete": "slet", + "deleted objects section": "sektion for slettede objekter", + "e.g., lib/Settings/config.json": "f.eks. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "f.eks. lib/Settings/register.json", + "e.g., lib/settings/config.json": "f.eks. lib/settings/config.json", + "e.g., lib/settings/register.json": "f.eks. lib/settings/register.json", + "execute": "udfør", + "failed": "mislykkedes", + "file": "fil", + "files": "filer", + "file{plural}": "fil{plural}", + "folder": "mappe", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "i {register}", + "in_progress": "i gang", + "inherited": "nedarvet", + "list": "liste", + "log{plural}": "log{plural}", + "manage": "administrér", + "members": "medlemmer", + "menu or contact someone with permission to create agents.": "menuen eller kontakt en person med tilladelse til at oprette agenter.", + "message content is required": "beskedindhold er påkrævet", + "messages": "beskeder", + "missing": "mangler", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL og API-nøgle er påkrævet", + "n8n configuration saved successfully": "n8n-konfiguration blev gemt", + "n8n connection not configured": "n8n-forbindelse er ikke konfigureret", + "n8n connection successful": "n8n-forbindelse oprettet", + "n8n connection test failed: {message}": "n8n-forbindelsestest mislykkedes: {message}", + "n8n connection test successful": "n8n-forbindelsestest lykkedes", + "n8n initialization failed: {message}": "n8n-initialisering mislykkedes: {message}", + "n8n integration disabled": "n8n-integration deaktiveret", + "n8n integration enabled": "n8n-integration aktiveret", + "n8n project initialized successfully": "n8n-projekt blev initialiseret", + "n8n settings saved successfully": "n8n-indstillinger blev gemt", + "notify_push configuration guide": "konfigurationsvejledning for notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push er installeret, men OpenRegister har endnu ikke bekræftet en vellykket push. Udløs en objektlagring for at aktivere, eller kontrollér din notify_push-konfiguration.", + "notify_push is installed but not yet active": "notify_push er installeret, men endnu ikke aktiv", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekter", + "objects processed": "objekter behandlet", + "object{plural}": "objekt{plural}", + "or pick a range": "eller vælg et interval", + "org-...": "org-...", + "pending": "afventer", + "provides a good balance between speed and accuracy.": "giver en god balance mellem hastighed og nøjagtighed.", + "public": "offentlig", + "read": "læs", + "register(s)": "register/registre", + "register(s) selected": "register/registre valgt", + "register{plural}": "register{plural}", + "rejected": "afvist", + "required": "påkrævet", + "results": "resultater", + "schema(s)": "skema(er)", + "schema(s) selected": "skema(er) valgt", + "schema{plural}": "skema{plural}", + "selected objects": "valgte objekter", + "set": "sæt", + "sk-...": "sk-...", + "this application": "denne applikation", + "to": "til", + "type must be \"positive\" or \"negative\"": "type skal være \"positive\" eller \"negative\"", + "update": "opdatér", + "vectorized": "vektoriseret", + "view": "visning", + "views selected": "visninger valgt", + "waiting": "venter", + "{active} active, {deleted} deleted": "{active} aktive, {deleted} slettede", + "{count} audit trails cleared successfully": "{count} revisionsspor blev ryddet", + "{count} compliance issue(s) detected": "{count} overholdelsesproblem(er) fundet", + "{count} deletions": "{count} sletninger", + "{count} documents found": "{count} dokumenter fundet", + "{count} email": "{count} e-mail", + "{count} emails": "{count} e-mails", + "{count} entries": "{count} poster", + "{count} issues": "{count} problemer", + "{count} members": "{count} medlemmer", + "{count} searches": "{count} søgninger", + "{count} selected": "{count} valgt", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} objekt(er) matchede. {erased} slettet.", + "{minutes} minutes ago": "for {minutes} minutter siden", + "{percentage}% of objects": "{percentage}% af objekterne", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} i {register} / {schema}", + "{type} cache cleared successfully": "{type}-cache blev ryddet", + "ℹ️ Current Configuration": "ℹ️ Nuværende konfiguration", + "ℹ️ current configuration": "ℹ️ nuværende konfiguration", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ FARE: Alle {total} objekter vil blive PERMANENT SLETTET fra databasen. Denne handling er UGENKALDELIG og kan ikke fortrydes!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ADVARSEL OM PERMANENT SLETNING ⚠️\n\nDu er ved PERMANENT at slette ALLE objekter for skemaet \"{schema}\":\n\n• Aktive objekter: {active}\n• Blødt slettede objekter: {deleted}\n• I alt: {total}\n\nDisse objekter vil blive fjernet helt fra databasen og KAN IKKE gendannes.\n\nEr du helt sikker?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ADVARSEL OM PERMANENT SLETNING ⚠️\n\nDu er ved PERMANENT at slette {count} blødt slettede objekter for skemaet \"{schema}\".\n\nDisse objekter vil blive fjernet helt fra databasen og KAN IKKE gendannes.\n\nEr du helt sikker?", + "⚡ Batch Processing": "⚡ Batchbehandling", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR kan håndtere millioner af dokumenter med søgesvartider på under et sekund...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Omfattende kontekst ✓ Mindre risiko for at overse detaljer ✗ Langsommere svar ✗ Kan inkludere mindre relevant information", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Hurtigere svar ✓ Mere fokuserede svar ✗ Kan overse relevant information", + "✨ AI Features": "✨ AI-funktioner", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 I distribueret tilstand bruger SOLR ZooKeeper til klyngekoordinering...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Facetteret søgning giver brugerne mulighed for at bore ned i resultater efter kategorier...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Vidste du det? SOLR driver søgning for Netflix, Apple og mange andre store websteder!", + "💰 View Selection (Cost Optimization)": "💰 Valg af visning (omkostningsoptimering)", + "📄 Object Serialization": "📄 Objektserialisering", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR leverer omfattende analyser og statistik om søgeydelse...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets indeholder skemaet og konfigurationsfilerne til dit søgeindeks...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikering sikrer, at dit søgeindeks er tilgængeligt, selv hvis noder fejler...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR er en kraftfuld virksomhedssøgeplatform bygget på Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Vektoriseringsindstillinger", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automatisk skalering kan dynamisk tilføje eller fjerne noder baseret på belastning...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Sikkerhedsfunktioner omfatter godkendelse, autorisation og SSL-kryptering...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-mails" + ], + "Purge {count} object from database": [ + "Ryd {count} objekt fra databasen", + "Ryd {count} objekter fra databasen" + ], + "Restore {count} object": [ + "Gendan {count} objekt", + "Gendan {count} objekter" + ], + "Successfully restored {count} object": [ + "{count} objekt gendannet", + "{count} objekter gendannet" + ], + "Delete {count} object": [ + "Slet {count} objekt", + "Slet {count} objekter" + ], + "Object successfully deleted": [ + "Objekt slettet", + "Objekter slettet" + ] + } +} diff --git a/l10n/de.js b/l10n/de.js new file mode 100644 index 0000000000..b87dec84d7 --- /dev/null +++ b/l10n/de.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(kein Betreff)", + "(no title)" : "(kein Titel)", + "(unnamed)" : "(unbenannt)", + "(untitled card)" : "(unbenannte Karte)", + "Activities" : "Aktivitäten", + "Add Endpoint" : "Endpunkt hinzufügen", + "Add Source" : "Quelle hinzufügen", + "Add card" : "Karte hinzufügen", + "Add contact" : "Kontakt hinzufügen", + "All clear" : "Alles in Ordnung", + "Articles" : "Artikel", + "Code" : "Code", + "Compliance" : "Compliance", + "Contacts" : "Kontakte", + "Create Agent" : "Agent erstellen", + "Create Application" : "Anwendung erstellen", + "Create event" : "Termin erstellen", + "Erasure complete" : "Löschung abgeschlossen", + "Erasure preview" : "Löschvorschau", + "Expand details" : "Details aufklappen", + "Save changes" : "Änderungen speichern", + "📄 Object Serialization" : "📄 Objektserialisierung", + "🔢 Vectorization Settings" : "🔢 Vektorisierungseinstellungen", + "💰 View Selection (Cost Optimization)" : "💰 Ansichtsauswahl (Kostenoptimierung)", + "AI Assistant" : "KI-Assistent", + "AI Chat" : "KI-Chat", + "API Key" : "API-Schlüssel", + "API tokens saved successfully" : "API-Token erfolgreich gespeichert", + "About ConfigSets" : "Über ConfigSets", + "Action" : "Aktion", + "Action Distribution" : "Aktionsverteilung", + "Action:" : "Aktion:", + "Actions" : "Aktionen", + "Active" : "Aktiv", + "Active Collections" : "Aktive Collections", + "Active Filters" : "Aktive Filter", + "Active Organisations" : "Aktive Organisationen", + "Active filters:" : "Aktive Filter:", + "Active organisation set successfully" : "Aktive Organisation erfolgreich festgelegt", + "Activity Period" : "Aktivitätszeitraum", + "Add Groups" : "Gruppen hinzufügen", + "Add Schema" : "Schema hinzufügen", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Fügen Sie Schematitel, Beschreibungen und Registerinformationen hinzu, um einen reichhaltigeren Kontext für die Suche bereitzustellen", + "Add to favorites" : "Zu Favoriten hinzufügen", + "Added" : "Hinzugefügt", + "Added to favorites" : "Zu Favoriten hinzugefügt", + "Additional Information" : "Zusätzliche Informationen", + "Additional feedback saved. Thank you!" : "Zusätzliches Feedback gespeichert. Vielen Dank!", + "Admin" : "Administrator", + "Advanced" : "Erweitert", + "Advanced Filters" : "Erweiterte Filter", + "Advanced Options" : "Erweiterte Optionen", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Erweiterte Filter sind bei Verwendung der Datenbankquelle nicht verfügbar. Wechseln Sie zu Auto oder SOLR-Index, um Filteroptionen zu erhalten.", + "After" : "Nach", + "Agent deleted successfully" : "Agent erfolgreich gelöscht", + "Agents" : "Agenten", + "All" : "Alle", + "All Categories" : "Alle Kategorien", + "All Collections" : "Alle Collections", + "All Confidence Levels" : "Alle Konfidenzstufen", + "All ConfigSets" : "Alle ConfigSets", + "All Files" : "Alle Dateien", + "All Levels" : "Alle Stufen", + "All Types" : "Alle Typen", + "All Webhooks" : "Alle Webhooks", + "All actions" : "Alle Aktionen", + "All audit trails cleared successfully" : "Alle Prüfprotokolle erfolgreich gelöscht", + "All registers" : "Alle Register", + "All schemas" : "Alle Schemas", + "All searches" : "Alle Suchen", + "All users" : "Alle Benutzer", + "An error occurred while clearing audit trails" : "Beim Löschen der Prüfprotokolle ist ein Fehler aufgetreten", + "An error occurred while deleting the audit trail" : "Beim Löschen des Prüfprotokolls ist ein Fehler aufgetreten", + "An error occurred while deleting the view" : "Beim Löschen der Ansicht ist ein Fehler aufgetreten", + "An error occurred while permanently deleting the objects" : "Beim endgültigen Löschen der Objekte ist ein Fehler aufgetreten", + "Analysis completed:" : "Analyse abgeschlossen:", + "Analysis steps:" : "Analyseschritte:", + "Analytics" : "Analysen", + "Analyze Objects" : "Objekte analysieren", + "Analyze Properties" : "Eigenschaften analysieren", + "Analyze existing properties for improvement opportunities" : "Bestehende Eigenschaften auf Verbesserungsmöglichkeiten analysieren", + "Analyzing..." : "Wird analysiert...", + "Any favorites and sharing settings for this view" : "Alle Favoriten- und Freigabeeinstellungen für diese Ansicht", + "Any user" : "Beliebiger Benutzer", + "App store cache invalidated successfully" : "App-Store-Cache erfolgreich ungültig gemacht", + "Application deleted successfully" : "Anwendung erfolgreich gelöscht", + "Applications" : "Anwendungen", + "Apply Changes" : "Änderungen anwenden", + "Archive" : "Archivieren", + "Archive conversation" : "Konversation archivieren", + "Archived conversations are hidden from your active list" : "Archivierte Konversationen werden in Ihrer aktiven Liste ausgeblendet", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Möchten Sie alte Suchprotokolle wirklich bereinigen? Dadurch werden Einträge gelöscht, die älter als 30 Tage sind.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Möchten Sie die ausgewählten Prüfprotokolle wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Möchten Sie die ausgewählten Suchprotokolle wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are you sure you want to delete this ConfigSet?" : "Möchten Sie dieses ConfigSet wirklich löschen?", + "Are you sure you want to permanently delete" : "Möchten Sie wirklich endgültig löschen", + "Ask a question..." : "Stellen Sie eine Frage...", + "Ask questions about your data using natural language" : "Stellen Sie Fragen zu Ihren Daten in natürlicher Sprache", + "Attempt" : "Versuch", + "Audit Trail Changes" : "Prüfprotokolländerungen", + "Audit Trail Details" : "Prüfprotokolldetails", + "Audit Trail Management" : "Prüfprotokollverwaltung", + "Audit Trail Statistics" : "Prüfprotokollstatistiken", + "Audit Trails" : "Prüfprotokolle", + "Audit trail data copied to clipboard" : "Prüfprotokolldaten in die Zwischenablage kopiert", + "Audit trail deleted successfully" : "Prüfprotokoll erfolgreich gelöscht", + "Audit trail successfully deleted" : "Prüfprotokoll erfolgreich gelöscht", + "Authentication" : "Authentifizierung", + "Auto-commit disabled" : "Auto-Commit deaktiviert", + "Auto-commit enabled" : "Auto-Commit aktiviert", + "Auto-create Default Organisation" : "Standardorganisation automatisch erstellen", + "Auto-retry failed vectorizations" : "Fehlgeschlagene Vektorisierungen automatisch wiederholen", + "Automatically commit changes to SOLR index" : "Änderungen automatisch in den SOLR-Index übernehmen", + "Automatically create a default organisation if none exists when the app is initialized" : "Automatisch eine Standardorganisation erstellen, falls beim Initialisieren der App keine vorhanden ist", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Automatisch Vektoreinbettungen aus Text-Chunks generieren, wenn Dateien hochgeladen und verarbeitet werden", + "Automatically generate vector embeddings when objects are created or updated" : "Automatisch Vektoreinbettungen generieren, wenn Objekte erstellt oder aktualisiert werden", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Fehlgeschlagene Vektorisierungsversuche automatisch wiederholen (max. 3 Wiederholungen)", + "Available Filters" : "Verfügbare Filter", + "Available Workflows" : "Verfügbare Workflows", + "Avg Execution Time" : "Durchschn. Ausführungszeit", + "Avg Members/Org" : "Durchschn. Mitglieder/Org", + "Avg Object Views/Session" : "Durchschn. Objektaufrufe/Sitzung", + "Avg Response Time" : "Durchschn. Antwortzeit", + "Avg Results/Search" : "Durchschn. Ergebnisse/Suche", + "Avg Searches/Session" : "Durchschn. Suchen/Sitzung", + "Back" : "Zurück", + "Back to Entities" : "Zurück zu Entitäten", + "Back to Registers" : "Zurück zu Registern", + "Back to Webhooks" : "Zurück zu Webhooks", + "Back to applications" : "Zurück zu Anwendungen", + "Back to entities" : "Zurück zu Entitäten", + "Backend updated successfully. Please reload the application." : "Backend erfolgreich aktualisiert. Bitte laden Sie die Anwendung neu.", + "Base URL (Optional)" : "Basis-URL (optional)", + "Basic Information" : "Grundlegende Informationen", + "Batch Size" : "Stapelgröße", + "Batch extraction completed" : "Stapelextraktion abgeschlossen", + "Before" : "Vor", + "Before object vectorization can work:" : "Bevor die Objektvektorisierung funktionieren kann:", + "Behavior Issue" : "Verhaltensproblem", + "Blob storage has been retired. All objects now use magic tables." : "Der Blob-Speicher wurde abgeschafft. Alle Objekte verwenden jetzt Magic Tables.", + "Branch" : "Branch", + "Bulk delete operation completed successfully" : "Massenlöschvorgang erfolgreich abgeschlossen", + "Bulk save operation completed successfully" : "Massenspeichervorgang erfolgreich abgeschlossen", + "Business Data" : "Geschäftsdaten", + "Cache cleared successfully" : "Cache erfolgreich geleert", + "Calculate Sizes" : "Größen berechnen", + "Cancel" : "Abbrechen", + "Cannot delete: objects are still attached" : "Löschen nicht möglich: Es sind noch Objekte angehängt", + "Category" : "Kategorie", + "Change Type" : "Änderungstyp", + "Changes" : "Änderungen", + "Changes copied to clipboard" : "Änderungen in die Zwischenablage kopiert", + "Chat Model" : "Chat-Modell", + "Chat Provider" : "Chat-Anbieter", + "Chat Provider (RAG)" : "Chat-Anbieter (RAG)", + "Chat Settings" : "Chat-Einstellungen", + "Chat provider connection successful!" : "Verbindung zum Chat-Anbieter erfolgreich!", + "Chat settings" : "Chat-Einstellungen", + "Choose a register" : "Register auswählen", + "Choose a schema" : "Schema auswählen", + "Choose how vector similarity calculations are performed for semantic search" : "Wählen Sie, wie Vektorähnlichkeitsberechnungen für die semantische Suche durchgeführt werden", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Wählen Sie, welche Dateitypen in den Vektorisierungsprozess einbezogen werden sollen. Nur Dateien mit extrahiertem Text und Chunks werden verarbeitet.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Wählen Sie, welche Ansichten in den Vektorisierungsprozess einbezogen werden sollen. Lassen Sie das Feld leer, um alle Ansichten gemäß Ihrer Konfiguration zu verarbeiten.", + "Chunk" : "Chunk", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Chunk-Löschung noch nicht implementiert. Verwenden Sie Chunk-basierte Endpunkte.", + "Chunks" : "Chunks", + "Chunks to Vectorize:" : "Zu vektorisierende Chunks:", + "Cleanup Old Trails" : "Alte Protokolle bereinigen", + "Cleanup completed" : "Bereinigung abgeschlossen", + "Clear All Embeddings" : "Alle Einbettungen löschen", + "Clear Entries" : "Einträge löschen", + "Clear Filtered Audit Trails" : "Gefilterte Prüfprotokolle löschen", + "Clear Filters" : "Filter zurücksetzen", + "Clear Index" : "Index löschen", + "Clear Selection" : "Auswahl aufheben", + "Clear all" : "Alle löschen", + "Clear all filters" : "Alle Filter zurücksetzen", + "Clear date range" : "Datumsbereich zurücksetzen", + "Clearing..." : "Wird gelöscht...", + "Close" : "Schließen", + "Collection Management" : "Collection-Verwaltung", + "Collection Name" : "Collection-Name", + "Collection assignments updated successfully" : "Collection-Zuweisungen erfolgreich aktualisiert", + "Collection cleared successfully" : "Collection erfolgreich geleert", + "Collection copied successfully" : "Collection erfolgreich kopiert", + "Collection created successfully" : "Collection erfolgreich erstellt", + "Collection deleted successfully" : "Collection erfolgreich gelöscht", + "Collection used to store and index file metadata and content" : "Collection zum Speichern und Indizieren von Dateimetadaten und -inhalten", + "Collection used to store and index object data" : "Collection zum Speichern und Indizieren von Objektdaten", + "Collections:" : "Collections:", + "Commit Message" : "Commit-Nachricht", + "Commit Within (ms)" : "Commit innerhalb (ms)", + "Commit message" : "Commit-Nachricht", + "Compare current schema with real object data" : "Aktuelles Schema mit echten Objektdaten vergleichen", + "Completed" : "Abgeschlossen", + "Complex" : "Komplex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Komplexe Abfragen: Erweiterte Suchen mit mehreren Filtern, Operatoren und komplexen Parameterkombinationen", + "Confidence Level" : "Konfidenzstufe", + "Confidence Score" : "Konfidenzwert", + "Config must be provided as an object" : "Die Konfiguration muss als Objekt angegeben werden", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSet-Verwaltung", + "ConfigSet Name" : "ConfigSet-Name", + "ConfigSet created successfully" : "ConfigSet erfolgreich erstellt", + "ConfigSet deleted successfully" : "ConfigSet erfolgreich gelöscht", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets definieren das Schema und die Konfiguration für Ihre SOLR-Collections. Sie enthalten Felddefinitionen, Analyzer und andere Sucheinstellungen.", + "Configuration" : "Konfiguration", + "Configuration Data" : "Konfigurationsdaten", + "Configuration saved" : "Konfiguration gespeichert", + "Configurations" : "Konfigurationen", + "Configure Facets" : "Facetten konfigurieren", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurieren Sie Large-Language-Model-(LLM)-Anbieter für KI-gestützte Funktionen wie semantische Suche, Einbettungen und Chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurieren Sie grundlegende Verbindungseinstellungen für Ihren SOLR-Server, einschließlich Authentifizierung und Netzwerkoptionen. Verwenden Sie die separaten Dialoge für ConfigSet- und Collection-Verwaltung, um Cores und Collections zu verwalten.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurieren Sie, wie Datenbankobjekte für die semantische Suche in Vektoreinbettungen umgewandelt werden. Objekte werden direkt vektorisiert, ohne dass eine Textextraktion erforderlich ist.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurieren Sie, wie Objekte vor der Vektorisierung in Text umgewandelt werden. Diese Einstellungen beeinflussen Suchqualität und Kontext.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurieren Sie Parameter für die Dateivektorisierung. Dieser Prozess generiert Vektoreinbettungen für alle extrahierten Datei-Chunks.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurieren Sie Parameter für die Objektvektorisierung. Dieser Prozess generiert Vektoreinbettungen für alle Objekte, die Ihren Ansichtsfiltern entsprechen.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurieren Sie, welche Datentypen durchsucht und wie viele Quellen abgerufen werden sollen", + "Connection Failed" : "Verbindung fehlgeschlagen", + "Connection Settings" : "Verbindungseinstellungen", + "Connection Successful!" : "Verbindung erfolgreich!", + "Connection failed" : "Verbindung fehlgeschlagen", + "Connection protocol" : "Verbindungsprotokoll", + "Connection successful" : "Verbindung erfolgreich", + "Connection timeout in seconds" : "Verbindungszeitüberschreitung in Sekunden", + "Constant delay between retries (5 minutes)" : "Konstante Verzögerung zwischen Wiederholungen (5 Minuten)", + "Constraint Issue" : "Einschränkungsproblem", + "Content Filters" : "Inhaltsfilter", + "Continue to Properties" : "Weiter zu Eigenschaften", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Steuern Sie, welche Objektansichten vektorisiert werden sollen, um API-Kosten zu senken. Vektorisieren Sie nur Ansichten, die von der semantischen Suche profitieren.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Steuern Sie, welche Ansichten und Werkzeuge die KI in dieser Konversation verwenden kann. Standardmäßig sind alle Agentenfunktionen aktiviert.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Steuert die Zufälligkeit (0 = deterministisch, 2 = sehr kreativ)", + "Conversation ID is required" : "Konversations-ID ist erforderlich", + "Conversation archived" : "Konversation archiviert", + "Conversation archived successfully" : "Konversation erfolgreich archiviert", + "Conversation cleared successfully" : "Konversation erfolgreich geleert", + "Conversation deleted" : "Konversation gelöscht", + "Conversation permanently deleted" : "Konversation endgültig gelöscht", + "Conversation renamed" : "Konversation umbenannt", + "Conversation restored" : "Konversation wiederhergestellt", + "Conversation title" : "Konversationstitel", + "Copied!" : "Kopiert!", + "Copy" : "Kopieren", + "Copy Changes" : "Änderungen kopieren", + "Copy Collection" : "Collection kopieren", + "Copy Data" : "Daten kopieren", + "Copy Full Data" : "Vollständige Daten kopieren", + "Copying..." : "Wird kopiert...", + "Count" : "Anzahl", + "Counting objects..." : "Objekte werden gezählt...", + "Costs" : "Kosten", + "Create" : "Erstellen", + "Create Collection" : "Collection erstellen", + "Create ConfigSet" : "ConfigSet erstellen", + "Create New Collection" : "Neue Collection erstellen", + "Create New ConfigSet" : "Neues ConfigSet erstellen", + "Create Operations" : "Erstellungsvorgänge", + "Create Webhook" : "Webhook erstellen", + "Create a copy of collection:" : "Kopie der Collection erstellen:", + "Create a new ConfigSet based on the _default template" : "Ein neues ConfigSet basierend auf der Vorlage _default erstellen", + "Create a new SOLR collection from an existing ConfigSet" : "Eine neue SOLR-Collection aus einem bestehenden ConfigSet erstellen", + "Create your first AI agent to get started." : "Erstellen Sie Ihren ersten KI-Agenten, um zu beginnen.", + "Created" : "Erstellt", + "Created:" : "Erstellt:", + "Creating..." : "Wird erstellt...", + "Current Filters" : "Aktuelle Filter", + "Current Type:" : "Aktueller Typ:", + "Custom API endpoint if using a different region" : "Benutzerdefinierter API-Endpunkt bei Verwendung einer anderen Region", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Benutzerdefinierte HTTP-Header (einer pro Zeile, Format: Header-Name: Wert)", + "Daily" : "Täglich", + "Dashboard" : "Dashboard", + "Data Source" : "Datenquelle", + "Data sources" : "Datenquellen", + "Data type does not match observed values" : "Datentyp stimmt nicht mit den beobachteten Werten überein", + "Database URL" : "Datenbank-URL", + "Database information refreshed" : "Datenbankinformationen aktualisiert", + "Date Range" : "Datumsbereich", + "Default" : "Standard", + "Default Organisation" : "Standardorganisation", + "Default: 5" : "Standard: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Verzögerungen verdoppeln sich mit jedem Versuch (2, 4, 8 Minuten...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Verzögerungen nehmen linear zu (5, 10, 15 Minuten...)", + "Delete" : "Löschen", + "Delete Application" : "Anwendung löschen", + "Delete Audit Trail" : "Prüfprotokoll löschen", + "Delete Collection" : "Collection löschen", + "Delete ConfigSet" : "ConfigSet löschen", + "Delete Objects" : "Objekte löschen", + "Delete Operations" : "Löschvorgänge", + "Delete View" : "Ansicht löschen", + "Delete all objects in this schema" : "Alle Objekte in diesem Schema löschen", + "Delete permanently" : "Endgültig löschen", + "Delete this conversation?" : "Diese Konversation löschen?", + "Delete view" : "Ansicht löschen", + "Deleted" : "Gelöscht", + "Deleted By" : "Gelöscht von", + "Deleted Date" : "Löschdatum", + "Deleted Items Management" : "Verwaltung gelöschter Elemente", + "Deleted This Week" : "Diese Woche gelöscht", + "Deleted Today" : "Heute gelöscht", + "Deleted:" : "Gelöscht:", + "Deleting..." : "Wird gelöscht...", + "Deletion Date Range" : "Löschdatumsbereich", + "Deletion Statistics" : "Löschstatistiken", + "Deprecated" : "Veraltet", + "Description" : "Beschreibung", + "Description:" : "Beschreibung:", + "Detect data types and patterns" : "Datentypen und Muster erkennen", + "Detected At" : "Erkannt am", + "Detected Format:" : "Erkanntes Format:", + "Detected Issues:" : "Erkannte Probleme:", + "Disable" : "Deaktivieren", + "Disabled" : "Deaktiviert", + "Discover Files" : "Dateien entdecken", + "Discovered Properties" : "Entdeckte Eigenschaften", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Möchten Sie alle gefilterten Prüfprotokolleinträge endgültig löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Möchten Sie diesen Prüfprotokolleintrag endgültig löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "Documents" : "Dokumente", + "Download API Spec" : "API-Spezifikation herunterladen", + "Edit" : "Bearbeiten", + "Edit Register" : "Register bearbeiten", + "Edit Schema" : "Schema bearbeiten", + "Edit View" : "Ansicht bearbeiten", + "Edit Webhook" : "Webhook bearbeiten", + "Edit view" : "Ansicht bearbeiten", + "Edit view details" : "Ansichtsdetails bearbeiten", + "Effectiveness" : "Wirksamkeit", + "Email" : "E-Mail", + "Emails" : "E-Mails", + "Embedding Model" : "Einbettungsmodell", + "Embedding Provider" : "Einbettungsanbieter", + "Embedding generated successfully" : "Einbettung erfolgreich generiert", + "Embedding provider connection successful!" : "Verbindung zum Einbettungsanbieter erfolgreich!", + "Enable" : "Aktivieren", + "Enable auto-creation" : "Automatische Erstellung aktivieren", + "Enable automatic file vectorization" : "Automatische Dateivektorisierung aktivieren", + "Enable automatic object vectorization" : "Automatische Objektvektorisierung aktivieren", + "Enable automatic synchronization" : "Automatische Synchronisierung aktivieren", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Detaillierte Protokollierung für SOLR-Vorgänge aktivieren (empfohlen für die Fehlersuche)", + "Enable faceting" : "Facettierung aktivieren", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Aktivieren oder deaktivieren Sie LLM-Funktionen. Konfigurieren Sie Anbieter und Modelle über die Schaltfläche LLM-Konfiguration oben.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Aktivieren oder deaktivieren Sie die SOLR-Suchintegration. Konfigurieren Sie Verbindungseinstellungen über die Schaltfläche Verbindungseinstellungen oben.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Aktivieren oder deaktivieren Sie die n8n-Workflow-Integration. Konfigurieren Sie die Verbindungseinstellungen unten.", + "Enable or disable this webhook" : "Diesen Webhook aktivieren oder deaktivieren", + "Enable vectorization for all existing and future views (may increase costs)" : "Vektorisierung für alle bestehenden und zukünftigen Ansichten aktivieren (kann Kosten erhöhen)", + "Enabled" : "Aktiviert", + "Endpoints" : "Endpunkte", + "Enter ConfigSet name" : "ConfigSet-Namen eingeben", + "Enter collection name" : "Collection-Namen eingeben", + "Enter description (optional)..." : "Beschreibung eingeben (optional)...", + "Enter new collection name" : "Neuen Collection-Namen eingeben", + "Enter object ID" : "Objekt-ID eingeben", + "Enter search term" : "Suchbegriff eingeben", + "Enter view name..." : "Ansichtsnamen eingeben...", + "Enter webhook name" : "Webhook-Namen eingeben", + "Entities" : "Entitäten", + "Entity Information" : "Entitätsinformationen", + "Entity deleted successfully" : "Entität erfolgreich gelöscht", + "Entity not found" : "Entität nicht gefunden", + "Entries to be deleted:" : "Zu löschende Einträge:", + "Enum Issue" : "Enum-Problem", + "Enum constraint is missing" : "Enum-Einschränkung fehlt", + "Error" : "Fehler", + "Error Details" : "Fehlerdetails", + "Error Information" : "Fehlerinformationen", + "Error Message" : "Fehlermeldung", + "Error loading application" : "Fehler beim Laden der Anwendung", + "Error loading audit trails" : "Fehler beim Laden der Prüfprotokolle", + "Error loading entity" : "Fehler beim Laden der Entität", + "Error loading search trails" : "Fehler beim Laden der Suchprotokolle", + "Estimated Batches:" : "Geschätzte Stapel:", + "Estimated Cost:" : "Geschätzte Kosten:", + "Estimated Duration:" : "Geschätzte Dauer:", + "Event" : "Ereignis", + "Event Property for Payload" : "Ereigniseigenschaft für Payload", + "Events" : "Ereignisse", + "Example Value" : "Beispielwert", + "Exclusive Maximum" : "Exklusives Maximum", + "Exclusive Minimum" : "Exklusives Minimum", + "Execution Mode" : "Ausführungsmodus", + "Execution Time" : "Ausführungszeit", + "Execution Time Range" : "Ausführungszeitbereich", + "Existing Improvements" : "Bestehende Verbesserungen", + "Exponential" : "Exponentiell", + "Export" : "Exportieren", + "Export completed successfully" : "Export erfolgreich abgeschlossen", + "Export, view, or delete audit trails" : "Prüfprotokolle exportieren, anzeigen oder löschen", + "Extend Schema" : "Schema erweitern", + "Extended" : "Erweitert", + "Extensions" : "Erweiterungen", + "Extract Pending Files" : "Ausstehende Dateien extrahieren", + "Extract properties from each object" : "Eigenschaften aus jedem Objekt extrahieren", + "Extracted At" : "Extrahiert am", + "Extraction Status" : "Extraktionsstatus", + "Facet configuration updated successfully" : "Facettenkonfiguration erfolgreich aktualisiert", + "Facets discovered and configured successfully" : "Facetten erfolgreich entdeckt und konfiguriert", + "Facets discovered successfully" : "Facetten erfolgreich entdeckt", + "Failed" : "Fehlgeschlagen", + "Failed to analyze schema properties" : "Schemaeigenschaften konnten nicht analysiert werden", + "Failed to archive conversation" : "Konversation konnte nicht archiviert werden", + "Failed to calculate sizes" : "Größen konnten nicht berechnet werden", + "Failed to clear collection" : "Collection konnte nicht geleert werden", + "Failed to copy changes" : "Änderungen konnten nicht kopiert werden", + "Failed to copy data" : "Daten konnten nicht kopiert werden", + "Failed to copy data to clipboard" : "Daten konnten nicht in die Zwischenablage kopiert werden", + "Failed to create conversation" : "Konversation konnte nicht erstellt werden", + "Failed to create or find project" : "Projekt konnte nicht erstellt oder gefunden werden", + "Failed to delete collection" : "Collection konnte nicht gelöscht werden", + "Failed to delete conversation" : "Konversation konnte nicht gelöscht werden", + "Failed to delete webhook" : "Webhook konnte nicht gelöscht werden", + "Failed to download API specification" : "API-Spezifikation konnte nicht heruntergeladen werden", + "Failed to get SOLR field configuration" : "SOLR-Feldkonfiguration konnte nicht abgerufen werden", + "Failed to get entity categories" : "Entitätskategorien konnten nicht abgerufen werden", + "Failed to get entity statistics" : "Entitätsstatistiken konnten nicht abgerufen werden", + "Failed to get entity types" : "Entitätstypen konnten nicht abgerufen werden", + "Failed to load LLM configuration" : "LLM-Konfiguration konnte nicht geladen werden", + "Failed to load Nextcloud groups" : "Nextcloud-Gruppen konnten nicht geladen werden", + "Failed to load conversation" : "Konversation konnte nicht geladen werden", + "Failed to load entities" : "Entitäten konnten nicht geladen werden", + "Failed to load entity" : "Entität konnte nicht geladen werden", + "Failed to load files" : "Dateien konnten nicht geladen werden", + "Failed to load organisations" : "Organisationen konnten nicht geladen werden", + "Failed to load templates" : "Vorlagen konnten nicht geladen werden", + "Failed to load webhooks" : "Webhooks konnten nicht geladen werden", + "Failed to load workflows" : "Workflows konnten nicht geladen werden", + "Failed to refresh database information" : "Datenbankinformationen konnten nicht aktualisiert werden", + "Failed to reindex collection" : "Collection konnte nicht neu indiziert werden", + "Failed to rename conversation" : "Konversation konnte nicht umbenannt werden", + "Failed to restore conversation" : "Konversation konnte nicht wiederhergestellt werden", + "Failed to retry extraction" : "Extraktion konnte nicht wiederholt werden", + "Failed to retry webhook" : "Webhook konnte nicht wiederholt werden", + "Failed to save GitHub token" : "GitHub-Token konnte nicht gespeichert werden", + "Failed to save GitLab URL" : "GitLab-URL konnte nicht gespeichert werden", + "Failed to save GitLab token" : "GitLab-Token konnte nicht gespeichert werden", + "Failed to save additional feedback" : "Zusätzliches Feedback konnte nicht gespeichert werden", + "Failed to save n8n configuration" : "n8n-Konfiguration konnte nicht gespeichert werden", + "Failed to save roles" : "Rollen konnten nicht gespeichert werden", + "Failed to save settings" : "Einstellungen konnten nicht gespeichert werden", + "Failed to save webhook" : "Webhook konnte nicht gespeichert werden", + "Failed to send feedback" : "Feedback konnte nicht gesendet werden", + "Failed to test webhook" : "Webhook konnte nicht getestet werden", + "Failed to update favorite status" : "Favoritenstatus konnte nicht aktualisiert werden", + "Failed to update schema properties" : "Schemaeigenschaften konnten nicht aktualisiert werden", + "Failed to update view" : "Ansicht konnte nicht aktualisiert werden", + "Failed to update webhook" : "Webhook konnte nicht aktualisiert werden", + "Feedback recorded" : "Feedback erfasst", + "Fewer sources (1-3):" : "Weniger Quellen (1-3):", + "Field" : "Feld", + "File" : "Datei", + "File Chunk Prediction" : "Datei-Chunk-Vorhersage", + "File Collection" : "Datei-Collection", + "File Management" : "Dateiverwaltung", + "File Name" : "Dateiname", + "File Path" : "Dateipfad", + "File Type Selection" : "Dateitypauswahl", + "File Vectorization" : "Dateivektorisierung", + "File Warmup" : "Datei-Aufwärmung", + "File actions menu" : "Dateiaktionsmenü", + "File anonymized successfully" : "Datei erfolgreich anonymisiert", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Die Datei-Chunk-Vektorisierung ist noch nicht implementiert. Die Chunks sind bereit und gespeichert, aber der Vektorisierungsdienst befindet sich in Entwicklung.", + "File collection not configured" : "Datei-Collection nicht konfiguriert", + "File discovery completed" : "Dateierkennung abgeschlossen", + "File extraction completed" : "Dateiextraktion abgeschlossen", + "File extraction queued" : "Dateiextraktion in Warteschlange eingereiht", + "File indexed successfully" : "Datei erfolgreich indiziert", + "File is already anonymized" : "Datei ist bereits anonymisiert", + "File not found" : "Datei nicht gefunden", + "File settings updated successfully" : "Dateieinstellungen erfolgreich aktualisiert", + "File sources" : "Dateiquellen", + "File vectorization configuration saved successfully" : "Dateivektorisierungskonfiguration erfolgreich gespeichert", + "File vectorization started. Check the statistics section for progress." : "Dateivektorisierung gestartet. Überprüfen Sie den Statistikbereich für den Fortschritt.", + "File warmup completed" : "Datei-Aufwärmung abgeschlossen", + "Files" : "Dateien", + "Files with Completed Extraction:" : "Dateien mit abgeschlossener Extraktion:", + "Files → fileCollection, Objects → objectCollection" : "Dateien → fileCollection, Objekte → objectCollection", + "Filter Audit Trails" : "Prüfprotokolle filtern", + "Filter Deleted Items" : "Gelöschte Elemente filtern", + "Filter Objects" : "Objekte filtern", + "Filter Properties" : "Eigenschaften filtern", + "Filter Search Trails" : "Suchprotokolle filtern", + "Filter Statistics" : "Statistiken filtern", + "Filter and analyze search trail entries" : "Suchprotokolleinträge filtern und analysieren", + "Filter and manage audit trail entries" : "Prüfprotokolleinträge filtern und verwalten", + "Filter and manage soft deleted items" : "Vorläufig gelöschte Elemente filtern und verwalten", + "Filter by object ID" : "Nach Objekt-ID filtern", + "Filter by search term" : "Nach Suchbegriff filtern", + "Filter by webhook" : "Nach Webhook filtern", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filterdaten automatisch geladen. Verwenden Sie die Filter unten, um Ihre Suche zu verfeinern.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Webhook-Auslöser nach Payload-Eigenschaften filtern (eine pro Zeile, Format: Schlüssel: Wert)", + "Filtered" : "Gefiltert", + "Filters" : "Filter", + "Fireworks AI Chat Settings" : "Fireworks-AI-Chat-Einstellungen", + "Fireworks AI Embedding Configuration" : "Fireworks-AI-Einbettungskonfiguration", + "First" : "Erste", + "Fixed" : "Behoben", + "For chat and retrieval-augmented generation" : "Für Chat und Retrieval-Augmented Generation", + "For vector embeddings and semantic search" : "Für Vektoreinbettungen und semantische Suche", + "Format" : "Format", + "Format Issue" : "Formatproblem", + "Format constraint is missing" : "Formateinschränkung fehlt", + "From Date" : "Von Datum", + "From date" : "Von Datum", + "Full data copied to clipboard" : "Vollständige Daten in die Zwischenablage kopiert", + "General Issue" : "Allgemeines Problem", + "Generate recommendations and confidence scores" : "Empfehlungen und Konfidenzwerte generieren", + "Generate vectors immediately when new objects are created" : "Vektoren sofort generieren, wenn neue Objekte erstellt werden", + "GitHub token is valid" : "GitHub-Token ist gültig", + "GitHub token removed successfully" : "GitHub-Token erfolgreich entfernt", + "GitHub token saved successfully" : "GitHub-Token erfolgreich gespeichert", + "GitLab URL saved successfully" : "GitLab-URL erfolgreich gespeichert", + "GitLab token is valid" : "GitLab-Token ist gültig", + "GitLab token saved successfully" : "GitLab-Token erfolgreich gespeichert", + "HTTP Method" : "HTTP-Methode", + "HTTP method used to send webhook requests" : "HTTP-Methode zum Senden von Webhook-Anfragen", + "Headers" : "Header", + "Health" : "Zustand", + "Heartbeat successful - connection kept alive" : "Heartbeat erfolgreich – Verbindung wird aufrechterhalten", + "Helpful" : "Hilfreich", + "Hide Filters" : "Filter ausblenden", + "Hide in forms" : "In Formularen ausblenden", + "Hide in list view" : "In Listenansicht ausblenden", + "High" : "Hoch", + "High Confidence" : "Hohe Konfidenz", + "Host" : "Host", + "Hourly" : "Stündlich", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Wie tief verschachtelte Objekteigenschaften durchlaufen werden sollen (1-20). Höhere Werte erfassen mehr Details, erhöhen aber die Vektorgröße.", + "How often to check for updates (1-168 hours)" : "Wie oft auf Aktualisierungen geprüft werden soll (1-168 Stunden)", + "How to handle retries for failed webhook deliveries" : "Wie Wiederholungen für fehlgeschlagene Webhook-Zustellungen behandelt werden sollen", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "IP-Ratenbegrenzungen erfolgreich zurückgesetzt", + "Identify properties not in the schema" : "Eigenschaften identifizieren, die nicht im Schema enthalten sind", + "Immutable" : "Unveränderlich", + "Import" : "Importieren", + "Import successful" : "Import erfolgreich", + "Improved Property" : "Verbesserte Eigenschaft", + "In use" : "In Verwendung", + "Inactive" : "Inaktiv", + "Include IDs and names of related objects for better contextual search" : "IDs und Namen verwandter Objekte für eine bessere kontextbezogene Suche einbeziehen", + "Include related object references" : "Verwandte Objektreferenzen einbeziehen", + "Include schema and register metadata" : "Schema- und Registermetadaten einbeziehen", + "Initialization failed" : "Initialisierung fehlgeschlagen", + "Initialize Project" : "Projekt initialisieren", + "Inspect Fields" : "Felder untersuchen", + "Inspect Index" : "Index untersuchen", + "Invalid" : "Ungültig", + "Invalid batch size. Must be between 1 and 5000" : "Ungültige Stapelgröße. Muss zwischen 1 und 5000 liegen", + "Invalid field name provided" : "Ungültiger Feldname angegeben", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Ungültiges maxObjects. Muss 0 (alle) oder eine positive Zahl sein", + "Invalid:" : "Ungültig:", + "Issue" : "Problem", + "Items per page:" : "Elemente pro Seite:", + "Just now" : "Gerade eben", + "LLM Configuration" : "LLM-Konfiguration", + "LLM actions menu" : "LLM-Aktionsmenü", + "LLM configuration saved successfully" : "LLM-Konfiguration erfolgreich gespeichert", + "LLM features disabled" : "LLM-Funktionen deaktiviert", + "LLM features enabled" : "LLM-Funktionen aktiviert", + "LLM must be enabled with an embedding provider configured" : "LLM muss aktiviert sein und ein Einbettungsanbieter muss konfiguriert sein", + "LLM settings updated successfully" : "LLM-Einstellungen erfolgreich aktualisiert", + "Last" : "Letzte", + "Last Triggered" : "Zuletzt ausgelöst", + "Length Range:" : "Längenbereich:", + "Linear" : "Linear", + "Load Advanced Filters" : "Erweiterte Filter laden", + "Load advanced filters with live data from your search index" : "Erweiterte Filter mit Live-Daten aus Ihrem Suchindex laden", + "Load view" : "Ansicht laden", + "Loading ConfigSets..." : "ConfigSets werden geladen...", + "Loading advanced filters..." : "Erweiterte Filter werden geladen...", + "Loading agents..." : "Agenten werden geladen...", + "Loading application details..." : "Anwendungsdetails werden geladen...", + "Loading applications..." : "Anwendungen werden geladen...", + "Loading archived conversations..." : "Archivierte Konversationen werden geladen...", + "Loading audit trails..." : "Prüfprotokolle werden geladen...", + "Loading collections..." : "Collections werden geladen...", + "Loading configurations..." : "Konfigurationen werden geladen...", + "Loading conversation..." : "Konversation wird geladen...", + "Loading conversations..." : "Konversationen werden geladen...", + "Loading deleted items..." : "Gelöschte Elemente werden geladen...", + "Loading events..." : "Ereignisse werden geladen...", + "Loading filter data..." : "Filterdaten werden geladen...", + "Loading groups..." : "Gruppen werden geladen...", + "Loading log details..." : "Protokolldetails werden geladen...", + "Loading registers..." : "Register werden geladen...", + "Loading schemas..." : "Schemas werden geladen...", + "Loading search trails..." : "Suchprotokolle werden geladen...", + "Loading sources..." : "Quellen werden geladen...", + "Loading statistics..." : "Statistiken werden geladen...", + "Loading users..." : "Benutzer werden geladen...", + "Loading views..." : "Ansichten werden geladen...", + "Loading..." : "Wird geladen...", + "Local" : "Lokal", + "Local Version" : "Lokale Version", + "Locked" : "Gesperrt", + "Locked:" : "Gesperrt:", + "Login successful" : "Anmeldung erfolgreich", + "Logs" : "Protokolle", + "Low" : "Niedrig", + "Low Confidence" : "Niedrige Konfidenz", + "Lucene Version:" : "Lucene-Version:", + "Magic table synchronized successfully" : "Magic Table erfolgreich synchronisiert", + "Manage Organisation Roles" : "Organisationsrollen verwalten", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Verwalten Sie SOLR-Collections (Datenspeicher) und weisen Sie sie Objekten und Dateien zu.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Verwalten Sie SOLR-ConfigSets (Konfigurationsvorlagen) für Ihre Collections.", + "Manage and configure agents for automated tasks" : "Agenten für automatisierte Aufgaben verwalten und konfigurieren", + "Manage and monitor file text extraction status" : "Status der Dateitextextraktion verwalten und überwachen", + "Manage and restore soft deleted items from your registers" : "Vorläufig gelöschte Elemente aus Ihren Registern verwalten und wiederherstellen", + "Manage and view detected entities from files and objects" : "Aus Dateien und Objekten erkannte Entitäten verwalten und anzeigen", + "Manage document templates and themes" : "Dokumentvorlagen und Themes verwalten", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Verwalten Sie n8n-Workflows für die OpenRegister-Automatisierung. Workflows werden im konfigurierten Projekt gespeichert.", + "Manage webhooks for event-driven integrations" : "Webhooks für ereignisgesteuerte Integrationen verwalten", + "Manage your applications and modules" : "Verwalten Sie Ihre Anwendungen und Module", + "Manage your data registers and their configurations" : "Verwalten Sie Ihre Datenregister und deren Konfigurationen", + "Manage your data schemas and their properties" : "Verwalten Sie Ihre Datenschemas und deren Eigenschaften", + "Manage your data sources and their configurations" : "Verwalten Sie Ihre Datenquellen und deren Konfigurationen", + "Manage your saved search configurations" : "Verwalten Sie Ihre gespeicherten Suchkonfigurationen", + "Manage your system configurations and settings" : "Verwalten Sie Ihre Systemkonfigurationen und -einstellungen", + "Managed" : "Verwaltet", + "Max Files (0 = all)" : "Max. Dateien (0 = alle)", + "Max Length" : "Max. Länge", + "Max Length:" : "Max. Länge:", + "Max Objects (0 = all)" : "Max. Objekte (0 = alle)", + "Max Retries" : "Max. Wiederholungen", + "Max Shards/Node" : "Max. Shards/Knoten", + "Max execution time (ms)" : "Max. Ausführungszeit (ms)", + "Max ms" : "Max. ms", + "Max result count" : "Max. Ergebnisanzahl", + "Max results" : "Max. Ergebnisse", + "Maximum" : "Maximum", + "Maximum Nesting Depth" : "Maximale Verschachtelungstiefe", + "Maximum length constraint is missing" : "Maximallängen-Einschränkung fehlt", + "Maximum length is too restrictive" : "Maximallänge ist zu restriktiv", + "Maximum number of files to process. Set to 0 to process all files." : "Maximale Anzahl der zu verarbeitenden Dateien. Auf 0 setzen, um alle Dateien zu verarbeiten.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maximale Anzahl der zu verarbeitenden Objekte. Auf 0 setzen, um alle Objekte zu verarbeiten.", + "Maximum number of retry attempts for failed deliveries" : "Maximale Anzahl der Wiederholungsversuche für fehlgeschlagene Zustellungen", + "Maximum time to wait before committing changes" : "Maximale Wartezeit vor dem Übernehmen von Änderungen", + "Maximum tokens to generate" : "Maximal zu generierende Token", + "Maximum value constraint is missing" : "Maximalwert-Einschränkung fehlt", + "Maximum value is too restrictive" : "Maximalwert ist zu restriktiv", + "Medium" : "Mittel", + "Medium Confidence" : "Mittlere Konfidenz", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Mittlere Abfragen: Suchen mit etwas Filterung oder mehreren Parametern (z. B. Datumsbereiche, bestimmte Register/Schemas)", + "Members of selected groups can access this view" : "Mitglieder ausgewählter Gruppen können auf diese Ansicht zugreifen", + "Members:" : "Mitglieder:", + "Memory prediction calculated successfully" : "Speichervorhersage erfolgreich berechnet", + "Message content is required" : "Nachrichteninhalt ist erforderlich", + "Message does not belong to this conversation" : "Nachricht gehört nicht zu dieser Konversation", + "Metadata Filters" : "Metadatenfilter", + "Method" : "Methode", + "Min Length" : "Min. Länge", + "Min execution time (ms)" : "Min. Ausführungszeit (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. Ergebnisanzahl", + "Min results" : "Min. Ergebnisse", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Minimalwert-Einschränkung fehlt", + "Minimum value is too restrictive" : "Minimalwert ist zu restriktiv", + "Missing object fields created successfully" : "Fehlende Objektfelder erfolgreich erstellt", + "Mode:" : "Modus:", + "Model" : "Modell", + "Model Name" : "Modellname", + "Modified" : "Geändert", + "Monthly" : "Monatlich", + "More sources (10-20):" : "Mehr Quellen (10-20):", + "Most Active Objects" : "Aktivste Objekte", + "Multiple Of" : "Vielfaches von", + "Name" : "Name", + "Names cache warmup completed" : "Namen-Cache-Aufwärmung abgeschlossen", + "New Collection Name" : "Neuer Collection-Name", + "New Conversation" : "Neue Konversation", + "New Properties" : "Neue Eigenschaften", + "New Property" : "Neue Eigenschaft", + "New Value" : "Neuer Wert", + "New users without specific organisation membership will be automatically added to this organisation" : "Neue Benutzer ohne spezifische Organisationsmitgliedschaft werden automatisch dieser Organisation hinzugefügt", + "Next" : "Weiter", + "No" : "Nein", + "No ConfigSets found" : "Keine ConfigSets gefunden", + "No GitHub token configured" : "Kein GitHub-Token konfiguriert", + "No GitHub token provided" : "Kein GitHub-Token angegeben", + "No GitLab token provided" : "Kein GitLab-Token angegeben", + "No Organisation" : "Keine Organisation", + "No active filters" : "Keine aktiven Filter", + "No activity data available" : "Keine Aktivitätsdaten verfügbar", + "No agents available" : "Keine Agenten verfügbar", + "No agents found" : "Keine Agenten gefunden", + "No app store cache exists yet" : "Es existiert noch kein App-Store-Cache", + "No applications are available." : "Es sind keine Anwendungen verfügbar.", + "No applications found" : "Keine Anwendungen gefunden", + "No archived conversations" : "Keine archivierten Konversationen", + "No audit trail entries found" : "Keine Prüfprotokolleinträge gefunden", + "No changes recorded" : "Keine Änderungen aufgezeichnet", + "No chunks to vectorize" : "Keine Chunks zu vektorisieren", + "No collections found" : "Keine Collections gefunden", + "No configuration data" : "Keine Konfigurationsdaten", + "No configurations are available." : "Es sind keine Konfigurationen verfügbar.", + "No configurations found" : "Keine Konfigurationen gefunden", + "No configurations found for this application." : "Keine Konfigurationen für diese Anwendung gefunden.", + "No conversations yet. Create a new one to get started!" : "Noch keine Konversationen. Erstellen Sie eine neue, um zu beginnen!", + "No data available" : "Keine Daten verfügbar", + "No data available for chart" : "Keine Daten für das Diagramm verfügbar", + "No deleted items found" : "Keine gelöschten Elemente gefunden", + "No deletion data available" : "Keine Löschdaten verfügbar", + "No description found" : "Keine Beschreibung gefunden", + "No description provided" : "Keine Beschreibung angegeben", + "No entities detected in this file. Run text extraction first." : "In dieser Datei wurden keine Entitäten erkannt. Führen Sie zuerst die Textextraktion durch.", + "No entities found" : "Keine Entitäten gefunden", + "No entities have been detected yet" : "Es wurden noch keine Entitäten erkannt", + "No events found" : "Keine Ereignisse gefunden", + "No expired audit trails found to clear" : "Keine abgelaufenen Prüfprotokolle zum Löschen gefunden", + "No facetable fields available. Select a register and schema to see available filters." : "Keine facettierbaren Felder verfügbar. Wählen Sie ein Register und ein Schema, um verfügbare Filter anzuzeigen.", + "No files found" : "Keine Dateien gefunden", + "No files have been extracted yet" : "Es wurden noch keine Dateien extrahiert", + "No files to process" : "Keine Dateien zu verarbeiten", + "No files to reindex" : "Keine Dateien zum erneuten Indizieren", + "No filters are currently active. This will delete ALL audit trail entries!" : "Derzeit sind keine Filter aktiv. Dadurch werden ALLE Prüfprotokolleinträge gelöscht!", + "No log entries found" : "Keine Protokolleinträge gefunden", + "No logs are available for this configuration." : "Für diese Konfiguration sind keine Protokolle verfügbar.", + "No logs are available for this source." : "Für diese Quelle sind keine Protokolle verfügbar.", + "No logs found" : "Keine Protokolle gefunden", + "No mismatched fields found - SOLR schema is properly configured" : "Keine nicht übereinstimmenden Felder gefunden – das SOLR-Schema ist korrekt konfiguriert", + "No objects found" : "Keine Objekte gefunden", + "No objects found to vectorize" : "Keine Objekte zum Vektorisieren gefunden", + "No objects selected for deletion" : "Keine Objekte zum Löschen ausgewählt", + "No objects to delete" : "Keine Objekte zu löschen", + "No objects were permanently deleted" : "Es wurden keine Objekte endgültig gelöscht", + "No properties found" : "Keine Eigenschaften gefunden", + "No properties match your filters." : "Keine Eigenschaften entsprechen Ihren Filtern.", + "No purge date set" : "Kein Bereinigungsdatum festgelegt", + "No register data available" : "Keine Registerdaten verfügbar", + "No registers found" : "Keine Register gefunden", + "No registers found for this application." : "Keine Register für diese Anwendung gefunden.", + "No relations found" : "Keine Beziehungen gefunden", + "No request body available" : "Kein Anfragetext verfügbar", + "No response body available" : "Kein Antworttext verfügbar", + "No saved views yet. Create one in the Search tab!" : "Noch keine gespeicherten Ansichten. Erstellen Sie eine im Tab Suche!", + "No schema data available" : "Keine Schemadaten verfügbar", + "No schema selected for exploration" : "Kein Schema zur Erkundung ausgewählt", + "No schemas found" : "Keine Schemas gefunden", + "No schemas found for this application." : "Keine Schemas für diese Anwendung gefunden.", + "No search terms data available" : "Keine Suchbegriffsdaten verfügbar", + "No search trail entries found" : "Keine Suchprotokolleinträge gefunden", + "No sources are available." : "Es sind keine Quellen verfügbar.", + "No sources found" : "Keine Quellen gefunden", + "No templates found" : "Keine Vorlagen gefunden", + "No templates have been created yet" : "Es wurden noch keine Vorlagen erstellt", + "No valid magic-mapped register+schema combinations found" : "Keine gültigen magic-gemappten Register+Schema-Kombinationen gefunden", + "No views found. Create views first before configuring vectorization." : "Keine Ansichten gefunden. Erstellen Sie zuerst Ansichten, bevor Sie die Vektorisierung konfigurieren.", + "No views match your search" : "Keine Ansichten entsprechen Ihrer Suche", + "No webhooks found" : "Keine Webhooks gefunden", + "No webhooks have been configured yet" : "Es wurden noch keine Webhooks konfiguriert", + "No workflows found in this project. Create workflows in the n8n editor." : "Keine Workflows in diesem Projekt gefunden. Erstellen Sie Workflows im n8n-Editor.", + "None" : "Keine", + "Not helpful" : "Nicht hilfreich", + "Not in use" : "Nicht in Verwendung", + "Number Constraints" : "Zahleneinschränkungen", + "Number of chunks to vectorize in one API call (1-100)." : "Anzahl der in einem API-Aufruf zu vektorisierenden Chunks (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Anzahl der in einem API-Aufruf zu vektorisierenden Chunks. Höher = schneller, aber mehr Speicher. Empfohlen: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Anzahl der in jedem Stapel zu verarbeitenden Objekte (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Anzahl der in einem API-Aufruf zu vektorisierenden Objekte. Höher = schneller, aber mehr Speicher. Empfohlen: 10-50.", + "Numeric Range:" : "Zahlenbereich:", + "Object" : "Objekt", + "Object Collection" : "Objekt-Collection", + "Object Count Prediction" : "Objektanzahl-Vorhersage", + "Object ID" : "Objekt-ID", + "Object Management" : "Objektverwaltung", + "Object Vectorization" : "Objektvektorisierung", + "Object Warmup" : "Objekt-Aufwärmung", + "Object collection not configured" : "Objekt-Collection nicht konfiguriert", + "Object not found" : "Objekt nicht gefunden", + "Object permanently deleted" : "Objekt endgültig gelöscht", + "Object restored successfully" : "Objekt erfolgreich wiederhergestellt", + "Object settings updated successfully" : "Objekteinstellungen erfolgreich aktualisiert", + "Object sources" : "Objektquellen", + "Object unlocked successfully" : "Objekt erfolgreich entsperrt", + "Object vectorization configuration saved successfully" : "Objektvektorisierungskonfiguration erfolgreich gespeichert", + "Object vectorized successfully" : "Objekt erfolgreich vektorisiert", + "Object:" : "Objekt:", + "Objects" : "Objekte", + "Objects Analyzed" : "Analysierte Objekte", + "Objects Distribution" : "Objektverteilung", + "Objects being analyzed" : "Objekte werden analysiert", + "Objects by Register" : "Objekte nach Register", + "Objects by Schema" : "Objekte nach Schema", + "Objects deletion completed successfully" : "Objektlöschung erfolgreich abgeschlossen", + "Objects in schema" : "Objekte im Schema", + "Objects to Process:" : "Zu verarbeitende Objekte:", + "Objects to be analyzed" : "Zu analysierende Objekte", + "Objects to be deleted" : "Zu löschende Objekte", + "Objects to be validated" : "Zu validierende Objekte", + "Objects will be serialized as JSON text before vectorization" : "Objekte werden vor der Vektorisierung als JSON-Text serialisiert", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekte werden vorläufig gelöscht (als gelöscht markiert, aber in der Datenbank behalten). Sie können bei Bedarf später wiederhergestellt werden.", + "Old Value" : "Alter Wert", + "Oldest Item (days)" : "Ältestes Element (Tage)", + "Ollama Chat Settings" : "Ollama-Chat-Einstellungen", + "Ollama Embedding Configuration" : "Ollama-Einbettungskonfiguration", + "Ollama URL" : "Ollama-URL", + "Only With Changes" : "Nur mit Änderungen", + "Open Register Objects" : "Open-Register-Objekte", + "Open n8n Editor" : "n8n-Editor öffnen", + "OpenAI Chat Settings" : "OpenAI-Chat-Einstellungen", + "OpenAI Embedding Configuration" : "OpenAI-Einbettungskonfiguration", + "OpenRegister Settings" : "OpenRegister-Einstellungen", + "Optional webhook secret for signature verification" : "Optionales Webhook-Geheimnis zur Signaturüberprüfung", + "Order" : "Reihenfolge", + "Organisation" : "Organisation", + "Organisation ID" : "Organisations-ID", + "Organisation Members" : "Organisationsmitglieder", + "Organisation Statistics" : "Organisationsstatistiken", + "Organisation created successfully" : "Organisation erfolgreich erstellt", + "Organisation settings saved successfully" : "Organisationseinstellungen erfolgreich gespeichert", + "Organisations" : "Organisationen", + "Organization" : "Organisation", + "Organization ID (Optional)" : "Organisations-ID (optional)", + "Orphaned Items" : "Verwaiste Elemente", + "Owner" : "Eigentümer", + "Owner:" : "Eigentümer:", + "Parallel Mode (Faster, more resource intensive)" : "Paralleler Modus (schneller, ressourcenintensiver)", + "Parallel:" : "Parallel:", + "Password" : "Passwort", + "Password for SOLR authentication (optional)" : "Passwort für SOLR-Authentifizierung (optional)", + "Path" : "Pfad", + "Path in repository" : "Pfad im Repository", + "Path where the configuration file will be saved in the repository" : "Pfad, unter dem die Konfigurationsdatei im Repository gespeichert wird", + "Path where the register OAS file will be saved in the repository" : "Pfad, unter dem die OAS-Datei des Registers im Repository gespeichert wird", + "Pattern (regex)" : "Muster (Regex)", + "Pattern Issue" : "Musterproblem", + "Pattern constraint is missing" : "Mustereinschränkung fehlt", + "Patterns:" : "Muster:", + "Pending" : "Ausstehend", + "Permanent Delete Mode" : "Modus für endgültiges Löschen", + "Permanently Delete" : "Endgültig löschen", + "Permanently delete already soft-deleted objects" : "Bereits vorläufig gelöschte Objekte endgültig löschen", + "Permanently delete this conversation?" : "Diese Konversation endgültig löschen?", + "Person" : "Person", + "Personal Data" : "Personenbezogene Daten", + "Phone" : "Telefon", + "Photos" : "Fotos", + "Please create an agent in the" : "Bitte erstellen Sie einen Agenten im", + "Please select an agent to continue" : "Bitte wählen Sie einen Agenten aus, um fortzufahren", + "Please select which register and schema to use for the new object" : "Bitte wählen Sie aus, welches Register und Schema für das neue Objekt verwendet werden soll", + "Please try again later." : "Bitte versuchen Sie es später erneut.", + "Please wait while we fetch your agents." : "Bitte warten Sie, während wir Ihre Agenten abrufen.", + "Please wait while we fetch your applications." : "Bitte warten Sie, während wir Ihre Anwendungen abrufen.", + "Please wait while we fetch your configurations." : "Bitte warten Sie, während wir Ihre Konfigurationen abrufen.", + "Please wait while we fetch your deleted items." : "Bitte warten Sie, während wir Ihre gelöschten Elemente abrufen.", + "Please wait while we fetch your sources." : "Bitte warten Sie, während wir Ihre Quellen abrufen.", + "Polls" : "Umfragen", + "Popular Search Terms" : "Beliebte Suchbegriffe", + "Port" : "Port", + "Prerequisites" : "Voraussetzungen", + "Press Enter to send, Shift+Enter for new line" : "Drücken Sie die Eingabetaste zum Senden, Umschalt+Eingabetaste für eine neue Zeile", + "Previous" : "Zurück", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Verarbeitet Chunks in Stapeln mit simulierter Parallelität.", + "Processes file chunks sequentially (safest)." : "Verarbeitet Datei-Chunks sequenziell (am sichersten).", + "Processes objects in chunks with simulated parallelism." : "Verarbeitet Objekte in Chunks mit simulierter Parallelität.", + "Processes objects sequentially (safest)." : "Verarbeitet Objekte sequenziell (am sichersten).", + "Processing" : "Verarbeitung", + "Processing Limits" : "Verarbeitungsgrenzen", + "Processing..." : "Wird verarbeitet...", + "Project initialized successfully" : "Projekt erfolgreich initialisiert", + "Project not found. Please initialize first." : "Projekt nicht gefunden. Bitte initialisieren Sie es zuerst.", + "Properties" : "Eigenschaften", + "Property Behaviors" : "Eigenschaftsverhalten", + "Property Configuration:" : "Eigenschaftskonfiguration:", + "Property Filters" : "Eigenschaftsfilter", + "Property Title" : "Eigenschaftstitel", + "Property Type" : "Eigenschaftstyp", + "Property can be improved" : "Eigenschaft kann verbessert werden", + "Protected" : "Geschützt", + "Provider" : "Anbieter", + "Provider is required for testing" : "Anbieter ist zum Testen erforderlich", + "Public" : "Öffentlich", + "Public View" : "Öffentliche Ansicht", + "Public views can be accessed by anyone in the system" : "Auf öffentliche Ansichten kann jeder im System zugreifen", + "Publish" : "Veröffentlichen", + "Publish Objects" : "Objekte veröffentlichen", + "Publish all objects in this schema" : "Alle Objekte in diesem Schema veröffentlichen", + "Published" : "Veröffentlicht", + "Published:" : "Veröffentlicht:", + "Publishing..." : "Wird veröffentlicht...", + "Purge" : "Bereinigen", + "Purge Date" : "Bereinigungsdatum", + "Query Complexity Distribution" : "Verteilung der Abfragekomplexität", + "Query parameter is required" : "Abfrageparameter ist erforderlich", + "RAG" : "RAG", + "RAG Configuration" : "RAG-Konfiguration", + "Rate limits cleared successfully" : "Ratenbegrenzungen erfolgreich zurückgesetzt", + "Raw Changes Data" : "Rohänderungsdaten", + "Re-vectorize on object update" : "Bei Objektaktualisierung neu vektorisieren", + "Read" : "Lesen", + "Recommendations:" : "Empfehlungen:", + "Recommended Type:" : "Empfohlener Typ:", + "Recommended: 5 sources" : "Empfohlen: 5 Quellen", + "Refresh" : "Aktualisieren", + "Refresh Data" : "Daten aktualisieren", + "Refresh Stats" : "Statistiken aktualisieren", + "Refresh Workflows" : "Workflows aktualisieren", + "Refresh dashboard" : "Dashboard aktualisieren", + "Refresh database info" : "Datenbankinformationen aktualisieren", + "Register" : "Register", + "Register ID" : "Register-ID", + "Register Statistics" : "Registerstatistiken", + "Register Totals" : "Registersummen", + "Register depublished successfully" : "Veröffentlichung des Registers erfolgreich aufgehoben", + "Register not found" : "Register nicht gefunden", + "Register objects deletion completed successfully" : "Löschung der Registerobjekte erfolgreich abgeschlossen", + "Register published successfully" : "Register erfolgreich veröffentlicht", + "Register/Schema Usage" : "Register-/Schemanutzung", + "Registers" : "Register", + "Reindex" : "Neu indizieren", + "Reindex completed" : "Neuindizierung abgeschlossen", + "Reindex completed successfully" : "Neuindizierung erfolgreich abgeschlossen", + "Reindex failed" : "Neuindizierung fehlgeschlagen", + "Relations" : "Beziehungen", + "Remote Version" : "Remote-Version", + "Remove" : "Entfernen", + "Remove filter" : "Filter entfernen", + "Remove from favorites" : "Aus Favoriten entfernen", + "Remove group" : "Gruppe entfernen", + "Removed" : "Entfernt", + "Removed from favorites" : "Aus Favoriten entfernt", + "Rename Conversation" : "Konversation umbenennen", + "Rename conversation" : "Konversation umbenennen", + "Replicas" : "Replikate", + "Repository" : "Repository", + "Request Body" : "Anfragetext", + "Request Data" : "Anfragedaten", + "Request Details" : "Anfragedetails", + "Request body must be JSON with a \"query\" field" : "Der Anfragetext muss JSON mit einem Feld \"query\" sein", + "Request timeout in seconds" : "Anfragezeitüberschreitung in Sekunden", + "Required field" : "Pflichtfeld", + "Required status is inconsistent" : "Pflichtstatus ist inkonsistent", + "Rerun Search" : "Suche erneut ausführen", + "Reset Changes" : "Änderungen zurücksetzen", + "Reset Filters" : "Filter zurücksetzen", + "Response Body" : "Antworttext", + "Response Details" : "Antwortdetails", + "Response Time:" : "Antwortzeit:", + "Restore" : "Wiederherstellen", + "Restore conversation" : "Konversation wiederherstellen", + "Restore or permanently delete items" : "Elemente wiederherstellen oder endgültig löschen", + "Result Count Range" : "Ergebnisanzahlbereich", + "Results" : "Ergebnisse", + "Retrieve all objects for this schema" : "Alle Objekte für dieses Schema abrufen", + "Retry" : "Wiederholen", + "Retry Failed Extractions" : "Fehlgeschlagene Extraktionen wiederholen", + "Retry Policy" : "Wiederholungsrichtlinie", + "Retry completed" : "Wiederholung abgeschlossen", + "Retrying..." : "Wird wiederholt...", + "Risk Level" : "Risikostufe", + "Roles updated successfully" : "Rollen erfolgreich aktualisiert", + "SOLR Connection Settings" : "SOLR-Verbindungseinstellungen", + "SOLR Version:" : "SOLR-Version:", + "SOLR actions menu" : "SOLR-Aktionsmenü", + "SOLR base path (usually /solr)" : "SOLR-Basispfad (normalerweise /solr)", + "SOLR is disabled" : "SOLR ist deaktiviert", + "SOLR is not available or not configured" : "SOLR ist nicht verfügbar oder nicht konfiguriert", + "SOLR logging disabled" : "SOLR-Protokollierung deaktiviert", + "SOLR logging enabled" : "SOLR-Protokollierung aktiviert", + "SOLR search disabled" : "SOLR-Suche deaktiviert", + "SOLR search enabled" : "SOLR-Suche aktiviert", + "SOLR server hostname or IP address" : "SOLR-Server-Hostname oder IP-Adresse", + "SOLR server port number (optional, defaults to 8983)" : "SOLR-Server-Portnummer (optional, Standard ist 8983)", + "SOLR setup completed successfully" : "SOLR-Einrichtung erfolgreich abgeschlossen", + "SOLR setup failed" : "SOLR-Einrichtung fehlgeschlagen", + "SOLR setup failed - check logs" : "SOLR-Einrichtung fehlgeschlagen – Protokolle prüfen", + "Sample Values:" : "Beispielwerte:", + "Save" : "Speichern", + "Save Configuration" : "Konfiguration speichern", + "Save Connection Settings" : "Verbindungseinstellungen speichern", + "Save Roles" : "Rollen speichern", + "Save Settings" : "Einstellungen speichern", + "Save current search as view" : "Aktuelle Suche als Ansicht speichern", + "Saved Views" : "Gespeicherte Ansichten", + "Saving..." : "Wird gespeichert...", + "Schema" : "Schema", + "Schema ID" : "Schema-ID", + "Schema Name" : "Schemaname", + "Schema Statistics" : "Schemastatistiken", + "Schema depublished successfully" : "Veröffentlichung des Schemas erfolgreich aufgehoben", + "Schema objects deletion completed successfully" : "Löschung der Schemaobjekte erfolgreich abgeschlossen", + "Schema published successfully" : "Schema erfolgreich veröffentlicht", + "Schema successfully created" : "Schema erfolgreich erstellt", + "Schema successfully updated" : "Schema erfolgreich aktualisiert", + "Schemas" : "Schemas", + "Scheme" : "Schema", + "Search / Views" : "Suche / Ansichten", + "Search Activity" : "Suchaktivität", + "Search Analytics" : "Suchanalysen", + "Search Entities" : "Entitäten suchen", + "Search Files" : "Dateien suchen", + "Search GitHub" : "GitHub durchsuchen", + "Search GitLab" : "GitLab durchsuchen", + "Search Method" : "Suchmethode", + "Search Term" : "Suchbegriff", + "Search Trail Details" : "Suchprotokolldetails", + "Search Trail Management" : "Suchprotokollverwaltung", + "Search Trail Statistics" : "Suchprotokollstatistiken", + "Search Trails" : "Suchprotokolle", + "Search Views" : "Ansichten durchsuchen", + "Search Webhooks" : "Webhooks durchsuchen", + "Search by file name or path" : "Nach Dateiname oder Pfad suchen", + "Search by name or URL" : "Nach Name oder URL suchen", + "Search by value" : "Nach Wert suchen", + "Search in Files" : "In Dateien suchen", + "Search in Objects" : "In Objekten suchen", + "Search property names..." : "Eigenschaftsnamen suchen...", + "Search trail deletion not implemented yet" : "Löschung von Suchprotokollen noch nicht implementiert", + "Search views..." : "Ansichten durchsuchen...", + "Secret" : "Geheimnis", + "Secret key for HMAC signature generation (optional)" : "Geheimer Schlüssel für die HMAC-Signaturgenerierung (optional)", + "Select AI Agent" : "KI-Agenten auswählen", + "Select All" : "Alle auswählen", + "Select ConfigSet" : "ConfigSet auswählen", + "Select File Types to Vectorize:" : "Zu vektorisierende Dateitypen auswählen:", + "Select HTTP method" : "HTTP-Methode auswählen", + "Select Register and Schema" : "Register und Schema auswählen", + "Select Views to Vectorize:" : "Zu vektorisierende Ansichten auswählen:", + "Select a Nextcloud group to add" : "Eine Nextcloud-Gruppe zum Hinzufügen auswählen", + "Select a branch" : "Branch auswählen", + "Select a model or type a custom model name" : "Ein Modell auswählen oder einen benutzerdefinierten Modellnamen eingeben", + "Select a repository" : "Repository auswählen", + "Select a repository you have write access to" : "Wählen Sie ein Repository aus, für das Sie Schreibzugriff haben", + "Select an AI Agent" : "Einen KI-Agenten auswählen", + "Select an AI agent to begin chatting with your data." : "Wählen Sie einen KI-Agenten aus, um mit Ihren Daten zu chatten.", + "Select backend" : "Backend auswählen", + "Select chat model" : "Chat-Modell auswählen", + "Select collection for files" : "Collection für Dateien auswählen", + "Select collection for objects" : "Collection für Objekte auswählen", + "Select default organisation" : "Standardorganisation auswählen", + "Select event to listen to..." : "Ereignis auswählen, auf das gehört werden soll...", + "Select model" : "Modell auswählen", + "Select options..." : "Optionen auswählen...", + "Select period" : "Zeitraum auswählen", + "Select property to send as payload" : "Eigenschaft auswählen, die als Payload gesendet werden soll", + "Select provider" : "Anbieter auswählen", + "Select registers and schemas to save a view" : "Register und Schemas auswählen, um eine Ansicht zu speichern", + "Select retry policy" : "Wiederholungsrichtlinie auswählen", + "Select the branch to publish to" : "Branch auswählen, auf dem veröffentlicht werden soll", + "Select the event this webhook should listen to" : "Ereignis auswählen, auf das dieser Webhook hören soll", + "Select views to vectorize:" : "Zu vektorisierende Ansichten auswählen:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Wählen Sie aus, welche Nextcloud-Gruppen für diese Organisation verfügbar sind. Benutzer in diesen Gruppen haben Zugriff auf Organisationsressourcen.", + "Select which data views the AI can search" : "Wählen Sie aus, welche Datenansichten die KI durchsuchen kann", + "Select which property from the event should be used as the webhook payload data" : "Wählen Sie aus, welche Eigenschaft des Ereignisses als Webhook-Payload-Daten verwendet werden soll", + "Select which tools the AI can use to perform actions" : "Wählen Sie aus, welche Werkzeuge die KI zum Ausführen von Aktionen verwenden kann", + "Selected" : "Ausgewählt", + "Selected Groups" : "Ausgewählte Gruppen", + "Selected audit trails deleted successfully" : "Ausgewählte Prüfprotokolle erfolgreich gelöscht", + "Selected search trails deleted successfully" : "Ausgewählte Suchprotokolle erfolgreich gelöscht", + "Selected users can access this view" : "Ausgewählte Benutzer können auf diese Ansicht zugreifen", + "Send additional feedback" : "Zusätzliches Feedback senden", + "Send as CloudEvent" : "Als CloudEvent senden", + "Sensitive PII" : "Sensible personenbezogene Daten", + "Serial Mode (Safer, slower)" : "Serieller Modus (sicherer, langsamer)", + "Serial:" : "Seriell:", + "Server Configuration" : "Serverkonfiguration", + "Server Information" : "Serverinformationen", + "Settings" : "Einstellungen", + "Shards" : "Shards", + "Share" : "Teilen", + "Share with Groups" : "Mit Gruppen teilen", + "Share with Users" : "Mit Benutzern teilen", + "Show Filters" : "Filter anzeigen", + "Show less" : "Weniger anzeigen", + "Show only entries with changes" : "Nur Einträge mit Änderungen anzeigen", + "Simple" : "Einfach", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Einfache Abfragen: Grundlegende Textsuchen mit minimalen Parametern (z. B. einzelner Suchbegriff, keine erweiterten Filter)", + "Size" : "Größe", + "Slug" : "Slug", + "Soft Delete Mode" : "Modus für vorläufiges Löschen", + "Soft Deleted Items" : "Vorläufig gelöschte Elemente", + "SolrCloud mode enabled" : "SolrCloud-Modus aktiviert", + "Source" : "Quelle", + "Sources" : "Quellen", + "Sources:" : "Quellen:", + "Standalone SOLR mode" : "Eigenständiger SOLR-Modus", + "Start Conversation" : "Konversation starten", + "Start Vectorization" : "Vektorisierung starten", + "Start a conversation" : "Eine Konversation starten", + "Starting names cache warmup..." : "Namen-Cache-Aufwärmung wird gestartet...", + "Starting..." : "Wird gestartet...", + "Statistics" : "Statistiken", + "Stats calculation not yet implemented" : "Statistikberechnung noch nicht implementiert", + "Status" : "Status", + "Status Code" : "Statuscode", + "String Constraints" : "Zeichenketteneinschränkungen", + "Success" : "Erfolg", + "Success Rate" : "Erfolgsrate", + "Success Status" : "Erfolgsstatus", + "Successful" : "Erfolgreich", + "Successfully joined organisation" : "Organisation erfolgreich beigetreten", + "Sync Table" : "Tabelle synchronisieren", + "Synchronization Settings" : "Synchronisierungseinstellungen", + "System Default" : "Systemstandard", + "Technical Description" : "Technische Beschreibung", + "Technical description for developers and administrators" : "Technische Beschreibung für Entwickler und Administratoren", + "Temperature" : "Temperatur", + "Templates" : "Vorlagen", + "Test" : "Testen", + "Test Chat" : "Chat testen", + "Test Connection" : "Verbindung testen", + "Test Embedding" : "Einbettung testen", + "Test endpoint executed successfully" : "Testendpunkt erfolgreich ausgeführt", + "Test webhook delivered successfully" : "Test-Webhook erfolgreich zugestellt", + "Test webhook delivery failed" : "Test-Webhook-Zustellung fehlgeschlagen", + "Test webhook sent successfully" : "Test-Webhook erfolgreich gesendet", + "Testing..." : "Wird getestet...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Text-Chunks werden während der Dateiextraktion generiert und in der Datenbank gespeichert. Die Vektorisierung liest diese vorab aufgeteilten Dateien und wandelt sie in Einbettungen um.", + "Text extracted successfully" : "Text erfolgreich extrahiert", + "Text extraction disabled" : "Textextraktion deaktiviert", + "The URL where webhook events will be sent" : "Die URL, an die Webhook-Ereignisse gesendet werden", + "The requested conversation does not exist" : "Die angeforderte Konversation existiert nicht", + "The saved view and all its search configuration" : "Die gespeicherte Ansicht und ihre gesamte Suchkonfiguration", + "There are no audit trail entries matching your current filters." : "Es gibt keine Prüfprotokolleinträge, die Ihren aktuellen Filtern entsprechen.", + "There are no deleted items matching your current filters." : "Es gibt keine gelöschten Elemente, die Ihren aktuellen Filtern entsprechen.", + "There are no search trail entries matching your current filters." : "Es gibt keine Suchprotokolleinträge, die Ihren aktuellen Filtern entsprechen.", + "There are no webhook log entries matching your filters." : "Es gibt keine Webhook-Protokolleinträge, die Ihren Filtern entsprechen.", + "This action cannot be undone." : "Diese Aktion kann nicht rückgängig gemacht werden.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Diese Aktion kann nicht rückgängig gemacht werden. Stellen Sie sicher, dass keine Collections dieses ConfigSet verwenden.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Dieser Agent hat keine konfigurierbaren Ansichten oder Werkzeuge, aber Sie können unten dennoch die RAG-Einstellungen anpassen.", + "This analysis may take some time" : "Diese Analyse kann einige Zeit dauern", + "This audit trail entry does not contain any change information." : "Dieser Prüfprotokolleintrag enthält keine Änderungsinformationen.", + "This configuration has no data defined." : "Für diese Konfiguration sind keine Daten definiert.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Dieser Endpunkt ist veraltet. Verwenden Sie stattdessen Chunk-basierte Endpunkte.", + "This entity has no relations to objects or files" : "Diese Entität hat keine Beziehungen zu Objekten oder Dateien", + "This is a test webhook from OpenRegister" : "Dies ist ein Test-Webhook von OpenRegister", + "This schema must use Magic Table configuration to sync" : "Dieses Schema muss die Magic-Table-Konfiguration verwenden, um zu synchronisieren", + "This source has no associated registers." : "Diese Quelle hat keine zugeordneten Register.", + "This will copy the _default ConfigSet with the new name" : "Dadurch wird das ConfigSet _default mit dem neuen Namen kopiert", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Dadurch werden ALLE Einbettungen (Vektoren) endgültig aus der Datenbank gelöscht. Sie müssen alle Objekte und Dateien neu vektorisieren. Diese Aktion kann nicht rückgängig gemacht werden.\\n\\nMöchten Sie wirklich fortfahren?", + "This will permanently delete:" : "Dadurch wird endgültig gelöscht:", + "Timeout (seconds)" : "Zeitüberschreitung (Sekunden)", + "Timestamp" : "Zeitstempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tipp: Aktivieren Sie nur Ansichten, die eine semantische Suche benötigen, um die Einbettungskosten zu minimieren. Einfache Nachschlagetabellen benötigen selten eine Vektorisierung.", + "Title" : "Titel", + "To Date" : "Bis Datum", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Um die Chunk-Größe oder -Strategie anzupassen, gehen Sie zu Dateikonfiguration → Verarbeitungsgrenzen.", + "To change the embedding provider or model, go to LLM Configuration." : "Um den Einbettungsanbieter oder das Modell zu ändern, gehen Sie zur LLM-Konfiguration.", + "To date" : "Bis Datum", + "Toggle search sidebar" : "Such-Seitenleiste umschalten", + "Tools" : "Werkzeuge", + "Top Deleters" : "Top-Löscher", + "Total" : "Gesamt", + "Total Audit Trails" : "Prüfprotokolle gesamt", + "Total Chunks Available:" : "Verfügbare Chunks gesamt:", + "Total Deleted Items" : "Gelöschte Elemente gesamt", + "Total Members" : "Mitglieder gesamt", + "Total Objects" : "Objekte gesamt", + "Total Objects in Database:" : "Objekte in der Datenbank gesamt:", + "Total Organisations" : "Organisationen gesamt", + "Total Results" : "Ergebnisse gesamt", + "Total Searches" : "Suchen gesamt", + "Total Size" : "Gesamtgröße", + "Total Storage" : "Gesamtspeicher", + "Total:" : "Gesamt:", + "Totals" : "Summen", + "Type" : "Typ", + "Type Issue" : "Typproblem", + "Type Variations:" : "Typvariationen:", + "Type must be \"positive\" or \"negative\"" : "Typ muss \"positive\" oder \"negative\" sein", + "Type to search for groups" : "Tippen, um nach Gruppen zu suchen", + "Type to search for users" : "Tippen, um nach Benutzern zu suchen", + "Type to search groups..." : "Tippen, um Gruppen zu suchen...", + "Type to search users..." : "Tippen, um Benutzer zu suchen...", + "URL" : "URL", + "URL where Ollama is running" : "URL, unter der Ollama läuft", + "UUID:" : "UUID:", + "Unchanged" : "Unverändert", + "Unique Search Terms" : "Eindeutige Suchbegriffe", + "Unique Terms" : "Eindeutige Begriffe", + "Unknown" : "Unbekannt", + "Unknown Register" : "Unbekanntes Register", + "Unknown Schema" : "Unbekanntes Schema", + "Untitled View" : "Unbenannte Ansicht", + "Update" : "Aktualisieren", + "Update Operations" : "Aktualisierungsvorgänge", + "Update configuration: ..." : "Konfiguration aktualisieren: ...", + "Update register OAS: ..." : "Register-OAS aktualisieren: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Vektoren aktualisieren, wenn sich Objektdaten ändern (empfohlen für genaue Suche)", + "Updated" : "Aktualisiert", + "Updated At" : "Aktualisiert am", + "Updated:" : "Aktualisiert:", + "Uptime:" : "Betriebszeit:", + "Usage Count" : "Nutzungsanzahl", + "Use SolrCloud with Zookeeper for distributed search" : "SolrCloud mit Zookeeper für verteilte Suche verwenden", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Verwenden Sie Filter, um Prüfprotokolleinträge nach Register, Schema, Aktionstyp, Benutzer, Datumsbereich oder Objekt-ID einzugrenzen.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Verwenden Sie Filter, um gelöschte Elemente nach Register, Schema, Löschdatum oder dem Benutzer, der sie gelöscht hat, einzugrenzen.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Verwenden Sie Filter, um Suchprotokolleinträge nach Register, Schema, Erfolgsstatus, Benutzer, Datumsbereich, Suchbegriffen oder Leistungsmetriken einzugrenzen.", + "Used By Collections" : "Verwendet von Collections", + "User" : "Benutzer", + "User Agent Statistics" : "User-Agent-Statistiken", + "User Description" : "Benutzerbeschreibung", + "User rate limits cleared successfully" : "Benutzer-Ratenbegrenzungen erfolgreich zurückgesetzt", + "User removed successfully" : "Benutzer erfolgreich entfernt", + "User-friendly description shown in forms and help text" : "Benutzerfreundliche Beschreibung, die in Formularen und Hilfetexten angezeigt wird", + "Username" : "Benutzername", + "Username for SOLR authentication (optional)" : "Benutzername für SOLR-Authentifizierung (optional)", + "Users" : "Benutzer", + "Using Pre-Generated Chunks" : "Vorgenerierte Chunks verwenden", + "Validate" : "Validieren", + "Validate Objects" : "Objekte validieren", + "Validation completed successfully" : "Validierung erfolgreich abgeschlossen", + "Validation failed" : "Validierung fehlgeschlagen", + "Value" : "Wert", + "Vector Dimensions" : "Vektordimensionen", + "Vector Search Backend" : "Vektorsuch-Backend", + "Vector field: _embedding_" : "Vektorfeld: _embedding_", + "Vectorization Triggers" : "Vektorisierungsauslöser", + "Vectorize All Files" : "Alle Dateien vektorisieren", + "Vectorize All Objects" : "Alle Objekte vektorisieren", + "Vectorize all file types" : "Alle Dateitypen vektorisieren", + "Vectorize all views" : "Alle Ansichten vektorisieren", + "Vectorize on object creation" : "Bei Objekterstellung vektorisieren", + "Vectors will be stored in your existing object and file collections" : "Vektoren werden in Ihren bestehenden Objekt- und Datei-Collections gespeichert", + "Version" : "Version", + "Very High" : "Sehr hoch", + "View API Docs" : "API-Dokumentation anzeigen", + "View Changes" : "Änderungen anzeigen", + "View Details" : "Details anzeigen", + "View Error" : "Fehler anzeigen", + "View File" : "Datei anzeigen", + "View Full Details" : "Vollständige Details anzeigen", + "View Logs" : "Protokolle anzeigen", + "View Name" : "Ansichtsname", + "View Object" : "Objekt anzeigen", + "View Parameters" : "Ansichtsparameter", + "View Selection" : "Ansichtsauswahl", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Suchprotokolle mit erweiterten Filter- und Analysefunktionen anzeigen und analysieren", + "View and analyze system audit trails with advanced filtering capabilities" : "System-Prüfprotokolle mit erweiterten Filterfunktionen anzeigen und analysieren", + "View deleted successfully" : "Ansicht erfolgreich gelöscht", + "View entity details and manage relations" : "Entitätsdetails anzeigen und Beziehungen verwalten", + "View name" : "Ansichtsname", + "View name is required" : "Ansichtsname ist erforderlich", + "View saved successfully!" : "Ansicht erfolgreich gespeichert!", + "View search analytics and manage search logs" : "Suchanalysen anzeigen und Suchprotokolle verwalten", + "View successfully deleted" : "Ansicht erfolgreich gelöscht", + "View successfully updated" : "Ansicht erfolgreich aktualisiert", + "View updated successfully!" : "Ansicht erfolgreich aktualisiert!", + "View webhook delivery logs and filter by webhook" : "Webhook-Zustellungsprotokolle anzeigen und nach Webhook filtern", + "Views" : "Ansichten", + "Visible to users" : "Für Benutzer sichtbar", + "Wait for Response" : "Auf Antwort warten", + "Wait for webhook response before continuing (required for request/response flows)" : "Auf Webhook-Antwort warten, bevor fortgefahren wird (erforderlich für Anfrage-/Antwort-Abläufe)", + "Warmup Names Cache" : "Namen-Cache aufwärmen", + "Warning" : "Warnung", + "Warning:" : "Warnung:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Webhook-Protokolldetails", + "Webhook Logs" : "Webhook-Protokolle", + "Webhook created successfully" : "Webhook erfolgreich erstellt", + "Webhook deleted" : "Webhook gelöscht", + "Webhook retry delivered successfully" : "Webhook-Wiederholung erfolgreich zugestellt", + "Webhook retry delivery failed" : "Webhook-Wiederholungszustellung fehlgeschlagen", + "Webhook updated" : "Webhook aktualisiert", + "Webhook updated successfully" : "Webhook erfolgreich aktualisiert", + "Webhooks" : "Webhooks", + "Weekly" : "Wöchentlich", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Webhook-Payload für bessere Interoperabilität im CloudEvents-Format einpacken", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Ja", + "You" : "Sie", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Sie können neue ConfigSets basierend auf der Vorlage _default erstellen oder benutzerdefinierte ConfigSets direkt auf Ihren SOLR-Server hochladen.", + "You do not have access to this conversation" : "Sie haben keinen Zugriff auf diese Konversation", + "You do not have permission to delete this conversation" : "Sie haben keine Berechtigung, diese Konversation zu löschen", + "You do not have permission to modify this conversation" : "Sie haben keine Berechtigung, diese Konversation zu ändern", + "You do not have permission to restore this conversation" : "Sie haben keine Berechtigung, diese Konversation wiederherzustellen", + "You must be logged in to favorite views" : "Sie müssen angemeldet sein, um Ansichten zu favorisieren", + "You need an AI agent to start a conversation." : "Sie benötigen einen KI-Agenten, um eine Konversation zu starten.", + "Your Fireworks AI API key. Get one at" : "Ihr Fireworks-AI-API-Schlüssel. Holen Sie sich einen unter", + "Your OpenAI API key. Get one at" : "Ihr OpenAI-API-Schlüssel. Holen Sie sich einen unter", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Ihr Feedback wurde erfasst. Optional können Sie hier zusätzliche Details angeben...", + "Zookeeper Hosts" : "Zookeeper-Hosts", + "Zookeeper Port" : "Zookeeper-Port", + "Zookeeper Settings (SolrCloud)" : "Zookeeper-Einstellungen (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper-Verbindungszeichenkette für SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper-Portnummer (optional, Standard ist 2181)", + "chunk" : "Chunk", + "chunks" : "Chunks", + "chunks processed" : "Chunks verarbeitet", + "configuration(s)" : "Konfiguration(en)", + "e.g., lib/Settings/config.json" : "z. B. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "z. B. lib/Settings/register.json", + "failed" : "fehlgeschlagen", + "file" : "Datei", + "files" : "Dateien", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "Mitglieder", + "menu or contact someone with permission to create agents." : "Menü oder kontaktieren Sie jemanden mit der Berechtigung, Agenten zu erstellen.", + "messages" : "Nachrichten", + "n8n URL and API key are required" : "n8n-URL und API-Schlüssel sind erforderlich", + "n8n configuration saved successfully" : "n8n-Konfiguration erfolgreich gespeichert", + "n8n connection not configured" : "n8n-Verbindung nicht konfiguriert", + "n8n connection successful" : "n8n-Verbindung erfolgreich", + "n8n connection test successful" : "n8n-Verbindungstest erfolgreich", + "n8n integration disabled" : "n8n-Integration deaktiviert", + "n8n integration enabled" : "n8n-Integration aktiviert", + "n8n project initialized successfully" : "n8n-Projekt erfolgreich initialisiert", + "n8n settings saved successfully" : "n8n-Einstellungen erfolgreich gespeichert", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "Objekte", + "objects processed" : "Objekte verarbeitet", + "or pick a range" : "oder einen Bereich auswählen", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "bietet eine gute Balance zwischen Geschwindigkeit und Genauigkeit.", + "register(s)" : "Register", + "register(s) selected" : "Register ausgewählt", + "required" : "erforderlich", + "results" : "Ergebnisse", + "schema(s)" : "Schema(s)", + "schema(s) selected" : "Schema(s) ausgewählt", + "sk-..." : "sk-...", + "this application" : "diese Anwendung", + "to" : "bis", + "vectorized" : "vektorisiert", + "views selected" : "Ansichten ausgewählt", + "ℹ️ Current Configuration" : "ℹ️ Aktuelle Konfiguration", + "⚡ Batch Processing" : "⚡ Stapelverarbeitung", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Umfassender Kontext ✓ Geringere Wahrscheinlichkeit, Details zu übersehen ✗ Langsamere Antworten ✗ Kann weniger relevante Informationen enthalten", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Schnellere Antworten ✓ Fokussiertere Antworten ✗ Kann relevante Informationen übersehen", + "✨ AI Features" : "✨ KI-Funktionen", + "All search trails cleared successfully" : "Alle Suchprotokolle erfolgreich gelöscht", + "Multiple search trail deletion not implemented yet" : "Löschung mehrerer Suchprotokolle noch nicht implementiert", + "No expired search trails found to clear" : "Keine abgelaufenen Suchprotokolle zum Löschen gefunden", + "Missing message" : "Nachricht fehlt", + "message content is required" : "Nachrichteninhalt ist erforderlich", + "Missing conversation or agentUuid" : "Konversation oder agentUuid fehlt", + "Access denied" : "Zugriff verweigert", + "Conversation not found" : "Konversation nicht gefunden", + "AI service not configured" : "KI-Dienst nicht konfiguriert", + "Failed to process message" : "Nachricht konnte nicht verarbeitet werden", + "Missing conversationId" : "conversationId fehlt", + "conversationId is required" : "conversationId ist erforderlich", + "Failed to fetch conversation history" : "Konversationsverlauf konnte nicht abgerufen werden", + "Failed to clear conversation" : "Konversation konnte nicht geleert werden", + "Invalid feedback type" : "Ungültiger Feedback-Typ", + "type must be \"positive\" or \"negative\"" : "Typ muss \"positive\" oder \"negative\" sein", + "Message not found" : "Nachricht nicht gefunden", + "Failed to save feedback" : "Feedback konnte nicht gespeichert werden", + "Failed to get chat statistics" : "Chat-Statistiken konnten nicht abgerufen werden", + "Not Found" : "Nicht gefunden", + "Engine not found" : "Engine nicht gefunden", + "Consumer not found" : "Consumer nicht gefunden", + "Backend parameter is required" : "Backend-Parameter ist erforderlich", + "Failed to get database information: %s" : "Datenbankinformationen konnten nicht abgerufen werden: %s", + "SOLR setup error: %s" : "SOLR-Einrichtungsfehler: %s", + "Reindex failed: %s" : "Neuindizierung fehlgeschlagen: %s", + "Anonymized" : "Anonymisiert", + "Entities detected" : "Entitäten erkannt", + "Extract Now" : "Jetzt extrahieren", + "Extracted at" : "Extrahiert am", + "Extraction" : "Extraktion", + "Failed to load extraction data" : "Extraktionsdaten konnten nicht geladen werden", + "Failed to load register data" : "Registerdaten konnten nicht geladen werden", + "Failed to retrieve extraction status." : "Extraktionsstatus konnte nicht abgerufen werden.", + "Failed to retrieve objects for file." : "Objekte für die Datei konnten nicht abgerufen werden.", + "No extraction data available for this file" : "Für diese Datei sind keine Extraktionsdaten verfügbar", + "No register objects reference this file" : "Keine Registerobjekte verweisen auf diese Datei", + "Not extracted" : "Nicht extrahiert", + "Register Objects" : "Registerobjekte", + "Risk level" : "Risikostufe", + "Text chunks" : "Text-Chunks", + "Unknown error" : "Unbekannter Fehler", + "Very high" : "Sehr hoch", + "{title} in {register} / {schema}" : "{title} in {register} / {schema}", + "Push Notifications" : "Push-Benachrichtigungen", + "Real-time push notification status via notify_push" : "Echtzeit-Push-Benachrichtigungsstatus über notify_push", + "Realtime push not available — the notify_push app is not installed" : "Echtzeit-Push nicht verfügbar — die App notify_push ist nicht installiert", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installieren Sie die App notify_push aus dem Nextcloud App Store, um Echtzeit-Aktualisierungen zu aktivieren.", + "Open Nextcloud App Store" : "Nextcloud App Store öffnen", + "notify_push is installed but not yet active" : "notify_push ist installiert, aber noch nicht aktiv", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push ist installiert, aber OpenRegister hat noch keinen erfolgreichen Push bestätigt. Lösen Sie ein Objektspeichern aus, um es zu aktivieren, oder überprüfen Sie Ihre notify_push-Konfiguration.", + "notify_push configuration guide" : "notify_push-Konfigurationsanleitung", + "Realtime push active" : "Echtzeit-Push aktiv", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Echtzeit-Push-Benachrichtigungen sind aktiv. Verbundene Clients erhalten sofortige Aktualisierungen, wenn Objekte erstellt, aktualisiert oder gelöscht werden.", + "Bookmarks" : "Bookmarks", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/de.json b/l10n/de.json new file mode 100644 index 0000000000..4854832226 --- /dev/null +++ b/l10n/de.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(no subject)": "(kein Betreff)", + "(no title)": "(kein Titel)", + "(unnamed)": "(unbenannt)", + "(untitled card)": "(unbenannte Karte)", + "Activities": "Aktivitäten", + "Add Endpoint": "Endpunkt hinzufügen", + "Add Source": "Quelle hinzufügen", + "Add card": "Karte hinzufügen", + "Add contact": "Kontakt hinzufügen", + "All clear": "Alles in Ordnung", + "Articles": "Artikel", + "Code": "Code", + "Compliance": "Compliance", + "Contacts": "Kontakte", + "Create Agent": "Agent erstellen", + "Create Application": "Anwendung erstellen", + "Create event": "Termin erstellen", + "Erasure complete": "Löschung abgeschlossen", + "Erasure preview": "Löschungsvorschau", + "Expand details": "Details einblenden", + "Save changes": "Änderungen speichern", + "📄 Object Serialization": "📄 Objektserialisierung", + "🔢 Vectorization Settings": "🔢 Vektorisierungseinstellungen", + "💰 View Selection (Cost Optimization)": "💰 Ansichtsauswahl (Kostenoptimierung)", + "AI Assistant": "KI-Assistent", + "AI Chat": "KI-Chat", + "API Key": "API-Schlüssel", + "API tokens saved successfully": "API-Token erfolgreich gespeichert", + "About ConfigSets": "Über ConfigSets", + "Action": "Aktion", + "Action Distribution": "Aktionsverteilung", + "Action:": "Aktion:", + "Actions": "Aktionen", + "Active": "Aktiv", + "Active Collections": "Aktive Collections", + "Active Filters": "Aktive Filter", + "Active Organisations": "Aktive Organisationen", + "Active filters:": "Aktive Filter:", + "Active organisation set successfully": "Aktive Organisation erfolgreich festgelegt", + "Activity Period": "Aktivitätszeitraum", + "Add Groups": "Gruppen hinzufügen", + "Add Schema": "Schema hinzufügen", + "Add schema titles, descriptions, and register information to provide richer context for search": "Fügen Sie Schematitel, Beschreibungen und Registerinformationen hinzu, um einen umfassenderen Kontext für die Suche bereitzustellen", + "Add to favorites": "Zu Favoriten hinzufügen", + "Added": "Hinzugefügt", + "Added to favorites": "Zu Favoriten hinzugefügt", + "Additional Information": "Zusätzliche Informationen", + "Additional feedback saved. Thank you!": "Zusätzliches Feedback gespeichert. Vielen Dank!", + "Admin": "Administration", + "Advanced": "Erweitert", + "Advanced Filters": "Erweiterte Filter", + "Advanced Options": "Erweiterte Optionen", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Erweiterte Filter sind bei Verwendung der Datenbankquelle nicht verfügbar. Wechseln Sie zu Auto oder SOLR-Index für Filteroptionen.", + "After": "Nach", + "Agent deleted successfully": "Agent erfolgreich gelöscht", + "Agents": "Agenten", + "All": "Alle", + "All Categories": "Alle Kategorien", + "All Collections": "Alle Collections", + "All Confidence Levels": "Alle Konfidenzstufen", + "All ConfigSets": "Alle ConfigSets", + "All Files": "Alle Dateien", + "All Levels": "Alle Stufen", + "All Types": "Alle Typen", + "All Webhooks": "Alle Webhooks", + "All actions": "Alle Aktionen", + "All audit trails cleared successfully": "Alle Audit-Trails erfolgreich gelöscht", + "All registers": "Alle Register", + "All schemas": "Alle Schemas", + "All searches": "Alle Suchen", + "All users": "Alle Benutzer", + "An error occurred while clearing audit trails": "Beim Löschen der Audit-Trails ist ein Fehler aufgetreten", + "An error occurred while deleting the audit trail": "Beim Löschen des Audit-Trails ist ein Fehler aufgetreten", + "An error occurred while deleting the view": "Beim Löschen der Ansicht ist ein Fehler aufgetreten", + "An error occurred while permanently deleting the objects": "Beim endgültigen Löschen der Objekte ist ein Fehler aufgetreten", + "Analysis completed:": "Analyse abgeschlossen:", + "Analysis steps:": "Analyseschritte:", + "Analytics": "Analysen", + "Analyze Objects": "Objekte analysieren", + "Analyze Properties": "Eigenschaften analysieren", + "Analyze existing properties for improvement opportunities": "Vorhandene Eigenschaften auf Verbesserungsmöglichkeiten analysieren", + "Analyzing...": "Analysiere …", + "Any favorites and sharing settings for this view": "Alle Favoriten- und Freigabeeinstellungen für diese Ansicht", + "Any user": "Beliebiger Benutzer", + "App store cache invalidated successfully": "App-Store-Cache erfolgreich invalidiert", + "Application deleted successfully": "Anwendung erfolgreich gelöscht", + "Applications": "Anwendungen", + "Apply Changes": "Änderungen anwenden", + "Archive": "Archivieren", + "Archive conversation": "Unterhaltung archivieren", + "Archived conversations are hidden from your active list": "Archivierte Unterhaltungen werden in Ihrer aktiven Liste ausgeblendet", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Sind Sie sicher, dass Sie alte Such-Trails bereinigen möchten? Dadurch werden Einträge gelöscht, die älter als 30 Tage sind.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Sind Sie sicher, dass Sie die ausgewählten Audit-Trails löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Sind Sie sicher, dass Sie die ausgewählten Such-Trails löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are you sure you want to delete this ConfigSet?": "Sind Sie sicher, dass Sie dieses ConfigSet löschen möchten?", + "Are you sure you want to permanently delete": "Sind Sie sicher, dass Sie endgültig löschen möchten", + "Ask a question...": "Stellen Sie eine Frage …", + "Ask questions about your data using natural language": "Stellen Sie Fragen zu Ihren Daten in natürlicher Sprache", + "Attempt": "Versuch", + "Audit Trail Changes": "Audit-Trail-Änderungen", + "Audit Trail Details": "Audit-Trail-Details", + "Audit Trail Management": "Audit-Trail-Verwaltung", + "Audit Trail Statistics": "Audit-Trail-Statistiken", + "Audit Trails": "Audit-Trails", + "Audit trail data copied to clipboard": "Audit-Trail-Daten in die Zwischenablage kopiert", + "Audit trail deleted successfully": "Audit-Trail erfolgreich gelöscht", + "Audit trail successfully deleted": "Audit-Trail erfolgreich gelöscht", + "Authentication": "Authentifizierung", + "Auto-commit disabled": "Auto-Commit deaktiviert", + "Auto-commit enabled": "Auto-Commit aktiviert", + "Auto-create Default Organisation": "Standardorganisation automatisch erstellen", + "Auto-retry failed vectorizations": "Fehlgeschlagene Vektorisierungen automatisch wiederholen", + "Automatically commit changes to SOLR index": "Änderungen automatisch im SOLR-Index festschreiben", + "Automatically create a default organisation if none exists when the app is initialized": "Automatisch eine Standardorganisation erstellen, wenn beim Initialisieren der App keine vorhanden ist", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automatisch Vektor-Embeddings aus Text-Chunks generieren, wenn Dateien hochgeladen und verarbeitet werden", + "Automatically generate vector embeddings when objects are created or updated": "Automatisch Vektor-Embeddings generieren, wenn Objekte erstellt oder aktualisiert werden", + "Automatically retry failed vectorization attempts (max 3 retries)": "Fehlgeschlagene Vektorisierungsversuche automatisch wiederholen (max. 3 Wiederholungen)", + "Available Filters": "Verfügbare Filter", + "Available Workflows": "Verfügbare Workflows", + "Avg Execution Time": "Durchschnittliche Ausführungszeit", + "Avg Members/Org": "Durchschn. Mitglieder/Org.", + "Avg Object Views/Session": "Durchschn. Objektaufrufe/Sitzung", + "Avg Response Time": "Durchschnittliche Antwortzeit", + "Avg Results/Search": "Durchschn. Ergebnisse/Suche", + "Avg Searches/Session": "Durchschn. Suchen/Sitzung", + "Back": "Zurück", + "Back to Entities": "Zurück zu Entitäten", + "Back to Registers": "Zurück zu Registern", + "Back to Webhooks": "Zurück zu Webhooks", + "Back to applications": "Zurück zu Anwendungen", + "Back to entities": "Zurück zu Entitäten", + "Backend updated successfully. Please reload the application.": "Backend erfolgreich aktualisiert. Bitte laden Sie die Anwendung neu.", + "Base URL (Optional)": "Basis-URL (optional)", + "Basic Information": "Grundlegende Informationen", + "Batch Size": "Stapelgröße", + "Batch extraction completed": "Stapelextraktion abgeschlossen", + "Before": "Vor", + "Before object vectorization can work:": "Bevor die Objektvektorisierung funktionieren kann:", + "Behavior Issue": "Verhaltensproblem", + "Blob storage has been retired. All objects now use magic tables.": "Blob-Speicher wurde stillgelegt. Alle Objekte verwenden jetzt Magic-Tabellen.", + "Branch": "Branch", + "Bulk delete operation completed successfully": "Massenlöschvorgang erfolgreich abgeschlossen", + "Bulk save operation completed successfully": "Massenspeichervorgang erfolgreich abgeschlossen", + "Business Data": "Geschäftsdaten", + "Cache cleared successfully": "Cache erfolgreich geleert", + "Calculate Sizes": "Größen berechnen", + "Cancel": "Abbrechen", + "Cannot delete: objects are still attached": "Löschen nicht möglich: Es sind noch Objekte angehängt", + "Category": "Kategorie", + "Change Type": "Änderungstyp", + "Changes": "Änderungen", + "Changes copied to clipboard": "Änderungen in die Zwischenablage kopiert", + "Chat Model": "Chat-Modell", + "Chat Provider": "Chat-Anbieter", + "Chat Provider (RAG)": "Chat-Anbieter (RAG)", + "Chat Settings": "Chat-Einstellungen", + "Chat provider connection successful!": "Verbindung zum Chat-Anbieter erfolgreich!", + "Chat settings": "Chat-Einstellungen", + "Choose a register": "Wählen Sie ein Register", + "Choose a schema": "Wählen Sie ein Schema", + "Choose how vector similarity calculations are performed for semantic search": "Wählen Sie, wie Vektorähnlichkeitsberechnungen für die semantische Suche durchgeführt werden", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Wählen Sie, welche Dateitypen in den Vektorisierungsprozess einbezogen werden sollen. Nur Dateien mit extrahiertem Text und Chunks werden verarbeitet.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Wählen Sie, welche Ansichten in den Vektorisierungsprozess einbezogen werden sollen. Lassen Sie das Feld leer, um alle Ansichten gemäß Ihrer Konfiguration zu verarbeiten.", + "Chunk": "Chunk", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Chunk-Löschung noch nicht implementiert. Verwenden Sie Chunk-basierte Endpunkte.", + "Chunks": "Chunks", + "Chunks to Vectorize:": "Zu vektorisierende Chunks:", + "Cleanup Old Trails": "Alte Trails bereinigen", + "Cleanup completed": "Bereinigung abgeschlossen", + "Clear All Embeddings": "Alle Embeddings löschen", + "Clear Entries": "Einträge löschen", + "Clear Filtered Audit Trails": "Gefilterte Audit-Trails löschen", + "Clear Filters": "Filter zurücksetzen", + "Clear Index": "Index löschen", + "Clear Selection": "Auswahl aufheben", + "Clear all": "Alle löschen", + "Clear all filters": "Alle Filter zurücksetzen", + "Clear date range": "Datumsbereich zurücksetzen", + "Clearing...": "Wird gelöscht …", + "Close": "Schließen", + "Collection Management": "Collection-Verwaltung", + "Collection Name": "Collection-Name", + "Collection assignments updated successfully": "Collection-Zuweisungen erfolgreich aktualisiert", + "Collection cleared successfully": "Collection erfolgreich geleert", + "Collection copied successfully": "Collection erfolgreich kopiert", + "Collection created successfully": "Collection erfolgreich erstellt", + "Collection deleted successfully": "Collection erfolgreich gelöscht", + "Collection used to store and index file metadata and content": "Collection zum Speichern und Indizieren von Dateimetadaten und -inhalten", + "Collection used to store and index object data": "Collection zum Speichern und Indizieren von Objektdaten", + "Collections:": "Collections:", + "Commit Message": "Commit-Nachricht", + "Commit Within (ms)": "Commit innerhalb von (ms)", + "Commit message": "Commit-Nachricht", + "Compare current schema with real object data": "Aktuelles Schema mit echten Objektdaten vergleichen", + "Completed": "Abgeschlossen", + "Complex": "Komplex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Komplexe Abfragen: Erweiterte Suchen mit mehreren Filtern, Operatoren und komplexen Parameterkombinationen", + "Confidence Level": "Konfidenzstufe", + "Confidence Score": "Konfidenzwert", + "Config must be provided as an object": "Die Konfiguration muss als Objekt bereitgestellt werden", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet-Verwaltung", + "ConfigSet Name": "ConfigSet-Name", + "ConfigSet created successfully": "ConfigSet erfolgreich erstellt", + "ConfigSet deleted successfully": "ConfigSet erfolgreich gelöscht", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets definieren das Schema und die Konfiguration für Ihre SOLR-Collections. Sie enthalten Felddefinitionen, Analyzer und andere Sucheinstellungen.", + "Configuration": "Konfiguration", + "Configuration Data": "Konfigurationsdaten", + "Configuration saved": "Konfiguration gespeichert", + "Configurations": "Konfigurationen", + "Configure Facets": "Facetten konfigurieren", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurieren Sie Anbieter für große Sprachmodelle (LLM) für KI-gestützte Funktionen wie semantische Suche, Embeddings und Chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurieren Sie grundlegende Verbindungseinstellungen für Ihren SOLR-Server, einschließlich Authentifizierung und Netzwerkoptionen. Verwenden Sie die separaten Dialoge für die ConfigSet- und Collection-Verwaltung, um Cores und Collections zu verwalten.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurieren Sie, wie Datenbankobjekte für die semantische Suche in Vektor-Embeddings umgewandelt werden. Objekte werden direkt vektorisiert, ohne dass eine Textextraktion erforderlich ist.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurieren Sie, wie Objekte vor der Vektorisierung in Text umgewandelt werden. Diese Einstellungen beeinflussen Suchqualität und Kontext.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurieren Sie Parameter für die Dateivektorisierung. Dieser Prozess generiert Vektor-Embeddings für alle extrahierten Datei-Chunks.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurieren Sie Parameter für die Objektvektorisierung. Dieser Prozess generiert Vektor-Embeddings für alle Objekte, die Ihren Ansichtsfiltern entsprechen.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurieren Sie, welche Datentypen durchsucht und wie viele Quellen abgerufen werden sollen", + "Connection Failed": "Verbindung fehlgeschlagen", + "Connection Settings": "Verbindungseinstellungen", + "Connection Successful!": "Verbindung erfolgreich!", + "Connection failed": "Verbindung fehlgeschlagen", + "Connection protocol": "Verbindungsprotokoll", + "Connection successful": "Verbindung erfolgreich", + "Connection timeout in seconds": "Verbindungs-Timeout in Sekunden", + "Constant delay between retries (5 minutes)": "Konstante Verzögerung zwischen Wiederholungen (5 Minuten)", + "Constraint Issue": "Einschränkungsproblem", + "Content Filters": "Inhaltsfilter", + "Continue to Properties": "Weiter zu Eigenschaften", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Steuern Sie, welche Objektansichten vektorisiert werden sollen, um API-Kosten zu senken. Vektorisieren Sie nur Ansichten, die von der semantischen Suche profitieren.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Steuern Sie, welche Ansichten und Werkzeuge die KI in dieser Unterhaltung verwenden kann. Standardmäßig sind alle Agentenfunktionen aktiviert.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Steuert die Zufälligkeit (0 = deterministisch, 2 = sehr kreativ)", + "Conversation ID is required": "Die Unterhaltungs-ID ist erforderlich", + "Conversation archived": "Unterhaltung archiviert", + "Conversation archived successfully": "Unterhaltung erfolgreich archiviert", + "Conversation cleared successfully": "Unterhaltung erfolgreich geleert", + "Conversation deleted": "Unterhaltung gelöscht", + "Conversation permanently deleted": "Unterhaltung endgültig gelöscht", + "Conversation renamed": "Unterhaltung umbenannt", + "Conversation restored": "Unterhaltung wiederhergestellt", + "Conversation title": "Unterhaltungstitel", + "Copied!": "Kopiert!", + "Copy": "Kopieren", + "Copy Changes": "Änderungen kopieren", + "Copy Collection": "Collection kopieren", + "Copy Data": "Daten kopieren", + "Copy Full Data": "Vollständige Daten kopieren", + "Copying...": "Wird kopiert …", + "Count": "Anzahl", + "Counting objects...": "Objekte werden gezählt …", + "Costs": "Kosten", + "Create": "Erstellen", + "Create Collection": "Collection erstellen", + "Create ConfigSet": "ConfigSet erstellen", + "Create New Collection": "Neue Collection erstellen", + "Create New ConfigSet": "Neues ConfigSet erstellen", + "Create Operations": "Erstellungsvorgänge", + "Create Webhook": "Webhook erstellen", + "Create a copy of collection:": "Eine Kopie der Collection erstellen:", + "Create a new ConfigSet based on the _default template": "Ein neues ConfigSet basierend auf der Vorlage _default erstellen", + "Create a new SOLR collection from an existing ConfigSet": "Eine neue SOLR-Collection aus einem vorhandenen ConfigSet erstellen", + "Create your first AI agent to get started.": "Erstellen Sie Ihren ersten KI-Agenten, um loszulegen.", + "Created": "Erstellt", + "Created:": "Erstellt:", + "Creating...": "Wird erstellt …", + "Current Filters": "Aktuelle Filter", + "Current Type:": "Aktueller Typ:", + "Custom API endpoint if using a different region": "Benutzerdefinierter API-Endpunkt bei Verwendung einer anderen Region", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Benutzerdefinierte HTTP-Header (einer pro Zeile, Format: Header-Name: Wert)", + "Daily": "Täglich", + "Dashboard": "Dashboard", + "Data Source": "Datenquelle", + "Data sources": "Datenquellen", + "Data type does not match observed values": "Der Datentyp stimmt nicht mit den beobachteten Werten überein", + "Database URL": "Datenbank-URL", + "Database information refreshed": "Datenbankinformationen aktualisiert", + "Date Range": "Datumsbereich", + "Default": "Standard", + "Default Organisation": "Standardorganisation", + "Default: 5": "Standard: 5", + "Delays double with each attempt (2, 4, 8 minutes...)": "Verzögerungen verdoppeln sich mit jedem Versuch (2, 4, 8 Minuten …)", + "Delays increase linearly (5, 10, 15 minutes...)": "Verzögerungen steigen linear an (5, 10, 15 Minuten …)", + "Delete": "Löschen", + "Delete Application": "Anwendung löschen", + "Delete Audit Trail": "Audit-Trail löschen", + "Delete Collection": "Collection löschen", + "Delete ConfigSet": "ConfigSet löschen", + "Delete Objects": "Objekte löschen", + "Delete Operations": "Löschvorgänge", + "Delete View": "Ansicht löschen", + "Delete all objects in this schema": "Alle Objekte in diesem Schema löschen", + "Delete permanently": "Endgültig löschen", + "Delete this conversation?": "Diese Unterhaltung löschen?", + "Delete view": "Ansicht löschen", + "Deleted": "Gelöscht", + "Deleted By": "Gelöscht von", + "Deleted Date": "Löschdatum", + "Deleted Items Management": "Verwaltung gelöschter Elemente", + "Deleted This Week": "Diese Woche gelöscht", + "Deleted Today": "Heute gelöscht", + "Deleted:": "Gelöscht:", + "Deleting...": "Wird gelöscht …", + "Deletion Date Range": "Löschdatumsbereich", + "Deletion Statistics": "Löschstatistiken", + "Deprecated": "Veraltet", + "Description": "Beschreibung", + "Description:": "Beschreibung:", + "Detect data types and patterns": "Datentypen und Muster erkennen", + "Detected At": "Erkannt am", + "Detected Format:": "Erkanntes Format:", + "Detected Issues:": "Erkannte Probleme:", + "Disable": "Deaktivieren", + "Disabled": "Deaktiviert", + "Discover Files": "Dateien entdecken", + "Discovered Properties": "Entdeckte Eigenschaften", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Möchten Sie alle gefilterten Audit-Trail-Einträge endgültig löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Möchten Sie diesen Audit-Trail-Eintrag endgültig löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "Documents": "Dokumente", + "Download API Spec": "API-Spezifikation herunterladen", + "Edit": "Bearbeiten", + "Edit Register": "Register bearbeiten", + "Edit Schema": "Schema bearbeiten", + "Edit View": "Ansicht bearbeiten", + "Edit Webhook": "Webhook bearbeiten", + "Edit view": "Ansicht bearbeiten", + "Edit view details": "Ansichtsdetails bearbeiten", + "Effectiveness": "Wirksamkeit", + "Email": "E-Mail", + "Emails": "E-Mails", + "Embedding Model": "Embedding-Modell", + "Embedding Provider": "Embedding-Anbieter", + "Embedding generated successfully": "Embedding erfolgreich generiert", + "Embedding provider connection successful!": "Verbindung zum Embedding-Anbieter erfolgreich!", + "Enable": "Aktivieren", + "Enable auto-creation": "Automatische Erstellung aktivieren", + "Enable automatic file vectorization": "Automatische Dateivektorisierung aktivieren", + "Enable automatic object vectorization": "Automatische Objektvektorisierung aktivieren", + "Enable automatic synchronization": "Automatische Synchronisierung aktivieren", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Detailliertes Logging für SOLR-Vorgänge aktivieren (empfohlen für die Fehlersuche)", + "Enable faceting": "Facettierung aktivieren", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "LLM-Funktionen aktivieren oder deaktivieren. Konfigurieren Sie Anbieter und Modelle über die Schaltfläche „LLM-Konfiguration“ oben.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "SOLR-Suchintegration aktivieren oder deaktivieren. Konfigurieren Sie die Verbindungseinstellungen über die Schaltfläche „Verbindungseinstellungen“ oben.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "n8n-Workflow-Integration aktivieren oder deaktivieren. Konfigurieren Sie die Verbindungseinstellungen unten.", + "Enable or disable this webhook": "Diesen Webhook aktivieren oder deaktivieren", + "Enable vectorization for all existing and future views (may increase costs)": "Vektorisierung für alle vorhandenen und zukünftigen Ansichten aktivieren (kann die Kosten erhöhen)", + "Enabled": "Aktiviert", + "Endpoints": "Endpunkte", + "Enter ConfigSet name": "ConfigSet-Namen eingeben", + "Enter collection name": "Collection-Namen eingeben", + "Enter description (optional)...": "Beschreibung eingeben (optional) …", + "Enter new collection name": "Neuen Collection-Namen eingeben", + "Enter object ID": "Objekt-ID eingeben", + "Enter search term": "Suchbegriff eingeben", + "Enter view name...": "Ansichtsnamen eingeben …", + "Enter webhook name": "Webhook-Namen eingeben", + "Entities": "Entitäten", + "Entity Information": "Entitätsinformationen", + "Entity deleted successfully": "Entität erfolgreich gelöscht", + "Entity not found": "Entität nicht gefunden", + "Entries to be deleted:": "Zu löschende Einträge:", + "Enum Issue": "Enum-Problem", + "Enum constraint is missing": "Die Enum-Einschränkung fehlt", + "Error": "Fehler", + "Error Details": "Fehlerdetails", + "Error Information": "Fehlerinformationen", + "Error Message": "Fehlermeldung", + "Error loading application": "Fehler beim Laden der Anwendung", + "Error loading audit trails": "Fehler beim Laden der Audit-Trails", + "Error loading entity": "Fehler beim Laden der Entität", + "Error loading search trails": "Fehler beim Laden der Such-Trails", + "Estimated Batches:": "Geschätzte Stapel:", + "Estimated Cost:": "Geschätzte Kosten:", + "Estimated Duration:": "Geschätzte Dauer:", + "Event": "Ereignis", + "Event Property for Payload": "Ereigniseigenschaft für Payload", + "Events": "Ereignisse", + "Example Value": "Beispielwert", + "Exclusive Maximum": "Exklusives Maximum", + "Exclusive Minimum": "Exklusives Minimum", + "Execution Mode": "Ausführungsmodus", + "Execution Time": "Ausführungszeit", + "Execution Time Range": "Ausführungszeitbereich", + "Existing Improvements": "Vorhandene Verbesserungen", + "Exponential": "Exponentiell", + "Export": "Exportieren", + "Export completed successfully": "Export erfolgreich abgeschlossen", + "Export, view, or delete audit trails": "Audit-Trails exportieren, anzeigen oder löschen", + "Extend Schema": "Schema erweitern", + "Extended": "Erweitert", + "Extensions": "Erweiterungen", + "Extract Pending Files": "Ausstehende Dateien extrahieren", + "Extract properties from each object": "Eigenschaften aus jedem Objekt extrahieren", + "Extracted At": "Extrahiert am", + "Extraction Status": "Extraktionsstatus", + "Facet configuration updated successfully": "Facettenkonfiguration erfolgreich aktualisiert", + "Facets discovered and configured successfully": "Facetten erfolgreich entdeckt und konfiguriert", + "Facets discovered successfully": "Facetten erfolgreich entdeckt", + "Failed": "Fehlgeschlagen", + "Failed to analyze schema properties": "Schemaeigenschaften konnten nicht analysiert werden", + "Failed to archive conversation": "Unterhaltung konnte nicht archiviert werden", + "Failed to calculate sizes": "Größen konnten nicht berechnet werden", + "Failed to clear collection": "Collection konnte nicht geleert werden", + "Failed to copy changes": "Änderungen konnten nicht kopiert werden", + "Failed to copy data": "Daten konnten nicht kopiert werden", + "Failed to copy data to clipboard": "Daten konnten nicht in die Zwischenablage kopiert werden", + "Failed to create conversation": "Unterhaltung konnte nicht erstellt werden", + "Failed to create or find project": "Projekt konnte nicht erstellt oder gefunden werden", + "Failed to delete collection": "Collection konnte nicht gelöscht werden", + "Failed to delete conversation": "Unterhaltung konnte nicht gelöscht werden", + "Failed to delete webhook": "Webhook konnte nicht gelöscht werden", + "Failed to download API specification": "API-Spezifikation konnte nicht heruntergeladen werden", + "Failed to get SOLR field configuration": "SOLR-Feldkonfiguration konnte nicht abgerufen werden", + "Failed to get entity categories": "Entitätskategorien konnten nicht abgerufen werden", + "Failed to get entity statistics": "Entitätsstatistiken konnten nicht abgerufen werden", + "Failed to get entity types": "Entitätstypen konnten nicht abgerufen werden", + "Failed to load LLM configuration": "LLM-Konfiguration konnte nicht geladen werden", + "Failed to load Nextcloud groups": "Nextcloud-Gruppen konnten nicht geladen werden", + "Failed to load conversation": "Unterhaltung konnte nicht geladen werden", + "Failed to load entities": "Entitäten konnten nicht geladen werden", + "Failed to load entity": "Entität konnte nicht geladen werden", + "Failed to load files": "Dateien konnten nicht geladen werden", + "Failed to load organisations": "Organisationen konnten nicht geladen werden", + "Failed to load templates": "Vorlagen konnten nicht geladen werden", + "Failed to load webhooks": "Webhooks konnten nicht geladen werden", + "Failed to load workflows": "Workflows konnten nicht geladen werden", + "Failed to refresh database information": "Datenbankinformationen konnten nicht aktualisiert werden", + "Failed to reindex collection": "Collection konnte nicht neu indiziert werden", + "Failed to rename conversation": "Unterhaltung konnte nicht umbenannt werden", + "Failed to restore conversation": "Unterhaltung konnte nicht wiederhergestellt werden", + "Failed to retry extraction": "Extraktion konnte nicht wiederholt werden", + "Failed to retry webhook": "Webhook konnte nicht wiederholt werden", + "Failed to save GitHub token": "GitHub-Token konnte nicht gespeichert werden", + "Failed to save GitLab URL": "GitLab-URL konnte nicht gespeichert werden", + "Failed to save GitLab token": "GitLab-Token konnte nicht gespeichert werden", + "Failed to save additional feedback": "Zusätzliches Feedback konnte nicht gespeichert werden", + "Failed to save n8n configuration": "n8n-Konfiguration konnte nicht gespeichert werden", + "Failed to save roles": "Rollen konnten nicht gespeichert werden", + "Failed to save settings": "Einstellungen konnten nicht gespeichert werden", + "Failed to save webhook": "Webhook konnte nicht gespeichert werden", + "Failed to send feedback": "Feedback konnte nicht gesendet werden", + "Failed to test webhook": "Webhook konnte nicht getestet werden", + "Failed to update favorite status": "Favoritenstatus konnte nicht aktualisiert werden", + "Failed to update schema properties": "Schemaeigenschaften konnten nicht aktualisiert werden", + "Failed to update view": "Ansicht konnte nicht aktualisiert werden", + "Failed to update webhook": "Webhook konnte nicht aktualisiert werden", + "Feedback recorded": "Feedback aufgezeichnet", + "Fewer sources (1-3):": "Weniger Quellen (1–3):", + "Field": "Feld", + "File": "Datei", + "File Chunk Prediction": "Datei-Chunk-Vorhersage", + "File Collection": "Datei-Collection", + "File Management": "Dateiverwaltung", + "File Name": "Dateiname", + "File Path": "Dateipfad", + "File Type Selection": "Dateitypauswahl", + "File Vectorization": "Dateivektorisierung", + "File Warmup": "Datei-Warmup", + "File actions menu": "Dateiaktionsmenü", + "File anonymized successfully": "Datei erfolgreich anonymisiert", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Die Datei-Chunk-Vektorisierung ist noch nicht implementiert. Die Chunks sind bereit und gespeichert, aber der Vektorisierungsdienst befindet sich in der Entwicklung.", + "File collection not configured": "Datei-Collection nicht konfiguriert", + "File discovery completed": "Dateierkennung abgeschlossen", + "File extraction completed": "Dateiextraktion abgeschlossen", + "File extraction queued": "Dateiextraktion in Warteschlange eingereiht", + "File indexed successfully": "Datei erfolgreich indiziert", + "File is already anonymized": "Die Datei ist bereits anonymisiert", + "File not found": "Datei nicht gefunden", + "File settings updated successfully": "Dateieinstellungen erfolgreich aktualisiert", + "File sources": "Dateiquellen", + "File vectorization configuration saved successfully": "Dateivektorisierungskonfiguration erfolgreich gespeichert", + "File vectorization started. Check the statistics section for progress.": "Dateivektorisierung gestartet. Überprüfen Sie den Statistikbereich für den Fortschritt.", + "File warmup completed": "Datei-Warmup abgeschlossen", + "Files": "Dateien", + "Files with Completed Extraction:": "Dateien mit abgeschlossener Extraktion:", + "Files → fileCollection, Objects → objectCollection": "Dateien → fileCollection, Objekte → objectCollection", + "Filter Audit Trails": "Audit-Trails filtern", + "Filter Deleted Items": "Gelöschte Elemente filtern", + "Filter Objects": "Objekte filtern", + "Filter Properties": "Eigenschaften filtern", + "Filter Search Trails": "Such-Trails filtern", + "Filter Statistics": "Statistiken filtern", + "Filter and analyze search trail entries": "Such-Trail-Einträge filtern und analysieren", + "Filter and manage audit trail entries": "Audit-Trail-Einträge filtern und verwalten", + "Filter and manage soft deleted items": "Vorläufig gelöschte Elemente filtern und verwalten", + "Filter by object ID": "Nach Objekt-ID filtern", + "Filter by search term": "Nach Suchbegriff filtern", + "Filter by webhook": "Nach Webhook filtern", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filterdaten automatisch geladen. Verwenden Sie die Filter unten, um Ihre Suche zu verfeinern.", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Webhook-Auslöser nach Payload-Eigenschaften filtern (eine pro Zeile, Format: Schlüssel: Wert)", + "Filtered": "Gefiltert", + "Filters": "Filter", + "Fireworks AI Chat Settings": "Fireworks-AI-Chat-Einstellungen", + "Fireworks AI Embedding Configuration": "Fireworks-AI-Embedding-Konfiguration", + "First": "Erste", + "Fixed": "Behoben", + "For chat and retrieval-augmented generation": "Für Chat und Retrieval-Augmented Generation", + "For vector embeddings and semantic search": "Für Vektor-Embeddings und semantische Suche", + "Format": "Format", + "Format Issue": "Formatproblem", + "Format constraint is missing": "Die Formateinschränkung fehlt", + "From Date": "Von Datum", + "From date": "Von Datum", + "Full data copied to clipboard": "Vollständige Daten in die Zwischenablage kopiert", + "General Issue": "Allgemeines Problem", + "Generate recommendations and confidence scores": "Empfehlungen und Konfidenzwerte generieren", + "Generate vectors immediately when new objects are created": "Vektoren sofort generieren, wenn neue Objekte erstellt werden", + "GitHub token is valid": "GitHub-Token ist gültig", + "GitHub token removed successfully": "GitHub-Token erfolgreich entfernt", + "GitHub token saved successfully": "GitHub-Token erfolgreich gespeichert", + "GitLab URL saved successfully": "GitLab-URL erfolgreich gespeichert", + "GitLab token is valid": "GitLab-Token ist gültig", + "GitLab token saved successfully": "GitLab-Token erfolgreich gespeichert", + "HTTP Method": "HTTP-Methode", + "HTTP method used to send webhook requests": "HTTP-Methode zum Senden von Webhook-Anfragen", + "Headers": "Header", + "Health": "Zustand", + "Heartbeat successful - connection kept alive": "Heartbeat erfolgreich – Verbindung aufrechterhalten", + "Helpful": "Hilfreich", + "Hide Filters": "Filter ausblenden", + "Hide in forms": "In Formularen ausblenden", + "Hide in list view": "In Listenansicht ausblenden", + "High": "Hoch", + "High Confidence": "Hohe Konfidenz", + "Host": "Host", + "Hourly": "Stündlich", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Wie tief verschachtelte Objekteigenschaften durchlaufen werden (1–20). Höhere Werte erfassen mehr Details, erhöhen aber die Vektorgröße.", + "How often to check for updates (1-168 hours)": "Wie oft nach Updates gesucht wird (1–168 Stunden)", + "How to handle retries for failed webhook deliveries": "Wie Wiederholungen für fehlgeschlagene Webhook-Zustellungen behandelt werden", + "ID": "ID", + "ID:": "ID:", + "IP rate limits cleared successfully": "IP-Ratenbegrenzungen erfolgreich zurückgesetzt", + "Identify properties not in the schema": "Eigenschaften identifizieren, die nicht im Schema enthalten sind", + "Immutable": "Unveränderlich", + "Import": "Importieren", + "Import successful": "Import erfolgreich", + "Improved Property": "Verbesserte Eigenschaft", + "In use": "In Verwendung", + "Inactive": "Inaktiv", + "Include IDs and names of related objects for better contextual search": "IDs und Namen verwandter Objekte für eine bessere kontextbezogene Suche einbeziehen", + "Include related object references": "Verwandte Objektreferenzen einbeziehen", + "Include schema and register metadata": "Schema- und Registermetadaten einbeziehen", + "Initialization failed": "Initialisierung fehlgeschlagen", + "Initialize Project": "Projekt initialisieren", + "Inspect Fields": "Felder prüfen", + "Inspect Index": "Index prüfen", + "Invalid": "Ungültig", + "Invalid batch size. Must be between 1 and 5000": "Ungültige Stapelgröße. Muss zwischen 1 und 5000 liegen", + "Invalid field name provided": "Ungültiger Feldname angegeben", + "Invalid maxObjects. Must be 0 (all) or positive number": "Ungültiger Wert für maxObjects. Muss 0 (alle) oder eine positive Zahl sein", + "Invalid:": "Ungültig:", + "Issue": "Problem", + "Items per page:": "Elemente pro Seite:", + "Just now": "Gerade eben", + "LLM Configuration": "LLM-Konfiguration", + "LLM actions menu": "LLM-Aktionsmenü", + "LLM configuration saved successfully": "LLM-Konfiguration erfolgreich gespeichert", + "LLM features disabled": "LLM-Funktionen deaktiviert", + "LLM features enabled": "LLM-Funktionen aktiviert", + "LLM must be enabled with an embedding provider configured": "LLM muss aktiviert sein und ein Embedding-Anbieter konfiguriert sein", + "LLM settings updated successfully": "LLM-Einstellungen erfolgreich aktualisiert", + "Last": "Letzte", + "Last Triggered": "Zuletzt ausgelöst", + "Length Range:": "Längenbereich:", + "Linear": "Linear", + "Load Advanced Filters": "Erweiterte Filter laden", + "Load advanced filters with live data from your search index": "Erweiterte Filter mit Live-Daten aus Ihrem Suchindex laden", + "Load view": "Ansicht laden", + "Loading ConfigSets...": "ConfigSets werden geladen …", + "Loading advanced filters...": "Erweiterte Filter werden geladen …", + "Loading agents...": "Agenten werden geladen …", + "Loading application details...": "Anwendungsdetails werden geladen …", + "Loading applications...": "Anwendungen werden geladen …", + "Loading archived conversations...": "Archivierte Unterhaltungen werden geladen …", + "Loading audit trails...": "Audit-Trails werden geladen …", + "Loading collections...": "Collections werden geladen …", + "Loading configurations...": "Konfigurationen werden geladen …", + "Loading conversation...": "Unterhaltung wird geladen …", + "Loading conversations...": "Unterhaltungen werden geladen …", + "Loading deleted items...": "Gelöschte Elemente werden geladen …", + "Loading events...": "Ereignisse werden geladen …", + "Loading filter data...": "Filterdaten werden geladen …", + "Loading groups...": "Gruppen werden geladen …", + "Loading log details...": "Protokolldetails werden geladen …", + "Loading registers...": "Register werden geladen …", + "Loading schemas...": "Schemas werden geladen …", + "Loading search trails...": "Such-Trails werden geladen …", + "Loading sources...": "Quellen werden geladen …", + "Loading statistics...": "Statistiken werden geladen …", + "Loading users...": "Benutzer werden geladen …", + "Loading views...": "Ansichten werden geladen …", + "Loading...": "Wird geladen …", + "Local": "Lokal", + "Local Version": "Lokale Version", + "Locked": "Gesperrt", + "Locked:": "Gesperrt:", + "Login successful": "Anmeldung erfolgreich", + "Logs": "Protokolle", + "Low": "Niedrig", + "Low Confidence": "Niedrige Konfidenz", + "Lucene Version:": "Lucene-Version:", + "Magic table synchronized successfully": "Magic-Tabelle erfolgreich synchronisiert", + "Manage Organisation Roles": "Organisationsrollen verwalten", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "SOLR-Collections (Datenspeicher) verwalten und sie Objekten und Dateien zuweisen.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "SOLR-ConfigSets (Konfigurationsvorlagen) für Ihre Collections verwalten.", + "Manage and configure agents for automated tasks": "Agenten für automatisierte Aufgaben verwalten und konfigurieren", + "Manage and monitor file text extraction status": "Status der Textextraktion von Dateien verwalten und überwachen", + "Manage and restore soft deleted items from your registers": "Vorläufig gelöschte Elemente aus Ihren Registern verwalten und wiederherstellen", + "Manage and view detected entities from files and objects": "Erkannte Entitäten aus Dateien und Objekten verwalten und anzeigen", + "Manage document templates and themes": "Dokumentvorlagen und Themes verwalten", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "n8n-Workflows für die OpenRegister-Automatisierung verwalten. Workflows werden im konfigurierten Projekt gespeichert.", + "Manage webhooks for event-driven integrations": "Webhooks für ereignisgesteuerte Integrationen verwalten", + "Manage your applications and modules": "Ihre Anwendungen und Module verwalten", + "Manage your data registers and their configurations": "Ihre Datenregister und deren Konfigurationen verwalten", + "Manage your data schemas and their properties": "Ihre Datenschemas und deren Eigenschaften verwalten", + "Manage your data sources and their configurations": "Ihre Datenquellen und deren Konfigurationen verwalten", + "Manage your saved search configurations": "Ihre gespeicherten Suchkonfigurationen verwalten", + "Manage your system configurations and settings": "Ihre Systemkonfigurationen und -einstellungen verwalten", + "Managed": "Verwaltet", + "Max Files (0 = all)": "Max. Dateien (0 = alle)", + "Max Length": "Max. Länge", + "Max Length:": "Max. Länge:", + "Max Objects (0 = all)": "Max. Objekte (0 = alle)", + "Max Retries": "Max. Wiederholungen", + "Max Shards/Node": "Max. Shards/Knoten", + "Max execution time (ms)": "Max. Ausführungszeit (ms)", + "Max ms": "Max. ms", + "Max result count": "Max. Ergebnisanzahl", + "Max results": "Max. Ergebnisse", + "Maximum": "Maximum", + "Maximum Nesting Depth": "Maximale Verschachtelungstiefe", + "Maximum length constraint is missing": "Die maximale Längeneinschränkung fehlt", + "Maximum length is too restrictive": "Die maximale Länge ist zu restriktiv", + "Maximum number of files to process. Set to 0 to process all files.": "Maximale Anzahl der zu verarbeitenden Dateien. Auf 0 setzen, um alle Dateien zu verarbeiten.", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maximale Anzahl der zu verarbeitenden Objekte. Auf 0 setzen, um alle Objekte zu verarbeiten.", + "Maximum number of retry attempts for failed deliveries": "Maximale Anzahl der Wiederholungsversuche für fehlgeschlagene Zustellungen", + "Maximum time to wait before committing changes": "Maximale Wartezeit vor dem Festschreiben von Änderungen", + "Maximum tokens to generate": "Maximale Anzahl der zu generierenden Token", + "Maximum value constraint is missing": "Die Einschränkung für den Maximalwert fehlt", + "Maximum value is too restrictive": "Der Maximalwert ist zu restriktiv", + "Medium": "Mittel", + "Medium Confidence": "Mittlere Konfidenz", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Mittlere Abfragen: Suchen mit etwas Filterung oder mehreren Parametern (z. B. Datumsbereiche, bestimmte Register/Schemas)", + "Members of selected groups can access this view": "Mitglieder ausgewählter Gruppen können auf diese Ansicht zugreifen", + "Members:": "Mitglieder:", + "Memory prediction calculated successfully": "Speichervorhersage erfolgreich berechnet", + "Message content is required": "Der Nachrichteninhalt ist erforderlich", + "Message does not belong to this conversation": "Die Nachricht gehört nicht zu dieser Unterhaltung", + "Metadata Filters": "Metadatenfilter", + "Method": "Methode", + "Min Length": "Min. Länge", + "Min execution time (ms)": "Min. Ausführungszeit (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. Ergebnisanzahl", + "Min results": "Min. Ergebnisse", + "Minimum": "Minimum", + "Minimum value constraint is missing": "Die Einschränkung für den Minimalwert fehlt", + "Minimum value is too restrictive": "Der Minimalwert ist zu restriktiv", + "Missing object fields created successfully": "Fehlende Objektfelder erfolgreich erstellt", + "Mode:": "Modus:", + "Model": "Modell", + "Model Name": "Modellname", + "Modified": "Geändert", + "Monthly": "Monatlich", + "More sources (10-20):": "Mehr Quellen (10–20):", + "Most Active Objects": "Aktivste Objekte", + "Multiple Of": "Vielfaches von", + "Name": "Name", + "Names cache warmup completed": "Namen-Cache-Warmup abgeschlossen", + "New Collection Name": "Neuer Collection-Name", + "New Conversation": "Neue Unterhaltung", + "New Properties": "Neue Eigenschaften", + "New Property": "Neue Eigenschaft", + "New Value": "Neuer Wert", + "New users without specific organisation membership will be automatically added to this organisation": "Neue Benutzer ohne spezifische Organisationsmitgliedschaft werden automatisch zu dieser Organisation hinzugefügt", + "Next": "Weiter", + "No": "Nein", + "No ConfigSets found": "Keine ConfigSets gefunden", + "No GitHub token configured": "Kein GitHub-Token konfiguriert", + "No GitHub token provided": "Kein GitHub-Token angegeben", + "No GitLab token provided": "Kein GitLab-Token angegeben", + "No Organisation": "Keine Organisation", + "No active filters": "Keine aktiven Filter", + "No activity data available": "Keine Aktivitätsdaten verfügbar", + "No agents available": "Keine Agenten verfügbar", + "No agents found": "Keine Agenten gefunden", + "No app store cache exists yet": "Es existiert noch kein App-Store-Cache", + "No applications are available.": "Es sind keine Anwendungen verfügbar.", + "No applications found": "Keine Anwendungen gefunden", + "No archived conversations": "Keine archivierten Unterhaltungen", + "No audit trail entries found": "Keine Audit-Trail-Einträge gefunden", + "No changes recorded": "Keine Änderungen aufgezeichnet", + "No chunks to vectorize": "Keine Chunks zum Vektorisieren", + "No collections found": "Keine Collections gefunden", + "No configuration data": "Keine Konfigurationsdaten", + "No configurations are available.": "Es sind keine Konfigurationen verfügbar.", + "No configurations found": "Keine Konfigurationen gefunden", + "No configurations found for this application.": "Keine Konfigurationen für diese Anwendung gefunden.", + "No conversations yet. Create a new one to get started!": "Noch keine Unterhaltungen. Erstellen Sie eine neue, um loszulegen!", + "No data available": "Keine Daten verfügbar", + "No data available for chart": "Keine Daten für das Diagramm verfügbar", + "No deleted items found": "Keine gelöschten Elemente gefunden", + "No deletion data available": "Keine Löschdaten verfügbar", + "No description found": "Keine Beschreibung gefunden", + "No description provided": "Keine Beschreibung angegeben", + "No entities detected in this file. Run text extraction first.": "In dieser Datei wurden keine Entitäten erkannt. Führen Sie zuerst die Textextraktion durch.", + "No entities found": "Keine Entitäten gefunden", + "No entities have been detected yet": "Es wurden noch keine Entitäten erkannt", + "No events found": "Keine Ereignisse gefunden", + "No expired audit trails found to clear": "Keine abgelaufenen Audit-Trails zum Löschen gefunden", + "No facetable fields available. Select a register and schema to see available filters.": "Keine facettierbaren Felder verfügbar. Wählen Sie ein Register und ein Schema, um verfügbare Filter zu sehen.", + "No files found": "Keine Dateien gefunden", + "No files have been extracted yet": "Es wurden noch keine Dateien extrahiert", + "No files to process": "Keine Dateien zu verarbeiten", + "No files to reindex": "Keine Dateien zum Neuindizieren", + "No filters are currently active. This will delete ALL audit trail entries!": "Derzeit sind keine Filter aktiv. Dadurch werden ALLE Audit-Trail-Einträge gelöscht!", + "No log entries found": "Keine Protokolleinträge gefunden", + "No logs are available for this configuration.": "Für diese Konfiguration sind keine Protokolle verfügbar.", + "No logs are available for this source.": "Für diese Quelle sind keine Protokolle verfügbar.", + "No logs found": "Keine Protokolle gefunden", + "No mismatched fields found - SOLR schema is properly configured": "Keine nicht übereinstimmenden Felder gefunden – das SOLR-Schema ist korrekt konfiguriert", + "No objects found": "Keine Objekte gefunden", + "No objects found to vectorize": "Keine Objekte zum Vektorisieren gefunden", + "No objects selected for deletion": "Keine Objekte zum Löschen ausgewählt", + "No objects to delete": "Keine Objekte zu löschen", + "No objects were permanently deleted": "Es wurden keine Objekte endgültig gelöscht", + "No properties found": "Keine Eigenschaften gefunden", + "No properties match your filters.": "Keine Eigenschaften entsprechen Ihren Filtern.", + "No purge date set": "Kein Bereinigungsdatum festgelegt", + "No register data available": "Keine Registerdaten verfügbar", + "No registers found": "Keine Register gefunden", + "No registers found for this application.": "Keine Register für diese Anwendung gefunden.", + "No relations found": "Keine Beziehungen gefunden", + "No request body available": "Kein Anfrage-Body verfügbar", + "No response body available": "Kein Antwort-Body verfügbar", + "No saved views yet. Create one in the Search tab!": "Noch keine gespeicherten Ansichten. Erstellen Sie eine im Tab „Suche“!", + "No schema data available": "Keine Schemadaten verfügbar", + "No schema selected for exploration": "Kein Schema zur Erkundung ausgewählt", + "No schemas found": "Keine Schemas gefunden", + "No schemas found for this application.": "Keine Schemas für diese Anwendung gefunden.", + "No search terms data available": "Keine Suchbegriffsdaten verfügbar", + "No search trail entries found": "Keine Such-Trail-Einträge gefunden", + "No sources are available.": "Es sind keine Quellen verfügbar.", + "No sources found": "Keine Quellen gefunden", + "No templates found": "Keine Vorlagen gefunden", + "No templates have been created yet": "Es wurden noch keine Vorlagen erstellt", + "No valid magic-mapped register+schema combinations found": "Keine gültigen magic-mapped Register+Schema-Kombinationen gefunden", + "No views found. Create views first before configuring vectorization.": "Keine Ansichten gefunden. Erstellen Sie zuerst Ansichten, bevor Sie die Vektorisierung konfigurieren.", + "No views match your search": "Keine Ansichten entsprechen Ihrer Suche", + "No webhooks found": "Keine Webhooks gefunden", + "No webhooks have been configured yet": "Es wurden noch keine Webhooks konfiguriert", + "No workflows found in this project. Create workflows in the n8n editor.": "In diesem Projekt wurden keine Workflows gefunden. Erstellen Sie Workflows im n8n-Editor.", + "None": "Keine", + "Not helpful": "Nicht hilfreich", + "Not in use": "Nicht in Verwendung", + "Number Constraints": "Zahleneinschränkungen", + "Number of chunks to vectorize in one API call (1-100).": "Anzahl der in einem API-Aufruf zu vektorisierenden Chunks (1–100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Anzahl der in einem API-Aufruf zu vektorisierenden Chunks. Höher = schneller, aber mehr Speicher. Empfohlen: 10–50.", + "Number of objects to process in each batch (1-5000).": "Anzahl der in jedem Stapel zu verarbeitenden Objekte (1–5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Anzahl der in einem API-Aufruf zu vektorisierenden Objekte. Höher = schneller, aber mehr Speicher. Empfohlen: 10–50.", + "Numeric Range:": "Zahlenbereich:", + "Object": "Objekt", + "Object Collection": "Objekt-Collection", + "Object Count Prediction": "Objektanzahl-Vorhersage", + "Object ID": "Objekt-ID", + "Object Management": "Objektverwaltung", + "Object Vectorization": "Objektvektorisierung", + "Object Warmup": "Objekt-Warmup", + "Object collection not configured": "Objekt-Collection nicht konfiguriert", + "Object not found": "Objekt nicht gefunden", + "Object permanently deleted": "Objekt endgültig gelöscht", + "Object restored successfully": "Objekt erfolgreich wiederhergestellt", + "Object settings updated successfully": "Objekteinstellungen erfolgreich aktualisiert", + "Object sources": "Objektquellen", + "Object unlocked successfully": "Objekt erfolgreich entsperrt", + "Object vectorization configuration saved successfully": "Objektvektorisierungskonfiguration erfolgreich gespeichert", + "Object vectorized successfully": "Objekt erfolgreich vektorisiert", + "Object:": "Objekt:", + "Objects": "Objekte", + "Objects Analyzed": "Analysierte Objekte", + "Objects Distribution": "Objektverteilung", + "Objects being analyzed": "Objekte werden analysiert", + "Objects by Register": "Objekte nach Register", + "Objects by Schema": "Objekte nach Schema", + "Objects deletion completed successfully": "Objektlöschung erfolgreich abgeschlossen", + "Objects in schema": "Objekte im Schema", + "Objects to Process:": "Zu verarbeitende Objekte:", + "Objects to be analyzed": "Zu analysierende Objekte", + "Objects to be deleted": "Zu löschende Objekte", + "Objects to be validated": "Zu validierende Objekte", + "Objects will be serialized as JSON text before vectorization": "Objekte werden vor der Vektorisierung als JSON-Text serialisiert", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekte werden vorläufig gelöscht (als gelöscht markiert, aber in der Datenbank behalten). Sie können bei Bedarf später wiederhergestellt werden.", + "Old Value": "Alter Wert", + "Oldest Item (days)": "Ältestes Element (Tage)", + "Ollama Chat Settings": "Ollama-Chat-Einstellungen", + "Ollama Embedding Configuration": "Ollama-Embedding-Konfiguration", + "Ollama URL": "Ollama-URL", + "Only With Changes": "Nur mit Änderungen", + "Open Register Objects": "Open-Register-Objekte", + "Open n8n Editor": "n8n-Editor öffnen", + "OpenAI Chat Settings": "OpenAI-Chat-Einstellungen", + "OpenAI Embedding Configuration": "OpenAI-Embedding-Konfiguration", + "OpenRegister Settings": "OpenRegister-Einstellungen", + "Optional webhook secret for signature verification": "Optionales Webhook-Geheimnis zur Signaturverifizierung", + "Order": "Reihenfolge", + "Organisation": "Organisation", + "Organisation ID": "Organisations-ID", + "Organisation Members": "Organisationsmitglieder", + "Organisation Statistics": "Organisationsstatistiken", + "Organisation created successfully": "Organisation erfolgreich erstellt", + "Organisation settings saved successfully": "Organisationseinstellungen erfolgreich gespeichert", + "Organisations": "Organisationen", + "Organization": "Organisation", + "Organization ID (Optional)": "Organisations-ID (optional)", + "Orphaned Items": "Verwaiste Elemente", + "Owner": "Eigentümer", + "Owner:": "Eigentümer:", + "Parallel Mode (Faster, more resource intensive)": "Parallelmodus (schneller, ressourcenintensiver)", + "Parallel:": "Parallel:", + "Password": "Passwort", + "Password for SOLR authentication (optional)": "Passwort für die SOLR-Authentifizierung (optional)", + "Path": "Pfad", + "Path in repository": "Pfad im Repository", + "Path where the configuration file will be saved in the repository": "Pfad, unter dem die Konfigurationsdatei im Repository gespeichert wird", + "Path where the register OAS file will be saved in the repository": "Pfad, unter dem die OAS-Datei des Registers im Repository gespeichert wird", + "Pattern (regex)": "Muster (Regex)", + "Pattern Issue": "Musterproblem", + "Pattern constraint is missing": "Die Mustereinschränkung fehlt", + "Patterns:": "Muster:", + "Pending": "Ausstehend", + "Permanent Delete Mode": "Modus „Endgültig löschen“", + "Permanently Delete": "Endgültig löschen", + "Permanently delete already soft-deleted objects": "Bereits vorläufig gelöschte Objekte endgültig löschen", + "Permanently delete this conversation?": "Diese Unterhaltung endgültig löschen?", + "Person": "Person", + "Personal Data": "Personenbezogene Daten", + "Phone": "Telefon", + "Photos": "Fotos", + "Please create an agent in the": "Bitte erstellen Sie einen Agenten im", + "Please select an agent to continue": "Bitte wählen Sie einen Agenten aus, um fortzufahren", + "Please select which register and schema to use for the new object": "Bitte wählen Sie aus, welches Register und Schema für das neue Objekt verwendet werden soll", + "Please try again later.": "Bitte versuchen Sie es später erneut.", + "Please wait while we fetch your agents.": "Bitte warten Sie, während wir Ihre Agenten abrufen.", + "Please wait while we fetch your applications.": "Bitte warten Sie, während wir Ihre Anwendungen abrufen.", + "Please wait while we fetch your configurations.": "Bitte warten Sie, während wir Ihre Konfigurationen abrufen.", + "Please wait while we fetch your deleted items.": "Bitte warten Sie, während wir Ihre gelöschten Elemente abrufen.", + "Please wait while we fetch your sources.": "Bitte warten Sie, während wir Ihre Quellen abrufen.", + "Polls": "Umfragen", + "Popular Search Terms": "Beliebte Suchbegriffe", + "Port": "Port", + "Prerequisites": "Voraussetzungen", + "Press Enter to send, Shift+Enter for new line": "Drücken Sie Enter zum Senden, Umschalt+Enter für eine neue Zeile", + "Previous": "Zurück", + "Private": "Privat", + "Processes chunks in batches with simulated parallelism.": "Verarbeitet Chunks in Stapeln mit simulierter Parallelität.", + "Processes file chunks sequentially (safest).": "Verarbeitet Datei-Chunks sequenziell (am sichersten).", + "Processes objects in chunks with simulated parallelism.": "Verarbeitet Objekte in Chunks mit simulierter Parallelität.", + "Processes objects sequentially (safest).": "Verarbeitet Objekte sequenziell (am sichersten).", + "Processing": "Wird verarbeitet", + "Processing Limits": "Verarbeitungsgrenzen", + "Processing...": "Wird verarbeitet …", + "Project initialized successfully": "Projekt erfolgreich initialisiert", + "Project not found. Please initialize first.": "Projekt nicht gefunden. Bitte zuerst initialisieren.", + "Properties": "Eigenschaften", + "Property Behaviors": "Eigenschaftsverhalten", + "Property Configuration:": "Eigenschaftskonfiguration:", + "Property Filters": "Eigenschaftsfilter", + "Property Title": "Eigenschaftstitel", + "Property Type": "Eigenschaftstyp", + "Property can be improved": "Die Eigenschaft kann verbessert werden", + "Protected": "Geschützt", + "Provider": "Anbieter", + "Provider is required for testing": "Für den Test ist ein Anbieter erforderlich", + "Public": "Öffentlich", + "Public View": "Öffentliche Ansicht", + "Public views can be accessed by anyone in the system": "Auf öffentliche Ansichten kann jeder im System zugreifen", + "Publish": "Veröffentlichen", + "Publish Objects": "Objekte veröffentlichen", + "Publish all objects in this schema": "Alle Objekte in diesem Schema veröffentlichen", + "Published": "Veröffentlicht", + "Published:": "Veröffentlicht:", + "Publishing...": "Wird veröffentlicht …", + "Purge": "Bereinigen", + "Purge Date": "Bereinigungsdatum", + "Query Complexity Distribution": "Verteilung der Abfragekomplexität", + "Query parameter is required": "Der Query-Parameter ist erforderlich", + "RAG": "RAG", + "RAG Configuration": "RAG-Konfiguration", + "Rate limits cleared successfully": "Ratenbegrenzungen erfolgreich zurückgesetzt", + "Raw Changes Data": "Rohdaten der Änderungen", + "Re-vectorize on object update": "Bei Objektaktualisierung neu vektorisieren", + "Read": "Lesen", + "Recommendations:": "Empfehlungen:", + "Recommended Type:": "Empfohlener Typ:", + "Recommended: 5 sources": "Empfohlen: 5 Quellen", + "Refresh": "Aktualisieren", + "Refresh Data": "Daten aktualisieren", + "Refresh Stats": "Statistiken aktualisieren", + "Refresh Workflows": "Workflows aktualisieren", + "Refresh dashboard": "Dashboard aktualisieren", + "Refresh database info": "Datenbankinformationen aktualisieren", + "Register": "Register", + "Register ID": "Register-ID", + "Register Statistics": "Registerstatistiken", + "Register Totals": "Registersummen", + "Register depublished successfully": "Register erfolgreich depubliziert", + "Register not found": "Register nicht gefunden", + "Register objects deletion completed successfully": "Löschung der Registerobjekte erfolgreich abgeschlossen", + "Register published successfully": "Register erfolgreich veröffentlicht", + "Register/Schema Usage": "Register-/Schemanutzung", + "Registers": "Register", + "Reindex": "Neu indizieren", + "Reindex completed": "Neuindizierung abgeschlossen", + "Reindex completed successfully": "Neuindizierung erfolgreich abgeschlossen", + "Reindex failed": "Neuindizierung fehlgeschlagen", + "Relations": "Beziehungen", + "Remote Version": "Remote-Version", + "Remove": "Entfernen", + "Remove filter": "Filter entfernen", + "Remove from favorites": "Aus Favoriten entfernen", + "Remove group": "Gruppe entfernen", + "Removed": "Entfernt", + "Removed from favorites": "Aus Favoriten entfernt", + "Rename Conversation": "Unterhaltung umbenennen", + "Rename conversation": "Unterhaltung umbenennen", + "Replicas": "Replikate", + "Repository": "Repository", + "Request Body": "Anfrage-Body", + "Request Data": "Anfragedaten", + "Request Details": "Anfragedetails", + "Request body must be JSON with a \"query\" field": "Der Anfrage-Body muss JSON mit einem Feld „query“ sein", + "Request timeout in seconds": "Anfrage-Timeout in Sekunden", + "Required field": "Pflichtfeld", + "Required status is inconsistent": "Der Pflichtstatus ist inkonsistent", + "Rerun Search": "Suche erneut ausführen", + "Reset Changes": "Änderungen zurücksetzen", + "Reset Filters": "Filter zurücksetzen", + "Response Body": "Antwort-Body", + "Response Details": "Antwortdetails", + "Response Time:": "Antwortzeit:", + "Restore": "Wiederherstellen", + "Restore conversation": "Unterhaltung wiederherstellen", + "Restore or permanently delete items": "Elemente wiederherstellen oder endgültig löschen", + "Result Count Range": "Ergebnisanzahlbereich", + "Results": "Ergebnisse", + "Retrieve all objects for this schema": "Alle Objekte für dieses Schema abrufen", + "Retry": "Wiederholen", + "Retry Failed Extractions": "Fehlgeschlagene Extraktionen wiederholen", + "Retry Policy": "Wiederholungsrichtlinie", + "Retry completed": "Wiederholung abgeschlossen", + "Retrying...": "Wird wiederholt …", + "Risk Level": "Risikostufe", + "Roles updated successfully": "Rollen erfolgreich aktualisiert", + "SOLR Connection Settings": "SOLR-Verbindungseinstellungen", + "SOLR Version:": "SOLR-Version:", + "SOLR actions menu": "SOLR-Aktionsmenü", + "SOLR base path (usually /solr)": "SOLR-Basispfad (normalerweise /solr)", + "SOLR is disabled": "SOLR ist deaktiviert", + "SOLR is not available or not configured": "SOLR ist nicht verfügbar oder nicht konfiguriert", + "SOLR logging disabled": "SOLR-Logging deaktiviert", + "SOLR logging enabled": "SOLR-Logging aktiviert", + "SOLR search disabled": "SOLR-Suche deaktiviert", + "SOLR search enabled": "SOLR-Suche aktiviert", + "SOLR server hostname or IP address": "SOLR-Server-Hostname oder IP-Adresse", + "SOLR server port number (optional, defaults to 8983)": "SOLR-Server-Portnummer (optional, Standard ist 8983)", + "SOLR setup completed successfully": "SOLR-Einrichtung erfolgreich abgeschlossen", + "SOLR setup failed": "SOLR-Einrichtung fehlgeschlagen", + "SOLR setup failed - check logs": "SOLR-Einrichtung fehlgeschlagen – Protokolle prüfen", + "Sample Values:": "Beispielwerte:", + "Save": "Speichern", + "Save Configuration": "Konfiguration speichern", + "Save Connection Settings": "Verbindungseinstellungen speichern", + "Save Roles": "Rollen speichern", + "Save Settings": "Einstellungen speichern", + "Save current search as view": "Aktuelle Suche als Ansicht speichern", + "Saved Views": "Gespeicherte Ansichten", + "Saving...": "Wird gespeichert …", + "Schema": "Schema", + "Schema ID": "Schema-ID", + "Schema Name": "Schemaname", + "Schema Statistics": "Schemastatistiken", + "Schema depublished successfully": "Schema erfolgreich depubliziert", + "Schema objects deletion completed successfully": "Löschung der Schemaobjekte erfolgreich abgeschlossen", + "Schema published successfully": "Schema erfolgreich veröffentlicht", + "Schema successfully created": "Schema erfolgreich erstellt", + "Schema successfully updated": "Schema erfolgreich aktualisiert", + "Schemas": "Schemas", + "Scheme": "Schema", + "Search / Views": "Suche / Ansichten", + "Search Activity": "Suchaktivität", + "Search Analytics": "Suchanalysen", + "Search Entities": "Entitäten durchsuchen", + "Search Files": "Dateien durchsuchen", + "Search GitHub": "GitHub durchsuchen", + "Search GitLab": "GitLab durchsuchen", + "Search Method": "Suchmethode", + "Search Term": "Suchbegriff", + "Search Trail Details": "Such-Trail-Details", + "Search Trail Management": "Such-Trail-Verwaltung", + "Search Trail Statistics": "Such-Trail-Statistiken", + "Search Trails": "Such-Trails", + "Search Views": "Ansichten durchsuchen", + "Search Webhooks": "Webhooks durchsuchen", + "Search by file name or path": "Nach Dateiname oder Pfad suchen", + "Search by name or URL": "Nach Name oder URL suchen", + "Search by value": "Nach Wert suchen", + "Search in Files": "In Dateien suchen", + "Search in Objects": "In Objekten suchen", + "Search property names...": "Eigenschaftsnamen durchsuchen …", + "Search trail deletion not implemented yet": "Such-Trail-Löschung noch nicht implementiert", + "Search views...": "Ansichten durchsuchen …", + "Secret": "Geheimnis", + "Secret key for HMAC signature generation (optional)": "Geheimer Schlüssel für die HMAC-Signaturerzeugung (optional)", + "Select AI Agent": "KI-Agent auswählen", + "Select All": "Alle auswählen", + "Select ConfigSet": "ConfigSet auswählen", + "Select File Types to Vectorize:": "Zu vektorisierende Dateitypen auswählen:", + "Select HTTP method": "HTTP-Methode auswählen", + "Select Register and Schema": "Register und Schema auswählen", + "Select Views to Vectorize:": "Zu vektorisierende Ansichten auswählen:", + "Select a Nextcloud group to add": "Eine hinzuzufügende Nextcloud-Gruppe auswählen", + "Select a branch": "Einen Branch auswählen", + "Select a model or type a custom model name": "Ein Modell auswählen oder einen benutzerdefinierten Modellnamen eingeben", + "Select a repository": "Ein Repository auswählen", + "Select a repository you have write access to": "Ein Repository auswählen, für das Sie Schreibzugriff haben", + "Select an AI Agent": "Einen KI-Agenten auswählen", + "Select an AI agent to begin chatting with your data.": "Wählen Sie einen KI-Agenten aus, um mit Ihren Daten zu chatten.", + "Select backend": "Backend auswählen", + "Select chat model": "Chat-Modell auswählen", + "Select collection for files": "Collection für Dateien auswählen", + "Select collection for objects": "Collection für Objekte auswählen", + "Select default organisation": "Standardorganisation auswählen", + "Select event to listen to...": "Ereignis auswählen, auf das gehört werden soll …", + "Select model": "Modell auswählen", + "Select options...": "Optionen auswählen …", + "Select period": "Zeitraum auswählen", + "Select property to send as payload": "Eigenschaft auswählen, die als Payload gesendet werden soll", + "Select provider": "Anbieter auswählen", + "Select registers and schemas to save a view": "Register und Schemas auswählen, um eine Ansicht zu speichern", + "Select retry policy": "Wiederholungsrichtlinie auswählen", + "Select the branch to publish to": "Den Branch auswählen, auf dem veröffentlicht werden soll", + "Select the event this webhook should listen to": "Das Ereignis auswählen, auf das dieser Webhook hören soll", + "Select views to vectorize:": "Zu vektorisierende Ansichten auswählen:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Wählen Sie aus, welche Nextcloud-Gruppen für diese Organisation verfügbar sind. Benutzer in diesen Gruppen haben Zugriff auf Organisationsressourcen.", + "Select which data views the AI can search": "Wählen Sie aus, welche Datenansichten die KI durchsuchen kann", + "Select which property from the event should be used as the webhook payload data": "Wählen Sie aus, welche Eigenschaft des Ereignisses als Webhook-Payload-Daten verwendet werden soll", + "Select which tools the AI can use to perform actions": "Wählen Sie aus, welche Werkzeuge die KI verwenden kann, um Aktionen auszuführen", + "Selected": "Ausgewählt", + "Selected Groups": "Ausgewählte Gruppen", + "Selected audit trails deleted successfully": "Ausgewählte Audit-Trails erfolgreich gelöscht", + "Selected search trails deleted successfully": "Ausgewählte Such-Trails erfolgreich gelöscht", + "Selected users can access this view": "Ausgewählte Benutzer können auf diese Ansicht zugreifen", + "Send additional feedback": "Zusätzliches Feedback senden", + "Send as CloudEvent": "Als CloudEvent senden", + "Sensitive PII": "Sensible personenbezogene Daten", + "Serial Mode (Safer, slower)": "Serieller Modus (sicherer, langsamer)", + "Serial:": "Seriell:", + "Server Configuration": "Serverkonfiguration", + "Server Information": "Serverinformationen", + "Settings": "Einstellungen", + "Shards": "Shards", + "Share": "Teilen", + "Share with Groups": "Mit Gruppen teilen", + "Share with Users": "Mit Benutzern teilen", + "Show Filters": "Filter anzeigen", + "Show less": "Weniger anzeigen", + "Show only entries with changes": "Nur Einträge mit Änderungen anzeigen", + "Simple": "Einfach", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Einfache Abfragen: Grundlegende Textsuchen mit minimalen Parametern (z. B. einzelner Suchbegriff, keine erweiterten Filter)", + "Size": "Größe", + "Slug": "Slug", + "Soft Delete Mode": "Modus „Vorläufig löschen“", + "Soft Deleted Items": "Vorläufig gelöschte Elemente", + "SolrCloud mode enabled": "SolrCloud-Modus aktiviert", + "Source": "Quelle", + "Sources": "Quellen", + "Sources:": "Quellen:", + "Standalone SOLR mode": "Standalone-SOLR-Modus", + "Start Conversation": "Unterhaltung starten", + "Start Vectorization": "Vektorisierung starten", + "Start a conversation": "Eine Unterhaltung starten", + "Starting names cache warmup...": "Namen-Cache-Warmup wird gestartet …", + "Starting...": "Wird gestartet …", + "Statistics": "Statistiken", + "Stats calculation not yet implemented": "Statistikberechnung noch nicht implementiert", + "Status": "Status", + "Status Code": "Statuscode", + "String Constraints": "Zeichenketteneinschränkungen", + "Success": "Erfolg", + "Success Rate": "Erfolgsrate", + "Success Status": "Erfolgsstatus", + "Successful": "Erfolgreich", + "Successfully joined organisation": "Erfolgreich der Organisation beigetreten", + "Sync Table": "Tabelle synchronisieren", + "Synchronization Settings": "Synchronisierungseinstellungen", + "System Default": "Systemstandard", + "Technical Description": "Technische Beschreibung", + "Technical description for developers and administrators": "Technische Beschreibung für Entwickler und Administratoren", + "Temperature": "Temperatur", + "Templates": "Vorlagen", + "Test": "Testen", + "Test Chat": "Chat testen", + "Test Connection": "Verbindung testen", + "Test Embedding": "Embedding testen", + "Test endpoint executed successfully": "Test-Endpunkt erfolgreich ausgeführt", + "Test webhook delivered successfully": "Test-Webhook erfolgreich zugestellt", + "Test webhook delivery failed": "Test-Webhook-Zustellung fehlgeschlagen", + "Test webhook sent successfully": "Test-Webhook erfolgreich gesendet", + "Testing...": "Wird getestet …", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Text-Chunks werden während der Dateiextraktion generiert und in der Datenbank gespeichert. Die Vektorisierung liest diese vorab in Chunks aufgeteilten Dateien und wandelt sie in Embeddings um.", + "Text extracted successfully": "Text erfolgreich extrahiert", + "Text extraction disabled": "Textextraktion deaktiviert", + "The URL where webhook events will be sent": "Die URL, an die Webhook-Ereignisse gesendet werden", + "The requested conversation does not exist": "Die angeforderte Unterhaltung existiert nicht", + "The saved view and all its search configuration": "Die gespeicherte Ansicht und ihre gesamte Suchkonfiguration", + "There are no audit trail entries matching your current filters.": "Es gibt keine Audit-Trail-Einträge, die Ihren aktuellen Filtern entsprechen.", + "There are no deleted items matching your current filters.": "Es gibt keine gelöschten Elemente, die Ihren aktuellen Filtern entsprechen.", + "There are no search trail entries matching your current filters.": "Es gibt keine Such-Trail-Einträge, die Ihren aktuellen Filtern entsprechen.", + "There are no webhook log entries matching your filters.": "Es gibt keine Webhook-Protokolleinträge, die Ihren Filtern entsprechen.", + "This action cannot be undone.": "Diese Aktion kann nicht rückgängig gemacht werden.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Diese Aktion kann nicht rückgängig gemacht werden. Stellen Sie sicher, dass keine Collections dieses ConfigSet verwenden.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Dieser Agent hat keine konfigurierbaren Ansichten oder Werkzeuge, aber Sie können die RAG-Einstellungen unten dennoch anpassen.", + "This analysis may take some time": "Diese Analyse kann einige Zeit dauern", + "This audit trail entry does not contain any change information.": "Dieser Audit-Trail-Eintrag enthält keine Änderungsinformationen.", + "This configuration has no data defined.": "Für diese Konfiguration sind keine Daten definiert.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Dieser Endpunkt ist veraltet. Verwenden Sie stattdessen Chunk-basierte Endpunkte.", + "This entity has no relations to objects or files": "Diese Entität hat keine Beziehungen zu Objekten oder Dateien", + "This is a test webhook from OpenRegister": "Dies ist ein Test-Webhook von OpenRegister", + "This schema must use Magic Table configuration to sync": "Dieses Schema muss die Magic-Table-Konfiguration verwenden, um zu synchronisieren", + "This source has no associated registers.": "Diese Quelle hat keine zugeordneten Register.", + "This will copy the _default ConfigSet with the new name": "Dadurch wird das ConfigSet _default mit dem neuen Namen kopiert", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Dadurch werden ALLE Embeddings (Vektoren) endgültig aus der Datenbank gelöscht. Sie müssen alle Objekte und Dateien neu vektorisieren. Diese Aktion kann nicht rückgängig gemacht werden.\\n\\nSind Sie sicher, dass Sie fortfahren möchten?", + "This will permanently delete:": "Dadurch wird endgültig gelöscht:", + "Timeout (seconds)": "Timeout (Sekunden)", + "Timestamp": "Zeitstempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tipp: Aktivieren Sie nur Ansichten, die eine semantische Suche benötigen, um die Embedding-Kosten zu minimieren. Einfache Nachschlagetabellen benötigen selten eine Vektorisierung.", + "Title": "Titel", + "To Date": "Bis Datum", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Um die Chunk-Größe oder -Strategie anzupassen, gehen Sie zu Dateikonfiguration → Verarbeitungsgrenzen.", + "To change the embedding provider or model, go to LLM Configuration.": "Um den Embedding-Anbieter oder das Modell zu ändern, gehen Sie zur LLM-Konfiguration.", + "To date": "Bis Datum", + "Toggle search sidebar": "Such-Seitenleiste umschalten", + "Tools": "Werkzeuge", + "Top Deleters": "Top-Löscher", + "Total": "Gesamt", + "Total Audit Trails": "Audit-Trails gesamt", + "Total Chunks Available:": "Verfügbare Chunks gesamt:", + "Total Deleted Items": "Gelöschte Elemente gesamt", + "Total Members": "Mitglieder gesamt", + "Total Objects": "Objekte gesamt", + "Total Objects in Database:": "Objekte in der Datenbank gesamt:", + "Total Organisations": "Organisationen gesamt", + "Total Results": "Ergebnisse gesamt", + "Total Searches": "Suchen gesamt", + "Total Size": "Gesamtgröße", + "Total Storage": "Gesamtspeicher", + "Total:": "Gesamt:", + "Totals": "Summen", + "Type": "Typ", + "Type Issue": "Typproblem", + "Type Variations:": "Typvariationen:", + "Type must be \"positive\" or \"negative\"": "Der Typ muss „positive“ oder „negative“ sein", + "Type to search for groups": "Tippen, um nach Gruppen zu suchen", + "Type to search for users": "Tippen, um nach Benutzern zu suchen", + "Type to search groups...": "Tippen, um Gruppen zu durchsuchen …", + "Type to search users...": "Tippen, um Benutzer zu suchen …", + "URL": "URL", + "URL where Ollama is running": "URL, unter der Ollama läuft", + "UUID:": "UUID:", + "Unchanged": "Unverändert", + "Unique Search Terms": "Eindeutige Suchbegriffe", + "Unique Terms": "Eindeutige Begriffe", + "Unknown": "Unbekannt", + "Unknown Register": "Unbekanntes Register", + "Unknown Schema": "Unbekanntes Schema", + "Untitled View": "Unbenannte Ansicht", + "Update": "Aktualisieren", + "Update Operations": "Aktualisierungsvorgänge", + "Update configuration: ...": "Konfiguration aktualisieren: …", + "Update register OAS: ...": "Register-OAS aktualisieren: …", + "Update vectors when object data changes (recommended for accurate search)": "Vektoren aktualisieren, wenn sich Objektdaten ändern (empfohlen für genaue Suche)", + "Updated": "Aktualisiert", + "Updated At": "Aktualisiert am", + "Updated:": "Aktualisiert:", + "Uptime:": "Betriebszeit:", + "Usage Count": "Nutzungsanzahl", + "Use SolrCloud with Zookeeper for distributed search": "SolrCloud mit Zookeeper für verteilte Suche verwenden", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Verwenden Sie Filter, um Audit-Trail-Einträge nach Register, Schema, Aktionstyp, Benutzer, Datumsbereich oder Objekt-ID einzugrenzen.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Verwenden Sie Filter, um gelöschte Elemente nach Register, Schema, Löschdatum oder dem Benutzer, der sie gelöscht hat, einzugrenzen.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Verwenden Sie Filter, um Suchverlaufseinträge nach Register, Schema, Erfolgsstatus, Benutzer, Datumsbereich, Suchbegriffen oder Leistungsmetriken einzugrenzen.", + "Used By Collections": "Verwendet von Sammlungen", + "User": "Benutzer", + "User Agent Statistics": "User-Agent-Statistiken", + "User Description": "Benutzerbeschreibung", + "User rate limits cleared successfully": "Benutzer-Ratenlimits erfolgreich zurückgesetzt", + "User removed successfully": "Benutzer erfolgreich entfernt", + "User-friendly description shown in forms and help text": "Benutzerfreundliche Beschreibung, die in Formularen und Hilfetexten angezeigt wird", + "Username": "Benutzername", + "Username for SOLR authentication (optional)": "Benutzername für die SOLR-Authentifizierung (optional)", + "Users": "Benutzer", + "Using Pre-Generated Chunks": "Vorab generierte Chunks verwenden", + "Validate": "Validieren", + "Validate Objects": "Objekte validieren", + "Validation completed successfully": "Validierung erfolgreich abgeschlossen", + "Validation failed": "Validierung fehlgeschlagen", + "Value": "Wert", + "Vector Dimensions": "Vektordimensionen", + "Vector Search Backend": "Vektorsuche-Backend", + "Vector field: _embedding_": "Vektorfeld: _embedding_", + "Vectorization Triggers": "Vektorisierungs-Auslöser", + "Vectorize All Files": "Alle Dateien vektorisieren", + "Vectorize All Objects": "Alle Objekte vektorisieren", + "Vectorize all file types": "Alle Dateitypen vektorisieren", + "Vectorize all views": "Alle Ansichten vektorisieren", + "Vectorize on object creation": "Beim Erstellen von Objekten vektorisieren", + "Vectors will be stored in your existing object and file collections": "Vektoren werden in Ihren bestehenden Objekt- und Dateisammlungen gespeichert", + "Version": "Version", + "Very High": "Sehr hoch", + "View API Docs": "API-Dokumentation anzeigen", + "View Changes": "Änderungen anzeigen", + "View Details": "Details anzeigen", + "View Error": "Fehler anzeigen", + "View File": "Datei anzeigen", + "View Full Details": "Vollständige Details anzeigen", + "View Logs": "Protokolle anzeigen", + "View Name": "Ansichtsname", + "View Object": "Objekt anzeigen", + "View Parameters": "Parameter anzeigen", + "View Selection": "Auswahl anzeigen", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Suchverlaufsprotokolle mit erweiterten Filter- und Analysefunktionen anzeigen und auswerten", + "View and analyze system audit trails with advanced filtering capabilities": "System-Audit-Trails mit erweiterten Filterfunktionen anzeigen und auswerten", + "View deleted successfully": "Ansicht erfolgreich gelöscht", + "View entity details and manage relations": "Entitätsdetails anzeigen und Beziehungen verwalten", + "View name": "Ansichtsname", + "View name is required": "Ansichtsname ist erforderlich", + "View saved successfully!": "Ansicht erfolgreich gespeichert!", + "View search analytics and manage search logs": "Suchanalysen anzeigen und Suchprotokolle verwalten", + "View successfully deleted": "Ansicht erfolgreich gelöscht", + "View successfully updated": "Ansicht erfolgreich aktualisiert", + "View updated successfully!": "Ansicht erfolgreich aktualisiert!", + "View webhook delivery logs and filter by webhook": "Webhook-Zustellungsprotokolle anzeigen und nach Webhook filtern", + "Views": "Ansichten", + "Visible to users": "Für Benutzer sichtbar", + "Wait for Response": "Auf Antwort warten", + "Wait for webhook response before continuing (required for request/response flows)": "Auf Webhook-Antwort warten, bevor fortgefahren wird (erforderlich für Anfrage-/Antwort-Abläufe)", + "Warmup Names Cache": "Namens-Cache vorwärmen", + "Warning": "Warnung", + "Warning:": "Warnung:", + "Webhook": "Webhook", + "Webhook Log Details": "Webhook-Protokolldetails", + "Webhook Logs": "Webhook-Protokolle", + "Webhook created successfully": "Webhook erfolgreich erstellt", + "Webhook deleted": "Webhook gelöscht", + "Webhook retry delivered successfully": "Webhook-Wiederholung erfolgreich zugestellt", + "Webhook retry delivery failed": "Webhook-Wiederholungszustellung fehlgeschlagen", + "Webhook updated": "Webhook aktualisiert", + "Webhook updated successfully": "Webhook erfolgreich aktualisiert", + "Webhooks": "Webhooks", + "Weekly": "Wöchentlich", + "Wrap webhook payload in CloudEvents format for better interoperability": "Webhook-Payload im CloudEvents-Format verpacken, um die Interoperabilität zu verbessern", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Ja", + "You": "Sie", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Sie können neue ConfigSets basierend auf der _default-Vorlage erstellen oder eigene direkt auf Ihren SOLR-Server hochladen.", + "You do not have access to this conversation": "Sie haben keinen Zugriff auf diese Unterhaltung", + "You do not have permission to delete this conversation": "Sie haben keine Berechtigung, diese Unterhaltung zu löschen", + "You do not have permission to modify this conversation": "Sie haben keine Berechtigung, diese Unterhaltung zu ändern", + "You do not have permission to restore this conversation": "Sie haben keine Berechtigung, diese Unterhaltung wiederherzustellen", + "You must be logged in to favorite views": "Sie müssen angemeldet sein, um Ansichten zu favorisieren", + "You need an AI agent to start a conversation.": "Sie benötigen einen KI-Agenten, um eine Unterhaltung zu beginnen.", + "Your Fireworks AI API key. Get one at": "Ihr Fireworks-AI-API-Schlüssel. Erhältlich unter", + "Your OpenAI API key. Get one at": "Ihr OpenAI-API-Schlüssel. Erhältlich unter", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Ihr Feedback wurde erfasst. Optional können Sie hier weitere Details angeben …", + "Zookeeper Hosts": "Zookeeper-Hosts", + "Zookeeper Port": "Zookeeper-Port", + "Zookeeper Settings (SolrCloud)": "Zookeeper-Einstellungen (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper-Verbindungszeichenfolge für SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper-Portnummer (optional, Standardwert 2181)", + "chunk": "Chunk", + "chunks": "Chunks", + "chunks processed": "Chunks verarbeitet", + "configuration(s)": "Konfiguration(en)", + "e.g., lib/Settings/config.json": "z. B. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "z. B. lib/Settings/register.json", + "failed": "fehlgeschlagen", + "file": "Datei", + "files": "Dateien", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook": "https://example.com/webhook", + "members": "Mitglieder", + "menu or contact someone with permission to create agents.": "Menü oder wenden Sie sich an jemanden mit der Berechtigung, Agenten zu erstellen.", + "messages": "Nachrichten", + "n8n URL and API key are required": "n8n-URL und API-Schlüssel sind erforderlich", + "n8n configuration saved successfully": "n8n-Konfiguration erfolgreich gespeichert", + "n8n connection not configured": "n8n-Verbindung nicht konfiguriert", + "n8n connection successful": "n8n-Verbindung erfolgreich", + "n8n connection test successful": "n8n-Verbindungstest erfolgreich", + "n8n integration disabled": "n8n-Integration deaktiviert", + "n8n integration enabled": "n8n-Integration aktiviert", + "n8n project initialized successfully": "n8n-Projekt erfolgreich initialisiert", + "n8n settings saved successfully": "n8n-Einstellungen erfolgreich gespeichert", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "Objekte", + "objects processed": "Objekte verarbeitet", + "or pick a range": "oder einen Bereich auswählen", + "org-...": "org-...", + "provides a good balance between speed and accuracy.": "bietet ein gutes Gleichgewicht zwischen Geschwindigkeit und Genauigkeit.", + "register(s)": "Register", + "register(s) selected": "Register ausgewählt", + "required": "erforderlich", + "results": "Ergebnisse", + "schema(s)": "Schema/Schemata", + "schema(s) selected": "Schema/Schemata ausgewählt", + "sk-...": "sk-...", + "this application": "diese Anwendung", + "to": "bis", + "vectorized": "vektorisiert", + "views selected": "Ansichten ausgewählt", + "ℹ️ Current Configuration": "ℹ️ Aktuelle Konfiguration", + "⚡ Batch Processing": "⚡ Stapelverarbeitung", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Umfassender Kontext ✓ Geringere Wahrscheinlichkeit, Details zu übersehen ✗ Langsamere Antworten ✗ Kann weniger relevante Informationen enthalten", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Schnellere Antworten ✓ Gezieltere Antworten ✗ Kann relevante Informationen übersehen", + "✨ AI Features": "✨ KI-Funktionen", + "All search trails cleared successfully": "Alle Suchverläufe erfolgreich gelöscht", + "Multiple search trail deletion not implemented yet": "Das Löschen mehrerer Suchverläufe ist noch nicht implementiert", + "No expired search trails found to clear": "Keine abgelaufenen Suchverläufe zum Löschen gefunden", + "Missing message": "Fehlende Nachricht", + "message content is required": "Nachrichteninhalt ist erforderlich", + "Missing conversation or agentUuid": "Fehlende Unterhaltung oder agentUuid", + "Access denied": "Zugriff verweigert", + "Conversation not found": "Unterhaltung nicht gefunden", + "AI service not configured": "KI-Dienst nicht konfiguriert", + "Failed to process message": "Nachricht konnte nicht verarbeitet werden", + "Missing conversationId": "Fehlende conversationId", + "conversationId is required": "conversationId ist erforderlich", + "Failed to fetch conversation history": "Unterhaltungsverlauf konnte nicht abgerufen werden", + "Failed to clear conversation": "Unterhaltung konnte nicht gelöscht werden", + "Invalid feedback type": "Ungültiger Feedback-Typ", + "type must be \"positive\" or \"negative\"": "type muss \"positive\" oder \"negative\" sein", + "Message not found": "Nachricht nicht gefunden", + "Failed to save feedback": "Feedback konnte nicht gespeichert werden", + "Failed to get chat statistics": "Chat-Statistiken konnten nicht abgerufen werden", + "Not Found": "Nicht gefunden", + "Engine not found": "Engine nicht gefunden", + "Consumer not found": "Consumer nicht gefunden", + "Backend parameter is required": "Backend-Parameter ist erforderlich", + "Failed to get database information: %s": "Datenbankinformationen konnten nicht abgerufen werden: %s", + "SOLR setup error: %s": "SOLR-Einrichtungsfehler: %s", + "Reindex failed: %s": "Neuindizierung fehlgeschlagen: %s", + "Anonymized": "Anonymisiert", + "Entities detected": "Entitäten erkannt", + "Extract Now": "Jetzt extrahieren", + "Extracted at": "Extrahiert am", + "Extraction": "Extraktion", + "Failed to load extraction data": "Extraktionsdaten konnten nicht geladen werden", + "Failed to load register data": "Registerdaten konnten nicht geladen werden", + "Failed to retrieve extraction status.": "Extraktionsstatus konnte nicht abgerufen werden.", + "Failed to retrieve objects for file.": "Objekte für die Datei konnten nicht abgerufen werden.", + "No extraction data available for this file": "Für diese Datei sind keine Extraktionsdaten verfügbar", + "No register objects reference this file": "Keine Registerobjekte verweisen auf diese Datei", + "Not extracted": "Nicht extrahiert", + "Register Objects": "Registerobjekte", + "Risk level": "Risikostufe", + "Text chunks": "Text-Chunks", + "Unknown error": "Unbekannter Fehler", + "Very high": "Sehr hoch", + "{title} in {register} / {schema}": "{title} in {register} / {schema}", + "Push Notifications": "Push-Benachrichtigungen", + "Real-time push notification status via notify_push": "Echtzeit-Push-Benachrichtigungsstatus über notify_push", + "Realtime push not available — the notify_push app is not installed": "Echtzeit-Push nicht verfügbar – die App notify_push ist nicht installiert", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installieren Sie die App notify_push aus dem Nextcloud App Store, um Echtzeit-Aktualisierungen zu aktivieren.", + "Open Nextcloud App Store": "Nextcloud App Store öffnen", + "notify_push is installed but not yet active": "notify_push ist installiert, aber noch nicht aktiv", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push ist installiert, aber OpenRegister hat noch keinen erfolgreichen Push bestätigt. Lösen Sie eine Objektspeicherung aus, um es zu aktivieren, oder überprüfen Sie Ihre notify_push-Konfiguration.", + "notify_push configuration guide": "notify_push-Konfigurationsanleitung", + "Realtime push active": "Echtzeit-Push aktiv", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Echtzeit-Push-Benachrichtigungen sind aktiv. Verbundene Clients erhalten sofortige Aktualisierungen, wenn Objekte erstellt, aktualisiert oder gelöscht werden.", + "(Dolphin required)": "(Dolphin erforderlich)", + "({count} selected)": "({count} ausgewählt)", + ", {failed} failed": ", {failed} fehlgeschlagen", + "0 = unlimited": "0 = unbegrenzt", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ WARNUNG VOR DAUERHAFTER LÖSCHUNG ⚠️\n\nSie sind dabei, ALLE Objekte für das Schema \"{schema}\" DAUERHAFT zu löschen:\n\n• Aktive Objekte: {active}\n• Soft-gelöschte Objekte: {deleted}\n• Gesamt: {total}\n\nDiese Objekte werden vollständig aus der Datenbank entfernt und KÖNNEN NICHT wiederhergestellt werden.\n\nSind Sie absolut sicher?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ WARNUNG VOR DAUERHAFTER LÖSCHUNG ⚠️\n\nSie sind dabei, {count} soft-gelöschte Objekte für das Schema \"{schema}\" DAUERHAFT zu löschen.\n\nDiese Objekte werden vollständig aus der Datenbank entfernt und KÖNNEN NICHT wiederhergestellt werden.\n\nSind Sie absolut sicher?", + "A deactivation request is pending.": "Eine Deaktivierungsanfrage ist ausstehend.", + "Access organisation dashboard and analytics": "Auf Organisations-Dashboard und Analysen zugreifen", + "Account": "Konto", + "Active organisation changed successfully": "Aktive Organisation erfolgreich geändert", + "Active Organisation:": "Aktive Organisation:", + "Activity": "Aktivität", + "Add": "Hinzufügen", + "Add a file or files": "Eine oder mehrere Dateien hinzufügen", + "Add Attachment": "Anhang hinzufügen", + "Add endpoint": "Endpunkt hinzufügen", + "Add oneOf entry": "oneOf-Eintrag hinzufügen", + "Add Property to '{title}'": "Eigenschaft zu '{title}' hinzufügen", + "Add range": "Bereich hinzufügen", + "Add Register": "Register hinzufügen", + "Add User": "Benutzer hinzufügen", + "Add User to Organisation": "Benutzer zur Organisation hinzufügen", + "Addresses": "Adressen", + "Agent successfully updated": "Agent erfolgreich aktualisiert", + "Agent Type": "Agententyp", + "Aggregated across schemas": "Über Schemata aggregiert", + "All file types": "Alle Dateitypen", + "All-day events": "Ganztägige Ereignisse", + "Allowed MIME Types": "Zulässige MIME-Typen", + "Already linked": "Bereits verknüpft", + "An error occurred while deleting the object": "Beim Löschen des Objekts ist ein Fehler aufgetreten", + "An error occurred while restoring the objects": "Beim Wiederherstellen der Objekte ist ein Fehler aufgetreten", + "An error occurred while saving the organisation": "Beim Speichern der Organisation ist ein Fehler aufgetreten", + "and add the files there.": "und fügen Sie die Dateien dort hinzu.", + "Anonymous Group": "Anonyme Gruppe", + "API Request Quota (requests/day)": "API-Anfrage-Kontingent (Anfragen/Tag)", + "API Token Configuration": "API-Token-Konfiguration", + "API Tokens": "API-Tokens", + "App ID": "App-ID", + "Application identifier for this configuration (optional)": "Anwendungskennung für diese Konfiguration (optional)", + "Application settings": "Anwendungseinstellungen", + "Application successfully created": "Anwendung erfolgreich erstellt", + "Application successfully updated": "Anwendung erfolgreich aktualisiert", + "Apply": "Anwenden", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Wenden Sie eine Rolle auf alle Schemata in diesem Register an, die keine expliziten Autorisierungs-Überschreibungen haben.", + "Approval Progress": "Genehmigungsfortschritt", + "Approve": "Genehmigen", + "Approved": "Genehmigt", + "approved": "genehmigt", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Sind Sie sicher, dass Sie alle Daten aus der Sammlung \"{name}\" löschen möchten?\n\nDies wird:\n• Alle indizierten Dokumente löschen\n• Die Sammlungsstruktur intakt lassen\n• Diese Aktion kann nicht rückgängig gemacht werden", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Sind Sie sicher, dass Sie die Sammlung \"{name}\" LÖSCHEN möchten?\n\nDies wird:\n• Die Sammlung und alle ihre Daten dauerhaft löschen\n• Alle indizierten Dokumente entfernen\n• Diese Aktion kann nicht rückgängig gemacht werden", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Sind Sie sicher, dass Sie die Anwendung \"{name}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are you sure you want to delete this configset?": "Sind Sie sicher, dass Sie dieses ConfigSet löschen möchten?", + "Are you sure you want to leave '{name}'?": "Sind Sie sicher, dass Sie '{name}' verlassen möchten?", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Sind Sie sicher, dass Sie die Sammlung \"{name}\" neu indizieren möchten?\n\nDies wird:\n• Den Index mit allen Objekten neu aufbauen\n• Mehrere Minuten dauern\n• Die Suchleistung während der Neuindizierung beeinträchtigen können", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Sind Sie sicher, dass Sie das Schema \"{schema}\" aus dem Register \"{register}\" entfernen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Sind Sie sicher, dass Sie {count} aktive Objekte für das Schema \"{schema}\" soft-löschen möchten?\n\nSie werden als gelöscht markiert, können aber später dauerhaft entfernt werden.", + "Array Object Configuration:": "Array-Objektkonfiguration:", + "Assigned collaborative tags": "Zugewiesene kollaborative Tags", + "Assignment notifications": "Zuweisungsbenachrichtigungen", + "Auto (e.g. Y for year, F Y for month)": "Auto (z. B. Y für Jahr, F Y für Monat)", + "Auto share": "Automatisch teilen", + "Automatically connect to the federation network.": "Automatisch mit dem Föderationsnetzwerk verbinden.", + "Automatically update catalogues.": "Kataloge automatisch aktualisieren.", + "Available placeholders:": "Verfügbare Platzhalter:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Avatar-Änderungen werden von Ihrem Authentifizierungsanbieter nicht unterstützt.", + "Avatar removed": "Avatar entfernt", + "Avatar updated successfully": "Avatar erfolgreich aktualisiert", + "Bandwidth Quota (MB/month)": "Bandbreitenkontingent (MB/Monat)", + "Behavior": "Verhalten", + "Bulk Role Assignment: {title}": "Massen-Rollenzuweisung: {title}", + "Cache grew from {old} to {new} entries.": "Der Cache ist von {old} auf {new} Einträge gewachsen.", + "Cache shrunk from {old} to {new} entries.": "Der Cache ist von {old} auf {new} Einträge geschrumpft.", + "Cache stayed the same at {size} entries.": "Der Cache ist bei {size} Einträgen gleich geblieben.", + "Calendar": "Kalender", + "Calendar name": "Kalendername", + "Calendar Provider Configuration": "Kalenderanbieter-Konfiguration", + "Cancel deactivation request": "Deaktivierungsanfrage abbrechen", + "Cannot edit: This register is managed by external configuration {title}": "Bearbeitung nicht möglich: Dieses Register wird durch die externe Konfiguration {title} verwaltet", + "Cannot remove schema with existing objects ({count} objects)": "Schema mit vorhandenen Objekten kann nicht entfernt werden ({count} Objekte)", + "Cannot remove schema {schema} because it contains {count} objects": "Schema {schema} kann nicht entfernt werden, da es {count} Objekte enthält", + "Cards": "Karten", + "Cascade delete": "Kaskadierendes Löschen", + "Change password": "Passwort ändern", + "Changes for Audit Trail #{id}": "Änderungen für Audit-Trail #{id}", + "Chat test failed: {error}": "Chat-Test fehlgeschlagen: {error}", + "Cleanup completed successfully. Deleted {count} entries.": "Bereinigung erfolgreich abgeschlossen. {count} Einträge gelöscht.", + "Cleanup failed: {error}": "Bereinigung fehlgeschlagen: {error}", + "Clear filters": "Filter zurücksetzen", + "Cluster name": "Cluster-Name", + "Collapse details": "Details einklappen", + "Color": "Farbe", + "Comment...": "Kommentar …", + "completed": "abgeschlossen", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Komplexe Abfragen: erweiterte Suchen mit mehreren Filtern, Operatoren und komplexen Parameterkombinationen", + "Confidence: {confidence}%": "Konfidenz: {confidence}%", + "Configuration is up to date": "Konfiguration ist auf dem neuesten Stand", + "Configuration saved successfully.": "Konfiguration erfolgreich gespeichert.", + "Configuration type (default, application, etc.)": "Konfigurationstyp (Standard, Anwendung usw.)", + "Configure Apache SOLR search engine for advanced search capabilities": "Apache-SOLR-Suchmaschine für erweiterte Suchfunktionen konfigurieren", + "Configure API tokens for external service integrations": "API-Tokens für externe Dienstintegrationen konfigurieren", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurieren Sie grundlegende Verbindungseinstellungen für Ihren SOLR-Server einschließlich Authentifizierung und Netzwerkoptionen. Verwenden Sie die separaten ConfigSet- und Sammlungsverwaltungsdialoge, um Cores und Sammlungen zu verwalten.", + "Configure default organisation and organisation-related settings": "Standardorganisation und organisationsbezogene Einstellungen konfigurieren", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurieren Sie Anbieter für große Sprachmodelle (LLM) für KI-gestützte Funktionen einschließlich semantischer Suche, Embeddings und Chat.", + "Configure multi-organization support and tenant isolation": "Unterstützung für mehrere Organisationen und Mandantenisolation konfigurieren", + "Configure SOLR Facets": "SOLR-Facetten konfigurieren", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurieren Sie dieses Schema, um Objekte als Ereignisse in der Nextcloud-Kalender-App anzuzeigen.", + "Confirm Account Deactivation": "Kontodeaktivierung bestätigen", + "Confirm deactivation": "Deaktivierung bestätigen", + "Confirm Object Deletion": "Objektlöschung bestätigen", + "Confirm Object Restoration": "Objektwiederherstellung bestätigen", + "Confirm Permanent Object Deletion": "Dauerhafte Objektlöschung bestätigen", + "Contracts": "Verträge", + "Conversation started with {agent}": "Unterhaltung mit {agent} begonnen", + "Conversations": "Unterhaltungen", + "Converting {schema} to blob storage...": "{schema} wird in Blob-Speicher umgewandelt …", + "Converting {schema} to magic table...": "{schema} wird in Magic-Table umgewandelt …", + "Copy of {name}": "Kopie von {name}", + "Copy to clipboard": "In die Zwischenablage kopieren", + "create": "erstellen", + "Create a new configset based on the _default template": "Ein neues ConfigSet basierend auf der _default-Vorlage erstellen", + "Create a new SOLR collection from an existing configset": "Eine neue SOLR-Sammlung aus einem bestehenden ConfigSet erstellen", + "Create another": "Weiteres erstellen", + "Create API Token": "API-Token erstellen", + "Create copies of": "Kopien erstellen von", + "Create new token": "Neues Token erstellen", + "Create Organisation": "Organisation erstellen", + "Current": "Aktuell", + "Current password": "Aktuelles Passwort", + "Custom display title for this facet": "Benutzerdefinierter Anzeigetitel für diese Facette", + "Custom HTTP headers (one per line, format: header-name: value)": "Benutzerdefinierte HTTP-Header (einer pro Zeile, Format: Header-Name: Wert)", + "Custom pattern:": "Benutzerdefiniertes Muster:", + "Custom Ranges:": "Benutzerdefinierte Bereiche:", + "Data": "Daten", + "Data Sources": "Datenquellen", + "Data Views": "Datenansichten", + "Date Faceting:": "Datums-Facettierung:", + "Dates": "Daten", + "Deactivation request cancelled": "Deaktivierungsanfrage abgebrochen", + "Deactivation request submitted": "Deaktivierungsanfrage eingereicht", + "Default Object Tenant": "Standard-Objektmandant", + "Default Owner": "Standardeigentümer", + "Default User (for cron/background jobs)": "Standardbenutzer (für Cron-/Hintergrundaufträge)", + "Default User Tenant": "Standard-Benutzermandant", + "Default value": "Standardwert", + "Default value (Email)": "Standardwert (E-Mail)", + "Default value (Password)": "Standardwert (Passwort)", + "Default value (Phone number)": "Standardwert (Telefonnummer)", + "Default value (Regex)": "Standardwert (Regex)", + "Defaults to current user. Select a different user if needed.": "Standardmäßig der aktuelle Benutzer. Wählen Sie bei Bedarf einen anderen Benutzer aus.", + "delete": "löschen", + "Delete ({count})": "Löschen ({count})", + "DELETE COLLECTION": "SAMMLUNG LÖSCHEN", + "Delete SOLR Collection": "SOLR-Sammlung löschen", + "deleted objects section": "Bereich für gelöschte Objekte", + "Deleting SOLR Collection...": "SOLR-Sammlung wird gelöscht …", + "Depublish": "Depublizieren", + "Description shown as tooltip": "Als Tooltip angezeigte Beschreibung", + "Description Template": "Beschreibungsvorlage", + "Detected: {date}": "Erkannt: {date}", + "Direct permission": "Direkte Berechtigung", + "Disable this to make the agent": "Deaktivieren Sie dies, um den Agenten", + "Display Format": "Anzeigeformat", + "Display Name": "Anzeigename", + "Dolphin OCR enabled": "Dolphin-OCR aktiviert", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Laden Sie eine Kopie aller Ihrer in OpenRegister gespeicherten personenbezogenen Daten herunter (DSGVO Artikel 20).", + "Download API Specification": "API-Spezifikation herunterladen", + "Drag a file or files here": "Eine oder mehrere Dateien hierher ziehen", + "Dry run completed: {count} fields would be created": "Probelauf abgeschlossen: {count} Felder würden erstellt werden", + "Dry run completed: {count} fields would be fixed": "Probelauf abgeschlossen: {count} Felder würden korrigiert werden", + "Duration in seconds (optional)": "Dauer in Sekunden (optional)", + "e.g., lib/settings/config.json": "z. B. lib/settings/config.json", + "e.g., lib/settings/register.json": "z. B. lib/settings/register.json", + "Edit Agent": "Agent bearbeiten", + "Edit Application": "Anwendung bearbeiten", + "Edit Configuration": "Konfiguration bearbeiten", + "Edit Endpoint": "Endpunkt bearbeiten", + "Edit labels": "Labels bearbeiten", + "Edit Organisation": "Organisation bearbeiten", + "Edit Property '{key}' of '{title}'": "Eigenschaft '{key}' von '{title}' bearbeiten", + "Edit Source": "Quelle bearbeiten", + "Edit tags for {name}": "Tags für {name} bearbeiten", + "Email Addresses": "E-Mail-Adressen", + "Email digest frequency": "Häufigkeit der E-Mail-Zusammenfassung", + "Embedding test failed: {error}": "Embedding-Test fehlgeschlagen: {error}", + "Enable calendar provider": "Kalenderanbieter aktivieren", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Aktivieren oder deaktivieren Sie LLM-Funktionen. Konfigurieren Sie Anbieter und Modelle über die Schaltfläche zur LLM-Konfiguration oben.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Aktivieren oder deaktivieren Sie die SOLR-Suchintegration. Konfigurieren Sie die Verbindungseinstellungen über die Schaltfläche für Verbindungseinstellungen oben.", + "Enable RAG": "RAG aktivieren", + "Enable tools that allow the agent to interact with data through function calling.": "Aktivieren Sie Werkzeuge, die es dem Agenten ermöglichen, über Function Calling mit Daten zu interagieren.", + "Enable write-back to target objects": "Rückschreiben auf Zielobjekte aktivieren", + "End Date Field": "Enddatumsfeld", + "Endpoint Path*": "Endpunktpfad*", + "Enter agent description (optional)": "Agentenbeschreibung eingeben (optional)", + "Enter agent name": "Agentennamen eingeben", + "Enter application description (optional)": "Anwendungsbeschreibung eingeben (optional)", + "Enter application name": "Anwendungsnamen eingeben", + "Enter configuration description (optional)": "Konfigurationsbeschreibung eingeben (optional)", + "Enter configuration title": "Konfigurationstitel eingeben", + "Enter Nextcloud usernames to grant access to this private agent": "Geben Sie Nextcloud-Benutzernamen ein, um Zugriff auf diesen privaten Agenten zu gewähren", + "Enter organisation description (optional)": "Organisationsbeschreibung eingeben (optional)", + "Enter organisation name": "Organisationsnamen eingeben", + "Enter search terms or leave empty to browse all": "Suchbegriffe eingeben oder leer lassen, um alle zu durchsuchen", + "Enter system prompt for the agent": "System-Prompt für den Agenten eingeben", + "Enter username and press Enter": "Benutzernamen eingeben und Enter drücken", + "Enter your API key": "Geben Sie Ihren API-Schlüssel ein", + "Enter your object here...": "Geben Sie hier Ihr Objekt ein …", + "Enter your schema here...": "Geben Sie hier Ihr Schema ein …", + "Entity ID: {id}": "Entitäts-ID: {id}", + "Entity Recognition": "Entitätserkennung", + "Entity Recognition Method": "Methode zur Entitätserkennung", + "Error deleting audit trails: {error}": "Fehler beim Löschen von Audit-Trails: {error}", + "Error deleting search trails: {error}": "Fehler beim Löschen von Suchverläufen: {error}", + "Example": "Beispiel", + "Example value for this property": "Beispielwert für diese Eigenschaft", + "Exclusive maximum": "Exklusives Maximum", + "Exclusive minimum": "Exklusives Minimum", + "execute": "ausführen", + "Expiration": "Ablauf", + "Expires": "Läuft ab", + "Expires in (e.g., 90d)": "Läuft ab in (z. B. 90d)", + "Export \"{schema}\" objects from \"{register}\"": "\"{schema}\"-Objekte aus \"{register}\" exportieren", + "Export Configuration": "Konfiguration exportieren", + "Export downloaded successfully": "Export erfolgreich heruntergeladen", + "Export failed: {error}": "Export fehlgeschlagen: {error}", + "Export Format:": "Exportformat:", + "Export is rate limited. Please try again later.": "Der Export ist ratenbegrenzt. Bitte versuchen Sie es später erneut.", + "Export my data": "Meine Daten exportieren", + "Export Objects": "Objekte exportieren", + "Exporting...": "Wird exportiert …", + "Extra Query Parameters": "Zusätzliche Abfrageparameter", + "Extract now": "Jetzt extrahieren", + "Extraction error for {file}: {error}": "Extraktionsfehler für {file}: {error}", + "Extraction Mode": "Extraktionsmodus", + "Extraction Scope": "Extraktionsumfang", + "Facet Description": "Facettenbeschreibung", + "Facet Order": "Facettenreihenfolge", + "Facet Title": "Facettentitel", + "Facet Type": "Facettentyp", + "Facetable": "Facettierbar", + "Faceting Configuration:": "Facettierungskonfiguration:", + "Failed to add user to organisation": "Benutzer konnte nicht zur Organisation hinzugefügt werden", + "Failed to cancel deactivation": "Deaktivierung konnte nicht abgebrochen werden", + "Failed to change active organisation: {error}": "Aktive Organisation konnte nicht geändert werden: {error}", + "Failed to change password": "Passwort konnte nicht geändert werden", + "Failed to check version: {error}": "Version konnte nicht überprüft werden: {error}", + "Failed to clear embeddings: {error}": "Embeddings konnten nicht gelöscht werden: {error}", + "Failed to copy to clipboard": "Kopieren in die Zwischenablage fehlgeschlagen", + "Failed to copy token": "Token konnte nicht kopiert werden", + "Failed to create missing SOLR fields: {error}": "Fehlende SOLR-Felder konnten nicht erstellt werden: {error}", + "Failed to create token": "Token konnte nicht erstellt werden", + "Failed to delete objects for {schema}: {error}": "Objekte für {schema} konnten nicht gelöscht werden: {error}", + "Failed to delete SOLR collection: {error}": "SOLR-Sammlung konnte nicht gelöscht werden: {error}", + "Failed to depublish register: {error}": "Register konnte nicht depubliziert werden: {error}", + "Failed to depublish schema: {error}": "Schema konnte nicht depubliziert werden: {error}", + "Failed to depublish: {error}": "Depublizieren fehlgeschlagen: {error}", + "Failed to export data": "Daten konnten nicht exportiert werden", + "Failed to export objects": "Objekte konnten nicht exportiert werden", + "Failed to fix mismatched SOLR fields: {error}": "Nicht übereinstimmende SOLR-Felder konnten nicht korrigiert werden: {error}", + "Failed to get response: {error}": "Antwort konnte nicht abgerufen werden: {error}", + "Failed to leave organisation: {error}": "Organisation konnte nicht verlassen werden: {error}", + "Failed to link object": "Objekt konnte nicht verknüpft werden", + "Failed to load view: {error}": "Ansicht konnte nicht geladen werden: {error}", + "Failed to publish register: {error}": "Register konnte nicht veröffentlicht werden: {error}", + "Failed to publish schema: {error}": "Schema konnte nicht veröffentlicht werden: {error}", + "Failed to publish: {error}": "Veröffentlichen fehlgeschlagen: {error}", + "Failed to remove avatar": "Avatar konnte nicht entfernt werden", + "Failed to remove link": "Verknüpfung konnte nicht entfernt werden", + "Failed to remove member: {error}": "Mitglied konnte nicht entfernt werden: {error}", + "Failed to remove schema {schema}: {error}": "Schema {schema} konnte nicht entfernt werden: {error}", + "Failed to remove user from organisation": "Benutzer konnte nicht aus der Organisation entfernt werden", + "Failed to request deactivation": "Deaktivierung konnte nicht angefordert werden", + "Failed to revoke token": "Token konnte nicht widerrufen werden", + "Failed to save configuration.": "Konfiguration konnte nicht gespeichert werden.", + "Failed to save configuration: {error}": "Konfiguration konnte nicht gespeichert werden: {error}", + "Failed to save facet configuration": "Facettenkonfiguration konnte nicht gespeichert werden", + "Failed to save preferences": "Einstellungen konnten nicht gespeichert werden", + "Failed to save view: {error}": "Ansicht konnte nicht gespeichert werden: {error}", + "Failed to set active organisation: {error}": "Aktive Organisation konnte nicht festgelegt werden: {error}", + "Failed to start file warmup: {error}": "Datei-Vorwärmung konnte nicht gestartet werden: {error}", + "Failed to start vectorization: {error}": "Vektorisierung konnte nicht gestartet werden: {error}", + "Failed to switch organisation: {error}": "Organisation konnte nicht gewechselt werden: {error}", + "Failed to sync magic table for {schema}: {error}": "Magic-Table für {schema} konnte nicht synchronisiert werden: {error}", + "Failed to update schema configuration for {schema}: {error}": "Schemakonfiguration für {schema} konnte nicht aktualisiert werden: {error}", + "Failed to update view: {error}": "Ansicht konnte nicht aktualisiert werden: {error}", + "Failed to upload avatar": "Avatar konnte nicht hochgeladen werden", + "Failed to validate {schema}: {error}": "{schema} konnte nicht validiert werden: {error}", + "Failed to vectorize files: {error}": "Dateien konnten nicht vektorisiert werden: {error}", + "Failed to warmup names cache: {error}": "Namens-Cache konnte nicht vorgewärmt werden: {error}", + "Federation": "Föderation", + "Fields to Display": "Anzuzeigende Felder", + "File #{id}": "Datei #{id}", + "File Handling": "Dateiverarbeitung", + "File Location": "Dateispeicherort", + "File name": "Dateiname", + "File Types to Process": "Zu verarbeitende Dateitypen", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Dateivektorisierung abgeschlossen! {vectorized} Chunks aus {files} Dateien vektorisiert. {failed} fehlgeschlagen.", + "Files added successfully": "Dateien erfolgreich hinzugefügt", + "Files → filecollection, objects → objectcollection": "Dateien → filecollection, Objekte → objectcollection", + "file{plural}": "Datei{plural}", + "Filter by type": "Nach Typ filtern", + "Filter fields...": "Felder filtern …", + "folder": "Ordner", + "From (e.g. -7 days or 2025-01-01)": "Von (z. B. -7 days oder 2025-01-01)", + "Function Tools": "Funktionswerkzeuge", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Erhalten Sie Echtzeit-Einblicke in den Datenzustand, die Wachstumstrends und die Qualitätsmetriken Ihrer Organisation.", + "GitHub Personal Access Token": "Persönlicher GitHub-Zugriffstoken", + "GitHub token is valid! Username: {username}": "GitHub-Token ist gültig! Benutzername: {username}", + "GitHub token test failed: {message}": "GitHub-Token-Test fehlgeschlagen: {message}", + "GitLab Personal Access Token": "Persönlicher GitLab-Zugriffstoken", + "GitLab token is valid! Username: {username}": "GitLab-Token ist gültig! Benutzername: {username}", + "GitLab token test failed: {message}": "GitLab-Token-Test fehlgeschlagen: {message}", + "Go to linked Register": "Zum verknüpften Register gehen", + "Go to linked Schema": "Zum verknüpften Schema gehen", + "Go to organisation": "Zur Organisation gehen", + "Grant additional permissions beyond standard CRUD operations": "Zusätzliche Berechtigungen über die standardmäßigen CRUD-Operationen hinaus gewähren", + "Group Quota": "Gruppenkontingent", + "Groups": "Gruppen", + "Here you can configure the details for various connections.": "Hier können Sie die Details für verschiedene Verbindungen konfigurieren.", + "Here you can configure the details for your organisation.": "Hier können Sie die Details für Ihre Organisation konfigurieren.", + "Hide": "Ausblenden", + "Hide in collection view": "In der Sammlungsansicht ausblenden", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "IBANs": "IBANs", + "ID of input mapping (optional)": "ID des Eingabe-Mappings (optional)", + "ID of output mapping (optional)": "ID des Ausgabe-Mappings (optional)", + "ID of the target resource": "ID der Zielressource", + "ID: {id}": "ID: {id}", + "Import Configuration": "Konfiguration importieren", + "Import Data into Register": "Daten in Register importieren", + "in {register}": "in {register}", + "in_progress": "in Bearbeitung", + "Inactive organisations cannot be used": "Inaktive Organisationen können nicht verwendet werden", + "Include ids and names of related objects for better contextual search": "IDs und Namen verwandter Objekte für eine bessere kontextbezogene Suche einbeziehen", + "Index": "Index", + "Information about the current OpenRegister installation": "Informationen über die aktuelle OpenRegister-Installation", + "inherited": "geerbt", + "Inherited from register": "Vom Register geerbt", + "Inherits permissions from register": "Erbt Berechtigungen vom Register", + "Input Mapping": "Eingabe-Mapping", + "Inspect SOLR Index": "SOLR-Index untersuchen", + "Internet location (URL) of this installation": "Internetadresse (URL) dieser Installation", + "Interval": "Intervall", + "Invite Users": "Benutzer einladen", + "Invited Users": "Eingeladene Benutzer", + "Key": "Schlüssel", + "Label": "Label", + "Labels": "Labels", + "Leave empty to allow all users access": "Leer lassen, um allen Benutzern Zugriff zu gewähren", + "Leave off for auto-detection from property format.": "Weglassen für die automatische Erkennung anhand des Eigenschaftsformats.", + "Left organisation successfully": "Organisation erfolgreich verlassen", + "Link": "Verknüpfen", + "Link an object to see it here.": "Verknüpfen Sie ein Objekt, um es hier zu sehen.", + "Link another object": "Weiteres Objekt verknüpfen", + "Link removed": "Verknüpfung entfernt", + "Link to Object": "Mit Objekt verknüpfen", + "Link to {name}": "Mit {name} verknüpfen", + "Linked by {user}": "Verknüpft von {user}", + "Linked Objects": "Verknüpfte Objekte", + "Linked to {name}": "Verknüpft mit {name}", + "list": "auflisten", + "Load more": "Mehr laden", + "Loading activity...": "Aktivität wird geladen …", + "Loading API tokens...": "API-Tokens werden geladen …", + "Loading available tools...": "Verfügbare Werkzeuge werden geladen …", + "Loading cache statistics...": "Cache-Statistiken werden geladen …", + "Loading endpoints": "Endpunkte werden geladen", + "Loading entities...": "Entitäten werden geladen …", + "Loading linked objects...": "Verknüpfte Objekte werden geladen …", + "Loading LLM configuration...": "LLM-Konfiguration wird geladen …", + "Loading multitenancy settings...": "Mandantenfähigkeitseinstellungen werden geladen …", + "Loading n8n configuration...": "n8n-Konfiguration wird geladen …", + "Loading permission matrix...": "Berechtigungsmatrix wird geladen …", + "Loading preferences...": "Einstellungen werden geladen …", + "Loading RBAC settings...": "RBAC-Einstellungen werden geladen …", + "Loading register data...": "Registerdaten werden geladen …", + "Loading retention settings...": "Aufbewahrungseinstellungen werden geladen …", + "Loading search configuration...": "Suchkonfiguration wird geladen …", + "Loading text extraction settings...": "Textextraktionseinstellungen werden geladen …", + "Loading tokens...": "Tokens werden geladen …", + "Loading user groups...": "Benutzergruppen werden geladen …", + "Location": "Standort", + "Location Field": "Standortfeld", + "Locations": "Standorte", + "log{plural}": "Protokoll{plural}", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Niedrigere Zahlen erscheinen zuerst in der Filter-Seitenleiste. Leer lassen für automatische Reihenfolge.", + "Magic table sync completed for {schema}": "Magic-Table-Synchronisierung für {schema} abgeschlossen", + "Mail Integration": "Mail-Integration", + "Make this installation discoverable within the federation network.": "Diese Installation im Föderationsnetzwerk auffindbar machen.", + "manage": "verwalten", + "Manage SOLR collections (data stores) and assign them for objects and files.": "SOLR-Sammlungen (Datenspeicher) verwalten und sie Objekten und Dateien zuweisen.", + "Manage SOLR configsets (configuration templates) for your collections.": "SOLR-ConfigSets (Konfigurationsvorlagen) für Ihre Sammlungen verwalten.", + "Manage your account settings, security, and personal data.": "Verwalten Sie Ihre Kontoeinstellungen, Sicherheit und personenbezogenen Daten.", + "Manage your chat conversations": "Verwalten Sie Ihre Chat-Unterhaltungen", + "Manage your organisations and switch between them": "Verwalten Sie Ihre Organisationen und wechseln Sie zwischen ihnen", + "Mappings": "Mappings", + "Max Tokens": "Max. Tokens", + "Maximum File Size (MB)": "Maximale Dateigröße (MB)", + "Maximum length": "Maximale Länge", + "Maximum number of items": "Maximale Anzahl von Elementen", + "Maximum value": "Maximalwert", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Mittlere Abfragen: Suchen mit etwas Filterung oder mehreren Parametern (z. B. Datumsbereiche, bestimmte Register/Schemata)", + "Member removed successfully": "Mitglied erfolgreich entfernt", + "Members": "Mitglieder", + "Members ({count})": "Mitglieder ({count})", + "Metadata": "Metadaten", + "Method*": "Methode*", + "Method: {method}": "Methode: {method}", + "Minimum length": "Minimale Länge", + "Minimum number of items": "Minimale Anzahl von Elementen", + "Minimum value": "Minimalwert", + "Multiple of": "Vielfaches von", + "Multitenancy": "Mandantenfähigkeit", + "My Account": "Mein Konto", + "myapp": "myapp", + "n8n connection test failed: {message}": "n8n-Verbindungstest fehlgeschlagen: {message}", + "n8n initialization failed: {message}": "n8n-Initialisierung fehlgeschlagen: {message}", + "Name *": "Name *", + "Name*": "Name*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Namens-Cache erfolgreich vorgewärmt: {count} Namen in {time} geladen. {cache}", + "Naming pattern for copies:": "Benennungsmuster für Kopien:", + "New password": "Neues Passwort", + "New User Group": "Neue Benutzergruppe", + "Nextcloud Groups": "Nextcloud-Gruppen", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Keine aktiven Objekte zum Soft-Löschen für Schema {schema}. Verwenden Sie \"Dauerhaft löschen\", um soft-gelöschte Objekte zu entfernen.", + "No activity found.": "Keine Aktivität gefunden.", + "No API tokens.": "Keine API-Tokens.", + "No approval steps for this object.": "Keine Genehmigungsschritte für dieses Objekt.", + "No configuration selected": "Keine Konfiguration ausgewählt", + "No endpoints defined": "Keine Endpunkte definiert", + "No entities detected for this email.": "Für diese E-Mail wurden keine Entitäten erkannt.", + "No files selected": "Keine Dateien ausgewählt", + "No groups found. Try a different search.": "Keine Gruppen gefunden. Versuchen Sie eine andere Suche.", + "No label": "Kein Label", + "No labels": "Keine Labels", + "No linked objects": "Keine verknüpften Objekte", + "No objects are currently selected for deletion.": "Derzeit sind keine Objekte zum Löschen ausgewählt.", + "No objects are currently selected for permanent deletion.": "Derzeit sind keine Objekte zur dauerhaften Löschung ausgewählt.", + "No objects are currently selected for restoration.": "Derzeit sind keine Objekte zur Wiederherstellung ausgewählt.", + "No objects linked to this email": "Keine Objekte mit dieser E-Mail verknüpft", + "No objects selected": "Keine Objekte ausgewählt", + "No objects selected for restoration": "Keine Objekte zur Wiederherstellung ausgewählt", + "No objects to delete for schema {schema}": "Keine zu löschenden Objekte für Schema {schema}", + "No organisations found": "Keine Organisationen gefunden", + "No registers found. Create a register to configure permissions.": "Keine Register gefunden. Erstellen Sie ein Register, um Berechtigungen zu konfigurieren.", + "No related cases found for this sender": "Keine zugehörigen Fälle für diesen Absender gefunden", + "No schemas configured for mail linking.": "Keine Schemata für die Mail-Verknüpfung konfiguriert.", + "No synchronizations found": "Keine Synchronisierungen gefunden", + "No tools available. Tools can be registered by installed apps.": "Keine Werkzeuge verfügbar. Werkzeuge können von installierten Apps registriert werden.", + "No users in this organisation.": "Keine Benutzer in dieser Organisation.", + "Note:": "Hinweis:", + "Notifications": "Benachrichtigungen", + "Number of Sources": "Anzahl der Quellen", + "Object #{id}": "Objekt #{id}", + "Object changes in owned objects": "Objektänderungen in eigenen Objekten", + "Object Configuration:": "Objektkonfiguration:", + "Object Extraction Mode": "Objekt-Extraktionsmodus", + "Object successfully modified": "Objekt erfolgreich geändert", + "Object Text Extraction": "Objekt-Textextraktion", + "Objects deletion completed for {schema}": "Objektlöschung für {schema} abgeschlossen", + "Objects will be soft deleted and moved to the": "Objekte werden soft-gelöscht und verschoben in den", + "objectType: object\naction: created": "objectType: object\naction: created", + "object{plural}": "Objekt{plural}", + "Only members of selected groups can access this organisation": "Nur Mitglieder ausgewählter Gruppen können auf diese Organisation zugreifen", + "Open folder": "Ordner öffnen", + "Open in OpenRegister": "In OpenRegister öffnen", + "Open OpenRegister sidebar": "OpenRegister-Seitenleiste öffnen", + "OpenRegister": "OpenRegister", + "Optional description for this facet": "Optionale Beschreibung für diese Facette", + "Optional end date property": "Optionale Enddatum-Eigenschaft", + "Optional event description template": "Optionale Ereignisbeschreibungsvorlage", + "Optional location property": "Optionale Standort-Eigenschaft", + "Optional URL-friendly identifier": "Optionale URL-freundliche Kennung", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Optional: Fügen Sie Abfrageparameter hinzu, um das referenzierte Schema zu filtern (z. B. status=active&type=public)", + "Or": "Oder", + "Organisation Configuration": "Organisationskonfiguration", + "Organisation membership changes": "Änderungen der Organisationsmitgliedschaft", + "Organisation name is required": "Organisationsname ist erforderlich", + "Organisation successfully updated": "Organisation erfolgreich aktualisiert", + "Organizations": "Organisationen", + "Other": "Sonstiges", + "Output Mapping": "Ausgabe-Mapping", + "Overview": "Übersicht", + "Page {current} of {total}": "Seite {current} von {total}", + "Password changes are not supported by your authentication provider.": "Passwortänderungen werden von Ihrem Authentifizierungsanbieter nicht unterstützt.", + "Password updated successfully": "Passwort erfolgreich aktualisiert", + "pending": "ausstehend", + "Permanently Delete ({count})": "Dauerhaft löschen ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Alle {count} soft-gelöschten Objekte dauerhaft löschen. Dies kann nicht rückgängig gemacht werden!", + "Permission Matrix": "Berechtigungsmatrix", + "Personal Data Export": "Export personenbezogener Daten", + "Persons": "Personen", + "Phone Numbers": "Telefonnummern", + "Please select a user": "Bitte wählen Sie einen Benutzer aus", + "Please select an organisation": "Bitte wählen Sie eine Organisation aus", + "Please select at least one item to import": "Bitte wählen Sie mindestens ein Element zum Importieren aus", + "Please select registers or schemas first": "Bitte wählen Sie zuerst Register oder Schemata aus", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Bitte warten Sie, während wir die SOLR-Sammlung dauerhaft löschen. Dies kann einen Moment dauern.", + "Preferences saved": "Einstellungen gespeichert", + "Press enter to send, shift+enter for new line": "Enter drücken zum Senden, Umschalt+Enter für eine neue Zeile", + "Prettify": "Verschönern", + "Preview:": "Vorschau:", + "Private Agent (Default)": "Privater Agent (Standard)", + "Private agents": "Private Agenten", + "Process Name (optional)": "Prozessname (optional)", + "Processed {indexed} files, {failed} failed": "{indexed} Dateien verarbeitet, {failed} fehlgeschlagen", + "Property could not be updated": "Eigenschaft konnte nicht aktualisiert werden", + "Property name of inversed relation": "Eigenschaftsname der invertierten Beziehung", + "Property successfully updated": "Eigenschaft erfolgreich aktualisiert", + "Publish Configuration to GitHub": "Konfiguration auf GitHub veröffentlichen", + "Publish OAS": "OAS veröffentlichen", + "Publish objects to make them publicly available": "Objekte veröffentlichen, um sie öffentlich verfügbar zu machen", + "Publish Register OAS to GitHub": "Register-OAS auf GitHub veröffentlichen", + "Query Help": "Abfragehilfe", + "Quota": "Kontingent", + "RBAC": "RBAC", + "read": "lesen", + "Reason": "Grund", + "Reason (optional)": "Grund (optional)", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Schema", + "Register and schema are required": "Register und Schema sind erforderlich", + "Register Overview": "Registerübersicht", + "Register:": "Register:", + "register{plural}": "Register{plural}", + "Reindex completed! Processed {count} objects in {duration}s": "Neuindizierung abgeschlossen! {count} Objekte in {duration}s verarbeitet", + "Reject": "Ablehnen", + "Rejected": "Abgelehnt", + "rejected": "abgelehnt", + "Related Cases": "Zugehörige Fälle", + "Remove avatar": "Avatar entfernen", + "Remove from list": "Aus Liste entfernen", + "Remove link": "Verknüpfung entfernen", + "Remove link to {name}": "Verknüpfung mit {name} entfernen", + "Remove link to {name}?": "Verknüpfung mit {name} entfernen?", + "Remove link to {title}": "Verknüpfung mit {title} entfernen", + "Remove oneOf entry": "oneOf-Eintrag entfernen", + "Remove property after write-back": "Eigenschaft nach Rückschreiben entfernen", + "Remove {title}": "{title} entfernen", + "Remove {userId} from this organisation?": "{userId} aus dieser Organisation entfernen?", + "Removing schema {schema}...": "Schema {schema} wird entfernt …", + "Request account deactivation": "Kontodeaktivierung anfordern", + "Request account deactivation. This will notify administrators for review.": "Kontodeaktivierung anfordern. Dies benachrichtigt Administratoren zur Überprüfung.", + "Request Quota (per day)": "Anfrage-Kontingent (pro Tag)", + "Requested at": "Angefordert am", + "Required": "Erforderlich", + "Required Token Scopes": "Erforderliche Token-Scopes", + "Requires Dolphin AI for OCR text extraction": "Erfordert Dolphin AI für die OCR-Textextraktion", + "Rerunning search: {searchTerm}": "Suche wird erneut ausgeführt: {searchTerm}", + "Resource Quotas": "Ressourcenkontingente", + "Retry upload": "Hochladen wiederholen", + "Review the selected objects below. You can remove any objects you don't want to include.": "Überprüfen Sie die unten ausgewählten Objekte. Sie können alle Objekte entfernen, die Sie nicht einbeziehen möchten.", + "Revoke": "Widerrufen", + "Right": "Recht", + "Roles and Permissions": "Rollen und Berechtigungen", + "Save labels": "Labels speichern", + "Save tags for {name}": "Tags für {name} speichern", + "Save the organisation first to manage users.": "Speichern Sie zuerst die Organisation, um Benutzer zu verwalten.", + "Schema configuration updated successfully for {schema}": "Schemakonfiguration für {schema} erfolgreich aktualisiert", + "Schema reference ($ref)": "Schemareferenz ($ref)", + "Schema {schema} removed successfully": "Schema {schema} erfolgreich entfernt", + "Schema:": "Schema:", + "schema{plural}": "Schema{plural}", + "Search": "Suchen", + "Search / views": "Suche / Ansichten", + "Search agents...": "Agenten suchen …", + "Search applications...": "Anwendungen suchen …", + "Search by title or UUID...": "Nach Titel oder UUID suchen …", + "Search Configuration": "Suchkonfiguration", + "Search configurations": "Konfigurationen durchsuchen", + "Search data sources...": "Datenquellen suchen …", + "Search groups...": "Gruppen suchen …", + "Search in Database Objects": "In Datenbankobjekten suchen", + "Search Mode": "Suchmodus", + "Search objects": "Objekte suchen", + "Search objects...": "Objekte suchen …", + "Search Query": "Suchabfrage", + "Search registers...": "Register suchen …", + "Search Results": "Suchergebnisse", + "Search schemas...": "Schemata suchen …", + "Search {name}...": "{name} suchen …", + "Searching...": "Wird gesucht …", + "Security": "Sicherheit", + "See organisations as a table": "Organisationen als Tabelle anzeigen", + "See organisations as cards": "Organisationen als Karten anzeigen", + "Select a date property": "Eine Datums-Eigenschaft auswählen", + "Select a register": "Ein Register auswählen", + "Select a register...": "Ein Register auswählen …", + "Select a schema": "Ein Schema auswählen", + "Select a schema...": "Ein Schema auswählen …", + "Select a user": "Einen Benutzer auswählen", + "Select Active Organisation": "Aktive Organisation auswählen", + "Select agent type": "Agententyp auswählen", + "Select an email to see linked objects": "Wählen Sie eine E-Mail aus, um verknüpfte Objekte zu sehen", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Wählen Sie eine Organisation und einen Benutzer aus, um diesen als Mitglied hinzuzufügen. Suchen Sie Organisationen nach Namen.", + "Select configuration type...": "Konfigurationstyp auswählen …", + "Select fields to display": "Anzuzeigende Felder auswählen", + "Select files with the correct extension": "Dateien mit der richtigen Erweiterung auswählen", + "Select group": "Gruppe auswählen", + "Select groups (optional)": "Gruppen auswählen (optional)", + "Select groups to notify...": "Zu benachrichtigende Gruppen auswählen …", + "Select groups with access to this agent": "Gruppen mit Zugriff auf diesen Agenten auswählen", + "Select groups...": "Gruppen auswählen …", + "Select one or more registers": "Ein oder mehrere Register auswählen", + "Select one or more schemas": "Ein oder mehrere Schemata auswählen", + "Select or create labels, or select \"No label\" to add files": "Labels auswählen oder erstellen oder \"Kein Label\" auswählen, um Dateien hinzuzufügen", + "Select owner application (optional)...": "Eigentümeranwendung auswählen (optional) …", + "Select role": "Rolle auswählen", + "Select schemas (empty = all schemas)": "Schemata auswählen (leer = alle Schemata)", + "Select schemas to warm up": "Vorzuwärmende Schemata auswählen", + "Select search mode": "Suchmodus auswählen", + "Select source type...": "Quelltyp auswählen …", + "Select target type": "Zieltyp auswählen", + "Select views to filter data (optional)": "Ansichten auswählen, um Daten zu filtern (optional)", + "Select views to limit which data the agent can access": "Ansichten auswählen, um einzuschränken, auf welche Daten der Agent zugreifen kann", + "Select {fieldName} values": "{fieldName}-Werte auswählen", + "Selected Objects ({count})": "Ausgewählte Objekte ({count})", + "Send as cloudevent": "Als CloudEvent senden", + "Set as Active": "Als aktiv festlegen", + "Set as active organisation": "Als aktive Organisation festlegen", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Legen Sie Limits für die API-Nutzung und den Token-Verbrauch fest. Verwenden Sie 0 für unbegrenzte Ressourcen.", + "Showing {showing} of {total} agents": "{showing} von {total} Agenten werden angezeigt", + "Showing {showing} of {total} applications": "{showing} von {total} Anwendungen werden angezeigt", + "Showing {showing} of {total} audit trail entries": "{showing} von {total} Audit-Trail-Einträgen werden angezeigt", + "Showing {showing} of {total} configurations": "{showing} von {total} Konfigurationen werden angezeigt", + "Showing {showing} of {total} deleted items": "{showing} von {total} gelöschten Elementen werden angezeigt", + "Showing {showing} of {total} entities": "{showing} von {total} Entitäten werden angezeigt", + "Showing {showing} of {total} files": "{showing} von {total} Dateien werden angezeigt", + "Showing {showing} of {total} log entries": "{showing} von {total} Protokolleinträgen werden angezeigt", + "Showing {showing} of {total} search trail entries": "{showing} von {total} Suchverlaufseinträgen werden angezeigt", + "Showing {showing} of {total} sources": "{showing} von {total} Quellen werden angezeigt", + "Showing {showing} of {total} templates": "{showing} von {total} Vorlagen werden angezeigt", + "Showing {showing} of {total} webhooks": "{showing} von {total} Webhooks werden angezeigt", + "Showing {shown} of {total} organisations": "{shown} von {total} Organisationen werden angezeigt", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Einfache Abfragen: grundlegende Textsuchen mit minimalen Parametern (z. B. einzelner Suchbegriff, keine erweiterten Filter)", + "SOLR collection deleted successfully": "SOLR-Sammlung erfolgreich gelöscht", + "SOLR Index Warmup": "SOLR-Index-Vorwärmung", + "Something went wrong while adding files": "Beim Hinzufügen von Dateien ist etwas schiefgelaufen", + "Source successfully updated": "Quelle erfolgreich aktualisiert", + "Special Rights": "Sonderrechte", + "Start Date Field": "Startdatumsfeld", + "Starting validation for {schema}...": "Validierung für {schema} wird gestartet …", + "Starting {action} for {schema}...": "{action} für {schema} wird gestartet …", + "Statistics and Metrics": "Statistiken und Metriken", + "Statistics refreshed": "Statistiken aktualisiert", + "Step": "Schritt", + "Storage": "Speicher", + "Storage Quota (MB)": "Speicherkontingent (MB)", + "Successfully added user to organisation: {name}": "Benutzer erfolgreich zur Organisation hinzugefügt: {name}", + "Successfully created {count} SOLR fields": "{count} SOLR-Felder erfolgreich erstellt", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "{count} Embeddings erfolgreich gelöscht. Bitte vektorisieren Sie Ihre Daten erneut.", + "Successfully deleted {count} objects for {schema}": "{count} Objekte für {schema} erfolgreich gelöscht", + "Successfully fixed {count} SOLR field configurations": "{count} SOLR-Feldkonfigurationen erfolgreich korrigiert", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Erfolgreich importiert: {registers} Register, {schemas} Schemata, {objects} Objekte", + "Successfully permanently deleted {count} objects": "{count} Objekte erfolgreich dauerhaft gelöscht", + "Successfully processed {count} files!": "{count} Dateien erfolgreich verarbeitet!", + "Successfully saved configuration for {count} facets!": "Konfiguration für {count} Facetten erfolgreich gespeichert!", + "Supported File Types": "Unterstützte Dateitypen", + "Switch Active Organisation": "Aktive Organisation wechseln", + "Switch Organisation": "Organisation wechseln", + "Syncing magic table for {schema}...": "Magic-Table für {schema} wird synchronisiert …", + "Syncs": "Synchronisierungen", + "System announcements": "Systemankündigungen", + "System Prompt": "System-Prompt", + "Table": "Tabelle", + "Target ID": "Ziel-ID", + "Target Type*": "Zieltyp*", + "Text Chunk #{id}": "Text-Chunk #{id}", + "Text Extraction": "Textextraktion", + "Text Extraction Engine": "Textextraktions-Engine", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Sie werden gemäß der für ihr Schema konfigurierten Aufbewahrungsfrist aufbewahrt und anschließend dauerhaft entfernt.", + "This action cannot be undone. Make sure no collections are using this configset.": "Diese Aktion kann nicht rückgängig gemacht werden. Stellen Sie sicher, dass keine Sammlungen dieses ConfigSet verwenden.", + "This action will submit a deactivation request to your administrators.": "Diese Aktion sendet eine Deaktivierungsanfrage an Ihre Administratoren.", + "This is not valid JSON": "Dies ist kein gültiges JSON", + "This key already exists on this schema": "Dieser Schlüssel existiert bereits in diesem Schema", + "This schema must use magic table configuration to sync": "Dieses Schema muss die Magic-Table-Konfiguration verwenden, um zu synchronisieren", + "This token will only be shown once. Copy it now.": "Dieses Token wird nur einmal angezeigt. Kopieren Sie es jetzt.", + "This will copy the _default configset with the new name": "Dies kopiert das _default-ConfigSet mit dem neuen Namen", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Dies löscht ALLE Embeddings (Vektoren) dauerhaft aus der Datenbank. Sie müssen alle Objekte und Dateien erneut vektorisieren. Diese Aktion kann nicht rückgängig gemacht werden.\n\nSind Sie sicher, dass Sie fortfahren möchten?", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Dies entfernt {count} bereits soft-gelöschte Objekte dauerhaft aus der Datenbank. Diese Aktion kann nicht rückgängig gemacht werden.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tipp: Aktivieren Sie nur Ansichten, die eine semantische Suche benötigen, um Embedding-Kosten zu minimieren. Einfache Nachschlagetabellen benötigen selten eine Vektorisierung.", + "Title *": "Titel *", + "Title Template": "Titelvorlage", + "Title*": "Titel*", + "To (e.g. -1 year or 2025-12-31)": "Bis (z. B. -1 year oder 2025-12-31)", + "To add files larger than or equal to 512MB, go to the": "Um Dateien mit einer Größe von mindestens 512 MB hinzuzufügen, gehen Sie zu", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Um die Chunk-Größe oder -Strategie anzupassen, gehen Sie zur Dateikonfiguration → Verarbeitungslimits.", + "To change the embedding provider or model, go to LLM configuration.": "Um den Embedding-Anbieter oder das Modell zu ändern, gehen Sie zur LLM-Konfiguration.", + "Token copied to clipboard": "Token in die Zwischenablage kopiert", + "Token Created": "Token erstellt", + "Token name": "Token-Name", + "Token Quota (per request)": "Token-Kontingent (pro Anfrage)", + "Token revoked": "Token widerrufen", + "Tools respect the agent's own role and organization boundaries.": "Werkzeuge respektieren die eigene Rolle und die Organisationsgrenzen des Agenten.", + "Try searching by UUID or with different keywords": "Versuchen Sie, nach UUID oder mit anderen Schlüsselwörtern zu suchen", + "Type \"DELETE COLLECTION\" to confirm:": "Geben Sie \"SAMMLUNG LÖSCHEN\" ein, um zu bestätigen:", + "Type to search for objects...": "Tippen, um nach Objekten zu suchen …", + "Type to search for organisations": "Tippen, um nach Organisationen zu suchen", + "Type to search...": "Tippen, um zu suchen …", + "Type your username to confirm": "Geben Sie Ihren Benutzernamen ein, um zu bestätigen", + "Unknown Object": "Unbekanntes Objekt", + "Unnamed Object": "Unbenanntes Objekt", + "update": "aktualisieren", + "Update available: {local} → {remote}": "Update verfügbar: {local} → {remote}", + "Upload new avatar": "Neuen Avatar hochladen", + "Url": "URL", + "Use AI agents for processing and analysis": "KI-Agenten für Verarbeitung und Analyse verwenden", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Standardbereiche verwenden (Letzte 7/30/90 Tage, Letztes Jahr, Älter)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Externen Speicher (z. B. MongoDB) anstelle des internen Nextcloud-Speichers verwenden.", + "Use Large Language Model features": "Funktionen für große Sprachmodelle verwenden", + "Use SolrCloud with zookeeper for distributed search": "SolrCloud mit Zookeeper für verteilte Suche verwenden", + "Use VNG APIs instead of MongoDB.": "VNG-APIs anstelle von MongoDB verwenden.", + "Used by": "Verwendet von", + "User Quota": "Benutzerkontingent", + "Uses": "Verwendungen", + "UUID copied to clipboard": "UUID in die Zwischenablage kopiert", + "Validation completed for {schema}": "Validierung für {schema} abgeschlossen", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validierung für {schema} abgeschlossen: {processed} verarbeitet, {updated} aktualisiert, {failed} fehlgeschlagen", + "Value list (split on ,)": "Werteliste (getrennt durch ,)", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorisierung abgeschlossen: {vectorized} Objekte vektorisiert, {failed} fehlgeschlagen", + "Vectorization failed: {error}": "Vektorisierung fehlgeschlagen: {error}", + "Version Information": "Versionsinformationen", + "View": "Anzeigen", + "view": "anzeigen", + "View and manage authorization across registers and schemas": "Autorisierung über Register und Schemata hinweg anzeigen und verwalten", + "View API Documentation": "API-Dokumentation anzeigen", + "View APIs": "APIs anzeigen", + "View object": "Objekt anzeigen", + "View {count} more": "{count} weitere anzeigen", + "Visible to end users": "Für Endbenutzer sichtbar", + "waiting": "wartet", + "Warmup failed": "Vorwärmung fehlgeschlagen", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Wenn der Agent ohne Benutzersitzung ausgeführt wird (z. B. geplante Aufgaben), wird der Kontext dieses Benutzers verwendet.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Wenn deaktiviert, zeigt diese Facette nur Werte aus diesem Schema an und enthält bei Auswahl einen Schemafilter.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Wenn aktiviert, werden beim Speichern dieses Objekts auch die Zielobjekte aktualisiert, sodass sie einen Verweis zurück auf dieses Objekt enthalten.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Wenn aktiviert, wird diese Eigenschaft nach der Aktualisierung der Zielobjekte aus dem Quellobjekt entfernt.", + "Workflow Management": "Workflow-Verwaltung", + "Wrap webhook payload in cloudevents format for better interoperability": "Webhook-Payload im CloudEvents-Format verpacken, um die Interoperabilität zu verbessern", + "You are not yet a member of any organisations.": "Sie sind noch kein Mitglied einer Organisation.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Sie können neue ConfigSets basierend auf der _default-Vorlage erstellen oder eigene direkt auf Ihren SOLR-Server hochladen.", + "You do not have permission to view the permission matrix. Admin access is required.": "Sie haben keine Berechtigung, die Berechtigungsmatrix anzuzeigen. Administratorzugriff ist erforderlich.", + "You have selected {count} properties to add to the schema.": "Sie haben {count} Eigenschaften zum Hinzufügen zum Schema ausgewählt.", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Ihr Feedback wurde erfasst. Optional können Sie hier weitere Details angeben …", + "{active} active, {deleted} deleted": "{active} aktiv, {deleted} gelöscht", + "{count} audit trails cleared successfully": "{count} Audit-Trails erfolgreich gelöscht", + "{count} deletions": "{count} Löschungen", + "{count} documents found": "{count} Dokumente gefunden", + "{count} entries": "{count} Einträge", + "{count} issues": "{count} Probleme", + "{count} members": "{count} Mitglieder", + "{count} searches": "{count} Suchen", + "{count} selected": "{count} ausgewählt", + "{minutes} minutes ago": "vor {minutes} Minuten", + "{percentage}% of objects": "{percentage}% der Objekte", + "{property} - {other}": "{property} - {other}", + "ℹ️ current configuration": "ℹ️ aktuelle Konfiguration", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ GEFAHR: Alle {total} Objekte werden DAUERHAFT aus der Datenbank GELÖSCHT. Diese Aktion ist UNWIEDERBRINGLICH und kann nicht rückgängig gemacht werden!", + "{count} compliance issue(s) detected": "{count} Compliance-Problem(e) erkannt", + "{count} email": "{count} E-Mail", + "{count} emails": "{count} E-Mails", + "{count} widget(s)": "{count} Widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} Objekt(e) gefunden. {erased} gelöscht.", + "{type} cache cleared successfully": "{type}-Cache erfolgreich geleert", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F WARNUNG VOR DAUERHAFTER LÖSCHUNG \\u26A0\\uFE0F\n\nSie sind dabei, {count} soft-gelöschte Objekte für das Schema \"{schema}\" DAUERHAFT zu löschen.\n\nDiese Objekte werden vollständig aus der Datenbank entfernt und KÖNNEN NICHT wiederhergestellt werden.\n\nSind Sie absolut sicher?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F WARNUNG VOR DAUERHAFTER LÖSCHUNG \\u26A0\\uFE0F\n\nSie sind dabei, ALLE Objekte für das Schema \"{schema}\" DAUERHAFT zu löschen:\n\n\\u2022 Aktive Objekte: {active}\n\\u2022 Soft-gelöschte Objekte: {deleted}\n\\u2022 Gesamt: {total}\n\nDiese Objekte werden vollständig aus der Datenbank entfernt und KÖNNEN NICHT wiederhergestellt werden.\n\nSind Sie absolut sicher?", + "Add a contact from any of your address books to associate it with this object.": "Fügen Sie einen Kontakt aus einem Ihrer Adressbücher hinzu, um ihn mit diesem Objekt zu verknüpfen.", + "Agent successfully created": "Agent erfolgreich erstellt", + "and accessible to all users in selected groups (or all users if no groups selected).": "und für alle Benutzer in ausgewählten Gruppen zugänglich (oder alle Benutzer, wenn keine Gruppen ausgewählt sind).", + "App store cache invalidated: {invalidated}": "App-Store-Cache ungültig gemacht: {invalidated}", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Diese verwerkingsactiviteit archivieren? Audit-Trail-Zeilen verweisen weiterhin darauf.", + "are only accessible to invited users.": "sind nur für eingeladene Benutzer zugänglich.", + "Attachment added to {name}": "Anhang zu {name} hinzugefügt", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art. 30 §4: Dieser Bericht verknüpft jede Verarbeitungstätigkeit mit den ihr zugeordneten Audit-Trail-Gesamtzahlen über die gesamte Lebensdauer. Prüfer und die Autoriteit Persoonsgegevens nutzen dies für die aufsichtsrechtliche Überprüfung.", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601-Dauer, z. B. P10Y, P30D)", + "by": "von", + "Calendar integration is not available": "Kalenderintegration ist nicht verfügbar", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Klicken Sie auf \"Compliance-Scan ausführen\", um Schemata zu finden, in denen PII erkannt wurde, aber keine Annotation zur Verarbeitungstätigkeit vorhanden ist.", + "Click to subscribe to notifications": "Klicken, um Benachrichtigungen zu abonnieren", + "Click to unsubscribe from notifications": "Klicken, um Benachrichtigungen abzubestellen", + "Code (short readable key, e.g. v-2026-001)": "Code (kurzer lesbarer Schlüssel, z. B. v-2026-001)", + "Contacts integration is not available": "Kontaktintegration ist nicht verfügbar", + "Create a new event or link an existing one from any of your calendars.": "Erstellen Sie ein neues Ereignis oder verknüpfen Sie ein bestehendes aus einem Ihrer Kalender.", + "Create or link a Deck card to track work on this object.": "Erstellen oder verknüpfen Sie eine Deck-Karte, um die Arbeit an diesem Objekt zu verfolgen.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Erstellen Sie die erste verwerkingsactiviteit, um damit zu beginnen, Audit-Trail-Zeilen mit ihrer AVG-Art.-30-Zuordnung zu kennzeichnen.", + "CSV": "CSV", + "Dashboard not found": "Dashboard nicht gefunden", + "Data-subject access request": "Antrag auf Auskunft der betroffenen Person", + "Deck": "Deck", + "Deck integration is not available": "Deck-Integration ist nicht verfügbar", + "Delete {count} object": "{count} Objekt löschen", + "Delete {count} objects": "{count} Objekte löschen", + "Doelbinding *": "Doelbinding *", + "DSAR": "DSAR", + "Edit verwerkingsactiviteit": "verwerkingsactiviteit bearbeiten", + "Erase (Art 17)": "Löschen (Art. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "{count} Objekt(e) für diese betroffene Person löschen? Diese Aktion wird im Audit-Trail protokolliert.", + "Every schema with detected PII has a processing-activity annotation.": "Jedes Schema mit erkannten PII hat eine Annotation zur Verarbeitungstätigkeit.", + "Excel (.xlsx)": "Excel (.xlsx)", + "Failed to add attachment to object": "Anhang konnte nicht zum Objekt hinzugefügt werden", + "Failed to clear {type} cache: {error}": "{type}-Cache konnte nicht geleert werden: {error}", + "Failed to clear audit trails: {error}": "Audit-Trails konnten nicht gelöscht werden: {error}", + "Failed to clear blob objects: {error}": "Blob-Objekte konnten nicht gelöscht werden: {error}", + "Failed to clear cache: {error}": "Cache konnte nicht geleert werden: {error}", + "Failed to clear search trails: {error}": "Suchverläufe konnten nicht gelöscht werden: {error}", + "Failed to discover files: {error}": "Dateien konnten nicht entdeckt werden: {error}", + "Failed to invalidate app store cache: {error}": "App-Store-Cache konnte nicht ungültig gemacht werden: {error}", + "Failed to load cache statistics: {error}": "Cache-Statistiken konnten nicht geladen werden: {error}", + "Failed to load Deck cards": "Deck-Karten konnten nicht geladen werden", + "Failed to load linked contacts": "Verknüpfte Kontakte konnten nicht geladen werden", + "Failed to load linked emails": "Verknüpfte E-Mails konnten nicht geladen werden", + "Failed to load linked events": "Verknüpfte Ereignisse konnten nicht geladen werden", + "Failed to load related entities": "Verwandte Entitäten konnten nicht geladen werden", + "Failed to load settings: {error}": "Einstellungen konnten nicht geladen werden: {error}", + "Failed to load statistics: {error}": "Statistiken konnten nicht geladen werden: {error}", + "Failed to save file settings: {error}": "Dateieinstellungen konnten nicht gespeichert werden: {error}", + "Failed to save LLM settings: {error}": "LLM-Einstellungen konnten nicht gespeichert werden: {error}", + "Failed to save settings: {error}": "Einstellungen konnten nicht gespeichert werden: {error}", + "Failed to save warmup interval: {error}": "Vorwärmintervall konnte nicht gespeichert werden: {error}", + "Failed to setup SOLR: {error}": "SOLR konnte nicht eingerichtet werden: {error}", + "Failed to start processing {type} files: {error}": "Verarbeitung von {type}-Dateien konnte nicht gestartet werden: {error}", + "Failed to test SOLR connection: {error}": "SOLR-Verbindung konnte nicht getestet werden: {error}", + "Failed to update LLM settings: {error}": "LLM-Einstellungen konnten nicht aktualisiert werden: {error}", + "Failed to update Multitenancy settings: {error}": "Mandantenfähigkeitseinstellungen konnten nicht aktualisiert werden: {error}", + "Failed to update RBAC settings: {error}": "RBAC-Einstellungen konnten nicht aktualisiert werden: {error}", + "Failed to update Retention settings: {error}": "Aufbewahrungseinstellungen konnten nicht aktualisiert werden: {error}", + "Failed to update SOLR settings: {error}": "SOLR-Einstellungen konnten nicht aktualisiert werden: {error}", + "File settings saved successfully": "Dateieinstellungen erfolgreich gespeichert", + "Filter and search entities": "Entitäten filtern und durchsuchen", + "Generate report": "Bericht erstellen", + "Generate the verantwoordingsdocument": "verantwoordingsdocument erstellen", + "Generated: {time}": "Erstellt: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Erhalten Sie Echtzeit-Einblicke in den Datenzustand Ihrer Organisation, indem Sie sich auf Register, Schemadefinitionen sowie Objektspeicherung und -nutzung konzentrieren.", + "HTML preview": "HTML-Vorschau", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importieren Sie die Vorlage report-bundle.json, um das Register `reports` zu erhalten, und erstellen Sie dann Ihr erstes Dashboard über die Standard-Objekt-UI. Dashboards deklarieren ihre Widgets in JSON, und der Renderer versorgt jedes Widget mit Live-Aggregationsdaten.", + "Integrations": "Integrationen", + "Inzage (Art 15)": "Auskunft (Art. 15)", + "Inzage results": "Auskunftsergebnisse", + "Languages": "Sprachen", + "Link an email from the Mail app sidebar to associate it with this object.": "Verknüpfen Sie eine E-Mail aus der Seitenleiste der Mail-App, um sie mit diesem Objekt zu verknüpfen.", + "Link existing event": "Bestehendes Ereignis verknüpfen", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Verknüpfte E-Mails, Kalenderereignisse, Kontakte und Deck-Karten werden hier angezeigt.", + "LLM settings saved successfully": "LLM-Einstellungen erfolgreich gespeichert", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Lokalisieren Sie jedes Objekt, das auf eine betroffene Person verweist (Art. 15 inzage), zeigen Sie eine Vorschau einer Löschung an (Art. 17 vergetelheid) oder exportieren Sie deren Daten (Art. 20 portabiliteit).", + "Mail integration is not available": "Mail-Integration ist nicht verfügbar", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Verwalten Sie Verarbeitungstätigkeiten, führen Sie Anträge betroffener Personen auf Auskunft aus und prüfen Sie die Compliance gemäß der EU-DSGVO / der niederländischen AVG.", + "Mass validation completed successfully": "Massenvalidierung erfolgreich abgeschlossen", + "Mass validation failed: {error}": "Massenvalidierung fehlgeschlagen: {error}", + "missing": "fehlt", + "Multitenancy settings updated successfully": "Mandantenfähigkeitseinstellungen erfolgreich aktualisiert", + "Naam": "Naam", + "Naam *": "Naam *", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Namens-Cache erfolgreich vorgewärmt: {count} Namen in {time} geladen. {message}", + "New activity": "Neue Tätigkeit", + "New Configuration": "Neue Konfiguration", + "New verwerkingsactiviteit": "Neue verwerkingsactiviteit", + "No contacts linked to this object": "Keine Kontakte mit diesem Objekt verknüpft", + "No dashboards yet": "Noch keine Dashboards", + "No Deck cards linked to this object": "Keine Deck-Karten mit diesem Objekt verknüpft", + "No emails linked to this object": "Keine E-Mails mit diesem Objekt verknüpft", + "No events linked to this object": "Keine Ereignisse mit diesem Objekt verknüpft", + "No matches": "Keine Übereinstimmungen", + "No personal data was found for this subject identifier.": "Für diese Kennung der betroffenen Person wurden keine personenbezogenen Daten gefunden.", + "No processing activities yet": "Noch keine Verarbeitungstätigkeiten", + "No related entities": "Keine verwandten Entitäten", + "No saved views yet. create one in the search tab!": "Noch keine gespeicherten Ansichten. Erstellen Sie eine im Such-Tab!", + "No scan run yet": "Noch kein Scan ausgeführt", + "Object metadata incomplete for file upload": "Objektmetadaten für den Datei-Upload unvollständig", + "Object successfully deleted": "Objekt erfolgreich gelöscht", + "Objects successfully deleted": "Objekte erfolgreich gelöscht", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Vom Betreiber definierte Dashboards und geplante Berichte. Jedes Dashboard ist ein vollwertiges Objekt im Register `reports`; Widgets werden im `widgets`-Array des Dashboards deklariert und live aus Aggregationen / GraphQL gerendert.", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Geordnete BCP-47-Sprach-Tags. Die erste Sprache ist der Standard des Registers und steuert den Accept-Language-Fallback für übersetzbare Eigenschaften.", + "Organisation successfully created": "Organisation erfolgreich erstellt", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "PDF": "PDF", + "PII hits": "PII-Treffer", + "PII rows": "PII-Zeilen", + "Portabiliteit (Art 20)": "Datenübertragbarkeit (Art. 20)", + "Preview erasure": "Vorschau der Löschung", + "Property could not be added": "Eigenschaft konnte nicht hinzugefügt werden", + "Property successfully added": "Eigenschaft erfolgreich hinzugefügt", + "public": "öffentlich", + "Purge {count} object from database": "{count} Objekt aus der Datenbank entfernen", + "Purge {count} objects from database": "{count} Objekte aus der Datenbank entfernen", + "RBAC settings updated successfully": "RBAC-Einstellungen erfolgreich aktualisiert", + "Rebase operation completed successfully": "Rebase-Vorgang erfolgreich abgeschlossen", + "Rebase operation failed: {error}": "Rebase-Vorgang fehlgeschlagen: {error}", + "Rechtsgrond": "Rechtsgrond", + "Register annotation": "Register-Annotation", + "Register objects": "Registerobjekte", + "Remove contact": "Kontakt entfernen", + "Remove Deck card": "Deck-Karte entfernen", + "Reports": "Berichte", + "Restore {count} object": "{count} Objekt wiederherstellen", + "Restore {count} objects": "{count} Objekte wiederherstellen", + "Retention settings updated successfully": "Aufbewahrungseinstellungen erfolgreich aktualisiert", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Überprüfen Sie die unten ausgewählten Objekte. Sie können alle Objekte, die Sie nicht löschen möchten, durch Klicken auf die Schaltfläche zum Entfernen entfernen.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Überprüfen Sie die unten ausgewählten Objekte. Sie können alle Objekte, die Sie nicht dauerhaft löschen möchten, durch Klicken auf die Schaltfläche zum Entfernen entfernen. Diese Aktion kann nicht rückgängig gemacht werden.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Überprüfen Sie die unten ausgewählten Objekte. Sie können alle Objekte, die Sie nicht wiederherstellen möchten, durch Klicken auf die Schaltfläche zum Entfernen entfernen. Objekte werden an ihrem ursprünglichen Speicherort wiederhergestellt.", + "Run compliance scan": "Compliance-Scan ausführen", + "Scheduled": "Geplant", + "Schema annotation": "Schema-Annotation", + "selected objects": "ausgewählte Objekte", + "set": "festlegen", + "Settings updated successfully": "Einstellungen erfolgreich aktualisiert", + "Show": "Anzeigen", + "Showing {count} dashboard(s)": "{count} Dashboard(s) werden angezeigt", + "Showing {count} processing activities": "{count} Verarbeitungstätigkeiten werden angezeigt", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Alle Objekte für dieses Schema soft-löschen ({active} aktiv, {deleted} bereits gelöscht)", + "SOLR connection test failed: {error}": "SOLR-Verbindungstest fehlgeschlagen: {error}", + "SOLR connection test successful!": "SOLR-Verbindungstest erfolgreich!", + "SOLR index warmup completed successfully": "SOLR-Index-Vorwärmung erfolgreich abgeschlossen", + "SOLR settings updated successfully": "SOLR-Einstellungen erfolgreich aktualisiert", + "SOLR setup completed successfully!": "SOLR-Einrichtung erfolgreich abgeschlossen!", + "SOLR setup failed: {error}": "SOLR-Einrichtung fehlgeschlagen: {error}", + "SOLR warmup failed: {error}": "SOLR-Vorwärmung fehlgeschlagen: {error}", + "Subject identifier (email, BSN, name, etc.)": "Kennung der betroffenen Person (E-Mail, BSN, Name usw.)", + "Subscribe": "Abonnieren", + "Subscribed": "Abonniert", + "Successfully cleared {count} audit trails": "{count} Audit-Trails erfolgreich gelöscht", + "Successfully cleared {count} blob storage objects": "{count} Blob-Speicherobjekte erfolgreich gelöscht", + "Successfully cleared {count} search trails": "{count} Suchverläufe erfolgreich gelöscht", + "Successfully restored {count} object": "{count} Objekt erfolgreich wiederhergestellt", + "Successfully restored {count} objects": "{count} Objekte erfolgreich wiederhergestellt", + "Technical Details": "Technische Details", + "Technische maatregelen": "Technische maatregelen", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Die Nextcloud-Kalender-App ist auf diesem Server nicht installiert oder aktiviert.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Die Nextcloud-Kontakte-App ist auf diesem Server nicht installiert oder aktiviert.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Die Nextcloud-Deck-App ist auf diesem Server nicht installiert oder aktiviert.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Die Nextcloud-Mail-App ist auf diesem Server nicht installiert oder aktiviert.", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Das angeforderte Dashboard konnte nicht geladen werden. Überprüfen Sie die URL oder wählen Sie ein anderes Dashboard aus der Liste.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Sie werden gemäß der für ihr Schema konfigurierten Aufbewahrungsfrist aufbewahrt und automatisch dauerhaft gelöscht, wenn die Aufbewahrungsfrist abläuft. Die Aufbewahrungsfrist ist pro Schema konfigurierbar und in den Einstellungen des Schemas zu finden.", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Werkzeuge werden mit den Standardbenutzerberechtigungen des Agenten ausgeführt, wenn keine Benutzersitzung aktiv ist (z. B. Cron-Jobs). Konfigurieren Sie den Standardbenutzer im Einstellungen-Tab.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Werkzeuge respektieren die Ansichten, Berechtigungen und Organisationsgrenzen des Agenten.", + "Total events": "Ereignisse insgesamt", + "Type filter (optional, e.g. email)": "Typfilter (optional, z. B. E-Mail)", + "Unknown widget type: {type}": "Unbekannter Widget-Typ: {type}", + "Unlink email": "E-Mail-Verknüpfung aufheben", + "Unlink event": "Ereignisverknüpfung aufheben", + "Untitled": "Unbenannt", + "Use filters to narrow down entities by type or category.": "Verwenden Sie Filter, um Entitäten nach Typ oder Kategorie einzugrenzen.", + "Verantwoording": "Verantwoording", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Warnung: Diese Eigenschaft hat inkonsistente Typen: {types}. Erwägen Sie, ob alle Objekte denselben Typ haben sollten.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Wir scannen alle zu diesem Schema gehörenden Objekte, um neue Eigenschaften zu entdecken und vorhandene Eigenschaften auf potenzielle Verbesserungen zu analysieren. Der Prozess umfasst die Untersuchung der Datenstruktur jedes Objekts, die Identifizierung von Eigenschaften, die im aktuellen Schema nicht definiert sind, und das Finden von Möglichkeiten, vorhandene Eigenschaftsdefinitionen mit besseren Einschränkungen, Formaten und Validierungsregeln zu verbessern.", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Wenn der Agent ohne Benutzersitzung ausgeführt wird (z. B. geplante Aufgaben), wird der Kontext dieses Benutzers verwendet", + "Workflows": "Workflows", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "Bookmarks": "Bookmarks", + "Categories of data subjects (one per line)": "Categories of data subjects (one per line)", + "Categories of personal data (one per line)": "Categories of personal data (one per line)", + "Configuring field mappings...": "Configuring field mappings...", + "Connecting to SOLR cluster...": "Connecting to SOLR cluster...", + "Creating search collections...": "Creating search collections...", + "Edit processing activity": "Edit processing activity", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Finalizing setup...": "Finalizing setup...", + "Forbidden": "Forbidden", + "Initializing SOLR setup...": "Initializing SOLR setup...", + "Legal basis *": "Legal basis *", + "New processing activity": "New processing activity", + "Optimizing search performance...": "Optimizing search performance...", + "Organisational measures": "Organisational measures", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Purpose limitation *": "Purpose limitation *", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Retention period (ISO-8601 duration, e.g. P10Y, P30D)", + "SOLR setup failed. Please check the configuration and try again.": "SOLR setup failed. Please check the configuration and try again.", + "Select all entities": "Select all entities", + "Select entity {value}": "Select entity {value}", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "Technical measures": "Technical measures", + "Uploading configuration sets...": "Uploading configuration sets...", + "Verifying server connectivity...": "Verifying server connectivity...", + "Waiting for cluster synchronization...": "Waiting for cluster synchronization...", + "You do not have access to this object": "You do not have access to this object", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR can handle millions of documents with sub-second search response times...", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Faceted search allows users to drill down into results by categories...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR provides rich analytics and statistics about search performance...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets contain the schema and configuration files for your search index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replication ensures your search index is available even if nodes fail...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Auto-scaling can dynamically add or remove nodes based on load...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Security features include authentication, authorization, and SSL encryption...", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} E-Mail", + "{count} E-Mails" + ], + "Purge {count} object from database": [ + "{count} Objekt aus der Datenbank bereinigen", + "{count} Objekte aus der Datenbank bereinigen" + ], + "Restore {count} object": [ + "{count} Objekt wiederherstellen", + "{count} Objekte wiederherstellen" + ], + "Successfully restored {count} object": [ + "{count} Objekt erfolgreich wiederhergestellt", + "{count} Objekte erfolgreich wiederhergestellt" + ], + "Delete {count} object": [ + "{count} Objekt löschen", + "{count} Objekte löschen" + ], + "Object successfully deleted": [ + "Objekt erfolgreich gelöscht", + "Objekte erfolgreich gelöscht" + ] + } +} diff --git a/l10n/el.js b/l10n/el.js new file mode 100644 index 0000000000..9226722e0a --- /dev/null +++ b/l10n/el.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(χωρίς θέμα)", + "(no title)" : "(χωρίς τίτλο)", + "(unnamed)" : "(χωρίς όνομα)", + "(untitled card)" : "(κάρτα χωρίς τίτλο)", + "Activities" : "Δραστηριότητες", + "Add Endpoint" : "Προσθήκη τελικού σημείου", + "Add Source" : "Προσθήκη πηγής", + "Add card" : "Προσθήκη κάρτας", + "Add contact" : "Προσθήκη επαφής", + "All clear" : "Όλα εντάξει", + "Articles" : "Άρθρα", + "Code" : "Κωδικός", + "Compliance" : "Συμμόρφωση", + "Contacts" : "Επαφές", + "Create Agent" : "Δημιουργία πράκτορα", + "Create Application" : "Δημιουργία εφαρμογής", + "Create event" : "Δημιουργία συμβάντος", + "Erasure complete" : "Η διαγραφή ολοκληρώθηκε", + "Erasure preview" : "Προεπισκόπηση διαγραφής", + "Expand details" : "Ανάπτυξη λεπτομερειών", + "Save changes" : "Αποθήκευση αλλαγών", + "📄 Object Serialization" : "📄 Σειριοποίηση αντικειμένου", + "🔢 Vectorization Settings" : "🔢 Ρυθμίσεις διανυσματοποίησης", + "💰 View Selection (Cost Optimization)" : "💰 Επιλογή προβολών (βελτιστοποίηση κόστους)", + "AI Assistant" : "Βοηθός τεχνητής νοημοσύνης", + "AI Chat" : "Συνομιλία τεχνητής νοημοσύνης", + "API Key" : "Κλειδί API", + "API tokens saved successfully" : "Τα διακριτικά API αποθηκεύτηκαν με επιτυχία", + "About ConfigSets" : "Σχετικά με τα ConfigSets", + "Action" : "Ενέργεια", + "Action Distribution" : "Κατανομή ενεργειών", + "Action:" : "Ενέργεια:", + "Actions" : "Ενέργειες", + "Active" : "Ενεργό", + "Active Collections" : "Ενεργές συλλογές", + "Active Filters" : "Ενεργά φίλτρα", + "Active Organisations" : "Ενεργοί οργανισμοί", + "Active filters:" : "Ενεργά φίλτρα:", + "Active organisation set successfully" : "Ο ενεργός οργανισμός ορίστηκε με επιτυχία", + "Activity Period" : "Περίοδος δραστηριότητας", + "Add Groups" : "Προσθήκη ομάδων", + "Add Schema" : "Προσθήκη σχήματος", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Προσθέστε τίτλους σχημάτων, περιγραφές και πληροφορίες μητρώου για να παρέχετε πλουσιότερο πλαίσιο στην αναζήτηση", + "Add to favorites" : "Προσθήκη στα αγαπημένα", + "Added" : "Προστέθηκε", + "Added to favorites" : "Προστέθηκε στα αγαπημένα", + "Additional Information" : "Πρόσθετες πληροφορίες", + "Additional feedback saved. Thank you!" : "Τα πρόσθετα σχόλια αποθηκεύτηκαν. Σας ευχαριστούμε!", + "Admin" : "Διαχειριστής", + "Advanced" : "Για προχωρημένους", + "Advanced Filters" : "Φίλτρα για προχωρημένους", + "Advanced Options" : "Επιλογές για προχωρημένους", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Τα φίλτρα για προχωρημένους δεν είναι διαθέσιμα όταν χρησιμοποιείται πηγή βάσης δεδομένων. Μεταβείτε σε Αυτόματο ή Ευρετήριο SOLR για επιλογές φιλτραρίσματος.", + "After" : "Μετά", + "Agent deleted successfully" : "Ο πράκτορας διαγράφηκε με επιτυχία", + "Agents" : "Πράκτορες", + "All" : "Όλα", + "All Categories" : "Όλες οι κατηγορίες", + "All Collections" : "Όλες οι συλλογές", + "All Confidence Levels" : "Όλα τα επίπεδα εμπιστοσύνης", + "All ConfigSets" : "Όλα τα ConfigSets", + "All Files" : "Όλα τα αρχεία", + "All Levels" : "Όλα τα επίπεδα", + "All Types" : "Όλοι οι τύποι", + "All Webhooks" : "Όλα τα webhooks", + "All actions" : "Όλες οι ενέργειες", + "All audit trails cleared successfully" : "Όλα τα ίχνη ελέγχου εκκαθαρίστηκαν με επιτυχία", + "All registers" : "Όλα τα μητρώα", + "All schemas" : "Όλα τα σχήματα", + "All searches" : "Όλες οι αναζητήσεις", + "All users" : "Όλοι οι χρήστες", + "An error occurred while clearing audit trails" : "Παρουσιάστηκε σφάλμα κατά την εκκαθάριση των ιχνών ελέγχου", + "An error occurred while deleting the audit trail" : "Παρουσιάστηκε σφάλμα κατά τη διαγραφή του ίχνους ελέγχου", + "An error occurred while deleting the view" : "Παρουσιάστηκε σφάλμα κατά τη διαγραφή της προβολής", + "An error occurred while permanently deleting the objects" : "Παρουσιάστηκε σφάλμα κατά την οριστική διαγραφή των αντικειμένων", + "Analysis completed:" : "Η ανάλυση ολοκληρώθηκε:", + "Analysis steps:" : "Βήματα ανάλυσης:", + "Analytics" : "Αναλυτικά στοιχεία", + "Analyze Objects" : "Ανάλυση αντικειμένων", + "Analyze Properties" : "Ανάλυση ιδιοτήτων", + "Analyze existing properties for improvement opportunities" : "Αναλύστε υπάρχουσες ιδιότητες για ευκαιρίες βελτίωσης", + "Analyzing..." : "Ανάλυση...", + "Any favorites and sharing settings for this view" : "Τυχόν αγαπημένα και ρυθμίσεις κοινής χρήσης για αυτήν την προβολή", + "Any user" : "Οποιοσδήποτε χρήστης", + "App store cache invalidated successfully" : "Η προσωρινή μνήμη του καταστήματος εφαρμογών ακυρώθηκε με επιτυχία", + "Application deleted successfully" : "Η εφαρμογή διαγράφηκε με επιτυχία", + "Applications" : "Εφαρμογές", + "Apply Changes" : "Εφαρμογή αλλαγών", + "Archive" : "Αρχειοθέτηση", + "Archive conversation" : "Αρχειοθέτηση συνομιλίας", + "Archived conversations are hidden from your active list" : "Οι αρχειοθετημένες συνομιλίες αποκρύπτονται από την ενεργή σας λίστα", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Είστε βέβαιοι ότι θέλετε να εκκαθαρίσετε τα παλιά ίχνη αναζήτησης; Αυτό θα διαγράψει τις καταχωρήσεις που είναι παλαιότερες από 30 ημέρες.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα ίχνη ελέγχου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα ίχνη αναζήτησης; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Are you sure you want to delete this ConfigSet?" : "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το ConfigSet;", + "Are you sure you want to permanently delete" : "Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά", + "Ask a question..." : "Κάντε μια ερώτηση...", + "Ask questions about your data using natural language" : "Κάντε ερωτήσεις σχετικά με τα δεδομένα σας χρησιμοποιώντας φυσική γλώσσα", + "Attempt" : "Προσπάθεια", + "Audit Trail Changes" : "Αλλαγές ίχνους ελέγχου", + "Audit Trail Details" : "Λεπτομέρειες ίχνους ελέγχου", + "Audit Trail Management" : "Διαχείριση ίχνους ελέγχου", + "Audit Trail Statistics" : "Στατιστικά ίχνους ελέγχου", + "Audit Trails" : "Ίχνη ελέγχου", + "Audit trail data copied to clipboard" : "Τα δεδομένα ίχνους ελέγχου αντιγράφηκαν στο πρόχειρο", + "Audit trail deleted successfully" : "Το ίχνος ελέγχου διαγράφηκε με επιτυχία", + "Audit trail successfully deleted" : "Το ίχνος ελέγχου διαγράφηκε με επιτυχία", + "Authentication" : "Έλεγχος ταυτότητας", + "Auto-commit disabled" : "Η αυτόματη υποβολή απενεργοποιήθηκε", + "Auto-commit enabled" : "Η αυτόματη υποβολή ενεργοποιήθηκε", + "Auto-create Default Organisation" : "Αυτόματη δημιουργία προεπιλεγμένου οργανισμού", + "Auto-retry failed vectorizations" : "Αυτόματη επανάληψη αποτυχημένων διανυσματοποιήσεων", + "Automatically commit changes to SOLR index" : "Αυτόματη υποβολή αλλαγών στο ευρετήριο SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Αυτόματη δημιουργία προεπιλεγμένου οργανισμού εάν δεν υπάρχει κανένας κατά την αρχικοποίηση της εφαρμογής", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Αυτόματη δημιουργία διανυσματικών ενσωματώσεων από τμήματα κειμένου όταν τα αρχεία μεταφορτώνονται και επεξεργάζονται", + "Automatically generate vector embeddings when objects are created or updated" : "Αυτόματη δημιουργία διανυσματικών ενσωματώσεων όταν τα αντικείμενα δημιουργούνται ή ενημερώνονται", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Αυτόματη επανάληψη αποτυχημένων προσπαθειών διανυσματοποίησης (έως 3 επαναλήψεις)", + "Available Filters" : "Διαθέσιμα φίλτρα", + "Available Workflows" : "Διαθέσιμες ροές εργασίας", + "Avg Execution Time" : "Μέσος χρόνος εκτέλεσης", + "Avg Members/Org" : "Μέσος όρος μελών/οργανισμό", + "Avg Object Views/Session" : "Μέσος όρος προβολών αντικειμένων/συνεδρία", + "Avg Response Time" : "Μέσος χρόνος απόκρισης", + "Avg Results/Search" : "Μέσος όρος αποτελεσμάτων/αναζήτηση", + "Avg Searches/Session" : "Μέσος όρος αναζητήσεων/συνεδρία", + "Back" : "Πίσω", + "Back to Entities" : "Πίσω στις οντότητες", + "Back to Registers" : "Πίσω στα μητρώα", + "Back to Webhooks" : "Πίσω στα webhooks", + "Back to applications" : "Πίσω στις εφαρμογές", + "Back to entities" : "Πίσω στις οντότητες", + "Backend updated successfully. Please reload the application." : "Το backend ενημερώθηκε με επιτυχία. Παρακαλώ επαναφορτώστε την εφαρμογή.", + "Base URL (Optional)" : "Βασικό URL (Προαιρετικό)", + "Basic Information" : "Βασικές πληροφορίες", + "Batch Size" : "Μέγεθος παρτίδας", + "Batch extraction completed" : "Η ομαδική εξαγωγή ολοκληρώθηκε", + "Before" : "Πριν", + "Before object vectorization can work:" : "Πριν λειτουργήσει η διανυσματοποίηση αντικειμένων:", + "Behavior Issue" : "Ζήτημα συμπεριφοράς", + "Bookmarks" : "Σελιδοδείκτες", + "Blob storage has been retired. All objects now use magic tables." : "Η αποθήκευση blob έχει αποσυρθεί. Όλα τα αντικείμενα χρησιμοποιούν πλέον magic tables.", + "Branch" : "Κλάδος", + "Bulk delete operation completed successfully" : "Η ομαδική λειτουργία διαγραφής ολοκληρώθηκε με επιτυχία", + "Bulk save operation completed successfully" : "Η ομαδική λειτουργία αποθήκευσης ολοκληρώθηκε με επιτυχία", + "Business Data" : "Επιχειρηματικά δεδομένα", + "Cache cleared successfully" : "Η προσωρινή μνήμη εκκαθαρίστηκε με επιτυχία", + "Calculate Sizes" : "Υπολογισμός μεγεθών", + "Cancel" : "Άκυρο", + "Cannot delete: objects are still attached" : "Δεν είναι δυνατή η διαγραφή: τα αντικείμενα είναι ακόμα συνδεδεμένα", + "Category" : "Κατηγορία", + "Change Type" : "Τύπος αλλαγής", + "Changes" : "Αλλαγές", + "Changes copied to clipboard" : "Οι αλλαγές αντιγράφηκαν στο πρόχειρο", + "Chat Model" : "Μοντέλο συνομιλίας", + "Chat Provider" : "Πάροχος συνομιλίας", + "Chat Provider (RAG)" : "Πάροχος συνομιλίας (RAG)", + "Chat Settings" : "Ρυθμίσεις συνομιλίας", + "Chat provider connection successful!" : "Η σύνδεση με τον πάροχο συνομιλίας ήταν επιτυχής!", + "Chat settings" : "Ρυθμίσεις συνομιλίας", + "Choose a register" : "Επιλέξτε ένα μητρώο", + "Choose a schema" : "Επιλέξτε ένα σχήμα", + "Choose how vector similarity calculations are performed for semantic search" : "Επιλέξτε πώς εκτελούνται οι υπολογισμοί διανυσματικής ομοιότητας για τη σημασιολογική αναζήτηση", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Επιλέξτε ποιους τύπους αρχείων θα συμπεριλάβετε στη διαδικασία διανυσματοποίησης. Θα επεξεργαστούν μόνο τα αρχεία με εξαγόμενο κείμενο και τμήματα.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Επιλέξτε ποιες προβολές θα συμπεριλάβετε στη διαδικασία διανυσματοποίησης. Αφήστε κενό για να επεξεργαστείτε όλες τις προβολές με βάση τη διαμόρφωσή σας.", + "Chunk" : "Τμήμα", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Η διαγραφή τμημάτων δεν έχει υλοποιηθεί ακόμα. Χρησιμοποιήστε τελικά σημεία βασισμένα σε τμήματα.", + "Chunks" : "Τμήματα", + "Chunks to Vectorize:" : "Τμήματα προς διανυσματοποίηση:", + "Cleanup Old Trails" : "Εκκαθάριση παλιών ιχνών", + "Cleanup completed" : "Η εκκαθάριση ολοκληρώθηκε", + "Clear All Embeddings" : "Εκκαθάριση όλων των ενσωματώσεων", + "Clear Entries" : "Εκκαθάριση καταχωρήσεων", + "Clear Filtered Audit Trails" : "Εκκαθάριση φιλτραρισμένων ιχνών ελέγχου", + "Clear Filters" : "Εκκαθάριση φίλτρων", + "Clear Index" : "Εκκαθάριση ευρετηρίου", + "Clear Selection" : "Εκκαθάριση επιλογής", + "Clear all" : "Εκκαθάριση όλων", + "Clear all filters" : "Εκκαθάριση όλων των φίλτρων", + "Clear date range" : "Εκκαθάριση εύρους ημερομηνιών", + "Clearing..." : "Εκκαθάριση...", + "Close" : "Κλείσιμο", + "Collection Management" : "Διαχείριση συλλογών", + "Collection Name" : "Όνομα συλλογής", + "Collection assignments updated successfully" : "Οι αναθέσεις συλλογών ενημερώθηκαν με επιτυχία", + "Collection cleared successfully" : "Η συλλογή εκκαθαρίστηκε με επιτυχία", + "Collection copied successfully" : "Η συλλογή αντιγράφηκε με επιτυχία", + "Collection created successfully" : "Η συλλογή δημιουργήθηκε με επιτυχία", + "Collection deleted successfully" : "Η συλλογή διαγράφηκε με επιτυχία", + "Collection used to store and index file metadata and content" : "Συλλογή που χρησιμοποιείται για την αποθήκευση και την καταχώρηση μεταδεδομένων και περιεχομένου αρχείων στο ευρετήριο", + "Collection used to store and index object data" : "Συλλογή που χρησιμοποιείται για την αποθήκευση και την καταχώρηση δεδομένων αντικειμένων στο ευρετήριο", + "Collections:" : "Συλλογές:", + "Commit Message" : "Μήνυμα υποβολής", + "Commit Within (ms)" : "Υποβολή εντός (ms)", + "Commit message" : "Μήνυμα υποβολής", + "Compare current schema with real object data" : "Σύγκριση του τρέχοντος σχήματος με πραγματικά δεδομένα αντικειμένων", + "Completed" : "Ολοκληρώθηκε", + "Complex" : "Σύνθετο", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Σύνθετα ερωτήματα: Αναζητήσεις για προχωρημένους με πολλαπλά φίλτρα, τελεστές και σύνθετους συνδυασμούς παραμέτρων", + "Confidence Level" : "Επίπεδο εμπιστοσύνης", + "Confidence Score" : "Βαθμολογία εμπιστοσύνης", + "Config must be provided as an object" : "Η διαμόρφωση πρέπει να παρέχεται ως αντικείμενο", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Διαχείριση ConfigSet", + "ConfigSet Name" : "Όνομα ConfigSet", + "ConfigSet created successfully" : "Το ConfigSet δημιουργήθηκε με επιτυχία", + "ConfigSet deleted successfully" : "Το ConfigSet διαγράφηκε με επιτυχία", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Τα ConfigSets ορίζουν το σχήμα και τη διαμόρφωση για τις συλλογές SOLR σας. Περιέχουν ορισμούς πεδίων, αναλυτές και άλλες ρυθμίσεις αναζήτησης.", + "Configuration" : "Διαμόρφωση", + "Configuration Data" : "Δεδομένα διαμόρφωσης", + "Configuration saved" : "Η διαμόρφωση αποθηκεύτηκε", + "Configurations" : "Διαμορφώσεις", + "Configure Facets" : "Διαμόρφωση όψεων", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Διαμορφώστε παρόχους Μεγάλων Γλωσσικών Μοντέλων (LLM) για λειτουργίες τεχνητής νοημοσύνης, συμπεριλαμβανομένης της σημασιολογικής αναζήτησης, των ενσωματώσεων και της συνομιλίας.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Διαμορφώστε βασικές ρυθμίσεις σύνδεσης για τον διακομιστή SOLR σας, συμπεριλαμβανομένου του ελέγχου ταυτότητας και των επιλογών δικτύου. Χρησιμοποιήστε τα ξεχωριστά παράθυρα διαλόγου διαχείρισης ConfigSet και συλλογών για τη διαχείριση πυρήνων και συλλογών.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Διαμορφώστε πώς τα αντικείμενα της βάσης δεδομένων μετατρέπονται σε διανυσματικές ενσωματώσεις για σημασιολογική αναζήτηση. Τα αντικείμενα διανυσματοποιούνται απευθείας χωρίς να απαιτείται εξαγωγή κειμένου.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Διαμορφώστε πώς τα αντικείμενα μετατρέπονται σε κείμενο πριν από τη διανυσματοποίηση. Αυτές οι ρυθμίσεις επηρεάζουν την ποιότητα και το πλαίσιο της αναζήτησης.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Διαμορφώστε παραμέτρους για τη διανυσματοποίηση αρχείων. Αυτή η διαδικασία θα δημιουργήσει διανυσματικές ενσωματώσεις για όλα τα εξαγόμενα τμήματα αρχείων.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Διαμορφώστε παραμέτρους για τη διανυσματοποίηση αντικειμένων. Αυτή η διαδικασία θα δημιουργήσει διανυσματικές ενσωματώσεις για όλα τα αντικείμενα που ταιριάζουν με τα φίλτρα προβολής σας.", + "Configure which types of data to search and how many sources to retrieve" : "Διαμορφώστε ποιους τύπους δεδομένων θα αναζητάτε και πόσες πηγές θα ανακτάτε", + "Connection Failed" : "Η σύνδεση απέτυχε", + "Connection Settings" : "Ρυθμίσεις σύνδεσης", + "Connection Successful!" : "Η σύνδεση ήταν επιτυχής!", + "Connection failed" : "Η σύνδεση απέτυχε", + "Connection protocol" : "Πρωτόκολλο σύνδεσης", + "Connection successful" : "Η σύνδεση ήταν επιτυχής", + "Connection timeout in seconds" : "Χρονικό όριο σύνδεσης σε δευτερόλεπτα", + "Constant delay between retries (5 minutes)" : "Σταθερή καθυστέρηση μεταξύ επαναλήψεων (5 λεπτά)", + "Constraint Issue" : "Ζήτημα περιορισμού", + "Content Filters" : "Φίλτρα περιεχομένου", + "Continue to Properties" : "Συνέχεια στις ιδιότητες", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Ελέγξτε ποιες προβολές αντικειμένων πρέπει να διανυσματοποιηθούν για τη μείωση του κόστους του API. Διανυσματοποιήστε μόνο προβολές που επωφελούνται από τη σημασιολογική αναζήτηση.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Ελέγξτε ποιες προβολές και εργαλεία μπορεί να χρησιμοποιήσει η τεχνητή νοημοσύνη σε αυτήν τη συνομιλία. Από προεπιλογή, όλες οι δυνατότητες του πράκτορα είναι ενεργοποιημένες.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Ελέγχει την τυχαιότητα (0 = ντετερμινιστικό, 2 = πολύ δημιουργικό)", + "Conversation ID is required" : "Απαιτείται αναγνωριστικό συνομιλίας", + "Conversation archived" : "Η συνομιλία αρχειοθετήθηκε", + "Conversation archived successfully" : "Η συνομιλία αρχειοθετήθηκε με επιτυχία", + "Conversation cleared successfully" : "Η συνομιλία εκκαθαρίστηκε με επιτυχία", + "Conversation deleted" : "Η συνομιλία διαγράφηκε", + "Conversation permanently deleted" : "Η συνομιλία διαγράφηκε οριστικά", + "Conversation renamed" : "Η συνομιλία μετονομάστηκε", + "Conversation restored" : "Η συνομιλία επαναφέρθηκε", + "Conversation title" : "Τίτλος συνομιλίας", + "Copied!" : "Αντιγράφηκε!", + "Copy" : "Αντιγραφή", + "Copy Changes" : "Αντιγραφή αλλαγών", + "Copy Collection" : "Αντιγραφή συλλογής", + "Copy Data" : "Αντιγραφή δεδομένων", + "Copy Full Data" : "Αντιγραφή πλήρων δεδομένων", + "Copying..." : "Αντιγραφή...", + "Count" : "Πλήθος", + "Counting objects..." : "Καταμέτρηση αντικειμένων...", + "Costs" : "Κόστη", + "Create" : "Δημιουργία", + "Create Collection" : "Δημιουργία συλλογής", + "Create ConfigSet" : "Δημιουργία ConfigSet", + "Create New Collection" : "Δημιουργία νέας συλλογής", + "Create New ConfigSet" : "Δημιουργία νέου ConfigSet", + "Create Operations" : "Λειτουργίες δημιουργίας", + "Create Webhook" : "Δημιουργία webhook", + "Create a copy of collection:" : "Δημιουργία αντιγράφου της συλλογής:", + "Create a new ConfigSet based on the _default template" : "Δημιουργία νέου ConfigSet βασισμένου στο πρότυπο _default", + "Create a new SOLR collection from an existing ConfigSet" : "Δημιουργία νέας συλλογής SOLR από υπάρχον ConfigSet", + "Create your first AI agent to get started." : "Δημιουργήστε τον πρώτο σας πράκτορα τεχνητής νοημοσύνης για να ξεκινήσετε.", + "Created" : "Δημιουργήθηκε", + "Created:" : "Δημιουργήθηκε:", + "Creating..." : "Δημιουργία...", + "Current Filters" : "Τρέχοντα φίλτρα", + "Current Type:" : "Τρέχων τύπος:", + "Custom API endpoint if using a different region" : "Προσαρμοσμένο τελικό σημείο API εάν χρησιμοποιείτε διαφορετική περιοχή", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Προσαρμοσμένες κεφαλίδες HTTP (μία ανά γραμμή, μορφή: Header-Name: value)", + "Daily" : "Καθημερινά", + "Dashboard" : "Πίνακας ελέγχου", + "Data Source" : "Πηγή δεδομένων", + "Data sources" : "Πηγές δεδομένων", + "Data type does not match observed values" : "Ο τύπος δεδομένων δεν ταιριάζει με τις παρατηρούμενες τιμές", + "Database URL" : "URL βάσης δεδομένων", + "Database information refreshed" : "Οι πληροφορίες της βάσης δεδομένων ανανεώθηκαν", + "Date Range" : "Εύρος ημερομηνιών", + "Default" : "Προεπιλογή", + "Default Organisation" : "Προεπιλεγμένος οργανισμός", + "Default: 5" : "Προεπιλογή: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Οι καθυστερήσεις διπλασιάζονται με κάθε προσπάθεια (2, 4, 8 λεπτά...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Οι καθυστερήσεις αυξάνονται γραμμικά (5, 10, 15 λεπτά...)", + "Delete" : "Διαγραφή", + "Delete Application" : "Διαγραφή εφαρμογής", + "Delete Audit Trail" : "Διαγραφή ίχνους ελέγχου", + "Delete Collection" : "Διαγραφή συλλογής", + "Delete ConfigSet" : "Διαγραφή ConfigSet", + "Delete Objects" : "Διαγραφή αντικειμένων", + "Delete Operations" : "Λειτουργίες διαγραφής", + "Delete View" : "Διαγραφή προβολής", + "Delete all objects in this schema" : "Διαγραφή όλων των αντικειμένων σε αυτό το σχήμα", + "Delete permanently" : "Οριστική διαγραφή", + "Delete this conversation?" : "Διαγραφή αυτής της συνομιλίας;", + "Delete view" : "Διαγραφή προβολής", + "Deleted" : "Διαγράφηκε", + "Deleted By" : "Διαγράφηκε από", + "Deleted Date" : "Ημερομηνία διαγραφής", + "Deleted Items Management" : "Διαχείριση διαγραμμένων στοιχείων", + "Deleted This Week" : "Διαγράφηκαν αυτήν την εβδομάδα", + "Deleted Today" : "Διαγράφηκαν σήμερα", + "Deleted:" : "Διαγράφηκε:", + "Deleting..." : "Διαγραφή...", + "Deletion Date Range" : "Εύρος ημερομηνιών διαγραφής", + "Deletion Statistics" : "Στατιστικά διαγραφής", + "Deprecated" : "Καταργημένο", + "Description" : "Περιγραφή", + "Description:" : "Περιγραφή:", + "Detect data types and patterns" : "Εντοπισμός τύπων δεδομένων και μοτίβων", + "Detected At" : "Εντοπίστηκε στις", + "Detected Format:" : "Εντοπισμένη μορφή:", + "Detected Issues:" : "Εντοπισμένα ζητήματα:", + "Disable" : "Απενεργοποίηση", + "Disabled" : "Απενεργοποιημένο", + "Discover Files" : "Ανακάλυψη αρχείων", + "Discovered Properties" : "Ανακαλυφθείσες ιδιότητες", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Θέλετε να διαγράψετε οριστικά όλες τις φιλτραρισμένες καταχωρήσεις ίχνους ελέγχου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Θέλετε να διαγράψετε οριστικά αυτήν την καταχώρηση ίχνους ελέγχου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Documents" : "Έγγραφα", + "Download API Spec" : "Λήψη προδιαγραφής API", + "Edit" : "Επεξεργασία", + "Edit Register" : "Επεξεργασία μητρώου", + "Edit Schema" : "Επεξεργασία σχήματος", + "Edit View" : "Επεξεργασία προβολής", + "Edit Webhook" : "Επεξεργασία webhook", + "Edit view" : "Επεξεργασία προβολής", + "Edit view details" : "Επεξεργασία λεπτομερειών προβολής", + "Effectiveness" : "Αποτελεσματικότητα", + "Email" : "Email", + "Emails" : "Email", + "Embedding Model" : "Μοντέλο ενσωμάτωσης", + "Embedding Provider" : "Πάροχος ενσωμάτωσης", + "Embedding generated successfully" : "Η ενσωμάτωση δημιουργήθηκε με επιτυχία", + "Embedding provider connection successful!" : "Η σύνδεση με τον πάροχο ενσωμάτωσης ήταν επιτυχής!", + "Enable" : "Ενεργοποίηση", + "Enable auto-creation" : "Ενεργοποίηση αυτόματης δημιουργίας", + "Enable automatic file vectorization" : "Ενεργοποίηση αυτόματης διανυσματοποίησης αρχείων", + "Enable automatic object vectorization" : "Ενεργοποίηση αυτόματης διανυσματοποίησης αντικειμένων", + "Enable automatic synchronization" : "Ενεργοποίηση αυτόματου συγχρονισμού", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Ενεργοποίηση λεπτομερούς καταγραφής για λειτουργίες SOLR (συνιστάται για αποσφαλμάτωση)", + "Enable faceting" : "Ενεργοποίηση όψεων", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Ενεργοποιήστε ή απενεργοποιήστε τις λειτουργίες LLM. Διαμορφώστε παρόχους και μοντέλα χρησιμοποιώντας το κουμπί Διαμόρφωση LLM παραπάνω.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Ενεργοποιήστε ή απενεργοποιήστε την ενσωμάτωση αναζήτησης SOLR. Διαμορφώστε τις ρυθμίσεις σύνδεσης χρησιμοποιώντας το κουμπί Ρυθμίσεις σύνδεσης παραπάνω.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Ενεργοποιήστε ή απενεργοποιήστε την ενσωμάτωση ροής εργασίας n8n. Διαμορφώστε τις ρυθμίσεις σύνδεσης παρακάτω.", + "Enable or disable this webhook" : "Ενεργοποιήστε ή απενεργοποιήστε αυτό το webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Ενεργοποίηση διανυσματοποίησης για όλες τις υπάρχουσες και μελλοντικές προβολές (μπορεί να αυξήσει το κόστος)", + "Enabled" : "Ενεργοποιημένο", + "Endpoints" : "Τελικά σημεία", + "Enter ConfigSet name" : "Εισαγάγετε όνομα ConfigSet", + "Enter collection name" : "Εισαγάγετε όνομα συλλογής", + "Enter description (optional)..." : "Εισαγάγετε περιγραφή (προαιρετικά)...", + "Enter new collection name" : "Εισαγάγετε νέο όνομα συλλογής", + "Enter object ID" : "Εισαγάγετε αναγνωριστικό αντικειμένου", + "Enter search term" : "Εισαγάγετε όρο αναζήτησης", + "Enter view name..." : "Εισαγάγετε όνομα προβολής...", + "Enter webhook name" : "Εισαγάγετε όνομα webhook", + "Entities" : "Οντότητες", + "Entity Information" : "Πληροφορίες οντότητας", + "Entity deleted successfully" : "Η οντότητα διαγράφηκε με επιτυχία", + "Entity not found" : "Η οντότητα δεν βρέθηκε", + "Entries to be deleted:" : "Καταχωρήσεις προς διαγραφή:", + "Enum Issue" : "Ζήτημα απαρίθμησης", + "Enum constraint is missing" : "Λείπει ο περιορισμός απαρίθμησης", + "Error" : "Σφάλμα", + "Error Details" : "Λεπτομέρειες σφάλματος", + "Error Information" : "Πληροφορίες σφάλματος", + "Error Message" : "Μήνυμα σφάλματος", + "Error loading application" : "Σφάλμα φόρτωσης εφαρμογής", + "Error loading audit trails" : "Σφάλμα φόρτωσης ιχνών ελέγχου", + "Error loading entity" : "Σφάλμα φόρτωσης οντότητας", + "Error loading search trails" : "Σφάλμα φόρτωσης ιχνών αναζήτησης", + "Estimated Batches:" : "Εκτιμώμενες παρτίδες:", + "Estimated Cost:" : "Εκτιμώμενο κόστος:", + "Estimated Duration:" : "Εκτιμώμενη διάρκεια:", + "Event" : "Συμβάν", + "Event Property for Payload" : "Ιδιότητα συμβάντος για το ωφέλιμο φορτίο", + "Events" : "Συμβάντα", + "Example Value" : "Παράδειγμα τιμής", + "Exclusive Maximum" : "Αποκλειστικό μέγιστο", + "Exclusive Minimum" : "Αποκλειστικό ελάχιστο", + "Execution Mode" : "Λειτουργία εκτέλεσης", + "Execution Time" : "Χρόνος εκτέλεσης", + "Execution Time Range" : "Εύρος χρόνου εκτέλεσης", + "Existing Improvements" : "Υπάρχουσες βελτιώσεις", + "Exponential" : "Εκθετικό", + "Export" : "Εξαγωγή", + "Export completed successfully" : "Η εξαγωγή ολοκληρώθηκε με επιτυχία", + "Export, view, or delete audit trails" : "Εξαγωγή, προβολή ή διαγραφή ιχνών ελέγχου", + "Extend Schema" : "Επέκταση σχήματος", + "Extended" : "Επεκταμένο", + "Extensions" : "Επεκτάσεις", + "Extract Pending Files" : "Εξαγωγή αρχείων σε εκκρεμότητα", + "Extract properties from each object" : "Εξαγωγή ιδιοτήτων από κάθε αντικείμενο", + "Extracted At" : "Εξήχθη στις", + "Extraction Status" : "Κατάσταση εξαγωγής", + "Facet configuration updated successfully" : "Η διαμόρφωση όψεων ενημερώθηκε με επιτυχία", + "Facets discovered and configured successfully" : "Οι όψεις ανακαλύφθηκαν και διαμορφώθηκαν με επιτυχία", + "Facets discovered successfully" : "Οι όψεις ανακαλύφθηκαν με επιτυχία", + "Failed" : "Απέτυχε", + "Failed to analyze schema properties" : "Αποτυχία ανάλυσης ιδιοτήτων σχήματος", + "Failed to archive conversation" : "Αποτυχία αρχειοθέτησης συνομιλίας", + "Failed to calculate sizes" : "Αποτυχία υπολογισμού μεγεθών", + "Failed to clear collection" : "Αποτυχία εκκαθάρισης συλλογής", + "Failed to copy changes" : "Αποτυχία αντιγραφής αλλαγών", + "Failed to copy data" : "Αποτυχία αντιγραφής δεδομένων", + "Failed to copy data to clipboard" : "Αποτυχία αντιγραφής δεδομένων στο πρόχειρο", + "Failed to create conversation" : "Αποτυχία δημιουργίας συνομιλίας", + "Failed to create or find project" : "Αποτυχία δημιουργίας ή εύρεσης έργου", + "Failed to delete collection" : "Αποτυχία διαγραφής συλλογής", + "Failed to delete conversation" : "Αποτυχία διαγραφής συνομιλίας", + "Failed to delete webhook" : "Αποτυχία διαγραφής webhook", + "Failed to download API specification" : "Αποτυχία λήψης προδιαγραφής API", + "Failed to get SOLR field configuration" : "Αποτυχία λήψης διαμόρφωσης πεδίου SOLR", + "Failed to get entity categories" : "Αποτυχία λήψης κατηγοριών οντοτήτων", + "Failed to get entity statistics" : "Αποτυχία λήψης στατιστικών οντοτήτων", + "Failed to get entity types" : "Αποτυχία λήψης τύπων οντοτήτων", + "Failed to load LLM configuration" : "Αποτυχία φόρτωσης διαμόρφωσης LLM", + "Failed to load Nextcloud groups" : "Αποτυχία φόρτωσης ομάδων Nextcloud", + "Failed to load conversation" : "Αποτυχία φόρτωσης συνομιλίας", + "Failed to load entities" : "Αποτυχία φόρτωσης οντοτήτων", + "Failed to load entity" : "Αποτυχία φόρτωσης οντότητας", + "Failed to load files" : "Αποτυχία φόρτωσης αρχείων", + "Failed to load organisations" : "Αποτυχία φόρτωσης οργανισμών", + "Failed to load templates" : "Αποτυχία φόρτωσης προτύπων", + "Failed to load webhooks" : "Αποτυχία φόρτωσης webhooks", + "Failed to load workflows" : "Αποτυχία φόρτωσης ροών εργασίας", + "Failed to refresh database information" : "Αποτυχία ανανέωσης πληροφοριών βάσης δεδομένων", + "Failed to reindex collection" : "Αποτυχία επανευρετηρίασης συλλογής", + "Failed to rename conversation" : "Αποτυχία μετονομασίας συνομιλίας", + "Failed to restore conversation" : "Αποτυχία επαναφοράς συνομιλίας", + "Failed to retry extraction" : "Αποτυχία επανάληψης εξαγωγής", + "Failed to retry webhook" : "Αποτυχία επανάληψης webhook", + "Failed to save GitHub token" : "Αποτυχία αποθήκευσης διακριτικού GitHub", + "Failed to save GitLab URL" : "Αποτυχία αποθήκευσης URL GitLab", + "Failed to save GitLab token" : "Αποτυχία αποθήκευσης διακριτικού GitLab", + "Failed to save additional feedback" : "Αποτυχία αποθήκευσης πρόσθετων σχολίων", + "Failed to save n8n configuration" : "Αποτυχία αποθήκευσης διαμόρφωσης n8n", + "Failed to save roles" : "Αποτυχία αποθήκευσης ρόλων", + "Failed to save settings" : "Αποτυχία αποθήκευσης ρυθμίσεων", + "Failed to save webhook" : "Αποτυχία αποθήκευσης webhook", + "Failed to send feedback" : "Αποτυχία αποστολής σχολίων", + "Failed to test webhook" : "Αποτυχία δοκιμής webhook", + "Failed to update favorite status" : "Αποτυχία ενημέρωσης κατάστασης αγαπημένου", + "Failed to update schema properties" : "Αποτυχία ενημέρωσης ιδιοτήτων σχήματος", + "Failed to update view" : "Αποτυχία ενημέρωσης προβολής", + "Failed to update webhook" : "Αποτυχία ενημέρωσης webhook", + "Feedback recorded" : "Τα σχόλια καταγράφηκαν", + "Fewer sources (1-3):" : "Λιγότερες πηγές (1-3):", + "Field" : "Πεδίο", + "File" : "Αρχείο", + "File Chunk Prediction" : "Πρόβλεψη τμημάτων αρχείου", + "File Collection" : "Συλλογή αρχείων", + "File Management" : "Διαχείριση αρχείων", + "File Name" : "Όνομα αρχείου", + "File Path" : "Διαδρομή αρχείου", + "File Type Selection" : "Επιλογή τύπου αρχείου", + "File Vectorization" : "Διανυσματοποίηση αρχείων", + "File Warmup" : "Προθέρμανση αρχείων", + "File actions menu" : "Μενού ενεργειών αρχείου", + "File anonymized successfully" : "Το αρχείο ανωνυμοποιήθηκε με επιτυχία", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Η διανυσματοποίηση τμημάτων αρχείου δεν έχει υλοποιηθεί ακόμα. Τα τμήματα είναι έτοιμα και αποθηκευμένα, αλλά η υπηρεσία διανυσματοποίησης βρίσκεται υπό ανάπτυξη.", + "File collection not configured" : "Η συλλογή αρχείων δεν έχει διαμορφωθεί", + "File discovery completed" : "Η ανακάλυψη αρχείων ολοκληρώθηκε", + "File extraction completed" : "Η εξαγωγή αρχείων ολοκληρώθηκε", + "File extraction queued" : "Η εξαγωγή αρχείων τέθηκε σε ουρά", + "File indexed successfully" : "Το αρχείο καταχωρήθηκε στο ευρετήριο με επιτυχία", + "File is already anonymized" : "Το αρχείο είναι ήδη ανωνυμοποιημένο", + "File not found" : "Το αρχείο δεν βρέθηκε", + "File settings updated successfully" : "Οι ρυθμίσεις αρχείου ενημερώθηκαν με επιτυχία", + "File sources" : "Πηγές αρχείων", + "File vectorization configuration saved successfully" : "Η διαμόρφωση διανυσματοποίησης αρχείων αποθηκεύτηκε με επιτυχία", + "File vectorization started. Check the statistics section for progress." : "Η διανυσματοποίηση αρχείων ξεκίνησε. Ελέγξτε την ενότητα στατιστικών για την πρόοδο.", + "File warmup completed" : "Η προθέρμανση αρχείων ολοκληρώθηκε", + "Files" : "Αρχεία", + "Files with Completed Extraction:" : "Αρχεία με ολοκληρωμένη εξαγωγή:", + "Files → fileCollection, Objects → objectCollection" : "Αρχεία → fileCollection, Αντικείμενα → objectCollection", + "Filter Audit Trails" : "Φιλτράρισμα ιχνών ελέγχου", + "Filter Deleted Items" : "Φιλτράρισμα διαγραμμένων στοιχείων", + "Filter Objects" : "Φιλτράρισμα αντικειμένων", + "Filter Properties" : "Φιλτράρισμα ιδιοτήτων", + "Filter Search Trails" : "Φιλτράρισμα ιχνών αναζήτησης", + "Filter Statistics" : "Φιλτράρισμα στατιστικών", + "Filter and analyze search trail entries" : "Φιλτράρετε και αναλύστε καταχωρήσεις ιχνών αναζήτησης", + "Filter and manage audit trail entries" : "Φιλτράρετε και διαχειριστείτε καταχωρήσεις ιχνών ελέγχου", + "Filter and manage soft deleted items" : "Φιλτράρετε και διαχειριστείτε στοιχεία ήπιας διαγραφής", + "Filter by object ID" : "Φιλτράρισμα κατά αναγνωριστικό αντικειμένου", + "Filter by search term" : "Φιλτράρισμα κατά όρο αναζήτησης", + "Filter by webhook" : "Φιλτράρισμα κατά webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Τα δεδομένα φίλτρου φορτώθηκαν αυτόματα. Χρησιμοποιήστε τα παρακάτω φίλτρα για να βελτιώσετε την αναζήτησή σας.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Φιλτράρετε τους ενεργοποιητές webhook κατά ιδιότητες ωφέλιμου φορτίου (μία ανά γραμμή, μορφή: key: value)", + "Filtered" : "Φιλτραρισμένο", + "Filters" : "Φίλτρα", + "Fireworks AI Chat Settings" : "Ρυθμίσεις συνομιλίας Fireworks AI", + "Fireworks AI Embedding Configuration" : "Διαμόρφωση ενσωμάτωσης Fireworks AI", + "First" : "Πρώτο", + "Fixed" : "Σταθερό", + "For chat and retrieval-augmented generation" : "Για συνομιλία και παραγωγή ενισχυμένη με ανάκτηση", + "For vector embeddings and semantic search" : "Για διανυσματικές ενσωματώσεις και σημασιολογική αναζήτηση", + "Format" : "Μορφή", + "Format Issue" : "Ζήτημα μορφής", + "Format constraint is missing" : "Λείπει ο περιορισμός μορφής", + "From Date" : "Από ημερομηνία", + "From date" : "Από ημερομηνία", + "Full data copied to clipboard" : "Τα πλήρη δεδομένα αντιγράφηκαν στο πρόχειρο", + "General Issue" : "Γενικό ζήτημα", + "Generate recommendations and confidence scores" : "Δημιουργία συστάσεων και βαθμολογιών εμπιστοσύνης", + "Generate vectors immediately when new objects are created" : "Δημιουργία διανυσμάτων αμέσως όταν δημιουργούνται νέα αντικείμενα", + "GitHub token is valid" : "Το διακριτικό GitHub είναι έγκυρο", + "GitHub token removed successfully" : "Το διακριτικό GitHub αφαιρέθηκε με επιτυχία", + "GitHub token saved successfully" : "Το διακριτικό GitHub αποθηκεύτηκε με επιτυχία", + "GitLab URL saved successfully" : "Το URL GitLab αποθηκεύτηκε με επιτυχία", + "GitLab token is valid" : "Το διακριτικό GitLab είναι έγκυρο", + "GitLab token saved successfully" : "Το διακριτικό GitLab αποθηκεύτηκε με επιτυχία", + "HTTP Method" : "Μέθοδος HTTP", + "HTTP method used to send webhook requests" : "Μέθοδος HTTP που χρησιμοποιείται για την αποστολή αιτημάτων webhook", + "Headers" : "Κεφαλίδες", + "Health" : "Υγεία", + "Heartbeat successful - connection kept alive" : "Επιτυχής παλμός - η σύνδεση διατηρήθηκε ενεργή", + "Helpful" : "Χρήσιμο", + "Hide Filters" : "Απόκρυψη φίλτρων", + "Hide in forms" : "Απόκρυψη στις φόρμες", + "Hide in list view" : "Απόκρυψη στην προβολή λίστας", + "High" : "Υψηλό", + "High Confidence" : "Υψηλή εμπιστοσύνη", + "Host" : "Κεντρικός υπολογιστής", + "Hourly" : "Ωριαία", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Πόσο βαθιά θα διασχίζονται οι ένθετες ιδιότητες αντικειμένων (1-20). Οι υψηλότερες τιμές καταγράφουν περισσότερες λεπτομέρειες αλλά αυξάνουν το μέγεθος του διανύσματος.", + "How often to check for updates (1-168 hours)" : "Πόσο συχνά θα γίνεται έλεγχος για ενημερώσεις (1-168 ώρες)", + "How to handle retries for failed webhook deliveries" : "Πώς θα χειρίζονται οι επαναλήψεις για αποτυχημένες παραδόσεις webhook", + "ID" : "Αναγνωριστικό", + "ID:" : "Αναγνωριστικό:", + "IP rate limits cleared successfully" : "Τα όρια ρυθμού IP εκκαθαρίστηκαν με επιτυχία", + "Identify properties not in the schema" : "Εντοπισμός ιδιοτήτων που δεν υπάρχουν στο σχήμα", + "Immutable" : "Αμετάβλητο", + "Import" : "Εισαγωγή", + "Import successful" : "Η εισαγωγή ήταν επιτυχής", + "Improved Property" : "Βελτιωμένη ιδιότητα", + "In use" : "Σε χρήση", + "Inactive" : "Ανενεργό", + "Include IDs and names of related objects for better contextual search" : "Συμπεριλάβετε αναγνωριστικά και ονόματα σχετικών αντικειμένων για καλύτερη συμφραζόμενη αναζήτηση", + "Include related object references" : "Συμπερίληψη αναφορών σχετικών αντικειμένων", + "Include schema and register metadata" : "Συμπερίληψη μεταδεδομένων σχήματος και μητρώου", + "Initialization failed" : "Η αρχικοποίηση απέτυχε", + "Initialize Project" : "Αρχικοποίηση έργου", + "Inspect Fields" : "Επιθεώρηση πεδίων", + "Inspect Index" : "Επιθεώρηση ευρετηρίου", + "Invalid" : "Μη έγκυρο", + "Invalid batch size. Must be between 1 and 5000" : "Μη έγκυρο μέγεθος παρτίδας. Πρέπει να είναι μεταξύ 1 και 5000", + "Invalid field name provided" : "Δόθηκε μη έγκυρο όνομα πεδίου", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Μη έγκυρο maxObjects. Πρέπει να είναι 0 (όλα) ή θετικός αριθμός", + "Invalid:" : "Μη έγκυρο:", + "Issue" : "Ζήτημα", + "Items per page:" : "Στοιχεία ανά σελίδα:", + "Just now" : "Μόλις τώρα", + "LLM Configuration" : "Διαμόρφωση LLM", + "LLM actions menu" : "Μενού ενεργειών LLM", + "LLM configuration saved successfully" : "Η διαμόρφωση LLM αποθηκεύτηκε με επιτυχία", + "LLM features disabled" : "Οι λειτουργίες LLM απενεργοποιήθηκαν", + "LLM features enabled" : "Οι λειτουργίες LLM ενεργοποιήθηκαν", + "LLM must be enabled with an embedding provider configured" : "Το LLM πρέπει να είναι ενεργοποιημένο με διαμορφωμένο πάροχο ενσωμάτωσης", + "LLM settings updated successfully" : "Οι ρυθμίσεις LLM ενημερώθηκαν με επιτυχία", + "Last" : "Τελευταίο", + "Last Triggered" : "Τελευταία ενεργοποίηση", + "Length Range:" : "Εύρος μήκους:", + "Linear" : "Γραμμικό", + "Load Advanced Filters" : "Φόρτωση φίλτρων για προχωρημένους", + "Load advanced filters with live data from your search index" : "Φορτώστε φίλτρα για προχωρημένους με ζωντανά δεδομένα από το ευρετήριο αναζήτησής σας", + "Load view" : "Φόρτωση προβολής", + "Loading ConfigSets..." : "Φόρτωση ConfigSets...", + "Loading advanced filters..." : "Φόρτωση φίλτρων για προχωρημένους...", + "Loading agents..." : "Φόρτωση πρακτόρων...", + "Loading application details..." : "Φόρτωση λεπτομερειών εφαρμογής...", + "Loading applications..." : "Φόρτωση εφαρμογών...", + "Loading archived conversations..." : "Φόρτωση αρχειοθετημένων συνομιλιών...", + "Loading audit trails..." : "Φόρτωση ιχνών ελέγχου...", + "Loading collections..." : "Φόρτωση συλλογών...", + "Loading configurations..." : "Φόρτωση διαμορφώσεων...", + "Loading conversation..." : "Φόρτωση συνομιλίας...", + "Loading conversations..." : "Φόρτωση συνομιλιών...", + "Loading deleted items..." : "Φόρτωση διαγραμμένων στοιχείων...", + "Loading events..." : "Φόρτωση συμβάντων...", + "Loading filter data..." : "Φόρτωση δεδομένων φίλτρου...", + "Loading groups..." : "Φόρτωση ομάδων...", + "Loading log details..." : "Φόρτωση λεπτομερειών αρχείου καταγραφής...", + "Loading registers..." : "Φόρτωση μητρώων...", + "Loading schemas..." : "Φόρτωση σχημάτων...", + "Loading search trails..." : "Φόρτωση ιχνών αναζήτησης...", + "Loading sources..." : "Φόρτωση πηγών...", + "Loading statistics..." : "Φόρτωση στατιστικών...", + "Loading users..." : "Φόρτωση χρηστών...", + "Loading views..." : "Φόρτωση προβολών...", + "Loading..." : "Φόρτωση...", + "Local" : "Τοπικό", + "Local Version" : "Τοπική έκδοση", + "Locked" : "Κλειδωμένο", + "Locked:" : "Κλειδωμένο:", + "Login successful" : "Επιτυχής σύνδεση", + "Logs" : "Αρχεία καταγραφής", + "Low" : "Χαμηλό", + "Low Confidence" : "Χαμηλή εμπιστοσύνη", + "Lucene Version:" : "Έκδοση Lucene:", + "Magic table synchronized successfully" : "Ο magic table συγχρονίστηκε με επιτυχία", + "Manage Organisation Roles" : "Διαχείριση ρόλων οργανισμού", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Διαχειριστείτε τις συλλογές SOLR (αποθήκες δεδομένων) και αναθέστε τις για αντικείμενα και αρχεία.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Διαχειριστείτε τα ConfigSets SOLR (πρότυπα διαμόρφωσης) για τις συλλογές σας.", + "Manage and configure agents for automated tasks" : "Διαχειριστείτε και διαμορφώστε πράκτορες για αυτοματοποιημένες εργασίες", + "Manage and monitor file text extraction status" : "Διαχειριστείτε και παρακολουθήστε την κατάσταση εξαγωγής κειμένου αρχείων", + "Manage and restore soft deleted items from your registers" : "Διαχειριστείτε και επαναφέρετε στοιχεία ήπιας διαγραφής από τα μητρώα σας", + "Manage and view detected entities from files and objects" : "Διαχειριστείτε και προβάλετε εντοπισμένες οντότητες από αρχεία και αντικείμενα", + "Manage document templates and themes" : "Διαχειριστείτε πρότυπα εγγράφων και θέματα", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Διαχειριστείτε ροές εργασίας n8n για την αυτοματοποίηση του OpenRegister. Οι ροές εργασίας θα αποθηκευτούν στο διαμορφωμένο έργο.", + "Manage webhooks for event-driven integrations" : "Διαχειριστείτε webhooks για ενσωματώσεις βασισμένες σε συμβάντα", + "Manage your applications and modules" : "Διαχειριστείτε τις εφαρμογές και τις μονάδες σας", + "Manage your data registers and their configurations" : "Διαχειριστείτε τα μητρώα δεδομένων σας και τις διαμορφώσεις τους", + "Manage your data schemas and their properties" : "Διαχειριστείτε τα σχήματα δεδομένων σας και τις ιδιότητές τους", + "Manage your data sources and their configurations" : "Διαχειριστείτε τις πηγές δεδομένων σας και τις διαμορφώσεις τους", + "Manage your saved search configurations" : "Διαχειριστείτε τις αποθηκευμένες διαμορφώσεις αναζήτησης", + "Manage your system configurations and settings" : "Διαχειριστείτε τις διαμορφώσεις και τις ρυθμίσεις του συστήματός σας", + "Managed" : "Διαχειριζόμενο", + "Max Files (0 = all)" : "Μέγιστος αριθμός αρχείων (0 = όλα)", + "Max Length" : "Μέγιστο μήκος", + "Max Length:" : "Μέγιστο μήκος:", + "Max Objects (0 = all)" : "Μέγιστος αριθμός αντικειμένων (0 = όλα)", + "Max Retries" : "Μέγιστες επαναλήψεις", + "Max Shards/Node" : "Μέγιστα Shards/Κόμβο", + "Max execution time (ms)" : "Μέγιστος χρόνος εκτέλεσης (ms)", + "Max ms" : "Μέγιστα ms", + "Max result count" : "Μέγιστο πλήθος αποτελεσμάτων", + "Max results" : "Μέγιστα αποτελέσματα", + "Maximum" : "Μέγιστο", + "Maximum Nesting Depth" : "Μέγιστο βάθος ένθεσης", + "Maximum length constraint is missing" : "Λείπει ο περιορισμός μέγιστου μήκους", + "Maximum length is too restrictive" : "Το μέγιστο μήκος είναι πολύ περιοριστικό", + "Maximum number of files to process. Set to 0 to process all files." : "Μέγιστος αριθμός αρχείων προς επεξεργασία. Ορίστε το σε 0 για να επεξεργαστείτε όλα τα αρχεία.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Μέγιστος αριθμός αντικειμένων προς επεξεργασία. Ορίστε το σε 0 για να επεξεργαστείτε όλα τα αντικείμενα.", + "Maximum number of retry attempts for failed deliveries" : "Μέγιστος αριθμός προσπαθειών επανάληψης για αποτυχημένες παραδόσεις", + "Maximum time to wait before committing changes" : "Μέγιστος χρόνος αναμονής πριν την υποβολή αλλαγών", + "Maximum tokens to generate" : "Μέγιστος αριθμός διακριτικών προς δημιουργία", + "Maximum value constraint is missing" : "Λείπει ο περιορισμός μέγιστης τιμής", + "Maximum value is too restrictive" : "Η μέγιστη τιμή είναι πολύ περιοριστική", + "Medium" : "Μεσαίο", + "Medium Confidence" : "Μεσαία εμπιστοσύνη", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Μεσαία ερωτήματα: Αναζητήσεις με κάποιο φιλτράρισμα ή πολλαπλές παραμέτρους (π.χ. εύρη ημερομηνιών, συγκεκριμένα μητρώα/σχήματα)", + "Members of selected groups can access this view" : "Τα μέλη των επιλεγμένων ομάδων μπορούν να αποκτήσουν πρόσβαση σε αυτήν την προβολή", + "Members:" : "Μέλη:", + "Memory prediction calculated successfully" : "Η πρόβλεψη μνήμης υπολογίστηκε με επιτυχία", + "Message content is required" : "Απαιτείται περιεχόμενο μηνύματος", + "Message does not belong to this conversation" : "Το μήνυμα δεν ανήκει σε αυτήν τη συνομιλία", + "Metadata Filters" : "Φίλτρα μεταδεδομένων", + "Method" : "Μέθοδος", + "Min Length" : "Ελάχιστο μήκος", + "Min execution time (ms)" : "Ελάχιστος χρόνος εκτέλεσης (ms)", + "Min ms" : "Ελάχιστα ms", + "Min result count" : "Ελάχιστο πλήθος αποτελεσμάτων", + "Min results" : "Ελάχιστα αποτελέσματα", + "Minimum" : "Ελάχιστο", + "Minimum value constraint is missing" : "Λείπει ο περιορισμός ελάχιστης τιμής", + "Minimum value is too restrictive" : "Η ελάχιστη τιμή είναι πολύ περιοριστική", + "Missing object fields created successfully" : "Τα πεδία αντικειμένου που έλειπαν δημιουργήθηκαν με επιτυχία", + "Mode:" : "Λειτουργία:", + "Model" : "Μοντέλο", + "Model Name" : "Όνομα μοντέλου", + "Modified" : "Τροποποιήθηκε", + "Monthly" : "Μηνιαία", + "More sources (10-20):" : "Περισσότερες πηγές (10-20):", + "Most Active Objects" : "Πιο ενεργά αντικείμενα", + "Multiple Of" : "Πολλαπλάσιο του", + "Name" : "Όνομα", + "Names cache warmup completed" : "Η προθέρμανση της προσωρινής μνήμης ονομάτων ολοκληρώθηκε", + "New Collection Name" : "Όνομα νέας συλλογής", + "New Conversation" : "Νέα συνομιλία", + "New Properties" : "Νέες ιδιότητες", + "New Property" : "Νέα ιδιότητα", + "New Value" : "Νέα τιμή", + "New users without specific organisation membership will be automatically added to this organisation" : "Οι νέοι χρήστες χωρίς συγκεκριμένη συμμετοχή σε οργανισμό θα προστίθενται αυτόματα σε αυτόν τον οργανισμό", + "Next" : "Επόμενο", + "No" : "Όχι", + "No ConfigSets found" : "Δεν βρέθηκαν ConfigSets", + "No GitHub token configured" : "Δεν έχει διαμορφωθεί διακριτικό GitHub", + "No GitHub token provided" : "Δεν δόθηκε διακριτικό GitHub", + "No GitLab token provided" : "Δεν δόθηκε διακριτικό GitLab", + "No Organisation" : "Κανένας οργανισμός", + "No active filters" : "Δεν υπάρχουν ενεργά φίλτρα", + "No activity data available" : "Δεν υπάρχουν διαθέσιμα δεδομένα δραστηριότητας", + "No agents available" : "Δεν υπάρχουν διαθέσιμοι πράκτορες", + "No agents found" : "Δεν βρέθηκαν πράκτορες", + "No app store cache exists yet" : "Δεν υπάρχει ακόμα προσωρινή μνήμη καταστήματος εφαρμογών", + "No applications are available." : "Δεν υπάρχουν διαθέσιμες εφαρμογές.", + "No applications found" : "Δεν βρέθηκαν εφαρμογές", + "No archived conversations" : "Δεν υπάρχουν αρχειοθετημένες συνομιλίες", + "No audit trail entries found" : "Δεν βρέθηκαν καταχωρήσεις ίχνους ελέγχου", + "No changes recorded" : "Δεν καταγράφηκαν αλλαγές", + "No chunks to vectorize" : "Δεν υπάρχουν τμήματα προς διανυσματοποίηση", + "No collections found" : "Δεν βρέθηκαν συλλογές", + "No configuration data" : "Δεν υπάρχουν δεδομένα διαμόρφωσης", + "No configurations are available." : "Δεν υπάρχουν διαθέσιμες διαμορφώσεις.", + "No configurations found" : "Δεν βρέθηκαν διαμορφώσεις", + "No configurations found for this application." : "Δεν βρέθηκαν διαμορφώσεις για αυτήν την εφαρμογή.", + "No conversations yet. Create a new one to get started!" : "Δεν υπάρχουν ακόμα συνομιλίες. Δημιουργήστε μία νέα για να ξεκινήσετε!", + "No data available" : "Δεν υπάρχουν διαθέσιμα δεδομένα", + "No data available for chart" : "Δεν υπάρχουν διαθέσιμα δεδομένα για το γράφημα", + "No deleted items found" : "Δεν βρέθηκαν διαγραμμένα στοιχεία", + "No deletion data available" : "Δεν υπάρχουν διαθέσιμα δεδομένα διαγραφής", + "No description found" : "Δεν βρέθηκε περιγραφή", + "No description provided" : "Δεν δόθηκε περιγραφή", + "No entities detected in this file. Run text extraction first." : "Δεν εντοπίστηκαν οντότητες σε αυτό το αρχείο. Εκτελέστε πρώτα την εξαγωγή κειμένου.", + "No entities found" : "Δεν βρέθηκαν οντότητες", + "No entities have been detected yet" : "Δεν έχουν εντοπιστεί ακόμα οντότητες", + "No events found" : "Δεν βρέθηκαν συμβάντα", + "No expired audit trails found to clear" : "Δεν βρέθηκαν ληγμένα ίχνη ελέγχου προς εκκαθάριση", + "No facetable fields available. Select a register and schema to see available filters." : "Δεν υπάρχουν διαθέσιμα πεδία όψεων. Επιλέξτε ένα μητρώο και σχήμα για να δείτε τα διαθέσιμα φίλτρα.", + "No files found" : "Δεν βρέθηκαν αρχεία", + "No files have been extracted yet" : "Δεν έχουν εξαχθεί ακόμα αρχεία", + "No files to process" : "Δεν υπάρχουν αρχεία προς επεξεργασία", + "No files to reindex" : "Δεν υπάρχουν αρχεία προς επανευρετηρίαση", + "No filters are currently active. This will delete ALL audit trail entries!" : "Δεν υπάρχουν αυτήν τη στιγμή ενεργά φίλτρα. Αυτό θα διαγράψει ΟΛΕΣ τις καταχωρήσεις ίχνους ελέγχου!", + "No log entries found" : "Δεν βρέθηκαν καταχωρήσεις αρχείου καταγραφής", + "No logs are available for this configuration." : "Δεν υπάρχουν διαθέσιμα αρχεία καταγραφής για αυτήν τη διαμόρφωση.", + "No logs are available for this source." : "Δεν υπάρχουν διαθέσιμα αρχεία καταγραφής για αυτήν την πηγή.", + "No logs found" : "Δεν βρέθηκαν αρχεία καταγραφής", + "No mismatched fields found - SOLR schema is properly configured" : "Δεν βρέθηκαν πεδία που δεν ταιριάζουν - το σχήμα SOLR είναι σωστά διαμορφωμένο", + "No objects found" : "Δεν βρέθηκαν αντικείμενα", + "No objects found to vectorize" : "Δεν βρέθηκαν αντικείμενα προς διανυσματοποίηση", + "No objects selected for deletion" : "Δεν επιλέχθηκαν αντικείμενα για διαγραφή", + "No objects to delete" : "Δεν υπάρχουν αντικείμενα προς διαγραφή", + "No objects were permanently deleted" : "Δεν διαγράφηκαν οριστικά αντικείμενα", + "No properties found" : "Δεν βρέθηκαν ιδιότητες", + "No properties match your filters." : "Καμία ιδιότητα δεν ταιριάζει με τα φίλτρα σας.", + "No purge date set" : "Δεν έχει οριστεί ημερομηνία εκκαθάρισης", + "No register data available" : "Δεν υπάρχουν διαθέσιμα δεδομένα μητρώου", + "No registers found" : "Δεν βρέθηκαν μητρώα", + "No registers found for this application." : "Δεν βρέθηκαν μητρώα για αυτήν την εφαρμογή.", + "No relations found" : "Δεν βρέθηκαν σχέσεις", + "No request body available" : "Δεν υπάρχει διαθέσιμο σώμα αιτήματος", + "No response body available" : "Δεν υπάρχει διαθέσιμο σώμα απόκρισης", + "No saved views yet. Create one in the Search tab!" : "Δεν υπάρχουν ακόμα αποθηκευμένες προβολές. Δημιουργήστε μία στην καρτέλα Αναζήτηση!", + "No schema data available" : "Δεν υπάρχουν διαθέσιμα δεδομένα σχήματος", + "No schema selected for exploration" : "Δεν επιλέχθηκε σχήμα για εξερεύνηση", + "No schemas found" : "Δεν βρέθηκαν σχήματα", + "No schemas found for this application." : "Δεν βρέθηκαν σχήματα για αυτήν την εφαρμογή.", + "No search terms data available" : "Δεν υπάρχουν διαθέσιμα δεδομένα όρων αναζήτησης", + "No search trail entries found" : "Δεν βρέθηκαν καταχωρήσεις ίχνους αναζήτησης", + "No sources are available." : "Δεν υπάρχουν διαθέσιμες πηγές.", + "No sources found" : "Δεν βρέθηκαν πηγές", + "No templates found" : "Δεν βρέθηκαν πρότυπα", + "No templates have been created yet" : "Δεν έχουν δημιουργηθεί ακόμα πρότυπα", + "No valid magic-mapped register+schema combinations found" : "Δεν βρέθηκαν έγκυροι συνδυασμοί μητρώου+σχήματος με magic mapping", + "No views found. Create views first before configuring vectorization." : "Δεν βρέθηκαν προβολές. Δημιουργήστε πρώτα προβολές πριν διαμορφώσετε τη διανυσματοποίηση.", + "No views match your search" : "Καμία προβολή δεν ταιριάζει με την αναζήτησή σας", + "No webhooks found" : "Δεν βρέθηκαν webhooks", + "No webhooks have been configured yet" : "Δεν έχουν διαμορφωθεί ακόμα webhooks", + "No workflows found in this project. Create workflows in the n8n editor." : "Δεν βρέθηκαν ροές εργασίας σε αυτό το έργο. Δημιουργήστε ροές εργασίας στον επεξεργαστή n8n.", + "None" : "Κανένα", + "Not helpful" : "Μη χρήσιμο", + "Not in use" : "Εκτός χρήσης", + "Number Constraints" : "Περιορισμοί αριθμού", + "Number of chunks to vectorize in one API call (1-100)." : "Αριθμός τμημάτων προς διανυσματοποίηση σε μία κλήση API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Αριθμός τμημάτων προς διανυσματοποίηση σε μία κλήση API. Υψηλότερο = ταχύτερο αλλά περισσότερη μνήμη. Συνιστάται: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Αριθμός αντικειμένων προς επεξεργασία σε κάθε παρτίδα (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Αριθμός αντικειμένων προς διανυσματοποίηση σε μία κλήση API. Υψηλότερο = ταχύτερο αλλά περισσότερη μνήμη. Συνιστάται: 10-50.", + "Numeric Range:" : "Αριθμητικό εύρος:", + "Object" : "Αντικείμενο", + "Object Collection" : "Συλλογή αντικειμένων", + "Object Count Prediction" : "Πρόβλεψη πλήθους αντικειμένων", + "Object ID" : "Αναγνωριστικό αντικειμένου", + "Object Management" : "Διαχείριση αντικειμένων", + "Object Vectorization" : "Διανυσματοποίηση αντικειμένων", + "Object Warmup" : "Προθέρμανση αντικειμένων", + "Object collection not configured" : "Η συλλογή αντικειμένων δεν έχει διαμορφωθεί", + "Object not found" : "Το αντικείμενο δεν βρέθηκε", + "Object permanently deleted" : "Το αντικείμενο διαγράφηκε οριστικά", + "Object restored successfully" : "Το αντικείμενο επαναφέρθηκε με επιτυχία", + "Object settings updated successfully" : "Οι ρυθμίσεις αντικειμένου ενημερώθηκαν με επιτυχία", + "Object sources" : "Πηγές αντικειμένων", + "Object unlocked successfully" : "Το αντικείμενο ξεκλειδώθηκε με επιτυχία", + "Object vectorization configuration saved successfully" : "Η διαμόρφωση διανυσματοποίησης αντικειμένων αποθηκεύτηκε με επιτυχία", + "Object vectorized successfully" : "Το αντικείμενο διανυσματοποιήθηκε με επιτυχία", + "Object:" : "Αντικείμενο:", + "Objects" : "Αντικείμενα", + "Objects Analyzed" : "Αντικείμενα που αναλύθηκαν", + "Objects Distribution" : "Κατανομή αντικειμένων", + "Objects being analyzed" : "Αντικείμενα που αναλύονται", + "Objects by Register" : "Αντικείμενα ανά μητρώο", + "Objects by Schema" : "Αντικείμενα ανά σχήμα", + "Objects deletion completed successfully" : "Η διαγραφή αντικειμένων ολοκληρώθηκε με επιτυχία", + "Objects in schema" : "Αντικείμενα στο σχήμα", + "Objects to Process:" : "Αντικείμενα προς επεξεργασία:", + "Objects to be analyzed" : "Αντικείμενα προς ανάλυση", + "Objects to be deleted" : "Αντικείμενα προς διαγραφή", + "Objects to be validated" : "Αντικείμενα προς επικύρωση", + "Objects will be serialized as JSON text before vectorization" : "Τα αντικείμενα θα σειριοποιηθούν ως κείμενο JSON πριν από τη διανυσματοποίηση", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Τα αντικείμενα θα διαγραφούν ήπια (σημειώνονται ως διαγραμμένα αλλά διατηρούνται στη βάση δεδομένων). Μπορούν να ανακτηθούν αργότερα εάν χρειαστεί.", + "Old Value" : "Παλαιά τιμή", + "Oldest Item (days)" : "Παλαιότερο στοιχείο (ημέρες)", + "Ollama Chat Settings" : "Ρυθμίσεις συνομιλίας Ollama", + "Ollama Embedding Configuration" : "Διαμόρφωση ενσωμάτωσης Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Μόνο με αλλαγές", + "Open Register Objects" : "Αντικείμενα Open Register", + "Open n8n Editor" : "Άνοιγμα επεξεργαστή n8n", + "OpenAI Chat Settings" : "Ρυθμίσεις συνομιλίας OpenAI", + "OpenAI Embedding Configuration" : "Διαμόρφωση ενσωμάτωσης OpenAI", + "OpenRegister Settings" : "Ρυθμίσεις OpenRegister", + "Optional webhook secret for signature verification" : "Προαιρετικό μυστικό webhook για επαλήθευση υπογραφής", + "Order" : "Σειρά", + "Organisation" : "Οργανισμός", + "Organisation ID" : "Αναγνωριστικό οργανισμού", + "Organisation Members" : "Μέλη οργανισμού", + "Organisation Statistics" : "Στατιστικά οργανισμού", + "Organisation created successfully" : "Ο οργανισμός δημιουργήθηκε με επιτυχία", + "Organisation settings saved successfully" : "Οι ρυθμίσεις οργανισμού αποθηκεύτηκαν με επιτυχία", + "Organisations" : "Οργανισμοί", + "Organization" : "Οργανισμός", + "Organization ID (Optional)" : "Αναγνωριστικό οργανισμού (Προαιρετικό)", + "Orphaned Items" : "Ορφανά στοιχεία", + "Owner" : "Κάτοχος", + "Owner:" : "Κάτοχος:", + "Parallel Mode (Faster, more resource intensive)" : "Παράλληλη λειτουργία (Ταχύτερη, πιο εντατική σε πόρους)", + "Parallel:" : "Παράλληλα:", + "Password" : "Κωδικός πρόσβασης", + "Password for SOLR authentication (optional)" : "Κωδικός πρόσβασης για έλεγχο ταυτότητας SOLR (προαιρετικό)", + "Path" : "Διαδρομή", + "Path in repository" : "Διαδρομή στο αποθετήριο", + "Path where the configuration file will be saved in the repository" : "Διαδρομή όπου θα αποθηκευτεί το αρχείο διαμόρφωσης στο αποθετήριο", + "Path where the register OAS file will be saved in the repository" : "Διαδρομή όπου θα αποθηκευτεί το αρχείο OAS του μητρώου στο αποθετήριο", + "Pattern (regex)" : "Μοτίβο (regex)", + "Pattern Issue" : "Ζήτημα μοτίβου", + "Pattern constraint is missing" : "Λείπει ο περιορισμός μοτίβου", + "Patterns:" : "Μοτίβα:", + "Pending" : "Σε εκκρεμότητα", + "Permanent Delete Mode" : "Λειτουργία οριστικής διαγραφής", + "Permanently Delete" : "Οριστική διαγραφή", + "Permanently delete already soft-deleted objects" : "Οριστική διαγραφή ήδη ήπια διαγραμμένων αντικειμένων", + "Permanently delete this conversation?" : "Οριστική διαγραφή αυτής της συνομιλίας;", + "Person" : "Πρόσωπο", + "Personal Data" : "Προσωπικά δεδομένα", + "Phone" : "Τηλέφωνο", + "Photos" : "Φωτογραφίες", + "Please create an agent in the" : "Παρακαλώ δημιουργήστε έναν πράκτορα στο", + "Please select an agent to continue" : "Παρακαλώ επιλέξτε έναν πράκτορα για να συνεχίσετε", + "Please select which register and schema to use for the new object" : "Παρακαλώ επιλέξτε ποιο μητρώο και σχήμα θα χρησιμοποιήσετε για το νέο αντικείμενο", + "Please try again later." : "Παρακαλώ δοκιμάστε ξανά αργότερα.", + "Please wait while we fetch your agents." : "Παρακαλώ περιμένετε όσο ανακτούμε τους πράκτορές σας.", + "Please wait while we fetch your applications." : "Παρακαλώ περιμένετε όσο ανακτούμε τις εφαρμογές σας.", + "Please wait while we fetch your configurations." : "Παρακαλώ περιμένετε όσο ανακτούμε τις διαμορφώσεις σας.", + "Please wait while we fetch your deleted items." : "Παρακαλώ περιμένετε όσο ανακτούμε τα διαγραμμένα στοιχεία σας.", + "Please wait while we fetch your sources." : "Παρακαλώ περιμένετε όσο ανακτούμε τις πηγές σας.", + "Polls" : "Δημοσκοπήσεις", + "Popular Search Terms" : "Δημοφιλείς όροι αναζήτησης", + "Port" : "Θύρα", + "Prerequisites" : "Προαπαιτούμενα", + "Press Enter to send, Shift+Enter for new line" : "Πατήστε Enter για αποστολή, Shift+Enter για νέα γραμμή", + "Previous" : "Προηγούμενο", + "Private" : "Ιδιωτικό", + "Processes chunks in batches with simulated parallelism." : "Επεξεργάζεται τμήματα σε παρτίδες με προσομοιωμένο παραλληλισμό.", + "Processes file chunks sequentially (safest)." : "Επεξεργάζεται τα τμήματα αρχείων διαδοχικά (ασφαλέστερο).", + "Processes objects in chunks with simulated parallelism." : "Επεξεργάζεται αντικείμενα σε τμήματα με προσομοιωμένο παραλληλισμό.", + "Processes objects sequentially (safest)." : "Επεξεργάζεται αντικείμενα διαδοχικά (ασφαλέστερο).", + "Processing" : "Επεξεργασία", + "Processing Limits" : "Όρια επεξεργασίας", + "Processing..." : "Επεξεργασία...", + "Project initialized successfully" : "Το έργο αρχικοποιήθηκε με επιτυχία", + "Project not found. Please initialize first." : "Το έργο δεν βρέθηκε. Παρακαλώ αρχικοποιήστε πρώτα.", + "Properties" : "Ιδιότητες", + "Property Behaviors" : "Συμπεριφορές ιδιότητας", + "Property Configuration:" : "Διαμόρφωση ιδιότητας:", + "Property Filters" : "Φίλτρα ιδιοτήτων", + "Property Title" : "Τίτλος ιδιότητας", + "Property Type" : "Τύπος ιδιότητας", + "Property can be improved" : "Η ιδιότητα μπορεί να βελτιωθεί", + "Protected" : "Προστατευμένο", + "Provider" : "Πάροχος", + "Provider is required for testing" : "Απαιτείται πάροχος για τη δοκιμή", + "Public" : "Δημόσιο", + "Public View" : "Δημόσια προβολή", + "Public views can be accessed by anyone in the system" : "Οι δημόσιες προβολές είναι προσβάσιμες από οποιονδήποτε στο σύστημα", + "Publish" : "Δημοσίευση", + "Publish Objects" : "Δημοσίευση αντικειμένων", + "Publish all objects in this schema" : "Δημοσίευση όλων των αντικειμένων σε αυτό το σχήμα", + "Published" : "Δημοσιεύθηκε", + "Published:" : "Δημοσιεύθηκε:", + "Publishing..." : "Δημοσίευση...", + "Purge" : "Εκκαθάριση", + "Purge Date" : "Ημερομηνία εκκαθάρισης", + "Query Complexity Distribution" : "Κατανομή πολυπλοκότητας ερωτημάτων", + "Query parameter is required" : "Απαιτείται παράμετρος ερωτήματος", + "RAG" : "RAG", + "RAG Configuration" : "Διαμόρφωση RAG", + "Rate limits cleared successfully" : "Τα όρια ρυθμού εκκαθαρίστηκαν με επιτυχία", + "Raw Changes Data" : "Ακατέργαστα δεδομένα αλλαγών", + "Re-vectorize on object update" : "Επαναδιανυσματοποίηση κατά την ενημέρωση αντικειμένου", + "Read" : "Ανάγνωση", + "Recommendations:" : "Συστάσεις:", + "Recommended Type:" : "Συνιστώμενος τύπος:", + "Recommended: 5 sources" : "Συνιστάται: 5 πηγές", + "Refresh" : "Ανανέωση", + "Refresh Data" : "Ανανέωση δεδομένων", + "Refresh Stats" : "Ανανέωση στατιστικών", + "Refresh Workflows" : "Ανανέωση ροών εργασίας", + "Refresh dashboard" : "Ανανέωση πίνακα ελέγχου", + "Refresh database info" : "Ανανέωση πληροφοριών βάσης δεδομένων", + "Register" : "Μητρώο", + "Register ID" : "Αναγνωριστικό μητρώου", + "Register Statistics" : "Στατιστικά μητρώου", + "Register Totals" : "Σύνολα μητρώου", + "Register depublished successfully" : "Η δημοσίευση του μητρώου αναιρέθηκε με επιτυχία", + "Register not found" : "Το μητρώο δεν βρέθηκε", + "Register objects deletion completed successfully" : "Η διαγραφή αντικειμένων μητρώου ολοκληρώθηκε με επιτυχία", + "Register published successfully" : "Το μητρώο δημοσιεύθηκε με επιτυχία", + "Register/Schema Usage" : "Χρήση μητρώου/σχήματος", + "Registers" : "Μητρώα", + "Reindex" : "Επανευρετηρίαση", + "Reindex completed" : "Η επανευρετηρίαση ολοκληρώθηκε", + "Reindex completed successfully" : "Η επανευρετηρίαση ολοκληρώθηκε με επιτυχία", + "Reindex failed" : "Η επανευρετηρίαση απέτυχε", + "Relations" : "Σχέσεις", + "Remote Version" : "Απομακρυσμένη έκδοση", + "Remove" : "Αφαίρεση", + "Remove filter" : "Αφαίρεση φίλτρου", + "Remove from favorites" : "Αφαίρεση από τα αγαπημένα", + "Remove group" : "Αφαίρεση ομάδας", + "Removed" : "Αφαιρέθηκε", + "Removed from favorites" : "Αφαιρέθηκε από τα αγαπημένα", + "Rename Conversation" : "Μετονομασία συνομιλίας", + "Rename conversation" : "Μετονομασία συνομιλίας", + "Replicas" : "Αντίγραφα", + "Repository" : "Αποθετήριο", + "Request Body" : "Σώμα αιτήματος", + "Request Data" : "Δεδομένα αιτήματος", + "Request Details" : "Λεπτομέρειες αιτήματος", + "Request body must be JSON with a \"query\" field" : "Το σώμα του αιτήματος πρέπει να είναι JSON με ένα πεδίο \"query\"", + "Request timeout in seconds" : "Χρονικό όριο αιτήματος σε δευτερόλεπτα", + "Required field" : "Υποχρεωτικό πεδίο", + "Required status is inconsistent" : "Η κατάσταση υποχρεωτικότητας είναι ασυνεπής", + "Rerun Search" : "Επανεκτέλεση αναζήτησης", + "Reset Changes" : "Επαναφορά αλλαγών", + "Reset Filters" : "Επαναφορά φίλτρων", + "Response Body" : "Σώμα απόκρισης", + "Response Details" : "Λεπτομέρειες απόκρισης", + "Response Time:" : "Χρόνος απόκρισης:", + "Restore" : "Επαναφορά", + "Restore conversation" : "Επαναφορά συνομιλίας", + "Restore or permanently delete items" : "Επαναφορά ή οριστική διαγραφή στοιχείων", + "Result Count Range" : "Εύρος πλήθους αποτελεσμάτων", + "Results" : "Αποτελέσματα", + "Retrieve all objects for this schema" : "Ανάκτηση όλων των αντικειμένων για αυτό το σχήμα", + "Retry" : "Επανάληψη", + "Retry Failed Extractions" : "Επανάληψη αποτυχημένων εξαγωγών", + "Retry Policy" : "Πολιτική επανάληψης", + "Retry completed" : "Η επανάληψη ολοκληρώθηκε", + "Retrying..." : "Επανάληψη...", + "Risk Level" : "Επίπεδο κινδύνου", + "Roles updated successfully" : "Οι ρόλοι ενημερώθηκαν με επιτυχία", + "SOLR Connection Settings" : "Ρυθμίσεις σύνδεσης SOLR", + "SOLR Version:" : "Έκδοση SOLR:", + "SOLR actions menu" : "Μενού ενεργειών SOLR", + "SOLR base path (usually /solr)" : "Βασική διαδρομή SOLR (συνήθως /solr)", + "SOLR is disabled" : "Το SOLR είναι απενεργοποιημένο", + "SOLR is not available or not configured" : "Το SOLR δεν είναι διαθέσιμο ή δεν έχει διαμορφωθεί", + "SOLR logging disabled" : "Η καταγραφή SOLR απενεργοποιήθηκε", + "SOLR logging enabled" : "Η καταγραφή SOLR ενεργοποιήθηκε", + "SOLR search disabled" : "Η αναζήτηση SOLR απενεργοποιήθηκε", + "SOLR search enabled" : "Η αναζήτηση SOLR ενεργοποιήθηκε", + "SOLR server hostname or IP address" : "Όνομα κεντρικού υπολογιστή ή διεύθυνση IP του διακομιστή SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Αριθμός θύρας διακομιστή SOLR (προαιρετικό, προεπιλογή 8983)", + "SOLR setup completed successfully" : "Η εγκατάσταση SOLR ολοκληρώθηκε με επιτυχία", + "SOLR setup failed" : "Η εγκατάσταση SOLR απέτυχε", + "SOLR setup failed - check logs" : "Η εγκατάσταση SOLR απέτυχε - ελέγξτε τα αρχεία καταγραφής", + "Sample Values:" : "Δείγματα τιμών:", + "Save" : "Αποθήκευση", + "Save Configuration" : "Αποθήκευση διαμόρφωσης", + "Save Connection Settings" : "Αποθήκευση ρυθμίσεων σύνδεσης", + "Save Roles" : "Αποθήκευση ρόλων", + "Save Settings" : "Αποθήκευση ρυθμίσεων", + "Save current search as view" : "Αποθήκευση τρέχουσας αναζήτησης ως προβολή", + "Saved Views" : "Αποθηκευμένες προβολές", + "Saving..." : "Αποθήκευση...", + "Schema" : "Σχήμα", + "Schema ID" : "Αναγνωριστικό σχήματος", + "Schema Name" : "Όνομα σχήματος", + "Schema Statistics" : "Στατιστικά σχήματος", + "Schema depublished successfully" : "Η δημοσίευση του σχήματος αναιρέθηκε με επιτυχία", + "Schema objects deletion completed successfully" : "Η διαγραφή αντικειμένων σχήματος ολοκληρώθηκε με επιτυχία", + "Schema published successfully" : "Το σχήμα δημοσιεύθηκε με επιτυχία", + "Schema successfully created" : "Το σχήμα δημιουργήθηκε με επιτυχία", + "Schema successfully updated" : "Το σχήμα ενημερώθηκε με επιτυχία", + "Schemas" : "Σχήματα", + "Scheme" : "Σχήμα", + "Search / Views" : "Αναζήτηση / Προβολές", + "Search Activity" : "Δραστηριότητα αναζήτησης", + "Search Analytics" : "Αναλυτικά στοιχεία αναζήτησης", + "Search Entities" : "Αναζήτηση οντοτήτων", + "Search Files" : "Αναζήτηση αρχείων", + "Search GitHub" : "Αναζήτηση στο GitHub", + "Search GitLab" : "Αναζήτηση στο GitLab", + "Search Method" : "Μέθοδος αναζήτησης", + "Search Term" : "Όρος αναζήτησης", + "Search Trail Details" : "Λεπτομέρειες ίχνους αναζήτησης", + "Search Trail Management" : "Διαχείριση ίχνους αναζήτησης", + "Search Trail Statistics" : "Στατιστικά ίχνους αναζήτησης", + "Search Trails" : "Ίχνη αναζήτησης", + "Search Views" : "Αναζήτηση προβολών", + "Search Webhooks" : "Αναζήτηση webhooks", + "Search by file name or path" : "Αναζήτηση κατά όνομα ή διαδρομή αρχείου", + "Search by name or URL" : "Αναζήτηση κατά όνομα ή URL", + "Search by value" : "Αναζήτηση κατά τιμή", + "Search in Files" : "Αναζήτηση στα αρχεία", + "Search in Objects" : "Αναζήτηση στα αντικείμενα", + "Search property names..." : "Αναζήτηση ονομάτων ιδιοτήτων...", + "Search trail deletion not implemented yet" : "Η διαγραφή ίχνους αναζήτησης δεν έχει υλοποιηθεί ακόμα", + "Search views..." : "Αναζήτηση προβολών...", + "Secret" : "Μυστικό", + "Secret key for HMAC signature generation (optional)" : "Μυστικό κλειδί για τη δημιουργία υπογραφής HMAC (προαιρετικό)", + "Select AI Agent" : "Επιλογή πράκτορα τεχνητής νοημοσύνης", + "Select All" : "Επιλογή όλων", + "Select ConfigSet" : "Επιλογή ConfigSet", + "Select File Types to Vectorize:" : "Επιλογή τύπων αρχείων προς διανυσματοποίηση:", + "Select HTTP method" : "Επιλογή μεθόδου HTTP", + "Select Register and Schema" : "Επιλογή μητρώου και σχήματος", + "Select Views to Vectorize:" : "Επιλογή προβολών προς διανυσματοποίηση:", + "Select a Nextcloud group to add" : "Επιλέξτε μια ομάδα Nextcloud για προσθήκη", + "Select a branch" : "Επιλέξτε έναν κλάδο", + "Select a model or type a custom model name" : "Επιλέξτε ένα μοντέλο ή πληκτρολογήστε ένα προσαρμοσμένο όνομα μοντέλου", + "Select a repository" : "Επιλέξτε ένα αποθετήριο", + "Select a repository you have write access to" : "Επιλέξτε ένα αποθετήριο στο οποίο έχετε πρόσβαση εγγραφής", + "Select an AI Agent" : "Επιλέξτε έναν πράκτορα τεχνητής νοημοσύνης", + "Select an AI agent to begin chatting with your data." : "Επιλέξτε έναν πράκτορα τεχνητής νοημοσύνης για να αρχίσετε να συνομιλείτε με τα δεδομένα σας.", + "Select backend" : "Επιλογή backend", + "Select chat model" : "Επιλογή μοντέλου συνομιλίας", + "Select collection for files" : "Επιλογή συλλογής για αρχεία", + "Select collection for objects" : "Επιλογή συλλογής για αντικείμενα", + "Select default organisation" : "Επιλογή προεπιλεγμένου οργανισμού", + "Select event to listen to..." : "Επιλέξτε συμβάν για ακρόαση...", + "Select model" : "Επιλογή μοντέλου", + "Select options..." : "Επιλέξτε επιλογές...", + "Select period" : "Επιλογή περιόδου", + "Select property to send as payload" : "Επιλέξτε ιδιότητα προς αποστολή ως ωφέλιμο φορτίο", + "Select provider" : "Επιλογή παρόχου", + "Select registers and schemas to save a view" : "Επιλέξτε μητρώα και σχήματα για να αποθηκεύσετε μια προβολή", + "Select retry policy" : "Επιλογή πολιτικής επανάληψης", + "Select the branch to publish to" : "Επιλέξτε τον κλάδο για δημοσίευση", + "Select the event this webhook should listen to" : "Επιλέξτε το συμβάν στο οποίο πρέπει να ακούει αυτό το webhook", + "Select views to vectorize:" : "Επιλογή προβολών προς διανυσματοποίηση:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Επιλέξτε ποιες ομάδες Nextcloud είναι διαθέσιμες για αυτόν τον οργανισμό. Οι χρήστες σε αυτές τις ομάδες θα έχουν πρόσβαση στους πόρους του οργανισμού.", + "Select which data views the AI can search" : "Επιλέξτε ποιες προβολές δεδομένων μπορεί να αναζητήσει η τεχνητή νοημοσύνη", + "Select which property from the event should be used as the webhook payload data" : "Επιλέξτε ποια ιδιότητα από το συμβάν θα χρησιμοποιηθεί ως δεδομένα ωφέλιμου φορτίου του webhook", + "Select which tools the AI can use to perform actions" : "Επιλέξτε ποια εργαλεία μπορεί να χρησιμοποιήσει η τεχνητή νοημοσύνη για την εκτέλεση ενεργειών", + "Selected" : "Επιλεγμένο", + "Selected Groups" : "Επιλεγμένες ομάδες", + "Selected audit trails deleted successfully" : "Τα επιλεγμένα ίχνη ελέγχου διαγράφηκαν με επιτυχία", + "Selected search trails deleted successfully" : "Τα επιλεγμένα ίχνη αναζήτησης διαγράφηκαν με επιτυχία", + "Selected users can access this view" : "Οι επιλεγμένοι χρήστες μπορούν να αποκτήσουν πρόσβαση σε αυτήν την προβολή", + "Send additional feedback" : "Αποστολή πρόσθετων σχολίων", + "Send as CloudEvent" : "Αποστολή ως CloudEvent", + "Sensitive PII" : "Ευαίσθητα PII", + "Serial Mode (Safer, slower)" : "Διαδοχική λειτουργία (Ασφαλέστερη, πιο αργή)", + "Serial:" : "Διαδοχικά:", + "Server Configuration" : "Διαμόρφωση διακομιστή", + "Server Information" : "Πληροφορίες διακομιστή", + "Settings" : "Ρυθμίσεις", + "Shards" : "Shards", + "Share" : "Κοινή χρήση", + "Share with Groups" : "Κοινή χρήση με ομάδες", + "Share with Users" : "Κοινή χρήση με χρήστες", + "Show Filters" : "Εμφάνιση φίλτρων", + "Show less" : "Εμφάνιση λιγότερων", + "Show only entries with changes" : "Εμφάνιση μόνο καταχωρήσεων με αλλαγές", + "Simple" : "Απλό", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Απλά ερωτήματα: Βασικές αναζητήσεις κειμένου με ελάχιστες παραμέτρους (π.χ. μεμονωμένος όρος αναζήτησης, χωρίς φίλτρα για προχωρημένους)", + "Size" : "Μέγεθος", + "Slug" : "Slug", + "Soft Delete Mode" : "Λειτουργία ήπιας διαγραφής", + "Soft Deleted Items" : "Στοιχεία ήπιας διαγραφής", + "SolrCloud mode enabled" : "Η λειτουργία SolrCloud ενεργοποιήθηκε", + "Source" : "Πηγή", + "Sources" : "Πηγές", + "Sources:" : "Πηγές:", + "Standalone SOLR mode" : "Αυτόνομη λειτουργία SOLR", + "Start Conversation" : "Έναρξη συνομιλίας", + "Start Vectorization" : "Έναρξη διανυσματοποίησης", + "Start a conversation" : "Ξεκινήστε μια συνομιλία", + "Starting names cache warmup..." : "Έναρξη προθέρμανσης προσωρινής μνήμης ονομάτων...", + "Starting..." : "Έναρξη...", + "Statistics" : "Στατιστικά", + "Stats calculation not yet implemented" : "Ο υπολογισμός στατιστικών δεν έχει υλοποιηθεί ακόμα", + "Status" : "Κατάσταση", + "Status Code" : "Κωδικός κατάστασης", + "String Constraints" : "Περιορισμοί συμβολοσειράς", + "Success" : "Επιτυχία", + "Success Rate" : "Ποσοστό επιτυχίας", + "Success Status" : "Κατάσταση επιτυχίας", + "Successful" : "Επιτυχές", + "Successfully joined organisation" : "Επιτυχής συμμετοχή στον οργανισμό", + "Sync Table" : "Συγχρονισμός πίνακα", + "Synchronization Settings" : "Ρυθμίσεις συγχρονισμού", + "System Default" : "Προεπιλογή συστήματος", + "Technical Description" : "Τεχνική περιγραφή", + "Technical description for developers and administrators" : "Τεχνική περιγραφή για προγραμματιστές και διαχειριστές", + "Temperature" : "Θερμοκρασία", + "Templates" : "Πρότυπα", + "Test" : "Δοκιμή", + "Test Chat" : "Δοκιμή συνομιλίας", + "Test Connection" : "Δοκιμή σύνδεσης", + "Test Embedding" : "Δοκιμή ενσωμάτωσης", + "Test endpoint executed successfully" : "Το τελικό σημείο δοκιμής εκτελέστηκε με επιτυχία", + "Test webhook delivered successfully" : "Το δοκιμαστικό webhook παραδόθηκε με επιτυχία", + "Test webhook delivery failed" : "Η παράδοση του δοκιμαστικού webhook απέτυχε", + "Test webhook sent successfully" : "Το δοκιμαστικό webhook στάλθηκε με επιτυχία", + "Testing..." : "Δοκιμή...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Τα τμήματα κειμένου δημιουργούνται κατά την εξαγωγή αρχείων και αποθηκεύονται στη βάση δεδομένων. Η διανυσματοποίηση διαβάζει αυτά τα προτμηματοποιημένα αρχεία και τα μετατρέπει σε ενσωματώσεις.", + "Text extracted successfully" : "Το κείμενο εξήχθη με επιτυχία", + "Text extraction disabled" : "Η εξαγωγή κειμένου απενεργοποιήθηκε", + "The URL where webhook events will be sent" : "Το URL όπου θα αποστέλλονται τα συμβάντα webhook", + "The requested conversation does not exist" : "Η ζητούμενη συνομιλία δεν υπάρχει", + "The saved view and all its search configuration" : "Η αποθηκευμένη προβολή και όλη η διαμόρφωση αναζήτησής της", + "There are no audit trail entries matching your current filters." : "Δεν υπάρχουν καταχωρήσεις ίχνους ελέγχου που να ταιριάζουν με τα τρέχοντα φίλτρα σας.", + "There are no deleted items matching your current filters." : "Δεν υπάρχουν διαγραμμένα στοιχεία που να ταιριάζουν με τα τρέχοντα φίλτρα σας.", + "There are no search trail entries matching your current filters." : "Δεν υπάρχουν καταχωρήσεις ίχνους αναζήτησης που να ταιριάζουν με τα τρέχοντα φίλτρα σας.", + "There are no webhook log entries matching your filters." : "Δεν υπάρχουν καταχωρήσεις αρχείου καταγραφής webhook που να ταιριάζουν με τα φίλτρα σας.", + "This action cannot be undone." : "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί. Βεβαιωθείτε ότι καμία συλλογή δεν χρησιμοποιεί αυτό το ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Αυτός ο πράκτορας δεν έχει διαμορφώσιμες προβολές ή εργαλεία, αλλά μπορείτε ακόμα να προσαρμόσετε τις ρυθμίσεις RAG παρακάτω.", + "This analysis may take some time" : "Αυτή η ανάλυση μπορεί να διαρκέσει κάποιο χρόνο", + "This audit trail entry does not contain any change information." : "Αυτή η καταχώρηση ίχνους ελέγχου δεν περιέχει καμία πληροφορία αλλαγής.", + "This configuration has no data defined." : "Αυτή η διαμόρφωση δεν έχει ορισμένα δεδομένα.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Αυτό το τελικό σημείο έχει καταργηθεί. Χρησιμοποιήστε αντ' αυτού τελικά σημεία βασισμένα σε τμήματα.", + "This entity has no relations to objects or files" : "Αυτή η οντότητα δεν έχει σχέσεις με αντικείμενα ή αρχεία", + "This is a test webhook from OpenRegister" : "Αυτό είναι ένα δοκιμαστικό webhook από το OpenRegister", + "This schema must use Magic Table configuration to sync" : "Αυτό το σχήμα πρέπει να χρησιμοποιεί διαμόρφωση Magic Table για συγχρονισμό", + "This source has no associated registers." : "Αυτή η πηγή δεν έχει συσχετισμένα μητρώα.", + "This will copy the _default ConfigSet with the new name" : "Αυτό θα αντιγράψει το ConfigSet _default με το νέο όνομα", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Αυτό θα διαγράψει οριστικά ΟΛΕΣ τις ενσωματώσεις (διανύσματα) από τη βάση δεδομένων. Θα χρειαστεί να επαναδιανυσματοποιήσετε όλα τα αντικείμενα και τα αρχεία. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.\\n\\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;", + "This will permanently delete:" : "Αυτό θα διαγράψει οριστικά:", + "Timeout (seconds)" : "Χρονικό όριο (δευτερόλεπτα)", + "Timestamp" : "Χρονική σήμανση", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Συμβουλή: Ενεργοποιήστε μόνο τις προβολές που χρειάζονται σημασιολογική αναζήτηση για να ελαχιστοποιήσετε το κόστος ενσωμάτωσης. Οι απλοί πίνακες αναζήτησης σπάνια χρειάζονται διανυσματοποίηση.", + "Title" : "Τίτλος", + "To Date" : "Έως ημερομηνία", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Για να προσαρμόσετε το μέγεθος ή τη στρατηγική τμημάτων, μεταβείτε στη Διαμόρφωση αρχείων → Όρια επεξεργασίας.", + "To change the embedding provider or model, go to LLM Configuration." : "Για να αλλάξετε τον πάροχο ή το μοντέλο ενσωμάτωσης, μεταβείτε στη Διαμόρφωση LLM.", + "To date" : "Έως ημερομηνία", + "Toggle search sidebar" : "Εναλλαγή πλευρικής γραμμής αναζήτησης", + "Tools" : "Εργαλεία", + "Top Deleters" : "Κορυφαίοι διαγραφείς", + "Total" : "Σύνολο", + "Total Audit Trails" : "Σύνολο ιχνών ελέγχου", + "Total Chunks Available:" : "Σύνολο διαθέσιμων τμημάτων:", + "Total Deleted Items" : "Σύνολο διαγραμμένων στοιχείων", + "Total Members" : "Σύνολο μελών", + "Total Objects" : "Σύνολο αντικειμένων", + "Total Objects in Database:" : "Σύνολο αντικειμένων στη βάση δεδομένων:", + "Total Organisations" : "Σύνολο οργανισμών", + "Total Results" : "Σύνολο αποτελεσμάτων", + "Total Searches" : "Σύνολο αναζητήσεων", + "Total Size" : "Συνολικό μέγεθος", + "Total Storage" : "Συνολική αποθήκευση", + "Total:" : "Σύνολο:", + "Totals" : "Σύνολα", + "Type" : "Τύπος", + "Type Issue" : "Ζήτημα τύπου", + "Type Variations:" : "Παραλλαγές τύπου:", + "Type must be \"positive\" or \"negative\"" : "Ο τύπος πρέπει να είναι \"positive\" ή \"negative\"", + "Type to search for groups" : "Πληκτρολογήστε για αναζήτηση ομάδων", + "Type to search for users" : "Πληκτρολογήστε για αναζήτηση χρηστών", + "Type to search groups..." : "Πληκτρολογήστε για αναζήτηση ομάδων...", + "Type to search users..." : "Πληκτρολογήστε για αναζήτηση χρηστών...", + "URL" : "URL", + "URL where Ollama is running" : "URL όπου εκτελείται το Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Αμετάβλητο", + "Unique Search Terms" : "Μοναδικοί όροι αναζήτησης", + "Unique Terms" : "Μοναδικοί όροι", + "Unknown" : "Άγνωστο", + "Unknown Register" : "Άγνωστο μητρώο", + "Unknown Schema" : "Άγνωστο σχήμα", + "Untitled View" : "Προβολή χωρίς τίτλο", + "Update" : "Ενημέρωση", + "Update Operations" : "Λειτουργίες ενημέρωσης", + "Update configuration: ..." : "Ενημέρωση διαμόρφωσης: ...", + "Update register OAS: ..." : "Ενημέρωση OAS μητρώου: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Ενημέρωση διανυσμάτων όταν αλλάζουν τα δεδομένα αντικειμένου (συνιστάται για ακριβή αναζήτηση)", + "Updated" : "Ενημερώθηκε", + "Updated At" : "Ενημερώθηκε στις", + "Updated:" : "Ενημερώθηκε:", + "Uptime:" : "Χρόνος λειτουργίας:", + "Usage Count" : "Πλήθος χρήσεων", + "Use SolrCloud with Zookeeper for distributed search" : "Χρησιμοποιήστε το SolrCloud με Zookeeper για κατανεμημένη αναζήτηση", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Χρησιμοποιήστε φίλτρα για να περιορίσετε τις καταχωρήσεις ίχνους ελέγχου κατά μητρώο, σχήμα, τύπο ενέργειας, χρήστη, εύρος ημερομηνιών ή αναγνωριστικό αντικειμένου.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Χρησιμοποιήστε φίλτρα για να περιορίσετε τα διαγραμμένα στοιχεία κατά μητρώο, σχήμα, ημερομηνία διαγραφής ή χρήστη που τα διέγραψε.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Χρησιμοποιήστε φίλτρα για να περιορίσετε τις καταχωρήσεις ίχνους αναζήτησης κατά μητρώο, σχήμα, κατάσταση επιτυχίας, χρήστη, εύρος ημερομηνιών, όρους αναζήτησης ή μετρήσεις απόδοσης.", + "Used By Collections" : "Χρησιμοποιείται από συλλογές", + "User" : "Χρήστης", + "User Agent Statistics" : "Στατιστικά User Agent", + "User Description" : "Περιγραφή χρήστη", + "User rate limits cleared successfully" : "Τα όρια ρυθμού χρήστη εκκαθαρίστηκαν με επιτυχία", + "User removed successfully" : "Ο χρήστης αφαιρέθηκε με επιτυχία", + "User-friendly description shown in forms and help text" : "Φιλική προς τον χρήστη περιγραφή που εμφανίζεται στις φόρμες και στο κείμενο βοήθειας", + "Username" : "Όνομα χρήστη", + "Username for SOLR authentication (optional)" : "Όνομα χρήστη για έλεγχο ταυτότητας SOLR (προαιρετικό)", + "Users" : "Χρήστες", + "Using Pre-Generated Chunks" : "Χρήση προδημιουργημένων τμημάτων", + "Validate" : "Επικύρωση", + "Validate Objects" : "Επικύρωση αντικειμένων", + "Validation completed successfully" : "Η επικύρωση ολοκληρώθηκε με επιτυχία", + "Validation failed" : "Η επικύρωση απέτυχε", + "Value" : "Τιμή", + "Vector Dimensions" : "Διαστάσεις διανύσματος", + "Vector Search Backend" : "Backend διανυσματικής αναζήτησης", + "Vector field: _embedding_" : "Πεδίο διανύσματος: _embedding_", + "Vectorization Triggers" : "Ενεργοποιητές διανυσματοποίησης", + "Vectorize All Files" : "Διανυσματοποίηση όλων των αρχείων", + "Vectorize All Objects" : "Διανυσματοποίηση όλων των αντικειμένων", + "Vectorize all file types" : "Διανυσματοποίηση όλων των τύπων αρχείων", + "Vectorize all views" : "Διανυσματοποίηση όλων των προβολών", + "Vectorize on object creation" : "Διανυσματοποίηση κατά τη δημιουργία αντικειμένου", + "Vectors will be stored in your existing object and file collections" : "Τα διανύσματα θα αποθηκευτούν στις υπάρχουσες συλλογές αντικειμένων και αρχείων σας", + "Version" : "Έκδοση", + "Very High" : "Πολύ υψηλό", + "View API Docs" : "Προβολή εγγράφων API", + "View Changes" : "Προβολή αλλαγών", + "View Details" : "Προβολή λεπτομερειών", + "View Error" : "Προβολή σφάλματος", + "View File" : "Προβολή αρχείου", + "View Full Details" : "Προβολή πλήρων λεπτομερειών", + "View Logs" : "Προβολή αρχείων καταγραφής", + "View Name" : "Όνομα προβολής", + "View Object" : "Προβολή αντικειμένου", + "View Parameters" : "Προβολή παραμέτρων", + "View Selection" : "Επιλογή προβολών", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Προβάλετε και αναλύστε αρχεία καταγραφής ιχνών αναζήτησης με δυνατότητες φιλτραρίσματος για προχωρημένους και αναλυτικών στοιχείων", + "View and analyze system audit trails with advanced filtering capabilities" : "Προβάλετε και αναλύστε ίχνη ελέγχου συστήματος με δυνατότητες φιλτραρίσματος για προχωρημένους", + "View deleted successfully" : "Η προβολή διαγράφηκε με επιτυχία", + "View entity details and manage relations" : "Προβάλετε λεπτομέρειες οντότητας και διαχειριστείτε σχέσεις", + "View name" : "Όνομα προβολής", + "View name is required" : "Απαιτείται όνομα προβολής", + "View saved successfully!" : "Η προβολή αποθηκεύτηκε με επιτυχία!", + "View search analytics and manage search logs" : "Προβάλετε αναλυτικά στοιχεία αναζήτησης και διαχειριστείτε αρχεία καταγραφής αναζήτησης", + "View successfully deleted" : "Η προβολή διαγράφηκε με επιτυχία", + "View successfully updated" : "Η προβολή ενημερώθηκε με επιτυχία", + "View updated successfully!" : "Η προβολή ενημερώθηκε με επιτυχία!", + "View webhook delivery logs and filter by webhook" : "Προβάλετε αρχεία καταγραφής παράδοσης webhook και φιλτράρετε κατά webhook", + "Views" : "Προβολές", + "Visible to users" : "Ορατό στους χρήστες", + "Wait for Response" : "Αναμονή για απόκριση", + "Wait for webhook response before continuing (required for request/response flows)" : "Αναμονή για απόκριση webhook πριν τη συνέχιση (απαιτείται για ροές αιτήματος/απόκρισης)", + "Warmup Names Cache" : "Προθέρμανση προσωρινής μνήμης ονομάτων", + "Warning" : "Προειδοποίηση", + "Warning:" : "Προειδοποίηση:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Λεπτομέρειες αρχείου καταγραφής webhook", + "Webhook Logs" : "Αρχεία καταγραφής webhook", + "Webhook created successfully" : "Το webhook δημιουργήθηκε με επιτυχία", + "Webhook deleted" : "Το webhook διαγράφηκε", + "Webhook retry delivered successfully" : "Η επανάληψη του webhook παραδόθηκε με επιτυχία", + "Webhook retry delivery failed" : "Η παράδοση της επανάληψης του webhook απέτυχε", + "Webhook updated" : "Το webhook ενημερώθηκε", + "Webhook updated successfully" : "Το webhook ενημερώθηκε με επιτυχία", + "Webhooks" : "Webhooks", + "Weekly" : "Εβδομαδιαία", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Τυλίξτε το ωφέλιμο φορτίο του webhook σε μορφή CloudEvents για καλύτερη διαλειτουργικότητα", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Ναι", + "You" : "Εσείς", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Μπορείτε να δημιουργήσετε νέα ConfigSets βασισμένα στο πρότυπο _default ή να μεταφορτώσετε προσαρμοσμένα απευθείας στον διακομιστή SOLR σας.", + "You do not have access to this conversation" : "Δεν έχετε πρόσβαση σε αυτήν τη συνομιλία", + "You do not have permission to delete this conversation" : "Δεν έχετε δικαίωμα να διαγράψετε αυτήν τη συνομιλία", + "You do not have permission to modify this conversation" : "Δεν έχετε δικαίωμα να τροποποιήσετε αυτήν τη συνομιλία", + "You do not have permission to restore this conversation" : "Δεν έχετε δικαίωμα να επαναφέρετε αυτήν τη συνομιλία", + "You must be logged in to favorite views" : "Πρέπει να είστε συνδεδεμένοι για να προσθέσετε προβολές στα αγαπημένα", + "You need an AI agent to start a conversation." : "Χρειάζεστε έναν πράκτορα τεχνητής νοημοσύνης για να ξεκινήσετε μια συνομιλία.", + "Your Fireworks AI API key. Get one at" : "Το κλειδί API Fireworks AI σας. Αποκτήστε ένα στο", + "Your OpenAI API key. Get one at" : "Το κλειδί API OpenAI σας. Αποκτήστε ένα στο", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Τα σχόλιά σας καταγράφηκαν. Προαιρετικά, μπορείτε να παρέχετε πρόσθετες λεπτομέρειες εδώ...", + "Zookeeper Hosts" : "Κεντρικοί υπολογιστές Zookeeper", + "Zookeeper Port" : "Θύρα Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Ρυθμίσεις Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Συμβολοσειρά σύνδεσης Zookeeper για SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Αριθμός θύρας Zookeeper (προαιρετικό, προεπιλογή 2181)", + "chunk" : "τμήμα", + "chunks" : "τμήματα", + "chunks processed" : "τμήματα επεξεργάστηκαν", + "configuration(s)" : "διαμόρφωση(εις)", + "e.g., lib/Settings/config.json" : "π.χ. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "π.χ. lib/Settings/register.json", + "failed" : "απέτυχε", + "file" : "αρχείο", + "files" : "αρχεία", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "μέλη", + "menu or contact someone with permission to create agents." : "μενού ή επικοινωνήστε με κάποιον που έχει δικαίωμα δημιουργίας πρακτόρων.", + "messages" : "μηνύματα", + "n8n URL and API key are required" : "Απαιτούνται το URL n8n και το κλειδί API", + "n8n configuration saved successfully" : "Η διαμόρφωση n8n αποθηκεύτηκε με επιτυχία", + "n8n connection not configured" : "Η σύνδεση n8n δεν έχει διαμορφωθεί", + "n8n connection successful" : "Η σύνδεση n8n ήταν επιτυχής", + "n8n connection test successful" : "Η δοκιμή σύνδεσης n8n ήταν επιτυχής", + "n8n integration disabled" : "Η ενσωμάτωση n8n απενεργοποιήθηκε", + "n8n integration enabled" : "Η ενσωμάτωση n8n ενεργοποιήθηκε", + "n8n project initialized successfully" : "Το έργο n8n αρχικοποιήθηκε με επιτυχία", + "n8n settings saved successfully" : "Οι ρυθμίσεις n8n αποθηκεύτηκαν με επιτυχία", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "αντικείμενα", + "objects processed" : "αντικείμενα επεξεργάστηκαν", + "or pick a range" : "ή επιλέξτε ένα εύρος", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "παρέχει μια καλή ισορροπία μεταξύ ταχύτητας και ακρίβειας.", + "register(s)" : "μητρώο(α)", + "register(s) selected" : "μητρώο(α) επιλέχθηκαν", + "required" : "υποχρεωτικό", + "results" : "αποτελέσματα", + "schema(s)" : "σχήμα(τα)", + "schema(s) selected" : "σχήμα(τα) επιλέχθηκαν", + "sk-..." : "sk-...", + "this application" : "αυτή η εφαρμογή", + "to" : "έως", + "vectorized" : "διανυσματοποιήθηκε", + "views selected" : "προβολές επιλέχθηκαν", + "ℹ️ Current Configuration" : "ℹ️ Τρέχουσα διαμόρφωση", + "⚡ Batch Processing" : "⚡ Ομαδική επεξεργασία", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Περιεκτικό πλαίσιο ✓ Λιγότερο πιθανό να παραλείψει λεπτομέρειες ✗ Πιο αργές αποκρίσεις ✗ Μπορεί να περιλαμβάνει λιγότερο σχετικές πληροφορίες", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Ταχύτερες αποκρίσεις ✓ Πιο εστιασμένες απαντήσεις ✗ Μπορεί να παραλείψει σχετικές πληροφορίες", + "✨ AI Features" : "✨ Λειτουργίες τεχνητής νοημοσύνης", + "All search trails cleared successfully" : "Όλα τα ίχνη αναζήτησης εκκαθαρίστηκαν με επιτυχία", + "Multiple search trail deletion not implemented yet" : "Η διαγραφή πολλαπλών ιχνών αναζήτησης δεν έχει υλοποιηθεί ακόμα", + "No expired search trails found to clear" : "Δεν βρέθηκαν ληγμένα ίχνη αναζήτησης προς εκκαθάριση", + "Missing message" : "Λείπει μήνυμα", + "message content is required" : "απαιτείται περιεχόμενο μηνύματος", + "Missing conversation or agentUuid" : "Λείπει η συνομιλία ή το agentUuid", + "Access denied" : "Η πρόσβαση απορρίφθηκε", + "Conversation not found" : "Η συνομιλία δεν βρέθηκε", + "AI service not configured" : "Η υπηρεσία τεχνητής νοημοσύνης δεν έχει διαμορφωθεί", + "Failed to process message" : "Αποτυχία επεξεργασίας μηνύματος", + "Missing conversationId" : "Λείπει το conversationId", + "conversationId is required" : "Απαιτείται το conversationId", + "Failed to fetch conversation history" : "Αποτυχία ανάκτησης ιστορικού συνομιλίας", + "Failed to clear conversation" : "Αποτυχία εκκαθάρισης συνομιλίας", + "Invalid feedback type" : "Μη έγκυρος τύπος σχολίου", + "type must be \"positive\" or \"negative\"" : "ο τύπος πρέπει να είναι \"positive\" ή \"negative\"", + "Message not found" : "Το μήνυμα δεν βρέθηκε", + "Failed to save feedback" : "Αποτυχία αποθήκευσης σχολίων", + "Failed to get chat statistics" : "Αποτυχία λήψης στατιστικών συνομιλίας", + "Not Found" : "Δεν βρέθηκε", + "Engine not found" : "Η μηχανή δεν βρέθηκε", + "Consumer not found" : "Ο καταναλωτής δεν βρέθηκε", + "Backend parameter is required" : "Απαιτείται η παράμετρος backend", + "Failed to get database information: %s" : "Αποτυχία λήψης πληροφοριών βάσης δεδομένων: %s", + "SOLR setup error: %s" : "Σφάλμα εγκατάστασης SOLR: %s", + "Reindex failed: %s" : "Η επανευρετηρίαση απέτυχε: %s", + "Anonymized" : "Ανωνυμοποιημένο", + "Entities detected" : "Εντοπίστηκαν οντότητες", + "Extract Now" : "Εξαγωγή τώρα", + "Extracted at" : "Εξήχθη στις", + "Extraction" : "Εξαγωγή", + "Failed to load extraction data" : "Αποτυχία φόρτωσης δεδομένων εξαγωγής", + "Failed to load register data" : "Αποτυχία φόρτωσης δεδομένων μητρώου", + "Failed to retrieve extraction status." : "Αποτυχία ανάκτησης κατάστασης εξαγωγής.", + "Failed to retrieve objects for file." : "Αποτυχία ανάκτησης αντικειμένων για το αρχείο.", + "No extraction data available for this file" : "Δεν υπάρχουν διαθέσιμα δεδομένα εξαγωγής για αυτό το αρχείο", + "No register objects reference this file" : "Κανένα αντικείμενο μητρώου δεν αναφέρεται σε αυτό το αρχείο", + "Not extracted" : "Δεν εξήχθη", + "Register Objects" : "Αντικείμενα μητρώου", + "Risk level" : "Επίπεδο κινδύνου", + "Text chunks" : "Τμήματα κειμένου", + "Unknown error" : "Άγνωστο σφάλμα", + "Very high" : "Πολύ υψηλό", + "{title} in {register} / {schema}" : "{title} σε {register} / {schema}", + "Push Notifications" : "Ειδοποιήσεις Push", + "Real-time push notification status via notify_push" : "Κατάσταση ειδοποιήσεων push σε πραγματικό χρόνο μέσω notify_push", + "Realtime push not available — the notify_push app is not installed" : "Οι ειδοποιήσεις push σε πραγματικό χρόνο δεν είναι διαθέσιμες — η εφαρμογή notify_push δεν είναι εγκατεστημένη", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Εγκαταστήστε την εφαρμογή notify_push από το Nextcloud App Store για να ενεργοποιήσετε τις ενημερώσεις σε πραγματικό χρόνο.", + "Open Nextcloud App Store" : "Άνοιγμα Nextcloud App Store", + "notify_push is installed but not yet active" : "Το notify_push είναι εγκατεστημένο αλλά δεν είναι ακόμα ενεργό", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "Το notify_push είναι εγκατεστημένο αλλά το OpenRegister δεν έχει επιβεβαιώσει ακόμα μια επιτυχή ειδοποίηση push. Ενεργοποιήστε μια αποθήκευση αντικειμένου για να το ενεργοποιήσετε ή ελέγξτε τη διαμόρφωση του notify_push.", + "notify_push configuration guide" : "οδηγός διαμόρφωσης notify_push", + "Realtime push active" : "Ειδοποιήσεις push σε πραγματικό χρόνο ενεργές", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Οι ειδοποιήσεις push σε πραγματικό χρόνο είναι ενεργές. Οι συνδεδεμένοι πελάτες λαμβάνουν άμεσες ενημερώσεις όταν τα αντικείμενα δημιουργούνται, ενημερώνονται ή διαγράφονται.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/el.json b/l10n/el.json new file mode 100644 index 0000000000..f2a8e02f70 --- /dev/null +++ b/l10n/el.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(απαιτείται Dolphin)", + "(no subject)": "(χωρίς θέμα)", + "(no title)": "(χωρίς τίτλο)", + "(unnamed)": "(χωρίς όνομα)", + "(untitled card)": "(κάρτα χωρίς τίτλο)", + "({count} selected)": "({count} επιλεγμένα)", + ", {failed} failed": ", {failed} απέτυχαν", + "0 = unlimited": "0 = απεριόριστο", + "A deactivation request is pending.": "Εκκρεμεί ένα αίτημα απενεργοποίησης.", + "AI Assistant": "Βοηθός AI", + "AI Chat": "Συνομιλία AI", + "AI service not configured": "Η υπηρεσία AI δεν έχει διαμορφωθεί", + "API Key": "Κλειδί API", + "API Request Quota (requests/day)": "Όριο αιτημάτων API (αιτήματα/ημέρα)", + "API Token Configuration": "Διαμόρφωση διακριτικού API", + "API Tokens": "Διακριτικά API", + "API tokens saved successfully": "Τα διακριτικά API αποθηκεύτηκαν με επιτυχία", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: αυτή η αναφορά συνδέει κάθε verwerkingsactiviteit με τα συνολικά πλήθη του ίχνους ελέγχου που της αποδίδονται. Οι ελεγκτές και η Autoriteit Persoonsgegevens το χρησιμοποιούν για εποπτικό έλεγχο.", + "About ConfigSets": "Σχετικά με τα ConfigSets", + "Access denied": "Η πρόσβαση απορρίφθηκε", + "Access organisation dashboard and analytics": "Πρόσβαση στον πίνακα ελέγχου και στα αναλυτικά στοιχεία του οργανισμού", + "Account": "Λογαριασμός", + "Action": "Ενέργεια", + "Action Distribution": "Κατανομή ενεργειών", + "Action:": "Ενέργεια:", + "Actions": "Ενέργειες", + "Active": "Ενεργό", + "Active Collections": "Ενεργές συλλογές", + "Active Filters": "Ενεργά φίλτρα", + "Active Organisation:": "Ενεργός οργανισμός:", + "Active Organisations": "Ενεργοί οργανισμοί", + "Active filters:": "Ενεργά φίλτρα:", + "Active organisation changed successfully": "Ο ενεργός οργανισμός άλλαξε με επιτυχία", + "Active organisation set successfully": "Ο ενεργός οργανισμός ορίστηκε με επιτυχία", + "Activities": "Δραστηριότητες", + "Activity": "Δραστηριότητα", + "Activity Period": "Περίοδος δραστηριότητας", + "Add": "Προσθήκη", + "Add Attachment": "Προσθήκη συνημμένου", + "Add Endpoint": "Προσθήκη τελικού σημείου", + "Add Groups": "Προσθήκη ομάδων", + "Add Property to '{title}'": "Προσθήκη Ιδιότητας στο '{title}'", + "Add Register": "Προσθήκη Μητρώου", + "Add Schema": "Προσθήκη Σχήματος", + "Add Source": "Προσθήκη Πηγής", + "Add User": "Προσθήκη χρήστη", + "Add User to Organisation": "Προσθήκη χρήστη στον οργανισμό", + "Add a contact from any of your address books to associate it with this object.": "Προσθέστε μια επαφή από οποιοδήποτε από τα βιβλία διευθύνσεών σας για να τη συσχετίσετε με αυτό το Αντικείμενο.", + "Add a file or files": "Προσθήκη ενός ή περισσότερων αρχείων", + "Add card": "Προσθήκη κάρτας", + "Add contact": "Προσθήκη επαφής", + "Add endpoint": "Προσθήκη τελικού σημείου", + "Add oneOf entry": "Προσθήκη καταχώρισης oneOf", + "Add range": "Προσθήκη εύρους", + "Add schema titles, descriptions, and register information to provide richer context for search": "Προσθέστε τίτλους Σχημάτων, περιγραφές και πληροφορίες Μητρώου για να παρέχετε πλουσιότερο πλαίσιο για την αναζήτηση", + "Add to favorites": "Προσθήκη στα αγαπημένα", + "Added": "Προστέθηκε", + "Added to favorites": "Προστέθηκε στα αγαπημένα", + "Additional Information": "Πρόσθετες πληροφορίες", + "Additional feedback saved. Thank you!": "Τα πρόσθετα σχόλια αποθηκεύτηκαν. Σας ευχαριστούμε!", + "Addresses": "Διευθύνσεις", + "Admin": "Διαχειριστής", + "Advanced": "Για προχωρημένους", + "Advanced Filters": "Φίλτρα για προχωρημένους", + "Advanced Options": "Επιλογές για προχωρημένους", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Τα φίλτρα για προχωρημένους δεν είναι διαθέσιμα όταν χρησιμοποιείται πηγή βάσης δεδομένων. Μεταβείτε σε Αυτόματο ή SOLR Index για επιλογές φιλτραρίσματος.", + "After": "Μετά", + "Agent Type": "Τύπος πράκτορα", + "Agent deleted successfully": "Ο πράκτορας διαγράφηκε με επιτυχία", + "Agent successfully created": "Ο πράκτορας δημιουργήθηκε με επιτυχία", + "Agent successfully updated": "Ο πράκτορας ενημερώθηκε με επιτυχία", + "Agents": "Πράκτορες", + "Aggregated across schemas": "Συγκεντρωτικά σε όλα τα Σχήματα", + "All": "Όλα", + "All Categories": "Όλες οι κατηγορίες", + "All Collections": "Όλες οι συλλογές", + "All Confidence Levels": "Όλα τα επίπεδα εμπιστοσύνης", + "All ConfigSets": "Όλα τα ConfigSets", + "All Files": "Όλα τα Αρχεία", + "All Levels": "Όλα τα επίπεδα", + "All Types": "Όλοι οι τύποι", + "All Webhooks": "Όλα τα webhooks", + "All actions": "Όλες οι ενέργειες", + "All audit trails cleared successfully": "Όλα τα ίχνη ελέγχου εκκαθαρίστηκαν με επιτυχία", + "All clear": "Όλα εντάξει", + "All file types": "Όλοι οι τύποι αρχείων", + "All registers": "Όλα τα Μητρώα", + "All schemas": "Όλα τα Σχήματα", + "All search trails cleared successfully": "Όλα τα ίχνη αναζήτησης εκκαθαρίστηκαν με επιτυχία", + "All searches": "Όλες οι αναζητήσεις", + "All users": "Όλοι οι χρήστες", + "All-day events": "Ολοήμερα συμβάντα", + "Allowed MIME Types": "Επιτρεπόμενοι τύποι MIME", + "Already linked": "Έχει ήδη συνδεθεί", + "An error occurred while clearing audit trails": "Παρουσιάστηκε σφάλμα κατά την εκκαθάριση των ιχνών ελέγχου", + "An error occurred while deleting the audit trail": "Παρουσιάστηκε σφάλμα κατά τη διαγραφή του ίχνους ελέγχου", + "An error occurred while deleting the object": "Παρουσιάστηκε σφάλμα κατά τη διαγραφή του Αντικειμένου", + "An error occurred while deleting the view": "Παρουσιάστηκε σφάλμα κατά τη διαγραφή της προβολής", + "An error occurred while permanently deleting the objects": "Παρουσιάστηκε σφάλμα κατά την οριστική διαγραφή των Αντικειμένων", + "An error occurred while restoring the objects": "Παρουσιάστηκε σφάλμα κατά την επαναφορά των Αντικειμένων", + "An error occurred while saving the organisation": "Παρουσιάστηκε σφάλμα κατά την αποθήκευση του οργανισμού", + "Analysis completed:": "Η ανάλυση ολοκληρώθηκε:", + "Analysis steps:": "Βήματα ανάλυσης:", + "Analytics": "Αναλυτικά στοιχεία", + "Analyze Objects": "Ανάλυση Αντικειμένων", + "Analyze Properties": "Ανάλυση Ιδιοτήτων", + "Analyze existing properties for improvement opportunities": "Αναλύστε τις υπάρχουσες Ιδιότητες για ευκαιρίες βελτίωσης", + "Analyzing...": "Ανάλυση σε εξέλιξη...", + "Anonymized": "Ανωνυμοποιήθηκε", + "Anonymous Group": "Ανώνυμη ομάδα", + "Any favorites and sharing settings for this view": "Τυχόν αγαπημένα και ρυθμίσεις κοινής χρήσης για αυτήν την προβολή", + "Any user": "Οποιοσδήποτε χρήστης", + "App ID": "App ID", + "App store cache invalidated successfully": "Η προσωρινή μνήμη του App store ακυρώθηκε με επιτυχία", + "App store cache invalidated: {invalidated}": "Η προσωρινή μνήμη του App store ακυρώθηκε: {invalidated}", + "Application deleted successfully": "Η εφαρμογή διαγράφηκε με επιτυχία", + "Application identifier for this configuration (optional)": "Αναγνωριστικό εφαρμογής για αυτήν τη διαμόρφωση (προαιρετικό)", + "Application settings": "Ρυθμίσεις εφαρμογής", + "Application successfully created": "Η εφαρμογή δημιουργήθηκε με επιτυχία", + "Application successfully updated": "Η εφαρμογή ενημερώθηκε με επιτυχία", + "Applications": "Εφαρμογές", + "Apply": "Εφαρμογή", + "Apply Changes": "Εφαρμογή αλλαγών", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Εφαρμόστε έναν ρόλο σε όλα τα Σχήματα αυτού του Μητρώου που δεν έχουν ρητές παρακάμψεις εξουσιοδότησης.", + "Approval Progress": "Πρόοδος έγκρισης", + "Approve": "Έγκριση", + "Approved": "Εγκρίθηκε", + "Archive": "Αρχειοθέτηση", + "Archive conversation": "Αρχειοθέτηση συνομιλίας", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Αρχειοθέτηση αυτής της verwerkingsactiviteit; Οι γραμμές του ίχνους ελέγχου θα συνεχίσουν να αναφέρονται σε αυτήν.", + "Archived conversations are hidden from your active list": "Οι αρχειοθετημένες συνομιλίες αποκρύπτονται από την ενεργή σας λίστα", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Είστε βέβαιοι ότι θέλετε να ΔΙΑΓΡΑΨΕΤΕ τη συλλογή \"{name}\";\n\nΑυτό θα:\n• Διαγράψει οριστικά τη συλλογή και όλα τα δεδομένα της\n• Αφαιρέσει όλα τα ευρετηριασμένα έγγραφα\n• Αυτή η ενέργεια δεν μπορεί να αναιρεθεί", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Είστε βέβαιοι ότι θέλετε να εκκαθαρίσετε τα παλιά ίχνη αναζήτησης; Αυτό θα διαγράψει τις καταχωρίσεις παλαιότερες των 30 ημερών.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Είστε βέβαιοι ότι θέλετε να εκκαθαρίσετε όλα τα δεδομένα από τη συλλογή \"{name}\";\n\nΑυτό θα:\n• Διαγράψει όλα τα ευρετηριασμένα έγγραφα\n• Διατηρήσει άθικτη τη δομή της συλλογής\n• Αυτή η ενέργεια δεν μπορεί να αναιρεθεί", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Είστε βέβαιοι ότι θέλετε να διαγράψετε την εφαρμογή \"{name}\"; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα ίχνη ελέγχου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα ίχνη αναζήτησης; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Are you sure you want to delete this ConfigSet?": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το ConfigSet;", + "Are you sure you want to delete this configset?": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το configset;", + "Are you sure you want to leave '{name}'?": "Είστε βέβαιοι ότι θέλετε να αποχωρήσετε από το '{name}';", + "Are you sure you want to permanently delete": "Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Είστε βέβαιοι ότι θέλετε να επαναδημιουργήσετε το ευρετήριο της συλλογής \"{name}\";\n\nΑυτό θα:\n• Επαναδημιουργήσει το ευρετήριο με όλα τα Αντικείμενα\n• Διαρκέσει αρκετά λεπτά για να ολοκληρωθεί\n• Ενδέχεται να επηρεάσει την απόδοση αναζήτησης κατά την επαναδημιουργία του ευρετηρίου", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε το Σχήμα \"{schema}\" από το Μητρώο \"{register}\"; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Είστε βέβαιοι ότι θέλετε να διαγράψετε προσωρινά {count} ενεργά Αντικείμενα για το Σχήμα \"{schema}\";\n\nΘα επισημανθούν ως διαγραμμένα αλλά μπορούν να αφαιρεθούν οριστικά αργότερα.", + "Array Object Configuration:": "Διαμόρφωση Αντικειμένου πίνακα:", + "Articles": "Άρθρα", + "Ask a question...": "Κάντε μια ερώτηση...", + "Ask questions about your data using natural language": "Κάντε ερωτήσεις σχετικά με τα δεδομένα σας χρησιμοποιώντας φυσική γλώσσα", + "Assigned collaborative tags": "Αντιστοιχισμένες συνεργατικές ετικέτες", + "Assignment notifications": "Ειδοποιήσεις ανάθεσης", + "Attachment added to {name}": "Το συνημμένο προστέθηκε στο {name}", + "Attempt": "Προσπάθεια", + "Audit Trail Changes": "Αλλαγές ίχνους ελέγχου", + "Audit Trail Details": "Λεπτομέρειες ίχνους ελέγχου", + "Audit Trail Management": "Διαχείριση ίχνους ελέγχου", + "Audit Trail Statistics": "Στατιστικά ίχνους ελέγχου", + "Audit Trails": "Ίχνη ελέγχου", + "Audit trail data copied to clipboard": "Τα δεδομένα του ίχνους ελέγχου αντιγράφηκαν στο πρόχειρο", + "Audit trail deleted successfully": "Το ίχνος ελέγχου διαγράφηκε με επιτυχία", + "Audit trail successfully deleted": "Το ίχνος ελέγχου διαγράφηκε με επιτυχία", + "Authentication": "Έλεγχος ταυτότητας", + "Auto (e.g. Y for year, F Y for month)": "Αυτόματο (π.χ. Y για έτος, F Y για μήνα)", + "Auto share": "Αυτόματη κοινή χρήση", + "Auto-commit disabled": "Η αυτόματη υποβολή απενεργοποιήθηκε", + "Auto-commit enabled": "Η αυτόματη υποβολή ενεργοποιήθηκε", + "Auto-create Default Organisation": "Αυτόματη δημιουργία προεπιλεγμένου οργανισμού", + "Auto-retry failed vectorizations": "Αυτόματη επανάληψη αποτυχημένων διανυσματοποιήσεων", + "Automatically commit changes to SOLR index": "Αυτόματη υποβολή αλλαγών στο SOLR index", + "Automatically connect to the federation network.": "Αυτόματη σύνδεση στο δίκτυο ομοσπονδίας.", + "Automatically create a default organisation if none exists when the app is initialized": "Αυτόματη δημιουργία προεπιλεγμένου οργανισμού εάν δεν υπάρχει κατά την αρχικοποίηση της εφαρμογής", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Αυτόματη δημιουργία διανυσματικών embeddings από τμήματα κειμένου όταν τα Αρχεία μεταφορτώνονται και επεξεργάζονται", + "Automatically generate vector embeddings when objects are created or updated": "Αυτόματη δημιουργία διανυσματικών embeddings όταν δημιουργούνται ή ενημερώνονται Αντικείμενα", + "Automatically retry failed vectorization attempts (max 3 retries)": "Αυτόματη επανάληψη αποτυχημένων προσπαθειών διανυσματοποίησης (μέγιστο 3 επαναλήψεις)", + "Automatically update catalogues.": "Αυτόματη ενημέρωση καταλόγων.", + "Available Filters": "Διαθέσιμα φίλτρα", + "Available Workflows": "Διαθέσιμες ροές εργασίας", + "Available placeholders:": "Διαθέσιμα σύμβολα κράτησης θέσης:", + "Avatar": "Άβαταρ", + "Avatar changes are not supported by your authentication provider.": "Οι αλλαγές άβαταρ δεν υποστηρίζονται από τον πάροχο ελέγχου ταυτότητάς σας.", + "Avatar removed": "Το άβαταρ αφαιρέθηκε", + "Avatar updated successfully": "Το άβαταρ ενημερώθηκε με επιτυχία", + "Avg Execution Time": "Μέσος χρόνος εκτέλεσης", + "Avg Members/Org": "Μέσοι όροι μελών/οργανισμό", + "Avg Object Views/Session": "Μέσες προβολές Αντικειμένων/συνεδρία", + "Avg Response Time": "Μέσος χρόνος απόκρισης", + "Avg Results/Search": "Μέσα αποτελέσματα/αναζήτηση", + "Avg Searches/Session": "Μέσες αναζητήσεις/συνεδρία", + "Back": "Πίσω", + "Back to Entities": "Επιστροφή στις οντότητες", + "Back to Registers": "Επιστροφή στα Μητρώα", + "Back to Webhooks": "Επιστροφή στα webhooks", + "Back to applications": "Επιστροφή στις εφαρμογές", + "Back to entities": "Επιστροφή στις οντότητες", + "Backend parameter is required": "Η παράμετρος backend είναι υποχρεωτική", + "Backend updated successfully. Please reload the application.": "Το backend ενημερώθηκε με επιτυχία. Επαναφορτώστε την εφαρμογή.", + "Bandwidth Quota (MB/month)": "Όριο εύρους ζώνης (MB/μήνα)", + "Base URL (Optional)": "Βασικό URL (Προαιρετικό)", + "Basic Information": "Βασικές πληροφορίες", + "Batch Size": "Μέγεθος παρτίδας", + "Batch extraction completed": "Η ομαδική εξαγωγή ολοκληρώθηκε", + "Before": "Πριν", + "Before object vectorization can work:": "Πριν λειτουργήσει η διανυσματοποίηση Αντικειμένων:", + "Behavior": "Συμπεριφορά", + "Behavior Issue": "Ζήτημα συμπεριφοράς", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (διάρκεια ISO-8601, π.χ. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Η αποθήκευση blob έχει αποσυρθεί. Όλα τα Αντικείμενα χρησιμοποιούν πλέον magic tables.", + "Bookmarks": "Σελιδοδείκτες", + "Branch": "Κλάδος", + "Bulk Role Assignment: {title}": "Μαζική ανάθεση ρόλων: {title}", + "Bulk delete operation completed successfully": "Η μαζική λειτουργία διαγραφής ολοκληρώθηκε με επιτυχία", + "Bulk save operation completed successfully": "Η μαζική λειτουργία αποθήκευσης ολοκληρώθηκε με επιτυχία", + "Business Data": "Επιχειρηματικά δεδομένα", + "CSV": "CSV", + "Cache cleared successfully": "Η προσωρινή μνήμη εκκαθαρίστηκε με επιτυχία", + "Cache grew from {old} to {new} entries.": "Η προσωρινή μνήμη αυξήθηκε από {old} σε {new} καταχωρίσεις.", + "Cache shrunk from {old} to {new} entries.": "Η προσωρινή μνήμη μειώθηκε από {old} σε {new} καταχωρίσεις.", + "Cache stayed the same at {size} entries.": "Η προσωρινή μνήμη παρέμεινε ίδια στις {size} καταχωρίσεις.", + "Calculate Sizes": "Υπολογισμός μεγεθών", + "Calendar": "Ημερολόγιο", + "Calendar Provider Configuration": "Διαμόρφωση παρόχου ημερολογίου", + "Calendar integration is not available": "Η ενσωμάτωση ημερολογίου δεν είναι διαθέσιμη", + "Calendar name": "Όνομα ημερολογίου", + "Cancel": "Άκυρο", + "Cancel deactivation request": "Ακύρωση αιτήματος απενεργοποίησης", + "Cannot delete: objects are still attached": "Δεν είναι δυνατή η διαγραφή: εξακολουθούν να υπάρχουν συνδεδεμένα Αντικείμενα", + "Cannot edit: This register is managed by external configuration {title}": "Δεν είναι δυνατή η επεξεργασία: Αυτό το Μητρώο διαχειρίζεται από εξωτερική διαμόρφωση {title}", + "Cannot remove schema with existing objects ({count} objects)": "Δεν είναι δυνατή η αφαίρεση Σχήματος με υπάρχοντα Αντικείμενα ({count} Αντικείμενα)", + "Cannot remove schema {schema} because it contains {count} objects": "Δεν είναι δυνατή η αφαίρεση του Σχήματος {schema} επειδή περιέχει {count} Αντικείμενα", + "Cards": "Κάρτες", + "Cascade delete": "Διαδοχική διαγραφή", + "Categories of data subjects (one per line)": "Κατηγορίες υποκειμένων των δεδομένων (μία ανά γραμμή)", + "Categories of personal data (one per line)": "Κατηγορίες προσωπικών δεδομένων (μία ανά γραμμή)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Κατηγορία", + "Change Type": "Τύπος αλλαγής", + "Change password": "Αλλαγή κωδικού πρόσβασης", + "Changes": "Αλλαγές", + "Changes copied to clipboard": "Οι αλλαγές αντιγράφηκαν στο πρόχειρο", + "Changes for Audit Trail #{id}": "Αλλαγές για το ίχνος ελέγχου #{id}", + "Chat Model": "Μοντέλο συνομιλίας", + "Chat Provider": "Πάροχος συνομιλίας", + "Chat Provider (RAG)": "Πάροχος συνομιλίας (RAG)", + "Chat Settings": "Ρυθμίσεις συνομιλίας", + "Chat provider connection successful!": "Η σύνδεση με τον πάροχο συνομιλίας ήταν επιτυχής!", + "Chat settings": "Ρυθμίσεις συνομιλίας", + "Chat test failed: {error}": "Η δοκιμή συνομιλίας απέτυχε: {error}", + "Choose a register": "Επιλέξτε ένα Μητρώο", + "Choose a schema": "Επιλέξτε ένα Σχήμα", + "Choose how vector similarity calculations are performed for semantic search": "Επιλέξτε τον τρόπο με τον οποίο εκτελούνται οι υπολογισμοί διανυσματικής ομοιότητας για τη σημασιολογική αναζήτηση", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Επιλέξτε ποιους τύπους αρχείων θα συμπεριλάβετε στη διαδικασία διανυσματοποίησης. Θα επεξεργαστούν μόνο τα Αρχεία με εξαγμένο κείμενο και τμήματα.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Επιλέξτε ποιες προβολές θα συμπεριλάβετε στη διαδικασία διανυσματοποίησης. Αφήστε το κενό για να επεξεργαστούν όλες οι προβολές με βάση τη διαμόρφωσή σας.", + "Chunk": "τμήμα", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Η διαγραφή τμημάτων δεν έχει υλοποιηθεί ακόμη. Χρησιμοποιήστε τελικά σημεία βασισμένα σε τμήματα.", + "Chunks": "Τμήματα", + "Chunks to Vectorize:": "Τμήματα προς διανυσματοποίηση:", + "Cleanup Old Trails": "Εκκαθάριση παλιών ιχνών", + "Cleanup completed": "Η εκκαθάριση ολοκληρώθηκε", + "Cleanup completed successfully. Deleted {count} entries.": "Η εκκαθάριση ολοκληρώθηκε με επιτυχία. Διαγράφηκαν {count} καταχωρίσεις.", + "Cleanup failed: {error}": "Η εκκαθάριση απέτυχε: {error}", + "Clear All Embeddings": "Εκκαθάριση όλων των embeddings", + "Clear Entries": "Εκκαθάριση καταχωρίσεων", + "Clear Filtered Audit Trails": "Εκκαθάριση φιλτραρισμένων ιχνών ελέγχου", + "Clear Filters": "Εκκαθάριση φίλτρων", + "Clear Index": "Εκκαθάριση ευρετηρίου", + "Clear Selection": "Εκκαθάριση επιλογής", + "Clear all": "Εκκαθάριση όλων", + "Clear all filters": "Εκκαθάριση όλων των φίλτρων", + "Clear date range": "Εκκαθάριση εύρους ημερομηνιών", + "Clear filters": "Εκκαθάριση φίλτρων", + "Clearing...": "Εκκαθάριση σε εξέλιξη...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Κάντε κλικ στην Εκτέλεση σάρωσης συμμόρφωσης για να βρείτε Σχήματα όπου έχει εντοπιστεί PII αλλά δεν υπάρχει σχολιασμός verwerkingsactiviteit.", + "Click to subscribe to notifications": "Κάντε κλικ για εγγραφή σε ειδοποιήσεις", + "Click to unsubscribe from notifications": "Κάντε κλικ για κατάργηση εγγραφής από ειδοποιήσεις", + "Close": "Κλείσιμο", + "Cluster name": "Όνομα συστάδας", + "Code": "Κωδικός", + "Code (short readable key, e.g. v-2026-001)": "Κωδικός (σύντομο αναγνώσιμο κλειδί, π.χ. v-2026-001)", + "Collapse details": "Σύμπτυξη λεπτομερειών", + "Collection Management": "Διαχείριση συλλογής", + "Collection Name": "Όνομα συλλογής", + "Collection assignments updated successfully": "Οι αναθέσεις συλλογής ενημερώθηκαν με επιτυχία", + "Collection cleared successfully": "Η συλλογή εκκαθαρίστηκε με επιτυχία", + "Collection copied successfully": "Η συλλογή αντιγράφηκε με επιτυχία", + "Collection created successfully": "Η συλλογή δημιουργήθηκε με επιτυχία", + "Collection deleted successfully": "Η συλλογή διαγράφηκε με επιτυχία", + "Collection used to store and index file metadata and content": "Συλλογή που χρησιμοποιείται για την αποθήκευση και ευρετηρίαση μεταδεδομένων και περιεχομένου αρχείων", + "Collection used to store and index object data": "Συλλογή που χρησιμοποιείται για την αποθήκευση και ευρετηρίαση δεδομένων Αντικειμένων", + "Collections:": "Συλλογές:", + "Color": "Χρώμα", + "Comment...": "Σχόλιο...", + "Commit Message": "Μήνυμα υποβολής", + "Commit Within (ms)": "Υποβολή εντός (ms)", + "Commit message": "Μήνυμα υποβολής", + "Compare current schema with real object data": "Σύγκριση τρέχοντος Σχήματος με πραγματικά δεδομένα Αντικειμένων", + "Completed": "Ολοκληρώθηκε", + "Complex": "Σύνθετο", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Σύνθετα ερωτήματα: Αναζητήσεις για προχωρημένους με πολλαπλά φίλτρα, τελεστές και σύνθετους συνδυασμούς παραμέτρων", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Σύνθετα ερωτήματα: αναζητήσεις για προχωρημένους με πολλαπλά φίλτρα, τελεστές και σύνθετους συνδυασμούς παραμέτρων", + "Compliance": "Συμμόρφωση", + "Confidence Level": "Επίπεδο εμπιστοσύνης", + "Confidence Score": "Βαθμολογία εμπιστοσύνης", + "Confidence: {confidence}%": "Εμπιστοσύνη: {confidence}%", + "Config must be provided as an object": "Η διαμόρφωση πρέπει να παρέχεται ως αντικείμενο", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Διαχείριση ConfigSet", + "ConfigSet Name": "Όνομα ConfigSet", + "ConfigSet created successfully": "Το ConfigSet δημιουργήθηκε με επιτυχία", + "ConfigSet deleted successfully": "Το ConfigSet διαγράφηκε με επιτυχία", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Τα ConfigSets ορίζουν το Σχήμα και τη διαμόρφωση για τις συλλογές SOLR σας. Περιέχουν ορισμούς πεδίων, αναλυτές και άλλες ρυθμίσεις αναζήτησης.", + "Configuration": "Διαμόρφωση", + "Configuration Data": "Δεδομένα διαμόρφωσης", + "Configuration is up to date": "Η διαμόρφωση είναι ενημερωμένη", + "Configuration saved": "Η διαμόρφωση αποθηκεύτηκε", + "Configuration saved successfully.": "Η διαμόρφωση αποθηκεύτηκε με επιτυχία.", + "Configuration type (default, application, etc.)": "Τύπος διαμόρφωσης (προεπιλογή, εφαρμογή, κ.λπ.)", + "Configurations": "Διαμορφώσεις", + "Configure API tokens for external service integrations": "Διαμορφώστε διακριτικά API για ενσωματώσεις εξωτερικών υπηρεσιών", + "Configure Apache SOLR search engine for advanced search capabilities": "Διαμορφώστε τη μηχανή αναζήτησης Apache SOLR για δυνατότητες αναζήτησης για προχωρημένους", + "Configure Facets": "Διαμόρφωση όψεων", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Διαμορφώστε παρόχους LLM για λειτουργίες με υποστήριξη AI, συμπεριλαμβανομένης της σημασιολογικής αναζήτησης, των embeddings και της συνομιλίας.", + "Configure SOLR Facets": "Διαμόρφωση όψεων SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Διαμορφώστε τις βασικές ρυθμίσεις σύνδεσης για τον διακομιστή SOLR σας, συμπεριλαμβανομένων των επιλογών ελέγχου ταυτότητας και δικτύου. Χρησιμοποιήστε τα ξεχωριστά παράθυρα διαλόγου ConfigSet και Διαχείρισης συλλογής για τη διαχείριση cores και συλλογών.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Διαμορφώστε τις βασικές ρυθμίσεις σύνδεσης για τον διακομιστή SOLR σας, συμπεριλαμβανομένων των επιλογών ελέγχου ταυτότητας και δικτύου. Χρησιμοποιήστε τα ξεχωριστά παράθυρα διαλόγου configset και διαχείρισης συλλογής για τη διαχείριση cores και συλλογών.", + "Configure default organisation and organisation-related settings": "Διαμορφώστε τον προεπιλεγμένο οργανισμό και τις ρυθμίσεις που σχετίζονται με τον οργανισμό", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Διαμορφώστε τον τρόπο με τον οποίο τα Αντικείμενα της βάσης δεδομένων μετατρέπονται σε διανυσματικά embeddings για σημασιολογική αναζήτηση. Τα Αντικείμενα διανυσματοποιούνται απευθείας χωρίς να απαιτείται εξαγωγή κειμένου.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Διαμορφώστε τον τρόπο με τον οποίο τα Αντικείμενα μετατρέπονται σε κείμενο πριν από τη διανυσματοποίηση. Αυτές οι ρυθμίσεις επηρεάζουν την ποιότητα και το πλαίσιο της αναζήτησης.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Διαμορφώστε παρόχους LLM για λειτουργίες με υποστήριξη AI, συμπεριλαμβανομένης της σημασιολογικής αναζήτησης, των embeddings και της συνομιλίας.", + "Configure multi-organization support and tenant isolation": "Διαμορφώστε την υποστήριξη πολλαπλών οργανισμών και την απομόνωση μισθωτών", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Διαμορφώστε παραμέτρους για τη διανυσματοποίηση αρχείων. Αυτή η διαδικασία θα δημιουργήσει διανυσματικά embeddings για όλα τα εξαγμένα τμήματα αρχείων.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Διαμορφώστε παραμέτρους για τη διανυσματοποίηση Αντικειμένων. Αυτή η διαδικασία θα δημιουργήσει διανυσματικά embeddings για όλα τα Αντικείμενα που ταιριάζουν με τα φίλτρα προβολής σας.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Διαμορφώστε αυτό το Σχήμα ώστε να εμφανίζει Αντικείμενα ως συμβάντα στην εφαρμογή Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Διαμορφώστε ποιους τύπους δεδομένων θα αναζητηθούν και πόσες πηγές θα ανακτηθούν", + "Configuring field mappings...": "Διαμόρφωση αντιστοιχίσεων πεδίων...", + "Confirm Account Deactivation": "Επιβεβαίωση απενεργοποίησης λογαριασμού", + "Confirm Object Deletion": "Επιβεβαίωση διαγραφής Αντικειμένου", + "Confirm Object Restoration": "Επιβεβαίωση επαναφοράς Αντικειμένου", + "Confirm Permanent Object Deletion": "Επιβεβαίωση οριστικής διαγραφής Αντικειμένου", + "Confirm deactivation": "Επιβεβαίωση απενεργοποίησης", + "Connecting to SOLR cluster...": "Σύνδεση στη συστάδα SOLR...", + "Connection Failed": "Η σύνδεση απέτυχε", + "Connection Settings": "Ρυθμίσεις σύνδεσης", + "Connection Successful!": "Η σύνδεση ήταν επιτυχής!", + "Connection failed": "Η σύνδεση απέτυχε", + "Connection protocol": "Πρωτόκολλο σύνδεσης", + "Connection successful": "Η σύνδεση ήταν επιτυχής", + "Connection timeout in seconds": "Χρονικό όριο σύνδεσης σε δευτερόλεπτα", + "Constant delay between retries (5 minutes)": "Σταθερή καθυστέρηση μεταξύ επαναλήψεων (5 λεπτά)", + "Constraint Issue": "Ζήτημα περιορισμού", + "Consumer not found": "Ο καταναλωτής δεν βρέθηκε", + "Contacts": "Επαφές", + "Contacts integration is not available": "Η ενσωμάτωση επαφών δεν είναι διαθέσιμη", + "Content Filters": "Φίλτρα περιεχομένου", + "Continue to Properties": "Συνέχεια στις Ιδιότητες", + "Contracts": "Συμβάσεις", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Ελέγξτε ποιες προβολές Αντικειμένων θα πρέπει να διανυσματοποιηθούν για τη μείωση του κόστους API. Διανυσματοποιήστε μόνο προβολές που επωφελούνται από τη σημασιολογική αναζήτηση.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Ελέγξτε ποιες προβολές και εργαλεία μπορεί να χρησιμοποιήσει η AI σε αυτήν τη συνομιλία. Από προεπιλογή, όλες οι δυνατότητες του πράκτορα είναι ενεργοποιημένες.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Ελέγχει την τυχαιότητα (0 = ντετερμινιστικό, 2 = πολύ δημιουργικό)", + "Conversation ID is required": "Το αναγνωριστικό συνομιλίας είναι υποχρεωτικό", + "Conversation archived": "Η συνομιλία αρχειοθετήθηκε", + "Conversation archived successfully": "Η συνομιλία αρχειοθετήθηκε με επιτυχία", + "Conversation cleared successfully": "Η συνομιλία εκκαθαρίστηκε με επιτυχία", + "Conversation deleted": "Η συνομιλία διαγράφηκε", + "Conversation not found": "Η συνομιλία δεν βρέθηκε", + "Conversation permanently deleted": "Η συνομιλία διαγράφηκε οριστικά", + "Conversation renamed": "Η συνομιλία μετονομάστηκε", + "Conversation restored": "Η συνομιλία επαναφέρθηκε", + "Conversation started with {agent}": "Η συνομιλία ξεκίνησε με {agent}", + "Conversation title": "Τίτλος συνομιλίας", + "Conversations": "Συνομιλίες", + "Converting {schema} to blob storage...": "Μετατροπή του {schema} σε αποθήκευση blob...", + "Converting {schema} to magic table...": "Μετατροπή του {schema} σε magic table...", + "Copied!": "Αντιγράφηκε!", + "Copy": "Αντιγραφή", + "Copy Changes": "Αντιγραφή αλλαγών", + "Copy Collection": "Αντιγραφή συλλογής", + "Copy Data": "Αντιγραφή δεδομένων", + "Copy Full Data": "Αντιγραφή πλήρων δεδομένων", + "Copy of {name}": "Αντίγραφο του {name}", + "Copy to clipboard": "Αντιγραφή στο πρόχειρο", + "Copying...": "Αντιγραφή σε εξέλιξη...", + "Costs": "Κόστη", + "Count": "Πλήθος", + "Counting objects...": "Καταμέτρηση Αντικειμένων...", + "Create": "δημιουργία", + "Create API Token": "Δημιουργία διακριτικού API", + "Create Agent": "Δημιουργία πράκτορα", + "Create Application": "Δημιουργία εφαρμογής", + "Create Collection": "Δημιουργία συλλογής", + "Create ConfigSet": "Δημιουργία ConfigSet", + "Create New Collection": "Δημιουργία νέας συλλογής", + "Create New ConfigSet": "Δημιουργία νέου ConfigSet", + "Create Operations": "Λειτουργίες δημιουργίας", + "Create Organisation": "Δημιουργία οργανισμού", + "Create Webhook": "Δημιουργία webhook", + "Create a copy of collection:": "Δημιουργία αντιγράφου της συλλογής:", + "Create a new ConfigSet based on the _default template": "Δημιουργία νέου ConfigSet βασισμένου στο πρότυπο _default", + "Create a new SOLR collection from an existing ConfigSet": "Δημιουργία νέας συλλογής SOLR από υπάρχον ConfigSet", + "Create a new SOLR collection from an existing configset": "Δημιουργία νέας συλλογής SOLR από υπάρχον configset", + "Create a new configset based on the _default template": "Δημιουργία νέου configset βασισμένου στο πρότυπο _default", + "Create a new event or link an existing one from any of your calendars.": "Δημιουργήστε ένα νέο συμβάν ή συνδέστε ένα υπάρχον από οποιοδήποτε από τα ημερολόγιά σας.", + "Create another": "Δημιουργία άλλου", + "Create copies of": "Δημιουργία αντιγράφων του", + "Create event": "Δημιουργία συμβάντος", + "Create new token": "Δημιουργία νέου διακριτικού", + "Create or link a Deck card to track work on this object.": "Δημιουργήστε ή συνδέστε μια κάρτα Deck για να παρακολουθείτε την εργασία σε αυτό το Αντικείμενο.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Δημιουργήστε την πρώτη verwerkingsactiviteit για να αρχίσετε να επισημαίνετε τις γραμμές του ίχνους ελέγχου με την απόδοσή τους κατά AVG Art 30.", + "Create your first AI agent to get started.": "Δημιουργήστε τον πρώτο σας πράκτορα AI για να ξεκινήσετε.", + "Created": "Δημιουργήθηκε", + "Created:": "Δημιουργήθηκε:", + "Creating search collections...": "Δημιουργία συλλογών αναζήτησης...", + "Creating...": "Δημιουργία σε εξέλιξη...", + "Current": "Τρέχον", + "Current Filters": "Τρέχοντα φίλτρα", + "Current Type:": "Τρέχων τύπος:", + "Current password": "Τρέχων κωδικός πρόσβασης", + "Custom API endpoint if using a different region": "Προσαρμοσμένο τελικό σημείο API εάν χρησιμοποιείτε διαφορετική περιοχή", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Προσαρμοσμένες κεφαλίδες HTTP (μία ανά γραμμή, μορφή: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Προσαρμοσμένες κεφαλίδες HTTP (μία ανά γραμμή, μορφή: header-name: value)", + "Custom Ranges:": "Προσαρμοσμένα εύρη:", + "Custom display title for this facet": "Προσαρμοσμένος τίτλος εμφάνισης για αυτήν την όψη", + "Custom pattern:": "Προσαρμοσμένο μοτίβο:", + "DELETE COLLECTION": "ΔΙΑΓΡΑΦΗ ΣΥΛΛΟΓΗΣ", + "DSAR": "DSAR", + "Daily": "Καθημερινά", + "Dashboard": "Πίνακας ελέγχου", + "Dashboard not found": "Ο πίνακας ελέγχου δεν βρέθηκε", + "Data": "Δεδομένα", + "Data Source": "Πηγή δεδομένων", + "Data Sources": "Πηγές δεδομένων", + "Data Views": "Προβολές δεδομένων", + "Data sources": "Πηγές δεδομένων", + "Data type does not match observed values": "Ο τύπος δεδομένων δεν ταιριάζει με τις παρατηρούμενες τιμές", + "Data-subject access request": "Αίτημα πρόσβασης υποκειμένου των δεδομένων", + "Database URL": "URL βάσης δεδομένων", + "Database information refreshed": "Οι πληροφορίες της βάσης δεδομένων ανανεώθηκαν", + "Date Faceting:": "Όψεις ημερομηνίας:", + "Date Range": "Εύρος ημερομηνιών", + "Dates": "Ημερομηνίες", + "Deactivation request cancelled": "Το αίτημα απενεργοποίησης ακυρώθηκε", + "Deactivation request submitted": "Το αίτημα απενεργοποίησης υποβλήθηκε", + "Deck": "Deck", + "Deck integration is not available": "Η ενσωμάτωση Deck δεν είναι διαθέσιμη", + "Default": "Προεπιλογή", + "Default Object Tenant": "Προεπιλεγμένος μισθωτής Αντικειμένου", + "Default Organisation": "Προεπιλεγμένος οργανισμός", + "Default Owner": "Προεπιλεγμένος ιδιοκτήτης", + "Default User (for cron/background jobs)": "Προεπιλεγμένος χρήστης (για cron/εργασίες παρασκηνίου)", + "Default User Tenant": "Προεπιλεγμένος μισθωτής χρήστη", + "Default value": "Προεπιλεγμένη τιμή", + "Default value (Email)": "Προεπιλεγμένη τιμή (Email)", + "Default value (Password)": "Προεπιλεγμένη τιμή (Κωδικός πρόσβασης)", + "Default value (Phone number)": "Προεπιλεγμένη τιμή (Αριθμός τηλεφώνου)", + "Default value (Regex)": "Προεπιλεγμένη τιμή (Regex)", + "Default: 5": "Προεπιλογή: 5", + "Defaults to current user. Select a different user if needed.": "Προεπιλέγεται ο τρέχων χρήστης. Επιλέξτε διαφορετικό χρήστη εάν χρειάζεται.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Οι καθυστερήσεις διπλασιάζονται με κάθε προσπάθεια (2, 4, 8 λεπτά...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Οι καθυστερήσεις αυξάνονται γραμμικά (5, 10, 15 λεπτά...)", + "Delete": "διαγραφή", + "Delete ({count})": "Διαγραφή ({count})", + "Delete Application": "Διαγραφή εφαρμογής", + "Delete Audit Trail": "Διαγραφή ίχνους ελέγχου", + "Delete Collection": "Διαγραφή Συλλογής", + "Delete ConfigSet": "Διαγραφή ConfigSet", + "Delete Objects": "Διαγραφή Αντικειμένων", + "Delete Operations": "Διαγραφή Λειτουργιών", + "Delete SOLR Collection": "Διαγραφή Συλλογής SOLR", + "Delete View": "Διαγραφή Προβολής", + "Delete all objects in this schema": "Διαγραφή όλων των αντικειμένων σε αυτό το Σχήμα", + "Delete permanently": "Οριστική διαγραφή", + "Delete this conversation?": "Διαγραφή αυτής της συνομιλίας;", + "Delete view": "Διαγραφή προβολής", + "Delete {count} object": "Διαγραφή {count} αντικειμένου", + "Delete {count} objects": "Διαγραφή {count} αντικειμένων", + "Deleted": "Διαγράφηκε", + "Deleted By": "Διαγράφηκε από", + "Deleted Date": "Ημερομηνία Διαγραφής", + "Deleted Items Management": "Διαχείριση Διαγραμμένων Στοιχείων", + "Deleted This Week": "Διαγράφηκαν Αυτήν την Εβδομάδα", + "Deleted Today": "Διαγράφηκαν Σήμερα", + "Deleted:": "Διαγράφηκε:", + "Deleting SOLR Collection...": "Διαγραφή Συλλογής SOLR...", + "Deleting...": "Διαγραφή...", + "Deletion Date Range": "Εύρος Ημερομηνιών Διαγραφής", + "Deletion Statistics": "Στατιστικά Διαγραφής", + "Deprecated": "Καταργημένο", + "Depublish": "Απόσυρση δημοσίευσης", + "Description": "Περιγραφή", + "Description Template": "Πρότυπο Περιγραφής", + "Description shown as tooltip": "Περιγραφή που εμφανίζεται ως υπόδειξη", + "Description:": "Περιγραφή:", + "Detect data types and patterns": "Ανίχνευση τύπων δεδομένων και μοτίβων", + "Detected At": "Ανιχνεύθηκε Στις", + "Detected Format:": "Ανιχνευμένη Μορφή:", + "Detected Issues:": "Ανιχνευμένα Ζητήματα:", + "Detected: {date}": "Ανιχνεύθηκε: {date}", + "Direct permission": "Άμεση άδεια", + "Disable": "Απενεργοποίηση", + "Disable this to make the agent": "Απενεργοποιήστε το για να κάνετε τον πράκτορα", + "Disabled": "Απενεργοποιημένο", + "Discover Files": "Ανακάλυψη Αρχείων", + "Discovered Properties": "Ανακαλυφθείσες Ιδιότητες", + "Display Format": "Μορφή Εμφάνισης", + "Display Name": "Εμφανιζόμενο Όνομα", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Θέλετε να διαγράψετε οριστικά όλες τις φιλτραρισμένες καταχωρήσεις ιχνηλασίας ελέγχου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Θέλετε να διαγράψετε οριστικά αυτήν την καταχώρηση ιχνηλασίας ελέγχου; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Documents": "Έγγραφα", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR ενεργοποιημένο", + "Download API Spec": "Λήψη Προδιαγραφής API", + "Download API Specification": "Λήψη Προδιαγραφής API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Λήψη αντιγράφου όλων των προσωπικών σας δεδομένων που είναι αποθηκευμένα στο OpenRegister (GDPR Άρθρο 20).", + "Drag a file or files here": "Σύρετε ένα Αρχείο ή αρχεία εδώ", + "Dry run completed: {count} fields would be created": "Η δοκιμαστική εκτέλεση ολοκληρώθηκε: θα δημιουργούνταν {count} πεδία", + "Dry run completed: {count} fields would be fixed": "Η δοκιμαστική εκτέλεση ολοκληρώθηκε: θα διορθώνονταν {count} πεδία", + "Duration in seconds (optional)": "Διάρκεια σε δευτερόλεπτα (προαιρετικό)", + "Edit": "Επεξεργασία", + "Edit Agent": "Επεξεργασία Πράκτορα", + "Edit Application": "Επεξεργασία Εφαρμογής", + "Edit Configuration": "Επεξεργασία Διαμόρφωσης", + "Edit Endpoint": "Επεξεργασία Σημείου Πρόσβασης", + "Edit Organisation": "Επεξεργασία Οργανισμού", + "Edit Property '{key}' of '{title}'": "Επεξεργασία Ιδιότητας '{key}' του '{title}'", + "Edit Register": "Επεξεργασία Μητρώου", + "Edit Schema": "Επεξεργασία Σχήματος", + "Edit Source": "Επεξεργασία Πηγής", + "Edit View": "Επεξεργασία Προβολής", + "Edit Webhook": "Επεξεργασία Webhook", + "Edit labels": "Επεξεργασία ετικετών", + "Edit processing activity": "Επεξεργασία δραστηριότητας επεξεργασίας", + "Edit tags for {name}": "Επεξεργασία ετικετών για {name}", + "Edit verwerkingsactiviteit": "Edit verwerkingsactiviteit", + "Edit view": "Επεξεργασία προβολής", + "Edit view details": "Επεξεργασία λεπτομερειών προβολής", + "Effectiveness": "Αποτελεσματικότητα", + "Email": "Email", + "Email Addresses": "Διευθύνσεις Email", + "Email digest frequency": "Συχνότητα σύνοψης Email", + "Emails": "Emails", + "Embedding Model": "Μοντέλο Ενσωμάτωσης", + "Embedding Provider": "Πάροχος Ενσωμάτωσης", + "Embedding generated successfully": "Η ενσωμάτωση δημιουργήθηκε με επιτυχία", + "Embedding provider connection successful!": "Η σύνδεση με τον πάροχο ενσωμάτωσης ήταν επιτυχής!", + "Embedding test failed: {error}": "Η δοκιμή ενσωμάτωσης απέτυχε: {error}", + "Enable": "Ενεργοποίηση", + "Enable RAG": "Ενεργοποίηση RAG", + "Enable auto-creation": "Ενεργοποίηση αυτόματης δημιουργίας", + "Enable automatic file vectorization": "Ενεργοποίηση αυτόματης διανυσματοποίησης Αρχείων", + "Enable automatic object vectorization": "Ενεργοποίηση αυτόματης διανυσματοποίησης Αντικειμένων", + "Enable automatic synchronization": "Ενεργοποίηση αυτόματου συγχρονισμού", + "Enable calendar provider": "Ενεργοποίηση παρόχου ημερολογίου", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Ενεργοποίηση λεπτομερούς καταγραφής για λειτουργίες SOLR (συνιστάται για αποσφαλμάτωση)", + "Enable faceting": "Ενεργοποίηση πλευρικής ταξινόμησης", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Ενεργοποιήστε ή απενεργοποιήστε τις λειτουργίες LLM. Διαμορφώστε παρόχους και μοντέλα χρησιμοποιώντας το κουμπί Διαμόρφωσης LLM παραπάνω.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Ενεργοποιήστε ή απενεργοποιήστε τις λειτουργίες LLM. Διαμορφώστε παρόχους και μοντέλα χρησιμοποιώντας το κουμπί διαμόρφωσης LLM παραπάνω.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Ενεργοποιήστε ή απενεργοποιήστε την ενσωμάτωση αναζήτησης SOLR. Διαμορφώστε τις ρυθμίσεις σύνδεσης χρησιμοποιώντας το κουμπί Ρυθμίσεις Σύνδεσης παραπάνω.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Ενεργοποιήστε ή απενεργοποιήστε την ενσωμάτωση αναζήτησης SOLR. Διαμορφώστε τις ρυθμίσεις σύνδεσης χρησιμοποιώντας το κουμπί ρυθμίσεων σύνδεσης παραπάνω.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Ενεργοποιήστε ή απενεργοποιήστε την ενσωμάτωση ροής εργασίας n8n. Διαμορφώστε τις ρυθμίσεις σύνδεσης παρακάτω.", + "Enable or disable this webhook": "Ενεργοποίηση ή απενεργοποίηση αυτού του webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Ενεργοποιήστε εργαλεία που επιτρέπουν στον πράκτορα να αλληλεπιδρά με δεδομένα μέσω κλήσης συναρτήσεων.", + "Enable vectorization for all existing and future views (may increase costs)": "Ενεργοποίηση διανυσματοποίησης για όλες τις υπάρχουσες και μελλοντικές προβολές (ενδέχεται να αυξήσει το κόστος)", + "Enable write-back to target objects": "Ενεργοποίηση εγγραφής πίσω στα Αντικείμενα-στόχους", + "Enabled": "Ενεργοποιημένο", + "End Date Field": "Πεδίο Ημερομηνίας Λήξης", + "Endpoint Path*": "Διαδρομή Σημείου Πρόσβασης*", + "Endpoints": "Σημεία Πρόσβασης", + "Engine not found": "Η μηχανή δεν βρέθηκε", + "Enter ConfigSet name": "Εισαγάγετε όνομα ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Εισαγάγετε ονόματα χρηστών Nextcloud για να παραχωρήσετε πρόσβαση σε αυτόν τον ιδιωτικό πράκτορα", + "Enter agent description (optional)": "Εισαγάγετε περιγραφή πράκτορα (προαιρετικό)", + "Enter agent name": "Εισαγάγετε όνομα πράκτορα", + "Enter application description (optional)": "Εισαγάγετε περιγραφή εφαρμογής (προαιρετικό)", + "Enter application name": "Εισαγάγετε όνομα εφαρμογής", + "Enter collection name": "Εισαγάγετε όνομα συλλογής", + "Enter configuration description (optional)": "Εισαγάγετε περιγραφή διαμόρφωσης (προαιρετικό)", + "Enter configuration title": "Εισαγάγετε τίτλο διαμόρφωσης", + "Enter description (optional)...": "Εισαγάγετε περιγραφή (προαιρετικό)...", + "Enter new collection name": "Εισαγάγετε νέο όνομα συλλογής", + "Enter object ID": "Εισαγάγετε αναγνωριστικό Αντικειμένου", + "Enter organisation description (optional)": "Εισαγάγετε περιγραφή οργανισμού (προαιρετικό)", + "Enter organisation name": "Εισαγάγετε όνομα οργανισμού", + "Enter search term": "Εισαγάγετε όρο αναζήτησης", + "Enter search terms or leave empty to browse all": "Εισαγάγετε όρους αναζήτησης ή αφήστε κενό για περιήγηση σε όλα", + "Enter system prompt for the agent": "Εισαγάγετε προτροπή συστήματος για τον πράκτορα", + "Enter username and press Enter": "Εισαγάγετε όνομα χρήστη και πατήστε Enter", + "Enter view name...": "Εισαγάγετε όνομα προβολής...", + "Enter webhook name": "Εισαγάγετε όνομα webhook", + "Enter your API key": "Εισαγάγετε το κλειδί API σας", + "Enter your object here...": "Εισαγάγετε το Αντικείμενό σας εδώ...", + "Enter your schema here...": "Εισαγάγετε το Σχήμα σας εδώ...", + "Entities": "Οντότητες", + "Entities detected": "Εντοπίστηκαν οντότητες", + "Entity ID: {id}": "Αναγνωριστικό Οντότητας: {id}", + "Entity Information": "Πληροφορίες Οντότητας", + "Entity Recognition": "Αναγνώριση Οντοτήτων", + "Entity Recognition Method": "Μέθοδος Αναγνώρισης Οντοτήτων", + "Entity deleted successfully": "Η οντότητα διαγράφηκε με επιτυχία", + "Entity not found": "Η οντότητα δεν βρέθηκε", + "Entries to be deleted:": "Καταχωρήσεις προς διαγραφή:", + "Enum Issue": "Ζήτημα Enum", + "Enum constraint is missing": "Λείπει ο περιορισμός enum", + "Erase (Art 17)": "Διαγραφή (Άρθρο 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Διαγραφή {count} Αντικειμένου/-ων για αυτό το υποκείμενο; Αυτή η ενέργεια καταγράφεται στην ιχνηλασία ελέγχου.", + "Erasure complete": "Η διαγραφή ολοκληρώθηκε", + "Erasure preview": "Προεπισκόπηση διαγραφής", + "Error": "Σφάλμα", + "Error Details": "Λεπτομέρειες Σφάλματος", + "Error Information": "Πληροφορίες Σφάλματος", + "Error Message": "Μήνυμα Σφάλματος", + "Error deleting audit trails: {error}": "Σφάλμα κατά τη διαγραφή ιχνηλασιών ελέγχου: {error}", + "Error deleting search trails: {error}": "Σφάλμα κατά τη διαγραφή ιχνηλασιών αναζήτησης: {error}", + "Error loading application": "Σφάλμα κατά τη φόρτωση εφαρμογής", + "Error loading audit trails": "Σφάλμα κατά τη φόρτωση ιχνηλασιών ελέγχου", + "Error loading entity": "Σφάλμα κατά τη φόρτωση οντότητας", + "Error loading search trails": "Σφάλμα κατά τη φόρτωση ιχνηλασιών αναζήτησης", + "Estimated Batches:": "Εκτιμώμενες Παρτίδες:", + "Estimated Cost:": "Εκτιμώμενο Κόστος:", + "Estimated Duration:": "Εκτιμώμενη Διάρκεια:", + "Event": "Συμβάν", + "Event Property for Payload": "Ιδιότητα Συμβάντος για το Ωφέλιμο Φορτίο", + "Events": "Συμβάντα", + "Every schema with detected PII has a processing-activity annotation.": "Κάθε Σχήμα με ανιχνευμένα PII διαθέτει επισημείωση δραστηριότητας επεξεργασίας.", + "Example": "Παράδειγμα", + "Example Value": "Παράδειγμα Τιμής", + "Example value for this property": "Παράδειγμα τιμής για αυτήν την Ιδιότητα", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Αποκλειστικό Μέγιστο", + "Exclusive Minimum": "Αποκλειστικό Ελάχιστο", + "Exclusive maximum": "Αποκλειστικό μέγιστο", + "Exclusive minimum": "Αποκλειστικό ελάχιστο", + "Execution Mode": "Λειτουργία Εκτέλεσης", + "Execution Time": "Χρόνος Εκτέλεσης", + "Execution Time Range": "Εύρος Χρόνου Εκτέλεσης", + "Existing Improvements": "Υπάρχουσες Βελτιώσεις", + "Expand details": "Ανάπτυξη λεπτομερειών", + "Expiration": "Λήξη", + "Expires": "Λήγει", + "Expires in (e.g., 90d)": "Λήγει σε (π.χ., 90d)", + "Exponential": "Εκθετικό", + "Export": "Εξαγωγή", + "Export \"{schema}\" objects from \"{register}\"": "Εξαγωγή Αντικειμένων \"{schema}\" από \"{register}\"", + "Export Configuration": "Εξαγωγή Διαμόρφωσης", + "Export Format:": "Μορφή Εξαγωγής:", + "Export Objects": "Εξαγωγή Αντικειμένων", + "Export completed successfully": "Η εξαγωγή ολοκληρώθηκε με επιτυχία", + "Export downloaded successfully": "Η εξαγωγή λήφθηκε με επιτυχία", + "Export failed: {error}": "Η εξαγωγή απέτυχε: {error}", + "Export is rate limited. Please try again later.": "Η εξαγωγή έχει περιορισμό ρυθμού. Δοκιμάστε ξανά αργότερα.", + "Export my data": "Εξαγωγή των δεδομένων μου", + "Export, view, or delete audit trails": "Εξαγωγή, προβολή ή διαγραφή ιχνηλασιών ελέγχου", + "Exporting...": "Εξαγωγή...", + "Extend Schema": "Επέκταση Σχήματος", + "Extended": "Εκτεταμένο", + "Extensions": "Επεκτάσεις", + "Extra Query Parameters": "Επιπλέον Παράμετροι Ερωτήματος", + "Extract Now": "Εξαγωγή Τώρα", + "Extract Pending Files": "Εξαγωγή Εκκρεμών Αρχείων", + "Extract now": "Εξαγωγή τώρα", + "Extract properties from each object": "Εξαγωγή Ιδιοτήτων από κάθε Αντικείμενο", + "Extracted At": "Εξήχθη Στις", + "Extracted at": "Εξήχθη στις", + "Extraction": "Εξαγωγή", + "Extraction Mode": "Λειτουργία Εξαγωγής", + "Extraction Scope": "Εμβέλεια Εξαγωγής", + "Extraction Status": "Κατάσταση Εξαγωγής", + "Extraction error for {file}: {error}": "Σφάλμα εξαγωγής για {file}: {error}", + "Facet Description": "Περιγραφή Όψης", + "Facet Order": "Σειρά Όψης", + "Facet Title": "Τίτλος Όψης", + "Facet Type": "Τύπος Όψης", + "Facet configuration updated successfully": "Η διαμόρφωση όψης ενημερώθηκε με επιτυχία", + "Facetable": "Με δυνατότητα όψης", + "Faceting Configuration:": "Διαμόρφωση Πλευρικής Ταξινόμησης:", + "Facets discovered and configured successfully": "Οι όψεις ανακαλύφθηκαν και διαμορφώθηκαν με επιτυχία", + "Facets discovered successfully": "Οι όψεις ανακαλύφθηκαν με επιτυχία", + "Failed": "Απέτυχε", + "Failed to add attachment to object": "Αποτυχία προσθήκης συνημμένου στο Αντικείμενο", + "Failed to add user to organisation": "Αποτυχία προσθήκης χρήστη στον οργανισμό", + "Failed to analyze schema properties": "Αποτυχία ανάλυσης Ιδιοτήτων Σχήματος", + "Failed to archive conversation": "Αποτυχία αρχειοθέτησης συνομιλίας", + "Failed to calculate sizes": "Αποτυχία υπολογισμού μεγεθών", + "Failed to cancel deactivation": "Αποτυχία ακύρωσης απενεργοποίησης", + "Failed to change active organisation: {error}": "Αποτυχία αλλαγής ενεργού οργανισμού: {error}", + "Failed to change password": "Αποτυχία αλλαγής κωδικού πρόσβασης", + "Failed to check version: {error}": "Αποτυχία ελέγχου έκδοσης: {error}", + "Failed to clear audit trails: {error}": "Αποτυχία εκκαθάρισης ιχνηλασιών ελέγχου: {error}", + "Failed to clear blob objects: {error}": "Αποτυχία εκκαθάρισης αντικειμένων blob: {error}", + "Failed to clear cache: {error}": "Αποτυχία εκκαθάρισης κρυφής μνήμης: {error}", + "Failed to clear collection": "Αποτυχία εκκαθάρισης συλλογής", + "Failed to clear conversation": "Αποτυχία εκκαθάρισης συνομιλίας", + "Failed to clear embeddings: {error}": "Αποτυχία εκκαθάρισης ενσωματώσεων: {error}", + "Failed to clear search trails: {error}": "Αποτυχία εκκαθάρισης ιχνηλασιών αναζήτησης: {error}", + "Failed to clear {type} cache: {error}": "Αποτυχία εκκαθάρισης κρυφής μνήμης {type}: {error}", + "Failed to copy changes": "Αποτυχία αντιγραφής αλλαγών", + "Failed to copy data": "Αποτυχία αντιγραφής δεδομένων", + "Failed to copy data to clipboard": "Αποτυχία αντιγραφής δεδομένων στο πρόχειρο", + "Failed to copy to clipboard": "Αποτυχία αντιγραφής στο πρόχειρο", + "Failed to copy token": "Αποτυχία αντιγραφής token", + "Failed to create conversation": "Αποτυχία δημιουργίας συνομιλίας", + "Failed to create missing SOLR fields: {error}": "Αποτυχία δημιουργίας πεδίων SOLR που λείπουν: {error}", + "Failed to create or find project": "Αποτυχία δημιουργίας ή εύρεσης έργου", + "Failed to create token": "Αποτυχία δημιουργίας token", + "Failed to delete SOLR collection: {error}": "Αποτυχία διαγραφής συλλογής SOLR: {error}", + "Failed to delete collection": "Αποτυχία διαγραφής συλλογής", + "Failed to delete conversation": "Αποτυχία διαγραφής συνομιλίας", + "Failed to delete objects for {schema}: {error}": "Αποτυχία διαγραφής Αντικειμένων για {schema}: {error}", + "Failed to delete webhook": "Αποτυχία διαγραφής webhook", + "Failed to depublish register: {error}": "Αποτυχία απόσυρσης δημοσίευσης Μητρώου: {error}", + "Failed to depublish schema: {error}": "Αποτυχία απόσυρσης δημοσίευσης Σχήματος: {error}", + "Failed to depublish: {error}": "Αποτυχία απόσυρσης δημοσίευσης: {error}", + "Failed to discover files: {error}": "Αποτυχία ανακάλυψης Αρχείων: {error}", + "Failed to download API specification": "Αποτυχία λήψης προδιαγραφής API", + "Failed to export data": "Αποτυχία εξαγωγής δεδομένων", + "Failed to export objects": "Αποτυχία εξαγωγής Αντικειμένων", + "Failed to fetch conversation history": "Αποτυχία ανάκτησης ιστορικού συνομιλίας", + "Failed to fix mismatched SOLR fields: {error}": "Αποτυχία διόρθωσης αναντίστοιχων πεδίων SOLR: {error}", + "Failed to get SOLR field configuration": "Αποτυχία λήψης διαμόρφωσης πεδίου SOLR", + "Failed to get chat statistics": "Αποτυχία λήψης στατιστικών συνομιλίας", + "Failed to get database information: %s": "Αποτυχία λήψης πληροφοριών βάσης δεδομένων: %s", + "Failed to get entity categories": "Αποτυχία λήψης κατηγοριών οντοτήτων", + "Failed to get entity statistics": "Αποτυχία λήψης στατιστικών οντοτήτων", + "Failed to get entity types": "Αποτυχία λήψης τύπων οντοτήτων", + "Failed to get response: {error}": "Αποτυχία λήψης απόκρισης: {error}", + "Failed to invalidate app store cache: {error}": "Αποτυχία ακύρωσης κρυφής μνήμης app store: {error}", + "Failed to leave organisation: {error}": "Αποτυχία αποχώρησης από τον οργανισμό: {error}", + "Failed to link object": "Αποτυχία σύνδεσης Αντικειμένου", + "Failed to load Deck cards": "Αποτυχία φόρτωσης καρτών Deck", + "Failed to load LLM configuration": "Αποτυχία φόρτωσης διαμόρφωσης LLM", + "Failed to load Nextcloud groups": "Αποτυχία φόρτωσης ομάδων Nextcloud", + "Failed to load cache statistics: {error}": "Αποτυχία φόρτωσης στατιστικών κρυφής μνήμης: {error}", + "Failed to load conversation": "Αποτυχία φόρτωσης συνομιλίας", + "Failed to load entities": "Αποτυχία φόρτωσης οντοτήτων", + "Failed to load entity": "Αποτυχία φόρτωσης οντότητας", + "Failed to load extraction data": "Αποτυχία φόρτωσης δεδομένων εξαγωγής", + "Failed to load files": "Αποτυχία φόρτωσης Αρχείων", + "Failed to load linked contacts": "Αποτυχία φόρτωσης συνδεδεμένων επαφών", + "Failed to load linked emails": "Αποτυχία φόρτωσης συνδεδεμένων email", + "Failed to load linked events": "Αποτυχία φόρτωσης συνδεδεμένων συμβάντων", + "Failed to load organisations": "Αποτυχία φόρτωσης οργανισμών", + "Failed to load register data": "Αποτυχία φόρτωσης δεδομένων Μητρώου", + "Failed to load related entities": "Αποτυχία φόρτωσης σχετικών οντοτήτων", + "Failed to load settings: {error}": "Αποτυχία φόρτωσης ρυθμίσεων: {error}", + "Failed to load statistics: {error}": "Αποτυχία φόρτωσης στατιστικών: {error}", + "Failed to load templates": "Αποτυχία φόρτωσης προτύπων", + "Failed to load view: {error}": "Αποτυχία φόρτωσης προβολής: {error}", + "Failed to load webhooks": "Αποτυχία φόρτωσης webhooks", + "Failed to load workflows": "Αποτυχία φόρτωσης ροών εργασίας", + "Failed to process message": "Αποτυχία επεξεργασίας μηνύματος", + "Failed to publish register: {error}": "Αποτυχία δημοσίευσης Μητρώου: {error}", + "Failed to publish schema: {error}": "Αποτυχία δημοσίευσης Σχήματος: {error}", + "Failed to publish: {error}": "Αποτυχία δημοσίευσης: {error}", + "Failed to refresh database information": "Αποτυχία ανανέωσης πληροφοριών βάσης δεδομένων", + "Failed to reindex collection": "Αποτυχία αναδημιουργίας ευρετηρίου συλλογής", + "Failed to remove avatar": "Αποτυχία αφαίρεσης άβαταρ", + "Failed to remove link": "Αποτυχία αφαίρεσης συνδέσμου", + "Failed to remove member: {error}": "Αποτυχία αφαίρεσης μέλους: {error}", + "Failed to remove schema {schema}: {error}": "Αποτυχία αφαίρεσης Σχήματος {schema}: {error}", + "Failed to remove user from organisation": "Αποτυχία αφαίρεσης χρήστη από τον οργανισμό", + "Failed to rename conversation": "Αποτυχία μετονομασίας συνομιλίας", + "Failed to request deactivation": "Αποτυχία αιτήματος απενεργοποίησης", + "Failed to restore conversation": "Αποτυχία επαναφοράς συνομιλίας", + "Failed to retrieve extraction status.": "Αποτυχία ανάκτησης κατάστασης εξαγωγής.", + "Failed to retrieve objects for file.": "Αποτυχία ανάκτησης Αντικειμένων για το Αρχείο.", + "Failed to retry extraction": "Αποτυχία επανάληψης εξαγωγής", + "Failed to retry webhook": "Αποτυχία επανάληψης webhook", + "Failed to revoke token": "Αποτυχία ανάκλησης token", + "Failed to save GitHub token": "Αποτυχία αποθήκευσης GitHub token", + "Failed to save GitLab URL": "Αποτυχία αποθήκευσης GitLab URL", + "Failed to save GitLab token": "Αποτυχία αποθήκευσης GitLab token", + "Failed to save LLM settings: {error}": "Αποτυχία αποθήκευσης ρυθμίσεων LLM: {error}", + "Failed to save additional feedback": "Αποτυχία αποθήκευσης πρόσθετων σχολίων", + "Failed to save configuration.": "Αποτυχία αποθήκευσης διαμόρφωσης.", + "Failed to save configuration: {error}": "Αποτυχία αποθήκευσης διαμόρφωσης: {error}", + "Failed to save facet configuration": "Αποτυχία αποθήκευσης διαμόρφωσης όψης", + "Failed to save feedback": "Αποτυχία αποθήκευσης σχολίων", + "Failed to save file settings: {error}": "Αποτυχία αποθήκευσης ρυθμίσεων Αρχείου: {error}", + "Failed to save n8n configuration": "Αποτυχία αποθήκευσης διαμόρφωσης n8n", + "Failed to save preferences": "Αποτυχία αποθήκευσης προτιμήσεων", + "Failed to save roles": "Αποτυχία αποθήκευσης ρόλων", + "Failed to save settings": "Αποτυχία αποθήκευσης ρυθμίσεων", + "Failed to save settings: {error}": "Αποτυχία αποθήκευσης ρυθμίσεων: {error}", + "Failed to save view: {error}": "Αποτυχία αποθήκευσης προβολής: {error}", + "Failed to save warmup interval: {error}": "Αποτυχία αποθήκευσης διαστήματος προθέρμανσης: {error}", + "Failed to save webhook": "Αποτυχία αποθήκευσης webhook", + "Failed to send feedback": "Αποτυχία αποστολής σχολίων", + "Failed to set active organisation: {error}": "Αποτυχία ορισμού ενεργού οργανισμού: {error}", + "Failed to setup SOLR: {error}": "Αποτυχία ρύθμισης SOLR: {error}", + "Failed to start file warmup: {error}": "Αποτυχία έναρξης προθέρμανσης Αρχείων: {error}", + "Failed to start processing {type} files: {error}": "Αποτυχία έναρξης επεξεργασίας Αρχείων {type}: {error}", + "Failed to start vectorization: {error}": "Αποτυχία έναρξης διανυσματοποίησης: {error}", + "Failed to switch organisation: {error}": "Αποτυχία εναλλαγής οργανισμού: {error}", + "Failed to sync magic table for {schema}: {error}": "Αποτυχία συγχρονισμού magic table για {schema}: {error}", + "Failed to test SOLR connection: {error}": "Αποτυχία δοκιμής σύνδεσης SOLR: {error}", + "Failed to test webhook": "Αποτυχία δοκιμής webhook", + "Failed to update LLM settings: {error}": "Αποτυχία ενημέρωσης ρυθμίσεων LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Αποτυχία ενημέρωσης ρυθμίσεων Multitenancy: {error}", + "Failed to update RBAC settings: {error}": "Αποτυχία ενημέρωσης ρυθμίσεων RBAC: {error}", + "Failed to update Retention settings: {error}": "Αποτυχία ενημέρωσης ρυθμίσεων διατήρησης: {error}", + "Failed to update SOLR settings: {error}": "Αποτυχία ενημέρωσης ρυθμίσεων SOLR: {error}", + "Failed to update favorite status": "Αποτυχία ενημέρωσης κατάστασης αγαπημένου", + "Failed to update schema configuration for {schema}: {error}": "Αποτυχία ενημέρωσης διαμόρφωσης Σχήματος για {schema}: {error}", + "Failed to update schema properties": "Αποτυχία ενημέρωσης Ιδιοτήτων Σχήματος", + "Failed to update view": "Αποτυχία ενημέρωσης προβολής", + "Failed to update view: {error}": "Αποτυχία ενημέρωσης προβολής: {error}", + "Failed to update webhook": "Αποτυχία ενημέρωσης webhook", + "Failed to upload avatar": "Αποτυχία μεταφόρτωσης άβαταρ", + "Failed to validate {schema}: {error}": "Αποτυχία επικύρωσης {schema}: {error}", + "Failed to vectorize files: {error}": "Αποτυχία διανυσματοποίησης Αρχείων: {error}", + "Failed to warmup names cache: {error}": "Αποτυχία προθέρμανσης κρυφής μνήμης ονομάτων: {error}", + "Federation": "Ομοσπονδία", + "Feedback recorded": "Τα σχόλια καταγράφηκαν", + "Fewer sources (1-3):": "Λιγότερες Πηγές (1-3):", + "Field": "Πεδίο", + "Fields to Display": "Πεδία προς Εμφάνιση", + "File": "Αρχείο", + "File #{id}": "Αρχείο #{id}", + "File Chunk Prediction": "Πρόβλεψη Τμημάτων Αρχείου", + "File Collection": "Συλλογή Αρχείων", + "File Handling": "Διαχείριση Αρχείων", + "File Location": "Τοποθεσία Αρχείου", + "File Management": "Διαχείριση Αρχείων", + "File Name": "Όνομα Αρχείου", + "File Path": "Διαδρομή Αρχείου", + "File Type Selection": "Επιλογή Τύπου Αρχείου", + "File Types to Process": "Τύποι Αρχείων προς Επεξεργασία", + "File Vectorization": "Διανυσματοποίηση Αρχείων", + "File Warmup": "Προθέρμανση Αρχείων", + "File actions menu": "Μενού ενεργειών Αρχείου", + "File anonymized successfully": "Το Αρχείο ανωνυμοποιήθηκε με επιτυχία", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Η διανυσματοποίηση τμημάτων Αρχείου δεν έχει υλοποιηθεί ακόμη. Τα τμήματα είναι έτοιμα και αποθηκευμένα, αλλά η υπηρεσία διανυσματοποίησης βρίσκεται υπό ανάπτυξη.", + "File collection not configured": "Η συλλογή Αρχείων δεν έχει διαμορφωθεί", + "File discovery completed": "Η ανακάλυψη Αρχείων ολοκληρώθηκε", + "File extraction completed": "Η εξαγωγή Αρχείων ολοκληρώθηκε", + "File extraction queued": "Η εξαγωγή Αρχείων μπήκε σε ουρά", + "File indexed successfully": "Το Αρχείο προστέθηκε στο ευρετήριο με επιτυχία", + "File is already anonymized": "Το Αρχείο είναι ήδη ανωνυμοποιημένο", + "File name": "Όνομα Αρχείου", + "File not found": "Το Αρχείο δεν βρέθηκε", + "File settings saved successfully": "Οι ρυθμίσεις Αρχείου αποθηκεύτηκαν με επιτυχία", + "File settings updated successfully": "Οι ρυθμίσεις Αρχείου ενημερώθηκαν με επιτυχία", + "File sources": "Πηγές Αρχείων", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Η διανυσματοποίηση Αρχείων ολοκληρώθηκε! {vectorized} τμήματα διανυσματοποιήθηκαν από {files} Αρχεία. {failed} απέτυχαν.", + "File vectorization configuration saved successfully": "Η διαμόρφωση διανυσματοποίησης Αρχείων αποθηκεύτηκε με επιτυχία", + "File vectorization started. Check the statistics section for progress.": "Η διανυσματοποίηση Αρχείων ξεκίνησε. Ελέγξτε την ενότητα στατιστικών για την πρόοδο.", + "File warmup completed": "Η προθέρμανση Αρχείων ολοκληρώθηκε", + "Files": "Αρχεία", + "Files added successfully": "Τα Αρχεία προστέθηκαν με επιτυχία", + "Files with Completed Extraction:": "Αρχεία με Ολοκληρωμένη Εξαγωγή:", + "Files → fileCollection, Objects → objectCollection": "Αρχεία → fileCollection, Αντικείμενα → objectCollection", + "Files → filecollection, objects → objectcollection": "Αρχεία → filecollection, αντικείμενα → objectcollection", + "Filter Audit Trails": "Φιλτράρισμα Ιχνηλασιών Ελέγχου", + "Filter Deleted Items": "Φιλτράρισμα Διαγραμμένων Στοιχείων", + "Filter Objects": "Φιλτράρισμα Αντικειμένων", + "Filter Properties": "Φιλτράρισμα Ιδιοτήτων", + "Filter Search Trails": "Φιλτράρισμα Ιχνηλασιών Αναζήτησης", + "Filter Statistics": "Φιλτράρισμα Στατιστικών", + "Filter and analyze search trail entries": "Φιλτράρισμα και ανάλυση καταχωρήσεων ιχνηλασίας αναζήτησης", + "Filter and manage audit trail entries": "Φιλτράρισμα και διαχείριση καταχωρήσεων ιχνηλασίας ελέγχου", + "Filter and manage soft deleted items": "Φιλτράρισμα και διαχείριση ήπια διαγραμμένων στοιχείων", + "Filter and search entities": "Φιλτράρισμα και αναζήτηση οντοτήτων", + "Filter by object ID": "Φιλτράρισμα κατά αναγνωριστικό Αντικειμένου", + "Filter by search term": "Φιλτράρισμα κατά όρο αναζήτησης", + "Filter by type": "Φιλτράρισμα κατά τύπο", + "Filter by webhook": "Φιλτράρισμα κατά webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Τα δεδομένα φίλτρου φορτώθηκαν αυτόματα. Χρησιμοποιήστε τα παρακάτω φίλτρα για να βελτιώσετε την αναζήτησή σας.", + "Filter fields...": "Φιλτράρισμα πεδίων...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Φιλτράρισμα ενεργοποιητών webhook κατά Ιδιότητες ωφέλιμου φορτίου (μία ανά γραμμή, μορφή: key: value)", + "Filtered": "Φιλτραρισμένο", + "Filters": "Φίλτρα", + "Finalizing setup...": "Οριστικοποίηση ρύθμισης...", + "Fireworks AI Chat Settings": "Ρυθμίσεις Συνομιλίας Fireworks AI", + "Fireworks AI Embedding Configuration": "Διαμόρφωση Ενσωμάτωσης Fireworks AI", + "First": "Πρώτο", + "Fixed": "Σταθερό", + "For chat and retrieval-augmented generation": "Για συνομιλία και παραγωγή με ενίσχυση ανάκτησης", + "For vector embeddings and semantic search": "Για διανυσματικές ενσωματώσεις και σημασιολογική αναζήτηση", + "Format": "Μορφή", + "Format Issue": "Ζήτημα Μορφής", + "Format constraint is missing": "Λείπει ο περιορισμός μορφής", + "From (e.g. -7 days or 2025-01-01)": "Από (π.χ. -7 days ή 2025-01-01)", + "From Date": "Από Ημερομηνία", + "From date": "Από ημερομηνία", + "Full data copied to clipboard": "Όλα τα δεδομένα αντιγράφηκαν στο πρόχειρο", + "Function Tools": "Εργαλεία Συναρτήσεων", + "General Issue": "Γενικό Ζήτημα", + "Generate recommendations and confidence scores": "Δημιουργία συστάσεων και βαθμολογιών εμπιστοσύνης", + "Generate report": "Δημιουργία αναφοράς", + "Generate the verantwoordingsdocument": "Δημιουργία του verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Δημιουργία διανυσμάτων αμέσως μόλις δημιουργηθούν νέα Αντικείμενα", + "Generated: {time}": "Δημιουργήθηκε: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Αποκτήστε πληροφορίες σε πραγματικό χρόνο για την υγεία των δεδομένων του οργανισμού σας, εστιάζοντας στα Μητρώα, τους ορισμούς Σχημάτων και την αποθήκευση και χρήση Αντικειμένων.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Αποκτήστε πληροφορίες σε πραγματικό χρόνο για την υγεία των δεδομένων του οργανισμού σας, τις τάσεις ανάπτυξης και τις μετρήσεις ποιότητας.", + "GitHub Personal Access Token": "Προσωπικό Token Πρόσβασης GitHub", + "GitHub token is valid": "Το GitHub token είναι έγκυρο", + "GitHub token is valid! Username: {username}": "Το GitHub token είναι έγκυρο! Όνομα χρήστη: {username}", + "GitHub token removed successfully": "Το GitHub token αφαιρέθηκε με επιτυχία", + "GitHub token saved successfully": "Το GitHub token αποθηκεύτηκε με επιτυχία", + "GitHub token test failed: {message}": "Η δοκιμή του GitHub token απέτυχε: {message}", + "GitLab Personal Access Token": "Προσωπικό Token Πρόσβασης GitLab", + "GitLab URL saved successfully": "Το GitLab URL αποθηκεύτηκε με επιτυχία", + "GitLab token is valid": "Το GitLab token είναι έγκυρο", + "GitLab token is valid! Username: {username}": "Το GitLab token είναι έγκυρο! Όνομα χρήστη: {username}", + "GitLab token saved successfully": "Το GitLab token αποθηκεύτηκε με επιτυχία", + "GitLab token test failed: {message}": "Η δοκιμή του GitLab token απέτυχε: {message}", + "Go to linked Register": "Μετάβαση στο συνδεδεμένο Μητρώο", + "Go to linked Schema": "Μετάβαση στο συνδεδεμένο Σχήμα", + "Go to organisation": "Μετάβαση στον οργανισμό", + "Grant additional permissions beyond standard CRUD operations": "Παραχώρηση πρόσθετων αδειών πέρα από τις τυπικές λειτουργίες CRUD", + "Group Quota": "Όριο Ομάδας", + "Groups": "Ομάδες", + "HTML preview": "Προεπισκόπηση HTML", + "HTTP Method": "Μέθοδος HTTP", + "HTTP method used to send webhook requests": "Μέθοδος HTTP που χρησιμοποιείται για την αποστολή αιτημάτων webhook", + "Headers": "Κεφαλίδες", + "Health": "Υγεία", + "Heartbeat successful - connection kept alive": "Επιτυχής παλμός - η σύνδεση διατηρήθηκε ενεργή", + "Helpful": "Χρήσιμο", + "Here you can configure the details for various connections.": "Εδώ μπορείτε να διαμορφώσετε τις λεπτομέρειες για διάφορες συνδέσεις.", + "Here you can configure the details for your organisation.": "Εδώ μπορείτε να διαμορφώσετε τις λεπτομέρειες για τον οργανισμό σας.", + "Hide": "Απόκρυψη", + "Hide Filters": "Απόκρυψη Φίλτρων", + "Hide in collection view": "Απόκρυψη στην προβολή συλλογής", + "Hide in forms": "Απόκρυψη στις φόρμες", + "Hide in list view": "Απόκρυψη στην προβολή λίστας", + "High": "Υψηλό", + "High Confidence": "Υψηλή Εμπιστοσύνη", + "Host": "Κεντρικός υπολογιστής", + "Hourly": "Ωριαία", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Πόσο βαθιά να διασχιστούν οι ένθετες Ιδιότητες Αντικειμένου (1-20). Οι υψηλότερες τιμές καταγράφουν περισσότερες λεπτομέρειες αλλά αυξάνουν το μέγεθος του διανύσματος.", + "How often to check for updates (1-168 hours)": "Πόσο συχνά να γίνεται έλεγχος για ενημερώσεις (1-168 ώρες)", + "How to handle retries for failed webhook deliveries": "Πώς να γίνεται ο χειρισμός επαναλήψεων για αποτυχημένες παραδόσεις webhook", + "IBANs": "IBANs", + "ID": "ID", + "ID of input mapping (optional)": "ID της αντιστοίχισης εισόδου (προαιρετικό)", + "ID of output mapping (optional)": "ID της αντιστοίχισης εξόδου (προαιρετικό)", + "ID of the target resource": "ID του πόρου-στόχου", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Τα όρια ρυθμού IP εκκαθαρίστηκαν με επιτυχία", + "Identify properties not in the schema": "Εντοπισμός ιδιοτήτων που δεν υπάρχουν στο Σχήμα", + "Immutable": "Αμετάβλητο", + "Import": "Εισαγωγή", + "Import Configuration": "Εισαγωγή διαμόρφωσης", + "Import Data into Register": "Εισαγωγή δεδομένων στο Μητρώο", + "Import successful": "Επιτυχής εισαγωγή", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Εισαγάγετε το πρότυπο report-bundle.json για να αποκτήσετε το Μητρώο `reports`, και στη συνέχεια δημιουργήστε τον πρώτο σας πίνακα ελέγχου μέσω του τυπικού περιβάλλοντος Αντικειμένου. Οι πίνακες ελέγχου δηλώνουν τα γραφικά στοιχεία τους σε JSON και ο αποδότης τροφοδοτεί κάθε γραφικό στοιχείο με ζωντανά δεδομένα συγκέντρωσης.", + "Improved Property": "Βελτιωμένη Ιδιότητα", + "In use": "Σε χρήση", + "Inactive": "Ανενεργό", + "Inactive organisations cannot be used": "Οι ανενεργοί οργανισμοί δεν μπορούν να χρησιμοποιηθούν", + "Include IDs and names of related objects for better contextual search": "Συμπερίληψη των ID και των ονομάτων των σχετιζόμενων Αντικειμένων για καλύτερη συμφραστική αναζήτηση", + "Include ids and names of related objects for better contextual search": "Συμπερίληψη των ID και των ονομάτων των σχετιζόμενων Αντικειμένων για καλύτερη συμφραστική αναζήτηση", + "Include related object references": "Συμπερίληψη αναφορών σχετιζόμενων Αντικειμένων", + "Include schema and register metadata": "Συμπερίληψη μεταδεδομένων Σχήματος και Μητρώου", + "Index": "Ευρετήριο", + "Information about the current OpenRegister installation": "Πληροφορίες σχετικά με την τρέχουσα εγκατάσταση OpenRegister", + "Inherited from register": "Κληρονομήθηκε από το Μητρώο", + "Inherits permissions from register": "Κληρονομεί δικαιώματα από το Μητρώο", + "Initialization failed": "Η αρχικοποίηση απέτυχε", + "Initialize Project": "Αρχικοποίηση έργου", + "Initializing SOLR setup...": "Αρχικοποίηση εγκατάστασης SOLR...", + "Input Mapping": "Αντιστοίχιση εισόδου", + "Inspect Fields": "Επιθεώρηση πεδίων", + "Inspect Index": "Επιθεώρηση ευρετηρίου", + "Inspect SOLR Index": "Επιθεώρηση ευρετηρίου SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Εγκαταστήστε την εφαρμογή notify_push από το Nextcloud App Store για να ενεργοποιήσετε τις ενημερώσεις σε πραγματικό χρόνο.", + "Integrations": "Ενσωματώσεις", + "Internet location (URL) of this installation": "Διαδικτυακή τοποθεσία (URL) αυτής της εγκατάστασης", + "Interval": "Διάστημα", + "Invalid": "Μη έγκυρο", + "Invalid batch size. Must be between 1 and 5000": "Μη έγκυρο μέγεθος παρτίδας. Πρέπει να είναι μεταξύ 1 και 5000", + "Invalid feedback type": "Μη έγκυρος τύπος ανατροφοδότησης", + "Invalid field name provided": "Παρασχέθηκε μη έγκυρο όνομα πεδίου", + "Invalid maxObjects. Must be 0 (all) or positive number": "Μη έγκυρο maxObjects. Πρέπει να είναι 0 (όλα) ή θετικός αριθμός", + "Invalid:": "Μη έγκυρο:", + "Invite Users": "Πρόσκληση χρηστών", + "Invited Users": "Προσκεκλημένοι χρήστες", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Αποτελέσματα inzage", + "Issue": "Ζήτημα", + "Items per page:": "Στοιχεία ανά σελίδα:", + "Just now": "Μόλις τώρα", + "Key": "Κλειδί", + "LLM Configuration": "Διαμόρφωση LLM", + "LLM actions menu": "Μενού ενεργειών LLM", + "LLM configuration saved successfully": "Η διαμόρφωση LLM αποθηκεύτηκε με επιτυχία", + "LLM features disabled": "Οι λειτουργίες LLM απενεργοποιήθηκαν", + "LLM features enabled": "Οι λειτουργίες LLM ενεργοποιήθηκαν", + "LLM must be enabled with an embedding provider configured": "Το LLM πρέπει να είναι ενεργοποιημένο με διαμορφωμένο πάροχο ενσωμάτωσης", + "LLM settings saved successfully": "Οι ρυθμίσεις LLM αποθηκεύτηκαν με επιτυχία", + "LLM settings updated successfully": "Οι ρυθμίσεις LLM ενημερώθηκαν με επιτυχία", + "Label": "Ετικέτα", + "Labels": "Ετικέτες", + "Languages": "Γλώσσες", + "Last": "Τελευταίο", + "Last Triggered": "Τελευταία ενεργοποίηση", + "Leave empty to allow all users access": "Αφήστε το κενό για να επιτρέψετε την πρόσβαση σε όλους τους χρήστες", + "Leave off for auto-detection from property format.": "Αφήστε το απενεργοποιημένο για αυτόματο εντοπισμό από τη μορφή της Ιδιότητας.", + "Left organisation successfully": "Αποχωρήσατε από τον οργανισμό με επιτυχία", + "Legal basis *": "Νομική βάση *", + "Length Range:": "Εύρος μήκους:", + "Linear": "Γραμμικό", + "Link": "Σύνδεσμος", + "Link an email from the Mail app sidebar to associate it with this object.": "Συνδέστε ένα email από την πλευρική μπάρα της εφαρμογής Mail για να το συσχετίσετε με αυτό το Αντικείμενο.", + "Link an object to see it here.": "Συνδέστε ένα Αντικείμενο για να το δείτε εδώ.", + "Link another object": "Σύνδεση άλλου Αντικειμένου", + "Link existing event": "Σύνδεση υπάρχοντος συμβάντος", + "Link removed": "Ο σύνδεσμος αφαιρέθηκε", + "Link to Object": "Σύνδεση με Αντικείμενο", + "Link to {name}": "Σύνδεση με {name}", + "Linked Objects": "Συνδεδεμένα Αντικείμενα", + "Linked by {user}": "Συνδέθηκε από τον/την {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Τα συνδεδεμένα email, συμβάντα ημερολογίου, επαφές και κάρτες Deck θα εμφανιστούν εδώ.", + "Linked to {name}": "Συνδεδεμένο με {name}", + "Load Advanced Filters": "Φόρτωση σύνθετων φίλτρων", + "Load advanced filters with live data from your search index": "Φορτώστε σύνθετα φίλτρα με ζωντανά δεδομένα από το ευρετήριο αναζήτησής σας", + "Load more": "Φόρτωση περισσότερων", + "Load view": "Φόρτωση προβολής", + "Loading API tokens...": "Φόρτωση διακριτικών API...", + "Loading ConfigSets...": "Φόρτωση ConfigSets...", + "Loading LLM configuration...": "Φόρτωση διαμόρφωσης LLM...", + "Loading RBAC settings...": "Φόρτωση ρυθμίσεων RBAC...", + "Loading activity...": "Φόρτωση δραστηριότητας...", + "Loading advanced filters...": "Φόρτωση σύνθετων φίλτρων...", + "Loading agents...": "Φόρτωση πρακτόρων...", + "Loading application details...": "Φόρτωση λεπτομερειών εφαρμογής...", + "Loading applications...": "Φόρτωση εφαρμογών...", + "Loading archived conversations...": "Φόρτωση αρχειοθετημένων συνομιλιών...", + "Loading audit trails...": "Φόρτωση ιχνών ελέγχου...", + "Loading available tools...": "Φόρτωση διαθέσιμων εργαλείων...", + "Loading cache statistics...": "Φόρτωση στατιστικών προσωρινής μνήμης...", + "Loading collections...": "Φόρτωση συλλογών...", + "Loading configurations...": "Φόρτωση διαμορφώσεων...", + "Loading conversation...": "Φόρτωση συνομιλίας...", + "Loading conversations...": "Φόρτωση συνομιλιών...", + "Loading deleted items...": "Φόρτωση διαγραμμένων στοιχείων...", + "Loading endpoints": "Φόρτωση σημείων πρόσβασης", + "Loading entities...": "Φόρτωση οντοτήτων...", + "Loading events...": "Φόρτωση συμβάντων...", + "Loading filter data...": "Φόρτωση δεδομένων φίλτρου...", + "Loading groups...": "Φόρτωση ομάδων...", + "Loading linked objects...": "Φόρτωση συνδεδεμένων Αντικειμένων...", + "Loading log details...": "Φόρτωση λεπτομερειών αρχείου καταγραφής...", + "Loading multitenancy settings...": "Φόρτωση ρυθμίσεων πολυμίσθωσης...", + "Loading n8n configuration...": "Φόρτωση διαμόρφωσης n8n...", + "Loading permission matrix...": "Φόρτωση πίνακα δικαιωμάτων...", + "Loading preferences...": "Φόρτωση προτιμήσεων...", + "Loading register data...": "Φόρτωση δεδομένων Μητρώου...", + "Loading registers...": "Φόρτωση Μητρώων...", + "Loading retention settings...": "Φόρτωση ρυθμίσεων διατήρησης...", + "Loading schemas...": "Φόρτωση Σχημάτων...", + "Loading search configuration...": "Φόρτωση διαμόρφωσης αναζήτησης...", + "Loading search trails...": "Φόρτωση ιχνών αναζήτησης...", + "Loading sources...": "Φόρτωση Πηγών...", + "Loading statistics...": "Φόρτωση στατιστικών...", + "Loading text extraction settings...": "Φόρτωση ρυθμίσεων εξαγωγής κειμένου...", + "Loading tokens...": "Φόρτωση διακριτικών...", + "Loading user groups...": "Φόρτωση ομάδων χρηστών...", + "Loading users...": "Φόρτωση χρηστών...", + "Loading views...": "Φόρτωση προβολών...", + "Loading...": "Φόρτωση...", + "Local": "Τοπικό", + "Local Version": "Τοπική έκδοση", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Εντοπίστε κάθε Αντικείμενο που αναφέρεται σε ένα υποκείμενο δεδομένων (Art 15 inzage), κάντε προεπισκόπηση μιας διαγραφής (Art 17 vergetelheid) ή εξαγάγετε τα δεδομένα του (Art 20 portabiliteit).", + "Location": "Τοποθεσία", + "Location Field": "Πεδίο τοποθεσίας", + "Locations": "Τοποθεσίες", + "Locked": "Κλειδωμένο", + "Locked:": "Κλειδωμένο:", + "Login successful": "Επιτυχής σύνδεση", + "Logs": "Αρχεία καταγραφής", + "Low": "Χαμηλό", + "Low Confidence": "Χαμηλή εμπιστοσύνη", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Οι μικρότεροι αριθμοί εμφανίζονται πρώτοι στην πλευρική μπάρα φίλτρων. Αφήστε το κενό για αυτόματη ταξινόμηση.", + "Lucene Version:": "Έκδοση Lucene:", + "Magic table sync completed for {schema}": "Ο συγχρονισμός του πίνακα magic ολοκληρώθηκε για το {schema}", + "Magic table synchronized successfully": "Ο πίνακας magic συγχρονίστηκε με επιτυχία", + "Mail Integration": "Ενσωμάτωση Mail", + "Mail integration is not available": "Η ενσωμάτωση Mail δεν είναι διαθέσιμη", + "Make this installation discoverable within the federation network.": "Κάντε αυτή την εγκατάσταση ανιχνεύσιμη εντός του δικτύου ομοσπονδίας.", + "Manage Organisation Roles": "Διαχείριση ρόλων οργανισμού", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Διαχειριστείτε τις Συλλογές SOLR (αποθηκευτικούς χώρους δεδομένων) και αναθέστε τις για Αντικείμενα και Αρχεία.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Διαχειριστείτε τα SOLR ConfigSets (πρότυπα διαμόρφωσης) για τις συλλογές σας.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Διαχειριστείτε τις συλλογές SOLR (αποθηκευτικούς χώρους δεδομένων) και αναθέστε τις για Αντικείμενα και Αρχεία.", + "Manage SOLR configsets (configuration templates) for your collections.": "Διαχειριστείτε τα SOLR configsets (πρότυπα διαμόρφωσης) για τις συλλογές σας.", + "Manage and configure agents for automated tasks": "Διαχειριστείτε και διαμορφώστε πράκτορες για αυτοματοποιημένες εργασίες", + "Manage and monitor file text extraction status": "Διαχειριστείτε και παρακολουθήστε την κατάσταση εξαγωγής κειμένου Αρχείων", + "Manage and restore soft deleted items from your registers": "Διαχειριστείτε και επαναφέρετε προσωρινά διαγραμμένα στοιχεία από τα Μητρώα σας", + "Manage and view detected entities from files and objects": "Διαχειριστείτε και προβάλετε εντοπισμένες οντότητες από Αρχεία και Αντικείμενα", + "Manage document templates and themes": "Διαχείριση προτύπων εγγράφων και θεμάτων", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Διαχειριστείτε ροές εργασίας n8n για την αυτοματοποίηση του OpenRegister. Οι ροές εργασίας θα αποθηκευτούν στο διαμορφωμένο έργο.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Διαχειριστείτε δραστηριότητες επεξεργασίας, εκτελέστε αιτήματα πρόσβασης υποκειμένων δεδομένων και ελέγξτε τη συμμόρφωση σύμφωνα με τον GDPR της ΕΕ / το ολλανδικό AVG.", + "Manage webhooks for event-driven integrations": "Διαχειριστείτε webhooks για ενσωματώσεις βασισμένες σε συμβάντα", + "Manage your account settings, security, and personal data.": "Διαχειριστείτε τις ρυθμίσεις του λογαριασμού σας, την ασφάλεια και τα προσωπικά σας δεδομένα.", + "Manage your applications and modules": "Διαχειριστείτε τις εφαρμογές και τις μονάδες σας", + "Manage your chat conversations": "Διαχειριστείτε τις συνομιλίες σας", + "Manage your data registers and their configurations": "Διαχειριστείτε τα Μητρώα δεδομένων σας και τις διαμορφώσεις τους", + "Manage your data schemas and their properties": "Διαχειριστείτε τα Σχήματα δεδομένων σας και τις Ιδιότητές τους", + "Manage your data sources and their configurations": "Διαχειριστείτε τις Πηγές δεδομένων σας και τις διαμορφώσεις τους", + "Manage your organisations and switch between them": "Διαχειριστείτε τους οργανισμούς σας και εναλλάσσεστε μεταξύ τους", + "Manage your saved search configurations": "Διαχειριστείτε τις αποθηκευμένες διαμορφώσεις αναζήτησής σας", + "Manage your system configurations and settings": "Διαχειριστείτε τις διαμορφώσεις και τις ρυθμίσεις του συστήματός σας", + "Managed": "Υπό διαχείριση", + "Mappings": "Αντιστοιχίσεις", + "Mass validation completed successfully": "Η μαζική επικύρωση ολοκληρώθηκε με επιτυχία", + "Mass validation failed: {error}": "Η μαζική επικύρωση απέτυχε: {error}", + "Max Files (0 = all)": "Μέγιστος αριθμός Αρχείων (0 = όλα)", + "Max Length": "Μέγιστο μήκος", + "Max Length:": "Μέγιστο μήκος:", + "Max Objects (0 = all)": "Μέγιστος αριθμός Αντικειμένων (0 = όλα)", + "Max Retries": "Μέγιστος αριθμός επαναλήψεων", + "Max Shards/Node": "Μέγιστος αριθμός Shards/Κόμβο", + "Max Tokens": "Μέγιστος αριθμός διακριτικών", + "Max execution time (ms)": "Μέγιστος χρόνος εκτέλεσης (ms)", + "Max ms": "Μέγιστα ms", + "Max result count": "Μέγιστος αριθμός αποτελεσμάτων", + "Max results": "Μέγιστα αποτελέσματα", + "Maximum": "Μέγιστο", + "Maximum File Size (MB)": "Μέγιστο μέγεθος Αρχείου (MB)", + "Maximum Nesting Depth": "Μέγιστο βάθος ένθεσης", + "Maximum length": "Μέγιστο μήκος", + "Maximum length constraint is missing": "Ο περιορισμός μέγιστου μήκους λείπει", + "Maximum length is too restrictive": "Το μέγιστο μήκος είναι υπερβολικά περιοριστικό", + "Maximum number of files to process. Set to 0 to process all files.": "Μέγιστος αριθμός Αρχείων προς επεξεργασία. Ορίστε σε 0 για επεξεργασία όλων των Αρχείων.", + "Maximum number of items": "Μέγιστος αριθμός στοιχείων", + "Maximum number of objects to process. Set to 0 to process all objects.": "Μέγιστος αριθμός Αντικειμένων προς επεξεργασία. Ορίστε σε 0 για επεξεργασία όλων των Αντικειμένων.", + "Maximum number of retry attempts for failed deliveries": "Μέγιστος αριθμός προσπαθειών επανάληψης για αποτυχημένες παραδόσεις", + "Maximum time to wait before committing changes": "Μέγιστος χρόνος αναμονής πριν από την οριστικοποίηση των αλλαγών", + "Maximum tokens to generate": "Μέγιστος αριθμός διακριτικών προς δημιουργία", + "Maximum value": "Μέγιστη τιμή", + "Maximum value constraint is missing": "Ο περιορισμός μέγιστης τιμής λείπει", + "Maximum value is too restrictive": "Η μέγιστη τιμή είναι υπερβολικά περιοριστική", + "Medium": "Μεσαίο", + "Medium Confidence": "Μεσαία εμπιστοσύνη", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Μεσαία ερωτήματα: Αναζητήσεις με κάποιο φιλτράρισμα ή πολλαπλές παραμέτρους (π.χ. εύρη ημερομηνιών, συγκεκριμένα Μητρώα/Σχήματα)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Μεσαία ερωτήματα: αναζητήσεις με κάποιο φιλτράρισμα ή πολλαπλές παραμέτρους (π.χ. εύρη ημερομηνιών, συγκεκριμένα Μητρώα/Σχήματα)", + "Member removed successfully": "Το μέλος αφαιρέθηκε με επιτυχία", + "Members": "Μέλη", + "Members ({count})": "Μέλη ({count})", + "Members of selected groups can access this view": "Τα μέλη των επιλεγμένων ομάδων μπορούν να έχουν πρόσβαση σε αυτή την προβολή", + "Members:": "Μέλη:", + "Memory prediction calculated successfully": "Η πρόβλεψη μνήμης υπολογίστηκε με επιτυχία", + "Message content is required": "Το περιεχόμενο του μηνύματος είναι υποχρεωτικό", + "Message does not belong to this conversation": "Το μήνυμα δεν ανήκει σε αυτή τη συνομιλία", + "Message not found": "Το μήνυμα δεν βρέθηκε", + "Metadata": "Μεταδεδομένα", + "Metadata Filters": "Φίλτρα μεταδεδομένων", + "Method": "Μέθοδος", + "Method*": "Μέθοδος*", + "Method: {method}": "Μέθοδος: {method}", + "Min Length": "Ελάχιστο μήκος", + "Min execution time (ms)": "Ελάχιστος χρόνος εκτέλεσης (ms)", + "Min ms": "Ελάχιστα ms", + "Min result count": "Ελάχιστος αριθμός αποτελεσμάτων", + "Min results": "Ελάχιστα αποτελέσματα", + "Minimum": "Ελάχιστο", + "Minimum length": "Ελάχιστο μήκος", + "Minimum number of items": "Ελάχιστος αριθμός στοιχείων", + "Minimum value": "Ελάχιστη τιμή", + "Minimum value constraint is missing": "Ο περιορισμός ελάχιστης τιμής λείπει", + "Minimum value is too restrictive": "Η ελάχιστη τιμή είναι υπερβολικά περιοριστική", + "Missing conversation or agentUuid": "Λείπει η συνομιλία ή το agentUuid", + "Missing conversationId": "Λείπει το conversationId", + "Missing message": "Λείπει το μήνυμα", + "Missing object fields created successfully": "Τα πεδία Αντικειμένου που έλειπαν δημιουργήθηκαν με επιτυχία", + "Mode:": "Λειτουργία:", + "Model": "Μοντέλο", + "Model Name": "Όνομα μοντέλου", + "Modified": "Τροποποιήθηκε", + "Monthly": "Μηνιαία", + "More sources (10-20):": "Περισσότερες Πηγές (10-20):", + "Most Active Objects": "Πιο ενεργά Αντικείμενα", + "Multiple Of": "Πολλαπλάσιο του", + "Multiple of": "Πολλαπλάσιο του", + "Multiple search trail deletion not implemented yet": "Η διαγραφή πολλαπλών ιχνών αναζήτησης δεν έχει υλοποιηθεί ακόμη", + "Multitenancy": "Πολυμίσθωση", + "Multitenancy settings updated successfully": "Οι ρυθμίσεις πολυμίσθωσης ενημερώθηκαν με επιτυχία", + "My Account": "Ο λογαριασμός μου", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Όνομα", + "Name *": "Όνομα *", + "Name*": "Όνομα*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Η προσωρινή μνήμη ονομάτων προθερμάνθηκε με επιτυχία: φορτώθηκαν {count} ονόματα σε {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Η προσωρινή μνήμη ονομάτων προθερμάνθηκε με επιτυχία: φορτώθηκαν {count} ονόματα σε {time}. {message}", + "Names cache warmup completed": "Η προθέρμανση της προσωρινής μνήμης ονομάτων ολοκληρώθηκε", + "Naming pattern for copies:": "Μοτίβο ονομασίας για αντίγραφα:", + "New Collection Name": "Όνομα νέας συλλογής", + "New Configuration": "Νέα διαμόρφωση", + "New Conversation": "Νέα συνομιλία", + "New Properties": "Νέες Ιδιότητες", + "New Property": "Νέα Ιδιότητα", + "New User Group": "Νέα ομάδα χρηστών", + "New Value": "Νέα τιμή", + "New activity": "Νέα δραστηριότητα", + "New password": "Νέος κωδικός πρόσβασης", + "New processing activity": "Νέα δραστηριότητα επεξεργασίας", + "New users without specific organisation membership will be automatically added to this organisation": "Οι νέοι χρήστες χωρίς συγκεκριμένη συμμετοχή σε οργανισμό θα προστίθενται αυτόματα σε αυτόν τον οργανισμό", + "New verwerkingsactiviteit": "Νέα verwerkingsactiviteit", + "Next": "Επόμενο", + "Nextcloud Groups": "Ομάδες Nextcloud", + "No": "Όχι", + "No API tokens.": "Δεν υπάρχουν διακριτικά API.", + "No ConfigSets found": "Δεν βρέθηκαν ConfigSets", + "No Deck cards linked to this object": "Δεν υπάρχουν κάρτες Deck συνδεδεμένες με αυτό το Αντικείμενο", + "No GitHub token configured": "Δεν έχει διαμορφωθεί διακριτικό GitHub", + "No GitHub token provided": "Δεν παρασχέθηκε διακριτικό GitHub", + "No GitLab token provided": "Δεν παρασχέθηκε διακριτικό GitLab", + "No Organisation": "Χωρίς οργανισμό", + "No active filters": "Δεν υπάρχουν ενεργά φίλτρα", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Δεν υπάρχουν ενεργά Αντικείμενα για προσωρινή διαγραφή για το Σχήμα {schema}. Χρησιμοποιήστε τη \"Μόνιμη διαγραφή\" για να αφαιρέσετε τα προσωρινά διαγραμμένα Αντικείμενα.", + "No activity data available": "Δεν υπάρχουν διαθέσιμα δεδομένα δραστηριότητας", + "No activity found.": "Δεν βρέθηκε δραστηριότητα.", + "No agents available": "Δεν υπάρχουν διαθέσιμοι πράκτορες", + "No agents found": "Δεν βρέθηκαν πράκτορες", + "No app store cache exists yet": "Δεν υπάρχει ακόμη προσωρινή μνήμη του app store", + "No applications are available.": "Δεν υπάρχουν διαθέσιμες εφαρμογές.", + "No applications found": "Δεν βρέθηκαν εφαρμογές", + "No approval steps for this object.": "Δεν υπάρχουν βήματα έγκρισης για αυτό το Αντικείμενο.", + "No archived conversations": "Δεν υπάρχουν αρχειοθετημένες συνομιλίες", + "No audit trail entries found": "Δεν βρέθηκαν εγγραφές ίχνους ελέγχου", + "No changes recorded": "Δεν καταγράφηκαν αλλαγές", + "No chunks to vectorize": "Δεν υπάρχουν τμήματα προς διανυσματοποίηση", + "No collections found": "Δεν βρέθηκαν συλλογές", + "No configuration data": "Δεν υπάρχουν δεδομένα διαμόρφωσης", + "No configuration selected": "Δεν έχει επιλεγεί διαμόρφωση", + "No configurations are available.": "Δεν υπάρχουν διαθέσιμες διαμορφώσεις.", + "No configurations found": "Δεν βρέθηκαν διαμορφώσεις", + "No configurations found for this application.": "Δεν βρέθηκαν διαμορφώσεις για αυτή την εφαρμογή.", + "No contacts linked to this object": "Δεν υπάρχουν επαφές συνδεδεμένες με αυτό το Αντικείμενο", + "No conversations yet. Create a new one to get started!": "Δεν υπάρχουν ακόμη συνομιλίες. Δημιουργήστε μια νέα για να ξεκινήσετε!", + "No dashboards yet": "Δεν υπάρχουν ακόμη πίνακες ελέγχου", + "No data available": "Δεν υπάρχουν διαθέσιμα δεδομένα", + "No data available for chart": "Δεν υπάρχουν διαθέσιμα δεδομένα για το γράφημα", + "No deleted items found": "Δεν βρέθηκαν διαγραμμένα στοιχεία", + "No deletion data available": "Δεν υπάρχουν διαθέσιμα δεδομένα διαγραφής", + "No description found": "Δεν βρέθηκε περιγραφή", + "No description provided": "Δεν παρασχέθηκε περιγραφή", + "No emails linked to this object": "Δεν υπάρχουν email συνδεδεμένα με αυτό το Αντικείμενο", + "No endpoints defined": "Δεν έχουν οριστεί σημεία πρόσβασης", + "No entities detected for this email.": "Δεν εντοπίστηκαν οντότητες για αυτό το email.", + "No entities detected in this file. Run text extraction first.": "Δεν εντοπίστηκαν οντότητες σε αυτό το Αρχείο. Εκτελέστε πρώτα την εξαγωγή κειμένου.", + "No entities found": "Δεν βρέθηκαν οντότητες", + "No entities have been detected yet": "Δεν έχουν εντοπιστεί ακόμη οντότητες", + "No events found": "Δεν βρέθηκαν συμβάντα", + "No events linked to this object": "Δεν υπάρχουν συμβάντα συνδεδεμένα με αυτό το Αντικείμενο", + "No expired audit trails found to clear": "Δεν βρέθηκαν ληγμένα ίχνη ελέγχου προς εκκαθάριση", + "No expired search trails found to clear": "Δεν βρέθηκαν ληγμένα ίχνη αναζήτησης προς εκκαθάριση", + "No extraction data available for this file": "Δεν υπάρχουν διαθέσιμα δεδομένα εξαγωγής για αυτό το Αρχείο", + "No facetable fields available. Select a register and schema to see available filters.": "Δεν υπάρχουν διαθέσιμα πεδία με δυνατότητα facet. Επιλέξτε ένα Μητρώο και ένα Σχήμα για να δείτε τα διαθέσιμα φίλτρα.", + "No files found": "Δεν βρέθηκαν Αρχεία", + "No files have been extracted yet": "Δεν έχουν εξαχθεί ακόμη Αρχεία", + "No files selected": "Δεν επιλέχθηκαν Αρχεία", + "No files to process": "Δεν υπάρχουν Αρχεία προς επεξεργασία", + "No files to reindex": "Δεν υπάρχουν Αρχεία προς επανευρετηρίαση", + "No filters are currently active. This will delete ALL audit trail entries!": "Δεν υπάρχουν επί του παρόντος ενεργά φίλτρα. Αυτό θα διαγράψει ΟΛΕΣ τις εγγραφές ίχνους ελέγχου!", + "No groups found. Try a different search.": "Δεν βρέθηκαν ομάδες. Δοκιμάστε διαφορετική αναζήτηση.", + "No label": "Χωρίς ετικέτα", + "No labels": "Χωρίς ετικέτες", + "No linked objects": "Δεν υπάρχουν συνδεδεμένα Αντικείμενα", + "No log entries found": "Δεν βρέθηκαν εγγραφές αρχείου καταγραφής", + "No logs are available for this configuration.": "Δεν υπάρχουν διαθέσιμα αρχεία καταγραφής για αυτή τη διαμόρφωση.", + "No logs are available for this source.": "Δεν υπάρχουν διαθέσιμα αρχεία καταγραφής για αυτή την Πηγή.", + "No logs found": "Δεν βρέθηκαν αρχεία καταγραφής", + "No matches": "Δεν υπάρχουν αντιστοιχίσεις", + "No mismatched fields found - SOLR schema is properly configured": "Δεν βρέθηκαν πεδία με αναντιστοιχία - το Σχήμα SOLR έχει διαμορφωθεί σωστά", + "No objects are currently selected for deletion.": "Δεν έχουν επιλεγεί επί του παρόντος Αντικείμενα για διαγραφή.", + "No objects are currently selected for permanent deletion.": "Δεν έχουν επιλεγεί επί του παρόντος Αντικείμενα για μόνιμη διαγραφή.", + "No objects are currently selected for restoration.": "Δεν έχουν επιλεγεί επί του παρόντος Αντικείμενα για επαναφορά.", + "No objects found": "Δεν βρέθηκαν Αντικείμενα", + "No objects found to vectorize": "Δεν βρέθηκαν Αντικείμενα προς διανυσματοποίηση", + "No objects linked to this email": "Δεν υπάρχουν Αντικείμενα συνδεδεμένα με αυτό το email", + "No objects selected": "Δεν επιλέχθηκαν Αντικείμενα", + "No objects selected for deletion": "Δεν επιλέχθηκαν Αντικείμενα για διαγραφή", + "No objects selected for restoration": "Δεν επιλέχθηκαν Αντικείμενα για επαναφορά", + "No objects to delete": "Δεν υπάρχουν Αντικείμενα προς διαγραφή", + "No objects to delete for schema {schema}": "Δεν υπάρχουν Αντικείμενα προς διαγραφή για το Σχήμα {schema}", + "No objects were permanently deleted": "Δεν διαγράφηκαν μόνιμα Αντικείμενα", + "No organisations found": "Δεν βρέθηκαν οργανισμοί", + "No personal data was found for this subject identifier.": "Δεν βρέθηκαν προσωπικά δεδομένα για αυτό το αναγνωριστικό υποκειμένου.", + "No processing activities yet": "Δεν υπάρχουν ακόμη δραστηριότητες επεξεργασίας", + "No properties found": "Δεν βρέθηκαν Ιδιότητες", + "No properties match your filters.": "Καμία Ιδιότητα δεν ταιριάζει με τα φίλτρα σας.", + "No purge date set": "Δεν έχει οριστεί ημερομηνία εκκαθάρισης", + "No register data available": "Δεν υπάρχουν διαθέσιμα δεδομένα Μητρώου", + "No register objects reference this file": "Κανένα Αντικείμενο Μητρώου δεν αναφέρεται σε αυτό το Αρχείο", + "No registers found": "Δεν βρέθηκαν Μητρώα", + "No registers found for this application.": "Δεν βρέθηκαν Μητρώα για αυτή την εφαρμογή.", + "No registers found. Create a register to configure permissions.": "Δεν βρέθηκαν Μητρώα. Δημιουργήστε ένα Μητρώο για να διαμορφώσετε δικαιώματα.", + "No related cases found for this sender": "Δεν βρέθηκαν σχετικές υποθέσεις για αυτόν τον αποστολέα", + "No related entities": "Δεν υπάρχουν σχετικές οντότητες", + "No relations found": "Δεν βρέθηκαν σχέσεις", + "No request body available": "Δεν υπάρχει διαθέσιμο σώμα αιτήματος", + "No response body available": "Δεν υπάρχει διαθέσιμο σώμα απόκρισης", + "No saved views yet. Create one in the Search tab!": "Δεν υπάρχουν ακόμη αποθηκευμένες προβολές. Δημιουργήστε μία στην καρτέλα Αναζήτηση!", + "No saved views yet. create one in the search tab!": "Δεν υπάρχουν ακόμη αποθηκευμένες προβολές. δημιουργήστε μία στην καρτέλα αναζήτησης!", + "No scan run yet": "Δεν έχει εκτελεστεί ακόμη σάρωση", + "No schema data available": "Δεν υπάρχουν διαθέσιμα δεδομένα Σχήματος", + "No schema selected for exploration": "Δεν έχει επιλεγεί Σχήμα για εξερεύνηση", + "No schemas configured for mail linking.": "Δεν έχουν διαμορφωθεί Σχήματα για σύνδεση mail.", + "No schemas found": "Δεν βρέθηκαν Σχήματα", + "No schemas found for this application.": "Δεν βρέθηκαν Σχήματα για αυτή την εφαρμογή.", + "No search terms data available": "Δεν υπάρχουν διαθέσιμα δεδομένα όρων αναζήτησης", + "No search trail entries found": "Δεν βρέθηκαν εγγραφές ίχνους αναζήτησης", + "No sources are available.": "Δεν υπάρχουν διαθέσιμες Πηγές.", + "No sources found": "Δεν βρέθηκαν Πηγές", + "No synchronizations found": "Δεν βρέθηκαν συγχρονισμοί", + "No templates found": "Δεν βρέθηκαν πρότυπα", + "No templates have been created yet": "Δεν έχουν δημιουργηθεί ακόμη πρότυπα", + "No tools available. Tools can be registered by installed apps.": "Δεν υπάρχουν διαθέσιμα εργαλεία. Τα εργαλεία μπορούν να καταχωρηθούν από εγκατεστημένες εφαρμογές.", + "No users in this organisation.": "Δεν υπάρχουν χρήστες σε αυτόν τον οργανισμό.", + "No valid magic-mapped register+schema combinations found": "Δεν βρέθηκαν έγκυροι συνδυασμοί magic-mapped Μητρώου+Σχήματος", + "No views found. Create views first before configuring vectorization.": "Δεν βρέθηκαν προβολές. Δημιουργήστε πρώτα προβολές πριν διαμορφώσετε τη διανυσματοποίηση.", + "No views match your search": "Καμία προβολή δεν ταιριάζει με την αναζήτησή σας", + "No webhooks found": "Δεν βρέθηκαν webhooks", + "No webhooks have been configured yet": "Δεν έχουν διαμορφωθεί ακόμη webhooks", + "No workflows found in this project. Create workflows in the n8n editor.": "Δεν βρέθηκαν ροές εργασίας σε αυτό το έργο. Δημιουργήστε ροές εργασίας στον επεξεργαστή n8n.", + "None": "Κανένα", + "Not Found": "Δεν βρέθηκε", + "Not extracted": "Δεν εξήχθη", + "Not helpful": "Μη χρήσιμο", + "Not in use": "Δεν χρησιμοποιείται", + "Note:": "Σημείωση:", + "Notifications": "Ειδοποιήσεις", + "Number Constraints": "Περιορισμοί αριθμού", + "Number of Sources": "Αριθμός Πηγών", + "Number of chunks to vectorize in one API call (1-100).": "Αριθμός τμημάτων προς διανυσματοποίηση σε μία κλήση API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Αριθμός τμημάτων προς διανυσματοποίηση σε μία κλήση API. Υψηλότερος = ταχύτερος αλλά περισσότερη μνήμη. Συνιστάται: 10-50.", + "Number of objects to process in each batch (1-5000).": "Αριθμός Αντικειμένων προς επεξεργασία σε κάθε παρτίδα (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Αριθμός Αντικειμένων προς διανυσματοποίηση σε μία κλήση API. Υψηλότερος = ταχύτερος αλλά περισσότερη μνήμη. Συνιστάται: 10-50.", + "Numeric Range:": "Αριθμητικό εύρος:", + "Object": "Αντικείμενο", + "Object #{id}": "Αντικείμενο #{id}", + "Object Collection": "Συλλογή Αντικειμένων", + "Object Configuration:": "Διαμόρφωση Αντικειμένου:", + "Object Count Prediction": "Πρόβλεψη αριθμού Αντικειμένων", + "Object Extraction Mode": "Λειτουργία εξαγωγής Αντικειμένων", + "Object ID": "ID Αντικειμένου", + "Object Management": "Διαχείριση Αντικειμένων", + "Object Text Extraction": "Εξαγωγή κειμένου Αντικειμένου", + "Object Vectorization": "Διανυσματοποίηση Αντικειμένου", + "Object Warmup": "Προθέρμανση Αντικειμένου", + "Object changes in owned objects": "Αλλαγές Αντικειμένου σε Αντικείμενα ιδιοκτησίας σας", + "Object collection not configured": "Η συλλογή Αντικειμένων δεν έχει διαμορφωθεί", + "Object metadata incomplete for file upload": "Τα μεταδεδομένα Αντικειμένου είναι ελλιπή για τη μεταφόρτωση Αρχείου", + "Object not found": "Το Αντικείμενο δεν βρέθηκε", + "Object permanently deleted": "Το Αντικείμενο διαγράφηκε μόνιμα", + "Object restored successfully": "Το Αντικείμενο επαναφέρθηκε με επιτυχία", + "Object settings updated successfully": "Οι ρυθμίσεις Αντικειμένου ενημερώθηκαν με επιτυχία", + "Object sources": "Πηγές Αντικειμένου", + "Object successfully deleted": "Το Αντικείμενο διαγράφηκε με επιτυχία", + "Object successfully modified": "Το Αντικείμενο τροποποιήθηκε με επιτυχία", + "Object unlocked successfully": "Το Αντικείμενο ξεκλειδώθηκε με επιτυχία", + "Object vectorization configuration saved successfully": "Η διαμόρφωση διανυσματοποίησης Αντικειμένου αποθηκεύτηκε με επιτυχία", + "Object vectorized successfully": "Το Αντικείμενο διανυσματοποιήθηκε με επιτυχία", + "Object:": "Αντικείμενο:", + "Objects": "Αντικείμενα", + "Objects Analyzed": "Αναλυμένα Αντικείμενα", + "Objects Distribution": "Κατανομή Αντικειμένων", + "Objects being analyzed": "Αντικείμενα υπό ανάλυση", + "Objects by Register": "Αντικείμενα ανά Μητρώο", + "Objects by Schema": "Αντικείμενα ανά Σχήμα", + "Objects deletion completed for {schema}": "Η διαγραφή Αντικειμένων ολοκληρώθηκε για το {schema}", + "Objects deletion completed successfully": "Η διαγραφή Αντικειμένων ολοκληρώθηκε με επιτυχία", + "Objects in schema": "Αντικείμενα στο Σχήμα", + "Objects successfully deleted": "Τα Αντικείμενα διαγράφηκαν με επιτυχία", + "Objects to Process:": "Αντικείμενα προς επεξεργασία:", + "Objects to be analyzed": "Αντικείμενα προς ανάλυση", + "Objects to be deleted": "Αντικείμενα προς διαγραφή", + "Objects to be validated": "Αντικείμενα προς επικύρωση", + "Objects will be serialized as JSON text before vectorization": "Τα Αντικείμενα θα σειριοποιηθούν ως κείμενο JSON πριν από τη διανυσματοποίηση", + "Objects will be soft deleted and moved to the": "Τα Αντικείμενα θα διαγραφούν προσωρινά και θα μεταφερθούν στο", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Τα Αντικείμενα θα διαγραφούν προσωρινά (επισημαίνονται ως διαγραμμένα αλλά διατηρούνται στη βάση δεδομένων). Μπορούν να ανακτηθούν αργότερα εάν χρειαστεί.", + "Old Value": "Παλαιά τιμή", + "Oldest Item (days)": "Παλαιότερο στοιχείο (ημέρες)", + "Ollama Chat Settings": "Ρυθμίσεις συνομιλίας Ollama", + "Ollama Embedding Configuration": "Διαμόρφωση ενσωμάτωσης Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Μόνο με αλλαγές", + "Only members of selected groups can access this organisation": "Μόνο τα μέλη των επιλεγμένων ομάδων μπορούν να έχουν πρόσβαση σε αυτόν τον οργανισμό", + "Open Nextcloud App Store": "Άνοιγμα Nextcloud App Store", + "Open OpenRegister sidebar": "Άνοιγμα πλευρικής μπάρας OpenRegister", + "Open Register Objects": "Άνοιγμα Αντικειμένων Μητρώου", + "Open folder": "Άνοιγμα φακέλου", + "Open in OpenRegister": "Άνοιγμα στο OpenRegister", + "Open n8n Editor": "Άνοιγμα επεξεργαστή n8n", + "OpenAI Chat Settings": "Ρυθμίσεις συνομιλίας OpenAI", + "OpenAI Embedding Configuration": "Διαμόρφωση ενσωμάτωσης OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Ρυθμίσεις OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Πίνακες ελέγχου οριζόμενοι από τον χειριστή και προγραμματισμένες αναφορές. Κάθε πίνακας ελέγχου είναι ένα πρωταρχικό Αντικείμενο στο Μητρώο `reports`· τα γραφικά στοιχεία δηλώνονται στον πίνακα `widgets` του πίνακα ελέγχου και αποδίδονται ζωντανά από συγκεντρώσεις / GraphQL.", + "Optimizing search performance...": "Βελτιστοποίηση απόδοσης αναζήτησης...", + "Optional URL-friendly identifier": "Προαιρετικό αναγνωριστικό φιλικό προς URL", + "Optional description for this facet": "Προαιρετική περιγραφή για αυτό το facet", + "Optional end date property": "Προαιρετική Ιδιότητα ημερομηνίας λήξης", + "Optional event description template": "Προαιρετικό πρότυπο περιγραφής συμβάντος", + "Optional location property": "Προαιρετική Ιδιότητα τοποθεσίας", + "Optional webhook secret for signature verification": "Προαιρετικό μυστικό webhook για επαλήθευση υπογραφής", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Προαιρετικό: Προσθέστε παραμέτρους ερωτήματος για να φιλτράρετε το αναφερόμενο Σχήμα (π.χ. status=active&type=public)", + "Or": "Ή", + "Order": "Σειρά", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Ταξινομημένες ετικέτες γλώσσας BCP 47. Η πρώτη γλώσσα είναι η προεπιλογή του Μητρώου και καθορίζει την εναλλακτική Accept-Language για μεταφράσιμες ιδιότητες.", + "Organisation": "Οργανισμός", + "Organisation Configuration": "Διαμόρφωση οργανισμού", + "Organisation ID": "Αναγνωριστικό οργανισμού", + "Organisation Members": "Μέλη οργανισμού", + "Organisation Statistics": "Στατιστικά οργανισμού", + "Organisation created successfully": "Ο οργανισμός δημιουργήθηκε με επιτυχία", + "Organisation membership changes": "Αλλαγές ιδιότητας μέλους οργανισμού", + "Organisation name is required": "Το όνομα οργανισμού είναι υποχρεωτικό", + "Organisation settings saved successfully": "Οι ρυθμίσεις οργανισμού αποθηκεύτηκαν με επιτυχία", + "Organisation successfully created": "Ο οργανισμός δημιουργήθηκε με επιτυχία", + "Organisation successfully updated": "Ο οργανισμός ενημερώθηκε με επιτυχία", + "Organisational measures": "Οργανωτικά μέτρα", + "Organisations": "Οργανισμοί", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Οργανισμός", + "Organization ID (Optional)": "Αναγνωριστικό οργανισμού (Προαιρετικό)", + "Organizations": "Οργανισμοί", + "Orphaned Items": "Ορφανά στοιχεία", + "Other": "Άλλο", + "Output Mapping": "Αντιστοίχιση εξόδου", + "Overview": "Επισκόπηση", + "Owner": "Κάτοχος", + "Owner:": "Κάτοχος:", + "PDF": "PDF", + "PII hits": "Εντοπισμοί PII", + "PII rows": "Γραμμές PII", + "Page {current} of {total}": "Σελίδα {current} από {total}", + "Parallel Mode (Faster, more resource intensive)": "Παράλληλη λειτουργία (Ταχύτερη, με μεγαλύτερη κατανάλωση πόρων)", + "Parallel:": "Παράλληλα:", + "Password": "Κωδικός πρόσβασης", + "Password changes are not supported by your authentication provider.": "Οι αλλαγές κωδικού πρόσβασης δεν υποστηρίζονται από τον πάροχο ταυτοποίησής σας.", + "Password for SOLR authentication (optional)": "Κωδικός πρόσβασης για ταυτοποίηση SOLR (προαιρετικό)", + "Password updated successfully": "Ο κωδικός πρόσβασης ενημερώθηκε με επιτυχία", + "Path": "Διαδρομή", + "Path in repository": "Διαδρομή στο αποθετήριο", + "Path where the configuration file will be saved in the repository": "Διαδρομή όπου θα αποθηκευτεί το αρχείο διαμόρφωσης στο αποθετήριο", + "Path where the register OAS file will be saved in the repository": "Διαδρομή όπου θα αποθηκευτεί το αρχείο OAS του Μητρώου στο αποθετήριο", + "Pattern (regex)": "Μοτίβο (regex)", + "Pattern Issue": "Ζήτημα μοτίβου", + "Pattern constraint is missing": "Ο περιορισμός μοτίβου λείπει", + "Patterns:": "Μοτίβα:", + "Pending": "Σε εκκρεμότητα", + "Permanent Delete Mode": "Λειτουργία οριστικής διαγραφής", + "Permanently Delete": "Οριστική διαγραφή", + "Permanently Delete ({count})": "Οριστική διαγραφή ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Οριστική διαγραφή και των {count} προσωρινά διαγραμμένων Αντικειμένων. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!", + "Permanently delete already soft-deleted objects": "Οριστική διαγραφή ήδη προσωρινά διαγραμμένων Αντικειμένων", + "Permanently delete this conversation?": "Οριστική διαγραφή αυτής της συνομιλίας;", + "Permission Matrix": "Πίνακας δικαιωμάτων", + "Person": "Πρόσωπο", + "Personal Data": "Προσωπικά δεδομένα", + "Personal Data Export": "Εξαγωγή προσωπικών δεδομένων", + "Persons": "Πρόσωπα", + "Phone": "Τηλέφωνο", + "Phone Numbers": "Αριθμοί τηλεφώνου", + "Photos": "Φωτογραφίες", + "Please create an agent in the": "Δημιουργήστε έναν πράκτορα στο", + "Please select a user": "Επιλέξτε έναν χρήστη", + "Please select an agent to continue": "Επιλέξτε έναν πράκτορα για να συνεχίσετε", + "Please select an organisation": "Επιλέξτε έναν οργανισμό", + "Please select at least one item to import": "Επιλέξτε τουλάχιστον ένα στοιχείο για εισαγωγή", + "Please select registers or schemas first": "Επιλέξτε πρώτα Μητρώα ή Σχήματα", + "Please select which register and schema to use for the new object": "Επιλέξτε ποιο Μητρώο και Σχήμα θα χρησιμοποιηθούν για το νέο Αντικείμενο", + "Please try again later.": "Δοκιμάστε ξανά αργότερα.", + "Please wait while we fetch your agents.": "Περιμένετε όσο ανακτούμε τους πράκτορές σας.", + "Please wait while we fetch your applications.": "Περιμένετε όσο ανακτούμε τις εφαρμογές σας.", + "Please wait while we fetch your configurations.": "Περιμένετε όσο ανακτούμε τις διαμορφώσεις σας.", + "Please wait while we fetch your deleted items.": "Περιμένετε όσο ανακτούμε τα διαγραμμένα στοιχεία σας.", + "Please wait while we fetch your sources.": "Περιμένετε όσο ανακτούμε τις Πηγές σας.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Περιμένετε όσο διαγράφουμε οριστικά τη συλλογή SOLR. Αυτό μπορεί να διαρκέσει μερικές στιγμές.", + "Polls": "Δημοσκοπήσεις", + "Popular Search Terms": "Δημοφιλείς όροι αναζήτησης", + "Port": "Θύρα", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Οι προτιμήσεις αποθηκεύτηκαν", + "Prerequisites": "Προαπαιτούμενα", + "Press Enter to send, Shift+Enter for new line": "Πατήστε Enter για αποστολή, Shift+Enter για νέα γραμμή", + "Press enter to send, shift+enter for new line": "Πατήστε enter για αποστολή, shift+enter για νέα γραμμή", + "Prettify": "Μορφοποίηση", + "Preview erasure": "Προεπισκόπηση διαγραφής", + "Preview:": "Προεπισκόπηση:", + "Previous": "Προηγούμενο", + "Private": "Ιδιωτικό", + "Private Agent (Default)": "Ιδιωτικός πράκτορας (Προεπιλογή)", + "Private agents": "Ιδιωτικοί πράκτορες", + "Process Name (optional)": "Όνομα διεργασίας (προαιρετικό)", + "Processed {indexed} files, {failed} failed": "Επεξεργάστηκαν {indexed} Αρχεία, {failed} απέτυχαν", + "Processes chunks in batches with simulated parallelism.": "Επεξεργάζεται τμήματα σε παρτίδες με προσομοιωμένη παραλληλία.", + "Processes file chunks sequentially (safest).": "Επεξεργάζεται τμήματα Αρχείων διαδοχικά (ασφαλέστερο).", + "Processes objects in chunks with simulated parallelism.": "Επεξεργάζεται Αντικείμενα σε τμήματα με προσομοιωμένη παραλληλία.", + "Processes objects sequentially (safest).": "Επεξεργάζεται Αντικείμενα διαδοχικά (ασφαλέστερο).", + "Processing": "Επεξεργασία", + "Processing Limits": "Όρια επεξεργασίας", + "Processing...": "Επεξεργασία...", + "Project initialized successfully": "Το έργο αρχικοποιήθηκε με επιτυχία", + "Project not found. Please initialize first.": "Το έργο δεν βρέθηκε. Αρχικοποιήστε το πρώτα.", + "Properties": "Ιδιότητες", + "Property Behaviors": "Συμπεριφορές Ιδιότητας", + "Property Configuration:": "Διαμόρφωση Ιδιότητας:", + "Property Filters": "Φίλτρα Ιδιότητας", + "Property Title": "Τίτλος Ιδιότητας", + "Property Type": "Τύπος Ιδιότητας", + "Property can be improved": "Η Ιδιότητα μπορεί να βελτιωθεί", + "Property could not be added": "Η Ιδιότητα δεν ήταν δυνατό να προστεθεί", + "Property could not be updated": "Η Ιδιότητα δεν ήταν δυνατό να ενημερωθεί", + "Property name of inversed relation": "Όνομα Ιδιότητας της αντεστραμμένης σχέσης", + "Property successfully added": "Η Ιδιότητα προστέθηκε με επιτυχία", + "Property successfully updated": "Η Ιδιότητα ενημερώθηκε με επιτυχία", + "Protected": "Προστατευμένο", + "Provider": "Πάροχος", + "Provider is required for testing": "Ο πάροχος είναι υποχρεωτικός για τη δοκιμή", + "Public": "Δημόσιο", + "Public View": "Δημόσια προβολή", + "Public views can be accessed by anyone in the system": "Οι δημόσιες προβολές είναι προσβάσιμες από οποιονδήποτε στο σύστημα", + "Publish": "Δημοσίευση", + "Publish Configuration to GitHub": "Δημοσίευση διαμόρφωσης στο GitHub", + "Publish OAS": "Δημοσίευση OAS", + "Publish Objects": "Δημοσίευση Αντικειμένων", + "Publish Register OAS to GitHub": "Δημοσίευση OAS Μητρώου στο GitHub", + "Publish all objects in this schema": "Δημοσίευση όλων των Αντικειμένων σε αυτό το Σχήμα", + "Publish objects to make them publicly available": "Δημοσιεύστε Αντικείμενα για να τα καταστήσετε δημόσια διαθέσιμα", + "Published": "Δημοσιευμένο", + "Published:": "Δημοσιεύτηκε:", + "Publishing...": "Δημοσίευση...", + "Purge": "Εκκαθάριση", + "Purge Date": "Ημερομηνία εκκαθάρισης", + "Purge {count} object from database": "Εκκαθάριση {count} Αντικειμένου από τη βάση δεδομένων", + "Purge {count} objects from database": "Εκκαθάριση {count} Αντικειμένων από τη βάση δεδομένων", + "Purpose limitation *": "Περιορισμός σκοπού *", + "Push Notifications": "Ειδοποιήσεις push", + "Query Complexity Distribution": "Κατανομή πολυπλοκότητας ερωτημάτων", + "Query Help": "Βοήθεια ερωτημάτων", + "Query parameter is required": "Η παράμετρος ερωτήματος είναι υποχρεωτική", + "Quota": "Όριο", + "RAG": "RAG", + "RAG Configuration": "Διαμόρφωση RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Οι ρυθμίσεις RBAC ενημερώθηκαν με επιτυχία", + "Rate limits cleared successfully": "Τα όρια ρυθμού εκκαθαρίστηκαν με επιτυχία", + "Raw Changes Data": "Ακατέργαστα δεδομένα αλλαγών", + "Re-vectorize on object update": "Επαναδιανυσματοποίηση κατά την ενημέρωση Αντικειμένου", + "Read": "Ανάγνωση", + "Real-time push notification status via notify_push": "Κατάσταση ειδοποιήσεων push σε πραγματικό χρόνο μέσω notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Οι ειδοποιήσεις push σε πραγματικό χρόνο είναι ενεργές. Οι συνδεδεμένοι πελάτες λαμβάνουν άμεσες ενημερώσεις όταν δημιουργούνται, ενημερώνονται ή διαγράφονται Αντικείμενα.", + "Realtime push active": "Push πραγματικού χρόνου ενεργό", + "Realtime push not available — the notify_push app is not installed": "Το push πραγματικού χρόνου δεν είναι διαθέσιμο — η εφαρμογή notify_push δεν είναι εγκατεστημένη", + "Reason": "Αιτία", + "Reason (optional)": "Αιτία (προαιρετικό)", + "Rebase operation completed successfully": "Η λειτουργία rebase ολοκληρώθηκε με επιτυχία", + "Rebase operation failed: {error}": "Η λειτουργία rebase απέτυχε: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Συστάσεις:", + "Recommended Type:": "Συνιστώμενος τύπος:", + "Recommended: 5 sources": "Συνιστάται: 5 Πηγές", + "Refresh": "Ανανέωση", + "Refresh Data": "Ανανέωση δεδομένων", + "Refresh Stats": "Ανανέωση στατιστικών", + "Refresh Workflows": "Ανανέωση ροών εργασίας", + "Refresh dashboard": "Ανανέωση πίνακα ελέγχου", + "Refresh database info": "Ανανέωση πληροφοριών βάσης δεδομένων", + "Register": "Μητρώο", + "Register #{id}": "Μητρώο #{id}", + "Register / Schema": "Μητρώο / Σχήμα", + "Register ID": "Αναγνωριστικό Μητρώου", + "Register Objects": "Αντικείμενα Μητρώου", + "Register Overview": "Επισκόπηση Μητρώου", + "Register Statistics": "Στατιστικά Μητρώου", + "Register Totals": "Σύνολα Μητρώου", + "Register and schema are required": "Το Μητρώο και το Σχήμα είναι υποχρεωτικά", + "Register annotation": "Σχολιασμός Μητρώου", + "Register depublished successfully": "Η αποδημοσίευση του Μητρώου ολοκληρώθηκε με επιτυχία", + "Register not found": "Το Μητρώο δεν βρέθηκε", + "Register objects": "Αντικείμενα Μητρώου", + "Register objects deletion completed successfully": "Η διαγραφή των Αντικειμένων του Μητρώου ολοκληρώθηκε με επιτυχία", + "Register published successfully": "Το Μητρώο δημοσιεύτηκε με επιτυχία", + "Register/Schema Usage": "Χρήση Μητρώου/Σχήματος", + "Register:": "Μητρώο:", + "Registers": "Μητρώα", + "Reindex": "Επαναευρετηρίαση", + "Reindex completed": "Η επαναευρετηρίαση ολοκληρώθηκε", + "Reindex completed successfully": "Η επαναευρετηρίαση ολοκληρώθηκε με επιτυχία", + "Reindex completed! Processed {count} objects in {duration}s": "Η επαναευρετηρίαση ολοκληρώθηκε! Επεξεργάστηκαν {count} Αντικείμενα σε {duration}δ", + "Reindex failed": "Η επαναευρετηρίαση απέτυχε", + "Reindex failed: %s": "Η επαναευρετηρίαση απέτυχε: %s", + "Reject": "Απόρριψη", + "Rejected": "Απορρίφθηκε", + "Related Cases": "Σχετικές υποθέσεις", + "Relations": "Σχέσεις", + "Remote Version": "Απομακρυσμένη έκδοση", + "Remove": "Αφαίρεση", + "Remove Deck card": "Αφαίρεση κάρτας Deck", + "Remove avatar": "Αφαίρεση άβαταρ", + "Remove contact": "Αφαίρεση επαφής", + "Remove filter": "Αφαίρεση φίλτρου", + "Remove from favorites": "Αφαίρεση από τα αγαπημένα", + "Remove from list": "Αφαίρεση από τη λίστα", + "Remove group": "Αφαίρεση ομάδας", + "Remove link": "Αφαίρεση συνδέσμου", + "Remove link to {name}": "Αφαίρεση συνδέσμου προς {name}", + "Remove link to {name}?": "Αφαίρεση συνδέσμου προς {name};", + "Remove link to {title}": "Αφαίρεση συνδέσμου προς {title}", + "Remove oneOf entry": "Αφαίρεση καταχώρισης oneOf", + "Remove property after write-back": "Αφαίρεση Ιδιότητας μετά την εγγραφή πίσω", + "Remove {title}": "Αφαίρεση {title}", + "Remove {userId} from this organisation?": "Αφαίρεση του {userId} από αυτόν τον οργανισμό;", + "Removed": "Αφαιρέθηκε", + "Removed from favorites": "Αφαιρέθηκε από τα αγαπημένα", + "Removing schema {schema}...": "Αφαίρεση Σχήματος {schema}...", + "Rename Conversation": "Μετονομασία συνομιλίας", + "Rename conversation": "Μετονομασία συνομιλίας", + "Replicas": "Αντίγραφα", + "Reports": "Αναφορές", + "Repository": "Αποθετήριο", + "Request Body": "Σώμα αιτήματος", + "Request Data": "Δεδομένα αιτήματος", + "Request Details": "Λεπτομέρειες αιτήματος", + "Request Quota (per day)": "Όριο αιτημάτων (ανά ημέρα)", + "Request account deactivation": "Αίτημα απενεργοποίησης λογαριασμού", + "Request account deactivation. This will notify administrators for review.": "Αίτημα απενεργοποίησης λογαριασμού. Αυτό θα ειδοποιήσει τους διαχειριστές για έλεγχο.", + "Request body must be JSON with a \"query\" field": "Το σώμα αιτήματος πρέπει να είναι JSON με ένα πεδίο \"query\"", + "Request timeout in seconds": "Χρονικό όριο αιτήματος σε δευτερόλεπτα", + "Requested at": "Αιτήθηκε στις", + "Required": "Υποχρεωτικό", + "Required Token Scopes": "Απαιτούμενα πεδία εφαρμογής διακριτικού", + "Required field": "Υποχρεωτικό πεδίο", + "Required status is inconsistent": "Η κατάσταση υποχρεωτικότητας είναι ασυνεπής", + "Requires Dolphin AI for OCR text extraction": "Απαιτεί Dolphin AI για εξαγωγή κειμένου OCR", + "Rerun Search": "Επανεκτέλεση αναζήτησης", + "Rerunning search: {searchTerm}": "Επανεκτέλεση αναζήτησης: {searchTerm}", + "Reset Changes": "Επαναφορά αλλαγών", + "Reset Filters": "Επαναφορά φίλτρων", + "Resource Quotas": "Όρια πόρων", + "Response Body": "Σώμα απόκρισης", + "Response Details": "Λεπτομέρειες απόκρισης", + "Response Time:": "Χρόνος απόκρισης:", + "Restore": "Επαναφορά", + "Restore conversation": "Επαναφορά συνομιλίας", + "Restore or permanently delete items": "Επαναφορά ή οριστική διαγραφή στοιχείων", + "Restore {count} object": "Επαναφορά {count} Αντικειμένου", + "Restore {count} objects": "Επαναφορά {count} Αντικειμένων", + "Result Count Range": "Εύρος πλήθους αποτελεσμάτων", + "Results": "Αποτελέσματα", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Περίοδος διατήρησης (διάρκεια ISO-8601, π.χ. P10Y, P30D)", + "Retention settings updated successfully": "Οι ρυθμίσεις διατήρησης ενημερώθηκαν με επιτυχία", + "Retrieve all objects for this schema": "Ανάκτηση όλων των Αντικειμένων για αυτό το Σχήμα", + "Retry": "Επανάληψη", + "Retry Failed Extractions": "Επανάληψη αποτυχημένων εξαγωγών", + "Retry Policy": "Πολιτική επανάληψης", + "Retry completed": "Η επανάληψη ολοκληρώθηκε", + "Retry upload": "Επανάληψη μεταφόρτωσης", + "Retrying...": "Επανάληψη...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Ελέγξτε τα επιλεγμένα Αντικείμενα παρακάτω. Μπορείτε να αφαιρέσετε όποια Αντικείμενα δεν θέλετε να διαγράψετε κάνοντας κλικ στο κουμπί αφαίρεσης.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Ελέγξτε τα επιλεγμένα Αντικείμενα παρακάτω. Μπορείτε να αφαιρέσετε όποια Αντικείμενα δεν θέλετε να συμπεριλάβετε.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Ελέγξτε τα επιλεγμένα Αντικείμενα παρακάτω. Μπορείτε να αφαιρέσετε όποια Αντικείμενα δεν θέλετε να διαγράψετε οριστικά κάνοντας κλικ στο κουμπί αφαίρεσης. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Ελέγξτε τα επιλεγμένα Αντικείμενα παρακάτω. Μπορείτε να αφαιρέσετε όποια Αντικείμενα δεν θέλετε να επαναφέρετε κάνοντας κλικ στο κουμπί αφαίρεσης. Τα Αντικείμενα θα επαναφερθούν στην αρχική τους θέση.", + "Revoke": "Ανάκληση", + "Right": "Δικαίωμα", + "Risk Level": "Επίπεδο κινδύνου", + "Risk level": "Επίπεδο κινδύνου", + "Roles and Permissions": "Ρόλοι και δικαιώματα", + "Roles updated successfully": "Οι ρόλοι ενημερώθηκαν με επιτυχία", + "Run compliance scan": "Εκτέλεση σάρωσης συμμόρφωσης", + "SOLR Connection Settings": "Ρυθμίσεις σύνδεσης SOLR", + "SOLR Index Warmup": "Προθέρμανση ευρετηρίου SOLR", + "SOLR Version:": "Έκδοση SOLR:", + "SOLR actions menu": "Μενού ενεργειών SOLR", + "SOLR base path (usually /solr)": "Βασική διαδρομή SOLR (συνήθως /solr)", + "SOLR collection deleted successfully": "Η συλλογή SOLR διαγράφηκε με επιτυχία", + "SOLR connection test failed: {error}": "Η δοκιμή σύνδεσης SOLR απέτυχε: {error}", + "SOLR connection test successful!": "Η δοκιμή σύνδεσης SOLR ήταν επιτυχής!", + "SOLR index warmup completed successfully": "Η προθέρμανση ευρετηρίου SOLR ολοκληρώθηκε με επιτυχία", + "SOLR is disabled": "Το SOLR είναι απενεργοποιημένο", + "SOLR is not available or not configured": "Το SOLR δεν είναι διαθέσιμο ή δεν έχει διαμορφωθεί", + "SOLR logging disabled": "Η καταγραφή SOLR απενεργοποιήθηκε", + "SOLR logging enabled": "Η καταγραφή SOLR ενεργοποιήθηκε", + "SOLR search disabled": "Η αναζήτηση SOLR απενεργοποιήθηκε", + "SOLR search enabled": "Η αναζήτηση SOLR ενεργοποιήθηκε", + "SOLR server hostname or IP address": "Όνομα κεντρικού υπολογιστή ή διεύθυνση IP του διακομιστή SOLR", + "SOLR server port number (optional, defaults to 8983)": "Αριθμός θύρας διακομιστή SOLR (προαιρετικό, προεπιλογή 8983)", + "SOLR settings updated successfully": "Οι ρυθμίσεις SOLR ενημερώθηκαν με επιτυχία", + "SOLR setup completed successfully": "Η εγκατάσταση SOLR ολοκληρώθηκε με επιτυχία", + "SOLR setup completed successfully!": "Η εγκατάσταση SOLR ολοκληρώθηκε με επιτυχία!", + "SOLR setup error: %s": "Σφάλμα εγκατάστασης SOLR: %s", + "SOLR setup failed": "Η εγκατάσταση SOLR απέτυχε", + "SOLR setup failed - check logs": "Η εγκατάσταση SOLR απέτυχε - ελέγξτε τα αρχεία καταγραφής", + "SOLR setup failed. Please check the configuration and try again.": "Η εγκατάσταση SOLR απέτυχε. Ελέγξτε τη διαμόρφωση και δοκιμάστε ξανά.", + "SOLR setup failed: {error}": "Η εγκατάσταση SOLR απέτυχε: {error}", + "SOLR warmup failed: {error}": "Η προθέρμανση SOLR απέτυχε: {error}", + "Sample Values:": "Δείγμα τιμών:", + "Save": "Αποθήκευση", + "Save Configuration": "Αποθήκευση διαμόρφωσης", + "Save Connection Settings": "Αποθήκευση ρυθμίσεων σύνδεσης", + "Save Roles": "Αποθήκευση ρόλων", + "Save Settings": "Αποθήκευση ρυθμίσεων", + "Save changes": "Αποθήκευση αλλαγών", + "Save current search as view": "Αποθήκευση τρέχουσας αναζήτησης ως προβολή", + "Save labels": "Αποθήκευση ετικετών", + "Save tags for {name}": "Αποθήκευση ετικετών για {name}", + "Save the organisation first to manage users.": "Αποθηκεύστε πρώτα τον οργανισμό για να διαχειριστείτε χρήστες.", + "Saved Views": "Αποθηκευμένες προβολές", + "Saving...": "Αποθήκευση...", + "Scheduled": "Προγραμματισμένο", + "Schema": "Σχήμα", + "Schema ID": "Αναγνωριστικό Σχήματος", + "Schema Name": "Όνομα Σχήματος", + "Schema Statistics": "Στατιστικά Σχήματος", + "Schema annotation": "Σχολιασμός Σχήματος", + "Schema configuration updated successfully for {schema}": "Η διαμόρφωση Σχήματος ενημερώθηκε με επιτυχία για {schema}", + "Schema depublished successfully": "Η αποδημοσίευση του Σχήματος ολοκληρώθηκε με επιτυχία", + "Schema objects deletion completed successfully": "Η διαγραφή των Αντικειμένων του Σχήματος ολοκληρώθηκε με επιτυχία", + "Schema published successfully": "Το Σχήμα δημοσιεύτηκε με επιτυχία", + "Schema reference ($ref)": "Αναφορά Σχήματος ($ref)", + "Schema successfully created": "Το Σχήμα δημιουργήθηκε με επιτυχία", + "Schema successfully updated": "Το Σχήμα ενημερώθηκε με επιτυχία", + "Schema {schema} removed successfully": "Το Σχήμα {schema} αφαιρέθηκε με επιτυχία", + "Schema:": "Σχήμα:", + "Schemas": "Σχήματα", + "Scheme": "Σχήμα κωδικοποίησης", + "Search": "Αναζήτηση", + "Search / Views": "Αναζήτηση / Προβολές", + "Search / views": "Αναζήτηση / προβολές", + "Search Activity": "Δραστηριότητα αναζήτησης", + "Search Analytics": "Αναλυτικά στοιχεία αναζήτησης", + "Search Configuration": "Διαμόρφωση αναζήτησης", + "Search Entities": "Αναζήτηση οντοτήτων", + "Search Files": "Αναζήτηση Αρχείων", + "Search GitHub": "Αναζήτηση στο GitHub", + "Search GitLab": "Αναζήτηση στο GitLab", + "Search Method": "Μέθοδος αναζήτησης", + "Search Mode": "Λειτουργία αναζήτησης", + "Search Query": "Ερώτημα αναζήτησης", + "Search Results": "Αποτελέσματα αναζήτησης", + "Search Term": "Όρος αναζήτησης", + "Search Trail Details": "Λεπτομέρειες ίχνους αναζήτησης", + "Search Trail Management": "Διαχείριση ιχνών αναζήτησης", + "Search Trail Statistics": "Στατιστικά ιχνών αναζήτησης", + "Search Trails": "Ίχνη αναζήτησης", + "Search Views": "Αναζήτηση προβολών", + "Search Webhooks": "Αναζήτηση webhooks", + "Search agents...": "Αναζήτηση πρακτόρων...", + "Search applications...": "Αναζήτηση εφαρμογών...", + "Search by file name or path": "Αναζήτηση κατά όνομα Αρχείου ή διαδρομή", + "Search by name or URL": "Αναζήτηση κατά όνομα ή URL", + "Search by title or UUID...": "Αναζήτηση κατά τίτλο ή UUID...", + "Search by value": "Αναζήτηση κατά τιμή", + "Search configurations": "Αναζήτηση διαμορφώσεων", + "Search data sources...": "Αναζήτηση Πηγών δεδομένων...", + "Search groups...": "Αναζήτηση ομάδων...", + "Search in Database Objects": "Αναζήτηση σε Αντικείμενα βάσης δεδομένων", + "Search in Files": "Αναζήτηση σε Αρχεία", + "Search in Objects": "Αναζήτηση σε Αντικείμενα", + "Search objects": "Αναζήτηση Αντικειμένων", + "Search objects...": "Αναζήτηση Αντικειμένων...", + "Search property names...": "Αναζήτηση ονομάτων Ιδιοτήτων...", + "Search registers...": "Αναζήτηση Μητρώων...", + "Search schemas...": "Αναζήτηση Σχημάτων...", + "Search trail deletion not implemented yet": "Η διαγραφή ίχνους αναζήτησης δεν έχει υλοποιηθεί ακόμη", + "Search views...": "Αναζήτηση προβολών...", + "Search {name}...": "Αναζήτηση {name}...", + "Searching...": "Αναζήτηση...", + "Secret": "Μυστικό", + "Secret key for HMAC signature generation (optional)": "Μυστικό κλειδί για δημιουργία υπογραφής HMAC (προαιρετικό)", + "Security": "Ασφάλεια", + "See organisations as a table": "Προβολή οργανισμών ως πίνακας", + "See organisations as cards": "Προβολή οργανισμών ως κάρτες", + "Select AI Agent": "Επιλογή πράκτορα AI", + "Select Active Organisation": "Επιλογή ενεργού οργανισμού", + "Select All": "Επιλογή όλων", + "Select ConfigSet": "Επιλογή ConfigSet", + "Select File Types to Vectorize:": "Επιλογή τύπων Αρχείων προς διανυσματοποίηση:", + "Select HTTP method": "Επιλογή μεθόδου HTTP", + "Select Register and Schema": "Επιλογή Μητρώου και Σχήματος", + "Select Views to Vectorize:": "Επιλογή προβολών προς διανυσματοποίηση:", + "Select a Nextcloud group to add": "Επιλέξτε μια ομάδα Nextcloud για προσθήκη", + "Select a branch": "Επιλέξτε έναν κλάδο", + "Select a date property": "Επιλέξτε μια Ιδιότητα ημερομηνίας", + "Select a model or type a custom model name": "Επιλέξτε ένα μοντέλο ή πληκτρολογήστε ένα προσαρμοσμένο όνομα μοντέλου", + "Select a register": "Επιλέξτε ένα Μητρώο", + "Select a register...": "Επιλέξτε ένα Μητρώο...", + "Select a repository": "Επιλέξτε ένα αποθετήριο", + "Select a repository you have write access to": "Επιλέξτε ένα αποθετήριο στο οποίο έχετε δικαίωμα εγγραφής", + "Select a schema": "Επιλέξτε ένα Σχήμα", + "Select a schema...": "Επιλέξτε ένα Σχήμα...", + "Select a user": "Επιλέξτε έναν χρήστη", + "Select agent type": "Επιλέξτε τύπο πράκτορα", + "Select all entities": "Επιλογή όλων των οντοτήτων", + "Select an AI Agent": "Επιλέξτε έναν πράκτορα AI", + "Select an AI agent to begin chatting with your data.": "Επιλέξτε έναν πράκτορα AI για να ξεκινήσετε συνομιλία με τα δεδομένα σας.", + "Select an email to see linked objects": "Επιλέξτε ένα email για να δείτε τα συνδεδεμένα Αντικείμενα", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Επιλέξτε έναν οργανισμό και έναν χρήστη για να τον προσθέσετε ως μέλος. Αναζητήστε οργανισμούς κατά όνομα.", + "Select backend": "Επιλέξτε backend", + "Select chat model": "Επιλέξτε μοντέλο συνομιλίας", + "Select collection for files": "Επιλέξτε συλλογή για Αρχεία", + "Select collection for objects": "Επιλέξτε συλλογή για Αντικείμενα", + "Select configuration type...": "Επιλέξτε τύπο διαμόρφωσης...", + "Select default organisation": "Επιλέξτε προεπιλεγμένο οργανισμό", + "Select entity {value}": "Επιλογή οντότητας {value}", + "Select event to listen to...": "Επιλέξτε συμβάν προς παρακολούθηση...", + "Select fields to display": "Επιλέξτε πεδία προς εμφάνιση", + "Select files with the correct extension": "Επιλέξτε Αρχεία με τη σωστή επέκταση", + "Select group": "Επιλέξτε ομάδα", + "Select groups (optional)": "Επιλέξτε ομάδες (προαιρετικό)", + "Select groups to notify...": "Επιλέξτε ομάδες προς ειδοποίηση...", + "Select groups with access to this agent": "Επιλέξτε ομάδες με πρόσβαση σε αυτόν τον πράκτορα", + "Select groups...": "Επιλέξτε ομάδες...", + "Select model": "Επιλέξτε μοντέλο", + "Select one or more registers": "Επιλέξτε ένα ή περισσότερα Μητρώα", + "Select one or more schemas": "Επιλέξτε ένα ή περισσότερα Σχήματα", + "Select options...": "Επιλέξτε επιλογές...", + "Select or create labels, or select \"No label\" to add files": "Επιλέξτε ή δημιουργήστε ετικέτες ή επιλέξτε \"Χωρίς ετικέτα\" για να προσθέσετε Αρχεία", + "Select owner application (optional)...": "Επιλέξτε εφαρμογή κατόχου (προαιρετικό)...", + "Select period": "Επιλέξτε περίοδο", + "Select property to send as payload": "Επιλέξτε Ιδιότητα προς αποστολή ως ωφέλιμο φορτίο", + "Select provider": "Επιλέξτε πάροχο", + "Select registers and schemas to save a view": "Επιλέξτε Μητρώα και Σχήματα για να αποθηκεύσετε μια προβολή", + "Select retry policy": "Επιλέξτε πολιτική επανάληψης", + "Select role": "Επιλέξτε ρόλο", + "Select schemas (empty = all schemas)": "Επιλέξτε Σχήματα (κενό = όλα τα Σχήματα)", + "Select schemas to warm up": "Επιλέξτε Σχήματα προς προθέρμανση", + "Select search mode": "Επιλέξτε λειτουργία αναζήτησης", + "Select source type...": "Επιλέξτε τύπο Πηγής...", + "Select target type": "Επιλέξτε τύπο προορισμού", + "Select the branch to publish to": "Επιλέξτε τον κλάδο στον οποίο θα γίνει δημοσίευση", + "Select the event this webhook should listen to": "Επιλέξτε το συμβάν που πρέπει να παρακολουθεί αυτό το webhook", + "Select views to filter data (optional)": "Επιλέξτε προβολές για φιλτράρισμα δεδομένων (προαιρετικό)", + "Select views to limit which data the agent can access": "Επιλέξτε προβολές για να περιορίσετε σε ποια δεδομένα έχει πρόσβαση ο πράκτορας", + "Select views to vectorize:": "Επιλέξτε προβολές προς διανυσματοποίηση:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Επιλέξτε ποιες ομάδες Nextcloud είναι διαθέσιμες για αυτόν τον οργανισμό. Οι χρήστες σε αυτές τις ομάδες θα έχουν πρόσβαση στους πόρους του οργανισμού.", + "Select which data views the AI can search": "Επιλέξτε σε ποιες προβολές δεδομένων μπορεί να αναζητήσει το AI", + "Select which property from the event should be used as the webhook payload data": "Επιλέξτε ποια Ιδιότητα από το συμβάν θα χρησιμοποιηθεί ως δεδομένα ωφέλιμου φορτίου του webhook", + "Select which tools the AI can use to perform actions": "Επιλέξτε ποια εργαλεία μπορεί να χρησιμοποιήσει το AI για την εκτέλεση ενεργειών", + "Select {fieldName} values": "Επιλέξτε τιμές {fieldName}", + "Selected": "Επιλεγμένο", + "Selected Groups": "Επιλεγμένες ομάδες", + "Selected Objects ({count})": "Επιλεγμένα Αντικείμενα ({count})", + "Selected audit trails deleted successfully": "Τα επιλεγμένα ίχνη ελέγχου διαγράφηκαν με επιτυχία", + "Selected search trails deleted successfully": "Τα επιλεγμένα ίχνη αναζήτησης διαγράφηκαν με επιτυχία", + "Selected users can access this view": "Οι επιλεγμένοι χρήστες μπορούν να αποκτήσουν πρόσβαση σε αυτήν την προβολή", + "Send additional feedback": "Αποστολή πρόσθετων σχολίων", + "Send as CloudEvent": "Αποστολή ως CloudEvent", + "Send as cloudevent": "Αποστολή ως CloudEvent", + "Sensitive PII": "Ευαίσθητα PII", + "Serial Mode (Safer, slower)": "Σειριακή λειτουργία (Ασφαλέστερη, πιο αργή)", + "Serial:": "Σειριακά:", + "Server Configuration": "Διαμόρφωση διακομιστή", + "Server Information": "Πληροφορίες διακομιστή", + "Set as Active": "Ορισμός ως ενεργό", + "Set as active organisation": "Ορισμός ως ενεργός οργανισμός", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Ορίστε όρια για τη χρήση API και την κατανάλωση διακριτικών. Χρησιμοποιήστε 0 για απεριόριστους πόρους.", + "Settings": "Ρυθμίσεις", + "Settings updated successfully": "Οι ρυθμίσεις ενημερώθηκαν με επιτυχία", + "Shards": "Shards", + "Share": "Κοινή χρήση", + "Share with Groups": "Κοινή χρήση με ομάδες", + "Share with Users": "Κοινή χρήση με χρήστες", + "Show": "Εμφάνιση", + "Show Filters": "Εμφάνιση φίλτρων", + "Show less": "Εμφάνιση λιγότερων", + "Show only entries with changes": "Εμφάνιση μόνο καταχωρίσεων με αλλαγές", + "Showing {count} dashboard(s)": "Εμφάνιση {count} πίνακα(-ων) ελέγχου", + "Showing {count} processing activities": "Εμφάνιση {count} δραστηριοτήτων επεξεργασίας", + "Showing {showing} of {total} agents": "Εμφάνιση {showing} από {total} πράκτορες", + "Showing {showing} of {total} applications": "Εμφάνιση {showing} από {total} εφαρμογές", + "Showing {showing} of {total} audit trail entries": "Εμφάνιση {showing} από {total} καταχωρίσεις ίχνους ελέγχου", + "Showing {showing} of {total} configurations": "Εμφάνιση {showing} από {total} διαμορφώσεις", + "Showing {showing} of {total} deleted items": "Εμφάνιση {showing} από {total} διαγραμμένα στοιχεία", + "Showing {showing} of {total} entities": "Εμφάνιση {showing} από {total} οντότητες", + "Showing {showing} of {total} files": "Εμφάνιση {showing} από {total} Αρχεία", + "Showing {showing} of {total} log entries": "Εμφάνιση {showing} από {total} καταχωρίσεις αρχείου καταγραφής", + "Showing {showing} of {total} search trail entries": "Εμφάνιση {showing} από {total} καταχωρίσεις ίχνους αναζήτησης", + "Showing {showing} of {total} sources": "Εμφάνιση {showing} από {total} Πηγές", + "Showing {showing} of {total} templates": "Εμφάνιση {showing} από {total} πρότυπα", + "Showing {showing} of {total} webhooks": "Εμφάνιση {showing} από {total} webhooks", + "Showing {shown} of {total} organisations": "Εμφάνιση {shown} από {total} οργανισμούς", + "Simple": "Απλό", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Απλά ερωτήματα: Βασικές αναζητήσεις κειμένου με ελάχιστες παραμέτρους (π.χ. μοναδικός όρος αναζήτησης, χωρίς προηγμένα φίλτρα)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Απλά ερωτήματα: βασικές αναζητήσεις κειμένου με ελάχιστες παραμέτρους (π.χ. μοναδικός όρος αναζήτησης, χωρίς προηγμένα φίλτρα)", + "Size": "Μέγεθος", + "Slug": "Slug", + "Soft Delete Mode": "Λειτουργία προσωρινής διαγραφής", + "Soft Deleted Items": "Προσωρινά διαγραμμένα στοιχεία", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Προσωρινή διαγραφή όλων των Αντικειμένων για αυτό το Σχήμα ({active} ενεργά, {deleted} ήδη διαγραμμένα)", + "SolrCloud mode enabled": "Η λειτουργία SolrCloud ενεργοποιήθηκε", + "Something went wrong while adding files": "Παρουσιάστηκε σφάλμα κατά την προσθήκη Αρχείων", + "Source": "Πηγή", + "Source successfully updated": "Η Πηγή ενημερώθηκε με επιτυχία", + "Sources": "Πηγές", + "Sources:": "Πηγές:", + "Special Rights": "Ειδικά δικαιώματα", + "Standalone SOLR mode": "Αυτόνομη λειτουργία SOLR", + "Start Conversation": "Έναρξη συνομιλίας", + "Start Date Field": "Πεδίο ημερομηνίας έναρξης", + "Start Vectorization": "Έναρξη διανυσματοποίησης", + "Start a conversation": "Έναρξη συνομιλίας", + "Starting names cache warmup...": "Έναρξη προθέρμανσης της κρυφής μνήμης ονομάτων...", + "Starting validation for {schema}...": "Έναρξη επικύρωσης για {schema}...", + "Starting {action} for {schema}...": "Έναρξη {action} για {schema}...", + "Starting...": "Έναρξη...", + "Statistics": "Στατιστικά", + "Statistics and Metrics": "Στατιστικά και μετρήσεις", + "Statistics refreshed": "Τα στατιστικά ανανεώθηκαν", + "Stats calculation not yet implemented": "Ο υπολογισμός στατιστικών δεν έχει υλοποιηθεί ακόμη", + "Status": "Κατάσταση", + "Status Code": "Κωδικός κατάστασης", + "Step": "Βήμα", + "Storage": "Αποθηκευτικός χώρος", + "Storage Quota (MB)": "Ποσόστωση αποθηκευτικού χώρου (MB)", + "String Constraints": "Περιορισμοί συμβολοσειράς", + "Subject identifier (email, BSN, name, etc.)": "Αναγνωριστικό υποκειμένου (email, BSN, όνομα κ.λπ.)", + "Subscribe": "Εγγραφή", + "Subscribed": "Εγγεγραμμένος", + "Success": "Επιτυχία", + "Success Rate": "Ποσοστό επιτυχίας", + "Success Status": "Κατάσταση επιτυχίας", + "Successful": "Επιτυχής", + "Successfully added user to organisation: {name}": "Ο χρήστης προστέθηκε με επιτυχία στον οργανισμό: {name}", + "Successfully cleared {count} audit trails": "Εκκαθαρίστηκαν με επιτυχία {count} ίχνη ελέγχου", + "Successfully cleared {count} blob storage objects": "Εκκαθαρίστηκαν με επιτυχία {count} αντικείμενα αποθήκευσης blob", + "Successfully cleared {count} search trails": "Εκκαθαρίστηκαν με επιτυχία {count} ίχνη αναζήτησης", + "Successfully created {count} SOLR fields": "Δημιουργήθηκαν με επιτυχία {count} πεδία SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Διαγράφηκαν με επιτυχία {count} ενσωματώσεις. Παρακαλούμε επαναδιανυσματοποιήστε τα δεδομένα σας.", + "Successfully deleted {count} objects for {schema}": "Διαγράφηκαν με επιτυχία {count} αντικείμενα για {schema}", + "Successfully fixed {count} SOLR field configurations": "Διορθώθηκαν με επιτυχία {count} διαμορφώσεις πεδίων SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Εισήχθησαν με επιτυχία: {registers} Μητρώα, {schemas} Σχήματα, {objects} Αντικείμενα", + "Successfully joined organisation": "Επιτυχής ένταξη στον οργανισμό", + "Successfully permanently deleted {count} objects": "Διαγράφηκαν οριστικά με επιτυχία {count} αντικείμενα", + "Successfully processed {count} files!": "Επεξεργάστηκαν με επιτυχία {count} αρχεία!", + "Successfully restored {count} object": "Αποκαταστάθηκε με επιτυχία {count} αντικείμενο", + "Successfully restored {count} objects": "Αποκαταστάθηκαν με επιτυχία {count} αντικείμενα", + "Successfully saved configuration for {count} facets!": "Αποθηκεύτηκε με επιτυχία η διαμόρφωση για {count} όψεις!", + "Supported File Types": "Υποστηριζόμενοι τύποι αρχείων", + "Switch Active Organisation": "Εναλλαγή ενεργού οργανισμού", + "Switch Organisation": "Εναλλαγή οργανισμού", + "Sync Table": "Συγχρονισμός πίνακα", + "Synchronization Settings": "Ρυθμίσεις συγχρονισμού", + "Syncing magic table for {schema}...": "Συγχρονισμός μαγικού πίνακα για {schema}...", + "Syncs": "Συγχρονισμοί", + "System Default": "Προεπιλογή συστήματος", + "System Prompt": "Προτροπή συστήματος", + "System announcements": "Ανακοινώσεις συστήματος", + "Table": "Πίνακας", + "Target ID": "Αναγνωριστικό στόχου", + "Target Type*": "Τύπος στόχου*", + "Technical Description": "Τεχνική περιγραφή", + "Technical Details": "Τεχνικές λεπτομέρειες", + "Technical description for developers and administrators": "Τεχνική περιγραφή για προγραμματιστές και διαχειριστές", + "Technical measures": "Τεχνικά μέτρα", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Θερμοκρασία", + "Templates": "Πρότυπα", + "Test": "Δοκιμή", + "Test Chat": "Δοκιμαστική συνομιλία", + "Test Connection": "Δοκιμή σύνδεσης", + "Test Embedding": "Δοκιμή ενσωμάτωσης", + "Test endpoint executed successfully": "Το δοκιμαστικό σημείο πρόσβασης εκτελέστηκε με επιτυχία", + "Test webhook delivered successfully": "Το δοκιμαστικό webhook παραδόθηκε με επιτυχία", + "Test webhook delivery failed": "Η παράδοση του δοκιμαστικού webhook απέτυχε", + "Test webhook sent successfully": "Το δοκιμαστικό webhook στάλθηκε με επιτυχία", + "Testing...": "Δοκιμή...", + "Text Chunk #{id}": "Τμήμα κειμένου #{id}", + "Text Extraction": "Εξαγωγή κειμένου", + "Text Extraction Engine": "Μηχανή εξαγωγής κειμένου", + "Text chunks": "Τμήματα κειμένου", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Τα τμήματα κειμένου δημιουργούνται κατά την εξαγωγή του αρχείου και αποθηκεύονται στη βάση δεδομένων. Η διανυσματοποίηση διαβάζει αυτά τα προ-τμηματοποιημένα αρχεία και τα μετατρέπει σε ενσωματώσεις.", + "Text extracted successfully": "Το κείμενο εξήχθη με επιτυχία", + "Text extraction disabled": "Η εξαγωγή κειμένου απενεργοποιήθηκε", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Η εφαρμογή Nextcloud Calendar δεν είναι εγκατεστημένη ή ενεργοποιημένη σε αυτόν τον διακομιστή.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Η εφαρμογή Nextcloud Contacts δεν είναι εγκατεστημένη ή ενεργοποιημένη σε αυτόν τον διακομιστή.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Η εφαρμογή Nextcloud Deck δεν είναι εγκατεστημένη ή ενεργοποιημένη σε αυτόν τον διακομιστή.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Η εφαρμογή Nextcloud Mail δεν είναι εγκατεστημένη ή ενεργοποιημένη σε αυτόν τον διακομιστή.", + "The URL where webhook events will be sent": "Το URL όπου θα αποστέλλονται τα συμβάντα webhook", + "The requested conversation does not exist": "Η ζητούμενη συνομιλία δεν υπάρχει", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Δεν ήταν δυνατή η φόρτωση του ζητούμενου πίνακα ελέγχου. Επαληθεύστε το URL ή επιλέξτε άλλον πίνακα ελέγχου από τη λίστα.", + "The saved view and all its search configuration": "Η αποθηκευμένη προβολή και όλη η διαμόρφωση αναζήτησής της", + "There are no audit trail entries matching your current filters.": "Δεν υπάρχουν καταχωρήσεις ίχνους ελέγχου που να ταιριάζουν με τα τρέχοντα φίλτρα σας.", + "There are no deleted items matching your current filters.": "Δεν υπάρχουν διαγραμμένα στοιχεία που να ταιριάζουν με τα τρέχοντα φίλτρα σας.", + "There are no search trail entries matching your current filters.": "Δεν υπάρχουν καταχωρήσεις ίχνους αναζήτησης που να ταιριάζουν με τα τρέχοντα φίλτρα σας.", + "There are no webhook log entries matching your filters.": "Δεν υπάρχουν καταχωρήσεις αρχείου καταγραφής webhook που να ταιριάζουν με τα φίλτρα σας.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Θα διατηρηθούν σύμφωνα με την περίοδο διατήρησης που έχει διαμορφωθεί στο Σχήμα τους και θα διαγραφούν αυτόματα οριστικά όταν λήξει η περίοδος διατήρησης. Η περίοδος διατήρησης είναι διαμορφώσιμη ανά Σχήμα και μπορεί να βρεθεί στις ρυθμίσεις του Σχήματος.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Θα διατηρηθούν σύμφωνα με την περίοδο διατήρησης που έχει διαμορφωθεί στο Σχήμα τους και στη συνέχεια θα αφαιρεθούν οριστικά.", + "This action cannot be undone.": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί. Βεβαιωθείτε ότι καμία συλλογή δεν χρησιμοποιεί αυτό το ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί. Βεβαιωθείτε ότι καμία συλλογή δεν χρησιμοποιεί αυτό το ConfigSet.", + "This action will submit a deactivation request to your administrators.": "Αυτή η ενέργεια θα υποβάλει αίτημα απενεργοποίησης στους διαχειριστές σας.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Αυτός ο πράκτορας δεν διαθέτει διαμορφώσιμες προβολές ή εργαλεία, αλλά μπορείτε ωστόσο να προσαρμόσετε τις ρυθμίσεις RAG παρακάτω.", + "This analysis may take some time": "Αυτή η ανάλυση ενδέχεται να διαρκέσει κάποιο χρόνο", + "This audit trail entry does not contain any change information.": "Αυτή η καταχώρηση ίχνους ελέγχου δεν περιέχει πληροφορίες αλλαγής.", + "This configuration has no data defined.": "Αυτή η διαμόρφωση δεν έχει καθορισμένα δεδομένα.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Αυτό το σημείο πρόσβασης έχει καταργηθεί. Χρησιμοποιήστε σημεία πρόσβασης βασισμένα σε τμήματα αντ' αυτού.", + "This entity has no relations to objects or files": "Αυτή η οντότητα δεν έχει σχέσεις με Αντικείμενα ή Αρχεία", + "This is a test webhook from OpenRegister": "Αυτό είναι ένα δοκιμαστικό webhook από το OpenRegister", + "This is not valid JSON": "Αυτό δεν είναι έγκυρο JSON", + "This key already exists on this schema": "Αυτό το κλειδί υπάρχει ήδη σε αυτό το Σχήμα", + "This schema must use Magic Table configuration to sync": "Αυτό το Σχήμα πρέπει να χρησιμοποιεί διαμόρφωση Magic Table για συγχρονισμό", + "This schema must use magic table configuration to sync": "Αυτό το Σχήμα πρέπει να χρησιμοποιεί διαμόρφωση μαγικού πίνακα για συγχρονισμό", + "This source has no associated registers.": "Αυτή η Πηγή δεν έχει συσχετισμένα Μητρώα.", + "This token will only be shown once. Copy it now.": "Αυτό το διακριτικό θα εμφανιστεί μόνο μία φορά. Αντιγράψτε το τώρα.", + "This will copy the _default ConfigSet with the new name": "Αυτό θα αντιγράψει το ConfigSet _default με το νέο όνομα", + "This will copy the _default configset with the new name": "Αυτό θα αντιγράψει το ConfigSet _default με το νέο όνομα", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Αυτό θα διαγράψει οριστικά ΟΛΕΣ τις ενσωματώσεις (διανύσματα) από τη βάση δεδομένων. Θα χρειαστεί να επαναδιανυσματοποιήσετε όλα τα Αντικείμενα και Αρχεία. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.\n\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Αυτό θα διαγράψει οριστικά ΟΛΕΣ τις ενσωματώσεις (διανύσματα) από τη βάση δεδομένων. Θα χρειαστεί να επαναδιανυσματοποιήσετε όλα τα Αντικείμενα και Αρχεία. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.\\n\\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;", + "This will permanently delete:": "Αυτό θα διαγράψει οριστικά:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Αυτό θα αφαιρέσει οριστικά {count} ήδη διαγραμμένα Αντικείμενα από τη βάση δεδομένων. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "Timeout (seconds)": "Χρονικό όριο (δευτερόλεπτα)", + "Timestamp": "Χρονοσήμανση", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Συμβουλή: Ενεργοποιήστε μόνο τις προβολές που χρειάζονται σημασιολογική αναζήτηση για να ελαχιστοποιήσετε το κόστος ενσωματώσεων. Οι απλοί πίνακες αναζήτησης σπάνια χρειάζονται διανυσματοποίηση.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Συμβουλή: ενεργοποιήστε μόνο τις προβολές που χρειάζονται σημασιολογική αναζήτηση για να ελαχιστοποιήσετε το κόστος ενσωματώσεων. Οι απλοί πίνακες αναζήτησης σπάνια χρειάζονται διανυσματοποίηση.", + "Title": "Τίτλος", + "Title *": "Τίτλος *", + "Title Template": "Πρότυπο τίτλου", + "Title*": "Τίτλος*", + "To (e.g. -1 year or 2025-12-31)": "Έως (π.χ. -1 year ή 2025-12-31)", + "To Date": "Έως ημερομηνία", + "To add files larger than or equal to 512MB, go to the": "Για να προσθέσετε Αρχεία μεγαλύτερα ή ίσα των 512MB, μεταβείτε στο", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Για να προσαρμόσετε το μέγεθος ή τη στρατηγική τμημάτων, μεταβείτε στη Διαμόρφωση αρχείων → Όρια επεξεργασίας.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Για να προσαρμόσετε το μέγεθος ή τη στρατηγική τμημάτων, μεταβείτε στη διαμόρφωση αρχείων → όρια επεξεργασίας.", + "To change the embedding provider or model, go to LLM Configuration.": "Για να αλλάξετε τον πάροχο ή το μοντέλο ενσωμάτωσης, μεταβείτε στη Διαμόρφωση LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Για να αλλάξετε τον πάροχο ή το μοντέλο ενσωμάτωσης, μεταβείτε στη διαμόρφωση LLM.", + "To date": "Έως ημερομηνία", + "Toggle search sidebar": "Εναλλαγή πλευρικής γραμμής αναζήτησης", + "Token Created": "Το διακριτικό δημιουργήθηκε", + "Token Quota (per request)": "Ποσόστωση διακριτικών (ανά αίτημα)", + "Token copied to clipboard": "Το διακριτικό αντιγράφηκε στο πρόχειρο", + "Token name": "Όνομα διακριτικού", + "Token revoked": "Το διακριτικό ανακλήθηκε", + "Tools": "Εργαλεία", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Τα εργαλεία εκτελούνται με τα προεπιλεγμένα δικαιώματα χρήστη του πράκτορα όταν δεν υπάρχει ενεργή συνεδρία χρήστη (π.χ. εργασίες cron). Διαμορφώστε τον προεπιλεγμένο χρήστη στην καρτέλα Ρυθμίσεις.", + "Tools respect the agent's own role and organization boundaries.": "Τα εργαλεία σέβονται τον ρόλο και τα όρια οργανισμού του ίδιου του πράκτορα.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Τα εργαλεία σέβονται τις προβολές, τα δικαιώματα και τα όρια οργανισμού του πράκτορα.", + "Top Deleters": "Κορυφαίοι διαγράφοντες", + "Total": "Σύνολο", + "Total Audit Trails": "Σύνολο ιχνών ελέγχου", + "Total Chunks Available:": "Σύνολο διαθέσιμων τμημάτων:", + "Total Deleted Items": "Σύνολο διαγραμμένων στοιχείων", + "Total Members": "Σύνολο μελών", + "Total Objects": "Σύνολο Αντικειμένων", + "Total Objects in Database:": "Σύνολο Αντικειμένων στη βάση δεδομένων:", + "Total Organisations": "Σύνολο οργανισμών", + "Total Results": "Σύνολο αποτελεσμάτων", + "Total Searches": "Σύνολο αναζητήσεων", + "Total Size": "Συνολικό μέγεθος", + "Total Storage": "Συνολικός αποθηκευτικός χώρος", + "Total events": "Σύνολο συμβάντων", + "Total:": "Σύνολο:", + "Totals": "Σύνολα", + "Try searching by UUID or with different keywords": "Δοκιμάστε αναζήτηση με UUID ή με διαφορετικές λέξεις-κλειδιά", + "Type": "Τύπος", + "Type \"DELETE COLLECTION\" to confirm:": "Πληκτρολογήστε \"DELETE COLLECTION\" για επιβεβαίωση:", + "Type Issue": "Πρόβλημα τύπου", + "Type Variations:": "Παραλλαγές τύπου:", + "Type filter (optional, e.g. email)": "Φίλτρο τύπου (προαιρετικό, π.χ. email)", + "Type must be \"positive\" or \"negative\"": "Ο τύπος πρέπει να είναι \"positive\" ή \"negative\"", + "Type to search for groups": "Πληκτρολογήστε για αναζήτηση ομάδων", + "Type to search for objects...": "Πληκτρολογήστε για αναζήτηση Αντικειμένων...", + "Type to search for organisations": "Πληκτρολογήστε για αναζήτηση οργανισμών", + "Type to search for users": "Πληκτρολογήστε για αναζήτηση χρηστών", + "Type to search groups...": "Πληκτρολογήστε για αναζήτηση ομάδων...", + "Type to search users...": "Πληκτρολογήστε για αναζήτηση χρηστών...", + "Type to search...": "Πληκτρολογήστε για αναζήτηση...", + "Type your username to confirm": "Πληκτρολογήστε το όνομα χρήστη σας για επιβεβαίωση", + "URL": "URL", + "URL where Ollama is running": "URL όπου εκτελείται το Ollama", + "UUID copied to clipboard": "Το UUID αντιγράφηκε στο πρόχειρο", + "UUID:": "UUID:", + "Unchanged": "Αμετάβλητο", + "Unique Search Terms": "Μοναδικοί όροι αναζήτησης", + "Unique Terms": "Μοναδικοί όροι", + "Unknown": "Άγνωστο", + "Unknown Object": "Άγνωστο Αντικείμενο", + "Unknown Register": "Άγνωστο Μητρώο", + "Unknown Schema": "Άγνωστο Σχήμα", + "Unknown error": "Άγνωστο σφάλμα", + "Unknown widget type: {type}": "Άγνωστος τύπος γραφικού στοιχείου: {type}", + "Unlink email": "Αποσύνδεση email", + "Unlink event": "Αποσύνδεση συμβάντος", + "Unnamed Object": "Ανώνυμο Αντικείμενο", + "Untitled": "Χωρίς τίτλο", + "Untitled View": "Προβολή χωρίς τίτλο", + "Update": "Ενημέρωση", + "Update Operations": "Λειτουργίες ενημέρωσης", + "Update available: {local} → {remote}": "Διαθέσιμη ενημέρωση: {local} → {remote}", + "Update configuration: ...": "Ενημέρωση διαμόρφωσης: ...", + "Update register OAS: ...": "Ενημέρωση OAS Μητρώου: ...", + "Update vectors when object data changes (recommended for accurate search)": "Ενημέρωση διανυσμάτων όταν αλλάζουν τα δεδομένα Αντικειμένου (συνιστάται για ακριβή αναζήτηση)", + "Updated": "Ενημερώθηκε", + "Updated At": "Ενημερώθηκε στις", + "Updated:": "Ενημερώθηκε:", + "Upload new avatar": "Μεταφόρτωση νέου άβαταρ", + "Uploading configuration sets...": "Μεταφόρτωση συνόλων διαμόρφωσης...", + "Uptime:": "Χρόνος λειτουργίας:", + "Url": "Url", + "Usage Count": "Πλήθος χρήσεων", + "Use AI agents for processing and analysis": "Χρήση πρακτόρων AI για επεξεργασία και ανάλυση", + "Use Large Language Model features": "Χρήση δυνατοτήτων Large Language Model", + "Use SolrCloud with Zookeeper for distributed search": "Χρήση του SolrCloud με Zookeeper για κατανεμημένη αναζήτηση", + "Use SolrCloud with zookeeper for distributed search": "Χρήση του SolrCloud με Zookeeper για κατανεμημένη αναζήτηση", + "Use VNG APIs instead of MongoDB.": "Χρήση API της VNG αντί για MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Χρήση προεπιλεγμένων εύρων (Τελευταίες 7/30/90 ημέρες, Τελευταίο έτος, Παλαιότερα)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Χρήση εξωτερικού αποθηκευτικού χώρου (π.χ. MongoDB) αντί του εσωτερικού αποθηκευτικού χώρου του Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Χρησιμοποιήστε φίλτρα για να περιορίσετε τις καταχωρήσεις ίχνους ελέγχου ανά Μητρώο, Σχήμα, τύπο ενέργειας, χρήστη, εύρος ημερομηνιών ή αναγνωριστικό Αντικειμένου.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Χρησιμοποιήστε φίλτρα για να περιορίσετε τα διαγραμμένα στοιχεία ανά Μητρώο, Σχήμα, ημερομηνία διαγραφής ή χρήστη που τα διέγραψε.", + "Use filters to narrow down entities by type or category.": "Χρησιμοποιήστε φίλτρα για να περιορίσετε τις οντότητες ανά τύπο ή κατηγορία.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Χρησιμοποιήστε φίλτρα για να περιορίσετε τις καταχωρήσεις ίχνους αναζήτησης ανά Μητρώο, Σχήμα, κατάσταση επιτυχίας, χρήστη, εύρος ημερομηνιών, όρους αναζήτησης ή μετρήσεις απόδοσης.", + "Used By Collections": "Χρησιμοποιείται από συλλογές", + "Used by": "Χρησιμοποιείται από", + "User": "Χρήστης", + "User Agent Statistics": "Στατιστικά User Agent", + "User Description": "Περιγραφή χρήστη", + "User Quota": "Ποσόστωση χρήστη", + "User rate limits cleared successfully": "Τα όρια ρυθμού χρήστη εκκαθαρίστηκαν με επιτυχία", + "User removed successfully": "Ο χρήστης αφαιρέθηκε με επιτυχία", + "User-friendly description shown in forms and help text": "Φιλική προς τον χρήστη περιγραφή που εμφανίζεται σε φόρμες και κείμενο βοήθειας", + "Username": "Όνομα χρήστη", + "Username for SOLR authentication (optional)": "Όνομα χρήστη για έλεγχο ταυτότητας SOLR (προαιρετικό)", + "Users": "Χρήστες", + "Uses": "Χρήσεις", + "Using Pre-Generated Chunks": "Χρήση προ-δημιουργημένων τμημάτων", + "Validate": "Επικύρωση", + "Validate Objects": "Επικύρωση Αντικειμένων", + "Validation completed for {schema}": "Η επικύρωση ολοκληρώθηκε για {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Η επικύρωση ολοκληρώθηκε για {schema}: {processed} επεξεργάστηκαν, {updated} ενημερώθηκαν, {failed} απέτυχαν", + "Validation completed successfully": "Η επικύρωση ολοκληρώθηκε με επιτυχία", + "Validation failed": "Η επικύρωση απέτυχε", + "Value": "Τιμή", + "Value list (split on ,)": "Λίστα τιμών (διαχωρισμός με ,)", + "Vector Dimensions": "Διαστάσεις διανύσματος", + "Vector Search Backend": "Backend διανυσματικής αναζήτησης", + "Vector field: _embedding_": "Πεδίο διανύσματος: _embedding_", + "Vectorization Triggers": "Ενεργοποιητές διανυσματοποίησης", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Η διανυσματοποίηση ολοκληρώθηκε: {vectorized} Αντικείμενα διανυσματοποιήθηκαν, {failed} απέτυχαν", + "Vectorization failed: {error}": "Η διανυσματοποίηση απέτυχε: {error}", + "Vectorize All Files": "Διανυσματοποίηση όλων των Αρχείων", + "Vectorize All Objects": "Διανυσματοποίηση όλων των Αντικειμένων", + "Vectorize all file types": "Διανυσματοποίηση όλων των τύπων αρχείων", + "Vectorize all views": "Διανυσματοποίηση όλων των προβολών", + "Vectorize on object creation": "Διανυσματοποίηση κατά τη δημιουργία Αντικειμένου", + "Vectors will be stored in your existing object and file collections": "Τα διανύσματα θα αποθηκευτούν στις υπάρχουσες συλλογές Αντικειμένων και Αρχείων σας", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Επαλήθευση συνδεσιμότητας διακομιστή...", + "Version": "Έκδοση", + "Version Information": "Πληροφορίες έκδοσης", + "Very High": "Πολύ υψηλό", + "Very high": "Πολύ υψηλό", + "View": "Προβολή", + "View API Docs": "Προβολή τεκμηρίωσης API", + "View API Documentation": "Προβολή τεκμηρίωσης API", + "View APIs": "Προβολή API", + "View Changes": "Προβολή αλλαγών", + "View Details": "Προβολή λεπτομερειών", + "View Error": "Προβολή σφάλματος", + "View File": "Προβολή Αρχείου", + "View Full Details": "Προβολή πλήρων λεπτομερειών", + "View Logs": "Προβολή αρχείων καταγραφής", + "View Name": "Όνομα προβολής", + "View Object": "Προβολή Αντικειμένου", + "View Parameters": "Παράμετροι προβολής", + "View Selection": "Επιλογή προβολής", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Προβολή και ανάλυση αρχείων καταγραφής ίχνους αναζήτησης με προηγμένες δυνατότητες φιλτραρίσματος και αναλυτικών στοιχείων", + "View and analyze system audit trails with advanced filtering capabilities": "Προβολή και ανάλυση ιχνών ελέγχου συστήματος με προηγμένες δυνατότητες φιλτραρίσματος", + "View and manage authorization across registers and schemas": "Προβολή και διαχείριση εξουσιοδότησης σε όλα τα Μητρώα και Σχήματα", + "View deleted successfully": "Η προβολή διαγράφηκε με επιτυχία", + "View entity details and manage relations": "Προβολή λεπτομερειών οντότητας και διαχείριση σχέσεων", + "View name": "Όνομα προβολής", + "View name is required": "Το όνομα προβολής είναι υποχρεωτικό", + "View object": "Προβολή Αντικειμένου", + "View saved successfully!": "Η προβολή αποθηκεύτηκε με επιτυχία!", + "View search analytics and manage search logs": "Προβολή αναλυτικών στοιχείων αναζήτησης και διαχείριση αρχείων καταγραφής αναζήτησης", + "View successfully deleted": "Η προβολή διαγράφηκε με επιτυχία", + "View successfully updated": "Η προβολή ενημερώθηκε με επιτυχία", + "View updated successfully!": "Η προβολή ενημερώθηκε με επιτυχία!", + "View webhook delivery logs and filter by webhook": "Προβολή αρχείων καταγραφής παράδοσης webhook και φιλτράρισμα ανά webhook", + "View {count} more": "Προβολή {count} ακόμη", + "Views": "Προβολές", + "Visible to end users": "Ορατό στους τελικούς χρήστες", + "Visible to users": "Ορατό στους χρήστες", + "Wait for Response": "Αναμονή για απόκριση", + "Wait for webhook response before continuing (required for request/response flows)": "Αναμονή για απόκριση webhook πριν τη συνέχεια (απαιτείται για ροές αιτήματος/απόκρισης)", + "Waiting for cluster synchronization...": "Αναμονή για συγχρονισμό συμπλέγματος...", + "Warmup Names Cache": "Προθέρμανση κρυφής μνήμης ονομάτων", + "Warmup failed": "Η προθέρμανση απέτυχε", + "Warning": "Προειδοποίηση", + "Warning:": "Προειδοποίηση:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Προειδοποίηση: Αυτή η Ιδιότητα έχει ασυνεπείς τύπους: {types}. Εξετάστε αν όλα τα Αντικείμενα θα πρέπει να έχουν τον ίδιο τύπο.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Θα σαρώσουμε όλα τα Αντικείμενα που ανήκουν σε αυτό το Σχήμα για να ανακαλύψουμε νέες Ιδιότητες και να αναλύσουμε τις υπάρχουσες Ιδιότητες για πιθανές βελτιώσεις. Η διαδικασία περιλαμβάνει την εξέταση της δομής δεδομένων κάθε Αντικειμένου, τον εντοπισμό Ιδιοτήτων που δεν ορίζονται στο τρέχον Σχήμα και την εύρεση ευκαιριών για τη βελτίωση των υπαρχόντων ορισμών Ιδιοτήτων με καλύτερους περιορισμούς, μορφές και κανόνες επικύρωσης.", + "Webhook": "Webhook", + "Webhook Log Details": "Λεπτομέρειες αρχείου καταγραφής webhook", + "Webhook Logs": "Αρχεία καταγραφής webhook", + "Webhook created successfully": "Το webhook δημιουργήθηκε με επιτυχία", + "Webhook deleted": "Το webhook διαγράφηκε", + "Webhook retry delivered successfully": "Η επανάληψη του webhook παραδόθηκε με επιτυχία", + "Webhook retry delivery failed": "Η παράδοση της επανάληψης webhook απέτυχε", + "Webhook updated": "Το webhook ενημερώθηκε", + "Webhook updated successfully": "Το webhook ενημερώθηκε με επιτυχία", + "Webhooks": "Webhooks", + "Weekly": "Εβδομαδιαία", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Όταν ο πράκτορας εκτελείται χωρίς συνεδρία χρήστη (π.χ. προγραμματισμένες εργασίες), θα χρησιμοποιηθεί το πλαίσιο αυτού του χρήστη", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Όταν ο πράκτορας εκτελείται χωρίς συνεδρία χρήστη (π.χ. προγραμματισμένες εργασίες), θα χρησιμοποιηθεί το πλαίσιο αυτού του χρήστη.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Όταν είναι απενεργοποιημένη, αυτή η όψη θα εμφανίζει μόνο τιμές από αυτό το Σχήμα και θα περιλαμβάνει φίλτρο Σχήματος όταν επιλεγεί.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Όταν είναι ενεργοποιημένο, η αποθήκευση αυτού του Αντικειμένου θα ενημερώσει επίσης τα Αντικείμενα-στόχους ώστε να περιλαμβάνουν μια αναφορά πίσω σε αυτό το Αντικείμενο.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Όταν είναι ενεργοποιημένο, αυτή η Ιδιότητα θα αφαιρεθεί από το Αντικείμενο-πηγή μετά την ενημέρωση των Αντικειμένων-στόχων.", + "Workflow Management": "Διαχείριση ροών εργασίας", + "Workflows": "Ροές εργασίας", + "Wrap webhook payload in CloudEvents format for better interoperability": "Τύλιγμα του ωφέλιμου φορτίου webhook σε μορφή CloudEvents για καλύτερη διαλειτουργικότητα", + "Wrap webhook payload in cloudevents format for better interoperability": "Τύλιγμα του ωφέλιμου φορτίου webhook σε μορφή CloudEvents για καλύτερη διαλειτουργικότητα", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Ναι", + "You": "Εσείς", + "You are not yet a member of any organisations.": "Δεν είστε ακόμη μέλος κανενός οργανισμού.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Μπορείτε να δημιουργήσετε νέα ConfigSets βάσει του προτύπου _default ή να μεταφορτώσετε προσαρμοσμένα απευθείας στον διακομιστή SOLR σας.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Μπορείτε να δημιουργήσετε νέα ConfigSets βάσει του προτύπου _default ή να μεταφορτώσετε προσαρμοσμένα απευθείας στον διακομιστή SOLR σας.", + "You do not have access to this conversation": "Δεν έχετε πρόσβαση σε αυτή τη συνομιλία", + "You do not have permission to delete this conversation": "Δεν έχετε δικαίωμα να διαγράψετε αυτή τη συνομιλία", + "You do not have permission to modify this conversation": "Δεν έχετε δικαίωμα να τροποποιήσετε αυτή τη συνομιλία", + "You do not have permission to restore this conversation": "Δεν έχετε δικαίωμα να αποκαταστήσετε αυτή τη συνομιλία", + "You do not have permission to view the permission matrix. Admin access is required.": "Δεν έχετε δικαίωμα να προβάλετε τον πίνακα δικαιωμάτων. Απαιτείται πρόσβαση διαχειριστή.", + "You have selected {count} properties to add to the schema.": "Έχετε επιλέξει {count} Ιδιότητες για προσθήκη στο Σχήμα.", + "You must be logged in to favorite views": "Πρέπει να έχετε συνδεθεί για να ορίσετε προβολές ως αγαπημένες", + "You need an AI agent to start a conversation.": "Χρειάζεστε έναν πράκτορα AI για να ξεκινήσετε μια συνομιλία.", + "Your Fireworks AI API key. Get one at": "Το κλειδί API του Fireworks AI σας. Αποκτήστε ένα στο", + "Your OpenAI API key. Get one at": "Το κλειδί API του OpenAI σας. Αποκτήστε ένα στο", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Τα σχόλιά σας καταγράφηκαν. Προαιρετικά, μπορείτε να παράσχετε πρόσθετες λεπτομέρειες εδώ...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Τα σχόλιά σας καταγράφηκαν. προαιρετικά, μπορείτε να παράσχετε πρόσθετες λεπτομέρειες εδώ...", + "Zookeeper Hosts": "Κεντρικοί υπολογιστές Zookeeper", + "Zookeeper Port": "Θύρα Zookeeper", + "Zookeeper Settings (SolrCloud)": "Ρυθμίσεις Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Συμβολοσειρά σύνδεσης Zookeeper για SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Αριθμός θύρας Zookeeper (προαιρετικό, προεπιλογή 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ΠΡΟΕΙΔΟΠΟΙΗΣΗ ΟΡΙΣΤΙΚΗΣ ΔΙΑΓΡΑΦΗΣ \\u26A0\\uFE0F\n\nΠρόκειται να διαγράψετε ΟΡΙΣΤΙΚΑ ΟΛΑ τα Αντικείμενα για το Σχήμα \"{schema}\":\n\n\\u2022 Ενεργά Αντικείμενα: {active}\n\\u2022 Διαγραμμένα Αντικείμενα: {deleted}\n\\u2022 Σύνολο: {total}\n\nΑυτά τα Αντικείμενα θα αφαιρεθούν εντελώς από τη βάση δεδομένων και ΔΕΝ μπορούν να ανακτηθούν.\n\nΕίστε απολύτως βέβαιοι;", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ΠΡΟΕΙΔΟΠΟΙΗΣΗ ΟΡΙΣΤΙΚΗΣ ΔΙΑΓΡΑΦΗΣ \\u26A0\\uFE0F\n\nΠρόκειται να διαγράψετε ΟΡΙΣΤΙΚΑ {count} διαγραμμένα Αντικείμενα για το Σχήμα \"{schema}\".\n\nΑυτά τα Αντικείμενα θα αφαιρεθούν εντελώς από τη βάση δεδομένων και ΔΕΝ μπορούν να ανακτηθούν.\n\nΕίστε απολύτως βέβαιοι;", + "and accessible to all users in selected groups (or all users if no groups selected).": "και προσβάσιμα σε όλους τους χρήστες των επιλεγμένων ομάδων (ή σε όλους τους χρήστες αν δεν έχουν επιλεγεί ομάδες).", + "and add the files there.": "και προσθέστε τα Αρχεία εκεί.", + "approved": "εγκεκριμένο", + "are only accessible to invited users.": "είναι προσβάσιμα μόνο σε προσκεκλημένους χρήστες.", + "by": "από", + "chunk": "τμήμα", + "chunks": "τμήματα", + "chunks processed": "τμήματα επεξεργάστηκαν", + "completed": "ολοκληρώθηκε", + "configuration(s)": "διαμόρφωση(εις)", + "conversationId is required": "Το conversationId είναι υποχρεωτικό", + "create": "δημιουργία", + "delete": "διαγραφή", + "deleted objects section": "ενότητα διαγραμμένων Αντικειμένων", + "e.g., lib/Settings/config.json": "e.g., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "e.g., lib/Settings/register.json", + "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", + "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", + "execute": "εκτέλεση", + "failed": "απέτυχε", + "file": "αρχείο", + "files": "αρχεία", + "file{plural}": "αρχείο{plural}", + "folder": "φάκελος", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "σε {register}", + "in_progress": "σε εξέλιξη", + "inherited": "κληρονομημένο", + "list": "λίστα", + "log{plural}": "αρχείο καταγραφής{plural}", + "manage": "διαχείριση", + "members": "μέλη", + "menu or contact someone with permission to create agents.": "μενού ή επικοινωνήστε με κάποιον που έχει δικαίωμα δημιουργίας πρακτόρων.", + "message content is required": "Το περιεχόμενο του μηνύματος είναι υποχρεωτικό", + "messages": "μηνύματα", + "missing": "λείπει", + "myapp": "myapp", + "n8n URL and API key are required": "Το URL και το κλειδί API του n8n είναι υποχρεωτικά", + "n8n configuration saved successfully": "Η διαμόρφωση του n8n αποθηκεύτηκε με επιτυχία", + "n8n connection not configured": "Η σύνδεση n8n δεν έχει διαμορφωθεί", + "n8n connection successful": "Η σύνδεση n8n ήταν επιτυχής", + "n8n connection test failed: {message}": "Η δοκιμή σύνδεσης n8n απέτυχε: {message}", + "n8n connection test successful": "Η δοκιμή σύνδεσης n8n ήταν επιτυχής", + "n8n initialization failed: {message}": "Η αρχικοποίηση του n8n απέτυχε: {message}", + "n8n integration disabled": "Η ενσωμάτωση n8n απενεργοποιήθηκε", + "n8n integration enabled": "Η ενσωμάτωση n8n ενεργοποιήθηκε", + "n8n project initialized successfully": "Το έργο n8n αρχικοποιήθηκε με επιτυχία", + "n8n settings saved successfully": "Οι ρυθμίσεις n8n αποθηκεύτηκαν με επιτυχία", + "notify_push configuration guide": "οδηγός διαμόρφωσης notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "Το notify_push είναι εγκατεστημένο αλλά το OpenRegister δεν έχει επιβεβαιώσει ακόμη μια επιτυχημένη ώθηση. Ενεργοποιήστε μια αποθήκευση Αντικειμένου για να το ενεργοποιήσετε ή ελέγξτε τη διαμόρφωση του notify_push.", + "notify_push is installed but not yet active": "Το notify_push είναι εγκατεστημένο αλλά δεν είναι ακόμη ενεργό", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "αντικείμενα", + "objects processed": "αντικείμενα επεξεργάστηκαν", + "object{plural}": "αντικείμενο{plural}", + "or pick a range": "ή επιλέξτε ένα εύρος", + "org-...": "org-...", + "pending": "σε εκκρεμότητα", + "provides a good balance between speed and accuracy.": "προσφέρει καλή ισορροπία μεταξύ ταχύτητας και ακρίβειας.", + "public": "δημόσιο", + "read": "ανάγνωση", + "register(s)": "Μητρώο(α)", + "register(s) selected": "Μητρώο(α) επιλέχθηκαν", + "register{plural}": "Μητρώο{plural}", + "rejected": "απορρίφθηκε", + "required": "υποχρεωτικό", + "results": "αποτελέσματα", + "schema(s)": "Σχήμα(τα)", + "schema(s) selected": "Σχήμα(τα) επιλέχθηκαν", + "schema{plural}": "Σχήμα{plural}", + "selected objects": "επιλεγμένα Αντικείμενα", + "set": "ορισμός", + "sk-...": "sk-...", + "this application": "αυτή η εφαρμογή", + "to": "έως", + "type must be \"positive\" or \"negative\"": "ο τύπος πρέπει να είναι \"positive\" ή \"negative\"", + "update": "ενημέρωση", + "vectorized": "διανυσματοποιήθηκε", + "view": "προβολή", + "views selected": "προβολές επιλέχθηκαν", + "waiting": "αναμονή", + "{active} active, {deleted} deleted": "{active} ενεργά, {deleted} διαγραμμένα", + "{count} audit trails cleared successfully": "{count} ίχνη ελέγχου εκκαθαρίστηκαν με επιτυχία", + "{count} compliance issue(s) detected": "Εντοπίστηκαν {count} ζήτημα(τα) συμμόρφωσης", + "{count} deletions": "{count} διαγραφές", + "{count} documents found": "Βρέθηκαν {count} έγγραφα", + "{count} email": "{count} email", + "{count} emails": "{count} email", + "{count} entries": "{count} καταχωρήσεις", + "{count} issues": "{count} ζητήματα", + "{count} members": "{count} μέλη", + "{count} searches": "{count} αναζητήσεις", + "{count} selected": "{count} επιλέχθηκαν", + "{count} widget(s)": "{count} γραφικό στοιχείο(α)", + "{matched} object(s) matched. {erased} erased.": "{matched} Αντικείμενο(α) ταίριαξαν. {erased} διαγράφηκαν.", + "{minutes} minutes ago": "πριν από {minutes} λεπτά", + "{percentage}% of objects": "{percentage}% των Αντικειμένων", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} σε {register} / {schema}", + "{type} cache cleared successfully": "Η κρυφή μνήμη {type} εκκαθαρίστηκε με επιτυχία", + "ℹ️ Current Configuration": "ℹ️ Τρέχουσα διαμόρφωση", + "ℹ️ current configuration": "ℹ️ τρέχουσα διαμόρφωση", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ ΚΙΝΔΥΝΟΣ: Όλα τα {total} Αντικείμενα θα ΔΙΑΓΡΑΦΟΥΝ ΟΡΙΣΤΙΚΑ από τη βάση δεδομένων. Αυτή η ενέργεια είναι ΜΗ ΑΝΑΚΤΗΣΙΜΗ και δεν μπορεί να αναιρεθεί!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ΠΡΟΕΙΔΟΠΟΙΗΣΗ ΟΡΙΣΤΙΚΗΣ ΔΙΑΓΡΑΦΗΣ ⚠️\n\nΠρόκειται να διαγράψετε ΟΡΙΣΤΙΚΑ ΟΛΑ τα Αντικείμενα για το Σχήμα \"{schema}\":\n\n• Ενεργά Αντικείμενα: {active}\n• Διαγραμμένα Αντικείμενα: {deleted}\n• Σύνολο: {total}\n\nΑυτά τα Αντικείμενα θα αφαιρεθούν εντελώς από τη βάση δεδομένων και ΔΕΝ μπορούν να ανακτηθούν.\n\nΕίστε απολύτως βέβαιοι;", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ΠΡΟΕΙΔΟΠΟΙΗΣΗ ΟΡΙΣΤΙΚΗΣ ΔΙΑΓΡΑΦΗΣ ⚠️\n\nΠρόκειται να διαγράψετε ΟΡΙΣΤΙΚΑ {count} διαγραμμένα Αντικείμενα για το Σχήμα \"{schema}\".\n\nΑυτά τα Αντικείμενα θα αφαιρεθούν εντελώς από τη βάση δεδομένων και ΔΕΝ μπορούν να ανακτηθούν.\n\nΕίστε απολύτως βέβαιοι;", + "⚡ Batch Processing": "⚡ Μαζική επεξεργασία", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ Το SOLR μπορεί να διαχειριστεί εκατομμύρια έγγραφα με χρόνους απόκρισης αναζήτησης κάτω του δευτερολέπτου...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Ολοκληρωμένο πλαίσιο ✓ Λιγότερο πιθανό να παραλείψει λεπτομέρειες ✗ Πιο αργές αποκρίσεις ✗ Ενδέχεται να περιλαμβάνει λιγότερο σχετικές πληροφορίες", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Ταχύτερες αποκρίσεις ✓ Πιο εστιασμένες απαντήσεις ✗ Ενδέχεται να παραλείψει σχετικές πληροφορίες", + "✨ AI Features": "✨ Δυνατότητες AI", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Στην κατανεμημένη λειτουργία, το SOLR χρησιμοποιεί το ZooKeeper για τον συντονισμό του συμπλέγματος...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Η αναζήτηση κατά όψεις επιτρέπει στους χρήστες να εμβαθύνουν στα αποτελέσματα ανά κατηγορίες...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Γνωρίζατε; Το SOLR τροφοδοτεί την αναζήτηση για το Netflix, την Apple και πολλούς άλλους μεγάλους ιστότοπους!", + "💰 View Selection (Cost Optimization)": "💰 Επιλογή προβολής (Βελτιστοποίηση κόστους)", + "📄 Object Serialization": "📄 Σειριοποίηση Αντικειμένου", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 Το SOLR παρέχει πλούσια αναλυτικά στοιχεία και στατιστικά σχετικά με την απόδοση αναζήτησης...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Τα ConfigSets περιέχουν το Σχήμα και τα αρχεία διαμόρφωσης για το ευρετήριο αναζήτησής σας...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Η αναπαραγωγή διασφαλίζει ότι το ευρετήριο αναζήτησής σας είναι διαθέσιμο ακόμη και αν αποτύχουν κόμβοι...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 Το SOLR είναι μια ισχυρή επιχειρηματική πλατφόρμα αναζήτησης βασισμένη στο Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Ρυθμίσεις διανυσματοποίησης", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Η αυτόματη κλιμάκωση μπορεί να προσθέτει ή να αφαιρεί κόμβους δυναμικά ανάλογα με το φορτίο...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Οι δυνατότητες ασφαλείας περιλαμβάνουν έλεγχο ταυτότητας, εξουσιοδότηση και κρυπτογράφηση SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} email", + "{count} emails" + ], + "Purge {count} object from database": [ + "Εκκαθάριση {count} Αντικειμένου από τη βάση δεδομένων", + "Εκκαθάριση {count} Αντικειμένων από τη βάση δεδομένων" + ], + "Restore {count} object": [ + "Επαναφορά {count} Αντικειμένου", + "Επαναφορά {count} Αντικειμένων" + ], + "Successfully restored {count} object": [ + "Αποκαταστάθηκε με επιτυχία {count} Αντικείμενο", + "Αποκαταστάθηκαν με επιτυχία {count} Αντικείμενα" + ], + "Delete {count} object": [ + "Διαγραφή {count} Αντικειμένου", + "Διαγραφή {count} Αντικειμένων" + ], + "Object successfully deleted": [ + "Το Αντικείμενο διαγράφηκε με επιτυχία", + "Τα Αντικείμενα διαγράφηκαν με επιτυχία" + ] + } +} diff --git a/l10n/en.js b/l10n/en.js index 25bcda2dac..136c59462e 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -1,6 +1,27 @@ OC.L10N.register( "openregister", { + "(no subject)" : "(no subject)", + "(no title)" : "(no title)", + "(unnamed)" : "(unnamed)", + "(untitled card)" : "(untitled card)", + "Activities" : "Activities", + "Add Endpoint" : "Add Endpoint", + "Add Source" : "Add Source", + "Add card" : "Add card", + "Add contact" : "Add contact", + "All clear" : "All clear", + "Articles" : "Articles", + "Code" : "Code", + "Compliance" : "Compliance", + "Contacts" : "Contacts", + "Create Agent" : "Create Agent", + "Create Application" : "Create Application", + "Create event" : "Create event", + "Erasure complete" : "Erasure complete", + "Erasure preview" : "Erasure preview", + "Expand details" : "Expand details", + "Save changes" : "Save changes", "📄 Object Serialization" : "📄 Object Serialization", "🔢 Vectorization Settings" : "🔢 Vectorization Settings", "💰 View Selection (Cost Optimization)" : "💰 View Selection (Cost Optimization)", @@ -121,6 +142,7 @@ OC.L10N.register( "Before" : "Before", "Before object vectorization can work:" : "Before object vectorization can work:", "Behavior Issue" : "Behavior Issue", + "Bookmarks" : "Bookmarks", "Blob storage has been retired. All objects now use magic tables." : "Blob storage has been retired. All objects now use magic tables.", "Branch" : "Branch", "Bulk delete operation completed successfully" : "Bulk delete operation completed successfully", @@ -233,6 +255,7 @@ OC.L10N.register( "Copying..." : "Copying...", "Count" : "Count", "Counting objects..." : "Counting objects...", + "Costs" : "Costs", "Create" : "Create", "Create Collection" : "Create Collection", "Create ConfigSet" : "Create ConfigSet", @@ -310,6 +333,7 @@ OC.L10N.register( "Edit view details" : "Edit view details", "Effectiveness" : "Effectiveness", "Email" : "Email", + "Emails" : "Emails", "Embedding Model" : "Embedding Model", "Embedding Provider" : "Embedding Provider", "Embedding generated successfully" : "Embedding generated successfully", @@ -447,6 +471,10 @@ OC.L10N.register( "File extraction queued" : "File extraction queued", "File indexed successfully" : "File indexed successfully", "File is already anonymized" : "File is already anonymized", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", "File not found" : "File not found", "File settings updated successfully" : "File settings updated successfully", "File sources" : "File sources", @@ -454,6 +482,7 @@ OC.L10N.register( "File vectorization started. Check the statistics section for progress." : "File vectorization started. Check the statistics section for progress.", "File warmup completed" : "File warmup completed", "Files" : "Files", + "Files folder not found" : "Files folder not found", "Files with Completed Extraction:" : "Files with Completed Extraction:", "Files → fileCollection, Objects → objectCollection" : "Files → fileCollection, Objects → objectCollection", "Filter Audit Trails" : "Filter Audit Trails", @@ -478,6 +507,7 @@ OC.L10N.register( "Fixed" : "Fixed", "For chat and retrieval-augmented generation" : "For chat and retrieval-augmented generation", "For vector embeddings and semantic search" : "For vector embeddings and semantic search", + "Forbidden" : "Forbidden", "Format" : "Format", "Format Issue" : "Format Issue", "Format constraint is missing" : "Format constraint is missing", @@ -817,6 +847,7 @@ OC.L10N.register( "Person" : "Person", "Personal Data" : "Personal Data", "Phone" : "Phone", + "Photos" : "Photos", "Please create an agent in the" : "Please create an agent in the", "Please select an agent to continue" : "Please select an agent to continue", "Please select which register and schema to use for the new object" : "Please select which register and schema to use for the new object", @@ -826,6 +857,7 @@ OC.L10N.register( "Please wait while we fetch your configurations." : "Please wait while we fetch your configurations.", "Please wait while we fetch your deleted items." : "Please wait while we fetch your deleted items.", "Please wait while we fetch your sources." : "Please wait while we fetch your sources.", + "Polls" : "Polls", "Popular Search Terms" : "Popular Search Terms", "Port" : "Port", "Prerequisites" : "Prerequisites", @@ -1242,7 +1274,12 @@ OC.L10N.register( "Yes" : "Yes", "You" : "You", "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", "You do not have access to this conversation" : "You do not have access to this conversation", + "You do not have access to this object" : "You do not have access to this object", "You do not have permission to delete this conversation" : "You do not have permission to delete this conversation", "You do not have permission to modify this conversation" : "You do not have permission to modify this conversation", "You do not have permission to restore this conversation" : "You do not have permission to restore this conversation", @@ -1356,7 +1393,22 @@ OC.L10N.register( "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.", "notify_push configuration guide" : "notify_push configuration guide", "Realtime push active" : "Realtime push active", - "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/en.json b/l10n/en.json index 5e89d52b10..012054ff01 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -1,15 +1,30 @@ { "translations": { - "📄 Object Serialization": "📄 Object Serialization", - "🔢 Vectorization Settings": "🔢 Vectorization Settings", - "💰 View Selection (Cost Optimization)": "💰 View Selection (Cost Optimization)", "3": "3", "30": "30", + "(Dolphin required)": "(Dolphin required)", + "(no subject)": "(no subject)", + "(no title)": "(no title)", + "(unnamed)": "(unnamed)", + "(untitled card)": "(untitled card)", + "({count} selected)": "({count} selected)", + ", {failed} failed": ", {failed} failed", + "0 = unlimited": "0 = unlimited", + "A deactivation request is pending.": "A deactivation request is pending.", "AI Assistant": "AI Assistant", "AI Chat": "AI Chat", + "AI service not configured": "AI service not configured", "API Key": "API Key", + "API Request Quota (requests/day)": "API Request Quota (requests/day)", + "API Token Configuration": "API Token Configuration", + "API Tokens": "API Tokens", "API tokens saved successfully": "API tokens saved successfully", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.", "About ConfigSets": "About ConfigSets", + "Access denied": "Access denied", + "Access organisation dashboard and analytics": "Access organisation dashboard and analytics", + "Account": "Account", "Action": "Action", "Action Distribution": "Action Distribution", "Action:": "Action:", @@ -17,26 +32,50 @@ "Active": "Active", "Active Collections": "Active Collections", "Active Filters": "Active Filters", + "Active Organisation:": "Active Organisation:", "Active Organisations": "Active Organisations", "Active filters:": "Active filters:", + "Active organisation changed successfully": "Active organisation changed successfully", "Active organisation set successfully": "Active organisation set successfully", + "Activities": "Activities", + "Activity": "Activity", "Activity Period": "Activity Period", + "Add": "Add", + "Add Attachment": "Add Attachment", + "Add Endpoint": "Add Endpoint", "Add Groups": "Add Groups", + "Add Property to '{title}'": "Add Property to '{title}'", + "Add Register": "Add Register", "Add Schema": "Add Schema", + "Add Source": "Add Source", + "Add User": "Add User", + "Add User to Organisation": "Add User to Organisation", + "Add a contact from any of your address books to associate it with this object.": "Add a contact from any of your address books to associate it with this object.", + "Add a file or files": "Add a file or files", + "Add card": "Add card", + "Add contact": "Add contact", + "Add endpoint": "Add endpoint", + "Add oneOf entry": "Add oneOf entry", + "Add range": "Add range", "Add schema titles, descriptions, and register information to provide richer context for search": "Add schema titles, descriptions, and register information to provide richer context for search", "Add to favorites": "Add to favorites", "Added": "Added", "Added to favorites": "Added to favorites", "Additional Information": "Additional Information", "Additional feedback saved. Thank you!": "Additional feedback saved. Thank you!", + "Addresses": "Addresses", "Admin": "Admin", "Advanced": "Advanced", "Advanced Filters": "Advanced Filters", "Advanced Options": "Advanced Options", "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.", "After": "After", + "Agent Type": "Agent Type", "Agent deleted successfully": "Agent deleted successfully", + "Agent successfully created": "Agent successfully created", + "Agent successfully updated": "Agent successfully updated", "Agents": "Agents", + "Aggregated across schemas": "Aggregated across schemas", "All": "All", "All Categories": "All Categories", "All Collections": "All Collections", @@ -48,14 +87,23 @@ "All Webhooks": "All Webhooks", "All actions": "All actions", "All audit trails cleared successfully": "All audit trails cleared successfully", + "All clear": "All clear", + "All file types": "All file types", "All registers": "All registers", "All schemas": "All schemas", + "All search trails cleared successfully": "All search trails cleared successfully", "All searches": "All searches", "All users": "All users", + "All-day events": "All-day events", + "Allowed MIME Types": "Allowed MIME Types", + "Already linked": "Already linked", "An error occurred while clearing audit trails": "An error occurred while clearing audit trails", "An error occurred while deleting the audit trail": "An error occurred while deleting the audit trail", + "An error occurred while deleting the object": "An error occurred while deleting the object", "An error occurred while deleting the view": "An error occurred while deleting the view", "An error occurred while permanently deleting the objects": "An error occurred while permanently deleting the objects", + "An error occurred while restoring the objects": "An error occurred while restoring the objects", + "An error occurred while saving the organisation": "An error occurred while saving the organisation", "Analysis completed:": "Analysis completed:", "Analysis steps:": "Analysis steps:", "Analytics": "Analytics", @@ -63,22 +111,49 @@ "Analyze Properties": "Analyze Properties", "Analyze existing properties for improvement opportunities": "Analyze existing properties for improvement opportunities", "Analyzing...": "Analyzing...", + "Anonymized": "Anonymized", + "Anonymous Group": "Anonymous Group", "Any favorites and sharing settings for this view": "Any favorites and sharing settings for this view", "Any user": "Any user", + "App ID": "App ID", "App store cache invalidated successfully": "App store cache invalidated successfully", + "App store cache invalidated: {invalidated}": "App store cache invalidated: {invalidated}", "Application deleted successfully": "Application deleted successfully", + "Application identifier for this configuration (optional)": "Application identifier for this configuration (optional)", + "Application settings": "Application settings", + "Application successfully created": "Application successfully created", + "Application successfully updated": "Application successfully updated", "Applications": "Applications", + "Apply": "Apply", "Apply Changes": "Apply Changes", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Apply a role to all schemas in this register that do not have explicit authorization overrides.", + "Approval Progress": "Approval Progress", + "Approve": "Approve", + "Approved": "Approved", "Archive": "Archive", "Archive conversation": "Archive conversation", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.", "Archived conversations are hidden from your active list": "Archived conversations are hidden from your active list", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone", "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.", "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Are you sure you want to delete the selected audit trails? This action cannot be undone.", "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Are you sure you want to delete the selected search trails? This action cannot be undone.", "Are you sure you want to delete this ConfigSet?": "Are you sure you want to delete this ConfigSet?", + "Are you sure you want to delete this configset?": "Are you sure you want to delete this configset?", + "Are you sure you want to leave '{name}'?": "Are you sure you want to leave '{name}'?", "Are you sure you want to permanently delete": "Are you sure you want to permanently delete", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.", + "Array Object Configuration:": "Array Object Configuration:", + "Articles": "Articles", "Ask a question...": "Ask a question...", "Ask questions about your data using natural language": "Ask questions about your data using natural language", + "Assigned collaborative tags": "Assigned collaborative tags", + "Assignment notifications": "Assignment notifications", + "Attachment added to {name}": "Attachment added to {name}", "Attempt": "Attempt", "Audit Trail Changes": "Audit Trail Changes", "Audit Trail Details": "Audit Trail Details", @@ -89,17 +164,26 @@ "Audit trail deleted successfully": "Audit trail deleted successfully", "Audit trail successfully deleted": "Audit trail successfully deleted", "Authentication": "Authentication", + "Auto (e.g. Y for year, F Y for month)": "Auto (e.g. Y for year, F Y for month)", + "Auto share": "Auto share", "Auto-commit disabled": "Auto-commit disabled", "Auto-commit enabled": "Auto-commit enabled", "Auto-create Default Organisation": "Auto-create Default Organisation", "Auto-retry failed vectorizations": "Auto-retry failed vectorizations", "Automatically commit changes to SOLR index": "Automatically commit changes to SOLR index", + "Automatically connect to the federation network.": "Automatically connect to the federation network.", "Automatically create a default organisation if none exists when the app is initialized": "Automatically create a default organisation if none exists when the app is initialized", "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automatically generate vector embeddings from text chunks when files are uploaded and processed", "Automatically generate vector embeddings when objects are created or updated": "Automatically generate vector embeddings when objects are created or updated", "Automatically retry failed vectorization attempts (max 3 retries)": "Automatically retry failed vectorization attempts (max 3 retries)", + "Automatically update catalogues.": "Automatically update catalogues.", "Available Filters": "Available Filters", "Available Workflows": "Available Workflows", + "Available placeholders:": "Available placeholders:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Avatar changes are not supported by your authentication provider.", + "Avatar removed": "Avatar removed", + "Avatar updated successfully": "Avatar updated successfully", "Avg Execution Time": "Avg Execution Time", "Avg Members/Org": "Avg Members/Org", "Avg Object Views/Session": "Avg Object Views/Session", @@ -112,33 +196,62 @@ "Back to Webhooks": "Back to Webhooks", "Back to applications": "Back to applications", "Back to entities": "Back to entities", + "Backend parameter is required": "Backend parameter is required", "Backend updated successfully. Please reload the application.": "Backend updated successfully. Please reload the application.", + "Bandwidth Quota (MB/month)": "Bandwidth Quota (MB/month)", "Base URL (Optional)": "Base URL (Optional)", "Basic Information": "Basic Information", "Batch Size": "Batch Size", "Batch extraction completed": "Batch extraction completed", "Before": "Before", "Before object vectorization can work:": "Before object vectorization can work:", + "Behavior": "Behavior", "Behavior Issue": "Behavior Issue", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)", "Blob storage has been retired. All objects now use magic tables.": "Blob storage has been retired. All objects now use magic tables.", + "Bookmarks": "Bookmarks", "Branch": "Branch", + "Bulk Role Assignment: {title}": "Bulk Role Assignment: {title}", "Bulk delete operation completed successfully": "Bulk delete operation completed successfully", "Bulk save operation completed successfully": "Bulk save operation completed successfully", "Business Data": "Business Data", + "CSV": "CSV", "Cache cleared successfully": "Cache cleared successfully", + "Cache grew from {old} to {new} entries.": "Cache grew from {old} to {new} entries.", + "Cache shrunk from {old} to {new} entries.": "Cache shrunk from {old} to {new} entries.", + "Cache stayed the same at {size} entries.": "Cache stayed the same at {size} entries.", "Calculate Sizes": "Calculate Sizes", + "Calendar": "Calendar", + "Calendar Provider Configuration": "Calendar Provider Configuration", + "Calendar integration is not available": "Calendar integration is not available", + "Calendar name": "Calendar name", "Cancel": "Cancel", + "Cancel deactivation request": "Cancel deactivation request", "Cannot delete: objects are still attached": "Cannot delete: objects are still attached", + "Cannot edit: This register is managed by external configuration {title}": "Cannot edit: This register is managed by external configuration {title}", + "Cannot remove schema with existing objects ({count} objects)": "Cannot remove schema with existing objects ({count} objects)", + "Cannot remove schema {schema} because it contains {count} objects": "Cannot remove schema {schema} because it contains {count} objects", + "Cards": "Cards", + "Cascade delete": "Cascade delete", + "Categories of data subjects (one per line)": "Categories of data subjects (one per line)", + "Categories of personal data (one per line)": "Categories of personal data (one per line)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", "Category": "Category", "Change Type": "Change Type", + "Change password": "Change password", "Changes": "Changes", "Changes copied to clipboard": "Changes copied to clipboard", + "Changes for Audit Trail #{id}": "Changes for Audit Trail #{id}", "Chat Model": "Chat Model", "Chat Provider": "Chat Provider", "Chat Provider (RAG)": "Chat Provider (RAG)", "Chat Settings": "Chat Settings", "Chat provider connection successful!": "Chat provider connection successful!", "Chat settings": "Chat settings", + "Chat test failed: {error}": "Chat test failed: {error}", "Choose a register": "Choose a register", "Choose a schema": "Choose a schema", "Choose how vector similarity calculations are performed for semantic search": "Choose how vector similarity calculations are performed for semantic search", @@ -150,6 +263,8 @@ "Chunks to Vectorize:": "Chunks to Vectorize:", "Cleanup Old Trails": "Cleanup Old Trails", "Cleanup completed": "Cleanup completed", + "Cleanup completed successfully. Deleted {count} entries.": "Cleanup completed successfully. Deleted {count} entries.", + "Cleanup failed: {error}": "Cleanup failed: {error}", "Clear All Embeddings": "Clear All Embeddings", "Clear Entries": "Clear Entries", "Clear Filtered Audit Trails": "Clear Filtered Audit Trails", @@ -159,8 +274,16 @@ "Clear all": "Clear all", "Clear all filters": "Clear all filters", "Clear date range": "Clear date range", + "Clear filters": "Clear filters", "Clearing...": "Clearing...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.", + "Click to subscribe to notifications": "Click to subscribe to notifications", + "Click to unsubscribe from notifications": "Click to unsubscribe from notifications", "Close": "Close", + "Cluster name": "Cluster name", + "Code": "Code", + "Code (short readable key, e.g. v-2026-001)": "Code (short readable key, e.g. v-2026-001)", + "Collapse details": "Collapse details", "Collection Management": "Collection Management", "Collection Name": "Collection Name", "Collection assignments updated successfully": "Collection assignments updated successfully", @@ -171,6 +294,8 @@ "Collection used to store and index file metadata and content": "Collection used to store and index file metadata and content", "Collection used to store and index object data": "Collection used to store and index object data", "Collections:": "Collections:", + "Color": "Color", + "Comment...": "Comment...", "Commit Message": "Commit Message", "Commit Within (ms)": "Commit Within (ms)", "Commit message": "Commit message", @@ -178,8 +303,11 @@ "Completed": "Completed", "Complex": "Complex", "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations", + "Compliance": "Compliance", "Confidence Level": "Confidence Level", "Confidence Score": "Confidence Score", + "Confidence: {confidence}%": "Confidence: {confidence}%", "Config must be provided as an object": "Config must be provided as an object", "ConfigSet": "ConfigSet", "ConfigSet Management": "ConfigSet Management", @@ -190,16 +318,34 @@ "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.", "Configuration": "Configuration", "Configuration Data": "Configuration Data", + "Configuration is up to date": "Configuration is up to date", "Configuration saved": "Configuration saved", + "Configuration saved successfully.": "Configuration saved successfully.", + "Configuration type (default, application, etc.)": "Configuration type (default, application, etc.)", "Configurations": "Configurations", + "Configure API tokens for external service integrations": "Configure API tokens for external service integrations", + "Configure Apache SOLR search engine for advanced search capabilities": "Configure Apache SOLR search engine for advanced search capabilities", "Configure Facets": "Configure Facets", "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.", + "Configure SOLR Facets": "Configure SOLR Facets", "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.", + "Configure default organisation and organisation-related settings": "Configure default organisation and organisation-related settings", "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.", "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Configure how objects are converted to text before vectorization. These settings affect search quality and context.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.", + "Configure multi-organization support and tenant isolation": "Configure multi-organization support and tenant isolation", "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.", "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configure this schema to surface objects as events in the Nextcloud Calendar app.", "Configure which types of data to search and how many sources to retrieve": "Configure which types of data to search and how many sources to retrieve", + "Configuring field mappings...": "Configuring field mappings...", + "Confirm Account Deactivation": "Confirm Account Deactivation", + "Confirm Object Deletion": "Confirm Object Deletion", + "Confirm Object Restoration": "Confirm Object Restoration", + "Confirm Permanent Object Deletion": "Confirm Permanent Object Deletion", + "Confirm deactivation": "Confirm deactivation", + "Connecting to SOLR cluster...": "Connecting to SOLR cluster...", "Connection Failed": "Connection Failed", "Connection Settings": "Connection Settings", "Connection Successful!": "Connection Successful!", @@ -209,8 +355,12 @@ "Connection timeout in seconds": "Connection timeout in seconds", "Constant delay between retries (5 minutes)": "Constant delay between retries (5 minutes)", "Constraint Issue": "Constraint Issue", + "Consumer not found": "Consumer not found", + "Contacts": "Contacts", + "Contacts integration is not available": "Contacts integration is not available", "Content Filters": "Content Filters", "Continue to Properties": "Continue to Properties", + "Contracts": "Contracts", "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.", "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.", "Controls randomness (0 = deterministic, 2 = very creative)": "Controls randomness (0 = deterministic, 2 = very creative)", @@ -219,62 +369,117 @@ "Conversation archived successfully": "Conversation archived successfully", "Conversation cleared successfully": "Conversation cleared successfully", "Conversation deleted": "Conversation deleted", + "Conversation not found": "Conversation not found", "Conversation permanently deleted": "Conversation permanently deleted", "Conversation renamed": "Conversation renamed", "Conversation restored": "Conversation restored", + "Conversation started with {agent}": "Conversation started with {agent}", "Conversation title": "Conversation title", + "Conversations": "Conversations", + "Converting {schema} to blob storage...": "Converting {schema} to blob storage...", + "Converting {schema} to magic table...": "Converting {schema} to magic table...", "Copied!": "Copied!", "Copy": "Copy", "Copy Changes": "Copy Changes", "Copy Collection": "Copy Collection", "Copy Data": "Copy Data", "Copy Full Data": "Copy Full Data", + "Copy of {name}": "Copy of {name}", + "Copy to clipboard": "Copy to clipboard", "Copying...": "Copying...", + "Costs": "Costs", "Count": "Count", "Counting objects...": "Counting objects...", "Create": "Create", + "Create API Token": "Create API Token", + "Create Agent": "Create Agent", + "Create Application": "Create Application", "Create Collection": "Create Collection", "Create ConfigSet": "Create ConfigSet", "Create New Collection": "Create New Collection", "Create New ConfigSet": "Create New ConfigSet", "Create Operations": "Create Operations", + "Create Organisation": "Create Organisation", "Create Webhook": "Create Webhook", "Create a copy of collection:": "Create a copy of collection:", "Create a new ConfigSet based on the _default template": "Create a new ConfigSet based on the _default template", "Create a new SOLR collection from an existing ConfigSet": "Create a new SOLR collection from an existing ConfigSet", + "Create a new SOLR collection from an existing configset": "Create a new SOLR collection from an existing configset", + "Create a new configset based on the _default template": "Create a new configset based on the _default template", + "Create a new event or link an existing one from any of your calendars.": "Create a new event or link an existing one from any of your calendars.", + "Create another": "Create another", + "Create copies of": "Create copies of", + "Create event": "Create event", + "Create new token": "Create new token", + "Create or link a Deck card to track work on this object.": "Create or link a Deck card to track work on this object.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.", "Create your first AI agent to get started.": "Create your first AI agent to get started.", "Created": "Created", "Created:": "Created:", + "Creating search collections...": "Creating search collections...", "Creating...": "Creating...", + "Current": "Current", "Current Filters": "Current Filters", "Current Type:": "Current Type:", + "Current password": "Current password", "Custom API endpoint if using a different region": "Custom API endpoint if using a different region", "Custom HTTP headers (one per line, format: Header-Name: value)": "Custom HTTP headers (one per line, format: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Custom HTTP headers (one per line, format: header-name: value)", + "Custom Ranges:": "Custom Ranges:", + "Custom display title for this facet": "Custom display title for this facet", + "Custom pattern:": "Custom pattern:", + "DELETE COLLECTION": "DELETE COLLECTION", + "DSAR": "DSAR", "Daily": "Daily", "Dashboard": "Dashboard", + "Dashboard not found": "Dashboard not found", + "Data": "Data", "Data Source": "Data Source", + "Data Sources": "Data Sources", + "Data Views": "Data Views", "Data sources": "Data sources", "Data type does not match observed values": "Data type does not match observed values", + "Data-subject access request": "Data-subject access request", "Database URL": "Database URL", "Database information refreshed": "Database information refreshed", + "Date Faceting:": "Date Faceting:", "Date Range": "Date Range", + "Dates": "Dates", + "Deactivation request cancelled": "Deactivation request cancelled", + "Deactivation request submitted": "Deactivation request submitted", + "Deck": "Deck", + "Deck integration is not available": "Deck integration is not available", "Default": "Default", + "Default Object Tenant": "Default Object Tenant", "Default Organisation": "Default Organisation", + "Default Owner": "Default Owner", + "Default User (for cron/background jobs)": "Default User (for cron/background jobs)", + "Default User Tenant": "Default User Tenant", + "Default value": "Default value", + "Default value (Email)": "Default value (Email)", + "Default value (Password)": "Default value (Password)", + "Default value (Phone number)": "Default value (Phone number)", + "Default value (Regex)": "Default value (Regex)", "Default: 5": "Default: 5", + "Defaults to current user. Select a different user if needed.": "Defaults to current user. Select a different user if needed.", "Delays double with each attempt (2, 4, 8 minutes...)": "Delays double with each attempt (2, 4, 8 minutes...)", "Delays increase linearly (5, 10, 15 minutes...)": "Delays increase linearly (5, 10, 15 minutes...)", "Delete": "Delete", + "Delete ({count})": "Delete ({count})", "Delete Application": "Delete Application", "Delete Audit Trail": "Delete Audit Trail", "Delete Collection": "Delete Collection", "Delete ConfigSet": "Delete ConfigSet", "Delete Objects": "Delete Objects", "Delete Operations": "Delete Operations", + "Delete SOLR Collection": "Delete SOLR Collection", "Delete View": "Delete View", "Delete all objects in this schema": "Delete all objects in this schema", "Delete permanently": "Delete permanently", "Delete this conversation?": "Delete this conversation?", "Delete view": "Delete view", + "Delete {count} object": "Delete {count} object", + "Delete {count} objects": "Delete {count} objects", "Deleted": "Deleted", "Deleted By": "Deleted By", "Deleted Date": "Deleted Date", @@ -282,70 +487,136 @@ "Deleted This Week": "Deleted This Week", "Deleted Today": "Deleted Today", "Deleted:": "Deleted:", + "Deleting SOLR Collection...": "Deleting SOLR Collection...", "Deleting...": "Deleting...", "Deletion Date Range": "Deletion Date Range", "Deletion Statistics": "Deletion Statistics", "Deprecated": "Deprecated", + "Depublish": "Depublish", "Description": "Description", + "Description Template": "Description Template", + "Description shown as tooltip": "Description shown as tooltip", "Description:": "Description:", "Detect data types and patterns": "Detect data types and patterns", "Detected At": "Detected At", "Detected Format:": "Detected Format:", "Detected Issues:": "Detected Issues:", + "Detected: {date}": "Detected: {date}", + "Direct permission": "Direct permission", "Disable": "Disable", + "Disable this to make the agent": "Disable this to make the agent", "Disabled": "Disabled", "Discover Files": "Discover Files", "Discovered Properties": "Discovered Properties", + "Display Format": "Display Format", + "Display Name": "Display Name", "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.", "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Do you want to permanently delete this audit trail entry? This action cannot be undone.", "Documents": "Documents", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR enabled", "Download API Spec": "Download API Spec", + "Download API Specification": "Download API Specification", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).", + "Drag a file or files here": "Drag a file or files here", + "Dry run completed: {count} fields would be created": "Dry run completed: {count} fields would be created", + "Dry run completed: {count} fields would be fixed": "Dry run completed: {count} fields would be fixed", + "Duration in seconds (optional)": "Duration in seconds (optional)", "Edit": "Edit", + "Edit Agent": "Edit Agent", + "Edit Application": "Edit Application", + "Edit Configuration": "Edit Configuration", + "Edit Endpoint": "Edit Endpoint", + "Edit Organisation": "Edit Organisation", + "Edit Property '{key}' of '{title}'": "Edit Property '{key}' of '{title}'", "Edit Register": "Edit Register", "Edit Schema": "Edit Schema", + "Edit Source": "Edit Source", "Edit View": "Edit View", "Edit Webhook": "Edit Webhook", + "Edit labels": "Edit labels", + "Edit processing activity": "Edit processing activity", + "Edit tags for {name}": "Edit tags for {name}", + "Edit verwerkingsactiviteit": "Edit verwerkingsactiviteit", "Edit view": "Edit view", "Edit view details": "Edit view details", "Effectiveness": "Effectiveness", "Email": "Email", + "Email Addresses": "Email Addresses", + "Email digest frequency": "Email digest frequency", + "Emails": "Emails", "Embedding Model": "Embedding Model", "Embedding Provider": "Embedding Provider", "Embedding generated successfully": "Embedding generated successfully", "Embedding provider connection successful!": "Embedding provider connection successful!", + "Embedding test failed: {error}": "Embedding test failed: {error}", "Enable": "Enable", + "Enable RAG": "Enable RAG", "Enable auto-creation": "Enable auto-creation", "Enable automatic file vectorization": "Enable automatic file vectorization", "Enable automatic object vectorization": "Enable automatic object vectorization", "Enable automatic synchronization": "Enable automatic synchronization", + "Enable calendar provider": "Enable calendar provider", "Enable detailed logging for SOLR operations (recommended for debugging)": "Enable detailed logging for SOLR operations (recommended for debugging)", "Enable faceting": "Enable faceting", "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.", "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.", "Enable or disable n8n workflow integration. Configure connection settings below.": "Enable or disable n8n workflow integration. Configure connection settings below.", "Enable or disable this webhook": "Enable or disable this webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Enable tools that allow the agent to interact with data through function calling.", "Enable vectorization for all existing and future views (may increase costs)": "Enable vectorization for all existing and future views (may increase costs)", + "Enable write-back to target objects": "Enable write-back to target objects", "Enabled": "Enabled", + "End Date Field": "End Date Field", + "Endpoint Path*": "Endpoint Path*", "Endpoints": "Endpoints", + "Engine not found": "Engine not found", "Enter ConfigSet name": "Enter ConfigSet name", + "Enter Nextcloud usernames to grant access to this private agent": "Enter Nextcloud usernames to grant access to this private agent", + "Enter agent description (optional)": "Enter agent description (optional)", + "Enter agent name": "Enter agent name", + "Enter application description (optional)": "Enter application description (optional)", + "Enter application name": "Enter application name", "Enter collection name": "Enter collection name", + "Enter configuration description (optional)": "Enter configuration description (optional)", + "Enter configuration title": "Enter configuration title", "Enter description (optional)...": "Enter description (optional)...", "Enter new collection name": "Enter new collection name", "Enter object ID": "Enter object ID", + "Enter organisation description (optional)": "Enter organisation description (optional)", + "Enter organisation name": "Enter organisation name", "Enter search term": "Enter search term", + "Enter search terms or leave empty to browse all": "Enter search terms or leave empty to browse all", + "Enter system prompt for the agent": "Enter system prompt for the agent", + "Enter username and press Enter": "Enter username and press Enter", "Enter view name...": "Enter view name...", "Enter webhook name": "Enter webhook name", + "Enter your API key": "Enter your API key", + "Enter your object here...": "Enter your object here...", + "Enter your schema here...": "Enter your schema here...", "Entities": "Entities", + "Entities detected": "Entities detected", + "Entity ID: {id}": "Entity ID: {id}", "Entity Information": "Entity Information", + "Entity Recognition": "Entity Recognition", + "Entity Recognition Method": "Entity Recognition Method", "Entity deleted successfully": "Entity deleted successfully", "Entity not found": "Entity not found", "Entries to be deleted:": "Entries to be deleted:", "Enum Issue": "Enum Issue", "Enum constraint is missing": "Enum constraint is missing", + "Erase (Art 17)": "Erase (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Erase {count} object(s) for this subject? This action is logged in the audit trail.", + "Erasure complete": "Erasure complete", + "Erasure preview": "Erasure preview", "Error": "Error", "Error Details": "Error Details", "Error Information": "Error Information", "Error Message": "Error Message", + "Error deleting audit trails: {error}": "Error deleting audit trails: {error}", + "Error deleting search trails: {error}": "Error deleting search trails: {error}", "Error loading application": "Error loading application", "Error loading audit trails": "Error loading audit trails", "Error loading entity": "Error loading entity", @@ -356,85 +627,210 @@ "Event": "Event", "Event Property for Payload": "Event Property for Payload", "Events": "Events", + "Every schema with detected PII has a processing-activity annotation.": "Every schema with detected PII has a processing-activity annotation.", + "Example": "Example", "Example Value": "Example Value", + "Example value for this property": "Example value for this property", + "Excel (.xlsx)": "Excel (.xlsx)", "Exclusive Maximum": "Exclusive Maximum", "Exclusive Minimum": "Exclusive Minimum", + "Exclusive maximum": "Exclusive maximum", + "Exclusive minimum": "Exclusive minimum", "Execution Mode": "Execution Mode", "Execution Time": "Execution Time", "Execution Time Range": "Execution Time Range", "Existing Improvements": "Existing Improvements", + "Expand details": "Expand details", + "Expiration": "Expiration", + "Expires": "Expires", + "Expires in (e.g., 90d)": "Expires in (e.g., 90d)", "Exponential": "Exponential", "Export": "Export", + "Export \"{schema}\" objects from \"{register}\"": "Export \"{schema}\" objects from \"{register}\"", + "Export Configuration": "Export Configuration", + "Export Format:": "Export Format:", + "Export Objects": "Export Objects", "Export completed successfully": "Export completed successfully", + "Export downloaded successfully": "Export downloaded successfully", + "Export failed: {error}": "Export failed: {error}", + "Export is rate limited. Please try again later.": "Export is rate limited. Please try again later.", + "Export my data": "Export my data", "Export, view, or delete audit trails": "Export, view, or delete audit trails", + "Exporting...": "Exporting...", "Extend Schema": "Extend Schema", "Extended": "Extended", "Extensions": "Extensions", + "Extra Query Parameters": "Extra Query Parameters", + "Extract Now": "Extract Now", "Extract Pending Files": "Extract Pending Files", + "Extract now": "Extract now", "Extract properties from each object": "Extract properties from each object", "Extracted At": "Extracted At", + "Extracted at": "Extracted at", + "Extraction": "Extraction", + "Extraction Mode": "Extraction Mode", + "Extraction Scope": "Extraction Scope", "Extraction Status": "Extraction Status", + "Extraction error for {file}: {error}": "Extraction error for {file}: {error}", + "Facet Description": "Facet Description", + "Facet Order": "Facet Order", + "Facet Title": "Facet Title", + "Facet Type": "Facet Type", "Facet configuration updated successfully": "Facet configuration updated successfully", + "Facetable": "Facetable", + "Faceting Configuration:": "Faceting Configuration:", "Facets discovered and configured successfully": "Facets discovered and configured successfully", "Facets discovered successfully": "Facets discovered successfully", "Failed": "Failed", + "Failed to add attachment to object": "Failed to add attachment to object", + "Failed to add user to organisation": "Failed to add user to organisation", "Failed to analyze schema properties": "Failed to analyze schema properties", "Failed to archive conversation": "Failed to archive conversation", "Failed to calculate sizes": "Failed to calculate sizes", + "Failed to cancel deactivation": "Failed to cancel deactivation", + "Failed to change active organisation: {error}": "Failed to change active organisation: {error}", + "Failed to change password": "Failed to change password", + "Failed to check version: {error}": "Failed to check version: {error}", + "Failed to clear audit trails: {error}": "Failed to clear audit trails: {error}", + "Failed to clear blob objects: {error}": "Failed to clear blob objects: {error}", + "Failed to clear cache: {error}": "Failed to clear cache: {error}", "Failed to clear collection": "Failed to clear collection", + "Failed to clear conversation": "Failed to clear conversation", + "Failed to clear embeddings: {error}": "Failed to clear embeddings: {error}", + "Failed to clear search trails: {error}": "Failed to clear search trails: {error}", + "Failed to clear {type} cache: {error}": "Failed to clear {type} cache: {error}", "Failed to copy changes": "Failed to copy changes", "Failed to copy data": "Failed to copy data", "Failed to copy data to clipboard": "Failed to copy data to clipboard", + "Failed to copy to clipboard": "Failed to copy to clipboard", + "Failed to copy token": "Failed to copy token", "Failed to create conversation": "Failed to create conversation", + "Failed to create missing SOLR fields: {error}": "Failed to create missing SOLR fields: {error}", "Failed to create or find project": "Failed to create or find project", + "Failed to create token": "Failed to create token", + "Failed to delete SOLR collection: {error}": "Failed to delete SOLR collection: {error}", "Failed to delete collection": "Failed to delete collection", "Failed to delete conversation": "Failed to delete conversation", + "Failed to delete objects for {schema}: {error}": "Failed to delete objects for {schema}: {error}", "Failed to delete webhook": "Failed to delete webhook", + "Failed to depublish register: {error}": "Failed to depublish register: {error}", + "Failed to depublish schema: {error}": "Failed to depublish schema: {error}", + "Failed to depublish: {error}": "Failed to depublish: {error}", + "Failed to discover files: {error}": "Failed to discover files: {error}", "Failed to download API specification": "Failed to download API specification", + "Failed to export data": "Failed to export data", + "Failed to export objects": "Failed to export objects", + "Failed to fetch conversation history": "Failed to fetch conversation history", + "Failed to fix mismatched SOLR fields: {error}": "Failed to fix mismatched SOLR fields: {error}", "Failed to get SOLR field configuration": "Failed to get SOLR field configuration", + "Failed to get chat statistics": "Failed to get chat statistics", + "Failed to get database information: %s": "Failed to get database information: %s", "Failed to get entity categories": "Failed to get entity categories", "Failed to get entity statistics": "Failed to get entity statistics", "Failed to get entity types": "Failed to get entity types", + "Failed to get response: {error}": "Failed to get response: {error}", + "Failed to invalidate app store cache: {error}": "Failed to invalidate app store cache: {error}", + "Failed to leave organisation: {error}": "Failed to leave organisation: {error}", + "Failed to link object": "Failed to link object", + "Failed to load Deck cards": "Failed to load Deck cards", "Failed to load LLM configuration": "Failed to load LLM configuration", "Failed to load Nextcloud groups": "Failed to load Nextcloud groups", + "Failed to load cache statistics: {error}": "Failed to load cache statistics: {error}", "Failed to load conversation": "Failed to load conversation", "Failed to load entities": "Failed to load entities", "Failed to load entity": "Failed to load entity", + "Failed to load extraction data": "Failed to load extraction data", "Failed to load files": "Failed to load files", + "Failed to load linked contacts": "Failed to load linked contacts", + "Failed to load linked emails": "Failed to load linked emails", + "Failed to load linked events": "Failed to load linked events", "Failed to load organisations": "Failed to load organisations", + "Failed to load register data": "Failed to load register data", + "Failed to load related entities": "Failed to load related entities", + "Failed to load settings: {error}": "Failed to load settings: {error}", + "Failed to load statistics: {error}": "Failed to load statistics: {error}", "Failed to load templates": "Failed to load templates", + "Failed to load view: {error}": "Failed to load view: {error}", "Failed to load webhooks": "Failed to load webhooks", "Failed to load workflows": "Failed to load workflows", + "Failed to process message": "Failed to process message", + "Failed to publish register: {error}": "Failed to publish register: {error}", + "Failed to publish schema: {error}": "Failed to publish schema: {error}", + "Failed to publish: {error}": "Failed to publish: {error}", "Failed to refresh database information": "Failed to refresh database information", "Failed to reindex collection": "Failed to reindex collection", + "Failed to remove avatar": "Failed to remove avatar", + "Failed to remove link": "Failed to remove link", + "Failed to remove member: {error}": "Failed to remove member: {error}", + "Failed to remove schema {schema}: {error}": "Failed to remove schema {schema}: {error}", + "Failed to remove user from organisation": "Failed to remove user from organisation", "Failed to rename conversation": "Failed to rename conversation", + "Failed to request deactivation": "Failed to request deactivation", "Failed to restore conversation": "Failed to restore conversation", + "Failed to retrieve extraction status.": "Failed to retrieve extraction status.", + "Failed to retrieve objects for file.": "Failed to retrieve objects for file.", "Failed to retry extraction": "Failed to retry extraction", "Failed to retry webhook": "Failed to retry webhook", + "Failed to revoke token": "Failed to revoke token", "Failed to save GitHub token": "Failed to save GitHub token", "Failed to save GitLab URL": "Failed to save GitLab URL", "Failed to save GitLab token": "Failed to save GitLab token", + "Failed to save LLM settings: {error}": "Failed to save LLM settings: {error}", "Failed to save additional feedback": "Failed to save additional feedback", + "Failed to save configuration.": "Failed to save configuration.", + "Failed to save configuration: {error}": "Failed to save configuration: {error}", + "Failed to save facet configuration": "Failed to save facet configuration", + "Failed to save feedback": "Failed to save feedback", + "Failed to save file settings: {error}": "Failed to save file settings: {error}", "Failed to save n8n configuration": "Failed to save n8n configuration", + "Failed to save preferences": "Failed to save preferences", "Failed to save roles": "Failed to save roles", "Failed to save settings": "Failed to save settings", + "Failed to save settings: {error}": "Failed to save settings: {error}", + "Failed to save view: {error}": "Failed to save view: {error}", + "Failed to save warmup interval: {error}": "Failed to save warmup interval: {error}", "Failed to save webhook": "Failed to save webhook", "Failed to send feedback": "Failed to send feedback", + "Failed to set active organisation: {error}": "Failed to set active organisation: {error}", + "Failed to setup SOLR: {error}": "Failed to setup SOLR: {error}", + "Failed to start file warmup: {error}": "Failed to start file warmup: {error}", + "Failed to start processing {type} files: {error}": "Failed to start processing {type} files: {error}", + "Failed to start vectorization: {error}": "Failed to start vectorization: {error}", + "Failed to switch organisation: {error}": "Failed to switch organisation: {error}", + "Failed to sync magic table for {schema}: {error}": "Failed to sync magic table for {schema}: {error}", + "Failed to test SOLR connection: {error}": "Failed to test SOLR connection: {error}", "Failed to test webhook": "Failed to test webhook", + "Failed to update LLM settings: {error}": "Failed to update LLM settings: {error}", + "Failed to update Multitenancy settings: {error}": "Failed to update Multitenancy settings: {error}", + "Failed to update RBAC settings: {error}": "Failed to update RBAC settings: {error}", + "Failed to update Retention settings: {error}": "Failed to update Retention settings: {error}", + "Failed to update SOLR settings: {error}": "Failed to update SOLR settings: {error}", "Failed to update favorite status": "Failed to update favorite status", + "Failed to update schema configuration for {schema}: {error}": "Failed to update schema configuration for {schema}: {error}", "Failed to update schema properties": "Failed to update schema properties", "Failed to update view": "Failed to update view", + "Failed to update view: {error}": "Failed to update view: {error}", "Failed to update webhook": "Failed to update webhook", + "Failed to upload avatar": "Failed to upload avatar", + "Failed to validate {schema}: {error}": "Failed to validate {schema}: {error}", + "Failed to vectorize files: {error}": "Failed to vectorize files: {error}", + "Failed to warmup names cache: {error}": "Failed to warmup names cache: {error}", + "Federation": "Federation", "Feedback recorded": "Feedback recorded", "Fewer sources (1-3):": "Fewer sources (1-3):", "Field": "Field", + "Fields to Display": "Fields to Display", "File": "File", + "File #{id}": "File #{id}", "File Chunk Prediction": "File Chunk Prediction", "File Collection": "File Collection", + "File Handling": "File Handling", + "File Location": "File Location", "File Management": "File Management", "File Name": "File Name", "File Path": "File Path", "File Type Selection": "File Type Selection", + "File Types to Process": "File Types to Process", "File Vectorization": "File Vectorization", "File Warmup": "File Warmup", "File actions menu": "File actions menu", @@ -446,15 +842,25 @@ "File extraction queued": "File extraction queued", "File indexed successfully": "File indexed successfully", "File is already anonymized": "File is already anonymized", + "File content is required": "File content is required", + "File name": "File name", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", "File not found": "File not found", + "File settings saved successfully": "File settings saved successfully", "File settings updated successfully": "File settings updated successfully", "File sources": "File sources", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.", "File vectorization configuration saved successfully": "File vectorization configuration saved successfully", "File vectorization started. Check the statistics section for progress.": "File vectorization started. Check the statistics section for progress.", "File warmup completed": "File warmup completed", "Files": "Files", + "Files added successfully": "Files added successfully", + "Files folder not found": "Files folder not found", "Files with Completed Extraction:": "Files with Completed Extraction:", "Files → fileCollection, Objects → objectCollection": "Files → fileCollection, Objects → objectCollection", + "Files → filecollection, objects → objectcollection": "Files → filecollection, objects → objectcollection", "Filter Audit Trails": "Filter Audit Trails", "Filter Deleted Items": "Filter Deleted Items", "Filter Objects": "Filter Objects", @@ -464,41 +870,70 @@ "Filter and analyze search trail entries": "Filter and analyze search trail entries", "Filter and manage audit trail entries": "Filter and manage audit trail entries", "Filter and manage soft deleted items": "Filter and manage soft deleted items", + "Filter and search entities": "Filter and search entities", "Filter by object ID": "Filter by object ID", "Filter by search term": "Filter by search term", + "Filter by type": "Filter by type", "Filter by webhook": "Filter by webhook", "Filter data loaded automatically. Use the filters below to refine your search.": "Filter data loaded automatically. Use the filters below to refine your search.", + "Filter fields...": "Filter fields...", "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filter webhook triggers by payload properties (one per line, format: key: value)", "Filtered": "Filtered", "Filters": "Filters", + "Finalizing setup...": "Finalizing setup...", "Fireworks AI Chat Settings": "Fireworks AI Chat Settings", "Fireworks AI Embedding Configuration": "Fireworks AI Embedding Configuration", "First": "First", "Fixed": "Fixed", "For chat and retrieval-augmented generation": "For chat and retrieval-augmented generation", "For vector embeddings and semantic search": "For vector embeddings and semantic search", + "Forbidden": "Forbidden", "Format": "Format", "Format Issue": "Format Issue", "Format constraint is missing": "Format constraint is missing", + "From (e.g. -7 days or 2025-01-01)": "From (e.g. -7 days or 2025-01-01)", "From Date": "From Date", "From date": "From date", "Full data copied to clipboard": "Full data copied to clipboard", + "Function Tools": "Function Tools", "General Issue": "General Issue", "Generate recommendations and confidence scores": "Generate recommendations and confidence scores", + "Generate report": "Generate report", + "Generate the verantwoordingsdocument": "Generate the verantwoordingsdocument", "Generate vectors immediately when new objects are created": "Generate vectors immediately when new objects are created", + "Generated: {time}": "Generated: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Get real-time insights into your organization's data health, growth trends, and quality metrics.", + "GitHub Personal Access Token": "GitHub Personal Access Token", "GitHub token is valid": "GitHub token is valid", + "GitHub token is valid! Username: {username}": "GitHub token is valid! Username: {username}", "GitHub token removed successfully": "GitHub token removed successfully", "GitHub token saved successfully": "GitHub token saved successfully", + "GitHub token test failed: {message}": "GitHub token test failed: {message}", + "GitLab Personal Access Token": "GitLab Personal Access Token", "GitLab URL saved successfully": "GitLab URL saved successfully", "GitLab token is valid": "GitLab token is valid", + "GitLab token is valid! Username: {username}": "GitLab token is valid! Username: {username}", "GitLab token saved successfully": "GitLab token saved successfully", + "GitLab token test failed: {message}": "GitLab token test failed: {message}", + "Go to linked Register": "Go to linked Register", + "Go to linked Schema": "Go to linked Schema", + "Go to organisation": "Go to organisation", + "Grant additional permissions beyond standard CRUD operations": "Grant additional permissions beyond standard CRUD operations", + "Group Quota": "Group Quota", + "Groups": "Groups", + "HTML preview": "HTML preview", "HTTP Method": "HTTP Method", "HTTP method used to send webhook requests": "HTTP method used to send webhook requests", "Headers": "Headers", "Health": "Health", "Heartbeat successful - connection kept alive": "Heartbeat successful - connection kept alive", "Helpful": "Helpful", + "Here you can configure the details for various connections.": "Here you can configure the details for various connections.", + "Here you can configure the details for your organisation.": "Here you can configure the details for your organisation.", + "Hide": "Hide", "Hide Filters": "Hide Filters", + "Hide in collection view": "Hide in collection view", "Hide in forms": "Hide in forms", "Hide in list view": "Hide in list view", "High": "High", @@ -508,135 +943,239 @@ "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.", "How often to check for updates (1-168 hours)": "How often to check for updates (1-168 hours)", "How to handle retries for failed webhook deliveries": "How to handle retries for failed webhook deliveries", + "IBANs": "IBANs", "ID": "ID", + "ID of input mapping (optional)": "ID of input mapping (optional)", + "ID of output mapping (optional)": "ID of output mapping (optional)", + "ID of the target resource": "ID of the target resource", "ID:": "ID:", + "ID: {id}": "ID: {id}", "IP rate limits cleared successfully": "IP rate limits cleared successfully", "Identify properties not in the schema": "Identify properties not in the schema", "Immutable": "Immutable", "Import": "Import", + "Import Configuration": "Import Configuration", + "Import Data into Register": "Import Data into Register", "Import successful": "Import successful", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.", "Improved Property": "Improved Property", "In use": "In use", "Inactive": "Inactive", + "Inactive organisations cannot be used": "Inactive organisations cannot be used", "Include IDs and names of related objects for better contextual search": "Include IDs and names of related objects for better contextual search", + "Include ids and names of related objects for better contextual search": "Include ids and names of related objects for better contextual search", "Include related object references": "Include related object references", "Include schema and register metadata": "Include schema and register metadata", + "Index": "Index", + "Information about the current OpenRegister installation": "Information about the current OpenRegister installation", + "Inherited from register": "Inherited from register", + "Inherits permissions from register": "Inherits permissions from register", "Initialization failed": "Initialization failed", "Initialize Project": "Initialize Project", + "Initializing SOLR setup...": "Initializing SOLR setup...", + "Input Mapping": "Input Mapping", "Inspect Fields": "Inspect Fields", "Inspect Index": "Inspect Index", + "Inspect SOLR Index": "Inspect SOLR Index", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Install the notify_push app from the Nextcloud App Store to enable real-time updates.", + "Integrations": "Integrations", + "Internet location (URL) of this installation": "Internet location (URL) of this installation", + "Interval": "Interval", "Invalid": "Invalid", "Invalid batch size. Must be between 1 and 5000": "Invalid batch size. Must be between 1 and 5000", + "Invalid feedback type": "Invalid feedback type", "Invalid field name provided": "Invalid field name provided", "Invalid maxObjects. Must be 0 (all) or positive number": "Invalid maxObjects. Must be 0 (all) or positive number", "Invalid:": "Invalid:", + "Invite Users": "Invite Users", + "Invited Users": "Invited Users", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Inzage results", "Issue": "Issue", "Items per page:": "Items per page:", "Just now": "Just now", + "Key": "Key", "LLM Configuration": "LLM Configuration", "LLM actions menu": "LLM actions menu", "LLM configuration saved successfully": "LLM configuration saved successfully", "LLM features disabled": "LLM features disabled", "LLM features enabled": "LLM features enabled", "LLM must be enabled with an embedding provider configured": "LLM must be enabled with an embedding provider configured", + "LLM settings saved successfully": "LLM settings saved successfully", "LLM settings updated successfully": "LLM settings updated successfully", + "Label": "Label", + "Labels": "Labels", + "Languages": "Languages", "Last": "Last", "Last Triggered": "Last Triggered", + "Leave empty to allow all users access": "Leave empty to allow all users access", + "Leave off for auto-detection from property format.": "Leave off for auto-detection from property format.", + "Left organisation successfully": "Left organisation successfully", + "Legal basis *": "Legal basis *", "Length Range:": "Length Range:", "Linear": "Linear", + "Link": "Link", + "Link an email from the Mail app sidebar to associate it with this object.": "Link an email from the Mail app sidebar to associate it with this object.", + "Link an object to see it here.": "Link an object to see it here.", + "Link another object": "Link another object", + "Link existing event": "Link existing event", + "Link removed": "Link removed", + "Link to Object": "Link to Object", + "Link to {name}": "Link to {name}", + "Linked Objects": "Linked Objects", + "Linked by {user}": "Linked by {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Linked emails, calendar events, contacts and Deck cards will appear here.", + "Linked to {name}": "Linked to {name}", "Load Advanced Filters": "Load Advanced Filters", "Load advanced filters with live data from your search index": "Load advanced filters with live data from your search index", + "Load more": "Load more", "Load view": "Load view", + "Loading API tokens...": "Loading API tokens...", "Loading ConfigSets...": "Loading ConfigSets...", + "Loading LLM configuration...": "Loading LLM configuration...", + "Loading RBAC settings...": "Loading RBAC settings...", + "Loading activity...": "Loading activity...", "Loading advanced filters...": "Loading advanced filters...", "Loading agents...": "Loading agents...", "Loading application details...": "Loading application details...", "Loading applications...": "Loading applications...", "Loading archived conversations...": "Loading archived conversations...", "Loading audit trails...": "Loading audit trails...", + "Loading available tools...": "Loading available tools...", + "Loading cache statistics...": "Loading cache statistics...", "Loading collections...": "Loading collections...", "Loading configurations...": "Loading configurations...", "Loading conversation...": "Loading conversation...", "Loading conversations...": "Loading conversations...", "Loading deleted items...": "Loading deleted items...", + "Loading endpoints": "Loading endpoints", + "Loading entities...": "Loading entities...", "Loading events...": "Loading events...", "Loading filter data...": "Loading filter data...", "Loading groups...": "Loading groups...", + "Loading linked objects...": "Loading linked objects...", "Loading log details...": "Loading log details...", + "Loading multitenancy settings...": "Loading multitenancy settings...", + "Loading n8n configuration...": "Loading n8n configuration...", + "Loading permission matrix...": "Loading permission matrix...", + "Loading preferences...": "Loading preferences...", + "Loading register data...": "Loading register data...", "Loading registers...": "Loading registers...", + "Loading retention settings...": "Loading retention settings...", "Loading schemas...": "Loading schemas...", + "Loading search configuration...": "Loading search configuration...", "Loading search trails...": "Loading search trails...", "Loading sources...": "Loading sources...", "Loading statistics...": "Loading statistics...", + "Loading text extraction settings...": "Loading text extraction settings...", + "Loading tokens...": "Loading tokens...", + "Loading user groups...": "Loading user groups...", "Loading users...": "Loading users...", "Loading views...": "Loading views...", "Loading...": "Loading...", "Local": "Local", "Local Version": "Local Version", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).", + "Location": "Location", + "Location Field": "Location Field", + "Locations": "Locations", "Locked": "Locked", "Locked:": "Locked:", "Login successful": "Login successful", "Logs": "Logs", "Low": "Low", "Low Confidence": "Low Confidence", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.", "Lucene Version:": "Lucene Version:", + "Magic table sync completed for {schema}": "Magic table sync completed for {schema}", "Magic table synchronized successfully": "Magic table synchronized successfully", + "Mail Integration": "Mail Integration", + "Mail integration is not available": "Mail integration is not available", + "Make this installation discoverable within the federation network.": "Make this installation discoverable within the federation network.", "Manage Organisation Roles": "Manage Organisation Roles", "Manage SOLR Collections (data stores) and assign them for objects and files.": "Manage SOLR Collections (data stores) and assign them for objects and files.", "Manage SOLR ConfigSets (configuration templates) for your collections.": "Manage SOLR ConfigSets (configuration templates) for your collections.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Manage SOLR collections (data stores) and assign them for objects and files.", + "Manage SOLR configsets (configuration templates) for your collections.": "Manage SOLR configsets (configuration templates) for your collections.", "Manage and configure agents for automated tasks": "Manage and configure agents for automated tasks", "Manage and monitor file text extraction status": "Manage and monitor file text extraction status", "Manage and restore soft deleted items from your registers": "Manage and restore soft deleted items from your registers", "Manage and view detected entities from files and objects": "Manage and view detected entities from files and objects", "Manage document templates and themes": "Manage document templates and themes", "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.", "Manage webhooks for event-driven integrations": "Manage webhooks for event-driven integrations", + "Manage your account settings, security, and personal data.": "Manage your account settings, security, and personal data.", "Manage your applications and modules": "Manage your applications and modules", + "Manage your chat conversations": "Manage your chat conversations", "Manage your data registers and their configurations": "Manage your data registers and their configurations", "Manage your data schemas and their properties": "Manage your data schemas and their properties", "Manage your data sources and their configurations": "Manage your data sources and their configurations", + "Manage your organisations and switch between them": "Manage your organisations and switch between them", "Manage your saved search configurations": "Manage your saved search configurations", "Manage your system configurations and settings": "Manage your system configurations and settings", "Managed": "Managed", + "Mappings": "Mappings", + "Mass validation completed successfully": "Mass validation completed successfully", + "Mass validation failed: {error}": "Mass validation failed: {error}", "Max Files (0 = all)": "Max Files (0 = all)", "Max Length": "Max Length", "Max Length:": "Max Length:", "Max Objects (0 = all)": "Max Objects (0 = all)", "Max Retries": "Max Retries", "Max Shards/Node": "Max Shards/Node", + "Max Tokens": "Max Tokens", "Max execution time (ms)": "Max execution time (ms)", "Max ms": "Max ms", "Max result count": "Max result count", "Max results": "Max results", "Maximum": "Maximum", + "Maximum File Size (MB)": "Maximum File Size (MB)", "Maximum Nesting Depth": "Maximum Nesting Depth", + "Maximum length": "Maximum length", "Maximum length constraint is missing": "Maximum length constraint is missing", "Maximum length is too restrictive": "Maximum length is too restrictive", "Maximum number of files to process. Set to 0 to process all files.": "Maximum number of files to process. Set to 0 to process all files.", + "Maximum number of items": "Maximum number of items", "Maximum number of objects to process. Set to 0 to process all objects.": "Maximum number of objects to process. Set to 0 to process all objects.", "Maximum number of retry attempts for failed deliveries": "Maximum number of retry attempts for failed deliveries", "Maximum time to wait before committing changes": "Maximum time to wait before committing changes", "Maximum tokens to generate": "Maximum tokens to generate", + "Maximum value": "Maximum value", "Maximum value constraint is missing": "Maximum value constraint is missing", "Maximum value is too restrictive": "Maximum value is too restrictive", "Medium": "Medium", "Medium Confidence": "Medium Confidence", "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)", + "Member removed successfully": "Member removed successfully", + "Members": "Members", + "Members ({count})": "Members ({count})", "Members of selected groups can access this view": "Members of selected groups can access this view", "Members:": "Members:", "Memory prediction calculated successfully": "Memory prediction calculated successfully", "Message content is required": "Message content is required", "Message does not belong to this conversation": "Message does not belong to this conversation", + "Message not found": "Message not found", + "Metadata": "Metadata", "Metadata Filters": "Metadata Filters", "Method": "Method", + "Method*": "Method*", + "Method: {method}": "Method: {method}", "Min Length": "Min Length", "Min execution time (ms)": "Min execution time (ms)", "Min ms": "Min ms", "Min result count": "Min result count", "Min results": "Min results", "Minimum": "Minimum", + "Minimum length": "Minimum length", + "Minimum number of items": "Minimum number of items", + "Minimum value": "Minimum value", "Minimum value constraint is missing": "Minimum value constraint is missing", "Minimum value is too restrictive": "Minimum value is too restrictive", + "Missing conversation or agentUuid": "Missing conversation or agentUuid", + "Missing conversationId": "Missing conversationId", + "Missing message": "Missing message", "Missing object fields created successfully": "Missing object fields created successfully", "Mode:": "Mode:", "Model": "Model", @@ -646,85 +1185,144 @@ "More sources (10-20):": "More sources (10-20):", "Most Active Objects": "Most Active Objects", "Multiple Of": "Multiple Of", + "Multiple of": "Multiple of", + "Multiple search trail deletion not implemented yet": "Multiple search trail deletion not implemented yet", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Multitenancy settings updated successfully", + "My Account": "My Account", + "Naam": "Naam", + "Naam *": "Naam *", "Name": "Name", + "Name *": "Name *", + "Name*": "Name*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Names cache warmed up successfully: {count} names loaded in {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Names cache warmed up successfully: {count} names loaded in {time}. {message}", "Names cache warmup completed": "Names cache warmup completed", + "Naming pattern for copies:": "Naming pattern for copies:", "New Collection Name": "New Collection Name", + "New Configuration": "New Configuration", "New Conversation": "New Conversation", "New Properties": "New Properties", "New Property": "New Property", + "New User Group": "New User Group", "New Value": "New Value", + "New activity": "New activity", + "New password": "New password", + "New processing activity": "New processing activity", "New users without specific organisation membership will be automatically added to this organisation": "New users without specific organisation membership will be automatically added to this organisation", + "New verwerkingsactiviteit": "New verwerkingsactiviteit", "Next": "Next", + "Nextcloud Groups": "Nextcloud Groups", "No": "No", + "No API tokens.": "No API tokens.", "No ConfigSets found": "No ConfigSets found", + "No Deck cards linked to this object": "No Deck cards linked to this object", "No GitHub token configured": "No GitHub token configured", "No GitHub token provided": "No GitHub token provided", "No GitLab token provided": "No GitLab token provided", "No Organisation": "No Organisation", "No active filters": "No active filters", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.", "No activity data available": "No activity data available", + "No activity found.": "No activity found.", "No agents available": "No agents available", "No agents found": "No agents found", "No app store cache exists yet": "No app store cache exists yet", "No applications are available.": "No applications are available.", "No applications found": "No applications found", + "No approval steps for this object.": "No approval steps for this object.", "No archived conversations": "No archived conversations", "No audit trail entries found": "No audit trail entries found", "No changes recorded": "No changes recorded", "No chunks to vectorize": "No chunks to vectorize", "No collections found": "No collections found", "No configuration data": "No configuration data", + "No configuration selected": "No configuration selected", "No configurations are available.": "No configurations are available.", "No configurations found": "No configurations found", "No configurations found for this application.": "No configurations found for this application.", + "No contacts linked to this object": "No contacts linked to this object", "No conversations yet. Create a new one to get started!": "No conversations yet. Create a new one to get started!", + "No dashboards yet": "No dashboards yet", "No data available": "No data available", "No data available for chart": "No data available for chart", "No deleted items found": "No deleted items found", "No deletion data available": "No deletion data available", "No description found": "No description found", "No description provided": "No description provided", + "No emails linked to this object": "No emails linked to this object", + "No endpoints defined": "No endpoints defined", + "No entities detected for this email.": "No entities detected for this email.", "No entities detected in this file. Run text extraction first.": "No entities detected in this file. Run text extraction first.", "No entities found": "No entities found", "No entities have been detected yet": "No entities have been detected yet", "No events found": "No events found", + "No events linked to this object": "No events linked to this object", "No expired audit trails found to clear": "No expired audit trails found to clear", + "No expired search trails found to clear": "No expired search trails found to clear", + "No extraction data available for this file": "No extraction data available for this file", "No facetable fields available. Select a register and schema to see available filters.": "No facetable fields available. Select a register and schema to see available filters.", "No files found": "No files found", "No files have been extracted yet": "No files have been extracted yet", + "No files selected": "No files selected", "No files to process": "No files to process", "No files to reindex": "No files to reindex", "No filters are currently active. This will delete ALL audit trail entries!": "No filters are currently active. This will delete ALL audit trail entries!", + "No groups found. Try a different search.": "No groups found. Try a different search.", + "No label": "No label", + "No labels": "No labels", + "No linked objects": "No linked objects", "No log entries found": "No log entries found", "No logs are available for this configuration.": "No logs are available for this configuration.", "No logs are available for this source.": "No logs are available for this source.", "No logs found": "No logs found", + "No matches": "No matches", "No mismatched fields found - SOLR schema is properly configured": "No mismatched fields found - SOLR schema is properly configured", + "No objects are currently selected for deletion.": "No objects are currently selected for deletion.", + "No objects are currently selected for permanent deletion.": "No objects are currently selected for permanent deletion.", + "No objects are currently selected for restoration.": "No objects are currently selected for restoration.", "No objects found": "No objects found", "No objects found to vectorize": "No objects found to vectorize", + "No objects linked to this email": "No objects linked to this email", + "No objects selected": "No objects selected", "No objects selected for deletion": "No objects selected for deletion", + "No objects selected for restoration": "No objects selected for restoration", "No objects to delete": "No objects to delete", + "No objects to delete for schema {schema}": "No objects to delete for schema {schema}", "No objects were permanently deleted": "No objects were permanently deleted", + "No organisations found": "No organisations found", + "No personal data was found for this subject identifier.": "No personal data was found for this subject identifier.", + "No processing activities yet": "No processing activities yet", "No properties found": "No properties found", "No properties match your filters.": "No properties match your filters.", "No purge date set": "No purge date set", "No register data available": "No register data available", + "No register objects reference this file": "No register objects reference this file", "No registers found": "No registers found", "No registers found for this application.": "No registers found for this application.", + "No registers found. Create a register to configure permissions.": "No registers found. Create a register to configure permissions.", + "No related cases found for this sender": "No related cases found for this sender", + "No related entities": "No related entities", "No relations found": "No relations found", "No request body available": "No request body available", "No response body available": "No response body available", "No saved views yet. Create one in the Search tab!": "No saved views yet. Create one in the Search tab!", + "No saved views yet. create one in the search tab!": "No saved views yet. create one in the search tab!", + "No scan run yet": "No scan run yet", "No schema data available": "No schema data available", "No schema selected for exploration": "No schema selected for exploration", + "No schemas configured for mail linking.": "No schemas configured for mail linking.", "No schemas found": "No schemas found", "No schemas found for this application.": "No schemas found for this application.", "No search terms data available": "No search terms data available", "No search trail entries found": "No search trail entries found", "No sources are available.": "No sources are available.", "No sources found": "No sources found", + "No synchronizations found": "No synchronizations found", "No templates found": "No templates found", "No templates have been created yet": "No templates have been created yet", + "No tools available. Tools can be registered by installed apps.": "No tools available. Tools can be registered by installed apps.", + "No users in this organisation.": "No users in this organisation.", "No valid magic-mapped register+schema combinations found": "No valid magic-mapped register+schema combinations found", "No views found. Create views first before configuring vectorization.": "No views found. Create views first before configuring vectorization.", "No views match your search": "No views match your search", @@ -732,27 +1330,40 @@ "No webhooks have been configured yet": "No webhooks have been configured yet", "No workflows found in this project. Create workflows in the n8n editor.": "No workflows found in this project. Create workflows in the n8n editor.", "None": "None", + "Not Found": "Not Found", + "Not extracted": "Not extracted", "Not helpful": "Not helpful", "Not in use": "Not in use", + "Note:": "Note:", + "Notifications": "Notifications", "Number Constraints": "Number Constraints", + "Number of Sources": "Number of Sources", "Number of chunks to vectorize in one API call (1-100).": "Number of chunks to vectorize in one API call (1-100).", "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.", "Number of objects to process in each batch (1-5000).": "Number of objects to process in each batch (1-5000).", "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.", "Numeric Range:": "Numeric Range:", "Object": "Object", + "Object #{id}": "Object #{id}", "Object Collection": "Object Collection", + "Object Configuration:": "Object Configuration:", "Object Count Prediction": "Object Count Prediction", + "Object Extraction Mode": "Object Extraction Mode", "Object ID": "Object ID", "Object Management": "Object Management", + "Object Text Extraction": "Object Text Extraction", "Object Vectorization": "Object Vectorization", "Object Warmup": "Object Warmup", + "Object changes in owned objects": "Object changes in owned objects", "Object collection not configured": "Object collection not configured", + "Object metadata incomplete for file upload": "Object metadata incomplete for file upload", "Object not found": "Object not found", "Object permanently deleted": "Object permanently deleted", "Object restored successfully": "Object restored successfully", "Object settings updated successfully": "Object settings updated successfully", "Object sources": "Object sources", + "Object successfully deleted": "Object successfully deleted", + "Object successfully modified": "Object successfully modified", "Object unlocked successfully": "Object unlocked successfully", "Object vectorization configuration saved successfully": "Object vectorization configuration saved successfully", "Object vectorized successfully": "Object vectorized successfully", @@ -763,13 +1374,16 @@ "Objects being analyzed": "Objects being analyzed", "Objects by Register": "Objects by Register", "Objects by Schema": "Objects by Schema", + "Objects deletion completed for {schema}": "Objects deletion completed for {schema}", "Objects deletion completed successfully": "Objects deletion completed successfully", "Objects in schema": "Objects in schema", + "Objects successfully deleted": "Objects successfully deleted", "Objects to Process:": "Objects to Process:", "Objects to be analyzed": "Objects to be analyzed", "Objects to be deleted": "Objects to be deleted", "Objects to be validated": "Objects to be validated", "Objects will be serialized as JSON text before vectorization": "Objects will be serialized as JSON text before vectorization", + "Objects will be soft deleted and moved to the": "Objects will be soft deleted and moved to the", "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.", "Old Value": "Old Value", "Oldest Item (days)": "Oldest Item (days)", @@ -777,29 +1391,63 @@ "Ollama Embedding Configuration": "Ollama Embedding Configuration", "Ollama URL": "Ollama URL", "Only With Changes": "Only With Changes", + "Only members of selected groups can access this organisation": "Only members of selected groups can access this organisation", + "Open Nextcloud App Store": "Open Nextcloud App Store", + "Open OpenRegister sidebar": "Open OpenRegister sidebar", "Open Register Objects": "Open Register Objects", + "Open folder": "Open folder", + "Open in OpenRegister": "Open in OpenRegister", "Open n8n Editor": "Open n8n Editor", "OpenAI Chat Settings": "OpenAI Chat Settings", "OpenAI Embedding Configuration": "OpenAI Embedding Configuration", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", "OpenRegister Settings": "OpenRegister Settings", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.", + "Optimizing search performance...": "Optimizing search performance...", + "Optional URL-friendly identifier": "Optional URL-friendly identifier", + "Optional description for this facet": "Optional description for this facet", + "Optional end date property": "Optional end date property", + "Optional event description template": "Optional event description template", + "Optional location property": "Optional location property", "Optional webhook secret for signature verification": "Optional webhook secret for signature verification", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)", + "Or": "Or", "Order": "Order", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.", "Organisation": "Organisation", + "Organisation Configuration": "Organisation Configuration", "Organisation ID": "Organisation ID", "Organisation Members": "Organisation Members", "Organisation Statistics": "Organisation Statistics", "Organisation created successfully": "Organisation created successfully", + "Organisation membership changes": "Organisation membership changes", + "Organisation name is required": "Organisation name is required", "Organisation settings saved successfully": "Organisation settings saved successfully", + "Organisation successfully created": "Organisation successfully created", + "Organisation successfully updated": "Organisation successfully updated", + "Organisational measures": "Organisational measures", "Organisations": "Organisations", + "Organisatorische maatregelen": "Organisatorische maatregelen", "Organization": "Organization", "Organization ID (Optional)": "Organization ID (Optional)", + "Organizations": "Organizations", "Orphaned Items": "Orphaned Items", + "Other": "Other", + "Output Mapping": "Output Mapping", + "Overview": "Overview", "Owner": "Owner", "Owner:": "Owner:", + "PDF": "PDF", + "PII hits": "PII hits", + "PII rows": "PII rows", + "Page {current} of {total}": "Page {current} of {total}", "Parallel Mode (Faster, more resource intensive)": "Parallel Mode (Faster, more resource intensive)", "Parallel:": "Parallel:", "Password": "Password", + "Password changes are not supported by your authentication provider.": "Password changes are not supported by your authentication provider.", "Password for SOLR authentication (optional)": "Password for SOLR authentication (optional)", + "Password updated successfully": "Password updated successfully", "Path": "Path", "Path in repository": "Path in repository", "Path where the configuration file will be saved in the repository": "Path where the configuration file will be saved in the repository", @@ -811,13 +1459,24 @@ "Pending": "Pending", "Permanent Delete Mode": "Permanent Delete Mode", "Permanently Delete": "Permanently Delete", + "Permanently Delete ({count})": "Permanently Delete ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Permanently delete all {count} soft-deleted objects. This cannot be undone!", "Permanently delete already soft-deleted objects": "Permanently delete already soft-deleted objects", "Permanently delete this conversation?": "Permanently delete this conversation?", + "Permission Matrix": "Permission Matrix", "Person": "Person", "Personal Data": "Personal Data", + "Personal Data Export": "Personal Data Export", + "Persons": "Persons", "Phone": "Phone", + "Phone Numbers": "Phone Numbers", + "Photos": "Photos", "Please create an agent in the": "Please create an agent in the", + "Please select a user": "Please select a user", "Please select an agent to continue": "Please select an agent to continue", + "Please select an organisation": "Please select an organisation", + "Please select at least one item to import": "Please select at least one item to import", + "Please select registers or schemas first": "Please select registers or schemas first", "Please select which register and schema to use for the new object": "Please select which register and schema to use for the new object", "Please try again later.": "Please try again later.", "Please wait while we fetch your agents.": "Please wait while we fetch your agents.", @@ -825,12 +1484,25 @@ "Please wait while we fetch your configurations.": "Please wait while we fetch your configurations.", "Please wait while we fetch your deleted items.": "Please wait while we fetch your deleted items.", "Please wait while we fetch your sources.": "Please wait while we fetch your sources.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Please wait while we permanently delete the SOLR collection. This may take a few moments.", + "Polls": "Polls", "Popular Search Terms": "Popular Search Terms", "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Preferences saved", "Prerequisites": "Prerequisites", "Press Enter to send, Shift+Enter for new line": "Press Enter to send, Shift+Enter for new line", + "Press enter to send, shift+enter for new line": "Press enter to send, shift+enter for new line", + "Prettify": "Prettify", + "Preview erasure": "Preview erasure", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Preview:": "Preview:", "Previous": "Previous", "Private": "Private", + "Private Agent (Default)": "Private Agent (Default)", + "Private agents": "Private agents", + "Process Name (optional)": "Process Name (optional)", + "Processed {indexed} files, {failed} failed": "Processed {indexed} files, {failed} failed", "Processes chunks in batches with simulated parallelism.": "Processes chunks in batches with simulated parallelism.", "Processes file chunks sequentially (safest).": "Processes file chunks sequentially (safest).", "Processes objects in chunks with simulated parallelism.": "Processes objects in chunks with simulated parallelism.", @@ -847,6 +1519,11 @@ "Property Title": "Property Title", "Property Type": "Property Type", "Property can be improved": "Property can be improved", + "Property could not be added": "Property could not be added", + "Property could not be updated": "Property could not be updated", + "Property name of inversed relation": "Property name of inversed relation", + "Property successfully added": "Property successfully added", + "Property successfully updated": "Property successfully updated", "Protected": "Protected", "Provider": "Provider", "Provider is required for testing": "Provider is required for testing", @@ -854,21 +1531,57 @@ "Public View": "Public View", "Public views can be accessed by anyone in the system": "Public views can be accessed by anyone in the system", "Publish": "Publish", + "Publish Configuration to GitHub": "Publish Configuration to GitHub", + "Publish OAS": "Publish OAS", "Publish Objects": "Publish Objects", + "Publish Register OAS to GitHub": "Publish Register OAS to GitHub", "Publish all objects in this schema": "Publish all objects in this schema", + "Publish objects to make them publicly available": "Publish objects to make them publicly available", "Published": "Published", "Published:": "Published:", "Publishing...": "Publishing...", "Purge": "Purge", "Purge Date": "Purge Date", + "Purge {count} object from database": "Purge {count} object from database", + "Purge {count} objects from database": "Purge {count} objects from database", + "Purpose limitation *": "Purpose limitation *", + "Push Notifications": "Push Notifications", "Query Complexity Distribution": "Query Complexity Distribution", + "Query Help": "Query Help", "Query parameter is required": "Query parameter is required", + "Quota": "Quota", "RAG": "RAG", "RAG Configuration": "RAG Configuration", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC settings updated successfully", "Rate limits cleared successfully": "Rate limits cleared successfully", "Raw Changes Data": "Raw Changes Data", "Re-vectorize on object update": "Re-vectorize on object update", "Read": "Read", + "Real-time push notification status via notify_push": "Real-time push notification status via notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Realtime push active": "Realtime push active", + "Realtime push not available — the notify_push app is not installed": "Realtime push not available — the notify_push app is not installed", + "Reason": "Reason", + "Reason (optional)": "Reason (optional)", + "Rebase operation completed successfully": "Rebase operation completed successfully", + "Rebase operation failed: {error}": "Rebase operation failed: {error}", + "Rechtsgrond": "Rechtsgrond", "Recommendations:": "Recommendations:", "Recommended Type:": "Recommended Type:", "Recommended: 5 sources": "Recommended: 5 sources", @@ -879,61 +1592,116 @@ "Refresh dashboard": "Refresh dashboard", "Refresh database info": "Refresh database info", "Register": "Register", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Schema", "Register ID": "Register ID", + "Register Objects": "Register Objects", + "Register Overview": "Register Overview", "Register Statistics": "Register Statistics", "Register Totals": "Register Totals", + "Register and schema are required": "Register and schema are required", + "Register annotation": "Register annotation", "Register depublished successfully": "Register depublished successfully", "Register not found": "Register not found", + "Register objects": "Register objects", "Register objects deletion completed successfully": "Register objects deletion completed successfully", "Register published successfully": "Register published successfully", "Register/Schema Usage": "Register/Schema Usage", + "Register:": "Register:", "Registers": "Registers", "Reindex": "Reindex", "Reindex completed": "Reindex completed", "Reindex completed successfully": "Reindex completed successfully", + "Reindex completed! Processed {count} objects in {duration}s": "Reindex completed! Processed {count} objects in {duration}s", "Reindex failed": "Reindex failed", + "Reindex failed: %s": "Reindex failed: %s", + "Reject": "Reject", + "Rejected": "Rejected", + "Related Cases": "Related Cases", "Relations": "Relations", "Remote Version": "Remote Version", "Remove": "Remove", + "Remove Deck card": "Remove Deck card", + "Remove avatar": "Remove avatar", + "Remove contact": "Remove contact", "Remove filter": "Remove filter", "Remove from favorites": "Remove from favorites", + "Remove from list": "Remove from list", "Remove group": "Remove group", + "Remove link": "Remove link", + "Remove link to {name}": "Remove link to {name}", + "Remove link to {name}?": "Remove link to {name}?", + "Remove link to {title}": "Remove link to {title}", + "Remove oneOf entry": "Remove oneOf entry", + "Remove property after write-back": "Remove property after write-back", + "Remove {title}": "Remove {title}", + "Remove {userId} from this organisation?": "Remove {userId} from this organisation?", "Removed": "Removed", "Removed from favorites": "Removed from favorites", + "Removing schema {schema}...": "Removing schema {schema}...", "Rename Conversation": "Rename Conversation", "Rename conversation": "Rename conversation", "Replicas": "Replicas", + "Reports": "Reports", "Repository": "Repository", "Request Body": "Request Body", "Request Data": "Request Data", "Request Details": "Request Details", + "Request Quota (per day)": "Request Quota (per day)", + "Request account deactivation": "Request account deactivation", + "Request account deactivation. This will notify administrators for review.": "Request account deactivation. This will notify administrators for review.", "Request body must be JSON with a \"query\" field": "Request body must be JSON with a \"query\" field", "Request timeout in seconds": "Request timeout in seconds", + "Requested at": "Requested at", + "Required": "Required", + "Required Token Scopes": "Required Token Scopes", "Required field": "Required field", "Required status is inconsistent": "Required status is inconsistent", + "Requires Dolphin AI for OCR text extraction": "Requires Dolphin AI for OCR text extraction", "Rerun Search": "Rerun Search", + "Rerunning search: {searchTerm}": "Rerunning search: {searchTerm}", "Reset Changes": "Reset Changes", "Reset Filters": "Reset Filters", + "Resource Quotas": "Resource Quotas", "Response Body": "Response Body", "Response Details": "Response Details", "Response Time:": "Response Time:", "Restore": "Restore", "Restore conversation": "Restore conversation", "Restore or permanently delete items": "Restore or permanently delete items", + "Restore {count} object": "Restore {count} object", + "Restore {count} objects": "Restore {count} objects", "Result Count Range": "Result Count Range", "Results": "Results", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Retention period (ISO-8601 duration, e.g. P10Y, P30D)", + "Retention settings updated successfully": "Retention settings updated successfully", "Retrieve all objects for this schema": "Retrieve all objects for this schema", "Retry": "Retry", "Retry Failed Extractions": "Retry Failed Extractions", "Retry Policy": "Retry Policy", "Retry completed": "Retry completed", + "Retry upload": "Retry upload", "Retrying...": "Retrying...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Review the selected objects below. You can remove any objects you don't want to include.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.", + "Revoke": "Revoke", + "Right": "Right", "Risk Level": "Risk Level", + "Risk level": "Risk level", + "Roles and Permissions": "Roles and Permissions", "Roles updated successfully": "Roles updated successfully", + "Run compliance scan": "Run compliance scan", "SOLR Connection Settings": "SOLR Connection Settings", + "SOLR Index Warmup": "SOLR Index Warmup", "SOLR Version:": "SOLR Version:", "SOLR actions menu": "SOLR actions menu", "SOLR base path (usually /solr)": "SOLR base path (usually /solr)", + "SOLR collection deleted successfully": "SOLR collection deleted successfully", + "SOLR connection test failed: {error}": "SOLR connection test failed: {error}", + "SOLR connection test successful!": "SOLR connection test successful!", + "SOLR index warmup completed successfully": "SOLR index warmup completed successfully", "SOLR is disabled": "SOLR is disabled", "SOLR is not available or not configured": "SOLR is not available or not configured", "SOLR logging disabled": "SOLR logging disabled", @@ -942,37 +1710,59 @@ "SOLR search enabled": "SOLR search enabled", "SOLR server hostname or IP address": "SOLR server hostname or IP address", "SOLR server port number (optional, defaults to 8983)": "SOLR server port number (optional, defaults to 8983)", + "SOLR settings updated successfully": "SOLR settings updated successfully", "SOLR setup completed successfully": "SOLR setup completed successfully", + "SOLR setup completed successfully!": "SOLR setup completed successfully!", + "SOLR setup error: %s": "SOLR setup error: %s", "SOLR setup failed": "SOLR setup failed", "SOLR setup failed - check logs": "SOLR setup failed - check logs", + "SOLR setup failed. Please check the configuration and try again.": "SOLR setup failed. Please check the configuration and try again.", + "SOLR setup failed: {error}": "SOLR setup failed: {error}", + "SOLR warmup failed: {error}": "SOLR warmup failed: {error}", "Sample Values:": "Sample Values:", "Save": "Save", "Save Configuration": "Save Configuration", "Save Connection Settings": "Save Connection Settings", "Save Roles": "Save Roles", "Save Settings": "Save Settings", + "Save changes": "Save changes", "Save current search as view": "Save current search as view", + "Save labels": "Save labels", + "Save tags for {name}": "Save tags for {name}", + "Save the organisation first to manage users.": "Save the organisation first to manage users.", "Saved Views": "Saved Views", "Saving...": "Saving...", + "Scheduled": "Scheduled", "Schema": "Schema", "Schema ID": "Schema ID", "Schema Name": "Schema Name", "Schema Statistics": "Schema Statistics", + "Schema annotation": "Schema annotation", + "Schema configuration updated successfully for {schema}": "Schema configuration updated successfully for {schema}", "Schema depublished successfully": "Schema depublished successfully", "Schema objects deletion completed successfully": "Schema objects deletion completed successfully", "Schema published successfully": "Schema published successfully", + "Schema reference ($ref)": "Schema reference ($ref)", "Schema successfully created": "Schema successfully created", "Schema successfully updated": "Schema successfully updated", + "Schema {schema} removed successfully": "Schema {schema} removed successfully", + "Schema:": "Schema:", "Schemas": "Schemas", "Scheme": "Scheme", + "Search": "Search", "Search / Views": "Search / Views", + "Search / views": "Search / views", "Search Activity": "Search Activity", "Search Analytics": "Search Analytics", + "Search Configuration": "Search Configuration", "Search Entities": "Search Entities", "Search Files": "Search Files", "Search GitHub": "Search GitHub", "Search GitLab": "Search GitLab", "Search Method": "Search Method", + "Search Mode": "Search Mode", + "Search Query": "Search Query", + "Search Results": "Search Results", "Search Term": "Search Term", "Search Trail Details": "Search Trail Details", "Search Trail Management": "Search Trail Management", @@ -980,17 +1770,34 @@ "Search Trails": "Search Trails", "Search Views": "Search Views", "Search Webhooks": "Search Webhooks", + "Search agents...": "Search agents...", + "Search applications...": "Search applications...", "Search by file name or path": "Search by file name or path", "Search by name or URL": "Search by name or URL", + "Search by title or UUID...": "Search by title or UUID...", "Search by value": "Search by value", + "Search configurations": "Search configurations", + "Search data sources...": "Search data sources...", + "Search groups...": "Search groups...", + "Search in Database Objects": "Search in Database Objects", "Search in Files": "Search in Files", "Search in Objects": "Search in Objects", + "Search objects": "Search objects", + "Search objects...": "Search objects...", "Search property names...": "Search property names...", + "Search registers...": "Search registers...", + "Search schemas...": "Search schemas...", "Search trail deletion not implemented yet": "Search trail deletion not implemented yet", "Search views...": "Search views...", + "Search {name}...": "Search {name}...", + "Searching...": "Searching...", "Secret": "Secret", "Secret key for HMAC signature generation (optional)": "Secret key for HMAC signature generation (optional)", + "Security": "Security", + "See organisations as a table": "See organisations as a table", + "See organisations as cards": "See organisations as cards", "Select AI Agent": "Select AI Agent", + "Select Active Organisation": "Select Active Organisation", "Select All": "Select All", "Select ConfigSet": "Select ConfigSet", "Select File Types to Vectorize:": "Select File Types to Vectorize:", @@ -999,82 +1806,182 @@ "Select Views to Vectorize:": "Select Views to Vectorize:", "Select a Nextcloud group to add": "Select a Nextcloud group to add", "Select a branch": "Select a branch", + "Select a date property": "Select a date property", "Select a model or type a custom model name": "Select a model or type a custom model name", + "Select a register": "Select a register", + "Select a register...": "Select a register...", "Select a repository": "Select a repository", "Select a repository you have write access to": "Select a repository you have write access to", + "Select a schema": "Select a schema", + "Select a schema...": "Select a schema...", + "Select a user": "Select a user", + "Select agent type": "Select agent type", + "Select all entities": "Select all entities", "Select an AI Agent": "Select an AI Agent", "Select an AI agent to begin chatting with your data.": "Select an AI agent to begin chatting with your data.", + "Select an email to see linked objects": "Select an email to see linked objects", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Select an organisation and user to add them as a member. Search for organisations by name.", "Select backend": "Select backend", "Select chat model": "Select chat model", "Select collection for files": "Select collection for files", "Select collection for objects": "Select collection for objects", + "Select configuration type...": "Select configuration type...", "Select default organisation": "Select default organisation", + "Select entity {value}": "Select entity {value}", "Select event to listen to...": "Select event to listen to...", + "Select fields to display": "Select fields to display", + "Select files with the correct extension": "Select files with the correct extension", + "Select group": "Select group", + "Select groups (optional)": "Select groups (optional)", + "Select groups to notify...": "Select groups to notify...", + "Select groups with access to this agent": "Select groups with access to this agent", + "Select groups...": "Select groups...", "Select model": "Select model", + "Select one or more registers": "Select one or more registers", + "Select one or more schemas": "Select one or more schemas", "Select options...": "Select options...", + "Select or create labels, or select \"No label\" to add files": "Select or create labels, or select \"No label\" to add files", + "Select owner application (optional)...": "Select owner application (optional)...", "Select period": "Select period", "Select property to send as payload": "Select property to send as payload", "Select provider": "Select provider", "Select registers and schemas to save a view": "Select registers and schemas to save a view", "Select retry policy": "Select retry policy", + "Select role": "Select role", + "Select schemas (empty = all schemas)": "Select schemas (empty = all schemas)", + "Select schemas to warm up": "Select schemas to warm up", + "Select search mode": "Select search mode", + "Select source type...": "Select source type...", + "Select target type": "Select target type", "Select the branch to publish to": "Select the branch to publish to", "Select the event this webhook should listen to": "Select the event this webhook should listen to", + "Select views to filter data (optional)": "Select views to filter data (optional)", + "Select views to limit which data the agent can access": "Select views to limit which data the agent can access", "Select views to vectorize:": "Select views to vectorize:", "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.", "Select which data views the AI can search": "Select which data views the AI can search", "Select which property from the event should be used as the webhook payload data": "Select which property from the event should be used as the webhook payload data", "Select which tools the AI can use to perform actions": "Select which tools the AI can use to perform actions", + "Select {fieldName} values": "Select {fieldName} values", "Selected": "Selected", "Selected Groups": "Selected Groups", + "Selected Objects ({count})": "Selected Objects ({count})", "Selected audit trails deleted successfully": "Selected audit trails deleted successfully", "Selected search trails deleted successfully": "Selected search trails deleted successfully", "Selected users can access this view": "Selected users can access this view", "Send additional feedback": "Send additional feedback", "Send as CloudEvent": "Send as CloudEvent", + "Send as cloudevent": "Send as cloudevent", "Sensitive PII": "Sensitive PII", "Serial Mode (Safer, slower)": "Serial Mode (Safer, slower)", "Serial:": "Serial:", "Server Configuration": "Server Configuration", "Server Information": "Server Information", + "Set as Active": "Set as Active", + "Set as active organisation": "Set as active organisation", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Set limits for API usage and token consumption. Use 0 for unlimited resources.", "Settings": "Settings", + "Settings updated successfully": "Settings updated successfully", "Shards": "Shards", "Share": "Share", "Share with Groups": "Share with Groups", "Share with Users": "Share with Users", + "Show": "Show", "Show Filters": "Show Filters", "Show less": "Show less", "Show only entries with changes": "Show only entries with changes", + "Showing {count} dashboard(s)": "Showing {count} dashboard(s)", + "Showing {count} processing activities": "Showing {count} processing activities", + "Showing {showing} of {total} agents": "Showing {showing} of {total} agents", + "Showing {showing} of {total} applications": "Showing {showing} of {total} applications", + "Showing {showing} of {total} audit trail entries": "Showing {showing} of {total} audit trail entries", + "Showing {showing} of {total} configurations": "Showing {showing} of {total} configurations", + "Showing {showing} of {total} deleted items": "Showing {showing} of {total} deleted items", + "Showing {showing} of {total} entities": "Showing {showing} of {total} entities", + "Showing {showing} of {total} files": "Showing {showing} of {total} files", + "Showing {showing} of {total} log entries": "Showing {showing} of {total} log entries", + "Showing {showing} of {total} search trail entries": "Showing {showing} of {total} search trail entries", + "Showing {showing} of {total} sources": "Showing {showing} of {total} sources", + "Showing {showing} of {total} templates": "Showing {showing} of {total} templates", + "Showing {showing} of {total} webhooks": "Showing {showing} of {total} webhooks", + "Showing {shown} of {total} organisations": "Showing {shown} of {total} organisations", "Simple": "Simple", "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)", "Size": "Size", "Slug": "Slug", "Soft Delete Mode": "Soft Delete Mode", "Soft Deleted Items": "Soft Deleted Items", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Soft delete all objects for this schema ({active} active, {deleted} already deleted)", "SolrCloud mode enabled": "SolrCloud mode enabled", + "Something went wrong while adding files": "Something went wrong while adding files", "Source": "Source", + "Source object not found": "Source object not found", + "Source successfully updated": "Source successfully updated", "Sources": "Sources", "Sources:": "Sources:", + "Special Rights": "Special Rights", "Standalone SOLR mode": "Standalone SOLR mode", "Start Conversation": "Start Conversation", + "Start Date Field": "Start Date Field", "Start Vectorization": "Start Vectorization", "Start a conversation": "Start a conversation", "Starting names cache warmup...": "Starting names cache warmup...", + "Starting validation for {schema}...": "Starting validation for {schema}...", + "Starting {action} for {schema}...": "Starting {action} for {schema}...", "Starting...": "Starting...", "Statistics": "Statistics", + "Statistics and Metrics": "Statistics and Metrics", + "Statistics refreshed": "Statistics refreshed", "Stats calculation not yet implemented": "Stats calculation not yet implemented", "Status": "Status", "Status Code": "Status Code", + "Step": "Step", + "Storage": "Storage", + "Storage Quota (MB)": "Storage Quota (MB)", "String Constraints": "String Constraints", + "Subject identifier (email, BSN, name, etc.)": "Subject identifier (email, BSN, name, etc.)", + "Subscribe": "Subscribe", + "Subscribed": "Subscribed", "Success": "Success", "Success Rate": "Success Rate", "Success Status": "Success Status", "Successful": "Successful", + "Successfully added user to organisation: {name}": "Successfully added user to organisation: {name}", + "Successfully cleared {count} audit trails": "Successfully cleared {count} audit trails", + "Successfully cleared {count} blob storage objects": "Successfully cleared {count} blob storage objects", + "Successfully cleared {count} search trails": "Successfully cleared {count} search trails", + "Successfully created {count} SOLR fields": "Successfully created {count} SOLR fields", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Successfully deleted {count} embeddings. Please re-vectorize your data.", + "Successfully deleted {count} objects for {schema}": "Successfully deleted {count} objects for {schema}", + "Successfully fixed {count} SOLR field configurations": "Successfully fixed {count} SOLR field configurations", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects", "Successfully joined organisation": "Successfully joined organisation", + "Successfully permanently deleted {count} objects": "Successfully permanently deleted {count} objects", + "Successfully processed {count} files!": "Successfully processed {count} files!", + "Successfully restored {count} object": "Successfully restored {count} object", + "Successfully restored {count} objects": "Successfully restored {count} objects", + "Successfully saved configuration for {count} facets!": "Successfully saved configuration for {count} facets!", + "Supported File Types": "Supported File Types", + "Switch Active Organisation": "Switch Active Organisation", + "Switch Organisation": "Switch Organisation", "Sync Table": "Sync Table", "Synchronization Settings": "Synchronization Settings", + "Syncing magic table for {schema}...": "Syncing magic table for {schema}...", + "Syncs": "Syncs", "System Default": "System Default", + "System Prompt": "System Prompt", + "System announcements": "System announcements", + "Table": "Table", + "Target ID": "Target ID", + "Target Type*": "Target Type*", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", "Technical Description": "Technical Description", + "Technical Details": "Technical Details", "Technical description for developers and administrators": "Technical description for developers and administrators", + "Technical measures": "Technical measures", + "Technische maatregelen": "Technische maatregelen", "Temperature": "Temperature", "Templates": "Templates", "Test": "Test", @@ -1086,18 +1993,31 @@ "Test webhook delivery failed": "Test webhook delivery failed", "Test webhook sent successfully": "Test webhook sent successfully", "Testing...": "Testing...", + "Text Chunk #{id}": "Text Chunk #{id}", + "Text Extraction": "Text Extraction", + "Text Extraction Engine": "Text Extraction Engine", + "Text chunks": "Text chunks", "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.", "Text extracted successfully": "Text extracted successfully", "Text extraction disabled": "Text extraction disabled", + "The Nextcloud Calendar app is not installed or enabled on this server.": "The Nextcloud Calendar app is not installed or enabled on this server.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "The Nextcloud Contacts app is not installed or enabled on this server.", + "The Nextcloud Deck app is not installed or enabled on this server.": "The Nextcloud Deck app is not installed or enabled on this server.", + "The Nextcloud Mail app is not installed or enabled on this server.": "The Nextcloud Mail app is not installed or enabled on this server.", "The URL where webhook events will be sent": "The URL where webhook events will be sent", "The requested conversation does not exist": "The requested conversation does not exist", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.", "The saved view and all its search configuration": "The saved view and all its search configuration", "There are no audit trail entries matching your current filters.": "There are no audit trail entries matching your current filters.", "There are no deleted items matching your current filters.": "There are no deleted items matching your current filters.", "There are no search trail entries matching your current filters.": "There are no search trail entries matching your current filters.", "There are no webhook log entries matching your filters.": "There are no webhook log entries matching your filters.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "They will be retained according to their schema's configured retention period and then permanently removed.", "This action cannot be undone.": "This action cannot be undone.", "This action cannot be undone. Make sure no collections are using this ConfigSet.": "This action cannot be undone. Make sure no collections are using this ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "This action cannot be undone. Make sure no collections are using this configset.", + "This action will submit a deactivation request to your administrators.": "This action will submit a deactivation request to your administrators.", "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "This agent has no configurable views or tools, but you can still adjust RAG settings below.", "This analysis may take some time": "This analysis may take some time", "This audit trail entry does not contain any change information.": "This audit trail entry does not contain any change information.", @@ -1105,21 +2025,44 @@ "This endpoint is deprecated. Use chunk-based endpoints instead.": "This endpoint is deprecated. Use chunk-based endpoints instead.", "This entity has no relations to objects or files": "This entity has no relations to objects or files", "This is a test webhook from OpenRegister": "This is a test webhook from OpenRegister", + "This is not valid JSON": "This is not valid JSON", + "This key already exists on this schema": "This key already exists on this schema", "This schema must use Magic Table configuration to sync": "This schema must use Magic Table configuration to sync", + "This schema must use magic table configuration to sync": "This schema must use magic table configuration to sync", "This source has no associated registers.": "This source has no associated registers.", + "This token will only be shown once. Copy it now.": "This token will only be shown once. Copy it now.", "This will copy the _default ConfigSet with the new name": "This will copy the _default ConfigSet with the new name", + "This will copy the _default configset with the new name": "This will copy the _default configset with the new name", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?", "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?", "This will permanently delete:": "This will permanently delete:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.", "Timeout (seconds)": "Timeout (seconds)", "Timestamp": "Timestamp", "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.", "Title": "Title", + "Title *": "Title *", + "Title Template": "Title Template", + "Title*": "Title*", + "To (e.g. -1 year or 2025-12-31)": "To (e.g. -1 year or 2025-12-31)", "To Date": "To Date", + "To add files larger than or equal to 512MB, go to the": "To add files larger than or equal to 512MB, go to the", "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "To adjust chunk size or strategy, go to File Configuration → Processing Limits.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "To adjust chunk size or strategy, go to file configuration → processing limits.", "To change the embedding provider or model, go to LLM Configuration.": "To change the embedding provider or model, go to LLM Configuration.", + "To change the embedding provider or model, go to LLM configuration.": "To change the embedding provider or model, go to LLM configuration.", "To date": "To date", "Toggle search sidebar": "Toggle search sidebar", + "Token Created": "Token Created", + "Token Quota (per request)": "Token Quota (per request)", + "Token copied to clipboard": "Token copied to clipboard", + "Token name": "Token name", + "Token revoked": "Token revoked", "Tools": "Tools", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.", + "Tools respect the agent's own role and organization boundaries.": "Tools respect the agent's own role and organization boundaries.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Tools respect the agent's views, permissions, and organization boundaries.", "Top Deleters": "Top Deleters", "Total": "Total", "Total Audit Trails": "Total Audit Trails", @@ -1133,69 +2076,111 @@ "Total Searches": "Total Searches", "Total Size": "Total Size", "Total Storage": "Total Storage", + "Total events": "Total events", "Total:": "Total:", "Totals": "Totals", + "Try searching by UUID or with different keywords": "Try searching by UUID or with different keywords", "Type": "Type", + "Type \"DELETE COLLECTION\" to confirm:": "Type \"DELETE COLLECTION\" to confirm:", "Type Issue": "Type Issue", "Type Variations:": "Type Variations:", + "Type filter (optional, e.g. email)": "Type filter (optional, e.g. email)", "Type must be \"positive\" or \"negative\"": "Type must be \"positive\" or \"negative\"", "Type to search for groups": "Type to search for groups", + "Type to search for objects...": "Type to search for objects...", + "Type to search for organisations": "Type to search for organisations", "Type to search for users": "Type to search for users", "Type to search groups...": "Type to search groups...", "Type to search users...": "Type to search users...", + "Type to search...": "Type to search...", + "Type your username to confirm": "Type your username to confirm", "URL": "URL", "URL where Ollama is running": "URL where Ollama is running", + "UUID copied to clipboard": "UUID copied to clipboard", "UUID:": "UUID:", "Unchanged": "Unchanged", "Unique Search Terms": "Unique Search Terms", "Unique Terms": "Unique Terms", "Unknown": "Unknown", + "Unknown Object": "Unknown Object", "Unknown Register": "Unknown Register", "Unknown Schema": "Unknown Schema", + "Unknown error": "Unknown error", + "Unknown widget type: {type}": "Unknown widget type: {type}", + "Unlink email": "Unlink email", + "Unlink event": "Unlink event", + "Unnamed Object": "Unnamed Object", + "Untitled": "Untitled", "Untitled View": "Untitled View", "Update": "Update", "Update Operations": "Update Operations", + "Update available: {local} → {remote}": "Update available: {local} → {remote}", "Update configuration: ...": "Update configuration: ...", "Update register OAS: ...": "Update register OAS: ...", "Update vectors when object data changes (recommended for accurate search)": "Update vectors when object data changes (recommended for accurate search)", "Updated": "Updated", "Updated At": "Updated At", "Updated:": "Updated:", + "Upload new avatar": "Upload new avatar", + "Uploading configuration sets...": "Uploading configuration sets...", "Uptime:": "Uptime:", + "Url": "Url", "Usage Count": "Usage Count", + "Use AI agents for processing and analysis": "Use AI agents for processing and analysis", + "Use Large Language Model features": "Use Large Language Model features", "Use SolrCloud with Zookeeper for distributed search": "Use SolrCloud with Zookeeper for distributed search", + "Use SolrCloud with zookeeper for distributed search": "Use SolrCloud with zookeeper for distributed search", + "Use VNG APIs instead of MongoDB.": "Use VNG APIs instead of MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Use default ranges (Last 7/30/90 days, Last year, Older)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.", "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.", "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.", + "Use filters to narrow down entities by type or category.": "Use filters to narrow down entities by type or category.", "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.", "Used By Collections": "Used By Collections", + "Used by": "Used by", "User": "User", "User Agent Statistics": "User Agent Statistics", "User Description": "User Description", + "User Quota": "User Quota", "User rate limits cleared successfully": "User rate limits cleared successfully", "User removed successfully": "User removed successfully", "User-friendly description shown in forms and help text": "User-friendly description shown in forms and help text", "Username": "Username", "Username for SOLR authentication (optional)": "Username for SOLR authentication (optional)", "Users": "Users", + "Uses": "Uses", "Using Pre-Generated Chunks": "Using Pre-Generated Chunks", "Validate": "Validate", "Validate Objects": "Validate Objects", + "Validation completed for {schema}": "Validation completed for {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed", "Validation completed successfully": "Validation completed successfully", "Validation failed": "Validation failed", "Value": "Value", + "Value list (split on ,)": "Value list (split on ,)", "Vector Dimensions": "Vector Dimensions", "Vector Search Backend": "Vector Search Backend", "Vector field: _embedding_": "Vector field: _embedding_", "Vectorization Triggers": "Vectorization Triggers", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vectorization completed: {vectorized} objects vectorized, {failed} failed", + "Vectorization failed: {error}": "Vectorization failed: {error}", "Vectorize All Files": "Vectorize All Files", "Vectorize All Objects": "Vectorize All Objects", "Vectorize all file types": "Vectorize all file types", "Vectorize all views": "Vectorize all views", "Vectorize on object creation": "Vectorize on object creation", "Vectors will be stored in your existing object and file collections": "Vectors will be stored in your existing object and file collections", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Verifying server connectivity...", "Version": "Version", + "Version Information": "Version Information", "Very High": "Very High", + "Very high": "Very high", + "View": "View", "View API Docs": "View API Docs", + "View API Documentation": "View API Documentation", + "View APIs": "View APIs", "View Changes": "View Changes", "View Details": "View Details", "View Error": "View Error", @@ -1208,23 +2193,31 @@ "View Selection": "View Selection", "View and analyze search trail logs with advanced filtering and analytics capabilities": "View and analyze search trail logs with advanced filtering and analytics capabilities", "View and analyze system audit trails with advanced filtering capabilities": "View and analyze system audit trails with advanced filtering capabilities", + "View and manage authorization across registers and schemas": "View and manage authorization across registers and schemas", "View deleted successfully": "View deleted successfully", "View entity details and manage relations": "View entity details and manage relations", "View name": "View name", "View name is required": "View name is required", + "View object": "View object", "View saved successfully!": "View saved successfully!", "View search analytics and manage search logs": "View search analytics and manage search logs", "View successfully deleted": "View successfully deleted", "View successfully updated": "View successfully updated", "View updated successfully!": "View updated successfully!", "View webhook delivery logs and filter by webhook": "View webhook delivery logs and filter by webhook", + "View {count} more": "View {count} more", "Views": "Views", + "Visible to end users": "Visible to end users", "Visible to users": "Visible to users", "Wait for Response": "Wait for Response", "Wait for webhook response before continuing (required for request/response flows)": "Wait for webhook response before continuing (required for request/response flows)", + "Waiting for cluster synchronization...": "Waiting for cluster synchronization...", "Warmup Names Cache": "Warmup Names Cache", + "Warmup failed": "Warmup failed", "Warning": "Warning", "Warning:": "Warning:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.", "Webhook": "Webhook", "Webhook Log Details": "Webhook Log Details", "Webhook Logs": "Webhook Logs", @@ -1236,884 +2229,411 @@ "Webhook updated successfully": "Webhook updated successfully", "Webhooks": "Webhooks", "Weekly": "Weekly", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "When disabled, this facet will only show values from this schema and will include a schema filter when selected.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "When enabled, saving this object will also update the target objects to include a reference back to this object.", + "When enabled, this property will be removed from the source object after updating the target objects.": "When enabled, this property will be removed from the source object after updating the target objects.", + "Workflow Management": "Workflow Management", + "Workflows": "Workflows", "Wrap webhook payload in CloudEvents format for better interoperability": "Wrap webhook payload in CloudEvents format for better interoperability", + "Wrap webhook payload in cloudevents format for better interoperability": "Wrap webhook payload in cloudevents format for better interoperability", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", "Yes": "Yes", "You": "You", + "You are not yet a member of any organisations.": "You are not yet a member of any organisations.", "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.", "You do not have access to this conversation": "You do not have access to this conversation", + "You do not have access to this object": "You do not have access to this object", "You do not have permission to delete this conversation": "You do not have permission to delete this conversation", "You do not have permission to modify this conversation": "You do not have permission to modify this conversation", "You do not have permission to restore this conversation": "You do not have permission to restore this conversation", + "You do not have permission to view the permission matrix. Admin access is required.": "You do not have permission to view the permission matrix. Admin access is required.", + "You have selected {count} properties to add to the schema.": "You have selected {count} properties to add to the schema.", "You must be logged in to favorite views": "You must be logged in to favorite views", "You need an AI agent to start a conversation.": "You need an AI agent to start a conversation.", "Your Fireworks AI API key. Get one at": "Your Fireworks AI API key. Get one at", "Your OpenAI API key. Get one at": "Your OpenAI API key. Get one at", "Your feedback has been recorded. Optionally, you can provide additional details here...": "Your feedback has been recorded. Optionally, you can provide additional details here...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Your feedback has been recorded. optionally, you can provide additional details here...", "Zookeeper Hosts": "Zookeeper Hosts", "Zookeeper Port": "Zookeeper Port", "Zookeeper Settings (SolrCloud)": "Zookeeper Settings (SolrCloud)", "Zookeeper connection string for SolrCloud": "Zookeeper connection string for SolrCloud", "Zookeeper port number (optional, defaults to 2181)": "Zookeeper port number (optional, defaults to 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?", + "and accessible to all users in selected groups (or all users if no groups selected).": "and accessible to all users in selected groups (or all users if no groups selected).", + "and add the files there.": "and add the files there.", + "approved": "approved", + "are only accessible to invited users.": "are only accessible to invited users.", + "by": "by", "chunk": "chunk", "chunks": "chunks", "chunks processed": "chunks processed", + "completed": "completed", "configuration(s)": "configuration(s)", + "conversationId is required": "conversationId is required", + "create": "create", + "delete": "delete", + "deleted objects section": "deleted objects section", "e.g., lib/Settings/config.json": "e.g., lib/Settings/config.json", "e.g., lib/Settings/register.json": "e.g., lib/Settings/register.json", + "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", + "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", + "execute": "execute", "failed": "failed", "file": "file", "files": "files", + "file{plural}": "file{plural}", + "folder": "folder", "fw_...": "fw_...", "http://localhost:11434": "http://localhost:11434", "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "in {register}", + "in_progress": "in progress", + "inherited": "inherited", + "list": "list", + "log{plural}": "log{plural}", + "manage": "manage", "members": "members", "menu or contact someone with permission to create agents.": "menu or contact someone with permission to create agents.", + "message content is required": "message content is required", "messages": "messages", + "missing": "missing", + "myapp": "myapp", "n8n URL and API key are required": "n8n URL and API key are required", "n8n configuration saved successfully": "n8n configuration saved successfully", "n8n connection not configured": "n8n connection not configured", "n8n connection successful": "n8n connection successful", + "n8n connection test failed: {message}": "n8n connection test failed: {message}", "n8n connection test successful": "n8n connection test successful", + "n8n initialization failed: {message}": "n8n initialization failed: {message}", "n8n integration disabled": "n8n integration disabled", "n8n integration enabled": "n8n integration enabled", "n8n project initialized successfully": "n8n project initialized successfully", "n8n settings saved successfully": "n8n settings saved successfully", + "notify_push configuration guide": "notify_push configuration guide", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.", + "notify_push is installed but not yet active": "notify_push is installed but not yet active", + "objectType: object\naction: created": "objectType: object\naction: created", "objectType: object\\naction: created": "objectType: object\\naction: created", "objects": "objects", "objects processed": "objects processed", + "object{plural}": "object{plural}", "or pick a range": "or pick a range", "org-...": "org-...", + "pending": "pending", "provides a good balance between speed and accuracy.": "provides a good balance between speed and accuracy.", + "public": "public", + "read": "read", "register(s)": "register(s)", "register(s) selected": "register(s) selected", + "register{plural}": "register{plural}", + "rejected": "rejected", "required": "required", "results": "results", "schema(s)": "schema(s)", "schema(s) selected": "schema(s) selected", + "schema{plural}": "schema{plural}", + "selected objects": "selected objects", + "set": "set", "sk-...": "sk-...", "this application": "this application", "to": "to", + "type must be \"positive\" or \"negative\"": "type must be \"positive\" or \"negative\"", + "update": "update", "vectorized": "vectorized", + "view": "view", "views selected": "views selected", + "waiting": "waiting", + "{active} active, {deleted} deleted": "{active} active, {deleted} deleted", + "{count} audit trails cleared successfully": "{count} audit trails cleared successfully", + "{count} compliance issue(s) detected": "{count} compliance issue(s) detected", + "{count} deletions": "{count} deletions", + "{count} documents found": "{count} documents found", + "{count} email": "{count} email", + "{count} emails": "{count} emails", + "{count} entries": "{count} entries", + "{count} issues": "{count} issues", + "{count} members": "{count} members", + "{count} searches": "{count} searches", + "{count} selected": "{count} selected", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} object(s) matched. {erased} erased.", + "{minutes} minutes ago": "{minutes} minutes ago", + "{percentage}% of objects": "{percentage}% of objects", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} in {register} / {schema}", + "{type} cache cleared successfully": "{type} cache cleared successfully", "ℹ️ Current Configuration": "ℹ️ Current Configuration", + "ℹ️ current configuration": "ℹ️ current configuration", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?", "⚡ Batch Processing": "⚡ Batch Processing", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR can handle millions of documents with sub-second search response times...", "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information", "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Faster responses ✓ More focused answers ✗ May miss relevant information", "✨ AI Features": "✨ AI Features", - "All search trails cleared successfully": "All search trails cleared successfully", - "Multiple search trail deletion not implemented yet": "Multiple search trail deletion not implemented yet", - "No expired search trails found to clear": "No expired search trails found to clear", - "Missing message": "Missing message", - "message content is required": "message content is required", - "Missing conversation or agentUuid": "Missing conversation or agentUuid", - "Access denied": "Access denied", - "Conversation not found": "Conversation not found", - "AI service not configured": "AI service not configured", - "Failed to process message": "Failed to process message", - "Missing conversationId": "Missing conversationId", - "conversationId is required": "conversationId is required", - "Failed to fetch conversation history": "Failed to fetch conversation history", - "Failed to clear conversation": "Failed to clear conversation", - "Invalid feedback type": "Invalid feedback type", - "type must be \"positive\" or \"negative\"": "type must be \"positive\" or \"negative\"", - "Message not found": "Message not found", - "Failed to save feedback": "Failed to save feedback", - "Failed to get chat statistics": "Failed to get chat statistics", - "Not Found": "Not Found", - "Engine not found": "Engine not found", - "Consumer not found": "Consumer not found", - "Backend parameter is required": "Backend parameter is required", - "Failed to get database information: %s": "Failed to get database information: %s", - "SOLR setup error: %s": "SOLR setup error: %s", - "Reindex failed: %s": "Reindex failed: %s", - "Anonymized": "Anonymized", - "Entities detected": "Entities detected", - "Extract Now": "Extract Now", - "Extracted at": "Extracted at", - "Extraction": "Extraction", - "Failed to load extraction data": "Failed to load extraction data", - "Failed to load register data": "Failed to load register data", - "Failed to retrieve extraction status.": "Failed to retrieve extraction status.", - "Failed to retrieve objects for file.": "Failed to retrieve objects for file.", - "No extraction data available for this file": "No extraction data available for this file", - "No register objects reference this file": "No register objects reference this file", - "Not extracted": "Not extracted", - "Register Objects": "Register Objects", - "Risk level": "Risk level", - "Text chunks": "Text chunks", - "Unknown error": "Unknown error", - "Very high": "Very high", - "{title} in {register} / {schema}": "{title} in {register} / {schema}", - "Push Notifications": "Push Notifications", - "Real-time push notification status via notify_push": "Real-time push notification status via notify_push", - "Realtime push not available — the notify_push app is not installed": "Realtime push not available — the notify_push app is not installed", - "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Install the notify_push app from the Nextcloud App Store to enable real-time updates.", - "Open Nextcloud App Store": "Open Nextcloud App Store", - "notify_push is installed but not yet active": "notify_push is installed but not yet active", - "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.", - "notify_push configuration guide": "notify_push configuration guide", - "Realtime push active": "Realtime push active", - "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.", - "(Dolphin required)": "(Dolphin required)", - "({count} selected)": "({count} selected)", - ", {failed} failed": ", {failed} failed", - "0 = unlimited": "0 = unlimited", - "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?", - "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?", - "A deactivation request is pending.": "A deactivation request is pending.", - "Access organisation dashboard and analytics": "Access organisation dashboard and analytics", - "Account": "Account", - "Active organisation changed successfully": "Active organisation changed successfully", - "Active Organisation:": "Active Organisation:", - "Activity": "Activity", - "Add": "Add", - "Add a file or files": "Add a file or files", - "Add Attachment": "Add Attachment", - "Add endpoint": "Add endpoint", - "Add oneOf entry": "Add oneOf entry", - "Add Property to '{title}'": "Add Property to '{title}'", - "Add range": "Add range", - "Add Register": "Add Register", - "Add User": "Add User", - "Add User to Organisation": "Add User to Organisation", - "Addresses": "Addresses", - "Agent successfully updated": "Agent successfully updated", - "Agent Type": "Agent Type", - "Aggregated across schemas": "Aggregated across schemas", - "All file types": "All file types", - "All-day events": "All-day events", - "Allowed MIME Types": "Allowed MIME Types", - "Already linked": "Already linked", - "An error occurred while deleting the object": "An error occurred while deleting the object", - "An error occurred while restoring the objects": "An error occurred while restoring the objects", - "An error occurred while saving the organisation": "An error occurred while saving the organisation", - "and add the files there.": "and add the files there.", - "Anonymous Group": "Anonymous Group", - "API Request Quota (requests/day)": "API Request Quota (requests/day)", - "API Token Configuration": "API Token Configuration", - "API Tokens": "API Tokens", - "App ID": "App ID", - "Application identifier for this configuration (optional)": "Application identifier for this configuration (optional)", - "Application settings": "Application settings", - "Application successfully created": "Application successfully created", - "Application successfully updated": "Application successfully updated", - "Apply": "Apply", - "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Apply a role to all schemas in this register that do not have explicit authorization overrides.", - "Approval Progress": "Approval Progress", - "Approve": "Approve", - "Approved": "Approved", - "approved": "approved", - "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone", - "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone", - "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.", - "Are you sure you want to delete this configset?": "Are you sure you want to delete this configset?", - "Are you sure you want to leave '{name}'?": "Are you sure you want to leave '{name}'?", - "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing", - "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.", - "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.", - "Array Object Configuration:": "Array Object Configuration:", - "Assigned collaborative tags": "Assigned collaborative tags", - "Assignment notifications": "Assignment notifications", - "Auto (e.g. Y for year, F Y for month)": "Auto (e.g. Y for year, F Y for month)", - "Auto share": "Auto share", - "Automatically connect to the federation network.": "Automatically connect to the federation network.", - "Automatically update catalogues.": "Automatically update catalogues.", - "Available placeholders:": "Available placeholders:", - "Avatar": "Avatar", - "Avatar changes are not supported by your authentication provider.": "Avatar changes are not supported by your authentication provider.", - "Avatar removed": "Avatar removed", - "Avatar updated successfully": "Avatar updated successfully", - "Bandwidth Quota (MB/month)": "Bandwidth Quota (MB/month)", - "Behavior": "Behavior", - "Bulk Role Assignment: {title}": "Bulk Role Assignment: {title}", - "Cache grew from {old} to {new} entries.": "Cache grew from {old} to {new} entries.", - "Cache shrunk from {old} to {new} entries.": "Cache shrunk from {old} to {new} entries.", - "Cache stayed the same at {size} entries.": "Cache stayed the same at {size} entries.", - "Calendar": "Calendar", - "Calendar name": "Calendar name", - "Calendar Provider Configuration": "Calendar Provider Configuration", - "Cancel deactivation request": "Cancel deactivation request", - "Cannot edit: This register is managed by external configuration {title}": "Cannot edit: This register is managed by external configuration {title}", - "Cannot remove schema with existing objects ({count} objects)": "Cannot remove schema with existing objects ({count} objects)", - "Cannot remove schema {schema} because it contains {count} objects": "Cannot remove schema {schema} because it contains {count} objects", - "Cards": "Cards", - "Cascade delete": "Cascade delete", - "Change password": "Change password", - "Changes for Audit Trail #{id}": "Changes for Audit Trail #{id}", - "Chat test failed: {error}": "Chat test failed: {error}", - "Cleanup completed successfully. Deleted {count} entries.": "Cleanup completed successfully. Deleted {count} entries.", - "Cleanup failed: {error}": "Cleanup failed: {error}", - "Clear filters": "Clear filters", - "Cluster name": "Cluster name", - "Collapse details": "Collapse details", - "Color": "Color", - "Comment...": "Comment...", - "completed": "completed", - "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations", - "Confidence: {confidence}%": "Confidence: {confidence}%", - "Configuration is up to date": "Configuration is up to date", - "Configuration saved successfully.": "Configuration saved successfully.", - "Configuration type (default, application, etc.)": "Configuration type (default, application, etc.)", - "Configure Apache SOLR search engine for advanced search capabilities": "Configure Apache SOLR search engine for advanced search capabilities", - "Configure API tokens for external service integrations": "Configure API tokens for external service integrations", - "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.", - "Configure default organisation and organisation-related settings": "Configure default organisation and organisation-related settings", - "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.", - "Configure multi-organization support and tenant isolation": "Configure multi-organization support and tenant isolation", - "Configure SOLR Facets": "Configure SOLR Facets", - "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configure this schema to surface objects as events in the Nextcloud Calendar app.", - "Confirm Account Deactivation": "Confirm Account Deactivation", - "Confirm deactivation": "Confirm deactivation", - "Confirm Object Deletion": "Confirm Object Deletion", - "Confirm Object Restoration": "Confirm Object Restoration", - "Confirm Permanent Object Deletion": "Confirm Permanent Object Deletion", - "Contracts": "Contracts", - "Conversation started with {agent}": "Conversation started with {agent}", - "Conversations": "Conversations", - "Converting {schema} to blob storage...": "Converting {schema} to blob storage...", - "Converting {schema} to magic table...": "Converting {schema} to magic table...", - "Copy of {name}": "Copy of {name}", - "Copy to clipboard": "Copy to clipboard", - "create": "create", - "Create a new configset based on the _default template": "Create a new configset based on the _default template", - "Create a new SOLR collection from an existing configset": "Create a new SOLR collection from an existing configset", - "Create another": "Create another", - "Create API Token": "Create API Token", - "Create copies of": "Create copies of", - "Create new token": "Create new token", - "Create Organisation": "Create Organisation", - "Current": "Current", - "Current password": "Current password", - "Custom display title for this facet": "Custom display title for this facet", - "Custom HTTP headers (one per line, format: header-name: value)": "Custom HTTP headers (one per line, format: header-name: value)", - "Custom pattern:": "Custom pattern:", - "Custom Ranges:": "Custom Ranges:", - "Data": "Data", - "Data Sources": "Data Sources", - "Data Views": "Data Views", - "Date Faceting:": "Date Faceting:", - "Dates": "Dates", - "Deactivation request cancelled": "Deactivation request cancelled", - "Deactivation request submitted": "Deactivation request submitted", - "Default Object Tenant": "Default Object Tenant", - "Default Owner": "Default Owner", - "Default User (for cron/background jobs)": "Default User (for cron/background jobs)", - "Default User Tenant": "Default User Tenant", - "Default value": "Default value", - "Default value (Email)": "Default value (Email)", - "Default value (Password)": "Default value (Password)", - "Default value (Phone number)": "Default value (Phone number)", - "Default value (Regex)": "Default value (Regex)", - "Defaults to current user. Select a different user if needed.": "Defaults to current user. Select a different user if needed.", - "delete": "delete", - "Delete ({count})": "Delete ({count})", - "DELETE COLLECTION": "DELETE COLLECTION", - "Delete SOLR Collection": "Delete SOLR Collection", - "deleted objects section": "deleted objects section", - "Deleting SOLR Collection...": "Deleting SOLR Collection...", - "Depublish": "Depublish", - "Description shown as tooltip": "Description shown as tooltip", - "Description Template": "Description Template", - "Detected: {date}": "Detected: {date}", - "Direct permission": "Direct permission", - "Disable this to make the agent": "Disable this to make the agent", - "Display Format": "Display Format", - "Display Name": "Display Name", - "Dolphin OCR enabled": "Dolphin OCR enabled", - "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).", - "Download API Specification": "Download API Specification", - "Drag a file or files here": "Drag a file or files here", - "Dry run completed: {count} fields would be created": "Dry run completed: {count} fields would be created", - "Dry run completed: {count} fields would be fixed": "Dry run completed: {count} fields would be fixed", - "Duration in seconds (optional)": "Duration in seconds (optional)", - "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", - "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", - "Edit Agent": "Edit Agent", - "Edit Application": "Edit Application", - "Edit Configuration": "Edit Configuration", - "Edit Endpoint": "Edit Endpoint", - "Edit labels": "Edit labels", - "Edit Organisation": "Edit Organisation", - "Edit Property '{key}' of '{title}'": "Edit Property '{key}' of '{title}'", - "Edit Source": "Edit Source", - "Edit tags for {name}": "Edit tags for {name}", - "Email Addresses": "Email Addresses", - "Email digest frequency": "Email digest frequency", - "Embedding test failed: {error}": "Embedding test failed: {error}", - "Enable calendar provider": "Enable calendar provider", - "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.", - "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.", - "Enable RAG": "Enable RAG", - "Enable tools that allow the agent to interact with data through function calling.": "Enable tools that allow the agent to interact with data through function calling.", - "Enable write-back to target objects": "Enable write-back to target objects", - "End Date Field": "End Date Field", - "Endpoint Path*": "Endpoint Path*", - "Enter agent description (optional)": "Enter agent description (optional)", - "Enter agent name": "Enter agent name", - "Enter application description (optional)": "Enter application description (optional)", - "Enter application name": "Enter application name", - "Enter configuration description (optional)": "Enter configuration description (optional)", - "Enter configuration title": "Enter configuration title", - "Enter Nextcloud usernames to grant access to this private agent": "Enter Nextcloud usernames to grant access to this private agent", - "Enter organisation description (optional)": "Enter organisation description (optional)", - "Enter organisation name": "Enter organisation name", - "Enter search terms or leave empty to browse all": "Enter search terms or leave empty to browse all", - "Enter system prompt for the agent": "Enter system prompt for the agent", - "Enter username and press Enter": "Enter username and press Enter", - "Enter your API key": "Enter your API key", - "Enter your object here...": "Enter your object here...", - "Enter your schema here...": "Enter your schema here...", - "Entity ID: {id}": "Entity ID: {id}", - "Entity Recognition": "Entity Recognition", - "Entity Recognition Method": "Entity Recognition Method", - "Error deleting audit trails: {error}": "Error deleting audit trails: {error}", - "Error deleting search trails: {error}": "Error deleting search trails: {error}", - "Example": "Example", - "Example value for this property": "Example value for this property", - "Exclusive maximum": "Exclusive maximum", - "Exclusive minimum": "Exclusive minimum", - "execute": "execute", - "Expiration": "Expiration", - "Expires": "Expires", - "Expires in (e.g., 90d)": "Expires in (e.g., 90d)", - "Export \"{schema}\" objects from \"{register}\"": "Export \"{schema}\" objects from \"{register}\"", - "Export Configuration": "Export Configuration", - "Export downloaded successfully": "Export downloaded successfully", - "Export failed: {error}": "Export failed: {error}", - "Export Format:": "Export Format:", - "Export is rate limited. Please try again later.": "Export is rate limited. Please try again later.", - "Export my data": "Export my data", - "Export Objects": "Export Objects", - "Exporting...": "Exporting...", - "Extra Query Parameters": "Extra Query Parameters", - "Extract now": "Extract now", - "Extraction error for {file}: {error}": "Extraction error for {file}: {error}", - "Extraction Mode": "Extraction Mode", - "Extraction Scope": "Extraction Scope", - "Facet Description": "Facet Description", - "Facet Order": "Facet Order", - "Facet Title": "Facet Title", - "Facet Type": "Facet Type", - "Facetable": "Facetable", - "Faceting Configuration:": "Faceting Configuration:", - "Failed to add user to organisation": "Failed to add user to organisation", - "Failed to cancel deactivation": "Failed to cancel deactivation", - "Failed to change active organisation: {error}": "Failed to change active organisation: {error}", - "Failed to change password": "Failed to change password", - "Failed to check version: {error}": "Failed to check version: {error}", - "Failed to clear embeddings: {error}": "Failed to clear embeddings: {error}", - "Failed to copy to clipboard": "Failed to copy to clipboard", - "Failed to copy token": "Failed to copy token", - "Failed to create missing SOLR fields: {error}": "Failed to create missing SOLR fields: {error}", - "Failed to create token": "Failed to create token", - "Failed to delete objects for {schema}: {error}": "Failed to delete objects for {schema}: {error}", - "Failed to delete SOLR collection: {error}": "Failed to delete SOLR collection: {error}", - "Failed to depublish register: {error}": "Failed to depublish register: {error}", - "Failed to depublish schema: {error}": "Failed to depublish schema: {error}", - "Failed to depublish: {error}": "Failed to depublish: {error}", - "Failed to export data": "Failed to export data", - "Failed to export objects": "Failed to export objects", - "Failed to fix mismatched SOLR fields: {error}": "Failed to fix mismatched SOLR fields: {error}", - "Failed to get response: {error}": "Failed to get response: {error}", - "Failed to leave organisation: {error}": "Failed to leave organisation: {error}", - "Failed to link object": "Failed to link object", - "Failed to load view: {error}": "Failed to load view: {error}", - "Failed to publish register: {error}": "Failed to publish register: {error}", - "Failed to publish schema: {error}": "Failed to publish schema: {error}", - "Failed to publish: {error}": "Failed to publish: {error}", - "Failed to remove avatar": "Failed to remove avatar", - "Failed to remove link": "Failed to remove link", - "Failed to remove member: {error}": "Failed to remove member: {error}", - "Failed to remove schema {schema}: {error}": "Failed to remove schema {schema}: {error}", - "Failed to remove user from organisation": "Failed to remove user from organisation", - "Failed to request deactivation": "Failed to request deactivation", - "Failed to revoke token": "Failed to revoke token", - "Failed to save configuration.": "Failed to save configuration.", - "Failed to save configuration: {error}": "Failed to save configuration: {error}", - "Failed to save facet configuration": "Failed to save facet configuration", - "Failed to save preferences": "Failed to save preferences", - "Failed to save view: {error}": "Failed to save view: {error}", - "Failed to set active organisation: {error}": "Failed to set active organisation: {error}", - "Failed to start file warmup: {error}": "Failed to start file warmup: {error}", - "Failed to start vectorization: {error}": "Failed to start vectorization: {error}", - "Failed to switch organisation: {error}": "Failed to switch organisation: {error}", - "Failed to sync magic table for {schema}: {error}": "Failed to sync magic table for {schema}: {error}", - "Failed to update schema configuration for {schema}: {error}": "Failed to update schema configuration for {schema}: {error}", - "Failed to update view: {error}": "Failed to update view: {error}", - "Failed to upload avatar": "Failed to upload avatar", - "Failed to validate {schema}: {error}": "Failed to validate {schema}: {error}", - "Failed to vectorize files: {error}": "Failed to vectorize files: {error}", - "Failed to warmup names cache: {error}": "Failed to warmup names cache: {error}", - "Federation": "Federation", - "Fields to Display": "Fields to Display", - "File #{id}": "File #{id}", - "File Handling": "File Handling", - "File Location": "File Location", - "File name": "File name", - "File Types to Process": "File Types to Process", - "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.", - "Files added successfully": "Files added successfully", - "Files → filecollection, objects → objectcollection": "Files → filecollection, objects → objectcollection", - "file{plural}": "file{plural}", - "Filter by type": "Filter by type", - "Filter fields...": "Filter fields...", - "folder": "folder", - "From (e.g. -7 days or 2025-01-01)": "From (e.g. -7 days or 2025-01-01)", - "Function Tools": "Function Tools", - "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Get real-time insights into your organization's data health, growth trends, and quality metrics.", - "GitHub Personal Access Token": "GitHub Personal Access Token", - "GitHub token is valid! Username: {username}": "GitHub token is valid! Username: {username}", - "GitHub token test failed: {message}": "GitHub token test failed: {message}", - "GitLab Personal Access Token": "GitLab Personal Access Token", - "GitLab token is valid! Username: {username}": "GitLab token is valid! Username: {username}", - "GitLab token test failed: {message}": "GitLab token test failed: {message}", - "Go to linked Register": "Go to linked Register", - "Go to linked Schema": "Go to linked Schema", - "Go to organisation": "Go to organisation", - "Grant additional permissions beyond standard CRUD operations": "Grant additional permissions beyond standard CRUD operations", - "Group Quota": "Group Quota", - "Groups": "Groups", - "Here you can configure the details for various connections.": "Here you can configure the details for various connections.", - "Here you can configure the details for your organisation.": "Here you can configure the details for your organisation.", - "Hide": "Hide", - "Hide in collection view": "Hide in collection view", - "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", - "IBANs": "IBANs", - "ID of input mapping (optional)": "ID of input mapping (optional)", - "ID of output mapping (optional)": "ID of output mapping (optional)", - "ID of the target resource": "ID of the target resource", - "ID: {id}": "ID: {id}", - "Import Configuration": "Import Configuration", - "Import Data into Register": "Import Data into Register", - "in {register}": "in {register}", - "in_progress": "in progress", - "Inactive organisations cannot be used": "Inactive organisations cannot be used", - "Include ids and names of related objects for better contextual search": "Include ids and names of related objects for better contextual search", - "Index": "Index", - "Information about the current OpenRegister installation": "Information about the current OpenRegister installation", - "inherited": "inherited", - "Inherited from register": "Inherited from register", - "Inherits permissions from register": "Inherits permissions from register", - "Input Mapping": "Input Mapping", - "Inspect SOLR Index": "Inspect SOLR Index", - "Internet location (URL) of this installation": "Internet location (URL) of this installation", - "Interval": "Interval", - "Invite Users": "Invite Users", - "Invited Users": "Invited Users", - "Key": "Key", - "Label": "Label", - "Labels": "Labels", - "Leave empty to allow all users access": "Leave empty to allow all users access", - "Leave off for auto-detection from property format.": "Leave off for auto-detection from property format.", - "Left organisation successfully": "Left organisation successfully", - "Link": "Link", - "Link an object to see it here.": "Link an object to see it here.", - "Link another object": "Link another object", - "Link removed": "Link removed", - "Link to Object": "Link to Object", - "Link to {name}": "Link to {name}", - "Linked by {user}": "Linked by {user}", - "Linked Objects": "Linked Objects", - "Linked to {name}": "Linked to {name}", - "list": "list", - "Load more": "Load more", - "Loading activity...": "Loading activity...", - "Loading API tokens...": "Loading API tokens...", - "Loading available tools...": "Loading available tools...", - "Loading cache statistics...": "Loading cache statistics...", - "Loading endpoints": "Loading endpoints", - "Loading entities...": "Loading entities...", - "Loading linked objects...": "Loading linked objects...", - "Loading LLM configuration...": "Loading LLM configuration...", - "Loading multitenancy settings...": "Loading multitenancy settings...", - "Loading n8n configuration...": "Loading n8n configuration...", - "Loading permission matrix...": "Loading permission matrix...", - "Loading preferences...": "Loading preferences...", - "Loading RBAC settings...": "Loading RBAC settings...", - "Loading register data...": "Loading register data...", - "Loading retention settings...": "Loading retention settings...", - "Loading search configuration...": "Loading search configuration...", - "Loading text extraction settings...": "Loading text extraction settings...", - "Loading tokens...": "Loading tokens...", - "Loading user groups...": "Loading user groups...", - "Location": "Location", - "Location Field": "Location Field", - "Locations": "Locations", - "log{plural}": "log{plural}", - "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.", - "Magic table sync completed for {schema}": "Magic table sync completed for {schema}", - "Mail Integration": "Mail Integration", - "Make this installation discoverable within the federation network.": "Make this installation discoverable within the federation network.", - "manage": "manage", - "Manage SOLR collections (data stores) and assign them for objects and files.": "Manage SOLR collections (data stores) and assign them for objects and files.", - "Manage SOLR configsets (configuration templates) for your collections.": "Manage SOLR configsets (configuration templates) for your collections.", - "Manage your account settings, security, and personal data.": "Manage your account settings, security, and personal data.", - "Manage your chat conversations": "Manage your chat conversations", - "Manage your organisations and switch between them": "Manage your organisations and switch between them", - "Mappings": "Mappings", - "Max Tokens": "Max Tokens", - "Maximum File Size (MB)": "Maximum File Size (MB)", - "Maximum length": "Maximum length", - "Maximum number of items": "Maximum number of items", - "Maximum value": "Maximum value", - "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)", - "Member removed successfully": "Member removed successfully", - "Members": "Members", - "Members ({count})": "Members ({count})", - "Metadata": "Metadata", - "Method*": "Method*", - "Method: {method}": "Method: {method}", - "Minimum length": "Minimum length", - "Minimum number of items": "Minimum number of items", - "Minimum value": "Minimum value", - "Multiple of": "Multiple of", - "Multitenancy": "Multitenancy", - "My Account": "My Account", - "myapp": "myapp", - "n8n connection test failed: {message}": "n8n connection test failed: {message}", - "n8n initialization failed: {message}": "n8n initialization failed: {message}", - "Name *": "Name *", - "Name*": "Name*", - "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Names cache warmed up successfully: {count} names loaded in {time}. {cache}", - "Naming pattern for copies:": "Naming pattern for copies:", - "New password": "New password", - "New User Group": "New User Group", - "Nextcloud Groups": "Nextcloud Groups", - "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.", - "No activity found.": "No activity found.", - "No API tokens.": "No API tokens.", - "No approval steps for this object.": "No approval steps for this object.", - "No configuration selected": "No configuration selected", - "No endpoints defined": "No endpoints defined", - "No entities detected for this email.": "No entities detected for this email.", - "No files selected": "No files selected", - "No groups found. Try a different search.": "No groups found. Try a different search.", - "No label": "No label", - "No labels": "No labels", - "No linked objects": "No linked objects", - "No objects are currently selected for deletion.": "No objects are currently selected for deletion.", - "No objects are currently selected for permanent deletion.": "No objects are currently selected for permanent deletion.", - "No objects are currently selected for restoration.": "No objects are currently selected for restoration.", - "No objects linked to this email": "No objects linked to this email", - "No objects selected": "No objects selected", - "No objects selected for restoration": "No objects selected for restoration", - "No objects to delete for schema {schema}": "No objects to delete for schema {schema}", - "No organisations found": "No organisations found", - "No registers found. Create a register to configure permissions.": "No registers found. Create a register to configure permissions.", - "No related cases found for this sender": "No related cases found for this sender", - "No schemas configured for mail linking.": "No schemas configured for mail linking.", - "No synchronizations found": "No synchronizations found", - "No tools available. Tools can be registered by installed apps.": "No tools available. Tools can be registered by installed apps.", - "No users in this organisation.": "No users in this organisation.", - "Note:": "Note:", - "Notifications": "Notifications", - "Number of Sources": "Number of Sources", - "Object #{id}": "Object #{id}", - "Object changes in owned objects": "Object changes in owned objects", - "Object Configuration:": "Object Configuration:", - "Object Extraction Mode": "Object Extraction Mode", - "Object successfully modified": "Object successfully modified", - "Object Text Extraction": "Object Text Extraction", - "Objects deletion completed for {schema}": "Objects deletion completed for {schema}", - "Objects will be soft deleted and moved to the": "Objects will be soft deleted and moved to the", - "objectType: object\naction: created": "objectType: object\naction: created", - "object{plural}": "object{plural}", - "Only members of selected groups can access this organisation": "Only members of selected groups can access this organisation", - "Open folder": "Open folder", - "Open in OpenRegister": "Open in OpenRegister", - "Open OpenRegister sidebar": "Open OpenRegister sidebar", - "OpenRegister": "OpenRegister", - "Optional description for this facet": "Optional description for this facet", - "Optional end date property": "Optional end date property", - "Optional event description template": "Optional event description template", - "Optional location property": "Optional location property", - "Optional URL-friendly identifier": "Optional URL-friendly identifier", - "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)", - "Or": "Or", - "Organisation Configuration": "Organisation Configuration", - "Organisation membership changes": "Organisation membership changes", - "Organisation name is required": "Organisation name is required", - "Organisation successfully updated": "Organisation successfully updated", - "Organizations": "Organizations", - "Other": "Other", - "Output Mapping": "Output Mapping", - "Overview": "Overview", - "Page {current} of {total}": "Page {current} of {total}", - "Password changes are not supported by your authentication provider.": "Password changes are not supported by your authentication provider.", - "Password updated successfully": "Password updated successfully", - "pending": "pending", - "Permanently Delete ({count})": "Permanently Delete ({count})", - "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Permanently delete all {count} soft-deleted objects. This cannot be undone!", - "Permission Matrix": "Permission Matrix", - "Personal Data Export": "Personal Data Export", - "Persons": "Persons", - "Phone Numbers": "Phone Numbers", - "Please select a user": "Please select a user", - "Please select an organisation": "Please select an organisation", - "Please select at least one item to import": "Please select at least one item to import", - "Please select registers or schemas first": "Please select registers or schemas first", - "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Please wait while we permanently delete the SOLR collection. This may take a few moments.", - "Preferences saved": "Preferences saved", - "Press enter to send, shift+enter for new line": "Press enter to send, shift+enter for new line", - "Prettify": "Prettify", - "Preview:": "Preview:", - "Private Agent (Default)": "Private Agent (Default)", - "Private agents": "Private agents", - "Process Name (optional)": "Process Name (optional)", - "Processed {indexed} files, {failed} failed": "Processed {indexed} files, {failed} failed", - "Property could not be updated": "Property could not be updated", - "Property name of inversed relation": "Property name of inversed relation", - "Property successfully updated": "Property successfully updated", - "Publish Configuration to GitHub": "Publish Configuration to GitHub", - "Publish OAS": "Publish OAS", - "Publish objects to make them publicly available": "Publish objects to make them publicly available", - "Publish Register OAS to GitHub": "Publish Register OAS to GitHub", - "Query Help": "Query Help", - "Quota": "Quota", - "RBAC": "RBAC", - "read": "read", - "Reason": "Reason", - "Reason (optional)": "Reason (optional)", - "Register #{id}": "Register #{id}", - "Register / Schema": "Register / Schema", - "Register and schema are required": "Register and schema are required", - "Register Overview": "Register Overview", - "Register:": "Register:", - "register{plural}": "register{plural}", - "Reindex completed! Processed {count} objects in {duration}s": "Reindex completed! Processed {count} objects in {duration}s", - "Reject": "Reject", - "Rejected": "Rejected", - "rejected": "rejected", - "Related Cases": "Related Cases", - "Remove avatar": "Remove avatar", - "Remove from list": "Remove from list", - "Remove link": "Remove link", - "Remove link to {name}": "Remove link to {name}", - "Remove link to {name}?": "Remove link to {name}?", - "Remove link to {title}": "Remove link to {title}", - "Remove oneOf entry": "Remove oneOf entry", - "Remove property after write-back": "Remove property after write-back", - "Remove {title}": "Remove {title}", - "Remove {userId} from this organisation?": "Remove {userId} from this organisation?", - "Removing schema {schema}...": "Removing schema {schema}...", - "Request account deactivation": "Request account deactivation", - "Request account deactivation. This will notify administrators for review.": "Request account deactivation. This will notify administrators for review.", - "Request Quota (per day)": "Request Quota (per day)", - "Requested at": "Requested at", - "Required": "Required", - "Required Token Scopes": "Required Token Scopes", - "Requires Dolphin AI for OCR text extraction": "Requires Dolphin AI for OCR text extraction", - "Rerunning search: {searchTerm}": "Rerunning search: {searchTerm}", - "Resource Quotas": "Resource Quotas", - "Retry upload": "Retry upload", - "Review the selected objects below. You can remove any objects you don't want to include.": "Review the selected objects below. You can remove any objects you don't want to include.", - "Revoke": "Revoke", - "Right": "Right", - "Roles and Permissions": "Roles and Permissions", - "Save labels": "Save labels", - "Save tags for {name}": "Save tags for {name}", - "Save the organisation first to manage users.": "Save the organisation first to manage users.", - "Schema configuration updated successfully for {schema}": "Schema configuration updated successfully for {schema}", - "Schema reference ($ref)": "Schema reference ($ref)", - "Schema {schema} removed successfully": "Schema {schema} removed successfully", - "Schema:": "Schema:", - "schema{plural}": "schema{plural}", - "Search": "Search", - "Search / views": "Search / views", - "Search agents...": "Search agents...", - "Search applications...": "Search applications...", - "Search by title or UUID...": "Search by title or UUID...", - "Search Configuration": "Search Configuration", - "Search configurations": "Search configurations", - "Search data sources...": "Search data sources...", - "Search groups...": "Search groups...", - "Search in Database Objects": "Search in Database Objects", - "Search Mode": "Search Mode", - "Search objects": "Search objects", - "Search objects...": "Search objects...", - "Search Query": "Search Query", - "Search registers...": "Search registers...", - "Search Results": "Search Results", - "Search schemas...": "Search schemas...", - "Search {name}...": "Search {name}...", - "Searching...": "Searching...", - "Security": "Security", - "See organisations as a table": "See organisations as a table", - "See organisations as cards": "See organisations as cards", - "Select a date property": "Select a date property", - "Select a register": "Select a register", - "Select a register...": "Select a register...", - "Select a schema": "Select a schema", - "Select a schema...": "Select a schema...", - "Select a user": "Select a user", - "Select Active Organisation": "Select Active Organisation", - "Select agent type": "Select agent type", - "Select an email to see linked objects": "Select an email to see linked objects", - "Select an organisation and user to add them as a member. Search for organisations by name.": "Select an organisation and user to add them as a member. Search for organisations by name.", - "Select configuration type...": "Select configuration type...", - "Select fields to display": "Select fields to display", - "Select files with the correct extension": "Select files with the correct extension", - "Select group": "Select group", - "Select groups (optional)": "Select groups (optional)", - "Select groups to notify...": "Select groups to notify...", - "Select groups with access to this agent": "Select groups with access to this agent", - "Select groups...": "Select groups...", - "Select one or more registers": "Select one or more registers", - "Select one or more schemas": "Select one or more schemas", - "Select or create labels, or select \"No label\" to add files": "Select or create labels, or select \"No label\" to add files", - "Select owner application (optional)...": "Select owner application (optional)...", - "Select role": "Select role", - "Select schemas (empty = all schemas)": "Select schemas (empty = all schemas)", - "Select schemas to warm up": "Select schemas to warm up", - "Select search mode": "Select search mode", - "Select source type...": "Select source type...", - "Select target type": "Select target type", - "Select views to filter data (optional)": "Select views to filter data (optional)", - "Select views to limit which data the agent can access": "Select views to limit which data the agent can access", - "Select {fieldName} values": "Select {fieldName} values", - "Selected Objects ({count})": "Selected Objects ({count})", - "Send as cloudevent": "Send as cloudevent", - "Set as Active": "Set as Active", - "Set as active organisation": "Set as active organisation", - "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Set limits for API usage and token consumption. Use 0 for unlimited resources.", - "Showing {showing} of {total} agents": "Showing {showing} of {total} agents", - "Showing {showing} of {total} applications": "Showing {showing} of {total} applications", - "Showing {showing} of {total} audit trail entries": "Showing {showing} of {total} audit trail entries", - "Showing {showing} of {total} configurations": "Showing {showing} of {total} configurations", - "Showing {showing} of {total} deleted items": "Showing {showing} of {total} deleted items", - "Showing {showing} of {total} entities": "Showing {showing} of {total} entities", - "Showing {showing} of {total} files": "Showing {showing} of {total} files", - "Showing {showing} of {total} log entries": "Showing {showing} of {total} log entries", - "Showing {showing} of {total} search trail entries": "Showing {showing} of {total} search trail entries", - "Showing {showing} of {total} sources": "Showing {showing} of {total} sources", - "Showing {showing} of {total} templates": "Showing {showing} of {total} templates", - "Showing {showing} of {total} webhooks": "Showing {showing} of {total} webhooks", - "Showing {shown} of {total} organisations": "Showing {shown} of {total} organisations", - "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)", - "SOLR collection deleted successfully": "SOLR collection deleted successfully", - "SOLR Index Warmup": "SOLR Index Warmup", - "Something went wrong while adding files": "Something went wrong while adding files", - "Source successfully updated": "Source successfully updated", - "Special Rights": "Special Rights", - "Start Date Field": "Start Date Field", - "Starting validation for {schema}...": "Starting validation for {schema}...", - "Starting {action} for {schema}...": "Starting {action} for {schema}...", - "Statistics and Metrics": "Statistics and Metrics", - "Statistics refreshed": "Statistics refreshed", - "Step": "Step", - "Storage": "Storage", - "Storage Quota (MB)": "Storage Quota (MB)", - "Successfully added user to organisation: {name}": "Successfully added user to organisation: {name}", - "Successfully created {count} SOLR fields": "Successfully created {count} SOLR fields", - "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Successfully deleted {count} embeddings. Please re-vectorize your data.", - "Successfully deleted {count} objects for {schema}": "Successfully deleted {count} objects for {schema}", - "Successfully fixed {count} SOLR field configurations": "Successfully fixed {count} SOLR field configurations", - "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects", - "Successfully permanently deleted {count} objects": "Successfully permanently deleted {count} objects", - "Successfully processed {count} files!": "Successfully processed {count} files!", - "Successfully saved configuration for {count} facets!": "Successfully saved configuration for {count} facets!", - "Supported File Types": "Supported File Types", - "Switch Active Organisation": "Switch Active Organisation", - "Switch Organisation": "Switch Organisation", - "Syncing magic table for {schema}...": "Syncing magic table for {schema}...", - "Syncs": "Syncs", - "System announcements": "System announcements", - "System Prompt": "System Prompt", - "Table": "Table", - "Target ID": "Target ID", - "Target Type*": "Target Type*", - "Text Chunk #{id}": "Text Chunk #{id}", - "Text Extraction": "Text Extraction", - "Text Extraction Engine": "Text Extraction Engine", - "They will be retained according to their schema's configured retention period and then permanently removed.": "They will be retained according to their schema's configured retention period and then permanently removed.", - "This action cannot be undone. Make sure no collections are using this configset.": "This action cannot be undone. Make sure no collections are using this configset.", - "This action will submit a deactivation request to your administrators.": "This action will submit a deactivation request to your administrators.", - "This is not valid JSON": "This is not valid JSON", - "This key already exists on this schema": "This key already exists on this schema", - "This schema must use magic table configuration to sync": "This schema must use magic table configuration to sync", - "This token will only be shown once. Copy it now.": "This token will only be shown once. Copy it now.", - "This will copy the _default configset with the new name": "This will copy the _default configset with the new name", - "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?", - "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.", - "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.", - "Title *": "Title *", - "Title Template": "Title Template", - "Title*": "Title*", - "To (e.g. -1 year or 2025-12-31)": "To (e.g. -1 year or 2025-12-31)", - "To add files larger than or equal to 512MB, go to the": "To add files larger than or equal to 512MB, go to the", - "To adjust chunk size or strategy, go to file configuration → processing limits.": "To adjust chunk size or strategy, go to file configuration → processing limits.", - "To change the embedding provider or model, go to LLM configuration.": "To change the embedding provider or model, go to LLM configuration.", - "Token copied to clipboard": "Token copied to clipboard", - "Token Created": "Token Created", - "Token name": "Token name", - "Token Quota (per request)": "Token Quota (per request)", - "Token revoked": "Token revoked", - "Tools respect the agent's own role and organization boundaries.": "Tools respect the agent's own role and organization boundaries.", - "Try searching by UUID or with different keywords": "Try searching by UUID or with different keywords", - "Type \"DELETE COLLECTION\" to confirm:": "Type \"DELETE COLLECTION\" to confirm:", - "Type to search for objects...": "Type to search for objects...", - "Type to search for organisations": "Type to search for organisations", - "Type to search...": "Type to search...", - "Type your username to confirm": "Type your username to confirm", - "Unknown Object": "Unknown Object", - "Unnamed Object": "Unnamed Object", - "update": "update", - "Update available: {local} → {remote}": "Update available: {local} → {remote}", - "Upload new avatar": "Upload new avatar", - "Url": "Url", - "Use AI agents for processing and analysis": "Use AI agents for processing and analysis", - "Use default ranges (Last 7/30/90 days, Last year, Older)": "Use default ranges (Last 7/30/90 days, Last year, Older)", - "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.", - "Use Large Language Model features": "Use Large Language Model features", - "Use SolrCloud with zookeeper for distributed search": "Use SolrCloud with zookeeper for distributed search", - "Use VNG APIs instead of MongoDB.": "Use VNG APIs instead of MongoDB.", - "Used by": "Used by", - "User Quota": "User Quota", - "Uses": "Uses", - "UUID copied to clipboard": "UUID copied to clipboard", - "Validation completed for {schema}": "Validation completed for {schema}", - "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed", - "Value list (split on ,)": "Value list (split on ,)", - "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vectorization completed: {vectorized} objects vectorized, {failed} failed", - "Vectorization failed: {error}": "Vectorization failed: {error}", - "Version Information": "Version Information", - "View": "View", - "view": "view", - "View and manage authorization across registers and schemas": "View and manage authorization across registers and schemas", - "View API Documentation": "View API Documentation", - "View APIs": "View APIs", - "View object": "View object", - "View {count} more": "View {count} more", - "Visible to end users": "Visible to end users", - "waiting": "waiting", - "Warmup failed": "Warmup failed", - "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.", - "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "When disabled, this facet will only show values from this schema and will include a schema filter when selected.", - "When enabled, saving this object will also update the target objects to include a reference back to this object.": "When enabled, saving this object will also update the target objects to include a reference back to this object.", - "When enabled, this property will be removed from the source object after updating the target objects.": "When enabled, this property will be removed from the source object after updating the target objects.", - "Workflow Management": "Workflow Management", - "Wrap webhook payload in cloudevents format for better interoperability": "Wrap webhook payload in cloudevents format for better interoperability", - "You are not yet a member of any organisations.": "You are not yet a member of any organisations.", - "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.", - "You do not have permission to view the permission matrix. Admin access is required.": "You do not have permission to view the permission matrix. Admin access is required.", - "You have selected {count} properties to add to the schema.": "You have selected {count} properties to add to the schema.", - "Your feedback has been recorded. optionally, you can provide additional details here...": "Your feedback has been recorded. optionally, you can provide additional details here...", - "{active} active, {deleted} deleted": "{active} active, {deleted} deleted", - "{count} audit trails cleared successfully": "{count} audit trails cleared successfully", - "{count} deletions": "{count} deletions", - "{count} documents found": "{count} documents found", - "{count} entries": "{count} entries", - "{count} issues": "{count} issues", - "{count} members": "{count} members", - "{count} searches": "{count} searches", - "{count} selected": "{count} selected", - "{minutes} minutes ago": "{minutes} minutes ago", - "{percentage}% of objects": "{percentage}% of objects", - "{property} - {other}": "{property} - {other}", - "ℹ️ current configuration": "ℹ️ current configuration", - "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!" + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Faceted search allows users to drill down into results by categories...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!", + "💰 View Selection (Cost Optimization)": "💰 View Selection (Cost Optimization)", + "📄 Object Serialization": "📄 Object Serialization", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR provides rich analytics and statistics about search performance...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets contain the schema and configuration files for your search index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replication ensures your search index is available even if nodes fail...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Vectorization Settings", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Auto-scaling can dynamically add or remove nodes based on load...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Security features include authentication, authorization, and SSL encryption...", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." }, "plurals": { "{count} email": [ diff --git a/l10n/es.js b/l10n/es.js new file mode 100644 index 0000000000..79e8c2f6df --- /dev/null +++ b/l10n/es.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(sin asunto)", + "(no title)" : "(sin título)", + "(unnamed)" : "(sin nombre)", + "(untitled card)" : "(tarjeta sin título)", + "Activities" : "Actividades", + "Add Endpoint" : "Añadir punto de acceso", + "Add Source" : "Añadir fuente", + "Add card" : "Añadir tarjeta", + "Add contact" : "Añadir contacto", + "All clear" : "Todo correcto", + "Articles" : "Artículos", + "Code" : "Código", + "Compliance" : "Cumplimiento", + "Contacts" : "Contactos", + "Create Agent" : "Crear agente", + "Create Application" : "Crear aplicación", + "Create event" : "Crear evento", + "Erasure complete" : "Borrado completado", + "Erasure preview" : "Vista previa del borrado", + "Expand details" : "Expandir detalles", + "Save changes" : "Guardar cambios", + "📄 Object Serialization" : "📄 Serialización de objetos", + "🔢 Vectorization Settings" : "🔢 Ajustes de vectorización", + "💰 View Selection (Cost Optimization)" : "💰 Selección de vistas (optimización de costes)", + "AI Assistant" : "Asistente de IA", + "AI Chat" : "Chat de IA", + "API Key" : "Clave de API", + "API tokens saved successfully" : "Tokens de API guardados correctamente", + "About ConfigSets" : "Acerca de los ConfigSets", + "Action" : "Acción", + "Action Distribution" : "Distribución de acciones", + "Action:" : "Acción:", + "Actions" : "Acciones", + "Active" : "Activo", + "Active Collections" : "Colecciones activas", + "Active Filters" : "Filtros activos", + "Active Organisations" : "Organizaciones activas", + "Active filters:" : "Filtros activos:", + "Active organisation set successfully" : "Organización activa establecida correctamente", + "Activity Period" : "Periodo de actividad", + "Add Groups" : "Añadir grupos", + "Add Schema" : "Añadir esquema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Añada títulos de esquemas, descripciones e información de registro para proporcionar un contexto más rico para la búsqueda", + "Add to favorites" : "Añadir a favoritos", + "Added" : "Añadido", + "Added to favorites" : "Añadido a favoritos", + "Additional Information" : "Información adicional", + "Additional feedback saved. Thank you!" : "Comentarios adicionales guardados. ¡Gracias!", + "Admin" : "Administrador", + "Advanced" : "Avanzado", + "Advanced Filters" : "Filtros avanzados", + "Advanced Options" : "Opciones avanzadas", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Los filtros avanzados no están disponibles al usar la fuente de base de datos. Cambie a Automático o Índice SOLR para ver las opciones de filtrado.", + "After" : "Después", + "Agent deleted successfully" : "Agente eliminado correctamente", + "Agents" : "Agentes", + "All" : "Todos", + "All Categories" : "Todas las categorías", + "All Collections" : "Todas las colecciones", + "All Confidence Levels" : "Todos los niveles de confianza", + "All ConfigSets" : "Todos los ConfigSets", + "All Files" : "Todos los archivos", + "All Levels" : "Todos los niveles", + "All Types" : "Todos los tipos", + "All Webhooks" : "Todos los webhooks", + "All actions" : "Todas las acciones", + "All audit trails cleared successfully" : "Todos los registros de auditoría se han borrado correctamente", + "All registers" : "Todos los registros", + "All schemas" : "Todos los esquemas", + "All searches" : "Todas las búsquedas", + "All users" : "Todos los usuarios", + "An error occurred while clearing audit trails" : "Se produjo un error al borrar los registros de auditoría", + "An error occurred while deleting the audit trail" : "Se produjo un error al eliminar el registro de auditoría", + "An error occurred while deleting the view" : "Se produjo un error al eliminar la vista", + "An error occurred while permanently deleting the objects" : "Se produjo un error al eliminar permanentemente los objetos", + "Analysis completed:" : "Análisis completado:", + "Analysis steps:" : "Pasos del análisis:", + "Analytics" : "Analítica", + "Analyze Objects" : "Analizar objetos", + "Analyze Properties" : "Analizar propiedades", + "Analyze existing properties for improvement opportunities" : "Analizar las propiedades existentes en busca de oportunidades de mejora", + "Analyzing..." : "Analizando...", + "Any favorites and sharing settings for this view" : "Cualquier ajuste de favoritos y de compartición de esta vista", + "Any user" : "Cualquier usuario", + "App store cache invalidated successfully" : "Caché de la tienda de aplicaciones invalidada correctamente", + "Application deleted successfully" : "Aplicación eliminada correctamente", + "Applications" : "Aplicaciones", + "Apply Changes" : "Aplicar cambios", + "Archive" : "Archivar", + "Archive conversation" : "Archivar conversación", + "Archived conversations are hidden from your active list" : "Las conversaciones archivadas se ocultan de su lista activa", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "¿Está seguro de que desea limpiar los registros de búsqueda antiguos? Esto eliminará las entradas con más de 30 días de antigüedad.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "¿Está seguro de que desea eliminar los registros de auditoría seleccionados? Esta acción no se puede deshacer.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "¿Está seguro de que desea eliminar los registros de búsqueda seleccionados? Esta acción no se puede deshacer.", + "Are you sure you want to delete this ConfigSet?" : "¿Está seguro de que desea eliminar este ConfigSet?", + "Are you sure you want to permanently delete" : "¿Está seguro de que desea eliminar permanentemente", + "Ask a question..." : "Haga una pregunta...", + "Ask questions about your data using natural language" : "Haga preguntas sobre sus datos usando lenguaje natural", + "Attempt" : "Intento", + "Audit Trail Changes" : "Cambios del registro de auditoría", + "Audit Trail Details" : "Detalles del registro de auditoría", + "Audit Trail Management" : "Gestión del registro de auditoría", + "Audit Trail Statistics" : "Estadísticas del registro de auditoría", + "Audit Trails" : "Registros de auditoría", + "Audit trail data copied to clipboard" : "Datos del registro de auditoría copiados al portapapeles", + "Audit trail deleted successfully" : "Registro de auditoría eliminado correctamente", + "Audit trail successfully deleted" : "Registro de auditoría eliminado correctamente", + "Authentication" : "Autenticación", + "Auto-commit disabled" : "Confirmación automática deshabilitada", + "Auto-commit enabled" : "Confirmación automática habilitada", + "Auto-create Default Organisation" : "Crear organización predeterminada automáticamente", + "Auto-retry failed vectorizations" : "Reintentar automáticamente las vectorizaciones fallidas", + "Automatically commit changes to SOLR index" : "Confirmar automáticamente los cambios en el índice SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Crear automáticamente una organización predeterminada si no existe ninguna cuando se inicializa la aplicación", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Generar automáticamente embeddings vectoriales a partir de fragmentos de texto cuando se suban y procesen los archivos", + "Automatically generate vector embeddings when objects are created or updated" : "Generar automáticamente embeddings vectoriales cuando se creen o actualicen objetos", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Reintentar automáticamente los intentos de vectorización fallidos (máximo 3 reintentos)", + "Available Filters" : "Filtros disponibles", + "Available Workflows" : "Flujos de trabajo disponibles", + "Avg Execution Time" : "Tiempo medio de ejecución", + "Avg Members/Org" : "Promedio de miembros/org.", + "Avg Object Views/Session" : "Promedio de vistas de objetos/sesión", + "Avg Response Time" : "Tiempo medio de respuesta", + "Avg Results/Search" : "Promedio de resultados/búsqueda", + "Avg Searches/Session" : "Promedio de búsquedas/sesión", + "Back" : "Atrás", + "Back to Entities" : "Volver a entidades", + "Back to Registers" : "Volver a registros", + "Back to Webhooks" : "Volver a webhooks", + "Back to applications" : "Volver a aplicaciones", + "Back to entities" : "Volver a entidades", + "Backend updated successfully. Please reload the application." : "Backend actualizado correctamente. Por favor, recargue la aplicación.", + "Base URL (Optional)" : "URL base (opcional)", + "Basic Information" : "Información básica", + "Batch Size" : "Tamaño del lote", + "Batch extraction completed" : "Extracción por lotes completada", + "Before" : "Antes", + "Before object vectorization can work:" : "Antes de que la vectorización de objetos pueda funcionar:", + "Behavior Issue" : "Problema de comportamiento", + "Blob storage has been retired. All objects now use magic tables." : "El almacenamiento de blobs ha sido retirado. Todos los objetos usan ahora tablas mágicas.", + "Branch" : "Rama", + "Bulk delete operation completed successfully" : "Operación de eliminación masiva completada correctamente", + "Bulk save operation completed successfully" : "Operación de guardado masivo completada correctamente", + "Business Data" : "Datos empresariales", + "Cache cleared successfully" : "Caché borrada correctamente", + "Calculate Sizes" : "Calcular tamaños", + "Cancel" : "Cancelar", + "Cannot delete: objects are still attached" : "No se puede eliminar: aún hay objetos asociados", + "Category" : "Categoría", + "Change Type" : "Tipo de cambio", + "Changes" : "Cambios", + "Changes copied to clipboard" : "Cambios copiados al portapapeles", + "Chat Model" : "Modelo de chat", + "Chat Provider" : "Proveedor de chat", + "Chat Provider (RAG)" : "Proveedor de chat (RAG)", + "Chat Settings" : "Ajustes de chat", + "Chat provider connection successful!" : "¡Conexión con el proveedor de chat correcta!", + "Chat settings" : "Ajustes de chat", + "Choose a register" : "Elija un registro", + "Choose a schema" : "Elija un esquema", + "Choose how vector similarity calculations are performed for semantic search" : "Elija cómo se realizan los cálculos de similitud vectorial para la búsqueda semántica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Elija qué tipos de archivos incluir en el proceso de vectorización. Solo se procesarán los archivos con texto extraído y fragmentos.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Elija qué vistas incluir en el proceso de vectorización. Déjelo vacío para procesar todas las vistas según su configuración.", + "Chunk" : "Fragmento", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "La eliminación de fragmentos aún no está implementada. Use los puntos de acceso basados en fragmentos.", + "Chunks" : "Fragmentos", + "Chunks to Vectorize:" : "Fragmentos a vectorizar:", + "Cleanup Old Trails" : "Limpiar registros antiguos", + "Cleanup completed" : "Limpieza completada", + "Clear All Embeddings" : "Borrar todos los embeddings", + "Clear Entries" : "Borrar entradas", + "Clear Filtered Audit Trails" : "Borrar registros de auditoría filtrados", + "Clear Filters" : "Borrar filtros", + "Clear Index" : "Borrar índice", + "Clear Selection" : "Borrar selección", + "Clear all" : "Borrar todo", + "Clear all filters" : "Borrar todos los filtros", + "Clear date range" : "Borrar rango de fechas", + "Clearing..." : "Borrando...", + "Close" : "Cerrar", + "Collection Management" : "Gestión de colecciones", + "Collection Name" : "Nombre de la colección", + "Collection assignments updated successfully" : "Asignaciones de colección actualizadas correctamente", + "Collection cleared successfully" : "Colección borrada correctamente", + "Collection copied successfully" : "Colección copiada correctamente", + "Collection created successfully" : "Colección creada correctamente", + "Collection deleted successfully" : "Colección eliminada correctamente", + "Collection used to store and index file metadata and content" : "Colección utilizada para almacenar e indexar los metadatos y el contenido de los archivos", + "Collection used to store and index object data" : "Colección utilizada para almacenar e indexar los datos de objetos", + "Collections:" : "Colecciones:", + "Commit Message" : "Mensaje de confirmación", + "Commit Within (ms)" : "Confirmar en (ms)", + "Commit message" : "Mensaje de confirmación", + "Compare current schema with real object data" : "Comparar el esquema actual con datos de objetos reales", + "Completed" : "Completado", + "Complex" : "Complejo", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Consultas complejas: búsquedas avanzadas con múltiples filtros, operadores y combinaciones complejas de parámetros", + "Confidence Level" : "Nivel de confianza", + "Confidence Score" : "Puntuación de confianza", + "Config must be provided as an object" : "La configuración debe proporcionarse como un objeto", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Gestión de ConfigSets", + "ConfigSet Name" : "Nombre del ConfigSet", + "ConfigSet created successfully" : "ConfigSet creado correctamente", + "ConfigSet deleted successfully" : "ConfigSet eliminado correctamente", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Los ConfigSets definen el esquema y la configuración de sus colecciones SOLR. Contienen definiciones de campos, analizadores y otros ajustes de búsqueda.", + "Configuration" : "Configuración", + "Configuration Data" : "Datos de configuración", + "Configuration saved" : "Configuración guardada", + "Configurations" : "Configuraciones", + "Configure Facets" : "Configurar facetas", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Configure los proveedores de modelos de lenguaje de gran tamaño (LLM) para las funciones impulsadas por IA, incluida la búsqueda semántica, los embeddings y el chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Configure los ajustes básicos de conexión de su servidor SOLR, incluidas las opciones de autenticación y red. Use los diálogos independientes de gestión de ConfigSets y colecciones para gestionar los cores y las colecciones.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Configure cómo se convierten los objetos de la base de datos en embeddings vectoriales para la búsqueda semántica. Los objetos se vectorizan directamente sin necesidad de extracción de texto.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Configure cómo se convierten los objetos en texto antes de la vectorización. Estos ajustes afectan a la calidad y al contexto de la búsqueda.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Configure los parámetros para la vectorización de archivos. Este proceso generará embeddings vectoriales para todos los fragmentos de archivos extraídos.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Configure los parámetros para la vectorización de objetos. Este proceso generará embeddings vectoriales para todos los objetos que coincidan con los filtros de su vista.", + "Configure which types of data to search and how many sources to retrieve" : "Configure qué tipos de datos buscar y cuántas fuentes recuperar", + "Connection Failed" : "Conexión fallida", + "Connection Settings" : "Ajustes de conexión", + "Connection Successful!" : "¡Conexión correcta!", + "Connection failed" : "Conexión fallida", + "Connection protocol" : "Protocolo de conexión", + "Connection successful" : "Conexión correcta", + "Connection timeout in seconds" : "Tiempo de espera de conexión en segundos", + "Constant delay between retries (5 minutes)" : "Retardo constante entre reintentos (5 minutos)", + "Constraint Issue" : "Problema de restricción", + "Content Filters" : "Filtros de contenido", + "Continue to Properties" : "Continuar a propiedades", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Controle qué vistas de objetos deben vectorizarse para reducir los costes de la API. Vectorice solo las vistas que se beneficien de la búsqueda semántica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Controle qué vistas y herramientas puede usar la IA en esta conversación. De forma predeterminada, todas las capacidades del agente están habilitadas.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Controla la aleatoriedad (0 = determinista, 2 = muy creativo)", + "Conversation ID is required" : "El ID de conversación es obligatorio", + "Conversation archived" : "Conversación archivada", + "Conversation archived successfully" : "Conversación archivada correctamente", + "Conversation cleared successfully" : "Conversación borrada correctamente", + "Conversation deleted" : "Conversación eliminada", + "Conversation permanently deleted" : "Conversación eliminada permanentemente", + "Conversation renamed" : "Conversación renombrada", + "Conversation restored" : "Conversación restaurada", + "Conversation title" : "Título de la conversación", + "Copied!" : "¡Copiado!", + "Copy" : "Copiar", + "Copy Changes" : "Copiar cambios", + "Copy Collection" : "Copiar colección", + "Copy Data" : "Copiar datos", + "Copy Full Data" : "Copiar todos los datos", + "Copying..." : "Copiando...", + "Count" : "Recuento", + "Counting objects..." : "Contando objetos...", + "Costs" : "Costes", + "Create" : "Crear", + "Create Collection" : "Crear colección", + "Create ConfigSet" : "Crear ConfigSet", + "Create New Collection" : "Crear nueva colección", + "Create New ConfigSet" : "Crear nuevo ConfigSet", + "Create Operations" : "Operaciones de creación", + "Create Webhook" : "Crear webhook", + "Create a copy of collection:" : "Crear una copia de la colección:", + "Create a new ConfigSet based on the _default template" : "Crear un nuevo ConfigSet basado en la plantilla _default", + "Create a new SOLR collection from an existing ConfigSet" : "Crear una nueva colección SOLR a partir de un ConfigSet existente", + "Create your first AI agent to get started." : "Cree su primer agente de IA para empezar.", + "Created" : "Creado", + "Created:" : "Creado:", + "Creating..." : "Creando...", + "Current Filters" : "Filtros actuales", + "Current Type:" : "Tipo actual:", + "Custom API endpoint if using a different region" : "Punto de acceso de API personalizado si usa una región diferente", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Cabeceras HTTP personalizadas (una por línea, formato: Nombre-Cabecera: valor)", + "Daily" : "Diario", + "Dashboard" : "Panel de control", + "Data Source" : "Fuente de datos", + "Data sources" : "Fuentes de datos", + "Data type does not match observed values" : "El tipo de datos no coincide con los valores observados", + "Database URL" : "URL de la base de datos", + "Database information refreshed" : "Información de la base de datos actualizada", + "Date Range" : "Rango de fechas", + "Default" : "Predeterminado", + "Default Organisation" : "Organización predeterminada", + "Default: 5" : "Predeterminado: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Los retardos se duplican con cada intento (2, 4, 8 minutos...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Los retardos aumentan de forma lineal (5, 10, 15 minutos...)", + "Delete" : "Eliminar", + "Delete Application" : "Eliminar aplicación", + "Delete Audit Trail" : "Eliminar registro de auditoría", + "Delete Collection" : "Eliminar colección", + "Delete ConfigSet" : "Eliminar ConfigSet", + "Delete Objects" : "Eliminar objetos", + "Delete Operations" : "Operaciones de eliminación", + "Delete View" : "Eliminar vista", + "Delete all objects in this schema" : "Eliminar todos los objetos de este esquema", + "Delete permanently" : "Eliminar permanentemente", + "Delete this conversation?" : "¿Eliminar esta conversación?", + "Delete view" : "Eliminar vista", + "Deleted" : "Eliminado", + "Deleted By" : "Eliminado por", + "Deleted Date" : "Fecha de eliminación", + "Deleted Items Management" : "Gestión de elementos eliminados", + "Deleted This Week" : "Eliminados esta semana", + "Deleted Today" : "Eliminados hoy", + "Deleted:" : "Eliminado:", + "Deleting..." : "Eliminando...", + "Deletion Date Range" : "Rango de fechas de eliminación", + "Deletion Statistics" : "Estadísticas de eliminación", + "Deprecated" : "Obsoleto", + "Description" : "Descripción", + "Description:" : "Descripción:", + "Detect data types and patterns" : "Detectar tipos de datos y patrones", + "Detected At" : "Detectado el", + "Detected Format:" : "Formato detectado:", + "Detected Issues:" : "Problemas detectados:", + "Disable" : "Deshabilitar", + "Disabled" : "Deshabilitado", + "Discover Files" : "Descubrir archivos", + "Discovered Properties" : "Propiedades descubiertas", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "¿Desea eliminar permanentemente todas las entradas de registro de auditoría filtradas? Esta acción no se puede deshacer.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "¿Desea eliminar permanentemente esta entrada de registro de auditoría? Esta acción no se puede deshacer.", + "Documents" : "Documentos", + "Download API Spec" : "Descargar especificación de la API", + "Edit" : "Editar", + "Edit Register" : "Editar registro", + "Edit Schema" : "Editar esquema", + "Edit View" : "Editar vista", + "Edit Webhook" : "Editar webhook", + "Edit view" : "Editar vista", + "Edit view details" : "Editar detalles de la vista", + "Effectiveness" : "Eficacia", + "Email" : "Correo electrónico", + "Emails" : "Correos electrónicos", + "Embedding Model" : "Modelo de embedding", + "Embedding Provider" : "Proveedor de embedding", + "Embedding generated successfully" : "Embedding generado correctamente", + "Embedding provider connection successful!" : "¡Conexión con el proveedor de embedding correcta!", + "Enable" : "Habilitar", + "Enable auto-creation" : "Habilitar creación automática", + "Enable automatic file vectorization" : "Habilitar la vectorización automática de archivos", + "Enable automatic object vectorization" : "Habilitar la vectorización automática de objetos", + "Enable automatic synchronization" : "Habilitar la sincronización automática", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Habilitar el registro detallado de las operaciones de SOLR (recomendado para depuración)", + "Enable faceting" : "Habilitar facetas", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Habilite o deshabilite las funciones de LLM. Configure los proveedores y modelos usando el botón de Configuración de LLM de arriba.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Habilite o deshabilite la integración de búsqueda SOLR. Configure los ajustes de conexión usando el botón de Ajustes de conexión de arriba.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Habilite o deshabilite la integración del flujo de trabajo de n8n. Configure los ajustes de conexión a continuación.", + "Enable or disable this webhook" : "Habilitar o deshabilitar este webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Habilitar la vectorización para todas las vistas existentes y futuras (puede aumentar los costes)", + "Enabled" : "Habilitado", + "Endpoints" : "Puntos de acceso", + "Enter ConfigSet name" : "Introduzca el nombre del ConfigSet", + "Enter collection name" : "Introduzca el nombre de la colección", + "Enter description (optional)..." : "Introduzca una descripción (opcional)...", + "Enter new collection name" : "Introduzca el nuevo nombre de la colección", + "Enter object ID" : "Introduzca el ID del objeto", + "Enter search term" : "Introduzca el término de búsqueda", + "Enter view name..." : "Introduzca el nombre de la vista...", + "Enter webhook name" : "Introduzca el nombre del webhook", + "Entities" : "Entidades", + "Entity Information" : "Información de la entidad", + "Entity deleted successfully" : "Entidad eliminada correctamente", + "Entity not found" : "Entidad no encontrada", + "Entries to be deleted:" : "Entradas que se eliminarán:", + "Enum Issue" : "Problema de enumeración", + "Enum constraint is missing" : "Falta la restricción de enumeración", + "Error" : "Error", + "Error Details" : "Detalles del error", + "Error Information" : "Información del error", + "Error Message" : "Mensaje de error", + "Error loading application" : "Error al cargar la aplicación", + "Error loading audit trails" : "Error al cargar los registros de auditoría", + "Error loading entity" : "Error al cargar la entidad", + "Error loading search trails" : "Error al cargar los registros de búsqueda", + "Estimated Batches:" : "Lotes estimados:", + "Estimated Cost:" : "Coste estimado:", + "Estimated Duration:" : "Duración estimada:", + "Event" : "Evento", + "Event Property for Payload" : "Propiedad del evento para la carga útil", + "Events" : "Eventos", + "Example Value" : "Valor de ejemplo", + "Exclusive Maximum" : "Máximo exclusivo", + "Exclusive Minimum" : "Mínimo exclusivo", + "Execution Mode" : "Modo de ejecución", + "Execution Time" : "Tiempo de ejecución", + "Execution Time Range" : "Rango de tiempo de ejecución", + "Existing Improvements" : "Mejoras existentes", + "Exponential" : "Exponencial", + "Export" : "Exportar", + "Export completed successfully" : "Exportación completada correctamente", + "Export, view, or delete audit trails" : "Exportar, ver o eliminar registros de auditoría", + "Extend Schema" : "Extender esquema", + "Extended" : "Extendido", + "Extensions" : "Extensiones", + "Extract Pending Files" : "Extraer archivos pendientes", + "Extract properties from each object" : "Extraer propiedades de cada objeto", + "Extracted At" : "Extraído el", + "Extraction Status" : "Estado de la extracción", + "Facet configuration updated successfully" : "Configuración de facetas actualizada correctamente", + "Facets discovered and configured successfully" : "Facetas descubiertas y configuradas correctamente", + "Facets discovered successfully" : "Facetas descubiertas correctamente", + "Failed" : "Fallido", + "Failed to analyze schema properties" : "No se pudieron analizar las propiedades del esquema", + "Failed to archive conversation" : "No se pudo archivar la conversación", + "Failed to calculate sizes" : "No se pudieron calcular los tamaños", + "Failed to clear collection" : "No se pudo borrar la colección", + "Failed to copy changes" : "No se pudieron copiar los cambios", + "Failed to copy data" : "No se pudieron copiar los datos", + "Failed to copy data to clipboard" : "No se pudieron copiar los datos al portapapeles", + "Failed to create conversation" : "No se pudo crear la conversación", + "Failed to create or find project" : "No se pudo crear o encontrar el proyecto", + "Failed to delete collection" : "No se pudo eliminar la colección", + "Failed to delete conversation" : "No se pudo eliminar la conversación", + "Failed to delete webhook" : "No se pudo eliminar el webhook", + "Failed to download API specification" : "No se pudo descargar la especificación de la API", + "Failed to get SOLR field configuration" : "No se pudo obtener la configuración de campos de SOLR", + "Failed to get entity categories" : "No se pudieron obtener las categorías de entidades", + "Failed to get entity statistics" : "No se pudieron obtener las estadísticas de entidades", + "Failed to get entity types" : "No se pudieron obtener los tipos de entidades", + "Failed to load LLM configuration" : "No se pudo cargar la configuración de LLM", + "Failed to load Nextcloud groups" : "No se pudieron cargar los grupos de Nextcloud", + "Failed to load conversation" : "No se pudo cargar la conversación", + "Failed to load entities" : "No se pudieron cargar las entidades", + "Failed to load entity" : "No se pudo cargar la entidad", + "Failed to load files" : "No se pudieron cargar los archivos", + "Failed to load organisations" : "No se pudieron cargar las organizaciones", + "Failed to load templates" : "No se pudieron cargar las plantillas", + "Failed to load webhooks" : "No se pudieron cargar los webhooks", + "Failed to load workflows" : "No se pudieron cargar los flujos de trabajo", + "Failed to refresh database information" : "No se pudo actualizar la información de la base de datos", + "Failed to reindex collection" : "No se pudo reindexar la colección", + "Failed to rename conversation" : "No se pudo renombrar la conversación", + "Failed to restore conversation" : "No se pudo restaurar la conversación", + "Failed to retry extraction" : "No se pudo reintentar la extracción", + "Failed to retry webhook" : "No se pudo reintentar el webhook", + "Failed to save GitHub token" : "No se pudo guardar el token de GitHub", + "Failed to save GitLab URL" : "No se pudo guardar la URL de GitLab", + "Failed to save GitLab token" : "No se pudo guardar el token de GitLab", + "Failed to save additional feedback" : "No se pudieron guardar los comentarios adicionales", + "Failed to save n8n configuration" : "No se pudo guardar la configuración de n8n", + "Failed to save roles" : "No se pudieron guardar los roles", + "Failed to save settings" : "No se pudieron guardar los ajustes", + "Failed to save webhook" : "No se pudo guardar el webhook", + "Failed to send feedback" : "No se pudieron enviar los comentarios", + "Failed to test webhook" : "No se pudo probar el webhook", + "Failed to update favorite status" : "No se pudo actualizar el estado de favorito", + "Failed to update schema properties" : "No se pudieron actualizar las propiedades del esquema", + "Failed to update view" : "No se pudo actualizar la vista", + "Failed to update webhook" : "No se pudo actualizar el webhook", + "Feedback recorded" : "Comentarios registrados", + "Fewer sources (1-3):" : "Menos fuentes (1-3):", + "Field" : "Campo", + "File" : "Archivo", + "File Chunk Prediction" : "Predicción de fragmentos de archivo", + "File Collection" : "Colección de archivos", + "File Management" : "Gestión de archivos", + "File Name" : "Nombre del archivo", + "File Path" : "Ruta del archivo", + "File Type Selection" : "Selección de tipo de archivo", + "File Vectorization" : "Vectorización de archivos", + "File Warmup" : "Precalentamiento de archivos", + "File actions menu" : "Menú de acciones del archivo", + "File anonymized successfully" : "Archivo anonimizado correctamente", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "La vectorización de fragmentos de archivo aún no está implementada. Los fragmentos están listos y almacenados, pero el servicio de vectorización está en desarrollo.", + "File collection not configured" : "Colección de archivos no configurada", + "File discovery completed" : "Descubrimiento de archivos completado", + "File extraction completed" : "Extracción de archivos completada", + "File extraction queued" : "Extracción de archivos en cola", + "File indexed successfully" : "Archivo indexado correctamente", + "File is already anonymized" : "El archivo ya está anonimizado", + "File not found" : "Archivo no encontrado", + "File settings updated successfully" : "Ajustes del archivo actualizados correctamente", + "File sources" : "Fuentes de archivos", + "File vectorization configuration saved successfully" : "Configuración de vectorización de archivos guardada correctamente", + "File vectorization started. Check the statistics section for progress." : "Vectorización de archivos iniciada. Consulte la sección de estadísticas para ver el progreso.", + "File warmup completed" : "Precalentamiento de archivos completado", + "Files" : "Archivos", + "Files with Completed Extraction:" : "Archivos con extracción completada:", + "Files → fileCollection, Objects → objectCollection" : "Archivos → fileCollection, Objetos → objectCollection", + "Filter Audit Trails" : "Filtrar registros de auditoría", + "Filter Deleted Items" : "Filtrar elementos eliminados", + "Filter Objects" : "Filtrar objetos", + "Filter Properties" : "Filtrar propiedades", + "Filter Search Trails" : "Filtrar registros de búsqueda", + "Filter Statistics" : "Filtrar estadísticas", + "Filter and analyze search trail entries" : "Filtrar y analizar las entradas de registro de búsqueda", + "Filter and manage audit trail entries" : "Filtrar y gestionar las entradas de registro de auditoría", + "Filter and manage soft deleted items" : "Filtrar y gestionar los elementos eliminados de forma reversible", + "Filter by object ID" : "Filtrar por ID de objeto", + "Filter by search term" : "Filtrar por término de búsqueda", + "Filter by webhook" : "Filtrar por webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Datos de filtro cargados automáticamente. Use los filtros de abajo para refinar su búsqueda.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrar los disparadores del webhook por propiedades de la carga útil (una por línea, formato: clave: valor)", + "Filtered" : "Filtrado", + "Filters" : "Filtros", + "Fireworks AI Chat Settings" : "Ajustes de chat de Fireworks AI", + "Fireworks AI Embedding Configuration" : "Configuración de embedding de Fireworks AI", + "First" : "Primero", + "Fixed" : "Fijo", + "For chat and retrieval-augmented generation" : "Para chat y generación aumentada por recuperación", + "For vector embeddings and semantic search" : "Para embeddings vectoriales y búsqueda semántica", + "Format" : "Formato", + "Format Issue" : "Problema de formato", + "Format constraint is missing" : "Falta la restricción de formato", + "From Date" : "Desde fecha", + "From date" : "Desde fecha", + "Full data copied to clipboard" : "Todos los datos copiados al portapapeles", + "General Issue" : "Problema general", + "Generate recommendations and confidence scores" : "Generar recomendaciones y puntuaciones de confianza", + "Generate vectors immediately when new objects are created" : "Generar vectores inmediatamente cuando se creen nuevos objetos", + "GitHub token is valid" : "El token de GitHub es válido", + "GitHub token removed successfully" : "Token de GitHub eliminado correctamente", + "GitHub token saved successfully" : "Token de GitHub guardado correctamente", + "GitLab URL saved successfully" : "URL de GitLab guardada correctamente", + "GitLab token is valid" : "El token de GitLab es válido", + "GitLab token saved successfully" : "Token de GitLab guardado correctamente", + "HTTP Method" : "Método HTTP", + "HTTP method used to send webhook requests" : "Método HTTP utilizado para enviar las solicitudes del webhook", + "Headers" : "Cabeceras", + "Health" : "Estado", + "Heartbeat successful - connection kept alive" : "Latido correcto - conexión mantenida activa", + "Helpful" : "Útil", + "Hide Filters" : "Ocultar filtros", + "Hide in forms" : "Ocultar en formularios", + "Hide in list view" : "Ocultar en la vista de lista", + "High" : "Alto", + "High Confidence" : "Confianza alta", + "Host" : "Host", + "Hourly" : "Cada hora", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Hasta qué profundidad recorrer las propiedades de objetos anidados (1-20). Los valores más altos capturan más detalle pero aumentan el tamaño del vector.", + "How often to check for updates (1-168 hours)" : "Con qué frecuencia comprobar si hay actualizaciones (1-168 horas)", + "How to handle retries for failed webhook deliveries" : "Cómo gestionar los reintentos de las entregas de webhook fallidas", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Límites de tasa por IP borrados correctamente", + "Identify properties not in the schema" : "Identificar propiedades que no están en el esquema", + "Immutable" : "Inmutable", + "Import" : "Importar", + "Import successful" : "Importación correcta", + "Improved Property" : "Propiedad mejorada", + "In use" : "En uso", + "Inactive" : "Inactivo", + "Include IDs and names of related objects for better contextual search" : "Incluir los ID y nombres de los objetos relacionados para una mejor búsqueda contextual", + "Include related object references" : "Incluir referencias a objetos relacionados", + "Include schema and register metadata" : "Incluir los metadatos del esquema y del registro", + "Initialization failed" : "Error de inicialización", + "Initialize Project" : "Inicializar proyecto", + "Inspect Fields" : "Inspeccionar campos", + "Inspect Index" : "Inspeccionar índice", + "Invalid" : "No válido", + "Invalid batch size. Must be between 1 and 5000" : "Tamaño de lote no válido. Debe estar entre 1 y 5000", + "Invalid field name provided" : "Se proporcionó un nombre de campo no válido", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects no válido. Debe ser 0 (todos) o un número positivo", + "Invalid:" : "No válido:", + "Issue" : "Problema", + "Items per page:" : "Elementos por página:", + "Just now" : "Justo ahora", + "LLM Configuration" : "Configuración de LLM", + "LLM actions menu" : "Menú de acciones de LLM", + "LLM configuration saved successfully" : "Configuración de LLM guardada correctamente", + "LLM features disabled" : "Funciones de LLM deshabilitadas", + "LLM features enabled" : "Funciones de LLM habilitadas", + "LLM must be enabled with an embedding provider configured" : "El LLM debe estar habilitado con un proveedor de embedding configurado", + "LLM settings updated successfully" : "Ajustes de LLM actualizados correctamente", + "Last" : "Último", + "Last Triggered" : "Última activación", + "Length Range:" : "Rango de longitud:", + "Linear" : "Lineal", + "Load Advanced Filters" : "Cargar filtros avanzados", + "Load advanced filters with live data from your search index" : "Cargar filtros avanzados con datos en directo de su índice de búsqueda", + "Load view" : "Cargar vista", + "Loading ConfigSets..." : "Cargando ConfigSets...", + "Loading advanced filters..." : "Cargando filtros avanzados...", + "Loading agents..." : "Cargando agentes...", + "Loading application details..." : "Cargando detalles de la aplicación...", + "Loading applications..." : "Cargando aplicaciones...", + "Loading archived conversations..." : "Cargando conversaciones archivadas...", + "Loading audit trails..." : "Cargando registros de auditoría...", + "Loading collections..." : "Cargando colecciones...", + "Loading configurations..." : "Cargando configuraciones...", + "Loading conversation..." : "Cargando conversación...", + "Loading conversations..." : "Cargando conversaciones...", + "Loading deleted items..." : "Cargando elementos eliminados...", + "Loading events..." : "Cargando eventos...", + "Loading filter data..." : "Cargando datos de filtro...", + "Loading groups..." : "Cargando grupos...", + "Loading log details..." : "Cargando detalles del registro...", + "Loading registers..." : "Cargando registros...", + "Loading schemas..." : "Cargando esquemas...", + "Loading search trails..." : "Cargando registros de búsqueda...", + "Loading sources..." : "Cargando fuentes...", + "Loading statistics..." : "Cargando estadísticas...", + "Loading users..." : "Cargando usuarios...", + "Loading views..." : "Cargando vistas...", + "Loading..." : "Cargando...", + "Local" : "Local", + "Local Version" : "Versión local", + "Locked" : "Bloqueado", + "Locked:" : "Bloqueado:", + "Login successful" : "Inicio de sesión correcto", + "Logs" : "Registros", + "Low" : "Bajo", + "Low Confidence" : "Confianza baja", + "Lucene Version:" : "Versión de Lucene:", + "Magic table synchronized successfully" : "Tabla mágica sincronizada correctamente", + "Manage Organisation Roles" : "Gestionar roles de la organización", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Gestione las colecciones SOLR (almacenes de datos) y asígnelas para objetos y archivos.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Gestione los ConfigSets de SOLR (plantillas de configuración) para sus colecciones.", + "Manage and configure agents for automated tasks" : "Gestione y configure agentes para tareas automatizadas", + "Manage and monitor file text extraction status" : "Gestione y supervise el estado de la extracción de texto de archivos", + "Manage and restore soft deleted items from your registers" : "Gestione y restaure elementos eliminados de forma reversible de sus registros", + "Manage and view detected entities from files and objects" : "Gestione y vea las entidades detectadas en archivos y objetos", + "Manage document templates and themes" : "Gestione plantillas de documentos y temas", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Gestione los flujos de trabajo de n8n para la automatización de OpenRegister. Los flujos de trabajo se almacenarán en el proyecto configurado.", + "Manage webhooks for event-driven integrations" : "Gestione webhooks para integraciones basadas en eventos", + "Manage your applications and modules" : "Gestione sus aplicaciones y módulos", + "Manage your data registers and their configurations" : "Gestione sus registros de datos y sus configuraciones", + "Manage your data schemas and their properties" : "Gestione sus esquemas de datos y sus propiedades", + "Manage your data sources and their configurations" : "Gestione sus fuentes de datos y sus configuraciones", + "Manage your saved search configurations" : "Gestione sus configuraciones de búsqueda guardadas", + "Manage your system configurations and settings" : "Gestione sus configuraciones y ajustes del sistema", + "Managed" : "Gestionado", + "Max Files (0 = all)" : "Máx. archivos (0 = todos)", + "Max Length" : "Longitud máx.", + "Max Length:" : "Longitud máx.:", + "Max Objects (0 = all)" : "Máx. objetos (0 = todos)", + "Max Retries" : "Reintentos máx.", + "Max Shards/Node" : "Máx. fragmentos/nodo", + "Max execution time (ms)" : "Tiempo máx. de ejecución (ms)", + "Max ms" : "Máx. ms", + "Max result count" : "Recuento máx. de resultados", + "Max results" : "Resultados máx.", + "Maximum" : "Máximo", + "Maximum Nesting Depth" : "Profundidad máxima de anidamiento", + "Maximum length constraint is missing" : "Falta la restricción de longitud máxima", + "Maximum length is too restrictive" : "La longitud máxima es demasiado restrictiva", + "Maximum number of files to process. Set to 0 to process all files." : "Número máximo de archivos a procesar. Establézcalo en 0 para procesar todos los archivos.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Número máximo de objetos a procesar. Establézcalo en 0 para procesar todos los objetos.", + "Maximum number of retry attempts for failed deliveries" : "Número máximo de intentos de reintento para las entregas fallidas", + "Maximum time to wait before committing changes" : "Tiempo máximo de espera antes de confirmar los cambios", + "Maximum tokens to generate" : "Tokens máximos a generar", + "Maximum value constraint is missing" : "Falta la restricción de valor máximo", + "Maximum value is too restrictive" : "El valor máximo es demasiado restrictivo", + "Medium" : "Medio", + "Medium Confidence" : "Confianza media", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Consultas medias: búsquedas con algún filtrado o múltiples parámetros (p. ej., rangos de fechas, registros/esquemas específicos)", + "Members of selected groups can access this view" : "Los miembros de los grupos seleccionados pueden acceder a esta vista", + "Members:" : "Miembros:", + "Memory prediction calculated successfully" : "Predicción de memoria calculada correctamente", + "Message content is required" : "El contenido del mensaje es obligatorio", + "Message does not belong to this conversation" : "El mensaje no pertenece a esta conversación", + "Metadata Filters" : "Filtros de metadatos", + "Method" : "Método", + "Min Length" : "Longitud mín.", + "Min execution time (ms)" : "Tiempo mín. de ejecución (ms)", + "Min ms" : "Mín. ms", + "Min result count" : "Recuento mín. de resultados", + "Min results" : "Resultados mín.", + "Minimum" : "Mínimo", + "Minimum value constraint is missing" : "Falta la restricción de valor mínimo", + "Minimum value is too restrictive" : "El valor mínimo es demasiado restrictivo", + "Missing object fields created successfully" : "Campos de objeto faltantes creados correctamente", + "Mode:" : "Modo:", + "Model" : "Modelo", + "Model Name" : "Nombre del modelo", + "Modified" : "Modificado", + "Monthly" : "Mensual", + "More sources (10-20):" : "Más fuentes (10-20):", + "Most Active Objects" : "Objetos más activos", + "Multiple Of" : "Múltiplo de", + "Name" : "Nombre", + "Names cache warmup completed" : "Precalentamiento de la caché de nombres completado", + "New Collection Name" : "Nuevo nombre de la colección", + "New Conversation" : "Nueva conversación", + "New Properties" : "Nuevas propiedades", + "New Property" : "Nueva propiedad", + "New Value" : "Nuevo valor", + "New users without specific organisation membership will be automatically added to this organisation" : "Los nuevos usuarios sin pertenencia a una organización específica se añadirán automáticamente a esta organización", + "Next" : "Siguiente", + "No" : "No", + "No ConfigSets found" : "No se encontraron ConfigSets", + "No GitHub token configured" : "No hay ningún token de GitHub configurado", + "No GitHub token provided" : "No se proporcionó ningún token de GitHub", + "No GitLab token provided" : "No se proporcionó ningún token de GitLab", + "No Organisation" : "Sin organización", + "No active filters" : "Sin filtros activos", + "No activity data available" : "No hay datos de actividad disponibles", + "No agents available" : "No hay agentes disponibles", + "No agents found" : "No se encontraron agentes", + "No app store cache exists yet" : "Todavía no existe caché de la tienda de aplicaciones", + "No applications are available." : "No hay aplicaciones disponibles.", + "No applications found" : "No se encontraron aplicaciones", + "No archived conversations" : "No hay conversaciones archivadas", + "No audit trail entries found" : "No se encontraron entradas de registro de auditoría", + "No changes recorded" : "No se registraron cambios", + "No chunks to vectorize" : "No hay fragmentos para vectorizar", + "No collections found" : "No se encontraron colecciones", + "No configuration data" : "No hay datos de configuración", + "No configurations are available." : "No hay configuraciones disponibles.", + "No configurations found" : "No se encontraron configuraciones", + "No configurations found for this application." : "No se encontraron configuraciones para esta aplicación.", + "No conversations yet. Create a new one to get started!" : "Aún no hay conversaciones. ¡Cree una nueva para empezar!", + "No data available" : "No hay datos disponibles", + "No data available for chart" : "No hay datos disponibles para el gráfico", + "No deleted items found" : "No se encontraron elementos eliminados", + "No deletion data available" : "No hay datos de eliminación disponibles", + "No description found" : "No se encontró ninguna descripción", + "No description provided" : "No se proporcionó ninguna descripción", + "No entities detected in this file. Run text extraction first." : "No se detectaron entidades en este archivo. Ejecute primero la extracción de texto.", + "No entities found" : "No se encontraron entidades", + "No entities have been detected yet" : "Aún no se ha detectado ninguna entidad", + "No events found" : "No se encontraron eventos", + "No expired audit trails found to clear" : "No se encontraron registros de auditoría caducados para borrar", + "No facetable fields available. Select a register and schema to see available filters." : "No hay campos facetables disponibles. Seleccione un registro y un esquema para ver los filtros disponibles.", + "No files found" : "No se encontraron archivos", + "No files have been extracted yet" : "Aún no se ha extraído ningún archivo", + "No files to process" : "No hay archivos para procesar", + "No files to reindex" : "No hay archivos para reindexar", + "No filters are currently active. This will delete ALL audit trail entries!" : "Actualmente no hay filtros activos. ¡Esto eliminará TODAS las entradas de registro de auditoría!", + "No log entries found" : "No se encontraron entradas de registro", + "No logs are available for this configuration." : "No hay registros disponibles para esta configuración.", + "No logs are available for this source." : "No hay registros disponibles para esta fuente.", + "No logs found" : "No se encontraron registros", + "No mismatched fields found - SOLR schema is properly configured" : "No se encontraron campos no coincidentes - el esquema de SOLR está correctamente configurado", + "No objects found" : "No se encontraron objetos", + "No objects found to vectorize" : "No se encontraron objetos para vectorizar", + "No objects selected for deletion" : "No hay objetos seleccionados para eliminar", + "No objects to delete" : "No hay objetos para eliminar", + "No objects were permanently deleted" : "No se eliminó permanentemente ningún objeto", + "No properties found" : "No se encontraron propiedades", + "No properties match your filters." : "Ninguna propiedad coincide con sus filtros.", + "No purge date set" : "No se ha establecido fecha de purga", + "No register data available" : "No hay datos de registro disponibles", + "No registers found" : "No se encontraron registros", + "No registers found for this application." : "No se encontraron registros para esta aplicación.", + "No relations found" : "No se encontraron relaciones", + "No request body available" : "No hay cuerpo de solicitud disponible", + "No response body available" : "No hay cuerpo de respuesta disponible", + "No saved views yet. Create one in the Search tab!" : "Aún no hay vistas guardadas. ¡Cree una en la pestaña de Búsqueda!", + "No schema data available" : "No hay datos de esquema disponibles", + "No schema selected for exploration" : "No se seleccionó ningún esquema para explorar", + "No schemas found" : "No se encontraron esquemas", + "No schemas found for this application." : "No se encontraron esquemas para esta aplicación.", + "No search terms data available" : "No hay datos de términos de búsqueda disponibles", + "No search trail entries found" : "No se encontraron entradas de registro de búsqueda", + "No sources are available." : "No hay fuentes disponibles.", + "No sources found" : "No se encontraron fuentes", + "No templates found" : "No se encontraron plantillas", + "No templates have been created yet" : "Aún no se ha creado ninguna plantilla", + "No valid magic-mapped register+schema combinations found" : "No se encontraron combinaciones válidas de registro+esquema con mapeo mágico", + "No views found. Create views first before configuring vectorization." : "No se encontraron vistas. Cree vistas primero antes de configurar la vectorización.", + "No views match your search" : "Ninguna vista coincide con su búsqueda", + "No webhooks found" : "No se encontraron webhooks", + "No webhooks have been configured yet" : "Aún no se ha configurado ningún webhook", + "No workflows found in this project. Create workflows in the n8n editor." : "No se encontraron flujos de trabajo en este proyecto. Cree flujos de trabajo en el editor de n8n.", + "None" : "Ninguno", + "Not helpful" : "No útil", + "Not in use" : "No en uso", + "Number Constraints" : "Restricciones numéricas", + "Number of chunks to vectorize in one API call (1-100)." : "Número de fragmentos a vectorizar en una llamada a la API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Número de fragmentos a vectorizar en una llamada a la API. Mayor = más rápido pero más memoria. Recomendado: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Número de objetos a procesar en cada lote (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Número de objetos a vectorizar en una llamada a la API. Mayor = más rápido pero más memoria. Recomendado: 10-50.", + "Numeric Range:" : "Rango numérico:", + "Object" : "Objeto", + "Object Collection" : "Colección de objetos", + "Object Count Prediction" : "Predicción del recuento de objetos", + "Object ID" : "ID del objeto", + "Object Management" : "Gestión de objetos", + "Object Vectorization" : "Vectorización de objetos", + "Object Warmup" : "Precalentamiento de objetos", + "Object collection not configured" : "Colección de objetos no configurada", + "Object not found" : "Objeto no encontrado", + "Object permanently deleted" : "Objeto eliminado permanentemente", + "Object restored successfully" : "Objeto restaurado correctamente", + "Object settings updated successfully" : "Ajustes del objeto actualizados correctamente", + "Object sources" : "Fuentes de objetos", + "Object unlocked successfully" : "Objeto desbloqueado correctamente", + "Object vectorization configuration saved successfully" : "Configuración de vectorización de objetos guardada correctamente", + "Object vectorized successfully" : "Objeto vectorizado correctamente", + "Object:" : "Objeto:", + "Objects" : "Objetos", + "Objects Analyzed" : "Objetos analizados", + "Objects Distribution" : "Distribución de objetos", + "Objects being analyzed" : "Objetos en análisis", + "Objects by Register" : "Objetos por registro", + "Objects by Schema" : "Objetos por esquema", + "Objects deletion completed successfully" : "Eliminación de objetos completada correctamente", + "Objects in schema" : "Objetos en el esquema", + "Objects to Process:" : "Objetos a procesar:", + "Objects to be analyzed" : "Objetos que se analizarán", + "Objects to be deleted" : "Objetos que se eliminarán", + "Objects to be validated" : "Objetos que se validarán", + "Objects will be serialized as JSON text before vectorization" : "Los objetos se serializarán como texto JSON antes de la vectorización", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Los objetos se eliminarán de forma reversible (marcados como eliminados pero conservados en la base de datos). Se pueden recuperar más tarde si es necesario.", + "Old Value" : "Valor anterior", + "Oldest Item (days)" : "Elemento más antiguo (días)", + "Ollama Chat Settings" : "Ajustes de chat de Ollama", + "Ollama Embedding Configuration" : "Configuración de embedding de Ollama", + "Ollama URL" : "URL de Ollama", + "Only With Changes" : "Solo con cambios", + "Open Register Objects" : "Objetos de Open Register", + "Open n8n Editor" : "Abrir el editor de n8n", + "OpenAI Chat Settings" : "Ajustes de chat de OpenAI", + "OpenAI Embedding Configuration" : "Configuración de embedding de OpenAI", + "OpenRegister Settings" : "Ajustes de OpenRegister", + "Optional webhook secret for signature verification" : "Secreto opcional del webhook para la verificación de firma", + "Order" : "Orden", + "Organisation" : "Organización", + "Organisation ID" : "ID de la organización", + "Organisation Members" : "Miembros de la organización", + "Organisation Statistics" : "Estadísticas de la organización", + "Organisation created successfully" : "Organización creada correctamente", + "Organisation settings saved successfully" : "Ajustes de la organización guardados correctamente", + "Organisations" : "Organizaciones", + "Organization" : "Organización", + "Organization ID (Optional)" : "ID de la organización (opcional)", + "Orphaned Items" : "Elementos huérfanos", + "Owner" : "Propietario", + "Owner:" : "Propietario:", + "Parallel Mode (Faster, more resource intensive)" : "Modo paralelo (más rápido, más intensivo en recursos)", + "Parallel:" : "Paralelo:", + "Password" : "Contraseña", + "Password for SOLR authentication (optional)" : "Contraseña para la autenticación de SOLR (opcional)", + "Path" : "Ruta", + "Path in repository" : "Ruta en el repositorio", + "Path where the configuration file will be saved in the repository" : "Ruta donde se guardará el archivo de configuración en el repositorio", + "Path where the register OAS file will be saved in the repository" : "Ruta donde se guardará el archivo OAS del registro en el repositorio", + "Pattern (regex)" : "Patrón (regex)", + "Pattern Issue" : "Problema de patrón", + "Pattern constraint is missing" : "Falta la restricción de patrón", + "Patterns:" : "Patrones:", + "Pending" : "Pendiente", + "Permanent Delete Mode" : "Modo de eliminación permanente", + "Permanently Delete" : "Eliminar permanentemente", + "Permanently delete already soft-deleted objects" : "Eliminar permanentemente los objetos ya eliminados de forma reversible", + "Permanently delete this conversation?" : "¿Eliminar permanentemente esta conversación?", + "Person" : "Persona", + "Personal Data" : "Datos personales", + "Phone" : "Teléfono", + "Photos" : "Fotos", + "Please create an agent in the" : "Por favor, cree un agente en el", + "Please select an agent to continue" : "Por favor, seleccione un agente para continuar", + "Please select which register and schema to use for the new object" : "Por favor, seleccione qué registro y esquema usar para el nuevo objeto", + "Please try again later." : "Por favor, inténtelo de nuevo más tarde.", + "Please wait while we fetch your agents." : "Por favor, espere mientras recuperamos sus agentes.", + "Please wait while we fetch your applications." : "Por favor, espere mientras recuperamos sus aplicaciones.", + "Please wait while we fetch your configurations." : "Por favor, espere mientras recuperamos sus configuraciones.", + "Please wait while we fetch your deleted items." : "Por favor, espere mientras recuperamos sus elementos eliminados.", + "Please wait while we fetch your sources." : "Por favor, espere mientras recuperamos sus fuentes.", + "Polls" : "Encuestas", + "Popular Search Terms" : "Términos de búsqueda populares", + "Port" : "Puerto", + "Prerequisites" : "Requisitos previos", + "Press Enter to send, Shift+Enter for new line" : "Pulse Intro para enviar, Mayús+Intro para una nueva línea", + "Previous" : "Anterior", + "Private" : "Privado", + "Processes chunks in batches with simulated parallelism." : "Procesa los fragmentos en lotes con paralelismo simulado.", + "Processes file chunks sequentially (safest)." : "Procesa los fragmentos de archivo secuencialmente (lo más seguro).", + "Processes objects in chunks with simulated parallelism." : "Procesa los objetos en fragmentos con paralelismo simulado.", + "Processes objects sequentially (safest)." : "Procesa los objetos secuencialmente (lo más seguro).", + "Processing" : "Procesando", + "Processing Limits" : "Límites de procesamiento", + "Processing..." : "Procesando...", + "Project initialized successfully" : "Proyecto inicializado correctamente", + "Project not found. Please initialize first." : "Proyecto no encontrado. Por favor, inicialícelo primero.", + "Properties" : "Propiedades", + "Property Behaviors" : "Comportamientos de la propiedad", + "Property Configuration:" : "Configuración de la propiedad:", + "Property Filters" : "Filtros de propiedad", + "Property Title" : "Título de la propiedad", + "Property Type" : "Tipo de propiedad", + "Property can be improved" : "La propiedad se puede mejorar", + "Protected" : "Protegido", + "Provider" : "Proveedor", + "Provider is required for testing" : "El proveedor es obligatorio para realizar pruebas", + "Public" : "Público", + "Public View" : "Vista pública", + "Public views can be accessed by anyone in the system" : "Las vistas públicas pueden ser accedidas por cualquiera en el sistema", + "Publish" : "Publicar", + "Publish Objects" : "Publicar objetos", + "Publish all objects in this schema" : "Publicar todos los objetos de este esquema", + "Published" : "Publicado", + "Published:" : "Publicado:", + "Publishing..." : "Publicando...", + "Purge" : "Purgar", + "Purge Date" : "Fecha de purga", + "Query Complexity Distribution" : "Distribución de la complejidad de las consultas", + "Query parameter is required" : "El parámetro de consulta es obligatorio", + "RAG" : "RAG", + "RAG Configuration" : "Configuración de RAG", + "Rate limits cleared successfully" : "Límites de tasa borrados correctamente", + "Raw Changes Data" : "Datos de cambios sin procesar", + "Re-vectorize on object update" : "Revectorizar al actualizar el objeto", + "Read" : "Leer", + "Recommendations:" : "Recomendaciones:", + "Recommended Type:" : "Tipo recomendado:", + "Recommended: 5 sources" : "Recomendado: 5 fuentes", + "Refresh" : "Actualizar", + "Refresh Data" : "Actualizar datos", + "Refresh Stats" : "Actualizar estadísticas", + "Refresh Workflows" : "Actualizar flujos de trabajo", + "Refresh dashboard" : "Actualizar panel de control", + "Refresh database info" : "Actualizar información de la base de datos", + "Register" : "Registro", + "Register ID" : "ID del registro", + "Register Statistics" : "Estadísticas del registro", + "Register Totals" : "Totales del registro", + "Register depublished successfully" : "Registro despublicado correctamente", + "Register not found" : "Registro no encontrado", + "Register objects deletion completed successfully" : "Eliminación de objetos del registro completada correctamente", + "Register published successfully" : "Registro publicado correctamente", + "Register/Schema Usage" : "Uso de registro/esquema", + "Registers" : "Registros", + "Reindex" : "Reindexar", + "Reindex completed" : "Reindexación completada", + "Reindex completed successfully" : "Reindexación completada correctamente", + "Reindex failed" : "Reindexación fallida", + "Relations" : "Relaciones", + "Remote Version" : "Versión remota", + "Remove" : "Quitar", + "Remove filter" : "Quitar filtro", + "Remove from favorites" : "Quitar de favoritos", + "Remove group" : "Quitar grupo", + "Removed" : "Quitado", + "Removed from favorites" : "Quitado de favoritos", + "Rename Conversation" : "Renombrar conversación", + "Rename conversation" : "Renombrar conversación", + "Replicas" : "Réplicas", + "Repository" : "Repositorio", + "Request Body" : "Cuerpo de la solicitud", + "Request Data" : "Datos de la solicitud", + "Request Details" : "Detalles de la solicitud", + "Request body must be JSON with a \"query\" field" : "El cuerpo de la solicitud debe ser JSON con un campo \"query\"", + "Request timeout in seconds" : "Tiempo de espera de la solicitud en segundos", + "Required field" : "Campo obligatorio", + "Required status is inconsistent" : "El estado de obligatoriedad es inconsistente", + "Rerun Search" : "Volver a ejecutar la búsqueda", + "Reset Changes" : "Restablecer cambios", + "Reset Filters" : "Restablecer filtros", + "Response Body" : "Cuerpo de la respuesta", + "Response Details" : "Detalles de la respuesta", + "Response Time:" : "Tiempo de respuesta:", + "Restore" : "Restaurar", + "Restore conversation" : "Restaurar conversación", + "Restore or permanently delete items" : "Restaurar o eliminar permanentemente elementos", + "Result Count Range" : "Rango del recuento de resultados", + "Results" : "Resultados", + "Retrieve all objects for this schema" : "Recuperar todos los objetos de este esquema", + "Retry" : "Reintentar", + "Retry Failed Extractions" : "Reintentar extracciones fallidas", + "Retry Policy" : "Política de reintentos", + "Retry completed" : "Reintento completado", + "Retrying..." : "Reintentando...", + "Risk Level" : "Nivel de riesgo", + "Roles updated successfully" : "Roles actualizados correctamente", + "SOLR Connection Settings" : "Ajustes de conexión de SOLR", + "SOLR Version:" : "Versión de SOLR:", + "SOLR actions menu" : "Menú de acciones de SOLR", + "SOLR base path (usually /solr)" : "Ruta base de SOLR (normalmente /solr)", + "SOLR is disabled" : "SOLR está deshabilitado", + "SOLR is not available or not configured" : "SOLR no está disponible o no está configurado", + "SOLR logging disabled" : "Registro de SOLR deshabilitado", + "SOLR logging enabled" : "Registro de SOLR habilitado", + "SOLR search disabled" : "Búsqueda de SOLR deshabilitada", + "SOLR search enabled" : "Búsqueda de SOLR habilitada", + "SOLR server hostname or IP address" : "Nombre de host o dirección IP del servidor SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Número de puerto del servidor SOLR (opcional, predeterminado 8983)", + "SOLR setup completed successfully" : "Configuración de SOLR completada correctamente", + "SOLR setup failed" : "Configuración de SOLR fallida", + "SOLR setup failed - check logs" : "Configuración de SOLR fallida - revise los registros", + "Sample Values:" : "Valores de muestra:", + "Save" : "Guardar", + "Save Configuration" : "Guardar configuración", + "Save Connection Settings" : "Guardar ajustes de conexión", + "Save Roles" : "Guardar roles", + "Save Settings" : "Guardar ajustes", + "Save current search as view" : "Guardar la búsqueda actual como vista", + "Saved Views" : "Vistas guardadas", + "Saving..." : "Guardando...", + "Schema" : "Esquema", + "Schema ID" : "ID del esquema", + "Schema Name" : "Nombre del esquema", + "Schema Statistics" : "Estadísticas del esquema", + "Schema depublished successfully" : "Esquema despublicado correctamente", + "Schema objects deletion completed successfully" : "Eliminación de objetos del esquema completada correctamente", + "Schema published successfully" : "Esquema publicado correctamente", + "Schema successfully created" : "Esquema creado correctamente", + "Schema successfully updated" : "Esquema actualizado correctamente", + "Schemas" : "Esquemas", + "Scheme" : "Esquema", + "Search / Views" : "Búsqueda / Vistas", + "Search Activity" : "Actividad de búsqueda", + "Search Analytics" : "Analítica de búsqueda", + "Search Entities" : "Buscar entidades", + "Search Files" : "Buscar archivos", + "Search GitHub" : "Buscar en GitHub", + "Search GitLab" : "Buscar en GitLab", + "Search Method" : "Método de búsqueda", + "Search Term" : "Término de búsqueda", + "Search Trail Details" : "Detalles del registro de búsqueda", + "Search Trail Management" : "Gestión del registro de búsqueda", + "Search Trail Statistics" : "Estadísticas del registro de búsqueda", + "Search Trails" : "Registros de búsqueda", + "Search Views" : "Buscar vistas", + "Search Webhooks" : "Buscar webhooks", + "Search by file name or path" : "Buscar por nombre de archivo o ruta", + "Search by name or URL" : "Buscar por nombre o URL", + "Search by value" : "Buscar por valor", + "Search in Files" : "Buscar en archivos", + "Search in Objects" : "Buscar en objetos", + "Search property names..." : "Buscar nombres de propiedades...", + "Search trail deletion not implemented yet" : "La eliminación de registros de búsqueda aún no está implementada", + "Search views..." : "Buscar vistas...", + "Secret" : "Secreto", + "Secret key for HMAC signature generation (optional)" : "Clave secreta para la generación de firmas HMAC (opcional)", + "Select AI Agent" : "Seleccionar agente de IA", + "Select All" : "Seleccionar todo", + "Select ConfigSet" : "Seleccionar ConfigSet", + "Select File Types to Vectorize:" : "Seleccionar tipos de archivo a vectorizar:", + "Select HTTP method" : "Seleccionar método HTTP", + "Select Register and Schema" : "Seleccionar registro y esquema", + "Select Views to Vectorize:" : "Seleccionar vistas a vectorizar:", + "Select a Nextcloud group to add" : "Seleccione un grupo de Nextcloud para añadir", + "Select a branch" : "Seleccione una rama", + "Select a model or type a custom model name" : "Seleccione un modelo o escriba un nombre de modelo personalizado", + "Select a repository" : "Seleccione un repositorio", + "Select a repository you have write access to" : "Seleccione un repositorio al que tenga acceso de escritura", + "Select an AI Agent" : "Seleccione un agente de IA", + "Select an AI agent to begin chatting with your data." : "Seleccione un agente de IA para empezar a chatear con sus datos.", + "Select backend" : "Seleccionar backend", + "Select chat model" : "Seleccionar modelo de chat", + "Select collection for files" : "Seleccionar colección para archivos", + "Select collection for objects" : "Seleccionar colección para objetos", + "Select default organisation" : "Seleccionar organización predeterminada", + "Select event to listen to..." : "Seleccione el evento a escuchar...", + "Select model" : "Seleccionar modelo", + "Select options..." : "Seleccionar opciones...", + "Select period" : "Seleccionar periodo", + "Select property to send as payload" : "Seleccionar propiedad a enviar como carga útil", + "Select provider" : "Seleccionar proveedor", + "Select registers and schemas to save a view" : "Seleccione registros y esquemas para guardar una vista", + "Select retry policy" : "Seleccionar política de reintentos", + "Select the branch to publish to" : "Seleccione la rama en la que publicar", + "Select the event this webhook should listen to" : "Seleccione el evento que este webhook debe escuchar", + "Select views to vectorize:" : "Seleccionar vistas a vectorizar:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Seleccione qué grupos de Nextcloud están disponibles para esta organización. Los usuarios de estos grupos tendrán acceso a los recursos de la organización.", + "Select which data views the AI can search" : "Seleccione qué vistas de datos puede buscar la IA", + "Select which property from the event should be used as the webhook payload data" : "Seleccione qué propiedad del evento debe usarse como datos de la carga útil del webhook", + "Select which tools the AI can use to perform actions" : "Seleccione qué herramientas puede usar la IA para realizar acciones", + "Selected" : "Seleccionado", + "Selected Groups" : "Grupos seleccionados", + "Selected audit trails deleted successfully" : "Registros de auditoría seleccionados eliminados correctamente", + "Selected search trails deleted successfully" : "Registros de búsqueda seleccionados eliminados correctamente", + "Selected users can access this view" : "Los usuarios seleccionados pueden acceder a esta vista", + "Send additional feedback" : "Enviar comentarios adicionales", + "Send as CloudEvent" : "Enviar como CloudEvent", + "Sensitive PII" : "PII sensible", + "Serial Mode (Safer, slower)" : "Modo serie (más seguro, más lento)", + "Serial:" : "Serie:", + "Server Configuration" : "Configuración del servidor", + "Server Information" : "Información del servidor", + "Settings" : "Ajustes", + "Shards" : "Fragmentos", + "Share" : "Compartir", + "Share with Groups" : "Compartir con grupos", + "Share with Users" : "Compartir con usuarios", + "Show Filters" : "Mostrar filtros", + "Show less" : "Mostrar menos", + "Show only entries with changes" : "Mostrar solo entradas con cambios", + "Simple" : "Simple", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Consultas simples: búsquedas de texto básicas con parámetros mínimos (p. ej., un solo término de búsqueda, sin filtros avanzados)", + "Size" : "Tamaño", + "Slug" : "Slug", + "Soft Delete Mode" : "Modo de eliminación reversible", + "Soft Deleted Items" : "Elementos eliminados de forma reversible", + "SolrCloud mode enabled" : "Modo SolrCloud habilitado", + "Source" : "Fuente", + "Sources" : "Fuentes", + "Sources:" : "Fuentes:", + "Standalone SOLR mode" : "Modo SOLR independiente", + "Start Conversation" : "Iniciar conversación", + "Start Vectorization" : "Iniciar vectorización", + "Start a conversation" : "Iniciar una conversación", + "Starting names cache warmup..." : "Iniciando el precalentamiento de la caché de nombres...", + "Starting..." : "Iniciando...", + "Statistics" : "Estadísticas", + "Stats calculation not yet implemented" : "El cálculo de estadísticas aún no está implementado", + "Status" : "Estado", + "Status Code" : "Código de estado", + "String Constraints" : "Restricciones de cadena", + "Success" : "Éxito", + "Success Rate" : "Tasa de éxito", + "Success Status" : "Estado de éxito", + "Successful" : "Correcto", + "Successfully joined organisation" : "Se ha unido a la organización correctamente", + "Sync Table" : "Sincronizar tabla", + "Synchronization Settings" : "Ajustes de sincronización", + "System Default" : "Predeterminado del sistema", + "Technical Description" : "Descripción técnica", + "Technical description for developers and administrators" : "Descripción técnica para desarrolladores y administradores", + "Temperature" : "Temperatura", + "Templates" : "Plantillas", + "Test" : "Probar", + "Test Chat" : "Probar chat", + "Test Connection" : "Probar conexión", + "Test Embedding" : "Probar embedding", + "Test endpoint executed successfully" : "Punto de acceso de prueba ejecutado correctamente", + "Test webhook delivered successfully" : "Webhook de prueba entregado correctamente", + "Test webhook delivery failed" : "La entrega del webhook de prueba falló", + "Test webhook sent successfully" : "Webhook de prueba enviado correctamente", + "Testing..." : "Probando...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Los fragmentos de texto se generan durante la extracción de archivos y se almacenan en la base de datos. La vectorización lee estos archivos prefragmentados y los convierte en embeddings.", + "Text extracted successfully" : "Texto extraído correctamente", + "Text extraction disabled" : "Extracción de texto deshabilitada", + "The URL where webhook events will be sent" : "La URL a la que se enviarán los eventos del webhook", + "The requested conversation does not exist" : "La conversación solicitada no existe", + "The saved view and all its search configuration" : "La vista guardada y toda su configuración de búsqueda", + "There are no audit trail entries matching your current filters." : "No hay entradas de registro de auditoría que coincidan con sus filtros actuales.", + "There are no deleted items matching your current filters." : "No hay elementos eliminados que coincidan con sus filtros actuales.", + "There are no search trail entries matching your current filters." : "No hay entradas de registro de búsqueda que coincidan con sus filtros actuales.", + "There are no webhook log entries matching your filters." : "No hay entradas de registro de webhook que coincidan con sus filtros.", + "This action cannot be undone." : "Esta acción no se puede deshacer.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Esta acción no se puede deshacer. Asegúrese de que ninguna colección esté usando este ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Este agente no tiene vistas ni herramientas configurables, pero aún puede ajustar los ajustes de RAG a continuación.", + "This analysis may take some time" : "Este análisis puede llevar algún tiempo", + "This audit trail entry does not contain any change information." : "Esta entrada de registro de auditoría no contiene información de cambios.", + "This configuration has no data defined." : "Esta configuración no tiene datos definidos.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Este punto de acceso está obsoleto. Use en su lugar los puntos de acceso basados en fragmentos.", + "This entity has no relations to objects or files" : "Esta entidad no tiene relaciones con objetos ni archivos", + "This is a test webhook from OpenRegister" : "Este es un webhook de prueba de OpenRegister", + "This schema must use Magic Table configuration to sync" : "Este esquema debe usar la configuración de Tabla Mágica para sincronizar", + "This source has no associated registers." : "Esta fuente no tiene registros asociados.", + "This will copy the _default ConfigSet with the new name" : "Esto copiará el ConfigSet _default con el nuevo nombre", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Esto eliminará permanentemente TODOS los embeddings (vectores) de la base de datos. Necesitará revectorizar todos los objetos y archivos. Esta acción no se puede deshacer.\\n\\n¿Está seguro de que desea continuar?", + "This will permanently delete:" : "Esto eliminará permanentemente:", + "Timeout (seconds)" : "Tiempo de espera (segundos)", + "Timestamp" : "Marca de tiempo", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Consejo: Habilite solo las vistas que necesiten búsqueda semántica para minimizar los costes de embedding. Las tablas de búsqueda simples rara vez necesitan vectorización.", + "Title" : "Título", + "To Date" : "Hasta fecha", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Para ajustar el tamaño o la estrategia de los fragmentos, vaya a Configuración de archivos → Límites de procesamiento.", + "To change the embedding provider or model, go to LLM Configuration." : "Para cambiar el proveedor o el modelo de embedding, vaya a Configuración de LLM.", + "To date" : "Hasta fecha", + "Toggle search sidebar" : "Alternar la barra lateral de búsqueda", + "Tools" : "Herramientas", + "Top Deleters" : "Principales eliminadores", + "Total" : "Total", + "Total Audit Trails" : "Total de registros de auditoría", + "Total Chunks Available:" : "Total de fragmentos disponibles:", + "Total Deleted Items" : "Total de elementos eliminados", + "Total Members" : "Total de miembros", + "Total Objects" : "Total de objetos", + "Total Objects in Database:" : "Total de objetos en la base de datos:", + "Total Organisations" : "Total de organizaciones", + "Total Results" : "Total de resultados", + "Total Searches" : "Total de búsquedas", + "Total Size" : "Tamaño total", + "Total Storage" : "Almacenamiento total", + "Total:" : "Total:", + "Totals" : "Totales", + "Type" : "Tipo", + "Type Issue" : "Problema de tipo", + "Type Variations:" : "Variaciones de tipo:", + "Type must be \"positive\" or \"negative\"" : "El tipo debe ser \"positive\" o \"negative\"", + "Type to search for groups" : "Escriba para buscar grupos", + "Type to search for users" : "Escriba para buscar usuarios", + "Type to search groups..." : "Escriba para buscar grupos...", + "Type to search users..." : "Escriba para buscar usuarios...", + "URL" : "URL", + "URL where Ollama is running" : "URL donde se está ejecutando Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Sin cambios", + "Unique Search Terms" : "Términos de búsqueda únicos", + "Unique Terms" : "Términos únicos", + "Unknown" : "Desconocido", + "Unknown Register" : "Registro desconocido", + "Unknown Schema" : "Esquema desconocido", + "Untitled View" : "Vista sin título", + "Update" : "Actualizar", + "Update Operations" : "Operaciones de actualización", + "Update configuration: ..." : "Actualizar configuración: ...", + "Update register OAS: ..." : "Actualizar OAS del registro: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Actualizar los vectores cuando cambien los datos del objeto (recomendado para una búsqueda precisa)", + "Updated" : "Actualizado", + "Updated At" : "Actualizado el", + "Updated:" : "Actualizado:", + "Uptime:" : "Tiempo de actividad:", + "Usage Count" : "Recuento de uso", + "Use SolrCloud with Zookeeper for distributed search" : "Usar SolrCloud con Zookeeper para la búsqueda distribuida", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Use filtros para acotar las entradas de registro de auditoría por registro, esquema, tipo de acción, usuario, rango de fechas o ID de objeto.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Use filtros para acotar los elementos eliminados por registro, esquema, fecha de eliminación o usuario que los eliminó.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Use filtros para acotar las entradas de registro de búsqueda por registro, esquema, estado de éxito, usuario, rango de fechas, términos de búsqueda o métricas de rendimiento.", + "Used By Collections" : "Usado por colecciones", + "User" : "Usuario", + "User Agent Statistics" : "Estadísticas del agente de usuario", + "User Description" : "Descripción del usuario", + "User rate limits cleared successfully" : "Límites de tasa por usuario borrados correctamente", + "User removed successfully" : "Usuario eliminado correctamente", + "User-friendly description shown in forms and help text" : "Descripción fácil de usar mostrada en formularios y texto de ayuda", + "Username" : "Nombre de usuario", + "Username for SOLR authentication (optional)" : "Nombre de usuario para la autenticación de SOLR (opcional)", + "Users" : "Usuarios", + "Using Pre-Generated Chunks" : "Usando fragmentos pregenerados", + "Validate" : "Validar", + "Validate Objects" : "Validar objetos", + "Validation completed successfully" : "Validación completada correctamente", + "Validation failed" : "Validación fallida", + "Value" : "Valor", + "Vector Dimensions" : "Dimensiones del vector", + "Vector Search Backend" : "Backend de búsqueda vectorial", + "Vector field: _embedding_" : "Campo del vector: _embedding_", + "Vectorization Triggers" : "Disparadores de vectorización", + "Vectorize All Files" : "Vectorizar todos los archivos", + "Vectorize All Objects" : "Vectorizar todos los objetos", + "Vectorize all file types" : "Vectorizar todos los tipos de archivo", + "Vectorize all views" : "Vectorizar todas las vistas", + "Vectorize on object creation" : "Vectorizar al crear el objeto", + "Vectors will be stored in your existing object and file collections" : "Los vectores se almacenarán en sus colecciones existentes de objetos y archivos", + "Version" : "Versión", + "Very High" : "Muy alto", + "View API Docs" : "Ver documentación de la API", + "View Changes" : "Ver cambios", + "View Details" : "Ver detalles", + "View Error" : "Ver error", + "View File" : "Ver archivo", + "View Full Details" : "Ver todos los detalles", + "View Logs" : "Ver registros", + "View Name" : "Nombre de la vista", + "View Object" : "Ver objeto", + "View Parameters" : "Ver parámetros", + "View Selection" : "Selección de vistas", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Vea y analice los registros de búsqueda con capacidades avanzadas de filtrado y analítica", + "View and analyze system audit trails with advanced filtering capabilities" : "Vea y analice los registros de auditoría del sistema con capacidades avanzadas de filtrado", + "View deleted successfully" : "Vista eliminada correctamente", + "View entity details and manage relations" : "Ver detalles de la entidad y gestionar relaciones", + "View name" : "Nombre de la vista", + "View name is required" : "El nombre de la vista es obligatorio", + "View saved successfully!" : "¡Vista guardada correctamente!", + "View search analytics and manage search logs" : "Ver la analítica de búsqueda y gestionar los registros de búsqueda", + "View successfully deleted" : "Vista eliminada correctamente", + "View successfully updated" : "Vista actualizada correctamente", + "View updated successfully!" : "¡Vista actualizada correctamente!", + "View webhook delivery logs and filter by webhook" : "Ver los registros de entrega de webhooks y filtrar por webhook", + "Views" : "Vistas", + "Visible to users" : "Visible para los usuarios", + "Wait for Response" : "Esperar respuesta", + "Wait for webhook response before continuing (required for request/response flows)" : "Esperar la respuesta del webhook antes de continuar (necesario para los flujos de solicitud/respuesta)", + "Warmup Names Cache" : "Precalentar caché de nombres", + "Warning" : "Advertencia", + "Warning:" : "Advertencia:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detalles del registro del webhook", + "Webhook Logs" : "Registros del webhook", + "Webhook created successfully" : "Webhook creado correctamente", + "Webhook deleted" : "Webhook eliminado", + "Webhook retry delivered successfully" : "Reintento del webhook entregado correctamente", + "Webhook retry delivery failed" : "La entrega del reintento del webhook falló", + "Webhook updated" : "Webhook actualizado", + "Webhook updated successfully" : "Webhook actualizado correctamente", + "Webhooks" : "Webhooks", + "Weekly" : "Semanal", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Envolver la carga útil del webhook en formato CloudEvents para una mejor interoperabilidad", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Sí", + "You" : "Usted", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Puede crear nuevos ConfigSets basados en la plantilla _default, o subir personalizados directamente a su servidor SOLR.", + "You do not have access to this conversation" : "No tiene acceso a esta conversación", + "You do not have permission to delete this conversation" : "No tiene permiso para eliminar esta conversación", + "You do not have permission to modify this conversation" : "No tiene permiso para modificar esta conversación", + "You do not have permission to restore this conversation" : "No tiene permiso para restaurar esta conversación", + "You must be logged in to favorite views" : "Debe haber iniciado sesión para marcar vistas como favoritas", + "You need an AI agent to start a conversation." : "Necesita un agente de IA para iniciar una conversación.", + "Your Fireworks AI API key. Get one at" : "Su clave de API de Fireworks AI. Obtenga una en", + "Your OpenAI API key. Get one at" : "Su clave de API de OpenAI. Obtenga una en", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Sus comentarios han sido registrados. Opcionalmente, puede proporcionar detalles adicionales aquí...", + "Zookeeper Hosts" : "Hosts de Zookeeper", + "Zookeeper Port" : "Puerto de Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Ajustes de Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Cadena de conexión de Zookeeper para SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Número de puerto de Zookeeper (opcional, predeterminado 2181)", + "chunk" : "fragmento", + "chunks" : "fragmentos", + "chunks processed" : "fragmentos procesados", + "configuration(s)" : "configuración(es)", + "e.g., lib/Settings/config.json" : "p. ej., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "p. ej., lib/Settings/register.json", + "failed" : "fallido", + "file" : "archivo", + "files" : "archivos", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "miembros", + "menu or contact someone with permission to create agents." : "menú o contacte con alguien que tenga permiso para crear agentes.", + "messages" : "mensajes", + "n8n URL and API key are required" : "Se requieren la URL y la clave de API de n8n", + "n8n configuration saved successfully" : "Configuración de n8n guardada correctamente", + "n8n connection not configured" : "Conexión de n8n no configurada", + "n8n connection successful" : "Conexión de n8n correcta", + "n8n connection test successful" : "Prueba de conexión de n8n correcta", + "n8n integration disabled" : "Integración de n8n deshabilitada", + "n8n integration enabled" : "Integración de n8n habilitada", + "n8n project initialized successfully" : "Proyecto de n8n inicializado correctamente", + "n8n settings saved successfully" : "Ajustes de n8n guardados correctamente", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objetos", + "objects processed" : "objetos procesados", + "or pick a range" : "o elija un rango", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "ofrece un buen equilibrio entre velocidad y precisión.", + "register(s)" : "registro(s)", + "register(s) selected" : "registro(s) seleccionado(s)", + "required" : "obligatorio", + "results" : "resultados", + "schema(s)" : "esquema(s)", + "schema(s) selected" : "esquema(s) seleccionado(s)", + "sk-..." : "sk-...", + "this application" : "esta aplicación", + "to" : "a", + "vectorized" : "vectorizado", + "views selected" : "vistas seleccionadas", + "ℹ️ Current Configuration" : "ℹ️ Configuración actual", + "⚡ Batch Processing" : "⚡ Procesamiento por lotes", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Contexto completo ✓ Menos probabilidad de pasar por alto detalles ✗ Respuestas más lentas ✗ Puede incluir información menos relevante", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Respuestas más rápidas ✓ Respuestas más enfocadas ✗ Puede pasar por alto información relevante", + "✨ AI Features" : "✨ Funciones de IA", + "All search trails cleared successfully" : "Todos los registros de búsqueda se han borrado correctamente", + "Multiple search trail deletion not implemented yet" : "La eliminación de múltiples registros de búsqueda aún no está implementada", + "No expired search trails found to clear" : "No se encontraron registros de búsqueda caducados para borrar", + "Missing message" : "Falta el mensaje", + "message content is required" : "el contenido del mensaje es obligatorio", + "Missing conversation or agentUuid" : "Falta la conversación o el agentUuid", + "Access denied" : "Acceso denegado", + "Conversation not found" : "Conversación no encontrada", + "AI service not configured" : "Servicio de IA no configurado", + "Failed to process message" : "No se pudo procesar el mensaje", + "Missing conversationId" : "Falta el conversationId", + "conversationId is required" : "el conversationId es obligatorio", + "Failed to fetch conversation history" : "No se pudo recuperar el historial de la conversación", + "Failed to clear conversation" : "No se pudo borrar la conversación", + "Invalid feedback type" : "Tipo de comentario no válido", + "type must be \"positive\" or \"negative\"" : "el tipo debe ser \"positive\" o \"negative\"", + "Message not found" : "Mensaje no encontrado", + "Failed to save feedback" : "No se pudieron guardar los comentarios", + "Failed to get chat statistics" : "No se pudieron obtener las estadísticas del chat", + "Not Found" : "No encontrado", + "Engine not found" : "Motor no encontrado", + "Consumer not found" : "Consumidor no encontrado", + "Backend parameter is required" : "El parámetro backend es obligatorio", + "Failed to get database information: %s" : "No se pudo obtener la información de la base de datos: %s", + "SOLR setup error: %s" : "Error en la configuración de SOLR: %s", + "Reindex failed: %s" : "Reindexación fallida: %s", + "Anonymized" : "Anonimizado", + "Entities detected" : "Entidades detectadas", + "Extract Now" : "Extraer ahora", + "Extracted at" : "Extraído el", + "Extraction" : "Extracción", + "Failed to load extraction data" : "No se pudieron cargar los datos de extracción", + "Failed to load register data" : "No se pudieron cargar los datos del registro", + "Failed to retrieve extraction status." : "No se pudo recuperar el estado de la extracción.", + "Failed to retrieve objects for file." : "No se pudieron recuperar los objetos del archivo.", + "No extraction data available for this file" : "No hay datos de extracción disponibles para este archivo", + "No register objects reference this file" : "Ningún objeto de registro hace referencia a este archivo", + "Not extracted" : "No extraído", + "Register Objects" : "Objetos del registro", + "Risk level" : "Nivel de riesgo", + "Text chunks" : "Fragmentos de texto", + "Unknown error" : "Error desconocido", + "Very high" : "Muy alto", + "{title} in {register} / {schema}" : "{title} en {register} / {schema}", + "Push Notifications" : "Notificaciones push", + "Real-time push notification status via notify_push" : "Estado de las notificaciones push en tiempo real a través de notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push en tiempo real no disponible — la aplicación notify_push no está instalada", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Instale la aplicación notify_push desde la tienda de aplicaciones de Nextcloud para habilitar las actualizaciones en tiempo real.", + "Open Nextcloud App Store" : "Abrir la tienda de aplicaciones de Nextcloud", + "notify_push is installed but not yet active" : "notify_push está instalado pero aún no está activo", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push está instalado pero OpenRegister aún no ha confirmado un envío push correcto. Guarde un objeto para activarlo, o revise su configuración de notify_push.", + "notify_push configuration guide" : "guía de configuración de notify_push", + "Realtime push active" : "Push en tiempo real activo", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Las notificaciones push en tiempo real están activas. Los clientes conectados reciben actualizaciones instantáneas cuando se crean, actualizan o eliminan objetos.", + "Bookmarks" : "Bookmarks", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/es.json b/l10n/es.json new file mode 100644 index 0000000000..4c6f6c1ee5 --- /dev/null +++ b/l10n/es.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(no subject)": "(sin asunto)", + "(no title)": "(sin título)", + "(unnamed)": "(sin nombre)", + "(untitled card)": "(tarjeta sin título)", + "Activities": "Actividades", + "Add Endpoint": "Añadir endpoint", + "Add Source": "Añadir fuente", + "Add card": "Añadir tarjeta", + "Add contact": "Añadir contacto", + "All clear": "Todo correcto", + "Articles": "Artículos", + "Code": "Código", + "Compliance": "Cumplimiento", + "Contacts": "Contactos", + "Create Agent": "Crear agente", + "Create Application": "Crear aplicación", + "Create event": "Crear evento", + "Erasure complete": "Borrado completado", + "Erasure preview": "Vista previa del borrado", + "Expand details": "Expandir detalles", + "Save changes": "Guardar cambios", + "📄 Object Serialization": "📄 Serialización de objetos", + "🔢 Vectorization Settings": "🔢 Ajustes de vectorización", + "💰 View Selection (Cost Optimization)": "💰 Selección de vistas (optimización de costes)", + "AI Assistant": "Asistente de IA", + "AI Chat": "Chat de IA", + "API Key": "Clave de API", + "API tokens saved successfully": "Tokens de API guardados correctamente", + "About ConfigSets": "Acerca de los ConfigSets", + "Action": "Acción", + "Action Distribution": "Distribución de acciones", + "Action:": "Acción:", + "Actions": "Acciones", + "Active": "Activo", + "Active Collections": "Colecciones activas", + "Active Filters": "Filtros activos", + "Active Organisations": "Organizaciones activas", + "Active filters:": "Filtros activos:", + "Active organisation set successfully": "Organización activa establecida correctamente", + "Activity Period": "Periodo de actividad", + "Add Groups": "Añadir grupos", + "Add Schema": "Añadir esquema", + "Add schema titles, descriptions, and register information to provide richer context for search": "Añada títulos de esquema, descripciones e información de registro para proporcionar un contexto más rico en las búsquedas", + "Add to favorites": "Añadir a favoritos", + "Added": "Añadido", + "Added to favorites": "Añadido a favoritos", + "Additional Information": "Información adicional", + "Additional feedback saved. Thank you!": "Comentarios adicionales guardados. ¡Gracias!", + "Admin": "Administrador", + "Advanced": "Avanzado", + "Advanced Filters": "Filtros avanzados", + "Advanced Options": "Opciones avanzadas", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Los filtros avanzados no están disponibles al usar la fuente de base de datos. Cambie a Auto o Índice SOLR para acceder a las opciones de filtrado.", + "After": "Después", + "Agent deleted successfully": "Agente eliminado correctamente", + "Agents": "Agentes", + "All": "Todos", + "All Categories": "Todas las categorías", + "All Collections": "Todas las colecciones", + "All Confidence Levels": "Todos los niveles de confianza", + "All ConfigSets": "Todos los ConfigSets", + "All Files": "Todos los archivos", + "All Levels": "Todos los niveles", + "All Types": "Todos los tipos", + "All Webhooks": "Todos los webhooks", + "All actions": "Todas las acciones", + "All audit trails cleared successfully": "Todos los registros de auditoría se borraron correctamente", + "All registers": "Todos los registros", + "All schemas": "Todos los esquemas", + "All searches": "Todas las búsquedas", + "All users": "Todos los usuarios", + "An error occurred while clearing audit trails": "Se produjo un error al borrar los registros de auditoría", + "An error occurred while deleting the audit trail": "Se produjo un error al eliminar el registro de auditoría", + "An error occurred while deleting the view": "Se produjo un error al eliminar la vista", + "An error occurred while permanently deleting the objects": "Se produjo un error al eliminar permanentemente los objetos", + "Analysis completed:": "Análisis completado:", + "Analysis steps:": "Pasos del análisis:", + "Analytics": "Analíticas", + "Analyze Objects": "Analizar objetos", + "Analyze Properties": "Analizar propiedades", + "Analyze existing properties for improvement opportunities": "Analizar las propiedades existentes en busca de oportunidades de mejora", + "Analyzing...": "Analizando...", + "Any favorites and sharing settings for this view": "Cualquier ajuste de favoritos y de uso compartido de esta vista", + "Any user": "Cualquier usuario", + "App store cache invalidated successfully": "Caché de la tienda de aplicaciones invalidada correctamente", + "Application deleted successfully": "Aplicación eliminada correctamente", + "Applications": "Aplicaciones", + "Apply Changes": "Aplicar cambios", + "Archive": "Archivar", + "Archive conversation": "Archivar conversación", + "Archived conversations are hidden from your active list": "Las conversaciones archivadas se ocultan de su lista activa", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "¿Está seguro de que desea limpiar los registros de búsqueda antiguos? Esto eliminará las entradas con más de 30 días.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "¿Está seguro de que desea eliminar los registros de auditoría seleccionados? Esta acción no se puede deshacer.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "¿Está seguro de que desea eliminar los registros de búsqueda seleccionados? Esta acción no se puede deshacer.", + "Are you sure you want to delete this ConfigSet?": "¿Está seguro de que desea eliminar este ConfigSet?", + "Are you sure you want to permanently delete": "¿Está seguro de que desea eliminar permanentemente", + "Ask a question...": "Haga una pregunta...", + "Ask questions about your data using natural language": "Haga preguntas sobre sus datos usando lenguaje natural", + "Attempt": "Intento", + "Audit Trail Changes": "Cambios del registro de auditoría", + "Audit Trail Details": "Detalles del registro de auditoría", + "Audit Trail Management": "Gestión del registro de auditoría", + "Audit Trail Statistics": "Estadísticas del registro de auditoría", + "Audit Trails": "Registros de auditoría", + "Audit trail data copied to clipboard": "Datos del registro de auditoría copiados al portapapeles", + "Audit trail deleted successfully": "Registro de auditoría eliminado correctamente", + "Audit trail successfully deleted": "Registro de auditoría eliminado correctamente", + "Authentication": "Autenticación", + "Auto-commit disabled": "Confirmación automática desactivada", + "Auto-commit enabled": "Confirmación automática activada", + "Auto-create Default Organisation": "Crear automáticamente la organización predeterminada", + "Auto-retry failed vectorizations": "Reintentar automáticamente las vectorizaciones fallidas", + "Automatically commit changes to SOLR index": "Confirmar automáticamente los cambios en el índice SOLR", + "Automatically create a default organisation if none exists when the app is initialized": "Crear automáticamente una organización predeterminada si no existe ninguna cuando se inicializa la aplicación", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Generar automáticamente embeddings vectoriales a partir de fragmentos de texto cuando se cargan y procesan los archivos", + "Automatically generate vector embeddings when objects are created or updated": "Generar automáticamente embeddings vectoriales cuando se crean o actualizan objetos", + "Automatically retry failed vectorization attempts (max 3 retries)": "Reintentar automáticamente los intentos de vectorización fallidos (máximo 3 reintentos)", + "Available Filters": "Filtros disponibles", + "Available Workflows": "Flujos de trabajo disponibles", + "Avg Execution Time": "Tiempo medio de ejecución", + "Avg Members/Org": "Media de miembros/org.", + "Avg Object Views/Session": "Media de vistas de objetos/sesión", + "Avg Response Time": "Tiempo medio de respuesta", + "Avg Results/Search": "Media de resultados/búsqueda", + "Avg Searches/Session": "Media de búsquedas/sesión", + "Back": "Atrás", + "Back to Entities": "Volver a las entidades", + "Back to Registers": "Volver a los registros", + "Back to Webhooks": "Volver a los webhooks", + "Back to applications": "Volver a las aplicaciones", + "Back to entities": "Volver a las entidades", + "Backend updated successfully. Please reload the application.": "Backend actualizado correctamente. Vuelva a cargar la aplicación.", + "Base URL (Optional)": "URL base (opcional)", + "Basic Information": "Información básica", + "Batch Size": "Tamaño del lote", + "Batch extraction completed": "Extracción por lotes completada", + "Before": "Antes", + "Before object vectorization can work:": "Antes de que funcione la vectorización de objetos:", + "Behavior Issue": "Problema de comportamiento", + "Blob storage has been retired. All objects now use magic tables.": "El almacenamiento de blobs se ha retirado. Todos los objetos usan ahora tablas mágicas.", + "Branch": "Rama", + "Bulk delete operation completed successfully": "Operación de eliminación masiva completada correctamente", + "Bulk save operation completed successfully": "Operación de guardado masivo completada correctamente", + "Business Data": "Datos comerciales", + "Cache cleared successfully": "Caché borrada correctamente", + "Calculate Sizes": "Calcular tamaños", + "Cancel": "Cancelar", + "Cannot delete: objects are still attached": "No se puede eliminar: todavía hay objetos adjuntos", + "Category": "Categoría", + "Change Type": "Tipo de cambio", + "Changes": "Cambios", + "Changes copied to clipboard": "Cambios copiados al portapapeles", + "Chat Model": "Modelo de chat", + "Chat Provider": "Proveedor de chat", + "Chat Provider (RAG)": "Proveedor de chat (RAG)", + "Chat Settings": "Ajustes del chat", + "Chat provider connection successful!": "¡Conexión con el proveedor de chat exitosa!", + "Chat settings": "Ajustes del chat", + "Choose a register": "Elija un registro", + "Choose a schema": "Elija un esquema", + "Choose how vector similarity calculations are performed for semantic search": "Elija cómo se realizan los cálculos de similitud vectorial para la búsqueda semántica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Elija qué tipos de archivo incluir en el proceso de vectorización. Solo se procesarán los archivos con texto extraído y fragmentos.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Elija qué vistas incluir en el proceso de vectorización. Déjelo vacío para procesar todas las vistas según su configuración.", + "Chunk": "Fragmento", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "La eliminación de fragmentos aún no está implementada. Use endpoints basados en fragmentos.", + "Chunks": "Fragmentos", + "Chunks to Vectorize:": "Fragmentos a vectorizar:", + "Cleanup Old Trails": "Limpiar registros antiguos", + "Cleanup completed": "Limpieza completada", + "Clear All Embeddings": "Borrar todos los embeddings", + "Clear Entries": "Borrar entradas", + "Clear Filtered Audit Trails": "Borrar los registros de auditoría filtrados", + "Clear Filters": "Borrar filtros", + "Clear Index": "Borrar índice", + "Clear Selection": "Borrar selección", + "Clear all": "Borrar todo", + "Clear all filters": "Borrar todos los filtros", + "Clear date range": "Borrar el intervalo de fechas", + "Clearing...": "Borrando...", + "Close": "Cerrar", + "Collection Management": "Gestión de colecciones", + "Collection Name": "Nombre de la colección", + "Collection assignments updated successfully": "Asignaciones de colección actualizadas correctamente", + "Collection cleared successfully": "Colección borrada correctamente", + "Collection copied successfully": "Colección copiada correctamente", + "Collection created successfully": "Colección creada correctamente", + "Collection deleted successfully": "Colección eliminada correctamente", + "Collection used to store and index file metadata and content": "Colección utilizada para almacenar e indexar los metadatos y el contenido de los archivos", + "Collection used to store and index object data": "Colección utilizada para almacenar e indexar los datos de los objetos", + "Collections:": "Colecciones:", + "Commit Message": "Mensaje de confirmación", + "Commit Within (ms)": "Confirmar dentro de (ms)", + "Commit message": "Mensaje de confirmación", + "Compare current schema with real object data": "Comparar el esquema actual con los datos reales de los objetos", + "Completed": "Completado", + "Complex": "Complejo", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Consultas complejas: búsquedas avanzadas con múltiples filtros, operadores y combinaciones complejas de parámetros", + "Confidence Level": "Nivel de confianza", + "Confidence Score": "Puntuación de confianza", + "Config must be provided as an object": "La configuración debe proporcionarse como un objeto", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Gestión de ConfigSets", + "ConfigSet Name": "Nombre del ConfigSet", + "ConfigSet created successfully": "ConfigSet creado correctamente", + "ConfigSet deleted successfully": "ConfigSet eliminado correctamente", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Los ConfigSets definen el esquema y la configuración de sus colecciones SOLR. Contienen definiciones de campos, analizadores y otros ajustes de búsqueda.", + "Configuration": "Configuración", + "Configuration Data": "Datos de configuración", + "Configuration saved": "Configuración guardada", + "Configurations": "Configuraciones", + "Configure Facets": "Configurar facetas", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Configure los proveedores de modelos de lenguaje grandes (LLM) para las funciones impulsadas por IA, incluida la búsqueda semántica, los embeddings y el chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Configure los ajustes básicos de conexión de su servidor SOLR, incluidas las opciones de autenticación y red. Use los diálogos separados de gestión de ConfigSets y colecciones para administrar los cores y las colecciones.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Configure cómo se convierten los objetos de la base de datos en embeddings vectoriales para la búsqueda semántica. Los objetos se vectorizan directamente sin necesidad de extracción de texto.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Configure cómo se convierten los objetos a texto antes de la vectorización. Estos ajustes afectan a la calidad y el contexto de la búsqueda.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Configure los parámetros para la vectorización de archivos. Este proceso generará embeddings vectoriales para todos los fragmentos de archivo extraídos.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Configure los parámetros para la vectorización de objetos. Este proceso generará embeddings vectoriales para todos los objetos que coincidan con los filtros de su vista.", + "Configure which types of data to search and how many sources to retrieve": "Configure qué tipos de datos buscar y cuántas fuentes recuperar", + "Connection Failed": "Conexión fallida", + "Connection Settings": "Ajustes de conexión", + "Connection Successful!": "¡Conexión exitosa!", + "Connection failed": "Conexión fallida", + "Connection protocol": "Protocolo de conexión", + "Connection successful": "Conexión exitosa", + "Connection timeout in seconds": "Tiempo de espera de conexión en segundos", + "Constant delay between retries (5 minutes)": "Retardo constante entre reintentos (5 minutos)", + "Constraint Issue": "Problema de restricción", + "Content Filters": "Filtros de contenido", + "Continue to Properties": "Continuar a las propiedades", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Controle qué vistas de objetos deben vectorizarse para reducir los costes de API. Vectorice solo las vistas que se benefician de la búsqueda semántica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Controle qué vistas y herramientas puede usar la IA en esta conversación. De forma predeterminada, todas las capacidades del agente están habilitadas.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Controla la aleatoriedad (0 = determinista, 2 = muy creativo)", + "Conversation ID is required": "Se requiere el ID de la conversación", + "Conversation archived": "Conversación archivada", + "Conversation archived successfully": "Conversación archivada correctamente", + "Conversation cleared successfully": "Conversación borrada correctamente", + "Conversation deleted": "Conversación eliminada", + "Conversation permanently deleted": "Conversación eliminada permanentemente", + "Conversation renamed": "Conversación renombrada", + "Conversation restored": "Conversación restaurada", + "Conversation title": "Título de la conversación", + "Copied!": "¡Copiado!", + "Copy": "Copiar", + "Copy Changes": "Copiar cambios", + "Copy Collection": "Copiar colección", + "Copy Data": "Copiar datos", + "Copy Full Data": "Copiar todos los datos", + "Copying...": "Copiando...", + "Count": "Recuento", + "Counting objects...": "Contando objetos...", + "Costs": "Costes", + "Create": "Crear", + "Create Collection": "Crear colección", + "Create ConfigSet": "Crear ConfigSet", + "Create New Collection": "Crear nueva colección", + "Create New ConfigSet": "Crear nuevo ConfigSet", + "Create Operations": "Crear operaciones", + "Create Webhook": "Crear webhook", + "Create a copy of collection:": "Crear una copia de la colección:", + "Create a new ConfigSet based on the _default template": "Crear un nuevo ConfigSet basado en la plantilla _default", + "Create a new SOLR collection from an existing ConfigSet": "Crear una nueva colección SOLR a partir de un ConfigSet existente", + "Create your first AI agent to get started.": "Cree su primer agente de IA para empezar.", + "Created": "Creado", + "Created:": "Creado:", + "Creating...": "Creando...", + "Current Filters": "Filtros actuales", + "Current Type:": "Tipo actual:", + "Custom API endpoint if using a different region": "Endpoint de API personalizado si se usa una región diferente", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Cabeceras HTTP personalizadas (una por línea, formato: Nombre-Cabecera: valor)", + "Daily": "Diariamente", + "Dashboard": "Panel de control", + "Data Source": "Fuente de datos", + "Data sources": "Fuentes de datos", + "Data type does not match observed values": "El tipo de dato no coincide con los valores observados", + "Database URL": "URL de la base de datos", + "Database information refreshed": "Información de la base de datos actualizada", + "Date Range": "Intervalo de fechas", + "Default": "Predeterminado", + "Default Organisation": "Organización predeterminada", + "Default: 5": "Predeterminado: 5", + "Delays double with each attempt (2, 4, 8 minutes...)": "Los retardos se duplican con cada intento (2, 4, 8 minutos...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Los retardos aumentan de forma lineal (5, 10, 15 minutos...)", + "Delete": "Eliminar", + "Delete Application": "Eliminar aplicación", + "Delete Audit Trail": "Eliminar registro de auditoría", + "Delete Collection": "Eliminar colección", + "Delete ConfigSet": "Eliminar ConfigSet", + "Delete Objects": "Eliminar objetos", + "Delete Operations": "Eliminar operaciones", + "Delete View": "Eliminar vista", + "Delete all objects in this schema": "Eliminar todos los objetos de este esquema", + "Delete permanently": "Eliminar permanentemente", + "Delete this conversation?": "¿Eliminar esta conversación?", + "Delete view": "Eliminar vista", + "Deleted": "Eliminado", + "Deleted By": "Eliminado por", + "Deleted Date": "Fecha de eliminación", + "Deleted Items Management": "Gestión de elementos eliminados", + "Deleted This Week": "Eliminados esta semana", + "Deleted Today": "Eliminados hoy", + "Deleted:": "Eliminado:", + "Deleting...": "Eliminando...", + "Deletion Date Range": "Intervalo de fechas de eliminación", + "Deletion Statistics": "Estadísticas de eliminación", + "Deprecated": "Obsoleto", + "Description": "Descripción", + "Description:": "Descripción:", + "Detect data types and patterns": "Detectar tipos de datos y patrones", + "Detected At": "Detectado el", + "Detected Format:": "Formato detectado:", + "Detected Issues:": "Problemas detectados:", + "Disable": "Desactivar", + "Disabled": "Desactivado", + "Discover Files": "Descubrir archivos", + "Discovered Properties": "Propiedades descubiertas", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "¿Desea eliminar permanentemente todas las entradas de registro de auditoría filtradas? Esta acción no se puede deshacer.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "¿Desea eliminar permanentemente esta entrada de registro de auditoría? Esta acción no se puede deshacer.", + "Documents": "Documentos", + "Download API Spec": "Descargar especificación de la API", + "Edit": "Editar", + "Edit Register": "Editar registro", + "Edit Schema": "Editar esquema", + "Edit View": "Editar vista", + "Edit Webhook": "Editar webhook", + "Edit view": "Editar vista", + "Edit view details": "Editar detalles de la vista", + "Effectiveness": "Eficacia", + "Email": "Correo electrónico", + "Emails": "Correos electrónicos", + "Embedding Model": "Modelo de embeddings", + "Embedding Provider": "Proveedor de embeddings", + "Embedding generated successfully": "Embedding generado correctamente", + "Embedding provider connection successful!": "¡Conexión con el proveedor de embeddings exitosa!", + "Enable": "Activar", + "Enable auto-creation": "Activar creación automática", + "Enable automatic file vectorization": "Activar la vectorización automática de archivos", + "Enable automatic object vectorization": "Activar la vectorización automática de objetos", + "Enable automatic synchronization": "Activar la sincronización automática", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Activar el registro detallado de las operaciones SOLR (recomendado para depuración)", + "Enable faceting": "Activar facetado", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Active o desactive las funciones de LLM. Configure los proveedores y modelos usando el botón de configuración de LLM de arriba.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Active o desactive la integración de búsqueda SOLR. Configure los ajustes de conexión usando el botón de ajustes de conexión de arriba.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Active o desactive la integración de flujos de trabajo de n8n. Configure los ajustes de conexión a continuación.", + "Enable or disable this webhook": "Active o desactive este webhook", + "Enable vectorization for all existing and future views (may increase costs)": "Activar la vectorización para todas las vistas existentes y futuras (puede aumentar los costes)", + "Enabled": "Activado", + "Endpoints": "Endpoints", + "Enter ConfigSet name": "Introduzca el nombre del ConfigSet", + "Enter collection name": "Introduzca el nombre de la colección", + "Enter description (optional)...": "Introduzca una descripción (opcional)...", + "Enter new collection name": "Introduzca el nombre de la nueva colección", + "Enter object ID": "Introduzca el ID del objeto", + "Enter search term": "Introduzca el término de búsqueda", + "Enter view name...": "Introduzca el nombre de la vista...", + "Enter webhook name": "Introduzca el nombre del webhook", + "Entities": "Entidades", + "Entity Information": "Información de la entidad", + "Entity deleted successfully": "Entidad eliminada correctamente", + "Entity not found": "Entidad no encontrada", + "Entries to be deleted:": "Entradas que se eliminarán:", + "Enum Issue": "Problema de enumeración", + "Enum constraint is missing": "Falta la restricción de enumeración", + "Error": "Error", + "Error Details": "Detalles del error", + "Error Information": "Información del error", + "Error Message": "Mensaje de error", + "Error loading application": "Error al cargar la aplicación", + "Error loading audit trails": "Error al cargar los registros de auditoría", + "Error loading entity": "Error al cargar la entidad", + "Error loading search trails": "Error al cargar los registros de búsqueda", + "Estimated Batches:": "Lotes estimados:", + "Estimated Cost:": "Coste estimado:", + "Estimated Duration:": "Duración estimada:", + "Event": "Evento", + "Event Property for Payload": "Propiedad del evento para la carga útil", + "Events": "Eventos", + "Example Value": "Valor de ejemplo", + "Exclusive Maximum": "Máximo exclusivo", + "Exclusive Minimum": "Mínimo exclusivo", + "Execution Mode": "Modo de ejecución", + "Execution Time": "Tiempo de ejecución", + "Execution Time Range": "Intervalo de tiempo de ejecución", + "Existing Improvements": "Mejoras existentes", + "Exponential": "Exponencial", + "Export": "Exportar", + "Export completed successfully": "Exportación completada correctamente", + "Export, view, or delete audit trails": "Exportar, ver o eliminar registros de auditoría", + "Extend Schema": "Extender esquema", + "Extended": "Extendido", + "Extensions": "Extensiones", + "Extract Pending Files": "Extraer archivos pendientes", + "Extract properties from each object": "Extraer propiedades de cada objeto", + "Extracted At": "Extraído el", + "Extraction Status": "Estado de la extracción", + "Facet configuration updated successfully": "Configuración de facetas actualizada correctamente", + "Facets discovered and configured successfully": "Facetas descubiertas y configuradas correctamente", + "Facets discovered successfully": "Facetas descubiertas correctamente", + "Failed": "Fallido", + "Failed to analyze schema properties": "No se pudieron analizar las propiedades del esquema", + "Failed to archive conversation": "No se pudo archivar la conversación", + "Failed to calculate sizes": "No se pudieron calcular los tamaños", + "Failed to clear collection": "No se pudo borrar la colección", + "Failed to copy changes": "No se pudieron copiar los cambios", + "Failed to copy data": "No se pudieron copiar los datos", + "Failed to copy data to clipboard": "No se pudieron copiar los datos al portapapeles", + "Failed to create conversation": "No se pudo crear la conversación", + "Failed to create or find project": "No se pudo crear o encontrar el proyecto", + "Failed to delete collection": "No se pudo eliminar la colección", + "Failed to delete conversation": "No se pudo eliminar la conversación", + "Failed to delete webhook": "No se pudo eliminar el webhook", + "Failed to download API specification": "No se pudo descargar la especificación de la API", + "Failed to get SOLR field configuration": "No se pudo obtener la configuración de campos de SOLR", + "Failed to get entity categories": "No se pudieron obtener las categorías de entidad", + "Failed to get entity statistics": "No se pudieron obtener las estadísticas de entidad", + "Failed to get entity types": "No se pudieron obtener los tipos de entidad", + "Failed to load LLM configuration": "No se pudo cargar la configuración de LLM", + "Failed to load Nextcloud groups": "No se pudieron cargar los grupos de Nextcloud", + "Failed to load conversation": "No se pudo cargar la conversación", + "Failed to load entities": "No se pudieron cargar las entidades", + "Failed to load entity": "No se pudo cargar la entidad", + "Failed to load files": "No se pudieron cargar los archivos", + "Failed to load organisations": "No se pudieron cargar las organizaciones", + "Failed to load templates": "No se pudieron cargar las plantillas", + "Failed to load webhooks": "No se pudieron cargar los webhooks", + "Failed to load workflows": "No se pudieron cargar los flujos de trabajo", + "Failed to refresh database information": "No se pudo actualizar la información de la base de datos", + "Failed to reindex collection": "No se pudo reindexar la colección", + "Failed to rename conversation": "No se pudo renombrar la conversación", + "Failed to restore conversation": "No se pudo restaurar la conversación", + "Failed to retry extraction": "No se pudo reintentar la extracción", + "Failed to retry webhook": "No se pudo reintentar el webhook", + "Failed to save GitHub token": "No se pudo guardar el token de GitHub", + "Failed to save GitLab URL": "No se pudo guardar la URL de GitLab", + "Failed to save GitLab token": "No se pudo guardar el token de GitLab", + "Failed to save additional feedback": "No se pudieron guardar los comentarios adicionales", + "Failed to save n8n configuration": "No se pudo guardar la configuración de n8n", + "Failed to save roles": "No se pudieron guardar los roles", + "Failed to save settings": "No se pudieron guardar los ajustes", + "Failed to save webhook": "No se pudo guardar el webhook", + "Failed to send feedback": "No se pudieron enviar los comentarios", + "Failed to test webhook": "No se pudo probar el webhook", + "Failed to update favorite status": "No se pudo actualizar el estado de favorito", + "Failed to update schema properties": "No se pudieron actualizar las propiedades del esquema", + "Failed to update view": "No se pudo actualizar la vista", + "Failed to update webhook": "No se pudo actualizar el webhook", + "Feedback recorded": "Comentarios registrados", + "Fewer sources (1-3):": "Menos fuentes (1-3):", + "Field": "Campo", + "File": "Archivo", + "File Chunk Prediction": "Predicción de fragmentos de archivo", + "File Collection": "Colección de archivos", + "File Management": "Gestión de archivos", + "File Name": "Nombre del archivo", + "File Path": "Ruta del archivo", + "File Type Selection": "Selección de tipo de archivo", + "File Vectorization": "Vectorización de archivos", + "File Warmup": "Calentamiento de archivos", + "File actions menu": "Menú de acciones de archivo", + "File anonymized successfully": "Archivo anonimizado correctamente", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "La vectorización de fragmentos de archivo aún no está implementada. Los fragmentos están listos y almacenados, pero el servicio de vectorización está en desarrollo.", + "File collection not configured": "Colección de archivos no configurada", + "File discovery completed": "Descubrimiento de archivos completado", + "File extraction completed": "Extracción de archivos completada", + "File extraction queued": "Extracción de archivos en cola", + "File indexed successfully": "Archivo indexado correctamente", + "File is already anonymized": "El archivo ya está anonimizado", + "File not found": "Archivo no encontrado", + "File settings updated successfully": "Ajustes del archivo actualizados correctamente", + "File sources": "Fuentes de archivos", + "File vectorization configuration saved successfully": "Configuración de vectorización de archivos guardada correctamente", + "File vectorization started. Check the statistics section for progress.": "Vectorización de archivos iniciada. Consulte la sección de estadísticas para ver el progreso.", + "File warmup completed": "Calentamiento de archivos completado", + "Files": "Archivos", + "Files with Completed Extraction:": "Archivos con extracción completada:", + "Files → fileCollection, Objects → objectCollection": "Archivos → fileCollection, Objetos → objectCollection", + "Filter Audit Trails": "Filtrar registros de auditoría", + "Filter Deleted Items": "Filtrar elementos eliminados", + "Filter Objects": "Filtrar objetos", + "Filter Properties": "Filtrar propiedades", + "Filter Search Trails": "Filtrar registros de búsqueda", + "Filter Statistics": "Estadísticas de filtros", + "Filter and analyze search trail entries": "Filtrar y analizar las entradas de registro de búsqueda", + "Filter and manage audit trail entries": "Filtrar y gestionar las entradas de registro de auditoría", + "Filter and manage soft deleted items": "Filtrar y gestionar los elementos eliminados de forma temporal", + "Filter by object ID": "Filtrar por ID de objeto", + "Filter by search term": "Filtrar por término de búsqueda", + "Filter by webhook": "Filtrar por webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Datos de filtro cargados automáticamente. Use los filtros de abajo para refinar su búsqueda.", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtre los disparadores de webhook por propiedades de la carga útil (una por línea, formato: clave: valor)", + "Filtered": "Filtrado", + "Filters": "Filtros", + "Fireworks AI Chat Settings": "Ajustes del chat de Fireworks AI", + "Fireworks AI Embedding Configuration": "Configuración de embeddings de Fireworks AI", + "First": "Primero", + "Fixed": "Fijo", + "For chat and retrieval-augmented generation": "Para chat y generación aumentada por recuperación", + "For vector embeddings and semantic search": "Para embeddings vectoriales y búsqueda semántica", + "Format": "Formato", + "Format Issue": "Problema de formato", + "Format constraint is missing": "Falta la restricción de formato", + "From Date": "Fecha de inicio", + "From date": "Fecha de inicio", + "Full data copied to clipboard": "Todos los datos copiados al portapapeles", + "General Issue": "Problema general", + "Generate recommendations and confidence scores": "Generar recomendaciones y puntuaciones de confianza", + "Generate vectors immediately when new objects are created": "Generar vectores inmediatamente cuando se crean nuevos objetos", + "GitHub token is valid": "El token de GitHub es válido", + "GitHub token removed successfully": "Token de GitHub eliminado correctamente", + "GitHub token saved successfully": "Token de GitHub guardado correctamente", + "GitLab URL saved successfully": "URL de GitLab guardada correctamente", + "GitLab token is valid": "El token de GitLab es válido", + "GitLab token saved successfully": "Token de GitLab guardado correctamente", + "HTTP Method": "Método HTTP", + "HTTP method used to send webhook requests": "Método HTTP utilizado para enviar las solicitudes de webhook", + "Headers": "Cabeceras", + "Health": "Estado", + "Heartbeat successful - connection kept alive": "Latido exitoso: conexión mantenida activa", + "Helpful": "Útil", + "Hide Filters": "Ocultar filtros", + "Hide in forms": "Ocultar en los formularios", + "Hide in list view": "Ocultar en la vista de lista", + "High": "Alto", + "High Confidence": "Confianza alta", + "Host": "Host", + "Hourly": "Cada hora", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Cuánto profundizar al recorrer las propiedades anidadas de los objetos (1-20). Los valores más altos capturan más detalle pero aumentan el tamaño del vector.", + "How often to check for updates (1-168 hours)": "Con qué frecuencia comprobar si hay actualizaciones (1-168 horas)", + "How to handle retries for failed webhook deliveries": "Cómo gestionar los reintentos de las entregas de webhook fallidas", + "ID": "ID", + "ID:": "ID:", + "IP rate limits cleared successfully": "Límites de frecuencia por IP borrados correctamente", + "Identify properties not in the schema": "Identificar propiedades que no están en el esquema", + "Immutable": "Inmutable", + "Import": "Importar", + "Import successful": "Importación exitosa", + "Improved Property": "Propiedad mejorada", + "In use": "En uso", + "Inactive": "Inactivo", + "Include IDs and names of related objects for better contextual search": "Incluir los ID y nombres de los objetos relacionados para una mejor búsqueda contextual", + "Include related object references": "Incluir referencias a objetos relacionados", + "Include schema and register metadata": "Incluir los metadatos del esquema y del registro", + "Initialization failed": "La inicialización falló", + "Initialize Project": "Inicializar proyecto", + "Inspect Fields": "Inspeccionar campos", + "Inspect Index": "Inspeccionar índice", + "Invalid": "No válido", + "Invalid batch size. Must be between 1 and 5000": "Tamaño de lote no válido. Debe estar entre 1 y 5000", + "Invalid field name provided": "Nombre de campo proporcionado no válido", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects no válido. Debe ser 0 (todos) o un número positivo", + "Invalid:": "No válido:", + "Issue": "Problema", + "Items per page:": "Elementos por página:", + "Just now": "Ahora mismo", + "LLM Configuration": "Configuración de LLM", + "LLM actions menu": "Menú de acciones de LLM", + "LLM configuration saved successfully": "Configuración de LLM guardada correctamente", + "LLM features disabled": "Funciones de LLM desactivadas", + "LLM features enabled": "Funciones de LLM activadas", + "LLM must be enabled with an embedding provider configured": "El LLM debe estar activado con un proveedor de embeddings configurado", + "LLM settings updated successfully": "Ajustes de LLM actualizados correctamente", + "Last": "Último", + "Last Triggered": "Último disparo", + "Length Range:": "Intervalo de longitud:", + "Linear": "Lineal", + "Load Advanced Filters": "Cargar filtros avanzados", + "Load advanced filters with live data from your search index": "Cargar filtros avanzados con datos en vivo de su índice de búsqueda", + "Load view": "Cargar vista", + "Loading ConfigSets...": "Cargando ConfigSets...", + "Loading advanced filters...": "Cargando filtros avanzados...", + "Loading agents...": "Cargando agentes...", + "Loading application details...": "Cargando detalles de la aplicación...", + "Loading applications...": "Cargando aplicaciones...", + "Loading archived conversations...": "Cargando conversaciones archivadas...", + "Loading audit trails...": "Cargando registros de auditoría...", + "Loading collections...": "Cargando colecciones...", + "Loading configurations...": "Cargando configuraciones...", + "Loading conversation...": "Cargando conversación...", + "Loading conversations...": "Cargando conversaciones...", + "Loading deleted items...": "Cargando elementos eliminados...", + "Loading events...": "Cargando eventos...", + "Loading filter data...": "Cargando datos de filtro...", + "Loading groups...": "Cargando grupos...", + "Loading log details...": "Cargando detalles del registro...", + "Loading registers...": "Cargando registros...", + "Loading schemas...": "Cargando esquemas...", + "Loading search trails...": "Cargando registros de búsqueda...", + "Loading sources...": "Cargando fuentes...", + "Loading statistics...": "Cargando estadísticas...", + "Loading users...": "Cargando usuarios...", + "Loading views...": "Cargando vistas...", + "Loading...": "Cargando...", + "Local": "Local", + "Local Version": "Versión local", + "Locked": "Bloqueado", + "Locked:": "Bloqueado:", + "Login successful": "Inicio de sesión exitoso", + "Logs": "Registros", + "Low": "Bajo", + "Low Confidence": "Confianza baja", + "Lucene Version:": "Versión de Lucene:", + "Magic table synchronized successfully": "Tabla mágica sincronizada correctamente", + "Manage Organisation Roles": "Gestionar roles de la organización", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Gestione las colecciones SOLR (almacenes de datos) y asígnelas para objetos y archivos.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Gestione los ConfigSets de SOLR (plantillas de configuración) para sus colecciones.", + "Manage and configure agents for automated tasks": "Gestione y configure agentes para tareas automatizadas", + "Manage and monitor file text extraction status": "Gestione y supervise el estado de la extracción de texto de los archivos", + "Manage and restore soft deleted items from your registers": "Gestione y restaure los elementos eliminados de forma temporal de sus registros", + "Manage and view detected entities from files and objects": "Gestione y vea las entidades detectadas en archivos y objetos", + "Manage document templates and themes": "Gestione plantillas y temas de documentos", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Gestione los flujos de trabajo de n8n para la automatización de OpenRegister. Los flujos de trabajo se almacenarán en el proyecto configurado.", + "Manage webhooks for event-driven integrations": "Gestione webhooks para integraciones basadas en eventos", + "Manage your applications and modules": "Gestione sus aplicaciones y módulos", + "Manage your data registers and their configurations": "Gestione sus registros de datos y sus configuraciones", + "Manage your data schemas and their properties": "Gestione sus esquemas de datos y sus propiedades", + "Manage your data sources and their configurations": "Gestione sus fuentes de datos y sus configuraciones", + "Manage your saved search configurations": "Gestione sus configuraciones de búsqueda guardadas", + "Manage your system configurations and settings": "Gestione sus configuraciones y ajustes del sistema", + "Managed": "Gestionado", + "Max Files (0 = all)": "Máximo de archivos (0 = todos)", + "Max Length": "Longitud máxima", + "Max Length:": "Longitud máxima:", + "Max Objects (0 = all)": "Máximo de objetos (0 = todos)", + "Max Retries": "Máximo de reintentos", + "Max Shards/Node": "Máximo de fragmentos/nodo", + "Max execution time (ms)": "Tiempo máximo de ejecución (ms)", + "Max ms": "ms máximo", + "Max result count": "Recuento máximo de resultados", + "Max results": "Máximo de resultados", + "Maximum": "Máximo", + "Maximum Nesting Depth": "Profundidad máxima de anidamiento", + "Maximum length constraint is missing": "Falta la restricción de longitud máxima", + "Maximum length is too restrictive": "La longitud máxima es demasiado restrictiva", + "Maximum number of files to process. Set to 0 to process all files.": "Número máximo de archivos a procesar. Establézcalo en 0 para procesar todos los archivos.", + "Maximum number of objects to process. Set to 0 to process all objects.": "Número máximo de objetos a procesar. Establézcalo en 0 para procesar todos los objetos.", + "Maximum number of retry attempts for failed deliveries": "Número máximo de intentos de reintento para las entregas fallidas", + "Maximum time to wait before committing changes": "Tiempo máximo de espera antes de confirmar los cambios", + "Maximum tokens to generate": "Número máximo de tokens a generar", + "Maximum value constraint is missing": "Falta la restricción de valor máximo", + "Maximum value is too restrictive": "El valor máximo es demasiado restrictivo", + "Medium": "Medio", + "Medium Confidence": "Confianza media", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Consultas medias: búsquedas con algún filtrado o varios parámetros (p. ej., intervalos de fechas, registros/esquemas específicos)", + "Members of selected groups can access this view": "Los miembros de los grupos seleccionados pueden acceder a esta vista", + "Members:": "Miembros:", + "Memory prediction calculated successfully": "Predicción de memoria calculada correctamente", + "Message content is required": "El contenido del mensaje es obligatorio", + "Message does not belong to this conversation": "El mensaje no pertenece a esta conversación", + "Metadata Filters": "Filtros de metadatos", + "Method": "Método", + "Min Length": "Longitud mínima", + "Min execution time (ms)": "Tiempo mínimo de ejecución (ms)", + "Min ms": "ms mínimo", + "Min result count": "Recuento mínimo de resultados", + "Min results": "Mínimo de resultados", + "Minimum": "Mínimo", + "Minimum value constraint is missing": "Falta la restricción de valor mínimo", + "Minimum value is too restrictive": "El valor mínimo es demasiado restrictivo", + "Missing object fields created successfully": "Campos de objeto faltantes creados correctamente", + "Mode:": "Modo:", + "Model": "Modelo", + "Model Name": "Nombre del modelo", + "Modified": "Modificado", + "Monthly": "Mensualmente", + "More sources (10-20):": "Más fuentes (10-20):", + "Most Active Objects": "Objetos más activos", + "Multiple Of": "Múltiplo de", + "Name": "Nombre", + "Names cache warmup completed": "Calentamiento de la caché de nombres completado", + "New Collection Name": "Nombre de la nueva colección", + "New Conversation": "Nueva conversación", + "New Properties": "Nuevas propiedades", + "New Property": "Nueva propiedad", + "New Value": "Nuevo valor", + "New users without specific organisation membership will be automatically added to this organisation": "Los nuevos usuarios sin pertenencia a una organización específica se añadirán automáticamente a esta organización", + "Next": "Siguiente", + "No": "No", + "No ConfigSets found": "No se encontraron ConfigSets", + "No GitHub token configured": "No hay ningún token de GitHub configurado", + "No GitHub token provided": "No se proporcionó ningún token de GitHub", + "No GitLab token provided": "No se proporcionó ningún token de GitLab", + "No Organisation": "Sin organización", + "No active filters": "No hay filtros activos", + "No activity data available": "No hay datos de actividad disponibles", + "No agents available": "No hay agentes disponibles", + "No agents found": "No se encontraron agentes", + "No app store cache exists yet": "Aún no existe ninguna caché de la tienda de aplicaciones", + "No applications are available.": "No hay aplicaciones disponibles.", + "No applications found": "No se encontraron aplicaciones", + "No archived conversations": "No hay conversaciones archivadas", + "No audit trail entries found": "No se encontraron entradas de registro de auditoría", + "No changes recorded": "No se registraron cambios", + "No chunks to vectorize": "No hay fragmentos que vectorizar", + "No collections found": "No se encontraron colecciones", + "No configuration data": "No hay datos de configuración", + "No configurations are available.": "No hay configuraciones disponibles.", + "No configurations found": "No se encontraron configuraciones", + "No configurations found for this application.": "No se encontraron configuraciones para esta aplicación.", + "No conversations yet. Create a new one to get started!": "Aún no hay conversaciones. ¡Cree una nueva para empezar!", + "No data available": "No hay datos disponibles", + "No data available for chart": "No hay datos disponibles para el gráfico", + "No deleted items found": "No se encontraron elementos eliminados", + "No deletion data available": "No hay datos de eliminación disponibles", + "No description found": "No se encontró ninguna descripción", + "No description provided": "No se proporcionó ninguna descripción", + "No entities detected in this file. Run text extraction first.": "No se detectaron entidades en este archivo. Ejecute primero la extracción de texto.", + "No entities found": "No se encontraron entidades", + "No entities have been detected yet": "Aún no se ha detectado ninguna entidad", + "No events found": "No se encontraron eventos", + "No expired audit trails found to clear": "No se encontraron registros de auditoría caducados que borrar", + "No facetable fields available. Select a register and schema to see available filters.": "No hay campos facetables disponibles. Seleccione un registro y un esquema para ver los filtros disponibles.", + "No files found": "No se encontraron archivos", + "No files have been extracted yet": "Aún no se ha extraído ningún archivo", + "No files to process": "No hay archivos que procesar", + "No files to reindex": "No hay archivos que reindexar", + "No filters are currently active. This will delete ALL audit trail entries!": "Actualmente no hay filtros activos. ¡Esto eliminará TODAS las entradas de registro de auditoría!", + "No log entries found": "No se encontraron entradas de registro", + "No logs are available for this configuration.": "No hay registros disponibles para esta configuración.", + "No logs are available for this source.": "No hay registros disponibles para esta fuente.", + "No logs found": "No se encontraron registros", + "No mismatched fields found - SOLR schema is properly configured": "No se encontraron campos discrepantes: el esquema SOLR está configurado correctamente", + "No objects found": "No se encontraron objetos", + "No objects found to vectorize": "No se encontraron objetos que vectorizar", + "No objects selected for deletion": "No se seleccionaron objetos para eliminar", + "No objects to delete": "No hay objetos que eliminar", + "No objects were permanently deleted": "No se eliminó permanentemente ningún objeto", + "No properties found": "No se encontraron propiedades", + "No properties match your filters.": "Ninguna propiedad coincide con sus filtros.", + "No purge date set": "No se ha establecido ninguna fecha de purga", + "No register data available": "No hay datos de registro disponibles", + "No registers found": "No se encontraron registros", + "No registers found for this application.": "No se encontraron registros para esta aplicación.", + "No relations found": "No se encontraron relaciones", + "No request body available": "No hay cuerpo de solicitud disponible", + "No response body available": "No hay cuerpo de respuesta disponible", + "No saved views yet. Create one in the Search tab!": "Aún no hay vistas guardadas. ¡Cree una en la pestaña de búsqueda!", + "No schema data available": "No hay datos de esquema disponibles", + "No schema selected for exploration": "No se ha seleccionado ningún esquema para explorar", + "No schemas found": "No se encontraron esquemas", + "No schemas found for this application.": "No se encontraron esquemas para esta aplicación.", + "No search terms data available": "No hay datos de términos de búsqueda disponibles", + "No search trail entries found": "No se encontraron entradas de registro de búsqueda", + "No sources are available.": "No hay fuentes disponibles.", + "No sources found": "No se encontraron fuentes", + "No templates found": "No se encontraron plantillas", + "No templates have been created yet": "Aún no se ha creado ninguna plantilla", + "No valid magic-mapped register+schema combinations found": "No se encontraron combinaciones válidas de registro+esquema con mapeo mágico", + "No views found. Create views first before configuring vectorization.": "No se encontraron vistas. Cree vistas antes de configurar la vectorización.", + "No views match your search": "Ninguna vista coincide con su búsqueda", + "No webhooks found": "No se encontraron webhooks", + "No webhooks have been configured yet": "Aún no se ha configurado ningún webhook", + "No workflows found in this project. Create workflows in the n8n editor.": "No se encontraron flujos de trabajo en este proyecto. Cree flujos de trabajo en el editor de n8n.", + "None": "Ninguno", + "Not helpful": "No es útil", + "Not in use": "No está en uso", + "Number Constraints": "Restricciones numéricas", + "Number of chunks to vectorize in one API call (1-100).": "Número de fragmentos a vectorizar en una sola llamada a la API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Número de fragmentos a vectorizar en una sola llamada a la API. Más alto = más rápido pero más memoria. Recomendado: 10-50.", + "Number of objects to process in each batch (1-5000).": "Número de objetos a procesar en cada lote (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Número de objetos a vectorizar en una sola llamada a la API. Más alto = más rápido pero más memoria. Recomendado: 10-50.", + "Numeric Range:": "Intervalo numérico:", + "Object": "Objeto", + "Object Collection": "Colección de objetos", + "Object Count Prediction": "Predicción del recuento de objetos", + "Object ID": "ID del objeto", + "Object Management": "Gestión de objetos", + "Object Vectorization": "Vectorización de objetos", + "Object Warmup": "Calentamiento de objetos", + "Object collection not configured": "Colección de objetos no configurada", + "Object not found": "Objeto no encontrado", + "Object permanently deleted": "Objeto eliminado permanentemente", + "Object restored successfully": "Objeto restaurado correctamente", + "Object settings updated successfully": "Ajustes del objeto actualizados correctamente", + "Object sources": "Fuentes de objetos", + "Object unlocked successfully": "Objeto desbloqueado correctamente", + "Object vectorization configuration saved successfully": "Configuración de vectorización de objetos guardada correctamente", + "Object vectorized successfully": "Objeto vectorizado correctamente", + "Object:": "Objeto:", + "Objects": "Objetos", + "Objects Analyzed": "Objetos analizados", + "Objects Distribution": "Distribución de objetos", + "Objects being analyzed": "Objetos en análisis", + "Objects by Register": "Objetos por registro", + "Objects by Schema": "Objetos por esquema", + "Objects deletion completed successfully": "Eliminación de objetos completada correctamente", + "Objects in schema": "Objetos en el esquema", + "Objects to Process:": "Objetos a procesar:", + "Objects to be analyzed": "Objetos que se analizarán", + "Objects to be deleted": "Objetos que se eliminarán", + "Objects to be validated": "Objetos que se validarán", + "Objects will be serialized as JSON text before vectorization": "Los objetos se serializarán como texto JSON antes de la vectorización", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Los objetos se eliminarán de forma temporal (marcados como eliminados pero conservados en la base de datos). Se pueden recuperar más adelante si es necesario.", + "Old Value": "Valor anterior", + "Oldest Item (days)": "Elemento más antiguo (días)", + "Ollama Chat Settings": "Ajustes del chat de Ollama", + "Ollama Embedding Configuration": "Configuración de embeddings de Ollama", + "Ollama URL": "URL de Ollama", + "Only With Changes": "Solo con cambios", + "Open Register Objects": "Objetos de Open Register", + "Open n8n Editor": "Abrir el editor de n8n", + "OpenAI Chat Settings": "Ajustes del chat de OpenAI", + "OpenAI Embedding Configuration": "Configuración de embeddings de OpenAI", + "OpenRegister Settings": "Ajustes de OpenRegister", + "Optional webhook secret for signature verification": "Secreto de webhook opcional para la verificación de firma", + "Order": "Orden", + "Organisation": "Organización", + "Organisation ID": "ID de la organización", + "Organisation Members": "Miembros de la organización", + "Organisation Statistics": "Estadísticas de la organización", + "Organisation created successfully": "Organización creada correctamente", + "Organisation settings saved successfully": "Ajustes de la organización guardados correctamente", + "Organisations": "Organizaciones", + "Organization": "Organización", + "Organization ID (Optional)": "ID de la organización (opcional)", + "Orphaned Items": "Elementos huérfanos", + "Owner": "Propietario", + "Owner:": "Propietario:", + "Parallel Mode (Faster, more resource intensive)": "Modo paralelo (más rápido, mayor consumo de recursos)", + "Parallel:": "Paralelo:", + "Password": "Contraseña", + "Password for SOLR authentication (optional)": "Contraseña para la autenticación de SOLR (opcional)", + "Path": "Ruta", + "Path in repository": "Ruta en el repositorio", + "Path where the configuration file will be saved in the repository": "Ruta donde se guardará el archivo de configuración en el repositorio", + "Path where the register OAS file will be saved in the repository": "Ruta donde se guardará el archivo OAS del registro en el repositorio", + "Pattern (regex)": "Patrón (regex)", + "Pattern Issue": "Problema de patrón", + "Pattern constraint is missing": "Falta la restricción de patrón", + "Patterns:": "Patrones:", + "Pending": "Pendiente", + "Permanent Delete Mode": "Modo de eliminación permanente", + "Permanently Delete": "Eliminar permanentemente", + "Permanently delete already soft-deleted objects": "Eliminar permanentemente los objetos ya eliminados de forma temporal", + "Permanently delete this conversation?": "¿Eliminar permanentemente esta conversación?", + "Person": "Persona", + "Personal Data": "Datos personales", + "Phone": "Teléfono", + "Photos": "Fotos", + "Please create an agent in the": "Cree un agente en el", + "Please select an agent to continue": "Seleccione un agente para continuar", + "Please select which register and schema to use for the new object": "Seleccione qué registro y esquema usar para el nuevo objeto", + "Please try again later.": "Vuelva a intentarlo más tarde.", + "Please wait while we fetch your agents.": "Espere mientras recuperamos sus agentes.", + "Please wait while we fetch your applications.": "Espere mientras recuperamos sus aplicaciones.", + "Please wait while we fetch your configurations.": "Espere mientras recuperamos sus configuraciones.", + "Please wait while we fetch your deleted items.": "Espere mientras recuperamos sus elementos eliminados.", + "Please wait while we fetch your sources.": "Espere mientras recuperamos sus fuentes.", + "Polls": "Encuestas", + "Popular Search Terms": "Términos de búsqueda populares", + "Port": "Puerto", + "Prerequisites": "Requisitos previos", + "Press Enter to send, Shift+Enter for new line": "Pulse Intro para enviar, Mayús+Intro para una nueva línea", + "Previous": "Anterior", + "Private": "Privado", + "Processes chunks in batches with simulated parallelism.": "Procesa los fragmentos en lotes con paralelismo simulado.", + "Processes file chunks sequentially (safest).": "Procesa los fragmentos de archivo secuencialmente (lo más seguro).", + "Processes objects in chunks with simulated parallelism.": "Procesa los objetos en fragmentos con paralelismo simulado.", + "Processes objects sequentially (safest).": "Procesa los objetos secuencialmente (lo más seguro).", + "Processing": "Procesando", + "Processing Limits": "Límites de procesamiento", + "Processing...": "Procesando...", + "Project initialized successfully": "Proyecto inicializado correctamente", + "Project not found. Please initialize first.": "Proyecto no encontrado. Inicialícelo primero.", + "Properties": "Propiedades", + "Property Behaviors": "Comportamientos de la propiedad", + "Property Configuration:": "Configuración de la propiedad:", + "Property Filters": "Filtros de propiedades", + "Property Title": "Título de la propiedad", + "Property Type": "Tipo de propiedad", + "Property can be improved": "La propiedad se puede mejorar", + "Protected": "Protegido", + "Provider": "Proveedor", + "Provider is required for testing": "Se requiere un proveedor para realizar la prueba", + "Public": "Público", + "Public View": "Vista pública", + "Public views can be accessed by anyone in the system": "Cualquier persona en el sistema puede acceder a las vistas públicas", + "Publish": "Publicar", + "Publish Objects": "Publicar objetos", + "Publish all objects in this schema": "Publicar todos los objetos de este esquema", + "Published": "Publicado", + "Published:": "Publicado:", + "Publishing...": "Publicando...", + "Purge": "Purgar", + "Purge Date": "Fecha de purga", + "Query Complexity Distribution": "Distribución de la complejidad de las consultas", + "Query parameter is required": "El parámetro de consulta es obligatorio", + "RAG": "RAG", + "RAG Configuration": "Configuración de RAG", + "Rate limits cleared successfully": "Límites de frecuencia borrados correctamente", + "Raw Changes Data": "Datos de cambios sin procesar", + "Re-vectorize on object update": "Volver a vectorizar al actualizar el objeto", + "Read": "Leer", + "Recommendations:": "Recomendaciones:", + "Recommended Type:": "Tipo recomendado:", + "Recommended: 5 sources": "Recomendado: 5 fuentes", + "Refresh": "Actualizar", + "Refresh Data": "Actualizar datos", + "Refresh Stats": "Actualizar estadísticas", + "Refresh Workflows": "Actualizar flujos de trabajo", + "Refresh dashboard": "Actualizar el panel de control", + "Refresh database info": "Actualizar la información de la base de datos", + "Register": "Registro", + "Register ID": "ID del registro", + "Register Statistics": "Estadísticas del registro", + "Register Totals": "Totales del registro", + "Register depublished successfully": "Registro despublicado correctamente", + "Register not found": "Registro no encontrado", + "Register objects deletion completed successfully": "Eliminación de objetos del registro completada correctamente", + "Register published successfully": "Registro publicado correctamente", + "Register/Schema Usage": "Uso de registros/esquemas", + "Registers": "Registros", + "Reindex": "Reindexar", + "Reindex completed": "Reindexación completada", + "Reindex completed successfully": "Reindexación completada correctamente", + "Reindex failed": "La reindexación falló", + "Relations": "Relaciones", + "Remote Version": "Versión remota", + "Remove": "Eliminar", + "Remove filter": "Eliminar filtro", + "Remove from favorites": "Eliminar de favoritos", + "Remove group": "Eliminar grupo", + "Removed": "Eliminado", + "Removed from favorites": "Eliminado de favoritos", + "Rename Conversation": "Renombrar conversación", + "Rename conversation": "Renombrar conversación", + "Replicas": "Réplicas", + "Repository": "Repositorio", + "Request Body": "Cuerpo de la solicitud", + "Request Data": "Datos de la solicitud", + "Request Details": "Detalles de la solicitud", + "Request body must be JSON with a \"query\" field": "El cuerpo de la solicitud debe ser JSON con un campo \"query\"", + "Request timeout in seconds": "Tiempo de espera de la solicitud en segundos", + "Required field": "Campo obligatorio", + "Required status is inconsistent": "El estado de obligatoriedad es inconsistente", + "Rerun Search": "Volver a ejecutar la búsqueda", + "Reset Changes": "Restablecer cambios", + "Reset Filters": "Restablecer filtros", + "Response Body": "Cuerpo de la respuesta", + "Response Details": "Detalles de la respuesta", + "Response Time:": "Tiempo de respuesta:", + "Restore": "Restaurar", + "Restore conversation": "Restaurar conversación", + "Restore or permanently delete items": "Restaurar o eliminar permanentemente elementos", + "Result Count Range": "Intervalo de recuento de resultados", + "Results": "Resultados", + "Retrieve all objects for this schema": "Recuperar todos los objetos de este esquema", + "Retry": "Reintentar", + "Retry Failed Extractions": "Reintentar las extracciones fallidas", + "Retry Policy": "Política de reintentos", + "Retry completed": "Reintento completado", + "Retrying...": "Reintentando...", + "Risk Level": "Nivel de riesgo", + "Roles updated successfully": "Roles actualizados correctamente", + "SOLR Connection Settings": "Ajustes de conexión de SOLR", + "SOLR Version:": "Versión de SOLR:", + "SOLR actions menu": "Menú de acciones de SOLR", + "SOLR base path (usually /solr)": "Ruta base de SOLR (normalmente /solr)", + "SOLR is disabled": "SOLR está desactivado", + "SOLR is not available or not configured": "SOLR no está disponible o no está configurado", + "SOLR logging disabled": "Registro de SOLR desactivado", + "SOLR logging enabled": "Registro de SOLR activado", + "SOLR search disabled": "Búsqueda de SOLR desactivada", + "SOLR search enabled": "Búsqueda de SOLR activada", + "SOLR server hostname or IP address": "Nombre de host o dirección IP del servidor SOLR", + "SOLR server port number (optional, defaults to 8983)": "Número de puerto del servidor SOLR (opcional, predeterminado 8983)", + "SOLR setup completed successfully": "Configuración de SOLR completada correctamente", + "SOLR setup failed": "La configuración de SOLR falló", + "SOLR setup failed - check logs": "La configuración de SOLR falló: consulte los registros", + "Sample Values:": "Valores de muestra:", + "Save": "Guardar", + "Save Configuration": "Guardar configuración", + "Save Connection Settings": "Guardar ajustes de conexión", + "Save Roles": "Guardar roles", + "Save Settings": "Guardar ajustes", + "Save current search as view": "Guardar la búsqueda actual como vista", + "Saved Views": "Vistas guardadas", + "Saving...": "Guardando...", + "Schema": "Esquema", + "Schema ID": "ID del esquema", + "Schema Name": "Nombre del esquema", + "Schema Statistics": "Estadísticas del esquema", + "Schema depublished successfully": "Esquema despublicado correctamente", + "Schema objects deletion completed successfully": "Eliminación de objetos del esquema completada correctamente", + "Schema published successfully": "Esquema publicado correctamente", + "Schema successfully created": "Esquema creado correctamente", + "Schema successfully updated": "Esquema actualizado correctamente", + "Schemas": "Esquemas", + "Scheme": "Esquema", + "Search / Views": "Búsqueda / Vistas", + "Search Activity": "Actividad de búsqueda", + "Search Analytics": "Analíticas de búsqueda", + "Search Entities": "Buscar entidades", + "Search Files": "Buscar archivos", + "Search GitHub": "Buscar en GitHub", + "Search GitLab": "Buscar en GitLab", + "Search Method": "Método de búsqueda", + "Search Term": "Término de búsqueda", + "Search Trail Details": "Detalles del registro de búsqueda", + "Search Trail Management": "Gestión del registro de búsqueda", + "Search Trail Statistics": "Estadísticas del registro de búsqueda", + "Search Trails": "Registros de búsqueda", + "Search Views": "Buscar vistas", + "Search Webhooks": "Buscar webhooks", + "Search by file name or path": "Buscar por nombre de archivo o ruta", + "Search by name or URL": "Buscar por nombre o URL", + "Search by value": "Buscar por valor", + "Search in Files": "Buscar en archivos", + "Search in Objects": "Buscar en objetos", + "Search property names...": "Buscar nombres de propiedades...", + "Search trail deletion not implemented yet": "La eliminación de registros de búsqueda aún no está implementada", + "Search views...": "Buscar vistas...", + "Secret": "Secreto", + "Secret key for HMAC signature generation (optional)": "Clave secreta para la generación de firma HMAC (opcional)", + "Select AI Agent": "Seleccionar agente de IA", + "Select All": "Seleccionar todo", + "Select ConfigSet": "Seleccionar ConfigSet", + "Select File Types to Vectorize:": "Seleccione los tipos de archivo a vectorizar:", + "Select HTTP method": "Seleccionar método HTTP", + "Select Register and Schema": "Seleccionar registro y esquema", + "Select Views to Vectorize:": "Seleccione las vistas a vectorizar:", + "Select a Nextcloud group to add": "Seleccione un grupo de Nextcloud para añadir", + "Select a branch": "Seleccione una rama", + "Select a model or type a custom model name": "Seleccione un modelo o escriba un nombre de modelo personalizado", + "Select a repository": "Seleccione un repositorio", + "Select a repository you have write access to": "Seleccione un repositorio en el que tenga acceso de escritura", + "Select an AI Agent": "Seleccione un agente de IA", + "Select an AI agent to begin chatting with your data.": "Seleccione un agente de IA para empezar a chatear con sus datos.", + "Select backend": "Seleccionar backend", + "Select chat model": "Seleccionar modelo de chat", + "Select collection for files": "Seleccionar colección para archivos", + "Select collection for objects": "Seleccionar colección para objetos", + "Select default organisation": "Seleccionar organización predeterminada", + "Select event to listen to...": "Seleccione el evento que se debe escuchar...", + "Select model": "Seleccionar modelo", + "Select options...": "Seleccionar opciones...", + "Select period": "Seleccionar periodo", + "Select property to send as payload": "Seleccione la propiedad a enviar como carga útil", + "Select provider": "Seleccionar proveedor", + "Select registers and schemas to save a view": "Seleccione registros y esquemas para guardar una vista", + "Select retry policy": "Seleccionar política de reintentos", + "Select the branch to publish to": "Seleccione la rama en la que publicar", + "Select the event this webhook should listen to": "Seleccione el evento que este webhook debe escuchar", + "Select views to vectorize:": "Seleccione las vistas a vectorizar:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Seleccione qué grupos de Nextcloud están disponibles para esta organización. Los usuarios de estos grupos tendrán acceso a los recursos de la organización.", + "Select which data views the AI can search": "Seleccione en qué vistas de datos puede buscar la IA", + "Select which property from the event should be used as the webhook payload data": "Seleccione qué propiedad del evento se debe usar como datos de la carga útil del webhook", + "Select which tools the AI can use to perform actions": "Seleccione qué herramientas puede usar la IA para realizar acciones", + "Selected": "Seleccionado", + "Selected Groups": "Grupos seleccionados", + "Selected audit trails deleted successfully": "Registros de auditoría seleccionados eliminados correctamente", + "Selected search trails deleted successfully": "Registros de búsqueda seleccionados eliminados correctamente", + "Selected users can access this view": "Los usuarios seleccionados pueden acceder a esta vista", + "Send additional feedback": "Enviar comentarios adicionales", + "Send as CloudEvent": "Enviar como CloudEvent", + "Sensitive PII": "PII sensible", + "Serial Mode (Safer, slower)": "Modo en serie (más seguro, más lento)", + "Serial:": "En serie:", + "Server Configuration": "Configuración del servidor", + "Server Information": "Información del servidor", + "Settings": "Ajustes", + "Shards": "Fragmentos", + "Share": "Compartir", + "Share with Groups": "Compartir con grupos", + "Share with Users": "Compartir con usuarios", + "Show Filters": "Mostrar filtros", + "Show less": "Mostrar menos", + "Show only entries with changes": "Mostrar solo las entradas con cambios", + "Simple": "Simple", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Consultas simples: búsquedas de texto básicas con parámetros mínimos (p. ej., un solo término de búsqueda, sin filtros avanzados)", + "Size": "Tamaño", + "Slug": "Slug", + "Soft Delete Mode": "Modo de eliminación temporal", + "Soft Deleted Items": "Elementos eliminados de forma temporal", + "SolrCloud mode enabled": "Modo SolrCloud activado", + "Source": "Fuente", + "Sources": "Fuentes", + "Sources:": "Fuentes:", + "Standalone SOLR mode": "Modo SOLR independiente", + "Start Conversation": "Iniciar conversación", + "Start Vectorization": "Iniciar vectorización", + "Start a conversation": "Iniciar una conversación", + "Starting names cache warmup...": "Iniciando el calentamiento de la caché de nombres...", + "Starting...": "Iniciando...", + "Statistics": "Estadísticas", + "Stats calculation not yet implemented": "El cálculo de estadísticas aún no está implementado", + "Status": "Estado", + "Status Code": "Código de estado", + "String Constraints": "Restricciones de cadena", + "Success": "Éxito", + "Success Rate": "Tasa de éxito", + "Success Status": "Estado de éxito", + "Successful": "Exitoso", + "Successfully joined organisation": "Se unió a la organización correctamente", + "Sync Table": "Sincronizar tabla", + "Synchronization Settings": "Ajustes de sincronización", + "System Default": "Predeterminado del sistema", + "Technical Description": "Descripción técnica", + "Technical description for developers and administrators": "Descripción técnica para desarrolladores y administradores", + "Temperature": "Temperatura", + "Templates": "Plantillas", + "Test": "Probar", + "Test Chat": "Probar chat", + "Test Connection": "Probar conexión", + "Test Embedding": "Probar embedding", + "Test endpoint executed successfully": "Endpoint de prueba ejecutado correctamente", + "Test webhook delivered successfully": "Webhook de prueba entregado correctamente", + "Test webhook delivery failed": "La entrega del webhook de prueba falló", + "Test webhook sent successfully": "Webhook de prueba enviado correctamente", + "Testing...": "Probando...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Los fragmentos de texto se generan durante la extracción de archivos y se almacenan en la base de datos. La vectorización lee estos archivos previamente fragmentados y los convierte en embeddings.", + "Text extracted successfully": "Texto extraído correctamente", + "Text extraction disabled": "Extracción de texto desactivada", + "The URL where webhook events will be sent": "La URL a la que se enviarán los eventos del webhook", + "The requested conversation does not exist": "La conversación solicitada no existe", + "The saved view and all its search configuration": "La vista guardada y toda su configuración de búsqueda", + "There are no audit trail entries matching your current filters.": "No hay entradas de registro de auditoría que coincidan con sus filtros actuales.", + "There are no deleted items matching your current filters.": "No hay elementos eliminados que coincidan con sus filtros actuales.", + "There are no search trail entries matching your current filters.": "No hay entradas de registro de búsqueda que coincidan con sus filtros actuales.", + "There are no webhook log entries matching your filters.": "No hay entradas de registro de webhook que coincidan con sus filtros.", + "This action cannot be undone.": "Esta acción no se puede deshacer.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Esta acción no se puede deshacer. Asegúrese de que ninguna colección esté usando este ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Este agente no tiene vistas ni herramientas configurables, pero aún puede ajustar la configuración de RAG a continuación.", + "This analysis may take some time": "Este análisis puede tardar un tiempo", + "This audit trail entry does not contain any change information.": "Esta entrada de registro de auditoría no contiene ninguna información de cambios.", + "This configuration has no data defined.": "Esta configuración no tiene datos definidos.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Este endpoint está obsoleto. Use endpoints basados en fragmentos en su lugar.", + "This entity has no relations to objects or files": "Esta entidad no tiene relaciones con objetos o archivos", + "This is a test webhook from OpenRegister": "Este es un webhook de prueba de OpenRegister", + "This schema must use Magic Table configuration to sync": "Este esquema debe usar la configuración de tabla mágica para sincronizarse", + "This source has no associated registers.": "Esta fuente no tiene registros asociados.", + "This will copy the _default ConfigSet with the new name": "Esto copiará el ConfigSet _default con el nuevo nombre", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Esto eliminará permanentemente TODOS los embeddings (vectores) de la base de datos. Tendrá que volver a vectorizar todos los objetos y archivos. Esta acción no se puede deshacer.\\n\\n¿Está seguro de que desea continuar?", + "This will permanently delete:": "Esto eliminará permanentemente:", + "Timeout (seconds)": "Tiempo de espera (segundos)", + "Timestamp": "Marca de tiempo", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Consejo: active solo las vistas que necesitan búsqueda semántica para minimizar los costes de embeddings. Las tablas de consulta simples rara vez necesitan vectorización.", + "Title": "Título", + "To Date": "Fecha de fin", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Para ajustar el tamaño o la estrategia de los fragmentos, vaya a Configuración de archivos → Límites de procesamiento.", + "To change the embedding provider or model, go to LLM Configuration.": "Para cambiar el proveedor o el modelo de embeddings, vaya a Configuración de LLM.", + "To date": "Fecha de fin", + "Toggle search sidebar": "Alternar la barra lateral de búsqueda", + "Tools": "Herramientas", + "Top Deleters": "Principales eliminadores", + "Total": "Total", + "Total Audit Trails": "Total de registros de auditoría", + "Total Chunks Available:": "Total de fragmentos disponibles:", + "Total Deleted Items": "Total de elementos eliminados", + "Total Members": "Total de miembros", + "Total Objects": "Total de objetos", + "Total Objects in Database:": "Total de objetos en la base de datos:", + "Total Organisations": "Total de organizaciones", + "Total Results": "Total de resultados", + "Total Searches": "Total de búsquedas", + "Total Size": "Tamaño total", + "Total Storage": "Almacenamiento total", + "Total:": "Total:", + "Totals": "Totales", + "Type": "Tipo", + "Type Issue": "Problema de tipo", + "Type Variations:": "Variaciones de tipo:", + "Type must be \"positive\" or \"negative\"": "El tipo debe ser \"positive\" o \"negative\"", + "Type to search for groups": "Escriba para buscar grupos", + "Type to search for users": "Escriba para buscar usuarios", + "Type to search groups...": "Escriba para buscar grupos...", + "Type to search users...": "Escriba para buscar usuarios...", + "URL": "URL", + "URL where Ollama is running": "URL donde se ejecuta Ollama", + "UUID:": "UUID:", + "Unchanged": "Sin cambios", + "Unique Search Terms": "Términos de búsqueda únicos", + "Unique Terms": "Términos únicos", + "Unknown": "Desconocido", + "Unknown Register": "Registro desconocido", + "Unknown Schema": "Esquema desconocido", + "Untitled View": "Vista sin título", + "Update": "Actualizar", + "Update Operations": "Operaciones de actualización", + "Update configuration: ...": "Actualizar configuración: ...", + "Update register OAS: ...": "Actualizar OAS del registro: ...", + "Update vectors when object data changes (recommended for accurate search)": "Actualizar los vectores cuando cambien los datos del objeto (recomendado para una búsqueda precisa)", + "Updated": "Actualizado", + "Updated At": "Actualizado el", + "Updated:": "Actualizado:", + "Uptime:": "Tiempo de actividad:", + "Usage Count": "Número de usos", + "Use SolrCloud with Zookeeper for distributed search": "Usar SolrCloud con Zookeeper para la búsqueda distribuida", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Use filtros para acotar las entradas del registro de auditoría por registro, esquema, tipo de acción, usuario, intervalo de fechas o ID de objeto.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Use filtros para acotar los elementos eliminados por registro, esquema, fecha de eliminación o usuario que los eliminó.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Use filtros para acotar las entradas del registro de búsquedas por registro, esquema, estado de éxito, usuario, intervalo de fechas, términos de búsqueda o métricas de rendimiento.", + "Used By Collections": "Usado por colecciones", + "User": "Usuario", + "User Agent Statistics": "Estadísticas de agente de usuario", + "User Description": "Descripción del usuario", + "User rate limits cleared successfully": "Límites de tasa del usuario borrados correctamente", + "User removed successfully": "Usuario eliminado correctamente", + "User-friendly description shown in forms and help text": "Descripción fácil de entender que se muestra en los formularios y en el texto de ayuda", + "Username": "Nombre de usuario", + "Username for SOLR authentication (optional)": "Nombre de usuario para la autenticación de SOLR (opcional)", + "Users": "Usuarios", + "Using Pre-Generated Chunks": "Usando fragmentos pregenerados", + "Validate": "Validar", + "Validate Objects": "Validar objetos", + "Validation completed successfully": "Validación completada correctamente", + "Validation failed": "La validación ha fallado", + "Value": "Valor", + "Vector Dimensions": "Dimensiones del vector", + "Vector Search Backend": "Backend de búsqueda vectorial", + "Vector field: _embedding_": "Campo vectorial: _embedding_", + "Vectorization Triggers": "Desencadenantes de vectorización", + "Vectorize All Files": "Vectorizar todos los archivos", + "Vectorize All Objects": "Vectorizar todos los objetos", + "Vectorize all file types": "Vectorizar todos los tipos de archivo", + "Vectorize all views": "Vectorizar todas las vistas", + "Vectorize on object creation": "Vectorizar al crear el objeto", + "Vectors will be stored in your existing object and file collections": "Los vectores se almacenarán en sus colecciones de objetos y archivos existentes", + "Version": "Versión", + "Very High": "Muy alto", + "View API Docs": "Ver documentación de la API", + "View Changes": "Ver cambios", + "View Details": "Ver detalles", + "View Error": "Ver error", + "View File": "Ver archivo", + "View Full Details": "Ver todos los detalles", + "View Logs": "Ver registros", + "View Name": "Nombre de la vista", + "View Object": "Ver objeto", + "View Parameters": "Ver parámetros", + "View Selection": "Ver selección", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Vea y analice los registros del rastro de búsquedas con funciones avanzadas de filtrado y análisis", + "View and analyze system audit trails with advanced filtering capabilities": "Vea y analice los rastros de auditoría del sistema con funciones avanzadas de filtrado", + "View deleted successfully": "Vista eliminada correctamente", + "View entity details and manage relations": "Ver los detalles de la entidad y gestionar las relaciones", + "View name": "Nombre de la vista", + "View name is required": "El nombre de la vista es obligatorio", + "View saved successfully!": "¡Vista guardada correctamente!", + "View search analytics and manage search logs": "Ver las analíticas de búsqueda y gestionar los registros de búsqueda", + "View successfully deleted": "Vista eliminada correctamente", + "View successfully updated": "Vista actualizada correctamente", + "View updated successfully!": "¡Vista actualizada correctamente!", + "View webhook delivery logs and filter by webhook": "Ver los registros de entrega de webhooks y filtrar por webhook", + "Views": "Vistas", + "Visible to users": "Visible para los usuarios", + "Wait for Response": "Esperar respuesta", + "Wait for webhook response before continuing (required for request/response flows)": "Esperar la respuesta del webhook antes de continuar (obligatorio para los flujos de solicitud/respuesta)", + "Warmup Names Cache": "Precalentar la caché de nombres", + "Warning": "Advertencia", + "Warning:": "Advertencia:", + "Webhook": "Webhook", + "Webhook Log Details": "Detalles del registro del webhook", + "Webhook Logs": "Registros del webhook", + "Webhook created successfully": "Webhook creado correctamente", + "Webhook deleted": "Webhook eliminado", + "Webhook retry delivered successfully": "Reintento del webhook entregado correctamente", + "Webhook retry delivery failed": "Falló la entrega del reintento del webhook", + "Webhook updated": "Webhook actualizado", + "Webhook updated successfully": "Webhook actualizado correctamente", + "Webhooks": "Webhooks", + "Weekly": "Semanalmente", + "Wrap webhook payload in CloudEvents format for better interoperability": "Envolver la carga útil del webhook en formato CloudEvents para una mejor interoperabilidad", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Sí", + "You": "Usted", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Puede crear nuevos ConfigSets basados en la plantilla _default, o subir personalizados directamente a su servidor SOLR.", + "You do not have access to this conversation": "No tiene acceso a esta conversación", + "You do not have permission to delete this conversation": "No tiene permiso para eliminar esta conversación", + "You do not have permission to modify this conversation": "No tiene permiso para modificar esta conversación", + "You do not have permission to restore this conversation": "No tiene permiso para restaurar esta conversación", + "You must be logged in to favorite views": "Debe haber iniciado sesión para marcar vistas como favoritas", + "You need an AI agent to start a conversation.": "Necesita un agente de IA para iniciar una conversación.", + "Your Fireworks AI API key. Get one at": "Su clave de API de Fireworks AI. Obtenga una en", + "Your OpenAI API key. Get one at": "Su clave de API de OpenAI. Obtenga una en", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Sus comentarios se han registrado. Opcionalmente, puede proporcionar detalles adicionales aquí...", + "Zookeeper Hosts": "Hosts de Zookeeper", + "Zookeeper Port": "Puerto de Zookeeper", + "Zookeeper Settings (SolrCloud)": "Configuración de Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Cadena de conexión de Zookeeper para SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Número de puerto de Zookeeper (opcional, predeterminado 2181)", + "chunk": "fragmento", + "chunks": "fragmentos", + "chunks processed": "fragmentos procesados", + "configuration(s)": "configuración(es)", + "e.g., lib/Settings/config.json": "p. ej., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "p. ej., lib/Settings/register.json", + "failed": "fallido", + "file": "archivo", + "files": "archivos", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook": "https://example.com/webhook", + "members": "miembros", + "menu or contact someone with permission to create agents.": "menú o póngase en contacto con alguien que tenga permiso para crear agentes.", + "messages": "mensajes", + "n8n URL and API key are required": "La URL y la clave de API de n8n son obligatorias", + "n8n configuration saved successfully": "Configuración de n8n guardada correctamente", + "n8n connection not configured": "Conexión de n8n no configurada", + "n8n connection successful": "Conexión de n8n correcta", + "n8n connection test successful": "Prueba de conexión de n8n correcta", + "n8n integration disabled": "Integración de n8n desactivada", + "n8n integration enabled": "Integración de n8n activada", + "n8n project initialized successfully": "Proyecto de n8n inicializado correctamente", + "n8n settings saved successfully": "Configuración de n8n guardada correctamente", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objetos", + "objects processed": "objetos procesados", + "or pick a range": "o elija un intervalo", + "org-...": "org-...", + "provides a good balance between speed and accuracy.": "ofrece un buen equilibrio entre velocidad y precisión.", + "register(s)": "registro(s)", + "register(s) selected": "registro(s) seleccionado(s)", + "required": "obligatorio", + "results": "resultados", + "schema(s)": "esquema(s)", + "schema(s) selected": "esquema(s) seleccionado(s)", + "sk-...": "sk-...", + "this application": "esta aplicación", + "to": "a", + "vectorized": "vectorizado", + "views selected": "vistas seleccionadas", + "ℹ️ Current Configuration": "ℹ️ Configuración actual", + "⚡ Batch Processing": "⚡ Procesamiento por lotes", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Contexto completo ✓ Menos probable que se omitan detalles ✗ Respuestas más lentas ✗ Puede incluir información menos relevante", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Respuestas más rápidas ✓ Respuestas más centradas ✗ Puede omitir información relevante", + "✨ AI Features": "✨ Funciones de IA", + "All search trails cleared successfully": "Todos los rastros de búsqueda borrados correctamente", + "Multiple search trail deletion not implemented yet": "La eliminación múltiple de rastros de búsqueda aún no está implementada", + "No expired search trails found to clear": "No se encontraron rastros de búsqueda caducados para borrar", + "Missing message": "Falta el mensaje", + "message content is required": "el contenido del mensaje es obligatorio", + "Missing conversation or agentUuid": "Falta la conversación o agentUuid", + "Access denied": "Acceso denegado", + "Conversation not found": "Conversación no encontrada", + "AI service not configured": "Servicio de IA no configurado", + "Failed to process message": "No se pudo procesar el mensaje", + "Missing conversationId": "Falta conversationId", + "conversationId is required": "conversationId es obligatorio", + "Failed to fetch conversation history": "No se pudo obtener el historial de la conversación", + "Failed to clear conversation": "No se pudo borrar la conversación", + "Invalid feedback type": "Tipo de comentario no válido", + "type must be \"positive\" or \"negative\"": "el tipo debe ser \"positive\" o \"negative\"", + "Message not found": "Mensaje no encontrado", + "Failed to save feedback": "No se pudieron guardar los comentarios", + "Failed to get chat statistics": "No se pudieron obtener las estadísticas del chat", + "Not Found": "No encontrado", + "Engine not found": "Motor no encontrado", + "Consumer not found": "Consumidor no encontrado", + "Backend parameter is required": "El parámetro de backend es obligatorio", + "Failed to get database information: %s": "No se pudo obtener la información de la base de datos: %s", + "SOLR setup error: %s": "Error de configuración de SOLR: %s", + "Reindex failed: %s": "La reindexación ha fallado: %s", + "Anonymized": "Anonimizado", + "Entities detected": "Entidades detectadas", + "Extract Now": "Extraer ahora", + "Extracted at": "Extraído el", + "Extraction": "Extracción", + "Failed to load extraction data": "No se pudieron cargar los datos de extracción", + "Failed to load register data": "No se pudieron cargar los datos del registro", + "Failed to retrieve extraction status.": "No se pudo recuperar el estado de la extracción.", + "Failed to retrieve objects for file.": "No se pudieron recuperar los objetos del archivo.", + "No extraction data available for this file": "No hay datos de extracción disponibles para este archivo", + "No register objects reference this file": "Ningún objeto del registro hace referencia a este archivo", + "Not extracted": "No extraído", + "Register Objects": "Objetos del registro", + "Risk level": "Nivel de riesgo", + "Text chunks": "Fragmentos de texto", + "Unknown error": "Error desconocido", + "Very high": "Muy alto", + "{title} in {register} / {schema}": "{title} en {register} / {schema}", + "Push Notifications": "Notificaciones push", + "Real-time push notification status via notify_push": "Estado de las notificaciones push en tiempo real a través de notify_push", + "Realtime push not available — the notify_push app is not installed": "Push en tiempo real no disponible: la aplicación notify_push no está instalada", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instale la aplicación notify_push desde la tienda de aplicaciones de Nextcloud para activar las actualizaciones en tiempo real.", + "Open Nextcloud App Store": "Abrir la tienda de aplicaciones de Nextcloud", + "notify_push is installed but not yet active": "notify_push está instalado pero aún no está activo", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push está instalado pero OpenRegister aún no ha confirmado un push correcto. Active guardando un objeto, o compruebe su configuración de notify_push.", + "notify_push configuration guide": "guía de configuración de notify_push", + "Realtime push active": "Push en tiempo real activo", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Las notificaciones push en tiempo real están activas. Los clientes conectados reciben actualizaciones instantáneas cuando se crean, actualizan o eliminan objetos.", + "(Dolphin required)": "(Se requiere Dolphin)", + "({count} selected)": "({count} seleccionados)", + ", {failed} failed": ", {failed} fallidos", + "0 = unlimited": "0 = ilimitado", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ADVERTENCIA DE ELIMINACIÓN PERMANENTE ⚠️\n\nEstá a punto de eliminar PERMANENTEMENTE TODOS los objetos del esquema \"{schema}\":\n\n• Objetos activos: {active}\n• Objetos eliminados temporalmente: {deleted}\n• Total: {total}\n\nEstos objetos se eliminarán por completo de la base de datos y NO se podrán recuperar.\n\n¿Está completamente seguro?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ADVERTENCIA DE ELIMINACIÓN PERMANENTE ⚠️\n\nEstá a punto de eliminar PERMANENTEMENTE {count} objetos eliminados temporalmente del esquema \"{schema}\".\n\nEstos objetos se eliminarán por completo de la base de datos y NO se podrán recuperar.\n\n¿Está completamente seguro?", + "A deactivation request is pending.": "Hay una solicitud de desactivación pendiente.", + "Access organisation dashboard and analytics": "Acceder al panel de control y las analíticas de la organización", + "Account": "Cuenta", + "Active organisation changed successfully": "Organización activa cambiada correctamente", + "Active Organisation:": "Organización activa:", + "Activity": "Actividad", + "Add": "Añadir", + "Add a file or files": "Añadir un archivo o archivos", + "Add Attachment": "Añadir adjunto", + "Add endpoint": "Añadir endpoint", + "Add oneOf entry": "Añadir entrada oneOf", + "Add Property to '{title}'": "Añadir propiedad a '{title}'", + "Add range": "Añadir intervalo", + "Add Register": "Añadir registro", + "Add User": "Añadir usuario", + "Add User to Organisation": "Añadir usuario a la organización", + "Addresses": "Direcciones", + "Agent successfully updated": "Agente actualizado correctamente", + "Agent Type": "Tipo de agente", + "Aggregated across schemas": "Agregado entre esquemas", + "All file types": "Todos los tipos de archivo", + "All-day events": "Eventos de todo el día", + "Allowed MIME Types": "Tipos MIME permitidos", + "Already linked": "Ya vinculado", + "An error occurred while deleting the object": "Se produjo un error al eliminar el objeto", + "An error occurred while restoring the objects": "Se produjo un error al restaurar los objetos", + "An error occurred while saving the organisation": "Se produjo un error al guardar la organización", + "and add the files there.": "y añada los archivos allí.", + "Anonymous Group": "Grupo anónimo", + "API Request Quota (requests/day)": "Cuota de solicitudes de API (solicitudes/día)", + "API Token Configuration": "Configuración de tokens de API", + "API Tokens": "Tokens de API", + "App ID": "ID de la aplicación", + "Application identifier for this configuration (optional)": "Identificador de la aplicación para esta configuración (opcional)", + "Application settings": "Configuración de la aplicación", + "Application successfully created": "Aplicación creada correctamente", + "Application successfully updated": "Aplicación actualizada correctamente", + "Apply": "Aplicar", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Aplicar un rol a todos los esquemas de este registro que no tengan anulaciones de autorización explícitas.", + "Approval Progress": "Progreso de la aprobación", + "Approve": "Aprobar", + "Approved": "Aprobado", + "approved": "aprobado", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "¿Está seguro de que desea borrar todos los datos de la colección \"{name}\"?\n\nEsto hará lo siguiente:\n• Eliminar todos los documentos indexados\n• Mantener intacta la estructura de la colección\n• Esta acción no se puede deshacer", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "¿Está seguro de que desea ELIMINAR la colección \"{name}\"?\n\nEsto hará lo siguiente:\n• Eliminar permanentemente la colección y todos sus datos\n• Eliminar todos los documentos indexados\n• Esta acción no se puede deshacer", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "¿Está seguro de que desea eliminar la aplicación \"{name}\"? Esta acción no se puede deshacer.", + "Are you sure you want to delete this configset?": "¿Está seguro de que desea eliminar este configset?", + "Are you sure you want to leave '{name}'?": "¿Está seguro de que desea abandonar '{name}'?", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "¿Está seguro de que desea reindexar la colección \"{name}\"?\n\nEsto hará lo siguiente:\n• Reconstruir el índice con todos los objetos\n• Tardar varios minutos en completarse\n• Puede afectar al rendimiento de la búsqueda durante la reindexación", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "¿Está seguro de que desea eliminar el esquema \"{schema}\" del registro \"{register}\"? Esta acción no se puede deshacer.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "¿Está seguro de que desea eliminar temporalmente {count} objetos activos del esquema \"{schema}\"?\n\nSe marcarán como eliminados, pero podrán eliminarse permanentemente más adelante.", + "Array Object Configuration:": "Configuración de objeto de array:", + "Assigned collaborative tags": "Etiquetas colaborativas asignadas", + "Assignment notifications": "Notificaciones de asignación", + "Auto (e.g. Y for year, F Y for month)": "Auto (p. ej. Y para año, F Y para mes)", + "Auto share": "Compartir automáticamente", + "Automatically connect to the federation network.": "Conectarse automáticamente a la red de federación.", + "Automatically update catalogues.": "Actualizar automáticamente los catálogos.", + "Available placeholders:": "Marcadores de posición disponibles:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Su proveedor de autenticación no admite cambios de avatar.", + "Avatar removed": "Avatar eliminado", + "Avatar updated successfully": "Avatar actualizado correctamente", + "Bandwidth Quota (MB/month)": "Cuota de ancho de banda (MB/mes)", + "Behavior": "Comportamiento", + "Bulk Role Assignment: {title}": "Asignación masiva de roles: {title}", + "Cache grew from {old} to {new} entries.": "La caché creció de {old} a {new} entradas.", + "Cache shrunk from {old} to {new} entries.": "La caché se redujo de {old} a {new} entradas.", + "Cache stayed the same at {size} entries.": "La caché se mantuvo igual en {size} entradas.", + "Calendar": "Calendario", + "Calendar name": "Nombre del calendario", + "Calendar Provider Configuration": "Configuración del proveedor de calendario", + "Cancel deactivation request": "Cancelar la solicitud de desactivación", + "Cannot edit: This register is managed by external configuration {title}": "No se puede editar: este registro lo gestiona la configuración externa {title}", + "Cannot remove schema with existing objects ({count} objects)": "No se puede eliminar un esquema con objetos existentes ({count} objetos)", + "Cannot remove schema {schema} because it contains {count} objects": "No se puede eliminar el esquema {schema} porque contiene {count} objetos", + "Cards": "Tarjetas", + "Cascade delete": "Eliminación en cascada", + "Change password": "Cambiar contraseña", + "Changes for Audit Trail #{id}": "Cambios para el rastro de auditoría n.º {id}", + "Chat test failed: {error}": "La prueba del chat ha fallado: {error}", + "Cleanup completed successfully. Deleted {count} entries.": "Limpieza completada correctamente. Se eliminaron {count} entradas.", + "Cleanup failed: {error}": "La limpieza ha fallado: {error}", + "Clear filters": "Borrar filtros", + "Cluster name": "Nombre del clúster", + "Collapse details": "Contraer detalles", + "Color": "Color", + "Comment...": "Comentario...", + "completed": "completado", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Consultas complejas: búsquedas avanzadas con múltiples filtros, operadores y combinaciones complejas de parámetros", + "Confidence: {confidence}%": "Confianza: {confidence}%", + "Configuration is up to date": "La configuración está actualizada", + "Configuration saved successfully.": "Configuración guardada correctamente.", + "Configuration type (default, application, etc.)": "Tipo de configuración (predeterminada, aplicación, etc.)", + "Configure Apache SOLR search engine for advanced search capabilities": "Configure el motor de búsqueda Apache SOLR para funciones de búsqueda avanzadas", + "Configure API tokens for external service integrations": "Configure tokens de API para integraciones con servicios externos", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configure los ajustes básicos de conexión de su servidor SOLR, incluidas las opciones de autenticación y de red. Use los diálogos separados de gestión de configsets y colecciones para gestionar cores y colecciones.", + "Configure default organisation and organisation-related settings": "Configure la organización predeterminada y los ajustes relacionados con la organización", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configure proveedores de modelos de lenguaje grandes (LLM) para funciones impulsadas por IA, como la búsqueda semántica, los embeddings y el chat.", + "Configure multi-organization support and tenant isolation": "Configure la compatibilidad con múltiples organizaciones y el aislamiento de inquilinos", + "Configure SOLR Facets": "Configurar facetas de SOLR", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configure este esquema para mostrar objetos como eventos en la aplicación Calendario de Nextcloud.", + "Confirm Account Deactivation": "Confirmar la desactivación de la cuenta", + "Confirm deactivation": "Confirmar desactivación", + "Confirm Object Deletion": "Confirmar la eliminación del objeto", + "Confirm Object Restoration": "Confirmar la restauración del objeto", + "Confirm Permanent Object Deletion": "Confirmar la eliminación permanente del objeto", + "Contracts": "Contratos", + "Conversation started with {agent}": "Conversación iniciada con {agent}", + "Conversations": "Conversaciones", + "Converting {schema} to blob storage...": "Convirtiendo {schema} a almacenamiento blob...", + "Converting {schema} to magic table...": "Convirtiendo {schema} a tabla mágica...", + "Copy of {name}": "Copia de {name}", + "Copy to clipboard": "Copiar al portapapeles", + "create": "crear", + "Create a new configset based on the _default template": "Crear un nuevo configset basado en la plantilla _default", + "Create a new SOLR collection from an existing configset": "Crear una nueva colección de SOLR a partir de un configset existente", + "Create another": "Crear otro", + "Create API Token": "Crear token de API", + "Create copies of": "Crear copias de", + "Create new token": "Crear nuevo token", + "Create Organisation": "Crear organización", + "Current": "Actual", + "Current password": "Contraseña actual", + "Custom display title for this facet": "Título de visualización personalizado para esta faceta", + "Custom HTTP headers (one per line, format: header-name: value)": "Cabeceras HTTP personalizadas (una por línea, formato: nombre-cabecera: valor)", + "Custom pattern:": "Patrón personalizado:", + "Custom Ranges:": "Intervalos personalizados:", + "Data": "Datos", + "Data Sources": "Fuentes de datos", + "Data Views": "Vistas de datos", + "Date Faceting:": "Facetado por fecha:", + "Dates": "Fechas", + "Deactivation request cancelled": "Solicitud de desactivación cancelada", + "Deactivation request submitted": "Solicitud de desactivación enviada", + "Default Object Tenant": "Inquilino de objeto predeterminado", + "Default Owner": "Propietario predeterminado", + "Default User (for cron/background jobs)": "Usuario predeterminado (para cron/tareas en segundo plano)", + "Default User Tenant": "Inquilino de usuario predeterminado", + "Default value": "Valor predeterminado", + "Default value (Email)": "Valor predeterminado (Correo electrónico)", + "Default value (Password)": "Valor predeterminado (Contraseña)", + "Default value (Phone number)": "Valor predeterminado (Número de teléfono)", + "Default value (Regex)": "Valor predeterminado (Regex)", + "Defaults to current user. Select a different user if needed.": "El valor predeterminado es el usuario actual. Seleccione un usuario diferente si es necesario.", + "delete": "eliminar", + "Delete ({count})": "Eliminar ({count})", + "DELETE COLLECTION": "ELIMINAR COLECCIÓN", + "Delete SOLR Collection": "Eliminar colección de SOLR", + "deleted objects section": "sección de objetos eliminados", + "Deleting SOLR Collection...": "Eliminando colección de SOLR...", + "Depublish": "Despublicar", + "Description shown as tooltip": "Descripción que se muestra como información sobre herramientas", + "Description Template": "Plantilla de descripción", + "Detected: {date}": "Detectado: {date}", + "Direct permission": "Permiso directo", + "Disable this to make the agent": "Desactive esto para que el agente sea", + "Display Format": "Formato de visualización", + "Display Name": "Nombre para mostrar", + "Dolphin OCR enabled": "OCR de Dolphin activado", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Descargue una copia de todos sus datos personales almacenados en OpenRegister (Artículo 20 del RGPD).", + "Download API Specification": "Descargar la especificación de la API", + "Drag a file or files here": "Arrastre un archivo o archivos aquí", + "Dry run completed: {count} fields would be created": "Ejecución de prueba completada: se crearían {count} campos", + "Dry run completed: {count} fields would be fixed": "Ejecución de prueba completada: se corregirían {count} campos", + "Duration in seconds (optional)": "Duración en segundos (opcional)", + "e.g., lib/settings/config.json": "p. ej., lib/settings/config.json", + "e.g., lib/settings/register.json": "p. ej., lib/settings/register.json", + "Edit Agent": "Editar agente", + "Edit Application": "Editar aplicación", + "Edit Configuration": "Editar configuración", + "Edit Endpoint": "Editar endpoint", + "Edit labels": "Editar etiquetas", + "Edit Organisation": "Editar organización", + "Edit Property '{key}' of '{title}'": "Editar la propiedad '{key}' de '{title}'", + "Edit Source": "Editar fuente", + "Edit tags for {name}": "Editar etiquetas para {name}", + "Email Addresses": "Direcciones de correo electrónico", + "Email digest frequency": "Frecuencia del resumen por correo electrónico", + "Embedding test failed: {error}": "La prueba de embeddings ha fallado: {error}", + "Enable calendar provider": "Activar el proveedor de calendario", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Active o desactive las funciones de LLM. Configure los proveedores y modelos mediante el botón de configuración de LLM de arriba.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Active o desactive la integración de búsqueda de SOLR. Configure los ajustes de conexión mediante el botón de configuración de conexión de arriba.", + "Enable RAG": "Activar RAG", + "Enable tools that allow the agent to interact with data through function calling.": "Active herramientas que permitan al agente interactuar con los datos mediante llamadas a funciones.", + "Enable write-back to target objects": "Activar la reescritura en los objetos de destino", + "End Date Field": "Campo de fecha de fin", + "Endpoint Path*": "Ruta del endpoint*", + "Enter agent description (optional)": "Introduzca la descripción del agente (opcional)", + "Enter agent name": "Introduzca el nombre del agente", + "Enter application description (optional)": "Introduzca la descripción de la aplicación (opcional)", + "Enter application name": "Introduzca el nombre de la aplicación", + "Enter configuration description (optional)": "Introduzca la descripción de la configuración (opcional)", + "Enter configuration title": "Introduzca el título de la configuración", + "Enter Nextcloud usernames to grant access to this private agent": "Introduzca nombres de usuario de Nextcloud para conceder acceso a este agente privado", + "Enter organisation description (optional)": "Introduzca la descripción de la organización (opcional)", + "Enter organisation name": "Introduzca el nombre de la organización", + "Enter search terms or leave empty to browse all": "Introduzca términos de búsqueda o déjelo vacío para examinar todo", + "Enter system prompt for the agent": "Introduzca el prompt de sistema para el agente", + "Enter username and press Enter": "Introduzca el nombre de usuario y pulse Intro", + "Enter your API key": "Introduzca su clave de API", + "Enter your object here...": "Introduzca su objeto aquí...", + "Enter your schema here...": "Introduzca su esquema aquí...", + "Entity ID: {id}": "ID de entidad: {id}", + "Entity Recognition": "Reconocimiento de entidades", + "Entity Recognition Method": "Método de reconocimiento de entidades", + "Error deleting audit trails: {error}": "Error al eliminar los rastros de auditoría: {error}", + "Error deleting search trails: {error}": "Error al eliminar los rastros de búsqueda: {error}", + "Example": "Ejemplo", + "Example value for this property": "Valor de ejemplo para esta propiedad", + "Exclusive maximum": "Máximo exclusivo", + "Exclusive minimum": "Mínimo exclusivo", + "execute": "ejecutar", + "Expiration": "Caducidad", + "Expires": "Caduca", + "Expires in (e.g., 90d)": "Caduca en (p. ej., 90d)", + "Export \"{schema}\" objects from \"{register}\"": "Exportar objetos \"{schema}\" de \"{register}\"", + "Export Configuration": "Exportar configuración", + "Export downloaded successfully": "Exportación descargada correctamente", + "Export failed: {error}": "La exportación ha fallado: {error}", + "Export Format:": "Formato de exportación:", + "Export is rate limited. Please try again later.": "La exportación tiene un límite de tasa. Inténtelo de nuevo más tarde.", + "Export my data": "Exportar mis datos", + "Export Objects": "Exportar objetos", + "Exporting...": "Exportando...", + "Extra Query Parameters": "Parámetros de consulta adicionales", + "Extract now": "Extraer ahora", + "Extraction error for {file}: {error}": "Error de extracción para {file}: {error}", + "Extraction Mode": "Modo de extracción", + "Extraction Scope": "Alcance de la extracción", + "Facet Description": "Descripción de la faceta", + "Facet Order": "Orden de la faceta", + "Facet Title": "Título de la faceta", + "Facet Type": "Tipo de faceta", + "Facetable": "Facetable", + "Faceting Configuration:": "Configuración de facetado:", + "Failed to add user to organisation": "No se pudo añadir el usuario a la organización", + "Failed to cancel deactivation": "No se pudo cancelar la desactivación", + "Failed to change active organisation: {error}": "No se pudo cambiar la organización activa: {error}", + "Failed to change password": "No se pudo cambiar la contraseña", + "Failed to check version: {error}": "No se pudo comprobar la versión: {error}", + "Failed to clear embeddings: {error}": "No se pudieron borrar los embeddings: {error}", + "Failed to copy to clipboard": "No se pudo copiar al portapapeles", + "Failed to copy token": "No se pudo copiar el token", + "Failed to create missing SOLR fields: {error}": "No se pudieron crear los campos de SOLR que faltaban: {error}", + "Failed to create token": "No se pudo crear el token", + "Failed to delete objects for {schema}: {error}": "No se pudieron eliminar los objetos de {schema}: {error}", + "Failed to delete SOLR collection: {error}": "No se pudo eliminar la colección de SOLR: {error}", + "Failed to depublish register: {error}": "No se pudo despublicar el registro: {error}", + "Failed to depublish schema: {error}": "No se pudo despublicar el esquema: {error}", + "Failed to depublish: {error}": "No se pudo despublicar: {error}", + "Failed to export data": "No se pudieron exportar los datos", + "Failed to export objects": "No se pudieron exportar los objetos", + "Failed to fix mismatched SOLR fields: {error}": "No se pudieron corregir los campos de SOLR no coincidentes: {error}", + "Failed to get response: {error}": "No se pudo obtener la respuesta: {error}", + "Failed to leave organisation: {error}": "No se pudo abandonar la organización: {error}", + "Failed to link object": "No se pudo vincular el objeto", + "Failed to load view: {error}": "No se pudo cargar la vista: {error}", + "Failed to publish register: {error}": "No se pudo publicar el registro: {error}", + "Failed to publish schema: {error}": "No se pudo publicar el esquema: {error}", + "Failed to publish: {error}": "No se pudo publicar: {error}", + "Failed to remove avatar": "No se pudo eliminar el avatar", + "Failed to remove link": "No se pudo eliminar el enlace", + "Failed to remove member: {error}": "No se pudo eliminar el miembro: {error}", + "Failed to remove schema {schema}: {error}": "No se pudo eliminar el esquema {schema}: {error}", + "Failed to remove user from organisation": "No se pudo eliminar el usuario de la organización", + "Failed to request deactivation": "No se pudo solicitar la desactivación", + "Failed to revoke token": "No se pudo revocar el token", + "Failed to save configuration.": "No se pudo guardar la configuración.", + "Failed to save configuration: {error}": "No se pudo guardar la configuración: {error}", + "Failed to save facet configuration": "No se pudo guardar la configuración de la faceta", + "Failed to save preferences": "No se pudieron guardar las preferencias", + "Failed to save view: {error}": "No se pudo guardar la vista: {error}", + "Failed to set active organisation: {error}": "No se pudo establecer la organización activa: {error}", + "Failed to start file warmup: {error}": "No se pudo iniciar el precalentamiento de archivos: {error}", + "Failed to start vectorization: {error}": "No se pudo iniciar la vectorización: {error}", + "Failed to switch organisation: {error}": "No se pudo cambiar de organización: {error}", + "Failed to sync magic table for {schema}: {error}": "No se pudo sincronizar la tabla mágica para {schema}: {error}", + "Failed to update schema configuration for {schema}: {error}": "No se pudo actualizar la configuración del esquema para {schema}: {error}", + "Failed to update view: {error}": "No se pudo actualizar la vista: {error}", + "Failed to upload avatar": "No se pudo subir el avatar", + "Failed to validate {schema}: {error}": "No se pudo validar {schema}: {error}", + "Failed to vectorize files: {error}": "No se pudieron vectorizar los archivos: {error}", + "Failed to warmup names cache: {error}": "No se pudo precalentar la caché de nombres: {error}", + "Federation": "Federación", + "Fields to Display": "Campos para mostrar", + "File #{id}": "Archivo n.º {id}", + "File Handling": "Gestión de archivos", + "File Location": "Ubicación del archivo", + "File name": "Nombre del archivo", + "File Types to Process": "Tipos de archivo para procesar", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "¡Vectorización de archivos completada! {vectorized} fragmentos vectorizados de {files} archivos. {failed} fallidos.", + "Files added successfully": "Archivos añadidos correctamente", + "Files → filecollection, objects → objectcollection": "Archivos → filecollection, objetos → objectcollection", + "file{plural}": "archivo{plural}", + "Filter by type": "Filtrar por tipo", + "Filter fields...": "Filtrar campos...", + "folder": "carpeta", + "From (e.g. -7 days or 2025-01-01)": "Desde (p. ej. -7 days o 2025-01-01)", + "Function Tools": "Herramientas de funciones", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Obtenga información en tiempo real sobre la salud de los datos de su organización, las tendencias de crecimiento y las métricas de calidad.", + "GitHub Personal Access Token": "Token de acceso personal de GitHub", + "GitHub token is valid! Username: {username}": "¡El token de GitHub es válido! Nombre de usuario: {username}", + "GitHub token test failed: {message}": "La prueba del token de GitHub ha fallado: {message}", + "GitLab Personal Access Token": "Token de acceso personal de GitLab", + "GitLab token is valid! Username: {username}": "¡El token de GitLab es válido! Nombre de usuario: {username}", + "GitLab token test failed: {message}": "La prueba del token de GitLab ha fallado: {message}", + "Go to linked Register": "Ir al registro vinculado", + "Go to linked Schema": "Ir al esquema vinculado", + "Go to organisation": "Ir a la organización", + "Grant additional permissions beyond standard CRUD operations": "Conceder permisos adicionales más allá de las operaciones CRUD estándar", + "Group Quota": "Cuota de grupo", + "Groups": "Grupos", + "Here you can configure the details for various connections.": "Aquí puede configurar los detalles de diversas conexiones.", + "Here you can configure the details for your organisation.": "Aquí puede configurar los detalles de su organización.", + "Hide": "Ocultar", + "Hide in collection view": "Ocultar en la vista de colección", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "IBANs": "IBAN", + "ID of input mapping (optional)": "ID del mapeo de entrada (opcional)", + "ID of output mapping (optional)": "ID del mapeo de salida (opcional)", + "ID of the target resource": "ID del recurso de destino", + "ID: {id}": "ID: {id}", + "Import Configuration": "Importar configuración", + "Import Data into Register": "Importar datos al registro", + "in {register}": "en {register}", + "in_progress": "en curso", + "Inactive organisations cannot be used": "Las organizaciones inactivas no se pueden usar", + "Include ids and names of related objects for better contextual search": "Incluir ids y nombres de objetos relacionados para una mejor búsqueda contextual", + "Index": "Índice", + "Information about the current OpenRegister installation": "Información sobre la instalación actual de OpenRegister", + "inherited": "heredado", + "Inherited from register": "Heredado del registro", + "Inherits permissions from register": "Hereda los permisos del registro", + "Input Mapping": "Mapeo de entrada", + "Inspect SOLR Index": "Inspeccionar el índice de SOLR", + "Internet location (URL) of this installation": "Ubicación en internet (URL) de esta instalación", + "Interval": "Intervalo", + "Invite Users": "Invitar usuarios", + "Invited Users": "Usuarios invitados", + "Key": "Clave", + "Label": "Etiqueta", + "Labels": "Etiquetas", + "Leave empty to allow all users access": "Déjelo vacío para permitir el acceso a todos los usuarios", + "Leave off for auto-detection from property format.": "Déjelo desactivado para la detección automática a partir del formato de la propiedad.", + "Left organisation successfully": "Abandonó la organización correctamente", + "Link": "Vincular", + "Link an object to see it here.": "Vincule un objeto para verlo aquí.", + "Link another object": "Vincular otro objeto", + "Link removed": "Enlace eliminado", + "Link to Object": "Vincular al objeto", + "Link to {name}": "Vincular a {name}", + "Linked by {user}": "Vinculado por {user}", + "Linked Objects": "Objetos vinculados", + "Linked to {name}": "Vinculado a {name}", + "list": "lista", + "Load more": "Cargar más", + "Loading activity...": "Cargando actividad...", + "Loading API tokens...": "Cargando tokens de API...", + "Loading available tools...": "Cargando herramientas disponibles...", + "Loading cache statistics...": "Cargando estadísticas de la caché...", + "Loading endpoints": "Cargando endpoints", + "Loading entities...": "Cargando entidades...", + "Loading linked objects...": "Cargando objetos vinculados...", + "Loading LLM configuration...": "Cargando configuración de LLM...", + "Loading multitenancy settings...": "Cargando configuración de multiinquilino...", + "Loading n8n configuration...": "Cargando configuración de n8n...", + "Loading permission matrix...": "Cargando matriz de permisos...", + "Loading preferences...": "Cargando preferencias...", + "Loading RBAC settings...": "Cargando configuración de RBAC...", + "Loading register data...": "Cargando datos del registro...", + "Loading retention settings...": "Cargando configuración de retención...", + "Loading search configuration...": "Cargando configuración de búsqueda...", + "Loading text extraction settings...": "Cargando configuración de extracción de texto...", + "Loading tokens...": "Cargando tokens...", + "Loading user groups...": "Cargando grupos de usuarios...", + "Location": "Ubicación", + "Location Field": "Campo de ubicación", + "Locations": "Ubicaciones", + "log{plural}": "registro{plural}", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Los números más bajos aparecen primero en la barra lateral de filtros. Déjelo vacío para una ordenación automática.", + "Magic table sync completed for {schema}": "Sincronización de tabla mágica completada para {schema}", + "Mail Integration": "Integración de correo", + "Make this installation discoverable within the federation network.": "Hacer que esta instalación sea detectable dentro de la red de federación.", + "manage": "gestionar", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Gestione las colecciones de SOLR (almacenes de datos) y asígnelas para objetos y archivos.", + "Manage SOLR configsets (configuration templates) for your collections.": "Gestione los configsets de SOLR (plantillas de configuración) para sus colecciones.", + "Manage your account settings, security, and personal data.": "Gestione la configuración de su cuenta, la seguridad y los datos personales.", + "Manage your chat conversations": "Gestione sus conversaciones de chat", + "Manage your organisations and switch between them": "Gestione sus organizaciones y cambie entre ellas", + "Mappings": "Mapeos", + "Max Tokens": "Tokens máximos", + "Maximum File Size (MB)": "Tamaño máximo de archivo (MB)", + "Maximum length": "Longitud máxima", + "Maximum number of items": "Número máximo de elementos", + "Maximum value": "Valor máximo", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Consultas medias: búsquedas con algún filtrado o varios parámetros (p. ej., intervalos de fechas, registros/esquemas específicos)", + "Member removed successfully": "Miembro eliminado correctamente", + "Members": "Miembros", + "Members ({count})": "Miembros ({count})", + "Metadata": "Metadatos", + "Method*": "Método*", + "Method: {method}": "Método: {method}", + "Minimum length": "Longitud mínima", + "Minimum number of items": "Número mínimo de elementos", + "Minimum value": "Valor mínimo", + "Multiple of": "Múltiplo de", + "Multitenancy": "Multiinquilino", + "My Account": "Mi cuenta", + "myapp": "myapp", + "n8n connection test failed: {message}": "La prueba de conexión de n8n ha fallado: {message}", + "n8n initialization failed: {message}": "La inicialización de n8n ha fallado: {message}", + "Name *": "Nombre *", + "Name*": "Nombre*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Caché de nombres precalentada correctamente: {count} nombres cargados en {time}. {cache}", + "Naming pattern for copies:": "Patrón de nombres para las copias:", + "New password": "Nueva contraseña", + "New User Group": "Nuevo grupo de usuarios", + "Nextcloud Groups": "Grupos de Nextcloud", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "No hay objetos activos para eliminar temporalmente en el esquema {schema}. Use \"Eliminar permanentemente\" para eliminar los objetos eliminados temporalmente.", + "No activity found.": "No se encontró actividad.", + "No API tokens.": "No hay tokens de API.", + "No approval steps for this object.": "No hay pasos de aprobación para este objeto.", + "No configuration selected": "No se ha seleccionado ninguna configuración", + "No endpoints defined": "No hay endpoints definidos", + "No entities detected for this email.": "No se detectaron entidades para este correo electrónico.", + "No files selected": "No se han seleccionado archivos", + "No groups found. Try a different search.": "No se encontraron grupos. Pruebe con una búsqueda diferente.", + "No label": "Sin etiqueta", + "No labels": "Sin etiquetas", + "No linked objects": "No hay objetos vinculados", + "No objects are currently selected for deletion.": "Actualmente no hay objetos seleccionados para eliminar.", + "No objects are currently selected for permanent deletion.": "Actualmente no hay objetos seleccionados para la eliminación permanente.", + "No objects are currently selected for restoration.": "Actualmente no hay objetos seleccionados para restaurar.", + "No objects linked to this email": "No hay objetos vinculados a este correo electrónico", + "No objects selected": "No se han seleccionado objetos", + "No objects selected for restoration": "No se han seleccionado objetos para restaurar", + "No objects to delete for schema {schema}": "No hay objetos para eliminar en el esquema {schema}", + "No organisations found": "No se encontraron organizaciones", + "No registers found. Create a register to configure permissions.": "No se encontraron registros. Cree un registro para configurar los permisos.", + "No related cases found for this sender": "No se encontraron casos relacionados para este remitente", + "No schemas configured for mail linking.": "No hay esquemas configurados para la vinculación de correo.", + "No synchronizations found": "No se encontraron sincronizaciones", + "No tools available. Tools can be registered by installed apps.": "No hay herramientas disponibles. Las aplicaciones instaladas pueden registrar herramientas.", + "No users in this organisation.": "No hay usuarios en esta organización.", + "Note:": "Nota:", + "Notifications": "Notificaciones", + "Number of Sources": "Número de fuentes", + "Object #{id}": "Objeto n.º {id}", + "Object changes in owned objects": "Cambios de objeto en los objetos propios", + "Object Configuration:": "Configuración del objeto:", + "Object Extraction Mode": "Modo de extracción de objetos", + "Object successfully modified": "Objeto modificado correctamente", + "Object Text Extraction": "Extracción de texto de objetos", + "Objects deletion completed for {schema}": "Eliminación de objetos completada para {schema}", + "Objects will be soft deleted and moved to the": "Los objetos se eliminarán temporalmente y se moverán a", + "objectType: object\naction: created": "objectType: object\naction: created", + "object{plural}": "objeto{plural}", + "Only members of selected groups can access this organisation": "Solo los miembros de los grupos seleccionados pueden acceder a esta organización", + "Open folder": "Abrir carpeta", + "Open in OpenRegister": "Abrir en OpenRegister", + "Open OpenRegister sidebar": "Abrir la barra lateral de OpenRegister", + "OpenRegister": "OpenRegister", + "Optional description for this facet": "Descripción opcional para esta faceta", + "Optional end date property": "Propiedad opcional de fecha de fin", + "Optional event description template": "Plantilla opcional de descripción del evento", + "Optional location property": "Propiedad opcional de ubicación", + "Optional URL-friendly identifier": "Identificador opcional apto para URL", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Opcional: Añada parámetros de consulta para filtrar el esquema referenciado (p. ej., status=active&type=public)", + "Or": "O", + "Organisation Configuration": "Configuración de la organización", + "Organisation membership changes": "Cambios de pertenencia a la organización", + "Organisation name is required": "El nombre de la organización es obligatorio", + "Organisation successfully updated": "Organización actualizada correctamente", + "Organizations": "Organizaciones", + "Other": "Otro", + "Output Mapping": "Mapeo de salida", + "Overview": "Resumen", + "Page {current} of {total}": "Página {current} de {total}", + "Password changes are not supported by your authentication provider.": "Su proveedor de autenticación no admite cambios de contraseña.", + "Password updated successfully": "Contraseña actualizada correctamente", + "pending": "pendiente", + "Permanently Delete ({count})": "Eliminar permanentemente ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Eliminar permanentemente los {count} objetos eliminados temporalmente. ¡Esto no se puede deshacer!", + "Permission Matrix": "Matriz de permisos", + "Personal Data Export": "Exportación de datos personales", + "Persons": "Personas", + "Phone Numbers": "Números de teléfono", + "Please select a user": "Seleccione un usuario", + "Please select an organisation": "Seleccione una organización", + "Please select at least one item to import": "Seleccione al menos un elemento para importar", + "Please select registers or schemas first": "Seleccione primero los registros o esquemas", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Espere mientras eliminamos permanentemente la colección de SOLR. Esto puede tardar unos momentos.", + "Preferences saved": "Preferencias guardadas", + "Press enter to send, shift+enter for new line": "Pulse Intro para enviar, Mayús+Intro para una nueva línea", + "Prettify": "Embellecer", + "Preview:": "Vista previa:", + "Private Agent (Default)": "Agente privado (predeterminado)", + "Private agents": "Agentes privados", + "Process Name (optional)": "Nombre del proceso (opcional)", + "Processed {indexed} files, {failed} failed": "Se procesaron {indexed} archivos, {failed} fallidos", + "Property could not be updated": "No se pudo actualizar la propiedad", + "Property name of inversed relation": "Nombre de la propiedad de la relación invertida", + "Property successfully updated": "Propiedad actualizada correctamente", + "Publish Configuration to GitHub": "Publicar la configuración en GitHub", + "Publish OAS": "Publicar OAS", + "Publish objects to make them publicly available": "Publicar objetos para que estén disponibles públicamente", + "Publish Register OAS to GitHub": "Publicar el OAS del registro en GitHub", + "Query Help": "Ayuda de consultas", + "Quota": "Cuota", + "RBAC": "RBAC", + "read": "leer", + "Reason": "Motivo", + "Reason (optional)": "Motivo (opcional)", + "Register #{id}": "Registro n.º {id}", + "Register / Schema": "Registro / Esquema", + "Register and schema are required": "El registro y el esquema son obligatorios", + "Register Overview": "Resumen del registro", + "Register:": "Registro:", + "register{plural}": "registro{plural}", + "Reindex completed! Processed {count} objects in {duration}s": "¡Reindexación completada! Se procesaron {count} objetos en {duration}s", + "Reject": "Rechazar", + "Rejected": "Rechazado", + "rejected": "rechazado", + "Related Cases": "Casos relacionados", + "Remove avatar": "Eliminar avatar", + "Remove from list": "Eliminar de la lista", + "Remove link": "Eliminar enlace", + "Remove link to {name}": "Eliminar el enlace a {name}", + "Remove link to {name}?": "¿Eliminar el enlace a {name}?", + "Remove link to {title}": "Eliminar el enlace a {title}", + "Remove oneOf entry": "Eliminar entrada oneOf", + "Remove property after write-back": "Eliminar la propiedad tras la reescritura", + "Remove {title}": "Eliminar {title}", + "Remove {userId} from this organisation?": "¿Eliminar a {userId} de esta organización?", + "Removing schema {schema}...": "Eliminando el esquema {schema}...", + "Request account deactivation": "Solicitar la desactivación de la cuenta", + "Request account deactivation. This will notify administrators for review.": "Solicitar la desactivación de la cuenta. Esto notificará a los administradores para su revisión.", + "Request Quota (per day)": "Cuota de solicitudes (por día)", + "Requested at": "Solicitado el", + "Required": "Obligatorio", + "Required Token Scopes": "Ámbitos de token requeridos", + "Requires Dolphin AI for OCR text extraction": "Requiere Dolphin AI para la extracción de texto OCR", + "Rerunning search: {searchTerm}": "Reejecutando la búsqueda: {searchTerm}", + "Resource Quotas": "Cuotas de recursos", + "Retry upload": "Reintentar la subida", + "Review the selected objects below. You can remove any objects you don't want to include.": "Revise los objetos seleccionados a continuación. Puede eliminar cualquier objeto que no quiera incluir.", + "Revoke": "Revocar", + "Right": "Derecha", + "Roles and Permissions": "Roles y permisos", + "Save labels": "Guardar etiquetas", + "Save tags for {name}": "Guardar etiquetas para {name}", + "Save the organisation first to manage users.": "Guarde primero la organización para gestionar los usuarios.", + "Schema configuration updated successfully for {schema}": "Configuración del esquema actualizada correctamente para {schema}", + "Schema reference ($ref)": "Referencia del esquema ($ref)", + "Schema {schema} removed successfully": "Esquema {schema} eliminado correctamente", + "Schema:": "Esquema:", + "schema{plural}": "esquema{plural}", + "Search": "Buscar", + "Search / views": "Buscar / vistas", + "Search agents...": "Buscar agentes...", + "Search applications...": "Buscar aplicaciones...", + "Search by title or UUID...": "Buscar por título o UUID...", + "Search Configuration": "Configuración de búsqueda", + "Search configurations": "Buscar configuraciones", + "Search data sources...": "Buscar fuentes de datos...", + "Search groups...": "Buscar grupos...", + "Search in Database Objects": "Buscar en los objetos de la base de datos", + "Search Mode": "Modo de búsqueda", + "Search objects": "Buscar objetos", + "Search objects...": "Buscar objetos...", + "Search Query": "Consulta de búsqueda", + "Search registers...": "Buscar registros...", + "Search Results": "Resultados de la búsqueda", + "Search schemas...": "Buscar esquemas...", + "Search {name}...": "Buscar {name}...", + "Searching...": "Buscando...", + "Security": "Seguridad", + "See organisations as a table": "Ver las organizaciones como una tabla", + "See organisations as cards": "Ver las organizaciones como tarjetas", + "Select a date property": "Seleccione una propiedad de fecha", + "Select a register": "Seleccione un registro", + "Select a register...": "Seleccione un registro...", + "Select a schema": "Seleccione un esquema", + "Select a schema...": "Seleccione un esquema...", + "Select a user": "Seleccione un usuario", + "Select Active Organisation": "Seleccionar la organización activa", + "Select agent type": "Seleccione el tipo de agente", + "Select an email to see linked objects": "Seleccione un correo electrónico para ver los objetos vinculados", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Seleccione una organización y un usuario para añadirlo como miembro. Busque organizaciones por nombre.", + "Select configuration type...": "Seleccione el tipo de configuración...", + "Select fields to display": "Seleccione los campos para mostrar", + "Select files with the correct extension": "Seleccione archivos con la extensión correcta", + "Select group": "Seleccione un grupo", + "Select groups (optional)": "Seleccione grupos (opcional)", + "Select groups to notify...": "Seleccione grupos para notificar...", + "Select groups with access to this agent": "Seleccione los grupos con acceso a este agente", + "Select groups...": "Seleccione grupos...", + "Select one or more registers": "Seleccione uno o más registros", + "Select one or more schemas": "Seleccione uno o más esquemas", + "Select or create labels, or select \"No label\" to add files": "Seleccione o cree etiquetas, o seleccione \"Sin etiqueta\" para añadir archivos", + "Select owner application (optional)...": "Seleccione la aplicación propietaria (opcional)...", + "Select role": "Seleccione un rol", + "Select schemas (empty = all schemas)": "Seleccione esquemas (vacío = todos los esquemas)", + "Select schemas to warm up": "Seleccione esquemas para precalentar", + "Select search mode": "Seleccione el modo de búsqueda", + "Select source type...": "Seleccione el tipo de fuente...", + "Select target type": "Seleccione el tipo de destino", + "Select views to filter data (optional)": "Seleccione vistas para filtrar los datos (opcional)", + "Select views to limit which data the agent can access": "Seleccione vistas para limitar a qué datos puede acceder el agente", + "Select {fieldName} values": "Seleccione los valores de {fieldName}", + "Selected Objects ({count})": "Objetos seleccionados ({count})", + "Send as cloudevent": "Enviar como cloudevent", + "Set as Active": "Establecer como activa", + "Set as active organisation": "Establecer como organización activa", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Establezca límites para el uso de la API y el consumo de tokens. Use 0 para recursos ilimitados.", + "Showing {showing} of {total} agents": "Mostrando {showing} de {total} agentes", + "Showing {showing} of {total} applications": "Mostrando {showing} de {total} aplicaciones", + "Showing {showing} of {total} audit trail entries": "Mostrando {showing} de {total} entradas del rastro de auditoría", + "Showing {showing} of {total} configurations": "Mostrando {showing} de {total} configuraciones", + "Showing {showing} of {total} deleted items": "Mostrando {showing} de {total} elementos eliminados", + "Showing {showing} of {total} entities": "Mostrando {showing} de {total} entidades", + "Showing {showing} of {total} files": "Mostrando {showing} de {total} archivos", + "Showing {showing} of {total} log entries": "Mostrando {showing} de {total} entradas de registro", + "Showing {showing} of {total} search trail entries": "Mostrando {showing} de {total} entradas del rastro de búsquedas", + "Showing {showing} of {total} sources": "Mostrando {showing} de {total} fuentes", + "Showing {showing} of {total} templates": "Mostrando {showing} de {total} plantillas", + "Showing {showing} of {total} webhooks": "Mostrando {showing} de {total} webhooks", + "Showing {shown} of {total} organisations": "Mostrando {shown} de {total} organizaciones", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Consultas simples: búsquedas de texto básicas con parámetros mínimos (p. ej., un solo término de búsqueda, sin filtros avanzados)", + "SOLR collection deleted successfully": "Colección de SOLR eliminada correctamente", + "SOLR Index Warmup": "Precalentamiento del índice de SOLR", + "Something went wrong while adding files": "Algo salió mal al añadir archivos", + "Source successfully updated": "Fuente actualizada correctamente", + "Special Rights": "Derechos especiales", + "Start Date Field": "Campo de fecha de inicio", + "Starting validation for {schema}...": "Iniciando la validación para {schema}...", + "Starting {action} for {schema}...": "Iniciando {action} para {schema}...", + "Statistics and Metrics": "Estadísticas y métricas", + "Statistics refreshed": "Estadísticas actualizadas", + "Step": "Paso", + "Storage": "Almacenamiento", + "Storage Quota (MB)": "Cuota de almacenamiento (MB)", + "Successfully added user to organisation: {name}": "Usuario añadido correctamente a la organización: {name}", + "Successfully created {count} SOLR fields": "Se crearon correctamente {count} campos de SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Se eliminaron correctamente {count} embeddings. Vuelva a vectorizar sus datos.", + "Successfully deleted {count} objects for {schema}": "Se eliminaron correctamente {count} objetos de {schema}", + "Successfully fixed {count} SOLR field configurations": "Se corrigieron correctamente {count} configuraciones de campos de SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importado correctamente: {registers} registros, {schemas} esquemas, {objects} objetos", + "Successfully permanently deleted {count} objects": "Se eliminaron permanentemente {count} objetos correctamente", + "Successfully processed {count} files!": "¡Se procesaron correctamente {count} archivos!", + "Successfully saved configuration for {count} facets!": "¡Configuración guardada correctamente para {count} facetas!", + "Supported File Types": "Tipos de archivo admitidos", + "Switch Active Organisation": "Cambiar la organización activa", + "Switch Organisation": "Cambiar de organización", + "Syncing magic table for {schema}...": "Sincronizando la tabla mágica para {schema}...", + "Syncs": "Sincronizaciones", + "System announcements": "Anuncios del sistema", + "System Prompt": "Prompt de sistema", + "Table": "Tabla", + "Target ID": "ID de destino", + "Target Type*": "Tipo de destino*", + "Text Chunk #{id}": "Fragmento de texto n.º {id}", + "Text Extraction": "Extracción de texto", + "Text Extraction Engine": "Motor de extracción de texto", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Se conservarán según el período de retención configurado en su esquema y luego se eliminarán permanentemente.", + "This action cannot be undone. Make sure no collections are using this configset.": "Esta acción no se puede deshacer. Asegúrese de que ninguna colección esté usando este configset.", + "This action will submit a deactivation request to your administrators.": "Esta acción enviará una solicitud de desactivación a sus administradores.", + "This is not valid JSON": "Esto no es JSON válido", + "This key already exists on this schema": "Esta clave ya existe en este esquema", + "This schema must use magic table configuration to sync": "Este esquema debe usar la configuración de tabla mágica para sincronizar", + "This token will only be shown once. Copy it now.": "Este token solo se mostrará una vez. Cópielo ahora.", + "This will copy the _default configset with the new name": "Esto copiará el configset _default con el nuevo nombre", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Esto eliminará permanentemente TODOS los embeddings (vectores) de la base de datos. Tendrá que volver a vectorizar todos los objetos y archivos. Esta acción no se puede deshacer.\n\n¿Está seguro de que desea continuar?", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Esto eliminará permanentemente {count} objetos ya eliminados temporalmente de la base de datos. Esta acción no se puede deshacer.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Consejo: active solo las vistas que necesiten búsqueda semántica para minimizar los costes de embedding. Las tablas de consulta simples rara vez necesitan vectorización.", + "Title *": "Título *", + "Title Template": "Plantilla de título", + "Title*": "Título*", + "To (e.g. -1 year or 2025-12-31)": "Hasta (p. ej. -1 year o 2025-12-31)", + "To add files larger than or equal to 512MB, go to the": "Para añadir archivos mayores o iguales a 512MB, vaya a", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Para ajustar el tamaño o la estrategia de los fragmentos, vaya a la configuración de archivos → límites de procesamiento.", + "To change the embedding provider or model, go to LLM configuration.": "Para cambiar el proveedor o el modelo de embedding, vaya a la configuración de LLM.", + "Token copied to clipboard": "Token copiado al portapapeles", + "Token Created": "Token creado", + "Token name": "Nombre del token", + "Token Quota (per request)": "Cuota de tokens (por solicitud)", + "Token revoked": "Token revocado", + "Tools respect the agent's own role and organization boundaries.": "Las herramientas respetan el propio rol del agente y los límites de la organización.", + "Try searching by UUID or with different keywords": "Pruebe a buscar por UUID o con palabras clave diferentes", + "Type \"DELETE COLLECTION\" to confirm:": "Escriba \"ELIMINAR COLECCIÓN\" para confirmar:", + "Type to search for objects...": "Escriba para buscar objetos...", + "Type to search for organisations": "Escriba para buscar organizaciones", + "Type to search...": "Escriba para buscar...", + "Type your username to confirm": "Escriba su nombre de usuario para confirmar", + "Unknown Object": "Objeto desconocido", + "Unnamed Object": "Objeto sin nombre", + "update": "actualizar", + "Update available: {local} → {remote}": "Actualización disponible: {local} → {remote}", + "Upload new avatar": "Subir nuevo avatar", + "Url": "URL", + "Use AI agents for processing and analysis": "Usar agentes de IA para el procesamiento y el análisis", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Usar intervalos predeterminados (Últimos 7/30/90 días, Último año, Más antiguos)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Usar almacenamiento externo (p. ej. MongoDB) en lugar del almacenamiento interno de Nextcloud.", + "Use Large Language Model features": "Usar funciones de modelos de lenguaje grandes", + "Use SolrCloud with zookeeper for distributed search": "Usar SolrCloud con zookeeper para la búsqueda distribuida", + "Use VNG APIs instead of MongoDB.": "Usar las API de VNG en lugar de MongoDB.", + "Used by": "Usado por", + "User Quota": "Cuota de usuario", + "Uses": "Usos", + "UUID copied to clipboard": "UUID copiado al portapapeles", + "Validation completed for {schema}": "Validación completada para {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validación completada para {schema}: {processed} procesados, {updated} actualizados, {failed} fallidos", + "Value list (split on ,)": "Lista de valores (separados por ,)", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vectorización completada: {vectorized} objetos vectorizados, {failed} fallidos", + "Vectorization failed: {error}": "La vectorización ha fallado: {error}", + "Version Information": "Información de la versión", + "View": "Ver", + "view": "ver", + "View and manage authorization across registers and schemas": "Ver y gestionar la autorización en todos los registros y esquemas", + "View API Documentation": "Ver la documentación de la API", + "View APIs": "Ver las API", + "View object": "Ver objeto", + "View {count} more": "Ver {count} más", + "Visible to end users": "Visible para los usuarios finales", + "waiting": "esperando", + "Warmup failed": "El precalentamiento ha fallado", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Cuando el agente se ejecuta sin una sesión de usuario (p. ej., tareas programadas), se usará el contexto de este usuario.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Cuando está desactivado, esta faceta solo mostrará valores de este esquema e incluirá un filtro de esquema al seleccionarse.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Cuando está activado, al guardar este objeto también se actualizarán los objetos de destino para incluir una referencia de vuelta a este objeto.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Cuando está activado, esta propiedad se eliminará del objeto de origen tras actualizar los objetos de destino.", + "Workflow Management": "Gestión de flujos de trabajo", + "Wrap webhook payload in cloudevents format for better interoperability": "Envolver la carga útil del webhook en formato cloudevents para una mejor interoperabilidad", + "You are not yet a member of any organisations.": "Aún no es miembro de ninguna organización.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Puede crear nuevos configsets basados en la plantilla _default, o subir personalizados directamente a su servidor SOLR.", + "You do not have permission to view the permission matrix. Admin access is required.": "No tiene permiso para ver la matriz de permisos. Se requiere acceso de administrador.", + "You have selected {count} properties to add to the schema.": "Ha seleccionado {count} propiedades para añadir al esquema.", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Sus comentarios se han registrado. opcionalmente, puede proporcionar detalles adicionales aquí...", + "{active} active, {deleted} deleted": "{active} activos, {deleted} eliminados", + "{count} audit trails cleared successfully": "{count} rastros de auditoría borrados correctamente", + "{count} deletions": "{count} eliminaciones", + "{count} documents found": "{count} documentos encontrados", + "{count} entries": "{count} entradas", + "{count} issues": "{count} incidencias", + "{count} members": "{count} miembros", + "{count} searches": "{count} búsquedas", + "{count} selected": "{count} seleccionados", + "{minutes} minutes ago": "hace {minutes} minutos", + "{percentage}% of objects": "{percentage}% de los objetos", + "{property} - {other}": "{property} - {other}", + "ℹ️ current configuration": "ℹ️ configuración actual", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ PELIGRO: Los {total} objetos se ELIMINARÁN PERMANENTEMENTE de la base de datos. ¡Esta acción es IRRECUPERABLE y no se puede deshacer!", + "{count} compliance issue(s) detected": "{count} incidencia(s) de cumplimiento detectada(s)", + "{count} email": "{count} correo electrónico", + "{count} emails": "{count} correos electrónicos", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} objeto(s) coincidente(s). {erased} borrado(s).", + "{type} cache cleared successfully": "Caché de {type} borrada correctamente", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ADVERTENCIA DE ELIMINACIÓN PERMANENTE \\u26A0\\uFE0F\n\nEstá a punto de eliminar PERMANENTEMENTE {count} objetos eliminados temporalmente del esquema \"{schema}\".\n\nEstos objetos se eliminarán por completo de la base de datos y NO se podrán recuperar.\n\n¿Está completamente seguro?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ADVERTENCIA DE ELIMINACIÓN PERMANENTE \\u26A0\\uFE0F\n\nEstá a punto de eliminar PERMANENTEMENTE TODOS los objetos del esquema \"{schema}\":\n\n\\u2022 Objetos activos: {active}\n\\u2022 Objetos eliminados temporalmente: {deleted}\n\\u2022 Total: {total}\n\nEstos objetos se eliminarán por completo de la base de datos y NO se podrán recuperar.\n\n¿Está completamente seguro?", + "Add a contact from any of your address books to associate it with this object.": "Añada un contacto de cualquiera de sus libretas de direcciones para asociarlo con este objeto.", + "Agent successfully created": "Agente creado correctamente", + "and accessible to all users in selected groups (or all users if no groups selected).": "y accesible para todos los usuarios de los grupos seleccionados (o todos los usuarios si no se selecciona ningún grupo).", + "App store cache invalidated: {invalidated}": "Caché de la tienda de aplicaciones invalidada: {invalidated}", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "¿Archivar esta verwerkingsactiviteit? Las filas del rastro de auditoría seguirán haciendo referencia a ella.", + "are only accessible to invited users.": "solo son accesibles para los usuarios invitados.", + "Attachment added to {name}": "Adjunto añadido a {name}", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: este informe combina cada actividad de tratamiento con los recuentos del rastro de auditoría de por vida que se le atribuyen. Los auditores y la Autoriteit Persoonsgegevens lo utilizan para la revisión de supervisión.", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (duración ISO-8601, p. ej. P10Y, P30D)", + "by": "por", + "Calendar integration is not available": "La integración del calendario no está disponible", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Haga clic en Ejecutar análisis de cumplimiento para encontrar esquemas en los que se ha detectado PII pero no existe ninguna anotación de actividad de tratamiento.", + "Click to subscribe to notifications": "Haga clic para suscribirse a las notificaciones", + "Click to unsubscribe from notifications": "Haga clic para cancelar la suscripción a las notificaciones", + "Code (short readable key, e.g. v-2026-001)": "Código (clave corta legible, p. ej. v-2026-001)", + "Contacts integration is not available": "La integración de contactos no está disponible", + "Create a new event or link an existing one from any of your calendars.": "Cree un nuevo evento o vincule uno existente desde cualquiera de sus calendarios.", + "Create or link a Deck card to track work on this object.": "Cree o vincule una tarjeta de Deck para realizar el seguimiento del trabajo en este objeto.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Cree la primera verwerkingsactiviteit para empezar a etiquetar las filas del rastro de auditoría con su atribución del AVG Art 30.", + "CSV": "CSV", + "Dashboard not found": "Panel de control no encontrado", + "Data-subject access request": "Solicitud de acceso del interesado", + "Deck": "Deck", + "Deck integration is not available": "La integración de Deck no está disponible", + "Delete {count} object": "Eliminar {count} objeto", + "Delete {count} objects": "Eliminar {count} objetos", + "Doelbinding *": "Doelbinding *", + "DSAR": "DSAR", + "Edit verwerkingsactiviteit": "Editar verwerkingsactiviteit", + "Erase (Art 17)": "Borrar (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "¿Borrar {count} objeto(s) de este interesado? Esta acción queda registrada en el rastro de auditoría.", + "Every schema with detected PII has a processing-activity annotation.": "Cada esquema con PII detectado tiene una anotación de actividad de tratamiento.", + "Excel (.xlsx)": "Excel (.xlsx)", + "Failed to add attachment to object": "No se pudo añadir el adjunto al objeto", + "Failed to clear {type} cache: {error}": "No se pudo borrar la caché de {type}: {error}", + "Failed to clear audit trails: {error}": "No se pudieron borrar los rastros de auditoría: {error}", + "Failed to clear blob objects: {error}": "No se pudieron borrar los objetos blob: {error}", + "Failed to clear cache: {error}": "No se pudo borrar la caché: {error}", + "Failed to clear search trails: {error}": "No se pudieron borrar los rastros de búsqueda: {error}", + "Failed to discover files: {error}": "No se pudieron descubrir los archivos: {error}", + "Failed to invalidate app store cache: {error}": "No se pudo invalidar la caché de la tienda de aplicaciones: {error}", + "Failed to load cache statistics: {error}": "No se pudieron cargar las estadísticas de la caché: {error}", + "Failed to load Deck cards": "No se pudieron cargar las tarjetas de Deck", + "Failed to load linked contacts": "No se pudieron cargar los contactos vinculados", + "Failed to load linked emails": "No se pudieron cargar los correos electrónicos vinculados", + "Failed to load linked events": "No se pudieron cargar los eventos vinculados", + "Failed to load related entities": "No se pudieron cargar las entidades relacionadas", + "Failed to load settings: {error}": "No se pudo cargar la configuración: {error}", + "Failed to load statistics: {error}": "No se pudieron cargar las estadísticas: {error}", + "Failed to save file settings: {error}": "No se pudo guardar la configuración de archivos: {error}", + "Failed to save LLM settings: {error}": "No se pudo guardar la configuración de LLM: {error}", + "Failed to save settings: {error}": "No se pudo guardar la configuración: {error}", + "Failed to save warmup interval: {error}": "No se pudo guardar el intervalo de precalentamiento: {error}", + "Failed to setup SOLR: {error}": "No se pudo configurar SOLR: {error}", + "Failed to start processing {type} files: {error}": "No se pudo iniciar el procesamiento de archivos {type}: {error}", + "Failed to test SOLR connection: {error}": "No se pudo probar la conexión de SOLR: {error}", + "Failed to update LLM settings: {error}": "No se pudo actualizar la configuración de LLM: {error}", + "Failed to update Multitenancy settings: {error}": "No se pudo actualizar la configuración de multiinquilino: {error}", + "Failed to update RBAC settings: {error}": "No se pudo actualizar la configuración de RBAC: {error}", + "Failed to update Retention settings: {error}": "No se pudo actualizar la configuración de retención: {error}", + "Failed to update SOLR settings: {error}": "No se pudo actualizar la configuración de SOLR: {error}", + "File settings saved successfully": "Configuración de archivos guardada correctamente", + "Filter and search entities": "Filtrar y buscar entidades", + "Generate report": "Generar informe", + "Generate the verantwoordingsdocument": "Generar el verantwoordingsdocument", + "Generated: {time}": "Generado: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Obtenga información en tiempo real sobre la salud de los datos de su organización centrándose en los registros, las definiciones de esquemas y el almacenamiento y uso de objetos.", + "HTML preview": "Vista previa HTML", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importe la plantilla report-bundle.json para obtener el registro `reports`, luego cree su primer panel de control a través de la interfaz de objetos estándar. Los paneles de control declaran sus widgets en JSON y el renderizador alimenta a cada widget con datos de agregación en directo.", + "Integrations": "Integraciones", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Resultados de inzage", + "Languages": "Idiomas", + "Link an email from the Mail app sidebar to associate it with this object.": "Vincule un correo electrónico desde la barra lateral de la aplicación Correo para asociarlo con este objeto.", + "Link existing event": "Vincular evento existente", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Los correos electrónicos, eventos de calendario, contactos y tarjetas de Deck vinculados aparecerán aquí.", + "LLM settings saved successfully": "Configuración de LLM guardada correctamente", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Localice todos los objetos que hagan referencia a un interesado (Art 15 inzage), previsualice un borrado (Art 17 vergetelheid) o exporte sus datos (Art 20 portabiliteit).", + "Mail integration is not available": "La integración de correo no está disponible", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Gestione las actividades de tratamiento, ejecute solicitudes de acceso de los interesados y audite el cumplimiento conforme al RGPD de la UE / AVG neerlandesa.", + "Mass validation completed successfully": "Validación masiva completada correctamente", + "Mass validation failed: {error}": "La validación masiva ha fallado: {error}", + "missing": "ausente", + "Multitenancy settings updated successfully": "Configuración de multiinquilino actualizada correctamente", + "Naam": "Naam", + "Naam *": "Naam *", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Caché de nombres precalentada correctamente: {count} nombres cargados en {time}. {message}", + "New activity": "Nueva actividad", + "New Configuration": "Nueva configuración", + "New verwerkingsactiviteit": "Nueva verwerkingsactiviteit", + "No contacts linked to this object": "No hay contactos vinculados a este objeto", + "No dashboards yet": "Aún no hay paneles de control", + "No Deck cards linked to this object": "No hay tarjetas de Deck vinculadas a este objeto", + "No emails linked to this object": "No hay correos electrónicos vinculados a este objeto", + "No events linked to this object": "No hay eventos vinculados a este objeto", + "No matches": "No hay coincidencias", + "No personal data was found for this subject identifier.": "No se encontraron datos personales para este identificador de interesado.", + "No processing activities yet": "Aún no hay actividades de tratamiento", + "No related entities": "No hay entidades relacionadas", + "No saved views yet. create one in the search tab!": "Aún no hay vistas guardadas. ¡cree una en la pestaña de búsqueda!", + "No scan run yet": "Aún no se ha ejecutado ningún análisis", + "Object metadata incomplete for file upload": "Los metadatos del objeto están incompletos para la subida del archivo", + "Object successfully deleted": "Objeto eliminado correctamente", + "Objects successfully deleted": "Objetos eliminados correctamente", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Paneles de control definidos por el operador e informes programados. Cada panel de control es un objeto de primera clase en el registro `reports`; los widgets se declaran en el array `widgets` del panel y se renderizan en directo a partir de agregaciones / GraphQL.", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Etiquetas de idioma BCP 47 ordenadas. El primer idioma es el predeterminado del registro y determina el respaldo de Accept-Language para las propiedades traducibles.", + "Organisation successfully created": "Organización creada correctamente", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "PDF": "PDF", + "PII hits": "Coincidencias de PII", + "PII rows": "Filas de PII", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preview erasure": "Previsualizar el borrado", + "Property could not be added": "No se pudo añadir la propiedad", + "Property successfully added": "Propiedad añadida correctamente", + "public": "público", + "Purge {count} object from database": "Purgar {count} objeto de la base de datos", + "Purge {count} objects from database": "Purgar {count} objetos de la base de datos", + "RBAC settings updated successfully": "Configuración de RBAC actualizada correctamente", + "Rebase operation completed successfully": "Operación de rebase completada correctamente", + "Rebase operation failed: {error}": "La operación de rebase ha fallado: {error}", + "Rechtsgrond": "Rechtsgrond", + "Register annotation": "Anotación del registro", + "Register objects": "Objetos del registro", + "Remove contact": "Eliminar contacto", + "Remove Deck card": "Eliminar tarjeta de Deck", + "Reports": "Informes", + "Restore {count} object": "Restaurar {count} objeto", + "Restore {count} objects": "Restaurar {count} objetos", + "Retention settings updated successfully": "Configuración de retención actualizada correctamente", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Revise los objetos seleccionados a continuación. Puede eliminar cualquier objeto que no quiera borrar haciendo clic en el botón de eliminar.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Revise los objetos seleccionados a continuación. Puede eliminar cualquier objeto que no quiera eliminar permanentemente haciendo clic en el botón de eliminar. Esta acción no se puede deshacer.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Revise los objetos seleccionados a continuación. Puede eliminar cualquier objeto que no quiera restaurar haciendo clic en el botón de eliminar. Los objetos se restaurarán a su ubicación original.", + "Run compliance scan": "Ejecutar análisis de cumplimiento", + "Scheduled": "Programado", + "Schema annotation": "Anotación del esquema", + "selected objects": "objetos seleccionados", + "set": "establecer", + "Settings updated successfully": "Configuración actualizada correctamente", + "Show": "Mostrar", + "Showing {count} dashboard(s)": "Mostrando {count} panel(es) de control", + "Showing {count} processing activities": "Mostrando {count} actividades de tratamiento", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Eliminar temporalmente todos los objetos de este esquema ({active} activos, {deleted} ya eliminados)", + "SOLR connection test failed: {error}": "La prueba de conexión de SOLR ha fallado: {error}", + "SOLR connection test successful!": "¡Prueba de conexión de SOLR correcta!", + "SOLR index warmup completed successfully": "Precalentamiento del índice de SOLR completado correctamente", + "SOLR settings updated successfully": "Configuración de SOLR actualizada correctamente", + "SOLR setup completed successfully!": "¡Configuración de SOLR completada correctamente!", + "SOLR setup failed: {error}": "La configuración de SOLR ha fallado: {error}", + "SOLR warmup failed: {error}": "El precalentamiento de SOLR ha fallado: {error}", + "Subject identifier (email, BSN, name, etc.)": "Identificador del interesado (correo electrónico, BSN, nombre, etc.)", + "Subscribe": "Suscribirse", + "Subscribed": "Suscrito", + "Successfully cleared {count} audit trails": "Se borraron correctamente {count} rastros de auditoría", + "Successfully cleared {count} blob storage objects": "Se borraron correctamente {count} objetos de almacenamiento blob", + "Successfully cleared {count} search trails": "Se borraron correctamente {count} rastros de búsqueda", + "Successfully restored {count} object": "Se restauró correctamente {count} objeto", + "Successfully restored {count} objects": "Se restauraron correctamente {count} objetos", + "Technical Details": "Detalles técnicos", + "Technische maatregelen": "Technische maatregelen", + "The Nextcloud Calendar app is not installed or enabled on this server.": "La aplicación Calendario de Nextcloud no está instalada ni activada en este servidor.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "La aplicación Contactos de Nextcloud no está instalada ni activada en este servidor.", + "The Nextcloud Deck app is not installed or enabled on this server.": "La aplicación Deck de Nextcloud no está instalada ni activada en este servidor.", + "The Nextcloud Mail app is not installed or enabled on this server.": "La aplicación Correo de Nextcloud no está instalada ni activada en este servidor.", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "No se pudo cargar el panel de control solicitado. Verifique la URL o elija otro panel de control de la lista.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Se conservarán según el período de retención configurado en su esquema y se eliminarán automáticamente de forma permanente cuando caduque el período de retención. El período de retención es configurable por esquema y se encuentra en la configuración del esquema.", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Las herramientas se ejecutan con los permisos de usuario predeterminados del agente cuando no hay ninguna sesión de usuario activa (p. ej., tareas cron). Configure el usuario predeterminado en la pestaña de Configuración.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Las herramientas respetan las vistas, los permisos y los límites de la organización del agente.", + "Total events": "Total de eventos", + "Type filter (optional, e.g. email)": "Filtro de tipo (opcional, p. ej. email)", + "Unknown widget type: {type}": "Tipo de widget desconocido: {type}", + "Unlink email": "Desvincular correo electrónico", + "Unlink event": "Desvincular evento", + "Untitled": "Sin título", + "Use filters to narrow down entities by type or category.": "Use filtros para acotar las entidades por tipo o categoría.", + "Verantwoording": "Verantwoording", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Advertencia: Esta propiedad tiene tipos inconsistentes: {types}. Considere si todos los objetos deberían tener el mismo tipo.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Analizaremos todos los objetos pertenecientes a este esquema para descubrir nuevas propiedades y analizar las propiedades existentes en busca de posibles mejoras. El proceso consiste en examinar la estructura de datos de cada objeto, identificar propiedades no definidas en el esquema actual y encontrar oportunidades para mejorar las definiciones de propiedades existentes con mejores restricciones, formatos y reglas de validación.", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Cuando el agente se ejecuta sin una sesión de usuario (p. ej., tareas programadas), se usará el contexto de este usuario", + "Workflows": "Flujos de trabajo", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "Bookmarks": "Bookmarks", + "Categories of data subjects (one per line)": "Categories of data subjects (one per line)", + "Categories of personal data (one per line)": "Categories of personal data (one per line)", + "Configuring field mappings...": "Configuring field mappings...", + "Connecting to SOLR cluster...": "Connecting to SOLR cluster...", + "Creating search collections...": "Creating search collections...", + "Edit processing activity": "Edit processing activity", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Finalizing setup...": "Finalizing setup...", + "Forbidden": "Forbidden", + "Initializing SOLR setup...": "Initializing SOLR setup...", + "Legal basis *": "Legal basis *", + "New processing activity": "New processing activity", + "Optimizing search performance...": "Optimizing search performance...", + "Organisational measures": "Organisational measures", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Purpose limitation *": "Purpose limitation *", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Retention period (ISO-8601 duration, e.g. P10Y, P30D)", + "SOLR setup failed. Please check the configuration and try again.": "SOLR setup failed. Please check the configuration and try again.", + "Select all entities": "Select all entities", + "Select entity {value}": "Select entity {value}", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "Technical measures": "Technical measures", + "Uploading configuration sets...": "Uploading configuration sets...", + "Verifying server connectivity...": "Verifying server connectivity...", + "Waiting for cluster synchronization...": "Waiting for cluster synchronization...", + "You do not have access to this object": "You do not have access to this object", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR can handle millions of documents with sub-second search response times...", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Faceted search allows users to drill down into results by categories...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR provides rich analytics and statistics about search performance...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets contain the schema and configuration files for your search index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replication ensures your search index is available even if nodes fail...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Auto-scaling can dynamically add or remove nodes based on load...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Security features include authentication, authorization, and SSL encryption...", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} correo electrónico", + "{count} correos electrónicos" + ], + "Purge {count} object from database": [ + "Purgar {count} objeto de la base de datos", + "Purgar {count} objetos de la base de datos" + ], + "Restore {count} object": [ + "Restaurar {count} objeto", + "Restaurar {count} objetos" + ], + "Successfully restored {count} object": [ + "Se restauró {count} objeto correctamente", + "Se restauraron {count} objetos correctamente" + ], + "Delete {count} object": [ + "Eliminar {count} objeto", + "Eliminar {count} objetos" + ], + "Object successfully deleted": [ + "Objeto eliminado correctamente", + "Objetos eliminados correctamente" + ] + } +} diff --git a/l10n/et.js b/l10n/et.js new file mode 100644 index 0000000000..e63722b841 --- /dev/null +++ b/l10n/et.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(teemata)", + "(no title)" : "(pealkirjata)", + "(unnamed)" : "(nimetu)", + "(untitled card)" : "(pealkirjata kaart)", + "Activities" : "Tegevused", + "Add Endpoint" : "Lisa lõp-punkt", + "Add Source" : "Lisa allikas", + "Add card" : "Lisa kaart", + "Add contact" : "Lisa kontakt", + "All clear" : "Kõik korras", + "Articles" : "Artiklid", + "Code" : "Kood", + "Compliance" : "Vastavus", + "Contacts" : "Kontaktid", + "Create Agent" : "Loo agent", + "Create Application" : "Loo rakendus", + "Create event" : "Loo sündmus", + "Erasure complete" : "Kustutamine lõpetatud", + "Erasure preview" : "Kustutamise eelvaade", + "Expand details" : "Laienda üksikasju", + "Save changes" : "Salvesta muudatused", + "📄 Object Serialization" : "📄 Objekti serialiseerimine", + "🔢 Vectorization Settings" : "🔢 Vektoriseerimise seaded", + "💰 View Selection (Cost Optimization)" : "💰 Vaate valik (kulude optimeerimine)", + "AI Assistant" : "Tehisintellekti abiline", + "AI Chat" : "Tehisintellekti vestlus", + "API Key" : "API võti", + "API tokens saved successfully" : "API load salvestati edukalt", + "About ConfigSets" : "ConfigSetide kohta", + "Action" : "Tegevus", + "Action Distribution" : "Tegevuste jaotus", + "Action:" : "Tegevus:", + "Actions" : "Tegevused", + "Active" : "Aktiivne", + "Active Collections" : "Aktiivsed kollektsioonid", + "Active Filters" : "Aktiivsed filtrid", + "Active Organisations" : "Aktiivsed organisatsioonid", + "Active filters:" : "Aktiivsed filtrid:", + "Active organisation set successfully" : "Aktiivne organisatsioon seati edukalt", + "Activity Period" : "Tegevusperiood", + "Add Groups" : "Lisa rühmad", + "Add Schema" : "Lisa skeem", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Lisa skeemi pealkirjad, kirjeldused ja registri teave, et pakkuda otsingule rikkalikumat konteksti", + "Add to favorites" : "Lisa lemmikutesse", + "Added" : "Lisatud", + "Added to favorites" : "Lisatud lemmikutesse", + "Additional Information" : "Lisateave", + "Additional feedback saved. Thank you!" : "Lisatagasiside salvestati. Aitäh!", + "Admin" : "Administraator", + "Advanced" : "Täpsem", + "Advanced Filters" : "Täpsemad filtrid", + "Advanced Options" : "Täpsemad valikud", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Täpsemad filtrid ei ole andmebaasi allika kasutamisel saadaval. Filtreerimisvalikute jaoks lülitu režiimile Auto või SOLR-indeks.", + "After" : "Pärast", + "Agent deleted successfully" : "Agent kustutati edukalt", + "Agents" : "Agendid", + "All" : "Kõik", + "All Categories" : "Kõik kategooriad", + "All Collections" : "Kõik kollektsioonid", + "All Confidence Levels" : "Kõik usaldustasemed", + "All ConfigSets" : "Kõik ConfigSetid", + "All Files" : "Kõik failid", + "All Levels" : "Kõik tasemed", + "All Types" : "Kõik tüübid", + "All Webhooks" : "Kõik veebihaagid", + "All actions" : "Kõik tegevused", + "All audit trails cleared successfully" : "Kõik auditijäljed puhastati edukalt", + "All registers" : "Kõik registrid", + "All schemas" : "Kõik skeemid", + "All searches" : "Kõik otsingud", + "All users" : "Kõik kasutajad", + "An error occurred while clearing audit trails" : "Auditijälgede puhastamisel tekkis viga", + "An error occurred while deleting the audit trail" : "Auditijälje kustutamisel tekkis viga", + "An error occurred while deleting the view" : "Vaate kustutamisel tekkis viga", + "An error occurred while permanently deleting the objects" : "Objektide jäädavalt kustutamisel tekkis viga", + "Analysis completed:" : "Analüüs lõpetatud:", + "Analysis steps:" : "Analüüsi sammud:", + "Analytics" : "Analüütika", + "Analyze Objects" : "Analüüsi objekte", + "Analyze Properties" : "Analüüsi atribuute", + "Analyze existing properties for improvement opportunities" : "Analüüsi olemasolevaid atribuute parendusvõimaluste leidmiseks", + "Analyzing..." : "Analüüsimine...", + "Any favorites and sharing settings for this view" : "Selle vaate lemmiku- ja jagamisseaded", + "Any user" : "Mis tahes kasutaja", + "App store cache invalidated successfully" : "Rakenduste poe vahemälu tühistati edukalt", + "Application deleted successfully" : "Rakendus kustutati edukalt", + "Applications" : "Rakendused", + "Apply Changes" : "Rakenda muudatused", + "Archive" : "Arhiveeri", + "Archive conversation" : "Arhiveeri vestlus", + "Archived conversations are hidden from your active list" : "Arhiveeritud vestlused on teie aktiivsest loendist peidetud", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Kas olete kindel, et soovite vanad otsingujäljed puhastada? See kustutab kirjed, mis on vanemad kui 30 päeva.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Kas olete kindel, et soovite valitud auditijäljed kustutada? Seda tegevust ei saa tagasi võtta.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Kas olete kindel, et soovite valitud otsingujäljed kustutada? Seda tegevust ei saa tagasi võtta.", + "Are you sure you want to delete this ConfigSet?" : "Kas olete kindel, et soovite selle ConfigSeti kustutada?", + "Are you sure you want to permanently delete" : "Kas olete kindel, et soovite jäädavalt kustutada", + "Ask a question..." : "Esita küsimus...", + "Ask questions about your data using natural language" : "Esita oma andmete kohta küsimusi loomulikus keeles", + "Attempt" : "Katse", + "Audit Trail Changes" : "Auditijälje muudatused", + "Audit Trail Details" : "Auditijälje üksikasjad", + "Audit Trail Management" : "Auditijälgede haldus", + "Audit Trail Statistics" : "Auditijälgede statistika", + "Audit Trails" : "Auditijäljed", + "Audit trail data copied to clipboard" : "Auditijälje andmed kopeeriti lõikelauale", + "Audit trail deleted successfully" : "Auditijälg kustutati edukalt", + "Audit trail successfully deleted" : "Auditijälg kustutati edukalt", + "Authentication" : "Autentimine", + "Auto-commit disabled" : "Automaatne sissekanne keelatud", + "Auto-commit enabled" : "Automaatne sissekanne lubatud", + "Auto-create Default Organisation" : "Loo vaikimisi organisatsioon automaatselt", + "Auto-retry failed vectorizations" : "Proovi ebaõnnestunud vektoriseerimisi automaatselt uuesti", + "Automatically commit changes to SOLR index" : "Kanna muudatused SOLR-indeksisse automaatselt sisse", + "Automatically create a default organisation if none exists when the app is initialized" : "Loo rakenduse lähtestamisel automaatselt vaikimisi organisatsioon, kui ühtegi ei ole olemas", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Loo failide üleslaadimisel ja töötlemisel automaatselt vektorvälistused tekstitükkidest", + "Automatically generate vector embeddings when objects are created or updated" : "Loo objektide loomisel või uuendamisel automaatselt vektorvälistused", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Proovi ebaõnnestunud vektoriseerimiskatseid automaatselt uuesti (kuni 3 korda)", + "Available Filters" : "Saadaolevad filtrid", + "Available Workflows" : "Saadaolevad töövood", + "Avg Execution Time" : "Keskmine täitmisaeg", + "Avg Members/Org" : "Keskmine liikmete arv organisatsiooni kohta", + "Avg Object Views/Session" : "Keskmine objektivaadete arv seansi kohta", + "Avg Response Time" : "Keskmine vastusaeg", + "Avg Results/Search" : "Keskmine tulemuste arv otsingu kohta", + "Avg Searches/Session" : "Keskmine otsingute arv seansi kohta", + "Back" : "Tagasi", + "Back to Entities" : "Tagasi olemite juurde", + "Back to Registers" : "Tagasi registrite juurde", + "Back to Webhooks" : "Tagasi veebihaakide juurde", + "Back to applications" : "Tagasi rakenduste juurde", + "Back to entities" : "Tagasi olemite juurde", + "Backend updated successfully. Please reload the application." : "Taustasüsteem uuendati edukalt. Palun laadige rakendus uuesti.", + "Base URL (Optional)" : "Baas-URL (valikuline)", + "Basic Information" : "Põhiteave", + "Batch Size" : "Partii suurus", + "Batch extraction completed" : "Partiieraldus lõpetatud", + "Before" : "Enne", + "Before object vectorization can work:" : "Enne kui objektide vektoriseerimine saab toimida:", + "Behavior Issue" : "Käitumisprobleem", + "Bookmarks" : "Järjehoidjad", + "Blob storage has been retired. All objects now use magic tables." : "Blob-salvestus on kasutuselt kõrvaldatud. Kõik objektid kasutavad nüüd maagilisi tabeleid.", + "Branch" : "Haru", + "Bulk delete operation completed successfully" : "Hulgikustutamise toiming lõpetati edukalt", + "Bulk save operation completed successfully" : "Hulgisalvestamise toiming lõpetati edukalt", + "Business Data" : "Äriandmed", + "Cache cleared successfully" : "Vahemälu puhastati edukalt", + "Calculate Sizes" : "Arvuta suurused", + "Cancel" : "Tühista", + "Cannot delete: objects are still attached" : "Ei saa kustutada: objektid on endiselt seotud", + "Category" : "Kategooria", + "Change Type" : "Muudatuse tüüp", + "Changes" : "Muudatused", + "Changes copied to clipboard" : "Muudatused kopeeriti lõikelauale", + "Chat Model" : "Vestlusmudel", + "Chat Provider" : "Vestlusteenuse pakkuja", + "Chat Provider (RAG)" : "Vestlusteenuse pakkuja (RAG)", + "Chat Settings" : "Vestluse seaded", + "Chat provider connection successful!" : "Vestlusteenuse pakkuja ühendus õnnestus!", + "Chat settings" : "Vestluse seaded", + "Choose a register" : "Vali register", + "Choose a schema" : "Vali skeem", + "Choose how vector similarity calculations are performed for semantic search" : "Vali, kuidas tehakse semantilise otsingu jaoks vektorisarnasuse arvutusi", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Vali, millised failitüübid kaasata vektoriseerimisprotsessi. Töödeldakse ainult faile, millel on eraldatud tekst ja tükid.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Vali, millised vaated kaasata vektoriseerimisprotsessi. Jäta tühjaks, et töödelda kõiki vaateid vastavalt teie konfiguratsioonile.", + "Chunk" : "Tükk", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Tükkide kustutamine pole veel teostatud. Kasuta tükipõhiseid lõpp-punkte.", + "Chunks" : "Tükid", + "Chunks to Vectorize:" : "Vektoriseeritavad tükid:", + "Cleanup Old Trails" : "Puhasta vanad jäljed", + "Cleanup completed" : "Puhastamine lõpetatud", + "Clear All Embeddings" : "Tühjenda kõik välistused", + "Clear Entries" : "Tühjenda kirjed", + "Clear Filtered Audit Trails" : "Tühjenda filtreeritud auditijäljed", + "Clear Filters" : "Tühjenda filtrid", + "Clear Index" : "Tühjenda indeks", + "Clear Selection" : "Tühjenda valik", + "Clear all" : "Tühjenda kõik", + "Clear all filters" : "Tühjenda kõik filtrid", + "Clear date range" : "Tühjenda kuupäevavahemik", + "Clearing..." : "Tühjendamine...", + "Close" : "Sulge", + "Collection Management" : "Kollektsioonide haldus", + "Collection Name" : "Kollektsiooni nimi", + "Collection assignments updated successfully" : "Kollektsiooni määrangud uuendati edukalt", + "Collection cleared successfully" : "Kollektsioon tühjendati edukalt", + "Collection copied successfully" : "Kollektsioon kopeeriti edukalt", + "Collection created successfully" : "Kollektsioon loodi edukalt", + "Collection deleted successfully" : "Kollektsioon kustutati edukalt", + "Collection used to store and index file metadata and content" : "Kollektsioon failide metaandmete ja sisu salvestamiseks ja indekseerimiseks", + "Collection used to store and index object data" : "Kollektsioon objektiandmete salvestamiseks ja indekseerimiseks", + "Collections:" : "Kollektsioonid:", + "Commit Message" : "Sissekande sõnum", + "Commit Within (ms)" : "Kanna sisse aja jooksul (ms)", + "Commit message" : "Sissekande sõnum", + "Compare current schema with real object data" : "Võrdle praegust skeemi tegelike objektiandmetega", + "Completed" : "Lõpetatud", + "Complex" : "Keeruline", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Keerukad päringud: täpsemad otsingud mitme filtri, operaatori ja keerukate parameetrikombinatsioonidega", + "Confidence Level" : "Usaldustase", + "Confidence Score" : "Usaldusskoor", + "Config must be provided as an object" : "Konfiguratsioon tuleb esitada objektina", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSetide haldus", + "ConfigSet Name" : "ConfigSeti nimi", + "ConfigSet created successfully" : "ConfigSet loodi edukalt", + "ConfigSet deleted successfully" : "ConfigSet kustutati edukalt", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSetid määratlevad teie SOLR-kollektsioonide skeemi ja konfiguratsiooni. Need sisaldavad väljade määratlusi, analüsaatoreid ja muid otsinguseadeid.", + "Configuration" : "Konfiguratsioon", + "Configuration Data" : "Konfiguratsiooni andmed", + "Configuration saved" : "Konfiguratsioon salvestatud", + "Configurations" : "Konfiguratsioonid", + "Configure Facets" : "Seadista tahud", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Seadista suure keelemudeli (LLM) pakkujad tehisintellekti põhiste funktsioonide jaoks, sealhulgas semantiline otsing, välistused ja vestlus.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Seadista oma SOLR-serveri põhilised ühenduse seaded, sealhulgas autentimine ja võrguvalikud. Tuumade ja kollektsioonide haldamiseks kasuta eraldi ConfigSeti ja kollektsioonide halduse dialooge.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Seadista, kuidas andmebaasiobjektid teisendatakse semantilise otsingu jaoks vektorvälistusteks. Objektid vektoriseeritakse otse ilma tekstieralduseta.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Seadista, kuidas objektid teisendatakse enne vektoriseerimist tekstiks. Need seaded mõjutavad otsingu kvaliteeti ja konteksti.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Seadista failide vektoriseerimise parameetrid. See protsess loob vektorvälistused kõigi eraldatud failitükkide jaoks.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Seadista objektide vektoriseerimise parameetrid. See protsess loob vektorvälistused kõigi teie vaate filtritele vastavate objektide jaoks.", + "Configure which types of data to search and how many sources to retrieve" : "Seadista, milliseid andmetüüpe otsida ja mitu allikat hankida", + "Connection Failed" : "Ühendus ebaõnnestus", + "Connection Settings" : "Ühenduse seaded", + "Connection Successful!" : "Ühendus õnnestus!", + "Connection failed" : "Ühendus ebaõnnestus", + "Connection protocol" : "Ühendusprotokoll", + "Connection successful" : "Ühendus õnnestus", + "Connection timeout in seconds" : "Ühenduse ajalõpp sekundites", + "Constant delay between retries (5 minutes)" : "Püsiv viivitus korduskatsete vahel (5 minutit)", + "Constraint Issue" : "Kitsenduse probleem", + "Content Filters" : "Sisufiltrid", + "Continue to Properties" : "Jätka atribuutidega", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Halda, milliseid objektivaateid tuleks API-kulude vähendamiseks vektoriseerida. Vektoriseeri ainult vaateid, mis saavad kasu semantilisest otsingust.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Halda, milliseid vaateid ja tööriistu saab tehisintellekt selles vestluses kasutada. Vaikimisi on kõik agendi võimekused lubatud.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Juhib juhuslikkust (0 = deterministlik, 2 = väga loov)", + "Conversation ID is required" : "Vestluse ID on nõutav", + "Conversation archived" : "Vestlus arhiveeritud", + "Conversation archived successfully" : "Vestlus arhiveeriti edukalt", + "Conversation cleared successfully" : "Vestlus tühjendati edukalt", + "Conversation deleted" : "Vestlus kustutatud", + "Conversation permanently deleted" : "Vestlus jäädavalt kustutatud", + "Conversation renamed" : "Vestlus ümber nimetatud", + "Conversation restored" : "Vestlus taastatud", + "Conversation title" : "Vestluse pealkiri", + "Copied!" : "Kopeeritud!", + "Copy" : "Kopeeri", + "Copy Changes" : "Kopeeri muudatused", + "Copy Collection" : "Kopeeri kollektsioon", + "Copy Data" : "Kopeeri andmed", + "Copy Full Data" : "Kopeeri kõik andmed", + "Copying..." : "Kopeerimine...", + "Count" : "Arv", + "Counting objects..." : "Objektide loendamine...", + "Costs" : "Kulud", + "Create" : "Loo", + "Create Collection" : "Loo kollektsioon", + "Create ConfigSet" : "Loo ConfigSet", + "Create New Collection" : "Loo uus kollektsioon", + "Create New ConfigSet" : "Loo uus ConfigSet", + "Create Operations" : "Loomistoimingud", + "Create Webhook" : "Loo veebihaak", + "Create a copy of collection:" : "Loo kollektsioonist koopia:", + "Create a new ConfigSet based on the _default template" : "Loo uus ConfigSet malli _default põhjal", + "Create a new SOLR collection from an existing ConfigSet" : "Loo olemasolevast ConfigSetist uus SOLR-kollektsioon", + "Create your first AI agent to get started." : "Alustamiseks looge oma esimene tehisintellekti agent.", + "Created" : "Loodud", + "Created:" : "Loodud:", + "Creating..." : "Loomine...", + "Current Filters" : "Praegused filtrid", + "Current Type:" : "Praegune tüüp:", + "Custom API endpoint if using a different region" : "Kohandatud API lõpp-punkt, kui kasutatakse muud piirkonda", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Kohandatud HTTP päised (üks rea kohta, vorming: Päise-Nimi: väärtus)", + "Daily" : "Iga päev", + "Dashboard" : "Töölaud", + "Data Source" : "Andmeallikas", + "Data sources" : "Andmeallikad", + "Data type does not match observed values" : "Andmetüüp ei vasta täheldatud väärtustele", + "Database URL" : "Andmebaasi URL", + "Database information refreshed" : "Andmebaasi teave värskendatud", + "Date Range" : "Kuupäevavahemik", + "Default" : "Vaikimisi", + "Default Organisation" : "Vaikimisi organisatsioon", + "Default: 5" : "Vaikimisi: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Viivitused kahekordistuvad iga katsega (2, 4, 8 minutit...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Viivitused suurenevad lineaarselt (5, 10, 15 minutit...)", + "Delete" : "Kustuta", + "Delete Application" : "Kustuta rakendus", + "Delete Audit Trail" : "Kustuta auditijälg", + "Delete Collection" : "Kustuta kollektsioon", + "Delete ConfigSet" : "Kustuta ConfigSet", + "Delete Objects" : "Kustuta objektid", + "Delete Operations" : "Kustutamistoimingud", + "Delete View" : "Kustuta vaade", + "Delete all objects in this schema" : "Kustuta kõik selle skeemi objektid", + "Delete permanently" : "Kustuta jäädavalt", + "Delete this conversation?" : "Kas kustutada see vestlus?", + "Delete view" : "Kustuta vaade", + "Deleted" : "Kustutatud", + "Deleted By" : "Kustutaja", + "Deleted Date" : "Kustutamise kuupäev", + "Deleted Items Management" : "Kustutatud üksuste haldus", + "Deleted This Week" : "Kustutatud sel nädalal", + "Deleted Today" : "Kustutatud täna", + "Deleted:" : "Kustutatud:", + "Deleting..." : "Kustutamine...", + "Deletion Date Range" : "Kustutamise kuupäevavahemik", + "Deletion Statistics" : "Kustutamise statistika", + "Deprecated" : "Iganenud", + "Description" : "Kirjeldus", + "Description:" : "Kirjeldus:", + "Detect data types and patterns" : "Tuvasta andmetüübid ja mustrid", + "Detected At" : "Tuvastatud", + "Detected Format:" : "Tuvastatud vorming:", + "Detected Issues:" : "Tuvastatud probleemid:", + "Disable" : "Keela", + "Disabled" : "Keelatud", + "Discover Files" : "Avasta failid", + "Discovered Properties" : "Avastatud atribuudid", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Kas soovite kõik filtreeritud auditijälje kirjed jäädavalt kustutada? Seda tegevust ei saa tagasi võtta.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Kas soovite selle auditijälje kirje jäädavalt kustutada? Seda tegevust ei saa tagasi võtta.", + "Documents" : "Dokumendid", + "Download API Spec" : "Laadi alla API spetsifikatsioon", + "Edit" : "Muuda", + "Edit Register" : "Muuda registrit", + "Edit Schema" : "Muuda skeemi", + "Edit View" : "Muuda vaadet", + "Edit Webhook" : "Muuda veebihaaki", + "Edit view" : "Muuda vaadet", + "Edit view details" : "Muuda vaate üksikasju", + "Effectiveness" : "Tõhusus", + "Email" : "E-post", + "Emails" : "E-kirjad", + "Embedding Model" : "Välistuste mudel", + "Embedding Provider" : "Välistuste pakkuja", + "Embedding generated successfully" : "Välistus loodi edukalt", + "Embedding provider connection successful!" : "Välistuste pakkuja ühendus õnnestus!", + "Enable" : "Luba", + "Enable auto-creation" : "Luba automaatne loomine", + "Enable automatic file vectorization" : "Luba failide automaatne vektoriseerimine", + "Enable automatic object vectorization" : "Luba objektide automaatne vektoriseerimine", + "Enable automatic synchronization" : "Luba automaatne sünkroonimine", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Luba SOLR-toimingute üksikasjalik logimine (soovitatav silumiseks)", + "Enable faceting" : "Luba tahkude kasutamine", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Luba või keela LLM-funktsioonid. Seadista pakkujad ja mudelid ülaloleva LLM-konfiguratsiooni nupu abil.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Luba või keela SOLR-otsingu lõimimine. Seadista ühenduse seaded ülaloleva ühenduse seadete nupu abil.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Luba või keela n8n töövoo lõimimine. Seadista ühenduse seaded allpool.", + "Enable or disable this webhook" : "Luba või keela see veebihaak", + "Enable vectorization for all existing and future views (may increase costs)" : "Luba vektoriseerimine kõigi olemasolevate ja tulevaste vaadete jaoks (võib suurendada kulusid)", + "Enabled" : "Lubatud", + "Endpoints" : "Lõpp-punktid", + "Enter ConfigSet name" : "Sisesta ConfigSeti nimi", + "Enter collection name" : "Sisesta kollektsiooni nimi", + "Enter description (optional)..." : "Sisesta kirjeldus (valikuline)...", + "Enter new collection name" : "Sisesta uue kollektsiooni nimi", + "Enter object ID" : "Sisesta objekti ID", + "Enter search term" : "Sisesta otsingusõna", + "Enter view name..." : "Sisesta vaate nimi...", + "Enter webhook name" : "Sisesta veebihaagi nimi", + "Entities" : "Olemid", + "Entity Information" : "Olemi teave", + "Entity deleted successfully" : "Olem kustutati edukalt", + "Entity not found" : "Olemit ei leitud", + "Entries to be deleted:" : "Kustutatavad kirjed:", + "Enum Issue" : "Loendi probleem", + "Enum constraint is missing" : "Loendi kitsendus puudub", + "Error" : "Viga", + "Error Details" : "Vea üksikasjad", + "Error Information" : "Vea teave", + "Error Message" : "Veateade", + "Error loading application" : "Viga rakenduse laadimisel", + "Error loading audit trails" : "Viga auditijälgede laadimisel", + "Error loading entity" : "Viga olemi laadimisel", + "Error loading search trails" : "Viga otsingujälgede laadimisel", + "Estimated Batches:" : "Hinnanguline partiide arv:", + "Estimated Cost:" : "Hinnanguline kulu:", + "Estimated Duration:" : "Hinnanguline kestus:", + "Event" : "Sündmus", + "Event Property for Payload" : "Sündmuse atribuut kasuliku koorma jaoks", + "Events" : "Sündmused", + "Example Value" : "Näidisväärtus", + "Exclusive Maximum" : "Välistav maksimum", + "Exclusive Minimum" : "Välistav miinimum", + "Execution Mode" : "Täitmisrežiim", + "Execution Time" : "Täitmisaeg", + "Execution Time Range" : "Täitmisaja vahemik", + "Existing Improvements" : "Olemasolevad parendused", + "Exponential" : "Eksponentsiaalne", + "Export" : "Ekspordi", + "Export completed successfully" : "Eksport lõpetati edukalt", + "Export, view, or delete audit trails" : "Ekspordi, vaata või kustuta auditijälgi", + "Extend Schema" : "Laienda skeemi", + "Extended" : "Laiendatud", + "Extensions" : "Laiendused", + "Extract Pending Files" : "Eralda ootel failid", + "Extract properties from each object" : "Eralda igast objektist atribuudid", + "Extracted At" : "Eraldatud", + "Extraction Status" : "Eralduse olek", + "Facet configuration updated successfully" : "Tahkude konfiguratsioon uuendati edukalt", + "Facets discovered and configured successfully" : "Tahud avastati ja seadistati edukalt", + "Facets discovered successfully" : "Tahud avastati edukalt", + "Failed" : "Ebaõnnestus", + "Failed to analyze schema properties" : "Skeemi atribuutide analüüsimine ebaõnnestus", + "Failed to archive conversation" : "Vestluse arhiveerimine ebaõnnestus", + "Failed to calculate sizes" : "Suuruste arvutamine ebaõnnestus", + "Failed to clear collection" : "Kollektsiooni tühjendamine ebaõnnestus", + "Failed to copy changes" : "Muudatuste kopeerimine ebaõnnestus", + "Failed to copy data" : "Andmete kopeerimine ebaõnnestus", + "Failed to copy data to clipboard" : "Andmete kopeerimine lõikelauale ebaõnnestus", + "Failed to create conversation" : "Vestluse loomine ebaõnnestus", + "Failed to create or find project" : "Projekti loomine või leidmine ebaõnnestus", + "Failed to delete collection" : "Kollektsiooni kustutamine ebaõnnestus", + "Failed to delete conversation" : "Vestluse kustutamine ebaõnnestus", + "Failed to delete webhook" : "Veebihaagi kustutamine ebaõnnestus", + "Failed to download API specification" : "API spetsifikatsiooni allalaadimine ebaõnnestus", + "Failed to get SOLR field configuration" : "SOLR-välja konfiguratsiooni hankimine ebaõnnestus", + "Failed to get entity categories" : "Olemi kategooriate hankimine ebaõnnestus", + "Failed to get entity statistics" : "Olemi statistika hankimine ebaõnnestus", + "Failed to get entity types" : "Olemi tüüpide hankimine ebaõnnestus", + "Failed to load LLM configuration" : "LLM-konfiguratsiooni laadimine ebaõnnestus", + "Failed to load Nextcloud groups" : "Nextcloudi rühmade laadimine ebaõnnestus", + "Failed to load conversation" : "Vestluse laadimine ebaõnnestus", + "Failed to load entities" : "Olemite laadimine ebaõnnestus", + "Failed to load entity" : "Olemi laadimine ebaõnnestus", + "Failed to load files" : "Failide laadimine ebaõnnestus", + "Failed to load organisations" : "Organisatsioonide laadimine ebaõnnestus", + "Failed to load templates" : "Mallide laadimine ebaõnnestus", + "Failed to load webhooks" : "Veebihaakide laadimine ebaõnnestus", + "Failed to load workflows" : "Töövoogude laadimine ebaõnnestus", + "Failed to refresh database information" : "Andmebaasi teabe värskendamine ebaõnnestus", + "Failed to reindex collection" : "Kollektsiooni taasindekseerimine ebaõnnestus", + "Failed to rename conversation" : "Vestluse ümbernimetamine ebaõnnestus", + "Failed to restore conversation" : "Vestluse taastamine ebaõnnestus", + "Failed to retry extraction" : "Eralduse uuesti proovimine ebaõnnestus", + "Failed to retry webhook" : "Veebihaagi uuesti proovimine ebaõnnestus", + "Failed to save GitHub token" : "GitHubi loa salvestamine ebaõnnestus", + "Failed to save GitLab URL" : "GitLabi URL-i salvestamine ebaõnnestus", + "Failed to save GitLab token" : "GitLabi loa salvestamine ebaõnnestus", + "Failed to save additional feedback" : "Lisatagasiside salvestamine ebaõnnestus", + "Failed to save n8n configuration" : "n8n konfiguratsiooni salvestamine ebaõnnestus", + "Failed to save roles" : "Rollide salvestamine ebaõnnestus", + "Failed to save settings" : "Seadete salvestamine ebaõnnestus", + "Failed to save webhook" : "Veebihaagi salvestamine ebaõnnestus", + "Failed to send feedback" : "Tagasiside saatmine ebaõnnestus", + "Failed to test webhook" : "Veebihaagi testimine ebaõnnestus", + "Failed to update favorite status" : "Lemmikuoleku uuendamine ebaõnnestus", + "Failed to update schema properties" : "Skeemi atribuutide uuendamine ebaõnnestus", + "Failed to update view" : "Vaate uuendamine ebaõnnestus", + "Failed to update webhook" : "Veebihaagi uuendamine ebaõnnestus", + "Feedback recorded" : "Tagasiside salvestatud", + "Fewer sources (1-3):" : "Vähem allikaid (1-3):", + "Field" : "Väli", + "File" : "Fail", + "File Chunk Prediction" : "Failitükkide prognoos", + "File Collection" : "Failide kollektsioon", + "File Management" : "Failide haldus", + "File Name" : "Faili nimi", + "File Path" : "Faili tee", + "File Type Selection" : "Failitüübi valik", + "File Vectorization" : "Failide vektoriseerimine", + "File Warmup" : "Failide soojendus", + "File actions menu" : "Failitoimingute menüü", + "File anonymized successfully" : "Fail anonüümiti edukalt", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Failitükkide vektoriseerimine pole veel teostatud. Tükid on valmis ja salvestatud, kuid vektoriseerimisteenus on arendamisel.", + "File collection not configured" : "Failide kollektsioon pole seadistatud", + "File discovery completed" : "Failide avastamine lõpetatud", + "File extraction completed" : "Failide eraldus lõpetatud", + "File extraction queued" : "Failide eraldus järjekorda pandud", + "File indexed successfully" : "Fail indekseeriti edukalt", + "File is already anonymized" : "Fail on juba anonüümitud", + "File not found" : "Faili ei leitud", + "File settings updated successfully" : "Failiseaded uuendati edukalt", + "File sources" : "Failiallikad", + "File vectorization configuration saved successfully" : "Failide vektoriseerimise konfiguratsioon salvestati edukalt", + "File vectorization started. Check the statistics section for progress." : "Failide vektoriseerimine alustatud. Edenemist vaadake statistika jaotisest.", + "File warmup completed" : "Failide soojendus lõpetatud", + "Files" : "Failid", + "Files with Completed Extraction:" : "Lõpetatud eraldusega failid:", + "Files → fileCollection, Objects → objectCollection" : "Failid → fileCollection, Objektid → objectCollection", + "Filter Audit Trails" : "Filtreeri auditijälgi", + "Filter Deleted Items" : "Filtreeri kustutatud üksusi", + "Filter Objects" : "Filtreeri objekte", + "Filter Properties" : "Filtreeri atribuute", + "Filter Search Trails" : "Filtreeri otsingujälgi", + "Filter Statistics" : "Filtreeri statistikat", + "Filter and analyze search trail entries" : "Filtreeri ja analüüsi otsingujälje kirjeid", + "Filter and manage audit trail entries" : "Filtreeri ja halda auditijälje kirjeid", + "Filter and manage soft deleted items" : "Filtreeri ja halda pehmelt kustutatud üksusi", + "Filter by object ID" : "Filtreeri objekti ID järgi", + "Filter by search term" : "Filtreeri otsingusõna järgi", + "Filter by webhook" : "Filtreeri veebihaagi järgi", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filtriandmed laaditi automaatselt. Otsingu täpsustamiseks kasuta allolevaid filtreid.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtreeri veebihaagi käivitusi kasuliku koorma atribuutide järgi (üks rea kohta, vorming: võti: väärtus)", + "Filtered" : "Filtreeritud", + "Filters" : "Filtrid", + "Fireworks AI Chat Settings" : "Fireworks AI vestluse seaded", + "Fireworks AI Embedding Configuration" : "Fireworks AI välistuste konfiguratsioon", + "First" : "Esimene", + "Fixed" : "Parandatud", + "For chat and retrieval-augmented generation" : "Vestluse ja hankimisega täiendatud genereerimise jaoks", + "For vector embeddings and semantic search" : "Vektorvälistuste ja semantilise otsingu jaoks", + "Format" : "Vorming", + "Format Issue" : "Vormingu probleem", + "Format constraint is missing" : "Vormingu kitsendus puudub", + "From Date" : "Alates kuupäevast", + "From date" : "Alates kuupäevast", + "Full data copied to clipboard" : "Kõik andmed kopeeriti lõikelauale", + "General Issue" : "Üldine probleem", + "Generate recommendations and confidence scores" : "Loo soovitused ja usaldusskoorid", + "Generate vectors immediately when new objects are created" : "Loo vektorid kohe, kui uued objektid luuakse", + "GitHub token is valid" : "GitHubi luba on kehtiv", + "GitHub token removed successfully" : "GitHubi luba eemaldati edukalt", + "GitHub token saved successfully" : "GitHubi luba salvestati edukalt", + "GitLab URL saved successfully" : "GitLabi URL salvestati edukalt", + "GitLab token is valid" : "GitLabi luba on kehtiv", + "GitLab token saved successfully" : "GitLabi luba salvestati edukalt", + "HTTP Method" : "HTTP meetod", + "HTTP method used to send webhook requests" : "Veebihaagi päringute saatmiseks kasutatav HTTP meetod", + "Headers" : "Päised", + "Health" : "Seisund", + "Heartbeat successful - connection kept alive" : "Pulss õnnestus - ühendus hoiti elus", + "Helpful" : "Kasulik", + "Hide Filters" : "Peida filtrid", + "Hide in forms" : "Peida vormidel", + "Hide in list view" : "Peida loendivaates", + "High" : "Kõrge", + "High Confidence" : "Kõrge usaldus", + "Host" : "Host", + "Hourly" : "Iga tund", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Kui sügavale läbida pesastatud objekti atribuute (1-20). Suuremad väärtused jäädvustavad rohkem detaile, kuid suurendavad vektori suurust.", + "How often to check for updates (1-168 hours)" : "Kui sageli uuendusi kontrollida (1-168 tundi)", + "How to handle retries for failed webhook deliveries" : "Kuidas käsitleda korduskatseid ebaõnnestunud veebihaagi kohaletoimetamise korral", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "IP-päringusagedusepiirangud puhastati edukalt", + "Identify properties not in the schema" : "Tuvasta atribuudid, mis ei ole skeemis", + "Immutable" : "Muutumatu", + "Import" : "Impordi", + "Import successful" : "Import õnnestus", + "Improved Property" : "Parendatud atribuut", + "In use" : "Kasutusel", + "Inactive" : "Mitteaktiivne", + "Include IDs and names of related objects for better contextual search" : "Kaasa seotud objektide ID-d ja nimed parema kontekstuaalse otsingu jaoks", + "Include related object references" : "Kaasa seotud objektide viited", + "Include schema and register metadata" : "Kaasa skeemi ja registri metaandmed", + "Initialization failed" : "Lähtestamine ebaõnnestus", + "Initialize Project" : "Lähtesta projekt", + "Inspect Fields" : "Kontrolli välju", + "Inspect Index" : "Kontrolli indeksit", + "Invalid" : "Sobimatu", + "Invalid batch size. Must be between 1 and 5000" : "Sobimatu partii suurus. Peab olema vahemikus 1 kuni 5000", + "Invalid field name provided" : "Esitati sobimatu väljanimi", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Sobimatu maxObjects. Peab olema 0 (kõik) või positiivne arv", + "Invalid:" : "Sobimatu:", + "Issue" : "Probleem", + "Items per page:" : "Üksusi lehekülje kohta:", + "Just now" : "Just nüüd", + "LLM Configuration" : "LLM-konfiguratsioon", + "LLM actions menu" : "LLM-toimingute menüü", + "LLM configuration saved successfully" : "LLM-konfiguratsioon salvestati edukalt", + "LLM features disabled" : "LLM-funktsioonid keelatud", + "LLM features enabled" : "LLM-funktsioonid lubatud", + "LLM must be enabled with an embedding provider configured" : "LLM peab olema lubatud ja välistuste pakkuja seadistatud", + "LLM settings updated successfully" : "LLM-seaded uuendati edukalt", + "Last" : "Viimane", + "Last Triggered" : "Viimati käivitatud", + "Length Range:" : "Pikkusevahemik:", + "Linear" : "Lineaarne", + "Load Advanced Filters" : "Laadi täpsemad filtrid", + "Load advanced filters with live data from your search index" : "Laadi täpsemad filtrid reaalajas andmetega teie otsinguindeksist", + "Load view" : "Laadi vaade", + "Loading ConfigSets..." : "ConfigSetide laadimine...", + "Loading advanced filters..." : "Täpsemate filtrite laadimine...", + "Loading agents..." : "Agentide laadimine...", + "Loading application details..." : "Rakenduse üksikasjade laadimine...", + "Loading applications..." : "Rakenduste laadimine...", + "Loading archived conversations..." : "Arhiveeritud vestluste laadimine...", + "Loading audit trails..." : "Auditijälgede laadimine...", + "Loading collections..." : "Kollektsioonide laadimine...", + "Loading configurations..." : "Konfiguratsioonide laadimine...", + "Loading conversation..." : "Vestluse laadimine...", + "Loading conversations..." : "Vestluste laadimine...", + "Loading deleted items..." : "Kustutatud üksuste laadimine...", + "Loading events..." : "Sündmuste laadimine...", + "Loading filter data..." : "Filtriandmete laadimine...", + "Loading groups..." : "Rühmade laadimine...", + "Loading log details..." : "Logi üksikasjade laadimine...", + "Loading registers..." : "Registrite laadimine...", + "Loading schemas..." : "Skeemide laadimine...", + "Loading search trails..." : "Otsingujälgede laadimine...", + "Loading sources..." : "Allikate laadimine...", + "Loading statistics..." : "Statistika laadimine...", + "Loading users..." : "Kasutajate laadimine...", + "Loading views..." : "Vaadete laadimine...", + "Loading..." : "Laadimine...", + "Local" : "Kohalik", + "Local Version" : "Kohalik versioon", + "Locked" : "Lukustatud", + "Locked:" : "Lukustatud:", + "Login successful" : "Sisselogimine õnnestus", + "Logs" : "Logid", + "Low" : "Madal", + "Low Confidence" : "Madal usaldus", + "Lucene Version:" : "Lucene'i versioon:", + "Magic table synchronized successfully" : "Maagiline tabel sünkrooniti edukalt", + "Manage Organisation Roles" : "Halda organisatsiooni rolle", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Halda SOLR-kollektsioone (andmehoidlaid) ja määra need objektidele ja failidele.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Halda oma kollektsioonide SOLR ConfigSette (konfiguratsioonimalle).", + "Manage and configure agents for automated tasks" : "Halda ja seadista agente automatiseeritud ülesannete jaoks", + "Manage and monitor file text extraction status" : "Halda ja jälgi failide tekstieralduse olekut", + "Manage and restore soft deleted items from your registers" : "Halda ja taasta oma registritest pehmelt kustutatud üksusi", + "Manage and view detected entities from files and objects" : "Halda ja vaata failidest ja objektidest tuvastatud olemeid", + "Manage document templates and themes" : "Halda dokumendimalle ja teemasid", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Halda n8n töövooge OpenRegisteri automatiseerimiseks. Töövood salvestatakse seadistatud projekti.", + "Manage webhooks for event-driven integrations" : "Halda veebihaake sündmustepõhiste lõimingute jaoks", + "Manage your applications and modules" : "Halda oma rakendusi ja mooduleid", + "Manage your data registers and their configurations" : "Halda oma andmeregistreid ja nende konfiguratsioone", + "Manage your data schemas and their properties" : "Halda oma andmeskeeme ja nende atribuute", + "Manage your data sources and their configurations" : "Halda oma andmeallikaid ja nende konfiguratsioone", + "Manage your saved search configurations" : "Halda oma salvestatud otsingukonfiguratsioone", + "Manage your system configurations and settings" : "Halda oma süsteemikonfiguratsioone ja seadeid", + "Managed" : "Hallatud", + "Max Files (0 = all)" : "Maksimaalne failide arv (0 = kõik)", + "Max Length" : "Maksimaalne pikkus", + "Max Length:" : "Maksimaalne pikkus:", + "Max Objects (0 = all)" : "Maksimaalne objektide arv (0 = kõik)", + "Max Retries" : "Maksimaalne korduskatsete arv", + "Max Shards/Node" : "Maksimaalne kildude arv sõlme kohta", + "Max execution time (ms)" : "Maksimaalne täitmisaeg (ms)", + "Max ms" : "Maks ms", + "Max result count" : "Maksimaalne tulemuste arv", + "Max results" : "Maksimaalne tulemuste arv", + "Maximum" : "Maksimum", + "Maximum Nesting Depth" : "Maksimaalne pesastamise sügavus", + "Maximum length constraint is missing" : "Maksimaalse pikkuse kitsendus puudub", + "Maximum length is too restrictive" : "Maksimaalne pikkus on liiga piirav", + "Maximum number of files to process. Set to 0 to process all files." : "Maksimaalne töödeldavate failide arv. Kõigi failide töötlemiseks määra väärtuseks 0.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maksimaalne töödeldavate objektide arv. Kõigi objektide töötlemiseks määra väärtuseks 0.", + "Maximum number of retry attempts for failed deliveries" : "Maksimaalne korduskatsete arv ebaõnnestunud kohaletoimetamiste korral", + "Maximum time to wait before committing changes" : "Maksimaalne ooteaeg enne muudatuste sissekandmist", + "Maximum tokens to generate" : "Maksimaalne genereeritavate lubade arv", + "Maximum value constraint is missing" : "Maksimaalse väärtuse kitsendus puudub", + "Maximum value is too restrictive" : "Maksimaalne väärtus on liiga piirav", + "Medium" : "Keskmine", + "Medium Confidence" : "Keskmine usaldus", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Keskmised päringud: otsingud mõningase filtreerimise või mitme parameetriga (nt kuupäevavahemikud, kindlad registrid/skeemid)", + "Members of selected groups can access this view" : "Valitud rühmade liikmed pääsevad sellele vaatele juurde", + "Members:" : "Liikmed:", + "Memory prediction calculated successfully" : "Mäluprognoos arvutati edukalt", + "Message content is required" : "Sõnumi sisu on nõutav", + "Message does not belong to this conversation" : "Sõnum ei kuulu sellesse vestlusse", + "Metadata Filters" : "Metaandmete filtrid", + "Method" : "Meetod", + "Min Length" : "Minimaalne pikkus", + "Min execution time (ms)" : "Minimaalne täitmisaeg (ms)", + "Min ms" : "Min ms", + "Min result count" : "Minimaalne tulemuste arv", + "Min results" : "Minimaalne tulemuste arv", + "Minimum" : "Miinimum", + "Minimum value constraint is missing" : "Minimaalse väärtuse kitsendus puudub", + "Minimum value is too restrictive" : "Minimaalne väärtus on liiga piirav", + "Missing object fields created successfully" : "Puuduvad objektiväljad loodi edukalt", + "Mode:" : "Režiim:", + "Model" : "Mudel", + "Model Name" : "Mudeli nimi", + "Modified" : "Muudetud", + "Monthly" : "Iga kuu", + "More sources (10-20):" : "Rohkem allikaid (10-20):", + "Most Active Objects" : "Kõige aktiivsemad objektid", + "Multiple Of" : "Kordne", + "Name" : "Nimi", + "Names cache warmup completed" : "Nimede vahemälu soojendus lõpetatud", + "New Collection Name" : "Uue kollektsiooni nimi", + "New Conversation" : "Uus vestlus", + "New Properties" : "Uued atribuudid", + "New Property" : "Uus atribuut", + "New Value" : "Uus väärtus", + "New users without specific organisation membership will be automatically added to this organisation" : "Uued kasutajad ilma kindla organisatsiooni liikmesuseta lisatakse automaatselt sellesse organisatsiooni", + "Next" : "Järgmine", + "No" : "Ei", + "No ConfigSets found" : "ConfigSette ei leitud", + "No GitHub token configured" : "Ühtegi GitHubi luba pole seadistatud", + "No GitHub token provided" : "Ühtegi GitHubi luba ei esitatud", + "No GitLab token provided" : "Ühtegi GitLabi luba ei esitatud", + "No Organisation" : "Organisatsioon puudub", + "No active filters" : "Aktiivseid filtreid pole", + "No activity data available" : "Tegevusandmeid pole saadaval", + "No agents available" : "Agente pole saadaval", + "No agents found" : "Agente ei leitud", + "No app store cache exists yet" : "Rakenduste poe vahemälu pole veel olemas", + "No applications are available." : "Rakendusi pole saadaval.", + "No applications found" : "Rakendusi ei leitud", + "No archived conversations" : "Arhiveeritud vestlusi pole", + "No audit trail entries found" : "Auditijälje kirjeid ei leitud", + "No changes recorded" : "Muudatusi ei salvestatud", + "No chunks to vectorize" : "Vektoriseeritavaid tükke pole", + "No collections found" : "Kollektsioone ei leitud", + "No configuration data" : "Konfiguratsiooniandmeid pole", + "No configurations are available." : "Konfiguratsioone pole saadaval.", + "No configurations found" : "Konfiguratsioone ei leitud", + "No configurations found for this application." : "Selle rakenduse jaoks konfiguratsioone ei leitud.", + "No conversations yet. Create a new one to get started!" : "Vestlusi pole veel. Alustamiseks looge uus!", + "No data available" : "Andmeid pole saadaval", + "No data available for chart" : "Diagrammi jaoks andmeid pole saadaval", + "No deleted items found" : "Kustutatud üksusi ei leitud", + "No deletion data available" : "Kustutamise andmeid pole saadaval", + "No description found" : "Kirjeldust ei leitud", + "No description provided" : "Kirjeldust ei esitatud", + "No entities detected in this file. Run text extraction first." : "Selles failis ei tuvastatud olemeid. Käivita esmalt tekstieraldus.", + "No entities found" : "Olemeid ei leitud", + "No entities have been detected yet" : "Ühtegi olemit pole veel tuvastatud", + "No events found" : "Sündmusi ei leitud", + "No expired audit trails found to clear" : "Aegunud auditijälgi puhastamiseks ei leitud", + "No facetable fields available. Select a register and schema to see available filters." : "Tahkudesse sobivaid välju pole saadaval. Saadaolevate filtrite vaatamiseks vali register ja skeem.", + "No files found" : "Faile ei leitud", + "No files have been extracted yet" : "Ühtegi faili pole veel eraldatud", + "No files to process" : "Töödeldavaid faile pole", + "No files to reindex" : "Taasindekseeritavaid faile pole", + "No filters are currently active. This will delete ALL audit trail entries!" : "Praegu pole ükski filter aktiivne. See kustutab KÕIK auditijälje kirjed!", + "No log entries found" : "Logikirjeid ei leitud", + "No logs are available for this configuration." : "Selle konfiguratsiooni jaoks logisid pole saadaval.", + "No logs are available for this source." : "Selle allika jaoks logisid pole saadaval.", + "No logs found" : "Logisid ei leitud", + "No mismatched fields found - SOLR schema is properly configured" : "Mittevastavaid välju ei leitud - SOLR-skeem on õigesti seadistatud", + "No objects found" : "Objekte ei leitud", + "No objects found to vectorize" : "Vektoriseeritavaid objekte ei leitud", + "No objects selected for deletion" : "Kustutamiseks pole objekte valitud", + "No objects to delete" : "Kustutatavaid objekte pole", + "No objects were permanently deleted" : "Ühtegi objekti ei kustutatud jäädavalt", + "No properties found" : "Atribuute ei leitud", + "No properties match your filters." : "Ükski atribuut ei vasta teie filtritele.", + "No purge date set" : "Puhastamise kuupäeva pole määratud", + "No register data available" : "Registriandmeid pole saadaval", + "No registers found" : "Registreid ei leitud", + "No registers found for this application." : "Selle rakenduse jaoks registreid ei leitud.", + "No relations found" : "Seoseid ei leitud", + "No request body available" : "Päringu keha pole saadaval", + "No response body available" : "Vastuse keha pole saadaval", + "No saved views yet. Create one in the Search tab!" : "Salvestatud vaateid pole veel. Looge üks otsingu vahekaardil!", + "No schema data available" : "Skeemiandmeid pole saadaval", + "No schema selected for exploration" : "Uurimiseks pole skeemi valitud", + "No schemas found" : "Skeeme ei leitud", + "No schemas found for this application." : "Selle rakenduse jaoks skeeme ei leitud.", + "No search terms data available" : "Otsingusõnade andmeid pole saadaval", + "No search trail entries found" : "Otsingujälje kirjeid ei leitud", + "No sources are available." : "Allikaid pole saadaval.", + "No sources found" : "Allikaid ei leitud", + "No templates found" : "Malle ei leitud", + "No templates have been created yet" : "Ühtegi malli pole veel loodud", + "No valid magic-mapped register+schema combinations found" : "Kehtivaid maagiliselt vastendatud registri ja skeemi kombinatsioone ei leitud", + "No views found. Create views first before configuring vectorization." : "Vaateid ei leitud. Enne vektoriseerimise seadistamist looge vaated.", + "No views match your search" : "Ükski vaade ei vasta teie otsingule", + "No webhooks found" : "Veebihaake ei leitud", + "No webhooks have been configured yet" : "Ühtegi veebihaaki pole veel seadistatud", + "No workflows found in this project. Create workflows in the n8n editor." : "Selles projektis töövooge ei leitud. Looge töövood n8n redaktoris.", + "None" : "Puudub", + "Not helpful" : "Pole kasulik", + "Not in use" : "Pole kasutusel", + "Number Constraints" : "Arvu kitsendused", + "Number of chunks to vectorize in one API call (1-100)." : "Ühe API-kõnega vektoriseeritavate tükkide arv (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Ühe API-kõnega vektoriseeritavate tükkide arv. Suurem = kiirem, kuid rohkem mälu. Soovitatav: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Igas partiis töödeldavate objektide arv (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Ühe API-kõnega vektoriseeritavate objektide arv. Suurem = kiirem, kuid rohkem mälu. Soovitatav: 10-50.", + "Numeric Range:" : "Arvuvahemik:", + "Object" : "Objekt", + "Object Collection" : "Objektide kollektsioon", + "Object Count Prediction" : "Objektide arvu prognoos", + "Object ID" : "Objekti ID", + "Object Management" : "Objektide haldus", + "Object Vectorization" : "Objektide vektoriseerimine", + "Object Warmup" : "Objektide soojendus", + "Object collection not configured" : "Objektide kollektsioon pole seadistatud", + "Object not found" : "Objekti ei leitud", + "Object permanently deleted" : "Objekt jäädavalt kustutatud", + "Object restored successfully" : "Objekt taastati edukalt", + "Object settings updated successfully" : "Objektiseaded uuendati edukalt", + "Object sources" : "Objektiallikad", + "Object unlocked successfully" : "Objekt avati edukalt", + "Object vectorization configuration saved successfully" : "Objektide vektoriseerimise konfiguratsioon salvestati edukalt", + "Object vectorized successfully" : "Objekt vektoriseeriti edukalt", + "Object:" : "Objekt:", + "Objects" : "Objektid", + "Objects Analyzed" : "Analüüsitud objektid", + "Objects Distribution" : "Objektide jaotus", + "Objects being analyzed" : "Analüüsitavad objektid", + "Objects by Register" : "Objektid registri järgi", + "Objects by Schema" : "Objektid skeemi järgi", + "Objects deletion completed successfully" : "Objektide kustutamine lõpetati edukalt", + "Objects in schema" : "Objektid skeemis", + "Objects to Process:" : "Töödeldavad objektid:", + "Objects to be analyzed" : "Analüüsitavad objektid", + "Objects to be deleted" : "Kustutatavad objektid", + "Objects to be validated" : "Valideeritavad objektid", + "Objects will be serialized as JSON text before vectorization" : "Objektid serialiseeritakse enne vektoriseerimist JSON-tekstina", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objektid kustutatakse pehmelt (märgitakse kustutatuks, kuid säilitatakse andmebaasis). Vajaduse korral saab neid hiljem taastada.", + "Old Value" : "Vana väärtus", + "Oldest Item (days)" : "Vanim üksus (päevades)", + "Ollama Chat Settings" : "Ollama vestluse seaded", + "Ollama Embedding Configuration" : "Ollama välistuste konfiguratsioon", + "Ollama URL" : "Ollama URL", + "Only With Changes" : "Ainult muudatustega", + "Open Register Objects" : "Open Registeri objektid", + "Open n8n Editor" : "Ava n8n redaktor", + "OpenAI Chat Settings" : "OpenAI vestluse seaded", + "OpenAI Embedding Configuration" : "OpenAI välistuste konfiguratsioon", + "OpenRegister Settings" : "OpenRegisteri seaded", + "Optional webhook secret for signature verification" : "Valikuline veebihaagi saladus allkirja kontrollimiseks", + "Order" : "Järjestus", + "Organisation" : "Organisatsioon", + "Organisation ID" : "Organisatsiooni ID", + "Organisation Members" : "Organisatsiooni liikmed", + "Organisation Statistics" : "Organisatsiooni statistika", + "Organisation created successfully" : "Organisatsioon loodi edukalt", + "Organisation settings saved successfully" : "Organisatsiooni seaded salvestati edukalt", + "Organisations" : "Organisatsioonid", + "Organization" : "Organisatsioon", + "Organization ID (Optional)" : "Organisatsiooni ID (valikuline)", + "Orphaned Items" : "Orvuks jäänud üksused", + "Owner" : "Omanik", + "Owner:" : "Omanik:", + "Parallel Mode (Faster, more resource intensive)" : "Paralleelrežiim (kiirem, ressursimahukam)", + "Parallel:" : "Paralleelne:", + "Password" : "Parool", + "Password for SOLR authentication (optional)" : "Parool SOLR-autentimiseks (valikuline)", + "Path" : "Tee", + "Path in repository" : "Tee hoidlas", + "Path where the configuration file will be saved in the repository" : "Tee, kuhu konfiguratsioonifail hoidlas salvestatakse", + "Path where the register OAS file will be saved in the repository" : "Tee, kuhu registri OAS-fail hoidlas salvestatakse", + "Pattern (regex)" : "Muster (regulaaravaldis)", + "Pattern Issue" : "Mustri probleem", + "Pattern constraint is missing" : "Mustri kitsendus puudub", + "Patterns:" : "Mustrid:", + "Pending" : "Ootel", + "Permanent Delete Mode" : "Jäädava kustutamise režiim", + "Permanently Delete" : "Kustuta jäädavalt", + "Permanently delete already soft-deleted objects" : "Kustuta jäädavalt juba pehmelt kustutatud objektid", + "Permanently delete this conversation?" : "Kas kustutada see vestlus jäädavalt?", + "Person" : "Isik", + "Personal Data" : "Isikuandmed", + "Phone" : "Telefon", + "Photos" : "Fotod", + "Please create an agent in the" : "Palun looge agent menüüs", + "Please select an agent to continue" : "Jätkamiseks valige agent", + "Please select which register and schema to use for the new object" : "Palun valige, millist registrit ja skeemi uue objekti jaoks kasutada", + "Please try again later." : "Palun proovige hiljem uuesti.", + "Please wait while we fetch your agents." : "Palun oodake, kuni laadime teie agendid.", + "Please wait while we fetch your applications." : "Palun oodake, kuni laadime teie rakendused.", + "Please wait while we fetch your configurations." : "Palun oodake, kuni laadime teie konfiguratsioonid.", + "Please wait while we fetch your deleted items." : "Palun oodake, kuni laadime teie kustutatud üksused.", + "Please wait while we fetch your sources." : "Palun oodake, kuni laadime teie allikad.", + "Polls" : "Küsitlused", + "Popular Search Terms" : "Populaarsed otsingusõnad", + "Port" : "Port", + "Prerequisites" : "Eeltingimused", + "Press Enter to send, Shift+Enter for new line" : "Saatmiseks vajutage Enter, uue rea jaoks Shift+Enter", + "Previous" : "Eelmine", + "Private" : "Privaatne", + "Processes chunks in batches with simulated parallelism." : "Töötleb tükke partiidena simuleeritud paralleelsusega.", + "Processes file chunks sequentially (safest)." : "Töötleb failitükke järjestikku (kõige turvalisem).", + "Processes objects in chunks with simulated parallelism." : "Töötleb objekte tükkidena simuleeritud paralleelsusega.", + "Processes objects sequentially (safest)." : "Töötleb objekte järjestikku (kõige turvalisem).", + "Processing" : "Töötlemine", + "Processing Limits" : "Töötlemise piirangud", + "Processing..." : "Töötlemine...", + "Project initialized successfully" : "Projekt lähtestati edukalt", + "Project not found. Please initialize first." : "Projekti ei leitud. Palun lähtestage esmalt.", + "Properties" : "Atribuudid", + "Property Behaviors" : "Atribuudi käitumised", + "Property Configuration:" : "Atribuudi konfiguratsioon:", + "Property Filters" : "Atribuudifiltrid", + "Property Title" : "Atribuudi pealkiri", + "Property Type" : "Atribuudi tüüp", + "Property can be improved" : "Atribuuti saab parendada", + "Protected" : "Kaitstud", + "Provider" : "Pakkuja", + "Provider is required for testing" : "Testimiseks on pakkuja nõutav", + "Public" : "Avalik", + "Public View" : "Avalik vaade", + "Public views can be accessed by anyone in the system" : "Avalikele vaadetele pääseb juurde igaüks süsteemis", + "Publish" : "Avalda", + "Publish Objects" : "Avalda objektid", + "Publish all objects in this schema" : "Avalda kõik selle skeemi objektid", + "Published" : "Avaldatud", + "Published:" : "Avaldatud:", + "Publishing..." : "Avaldamine...", + "Purge" : "Puhasta", + "Purge Date" : "Puhastamise kuupäev", + "Query Complexity Distribution" : "Päringu keerukuse jaotus", + "Query parameter is required" : "Päringu parameeter on nõutav", + "RAG" : "RAG", + "RAG Configuration" : "RAG-konfiguratsioon", + "Rate limits cleared successfully" : "Päringusagedusepiirangud puhastati edukalt", + "Raw Changes Data" : "Toormuudatuste andmed", + "Re-vectorize on object update" : "Vektoriseeri uuesti objekti uuendamisel", + "Read" : "Loe", + "Recommendations:" : "Soovitused:", + "Recommended Type:" : "Soovitatav tüüp:", + "Recommended: 5 sources" : "Soovitatav: 5 allikat", + "Refresh" : "Värskenda", + "Refresh Data" : "Värskenda andmeid", + "Refresh Stats" : "Värskenda statistikat", + "Refresh Workflows" : "Värskenda töövooge", + "Refresh dashboard" : "Värskenda töölauda", + "Refresh database info" : "Värskenda andmebaasi teavet", + "Register" : "Register", + "Register ID" : "Registri ID", + "Register Statistics" : "Registri statistika", + "Register Totals" : "Registri kogusummad", + "Register depublished successfully" : "Registri avaldamine tühistati edukalt", + "Register not found" : "Registrit ei leitud", + "Register objects deletion completed successfully" : "Registri objektide kustutamine lõpetati edukalt", + "Register published successfully" : "Register avaldati edukalt", + "Register/Schema Usage" : "Registri/skeemi kasutus", + "Registers" : "Registrid", + "Reindex" : "Taasindekseeri", + "Reindex completed" : "Taasindekseerimine lõpetatud", + "Reindex completed successfully" : "Taasindekseerimine lõpetati edukalt", + "Reindex failed" : "Taasindekseerimine ebaõnnestus", + "Relations" : "Seosed", + "Remote Version" : "Kaugversioon", + "Remove" : "Eemalda", + "Remove filter" : "Eemalda filter", + "Remove from favorites" : "Eemalda lemmikutest", + "Remove group" : "Eemalda rühm", + "Removed" : "Eemaldatud", + "Removed from favorites" : "Eemaldatud lemmikutest", + "Rename Conversation" : "Nimeta vestlus ümber", + "Rename conversation" : "Nimeta vestlus ümber", + "Replicas" : "Koopiad", + "Repository" : "Hoidla", + "Request Body" : "Päringu keha", + "Request Data" : "Päringu andmed", + "Request Details" : "Päringu üksikasjad", + "Request body must be JSON with a \"query\" field" : "Päringu keha peab olema JSON väljaga \"query\"", + "Request timeout in seconds" : "Päringu ajalõpp sekundites", + "Required field" : "Kohustuslik väli", + "Required status is inconsistent" : "Kohustuslik olek on ebajärjekindel", + "Rerun Search" : "Käivita otsing uuesti", + "Reset Changes" : "Lähtesta muudatused", + "Reset Filters" : "Lähtesta filtrid", + "Response Body" : "Vastuse keha", + "Response Details" : "Vastuse üksikasjad", + "Response Time:" : "Vastusaeg:", + "Restore" : "Taasta", + "Restore conversation" : "Taasta vestlus", + "Restore or permanently delete items" : "Taasta või kustuta üksused jäädavalt", + "Result Count Range" : "Tulemuste arvu vahemik", + "Results" : "Tulemused", + "Retrieve all objects for this schema" : "Hangi kõik selle skeemi objektid", + "Retry" : "Proovi uuesti", + "Retry Failed Extractions" : "Proovi ebaõnnestunud eraldusi uuesti", + "Retry Policy" : "Korduskatse poliitika", + "Retry completed" : "Korduskatse lõpetatud", + "Retrying..." : "Uuesti proovimine...", + "Risk Level" : "Riskitase", + "Roles updated successfully" : "Rollid uuendati edukalt", + "SOLR Connection Settings" : "SOLR-ühenduse seaded", + "SOLR Version:" : "SOLR-i versioon:", + "SOLR actions menu" : "SOLR-toimingute menüü", + "SOLR base path (usually /solr)" : "SOLR-i baastee (tavaliselt /solr)", + "SOLR is disabled" : "SOLR on keelatud", + "SOLR is not available or not configured" : "SOLR pole saadaval või pole seadistatud", + "SOLR logging disabled" : "SOLR-logimine keelatud", + "SOLR logging enabled" : "SOLR-logimine lubatud", + "SOLR search disabled" : "SOLR-otsing keelatud", + "SOLR search enabled" : "SOLR-otsing lubatud", + "SOLR server hostname or IP address" : "SOLR-serveri hostinimi või IP-aadress", + "SOLR server port number (optional, defaults to 8983)" : "SOLR-serveri pordinumber (valikuline, vaikimisi 8983)", + "SOLR setup completed successfully" : "SOLR-i seadistamine lõpetati edukalt", + "SOLR setup failed" : "SOLR-i seadistamine ebaõnnestus", + "SOLR setup failed - check logs" : "SOLR-i seadistamine ebaõnnestus - kontrollige logisid", + "Sample Values:" : "Näidisväärtused:", + "Save" : "Salvesta", + "Save Configuration" : "Salvesta konfiguratsioon", + "Save Connection Settings" : "Salvesta ühenduse seaded", + "Save Roles" : "Salvesta rollid", + "Save Settings" : "Salvesta seaded", + "Save current search as view" : "Salvesta praegune otsing vaatena", + "Saved Views" : "Salvestatud vaated", + "Saving..." : "Salvestamine...", + "Schema" : "Skeem", + "Schema ID" : "Skeemi ID", + "Schema Name" : "Skeemi nimi", + "Schema Statistics" : "Skeemi statistika", + "Schema depublished successfully" : "Skeemi avaldamine tühistati edukalt", + "Schema objects deletion completed successfully" : "Skeemi objektide kustutamine lõpetati edukalt", + "Schema published successfully" : "Skeem avaldati edukalt", + "Schema successfully created" : "Skeem loodi edukalt", + "Schema successfully updated" : "Skeem uuendati edukalt", + "Schemas" : "Skeemid", + "Scheme" : "Skeem", + "Search / Views" : "Otsing / vaated", + "Search Activity" : "Otsingu tegevus", + "Search Analytics" : "Otsinguanalüütika", + "Search Entities" : "Otsi olemeid", + "Search Files" : "Otsi faile", + "Search GitHub" : "Otsi GitHubist", + "Search GitLab" : "Otsi GitLabist", + "Search Method" : "Otsingumeetod", + "Search Term" : "Otsingusõna", + "Search Trail Details" : "Otsingujälje üksikasjad", + "Search Trail Management" : "Otsingujälgede haldus", + "Search Trail Statistics" : "Otsingujälgede statistika", + "Search Trails" : "Otsingujäljed", + "Search Views" : "Otsi vaateid", + "Search Webhooks" : "Otsi veebihaake", + "Search by file name or path" : "Otsi faili nime või tee järgi", + "Search by name or URL" : "Otsi nime või URL-i järgi", + "Search by value" : "Otsi väärtuse järgi", + "Search in Files" : "Otsi failidest", + "Search in Objects" : "Otsi objektidest", + "Search property names..." : "Otsi atribuutide nimesid...", + "Search trail deletion not implemented yet" : "Otsingujälje kustutamine pole veel teostatud", + "Search views..." : "Otsi vaateid...", + "Secret" : "Saladus", + "Secret key for HMAC signature generation (optional)" : "Salajane võti HMAC-allkirja genereerimiseks (valikuline)", + "Select AI Agent" : "Vali tehisintellekti agent", + "Select All" : "Vali kõik", + "Select ConfigSet" : "Vali ConfigSet", + "Select File Types to Vectorize:" : "Vali vektoriseeritavad failitüübid:", + "Select HTTP method" : "Vali HTTP meetod", + "Select Register and Schema" : "Vali register ja skeem", + "Select Views to Vectorize:" : "Vali vektoriseeritavad vaated:", + "Select a Nextcloud group to add" : "Vali lisatav Nextcloudi rühm", + "Select a branch" : "Vali haru", + "Select a model or type a custom model name" : "Vali mudel või sisesta kohandatud mudeli nimi", + "Select a repository" : "Vali hoidla", + "Select a repository you have write access to" : "Vali hoidla, millele teil on kirjutusõigus", + "Select an AI Agent" : "Vali tehisintellekti agent", + "Select an AI agent to begin chatting with your data." : "Vali tehisintellekti agent, et alustada oma andmetega vestlust.", + "Select backend" : "Vali taustasüsteem", + "Select chat model" : "Vali vestlusmudel", + "Select collection for files" : "Vali kollektsioon failide jaoks", + "Select collection for objects" : "Vali kollektsioon objektide jaoks", + "Select default organisation" : "Vali vaikimisi organisatsioon", + "Select event to listen to..." : "Vali sündmus, mida kuulata...", + "Select model" : "Vali mudel", + "Select options..." : "Vali valikud...", + "Select period" : "Vali periood", + "Select property to send as payload" : "Vali atribuut, mis saadetakse kasuliku koormana", + "Select provider" : "Vali pakkuja", + "Select registers and schemas to save a view" : "Vali registrid ja skeemid vaate salvestamiseks", + "Select retry policy" : "Vali korduskatse poliitika", + "Select the branch to publish to" : "Vali haru, kuhu avaldada", + "Select the event this webhook should listen to" : "Vali sündmus, mida see veebihaak peaks kuulama", + "Select views to vectorize:" : "Vali vektoriseeritavad vaated:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Vali, millised Nextcloudi rühmad on selle organisatsiooni jaoks saadaval. Nendesse rühmadesse kuuluvatel kasutajatel on juurdepääs organisatsiooni ressurssidele.", + "Select which data views the AI can search" : "Vali, milliseid andmevaateid saab tehisintellekt otsida", + "Select which property from the event should be used as the webhook payload data" : "Vali, millist sündmuse atribuuti tuleks kasutada veebihaagi kasuliku koorma andmetena", + "Select which tools the AI can use to perform actions" : "Vali, milliseid tööriistu saab tehisintellekt toimingute tegemiseks kasutada", + "Selected" : "Valitud", + "Selected Groups" : "Valitud rühmad", + "Selected audit trails deleted successfully" : "Valitud auditijäljed kustutati edukalt", + "Selected search trails deleted successfully" : "Valitud otsingujäljed kustutati edukalt", + "Selected users can access this view" : "Valitud kasutajad pääsevad sellele vaatele juurde", + "Send additional feedback" : "Saada lisatagasiside", + "Send as CloudEvent" : "Saada CloudEventina", + "Sensitive PII" : "Tundlikud isikuandmed", + "Serial Mode (Safer, slower)" : "Järjestikrežiim (turvalisem, aeglasem)", + "Serial:" : "Järjestikune:", + "Server Configuration" : "Serveri konfiguratsioon", + "Server Information" : "Serveri teave", + "Settings" : "Seaded", + "Shards" : "Killud", + "Share" : "Jaga", + "Share with Groups" : "Jaga rühmadega", + "Share with Users" : "Jaga kasutajatega", + "Show Filters" : "Näita filtreid", + "Show less" : "Näita vähem", + "Show only entries with changes" : "Näita ainult muudatustega kirjeid", + "Simple" : "Lihtne", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Lihtsad päringud: põhilised tekstiotsingud minimaalsete parameetritega (nt üks otsingusõna, ilma täpsemate filtriteta)", + "Size" : "Suurus", + "Slug" : "Nälk", + "Soft Delete Mode" : "Pehme kustutamise režiim", + "Soft Deleted Items" : "Pehmelt kustutatud üksused", + "SolrCloud mode enabled" : "SolrCloud režiim lubatud", + "Source" : "Allikas", + "Sources" : "Allikad", + "Sources:" : "Allikad:", + "Standalone SOLR mode" : "Eraldiseisev SOLR-režiim", + "Start Conversation" : "Alusta vestlust", + "Start Vectorization" : "Alusta vektoriseerimist", + "Start a conversation" : "Alusta vestlust", + "Starting names cache warmup..." : "Nimede vahemälu soojenduse alustamine...", + "Starting..." : "Alustamine...", + "Statistics" : "Statistika", + "Stats calculation not yet implemented" : "Statistika arvutamine pole veel teostatud", + "Status" : "Olek", + "Status Code" : "Olekukood", + "String Constraints" : "Stringi kitsendused", + "Success" : "Õnnestus", + "Success Rate" : "Õnnestumise määr", + "Success Status" : "Õnnestumise olek", + "Successful" : "Õnnestus", + "Successfully joined organisation" : "Organisatsiooniga liitumine õnnestus", + "Sync Table" : "Sünkrooni tabel", + "Synchronization Settings" : "Sünkroonimise seaded", + "System Default" : "Süsteemi vaikeväärtus", + "Technical Description" : "Tehniline kirjeldus", + "Technical description for developers and administrators" : "Tehniline kirjeldus arendajatele ja administraatoritele", + "Temperature" : "Temperatuur", + "Templates" : "Mallid", + "Test" : "Testi", + "Test Chat" : "Testi vestlust", + "Test Connection" : "Testi ühendust", + "Test Embedding" : "Testi välistust", + "Test endpoint executed successfully" : "Testi lõpp-punkt täideti edukalt", + "Test webhook delivered successfully" : "Testveebihaak toimetati edukalt kohale", + "Test webhook delivery failed" : "Testveebihaagi kohaletoimetamine ebaõnnestus", + "Test webhook sent successfully" : "Testveebihaak saadeti edukalt", + "Testing..." : "Testimine...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Tekstitükid luuakse failide eralduse käigus ja salvestatakse andmebaasi. Vektoriseerimine loeb need eelnevalt tükeldatud failid ja teisendab need välistusteks.", + "Text extracted successfully" : "Tekst eraldati edukalt", + "Text extraction disabled" : "Tekstieraldus keelatud", + "The URL where webhook events will be sent" : "URL, kuhu veebihaagi sündmused saadetakse", + "The requested conversation does not exist" : "Soovitud vestlust pole olemas", + "The saved view and all its search configuration" : "Salvestatud vaade ja kogu selle otsingukonfiguratsioon", + "There are no audit trail entries matching your current filters." : "Teie praegustele filtritele vastavaid auditijälje kirjeid pole.", + "There are no deleted items matching your current filters." : "Teie praegustele filtritele vastavaid kustutatud üksusi pole.", + "There are no search trail entries matching your current filters." : "Teie praegustele filtritele vastavaid otsingujälje kirjeid pole.", + "There are no webhook log entries matching your filters." : "Teie filtritele vastavaid veebihaagi logikirjeid pole.", + "This action cannot be undone." : "Seda tegevust ei saa tagasi võtta.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Seda tegevust ei saa tagasi võtta. Veenduge, et ükski kollektsioon seda ConfigSeti ei kasuta.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Sellel agendil pole seadistatavaid vaateid ega tööriistu, kuid saate siiski allpool RAG-seadeid kohandada.", + "This analysis may take some time" : "See analüüs võib veidi aega võtta", + "This audit trail entry does not contain any change information." : "See auditijälje kirje ei sisalda muudatuste teavet.", + "This configuration has no data defined." : "Sellele konfiguratsioonile pole andmeid määratud.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "See lõpp-punkt on iganenud. Kasuta selle asemel tükipõhiseid lõpp-punkte.", + "This entity has no relations to objects or files" : "Sellel olemil pole seoseid objektide ega failidega", + "This is a test webhook from OpenRegister" : "See on testveebihaak OpenRegisterist", + "This schema must use Magic Table configuration to sync" : "See skeem peab sünkroonimiseks kasutama maagilise tabeli konfiguratsiooni", + "This source has no associated registers." : "Sellel allikal pole seotud registreid.", + "This will copy the _default ConfigSet with the new name" : "See kopeerib ConfigSeti _default uue nimega", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "See kustutab andmebaasist jäädavalt KÕIK välistused (vektorid). Peate kõik objektid ja failid uuesti vektoriseerima. Seda tegevust ei saa tagasi võtta.\\n\\nKas olete kindel, et soovite jätkata?", + "This will permanently delete:" : "See kustutab jäädavalt:", + "Timeout (seconds)" : "Ajalõpp (sekundites)", + "Timestamp" : "Ajatempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Näpunäide: lubage ainult vaated, mis vajavad semantilist otsingut, et minimeerida välistuste kulusid. Lihtsad otsingutabelid vajavad harva vektoriseerimist.", + "Title" : "Pealkiri", + "To Date" : "Kuni kuupäevani", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Tüki suuruse või strateegia muutmiseks minge jaotisse Failide konfiguratsioon → Töötlemise piirangud.", + "To change the embedding provider or model, go to LLM Configuration." : "Välistuste pakkuja või mudeli muutmiseks minge jaotisse LLM-konfiguratsioon.", + "To date" : "Kuni kuupäevani", + "Toggle search sidebar" : "Lülita otsingu külgriba", + "Tools" : "Tööriistad", + "Top Deleters" : "Aktiivseimad kustutajad", + "Total" : "Kokku", + "Total Audit Trails" : "Auditijälgi kokku", + "Total Chunks Available:" : "Saadaolevaid tükke kokku:", + "Total Deleted Items" : "Kustutatud üksusi kokku", + "Total Members" : "Liikmeid kokku", + "Total Objects" : "Objekte kokku", + "Total Objects in Database:" : "Objekte andmebaasis kokku:", + "Total Organisations" : "Organisatsioone kokku", + "Total Results" : "Tulemusi kokku", + "Total Searches" : "Otsinguid kokku", + "Total Size" : "Kogusuurus", + "Total Storage" : "Kogusalvestusruum", + "Total:" : "Kokku:", + "Totals" : "Kogusummad", + "Type" : "Tüüp", + "Type Issue" : "Tüübi probleem", + "Type Variations:" : "Tüübi variatsioonid:", + "Type must be \"positive\" or \"negative\"" : "Tüüp peab olema \"positive\" või \"negative\"", + "Type to search for groups" : "Sisesta rühmade otsimiseks", + "Type to search for users" : "Sisesta kasutajate otsimiseks", + "Type to search groups..." : "Sisesta rühmade otsimiseks...", + "Type to search users..." : "Sisesta kasutajate otsimiseks...", + "URL" : "URL", + "URL where Ollama is running" : "URL, kus Ollama töötab", + "UUID:" : "UUID:", + "Unchanged" : "Muutmata", + "Unique Search Terms" : "Unikaalsed otsingusõnad", + "Unique Terms" : "Unikaalsed terminid", + "Unknown" : "Tundmatu", + "Unknown Register" : "Tundmatu register", + "Unknown Schema" : "Tundmatu skeem", + "Untitled View" : "Pealkirjata vaade", + "Update" : "Uuenda", + "Update Operations" : "Uuendamistoimingud", + "Update configuration: ..." : "Uuenda konfiguratsiooni: ...", + "Update register OAS: ..." : "Uuenda registri OAS-i: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Uuenda vektoreid, kui objekti andmed muutuvad (soovitatav täpse otsingu jaoks)", + "Updated" : "Uuendatud", + "Updated At" : "Uuendatud", + "Updated:" : "Uuendatud:", + "Uptime:" : "Tööaeg:", + "Usage Count" : "Kasutuste arv", + "Use SolrCloud with Zookeeper for distributed search" : "Kasuta hajusotsingu jaoks SolrCloudi koos Zookeeperiga", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Kasuta filtreid auditijälje kirjete kitsendamiseks registri, skeemi, tegevuse tüübi, kasutaja, kuupäevavahemiku või objekti ID järgi.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Kasuta filtreid kustutatud üksuste kitsendamiseks registri, skeemi, kustutamise kuupäeva või kustutaja järgi.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Kasuta filtreid otsingujälje kirjete kitsendamiseks registri, skeemi, õnnestumise oleku, kasutaja, kuupäevavahemiku, otsingusõnade või jõudlusmõõdikute järgi.", + "Used By Collections" : "Kasutavad kollektsioonid", + "User" : "Kasutaja", + "User Agent Statistics" : "Kasutajaagendi statistika", + "User Description" : "Kasutaja kirjeldus", + "User rate limits cleared successfully" : "Kasutaja päringusagedusepiirangud puhastati edukalt", + "User removed successfully" : "Kasutaja eemaldati edukalt", + "User-friendly description shown in forms and help text" : "Kasutajasõbralik kirjeldus, mis kuvatakse vormidel ja abitekstis", + "Username" : "Kasutajanimi", + "Username for SOLR authentication (optional)" : "Kasutajanimi SOLR-autentimiseks (valikuline)", + "Users" : "Kasutajad", + "Using Pre-Generated Chunks" : "Eelnevalt loodud tükkide kasutamine", + "Validate" : "Valideeri", + "Validate Objects" : "Valideeri objektid", + "Validation completed successfully" : "Valideerimine lõpetati edukalt", + "Validation failed" : "Valideerimine ebaõnnestus", + "Value" : "Väärtus", + "Vector Dimensions" : "Vektori mõõtmed", + "Vector Search Backend" : "Vektorotsingu taustasüsteem", + "Vector field: _embedding_" : "Vektoriväli: _embedding_", + "Vectorization Triggers" : "Vektoriseerimise käivitajad", + "Vectorize All Files" : "Vektoriseeri kõik failid", + "Vectorize All Objects" : "Vektoriseeri kõik objektid", + "Vectorize all file types" : "Vektoriseeri kõik failitüübid", + "Vectorize all views" : "Vektoriseeri kõik vaated", + "Vectorize on object creation" : "Vektoriseeri objekti loomisel", + "Vectors will be stored in your existing object and file collections" : "Vektorid salvestatakse teie olemasolevatesse objektide ja failide kollektsioonidesse", + "Version" : "Versioon", + "Very High" : "Väga kõrge", + "View API Docs" : "Vaata API dokumentatsiooni", + "View Changes" : "Vaata muudatusi", + "View Details" : "Vaata üksikasju", + "View Error" : "Vaata viga", + "View File" : "Vaata faili", + "View Full Details" : "Vaata kõiki üksikasju", + "View Logs" : "Vaata logisid", + "View Name" : "Vaate nimi", + "View Object" : "Vaata objekti", + "View Parameters" : "Vaate parameetrid", + "View Selection" : "Vaate valik", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Vaata ja analüüsi otsingujälje logisid täpsemate filtreerimis- ja analüütikavõimalustega", + "View and analyze system audit trails with advanced filtering capabilities" : "Vaata ja analüüsi süsteemi auditijälgi täpsemate filtreerimisvõimalustega", + "View deleted successfully" : "Vaade kustutati edukalt", + "View entity details and manage relations" : "Vaata olemi üksikasju ja halda seoseid", + "View name" : "Vaate nimi", + "View name is required" : "Vaate nimi on nõutav", + "View saved successfully!" : "Vaade salvestati edukalt!", + "View search analytics and manage search logs" : "Vaata otsinguanalüütikat ja halda otsingulogisid", + "View successfully deleted" : "Vaade kustutati edukalt", + "View successfully updated" : "Vaade uuendati edukalt", + "View updated successfully!" : "Vaade uuendati edukalt!", + "View webhook delivery logs and filter by webhook" : "Vaata veebihaagi kohaletoimetamise logisid ja filtreeri veebihaagi järgi", + "Views" : "Vaated", + "Visible to users" : "Nähtav kasutajatele", + "Wait for Response" : "Oota vastust", + "Wait for webhook response before continuing (required for request/response flows)" : "Oota enne jätkamist veebihaagi vastust (nõutav päringu/vastuse voogude korral)", + "Warmup Names Cache" : "Soojenda nimede vahemälu", + "Warning" : "Hoiatus", + "Warning:" : "Hoiatus:", + "Webhook" : "Veebihaak", + "Webhook Log Details" : "Veebihaagi logi üksikasjad", + "Webhook Logs" : "Veebihaagi logid", + "Webhook created successfully" : "Veebihaak loodi edukalt", + "Webhook deleted" : "Veebihaak kustutatud", + "Webhook retry delivered successfully" : "Veebihaagi korduskatse toimetati edukalt kohale", + "Webhook retry delivery failed" : "Veebihaagi korduskatse kohaletoimetamine ebaõnnestus", + "Webhook updated" : "Veebihaak uuendatud", + "Webhook updated successfully" : "Veebihaak uuendati edukalt", + "Webhooks" : "Veebihaagid", + "Weekly" : "Iga nädal", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Pakenda veebihaagi kasulik koorem parema koostalitlusvõime jaoks CloudEventsi vormingusse", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Jah", + "You" : "Teie", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Saate luua uusi ConfigSette malli _default põhjal või laadida kohandatud ConfigSette otse oma SOLR-serverisse.", + "You do not have access to this conversation" : "Teil pole sellele vestlusele juurdepääsu", + "You do not have permission to delete this conversation" : "Teil pole õigust seda vestlust kustutada", + "You do not have permission to modify this conversation" : "Teil pole õigust seda vestlust muuta", + "You do not have permission to restore this conversation" : "Teil pole õigust seda vestlust taastada", + "You must be logged in to favorite views" : "Vaadete lemmikuks märkimiseks peate olema sisse logitud", + "You need an AI agent to start a conversation." : "Vestluse alustamiseks vajate tehisintellekti agenti.", + "Your Fireworks AI API key. Get one at" : "Teie Fireworks AI API võti. Hankige see aadressilt", + "Your OpenAI API key. Get one at" : "Teie OpenAI API võti. Hankige see aadressilt", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Teie tagasiside on salvestatud. Soovi korral saate siia lisada täiendavaid üksikasju...", + "Zookeeper Hosts" : "Zookeeperi hostid", + "Zookeeper Port" : "Zookeeperi port", + "Zookeeper Settings (SolrCloud)" : "Zookeeperi seaded (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeperi ühendusstring SolrCloudi jaoks", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeperi pordinumber (valikuline, vaikimisi 2181)", + "chunk" : "tükk", + "chunks" : "tükid", + "chunks processed" : "tükki töödeldud", + "configuration(s)" : "konfiguratsioon(id)", + "e.g., lib/Settings/config.json" : "nt lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "nt lib/Settings/register.json", + "failed" : "ebaõnnestus", + "file" : "fail", + "files" : "failid", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "liikmed", + "menu or contact someone with permission to create agents." : "või võtke ühendust kellegagi, kellel on õigus agente luua.", + "messages" : "sõnumid", + "n8n URL and API key are required" : "n8n URL ja API võti on nõutavad", + "n8n configuration saved successfully" : "n8n konfiguratsioon salvestati edukalt", + "n8n connection not configured" : "n8n ühendus pole seadistatud", + "n8n connection successful" : "n8n ühendus õnnestus", + "n8n connection test successful" : "n8n ühenduse test õnnestus", + "n8n integration disabled" : "n8n lõiming keelatud", + "n8n integration enabled" : "n8n lõiming lubatud", + "n8n project initialized successfully" : "n8n projekt lähtestati edukalt", + "n8n settings saved successfully" : "n8n seaded salvestati edukalt", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objektid", + "objects processed" : "objekti töödeldud", + "or pick a range" : "või vali vahemik", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "pakub head tasakaalu kiiruse ja täpsuse vahel.", + "register(s)" : "register(id)", + "register(s) selected" : "register(it) valitud", + "required" : "nõutav", + "results" : "tulemused", + "schema(s)" : "skeem(id)", + "schema(s) selected" : "skeem(e) valitud", + "sk-..." : "sk-...", + "this application" : "see rakendus", + "to" : "kuni", + "vectorized" : "vektoriseeritud", + "views selected" : "vaadet valitud", + "ℹ️ Current Configuration" : "ℹ️ Praegune konfiguratsioon", + "⚡ Batch Processing" : "⚡ Partiitöötlus", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Põhjalik kontekst ✓ Vähem tõenäoline detailide kaotamine ✗ Aeglasemad vastused ✗ Võib sisaldada vähem asjakohast teavet", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Kiiremad vastused ✓ Fokusseeritumad vastused ✗ Võib kaotada asjakohast teavet", + "✨ AI Features" : "✨ Tehisintellekti funktsioonid", + "All search trails cleared successfully" : "Kõik otsingujäljed puhastati edukalt", + "Multiple search trail deletion not implemented yet" : "Mitme otsingujälje kustutamine pole veel teostatud", + "No expired search trails found to clear" : "Aegunud otsingujälgi puhastamiseks ei leitud", + "Missing message" : "Sõnum puudub", + "message content is required" : "sõnumi sisu on nõutav", + "Missing conversation or agentUuid" : "Vestlus või agentUuid puudub", + "Access denied" : "Juurdepääs keelatud", + "Conversation not found" : "Vestlust ei leitud", + "AI service not configured" : "Tehisintellekti teenus pole seadistatud", + "Failed to process message" : "Sõnumi töötlemine ebaõnnestus", + "Missing conversationId" : "conversationId puudub", + "conversationId is required" : "conversationId on nõutav", + "Failed to fetch conversation history" : "Vestluse ajaloo hankimine ebaõnnestus", + "Failed to clear conversation" : "Vestluse tühjendamine ebaõnnestus", + "Invalid feedback type" : "Sobimatu tagasiside tüüp", + "type must be \"positive\" or \"negative\"" : "tüüp peab olema \"positive\" või \"negative\"", + "Message not found" : "Sõnumit ei leitud", + "Failed to save feedback" : "Tagasiside salvestamine ebaõnnestus", + "Failed to get chat statistics" : "Vestlusstatistika hankimine ebaõnnestus", + "Not Found" : "Ei leitud", + "Engine not found" : "Mootorit ei leitud", + "Consumer not found" : "Tarbijat ei leitud", + "Backend parameter is required" : "Taustasüsteemi parameeter on nõutav", + "Failed to get database information: %s" : "Andmebaasi teabe hankimine ebaõnnestus: %s", + "SOLR setup error: %s" : "SOLR-i seadistamise viga: %s", + "Reindex failed: %s" : "Taasindekseerimine ebaõnnestus: %s", + "Anonymized" : "Anonüümitud", + "Entities detected" : "Olemid tuvastatud", + "Extract Now" : "Eralda kohe", + "Extracted at" : "Eraldatud", + "Extraction" : "Eraldus", + "Failed to load extraction data" : "Eraldusandmete laadimine ebaõnnestus", + "Failed to load register data" : "Registriandmete laadimine ebaõnnestus", + "Failed to retrieve extraction status." : "Eralduse oleku hankimine ebaõnnestus.", + "Failed to retrieve objects for file." : "Faili objektide hankimine ebaõnnestus.", + "No extraction data available for this file" : "Selle faili jaoks eraldusandmeid pole saadaval", + "No register objects reference this file" : "Ükski registri objekt ei viita sellele failile", + "Not extracted" : "Pole eraldatud", + "Register Objects" : "Registri objektid", + "Risk level" : "Riskitase", + "Text chunks" : "Tekstitükid", + "Unknown error" : "Tundmatu viga", + "Very high" : "Väga kõrge", + "{title} in {register} / {schema}" : "{title} registris {register} / {schema}", + "Push Notifications" : "Tõuketeavitused", + "Real-time push notification status via notify_push" : "Reaalajas tõuketeavituse olek notify_push kaudu", + "Realtime push not available — the notify_push app is not installed" : "Reaalajas tõuge pole saadaval — rakendus notify_push pole paigaldatud", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Reaalajas uuenduste lubamiseks paigaldage Nextcloudi rakenduste poest rakendus notify_push.", + "Open Nextcloud App Store" : "Ava Nextcloudi rakenduste pood", + "notify_push is installed but not yet active" : "notify_push on paigaldatud, kuid pole veel aktiivne", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push on paigaldatud, kuid OpenRegister pole veel edukat tõuget kinnitanud. Aktiveerimiseks käivitage objekti salvestamine või kontrollige oma notify_push konfiguratsiooni.", + "notify_push configuration guide" : "notify_push konfiguratsioonijuhend", + "Realtime push active" : "Reaalajas tõuge aktiivne", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Reaalajas tõuketeavitused on aktiivsed. Ühendatud kliendid saavad objektide loomisel, uuendamisel või kustutamisel kohesed uuendused.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/et.json b/l10n/et.json new file mode 100644 index 0000000000..7f208652a1 --- /dev/null +++ b/l10n/et.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin nõutav)", + "(no subject)": "(teema puudub)", + "(no title)": "(pealkiri puudub)", + "(unnamed)": "(nimetu)", + "(untitled card)": "(pealkirjata kaart)", + "({count} selected)": "({count} valitud)", + ", {failed} failed": ", {failed} ebaõnnestus", + "0 = unlimited": "0 = piiramatu", + "A deactivation request is pending.": "Deaktiveerimistaotlus on ootel.", + "AI Assistant": "AI-assistent", + "AI Chat": "AI-vestlus", + "AI service not configured": "AI-teenus ei ole seadistatud", + "API Key": "API-võti", + "API Request Quota (requests/day)": "API päringukvoot (päringut/päevas)", + "API Token Configuration": "API-tokeni seadistus", + "API Tokens": "API-tokenid", + "API tokens saved successfully": "API-tokenid edukalt salvestatud", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: see aruanne ühendab iga verwerkingsactiviteit'i talle omistatud kogu eluaja auditi jälje arvuga. Audiitorid ja Autoriteit Persoonsgegevens kasutavad seda järelevalveülevaatuseks.", + "About ConfigSets": "ConfigSet'ide kohta", + "Access denied": "Juurdepääs keelatud", + "Access organisation dashboard and analytics": "Juurdepääs organisatsiooni töölauale ja analüütikale", + "Account": "Konto", + "Action": "Toiming", + "Action Distribution": "Toimingute jaotus", + "Action:": "Toiming:", + "Actions": "Toimingud", + "Active": "Aktiivne", + "Active Collections": "Aktiivsed kollektsioonid", + "Active Filters": "Aktiivsed filtrid", + "Active Organisation:": "Aktiivne organisatsioon:", + "Active Organisations": "Aktiivsed organisatsioonid", + "Active filters:": "Aktiivsed filtrid:", + "Active organisation changed successfully": "Aktiivne organisatsioon edukalt muudetud", + "Active organisation set successfully": "Aktiivne organisatsioon edukalt määratud", + "Activities": "Tegevused", + "Activity": "Tegevus", + "Activity Period": "Tegevusperiood", + "Add": "Lisa", + "Add Attachment": "Lisa manus", + "Add Endpoint": "Lisa lõpp-punkt", + "Add Groups": "Lisa rühmad", + "Add Property to '{title}'": "Lisa atribuut objektile '{title}'", + "Add Register": "Lisa register", + "Add Schema": "Lisa skeem", + "Add Source": "Lisa allikas", + "Add User": "Lisa kasutaja", + "Add User to Organisation": "Lisa kasutaja organisatsiooni", + "Add a contact from any of your address books to associate it with this object.": "Lisage kontakt mis tahes oma aadressiraamatust, et seostada see selle objektiga.", + "Add a file or files": "Lisa fail või failid", + "Add card": "Lisa kaart", + "Add contact": "Lisa kontakt", + "Add endpoint": "Lisa lõpp-punkt", + "Add oneOf entry": "Lisa oneOf-kirje", + "Add range": "Lisa vahemik", + "Add schema titles, descriptions, and register information to provide richer context for search": "Lisage skeemide pealkirjad, kirjeldused ja registriteave, et pakkuda otsingule rikkalikumat konteksti", + "Add to favorites": "Lisa lemmikutesse", + "Added": "Lisatud", + "Added to favorites": "Lemmikutesse lisatud", + "Additional Information": "Lisateave", + "Additional feedback saved. Thank you!": "Täiendav tagasiside salvestatud. Aitäh!", + "Addresses": "Aadressid", + "Admin": "Administraator", + "Advanced": "Täpsem", + "Advanced Filters": "Täpsemad filtrid", + "Advanced Options": "Täpsemad valikud", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Täpsemad filtrid ei ole andmebaasiallika kasutamisel saadaval. Filtreerimisvalikute jaoks lülitage Auto või SOLR Index režiimile.", + "After": "Pärast", + "Agent Type": "Agendi tüüp", + "Agent deleted successfully": "Agent edukalt kustutatud", + "Agent successfully created": "Agent edukalt loodud", + "Agent successfully updated": "Agent edukalt uuendatud", + "Agents": "Agendid", + "Aggregated across schemas": "Koondatud skeemide lõikes", + "All": "Kõik", + "All Categories": "Kõik kategooriad", + "All Collections": "Kõik kollektsioonid", + "All Confidence Levels": "Kõik usaldustasemed", + "All ConfigSets": "Kõik ConfigSet'id", + "All Files": "Kõik failid", + "All Levels": "Kõik tasemed", + "All Types": "Kõik tüübid", + "All Webhooks": "Kõik veebihaagid", + "All actions": "Kõik toimingud", + "All audit trails cleared successfully": "Kõik auditi jäljed edukalt tühjendatud", + "All clear": "Kõik korras", + "All file types": "Kõik failitüübid", + "All registers": "Kõik registrid", + "All schemas": "Kõik skeemid", + "All search trails cleared successfully": "Kõik otsingujäljed edukalt tühjendatud", + "All searches": "Kõik otsingud", + "All users": "Kõik kasutajad", + "All-day events": "Kogu päeva sündmused", + "Allowed MIME Types": "Lubatud MIME-tüübid", + "Already linked": "Juba lingitud", + "An error occurred while clearing audit trails": "Auditi jälgede tühjendamisel ilmnes viga", + "An error occurred while deleting the audit trail": "Auditi jälje kustutamisel ilmnes viga", + "An error occurred while deleting the object": "Objekti kustutamisel ilmnes viga", + "An error occurred while deleting the view": "Vaate kustutamisel ilmnes viga", + "An error occurred while permanently deleting the objects": "Objektide jäädavalt kustutamisel ilmnes viga", + "An error occurred while restoring the objects": "Objektide taastamisel ilmnes viga", + "An error occurred while saving the organisation": "Organisatsiooni salvestamisel ilmnes viga", + "Analysis completed:": "Analüüs lõpetatud:", + "Analysis steps:": "Analüüsi sammud:", + "Analytics": "Analüütika", + "Analyze Objects": "Analüüsi objekte", + "Analyze Properties": "Analüüsi atribuute", + "Analyze existing properties for improvement opportunities": "Analüüsige olemasolevaid atribuute parendamisvõimaluste leidmiseks", + "Analyzing...": "Analüüsimine...", + "Anonymized": "Anonüümitud", + "Anonymous Group": "Anonüümne rühm", + "Any favorites and sharing settings for this view": "Kõik selle vaate lemmikud ja jagamisseaded", + "Any user": "Iga kasutaja", + "App ID": "Rakenduse ID", + "App store cache invalidated successfully": "Rakendustepoe vahemälu edukalt kehtetuks tunnistatud", + "App store cache invalidated: {invalidated}": "Rakendustepoe vahemälu kehtetuks tunnistatud: {invalidated}", + "Application deleted successfully": "Rakendus edukalt kustutatud", + "Application identifier for this configuration (optional)": "Selle seadistuse rakenduse identifikaator (valikuline)", + "Application settings": "Rakenduse seaded", + "Application successfully created": "Rakendus edukalt loodud", + "Application successfully updated": "Rakendus edukalt uuendatud", + "Applications": "Rakendused", + "Apply": "Rakenda", + "Apply Changes": "Rakenda muudatused", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Rakendage roll kõigile selle registri skeemidele, millel puuduvad selgesõnalised autoriseerimise alistamised.", + "Approval Progress": "Heakskiitmise edenemine", + "Approve": "Kiida heaks", + "Approved": "Heaks kiidetud", + "Archive": "Arhiveeri", + "Archive conversation": "Arhiveeri vestlus", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Kas arhiveerida see verwerkingsactiviteit? Auditi jälje read viitavad sellele jätkuvalt.", + "Archived conversations are hidden from your active list": "Arhiveeritud vestlused on teie aktiivsest loendist peidetud", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Kas olete kindel, et soovite KUSTUTADA kollektsiooni „{name}“?\n\nSee:\n• Kustutab jäädavalt kollektsiooni ja kõik selle andmed\n• Eemaldab kõik indekseeritud dokumendid\n• Seda toimingut ei saa tagasi võtta", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Kas olete kindel, et soovite vanad otsingujäljed puhastada? See kustutab vanemad kui 30 päeva kirjed.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Kas olete kindel, et soovite tühjendada kõik andmed kollektsioonist „{name}“?\n\nSee:\n• Kustutab kõik indekseeritud dokumendid\n• Säilitab kollektsiooni struktuuri puutumatuna\n• Seda toimingut ei saa tagasi võtta", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Kas olete kindel, et soovite kustutada rakenduse „{name}“? Seda toimingut ei saa tagasi võtta.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Kas olete kindel, et soovite kustutada valitud auditi jäljed? Seda toimingut ei saa tagasi võtta.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Kas olete kindel, et soovite kustutada valitud otsingujäljed? Seda toimingut ei saa tagasi võtta.", + "Are you sure you want to delete this ConfigSet?": "Kas olete kindel, et soovite selle ConfigSet'i kustutada?", + "Are you sure you want to delete this configset?": "Kas olete kindel, et soovite selle configset'i kustutada?", + "Are you sure you want to leave '{name}'?": "Kas olete kindel, et soovite lahkuda organisatsioonist '{name}'?", + "Are you sure you want to permanently delete": "Kas olete kindel, et soovite jäädavalt kustutada", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Kas olete kindel, et soovite kollektsiooni „{name}“ uuesti indekseerida?\n\nSee:\n• Loob indeksi kõigi objektidega uuesti\n• Võtab lõpetamiseks mitu minutit\n• Võib uuesti indekseerimise ajal mõjutada otsingu jõudlust", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Kas olete kindel, et soovite eemaldada skeemi „{schema}“ registrist „{register}“? Seda toimingut ei saa tagasi võtta.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Kas olete kindel, et soovite pehmelt kustutada {count} aktiivset objekti skeemi „{schema}“ jaoks?\n\nNeed märgitakse kustutatuks, kuid neid saab hiljem jäädavalt eemaldada.", + "Array Object Configuration:": "Massiivi objekti seadistus:", + "Articles": "Artiklid", + "Ask a question...": "Esitage küsimus...", + "Ask questions about your data using natural language": "Esitage oma andmete kohta küsimusi loomulikus keeles", + "Assigned collaborative tags": "Määratud koostöösildid", + "Assignment notifications": "Määramise teavitused", + "Attachment added to {name}": "Manus lisatud objektile {name}", + "Attempt": "Katse", + "Audit Trail Changes": "Auditi jälje muudatused", + "Audit Trail Details": "Auditi jälje üksikasjad", + "Audit Trail Management": "Auditi jälje haldus", + "Audit Trail Statistics": "Auditi jälje statistika", + "Audit Trails": "Auditi jäljed", + "Audit trail data copied to clipboard": "Auditi jälje andmed kopeeritud lõikelauale", + "Audit trail deleted successfully": "Auditi jälg edukalt kustutatud", + "Audit trail successfully deleted": "Auditi jälg edukalt kustutatud", + "Authentication": "Autentimine", + "Auto (e.g. Y for year, F Y for month)": "Auto (nt Y aasta jaoks, F Y kuu jaoks)", + "Auto share": "Automaatne jagamine", + "Auto-commit disabled": "Automaatne sissekanne keelatud", + "Auto-commit enabled": "Automaatne sissekanne lubatud", + "Auto-create Default Organisation": "Loo automaatselt vaikimisi organisatsioon", + "Auto-retry failed vectorizations": "Proovi ebaõnnestunud vektoriseerimisi automaatselt uuesti", + "Automatically commit changes to SOLR index": "Kanna muudatused automaatselt SOLR-indeksisse", + "Automatically connect to the federation network.": "Ühenda automaatselt föderatsioonivõrku.", + "Automatically create a default organisation if none exists when the app is initialized": "Loo automaatselt vaikimisi organisatsioon, kui rakenduse lähtestamisel ei ole ühtegi olemas", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Loo automaatselt vektorvdistumised tekstitükkidest, kui failid laaditakse üles ja töödeldakse", + "Automatically generate vector embeddings when objects are created or updated": "Loo automaatselt vektorvistumised, kui objekte luuakse või uuendatakse", + "Automatically retry failed vectorization attempts (max 3 retries)": "Proovi ebaõnnestunud vektoriseerimiskatseid automaatselt uuesti (max 3 korduskatset)", + "Automatically update catalogues.": "Uuenda katalooge automaatselt.", + "Available Filters": "Saadaolevad filtrid", + "Available Workflows": "Saadaolevad töövood", + "Available placeholders:": "Saadaolevad kohatäited:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Avatari muudatusi ei toeta teie autentimisteenuse pakkuja.", + "Avatar removed": "Avatar eemaldatud", + "Avatar updated successfully": "Avatar edukalt uuendatud", + "Avg Execution Time": "Keskmine täitmisaeg", + "Avg Members/Org": "Keskmine liikmeid/org", + "Avg Object Views/Session": "Keskmine objektivaatamisi/seanss", + "Avg Response Time": "Keskmine vastamisaeg", + "Avg Results/Search": "Keskmine tulemusi/otsing", + "Avg Searches/Session": "Keskmine otsinguid/seanss", + "Back": "Tagasi", + "Back to Entities": "Tagasi olemite juurde", + "Back to Registers": "Tagasi registrite juurde", + "Back to Webhooks": "Tagasi veebihaakide juurde", + "Back to applications": "Tagasi rakenduste juurde", + "Back to entities": "Tagasi olemite juurde", + "Backend parameter is required": "Taustasüsteemi parameeter on nõutav", + "Backend updated successfully. Please reload the application.": "Taustasüsteem edukalt uuendatud. Palun laadige rakendus uuesti.", + "Bandwidth Quota (MB/month)": "Ribalaiuse kvoot (MB/kuus)", + "Base URL (Optional)": "Baas-URL (valikuline)", + "Basic Information": "Põhiteave", + "Batch Size": "Partii suurus", + "Batch extraction completed": "Partii ekstraktimine lõpetatud", + "Before": "Enne", + "Before object vectorization can work:": "Enne kui objekti vektoriseerimine saab toimida:", + "Behavior": "Käitumine", + "Behavior Issue": "Käitumisprobleem", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 kestus, nt P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob-salvestus on kasutuselt kõrvaldatud. Kõik objektid kasutavad nüüd magic-tabeleid.", + "Bookmarks": "Järjehoidjad", + "Branch": "Haru", + "Bulk Role Assignment: {title}": "Rollide hulgimäärang: {title}", + "Bulk delete operation completed successfully": "Hulgikustutamise toiming edukalt lõpetatud", + "Bulk save operation completed successfully": "Hulgisalvestamise toiming edukalt lõpetatud", + "Business Data": "Ärimudeli andmed", + "CSV": "CSV", + "Cache cleared successfully": "Vahemälu edukalt tühjendatud", + "Cache grew from {old} to {new} entries.": "Vahemälu kasvas {old} kirjelt {new} kirjeni.", + "Cache shrunk from {old} to {new} entries.": "Vahemälu kahanes {old} kirjelt {new} kirjeni.", + "Cache stayed the same at {size} entries.": "Vahemälu jäi samaks: {size} kirjet.", + "Calculate Sizes": "Arvuta suurused", + "Calendar": "Kalender", + "Calendar Provider Configuration": "Kalendriteenuse pakkuja seadistus", + "Calendar integration is not available": "Kalendri lõimimine ei ole saadaval", + "Calendar name": "Kalendri nimi", + "Cancel": "Tühista", + "Cancel deactivation request": "Tühista deaktiveerimistaotlus", + "Cannot delete: objects are still attached": "Ei saa kustutada: objektid on endiselt seotud", + "Cannot edit: This register is managed by external configuration {title}": "Ei saa redigeerida: seda registrit haldab väline seadistus {title}", + "Cannot remove schema with existing objects ({count} objects)": "Ei saa eemaldada skeemi, millel on olemasolevaid objekte ({count} objekti)", + "Cannot remove schema {schema} because it contains {count} objects": "Ei saa eemaldada skeemi {schema}, kuna see sisaldab {count} objekti", + "Cards": "Kaardid", + "Cascade delete": "Kaskaadkustutus", + "Categories of data subjects (one per line)": "Andmesubjektide kategooriad (üks rea kohta)", + "Categories of personal data (one per line)": "Isikuandmete kategooriad (üks rea kohta)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategooria", + "Change Type": "Muutuse tüüp", + "Change password": "Muuda parooli", + "Changes": "Muudatused", + "Changes copied to clipboard": "Muudatused kopeeritud lõikelauale", + "Changes for Audit Trail #{id}": "Muudatused auditi jälje #{id} jaoks", + "Chat Model": "Vestlusmudel", + "Chat Provider": "Vestlusteenuse pakkuja", + "Chat Provider (RAG)": "Vestlusteenuse pakkuja (RAG)", + "Chat Settings": "Vestluse seaded", + "Chat provider connection successful!": "Vestlusteenuse pakkuja ühendus edukas!", + "Chat settings": "Vestluse seaded", + "Chat test failed: {error}": "Vestluse test ebaõnnestus: {error}", + "Choose a register": "Valige register", + "Choose a schema": "Valige skeem", + "Choose how vector similarity calculations are performed for semantic search": "Valige, kuidas vektori sarnasuse arvutusi semantilise otsingu jaoks tehakse", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Valige, millised failitüübid vektoriseerimisprotsessi kaasata. Töödeldakse ainult faile, millel on ekstraktitud tekst ja tükid.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Valige, millised vaated vektoriseerimisprotsessi kaasata. Jätke tühjaks, et töödelda kõik vaated vastavalt teie seadistusele.", + "Chunk": "Tükk", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Tükkide kustutamine ei ole veel rakendatud. Kasutage tükipõhiseid lõpp-punkte.", + "Chunks": "Tükid", + "Chunks to Vectorize:": "Vektoriseeritavad tükid:", + "Cleanup Old Trails": "Puhasta vanad jäljed", + "Cleanup completed": "Puhastamine lõpetatud", + "Cleanup completed successfully. Deleted {count} entries.": "Puhastamine edukalt lõpetatud. Kustutati {count} kirjet.", + "Cleanup failed: {error}": "Puhastamine ebaõnnestus: {error}", + "Clear All Embeddings": "Tühjenda kõik vistumised", + "Clear Entries": "Tühjenda kirjed", + "Clear Filtered Audit Trails": "Tühjenda filtreeritud auditi jäljed", + "Clear Filters": "Tühjenda filtrid", + "Clear Index": "Tühjenda indeks", + "Clear Selection": "Tühjenda valik", + "Clear all": "Tühjenda kõik", + "Clear all filters": "Tühjenda kõik filtrid", + "Clear date range": "Tühjenda kuupäevavahemik", + "Clear filters": "Tühjenda filtrid", + "Clearing...": "Tühjendamine...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Klõpsake Käivita vastavusskaneerimine, et leida skeemid, kus on tuvastatud isikuandmeid (PII), kuid puudub töötlemistegevuse annotatsioon.", + "Click to subscribe to notifications": "Klõpsake teavituste tellimiseks", + "Click to unsubscribe from notifications": "Klõpsake teavituste tellimuse tühistamiseks", + "Close": "Sulge", + "Cluster name": "Klastri nimi", + "Code": "Kood", + "Code (short readable key, e.g. v-2026-001)": "Kood (lühike loetav võti, nt v-2026-001)", + "Collapse details": "Ahenda üksikasjad", + "Collection Management": "Kollektsioonihaldus", + "Collection Name": "Kollektsiooni nimi", + "Collection assignments updated successfully": "Kollektsiooni määrangud edukalt uuendatud", + "Collection cleared successfully": "Kollektsioon edukalt tühjendatud", + "Collection copied successfully": "Kollektsioon edukalt kopeeritud", + "Collection created successfully": "Kollektsioon edukalt loodud", + "Collection deleted successfully": "Kollektsioon edukalt kustutatud", + "Collection used to store and index file metadata and content": "Kollektsioon, mida kasutatakse faili metaandmete ja sisu salvestamiseks ja indekseerimiseks", + "Collection used to store and index object data": "Kollektsioon, mida kasutatakse objektiandmete salvestamiseks ja indekseerimiseks", + "Collections:": "Kollektsioonid:", + "Color": "Värv", + "Comment...": "Kommentaar...", + "Commit Message": "Sissekande sõnum", + "Commit Within (ms)": "Sissekanne aja jooksul (ms)", + "Commit message": "Sissekande sõnum", + "Compare current schema with real object data": "Võrdle praegust skeemi reaalsete objektiandmetega", + "Completed": "Lõpetatud", + "Complex": "Keeruline", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Keerulised päringud: täpsemad otsingud mitme filtri, operaatori ja keerukate parameetrikombinatsioonidega", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Keerulised päringud: täpsemad otsingud mitme filtri, operaatori ja keerukate parameetrikombinatsioonidega", + "Compliance": "Vastavus", + "Confidence Level": "Usaldustase", + "Confidence Score": "Usaldusskoor", + "Confidence: {confidence}%": "Usaldus: {confidence}%", + "Config must be provided as an object": "Konfiguratsioon tuleb esitada objektina", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet'i haldus", + "ConfigSet Name": "ConfigSet'i nimi", + "ConfigSet created successfully": "ConfigSet edukalt loodud", + "ConfigSet deleted successfully": "ConfigSet edukalt kustutatud", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSet'id määratlevad teie SOLR-kollektsioonide skeemi ja seadistuse. Need sisaldavad väljade määratlusi, analüsaatoreid ja muid otsinguseadeid.", + "Configuration": "Seadistus", + "Configuration Data": "Seadistuse andmed", + "Configuration is up to date": "Seadistus on ajakohane", + "Configuration saved": "Seadistus salvestatud", + "Configuration saved successfully.": "Seadistus edukalt salvestatud.", + "Configuration type (default, application, etc.)": "Seadistuse tüüp (vaikimisi, rakendus jne)", + "Configurations": "Seadistused", + "Configure API tokens for external service integrations": "Seadistage API-tokenid väliste teenuste lõimimiseks", + "Configure Apache SOLR search engine for advanced search capabilities": "Seadistage Apache SOLR otsingumootor täpsemate otsinguvõimaluste jaoks", + "Configure Facets": "Seadista tahud", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Seadistage suurte keelemudelite (LLM) teenusepakkujad AI-põhiste funktsioonide jaoks, sealhulgas semantiline otsing, vistumised ja vestlus.", + "Configure SOLR Facets": "Seadista SOLR-tahud", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Seadistage oma SOLR-serveri põhilised ühendusseaded, sealhulgas autentimine ja võrguvalikud. Tuumade ja kollektsioonide haldamiseks kasutage eraldi ConfigSet'i ja kollektsioonihalduse dialooge.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Seadistage oma SOLR-serveri põhilised ühendusseaded, sealhulgas autentimine ja võrguvalikud. Tuumade ja kollektsioonide haldamiseks kasutage eraldi configset'i ja kollektsioonihalduse dialooge.", + "Configure default organisation and organisation-related settings": "Seadistage vaikimisi organisatsioon ja organisatsiooniga seotud seaded", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Seadistage, kuidas andmebaasiobjekte teisendatakse vektorvistumisteks semantilise otsingu jaoks. Objektid vektoriseeritakse otse ilma tekstiekstraktsiooni vajaduseta.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Seadistage, kuidas objekte enne vektoriseerimist tekstiks teisendatakse. Need seaded mõjutavad otsingu kvaliteeti ja konteksti.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Seadistage suurte keelemudelite (LLM) teenusepakkujad ai-põhiste funktsioonide jaoks, sealhulgas semantiline otsing, vistumised ja vestlus.", + "Configure multi-organization support and tenant isolation": "Seadistage mitme organisatsiooni tugi ja üürniku isolatsioon", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Seadistage faili vektoriseerimise parameetrid. See protsess loob vektorvistumised kõigi ekstraktitud failitükkide jaoks.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Seadistage objekti vektoriseerimise parameetrid. See protsess loob vektorvistumised kõigi teie vaatefiltritele vastavate objektide jaoks.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Seadistage see skeem, et kuvada objekte sündmustena Nextcloud kalendrirakenduses.", + "Configure which types of data to search and how many sources to retrieve": "Seadistage, milliseid andmetüüpe otsida ja mitu allikat hankida", + "Configuring field mappings...": "Väljade vastendamiste seadistamine...", + "Confirm Account Deactivation": "Kinnita konto deaktiveerimine", + "Confirm Object Deletion": "Kinnita objekti kustutamine", + "Confirm Object Restoration": "Kinnita objekti taastamine", + "Confirm Permanent Object Deletion": "Kinnita objekti jäädav kustutamine", + "Confirm deactivation": "Kinnita deaktiveerimine", + "Connecting to SOLR cluster...": "Ühendamine SOLR-klastriga...", + "Connection Failed": "Ühendus ebaõnnestus", + "Connection Settings": "Ühendusseaded", + "Connection Successful!": "Ühendus edukas!", + "Connection failed": "Ühendus ebaõnnestus", + "Connection protocol": "Ühendusprotokoll", + "Connection successful": "Ühendus edukas", + "Connection timeout in seconds": "Ühenduse ajalõpp sekundites", + "Constant delay between retries (5 minutes)": "Püsiv viivitus korduskatsete vahel (5 minutit)", + "Constraint Issue": "Piiranguprobleem", + "Consumer not found": "Tarbijat ei leitud", + "Contacts": "Kontaktid", + "Contacts integration is not available": "Kontaktide lõimimine ei ole saadaval", + "Content Filters": "Sisufiltrid", + "Continue to Properties": "Jätka atribuutide juurde", + "Contracts": "Lepingud", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontrollige, milliseid objektivaateid tuleks vektoriseerida API-kulude vähendamiseks. Vektoriseerige ainult vaateid, mis saavad semantilisest otsingust kasu.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontrollige, milliseid vaateid ja tööriistu AI selles vestluses kasutada saab. Vaikimisi on kõik agendi võimekused lubatud.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Juhib juhuslikkust (0 = deterministlik, 2 = väga loominguline)", + "Conversation ID is required": "Vestluse ID on nõutav", + "Conversation archived": "Vestlus arhiveeritud", + "Conversation archived successfully": "Vestlus edukalt arhiveeritud", + "Conversation cleared successfully": "Vestlus edukalt tühjendatud", + "Conversation deleted": "Vestlus kustutatud", + "Conversation not found": "Vestlust ei leitud", + "Conversation permanently deleted": "Vestlus jäädavalt kustutatud", + "Conversation renamed": "Vestlus ümber nimetatud", + "Conversation restored": "Vestlus taastatud", + "Conversation started with {agent}": "Vestlus alustatud agendiga {agent}", + "Conversation title": "Vestluse pealkiri", + "Conversations": "Vestlused", + "Converting {schema} to blob storage...": "Skeemi {schema} teisendamine blob-salvestusse...", + "Converting {schema} to magic table...": "Skeemi {schema} teisendamine magic-tabeliks...", + "Copied!": "Kopeeritud!", + "Copy": "Kopeeri", + "Copy Changes": "Kopeeri muudatused", + "Copy Collection": "Kopeeri kollektsioon", + "Copy Data": "Kopeeri andmed", + "Copy Full Data": "Kopeeri kõik andmed", + "Copy of {name}": "Koopia objektist {name}", + "Copy to clipboard": "Kopeeri lõikelauale", + "Copying...": "Kopeerimine...", + "Costs": "Kulud", + "Count": "Arv", + "Counting objects...": "Objektide loendamine...", + "Create": "Loo", + "Create API Token": "Loo API-token", + "Create Agent": "Loo agent", + "Create Application": "Loo rakendus", + "Create Collection": "Loo kollektsioon", + "Create ConfigSet": "Loo ConfigSet", + "Create New Collection": "Loo uus kollektsioon", + "Create New ConfigSet": "Loo uus ConfigSet", + "Create Operations": "Loomistoimingud", + "Create Organisation": "Loo organisatsioon", + "Create Webhook": "Loo veebihaak", + "Create a copy of collection:": "Loo kollektsioonist koopia:", + "Create a new ConfigSet based on the _default template": "Loo uus ConfigSet _default malli põhjal", + "Create a new SOLR collection from an existing ConfigSet": "Loo uus SOLR kollektsioon olemasolevast ConfigSet-ist", + "Create a new SOLR collection from an existing configset": "Loo uus SOLR kollektsioon olemasolevast configset-ist", + "Create a new configset based on the _default template": "Loo uus configset _default malli põhjal", + "Create a new event or link an existing one from any of your calendars.": "Looge uus sündmus või linkige olemasolev mõnest oma kalendrist.", + "Create another": "Loo veel üks", + "Create copies of": "Loo koopiad", + "Create event": "Loo sündmus", + "Create new token": "Loo uus luba", + "Create or link a Deck card to track work on this object.": "Looge või linkige Deck kaart selle objektiga seotud töö jälgimiseks.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Looge esimene verwerkingsactiviteit, et alustada auditijälje ridade märgistamist nende AVG Art 30 omistusega.", + "Create your first AI agent to get started.": "Looge alustamiseks oma esimene tehisintellekti agent.", + "Created": "Loodud", + "Created:": "Loodud:", + "Creating search collections...": "Otsingukollektsioonide loomine...", + "Creating...": "Loomine...", + "Current": "Praegune", + "Current Filters": "Praegused filtrid", + "Current Type:": "Praegune tüüp:", + "Current password": "Praegune parool", + "Custom API endpoint if using a different region": "Kohandatud API lõpp-punkt, kui kasutatakse teist piirkonda", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Kohandatud HTTP päised (üks rea kohta, vorming: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Kohandatud HTTP päised (üks rea kohta, vorming: header-name: value)", + "Custom Ranges:": "Kohandatud vahemikud:", + "Custom display title for this facet": "Selle tahu kohandatud kuvanimi", + "Custom pattern:": "Kohandatud muster:", + "DELETE COLLECTION": "KUSTUTA KOLLEKTSIOON", + "DSAR": "DSAR", + "Daily": "Iga päev", + "Dashboard": "Töölaud", + "Dashboard not found": "Töölauda ei leitud", + "Data": "Andmed", + "Data Source": "Andmeallikas", + "Data Sources": "Andmeallikad", + "Data Views": "Andmevaated", + "Data sources": "Andmeallikad", + "Data type does not match observed values": "Andmetüüp ei vasta täheldatud väärtustele", + "Data-subject access request": "Andmesubjekti juurdepääsutaotlus", + "Database URL": "Andmebaasi URL", + "Database information refreshed": "Andmebaasi teave värskendati", + "Date Faceting:": "Kuupäeva tahkudeks jaotamine:", + "Date Range": "Kuupäevavahemik", + "Dates": "Kuupäevad", + "Deactivation request cancelled": "Deaktiveerimise taotlus tühistati", + "Deactivation request submitted": "Deaktiveerimise taotlus esitati", + "Deck": "Deck", + "Deck integration is not available": "Deck integratsioon ei ole saadaval", + "Default": "Vaikimisi", + "Default Object Tenant": "Objekti vaikeüürnik", + "Default Organisation": "Vaikeorganisatsioon", + "Default Owner": "Vaikeomanik", + "Default User (for cron/background jobs)": "Vaikekasutaja (croni/taustatööde jaoks)", + "Default User Tenant": "Kasutaja vaikeüürnik", + "Default value": "Vaikeväärtus", + "Default value (Email)": "Vaikeväärtus (e-post)", + "Default value (Password)": "Vaikeväärtus (parool)", + "Default value (Phone number)": "Vaikeväärtus (telefoninumber)", + "Default value (Regex)": "Vaikeväärtus (regulaaravaldis)", + "Default: 5": "Vaikimisi: 5", + "Defaults to current user. Select a different user if needed.": "Vaikimisi praegune kasutaja. Valige vajadusel teine kasutaja.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Viivitused kahekordistuvad iga katsega (2, 4, 8 minutit...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Viivitused suurenevad lineaarselt (5, 10, 15 minutit...)", + "Delete": "Kustuta", + "Delete ({count})": "Kustuta ({count})", + "Delete Application": "Kustuta rakendus", + "Delete Audit Trail": "Kustuta auditijälg", + "Delete Collection": "Kustuta kollektsioon", + "Delete ConfigSet": "Kustuta ConfigSet", + "Delete Objects": "Kustuta objektid", + "Delete Operations": "Kustutamistoimingud", + "Delete SOLR Collection": "Kustuta SOLR kollektsioon", + "Delete View": "Kustuta vaade", + "Delete all objects in this schema": "Kustuta kõik selle skeemi objektid", + "Delete permanently": "Kustuta jäädavalt", + "Delete this conversation?": "Kas kustutada see vestlus?", + "Delete view": "Kustuta vaade", + "Delete {count} object": "Kustuta {count} objekt", + "Delete {count} objects": "Kustuta {count} objekti", + "Deleted": "Kustutatud", + "Deleted By": "Kustutaja", + "Deleted Date": "Kustutamise kuupäev", + "Deleted Items Management": "Kustutatud üksuste haldus", + "Deleted This Week": "Sel nädalal kustutatud", + "Deleted Today": "Täna kustutatud", + "Deleted:": "Kustutatud:", + "Deleting SOLR Collection...": "SOLR kollektsiooni kustutamine...", + "Deleting...": "Kustutamine...", + "Deletion Date Range": "Kustutamise kuupäevavahemik", + "Deletion Statistics": "Kustutamise statistika", + "Deprecated": "Aegunud", + "Depublish": "Tühista avaldamine", + "Description": "Kirjeldus", + "Description Template": "Kirjelduse mall", + "Description shown as tooltip": "Kohtspikrina kuvatav kirjeldus", + "Description:": "Kirjeldus:", + "Detect data types and patterns": "Tuvasta andmetüübid ja mustrid", + "Detected At": "Tuvastatud", + "Detected Format:": "Tuvastatud vorming:", + "Detected Issues:": "Tuvastatud probleemid:", + "Detected: {date}": "Tuvastatud: {date}", + "Direct permission": "Otsene õigus", + "Disable": "Keela", + "Disable this to make the agent": "Keelake see, et muuta agent", + "Disabled": "Keelatud", + "Discover Files": "Avasta failid", + "Discovered Properties": "Avastatud atribuudid", + "Display Format": "Kuvavorming", + "Display Name": "Kuvanimi", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Kas soovite jäädavalt kustutada kõik filtreeritud auditijälje kirjed? Seda toimingut ei saa tagasi võtta.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Kas soovite jäädavalt kustutada selle auditijälje kirje? Seda toimingut ei saa tagasi võtta.", + "Documents": "Dokumendid", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR lubatud", + "Download API Spec": "Laadi alla API spetsifikatsioon", + "Download API Specification": "Laadi alla API spetsifikatsioon", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Laadige alla koopia kõigist OpenRegister-is salvestatud isikuandmetest (GDPR artikkel 20).", + "Drag a file or files here": "Lohistage fail või failid siia", + "Dry run completed: {count} fields would be created": "Proovikäik lõpetatud: loodaks {count} välja", + "Dry run completed: {count} fields would be fixed": "Proovikäik lõpetatud: parandataks {count} välja", + "Duration in seconds (optional)": "Kestus sekundites (valikuline)", + "Edit": "Muuda", + "Edit Agent": "Muuda agenti", + "Edit Application": "Muuda rakendust", + "Edit Configuration": "Muuda konfiguratsiooni", + "Edit Endpoint": "Muuda lõpp-punkti", + "Edit Organisation": "Muuda organisatsiooni", + "Edit Property '{key}' of '{title}'": "Muuda objekti '{title}' atribuuti '{key}'", + "Edit Register": "Muuda registrit", + "Edit Schema": "Muuda skeemi", + "Edit Source": "Muuda allikat", + "Edit View": "Muuda vaadet", + "Edit Webhook": "Muuda veebihaaki", + "Edit labels": "Muuda silte", + "Edit processing activity": "Muuda töötlemistoimingut", + "Edit tags for {name}": "Muuda {name} silte", + "Edit verwerkingsactiviteit": "Muuda verwerkingsactiviteit", + "Edit view": "Muuda vaadet", + "Edit view details": "Muuda vaate üksikasju", + "Effectiveness": "Tõhusus", + "Email": "E-post", + "Email Addresses": "E-posti aadressid", + "Email digest frequency": "E-posti kokkuvõtte sagedus", + "Emails": "E-kirjad", + "Embedding Model": "Manustamismudel", + "Embedding Provider": "Manustamise pakkuja", + "Embedding generated successfully": "Manustamine loodi edukalt", + "Embedding provider connection successful!": "Manustamise pakkuja ühendus õnnestus!", + "Embedding test failed: {error}": "Manustamise test ebaõnnestus: {error}", + "Enable": "Luba", + "Enable RAG": "Luba RAG", + "Enable auto-creation": "Luba automaatne loomine", + "Enable automatic file vectorization": "Luba failide automaatne vektoriseerimine", + "Enable automatic object vectorization": "Luba objektide automaatne vektoriseerimine", + "Enable automatic synchronization": "Luba automaatne sünkroonimine", + "Enable calendar provider": "Luba kalendri pakkuja", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Luba SOLR toimingute üksikasjalik logimine (soovitatav silumiseks)", + "Enable faceting": "Luba tahkudeks jaotamine", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Lubage või keelake LLM funktsioonid. Seadistage pakkujad ja mudelid ülaltoodud LLM konfiguratsiooni nupu abil.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Lubage või keelake LLM funktsioonid. Seadistage pakkujad ja mudelid ülaltoodud LLM konfiguratsiooni nupu abil.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Lubage või keelake SOLR otsingu integratsioon. Seadistage ühenduse seaded ülaltoodud ühenduse seadete nupu abil.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Lubage või keelake SOLR otsingu integratsioon. Seadistage ühenduse seaded ülaltoodud ühenduse seadete nupu abil.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Lubage või keelake n8n töövoo integratsioon. Seadistage ühenduse seaded allpool.", + "Enable or disable this webhook": "Luba või keela see veebihaak", + "Enable tools that allow the agent to interact with data through function calling.": "Lubage tööriistad, mis võimaldavad agendil andmetega suhelda funktsioonikutsete kaudu.", + "Enable vectorization for all existing and future views (may increase costs)": "Luba vektoriseerimine kõigi olemasolevate ja tulevaste vaadete jaoks (võib suurendada kulusid)", + "Enable write-back to target objects": "Luba tagasikirjutamine sihtobjektidesse", + "Enabled": "Lubatud", + "End Date Field": "Lõppkuupäeva väli", + "Endpoint Path*": "Lõpp-punkti tee*", + "Endpoints": "Lõpp-punktid", + "Engine not found": "Mootorit ei leitud", + "Enter ConfigSet name": "Sisestage ConfigSet nimi", + "Enter Nextcloud usernames to grant access to this private agent": "Sisestage Nextcloud kasutajanimed, et anda juurdepääs sellele privaatsele agendile", + "Enter agent description (optional)": "Sisestage agendi kirjeldus (valikuline)", + "Enter agent name": "Sisestage agendi nimi", + "Enter application description (optional)": "Sisestage rakenduse kirjeldus (valikuline)", + "Enter application name": "Sisestage rakenduse nimi", + "Enter collection name": "Sisestage kollektsiooni nimi", + "Enter configuration description (optional)": "Sisestage konfiguratsiooni kirjeldus (valikuline)", + "Enter configuration title": "Sisestage konfiguratsiooni pealkiri", + "Enter description (optional)...": "Sisestage kirjeldus (valikuline)...", + "Enter new collection name": "Sisestage uue kollektsiooni nimi", + "Enter object ID": "Sisestage objekti ID", + "Enter organisation description (optional)": "Sisestage organisatsiooni kirjeldus (valikuline)", + "Enter organisation name": "Sisestage organisatsiooni nimi", + "Enter search term": "Sisestage otsingusõna", + "Enter search terms or leave empty to browse all": "Sisestage otsingusõnad või jätke tühjaks, et sirvida kõiki", + "Enter system prompt for the agent": "Sisestage agendi süsteemiviip", + "Enter username and press Enter": "Sisestage kasutajanimi ja vajutage Enter", + "Enter view name...": "Sisestage vaate nimi...", + "Enter webhook name": "Sisestage veebihaagi nimi", + "Enter your API key": "Sisestage oma API võti", + "Enter your object here...": "Sisestage oma objekt siia...", + "Enter your schema here...": "Sisestage oma skeem siia...", + "Entities": "Olemid", + "Entities detected": "Olemid tuvastatud", + "Entity ID: {id}": "Olemi ID: {id}", + "Entity Information": "Olemi teave", + "Entity Recognition": "Olemituvastus", + "Entity Recognition Method": "Olemituvastuse meetod", + "Entity deleted successfully": "Olem kustutati edukalt", + "Entity not found": "Olemit ei leitud", + "Entries to be deleted:": "Kustutatavad kirjed:", + "Enum Issue": "Loendi probleem", + "Enum constraint is missing": "Loendi piirang puudub", + "Erase (Art 17)": "Kustuta (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Kas kustutada selle subjekti jaoks {count} objekt(i)? See toiming logitakse auditijälge.", + "Erasure complete": "Kustutamine lõpetatud", + "Erasure preview": "Kustutamise eelvaade", + "Error": "Viga", + "Error Details": "Vea üksikasjad", + "Error Information": "Vea teave", + "Error Message": "Veateade", + "Error deleting audit trails: {error}": "Viga auditijälgede kustutamisel: {error}", + "Error deleting search trails: {error}": "Viga otsingujälgede kustutamisel: {error}", + "Error loading application": "Viga rakenduse laadimisel", + "Error loading audit trails": "Viga auditijälgede laadimisel", + "Error loading entity": "Viga olemi laadimisel", + "Error loading search trails": "Viga otsingujälgede laadimisel", + "Estimated Batches:": "Hinnangulised partiid:", + "Estimated Cost:": "Hinnanguline kulu:", + "Estimated Duration:": "Hinnanguline kestus:", + "Event": "Sündmus", + "Event Property for Payload": "Sündmuse atribuut kasuliku koorma jaoks", + "Events": "Sündmused", + "Every schema with detected PII has a processing-activity annotation.": "Igal tuvastatud isikuandmetega skeemil on töötlemistoimingu annotatsioon.", + "Example": "Näide", + "Example Value": "Näidisväärtus", + "Example value for this property": "Selle atribuudi näidisväärtus", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Välistav maksimum", + "Exclusive Minimum": "Välistav miinimum", + "Exclusive maximum": "Välistav maksimum", + "Exclusive minimum": "Välistav miinimum", + "Execution Mode": "Käivitusrežiim", + "Execution Time": "Käivitusaeg", + "Execution Time Range": "Käivitusaja vahemik", + "Existing Improvements": "Olemasolevad täiustused", + "Expand details": "Laienda üksikasju", + "Expiration": "Aegumine", + "Expires": "Aegub", + "Expires in (e.g., 90d)": "Aegub (nt 90d)", + "Exponential": "Eksponentsiaalne", + "Export": "Ekspordi", + "Export \"{schema}\" objects from \"{register}\"": "Ekspordi \"{schema}\" objektid registrist \"{register}\"", + "Export Configuration": "Ekspordi konfiguratsioon", + "Export Format:": "Ekspordivorming:", + "Export Objects": "Ekspordi objektid", + "Export completed successfully": "Eksport lõpetati edukalt", + "Export downloaded successfully": "Eksport laaditi edukalt alla", + "Export failed: {error}": "Eksport ebaõnnestus: {error}", + "Export is rate limited. Please try again later.": "Eksport on kiiruse osas piiratud. Proovige hiljem uuesti.", + "Export my data": "Ekspordi minu andmed", + "Export, view, or delete audit trails": "Ekspordi, vaata või kustuta auditijälgi", + "Exporting...": "Eksportimine...", + "Extend Schema": "Laienda skeemi", + "Extended": "Laiendatud", + "Extensions": "Laiendused", + "Extra Query Parameters": "Lisapäringuparameetrid", + "Extract Now": "Eralda kohe", + "Extract Pending Files": "Eralda ootel failid", + "Extract now": "Eralda kohe", + "Extract properties from each object": "Eralda atribuudid igast objektist", + "Extracted At": "Eraldatud", + "Extracted at": "Eraldatud", + "Extraction": "Eraldamine", + "Extraction Mode": "Eraldamise režiim", + "Extraction Scope": "Eraldamise ulatus", + "Extraction Status": "Eraldamise olek", + "Extraction error for {file}: {error}": "Eraldamise viga failil {file}: {error}", + "Facet Description": "Tahu kirjeldus", + "Facet Order": "Tahkude järjestus", + "Facet Title": "Tahu pealkiri", + "Facet Type": "Tahu tüüp", + "Facet configuration updated successfully": "Tahu konfiguratsioon värskendati edukalt", + "Facetable": "Tahkudeks jaotatav", + "Faceting Configuration:": "Tahkudeks jaotamise konfiguratsioon:", + "Facets discovered and configured successfully": "Tahud avastati ja seadistati edukalt", + "Facets discovered successfully": "Tahud avastati edukalt", + "Failed": "Ebaõnnestus", + "Failed to add attachment to object": "Manuse objektile lisamine ebaõnnestus", + "Failed to add user to organisation": "Kasutaja organisatsiooni lisamine ebaõnnestus", + "Failed to analyze schema properties": "Skeemi atribuutide analüüsimine ebaõnnestus", + "Failed to archive conversation": "Vestluse arhiveerimine ebaõnnestus", + "Failed to calculate sizes": "Suuruste arvutamine ebaõnnestus", + "Failed to cancel deactivation": "Deaktiveerimise tühistamine ebaõnnestus", + "Failed to change active organisation: {error}": "Aktiivse organisatsiooni muutmine ebaõnnestus: {error}", + "Failed to change password": "Parooli muutmine ebaõnnestus", + "Failed to check version: {error}": "Versiooni kontrollimine ebaõnnestus: {error}", + "Failed to clear audit trails: {error}": "Auditijälgede tühjendamine ebaõnnestus: {error}", + "Failed to clear blob objects: {error}": "Blob-objektide tühjendamine ebaõnnestus: {error}", + "Failed to clear cache: {error}": "Vahemälu tühjendamine ebaõnnestus: {error}", + "Failed to clear collection": "Kollektsiooni tühjendamine ebaõnnestus", + "Failed to clear conversation": "Vestluse tühjendamine ebaõnnestus", + "Failed to clear embeddings: {error}": "Manustuste tühjendamine ebaõnnestus: {error}", + "Failed to clear search trails: {error}": "Otsingujälgede tühjendamine ebaõnnestus: {error}", + "Failed to clear {type} cache: {error}": "{type} vahemälu tühjendamine ebaõnnestus: {error}", + "Failed to copy changes": "Muudatuste kopeerimine ebaõnnestus", + "Failed to copy data": "Andmete kopeerimine ebaõnnestus", + "Failed to copy data to clipboard": "Andmete lõikelauale kopeerimine ebaõnnestus", + "Failed to copy to clipboard": "Lõikelauale kopeerimine ebaõnnestus", + "Failed to copy token": "Loa kopeerimine ebaõnnestus", + "Failed to create conversation": "Vestluse loomine ebaõnnestus", + "Failed to create missing SOLR fields: {error}": "Puuduvate SOLR väljade loomine ebaõnnestus: {error}", + "Failed to create or find project": "Projekti loomine või leidmine ebaõnnestus", + "Failed to create token": "Loa loomine ebaõnnestus", + "Failed to delete SOLR collection: {error}": "SOLR kollektsiooni kustutamine ebaõnnestus: {error}", + "Failed to delete collection": "Kollektsiooni kustutamine ebaõnnestus", + "Failed to delete conversation": "Vestluse kustutamine ebaõnnestus", + "Failed to delete objects for {schema}: {error}": "Skeemi {schema} objektide kustutamine ebaõnnestus: {error}", + "Failed to delete webhook": "Veebihaagi kustutamine ebaõnnestus", + "Failed to depublish register: {error}": "Registri avaldamise tühistamine ebaõnnestus: {error}", + "Failed to depublish schema: {error}": "Skeemi avaldamise tühistamine ebaõnnestus: {error}", + "Failed to depublish: {error}": "Avaldamise tühistamine ebaõnnestus: {error}", + "Failed to discover files: {error}": "Failide avastamine ebaõnnestus: {error}", + "Failed to download API specification": "API spetsifikatsiooni allalaadimine ebaõnnestus", + "Failed to export data": "Andmete eksportimine ebaõnnestus", + "Failed to export objects": "Objektide eksportimine ebaõnnestus", + "Failed to fetch conversation history": "Vestluse ajaloo toomine ebaõnnestus", + "Failed to fix mismatched SOLR fields: {error}": "Mittevastavate SOLR väljade parandamine ebaõnnestus: {error}", + "Failed to get SOLR field configuration": "SOLR välja konfiguratsiooni hankimine ebaõnnestus", + "Failed to get chat statistics": "Vestluse statistika hankimine ebaõnnestus", + "Failed to get database information: %s": "Andmebaasi teabe hankimine ebaõnnestus: %s", + "Failed to get entity categories": "Olemi kategooriate hankimine ebaõnnestus", + "Failed to get entity statistics": "Olemi statistika hankimine ebaõnnestus", + "Failed to get entity types": "Olemi tüüpide hankimine ebaõnnestus", + "Failed to get response: {error}": "Vastuse hankimine ebaõnnestus: {error}", + "Failed to invalidate app store cache: {error}": "App Store vahemälu kehtetuks tunnistamine ebaõnnestus: {error}", + "Failed to leave organisation: {error}": "Organisatsioonist lahkumine ebaõnnestus: {error}", + "Failed to link object": "Objekti linkimine ebaõnnestus", + "Failed to load Deck cards": "Deck kaartide laadimine ebaõnnestus", + "Failed to load LLM configuration": "LLM konfiguratsiooni laadimine ebaõnnestus", + "Failed to load Nextcloud groups": "Nextcloud gruppide laadimine ebaõnnestus", + "Failed to load cache statistics: {error}": "Vahemälu statistika laadimine ebaõnnestus: {error}", + "Failed to load conversation": "Vestluse laadimine ebaõnnestus", + "Failed to load entities": "Olemite laadimine ebaõnnestus", + "Failed to load entity": "Olemi laadimine ebaõnnestus", + "Failed to load extraction data": "Eraldamise andmete laadimine ebaõnnestus", + "Failed to load files": "Failide laadimine ebaõnnestus", + "Failed to load linked contacts": "Lingitud kontaktide laadimine ebaõnnestus", + "Failed to load linked emails": "Lingitud e-kirjade laadimine ebaõnnestus", + "Failed to load linked events": "Lingitud sündmuste laadimine ebaõnnestus", + "Failed to load organisations": "Organisatsioonide laadimine ebaõnnestus", + "Failed to load register data": "Registri andmete laadimine ebaõnnestus", + "Failed to load related entities": "Seotud olemite laadimine ebaõnnestus", + "Failed to load settings: {error}": "Seadete laadimine ebaõnnestus: {error}", + "Failed to load statistics: {error}": "Statistika laadimine ebaõnnestus: {error}", + "Failed to load templates": "Mallide laadimine ebaõnnestus", + "Failed to load view: {error}": "Vaate laadimine ebaõnnestus: {error}", + "Failed to load webhooks": "Veebihaakide laadimine ebaõnnestus", + "Failed to load workflows": "Töövoogude laadimine ebaõnnestus", + "Failed to process message": "Sõnumi töötlemine ebaõnnestus", + "Failed to publish register: {error}": "Registri avaldamine ebaõnnestus: {error}", + "Failed to publish schema: {error}": "Skeemi avaldamine ebaõnnestus: {error}", + "Failed to publish: {error}": "Avaldamine ebaõnnestus: {error}", + "Failed to refresh database information": "Andmebaasi teabe värskendamine ebaõnnestus", + "Failed to reindex collection": "Kollektsiooni uuesti indekseerimine ebaõnnestus", + "Failed to remove avatar": "Avatari eemaldamine ebaõnnestus", + "Failed to remove link": "Lingi eemaldamine ebaõnnestus", + "Failed to remove member: {error}": "Liikme eemaldamine ebaõnnestus: {error}", + "Failed to remove schema {schema}: {error}": "Skeemi {schema} eemaldamine ebaõnnestus: {error}", + "Failed to remove user from organisation": "Kasutaja organisatsioonist eemaldamine ebaõnnestus", + "Failed to rename conversation": "Vestluse ümbernimetamine ebaõnnestus", + "Failed to request deactivation": "Deaktiveerimise taotlemine ebaõnnestus", + "Failed to restore conversation": "Vestluse taastamine ebaõnnestus", + "Failed to retrieve extraction status.": "Eraldamise oleku hankimine ebaõnnestus.", + "Failed to retrieve objects for file.": "Faili objektide hankimine ebaõnnestus.", + "Failed to retry extraction": "Eraldamise uuesti proovimine ebaõnnestus", + "Failed to retry webhook": "Veebihaagi uuesti proovimine ebaõnnestus", + "Failed to revoke token": "Loa tühistamine ebaõnnestus", + "Failed to save GitHub token": "GitHub loa salvestamine ebaõnnestus", + "Failed to save GitLab URL": "GitLab URL-i salvestamine ebaõnnestus", + "Failed to save GitLab token": "GitLab loa salvestamine ebaõnnestus", + "Failed to save LLM settings: {error}": "LLM seadete salvestamine ebaõnnestus: {error}", + "Failed to save additional feedback": "Lisatagasiside salvestamine ebaõnnestus", + "Failed to save configuration.": "Konfiguratsiooni salvestamine ebaõnnestus.", + "Failed to save configuration: {error}": "Konfiguratsiooni salvestamine ebaõnnestus: {error}", + "Failed to save facet configuration": "Tahu konfiguratsiooni salvestamine ebaõnnestus", + "Failed to save feedback": "Tagasiside salvestamine ebaõnnestus", + "Failed to save file settings: {error}": "Faili seadete salvestamine ebaõnnestus: {error}", + "Failed to save n8n configuration": "n8n konfiguratsiooni salvestamine ebaõnnestus", + "Failed to save preferences": "Eelistuste salvestamine ebaõnnestus", + "Failed to save roles": "Rollide salvestamine ebaõnnestus", + "Failed to save settings": "Seadete salvestamine ebaõnnestus", + "Failed to save settings: {error}": "Seadete salvestamine ebaõnnestus: {error}", + "Failed to save view: {error}": "Vaate salvestamine ebaõnnestus: {error}", + "Failed to save warmup interval: {error}": "Soojendusintervalli salvestamine ebaõnnestus: {error}", + "Failed to save webhook": "Veebihaagi salvestamine ebaõnnestus", + "Failed to send feedback": "Tagasiside saatmine ebaõnnestus", + "Failed to set active organisation: {error}": "Aktiivse organisatsiooni määramine ebaõnnestus: {error}", + "Failed to setup SOLR: {error}": "SOLR-i seadistamine ebaõnnestus: {error}", + "Failed to start file warmup: {error}": "Failide soojendamise käivitamine ebaõnnestus: {error}", + "Failed to start processing {type} files: {error}": "Failide {type} töötlemise käivitamine ebaõnnestus: {error}", + "Failed to start vectorization: {error}": "Vektoriseerimise käivitamine ebaõnnestus: {error}", + "Failed to switch organisation: {error}": "Organisatsiooni vahetamine ebaõnnestus: {error}", + "Failed to sync magic table for {schema}: {error}": "Maagilise tabeli sünkroonimine skeemi {schema} jaoks ebaõnnestus: {error}", + "Failed to test SOLR connection: {error}": "SOLR-i ühenduse testimine ebaõnnestus: {error}", + "Failed to test webhook": "Veebihaagi testimine ebaõnnestus", + "Failed to update LLM settings: {error}": "LLM-i sätete uuendamine ebaõnnestus: {error}", + "Failed to update Multitenancy settings: {error}": "Mitmerentniku sätete uuendamine ebaõnnestus: {error}", + "Failed to update RBAC settings: {error}": "RBAC-i sätete uuendamine ebaõnnestus: {error}", + "Failed to update Retention settings: {error}": "Säilitamise sätete uuendamine ebaõnnestus: {error}", + "Failed to update SOLR settings: {error}": "SOLR-i sätete uuendamine ebaõnnestus: {error}", + "Failed to update favorite status": "Lemmiku oleku uuendamine ebaõnnestus", + "Failed to update schema configuration for {schema}: {error}": "Skeemi {schema} seadistuse uuendamine ebaõnnestus: {error}", + "Failed to update schema properties": "Skeemi atribuutide uuendamine ebaõnnestus", + "Failed to update view": "Vaate uuendamine ebaõnnestus", + "Failed to update view: {error}": "Vaate uuendamine ebaõnnestus: {error}", + "Failed to update webhook": "Veebihaagi uuendamine ebaõnnestus", + "Failed to upload avatar": "Avatari üleslaadimine ebaõnnestus", + "Failed to validate {schema}: {error}": "Skeemi {schema} valideerimine ebaõnnestus: {error}", + "Failed to vectorize files: {error}": "Failide vektoriseerimine ebaõnnestus: {error}", + "Failed to warmup names cache: {error}": "Nimede vahemälu soojendamine ebaõnnestus: {error}", + "Federation": "Föderatsioon", + "Feedback recorded": "Tagasiside salvestatud", + "Fewer sources (1-3):": "Vähem allikaid (1-3):", + "Field": "Väli", + "Fields to Display": "Kuvatavad väljad", + "File": "Fail", + "File #{id}": "Fail #{id}", + "File Chunk Prediction": "Failitükkide ennustamine", + "File Collection": "Failikogu", + "File Handling": "Failide käsitlemine", + "File Location": "Faili asukoht", + "File Management": "Failide haldus", + "File Name": "Faili nimi", + "File Path": "Faili tee", + "File Type Selection": "Failitüübi valik", + "File Types to Process": "Töödeldavad failitüübid", + "File Vectorization": "Failide vektoriseerimine", + "File Warmup": "Failide soojendamine", + "File actions menu": "Faili toimingute menüü", + "File anonymized successfully": "Fail anonüümiti edukalt", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Failitükkide vektoriseerimine ei ole veel teostatud. Tükid on valmis ja salvestatud, kuid vektoriseerimisteenus on arendamisel.", + "File collection not configured": "Failikogu ei ole seadistatud", + "File discovery completed": "Failide avastamine lõpetatud", + "File extraction completed": "Failide eraldamine lõpetatud", + "File extraction queued": "Failide eraldamine järjekorda lisatud", + "File indexed successfully": "Fail indekseeriti edukalt", + "File is already anonymized": "Fail on juba anonüümitud", + "File name": "Faili nimi", + "File not found": "Faili ei leitud", + "File settings saved successfully": "Failide sätted salvestati edukalt", + "File settings updated successfully": "Failide sätted uuendati edukalt", + "File sources": "Failiallikad", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Failide vektoriseerimine lõpetatud! {vectorized} tükki vektoriseeriti {files} failist. {failed} ebaõnnestus.", + "File vectorization configuration saved successfully": "Failide vektoriseerimise seadistus salvestati edukalt", + "File vectorization started. Check the statistics section for progress.": "Failide vektoriseerimine alustatud. Edenemise jälgimiseks vaadake statistika jaotist.", + "File warmup completed": "Failide soojendamine lõpetatud", + "Files": "Failid", + "Files added successfully": "Failid lisati edukalt", + "Files with Completed Extraction:": "Lõpetatud eraldamisega failid:", + "Files → fileCollection, Objects → objectCollection": "Failid → fileCollection, Objektid → objectCollection", + "Files → filecollection, objects → objectcollection": "Failid → filecollection, objektid → objectcollection", + "Filter Audit Trails": "Filtreeri auditijälgi", + "Filter Deleted Items": "Filtreeri kustutatud üksusi", + "Filter Objects": "Filtreeri objekte", + "Filter Properties": "Filtreeri atribuute", + "Filter Search Trails": "Filtreeri otsingujälgi", + "Filter Statistics": "Filtreeri statistikat", + "Filter and analyze search trail entries": "Filtreerige ja analüüsige otsingujälgede kirjeid", + "Filter and manage audit trail entries": "Filtreerige ja hallake auditijälgede kirjeid", + "Filter and manage soft deleted items": "Filtreerige ja hallake pehmelt kustutatud üksusi", + "Filter and search entities": "Filtreerige ja otsige üksusi", + "Filter by object ID": "Filtreeri objekti ID järgi", + "Filter by search term": "Filtreeri otsingusõna järgi", + "Filter by type": "Filtreeri tüübi järgi", + "Filter by webhook": "Filtreeri veebihaagi järgi", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filtreerimisandmed laaditi automaatselt. Otsingu täpsustamiseks kasutage alltoodud filtreid.", + "Filter fields...": "Filtreeri välju...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtreerige veebihaagi käivitajaid kasuliku koormuse atribuutide järgi (üks rea kohta, vorming: võti: väärtus)", + "Filtered": "Filtreeritud", + "Filters": "Filtrid", + "Finalizing setup...": "Seadistuse lõpetamine...", + "Fireworks AI Chat Settings": "Fireworks AI vestluse sätted", + "Fireworks AI Embedding Configuration": "Fireworks AI manustamise seadistus", + "First": "Esimene", + "Fixed": "Fikseeritud", + "For chat and retrieval-augmented generation": "Vestluse ja otsingupõhise genereerimise jaoks", + "For vector embeddings and semantic search": "Vektormanustuste ja semantilise otsingu jaoks", + "Format": "Vorming", + "Format Issue": "Vormingu probleem", + "Format constraint is missing": "Vormingu piirang puudub", + "From (e.g. -7 days or 2025-01-01)": "Alates (nt -7 päeva või 2025-01-01)", + "From Date": "Alguskuupäev", + "From date": "Alguskuupäev", + "Full data copied to clipboard": "Kõik andmed kopeeriti lõikelauale", + "Function Tools": "Funktsioonitööriistad", + "General Issue": "Üldine probleem", + "Generate recommendations and confidence scores": "Genereeri soovitused ja usaldusskoorid", + "Generate report": "Genereeri aruanne", + "Generate the verantwoordingsdocument": "Genereeri verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Genereeri vektorid kohe, kui luuakse uued objektid", + "Generated: {time}": "Genereeritud: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Saage reaalajas ülevaade oma organisatsiooni andmete seisukorrast, keskendudes registritele, skeemimääratlustele ning objektide salvestamisele ja kasutamisele.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Saage reaalajas ülevaade oma organisatsiooni andmete seisukorrast, kasvutrendidest ja kvaliteedinäitajatest.", + "GitHub Personal Access Token": "GitHubi isiklik juurdepääsuluba", + "GitHub token is valid": "GitHubi luba on kehtiv", + "GitHub token is valid! Username: {username}": "GitHubi luba on kehtiv! Kasutajanimi: {username}", + "GitHub token removed successfully": "GitHubi luba eemaldati edukalt", + "GitHub token saved successfully": "GitHubi luba salvestati edukalt", + "GitHub token test failed: {message}": "GitHubi loa testimine ebaõnnestus: {message}", + "GitLab Personal Access Token": "GitLabi isiklik juurdepääsuluba", + "GitLab URL saved successfully": "GitLabi URL salvestati edukalt", + "GitLab token is valid": "GitLabi luba on kehtiv", + "GitLab token is valid! Username: {username}": "GitLabi luba on kehtiv! Kasutajanimi: {username}", + "GitLab token saved successfully": "GitLabi luba salvestati edukalt", + "GitLab token test failed: {message}": "GitLabi loa testimine ebaõnnestus: {message}", + "Go to linked Register": "Mine seotud Registri juurde", + "Go to linked Schema": "Mine seotud Skeemi juurde", + "Go to organisation": "Mine organisatsiooni juurde", + "Grant additional permissions beyond standard CRUD operations": "Anna lisaõigused lisaks tavapärastele CRUD-toimingutele", + "Group Quota": "Grupi kvoot", + "Groups": "Grupid", + "HTML preview": "HTML-i eelvaade", + "HTTP Method": "HTTP-meetod", + "HTTP method used to send webhook requests": "Veebihaagi päringute saatmiseks kasutatav HTTP-meetod", + "Headers": "Päised", + "Health": "Seisukord", + "Heartbeat successful - connection kept alive": "Südamelöök õnnestus – ühendus hoiti elus", + "Helpful": "Abivalmis", + "Here you can configure the details for various connections.": "Siin saate seadistada erinevate ühenduste üksikasju.", + "Here you can configure the details for your organisation.": "Siin saate seadistada oma organisatsiooni üksikasju.", + "Hide": "Peida", + "Hide Filters": "Peida filtrid", + "Hide in collection view": "Peida kogu vaates", + "Hide in forms": "Peida vormides", + "Hide in list view": "Peida loendivaates", + "High": "Kõrge", + "High Confidence": "Kõrge usaldus", + "Host": "Host", + "Hourly": "Iga tund", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Kui sügavale läbida pesastatud objekti atribuute (1-20). Kõrgemad väärtused jäädvustavad rohkem üksikasju, kuid suurendavad vektori suurust.", + "How often to check for updates (1-168 hours)": "Kui sageli kontrollida uuendusi (1-168 tundi)", + "How to handle retries for failed webhook deliveries": "Kuidas käsitleda korduskatseid ebaõnnestunud veebihaagi edastuste puhul", + "IBANs": "IBANid", + "ID": "ID", + "ID of input mapping (optional)": "Sisendvastenduse ID (valikuline)", + "ID of output mapping (optional)": "Väljundvastenduse ID (valikuline)", + "ID of the target resource": "Sihtressursi ID", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "IP päringusageduse piirangud kustutati edukalt", + "Identify properties not in the schema": "Tuvasta atribuudid, mida skeemis ei ole", + "Immutable": "Muutumatu", + "Import": "Impordi", + "Import Configuration": "Impordi seadistus", + "Import Data into Register": "Impordi andmed Registrisse", + "Import successful": "Import õnnestus", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importige mall report-bundle.json, et saada register `reports`, seejärel looge oma esimene töölaud standardse objekti kasutajaliidese kaudu. Töölauad määratlevad oma vidinad JSON-vormingus ning renderdaja edastab igale vidinale reaalajas koondandmed.", + "Improved Property": "Täiustatud atribuut", + "In use": "Kasutusel", + "Inactive": "Mitteaktiivne", + "Inactive organisations cannot be used": "Mitteaktiivseid organisatsioone ei saa kasutada", + "Include IDs and names of related objects for better contextual search": "Kaasa seotud objektide ID-d ja nimed parema kontekstipõhise otsingu jaoks", + "Include ids and names of related objects for better contextual search": "Kaasa seotud objektide ID-d ja nimed parema kontekstipõhise otsingu jaoks", + "Include related object references": "Kaasa seotud objektide viited", + "Include schema and register metadata": "Kaasa skeemi ja registri metaandmed", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Teave praeguse OpenRegister installatsiooni kohta", + "Inherited from register": "Päritud registrist", + "Inherits permissions from register": "Pärib õigused registrist", + "Initialization failed": "Lähtestamine ebaõnnestus", + "Initialize Project": "Lähtesta projekt", + "Initializing SOLR setup...": "SOLR-i seadistuse lähtestamine...", + "Input Mapping": "Sisendvastendus", + "Inspect Fields": "Kontrolli välju", + "Inspect Index": "Kontrolli indeksit", + "Inspect SOLR Index": "Kontrolli SOLR-i indeksit", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Reaalajas uuenduste lubamiseks installige Nextcloudi rakenduste poest rakendus notify_push.", + "Integrations": "Integratsioonid", + "Internet location (URL) of this installation": "Selle installatsiooni internetiasukoht (URL)", + "Interval": "Intervall", + "Invalid": "Kehtetu", + "Invalid batch size. Must be between 1 and 5000": "Kehtetu partii suurus. Peab olema vahemikus 1 kuni 5000", + "Invalid feedback type": "Kehtetu tagasiside tüüp", + "Invalid field name provided": "Esitati kehtetu välja nimi", + "Invalid maxObjects. Must be 0 (all) or positive number": "Kehtetu maxObjects. Peab olema 0 (kõik) või positiivne arv", + "Invalid:": "Kehtetu:", + "Invite Users": "Kutsu kasutajaid", + "Invited Users": "Kutsutud kasutajad", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Inzage tulemused", + "Issue": "Probleem", + "Items per page:": "Üksusi lehe kohta:", + "Just now": "Just praegu", + "Key": "Võti", + "LLM Configuration": "LLM-i seadistus", + "LLM actions menu": "LLM-i toimingute menüü", + "LLM configuration saved successfully": "LLM-i seadistus salvestati edukalt", + "LLM features disabled": "LLM-i funktsioonid keelatud", + "LLM features enabled": "LLM-i funktsioonid lubatud", + "LLM must be enabled with an embedding provider configured": "LLM peab olema lubatud ja manustamise pakkuja seadistatud", + "LLM settings saved successfully": "LLM-i sätted salvestati edukalt", + "LLM settings updated successfully": "LLM-i sätted uuendati edukalt", + "Label": "Silt", + "Labels": "Sildid", + "Languages": "Keeled", + "Last": "Viimane", + "Last Triggered": "Viimati käivitatud", + "Leave empty to allow all users access": "Jätke tühjaks, et lubada juurdepääs kõigile kasutajatele", + "Leave off for auto-detection from property format.": "Jätke väljas, et tuvastada automaatselt atribuudi vormingust.", + "Left organisation successfully": "Organisatsioonist lahkuti edukalt", + "Legal basis *": "Õiguslik alus *", + "Length Range:": "Pikkuse vahemik:", + "Linear": "Lineaarne", + "Link": "Link", + "Link an email from the Mail app sidebar to associate it with this object.": "Lingi e-kiri Mail-rakenduse külgribalt, et seostada see selle objektiga.", + "Link an object to see it here.": "Lingi objekt, et seda siin näha.", + "Link another object": "Lingi teine objekt", + "Link existing event": "Lingi olemasolev sündmus", + "Link removed": "Link eemaldatud", + "Link to Object": "Lingi objektiga", + "Link to {name}": "Lingi objektiga {name}", + "Linked Objects": "Seotud objektid", + "Linked by {user}": "Linkis {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Seotud e-kirjad, kalendrisündmused, kontaktid ja Deck-kaardid kuvatakse siin.", + "Linked to {name}": "Seotud objektiga {name}", + "Load Advanced Filters": "Laadi täpsemad filtrid", + "Load advanced filters with live data from your search index": "Laadi täpsemad filtrid reaalajas andmetega oma otsinguindeksist", + "Load more": "Laadi rohkem", + "Load view": "Laadi vaade", + "Loading API tokens...": "API-lubade laadimine...", + "Loading ConfigSets...": "ConfigSetide laadimine...", + "Loading LLM configuration...": "LLM-i seadistuse laadimine...", + "Loading RBAC settings...": "RBAC-i sätete laadimine...", + "Loading activity...": "Tegevuse laadimine...", + "Loading advanced filters...": "Täpsemate filtrite laadimine...", + "Loading agents...": "Agentide laadimine...", + "Loading application details...": "Rakenduse üksikasjade laadimine...", + "Loading applications...": "Rakenduste laadimine...", + "Loading archived conversations...": "Arhiveeritud vestluste laadimine...", + "Loading audit trails...": "Auditijälgede laadimine...", + "Loading available tools...": "Saadaolevate tööriistade laadimine...", + "Loading cache statistics...": "Vahemälu statistika laadimine...", + "Loading collections...": "Kogude laadimine...", + "Loading configurations...": "Seadistuste laadimine...", + "Loading conversation...": "Vestluse laadimine...", + "Loading conversations...": "Vestluste laadimine...", + "Loading deleted items...": "Kustutatud üksuste laadimine...", + "Loading endpoints": "Lõpp-punktide laadimine", + "Loading entities...": "Üksuste laadimine...", + "Loading events...": "Sündmuste laadimine...", + "Loading filter data...": "Filtreerimisandmete laadimine...", + "Loading groups...": "Gruppide laadimine...", + "Loading linked objects...": "Seotud objektide laadimine...", + "Loading log details...": "Logi üksikasjade laadimine...", + "Loading multitenancy settings...": "Mitmerentniku sätete laadimine...", + "Loading n8n configuration...": "n8n seadistuse laadimine...", + "Loading permission matrix...": "Õiguste maatriksi laadimine...", + "Loading preferences...": "Eelistuste laadimine...", + "Loading register data...": "Registri andmete laadimine...", + "Loading registers...": "Registrite laadimine...", + "Loading retention settings...": "Säilitamise sätete laadimine...", + "Loading schemas...": "Skeemide laadimine...", + "Loading search configuration...": "Otsingu seadistuse laadimine...", + "Loading search trails...": "Otsingujälgede laadimine...", + "Loading sources...": "Allikate laadimine...", + "Loading statistics...": "Statistika laadimine...", + "Loading text extraction settings...": "Teksti eraldamise sätete laadimine...", + "Loading tokens...": "Lubade laadimine...", + "Loading user groups...": "Kasutajagruppide laadimine...", + "Loading users...": "Kasutajate laadimine...", + "Loading views...": "Vaadete laadimine...", + "Loading...": "Laadimine...", + "Local": "Kohalik", + "Local Version": "Kohalik versioon", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Leidke iga objekt, mis viitab andmesubjektile (Art 15 inzage), kuvage kustutamise eelvaade (Art 17 vergetelheid) või eksportige nende andmed (Art 20 portabiliteit).", + "Location": "Asukoht", + "Location Field": "Asukoha väli", + "Locations": "Asukohad", + "Locked": "Lukustatud", + "Locked:": "Lukustatud:", + "Login successful": "Sisselogimine õnnestus", + "Logs": "Logid", + "Low": "Madal", + "Low Confidence": "Madal usaldus", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Madalamad numbrid kuvatakse filtri külgribal esimesena. Automaatse järjestamise jaoks jätke tühjaks.", + "Lucene Version:": "Lucene'i versioon:", + "Magic table sync completed for {schema}": "Maagilise tabeli sünkroonimine lõpetatud skeemi {schema} jaoks", + "Magic table synchronized successfully": "Maagiline tabel sünkrooniti edukalt", + "Mail Integration": "Mail-integratsioon", + "Mail integration is not available": "Mail-integratsioon ei ole saadaval", + "Make this installation discoverable within the federation network.": "Tehke see installatsioon föderatsioonivõrgus leitavaks.", + "Manage Organisation Roles": "Halda organisatsiooni rolle", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Hallake SOLR-i kogusid (andmehoidlaid) ja määrake need objektide ja failide jaoks.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Hallake oma kogude SOLR-i ConfigSete (seadistusmalle).", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Hallake SOLR-i kogusid (andmehoidlaid) ja määrake need objektide ja failide jaoks.", + "Manage SOLR configsets (configuration templates) for your collections.": "Hallake oma kogude SOLR-i configsete (seadistusmalle).", + "Manage and configure agents for automated tasks": "Hallake ja seadistage agente automatiseeritud ülesannete jaoks", + "Manage and monitor file text extraction status": "Hallake ja jälgige failide teksti eraldamise olekut", + "Manage and restore soft deleted items from your registers": "Hallake ja taastage oma registritest pehmelt kustutatud üksusi", + "Manage and view detected entities from files and objects": "Hallake ja vaadake failidest ja objektidest tuvastatud üksusi", + "Manage document templates and themes": "Hallake dokumendimalle ja teemasid", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Hallake n8n töövooge OpenRegisteri automatiseerimiseks. Töövood salvestatakse seadistatud projekti.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Hallake töötlemistoiminguid, käivitage andmesubjekti juurdepääsutaotlusi ning auditeerige vastavust EL-i GDPR / Hollandi AVG alusel.", + "Manage webhooks for event-driven integrations": "Hallake veebihaake sündmuspõhiste integratsioonide jaoks", + "Manage your account settings, security, and personal data.": "Hallake oma konto sätteid, turvalisust ja isikuandmeid.", + "Manage your applications and modules": "Hallake oma rakendusi ja mooduleid", + "Manage your chat conversations": "Hallake oma vestlusi", + "Manage your data registers and their configurations": "Hallake oma andmeregistreid ja nende seadistusi", + "Manage your data schemas and their properties": "Hallake oma andmeskeeme ja nende atribuute", + "Manage your data sources and their configurations": "Hallake oma andmeallikaid ja nende seadistusi", + "Manage your organisations and switch between them": "Hallake oma organisatsioone ja vahetage nende vahel", + "Manage your saved search configurations": "Hallake oma salvestatud otsinguseadistusi", + "Manage your system configurations and settings": "Hallake oma süsteemiseadistusi ja sätteid", + "Managed": "Hallatud", + "Mappings": "Vastendused", + "Mass validation completed successfully": "Massvalideerimine lõpetati edukalt", + "Mass validation failed: {error}": "Massvalideerimine ebaõnnestus: {error}", + "Max Files (0 = all)": "Maksimaalne failide arv (0 = kõik)", + "Max Length": "Maksimaalne pikkus", + "Max Length:": "Maksimaalne pikkus:", + "Max Objects (0 = all)": "Maksimaalne objektide arv (0 = kõik)", + "Max Retries": "Maksimaalne korduskatsete arv", + "Max Shards/Node": "Maksimaalselt kilde/sõlm", + "Max Tokens": "Maksimaalne tokenite arv", + "Max execution time (ms)": "Maksimaalne täitmisaeg (ms)", + "Max ms": "Maks ms", + "Max result count": "Maksimaalne tulemuste arv", + "Max results": "Maksimaalsed tulemused", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksimaalne faili suurus (MB)", + "Maximum Nesting Depth": "Maksimaalne pesastamise sügavus", + "Maximum length": "Maksimaalne pikkus", + "Maximum length constraint is missing": "Maksimaalse pikkuse piirang puudub", + "Maximum length is too restrictive": "Maksimaalne pikkus on liiga piirav", + "Maximum number of files to process. Set to 0 to process all files.": "Maksimaalne töödeldavate failide arv. Kõigi failide töötlemiseks määrake väärtuseks 0.", + "Maximum number of items": "Maksimaalne üksuste arv", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksimaalne töödeldavate objektide arv. Kõigi objektide töötlemiseks määrake väärtuseks 0.", + "Maximum number of retry attempts for failed deliveries": "Maksimaalne korduskatsete arv ebaõnnestunud edastuste puhul", + "Maximum time to wait before committing changes": "Maksimaalne ooteaeg enne muudatuste kinnitamist", + "Maximum tokens to generate": "Maksimaalne genereeritavate tokenite arv", + "Maximum value": "Maksimaalne väärtus", + "Maximum value constraint is missing": "Maksimaalse väärtuse piirang puudub", + "Maximum value is too restrictive": "Maksimaalne väärtus on liiga piirav", + "Medium": "Keskmine", + "Medium Confidence": "Keskmine usaldus", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Keskmised päringud: otsingud mõningase filtreerimise või mitme parameetriga (nt kuupäevavahemikud, kindlad registrid/skeemid)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Keskmised päringud: otsingud mõningase filtreerimise või mitme parameetriga (nt kuupäevavahemikud, kindlad registrid/skeemid)", + "Member removed successfully": "Liige eemaldati edukalt", + "Members": "Liikmed", + "Members ({count})": "Liikmed ({count})", + "Members of selected groups can access this view": "Valitud gruppide liikmed pääsevad sellele vaatele juurde", + "Members:": "Liikmed:", + "Memory prediction calculated successfully": "Mälu ennustus arvutati edukalt", + "Message content is required": "Sõnumi sisu on nõutav", + "Message does not belong to this conversation": "Sõnum ei kuulu sellesse vestlusse", + "Message not found": "Sõnumit ei leitud", + "Metadata": "Metaandmed", + "Metadata Filters": "Metaandmete filtrid", + "Method": "Meetod", + "Method*": "Meetod*", + "Method: {method}": "Meetod: {method}", + "Min Length": "Minimaalne pikkus", + "Min execution time (ms)": "Minimaalne täitmisaeg (ms)", + "Min ms": "Min ms", + "Min result count": "Minimaalne tulemuste arv", + "Min results": "Minimaalsed tulemused", + "Minimum": "Miinimum", + "Minimum length": "Minimaalne pikkus", + "Minimum number of items": "Minimaalne üksuste arv", + "Minimum value": "Minimaalne väärtus", + "Minimum value constraint is missing": "Minimaalse väärtuse piirang puudub", + "Minimum value is too restrictive": "Minimaalne väärtus on liiga piirav", + "Missing conversation or agentUuid": "Puudub vestlus või agentUuid", + "Missing conversationId": "Puudub conversationId", + "Missing message": "Puudub sõnum", + "Missing object fields created successfully": "Puuduvad objekti väljad loodi edukalt", + "Mode:": "Režiim:", + "Model": "Mudel", + "Model Name": "Mudeli nimi", + "Modified": "Muudetud", + "Monthly": "Iga kuu", + "More sources (10-20):": "Rohkem allikaid (10-20):", + "Most Active Objects": "Kõige aktiivsemad objektid", + "Multiple Of": "Kordne", + "Multiple of": "Kordne", + "Multiple search trail deletion not implemented yet": "Mitme otsingujälje kustutamine ei ole veel teostatud", + "Multitenancy": "Mitmerentnikkus", + "Multitenancy settings updated successfully": "Mitmerentniku sätted uuendati edukalt", + "My Account": "Minu konto", + "Naam": "Naam", + "Naam *": "Nimi *", + "Name": "Nimi", + "Name *": "Nimi *", + "Name*": "Nimi*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Nimede vahemälu eelsoojendati edukalt: {count} nime laaditi ajaga {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Nimede vahemälu eelsoojendati edukalt: {count} nime laaditi ajaga {time}. {message}", + "Names cache warmup completed": "Nimede vahemälu eelsoojendus lõpetatud", + "Naming pattern for copies:": "Koopiate nimetamise muster:", + "New Collection Name": "Uue kogu nimi", + "New Configuration": "Uus konfiguratsioon", + "New Conversation": "Uus vestlus", + "New Properties": "Uued atribuudid", + "New Property": "Uus atribuut", + "New User Group": "Uus kasutajagrupp", + "New Value": "Uus väärtus", + "New activity": "Uus tegevus", + "New password": "Uus parool", + "New processing activity": "Uus töötlemistoiming", + "New users without specific organisation membership will be automatically added to this organisation": "Kasutajad ilma konkreetse organisatsiooni liikmesuseta lisatakse automaatselt sellesse organisatsiooni", + "New verwerkingsactiviteit": "Uus verwerkingsactiviteit", + "Next": "Järgmine", + "Nextcloud Groups": "Nextcloudi grupid", + "No": "Ei", + "No API tokens.": "API-märgendeid pole.", + "No ConfigSets found": "ConfigSet'e ei leitud", + "No Deck cards linked to this object": "Selle objektiga pole seotud ühtegi Deck-kaarti", + "No GitHub token configured": "GitHubi märgendit pole seadistatud", + "No GitHub token provided": "GitHubi märgendit ei esitatud", + "No GitLab token provided": "GitLabi märgendit ei esitatud", + "No Organisation": "Organisatsioon puudub", + "No active filters": "Aktiivseid filtreid pole", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Skeemil {schema} pole aktiivseid objekte pehmeks kustutamiseks. Kasutage funktsiooni \"Kustuta jäädavalt\", et eemaldada pehmelt kustutatud objektid.", + "No activity data available": "Tegevusandmed puuduvad", + "No activity found.": "Tegevusi ei leitud.", + "No agents available": "Agente pole saadaval", + "No agents found": "Agente ei leitud", + "No app store cache exists yet": "Rakenduspoe vahemälu pole veel olemas", + "No applications are available.": "Rakendusi pole saadaval.", + "No applications found": "Rakendusi ei leitud", + "No approval steps for this object.": "Sellel objektil pole kinnitamise samme.", + "No archived conversations": "Arhiveeritud vestlusi pole", + "No audit trail entries found": "Auditijälje kirjeid ei leitud", + "No changes recorded": "Muudatusi ei salvestatud", + "No chunks to vectorize": "Vektoriseeritavaid tükke pole", + "No collections found": "Kogusid ei leitud", + "No configuration data": "Konfiguratsiooniandmed puuduvad", + "No configuration selected": "Konfiguratsiooni pole valitud", + "No configurations are available.": "Konfiguratsioone pole saadaval.", + "No configurations found": "Konfiguratsioone ei leitud", + "No configurations found for this application.": "Selle rakenduse jaoks ei leitud konfiguratsioone.", + "No contacts linked to this object": "Selle objektiga pole seotud ühtegi kontakti", + "No conversations yet. Create a new one to get started!": "Vestlusi pole veel. Looge uus, et alustada!", + "No dashboards yet": "Töölaudu pole veel", + "No data available": "Andmeid pole saadaval", + "No data available for chart": "Diagrammi jaoks pole andmeid saadaval", + "No deleted items found": "Kustutatud üksusi ei leitud", + "No deletion data available": "Kustutamisandmed puuduvad", + "No description found": "Kirjeldust ei leitud", + "No description provided": "Kirjeldust ei esitatud", + "No emails linked to this object": "Selle objektiga pole seotud ühtegi e-kirja", + "No endpoints defined": "Lõpp-punkte pole määratletud", + "No entities detected for this email.": "Selle e-kirja jaoks ei tuvastatud üksusi.", + "No entities detected in this file. Run text extraction first.": "Selles failis ei tuvastatud üksusi. Käivitage esmalt teksti eraldamine.", + "No entities found": "Üksusi ei leitud", + "No entities have been detected yet": "Üksusi pole veel tuvastatud", + "No events found": "Sündmusi ei leitud", + "No events linked to this object": "Selle objektiga pole seotud ühtegi sündmust", + "No expired audit trails found to clear": "Kustutamiseks ei leitud aegunud auditijälgi", + "No expired search trails found to clear": "Kustutamiseks ei leitud aegunud otsingujälgi", + "No extraction data available for this file": "Selle faili jaoks pole eraldamisandmeid saadaval", + "No facetable fields available. Select a register and schema to see available filters.": "Tahuväljad pole saadaval. Valige register ja skeem, et näha saadaolevaid filtreid.", + "No files found": "Faile ei leitud", + "No files have been extracted yet": "Faile pole veel eraldatud", + "No files selected": "Faile pole valitud", + "No files to process": "Töötlemiseks pole faile", + "No files to reindex": "Uuesti indekseerimiseks pole faile", + "No filters are currently active. This will delete ALL audit trail entries!": "Praegu pole ükski filter aktiivne. See kustutab KÕIK auditijälje kirjed!", + "No groups found. Try a different search.": "Gruppe ei leitud. Proovige teist otsingut.", + "No label": "Silt puudub", + "No labels": "Silte pole", + "No linked objects": "Seotud objekte pole", + "No log entries found": "Logikirjeid ei leitud", + "No logs are available for this configuration.": "Selle konfiguratsiooni jaoks pole logisid saadaval.", + "No logs are available for this source.": "Selle allika jaoks pole logisid saadaval.", + "No logs found": "Logisid ei leitud", + "No matches": "Vasteid pole", + "No mismatched fields found - SOLR schema is properly configured": "Sobimatuid välju ei leitud – SOLR skeem on õigesti seadistatud", + "No objects are currently selected for deletion.": "Praegu pole kustutamiseks valitud ühtegi objekti.", + "No objects are currently selected for permanent deletion.": "Praegu pole jäädavaks kustutamiseks valitud ühtegi objekti.", + "No objects are currently selected for restoration.": "Praegu pole taastamiseks valitud ühtegi objekti.", + "No objects found": "Objekte ei leitud", + "No objects found to vectorize": "Vektoriseerimiseks ei leitud objekte", + "No objects linked to this email": "Selle e-kirjaga pole seotud ühtegi objekti", + "No objects selected": "Objekte pole valitud", + "No objects selected for deletion": "Kustutamiseks pole objekte valitud", + "No objects selected for restoration": "Taastamiseks pole objekte valitud", + "No objects to delete": "Kustutamiseks pole objekte", + "No objects to delete for schema {schema}": "Skeemi {schema} jaoks pole objekte kustutamiseks", + "No objects were permanently deleted": "Ühtegi objekti ei kustutatud jäädavalt", + "No organisations found": "Organisatsioone ei leitud", + "No personal data was found for this subject identifier.": "Selle subjekti identifikaatori jaoks ei leitud isikuandmeid.", + "No processing activities yet": "Töötlemistoiminguid pole veel", + "No properties found": "Atribuute ei leitud", + "No properties match your filters.": "Ükski atribuut ei vasta teie filtritele.", + "No purge date set": "Puhastamise kuupäeva pole määratud", + "No register data available": "Registriandmed puuduvad", + "No register objects reference this file": "Ükski registriobjekt ei viita sellele failile", + "No registers found": "Registreid ei leitud", + "No registers found for this application.": "Selle rakenduse jaoks ei leitud registreid.", + "No registers found. Create a register to configure permissions.": "Registreid ei leitud. Looge register, et seadistada õigused.", + "No related cases found for this sender": "Selle saatja jaoks ei leitud seotud juhtumeid", + "No related entities": "Seotud üksusi pole", + "No relations found": "Seoseid ei leitud", + "No request body available": "Päringu keha pole saadaval", + "No response body available": "Vastuse keha pole saadaval", + "No saved views yet. Create one in the Search tab!": "Salvestatud vaateid pole veel. Looge üks vahekaardil Otsing!", + "No saved views yet. create one in the search tab!": "Salvestatud vaateid pole veel. looge üks vahekaardil otsing!", + "No scan run yet": "Skannimist pole veel käivitatud", + "No schema data available": "Skeemi andmed puuduvad", + "No schema selected for exploration": "Uurimiseks pole skeemi valitud", + "No schemas configured for mail linking.": "E-kirjade sidumiseks pole ühtegi skeemi seadistatud.", + "No schemas found": "Skeeme ei leitud", + "No schemas found for this application.": "Selle rakenduse jaoks ei leitud skeeme.", + "No search terms data available": "Otsingusõnade andmed puuduvad", + "No search trail entries found": "Otsingujälje kirjeid ei leitud", + "No sources are available.": "Allikaid pole saadaval.", + "No sources found": "Allikaid ei leitud", + "No synchronizations found": "Sünkroonimisi ei leitud", + "No templates found": "Malle ei leitud", + "No templates have been created yet": "Malle pole veel loodud", + "No tools available. Tools can be registered by installed apps.": "Tööriistu pole saadaval. Tööriistu saavad registreerida paigaldatud rakendused.", + "No users in this organisation.": "Selles organisatsioonis pole kasutajaid.", + "No valid magic-mapped register+schema combinations found": "Kehtivaid magic-mapped register+skeem kombinatsioone ei leitud", + "No views found. Create views first before configuring vectorization.": "Vaateid ei leitud. Looge esmalt vaated, enne kui seadistate vektoriseerimise.", + "No views match your search": "Ükski vaade ei vasta teie otsingule", + "No webhooks found": "Veebihaake ei leitud", + "No webhooks have been configured yet": "Veebihaake pole veel seadistatud", + "No workflows found in this project. Create workflows in the n8n editor.": "Selles projektis ei leitud töövooge. Looge töövood n8n redaktoris.", + "None": "Puudub", + "Not Found": "Ei leitud", + "Not extracted": "Pole eraldatud", + "Not helpful": "Pole abiks", + "Not in use": "Pole kasutusel", + "Note:": "Märkus:", + "Notifications": "Teavitused", + "Number Constraints": "Arvupiirangud", + "Number of Sources": "Allikate arv", + "Number of chunks to vectorize in one API call (1-100).": "Ühe API-kõnega vektoriseeritavate tükkide arv (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Ühe API-kõnega vektoriseeritavate tükkide arv. Suurem = kiirem, kuid rohkem mälu. Soovitatav: 10-50.", + "Number of objects to process in each batch (1-5000).": "Igas partiis töödeldavate objektide arv (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Ühe API-kõnega vektoriseeritavate objektide arv. Suurem = kiirem, kuid rohkem mälu. Soovitatav: 10-50.", + "Numeric Range:": "Arvuvahemik:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Objektide kogu", + "Object Configuration:": "Objekti konfiguratsioon:", + "Object Count Prediction": "Objektide arvu prognoos", + "Object Extraction Mode": "Objekti eraldamise režiim", + "Object ID": "Objekti ID", + "Object Management": "Objektide haldus", + "Object Text Extraction": "Objekti teksti eraldamine", + "Object Vectorization": "Objekti vektoriseerimine", + "Object Warmup": "Objekti eelsoojendus", + "Object changes in owned objects": "Objekti muudatused omandatud objektides", + "Object collection not configured": "Objektide kogu pole seadistatud", + "Object metadata incomplete for file upload": "Objekti metaandmed on faili üleslaadimiseks puudulikud", + "Object not found": "Objekti ei leitud", + "Object permanently deleted": "Objekt kustutati jäädavalt", + "Object restored successfully": "Objekt taastati edukalt", + "Object settings updated successfully": "Objekti seaded uuendati edukalt", + "Object sources": "Objekti allikad", + "Object successfully deleted": "Objekt kustutati edukalt", + "Object successfully modified": "Objekti muudeti edukalt", + "Object unlocked successfully": "Objekt avati edukalt", + "Object vectorization configuration saved successfully": "Objekti vektoriseerimise konfiguratsioon salvestati edukalt", + "Object vectorized successfully": "Objekt vektoriseeriti edukalt", + "Object:": "Objekt:", + "Objects": "Objektid", + "Objects Analyzed": "Analüüsitud objektid", + "Objects Distribution": "Objektide jaotus", + "Objects being analyzed": "Analüüsitavad objektid", + "Objects by Register": "Objektid registri järgi", + "Objects by Schema": "Objektid skeemi järgi", + "Objects deletion completed for {schema}": "Objektide kustutamine lõpetati skeemi {schema} jaoks", + "Objects deletion completed successfully": "Objektide kustutamine lõpetati edukalt", + "Objects in schema": "Objektid skeemis", + "Objects successfully deleted": "Objektid kustutati edukalt", + "Objects to Process:": "Töödeldavad objektid:", + "Objects to be analyzed": "Analüüsitavad objektid", + "Objects to be deleted": "Kustutatavad objektid", + "Objects to be validated": "Valideeritavad objektid", + "Objects will be serialized as JSON text before vectorization": "Objektid serialiseeritakse enne vektoriseerimist JSON-tekstiks", + "Objects will be soft deleted and moved to the": "Objektid kustutatakse pehmelt ja teisaldatakse", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objektid kustutatakse pehmelt (märgitakse kustutatuks, kuid säilitatakse andmebaasis). Vajadusel saab need hiljem taastada.", + "Old Value": "Vana väärtus", + "Oldest Item (days)": "Vanim üksus (päevades)", + "Ollama Chat Settings": "Ollama vestluse seaded", + "Ollama Embedding Configuration": "Ollama manustamise konfiguratsioon", + "Ollama URL": "Ollama URL", + "Only With Changes": "Ainult muudatustega", + "Only members of selected groups can access this organisation": "Sellele organisatsioonile pääsevad ligi ainult valitud gruppide liikmed", + "Open Nextcloud App Store": "Ava Nextcloudi rakenduspood", + "Open OpenRegister sidebar": "Ava OpenRegister külgriba", + "Open Register Objects": "Ava registriobjektid", + "Open folder": "Ava kaust", + "Open in OpenRegister": "Ava OpenRegisteris", + "Open n8n Editor": "Ava n8n redaktor", + "OpenAI Chat Settings": "OpenAI vestluse seaded", + "OpenAI Embedding Configuration": "OpenAI manustamise konfiguratsioon", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegisteri seaded", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operaatori määratletud töölauad ja ajastatud aruanded. Iga töölaud on esmaklassiline objekt `reports` registris; vidinad deklareeritakse töölaua `widgets` massiivis ja renderdatakse reaalajas agregatsioonidest / GraphQL-ist.", + "Optimizing search performance...": "Otsingu jõudluse optimeerimine...", + "Optional URL-friendly identifier": "Valikuline URL-sõbralik identifikaator", + "Optional description for this facet": "Valikuline kirjeldus sellele tahule", + "Optional end date property": "Valikuline lõppkuupäeva atribuut", + "Optional event description template": "Valikuline sündmuse kirjelduse mall", + "Optional location property": "Valikuline asukoha atribuut", + "Optional webhook secret for signature verification": "Valikuline veebihaagi saladus allkirja kontrollimiseks", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Valikuline: lisage päringuparameetrid viidatud skeemi filtreerimiseks (nt status=active&type=public)", + "Or": "Või", + "Order": "Järjestus", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Järjestatud BCP 47 keelesildid. Esimene keel on registri vaikekeel ja juhib tõlgitavate atribuutide Accept-Language tagavaravalikut.", + "Organisation": "Organisatsioon", + "Organisation Configuration": "Organisatsiooni konfiguratsioon", + "Organisation ID": "Organisatsiooni ID", + "Organisation Members": "Organisatsiooni liikmed", + "Organisation Statistics": "Organisatsiooni statistika", + "Organisation created successfully": "Organisatsioon loodi edukalt", + "Organisation membership changes": "Organisatsiooni liikmesuse muudatused", + "Organisation name is required": "Organisatsiooni nimi on kohustuslik", + "Organisation settings saved successfully": "Organisatsiooni seaded salvestati edukalt", + "Organisation successfully created": "Organisatsioon loodi edukalt", + "Organisation successfully updated": "Organisatsioon uuendati edukalt", + "Organisational measures": "Organisatsioonilised meetmed", + "Organisations": "Organisatsioonid", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organisatsioon", + "Organization ID (Optional)": "Organisatsiooni ID (valikuline)", + "Organizations": "Organisatsioonid", + "Orphaned Items": "Orvuks jäänud üksused", + "Other": "Muu", + "Output Mapping": "Väljundi vastendamine", + "Overview": "Ülevaade", + "Owner": "Omanik", + "Owner:": "Omanik:", + "PDF": "PDF", + "PII hits": "PII tabamused", + "PII rows": "PII read", + "Page {current} of {total}": "Lehekülg {current} / {total}", + "Parallel Mode (Faster, more resource intensive)": "Paralleelrežiim (kiirem, ressursimahukam)", + "Parallel:": "Paralleelne:", + "Password": "Parool", + "Password changes are not supported by your authentication provider.": "Teie autentimisteenuse pakkuja ei toeta parooli muutmist.", + "Password for SOLR authentication (optional)": "Parool SOLR autentimiseks (valikuline)", + "Password updated successfully": "Parool uuendati edukalt", + "Path": "Tee", + "Path in repository": "Tee hoidlas", + "Path where the configuration file will be saved in the repository": "Tee, kuhu konfiguratsioonifail hoidlas salvestatakse", + "Path where the register OAS file will be saved in the repository": "Tee, kuhu registri OAS-fail hoidlas salvestatakse", + "Pattern (regex)": "Muster (regulaaravaldis)", + "Pattern Issue": "Mustri probleem", + "Pattern constraint is missing": "Mustri piirang puudub", + "Patterns:": "Mustrid:", + "Pending": "Ootel", + "Permanent Delete Mode": "Jäädava kustutamise režiim", + "Permanently Delete": "Kustuta jäädavalt", + "Permanently Delete ({count})": "Kustuta jäädavalt ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Kustuta jäädavalt kõik {count} pehmelt kustutatud objekti. Seda ei saa tagasi võtta!", + "Permanently delete already soft-deleted objects": "Kustuta jäädavalt juba pehmelt kustutatud objektid", + "Permanently delete this conversation?": "Kas kustutada see vestlus jäädavalt?", + "Permission Matrix": "Õiguste maatriks", + "Person": "Isik", + "Personal Data": "Isikuandmed", + "Personal Data Export": "Isikuandmete eksport", + "Persons": "Isikud", + "Phone": "Telefon", + "Phone Numbers": "Telefoninumbrid", + "Photos": "Fotod", + "Please create an agent in the": "Palun looge agent", + "Please select a user": "Palun valige kasutaja", + "Please select an agent to continue": "Palun valige jätkamiseks agent", + "Please select an organisation": "Palun valige organisatsioon", + "Please select at least one item to import": "Palun valige importimiseks vähemalt üks üksus", + "Please select registers or schemas first": "Palun valige esmalt registrid või skeemid", + "Please select which register and schema to use for the new object": "Palun valige, millist registrit ja skeemi uue objekti jaoks kasutada", + "Please try again later.": "Palun proovige hiljem uuesti.", + "Please wait while we fetch your agents.": "Palun oodake, kuni laadime teie agendid.", + "Please wait while we fetch your applications.": "Palun oodake, kuni laadime teie rakendused.", + "Please wait while we fetch your configurations.": "Palun oodake, kuni laadime teie konfiguratsioonid.", + "Please wait while we fetch your deleted items.": "Palun oodake, kuni laadime teie kustutatud üksused.", + "Please wait while we fetch your sources.": "Palun oodake, kuni laadime teie allikad.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Palun oodake, kuni kustutame SOLR kogu jäädavalt. See võib võtta mõne hetke.", + "Polls": "Küsitlused", + "Popular Search Terms": "Populaarsed otsingusõnad", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Eelistused salvestatud", + "Prerequisites": "Eeltingimused", + "Press Enter to send, Shift+Enter for new line": "Saatmiseks vajutage Enter, uue rea jaoks Shift+Enter", + "Press enter to send, shift+enter for new line": "saatmiseks vajutage enter, uue rea jaoks shift+enter", + "Prettify": "Vorminda", + "Preview erasure": "Kustutamise eelvaade", + "Preview:": "Eelvaade:", + "Previous": "Eelmine", + "Private": "Privaatne", + "Private Agent (Default)": "Privaatne agent (vaikimisi)", + "Private agents": "Privaatsed agendid", + "Process Name (optional)": "Protsessi nimi (valikuline)", + "Processed {indexed} files, {failed} failed": "Töödeldi {indexed} faili, {failed} ebaõnnestus", + "Processes chunks in batches with simulated parallelism.": "Töötleb tükke partiidena simuleeritud paralleelsusega.", + "Processes file chunks sequentially (safest).": "Töötleb failitükke järjestikku (kõige turvalisem).", + "Processes objects in chunks with simulated parallelism.": "Töötleb objekte tükkidena simuleeritud paralleelsusega.", + "Processes objects sequentially (safest).": "Töötleb objekte järjestikku (kõige turvalisem).", + "Processing": "Töötlemine", + "Processing Limits": "Töötlemise piirangud", + "Processing...": "Töötlemine...", + "Project initialized successfully": "Projekt lähtestati edukalt", + "Project not found. Please initialize first.": "Projekti ei leitud. Palun lähtestage esmalt.", + "Properties": "Atribuudid", + "Property Behaviors": "Atribuudi käitumine", + "Property Configuration:": "Atribuudi konfiguratsioon:", + "Property Filters": "Atribuudi filtrid", + "Property Title": "Atribuudi pealkiri", + "Property Type": "Atribuudi tüüp", + "Property can be improved": "Atribuuti saab parandada", + "Property could not be added": "Atribuuti ei saanud lisada", + "Property could not be updated": "Atribuuti ei saanud uuendada", + "Property name of inversed relation": "Pöördseose atribuudi nimi", + "Property successfully added": "Atribuut lisati edukalt", + "Property successfully updated": "Atribuut uuendati edukalt", + "Protected": "Kaitstud", + "Provider": "Teenusepakkuja", + "Provider is required for testing": "Testimiseks on teenusepakkuja kohustuslik", + "Public": "Avalik", + "Public View": "Avalik vaade", + "Public views can be accessed by anyone in the system": "Avalikele vaadetele pääseb ligi igaüks süsteemis", + "Publish": "Avalda", + "Publish Configuration to GitHub": "Avalda konfiguratsioon GitHubis", + "Publish OAS": "Avalda OAS", + "Publish Objects": "Avalda objektid", + "Publish Register OAS to GitHub": "Avalda registri OAS GitHubis", + "Publish all objects in this schema": "Avalda kõik selle skeemi objektid", + "Publish objects to make them publicly available": "Avalda objektid, et muuta need avalikult kättesaadavaks", + "Published": "Avaldatud", + "Published:": "Avaldatud:", + "Publishing...": "Avaldamine...", + "Purge": "Puhasta", + "Purge Date": "Puhastamise kuupäev", + "Purge {count} object from database": "Puhasta {count} objekt andmebaasist", + "Purge {count} objects from database": "Puhasta {count} objekti andmebaasist", + "Purpose limitation *": "Eesmärgi piirang *", + "Push Notifications": "Tõuketeavitused", + "Query Complexity Distribution": "Päringu keerukuse jaotus", + "Query Help": "Päringu abi", + "Query parameter is required": "Päringuparameeter on kohustuslik", + "Quota": "Kvoot", + "RAG": "RAG", + "RAG Configuration": "RAG konfiguratsioon", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC seaded uuendati edukalt", + "Rate limits cleared successfully": "Päringupiirangud tühjendati edukalt", + "Raw Changes Data": "Toormuudatuste andmed", + "Re-vectorize on object update": "Vektoriseeri uuesti objekti uuendamisel", + "Read": "Loe", + "Real-time push notification status via notify_push": "Reaalajas tõuketeavituste olek notify_push kaudu", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Reaalajas tõuketeavitused on aktiivsed. Ühendatud kliendid saavad koheseid värskendusi, kui objekte luuakse, uuendatakse või kustutatakse.", + "Realtime push active": "Reaalajas tõuge aktiivne", + "Realtime push not available — the notify_push app is not installed": "Reaalajas tõuge pole saadaval – notify_push rakendus pole paigaldatud", + "Reason": "Põhjus", + "Reason (optional)": "Põhjus (valikuline)", + "Rebase operation completed successfully": "Rebase-toiming lõpetati edukalt", + "Rebase operation failed: {error}": "Rebase-toiming ebaõnnestus: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Soovitused:", + "Recommended Type:": "Soovitatav tüüp:", + "Recommended: 5 sources": "Soovitatav: 5 allikat", + "Refresh": "Värskenda", + "Refresh Data": "Värskenda andmeid", + "Refresh Stats": "Värskenda statistikat", + "Refresh Workflows": "Värskenda töövooge", + "Refresh dashboard": "Värskenda töölauda", + "Refresh database info": "Värskenda andmebaasi teavet", + "Register": "Register", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Skeem", + "Register ID": "Registri ID", + "Register Objects": "Registriobjektid", + "Register Overview": "Registri ülevaade", + "Register Statistics": "Registri statistika", + "Register Totals": "Registri kogusummad", + "Register and schema are required": "Register ja skeem on kohustuslikud", + "Register annotation": "Registri annotatsioon", + "Register depublished successfully": "Registri avaldamine tühistati edukalt", + "Register not found": "Registrit ei leitud", + "Register objects": "Registri objektid", + "Register objects deletion completed successfully": "Registri objektide kustutamine viidi edukalt lõpule", + "Register published successfully": "Register avaldati edukalt", + "Register/Schema Usage": "Registri/skeemi kasutus", + "Register:": "Register:", + "Registers": "Registrid", + "Reindex": "Indekseeri uuesti", + "Reindex completed": "Uuesti indekseerimine lõpetatud", + "Reindex completed successfully": "Uuesti indekseerimine viidi edukalt lõpule", + "Reindex completed! Processed {count} objects in {duration}s": "Uuesti indekseerimine lõpetatud! Töödeldi {count} objekti {duration} sekundiga", + "Reindex failed": "Uuesti indekseerimine ebaõnnestus", + "Reindex failed: %s": "Uuesti indekseerimine ebaõnnestus: %s", + "Reject": "Lükka tagasi", + "Rejected": "Tagasi lükatud", + "Related Cases": "Seotud juhtumid", + "Relations": "Seosed", + "Remote Version": "Kaugversioon", + "Remove": "Eemalda", + "Remove Deck card": "Eemalda Deck kaart", + "Remove avatar": "Eemalda avatar", + "Remove contact": "Eemalda kontakt", + "Remove filter": "Eemalda filter", + "Remove from favorites": "Eemalda lemmikutest", + "Remove from list": "Eemalda loendist", + "Remove group": "Eemalda grupp", + "Remove link": "Eemalda link", + "Remove link to {name}": "Eemalda link objektile {name}", + "Remove link to {name}?": "Kas eemaldada link objektile {name}?", + "Remove link to {title}": "Eemalda link objektile {title}", + "Remove oneOf entry": "Eemalda oneOf kirje", + "Remove property after write-back": "Eemalda atribuut pärast tagasikirjutamist", + "Remove {title}": "Eemalda {title}", + "Remove {userId} from this organisation?": "Kas eemaldada {userId} sellest organisatsioonist?", + "Removed": "Eemaldatud", + "Removed from favorites": "Eemaldatud lemmikutest", + "Removing schema {schema}...": "Skeemi {schema} eemaldamine...", + "Rename Conversation": "Nimeta vestlus ümber", + "Rename conversation": "Nimeta vestlus ümber", + "Replicas": "Koopiad", + "Reports": "Aruanded", + "Repository": "Hoidla", + "Request Body": "Päringu sisu", + "Request Data": "Päringu andmed", + "Request Details": "Päringu üksikasjad", + "Request Quota (per day)": "Päringukvoot (päevas)", + "Request account deactivation": "Taotle konto deaktiveerimist", + "Request account deactivation. This will notify administrators for review.": "Taotle konto deaktiveerimist. See teavitab administraatoreid ülevaatamiseks.", + "Request body must be JSON with a \"query\" field": "Päringu sisu peab olema JSON väljaga \"query\"", + "Request timeout in seconds": "Päringu ajalõpp sekundites", + "Requested at": "Taotletud", + "Required": "Nõutav", + "Required Token Scopes": "Nõutavad loa skoobid", + "Required field": "Kohustuslik väli", + "Required status is inconsistent": "Nõutav olek on vastuoluline", + "Requires Dolphin AI for OCR text extraction": "Vajab OCR-i tekstituvastuseks Dolphin AI-d", + "Rerun Search": "Käivita otsing uuesti", + "Rerunning search: {searchTerm}": "Otsingu uuesti käivitamine: {searchTerm}", + "Reset Changes": "Lähtesta muudatused", + "Reset Filters": "Lähtesta filtrid", + "Resource Quotas": "Ressursikvoodid", + "Response Body": "Vastuse sisu", + "Response Details": "Vastuse üksikasjad", + "Response Time:": "Vastamisaeg:", + "Restore": "Taasta", + "Restore conversation": "Taasta vestlus", + "Restore or permanently delete items": "Taasta või kustuta üksused jäädavalt", + "Restore {count} object": "Taasta {count} objekt", + "Restore {count} objects": "Taasta {count} objekti", + "Result Count Range": "Tulemuste arvu vahemik", + "Results": "Tulemused", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Säilitusperiood (ISO-8601 kestus, nt P10Y, P30D)", + "Retention settings updated successfully": "Säilitamise sätted uuendati edukalt", + "Retrieve all objects for this schema": "Hangi kõik selle skeemi objektid", + "Retry": "Proovi uuesti", + "Retry Failed Extractions": "Proovi ebaõnnestunud eraldamisi uuesti", + "Retry Policy": "Uuesti proovimise poliitika", + "Retry completed": "Uuesti proovimine lõpetatud", + "Retry upload": "Proovi üleslaadimist uuesti", + "Retrying...": "Uuesti proovimine...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Vaadake allpool üle valitud objektid. Saate eemaldada objektid, mida te ei soovi kustutada, klõpsates eemaldamise nuppu.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Vaadake allpool üle valitud objektid. Saate eemaldada objektid, mida te ei soovi kaasata.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Vaadake allpool üle valitud objektid. Saate eemaldada objektid, mida te ei soovi jäädavalt kustutada, klõpsates eemaldamise nuppu. Seda toimingut ei saa tagasi võtta.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Vaadake allpool üle valitud objektid. Saate eemaldada objektid, mida te ei soovi taastada, klõpsates eemaldamise nuppu. Objektid taastatakse nende algsesse asukohta.", + "Revoke": "Tühista", + "Right": "Parem", + "Risk Level": "Riskitase", + "Risk level": "Riskitase", + "Roles and Permissions": "Rollid ja õigused", + "Roles updated successfully": "Rollid uuendati edukalt", + "Run compliance scan": "Käivita vastavuse skaneerimine", + "SOLR Connection Settings": "SOLR ühenduse sätted", + "SOLR Index Warmup": "SOLR indeksi soojendus", + "SOLR Version:": "SOLR versioon:", + "SOLR actions menu": "SOLR toimingute menüü", + "SOLR base path (usually /solr)": "SOLR baastee (tavaliselt /solr)", + "SOLR collection deleted successfully": "SOLR kollektsioon kustutati edukalt", + "SOLR connection test failed: {error}": "SOLR ühenduse test ebaõnnestus: {error}", + "SOLR connection test successful!": "SOLR ühenduse test õnnestus!", + "SOLR index warmup completed successfully": "SOLR indeksi soojendus viidi edukalt lõpule", + "SOLR is disabled": "SOLR on keelatud", + "SOLR is not available or not configured": "SOLR ei ole saadaval ega ole seadistatud", + "SOLR logging disabled": "SOLR logimine keelatud", + "SOLR logging enabled": "SOLR logimine lubatud", + "SOLR search disabled": "SOLR otsing keelatud", + "SOLR search enabled": "SOLR otsing lubatud", + "SOLR server hostname or IP address": "SOLR serveri hostinimi või IP-aadress", + "SOLR server port number (optional, defaults to 8983)": "SOLR serveri pordinumber (valikuline, vaikimisi 8983)", + "SOLR settings updated successfully": "SOLR sätted uuendati edukalt", + "SOLR setup completed successfully": "SOLR seadistamine viidi edukalt lõpule", + "SOLR setup completed successfully!": "SOLR seadistamine viidi edukalt lõpule!", + "SOLR setup error: %s": "SOLR seadistamise viga: %s", + "SOLR setup failed": "SOLR seadistamine ebaõnnestus", + "SOLR setup failed - check logs": "SOLR seadistamine ebaõnnestus - kontrollige logisid", + "SOLR setup failed. Please check the configuration and try again.": "SOLR seadistamine ebaõnnestus. Palun kontrollige konfiguratsiooni ja proovige uuesti.", + "SOLR setup failed: {error}": "SOLR seadistamine ebaõnnestus: {error}", + "SOLR warmup failed: {error}": "SOLR soojendus ebaõnnestus: {error}", + "Sample Values:": "Näidisväärtused:", + "Save": "Salvesta", + "Save Configuration": "Salvesta konfiguratsioon", + "Save Connection Settings": "Salvesta ühenduse sätted", + "Save Roles": "Salvesta rollid", + "Save Settings": "Salvesta sätted", + "Save changes": "Salvesta muudatused", + "Save current search as view": "Salvesta praegune otsing vaatena", + "Save labels": "Salvesta sildid", + "Save tags for {name}": "Salvesta sildid objektile {name}", + "Save the organisation first to manage users.": "Kasutajate haldamiseks salvestage esmalt organisatsioon.", + "Saved Views": "Salvestatud vaated", + "Saving...": "Salvestamine...", + "Scheduled": "Ajastatud", + "Schema": "Skeem", + "Schema ID": "Skeemi ID", + "Schema Name": "Skeemi nimi", + "Schema Statistics": "Skeemi statistika", + "Schema annotation": "Skeemi annotatsioon", + "Schema configuration updated successfully for {schema}": "Skeemi {schema} konfiguratsioon uuendati edukalt", + "Schema depublished successfully": "Skeemi avaldamine tühistati edukalt", + "Schema objects deletion completed successfully": "Skeemi objektide kustutamine viidi edukalt lõpule", + "Schema published successfully": "Skeem avaldati edukalt", + "Schema reference ($ref)": "Skeemi viide ($ref)", + "Schema successfully created": "Skeem loodi edukalt", + "Schema successfully updated": "Skeem uuendati edukalt", + "Schema {schema} removed successfully": "Skeem {schema} eemaldati edukalt", + "Schema:": "Skeem:", + "Schemas": "Skeemid", + "Scheme": "Skeem", + "Search": "Otsi", + "Search / Views": "Otsing / vaated", + "Search / views": "Otsing / vaated", + "Search Activity": "Otsingu tegevus", + "Search Analytics": "Otsingu analüütika", + "Search Configuration": "Otsingu konfiguratsioon", + "Search Entities": "Otsi olemeid", + "Search Files": "Otsi faile", + "Search GitHub": "Otsi GitHubist", + "Search GitLab": "Otsi GitLabist", + "Search Method": "Otsingumeetod", + "Search Mode": "Otsingurežiim", + "Search Query": "Otsingupäring", + "Search Results": "Otsingutulemused", + "Search Term": "Otsingusõna", + "Search Trail Details": "Otsingujälje üksikasjad", + "Search Trail Management": "Otsingujälje haldus", + "Search Trail Statistics": "Otsingujälje statistika", + "Search Trails": "Otsingujäljed", + "Search Views": "Otsi vaateid", + "Search Webhooks": "Otsi veebihooke", + "Search agents...": "Otsi agente...", + "Search applications...": "Otsi rakendusi...", + "Search by file name or path": "Otsi faili nime või tee järgi", + "Search by name or URL": "Otsi nime või URL-i järgi", + "Search by title or UUID...": "Otsi pealkirja või UUID järgi...", + "Search by value": "Otsi väärtuse järgi", + "Search configurations": "Otsi konfiguratsioone", + "Search data sources...": "Otsi andmeallikaid...", + "Search groups...": "Otsi gruppe...", + "Search in Database Objects": "Otsi andmebaasi objektidest", + "Search in Files": "Otsi failidest", + "Search in Objects": "Otsi objektidest", + "Search objects": "Otsi objekte", + "Search objects...": "Otsi objekte...", + "Search property names...": "Otsi atribuutide nimesid...", + "Search registers...": "Otsi registreid...", + "Search schemas...": "Otsi skeeme...", + "Search trail deletion not implemented yet": "Otsingujälje kustutamine ei ole veel rakendatud", + "Search views...": "Otsi vaateid...", + "Search {name}...": "Otsi {name}...", + "Searching...": "Otsimine...", + "Secret": "Saladus", + "Secret key for HMAC signature generation (optional)": "Salavõti HMAC allkirja genereerimiseks (valikuline)", + "Security": "Turvalisus", + "See organisations as a table": "Vaata organisatsioone tabelina", + "See organisations as cards": "Vaata organisatsioone kaartidena", + "Select AI Agent": "Vali AI agent", + "Select Active Organisation": "Vali aktiivne organisatsioon", + "Select All": "Vali kõik", + "Select ConfigSet": "Vali ConfigSet", + "Select File Types to Vectorize:": "Vali vektoriseeritavad failitüübid:", + "Select HTTP method": "Vali HTTP meetod", + "Select Register and Schema": "Vali register ja skeem", + "Select Views to Vectorize:": "Vali vektoriseeritavad vaated:", + "Select a Nextcloud group to add": "Vali lisatav Nextcloudi grupp", + "Select a branch": "Vali haru", + "Select a date property": "Vali kuupäeva atribuut", + "Select a model or type a custom model name": "Vali mudel või sisesta kohandatud mudeli nimi", + "Select a register": "Vali register", + "Select a register...": "Vali register...", + "Select a repository": "Vali hoidla", + "Select a repository you have write access to": "Vali hoidla, millele teil on kirjutusõigus", + "Select a schema": "Vali skeem", + "Select a schema...": "Vali skeem...", + "Select a user": "Vali kasutaja", + "Select agent type": "Vali agendi tüüp", + "Select all entities": "Vali kõik olemid", + "Select an AI Agent": "Vali AI agent", + "Select an AI agent to begin chatting with your data.": "Oma andmetega vestlema hakkamiseks valige AI agent.", + "Select an email to see linked objects": "Seotud objektide vaatamiseks valige e-kiri", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Liikme lisamiseks valige organisatsioon ja kasutaja. Otsige organisatsioone nime järgi.", + "Select backend": "Vali taustsüsteem", + "Select chat model": "Vali vestlusmudel", + "Select collection for files": "Vali kollektsioon failidele", + "Select collection for objects": "Vali kollektsioon objektidele", + "Select configuration type...": "Vali konfiguratsiooni tüüp...", + "Select default organisation": "Vali vaikeorganisatsioon", + "Select entity {value}": "Vali olem {value}", + "Select event to listen to...": "Vali kuulatav sündmus...", + "Select fields to display": "Vali kuvatavad väljad", + "Select files with the correct extension": "Vali õige laiendiga failid", + "Select group": "Vali grupp", + "Select groups (optional)": "Vali grupid (valikuline)", + "Select groups to notify...": "Vali teavitatavad grupid...", + "Select groups with access to this agent": "Vali grupid, kellel on juurdepääs sellele agendile", + "Select groups...": "Vali grupid...", + "Select model": "Vali mudel", + "Select one or more registers": "Vali üks või mitu registrit", + "Select one or more schemas": "Vali üks või mitu skeemi", + "Select options...": "Vali valikud...", + "Select or create labels, or select \"No label\" to add files": "Valige või looge sildid või valige failide lisamiseks \"Silt puudub\"", + "Select owner application (optional)...": "Vali omanikrakendus (valikuline)...", + "Select period": "Vali periood", + "Select property to send as payload": "Vali atribuut, mis saadetakse kasuliku koormana", + "Select provider": "Vali teenusepakkuja", + "Select registers and schemas to save a view": "Vaate salvestamiseks valige registrid ja skeemid", + "Select retry policy": "Vali uuesti proovimise poliitika", + "Select role": "Vali roll", + "Select schemas (empty = all schemas)": "Vali skeemid (tühi = kõik skeemid)", + "Select schemas to warm up": "Vali soojendatavad skeemid", + "Select search mode": "Vali otsingurežiim", + "Select source type...": "Vali allika tüüp...", + "Select target type": "Vali siht tüüp", + "Select the branch to publish to": "Vali haru, kuhu avaldada", + "Select the event this webhook should listen to": "Vali sündmus, mida see veebihook peaks kuulama", + "Select views to filter data (optional)": "Vali vaated andmete filtreerimiseks (valikuline)", + "Select views to limit which data the agent can access": "Vali vaated, et piirata, milliseid andmeid agent saab kasutada", + "Select views to vectorize:": "Vali vektoriseeritavad vaated:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Valige, millised Nextcloudi grupid on sellele organisatsioonile saadaval. Nendesse gruppidesse kuuluvatel kasutajatel on juurdepääs organisatsiooni ressurssidele.", + "Select which data views the AI can search": "Vali, milliseid andmevaateid AI saab otsida", + "Select which property from the event should be used as the webhook payload data": "Vali, millist sündmuse atribuuti tuleks kasutada veebihooki kasuliku koorma andmetena", + "Select which tools the AI can use to perform actions": "Vali, milliseid tööriistu AI saab toimingute tegemiseks kasutada", + "Select {fieldName} values": "Vali {fieldName} väärtused", + "Selected": "Valitud", + "Selected Groups": "Valitud grupid", + "Selected Objects ({count})": "Valitud objektid ({count})", + "Selected audit trails deleted successfully": "Valitud auditijäljed kustutati edukalt", + "Selected search trails deleted successfully": "Valitud otsingujäljed kustutati edukalt", + "Selected users can access this view": "Valitud kasutajatel on juurdepääs sellele vaatele", + "Send additional feedback": "Saada täiendavat tagasisidet", + "Send as CloudEvent": "Saada CloudEventina", + "Send as cloudevent": "Saada cloudeventina", + "Sensitive PII": "Tundlikud isikuandmed (PII)", + "Serial Mode (Safer, slower)": "Järjestikune režiim (turvalisem, aeglasem)", + "Serial:": "Järjekorranumber:", + "Server Configuration": "Serveri konfiguratsioon", + "Server Information": "Serveri teave", + "Set as Active": "Määra aktiivseks", + "Set as active organisation": "Määra aktiivseks organisatsiooniks", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Määrake API kasutuse ja loendite tarbimise piirangud. Piiramatute ressursside jaoks kasutage 0.", + "Settings": "Sätted", + "Settings updated successfully": "Sätted uuendati edukalt", + "Shards": "Killud", + "Share": "Jaga", + "Share with Groups": "Jaga gruppidega", + "Share with Users": "Jaga kasutajatega", + "Show": "Näita", + "Show Filters": "Näita filtreid", + "Show less": "Näita vähem", + "Show only entries with changes": "Näita ainult muudatustega kirjeid", + "Showing {count} dashboard(s)": "Kuvatakse {count} töölauda", + "Showing {count} processing activities": "Kuvatakse {count} töötlemistoimingut", + "Showing {showing} of {total} agents": "Kuvatakse {showing} / {total} agendist", + "Showing {showing} of {total} applications": "Kuvatakse {showing} / {total} rakendusest", + "Showing {showing} of {total} audit trail entries": "Kuvatakse {showing} / {total} auditijälje kirjest", + "Showing {showing} of {total} configurations": "Kuvatakse {showing} / {total} konfiguratsioonist", + "Showing {showing} of {total} deleted items": "Kuvatakse {showing} / {total} kustutatud üksusest", + "Showing {showing} of {total} entities": "Kuvatakse {showing} / {total} olemist", + "Showing {showing} of {total} files": "Kuvatakse {showing} / {total} failist", + "Showing {showing} of {total} log entries": "Kuvatakse {showing} / {total} logikirjest", + "Showing {showing} of {total} search trail entries": "Kuvatakse {showing} / {total} otsingujälje kirjest", + "Showing {showing} of {total} sources": "Kuvatakse {showing} / {total} allikast", + "Showing {showing} of {total} templates": "Kuvatakse {showing} / {total} mallist", + "Showing {showing} of {total} webhooks": "Kuvatakse {showing} / {total} veebihookist", + "Showing {shown} of {total} organisations": "Kuvatakse {shown} / {total} organisatsioonist", + "Simple": "Lihtne", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Lihtsad päringud: põhilised tekstiotsingud minimaalsete parameetritega (nt üks otsingusõna, täpsemad filtrid puuduvad)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Lihtsad päringud: põhilised tekstiotsingud minimaalsete parameetritega (nt üks otsingusõna, täpsemad filtrid puuduvad)", + "Size": "Suurus", + "Slug": "Identifikaator", + "Soft Delete Mode": "Pehme kustutamise režiim", + "Soft Deleted Items": "Pehmelt kustutatud üksused", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Kustuta pehmelt kõik selle skeemi objektid ({active} aktiivset, {deleted} juba kustutatud)", + "SolrCloud mode enabled": "SolrCloud režiim lubatud", + "Something went wrong while adding files": "Failide lisamisel läks midagi valesti", + "Source": "Allikas", + "Source successfully updated": "Allikas uuendati edukalt", + "Sources": "Allikad", + "Sources:": "Allikad:", + "Special Rights": "Eriõigused", + "Standalone SOLR mode": "Eraldiseisev SOLR režiim", + "Start Conversation": "Alusta vestlust", + "Start Date Field": "Alguskuupäeva väli", + "Start Vectorization": "Alusta vektoriseerimist", + "Start a conversation": "Alusta vestlust", + "Starting names cache warmup...": "Nimede vahemälu soojenduse alustamine...", + "Starting validation for {schema}...": "Skeemi {schema} valideerimise alustamine...", + "Starting {action} for {schema}...": "Toimingu {action} alustamine skeemi {schema} jaoks...", + "Starting...": "Alustamine...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika ja mõõdikud", + "Statistics refreshed": "Statistika värskendatud", + "Stats calculation not yet implemented": "Statistika arvutamine ei ole veel rakendatud", + "Status": "Olek", + "Status Code": "Olekukood", + "Step": "Samm", + "Storage": "Salvestusruum", + "Storage Quota (MB)": "Salvestuskvoot (MB)", + "String Constraints": "Stringi piirangud", + "Subject identifier (email, BSN, name, etc.)": "Subjekti identifikaator (e-post, BSN, nimi jne)", + "Subscribe": "Telli", + "Subscribed": "Tellitud", + "Success": "Õnnestus", + "Success Rate": "Õnnestumismäär", + "Success Status": "Õnnestumise olek", + "Successful": "Õnnestunud", + "Successfully added user to organisation: {name}": "Kasutaja lisati edukalt organisatsiooni: {name}", + "Successfully cleared {count} audit trails": "Edukalt tühjendati {count} auditijälge", + "Successfully cleared {count} blob storage objects": "Edukalt tühjendati {count} blob-salvestuse objekti", + "Successfully cleared {count} search trails": "Edukalt tühjendati {count} otsingujälge", + "Successfully created {count} SOLR fields": "Edukalt loodi {count} SOLR välja", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Edukalt kustutati {count} manustust. Palun vektoriseerige oma andmed uuesti.", + "Successfully deleted {count} objects for {schema}": "Edukalt kustutati {count} objekti skeemi {schema} jaoks", + "Successfully fixed {count} SOLR field configurations": "Edukalt parandati {count} SOLR välja konfiguratsiooni", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Edukalt imporditi: {registers} registrit, {schemas} skeemi, {objects} objekti", + "Successfully joined organisation": "Organisatsiooniga liituti edukalt", + "Successfully permanently deleted {count} objects": "Edukalt kustutati jäädavalt {count} objekti", + "Successfully processed {count} files!": "Edukalt töödeldi {count} faili!", + "Successfully restored {count} object": "Edukalt taastati {count} objekt", + "Successfully restored {count} objects": "Edukalt taastati {count} objekti", + "Successfully saved configuration for {count} facets!": "Edukalt salvestati konfiguratsioon {count} tahu jaoks!", + "Supported File Types": "Toetatud failitüübid", + "Switch Active Organisation": "Vaheta aktiivset organisatsiooni", + "Switch Organisation": "Vaheta organisatsiooni", + "Sync Table": "Sünkrooni tabel", + "Synchronization Settings": "Sünkroonimise sätted", + "Syncing magic table for {schema}...": "Skeemi {schema} maagilise tabeli sünkroonimine...", + "Syncs": "Sünkroonimised", + "System Default": "Süsteemi vaikeväärtus", + "System Prompt": "Süsteemi viip", + "System announcements": "Süsteemi teadaanded", + "Table": "Tabel", + "Target ID": "Sihtmärgi ID", + "Target Type*": "Sihtmärgi tüüp*", + "Technical Description": "Tehniline kirjeldus", + "Technical Details": "Tehnilised üksikasjad", + "Technical description for developers and administrators": "Tehniline kirjeldus arendajatele ja administraatoritele", + "Technical measures": "Tehnilised meetmed", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatuur", + "Templates": "Mallid", + "Test": "Testi", + "Test Chat": "Testi vestlust", + "Test Connection": "Testi ühendust", + "Test Embedding": "Testi manustust", + "Test endpoint executed successfully": "Testi lõpp-punkt täideti edukalt", + "Test webhook delivered successfully": "Testveebihook edastati edukalt", + "Test webhook delivery failed": "Testveebihooki edastamine ebaõnnestus", + "Test webhook sent successfully": "Testveebihook saadeti edukalt", + "Testing...": "Testimine...", + "Text Chunk #{id}": "Tekstilõik nr {id}", + "Text Extraction": "Teksti eraldamine", + "Text Extraction Engine": "Teksti eraldamise mootor", + "Text chunks": "Tekstilõigud", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Tekstilõigud genereeritakse faili eraldamise käigus ja salvestatakse andmebaasi. Vektoriseerimine loeb need eelnevalt lõikudeks jagatud failid ja teisendab need manustusteks.", + "Text extracted successfully": "Tekst eraldati edukalt", + "Text extraction disabled": "Teksti eraldamine keelatud", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloudi rakendus Calendar ei ole selles serveris paigaldatud ega lubatud.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloudi rakendus Contacts ei ole selles serveris paigaldatud ega lubatud.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloudi rakendus Deck ei ole selles serveris paigaldatud ega lubatud.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloudi rakendus Mail ei ole selles serveris paigaldatud ega lubatud.", + "The URL where webhook events will be sent": "URL, kuhu veebihaagi sündmused saadetakse", + "The requested conversation does not exist": "Soovitud vestlust ei ole olemas", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Soovitud töölauda ei õnnestunud laadida. Kontrollige URL-i või valige loendist mõni teine töölaud.", + "The saved view and all its search configuration": "Salvestatud vaade ja kogu selle otsingu seadistus", + "There are no audit trail entries matching your current filters.": "Teie praegustele filtritele vastavaid auditi jälje kirjeid ei ole.", + "There are no deleted items matching your current filters.": "Teie praegustele filtritele vastavaid kustutatud üksusi ei ole.", + "There are no search trail entries matching your current filters.": "Teie praegustele filtritele vastavaid otsingujälje kirjeid ei ole.", + "There are no webhook log entries matching your filters.": "Teie filtritele vastavaid veebihaagi logikirjeid ei ole.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Neid säilitatakse vastavalt nende skeemis seadistatud säilitusperioodile ning need kustutatakse automaatselt jäädavalt, kui säilitusperiood lõppeb. Säilitusperiood on seadistatav iga skeemi kohta ja selle leiate skeemi seadetest.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Neid säilitatakse vastavalt nende skeemis seadistatud säilitusperioodile ning seejärel eemaldatakse jäädavalt.", + "This action cannot be undone.": "Seda toimingut ei saa tagasi võtta.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Seda toimingut ei saa tagasi võtta. Veenduge, et ükski kogu ei kasuta seda ConfigSet'i.", + "This action cannot be undone. Make sure no collections are using this configset.": "Seda toimingut ei saa tagasi võtta. Veenduge, et ükski kogu ei kasuta seda configset'i.", + "This action will submit a deactivation request to your administrators.": "See toiming saadab teie administraatoritele deaktiveerimistaotluse.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Sellel agendil ei ole seadistatavaid vaateid ega tööriistu, kuid allpool saate siiski kohandada RAG-i seadeid.", + "This analysis may take some time": "See analüüs võib võtta veidi aega", + "This audit trail entry does not contain any change information.": "See auditi jälje kirje ei sisalda muudatuste teavet.", + "This configuration has no data defined.": "Sellele seadistusele ei ole andmeid määratud.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "See lõpp-punkt on aegunud. Kasutage selle asemel tükipõhiseid lõpp-punkte.", + "This entity has no relations to objects or files": "Sellel üksusel ei ole seoseid objektide ega failidega", + "This is a test webhook from OpenRegister": "See on testveebihaak OpenRegisterist", + "This is not valid JSON": "See ei ole kehtiv JSON", + "This key already exists on this schema": "See võti on selles skeemis juba olemas", + "This schema must use Magic Table configuration to sync": "See skeem peab sünkroonimiseks kasutama Magic Table seadistust", + "This schema must use magic table configuration to sync": "See skeem peab sünkroonimiseks kasutama maagilise tabeli seadistust", + "This source has no associated registers.": "Selle allikaga ei ole seotud ühtegi registrit.", + "This token will only be shown once. Copy it now.": "Seda lubavõtit kuvatakse ainult üks kord. Kopeerige see kohe.", + "This will copy the _default ConfigSet with the new name": "See kopeerib _default ConfigSet'i uue nimega", + "This will copy the _default configset with the new name": "See kopeerib _default configset'i uue nimega", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "See kustutab jäädavalt KÕIK manustused (vektorid) andmebaasist. Teil tuleb kõik objektid ja failid uuesti vektoriseerida. Seda toimingut ei saa tagasi võtta.\n\nKas olete kindel, et soovite jätkata?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "See kustutab jäädavalt KÕIK manustused (vektorid) andmebaasist. Teil tuleb kõik objektid ja failid uuesti vektoriseerida. Seda toimingut ei saa tagasi võtta.\\n\\nKas olete kindel, et soovite jätkata?", + "This will permanently delete:": "See kustutab jäädavalt:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "See eemaldab jäädavalt {count} juba pehmelt kustutatud objekti andmebaasist. Seda toimingut ei saa tagasi võtta.", + "Timeout (seconds)": "Ajalõpp (sekundites)", + "Timestamp": "Ajatempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Nõuanne: lubage ainult need vaated, mis vajavad semantilist otsingut, et minimeerida manustamise kulusid. Lihtsad otsingutabelid vajavad vektoriseerimist harva.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Nõuanne: lubage ainult need vaated, mis vajavad semantilist otsingut, et minimeerida manustamise kulusid. Lihtsad otsingutabelid vajavad vektoriseerimist harva.", + "Title": "Pealkiri", + "Title *": "Pealkiri *", + "Title Template": "Pealkirja mall", + "Title*": "Pealkiri*", + "To (e.g. -1 year or 2025-12-31)": "Kuni (nt -1 year või 2025-12-31)", + "To Date": "Lõppkuupäev", + "To add files larger than or equal to 512MB, go to the": "Suuremate kui või võrdsete 512 MB failide lisamiseks minge", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Tüki suuruse või strateegia kohandamiseks minge jaotisse File Configuration → Processing Limits.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Tüki suuruse või strateegia kohandamiseks minge faili seadistusse → töötlemise piirangud.", + "To change the embedding provider or model, go to LLM Configuration.": "Manustamise teenusepakkuja või mudeli muutmiseks minge jaotisse LLM Configuration.", + "To change the embedding provider or model, go to LLM configuration.": "Manustamise teenusepakkuja või mudeli muutmiseks minge LLM-i seadistusse.", + "To date": "Lõppkuupäev", + "Toggle search sidebar": "Lülita otsingu külgriba", + "Token Created": "Lubavõti loodud", + "Token Quota (per request)": "Lubavõtme kvoot (päringu kohta)", + "Token copied to clipboard": "Lubavõti kopeeriti lõikelauale", + "Token name": "Lubavõtme nimi", + "Token revoked": "Lubavõti tühistatud", + "Tools": "Tööriistad", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Tööriistad käivituvad agendi vaikekasutaja õigustega, kui ükski kasutajaseanss ei ole aktiivne (nt cron-tööd). Seadistage vaikekasutaja vahekaardil Seaded.", + "Tools respect the agent's own role and organization boundaries.": "Tööriistad arvestavad agendi enda rolli ja organisatsiooni piiridega.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Tööriistad arvestavad agendi vaadete, õiguste ja organisatsiooni piiridega.", + "Top Deleters": "Enim kustutajaid", + "Total": "Kokku", + "Total Audit Trails": "Auditijälgi kokku", + "Total Chunks Available:": "Saadaolevaid tükke kokku:", + "Total Deleted Items": "Kustutatud üksusi kokku", + "Total Members": "Liikmeid kokku", + "Total Objects": "Objekte kokku", + "Total Objects in Database:": "Objekte andmebaasis kokku:", + "Total Organisations": "Organisatsioone kokku", + "Total Results": "Tulemusi kokku", + "Total Searches": "Otsinguid kokku", + "Total Size": "Suurus kokku", + "Total Storage": "Salvestusruum kokku", + "Total events": "Sündmusi kokku", + "Total:": "Kokku:", + "Totals": "Kokkuvõtted", + "Try searching by UUID or with different keywords": "Proovige otsida UUID järgi või teiste märksõnadega", + "Type": "Tüüp", + "Type \"DELETE COLLECTION\" to confirm:": "Kinnitamiseks tippige \"DELETE COLLECTION\":", + "Type Issue": "Tüübiprobleem", + "Type Variations:": "Tüübivariatsioonid:", + "Type filter (optional, e.g. email)": "Tüübifilter (valikuline, nt email)", + "Type must be \"positive\" or \"negative\"": "Tüüp peab olema \"positive\" või \"negative\"", + "Type to search for groups": "Tippige gruppide otsimiseks", + "Type to search for objects...": "Tippige objektide otsimiseks...", + "Type to search for organisations": "Tippige organisatsioonide otsimiseks", + "Type to search for users": "Tippige kasutajate otsimiseks", + "Type to search groups...": "Tippige gruppide otsimiseks...", + "Type to search users...": "Tippige kasutajate otsimiseks...", + "Type to search...": "Tippige otsimiseks...", + "Type your username to confirm": "Kinnitamiseks tippige oma kasutajanimi", + "URL": "URL", + "URL where Ollama is running": "URL, kus Ollama töötab", + "UUID copied to clipboard": "UUID kopeeriti lõikelauale", + "UUID:": "UUID:", + "Unchanged": "Muutmata", + "Unique Search Terms": "Unikaalsed otsingusõnad", + "Unique Terms": "Unikaalsed sõnad", + "Unknown": "Tundmatu", + "Unknown Object": "Tundmatu objekt", + "Unknown Register": "Tundmatu register", + "Unknown Schema": "Tundmatu skeem", + "Unknown error": "Tundmatu viga", + "Unknown widget type: {type}": "Tundmatu vidina tüüp: {type}", + "Unlink email": "Eemalda e-kirja seos", + "Unlink event": "Eemalda sündmuse seos", + "Unnamed Object": "Nimetu objekt", + "Untitled": "Pealkirjata", + "Untitled View": "Pealkirjata vaade", + "Update": "Uuenda", + "Update Operations": "Uuendamistoimingud", + "Update available: {local} → {remote}": "Saadaval on uuendus: {local} → {remote}", + "Update configuration: ...": "Uuenda seadistust: ...", + "Update register OAS: ...": "Uuenda registri OAS-i: ...", + "Update vectors when object data changes (recommended for accurate search)": "Uuenda vektoreid objekti andmete muutumisel (soovitatav täpse otsingu jaoks)", + "Updated": "Uuendatud", + "Updated At": "Uuendatud", + "Updated:": "Uuendatud:", + "Upload new avatar": "Laadi üles uus avatar", + "Uploading configuration sets...": "Seadistuskomplektide üleslaadimine...", + "Uptime:": "Töötamise aeg:", + "Url": "URL", + "Usage Count": "Kasutuse arv", + "Use AI agents for processing and analysis": "Kasutage töötlemiseks ja analüüsiks tehisintellekti agente", + "Use Large Language Model features": "Kasutage suure keelemudeli funktsioone", + "Use SolrCloud with Zookeeper for distributed search": "Kasutage hajutatud otsingu jaoks SolrCloud'i koos ZooKeeperiga", + "Use SolrCloud with zookeeper for distributed search": "Kasutage hajutatud otsingu jaoks SolrCloud'i koos zookeeperiga", + "Use VNG APIs instead of MongoDB.": "Kasutage MongoDB asemel VNG API-sid.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Kasuta vaikimisi vahemikke (viimased 7/30/90 päeva, viimane aasta, vanemad)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Kasutage sisemise Nextcloudi salvestusruumi asemel välist salvestusruumi (nt MongoDB).", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Kasutage filtreid auditijälje kirjete kitsendamiseks registri, skeemi, toimingu tüübi, kasutaja, kuupäevavahemiku või objekti ID järgi.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Kasutage filtreid kustutatud üksuste kitsendamiseks registri, skeemi, kustutamiskuupäeva või neid kustutanud kasutaja järgi.", + "Use filters to narrow down entities by type or category.": "Kasutage filtreid üksuste kitsendamiseks tüübi või kategooria järgi.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Kasutage filtreid otsingujälje kirjete kitsendamiseks registri, skeemi, edukuse oleku, kasutaja, kuupäevavahemiku, otsingusõnade või jõudlusnäitajate järgi.", + "Used By Collections": "Kasutatud kogude poolt", + "Used by": "Kasutab", + "User": "Kasutaja", + "User Agent Statistics": "Kasutajaagendi statistika", + "User Description": "Kasutaja kirjeldus", + "User Quota": "Kasutaja kvoot", + "User rate limits cleared successfully": "Kasutaja päringusageduse piirangud edukalt lähtestatud", + "User removed successfully": "Kasutaja edukalt eemaldatud", + "User-friendly description shown in forms and help text": "Kasutajasõbralik kirjeldus, mida kuvatakse vormidel ja abitekstis", + "Username": "Kasutajanimi", + "Username for SOLR authentication (optional)": "Kasutajanimi SOLR-i autentimiseks (valikuline)", + "Users": "Kasutajad", + "Uses": "Kasutab", + "Using Pre-Generated Chunks": "Eelnevalt loodud tükkide kasutamine", + "Validate": "Valideeri", + "Validate Objects": "Valideeri objektid", + "Validation completed for {schema}": "Valideerimine lõpetatud skeemi {schema} jaoks", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Valideerimine lõpetatud skeemi {schema} jaoks: {processed} töödeldud, {updated} uuendatud, {failed} ebaõnnestunud", + "Validation completed successfully": "Valideerimine edukalt lõpetatud", + "Validation failed": "Valideerimine ebaõnnestus", + "Value": "Väärtus", + "Value list (split on ,)": "Väärtuste loend (eraldatud komaga ,)", + "Vector Dimensions": "Vektori mõõtmed", + "Vector Search Backend": "Vektorotsingu taustasüsteem", + "Vector field: _embedding_": "Vektori väli: _embedding_", + "Vectorization Triggers": "Vektoriseerimise päästikud", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektoriseerimine lõpetatud: {vectorized} objekti vektoriseeritud, {failed} ebaõnnestunud", + "Vectorization failed: {error}": "Vektoriseerimine ebaõnnestus: {error}", + "Vectorize All Files": "Vektoriseeri kõik failid", + "Vectorize All Objects": "Vektoriseeri kõik objektid", + "Vectorize all file types": "Vektoriseeri kõik failitüübid", + "Vectorize all views": "Vektoriseeri kõik vaated", + "Vectorize on object creation": "Vektoriseeri objekti loomisel", + "Vectors will be stored in your existing object and file collections": "Vektorid salvestatakse teie olemasolevatesse objekti- ja failikogudesse", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Serveriühenduse kontrollimine...", + "Version": "Versioon", + "Version Information": "Versiooniteave", + "Very High": "Väga kõrge", + "Very high": "Väga kõrge", + "View": "Vaade", + "View API Docs": "Vaata API dokumentatsiooni", + "View API Documentation": "Vaata API dokumentatsiooni", + "View APIs": "Vaata API-sid", + "View Changes": "Vaata muudatusi", + "View Details": "Vaata üksikasju", + "View Error": "Vaata viga", + "View File": "Vaata faili", + "View Full Details": "Vaata kõiki üksikasju", + "View Logs": "Vaata logisid", + "View Name": "Vaate nimi", + "View Object": "Vaata objekti", + "View Parameters": "Vaate parameetrid", + "View Selection": "Vaate valik", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Vaadake ja analüüsige otsingujälje logisid täiustatud filtreerimis- ja analüüsivõimalustega", + "View and analyze system audit trails with advanced filtering capabilities": "Vaadake ja analüüsige süsteemi auditijälgi täiustatud filtreerimisvõimalustega", + "View and manage authorization across registers and schemas": "Vaadake ja hallake volitusi registrite ja skeemide üleselt", + "View deleted successfully": "Vaade edukalt kustutatud", + "View entity details and manage relations": "Vaadake üksuse üksikasju ja hallake seoseid", + "View name": "Vaate nimi", + "View name is required": "Vaate nimi on nõutav", + "View object": "Vaata objekti", + "View saved successfully!": "Vaade edukalt salvestatud!", + "View search analytics and manage search logs": "Vaadake otsinguanalüütikat ja hallake otsingulogisid", + "View successfully deleted": "Vaade edukalt kustutatud", + "View successfully updated": "Vaade edukalt uuendatud", + "View updated successfully!": "Vaade edukalt uuendatud!", + "View webhook delivery logs and filter by webhook": "Vaadake veebihaagi kohaletoimetamise logisid ja filtreerige veebihaagi järgi", + "View {count} more": "Vaata veel {count}", + "Views": "Vaated", + "Visible to end users": "Nähtav lõppkasutajatele", + "Visible to users": "Nähtav kasutajatele", + "Wait for Response": "Oota vastust", + "Wait for webhook response before continuing (required for request/response flows)": "Oota enne jätkamist veebihaagi vastust (nõutav päringu/vastuse vooge jaoks)", + "Waiting for cluster synchronization...": "Klastri sünkroonimise ootamine...", + "Warmup Names Cache": "Soojenda nimede vahemälu", + "Warmup failed": "Soojendamine ebaõnnestus", + "Warning": "Hoiatus", + "Warning:": "Hoiatus:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Hoiatus: sellel atribuudil on ebajärjekindlad tüübid: {types}. Kaaluge, kas kõigil objektidel peaks olema sama tüüp.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Me skannime kõik sellesse skeemi kuuluvad objektid, et avastada uusi atribuute ja analüüsida olemasolevaid atribuute võimalike täienduste osas. Protsess hõlmab iga objekti andmestruktuuri uurimist, praeguses skeemis määratlemata atribuutide tuvastamist ning võimaluste leidmist olemasolevate atribuutide määratluste parandamiseks paremate kitsenduste, vormingute ja valideerimisreeglitega.", + "Webhook": "Veebihaak", + "Webhook Log Details": "Veebihaagi logi üksikasjad", + "Webhook Logs": "Veebihaagi logid", + "Webhook created successfully": "Veebihaak edukalt loodud", + "Webhook deleted": "Veebihaak kustutatud", + "Webhook retry delivered successfully": "Veebihaagi kordusproov edukalt kohale toimetatud", + "Webhook retry delivery failed": "Veebihaagi kordusproovi kohaletoimetamine ebaõnnestus", + "Webhook updated": "Veebihaak uuendatud", + "Webhook updated successfully": "Veebihaak edukalt uuendatud", + "Webhooks": "Veebihaagid", + "Weekly": "Iganädalaselt", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kui agent töötab ilma kasutajaseansita (nt ajastatud ülesanded), kasutatakse selle kasutaja konteksti", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kui agent töötab ilma kasutajaseansita (nt ajastatud ülesanded), kasutatakse selle kasutaja konteksti.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Kui see on keelatud, kuvab see tahk ainult selle skeemi väärtusi ning lisab valimisel skeemifiltri.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Kui see on lubatud, uuendab selle objekti salvestamine ka sihtobjekte, et lisada viide tagasi sellele objektile.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Kui see on lubatud, eemaldatakse see atribuut lähteobjektist pärast sihtobjektide uuendamist.", + "Workflow Management": "Töövoo haldus", + "Workflows": "Töövood", + "Wrap webhook payload in CloudEvents format for better interoperability": "Mässi veebihaagi kandevoog parema koostalitlusvõime jaoks CloudEvents-vormingusse", + "Wrap webhook payload in cloudevents format for better interoperability": "Mässi veebihaagi kandevoog parema koostalitlusvõime jaoks cloudevents-vormingusse", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Jah", + "You": "Teie", + "You are not yet a member of any organisations.": "Te ei ole veel ühegi organisatsiooni liige.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Saate luua uusi ConfigSet'e _default malli põhjal või laadida kohandatud komplekte otse oma SOLR-i serverisse üles.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Saate luua uusi configset'e _default malli põhjal või laadida kohandatud komplekte otse oma SOLR-i serverisse üles.", + "You do not have access to this conversation": "Teil ei ole sellele vestlusele juurdepääsu", + "You do not have permission to delete this conversation": "Teil ei ole õigust seda vestlust kustutada", + "You do not have permission to modify this conversation": "Teil ei ole õigust seda vestlust muuta", + "You do not have permission to restore this conversation": "Teil ei ole õigust seda vestlust taastada", + "You do not have permission to view the permission matrix. Admin access is required.": "Teil ei ole õigust õigustemaatriksit vaadata. Nõutav on administraatori juurdepääs.", + "You have selected {count} properties to add to the schema.": "Olete valinud skeemi lisamiseks {count} atribuuti.", + "You must be logged in to favorite views": "Vaadete lemmikuks märkimiseks peate olema sisse logitud", + "You need an AI agent to start a conversation.": "Vestluse alustamiseks vajate tehisintellekti agenti.", + "Your Fireworks AI API key. Get one at": "Teie Fireworks AI API-võti. Hankige see aadressilt", + "Your OpenAI API key. Get one at": "Teie OpenAI API-võti. Hankige see aadressilt", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Teie tagasiside on salvestatud. Soovi korral saate siia lisada täiendavaid üksikasju...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Teie tagasiside on salvestatud. Soovi korral saate siia lisada täiendavaid üksikasju...", + "Zookeeper Hosts": "ZooKeeperi hostid", + "Zookeeper Port": "ZooKeeperi port", + "Zookeeper Settings (SolrCloud)": "ZooKeeperi seaded (SolrCloud)", + "Zookeeper connection string for SolrCloud": "ZooKeeperi ühendusstring SolrCloud'i jaoks", + "Zookeeper port number (optional, defaults to 2181)": "ZooKeeperi pordinumber (valikuline, vaikimisi 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F JÄÄDAVA KUSTUTAMISE HOIATUS \\u26A0\\uFE0F\n\nTe olete kustutamas JÄÄDAVALT KÕIKI skeemi \"{schema}\" objekte:\n\n\\u2022 Aktiivsed objektid: {active}\n\\u2022 Pehmelt kustutatud objektid: {deleted}\n\\u2022 Kokku: {total}\n\nNeed objektid eemaldatakse andmebaasist täielikult ja neid EI SAA taastada.\n\nKas olete täiesti kindel?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F JÄÄDAVA KUSTUTAMISE HOIATUS \\u26A0\\uFE0F\n\nTe olete kustutamas JÄÄDAVALT {count} pehmelt kustutatud objekti skeemi \"{schema}\" jaoks.\n\nNeed objektid eemaldatakse andmebaasist täielikult ja neid EI SAA taastada.\n\nKas olete täiesti kindel?", + "and accessible to all users in selected groups (or all users if no groups selected).": "ning juurdepääsetav kõigile valitud gruppide kasutajatele (või kõigile kasutajatele, kui ühtegi gruppi ei ole valitud).", + "and add the files there.": "ja lisage failid sinna.", + "approved": "heaks kiidetud", + "are only accessible to invited users.": "on juurdepääsetavad ainult kutsutud kasutajatele.", + "by": "tegija", + "chunk": "tükk", + "chunks": "tükid", + "chunks processed": "tükki töödeldud", + "completed": "lõpetatud", + "configuration(s)": "seadistus(ed)", + "conversationId is required": "conversationId on nõutav", + "create": "loo", + "delete": "kustuta", + "deleted objects section": "kustutatud objektide jaotis", + "e.g., lib/Settings/config.json": "nt lib/Settings/config.json", + "e.g., lib/Settings/register.json": "nt lib/Settings/register.json", + "e.g., lib/settings/config.json": "nt lib/settings/config.json", + "e.g., lib/settings/register.json": "nt lib/settings/register.json", + "execute": "käivita", + "failed": "ebaõnnestus", + "file": "fail", + "files": "failid", + "file{plural}": "fail{plural}", + "folder": "kaust", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "registris {register}", + "in_progress": "pooleli", + "inherited": "päritud", + "list": "loend", + "log{plural}": "logi{plural}", + "manage": "halda", + "members": "liikmed", + "menu or contact someone with permission to create agents.": "menüüst või võtke ühendust kellegagi, kellel on õigus agente luua.", + "message content is required": "sõnumi sisu on nõutav", + "messages": "sõnumid", + "missing": "puudub", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL ja API-võti on nõutavad", + "n8n configuration saved successfully": "n8n seadistus edukalt salvestatud", + "n8n connection not configured": "n8n ühendus ei ole seadistatud", + "n8n connection successful": "n8n ühendus õnnestus", + "n8n connection test failed: {message}": "n8n ühenduse test ebaõnnestus: {message}", + "n8n connection test successful": "n8n ühenduse test õnnestus", + "n8n initialization failed: {message}": "n8n lähtestamine ebaõnnestus: {message}", + "n8n integration disabled": "n8n integratsioon keelatud", + "n8n integration enabled": "n8n integratsioon lubatud", + "n8n project initialized successfully": "n8n projekt edukalt lähtestatud", + "n8n settings saved successfully": "n8n seaded edukalt salvestatud", + "notify_push configuration guide": "notify_push seadistusjuhend", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push on paigaldatud, kuid OpenRegister ei ole veel kinnitanud edukat tõuget. Aktiveerimiseks salvestage mõni objekt või kontrollige oma notify_push seadistust.", + "notify_push is installed but not yet active": "notify_push on paigaldatud, kuid ei ole veel aktiivne", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objektid", + "objects processed": "objekti töödeldud", + "object{plural}": "objekt{plural}", + "or pick a range": "või valige vahemik", + "org-...": "org-...", + "pending": "ootel", + "provides a good balance between speed and accuracy.": "pakub head tasakaalu kiiruse ja täpsuse vahel.", + "public": "avalik", + "read": "loe", + "register(s)": "register(id)", + "register(s) selected": "register(it) valitud", + "register{plural}": "register{plural}", + "rejected": "tagasi lükatud", + "required": "nõutav", + "results": "tulemused", + "schema(s)": "skeem(id)", + "schema(s) selected": "skeem(e) valitud", + "schema{plural}": "skeem{plural}", + "selected objects": "valitud objektid", + "set": "määra", + "sk-...": "sk-...", + "this application": "see rakendus", + "to": "kuni", + "type must be \"positive\" or \"negative\"": "tüüp peab olema \"positive\" või \"negative\"", + "update": "uuenda", + "vectorized": "vektoriseeritud", + "view": "vaade", + "views selected": "vaadet valitud", + "waiting": "ootel", + "{active} active, {deleted} deleted": "{active} aktiivset, {deleted} kustutatud", + "{count} audit trails cleared successfully": "{count} auditijälge edukalt kustutatud", + "{count} compliance issue(s) detected": "Tuvastati {count} vastavusprobleem(i)", + "{count} deletions": "{count} kustutamist", + "{count} documents found": "Leiti {count} dokumenti", + "{count} email": "{count} e-kiri", + "{count} emails": "{count} e-kirja", + "{count} entries": "{count} kirjet", + "{count} issues": "{count} probleemi", + "{count} members": "{count} liiget", + "{count} searches": "{count} otsingut", + "{count} selected": "{count} valitud", + "{count} widget(s)": "{count} vidin(at)", + "{matched} object(s) matched. {erased} erased.": "{matched} objekti vastas. {erased} kustutatud.", + "{minutes} minutes ago": "{minutes} minutit tagasi", + "{percentage}% of objects": "{percentage}% objektidest", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} registris {register} / {schema}", + "{type} cache cleared successfully": "{type} vahemälu edukalt tühjendatud", + "ℹ️ Current Configuration": "ℹ️ Praegune seadistus", + "ℹ️ current configuration": "ℹ️ praegune seadistus", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ OHT: Kõik {total} objekti KUSTUTATAKSE andmebaasist JÄÄDAVALT. See toiming on TAASTAMATU ja seda ei saa tagasi võtta!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ JÄÄDAVA KUSTUTAMISE HOIATUS ⚠️\n\nTe olete kustutamas JÄÄDAVALT KÕIKI skeemi \"{schema}\" objekte:\n\n• Aktiivsed objektid: {active}\n• Pehmelt kustutatud objektid: {deleted}\n• Kokku: {total}\n\nNeed objektid eemaldatakse andmebaasist täielikult ja neid EI SAA taastada.\n\nKas olete täiesti kindel?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ JÄÄDAVA KUSTUTAMISE HOIATUS ⚠️\n\nTe olete kustutamas JÄÄDAVALT {count} pehmelt kustutatud objekti skeemi \"{schema}\" jaoks.\n\nNeed objektid eemaldatakse andmebaasist täielikult ja neid EI SAA taastada.\n\nKas olete täiesti kindel?", + "⚡ Batch Processing": "⚡ Pakktöötlus", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR suudab käsitleda miljoneid dokumente alla sekundi pikkuste otsingu vastusaegadega...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Põhjalik kontekst ✓ Väiksem tõenäosus üksikasju kahe silma vahele jätta ✗ Aeglasemad vastused ✗ Võib sisaldada vähem asjakohast teavet", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Kiiremad vastused ✓ Fokuseeritumad vastused ✗ Võib asjakohast teavet kahe silma vahele jätta", + "✨ AI Features": "✨ Tehisintellekti funktsioonid", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Hajutatud režiimis kasutab SOLR klastri koordineerimiseks ZooKeeperit...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Tahupõhine otsing võimaldab kasutajatel süveneda tulemustesse kategooriate kaupa...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Kas teadsite? SOLR toidab otsingut Netflixis, Apple'is ja paljudel teistel suurtel saitidel!", + "💰 View Selection (Cost Optimization)": "💰 Vaate valik (kulude optimeerimine)", + "📄 Object Serialization": "📄 Objekti serialiseerimine", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR pakub rikkalikku analüütikat ja statistikat otsingu jõudluse kohta...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSet'id sisaldavad teie otsinguindeksi skeemi- ja seadistusfaile...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikatsioon tagab, et teie otsinguindeks on saadaval ka sõlmede tõrke korral...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR on võimas ettevõtte otsinguplatvorm, mis põhineb Apache Lucene'il...", + "🔢 Vectorization Settings": "🔢 Vektoriseerimise seaded", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automaatne skaleerimine saab koormuse alusel sõlmi dünaamiliselt lisada või eemaldada...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Turvafunktsioonide hulka kuuluvad autentimine, autoriseerimine ja SSL-krüptimine...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-kiri", + "{count} e-kirja" + ], + "Purge {count} object from database": [ + "Kustuta {count} objekt andmebaasist jäädavalt", + "Kustuta {count} objekti andmebaasist jäädavalt" + ], + "Restore {count} object": [ + "Taasta {count} objekt", + "Taasta {count} objekti" + ], + "Successfully restored {count} object": [ + "{count} objekt edukalt taastatud", + "{count} objekti edukalt taastatud" + ], + "Delete {count} object": [ + "Kustuta {count} objekt", + "Kustuta {count} objekti" + ], + "Object successfully deleted": [ + "Objekt edukalt kustutatud", + "Objektid edukalt kustutatud" + ] + } +} diff --git a/l10n/fi.js b/l10n/fi.js new file mode 100644 index 0000000000..a1aa305731 --- /dev/null +++ b/l10n/fi.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(ei aihetta)", + "(no title)" : "(ei otsikkoa)", + "(unnamed)" : "(nimetön)", + "(untitled card)" : "(nimetön kortti)", + "Activities" : "Toiminnot", + "Add Endpoint" : "Lisää päätepiste", + "Add Source" : "Lisää lähde", + "Add card" : "Lisää kortti", + "Add contact" : "Lisää yhteystieto", + "All clear" : "Kaikki kunnossa", + "Articles" : "Artikkelit", + "Code" : "Koodi", + "Compliance" : "Vaatimustenmukaisuus", + "Contacts" : "Yhteystiedot", + "Create Agent" : "Luo agentti", + "Create Application" : "Luo sovellus", + "Create event" : "Luo tapahtuma", + "Erasure complete" : "Poisto valmis", + "Erasure preview" : "Poiston esikatselu", + "Expand details" : "Laajenna tiedot", + "Save changes" : "Tallenna muutokset", + "📄 Object Serialization" : "📄 Objektin sarjallistaminen", + "🔢 Vectorization Settings" : "🔢 Vektorointiasetukset", + "💰 View Selection (Cost Optimization)" : "💰 Näkymien valinta (kustannusoptimointi)", + "AI Assistant" : "Tekoälyavustaja", + "AI Chat" : "Tekoälykeskustelu", + "API Key" : "API-avain", + "API tokens saved successfully" : "API-tunnisteet tallennettiin onnistuneesti", + "About ConfigSets" : "Tietoja ConfigSet-asetuksista", + "Action" : "Toiminto", + "Action Distribution" : "Toimintojen jakauma", + "Action:" : "Toiminto:", + "Actions" : "Toiminnot", + "Active" : "Aktiivinen", + "Active Collections" : "Aktiiviset kokoelmat", + "Active Filters" : "Aktiiviset suodattimet", + "Active Organisations" : "Aktiiviset organisaatiot", + "Active filters:" : "Aktiiviset suodattimet:", + "Active organisation set successfully" : "Aktiivinen organisaatio asetettiin onnistuneesti", + "Activity Period" : "Toimintajakso", + "Add Groups" : "Lisää ryhmiä", + "Add Schema" : "Lisää skeema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Lisää skeeman otsikoita, kuvauksia ja rekisteritietoja tarjotaksesi rikkaamman kontekstin hakua varten", + "Add to favorites" : "Lisää suosikkeihin", + "Added" : "Lisätty", + "Added to favorites" : "Lisätty suosikkeihin", + "Additional Information" : "Lisätiedot", + "Additional feedback saved. Thank you!" : "Lisäpalaute tallennettu. Kiitos!", + "Admin" : "Ylläpitäjä", + "Advanced" : "Lisäasetukset", + "Advanced Filters" : "Lisäsuodattimet", + "Advanced Options" : "Lisävalinnat", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Lisäsuodattimet eivät ole käytettävissä tietokantalähdettä käytettäessä. Vaihda tilaan Auto tai SOLR-indeksi suodatusvaihtoehtoja varten.", + "After" : "Jälkeen", + "Agent deleted successfully" : "Agentti poistettiin onnistuneesti", + "Agents" : "Agentit", + "All" : "Kaikki", + "All Categories" : "Kaikki luokat", + "All Collections" : "Kaikki kokoelmat", + "All Confidence Levels" : "Kaikki luottamustasot", + "All ConfigSets" : "Kaikki ConfigSet-asetukset", + "All Files" : "Kaikki tiedostot", + "All Levels" : "Kaikki tasot", + "All Types" : "Kaikki tyypit", + "All Webhooks" : "Kaikki webhookit", + "All actions" : "Kaikki toiminnot", + "All audit trails cleared successfully" : "Kaikki valvontaketjut tyhjennettiin onnistuneesti", + "All registers" : "Kaikki rekisterit", + "All schemas" : "Kaikki skeemat", + "All searches" : "Kaikki haut", + "All users" : "Kaikki käyttäjät", + "An error occurred while clearing audit trails" : "Valvontaketjuja tyhjennettäessä tapahtui virhe", + "An error occurred while deleting the audit trail" : "Valvontaketjua poistettaessa tapahtui virhe", + "An error occurred while deleting the view" : "Näkymää poistettaessa tapahtui virhe", + "An error occurred while permanently deleting the objects" : "Objekteja pysyvästi poistettaessa tapahtui virhe", + "Analysis completed:" : "Analyysi valmis:", + "Analysis steps:" : "Analyysin vaiheet:", + "Analytics" : "Analytiikka", + "Analyze Objects" : "Analysoi objektit", + "Analyze Properties" : "Analysoi ominaisuudet", + "Analyze existing properties for improvement opportunities" : "Analysoi olemassa olevat ominaisuudet parannusmahdollisuuksien löytämiseksi", + "Analyzing..." : "Analysoidaan...", + "Any favorites and sharing settings for this view" : "Kaikki tämän näkymän suosikki- ja jakoasetukset", + "Any user" : "Kuka tahansa käyttäjä", + "App store cache invalidated successfully" : "Sovelluskaupan välimuisti mitätöitiin onnistuneesti", + "Application deleted successfully" : "Sovellus poistettiin onnistuneesti", + "Applications" : "Sovellukset", + "Apply Changes" : "Ota muutokset käyttöön", + "Archive" : "Arkistoi", + "Archive conversation" : "Arkistoi keskustelu", + "Archived conversations are hidden from your active list" : "Arkistoidut keskustelut on piilotettu aktiivisesta luettelostasi", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Haluatko varmasti siivota vanhat hakuketjut? Tämä poistaa yli 30 päivää vanhat merkinnät.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Haluatko varmasti poistaa valitut valvontaketjut? Tätä toimintoa ei voi peruuttaa.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Haluatko varmasti poistaa valitut hakuketjut? Tätä toimintoa ei voi peruuttaa.", + "Are you sure you want to delete this ConfigSet?" : "Haluatko varmasti poistaa tämän ConfigSet-asetuksen?", + "Are you sure you want to permanently delete" : "Haluatko varmasti poistaa pysyvästi", + "Ask a question..." : "Esitä kysymys...", + "Ask questions about your data using natural language" : "Esitä kysymyksiä tiedoistasi luonnollisella kielellä", + "Attempt" : "Yritys", + "Audit Trail Changes" : "Valvontaketjun muutokset", + "Audit Trail Details" : "Valvontaketjun tiedot", + "Audit Trail Management" : "Valvontaketjun hallinta", + "Audit Trail Statistics" : "Valvontaketjun tilastot", + "Audit Trails" : "Valvontaketjut", + "Audit trail data copied to clipboard" : "Valvontaketjun tiedot kopioitiin leikepöydälle", + "Audit trail deleted successfully" : "Valvontaketju poistettiin onnistuneesti", + "Audit trail successfully deleted" : "Valvontaketju poistettiin onnistuneesti", + "Authentication" : "Todennus", + "Auto-commit disabled" : "Automaattinen vahvistus poistettu käytöstä", + "Auto-commit enabled" : "Automaattinen vahvistus otettu käyttöön", + "Auto-create Default Organisation" : "Luo oletusorganisaatio automaattisesti", + "Auto-retry failed vectorizations" : "Yritä epäonnistuneita vektorointeja automaattisesti uudelleen", + "Automatically commit changes to SOLR index" : "Vahvista muutokset SOLR-indeksiin automaattisesti", + "Automatically create a default organisation if none exists when the app is initialized" : "Luo oletusorganisaatio automaattisesti, jos sellaista ei ole, kun sovellus alustetaan", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Luo vektoriupotukset automaattisesti tekstilohkoista, kun tiedostot ladataan ja käsitellään", + "Automatically generate vector embeddings when objects are created or updated" : "Luo vektoriupotukset automaattisesti, kun objekteja luodaan tai päivitetään", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Yritä epäonnistuneita vektorointiyrityksiä automaattisesti uudelleen (enintään 3 yritystä)", + "Available Filters" : "Käytettävissä olevat suodattimet", + "Available Workflows" : "Käytettävissä olevat työnkulut", + "Avg Execution Time" : "Keskim. suoritusaika", + "Avg Members/Org" : "Keskim. jäseniä/org.", + "Avg Object Views/Session" : "Keskim. objektinäkymiä/istunto", + "Avg Response Time" : "Keskim. vastausaika", + "Avg Results/Search" : "Keskim. tuloksia/haku", + "Avg Searches/Session" : "Keskim. hakuja/istunto", + "Back" : "Takaisin", + "Back to Entities" : "Takaisin entiteetteihin", + "Back to Registers" : "Takaisin rekistereihin", + "Back to Webhooks" : "Takaisin webhookeihin", + "Back to applications" : "Takaisin sovelluksiin", + "Back to entities" : "Takaisin entiteetteihin", + "Backend updated successfully. Please reload the application." : "Taustajärjestelmä päivitettiin onnistuneesti. Lataa sovellus uudelleen.", + "Base URL (Optional)" : "Perus-URL (valinnainen)", + "Basic Information" : "Perustiedot", + "Batch Size" : "Eräkoko", + "Batch extraction completed" : "Erätoiminen poiminta valmis", + "Before" : "Ennen", + "Before object vectorization can work:" : "Ennen kuin objektien vektorointi voi toimia:", + "Behavior Issue" : "Toimintaongelma", + "Bookmarks" : "Kirjanmerkit", + "Blob storage has been retired. All objects now use magic tables." : "Blob-tallennus on poistettu käytöstä. Kaikki objektit käyttävät nyt magic-tauluja.", + "Branch" : "Haara", + "Bulk delete operation completed successfully" : "Joukkopoistotoiminto valmistui onnistuneesti", + "Bulk save operation completed successfully" : "Joukkotallennustoiminto valmistui onnistuneesti", + "Business Data" : "Liiketoimintatiedot", + "Cache cleared successfully" : "Välimuisti tyhjennettiin onnistuneesti", + "Calculate Sizes" : "Laske koot", + "Cancel" : "Peruuta", + "Cannot delete: objects are still attached" : "Ei voi poistaa: objekteja on yhä liitettynä", + "Category" : "Luokka", + "Change Type" : "Muutoksen tyyppi", + "Changes" : "Muutokset", + "Changes copied to clipboard" : "Muutokset kopioitiin leikepöydälle", + "Chat Model" : "Keskustelumalli", + "Chat Provider" : "Keskustelupalveluntarjoaja", + "Chat Provider (RAG)" : "Keskustelupalveluntarjoaja (RAG)", + "Chat Settings" : "Keskusteluasetukset", + "Chat provider connection successful!" : "Yhteys keskustelupalveluntarjoajaan onnistui!", + "Chat settings" : "Keskusteluasetukset", + "Choose a register" : "Valitse rekisteri", + "Choose a schema" : "Valitse skeema", + "Choose how vector similarity calculations are performed for semantic search" : "Valitse, miten vektorin samankaltaisuuslaskelmat suoritetaan semanttista hakua varten", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Valitse, mitkä tiedostotyypit sisällytetään vektorointiprosessiin. Vain tiedostot, joista on poimittu tekstiä ja lohkoja, käsitellään.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Valitse, mitkä näkymät sisällytetään vektorointiprosessiin. Jätä tyhjäksi käsitelläksesi kaikki näkymät määritystesi mukaan.", + "Chunk" : "Lohko", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Lohkojen poistoa ei ole vielä toteutettu. Käytä lohkopohjaisia päätepisteitä.", + "Chunks" : "Lohkot", + "Chunks to Vectorize:" : "Vektoroitavat lohkot:", + "Cleanup Old Trails" : "Siivoa vanhat ketjut", + "Cleanup completed" : "Siivous valmis", + "Clear All Embeddings" : "Tyhjennä kaikki upotukset", + "Clear Entries" : "Tyhjennä merkinnät", + "Clear Filtered Audit Trails" : "Tyhjennä suodatetut valvontaketjut", + "Clear Filters" : "Tyhjennä suodattimet", + "Clear Index" : "Tyhjennä indeksi", + "Clear Selection" : "Tyhjennä valinta", + "Clear all" : "Tyhjennä kaikki", + "Clear all filters" : "Tyhjennä kaikki suodattimet", + "Clear date range" : "Tyhjennä päivämääräväli", + "Clearing..." : "Tyhjennetään...", + "Close" : "Sulje", + "Collection Management" : "Kokoelmien hallinta", + "Collection Name" : "Kokoelman nimi", + "Collection assignments updated successfully" : "Kokoelmamääritykset päivitettiin onnistuneesti", + "Collection cleared successfully" : "Kokoelma tyhjennettiin onnistuneesti", + "Collection copied successfully" : "Kokoelma kopioitiin onnistuneesti", + "Collection created successfully" : "Kokoelma luotiin onnistuneesti", + "Collection deleted successfully" : "Kokoelma poistettiin onnistuneesti", + "Collection used to store and index file metadata and content" : "Kokoelma, jota käytetään tiedostojen metatietojen ja sisällön tallentamiseen ja indeksointiin", + "Collection used to store and index object data" : "Kokoelma, jota käytetään objektitietojen tallentamiseen ja indeksointiin", + "Collections:" : "Kokoelmat:", + "Commit Message" : "Vahvistusviesti", + "Commit Within (ms)" : "Vahvista enintään (ms)", + "Commit message" : "Vahvistusviesti", + "Compare current schema with real object data" : "Vertaa nykyistä skeemaa todelliseen objektitietoon", + "Completed" : "Valmis", + "Complex" : "Monimutkainen", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Monimutkaiset kyselyt: edistyneet haut, joissa on useita suodattimia, operaattoreita ja monimutkaisia parametriyhdistelmiä", + "Confidence Level" : "Luottamustaso", + "Confidence Score" : "Luottamuspisteet", + "Config must be provided as an object" : "Määritys on annettava objektina", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSet-hallinta", + "ConfigSet Name" : "ConfigSet-nimi", + "ConfigSet created successfully" : "ConfigSet luotiin onnistuneesti", + "ConfigSet deleted successfully" : "ConfigSet poistettiin onnistuneesti", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSet-asetukset määrittelevät SOLR-kokoelmiesi skeeman ja määritykset. Ne sisältävät kenttämäärityksiä, analysaattoreita ja muita hakuasetuksia.", + "Configuration" : "Määritys", + "Configuration Data" : "Määritystiedot", + "Configuration saved" : "Määritys tallennettu", + "Configurations" : "Määritykset", + "Configure Facets" : "Määritä fasetit", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Määritä suuren kielimallin (LLM) palveluntarjoajat tekoälypohjaisia ominaisuuksia varten, mukaan lukien semanttinen haku, upotukset ja keskustelu.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Määritä SOLR-palvelimesi perusyhteysasetukset, mukaan lukien todennus ja verkkoasetukset. Hallitse ytimiä ja kokoelmia erillisten ConfigSet- ja kokoelmanhallinnan valintaikkunoiden avulla.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Määritä, miten tietokantaobjektit muunnetaan vektoriupotuksiksi semanttista hakua varten. Objektit vektoroidaan suoraan ilman tekstin poimintaa.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Määritä, miten objektit muunnetaan tekstiksi ennen vektorointia. Nämä asetukset vaikuttavat haun laatuun ja kontekstiin.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Määritä tiedostojen vektoroinnin parametrit. Tämä prosessi luo vektoriupotukset kaikille poimituille tiedostolohkoille.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Määritä objektien vektoroinnin parametrit. Tämä prosessi luo vektoriupotukset kaikille näkymäsuodattimiisi vastaaville objekteille.", + "Configure which types of data to search and how many sources to retrieve" : "Määritä, minkä tyyppisiä tietoja haetaan ja kuinka monta lähdettä noudetaan", + "Connection Failed" : "Yhteys epäonnistui", + "Connection Settings" : "Yhteysasetukset", + "Connection Successful!" : "Yhteys onnistui!", + "Connection failed" : "Yhteys epäonnistui", + "Connection protocol" : "Yhteysprotokolla", + "Connection successful" : "Yhteys onnistui", + "Connection timeout in seconds" : "Yhteyden aikakatkaisu sekunneissa", + "Constant delay between retries (5 minutes)" : "Vakioviive yritysten välillä (5 minuuttia)", + "Constraint Issue" : "Rajoiteongelma", + "Content Filters" : "Sisältösuodattimet", + "Continue to Properties" : "Jatka ominaisuuksiin", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Hallitse, mitkä objektinäkymät vektoroidaan API-kustannusten vähentämiseksi. Vektoroi vain näkymät, jotka hyötyvät semanttisesta hausta.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Hallitse, mitä näkymiä ja työkaluja tekoäly voi käyttää tässä keskustelussa. Oletuksena kaikki agentin ominaisuudet ovat käytössä.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Ohjaa satunnaisuutta (0 = deterministinen, 2 = erittäin luova)", + "Conversation ID is required" : "Keskustelun tunnus vaaditaan", + "Conversation archived" : "Keskustelu arkistoitu", + "Conversation archived successfully" : "Keskustelu arkistoitiin onnistuneesti", + "Conversation cleared successfully" : "Keskustelu tyhjennettiin onnistuneesti", + "Conversation deleted" : "Keskustelu poistettu", + "Conversation permanently deleted" : "Keskustelu poistettiin pysyvästi", + "Conversation renamed" : "Keskustelu nimettiin uudelleen", + "Conversation restored" : "Keskustelu palautettu", + "Conversation title" : "Keskustelun otsikko", + "Copied!" : "Kopioitu!", + "Copy" : "Kopioi", + "Copy Changes" : "Kopioi muutokset", + "Copy Collection" : "Kopioi kokoelma", + "Copy Data" : "Kopioi tiedot", + "Copy Full Data" : "Kopioi kaikki tiedot", + "Copying..." : "Kopioidaan...", + "Count" : "Määrä", + "Counting objects..." : "Lasketaan objekteja...", + "Costs" : "Kustannukset", + "Create" : "Luo", + "Create Collection" : "Luo kokoelma", + "Create ConfigSet" : "Luo ConfigSet", + "Create New Collection" : "Luo uusi kokoelma", + "Create New ConfigSet" : "Luo uusi ConfigSet", + "Create Operations" : "Luontitoiminnot", + "Create Webhook" : "Luo webhook", + "Create a copy of collection:" : "Luo kopio kokoelmasta:", + "Create a new ConfigSet based on the _default template" : "Luo uusi ConfigSet _default-mallin pohjalta", + "Create a new SOLR collection from an existing ConfigSet" : "Luo uusi SOLR-kokoelma olemassa olevasta ConfigSet-asetuksesta", + "Create your first AI agent to get started." : "Luo ensimmäinen tekoälyagenttisi päästäksesi alkuun.", + "Created" : "Luotu", + "Created:" : "Luotu:", + "Creating..." : "Luodaan...", + "Current Filters" : "Nykyiset suodattimet", + "Current Type:" : "Nykyinen tyyppi:", + "Custom API endpoint if using a different region" : "Mukautettu API-päätepiste, jos käytät eri aluetta", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Mukautetut HTTP-otsikot (yksi riviä kohden, muoto: Header-Name: arvo)", + "Daily" : "Päivittäin", + "Dashboard" : "Hallintapaneeli", + "Data Source" : "Tietolähde", + "Data sources" : "Tietolähteet", + "Data type does not match observed values" : "Tietotyyppi ei vastaa havaittuja arvoja", + "Database URL" : "Tietokannan URL", + "Database information refreshed" : "Tietokannan tiedot päivitetty", + "Date Range" : "Päivämääräväli", + "Default" : "Oletus", + "Default Organisation" : "Oletusorganisaatio", + "Default: 5" : "Oletus: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Viiveet kaksinkertaistuvat jokaisella yrityksellä (2, 4, 8 minuuttia...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Viiveet kasvavat lineaarisesti (5, 10, 15 minuuttia...)", + "Delete" : "Poista", + "Delete Application" : "Poista sovellus", + "Delete Audit Trail" : "Poista valvontaketju", + "Delete Collection" : "Poista kokoelma", + "Delete ConfigSet" : "Poista ConfigSet", + "Delete Objects" : "Poista objektit", + "Delete Operations" : "Poistotoiminnot", + "Delete View" : "Poista näkymä", + "Delete all objects in this schema" : "Poista kaikki tämän skeeman objektit", + "Delete permanently" : "Poista pysyvästi", + "Delete this conversation?" : "Poistetaanko tämä keskustelu?", + "Delete view" : "Poista näkymä", + "Deleted" : "Poistettu", + "Deleted By" : "Poistanut", + "Deleted Date" : "Poistopäivä", + "Deleted Items Management" : "Poistettujen kohteiden hallinta", + "Deleted This Week" : "Poistettu tällä viikolla", + "Deleted Today" : "Poistettu tänään", + "Deleted:" : "Poistettu:", + "Deleting..." : "Poistetaan...", + "Deletion Date Range" : "Poiston päivämääräväli", + "Deletion Statistics" : "Poiston tilastot", + "Deprecated" : "Vanhentunut", + "Description" : "Kuvaus", + "Description:" : "Kuvaus:", + "Detect data types and patterns" : "Tunnista tietotyypit ja kuviot", + "Detected At" : "Havaittu", + "Detected Format:" : "Havaittu muoto:", + "Detected Issues:" : "Havaitut ongelmat:", + "Disable" : "Poista käytöstä", + "Disabled" : "Poistettu käytöstä", + "Discover Files" : "Etsi tiedostoja", + "Discovered Properties" : "Löydetyt ominaisuudet", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Haluatko poistaa pysyvästi kaikki suodatetut valvontaketjumerkinnät? Tätä toimintoa ei voi peruuttaa.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Haluatko poistaa pysyvästi tämän valvontaketjumerkinnän? Tätä toimintoa ei voi peruuttaa.", + "Documents" : "Asiakirjat", + "Download API Spec" : "Lataa API-määritys", + "Edit" : "Muokkaa", + "Edit Register" : "Muokkaa rekisteriä", + "Edit Schema" : "Muokkaa skeemaa", + "Edit View" : "Muokkaa näkymää", + "Edit Webhook" : "Muokkaa webhookia", + "Edit view" : "Muokkaa näkymää", + "Edit view details" : "Muokkaa näkymän tietoja", + "Effectiveness" : "Tehokkuus", + "Email" : "Sähköposti", + "Emails" : "Sähköpostit", + "Embedding Model" : "Upotusmalli", + "Embedding Provider" : "Upotuspalveluntarjoaja", + "Embedding generated successfully" : "Upotus luotiin onnistuneesti", + "Embedding provider connection successful!" : "Yhteys upotuspalveluntarjoajaan onnistui!", + "Enable" : "Ota käyttöön", + "Enable auto-creation" : "Ota automaattinen luonti käyttöön", + "Enable automatic file vectorization" : "Ota automaattinen tiedostojen vektorointi käyttöön", + "Enable automatic object vectorization" : "Ota automaattinen objektien vektorointi käyttöön", + "Enable automatic synchronization" : "Ota automaattinen synkronointi käyttöön", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Ota yksityiskohtainen lokiin kirjaaminen käyttöön SOLR-toiminnoille (suositellaan virheenkorjaukseen)", + "Enable faceting" : "Ota fasetointi käyttöön", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Ota LLM-ominaisuudet käyttöön tai poista ne käytöstä. Määritä palveluntarjoajat ja mallit yllä olevalla LLM-määrityspainikkeella.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Ota SOLR-haun integraatio käyttöön tai poista se käytöstä. Määritä yhteysasetukset yllä olevalla Yhteysasetukset-painikkeella.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Ota n8n-työnkulkuintegraatio käyttöön tai poista se käytöstä. Määritä yhteysasetukset alta.", + "Enable or disable this webhook" : "Ota tämä webhook käyttöön tai poista se käytöstä", + "Enable vectorization for all existing and future views (may increase costs)" : "Ota vektorointi käyttöön kaikille olemassa oleville ja tuleville näkymille (voi lisätä kustannuksia)", + "Enabled" : "Käytössä", + "Endpoints" : "Päätepisteet", + "Enter ConfigSet name" : "Anna ConfigSet-nimi", + "Enter collection name" : "Anna kokoelman nimi", + "Enter description (optional)..." : "Anna kuvaus (valinnainen)...", + "Enter new collection name" : "Anna uuden kokoelman nimi", + "Enter object ID" : "Anna objektin tunnus", + "Enter search term" : "Anna hakutermi", + "Enter view name..." : "Anna näkymän nimi...", + "Enter webhook name" : "Anna webhookin nimi", + "Entities" : "Entiteetit", + "Entity Information" : "Entiteettitiedot", + "Entity deleted successfully" : "Entiteetti poistettiin onnistuneesti", + "Entity not found" : "Entiteettiä ei löytynyt", + "Entries to be deleted:" : "Poistettavat merkinnät:", + "Enum Issue" : "Luettelo-ongelma", + "Enum constraint is missing" : "Luettelorajoite puuttuu", + "Error" : "Virhe", + "Error Details" : "Virheen tiedot", + "Error Information" : "Virhetiedot", + "Error Message" : "Virheviesti", + "Error loading application" : "Virhe sovellusta ladattaessa", + "Error loading audit trails" : "Virhe valvontaketjuja ladattaessa", + "Error loading entity" : "Virhe entiteettiä ladattaessa", + "Error loading search trails" : "Virhe hakuketjuja ladattaessa", + "Estimated Batches:" : "Arvioidut erät:", + "Estimated Cost:" : "Arvioidut kustannukset:", + "Estimated Duration:" : "Arvioitu kesto:", + "Event" : "Tapahtuma", + "Event Property for Payload" : "Tapahtuman ominaisuus hyötykuormaa varten", + "Events" : "Tapahtumat", + "Example Value" : "Esimerkkiarvo", + "Exclusive Maximum" : "Eksklusiivinen maksimi", + "Exclusive Minimum" : "Eksklusiivinen minimi", + "Execution Mode" : "Suoritustila", + "Execution Time" : "Suoritusaika", + "Execution Time Range" : "Suoritusajan väli", + "Existing Improvements" : "Olemassa olevat parannukset", + "Exponential" : "Eksponentiaalinen", + "Export" : "Vie", + "Export completed successfully" : "Vienti valmistui onnistuneesti", + "Export, view, or delete audit trails" : "Vie, tarkastele tai poista valvontaketjuja", + "Extend Schema" : "Laajenna skeemaa", + "Extended" : "Laajennettu", + "Extensions" : "Laajennukset", + "Extract Pending Files" : "Poimi odottavat tiedostot", + "Extract properties from each object" : "Poimi ominaisuudet jokaisesta objektista", + "Extracted At" : "Poimittu", + "Extraction Status" : "Poiminnan tila", + "Facet configuration updated successfully" : "Fasettimääritys päivitettiin onnistuneesti", + "Facets discovered and configured successfully" : "Fasetit löydettiin ja määritettiin onnistuneesti", + "Facets discovered successfully" : "Fasetit löydettiin onnistuneesti", + "Failed" : "Epäonnistui", + "Failed to analyze schema properties" : "Skeeman ominaisuuksien analysointi epäonnistui", + "Failed to archive conversation" : "Keskustelun arkistointi epäonnistui", + "Failed to calculate sizes" : "Kokojen laskeminen epäonnistui", + "Failed to clear collection" : "Kokoelman tyhjentäminen epäonnistui", + "Failed to copy changes" : "Muutosten kopiointi epäonnistui", + "Failed to copy data" : "Tietojen kopiointi epäonnistui", + "Failed to copy data to clipboard" : "Tietojen kopiointi leikepöydälle epäonnistui", + "Failed to create conversation" : "Keskustelun luominen epäonnistui", + "Failed to create or find project" : "Projektin luominen tai löytäminen epäonnistui", + "Failed to delete collection" : "Kokoelman poistaminen epäonnistui", + "Failed to delete conversation" : "Keskustelun poistaminen epäonnistui", + "Failed to delete webhook" : "Webhookin poistaminen epäonnistui", + "Failed to download API specification" : "API-määrityksen lataaminen epäonnistui", + "Failed to get SOLR field configuration" : "SOLR-kenttämäärityksen hakeminen epäonnistui", + "Failed to get entity categories" : "Entiteettiluokkien hakeminen epäonnistui", + "Failed to get entity statistics" : "Entiteettitilastojen hakeminen epäonnistui", + "Failed to get entity types" : "Entiteettityyppien hakeminen epäonnistui", + "Failed to load LLM configuration" : "LLM-määrityksen lataaminen epäonnistui", + "Failed to load Nextcloud groups" : "Nextcloud-ryhmien lataaminen epäonnistui", + "Failed to load conversation" : "Keskustelun lataaminen epäonnistui", + "Failed to load entities" : "Entiteettien lataaminen epäonnistui", + "Failed to load entity" : "Entiteetin lataaminen epäonnistui", + "Failed to load files" : "Tiedostojen lataaminen epäonnistui", + "Failed to load organisations" : "Organisaatioiden lataaminen epäonnistui", + "Failed to load templates" : "Mallien lataaminen epäonnistui", + "Failed to load webhooks" : "Webhookien lataaminen epäonnistui", + "Failed to load workflows" : "Työnkulkujen lataaminen epäonnistui", + "Failed to refresh database information" : "Tietokannan tietojen päivittäminen epäonnistui", + "Failed to reindex collection" : "Kokoelman uudelleenindeksointi epäonnistui", + "Failed to rename conversation" : "Keskustelun uudelleennimeäminen epäonnistui", + "Failed to restore conversation" : "Keskustelun palauttaminen epäonnistui", + "Failed to retry extraction" : "Poiminnan uudelleenyritys epäonnistui", + "Failed to retry webhook" : "Webhookin uudelleenyritys epäonnistui", + "Failed to save GitHub token" : "GitHub-tunnisteen tallentaminen epäonnistui", + "Failed to save GitLab URL" : "GitLab-URL:n tallentaminen epäonnistui", + "Failed to save GitLab token" : "GitLab-tunnisteen tallentaminen epäonnistui", + "Failed to save additional feedback" : "Lisäpalautteen tallentaminen epäonnistui", + "Failed to save n8n configuration" : "n8n-määrityksen tallentaminen epäonnistui", + "Failed to save roles" : "Roolien tallentaminen epäonnistui", + "Failed to save settings" : "Asetusten tallentaminen epäonnistui", + "Failed to save webhook" : "Webhookin tallentaminen epäonnistui", + "Failed to send feedback" : "Palautteen lähettäminen epäonnistui", + "Failed to test webhook" : "Webhookin testaaminen epäonnistui", + "Failed to update favorite status" : "Suosikkitilan päivittäminen epäonnistui", + "Failed to update schema properties" : "Skeeman ominaisuuksien päivittäminen epäonnistui", + "Failed to update view" : "Näkymän päivittäminen epäonnistui", + "Failed to update webhook" : "Webhookin päivittäminen epäonnistui", + "Feedback recorded" : "Palaute tallennettu", + "Fewer sources (1-3):" : "Vähemmän lähteitä (1-3):", + "Field" : "Kenttä", + "File" : "Tiedosto", + "File Chunk Prediction" : "Tiedostolohkon ennuste", + "File Collection" : "Tiedostokokoelma", + "File Management" : "Tiedostojen hallinta", + "File Name" : "Tiedoston nimi", + "File Path" : "Tiedostopolku", + "File Type Selection" : "Tiedostotyypin valinta", + "File Vectorization" : "Tiedostojen vektorointi", + "File Warmup" : "Tiedostojen lämmitys", + "File actions menu" : "Tiedostotoimintojen valikko", + "File anonymized successfully" : "Tiedosto anonymisoitiin onnistuneesti", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Tiedostolohkojen vektorointia ei ole vielä toteutettu. Lohkot ovat valmiina ja tallennettuna, mutta vektorointipalvelu on kehitteillä.", + "File collection not configured" : "Tiedostokokoelmaa ei ole määritetty", + "File discovery completed" : "Tiedostojen etsintä valmis", + "File extraction completed" : "Tiedostojen poiminta valmis", + "File extraction queued" : "Tiedostojen poiminta asetettu jonoon", + "File indexed successfully" : "Tiedosto indeksoitiin onnistuneesti", + "File is already anonymized" : "Tiedosto on jo anonymisoitu", + "File not found" : "Tiedostoa ei löytynyt", + "File settings updated successfully" : "Tiedostoasetukset päivitettiin onnistuneesti", + "File sources" : "Tiedostolähteet", + "File vectorization configuration saved successfully" : "Tiedostojen vektorointimääritys tallennettiin onnistuneesti", + "File vectorization started. Check the statistics section for progress." : "Tiedostojen vektorointi aloitettu. Tarkista edistyminen tilasto-osiosta.", + "File warmup completed" : "Tiedostojen lämmitys valmis", + "Files" : "Tiedostot", + "Files with Completed Extraction:" : "Tiedostot, joiden poiminta on valmis:", + "Files → fileCollection, Objects → objectCollection" : "Tiedostot → fileCollection, objektit → objectCollection", + "Filter Audit Trails" : "Suodata valvontaketjuja", + "Filter Deleted Items" : "Suodata poistettuja kohteita", + "Filter Objects" : "Suodata objekteja", + "Filter Properties" : "Suodata ominaisuuksia", + "Filter Search Trails" : "Suodata hakuketjuja", + "Filter Statistics" : "Suodata tilastoja", + "Filter and analyze search trail entries" : "Suodata ja analysoi hakuketjumerkintöjä", + "Filter and manage audit trail entries" : "Suodata ja hallitse valvontaketjumerkintöjä", + "Filter and manage soft deleted items" : "Suodata ja hallitse pehmeästi poistettuja kohteita", + "Filter by object ID" : "Suodata objektin tunnuksen mukaan", + "Filter by search term" : "Suodata hakutermin mukaan", + "Filter by webhook" : "Suodata webhookin mukaan", + "Filter data loaded automatically. Use the filters below to refine your search." : "Suodatustiedot ladattu automaattisesti. Tarkenna hakuasi alla olevilla suodattimilla.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Suodata webhookien laukaisimet hyötykuorman ominaisuuksien mukaan (yksi riviä kohden, muoto: avain: arvo)", + "Filtered" : "Suodatettu", + "Filters" : "Suodattimet", + "Fireworks AI Chat Settings" : "Fireworks AI -keskusteluasetukset", + "Fireworks AI Embedding Configuration" : "Fireworks AI -upotusmääritys", + "First" : "Ensimmäinen", + "Fixed" : "Korjattu", + "For chat and retrieval-augmented generation" : "Keskustelua ja haulla täydennettyä generointia varten", + "For vector embeddings and semantic search" : "Vektoriupotuksia ja semanttista hakua varten", + "Format" : "Muoto", + "Format Issue" : "Muoto-ongelma", + "Format constraint is missing" : "Muotorajoite puuttuu", + "From Date" : "Alkupäivä", + "From date" : "Alkupäivä", + "Full data copied to clipboard" : "Kaikki tiedot kopioitiin leikepöydälle", + "General Issue" : "Yleinen ongelma", + "Generate recommendations and confidence scores" : "Luo suosituksia ja luottamuspisteitä", + "Generate vectors immediately when new objects are created" : "Luo vektorit välittömästi, kun uusia objekteja luodaan", + "GitHub token is valid" : "GitHub-tunniste on kelvollinen", + "GitHub token removed successfully" : "GitHub-tunniste poistettiin onnistuneesti", + "GitHub token saved successfully" : "GitHub-tunniste tallennettiin onnistuneesti", + "GitLab URL saved successfully" : "GitLab-URL tallennettiin onnistuneesti", + "GitLab token is valid" : "GitLab-tunniste on kelvollinen", + "GitLab token saved successfully" : "GitLab-tunniste tallennettiin onnistuneesti", + "HTTP Method" : "HTTP-menetelmä", + "HTTP method used to send webhook requests" : "HTTP-menetelmä, jota käytetään webhook-pyyntöjen lähettämiseen", + "Headers" : "Otsikot", + "Health" : "Kunto", + "Heartbeat successful - connection kept alive" : "Sykäys onnistui - yhteys pidetty yllä", + "Helpful" : "Hyödyllinen", + "Hide Filters" : "Piilota suodattimet", + "Hide in forms" : "Piilota lomakkeissa", + "Hide in list view" : "Piilota luettelonäkymässä", + "High" : "Korkea", + "High Confidence" : "Korkea luottamus", + "Host" : "Isäntä", + "Hourly" : "Tunneittain", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Kuinka syvälle sisäkkäisiä objektien ominaisuuksia käydään läpi (1-20). Korkeammat arvot tallentavat enemmän yksityiskohtia mutta kasvattavat vektorin kokoa.", + "How often to check for updates (1-168 hours)" : "Kuinka usein päivityksiä tarkistetaan (1-168 tuntia)", + "How to handle retries for failed webhook deliveries" : "Miten käsitellä uudelleenyrityksiä epäonnistuneille webhook-toimituksille", + "ID" : "Tunnus", + "ID:" : "Tunnus:", + "IP rate limits cleared successfully" : "IP-nopeusrajoitukset tyhjennettiin onnistuneesti", + "Identify properties not in the schema" : "Tunnista ominaisuudet, jotka eivät ole skeemassa", + "Immutable" : "Muuttumaton", + "Import" : "Tuo", + "Import successful" : "Tuonti onnistui", + "Improved Property" : "Parannettu ominaisuus", + "In use" : "Käytössä", + "Inactive" : "Ei-aktiivinen", + "Include IDs and names of related objects for better contextual search" : "Sisällytä liittyvien objektien tunnukset ja nimet paremman kontekstihaun saamiseksi", + "Include related object references" : "Sisällytä liittyvien objektien viittaukset", + "Include schema and register metadata" : "Sisällytä skeeman ja rekisterin metatiedot", + "Initialization failed" : "Alustus epäonnistui", + "Initialize Project" : "Alusta projekti", + "Inspect Fields" : "Tarkastele kenttiä", + "Inspect Index" : "Tarkastele indeksiä", + "Invalid" : "Virheellinen", + "Invalid batch size. Must be between 1 and 5000" : "Virheellinen eräkoko. Sen on oltava välillä 1 ja 5000", + "Invalid field name provided" : "Annettu virheellinen kentän nimi", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Virheellinen maxObjects-arvo. Sen on oltava 0 (kaikki) tai positiivinen luku", + "Invalid:" : "Virheellinen:", + "Issue" : "Ongelma", + "Items per page:" : "Kohteita sivua kohden:", + "Just now" : "Juuri nyt", + "LLM Configuration" : "LLM-määritys", + "LLM actions menu" : "LLM-toimintojen valikko", + "LLM configuration saved successfully" : "LLM-määritys tallennettiin onnistuneesti", + "LLM features disabled" : "LLM-ominaisuudet poistettu käytöstä", + "LLM features enabled" : "LLM-ominaisuudet otettu käyttöön", + "LLM must be enabled with an embedding provider configured" : "LLM on otettava käyttöön ja upotuspalveluntarjoaja on määritettävä", + "LLM settings updated successfully" : "LLM-asetukset päivitettiin onnistuneesti", + "Last" : "Viimeinen", + "Last Triggered" : "Viimeksi laukaistu", + "Length Range:" : "Pituusväli:", + "Linear" : "Lineaarinen", + "Load Advanced Filters" : "Lataa lisäsuodattimet", + "Load advanced filters with live data from your search index" : "Lataa lisäsuodattimet reaaliaikaisilla tiedoilla hakuindeksistäsi", + "Load view" : "Lataa näkymä", + "Loading ConfigSets..." : "Ladataan ConfigSet-asetuksia...", + "Loading advanced filters..." : "Ladataan lisäsuodattimia...", + "Loading agents..." : "Ladataan agentteja...", + "Loading application details..." : "Ladataan sovelluksen tietoja...", + "Loading applications..." : "Ladataan sovelluksia...", + "Loading archived conversations..." : "Ladataan arkistoituja keskusteluja...", + "Loading audit trails..." : "Ladataan valvontaketjuja...", + "Loading collections..." : "Ladataan kokoelmia...", + "Loading configurations..." : "Ladataan määrityksiä...", + "Loading conversation..." : "Ladataan keskustelua...", + "Loading conversations..." : "Ladataan keskusteluja...", + "Loading deleted items..." : "Ladataan poistettuja kohteita...", + "Loading events..." : "Ladataan tapahtumia...", + "Loading filter data..." : "Ladataan suodatustietoja...", + "Loading groups..." : "Ladataan ryhmiä...", + "Loading log details..." : "Ladataan lokin tietoja...", + "Loading registers..." : "Ladataan rekistereitä...", + "Loading schemas..." : "Ladataan skeemoja...", + "Loading search trails..." : "Ladataan hakuketjuja...", + "Loading sources..." : "Ladataan lähteitä...", + "Loading statistics..." : "Ladataan tilastoja...", + "Loading users..." : "Ladataan käyttäjiä...", + "Loading views..." : "Ladataan näkymiä...", + "Loading..." : "Ladataan...", + "Local" : "Paikallinen", + "Local Version" : "Paikallinen versio", + "Locked" : "Lukittu", + "Locked:" : "Lukittu:", + "Login successful" : "Kirjautuminen onnistui", + "Logs" : "Lokit", + "Low" : "Matala", + "Low Confidence" : "Matala luottamus", + "Lucene Version:" : "Lucene-versio:", + "Magic table synchronized successfully" : "Magic-taulu synkronoitiin onnistuneesti", + "Manage Organisation Roles" : "Hallitse organisaation rooleja", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Hallitse SOLR-kokoelmia (tietovarastoja) ja osoita ne objekteille ja tiedostoille.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Hallitse kokoelmiesi SOLR-ConfigSet-asetuksia (määritysmalleja).", + "Manage and configure agents for automated tasks" : "Hallitse ja määritä agentteja automatisoituja tehtäviä varten", + "Manage and monitor file text extraction status" : "Hallitse ja valvo tiedostojen tekstinpoiminnan tilaa", + "Manage and restore soft deleted items from your registers" : "Hallitse ja palauta pehmeästi poistettuja kohteita rekistereistäsi", + "Manage and view detected entities from files and objects" : "Hallitse ja tarkastele tiedostoista ja objekteista havaittuja entiteettejä", + "Manage document templates and themes" : "Hallitse asiakirjamalleja ja teemoja", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Hallitse n8n-työnkulkuja OpenRegister-automaatiota varten. Työnkulut tallennetaan määritettyyn projektiin.", + "Manage webhooks for event-driven integrations" : "Hallitse webhookeja tapahtumalähtöisiä integraatioita varten", + "Manage your applications and modules" : "Hallitse sovelluksiasi ja moduulejasi", + "Manage your data registers and their configurations" : "Hallitse tietorekistereitäsi ja niiden määrityksiä", + "Manage your data schemas and their properties" : "Hallitse tietoskeemojasi ja niiden ominaisuuksia", + "Manage your data sources and their configurations" : "Hallitse tietolähteitäsi ja niiden määrityksiä", + "Manage your saved search configurations" : "Hallitse tallennettuja hakumäärityksiäsi", + "Manage your system configurations and settings" : "Hallitse järjestelmämäärityksiäsi ja -asetuksiasi", + "Managed" : "Hallittu", + "Max Files (0 = all)" : "Tiedostojen enimmäismäärä (0 = kaikki)", + "Max Length" : "Enimmäispituus", + "Max Length:" : "Enimmäispituus:", + "Max Objects (0 = all)" : "Objektien enimmäismäärä (0 = kaikki)", + "Max Retries" : "Uudelleenyritysten enimmäismäärä", + "Max Shards/Node" : "Sirpaleiden enimmäismäärä/solmu", + "Max execution time (ms)" : "Suorituksen enimmäisaika (ms)", + "Max ms" : "Enint. ms", + "Max result count" : "Tulosten enimmäismäärä", + "Max results" : "Enimmäistulokset", + "Maximum" : "Maksimi", + "Maximum Nesting Depth" : "Sisäkkäisyyden enimmäissyvyys", + "Maximum length constraint is missing" : "Enimmäispituusrajoite puuttuu", + "Maximum length is too restrictive" : "Enimmäispituus on liian rajoittava", + "Maximum number of files to process. Set to 0 to process all files." : "Käsiteltävien tiedostojen enimmäismäärä. Aseta arvoon 0 käsitelläksesi kaikki tiedostot.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Käsiteltävien objektien enimmäismäärä. Aseta arvoon 0 käsitelläksesi kaikki objektit.", + "Maximum number of retry attempts for failed deliveries" : "Uudelleenyritysten enimmäismäärä epäonnistuneille toimituksille", + "Maximum time to wait before committing changes" : "Enimmäisaika, jonka odotetaan ennen muutosten vahvistamista", + "Maximum tokens to generate" : "Generoitavien tokenien enimmäismäärä", + "Maximum value constraint is missing" : "Enimmäisarvorajoite puuttuu", + "Maximum value is too restrictive" : "Enimmäisarvo on liian rajoittava", + "Medium" : "Keskitaso", + "Medium Confidence" : "Keskitason luottamus", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Keskitason kyselyt: haut, joissa on jonkin verran suodatusta tai useita parametreja (esim. päivämääräväliä, tiettyjä rekistereitä/skeemoja)", + "Members of selected groups can access this view" : "Valittujen ryhmien jäsenet voivat käyttää tätä näkymää", + "Members:" : "Jäsenet:", + "Memory prediction calculated successfully" : "Muistiennuste laskettiin onnistuneesti", + "Message content is required" : "Viestin sisältö vaaditaan", + "Message does not belong to this conversation" : "Viesti ei kuulu tähän keskusteluun", + "Metadata Filters" : "Metatietosuodattimet", + "Method" : "Menetelmä", + "Min Length" : "Vähimmäispituus", + "Min execution time (ms)" : "Suorituksen vähimmäisaika (ms)", + "Min ms" : "Väh. ms", + "Min result count" : "Tulosten vähimmäismäärä", + "Min results" : "Vähimmäistulokset", + "Minimum" : "Minimi", + "Minimum value constraint is missing" : "Vähimmäisarvorajoite puuttuu", + "Minimum value is too restrictive" : "Vähimmäisarvo on liian rajoittava", + "Missing object fields created successfully" : "Puuttuvat objektikentät luotiin onnistuneesti", + "Mode:" : "Tila:", + "Model" : "Malli", + "Model Name" : "Mallin nimi", + "Modified" : "Muokattu", + "Monthly" : "Kuukausittain", + "More sources (10-20):" : "Enemmän lähteitä (10-20):", + "Most Active Objects" : "Aktiivisimmat objektit", + "Multiple Of" : "Monikerta", + "Name" : "Nimi", + "Names cache warmup completed" : "Nimien välimuistin lämmitys valmis", + "New Collection Name" : "Uuden kokoelman nimi", + "New Conversation" : "Uusi keskustelu", + "New Properties" : "Uudet ominaisuudet", + "New Property" : "Uusi ominaisuus", + "New Value" : "Uusi arvo", + "New users without specific organisation membership will be automatically added to this organisation" : "Uudet käyttäjät, joilla ei ole tiettyä organisaatiojäsenyyttä, lisätään automaattisesti tähän organisaatioon", + "Next" : "Seuraava", + "No" : "Ei", + "No ConfigSets found" : "ConfigSet-asetuksia ei löytynyt", + "No GitHub token configured" : "GitHub-tunnistetta ei ole määritetty", + "No GitHub token provided" : "GitHub-tunnistetta ei annettu", + "No GitLab token provided" : "GitLab-tunnistetta ei annettu", + "No Organisation" : "Ei organisaatiota", + "No active filters" : "Ei aktiivisia suodattimia", + "No activity data available" : "Toimintatietoja ei ole saatavilla", + "No agents available" : "Agentteja ei ole saatavilla", + "No agents found" : "Agentteja ei löytynyt", + "No app store cache exists yet" : "Sovelluskaupan välimuistia ei ole vielä olemassa", + "No applications are available." : "Sovelluksia ei ole saatavilla.", + "No applications found" : "Sovelluksia ei löytynyt", + "No archived conversations" : "Ei arkistoituja keskusteluja", + "No audit trail entries found" : "Valvontaketjumerkintöjä ei löytynyt", + "No changes recorded" : "Muutoksia ei tallennettu", + "No chunks to vectorize" : "Ei vektoroitavia lohkoja", + "No collections found" : "Kokoelmia ei löytynyt", + "No configuration data" : "Ei määritystietoja", + "No configurations are available." : "Määrityksiä ei ole saatavilla.", + "No configurations found" : "Määrityksiä ei löytynyt", + "No configurations found for this application." : "Tälle sovellukselle ei löytynyt määrityksiä.", + "No conversations yet. Create a new one to get started!" : "Ei vielä keskusteluja. Luo uusi päästäksesi alkuun!", + "No data available" : "Tietoja ei ole saatavilla", + "No data available for chart" : "Kaaviolle ei ole saatavilla tietoja", + "No deleted items found" : "Poistettuja kohteita ei löytynyt", + "No deletion data available" : "Poistotietoja ei ole saatavilla", + "No description found" : "Kuvausta ei löytynyt", + "No description provided" : "Kuvausta ei annettu", + "No entities detected in this file. Run text extraction first." : "Tästä tiedostosta ei havaittu entiteettejä. Suorita ensin tekstin poiminta.", + "No entities found" : "Entiteettejä ei löytynyt", + "No entities have been detected yet" : "Entiteettejä ei ole vielä havaittu", + "No events found" : "Tapahtumia ei löytynyt", + "No expired audit trails found to clear" : "Vanhentuneita valvontaketjuja ei löytynyt tyhjennettäväksi", + "No facetable fields available. Select a register and schema to see available filters." : "Fasetoitavia kenttiä ei ole saatavilla. Valitse rekisteri ja skeema nähdäksesi käytettävissä olevat suodattimet.", + "No files found" : "Tiedostoja ei löytynyt", + "No files have been extracted yet" : "Tiedostoja ei ole vielä poimittu", + "No files to process" : "Ei käsiteltäviä tiedostoja", + "No files to reindex" : "Ei uudelleenindeksoitavia tiedostoja", + "No filters are currently active. This will delete ALL audit trail entries!" : "Yhtään suodatinta ei ole tällä hetkellä aktiivisena. Tämä poistaa KAIKKI valvontaketjumerkinnät!", + "No log entries found" : "Lokimerkintöjä ei löytynyt", + "No logs are available for this configuration." : "Tälle määritykselle ei ole saatavilla lokeja.", + "No logs are available for this source." : "Tälle lähteelle ei ole saatavilla lokeja.", + "No logs found" : "Lokeja ei löytynyt", + "No mismatched fields found - SOLR schema is properly configured" : "Yhteensopimattomia kenttiä ei löytynyt - SOLR-skeema on määritetty oikein", + "No objects found" : "Objekteja ei löytynyt", + "No objects found to vectorize" : "Vektoroitavia objekteja ei löytynyt", + "No objects selected for deletion" : "Poistettavaksi ei ole valittu objekteja", + "No objects to delete" : "Ei poistettavia objekteja", + "No objects were permanently deleted" : "Yhtään objektia ei poistettu pysyvästi", + "No properties found" : "Ominaisuuksia ei löytynyt", + "No properties match your filters." : "Mikään ominaisuus ei vastaa suodattimiasi.", + "No purge date set" : "Tyhjennyspäivää ei ole asetettu", + "No register data available" : "Rekisteritietoja ei ole saatavilla", + "No registers found" : "Rekistereitä ei löytynyt", + "No registers found for this application." : "Tälle sovellukselle ei löytynyt rekistereitä.", + "No relations found" : "Suhteita ei löytynyt", + "No request body available" : "Pyynnön runkoa ei ole saatavilla", + "No response body available" : "Vastauksen runkoa ei ole saatavilla", + "No saved views yet. Create one in the Search tab!" : "Ei vielä tallennettuja näkymiä. Luo sellainen Haku-välilehdellä!", + "No schema data available" : "Skeematietoja ei ole saatavilla", + "No schema selected for exploration" : "Tutkittavaksi ei ole valittu skeemaa", + "No schemas found" : "Skeemoja ei löytynyt", + "No schemas found for this application." : "Tälle sovellukselle ei löytynyt skeemoja.", + "No search terms data available" : "Hakutermitietoja ei ole saatavilla", + "No search trail entries found" : "Hakuketjumerkintöjä ei löytynyt", + "No sources are available." : "Lähteitä ei ole saatavilla.", + "No sources found" : "Lähteitä ei löytynyt", + "No templates found" : "Malleja ei löytynyt", + "No templates have been created yet" : "Malleja ei ole vielä luotu", + "No valid magic-mapped register+schema combinations found" : "Kelvollisia magic-mapattuja rekisteri+skeema-yhdistelmiä ei löytynyt", + "No views found. Create views first before configuring vectorization." : "Näkymiä ei löytynyt. Luo ensin näkymiä ennen vektoroinnin määrittämistä.", + "No views match your search" : "Mikään näkymä ei vastaa hakuasi", + "No webhooks found" : "Webhookeja ei löytynyt", + "No webhooks have been configured yet" : "Webhookeja ei ole vielä määritetty", + "No workflows found in this project. Create workflows in the n8n editor." : "Tästä projektista ei löytynyt työnkulkuja. Luo työnkulkuja n8n-editorissa.", + "None" : "Ei mitään", + "Not helpful" : "Ei hyödyllinen", + "Not in use" : "Ei käytössä", + "Number Constraints" : "Numerorajoitteet", + "Number of chunks to vectorize in one API call (1-100)." : "Yhdellä API-kutsulla vektoroitavien lohkojen määrä (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Yhdellä API-kutsulla vektoroitavien lohkojen määrä. Suurempi = nopeampi mutta enemmän muistia. Suositus: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Kussakin erässä käsiteltävien objektien määrä (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Yhdellä API-kutsulla vektoroitavien objektien määrä. Suurempi = nopeampi mutta enemmän muistia. Suositus: 10-50.", + "Numeric Range:" : "Numeroväli:", + "Object" : "Objekti", + "Object Collection" : "Objektikokoelma", + "Object Count Prediction" : "Objektien määrän ennuste", + "Object ID" : "Objektin tunnus", + "Object Management" : "Objektien hallinta", + "Object Vectorization" : "Objektien vektorointi", + "Object Warmup" : "Objektien lämmitys", + "Object collection not configured" : "Objektikokoelmaa ei ole määritetty", + "Object not found" : "Objektia ei löytynyt", + "Object permanently deleted" : "Objekti poistettiin pysyvästi", + "Object restored successfully" : "Objekti palautettiin onnistuneesti", + "Object settings updated successfully" : "Objektiasetukset päivitettiin onnistuneesti", + "Object sources" : "Objektilähteet", + "Object unlocked successfully" : "Objektin lukitus avattiin onnistuneesti", + "Object vectorization configuration saved successfully" : "Objektien vektorointimääritys tallennettiin onnistuneesti", + "Object vectorized successfully" : "Objekti vektoroitiin onnistuneesti", + "Object:" : "Objekti:", + "Objects" : "Objektit", + "Objects Analyzed" : "Analysoidut objektit", + "Objects Distribution" : "Objektien jakauma", + "Objects being analyzed" : "Analysoitavat objektit", + "Objects by Register" : "Objektit rekisterin mukaan", + "Objects by Schema" : "Objektit skeeman mukaan", + "Objects deletion completed successfully" : "Objektien poisto valmistui onnistuneesti", + "Objects in schema" : "Objektit skeemassa", + "Objects to Process:" : "Käsiteltävät objektit:", + "Objects to be analyzed" : "Analysoitavat objektit", + "Objects to be deleted" : "Poistettavat objektit", + "Objects to be validated" : "Vahvistettavat objektit", + "Objects will be serialized as JSON text before vectorization" : "Objektit sarjallistetaan JSON-tekstiksi ennen vektorointia", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objektit poistetaan pehmeästi (merkitään poistetuiksi mutta säilytetään tietokannassa). Ne voidaan palauttaa myöhemmin tarvittaessa.", + "Old Value" : "Vanha arvo", + "Oldest Item (days)" : "Vanhin kohde (päiviä)", + "Ollama Chat Settings" : "Ollama-keskusteluasetukset", + "Ollama Embedding Configuration" : "Ollama-upotusmääritys", + "Ollama URL" : "Ollama-URL", + "Only With Changes" : "Vain muutoksin", + "Open Register Objects" : "Open Register -objektit", + "Open n8n Editor" : "Avaa n8n-editori", + "OpenAI Chat Settings" : "OpenAI-keskusteluasetukset", + "OpenAI Embedding Configuration" : "OpenAI-upotusmääritys", + "OpenRegister Settings" : "OpenRegister-asetukset", + "Optional webhook secret for signature verification" : "Valinnainen webhook-salaisuus allekirjoituksen vahvistamista varten", + "Order" : "Järjestys", + "Organisation" : "Organisaatio", + "Organisation ID" : "Organisaation tunnus", + "Organisation Members" : "Organisaation jäsenet", + "Organisation Statistics" : "Organisaation tilastot", + "Organisation created successfully" : "Organisaatio luotiin onnistuneesti", + "Organisation settings saved successfully" : "Organisaation asetukset tallennettiin onnistuneesti", + "Organisations" : "Organisaatiot", + "Organization" : "Organisaatio", + "Organization ID (Optional)" : "Organisaation tunnus (valinnainen)", + "Orphaned Items" : "Orvot kohteet", + "Owner" : "Omistaja", + "Owner:" : "Omistaja:", + "Parallel Mode (Faster, more resource intensive)" : "Rinnakkaistila (nopeampi, enemmän resursseja vaativa)", + "Parallel:" : "Rinnakkainen:", + "Password" : "Salasana", + "Password for SOLR authentication (optional)" : "Salasana SOLR-todennusta varten (valinnainen)", + "Path" : "Polku", + "Path in repository" : "Polku tietovarastossa", + "Path where the configuration file will be saved in the repository" : "Polku, johon määritystiedosto tallennetaan tietovarastossa", + "Path where the register OAS file will be saved in the repository" : "Polku, johon rekisterin OAS-tiedosto tallennetaan tietovarastossa", + "Pattern (regex)" : "Kuvio (regex)", + "Pattern Issue" : "Kuvio-ongelma", + "Pattern constraint is missing" : "Kuviorajoite puuttuu", + "Patterns:" : "Kuviot:", + "Pending" : "Odottaa", + "Permanent Delete Mode" : "Pysyvä poistotila", + "Permanently Delete" : "Poista pysyvästi", + "Permanently delete already soft-deleted objects" : "Poista pysyvästi jo pehmeästi poistetut objektit", + "Permanently delete this conversation?" : "Poistetaanko tämä keskustelu pysyvästi?", + "Person" : "Henkilö", + "Personal Data" : "Henkilötiedot", + "Phone" : "Puhelin", + "Photos" : "Valokuvat", + "Please create an agent in the" : "Luo agentti kohdassa", + "Please select an agent to continue" : "Valitse agentti jatkaaksesi", + "Please select which register and schema to use for the new object" : "Valitse, mitä rekisteriä ja skeemaa käytetään uudelle objektille", + "Please try again later." : "Yritä myöhemmin uudelleen.", + "Please wait while we fetch your agents." : "Odota, kun haemme agenttejasi.", + "Please wait while we fetch your applications." : "Odota, kun haemme sovelluksiasi.", + "Please wait while we fetch your configurations." : "Odota, kun haemme määrityksiäsi.", + "Please wait while we fetch your deleted items." : "Odota, kun haemme poistettuja kohteitasi.", + "Please wait while we fetch your sources." : "Odota, kun haemme lähteitäsi.", + "Polls" : "Kyselyt", + "Popular Search Terms" : "Suositut hakutermit", + "Port" : "Portti", + "Prerequisites" : "Edellytykset", + "Press Enter to send, Shift+Enter for new line" : "Lähetä painamalla Enter, uusi rivi painamalla Shift+Enter", + "Previous" : "Edellinen", + "Private" : "Yksityinen", + "Processes chunks in batches with simulated parallelism." : "Käsittelee lohkoja erissä simuloidulla rinnakkaisuudella.", + "Processes file chunks sequentially (safest)." : "Käsittelee tiedostolohkoja peräkkäin (turvallisin).", + "Processes objects in chunks with simulated parallelism." : "Käsittelee objekteja lohkoina simuloidulla rinnakkaisuudella.", + "Processes objects sequentially (safest)." : "Käsittelee objekteja peräkkäin (turvallisin).", + "Processing" : "Käsitellään", + "Processing Limits" : "Käsittelyrajat", + "Processing..." : "Käsitellään...", + "Project initialized successfully" : "Projekti alustettiin onnistuneesti", + "Project not found. Please initialize first." : "Projektia ei löytynyt. Alusta ensin.", + "Properties" : "Ominaisuudet", + "Property Behaviors" : "Ominaisuuden käyttäytymiset", + "Property Configuration:" : "Ominaisuuden määritys:", + "Property Filters" : "Ominaisuussuodattimet", + "Property Title" : "Ominaisuuden otsikko", + "Property Type" : "Ominaisuuden tyyppi", + "Property can be improved" : "Ominaisuutta voidaan parantaa", + "Protected" : "Suojattu", + "Provider" : "Palveluntarjoaja", + "Provider is required for testing" : "Palveluntarjoaja vaaditaan testaamista varten", + "Public" : "Julkinen", + "Public View" : "Julkinen näkymä", + "Public views can be accessed by anyone in the system" : "Julkisia näkymiä voi käyttää kuka tahansa järjestelmässä", + "Publish" : "Julkaise", + "Publish Objects" : "Julkaise objektit", + "Publish all objects in this schema" : "Julkaise kaikki tämän skeeman objektit", + "Published" : "Julkaistu", + "Published:" : "Julkaistu:", + "Publishing..." : "Julkaistaan...", + "Purge" : "Tyhjennä", + "Purge Date" : "Tyhjennyspäivä", + "Query Complexity Distribution" : "Kyselyn monimutkaisuuden jakauma", + "Query parameter is required" : "Kyselyparametri vaaditaan", + "RAG" : "RAG", + "RAG Configuration" : "RAG-määritys", + "Rate limits cleared successfully" : "Nopeusrajoitukset tyhjennettiin onnistuneesti", + "Raw Changes Data" : "Raakamuutostiedot", + "Re-vectorize on object update" : "Vektoroi uudelleen objektin päivityksen yhteydessä", + "Read" : "Lue", + "Recommendations:" : "Suositukset:", + "Recommended Type:" : "Suositeltu tyyppi:", + "Recommended: 5 sources" : "Suositus: 5 lähdettä", + "Refresh" : "Päivitä", + "Refresh Data" : "Päivitä tiedot", + "Refresh Stats" : "Päivitä tilastot", + "Refresh Workflows" : "Päivitä työnkulut", + "Refresh dashboard" : "Päivitä hallintapaneeli", + "Refresh database info" : "Päivitä tietokannan tiedot", + "Register" : "Rekisteri", + "Register ID" : "Rekisterin tunnus", + "Register Statistics" : "Rekisterin tilastot", + "Register Totals" : "Rekisterin kokonaismäärät", + "Register depublished successfully" : "Rekisterin julkaisu peruttiin onnistuneesti", + "Register not found" : "Rekisteriä ei löytynyt", + "Register objects deletion completed successfully" : "Rekisterin objektien poisto valmistui onnistuneesti", + "Register published successfully" : "Rekisteri julkaistiin onnistuneesti", + "Register/Schema Usage" : "Rekisterin/skeeman käyttö", + "Registers" : "Rekisterit", + "Reindex" : "Indeksoi uudelleen", + "Reindex completed" : "Uudelleenindeksointi valmis", + "Reindex completed successfully" : "Uudelleenindeksointi valmistui onnistuneesti", + "Reindex failed" : "Uudelleenindeksointi epäonnistui", + "Relations" : "Suhteet", + "Remote Version" : "Etäversio", + "Remove" : "Poista", + "Remove filter" : "Poista suodatin", + "Remove from favorites" : "Poista suosikeista", + "Remove group" : "Poista ryhmä", + "Removed" : "Poistettu", + "Removed from favorites" : "Poistettu suosikeista", + "Rename Conversation" : "Nimeä keskustelu uudelleen", + "Rename conversation" : "Nimeä keskustelu uudelleen", + "Replicas" : "Replikat", + "Repository" : "Tietovarasto", + "Request Body" : "Pyynnön runko", + "Request Data" : "Pyynnön tiedot", + "Request Details" : "Pyynnön tiedot", + "Request body must be JSON with a \"query\" field" : "Pyynnön rungon on oltava JSON, jossa on \"query\"-kenttä", + "Request timeout in seconds" : "Pyynnön aikakatkaisu sekunneissa", + "Required field" : "Pakollinen kenttä", + "Required status is inconsistent" : "Pakollisuustila on epäjohdonmukainen", + "Rerun Search" : "Suorita haku uudelleen", + "Reset Changes" : "Palauta muutokset", + "Reset Filters" : "Palauta suodattimet", + "Response Body" : "Vastauksen runko", + "Response Details" : "Vastauksen tiedot", + "Response Time:" : "Vastausaika:", + "Restore" : "Palauta", + "Restore conversation" : "Palauta keskustelu", + "Restore or permanently delete items" : "Palauta tai poista kohteet pysyvästi", + "Result Count Range" : "Tulosten määrän väli", + "Results" : "Tulokset", + "Retrieve all objects for this schema" : "Nouda kaikki tämän skeeman objektit", + "Retry" : "Yritä uudelleen", + "Retry Failed Extractions" : "Yritä epäonnistuneita poimintoja uudelleen", + "Retry Policy" : "Uudelleenyrityskäytäntö", + "Retry completed" : "Uudelleenyritys valmis", + "Retrying..." : "Yritetään uudelleen...", + "Risk Level" : "Riskitaso", + "Roles updated successfully" : "Roolit päivitettiin onnistuneesti", + "SOLR Connection Settings" : "SOLR-yhteysasetukset", + "SOLR Version:" : "SOLR-versio:", + "SOLR actions menu" : "SOLR-toimintojen valikko", + "SOLR base path (usually /solr)" : "SOLR-peruspolku (yleensä /solr)", + "SOLR is disabled" : "SOLR on poistettu käytöstä", + "SOLR is not available or not configured" : "SOLR ei ole käytettävissä tai sitä ei ole määritetty", + "SOLR logging disabled" : "SOLR-lokiin kirjaaminen poistettu käytöstä", + "SOLR logging enabled" : "SOLR-lokiin kirjaaminen otettu käyttöön", + "SOLR search disabled" : "SOLR-haku poistettu käytöstä", + "SOLR search enabled" : "SOLR-haku otettu käyttöön", + "SOLR server hostname or IP address" : "SOLR-palvelimen isäntänimi tai IP-osoite", + "SOLR server port number (optional, defaults to 8983)" : "SOLR-palvelimen porttinumero (valinnainen, oletus 8983)", + "SOLR setup completed successfully" : "SOLR-asennus valmistui onnistuneesti", + "SOLR setup failed" : "SOLR-asennus epäonnistui", + "SOLR setup failed - check logs" : "SOLR-asennus epäonnistui - tarkista lokit", + "Sample Values:" : "Esimerkkiarvot:", + "Save" : "Tallenna", + "Save Configuration" : "Tallenna määritys", + "Save Connection Settings" : "Tallenna yhteysasetukset", + "Save Roles" : "Tallenna roolit", + "Save Settings" : "Tallenna asetukset", + "Save current search as view" : "Tallenna nykyinen haku näkymänä", + "Saved Views" : "Tallennetut näkymät", + "Saving..." : "Tallennetaan...", + "Schema" : "Skeema", + "Schema ID" : "Skeeman tunnus", + "Schema Name" : "Skeeman nimi", + "Schema Statistics" : "Skeeman tilastot", + "Schema depublished successfully" : "Skeeman julkaisu peruttiin onnistuneesti", + "Schema objects deletion completed successfully" : "Skeeman objektien poisto valmistui onnistuneesti", + "Schema published successfully" : "Skeema julkaistiin onnistuneesti", + "Schema successfully created" : "Skeema luotiin onnistuneesti", + "Schema successfully updated" : "Skeema päivitettiin onnistuneesti", + "Schemas" : "Skeemat", + "Scheme" : "Kaava", + "Search / Views" : "Haku / Näkymät", + "Search Activity" : "Hakutoiminta", + "Search Analytics" : "Hakuanalytiikka", + "Search Entities" : "Hae entiteettejä", + "Search Files" : "Hae tiedostoja", + "Search GitHub" : "Hae GitHubista", + "Search GitLab" : "Hae GitLabista", + "Search Method" : "Hakumenetelmä", + "Search Term" : "Hakutermi", + "Search Trail Details" : "Hakuketjun tiedot", + "Search Trail Management" : "Hakuketjun hallinta", + "Search Trail Statistics" : "Hakuketjun tilastot", + "Search Trails" : "Hakuketjut", + "Search Views" : "Hae näkymiä", + "Search Webhooks" : "Hae webhookeja", + "Search by file name or path" : "Hae tiedoston nimen tai polun mukaan", + "Search by name or URL" : "Hae nimen tai URL:n mukaan", + "Search by value" : "Hae arvon mukaan", + "Search in Files" : "Hae tiedostoista", + "Search in Objects" : "Hae objekteista", + "Search property names..." : "Hae ominaisuuksien nimiä...", + "Search trail deletion not implemented yet" : "Hakuketjun poistoa ei ole vielä toteutettu", + "Search views..." : "Hae näkymiä...", + "Secret" : "Salaisuus", + "Secret key for HMAC signature generation (optional)" : "Salainen avain HMAC-allekirjoituksen luomista varten (valinnainen)", + "Select AI Agent" : "Valitse tekoälyagentti", + "Select All" : "Valitse kaikki", + "Select ConfigSet" : "Valitse ConfigSet", + "Select File Types to Vectorize:" : "Valitse vektoroitavat tiedostotyypit:", + "Select HTTP method" : "Valitse HTTP-menetelmä", + "Select Register and Schema" : "Valitse rekisteri ja skeema", + "Select Views to Vectorize:" : "Valitse vektoroitavat näkymät:", + "Select a Nextcloud group to add" : "Valitse lisättävä Nextcloud-ryhmä", + "Select a branch" : "Valitse haara", + "Select a model or type a custom model name" : "Valitse malli tai kirjoita mukautettu mallin nimi", + "Select a repository" : "Valitse tietovarasto", + "Select a repository you have write access to" : "Valitse tietovarasto, johon sinulla on kirjoitusoikeus", + "Select an AI Agent" : "Valitse tekoälyagentti", + "Select an AI agent to begin chatting with your data." : "Valitse tekoälyagentti aloittaaksesi keskustelun tietojesi kanssa.", + "Select backend" : "Valitse taustajärjestelmä", + "Select chat model" : "Valitse keskustelumalli", + "Select collection for files" : "Valitse kokoelma tiedostoille", + "Select collection for objects" : "Valitse kokoelma objekteille", + "Select default organisation" : "Valitse oletusorganisaatio", + "Select event to listen to..." : "Valitse kuunneltava tapahtuma...", + "Select model" : "Valitse malli", + "Select options..." : "Valitse vaihtoehtoja...", + "Select period" : "Valitse jakso", + "Select property to send as payload" : "Valitse hyötykuormana lähetettävä ominaisuus", + "Select provider" : "Valitse palveluntarjoaja", + "Select registers and schemas to save a view" : "Valitse rekisterit ja skeemat näkymän tallentamiseksi", + "Select retry policy" : "Valitse uudelleenyrityskäytäntö", + "Select the branch to publish to" : "Valitse haara, johon julkaistaan", + "Select the event this webhook should listen to" : "Valitse tapahtuma, jota tämän webhookin tulisi kuunnella", + "Select views to vectorize:" : "Valitse vektoroitavat näkymät:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Valitse, mitkä Nextcloud-ryhmät ovat käytettävissä tälle organisaatiolle. Näiden ryhmien käyttäjillä on pääsy organisaation resursseihin.", + "Select which data views the AI can search" : "Valitse, mitä tietonäkymiä tekoäly voi hakea", + "Select which property from the event should be used as the webhook payload data" : "Valitse, mitä tapahtuman ominaisuutta käytetään webhookin hyötykuormatietona", + "Select which tools the AI can use to perform actions" : "Valitse, mitä työkaluja tekoäly voi käyttää toimintojen suorittamiseen", + "Selected" : "Valittu", + "Selected Groups" : "Valitut ryhmät", + "Selected audit trails deleted successfully" : "Valitut valvontaketjut poistettiin onnistuneesti", + "Selected search trails deleted successfully" : "Valitut hakuketjut poistettiin onnistuneesti", + "Selected users can access this view" : "Valitut käyttäjät voivat käyttää tätä näkymää", + "Send additional feedback" : "Lähetä lisäpalautetta", + "Send as CloudEvent" : "Lähetä CloudEvent-muodossa", + "Sensitive PII" : "Arkaluonteiset henkilötiedot", + "Serial Mode (Safer, slower)" : "Sarjatila (turvallisempi, hitaampi)", + "Serial:" : "Sarja:", + "Server Configuration" : "Palvelimen määritys", + "Server Information" : "Palvelimen tiedot", + "Settings" : "Asetukset", + "Shards" : "Sirpaleet", + "Share" : "Jaa", + "Share with Groups" : "Jaa ryhmien kanssa", + "Share with Users" : "Jaa käyttäjien kanssa", + "Show Filters" : "Näytä suodattimet", + "Show less" : "Näytä vähemmän", + "Show only entries with changes" : "Näytä vain merkinnät, joissa on muutoksia", + "Simple" : "Yksinkertainen", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Yksinkertaiset kyselyt: perustekstihaut minimaalisilla parametreilla (esim. yksi hakutermi, ei lisäsuodattimia)", + "Size" : "Koko", + "Slug" : "Slug", + "Soft Delete Mode" : "Pehmeä poistotila", + "Soft Deleted Items" : "Pehmeästi poistetut kohteet", + "SolrCloud mode enabled" : "SolrCloud-tila otettu käyttöön", + "Source" : "Lähde", + "Sources" : "Lähteet", + "Sources:" : "Lähteet:", + "Standalone SOLR mode" : "Itsenäinen SOLR-tila", + "Start Conversation" : "Aloita keskustelu", + "Start Vectorization" : "Aloita vektorointi", + "Start a conversation" : "Aloita keskustelu", + "Starting names cache warmup..." : "Aloitetaan nimien välimuistin lämmitystä...", + "Starting..." : "Aloitetaan...", + "Statistics" : "Tilastot", + "Stats calculation not yet implemented" : "Tilastojen laskentaa ei ole vielä toteutettu", + "Status" : "Tila", + "Status Code" : "Tilakoodi", + "String Constraints" : "Merkkijonorajoitteet", + "Success" : "Onnistui", + "Success Rate" : "Onnistumisaste", + "Success Status" : "Onnistumistila", + "Successful" : "Onnistunut", + "Successfully joined organisation" : "Organisaatioon liityttiin onnistuneesti", + "Sync Table" : "Synkronoi taulu", + "Synchronization Settings" : "Synkronointiasetukset", + "System Default" : "Järjestelmän oletus", + "Technical Description" : "Tekninen kuvaus", + "Technical description for developers and administrators" : "Tekninen kuvaus kehittäjille ja ylläpitäjille", + "Temperature" : "Lämpötila", + "Templates" : "Mallit", + "Test" : "Testaa", + "Test Chat" : "Testaa keskustelua", + "Test Connection" : "Testaa yhteys", + "Test Embedding" : "Testaa upotus", + "Test endpoint executed successfully" : "Testipäätepiste suoritettiin onnistuneesti", + "Test webhook delivered successfully" : "Testiwebhook toimitettiin onnistuneesti", + "Test webhook delivery failed" : "Testiwebhookin toimitus epäonnistui", + "Test webhook sent successfully" : "Testiwebhook lähetettiin onnistuneesti", + "Testing..." : "Testataan...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Tekstilohkot luodaan tiedostojen poiminnan aikana ja tallennetaan tietokantaan. Vektorointi lukee nämä esilohkotetut tiedostot ja muuntaa ne upotuksiksi.", + "Text extracted successfully" : "Teksti poimittiin onnistuneesti", + "Text extraction disabled" : "Tekstin poiminta poistettu käytöstä", + "The URL where webhook events will be sent" : "URL, johon webhook-tapahtumat lähetetään", + "The requested conversation does not exist" : "Pyydettyä keskustelua ei ole olemassa", + "The saved view and all its search configuration" : "Tallennettu näkymä ja kaikki sen hakumääritykset", + "There are no audit trail entries matching your current filters." : "Nykyisiä suodattimiasi vastaavia valvontaketjumerkintöjä ei ole.", + "There are no deleted items matching your current filters." : "Nykyisiä suodattimiasi vastaavia poistettuja kohteita ei ole.", + "There are no search trail entries matching your current filters." : "Nykyisiä suodattimiasi vastaavia hakuketjumerkintöjä ei ole.", + "There are no webhook log entries matching your filters." : "Suodattimiasi vastaavia webhook-lokimerkintöjä ei ole.", + "This action cannot be undone." : "Tätä toimintoa ei voi peruuttaa.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Tätä toimintoa ei voi peruuttaa. Varmista, ettei mikään kokoelma käytä tätä ConfigSet-asetusta.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Tällä agentilla ei ole määritettäviä näkymiä tai työkaluja, mutta voit silti säätää RAG-asetuksia alta.", + "This analysis may take some time" : "Tämä analyysi voi kestää jonkin aikaa", + "This audit trail entry does not contain any change information." : "Tämä valvontaketjumerkintä ei sisällä muutostietoja.", + "This configuration has no data defined." : "Tälle määritykselle ei ole määritetty tietoja.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Tämä päätepiste on vanhentunut. Käytä sen sijaan lohkopohjaisia päätepisteitä.", + "This entity has no relations to objects or files" : "Tällä entiteetillä ei ole suhteita objekteihin tai tiedostoihin", + "This is a test webhook from OpenRegister" : "Tämä on testiwebhook OpenRegisteristä", + "This schema must use Magic Table configuration to sync" : "Tämän skeeman on käytettävä Magic Table -määritystä synkronointia varten", + "This source has no associated registers." : "Tällä lähteellä ei ole liittyviä rekistereitä.", + "This will copy the _default ConfigSet with the new name" : "Tämä kopioi _default-ConfigSet-asetuksen uudella nimellä", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Tämä poistaa pysyvästi KAIKKI upotukset (vektorit) tietokannasta. Sinun on vektoroitava kaikki objektit ja tiedostot uudelleen. Tätä toimintoa ei voi peruuttaa.\\n\\nHaluatko varmasti jatkaa?", + "This will permanently delete:" : "Tämä poistaa pysyvästi:", + "Timeout (seconds)" : "Aikakatkaisu (sekuntia)", + "Timestamp" : "Aikaleima", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Vinkki: Ota käyttöön vain ne näkymät, jotka tarvitsevat semanttista hakua, minimoidaksesi upotuskustannukset. Yksinkertaiset hakutaulut tarvitsevat harvoin vektorointia.", + "Title" : "Otsikko", + "To Date" : "Loppupäivä", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Säätääksesi lohkokokoa tai -strategiaa, siirry kohtaan Tiedostomääritys → Käsittelyrajat.", + "To change the embedding provider or model, go to LLM Configuration." : "Vaihtaaksesi upotuspalveluntarjoajaa tai -mallia, siirry kohtaan LLM-määritys.", + "To date" : "Loppupäivä", + "Toggle search sidebar" : "Vaihda hakusivupalkkia", + "Tools" : "Työkalut", + "Top Deleters" : "Eniten poistaneet", + "Total" : "Yhteensä", + "Total Audit Trails" : "Valvontaketjuja yhteensä", + "Total Chunks Available:" : "Käytettävissä olevia lohkoja yhteensä:", + "Total Deleted Items" : "Poistettuja kohteita yhteensä", + "Total Members" : "Jäseniä yhteensä", + "Total Objects" : "Objekteja yhteensä", + "Total Objects in Database:" : "Objekteja tietokannassa yhteensä:", + "Total Organisations" : "Organisaatioita yhteensä", + "Total Results" : "Tuloksia yhteensä", + "Total Searches" : "Hakuja yhteensä", + "Total Size" : "Kokonaiskoko", + "Total Storage" : "Kokonaistallennustila", + "Total:" : "Yhteensä:", + "Totals" : "Kokonaismäärät", + "Type" : "Tyyppi", + "Type Issue" : "Tyyppiongelma", + "Type Variations:" : "Tyyppivaihtelut:", + "Type must be \"positive\" or \"negative\"" : "Tyypin on oltava \"positive\" tai \"negative\"", + "Type to search for groups" : "Kirjoita hakeaksesi ryhmiä", + "Type to search for users" : "Kirjoita hakeaksesi käyttäjiä", + "Type to search groups..." : "Kirjoita hakeaksesi ryhmiä...", + "Type to search users..." : "Kirjoita hakeaksesi käyttäjiä...", + "URL" : "URL", + "URL where Ollama is running" : "URL, jossa Ollama on käynnissä", + "UUID:" : "UUID:", + "Unchanged" : "Muuttumaton", + "Unique Search Terms" : "Yksilölliset hakutermit", + "Unique Terms" : "Yksilölliset termit", + "Unknown" : "Tuntematon", + "Unknown Register" : "Tuntematon rekisteri", + "Unknown Schema" : "Tuntematon skeema", + "Untitled View" : "Nimetön näkymä", + "Update" : "Päivitä", + "Update Operations" : "Päivitystoiminnot", + "Update configuration: ..." : "Päivitä määritys: ...", + "Update register OAS: ..." : "Päivitä rekisterin OAS: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Päivitä vektorit, kun objektin tiedot muuttuvat (suositellaan tarkkaa hakua varten)", + "Updated" : "Päivitetty", + "Updated At" : "Päivitetty", + "Updated:" : "Päivitetty:", + "Uptime:" : "Käyttöaika:", + "Usage Count" : "Käyttömäärä", + "Use SolrCloud with Zookeeper for distributed search" : "Käytä SolrCloudia Zookeeperin kanssa hajautettua hakua varten", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Käytä suodattimia rajataksesi valvontaketjumerkintöjä rekisterin, skeeman, toimintotyypin, käyttäjän, päivämäärävälin tai objektin tunnuksen mukaan.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Käytä suodattimia rajataksesi poistettuja kohteita rekisterin, skeeman, poistopäivän tai poistaneen käyttäjän mukaan.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Käytä suodattimia rajataksesi hakuketjumerkintöjä rekisterin, skeeman, onnistumistilan, käyttäjän, päivämäärävälin, hakutermien tai suorituskykymittareiden mukaan.", + "Used By Collections" : "Kokoelmien käytössä", + "User" : "Käyttäjä", + "User Agent Statistics" : "Käyttäjäagenttitilastot", + "User Description" : "Käyttäjäkuvaus", + "User rate limits cleared successfully" : "Käyttäjän nopeusrajoitukset tyhjennettiin onnistuneesti", + "User removed successfully" : "Käyttäjä poistettiin onnistuneesti", + "User-friendly description shown in forms and help text" : "Käyttäjäystävällinen kuvaus, joka näytetään lomakkeissa ja ohjeteksteissä", + "Username" : "Käyttäjänimi", + "Username for SOLR authentication (optional)" : "Käyttäjänimi SOLR-todennusta varten (valinnainen)", + "Users" : "Käyttäjät", + "Using Pre-Generated Chunks" : "Käytetään esiluotuja lohkoja", + "Validate" : "Vahvista", + "Validate Objects" : "Vahvista objektit", + "Validation completed successfully" : "Vahvistus valmistui onnistuneesti", + "Validation failed" : "Vahvistus epäonnistui", + "Value" : "Arvo", + "Vector Dimensions" : "Vektorin ulottuvuudet", + "Vector Search Backend" : "Vektorihaun taustajärjestelmä", + "Vector field: _embedding_" : "Vektorikenttä: _embedding_", + "Vectorization Triggers" : "Vektoroinnin laukaisimet", + "Vectorize All Files" : "Vektoroi kaikki tiedostot", + "Vectorize All Objects" : "Vektoroi kaikki objektit", + "Vectorize all file types" : "Vektoroi kaikki tiedostotyypit", + "Vectorize all views" : "Vektoroi kaikki näkymät", + "Vectorize on object creation" : "Vektoroi objektin luonnin yhteydessä", + "Vectors will be stored in your existing object and file collections" : "Vektorit tallennetaan olemassa oleviin objekti- ja tiedostokokoelmiisi", + "Version" : "Versio", + "Very High" : "Erittäin korkea", + "View API Docs" : "Näytä API-dokumentaatio", + "View Changes" : "Näytä muutokset", + "View Details" : "Näytä tiedot", + "View Error" : "Näytä virhe", + "View File" : "Näytä tiedosto", + "View Full Details" : "Näytä kaikki tiedot", + "View Logs" : "Näytä lokit", + "View Name" : "Näkymän nimi", + "View Object" : "Näytä objekti", + "View Parameters" : "Näytä parametrit", + "View Selection" : "Näkymän valinta", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Tarkastele ja analysoi hakuketjulokeja edistyneillä suodatus- ja analytiikkaominaisuuksilla", + "View and analyze system audit trails with advanced filtering capabilities" : "Tarkastele ja analysoi järjestelmän valvontaketjuja edistyneillä suodatusominaisuuksilla", + "View deleted successfully" : "Näkymä poistettiin onnistuneesti", + "View entity details and manage relations" : "Tarkastele entiteetin tietoja ja hallitse suhteita", + "View name" : "Näkymän nimi", + "View name is required" : "Näkymän nimi vaaditaan", + "View saved successfully!" : "Näkymä tallennettiin onnistuneesti!", + "View search analytics and manage search logs" : "Tarkastele hakuanalytiikkaa ja hallitse hakulokeja", + "View successfully deleted" : "Näkymä poistettiin onnistuneesti", + "View successfully updated" : "Näkymä päivitettiin onnistuneesti", + "View updated successfully!" : "Näkymä päivitettiin onnistuneesti!", + "View webhook delivery logs and filter by webhook" : "Tarkastele webhookien toimituslokeja ja suodata webhookin mukaan", + "Views" : "Näkymät", + "Visible to users" : "Näkyvissä käyttäjille", + "Wait for Response" : "Odota vastausta", + "Wait for webhook response before continuing (required for request/response flows)" : "Odota webhookin vastausta ennen jatkamista (vaaditaan pyyntö-/vastausvuoissa)", + "Warmup Names Cache" : "Lämmitä nimien välimuisti", + "Warning" : "Varoitus", + "Warning:" : "Varoitus:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Webhook-lokin tiedot", + "Webhook Logs" : "Webhook-lokit", + "Webhook created successfully" : "Webhook luotiin onnistuneesti", + "Webhook deleted" : "Webhook poistettu", + "Webhook retry delivered successfully" : "Webhookin uudelleenyritys toimitettiin onnistuneesti", + "Webhook retry delivery failed" : "Webhookin uudelleenyrityksen toimitus epäonnistui", + "Webhook updated" : "Webhook päivitetty", + "Webhook updated successfully" : "Webhook päivitettiin onnistuneesti", + "Webhooks" : "Webhookit", + "Weekly" : "Viikoittain", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Kääri webhookin hyötykuorma CloudEvents-muotoon paremman yhteentoimivuuden vuoksi", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Kyllä", + "You" : "Sinä", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Voit luoda uusia ConfigSet-asetuksia _default-mallin pohjalta tai ladata mukautettuja suoraan SOLR-palvelimellesi.", + "You do not have access to this conversation" : "Sinulla ei ole pääsyä tähän keskusteluun", + "You do not have permission to delete this conversation" : "Sinulla ei ole oikeutta poistaa tätä keskustelua", + "You do not have permission to modify this conversation" : "Sinulla ei ole oikeutta muokata tätä keskustelua", + "You do not have permission to restore this conversation" : "Sinulla ei ole oikeutta palauttaa tätä keskustelua", + "You must be logged in to favorite views" : "Sinun on oltava kirjautuneena lisätäksesi näkymiä suosikkeihin", + "You need an AI agent to start a conversation." : "Tarvitset tekoälyagentin aloittaaksesi keskustelun.", + "Your Fireworks AI API key. Get one at" : "Fireworks AI -API-avaimesi. Hanki sellainen osoitteesta", + "Your OpenAI API key. Get one at" : "OpenAI-API-avaimesi. Hanki sellainen osoitteesta", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Palautteesi on tallennettu. Voit halutessasi antaa lisätietoja tähän...", + "Zookeeper Hosts" : "Zookeeper-isännät", + "Zookeeper Port" : "Zookeeper-portti", + "Zookeeper Settings (SolrCloud)" : "Zookeeper-asetukset (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper-yhteysmerkkijono SolrCloudia varten", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper-porttinumero (valinnainen, oletus 2181)", + "chunk" : "lohko", + "chunks" : "lohkoa", + "chunks processed" : "lohkoa käsitelty", + "configuration(s)" : "määritys(tä)", + "e.g., lib/Settings/config.json" : "esim. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "esim. lib/Settings/register.json", + "failed" : "epäonnistui", + "file" : "tiedosto", + "files" : "tiedostoa", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "jäsentä", + "menu or contact someone with permission to create agents." : "-valikossa tai ota yhteyttä henkilöön, jolla on oikeus luoda agentteja.", + "messages" : "viestiä", + "n8n URL and API key are required" : "n8n-URL ja API-avain vaaditaan", + "n8n configuration saved successfully" : "n8n-määritys tallennettiin onnistuneesti", + "n8n connection not configured" : "n8n-yhteyttä ei ole määritetty", + "n8n connection successful" : "n8n-yhteys onnistui", + "n8n connection test successful" : "n8n-yhteystesti onnistui", + "n8n integration disabled" : "n8n-integraatio poistettu käytöstä", + "n8n integration enabled" : "n8n-integraatio otettu käyttöön", + "n8n project initialized successfully" : "n8n-projekti alustettiin onnistuneesti", + "n8n settings saved successfully" : "n8n-asetukset tallennettiin onnistuneesti", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objektia", + "objects processed" : "objektia käsitelty", + "or pick a range" : "tai valitse väli", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "tarjoaa hyvän tasapainon nopeuden ja tarkkuuden välillä.", + "register(s)" : "rekisteri(ä)", + "register(s) selected" : "rekisteri(ä) valittu", + "required" : "pakollinen", + "results" : "tulosta", + "schema(s)" : "skeema(a)", + "schema(s) selected" : "skeema(a) valittu", + "sk-..." : "sk-...", + "this application" : "tämä sovellus", + "to" : "-", + "vectorized" : "vektoroitu", + "views selected" : "näkymää valittu", + "ℹ️ Current Configuration" : "ℹ️ Nykyinen määritys", + "⚡ Batch Processing" : "⚡ Erätoiminen käsittely", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Kattava konteksti ✓ Epätodennäköisempää jättää yksityiskohtia huomiotta ✗ Hitaammat vastaukset ✗ Voi sisältää vähemmän olennaista tietoa", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Nopeammat vastaukset ✓ Tarkemmin kohdistetut vastaukset ✗ Voi jättää olennaista tietoa huomiotta", + "✨ AI Features" : "✨ Tekoälyominaisuudet", + "All search trails cleared successfully" : "Kaikki hakuketjut tyhjennettiin onnistuneesti", + "Multiple search trail deletion not implemented yet" : "Useiden hakuketjujen poistoa ei ole vielä toteutettu", + "No expired search trails found to clear" : "Vanhentuneita hakuketjuja ei löytynyt tyhjennettäväksi", + "Missing message" : "Puuttuva viesti", + "message content is required" : "viestin sisältö vaaditaan", + "Missing conversation or agentUuid" : "Puuttuva keskustelu tai agentUuid", + "Access denied" : "Pääsy evätty", + "Conversation not found" : "Keskustelua ei löytynyt", + "AI service not configured" : "Tekoälypalvelua ei ole määritetty", + "Failed to process message" : "Viestin käsittely epäonnistui", + "Missing conversationId" : "Puuttuva conversationId", + "conversationId is required" : "conversationId vaaditaan", + "Failed to fetch conversation history" : "Keskusteluhistorian hakeminen epäonnistui", + "Failed to clear conversation" : "Keskustelun tyhjentäminen epäonnistui", + "Invalid feedback type" : "Virheellinen palautetyyppi", + "type must be \"positive\" or \"negative\"" : "tyypin on oltava \"positive\" tai \"negative\"", + "Message not found" : "Viestiä ei löytynyt", + "Failed to save feedback" : "Palautteen tallentaminen epäonnistui", + "Failed to get chat statistics" : "Keskustelutilastojen hakeminen epäonnistui", + "Not Found" : "Ei löytynyt", + "Engine not found" : "Moottoria ei löytynyt", + "Consumer not found" : "Kuluttajaa ei löytynyt", + "Backend parameter is required" : "Taustajärjestelmäparametri vaaditaan", + "Failed to get database information: %s" : "Tietokannan tietojen hakeminen epäonnistui: %s", + "SOLR setup error: %s" : "SOLR-asennusvirhe: %s", + "Reindex failed: %s" : "Uudelleenindeksointi epäonnistui: %s", + "Anonymized" : "Anonymisoitu", + "Entities detected" : "Entiteettejä havaittu", + "Extract Now" : "Poimi nyt", + "Extracted at" : "Poimittu", + "Extraction" : "Poiminta", + "Failed to load extraction data" : "Poimintatietojen lataaminen epäonnistui", + "Failed to load register data" : "Rekisteritietojen lataaminen epäonnistui", + "Failed to retrieve extraction status." : "Poiminnan tilan hakeminen epäonnistui.", + "Failed to retrieve objects for file." : "Tiedoston objektien hakeminen epäonnistui.", + "No extraction data available for this file" : "Tälle tiedostolle ei ole saatavilla poimintatietoja", + "No register objects reference this file" : "Mikään rekisteriobjekti ei viittaa tähän tiedostoon", + "Not extracted" : "Ei poimittu", + "Register Objects" : "Rekisteriobjektit", + "Risk level" : "Riskitaso", + "Text chunks" : "Tekstilohkot", + "Unknown error" : "Tuntematon virhe", + "Very high" : "Erittäin korkea", + "{title} in {register} / {schema}" : "{title} kohteessa {register} / {schema}", + "Push Notifications" : "Push-ilmoitukset", + "Real-time push notification status via notify_push" : "Reaaliaikaisten push-ilmoitusten tila notify_push-sovelluksen kautta", + "Realtime push not available — the notify_push app is not installed" : "Reaaliaikainen push ei ole käytettävissä — notify_push-sovellusta ei ole asennettu", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Asenna notify_push-sovellus Nextcloud-sovelluskaupasta ottaaksesi reaaliaikaiset päivitykset käyttöön.", + "Open Nextcloud App Store" : "Avaa Nextcloud-sovelluskauppa", + "notify_push is installed but not yet active" : "notify_push on asennettu mutta ei vielä aktiivinen", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push on asennettu, mutta OpenRegister ei ole vielä vahvistanut onnistunutta pushia. Aktivoi se laukaisemalla objektin tallennus tai tarkista notify_push-määrityksesi.", + "notify_push configuration guide" : "notify_push-määritysopas", + "Realtime push active" : "Reaaliaikainen push aktiivinen", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Reaaliaikaiset push-ilmoitukset ovat aktiivisia. Yhdistetyt asiakkaat saavat välittömiä päivityksiä, kun objekteja luodaan, päivitetään tai poistetaan.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/fi.json b/l10n/fi.json new file mode 100644 index 0000000000..8280b91823 --- /dev/null +++ b/l10n/fi.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin vaaditaan)", + "(no subject)": "(ei aihetta)", + "(no title)": "(ei otsikkoa)", + "(unnamed)": "(nimetön)", + "(untitled card)": "(nimetön kortti)", + "({count} selected)": "({count} valittu)", + ", {failed} failed": ", {failed} epäonnistui", + "0 = unlimited": "0 = rajoittamaton", + "A deactivation request is pending.": "Deaktivointipyyntö on odottamassa.", + "AI Assistant": "Tekoälyavustaja", + "AI Chat": "Tekoälykeskustelu", + "AI service not configured": "Tekoälypalvelua ei ole määritetty", + "API Key": "API-avain", + "API Request Quota (requests/day)": "API-pyyntökiintiö (pyyntöä/päivä)", + "API Token Configuration": "API-tunnuksen määritys", + "API Tokens": "API-tunnukset", + "API tokens saved successfully": "API-tunnukset tallennettiin onnistuneesti", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: tämä raportti yhdistää jokaisen käsittelytoiminnan siihen liitettyihin elinkaaren aikaisiin tarkastuspolkulukuihin. Tarkastajat ja Autoriteit Persoonsgegevens käyttävät tätä valvonta-arviointiin.", + "About ConfigSets": "Tietoja ConfigSet-joukoista", + "Access denied": "Pääsy estetty", + "Access organisation dashboard and analytics": "Käytä organisaation koontinäyttöä ja analytiikkaa", + "Account": "Tili", + "Action": "Toiminto", + "Action Distribution": "Toimintojen jakautuminen", + "Action:": "Toiminto:", + "Actions": "Toiminnot", + "Active": "Aktiivinen", + "Active Collections": "Aktiiviset kokoelmat", + "Active Filters": "Aktiiviset suodattimet", + "Active Organisation:": "Aktiivinen organisaatio:", + "Active Organisations": "Aktiiviset organisaatiot", + "Active filters:": "Aktiiviset suodattimet:", + "Active organisation changed successfully": "Aktiivinen organisaatio vaihdettiin onnistuneesti", + "Active organisation set successfully": "Aktiivinen organisaatio asetettiin onnistuneesti", + "Activities": "Toiminnot", + "Activity": "Toiminta", + "Activity Period": "Toiminta-aika", + "Add": "Lisää", + "Add Attachment": "Lisää liite", + "Add Endpoint": "Lisää päätepiste", + "Add Groups": "Lisää ryhmiä", + "Add Property to '{title}'": "Lisää ominaisuus kohteeseen '{title}'", + "Add Register": "Lisää rekisteri", + "Add Schema": "Lisää skeema", + "Add Source": "Lisää lähde", + "Add User": "Lisää käyttäjä", + "Add User to Organisation": "Lisää käyttäjä organisaatioon", + "Add a contact from any of your address books to associate it with this object.": "Lisää yhteystieto mistä tahansa osoitekirjastasi liittääksesi sen tähän objektiin.", + "Add a file or files": "Lisää tiedosto tai tiedostoja", + "Add card": "Lisää kortti", + "Add contact": "Lisää yhteystieto", + "Add endpoint": "Lisää päätepiste", + "Add oneOf entry": "Lisää oneOf-merkintä", + "Add range": "Lisää väli", + "Add schema titles, descriptions, and register information to provide richer context for search": "Lisää skeeman otsikoita, kuvauksia ja rekisteritietoja tarjotaksesi rikkaamman kontekstin haulle", + "Add to favorites": "Lisää suosikkeihin", + "Added": "Lisätty", + "Added to favorites": "Lisätty suosikkeihin", + "Additional Information": "Lisätiedot", + "Additional feedback saved. Thank you!": "Lisäpalaute tallennettiin. Kiitos!", + "Addresses": "Osoitteet", + "Admin": "Ylläpitäjä", + "Advanced": "Lisäasetukset", + "Advanced Filters": "Lisäsuodattimet", + "Advanced Options": "Lisäasetukset", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Lisäsuodattimet eivät ole käytettävissä, kun käytetään tietokantalähdettä. Vaihda Auto- tai SOLR-indeksiin suodatusvaihtoehtoja varten.", + "After": "Jälkeen", + "Agent Type": "Agenttityyppi", + "Agent deleted successfully": "Agentti poistettiin onnistuneesti", + "Agent successfully created": "Agentti luotiin onnistuneesti", + "Agent successfully updated": "Agentti päivitettiin onnistuneesti", + "Agents": "Agentit", + "Aggregated across schemas": "Koottu skeemojen yli", + "All": "Kaikki", + "All Categories": "Kaikki kategoriat", + "All Collections": "Kaikki kokoelmat", + "All Confidence Levels": "Kaikki luottamustasot", + "All ConfigSets": "Kaikki ConfigSet-joukot", + "All Files": "Kaikki tiedostot", + "All Levels": "Kaikki tasot", + "All Types": "Kaikki tyypit", + "All Webhooks": "Kaikki webhookit", + "All actions": "Kaikki toiminnot", + "All audit trails cleared successfully": "Kaikki tarkastuspolut tyhjennettiin onnistuneesti", + "All clear": "Kaikki kunnossa", + "All file types": "Kaikki tiedostotyypit", + "All registers": "Kaikki rekisterit", + "All schemas": "Kaikki skeemat", + "All search trails cleared successfully": "Kaikki hakupolut tyhjennettiin onnistuneesti", + "All searches": "Kaikki haut", + "All users": "Kaikki käyttäjät", + "All-day events": "Koko päivän tapahtumat", + "Allowed MIME Types": "Sallitut MIME-tyypit", + "Already linked": "Jo linkitetty", + "An error occurred while clearing audit trails": "Tarkastuspolkuja tyhjennettäessä tapahtui virhe", + "An error occurred while deleting the audit trail": "Tarkastuspolkua poistettaessa tapahtui virhe", + "An error occurred while deleting the object": "Objektia poistettaessa tapahtui virhe", + "An error occurred while deleting the view": "Näkymää poistettaessa tapahtui virhe", + "An error occurred while permanently deleting the objects": "Objekteja pysyvästi poistettaessa tapahtui virhe", + "An error occurred while restoring the objects": "Objekteja palautettaessa tapahtui virhe", + "An error occurred while saving the organisation": "Organisaatiota tallennettaessa tapahtui virhe", + "Analysis completed:": "Analyysi valmis:", + "Analysis steps:": "Analyysin vaiheet:", + "Analytics": "Analytiikka", + "Analyze Objects": "Analysoi objektit", + "Analyze Properties": "Analysoi ominaisuudet", + "Analyze existing properties for improvement opportunities": "Analysoi olemassa olevia ominaisuuksia parannusmahdollisuuksien löytämiseksi", + "Analyzing...": "Analysoidaan...", + "Anonymized": "Anonymisoitu", + "Anonymous Group": "Anonyymi ryhmä", + "Any favorites and sharing settings for this view": "Kaikki tämän näkymän suosikki- ja jakoasetukset", + "Any user": "Kuka tahansa käyttäjä", + "App ID": "Sovellustunnus", + "App store cache invalidated successfully": "Sovelluskaupan välimuisti mitätöitiin onnistuneesti", + "App store cache invalidated: {invalidated}": "Sovelluskaupan välimuisti mitätöitiin: {invalidated}", + "Application deleted successfully": "Sovellus poistettiin onnistuneesti", + "Application identifier for this configuration (optional)": "Sovellustunniste tälle määritykselle (valinnainen)", + "Application settings": "Sovellusasetukset", + "Application successfully created": "Sovellus luotiin onnistuneesti", + "Application successfully updated": "Sovellus päivitettiin onnistuneesti", + "Applications": "Sovellukset", + "Apply": "Käytä", + "Apply Changes": "Käytä muutoksia", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Käytä roolia kaikkiin tämän rekisterin skeemoihin, joilla ei ole nimenomaisia valtuutusohituksia.", + "Approval Progress": "Hyväksynnän edistyminen", + "Approve": "Hyväksy", + "Approved": "Hyväksytty", + "Archive": "Arkistoi", + "Archive conversation": "Arkistoi keskustelu", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arkistoidaanko tämä verwerkingsactiviteit? Tarkastuspolun rivit viittaavat siihen edelleen.", + "Archived conversations are hidden from your active list": "Arkistoidut keskustelut piilotetaan aktiivisesta luettelostasi", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Haluatko varmasti POISTAA kokoelman \"{name}\"?\n\nTämä:\n• Poistaa pysyvästi kokoelman ja kaikki sen tiedot\n• Poistaa kaikki indeksoidut asiakirjat\n• Tätä toimintoa ei voi peruuttaa", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Haluatko varmasti siivota vanhat hakupolut? Tämä poistaa yli 30 päivää vanhat merkinnät.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Haluatko varmasti tyhjentää kaikki tiedot kokoelmasta \"{name}\"?\n\nTämä:\n• Poistaa kaikki indeksoidut asiakirjat\n• Pitää kokoelman rakenteen ennallaan\n• Tätä toimintoa ei voi peruuttaa", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Haluatko varmasti poistaa sovelluksen \"{name}\"? Tätä toimintoa ei voi peruuttaa.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Haluatko varmasti poistaa valitut tarkastuspolut? Tätä toimintoa ei voi peruuttaa.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Haluatko varmasti poistaa valitut hakupolut? Tätä toimintoa ei voi peruuttaa.", + "Are you sure you want to delete this ConfigSet?": "Haluatko varmasti poistaa tämän ConfigSet-joukon?", + "Are you sure you want to delete this configset?": "Haluatko varmasti poistaa tämän configset-joukon?", + "Are you sure you want to leave '{name}'?": "Haluatko varmasti poistua kohteesta '{name}'?", + "Are you sure you want to permanently delete": "Haluatko varmasti poistaa pysyvästi", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Haluatko varmasti indeksoida kokoelman \"{name}\" uudelleen?\n\nTämä:\n• Rakentaa indeksin uudelleen kaikkien objektien kanssa\n• Kestää useita minuutteja\n• Voi vaikuttaa hakusuorituskykyyn uudelleenindeksoinnin aikana", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Haluatko varmasti poistaa skeeman \"{schema}\" rekisteristä \"{register}\"? Tätä toimintoa ei voi peruuttaa.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Haluatko varmasti pehmeästi poistaa {count} aktiivista objektia skeemalle \"{schema}\"?\n\nNe merkitään poistetuiksi, mutta ne voidaan poistaa pysyvästi myöhemmin.", + "Array Object Configuration:": "Taulukko-objektin määritys:", + "Articles": "Artikkelit", + "Ask a question...": "Esitä kysymys...", + "Ask questions about your data using natural language": "Esitä kysymyksiä tiedoistasi luonnollisella kielellä", + "Assigned collaborative tags": "Määritetyt yhteistyötunnisteet", + "Assignment notifications": "Määritysilmoitukset", + "Attachment added to {name}": "Liite lisättiin kohteeseen {name}", + "Attempt": "Yritys", + "Audit Trail Changes": "Tarkastuspolun muutokset", + "Audit Trail Details": "Tarkastuspolun tiedot", + "Audit Trail Management": "Tarkastuspolun hallinta", + "Audit Trail Statistics": "Tarkastuspolun tilastot", + "Audit Trails": "Tarkastuspolut", + "Audit trail data copied to clipboard": "Tarkastuspolun tiedot kopioitiin leikepöydälle", + "Audit trail deleted successfully": "Tarkastuspolku poistettiin onnistuneesti", + "Audit trail successfully deleted": "Tarkastuspolku poistettiin onnistuneesti", + "Authentication": "Todennus", + "Auto (e.g. Y for year, F Y for month)": "Auto (esim. Y vuodelle, F Y kuukaudelle)", + "Auto share": "Automaattinen jako", + "Auto-commit disabled": "Automaattinen vahvistus poistettu käytöstä", + "Auto-commit enabled": "Automaattinen vahvistus käytössä", + "Auto-create Default Organisation": "Luo oletusorganisaatio automaattisesti", + "Auto-retry failed vectorizations": "Yritä epäonnistuneita vektoreita automaattisesti uudelleen", + "Automatically commit changes to SOLR index": "Vahvista muutokset automaattisesti SOLR-indeksiin", + "Automatically connect to the federation network.": "Yhdistä automaattisesti federaatioverkkoon.", + "Automatically create a default organisation if none exists when the app is initialized": "Luo automaattisesti oletusorganisaatio, jos sitä ei ole, kun sovellus alustetaan", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Luo vektoriupotukset automaattisesti tekstilohkoista, kun tiedostoja ladataan ja käsitellään", + "Automatically generate vector embeddings when objects are created or updated": "Luo vektoriupotukset automaattisesti, kun objekteja luodaan tai päivitetään", + "Automatically retry failed vectorization attempts (max 3 retries)": "Yritä epäonnistuneita vektorointiyrityksiä automaattisesti uudelleen (enintään 3 yritystä)", + "Automatically update catalogues.": "Päivitä luettelot automaattisesti.", + "Available Filters": "Käytettävissä olevat suodattimet", + "Available Workflows": "Käytettävissä olevat työnkulut", + "Available placeholders:": "Käytettävissä olevat paikkamerkit:", + "Avatar": "Profiilikuva", + "Avatar changes are not supported by your authentication provider.": "Todennuspalveluntarjoajasi ei tue profiilikuvan muutoksia.", + "Avatar removed": "Profiilikuva poistettu", + "Avatar updated successfully": "Profiilikuva päivitettiin onnistuneesti", + "Avg Execution Time": "Keskim. suoritusaika", + "Avg Members/Org": "Keskim. jäseniä/org.", + "Avg Object Views/Session": "Keskim. objektinäkymiä/istunto", + "Avg Response Time": "Keskim. vastausaika", + "Avg Results/Search": "Keskim. tuloksia/haku", + "Avg Searches/Session": "Keskim. hakuja/istunto", + "Back": "Takaisin", + "Back to Entities": "Takaisin entiteetteihin", + "Back to Registers": "Takaisin rekistereihin", + "Back to Webhooks": "Takaisin webhookeihin", + "Back to applications": "Takaisin sovelluksiin", + "Back to entities": "Takaisin entiteetteihin", + "Backend parameter is required": "Backend-parametri vaaditaan", + "Backend updated successfully. Please reload the application.": "Backend päivitettiin onnistuneesti. Lataa sovellus uudelleen.", + "Bandwidth Quota (MB/month)": "Kaistanleveyskiintiö (Mt/kuukausi)", + "Base URL (Optional)": "Perus-URL (valinnainen)", + "Basic Information": "Perustiedot", + "Batch Size": "Eräkoko", + "Batch extraction completed": "Erähaku valmis", + "Before": "Ennen", + "Before object vectorization can work:": "Ennen kuin objektivektorointi voi toimia:", + "Behavior": "Käyttäytyminen", + "Behavior Issue": "Käyttäytymisongelma", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601-kesto, esim. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob-tallennustila on poistettu käytöstä. Kaikki objektit käyttävät nyt magic-tauluja.", + "Bookmarks": "Kirjanmerkit", + "Branch": "Haara", + "Bulk Role Assignment: {title}": "Joukkoroolimääritys: {title}", + "Bulk delete operation completed successfully": "Joukkopoisto-operaatio suoritettiin onnistuneesti", + "Bulk save operation completed successfully": "Joukkotallennusoperaatio suoritettiin onnistuneesti", + "Business Data": "Liiketoimintatiedot", + "CSV": "CSV", + "Cache cleared successfully": "Välimuisti tyhjennettiin onnistuneesti", + "Cache grew from {old} to {new} entries.": "Välimuisti kasvoi {old} merkinnästä {new} merkintään.", + "Cache shrunk from {old} to {new} entries.": "Välimuisti pieneni {old} merkinnästä {new} merkintään.", + "Cache stayed the same at {size} entries.": "Välimuisti pysyi samana {size} merkinnässä.", + "Calculate Sizes": "Laske koot", + "Calendar": "Kalenteri", + "Calendar Provider Configuration": "Kalenteripalveluntarjoajan määritys", + "Calendar integration is not available": "Kalenteri-integraatio ei ole käytettävissä", + "Calendar name": "Kalenterin nimi", + "Cancel": "Peruuta", + "Cancel deactivation request": "Peruuta deaktivointipyyntö", + "Cannot delete: objects are still attached": "Ei voi poistaa: objekteja on edelleen liitettynä", + "Cannot edit: This register is managed by external configuration {title}": "Ei voi muokata: Tätä rekisteriä hallitaan ulkoisella määrityksellä {title}", + "Cannot remove schema with existing objects ({count} objects)": "Ei voi poistaa skeemaa, jolla on olemassa olevia objekteja ({count} objektia)", + "Cannot remove schema {schema} because it contains {count} objects": "Ei voi poistaa skeemaa {schema}, koska se sisältää {count} objektia", + "Cards": "Kortit", + "Cascade delete": "Ketjutettu poisto", + "Categories of data subjects (one per line)": "Rekisteröityjen kategoriat (yksi per rivi)", + "Categories of personal data (one per line)": "Henkilötietojen kategoriat (yksi per rivi)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategoria", + "Change Type": "Muutostyyppi", + "Change password": "Vaihda salasana", + "Changes": "Muutokset", + "Changes copied to clipboard": "Muutokset kopioitiin leikepöydälle", + "Changes for Audit Trail #{id}": "Muutokset tarkastuspolulle #{id}", + "Chat Model": "Keskustelumalli", + "Chat Provider": "Keskustelupalveluntarjoaja", + "Chat Provider (RAG)": "Keskustelupalveluntarjoaja (RAG)", + "Chat Settings": "Keskusteluasetukset", + "Chat provider connection successful!": "Keskustelupalveluntarjoajan yhteys onnistui!", + "Chat settings": "Keskusteluasetukset", + "Chat test failed: {error}": "Keskustelutesti epäonnistui: {error}", + "Choose a register": "Valitse rekisteri", + "Choose a schema": "Valitse skeema", + "Choose how vector similarity calculations are performed for semantic search": "Valitse, miten vektorisamankaltaisuuslaskelmat suoritetaan semanttista hakua varten", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Valitse, mitkä tiedostotyypit sisällytetään vektorointiprosessiin. Vain tiedostot, joista on poimittu tekstiä ja lohkoja, käsitellään.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Valitse, mitkä näkymät sisällytetään vektorointiprosessiin. Jätä tyhjäksi käsitelläksesi kaikki näkymät määrityksesi perusteella.", + "Chunk": "Lohko", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Lohkon poistoa ei ole vielä toteutettu. Käytä lohkopohjaisia päätepisteitä.", + "Chunks": "Lohkot", + "Chunks to Vectorize:": "Vektoroitavat lohkot:", + "Cleanup Old Trails": "Siivoa vanhat polut", + "Cleanup completed": "Siivous valmis", + "Cleanup completed successfully. Deleted {count} entries.": "Siivous suoritettiin onnistuneesti. Poistettiin {count} merkintää.", + "Cleanup failed: {error}": "Siivous epäonnistui: {error}", + "Clear All Embeddings": "Tyhjennä kaikki upotukset", + "Clear Entries": "Tyhjennä merkinnät", + "Clear Filtered Audit Trails": "Tyhjennä suodatetut tarkastuspolut", + "Clear Filters": "Tyhjennä suodattimet", + "Clear Index": "Tyhjennä indeksi", + "Clear Selection": "Tyhjennä valinta", + "Clear all": "Tyhjennä kaikki", + "Clear all filters": "Tyhjennä kaikki suodattimet", + "Clear date range": "Tyhjennä päivämääräväli", + "Clear filters": "Tyhjennä suodattimet", + "Clearing...": "Tyhjennetään...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Napsauta Suorita vaatimustenmukaisuustarkistus löytääksesi skeemat, joista PII on havaittu, mutta joista ei ole käsittelytoiminta-annotaatiota.", + "Click to subscribe to notifications": "Napsauta tilataksesi ilmoitukset", + "Click to unsubscribe from notifications": "Napsauta peruuttaaksesi ilmoitusten tilauksen", + "Close": "Sulje", + "Cluster name": "Klusterin nimi", + "Code": "Koodi", + "Code (short readable key, e.g. v-2026-001)": "Koodi (lyhyt luettava avain, esim. v-2026-001)", + "Collapse details": "Pienennä tiedot", + "Collection Management": "Kokoelman hallinta", + "Collection Name": "Kokoelman nimi", + "Collection assignments updated successfully": "Kokoelman määritykset päivitettiin onnistuneesti", + "Collection cleared successfully": "Kokoelma tyhjennettiin onnistuneesti", + "Collection copied successfully": "Kokoelma kopioitiin onnistuneesti", + "Collection created successfully": "Kokoelma luotiin onnistuneesti", + "Collection deleted successfully": "Kokoelma poistettiin onnistuneesti", + "Collection used to store and index file metadata and content": "Kokoelma, jota käytetään tiedostojen metatietojen ja sisällön tallentamiseen ja indeksointiin", + "Collection used to store and index object data": "Kokoelma, jota käytetään objektitietojen tallentamiseen ja indeksointiin", + "Collections:": "Kokoelmat:", + "Color": "Väri", + "Comment...": "Kommentti...", + "Commit Message": "Vahvistusviesti", + "Commit Within (ms)": "Vahvista aikarajassa (ms)", + "Commit message": "Vahvistusviesti", + "Compare current schema with real object data": "Vertaa nykyistä skeemaa todellisiin objektitietoihin", + "Completed": "Valmis", + "Complex": "Monimutkainen", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Monimutkaiset kyselyt: Lisähaut, joissa on useita suodattimia, operaattoreita ja monimutkaisia parametriyhdistelmiä", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Monimutkaiset kyselyt: lisähaut, joissa on useita suodattimia, operaattoreita ja monimutkaisia parametriyhdistelmiä", + "Compliance": "Vaatimustenmukaisuus", + "Confidence Level": "Luottamustaso", + "Confidence Score": "Luottamuspisteet", + "Confidence: {confidence}%": "Luottamus: {confidence}%", + "Config must be provided as an object": "Määritys on annettava objektina", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet-hallinta", + "ConfigSet Name": "ConfigSet-nimi", + "ConfigSet created successfully": "ConfigSet luotiin onnistuneesti", + "ConfigSet deleted successfully": "ConfigSet poistettiin onnistuneesti", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSet-joukot määrittävät SOLR-kokoelmiesi skeeman ja määrityksen. Ne sisältävät kenttämäärityksiä, analysaattoreita ja muita hakuasetuksia.", + "Configuration": "Määritys", + "Configuration Data": "Määritystiedot", + "Configuration is up to date": "Määritys on ajan tasalla", + "Configuration saved": "Määritys tallennettu", + "Configuration saved successfully.": "Määritys tallennettiin onnistuneesti.", + "Configuration type (default, application, etc.)": "Määritystyyppi (oletus, sovellus jne.)", + "Configurations": "Määritykset", + "Configure API tokens for external service integrations": "Määritä API-tunnukset ulkoisia palveluintegraatioita varten", + "Configure Apache SOLR search engine for advanced search capabilities": "Määritä Apache SOLR -hakukone edistyneitä hakuominaisuuksia varten", + "Configure Facets": "Määritä fasetit", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Määritä LLM-palveluntarjoajat tekoälypohjaisia ominaisuuksia varten, mukaan lukien semanttinen haku, upotukset ja keskustelu.", + "Configure SOLR Facets": "Määritä SOLR-fasetit", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Määritä SOLR-palvelimesi perusyhteysasetukset, mukaan lukien todennus- ja verkkovaihtoehdot. Käytä erillisiä ConfigSet- ja kokoelmanhallintaikkunoita ydinten ja kokoelmien hallintaan.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Määritä SOLR-palvelimesi perusyhteysasetukset, mukaan lukien todennus- ja verkkovaihtoehdot. Käytä erillisiä configset- ja kokoelmanhallintaikkunoita ydinten ja kokoelmien hallintaan.", + "Configure default organisation and organisation-related settings": "Määritä oletusorganisaatio ja organisaatioon liittyvät asetukset", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Määritä, miten tietokantaobjektit muunnetaan vektoriupotuksiksi semanttista hakua varten. Objektit vektoroidaan suoraan ilman tekstin poimintaa.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Määritä, miten objektit muunnetaan tekstiksi ennen vektorointia. Nämä asetukset vaikuttavat hakulaatuun ja kontekstiin.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Määritä LLM-palveluntarjoajat tekoälypohjaisia ominaisuuksia varten, mukaan lukien semanttinen haku, upotukset ja keskustelu.", + "Configure multi-organization support and tenant isolation": "Määritä usean organisaation tuki ja vuokralaiseristys", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Määritä tiedostovektoroinnin parametrit. Tämä prosessi luo vektoriupotukset kaikille poimituille tiedostolohkoille.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Määritä objektivektoroinnin parametrit. Tämä prosessi luo vektoriupotukset kaikille näkymäsuodattimiasi vastaaville objekteille.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Määritä tämä skeema tuomaan objektit esiin tapahtumina Nextcloud Calendar -sovelluksessa.", + "Configure which types of data to search and how many sources to retrieve": "Määritä, minkä tyyppisiä tietoja haetaan ja kuinka monta lähdettä haetaan", + "Configuring field mappings...": "Määritetään kenttämäärityksiä...", + "Confirm Account Deactivation": "Vahvista tilin deaktivointi", + "Confirm Object Deletion": "Vahvista objektin poisto", + "Confirm Object Restoration": "Vahvista objektin palautus", + "Confirm Permanent Object Deletion": "Vahvista objektin pysyvä poisto", + "Confirm deactivation": "Vahvista deaktivointi", + "Connecting to SOLR cluster...": "Yhdistetään SOLR-klusteriin...", + "Connection Failed": "Yhteys epäonnistui", + "Connection Settings": "Yhteysasetukset", + "Connection Successful!": "Yhteys onnistui!", + "Connection failed": "Yhteys epäonnistui", + "Connection protocol": "Yhteysprotokolla", + "Connection successful": "Yhteys onnistui", + "Connection timeout in seconds": "Yhteyden aikakatkaisu sekunteina", + "Constant delay between retries (5 minutes)": "Vakioviive uudelleenyritysten välillä (5 minuuttia)", + "Constraint Issue": "Rajoiteongelma", + "Consumer not found": "Kuluttajaa ei löytynyt", + "Contacts": "Yhteystiedot", + "Contacts integration is not available": "Yhteystietointegraatio ei ole käytettävissä", + "Content Filters": "Sisältösuodattimet", + "Continue to Properties": "Jatka ominaisuuksiin", + "Contracts": "Sopimukset", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Hallitse, mitkä objektinäkymät tulisi vektoroida API-kustannusten vähentämiseksi. Vektoroi vain näkymät, jotka hyötyvät semanttisesta hausta.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Hallitse, mitä näkymiä ja työkaluja tekoäly voi käyttää tässä keskustelussa. Oletuksena kaikki agentin ominaisuudet ovat käytössä.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Hallitsee satunnaisuutta (0 = deterministinen, 2 = erittäin luova)", + "Conversation ID is required": "Keskustelun tunnus vaaditaan", + "Conversation archived": "Keskustelu arkistoitu", + "Conversation archived successfully": "Keskustelu arkistoitiin onnistuneesti", + "Conversation cleared successfully": "Keskustelu tyhjennettiin onnistuneesti", + "Conversation deleted": "Keskustelu poistettu", + "Conversation not found": "Keskustelua ei löytynyt", + "Conversation permanently deleted": "Keskustelu poistettiin pysyvästi", + "Conversation renamed": "Keskustelu nimettiin uudelleen", + "Conversation restored": "Keskustelu palautettu", + "Conversation started with {agent}": "Keskustelu aloitettiin agentin {agent} kanssa", + "Conversation title": "Keskustelun otsikko", + "Conversations": "Keskustelut", + "Converting {schema} to blob storage...": "Muunnetaan {schema} blob-tallennustilaan...", + "Converting {schema} to magic table...": "Muunnetaan {schema} magic-tauluun...", + "Copied!": "Kopioitu!", + "Copy": "Kopioi", + "Copy Changes": "Kopioi muutokset", + "Copy Collection": "Kopioi kokoelma", + "Copy Data": "Kopioi tiedot", + "Copy Full Data": "Kopioi täydet tiedot", + "Copy of {name}": "Kopio kohteesta {name}", + "Copy to clipboard": "Kopioi leikepöydälle", + "Copying...": "Kopioidaan...", + "Costs": "Kustannukset", + "Count": "Määrä", + "Counting objects...": "Lasketaan objekteja...", + "Create": "Luo", + "Create API Token": "Luo API-tunnus", + "Create Agent": "Luo agentti", + "Create Application": "Luo sovellus", + "Create Collection": "Luo kokoelma", + "Create ConfigSet": "Luo ConfigSet", + "Create New Collection": "Luo uusi kokoelma", + "Create New ConfigSet": "Luo uusi ConfigSet", + "Create Operations": "Luontioperaatiot", + "Create Organisation": "Luo organisaatio", + "Create Webhook": "Luo webhook", + "Create a copy of collection:": "Luo kopio kokoelmasta:", + "Create a new ConfigSet based on the _default template": "Luo uusi ConfigSet _default-mallin pohjalta", + "Create a new SOLR collection from an existing ConfigSet": "Luo uusi SOLR-kokoelma olemassa olevasta ConfigSet-joukosta", + "Create a new SOLR collection from an existing configset": "Luo uusi SOLR-kokoelma olemassa olevasta configset-joukosta", + "Create a new configset based on the _default template": "Luo uusi configset _default-mallin pohjalta", + "Create a new event or link an existing one from any of your calendars.": "Luo uusi tapahtuma tai linkitä olemassa oleva mistä tahansa kalenteristasi.", + "Create another": "Luo toinen", + "Create copies of": "Luo kopioita kohteesta", + "Create event": "Luo tapahtuma", + "Create new token": "Luo uusi tunnus", + "Create or link a Deck card to track work on this object.": "Luo tai linkitä Deck-kortti seurataksesi tämän objektin työtä.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Luo ensimmäinen verwerkingsactiviteit aloittaaksesi tarkastuspolun rivien merkitsemisen niiden AVG Art 30 -kohdistuksella.", + "Create your first AI agent to get started.": "Luo ensimmäinen tekoälyagenttisi päästäksesi alkuun.", + "Created": "Luotu", + "Created:": "Luotu:", + "Creating search collections...": "Luodaan hakukokoelmia...", + "Creating...": "Luodaan...", + "Current": "Nykyinen", + "Current Filters": "Nykyiset suodattimet", + "Current Type:": "Nykyinen tyyppi:", + "Current password": "Nykyinen salasana", + "Custom API endpoint if using a different region": "Mukautettu API-päätepiste, jos käytät eri aluetta", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Mukautetut HTTP-otsikot (yksi per rivi, muoto: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Mukautetut HTTP-otsikot (yksi per rivi, muoto: header-name: value)", + "Custom Ranges:": "Mukautetut välit:", + "Custom display title for this facet": "Mukautettu näyttöotsikko tälle fasetille", + "Custom pattern:": "Mukautettu kuvio:", + "DELETE COLLECTION": "POISTA KOKOELMA", + "DSAR": "DSAR", + "Daily": "Päivittäin", + "Dashboard": "Koontinäyttö", + "Dashboard not found": "Koontinäyttöä ei löytynyt", + "Data": "Tiedot", + "Data Source": "Tietolähde", + "Data Sources": "Tietolähteet", + "Data Views": "Tietonäkymät", + "Data sources": "Tietolähteet", + "Data type does not match observed values": "Tietotyyppi ei vastaa havaittuja arvoja", + "Data-subject access request": "Rekisteröidyn tietopyyntö", + "Database URL": "Tietokannan URL", + "Database information refreshed": "Tietokannan tiedot päivitetty", + "Date Faceting:": "Päivämäärän fasetointi:", + "Date Range": "Aikaväli", + "Dates": "Päivämäärät", + "Deactivation request cancelled": "Deaktivointipyyntö peruutettu", + "Deactivation request submitted": "Deaktivointipyyntö lähetetty", + "Deck": "Deck", + "Deck integration is not available": "Deck-integraatio ei ole käytettävissä", + "Default": "Oletus", + "Default Object Tenant": "Objektin oletusvuokralainen", + "Default Organisation": "Oletusorganisaatio", + "Default Owner": "Oletusomistaja", + "Default User (for cron/background jobs)": "Oletuskäyttäjä (cron-/taustatöitä varten)", + "Default User Tenant": "Käyttäjän oletusvuokralainen", + "Default value": "Oletusarvo", + "Default value (Email)": "Oletusarvo (sähköposti)", + "Default value (Password)": "Oletusarvo (salasana)", + "Default value (Phone number)": "Oletusarvo (puhelinnumero)", + "Default value (Regex)": "Oletusarvo (Regex)", + "Default: 5": "Oletus: 5", + "Defaults to current user. Select a different user if needed.": "Oletuksena nykyinen käyttäjä. Valitse tarvittaessa toinen käyttäjä.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Viiveet kaksinkertaistuvat jokaisella yrityksellä (2, 4, 8 minuuttia...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Viiveet kasvavat lineaarisesti (5, 10, 15 minuuttia...)", + "Delete": "Poista", + "Delete ({count})": "Poista ({count})", + "Delete Application": "Poista sovellus", + "Delete Audit Trail": "Poista tarkastusloki", + "Delete Collection": "Poista kokoelma", + "Delete ConfigSet": "Poista ConfigSet", + "Delete Objects": "Poista objektit", + "Delete Operations": "Poistotoiminnot", + "Delete SOLR Collection": "Poista SOLR-kokoelma", + "Delete View": "Poista näkymä", + "Delete all objects in this schema": "Poista kaikki tämän skeeman objektit", + "Delete permanently": "Poista pysyvästi", + "Delete this conversation?": "Poistetaanko tämä keskustelu?", + "Delete view": "Poista näkymä", + "Delete {count} object": "Poista {count} objekti", + "Delete {count} objects": "Poista {count} objektia", + "Deleted": "Poistettu", + "Deleted By": "Poistanut", + "Deleted Date": "Poistopäivämäärä", + "Deleted Items Management": "Poistettujen kohteiden hallinta", + "Deleted This Week": "Poistettu tällä viikolla", + "Deleted Today": "Poistettu tänään", + "Deleted:": "Poistettu:", + "Deleting SOLR Collection...": "Poistetaan SOLR-kokoelmaa...", + "Deleting...": "Poistetaan...", + "Deletion Date Range": "Poiston aikaväli", + "Deletion Statistics": "Poistotilastot", + "Deprecated": "Vanhentunut", + "Depublish": "Peru julkaisu", + "Description": "Kuvaus", + "Description Template": "Kuvausmalli", + "Description shown as tooltip": "Työkaluvihjeenä näytettävä kuvaus", + "Description:": "Kuvaus:", + "Detect data types and patterns": "Tunnista tietotyypit ja kuviot", + "Detected At": "Havaittu", + "Detected Format:": "Havaittu muoto:", + "Detected Issues:": "Havaitut ongelmat:", + "Detected: {date}": "Havaittu: {date}", + "Direct permission": "Suora käyttöoikeus", + "Disable": "Poista käytöstä", + "Disable this to make the agent": "Poista tämä käytöstä tehdäksesi agentista", + "Disabled": "Poistettu käytöstä", + "Discover Files": "Etsi tiedostoja", + "Discovered Properties": "Löydetyt ominaisuudet", + "Display Format": "Näyttömuoto", + "Display Name": "Näyttönimi", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Haluatko poistaa pysyvästi kaikki suodatetut tarkastuslokimerkinnät? Tätä toimintoa ei voi peruuttaa.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Haluatko poistaa pysyvästi tämän tarkastuslokimerkinnän? Tätä toimintoa ei voi peruuttaa.", + "Documents": "Asiakirjat", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR käytössä", + "Download API Spec": "Lataa API-määritys", + "Download API Specification": "Lataa API-määritys", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Lataa kopio kaikista OpenRegisteriin tallennetuista henkilötiedoistasi (GDPR-artikla 20).", + "Drag a file or files here": "Vedä tiedosto tai tiedostoja tähän", + "Dry run completed: {count} fields would be created": "Koeajo valmis: {count} kenttää luotaisiin", + "Dry run completed: {count} fields would be fixed": "Koeajo valmis: {count} kenttää korjattaisiin", + "Duration in seconds (optional)": "Kesto sekunteina (valinnainen)", + "Edit": "Muokkaa", + "Edit Agent": "Muokkaa agenttia", + "Edit Application": "Muokkaa sovellusta", + "Edit Configuration": "Muokkaa määritystä", + "Edit Endpoint": "Muokkaa päätepistettä", + "Edit Organisation": "Muokkaa organisaatiota", + "Edit Property '{key}' of '{title}'": "Muokkaa kohteen '{title}' ominaisuutta '{key}'", + "Edit Register": "Muokkaa rekisteriä", + "Edit Schema": "Muokkaa skeemaa", + "Edit Source": "Muokkaa lähdettä", + "Edit View": "Muokkaa näkymää", + "Edit Webhook": "Muokkaa webhookia", + "Edit labels": "Muokkaa nimikkeitä", + "Edit processing activity": "Muokkaa käsittelytoimintoa", + "Edit tags for {name}": "Muokkaa kohteen {name} tunnisteita", + "Edit verwerkingsactiviteit": "Muokkaa verwerkingsactiviteit", + "Edit view": "Muokkaa näkymää", + "Edit view details": "Muokkaa näkymän tietoja", + "Effectiveness": "Tehokkuus", + "Email": "Sähköposti", + "Email Addresses": "Sähköpostiosoitteet", + "Email digest frequency": "Sähköpostikoosteen tiheys", + "Emails": "Sähköpostit", + "Embedding Model": "Upotusmalli", + "Embedding Provider": "Upotuspalveluntarjoaja", + "Embedding generated successfully": "Upotus luotu onnistuneesti", + "Embedding provider connection successful!": "Yhteys upotuspalveluntarjoajaan onnistui!", + "Embedding test failed: {error}": "Upotustesti epäonnistui: {error}", + "Enable": "Ota käyttöön", + "Enable RAG": "Ota RAG käyttöön", + "Enable auto-creation": "Ota automaattinen luonti käyttöön", + "Enable automatic file vectorization": "Ota tiedostojen automaattinen vektorointi käyttöön", + "Enable automatic object vectorization": "Ota objektien automaattinen vektorointi käyttöön", + "Enable automatic synchronization": "Ota automaattinen synkronointi käyttöön", + "Enable calendar provider": "Ota kalenteripalveluntarjoaja käyttöön", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Ota yksityiskohtainen lokitus käyttöön SOLR-toiminnoille (suositellaan virheenkorjaukseen)", + "Enable faceting": "Ota fasetointi käyttöön", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Ota LLM-ominaisuudet käyttöön tai poista ne käytöstä. Määritä palveluntarjoajat ja mallit yllä olevalla LLM-määrityspainikkeella.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Ota LLM-ominaisuudet käyttöön tai poista ne käytöstä. Määritä palveluntarjoajat ja mallit yllä olevalla LLM-määrityspainikkeella.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Ota SOLR-hakuintegraatio käyttöön tai poista se käytöstä. Määritä yhteysasetukset yllä olevalla Yhteysasetukset-painikkeella.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Ota SOLR-hakuintegraatio käyttöön tai poista se käytöstä. Määritä yhteysasetukset yllä olevalla yhteysasetukset-painikkeella.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Ota n8n-työnkulkuintegraatio käyttöön tai poista se käytöstä. Määritä yhteysasetukset alla.", + "Enable or disable this webhook": "Ota tämä webhook käyttöön tai poista se käytöstä", + "Enable tools that allow the agent to interact with data through function calling.": "Ota käyttöön työkalut, joiden avulla agentti voi olla vuorovaikutuksessa tietojen kanssa funktiokutsujen kautta.", + "Enable vectorization for all existing and future views (may increase costs)": "Ota vektorointi käyttöön kaikille olemassa oleville ja tuleville näkymille (voi lisätä kustannuksia)", + "Enable write-back to target objects": "Ota takaisinkirjoitus kohdeobjekteihin käyttöön", + "Enabled": "Käytössä", + "End Date Field": "Päättymispäivän kenttä", + "Endpoint Path*": "Päätepisteen polku*", + "Endpoints": "Päätepisteet", + "Engine not found": "Moottoria ei löytynyt", + "Enter ConfigSet name": "Anna ConfigSet-nimi", + "Enter Nextcloud usernames to grant access to this private agent": "Anna Nextcloud-käyttäjänimet, joille myönnetään pääsy tähän yksityiseen agenttiin", + "Enter agent description (optional)": "Anna agentin kuvaus (valinnainen)", + "Enter agent name": "Anna agentin nimi", + "Enter application description (optional)": "Anna sovelluksen kuvaus (valinnainen)", + "Enter application name": "Anna sovelluksen nimi", + "Enter collection name": "Anna kokoelman nimi", + "Enter configuration description (optional)": "Anna määrityksen kuvaus (valinnainen)", + "Enter configuration title": "Anna määrityksen otsikko", + "Enter description (optional)...": "Anna kuvaus (valinnainen)...", + "Enter new collection name": "Anna uuden kokoelman nimi", + "Enter object ID": "Anna objektin tunnus", + "Enter organisation description (optional)": "Anna organisaation kuvaus (valinnainen)", + "Enter organisation name": "Anna organisaation nimi", + "Enter search term": "Anna hakutermi", + "Enter search terms or leave empty to browse all": "Anna hakutermit tai jätä tyhjäksi selataksesi kaikkia", + "Enter system prompt for the agent": "Anna agentin järjestelmäkehote", + "Enter username and press Enter": "Anna käyttäjänimi ja paina Enter", + "Enter view name...": "Anna näkymän nimi...", + "Enter webhook name": "Anna webhookin nimi", + "Enter your API key": "Anna API-avaimesi", + "Enter your object here...": "Anna objektisi tähän...", + "Enter your schema here...": "Anna skeemasi tähän...", + "Entities": "Entiteetit", + "Entities detected": "Entiteetit havaittu", + "Entity ID: {id}": "Entiteetin tunnus: {id}", + "Entity Information": "Entiteetin tiedot", + "Entity Recognition": "Entiteettien tunnistus", + "Entity Recognition Method": "Entiteettien tunnistusmenetelmä", + "Entity deleted successfully": "Entiteetti poistettu onnistuneesti", + "Entity not found": "Entiteettiä ei löytynyt", + "Entries to be deleted:": "Poistettavat merkinnät:", + "Enum Issue": "Enum-ongelma", + "Enum constraint is missing": "Enum-rajoite puuttuu", + "Erase (Art 17)": "Poista (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Poistetaanko {count} objekti(a) tälle rekisteröidylle? Tämä toiminto kirjataan tarkastuslokiin.", + "Erasure complete": "Poisto valmis", + "Erasure preview": "Poiston esikatselu", + "Error": "Virhe", + "Error Details": "Virheen tiedot", + "Error Information": "Virheen tiedot", + "Error Message": "Virheilmoitus", + "Error deleting audit trails: {error}": "Virhe tarkastuslokien poistossa: {error}", + "Error deleting search trails: {error}": "Virhe hakulokien poistossa: {error}", + "Error loading application": "Virhe sovellusta ladattaessa", + "Error loading audit trails": "Virhe tarkastuslokeja ladattaessa", + "Error loading entity": "Virhe entiteettiä ladattaessa", + "Error loading search trails": "Virhe hakulokeja ladattaessa", + "Estimated Batches:": "Arvioidut erät:", + "Estimated Cost:": "Arvioitu kustannus:", + "Estimated Duration:": "Arvioitu kesto:", + "Event": "Tapahtuma", + "Event Property for Payload": "Tapahtuman ominaisuus hyötykuormaa varten", + "Events": "Tapahtumat", + "Every schema with detected PII has a processing-activity annotation.": "Jokaisella skeemalla, jossa on havaittu PII, on käsittelytoiminnon merkintä.", + "Example": "Esimerkki", + "Example Value": "Esimerkkiarvo", + "Example value for this property": "Esimerkkiarvo tälle ominaisuudelle", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Eksklusiivinen maksimi", + "Exclusive Minimum": "Eksklusiivinen minimi", + "Exclusive maximum": "Eksklusiivinen maksimi", + "Exclusive minimum": "Eksklusiivinen minimi", + "Execution Mode": "Suoritustila", + "Execution Time": "Suoritusaika", + "Execution Time Range": "Suoritusajan väli", + "Existing Improvements": "Olemassa olevat parannukset", + "Expand details": "Laajenna tiedot", + "Expiration": "Vanheneminen", + "Expires": "Vanhenee", + "Expires in (e.g., 90d)": "Vanhenee (esim. 90d)", + "Exponential": "Eksponentiaalinen", + "Export": "Vie", + "Export \"{schema}\" objects from \"{register}\"": "Vie \"{schema}\"-objektit rekisteristä \"{register}\"", + "Export Configuration": "Vie määritys", + "Export Format:": "Vientimuoto:", + "Export Objects": "Vie objektit", + "Export completed successfully": "Vienti valmistui onnistuneesti", + "Export downloaded successfully": "Vienti ladattu onnistuneesti", + "Export failed: {error}": "Vienti epäonnistui: {error}", + "Export is rate limited. Please try again later.": "Vienti on rajoitettu. Yritä myöhemmin uudelleen.", + "Export my data": "Vie tietoni", + "Export, view, or delete audit trails": "Vie, tarkastele tai poista tarkastuslokeja", + "Exporting...": "Viedään...", + "Extend Schema": "Laajenna skeemaa", + "Extended": "Laajennettu", + "Extensions": "Laajennukset", + "Extra Query Parameters": "Ylimääräiset kyselyparametrit", + "Extract Now": "Pura nyt", + "Extract Pending Files": "Pura odottavat tiedostot", + "Extract now": "Pura nyt", + "Extract properties from each object": "Pura ominaisuudet jokaisesta objektista", + "Extracted At": "Purettu", + "Extracted at": "Purettu", + "Extraction": "Purku", + "Extraction Mode": "Purkutila", + "Extraction Scope": "Purun laajuus", + "Extraction Status": "Purun tila", + "Extraction error for {file}: {error}": "Purkuvirhe tiedostolle {file}: {error}", + "Facet Description": "Fasetin kuvaus", + "Facet Order": "Fasetin järjestys", + "Facet Title": "Fasetin otsikko", + "Facet Type": "Fasetin tyyppi", + "Facet configuration updated successfully": "Fasetin määritys päivitetty onnistuneesti", + "Facetable": "Fasetoitava", + "Faceting Configuration:": "Fasetoinnin määritys:", + "Facets discovered and configured successfully": "Fasetit löydetty ja määritetty onnistuneesti", + "Facets discovered successfully": "Fasetit löydetty onnistuneesti", + "Failed": "Epäonnistui", + "Failed to add attachment to object": "Liitteen lisääminen objektiin epäonnistui", + "Failed to add user to organisation": "Käyttäjän lisääminen organisaatioon epäonnistui", + "Failed to analyze schema properties": "Skeeman ominaisuuksien analysointi epäonnistui", + "Failed to archive conversation": "Keskustelun arkistointi epäonnistui", + "Failed to calculate sizes": "Kokojen laskeminen epäonnistui", + "Failed to cancel deactivation": "Deaktivoinnin peruuttaminen epäonnistui", + "Failed to change active organisation: {error}": "Aktiivisen organisaation vaihtaminen epäonnistui: {error}", + "Failed to change password": "Salasanan vaihtaminen epäonnistui", + "Failed to check version: {error}": "Version tarkistaminen epäonnistui: {error}", + "Failed to clear audit trails: {error}": "Tarkastuslokien tyhjentäminen epäonnistui: {error}", + "Failed to clear blob objects: {error}": "Blob-objektien tyhjentäminen epäonnistui: {error}", + "Failed to clear cache: {error}": "Välimuistin tyhjentäminen epäonnistui: {error}", + "Failed to clear collection": "Kokoelman tyhjentäminen epäonnistui", + "Failed to clear conversation": "Keskustelun tyhjentäminen epäonnistui", + "Failed to clear embeddings: {error}": "Upotusten tyhjentäminen epäonnistui: {error}", + "Failed to clear search trails: {error}": "Hakulokien tyhjentäminen epäonnistui: {error}", + "Failed to clear {type} cache: {error}": "Välimuistin {type} tyhjentäminen epäonnistui: {error}", + "Failed to copy changes": "Muutosten kopiointi epäonnistui", + "Failed to copy data": "Tietojen kopiointi epäonnistui", + "Failed to copy data to clipboard": "Tietojen kopiointi leikepöydälle epäonnistui", + "Failed to copy to clipboard": "Kopiointi leikepöydälle epäonnistui", + "Failed to copy token": "Tunnisteen kopiointi epäonnistui", + "Failed to create conversation": "Keskustelun luominen epäonnistui", + "Failed to create missing SOLR fields: {error}": "Puuttuvien SOLR-kenttien luominen epäonnistui: {error}", + "Failed to create or find project": "Projektin luominen tai löytäminen epäonnistui", + "Failed to create token": "Tunnisteen luominen epäonnistui", + "Failed to delete SOLR collection: {error}": "SOLR-kokoelman poistaminen epäonnistui: {error}", + "Failed to delete collection": "Kokoelman poistaminen epäonnistui", + "Failed to delete conversation": "Keskustelun poistaminen epäonnistui", + "Failed to delete objects for {schema}: {error}": "Skeeman {schema} objektien poistaminen epäonnistui: {error}", + "Failed to delete webhook": "Webhookin poistaminen epäonnistui", + "Failed to depublish register: {error}": "Rekisterin julkaisun peruuttaminen epäonnistui: {error}", + "Failed to depublish schema: {error}": "Skeeman julkaisun peruuttaminen epäonnistui: {error}", + "Failed to depublish: {error}": "Julkaisun peruuttaminen epäonnistui: {error}", + "Failed to discover files: {error}": "Tiedostojen etsiminen epäonnistui: {error}", + "Failed to download API specification": "API-määrityksen lataaminen epäonnistui", + "Failed to export data": "Tietojen vieminen epäonnistui", + "Failed to export objects": "Objektien vieminen epäonnistui", + "Failed to fetch conversation history": "Keskusteluhistorian hakeminen epäonnistui", + "Failed to fix mismatched SOLR fields: {error}": "Yhteensopimattomien SOLR-kenttien korjaaminen epäonnistui: {error}", + "Failed to get SOLR field configuration": "SOLR-kenttämäärityksen hakeminen epäonnistui", + "Failed to get chat statistics": "Keskustelutilastojen hakeminen epäonnistui", + "Failed to get database information: %s": "Tietokannan tietojen hakeminen epäonnistui: %s", + "Failed to get entity categories": "Entiteettiluokkien hakeminen epäonnistui", + "Failed to get entity statistics": "Entiteettitilastojen hakeminen epäonnistui", + "Failed to get entity types": "Entiteettityyppien hakeminen epäonnistui", + "Failed to get response: {error}": "Vastauksen saaminen epäonnistui: {error}", + "Failed to invalidate app store cache: {error}": "Sovelluskaupan välimuistin mitätöinti epäonnistui: {error}", + "Failed to leave organisation: {error}": "Organisaatiosta poistuminen epäonnistui: {error}", + "Failed to link object": "Objektin linkittäminen epäonnistui", + "Failed to load Deck cards": "Deck-korttien lataaminen epäonnistui", + "Failed to load LLM configuration": "LLM-määrityksen lataaminen epäonnistui", + "Failed to load Nextcloud groups": "Nextcloud-ryhmien lataaminen epäonnistui", + "Failed to load cache statistics: {error}": "Välimuistitilastojen lataaminen epäonnistui: {error}", + "Failed to load conversation": "Keskustelun lataaminen epäonnistui", + "Failed to load entities": "Entiteettien lataaminen epäonnistui", + "Failed to load entity": "Entiteetin lataaminen epäonnistui", + "Failed to load extraction data": "Purkutietojen lataaminen epäonnistui", + "Failed to load files": "Tiedostojen lataaminen epäonnistui", + "Failed to load linked contacts": "Linkitettyjen yhteystietojen lataaminen epäonnistui", + "Failed to load linked emails": "Linkitettyjen sähköpostien lataaminen epäonnistui", + "Failed to load linked events": "Linkitettyjen tapahtumien lataaminen epäonnistui", + "Failed to load organisations": "Organisaatioiden lataaminen epäonnistui", + "Failed to load register data": "Rekisteritietojen lataaminen epäonnistui", + "Failed to load related entities": "Liittyvien entiteettien lataaminen epäonnistui", + "Failed to load settings: {error}": "Asetusten lataaminen epäonnistui: {error}", + "Failed to load statistics: {error}": "Tilastojen lataaminen epäonnistui: {error}", + "Failed to load templates": "Mallien lataaminen epäonnistui", + "Failed to load view: {error}": "Näkymän lataaminen epäonnistui: {error}", + "Failed to load webhooks": "Webhookien lataaminen epäonnistui", + "Failed to load workflows": "Työnkulkujen lataaminen epäonnistui", + "Failed to process message": "Viestin käsittely epäonnistui", + "Failed to publish register: {error}": "Rekisterin julkaiseminen epäonnistui: {error}", + "Failed to publish schema: {error}": "Skeeman julkaiseminen epäonnistui: {error}", + "Failed to publish: {error}": "Julkaiseminen epäonnistui: {error}", + "Failed to refresh database information": "Tietokannan tietojen päivittäminen epäonnistui", + "Failed to reindex collection": "Kokoelman uudelleenindeksointi epäonnistui", + "Failed to remove avatar": "Profiilikuvan poistaminen epäonnistui", + "Failed to remove link": "Linkin poistaminen epäonnistui", + "Failed to remove member: {error}": "Jäsenen poistaminen epäonnistui: {error}", + "Failed to remove schema {schema}: {error}": "Skeeman {schema} poistaminen epäonnistui: {error}", + "Failed to remove user from organisation": "Käyttäjän poistaminen organisaatiosta epäonnistui", + "Failed to rename conversation": "Keskustelun uudelleennimeäminen epäonnistui", + "Failed to request deactivation": "Deaktivoinnin pyytäminen epäonnistui", + "Failed to restore conversation": "Keskustelun palauttaminen epäonnistui", + "Failed to retrieve extraction status.": "Purun tilan hakeminen epäonnistui.", + "Failed to retrieve objects for file.": "Tiedoston objektien hakeminen epäonnistui.", + "Failed to retry extraction": "Purun uudelleenyrittäminen epäonnistui", + "Failed to retry webhook": "Webhookin uudelleenyrittäminen epäonnistui", + "Failed to revoke token": "Tunnisteen peruuttaminen epäonnistui", + "Failed to save GitHub token": "GitHub-tunnisteen tallentaminen epäonnistui", + "Failed to save GitLab URL": "GitLab-URL:n tallentaminen epäonnistui", + "Failed to save GitLab token": "GitLab-tunnisteen tallentaminen epäonnistui", + "Failed to save LLM settings: {error}": "LLM-asetusten tallentaminen epäonnistui: {error}", + "Failed to save additional feedback": "Lisäpalautteen tallentaminen epäonnistui", + "Failed to save configuration.": "Määrityksen tallentaminen epäonnistui.", + "Failed to save configuration: {error}": "Määrityksen tallentaminen epäonnistui: {error}", + "Failed to save facet configuration": "Fasetin määrityksen tallentaminen epäonnistui", + "Failed to save feedback": "Palautteen tallentaminen epäonnistui", + "Failed to save file settings: {error}": "Tiedostoasetusten tallentaminen epäonnistui: {error}", + "Failed to save n8n configuration": "n8n-määrityksen tallentaminen epäonnistui", + "Failed to save preferences": "Asetusten tallentaminen epäonnistui", + "Failed to save roles": "Roolien tallentaminen epäonnistui", + "Failed to save settings": "Asetusten tallentaminen epäonnistui", + "Failed to save settings: {error}": "Asetusten tallentaminen epäonnistui: {error}", + "Failed to save view: {error}": "Näkymän tallentaminen epäonnistui: {error}", + "Failed to save warmup interval: {error}": "Lämmitysvälin tallentaminen epäonnistui: {error}", + "Failed to save webhook": "Webhookin tallentaminen epäonnistui", + "Failed to send feedback": "Palautteen lähettäminen epäonnistui", + "Failed to set active organisation: {error}": "Aktiivisen organisaation asettaminen epäonnistui: {error}", + "Failed to setup SOLR: {error}": "SOLR:n määrittäminen epäonnistui: {error}", + "Failed to start file warmup: {error}": "Tiedostojen lämmityksen aloittaminen epäonnistui: {error}", + "Failed to start processing {type} files: {error}": "Tiedostojen {type} käsittelyn aloittaminen epäonnistui: {error}", + "Failed to start vectorization: {error}": "Vektoroinnin aloittaminen epäonnistui: {error}", + "Failed to switch organisation: {error}": "Organisaation vaihtaminen epäonnistui: {error}", + "Failed to sync magic table for {schema}: {error}": "Skeeman {schema} maagisen taulun synkronointi epäonnistui: {error}", + "Failed to test SOLR connection: {error}": "SOLR-yhteyden testaaminen epäonnistui: {error}", + "Failed to test webhook": "Webhookin testaaminen epäonnistui", + "Failed to update LLM settings: {error}": "LLM-asetusten päivittäminen epäonnistui: {error}", + "Failed to update Multitenancy settings: {error}": "Monivuokralaisuusasetusten päivittäminen epäonnistui: {error}", + "Failed to update RBAC settings: {error}": "RBAC-asetusten päivittäminen epäonnistui: {error}", + "Failed to update Retention settings: {error}": "Säilytysasetusten päivittäminen epäonnistui: {error}", + "Failed to update SOLR settings: {error}": "SOLR-asetusten päivittäminen epäonnistui: {error}", + "Failed to update favorite status": "Suosikkitilan päivittäminen epäonnistui", + "Failed to update schema configuration for {schema}: {error}": "Skeeman {schema} määrityksen päivittäminen epäonnistui: {error}", + "Failed to update schema properties": "Skeeman ominaisuuksien päivittäminen epäonnistui", + "Failed to update view": "Näkymän päivittäminen epäonnistui", + "Failed to update view: {error}": "Näkymän päivittäminen epäonnistui: {error}", + "Failed to update webhook": "Webhookin päivittäminen epäonnistui", + "Failed to upload avatar": "Profiilikuvan lataaminen epäonnistui", + "Failed to validate {schema}: {error}": "Skeeman {schema} validointi epäonnistui: {error}", + "Failed to vectorize files: {error}": "Tiedostojen vektorointi epäonnistui: {error}", + "Failed to warmup names cache: {error}": "Nimivälimuistin lämmittäminen epäonnistui: {error}", + "Federation": "Federaatio", + "Feedback recorded": "Palaute tallennettu", + "Fewer sources (1-3):": "Vähemmän lähteitä (1-3):", + "Field": "Kenttä", + "Fields to Display": "Näytettävät kentät", + "File": "Tiedosto", + "File #{id}": "Tiedosto #{id}", + "File Chunk Prediction": "Tiedoston lohkon ennuste", + "File Collection": "Tiedostokokoelma", + "File Handling": "Tiedostojen käsittely", + "File Location": "Tiedoston sijainti", + "File Management": "Tiedostojen hallinta", + "File Name": "Tiedoston nimi", + "File Path": "Tiedoston polku", + "File Type Selection": "Tiedostotyypin valinta", + "File Types to Process": "Käsiteltävät tiedostotyypit", + "File Vectorization": "Tiedostojen vektorointi", + "File Warmup": "Tiedostojen lämmitys", + "File actions menu": "Tiedostotoimintojen valikko", + "File anonymized successfully": "Tiedosto anonymisoitu onnistuneesti", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Tiedoston lohkojen vektorointia ei ole vielä toteutettu. Lohkot ovat valmiina ja tallennettuina, mutta vektorointipalvelu on kehitteillä.", + "File collection not configured": "Tiedostokokoelmaa ei ole määritetty", + "File discovery completed": "Tiedostojen etsintä valmistui", + "File extraction completed": "Tiedoston purku valmistui", + "File extraction queued": "Tiedoston purku jonossa", + "File indexed successfully": "Tiedosto indeksoitu onnistuneesti", + "File is already anonymized": "Tiedosto on jo anonymisoitu", + "File name": "Tiedoston nimi", + "File not found": "Tiedostoa ei löytynyt", + "File settings saved successfully": "Tiedostoasetukset tallennettu onnistuneesti", + "File settings updated successfully": "Tiedostoasetukset päivitetty onnistuneesti", + "File sources": "Tiedostolähteet", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Tiedostojen vektorointi valmistui! {vectorized} osaa vektoroitu {files} tiedostosta. {failed} epäonnistui.", + "File vectorization configuration saved successfully": "Tiedostojen vektoroinnin asetukset tallennettiin onnistuneesti", + "File vectorization started. Check the statistics section for progress.": "Tiedostojen vektorointi aloitettiin. Tarkista edistyminen tilastot-osiosta.", + "File warmup completed": "Tiedoston esilämmitys valmistui", + "Files": "Tiedostot", + "Files added successfully": "Tiedostot lisättiin onnistuneesti", + "Files with Completed Extraction:": "Tiedostot, joiden poiminta on valmis:", + "Files → fileCollection, Objects → objectCollection": "Files → fileCollection, Objects → objectCollection", + "Files → filecollection, objects → objectcollection": "Files → filecollection, objects → objectcollection", + "Filter Audit Trails": "Suodata auditointijäljet", + "Filter Deleted Items": "Suodata poistetut kohteet", + "Filter Objects": "Suodata objektit", + "Filter Properties": "Suodata ominaisuudet", + "Filter Search Trails": "Suodata hakujäljet", + "Filter Statistics": "Suodata tilastot", + "Filter and analyze search trail entries": "Suodata ja analysoi hakujälkien merkintöjä", + "Filter and manage audit trail entries": "Suodata ja hallitse auditointijälkien merkintöjä", + "Filter and manage soft deleted items": "Suodata ja hallitse pehmeästi poistettuja kohteita", + "Filter and search entities": "Suodata ja hae entiteettejä", + "Filter by object ID": "Suodata objektin tunnuksen mukaan", + "Filter by search term": "Suodata hakutermin mukaan", + "Filter by type": "Suodata tyypin mukaan", + "Filter by webhook": "Suodata webhookin mukaan", + "Filter data loaded automatically. Use the filters below to refine your search.": "Suodatintiedot ladattiin automaattisesti. Tarkenna hakuasi alla olevilla suodattimilla.", + "Filter fields...": "Suodata kenttiä...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Suodata webhookin laukaisimet hyötykuorman ominaisuuksien mukaan (yksi riviä kohden, muoto: avain: arvo)", + "Filtered": "Suodatettu", + "Filters": "Suodattimet", + "Finalizing setup...": "Viimeistellään asennusta...", + "Fireworks AI Chat Settings": "Fireworks AI -chat-asetukset", + "Fireworks AI Embedding Configuration": "Fireworks AI -upotusmääritys", + "First": "Ensimmäinen", + "Fixed": "Korjattu", + "For chat and retrieval-augmented generation": "Chattiin ja haulla rikastettuun generointiin", + "For vector embeddings and semantic search": "Vektoriupotuksiin ja semanttiseen hakuun", + "Format": "Muoto", + "Format Issue": "Muoto-ongelma", + "Format constraint is missing": "Muotorajoite puuttuu", + "From (e.g. -7 days or 2025-01-01)": "Alkaen (esim. -7 days tai 2025-01-01)", + "From Date": "Alkupäivämäärä", + "From date": "Alkupäivämäärä", + "Full data copied to clipboard": "Kaikki tiedot kopioitiin leikepöydälle", + "Function Tools": "Funktiotyökalut", + "General Issue": "Yleinen ongelma", + "Generate recommendations and confidence scores": "Luo suosituksia ja luottamuspisteitä", + "Generate report": "Luo raportti", + "Generate the verantwoordingsdocument": "Luo verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Luo vektorit välittömästi, kun uusia objekteja luodaan", + "Generated: {time}": "Luotu: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Saat reaaliaikaista tietoa organisaatiosi datan kunnosta keskittymällä rekistereihin, skeemamäärityksiin sekä objektien tallennukseen ja käyttöön.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Saat reaaliaikaista tietoa organisaatiosi datan kunnosta, kasvutrendeistä ja laatumittareista.", + "GitHub Personal Access Token": "GitHub-henkilökohtainen käyttöoikeustunnus", + "GitHub token is valid": "GitHub-tunnus on kelvollinen", + "GitHub token is valid! Username: {username}": "GitHub-tunnus on kelvollinen! Käyttäjänimi: {username}", + "GitHub token removed successfully": "GitHub-tunnus poistettiin onnistuneesti", + "GitHub token saved successfully": "GitHub-tunnus tallennettiin onnistuneesti", + "GitHub token test failed: {message}": "GitHub-tunnuksen testaus epäonnistui: {message}", + "GitLab Personal Access Token": "GitLab-henkilökohtainen käyttöoikeustunnus", + "GitLab URL saved successfully": "GitLab-URL tallennettiin onnistuneesti", + "GitLab token is valid": "GitLab-tunnus on kelvollinen", + "GitLab token is valid! Username: {username}": "GitLab-tunnus on kelvollinen! Käyttäjänimi: {username}", + "GitLab token saved successfully": "GitLab-tunnus tallennettiin onnistuneesti", + "GitLab token test failed: {message}": "GitLab-tunnuksen testaus epäonnistui: {message}", + "Go to linked Register": "Siirry linkitettyyn rekisteriin", + "Go to linked Schema": "Siirry linkitettyyn skeemaan", + "Go to organisation": "Siirry organisaatioon", + "Grant additional permissions beyond standard CRUD operations": "Myönnä lisäoikeuksia tavanomaisten CRUD-toimintojen lisäksi", + "Group Quota": "Ryhmäkiintiö", + "Groups": "Ryhmät", + "HTML preview": "HTML-esikatselu", + "HTTP Method": "HTTP-menetelmä", + "HTTP method used to send webhook requests": "Webhook-pyyntöjen lähettämiseen käytetty HTTP-menetelmä", + "Headers": "Otsikot", + "Health": "Kunto", + "Heartbeat successful - connection kept alive": "Sykäys onnistui - yhteys pidettiin auki", + "Helpful": "Hyödyllinen", + "Here you can configure the details for various connections.": "Täällä voit määrittää eri yhteyksien tiedot.", + "Here you can configure the details for your organisation.": "Täällä voit määrittää organisaatiosi tiedot.", + "Hide": "Piilota", + "Hide Filters": "Piilota suodattimet", + "Hide in collection view": "Piilota kokoelmanäkymässä", + "Hide in forms": "Piilota lomakkeissa", + "Hide in list view": "Piilota luettelonäkymässä", + "High": "Korkea", + "High Confidence": "Korkea luottamus", + "Host": "Isäntä", + "Hourly": "Tunneittain", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Kuinka syvälle sisäkkäisiä objektin ominaisuuksia käydään läpi (1-20). Suuremmat arvot tallentavat enemmän yksityiskohtia mutta kasvattavat vektorin kokoa.", + "How often to check for updates (1-168 hours)": "Kuinka usein päivityksiä tarkistetaan (1-168 tuntia)", + "How to handle retries for failed webhook deliveries": "Kuinka käsitellä epäonnistuneiden webhook-toimitusten uudelleenyrityksiä", + "IBANs": "IBANit", + "ID": "ID", + "ID of input mapping (optional)": "Syötekartoituksen tunnus (valinnainen)", + "ID of output mapping (optional)": "Tuloskartoituksen tunnus (valinnainen)", + "ID of the target resource": "Kohderesurssin tunnus", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "IP-nopeusrajoitukset tyhjennettiin onnistuneesti", + "Identify properties not in the schema": "Tunnista ominaisuudet, jotka eivät ole skeemassa", + "Immutable": "Muuttumaton", + "Import": "Tuo", + "Import Configuration": "Tuo määritys", + "Import Data into Register": "Tuo dataa rekisteriin", + "Import successful": "Tuonti onnistui", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Tuo report-bundle.json-malli saadaksesi `reports`-rekisterin, ja luo sitten ensimmäinen koontinäyttösi tavanomaisen objektikäyttöliittymän kautta. Koontinäytöt määrittävät widgettinsä JSON-muodossa, ja renderöijä syöttää kullekin widgetille reaaliaikaista koostedataa.", + "Improved Property": "Parannettu ominaisuus", + "In use": "Käytössä", + "Inactive": "Ei aktiivinen", + "Inactive organisations cannot be used": "Ei-aktiivisia organisaatioita ei voi käyttää", + "Include IDs and names of related objects for better contextual search": "Sisällytä liittyvien objektien tunnukset ja nimet paremman kontekstuaalisen haun mahdollistamiseksi", + "Include ids and names of related objects for better contextual search": "Sisällytä liittyvien objektien tunnukset ja nimet paremman kontekstuaalisen haun mahdollistamiseksi", + "Include related object references": "Sisällytä liittyvien objektien viittaukset", + "Include schema and register metadata": "Sisällytä skeeman ja rekisterin metatiedot", + "Index": "Indeksi", + "Information about the current OpenRegister installation": "Tietoja nykyisestä OpenRegister-asennuksesta", + "Inherited from register": "Peritty rekisteristä", + "Inherits permissions from register": "Perii oikeudet rekisteristä", + "Initialization failed": "Alustus epäonnistui", + "Initialize Project": "Alusta projekti", + "Initializing SOLR setup...": "Alustetaan SOLR-asennusta...", + "Input Mapping": "Syötekartoitus", + "Inspect Fields": "Tarkastele kenttiä", + "Inspect Index": "Tarkastele indeksiä", + "Inspect SOLR Index": "Tarkastele SOLR-indeksiä", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Asenna notify_push-sovellus Nextcloud App Storesta reaaliaikaisten päivitysten käyttöönottamiseksi.", + "Integrations": "Integraatiot", + "Internet location (URL) of this installation": "Tämän asennuksen internet-sijainti (URL)", + "Interval": "Aikaväli", + "Invalid": "Virheellinen", + "Invalid batch size. Must be between 1 and 5000": "Virheellinen eräkoko. Oltava välillä 1-5000", + "Invalid feedback type": "Virheellinen palautetyyppi", + "Invalid field name provided": "Annettu kentän nimi on virheellinen", + "Invalid maxObjects. Must be 0 (all) or positive number": "Virheellinen maxObjects. Oltava 0 (kaikki) tai positiivinen luku", + "Invalid:": "Virheellinen:", + "Invite Users": "Kutsu käyttäjiä", + "Invited Users": "Kutsutut käyttäjät", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Inzage-tulokset", + "Issue": "Ongelma", + "Items per page:": "Kohteita sivulla:", + "Just now": "Juuri nyt", + "Key": "Avain", + "LLM Configuration": "LLM-määritys", + "LLM actions menu": "LLM-toimintovalikko", + "LLM configuration saved successfully": "LLM-määritys tallennettiin onnistuneesti", + "LLM features disabled": "LLM-ominaisuudet poistettiin käytöstä", + "LLM features enabled": "LLM-ominaisuudet otettiin käyttöön", + "LLM must be enabled with an embedding provider configured": "LLM on otettava käyttöön ja upotuspalveluntarjoaja on määritettävä", + "LLM settings saved successfully": "LLM-asetukset tallennettiin onnistuneesti", + "LLM settings updated successfully": "LLM-asetukset päivitettiin onnistuneesti", + "Label": "Otsikko", + "Labels": "Otsikot", + "Languages": "Kielet", + "Last": "Viimeinen", + "Last Triggered": "Viimeksi laukaistu", + "Leave empty to allow all users access": "Jätä tyhjäksi salliaksesi kaikkien käyttäjien pääsyn", + "Leave off for auto-detection from property format.": "Jätä pois ominaisuuden muodosta tehtävää automaattista tunnistusta varten.", + "Left organisation successfully": "Organisaatiosta poistuminen onnistui", + "Legal basis *": "Oikeusperuste *", + "Length Range:": "Pituusalue:", + "Linear": "Lineaarinen", + "Link": "Linkki", + "Link an email from the Mail app sidebar to associate it with this object.": "Linkitä sähköposti Mail-sovelluksen sivupalkista yhdistääksesi sen tähän objektiin.", + "Link an object to see it here.": "Linkitä objekti nähdäksesi sen tässä.", + "Link another object": "Linkitä toinen objekti", + "Link existing event": "Linkitä olemassa oleva tapahtuma", + "Link removed": "Linkki poistettiin", + "Link to Object": "Linkki objektiin", + "Link to {name}": "Linkki kohteeseen {name}", + "Linked Objects": "Linkitetyt objektit", + "Linked by {user}": "Linkittänyt {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Linkitetyt sähköpostit, kalenteritapahtumat, yhteystiedot ja Deck-kortit näkyvät tässä.", + "Linked to {name}": "Linkitetty kohteeseen {name}", + "Load Advanced Filters": "Lataa lisäsuodattimet", + "Load advanced filters with live data from your search index": "Lataa lisäsuodattimet reaaliaikaisella datalla hakuindeksistäsi", + "Load more": "Lataa lisää", + "Load view": "Lataa näkymä", + "Loading API tokens...": "Ladataan API-tunnuksia...", + "Loading ConfigSets...": "Ladataan ConfigSetejä...", + "Loading LLM configuration...": "Ladataan LLM-määritystä...", + "Loading RBAC settings...": "Ladataan RBAC-asetuksia...", + "Loading activity...": "Ladataan toimintaa...", + "Loading advanced filters...": "Ladataan lisäsuodattimia...", + "Loading agents...": "Ladataan agentteja...", + "Loading application details...": "Ladataan sovelluksen tietoja...", + "Loading applications...": "Ladataan sovelluksia...", + "Loading archived conversations...": "Ladataan arkistoituja keskusteluja...", + "Loading audit trails...": "Ladataan auditointijälkiä...", + "Loading available tools...": "Ladataan käytettävissä olevia työkaluja...", + "Loading cache statistics...": "Ladataan välimuistitilastoja...", + "Loading collections...": "Ladataan kokoelmia...", + "Loading configurations...": "Ladataan määrityksiä...", + "Loading conversation...": "Ladataan keskustelua...", + "Loading conversations...": "Ladataan keskusteluja...", + "Loading deleted items...": "Ladataan poistettuja kohteita...", + "Loading endpoints": "Ladataan päätepisteitä", + "Loading entities...": "Ladataan entiteettejä...", + "Loading events...": "Ladataan tapahtumia...", + "Loading filter data...": "Ladataan suodatintietoja...", + "Loading groups...": "Ladataan ryhmiä...", + "Loading linked objects...": "Ladataan linkitettyjä objekteja...", + "Loading log details...": "Ladataan lokin tietoja...", + "Loading multitenancy settings...": "Ladataan monivuokralaisuusasetuksia...", + "Loading n8n configuration...": "Ladataan n8n-määritystä...", + "Loading permission matrix...": "Ladataan oikeusmatriisia...", + "Loading preferences...": "Ladataan asetuksia...", + "Loading register data...": "Ladataan rekisteritietoja...", + "Loading registers...": "Ladataan rekistereitä...", + "Loading retention settings...": "Ladataan säilytysasetuksia...", + "Loading schemas...": "Ladataan skeemoja...", + "Loading search configuration...": "Ladataan hakumääritystä...", + "Loading search trails...": "Ladataan hakujälkiä...", + "Loading sources...": "Ladataan lähteitä...", + "Loading statistics...": "Ladataan tilastoja...", + "Loading text extraction settings...": "Ladataan tekstinpoiminta-asetuksia...", + "Loading tokens...": "Ladataan tunnuksia...", + "Loading user groups...": "Ladataan käyttäjäryhmiä...", + "Loading users...": "Ladataan käyttäjiä...", + "Loading views...": "Ladataan näkymiä...", + "Loading...": "Ladataan...", + "Local": "Paikallinen", + "Local Version": "Paikallinen versio", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Paikanna jokainen rekisteröityyn viittaava objekti (Art 15 inzage), esikatsele poistoa (Art 17 vergetelheid) tai vie heidän tietonsa (Art 20 portabiliteit).", + "Location": "Sijainti", + "Location Field": "Sijaintikenttä", + "Locations": "Sijainnit", + "Locked": "Lukittu", + "Locked:": "Lukittu:", + "Login successful": "Kirjautuminen onnistui", + "Logs": "Lokit", + "Low": "Matala", + "Low Confidence": "Matala luottamus", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Pienemmät numerot näkyvät ensin suodatinsivupalkissa. Jätä tyhjäksi automaattista järjestystä varten.", + "Lucene Version:": "Lucene-versio:", + "Magic table sync completed for {schema}": "Maagisen taulun synkronointi valmistui skeemalle {schema}", + "Magic table synchronized successfully": "Maaginen taulu synkronoitiin onnistuneesti", + "Mail Integration": "Mail-integraatio", + "Mail integration is not available": "Mail-integraatio ei ole käytettävissä", + "Make this installation discoverable within the federation network.": "Tee tästä asennuksesta löydettävä federaatioverkossa.", + "Manage Organisation Roles": "Hallitse organisaation rooleja", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Hallitse SOLR-kokoelmia (datavarastoja) ja määritä ne objekteille ja tiedostoille.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Hallitse kokoelmiesi SOLR-ConfigSetejä (määritysmalleja).", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Hallitse SOLR-kokoelmia (datavarastoja) ja määritä ne objekteille ja tiedostoille.", + "Manage SOLR configsets (configuration templates) for your collections.": "Hallitse kokoelmiesi SOLR-configsetejä (määritysmalleja).", + "Manage and configure agents for automated tasks": "Hallitse ja määritä agentteja automatisoituja tehtäviä varten", + "Manage and monitor file text extraction status": "Hallitse ja seuraa tiedostojen tekstinpoiminnan tilaa", + "Manage and restore soft deleted items from your registers": "Hallitse ja palauta pehmeästi poistettuja kohteita rekistereistäsi", + "Manage and view detected entities from files and objects": "Hallitse ja tarkastele tiedostoista ja objekteista havaittuja entiteettejä", + "Manage document templates and themes": "Hallitse asiakirjamalleja ja teemoja", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Hallitse n8n-työnkulkuja OpenRegister-automaatiota varten. Työnkulut tallennetaan määritettyyn projektiin.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Hallitse käsittelytoimia, suorita rekisteröidyn tarkastuspyyntöjä ja auditoi vaatimustenmukaisuutta EU:n GDPR:n / Alankomaiden AVG:n mukaisesti.", + "Manage webhooks for event-driven integrations": "Hallitse webhookkeja tapahtumapohjaisia integraatioita varten", + "Manage your account settings, security, and personal data.": "Hallitse tilisi asetuksia, tietoturvaa ja henkilötietoja.", + "Manage your applications and modules": "Hallitse sovelluksiasi ja moduulejasi", + "Manage your chat conversations": "Hallitse chat-keskustelujasi", + "Manage your data registers and their configurations": "Hallitse datarekistereitäsi ja niiden määrityksiä", + "Manage your data schemas and their properties": "Hallitse dataskeemojasi ja niiden ominaisuuksia", + "Manage your data sources and their configurations": "Hallitse datalähteitäsi ja niiden määrityksiä", + "Manage your organisations and switch between them": "Hallitse organisaatioitasi ja vaihda niiden välillä", + "Manage your saved search configurations": "Hallitse tallennettuja hakumäärityksiäsi", + "Manage your system configurations and settings": "Hallitse järjestelmämäärityksiäsi ja -asetuksiasi", + "Managed": "Hallittu", + "Mappings": "Kartoitukset", + "Mass validation completed successfully": "Massavalidointi valmistui onnistuneesti", + "Mass validation failed: {error}": "Massavalidointi epäonnistui: {error}", + "Max Files (0 = all)": "Tiedostojen enimmäismäärä (0 = kaikki)", + "Max Length": "Enimmäispituus", + "Max Length:": "Enimmäispituus:", + "Max Objects (0 = all)": "Objektien enimmäismäärä (0 = kaikki)", + "Max Retries": "Uudelleenyritysten enimmäismäärä", + "Max Shards/Node": "Sirpaleiden enimmäismäärä/solmu", + "Max Tokens": "Tokenien enimmäismäärä", + "Max execution time (ms)": "Suoritusajan enimmäisaika (ms)", + "Max ms": "Enint. ms", + "Max result count": "Tulosten enimmäismäärä", + "Max results": "Tulosten enimmäismäärä", + "Maximum": "Enimmäismäärä", + "Maximum File Size (MB)": "Tiedoston enimmäiskoko (MB)", + "Maximum Nesting Depth": "Sisäkkäisyyden enimmäissyvyys", + "Maximum length": "Enimmäispituus", + "Maximum length constraint is missing": "Enimmäispituusrajoite puuttuu", + "Maximum length is too restrictive": "Enimmäispituus on liian rajoittava", + "Maximum number of files to process. Set to 0 to process all files.": "Käsiteltävien tiedostojen enimmäismäärä. Aseta 0 käsitelläksesi kaikki tiedostot.", + "Maximum number of items": "Kohteiden enimmäismäärä", + "Maximum number of objects to process. Set to 0 to process all objects.": "Käsiteltävien objektien enimmäismäärä. Aseta 0 käsitelläksesi kaikki objektit.", + "Maximum number of retry attempts for failed deliveries": "Epäonnistuneiden toimitusten uudelleenyritysten enimmäismäärä", + "Maximum time to wait before committing changes": "Enimmäisaika ennen muutosten vahvistamista", + "Maximum tokens to generate": "Generoitavien tokenien enimmäismäärä", + "Maximum value": "Enimmäisarvo", + "Maximum value constraint is missing": "Enimmäisarvorajoite puuttuu", + "Maximum value is too restrictive": "Enimmäisarvo on liian rajoittava", + "Medium": "Keskitaso", + "Medium Confidence": "Keskitason luottamus", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Keskitason kyselyt: Haut, joissa on jonkin verran suodatusta tai useita parametreja (esim. päivämääräalueet, tietyt rekisterit/skeemat)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Keskitason kyselyt: haut, joissa on jonkin verran suodatusta tai useita parametreja (esim. päivämääräalueet, tietyt rekisterit/skeemat)", + "Member removed successfully": "Jäsen poistettiin onnistuneesti", + "Members": "Jäsenet", + "Members ({count})": "Jäsenet ({count})", + "Members of selected groups can access this view": "Valittujen ryhmien jäsenet voivat käyttää tätä näkymää", + "Members:": "Jäsenet:", + "Memory prediction calculated successfully": "Muistiennuste laskettiin onnistuneesti", + "Message content is required": "Viestin sisältö on pakollinen", + "Message does not belong to this conversation": "Viesti ei kuulu tähän keskusteluun", + "Message not found": "Viestiä ei löytynyt", + "Metadata": "Metatiedot", + "Metadata Filters": "Metatietosuodattimet", + "Method": "Menetelmä", + "Method*": "Menetelmä*", + "Method: {method}": "Menetelmä: {method}", + "Min Length": "Vähimmäispituus", + "Min execution time (ms)": "Suoritusajan vähimmäisaika (ms)", + "Min ms": "Väh. ms", + "Min result count": "Tulosten vähimmäismäärä", + "Min results": "Tulosten vähimmäismäärä", + "Minimum": "Vähimmäismäärä", + "Minimum length": "Vähimmäispituus", + "Minimum number of items": "Kohteiden vähimmäismäärä", + "Minimum value": "Vähimmäisarvo", + "Minimum value constraint is missing": "Vähimmäisarvorajoite puuttuu", + "Minimum value is too restrictive": "Vähimmäisarvo on liian rajoittava", + "Missing conversation or agentUuid": "Puuttuva keskustelu tai agentUuid", + "Missing conversationId": "Puuttuva conversationId", + "Missing message": "Puuttuva viesti", + "Missing object fields created successfully": "Puuttuvat objektin kentät luotiin onnistuneesti", + "Mode:": "Tila:", + "Model": "Malli", + "Model Name": "Mallin nimi", + "Modified": "Muokattu", + "Monthly": "Kuukausittain", + "More sources (10-20):": "Lisää lähteitä (10-20):", + "Most Active Objects": "Aktiivisimmat objektit", + "Multiple Of": "Monikerta", + "Multiple of": "Monikerta", + "Multiple search trail deletion not implemented yet": "Useiden hakujälkien poistoa ei ole vielä toteutettu", + "Multitenancy": "Monivuokralaisuus", + "Multitenancy settings updated successfully": "Monivuokralaisuusasetukset päivitettiin onnistuneesti", + "My Account": "Oma tili", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Nimi", + "Name *": "Nimi *", + "Name*": "Nimi*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Nimivälimuisti esilämmitettiin onnistuneesti: {count} nimeä ladattiin ajassa {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Nimivälimuisti esilämmitettiin onnistuneesti: {count} nimeä ladattiin ajassa {time}. {message}", + "Names cache warmup completed": "Nimivälimuistin esilämmitys valmistui", + "Naming pattern for copies:": "Kopioiden nimeämismalli:", + "New Collection Name": "Uuden kokoelman nimi", + "New Configuration": "Uusi määritys", + "New Conversation": "Uusi keskustelu", + "New Properties": "Uudet ominaisuudet", + "New Property": "Uusi ominaisuus", + "New User Group": "Uusi käyttäjäryhmä", + "New Value": "Uusi arvo", + "New activity": "Uusi toiminta", + "New password": "Uusi salasana", + "New processing activity": "Uusi käsittelytoimi", + "New users without specific organisation membership will be automatically added to this organisation": "Uudet käyttäjät, joilla ei ole tiettyä organisaatiojäsenyyttä, lisätään automaattisesti tähän organisaatioon", + "New verwerkingsactiviteit": "Uusi verwerkingsactiviteit", + "Next": "Seuraava", + "Nextcloud Groups": "Nextcloud-ryhmät", + "No": "Ei", + "No API tokens.": "Ei API-tunnuksia.", + "No ConfigSets found": "ConfigSetejä ei löytynyt", + "No Deck cards linked to this object": "Tähän objektiin ei ole linkitetty Deck-kortteja", + "No GitHub token configured": "GitHub-tunnusta ei ole määritetty", + "No GitHub token provided": "GitHub-tunnusta ei annettu", + "No GitLab token provided": "GitLab-tunnusta ei annettu", + "No Organisation": "Ei organisaatiota", + "No active filters": "Ei aktiivisia suodattimia", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Ei aktiivisia objekteja pehmeästi poistettavaksi skeemalle {schema}. Käytä \"Poista pysyvästi\" poistaaksesi pehmeästi poistetut objektit.", + "No activity data available": "Toimintatietoja ei ole saatavilla", + "No activity found.": "Toimintaa ei löytynyt.", + "No agents available": "Ei käytettävissä olevia agentteja", + "No agents found": "Agentteja ei löytynyt", + "No app store cache exists yet": "App store -välimuistia ei ole vielä olemassa", + "No applications are available.": "Sovelluksia ei ole saatavilla.", + "No applications found": "Sovelluksia ei löytynyt", + "No approval steps for this object.": "Tälle objektille ei ole hyväksyntävaiheita.", + "No archived conversations": "Ei arkistoituja keskusteluja", + "No audit trail entries found": "Auditointijälkien merkintöjä ei löytynyt", + "No changes recorded": "Muutoksia ei kirjattu", + "No chunks to vectorize": "Ei vektoroitavia osia", + "No collections found": "Kokoelmia ei löytynyt", + "No configuration data": "Ei määritystietoja", + "No configuration selected": "Määritystä ei valittu", + "No configurations are available.": "Määrityksiä ei ole saatavilla.", + "No configurations found": "Määrityksiä ei löytynyt", + "No configurations found for this application.": "Tälle sovellukselle ei löytynyt määrityksiä.", + "No contacts linked to this object": "Tähän objektiin ei ole linkitetty yhteystietoja", + "No conversations yet. Create a new one to get started!": "Ei vielä keskusteluja. Luo uusi aloittaaksesi!", + "No dashboards yet": "Ei vielä koontinäyttöjä", + "No data available": "Tietoja ei ole saatavilla", + "No data available for chart": "Kaaviolle ei ole saatavilla tietoja", + "No deleted items found": "Poistettuja kohteita ei löytynyt", + "No deletion data available": "Poistotietoja ei ole saatavilla", + "No description found": "Kuvausta ei löytynyt", + "No description provided": "Kuvausta ei annettu", + "No emails linked to this object": "Tähän objektiin ei ole linkitetty sähköposteja", + "No endpoints defined": "Päätepisteitä ei ole määritetty", + "No entities detected for this email.": "Tälle sähköpostille ei havaittu entiteettejä.", + "No entities detected in this file. Run text extraction first.": "Tässä tiedostossa ei havaittu entiteettejä. Suorita ensin tekstinpoiminta.", + "No entities found": "Entiteettejä ei löytynyt", + "No entities have been detected yet": "Entiteettejä ei ole vielä havaittu", + "No events found": "Tapahtumia ei löytynyt", + "No events linked to this object": "Tähän objektiin ei ole linkitetty tapahtumia", + "No expired audit trails found to clear": "Vanhentuneita auditointijälkiä ei löytynyt tyhjennettäväksi", + "No expired search trails found to clear": "Vanhentuneita hakujälkiä ei löytynyt tyhjennettäväksi", + "No extraction data available for this file": "Tälle tiedostolle ei ole saatavilla poimintatietoja", + "No facetable fields available. Select a register and schema to see available filters.": "Ei fasetoitavia kenttiä saatavilla. Valitse rekisteri ja skeema nähdäksesi käytettävissä olevat suodattimet.", + "No files found": "Tiedostoja ei löytynyt", + "No files have been extracted yet": "Tiedostoja ei ole vielä poimittu", + "No files selected": "Tiedostoja ei valittu", + "No files to process": "Ei käsiteltäviä tiedostoja", + "No files to reindex": "Ei uudelleenindeksoitavia tiedostoja", + "No filters are currently active. This will delete ALL audit trail entries!": "Tällä hetkellä ei ole aktiivisia suodattimia. Tämä poistaa KAIKKI auditointijälkien merkinnät!", + "No groups found. Try a different search.": "Ryhmiä ei löytynyt. Kokeile eri hakua.", + "No label": "Ei otsikkoa", + "No labels": "Ei otsikoita", + "No linked objects": "Ei linkitettyjä objekteja", + "No log entries found": "Lokimerkintöjä ei löytynyt", + "No logs are available for this configuration.": "Tälle määritykselle ei ole saatavilla lokeja.", + "No logs are available for this source.": "Tälle lähteelle ei ole saatavilla lokeja.", + "No logs found": "Lokeja ei löytynyt", + "No matches": "Ei osumia", + "No mismatched fields found - SOLR schema is properly configured": "Yhteensopimattomia kenttiä ei löytynyt - SOLR-skeema on määritetty oikein", + "No objects are currently selected for deletion.": "Tällä hetkellä ei ole valittuna objekteja poistettavaksi.", + "No objects are currently selected for permanent deletion.": "Tällä hetkellä ei ole valittuna objekteja pysyvästi poistettavaksi.", + "No objects are currently selected for restoration.": "Tällä hetkellä ei ole valittuna objekteja palautettavaksi.", + "No objects found": "Objekteja ei löytynyt", + "No objects found to vectorize": "Vektoroitavia objekteja ei löytynyt", + "No objects linked to this email": "Tähän sähköpostiin ei ole linkitetty objekteja", + "No objects selected": "Objekteja ei valittu", + "No objects selected for deletion": "Objekteja ei valittu poistettavaksi", + "No objects selected for restoration": "Objekteja ei valittu palautettavaksi", + "No objects to delete": "Ei poistettavia objekteja", + "No objects to delete for schema {schema}": "Ei poistettavia objekteja skeemalle {schema}", + "No objects were permanently deleted": "Yhtään objektia ei poistettu pysyvästi", + "No organisations found": "Organisaatioita ei löytynyt", + "No personal data was found for this subject identifier.": "Tälle kohdetunnisteelle ei löytynyt henkilötietoja.", + "No processing activities yet": "Ei vielä käsittelytoimia", + "No properties found": "Ominaisuuksia ei löytynyt", + "No properties match your filters.": "Mikään ominaisuus ei vastaa suodattimiasi.", + "No purge date set": "Poistopäivää ei ole asetettu", + "No register data available": "Rekisteritietoja ei ole saatavilla", + "No register objects reference this file": "Mikään rekisteriobjekti ei viittaa tähän tiedostoon", + "No registers found": "Rekistereitä ei löytynyt", + "No registers found for this application.": "Tälle sovellukselle ei löytynyt rekistereitä.", + "No registers found. Create a register to configure permissions.": "Rekistereitä ei löytynyt. Luo rekisteri määrittääksesi käyttöoikeudet.", + "No related cases found for this sender": "Tälle lähettäjälle ei löytynyt liittyviä tapauksia", + "No related entities": "Ei liittyviä entiteettejä", + "No relations found": "Suhteita ei löytynyt", + "No request body available": "Pyynnön runkoa ei ole saatavilla", + "No response body available": "Vastauksen runkoa ei ole saatavilla", + "No saved views yet. Create one in the Search tab!": "Ei vielä tallennettuja näkymiä. Luo sellainen Haku-välilehdellä!", + "No saved views yet. create one in the search tab!": "Ei vielä tallennettuja näkymiä. Luo sellainen hakuvälilehdellä!", + "No scan run yet": "Skannausta ei ole vielä suoritettu", + "No schema data available": "Skeematietoja ei ole saatavilla", + "No schema selected for exploration": "Tutkimista varten ei ole valittu skeemaa", + "No schemas configured for mail linking.": "Sähköpostilinkitykseen ei ole määritetty skeemoja.", + "No schemas found": "Skeemoja ei löytynyt", + "No schemas found for this application.": "Tälle sovellukselle ei löytynyt skeemoja.", + "No search terms data available": "Hakutermitietoja ei ole saatavilla", + "No search trail entries found": "Hakuhistorian merkintöjä ei löytynyt", + "No sources are available.": "Lähteitä ei ole saatavilla.", + "No sources found": "Lähteitä ei löytynyt", + "No synchronizations found": "Synkronointeja ei löytynyt", + "No templates found": "Malleja ei löytynyt", + "No templates have been created yet": "Malleja ei ole vielä luotu", + "No tools available. Tools can be registered by installed apps.": "Työkaluja ei ole saatavilla. Asennetut sovellukset voivat rekisteröidä työkaluja.", + "No users in this organisation.": "Tässä organisaatiossa ei ole käyttäjiä.", + "No valid magic-mapped register+schema combinations found": "Kelvollisia magic-mapped rekisteri+skeema-yhdistelmiä ei löytynyt", + "No views found. Create views first before configuring vectorization.": "Näkymiä ei löytynyt. Luo ensin näkymiä ennen vektorisoinnin määrittämistä.", + "No views match your search": "Mikään näkymä ei vastaa hakuasi", + "No webhooks found": "Webhookkeja ei löytynyt", + "No webhooks have been configured yet": "Webhookkeja ei ole vielä määritetty", + "No workflows found in this project. Create workflows in the n8n editor.": "Tästä projektista ei löytynyt työnkulkuja. Luo työnkulkuja n8n-editorissa.", + "None": "Ei mitään", + "Not Found": "Ei löytynyt", + "Not extracted": "Ei poimittu", + "Not helpful": "Ei hyödyllinen", + "Not in use": "Ei käytössä", + "Note:": "Huomautus:", + "Notifications": "Ilmoitukset", + "Number Constraints": "Numerorajoitukset", + "Number of Sources": "Lähteiden määrä", + "Number of chunks to vectorize in one API call (1-100).": "Yhdessä API-kutsussa vektorisoitavien lohkojen määrä (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Yhdessä API-kutsussa vektorisoitavien lohkojen määrä. Suurempi = nopeampi mutta enemmän muistia. Suositus: 10-50.", + "Number of objects to process in each batch (1-5000).": "Kussakin erässä käsiteltävien objektien määrä (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Yhdessä API-kutsussa vektorisoitavien objektien määrä. Suurempi = nopeampi mutta enemmän muistia. Suositus: 10-50.", + "Numeric Range:": "Numeroalue:", + "Object": "Objekti", + "Object #{id}": "Objekti #{id}", + "Object Collection": "Objektikokoelma", + "Object Configuration:": "Objektin määritykset:", + "Object Count Prediction": "Objektimäärän ennuste", + "Object Extraction Mode": "Objektin poimintatila", + "Object ID": "Objektin tunnus", + "Object Management": "Objektien hallinta", + "Object Text Extraction": "Objektin tekstin poiminta", + "Object Vectorization": "Objektin vektorisointi", + "Object Warmup": "Objektin esilämmitys", + "Object changes in owned objects": "Objektimuutokset omistetuissa objekteissa", + "Object collection not configured": "Objektikokoelmaa ei ole määritetty", + "Object metadata incomplete for file upload": "Objektin metatiedot ovat puutteelliset tiedoston lataamista varten", + "Object not found": "Objektia ei löytynyt", + "Object permanently deleted": "Objekti poistettu pysyvästi", + "Object restored successfully": "Objekti palautettu onnistuneesti", + "Object settings updated successfully": "Objektin asetukset päivitetty onnistuneesti", + "Object sources": "Objektin lähteet", + "Object successfully deleted": "Objekti poistettu onnistuneesti", + "Object successfully modified": "Objekti muokattu onnistuneesti", + "Object unlocked successfully": "Objektin lukitus avattu onnistuneesti", + "Object vectorization configuration saved successfully": "Objektin vektorisoinnin määritykset tallennettu onnistuneesti", + "Object vectorized successfully": "Objekti vektorisoitu onnistuneesti", + "Object:": "Objekti:", + "Objects": "Objektit", + "Objects Analyzed": "Analysoidut objektit", + "Objects Distribution": "Objektien jakauma", + "Objects being analyzed": "Analysoitavat objektit", + "Objects by Register": "Objektit rekisterin mukaan", + "Objects by Schema": "Objektit skeeman mukaan", + "Objects deletion completed for {schema}": "Objektien poisto valmistui skeemalle {schema}", + "Objects deletion completed successfully": "Objektien poisto valmistui onnistuneesti", + "Objects in schema": "Objektit skeemassa", + "Objects successfully deleted": "Objektit poistettu onnistuneesti", + "Objects to Process:": "Käsiteltävät objektit:", + "Objects to be analyzed": "Analysoitavat objektit", + "Objects to be deleted": "Poistettavat objektit", + "Objects to be validated": "Validoitavat objektit", + "Objects will be serialized as JSON text before vectorization": "Objektit serialisoidaan JSON-tekstiksi ennen vektorisointia", + "Objects will be soft deleted and moved to the": "Objektit poistetaan pehmeästi ja siirretään kohteeseen", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objektit poistetaan pehmeästi (merkitään poistetuiksi mutta säilytetään tietokannassa). Ne voidaan palauttaa myöhemmin tarvittaessa.", + "Old Value": "Vanha arvo", + "Oldest Item (days)": "Vanhin kohde (päivää)", + "Ollama Chat Settings": "Ollama-keskusteluasetukset", + "Ollama Embedding Configuration": "Ollama-upotusmääritykset", + "Ollama URL": "Ollama URL", + "Only With Changes": "Vain muutoksilla", + "Only members of selected groups can access this organisation": "Vain valittujen ryhmien jäsenet voivat käyttää tätä organisaatiota", + "Open Nextcloud App Store": "Avaa Nextcloud App Store", + "Open OpenRegister sidebar": "Avaa OpenRegister-sivupalkki", + "Open Register Objects": "Avaa rekisteriobjektit", + "Open folder": "Avaa kansio", + "Open in OpenRegister": "Avaa OpenRegisterissä", + "Open n8n Editor": "Avaa n8n-editori", + "OpenAI Chat Settings": "OpenAI-keskusteluasetukset", + "OpenAI Embedding Configuration": "OpenAI-upotusmääritykset", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister-asetukset", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operaattorin määrittämät koontinäytöt ja ajastetut raportit. Jokainen koontinäyttö on ensiluokkainen objekti `reports`-rekisterissä; widgetit määritellään koontinäytön `widgets`-taulukossa ja renderöidään reaaliaikaisesti aggregaatioista / GraphQL:stä.", + "Optimizing search performance...": "Optimoidaan haun suorituskykyä...", + "Optional URL-friendly identifier": "Valinnainen URL-ystävällinen tunniste", + "Optional description for this facet": "Valinnainen kuvaus tälle fasetille", + "Optional end date property": "Valinnainen päättymispäivän ominaisuus", + "Optional event description template": "Valinnainen tapahtuman kuvausmalli", + "Optional location property": "Valinnainen sijainnin ominaisuus", + "Optional webhook secret for signature verification": "Valinnainen webhook-salaisuus allekirjoituksen vahvistamiseen", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Valinnainen: Lisää kyselyparametrit suodattaaksesi viitattua skeemaa (esim. status=active&type=public)", + "Or": "Tai", + "Order": "Järjestys", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Järjestetyt BCP 47 -kielitunnisteet. Ensimmäinen kieli on rekisterin oletuskieli ja ohjaa Accept-Language-varajärjestelmää käännettäville ominaisuuksille.", + "Organisation": "Organisaatio", + "Organisation Configuration": "Organisaation määritykset", + "Organisation ID": "Organisaation tunnus", + "Organisation Members": "Organisaation jäsenet", + "Organisation Statistics": "Organisaation tilastot", + "Organisation created successfully": "Organisaatio luotu onnistuneesti", + "Organisation membership changes": "Organisaation jäsenyysmuutokset", + "Organisation name is required": "Organisaation nimi vaaditaan", + "Organisation settings saved successfully": "Organisaation asetukset tallennettu onnistuneesti", + "Organisation successfully created": "Organisaatio luotu onnistuneesti", + "Organisation successfully updated": "Organisaatio päivitetty onnistuneesti", + "Organisational measures": "Organisatoriset toimenpiteet", + "Organisations": "Organisaatiot", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organisaatio", + "Organization ID (Optional)": "Organisaation tunnus (valinnainen)", + "Organizations": "Organisaatiot", + "Orphaned Items": "Orvot kohteet", + "Other": "Muu", + "Output Mapping": "Lähtökartoitus", + "Overview": "Yleiskatsaus", + "Owner": "Omistaja", + "Owner:": "Omistaja:", + "PDF": "PDF", + "PII hits": "PII-osumat", + "PII rows": "PII-rivit", + "Page {current} of {total}": "Sivu {current} / {total}", + "Parallel Mode (Faster, more resource intensive)": "Rinnakkaistila (nopeampi, enemmän resursseja vaativa)", + "Parallel:": "Rinnakkainen:", + "Password": "Salasana", + "Password changes are not supported by your authentication provider.": "Todennuksen tarjoajasi ei tue salasanan muutoksia.", + "Password for SOLR authentication (optional)": "Salasana SOLR-todennukseen (valinnainen)", + "Password updated successfully": "Salasana päivitetty onnistuneesti", + "Path": "Polku", + "Path in repository": "Polku tietovarastossa", + "Path where the configuration file will be saved in the repository": "Polku, johon määritystiedosto tallennetaan tietovarastossa", + "Path where the register OAS file will be saved in the repository": "Polku, johon rekisterin OAS-tiedosto tallennetaan tietovarastossa", + "Pattern (regex)": "Kuvio (regex)", + "Pattern Issue": "Kuvio-ongelma", + "Pattern constraint is missing": "Kuviorajoitus puuttuu", + "Patterns:": "Kuviot:", + "Pending": "Odottaa", + "Permanent Delete Mode": "Pysyvän poiston tila", + "Permanently Delete": "Poista pysyvästi", + "Permanently Delete ({count})": "Poista pysyvästi ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Poista pysyvästi kaikki {count} pehmeästi poistettua objektia. Tätä ei voi peruuttaa!", + "Permanently delete already soft-deleted objects": "Poista pysyvästi jo pehmeästi poistetut objektit", + "Permanently delete this conversation?": "Poistetaanko tämä keskustelu pysyvästi?", + "Permission Matrix": "Käyttöoikeusmatriisi", + "Person": "Henkilö", + "Personal Data": "Henkilötiedot", + "Personal Data Export": "Henkilötietojen vienti", + "Persons": "Henkilöt", + "Phone": "Puhelin", + "Phone Numbers": "Puhelinnumerot", + "Photos": "Valokuvat", + "Please create an agent in the": "Luo agentti kohteessa", + "Please select a user": "Valitse käyttäjä", + "Please select an agent to continue": "Valitse agentti jatkaaksesi", + "Please select an organisation": "Valitse organisaatio", + "Please select at least one item to import": "Valitse vähintään yksi tuotava kohde", + "Please select registers or schemas first": "Valitse ensin rekisterit tai skeemat", + "Please select which register and schema to use for the new object": "Valitse mitä rekisteriä ja skeemaa käytetään uudelle objektille", + "Please try again later.": "Yritä myöhemmin uudelleen.", + "Please wait while we fetch your agents.": "Odota, kun haemme agenttejasi.", + "Please wait while we fetch your applications.": "Odota, kun haemme sovelluksiasi.", + "Please wait while we fetch your configurations.": "Odota, kun haemme määrityksiäsi.", + "Please wait while we fetch your deleted items.": "Odota, kun haemme poistettuja kohteitasi.", + "Please wait while we fetch your sources.": "Odota, kun haemme lähteitäsi.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Odota, kun poistamme SOLR-kokoelman pysyvästi. Tämä voi kestää hetken.", + "Polls": "Kyselyt", + "Popular Search Terms": "Suositut hakutermit", + "Port": "Portti", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Asetukset tallennettu", + "Prerequisites": "Edellytykset", + "Press Enter to send, Shift+Enter for new line": "Paina Enter lähettääksesi, Shift+Enter lisätäksesi uuden rivin", + "Press enter to send, shift+enter for new line": "Paina enter lähettääksesi, shift+enter lisätäksesi uuden rivin", + "Prettify": "Kaunista", + "Preview erasure": "Esikatsele poistoa", + "Preview:": "Esikatselu:", + "Previous": "Edellinen", + "Private": "Yksityinen", + "Private Agent (Default)": "Yksityinen agentti (oletus)", + "Private agents": "Yksityiset agentit", + "Process Name (optional)": "Prosessin nimi (valinnainen)", + "Processed {indexed} files, {failed} failed": "Käsitelty {indexed} tiedostoa, {failed} epäonnistui", + "Processes chunks in batches with simulated parallelism.": "Käsittelee lohkoja erissä simuloidulla rinnakkaisuudella.", + "Processes file chunks sequentially (safest).": "Käsittelee tiedostolohkot peräkkäin (turvallisin).", + "Processes objects in chunks with simulated parallelism.": "Käsittelee objektit lohkoissa simuloidulla rinnakkaisuudella.", + "Processes objects sequentially (safest).": "Käsittelee objektit peräkkäin (turvallisin).", + "Processing": "Käsitellään", + "Processing Limits": "Käsittelyrajoitukset", + "Processing...": "Käsitellään...", + "Project initialized successfully": "Projekti alustettu onnistuneesti", + "Project not found. Please initialize first.": "Projektia ei löytynyt. Alusta ensin.", + "Properties": "Ominaisuudet", + "Property Behaviors": "Ominaisuuden käyttäytyminen", + "Property Configuration:": "Ominaisuuden määritykset:", + "Property Filters": "Ominaisuussuodattimet", + "Property Title": "Ominaisuuden otsikko", + "Property Type": "Ominaisuuden tyyppi", + "Property can be improved": "Ominaisuutta voidaan parantaa", + "Property could not be added": "Ominaisuutta ei voitu lisätä", + "Property could not be updated": "Ominaisuutta ei voitu päivittää", + "Property name of inversed relation": "Käänteisen suhteen ominaisuuden nimi", + "Property successfully added": "Ominaisuus lisätty onnistuneesti", + "Property successfully updated": "Ominaisuus päivitetty onnistuneesti", + "Protected": "Suojattu", + "Provider": "Tarjoaja", + "Provider is required for testing": "Tarjoaja vaaditaan testaamista varten", + "Public": "Julkinen", + "Public View": "Julkinen näkymä", + "Public views can be accessed by anyone in the system": "Julkisia näkymiä voi käyttää kuka tahansa järjestelmässä", + "Publish": "Julkaise", + "Publish Configuration to GitHub": "Julkaise määritykset GitHubiin", + "Publish OAS": "Julkaise OAS", + "Publish Objects": "Julkaise objektit", + "Publish Register OAS to GitHub": "Julkaise rekisterin OAS GitHubiin", + "Publish all objects in this schema": "Julkaise kaikki tämän skeeman objektit", + "Publish objects to make them publicly available": "Julkaise objektit tehdäksesi niistä julkisesti saatavilla olevia", + "Published": "Julkaistu", + "Published:": "Julkaistu:", + "Publishing...": "Julkaistaan...", + "Purge": "Tyhjennä", + "Purge Date": "Tyhjennyspäivä", + "Purge {count} object from database": "Tyhjennä {count} objekti tietokannasta", + "Purge {count} objects from database": "Tyhjennä {count} objektia tietokannasta", + "Purpose limitation *": "Käyttötarkoituksen rajoittaminen *", + "Push Notifications": "Push-ilmoitukset", + "Query Complexity Distribution": "Kyselyn monimutkaisuuden jakauma", + "Query Help": "Kyselyohje", + "Query parameter is required": "Kyselyparametri vaaditaan", + "Quota": "Kiintiö", + "RAG": "RAG", + "RAG Configuration": "RAG-määritykset", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC-asetukset päivitetty onnistuneesti", + "Rate limits cleared successfully": "Nopeusrajoitukset tyhjennetty onnistuneesti", + "Raw Changes Data": "Raakamuutostiedot", + "Re-vectorize on object update": "Vektorisoi uudelleen objektin päivityksen yhteydessä", + "Read": "Lue", + "Real-time push notification status via notify_push": "Reaaliaikaisen push-ilmoituksen tila notify_push-sovelluksen kautta", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Reaaliaikaiset push-ilmoitukset ovat aktiivisia. Yhdistetyt asiakkaat saavat välittömiä päivityksiä, kun objekteja luodaan, päivitetään tai poistetaan.", + "Realtime push active": "Reaaliaikainen push aktiivinen", + "Realtime push not available — the notify_push app is not installed": "Reaaliaikainen push ei ole saatavilla — notify_push-sovellusta ei ole asennettu", + "Reason": "Syy", + "Reason (optional)": "Syy (valinnainen)", + "Rebase operation completed successfully": "Rebase-toiminto valmistui onnistuneesti", + "Rebase operation failed: {error}": "Rebase-toiminto epäonnistui: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Suositukset:", + "Recommended Type:": "Suositeltu tyyppi:", + "Recommended: 5 sources": "Suositus: 5 lähdettä", + "Refresh": "Päivitä", + "Refresh Data": "Päivitä tiedot", + "Refresh Stats": "Päivitä tilastot", + "Refresh Workflows": "Päivitä työnkulut", + "Refresh dashboard": "Päivitä koontinäyttö", + "Refresh database info": "Päivitä tietokantatiedot", + "Register": "Rekisteri", + "Register #{id}": "Rekisteri #{id}", + "Register / Schema": "Rekisteri / Skeema", + "Register ID": "Rekisterin tunnus", + "Register Objects": "Rekisteriobjektit", + "Register Overview": "Rekisterin yleiskatsaus", + "Register Statistics": "Rekisterin tilastot", + "Register Totals": "Rekisterin kokonaismäärät", + "Register and schema are required": "Rekisteri ja skeema vaaditaan", + "Register annotation": "Rekisterin annotaatio", + "Register depublished successfully": "Rekisterin julkaisu peruttu onnistuneesti", + "Register not found": "Rekisteriä ei löytynyt", + "Register objects": "Rekisteriobjektit", + "Register objects deletion completed successfully": "Rekisteriobjektien poisto valmistui onnistuneesti", + "Register published successfully": "Rekisteri julkaistu onnistuneesti", + "Register/Schema Usage": "Rekisterin/Skeeman käyttö", + "Register:": "Rekisteri:", + "Registers": "Rekisterit", + "Reindex": "Indeksoi uudelleen", + "Reindex completed": "Uudelleenindeksointi valmistui", + "Reindex completed successfully": "Uudelleenindeksointi valmistui onnistuneesti", + "Reindex completed! Processed {count} objects in {duration}s": "Uudelleenindeksointi valmistui! Käsitelty {count} objektia ajassa {duration}s", + "Reindex failed": "Uudelleenindeksointi epäonnistui", + "Reindex failed: %s": "Uudelleenindeksointi epäonnistui: %s", + "Reject": "Hylkää", + "Rejected": "Hylätty", + "Related Cases": "Liittyvät tapaukset", + "Relations": "Suhteet", + "Remote Version": "Etäversio", + "Remove": "Poista", + "Remove Deck card": "Poista Deck-kortti", + "Remove avatar": "Poista avatar", + "Remove contact": "Poista yhteystieto", + "Remove filter": "Poista suodatin", + "Remove from favorites": "Poista suosikeista", + "Remove from list": "Poista listalta", + "Remove group": "Poista ryhmä", + "Remove link": "Poista linkki", + "Remove link to {name}": "Poista linkki kohteeseen {name}", + "Remove link to {name}?": "Poistetaanko linkki kohteeseen {name}?", + "Remove link to {title}": "Poista linkki kohteeseen {title}", + "Remove oneOf entry": "Poista oneOf-merkintä", + "Remove property after write-back": "Poista ominaisuus takaisinkirjoituksen jälkeen", + "Remove {title}": "Poista {title}", + "Remove {userId} from this organisation?": "Poistetaanko {userId} tästä organisaatiosta?", + "Removed": "Poistettu", + "Removed from favorites": "Poistettu suosikeista", + "Removing schema {schema}...": "Poistetaan skeemaa {schema}...", + "Rename Conversation": "Nimeä keskustelu uudelleen", + "Rename conversation": "Nimeä keskustelu uudelleen", + "Replicas": "Replikat", + "Reports": "Raportit", + "Repository": "Tietovarasto", + "Request Body": "Pyynnön runko", + "Request Data": "Pyyntötiedot", + "Request Details": "Pyynnön tiedot", + "Request Quota (per day)": "Pyyntökiintiö (päivässä)", + "Request account deactivation": "Pyydä tilin poistamista käytöstä", + "Request account deactivation. This will notify administrators for review.": "Pyydä tilin poistamista käytöstä. Tämä ilmoittaa ylläpitäjille tarkistusta varten.", + "Request body must be JSON with a \"query\" field": "Pyynnön rungon on oltava JSON, jossa on \"query\"-kenttä", + "Request timeout in seconds": "Pyynnön aikakatkaisu sekunneissa", + "Requested at": "Pyydetty", + "Required": "Pakollinen", + "Required Token Scopes": "Vaaditut token-laajuudet", + "Required field": "Pakollinen kenttä", + "Required status is inconsistent": "Pakollisuustila on epäjohdonmukainen", + "Requires Dolphin AI for OCR text extraction": "Vaatii Dolphin AI:n OCR-tekstin poimintaan", + "Rerun Search": "Suorita haku uudelleen", + "Rerunning search: {searchTerm}": "Suoritetaan hakua uudelleen: {searchTerm}", + "Reset Changes": "Nollaa muutokset", + "Reset Filters": "Nollaa suodattimet", + "Resource Quotas": "Resurssikiintiöt", + "Response Body": "Vastauksen runko", + "Response Details": "Vastauksen tiedot", + "Response Time:": "Vastausaika:", + "Restore": "Palauta", + "Restore conversation": "Palauta keskustelu", + "Restore or permanently delete items": "Palauta tai poista kohteet pysyvästi", + "Restore {count} object": "Palauta {count} objekti", + "Restore {count} objects": "Palauta {count} objektia", + "Result Count Range": "Tulosmäärän alue", + "Results": "Tulokset", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Säilytysaika (ISO-8601-kesto, esim. P10Y, P30D)", + "Retention settings updated successfully": "Säilytysasetukset päivitetty onnistuneesti", + "Retrieve all objects for this schema": "Hae kaikki tämän skeeman objektit", + "Retry": "Yritä uudelleen", + "Retry Failed Extractions": "Yritä epäonnistuneita poimintoja uudelleen", + "Retry Policy": "Uudelleenyrityskäytäntö", + "Retry completed": "Uudelleenyritys valmistui", + "Retry upload": "Yritä latausta uudelleen", + "Retrying...": "Yritetään uudelleen...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Tarkista valitut objektit alla. Voit poistaa objekteja, joita et halua poistaa, napsauttamalla poistopainiketta.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Tarkista valitut objektit alla. Voit poistaa objekteja, joita et halua sisällyttää.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Tarkista valitut objektit alla. Voit poistaa objekteja, joita et halua poistaa pysyvästi, napsauttamalla poistopainiketta. Tätä toimintoa ei voi peruuttaa.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Tarkista valitut objektit alla. Voit poistaa objekteja, joita et halua palauttaa, napsauttamalla poistopainiketta. Objektit palautetaan alkuperäiseen sijaintiinsa.", + "Revoke": "Peruuta", + "Right": "Oikea", + "Risk Level": "Riskitaso", + "Risk level": "Riskitaso", + "Roles and Permissions": "Roolit ja käyttöoikeudet", + "Roles updated successfully": "Roolit päivitetty onnistuneesti", + "Run compliance scan": "Suorita vaatimustenmukaisuusskannaus", + "SOLR Connection Settings": "SOLR-yhteysasetukset", + "SOLR Index Warmup": "SOLR-indeksin esilämmitys", + "SOLR Version:": "SOLR-versio:", + "SOLR actions menu": "SOLR-toimintovalikko", + "SOLR base path (usually /solr)": "SOLR-peruspolku (yleensä /solr)", + "SOLR collection deleted successfully": "SOLR-kokoelma poistettu onnistuneesti", + "SOLR connection test failed: {error}": "SOLR-yhteystesti epäonnistui: {error}", + "SOLR connection test successful!": "SOLR-yhteystesti onnistui!", + "SOLR index warmup completed successfully": "SOLR-indeksin esilämmitys valmistui onnistuneesti", + "SOLR is disabled": "SOLR on poistettu käytöstä", + "SOLR is not available or not configured": "SOLR ei ole saatavilla tai sitä ei ole määritetty", + "SOLR logging disabled": "SOLR-lokitus poistettu käytöstä", + "SOLR logging enabled": "SOLR-lokitus otettu käyttöön", + "SOLR search disabled": "SOLR-haku poistettu käytöstä", + "SOLR search enabled": "SOLR-haku otettu käyttöön", + "SOLR server hostname or IP address": "SOLR-palvelimen isäntänimi tai IP-osoite", + "SOLR server port number (optional, defaults to 8983)": "SOLR-palvelimen porttinumero (valinnainen, oletus 8983)", + "SOLR settings updated successfully": "SOLR-asetukset päivitetty onnistuneesti", + "SOLR setup completed successfully": "SOLR-asennus valmistui onnistuneesti", + "SOLR setup completed successfully!": "SOLR-asennus valmistui onnistuneesti!", + "SOLR setup error: %s": "SOLR-asennusvirhe: %s", + "SOLR setup failed": "SOLR-asennus epäonnistui", + "SOLR setup failed - check logs": "SOLR-asennus epäonnistui - tarkista lokit", + "SOLR setup failed. Please check the configuration and try again.": "SOLR-asennus epäonnistui. Tarkista määritykset ja yritä uudelleen.", + "SOLR setup failed: {error}": "SOLR-asennus epäonnistui: {error}", + "SOLR warmup failed: {error}": "SOLR-esilämmitys epäonnistui: {error}", + "Sample Values:": "Näytearvot:", + "Save": "Tallenna", + "Save Configuration": "Tallenna kokoonpano", + "Save Connection Settings": "Tallenna yhteysasetukset", + "Save Roles": "Tallenna roolit", + "Save Settings": "Tallenna asetukset", + "Save changes": "Tallenna muutokset", + "Save current search as view": "Tallenna nykyinen haku näkymänä", + "Save labels": "Tallenna nimikkeet", + "Save tags for {name}": "Tallenna tunnisteet kohteelle {name}", + "Save the organisation first to manage users.": "Tallenna organisaatio ensin hallitaksesi käyttäjiä.", + "Saved Views": "Tallennetut näkymät", + "Saving...": "Tallennetaan...", + "Scheduled": "Ajastettu", + "Schema": "Skeema", + "Schema ID": "Skeeman tunnus", + "Schema Name": "Skeeman nimi", + "Schema Statistics": "Skeeman tilastot", + "Schema annotation": "Skeeman merkintä", + "Schema configuration updated successfully for {schema}": "Skeeman kokoonpano päivitettiin onnistuneesti kohteelle {schema}", + "Schema depublished successfully": "Skeeman julkaisu peruttiin onnistuneesti", + "Schema objects deletion completed successfully": "Skeeman objektien poisto suoritettiin onnistuneesti", + "Schema published successfully": "Skeema julkaistiin onnistuneesti", + "Schema reference ($ref)": "Skeemaviite ($ref)", + "Schema successfully created": "Skeema luotiin onnistuneesti", + "Schema successfully updated": "Skeema päivitettiin onnistuneesti", + "Schema {schema} removed successfully": "Skeema {schema} poistettiin onnistuneesti", + "Schema:": "Skeema:", + "Schemas": "Skeemat", + "Scheme": "Kaava", + "Search": "Haku", + "Search / Views": "Haku / Näkymät", + "Search / views": "Haku / näkymät", + "Search Activity": "Hakutoiminta", + "Search Analytics": "Hakuanalytiikka", + "Search Configuration": "Hakukokoonpano", + "Search Entities": "Hae entiteettejä", + "Search Files": "Hae tiedostoja", + "Search GitHub": "Hae GitHubista", + "Search GitLab": "Hae GitLabista", + "Search Method": "Hakumenetelmä", + "Search Mode": "Hakutila", + "Search Query": "Hakukysely", + "Search Results": "Hakutulokset", + "Search Term": "Hakutermi", + "Search Trail Details": "Hakujäljen tiedot", + "Search Trail Management": "Hakujälkien hallinta", + "Search Trail Statistics": "Hakujäljen tilastot", + "Search Trails": "Hakujäljet", + "Search Views": "Hakunäkymät", + "Search Webhooks": "Hae webhookeja", + "Search agents...": "Hae agentteja...", + "Search applications...": "Hae sovelluksia...", + "Search by file name or path": "Hae tiedostonimen tai polun mukaan", + "Search by name or URL": "Hae nimen tai URL:n mukaan", + "Search by title or UUID...": "Hae otsikon tai UUID:n mukaan...", + "Search by value": "Hae arvon mukaan", + "Search configurations": "Hae kokoonpanoja", + "Search data sources...": "Hae tietolähteitä...", + "Search groups...": "Hae ryhmiä...", + "Search in Database Objects": "Hae tietokantaobjekteista", + "Search in Files": "Hae tiedostoista", + "Search in Objects": "Hae objekteista", + "Search objects": "Hae objekteja", + "Search objects...": "Hae objekteja...", + "Search property names...": "Hae ominaisuuksien nimiä...", + "Search registers...": "Hae rekistereitä...", + "Search schemas...": "Hae skeemoja...", + "Search trail deletion not implemented yet": "Hakujäljen poistoa ei ole vielä toteutettu", + "Search views...": "Hae näkymiä...", + "Search {name}...": "Hae {name}...", + "Searching...": "Haetaan...", + "Secret": "Salaisuus", + "Secret key for HMAC signature generation (optional)": "Salainen avain HMAC-allekirjoituksen luomiseen (valinnainen)", + "Security": "Suojaus", + "See organisations as a table": "Näytä organisaatiot taulukkona", + "See organisations as cards": "Näytä organisaatiot kortteina", + "Select AI Agent": "Valitse tekoälyagentti", + "Select Active Organisation": "Valitse aktiivinen organisaatio", + "Select All": "Valitse kaikki", + "Select ConfigSet": "Valitse ConfigSet", + "Select File Types to Vectorize:": "Valitse vektoroitavat tiedostotyypit:", + "Select HTTP method": "Valitse HTTP-menetelmä", + "Select Register and Schema": "Valitse rekisteri ja skeema", + "Select Views to Vectorize:": "Valitse vektoroitavat näkymät:", + "Select a Nextcloud group to add": "Valitse lisättävä Nextcloud-ryhmä", + "Select a branch": "Valitse haara", + "Select a date property": "Valitse päivämääräominaisuus", + "Select a model or type a custom model name": "Valitse malli tai kirjoita mukautettu mallin nimi", + "Select a register": "Valitse rekisteri", + "Select a register...": "Valitse rekisteri...", + "Select a repository": "Valitse arkisto", + "Select a repository you have write access to": "Valitse arkisto, johon sinulla on kirjoitusoikeus", + "Select a schema": "Valitse skeema", + "Select a schema...": "Valitse skeema...", + "Select a user": "Valitse käyttäjä", + "Select agent type": "Valitse agentin tyyppi", + "Select all entities": "Valitse kaikki entiteetit", + "Select an AI Agent": "Valitse tekoälyagentti", + "Select an AI agent to begin chatting with your data.": "Valitse tekoälyagentti aloittaaksesi keskustelun tietojesi kanssa.", + "Select an email to see linked objects": "Valitse sähköposti nähdäksesi linkitetyt objektit", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Valitse organisaatio ja käyttäjä lisätäksesi heidät jäseneksi. Hae organisaatioita nimen mukaan.", + "Select backend": "Valitse taustajärjestelmä", + "Select chat model": "Valitse keskustelumalli", + "Select collection for files": "Valitse kokoelma tiedostoille", + "Select collection for objects": "Valitse kokoelma objekteille", + "Select configuration type...": "Valitse kokoonpanon tyyppi...", + "Select default organisation": "Valitse oletusorganisaatio", + "Select entity {value}": "Valitse entiteetti {value}", + "Select event to listen to...": "Valitse kuunneltava tapahtuma...", + "Select fields to display": "Valitse näytettävät kentät", + "Select files with the correct extension": "Valitse tiedostot, joilla on oikea tiedostopääte", + "Select group": "Valitse ryhmä", + "Select groups (optional)": "Valitse ryhmät (valinnainen)", + "Select groups to notify...": "Valitse ilmoitettavat ryhmät...", + "Select groups with access to this agent": "Valitse ryhmät, joilla on pääsy tähän agenttiin", + "Select groups...": "Valitse ryhmät...", + "Select model": "Valitse malli", + "Select one or more registers": "Valitse yksi tai useampi rekisteri", + "Select one or more schemas": "Valitse yksi tai useampi skeema", + "Select options...": "Valitse vaihtoehdot...", + "Select or create labels, or select \"No label\" to add files": "Valitse tai luo nimikkeitä tai valitse \"Ei nimikettä\" lisätäksesi tiedostoja", + "Select owner application (optional)...": "Valitse omistajasovellus (valinnainen)...", + "Select period": "Valitse ajanjakso", + "Select property to send as payload": "Valitse hyötykuormana lähetettävä ominaisuus", + "Select provider": "Valitse palveluntarjoaja", + "Select registers and schemas to save a view": "Valitse rekisterit ja skeemat tallentaaksesi näkymän", + "Select retry policy": "Valitse uudelleenyrityskäytäntö", + "Select role": "Valitse rooli", + "Select schemas (empty = all schemas)": "Valitse skeemat (tyhjä = kaikki skeemat)", + "Select schemas to warm up": "Valitse esilämmitettävät skeemat", + "Select search mode": "Valitse hakutila", + "Select source type...": "Valitse lähdetyyppi...", + "Select target type": "Valitse kohdetyyppi", + "Select the branch to publish to": "Valitse haara, johon julkaistaan", + "Select the event this webhook should listen to": "Valitse tapahtuma, jota tämän webhookin tulisi kuunnella", + "Select views to filter data (optional)": "Valitse näkymät tietojen suodattamiseksi (valinnainen)", + "Select views to limit which data the agent can access": "Valitse näkymät rajoittaaksesi, mihin tietoihin agentilla on pääsy", + "Select views to vectorize:": "Valitse vektoroitavat näkymät:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Valitse, mitkä Nextcloud-ryhmät ovat käytettävissä tälle organisaatiolle. Näiden ryhmien käyttäjillä on pääsy organisaation resursseihin.", + "Select which data views the AI can search": "Valitse, mitä tietonäkymiä tekoäly voi hakea", + "Select which property from the event should be used as the webhook payload data": "Valitse, mitä tapahtuman ominaisuutta tulisi käyttää webhookin hyötykuormatietona", + "Select which tools the AI can use to perform actions": "Valitse, mitä työkaluja tekoäly voi käyttää toimintojen suorittamiseen", + "Select {fieldName} values": "Valitse {fieldName}-arvot", + "Selected": "Valittu", + "Selected Groups": "Valitut ryhmät", + "Selected Objects ({count})": "Valitut objektit ({count})", + "Selected audit trails deleted successfully": "Valitut tarkastusjäljet poistettiin onnistuneesti", + "Selected search trails deleted successfully": "Valitut hakujäljet poistettiin onnistuneesti", + "Selected users can access this view": "Valituilla käyttäjillä on pääsy tähän näkymään", + "Send additional feedback": "Lähetä lisäpalautetta", + "Send as CloudEvent": "Lähetä CloudEventina", + "Send as cloudevent": "Lähetä cloudeventina", + "Sensitive PII": "Arkaluontoinen PII", + "Serial Mode (Safer, slower)": "Sarjatila (turvallisempi, hitaampi)", + "Serial:": "Sarja:", + "Server Configuration": "Palvelimen kokoonpano", + "Server Information": "Palvelimen tiedot", + "Set as Active": "Aseta aktiiviseksi", + "Set as active organisation": "Aseta aktiiviseksi organisaatioksi", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Aseta rajat API-käytölle ja tokenien kulutukselle. Käytä arvoa 0 rajoittamattomille resursseille.", + "Settings": "Asetukset", + "Settings updated successfully": "Asetukset päivitettiin onnistuneesti", + "Shards": "Shardit", + "Share": "Jaa", + "Share with Groups": "Jaa ryhmien kanssa", + "Share with Users": "Jaa käyttäjien kanssa", + "Show": "Näytä", + "Show Filters": "Näytä suodattimet", + "Show less": "Näytä vähemmän", + "Show only entries with changes": "Näytä vain merkinnät, joissa on muutoksia", + "Showing {count} dashboard(s)": "Näytetään {count} koontinäyttöä", + "Showing {count} processing activities": "Näytetään {count} käsittelytoimintoa", + "Showing {showing} of {total} agents": "Näytetään {showing}/{total} agentista", + "Showing {showing} of {total} applications": "Näytetään {showing}/{total} sovelluksesta", + "Showing {showing} of {total} audit trail entries": "Näytetään {showing}/{total} tarkastusjälkimerkinnästä", + "Showing {showing} of {total} configurations": "Näytetään {showing}/{total} kokoonpanosta", + "Showing {showing} of {total} deleted items": "Näytetään {showing}/{total} poistetusta kohteesta", + "Showing {showing} of {total} entities": "Näytetään {showing}/{total} entiteetistä", + "Showing {showing} of {total} files": "Näytetään {showing}/{total} tiedostosta", + "Showing {showing} of {total} log entries": "Näytetään {showing}/{total} lokimerkinnästä", + "Showing {showing} of {total} search trail entries": "Näytetään {showing}/{total} hakujälkimerkinnästä", + "Showing {showing} of {total} sources": "Näytetään {showing}/{total} lähteestä", + "Showing {showing} of {total} templates": "Näytetään {showing}/{total} mallista", + "Showing {showing} of {total} webhooks": "Näytetään {showing}/{total} webhookista", + "Showing {shown} of {total} organisations": "Näytetään {shown}/{total} organisaatiosta", + "Simple": "Yksinkertainen", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Yksinkertaiset kyselyt: Perustekstihaut vähäisillä parametreilla (esim. yksittäinen hakutermi, ei kehittyneitä suodattimia)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Yksinkertaiset kyselyt: perustekstihaut vähäisillä parametreilla (esim. yksittäinen hakutermi, ei kehittyneitä suodattimia)", + "Size": "Koko", + "Slug": "Slug", + "Soft Delete Mode": "Pehmeän poiston tila", + "Soft Deleted Items": "Pehmeästi poistetut kohteet", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Poista pehmeästi kaikki tämän skeeman objektit ({active} aktiivista, {deleted} jo poistettua)", + "SolrCloud mode enabled": "SolrCloud-tila käytössä", + "Something went wrong while adding files": "Tiedostojen lisäämisessä tapahtui virhe", + "Source": "Lähde", + "Source successfully updated": "Lähde päivitettiin onnistuneesti", + "Sources": "Lähteet", + "Sources:": "Lähteet:", + "Special Rights": "Erityisoikeudet", + "Standalone SOLR mode": "Itsenäinen SOLR-tila", + "Start Conversation": "Aloita keskustelu", + "Start Date Field": "Aloituspäivämääräkenttä", + "Start Vectorization": "Aloita vektorointi", + "Start a conversation": "Aloita keskustelu", + "Starting names cache warmup...": "Aloitetaan nimivälimuistin esilämmitys...", + "Starting validation for {schema}...": "Aloitetaan kohteen {schema} validointi...", + "Starting {action} for {schema}...": "Aloitetaan {action} kohteelle {schema}...", + "Starting...": "Aloitetaan...", + "Statistics": "Tilastot", + "Statistics and Metrics": "Tilastot ja mittarit", + "Statistics refreshed": "Tilastot päivitetty", + "Stats calculation not yet implemented": "Tilastojen laskentaa ei ole vielä toteutettu", + "Status": "Tila", + "Status Code": "Tilakoodi", + "Step": "Vaihe", + "Storage": "Tallennustila", + "Storage Quota (MB)": "Tallennustilan kiintiö (Mt)", + "String Constraints": "Merkkijonorajoitteet", + "Subject identifier (email, BSN, name, etc.)": "Kohteen tunniste (sähköposti, BSN, nimi jne.)", + "Subscribe": "Tilaa", + "Subscribed": "Tilattu", + "Success": "Onnistui", + "Success Rate": "Onnistumisaste", + "Success Status": "Onnistumistila", + "Successful": "Onnistunut", + "Successfully added user to organisation: {name}": "Käyttäjä lisättiin onnistuneesti organisaatioon: {name}", + "Successfully cleared {count} audit trails": "Tyhjennettiin onnistuneesti {count} tarkastusjälkeä", + "Successfully cleared {count} blob storage objects": "Tyhjennettiin onnistuneesti {count} blob-tallennusobjektia", + "Successfully cleared {count} search trails": "Tyhjennettiin onnistuneesti {count} hakujälkeä", + "Successfully created {count} SOLR fields": "Luotiin onnistuneesti {count} SOLR-kenttää", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Poistettiin onnistuneesti {count} upotusta. Vektoroi tietosi uudelleen.", + "Successfully deleted {count} objects for {schema}": "Poistettiin onnistuneesti {count} objektia kohteelle {schema}", + "Successfully fixed {count} SOLR field configurations": "Korjattiin onnistuneesti {count} SOLR-kentän kokoonpanoa", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Tuotiin onnistuneesti: {registers} rekisteriä, {schemas} skeemaa, {objects} objektia", + "Successfully joined organisation": "Liityttiin onnistuneesti organisaatioon", + "Successfully permanently deleted {count} objects": "Poistettiin pysyvästi onnistuneesti {count} objektia", + "Successfully processed {count} files!": "Käsiteltiin onnistuneesti {count} tiedostoa!", + "Successfully restored {count} object": "Palautettiin onnistuneesti {count} objekti", + "Successfully restored {count} objects": "Palautettiin onnistuneesti {count} objektia", + "Successfully saved configuration for {count} facets!": "Tallennettiin onnistuneesti kokoonpano {count} fasetille!", + "Supported File Types": "Tuetut tiedostotyypit", + "Switch Active Organisation": "Vaihda aktiivista organisaatiota", + "Switch Organisation": "Vaihda organisaatiota", + "Sync Table": "Synkronoi taulu", + "Synchronization Settings": "Synkronointiasetukset", + "Syncing magic table for {schema}...": "Synkronoidaan maagista taulua kohteelle {schema}...", + "Syncs": "Synkronoinnit", + "System Default": "Järjestelmän oletus", + "System Prompt": "Järjestelmäkehote", + "System announcements": "Järjestelmäilmoitukset", + "Table": "Taulukko", + "Target ID": "Kohteen tunnus", + "Target Type*": "Kohteen tyyppi*", + "Technical Description": "Tekninen kuvaus", + "Technical Details": "Tekniset tiedot", + "Technical description for developers and administrators": "Tekninen kuvaus kehittäjille ja järjestelmänvalvojille", + "Technical measures": "Tekniset toimenpiteet", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Lämpötila", + "Templates": "Mallit", + "Test": "Testaa", + "Test Chat": "Testaa keskustelua", + "Test Connection": "Testaa yhteyttä", + "Test Embedding": "Testaa upotusta", + "Test endpoint executed successfully": "Testipääte suoritettiin onnistuneesti", + "Test webhook delivered successfully": "Testiwebhook toimitettiin onnistuneesti", + "Test webhook delivery failed": "Testiwebhookin toimitus epäonnistui", + "Test webhook sent successfully": "Testiwebhook lähetettiin onnistuneesti", + "Testing...": "Testataan...", + "Text Chunk #{id}": "Tekstilohko #{id}", + "Text Extraction": "Tekstin poiminta", + "Text Extraction Engine": "Tekstin poimintamoottori", + "Text chunks": "Tekstilohkot", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Tekstilohkot luodaan tiedoston poiminnan aikana ja tallennetaan tietokantaan. Vektorointi lukee nämä esilohkotut tiedostot ja muuntaa ne upotuksiksi.", + "Text extracted successfully": "Teksti poimittiin onnistuneesti", + "Text extraction disabled": "Tekstin poiminta poistettu käytöstä", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloud Calendar -sovellusta ei ole asennettu tai otettu käyttöön tällä palvelimella.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloud Contacts -sovellusta ei ole asennettu tai otettu käyttöön tällä palvelimella.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloud Deck -sovellusta ei ole asennettu tai otettu käyttöön tällä palvelimella.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloud Mail -sovellusta ei ole asennettu tai otettu käyttöön tällä palvelimella.", + "The URL where webhook events will be sent": "URL, johon webhook-tapahtumat lähetetään", + "The requested conversation does not exist": "Pyydettyä keskustelua ei ole olemassa", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Pyydettyä koontinäyttöä ei voitu ladata. Tarkista URL tai valitse toinen koontinäyttö luettelosta.", + "The saved view and all its search configuration": "Tallennettu näkymä ja kaikki sen hakukokoonpano", + "There are no audit trail entries matching your current filters.": "Nykyisiä suodattimiasi vastaavia tarkastusjälkimerkintöjä ei ole.", + "There are no deleted items matching your current filters.": "Nykyisiä suodattimiasi vastaavia poistettuja kohteita ei ole.", + "There are no search trail entries matching your current filters.": "Nykyisiä suodattimiasi vastaavia hakujälkimerkintöjä ei ole.", + "There are no webhook log entries matching your filters.": "Suodattimiasi vastaavia webhook-lokimerkintöjä ei ole.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Ne säilytetään skeeman määritetyn säilytysajan mukaisesti ja poistetaan automaattisesti pysyvästi, kun säilytysaika päättyy. Säilytysaika on määritettävissä skeemakohtaisesti, ja se löytyy skeeman asetuksista.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Ne säilytetään skeeman määritetyn säilytysajan mukaisesti ja poistetaan sitten pysyvästi.", + "This action cannot be undone.": "Tätä toimintoa ei voi peruuttaa.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Tätä toimintoa ei voi peruuttaa. Varmista, ettei mikään kokoelma käytä tätä ConfigSetiä.", + "This action cannot be undone. Make sure no collections are using this configset.": "Tätä toimintoa ei voi peruuttaa. Varmista, ettei mikään kokoelma käytä tätä configsetiä.", + "This action will submit a deactivation request to your administrators.": "Tämä toiminto lähettää poistopyynnön järjestelmänvalvojillesi.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Tällä agentilla ei ole määritettäviä näkymiä tai työkaluja, mutta voit silti säätää RAG-asetuksia alla.", + "This analysis may take some time": "Tämä analyysi voi kestää jonkin aikaa", + "This audit trail entry does not contain any change information.": "Tämä tarkastusjälkimerkintä ei sisällä mitään muutostietoja.", + "This configuration has no data defined.": "Tälle kokoonpanolle ei ole määritetty tietoja.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Tämä päätepiste on vanhentunut. Käytä sen sijaan lohkopohjaisia päätepisteitä.", + "This entity has no relations to objects or files": "Tällä entiteetillä ei ole suhteita objekteihin tai tiedostoihin", + "This is a test webhook from OpenRegister": "Tämä on testiwebhook OpenRegisteristä", + "This is not valid JSON": "Tämä ei ole kelvollista JSONia", + "This key already exists on this schema": "Tämä avain on jo olemassa tässä skeemassa", + "This schema must use Magic Table configuration to sync": "Tämän skeeman on käytettävä Magic Table -kokoonpanoa synkronointiin", + "This schema must use magic table configuration to sync": "Tämän skeeman on käytettävä maagista taulukokoonpanoa synkronointiin", + "This source has no associated registers.": "Tähän lähteeseen ei ole liitetty rekistereitä.", + "This token will only be shown once. Copy it now.": "Tämä token näytetään vain kerran. Kopioi se nyt.", + "This will copy the _default ConfigSet with the new name": "Tämä kopioi _default-ConfigSetin uudella nimellä", + "This will copy the _default configset with the new name": "Tämä kopioi _default-configsetin uudella nimellä", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Tämä poistaa pysyvästi KAIKKI upotukset (vektorit) tietokannasta. Sinun on vektoroitava kaikki objektit ja tiedostot uudelleen. Tätä toimintoa ei voi peruuttaa.\n\nHaluatko varmasti jatkaa?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Tämä poistaa pysyvästi KAIKKI upotukset (vektorit) tietokannasta. Sinun on vektoroitava kaikki objektit ja tiedostot uudelleen. Tätä toimintoa ei voi peruuttaa.\\n\\nHaluatko varmasti jatkaa?", + "This will permanently delete:": "Tämä poistaa pysyvästi:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Tämä poistaa pysyvästi {count} jo pehmeästi poistettua objektia tietokannasta. Tätä toimintoa ei voi peruuttaa.", + "Timeout (seconds)": "Aikakatkaisu (sekuntia)", + "Timestamp": "Aikaleima", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Vinkki: Ota käyttöön vain ne näkymät, jotka tarvitsevat semanttista hakua, minimoidaksesi upotuskustannukset. Yksinkertaiset hakutaulukot tarvitsevat harvoin vektorointia.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Vinkki: ota käyttöön vain ne näkymät, jotka tarvitsevat semanttista hakua, minimoidaksesi upotuskustannukset. Yksinkertaiset hakutaulukot tarvitsevat harvoin vektorointia.", + "Title": "Otsikko", + "Title *": "Otsikko *", + "Title Template": "Otsikkomalli", + "Title*": "Otsikko*", + "To (e.g. -1 year or 2025-12-31)": "Mihin (esim. -1 year tai 2025-12-31)", + "To Date": "Päättymispäivämäärä", + "To add files larger than or equal to 512MB, go to the": "Lisätäksesi vähintään 512 Mt:n tiedostoja, siirry kohtaan", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Säätääksesi lohkon kokoa tai strategiaa, siirry kohtaan Tiedostokokoonpano → Käsittelyrajat.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Säätääksesi lohkon kokoa tai strategiaa, siirry kohtaan tiedostokokoonpano → käsittelyrajat.", + "To change the embedding provider or model, go to LLM Configuration.": "Vaihtaaksesi upotuspalveluntarjoajaa tai -mallia, siirry kohtaan LLM-kokoonpano.", + "To change the embedding provider or model, go to LLM configuration.": "Vaihtaaksesi upotuspalveluntarjoajaa tai -mallia, siirry kohtaan LLM-kokoonpano.", + "To date": "Päättymispäivämäärä", + "Toggle search sidebar": "Vaihda haun sivupalkki", + "Token Created": "Token luotu", + "Token Quota (per request)": "Tokenkiintiö (pyyntöä kohden)", + "Token copied to clipboard": "Token kopioitiin leikepöydälle", + "Token name": "Tokenin nimi", + "Token revoked": "Token peruutettu", + "Tools": "Työkalut", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Työkalut suoritetaan agentin oletuskäyttäjän oikeuksilla, kun yhtään käyttäjäistuntoa ei ole aktiivisena (esim. cron-työt). Määritä oletuskäyttäjä Asetukset-välilehdellä.", + "Tools respect the agent's own role and organization boundaries.": "Työkalut noudattavat agentin omaa roolia ja organisaation rajoja.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Työkalut noudattavat agentin näkymiä, oikeuksia ja organisaation rajoja.", + "Top Deleters": "Eniten poistaneet", + "Total": "Yhteensä", + "Total Audit Trails": "Tarkastusjälkiä yhteensä", + "Total Chunks Available:": "Saatavilla olevia lohkoja yhteensä:", + "Total Deleted Items": "Poistettuja kohteita yhteensä", + "Total Members": "Jäseniä yhteensä", + "Total Objects": "Objekteja yhteensä", + "Total Objects in Database:": "Objekteja tietokannassa yhteensä:", + "Total Organisations": "Organisaatioita yhteensä", + "Total Results": "Tuloksia yhteensä", + "Total Searches": "Hakuja yhteensä", + "Total Size": "Koko yhteensä", + "Total Storage": "Tallennustila yhteensä", + "Total events": "Tapahtumia yhteensä", + "Total:": "Yhteensä:", + "Totals": "Yhteensä", + "Try searching by UUID or with different keywords": "Kokeile hakea UUID:llä tai eri avainsanoilla", + "Type": "Tyyppi", + "Type \"DELETE COLLECTION\" to confirm:": "Kirjoita \"DELETE COLLECTION\" vahvistaaksesi:", + "Type Issue": "Tyyppiongelma", + "Type Variations:": "Tyyppivariaatiot:", + "Type filter (optional, e.g. email)": "Tyyppisuodatin (valinnainen, esim. email)", + "Type must be \"positive\" or \"negative\"": "Tyypin on oltava \"positive\" tai \"negative\"", + "Type to search for groups": "Kirjoita hakeaksesi ryhmiä", + "Type to search for objects...": "Kirjoita hakeaksesi objekteja...", + "Type to search for organisations": "Kirjoita hakeaksesi organisaatioita", + "Type to search for users": "Kirjoita hakeaksesi käyttäjiä", + "Type to search groups...": "Kirjoita hakeaksesi ryhmiä...", + "Type to search users...": "Kirjoita hakeaksesi käyttäjiä...", + "Type to search...": "Kirjoita hakeaksesi...", + "Type your username to confirm": "Kirjoita käyttäjätunnuksesi vahvistaaksesi", + "URL": "URL", + "URL where Ollama is running": "URL, jossa Ollama on käynnissä", + "UUID copied to clipboard": "UUID kopioitiin leikepöydälle", + "UUID:": "UUID:", + "Unchanged": "Muuttumaton", + "Unique Search Terms": "Yksilölliset hakutermit", + "Unique Terms": "Yksilölliset termit", + "Unknown": "Tuntematon", + "Unknown Object": "Tuntematon objekti", + "Unknown Register": "Tuntematon rekisteri", + "Unknown Schema": "Tuntematon skeema", + "Unknown error": "Tuntematon virhe", + "Unknown widget type: {type}": "Tuntematon widget-tyyppi: {type}", + "Unlink email": "Poista sähköpostin linkitys", + "Unlink event": "Poista tapahtuman linkitys", + "Unnamed Object": "Nimetön objekti", + "Untitled": "Nimetön", + "Untitled View": "Nimetön näkymä", + "Update": "Päivitä", + "Update Operations": "Päivitystoiminnot", + "Update available: {local} → {remote}": "Päivitys saatavilla: {local} → {remote}", + "Update configuration: ...": "Päivitä kokoonpano: ...", + "Update register OAS: ...": "Päivitä rekisterin OAS: ...", + "Update vectors when object data changes (recommended for accurate search)": "Päivitä vektorit, kun objektin tiedot muuttuvat (suositeltavaa tarkkaa hakua varten)", + "Updated": "Päivitetty", + "Updated At": "Päivitetty", + "Updated:": "Päivitetty:", + "Upload new avatar": "Lataa uusi profiilikuva", + "Uploading configuration sets...": "Ladataan kokoonpanosarjoja...", + "Uptime:": "Käyttöaika:", + "Url": "Url", + "Usage Count": "Käyttömäärä", + "Use AI agents for processing and analysis": "Käytä tekoälyagentteja käsittelyyn ja analysointiin", + "Use Large Language Model features": "Käytä suurten kielimallien ominaisuuksia", + "Use SolrCloud with Zookeeper for distributed search": "Käytä SolrCloudia Zookeeperin kanssa hajautettuun hakuun", + "Use SolrCloud with zookeeper for distributed search": "Käytä SolrCloudia zookeeperin kanssa hajautettuun hakuun", + "Use VNG APIs instead of MongoDB.": "Käytä VNG-API:ja MongoDB:n sijaan.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Käytä oletusalueita (Viimeiset 7/30/90 päivää, Viime vuosi, Vanhemmat)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Käytä ulkoista tallennustilaa (esim. MongoDB) sisäisen Nextcloud-tallennustilan sijaan.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Käytä suodattimia rajataksesi tarkastusjälkimerkintöjä rekisterin, skeeman, toiminnon tyypin, käyttäjän, päivämääräalueen tai objektin tunnuksen mukaan.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Käytä suodattimia rajataksesi poistettuja kohteita rekisterin, skeeman, poistopäivämäärän tai poistajan mukaan.", + "Use filters to narrow down entities by type or category.": "Käytä suodattimia rajataksesi entiteettejä tyypin tai luokan mukaan.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Käytä suodattimia rajataksesi hakujälkimerkintöjä rekisterin, skeeman, onnistumistilan, käyttäjän, päivämääräalueen, hakutermien tai suorituskykymittareiden mukaan.", + "Used By Collections": "Käytetty kokoelmissa", + "Used by": "Käyttäjä", + "User": "Käyttäjä", + "User Agent Statistics": "Käyttäjäagentin tilastot", + "User Description": "Käyttäjän kuvaus", + "User Quota": "Käyttäjän kiintiö", + "User rate limits cleared successfully": "Käyttäjän pyyntörajoitukset tyhjennettiin onnistuneesti", + "User removed successfully": "Käyttäjä poistettiin onnistuneesti", + "User-friendly description shown in forms and help text": "Käyttäjäystävällinen kuvaus, joka näytetään lomakkeissa ja ohjeteksteissä", + "Username": "Käyttäjänimi", + "Username for SOLR authentication (optional)": "Käyttäjänimi SOLR-todennusta varten (valinnainen)", + "Users": "Käyttäjät", + "Uses": "Käyttökerrat", + "Using Pre-Generated Chunks": "Esiluotujen osien käyttö", + "Validate": "Vahvista", + "Validate Objects": "Vahvista objektit", + "Validation completed for {schema}": "Vahvistus valmistui skeemalle {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Vahvistus valmistui skeemalle {schema}: {processed} käsitelty, {updated} päivitetty, {failed} epäonnistui", + "Validation completed successfully": "Vahvistus valmistui onnistuneesti", + "Validation failed": "Vahvistus epäonnistui", + "Value": "Arvo", + "Value list (split on ,)": "Arvoluettelo (erotettu pilkulla)", + "Vector Dimensions": "Vektorin ulottuvuudet", + "Vector Search Backend": "Vektorihaun taustajärjestelmä", + "Vector field: _embedding_": "Vektorikenttä: _embedding_", + "Vectorization Triggers": "Vektoroinnin laukaisimet", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorointi valmistui: {vectorized} objektia vektoroitu, {failed} epäonnistui", + "Vectorization failed: {error}": "Vektorointi epäonnistui: {error}", + "Vectorize All Files": "Vektoroi kaikki tiedostot", + "Vectorize All Objects": "Vektoroi kaikki objektit", + "Vectorize all file types": "Vektoroi kaikki tiedostotyypit", + "Vectorize all views": "Vektoroi kaikki näkymät", + "Vectorize on object creation": "Vektoroi objektin luonnin yhteydessä", + "Vectors will be stored in your existing object and file collections": "Vektorit tallennetaan olemassa oleviin objekti- ja tiedostokokoelmiisi", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Tarkistetaan palvelinyhteyttä...", + "Version": "Versio", + "Version Information": "Versiotiedot", + "Very High": "Erittäin korkea", + "Very high": "Erittäin korkea", + "View": "Näkymä", + "View API Docs": "Näytä API-dokumentaatio", + "View API Documentation": "Näytä API-dokumentaatio", + "View APIs": "Näytä API:t", + "View Changes": "Näytä muutokset", + "View Details": "Näytä tiedot", + "View Error": "Näytä virhe", + "View File": "Näytä tiedosto", + "View Full Details": "Näytä kaikki tiedot", + "View Logs": "Näytä lokit", + "View Name": "Näkymän nimi", + "View Object": "Näytä objekti", + "View Parameters": "Näytä parametrit", + "View Selection": "Näkymän valinta", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Tarkastele ja analysoi hakujälkilokeja edistyneillä suodatus- ja analytiikkaominaisuuksilla", + "View and analyze system audit trails with advanced filtering capabilities": "Tarkastele ja analysoi järjestelmän tarkastusjälkiä edistyneillä suodatusominaisuuksilla", + "View and manage authorization across registers and schemas": "Tarkastele ja hallinnoi valtuutuksia rekistereiden ja skeemojen välillä", + "View deleted successfully": "Näkymä poistettiin onnistuneesti", + "View entity details and manage relations": "Tarkastele entiteetin tietoja ja hallinnoi suhteita", + "View name": "Näkymän nimi", + "View name is required": "Näkymän nimi vaaditaan", + "View object": "Näytä objekti", + "View saved successfully!": "Näkymä tallennettiin onnistuneesti!", + "View search analytics and manage search logs": "Tarkastele hakuanalytiikkaa ja hallinnoi hakulokeja", + "View successfully deleted": "Näkymä poistettiin onnistuneesti", + "View successfully updated": "Näkymä päivitettiin onnistuneesti", + "View updated successfully!": "Näkymä päivitettiin onnistuneesti!", + "View webhook delivery logs and filter by webhook": "Tarkastele webhook-toimituslokeja ja suodata webhookin mukaan", + "View {count} more": "Näytä {count} lisää", + "Views": "Näkymät", + "Visible to end users": "Näkyy loppukäyttäjille", + "Visible to users": "Näkyy käyttäjille", + "Wait for Response": "Odota vastausta", + "Wait for webhook response before continuing (required for request/response flows)": "Odota webhook-vastausta ennen jatkamista (vaaditaan pyyntö/vastaus-prosesseissa)", + "Waiting for cluster synchronization...": "Odotetaan klusterin synkronointia...", + "Warmup Names Cache": "Lämmitä nimivälimuisti", + "Warmup failed": "Lämmitys epäonnistui", + "Warning": "Varoitus", + "Warning:": "Varoitus:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Varoitus: Tällä ominaisuudella on epäyhtenäisiä tyyppejä: {types}. Harkitse, tulisiko kaikilla objekteilla olla sama tyyppi.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Skannaamme kaikki tähän skeemaan kuuluvat objektit löytääksemme uusia ominaisuuksia ja analysoidaksemme olemassa olevia ominaisuuksia mahdollisten parannusten varalta. Prosessi sisältää kunkin objektin tietorakenteen tarkastelun, nykyisessä skeemassa määrittelemättömien ominaisuuksien tunnistamisen sekä mahdollisuuksien löytämisen olemassa olevien ominaisuusmäärittelyjen parantamiseksi paremmilla rajoituksilla, muodoilla ja vahvistussäännöillä.", + "Webhook": "Webhook", + "Webhook Log Details": "Webhook-lokin tiedot", + "Webhook Logs": "Webhook-lokit", + "Webhook created successfully": "Webhook luotiin onnistuneesti", + "Webhook deleted": "Webhook poistettiin", + "Webhook retry delivered successfully": "Webhookin uudelleenyritys toimitettiin onnistuneesti", + "Webhook retry delivery failed": "Webhookin uudelleenyrityksen toimitus epäonnistui", + "Webhook updated": "Webhook päivitettiin", + "Webhook updated successfully": "Webhook päivitettiin onnistuneesti", + "Webhooks": "Webhookit", + "Weekly": "Viikoittain", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kun agentti suoritetaan ilman käyttäjäistuntoa (esim. ajastetut tehtävät), käytetään tämän käyttäjän kontekstia", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kun agentti suoritetaan ilman käyttäjäistuntoa (esim. ajastetut tehtävät), käytetään tämän käyttäjän kontekstia.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Kun poistettu käytöstä, tämä fasetti näyttää vain tämän skeeman arvot ja sisältää skeemasuodattimen valittaessa.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Kun käytössä, tämän objektin tallentaminen päivittää myös kohdeobjektit sisältämään viittauksen takaisin tähän objektiin.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Kun käytössä, tämä ominaisuus poistetaan lähdeobjektista kohdeobjektien päivittämisen jälkeen.", + "Workflow Management": "Työnkulun hallinta", + "Workflows": "Työnkulut", + "Wrap webhook payload in CloudEvents format for better interoperability": "Kääri webhook-hyötykuorma CloudEvents-muotoon paremman yhteentoimivuuden vuoksi", + "Wrap webhook payload in cloudevents format for better interoperability": "Kääri webhook-hyötykuorma cloudevents-muotoon paremman yhteentoimivuuden vuoksi", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Kyllä", + "You": "Sinä", + "You are not yet a member of any organisations.": "Et ole vielä minkään organisaation jäsen.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Voit luoda uusia ConfigSetejä _default-mallin pohjalta tai ladata mukautettuja suoraan SOLR-palvelimellesi.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Voit luoda uusia configsetejä _default-mallin pohjalta tai ladata mukautettuja suoraan SOLR-palvelimellesi.", + "You do not have access to this conversation": "Sinulla ei ole pääsyä tähän keskusteluun", + "You do not have permission to delete this conversation": "Sinulla ei ole oikeutta poistaa tätä keskustelua", + "You do not have permission to modify this conversation": "Sinulla ei ole oikeutta muokata tätä keskustelua", + "You do not have permission to restore this conversation": "Sinulla ei ole oikeutta palauttaa tätä keskustelua", + "You do not have permission to view the permission matrix. Admin access is required.": "Sinulla ei ole oikeutta tarkastella oikeusmatriisia. Järjestelmänvalvojan käyttöoikeus vaaditaan.", + "You have selected {count} properties to add to the schema.": "Olet valinnut {count} ominaisuutta lisättäväksi skeemaan.", + "You must be logged in to favorite views": "Sinun on oltava kirjautuneena suosikkinäkymien merkitsemiseksi", + "You need an AI agent to start a conversation.": "Tarvitset tekoälyagentin aloittaaksesi keskustelun.", + "Your Fireworks AI API key. Get one at": "Fireworks AI -API-avaimesi. Hanki sellainen osoitteesta", + "Your OpenAI API key. Get one at": "OpenAI-API-avaimesi. Hanki sellainen osoitteesta", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Palautteesi on tallennettu. Voit halutessasi antaa lisätietoja tässä...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Palautteesi on tallennettu. Voit halutessasi antaa lisätietoja tässä...", + "Zookeeper Hosts": "Zookeeper-isännät", + "Zookeeper Port": "Zookeeper-portti", + "Zookeeper Settings (SolrCloud)": "Zookeeper-asetukset (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper-yhteysmerkkijono SolrCloudia varten", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper-portin numero (valinnainen, oletusarvo 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F PYSYVÄN POISTON VAROITUS \\u26A0\\uFE0F\n\nOlet poistamassa PYSYVÄSTI KAIKKI objektit skeemasta \"{schema}\":\n\n\\u2022 Aktiiviset objektit: {active}\n\\u2022 Pehmeästi poistetut objektit: {deleted}\n\\u2022 Yhteensä: {total}\n\nNämä objektit poistetaan kokonaan tietokannasta, EIKÄ niitä voi palauttaa.\n\nOletko aivan varma?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F PYSYVÄN POISTON VAROITUS \\u26A0\\uFE0F\n\nOlet poistamassa PYSYVÄSTI {count} pehmeästi poistettua objektia skeemasta \"{schema}\".\n\nNämä objektit poistetaan kokonaan tietokannasta, EIKÄ niitä voi palauttaa.\n\nOletko aivan varma?", + "and accessible to all users in selected groups (or all users if no groups selected).": "ja kaikkien valittujen ryhmien käyttäjien käytettävissä (tai kaikkien käyttäjien, jos ryhmiä ei ole valittu).", + "and add the files there.": "ja lisää tiedostot sinne.", + "approved": "hyväksytty", + "are only accessible to invited users.": "ovat vain kutsuttujen käyttäjien käytettävissä.", + "by": "tekijä", + "chunk": "osa", + "chunks": "osat", + "chunks processed": "osaa käsitelty", + "completed": "valmis", + "configuration(s)": "määritys(tä)", + "conversationId is required": "conversationId vaaditaan", + "create": "luo", + "delete": "poista", + "deleted objects section": "poistettujen objektien osio", + "e.g., lib/Settings/config.json": "e.g., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "e.g., lib/Settings/register.json", + "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", + "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", + "execute": "suorita", + "failed": "epäonnistui", + "file": "tiedosto", + "files": "tiedostot", + "file{plural}": "tiedosto{plural}", + "folder": "kansio", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "rekisterissä {register}", + "in_progress": "käynnissä", + "inherited": "peritty", + "list": "luettele", + "log{plural}": "loki{plural}", + "manage": "hallinnoi", + "members": "jäsenet", + "menu or contact someone with permission to create agents.": "-valikko tai ota yhteyttä henkilöön, jolla on oikeus luoda agentteja.", + "message content is required": "viestin sisältö vaaditaan", + "messages": "viestit", + "missing": "puuttuu", + "myapp": "myapp", + "n8n URL and API key are required": "n8n-URL ja API-avain vaaditaan", + "n8n configuration saved successfully": "n8n-määritys tallennettiin onnistuneesti", + "n8n connection not configured": "n8n-yhteyttä ei ole määritetty", + "n8n connection successful": "n8n-yhteys onnistui", + "n8n connection test failed: {message}": "n8n-yhteystesti epäonnistui: {message}", + "n8n connection test successful": "n8n-yhteystesti onnistui", + "n8n initialization failed: {message}": "n8n-alustus epäonnistui: {message}", + "n8n integration disabled": "n8n-integraatio poistettu käytöstä", + "n8n integration enabled": "n8n-integraatio otettu käyttöön", + "n8n project initialized successfully": "n8n-projekti alustettiin onnistuneesti", + "n8n settings saved successfully": "n8n-asetukset tallennettiin onnistuneesti", + "notify_push configuration guide": "notify_push-määritysopas", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push on asennettu, mutta OpenRegister ei ole vielä vahvistanut onnistunutta puskua. Käynnistä objektin tallennus aktivoidaksesi tai tarkista notify_push-määrityksesi.", + "notify_push is installed but not yet active": "notify_push on asennettu, mutta ei vielä aktiivinen", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objektit", + "objects processed": "objektia käsitelty", + "object{plural}": "objekti{plural}", + "or pick a range": "tai valitse alue", + "org-...": "org-...", + "pending": "odottaa", + "provides a good balance between speed and accuracy.": "tarjoaa hyvän tasapainon nopeuden ja tarkkuuden välillä.", + "public": "julkinen", + "read": "lue", + "register(s)": "rekisteri(t)", + "register(s) selected": "rekisteri(ä) valittu", + "register{plural}": "rekisteri{plural}", + "rejected": "hylätty", + "required": "vaaditaan", + "results": "tulokset", + "schema(s)": "skeema(t)", + "schema(s) selected": "skeema(a) valittu", + "schema{plural}": "skeema{plural}", + "selected objects": "valitut objektit", + "set": "aseta", + "sk-...": "sk-...", + "this application": "tämä sovellus", + "to": "kohteeseen", + "type must be \"positive\" or \"negative\"": "tyypin on oltava \"positive\" tai \"negative\"", + "update": "päivitä", + "vectorized": "vektoroitu", + "view": "näytä", + "views selected": "näkymää valittu", + "waiting": "odottaa", + "{active} active, {deleted} deleted": "{active} aktiivista, {deleted} poistettua", + "{count} audit trails cleared successfully": "{count} tarkastusjälkeä tyhjennettiin onnistuneesti", + "{count} compliance issue(s) detected": "{count} vaatimustenmukaisuusongelma(a) havaittu", + "{count} deletions": "{count} poistoa", + "{count} documents found": "{count} dokumenttia löytyi", + "{count} email": "{count} sähköposti", + "{count} emails": "{count} sähköpostia", + "{count} entries": "{count} merkintää", + "{count} issues": "{count} ongelmaa", + "{count} members": "{count} jäsentä", + "{count} searches": "{count} hakua", + "{count} selected": "{count} valittu", + "{count} widget(s)": "{count} widget(iä)", + "{matched} object(s) matched. {erased} erased.": "{matched} objekti(a) täsmäsi. {erased} pyyhitty.", + "{minutes} minutes ago": "{minutes} minuuttia sitten", + "{percentage}% of objects": "{percentage} % objekteista", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} rekisterissä {register} / {schema}", + "{type} cache cleared successfully": "{type}-välimuisti tyhjennettiin onnistuneesti", + "ℹ️ Current Configuration": "ℹ️ Nykyinen määritys", + "ℹ️ current configuration": "ℹ️ nykyinen määritys", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ VAARA: Kaikki {total} objektia POISTETAAN PYSYVÄSTI tietokannasta. Tämä toiminto on PERUUTTAMATON eikä sitä voi kumota!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ PYSYVÄN POISTON VAROITUS ⚠️\n\nOlet poistamassa PYSYVÄSTI KAIKKI objektit skeemasta \"{schema}\":\n\n• Aktiiviset objektit: {active}\n• Pehmeästi poistetut objektit: {deleted}\n• Yhteensä: {total}\n\nNämä objektit poistetaan kokonaan tietokannasta, EIKÄ niitä voi palauttaa.\n\nOletko aivan varma?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ PYSYVÄN POISTON VAROITUS ⚠️\n\nOlet poistamassa PYSYVÄSTI {count} pehmeästi poistettua objektia skeemasta \"{schema}\".\n\nNämä objektit poistetaan kokonaan tietokannasta, EIKÄ niitä voi palauttaa.\n\nOletko aivan varma?", + "⚡ Batch Processing": "⚡ Eräkäsittely", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR pystyy käsittelemään miljoonia dokumentteja alle sekunnin hakuvasteajoilla...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Kattava konteksti ✓ Epätodennäköisemmin jää yksityiskohtia huomaamatta ✗ Hitaammat vastaukset ✗ Voi sisältää vähemmän olennaista tietoa", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Nopeammat vastaukset ✓ Kohdennetummat vastaukset ✗ Voi jäädä olennaista tietoa huomaamatta", + "✨ AI Features": "✨ Tekoälyominaisuudet", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Hajautetussa tilassa SOLR käyttää ZooKeeperia klusterin koordinointiin...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasettihaku antaa käyttäjien porautua tuloksiin luokittain...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Tiesitkö? SOLR tuottaa haun Netflixille, Applelle ja monille muille suurille sivustoille!", + "💰 View Selection (Cost Optimization)": "💰 Näkymän valinta (kustannusoptimointi)", + "📄 Object Serialization": "📄 Objektin sarjallistaminen", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR tarjoaa rikasta analytiikkaa ja tilastoja hakusuorituskyvystä...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSetit sisältävät hakuindeksisi skeema- ja määritystiedostot...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikointi varmistaa, että hakuindeksisi on käytettävissä, vaikka solmut vikaantuisivat...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR on tehokas yrityshakualusta, joka on rakennettu Apache Lucenen päälle...", + "🔢 Vectorization Settings": "🔢 Vektorointiasetukset", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automaattinen skaalaus voi dynaamisesti lisätä tai poistaa solmuja kuormituksen perusteella...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Tietoturvaominaisuuksiin kuuluvat todennus, valtuutus ja SSL-salaus...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} sähköposti", + "{count} sähköpostia" + ], + "Purge {count} object from database": [ + "Tyhjennä {count} objekti tietokannasta", + "Tyhjennä {count} objektia tietokannasta" + ], + "Restore {count} object": [ + "Palauta {count} objekti", + "Palauta {count} objektia" + ], + "Successfully restored {count} object": [ + "Palautettiin onnistuneesti {count} objekti", + "Palautettiin onnistuneesti {count} objektia" + ], + "Delete {count} object": [ + "Poista {count} objekti", + "Poista {count} objektia" + ], + "Object successfully deleted": [ + "Objekti poistettu onnistuneesti", + "Objektit poistettu onnistuneesti" + ] + } +} diff --git a/l10n/fr.js b/l10n/fr.js new file mode 100644 index 0000000000..2ab649a05c --- /dev/null +++ b/l10n/fr.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(aucun sujet)", + "(no title)" : "(aucun titre)", + "(unnamed)" : "(sans nom)", + "(untitled card)" : "(carte sans titre)", + "Activities" : "Activités", + "Add Endpoint" : "Ajouter un point d'accès", + "Add Source" : "Ajouter une source", + "Add card" : "Ajouter une carte", + "Add contact" : "Ajouter un contact", + "All clear" : "Tout est en ordre", + "Articles" : "Articles", + "Code" : "Code", + "Compliance" : "Conformité", + "Contacts" : "Contacts", + "Create Agent" : "Créer un agent", + "Create Application" : "Créer une application", + "Create event" : "Créer un événement", + "Erasure complete" : "Effacement terminé", + "Erasure preview" : "Aperçu de l'effacement", + "Expand details" : "Développer les détails", + "Save changes" : "Enregistrer les modifications", + "📄 Object Serialization" : "📄 Sérialisation des objets", + "🔢 Vectorization Settings" : "🔢 Réglages de vectorisation", + "💰 View Selection (Cost Optimization)" : "💰 Sélection de vues (optimisation des coûts)", + "AI Assistant" : "Assistant IA", + "AI Chat" : "Discussion IA", + "API Key" : "Clé API", + "API tokens saved successfully" : "Jetons API enregistrés avec succès", + "About ConfigSets" : "À propos des ConfigSets", + "Action" : "Action", + "Action Distribution" : "Répartition des actions", + "Action:" : "Action :", + "Actions" : "Actions", + "Active" : "Actif", + "Active Collections" : "Collections actives", + "Active Filters" : "Filtres actifs", + "Active Organisations" : "Organisations actives", + "Active filters:" : "Filtres actifs :", + "Active organisation set successfully" : "Organisation active définie avec succès", + "Activity Period" : "Période d'activité", + "Add Groups" : "Ajouter des groupes", + "Add Schema" : "Ajouter un schéma", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Ajoutez des titres de schéma, des descriptions et des informations de registre pour fournir un contexte plus riche à la recherche", + "Add to favorites" : "Ajouter aux favoris", + "Added" : "Ajouté", + "Added to favorites" : "Ajouté aux favoris", + "Additional Information" : "Informations supplémentaires", + "Additional feedback saved. Thank you!" : "Retour supplémentaire enregistré. Merci !", + "Admin" : "Administrateur", + "Advanced" : "Avancé", + "Advanced Filters" : "Filtres avancés", + "Advanced Options" : "Options avancées", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Les filtres avancés ne sont pas disponibles lors de l'utilisation de la source base de données. Passez à Auto ou à l'index SOLR pour les options de filtrage.", + "After" : "Après", + "Agent deleted successfully" : "Agent supprimé avec succès", + "Agents" : "Agents", + "All" : "Tout", + "All Categories" : "Toutes les catégories", + "All Collections" : "Toutes les collections", + "All Confidence Levels" : "Tous les niveaux de confiance", + "All ConfigSets" : "Tous les ConfigSets", + "All Files" : "Tous les fichiers", + "All Levels" : "Tous les niveaux", + "All Types" : "Tous les types", + "All Webhooks" : "Tous les webhooks", + "All actions" : "Toutes les actions", + "All audit trails cleared successfully" : "Tous les journaux d'audit ont été effacés avec succès", + "All registers" : "Tous les registres", + "All schemas" : "Tous les schémas", + "All searches" : "Toutes les recherches", + "All users" : "Tous les utilisateurs", + "An error occurred while clearing audit trails" : "Une erreur s'est produite lors de l'effacement des journaux d'audit", + "An error occurred while deleting the audit trail" : "Une erreur s'est produite lors de la suppression du journal d'audit", + "An error occurred while deleting the view" : "Une erreur s'est produite lors de la suppression de la vue", + "An error occurred while permanently deleting the objects" : "Une erreur s'est produite lors de la suppression définitive des objets", + "Analysis completed:" : "Analyse terminée :", + "Analysis steps:" : "Étapes de l'analyse :", + "Analytics" : "Analyses", + "Analyze Objects" : "Analyser les objets", + "Analyze Properties" : "Analyser les propriétés", + "Analyze existing properties for improvement opportunities" : "Analyser les propriétés existantes pour repérer des possibilités d'amélioration", + "Analyzing..." : "Analyse en cours...", + "Any favorites and sharing settings for this view" : "Tous les favoris et réglages de partage pour cette vue", + "Any user" : "N'importe quel utilisateur", + "App store cache invalidated successfully" : "Cache de l'App Store invalidé avec succès", + "Application deleted successfully" : "Application supprimée avec succès", + "Applications" : "Applications", + "Apply Changes" : "Appliquer les modifications", + "Archive" : "Archiver", + "Archive conversation" : "Archiver la conversation", + "Archived conversations are hidden from your active list" : "Les conversations archivées sont masquées de votre liste active", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Voulez-vous vraiment nettoyer les anciens journaux de recherche ? Cela supprimera les entrées de plus de 30 jours.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Voulez-vous vraiment supprimer les journaux d'audit sélectionnés ? Cette action est irréversible.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Voulez-vous vraiment supprimer les journaux de recherche sélectionnés ? Cette action est irréversible.", + "Are you sure you want to delete this ConfigSet?" : "Voulez-vous vraiment supprimer ce ConfigSet ?", + "Are you sure you want to permanently delete" : "Voulez-vous vraiment supprimer définitivement", + "Ask a question..." : "Posez une question...", + "Ask questions about your data using natural language" : "Posez des questions sur vos données en langage naturel", + "Attempt" : "Tentative", + "Audit Trail Changes" : "Modifications du journal d'audit", + "Audit Trail Details" : "Détails du journal d'audit", + "Audit Trail Management" : "Gestion du journal d'audit", + "Audit Trail Statistics" : "Statistiques du journal d'audit", + "Audit Trails" : "Journaux d'audit", + "Audit trail data copied to clipboard" : "Données du journal d'audit copiées dans le presse-papiers", + "Audit trail deleted successfully" : "Journal d'audit supprimé avec succès", + "Audit trail successfully deleted" : "Journal d'audit supprimé avec succès", + "Authentication" : "Authentification", + "Auto-commit disabled" : "Validation automatique désactivée", + "Auto-commit enabled" : "Validation automatique activée", + "Auto-create Default Organisation" : "Créer automatiquement l'organisation par défaut", + "Auto-retry failed vectorizations" : "Réessayer automatiquement les vectorisations échouées", + "Automatically commit changes to SOLR index" : "Valider automatiquement les modifications dans l'index SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Créer automatiquement une organisation par défaut s'il n'en existe aucune lors de l'initialisation de l'application", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Générer automatiquement des plongements vectoriels à partir des fragments de texte lorsque les fichiers sont téléversés et traités", + "Automatically generate vector embeddings when objects are created or updated" : "Générer automatiquement des plongements vectoriels lorsque les objets sont créés ou mis à jour", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Réessayer automatiquement les tentatives de vectorisation échouées (3 essais maximum)", + "Available Filters" : "Filtres disponibles", + "Available Workflows" : "Flux de travail disponibles", + "Avg Execution Time" : "Temps d'exécution moyen", + "Avg Members/Org" : "Membres moy./org.", + "Avg Object Views/Session" : "Vues d'objets moy./session", + "Avg Response Time" : "Temps de réponse moyen", + "Avg Results/Search" : "Résultats moy./recherche", + "Avg Searches/Session" : "Recherches moy./session", + "Back" : "Retour", + "Back to Entities" : "Retour aux entités", + "Back to Registers" : "Retour aux registres", + "Back to Webhooks" : "Retour aux webhooks", + "Back to applications" : "Retour aux applications", + "Back to entities" : "Retour aux entités", + "Backend updated successfully. Please reload the application." : "Backend mis à jour avec succès. Veuillez recharger l'application.", + "Base URL (Optional)" : "URL de base (facultatif)", + "Basic Information" : "Informations de base", + "Batch Size" : "Taille du lot", + "Batch extraction completed" : "Extraction par lot terminée", + "Before" : "Avant", + "Before object vectorization can work:" : "Avant que la vectorisation des objets puisse fonctionner :", + "Behavior Issue" : "Problème de comportement", + "Blob storage has been retired. All objects now use magic tables." : "Le stockage blob a été retiré. Tous les objets utilisent désormais les tables magiques.", + "Branch" : "Branche", + "Bulk delete operation completed successfully" : "Opération de suppression en masse terminée avec succès", + "Bulk save operation completed successfully" : "Opération d'enregistrement en masse terminée avec succès", + "Business Data" : "Données d'entreprise", + "Cache cleared successfully" : "Cache vidé avec succès", + "Calculate Sizes" : "Calculer les tailles", + "Cancel" : "Annuler", + "Cannot delete: objects are still attached" : "Suppression impossible : des objets sont encore rattachés", + "Category" : "Catégorie", + "Change Type" : "Type de modification", + "Changes" : "Modifications", + "Changes copied to clipboard" : "Modifications copiées dans le presse-papiers", + "Chat Model" : "Modèle de discussion", + "Chat Provider" : "Fournisseur de discussion", + "Chat Provider (RAG)" : "Fournisseur de discussion (RAG)", + "Chat Settings" : "Réglages de discussion", + "Chat provider connection successful!" : "Connexion au fournisseur de discussion réussie !", + "Chat settings" : "Réglages de discussion", + "Choose a register" : "Choisir un registre", + "Choose a schema" : "Choisir un schéma", + "Choose how vector similarity calculations are performed for semantic search" : "Choisissez comment les calculs de similarité vectorielle sont effectués pour la recherche sémantique", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Choisissez quels types de fichiers inclure dans le processus de vectorisation. Seuls les fichiers avec du texte extrait et des fragments seront traités.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Choisissez quelles vues inclure dans le processus de vectorisation. Laissez vide pour traiter toutes les vues selon votre configuration.", + "Chunk" : "Fragment", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "La suppression de fragments n'est pas encore implémentée. Utilisez les points d'accès par fragments.", + "Chunks" : "Fragments", + "Chunks to Vectorize:" : "Fragments à vectoriser :", + "Cleanup Old Trails" : "Nettoyer les anciens journaux", + "Cleanup completed" : "Nettoyage terminé", + "Clear All Embeddings" : "Effacer tous les plongements", + "Clear Entries" : "Effacer les entrées", + "Clear Filtered Audit Trails" : "Effacer les journaux d'audit filtrés", + "Clear Filters" : "Effacer les filtres", + "Clear Index" : "Effacer l'index", + "Clear Selection" : "Effacer la sélection", + "Clear all" : "Tout effacer", + "Clear all filters" : "Effacer tous les filtres", + "Clear date range" : "Effacer la plage de dates", + "Clearing..." : "Effacement en cours...", + "Close" : "Fermer", + "Collection Management" : "Gestion des collections", + "Collection Name" : "Nom de la collection", + "Collection assignments updated successfully" : "Affectations de collection mises à jour avec succès", + "Collection cleared successfully" : "Collection effacée avec succès", + "Collection copied successfully" : "Collection copiée avec succès", + "Collection created successfully" : "Collection créée avec succès", + "Collection deleted successfully" : "Collection supprimée avec succès", + "Collection used to store and index file metadata and content" : "Collection utilisée pour stocker et indexer les métadonnées et le contenu des fichiers", + "Collection used to store and index object data" : "Collection utilisée pour stocker et indexer les données des objets", + "Collections:" : "Collections :", + "Commit Message" : "Message de validation", + "Commit Within (ms)" : "Valider dans (ms)", + "Commit message" : "Message de validation", + "Compare current schema with real object data" : "Comparer le schéma actuel avec les données d'objet réelles", + "Completed" : "Terminé", + "Complex" : "Complexe", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Requêtes complexes : recherches avancées avec plusieurs filtres, opérateurs et combinaisons de paramètres complexes", + "Confidence Level" : "Niveau de confiance", + "Confidence Score" : "Score de confiance", + "Config must be provided as an object" : "La configuration doit être fournie sous forme d'objet", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Gestion des ConfigSets", + "ConfigSet Name" : "Nom du ConfigSet", + "ConfigSet created successfully" : "ConfigSet créé avec succès", + "ConfigSet deleted successfully" : "ConfigSet supprimé avec succès", + "ConfigSet:" : "ConfigSet :", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Les ConfigSets définissent le schéma et la configuration de vos collections SOLR. Ils contiennent les définitions de champs, les analyseurs et d'autres réglages de recherche.", + "Configuration" : "Configuration", + "Configuration Data" : "Données de configuration", + "Configuration saved" : "Configuration enregistrée", + "Configurations" : "Configurations", + "Configure Facets" : "Configurer les facettes", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Configurez les fournisseurs de grands modèles de langage (LLM) pour les fonctionnalités propulsées par l'IA, notamment la recherche sémantique, les plongements et la discussion.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Configurez les réglages de connexion de base pour votre serveur SOLR, notamment l'authentification et les options réseau. Utilisez les boîtes de dialogue distinctes de gestion des ConfigSets et des collections pour gérer les cœurs et les collections.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Configurez comment les objets de la base de données sont convertis en plongements vectoriels pour la recherche sémantique. Les objets sont directement vectorisés sans nécessiter d'extraction de texte.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Configurez comment les objets sont convertis en texte avant la vectorisation. Ces réglages affectent la qualité et le contexte de la recherche.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Configurez les paramètres de la vectorisation des fichiers. Ce processus générera des plongements vectoriels pour tous les fragments de fichier extraits.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Configurez les paramètres de la vectorisation des objets. Ce processus générera des plongements vectoriels pour tous les objets correspondant aux filtres de votre vue.", + "Configure which types of data to search and how many sources to retrieve" : "Configurez quels types de données rechercher et combien de sources récupérer", + "Connection Failed" : "Échec de la connexion", + "Connection Settings" : "Réglages de connexion", + "Connection Successful!" : "Connexion réussie !", + "Connection failed" : "Échec de la connexion", + "Connection protocol" : "Protocole de connexion", + "Connection successful" : "Connexion réussie", + "Connection timeout in seconds" : "Délai d'attente de connexion en secondes", + "Constant delay between retries (5 minutes)" : "Délai constant entre les tentatives (5 minutes)", + "Constraint Issue" : "Problème de contrainte", + "Content Filters" : "Filtres de contenu", + "Continue to Properties" : "Continuer vers les propriétés", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Contrôlez quelles vues d'objets doivent être vectorisées pour réduire les coûts d'API. Ne vectorisez que les vues qui bénéficient de la recherche sémantique.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Contrôlez quelles vues et quels outils l'IA peut utiliser dans cette conversation. Par défaut, toutes les capacités de l'agent sont activées.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Contrôle l'aléatoire (0 = déterministe, 2 = très créatif)", + "Conversation ID is required" : "L'identifiant de conversation est requis", + "Conversation archived" : "Conversation archivée", + "Conversation archived successfully" : "Conversation archivée avec succès", + "Conversation cleared successfully" : "Conversation effacée avec succès", + "Conversation deleted" : "Conversation supprimée", + "Conversation permanently deleted" : "Conversation supprimée définitivement", + "Conversation renamed" : "Conversation renommée", + "Conversation restored" : "Conversation restaurée", + "Conversation title" : "Titre de la conversation", + "Copied!" : "Copié !", + "Copy" : "Copier", + "Copy Changes" : "Copier les modifications", + "Copy Collection" : "Copier la collection", + "Copy Data" : "Copier les données", + "Copy Full Data" : "Copier toutes les données", + "Copying..." : "Copie en cours...", + "Count" : "Nombre", + "Counting objects..." : "Comptage des objets...", + "Costs" : "Coûts", + "Create" : "Créer", + "Create Collection" : "Créer une collection", + "Create ConfigSet" : "Créer un ConfigSet", + "Create New Collection" : "Créer une nouvelle collection", + "Create New ConfigSet" : "Créer un nouveau ConfigSet", + "Create Operations" : "Opérations de création", + "Create Webhook" : "Créer un webhook", + "Create a copy of collection:" : "Créer une copie de la collection :", + "Create a new ConfigSet based on the _default template" : "Créer un nouveau ConfigSet à partir du modèle _default", + "Create a new SOLR collection from an existing ConfigSet" : "Créer une nouvelle collection SOLR à partir d'un ConfigSet existant", + "Create your first AI agent to get started." : "Créez votre premier agent IA pour commencer.", + "Created" : "Créé", + "Created:" : "Créé :", + "Creating..." : "Création en cours...", + "Current Filters" : "Filtres actuels", + "Current Type:" : "Type actuel :", + "Custom API endpoint if using a different region" : "Point d'accès API personnalisé si vous utilisez une région différente", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "En-têtes HTTP personnalisés (un par ligne, format : Nom-En-tête: valeur)", + "Daily" : "Quotidien", + "Dashboard" : "Tableau de bord", + "Data Source" : "Source de données", + "Data sources" : "Sources de données", + "Data type does not match observed values" : "Le type de données ne correspond pas aux valeurs observées", + "Database URL" : "URL de la base de données", + "Database information refreshed" : "Informations de la base de données actualisées", + "Date Range" : "Plage de dates", + "Default" : "Par défaut", + "Default Organisation" : "Organisation par défaut", + "Default: 5" : "Par défaut : 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Les délais doublent à chaque tentative (2, 4, 8 minutes...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Les délais augmentent linéairement (5, 10, 15 minutes...)", + "Delete" : "Supprimer", + "Delete Application" : "Supprimer l'application", + "Delete Audit Trail" : "Supprimer le journal d'audit", + "Delete Collection" : "Supprimer la collection", + "Delete ConfigSet" : "Supprimer le ConfigSet", + "Delete Objects" : "Supprimer les objets", + "Delete Operations" : "Opérations de suppression", + "Delete View" : "Supprimer la vue", + "Delete all objects in this schema" : "Supprimer tous les objets de ce schéma", + "Delete permanently" : "Supprimer définitivement", + "Delete this conversation?" : "Supprimer cette conversation ?", + "Delete view" : "Supprimer la vue", + "Deleted" : "Supprimé", + "Deleted By" : "Supprimé par", + "Deleted Date" : "Date de suppression", + "Deleted Items Management" : "Gestion des éléments supprimés", + "Deleted This Week" : "Supprimés cette semaine", + "Deleted Today" : "Supprimés aujourd'hui", + "Deleted:" : "Supprimé :", + "Deleting..." : "Suppression en cours...", + "Deletion Date Range" : "Plage de dates de suppression", + "Deletion Statistics" : "Statistiques de suppression", + "Deprecated" : "Obsolète", + "Description" : "Description", + "Description:" : "Description :", + "Detect data types and patterns" : "Détecter les types de données et les motifs", + "Detected At" : "Détecté le", + "Detected Format:" : "Format détecté :", + "Detected Issues:" : "Problèmes détectés :", + "Disable" : "Désactiver", + "Disabled" : "Désactivé", + "Discover Files" : "Découvrir les fichiers", + "Discovered Properties" : "Propriétés découvertes", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Voulez-vous supprimer définitivement toutes les entrées de journal d'audit filtrées ? Cette action est irréversible.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Voulez-vous supprimer définitivement cette entrée de journal d'audit ? Cette action est irréversible.", + "Documents" : "Documents", + "Download API Spec" : "Télécharger la spécification API", + "Edit" : "Modifier", + "Edit Register" : "Modifier le registre", + "Edit Schema" : "Modifier le schéma", + "Edit View" : "Modifier la vue", + "Edit Webhook" : "Modifier le webhook", + "Edit view" : "Modifier la vue", + "Edit view details" : "Modifier les détails de la vue", + "Effectiveness" : "Efficacité", + "Email" : "Courriel", + "Emails" : "Courriels", + "Embedding Model" : "Modèle de plongement", + "Embedding Provider" : "Fournisseur de plongement", + "Embedding generated successfully" : "Plongement généré avec succès", + "Embedding provider connection successful!" : "Connexion au fournisseur de plongement réussie !", + "Enable" : "Activer", + "Enable auto-creation" : "Activer la création automatique", + "Enable automatic file vectorization" : "Activer la vectorisation automatique des fichiers", + "Enable automatic object vectorization" : "Activer la vectorisation automatique des objets", + "Enable automatic synchronization" : "Activer la synchronisation automatique", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Activer la journalisation détaillée des opérations SOLR (recommandé pour le débogage)", + "Enable faceting" : "Activer les facettes", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Activez ou désactivez les fonctionnalités LLM. Configurez les fournisseurs et les modèles à l'aide du bouton Configuration LLM ci-dessus.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Activez ou désactivez l'intégration de la recherche SOLR. Configurez les réglages de connexion à l'aide du bouton Réglages de connexion ci-dessus.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Activez ou désactivez l'intégration des flux de travail n8n. Configurez les réglages de connexion ci-dessous.", + "Enable or disable this webhook" : "Activer ou désactiver ce webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Activer la vectorisation pour toutes les vues existantes et futures (peut augmenter les coûts)", + "Enabled" : "Activé", + "Endpoints" : "Points d'accès", + "Enter ConfigSet name" : "Saisir le nom du ConfigSet", + "Enter collection name" : "Saisir le nom de la collection", + "Enter description (optional)..." : "Saisir une description (facultatif)...", + "Enter new collection name" : "Saisir le nom de la nouvelle collection", + "Enter object ID" : "Saisir l'identifiant de l'objet", + "Enter search term" : "Saisir le terme de recherche", + "Enter view name..." : "Saisir le nom de la vue...", + "Enter webhook name" : "Saisir le nom du webhook", + "Entities" : "Entités", + "Entity Information" : "Informations sur l'entité", + "Entity deleted successfully" : "Entité supprimée avec succès", + "Entity not found" : "Entité introuvable", + "Entries to be deleted:" : "Entrées à supprimer :", + "Enum Issue" : "Problème d'énumération", + "Enum constraint is missing" : "La contrainte d'énumération est manquante", + "Error" : "Erreur", + "Error Details" : "Détails de l'erreur", + "Error Information" : "Informations sur l'erreur", + "Error Message" : "Message d'erreur", + "Error loading application" : "Erreur lors du chargement de l'application", + "Error loading audit trails" : "Erreur lors du chargement des journaux d'audit", + "Error loading entity" : "Erreur lors du chargement de l'entité", + "Error loading search trails" : "Erreur lors du chargement des journaux de recherche", + "Estimated Batches:" : "Lots estimés :", + "Estimated Cost:" : "Coût estimé :", + "Estimated Duration:" : "Durée estimée :", + "Event" : "Événement", + "Event Property for Payload" : "Propriété d'événement pour la charge utile", + "Events" : "Événements", + "Example Value" : "Exemple de valeur", + "Exclusive Maximum" : "Maximum exclusif", + "Exclusive Minimum" : "Minimum exclusif", + "Execution Mode" : "Mode d'exécution", + "Execution Time" : "Temps d'exécution", + "Execution Time Range" : "Plage de temps d'exécution", + "Existing Improvements" : "Améliorations existantes", + "Exponential" : "Exponentiel", + "Export" : "Exporter", + "Export completed successfully" : "Exportation terminée avec succès", + "Export, view, or delete audit trails" : "Exporter, afficher ou supprimer les journaux d'audit", + "Extend Schema" : "Étendre le schéma", + "Extended" : "Étendu", + "Extensions" : "Extensions", + "Extract Pending Files" : "Extraire les fichiers en attente", + "Extract properties from each object" : "Extraire les propriétés de chaque objet", + "Extracted At" : "Extrait le", + "Extraction Status" : "Statut de l'extraction", + "Facet configuration updated successfully" : "Configuration des facettes mise à jour avec succès", + "Facets discovered and configured successfully" : "Facettes découvertes et configurées avec succès", + "Facets discovered successfully" : "Facettes découvertes avec succès", + "Failed" : "Échoué", + "Failed to analyze schema properties" : "Échec de l'analyse des propriétés du schéma", + "Failed to archive conversation" : "Échec de l'archivage de la conversation", + "Failed to calculate sizes" : "Échec du calcul des tailles", + "Failed to clear collection" : "Échec de l'effacement de la collection", + "Failed to copy changes" : "Échec de la copie des modifications", + "Failed to copy data" : "Échec de la copie des données", + "Failed to copy data to clipboard" : "Échec de la copie des données dans le presse-papiers", + "Failed to create conversation" : "Échec de la création de la conversation", + "Failed to create or find project" : "Échec de la création ou de la recherche du projet", + "Failed to delete collection" : "Échec de la suppression de la collection", + "Failed to delete conversation" : "Échec de la suppression de la conversation", + "Failed to delete webhook" : "Échec de la suppression du webhook", + "Failed to download API specification" : "Échec du téléchargement de la spécification API", + "Failed to get SOLR field configuration" : "Échec de la récupération de la configuration des champs SOLR", + "Failed to get entity categories" : "Échec de la récupération des catégories d'entités", + "Failed to get entity statistics" : "Échec de la récupération des statistiques d'entités", + "Failed to get entity types" : "Échec de la récupération des types d'entités", + "Failed to load LLM configuration" : "Échec du chargement de la configuration LLM", + "Failed to load Nextcloud groups" : "Échec du chargement des groupes Nextcloud", + "Failed to load conversation" : "Échec du chargement de la conversation", + "Failed to load entities" : "Échec du chargement des entités", + "Failed to load entity" : "Échec du chargement de l'entité", + "Failed to load files" : "Échec du chargement des fichiers", + "Failed to load organisations" : "Échec du chargement des organisations", + "Failed to load templates" : "Échec du chargement des modèles", + "Failed to load webhooks" : "Échec du chargement des webhooks", + "Failed to load workflows" : "Échec du chargement des flux de travail", + "Failed to refresh database information" : "Échec de l'actualisation des informations de la base de données", + "Failed to reindex collection" : "Échec de la réindexation de la collection", + "Failed to rename conversation" : "Échec du renommage de la conversation", + "Failed to restore conversation" : "Échec de la restauration de la conversation", + "Failed to retry extraction" : "Échec de la nouvelle tentative d'extraction", + "Failed to retry webhook" : "Échec de la nouvelle tentative du webhook", + "Failed to save GitHub token" : "Échec de l'enregistrement du jeton GitHub", + "Failed to save GitLab URL" : "Échec de l'enregistrement de l'URL GitLab", + "Failed to save GitLab token" : "Échec de l'enregistrement du jeton GitLab", + "Failed to save additional feedback" : "Échec de l'enregistrement du retour supplémentaire", + "Failed to save n8n configuration" : "Échec de l'enregistrement de la configuration n8n", + "Failed to save roles" : "Échec de l'enregistrement des rôles", + "Failed to save settings" : "Échec de l'enregistrement des réglages", + "Failed to save webhook" : "Échec de l'enregistrement du webhook", + "Failed to send feedback" : "Échec de l'envoi du retour", + "Failed to test webhook" : "Échec du test du webhook", + "Failed to update favorite status" : "Échec de la mise à jour du statut de favori", + "Failed to update schema properties" : "Échec de la mise à jour des propriétés du schéma", + "Failed to update view" : "Échec de la mise à jour de la vue", + "Failed to update webhook" : "Échec de la mise à jour du webhook", + "Feedback recorded" : "Retour enregistré", + "Fewer sources (1-3):" : "Moins de sources (1-3) :", + "Field" : "Champ", + "File" : "Fichier", + "File Chunk Prediction" : "Prédiction des fragments de fichier", + "File Collection" : "Collection de fichiers", + "File Management" : "Gestion des fichiers", + "File Name" : "Nom du fichier", + "File Path" : "Chemin du fichier", + "File Type Selection" : "Sélection du type de fichier", + "File Vectorization" : "Vectorisation des fichiers", + "File Warmup" : "Préchauffage des fichiers", + "File actions menu" : "Menu des actions de fichier", + "File anonymized successfully" : "Fichier anonymisé avec succès", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "La vectorisation des fragments de fichier n'est pas encore implémentée. Les fragments sont prêts et stockés, mais le service de vectorisation est en cours de développement.", + "File collection not configured" : "Collection de fichiers non configurée", + "File discovery completed" : "Découverte des fichiers terminée", + "File extraction completed" : "Extraction des fichiers terminée", + "File extraction queued" : "Extraction des fichiers mise en file d'attente", + "File indexed successfully" : "Fichier indexé avec succès", + "File is already anonymized" : "Le fichier est déjà anonymisé", + "File not found" : "Fichier introuvable", + "File settings updated successfully" : "Réglages du fichier mis à jour avec succès", + "File sources" : "Sources de fichiers", + "File vectorization configuration saved successfully" : "Configuration de la vectorisation des fichiers enregistrée avec succès", + "File vectorization started. Check the statistics section for progress." : "Vectorisation des fichiers démarrée. Consultez la section des statistiques pour suivre la progression.", + "File warmup completed" : "Préchauffage des fichiers terminé", + "Files" : "Fichiers", + "Files with Completed Extraction:" : "Fichiers avec extraction terminée :", + "Files → fileCollection, Objects → objectCollection" : "Fichiers → fileCollection, Objets → objectCollection", + "Filter Audit Trails" : "Filtrer les journaux d'audit", + "Filter Deleted Items" : "Filtrer les éléments supprimés", + "Filter Objects" : "Filtrer les objets", + "Filter Properties" : "Filtrer les propriétés", + "Filter Search Trails" : "Filtrer les journaux de recherche", + "Filter Statistics" : "Filtrer les statistiques", + "Filter and analyze search trail entries" : "Filtrer et analyser les entrées du journal de recherche", + "Filter and manage audit trail entries" : "Filtrer et gérer les entrées du journal d'audit", + "Filter and manage soft deleted items" : "Filtrer et gérer les éléments supprimés de façon réversible", + "Filter by object ID" : "Filtrer par identifiant d'objet", + "Filter by search term" : "Filtrer par terme de recherche", + "Filter by webhook" : "Filtrer par webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Données de filtre chargées automatiquement. Utilisez les filtres ci-dessous pour affiner votre recherche.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrer les déclencheurs de webhook par propriétés de charge utile (un par ligne, format : clé: valeur)", + "Filtered" : "Filtré", + "Filters" : "Filtres", + "Fireworks AI Chat Settings" : "Réglages de discussion Fireworks AI", + "Fireworks AI Embedding Configuration" : "Configuration des plongements Fireworks AI", + "First" : "Premier", + "Fixed" : "Fixe", + "For chat and retrieval-augmented generation" : "Pour la discussion et la génération augmentée par récupération", + "For vector embeddings and semantic search" : "Pour les plongements vectoriels et la recherche sémantique", + "Format" : "Format", + "Format Issue" : "Problème de format", + "Format constraint is missing" : "La contrainte de format est manquante", + "From Date" : "Date de début", + "From date" : "Date de début", + "Full data copied to clipboard" : "Toutes les données copiées dans le presse-papiers", + "General Issue" : "Problème général", + "Generate recommendations and confidence scores" : "Générer des recommandations et des scores de confiance", + "Generate vectors immediately when new objects are created" : "Générer les vecteurs immédiatement lorsque de nouveaux objets sont créés", + "GitHub token is valid" : "Le jeton GitHub est valide", + "GitHub token removed successfully" : "Jeton GitHub supprimé avec succès", + "GitHub token saved successfully" : "Jeton GitHub enregistré avec succès", + "GitLab URL saved successfully" : "URL GitLab enregistrée avec succès", + "GitLab token is valid" : "Le jeton GitLab est valide", + "GitLab token saved successfully" : "Jeton GitLab enregistré avec succès", + "HTTP Method" : "Méthode HTTP", + "HTTP method used to send webhook requests" : "Méthode HTTP utilisée pour envoyer les requêtes de webhook", + "Headers" : "En-têtes", + "Health" : "Santé", + "Heartbeat successful - connection kept alive" : "Pulsation réussie — connexion maintenue active", + "Helpful" : "Utile", + "Hide Filters" : "Masquer les filtres", + "Hide in forms" : "Masquer dans les formulaires", + "Hide in list view" : "Masquer dans la vue en liste", + "High" : "Élevé", + "High Confidence" : "Confiance élevée", + "Host" : "Hôte", + "Hourly" : "Toutes les heures", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Profondeur de parcours des propriétés d'objet imbriquées (1-20). Des valeurs plus élevées capturent plus de détails mais augmentent la taille du vecteur.", + "How often to check for updates (1-168 hours)" : "Fréquence de vérification des mises à jour (1-168 heures)", + "How to handle retries for failed webhook deliveries" : "Comment gérer les nouvelles tentatives pour les livraisons de webhook échouées", + "ID" : "ID", + "ID:" : "ID :", + "IP rate limits cleared successfully" : "Limites de débit par IP effacées avec succès", + "Identify properties not in the schema" : "Identifier les propriétés absentes du schéma", + "Immutable" : "Immuable", + "Import" : "Importer", + "Import successful" : "Importation réussie", + "Improved Property" : "Propriété améliorée", + "In use" : "Utilisé", + "Inactive" : "Inactif", + "Include IDs and names of related objects for better contextual search" : "Inclure les identifiants et les noms des objets liés pour une meilleure recherche contextuelle", + "Include related object references" : "Inclure les références d'objets liés", + "Include schema and register metadata" : "Inclure les métadonnées du schéma et du registre", + "Initialization failed" : "Échec de l'initialisation", + "Initialize Project" : "Initialiser le projet", + "Inspect Fields" : "Inspecter les champs", + "Inspect Index" : "Inspecter l'index", + "Invalid" : "Invalide", + "Invalid batch size. Must be between 1 and 5000" : "Taille de lot invalide. Doit être comprise entre 1 et 5000", + "Invalid field name provided" : "Nom de champ fourni invalide", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects invalide. Doit être 0 (tous) ou un nombre positif", + "Invalid:" : "Invalide :", + "Issue" : "Problème", + "Items per page:" : "Éléments par page :", + "Just now" : "À l'instant", + "LLM Configuration" : "Configuration LLM", + "LLM actions menu" : "Menu des actions LLM", + "LLM configuration saved successfully" : "Configuration LLM enregistrée avec succès", + "LLM features disabled" : "Fonctionnalités LLM désactivées", + "LLM features enabled" : "Fonctionnalités LLM activées", + "LLM must be enabled with an embedding provider configured" : "Le LLM doit être activé avec un fournisseur de plongement configuré", + "LLM settings updated successfully" : "Réglages LLM mis à jour avec succès", + "Last" : "Dernier", + "Last Triggered" : "Dernier déclenchement", + "Length Range:" : "Plage de longueur :", + "Linear" : "Linéaire", + "Load Advanced Filters" : "Charger les filtres avancés", + "Load advanced filters with live data from your search index" : "Charger les filtres avancés avec les données en direct de votre index de recherche", + "Load view" : "Charger la vue", + "Loading ConfigSets..." : "Chargement des ConfigSets...", + "Loading advanced filters..." : "Chargement des filtres avancés...", + "Loading agents..." : "Chargement des agents...", + "Loading application details..." : "Chargement des détails de l'application...", + "Loading applications..." : "Chargement des applications...", + "Loading archived conversations..." : "Chargement des conversations archivées...", + "Loading audit trails..." : "Chargement des journaux d'audit...", + "Loading collections..." : "Chargement des collections...", + "Loading configurations..." : "Chargement des configurations...", + "Loading conversation..." : "Chargement de la conversation...", + "Loading conversations..." : "Chargement des conversations...", + "Loading deleted items..." : "Chargement des éléments supprimés...", + "Loading events..." : "Chargement des événements...", + "Loading filter data..." : "Chargement des données de filtre...", + "Loading groups..." : "Chargement des groupes...", + "Loading log details..." : "Chargement des détails du journal...", + "Loading registers..." : "Chargement des registres...", + "Loading schemas..." : "Chargement des schémas...", + "Loading search trails..." : "Chargement des journaux de recherche...", + "Loading sources..." : "Chargement des sources...", + "Loading statistics..." : "Chargement des statistiques...", + "Loading users..." : "Chargement des utilisateurs...", + "Loading views..." : "Chargement des vues...", + "Loading..." : "Chargement...", + "Local" : "Local", + "Local Version" : "Version locale", + "Locked" : "Verrouillé", + "Locked:" : "Verrouillé :", + "Login successful" : "Connexion réussie", + "Logs" : "Journaux", + "Low" : "Faible", + "Low Confidence" : "Confiance faible", + "Lucene Version:" : "Version de Lucene :", + "Magic table synchronized successfully" : "Table magique synchronisée avec succès", + "Manage Organisation Roles" : "Gérer les rôles de l'organisation", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Gérez les collections SOLR (entrepôts de données) et affectez-les aux objets et aux fichiers.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Gérez les ConfigSets SOLR (modèles de configuration) pour vos collections.", + "Manage and configure agents for automated tasks" : "Gérez et configurez des agents pour les tâches automatisées", + "Manage and monitor file text extraction status" : "Gérez et surveillez le statut de l'extraction de texte des fichiers", + "Manage and restore soft deleted items from your registers" : "Gérez et restaurez les éléments supprimés de façon réversible de vos registres", + "Manage and view detected entities from files and objects" : "Gérez et consultez les entités détectées dans les fichiers et les objets", + "Manage document templates and themes" : "Gérez les modèles et les thèmes de documents", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Gérez les flux de travail n8n pour l'automatisation d'OpenRegister. Les flux de travail seront stockés dans le projet configuré.", + "Manage webhooks for event-driven integrations" : "Gérez les webhooks pour les intégrations pilotées par événements", + "Manage your applications and modules" : "Gérez vos applications et modules", + "Manage your data registers and their configurations" : "Gérez vos registres de données et leurs configurations", + "Manage your data schemas and their properties" : "Gérez vos schémas de données et leurs propriétés", + "Manage your data sources and their configurations" : "Gérez vos sources de données et leurs configurations", + "Manage your saved search configurations" : "Gérez vos configurations de recherche enregistrées", + "Manage your system configurations and settings" : "Gérez vos configurations et réglages système", + "Managed" : "Géré", + "Max Files (0 = all)" : "Nb max de fichiers (0 = tous)", + "Max Length" : "Longueur max", + "Max Length:" : "Longueur max :", + "Max Objects (0 = all)" : "Nb max d'objets (0 = tous)", + "Max Retries" : "Nb max de tentatives", + "Max Shards/Node" : "Nb max de fragments/nœud", + "Max execution time (ms)" : "Temps d'exécution max (ms)", + "Max ms" : "ms max", + "Max result count" : "Nombre max de résultats", + "Max results" : "Résultats max", + "Maximum" : "Maximum", + "Maximum Nesting Depth" : "Profondeur d'imbrication maximale", + "Maximum length constraint is missing" : "La contrainte de longueur maximale est manquante", + "Maximum length is too restrictive" : "La longueur maximale est trop restrictive", + "Maximum number of files to process. Set to 0 to process all files." : "Nombre maximal de fichiers à traiter. Définissez sur 0 pour traiter tous les fichiers.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Nombre maximal d'objets à traiter. Définissez sur 0 pour traiter tous les objets.", + "Maximum number of retry attempts for failed deliveries" : "Nombre maximal de nouvelles tentatives pour les livraisons échouées", + "Maximum time to wait before committing changes" : "Temps maximal d'attente avant de valider les modifications", + "Maximum tokens to generate" : "Nombre maximal de jetons à générer", + "Maximum value constraint is missing" : "La contrainte de valeur maximale est manquante", + "Maximum value is too restrictive" : "La valeur maximale est trop restrictive", + "Medium" : "Moyen", + "Medium Confidence" : "Confiance moyenne", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Requêtes moyennes : recherches avec un certain filtrage ou plusieurs paramètres (par ex. plages de dates, registres/schémas spécifiques)", + "Members of selected groups can access this view" : "Les membres des groupes sélectionnés peuvent accéder à cette vue", + "Members:" : "Membres :", + "Memory prediction calculated successfully" : "Prédiction de mémoire calculée avec succès", + "Message content is required" : "Le contenu du message est requis", + "Message does not belong to this conversation" : "Le message n'appartient pas à cette conversation", + "Metadata Filters" : "Filtres de métadonnées", + "Method" : "Méthode", + "Min Length" : "Longueur min", + "Min execution time (ms)" : "Temps d'exécution min (ms)", + "Min ms" : "ms min", + "Min result count" : "Nombre min de résultats", + "Min results" : "Résultats min", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "La contrainte de valeur minimale est manquante", + "Minimum value is too restrictive" : "La valeur minimale est trop restrictive", + "Missing object fields created successfully" : "Champs d'objet manquants créés avec succès", + "Mode:" : "Mode :", + "Model" : "Modèle", + "Model Name" : "Nom du modèle", + "Modified" : "Modifié", + "Monthly" : "Mensuel", + "More sources (10-20):" : "Plus de sources (10-20) :", + "Most Active Objects" : "Objets les plus actifs", + "Multiple Of" : "Multiple de", + "Name" : "Nom", + "Names cache warmup completed" : "Préchauffage du cache des noms terminé", + "New Collection Name" : "Nom de la nouvelle collection", + "New Conversation" : "Nouvelle conversation", + "New Properties" : "Nouvelles propriétés", + "New Property" : "Nouvelle propriété", + "New Value" : "Nouvelle valeur", + "New users without specific organisation membership will be automatically added to this organisation" : "Les nouveaux utilisateurs sans appartenance à une organisation spécifique seront automatiquement ajoutés à cette organisation", + "Next" : "Suivant", + "No" : "Non", + "No ConfigSets found" : "Aucun ConfigSet trouvé", + "No GitHub token configured" : "Aucun jeton GitHub configuré", + "No GitHub token provided" : "Aucun jeton GitHub fourni", + "No GitLab token provided" : "Aucun jeton GitLab fourni", + "No Organisation" : "Aucune organisation", + "No active filters" : "Aucun filtre actif", + "No activity data available" : "Aucune donnée d'activité disponible", + "No agents available" : "Aucun agent disponible", + "No agents found" : "Aucun agent trouvé", + "No app store cache exists yet" : "Aucun cache de l'App Store n'existe encore", + "No applications are available." : "Aucune application n'est disponible.", + "No applications found" : "Aucune application trouvée", + "No archived conversations" : "Aucune conversation archivée", + "No audit trail entries found" : "Aucune entrée de journal d'audit trouvée", + "No changes recorded" : "Aucune modification enregistrée", + "No chunks to vectorize" : "Aucun fragment à vectoriser", + "No collections found" : "Aucune collection trouvée", + "No configuration data" : "Aucune donnée de configuration", + "No configurations are available." : "Aucune configuration n'est disponible.", + "No configurations found" : "Aucune configuration trouvée", + "No configurations found for this application." : "Aucune configuration trouvée pour cette application.", + "No conversations yet. Create a new one to get started!" : "Aucune conversation pour l'instant. Créez-en une nouvelle pour commencer !", + "No data available" : "Aucune donnée disponible", + "No data available for chart" : "Aucune donnée disponible pour le graphique", + "No deleted items found" : "Aucun élément supprimé trouvé", + "No deletion data available" : "Aucune donnée de suppression disponible", + "No description found" : "Aucune description trouvée", + "No description provided" : "Aucune description fournie", + "No entities detected in this file. Run text extraction first." : "Aucune entité détectée dans ce fichier. Exécutez d'abord l'extraction de texte.", + "No entities found" : "Aucune entité trouvée", + "No entities have been detected yet" : "Aucune entité n'a encore été détectée", + "No events found" : "Aucun événement trouvé", + "No expired audit trails found to clear" : "Aucun journal d'audit expiré à effacer", + "No facetable fields available. Select a register and schema to see available filters." : "Aucun champ à facettes disponible. Sélectionnez un registre et un schéma pour voir les filtres disponibles.", + "No files found" : "Aucun fichier trouvé", + "No files have been extracted yet" : "Aucun fichier n'a encore été extrait", + "No files to process" : "Aucun fichier à traiter", + "No files to reindex" : "Aucun fichier à réindexer", + "No filters are currently active. This will delete ALL audit trail entries!" : "Aucun filtre n'est actuellement actif. Cela supprimera TOUTES les entrées de journal d'audit !", + "No log entries found" : "Aucune entrée de journal trouvée", + "No logs are available for this configuration." : "Aucun journal n'est disponible pour cette configuration.", + "No logs are available for this source." : "Aucun journal n'est disponible pour cette source.", + "No logs found" : "Aucun journal trouvé", + "No mismatched fields found - SOLR schema is properly configured" : "Aucun champ incohérent trouvé — le schéma SOLR est correctement configuré", + "No objects found" : "Aucun objet trouvé", + "No objects found to vectorize" : "Aucun objet trouvé à vectoriser", + "No objects selected for deletion" : "Aucun objet sélectionné pour la suppression", + "No objects to delete" : "Aucun objet à supprimer", + "No objects were permanently deleted" : "Aucun objet n'a été supprimé définitivement", + "No properties found" : "Aucune propriété trouvée", + "No properties match your filters." : "Aucune propriété ne correspond à vos filtres.", + "No purge date set" : "Aucune date de purge définie", + "No register data available" : "Aucune donnée de registre disponible", + "No registers found" : "Aucun registre trouvé", + "No registers found for this application." : "Aucun registre trouvé pour cette application.", + "No relations found" : "Aucune relation trouvée", + "No request body available" : "Aucun corps de requête disponible", + "No response body available" : "Aucun corps de réponse disponible", + "No saved views yet. Create one in the Search tab!" : "Aucune vue enregistrée pour l'instant. Créez-en une dans l'onglet Recherche !", + "No schema data available" : "Aucune donnée de schéma disponible", + "No schema selected for exploration" : "Aucun schéma sélectionné pour l'exploration", + "No schemas found" : "Aucun schéma trouvé", + "No schemas found for this application." : "Aucun schéma trouvé pour cette application.", + "No search terms data available" : "Aucune donnée de termes de recherche disponible", + "No search trail entries found" : "Aucune entrée de journal de recherche trouvée", + "No sources are available." : "Aucune source n'est disponible.", + "No sources found" : "Aucune source trouvée", + "No templates found" : "Aucun modèle trouvé", + "No templates have been created yet" : "Aucun modèle n'a encore été créé", + "No valid magic-mapped register+schema combinations found" : "Aucune combinaison registre+schéma à mappage magique valide trouvée", + "No views found. Create views first before configuring vectorization." : "Aucune vue trouvée. Créez d'abord des vues avant de configurer la vectorisation.", + "No views match your search" : "Aucune vue ne correspond à votre recherche", + "No webhooks found" : "Aucun webhook trouvé", + "No webhooks have been configured yet" : "Aucun webhook n'a encore été configuré", + "No workflows found in this project. Create workflows in the n8n editor." : "Aucun flux de travail trouvé dans ce projet. Créez des flux de travail dans l'éditeur n8n.", + "None" : "Aucun", + "Not helpful" : "Pas utile", + "Not in use" : "Non utilisé", + "Number Constraints" : "Contraintes numériques", + "Number of chunks to vectorize in one API call (1-100)." : "Nombre de fragments à vectoriser en un seul appel API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Nombre de fragments à vectoriser en un seul appel API. Plus élevé = plus rapide mais plus de mémoire. Recommandé : 10-50.", + "Number of objects to process in each batch (1-5000)." : "Nombre d'objets à traiter dans chaque lot (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Nombre d'objets à vectoriser en un seul appel API. Plus élevé = plus rapide mais plus de mémoire. Recommandé : 10-50.", + "Numeric Range:" : "Plage numérique :", + "Object" : "Objet", + "Object Collection" : "Collection d'objets", + "Object Count Prediction" : "Prédiction du nombre d'objets", + "Object ID" : "Identifiant de l'objet", + "Object Management" : "Gestion des objets", + "Object Vectorization" : "Vectorisation des objets", + "Object Warmup" : "Préchauffage des objets", + "Object collection not configured" : "Collection d'objets non configurée", + "Object not found" : "Objet introuvable", + "Object permanently deleted" : "Objet supprimé définitivement", + "Object restored successfully" : "Objet restauré avec succès", + "Object settings updated successfully" : "Réglages de l'objet mis à jour avec succès", + "Object sources" : "Sources d'objets", + "Object unlocked successfully" : "Objet déverrouillé avec succès", + "Object vectorization configuration saved successfully" : "Configuration de la vectorisation des objets enregistrée avec succès", + "Object vectorized successfully" : "Objet vectorisé avec succès", + "Object:" : "Objet :", + "Objects" : "Objets", + "Objects Analyzed" : "Objets analysés", + "Objects Distribution" : "Répartition des objets", + "Objects being analyzed" : "Objets en cours d'analyse", + "Objects by Register" : "Objets par registre", + "Objects by Schema" : "Objets par schéma", + "Objects deletion completed successfully" : "Suppression des objets terminée avec succès", + "Objects in schema" : "Objets dans le schéma", + "Objects to Process:" : "Objets à traiter :", + "Objects to be analyzed" : "Objets à analyser", + "Objects to be deleted" : "Objets à supprimer", + "Objects to be validated" : "Objets à valider", + "Objects will be serialized as JSON text before vectorization" : "Les objets seront sérialisés en texte JSON avant la vectorisation", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Les objets seront supprimés de façon réversible (marqués comme supprimés mais conservés dans la base de données). Ils pourront être récupérés ultérieurement si nécessaire.", + "Old Value" : "Ancienne valeur", + "Oldest Item (days)" : "Élément le plus ancien (jours)", + "Ollama Chat Settings" : "Réglages de discussion Ollama", + "Ollama Embedding Configuration" : "Configuration des plongements Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Uniquement avec modifications", + "Open Register Objects" : "Objets Open Register", + "Open n8n Editor" : "Ouvrir l'éditeur n8n", + "OpenAI Chat Settings" : "Réglages de discussion OpenAI", + "OpenAI Embedding Configuration" : "Configuration des plongements OpenAI", + "OpenRegister Settings" : "Réglages OpenRegister", + "Optional webhook secret for signature verification" : "Secret de webhook facultatif pour la vérification de la signature", + "Order" : "Ordre", + "Organisation" : "Organisation", + "Organisation ID" : "Identifiant de l'organisation", + "Organisation Members" : "Membres de l'organisation", + "Organisation Statistics" : "Statistiques de l'organisation", + "Organisation created successfully" : "Organisation créée avec succès", + "Organisation settings saved successfully" : "Réglages de l'organisation enregistrés avec succès", + "Organisations" : "Organisations", + "Organization" : "Organisation", + "Organization ID (Optional)" : "Identifiant de l'organisation (facultatif)", + "Orphaned Items" : "Éléments orphelins", + "Owner" : "Propriétaire", + "Owner:" : "Propriétaire :", + "Parallel Mode (Faster, more resource intensive)" : "Mode parallèle (plus rapide, plus gourmand en ressources)", + "Parallel:" : "Parallèle :", + "Password" : "Mot de passe", + "Password for SOLR authentication (optional)" : "Mot de passe pour l'authentification SOLR (facultatif)", + "Path" : "Chemin", + "Path in repository" : "Chemin dans le dépôt", + "Path where the configuration file will be saved in the repository" : "Chemin où le fichier de configuration sera enregistré dans le dépôt", + "Path where the register OAS file will be saved in the repository" : "Chemin où le fichier OAS du registre sera enregistré dans le dépôt", + "Pattern (regex)" : "Motif (regex)", + "Pattern Issue" : "Problème de motif", + "Pattern constraint is missing" : "La contrainte de motif est manquante", + "Patterns:" : "Motifs :", + "Pending" : "En attente", + "Permanent Delete Mode" : "Mode de suppression définitive", + "Permanently Delete" : "Supprimer définitivement", + "Permanently delete already soft-deleted objects" : "Supprimer définitivement les objets déjà supprimés de façon réversible", + "Permanently delete this conversation?" : "Supprimer définitivement cette conversation ?", + "Person" : "Personne", + "Personal Data" : "Données personnelles", + "Phone" : "Téléphone", + "Photos" : "Photos", + "Please create an agent in the" : "Veuillez créer un agent dans le", + "Please select an agent to continue" : "Veuillez sélectionner un agent pour continuer", + "Please select which register and schema to use for the new object" : "Veuillez sélectionner le registre et le schéma à utiliser pour le nouvel objet", + "Please try again later." : "Veuillez réessayer ultérieurement.", + "Please wait while we fetch your agents." : "Veuillez patienter pendant que nous récupérons vos agents.", + "Please wait while we fetch your applications." : "Veuillez patienter pendant que nous récupérons vos applications.", + "Please wait while we fetch your configurations." : "Veuillez patienter pendant que nous récupérons vos configurations.", + "Please wait while we fetch your deleted items." : "Veuillez patienter pendant que nous récupérons vos éléments supprimés.", + "Please wait while we fetch your sources." : "Veuillez patienter pendant que nous récupérons vos sources.", + "Polls" : "Sondages", + "Popular Search Terms" : "Termes de recherche populaires", + "Port" : "Port", + "Prerequisites" : "Prérequis", + "Press Enter to send, Shift+Enter for new line" : "Appuyez sur Entrée pour envoyer, Maj+Entrée pour un saut de ligne", + "Previous" : "Précédent", + "Private" : "Privé", + "Processes chunks in batches with simulated parallelism." : "Traite les fragments par lots avec un parallélisme simulé.", + "Processes file chunks sequentially (safest)." : "Traite les fragments de fichier de manière séquentielle (le plus sûr).", + "Processes objects in chunks with simulated parallelism." : "Traite les objets par fragments avec un parallélisme simulé.", + "Processes objects sequentially (safest)." : "Traite les objets de manière séquentielle (le plus sûr).", + "Processing" : "Traitement", + "Processing Limits" : "Limites de traitement", + "Processing..." : "Traitement en cours...", + "Project initialized successfully" : "Projet initialisé avec succès", + "Project not found. Please initialize first." : "Projet introuvable. Veuillez d'abord l'initialiser.", + "Properties" : "Propriétés", + "Property Behaviors" : "Comportements de la propriété", + "Property Configuration:" : "Configuration de la propriété :", + "Property Filters" : "Filtres de propriété", + "Property Title" : "Titre de la propriété", + "Property Type" : "Type de propriété", + "Property can be improved" : "La propriété peut être améliorée", + "Protected" : "Protégé", + "Provider" : "Fournisseur", + "Provider is required for testing" : "Le fournisseur est requis pour le test", + "Public" : "Public", + "Public View" : "Vue publique", + "Public views can be accessed by anyone in the system" : "Les vues publiques sont accessibles à tous les utilisateurs du système", + "Publish" : "Publier", + "Publish Objects" : "Publier les objets", + "Publish all objects in this schema" : "Publier tous les objets de ce schéma", + "Published" : "Publié", + "Published:" : "Publié :", + "Publishing..." : "Publication en cours...", + "Purge" : "Purger", + "Purge Date" : "Date de purge", + "Query Complexity Distribution" : "Répartition de la complexité des requêtes", + "Query parameter is required" : "Le paramètre de requête est requis", + "RAG" : "RAG", + "RAG Configuration" : "Configuration RAG", + "Rate limits cleared successfully" : "Limites de débit effacées avec succès", + "Raw Changes Data" : "Données brutes des modifications", + "Re-vectorize on object update" : "Revectoriser lors de la mise à jour de l'objet", + "Read" : "Lecture", + "Recommendations:" : "Recommandations :", + "Recommended Type:" : "Type recommandé :", + "Recommended: 5 sources" : "Recommandé : 5 sources", + "Refresh" : "Actualiser", + "Refresh Data" : "Actualiser les données", + "Refresh Stats" : "Actualiser les statistiques", + "Refresh Workflows" : "Actualiser les flux de travail", + "Refresh dashboard" : "Actualiser le tableau de bord", + "Refresh database info" : "Actualiser les informations de la base de données", + "Register" : "Registre", + "Register ID" : "Identifiant du registre", + "Register Statistics" : "Statistiques du registre", + "Register Totals" : "Totaux du registre", + "Register depublished successfully" : "Registre dépublié avec succès", + "Register not found" : "Registre introuvable", + "Register objects deletion completed successfully" : "Suppression des objets du registre terminée avec succès", + "Register published successfully" : "Registre publié avec succès", + "Register/Schema Usage" : "Utilisation registre/schéma", + "Registers" : "Registres", + "Reindex" : "Réindexer", + "Reindex completed" : "Réindexation terminée", + "Reindex completed successfully" : "Réindexation terminée avec succès", + "Reindex failed" : "Échec de la réindexation", + "Relations" : "Relations", + "Remote Version" : "Version distante", + "Remove" : "Supprimer", + "Remove filter" : "Supprimer le filtre", + "Remove from favorites" : "Retirer des favoris", + "Remove group" : "Supprimer le groupe", + "Removed" : "Supprimé", + "Removed from favorites" : "Retiré des favoris", + "Rename Conversation" : "Renommer la conversation", + "Rename conversation" : "Renommer la conversation", + "Replicas" : "Réplicas", + "Repository" : "Dépôt", + "Request Body" : "Corps de la requête", + "Request Data" : "Données de la requête", + "Request Details" : "Détails de la requête", + "Request body must be JSON with a \"query\" field" : "Le corps de la requête doit être au format JSON avec un champ « query »", + "Request timeout in seconds" : "Délai d'attente de la requête en secondes", + "Required field" : "Champ obligatoire", + "Required status is inconsistent" : "Le statut « obligatoire » est incohérent", + "Rerun Search" : "Relancer la recherche", + "Reset Changes" : "Réinitialiser les modifications", + "Reset Filters" : "Réinitialiser les filtres", + "Response Body" : "Corps de la réponse", + "Response Details" : "Détails de la réponse", + "Response Time:" : "Temps de réponse :", + "Restore" : "Restaurer", + "Restore conversation" : "Restaurer la conversation", + "Restore or permanently delete items" : "Restaurer ou supprimer définitivement les éléments", + "Result Count Range" : "Plage du nombre de résultats", + "Results" : "Résultats", + "Retrieve all objects for this schema" : "Récupérer tous les objets de ce schéma", + "Retry" : "Réessayer", + "Retry Failed Extractions" : "Réessayer les extractions échouées", + "Retry Policy" : "Politique de nouvelle tentative", + "Retry completed" : "Nouvelle tentative terminée", + "Retrying..." : "Nouvelle tentative en cours...", + "Risk Level" : "Niveau de risque", + "Roles updated successfully" : "Rôles mis à jour avec succès", + "SOLR Connection Settings" : "Réglages de connexion SOLR", + "SOLR Version:" : "Version de SOLR :", + "SOLR actions menu" : "Menu des actions SOLR", + "SOLR base path (usually /solr)" : "Chemin de base SOLR (généralement /solr)", + "SOLR is disabled" : "SOLR est désactivé", + "SOLR is not available or not configured" : "SOLR n'est pas disponible ou n'est pas configuré", + "SOLR logging disabled" : "Journalisation SOLR désactivée", + "SOLR logging enabled" : "Journalisation SOLR activée", + "SOLR search disabled" : "Recherche SOLR désactivée", + "SOLR search enabled" : "Recherche SOLR activée", + "SOLR server hostname or IP address" : "Nom d'hôte ou adresse IP du serveur SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Numéro de port du serveur SOLR (facultatif, 8983 par défaut)", + "SOLR setup completed successfully" : "Configuration de SOLR terminée avec succès", + "SOLR setup failed" : "Échec de la configuration de SOLR", + "SOLR setup failed - check logs" : "Échec de la configuration de SOLR — consultez les journaux", + "Sample Values:" : "Exemples de valeurs :", + "Save" : "Enregistrer", + "Save Configuration" : "Enregistrer la configuration", + "Save Connection Settings" : "Enregistrer les réglages de connexion", + "Save Roles" : "Enregistrer les rôles", + "Save Settings" : "Enregistrer les réglages", + "Save current search as view" : "Enregistrer la recherche actuelle comme vue", + "Saved Views" : "Vues enregistrées", + "Saving..." : "Enregistrement en cours...", + "Schema" : "Schéma", + "Schema ID" : "Identifiant du schéma", + "Schema Name" : "Nom du schéma", + "Schema Statistics" : "Statistiques du schéma", + "Schema depublished successfully" : "Schéma dépublié avec succès", + "Schema objects deletion completed successfully" : "Suppression des objets du schéma terminée avec succès", + "Schema published successfully" : "Schéma publié avec succès", + "Schema successfully created" : "Schéma créé avec succès", + "Schema successfully updated" : "Schéma mis à jour avec succès", + "Schemas" : "Schémas", + "Scheme" : "Schéma", + "Search / Views" : "Recherche / Vues", + "Search Activity" : "Activité de recherche", + "Search Analytics" : "Analyses de recherche", + "Search Entities" : "Rechercher des entités", + "Search Files" : "Rechercher des fichiers", + "Search GitHub" : "Rechercher sur GitHub", + "Search GitLab" : "Rechercher sur GitLab", + "Search Method" : "Méthode de recherche", + "Search Term" : "Terme de recherche", + "Search Trail Details" : "Détails du journal de recherche", + "Search Trail Management" : "Gestion du journal de recherche", + "Search Trail Statistics" : "Statistiques du journal de recherche", + "Search Trails" : "Journaux de recherche", + "Search Views" : "Rechercher des vues", + "Search Webhooks" : "Rechercher des webhooks", + "Search by file name or path" : "Rechercher par nom de fichier ou chemin", + "Search by name or URL" : "Rechercher par nom ou URL", + "Search by value" : "Rechercher par valeur", + "Search in Files" : "Rechercher dans les fichiers", + "Search in Objects" : "Rechercher dans les objets", + "Search property names..." : "Rechercher des noms de propriété...", + "Search trail deletion not implemented yet" : "La suppression du journal de recherche n'est pas encore implémentée", + "Search views..." : "Rechercher des vues...", + "Secret" : "Secret", + "Secret key for HMAC signature generation (optional)" : "Clé secrète pour la génération de la signature HMAC (facultatif)", + "Select AI Agent" : "Sélectionner un agent IA", + "Select All" : "Tout sélectionner", + "Select ConfigSet" : "Sélectionner un ConfigSet", + "Select File Types to Vectorize:" : "Sélectionner les types de fichiers à vectoriser :", + "Select HTTP method" : "Sélectionner la méthode HTTP", + "Select Register and Schema" : "Sélectionner un registre et un schéma", + "Select Views to Vectorize:" : "Sélectionner les vues à vectoriser :", + "Select a Nextcloud group to add" : "Sélectionner un groupe Nextcloud à ajouter", + "Select a branch" : "Sélectionner une branche", + "Select a model or type a custom model name" : "Sélectionnez un modèle ou saisissez un nom de modèle personnalisé", + "Select a repository" : "Sélectionner un dépôt", + "Select a repository you have write access to" : "Sélectionnez un dépôt sur lequel vous avez un accès en écriture", + "Select an AI Agent" : "Sélectionner un agent IA", + "Select an AI agent to begin chatting with your data." : "Sélectionnez un agent IA pour commencer à discuter avec vos données.", + "Select backend" : "Sélectionner le backend", + "Select chat model" : "Sélectionner le modèle de discussion", + "Select collection for files" : "Sélectionner la collection pour les fichiers", + "Select collection for objects" : "Sélectionner la collection pour les objets", + "Select default organisation" : "Sélectionner l'organisation par défaut", + "Select event to listen to..." : "Sélectionner l'événement à écouter...", + "Select model" : "Sélectionner le modèle", + "Select options..." : "Sélectionner des options...", + "Select period" : "Sélectionner la période", + "Select property to send as payload" : "Sélectionner la propriété à envoyer comme charge utile", + "Select provider" : "Sélectionner le fournisseur", + "Select registers and schemas to save a view" : "Sélectionner des registres et des schémas pour enregistrer une vue", + "Select retry policy" : "Sélectionner la politique de nouvelle tentative", + "Select the branch to publish to" : "Sélectionner la branche de publication", + "Select the event this webhook should listen to" : "Sélectionner l'événement que ce webhook doit écouter", + "Select views to vectorize:" : "Sélectionner les vues à vectoriser :", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Sélectionnez quels groupes Nextcloud sont disponibles pour cette organisation. Les utilisateurs de ces groupes auront accès aux ressources de l'organisation.", + "Select which data views the AI can search" : "Sélectionnez quelles vues de données l'IA peut rechercher", + "Select which property from the event should be used as the webhook payload data" : "Sélectionnez quelle propriété de l'événement doit être utilisée comme données de charge utile du webhook", + "Select which tools the AI can use to perform actions" : "Sélectionnez quels outils l'IA peut utiliser pour effectuer des actions", + "Selected" : "Sélectionné", + "Selected Groups" : "Groupes sélectionnés", + "Selected audit trails deleted successfully" : "Journaux d'audit sélectionnés supprimés avec succès", + "Selected search trails deleted successfully" : "Journaux de recherche sélectionnés supprimés avec succès", + "Selected users can access this view" : "Les utilisateurs sélectionnés peuvent accéder à cette vue", + "Send additional feedback" : "Envoyer un retour supplémentaire", + "Send as CloudEvent" : "Envoyer comme CloudEvent", + "Sensitive PII" : "Données personnelles sensibles", + "Serial Mode (Safer, slower)" : "Mode série (plus sûr, plus lent)", + "Serial:" : "Série :", + "Server Configuration" : "Configuration du serveur", + "Server Information" : "Informations sur le serveur", + "Settings" : "Réglages", + "Shards" : "Fragments", + "Share" : "Partager", + "Share with Groups" : "Partager avec des groupes", + "Share with Users" : "Partager avec des utilisateurs", + "Show Filters" : "Afficher les filtres", + "Show less" : "Afficher moins", + "Show only entries with changes" : "Afficher uniquement les entrées avec modifications", + "Simple" : "Simple", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Requêtes simples : recherches textuelles de base avec un minimum de paramètres (par ex. un seul terme de recherche, sans filtres avancés)", + "Size" : "Taille", + "Slug" : "Slug", + "Soft Delete Mode" : "Mode de suppression réversible", + "Soft Deleted Items" : "Éléments supprimés de façon réversible", + "SolrCloud mode enabled" : "Mode SolrCloud activé", + "Source" : "Source", + "Sources" : "Sources", + "Sources:" : "Sources :", + "Standalone SOLR mode" : "Mode SOLR autonome", + "Start Conversation" : "Démarrer la conversation", + "Start Vectorization" : "Démarrer la vectorisation", + "Start a conversation" : "Démarrer une conversation", + "Starting names cache warmup..." : "Démarrage du préchauffage du cache des noms...", + "Starting..." : "Démarrage...", + "Statistics" : "Statistiques", + "Stats calculation not yet implemented" : "Le calcul des statistiques n'est pas encore implémenté", + "Status" : "Statut", + "Status Code" : "Code de statut", + "String Constraints" : "Contraintes de chaîne", + "Success" : "Succès", + "Success Rate" : "Taux de réussite", + "Success Status" : "Statut de réussite", + "Successful" : "Réussi", + "Successfully joined organisation" : "Organisation rejointe avec succès", + "Sync Table" : "Synchroniser la table", + "Synchronization Settings" : "Réglages de synchronisation", + "System Default" : "Par défaut du système", + "Technical Description" : "Description technique", + "Technical description for developers and administrators" : "Description technique pour les développeurs et les administrateurs", + "Temperature" : "Température", + "Templates" : "Modèles", + "Test" : "Tester", + "Test Chat" : "Tester la discussion", + "Test Connection" : "Tester la connexion", + "Test Embedding" : "Tester le plongement", + "Test endpoint executed successfully" : "Point d'accès de test exécuté avec succès", + "Test webhook delivered successfully" : "Webhook de test livré avec succès", + "Test webhook delivery failed" : "Échec de la livraison du webhook de test", + "Test webhook sent successfully" : "Webhook de test envoyé avec succès", + "Testing..." : "Test en cours...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Les fragments de texte sont générés lors de l'extraction des fichiers et stockés dans la base de données. La vectorisation lit ces fichiers déjà fragmentés et les convertit en plongements.", + "Text extracted successfully" : "Texte extrait avec succès", + "Text extraction disabled" : "Extraction de texte désactivée", + "The URL where webhook events will be sent" : "L'URL où les événements de webhook seront envoyés", + "The requested conversation does not exist" : "La conversation demandée n'existe pas", + "The saved view and all its search configuration" : "La vue enregistrée et toute sa configuration de recherche", + "There are no audit trail entries matching your current filters." : "Aucune entrée de journal d'audit ne correspond à vos filtres actuels.", + "There are no deleted items matching your current filters." : "Aucun élément supprimé ne correspond à vos filtres actuels.", + "There are no search trail entries matching your current filters." : "Aucune entrée de journal de recherche ne correspond à vos filtres actuels.", + "There are no webhook log entries matching your filters." : "Aucune entrée de journal de webhook ne correspond à vos filtres.", + "This action cannot be undone." : "Cette action est irréversible.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Cette action est irréversible. Assurez-vous qu'aucune collection n'utilise ce ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Cet agent n'a aucune vue ni aucun outil configurable, mais vous pouvez tout de même ajuster les réglages RAG ci-dessous.", + "This analysis may take some time" : "Cette analyse peut prendre un certain temps", + "This audit trail entry does not contain any change information." : "Cette entrée de journal d'audit ne contient aucune information de modification.", + "This configuration has no data defined." : "Cette configuration n'a aucune donnée définie.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ce point d'accès est obsolète. Utilisez plutôt les points d'accès par fragments.", + "This entity has no relations to objects or files" : "Cette entité n'a aucune relation avec des objets ou des fichiers", + "This is a test webhook from OpenRegister" : "Ceci est un webhook de test d'OpenRegister", + "This schema must use Magic Table configuration to sync" : "Ce schéma doit utiliser la configuration de table magique pour se synchroniser", + "This source has no associated registers." : "Cette source n'a aucun registre associé.", + "This will copy the _default ConfigSet with the new name" : "Cela copiera le ConfigSet _default avec le nouveau nom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Cela supprimera définitivement TOUS les plongements (vecteurs) de la base de données. Vous devrez revectoriser tous les objets et fichiers. Cette action est irréversible.\\n\\nVoulez-vous vraiment continuer ?", + "This will permanently delete:" : "Cela supprimera définitivement :", + "Timeout (seconds)" : "Délai d'attente (secondes)", + "Timestamp" : "Horodatage", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Astuce : n'activez que les vues qui nécessitent une recherche sémantique pour minimiser les coûts de plongement. Les tables de correspondance simples ont rarement besoin de vectorisation.", + "Title" : "Titre", + "To Date" : "Date de fin", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Pour ajuster la taille des fragments ou la stratégie, allez dans Configuration des fichiers → Limites de traitement.", + "To change the embedding provider or model, go to LLM Configuration." : "Pour changer le fournisseur ou le modèle de plongement, allez dans Configuration LLM.", + "To date" : "Date de fin", + "Toggle search sidebar" : "Basculer la barre latérale de recherche", + "Tools" : "Outils", + "Top Deleters" : "Principaux auteurs de suppressions", + "Total" : "Total", + "Total Audit Trails" : "Total des journaux d'audit", + "Total Chunks Available:" : "Total des fragments disponibles :", + "Total Deleted Items" : "Total des éléments supprimés", + "Total Members" : "Total des membres", + "Total Objects" : "Total des objets", + "Total Objects in Database:" : "Total des objets dans la base de données :", + "Total Organisations" : "Total des organisations", + "Total Results" : "Total des résultats", + "Total Searches" : "Total des recherches", + "Total Size" : "Taille totale", + "Total Storage" : "Stockage total", + "Total:" : "Total :", + "Totals" : "Totaux", + "Type" : "Type", + "Type Issue" : "Problème de type", + "Type Variations:" : "Variations de type :", + "Type must be \"positive\" or \"negative\"" : "Le type doit être « positive » ou « negative »", + "Type to search for groups" : "Saisissez pour rechercher des groupes", + "Type to search for users" : "Saisissez pour rechercher des utilisateurs", + "Type to search groups..." : "Saisissez pour rechercher des groupes...", + "Type to search users..." : "Saisissez pour rechercher des utilisateurs...", + "URL" : "URL", + "URL where Ollama is running" : "URL où Ollama est en cours d'exécution", + "UUID:" : "UUID :", + "Unchanged" : "Inchangé", + "Unique Search Terms" : "Termes de recherche uniques", + "Unique Terms" : "Termes uniques", + "Unknown" : "Inconnu", + "Unknown Register" : "Registre inconnu", + "Unknown Schema" : "Schéma inconnu", + "Untitled View" : "Vue sans titre", + "Update" : "Mettre à jour", + "Update Operations" : "Opérations de mise à jour", + "Update configuration: ..." : "Mettre à jour la configuration : ...", + "Update register OAS: ..." : "Mettre à jour l'OAS du registre : ...", + "Update vectors when object data changes (recommended for accurate search)" : "Mettre à jour les vecteurs lorsque les données de l'objet changent (recommandé pour une recherche précise)", + "Updated" : "Mis à jour", + "Updated At" : "Mis à jour le", + "Updated:" : "Mis à jour :", + "Uptime:" : "Temps de fonctionnement :", + "Usage Count" : "Nombre d'utilisations", + "Use SolrCloud with Zookeeper for distributed search" : "Utiliser SolrCloud avec Zookeeper pour la recherche distribuée", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Utilisez les filtres pour affiner les entrées de journal d'audit par registre, schéma, type d'action, utilisateur, plage de dates ou identifiant d'objet.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Utilisez les filtres pour affiner les éléments supprimés par registre, schéma, date de suppression ou utilisateur qui les a supprimés.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Utilisez les filtres pour affiner les entrées de journal de recherche par registre, schéma, statut de réussite, utilisateur, plage de dates, termes de recherche ou indicateurs de performance.", + "Used By Collections" : "Utilisé par les collections", + "User" : "Utilisateur", + "User Agent Statistics" : "Statistiques de l'agent utilisateur", + "User Description" : "Description de l'utilisateur", + "User rate limits cleared successfully" : "Limites de débit par utilisateur effacées avec succès", + "User removed successfully" : "Utilisateur supprimé avec succès", + "User-friendly description shown in forms and help text" : "Description conviviale affichée dans les formulaires et les textes d'aide", + "Username" : "Nom d'utilisateur", + "Username for SOLR authentication (optional)" : "Nom d'utilisateur pour l'authentification SOLR (facultatif)", + "Users" : "Utilisateurs", + "Using Pre-Generated Chunks" : "Utilisation des fragments pré-générés", + "Validate" : "Valider", + "Validate Objects" : "Valider les objets", + "Validation completed successfully" : "Validation terminée avec succès", + "Validation failed" : "Échec de la validation", + "Value" : "Valeur", + "Vector Dimensions" : "Dimensions du vecteur", + "Vector Search Backend" : "Backend de recherche vectorielle", + "Vector field: _embedding_" : "Champ vectoriel : _embedding_", + "Vectorization Triggers" : "Déclencheurs de vectorisation", + "Vectorize All Files" : "Vectoriser tous les fichiers", + "Vectorize All Objects" : "Vectoriser tous les objets", + "Vectorize all file types" : "Vectoriser tous les types de fichiers", + "Vectorize all views" : "Vectoriser toutes les vues", + "Vectorize on object creation" : "Vectoriser lors de la création de l'objet", + "Vectors will be stored in your existing object and file collections" : "Les vecteurs seront stockés dans vos collections d'objets et de fichiers existantes", + "Version" : "Version", + "Very High" : "Très élevé", + "View API Docs" : "Voir la documentation API", + "View Changes" : "Voir les modifications", + "View Details" : "Voir les détails", + "View Error" : "Voir l'erreur", + "View File" : "Voir le fichier", + "View Full Details" : "Voir tous les détails", + "View Logs" : "Voir les journaux", + "View Name" : "Nom de la vue", + "View Object" : "Voir l'objet", + "View Parameters" : "Voir les paramètres", + "View Selection" : "Sélection de vues", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Consultez et analysez les journaux de recherche avec des capacités de filtrage avancé et d'analyse", + "View and analyze system audit trails with advanced filtering capabilities" : "Consultez et analysez les journaux d'audit système avec des capacités de filtrage avancé", + "View deleted successfully" : "Vue supprimée avec succès", + "View entity details and manage relations" : "Voir les détails de l'entité et gérer les relations", + "View name" : "Nom de la vue", + "View name is required" : "Le nom de la vue est requis", + "View saved successfully!" : "Vue enregistrée avec succès !", + "View search analytics and manage search logs" : "Consultez les analyses de recherche et gérez les journaux de recherche", + "View successfully deleted" : "Vue supprimée avec succès", + "View successfully updated" : "Vue mise à jour avec succès", + "View updated successfully!" : "Vue mise à jour avec succès !", + "View webhook delivery logs and filter by webhook" : "Consultez les journaux de livraison des webhooks et filtrez par webhook", + "Views" : "Vues", + "Visible to users" : "Visible par les utilisateurs", + "Wait for Response" : "Attendre la réponse", + "Wait for webhook response before continuing (required for request/response flows)" : "Attendre la réponse du webhook avant de continuer (requis pour les flux requête/réponse)", + "Warmup Names Cache" : "Préchauffer le cache des noms", + "Warning" : "Avertissement", + "Warning:" : "Avertissement :", + "Webhook" : "Webhook", + "Webhook Log Details" : "Détails du journal de webhook", + "Webhook Logs" : "Journaux de webhook", + "Webhook created successfully" : "Webhook créé avec succès", + "Webhook deleted" : "Webhook supprimé", + "Webhook retry delivered successfully" : "Nouvelle tentative de webhook livrée avec succès", + "Webhook retry delivery failed" : "Échec de la livraison de la nouvelle tentative de webhook", + "Webhook updated" : "Webhook mis à jour", + "Webhook updated successfully" : "Webhook mis à jour avec succès", + "Webhooks" : "Webhooks", + "Weekly" : "Hebdomadaire", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Encapsuler la charge utile du webhook au format CloudEvents pour une meilleure interopérabilité", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Oui", + "You" : "Vous", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Vous pouvez créer de nouveaux ConfigSets à partir du modèle _default, ou téléverser des ConfigSets personnalisés directement sur votre serveur SOLR.", + "You do not have access to this conversation" : "Vous n'avez pas accès à cette conversation", + "You do not have permission to delete this conversation" : "Vous n'avez pas la permission de supprimer cette conversation", + "You do not have permission to modify this conversation" : "Vous n'avez pas la permission de modifier cette conversation", + "You do not have permission to restore this conversation" : "Vous n'avez pas la permission de restaurer cette conversation", + "You must be logged in to favorite views" : "Vous devez être connecté pour mettre des vues en favoris", + "You need an AI agent to start a conversation." : "Vous avez besoin d'un agent IA pour démarrer une conversation.", + "Your Fireworks AI API key. Get one at" : "Votre clé API Fireworks AI. Obtenez-en une sur", + "Your OpenAI API key. Get one at" : "Votre clé API OpenAI. Obtenez-en une sur", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Votre retour a été enregistré. Vous pouvez éventuellement fournir des détails supplémentaires ici...", + "Zookeeper Hosts" : "Hôtes Zookeeper", + "Zookeeper Port" : "Port Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Réglages Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Chaîne de connexion Zookeeper pour SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Numéro de port Zookeeper (facultatif, 2181 par défaut)", + "chunk" : "fragment", + "chunks" : "fragments", + "chunks processed" : "fragments traités", + "configuration(s)" : "configuration(s)", + "e.g., lib/Settings/config.json" : "par ex. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "par ex. lib/Settings/register.json", + "failed" : "échoué", + "file" : "fichier", + "files" : "fichiers", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "membres", + "menu or contact someone with permission to create agents." : "menu ou contactez une personne autorisée à créer des agents.", + "messages" : "messages", + "n8n URL and API key are required" : "L'URL et la clé API n8n sont requises", + "n8n configuration saved successfully" : "Configuration n8n enregistrée avec succès", + "n8n connection not configured" : "Connexion n8n non configurée", + "n8n connection successful" : "Connexion n8n réussie", + "n8n connection test successful" : "Test de connexion n8n réussi", + "n8n integration disabled" : "Intégration n8n désactivée", + "n8n integration enabled" : "Intégration n8n activée", + "n8n project initialized successfully" : "Projet n8n initialisé avec succès", + "n8n settings saved successfully" : "Réglages n8n enregistrés avec succès", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objets", + "objects processed" : "objets traités", + "or pick a range" : "ou choisir une plage", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "offre un bon équilibre entre vitesse et précision.", + "register(s)" : "registre(s)", + "register(s) selected" : "registre(s) sélectionné(s)", + "required" : "obligatoire", + "results" : "résultats", + "schema(s)" : "schéma(s)", + "schema(s) selected" : "schéma(s) sélectionné(s)", + "sk-..." : "sk-...", + "this application" : "cette application", + "to" : "à", + "vectorized" : "vectorisé", + "views selected" : "vues sélectionnées", + "ℹ️ Current Configuration" : "ℹ️ Configuration actuelle", + "⚡ Batch Processing" : "⚡ Traitement par lot", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Contexte complet ✓ Moins susceptible de manquer des détails ✗ Réponses plus lentes ✗ Peut inclure des informations moins pertinentes", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Réponses plus rapides ✓ Réponses plus ciblées ✗ Peut manquer des informations pertinentes", + "✨ AI Features" : "✨ Fonctionnalités IA", + "All search trails cleared successfully" : "Tous les journaux de recherche ont été effacés avec succès", + "Multiple search trail deletion not implemented yet" : "La suppression multiple de journaux de recherche n'est pas encore implémentée", + "No expired search trails found to clear" : "Aucun journal de recherche expiré à effacer", + "Missing message" : "Message manquant", + "message content is required" : "le contenu du message est requis", + "Missing conversation or agentUuid" : "Conversation ou agentUuid manquant", + "Access denied" : "Accès refusé", + "Conversation not found" : "Conversation introuvable", + "AI service not configured" : "Service IA non configuré", + "Failed to process message" : "Échec du traitement du message", + "Missing conversationId" : "conversationId manquant", + "conversationId is required" : "conversationId est requis", + "Failed to fetch conversation history" : "Échec de la récupération de l'historique de la conversation", + "Failed to clear conversation" : "Échec de l'effacement de la conversation", + "Invalid feedback type" : "Type de retour invalide", + "type must be \"positive\" or \"negative\"" : "le type doit être « positive » ou « negative »", + "Message not found" : "Message introuvable", + "Failed to save feedback" : "Échec de l'enregistrement du retour", + "Failed to get chat statistics" : "Échec de la récupération des statistiques de discussion", + "Not Found" : "Introuvable", + "Engine not found" : "Moteur introuvable", + "Consumer not found" : "Consommateur introuvable", + "Backend parameter is required" : "Le paramètre backend est requis", + "Failed to get database information: %s" : "Échec de la récupération des informations de la base de données : %s", + "SOLR setup error: %s" : "Erreur de configuration SOLR : %s", + "Reindex failed: %s" : "Échec de la réindexation : %s", + "Anonymized" : "Anonymisé", + "Entities detected" : "Entités détectées", + "Extract Now" : "Extraire maintenant", + "Extracted at" : "Extrait le", + "Extraction" : "Extraction", + "Failed to load extraction data" : "Échec du chargement des données d'extraction", + "Failed to load register data" : "Échec du chargement des données du registre", + "Failed to retrieve extraction status." : "Échec de la récupération du statut de l'extraction.", + "Failed to retrieve objects for file." : "Échec de la récupération des objets du fichier.", + "No extraction data available for this file" : "Aucune donnée d'extraction disponible pour ce fichier", + "No register objects reference this file" : "Aucun objet de registre ne référence ce fichier", + "Not extracted" : "Non extrait", + "Register Objects" : "Objets du registre", + "Risk level" : "Niveau de risque", + "Text chunks" : "Fragments de texte", + "Unknown error" : "Erreur inconnue", + "Very high" : "Très élevé", + "{title} in {register} / {schema}" : "{title} dans {register} / {schema}", + "Push Notifications" : "Notifications push", + "Real-time push notification status via notify_push" : "Statut des notifications push en temps réel via notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push en temps réel non disponible — l'application notify_push n'est pas installée", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installez l'application notify_push depuis l'App Store Nextcloud pour activer les mises à jour en temps réel.", + "Open Nextcloud App Store" : "Ouvrir l'App Store Nextcloud", + "notify_push is installed but not yet active" : "notify_push est installé mais pas encore actif", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push est installé mais OpenRegister n'a pas encore confirmé un push réussi. Déclenchez un enregistrement d'objet pour l'activer, ou vérifiez votre configuration notify_push.", + "notify_push configuration guide" : "guide de configuration notify_push", + "Realtime push active" : "Push en temps réel actif", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Les notifications push en temps réel sont actives. Les clients connectés reçoivent des mises à jour instantanées lorsque des objets sont créés, mis à jour ou supprimés.", + "Bookmarks" : "Bookmarks", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n > 1);" +); diff --git a/l10n/fr.json b/l10n/fr.json new file mode 100644 index 0000000000..3e044d720b --- /dev/null +++ b/l10n/fr.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(no subject)": "(sans objet)", + "(no title)": "(sans titre)", + "(unnamed)": "(sans nom)", + "(untitled card)": "(carte sans titre)", + "Activities": "Activités", + "Add Endpoint": "Ajouter un point de terminaison", + "Add Source": "Ajouter une source", + "Add card": "Ajouter une carte", + "Add contact": "Ajouter un contact", + "All clear": "Tout est en ordre", + "Articles": "Articles", + "Code": "Code", + "Compliance": "Conformité", + "Contacts": "Contacts", + "Create Agent": "Créer un agent", + "Create Application": "Créer une application", + "Create event": "Créer un événement", + "Erasure complete": "Effacement terminé", + "Erasure preview": "Aperçu de l'effacement", + "Expand details": "Développer les détails", + "Save changes": "Enregistrer les modifications", + "📄 Object Serialization": "📄 Sérialisation des objets", + "🔢 Vectorization Settings": "🔢 Paramètres de vectorisation", + "💰 View Selection (Cost Optimization)": "💰 Sélection des vues (optimisation des coûts)", + "AI Assistant": "Assistant IA", + "AI Chat": "Chat IA", + "API Key": "Clé API", + "API tokens saved successfully": "Jetons API enregistrés avec succès", + "About ConfigSets": "À propos des ConfigSets", + "Action": "Action", + "Action Distribution": "Répartition des actions", + "Action:": "Action :", + "Actions": "Actions", + "Active": "Actif", + "Active Collections": "Collections actives", + "Active Filters": "Filtres actifs", + "Active Organisations": "Organisations actives", + "Active filters:": "Filtres actifs :", + "Active organisation set successfully": "Organisation active définie avec succès", + "Activity Period": "Période d'activité", + "Add Groups": "Ajouter des groupes", + "Add Schema": "Ajouter un schéma", + "Add schema titles, descriptions, and register information to provide richer context for search": "Ajoutez des titres de schéma, des descriptions et des informations de registre pour fournir un contexte plus riche à la recherche", + "Add to favorites": "Ajouter aux favoris", + "Added": "Ajouté", + "Added to favorites": "Ajouté aux favoris", + "Additional Information": "Informations supplémentaires", + "Additional feedback saved. Thank you!": "Commentaire supplémentaire enregistré. Merci !", + "Admin": "Administrateur", + "Advanced": "Avancé", + "Advanced Filters": "Filtres avancés", + "Advanced Options": "Options avancées", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Les filtres avancés ne sont pas disponibles avec la source base de données. Basculez vers Auto ou l'index SOLR pour les options de filtrage.", + "After": "Après", + "Agent deleted successfully": "Agent supprimé avec succès", + "Agents": "Agents", + "All": "Tous", + "All Categories": "Toutes les catégories", + "All Collections": "Toutes les collections", + "All Confidence Levels": "Tous les niveaux de confiance", + "All ConfigSets": "Tous les ConfigSets", + "All Files": "Tous les fichiers", + "All Levels": "Tous les niveaux", + "All Types": "Tous les types", + "All Webhooks": "Tous les webhooks", + "All actions": "Toutes les actions", + "All audit trails cleared successfully": "Toutes les pistes d'audit ont été effacées avec succès", + "All registers": "Tous les registres", + "All schemas": "Tous les schémas", + "All searches": "Toutes les recherches", + "All users": "Tous les utilisateurs", + "An error occurred while clearing audit trails": "Une erreur s'est produite lors de l'effacement des pistes d'audit", + "An error occurred while deleting the audit trail": "Une erreur s'est produite lors de la suppression de la piste d'audit", + "An error occurred while deleting the view": "Une erreur s'est produite lors de la suppression de la vue", + "An error occurred while permanently deleting the objects": "Une erreur s'est produite lors de la suppression définitive des objets", + "Analysis completed:": "Analyse terminée :", + "Analysis steps:": "Étapes de l'analyse :", + "Analytics": "Analytique", + "Analyze Objects": "Analyser les objets", + "Analyze Properties": "Analyser les propriétés", + "Analyze existing properties for improvement opportunities": "Analyser les propriétés existantes pour identifier les possibilités d'amélioration", + "Analyzing...": "Analyse en cours...", + "Any favorites and sharing settings for this view": "Tous les favoris et paramètres de partage de cette vue", + "Any user": "N'importe quel utilisateur", + "App store cache invalidated successfully": "Cache de l'App Store invalidé avec succès", + "Application deleted successfully": "Application supprimée avec succès", + "Applications": "Applications", + "Apply Changes": "Appliquer les modifications", + "Archive": "Archiver", + "Archive conversation": "Archiver la conversation", + "Archived conversations are hidden from your active list": "Les conversations archivées sont masquées de votre liste active", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Voulez-vous vraiment nettoyer les anciennes pistes de recherche ? Cela supprimera les entrées de plus de 30 jours.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Voulez-vous vraiment supprimer les pistes d'audit sélectionnées ? Cette action est irréversible.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Voulez-vous vraiment supprimer les pistes de recherche sélectionnées ? Cette action est irréversible.", + "Are you sure you want to delete this ConfigSet?": "Voulez-vous vraiment supprimer ce ConfigSet ?", + "Are you sure you want to permanently delete": "Voulez-vous vraiment supprimer définitivement", + "Ask a question...": "Posez une question...", + "Ask questions about your data using natural language": "Posez des questions sur vos données en langage naturel", + "Attempt": "Tentative", + "Audit Trail Changes": "Modifications de la piste d'audit", + "Audit Trail Details": "Détails de la piste d'audit", + "Audit Trail Management": "Gestion des pistes d'audit", + "Audit Trail Statistics": "Statistiques des pistes d'audit", + "Audit Trails": "Pistes d'audit", + "Audit trail data copied to clipboard": "Données de la piste d'audit copiées dans le presse-papiers", + "Audit trail deleted successfully": "Piste d'audit supprimée avec succès", + "Audit trail successfully deleted": "Piste d'audit supprimée avec succès", + "Authentication": "Authentification", + "Auto-commit disabled": "Validation automatique désactivée", + "Auto-commit enabled": "Validation automatique activée", + "Auto-create Default Organisation": "Créer automatiquement l'organisation par défaut", + "Auto-retry failed vectorizations": "Réessayer automatiquement les vectorisations échouées", + "Automatically commit changes to SOLR index": "Valider automatiquement les modifications dans l'index SOLR", + "Automatically create a default organisation if none exists when the app is initialized": "Créer automatiquement une organisation par défaut s'il n'en existe aucune lors de l'initialisation de l'application", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Générer automatiquement des plongements vectoriels à partir des fragments de texte lors du téléversement et du traitement des fichiers", + "Automatically generate vector embeddings when objects are created or updated": "Générer automatiquement des plongements vectoriels lors de la création ou de la mise à jour des objets", + "Automatically retry failed vectorization attempts (max 3 retries)": "Réessayer automatiquement les tentatives de vectorisation échouées (max. 3 tentatives)", + "Available Filters": "Filtres disponibles", + "Available Workflows": "Workflows disponibles", + "Avg Execution Time": "Temps d'exécution moyen", + "Avg Members/Org": "Membres moyens/org.", + "Avg Object Views/Session": "Vues d'objets moyennes/session", + "Avg Response Time": "Temps de réponse moyen", + "Avg Results/Search": "Résultats moyens/recherche", + "Avg Searches/Session": "Recherches moyennes/session", + "Back": "Retour", + "Back to Entities": "Retour aux entités", + "Back to Registers": "Retour aux registres", + "Back to Webhooks": "Retour aux webhooks", + "Back to applications": "Retour aux applications", + "Back to entities": "Retour aux entités", + "Backend updated successfully. Please reload the application.": "Backend mis à jour avec succès. Veuillez recharger l'application.", + "Base URL (Optional)": "URL de base (facultatif)", + "Basic Information": "Informations de base", + "Batch Size": "Taille du lot", + "Batch extraction completed": "Extraction par lot terminée", + "Before": "Avant", + "Before object vectorization can work:": "Pour que la vectorisation des objets fonctionne :", + "Behavior Issue": "Problème de comportement", + "Blob storage has been retired. All objects now use magic tables.": "Le stockage blob a été retiré. Tous les objets utilisent désormais les tables magiques.", + "Branch": "Branche", + "Bulk delete operation completed successfully": "Opération de suppression en masse terminée avec succès", + "Bulk save operation completed successfully": "Opération d'enregistrement en masse terminée avec succès", + "Business Data": "Données métier", + "Cache cleared successfully": "Cache vidé avec succès", + "Calculate Sizes": "Calculer les tailles", + "Cancel": "Annuler", + "Cannot delete: objects are still attached": "Suppression impossible : des objets sont encore attachés", + "Category": "Catégorie", + "Change Type": "Type de modification", + "Changes": "Modifications", + "Changes copied to clipboard": "Modifications copiées dans le presse-papiers", + "Chat Model": "Modèle de chat", + "Chat Provider": "Fournisseur de chat", + "Chat Provider (RAG)": "Fournisseur de chat (RAG)", + "Chat Settings": "Paramètres du chat", + "Chat provider connection successful!": "Connexion au fournisseur de chat réussie !", + "Chat settings": "Paramètres du chat", + "Choose a register": "Choisir un registre", + "Choose a schema": "Choisir un schéma", + "Choose how vector similarity calculations are performed for semantic search": "Choisissez comment les calculs de similarité vectorielle sont effectués pour la recherche sémantique", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Choisissez les types de fichiers à inclure dans le processus de vectorisation. Seuls les fichiers dont le texte et les fragments ont été extraits seront traités.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Choisissez les vues à inclure dans le processus de vectorisation. Laissez vide pour traiter toutes les vues selon votre configuration.", + "Chunk": "Fragment", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "La suppression de fragments n'est pas encore implémentée. Utilisez les points de terminaison basés sur les fragments.", + "Chunks": "Fragments", + "Chunks to Vectorize:": "Fragments à vectoriser :", + "Cleanup Old Trails": "Nettoyer les anciennes pistes", + "Cleanup completed": "Nettoyage terminé", + "Clear All Embeddings": "Effacer tous les plongements", + "Clear Entries": "Effacer les entrées", + "Clear Filtered Audit Trails": "Effacer les pistes d'audit filtrées", + "Clear Filters": "Effacer les filtres", + "Clear Index": "Effacer l'index", + "Clear Selection": "Effacer la sélection", + "Clear all": "Tout effacer", + "Clear all filters": "Effacer tous les filtres", + "Clear date range": "Effacer la plage de dates", + "Clearing...": "Effacement en cours...", + "Close": "Fermer", + "Collection Management": "Gestion des collections", + "Collection Name": "Nom de la collection", + "Collection assignments updated successfully": "Affectations de collection mises à jour avec succès", + "Collection cleared successfully": "Collection effacée avec succès", + "Collection copied successfully": "Collection copiée avec succès", + "Collection created successfully": "Collection créée avec succès", + "Collection deleted successfully": "Collection supprimée avec succès", + "Collection used to store and index file metadata and content": "Collection utilisée pour stocker et indexer les métadonnées et le contenu des fichiers", + "Collection used to store and index object data": "Collection utilisée pour stocker et indexer les données d'objets", + "Collections:": "Collections :", + "Commit Message": "Message de validation", + "Commit Within (ms)": "Valider dans (ms)", + "Commit message": "Message de validation", + "Compare current schema with real object data": "Comparer le schéma actuel avec les données d'objets réelles", + "Completed": "Terminé", + "Complex": "Complexe", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Requêtes complexes : recherches avancées avec plusieurs filtres, opérateurs et combinaisons de paramètres complexes", + "Confidence Level": "Niveau de confiance", + "Confidence Score": "Score de confiance", + "Config must be provided as an object": "La configuration doit être fournie sous forme d'objet", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Gestion des ConfigSets", + "ConfigSet Name": "Nom du ConfigSet", + "ConfigSet created successfully": "ConfigSet créé avec succès", + "ConfigSet deleted successfully": "ConfigSet supprimé avec succès", + "ConfigSet:": "ConfigSet :", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Les ConfigSets définissent le schéma et la configuration de vos collections SOLR. Ils contiennent les définitions de champs, les analyseurs et d'autres paramètres de recherche.", + "Configuration": "Configuration", + "Configuration Data": "Données de configuration", + "Configuration saved": "Configuration enregistrée", + "Configurations": "Configurations", + "Configure Facets": "Configurer les facettes", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Configurez les fournisseurs de grands modèles de langage (LLM) pour les fonctionnalités basées sur l'IA, notamment la recherche sémantique, les plongements et le chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Configurez les paramètres de connexion de base de votre serveur SOLR, y compris l'authentification et les options réseau. Utilisez les boîtes de dialogue distinctes de gestion des ConfigSets et des collections pour gérer les cœurs et les collections.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Configurez comment les objets de la base de données sont convertis en plongements vectoriels pour la recherche sémantique. Les objets sont vectorisés directement sans nécessiter d'extraction de texte.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Configurez comment les objets sont convertis en texte avant la vectorisation. Ces paramètres affectent la qualité et le contexte de la recherche.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Configurez les paramètres de la vectorisation des fichiers. Ce processus générera des plongements vectoriels pour tous les fragments de fichiers extraits.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Configurez les paramètres de la vectorisation des objets. Ce processus générera des plongements vectoriels pour tous les objets correspondant à vos filtres de vue.", + "Configure which types of data to search and how many sources to retrieve": "Configurez les types de données à rechercher et le nombre de sources à récupérer", + "Connection Failed": "Échec de la connexion", + "Connection Settings": "Paramètres de connexion", + "Connection Successful!": "Connexion réussie !", + "Connection failed": "Échec de la connexion", + "Connection protocol": "Protocole de connexion", + "Connection successful": "Connexion réussie", + "Connection timeout in seconds": "Délai d'expiration de la connexion en secondes", + "Constant delay between retries (5 minutes)": "Délai constant entre les tentatives (5 minutes)", + "Constraint Issue": "Problème de contrainte", + "Content Filters": "Filtres de contenu", + "Continue to Properties": "Continuer vers les propriétés", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Contrôlez quelles vues d'objets doivent être vectorisées pour réduire les coûts API. Ne vectorisez que les vues qui bénéficient de la recherche sémantique.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Contrôlez quelles vues et quels outils l'IA peut utiliser dans cette conversation. Par défaut, toutes les capacités de l'agent sont activées.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Contrôle l'aléatoire (0 = déterministe, 2 = très créatif)", + "Conversation ID is required": "L'identifiant de la conversation est requis", + "Conversation archived": "Conversation archivée", + "Conversation archived successfully": "Conversation archivée avec succès", + "Conversation cleared successfully": "Conversation effacée avec succès", + "Conversation deleted": "Conversation supprimée", + "Conversation permanently deleted": "Conversation supprimée définitivement", + "Conversation renamed": "Conversation renommée", + "Conversation restored": "Conversation restaurée", + "Conversation title": "Titre de la conversation", + "Copied!": "Copié !", + "Copy": "Copier", + "Copy Changes": "Copier les modifications", + "Copy Collection": "Copier la collection", + "Copy Data": "Copier les données", + "Copy Full Data": "Copier toutes les données", + "Copying...": "Copie en cours...", + "Count": "Nombre", + "Counting objects...": "Comptage des objets...", + "Costs": "Coûts", + "Create": "Créer", + "Create Collection": "Créer une collection", + "Create ConfigSet": "Créer un ConfigSet", + "Create New Collection": "Créer une nouvelle collection", + "Create New ConfigSet": "Créer un nouveau ConfigSet", + "Create Operations": "Opérations de création", + "Create Webhook": "Créer un webhook", + "Create a copy of collection:": "Créer une copie de la collection :", + "Create a new ConfigSet based on the _default template": "Créer un nouveau ConfigSet basé sur le modèle _default", + "Create a new SOLR collection from an existing ConfigSet": "Créer une nouvelle collection SOLR à partir d'un ConfigSet existant", + "Create your first AI agent to get started.": "Créez votre premier agent IA pour commencer.", + "Created": "Créé", + "Created:": "Créé :", + "Creating...": "Création en cours...", + "Current Filters": "Filtres actuels", + "Current Type:": "Type actuel :", + "Custom API endpoint if using a different region": "Point de terminaison API personnalisé si vous utilisez une autre région", + "Custom HTTP headers (one per line, format: Header-Name: value)": "En-têtes HTTP personnalisés (un par ligne, format : Nom-En-tête : valeur)", + "Daily": "Quotidien", + "Dashboard": "Tableau de bord", + "Data Source": "Source de données", + "Data sources": "Sources de données", + "Data type does not match observed values": "Le type de données ne correspond pas aux valeurs observées", + "Database URL": "URL de la base de données", + "Database information refreshed": "Informations de la base de données actualisées", + "Date Range": "Plage de dates", + "Default": "Par défaut", + "Default Organisation": "Organisation par défaut", + "Default: 5": "Par défaut : 5", + "Delays double with each attempt (2, 4, 8 minutes...)": "Les délais doublent à chaque tentative (2, 4, 8 minutes...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Les délais augmentent linéairement (5, 10, 15 minutes...)", + "Delete": "Supprimer", + "Delete Application": "Supprimer l'application", + "Delete Audit Trail": "Supprimer la piste d'audit", + "Delete Collection": "Supprimer la collection", + "Delete ConfigSet": "Supprimer le ConfigSet", + "Delete Objects": "Supprimer les objets", + "Delete Operations": "Opérations de suppression", + "Delete View": "Supprimer la vue", + "Delete all objects in this schema": "Supprimer tous les objets de ce schéma", + "Delete permanently": "Supprimer définitivement", + "Delete this conversation?": "Supprimer cette conversation ?", + "Delete view": "Supprimer la vue", + "Deleted": "Supprimé", + "Deleted By": "Supprimé par", + "Deleted Date": "Date de suppression", + "Deleted Items Management": "Gestion des éléments supprimés", + "Deleted This Week": "Supprimés cette semaine", + "Deleted Today": "Supprimés aujourd'hui", + "Deleted:": "Supprimé :", + "Deleting...": "Suppression en cours...", + "Deletion Date Range": "Plage de dates de suppression", + "Deletion Statistics": "Statistiques de suppression", + "Deprecated": "Obsolète", + "Description": "Description", + "Description:": "Description :", + "Detect data types and patterns": "Détecter les types de données et les motifs", + "Detected At": "Détecté le", + "Detected Format:": "Format détecté :", + "Detected Issues:": "Problèmes détectés :", + "Disable": "Désactiver", + "Disabled": "Désactivé", + "Discover Files": "Découvrir les fichiers", + "Discovered Properties": "Propriétés découvertes", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Voulez-vous supprimer définitivement toutes les entrées de piste d'audit filtrées ? Cette action est irréversible.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Voulez-vous supprimer définitivement cette entrée de piste d'audit ? Cette action est irréversible.", + "Documents": "Documents", + "Download API Spec": "Télécharger la spécification API", + "Edit": "Modifier", + "Edit Register": "Modifier le registre", + "Edit Schema": "Modifier le schéma", + "Edit View": "Modifier la vue", + "Edit Webhook": "Modifier le webhook", + "Edit view": "Modifier la vue", + "Edit view details": "Modifier les détails de la vue", + "Effectiveness": "Efficacité", + "Email": "E-mail", + "Emails": "E-mails", + "Embedding Model": "Modèle de plongement", + "Embedding Provider": "Fournisseur de plongement", + "Embedding generated successfully": "Plongement généré avec succès", + "Embedding provider connection successful!": "Connexion au fournisseur de plongement réussie !", + "Enable": "Activer", + "Enable auto-creation": "Activer la création automatique", + "Enable automatic file vectorization": "Activer la vectorisation automatique des fichiers", + "Enable automatic object vectorization": "Activer la vectorisation automatique des objets", + "Enable automatic synchronization": "Activer la synchronisation automatique", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Activer la journalisation détaillée des opérations SOLR (recommandé pour le débogage)", + "Enable faceting": "Activer les facettes", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Activez ou désactivez les fonctionnalités LLM. Configurez les fournisseurs et les modèles à l'aide du bouton Configuration LLM ci-dessus.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Activez ou désactivez l'intégration de la recherche SOLR. Configurez les paramètres de connexion à l'aide du bouton Paramètres de connexion ci-dessus.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Activez ou désactivez l'intégration des workflows n8n. Configurez les paramètres de connexion ci-dessous.", + "Enable or disable this webhook": "Activer ou désactiver ce webhook", + "Enable vectorization for all existing and future views (may increase costs)": "Activer la vectorisation pour toutes les vues existantes et futures (peut augmenter les coûts)", + "Enabled": "Activé", + "Endpoints": "Points de terminaison", + "Enter ConfigSet name": "Saisissez le nom du ConfigSet", + "Enter collection name": "Saisissez le nom de la collection", + "Enter description (optional)...": "Saisissez une description (facultatif)...", + "Enter new collection name": "Saisissez le nouveau nom de la collection", + "Enter object ID": "Saisissez l'identifiant de l'objet", + "Enter search term": "Saisissez le terme de recherche", + "Enter view name...": "Saisissez le nom de la vue...", + "Enter webhook name": "Saisissez le nom du webhook", + "Entities": "Entités", + "Entity Information": "Informations sur l'entité", + "Entity deleted successfully": "Entité supprimée avec succès", + "Entity not found": "Entité introuvable", + "Entries to be deleted:": "Entrées à supprimer :", + "Enum Issue": "Problème d'énumération", + "Enum constraint is missing": "La contrainte d'énumération est manquante", + "Error": "Erreur", + "Error Details": "Détails de l'erreur", + "Error Information": "Informations sur l'erreur", + "Error Message": "Message d'erreur", + "Error loading application": "Erreur lors du chargement de l'application", + "Error loading audit trails": "Erreur lors du chargement des pistes d'audit", + "Error loading entity": "Erreur lors du chargement de l'entité", + "Error loading search trails": "Erreur lors du chargement des pistes de recherche", + "Estimated Batches:": "Lots estimés :", + "Estimated Cost:": "Coût estimé :", + "Estimated Duration:": "Durée estimée :", + "Event": "Événement", + "Event Property for Payload": "Propriété d'événement pour la charge utile", + "Events": "Événements", + "Example Value": "Valeur d'exemple", + "Exclusive Maximum": "Maximum exclusif", + "Exclusive Minimum": "Minimum exclusif", + "Execution Mode": "Mode d'exécution", + "Execution Time": "Temps d'exécution", + "Execution Time Range": "Plage de temps d'exécution", + "Existing Improvements": "Améliorations existantes", + "Exponential": "Exponentiel", + "Export": "Exporter", + "Export completed successfully": "Exportation terminée avec succès", + "Export, view, or delete audit trails": "Exporter, consulter ou supprimer les pistes d'audit", + "Extend Schema": "Étendre le schéma", + "Extended": "Étendu", + "Extensions": "Extensions", + "Extract Pending Files": "Extraire les fichiers en attente", + "Extract properties from each object": "Extraire les propriétés de chaque objet", + "Extracted At": "Extrait le", + "Extraction Status": "Statut de l'extraction", + "Facet configuration updated successfully": "Configuration des facettes mise à jour avec succès", + "Facets discovered and configured successfully": "Facettes découvertes et configurées avec succès", + "Facets discovered successfully": "Facettes découvertes avec succès", + "Failed": "Échec", + "Failed to analyze schema properties": "Échec de l'analyse des propriétés du schéma", + "Failed to archive conversation": "Échec de l'archivage de la conversation", + "Failed to calculate sizes": "Échec du calcul des tailles", + "Failed to clear collection": "Échec de l'effacement de la collection", + "Failed to copy changes": "Échec de la copie des modifications", + "Failed to copy data": "Échec de la copie des données", + "Failed to copy data to clipboard": "Échec de la copie des données dans le presse-papiers", + "Failed to create conversation": "Échec de la création de la conversation", + "Failed to create or find project": "Échec de la création ou de la recherche du projet", + "Failed to delete collection": "Échec de la suppression de la collection", + "Failed to delete conversation": "Échec de la suppression de la conversation", + "Failed to delete webhook": "Échec de la suppression du webhook", + "Failed to download API specification": "Échec du téléchargement de la spécification API", + "Failed to get SOLR field configuration": "Échec de l'obtention de la configuration des champs SOLR", + "Failed to get entity categories": "Échec de l'obtention des catégories d'entités", + "Failed to get entity statistics": "Échec de l'obtention des statistiques d'entités", + "Failed to get entity types": "Échec de l'obtention des types d'entités", + "Failed to load LLM configuration": "Échec du chargement de la configuration LLM", + "Failed to load Nextcloud groups": "Échec du chargement des groupes Nextcloud", + "Failed to load conversation": "Échec du chargement de la conversation", + "Failed to load entities": "Échec du chargement des entités", + "Failed to load entity": "Échec du chargement de l'entité", + "Failed to load files": "Échec du chargement des fichiers", + "Failed to load organisations": "Échec du chargement des organisations", + "Failed to load templates": "Échec du chargement des modèles", + "Failed to load webhooks": "Échec du chargement des webhooks", + "Failed to load workflows": "Échec du chargement des workflows", + "Failed to refresh database information": "Échec de l'actualisation des informations de la base de données", + "Failed to reindex collection": "Échec de la réindexation de la collection", + "Failed to rename conversation": "Échec du renommage de la conversation", + "Failed to restore conversation": "Échec de la restauration de la conversation", + "Failed to retry extraction": "Échec de la nouvelle tentative d'extraction", + "Failed to retry webhook": "Échec de la nouvelle tentative du webhook", + "Failed to save GitHub token": "Échec de l'enregistrement du jeton GitHub", + "Failed to save GitLab URL": "Échec de l'enregistrement de l'URL GitLab", + "Failed to save GitLab token": "Échec de l'enregistrement du jeton GitLab", + "Failed to save additional feedback": "Échec de l'enregistrement du commentaire supplémentaire", + "Failed to save n8n configuration": "Échec de l'enregistrement de la configuration n8n", + "Failed to save roles": "Échec de l'enregistrement des rôles", + "Failed to save settings": "Échec de l'enregistrement des paramètres", + "Failed to save webhook": "Échec de l'enregistrement du webhook", + "Failed to send feedback": "Échec de l'envoi du commentaire", + "Failed to test webhook": "Échec du test du webhook", + "Failed to update favorite status": "Échec de la mise à jour du statut de favori", + "Failed to update schema properties": "Échec de la mise à jour des propriétés du schéma", + "Failed to update view": "Échec de la mise à jour de la vue", + "Failed to update webhook": "Échec de la mise à jour du webhook", + "Feedback recorded": "Commentaire enregistré", + "Fewer sources (1-3):": "Moins de sources (1-3) :", + "Field": "Champ", + "File": "Fichier", + "File Chunk Prediction": "Prédiction des fragments de fichiers", + "File Collection": "Collection de fichiers", + "File Management": "Gestion des fichiers", + "File Name": "Nom du fichier", + "File Path": "Chemin du fichier", + "File Type Selection": "Sélection des types de fichiers", + "File Vectorization": "Vectorisation des fichiers", + "File Warmup": "Préchauffage des fichiers", + "File actions menu": "Menu des actions de fichier", + "File anonymized successfully": "Fichier anonymisé avec succès", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "La vectorisation des fragments de fichiers n'est pas encore implémentée. Les fragments sont prêts et stockés, mais le service de vectorisation est en cours de développement.", + "File collection not configured": "Collection de fichiers non configurée", + "File discovery completed": "Découverte des fichiers terminée", + "File extraction completed": "Extraction des fichiers terminée", + "File extraction queued": "Extraction des fichiers mise en file d'attente", + "File indexed successfully": "Fichier indexé avec succès", + "File is already anonymized": "Le fichier est déjà anonymisé", + "File not found": "Fichier introuvable", + "File settings updated successfully": "Paramètres du fichier mis à jour avec succès", + "File sources": "Sources de fichiers", + "File vectorization configuration saved successfully": "Configuration de la vectorisation des fichiers enregistrée avec succès", + "File vectorization started. Check the statistics section for progress.": "Vectorisation des fichiers démarrée. Consultez la section des statistiques pour suivre la progression.", + "File warmup completed": "Préchauffage des fichiers terminé", + "Files": "Fichiers", + "Files with Completed Extraction:": "Fichiers avec extraction terminée :", + "Files → fileCollection, Objects → objectCollection": "Fichiers → fileCollection, Objets → objectCollection", + "Filter Audit Trails": "Filtrer les pistes d'audit", + "Filter Deleted Items": "Filtrer les éléments supprimés", + "Filter Objects": "Filtrer les objets", + "Filter Properties": "Filtrer les propriétés", + "Filter Search Trails": "Filtrer les pistes de recherche", + "Filter Statistics": "Filtrer les statistiques", + "Filter and analyze search trail entries": "Filtrer et analyser les entrées de piste de recherche", + "Filter and manage audit trail entries": "Filtrer et gérer les entrées de piste d'audit", + "Filter and manage soft deleted items": "Filtrer et gérer les éléments supprimés de façon réversible", + "Filter by object ID": "Filtrer par identifiant d'objet", + "Filter by search term": "Filtrer par terme de recherche", + "Filter by webhook": "Filtrer par webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Données de filtre chargées automatiquement. Utilisez les filtres ci-dessous pour affiner votre recherche.", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrer les déclencheurs de webhook par propriétés de charge utile (un par ligne, format : clé : valeur)", + "Filtered": "Filtré", + "Filters": "Filtres", + "Fireworks AI Chat Settings": "Paramètres de chat Fireworks AI", + "Fireworks AI Embedding Configuration": "Configuration de plongement Fireworks AI", + "First": "Premier", + "Fixed": "Corrigé", + "For chat and retrieval-augmented generation": "Pour le chat et la génération augmentée par récupération", + "For vector embeddings and semantic search": "Pour les plongements vectoriels et la recherche sémantique", + "Format": "Format", + "Format Issue": "Problème de format", + "Format constraint is missing": "La contrainte de format est manquante", + "From Date": "Date de début", + "From date": "Date de début", + "Full data copied to clipboard": "Toutes les données copiées dans le presse-papiers", + "General Issue": "Problème général", + "Generate recommendations and confidence scores": "Générer des recommandations et des scores de confiance", + "Generate vectors immediately when new objects are created": "Générer les vecteurs immédiatement lors de la création de nouveaux objets", + "GitHub token is valid": "Le jeton GitHub est valide", + "GitHub token removed successfully": "Jeton GitHub supprimé avec succès", + "GitHub token saved successfully": "Jeton GitHub enregistré avec succès", + "GitLab URL saved successfully": "URL GitLab enregistrée avec succès", + "GitLab token is valid": "Le jeton GitLab est valide", + "GitLab token saved successfully": "Jeton GitLab enregistré avec succès", + "HTTP Method": "Méthode HTTP", + "HTTP method used to send webhook requests": "Méthode HTTP utilisée pour envoyer les requêtes de webhook", + "Headers": "En-têtes", + "Health": "Santé", + "Heartbeat successful - connection kept alive": "Battement de cœur réussi - connexion maintenue active", + "Helpful": "Utile", + "Hide Filters": "Masquer les filtres", + "Hide in forms": "Masquer dans les formulaires", + "Hide in list view": "Masquer dans la vue en liste", + "High": "Élevé", + "High Confidence": "Confiance élevée", + "Host": "Hôte", + "Hourly": "Toutes les heures", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Profondeur de parcours des propriétés d'objets imbriquées (1-20). Des valeurs plus élevées capturent plus de détails mais augmentent la taille du vecteur.", + "How often to check for updates (1-168 hours)": "Fréquence de vérification des mises à jour (1-168 heures)", + "How to handle retries for failed webhook deliveries": "Comment gérer les nouvelles tentatives pour les livraisons de webhook échouées", + "ID": "ID", + "ID:": "ID :", + "IP rate limits cleared successfully": "Limites de débit IP effacées avec succès", + "Identify properties not in the schema": "Identifier les propriétés absentes du schéma", + "Immutable": "Immuable", + "Import": "Importer", + "Import successful": "Importation réussie", + "Improved Property": "Propriété améliorée", + "In use": "Utilisé", + "Inactive": "Inactif", + "Include IDs and names of related objects for better contextual search": "Inclure les identifiants et les noms des objets liés pour une meilleure recherche contextuelle", + "Include related object references": "Inclure les références aux objets liés", + "Include schema and register metadata": "Inclure les métadonnées du schéma et du registre", + "Initialization failed": "Échec de l'initialisation", + "Initialize Project": "Initialiser le projet", + "Inspect Fields": "Inspecter les champs", + "Inspect Index": "Inspecter l'index", + "Invalid": "Non valide", + "Invalid batch size. Must be between 1 and 5000": "Taille de lot non valide. Doit être comprise entre 1 et 5000", + "Invalid field name provided": "Nom de champ fourni non valide", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects non valide. Doit être 0 (tous) ou un nombre positif", + "Invalid:": "Non valide :", + "Issue": "Problème", + "Items per page:": "Éléments par page :", + "Just now": "À l'instant", + "LLM Configuration": "Configuration LLM", + "LLM actions menu": "Menu des actions LLM", + "LLM configuration saved successfully": "Configuration LLM enregistrée avec succès", + "LLM features disabled": "Fonctionnalités LLM désactivées", + "LLM features enabled": "Fonctionnalités LLM activées", + "LLM must be enabled with an embedding provider configured": "Le LLM doit être activé avec un fournisseur de plongement configuré", + "LLM settings updated successfully": "Paramètres LLM mis à jour avec succès", + "Last": "Dernier", + "Last Triggered": "Dernier déclenchement", + "Length Range:": "Plage de longueur :", + "Linear": "Linéaire", + "Load Advanced Filters": "Charger les filtres avancés", + "Load advanced filters with live data from your search index": "Charger les filtres avancés avec des données en direct de votre index de recherche", + "Load view": "Charger la vue", + "Loading ConfigSets...": "Chargement des ConfigSets...", + "Loading advanced filters...": "Chargement des filtres avancés...", + "Loading agents...": "Chargement des agents...", + "Loading application details...": "Chargement des détails de l'application...", + "Loading applications...": "Chargement des applications...", + "Loading archived conversations...": "Chargement des conversations archivées...", + "Loading audit trails...": "Chargement des pistes d'audit...", + "Loading collections...": "Chargement des collections...", + "Loading configurations...": "Chargement des configurations...", + "Loading conversation...": "Chargement de la conversation...", + "Loading conversations...": "Chargement des conversations...", + "Loading deleted items...": "Chargement des éléments supprimés...", + "Loading events...": "Chargement des événements...", + "Loading filter data...": "Chargement des données de filtre...", + "Loading groups...": "Chargement des groupes...", + "Loading log details...": "Chargement des détails du journal...", + "Loading registers...": "Chargement des registres...", + "Loading schemas...": "Chargement des schémas...", + "Loading search trails...": "Chargement des pistes de recherche...", + "Loading sources...": "Chargement des sources...", + "Loading statistics...": "Chargement des statistiques...", + "Loading users...": "Chargement des utilisateurs...", + "Loading views...": "Chargement des vues...", + "Loading...": "Chargement...", + "Local": "Local", + "Local Version": "Version locale", + "Locked": "Verrouillé", + "Locked:": "Verrouillé :", + "Login successful": "Connexion réussie", + "Logs": "Journaux", + "Low": "Faible", + "Low Confidence": "Confiance faible", + "Lucene Version:": "Version Lucene :", + "Magic table synchronized successfully": "Table magique synchronisée avec succès", + "Manage Organisation Roles": "Gérer les rôles de l'organisation", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Gérez les collections SOLR (entrepôts de données) et affectez-les aux objets et aux fichiers.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Gérez les ConfigSets SOLR (modèles de configuration) de vos collections.", + "Manage and configure agents for automated tasks": "Gérer et configurer les agents pour les tâches automatisées", + "Manage and monitor file text extraction status": "Gérer et surveiller le statut de l'extraction de texte des fichiers", + "Manage and restore soft deleted items from your registers": "Gérer et restaurer les éléments supprimés de façon réversible de vos registres", + "Manage and view detected entities from files and objects": "Gérer et consulter les entités détectées dans les fichiers et les objets", + "Manage document templates and themes": "Gérer les modèles de documents et les thèmes", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Gérez les workflows n8n pour l'automatisation d'OpenRegister. Les workflows seront stockés dans le projet configuré.", + "Manage webhooks for event-driven integrations": "Gérer les webhooks pour les intégrations pilotées par les événements", + "Manage your applications and modules": "Gérez vos applications et modules", + "Manage your data registers and their configurations": "Gérez vos registres de données et leurs configurations", + "Manage your data schemas and their properties": "Gérez vos schémas de données et leurs propriétés", + "Manage your data sources and their configurations": "Gérez vos sources de données et leurs configurations", + "Manage your saved search configurations": "Gérez vos configurations de recherche enregistrées", + "Manage your system configurations and settings": "Gérez vos configurations et paramètres système", + "Managed": "Géré", + "Max Files (0 = all)": "Fichiers max. (0 = tous)", + "Max Length": "Longueur max.", + "Max Length:": "Longueur max. :", + "Max Objects (0 = all)": "Objets max. (0 = tous)", + "Max Retries": "Tentatives max.", + "Max Shards/Node": "Fragments max./nœud", + "Max execution time (ms)": "Temps d'exécution max. (ms)", + "Max ms": "ms max.", + "Max result count": "Nombre de résultats max.", + "Max results": "Résultats max.", + "Maximum": "Maximum", + "Maximum Nesting Depth": "Profondeur d'imbrication maximale", + "Maximum length constraint is missing": "La contrainte de longueur maximale est manquante", + "Maximum length is too restrictive": "La longueur maximale est trop restrictive", + "Maximum number of files to process. Set to 0 to process all files.": "Nombre maximum de fichiers à traiter. Définissez sur 0 pour traiter tous les fichiers.", + "Maximum number of objects to process. Set to 0 to process all objects.": "Nombre maximum d'objets à traiter. Définissez sur 0 pour traiter tous les objets.", + "Maximum number of retry attempts for failed deliveries": "Nombre maximum de nouvelles tentatives pour les livraisons échouées", + "Maximum time to wait before committing changes": "Temps d'attente maximum avant de valider les modifications", + "Maximum tokens to generate": "Nombre maximum de jetons à générer", + "Maximum value constraint is missing": "La contrainte de valeur maximale est manquante", + "Maximum value is too restrictive": "La valeur maximale est trop restrictive", + "Medium": "Moyen", + "Medium Confidence": "Confiance moyenne", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Requêtes moyennes : recherches avec un certain filtrage ou plusieurs paramètres (par ex. plages de dates, registres/schémas spécifiques)", + "Members of selected groups can access this view": "Les membres des groupes sélectionnés peuvent accéder à cette vue", + "Members:": "Membres :", + "Memory prediction calculated successfully": "Prédiction de mémoire calculée avec succès", + "Message content is required": "Le contenu du message est requis", + "Message does not belong to this conversation": "Le message n'appartient pas à cette conversation", + "Metadata Filters": "Filtres de métadonnées", + "Method": "Méthode", + "Min Length": "Longueur min.", + "Min execution time (ms)": "Temps d'exécution min. (ms)", + "Min ms": "ms min.", + "Min result count": "Nombre de résultats min.", + "Min results": "Résultats min.", + "Minimum": "Minimum", + "Minimum value constraint is missing": "La contrainte de valeur minimale est manquante", + "Minimum value is too restrictive": "La valeur minimale est trop restrictive", + "Missing object fields created successfully": "Champs d'objet manquants créés avec succès", + "Mode:": "Mode :", + "Model": "Modèle", + "Model Name": "Nom du modèle", + "Modified": "Modifié", + "Monthly": "Mensuel", + "More sources (10-20):": "Plus de sources (10-20) :", + "Most Active Objects": "Objets les plus actifs", + "Multiple Of": "Multiple de", + "Name": "Nom", + "Names cache warmup completed": "Préchauffage du cache des noms terminé", + "New Collection Name": "Nom de la nouvelle collection", + "New Conversation": "Nouvelle conversation", + "New Properties": "Nouvelles propriétés", + "New Property": "Nouvelle propriété", + "New Value": "Nouvelle valeur", + "New users without specific organisation membership will be automatically added to this organisation": "Les nouveaux utilisateurs sans appartenance à une organisation spécifique seront automatiquement ajoutés à cette organisation", + "Next": "Suivant", + "No": "Non", + "No ConfigSets found": "Aucun ConfigSet trouvé", + "No GitHub token configured": "Aucun jeton GitHub configuré", + "No GitHub token provided": "Aucun jeton GitHub fourni", + "No GitLab token provided": "Aucun jeton GitLab fourni", + "No Organisation": "Aucune organisation", + "No active filters": "Aucun filtre actif", + "No activity data available": "Aucune donnée d'activité disponible", + "No agents available": "Aucun agent disponible", + "No agents found": "Aucun agent trouvé", + "No app store cache exists yet": "Aucun cache de l'App Store n'existe encore", + "No applications are available.": "Aucune application n'est disponible.", + "No applications found": "Aucune application trouvée", + "No archived conversations": "Aucune conversation archivée", + "No audit trail entries found": "Aucune entrée de piste d'audit trouvée", + "No changes recorded": "Aucune modification enregistrée", + "No chunks to vectorize": "Aucun fragment à vectoriser", + "No collections found": "Aucune collection trouvée", + "No configuration data": "Aucune donnée de configuration", + "No configurations are available.": "Aucune configuration n'est disponible.", + "No configurations found": "Aucune configuration trouvée", + "No configurations found for this application.": "Aucune configuration trouvée pour cette application.", + "No conversations yet. Create a new one to get started!": "Aucune conversation pour le moment. Créez-en une nouvelle pour commencer !", + "No data available": "Aucune donnée disponible", + "No data available for chart": "Aucune donnée disponible pour le graphique", + "No deleted items found": "Aucun élément supprimé trouvé", + "No deletion data available": "Aucune donnée de suppression disponible", + "No description found": "Aucune description trouvée", + "No description provided": "Aucune description fournie", + "No entities detected in this file. Run text extraction first.": "Aucune entité détectée dans ce fichier. Exécutez d'abord l'extraction de texte.", + "No entities found": "Aucune entité trouvée", + "No entities have been detected yet": "Aucune entité n'a encore été détectée", + "No events found": "Aucun événement trouvé", + "No expired audit trails found to clear": "Aucune piste d'audit expirée à effacer", + "No facetable fields available. Select a register and schema to see available filters.": "Aucun champ à facettes disponible. Sélectionnez un registre et un schéma pour voir les filtres disponibles.", + "No files found": "Aucun fichier trouvé", + "No files have been extracted yet": "Aucun fichier n'a encore été extrait", + "No files to process": "Aucun fichier à traiter", + "No files to reindex": "Aucun fichier à réindexer", + "No filters are currently active. This will delete ALL audit trail entries!": "Aucun filtre n'est actuellement actif. Cela supprimera TOUTES les entrées de piste d'audit !", + "No log entries found": "Aucune entrée de journal trouvée", + "No logs are available for this configuration.": "Aucun journal n'est disponible pour cette configuration.", + "No logs are available for this source.": "Aucun journal n'est disponible pour cette source.", + "No logs found": "Aucun journal trouvé", + "No mismatched fields found - SOLR schema is properly configured": "Aucun champ discordant trouvé - le schéma SOLR est correctement configuré", + "No objects found": "Aucun objet trouvé", + "No objects found to vectorize": "Aucun objet à vectoriser trouvé", + "No objects selected for deletion": "Aucun objet sélectionné pour la suppression", + "No objects to delete": "Aucun objet à supprimer", + "No objects were permanently deleted": "Aucun objet n'a été supprimé définitivement", + "No properties found": "Aucune propriété trouvée", + "No properties match your filters.": "Aucune propriété ne correspond à vos filtres.", + "No purge date set": "Aucune date de purge définie", + "No register data available": "Aucune donnée de registre disponible", + "No registers found": "Aucun registre trouvé", + "No registers found for this application.": "Aucun registre trouvé pour cette application.", + "No relations found": "Aucune relation trouvée", + "No request body available": "Aucun corps de requête disponible", + "No response body available": "Aucun corps de réponse disponible", + "No saved views yet. Create one in the Search tab!": "Aucune vue enregistrée pour le moment. Créez-en une dans l'onglet Recherche !", + "No schema data available": "Aucune donnée de schéma disponible", + "No schema selected for exploration": "Aucun schéma sélectionné pour l'exploration", + "No schemas found": "Aucun schéma trouvé", + "No schemas found for this application.": "Aucun schéma trouvé pour cette application.", + "No search terms data available": "Aucune donnée de termes de recherche disponible", + "No search trail entries found": "Aucune entrée de piste de recherche trouvée", + "No sources are available.": "Aucune source n'est disponible.", + "No sources found": "Aucune source trouvée", + "No templates found": "Aucun modèle trouvé", + "No templates have been created yet": "Aucun modèle n'a encore été créé", + "No valid magic-mapped register+schema combinations found": "Aucune combinaison registre+schéma à mappage magique valide trouvée", + "No views found. Create views first before configuring vectorization.": "Aucune vue trouvée. Créez d'abord des vues avant de configurer la vectorisation.", + "No views match your search": "Aucune vue ne correspond à votre recherche", + "No webhooks found": "Aucun webhook trouvé", + "No webhooks have been configured yet": "Aucun webhook n'a encore été configuré", + "No workflows found in this project. Create workflows in the n8n editor.": "Aucun workflow trouvé dans ce projet. Créez des workflows dans l'éditeur n8n.", + "None": "Aucun", + "Not helpful": "Pas utile", + "Not in use": "Non utilisé", + "Number Constraints": "Contraintes numériques", + "Number of chunks to vectorize in one API call (1-100).": "Nombre de fragments à vectoriser en un seul appel API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Nombre de fragments à vectoriser en un seul appel API. Plus élevé = plus rapide mais plus de mémoire. Recommandé : 10-50.", + "Number of objects to process in each batch (1-5000).": "Nombre d'objets à traiter dans chaque lot (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Nombre d'objets à vectoriser en un seul appel API. Plus élevé = plus rapide mais plus de mémoire. Recommandé : 10-50.", + "Numeric Range:": "Plage numérique :", + "Object": "Objet", + "Object Collection": "Collection d'objets", + "Object Count Prediction": "Prédiction du nombre d'objets", + "Object ID": "Identifiant d'objet", + "Object Management": "Gestion des objets", + "Object Vectorization": "Vectorisation des objets", + "Object Warmup": "Préchauffage des objets", + "Object collection not configured": "Collection d'objets non configurée", + "Object not found": "Objet introuvable", + "Object permanently deleted": "Objet supprimé définitivement", + "Object restored successfully": "Objet restauré avec succès", + "Object settings updated successfully": "Paramètres de l'objet mis à jour avec succès", + "Object sources": "Sources d'objets", + "Object unlocked successfully": "Objet déverrouillé avec succès", + "Object vectorization configuration saved successfully": "Configuration de la vectorisation des objets enregistrée avec succès", + "Object vectorized successfully": "Objet vectorisé avec succès", + "Object:": "Objet :", + "Objects": "Objets", + "Objects Analyzed": "Objets analysés", + "Objects Distribution": "Répartition des objets", + "Objects being analyzed": "Objets en cours d'analyse", + "Objects by Register": "Objets par registre", + "Objects by Schema": "Objets par schéma", + "Objects deletion completed successfully": "Suppression des objets terminée avec succès", + "Objects in schema": "Objets dans le schéma", + "Objects to Process:": "Objets à traiter :", + "Objects to be analyzed": "Objets à analyser", + "Objects to be deleted": "Objets à supprimer", + "Objects to be validated": "Objets à valider", + "Objects will be serialized as JSON text before vectorization": "Les objets seront sérialisés sous forme de texte JSON avant la vectorisation", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Les objets seront supprimés de façon réversible (marqués comme supprimés mais conservés dans la base de données). Ils pourront être récupérés ultérieurement si nécessaire.", + "Old Value": "Ancienne valeur", + "Oldest Item (days)": "Élément le plus ancien (jours)", + "Ollama Chat Settings": "Paramètres de chat Ollama", + "Ollama Embedding Configuration": "Configuration de plongement Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Seulement avec des modifications", + "Open Register Objects": "Objets Open Register", + "Open n8n Editor": "Ouvrir l'éditeur n8n", + "OpenAI Chat Settings": "Paramètres de chat OpenAI", + "OpenAI Embedding Configuration": "Configuration de plongement OpenAI", + "OpenRegister Settings": "Paramètres OpenRegister", + "Optional webhook secret for signature verification": "Secret de webhook facultatif pour la vérification de signature", + "Order": "Ordre", + "Organisation": "Organisation", + "Organisation ID": "Identifiant de l'organisation", + "Organisation Members": "Membres de l'organisation", + "Organisation Statistics": "Statistiques de l'organisation", + "Organisation created successfully": "Organisation créée avec succès", + "Organisation settings saved successfully": "Paramètres de l'organisation enregistrés avec succès", + "Organisations": "Organisations", + "Organization": "Organisation", + "Organization ID (Optional)": "Identifiant de l'organisation (facultatif)", + "Orphaned Items": "Éléments orphelins", + "Owner": "Propriétaire", + "Owner:": "Propriétaire :", + "Parallel Mode (Faster, more resource intensive)": "Mode parallèle (plus rapide, plus gourmand en ressources)", + "Parallel:": "Parallèle :", + "Password": "Mot de passe", + "Password for SOLR authentication (optional)": "Mot de passe pour l'authentification SOLR (facultatif)", + "Path": "Chemin", + "Path in repository": "Chemin dans le dépôt", + "Path where the configuration file will be saved in the repository": "Chemin où le fichier de configuration sera enregistré dans le dépôt", + "Path where the register OAS file will be saved in the repository": "Chemin où le fichier OAS du registre sera enregistré dans le dépôt", + "Pattern (regex)": "Motif (regex)", + "Pattern Issue": "Problème de motif", + "Pattern constraint is missing": "La contrainte de motif est manquante", + "Patterns:": "Motifs :", + "Pending": "En attente", + "Permanent Delete Mode": "Mode de suppression définitive", + "Permanently Delete": "Supprimer définitivement", + "Permanently delete already soft-deleted objects": "Supprimer définitivement les objets déjà supprimés de façon réversible", + "Permanently delete this conversation?": "Supprimer définitivement cette conversation ?", + "Person": "Personne", + "Personal Data": "Données personnelles", + "Phone": "Téléphone", + "Photos": "Photos", + "Please create an agent in the": "Veuillez créer un agent dans le", + "Please select an agent to continue": "Veuillez sélectionner un agent pour continuer", + "Please select which register and schema to use for the new object": "Veuillez sélectionner le registre et le schéma à utiliser pour le nouvel objet", + "Please try again later.": "Veuillez réessayer plus tard.", + "Please wait while we fetch your agents.": "Veuillez patienter pendant que nous récupérons vos agents.", + "Please wait while we fetch your applications.": "Veuillez patienter pendant que nous récupérons vos applications.", + "Please wait while we fetch your configurations.": "Veuillez patienter pendant que nous récupérons vos configurations.", + "Please wait while we fetch your deleted items.": "Veuillez patienter pendant que nous récupérons vos éléments supprimés.", + "Please wait while we fetch your sources.": "Veuillez patienter pendant que nous récupérons vos sources.", + "Polls": "Sondages", + "Popular Search Terms": "Termes de recherche populaires", + "Port": "Port", + "Prerequisites": "Prérequis", + "Press Enter to send, Shift+Enter for new line": "Appuyez sur Entrée pour envoyer, Maj+Entrée pour une nouvelle ligne", + "Previous": "Précédent", + "Private": "Privé", + "Processes chunks in batches with simulated parallelism.": "Traite les fragments par lots avec un parallélisme simulé.", + "Processes file chunks sequentially (safest).": "Traite les fragments de fichiers de manière séquentielle (le plus sûr).", + "Processes objects in chunks with simulated parallelism.": "Traite les objets par fragments avec un parallélisme simulé.", + "Processes objects sequentially (safest).": "Traite les objets de manière séquentielle (le plus sûr).", + "Processing": "Traitement", + "Processing Limits": "Limites de traitement", + "Processing...": "Traitement en cours...", + "Project initialized successfully": "Projet initialisé avec succès", + "Project not found. Please initialize first.": "Projet introuvable. Veuillez d'abord l'initialiser.", + "Properties": "Propriétés", + "Property Behaviors": "Comportements de propriété", + "Property Configuration:": "Configuration de la propriété :", + "Property Filters": "Filtres de propriété", + "Property Title": "Titre de la propriété", + "Property Type": "Type de propriété", + "Property can be improved": "La propriété peut être améliorée", + "Protected": "Protégé", + "Provider": "Fournisseur", + "Provider is required for testing": "Le fournisseur est requis pour les tests", + "Public": "Public", + "Public View": "Vue publique", + "Public views can be accessed by anyone in the system": "Les vues publiques sont accessibles à toute personne du système", + "Publish": "Publier", + "Publish Objects": "Publier les objets", + "Publish all objects in this schema": "Publier tous les objets de ce schéma", + "Published": "Publié", + "Published:": "Publié :", + "Publishing...": "Publication en cours...", + "Purge": "Purger", + "Purge Date": "Date de purge", + "Query Complexity Distribution": "Répartition de la complexité des requêtes", + "Query parameter is required": "Le paramètre de requête est requis", + "RAG": "RAG", + "RAG Configuration": "Configuration RAG", + "Rate limits cleared successfully": "Limites de débit effacées avec succès", + "Raw Changes Data": "Données brutes des modifications", + "Re-vectorize on object update": "Revectoriser lors de la mise à jour de l'objet", + "Read": "Lecture", + "Recommendations:": "Recommandations :", + "Recommended Type:": "Type recommandé :", + "Recommended: 5 sources": "Recommandé : 5 sources", + "Refresh": "Actualiser", + "Refresh Data": "Actualiser les données", + "Refresh Stats": "Actualiser les statistiques", + "Refresh Workflows": "Actualiser les workflows", + "Refresh dashboard": "Actualiser le tableau de bord", + "Refresh database info": "Actualiser les informations de la base de données", + "Register": "Registre", + "Register ID": "Identifiant du registre", + "Register Statistics": "Statistiques du registre", + "Register Totals": "Totaux du registre", + "Register depublished successfully": "Registre dépublié avec succès", + "Register not found": "Registre introuvable", + "Register objects deletion completed successfully": "Suppression des objets du registre terminée avec succès", + "Register published successfully": "Registre publié avec succès", + "Register/Schema Usage": "Utilisation des registres/schémas", + "Registers": "Registres", + "Reindex": "Réindexer", + "Reindex completed": "Réindexation terminée", + "Reindex completed successfully": "Réindexation terminée avec succès", + "Reindex failed": "Échec de la réindexation", + "Relations": "Relations", + "Remote Version": "Version distante", + "Remove": "Supprimer", + "Remove filter": "Supprimer le filtre", + "Remove from favorites": "Retirer des favoris", + "Remove group": "Supprimer le groupe", + "Removed": "Supprimé", + "Removed from favorites": "Retiré des favoris", + "Rename Conversation": "Renommer la conversation", + "Rename conversation": "Renommer la conversation", + "Replicas": "Réplicas", + "Repository": "Dépôt", + "Request Body": "Corps de la requête", + "Request Data": "Données de la requête", + "Request Details": "Détails de la requête", + "Request body must be JSON with a \"query\" field": "Le corps de la requête doit être au format JSON avec un champ « query »", + "Request timeout in seconds": "Délai d'expiration de la requête en secondes", + "Required field": "Champ obligatoire", + "Required status is inconsistent": "Le statut obligatoire est incohérent", + "Rerun Search": "Relancer la recherche", + "Reset Changes": "Réinitialiser les modifications", + "Reset Filters": "Réinitialiser les filtres", + "Response Body": "Corps de la réponse", + "Response Details": "Détails de la réponse", + "Response Time:": "Temps de réponse :", + "Restore": "Restaurer", + "Restore conversation": "Restaurer la conversation", + "Restore or permanently delete items": "Restaurer ou supprimer définitivement les éléments", + "Result Count Range": "Plage du nombre de résultats", + "Results": "Résultats", + "Retrieve all objects for this schema": "Récupérer tous les objets de ce schéma", + "Retry": "Réessayer", + "Retry Failed Extractions": "Réessayer les extractions échouées", + "Retry Policy": "Politique de nouvelle tentative", + "Retry completed": "Nouvelle tentative terminée", + "Retrying...": "Nouvelle tentative en cours...", + "Risk Level": "Niveau de risque", + "Roles updated successfully": "Rôles mis à jour avec succès", + "SOLR Connection Settings": "Paramètres de connexion SOLR", + "SOLR Version:": "Version SOLR :", + "SOLR actions menu": "Menu des actions SOLR", + "SOLR base path (usually /solr)": "Chemin de base SOLR (généralement /solr)", + "SOLR is disabled": "SOLR est désactivé", + "SOLR is not available or not configured": "SOLR n'est pas disponible ou n'est pas configuré", + "SOLR logging disabled": "Journalisation SOLR désactivée", + "SOLR logging enabled": "Journalisation SOLR activée", + "SOLR search disabled": "Recherche SOLR désactivée", + "SOLR search enabled": "Recherche SOLR activée", + "SOLR server hostname or IP address": "Nom d'hôte ou adresse IP du serveur SOLR", + "SOLR server port number (optional, defaults to 8983)": "Numéro de port du serveur SOLR (facultatif, 8983 par défaut)", + "SOLR setup completed successfully": "Configuration SOLR terminée avec succès", + "SOLR setup failed": "Échec de la configuration SOLR", + "SOLR setup failed - check logs": "Échec de la configuration SOLR - consultez les journaux", + "Sample Values:": "Valeurs d'exemple :", + "Save": "Enregistrer", + "Save Configuration": "Enregistrer la configuration", + "Save Connection Settings": "Enregistrer les paramètres de connexion", + "Save Roles": "Enregistrer les rôles", + "Save Settings": "Enregistrer les paramètres", + "Save current search as view": "Enregistrer la recherche actuelle en tant que vue", + "Saved Views": "Vues enregistrées", + "Saving...": "Enregistrement en cours...", + "Schema": "Schéma", + "Schema ID": "Identifiant du schéma", + "Schema Name": "Nom du schéma", + "Schema Statistics": "Statistiques du schéma", + "Schema depublished successfully": "Schéma dépublié avec succès", + "Schema objects deletion completed successfully": "Suppression des objets du schéma terminée avec succès", + "Schema published successfully": "Schéma publié avec succès", + "Schema successfully created": "Schéma créé avec succès", + "Schema successfully updated": "Schéma mis à jour avec succès", + "Schemas": "Schémas", + "Scheme": "Schéma", + "Search / Views": "Recherche / Vues", + "Search Activity": "Activité de recherche", + "Search Analytics": "Analytique de recherche", + "Search Entities": "Rechercher des entités", + "Search Files": "Rechercher des fichiers", + "Search GitHub": "Rechercher sur GitHub", + "Search GitLab": "Rechercher sur GitLab", + "Search Method": "Méthode de recherche", + "Search Term": "Terme de recherche", + "Search Trail Details": "Détails de la piste de recherche", + "Search Trail Management": "Gestion des pistes de recherche", + "Search Trail Statistics": "Statistiques des pistes de recherche", + "Search Trails": "Pistes de recherche", + "Search Views": "Rechercher des vues", + "Search Webhooks": "Rechercher des webhooks", + "Search by file name or path": "Rechercher par nom ou chemin de fichier", + "Search by name or URL": "Rechercher par nom ou URL", + "Search by value": "Rechercher par valeur", + "Search in Files": "Rechercher dans les fichiers", + "Search in Objects": "Rechercher dans les objets", + "Search property names...": "Rechercher des noms de propriétés...", + "Search trail deletion not implemented yet": "La suppression des pistes de recherche n'est pas encore implémentée", + "Search views...": "Rechercher des vues...", + "Secret": "Secret", + "Secret key for HMAC signature generation (optional)": "Clé secrète pour la génération de signature HMAC (facultatif)", + "Select AI Agent": "Sélectionner un agent IA", + "Select All": "Tout sélectionner", + "Select ConfigSet": "Sélectionner un ConfigSet", + "Select File Types to Vectorize:": "Sélectionner les types de fichiers à vectoriser :", + "Select HTTP method": "Sélectionner la méthode HTTP", + "Select Register and Schema": "Sélectionner le registre et le schéma", + "Select Views to Vectorize:": "Sélectionner les vues à vectoriser :", + "Select a Nextcloud group to add": "Sélectionner un groupe Nextcloud à ajouter", + "Select a branch": "Sélectionner une branche", + "Select a model or type a custom model name": "Sélectionnez un modèle ou saisissez un nom de modèle personnalisé", + "Select a repository": "Sélectionner un dépôt", + "Select a repository you have write access to": "Sélectionnez un dépôt sur lequel vous avez un accès en écriture", + "Select an AI Agent": "Sélectionner un agent IA", + "Select an AI agent to begin chatting with your data.": "Sélectionnez un agent IA pour commencer à discuter avec vos données.", + "Select backend": "Sélectionner le backend", + "Select chat model": "Sélectionner le modèle de chat", + "Select collection for files": "Sélectionner la collection pour les fichiers", + "Select collection for objects": "Sélectionner la collection pour les objets", + "Select default organisation": "Sélectionner l'organisation par défaut", + "Select event to listen to...": "Sélectionner l'événement à écouter...", + "Select model": "Sélectionner le modèle", + "Select options...": "Sélectionner les options...", + "Select period": "Sélectionner la période", + "Select property to send as payload": "Sélectionner la propriété à envoyer comme charge utile", + "Select provider": "Sélectionner le fournisseur", + "Select registers and schemas to save a view": "Sélectionnez des registres et des schémas pour enregistrer une vue", + "Select retry policy": "Sélectionner la politique de nouvelle tentative", + "Select the branch to publish to": "Sélectionnez la branche vers laquelle publier", + "Select the event this webhook should listen to": "Sélectionnez l'événement que ce webhook doit écouter", + "Select views to vectorize:": "Sélectionner les vues à vectoriser :", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Sélectionnez les groupes Nextcloud disponibles pour cette organisation. Les utilisateurs de ces groupes auront accès aux ressources de l'organisation.", + "Select which data views the AI can search": "Sélectionnez les vues de données dans lesquelles l'IA peut effectuer des recherches", + "Select which property from the event should be used as the webhook payload data": "Sélectionnez quelle propriété de l'événement doit être utilisée comme données de charge utile du webhook", + "Select which tools the AI can use to perform actions": "Sélectionnez les outils que l'IA peut utiliser pour effectuer des actions", + "Selected": "Sélectionné", + "Selected Groups": "Groupes sélectionnés", + "Selected audit trails deleted successfully": "Pistes d'audit sélectionnées supprimées avec succès", + "Selected search trails deleted successfully": "Pistes de recherche sélectionnées supprimées avec succès", + "Selected users can access this view": "Les utilisateurs sélectionnés peuvent accéder à cette vue", + "Send additional feedback": "Envoyer un commentaire supplémentaire", + "Send as CloudEvent": "Envoyer en tant que CloudEvent", + "Sensitive PII": "Données personnelles sensibles", + "Serial Mode (Safer, slower)": "Mode série (plus sûr, plus lent)", + "Serial:": "Série :", + "Server Configuration": "Configuration du serveur", + "Server Information": "Informations sur le serveur", + "Settings": "Paramètres", + "Shards": "Fragments", + "Share": "Partager", + "Share with Groups": "Partager avec des groupes", + "Share with Users": "Partager avec des utilisateurs", + "Show Filters": "Afficher les filtres", + "Show less": "Afficher moins", + "Show only entries with changes": "Afficher uniquement les entrées avec des modifications", + "Simple": "Simple", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Requêtes simples : recherches textuelles de base avec un minimum de paramètres (par ex. un seul terme de recherche, sans filtres avancés)", + "Size": "Taille", + "Slug": "Slug", + "Soft Delete Mode": "Mode de suppression réversible", + "Soft Deleted Items": "Éléments supprimés de façon réversible", + "SolrCloud mode enabled": "Mode SolrCloud activé", + "Source": "Source", + "Sources": "Sources", + "Sources:": "Sources :", + "Standalone SOLR mode": "Mode SOLR autonome", + "Start Conversation": "Démarrer la conversation", + "Start Vectorization": "Démarrer la vectorisation", + "Start a conversation": "Démarrer une conversation", + "Starting names cache warmup...": "Démarrage du préchauffage du cache des noms...", + "Starting...": "Démarrage en cours...", + "Statistics": "Statistiques", + "Stats calculation not yet implemented": "Le calcul des statistiques n'est pas encore implémenté", + "Status": "Statut", + "Status Code": "Code de statut", + "String Constraints": "Contraintes de chaîne", + "Success": "Succès", + "Success Rate": "Taux de réussite", + "Success Status": "Statut de réussite", + "Successful": "Réussi", + "Successfully joined organisation": "Organisation rejointe avec succès", + "Sync Table": "Synchroniser la table", + "Synchronization Settings": "Paramètres de synchronisation", + "System Default": "Valeur système par défaut", + "Technical Description": "Description technique", + "Technical description for developers and administrators": "Description technique pour les développeurs et les administrateurs", + "Temperature": "Température", + "Templates": "Modèles", + "Test": "Tester", + "Test Chat": "Tester le chat", + "Test Connection": "Tester la connexion", + "Test Embedding": "Tester le plongement", + "Test endpoint executed successfully": "Point de terminaison de test exécuté avec succès", + "Test webhook delivered successfully": "Webhook de test livré avec succès", + "Test webhook delivery failed": "Échec de la livraison du webhook de test", + "Test webhook sent successfully": "Webhook de test envoyé avec succès", + "Testing...": "Test en cours...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Les fragments de texte sont générés lors de l'extraction des fichiers et stockés dans la base de données. La vectorisation lit ces fichiers pré-fragmentés et les convertit en plongements.", + "Text extracted successfully": "Texte extrait avec succès", + "Text extraction disabled": "Extraction de texte désactivée", + "The URL where webhook events will be sent": "L'URL vers laquelle les événements de webhook seront envoyés", + "The requested conversation does not exist": "La conversation demandée n'existe pas", + "The saved view and all its search configuration": "La vue enregistrée et toute sa configuration de recherche", + "There are no audit trail entries matching your current filters.": "Aucune entrée de piste d'audit ne correspond à vos filtres actuels.", + "There are no deleted items matching your current filters.": "Aucun élément supprimé ne correspond à vos filtres actuels.", + "There are no search trail entries matching your current filters.": "Aucune entrée de piste de recherche ne correspond à vos filtres actuels.", + "There are no webhook log entries matching your filters.": "Aucune entrée de journal de webhook ne correspond à vos filtres.", + "This action cannot be undone.": "Cette action est irréversible.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Cette action est irréversible. Assurez-vous qu'aucune collection n'utilise ce ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Cet agent n'a pas de vues ou d'outils configurables, mais vous pouvez toujours ajuster les paramètres RAG ci-dessous.", + "This analysis may take some time": "Cette analyse peut prendre un certain temps", + "This audit trail entry does not contain any change information.": "Cette entrée de piste d'audit ne contient aucune information de modification.", + "This configuration has no data defined.": "Cette configuration n'a aucune donnée définie.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ce point de terminaison est obsolète. Utilisez plutôt les points de terminaison basés sur les fragments.", + "This entity has no relations to objects or files": "Cette entité n'a aucune relation avec des objets ou des fichiers", + "This is a test webhook from OpenRegister": "Ceci est un webhook de test d'OpenRegister", + "This schema must use Magic Table configuration to sync": "Ce schéma doit utiliser la configuration de table magique pour se synchroniser", + "This source has no associated registers.": "Cette source n'a aucun registre associé.", + "This will copy the _default ConfigSet with the new name": "Cela copiera le ConfigSet _default avec le nouveau nom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Cela supprimera définitivement TOUS les plongements (vecteurs) de la base de données. Vous devrez revectoriser tous les objets et fichiers. Cette action est irréversible.\\n\\nVoulez-vous vraiment continuer ?", + "This will permanently delete:": "Cela supprimera définitivement :", + "Timeout (seconds)": "Délai d'expiration (secondes)", + "Timestamp": "Horodatage", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Astuce : n'activez que les vues qui nécessitent une recherche sémantique afin de minimiser les coûts de plongement. Les simples tables de correspondance ont rarement besoin de vectorisation.", + "Title": "Titre", + "To Date": "Date de fin", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Pour ajuster la taille ou la stratégie des fragments, accédez à Configuration des fichiers → Limites de traitement.", + "To change the embedding provider or model, go to LLM Configuration.": "Pour changer le fournisseur ou le modèle de plongement, accédez à Configuration LLM.", + "To date": "Date de fin", + "Toggle search sidebar": "Basculer la barre latérale de recherche", + "Tools": "Outils", + "Top Deleters": "Principaux auteurs de suppressions", + "Total": "Total", + "Total Audit Trails": "Total des pistes d'audit", + "Total Chunks Available:": "Total des fragments disponibles :", + "Total Deleted Items": "Total des éléments supprimés", + "Total Members": "Total des membres", + "Total Objects": "Total des objets", + "Total Objects in Database:": "Total des objets dans la base de données :", + "Total Organisations": "Total des organisations", + "Total Results": "Total des résultats", + "Total Searches": "Total des recherches", + "Total Size": "Taille totale", + "Total Storage": "Stockage total", + "Total:": "Total :", + "Totals": "Totaux", + "Type": "Type", + "Type Issue": "Problème de type", + "Type Variations:": "Variations de type :", + "Type must be \"positive\" or \"negative\"": "Le type doit être « positive » ou « negative »", + "Type to search for groups": "Saisissez pour rechercher des groupes", + "Type to search for users": "Saisissez pour rechercher des utilisateurs", + "Type to search groups...": "Saisissez pour rechercher des groupes...", + "Type to search users...": "Saisissez pour rechercher des utilisateurs...", + "URL": "URL", + "URL where Ollama is running": "URL où Ollama est en cours d'exécution", + "UUID:": "UUID :", + "Unchanged": "Inchangé", + "Unique Search Terms": "Termes de recherche uniques", + "Unique Terms": "Termes uniques", + "Unknown": "Inconnu", + "Unknown Register": "Registre inconnu", + "Unknown Schema": "Schéma inconnu", + "Untitled View": "Vue sans titre", + "Update": "Mettre à jour", + "Update Operations": "Opérations de mise à jour", + "Update configuration: ...": "Mettre à jour la configuration : ...", + "Update register OAS: ...": "Mettre à jour l'OAS du registre : ...", + "Update vectors when object data changes (recommended for accurate search)": "Mettre à jour les vecteurs lorsque les données de l'objet changent (recommandé pour une recherche précise)", + "Updated": "Mis à jour", + "Updated At": "Mis à jour le", + "Updated:": "Mis à jour :", + "Uptime:": "Disponibilité :", + "Usage Count": "Nombre d'utilisations", + "Use SolrCloud with Zookeeper for distributed search": "Utiliser SolrCloud avec Zookeeper pour la recherche distribuée", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Utilisez les filtres pour affiner les entrées du journal d'audit par registre, schéma, type d'action, utilisateur, plage de dates ou identifiant d'objet.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Utilisez les filtres pour affiner les éléments supprimés par registre, schéma, date de suppression ou utilisateur les ayant supprimés.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Utilisez les filtres pour affiner les entrées du journal de recherche par registre, schéma, statut de réussite, utilisateur, plage de dates, termes de recherche ou métriques de performance.", + "Used By Collections": "Utilisé par les collections", + "User": "Utilisateur", + "User Agent Statistics": "Statistiques des agents utilisateurs", + "User Description": "Description de l'utilisateur", + "User rate limits cleared successfully": "Limites de débit de l'utilisateur réinitialisées avec succès", + "User removed successfully": "Utilisateur supprimé avec succès", + "User-friendly description shown in forms and help text": "Description conviviale affichée dans les formulaires et le texte d'aide", + "Username": "Nom d'utilisateur", + "Username for SOLR authentication (optional)": "Nom d'utilisateur pour l'authentification SOLR (facultatif)", + "Users": "Utilisateurs", + "Using Pre-Generated Chunks": "Utilisation de fragments pré-générés", + "Validate": "Valider", + "Validate Objects": "Valider les objets", + "Validation completed successfully": "Validation terminée avec succès", + "Validation failed": "La validation a échoué", + "Value": "Valeur", + "Vector Dimensions": "Dimensions du vecteur", + "Vector Search Backend": "Backend de recherche vectorielle", + "Vector field: _embedding_": "Champ vectoriel : _embedding_", + "Vectorization Triggers": "Déclencheurs de vectorisation", + "Vectorize All Files": "Vectoriser tous les fichiers", + "Vectorize All Objects": "Vectoriser tous les objets", + "Vectorize all file types": "Vectoriser tous les types de fichiers", + "Vectorize all views": "Vectoriser toutes les vues", + "Vectorize on object creation": "Vectoriser à la création de l'objet", + "Vectors will be stored in your existing object and file collections": "Les vecteurs seront stockés dans vos collections d'objets et de fichiers existantes", + "Version": "Version", + "Very High": "Très élevé", + "View API Docs": "Voir la documentation de l'API", + "View Changes": "Voir les modifications", + "View Details": "Voir les détails", + "View Error": "Voir l'erreur", + "View File": "Voir le fichier", + "View Full Details": "Voir tous les détails", + "View Logs": "Voir les journaux", + "View Name": "Nom de la vue", + "View Object": "Voir l'objet", + "View Parameters": "Voir les paramètres", + "View Selection": "Sélection de la vue", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Consultez et analysez les journaux de recherche avec des capacités avancées de filtrage et d'analyse", + "View and analyze system audit trails with advanced filtering capabilities": "Consultez et analysez les journaux d'audit du système avec des capacités de filtrage avancées", + "View deleted successfully": "Vue supprimée avec succès", + "View entity details and manage relations": "Voir les détails de l'entité et gérer les relations", + "View name": "Nom de la vue", + "View name is required": "Le nom de la vue est requis", + "View saved successfully!": "Vue enregistrée avec succès !", + "View search analytics and manage search logs": "Consultez les analyses de recherche et gérez les journaux de recherche", + "View successfully deleted": "Vue supprimée avec succès", + "View successfully updated": "Vue mise à jour avec succès", + "View updated successfully!": "Vue mise à jour avec succès !", + "View webhook delivery logs and filter by webhook": "Consultez les journaux de livraison des webhooks et filtrez par webhook", + "Views": "Vues", + "Visible to users": "Visible par les utilisateurs", + "Wait for Response": "Attendre la réponse", + "Wait for webhook response before continuing (required for request/response flows)": "Attendre la réponse du webhook avant de continuer (requis pour les flux requête/réponse)", + "Warmup Names Cache": "Préchauffer le cache des noms", + "Warning": "Avertissement", + "Warning:": "Avertissement :", + "Webhook": "Webhook", + "Webhook Log Details": "Détails du journal du webhook", + "Webhook Logs": "Journaux des webhooks", + "Webhook created successfully": "Webhook créé avec succès", + "Webhook deleted": "Webhook supprimé", + "Webhook retry delivered successfully": "Nouvelle tentative de webhook livrée avec succès", + "Webhook retry delivery failed": "Échec de la nouvelle tentative de livraison du webhook", + "Webhook updated": "Webhook mis à jour", + "Webhook updated successfully": "Webhook mis à jour avec succès", + "Webhooks": "Webhooks", + "Weekly": "Hebdomadaire", + "Wrap webhook payload in CloudEvents format for better interoperability": "Encapsuler la charge utile du webhook au format CloudEvents pour une meilleure interopérabilité", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Oui", + "You": "Vous", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Vous pouvez créer de nouveaux ConfigSets basés sur le modèle _default, ou en téléverser des personnalisés directement sur votre serveur SOLR.", + "You do not have access to this conversation": "Vous n'avez pas accès à cette conversation", + "You do not have permission to delete this conversation": "Vous n'êtes pas autorisé à supprimer cette conversation", + "You do not have permission to modify this conversation": "Vous n'êtes pas autorisé à modifier cette conversation", + "You do not have permission to restore this conversation": "Vous n'êtes pas autorisé à restaurer cette conversation", + "You must be logged in to favorite views": "Vous devez être connecté pour ajouter des vues aux favoris", + "You need an AI agent to start a conversation.": "Vous avez besoin d'un agent IA pour démarrer une conversation.", + "Your Fireworks AI API key. Get one at": "Votre clé d'API Fireworks AI. Obtenez-en une sur", + "Your OpenAI API key. Get one at": "Votre clé d'API OpenAI. Obtenez-en une sur", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Votre retour a été enregistré. Vous pouvez éventuellement fournir des détails supplémentaires ici...", + "Zookeeper Hosts": "Hôtes Zookeeper", + "Zookeeper Port": "Port Zookeeper", + "Zookeeper Settings (SolrCloud)": "Paramètres Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Chaîne de connexion Zookeeper pour SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Numéro de port Zookeeper (facultatif, par défaut 2181)", + "chunk": "fragment", + "chunks": "fragments", + "chunks processed": "fragments traités", + "configuration(s)": "configuration(s)", + "e.g., lib/Settings/config.json": "p. ex., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "p. ex., lib/Settings/register.json", + "failed": "échoué", + "file": "fichier", + "files": "fichiers", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook": "https://example.com/webhook", + "members": "membres", + "menu or contact someone with permission to create agents.": "menu ou contactez une personne autorisée à créer des agents.", + "messages": "messages", + "n8n URL and API key are required": "L'URL et la clé d'API n8n sont requises", + "n8n configuration saved successfully": "Configuration n8n enregistrée avec succès", + "n8n connection not configured": "Connexion n8n non configurée", + "n8n connection successful": "Connexion n8n réussie", + "n8n connection test successful": "Test de connexion n8n réussi", + "n8n integration disabled": "Intégration n8n désactivée", + "n8n integration enabled": "Intégration n8n activée", + "n8n project initialized successfully": "Projet n8n initialisé avec succès", + "n8n settings saved successfully": "Paramètres n8n enregistrés avec succès", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objets", + "objects processed": "objets traités", + "or pick a range": "ou choisissez une plage", + "org-...": "org-...", + "provides a good balance between speed and accuracy.": "offre un bon équilibre entre vitesse et précision.", + "register(s)": "registre(s)", + "register(s) selected": "registre(s) sélectionné(s)", + "required": "requis", + "results": "résultats", + "schema(s)": "schéma(s)", + "schema(s) selected": "schéma(s) sélectionné(s)", + "sk-...": "sk-...", + "this application": "cette application", + "to": "à", + "vectorized": "vectorisé", + "views selected": "vues sélectionnées", + "ℹ️ Current Configuration": "ℹ️ Configuration actuelle", + "⚡ Batch Processing": "⚡ Traitement par lots", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Contexte complet ✓ Moins susceptible de manquer des détails ✗ Réponses plus lentes ✗ Peut inclure des informations moins pertinentes", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Réponses plus rapides ✓ Réponses plus ciblées ✗ Peut manquer des informations pertinentes", + "✨ AI Features": "✨ Fonctionnalités IA", + "All search trails cleared successfully": "Tous les journaux de recherche ont été effacés avec succès", + "Multiple search trail deletion not implemented yet": "La suppression de plusieurs journaux de recherche n'est pas encore implémentée", + "No expired search trails found to clear": "Aucun journal de recherche expiré à effacer trouvé", + "Missing message": "Message manquant", + "message content is required": "le contenu du message est requis", + "Missing conversation or agentUuid": "Conversation ou agentUuid manquant", + "Access denied": "Accès refusé", + "Conversation not found": "Conversation introuvable", + "AI service not configured": "Service IA non configuré", + "Failed to process message": "Échec du traitement du message", + "Missing conversationId": "conversationId manquant", + "conversationId is required": "conversationId est requis", + "Failed to fetch conversation history": "Échec de la récupération de l'historique de la conversation", + "Failed to clear conversation": "Échec de l'effacement de la conversation", + "Invalid feedback type": "Type de retour invalide", + "type must be \"positive\" or \"negative\"": "le type doit être « positive » ou « negative »", + "Message not found": "Message introuvable", + "Failed to save feedback": "Échec de l'enregistrement du retour", + "Failed to get chat statistics": "Échec de la récupération des statistiques de chat", + "Not Found": "Introuvable", + "Engine not found": "Moteur introuvable", + "Consumer not found": "Consommateur introuvable", + "Backend parameter is required": "Le paramètre backend est requis", + "Failed to get database information: %s": "Échec de la récupération des informations de la base de données : %s", + "SOLR setup error: %s": "Erreur de configuration SOLR : %s", + "Reindex failed: %s": "Échec de la réindexation : %s", + "Anonymized": "Anonymisé", + "Entities detected": "Entités détectées", + "Extract Now": "Extraire maintenant", + "Extracted at": "Extrait le", + "Extraction": "Extraction", + "Failed to load extraction data": "Échec du chargement des données d'extraction", + "Failed to load register data": "Échec du chargement des données du registre", + "Failed to retrieve extraction status.": "Échec de la récupération du statut d'extraction.", + "Failed to retrieve objects for file.": "Échec de la récupération des objets pour le fichier.", + "No extraction data available for this file": "Aucune donnée d'extraction disponible pour ce fichier", + "No register objects reference this file": "Aucun objet de registre ne référence ce fichier", + "Not extracted": "Non extrait", + "Register Objects": "Objets du registre", + "Risk level": "Niveau de risque", + "Text chunks": "Fragments de texte", + "Unknown error": "Erreur inconnue", + "Very high": "Très élevé", + "{title} in {register} / {schema}": "{title} dans {register} / {schema}", + "Push Notifications": "Notifications push", + "Real-time push notification status via notify_push": "Statut des notifications push en temps réel via notify_push", + "Realtime push not available — the notify_push app is not installed": "Push en temps réel non disponible — l'application notify_push n'est pas installée", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installez l'application notify_push depuis l'App Store Nextcloud pour activer les mises à jour en temps réel.", + "Open Nextcloud App Store": "Ouvrir l'App Store Nextcloud", + "notify_push is installed but not yet active": "notify_push est installé mais pas encore actif", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push est installé mais OpenRegister n'a pas encore confirmé de push réussi. Déclenchez un enregistrement d'objet pour l'activer, ou vérifiez votre configuration notify_push.", + "notify_push configuration guide": "Guide de configuration de notify_push", + "Realtime push active": "Push en temps réel actif", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Les notifications push en temps réel sont actives. Les clients connectés reçoivent des mises à jour instantanées lorsque des objets sont créés, mis à jour ou supprimés.", + "(Dolphin required)": "(Dolphin requis)", + "({count} selected)": "({count} sélectionné(s))", + ", {failed} failed": ", {failed} échoué(s)", + "0 = unlimited": "0 = illimité", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVERTISSEMENT DE SUPPRESSION DÉFINITIVE ⚠️\n\nVous êtes sur le point de supprimer DÉFINITIVEMENT TOUS les objets du schéma « {schema} » :\n\n• Objets actifs : {active}\n• Objets supprimés temporairement : {deleted}\n• Total : {total}\n\nCes objets seront complètement retirés de la base de données et NE POURRONT PAS être récupérés.\n\nÊtes-vous absolument sûr ?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVERTISSEMENT DE SUPPRESSION DÉFINITIVE ⚠️\n\nVous êtes sur le point de supprimer DÉFINITIVEMENT {count} objets supprimés temporairement du schéma « {schema} ».\n\nCes objets seront complètement retirés de la base de données et NE POURRONT PAS être récupérés.\n\nÊtes-vous absolument sûr ?", + "A deactivation request is pending.": "Une demande de désactivation est en attente.", + "Access organisation dashboard and analytics": "Accéder au tableau de bord et aux analyses de l'organisation", + "Account": "Compte", + "Active organisation changed successfully": "Organisation active modifiée avec succès", + "Active Organisation:": "Organisation active :", + "Activity": "Activité", + "Add": "Ajouter", + "Add a file or files": "Ajouter un ou plusieurs fichiers", + "Add Attachment": "Ajouter une pièce jointe", + "Add endpoint": "Ajouter un point de terminaison", + "Add oneOf entry": "Ajouter une entrée oneOf", + "Add Property to '{title}'": "Ajouter une propriété à « {title} »", + "Add range": "Ajouter une plage", + "Add Register": "Ajouter un registre", + "Add User": "Ajouter un utilisateur", + "Add User to Organisation": "Ajouter un utilisateur à l'organisation", + "Addresses": "Adresses", + "Agent successfully updated": "Agent mis à jour avec succès", + "Agent Type": "Type d'agent", + "Aggregated across schemas": "Agrégé sur l'ensemble des schémas", + "All file types": "Tous les types de fichiers", + "All-day events": "Événements sur toute la journée", + "Allowed MIME Types": "Types MIME autorisés", + "Already linked": "Déjà lié", + "An error occurred while deleting the object": "Une erreur s'est produite lors de la suppression de l'objet", + "An error occurred while restoring the objects": "Une erreur s'est produite lors de la restauration des objets", + "An error occurred while saving the organisation": "Une erreur s'est produite lors de l'enregistrement de l'organisation", + "and add the files there.": "et ajoutez-y les fichiers.", + "Anonymous Group": "Groupe anonyme", + "API Request Quota (requests/day)": "Quota de requêtes d'API (requêtes/jour)", + "API Token Configuration": "Configuration des jetons d'API", + "API Tokens": "Jetons d'API", + "App ID": "ID de l'application", + "Application identifier for this configuration (optional)": "Identifiant de l'application pour cette configuration (facultatif)", + "Application settings": "Paramètres de l'application", + "Application successfully created": "Application créée avec succès", + "Application successfully updated": "Application mise à jour avec succès", + "Apply": "Appliquer", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Appliquer un rôle à tous les schémas de ce registre qui n'ont pas de remplacements d'autorisation explicites.", + "Approval Progress": "Progression de l'approbation", + "Approve": "Approuver", + "Approved": "Approuvé", + "approved": "approuvé", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Êtes-vous sûr de vouloir effacer toutes les données de la collection « {name} » ?\n\nCela va :\n• Supprimer tous les documents indexés\n• Conserver la structure de la collection intacte\n• Cette action est irréversible", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Êtes-vous sûr de vouloir SUPPRIMER la collection « {name} » ?\n\nCela va :\n• Supprimer définitivement la collection et toutes ses données\n• Retirer tous les documents indexés\n• Cette action est irréversible", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Êtes-vous sûr de vouloir supprimer l'application « {name} » ? Cette action est irréversible.", + "Are you sure you want to delete this configset?": "Êtes-vous sûr de vouloir supprimer ce configset ?", + "Are you sure you want to leave '{name}'?": "Êtes-vous sûr de vouloir quitter « {name} » ?", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Êtes-vous sûr de vouloir réindexer la collection « {name} » ?\n\nCela va :\n• Reconstruire l'index avec tous les objets\n• Prendre plusieurs minutes\n• Peut affecter les performances de recherche pendant la réindexation", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Êtes-vous sûr de vouloir retirer le schéma « {schema} » du registre « {register} » ? Cette action est irréversible.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Êtes-vous sûr de vouloir supprimer temporairement {count} objets actifs du schéma « {schema} » ?\n\nIls seront marqués comme supprimés mais pourront être retirés définitivement ultérieurement.", + "Array Object Configuration:": "Configuration de l'objet de type tableau :", + "Assigned collaborative tags": "Étiquettes collaboratives attribuées", + "Assignment notifications": "Notifications d'affectation", + "Auto (e.g. Y for year, F Y for month)": "Auto (p. ex. Y pour l'année, F Y pour le mois)", + "Auto share": "Partage automatique", + "Automatically connect to the federation network.": "Se connecter automatiquement au réseau de fédération.", + "Automatically update catalogues.": "Mettre à jour automatiquement les catalogues.", + "Available placeholders:": "Espaces réservés disponibles :", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Les modifications d'avatar ne sont pas prises en charge par votre fournisseur d'authentification.", + "Avatar removed": "Avatar supprimé", + "Avatar updated successfully": "Avatar mis à jour avec succès", + "Bandwidth Quota (MB/month)": "Quota de bande passante (Mo/mois)", + "Behavior": "Comportement", + "Bulk Role Assignment: {title}": "Attribution de rôle en masse : {title}", + "Cache grew from {old} to {new} entries.": "Le cache est passé de {old} à {new} entrées.", + "Cache shrunk from {old} to {new} entries.": "Le cache a diminué de {old} à {new} entrées.", + "Cache stayed the same at {size} entries.": "Le cache est resté identique à {size} entrées.", + "Calendar": "Agenda", + "Calendar name": "Nom de l'agenda", + "Calendar Provider Configuration": "Configuration du fournisseur d'agenda", + "Cancel deactivation request": "Annuler la demande de désactivation", + "Cannot edit: This register is managed by external configuration {title}": "Modification impossible : ce registre est géré par la configuration externe {title}", + "Cannot remove schema with existing objects ({count} objects)": "Impossible de retirer un schéma contenant des objets ({count} objets)", + "Cannot remove schema {schema} because it contains {count} objects": "Impossible de retirer le schéma {schema} car il contient {count} objets", + "Cards": "Cartes", + "Cascade delete": "Suppression en cascade", + "Change password": "Modifier le mot de passe", + "Changes for Audit Trail #{id}": "Modifications pour le journal d'audit n°{id}", + "Chat test failed: {error}": "Le test du chat a échoué : {error}", + "Cleanup completed successfully. Deleted {count} entries.": "Nettoyage terminé avec succès. {count} entrées supprimées.", + "Cleanup failed: {error}": "Le nettoyage a échoué : {error}", + "Clear filters": "Effacer les filtres", + "Cluster name": "Nom du cluster", + "Collapse details": "Réduire les détails", + "Color": "Couleur", + "Comment...": "Commentaire...", + "completed": "terminé", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Requêtes complexes : recherches avancées avec plusieurs filtres, opérateurs et combinaisons de paramètres complexes", + "Confidence: {confidence}%": "Confiance : {confidence} %", + "Configuration is up to date": "La configuration est à jour", + "Configuration saved successfully.": "Configuration enregistrée avec succès.", + "Configuration type (default, application, etc.)": "Type de configuration (default, application, etc.)", + "Configure Apache SOLR search engine for advanced search capabilities": "Configurer le moteur de recherche Apache SOLR pour des capacités de recherche avancées", + "Configure API tokens for external service integrations": "Configurer les jetons d'API pour les intégrations de services externes", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configurez les paramètres de connexion de base de votre serveur SOLR, y compris l'authentification et les options réseau. Utilisez les dialogues distincts de gestion des configsets et des collections pour gérer les cores et les collections.", + "Configure default organisation and organisation-related settings": "Configurer l'organisation par défaut et les paramètres liés à l'organisation", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configurez les fournisseurs de grands modèles de langage (LLM) pour les fonctionnalités basées sur l'IA, notamment la recherche sémantique, les embeddings et le chat.", + "Configure multi-organization support and tenant isolation": "Configurer la prise en charge multi-organisations et l'isolation des locataires", + "Configure SOLR Facets": "Configurer les facettes SOLR", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configurez ce schéma pour faire apparaître les objets en tant qu'événements dans l'application Agenda de Nextcloud.", + "Confirm Account Deactivation": "Confirmer la désactivation du compte", + "Confirm deactivation": "Confirmer la désactivation", + "Confirm Object Deletion": "Confirmer la suppression de l'objet", + "Confirm Object Restoration": "Confirmer la restauration de l'objet", + "Confirm Permanent Object Deletion": "Confirmer la suppression définitive de l'objet", + "Contracts": "Contrats", + "Conversation started with {agent}": "Conversation démarrée avec {agent}", + "Conversations": "Conversations", + "Converting {schema} to blob storage...": "Conversion de {schema} en stockage blob...", + "Converting {schema} to magic table...": "Conversion de {schema} en table magique...", + "Copy of {name}": "Copie de {name}", + "Copy to clipboard": "Copier dans le presse-papiers", + "create": "créer", + "Create a new configset based on the _default template": "Créer un nouveau configset basé sur le modèle _default", + "Create a new SOLR collection from an existing configset": "Créer une nouvelle collection SOLR à partir d'un configset existant", + "Create another": "En créer un autre", + "Create API Token": "Créer un jeton d'API", + "Create copies of": "Créer des copies de", + "Create new token": "Créer un nouveau jeton", + "Create Organisation": "Créer une organisation", + "Current": "Actuel", + "Current password": "Mot de passe actuel", + "Custom display title for this facet": "Titre d'affichage personnalisé pour cette facette", + "Custom HTTP headers (one per line, format: header-name: value)": "En-têtes HTTP personnalisés (un par ligne, format : nom-en-tête: valeur)", + "Custom pattern:": "Modèle personnalisé :", + "Custom Ranges:": "Plages personnalisées :", + "Data": "Données", + "Data Sources": "Sources de données", + "Data Views": "Vues de données", + "Date Faceting:": "Facettage par date :", + "Dates": "Dates", + "Deactivation request cancelled": "Demande de désactivation annulée", + "Deactivation request submitted": "Demande de désactivation soumise", + "Default Object Tenant": "Locataire par défaut des objets", + "Default Owner": "Propriétaire par défaut", + "Default User (for cron/background jobs)": "Utilisateur par défaut (pour les tâches cron/en arrière-plan)", + "Default User Tenant": "Locataire par défaut des utilisateurs", + "Default value": "Valeur par défaut", + "Default value (Email)": "Valeur par défaut (e-mail)", + "Default value (Password)": "Valeur par défaut (mot de passe)", + "Default value (Phone number)": "Valeur par défaut (numéro de téléphone)", + "Default value (Regex)": "Valeur par défaut (regex)", + "Defaults to current user. Select a different user if needed.": "Par défaut, l'utilisateur actuel. Sélectionnez un autre utilisateur si nécessaire.", + "delete": "supprimer", + "Delete ({count})": "Supprimer ({count})", + "DELETE COLLECTION": "SUPPRIMER LA COLLECTION", + "Delete SOLR Collection": "Supprimer la collection SOLR", + "deleted objects section": "section des objets supprimés", + "Deleting SOLR Collection...": "Suppression de la collection SOLR...", + "Depublish": "Dépublier", + "Description shown as tooltip": "Description affichée en infobulle", + "Description Template": "Modèle de description", + "Detected: {date}": "Détecté : {date}", + "Direct permission": "Permission directe", + "Disable this to make the agent": "Désactivez ceci pour rendre l'agent", + "Display Format": "Format d'affichage", + "Display Name": "Nom d'affichage", + "Dolphin OCR enabled": "OCR Dolphin activé", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Téléchargez une copie de toutes vos données personnelles stockées dans OpenRegister (article 20 du RGPD).", + "Download API Specification": "Télécharger la spécification de l'API", + "Drag a file or files here": "Glissez un ou plusieurs fichiers ici", + "Dry run completed: {count} fields would be created": "Simulation terminée : {count} champs seraient créés", + "Dry run completed: {count} fields would be fixed": "Simulation terminée : {count} champs seraient corrigés", + "Duration in seconds (optional)": "Durée en secondes (facultatif)", + "e.g., lib/settings/config.json": "p. ex., lib/settings/config.json", + "e.g., lib/settings/register.json": "p. ex., lib/settings/register.json", + "Edit Agent": "Modifier l'agent", + "Edit Application": "Modifier l'application", + "Edit Configuration": "Modifier la configuration", + "Edit Endpoint": "Modifier le point de terminaison", + "Edit labels": "Modifier les étiquettes", + "Edit Organisation": "Modifier l'organisation", + "Edit Property '{key}' of '{title}'": "Modifier la propriété « {key} » de « {title} »", + "Edit Source": "Modifier la source", + "Edit tags for {name}": "Modifier les étiquettes de {name}", + "Email Addresses": "Adresses e-mail", + "Email digest frequency": "Fréquence du résumé par e-mail", + "Embedding test failed: {error}": "Le test d'embedding a échoué : {error}", + "Enable calendar provider": "Activer le fournisseur d'agenda", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Activez ou désactivez les fonctionnalités LLM. Configurez les fournisseurs et les modèles à l'aide du bouton de configuration LLM ci-dessus.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Activez ou désactivez l'intégration de la recherche SOLR. Configurez les paramètres de connexion à l'aide du bouton de paramètres de connexion ci-dessus.", + "Enable RAG": "Activer le RAG", + "Enable tools that allow the agent to interact with data through function calling.": "Activez les outils qui permettent à l'agent d'interagir avec les données via l'appel de fonctions.", + "Enable write-back to target objects": "Activer la réécriture vers les objets cibles", + "End Date Field": "Champ de date de fin", + "Endpoint Path*": "Chemin du point de terminaison*", + "Enter agent description (optional)": "Saisissez la description de l'agent (facultatif)", + "Enter agent name": "Saisissez le nom de l'agent", + "Enter application description (optional)": "Saisissez la description de l'application (facultatif)", + "Enter application name": "Saisissez le nom de l'application", + "Enter configuration description (optional)": "Saisissez la description de la configuration (facultatif)", + "Enter configuration title": "Saisissez le titre de la configuration", + "Enter Nextcloud usernames to grant access to this private agent": "Saisissez les noms d'utilisateur Nextcloud pour accorder l'accès à cet agent privé", + "Enter organisation description (optional)": "Saisissez la description de l'organisation (facultatif)", + "Enter organisation name": "Saisissez le nom de l'organisation", + "Enter search terms or leave empty to browse all": "Saisissez des termes de recherche ou laissez vide pour tout parcourir", + "Enter system prompt for the agent": "Saisissez l'invite système pour l'agent", + "Enter username and press Enter": "Saisissez le nom d'utilisateur et appuyez sur Entrée", + "Enter your API key": "Saisissez votre clé d'API", + "Enter your object here...": "Saisissez votre objet ici...", + "Enter your schema here...": "Saisissez votre schéma ici...", + "Entity ID: {id}": "ID de l'entité : {id}", + "Entity Recognition": "Reconnaissance d'entités", + "Entity Recognition Method": "Méthode de reconnaissance d'entités", + "Error deleting audit trails: {error}": "Erreur lors de la suppression des journaux d'audit : {error}", + "Error deleting search trails: {error}": "Erreur lors de la suppression des journaux de recherche : {error}", + "Example": "Exemple", + "Example value for this property": "Valeur d'exemple pour cette propriété", + "Exclusive maximum": "Maximum exclusif", + "Exclusive minimum": "Minimum exclusif", + "execute": "exécuter", + "Expiration": "Expiration", + "Expires": "Expire", + "Expires in (e.g., 90d)": "Expire dans (p. ex., 90d)", + "Export \"{schema}\" objects from \"{register}\"": "Exporter les objets « {schema} » depuis « {register} »", + "Export Configuration": "Exporter la configuration", + "Export downloaded successfully": "Export téléchargé avec succès", + "Export failed: {error}": "L'export a échoué : {error}", + "Export Format:": "Format d'export :", + "Export is rate limited. Please try again later.": "L'export est limité en débit. Veuillez réessayer plus tard.", + "Export my data": "Exporter mes données", + "Export Objects": "Exporter les objets", + "Exporting...": "Export en cours...", + "Extra Query Parameters": "Paramètres de requête supplémentaires", + "Extract now": "Extraire maintenant", + "Extraction error for {file}: {error}": "Erreur d'extraction pour {file} : {error}", + "Extraction Mode": "Mode d'extraction", + "Extraction Scope": "Portée de l'extraction", + "Facet Description": "Description de la facette", + "Facet Order": "Ordre de la facette", + "Facet Title": "Titre de la facette", + "Facet Type": "Type de facette", + "Facetable": "Facettable", + "Faceting Configuration:": "Configuration du facettage :", + "Failed to add user to organisation": "Échec de l'ajout de l'utilisateur à l'organisation", + "Failed to cancel deactivation": "Échec de l'annulation de la désactivation", + "Failed to change active organisation: {error}": "Échec du changement d'organisation active : {error}", + "Failed to change password": "Échec de la modification du mot de passe", + "Failed to check version: {error}": "Échec de la vérification de la version : {error}", + "Failed to clear embeddings: {error}": "Échec de l'effacement des embeddings : {error}", + "Failed to copy to clipboard": "Échec de la copie dans le presse-papiers", + "Failed to copy token": "Échec de la copie du jeton", + "Failed to create missing SOLR fields: {error}": "Échec de la création des champs SOLR manquants : {error}", + "Failed to create token": "Échec de la création du jeton", + "Failed to delete objects for {schema}: {error}": "Échec de la suppression des objets pour {schema} : {error}", + "Failed to delete SOLR collection: {error}": "Échec de la suppression de la collection SOLR : {error}", + "Failed to depublish register: {error}": "Échec de la dépublication du registre : {error}", + "Failed to depublish schema: {error}": "Échec de la dépublication du schéma : {error}", + "Failed to depublish: {error}": "Échec de la dépublication : {error}", + "Failed to export data": "Échec de l'export des données", + "Failed to export objects": "Échec de l'export des objets", + "Failed to fix mismatched SOLR fields: {error}": "Échec de la correction des champs SOLR non concordants : {error}", + "Failed to get response: {error}": "Échec de l'obtention de la réponse : {error}", + "Failed to leave organisation: {error}": "Échec du départ de l'organisation : {error}", + "Failed to link object": "Échec de la liaison de l'objet", + "Failed to load view: {error}": "Échec du chargement de la vue : {error}", + "Failed to publish register: {error}": "Échec de la publication du registre : {error}", + "Failed to publish schema: {error}": "Échec de la publication du schéma : {error}", + "Failed to publish: {error}": "Échec de la publication : {error}", + "Failed to remove avatar": "Échec de la suppression de l'avatar", + "Failed to remove link": "Échec de la suppression du lien", + "Failed to remove member: {error}": "Échec de la suppression du membre : {error}", + "Failed to remove schema {schema}: {error}": "Échec du retrait du schéma {schema} : {error}", + "Failed to remove user from organisation": "Échec de la suppression de l'utilisateur de l'organisation", + "Failed to request deactivation": "Échec de la demande de désactivation", + "Failed to revoke token": "Échec de la révocation du jeton", + "Failed to save configuration.": "Échec de l'enregistrement de la configuration.", + "Failed to save configuration: {error}": "Échec de l'enregistrement de la configuration : {error}", + "Failed to save facet configuration": "Échec de l'enregistrement de la configuration de la facette", + "Failed to save preferences": "Échec de l'enregistrement des préférences", + "Failed to save view: {error}": "Échec de l'enregistrement de la vue : {error}", + "Failed to set active organisation: {error}": "Échec de la définition de l'organisation active : {error}", + "Failed to start file warmup: {error}": "Échec du démarrage du préchauffage des fichiers : {error}", + "Failed to start vectorization: {error}": "Échec du démarrage de la vectorisation : {error}", + "Failed to switch organisation: {error}": "Échec du changement d'organisation : {error}", + "Failed to sync magic table for {schema}: {error}": "Échec de la synchronisation de la table magique pour {schema} : {error}", + "Failed to update schema configuration for {schema}: {error}": "Échec de la mise à jour de la configuration du schéma pour {schema} : {error}", + "Failed to update view: {error}": "Échec de la mise à jour de la vue : {error}", + "Failed to upload avatar": "Échec du téléversement de l'avatar", + "Failed to validate {schema}: {error}": "Échec de la validation de {schema} : {error}", + "Failed to vectorize files: {error}": "Échec de la vectorisation des fichiers : {error}", + "Failed to warmup names cache: {error}": "Échec du préchauffage du cache des noms : {error}", + "Federation": "Fédération", + "Fields to Display": "Champs à afficher", + "File #{id}": "Fichier n°{id}", + "File Handling": "Gestion des fichiers", + "File Location": "Emplacement du fichier", + "File name": "Nom du fichier", + "File Types to Process": "Types de fichiers à traiter", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vectorisation des fichiers terminée ! {vectorized} fragments vectorisés à partir de {files} fichiers. {failed} échec(s).", + "Files added successfully": "Fichiers ajoutés avec succès", + "Files → filecollection, objects → objectcollection": "Fichiers → filecollection, objets → objectcollection", + "file{plural}": "fichier{plural}", + "Filter by type": "Filtrer par type", + "Filter fields...": "Filtrer les champs...", + "folder": "dossier", + "From (e.g. -7 days or 2025-01-01)": "Du (p. ex. -7 days ou 2025-01-01)", + "Function Tools": "Outils de fonction", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Obtenez des informations en temps réel sur la santé des données de votre organisation, les tendances de croissance et les métriques de qualité.", + "GitHub Personal Access Token": "Jeton d'accès personnel GitHub", + "GitHub token is valid! Username: {username}": "Le jeton GitHub est valide ! Nom d'utilisateur : {username}", + "GitHub token test failed: {message}": "Le test du jeton GitHub a échoué : {message}", + "GitLab Personal Access Token": "Jeton d'accès personnel GitLab", + "GitLab token is valid! Username: {username}": "Le jeton GitLab est valide ! Nom d'utilisateur : {username}", + "GitLab token test failed: {message}": "Le test du jeton GitLab a échoué : {message}", + "Go to linked Register": "Aller au registre lié", + "Go to linked Schema": "Aller au schéma lié", + "Go to organisation": "Aller à l'organisation", + "Grant additional permissions beyond standard CRUD operations": "Accorder des permissions supplémentaires au-delà des opérations CRUD standard", + "Group Quota": "Quota de groupe", + "Groups": "Groupes", + "Here you can configure the details for various connections.": "Vous pouvez configurer ici les détails de diverses connexions.", + "Here you can configure the details for your organisation.": "Vous pouvez configurer ici les détails de votre organisation.", + "Hide": "Masquer", + "Hide in collection view": "Masquer dans la vue collection", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "IBANs": "IBAN", + "ID of input mapping (optional)": "ID du mappage d'entrée (facultatif)", + "ID of output mapping (optional)": "ID du mappage de sortie (facultatif)", + "ID of the target resource": "ID de la ressource cible", + "ID: {id}": "ID : {id}", + "Import Configuration": "Importer la configuration", + "Import Data into Register": "Importer des données dans le registre", + "in {register}": "dans {register}", + "in_progress": "en cours", + "Inactive organisations cannot be used": "Les organisations inactives ne peuvent pas être utilisées", + "Include ids and names of related objects for better contextual search": "Inclure les identifiants et les noms des objets liés pour une meilleure recherche contextuelle", + "Index": "Index", + "Information about the current OpenRegister installation": "Informations sur l'installation OpenRegister actuelle", + "inherited": "hérité", + "Inherited from register": "Hérité du registre", + "Inherits permissions from register": "Hérite des permissions du registre", + "Input Mapping": "Mappage d'entrée", + "Inspect SOLR Index": "Inspecter l'index SOLR", + "Internet location (URL) of this installation": "Emplacement Internet (URL) de cette installation", + "Interval": "Intervalle", + "Invite Users": "Inviter des utilisateurs", + "Invited Users": "Utilisateurs invités", + "Key": "Clé", + "Label": "Libellé", + "Labels": "Libellés", + "Leave empty to allow all users access": "Laissez vide pour autoriser l'accès à tous les utilisateurs", + "Leave off for auto-detection from property format.": "Laissez désactivé pour la détection automatique à partir du format de la propriété.", + "Left organisation successfully": "Vous avez quitté l'organisation avec succès", + "Link": "Lien", + "Link an object to see it here.": "Liez un objet pour le voir ici.", + "Link another object": "Lier un autre objet", + "Link removed": "Lien supprimé", + "Link to Object": "Lier à l'objet", + "Link to {name}": "Lier à {name}", + "Linked by {user}": "Lié par {user}", + "Linked Objects": "Objets liés", + "Linked to {name}": "Lié à {name}", + "list": "lister", + "Load more": "Charger plus", + "Loading activity...": "Chargement de l'activité...", + "Loading API tokens...": "Chargement des jetons d'API...", + "Loading available tools...": "Chargement des outils disponibles...", + "Loading cache statistics...": "Chargement des statistiques du cache...", + "Loading endpoints": "Chargement des points de terminaison", + "Loading entities...": "Chargement des entités...", + "Loading linked objects...": "Chargement des objets liés...", + "Loading LLM configuration...": "Chargement de la configuration LLM...", + "Loading multitenancy settings...": "Chargement des paramètres multi-locataires...", + "Loading n8n configuration...": "Chargement de la configuration n8n...", + "Loading permission matrix...": "Chargement de la matrice des permissions...", + "Loading preferences...": "Chargement des préférences...", + "Loading RBAC settings...": "Chargement des paramètres RBAC...", + "Loading register data...": "Chargement des données du registre...", + "Loading retention settings...": "Chargement des paramètres de conservation...", + "Loading search configuration...": "Chargement de la configuration de recherche...", + "Loading text extraction settings...": "Chargement des paramètres d'extraction de texte...", + "Loading tokens...": "Chargement des jetons...", + "Loading user groups...": "Chargement des groupes d'utilisateurs...", + "Location": "Emplacement", + "Location Field": "Champ d'emplacement", + "Locations": "Emplacements", + "log{plural}": "journal{plural}", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Les nombres inférieurs apparaissent en premier dans la barre latérale des filtres. Laissez vide pour un classement automatique.", + "Magic table sync completed for {schema}": "Synchronisation de la table magique terminée pour {schema}", + "Mail Integration": "Intégration de la messagerie", + "Make this installation discoverable within the federation network.": "Rendre cette installation détectable au sein du réseau de fédération.", + "manage": "gérer", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Gérez les collections SOLR (entrepôts de données) et attribuez-les aux objets et aux fichiers.", + "Manage SOLR configsets (configuration templates) for your collections.": "Gérez les configsets SOLR (modèles de configuration) pour vos collections.", + "Manage your account settings, security, and personal data.": "Gérez les paramètres de votre compte, la sécurité et vos données personnelles.", + "Manage your chat conversations": "Gérez vos conversations de chat", + "Manage your organisations and switch between them": "Gérez vos organisations et basculez entre elles", + "Mappings": "Mappages", + "Max Tokens": "Jetons maximum", + "Maximum File Size (MB)": "Taille maximale du fichier (Mo)", + "Maximum length": "Longueur maximale", + "Maximum number of items": "Nombre maximal d'éléments", + "Maximum value": "Valeur maximale", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Requêtes moyennes : recherches avec un certain filtrage ou plusieurs paramètres (p. ex. plages de dates, registres/schémas spécifiques)", + "Member removed successfully": "Membre supprimé avec succès", + "Members": "Membres", + "Members ({count})": "Membres ({count})", + "Metadata": "Métadonnées", + "Method*": "Méthode*", + "Method: {method}": "Méthode : {method}", + "Minimum length": "Longueur minimale", + "Minimum number of items": "Nombre minimal d'éléments", + "Minimum value": "Valeur minimale", + "Multiple of": "Multiple de", + "Multitenancy": "Multi-location", + "My Account": "Mon compte", + "myapp": "myapp", + "n8n connection test failed: {message}": "Le test de connexion n8n a échoué : {message}", + "n8n initialization failed: {message}": "L'initialisation de n8n a échoué : {message}", + "Name *": "Nom *", + "Name*": "Nom*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Cache des noms préchauffé avec succès : {count} noms chargés en {time}. {cache}", + "Naming pattern for copies:": "Modèle de nommage pour les copies :", + "New password": "Nouveau mot de passe", + "New User Group": "Nouveau groupe d'utilisateurs", + "Nextcloud Groups": "Groupes Nextcloud", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Aucun objet actif à supprimer temporairement pour le schéma {schema}. Utilisez « Supprimer définitivement » pour retirer les objets supprimés temporairement.", + "No activity found.": "Aucune activité trouvée.", + "No API tokens.": "Aucun jeton d'API.", + "No approval steps for this object.": "Aucune étape d'approbation pour cet objet.", + "No configuration selected": "Aucune configuration sélectionnée", + "No endpoints defined": "Aucun point de terminaison défini", + "No entities detected for this email.": "Aucune entité détectée pour cet e-mail.", + "No files selected": "Aucun fichier sélectionné", + "No groups found. Try a different search.": "Aucun groupe trouvé. Essayez une recherche différente.", + "No label": "Aucun libellé", + "No labels": "Aucun libellé", + "No linked objects": "Aucun objet lié", + "No objects are currently selected for deletion.": "Aucun objet n'est actuellement sélectionné pour la suppression.", + "No objects are currently selected for permanent deletion.": "Aucun objet n'est actuellement sélectionné pour la suppression définitive.", + "No objects are currently selected for restoration.": "Aucun objet n'est actuellement sélectionné pour la restauration.", + "No objects linked to this email": "Aucun objet lié à cet e-mail", + "No objects selected": "Aucun objet sélectionné", + "No objects selected for restoration": "Aucun objet sélectionné pour la restauration", + "No objects to delete for schema {schema}": "Aucun objet à supprimer pour le schéma {schema}", + "No organisations found": "Aucune organisation trouvée", + "No registers found. Create a register to configure permissions.": "Aucun registre trouvé. Créez un registre pour configurer les permissions.", + "No related cases found for this sender": "Aucun dossier associé trouvé pour cet expéditeur", + "No schemas configured for mail linking.": "Aucun schéma configuré pour la liaison de messagerie.", + "No synchronizations found": "Aucune synchronisation trouvée", + "No tools available. Tools can be registered by installed apps.": "Aucun outil disponible. Les outils peuvent être enregistrés par les applications installées.", + "No users in this organisation.": "Aucun utilisateur dans cette organisation.", + "Note:": "Remarque :", + "Notifications": "Notifications", + "Number of Sources": "Nombre de sources", + "Object #{id}": "Objet n°{id}", + "Object changes in owned objects": "Modifications dans les objets dont vous êtes propriétaire", + "Object Configuration:": "Configuration de l'objet :", + "Object Extraction Mode": "Mode d'extraction des objets", + "Object successfully modified": "Objet modifié avec succès", + "Object Text Extraction": "Extraction de texte des objets", + "Objects deletion completed for {schema}": "Suppression des objets terminée pour {schema}", + "Objects will be soft deleted and moved to the": "Les objets seront supprimés temporairement et déplacés vers la", + "objectType: object\naction: created": "objectType: object\naction: created", + "object{plural}": "objet{plural}", + "Only members of selected groups can access this organisation": "Seuls les membres des groupes sélectionnés peuvent accéder à cette organisation", + "Open folder": "Ouvrir le dossier", + "Open in OpenRegister": "Ouvrir dans OpenRegister", + "Open OpenRegister sidebar": "Ouvrir la barre latérale d'OpenRegister", + "OpenRegister": "OpenRegister", + "Optional description for this facet": "Description facultative pour cette facette", + "Optional end date property": "Propriété de date de fin facultative", + "Optional event description template": "Modèle de description d'événement facultatif", + "Optional location property": "Propriété d'emplacement facultative", + "Optional URL-friendly identifier": "Identifiant compatible avec les URL facultatif", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Facultatif : ajoutez des paramètres de requête pour filtrer le schéma référencé (p. ex. status=active&type=public)", + "Or": "Ou", + "Organisation Configuration": "Configuration de l'organisation", + "Organisation membership changes": "Modifications de l'appartenance à l'organisation", + "Organisation name is required": "Le nom de l'organisation est requis", + "Organisation successfully updated": "Organisation mise à jour avec succès", + "Organizations": "Organisations", + "Other": "Autre", + "Output Mapping": "Mappage de sortie", + "Overview": "Aperçu", + "Page {current} of {total}": "Page {current} sur {total}", + "Password changes are not supported by your authentication provider.": "Les modifications de mot de passe ne sont pas prises en charge par votre fournisseur d'authentification.", + "Password updated successfully": "Mot de passe mis à jour avec succès", + "pending": "en attente", + "Permanently Delete ({count})": "Supprimer définitivement ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Supprimer définitivement les {count} objets supprimés temporairement. Cette action est irréversible !", + "Permission Matrix": "Matrice des permissions", + "Personal Data Export": "Export des données personnelles", + "Persons": "Personnes", + "Phone Numbers": "Numéros de téléphone", + "Please select a user": "Veuillez sélectionner un utilisateur", + "Please select an organisation": "Veuillez sélectionner une organisation", + "Please select at least one item to import": "Veuillez sélectionner au moins un élément à importer", + "Please select registers or schemas first": "Veuillez d'abord sélectionner des registres ou des schémas", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Veuillez patienter pendant que nous supprimons définitivement la collection SOLR. Cela peut prendre quelques instants.", + "Preferences saved": "Préférences enregistrées", + "Press enter to send, shift+enter for new line": "Appuyez sur Entrée pour envoyer, Maj+Entrée pour un saut de ligne", + "Prettify": "Embellir", + "Preview:": "Aperçu :", + "Private Agent (Default)": "Agent privé (par défaut)", + "Private agents": "Agents privés", + "Process Name (optional)": "Nom du processus (facultatif)", + "Processed {indexed} files, {failed} failed": "{indexed} fichiers traités, {failed} échec(s)", + "Property could not be updated": "La propriété n'a pas pu être mise à jour", + "Property name of inversed relation": "Nom de la propriété de la relation inversée", + "Property successfully updated": "Propriété mise à jour avec succès", + "Publish Configuration to GitHub": "Publier la configuration sur GitHub", + "Publish OAS": "Publier l'OAS", + "Publish objects to make them publicly available": "Publier les objets pour les rendre accessibles au public", + "Publish Register OAS to GitHub": "Publier l'OAS du registre sur GitHub", + "Query Help": "Aide pour les requêtes", + "Quota": "Quota", + "RBAC": "RBAC", + "read": "lire", + "Reason": "Raison", + "Reason (optional)": "Raison (facultatif)", + "Register #{id}": "Registre n°{id}", + "Register / Schema": "Registre / Schéma", + "Register and schema are required": "Le registre et le schéma sont requis", + "Register Overview": "Aperçu du registre", + "Register:": "Registre :", + "register{plural}": "registre{plural}", + "Reindex completed! Processed {count} objects in {duration}s": "Réindexation terminée ! {count} objets traités en {duration} s", + "Reject": "Rejeter", + "Rejected": "Rejeté", + "rejected": "rejeté", + "Related Cases": "Dossiers associés", + "Remove avatar": "Supprimer l'avatar", + "Remove from list": "Retirer de la liste", + "Remove link": "Supprimer le lien", + "Remove link to {name}": "Supprimer le lien vers {name}", + "Remove link to {name}?": "Supprimer le lien vers {name} ?", + "Remove link to {title}": "Supprimer le lien vers {title}", + "Remove oneOf entry": "Supprimer l'entrée oneOf", + "Remove property after write-back": "Supprimer la propriété après la réécriture", + "Remove {title}": "Supprimer {title}", + "Remove {userId} from this organisation?": "Retirer {userId} de cette organisation ?", + "Removing schema {schema}...": "Retrait du schéma {schema}...", + "Request account deactivation": "Demander la désactivation du compte", + "Request account deactivation. This will notify administrators for review.": "Demandez la désactivation du compte. Cela notifiera les administrateurs pour examen.", + "Request Quota (per day)": "Quota de requêtes (par jour)", + "Requested at": "Demandé le", + "Required": "Requis", + "Required Token Scopes": "Portées de jeton requises", + "Requires Dolphin AI for OCR text extraction": "Nécessite Dolphin AI pour l'extraction de texte par OCR", + "Rerunning search: {searchTerm}": "Relance de la recherche : {searchTerm}", + "Resource Quotas": "Quotas de ressources", + "Retry upload": "Réessayer le téléversement", + "Review the selected objects below. You can remove any objects you don't want to include.": "Examinez les objets sélectionnés ci-dessous. Vous pouvez retirer tout objet que vous ne souhaitez pas inclure.", + "Revoke": "Révoquer", + "Right": "Droite", + "Roles and Permissions": "Rôles et permissions", + "Save labels": "Enregistrer les libellés", + "Save tags for {name}": "Enregistrer les étiquettes de {name}", + "Save the organisation first to manage users.": "Enregistrez d'abord l'organisation pour gérer les utilisateurs.", + "Schema configuration updated successfully for {schema}": "Configuration du schéma mise à jour avec succès pour {schema}", + "Schema reference ($ref)": "Référence de schéma ($ref)", + "Schema {schema} removed successfully": "Schéma {schema} retiré avec succès", + "Schema:": "Schéma :", + "schema{plural}": "schéma{plural}", + "Search": "Rechercher", + "Search / views": "Recherche / vues", + "Search agents...": "Rechercher des agents...", + "Search applications...": "Rechercher des applications...", + "Search by title or UUID...": "Rechercher par titre ou UUID...", + "Search Configuration": "Configuration de la recherche", + "Search configurations": "Rechercher des configurations", + "Search data sources...": "Rechercher des sources de données...", + "Search groups...": "Rechercher des groupes...", + "Search in Database Objects": "Rechercher dans les objets de la base de données", + "Search Mode": "Mode de recherche", + "Search objects": "Rechercher des objets", + "Search objects...": "Rechercher des objets...", + "Search Query": "Requête de recherche", + "Search registers...": "Rechercher des registres...", + "Search Results": "Résultats de la recherche", + "Search schemas...": "Rechercher des schémas...", + "Search {name}...": "Rechercher {name}...", + "Searching...": "Recherche en cours...", + "Security": "Sécurité", + "See organisations as a table": "Voir les organisations sous forme de tableau", + "See organisations as cards": "Voir les organisations sous forme de cartes", + "Select a date property": "Sélectionnez une propriété de date", + "Select a register": "Sélectionnez un registre", + "Select a register...": "Sélectionnez un registre...", + "Select a schema": "Sélectionnez un schéma", + "Select a schema...": "Sélectionnez un schéma...", + "Select a user": "Sélectionnez un utilisateur", + "Select Active Organisation": "Sélectionner l'organisation active", + "Select agent type": "Sélectionnez le type d'agent", + "Select an email to see linked objects": "Sélectionnez un e-mail pour voir les objets liés", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Sélectionnez une organisation et un utilisateur pour l'ajouter en tant que membre. Recherchez les organisations par nom.", + "Select configuration type...": "Sélectionnez le type de configuration...", + "Select fields to display": "Sélectionnez les champs à afficher", + "Select files with the correct extension": "Sélectionnez les fichiers avec la bonne extension", + "Select group": "Sélectionnez un groupe", + "Select groups (optional)": "Sélectionnez des groupes (facultatif)", + "Select groups to notify...": "Sélectionnez les groupes à notifier...", + "Select groups with access to this agent": "Sélectionnez les groupes ayant accès à cet agent", + "Select groups...": "Sélectionnez des groupes...", + "Select one or more registers": "Sélectionnez un ou plusieurs registres", + "Select one or more schemas": "Sélectionnez un ou plusieurs schémas", + "Select or create labels, or select \"No label\" to add files": "Sélectionnez ou créez des libellés, ou sélectionnez « Aucun libellé » pour ajouter des fichiers", + "Select owner application (optional)...": "Sélectionnez l'application propriétaire (facultatif)...", + "Select role": "Sélectionnez un rôle", + "Select schemas (empty = all schemas)": "Sélectionnez les schémas (vide = tous les schémas)", + "Select schemas to warm up": "Sélectionnez les schémas à préchauffer", + "Select search mode": "Sélectionnez le mode de recherche", + "Select source type...": "Sélectionnez le type de source...", + "Select target type": "Sélectionnez le type de cible", + "Select views to filter data (optional)": "Sélectionnez les vues pour filtrer les données (facultatif)", + "Select views to limit which data the agent can access": "Sélectionnez les vues pour limiter les données auxquelles l'agent peut accéder", + "Select {fieldName} values": "Sélectionnez les valeurs de {fieldName}", + "Selected Objects ({count})": "Objets sélectionnés ({count})", + "Send as cloudevent": "Envoyer en tant que cloudevent", + "Set as Active": "Définir comme actif", + "Set as active organisation": "Définir comme organisation active", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Définissez des limites pour l'utilisation de l'API et la consommation de jetons. Utilisez 0 pour des ressources illimitées.", + "Showing {showing} of {total} agents": "Affichage de {showing} sur {total} agents", + "Showing {showing} of {total} applications": "Affichage de {showing} sur {total} applications", + "Showing {showing} of {total} audit trail entries": "Affichage de {showing} sur {total} entrées du journal d'audit", + "Showing {showing} of {total} configurations": "Affichage de {showing} sur {total} configurations", + "Showing {showing} of {total} deleted items": "Affichage de {showing} sur {total} éléments supprimés", + "Showing {showing} of {total} entities": "Affichage de {showing} sur {total} entités", + "Showing {showing} of {total} files": "Affichage de {showing} sur {total} fichiers", + "Showing {showing} of {total} log entries": "Affichage de {showing} sur {total} entrées de journal", + "Showing {showing} of {total} search trail entries": "Affichage de {showing} sur {total} entrées du journal de recherche", + "Showing {showing} of {total} sources": "Affichage de {showing} sur {total} sources", + "Showing {showing} of {total} templates": "Affichage de {showing} sur {total} modèles", + "Showing {showing} of {total} webhooks": "Affichage de {showing} sur {total} webhooks", + "Showing {shown} of {total} organisations": "Affichage de {shown} sur {total} organisations", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Requêtes simples : recherches de texte de base avec des paramètres minimaux (p. ex. terme de recherche unique, sans filtres avancés)", + "SOLR collection deleted successfully": "Collection SOLR supprimée avec succès", + "SOLR Index Warmup": "Préchauffage de l'index SOLR", + "Something went wrong while adding files": "Une erreur s'est produite lors de l'ajout des fichiers", + "Source successfully updated": "Source mise à jour avec succès", + "Special Rights": "Droits spéciaux", + "Start Date Field": "Champ de date de début", + "Starting validation for {schema}...": "Démarrage de la validation pour {schema}...", + "Starting {action} for {schema}...": "Démarrage de {action} pour {schema}...", + "Statistics and Metrics": "Statistiques et métriques", + "Statistics refreshed": "Statistiques actualisées", + "Step": "Étape", + "Storage": "Stockage", + "Storage Quota (MB)": "Quota de stockage (Mo)", + "Successfully added user to organisation: {name}": "Utilisateur ajouté avec succès à l'organisation : {name}", + "Successfully created {count} SOLR fields": "{count} champs SOLR créés avec succès", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "{count} embeddings supprimés avec succès. Veuillez revectoriser vos données.", + "Successfully deleted {count} objects for {schema}": "{count} objets supprimés avec succès pour {schema}", + "Successfully fixed {count} SOLR field configurations": "{count} configurations de champs SOLR corrigées avec succès", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importé avec succès : {registers} registres, {schemas} schémas, {objects} objets", + "Successfully permanently deleted {count} objects": "{count} objets supprimés définitivement avec succès", + "Successfully processed {count} files!": "{count} fichiers traités avec succès !", + "Successfully saved configuration for {count} facets!": "Configuration enregistrée avec succès pour {count} facettes !", + "Supported File Types": "Types de fichiers pris en charge", + "Switch Active Organisation": "Changer d'organisation active", + "Switch Organisation": "Changer d'organisation", + "Syncing magic table for {schema}...": "Synchronisation de la table magique pour {schema}...", + "Syncs": "Synchronisations", + "System announcements": "Annonces du système", + "System Prompt": "Invite système", + "Table": "Tableau", + "Target ID": "ID de la cible", + "Target Type*": "Type de cible*", + "Text Chunk #{id}": "Fragment de texte n°{id}", + "Text Extraction": "Extraction de texte", + "Text Extraction Engine": "Moteur d'extraction de texte", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Ils seront conservés selon la période de conservation configurée de leur schéma, puis retirés définitivement.", + "This action cannot be undone. Make sure no collections are using this configset.": "Cette action est irréversible. Assurez-vous qu'aucune collection n'utilise ce configset.", + "This action will submit a deactivation request to your administrators.": "Cette action soumettra une demande de désactivation à vos administrateurs.", + "This is not valid JSON": "Ce n'est pas un JSON valide", + "This key already exists on this schema": "Cette clé existe déjà dans ce schéma", + "This schema must use magic table configuration to sync": "Ce schéma doit utiliser la configuration de table magique pour se synchroniser", + "This token will only be shown once. Copy it now.": "Ce jeton ne sera affiché qu'une seule fois. Copiez-le maintenant.", + "This will copy the _default configset with the new name": "Cela copiera le configset _default avec le nouveau nom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Cela supprimera définitivement TOUS les embeddings (vecteurs) de la base de données. Vous devrez revectoriser tous les objets et fichiers. Cette action est irréversible.\n\nÊtes-vous sûr de vouloir continuer ?", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Cela retirera définitivement {count} objets déjà supprimés temporairement de la base de données. Cette action est irréversible.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Astuce : n'activez que les vues nécessitant une recherche sémantique pour minimiser les coûts d'embedding. Les simples tables de correspondance ont rarement besoin de vectorisation.", + "Title *": "Titre *", + "Title Template": "Modèle de titre", + "Title*": "Titre*", + "To (e.g. -1 year or 2025-12-31)": "Au (p. ex. -1 year ou 2025-12-31)", + "To add files larger than or equal to 512MB, go to the": "Pour ajouter des fichiers de taille supérieure ou égale à 512 Mo, accédez à", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Pour ajuster la taille des fragments ou la stratégie, accédez à la configuration des fichiers → limites de traitement.", + "To change the embedding provider or model, go to LLM configuration.": "Pour changer de fournisseur ou de modèle d'embedding, accédez à la configuration LLM.", + "Token copied to clipboard": "Jeton copié dans le presse-papiers", + "Token Created": "Jeton créé", + "Token name": "Nom du jeton", + "Token Quota (per request)": "Quota de jetons (par requête)", + "Token revoked": "Jeton révoqué", + "Tools respect the agent's own role and organization boundaries.": "Les outils respectent le rôle propre de l'agent et les limites de l'organisation.", + "Try searching by UUID or with different keywords": "Essayez de rechercher par UUID ou avec des mots-clés différents", + "Type \"DELETE COLLECTION\" to confirm:": "Saisissez « SUPPRIMER LA COLLECTION » pour confirmer :", + "Type to search for objects...": "Saisissez pour rechercher des objets...", + "Type to search for organisations": "Saisissez pour rechercher des organisations", + "Type to search...": "Saisissez pour rechercher...", + "Type your username to confirm": "Saisissez votre nom d'utilisateur pour confirmer", + "Unknown Object": "Objet inconnu", + "Unnamed Object": "Objet sans nom", + "update": "mettre à jour", + "Update available: {local} → {remote}": "Mise à jour disponible : {local} → {remote}", + "Upload new avatar": "Téléverser un nouvel avatar", + "Url": "Url", + "Use AI agents for processing and analysis": "Utiliser des agents IA pour le traitement et l'analyse", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Utiliser les plages par défaut (7/30/90 derniers jours, l'année dernière, plus ancien)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Utiliser un stockage externe (p. ex. MongoDB) au lieu du stockage interne de Nextcloud.", + "Use Large Language Model features": "Utiliser les fonctionnalités des grands modèles de langage", + "Use SolrCloud with zookeeper for distributed search": "Utiliser SolrCloud avec Zookeeper pour la recherche distribuée", + "Use VNG APIs instead of MongoDB.": "Utiliser les API VNG au lieu de MongoDB.", + "Used by": "Utilisé par", + "User Quota": "Quota d'utilisateur", + "Uses": "Utilisations", + "UUID copied to clipboard": "UUID copié dans le presse-papiers", + "Validation completed for {schema}": "Validation terminée pour {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validation terminée pour {schema} : {processed} traités, {updated} mis à jour, {failed} échec(s)", + "Value list (split on ,)": "Liste de valeurs (séparées par ,)", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vectorisation terminée : {vectorized} objets vectorisés, {failed} échec(s)", + "Vectorization failed: {error}": "La vectorisation a échoué : {error}", + "Version Information": "Informations sur la version", + "View": "Vue", + "view": "voir", + "View and manage authorization across registers and schemas": "Consultez et gérez l'autorisation sur l'ensemble des registres et des schémas", + "View API Documentation": "Voir la documentation de l'API", + "View APIs": "Voir les API", + "View object": "Voir l'objet", + "View {count} more": "Voir {count} de plus", + "Visible to end users": "Visible par les utilisateurs finaux", + "waiting": "en attente", + "Warmup failed": "Le préchauffage a échoué", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Lorsque l'agent s'exécute sans session utilisateur (p. ex. tâches planifiées), le contexte de cet utilisateur sera utilisé.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Lorsqu'elle est désactivée, cette facette n'affichera que les valeurs de ce schéma et inclura un filtre de schéma lorsqu'elle est sélectionnée.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Lorsqu'elle est activée, l'enregistrement de cet objet mettra également à jour les objets cibles pour inclure une référence vers cet objet.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Lorsqu'elle est activée, cette propriété sera supprimée de l'objet source après la mise à jour des objets cibles.", + "Workflow Management": "Gestion des flux de travail", + "Wrap webhook payload in cloudevents format for better interoperability": "Encapsuler la charge utile du webhook au format cloudevents pour une meilleure interopérabilité", + "You are not yet a member of any organisations.": "Vous n'êtes encore membre d'aucune organisation.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Vous pouvez créer de nouveaux configsets basés sur le modèle _default, ou en téléverser des personnalisés directement sur votre serveur SOLR.", + "You do not have permission to view the permission matrix. Admin access is required.": "Vous n'êtes pas autorisé à consulter la matrice des permissions. Un accès administrateur est requis.", + "You have selected {count} properties to add to the schema.": "Vous avez sélectionné {count} propriétés à ajouter au schéma.", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Votre retour a été enregistré. Vous pouvez éventuellement fournir des détails supplémentaires ici...", + "{active} active, {deleted} deleted": "{active} actif(s), {deleted} supprimé(s)", + "{count} audit trails cleared successfully": "{count} journaux d'audit effacés avec succès", + "{count} deletions": "{count} suppressions", + "{count} documents found": "{count} documents trouvés", + "{count} entries": "{count} entrées", + "{count} issues": "{count} problèmes", + "{count} members": "{count} membres", + "{count} searches": "{count} recherches", + "{count} selected": "{count} sélectionné(s)", + "{minutes} minutes ago": "il y a {minutes} minutes", + "{percentage}% of objects": "{percentage} % des objets", + "{property} - {other}": "{property} - {other}", + "ℹ️ current configuration": "ℹ️ configuration actuelle", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ DANGER : Tous les {total} objets seront SUPPRIMÉS DÉFINITIVEMENT de la base de données. Cette action est IRRÉCUPÉRABLE et irréversible !", + "{count} compliance issue(s) detected": "{count} problème(s) de conformité détecté(s)", + "{count} email": "{count} e-mail", + "{count} emails": "{count} e-mails", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} objet(s) correspondant(s). {erased} effacé(s).", + "{type} cache cleared successfully": "Cache {type} effacé avec succès", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVERTISSEMENT DE SUPPRESSION DÉFINITIVE \\u26A0\\uFE0F\n\nVous êtes sur le point de supprimer DÉFINITIVEMENT {count} objets supprimés temporairement du schéma \"{schema}\".\n\nCes objets seront complètement retirés de la base de données et NE POURRONT PAS être récupérés.\n\nÊtes-vous absolument sûr ?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVERTISSEMENT DE SUPPRESSION DÉFINITIVE \\u26A0\\uFE0F\n\nVous êtes sur le point de supprimer DÉFINITIVEMENT TOUS les objets du schéma \"{schema}\" :\n\n\\u2022 Objets actifs : {active}\n\\u2022 Objets supprimés temporairement : {deleted}\n\\u2022 Total : {total}\n\nCes objets seront complètement retirés de la base de données et NE POURRONT PAS être récupérés.\n\nÊtes-vous absolument sûr ?", + "Add a contact from any of your address books to associate it with this object.": "Ajoutez un contact depuis l'un de vos carnets d'adresses pour l'associer à cet objet.", + "Agent successfully created": "Agent créé avec succès", + "and accessible to all users in selected groups (or all users if no groups selected).": "et accessible à tous les utilisateurs des groupes sélectionnés (ou à tous les utilisateurs si aucun groupe n'est sélectionné).", + "App store cache invalidated: {invalidated}": "Cache de l'App Store invalidé : {invalidated}", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Archiver cette verwerkingsactiviteit ? Les lignes du journal d'audit continueront d'y faire référence.", + "are only accessible to invited users.": "ne sont accessibles qu'aux utilisateurs invités.", + "Attachment added to {name}": "Pièce jointe ajoutée à {name}", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4 : ce rapport associe chaque activité de traitement aux décomptes cumulés du journal d'audit qui lui sont attribués. Les auditeurs et l'Autoriteit Persoonsgegevens l'utilisent pour le contrôle de supervision.", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (durée ISO-8601, p. ex. P10Y, P30D)", + "by": "par", + "Calendar integration is not available": "L'intégration de l'agenda n'est pas disponible", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Cliquez sur Lancer l'analyse de conformité pour trouver les schémas où des données personnelles ont été détectées mais où aucune annotation d'activité de traitement n'existe.", + "Click to subscribe to notifications": "Cliquez pour vous abonner aux notifications", + "Click to unsubscribe from notifications": "Cliquez pour vous désabonner des notifications", + "Code (short readable key, e.g. v-2026-001)": "Code (clé courte et lisible, p. ex. v-2026-001)", + "Contacts integration is not available": "L'intégration des contacts n'est pas disponible", + "Create a new event or link an existing one from any of your calendars.": "Créez un nouvel événement ou liez-en un existant depuis l'un de vos agendas.", + "Create or link a Deck card to track work on this object.": "Créez ou liez une carte Deck pour suivre le travail sur cet objet.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Créez la première verwerkingsactiviteit pour commencer à étiqueter les lignes du journal d'audit avec leur attribution AVG Art 30.", + "CSV": "CSV", + "Dashboard not found": "Tableau de bord introuvable", + "Data-subject access request": "Demande d'accès de la personne concernée", + "Deck": "Deck", + "Deck integration is not available": "L'intégration de Deck n'est pas disponible", + "Delete {count} object": "Supprimer {count} objet", + "Delete {count} objects": "Supprimer {count} objets", + "Doelbinding *": "Doelbinding *", + "DSAR": "DSAR", + "Edit verwerkingsactiviteit": "Modifier la verwerkingsactiviteit", + "Erase (Art 17)": "Effacer (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Effacer {count} objet(s) pour cette personne concernée ? Cette action est enregistrée dans le journal d'audit.", + "Every schema with detected PII has a processing-activity annotation.": "Chaque schéma avec des données personnelles détectées possède une annotation d'activité de traitement.", + "Excel (.xlsx)": "Excel (.xlsx)", + "Failed to add attachment to object": "Échec de l'ajout de la pièce jointe à l'objet", + "Failed to clear {type} cache: {error}": "Échec de l'effacement du cache {type} : {error}", + "Failed to clear audit trails: {error}": "Échec de l'effacement des journaux d'audit : {error}", + "Failed to clear blob objects: {error}": "Échec de l'effacement des objets blob : {error}", + "Failed to clear cache: {error}": "Échec de l'effacement du cache : {error}", + "Failed to clear search trails: {error}": "Échec de l'effacement des journaux de recherche : {error}", + "Failed to discover files: {error}": "Échec de la découverte des fichiers : {error}", + "Failed to invalidate app store cache: {error}": "Échec de l'invalidation du cache de l'App Store : {error}", + "Failed to load cache statistics: {error}": "Échec du chargement des statistiques du cache : {error}", + "Failed to load Deck cards": "Échec du chargement des cartes Deck", + "Failed to load linked contacts": "Échec du chargement des contacts liés", + "Failed to load linked emails": "Échec du chargement des e-mails liés", + "Failed to load linked events": "Échec du chargement des événements liés", + "Failed to load related entities": "Échec du chargement des entités associées", + "Failed to load settings: {error}": "Échec du chargement des paramètres : {error}", + "Failed to load statistics: {error}": "Échec du chargement des statistiques : {error}", + "Failed to save file settings: {error}": "Échec de l'enregistrement des paramètres de fichier : {error}", + "Failed to save LLM settings: {error}": "Échec de l'enregistrement des paramètres LLM : {error}", + "Failed to save settings: {error}": "Échec de l'enregistrement des paramètres : {error}", + "Failed to save warmup interval: {error}": "Échec de l'enregistrement de l'intervalle de préchauffage : {error}", + "Failed to setup SOLR: {error}": "Échec de la configuration de SOLR : {error}", + "Failed to start processing {type} files: {error}": "Échec du démarrage du traitement des fichiers {type} : {error}", + "Failed to test SOLR connection: {error}": "Échec du test de connexion SOLR : {error}", + "Failed to update LLM settings: {error}": "Échec de la mise à jour des paramètres LLM : {error}", + "Failed to update Multitenancy settings: {error}": "Échec de la mise à jour des paramètres multi-locataires : {error}", + "Failed to update RBAC settings: {error}": "Échec de la mise à jour des paramètres RBAC : {error}", + "Failed to update Retention settings: {error}": "Échec de la mise à jour des paramètres de conservation : {error}", + "Failed to update SOLR settings: {error}": "Échec de la mise à jour des paramètres SOLR : {error}", + "File settings saved successfully": "Paramètres de fichier enregistrés avec succès", + "Filter and search entities": "Filtrer et rechercher des entités", + "Generate report": "Générer le rapport", + "Generate the verantwoordingsdocument": "Générer le verantwoordingsdocument", + "Generated: {time}": "Généré : {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Obtenez des informations en temps réel sur la santé des données de votre organisation en vous concentrant sur les registres, les définitions de schémas, ainsi que le stockage et l'utilisation des objets.", + "HTML preview": "Aperçu HTML", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importez le modèle report-bundle.json pour obtenir le registre `reports`, puis créez votre premier tableau de bord via l'interface standard des objets. Les tableaux de bord déclarent leurs widgets en JSON et le moteur de rendu alimente chaque widget avec des données d'agrégation en direct.", + "Integrations": "Intégrations", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Résultats de l'inzage", + "Languages": "Langues", + "Link an email from the Mail app sidebar to associate it with this object.": "Liez un e-mail depuis la barre latérale de l'application Mail pour l'associer à cet objet.", + "Link existing event": "Lier un événement existant", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Les e-mails liés, les événements d'agenda, les contacts et les cartes Deck apparaîtront ici.", + "LLM settings saved successfully": "Paramètres LLM enregistrés avec succès", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Localisez chaque objet référençant une personne concernée (Art 15 inzage), prévisualisez un effacement (Art 17 vergetelheid) ou exportez ses données (Art 20 portabiliteit).", + "Mail integration is not available": "L'intégration de la messagerie n'est pas disponible", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Gérez les activités de traitement, exécutez les demandes d'accès des personnes concernées et auditez la conformité au titre du RGPD de l'UE / de l'AVG néerlandais.", + "Mass validation completed successfully": "Validation en masse terminée avec succès", + "Mass validation failed: {error}": "La validation en masse a échoué : {error}", + "missing": "manquant", + "Multitenancy settings updated successfully": "Paramètres multi-locataires mis à jour avec succès", + "Naam": "Naam", + "Naam *": "Naam *", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Cache des noms préchauffé avec succès : {count} noms chargés en {time}. {message}", + "New activity": "Nouvelle activité", + "New Configuration": "Nouvelle configuration", + "New verwerkingsactiviteit": "Nouvelle verwerkingsactiviteit", + "No contacts linked to this object": "Aucun contact lié à cet objet", + "No dashboards yet": "Aucun tableau de bord pour le moment", + "No Deck cards linked to this object": "Aucune carte Deck liée à cet objet", + "No emails linked to this object": "Aucun e-mail lié à cet objet", + "No events linked to this object": "Aucun événement lié à cet objet", + "No matches": "Aucune correspondance", + "No personal data was found for this subject identifier.": "Aucune donnée personnelle n'a été trouvée pour cet identifiant de personne concernée.", + "No processing activities yet": "Aucune activité de traitement pour le moment", + "No related entities": "Aucune entité associée", + "No saved views yet. create one in the search tab!": "Aucune vue enregistrée pour le moment. Créez-en une dans l'onglet de recherche !", + "No scan run yet": "Aucune analyse exécutée pour le moment", + "Object metadata incomplete for file upload": "Métadonnées de l'objet incomplètes pour le téléversement du fichier", + "Object successfully deleted": "Objet supprimé avec succès", + "Objects successfully deleted": "Objets supprimés avec succès", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Tableaux de bord définis par l'opérateur et rapports planifiés. Chaque tableau de bord est un objet de première classe dans le registre `reports` ; les widgets sont déclarés dans le tableau `widgets` du tableau de bord et rendus en direct à partir des agrégations / GraphQL.", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Balises de langue BCP 47 ordonnées. La première langue est celle par défaut du registre et détermine le repli Accept-Language pour les propriétés traduisibles.", + "Organisation successfully created": "Organisation créée avec succès", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "PDF": "PDF", + "PII hits": "Occurrences de données personnelles", + "PII rows": "Lignes de données personnelles", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preview erasure": "Prévisualiser l'effacement", + "Property could not be added": "La propriété n'a pas pu être ajoutée", + "Property successfully added": "Propriété ajoutée avec succès", + "public": "public", + "Purge {count} object from database": "Purger {count} objet de la base de données", + "Purge {count} objects from database": "Purger {count} objets de la base de données", + "RBAC settings updated successfully": "Paramètres RBAC mis à jour avec succès", + "Rebase operation completed successfully": "Opération de rebasage terminée avec succès", + "Rebase operation failed: {error}": "L'opération de rebasage a échoué : {error}", + "Rechtsgrond": "Rechtsgrond", + "Register annotation": "Annotation du registre", + "Register objects": "Objets du registre", + "Remove contact": "Supprimer le contact", + "Remove Deck card": "Supprimer la carte Deck", + "Reports": "Rapports", + "Restore {count} object": "Restaurer {count} objet", + "Restore {count} objects": "Restaurer {count} objets", + "Retention settings updated successfully": "Paramètres de conservation mis à jour avec succès", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Examinez les objets sélectionnés ci-dessous. Vous pouvez retirer tout objet que vous ne souhaitez pas supprimer en cliquant sur le bouton de suppression.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Examinez les objets sélectionnés ci-dessous. Vous pouvez retirer tout objet que vous ne souhaitez pas supprimer définitivement en cliquant sur le bouton de suppression. Cette action est irréversible.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Examinez les objets sélectionnés ci-dessous. Vous pouvez retirer tout objet que vous ne souhaitez pas restaurer en cliquant sur le bouton de suppression. Les objets seront restaurés à leur emplacement d'origine.", + "Run compliance scan": "Lancer l'analyse de conformité", + "Scheduled": "Planifié", + "Schema annotation": "Annotation du schéma", + "selected objects": "objets sélectionnés", + "set": "définir", + "Settings updated successfully": "Paramètres mis à jour avec succès", + "Show": "Afficher", + "Showing {count} dashboard(s)": "Affichage de {count} tableau(x) de bord", + "Showing {count} processing activities": "Affichage de {count} activités de traitement", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Supprimer temporairement tous les objets de ce schéma ({active} actif(s), {deleted} déjà supprimé(s))", + "SOLR connection test failed: {error}": "Le test de connexion SOLR a échoué : {error}", + "SOLR connection test successful!": "Test de connexion SOLR réussi !", + "SOLR index warmup completed successfully": "Préchauffage de l'index SOLR terminé avec succès", + "SOLR settings updated successfully": "Paramètres SOLR mis à jour avec succès", + "SOLR setup completed successfully!": "Configuration de SOLR terminée avec succès !", + "SOLR setup failed: {error}": "La configuration de SOLR a échoué : {error}", + "SOLR warmup failed: {error}": "Le préchauffage de SOLR a échoué : {error}", + "Subject identifier (email, BSN, name, etc.)": "Identifiant de la personne concernée (e-mail, BSN, nom, etc.)", + "Subscribe": "S'abonner", + "Subscribed": "Abonné", + "Successfully cleared {count} audit trails": "{count} journaux d'audit effacés avec succès", + "Successfully cleared {count} blob storage objects": "{count} objets de stockage blob effacés avec succès", + "Successfully cleared {count} search trails": "{count} journaux de recherche effacés avec succès", + "Successfully restored {count} object": "{count} objet restauré avec succès", + "Successfully restored {count} objects": "{count} objets restaurés avec succès", + "Technical Details": "Détails techniques", + "Technische maatregelen": "Technische maatregelen", + "The Nextcloud Calendar app is not installed or enabled on this server.": "L'application Agenda de Nextcloud n'est pas installée ou activée sur ce serveur.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "L'application Contacts de Nextcloud n'est pas installée ou activée sur ce serveur.", + "The Nextcloud Deck app is not installed or enabled on this server.": "L'application Deck de Nextcloud n'est pas installée ou activée sur ce serveur.", + "The Nextcloud Mail app is not installed or enabled on this server.": "L'application Mail de Nextcloud n'est pas installée ou activée sur ce serveur.", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Le tableau de bord demandé n'a pas pu être chargé. Vérifiez l'URL ou choisissez un autre tableau de bord dans la liste.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Ils seront conservés selon la période de conservation configurée de leur schéma et automatiquement supprimés définitivement à l'expiration de la période de conservation. La période de conservation est configurable par schéma et se trouve dans les paramètres du schéma.", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Les outils s'exécutent avec les permissions de l'utilisateur par défaut de l'agent lorsqu'aucune session utilisateur n'est active (p. ex. tâches cron). Configurez l'utilisateur par défaut dans l'onglet Paramètres.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Les outils respectent les vues, les permissions et les limites de l'organisation de l'agent.", + "Total events": "Total des événements", + "Type filter (optional, e.g. email)": "Filtre de type (facultatif, p. ex. email)", + "Unknown widget type: {type}": "Type de widget inconnu : {type}", + "Unlink email": "Délier l'e-mail", + "Unlink event": "Délier l'événement", + "Untitled": "Sans titre", + "Use filters to narrow down entities by type or category.": "Utilisez les filtres pour affiner les entités par type ou catégorie.", + "Verantwoording": "Verantwoording", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Avertissement : cette propriété a des types incohérents : {types}. Vérifiez si tous les objets doivent avoir le même type.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Nous analyserons tous les objets appartenant à ce schéma afin de découvrir de nouvelles propriétés et d'analyser les propriétés existantes en vue d'améliorations potentielles. Le processus consiste à examiner la structure de données de chaque objet, à identifier les propriétés non définies dans le schéma actuel et à trouver des opportunités d'améliorer les définitions de propriétés existantes avec de meilleures contraintes, formats et règles de validation.", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Lorsque l'agent s'exécute sans session utilisateur (p. ex. tâches planifiées), le contexte de cet utilisateur sera utilisé", + "Workflows": "Flux de travail", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "Bookmarks": "Bookmarks", + "Categories of data subjects (one per line)": "Categories of data subjects (one per line)", + "Categories of personal data (one per line)": "Categories of personal data (one per line)", + "Configuring field mappings...": "Configuring field mappings...", + "Connecting to SOLR cluster...": "Connecting to SOLR cluster...", + "Creating search collections...": "Creating search collections...", + "Edit processing activity": "Edit processing activity", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Finalizing setup...": "Finalizing setup...", + "Forbidden": "Forbidden", + "Initializing SOLR setup...": "Initializing SOLR setup...", + "Legal basis *": "Legal basis *", + "New processing activity": "New processing activity", + "Optimizing search performance...": "Optimizing search performance...", + "Organisational measures": "Organisational measures", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Purpose limitation *": "Purpose limitation *", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Retention period (ISO-8601 duration, e.g. P10Y, P30D)", + "SOLR setup failed. Please check the configuration and try again.": "SOLR setup failed. Please check the configuration and try again.", + "Select all entities": "Select all entities", + "Select entity {value}": "Select entity {value}", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "Technical measures": "Technical measures", + "Uploading configuration sets...": "Uploading configuration sets...", + "Verifying server connectivity...": "Verifying server connectivity...", + "Waiting for cluster synchronization...": "Waiting for cluster synchronization...", + "You do not have access to this object": "You do not have access to this object", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR can handle millions of documents with sub-second search response times...", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Faceted search allows users to drill down into results by categories...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR provides rich analytics and statistics about search performance...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets contain the schema and configuration files for your search index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replication ensures your search index is available even if nodes fail...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Auto-scaling can dynamically add or remove nodes based on load...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Security features include authentication, authorization, and SSL encryption...", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-mails" + ], + "Purge {count} object from database": [ + "Purger {count} objet de la base de données", + "Purger {count} objets de la base de données" + ], + "Restore {count} object": [ + "Restaurer {count} objet", + "Restaurer {count} objets" + ], + "Successfully restored {count} object": [ + "{count} objet restauré avec succès", + "{count} objets restaurés avec succès" + ], + "Delete {count} object": [ + "Supprimer {count} objet", + "Supprimer {count} objets" + ], + "Object successfully deleted": [ + "Objet supprimé avec succès", + "Objets supprimés avec succès" + ] + } +} diff --git a/l10n/ga.js b/l10n/ga.js new file mode 100644 index 0000000000..a494c91b64 --- /dev/null +++ b/l10n/ga.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(gan ábhar)", + "(no title)" : "(gan teideal)", + "(unnamed)" : "(gan ainm)", + "(untitled card)" : "(cárta gan teideal)", + "Activities" : "Gníomhaíochtaí", + "Add Endpoint" : "Cuir Críochphointe Leis", + "Add Source" : "Cuir Foinse Leis", + "Add card" : "Cuir cárta leis", + "Add contact" : "Cuir teagmhálaí leis", + "All clear" : "Gach rud soiléir", + "Articles" : "Ailt", + "Code" : "Cód", + "Compliance" : "Comhlíonadh", + "Contacts" : "Teagmhálaithe", + "Create Agent" : "Cruthaigh Gníomhaire", + "Create Application" : "Cruthaigh Feidhmchlár", + "Create event" : "Cruthaigh imeacht", + "Erasure complete" : "Léirscriosadh críochnaithe", + "Erasure preview" : "Réamhamharc léirscriosta", + "Expand details" : "Leathnaigh sonraí", + "Save changes" : "Sábháil athruithe", + "📄 Object Serialization" : "📄 Srathú Réad", + "🔢 Vectorization Settings" : "🔢 Socruithe Veicteoirithe", + "💰 View Selection (Cost Optimization)" : "💰 Roghnú Amharc (Optamú Costais)", + "AI Assistant" : "Cúntóir AI", + "AI Chat" : "Comhrá AI", + "API Key" : "Eochair API", + "API tokens saved successfully" : "Sábháladh comharthaí API go rathúil", + "About ConfigSets" : "Maidir le ConfigSets", + "Action" : "Gníomh", + "Action Distribution" : "Dáileadh Gníomhartha", + "Action:" : "Gníomh:", + "Actions" : "Gníomhartha", + "Active" : "Gníomhach", + "Active Collections" : "Bailiúcháin Ghníomhacha", + "Active Filters" : "Scagairí Gníomhacha", + "Active Organisations" : "Eagraíochtaí Gníomhacha", + "Active filters:" : "Scagairí gníomhacha:", + "Active organisation set successfully" : "Socraíodh an eagraíocht ghníomhach go rathúil", + "Activity Period" : "Tréimhse Ghníomhaíochta", + "Add Groups" : "Cuir Grúpaí Leis", + "Add Schema" : "Cuir Scéimre Leis", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Cuir teidil scéimre, cur síos, agus eolas cláir leis chun comhthéacs níos saibhre a sholáthar don chuardach", + "Add to favorites" : "Cuir leis na ceanáin", + "Added" : "Curtha leis", + "Added to favorites" : "Curtha leis na ceanáin", + "Additional Information" : "Eolas Breise", + "Additional feedback saved. Thank you!" : "Sábháladh aiseolas breise. Go raibh maith agat!", + "Admin" : "Riarthóir", + "Advanced" : "Ardleibhéil", + "Advanced Filters" : "Scagairí Ardleibhéil", + "Advanced Options" : "Roghanna Ardleibhéil", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Níl scagairí ardleibhéil ar fáil agus foinse bunachair sonraí á húsáid. Athraigh go Auto nó SOLR Index le haghaidh roghanna scagtha.", + "After" : "Tar éis", + "Agent deleted successfully" : "Scriosadh an gníomhaire go rathúil", + "Agents" : "Gníomhairí", + "All" : "Gach", + "All Categories" : "Gach Catagóir", + "All Collections" : "Gach Bailiúchán", + "All Confidence Levels" : "Gach Leibhéal Muiníne", + "All ConfigSets" : "Gach ConfigSet", + "All Files" : "Gach Comhad", + "All Levels" : "Gach Leibhéal", + "All Types" : "Gach Cineál", + "All Webhooks" : "Gach Webhook", + "All actions" : "Gach gníomh", + "All audit trails cleared successfully" : "Glanadh gach rian iniúchta go rathúil", + "All registers" : "Gach clár", + "All schemas" : "Gach scéimre", + "All searches" : "Gach cuardach", + "All users" : "Gach úsáideoir", + "An error occurred while clearing audit trails" : "Tharla earráid agus rianta iniúchta á nglanadh", + "An error occurred while deleting the audit trail" : "Tharla earráid agus an rian iniúchta á scriosadh", + "An error occurred while deleting the view" : "Tharla earráid agus an t-amharc á scriosadh", + "An error occurred while permanently deleting the objects" : "Tharla earráid agus na réada á scriosadh go buan", + "Analysis completed:" : "Anailís críochnaithe:", + "Analysis steps:" : "Céimeanna anailíse:", + "Analytics" : "Anailísíocht", + "Analyze Objects" : "Déan Anailís ar Réada", + "Analyze Properties" : "Déan Anailís ar Airí", + "Analyze existing properties for improvement opportunities" : "Déan anailís ar airí atá ann cheana le haghaidh deiseanna feabhsúcháin", + "Analyzing..." : "Ag déanamh anailíse...", + "Any favorites and sharing settings for this view" : "Aon cheanáin agus socruithe comhroinnte don amharc seo", + "Any user" : "Aon úsáideoir", + "App store cache invalidated successfully" : "Neamhbhailíodh taisce an stóir aipeanna go rathúil", + "Application deleted successfully" : "Scriosadh an feidhmchlár go rathúil", + "Applications" : "Feidhmchláir", + "Apply Changes" : "Cuir Athruithe i bhFeidhm", + "Archive" : "Cartlann", + "Archive conversation" : "Cuir comhrá sa chartlann", + "Archived conversations are hidden from your active list" : "Tá comhráite sa chartlann i bhfolach ó do liosta gníomhach", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "An bhfuil tú cinnte gur mhaith leat seanrianta cuardaigh a ghlanadh? Scriosfaidh sé seo iontrálacha atá níos sine ná 30 lá.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "An bhfuil tú cinnte gur mhaith leat na rianta iniúchta roghnaithe a scriosadh? Ní féidir an gníomh seo a chealú.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "An bhfuil tú cinnte gur mhaith leat na rianta cuardaigh roghnaithe a scriosadh? Ní féidir an gníomh seo a chealú.", + "Are you sure you want to delete this ConfigSet?" : "An bhfuil tú cinnte gur mhaith leat an ConfigSet seo a scriosadh?", + "Are you sure you want to permanently delete" : "An bhfuil tú cinnte gur mhaith leat scriosadh go buan", + "Ask a question..." : "Cuir ceist...", + "Ask questions about your data using natural language" : "Cuir ceisteanna faoi do shonraí ag baint úsáide as teanga nádúrtha", + "Attempt" : "Iarracht", + "Audit Trail Changes" : "Athruithe Rian Iniúchta", + "Audit Trail Details" : "Sonraí Rian Iniúchta", + "Audit Trail Management" : "Bainistíocht Rian Iniúchta", + "Audit Trail Statistics" : "Staitisticí Rian Iniúchta", + "Audit Trails" : "Rianta Iniúchta", + "Audit trail data copied to clipboard" : "Cóipeáladh sonraí an rian iniúchta chuig an ngearrthaisce", + "Audit trail deleted successfully" : "Scriosadh an rian iniúchta go rathúil", + "Audit trail successfully deleted" : "Scriosadh an rian iniúchta go rathúil", + "Authentication" : "Fíordheimhniú", + "Auto-commit disabled" : "Díchumasaíodh uathchur", + "Auto-commit enabled" : "Cumasaíodh uathchur", + "Auto-create Default Organisation" : "Cruthaigh Eagraíocht Réamhshocraithe go hUathoibríoch", + "Auto-retry failed vectorizations" : "Atriail uathoibríoch ar veicteoiriú a theip", + "Automatically commit changes to SOLR index" : "Cuir athruithe i bhfeidhm go huathoibríoch ar innéacs SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Cruthaigh eagraíocht réamhshocraithe go huathoibríoch mura bhfuil ceann ann nuair a chuirtear an aip ar tús", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Gin leabuithe veicteoir go huathoibríoch ó smutáin téacs nuair a uaslódáiltear agus a phróiseáiltear comhaid", + "Automatically generate vector embeddings when objects are created or updated" : "Gin leabuithe veicteoir go huathoibríoch nuair a chruthaítear nó a nuashonraítear réada", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Atriail iarrachtaí veicteoirithe a theip go huathoibríoch (uasmhéad 3 atriail)", + "Available Filters" : "Scagairí ar Fáil", + "Available Workflows" : "Sreabháin Oibre ar Fáil", + "Avg Execution Time" : "Meánam Forghníomhaithe", + "Avg Members/Org" : "Meánbhaill/Eagraíocht", + "Avg Object Views/Session" : "Meánamhairc Réad/Seisiún", + "Avg Response Time" : "Meánam Freagartha", + "Avg Results/Search" : "Meántorthaí/Cuardach", + "Avg Searches/Session" : "Meánchuardaigh/Seisiún", + "Back" : "Ar Ais", + "Back to Entities" : "Ar Ais go hAonáin", + "Back to Registers" : "Ar Ais go Cláir", + "Back to Webhooks" : "Ar Ais go Webhooks", + "Back to applications" : "Ar ais go feidhmchláir", + "Back to entities" : "Ar ais go haonáin", + "Backend updated successfully. Please reload the application." : "Nuashonraíodh an t-inneall go rathúil. Athluchtaigh an feidhmchlár le do thoil.", + "Base URL (Optional)" : "Bun-URL (Roghnach)", + "Basic Information" : "Bunfhaisnéis", + "Batch Size" : "Méid Baisce", + "Batch extraction completed" : "Eastóscadh baisce críochnaithe", + "Before" : "Roimh", + "Before object vectorization can work:" : "Sula bhféadfaidh veicteoiriú réad oibriú:", + "Behavior Issue" : "Saincheist Iompair", + "Bookmarks" : "Leabharmharcanna", + "Blob storage has been retired. All objects now use magic tables." : "Tá stóráil blob curtha ar scor. Úsáideann gach réad táblaí draíochta anois.", + "Branch" : "Brainse", + "Bulk delete operation completed successfully" : "Críochnaíodh an oibríocht olscriosta go rathúil", + "Bulk save operation completed successfully" : "Críochnaíodh an oibríocht olsábhála go rathúil", + "Business Data" : "Sonraí Gnó", + "Cache cleared successfully" : "Glanadh an taisce go rathúil", + "Calculate Sizes" : "Ríomh Méideanna", + "Cancel" : "Cealaigh", + "Cannot delete: objects are still attached" : "Ní féidir scriosadh: tá réada ceangailte fós", + "Category" : "Catagóir", + "Change Type" : "Cineál Athraithe", + "Changes" : "Athruithe", + "Changes copied to clipboard" : "Cóipeáladh athruithe chuig an ngearrthaisce", + "Chat Model" : "Múnla Comhrá", + "Chat Provider" : "Soláthraí Comhrá", + "Chat Provider (RAG)" : "Soláthraí Comhrá (RAG)", + "Chat Settings" : "Socruithe Comhrá", + "Chat provider connection successful!" : "D'éirigh le nasc an tsoláthraí comhrá!", + "Chat settings" : "Socruithe comhrá", + "Choose a register" : "Roghnaigh clár", + "Choose a schema" : "Roghnaigh scéimre", + "Choose how vector similarity calculations are performed for semantic search" : "Roghnaigh conas a dhéantar ríomhanna cosúlachta veicteoir don chuardach séimeantach", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Roghnaigh na cineálacha comhad a chuirfear san áireamh sa phróiseas veicteoirithe. Ní phróiseálfar ach comhaid a bhfuil téacs agus smutáin eastósctha acu.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Roghnaigh na hamhairc a chuirfear san áireamh sa phróiseas veicteoirithe. Fág folamh chun gach amharc a phróiseáil bunaithe ar do chumraíocht.", + "Chunk" : "Smután", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Níl scriosadh smután curtha i bhfeidhm fós. Úsáid críochphointí smután-bhunaithe.", + "Chunks" : "Smutáin", + "Chunks to Vectorize:" : "Smutáin le Veicteoiriú:", + "Cleanup Old Trails" : "Glan Seanrianta", + "Cleanup completed" : "Glanadh críochnaithe", + "Clear All Embeddings" : "Glan Gach Leabú", + "Clear Entries" : "Glan Iontrálacha", + "Clear Filtered Audit Trails" : "Glan Rianta Iniúchta Scagtha", + "Clear Filters" : "Glan Scagairí", + "Clear Index" : "Glan Innéacs", + "Clear Selection" : "Glan Roghnúchán", + "Clear all" : "Glan gach rud", + "Clear all filters" : "Glan gach scagaire", + "Clear date range" : "Glan raon dátaí", + "Clearing..." : "Ag glanadh...", + "Close" : "Dún", + "Collection Management" : "Bainistíocht Bailiúcháin", + "Collection Name" : "Ainm Bailiúcháin", + "Collection assignments updated successfully" : "Nuashonraíodh sannacháin bailiúcháin go rathúil", + "Collection cleared successfully" : "Glanadh an bailiúchán go rathúil", + "Collection copied successfully" : "Cóipeáladh an bailiúchán go rathúil", + "Collection created successfully" : "Cruthaíodh an bailiúchán go rathúil", + "Collection deleted successfully" : "Scriosadh an bailiúchán go rathúil", + "Collection used to store and index file metadata and content" : "Bailiúchán a úsáidtear chun meiteashonraí agus ábhar comhad a stóráil agus a innéacsú", + "Collection used to store and index object data" : "Bailiúchán a úsáidtear chun sonraí réad a stóráil agus a innéacsú", + "Collections:" : "Bailiúcháin:", + "Commit Message" : "Teachtaireacht Tiomanta", + "Commit Within (ms)" : "Tiomain Laistigh (ms)", + "Commit message" : "Teachtaireacht tiomanta", + "Compare current schema with real object data" : "Déan comparáid idir an scéimre reatha agus fíorshonraí réad", + "Completed" : "Críochnaithe", + "Complex" : "Casta", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Ceisteanna casta: Cuardaigh ardleibhéil le scagairí iolracha, oibreoirí, agus teaglamaí paraiméadar casta", + "Confidence Level" : "Leibhéal Muiníne", + "Confidence Score" : "Scór Muiníne", + "Config must be provided as an object" : "Ní mór an chumraíocht a sholáthar mar réad", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Bainistíocht ConfigSet", + "ConfigSet Name" : "Ainm ConfigSet", + "ConfigSet created successfully" : "Cruthaíodh an ConfigSet go rathúil", + "ConfigSet deleted successfully" : "Scriosadh an ConfigSet go rathúil", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Sainíonn ConfigSets an scéimre agus an chumraíocht do do bhailiúcháin SOLR. Tá sainmhínithe réimsí, anailíseoirí, agus socruithe cuardaigh eile iontu.", + "Configuration" : "Cumraíocht", + "Configuration Data" : "Sonraí Cumraíochta", + "Configuration saved" : "Sábháladh an chumraíocht", + "Configurations" : "Cumraíochtaí", + "Configure Facets" : "Cumraigh Gnéithe", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Cumraigh soláthraithe Múnla Mór Teanga (LLM) le haghaidh gnéithe AI-chumhachtaithe lena n-áirítear cuardach séimeantach, leabuithe, agus comhrá.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Cumraigh bunsocruithe naisc do do fhreastalaí SOLR lena n-áirítear fíordheimhniú agus roghanna líonra. Úsáid na dialóga ConfigSet agus Bainistíocht Bailiúcháin ar leithligh chun cóir agus bailiúcháin a bhainistiú.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Cumraigh conas a thiontaítear réada bunachair sonraí ina leabuithe veicteoir don chuardach séimeantach. Déantar réada a veicteoiriú go díreach gan gá le heastóscadh téacs.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Cumraigh conas a thiontaítear réada go téacs roimh veicteoiriú. Bíonn tionchar ag na socruithe seo ar cháilíocht agus comhthéacs an chuardaigh.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Cumraigh paraiméadair le haghaidh veicteoirithe comhad. Ginfidh an próiseas seo leabuithe veicteoir do gach smután comhaid eastósctha.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Cumraigh paraiméadair le haghaidh veicteoirithe réad. Ginfidh an próiseas seo leabuithe veicteoir do gach réad a mheaitseálann do scagairí amhairc.", + "Configure which types of data to search and how many sources to retrieve" : "Cumraigh na cineálacha sonraí le cuardach agus an méid foinsí le haisghabháil", + "Connection Failed" : "Theip ar an Nasc", + "Connection Settings" : "Socruithe Naisc", + "Connection Successful!" : "D'éirigh leis an Nasc!", + "Connection failed" : "Theip ar an nasc", + "Connection protocol" : "Prótacal naisc", + "Connection successful" : "D'éirigh leis an nasc", + "Connection timeout in seconds" : "Teorainn ama naisc i soicindí", + "Constant delay between retries (5 minutes)" : "Moill sheasmhach idir atrialacha (5 nóiméad)", + "Constraint Issue" : "Saincheist Srianta", + "Content Filters" : "Scagairí Ábhair", + "Continue to Properties" : "Lean ar aghaidh go hAirí", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Rialaigh na hamhairc réad ba chóir a veicteoiriú chun costais API a laghdú. Ná veicteoirigh ach amhairc a bhaineann tairbhe as cuardach séimeantach.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Rialaigh na hamhairc agus na huirlisí is féidir leis an AI a úsáid sa chomhrá seo. De réir réamhshocraithe, tá gach cumas gníomhaire cumasaithe.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Rialaíonn sé randamacht (0 = cinntitheach, 2 = an-chruthaitheach)", + "Conversation ID is required" : "Tá aitheantas comhrá riachtanach", + "Conversation archived" : "Cuireadh an comhrá sa chartlann", + "Conversation archived successfully" : "Cuireadh an comhrá sa chartlann go rathúil", + "Conversation cleared successfully" : "Glanadh an comhrá go rathúil", + "Conversation deleted" : "Scriosadh an comhrá", + "Conversation permanently deleted" : "Scriosadh an comhrá go buan", + "Conversation renamed" : "Athainmníodh an comhrá", + "Conversation restored" : "Athchóiríodh an comhrá", + "Conversation title" : "Teideal comhrá", + "Copied!" : "Cóipeáilte!", + "Copy" : "Cóipeáil", + "Copy Changes" : "Cóipeáil Athruithe", + "Copy Collection" : "Cóipeáil Bailiúchán", + "Copy Data" : "Cóipeáil Sonraí", + "Copy Full Data" : "Cóipeáil Sonraí Iomlána", + "Copying..." : "Ag cóipeáil...", + "Count" : "Líon", + "Counting objects..." : "Ag comhaireamh réad...", + "Costs" : "Costais", + "Create" : "Cruthaigh", + "Create Collection" : "Cruthaigh Bailiúchán", + "Create ConfigSet" : "Cruthaigh ConfigSet", + "Create New Collection" : "Cruthaigh Bailiúchán Nua", + "Create New ConfigSet" : "Cruthaigh ConfigSet Nua", + "Create Operations" : "Oibríochtaí Cruthaithe", + "Create Webhook" : "Cruthaigh Webhook", + "Create a copy of collection:" : "Cruthaigh cóip den bhailiúchán:", + "Create a new ConfigSet based on the _default template" : "Cruthaigh ConfigSet nua bunaithe ar an teimpléad _default", + "Create a new SOLR collection from an existing ConfigSet" : "Cruthaigh bailiúchán SOLR nua ó ConfigSet atá ann cheana", + "Create your first AI agent to get started." : "Cruthaigh do chéad ghníomhaire AI le tosú.", + "Created" : "Cruthaithe", + "Created:" : "Cruthaithe:", + "Creating..." : "Ag cruthú...", + "Current Filters" : "Scagairí Reatha", + "Current Type:" : "Cineál Reatha:", + "Custom API endpoint if using a different region" : "Críochphointe API saincheaptha má úsáidtear réigiún difriúil", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Ceanntásca HTTP saincheaptha (ceann sa líne, formáid: Ainm-Ceanntásca: luach)", + "Daily" : "Laethúil", + "Dashboard" : "Deais", + "Data Source" : "Foinse Sonraí", + "Data sources" : "Foinsí sonraí", + "Data type does not match observed values" : "Ní mheaitseálann an cineál sonraí na luachanna breathnaithe", + "Database URL" : "URL Bunachair Sonraí", + "Database information refreshed" : "Athnuachan ar fhaisnéis an bhunachair sonraí", + "Date Range" : "Raon Dátaí", + "Default" : "Réamhshocrú", + "Default Organisation" : "Eagraíocht Réamhshocraithe", + "Default: 5" : "Réamhshocrú: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Méadaíonn moilleanna faoi dhó le gach iarracht (2, 4, 8 nóiméad...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Méadaíonn moilleanna go líneach (5, 10, 15 nóiméad...)", + "Delete" : "Scrios", + "Delete Application" : "Scrios Feidhmchlár", + "Delete Audit Trail" : "Scrios Rian Iniúchta", + "Delete Collection" : "Scrios Bailiúchán", + "Delete ConfigSet" : "Scrios ConfigSet", + "Delete Objects" : "Scrios Réada", + "Delete Operations" : "Oibríochtaí Scriosta", + "Delete View" : "Scrios Amharc", + "Delete all objects in this schema" : "Scrios gach réad sa scéimre seo", + "Delete permanently" : "Scrios go buan", + "Delete this conversation?" : "Scrios an comhrá seo?", + "Delete view" : "Scrios amharc", + "Deleted" : "Scriosta", + "Deleted By" : "Scriosta Ag", + "Deleted Date" : "Dáta Scriosta", + "Deleted Items Management" : "Bainistíocht Míreanna Scriosta", + "Deleted This Week" : "Scriosta an tSeachtain Seo", + "Deleted Today" : "Scriosta Inniu", + "Deleted:" : "Scriosta:", + "Deleting..." : "Ag scriosadh...", + "Deletion Date Range" : "Raon Dátaí Scriosta", + "Deletion Statistics" : "Staitisticí Scriosta", + "Deprecated" : "Dímholta", + "Description" : "Cur Síos", + "Description:" : "Cur Síos:", + "Detect data types and patterns" : "Braith cineálacha sonraí agus patrúin", + "Detected At" : "Braite Ag", + "Detected Format:" : "Formáid Bhraite:", + "Detected Issues:" : "Saincheisteanna Braite:", + "Disable" : "Díchumasaigh", + "Disabled" : "Díchumasaithe", + "Discover Files" : "Aimsigh Comhaid", + "Discovered Properties" : "Airí Aimsithe", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "An bhfuil tú ag iarraidh gach iontráil rian iniúchta scagtha a scriosadh go buan? Ní féidir an gníomh seo a chealú.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "An bhfuil tú ag iarraidh an iontráil rian iniúchta seo a scriosadh go buan? Ní féidir an gníomh seo a chealú.", + "Documents" : "Cáipéisí", + "Download API Spec" : "Íoslódáil Sonraíocht API", + "Edit" : "Cuir in Eagar", + "Edit Register" : "Cuir Clár in Eagar", + "Edit Schema" : "Cuir Scéimre in Eagar", + "Edit View" : "Cuir Amharc in Eagar", + "Edit Webhook" : "Cuir Webhook in Eagar", + "Edit view" : "Cuir amharc in eagar", + "Edit view details" : "Cuir sonraí an amhairc in eagar", + "Effectiveness" : "Éifeachtacht", + "Email" : "Ríomhphost", + "Emails" : "Ríomhphoist", + "Embedding Model" : "Múnla Leabaithe", + "Embedding Provider" : "Soláthraí Leabaithe", + "Embedding generated successfully" : "Gineadh an leabú go rathúil", + "Embedding provider connection successful!" : "D'éirigh le nasc an tsoláthraí leabaithe!", + "Enable" : "Cumasaigh", + "Enable auto-creation" : "Cumasaigh uathchruthú", + "Enable automatic file vectorization" : "Cumasaigh veicteoiriú comhad uathoibríoch", + "Enable automatic object vectorization" : "Cumasaigh veicteoiriú réad uathoibríoch", + "Enable automatic synchronization" : "Cumasaigh sioncronú uathoibríoch", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Cumasaigh logáil mhionsonraithe le haghaidh oibríochtaí SOLR (molta le haghaidh dífhabhtaithe)", + "Enable faceting" : "Cumasaigh gnéúchán", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Cumasaigh nó díchumasaigh gnéithe LLM. Cumraigh soláthraithe agus múnlaí ag baint úsáide as an gcnaipe Cumraíocht LLM thuas.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Cumasaigh nó díchumasaigh comhtháthú cuardaigh SOLR. Cumraigh socruithe naisc ag baint úsáide as an gcnaipe Socruithe Naisc thuas.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Cumasaigh nó díchumasaigh comhtháthú sreabháin oibre n8n. Cumraigh socruithe naisc thíos.", + "Enable or disable this webhook" : "Cumasaigh nó díchumasaigh an webhook seo", + "Enable vectorization for all existing and future views (may increase costs)" : "Cumasaigh veicteoiriú do gach amharc atá ann agus a bheidh ann amach anseo (d'fhéadfadh sé costais a mhéadú)", + "Enabled" : "Cumasaithe", + "Endpoints" : "Críochphointí", + "Enter ConfigSet name" : "Cuir isteach ainm ConfigSet", + "Enter collection name" : "Cuir isteach ainm bailiúcháin", + "Enter description (optional)..." : "Cuir isteach cur síos (roghnach)...", + "Enter new collection name" : "Cuir isteach ainm bailiúcháin nua", + "Enter object ID" : "Cuir isteach aitheantas réid", + "Enter search term" : "Cuir isteach téarma cuardaigh", + "Enter view name..." : "Cuir isteach ainm an amhairc...", + "Enter webhook name" : "Cuir isteach ainm an webhook", + "Entities" : "Aonáin", + "Entity Information" : "Faisnéis Aonáin", + "Entity deleted successfully" : "Scriosadh an t-aonán go rathúil", + "Entity not found" : "Aonán gan aimsiú", + "Entries to be deleted:" : "Iontrálacha le scriosadh:", + "Enum Issue" : "Saincheist Enum", + "Enum constraint is missing" : "Tá srianadh enum ar iarraidh", + "Error" : "Earráid", + "Error Details" : "Sonraí Earráide", + "Error Information" : "Faisnéis Earráide", + "Error Message" : "Teachtaireacht Earráide", + "Error loading application" : "Earráid agus an feidhmchlár á luchtú", + "Error loading audit trails" : "Earráid agus rianta iniúchta á luchtú", + "Error loading entity" : "Earráid agus an t-aonán á luchtú", + "Error loading search trails" : "Earráid agus rianta cuardaigh á luchtú", + "Estimated Batches:" : "Baisceanna Measta:", + "Estimated Cost:" : "Costas Measta:", + "Estimated Duration:" : "Fad Measta:", + "Event" : "Imeacht", + "Event Property for Payload" : "Airí Imeachta don Phálasta", + "Events" : "Imeachtaí", + "Example Value" : "Luach Samplach", + "Exclusive Maximum" : "Uasmhéid Eisiach", + "Exclusive Minimum" : "Íosmhéid Eisiach", + "Execution Mode" : "Mód Forghníomhaithe", + "Execution Time" : "Am Forghníomhaithe", + "Execution Time Range" : "Raon Ama Forghníomhaithe", + "Existing Improvements" : "Feabhsúcháin atá ann", + "Exponential" : "Easpónantúil", + "Export" : "Easpórtáil", + "Export completed successfully" : "Críochnaíodh an t-easpórtáil go rathúil", + "Export, view, or delete audit trails" : "Easpórtáil, amharc, nó scrios rianta iniúchta", + "Extend Schema" : "Leathnaigh Scéimre", + "Extended" : "Leathnaithe", + "Extensions" : "Eisínteachtaí", + "Extract Pending Files" : "Eastóscadh Comhad ar Feitheamh", + "Extract properties from each object" : "Eastóscadh airí ó gach réad", + "Extracted At" : "Eastósctha Ag", + "Extraction Status" : "Stádas Eastósctha", + "Facet configuration updated successfully" : "Nuashonraíodh cumraíocht na ngnéithe go rathúil", + "Facets discovered and configured successfully" : "Aimsíodh agus cumraíodh gnéithe go rathúil", + "Facets discovered successfully" : "Aimsíodh gnéithe go rathúil", + "Failed" : "Theip", + "Failed to analyze schema properties" : "Theip ar anailís a dhéanamh ar airí scéimre", + "Failed to archive conversation" : "Theip ar an gcomhrá a chur sa chartlann", + "Failed to calculate sizes" : "Theip ar ríomh na méideanna", + "Failed to clear collection" : "Theip ar an mbailiúchán a ghlanadh", + "Failed to copy changes" : "Theip ar na hathruithe a chóipeáil", + "Failed to copy data" : "Theip ar na sonraí a chóipeáil", + "Failed to copy data to clipboard" : "Theip ar na sonraí a chóipeáil chuig an ngearrthaisce", + "Failed to create conversation" : "Theip ar an gcomhrá a chruthú", + "Failed to create or find project" : "Theip ar thionscadal a chruthú nó a aimsiú", + "Failed to delete collection" : "Theip ar an mbailiúchán a scriosadh", + "Failed to delete conversation" : "Theip ar an gcomhrá a scriosadh", + "Failed to delete webhook" : "Theip ar an webhook a scriosadh", + "Failed to download API specification" : "Theip ar shonraíocht API a íoslódáil", + "Failed to get SOLR field configuration" : "Theip ar chumraíocht réimse SOLR a fháil", + "Failed to get entity categories" : "Theip ar chatagóirí aonáin a fháil", + "Failed to get entity statistics" : "Theip ar staitisticí aonáin a fháil", + "Failed to get entity types" : "Theip ar chineálacha aonáin a fháil", + "Failed to load LLM configuration" : "Theip ar chumraíocht LLM a luchtú", + "Failed to load Nextcloud groups" : "Theip ar ghrúpaí Nextcloud a luchtú", + "Failed to load conversation" : "Theip ar an gcomhrá a luchtú", + "Failed to load entities" : "Theip ar aonáin a luchtú", + "Failed to load entity" : "Theip ar an aonán a luchtú", + "Failed to load files" : "Theip ar chomhaid a luchtú", + "Failed to load organisations" : "Theip ar eagraíochtaí a luchtú", + "Failed to load templates" : "Theip ar theimpléid a luchtú", + "Failed to load webhooks" : "Theip ar webhooks a luchtú", + "Failed to load workflows" : "Theip ar shreabháin oibre a luchtú", + "Failed to refresh database information" : "Theip ar fhaisnéis an bhunachair sonraí a athnuachan", + "Failed to reindex collection" : "Theip ar an mbailiúchán a athinnéacsú", + "Failed to rename conversation" : "Theip ar an gcomhrá a athainmniú", + "Failed to restore conversation" : "Theip ar an gcomhrá a athchóiriú", + "Failed to retry extraction" : "Theip ar an eastóscadh a atriail", + "Failed to retry webhook" : "Theip ar an webhook a atriail", + "Failed to save GitHub token" : "Theip ar chomhartha GitHub a shábháil", + "Failed to save GitLab URL" : "Theip ar URL GitLab a shábháil", + "Failed to save GitLab token" : "Theip ar chomhartha GitLab a shábháil", + "Failed to save additional feedback" : "Theip ar aiseolas breise a shábháil", + "Failed to save n8n configuration" : "Theip ar chumraíocht n8n a shábháil", + "Failed to save roles" : "Theip ar róil a shábháil", + "Failed to save settings" : "Theip ar shocruithe a shábháil", + "Failed to save webhook" : "Theip ar an webhook a shábháil", + "Failed to send feedback" : "Theip ar aiseolas a sheoladh", + "Failed to test webhook" : "Theip ar an webhook a thástáil", + "Failed to update favorite status" : "Theip ar an stádas ceanáin a nuashonrú", + "Failed to update schema properties" : "Theip ar airí scéimre a nuashonrú", + "Failed to update view" : "Theip ar an amharc a nuashonrú", + "Failed to update webhook" : "Theip ar an webhook a nuashonrú", + "Feedback recorded" : "Aiseolas taifeadta", + "Fewer sources (1-3):" : "Níos lú foinsí (1-3):", + "Field" : "Réimse", + "File" : "Comhad", + "File Chunk Prediction" : "Tuar Smután Comhaid", + "File Collection" : "Bailiúchán Comhad", + "File Management" : "Bainistíocht Comhad", + "File Name" : "Ainm Comhaid", + "File Path" : "Conair Comhaid", + "File Type Selection" : "Roghnú Cineál Comhaid", + "File Vectorization" : "Veicteoiriú Comhad", + "File Warmup" : "Téamh Comhad", + "File actions menu" : "Roghchlár gníomhartha comhaid", + "File anonymized successfully" : "Anaithnidíodh an comhad go rathúil", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Níl veicteoiriú smután comhaid curtha i bhfeidhm fós. Tá na smutáin réidh agus stóráilte, ach tá an tseirbhís veicteoirithe á forbairt.", + "File collection not configured" : "Níl bailiúchán comhad cumraithe", + "File discovery completed" : "Aimsiú comhad críochnaithe", + "File extraction completed" : "Eastóscadh comhad críochnaithe", + "File extraction queued" : "Cuireadh eastóscadh comhad sa scuaine", + "File indexed successfully" : "Innéacsaíodh an comhad go rathúil", + "File is already anonymized" : "Tá an comhad anaithnidithe cheana", + "File not found" : "Comhad gan aimsiú", + "File settings updated successfully" : "Nuashonraíodh socruithe comhaid go rathúil", + "File sources" : "Foinsí comhad", + "File vectorization configuration saved successfully" : "Sábháladh cumraíocht veicteoirithe comhad go rathúil", + "File vectorization started. Check the statistics section for progress." : "Tosaíodh veicteoiriú comhad. Seiceáil an chuid staitisticí le haghaidh dul chun cinn.", + "File warmup completed" : "Téamh comhad críochnaithe", + "Files" : "Comhaid", + "Files with Completed Extraction:" : "Comhaid le hEastóscadh Críochnaithe:", + "Files → fileCollection, Objects → objectCollection" : "Comhaid → fileCollection, Réada → objectCollection", + "Filter Audit Trails" : "Scag Rianta Iniúchta", + "Filter Deleted Items" : "Scag Míreanna Scriosta", + "Filter Objects" : "Scag Réada", + "Filter Properties" : "Scag Airí", + "Filter Search Trails" : "Scag Rianta Cuardaigh", + "Filter Statistics" : "Scag Staitisticí", + "Filter and analyze search trail entries" : "Scag agus déan anailís ar iontrálacha rian cuardaigh", + "Filter and manage audit trail entries" : "Scag agus bainistigh iontrálacha rian iniúchta", + "Filter and manage soft deleted items" : "Scag agus bainistigh míreanna boigscriosta", + "Filter by object ID" : "Scag de réir aitheantais réid", + "Filter by search term" : "Scag de réir téarma cuardaigh", + "Filter by webhook" : "Scag de réir webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Luchtaíodh sonraí scagtha go huathoibríoch. Úsáid na scagairí thíos chun do chuardach a bheachtú.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Scag truicir webhook de réir airíonna pálasta (ceann sa líne, formáid: eochair: luach)", + "Filtered" : "Scagtha", + "Filters" : "Scagairí", + "Fireworks AI Chat Settings" : "Socruithe Comhrá Fireworks AI", + "Fireworks AI Embedding Configuration" : "Cumraíocht Leabaithe Fireworks AI", + "First" : "Céad", + "Fixed" : "Socraithe", + "For chat and retrieval-augmented generation" : "Le haghaidh comhrá agus giniúint aisghabhála-fheabhsaithe", + "For vector embeddings and semantic search" : "Le haghaidh leabuithe veicteoir agus cuardaigh shéimeantaigh", + "Format" : "Formáid", + "Format Issue" : "Saincheist Formáide", + "Format constraint is missing" : "Tá srianadh formáide ar iarraidh", + "From Date" : "Ón Dáta", + "From date" : "Ón dáta", + "Full data copied to clipboard" : "Cóipeáladh sonraí iomlána chuig an ngearrthaisce", + "General Issue" : "Saincheist Ghinearálta", + "Generate recommendations and confidence scores" : "Gin moltaí agus scóir mhuiníne", + "Generate vectors immediately when new objects are created" : "Gin veicteoirí láithreach nuair a chruthaítear réada nua", + "GitHub token is valid" : "Tá an comhartha GitHub bailí", + "GitHub token removed successfully" : "Baineadh an comhartha GitHub go rathúil", + "GitHub token saved successfully" : "Sábháladh an comhartha GitHub go rathúil", + "GitLab URL saved successfully" : "Sábháladh URL GitLab go rathúil", + "GitLab token is valid" : "Tá an comhartha GitLab bailí", + "GitLab token saved successfully" : "Sábháladh an comhartha GitLab go rathúil", + "HTTP Method" : "Modh HTTP", + "HTTP method used to send webhook requests" : "Modh HTTP a úsáidtear chun iarratais webhook a sheoladh", + "Headers" : "Ceanntásca", + "Health" : "Sláinte", + "Heartbeat successful - connection kept alive" : "D'éirigh leis an mbuille croí - coinníodh an nasc beo", + "Helpful" : "Cabhrach", + "Hide Filters" : "Folaigh Scagairí", + "Hide in forms" : "Folaigh i bhfoirmeacha", + "Hide in list view" : "Folaigh san amharc liosta", + "High" : "Ard", + "High Confidence" : "Muinín Ard", + "Host" : "Óstach", + "Hourly" : "Gach Uair", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Cé chomh domhain le hairíonna réad neadaithe a thrasnú (1-20). Gabhann luachanna níos airde níos mó sonraí ach méadaíonn siad méid an veicteora.", + "How often to check for updates (1-168 hours)" : "Cé chomh minic a sheiceáil le haghaidh nuashonruithe (1-168 uair)", + "How to handle retries for failed webhook deliveries" : "Conas atrialacha a láimhseáil le haghaidh seachadtaí webhook a theip", + "ID" : "Aitheantas", + "ID:" : "Aitheantas:", + "IP rate limits cleared successfully" : "Glanadh teorainneacha ráta IP go rathúil", + "Identify properties not in the schema" : "Aithin airí nach bhfuil sa scéimre", + "Immutable" : "Do-athraithe", + "Import" : "Iompórtáil", + "Import successful" : "D'éirigh leis an iompórtáil", + "Improved Property" : "Airí Feabhsaithe", + "In use" : "In úsáid", + "Inactive" : "Neamhghníomhach", + "Include IDs and names of related objects for better contextual search" : "Cuir aitheantais agus ainmneacha réad gaolmhar san áireamh le haghaidh cuardach comhthéacsúil níos fearr", + "Include related object references" : "Cuir tagairtí réad gaolmhar san áireamh", + "Include schema and register metadata" : "Cuir meiteashonraí scéimre agus cláir san áireamh", + "Initialization failed" : "Theip ar an túsú", + "Initialize Project" : "Túsaigh Tionscadal", + "Inspect Fields" : "Iniúch Réimsí", + "Inspect Index" : "Iniúch Innéacs", + "Invalid" : "Neamhbhailí", + "Invalid batch size. Must be between 1 and 5000" : "Méid baisce neamhbhailí. Caithfidh sé a bheith idir 1 agus 5000", + "Invalid field name provided" : "Soláthraíodh ainm réimse neamhbhailí", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects neamhbhailí. Caithfidh sé a bheith 0 (gach) nó uimhir dheimhneach", + "Invalid:" : "Neamhbhailí:", + "Issue" : "Saincheist", + "Items per page:" : "Míreanna sa leathanach:", + "Just now" : "Anois díreach", + "LLM Configuration" : "Cumraíocht LLM", + "LLM actions menu" : "Roghchlár gníomhartha LLM", + "LLM configuration saved successfully" : "Sábháladh cumraíocht LLM go rathúil", + "LLM features disabled" : "Díchumasaíodh gnéithe LLM", + "LLM features enabled" : "Cumasaíodh gnéithe LLM", + "LLM must be enabled with an embedding provider configured" : "Caithfidh LLM a bheith cumasaithe le soláthraí leabaithe cumraithe", + "LLM settings updated successfully" : "Nuashonraíodh socruithe LLM go rathúil", + "Last" : "Deireanach", + "Last Triggered" : "Truiceáilte go Deireanach", + "Length Range:" : "Raon Faid:", + "Linear" : "Líneach", + "Load Advanced Filters" : "Luchtaigh Scagairí Ardleibhéil", + "Load advanced filters with live data from your search index" : "Luchtaigh scagairí ardleibhéil le sonraí beo ó d'innéacs cuardaigh", + "Load view" : "Luchtaigh amharc", + "Loading ConfigSets..." : "Ag luchtú ConfigSets...", + "Loading advanced filters..." : "Ag luchtú scagairí ardleibhéil...", + "Loading agents..." : "Ag luchtú gníomhairí...", + "Loading application details..." : "Ag luchtú sonraí an fheidhmchláir...", + "Loading applications..." : "Ag luchtú feidhmchlár...", + "Loading archived conversations..." : "Ag luchtú comhráite cartlannaithe...", + "Loading audit trails..." : "Ag luchtú rianta iniúchta...", + "Loading collections..." : "Ag luchtú bailiúchán...", + "Loading configurations..." : "Ag luchtú cumraíochtaí...", + "Loading conversation..." : "Ag luchtú comhrá...", + "Loading conversations..." : "Ag luchtú comhráite...", + "Loading deleted items..." : "Ag luchtú míreanna scriosta...", + "Loading events..." : "Ag luchtú imeachtaí...", + "Loading filter data..." : "Ag luchtú sonraí scagaire...", + "Loading groups..." : "Ag luchtú grúpaí...", + "Loading log details..." : "Ag luchtú sonraí logála...", + "Loading registers..." : "Ag luchtú clár...", + "Loading schemas..." : "Ag luchtú scéimrí...", + "Loading search trails..." : "Ag luchtú rianta cuardaigh...", + "Loading sources..." : "Ag luchtú foinsí...", + "Loading statistics..." : "Ag luchtú staitisticí...", + "Loading users..." : "Ag luchtú úsáideoirí...", + "Loading views..." : "Ag luchtú amharc...", + "Loading..." : "Ag luchtú...", + "Local" : "Logánta", + "Local Version" : "Leagan Logánta", + "Locked" : "Faoi Ghlas", + "Locked:" : "Faoi Ghlas:", + "Login successful" : "D'éirigh leis an logáil isteach", + "Logs" : "Logaí", + "Low" : "Íseal", + "Low Confidence" : "Muinín Íseal", + "Lucene Version:" : "Leagan Lucene:", + "Magic table synchronized successfully" : "Sioncronaíodh an tábla draíochta go rathúil", + "Manage Organisation Roles" : "Bainistigh Róil Eagraíochta", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Bainistigh Bailiúcháin SOLR (stórais sonraí) agus sann iad do réada agus comhaid.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Bainistigh ConfigSets SOLR (teimpléid chumraíochta) do do bhailiúcháin.", + "Manage and configure agents for automated tasks" : "Bainistigh agus cumraigh gníomhairí le haghaidh tascanna uathoibrithe", + "Manage and monitor file text extraction status" : "Bainistigh agus déan monatóireacht ar stádas eastóscadh téacs comhaid", + "Manage and restore soft deleted items from your registers" : "Bainistigh agus athchóirigh míreanna boigscriosta ó do chláir", + "Manage and view detected entities from files and objects" : "Bainistigh agus amharc ar aonáin bhraite ó chomhaid agus réada", + "Manage document templates and themes" : "Bainistigh teimpléid agus téamaí cáipéisí", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Bainistigh sreabháin oibre n8n le haghaidh uathoibrithe OpenRegister. Stórálfar sreabháin oibre sa tionscadal cumraithe.", + "Manage webhooks for event-driven integrations" : "Bainistigh webhooks le haghaidh comhtháthuithe imeacht-thiomáinte", + "Manage your applications and modules" : "Bainistigh do fheidhmchláir agus modúil", + "Manage your data registers and their configurations" : "Bainistigh do chláir sonraí agus a gcumraíochtaí", + "Manage your data schemas and their properties" : "Bainistigh do scéimrí sonraí agus a n-airíonna", + "Manage your data sources and their configurations" : "Bainistigh d'fhoinsí sonraí agus a gcumraíochtaí", + "Manage your saved search configurations" : "Bainistigh do chumraíochtaí cuardaigh sábháilte", + "Manage your system configurations and settings" : "Bainistigh do chumraíochtaí agus socruithe córais", + "Managed" : "Bainistithe", + "Max Files (0 = all)" : "Uasmhéid Comhad (0 = gach)", + "Max Length" : "Uasfhad", + "Max Length:" : "Uasfhad:", + "Max Objects (0 = all)" : "Uasmhéid Réad (0 = gach)", + "Max Retries" : "Uasmhéid Atrialacha", + "Max Shards/Node" : "Uasmhéid Blúirí/Nód", + "Max execution time (ms)" : "Uasam forghníomhaithe (ms)", + "Max ms" : "Uas ms", + "Max result count" : "Uaslíon torthaí", + "Max results" : "Uastorthaí", + "Maximum" : "Uasmhéid", + "Maximum Nesting Depth" : "Uasdoimhneacht Neadaithe", + "Maximum length constraint is missing" : "Tá srianadh uasfhaid ar iarraidh", + "Maximum length is too restrictive" : "Tá an uasfhad ró-shrianta", + "Maximum number of files to process. Set to 0 to process all files." : "Uaslíon comhad le próiseáil. Socraigh go 0 chun gach comhad a phróiseáil.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Uaslíon réad le próiseáil. Socraigh go 0 chun gach réad a phróiseáil.", + "Maximum number of retry attempts for failed deliveries" : "Uaslíon iarrachtaí atrialacha le haghaidh seachadtaí a theip", + "Maximum time to wait before committing changes" : "Uasam le fanacht sula gcuirfear athruithe i bhfeidhm", + "Maximum tokens to generate" : "Uaslíon comharthaí le giniúint", + "Maximum value constraint is missing" : "Tá srianadh uasluacha ar iarraidh", + "Maximum value is too restrictive" : "Tá an t-uasluach ró-shrianta", + "Medium" : "Meánach", + "Medium Confidence" : "Muinín Mheánach", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Ceisteanna meánacha: Cuardaigh le roinnt scagtha nó paraiméadair iolracha (m.sh., raonta dátaí, cláir/scéimrí ar leith)", + "Members of selected groups can access this view" : "Is féidir le baill grúpaí roghnaithe rochtain a fháil ar an amharc seo", + "Members:" : "Baill:", + "Memory prediction calculated successfully" : "Ríomhadh tuar cuimhne go rathúil", + "Message content is required" : "Tá ábhar teachtaireachta riachtanach", + "Message does not belong to this conversation" : "Ní bhaineann an teachtaireacht leis an gcomhrá seo", + "Metadata Filters" : "Scagairí Meiteashonraí", + "Method" : "Modh", + "Min Length" : "Íosfhad", + "Min execution time (ms)" : "Íosam forghníomhaithe (ms)", + "Min ms" : "Íos ms", + "Min result count" : "Íoslíon torthaí", + "Min results" : "Íostorthaí", + "Minimum" : "Íosmhéid", + "Minimum value constraint is missing" : "Tá srianadh íosluacha ar iarraidh", + "Minimum value is too restrictive" : "Tá an t-íosluach ró-shrianta", + "Missing object fields created successfully" : "Cruthaíodh réimsí réad ar iarraidh go rathúil", + "Mode:" : "Mód:", + "Model" : "Múnla", + "Model Name" : "Ainm Múnla", + "Modified" : "Athraithe", + "Monthly" : "Míosúil", + "More sources (10-20):" : "Níos mó foinsí (10-20):", + "Most Active Objects" : "Réada is Gníomhaí", + "Multiple Of" : "Iolra De", + "Name" : "Ainm", + "Names cache warmup completed" : "Téamh taisce ainmneacha críochnaithe", + "New Collection Name" : "Ainm Bailiúcháin Nua", + "New Conversation" : "Comhrá Nua", + "New Properties" : "Airí Nua", + "New Property" : "Airí Nua", + "New Value" : "Luach Nua", + "New users without specific organisation membership will be automatically added to this organisation" : "Cuirfear úsáideoirí nua gan ballraíocht eagraíochta ar leith leis an eagraíocht seo go huathoibríoch", + "Next" : "Ar Aghaidh", + "No" : "Níl", + "No ConfigSets found" : "Níor aimsíodh aon ConfigSet", + "No GitHub token configured" : "Níl aon chomhartha GitHub cumraithe", + "No GitHub token provided" : "Níor soláthraíodh aon chomhartha GitHub", + "No GitLab token provided" : "Níor soláthraíodh aon chomhartha GitLab", + "No Organisation" : "Gan Eagraíocht", + "No active filters" : "Gan scagairí gníomhacha", + "No activity data available" : "Níl aon sonraí gníomhaíochta ar fáil", + "No agents available" : "Níl aon ghníomhairí ar fáil", + "No agents found" : "Níor aimsíodh aon ghníomhairí", + "No app store cache exists yet" : "Níl aon taisce stóir aipeanna ann fós", + "No applications are available." : "Níl aon fheidhmchláir ar fáil.", + "No applications found" : "Níor aimsíodh aon fheidhmchláir", + "No archived conversations" : "Gan comhráite cartlannaithe", + "No audit trail entries found" : "Níor aimsíodh aon iontrálacha rian iniúchta", + "No changes recorded" : "Níor taifeadadh aon athruithe", + "No chunks to vectorize" : "Gan smutáin le veicteoiriú", + "No collections found" : "Níor aimsíodh aon bhailiúcháin", + "No configuration data" : "Gan sonraí cumraíochta", + "No configurations are available." : "Níl aon chumraíochtaí ar fáil.", + "No configurations found" : "Níor aimsíodh aon chumraíochtaí", + "No configurations found for this application." : "Níor aimsíodh aon chumraíochtaí don fheidhmchlár seo.", + "No conversations yet. Create a new one to get started!" : "Gan comhráite fós. Cruthaigh ceann nua le tosú!", + "No data available" : "Níl aon sonraí ar fáil", + "No data available for chart" : "Níl aon sonraí ar fáil don chairt", + "No deleted items found" : "Níor aimsíodh aon mhíreanna scriosta", + "No deletion data available" : "Níl aon sonraí scriosta ar fáil", + "No description found" : "Níor aimsíodh aon chur síos", + "No description provided" : "Níor soláthraíodh aon chur síos", + "No entities detected in this file. Run text extraction first." : "Níor braitheadh aon aonáin sa chomhad seo. Rith eastóscadh téacs ar dtús.", + "No entities found" : "Níor aimsíodh aon aonáin", + "No entities have been detected yet" : "Níor braitheadh aon aonáin fós", + "No events found" : "Níor aimsíodh aon imeachtaí", + "No expired audit trails found to clear" : "Níor aimsíodh aon rianta iniúchta as feidhm le glanadh", + "No facetable fields available. Select a register and schema to see available filters." : "Níl aon réimsí gnéúchánaithe ar fáil. Roghnaigh clár agus scéimre chun scagairí ar fáil a fheiceáil.", + "No files found" : "Níor aimsíodh aon chomhaid", + "No files have been extracted yet" : "Níor eastósctaíodh aon chomhaid fós", + "No files to process" : "Gan comhaid le próiseáil", + "No files to reindex" : "Gan comhaid le hathinnéacsú", + "No filters are currently active. This will delete ALL audit trail entries!" : "Níl aon scagairí gníomhach faoi láthair. Scriosfaidh sé seo GACH iontráil rian iniúchta!", + "No log entries found" : "Níor aimsíodh aon iontrálacha logála", + "No logs are available for this configuration." : "Níl aon logaí ar fáil don chumraíocht seo.", + "No logs are available for this source." : "Níl aon logaí ar fáil don fhoinse seo.", + "No logs found" : "Níor aimsíodh aon logaí", + "No mismatched fields found - SOLR schema is properly configured" : "Níor aimsíodh aon réimsí neamh-mheaitseáilte - tá scéimre SOLR cumraithe i gceart", + "No objects found" : "Níor aimsíodh aon réada", + "No objects found to vectorize" : "Níor aimsíodh aon réada le veicteoiriú", + "No objects selected for deletion" : "Níor roghnaíodh aon réada le scriosadh", + "No objects to delete" : "Gan réada le scriosadh", + "No objects were permanently deleted" : "Níor scriosadh aon réada go buan", + "No properties found" : "Níor aimsíodh aon airí", + "No properties match your filters." : "Ní mheaitseálann aon airí do scagairí.", + "No purge date set" : "Níl aon dáta scriosta socraithe", + "No register data available" : "Níl aon sonraí cláir ar fáil", + "No registers found" : "Níor aimsíodh aon chláir", + "No registers found for this application." : "Níor aimsíodh aon chláir don fheidhmchlár seo.", + "No relations found" : "Níor aimsíodh aon ghaolta", + "No request body available" : "Níl aon chorp iarratais ar fáil", + "No response body available" : "Níl aon chorp freagartha ar fáil", + "No saved views yet. Create one in the Search tab!" : "Gan amhairc shábháilte fós. Cruthaigh ceann sa chluaisín Cuardaigh!", + "No schema data available" : "Níl aon sonraí scéimre ar fáil", + "No schema selected for exploration" : "Níor roghnaíodh aon scéimre le hiniúchadh", + "No schemas found" : "Níor aimsíodh aon scéimrí", + "No schemas found for this application." : "Níor aimsíodh aon scéimrí don fheidhmchlár seo.", + "No search terms data available" : "Níl aon sonraí téarmaí cuardaigh ar fáil", + "No search trail entries found" : "Níor aimsíodh aon iontrálacha rian cuardaigh", + "No sources are available." : "Níl aon fhoinsí ar fáil.", + "No sources found" : "Níor aimsíodh aon fhoinsí", + "No templates found" : "Níor aimsíodh aon teimpléid", + "No templates have been created yet" : "Níor cruthaíodh aon teimpléid fós", + "No valid magic-mapped register+schema combinations found" : "Níor aimsíodh aon teaglamaí bailí clár+scéimre draíocht-mhapáilte", + "No views found. Create views first before configuring vectorization." : "Níor aimsíodh aon amhairc. Cruthaigh amhairc ar dtús sula gcumróidh tú veicteoiriú.", + "No views match your search" : "Ní mheaitseálann aon amhairc do chuardach", + "No webhooks found" : "Níor aimsíodh aon webhooks", + "No webhooks have been configured yet" : "Níor cumraíodh aon webhooks fós", + "No workflows found in this project. Create workflows in the n8n editor." : "Níor aimsíodh aon sreabháin oibre sa tionscadal seo. Cruthaigh sreabháin oibre san eagarthóir n8n.", + "None" : "Tada", + "Not helpful" : "Gan cabhair", + "Not in use" : "Gan úsáid", + "Number Constraints" : "Srianta Uimhreacha", + "Number of chunks to vectorize in one API call (1-100)." : "Líon smután le veicteoiriú in aon ghlao API amháin (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Líon smután le veicteoiriú in aon ghlao API amháin. Níos airde = níos tapúla ach níos mó cuimhne. Molta: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Líon réad le próiseáil i ngach baisc (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Líon réad le veicteoiriú in aon ghlao API amháin. Níos airde = níos tapúla ach níos mó cuimhne. Molta: 10-50.", + "Numeric Range:" : "Raon Uimhriúil:", + "Object" : "Réad", + "Object Collection" : "Bailiúchán Réad", + "Object Count Prediction" : "Tuar Líon Réad", + "Object ID" : "Aitheantas Réid", + "Object Management" : "Bainistíocht Réad", + "Object Vectorization" : "Veicteoiriú Réad", + "Object Warmup" : "Téamh Réad", + "Object collection not configured" : "Níl bailiúchán réad cumraithe", + "Object not found" : "Réad gan aimsiú", + "Object permanently deleted" : "Scriosadh an réad go buan", + "Object restored successfully" : "Athchóiríodh an réad go rathúil", + "Object settings updated successfully" : "Nuashonraíodh socruithe réid go rathúil", + "Object sources" : "Foinsí réad", + "Object unlocked successfully" : "Díghlasáladh an réad go rathúil", + "Object vectorization configuration saved successfully" : "Sábháladh cumraíocht veicteoirithe réad go rathúil", + "Object vectorized successfully" : "Veicteoiríodh an réad go rathúil", + "Object:" : "Réad:", + "Objects" : "Réada", + "Objects Analyzed" : "Réada Anailísithe", + "Objects Distribution" : "Dáileadh Réad", + "Objects being analyzed" : "Réada á n-anailísiú", + "Objects by Register" : "Réada de réir Cláir", + "Objects by Schema" : "Réada de réir Scéimre", + "Objects deletion completed successfully" : "Críochnaíodh scriosadh réad go rathúil", + "Objects in schema" : "Réada sa scéimre", + "Objects to Process:" : "Réada le Próiseáil:", + "Objects to be analyzed" : "Réada le hanailísiú", + "Objects to be deleted" : "Réada le scriosadh", + "Objects to be validated" : "Réada le bailíochtú", + "Objects will be serialized as JSON text before vectorization" : "Déanfar réada a shrathú mar théacs JSON roimh veicteoiriú", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Déanfar réada a bhoigscriosadh (marcáilte mar scriosta ach coinnithe sa bhunachar sonraí). Is féidir iad a aisghabháil níos déanaí más gá.", + "Old Value" : "Sean-Luach", + "Oldest Item (days)" : "Mír is Sine (laethanta)", + "Ollama Chat Settings" : "Socruithe Comhrá Ollama", + "Ollama Embedding Configuration" : "Cumraíocht Leabaithe Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Le hAthruithe Amháin", + "Open Register Objects" : "Réada Open Register", + "Open n8n Editor" : "Oscail Eagarthóir n8n", + "OpenAI Chat Settings" : "Socruithe Comhrá OpenAI", + "OpenAI Embedding Configuration" : "Cumraíocht Leabaithe OpenAI", + "OpenRegister Settings" : "Socruithe OpenRegister", + "Optional webhook secret for signature verification" : "Rún webhook roghnach le haghaidh fíorú sínithe", + "Order" : "Ord", + "Organisation" : "Eagraíocht", + "Organisation ID" : "Aitheantas Eagraíochta", + "Organisation Members" : "Baill Eagraíochta", + "Organisation Statistics" : "Staitisticí Eagraíochta", + "Organisation created successfully" : "Cruthaíodh an eagraíocht go rathúil", + "Organisation settings saved successfully" : "Sábháladh socruithe eagraíochta go rathúil", + "Organisations" : "Eagraíochtaí", + "Organization" : "Eagraíocht", + "Organization ID (Optional)" : "Aitheantas Eagraíochta (Roghnach)", + "Orphaned Items" : "Míreanna Dílleachta", + "Owner" : "Úinéir", + "Owner:" : "Úinéir:", + "Parallel Mode (Faster, more resource intensive)" : "Mód Comhthreomhar (Níos tapúla, níos déine ar acmhainní)", + "Parallel:" : "Comhthreomhar:", + "Password" : "Pasfhocal", + "Password for SOLR authentication (optional)" : "Pasfhocal le haghaidh fíordheimhnithe SOLR (roghnach)", + "Path" : "Conair", + "Path in repository" : "Conair sa stórlann", + "Path where the configuration file will be saved in the repository" : "Conair ina sábhálfar an comhad cumraíochta sa stórlann", + "Path where the register OAS file will be saved in the repository" : "Conair ina sábhálfar comhad OAS an chláir sa stórlann", + "Pattern (regex)" : "Patrún (regex)", + "Pattern Issue" : "Saincheist Patrúin", + "Pattern constraint is missing" : "Tá srianadh patrúin ar iarraidh", + "Patterns:" : "Patrúin:", + "Pending" : "Ar feitheamh", + "Permanent Delete Mode" : "Mód Scriosta Buan", + "Permanently Delete" : "Scrios go Buan", + "Permanently delete already soft-deleted objects" : "Scrios go buan réada atá boigscriosta cheana", + "Permanently delete this conversation?" : "Scrios an comhrá seo go buan?", + "Person" : "Duine", + "Personal Data" : "Sonraí Pearsanta", + "Phone" : "Fón", + "Photos" : "Grianghraif", + "Please create an agent in the" : "Cruthaigh gníomhaire le do thoil sa", + "Please select an agent to continue" : "Roghnaigh gníomhaire le do thoil chun leanúint ar aghaidh", + "Please select which register and schema to use for the new object" : "Roghnaigh le do thoil cén clár agus scéimre le húsáid don réad nua", + "Please try again later." : "Bain triail eile as níos déanaí le do thoil.", + "Please wait while we fetch your agents." : "Fan le do thoil agus muid ag fáil do ghníomhairí.", + "Please wait while we fetch your applications." : "Fan le do thoil agus muid ag fáil do fheidhmchláir.", + "Please wait while we fetch your configurations." : "Fan le do thoil agus muid ag fáil do chumraíochtaí.", + "Please wait while we fetch your deleted items." : "Fan le do thoil agus muid ag fáil do mhíreanna scriosta.", + "Please wait while we fetch your sources." : "Fan le do thoil agus muid ag fáil d'fhoinsí.", + "Polls" : "Pobalbhreitheanna", + "Popular Search Terms" : "Téarmaí Cuardaigh Móréilimh", + "Port" : "Port", + "Prerequisites" : "Réamhriachtanais", + "Press Enter to send, Shift+Enter for new line" : "Brúigh Enter chun seoladh, Shift+Enter le haghaidh líne nua", + "Previous" : "Roimhe Seo", + "Private" : "Príobháideach", + "Processes chunks in batches with simulated parallelism." : "Próiseálann sé smutáin i mbaisceanna le comhthreomhaireacht insamhalta.", + "Processes file chunks sequentially (safest)." : "Próiseálann sé smutáin chomhaid go seicheamhach (is sábháilte).", + "Processes objects in chunks with simulated parallelism." : "Próiseálann sé réada i smutáin le comhthreomhaireacht insamhalta.", + "Processes objects sequentially (safest)." : "Próiseálann sé réada go seicheamhach (is sábháilte).", + "Processing" : "Ag próiseáil", + "Processing Limits" : "Teorainneacha Próiseála", + "Processing..." : "Ag próiseáil...", + "Project initialized successfully" : "Túsaíodh an tionscadal go rathúil", + "Project not found. Please initialize first." : "Tionscadal gan aimsiú. Túsaigh ar dtús le do thoil.", + "Properties" : "Airí", + "Property Behaviors" : "Iompraíochtaí Airí", + "Property Configuration:" : "Cumraíocht Airí:", + "Property Filters" : "Scagairí Airí", + "Property Title" : "Teideal Airí", + "Property Type" : "Cineál Airí", + "Property can be improved" : "Is féidir an t-airí a fheabhsú", + "Protected" : "Cosanta", + "Provider" : "Soláthraí", + "Provider is required for testing" : "Tá soláthraí riachtanach le haghaidh tástála", + "Public" : "Poiblí", + "Public View" : "Amharc Poiblí", + "Public views can be accessed by anyone in the system" : "Is féidir le duine ar bith sa chóras rochtain a fháil ar amhairc phoiblí", + "Publish" : "Foilsigh", + "Publish Objects" : "Foilsigh Réada", + "Publish all objects in this schema" : "Foilsigh gach réad sa scéimre seo", + "Published" : "Foilsithe", + "Published:" : "Foilsithe:", + "Publishing..." : "Ag foilsiú...", + "Purge" : "Glan", + "Purge Date" : "Dáta Glanta", + "Query Complexity Distribution" : "Dáileadh Castachta Ceiste", + "Query parameter is required" : "Tá paraiméadar ceiste riachtanach", + "RAG" : "RAG", + "RAG Configuration" : "Cumraíocht RAG", + "Rate limits cleared successfully" : "Glanadh teorainneacha ráta go rathúil", + "Raw Changes Data" : "Sonraí Athruithe Amha", + "Re-vectorize on object update" : "Athveicteoirigh ar nuashonrú réid", + "Read" : "Léigh", + "Recommendations:" : "Moltaí:", + "Recommended Type:" : "Cineál Molta:", + "Recommended: 5 sources" : "Molta: 5 foinse", + "Refresh" : "Athnuaigh", + "Refresh Data" : "Athnuaigh Sonraí", + "Refresh Stats" : "Athnuaigh Staitisticí", + "Refresh Workflows" : "Athnuaigh Sreabháin Oibre", + "Refresh dashboard" : "Athnuaigh an deais", + "Refresh database info" : "Athnuaigh faisnéis an bhunachair sonraí", + "Register" : "Clár", + "Register ID" : "Aitheantas Cláir", + "Register Statistics" : "Staitisticí Cláir", + "Register Totals" : "Iomláin Cláir", + "Register depublished successfully" : "Dífhoilsíodh an clár go rathúil", + "Register not found" : "Clár gan aimsiú", + "Register objects deletion completed successfully" : "Críochnaíodh scriosadh réad an chláir go rathúil", + "Register published successfully" : "Foilsíodh an clár go rathúil", + "Register/Schema Usage" : "Úsáid Clár/Scéimre", + "Registers" : "Cláir", + "Reindex" : "Athinnéacsaigh", + "Reindex completed" : "Athinnéacsú críochnaithe", + "Reindex completed successfully" : "Críochnaíodh an t-athinnéacsú go rathúil", + "Reindex failed" : "Theip ar an athinnéacsú", + "Relations" : "Gaolta", + "Remote Version" : "Leagan Cianda", + "Remove" : "Bain", + "Remove filter" : "Bain scagaire", + "Remove from favorites" : "Bain ó na ceanáin", + "Remove group" : "Bain grúpa", + "Removed" : "Bainte", + "Removed from favorites" : "Bainte ó na ceanáin", + "Rename Conversation" : "Athainmnigh Comhrá", + "Rename conversation" : "Athainmnigh comhrá", + "Replicas" : "Macasamhla", + "Repository" : "Stórlann", + "Request Body" : "Corp Iarratais", + "Request Data" : "Sonraí Iarratais", + "Request Details" : "Sonraí Iarratais", + "Request body must be JSON with a \"query\" field" : "Caithfidh corp an iarratais a bheith JSON le réimse \"query\"", + "Request timeout in seconds" : "Teorainn ama iarratais i soicindí", + "Required field" : "Réimse riachtanach", + "Required status is inconsistent" : "Tá an stádas riachtanach neamhsheasmhach", + "Rerun Search" : "Athrith Cuardach", + "Reset Changes" : "Athshocraigh Athruithe", + "Reset Filters" : "Athshocraigh Scagairí", + "Response Body" : "Corp Freagartha", + "Response Details" : "Sonraí Freagartha", + "Response Time:" : "Am Freagartha:", + "Restore" : "Athchóirigh", + "Restore conversation" : "Athchóirigh comhrá", + "Restore or permanently delete items" : "Athchóirigh nó scrios míreanna go buan", + "Result Count Range" : "Raon Líon Torthaí", + "Results" : "Torthaí", + "Retrieve all objects for this schema" : "Aisghabh gach réad don scéimre seo", + "Retry" : "Atriail", + "Retry Failed Extractions" : "Atriail Eastóscaí a Theip", + "Retry Policy" : "Polasaí Atrialacha", + "Retry completed" : "Atriail críochnaithe", + "Retrying..." : "Ag atriail...", + "Risk Level" : "Leibhéal Riosca", + "Roles updated successfully" : "Nuashonraíodh róil go rathúil", + "SOLR Connection Settings" : "Socruithe Naisc SOLR", + "SOLR Version:" : "Leagan SOLR:", + "SOLR actions menu" : "Roghchlár gníomhartha SOLR", + "SOLR base path (usually /solr)" : "Bunchonair SOLR (de ghnáth /solr)", + "SOLR is disabled" : "Tá SOLR díchumasaithe", + "SOLR is not available or not configured" : "Níl SOLR ar fáil nó níl sé cumraithe", + "SOLR logging disabled" : "Díchumasaíodh logáil SOLR", + "SOLR logging enabled" : "Cumasaíodh logáil SOLR", + "SOLR search disabled" : "Díchumasaíodh cuardach SOLR", + "SOLR search enabled" : "Cumasaíodh cuardach SOLR", + "SOLR server hostname or IP address" : "Óstainm freastalaí SOLR nó seoladh IP", + "SOLR server port number (optional, defaults to 8983)" : "Uimhir phoirt fhreastalaí SOLR (roghnach, réamhshocrú go 8983)", + "SOLR setup completed successfully" : "Críochnaíodh socrú SOLR go rathúil", + "SOLR setup failed" : "Theip ar shocrú SOLR", + "SOLR setup failed - check logs" : "Theip ar shocrú SOLR - seiceáil na logaí", + "Sample Values:" : "Luachanna Samplacha:", + "Save" : "Sábháil", + "Save Configuration" : "Sábháil Cumraíocht", + "Save Connection Settings" : "Sábháil Socruithe Naisc", + "Save Roles" : "Sábháil Róil", + "Save Settings" : "Sábháil Socruithe", + "Save current search as view" : "Sábháil an cuardach reatha mar amharc", + "Saved Views" : "Amhairc Shábháilte", + "Saving..." : "Ag sábháil...", + "Schema" : "Scéimre", + "Schema ID" : "Aitheantas Scéimre", + "Schema Name" : "Ainm Scéimre", + "Schema Statistics" : "Staitisticí Scéimre", + "Schema depublished successfully" : "Dífhoilsíodh an scéimre go rathúil", + "Schema objects deletion completed successfully" : "Críochnaíodh scriosadh réad an scéimre go rathúil", + "Schema published successfully" : "Foilsíodh an scéimre go rathúil", + "Schema successfully created" : "Cruthaíodh an scéimre go rathúil", + "Schema successfully updated" : "Nuashonraíodh an scéimre go rathúil", + "Schemas" : "Scéimrí", + "Scheme" : "Scéim", + "Search / Views" : "Cuardach / Amhairc", + "Search Activity" : "Gníomhaíocht Chuardaigh", + "Search Analytics" : "Anailísíocht Chuardaigh", + "Search Entities" : "Cuardaigh Aonáin", + "Search Files" : "Cuardaigh Comhaid", + "Search GitHub" : "Cuardaigh GitHub", + "Search GitLab" : "Cuardaigh GitLab", + "Search Method" : "Modh Cuardaigh", + "Search Term" : "Téarma Cuardaigh", + "Search Trail Details" : "Sonraí Rian Cuardaigh", + "Search Trail Management" : "Bainistíocht Rian Cuardaigh", + "Search Trail Statistics" : "Staitisticí Rian Cuardaigh", + "Search Trails" : "Rianta Cuardaigh", + "Search Views" : "Cuardaigh Amhairc", + "Search Webhooks" : "Cuardaigh Webhooks", + "Search by file name or path" : "Cuardaigh de réir ainm nó conair comhaid", + "Search by name or URL" : "Cuardaigh de réir ainm nó URL", + "Search by value" : "Cuardaigh de réir luacha", + "Search in Files" : "Cuardaigh i gComhaid", + "Search in Objects" : "Cuardaigh i Réada", + "Search property names..." : "Cuardaigh ainmneacha airí...", + "Search trail deletion not implemented yet" : "Níl scriosadh rian cuardaigh curtha i bhfeidhm fós", + "Search views..." : "Cuardaigh amhairc...", + "Secret" : "Rún", + "Secret key for HMAC signature generation (optional)" : "Eochair rúin le haghaidh giniúint sínithe HMAC (roghnach)", + "Select AI Agent" : "Roghnaigh Gníomhaire AI", + "Select All" : "Roghnaigh Gach Rud", + "Select ConfigSet" : "Roghnaigh ConfigSet", + "Select File Types to Vectorize:" : "Roghnaigh Cineálacha Comhaid le Veicteoiriú:", + "Select HTTP method" : "Roghnaigh modh HTTP", + "Select Register and Schema" : "Roghnaigh Clár agus Scéimre", + "Select Views to Vectorize:" : "Roghnaigh Amhairc le Veicteoiriú:", + "Select a Nextcloud group to add" : "Roghnaigh grúpa Nextcloud le cur leis", + "Select a branch" : "Roghnaigh brainse", + "Select a model or type a custom model name" : "Roghnaigh múnla nó clóscríobh ainm múnla saincheaptha", + "Select a repository" : "Roghnaigh stórlann", + "Select a repository you have write access to" : "Roghnaigh stórlann a bhfuil rochtain scríofa agat air", + "Select an AI Agent" : "Roghnaigh Gníomhaire AI", + "Select an AI agent to begin chatting with your data." : "Roghnaigh gníomhaire AI chun tosú ag comhrá le do shonraí.", + "Select backend" : "Roghnaigh inneall", + "Select chat model" : "Roghnaigh múnla comhrá", + "Select collection for files" : "Roghnaigh bailiúchán do chomhaid", + "Select collection for objects" : "Roghnaigh bailiúchán do réada", + "Select default organisation" : "Roghnaigh eagraíocht réamhshocraithe", + "Select event to listen to..." : "Roghnaigh imeacht le héisteacht leis...", + "Select model" : "Roghnaigh múnla", + "Select options..." : "Roghnaigh roghanna...", + "Select period" : "Roghnaigh tréimhse", + "Select property to send as payload" : "Roghnaigh airí le seoladh mar phálasta", + "Select provider" : "Roghnaigh soláthraí", + "Select registers and schemas to save a view" : "Roghnaigh cláir agus scéimrí chun amharc a shábháil", + "Select retry policy" : "Roghnaigh polasaí atrialacha", + "Select the branch to publish to" : "Roghnaigh an brainse le foilsiú chuige", + "Select the event this webhook should listen to" : "Roghnaigh an t-imeacht ar chóir don webhook seo éisteacht leis", + "Select views to vectorize:" : "Roghnaigh amhairc le veicteoiriú:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Roghnaigh na grúpaí Nextcloud atá ar fáil don eagraíocht seo. Beidh rochtain ag úsáideoirí sna grúpaí seo ar acmhainní na heagraíochta.", + "Select which data views the AI can search" : "Roghnaigh na hamhairc sonraí is féidir leis an AI a chuardach", + "Select which property from the event should be used as the webhook payload data" : "Roghnaigh cén t-airí ón imeacht ba chóir a úsáid mar shonraí pálasta an webhook", + "Select which tools the AI can use to perform actions" : "Roghnaigh na huirlisí is féidir leis an AI a úsáid chun gníomhartha a dhéanamh", + "Selected" : "Roghnaithe", + "Selected Groups" : "Grúpaí Roghnaithe", + "Selected audit trails deleted successfully" : "Scriosadh na rianta iniúchta roghnaithe go rathúil", + "Selected search trails deleted successfully" : "Scriosadh na rianta cuardaigh roghnaithe go rathúil", + "Selected users can access this view" : "Is féidir le húsáideoirí roghnaithe rochtain a fháil ar an amharc seo", + "Send additional feedback" : "Seol aiseolas breise", + "Send as CloudEvent" : "Seol mar CloudEvent", + "Sensitive PII" : "PII Íogair", + "Serial Mode (Safer, slower)" : "Mód Srathach (Níos sábháilte, níos moille)", + "Serial:" : "Srathach:", + "Server Configuration" : "Cumraíocht Freastalaí", + "Server Information" : "Faisnéis Freastalaí", + "Settings" : "Socruithe", + "Shards" : "Blúirí", + "Share" : "Comhroinn", + "Share with Groups" : "Comhroinn le Grúpaí", + "Share with Users" : "Comhroinn le hÚsáideoirí", + "Show Filters" : "Taispeáin Scagairí", + "Show less" : "Taispeáin níos lú", + "Show only entries with changes" : "Taispeáin iontrálacha le hathruithe amháin", + "Simple" : "Simplí", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Ceisteanna simplí: Bunchuardaigh téacs le híosmhéid paraiméadar (m.sh., téarma cuardaigh aonair, gan scagairí ardleibhéil)", + "Size" : "Méid", + "Slug" : "Slug", + "Soft Delete Mode" : "Mód Boigscriosta", + "Soft Deleted Items" : "Míreanna Boigscriosta", + "SolrCloud mode enabled" : "Cumasaíodh mód SolrCloud", + "Source" : "Foinse", + "Sources" : "Foinsí", + "Sources:" : "Foinsí:", + "Standalone SOLR mode" : "Mód SOLR neamhspleách", + "Start Conversation" : "Tosaigh Comhrá", + "Start Vectorization" : "Tosaigh Veicteoiriú", + "Start a conversation" : "Tosaigh comhrá", + "Starting names cache warmup..." : "Ag tosú téamh taisce ainmneacha...", + "Starting..." : "Ag tosú...", + "Statistics" : "Staitisticí", + "Stats calculation not yet implemented" : "Níl ríomh staitisticí curtha i bhfeidhm fós", + "Status" : "Stádas", + "Status Code" : "Cód Stádais", + "String Constraints" : "Srianta Teaghráin", + "Success" : "Rath", + "Success Rate" : "Ráta Ratha", + "Success Status" : "Stádas Ratha", + "Successful" : "Rathúil", + "Successfully joined organisation" : "D'éirigh le bheith páirteach san eagraíocht", + "Sync Table" : "Sioncronaigh Tábla", + "Synchronization Settings" : "Socruithe Sioncronaithe", + "System Default" : "Réamhshocrú Córais", + "Technical Description" : "Cur Síos Teicniúil", + "Technical description for developers and administrators" : "Cur síos teicniúil d'fhorbróirí agus riarthóirí", + "Temperature" : "Teocht", + "Templates" : "Teimpléid", + "Test" : "Tástáil", + "Test Chat" : "Tástáil Comhrá", + "Test Connection" : "Tástáil Nasc", + "Test Embedding" : "Tástáil Leabú", + "Test endpoint executed successfully" : "Forghníomhaíodh an críochphointe tástála go rathúil", + "Test webhook delivered successfully" : "Seachadadh an webhook tástála go rathúil", + "Test webhook delivery failed" : "Theip ar sheachadadh an webhook tástála", + "Test webhook sent successfully" : "Seoladh an webhook tástála go rathúil", + "Testing..." : "Ag tástáil...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Gintear smutáin téacs le linn eastóscadh comhaid agus stóráiltear iad sa bhunachar sonraí. Léann veicteoiriú na comhaid réamh-smutánaithe seo agus tiontaíonn iad ina leabuithe.", + "Text extracted successfully" : "Eastósctaíodh an téacs go rathúil", + "Text extraction disabled" : "Díchumasaíodh eastóscadh téacs", + "The URL where webhook events will be sent" : "An URL ina seolfar imeachtaí webhook", + "The requested conversation does not exist" : "Níl an comhrá iarrtha ann", + "The saved view and all its search configuration" : "An t-amharc sábháilte agus a chumraíocht chuardaigh ar fad", + "There are no audit trail entries matching your current filters." : "Níl aon iontrálacha rian iniúchta ann a mheaitseálann do scagairí reatha.", + "There are no deleted items matching your current filters." : "Níl aon mhíreanna scriosta ann a mheaitseálann do scagairí reatha.", + "There are no search trail entries matching your current filters." : "Níl aon iontrálacha rian cuardaigh ann a mheaitseálann do scagairí reatha.", + "There are no webhook log entries matching your filters." : "Níl aon iontrálacha logála webhook ann a mheaitseálann do scagairí.", + "This action cannot be undone." : "Ní féidir an gníomh seo a chealú.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Ní féidir an gníomh seo a chealú. Cinntigh nach bhfuil aon bhailiúcháin ag baint úsáide as an ConfigSet seo.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Níl aon amhairc nó uirlisí inchumraithe ag an ngníomhaire seo, ach is féidir leat socruithe RAG a choigeartú thíos fós.", + "This analysis may take some time" : "D'fhéadfadh an anailís seo tamall a thógáil", + "This audit trail entry does not contain any change information." : "Níl aon fhaisnéis athraithe san iontráil rian iniúchta seo.", + "This configuration has no data defined." : "Níl aon sonraí sainithe ag an gcumraíocht seo.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Tá an críochphointe seo dímholta. Úsáid críochphointí smután-bhunaithe ina ionad.", + "This entity has no relations to objects or files" : "Níl aon ghaolta ag an aonán seo le réada nó comhaid", + "This is a test webhook from OpenRegister" : "Is webhook tástála é seo ó OpenRegister", + "This schema must use Magic Table configuration to sync" : "Caithfidh an scéimre seo cumraíocht Tábla Draíochta a úsáid chun sioncronú", + "This source has no associated registers." : "Níl aon chláir ghaolmhara ag an bhfoinse seo.", + "This will copy the _default ConfigSet with the new name" : "Cóipeálfaidh sé seo an ConfigSet _default leis an ainm nua", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Scriosfaidh sé seo GACH leabú (veicteoir) ón mbunachar sonraí go buan. Beidh ort gach réad agus comhad a athveicteoiriú. Ní féidir an gníomh seo a chealú.\\n\\nAn bhfuil tú cinnte gur mhaith leat leanúint ar aghaidh?", + "This will permanently delete:" : "Scriosfaidh sé seo go buan:", + "Timeout (seconds)" : "Teorainn Ama (soicindí)", + "Timestamp" : "Stampa Ama", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Leid: Ná cumasaigh ach amhairc a bhfuil cuardach séimeantach de dhíth orthu chun costais leabaithe a íoslaghdú. Is annamh a bhíonn veicteoiriú de dhíth ar tháblaí cuardaigh simplí.", + "Title" : "Teideal", + "To Date" : "Go Dáta", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Chun méid nó straitéis smután a choigeartú, téigh go Cumraíocht Comhaid → Teorainneacha Próiseála.", + "To change the embedding provider or model, go to LLM Configuration." : "Chun an soláthraí leabaithe nó an múnla a athrú, téigh go Cumraíocht LLM.", + "To date" : "Go dáta", + "Toggle search sidebar" : "Scoránaigh barra taoibh cuardaigh", + "Tools" : "Uirlisí", + "Top Deleters" : "Príomh-Scriostóirí", + "Total" : "Iomlán", + "Total Audit Trails" : "Iomlán Rianta Iniúchta", + "Total Chunks Available:" : "Iomlán Smután ar Fáil:", + "Total Deleted Items" : "Iomlán Míreanna Scriosta", + "Total Members" : "Iomlán Ball", + "Total Objects" : "Iomlán Réad", + "Total Objects in Database:" : "Iomlán Réad sa Bhunachar Sonraí:", + "Total Organisations" : "Iomlán Eagraíochtaí", + "Total Results" : "Iomlán Torthaí", + "Total Searches" : "Iomlán Cuardach", + "Total Size" : "Méid Iomlán", + "Total Storage" : "Stóráil Iomlán", + "Total:" : "Iomlán:", + "Totals" : "Iomláin", + "Type" : "Cineál", + "Type Issue" : "Saincheist Cineáil", + "Type Variations:" : "Athruithe Cineáil:", + "Type must be \"positive\" or \"negative\"" : "Caithfidh an cineál a bheith \"positive\" nó \"negative\"", + "Type to search for groups" : "Clóscríobh chun grúpaí a chuardach", + "Type to search for users" : "Clóscríobh chun úsáideoirí a chuardach", + "Type to search groups..." : "Clóscríobh chun grúpaí a chuardach...", + "Type to search users..." : "Clóscríobh chun úsáideoirí a chuardach...", + "URL" : "URL", + "URL where Ollama is running" : "URL ina bhfuil Ollama ag rith", + "UUID:" : "UUID:", + "Unchanged" : "Gan athrú", + "Unique Search Terms" : "Téarmaí Cuardaigh Uathúla", + "Unique Terms" : "Téarmaí Uathúla", + "Unknown" : "Anaithnid", + "Unknown Register" : "Clár Anaithnid", + "Unknown Schema" : "Scéimre Anaithnid", + "Untitled View" : "Amharc Gan Teideal", + "Update" : "Nuashonraigh", + "Update Operations" : "Oibríochtaí Nuashonraithe", + "Update configuration: ..." : "Nuashonraigh cumraíocht: ...", + "Update register OAS: ..." : "Nuashonraigh OAS cláir: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Nuashonraigh veicteoirí nuair a athraíonn sonraí réid (molta le haghaidh cuardach cruinn)", + "Updated" : "Nuashonraithe", + "Updated At" : "Nuashonraithe Ag", + "Updated:" : "Nuashonraithe:", + "Uptime:" : "Aga oibre:", + "Usage Count" : "Líon Úsáide", + "Use SolrCloud with Zookeeper for distributed search" : "Úsáid SolrCloud le Zookeeper le haghaidh cuardaigh dáilte", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Úsáid scagairí chun iontrálacha rian iniúchta a chúngú de réir cláir, scéimre, cineál gnímh, úsáideora, raon dátaí, nó aitheantas réid.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Úsáid scagairí chun míreanna scriosta a chúngú de réir cláir, scéimre, dáta scriosta, nó an úsáideoir a scrios iad.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Úsáid scagairí chun iontrálacha rian cuardaigh a chúngú de réir cláir, scéimre, stádas ratha, úsáideora, raon dátaí, téarmaí cuardaigh, nó méadrachtaí feidhmíochta.", + "Used By Collections" : "Úsáidte ag Bailiúcháin", + "User" : "Úsáideoir", + "User Agent Statistics" : "Staitisticí Gníomhaire Úsáideora", + "User Description" : "Cur Síos Úsáideora", + "User rate limits cleared successfully" : "Glanadh teorainneacha ráta úsáideora go rathúil", + "User removed successfully" : "Baineadh an t-úsáideoir go rathúil", + "User-friendly description shown in forms and help text" : "Cur síos úsáideoir-chairdiúil a thaispeántar i bhfoirmeacha agus i dtéacs cabhrach", + "Username" : "Ainm Úsáideora", + "Username for SOLR authentication (optional)" : "Ainm úsáideora le haghaidh fíordheimhnithe SOLR (roghnach)", + "Users" : "Úsáideoirí", + "Using Pre-Generated Chunks" : "Ag Úsáid Smután Réamhghinte", + "Validate" : "Bailíochtaigh", + "Validate Objects" : "Bailíochtaigh Réada", + "Validation completed successfully" : "Críochnaíodh an bhailíochtú go rathúil", + "Validation failed" : "Theip ar an mbailíochtú", + "Value" : "Luach", + "Vector Dimensions" : "Toisí Veicteora", + "Vector Search Backend" : "Inneall Cuardaigh Veicteora", + "Vector field: _embedding_" : "Réimse veicteora: _embedding_", + "Vectorization Triggers" : "Truicir Veicteoirithe", + "Vectorize All Files" : "Veicteoirigh Gach Comhad", + "Vectorize All Objects" : "Veicteoirigh Gach Réad", + "Vectorize all file types" : "Veicteoirigh gach cineál comhaid", + "Vectorize all views" : "Veicteoirigh gach amharc", + "Vectorize on object creation" : "Veicteoirigh ar chruthú réid", + "Vectors will be stored in your existing object and file collections" : "Stórálfar veicteoirí i do bhailiúcháin réad agus comhad atá ann cheana", + "Version" : "Leagan", + "Very High" : "An-Ard", + "View API Docs" : "Amharc ar Cháipéisí API", + "View Changes" : "Amharc ar Athruithe", + "View Details" : "Amharc ar Shonraí", + "View Error" : "Amharc ar Earráid", + "View File" : "Amharc ar Chomhad", + "View Full Details" : "Amharc ar Shonraí Iomlána", + "View Logs" : "Amharc ar Logaí", + "View Name" : "Ainm Amhairc", + "View Object" : "Amharc ar Réad", + "View Parameters" : "Amharc ar Pharaiméadair", + "View Selection" : "Roghnú Amhairc", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Amharc agus déan anailís ar logaí rian cuardaigh le scagadh ardleibhéil agus cumais anailísíochta", + "View and analyze system audit trails with advanced filtering capabilities" : "Amharc agus déan anailís ar rianta iniúchta córais le cumais scagtha ardleibhéil", + "View deleted successfully" : "Scriosadh an t-amharc go rathúil", + "View entity details and manage relations" : "Amharc ar shonraí aonáin agus bainistigh gaolta", + "View name" : "Ainm amhairc", + "View name is required" : "Tá ainm amhairc riachtanach", + "View saved successfully!" : "Sábháladh an t-amharc go rathúil!", + "View search analytics and manage search logs" : "Amharc ar anailísíocht chuardaigh agus bainistigh logaí cuardaigh", + "View successfully deleted" : "Scriosadh an t-amharc go rathúil", + "View successfully updated" : "Nuashonraíodh an t-amharc go rathúil", + "View updated successfully!" : "Nuashonraíodh an t-amharc go rathúil!", + "View webhook delivery logs and filter by webhook" : "Amharc ar logaí seachadta webhook agus scag de réir webhook", + "Views" : "Amhairc", + "Visible to users" : "Le feiceáil ag úsáideoirí", + "Wait for Response" : "Fan le Freagra", + "Wait for webhook response before continuing (required for request/response flows)" : "Fan le freagra webhook sula leanfaidh tú ar aghaidh (riachtanach do shreabháin iarratais/freagartha)", + "Warmup Names Cache" : "Téigh Taisce Ainmneacha", + "Warning" : "Rabhadh", + "Warning:" : "Rabhadh:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Sonraí Logála Webhook", + "Webhook Logs" : "Logaí Webhook", + "Webhook created successfully" : "Cruthaíodh an webhook go rathúil", + "Webhook deleted" : "Scriosadh an webhook", + "Webhook retry delivered successfully" : "Seachadadh atriail webhook go rathúil", + "Webhook retry delivery failed" : "Theip ar sheachadadh atriail webhook", + "Webhook updated" : "Nuashonraíodh an webhook", + "Webhook updated successfully" : "Nuashonraíodh an webhook go rathúil", + "Webhooks" : "Webhooks", + "Weekly" : "Seachtainiúil", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Fill pálasta webhook i bhformáid CloudEvents le haghaidh idir-inoibritheachta níos fearr", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Tá", + "You" : "Tusa", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Is féidir leat ConfigSets nua a chruthú bunaithe ar an teimpléad _default, nó cinn shaincheaptha a uaslódáil go díreach chuig do fhreastalaí SOLR.", + "You do not have access to this conversation" : "Níl rochtain agat ar an gcomhrá seo", + "You do not have permission to delete this conversation" : "Níl cead agat an comhrá seo a scriosadh", + "You do not have permission to modify this conversation" : "Níl cead agat an comhrá seo a athrú", + "You do not have permission to restore this conversation" : "Níl cead agat an comhrá seo a athchóiriú", + "You must be logged in to favorite views" : "Caithfidh tú a bheith logáilte isteach chun amhairc a chur leis na ceanáin", + "You need an AI agent to start a conversation." : "Teastaíonn gníomhaire AI uait chun comhrá a thosú.", + "Your Fireworks AI API key. Get one at" : "D'eochair API Fireworks AI. Faigh ceann ag", + "Your OpenAI API key. Get one at" : "D'eochair API OpenAI. Faigh ceann ag", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Taifeadadh d'aiseolas. Más mian leat, is féidir leat sonraí breise a sholáthar anseo...", + "Zookeeper Hosts" : "Óstaigh Zookeeper", + "Zookeeper Port" : "Port Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Socruithe Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Teaghrán naisc Zookeeper le haghaidh SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Uimhir phoirt Zookeeper (roghnach, réamhshocrú go 2181)", + "chunk" : "smután", + "chunks" : "smutáin", + "chunks processed" : "smutáin próiseáilte", + "configuration(s)" : "cumraíocht(aí)", + "e.g., lib/Settings/config.json" : "m.sh., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "m.sh., lib/Settings/register.json", + "failed" : "theip", + "file" : "comhad", + "files" : "comhaid", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "baill", + "menu or contact someone with permission to create agents." : "roghchlár nó déan teagmháil le duine éigin a bhfuil cead aige gníomhairí a chruthú.", + "messages" : "teachtaireachtaí", + "n8n URL and API key are required" : "Tá URL n8n agus eochair API riachtanach", + "n8n configuration saved successfully" : "Sábháladh cumraíocht n8n go rathúil", + "n8n connection not configured" : "Níl nasc n8n cumraithe", + "n8n connection successful" : "D'éirigh leis an nasc n8n", + "n8n connection test successful" : "D'éirigh le tástáil naisc n8n", + "n8n integration disabled" : "Díchumasaíodh comhtháthú n8n", + "n8n integration enabled" : "Cumasaíodh comhtháthú n8n", + "n8n project initialized successfully" : "Túsaíodh tionscadal n8n go rathúil", + "n8n settings saved successfully" : "Sábháladh socruithe n8n go rathúil", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "réada", + "objects processed" : "réada próiseáilte", + "or pick a range" : "nó roghnaigh raon", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "soláthraíonn sé cothromaíocht mhaith idir luas agus cruinneas.", + "register(s)" : "clár(acha)", + "register(s) selected" : "clár(acha) roghnaithe", + "required" : "riachtanach", + "results" : "torthaí", + "schema(s)" : "scéimre(í)", + "schema(s) selected" : "scéimre(í) roghnaithe", + "sk-..." : "sk-...", + "this application" : "an feidhmchlár seo", + "to" : "go", + "vectorized" : "veicteoirithe", + "views selected" : "amhairc roghnaithe", + "ℹ️ Current Configuration" : "ℹ️ Cumraíocht Reatha", + "⚡ Batch Processing" : "⚡ Próiseáil Bhaisce", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Comhthéacs cuimsitheach ✓ Níos lú dóchúil sonraí a chailleadh ✗ Freagraí níos moille ✗ D'fhéadfadh faisnéis nach bhfuil chomh hábhartha a áireamh", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Freagraí níos tapúla ✓ Freagraí níos dírithe ✗ D'fhéadfadh faisnéis ábhartha a chailleadh", + "✨ AI Features" : "✨ Gnéithe AI", + "All search trails cleared successfully" : "Glanadh gach rian cuardaigh go rathúil", + "Multiple search trail deletion not implemented yet" : "Níl scriosadh iliomad rian cuardaigh curtha i bhfeidhm fós", + "No expired search trails found to clear" : "Níor aimsíodh aon rianta cuardaigh as feidhm le glanadh", + "Missing message" : "Teachtaireacht ar iarraidh", + "message content is required" : "tá ábhar teachtaireachta riachtanach", + "Missing conversation or agentUuid" : "Comhrá nó agentUuid ar iarraidh", + "Access denied" : "Rochtain diúltaithe", + "Conversation not found" : "Comhrá gan aimsiú", + "AI service not configured" : "Níl seirbhís AI cumraithe", + "Failed to process message" : "Theip ar an teachtaireacht a phróiseáil", + "Missing conversationId" : "conversationId ar iarraidh", + "conversationId is required" : "tá conversationId riachtanach", + "Failed to fetch conversation history" : "Theip ar stair an chomhrá a fháil", + "Failed to clear conversation" : "Theip ar an gcomhrá a ghlanadh", + "Invalid feedback type" : "Cineál aiseolais neamhbhailí", + "type must be \"positive\" or \"negative\"" : "caithfidh an cineál a bheith \"positive\" nó \"negative\"", + "Message not found" : "Teachtaireacht gan aimsiú", + "Failed to save feedback" : "Theip ar aiseolas a shábháil", + "Failed to get chat statistics" : "Theip ar staitisticí comhrá a fháil", + "Not Found" : "Gan Aimsiú", + "Engine not found" : "Inneall gan aimsiú", + "Consumer not found" : "Tomhaltóir gan aimsiú", + "Backend parameter is required" : "Tá paraiméadar innill riachtanach", + "Failed to get database information: %s" : "Theip ar fhaisnéis an bhunachair sonraí a fháil: %s", + "SOLR setup error: %s" : "Earráid socraithe SOLR: %s", + "Reindex failed: %s" : "Theip ar an athinnéacsú: %s", + "Anonymized" : "Anaithnidithe", + "Entities detected" : "Aonáin braite", + "Extract Now" : "Eastóscadh Anois", + "Extracted at" : "Eastósctha ag", + "Extraction" : "Eastóscadh", + "Failed to load extraction data" : "Theip ar shonraí eastósctha a luchtú", + "Failed to load register data" : "Theip ar shonraí cláir a luchtú", + "Failed to retrieve extraction status." : "Theip ar stádas eastósctha a aisghabháil.", + "Failed to retrieve objects for file." : "Theip ar réada don chomhad a aisghabháil.", + "No extraction data available for this file" : "Níl aon sonraí eastósctha ar fáil don chomhad seo", + "No register objects reference this file" : "Ní thagraíonn aon réada cláir don chomhad seo", + "Not extracted" : "Gan eastóscadh", + "Register Objects" : "Réada Cláir", + "Risk level" : "Leibhéal riosca", + "Text chunks" : "Smutáin téacs", + "Unknown error" : "Earráid anaithnid", + "Very high" : "An-ard", + "{title} in {register} / {schema}" : "{title} i {register} / {schema}", + "Push Notifications" : "Fógraí Brúite", + "Real-time push notification status via notify_push" : "Stádas fógra brúite fíor-ama trí notify_push", + "Realtime push not available — the notify_push app is not installed" : "Níl brú fíor-ama ar fáil — níl an aip notify_push suiteáilte", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Suiteáil an aip notify_push ó Stór Aipeanna Nextcloud chun nuashonruithe fíor-ama a chumasú.", + "Open Nextcloud App Store" : "Oscail Stór Aipeanna Nextcloud", + "notify_push is installed but not yet active" : "Tá notify_push suiteáilte ach níl sé gníomhach fós", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "Tá notify_push suiteáilte ach níl deimhniú faighte ag OpenRegister go fóill ar bhrú rathúil. Truiceáil sábháil réid chun é a ghníomhachtú, nó seiceáil do chumraíocht notify_push.", + "notify_push configuration guide" : "treoir chumraíochta notify_push", + "Realtime push active" : "Brú fíor-ama gníomhach", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Tá fógraí brúite fíor-ama gníomhach. Faigheann cliaint cheangailte nuashonruithe láithreacha nuair a chruthaítear, a nuashonraítear, nó a scriostar réada.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);" +); diff --git a/l10n/ga.json b/l10n/ga.json new file mode 100644 index 0000000000..76b1b1cb12 --- /dev/null +++ b/l10n/ga.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin riachtanach)", + "(no subject)": "(gan ábhar)", + "(no title)": "(gan teideal)", + "(unnamed)": "(gan ainm)", + "(untitled card)": "(cárta gan teideal)", + "({count} selected)": "({count} roghnaithe)", + ", {failed} failed": ", theip ar {failed}", + "0 = unlimited": "0 = gan teorainn", + "A deactivation request is pending.": "Tá iarratas díghníomhachtaithe ar feitheamh.", + "AI Assistant": "Cúntóir AI", + "AI Chat": "Comhrá AI", + "AI service not configured": "Níl an tseirbhís AI cumraithe", + "API Key": "Eochair API", + "API Request Quota (requests/day)": "Cuóta Iarratas API (iarratais/lá)", + "API Token Configuration": "Cumraíocht Comhartha API", + "API Tokens": "Comharthaí API", + "API tokens saved successfully": "Sábháladh na comharthaí API go rathúil", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: nascann an tuarascáil seo gach verwerkingsactiviteit le líon na rianta iniúchta saoil a chuirtear i leith. Úsáideann iniúchóirí agus an Autoriteit Persoonsgegevens é seo le haghaidh athbhreithnithe maoirseachta.", + "About ConfigSets": "Maidir le ConfigSets", + "Access denied": "Diúltaíodh rochtain", + "Access organisation dashboard and analytics": "Rochtain ar dheais agus ar anailísíocht na heagraíochta", + "Account": "Cuntas", + "Action": "Gníomh", + "Action Distribution": "Dáileadh Gníomhartha", + "Action:": "Gníomh:", + "Actions": "Gníomhartha", + "Active": "Gníomhach", + "Active Collections": "Bailiúcháin Ghníomhacha", + "Active Filters": "Scagairí Gníomhacha", + "Active Organisation:": "Eagraíocht Ghníomhach:", + "Active Organisations": "Eagraíochtaí Gníomhacha", + "Active filters:": "Scagairí gníomhacha:", + "Active organisation changed successfully": "Athraíodh an eagraíocht ghníomhach go rathúil", + "Active organisation set successfully": "Socraíodh an eagraíocht ghníomhach go rathúil", + "Activities": "Gníomhaíochtaí", + "Activity": "Gníomhaíocht", + "Activity Period": "Tréimhse Ghníomhaíochta", + "Add": "Cuir Leis", + "Add Attachment": "Cuir Iatán Leis", + "Add Endpoint": "Cuir Críochphointe Leis", + "Add Groups": "Cuir Grúpaí Leis", + "Add Property to '{title}'": "Cuir Airí le '{title}'", + "Add Register": "Cuir Clár Leis", + "Add Schema": "Cuir Scéimre Leis", + "Add Source": "Cuir Foinse Leis", + "Add User": "Cuir Úsáideoir Leis", + "Add User to Organisation": "Cuir Úsáideoir leis an Eagraíocht", + "Add a contact from any of your address books to associate it with this object.": "Cuir teagmháil ó cheann ar bith de do leabhair seoltaí leis chun é a nascadh leis an réad seo.", + "Add a file or files": "Cuir comhad nó comhaid leis", + "Add card": "Cuir cárta leis", + "Add contact": "Cuir teagmháil leis", + "Add endpoint": "Cuir críochphointe leis", + "Add oneOf entry": "Cuir iontráil oneOf leis", + "Add range": "Cuir raon leis", + "Add schema titles, descriptions, and register information to provide richer context for search": "Cuir teidil scéimre, cur síos, agus eolas cláir leis chun comhthéacs níos saibhre a sholáthar don chuardach", + "Add to favorites": "Cuir leis na ceanáin", + "Added": "Curtha leis", + "Added to favorites": "Curtha leis na ceanáin", + "Additional Information": "Eolas Breise", + "Additional feedback saved. Thank you!": "Sábháladh aiseolas breise. Go raibh maith agat!", + "Addresses": "Seoltaí", + "Admin": "Riarthóir", + "Advanced": "Casta", + "Advanced Filters": "Scagairí Casta", + "Advanced Options": "Roghanna Casta", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Níl scagairí casta ar fáil agus foinse bunachair sonraí á húsáid. Athraigh go Auto nó SOLR Index le haghaidh roghanna scagtha.", + "After": "Tar éis", + "Agent Type": "Cineál Gníomhaire", + "Agent deleted successfully": "Scriosadh an gníomhaire go rathúil", + "Agent successfully created": "Cruthaíodh an gníomhaire go rathúil", + "Agent successfully updated": "Nuashonraíodh an gníomhaire go rathúil", + "Agents": "Gníomhairí", + "Aggregated across schemas": "Comhiomlánaithe trasna scéimrí", + "All": "Uile", + "All Categories": "Gach Catagóir", + "All Collections": "Gach Bailiúchán", + "All Confidence Levels": "Gach Leibhéal Muiníne", + "All ConfigSets": "Gach ConfigSet", + "All Files": "Gach Comhad", + "All Levels": "Gach Leibhéal", + "All Types": "Gach Cineál", + "All Webhooks": "Gach Webhook", + "All actions": "Gach gníomh", + "All audit trails cleared successfully": "Glanadh gach rian iniúchta go rathúil", + "All clear": "Glan ar fad", + "All file types": "Gach cineál comhaid", + "All registers": "Gach clár", + "All schemas": "Gach scéimre", + "All search trails cleared successfully": "Glanadh gach rian cuardaigh go rathúil", + "All searches": "Gach cuardach", + "All users": "Gach úsáideoir", + "All-day events": "Imeachtaí lae iomláin", + "Allowed MIME Types": "Cineálacha MIME Ceadaithe", + "Already linked": "Nasctha cheana", + "An error occurred while clearing audit trails": "Tharla earráid agus rianta iniúchta á nglanadh", + "An error occurred while deleting the audit trail": "Tharla earráid agus an rian iniúchta á scriosadh", + "An error occurred while deleting the object": "Tharla earráid agus an réad á scriosadh", + "An error occurred while deleting the view": "Tharla earráid agus an t-amharc á scriosadh", + "An error occurred while permanently deleting the objects": "Tharla earráid agus na réada á scriosadh go buan", + "An error occurred while restoring the objects": "Tharla earráid agus na réada á n-athchóiriú", + "An error occurred while saving the organisation": "Tharla earráid agus an eagraíocht á sábháil", + "Analysis completed:": "Anailís críochnaithe:", + "Analysis steps:": "Céimeanna anailíse:", + "Analytics": "Anailísíocht", + "Analyze Objects": "Déan Anailís ar Réada", + "Analyze Properties": "Déan Anailís ar Airíonna", + "Analyze existing properties for improvement opportunities": "Déan anailís ar airíonna atá ann cheana le haghaidh deiseanna feabhsúcháin", + "Analyzing...": "Á anailísiú...", + "Anonymized": "Anaithnidithe", + "Anonymous Group": "Grúpa Anaithnid", + "Any favorites and sharing settings for this view": "Aon cheanáin agus socruithe comhroinnte don amharc seo", + "Any user": "Aon úsáideoir", + "App ID": "Aitheantas Aip", + "App store cache invalidated successfully": "Neamhbhailíodh taisce stór na n-aipeanna go rathúil", + "App store cache invalidated: {invalidated}": "Neamhbhailíodh taisce stór na n-aipeanna: {invalidated}", + "Application deleted successfully": "Scriosadh an feidhmchlár go rathúil", + "Application identifier for this configuration (optional)": "Aitheantóir feidhmchláir don chumraíocht seo (roghnach)", + "Application settings": "Socruithe feidhmchláir", + "Application successfully created": "Cruthaíodh an feidhmchlár go rathúil", + "Application successfully updated": "Nuashonraíodh an feidhmchlár go rathúil", + "Applications": "Feidhmchláir", + "Apply": "Cuir i bhFeidhm", + "Apply Changes": "Cuir Athruithe i bhFeidhm", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Cuir ról i bhfeidhm ar gach scéimre sa chlár seo nach bhfuil sáruithe údaraithe sainráite acu.", + "Approval Progress": "Dul Chun Cinn Faofa", + "Approve": "Faomh", + "Approved": "Faofa", + "Archive": "Cartlann", + "Archive conversation": "Cuir comhrá i gcartlann", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Cuir an verwerkingsactiviteit seo i gcartlann? Leanfaidh rónna an riain iniúchta ag tagairt dó.", + "Archived conversations are hidden from your active list": "Tá comhráite cartlannaithe i bhfolach ó do liosta gníomhach", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "An bhfuil tú cinnte gur mhaith leat an bailiúchán \"{name}\" a SCRIOSADH?\n\nDéanfaidh sé seo:\n• An bailiúchán agus a shonraí go léir a scriosadh go buan\n• Gach doiciméad innéacsaithe a bhaint\n• Ní féidir an gníomh seo a chealú", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "An bhfuil tú cinnte gur mhaith leat seanrianta cuardaigh a ghlanadh? Scriosfaidh sé seo iontrálacha atá níos sine ná 30 lá.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "An bhfuil tú cinnte gur mhaith leat na sonraí go léir a ghlanadh ón mbailiúchán \"{name}\"?\n\nDéanfaidh sé seo:\n• Gach doiciméad innéacsaithe a scriosadh\n• Struchtúr an bhailiúcháin a choinneáil slán\n• Ní féidir an gníomh seo a chealú", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "An bhfuil tú cinnte gur mhaith leat an feidhmchlár \"{name}\" a scriosadh? Ní féidir an gníomh seo a chealú.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "An bhfuil tú cinnte gur mhaith leat na rianta iniúchta roghnaithe a scriosadh? Ní féidir an gníomh seo a chealú.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "An bhfuil tú cinnte gur mhaith leat na rianta cuardaigh roghnaithe a scriosadh? Ní féidir an gníomh seo a chealú.", + "Are you sure you want to delete this ConfigSet?": "An bhfuil tú cinnte gur mhaith leat an ConfigSet seo a scriosadh?", + "Are you sure you want to delete this configset?": "An bhfuil tú cinnte gur mhaith leat an ConfigSet seo a scriosadh?", + "Are you sure you want to leave '{name}'?": "An bhfuil tú cinnte gur mhaith leat '{name}' a fhágáil?", + "Are you sure you want to permanently delete": "An bhfuil tú cinnte gur mhaith leat scriosadh go buan", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "An bhfuil tú cinnte gur mhaith leat an bailiúchán \"{name}\" a athinnéacsú?\n\nDéanfaidh sé seo:\n• An t-innéacs a atógáil leis na réada go léir\n• Roinnt nóiméad a thógáil le críochnú\n• Seans go gcuirfidh sé isteach ar fheidhmíocht cuardaigh le linn athinnéacsaithe", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "An bhfuil tú cinnte gur mhaith leat an scéimre \"{schema}\" a bhaint ón gclár \"{register}\"? Ní féidir an gníomh seo a chealú.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "An bhfuil tú cinnte gur mhaith leat {count} réad ghníomhacha a bhog-scriosadh don scéimre \"{schema}\"?\n\nMarcálfar mar scriosta iad ach is féidir iad a bhaint go buan níos déanaí.", + "Array Object Configuration:": "Cumraíocht Réad Eagair:", + "Articles": "Ailt", + "Ask a question...": "Cuir ceist...", + "Ask questions about your data using natural language": "Cuir ceisteanna faoi do shonraí ag baint úsáide as teanga nádúrtha", + "Assigned collaborative tags": "Clibeanna comhoibríocha sannta", + "Assignment notifications": "Fógraí sannta", + "Attachment added to {name}": "Cuireadh iatán le {name}", + "Attempt": "Iarracht", + "Audit Trail Changes": "Athruithe ar Rian Iniúchta", + "Audit Trail Details": "Sonraí an Riain Iniúchta", + "Audit Trail Management": "Bainistíocht Riain Iniúchta", + "Audit Trail Statistics": "Staidreamh Riain Iniúchta", + "Audit Trails": "Rianta Iniúchta", + "Audit trail data copied to clipboard": "Cóipeáladh sonraí an riain iniúchta chuig an ngearrthaisce", + "Audit trail deleted successfully": "Scriosadh an rian iniúchta go rathúil", + "Audit trail successfully deleted": "Scriosadh an rian iniúchta go rathúil", + "Authentication": "Fíordheimhniú", + "Auto (e.g. Y for year, F Y for month)": "Auto (e.g. Y for year, F Y for month)", + "Auto share": "Comhroinn go huathoibríoch", + "Auto-commit disabled": "Díchumasaíodh uath-thiomnú", + "Auto-commit enabled": "Cumasaíodh uath-thiomnú", + "Auto-create Default Organisation": "Cruthaigh Eagraíocht Réamhshocraithe go hUathoibríoch", + "Auto-retry failed vectorizations": "Atriail go huathoibríoch veicteoiriúcháin a theip", + "Automatically commit changes to SOLR index": "Tiomnaigh athruithe go huathoibríoch chuig an innéacs SOLR", + "Automatically connect to the federation network.": "Ceangail go huathoibríoch leis an líonra cónaidhme.", + "Automatically create a default organisation if none exists when the app is initialized": "Cruthaigh eagraíocht réamhshocraithe go huathoibríoch mura bhfuil aon cheann ann nuair a thúsaítear an aip", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Gin leabuithe veicteora go huathoibríoch ó smutáin téacs nuair a uaslódáiltear agus a phróiseáiltear comhaid", + "Automatically generate vector embeddings when objects are created or updated": "Gin leabuithe veicteora go huathoibríoch nuair a chruthaítear nó a nuashonraítear réada", + "Automatically retry failed vectorization attempts (max 3 retries)": "Atriail go huathoibríoch iarrachtaí veicteoiriúcháin a theip (uasmhéid 3 atriail)", + "Automatically update catalogues.": "Nuashonraigh catalóga go huathoibríoch.", + "Available Filters": "Scagairí ar Fáil", + "Available Workflows": "Sreafaí Oibre ar Fáil", + "Available placeholders:": "Áitchoinneálaithe ar fáil:", + "Avatar": "Abhatár", + "Avatar changes are not supported by your authentication provider.": "Ní thacaíonn do sholáthraí fíordheimhnithe le hathruithe abhatáir.", + "Avatar removed": "Baineadh an t-abhatár", + "Avatar updated successfully": "Nuashonraíodh an t-abhatár go rathúil", + "Avg Execution Time": "Meánam Forghníomhaithe", + "Avg Members/Org": "Meán Ball/Eagr", + "Avg Object Views/Session": "Meán Amharc Réad/Seisiún", + "Avg Response Time": "Meánam Freagartha", + "Avg Results/Search": "Meántorthaí/Cuardach", + "Avg Searches/Session": "Meánchuardaigh/Seisiún", + "Back": "Ar Ais", + "Back to Entities": "Ar Ais go hAonáin", + "Back to Registers": "Ar Ais go Cláir", + "Back to Webhooks": "Ar Ais go Webhooks", + "Back to applications": "Ar ais go feidhmchláir", + "Back to entities": "Ar ais go haonáin", + "Backend parameter is required": "Tá an paraiméadar inneall riachtanach", + "Backend updated successfully. Please reload the application.": "Nuashonraíodh an t-inneall go rathúil. Athluchtaigh an feidhmchlár le do thoil.", + "Bandwidth Quota (MB/month)": "Cuóta Bandaleithid (MB/mí)", + "Base URL (Optional)": "URL Bonn (Roghnach)", + "Basic Information": "Bunfhaisnéis", + "Batch Size": "Méid Baisce", + "Batch extraction completed": "Eastóscadh baisce críochnaithe", + "Before": "Roimh", + "Before object vectorization can work:": "Sular féidir le veicteoiriúchán réad oibriú:", + "Behavior": "Iompar", + "Behavior Issue": "Fadhb Iompair", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Tá stóráil blob scortha. Úsáideann gach réad táblaí draíochta anois.", + "Bookmarks": "Leabharmharcanna", + "Branch": "Brainse", + "Bulk Role Assignment: {title}": "Sannadh Róil ar Mhórchóir: {title}", + "Bulk delete operation completed successfully": "Críochnaíodh an oibríocht scriosta ar mhórchóir go rathúil", + "Bulk save operation completed successfully": "Críochnaíodh an oibríocht sábhála ar mhórchóir go rathúil", + "Business Data": "Sonraí Gnó", + "CSV": "CSV", + "Cache cleared successfully": "Glanadh an taisce go rathúil", + "Cache grew from {old} to {new} entries.": "D'fhás an taisce ó {old} go {new} iontráil.", + "Cache shrunk from {old} to {new} entries.": "Laghdaigh an taisce ó {old} go {new} iontráil.", + "Cache stayed the same at {size} entries.": "D'fhan an taisce mar an gcéanna ag {size} iontráil.", + "Calculate Sizes": "Ríomh Méideanna", + "Calendar": "Calendar", + "Calendar Provider Configuration": "Cumraíocht Soláthraí Calendar", + "Calendar integration is not available": "Níl comhtháthú Calendar ar fáil", + "Calendar name": "Ainm an Calendar", + "Cancel": "Cealaigh", + "Cancel deactivation request": "Cealaigh iarratas díghníomhachtaithe", + "Cannot delete: objects are still attached": "Ní féidir scriosadh: tá réada ceangailte fós", + "Cannot edit: This register is managed by external configuration {title}": "Ní féidir eagarthóireacht: tá an clár seo á bhainistiú ag cumraíocht sheachtrach {title}", + "Cannot remove schema with existing objects ({count} objects)": "Ní féidir scéimre a bhaint le réada atá ann cheana ({count} réad)", + "Cannot remove schema {schema} because it contains {count} objects": "Ní féidir an scéimre {schema} a bhaint mar go bhfuil {count} réad ann", + "Cards": "Cártaí", + "Cascade delete": "Scriosadh cascáideach", + "Categories of data subjects (one per line)": "Catagóirí ábhar sonraí (ceann amháin in aghaidh na líne)", + "Categories of personal data (one per line)": "Catagóirí sonraí pearsanta (ceann amháin in aghaidh na líne)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Catagóir", + "Change Type": "Cineál Athraithe", + "Change password": "Athraigh pasfhocal", + "Changes": "Athruithe", + "Changes copied to clipboard": "Cóipeáladh athruithe chuig an ngearrthaisce", + "Changes for Audit Trail #{id}": "Athruithe don Rian Iniúchta #{id}", + "Chat Model": "Múnla Comhrá", + "Chat Provider": "Soláthraí Comhrá", + "Chat Provider (RAG)": "Soláthraí Comhrá (RAG)", + "Chat Settings": "Socruithe Comhrá", + "Chat provider connection successful!": "D'éirigh le ceangal an tsoláthraí comhrá!", + "Chat settings": "Socruithe comhrá", + "Chat test failed: {error}": "Theip ar thástáil comhrá: {error}", + "Choose a register": "Roghnaigh clár", + "Choose a schema": "Roghnaigh scéimre", + "Choose how vector similarity calculations are performed for semantic search": "Roghnaigh conas a dhéantar ríomhaireachtaí cosúlachta veicteora le haghaidh cuardach séimeantach", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Roghnaigh na cineálacha comhaid le cur san áireamh sa phróiseas veicteoiriúcháin. Ní phróiseálfar ach comhaid a bhfuil téacs eastósctha agus smutáin acu.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Roghnaigh na hamhairc le cur san áireamh sa phróiseas veicteoiriúcháin. Fág folamh chun gach amharc a phróiseáil bunaithe ar do chumraíocht.", + "Chunk": "Smután", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Níl scriosadh smutáin curtha i bhfeidhm fós. Úsáid críochphointí smután-bhunaithe.", + "Chunks": "Smutáin", + "Chunks to Vectorize:": "Smutáin le Veicteoiriú:", + "Cleanup Old Trails": "Glan Seanrianta", + "Cleanup completed": "Glanadh críochnaithe", + "Cleanup completed successfully. Deleted {count} entries.": "Críochnaíodh an glanadh go rathúil. Scriosadh {count} iontráil.", + "Cleanup failed: {error}": "Theip ar an nglanadh: {error}", + "Clear All Embeddings": "Glan Gach Leabú", + "Clear Entries": "Glan Iontrálacha", + "Clear Filtered Audit Trails": "Glan Rianta Iniúchta Scagtha", + "Clear Filters": "Glan Scagairí", + "Clear Index": "Glan Innéacs", + "Clear Selection": "Glan Roghnúchán", + "Clear all": "Glan uile", + "Clear all filters": "Glan gach scagaire", + "Clear date range": "Glan raon dátaí", + "Clear filters": "Glan scagairí", + "Clearing...": "Á ghlanadh...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Cliceáil Rith scanadh comhlíonta chun scéimrí a aimsiú ina bhfuil PII braite ach nach bhfuil aon anótáil verwerkingsactiviteit ann.", + "Click to subscribe to notifications": "Cliceáil chun liostáil le fógraí", + "Click to unsubscribe from notifications": "Cliceáil chun díliostáil ó fhógraí", + "Close": "Dún", + "Cluster name": "Ainm braisle", + "Code": "Cód", + "Code (short readable key, e.g. v-2026-001)": "Cód (eochair ghairid inléite, e.g. v-2026-001)", + "Collapse details": "Laghdaigh sonraí", + "Collection Management": "Bainistíocht Bailiúcháin", + "Collection Name": "Ainm an Bhailiúcháin", + "Collection assignments updated successfully": "Nuashonraíodh sannacháin an bhailiúcháin go rathúil", + "Collection cleared successfully": "Glanadh an bailiúchán go rathúil", + "Collection copied successfully": "Cóipeáladh an bailiúchán go rathúil", + "Collection created successfully": "Cruthaíodh an bailiúchán go rathúil", + "Collection deleted successfully": "Scriosadh an bailiúchán go rathúil", + "Collection used to store and index file metadata and content": "Bailiúchán a úsáidtear chun meiteashonraí agus ábhar comhaid a stóráil agus a innéacsú", + "Collection used to store and index object data": "Bailiúchán a úsáidtear chun sonraí réad a stóráil agus a innéacsú", + "Collections:": "Bailiúcháin:", + "Color": "Dath", + "Comment...": "Trácht...", + "Commit Message": "Teachtaireacht Tiomnaithe", + "Commit Within (ms)": "Tiomnaigh Laistigh de (ms)", + "Commit message": "Teachtaireacht tiomnaithe", + "Compare current schema with real object data": "Cuir an scéimre reatha i gcomparáid le sonraí réada fíora", + "Completed": "Críochnaithe", + "Complex": "Casta", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Fiosrúcháin chasta: cuardaigh chasta le scagairí, oibreoirí, agus teaglaim chasta paraiméadar iolracha", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Fiosrúcháin chasta: cuardaigh chasta le scagairí, oibreoirí, agus teaglaim chasta paraiméadar iolracha", + "Compliance": "Comhlíonadh", + "Confidence Level": "Leibhéal Muiníne", + "Confidence Score": "Scór Muiníne", + "Confidence: {confidence}%": "Muinín: {confidence}%", + "Config must be provided as an object": "Ní mór an chumraíocht a sholáthar mar réad", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Bainistíocht ConfigSet", + "ConfigSet Name": "Ainm ConfigSet", + "ConfigSet created successfully": "Cruthaíodh an ConfigSet go rathúil", + "ConfigSet deleted successfully": "Scriosadh an ConfigSet go rathúil", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Sainíonn ConfigSets an scéimre agus an chumraíocht do do bhailiúcháin SOLR. Tá sainmhínithe réimsí, anailíseoirí, agus socruithe cuardaigh eile iontu.", + "Configuration": "Cumraíocht", + "Configuration Data": "Sonraí Cumraíochta", + "Configuration is up to date": "Tá an chumraíocht cothrom le dáta", + "Configuration saved": "Sábháladh an chumraíocht", + "Configuration saved successfully.": "Sábháladh an chumraíocht go rathúil.", + "Configuration type (default, application, etc.)": "Cineál cumraíochta (réamhshocraithe, feidhmchlár, srl.)", + "Configurations": "Cumraíochtaí", + "Configure API tokens for external service integrations": "Cumraigh comharthaí API le haghaidh comhtháthuithe seirbhíse seachtraí", + "Configure Apache SOLR search engine for advanced search capabilities": "Cumraigh inneall cuardaigh Apache SOLR le haghaidh cumais chuardaigh chasta", + "Configure Facets": "Cumraigh Gnéithe", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Cumraigh soláthraithe LLM (Múnla Mór Teanga) le haghaidh gnéithe cumhachtaithe ag AI lena n-áirítear cuardach séimeantach, leabuithe, agus comhrá.", + "Configure SOLR Facets": "Cumraigh Gnéithe SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Cumraigh bunsocruithe ceangail do do fhreastalaí SOLR lena n-áirítear roghanna fíordheimhnithe agus líonra. Úsáid na dialóga ar leithligh ConfigSet agus Bainistíocht Bailiúcháin chun croíthe agus bailiúcháin a bhainistiú.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Cumraigh bunsocruithe ceangail do do fhreastalaí SOLR lena n-áirítear roghanna fíordheimhnithe agus líonra. Úsáid na dialóga ar leithligh ConfigSet agus bainistíocht bailiúcháin chun croíthe agus bailiúcháin a bhainistiú.", + "Configure default organisation and organisation-related settings": "Cumraigh eagraíocht réamhshocraithe agus socruithe a bhaineann le heagraíochtaí", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Cumraigh conas a thiontaítear réada bunachair sonraí ina leabuithe veicteora le haghaidh cuardach séimeantach. Veicteoirítear réada go díreach gan gá le heastóscadh téacs.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Cumraigh conas a thiontaítear réada go téacs roimh veicteoiriú. Bíonn tionchar ag na socruithe seo ar cháilíocht agus comhthéacs an chuardaigh.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Cumraigh soláthraithe LLM (múnla mór teanga) le haghaidh gnéithe cumhachtaithe ag AI lena n-áirítear cuardach séimeantach, leabuithe, agus comhrá.", + "Configure multi-organization support and tenant isolation": "Cumraigh tacaíocht ileagraíochta agus leithlisiú tionónta", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Cumraigh paraiméadair le haghaidh veicteoiriú comhad. Ginfidh an próiseas seo leabuithe veicteora do gach smután comhaid eastósctha.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Cumraigh paraiméadair le haghaidh veicteoiriú réad. Ginfidh an próiseas seo leabuithe veicteora do gach réad a mheaitseálann do scagairí amhairc.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Cumraigh an scéimre seo chun réada a thaispeáint mar imeachtaí in aip Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Cumraigh na cineálacha sonraí le cuardach agus cé mhéad foinse le haisghabháil", + "Configuring field mappings...": "Mapálacha réimsí á gcumrú...", + "Confirm Account Deactivation": "Deimhnigh Díghníomhachtú Cuntais", + "Confirm Object Deletion": "Deimhnigh Scriosadh Réada", + "Confirm Object Restoration": "Deimhnigh Athchóiriú Réada", + "Confirm Permanent Object Deletion": "Deimhnigh Scriosadh Buan Réada", + "Confirm deactivation": "Deimhnigh díghníomhachtú", + "Connecting to SOLR cluster...": "Ag ceangal le braisle SOLR...", + "Connection Failed": "Theip ar an gCeangal", + "Connection Settings": "Socruithe Ceangail", + "Connection Successful!": "D'éirigh leis an gCeangal!", + "Connection failed": "Theip ar an gceangal", + "Connection protocol": "Prótacal ceangail", + "Connection successful": "D'éirigh leis an gceangal", + "Connection timeout in seconds": "Teorainn ama ceangail i soicindí", + "Constant delay between retries (5 minutes)": "Moill sheasta idir atrialacha (5 nóiméad)", + "Constraint Issue": "Fadhb Srianta", + "Consumer not found": "Tomhaltóir gan aimsiú", + "Contacts": "Contacts", + "Contacts integration is not available": "Níl comhtháthú Contacts ar fáil", + "Content Filters": "Scagairí Ábhair", + "Continue to Properties": "Lean ar aghaidh go hAiríonna", + "Contracts": "Conarthaí", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Rialaigh na hamhairc réad ar cheart iad a veicteoiriú chun costais API a laghdú. Ná veicteoirigh ach amhairc a bhaineann tairbhe as cuardach séimeantach.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Rialaigh na hamhairc agus na huirlisí is féidir leis an AI a úsáid sa chomhrá seo. De réir réamhshocraithe, tá gach cumas gníomhaire cumasaithe.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Rialaíonn randamacht (0 = cinntitheach, 2 = an-chruthaitheach)", + "Conversation ID is required": "Tá aitheantas an chomhrá riachtanach", + "Conversation archived": "Cuireadh an comhrá i gcartlann", + "Conversation archived successfully": "Cuireadh an comhrá i gcartlann go rathúil", + "Conversation cleared successfully": "Glanadh an comhrá go rathúil", + "Conversation deleted": "Scriosadh an comhrá", + "Conversation not found": "Comhrá gan aimsiú", + "Conversation permanently deleted": "Scriosadh an comhrá go buan", + "Conversation renamed": "Athainmníodh an comhrá", + "Conversation restored": "Athchóiríodh an comhrá", + "Conversation started with {agent}": "Tosaíodh comhrá le {agent}", + "Conversation title": "Teideal an chomhrá", + "Conversations": "Comhráite", + "Converting {schema} to blob storage...": "{schema} á thiontú go stóráil blob...", + "Converting {schema} to magic table...": "{schema} á thiontú go tábla draíochta...", + "Copied!": "Cóipeáilte!", + "Copy": "Cóipeáil", + "Copy Changes": "Cóipeáil Athruithe", + "Copy Collection": "Cóipeáil Bailiúchán", + "Copy Data": "Cóipeáil Sonraí", + "Copy Full Data": "Cóipeáil Sonraí Iomlána", + "Copy of {name}": "Cóip de {name}", + "Copy to clipboard": "Cóipeáil chuig an ngearrthaisce", + "Copying...": "Á chóipeáil...", + "Costs": "Costais", + "Count": "Líon", + "Counting objects...": "Réada á gcomhaireamh...", + "Create": "Cruthaigh", + "Create API Token": "Cruthaigh Comhartha API", + "Create Agent": "Cruthaigh Gníomhaire", + "Create Application": "Cruthaigh Feidhmchlár", + "Create Collection": "Cruthaigh Bailiúchán", + "Create ConfigSet": "Cruthaigh ConfigSet", + "Create New Collection": "Cruthaigh Bailiúchán Nua", + "Create New ConfigSet": "Cruthaigh ConfigSet Nua", + "Create Operations": "Cruthaigh Oibríochtaí", + "Create Organisation": "Cruthaigh Eagraíocht", + "Create Webhook": "Cruthaigh Webhook", + "Create a copy of collection:": "Cruthaigh cóip den bhailiúchán:", + "Create a new ConfigSet based on the _default template": "Cruthaigh ConfigSet nua bunaithe ar an teimpléad _default", + "Create a new SOLR collection from an existing ConfigSet": "Cruthaigh bailiúchán SOLR nua ó ConfigSet atá ann cheana", + "Create a new SOLR collection from an existing configset": "Cruthaigh bailiúchán SOLR nua ó ConfigSet atá ann cheana", + "Create a new configset based on the _default template": "Cruthaigh ConfigSet nua bunaithe ar an teimpléad _default", + "Create a new event or link an existing one from any of your calendars.": "Cruthaigh imeacht nua nó nasc ceann atá ann cheana ó cheann ar bith de do féilirí.", + "Create another": "Cruthaigh ceann eile", + "Create copies of": "Cruthaigh cóipeanna de", + "Create event": "Cruthaigh imeacht", + "Create new token": "Cruthaigh comhartha nua", + "Create or link a Deck card to track work on this object.": "Cruthaigh nó nasc cárta Deck chun obair ar an réad seo a rianú.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Cruthaigh an chéad verwerkingsactiviteit chun tús a chur le clibeáil rónna an riain iniúchta lena sannadh AVG Art 30.", + "Create your first AI agent to get started.": "Cruthaigh do chéad ghníomhaire AI chun tosú.", + "Created": "Cruthaithe", + "Created:": "Cruthaithe:", + "Creating search collections...": "Bailiúcháin chuardaigh á gcruthú...", + "Creating...": "Á chruthú...", + "Current": "Reatha", + "Current Filters": "Scagairí Reatha", + "Current Type:": "Cineál Reatha:", + "Current password": "Pasfhocal reatha", + "Custom API endpoint if using a different region": "Críochphointe API saincheaptha má úsáidtear réigiún difriúil", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Ceanntásca HTTP saincheaptha (ceann amháin in aghaidh na líne, formáid: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Ceanntásca HTTP saincheaptha (ceann amháin in aghaidh na líne, formáid: header-name: value)", + "Custom Ranges:": "Raonta Saincheaptha:", + "Custom display title for this facet": "Teideal taispeána saincheaptha don ghné seo", + "Custom pattern:": "Patrún saincheaptha:", + "DELETE COLLECTION": "SCRIOS BAILIÚCHÁN", + "DSAR": "DSAR", + "Daily": "Laethúil", + "Dashboard": "Deais", + "Dashboard not found": "Deais gan aimsiú", + "Data": "Sonraí", + "Data Source": "Foinse Sonraí", + "Data Sources": "Foinsí Sonraí", + "Data Views": "Amhairc Sonraí", + "Data sources": "Foinsí sonraí", + "Data type does not match observed values": "Ní mheaitseálann an cineál sonraí na luachanna a breathnaíodh", + "Data-subject access request": "Iarratas rochtana ó ábhar sonraí", + "Database URL": "URL Bunachair Sonraí", + "Database information refreshed": "Athnuadh faisnéis an bhunachair sonraí", + "Date Faceting:": "Gnéthú Dáta:", + "Date Range": "Raon Dátaí", + "Dates": "Dátaí", + "Deactivation request cancelled": "Cealaíodh an t-iarratas díghníomhachtaithe", + "Deactivation request submitted": "Cuireadh an t-iarratas díghníomhachtaithe isteach", + "Deck": "Deck", + "Deck integration is not available": "Níl comhtháthú Deck ar fáil", + "Default": "Réamhshocraithe", + "Default Object Tenant": "Tionónta Réad Réamhshocraithe", + "Default Organisation": "Eagraíocht Réamhshocraithe", + "Default Owner": "Úinéir Réamhshocraithe", + "Default User (for cron/background jobs)": "Úsáideoir Réamhshocraithe (do phoist cron/chúlra)", + "Default User Tenant": "Tionónta Úsáideora Réamhshocraithe", + "Default value": "Luach réamhshocraithe", + "Default value (Email)": "Luach réamhshocraithe (Ríomhphost)", + "Default value (Password)": "Luach réamhshocraithe (Pasfhocal)", + "Default value (Phone number)": "Luach réamhshocraithe (Uimhir ghutháin)", + "Default value (Regex)": "Luach réamhshocraithe (Regex)", + "Default: 5": "Réamhshocraithe: 5", + "Defaults to current user. Select a different user if needed.": "Réamhshocraítear don úsáideoir reatha. Roghnaigh úsáideoir difriúil más gá.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Dúbailtear moilleanna le gach iarracht (2, 4, 8 nóiméad...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Méadaíonn moilleanna go líneach (5, 10, 15 nóiméad...)", + "Delete": "Scrios", + "Delete ({count})": "Scrios ({count})", + "Delete Application": "Scrios Feidhmchlár", + "Delete Audit Trail": "Scrios Rian Iniúchta", + "Delete Collection": "Scrios Bailiúchán", + "Delete ConfigSet": "Scrios ConfigSet", + "Delete Objects": "Scrios Réada", + "Delete Operations": "Scrios Oibríochtaí", + "Delete SOLR Collection": "Scrios Bailiúchán SOLR", + "Delete View": "Scrios Amharc", + "Delete all objects in this schema": "Scrios gach réad sa scéimre seo", + "Delete permanently": "Scrios go buan", + "Delete this conversation?": "Scrios an comhrá seo?", + "Delete view": "Scrios amharc", + "Delete {count} object": "Scrios {count} réad", + "Delete {count} objects": "Scrios {count} réad", + "Deleted": "Scriosta", + "Deleted By": "Scriosta Ag", + "Deleted Date": "Dáta Scriosta", + "Deleted Items Management": "Bainistíocht Míreanna Scriosta", + "Deleted This Week": "Scriosta an tSeachtain Seo", + "Deleted Today": "Scriosta Inniu", + "Deleted:": "Scriosta:", + "Deleting SOLR Collection...": "Bailiúchán SOLR á scriosadh...", + "Deleting...": "Á scriosadh...", + "Deletion Date Range": "Raon Dátaí Scriosta", + "Deletion Statistics": "Staidreamh Scriosta", + "Deprecated": "Dímholta", + "Depublish": "Dífhoilsigh", + "Description": "Cur Síos", + "Description Template": "Teimpléad Cur Síos", + "Description shown as tooltip": "Cur síos a thaispeántar mar leid uirlise", + "Description:": "Cur Síos:", + "Detect data types and patterns": "Braith cineálacha agus patrúin sonraí", + "Detected At": "Braite Ag", + "Detected Format:": "Formáid Bhraite:", + "Detected Issues:": "Fadhbanna Braite:", + "Detected: {date}": "Braite: {date}", + "Direct permission": "Cead díreach", + "Disable": "Díchumasaigh", + "Disable this to make the agent": "Díchumasaigh é seo chun an gníomhaire a dhéanamh", + "Disabled": "Díchumasaithe", + "Discover Files": "Faigh Comhaid", + "Discovered Properties": "Airíonna a Aimsíodh", + "Display Format": "Formáid Taispeána", + "Display Name": "Ainm Taispeána", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "An bhfuil tú ag iarraidh gach iontráil scagtha den rian iniúchta a scriosadh go buan? Ní féidir an gníomh seo a chealú.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "An bhfuil tú ag iarraidh an iontráil seo den rian iniúchta a scriosadh go buan? Ní féidir an gníomh seo a chealú.", + "Documents": "Doiciméid", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "OCR Dolphin cumasaithe", + "Download API Spec": "Íoslódáil Sonraíocht API", + "Download API Specification": "Íoslódáil Sonraíocht API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Íoslódáil cóip de do shonraí pearsanta go léir atá stóráilte in OpenRegister (GDPR Airteagal 20).", + "Drag a file or files here": "Tarraing comhad nó comhaid anseo", + "Dry run completed: {count} fields would be created": "Críochnaíodh an tirimrith: chruthófaí {count} réimse", + "Dry run completed: {count} fields would be fixed": "Críochnaíodh an tirimrith: shocrófaí {count} réimse", + "Duration in seconds (optional)": "Fad i soicindí (roghnach)", + "Edit": "Cuir in Eagar", + "Edit Agent": "Cuir Gníomhaire in Eagar", + "Edit Application": "Cuir Feidhmchlár in Eagar", + "Edit Configuration": "Cuir Cumraíocht in Eagar", + "Edit Endpoint": "Cuir Críochphointe in Eagar", + "Edit Organisation": "Cuir Eagraíocht in Eagar", + "Edit Property '{key}' of '{title}'": "Cuir Airí '{key}' de '{title}' in Eagar", + "Edit Register": "Cuir Clár in Eagar", + "Edit Schema": "Cuir Scéimre in Eagar", + "Edit Source": "Cuir Foinse in Eagar", + "Edit View": "Cuir Amharc in Eagar", + "Edit Webhook": "Cuir Webhook in Eagar", + "Edit labels": "Cuir lipéid in eagar", + "Edit processing activity": "Cuir verwerkingsactiviteit in eagar", + "Edit tags for {name}": "Cuir clibeanna in eagar do {name}", + "Edit verwerkingsactiviteit": "Cuir verwerkingsactiviteit in eagar", + "Edit view": "Cuir amharc in eagar", + "Edit view details": "Cuir sonraí an amhairc in eagar", + "Effectiveness": "Éifeachtacht", + "Email": "Ríomhphost", + "Email Addresses": "Seoltaí Ríomhphoist", + "Email digest frequency": "Minicíocht achoimre ríomhphoist", + "Emails": "Ríomhphoist", + "Embedding Model": "Múnla Leabúcháin", + "Embedding Provider": "Soláthraí Leabúcháin", + "Embedding generated successfully": "Gineadh an leabú go rathúil", + "Embedding provider connection successful!": "D'éirigh le ceangal an tsoláthraí leabúcháin!", + "Embedding test failed: {error}": "Theip ar thástáil leabúcháin: {error}", + "Enable": "Cumasaigh", + "Enable RAG": "Cumasaigh RAG", + "Enable auto-creation": "Cumasaigh uath-chruthú", + "Enable automatic file vectorization": "Cumasaigh veicteoiriú comhad uathoibríoch", + "Enable automatic object vectorization": "Cumasaigh veicteoiriú réad uathoibríoch", + "Enable automatic synchronization": "Cumasaigh sioncronú uathoibríoch", + "Enable calendar provider": "Cumasaigh soláthraí féilire", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Cumasaigh logáil mhionsonraithe le haghaidh oibríochtaí SOLR (molta le haghaidh dífhabhtaithe)", + "Enable faceting": "Cumasaigh gnéthú", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Cumasaigh nó díchumasaigh gnéithe LLM. Cumraigh soláthraithe agus múnlaí ag baint úsáide as an gcnaipe Cumraíocht LLM thuas.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Cumasaigh nó díchumasaigh gnéithe LLM. Cumraigh soláthraithe agus múnlaí ag baint úsáide as an gcnaipe cumraíochta LLM thuas.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Cumasaigh nó díchumasaigh comhtháthú cuardaigh SOLR. Cumraigh socruithe ceangail ag baint úsáide as an gcnaipe Socruithe Ceangail thuas.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Cumasaigh nó díchumasaigh comhtháthú cuardaigh SOLR. Cumraigh socruithe ceangail ag baint úsáide as an gcnaipe socruithe ceangail thuas.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Cumasaigh nó díchumasaigh comhtháthú sreafa oibre n8n. Cumraigh socruithe ceangail thíos.", + "Enable or disable this webhook": "Cumasaigh nó díchumasaigh an webhook seo", + "Enable tools that allow the agent to interact with data through function calling.": "Cumasaigh uirlisí a cheadaíonn don ghníomhaire idirghníomhú le sonraí trí ghlao feidhme.", + "Enable vectorization for all existing and future views (may increase costs)": "Cumasaigh veicteoiriú do gach amharc atá ann cheana agus amach anseo (seans go méadóidh costais)", + "Enable write-back to target objects": "Cumasaigh scríobh ar ais chuig réada sprice", + "Enabled": "Cumasaithe", + "End Date Field": "Réimse Dáta Deiridh", + "Endpoint Path*": "Conair Chríochphointe*", + "Endpoints": "Críochphointí", + "Engine not found": "Inneall gan aimsiú", + "Enter ConfigSet name": "Iontráil ainm ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Iontráil ainmneacha úsáideora Nextcloud chun rochtain a thabhairt ar an ngníomhaire príobháideach seo", + "Enter agent description (optional)": "Iontráil cur síos gníomhaire (roghnach)", + "Enter agent name": "Iontráil ainm gníomhaire", + "Enter application description (optional)": "Iontráil cur síos feidhmchláir (roghnach)", + "Enter application name": "Iontráil ainm feidhmchláir", + "Enter collection name": "Iontráil ainm bailiúcháin", + "Enter configuration description (optional)": "Iontráil cur síos cumraíochta (roghnach)", + "Enter configuration title": "Iontráil teideal cumraíochta", + "Enter description (optional)...": "Iontráil cur síos (roghnach)...", + "Enter new collection name": "Iontráil ainm bailiúcháin nua", + "Enter object ID": "Iontráil aitheantas réada", + "Enter organisation description (optional)": "Iontráil cur síos eagraíochta (roghnach)", + "Enter organisation name": "Iontráil ainm eagraíochta", + "Enter search term": "Iontráil téarma cuardaigh", + "Enter search terms or leave empty to browse all": "Iontráil téarmaí cuardaigh nó fág folamh chun gach rud a bhrabhsáil", + "Enter system prompt for the agent": "Iontráil leid an chórais don ghníomhaire", + "Enter username and press Enter": "Iontráil ainm úsáideora agus brúigh Enter", + "Enter view name...": "Iontráil ainm amhairc...", + "Enter webhook name": "Iontráil ainm webhook", + "Enter your API key": "Iontráil d'eochair API", + "Enter your object here...": "Iontráil do réad anseo...", + "Enter your schema here...": "Iontráil do scéimre anseo...", + "Entities": "Aonáin", + "Entities detected": "Aonáin braite", + "Entity ID: {id}": "Aitheantas Aonáin: {id}", + "Entity Information": "Faisnéis Aonáin", + "Entity Recognition": "Aithint Aonán", + "Entity Recognition Method": "Modh Aithinte Aonán", + "Entity deleted successfully": "Scriosadh an t-aonán go rathúil", + "Entity not found": "Aonán gan aimsiú", + "Entries to be deleted:": "Iontrálacha le scriosadh:", + "Enum Issue": "Fadhb Enum", + "Enum constraint is missing": "Tá srian enum ar iarraidh", + "Erase (Art 17)": "Scrios (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Scrios {count} réad don ábhar seo? Logáiltear an gníomh seo sa rian iniúchta.", + "Erasure complete": "Scriosadh críochnaithe", + "Erasure preview": "Réamhamharc scriosta", + "Error": "Earráid", + "Error Details": "Sonraí Earráide", + "Error Information": "Faisnéis Earráide", + "Error Message": "Teachtaireacht Earráide", + "Error deleting audit trails: {error}": "Earráid agus rianta iniúchta á scriosadh: {error}", + "Error deleting search trails: {error}": "Earráid agus rianta cuardaigh á scriosadh: {error}", + "Error loading application": "Earráid agus feidhmchlár á luchtú", + "Error loading audit trails": "Earráid agus rianta iniúchta á luchtú", + "Error loading entity": "Earráid agus aonán á luchtú", + "Error loading search trails": "Earráid agus rianta cuardaigh á luchtú", + "Estimated Batches:": "Baisceanna Measta:", + "Estimated Cost:": "Costas Measta:", + "Estimated Duration:": "Fad Measta:", + "Event": "Imeacht", + "Event Property for Payload": "Airí Imeachta don Phálasta", + "Events": "Imeachtaí", + "Every schema with detected PII has a processing-activity annotation.": "Tá anótáil verwerkingsactiviteit ag gach scéimre a bhfuil PII braite ann.", + "Example": "Sampla", + "Example Value": "Luach Samplach", + "Example value for this property": "Luach samplach don airí seo", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Uasmhéid Eisiach", + "Exclusive Minimum": "Íosmhéid Eisiach", + "Exclusive maximum": "Uasmhéid eisiach", + "Exclusive minimum": "Íosmhéid eisiach", + "Execution Mode": "Mód Forghníomhaithe", + "Execution Time": "Am Forghníomhaithe", + "Execution Time Range": "Raon Ama Forghníomhaithe", + "Existing Improvements": "Feabhsúcháin atá ann cheana", + "Expand details": "Leathnaigh sonraí", + "Expiration": "Éag", + "Expires": "Éagann", + "Expires in (e.g., 90d)": "Éagann i (e.g., 90d)", + "Exponential": "Easpónantúil", + "Export": "Easpórtáil", + "Export \"{schema}\" objects from \"{register}\"": "Easpórtáil réada \"{schema}\" ó \"{register}\"", + "Export Configuration": "Easpórtáil Cumraíocht", + "Export Format:": "Formáid Easpórtála:", + "Export Objects": "Easpórtáil Réada", + "Export completed successfully": "Críochnaíodh an easpórtáil go rathúil", + "Export downloaded successfully": "Íoslódáladh an easpórtáil go rathúil", + "Export failed: {error}": "Theip ar an easpórtáil: {error}", + "Export is rate limited. Please try again later.": "Tá an easpórtáil teoranta de réir ráta. Bain triail eile as níos déanaí le do thoil.", + "Export my data": "Easpórtáil mo shonraí", + "Export, view, or delete audit trails": "Easpórtáil, féach, nó scrios rianta iniúchta", + "Exporting...": "Á easpórtáil...", + "Extend Schema": "Sín Scéimre", + "Extended": "Sínte", + "Extensions": "Eisínteachtaí", + "Extra Query Parameters": "Paraiméadair Bhreise Fhiosrúcháin", + "Extract Now": "Eastóscadh Anois", + "Extract Pending Files": "Eastóscadh Comhaid ar Feitheamh", + "Extract now": "Eastóscadh anois", + "Extract properties from each object": "Eastósc airíonna ó gach réad", + "Extracted At": "Eastósctha Ag", + "Extracted at": "Eastósctha ag", + "Extraction": "Eastóscadh", + "Extraction Mode": "Mód Eastósctha", + "Extraction Scope": "Scóip Eastósctha", + "Extraction Status": "Stádas Eastósctha", + "Extraction error for {file}: {error}": "Earráid eastósctha do {file}: {error}", + "Facet Description": "Cur Síos Gné", + "Facet Order": "Ord Gnéithe", + "Facet Title": "Teideal Gné", + "Facet Type": "Cineál Gné", + "Facet configuration updated successfully": "Nuashonraíodh cumraíocht na ngnéithe go rathúil", + "Facetable": "Ingnéthaithe", + "Faceting Configuration:": "Cumraíocht Gnéthaithe:", + "Facets discovered and configured successfully": "Aimsíodh agus cumraíodh gnéithe go rathúil", + "Facets discovered successfully": "Aimsíodh gnéithe go rathúil", + "Failed": "Theip", + "Failed to add attachment to object": "Theip ar iatán a chur leis an réad", + "Failed to add user to organisation": "Theip ar úsáideoir a chur leis an eagraíocht", + "Failed to analyze schema properties": "Theip ar anailís a dhéanamh ar airíonna scéimre", + "Failed to archive conversation": "Theip ar an gcomhrá a chur i gcartlann", + "Failed to calculate sizes": "Theip ar mhéideanna a ríomh", + "Failed to cancel deactivation": "Theip ar an díghníomhachtú a chealú", + "Failed to change active organisation: {error}": "Theip ar an eagraíocht ghníomhach a athrú: {error}", + "Failed to change password": "Theip ar an bpasfhocal a athrú", + "Failed to check version: {error}": "Theip ar an leagan a sheiceáil: {error}", + "Failed to clear audit trails: {error}": "Theip ar rianta iniúchta a ghlanadh: {error}", + "Failed to clear blob objects: {error}": "Theip ar réada blob a ghlanadh: {error}", + "Failed to clear cache: {error}": "Theip ar an taisce a ghlanadh: {error}", + "Failed to clear collection": "Theip ar an mbailiúchán a ghlanadh", + "Failed to clear conversation": "Theip ar an gcomhrá a ghlanadh", + "Failed to clear embeddings: {error}": "Theip ar leabuithe a ghlanadh: {error}", + "Failed to clear search trails: {error}": "Theip ar rianta cuardaigh a ghlanadh: {error}", + "Failed to clear {type} cache: {error}": "Theip ar thaisce {type} a ghlanadh: {error}", + "Failed to copy changes": "Theip ar athruithe a chóipeáil", + "Failed to copy data": "Theip ar shonraí a chóipeáil", + "Failed to copy data to clipboard": "Theip ar shonraí a chóipeáil chuig an ngearrthaisce", + "Failed to copy to clipboard": "Theip ar chóipeáil chuig an ngearrthaisce", + "Failed to copy token": "Theip ar an gcomhartha a chóipeáil", + "Failed to create conversation": "Theip ar chomhrá a chruthú", + "Failed to create missing SOLR fields: {error}": "Theip ar réimsí SOLR atá ar iarraidh a chruthú: {error}", + "Failed to create or find project": "Theip ar thionscadal a chruthú nó a aimsiú", + "Failed to create token": "Theip ar chomhartha a chruthú", + "Failed to delete SOLR collection: {error}": "Theip ar bhailiúchán SOLR a scriosadh: {error}", + "Failed to delete collection": "Theip ar an mbailiúchán a scriosadh", + "Failed to delete conversation": "Theip ar an gcomhrá a scriosadh", + "Failed to delete objects for {schema}: {error}": "Theip ar réada a scriosadh do {schema}: {error}", + "Failed to delete webhook": "Theip ar an webhook a scriosadh", + "Failed to depublish register: {error}": "Theip ar an gclár a dhífhoilsiú: {error}", + "Failed to depublish schema: {error}": "Theip ar an scéimre a dhífhoilsiú: {error}", + "Failed to depublish: {error}": "Theip ar an dífhoilsiú: {error}", + "Failed to discover files: {error}": "Theip ar chomhaid a aimsiú: {error}", + "Failed to download API specification": "Theip ar shonraíocht API a íoslódáil", + "Failed to export data": "Theip ar shonraí a easpórtáil", + "Failed to export objects": "Theip ar réada a easpórtáil", + "Failed to fetch conversation history": "Theip ar stair an chomhrá a fháil", + "Failed to fix mismatched SOLR fields: {error}": "Theip ar réimsí SOLR mímheaitseáilte a shocrú: {error}", + "Failed to get SOLR field configuration": "Theip ar chumraíocht réimsí SOLR a fháil", + "Failed to get chat statistics": "Theip ar staidreamh comhrá a fháil", + "Failed to get database information: %s": "Theip ar fhaisnéis bunachair sonraí a fháil: %s", + "Failed to get entity categories": "Theip ar chatagóirí aonán a fháil", + "Failed to get entity statistics": "Theip ar staidreamh aonán a fháil", + "Failed to get entity types": "Theip ar chineálacha aonán a fháil", + "Failed to get response: {error}": "Theip ar fhreagra a fháil: {error}", + "Failed to invalidate app store cache: {error}": "Theip ar thaisce stór na n-aipeanna a neamhbhailiú: {error}", + "Failed to leave organisation: {error}": "Theip ar an eagraíocht a fhágáil: {error}", + "Failed to link object": "Theip ar an réad a nascadh", + "Failed to load Deck cards": "Theip ar chártaí Deck a luchtú", + "Failed to load LLM configuration": "Theip ar chumraíocht LLM a luchtú", + "Failed to load Nextcloud groups": "Theip ar ghrúpaí Nextcloud a luchtú", + "Failed to load cache statistics: {error}": "Theip ar staidreamh taisce a luchtú: {error}", + "Failed to load conversation": "Theip ar an gcomhrá a luchtú", + "Failed to load entities": "Theip ar aonáin a luchtú", + "Failed to load entity": "Theip ar an aonán a luchtú", + "Failed to load extraction data": "Theip ar shonraí eastósctha a luchtú", + "Failed to load files": "Theip ar chomhaid a luchtú", + "Failed to load linked contacts": "Theip ar theagmhálacha nasctha a luchtú", + "Failed to load linked emails": "Theip ar ríomhphoist nasctha a luchtú", + "Failed to load linked events": "Theip ar imeachtaí nasctha a luchtú", + "Failed to load organisations": "Theip ar eagraíochtaí a luchtú", + "Failed to load register data": "Theip ar shonraí cláir a luchtú", + "Failed to load related entities": "Theip ar aonáin ghaolmhara a luchtú", + "Failed to load settings: {error}": "Theip ar shocruithe a luchtú: {error}", + "Failed to load statistics: {error}": "Theip ar staidreamh a luchtú: {error}", + "Failed to load templates": "Theip ar theimpléid a luchtú", + "Failed to load view: {error}": "Theip ar an amharc a luchtú: {error}", + "Failed to load webhooks": "Theip ar webhooks a luchtú", + "Failed to load workflows": "Theip ar shreafaí oibre a luchtú", + "Failed to process message": "Theip ar an teachtaireacht a phróiseáil", + "Failed to publish register: {error}": "Theip ar an gclár a fhoilsiú: {error}", + "Failed to publish schema: {error}": "Theip ar an scéimre a fhoilsiú: {error}", + "Failed to publish: {error}": "Theip ar an bhfoilsiú: {error}", + "Failed to refresh database information": "Theip ar fhaisnéis an bhunachair sonraí a athnuachan", + "Failed to reindex collection": "Theip ar an mbailiúchán a athinnéacsú", + "Failed to remove avatar": "Theip ar an abhatár a bhaint", + "Failed to remove link": "Theip ar an nasc a bhaint", + "Failed to remove member: {error}": "Theip ar an mball a bhaint: {error}", + "Failed to remove schema {schema}: {error}": "Theip ar an scéimre {schema} a bhaint: {error}", + "Failed to remove user from organisation": "Theip ar úsáideoir a bhaint ón eagraíocht", + "Failed to rename conversation": "Theip ar an gcomhrá a athainmniú", + "Failed to request deactivation": "Theip ar dhíghníomhachtú a iarraidh", + "Failed to restore conversation": "Theip ar an gcomhrá a athchóiriú", + "Failed to retrieve extraction status.": "Theip ar stádas eastósctha a aisghabháil.", + "Failed to retrieve objects for file.": "Theip ar réada a aisghabháil don chomhad.", + "Failed to retry extraction": "Theip ar an eastóscadh a atriail", + "Failed to retry webhook": "Theip ar an webhook a atriail", + "Failed to revoke token": "Theip ar an gcomhartha a chúlghairm", + "Failed to save GitHub token": "Theip ar an gcomhartha GitHub a shábháil", + "Failed to save GitLab URL": "Theip ar URL GitLab a shábháil", + "Failed to save GitLab token": "Theip ar an gcomhartha GitLab a shábháil", + "Failed to save LLM settings: {error}": "Theip ar shocruithe LLM a shábháil: {error}", + "Failed to save additional feedback": "Theip ar aiseolas breise a shábháil", + "Failed to save configuration.": "Theip ar an gcumraíocht a shábháil.", + "Failed to save configuration: {error}": "Theip ar an gcumraíocht a shábháil: {error}", + "Failed to save facet configuration": "Theip ar chumraíocht na ngnéithe a shábháil", + "Failed to save feedback": "Theip ar aiseolas a shábháil", + "Failed to save file settings: {error}": "Theip ar shocruithe comhaid a shábháil: {error}", + "Failed to save n8n configuration": "Theip ar chumraíocht n8n a shábháil", + "Failed to save preferences": "Theip ar shainroghanna a shábháil", + "Failed to save roles": "Theip ar róil a shábháil", + "Failed to save settings": "Theip ar shocruithe a shábháil", + "Failed to save settings: {error}": "Theip ar shocruithe a shábháil: {error}", + "Failed to save view: {error}": "Theip ar an amharc a shábháil: {error}", + "Failed to save warmup interval: {error}": "Theip ar an eatramh téimh a shábháil: {error}", + "Failed to save webhook": "Theip ar an webhook a shábháil", + "Failed to send feedback": "Theip ar aiseolas a sheoladh", + "Failed to set active organisation: {error}": "Theip ar an eagraíocht ghníomhach a shocrú: {error}", + "Failed to setup SOLR: {error}": "Theip ar SOLR a shocrú: {error}", + "Failed to start file warmup: {error}": "Theip ar théamh comhad a thosú: {error}", + "Failed to start processing {type} files: {error}": "Theip ar phróiseáil comhad {type} a thosú: {error}", + "Failed to start vectorization: {error}": "Theip ar veicteoiriú a thosú: {error}", + "Failed to switch organisation: {error}": "Theip ar eagraíocht a athrú: {error}", + "Failed to sync magic table for {schema}: {error}": "Theip ar an tábla draíochta a shioncronú do {schema}: {error}", + "Failed to test SOLR connection: {error}": "Theip ar cheangal SOLR a thástáil: {error}", + "Failed to test webhook": "Theip ar an webhook a thástáil", + "Failed to update LLM settings: {error}": "Theip ar shocruithe LLM a nuashonrú: {error}", + "Failed to update Multitenancy settings: {error}": "Theip ar shocruithe Iltionóntachta a nuashonrú: {error}", + "Failed to update RBAC settings: {error}": "Theip ar shocruithe RBAC a nuashonrú: {error}", + "Failed to update Retention settings: {error}": "Theip ar shocruithe Coinneála a nuashonrú: {error}", + "Failed to update SOLR settings: {error}": "Theip ar shocruithe SOLR a nuashonrú: {error}", + "Failed to update favorite status": "Theip ar stádas ceanáin a nuashonrú", + "Failed to update schema configuration for {schema}: {error}": "Theip ar chumraíocht scéimre a nuashonrú do {schema}: {error}", + "Failed to update schema properties": "Theip ar airíonna scéimre a nuashonrú", + "Failed to update view": "Theip ar an amharc a nuashonrú", + "Failed to update view: {error}": "Theip ar an amharc a nuashonrú: {error}", + "Failed to update webhook": "Theip ar an webhook a nuashonrú", + "Failed to upload avatar": "Theip ar an abhatár a uaslódáil", + "Failed to validate {schema}: {error}": "Theip ar {schema} a bhailíochtú: {error}", + "Failed to vectorize files: {error}": "Theip ar chomhaid a veicteoiriú: {error}", + "Failed to warmup names cache: {error}": "Theip ar thaisce na n-ainmneacha a théamh: {error}", + "Federation": "Cónaidhm", + "Feedback recorded": "Aiseolas taifeadta", + "Fewer sources (1-3):": "Níos lú foinsí (1-3):", + "Field": "Réimse", + "Fields to Display": "Réimsí le Taispeáint", + "File": "Comhad", + "File #{id}": "Comhad #{id}", + "File Chunk Prediction": "Tuar Smutáin Comhaid", + "File Collection": "Bailiúchán Comhad", + "File Handling": "Láimhseáil Comhad", + "File Location": "Suíomh Comhaid", + "File Management": "Bainistíocht Comhad", + "File Name": "Ainm Comhaid", + "File Path": "Conair Comhaid", + "File Type Selection": "Roghnú Cineáil Comhaid", + "File Types to Process": "Cineálacha Comhaid le Próiseáil", + "File Vectorization": "Veicteoiriú Comhad", + "File Warmup": "Téamh Comhad", + "File actions menu": "Roghchlár gníomhartha comhaid", + "File anonymized successfully": "Anaithnidíodh an comhad go rathúil", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Níl veicteoiriú smutáin comhaid curtha i bhfeidhm fós. Tá na smutáin réidh agus stóráilte, ach tá an tseirbhís veicteoiriúcháin á forbairt.", + "File collection not configured": "Níl bailiúchán comhad cumraithe", + "File discovery completed": "Aimsiú comhad críochnaithe", + "File extraction completed": "Eastóscadh comhaid críochnaithe", + "File extraction queued": "Eastóscadh comhaid i scuaine", + "File indexed successfully": "Innéacsaíodh an comhad go rathúil", + "File is already anonymized": "Tá an comhad anaithnidithe cheana", + "File name": "Ainm comhaid", + "File not found": "Comhad gan aimsiú", + "File settings saved successfully": "Sábháladh socruithe comhaid go rathúil", + "File settings updated successfully": "Nuashonraíodh socruithe comhaid go rathúil", + "File sources": "Foinsí comhad", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Veicteoiriú comhad críochnaithe! {vectorized} smután veicteoirithe ó {files} comhad. {failed} teipthe.", + "File vectorization configuration saved successfully": "Sábháladh cumraíocht veicteoirithe comhad go rathúil", + "File vectorization started. Check the statistics section for progress.": "Tosaíodh veicteoiriú comhad. Seiceáil an rannán staidrimh le haghaidh dul chun cinn.", + "File warmup completed": "Téamh comhad críochnaithe", + "Files": "Comhaid", + "Files added successfully": "Cuireadh comhaid leis go rathúil", + "Files with Completed Extraction:": "Comhaid a bhfuil Eastóscadh Críochnaithe acu:", + "Files → fileCollection, Objects → objectCollection": "Files → fileCollection, Objects → objectCollection", + "Files → filecollection, objects → objectcollection": "Files → filecollection, objects → objectcollection", + "Filter Audit Trails": "Scag Loirg Iniúchta", + "Filter Deleted Items": "Scag Míreanna Scriosta", + "Filter Objects": "Scag Réada", + "Filter Properties": "Scag Airíonna", + "Filter Search Trails": "Scag Loirg Chuardaigh", + "Filter Statistics": "Scag Staidreamh", + "Filter and analyze search trail entries": "Scag agus déan anailís ar iontrálacha loirg chuardaigh", + "Filter and manage audit trail entries": "Scag agus bainistigh iontrálacha loirg iniúchta", + "Filter and manage soft deleted items": "Scag agus bainistigh míreanna a scriosadh go bog", + "Filter and search entities": "Scag agus cuardaigh aonáin", + "Filter by object ID": "Scag de réir aitheantais réid", + "Filter by search term": "Scag de réir téarma cuardaigh", + "Filter by type": "Scag de réir cineáil", + "Filter by webhook": "Scag de réir Webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Lódáladh sonraí scagtha go huathoibríoch. Úsáid na scagairí thíos chun do chuardach a bheachtú.", + "Filter fields...": "Scag réimsí...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Scag truicir Webhook de réir airíonna an ualaigh íoctha (ceann sa líne, formáid: eochair: luach)", + "Filtered": "Scagtha", + "Filters": "Scagairí", + "Finalizing setup...": "Socrú á thabhairt chun críche...", + "Fireworks AI Chat Settings": "Socruithe Comhrá Fireworks AI", + "Fireworks AI Embedding Configuration": "Cumraíocht Leabaithe Fireworks AI", + "First": "An Chéad Cheann", + "Fixed": "Socraithe", + "For chat and retrieval-augmented generation": "Le haghaidh comhrá agus giniúint aisghabháil-fheabhsaithe", + "For vector embeddings and semantic search": "Le haghaidh leabuithe veicteoir agus cuardach séimeantach", + "Format": "Formáid", + "Format Issue": "Fadhb Fhormáide", + "Format constraint is missing": "Tá an srian formáide ar iarraidh", + "From (e.g. -7 days or 2025-01-01)": "Ó (e.g. -7 days or 2025-01-01)", + "From Date": "Ó Dháta", + "From date": "Ó dháta", + "Full data copied to clipboard": "Cóipeáladh na sonraí iomlána chuig an ngearrthaisce", + "Function Tools": "Uirlisí Feidhme", + "General Issue": "Fadhb Ghinearálta", + "Generate recommendations and confidence scores": "Gin moltaí agus scóir mhuiníne", + "Generate report": "Gin tuairisc", + "Generate the verantwoordingsdocument": "Gin an verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Gin veicteoirí láithreach nuair a chruthaítear réada nua", + "Generated: {time}": "Ginte: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Faigh léargais fhíor-ama ar shláinte sonraí d'eagraíochta trí dhíriú ar chláir, ar shainmhínithe scéimre, agus ar stóráil agus úsáid réad.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Faigh léargais fhíor-ama ar shláinte sonraí d'eagraíochta, ar threochtaí fáis, agus ar mhéadrachtaí cáilíochta.", + "GitHub Personal Access Token": "Comhartha Rochtana Pearsanta GitHub", + "GitHub token is valid": "Tá an comhartha GitHub bailí", + "GitHub token is valid! Username: {username}": "Tá an comhartha GitHub bailí! Ainm úsáideora: {username}", + "GitHub token removed successfully": "Baineadh comhartha GitHub go rathúil", + "GitHub token saved successfully": "Sábháladh comhartha GitHub go rathúil", + "GitHub token test failed: {message}": "Theip ar thástáil chomhartha GitHub: {message}", + "GitLab Personal Access Token": "Comhartha Rochtana Pearsanta GitLab", + "GitLab URL saved successfully": "Sábháladh URL GitLab go rathúil", + "GitLab token is valid": "Tá an comhartha GitLab bailí", + "GitLab token is valid! Username: {username}": "Tá an comhartha GitLab bailí! Ainm úsáideora: {username}", + "GitLab token saved successfully": "Sábháladh comhartha GitLab go rathúil", + "GitLab token test failed: {message}": "Theip ar thástáil chomhartha GitLab: {message}", + "Go to linked Register": "Téigh chuig an gClár nasctha", + "Go to linked Schema": "Téigh chuig an Scéimre nasctha", + "Go to organisation": "Téigh chuig an eagraíocht", + "Grant additional permissions beyond standard CRUD operations": "Deonaigh ceadanna breise lasmuigh d'oibríochtaí caighdeánacha CRUD", + "Group Quota": "Cuóta Grúpa", + "Groups": "Grúpaí", + "HTML preview": "Réamhamharc HTML", + "HTTP Method": "Modh HTTP", + "HTTP method used to send webhook requests": "Modh HTTP a úsáidtear chun iarratais Webhook a sheoladh", + "Headers": "Ceanntásca", + "Health": "Sláinte", + "Heartbeat successful - connection kept alive": "Buille croí rathúil - coinníodh an nasc beo", + "Helpful": "Cabhrach", + "Here you can configure the details for various connections.": "Anseo is féidir leat sonraí a chumrú le haghaidh naisc éagsúla.", + "Here you can configure the details for your organisation.": "Anseo is féidir leat sonraí d'eagraíochta a chumrú.", + "Hide": "Folaigh", + "Hide Filters": "Folaigh Scagairí", + "Hide in collection view": "Folaigh san amharc bailiúcháin", + "Hide in forms": "Folaigh i bhfoirmeacha", + "Hide in list view": "Folaigh san amharc liosta", + "High": "Ard", + "High Confidence": "Muinín Ard", + "Host": "Óstach", + "Hourly": "In aghaidh na huaire", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Cé chomh domhain is a thrasnaítear airíonna neadaithe réid (1-20). Gabhann luachanna níos airde níos mó sonraí ach méadaíonn siad méid an veicteora.", + "How often to check for updates (1-168 hours)": "Cé chomh minic is a sheiceáiltear le haghaidh nuashonruithe (1-168 uair)", + "How to handle retries for failed webhook deliveries": "Conas atriailacha a láimhseáil le haghaidh seachadtaí Webhook teipthe", + "IBANs": "IBANanna", + "ID": "ID", + "ID of input mapping (optional)": "ID an mhapála ionchuir (roghnach)", + "ID of output mapping (optional)": "ID an mhapála aschuir (roghnach)", + "ID of the target resource": "ID na hacmhainne sprice", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Glanadh teorainneacha ráta IP go rathúil", + "Identify properties not in the schema": "Aithin airíonna nach bhfuil sa scéimre", + "Immutable": "Do-athraithe", + "Import": "Iompórtáil", + "Import Configuration": "Iompórtáil Cumraíocht", + "Import Data into Register": "Iompórtáil Sonraí isteach sa Chlár", + "Import successful": "Iompórtáil rathúil", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Iompórtáil an teimpléad report-bundle.json chun an clár `reports` a fháil, ansin cruthaigh do chéad deais trí UI chaighdeánach na réad. Dearbhaíonn deaiseanna a ngiuirléidí in JSON agus cothaíonn an rindreálaí sonraí comhiomlánaithe beo do gach giuirléid.", + "Improved Property": "Airí Feabhsaithe", + "In use": "In úsáid", + "Inactive": "Neamhghníomhach", + "Inactive organisations cannot be used": "Ní féidir eagraíochtaí neamhghníomhacha a úsáid", + "Include IDs and names of related objects for better contextual search": "Cuir aitheantais agus ainmneacha réad gaolmhar san áireamh le haghaidh cuardach comhthéacsúil níos fearr", + "Include ids and names of related objects for better contextual search": "Cuir aitheantais agus ainmneacha réad gaolmhar san áireamh le haghaidh cuardach comhthéacsúil níos fearr", + "Include related object references": "Cuir tagairtí réad gaolmhar san áireamh", + "Include schema and register metadata": "Cuir meiteashonraí scéimre agus cláir san áireamh", + "Index": "Innéacs", + "Information about the current OpenRegister installation": "Faisnéis faoin tsuiteáil reatha OpenRegister", + "Inherited from register": "Le hoidhreacht ón gclár", + "Inherits permissions from register": "Faigheann sé ceadanna le hoidhreacht ón gclár", + "Initialization failed": "Theip ar an túsú", + "Initialize Project": "Túsaigh Tionscadal", + "Initializing SOLR setup...": "Socrú SOLR á thúsú...", + "Input Mapping": "Mapáil Ionchuir", + "Inspect Fields": "Iniúch Réimsí", + "Inspect Index": "Iniúch Innéacs", + "Inspect SOLR Index": "Iniúch Innéacs SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Suiteáil an aip notify_push ó Stór Aipeanna Nextcloud chun nuashonruithe fíor-ama a chumasú.", + "Integrations": "Comhtháthuithe", + "Internet location (URL) of this installation": "Suíomh idirlín (URL) na suiteála seo", + "Interval": "Eatramh", + "Invalid": "Neamhbhailí", + "Invalid batch size. Must be between 1 and 5000": "Méid baisce neamhbhailí. Caithfidh sé a bheith idir 1 agus 5000", + "Invalid feedback type": "Cineál aiseolais neamhbhailí", + "Invalid field name provided": "Soláthraíodh ainm réimse neamhbhailí", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects neamhbhailí. Caithfidh sé a bheith 0 (gach ceann) nó uimhir dheimhneach", + "Invalid:": "Neamhbhailí:", + "Invite Users": "Tabhair Cuireadh d'Úsáideoirí", + "Invited Users": "Úsáideoirí ar Tugadh Cuireadh dóibh", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Torthaí Inzage", + "Issue": "Fadhb", + "Items per page:": "Míreanna in aghaidh an leathanaigh:", + "Just now": "Anois díreach", + "Key": "Eochair", + "LLM Configuration": "Cumraíocht LLM", + "LLM actions menu": "Roghchlár gníomhartha LLM", + "LLM configuration saved successfully": "Sábháladh cumraíocht LLM go rathúil", + "LLM features disabled": "Gnéithe LLM díchumasaithe", + "LLM features enabled": "Gnéithe LLM cumasaithe", + "LLM must be enabled with an embedding provider configured": "Caithfidh LLM a bheith cumasaithe le soláthraí leabaithe cumraithe", + "LLM settings saved successfully": "Sábháladh socruithe LLM go rathúil", + "LLM settings updated successfully": "Nuashonraíodh socruithe LLM go rathúil", + "Label": "Lipéad", + "Labels": "Lipéid", + "Languages": "Teangacha", + "Last": "An Ceann Deireanach", + "Last Triggered": "Truicearáilte go Deireanach", + "Leave empty to allow all users access": "Fág folamh chun rochtain a cheadú do gach úsáideoir", + "Leave off for auto-detection from property format.": "Fág múchta le haghaidh uathbhrath ó fhormáid an airí.", + "Left organisation successfully": "D'fhág tú an eagraíocht go rathúil", + "Legal basis *": "Bunús dlí *", + "Length Range:": "Raon Faid:", + "Linear": "Líneach", + "Link": "Nasc", + "Link an email from the Mail app sidebar to associate it with this object.": "Nasc ríomhphost ó bharra taoibh na haipe Mail chun é a cheangal leis an réad seo.", + "Link an object to see it here.": "Nasc réad chun é a fheiceáil anseo.", + "Link another object": "Nasc réad eile", + "Link existing event": "Nasc imeacht atá ann cheana", + "Link removed": "Baineadh an nasc", + "Link to Object": "Nasc le Réad", + "Link to {name}": "Nasc le {name}", + "Linked Objects": "Réada Nasctha", + "Linked by {user}": "Nasctha ag {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Taispeánfar ríomhphoist nasctha, imeachtaí Calendar, teagmhálaithe agus cártaí Deck anseo.", + "Linked to {name}": "Nasctha le {name}", + "Load Advanced Filters": "Lódáil Scagairí Casta", + "Load advanced filters with live data from your search index": "Lódáil scagairí casta le sonraí beo ó d'innéacs cuardaigh", + "Load more": "Lódáil tuilleadh", + "Load view": "Lódáil amharc", + "Loading API tokens...": "Comharthaí API á lódáil...", + "Loading ConfigSets...": "ConfigSets á lódáil...", + "Loading LLM configuration...": "Cumraíocht LLM á lódáil...", + "Loading RBAC settings...": "Socruithe RBAC á lódáil...", + "Loading activity...": "Gníomhaíocht á lódáil...", + "Loading advanced filters...": "Scagairí casta á lódáil...", + "Loading agents...": "Gníomhairí á lódáil...", + "Loading application details...": "Sonraí feidhmchláir á lódáil...", + "Loading applications...": "Feidhmchláir á lódáil...", + "Loading archived conversations...": "Comhráite cartlannaithe á lódáil...", + "Loading audit trails...": "Loirg iniúchta á lódáil...", + "Loading available tools...": "Uirlisí atá ar fáil á lódáil...", + "Loading cache statistics...": "Staidreamh taisce á lódáil...", + "Loading collections...": "Bailiúcháin á lódáil...", + "Loading configurations...": "Cumraíochtaí á lódáil...", + "Loading conversation...": "Comhrá á lódáil...", + "Loading conversations...": "Comhráite á lódáil...", + "Loading deleted items...": "Míreanna scriosta á lódáil...", + "Loading endpoints": "Críochphointí á lódáil", + "Loading entities...": "Aonáin á lódáil...", + "Loading events...": "Imeachtaí á lódáil...", + "Loading filter data...": "Sonraí scagtha á lódáil...", + "Loading groups...": "Grúpaí á lódáil...", + "Loading linked objects...": "Réada nasctha á lódáil...", + "Loading log details...": "Sonraí logála á lódáil...", + "Loading multitenancy settings...": "Socruithe il-tionóntachta á lódáil...", + "Loading n8n configuration...": "Cumraíocht n8n á lódáil...", + "Loading permission matrix...": "Maitrís ceadanna á lódáil...", + "Loading preferences...": "Sainroghanna á lódáil...", + "Loading register data...": "Sonraí cláir á lódáil...", + "Loading registers...": "Cláir á lódáil...", + "Loading retention settings...": "Socruithe coinneála á lódáil...", + "Loading schemas...": "Scéimrí á lódáil...", + "Loading search configuration...": "Cumraíocht chuardaigh á lódáil...", + "Loading search trails...": "Loirg chuardaigh á lódáil...", + "Loading sources...": "Foinsí á lódáil...", + "Loading statistics...": "Staidreamh á lódáil...", + "Loading text extraction settings...": "Socruithe eastósctha téacs á lódáil...", + "Loading tokens...": "Comharthaí á lódáil...", + "Loading user groups...": "Grúpaí úsáideoirí á lódáil...", + "Loading users...": "Úsáideoirí á lódáil...", + "Loading views...": "Amhairc á lódáil...", + "Loading...": "Á lódáil...", + "Local": "Logánta", + "Local Version": "Leagan Logánta", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Aimsigh gach réad a thagraíonn d'ábhar sonraí (Art 15 inzage), déan réamhamharc ar léirscriosadh (Art 17 vergetelheid), nó easpórtáil a gcuid sonraí (Art 20 portabiliteit).", + "Location": "Suíomh", + "Location Field": "Réimse Suímh", + "Locations": "Suíomhanna", + "Locked": "Faoi Ghlas", + "Locked:": "Faoi Ghlas:", + "Login successful": "Logáil isteach rathúil", + "Logs": "Logaí", + "Low": "Íseal", + "Low Confidence": "Muinín Íseal", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Taispeántar uimhreacha níos ísle ar dtús sa bharra taoibh scagaire. Fág folamh le haghaidh ordú uathoibríoch.", + "Lucene Version:": "Leagan Lucene:", + "Magic table sync completed for {schema}": "Sioncronú tábla draíochta críochnaithe do {schema}", + "Magic table synchronized successfully": "Sioncronaíodh an tábla draíochta go rathúil", + "Mail Integration": "Comhtháthú Mail", + "Mail integration is not available": "Níl comhtháthú Mail ar fáil", + "Make this installation discoverable within the federation network.": "Déan an tsuiteáil seo inaimsithe laistigh den líonra cónaidhme.", + "Manage Organisation Roles": "Bainistigh Róil Eagraíochta", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Bainistigh Bailiúcháin SOLR (stórais sonraí) agus sann iad le haghaidh réad agus comhad.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Bainistigh ConfigSets SOLR (teimpléid chumraíochta) le haghaidh do bhailiúchán.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Bainistigh bailiúcháin SOLR (stórais sonraí) agus sann iad le haghaidh réad agus comhad.", + "Manage SOLR configsets (configuration templates) for your collections.": "Bainistigh ConfigSets SOLR (teimpléid chumraíochta) le haghaidh do bhailiúchán.", + "Manage and configure agents for automated tasks": "Bainistigh agus cumraigh gníomhairí le haghaidh tascanna uathoibrithe", + "Manage and monitor file text extraction status": "Bainistigh agus déan monatóireacht ar stádas eastósctha téacs comhad", + "Manage and restore soft deleted items from your registers": "Bainistigh agus athchóirigh míreanna a scriosadh go bog ó do chláir", + "Manage and view detected entities from files and objects": "Bainistigh agus féach ar aonáin a braitheadh ó chomhaid agus ó réada", + "Manage document templates and themes": "Bainistigh teimpléid agus téamaí cáipéise", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Bainistigh sreafaí oibre n8n le haghaidh uathoibriú OpenRegister. Stórálfar sreafaí oibre sa tionscadal cumraithe.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Bainistigh gníomhaíochtaí próiseála, rith iarratais rochtana ábhair sonraí, agus déan iniúchadh ar chomhlíonadh faoin GDPR an AE / AVG na hÍsiltíre.", + "Manage webhooks for event-driven integrations": "Bainistigh Webhooks le haghaidh comhtháthuithe imeacht-tiomáinte", + "Manage your account settings, security, and personal data.": "Bainistigh socruithe do chuntais, slándáil, agus sonraí pearsanta.", + "Manage your applications and modules": "Bainistigh d'fheidhmchláir agus do mhodúil", + "Manage your chat conversations": "Bainistigh do chomhráite", + "Manage your data registers and their configurations": "Bainistigh do chláir sonraí agus a gcumraíochtaí", + "Manage your data schemas and their properties": "Bainistigh do scéimrí sonraí agus a n-airíonna", + "Manage your data sources and their configurations": "Bainistigh d'fhoinsí sonraí agus a gcumraíochtaí", + "Manage your organisations and switch between them": "Bainistigh d'eagraíochtaí agus aistrigh eatarthu", + "Manage your saved search configurations": "Bainistigh do chumraíochtaí cuardaigh sábháilte", + "Manage your system configurations and settings": "Bainistigh do chumraíochtaí agus socruithe córais", + "Managed": "Bainistithe", + "Mappings": "Mapálacha", + "Mass validation completed successfully": "Bailíochtú oll-mhéide críochnaithe go rathúil", + "Mass validation failed: {error}": "Theip ar bhailíochtú oll-mhéide: {error}", + "Max Files (0 = all)": "Uasmhéid Comhad (0 = gach ceann)", + "Max Length": "Uasfhad", + "Max Length:": "Uasfhad:", + "Max Objects (0 = all)": "Uasmhéid Réad (0 = gach ceann)", + "Max Retries": "Uas-atriailacha", + "Max Shards/Node": "Uasmhéid Blúirí/Nód", + "Max Tokens": "Uas-Chomharthaí", + "Max execution time (ms)": "Uas-am rite (ms)", + "Max ms": "Uas ms", + "Max result count": "Uaslíon torthaí", + "Max results": "Uasmhéid torthaí", + "Maximum": "Uasmhéid", + "Maximum File Size (MB)": "Uasmhéid Comhaid (MB)", + "Maximum Nesting Depth": "Uasdoimhneacht Neadaithe", + "Maximum length": "Uasfhad", + "Maximum length constraint is missing": "Tá an srian uasfhaid ar iarraidh", + "Maximum length is too restrictive": "Tá an uasfhad ró-shriantach", + "Maximum number of files to process. Set to 0 to process all files.": "Uaslíon na gcomhad le próiseáil. Socraigh go 0 chun gach comhad a phróiseáil.", + "Maximum number of items": "Uaslíon míreanna", + "Maximum number of objects to process. Set to 0 to process all objects.": "Uaslíon na réad le próiseáil. Socraigh go 0 chun gach réad a phróiseáil.", + "Maximum number of retry attempts for failed deliveries": "Uaslíon iarrachtaí atriailacha le haghaidh seachadtaí teipthe", + "Maximum time to wait before committing changes": "Uas-am le fanacht sula gcuirtear athruithe i bhfeidhm", + "Maximum tokens to generate": "Uas-chomharthaí le giniúint", + "Maximum value": "Uasluach", + "Maximum value constraint is missing": "Tá an srian uasluacha ar iarraidh", + "Maximum value is too restrictive": "Tá an uasluach ró-shriantach", + "Medium": "Meánach", + "Medium Confidence": "Muinín Mheánach", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Fiosrúcháin mheánacha: Cuardaigh le roinnt scagtha nó le hilpharaiméadair (e.g., raonta dáta, cláir/scéimrí ar leith)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Fiosrúcháin mheánacha: cuardaigh le roinnt scagtha nó le hilpharaiméadair (e.g., raonta dáta, cláir/scéimrí ar leith)", + "Member removed successfully": "Baineadh an ball go rathúil", + "Members": "Baill", + "Members ({count})": "Baill ({count})", + "Members of selected groups can access this view": "Is féidir le baill na ngrúpaí roghnaithe rochtain a fháil ar an amharc seo", + "Members:": "Baill:", + "Memory prediction calculated successfully": "Ríomhadh tuar cuimhne go rathúil", + "Message content is required": "Tá ábhar teachtaireachta ag teastáil", + "Message does not belong to this conversation": "Ní bhaineann an teachtaireacht leis an gcomhrá seo", + "Message not found": "Teachtaireacht gan aimsiú", + "Metadata": "Meiteashonraí", + "Metadata Filters": "Scagairí Meiteashonraí", + "Method": "Modh", + "Method*": "Modh*", + "Method: {method}": "Modh: {method}", + "Min Length": "Íosfhad", + "Min execution time (ms)": "Íos-am rite (ms)", + "Min ms": "Íos ms", + "Min result count": "Íoslíon torthaí", + "Min results": "Íosmhéid torthaí", + "Minimum": "Íosmhéid", + "Minimum length": "Íosfhad", + "Minimum number of items": "Íoslíon míreanna", + "Minimum value": "Íosluach", + "Minimum value constraint is missing": "Tá an srian íosluacha ar iarraidh", + "Minimum value is too restrictive": "Tá an t-íosluach ró-shriantach", + "Missing conversation or agentUuid": "Comhrá nó agentUuid ar iarraidh", + "Missing conversationId": "conversationId ar iarraidh", + "Missing message": "Teachtaireacht ar iarraidh", + "Missing object fields created successfully": "Cruthaíodh réimsí réid atá ar iarraidh go rathúil", + "Mode:": "Mód:", + "Model": "Múnla", + "Model Name": "Ainm an Mhúnla", + "Modified": "Athraithe", + "Monthly": "In aghaidh na míosa", + "More sources (10-20):": "Tuilleadh foinsí (10-20):", + "Most Active Objects": "Na Réada is Gníomhaí", + "Multiple Of": "Iolraí De", + "Multiple of": "Iolraí de", + "Multiple search trail deletion not implemented yet": "Níl scriosadh il-lorg cuardaigh curtha i bhfeidhm fós", + "Multitenancy": "Il-tionóntacht", + "Multitenancy settings updated successfully": "Nuashonraíodh socruithe il-tionóntachta go rathúil", + "My Account": "Mo Chuntas", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Ainm", + "Name *": "Ainm *", + "Name*": "Ainm*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Téadh taisce na n-ainmneacha go rathúil: {count} ainm lódáilte in {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Téadh taisce na n-ainmneacha go rathúil: {count} ainm lódáilte in {time}. {message}", + "Names cache warmup completed": "Téamh thaisce na n-ainmneacha críochnaithe", + "Naming pattern for copies:": "Patrún ainmnithe le haghaidh cóipeanna:", + "New Collection Name": "Ainm Bailiúcháin Nua", + "New Configuration": "Cumraíocht Nua", + "New Conversation": "Comhrá Nua", + "New Properties": "Airíonna Nua", + "New Property": "Airí Nua", + "New User Group": "Grúpa Úsáideoirí Nua", + "New Value": "Luach Nua", + "New activity": "Gníomhaíocht nua", + "New password": "Pasfhocal nua", + "New processing activity": "Gníomhaíocht phróiseála nua", + "New users without specific organisation membership will be automatically added to this organisation": "Cuirfear úsáideoirí nua gan ballraíocht eagraíochta ar leith leis an eagraíocht seo go huathoibríoch", + "New verwerkingsactiviteit": "verwerkingsactiviteit nua", + "Next": "Ar Aghaidh", + "Nextcloud Groups": "Grúpaí Nextcloud", + "No": "Níl", + "No API tokens.": "Gan comharthaí API.", + "No ConfigSets found": "Níor aimsíodh aon ConfigSets", + "No Deck cards linked to this object": "Níl aon chárta Deck nasctha leis an réad seo", + "No GitHub token configured": "Níl aon chomhartha GitHub cumraithe", + "No GitHub token provided": "Níor soláthraíodh aon chomhartha GitHub", + "No GitLab token provided": "Níor soláthraíodh aon chomhartha GitLab", + "No Organisation": "Gan Eagraíocht", + "No active filters": "Gan scagairí gníomhacha", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Níl aon réad gníomhach le scriosadh go bog don scéimre {schema}. Úsáid \"Scrios go Buan\" chun réada a scriosadh go bog a bhaint.", + "No activity data available": "Níl aon sonraí gníomhaíochta ar fáil", + "No activity found.": "Níor aimsíodh aon ghníomhaíocht.", + "No agents available": "Níl aon ghníomhaire ar fáil", + "No agents found": "Níor aimsíodh aon ghníomhaire", + "No app store cache exists yet": "Níl aon taisce stóir aipeanna ann fós", + "No applications are available.": "Níl aon fheidhmchlár ar fáil.", + "No applications found": "Níor aimsíodh aon fheidhmchlár", + "No approval steps for this object.": "Níl aon chéim cheadaithe don réad seo.", + "No archived conversations": "Gan comhráite cartlannaithe", + "No audit trail entries found": "Níor aimsíodh aon iontráil lorg iniúchta", + "No changes recorded": "Níor taifeadadh aon athrú", + "No chunks to vectorize": "Gan smután le veicteoiriú", + "No collections found": "Níor aimsíodh aon bhailiúchán", + "No configuration data": "Gan sonraí cumraíochta", + "No configuration selected": "Níor roghnaíodh aon chumraíocht", + "No configurations are available.": "Níl aon chumraíocht ar fáil.", + "No configurations found": "Níor aimsíodh aon chumraíocht", + "No configurations found for this application.": "Níor aimsíodh aon chumraíocht don fheidhmchlár seo.", + "No contacts linked to this object": "Níl aon teagmhálaí nasctha leis an réad seo", + "No conversations yet. Create a new one to get started!": "Gan comhráite fós. Cruthaigh ceann nua chun tosú!", + "No dashboards yet": "Gan deaiseanna fós", + "No data available": "Níl aon sonraí ar fáil", + "No data available for chart": "Níl aon sonraí ar fáil don chairt", + "No deleted items found": "Níor aimsíodh aon mhír scriosta", + "No deletion data available": "Níl aon sonraí scriosta ar fáil", + "No description found": "Níor aimsíodh aon chur síos", + "No description provided": "Níor soláthraíodh aon chur síos", + "No emails linked to this object": "Níl aon ríomhphost nasctha leis an réad seo", + "No endpoints defined": "Níl aon chríochphointe sainmhínithe", + "No entities detected for this email.": "Níor braitheadh aon aonán don ríomhphost seo.", + "No entities detected in this file. Run text extraction first.": "Níor braitheadh aon aonán sa chomhad seo. Rith eastóscadh téacs ar dtús.", + "No entities found": "Níor aimsíodh aon aonán", + "No entities have been detected yet": "Níor braitheadh aon aonán fós", + "No events found": "Níor aimsíodh aon imeacht", + "No events linked to this object": "Níl aon imeacht nasctha leis an réad seo", + "No expired audit trails found to clear": "Níor aimsíodh aon lorg iniúchta éagtha le glanadh", + "No expired search trails found to clear": "Níor aimsíodh aon lorg cuardaigh éagtha le glanadh", + "No extraction data available for this file": "Níl aon sonraí eastósctha ar fáil don chomhad seo", + "No facetable fields available. Select a register and schema to see available filters.": "Níl aon réimse gné-inscaglaithe ar fáil. Roghnaigh clár agus scéimre chun na scagairí atá ar fáil a fheiceáil.", + "No files found": "Níor aimsíodh aon chomhad", + "No files have been extracted yet": "Níor eastóscadh aon chomhad fós", + "No files selected": "Níor roghnaíodh aon chomhad", + "No files to process": "Gan comhaid le próiseáil", + "No files to reindex": "Gan comhaid le hath-innéacsú", + "No filters are currently active. This will delete ALL audit trail entries!": "Níl aon scagaire gníomhach faoi láthair. Scriosfaidh sé seo GACH iontráil lorg iniúchta!", + "No groups found. Try a different search.": "Níor aimsíodh aon ghrúpa. Bain triail as cuardach difriúil.", + "No label": "Gan lipéad", + "No labels": "Gan lipéid", + "No linked objects": "Gan réada nasctha", + "No log entries found": "Níor aimsíodh aon iontráil loga", + "No logs are available for this configuration.": "Níl aon loga ar fáil don chumraíocht seo.", + "No logs are available for this source.": "Níl aon loga ar fáil don fhoinse seo.", + "No logs found": "Níor aimsíodh aon loga", + "No matches": "Gan meaitseanna", + "No mismatched fields found - SOLR schema is properly configured": "Níor aimsíodh aon réimse mímheaitseáilte - tá an scéimre SOLR cumraithe i gceart", + "No objects are currently selected for deletion.": "Níl aon réad roghnaithe faoi láthair le scriosadh.", + "No objects are currently selected for permanent deletion.": "Níl aon réad roghnaithe faoi láthair le scriosadh buan.", + "No objects are currently selected for restoration.": "Níl aon réad roghnaithe faoi láthair le hathchóiriú.", + "No objects found": "Níor aimsíodh aon réad", + "No objects found to vectorize": "Níor aimsíodh aon réad le veicteoiriú", + "No objects linked to this email": "Níl aon réad nasctha leis an ríomhphost seo", + "No objects selected": "Níor roghnaíodh aon réad", + "No objects selected for deletion": "Níor roghnaíodh aon réad le scriosadh", + "No objects selected for restoration": "Níor roghnaíodh aon réad le hathchóiriú", + "No objects to delete": "Gan réada le scriosadh", + "No objects to delete for schema {schema}": "Gan réada le scriosadh don scéimre {schema}", + "No objects were permanently deleted": "Níor scriosadh aon réad go buan", + "No organisations found": "Níor aimsíodh aon eagraíocht", + "No personal data was found for this subject identifier.": "Níor aimsíodh aon sonraí pearsanta don aitheantóir ábhair seo.", + "No processing activities yet": "Gan gníomhaíochtaí próiseála fós", + "No properties found": "Níor aimsíodh aon airí", + "No properties match your filters.": "Níl aon airí ag teacht le do scagairí.", + "No purge date set": "Níl aon dáta glanta socraithe", + "No register data available": "Níl aon sonraí cláir ar fáil", + "No register objects reference this file": "Níl aon réad cláir ag tagairt don chomhad seo", + "No registers found": "Níor aimsíodh aon chlár", + "No registers found for this application.": "Níor aimsíodh aon chlár don fheidhmchlár seo.", + "No registers found. Create a register to configure permissions.": "Níor aimsíodh aon chlár. Cruthaigh clár chun ceadanna a chumrú.", + "No related cases found for this sender": "Níor aimsíodh aon chás gaolmhar don seoltóir seo", + "No related entities": "Gan aonáin ghaolmhara", + "No relations found": "Níor aimsíodh aon ghaol", + "No request body available": "Níl aon chorp iarratais ar fáil", + "No response body available": "Níl aon chorp freagartha ar fáil", + "No saved views yet. Create one in the Search tab!": "Gan amhairc shábháilte fós. Cruthaigh ceann sa chluaisín Cuardach!", + "No saved views yet. create one in the search tab!": "Gan amhairc shábháilte fós. cruthaigh ceann sa chluaisín cuardaigh!", + "No scan run yet": "Níor ritheadh aon scanadh fós", + "No schema data available": "Níl aon sonraí scéimre ar fáil", + "No schema selected for exploration": "Níor roghnaíodh aon scéimre le taiscéaladh", + "No schemas configured for mail linking.": "Níl aon scéimre cumraithe le haghaidh nasctha ríomhphoist.", + "No schemas found": "Níor aimsíodh aon scéimre", + "No schemas found for this application.": "Níor aimsíodh aon scéimre don fheidhmchlár seo.", + "No search terms data available": "Níl aon sonraí téarmaí cuardaigh ar fáil", + "No search trail entries found": "Níor aimsíodh aon iontráil lorg cuardaigh", + "No sources are available.": "Níl aon fhoinse ar fáil.", + "No sources found": "Níor aimsíodh aon fhoinse", + "No synchronizations found": "Níor aimsíodh aon sioncronú", + "No templates found": "Níor aimsíodh aon teimpléad", + "No templates have been created yet": "Níor cruthaíodh aon teimpléad fós", + "No tools available. Tools can be registered by installed apps.": "Níl aon uirlis ar fáil. Is féidir uirlisí a chlárú ag aipeanna suiteáilte.", + "No users in this organisation.": "Níl aon úsáideoir san eagraíocht seo.", + "No valid magic-mapped register+schema combinations found": "Níor aimsíodh aon teaglaim bhailí clár+scéimre draíocht-mhapáilte", + "No views found. Create views first before configuring vectorization.": "Níor aimsíodh aon amharc. Cruthaigh amhairc ar dtús sula gcumraítear veicteoiriú.", + "No views match your search": "Níl aon amharc ag teacht le do chuardach", + "No webhooks found": "Níor aimsíodh aon Webhook", + "No webhooks have been configured yet": "Níor cumraíodh aon Webhook fós", + "No workflows found in this project. Create workflows in the n8n editor.": "Níor aimsíodh aon sreabhadh oibre sa tionscadal seo. Cruthaigh sreafaí oibre san eagarthóir n8n.", + "None": "Ceann ar bith", + "Not Found": "Gan Aimsiú", + "Not extracted": "Gan eastóscadh", + "Not helpful": "Gan chabhair", + "Not in use": "Gan úsáid", + "Note:": "Nóta:", + "Notifications": "Fógraí", + "Number Constraints": "Srianta Uimhreacha", + "Number of Sources": "Líon na bhFoinsí", + "Number of chunks to vectorize in one API call (1-100).": "Líon na smután le veicteoiriú in aon ghlao API amháin (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Líon na smután le veicteoiriú in aon ghlao API amháin. Níos airde = níos tapúla ach níos mó cuimhne. Molta: 10-50.", + "Number of objects to process in each batch (1-5000).": "Líon na réad le próiseáil i ngach baisc (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Líon na réad le veicteoiriú in aon ghlao API amháin. Níos airde = níos tapúla ach níos mó cuimhne. Molta: 10-50.", + "Numeric Range:": "Raon Uimhriúil:", + "Object": "Réad", + "Object #{id}": "Réad #{id}", + "Object Collection": "Bailiúchán Réad", + "Object Configuration:": "Cumraíocht Réid:", + "Object Count Prediction": "Tuar Líon Réad", + "Object Extraction Mode": "Mód Eastósctha Réad", + "Object ID": "Aitheantas Réid", + "Object Management": "Bainistíocht Réad", + "Object Text Extraction": "Eastóscadh Téacs Réid", + "Object Vectorization": "Veicteoiriú Réid", + "Object Warmup": "Téamh Réid", + "Object changes in owned objects": "Athruithe réid i réada faoi úinéireacht", + "Object collection not configured": "Níl bailiúchán réad cumraithe", + "Object metadata incomplete for file upload": "Tá meiteashonraí réid neamhiomlán le haghaidh uaslódáil comhaid", + "Object not found": "Réad gan aimsiú", + "Object permanently deleted": "Réad scriosta go buan", + "Object restored successfully": "Athchóiríodh an réad go rathúil", + "Object settings updated successfully": "Nuashonraíodh socruithe réid go rathúil", + "Object sources": "Foinsí réid", + "Object successfully deleted": "Scriosadh an réad go rathúil", + "Object successfully modified": "Athraíodh an réad go rathúil", + "Object unlocked successfully": "Díghlasáladh an réad go rathúil", + "Object vectorization configuration saved successfully": "Sábháladh cumraíocht veicteoirithe réid go rathúil", + "Object vectorized successfully": "Veicteoiríodh an réad go rathúil", + "Object:": "Réad:", + "Objects": "Réada", + "Objects Analyzed": "Réada Anailísithe", + "Objects Distribution": "Dáileadh Réad", + "Objects being analyzed": "Réada á n-anailísiú", + "Objects by Register": "Réada de réir Cláir", + "Objects by Schema": "Réada de réir Scéimre", + "Objects deletion completed for {schema}": "Scriosadh réad críochnaithe do {schema}", + "Objects deletion completed successfully": "Scriosadh réad críochnaithe go rathúil", + "Objects in schema": "Réada sa scéimre", + "Objects successfully deleted": "Scriosadh na réada go rathúil", + "Objects to Process:": "Réada le Próiseáil:", + "Objects to be analyzed": "Réada le hanailísiú", + "Objects to be deleted": "Réada le scriosadh", + "Objects to be validated": "Réada le bailíochtú", + "Objects will be serialized as JSON text before vectorization": "Sraithfear réada mar théacs JSON roimh veicteoiriú", + "Objects will be soft deleted and moved to the": "Scriosfar réada go bog agus bogfar iad chuig an", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Scriosfar réada go bog (marcáilte mar scriosta ach coinnithe sa bhunachar sonraí). Is féidir iad a aisghabháil níos déanaí más gá.", + "Old Value": "Sean-Luach", + "Oldest Item (days)": "An Mhír is Sine (laethanta)", + "Ollama Chat Settings": "Socruithe Comhrá Ollama", + "Ollama Embedding Configuration": "Cumraíocht Leabaithe Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Le hAthruithe Amháin", + "Only members of selected groups can access this organisation": "Ní féidir ach le baill na ngrúpaí roghnaithe rochtain a fháil ar an eagraíocht seo", + "Open Nextcloud App Store": "Oscail Stór Aipeanna Nextcloud", + "Open OpenRegister sidebar": "Oscail barra taoibh OpenRegister", + "Open Register Objects": "Réada Open Register", + "Open folder": "Oscail fillteán", + "Open in OpenRegister": "Oscail in OpenRegister", + "Open n8n Editor": "Oscail Eagarthóir n8n", + "OpenAI Chat Settings": "Socruithe Comhrá OpenAI", + "OpenAI Embedding Configuration": "Cumraíocht Leabaithe OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Socruithe OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Deaiseanna oibreoir-shainithe agus tuairiscí sceidealta. Is réad den chéad ghrád é gach deais sa chlár `reports`; dearbhaítear giuirléidí in eagar `widgets` an deais agus rindreáiltear go beo iad ó chomhiomláin / GraphQL.", + "Optimizing search performance...": "Feidhmíocht chuardaigh á barrfheabhsú...", + "Optional URL-friendly identifier": "Aitheantóir URL-fhabhrach roghnach", + "Optional description for this facet": "Cur síos roghnach don ghné seo", + "Optional end date property": "Airí dáta deiridh roghnach", + "Optional event description template": "Teimpléad cur síos imeachta roghnach", + "Optional location property": "Airí suímh roghnach", + "Optional webhook secret for signature verification": "Rún Webhook roghnach le haghaidh fíorú sínithe", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Roghnach: Cuir paraiméadair fhiosrúcháin leis chun an scéimre dá dtagraítear a scagadh (e.g., status=active&type=public)", + "Or": "Nó", + "Order": "Ordú", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Clibeanna teanga BCP 47 ordaithe. Is é an chéad teanga réamhshocrú an chláir agus tiomáineann sé cúlú Accept-Language le haghaidh airíonna inaistrithe.", + "Organisation": "Eagraíocht", + "Organisation Configuration": "Cumraíocht Eagraíochta", + "Organisation ID": "Aitheantas Eagraíochta", + "Organisation Members": "Baill Eagraíochta", + "Organisation Statistics": "Staidreamh Eagraíochta", + "Organisation created successfully": "Cruthaíodh an eagraíocht go rathúil", + "Organisation membership changes": "Athruithe ar bhallraíocht eagraíochta", + "Organisation name is required": "Tá ainm eagraíochta ag teastáil", + "Organisation settings saved successfully": "Sábháladh socruithe na heagraíochta go rathúil", + "Organisation successfully created": "Cruthaíodh an eagraíocht go rathúil", + "Organisation successfully updated": "Nuashonraíodh an eagraíocht go rathúil", + "Organisational measures": "Bearta eagraíochtúla", + "Organisations": "Eagraíochtaí", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Eagraíocht", + "Organization ID (Optional)": "Aitheantas Eagraíochta (Roghnach)", + "Organizations": "Eagraíochtaí", + "Orphaned Items": "Míreanna Dílleachta", + "Other": "Eile", + "Output Mapping": "Mapáil Aschuir", + "Overview": "Forléargas", + "Owner": "Úinéir", + "Owner:": "Úinéir:", + "PDF": "PDF", + "PII hits": "Amais PII", + "PII rows": "Sraitheanna PII", + "Page {current} of {total}": "Leathanach {current} as {total}", + "Parallel Mode (Faster, more resource intensive)": "Mód Comhthreomhar (Níos tapúla, níos déine ar acmhainní)", + "Parallel:": "Comhthreomhar:", + "Password": "Pasfhocal", + "Password changes are not supported by your authentication provider.": "Ní thacaíonn do sholáthraí fíordheimhnithe le hathruithe pasfhocail.", + "Password for SOLR authentication (optional)": "Pasfhocal d'fhíordheimhniú SOLR (roghnach)", + "Password updated successfully": "Nuashonraíodh an pasfhocal go rathúil", + "Path": "Conair", + "Path in repository": "Conair sa stór", + "Path where the configuration file will be saved in the repository": "An chonair ina sábhálfar an comhad cumraíochta sa stór", + "Path where the register OAS file will be saved in the repository": "An chonair ina sábhálfar comhad OAS an chláir sa stór", + "Pattern (regex)": "Patrún (regex)", + "Pattern Issue": "Fadhb Phatrúin", + "Pattern constraint is missing": "Tá srian an phatrúin ar iarraidh", + "Patterns:": "Patrúin:", + "Pending": "Ar feitheamh", + "Permanent Delete Mode": "Mód Scriosta Buan", + "Permanently Delete": "Scrios go Buan", + "Permanently Delete ({count})": "Scrios go Buan ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Scrios go buan na {count} réad uile a scriosadh go bog. Ní féidir é seo a chur ar ceal!", + "Permanently delete already soft-deleted objects": "Scrios go buan réada atá scriosta go bog cheana féin", + "Permanently delete this conversation?": "Scrios an comhrá seo go buan?", + "Permission Matrix": "Maitrís Ceadanna", + "Person": "Duine", + "Personal Data": "Sonraí Pearsanta", + "Personal Data Export": "Easpórtáil Sonraí Pearsanta", + "Persons": "Daoine", + "Phone": "Fón", + "Phone Numbers": "Uimhreacha Fóin", + "Photos": "Grianghraif", + "Please create an agent in the": "Cruthaigh gníomhaire le do thoil sa", + "Please select a user": "Roghnaigh úsáideoir le do thoil", + "Please select an agent to continue": "Roghnaigh gníomhaire le leanúint ar aghaidh, le do thoil", + "Please select an organisation": "Roghnaigh eagraíocht le do thoil", + "Please select at least one item to import": "Roghnaigh mír amháin ar a laghad le hiompórtáil, le do thoil", + "Please select registers or schemas first": "Roghnaigh cláir nó scéimrí ar dtús, le do thoil", + "Please select which register and schema to use for the new object": "Roghnaigh cén clár agus cén scéimre a úsáidfear don réad nua, le do thoil", + "Please try again later.": "Bain triail eile as níos déanaí, le do thoil.", + "Please wait while we fetch your agents.": "Fan le do thoil agus muid ag fáil do ghníomhairí.", + "Please wait while we fetch your applications.": "Fan le do thoil agus muid ag fáil d'fheidhmchláir.", + "Please wait while we fetch your configurations.": "Fan le do thoil agus muid ag fáil do chumraíochtaí.", + "Please wait while we fetch your deleted items.": "Fan le do thoil agus muid ag fáil do mhíreanna scriosta.", + "Please wait while we fetch your sources.": "Fan le do thoil agus muid ag fáil do fhoinsí.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Fan le do thoil agus muid ag scriosadh bhailiúchán SOLR go buan. D'fhéadfadh sé seo cúpla nóiméad a thógáil.", + "Polls": "Vótaíochtaí", + "Popular Search Terms": "Téarmaí Cuardaigh Coitianta", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Sábháladh na sainroghanna", + "Prerequisites": "Réamhriachtanais", + "Press Enter to send, Shift+Enter for new line": "Brúigh Enter le seoladh, Shift+Enter le líne nua", + "Press enter to send, shift+enter for new line": "Brúigh enter le seoladh, shift+enter le líne nua", + "Prettify": "Slachtmhaigh", + "Preview erasure": "Réamhamharc ar an scriosadh", + "Preview:": "Réamhamharc:", + "Previous": "Roimhe Seo", + "Private": "Príobháideach", + "Private Agent (Default)": "Gníomhaire Príobháideach (Réamhshocrú)", + "Private agents": "Gníomhairí príobháideacha", + "Process Name (optional)": "Ainm an Phróisis (roghnach)", + "Processed {indexed} files, {failed} failed": "Próiseáladh {indexed} comhad, theip ar {failed}", + "Processes chunks in batches with simulated parallelism.": "Próiseálann sé smután i mbaisceanna le comhthreomhaireacht insamhladh.", + "Processes file chunks sequentially (safest).": "Próiseálann sé smután comhad go seicheamhach (is sábháilte).", + "Processes objects in chunks with simulated parallelism.": "Próiseálann sé réada i smután le comhthreomhaireacht insamhladh.", + "Processes objects sequentially (safest).": "Próiseálann sé réada go seicheamhach (is sábháilte).", + "Processing": "Á phróiseáil", + "Processing Limits": "Teorainneacha Próiseála", + "Processing...": "Á phróiseáil...", + "Project initialized successfully": "Túsaíodh an tionscadal go rathúil", + "Project not found. Please initialize first.": "Tionscadal gan aimsiú. Túsaigh ar dtús, le do thoil.", + "Properties": "Airíonna", + "Property Behaviors": "Iompar Airíonna", + "Property Configuration:": "Cumraíocht Airí:", + "Property Filters": "Scagairí Airíonna", + "Property Title": "Teideal Airí", + "Property Type": "Cineál Airí", + "Property can be improved": "Is féidir an t-airí a fheabhsú", + "Property could not be added": "Níorbh fhéidir an t-airí a chur leis", + "Property could not be updated": "Níorbh fhéidir an t-airí a nuashonrú", + "Property name of inversed relation": "Ainm airí an chaidrimh inbhéartaithe", + "Property successfully added": "Cuireadh an t-airí leis go rathúil", + "Property successfully updated": "Nuashonraíodh an t-airí go rathúil", + "Protected": "Cosanta", + "Provider": "Soláthraí", + "Provider is required for testing": "Tá soláthraí riachtanach le haghaidh tástála", + "Public": "Poiblí", + "Public View": "Amharc Poiblí", + "Public views can be accessed by anyone in the system": "Is féidir le duine ar bith sa chóras rochtain a fháil ar amhairc phoiblí", + "Publish": "Foilsigh", + "Publish Configuration to GitHub": "Foilsigh Cumraíocht chuig GitHub", + "Publish OAS": "Foilsigh OAS", + "Publish Objects": "Foilsigh Réada", + "Publish Register OAS to GitHub": "Foilsigh OAS an Chláir chuig GitHub", + "Publish all objects in this schema": "Foilsigh na réada uile sa scéimre seo", + "Publish objects to make them publicly available": "Foilsigh réada le go mbeidh siad ar fáil go poiblí", + "Published": "Foilsithe", + "Published:": "Foilsithe:", + "Publishing...": "Á fhoilsiú...", + "Purge": "Glan", + "Purge Date": "Dáta Glanta", + "Purge {count} object from database": "Glan {count} réad ón mbunachar sonraí", + "Purge {count} objects from database": "Glan {count} réad ón mbunachar sonraí", + "Purpose limitation *": "Teorannú cuspóra *", + "Push Notifications": "Fógraí Brú", + "Query Complexity Distribution": "Dáileadh Castachta Ceisteanna", + "Query Help": "Cabhair Cheiste", + "Query parameter is required": "Tá an paraiméadar ceiste riachtanach", + "Quota": "Cuóta", + "RAG": "RAG", + "RAG Configuration": "Cumraíocht RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Nuashonraíodh socruithe RBAC go rathúil", + "Rate limits cleared successfully": "Glanadh na teorainneacha ráta go rathúil", + "Raw Changes Data": "Amhshonraí Athruithe", + "Re-vectorize on object update": "Athveicteoirigh nuair a nuashonraítear réad", + "Read": "Léigh", + "Real-time push notification status via notify_push": "Stádas fógraí brú fíor-ama trí notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Tá fógraí brú fíor-ama gníomhach. Faigheann cliaint nasctha nuashonruithe láithreacha nuair a chruthaítear, a nuashonraítear nó a scriostar réada.", + "Realtime push active": "Brú fíor-ama gníomhach", + "Realtime push not available — the notify_push app is not installed": "Níl brú fíor-ama ar fáil — níl an aip notify_push suiteáilte", + "Reason": "Cúis", + "Reason (optional)": "Cúis (roghnach)", + "Rebase operation completed successfully": "Críochnaíodh an oibríocht atháitrithe go rathúil", + "Rebase operation failed: {error}": "Theip ar an oibríocht atháitrithe: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Moltaí:", + "Recommended Type:": "Cineál Molta:", + "Recommended: 5 sources": "Molta: 5 fhoinse", + "Refresh": "Athnuaigh", + "Refresh Data": "Athnuaigh Sonraí", + "Refresh Stats": "Athnuaigh Staitisticí", + "Refresh Workflows": "Athnuaigh Sreabhadh Oibre", + "Refresh dashboard": "Athnuaigh an deais", + "Refresh database info": "Athnuaigh faisnéis an bhunachair sonraí", + "Register": "Clár", + "Register #{id}": "Clár #{id}", + "Register / Schema": "Clár / Scéimre", + "Register ID": "Aitheantas Cláir", + "Register Objects": "Réada an Chláir", + "Register Overview": "Forléargas Cláir", + "Register Statistics": "Staitisticí Cláir", + "Register Totals": "Iomláin Chláir", + "Register and schema are required": "Tá clár agus scéimre riachtanach", + "Register annotation": "Anótáil chláir", + "Register depublished successfully": "Dífhoilsíodh an clár go rathúil", + "Register not found": "Clár gan aimsiú", + "Register objects": "Réada an chláir", + "Register objects deletion completed successfully": "Críochnaíodh scriosadh réada an chláir go rathúil", + "Register published successfully": "Foilsíodh an clár go rathúil", + "Register/Schema Usage": "Úsáid Cláir/Scéimre", + "Register:": "Clár:", + "Registers": "Cláir", + "Reindex": "Athinnéacsaigh", + "Reindex completed": "Críochnaíodh an t-athinnéacsú", + "Reindex completed successfully": "Críochnaíodh an t-athinnéacsú go rathúil", + "Reindex completed! Processed {count} objects in {duration}s": "Críochnaíodh an t-athinnéacsú! Próiseáladh {count} réad i {duration}s", + "Reindex failed": "Theip ar an athinnéacsú", + "Reindex failed: %s": "Theip ar an athinnéacsú: %s", + "Reject": "Diúltaigh", + "Rejected": "Diúltaithe", + "Related Cases": "Cásanna Gaolmhara", + "Relations": "Caidrimh", + "Remote Version": "Leagan Cianda", + "Remove": "Bain", + "Remove Deck card": "Bain cárta Deck", + "Remove avatar": "Bain an abhatár", + "Remove contact": "Bain teagmháil", + "Remove filter": "Bain scagaire", + "Remove from favorites": "Bain ó na ceanáin", + "Remove from list": "Bain ón liosta", + "Remove group": "Bain grúpa", + "Remove link": "Bain nasc", + "Remove link to {name}": "Bain an nasc le {name}", + "Remove link to {name}?": "Bain an nasc le {name}?", + "Remove link to {title}": "Bain an nasc le {title}", + "Remove oneOf entry": "Bain iontráil oneOf", + "Remove property after write-back": "Bain an t-airí tar éis aisscríofa", + "Remove {title}": "Bain {title}", + "Remove {userId} from this organisation?": "Bain {userId} ón eagraíocht seo?", + "Removed": "Bainte", + "Removed from favorites": "Bainte ó na ceanáin", + "Removing schema {schema}...": "Ag baint scéimre {schema}...", + "Rename Conversation": "Athainmnigh Comhrá", + "Rename conversation": "Athainmnigh comhrá", + "Replicas": "Macasamhla", + "Reports": "Tuarascálacha", + "Repository": "Stór", + "Request Body": "Corp na hIarrata", + "Request Data": "Sonraí na hIarrata", + "Request Details": "Sonraí na hIarrata", + "Request Quota (per day)": "Cuóta Iarratas (sa lá)", + "Request account deactivation": "Iarr díghníomhachtú cuntais", + "Request account deactivation. This will notify administrators for review.": "Iarr díghníomhachtú cuntais. Cuirfidh sé seo riarthóirí ar an eolas le hathbhreithniú a dhéanamh.", + "Request body must be JSON with a \"query\" field": "Caithfidh corp na hiarrata a bheith ina JSON le réimse \"query\"", + "Request timeout in seconds": "Teorainn ama iarrata i soicindí", + "Requested at": "Iarrtha ag", + "Required": "Riachtanach", + "Required Token Scopes": "Scóip Chomharthaí Riachtanacha", + "Required field": "Réimse riachtanach", + "Required status is inconsistent": "Tá an stádas riachtanach neamhréireach", + "Requires Dolphin AI for OCR text extraction": "Teastaíonn Dolphin AI le téacs OCR a bhaint amach", + "Rerun Search": "Athrith Cuardach", + "Rerunning search: {searchTerm}": "Ag athrith cuardaigh: {searchTerm}", + "Reset Changes": "Athshocraigh Athruithe", + "Reset Filters": "Athshocraigh Scagairí", + "Resource Quotas": "Cuótaí Acmhainní", + "Response Body": "Corp an Fhreagra", + "Response Details": "Sonraí an Fhreagra", + "Response Time:": "Am Freagartha:", + "Restore": "Athchóirigh", + "Restore conversation": "Athchóirigh comhrá", + "Restore or permanently delete items": "Athchóirigh nó scrios go buan míreanna", + "Restore {count} object": "Athchóirigh {count} réad", + "Restore {count} objects": "Athchóirigh {count} réad", + "Result Count Range": "Raon Líon Torthaí", + "Results": "Torthaí", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Tréimhse choinneála (fad ISO-8601, e.g. P10Y, P30D)", + "Retention settings updated successfully": "Nuashonraíodh na socruithe coinneála go rathúil", + "Retrieve all objects for this schema": "Aisghabh na réada uile don scéimre seo", + "Retry": "Atriail", + "Retry Failed Extractions": "Atriail Eastóscáin a Theip", + "Retry Policy": "Beartas Atrialach", + "Retry completed": "Críochnaíodh an atriail", + "Retry upload": "Atriail uaslódáil", + "Retrying...": "Ag atriail...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Déan athbhreithniú ar na réada roghnaithe thíos. Is féidir leat aon réada nach dteastaíonn uait a scriosadh a bhaint trí chliceáil ar an gcnaipe bain.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Déan athbhreithniú ar na réada roghnaithe thíos. Is féidir leat aon réada nach dteastaíonn uait a chur san áireamh a bhaint.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Déan athbhreithniú ar na réada roghnaithe thíos. Is féidir leat aon réada nach dteastaíonn uait a scriosadh go buan a bhaint trí chliceáil ar an gcnaipe bain. Ní féidir an gníomh seo a chur ar ceal.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Déan athbhreithniú ar na réada roghnaithe thíos. Is féidir leat aon réada nach dteastaíonn uait a athchóiriú a bhaint trí chliceáil ar an gcnaipe bain. Athchóireofar réada chuig a suíomh bunaidh.", + "Revoke": "Cúlghair", + "Right": "Ceart", + "Risk Level": "Leibhéal Riosca", + "Risk level": "Leibhéal riosca", + "Roles and Permissions": "Róil agus Ceadanna", + "Roles updated successfully": "Nuashonraíodh na róil go rathúil", + "Run compliance scan": "Rith scanadh comhlíontachta", + "SOLR Connection Settings": "Socruithe Nasctha SOLR", + "SOLR Index Warmup": "Téamh Innéacs SOLR", + "SOLR Version:": "Leagan SOLR:", + "SOLR actions menu": "Roghchlár gníomhartha SOLR", + "SOLR base path (usually /solr)": "Bunchonair SOLR (de ghnáth /solr)", + "SOLR collection deleted successfully": "Scriosadh bailiúchán SOLR go rathúil", + "SOLR connection test failed: {error}": "Theip ar thástáil nasctha SOLR: {error}", + "SOLR connection test successful!": "D'éirigh le tástáil nasctha SOLR!", + "SOLR index warmup completed successfully": "Críochnaíodh téamh innéacs SOLR go rathúil", + "SOLR is disabled": "Tá SOLR díchumasaithe", + "SOLR is not available or not configured": "Níl SOLR ar fáil nó níl sé cumraithe", + "SOLR logging disabled": "Logáil SOLR díchumasaithe", + "SOLR logging enabled": "Logáil SOLR cumasaithe", + "SOLR search disabled": "Cuardach SOLR díchumasaithe", + "SOLR search enabled": "Cuardach SOLR cumasaithe", + "SOLR server hostname or IP address": "Óstainm freastalaí SOLR nó seoladh IP", + "SOLR server port number (optional, defaults to 8983)": "Uimhir port an fhreastalaí SOLR (roghnach, réamhshocrú 8983)", + "SOLR settings updated successfully": "Nuashonraíodh socruithe SOLR go rathúil", + "SOLR setup completed successfully": "Críochnaíodh socrú SOLR go rathúil", + "SOLR setup completed successfully!": "Críochnaíodh socrú SOLR go rathúil!", + "SOLR setup error: %s": "Earráid socraithe SOLR: %s", + "SOLR setup failed": "Theip ar shocrú SOLR", + "SOLR setup failed - check logs": "Theip ar shocrú SOLR - seiceáil na logaí", + "SOLR setup failed. Please check the configuration and try again.": "Theip ar shocrú SOLR. Seiceáil an chumraíocht agus bain triail eile as, le do thoil.", + "SOLR setup failed: {error}": "Theip ar shocrú SOLR: {error}", + "SOLR warmup failed: {error}": "Theip ar théamh SOLR: {error}", + "Sample Values:": "Luachanna Samplacha:", + "Save": "Sábháil", + "Save Configuration": "Sábháil Cumraíocht", + "Save Connection Settings": "Sábháil Socruithe Nasctha", + "Save Roles": "Sábháil Róil", + "Save Settings": "Sábháil Socruithe", + "Save changes": "Sábháil athruithe", + "Save current search as view": "Sábháil an cuardach reatha mar amharc", + "Save labels": "Sábháil lipéid", + "Save tags for {name}": "Sábháil clibeanna do {name}", + "Save the organisation first to manage users.": "Sábháil an eagraíocht ar dtús le húsáideoirí a bhainistiú.", + "Saved Views": "Amhairc Shábháilte", + "Saving...": "Á shábháil...", + "Scheduled": "Sceidealta", + "Schema": "Scéimre", + "Schema ID": "Aitheantas Scéimre", + "Schema Name": "Ainm Scéimre", + "Schema Statistics": "Staitisticí Scéimre", + "Schema annotation": "Anótáil scéimre", + "Schema configuration updated successfully for {schema}": "Nuashonraíodh cumraíocht an scéimre go rathúil do {schema}", + "Schema depublished successfully": "Dífhoilsíodh an scéimre go rathúil", + "Schema objects deletion completed successfully": "Críochnaíodh scriosadh réada an scéimre go rathúil", + "Schema published successfully": "Foilsíodh an scéimre go rathúil", + "Schema reference ($ref)": "Tagairt scéimre ($ref)", + "Schema successfully created": "Cruthaíodh an scéimre go rathúil", + "Schema successfully updated": "Nuashonraíodh an scéimre go rathúil", + "Schema {schema} removed successfully": "Baineadh an scéimre {schema} go rathúil", + "Schema:": "Scéimre:", + "Schemas": "Scéimrí", + "Scheme": "Scéim", + "Search": "Cuardach", + "Search / Views": "Cuardach / Amhairc", + "Search / views": "Cuardach / amhairc", + "Search Activity": "Gníomhaíocht Chuardaigh", + "Search Analytics": "Anailísíocht Chuardaigh", + "Search Configuration": "Cumraíocht Chuardaigh", + "Search Entities": "Cuardaigh Aonáin", + "Search Files": "Cuardaigh Comhaid", + "Search GitHub": "Cuardaigh GitHub", + "Search GitLab": "Cuardaigh GitLab", + "Search Method": "Modh Cuardaigh", + "Search Mode": "Mód Cuardaigh", + "Search Query": "Ceist Chuardaigh", + "Search Results": "Torthaí Cuardaigh", + "Search Term": "Téarma Cuardaigh", + "Search Trail Details": "Sonraí Loirg Chuardaigh", + "Search Trail Management": "Bainistíocht Loirg Chuardaigh", + "Search Trail Statistics": "Staitisticí Loirg Chuardaigh", + "Search Trails": "Loirg Chuardaigh", + "Search Views": "Amhairc Chuardaigh", + "Search Webhooks": "Cuardaigh Webhooks", + "Search agents...": "Cuardaigh gníomhairí...", + "Search applications...": "Cuardaigh feidhmchláir...", + "Search by file name or path": "Cuardaigh de réir ainm comhaid nó conaire", + "Search by name or URL": "Cuardaigh de réir ainm nó URL", + "Search by title or UUID...": "Cuardaigh de réir teideal nó UUID...", + "Search by value": "Cuardaigh de réir luacha", + "Search configurations": "Cuardaigh cumraíochtaí", + "Search data sources...": "Cuardaigh foinsí sonraí...", + "Search groups...": "Cuardaigh grúpaí...", + "Search in Database Objects": "Cuardaigh i Réada an Bhunachair Sonraí", + "Search in Files": "Cuardaigh i gComhaid", + "Search in Objects": "Cuardaigh i Réada", + "Search objects": "Cuardaigh réada", + "Search objects...": "Cuardaigh réada...", + "Search property names...": "Cuardaigh ainmneacha airíonna...", + "Search registers...": "Cuardaigh cláir...", + "Search schemas...": "Cuardaigh scéimrí...", + "Search trail deletion not implemented yet": "Níl scriosadh loirg chuardaigh curtha i bhfeidhm fós", + "Search views...": "Cuardaigh amhairc...", + "Search {name}...": "Cuardaigh {name}...", + "Searching...": "Ag cuardach...", + "Secret": "Rún", + "Secret key for HMAC signature generation (optional)": "Eochair rúnda le sínithe HMAC a ghiniúint (roghnach)", + "Security": "Slándáil", + "See organisations as a table": "Féach ar eagraíochtaí mar thábla", + "See organisations as cards": "Féach ar eagraíochtaí mar chártaí", + "Select AI Agent": "Roghnaigh Gníomhaire AI", + "Select Active Organisation": "Roghnaigh Eagraíocht Ghníomhach", + "Select All": "Roghnaigh Gach Rud", + "Select ConfigSet": "Roghnaigh ConfigSet", + "Select File Types to Vectorize:": "Roghnaigh Cineálacha Comhaid le Veicteoiriú:", + "Select HTTP method": "Roghnaigh modh HTTP", + "Select Register and Schema": "Roghnaigh Clár agus Scéimre", + "Select Views to Vectorize:": "Roghnaigh Amhairc le Veicteoiriú:", + "Select a Nextcloud group to add": "Roghnaigh grúpa Nextcloud le cur leis", + "Select a branch": "Roghnaigh brainse", + "Select a date property": "Roghnaigh airí dáta", + "Select a model or type a custom model name": "Roghnaigh múnla nó clóscríobh ainm múnla saincheaptha", + "Select a register": "Roghnaigh clár", + "Select a register...": "Roghnaigh clár...", + "Select a repository": "Roghnaigh stór", + "Select a repository you have write access to": "Roghnaigh stór a bhfuil rochtain scríofa agat air", + "Select a schema": "Roghnaigh scéimre", + "Select a schema...": "Roghnaigh scéimre...", + "Select a user": "Roghnaigh úsáideoir", + "Select agent type": "Roghnaigh cineál gníomhaire", + "Select all entities": "Roghnaigh gach aonán", + "Select an AI Agent": "Roghnaigh Gníomhaire AI", + "Select an AI agent to begin chatting with your data.": "Roghnaigh gníomhaire AI le tosú ag comhrá le do shonraí.", + "Select an email to see linked objects": "Roghnaigh ríomhphost le réada nasctha a fheiceáil", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Roghnaigh eagraíocht agus úsáideoir le hiad a chur leis mar bhall. Cuardaigh eagraíochtaí de réir ainm.", + "Select backend": "Roghnaigh inneall", + "Select chat model": "Roghnaigh múnla comhrá", + "Select collection for files": "Roghnaigh bailiúchán do chomhaid", + "Select collection for objects": "Roghnaigh bailiúchán do réada", + "Select configuration type...": "Roghnaigh cineál cumraíochta...", + "Select default organisation": "Roghnaigh eagraíocht réamhshocraithe", + "Select entity {value}": "Roghnaigh aonán {value}", + "Select event to listen to...": "Roghnaigh teagmhas le héisteacht leis...", + "Select fields to display": "Roghnaigh réimsí le taispeáint", + "Select files with the correct extension": "Roghnaigh comhaid leis an iarmhír cheart", + "Select group": "Roghnaigh grúpa", + "Select groups (optional)": "Roghnaigh grúpaí (roghnach)", + "Select groups to notify...": "Roghnaigh grúpaí le cur ar an eolas...", + "Select groups with access to this agent": "Roghnaigh grúpaí a bhfuil rochtain acu ar an ngníomhaire seo", + "Select groups...": "Roghnaigh grúpaí...", + "Select model": "Roghnaigh múnla", + "Select one or more registers": "Roghnaigh clár amháin nó níos mó", + "Select one or more schemas": "Roghnaigh scéimre amháin nó níos mó", + "Select options...": "Roghnaigh roghanna...", + "Select or create labels, or select \"No label\" to add files": "Roghnaigh nó cruthaigh lipéid, nó roghnaigh \"No label\" le comhaid a chur leis", + "Select owner application (optional)...": "Roghnaigh feidhmchlár úinéara (roghnach)...", + "Select period": "Roghnaigh tréimhse", + "Select property to send as payload": "Roghnaigh airí le seoladh mar phálasta", + "Select provider": "Roghnaigh soláthraí", + "Select registers and schemas to save a view": "Roghnaigh cláir agus scéimrí le hamharc a shábháil", + "Select retry policy": "Roghnaigh beartas atrialach", + "Select role": "Roghnaigh ról", + "Select schemas (empty = all schemas)": "Roghnaigh scéimrí (folamh = na scéimrí uile)", + "Select schemas to warm up": "Roghnaigh scéimrí le téamh", + "Select search mode": "Roghnaigh mód cuardaigh", + "Select source type...": "Roghnaigh cineál foinse...", + "Select target type": "Roghnaigh cineál sprice", + "Select the branch to publish to": "Roghnaigh an brainse le foilsiú chuige", + "Select the event this webhook should listen to": "Roghnaigh an teagmhas ar cheart don webhook seo éisteacht leis", + "Select views to filter data (optional)": "Roghnaigh amhairc le sonraí a scagadh (roghnach)", + "Select views to limit which data the agent can access": "Roghnaigh amhairc le teorainn a chur leis na sonraí ar féidir leis an ngníomhaire rochtain a fháil orthu", + "Select views to vectorize:": "Roghnaigh amhairc le veicteoiriú:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Roghnaigh cé na grúpaí Nextcloud atá ar fáil don eagraíocht seo. Beidh rochtain ag úsáideoirí sna grúpaí seo ar acmhainní na heagraíochta.", + "Select which data views the AI can search": "Roghnaigh cé na hamhairc sonraí ar féidir leis an AI iad a chuardach", + "Select which property from the event should be used as the webhook payload data": "Roghnaigh cén t-airí ón teagmhas ba cheart a úsáid mar shonraí pálasta an webhook", + "Select which tools the AI can use to perform actions": "Roghnaigh cé na huirlisí ar féidir leis an AI iad a úsáid le gníomhartha a dhéanamh", + "Select {fieldName} values": "Roghnaigh luachanna {fieldName}", + "Selected": "Roghnaithe", + "Selected Groups": "Grúpaí Roghnaithe", + "Selected Objects ({count})": "Réada Roghnaithe ({count})", + "Selected audit trails deleted successfully": "Scriosadh na loirg iniúchta roghnaithe go rathúil", + "Selected search trails deleted successfully": "Scriosadh na loirg chuardaigh roghnaithe go rathúil", + "Selected users can access this view": "Is féidir le húsáideoirí roghnaithe rochtain a fháil ar an amharc seo", + "Send additional feedback": "Seol aiseolas breise", + "Send as CloudEvent": "Seol mar CloudEvent", + "Send as cloudevent": "Seol mar cloudevent", + "Sensitive PII": "PII Íogair", + "Serial Mode (Safer, slower)": "Mód Sraitheach (Níos sábháilte, níos moille)", + "Serial:": "Sraitheach:", + "Server Configuration": "Cumraíocht Freastalaí", + "Server Information": "Faisnéis Freastalaí", + "Set as Active": "Socraigh mar Ghníomhach", + "Set as active organisation": "Socraigh mar eagraíocht ghníomhach", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Socraigh teorainneacha le haghaidh úsáid API agus tomhaltas comharthaí. Úsáid 0 le haghaidh acmhainní gan teorainn.", + "Settings": "Socruithe", + "Settings updated successfully": "Nuashonraíodh na socruithe go rathúil", + "Shards": "Blúirí", + "Share": "Comhroinn", + "Share with Groups": "Comhroinn le Grúpaí", + "Share with Users": "Comhroinn le hÚsáideoirí", + "Show": "Taispeáin", + "Show Filters": "Taispeáin Scagairí", + "Show less": "Taispeáin níos lú", + "Show only entries with changes": "Taispeáin iontrálacha le hathruithe amháin", + "Showing {count} dashboard(s)": "Ag taispeáint {count} deais(eanna)", + "Showing {count} processing activities": "Ag taispeáint {count} gníomhaíocht phróiseála", + "Showing {showing} of {total} agents": "Ag taispeáint {showing} as {total} gníomhaire", + "Showing {showing} of {total} applications": "Ag taispeáint {showing} as {total} feidhmchlár", + "Showing {showing} of {total} audit trail entries": "Ag taispeáint {showing} as {total} iontráil loirg iniúchta", + "Showing {showing} of {total} configurations": "Ag taispeáint {showing} as {total} cumraíocht", + "Showing {showing} of {total} deleted items": "Ag taispeáint {showing} as {total} mír scriosta", + "Showing {showing} of {total} entities": "Ag taispeáint {showing} as {total} aonán", + "Showing {showing} of {total} files": "Ag taispeáint {showing} as {total} comhad", + "Showing {showing} of {total} log entries": "Ag taispeáint {showing} as {total} iontráil loga", + "Showing {showing} of {total} search trail entries": "Ag taispeáint {showing} as {total} iontráil loirg chuardaigh", + "Showing {showing} of {total} sources": "Ag taispeáint {showing} as {total} foinse", + "Showing {showing} of {total} templates": "Ag taispeáint {showing} as {total} teimpléad", + "Showing {showing} of {total} webhooks": "Ag taispeáint {showing} as {total} webhook", + "Showing {shown} of {total} organisations": "Ag taispeáint {shown} as {total} eagraíocht", + "Simple": "Simplí", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Ceisteanna simplí: Cuardaigh téacs bunúsacha le paraiméadair íosta (e.g., téarma cuardaigh aonair, gan scagairí ardleibhéil)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Ceisteanna simplí: cuardaigh téacs bunúsacha le paraiméadair íosta (e.g., téarma cuardaigh aonair, gan scagairí ardleibhéil)", + "Size": "Méid", + "Slug": "Slug", + "Soft Delete Mode": "Mód Scriosta Bhoig", + "Soft Deleted Items": "Míreanna Scriosta go Bog", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Scrios go bog na réada uile don scéimre seo ({active} gníomhach, {deleted} scriosta cheana)", + "SolrCloud mode enabled": "Mód SolrCloud cumasaithe", + "Something went wrong while adding files": "Chuaigh rud éigin cearr agus comhaid á gcur leis", + "Source": "Foinse", + "Source successfully updated": "Nuashonraíodh an fhoinse go rathúil", + "Sources": "Foinsí", + "Sources:": "Foinsí:", + "Special Rights": "Cearta Speisialta", + "Standalone SOLR mode": "Mód SOLR neamhspleách", + "Start Conversation": "Tosaigh Comhrá", + "Start Date Field": "Réimse Dáta Tosaigh", + "Start Vectorization": "Tosaigh Veicteoiriú", + "Start a conversation": "Tosaigh comhrá", + "Starting names cache warmup...": "Ag tosú téamh thaisce na n-ainmneacha...", + "Starting validation for {schema}...": "Ag tosú bailíochtú do {schema}...", + "Starting {action} for {schema}...": "Ag tosú {action} do {schema}...", + "Starting...": "Ag tosú...", + "Statistics": "Staidreamh", + "Statistics and Metrics": "Staidreamh agus Méadracht", + "Statistics refreshed": "Athnuaadh an staidreamh", + "Stats calculation not yet implemented": "Níl ríomh an staidrimh curtha i bhfeidhm fós", + "Status": "Stádas", + "Status Code": "Cód Stádais", + "Step": "Céim", + "Storage": "Stóráil", + "Storage Quota (MB)": "Cuóta Stórála (MB)", + "String Constraints": "Srianta Teaghráin", + "Subject identifier (email, BSN, name, etc.)": "Aitheantóir ábhair (ríomhphost, BSN, ainm, srl.)", + "Subscribe": "Liostáil", + "Subscribed": "Liostáilte", + "Success": "Rath", + "Success Rate": "Ráta Ratha", + "Success Status": "Stádas Ratha", + "Successful": "Rathúil", + "Successfully added user to organisation: {name}": "D'éirigh le húsáideoir a chur leis an eagraíocht: {name}", + "Successfully cleared {count} audit trails": "Glanadh {count} rian iniúchta go rathúil", + "Successfully cleared {count} blob storage objects": "Glanadh {count} réad stórála blob go rathúil", + "Successfully cleared {count} search trails": "Glanadh {count} rian cuardaigh go rathúil", + "Successfully created {count} SOLR fields": "Cruthaíodh {count} réimse SOLR go rathúil", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Scriosadh {count} leabúchán go rathúil. Athveicteoirigh do chuid sonraí le do thoil.", + "Successfully deleted {count} objects for {schema}": "Scriosadh {count} réad don scéimre {schema} go rathúil", + "Successfully fixed {count} SOLR field configurations": "Deisíodh {count} cumraíocht réimse SOLR go rathúil", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Iompórtáladh go rathúil: {registers} clár, {schemas} scéimre, {objects} réad", + "Successfully joined organisation": "Ceanglaíodh leis an eagraíocht go rathúil", + "Successfully permanently deleted {count} objects": "Scriosadh {count} réad go buan go rathúil", + "Successfully processed {count} files!": "Próiseáladh {count} comhad go rathúil!", + "Successfully restored {count} object": "Athchóiríodh {count} réad go rathúil", + "Successfully restored {count} objects": "Athchóiríodh {count} réad go rathúil", + "Successfully saved configuration for {count} facets!": "Sábháladh an chumraíocht do {count} gné go rathúil!", + "Supported File Types": "Cineálacha Comhad a dTacaítear Leo", + "Switch Active Organisation": "Athraigh an Eagraíocht Ghníomhach", + "Switch Organisation": "Athraigh Eagraíocht", + "Sync Table": "Sioncronaigh Tábla", + "Synchronization Settings": "Socruithe Sioncronaithe", + "Syncing magic table for {schema}...": "Tábla draíochta á shioncronú don scéimre {schema}...", + "Syncs": "Sioncronuithe", + "System Default": "Réamhshocrú an Chórais", + "System Prompt": "Leid an Chórais", + "System announcements": "Fógraí an chórais", + "Table": "Tábla", + "Target ID": "ID an Spriocréid", + "Target Type*": "Cineál Sprice*", + "Technical Description": "Cur Síos Teicniúil", + "Technical Details": "Sonraí Teicniúla", + "Technical description for developers and administrators": "Cur síos teicniúil d'fhorbróirí agus do riarthóirí", + "Technical measures": "Bearta teicniúla", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Teocht", + "Templates": "Teimpléid", + "Test": "Tástáil", + "Test Chat": "Tástáil Comhrá", + "Test Connection": "Tástáil Nasc", + "Test Embedding": "Tástáil Leabúchán", + "Test endpoint executed successfully": "Rinneadh an críochphointe tástála a fheidhmiú go rathúil", + "Test webhook delivered successfully": "Seachadadh an Webhook tástála go rathúil", + "Test webhook delivery failed": "Theip ar sheachadadh an Webhook tástála", + "Test webhook sent successfully": "Seoladh an Webhook tástála go rathúil", + "Testing...": "Ag tástáil...", + "Text Chunk #{id}": "Smután Téacs #{id}", + "Text Extraction": "Eastóscadh Téacs", + "Text Extraction Engine": "Inneall Eastósctha Téacs", + "Text chunks": "Smutáin téacs", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Gintear smutáin téacs le linn eastósctha comhaid agus stóráiltear sa bhunachar sonraí iad. Léann an veicteoiriú na comhaid réamhshmutánaithe seo agus déanann sé leabúcháin díobh.", + "Text extracted successfully": "Eastóscadh an téacs go rathúil", + "Text extraction disabled": "Eastóscadh téacs díchumasaithe", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Níl an feidhmchlár Nextcloud Calendar suiteáilte ná cumasaithe ar an bhfreastalaí seo.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Níl an feidhmchlár Nextcloud Contacts suiteáilte ná cumasaithe ar an bhfreastalaí seo.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Níl an feidhmchlár Nextcloud Deck suiteáilte ná cumasaithe ar an bhfreastalaí seo.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Níl an feidhmchlár Nextcloud Mail suiteáilte ná cumasaithe ar an bhfreastalaí seo.", + "The URL where webhook events will be sent": "An URL ina seolfar imeachtaí Webhook", + "The requested conversation does not exist": "Níl an comhrá iarrtha ann", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Níorbh fhéidir an deais iarrtha a luchtú. Fíoraigh an URL nó roghnaigh deais eile ón liosta.", + "The saved view and all its search configuration": "An t-amharc sábháilte agus a chumraíocht chuardaigh ar fad", + "There are no audit trail entries matching your current filters.": "Níl aon iontrálacha rian iniúchta ann a mheaitseálann do scagairí reatha.", + "There are no deleted items matching your current filters.": "Níl aon mhíreanna scriosta ann a mheaitseálann do scagairí reatha.", + "There are no search trail entries matching your current filters.": "Níl aon iontrálacha rian cuardaigh ann a mheaitseálann do scagairí reatha.", + "There are no webhook log entries matching your filters.": "Níl aon iontrálacha logála Webhook ann a mheaitseálann do scagairí.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Coinneofar iad de réir thréimhse choinneála chumraithe a scéimre agus scriosfar go huathoibríoch go buan iad nuair a rachaidh an tréimhse choinneála in éag. Tá an tréimhse choinneála inchumraithe in aghaidh an scéimre agus is féidir í a aimsiú i socruithe an scéimre.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Coinneofar iad de réir thréimhse choinneála chumraithe a scéimre agus ansin bainfear go buan iad.", + "This action cannot be undone.": "Ní féidir an gníomh seo a chealú.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Ní féidir an gníomh seo a chealú. Cinntigh nach bhfuil aon bhailiúcháin ag baint úsáide as an ConfigSet seo.", + "This action cannot be undone. Make sure no collections are using this configset.": "Ní féidir an gníomh seo a chealú. Cinntigh nach bhfuil aon bhailiúcháin ag baint úsáide as an configset seo.", + "This action will submit a deactivation request to your administrators.": "Cuirfidh an gníomh seo iarratas díghníomhachtaithe faoi bhráid do riarthóirí.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Níl aon amhairc nó uirlisí inchumraithe ag an ngníomhaire seo, ach is féidir leat na socruithe RAG thíos a choigeartú fós.", + "This analysis may take some time": "D'fhéadfadh an anailís seo tamall a thógáil", + "This audit trail entry does not contain any change information.": "Níl aon eolas faoi athruithe san iontráil rian iniúchta seo.", + "This configuration has no data defined.": "Níl aon sonraí sainmhínithe sa chumraíocht seo.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Tá an críochphointe seo dímholta. Úsáid críochphointí smután-bhunaithe ina ionad.", + "This entity has no relations to objects or files": "Níl aon ghaolta ag an aonán seo le réada ná le comhaid", + "This is a test webhook from OpenRegister": "Is Webhook tástála é seo ó OpenRegister", + "This is not valid JSON": "Ní JSON bailí é seo", + "This key already exists on this schema": "Tá an eochair seo ann cheana ar an scéimre seo", + "This schema must use Magic Table configuration to sync": "Caithfidh an scéimre seo cumraíocht Tábla Draíochta a úsáid chun sioncronú", + "This schema must use magic table configuration to sync": "Caithfidh an scéimre seo cumraíocht tábla draíochta a úsáid chun sioncronú", + "This source has no associated registers.": "Níl aon chláir bhainteacha ag an bhfoinse seo.", + "This token will only be shown once. Copy it now.": "Ní thaispeánfar an comhartha seo ach uair amháin. Cóipeáil anois é.", + "This will copy the _default ConfigSet with the new name": "Cóipeálfaidh sé seo an _default ConfigSet leis an ainm nua", + "This will copy the _default configset with the new name": "Cóipeálfaidh sé seo an _default configset leis an ainm nua", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Scriosfaidh sé seo GACH leabúchán (veicteoir) go buan ón mbunachar sonraí. Beidh ort gach réad agus comhad a athveicteoiriú. Ní féidir an gníomh seo a chealú.\n\nAn bhfuil tú cinnte gur mhaith leat leanúint ar aghaidh?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Scriosfaidh sé seo GACH leabúchán (veicteoir) go buan ón mbunachar sonraí. Beidh ort gach réad agus comhad a athveicteoiriú. Ní féidir an gníomh seo a chealú.\\n\\nAn bhfuil tú cinnte gur mhaith leat leanúint ar aghaidh?", + "This will permanently delete:": "Scriosfaidh sé seo go buan:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Bainfidh sé seo {count} réad atá scriosta go bog cheana go buan ón mbunachar sonraí. Ní féidir an gníomh seo a chealú.", + "Timeout (seconds)": "Teorainn ama (soicindí)", + "Timestamp": "Stampa ama", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Leid: Ná cumasaigh ach amhairc a dteastaíonn cuardach séimeantach uathu chun costais leabúcháin a íoslaghdú. Is annamh a bhíonn veicteoiriú de dhíth ar tháblaí cuardaigh shimplí.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Leid: ná cumasaigh ach amhairc a dteastaíonn cuardach séimeantach uathu chun costais leabúcháin a íoslaghdú. Is annamh a bhíonn veicteoiriú de dhíth ar tháblaí cuardaigh shimplí.", + "Title": "Teideal", + "Title *": "Teideal *", + "Title Template": "Teimpléad Teidil", + "Title*": "Teideal*", + "To (e.g. -1 year or 2025-12-31)": "Go (m.sh. -1 bhliain nó 2025-12-31)", + "To Date": "Go Dáta", + "To add files larger than or equal to 512MB, go to the": "Chun comhaid atá níos mó ná nó cothrom le 512MB a chur leis, téigh chuig an", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Chun méid smutáin nó straitéis a choigeartú, téigh chuig Cumraíocht Comhad → Teorainneacha Próiseála.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Chun méid smutáin nó straitéis a choigeartú, téigh chuig cumraíocht comhad → teorainneacha próiseála.", + "To change the embedding provider or model, go to LLM Configuration.": "Chun an soláthraí leabúcháin nó an tsamhail a athrú, téigh chuig Cumraíocht LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Chun an soláthraí leabúcháin nó an tsamhail a athrú, téigh chuig cumraíocht LLM.", + "To date": "Go dáta", + "Toggle search sidebar": "Scoránaigh barra taoibh an chuardaigh", + "Token Created": "Comhartha Cruthaithe", + "Token Quota (per request)": "Cuóta Comharthaí (in aghaidh an iarratais)", + "Token copied to clipboard": "Cóipeáladh an comhartha chuig an ngearrthaisce", + "Token name": "Ainm an chomhartha", + "Token revoked": "Cúlghaireadh an comhartha", + "Tools": "Uirlisí", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Feidhmíonn uirlisí le ceadanna réamhshocraithe úsáideora an ghníomhaire nuair nach bhfuil aon seisiún úsáideora gníomhach (m.sh. cron-phoist). Cumraigh an t-úsáideoir réamhshocraithe sa chluaisín Socruithe.", + "Tools respect the agent's own role and organization boundaries.": "Urramaíonn uirlisí ról féin an ghníomhaire agus teorainneacha na heagraíochta.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Urramaíonn uirlisí amhairc, ceadanna agus teorainneacha eagraíochta an ghníomhaire.", + "Top Deleters": "Príomh-Scriostóirí", + "Total": "Iomlán", + "Total Audit Trails": "Riain Iniúchta Iomlána", + "Total Chunks Available:": "Smutáin Iomlána ar Fáil:", + "Total Deleted Items": "Míreanna Scriosta Iomlána", + "Total Members": "Comhaltaí Iomlána", + "Total Objects": "Réada Iomlána", + "Total Objects in Database:": "Réada Iomlána sa Bhunachar Sonraí:", + "Total Organisations": "Eagraíochtaí Iomlána", + "Total Results": "Torthaí Iomlána", + "Total Searches": "Cuardaigh Iomlána", + "Total Size": "Méid Iomlán", + "Total Storage": "Stóráil Iomlán", + "Total events": "Imeachtaí iomlána", + "Total:": "Iomlán:", + "Totals": "Iomláin", + "Try searching by UUID or with different keywords": "Bain triail as cuardach de réir UUID nó le heochairfhocail éagsúla", + "Type": "Cineál", + "Type \"DELETE COLLECTION\" to confirm:": "Clóscríobh \"DELETE COLLECTION\" chun deimhniú:", + "Type Issue": "Fadhb Chineáil", + "Type Variations:": "Athruithe Cineáil:", + "Type filter (optional, e.g. email)": "Scagaire cineáil (roghnach, m.sh. ríomhphost)", + "Type must be \"positive\" or \"negative\"": "Caithfidh an cineál a bheith \"positive\" nó \"negative\"", + "Type to search for groups": "Clóscríobh chun cuardach a dhéanamh ar ghrúpaí", + "Type to search for objects...": "Clóscríobh chun cuardach a dhéanamh ar réada...", + "Type to search for organisations": "Clóscríobh chun cuardach a dhéanamh ar eagraíochtaí", + "Type to search for users": "Clóscríobh chun cuardach a dhéanamh ar úsáideoirí", + "Type to search groups...": "Clóscríobh chun grúpaí a chuardach...", + "Type to search users...": "Clóscríobh chun úsáideoirí a chuardach...", + "Type to search...": "Clóscríobh chun cuardach...", + "Type your username to confirm": "Clóscríobh d'ainm úsáideora chun deimhniú", + "URL": "URL", + "URL where Ollama is running": "URL ina bhfuil Ollama ag rith", + "UUID copied to clipboard": "Cóipeáladh an UUID chuig an ngearrthaisce", + "UUID:": "UUID:", + "Unchanged": "Gan athrú", + "Unique Search Terms": "Téarmaí Cuardaigh Uathúla", + "Unique Terms": "Téarmaí Uathúla", + "Unknown": "Anaithnid", + "Unknown Object": "Réad Anaithnid", + "Unknown Register": "Clár Anaithnid", + "Unknown Schema": "Scéimre Anaithnid", + "Unknown error": "Earráid anaithnid", + "Unknown widget type: {type}": "Cineál giuirléide anaithnid: {type}", + "Unlink email": "Dínasc ríomhphost", + "Unlink event": "Dínasc imeacht", + "Unnamed Object": "Réad gan ainm", + "Untitled": "Gan teideal", + "Untitled View": "Amharc gan teideal", + "Update": "Nuashonraigh", + "Update Operations": "Oibríochtaí Nuashonraithe", + "Update available: {local} → {remote}": "Nuashonrú ar fáil: {local} → {remote}", + "Update configuration: ...": "Nuashonraigh cumraíocht: ...", + "Update register OAS: ...": "Nuashonraigh OAS an chláir: ...", + "Update vectors when object data changes (recommended for accurate search)": "Nuashonraigh veicteoirí nuair a athraíonn sonraí réid (molta le haghaidh cuardaigh chruinn)", + "Updated": "Nuashonraithe", + "Updated At": "Nuashonraithe Ag", + "Updated:": "Nuashonraithe:", + "Upload new avatar": "Uaslódáil abhatár nua", + "Uploading configuration sets...": "Tacair chumraíochta á n-uaslódáil...", + "Uptime:": "Aga oibre:", + "Url": "Url", + "Usage Count": "Líon Úsáide", + "Use AI agents for processing and analysis": "Úsáid gníomhairí IS le haghaidh próiseála agus anailíse", + "Use Large Language Model features": "Úsáid gnéithe LLM", + "Use SolrCloud with Zookeeper for distributed search": "Úsáid SolrCloud le Zookeeper le haghaidh cuardaigh dáilte", + "Use SolrCloud with zookeeper for distributed search": "Úsáid SolrCloud le zookeeper le haghaidh cuardaigh dáilte", + "Use VNG APIs instead of MongoDB.": "Úsáid APIanna VNG in ionad MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Úsáid raonta réamhshocraithe (7/30/90 lá deireanacha, an bhliain seo caite, níos sine)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Úsáid stóráil sheachtrach (m.sh. MongoDB) in ionad stóráil inmheánach Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Úsáid scagairí chun iontrálacha rian iniúchta a chúngú de réir cláir, scéimre, cineáil gnímh, úsáideora, raoin dáta nó ID réid.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Úsáid scagairí chun míreanna scriosta a chúngú de réir cláir, scéimre, dáta scriosta nó an úsáideora a scrios iad.", + "Use filters to narrow down entities by type or category.": "Úsáid scagairí chun aonáin a chúngú de réir cineáil nó catagóire.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Úsáid scagairí chun iontrálacha rian cuardaigh a chúngú de réir cláir, scéimre, stádais ratha, úsáideora, raoin dáta, téarmaí cuardaigh nó méadrachtaí feidhmíochta.", + "Used By Collections": "Úsáidte ag Bailiúcháin", + "Used by": "Úsáidte ag", + "User": "Úsáideoir", + "User Agent Statistics": "Staidreamh Gníomhaire Úsáideora", + "User Description": "Cur Síos ar an Úsáideoir", + "User Quota": "Cuóta Úsáideora", + "User rate limits cleared successfully": "Glanadh teorainneacha ráta úsáideora go rathúil", + "User removed successfully": "Baineadh an t-úsáideoir go rathúil", + "User-friendly description shown in forms and help text": "Cur síos atá éasca le húsáid a thaispeántar i bhfoirmeacha agus i dtéacs cabhrach", + "Username": "Ainm úsáideora", + "Username for SOLR authentication (optional)": "Ainm úsáideora le haghaidh fíordheimhnithe SOLR (roghnach)", + "Users": "Úsáideoirí", + "Uses": "Úsáidí", + "Using Pre-Generated Chunks": "Smutáin Réamhghinte á nÚsáid", + "Validate": "Bailíochtaigh", + "Validate Objects": "Bailíochtaigh Réada", + "Validation completed for {schema}": "Críochnaíodh an bhailíochtú don scéimre {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Críochnaíodh an bhailíochtú don scéimre {schema}: {processed} próiseáilte, {updated} nuashonraithe, {failed} teipthe", + "Validation completed successfully": "Críochnaíodh an bhailíochtú go rathúil", + "Validation failed": "Theip ar an mbailíochtú", + "Value": "Luach", + "Value list (split on ,)": "Liosta luachanna (scoilt ar ,)", + "Vector Dimensions": "Toisí Veicteora", + "Vector Search Backend": "Inneall Cuardaigh Veicteora", + "Vector field: _embedding_": "Réimse veicteora: _embedding_", + "Vectorization Triggers": "Truicir Veicteoirithe", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Críochnaíodh an veicteoiriú: {vectorized} réad veicteoirithe, {failed} teipthe", + "Vectorization failed: {error}": "Theip ar an veicteoiriú: {error}", + "Vectorize All Files": "Veicteoirigh Gach Comhad", + "Vectorize All Objects": "Veicteoirigh Gach Réad", + "Vectorize all file types": "Veicteoirigh gach cineál comhaid", + "Vectorize all views": "Veicteoirigh gach amharc", + "Vectorize on object creation": "Veicteoirigh ar chruthú réid", + "Vectors will be stored in your existing object and file collections": "Stórálfar veicteoirí i do bhailiúcháin réad agus comhad atá ann cheana", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Nascacht an fhreastalaí á fíorú...", + "Version": "Leagan", + "Version Information": "Eolas faoin Leagan", + "Very High": "An-Ard", + "Very high": "An-ard", + "View": "Amharc", + "View API Docs": "Féach ar Cháipéisí API", + "View API Documentation": "Féach ar Dhoiciméadú API", + "View APIs": "Féach ar APInna", + "View Changes": "Féach ar Athruithe", + "View Details": "Féach ar Shonraí", + "View Error": "Féach ar Earráid", + "View File": "Féach ar Chomhad", + "View Full Details": "Féach ar Shonraí Iomlána", + "View Logs": "Féach ar Logaí", + "View Name": "Ainm an Amhairc", + "View Object": "Féach ar Réad", + "View Parameters": "Féach ar Pharaiméadair", + "View Selection": "Roghnú Amhairc", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Féach ar logaí rian cuardaigh agus déan anailís orthu le scagadh ardleibhéil agus cumais anailíse", + "View and analyze system audit trails with advanced filtering capabilities": "Féach ar riain iniúchta an chórais agus déan anailís orthu le cumais scagtha ardleibhéil", + "View and manage authorization across registers and schemas": "Féach ar údarú agus bainistigh é ar fud clár agus scéimrí", + "View deleted successfully": "Scriosadh an t-amharc go rathúil", + "View entity details and manage relations": "Féach ar shonraí an aonáin agus bainistigh gaolta", + "View name": "Ainm an amhairc", + "View name is required": "Tá ainm an amhairc riachtanach", + "View object": "Féach ar réad", + "View saved successfully!": "Sábháladh an t-amharc go rathúil!", + "View search analytics and manage search logs": "Féach ar anailísíocht chuardaigh agus bainistigh logaí cuardaigh", + "View successfully deleted": "Scriosadh an t-amharc go rathúil", + "View successfully updated": "Nuashonraíodh an t-amharc go rathúil", + "View updated successfully!": "Nuashonraíodh an t-amharc go rathúil!", + "View webhook delivery logs and filter by webhook": "Féach ar logaí seachadta Webhook agus scag de réir Webhook", + "View {count} more": "Féach ar {count} eile", + "Views": "Amhairc", + "Visible to end users": "Infheicthe d'úsáideoirí deiridh", + "Visible to users": "Infheicthe d'úsáideoirí", + "Wait for Response": "Fan le Freagra", + "Wait for webhook response before continuing (required for request/response flows)": "Fan le freagra Webhook sula leanann tú ar aghaidh (riachtanach do shreafaí iarratais/freagra)", + "Waiting for cluster synchronization...": "Ag fanacht le sioncronú braisle...", + "Warmup Names Cache": "Téamh Taisce na nAinmneacha", + "Warmup failed": "Theip ar an téamh", + "Warning": "Rabhadh", + "Warning:": "Rabhadh:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Rabhadh: Tá cineálacha neamhsheasmhacha ag an airí seo: {types}. Smaoinigh ar cibé ar cheart an cineál céanna a bheith ag gach réad.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Scanfaimid gach réad a bhaineann leis an scéimre seo chun airíonna nua a fháil amach agus chun anailís a dhéanamh ar airíonna atá ann cheana le haghaidh feabhsuithe féideartha. Baineann an próiseas le scrúdú a dhéanamh ar struchtúr sonraí gach réid, le hairíonna nach bhfuil sainmhínithe sa scéimre reatha a aithint, agus le deiseanna a fháil chun na sainmhínithe airíonna atá ann a fheabhsú le srianta, formáidí agus rialacha bailíochtaithe níos fearr.", + "Webhook": "Webhook", + "Webhook Log Details": "Sonraí Loga Webhook", + "Webhook Logs": "Logaí Webhook", + "Webhook created successfully": "Cruthaíodh an Webhook go rathúil", + "Webhook deleted": "Scriosadh an Webhook", + "Webhook retry delivered successfully": "Seachadadh atriail Webhook go rathúil", + "Webhook retry delivery failed": "Theip ar sheachadadh atriail Webhook", + "Webhook updated": "Nuashonraíodh an Webhook", + "Webhook updated successfully": "Nuashonraíodh an Webhook go rathúil", + "Webhooks": "Webhooks", + "Weekly": "Seachtainiúil", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Nuair a ritheann an gníomhaire gan seisiún úsáideora (m.sh. tascanna sceidealta), úsáidfear comhthéacs an úsáideora seo", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Nuair a ritheann an gníomhaire gan seisiún úsáideora (m.sh. tascanna sceidealta), úsáidfear comhthéacs an úsáideora seo.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Nuair atá sé díchumasaithe, ní thaispeánfaidh an ghné seo ach luachanna ón scéimre seo agus áireofar scagaire scéimre nuair a roghnaítear é.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Nuair atá sé cumasaithe, déanfaidh sábháil an réid seo na spriocréada a nuashonrú freisin chun tagairt ar ais don réad seo a chur san áireamh.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Nuair atá sé cumasaithe, bainfear an airí seo den fhoinsréad tar éis na spriocréada a nuashonrú.", + "Workflow Management": "Bainistíocht Sreafaí Oibre", + "Workflows": "Sreafaí Oibre", + "Wrap webhook payload in CloudEvents format for better interoperability": "Fill an t-ualach pá Webhook i bhformáid CloudEvents le haghaidh idir-inoibritheacht níos fearr", + "Wrap webhook payload in cloudevents format for better interoperability": "Fill an t-ualach pá Webhook i bhformáid cloudevents le haghaidh idir-inoibritheacht níos fearr", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Tá", + "You": "Tú", + "You are not yet a member of any organisations.": "Níl tú i do chomhalta d'aon eagraíocht fós.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Is féidir leat ConfigSets nua a chruthú bunaithe ar an teimpléad _default, nó cinn shaincheaptha a uaslódáil go díreach chuig do fhreastalaí SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Is féidir leat configsets nua a chruthú bunaithe ar an teimpléad _default, nó cinn shaincheaptha a uaslódáil go díreach chuig do fhreastalaí SOLR.", + "You do not have access to this conversation": "Níl rochtain agat ar an gcomhrá seo", + "You do not have permission to delete this conversation": "Níl cead agat an comhrá seo a scriosadh", + "You do not have permission to modify this conversation": "Níl cead agat an comhrá seo a mhodhnú", + "You do not have permission to restore this conversation": "Níl cead agat an comhrá seo a athchóiriú", + "You do not have permission to view the permission matrix. Admin access is required.": "Níl cead agat an maitrís cheadanna a fheiceáil. Tá rochtain riaracháin riachtanach.", + "You have selected {count} properties to add to the schema.": "Roghnaigh tú {count} airí le cur leis an scéimre.", + "You must be logged in to favorite views": "Caithfidh tú a bheith logáilte isteach chun amhairc a chur le ceanáin", + "You need an AI agent to start a conversation.": "Teastaíonn gníomhaire IS uait chun comhrá a thosú.", + "Your Fireworks AI API key. Get one at": "D'eochair API Fireworks AI. Faigh ceann ag", + "Your OpenAI API key. Get one at": "D'eochair API OpenAI. Faigh ceann ag", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Taifeadadh d'aiseolas. Go roghnach, is féidir leat sonraí breise a chur ar fáil anseo...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Taifeadadh d'aiseolas. go roghnach, is féidir leat sonraí breise a chur ar fáil anseo...", + "Zookeeper Hosts": "Óstaigh Zookeeper", + "Zookeeper Port": "Port Zookeeper", + "Zookeeper Settings (SolrCloud)": "Socruithe Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Teaghrán nasctha Zookeeper le haghaidh SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Uimhir phoirt Zookeeper (roghnach, réamhshocrú 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F RABHADH SCRIOSTA BUAN \\u26A0\\uFE0F\n\nTá tú ar tí GACH réad a scriosadh GO BUAN don scéimre \"{schema}\":\n\n\\u2022 Réada gníomhacha: {active}\n\\u2022 Réada scriosta go bog: {deleted}\n\\u2022 Iomlán: {total}\n\nBainfear na réada seo go hiomlán den bhunachar sonraí agus NÍ FÉIDIR iad a aisghabháil.\n\nAn bhfuil tú lánchinnte?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F RABHADH SCRIOSTA BUAN \\u26A0\\uFE0F\n\nTá tú ar tí {count} réad scriosta go bog a scriosadh GO BUAN don scéimre \"{schema}\".\n\nBainfear na réada seo go hiomlán den bhunachar sonraí agus NÍ FÉIDIR iad a aisghabháil.\n\nAn bhfuil tú lánchinnte?", + "and accessible to all users in selected groups (or all users if no groups selected).": "agus inrochtana ag gach úsáideoir sna grúpaí roghnaithe (nó ag gach úsáideoir mura roghnaítear aon ghrúpaí).", + "and add the files there.": "agus cuir na comhaid ann.", + "approved": "ceadaithe", + "are only accessible to invited users.": "níl siad inrochtana ach ag úsáideoirí cuireadh.", + "by": "le", + "chunk": "smután", + "chunks": "smutáin", + "chunks processed": "smutáin próiseáilte", + "completed": "críochnaithe", + "configuration(s)": "cumraíocht(aí)", + "conversationId is required": "tá conversationId riachtanach", + "create": "cruthaigh", + "delete": "scrios", + "deleted objects section": "rannán réada scriosta", + "e.g., lib/Settings/config.json": "e.g., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "e.g., lib/Settings/register.json", + "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", + "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", + "execute": "feidhmigh", + "failed": "teipthe", + "file": "comhad", + "files": "comhaid", + "file{plural}": "comhad{plural}", + "folder": "fillteán", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "i {register}", + "in_progress": "ar siúl", + "inherited": "oidhreachtaithe", + "list": "liosta", + "log{plural}": "log{plural}", + "manage": "bainistigh", + "members": "comhaltaí", + "menu or contact someone with permission to create agents.": "roghchlár nó déan teagmháil le duine a bhfuil cead aige gníomhairí a chruthú.", + "message content is required": "tá ábhar na teachtaireachta riachtanach", + "messages": "teachtaireachtaí", + "missing": "ar iarraidh", + "myapp": "myapp", + "n8n URL and API key are required": "tá URL n8n agus eochair API riachtanach", + "n8n configuration saved successfully": "Sábháladh cumraíocht n8n go rathúil", + "n8n connection not configured": "Níl nasc n8n cumraithe", + "n8n connection successful": "D'éirigh le nasc n8n", + "n8n connection test failed: {message}": "Theip ar thástáil nasc n8n: {message}", + "n8n connection test successful": "D'éirigh le tástáil nasc n8n", + "n8n initialization failed: {message}": "Theip ar thúsú n8n: {message}", + "n8n integration disabled": "Comhtháthú n8n díchumasaithe", + "n8n integration enabled": "Comhtháthú n8n cumasaithe", + "n8n project initialized successfully": "Túsaíodh tionscadal n8n go rathúil", + "n8n settings saved successfully": "Sábháladh socruithe n8n go rathúil", + "notify_push configuration guide": "treoir chumraíochta notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "Tá notify_push suiteáilte ach níor dheimhnigh OpenRegister brú rathúil fós. Spreag sábháil réid chun é a ghníomhachtú, nó seiceáil do chumraíocht notify_push.", + "notify_push is installed but not yet active": "Tá notify_push suiteáilte ach níl sé gníomhach fós", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "réada", + "objects processed": "réada próiseáilte", + "object{plural}": "réad{plural}", + "or pick a range": "nó roghnaigh raon", + "org-...": "org-...", + "pending": "ar feitheamh", + "provides a good balance between speed and accuracy.": "soláthraíonn sé cothromaíocht mhaith idir luas agus cruinneas.", + "public": "poiblí", + "read": "léigh", + "register(s)": "clár", + "register(s) selected": "clár roghnaithe", + "register{plural}": "clár{plural}", + "rejected": "diúltaithe", + "required": "riachtanach", + "results": "torthaí", + "schema(s)": "scéimre(í)", + "schema(s) selected": "scéimre(í) roghnaithe", + "schema{plural}": "scéimre{plural}", + "selected objects": "réada roghnaithe", + "set": "socraigh", + "sk-...": "sk-...", + "this application": "an feidhmchlár seo", + "to": "go", + "type must be \"positive\" or \"negative\"": "caithfidh an cineál a bheith \"positive\" nó \"negative\"", + "update": "nuashonraigh", + "vectorized": "veicteoirithe", + "view": "amharc", + "views selected": "amhairc roghnaithe", + "waiting": "ag fanacht", + "{active} active, {deleted} deleted": "{active} gníomhach, {deleted} scriosta", + "{count} audit trails cleared successfully": "Glanadh {count} rian iniúchta go rathúil", + "{count} compliance issue(s) detected": "Braitheadh {count} fhadhb chomhlíonta", + "{count} deletions": "{count} scriosadh", + "{count} documents found": "Aimsíodh {count} doiciméad", + "{count} email": "{count} ríomhphost", + "{count} emails": "{count} ríomhphost", + "{count} entries": "{count} iontráil", + "{count} issues": "{count} fadhb", + "{count} members": "{count} comhalta", + "{count} searches": "{count} cuardach", + "{count} selected": "{count} roghnaithe", + "{count} widget(s)": "{count} giuirléid", + "{matched} object(s) matched. {erased} erased.": "Meaitseáladh {matched} réad. Scriosadh {erased}.", + "{minutes} minutes ago": "{minutes} nóiméad ó shin", + "{percentage}% of objects": "{percentage}% de na réada", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} i {register} / {schema}", + "{type} cache cleared successfully": "Glanadh taisce {type} go rathúil", + "ℹ️ Current Configuration": "ℹ️ Cumraíocht Reatha", + "ℹ️ current configuration": "ℹ️ cumraíocht reatha", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ CONTÚIRT: Scriosfar gach {total} réad GO BUAN ón mbunachar sonraí. Tá an gníomh seo DO-AISGHABHÁLA agus ní féidir é a chealú!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ RABHADH SCRIOSTA BUAN ⚠️\n\nTá tú ar tí GACH réad a scriosadh GO BUAN don scéimre \"{schema}\":\n\n• Réada gníomhacha: {active}\n• Réada scriosta go bog: {deleted}\n• Iomlán: {total}\n\nBainfear na réada seo go hiomlán den bhunachar sonraí agus NÍ FÉIDIR iad a aisghabháil.\n\nAn bhfuil tú lánchinnte?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ RABHADH SCRIOSTA BUAN ⚠️\n\nTá tú ar tí {count} réad scriosta go bog a scriosadh GO BUAN don scéimre \"{schema}\".\n\nBainfear na réada seo go hiomlán den bhunachar sonraí agus NÍ FÉIDIR iad a aisghabháil.\n\nAn bhfuil tú lánchinnte?", + "⚡ Batch Processing": "⚡ Próiseáil Bhaisce", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ Is féidir le SOLR na milliúin doiciméad a láimhseáil le hamanna freagartha cuardaigh faoi bhun soicind...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Comhthéacs cuimsitheach ✓ Níos lú seans go gcaillfear sonraí ✗ Freagraí níos moille ✗ D'fhéadfadh eolas nach bhfuil chomh hábhartha a bheith san áireamh", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Freagraí níos tapúla ✓ Freagraí níos dírithe ✗ D'fhéadfadh eolas ábhartha a chailleadh", + "✨ AI Features": "✨ Gnéithe IS", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 I mód dáilte, úsáideann SOLR ZooKeeper le haghaidh comhordú braisle...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Ceadaíonn cuardach gnéitheach d'úsáideoirí druileáil isteach i dtorthaí de réir catagóirí...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 An raibh a fhios agat? Cumhachtaíonn SOLR cuardach do Netflix, Apple agus go leor mórshuíomhanna eile!", + "💰 View Selection (Cost Optimization)": "💰 Roghnú Amhairc (Optamú Costais)", + "📄 Object Serialization": "📄 Sraitheadh Réad", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 Soláthraíonn SOLR anailísíocht agus staidreamh saibhir faoi fheidhmíocht chuardaigh...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Tá an scéimre agus na comhaid chumraíochta do d'innéacs cuardaigh i ConfigSets...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Cinntíonn macasamhlú go bhfuil d'innéacs cuardaigh ar fáil fiú má theipeann ar nóid...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 Is ardán cuardaigh fiontair cumhachtach é SOLR atá tógtha ar Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Socruithe Veicteoirithe", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Is féidir le huathscálú nóid a chur leis nó a bhaint go dinimiciúil bunaithe ar an ualach...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Áirítear le gnéithe slándála fíordheimhniú, údarú agus criptiú SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} ríomhphost", + "{count} ríomhphost" + ], + "Purge {count} object from database": [ + "Glan {count} réad ón mbunachar sonraí", + "Glan {count} réad ón mbunachar sonraí" + ], + "Restore {count} object": [ + "Athchóirigh {count} réad", + "Athchóirigh {count} réad" + ], + "Successfully restored {count} object": [ + "Athchóiríodh {count} réad go rathúil", + "Athchóiríodh {count} réad go rathúil" + ], + "Delete {count} object": [ + "Scrios {count} réad", + "Scrios {count} réad" + ], + "Object successfully deleted": [ + "Scriosadh an réad go rathúil", + "Scriosadh na réada go rathúil" + ] + } +} diff --git a/l10n/hr.js b/l10n/hr.js new file mode 100644 index 0000000000..1ce62ac8ee --- /dev/null +++ b/l10n/hr.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(bez predmeta)", + "(no title)" : "(bez naslova)", + "(unnamed)" : "(neimenovano)", + "(untitled card)" : "(kartica bez naslova)", + "Activities" : "Aktivnosti", + "Add Endpoint" : "Dodaj krajnju točku", + "Add Source" : "Dodaj izvor", + "Add card" : "Dodaj karticu", + "Add contact" : "Dodaj kontakt", + "All clear" : "Sve je u redu", + "Articles" : "Članci", + "Code" : "Kôd", + "Compliance" : "Sukladnost", + "Contacts" : "Kontakti", + "Create Agent" : "Stvori agenta", + "Create Application" : "Stvori aplikaciju", + "Create event" : "Stvori događaj", + "Erasure complete" : "Brisanje dovršeno", + "Erasure preview" : "Pregled brisanja", + "Expand details" : "Proširi pojedinosti", + "Save changes" : "Spremi promjene", + "📄 Object Serialization" : "📄 Serijalizacija objekta", + "🔢 Vectorization Settings" : "🔢 Postavke vektorizacije", + "💰 View Selection (Cost Optimization)" : "💰 Odabir prikaza (optimizacija troškova)", + "AI Assistant" : "AI asistent", + "AI Chat" : "AI razgovor", + "API Key" : "API ključ", + "API tokens saved successfully" : "API tokeni uspješno spremljeni", + "About ConfigSets" : "O ConfigSetovima", + "Action" : "Radnja", + "Action Distribution" : "Raspodjela radnji", + "Action:" : "Radnja:", + "Actions" : "Radnje", + "Active" : "Aktivno", + "Active Collections" : "Aktivne zbirke", + "Active Filters" : "Aktivni filtri", + "Active Organisations" : "Aktivne organizacije", + "Active filters:" : "Aktivni filtri:", + "Active organisation set successfully" : "Aktivna organizacija uspješno postavljena", + "Activity Period" : "Razdoblje aktivnosti", + "Add Groups" : "Dodaj grupe", + "Add Schema" : "Dodaj shemu", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Dodajte naslove shema, opise i informacije o registru kako biste pružili bogatiji kontekst za pretraživanje", + "Add to favorites" : "Dodaj u favorite", + "Added" : "Dodano", + "Added to favorites" : "Dodano u favorite", + "Additional Information" : "Dodatne informacije", + "Additional feedback saved. Thank you!" : "Dodatne povratne informacije spremljene. Hvala vam!", + "Admin" : "Administrator", + "Advanced" : "Napredno", + "Advanced Filters" : "Napredni filtri", + "Advanced Options" : "Napredne mogućnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Napredni filtri nisu dostupni pri korištenju izvora baze podataka. Prebacite se na Auto ili SOLR indeks za mogućnosti filtriranja.", + "After" : "Nakon", + "Agent deleted successfully" : "Agent uspješno izbrisan", + "Agents" : "Agenti", + "All" : "Sve", + "All Categories" : "Sve kategorije", + "All Collections" : "Sve zbirke", + "All Confidence Levels" : "Sve razine pouzdanosti", + "All ConfigSets" : "Svi ConfigSetovi", + "All Files" : "Sve datoteke", + "All Levels" : "Sve razine", + "All Types" : "Svi tipovi", + "All Webhooks" : "Svi webhookovi", + "All actions" : "Sve radnje", + "All audit trails cleared successfully" : "Svi tragovi revizije uspješno očišćeni", + "All registers" : "Svi registri", + "All schemas" : "Sve sheme", + "All searches" : "Sva pretraživanja", + "All users" : "Svi korisnici", + "An error occurred while clearing audit trails" : "Došlo je do pogreške pri čišćenju tragova revizije", + "An error occurred while deleting the audit trail" : "Došlo je do pogreške pri brisanju traga revizije", + "An error occurred while deleting the view" : "Došlo je do pogreške pri brisanju prikaza", + "An error occurred while permanently deleting the objects" : "Došlo je do pogreške pri trajnom brisanju objekata", + "Analysis completed:" : "Analiza dovršena:", + "Analysis steps:" : "Koraci analize:", + "Analytics" : "Analitika", + "Analyze Objects" : "Analiziraj objekte", + "Analyze Properties" : "Analiziraj svojstva", + "Analyze existing properties for improvement opportunities" : "Analizirajte postojeća svojstva za prilike za poboljšanje", + "Analyzing..." : "Analiziranje...", + "Any favorites and sharing settings for this view" : "Svi favoriti i postavke dijeljenja za ovaj prikaz", + "Any user" : "Bilo koji korisnik", + "App store cache invalidated successfully" : "Predmemorija trgovine aplikacija uspješno poništena", + "Application deleted successfully" : "Aplikacija uspješno izbrisana", + "Applications" : "Aplikacije", + "Apply Changes" : "Primijeni promjene", + "Archive" : "Arhiviraj", + "Archive conversation" : "Arhiviraj razgovor", + "Archived conversations are hidden from your active list" : "Arhivirani razgovori skriveni su s vašeg aktivnog popisa", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Jeste li sigurni da želite očistiti stare tragove pretraživanja? Time će se izbrisati unosi stariji od 30 dana.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Jeste li sigurni da želite izbrisati odabrane tragove revizije? Ova se radnja ne može poništiti.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Jeste li sigurni da želite izbrisati odabrane tragove pretraživanja? Ova se radnja ne može poništiti.", + "Are you sure you want to delete this ConfigSet?" : "Jeste li sigurni da želite izbrisati ovaj ConfigSet?", + "Are you sure you want to permanently delete" : "Jeste li sigurni da želite trajno izbrisati", + "Ask a question..." : "Postavite pitanje...", + "Ask questions about your data using natural language" : "Postavljajte pitanja o svojim podacima koristeći prirodni jezik", + "Attempt" : "Pokušaj", + "Audit Trail Changes" : "Promjene traga revizije", + "Audit Trail Details" : "Pojedinosti traga revizije", + "Audit Trail Management" : "Upravljanje tragom revizije", + "Audit Trail Statistics" : "Statistika traga revizije", + "Audit Trails" : "Tragovi revizije", + "Audit trail data copied to clipboard" : "Podaci traga revizije kopirani u međuspremnik", + "Audit trail deleted successfully" : "Trag revizije uspješno izbrisan", + "Audit trail successfully deleted" : "Trag revizije uspješno izbrisan", + "Authentication" : "Autentifikacija", + "Auto-commit disabled" : "Automatsko predavanje onemogućeno", + "Auto-commit enabled" : "Automatsko predavanje omogućeno", + "Auto-create Default Organisation" : "Automatski stvori zadanu organizaciju", + "Auto-retry failed vectorizations" : "Automatski ponovi neuspjele vektorizacije", + "Automatically commit changes to SOLR index" : "Automatski predaj promjene u SOLR indeks", + "Automatically create a default organisation if none exists when the app is initialized" : "Automatski stvori zadanu organizaciju ako nijedna ne postoji pri inicijalizaciji aplikacije", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Automatski generiraj vektorska ugrađivanja iz tekstualnih dijelova kada se datoteke prenose i obrađuju", + "Automatically generate vector embeddings when objects are created or updated" : "Automatski generiraj vektorska ugrađivanja kada se objekti stvore ili ažuriraju", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Automatski ponovi neuspjele pokušaje vektorizacije (najviše 3 ponavljanja)", + "Available Filters" : "Dostupni filtri", + "Available Workflows" : "Dostupni tijekovi rada", + "Avg Execution Time" : "Prosječno vrijeme izvršavanja", + "Avg Members/Org" : "Prosječno članova/org.", + "Avg Object Views/Session" : "Prosječno pregleda objekta/sesiji", + "Avg Response Time" : "Prosječno vrijeme odgovora", + "Avg Results/Search" : "Prosječno rezultata/pretraživanju", + "Avg Searches/Session" : "Prosječno pretraživanja/sesiji", + "Back" : "Natrag", + "Back to Entities" : "Natrag na entitete", + "Back to Registers" : "Natrag na registre", + "Back to Webhooks" : "Natrag na webhookove", + "Back to applications" : "Natrag na aplikacije", + "Back to entities" : "Natrag na entitete", + "Backend updated successfully. Please reload the application." : "Pozadinski sustav uspješno ažuriran. Ponovno učitajte aplikaciju.", + "Base URL (Optional)" : "Osnovni URL (neobavezno)", + "Basic Information" : "Osnovne informacije", + "Batch Size" : "Veličina serije", + "Batch extraction completed" : "Serijsko izdvajanje dovršeno", + "Before" : "Prije", + "Before object vectorization can work:" : "Prije nego što vektorizacija objekta može raditi:", + "Behavior Issue" : "Problem s ponašanjem", + "Bookmarks" : "Oznake", + "Blob storage has been retired. All objects now use magic tables." : "Pohrana blobova povučena je iz uporabe. Svi objekti sada koriste čarobne tablice.", + "Branch" : "Grana", + "Bulk delete operation completed successfully" : "Skupna operacija brisanja uspješno dovršena", + "Bulk save operation completed successfully" : "Skupna operacija spremanja uspješno dovršena", + "Business Data" : "Poslovni podaci", + "Cache cleared successfully" : "Predmemorija uspješno očišćena", + "Calculate Sizes" : "Izračunaj veličine", + "Cancel" : "Odustani", + "Cannot delete: objects are still attached" : "Nije moguće izbrisati: objekti su još uvijek priloženi", + "Category" : "Kategorija", + "Change Type" : "Tip promjene", + "Changes" : "Promjene", + "Changes copied to clipboard" : "Promjene kopirane u međuspremnik", + "Chat Model" : "Model razgovora", + "Chat Provider" : "Pružatelj razgovora", + "Chat Provider (RAG)" : "Pružatelj razgovora (RAG)", + "Chat Settings" : "Postavke razgovora", + "Chat provider connection successful!" : "Povezivanje s pružateljem razgovora uspješno!", + "Chat settings" : "Postavke razgovora", + "Choose a register" : "Odaberite registar", + "Choose a schema" : "Odaberite shemu", + "Choose how vector similarity calculations are performed for semantic search" : "Odaberite kako se izvode izračuni vektorske sličnosti za semantičko pretraživanje", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Odaberite koje tipove datoteka uključiti u proces vektorizacije. Obradit će se samo datoteke s izdvojenim tekstom i dijelovima.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Odaberite koje prikaze uključiti u proces vektorizacije. Ostavite prazno za obradu svih prikaza na temelju vaše konfiguracije.", + "Chunk" : "Dio", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Brisanje dijelova još nije implementirano. Koristite krajnje točke temeljene na dijelovima.", + "Chunks" : "Dijelovi", + "Chunks to Vectorize:" : "Dijelovi za vektorizaciju:", + "Cleanup Old Trails" : "Očisti stare tragove", + "Cleanup completed" : "Čišćenje dovršeno", + "Clear All Embeddings" : "Očisti sva ugrađivanja", + "Clear Entries" : "Očisti unose", + "Clear Filtered Audit Trails" : "Očisti filtrirane tragove revizije", + "Clear Filters" : "Očisti filtre", + "Clear Index" : "Očisti indeks", + "Clear Selection" : "Očisti odabir", + "Clear all" : "Očisti sve", + "Clear all filters" : "Očisti sve filtre", + "Clear date range" : "Očisti raspon datuma", + "Clearing..." : "Čišćenje...", + "Close" : "Zatvori", + "Collection Management" : "Upravljanje zbirkom", + "Collection Name" : "Naziv zbirke", + "Collection assignments updated successfully" : "Dodjele zbirke uspješno ažurirane", + "Collection cleared successfully" : "Zbirka uspješno očišćena", + "Collection copied successfully" : "Zbirka uspješno kopirana", + "Collection created successfully" : "Zbirka uspješno stvorena", + "Collection deleted successfully" : "Zbirka uspješno izbrisana", + "Collection used to store and index file metadata and content" : "Zbirka koja se koristi za pohranu i indeksiranje metapodataka i sadržaja datoteka", + "Collection used to store and index object data" : "Zbirka koja se koristi za pohranu i indeksiranje podataka objekata", + "Collections:" : "Zbirke:", + "Commit Message" : "Poruka predavanja", + "Commit Within (ms)" : "Predaj unutar (ms)", + "Commit message" : "Poruka predavanja", + "Compare current schema with real object data" : "Usporedite trenutnu shemu sa stvarnim podacima objekta", + "Completed" : "Dovršeno", + "Complex" : "Složeno", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Složeni upiti: napredna pretraživanja s više filtara, operatora i složenih kombinacija parametara", + "Confidence Level" : "Razina pouzdanosti", + "Confidence Score" : "Ocjena pouzdanosti", + "Config must be provided as an object" : "Konfiguracija mora biti navedena kao objekt", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Upravljanje ConfigSetom", + "ConfigSet Name" : "Naziv ConfigSeta", + "ConfigSet created successfully" : "ConfigSet uspješno stvoren", + "ConfigSet deleted successfully" : "ConfigSet uspješno izbrisan", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSetovi definiraju shemu i konfiguraciju za vaše SOLR zbirke. Sadrže definicije polja, analizatore i druge postavke pretraživanja.", + "Configuration" : "Konfiguracija", + "Configuration Data" : "Podaci konfiguracije", + "Configuration saved" : "Konfiguracija spremljena", + "Configurations" : "Konfiguracije", + "Configure Facets" : "Konfiguriraj fasete", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurirajte pružatelje velikih jezičnih modela (LLM) za značajke pokretane umjetnom inteligencijom uključujući semantičko pretraživanje, ugrađivanja i razgovor.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurirajte osnovne postavke povezivanja za vaš SOLR poslužitelj uključujući autentifikaciju i mrežne mogućnosti. Koristite zasebne dijaloge za upravljanje ConfigSetom i zbirkom za upravljanje jezgrama i zbirkama.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurirajte kako se objekti baze podataka pretvaraju u vektorska ugrađivanja za semantičko pretraživanje. Objekti se izravno vektoriziraju bez potrebe za izdvajanjem teksta.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurirajte kako se objekti pretvaraju u tekst prije vektorizacije. Ove postavke utječu na kvalitetu pretraživanja i kontekst.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurirajte parametre za vektorizaciju datoteka. Ovaj će proces generirati vektorska ugrađivanja za sve izdvojene dijelove datoteka.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurirajte parametre za vektorizaciju objekata. Ovaj će proces generirati vektorska ugrađivanja za sve objekte koji odgovaraju vašim filtrima prikaza.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurirajte koje tipove podataka pretraživati i koliko izvora dohvatiti", + "Connection Failed" : "Povezivanje nije uspjelo", + "Connection Settings" : "Postavke povezivanja", + "Connection Successful!" : "Povezivanje uspješno!", + "Connection failed" : "Povezivanje nije uspjelo", + "Connection protocol" : "Protokol povezivanja", + "Connection successful" : "Povezivanje uspješno", + "Connection timeout in seconds" : "Istek vremena povezivanja u sekundama", + "Constant delay between retries (5 minutes)" : "Stalna odgoda između ponavljanja (5 minuta)", + "Constraint Issue" : "Problem s ograničenjem", + "Content Filters" : "Filtri sadržaja", + "Continue to Properties" : "Nastavi na svojstva", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontrolirajte koje prikaze objekata treba vektorizirati radi smanjenja troškova API-ja. Vektorizirajte samo prikaze koji imaju koristi od semantičkog pretraživanja.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontrolirajte koje prikaze i alate AI može koristiti u ovom razgovoru. Prema zadanim postavkama, sve mogućnosti agenta su omogućene.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Kontrolira nasumičnost (0 = determinističko, 2 = vrlo kreativno)", + "Conversation ID is required" : "ID razgovora je obavezan", + "Conversation archived" : "Razgovor arhiviran", + "Conversation archived successfully" : "Razgovor uspješno arhiviran", + "Conversation cleared successfully" : "Razgovor uspješno očišćen", + "Conversation deleted" : "Razgovor izbrisan", + "Conversation permanently deleted" : "Razgovor trajno izbrisan", + "Conversation renamed" : "Razgovor preimenovan", + "Conversation restored" : "Razgovor vraćen", + "Conversation title" : "Naslov razgovora", + "Copied!" : "Kopirano!", + "Copy" : "Kopiraj", + "Copy Changes" : "Kopiraj promjene", + "Copy Collection" : "Kopiraj zbirku", + "Copy Data" : "Kopiraj podatke", + "Copy Full Data" : "Kopiraj sve podatke", + "Copying..." : "Kopiranje...", + "Count" : "Broj", + "Counting objects..." : "Brojanje objekata...", + "Costs" : "Troškovi", + "Create" : "Stvori", + "Create Collection" : "Stvori zbirku", + "Create ConfigSet" : "Stvori ConfigSet", + "Create New Collection" : "Stvori novu zbirku", + "Create New ConfigSet" : "Stvori novi ConfigSet", + "Create Operations" : "Operacije stvaranja", + "Create Webhook" : "Stvori webhook", + "Create a copy of collection:" : "Stvori kopiju zbirke:", + "Create a new ConfigSet based on the _default template" : "Stvori novi ConfigSet na temelju predloška _default", + "Create a new SOLR collection from an existing ConfigSet" : "Stvori novu SOLR zbirku iz postojećeg ConfigSeta", + "Create your first AI agent to get started." : "Stvorite svog prvog AI agenta za početak.", + "Created" : "Stvoreno", + "Created:" : "Stvoreno:", + "Creating..." : "Stvaranje...", + "Current Filters" : "Trenutni filtri", + "Current Type:" : "Trenutni tip:", + "Custom API endpoint if using a different region" : "Prilagođena krajnja točka API-ja ako se koristi drugačija regija", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Prilagođena HTTP zaglavlja (jedno po retku, format: Header-Name: value)", + "Daily" : "Dnevno", + "Dashboard" : "Nadzorna ploča", + "Data Source" : "Izvor podataka", + "Data sources" : "Izvori podataka", + "Data type does not match observed values" : "Tip podataka ne odgovara promatranim vrijednostima", + "Database URL" : "URL baze podataka", + "Database information refreshed" : "Informacije o bazi podataka osvježene", + "Date Range" : "Raspon datuma", + "Default" : "Zadano", + "Default Organisation" : "Zadana organizacija", + "Default: 5" : "Zadano: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Odgode se udvostručuju sa svakim pokušajem (2, 4, 8 minuta...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Odgode se linearno povećavaju (5, 10, 15 minuta...)", + "Delete" : "Izbriši", + "Delete Application" : "Izbriši aplikaciju", + "Delete Audit Trail" : "Izbriši trag revizije", + "Delete Collection" : "Izbriši zbirku", + "Delete ConfigSet" : "Izbriši ConfigSet", + "Delete Objects" : "Izbriši objekte", + "Delete Operations" : "Operacije brisanja", + "Delete View" : "Izbriši prikaz", + "Delete all objects in this schema" : "Izbriši sve objekte u ovoj shemi", + "Delete permanently" : "Izbriši trajno", + "Delete this conversation?" : "Izbrisati ovaj razgovor?", + "Delete view" : "Izbriši prikaz", + "Deleted" : "Izbrisano", + "Deleted By" : "Izbrisao", + "Deleted Date" : "Datum brisanja", + "Deleted Items Management" : "Upravljanje izbrisanim stavkama", + "Deleted This Week" : "Izbrisano ovaj tjedan", + "Deleted Today" : "Izbrisano danas", + "Deleted:" : "Izbrisano:", + "Deleting..." : "Brisanje...", + "Deletion Date Range" : "Raspon datuma brisanja", + "Deletion Statistics" : "Statistika brisanja", + "Deprecated" : "Zastarjelo", + "Description" : "Opis", + "Description:" : "Opis:", + "Detect data types and patterns" : "Otkrij tipove podataka i obrasce", + "Detected At" : "Otkriveno u", + "Detected Format:" : "Otkriveni format:", + "Detected Issues:" : "Otkriveni problemi:", + "Disable" : "Onemogući", + "Disabled" : "Onemogućeno", + "Discover Files" : "Otkrij datoteke", + "Discovered Properties" : "Otkrivena svojstva", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Želite li trajno izbrisati sve filtrirane unose traga revizije? Ova se radnja ne može poništiti.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Želite li trajno izbrisati ovaj unos traga revizije? Ova se radnja ne može poništiti.", + "Documents" : "Dokumenti", + "Download API Spec" : "Preuzmi API specifikaciju", + "Edit" : "Uredi", + "Edit Register" : "Uredi registar", + "Edit Schema" : "Uredi shemu", + "Edit View" : "Uredi prikaz", + "Edit Webhook" : "Uredi webhook", + "Edit view" : "Uredi prikaz", + "Edit view details" : "Uredi pojedinosti prikaza", + "Effectiveness" : "Učinkovitost", + "Email" : "E-pošta", + "Emails" : "E-pošte", + "Embedding Model" : "Model ugrađivanja", + "Embedding Provider" : "Pružatelj ugrađivanja", + "Embedding generated successfully" : "Ugrađivanje uspješno generirano", + "Embedding provider connection successful!" : "Povezivanje s pružateljem ugrađivanja uspješno!", + "Enable" : "Omogući", + "Enable auto-creation" : "Omogući automatsko stvaranje", + "Enable automatic file vectorization" : "Omogući automatsku vektorizaciju datoteka", + "Enable automatic object vectorization" : "Omogući automatsku vektorizaciju objekata", + "Enable automatic synchronization" : "Omogući automatsku sinkronizaciju", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Omogući detaljno bilježenje za SOLR operacije (preporučeno za otklanjanje pogrešaka)", + "Enable faceting" : "Omogući fasetiranje", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Omogućite ili onemogućite LLM značajke. Konfigurirajte pružatelje i modele pomoću gumba LLM konfiguracija iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Omogućite ili onemogućite integraciju SOLR pretraživanja. Konfigurirajte postavke povezivanja pomoću gumba Postavke povezivanja iznad.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Omogućite ili onemogućite integraciju n8n tijeka rada. Konfigurirajte postavke povezivanja u nastavku.", + "Enable or disable this webhook" : "Omogućite ili onemogućite ovaj webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Omogući vektorizaciju za sve postojeće i buduće prikaze (može povećati troškove)", + "Enabled" : "Omogućeno", + "Endpoints" : "Krajnje točke", + "Enter ConfigSet name" : "Unesite naziv ConfigSeta", + "Enter collection name" : "Unesite naziv zbirke", + "Enter description (optional)..." : "Unesite opis (neobavezno)...", + "Enter new collection name" : "Unesite novi naziv zbirke", + "Enter object ID" : "Unesite ID objekta", + "Enter search term" : "Unesite pojam za pretraživanje", + "Enter view name..." : "Unesite naziv prikaza...", + "Enter webhook name" : "Unesite naziv webhooka", + "Entities" : "Entiteti", + "Entity Information" : "Informacije o entitetu", + "Entity deleted successfully" : "Entitet uspješno izbrisan", + "Entity not found" : "Entitet nije pronađen", + "Entries to be deleted:" : "Unosi za brisanje:", + "Enum Issue" : "Problem s enumeracijom", + "Enum constraint is missing" : "Nedostaje ograničenje enumeracije", + "Error" : "Pogreška", + "Error Details" : "Pojedinosti pogreške", + "Error Information" : "Informacije o pogrešci", + "Error Message" : "Poruka o pogrešci", + "Error loading application" : "Pogreška pri učitavanju aplikacije", + "Error loading audit trails" : "Pogreška pri učitavanju tragova revizije", + "Error loading entity" : "Pogreška pri učitavanju entiteta", + "Error loading search trails" : "Pogreška pri učitavanju tragova pretraživanja", + "Estimated Batches:" : "Procijenjeni broj serija:", + "Estimated Cost:" : "Procijenjeni trošak:", + "Estimated Duration:" : "Procijenjeno trajanje:", + "Event" : "Događaj", + "Event Property for Payload" : "Svojstvo događaja za korisni teret", + "Events" : "Događaji", + "Example Value" : "Primjer vrijednosti", + "Exclusive Maximum" : "Isključivi maksimum", + "Exclusive Minimum" : "Isključivi minimum", + "Execution Mode" : "Način izvršavanja", + "Execution Time" : "Vrijeme izvršavanja", + "Execution Time Range" : "Raspon vremena izvršavanja", + "Existing Improvements" : "Postojeća poboljšanja", + "Exponential" : "Eksponencijalno", + "Export" : "Izvoz", + "Export completed successfully" : "Izvoz uspješno dovršen", + "Export, view, or delete audit trails" : "Izvezite, pregledajte ili izbrišite tragove revizije", + "Extend Schema" : "Proširi shemu", + "Extended" : "Prošireno", + "Extensions" : "Proširenja", + "Extract Pending Files" : "Izdvoji datoteke na čekanju", + "Extract properties from each object" : "Izdvoji svojstva iz svakog objekta", + "Extracted At" : "Izdvojeno u", + "Extraction Status" : "Status izdvajanja", + "Facet configuration updated successfully" : "Konfiguracija faseta uspješno ažurirana", + "Facets discovered and configured successfully" : "Fasete uspješno otkrivene i konfigurirane", + "Facets discovered successfully" : "Fasete uspješno otkrivene", + "Failed" : "Neuspjelo", + "Failed to analyze schema properties" : "Analiza svojstava sheme nije uspjela", + "Failed to archive conversation" : "Arhiviranje razgovora nije uspjelo", + "Failed to calculate sizes" : "Izračun veličina nije uspio", + "Failed to clear collection" : "Čišćenje zbirke nije uspjelo", + "Failed to copy changes" : "Kopiranje promjena nije uspjelo", + "Failed to copy data" : "Kopiranje podataka nije uspjelo", + "Failed to copy data to clipboard" : "Kopiranje podataka u međuspremnik nije uspjelo", + "Failed to create conversation" : "Stvaranje razgovora nije uspjelo", + "Failed to create or find project" : "Stvaranje ili pronalaženje projekta nije uspjelo", + "Failed to delete collection" : "Brisanje zbirke nije uspjelo", + "Failed to delete conversation" : "Brisanje razgovora nije uspjelo", + "Failed to delete webhook" : "Brisanje webhooka nije uspjelo", + "Failed to download API specification" : "Preuzimanje API specifikacije nije uspjelo", + "Failed to get SOLR field configuration" : "Dohvaćanje konfiguracije SOLR polja nije uspjelo", + "Failed to get entity categories" : "Dohvaćanje kategorija entiteta nije uspjelo", + "Failed to get entity statistics" : "Dohvaćanje statistike entiteta nije uspjelo", + "Failed to get entity types" : "Dohvaćanje tipova entiteta nije uspjelo", + "Failed to load LLM configuration" : "Učitavanje LLM konfiguracije nije uspjelo", + "Failed to load Nextcloud groups" : "Učitavanje Nextcloud grupa nije uspjelo", + "Failed to load conversation" : "Učitavanje razgovora nije uspjelo", + "Failed to load entities" : "Učitavanje entiteta nije uspjelo", + "Failed to load entity" : "Učitavanje entiteta nije uspjelo", + "Failed to load files" : "Učitavanje datoteka nije uspjelo", + "Failed to load organisations" : "Učitavanje organizacija nije uspjelo", + "Failed to load templates" : "Učitavanje predložaka nije uspjelo", + "Failed to load webhooks" : "Učitavanje webhookova nije uspjelo", + "Failed to load workflows" : "Učitavanje tijekova rada nije uspjelo", + "Failed to refresh database information" : "Osvježavanje informacija o bazi podataka nije uspjelo", + "Failed to reindex collection" : "Ponovno indeksiranje zbirke nije uspjelo", + "Failed to rename conversation" : "Preimenovanje razgovora nije uspjelo", + "Failed to restore conversation" : "Vraćanje razgovora nije uspjelo", + "Failed to retry extraction" : "Ponovni pokušaj izdvajanja nije uspio", + "Failed to retry webhook" : "Ponovni pokušaj webhooka nije uspio", + "Failed to save GitHub token" : "Spremanje GitHub tokena nije uspjelo", + "Failed to save GitLab URL" : "Spremanje GitLab URL-a nije uspjelo", + "Failed to save GitLab token" : "Spremanje GitLab tokena nije uspjelo", + "Failed to save additional feedback" : "Spremanje dodatnih povratnih informacija nije uspjelo", + "Failed to save n8n configuration" : "Spremanje n8n konfiguracije nije uspjelo", + "Failed to save roles" : "Spremanje uloga nije uspjelo", + "Failed to save settings" : "Spremanje postavki nije uspjelo", + "Failed to save webhook" : "Spremanje webhooka nije uspjelo", + "Failed to send feedback" : "Slanje povratnih informacija nije uspjelo", + "Failed to test webhook" : "Testiranje webhooka nije uspjelo", + "Failed to update favorite status" : "Ažuriranje statusa favorita nije uspjelo", + "Failed to update schema properties" : "Ažuriranje svojstava sheme nije uspjelo", + "Failed to update view" : "Ažuriranje prikaza nije uspjelo", + "Failed to update webhook" : "Ažuriranje webhooka nije uspjelo", + "Feedback recorded" : "Povratne informacije zabilježene", + "Fewer sources (1-3):" : "Manje izvora (1-3):", + "Field" : "Polje", + "File" : "Datoteka", + "File Chunk Prediction" : "Predviđanje dijelova datoteke", + "File Collection" : "Zbirka datoteka", + "File Management" : "Upravljanje datotekama", + "File Name" : "Naziv datoteke", + "File Path" : "Putanja datoteke", + "File Type Selection" : "Odabir tipa datoteke", + "File Vectorization" : "Vektorizacija datoteka", + "File Warmup" : "Zagrijavanje datoteka", + "File actions menu" : "Izbornik radnji datoteke", + "File anonymized successfully" : "Datoteka uspješno anonimizirana", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vektorizacija dijelova datoteke još nije implementirana. Dijelovi su spremni i pohranjeni, ali usluga vektorizacije je u razvoju.", + "File collection not configured" : "Zbirka datoteka nije konfigurirana", + "File discovery completed" : "Otkrivanje datoteka dovršeno", + "File extraction completed" : "Izdvajanje datoteka dovršeno", + "File extraction queued" : "Izdvajanje datoteka stavljeno u red", + "File indexed successfully" : "Datoteka uspješno indeksirana", + "File is already anonymized" : "Datoteka je već anonimizirana", + "File not found" : "Datoteka nije pronađena", + "File settings updated successfully" : "Postavke datoteke uspješno ažurirane", + "File sources" : "Izvori datoteka", + "File vectorization configuration saved successfully" : "Konfiguracija vektorizacije datoteka uspješno spremljena", + "File vectorization started. Check the statistics section for progress." : "Vektorizacija datoteka započela. Provjerite odjeljak statistike za napredak.", + "File warmup completed" : "Zagrijavanje datoteka dovršeno", + "Files" : "Datoteke", + "Files with Completed Extraction:" : "Datoteke s dovršenim izdvajanjem:", + "Files → fileCollection, Objects → objectCollection" : "Datoteke → fileCollection, Objekti → objectCollection", + "Filter Audit Trails" : "Filtriraj tragove revizije", + "Filter Deleted Items" : "Filtriraj izbrisane stavke", + "Filter Objects" : "Filtriraj objekte", + "Filter Properties" : "Filtriraj svojstva", + "Filter Search Trails" : "Filtriraj tragove pretraživanja", + "Filter Statistics" : "Statistika filtara", + "Filter and analyze search trail entries" : "Filtrirajte i analizirajte unose tragova pretraživanja", + "Filter and manage audit trail entries" : "Filtrirajte i upravljajte unosima tragova revizije", + "Filter and manage soft deleted items" : "Filtrirajte i upravljajte meko izbrisanim stavkama", + "Filter by object ID" : "Filtriraj po ID-u objekta", + "Filter by search term" : "Filtriraj po pojmu za pretraživanje", + "Filter by webhook" : "Filtriraj po webhooku", + "Filter data loaded automatically. Use the filters below to refine your search." : "Podaci filtra automatski učitani. Koristite filtre u nastavku za pročišćavanje pretraživanja.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrirajte okidače webhooka po svojstvima korisnog tereta (jedno po retku, format: key: value)", + "Filtered" : "Filtrirano", + "Filters" : "Filtri", + "Fireworks AI Chat Settings" : "Postavke razgovora Fireworks AI", + "Fireworks AI Embedding Configuration" : "Konfiguracija ugrađivanja Fireworks AI", + "First" : "Prvi", + "Fixed" : "Popravljeno", + "For chat and retrieval-augmented generation" : "Za razgovor i generiranje uz pomoć dohvaćanja", + "For vector embeddings and semantic search" : "Za vektorska ugrađivanja i semantičko pretraživanje", + "Format" : "Format", + "Format Issue" : "Problem s formatom", + "Format constraint is missing" : "Nedostaje ograničenje formata", + "From Date" : "Od datuma", + "From date" : "Od datuma", + "Full data copied to clipboard" : "Svi podaci kopirani u međuspremnik", + "General Issue" : "Opći problem", + "Generate recommendations and confidence scores" : "Generiraj preporuke i ocjene pouzdanosti", + "Generate vectors immediately when new objects are created" : "Generiraj vektore odmah kada se stvore novi objekti", + "GitHub token is valid" : "GitHub token je valjan", + "GitHub token removed successfully" : "GitHub token uspješno uklonjen", + "GitHub token saved successfully" : "GitHub token uspješno spremljen", + "GitLab URL saved successfully" : "GitLab URL uspješno spremljen", + "GitLab token is valid" : "GitLab token je valjan", + "GitLab token saved successfully" : "GitLab token uspješno spremljen", + "HTTP Method" : "HTTP metoda", + "HTTP method used to send webhook requests" : "HTTP metoda koja se koristi za slanje webhook zahtjeva", + "Headers" : "Zaglavlja", + "Health" : "Zdravlje", + "Heartbeat successful - connection kept alive" : "Otkucaj uspješan - veza održana aktivnom", + "Helpful" : "Korisno", + "Hide Filters" : "Sakrij filtre", + "Hide in forms" : "Sakrij u obrascima", + "Hide in list view" : "Sakrij u prikazu popisa", + "High" : "Visoko", + "High Confidence" : "Visoka pouzdanost", + "Host" : "Domaćin", + "Hourly" : "Po satu", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Koliko duboko prolaziti kroz ugniježđena svojstva objekta (1-20). Veće vrijednosti hvataju više detalja, ali povećavaju veličinu vektora.", + "How often to check for updates (1-168 hours)" : "Koliko često provjeravati ažuriranja (1-168 sati)", + "How to handle retries for failed webhook deliveries" : "Kako rukovati ponavljanjima za neuspjele isporuke webhooka", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Ograničenja brzine IP-a uspješno očišćena", + "Identify properties not in the schema" : "Identificiraj svojstva koja nisu u shemi", + "Immutable" : "Nepromjenjivo", + "Import" : "Uvoz", + "Import successful" : "Uvoz uspješan", + "Improved Property" : "Poboljšano svojstvo", + "In use" : "U upotrebi", + "Inactive" : "Neaktivno", + "Include IDs and names of related objects for better contextual search" : "Uključi ID-ove i nazive povezanih objekata za bolje kontekstualno pretraživanje", + "Include related object references" : "Uključi reference povezanih objekata", + "Include schema and register metadata" : "Uključi metapodatke sheme i registra", + "Initialization failed" : "Inicijalizacija nije uspjela", + "Initialize Project" : "Inicijaliziraj projekt", + "Inspect Fields" : "Pregledaj polja", + "Inspect Index" : "Pregledaj indeks", + "Invalid" : "Nevaljano", + "Invalid batch size. Must be between 1 and 5000" : "Nevaljana veličina serije. Mora biti između 1 i 5000", + "Invalid field name provided" : "Naveden je nevaljan naziv polja", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Nevaljan maxObjects. Mora biti 0 (svi) ili pozitivan broj", + "Invalid:" : "Nevaljano:", + "Issue" : "Problem", + "Items per page:" : "Stavki po stranici:", + "Just now" : "Upravo sada", + "LLM Configuration" : "LLM konfiguracija", + "LLM actions menu" : "Izbornik LLM radnji", + "LLM configuration saved successfully" : "LLM konfiguracija uspješno spremljena", + "LLM features disabled" : "LLM značajke onemogućene", + "LLM features enabled" : "LLM značajke omogućene", + "LLM must be enabled with an embedding provider configured" : "LLM mora biti omogućen s konfiguriranim pružateljem ugrađivanja", + "LLM settings updated successfully" : "LLM postavke uspješno ažurirane", + "Last" : "Posljednji", + "Last Triggered" : "Posljednji put aktivirano", + "Length Range:" : "Raspon duljine:", + "Linear" : "Linearno", + "Load Advanced Filters" : "Učitaj napredne filtre", + "Load advanced filters with live data from your search index" : "Učitajte napredne filtre s podacima uživo iz vašeg indeksa pretraživanja", + "Load view" : "Učitaj prikaz", + "Loading ConfigSets..." : "Učitavanje ConfigSetova...", + "Loading advanced filters..." : "Učitavanje naprednih filtara...", + "Loading agents..." : "Učitavanje agenata...", + "Loading application details..." : "Učitavanje pojedinosti aplikacije...", + "Loading applications..." : "Učitavanje aplikacija...", + "Loading archived conversations..." : "Učitavanje arhiviranih razgovora...", + "Loading audit trails..." : "Učitavanje tragova revizije...", + "Loading collections..." : "Učitavanje zbirki...", + "Loading configurations..." : "Učitavanje konfiguracija...", + "Loading conversation..." : "Učitavanje razgovora...", + "Loading conversations..." : "Učitavanje razgovora...", + "Loading deleted items..." : "Učitavanje izbrisanih stavki...", + "Loading events..." : "Učitavanje događaja...", + "Loading filter data..." : "Učitavanje podataka filtra...", + "Loading groups..." : "Učitavanje grupa...", + "Loading log details..." : "Učitavanje pojedinosti zapisnika...", + "Loading registers..." : "Učitavanje registara...", + "Loading schemas..." : "Učitavanje shema...", + "Loading search trails..." : "Učitavanje tragova pretraživanja...", + "Loading sources..." : "Učitavanje izvora...", + "Loading statistics..." : "Učitavanje statistike...", + "Loading users..." : "Učitavanje korisnika...", + "Loading views..." : "Učitavanje prikaza...", + "Loading..." : "Učitavanje...", + "Local" : "Lokalno", + "Local Version" : "Lokalna verzija", + "Locked" : "Zaključano", + "Locked:" : "Zaključano:", + "Login successful" : "Prijava uspješna", + "Logs" : "Zapisnici", + "Low" : "Nisko", + "Low Confidence" : "Niska pouzdanost", + "Lucene Version:" : "Lucene verzija:", + "Magic table synchronized successfully" : "Čarobna tablica uspješno sinkronizirana", + "Manage Organisation Roles" : "Upravljaj ulogama organizacije", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Upravljajte SOLR zbirkama (spremištima podataka) i dodijelite ih za objekte i datoteke.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Upravljajte SOLR ConfigSetovima (konfiguracijskim predlošcima) za vaše zbirke.", + "Manage and configure agents for automated tasks" : "Upravljajte i konfigurirajte agente za automatizirane zadatke", + "Manage and monitor file text extraction status" : "Upravljajte i nadzirite status izdvajanja teksta iz datoteka", + "Manage and restore soft deleted items from your registers" : "Upravljajte i vraćajte meko izbrisane stavke iz svojih registara", + "Manage and view detected entities from files and objects" : "Upravljajte i pregledajte otkrivene entitete iz datoteka i objekata", + "Manage document templates and themes" : "Upravljajte predlošcima dokumenata i temama", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Upravljajte n8n tijekovima rada za automatizaciju OpenRegistera. Tijekovi rada bit će pohranjeni u konfiguriranom projektu.", + "Manage webhooks for event-driven integrations" : "Upravljajte webhookovima za integracije pokretane događajima", + "Manage your applications and modules" : "Upravljajte svojim aplikacijama i modulima", + "Manage your data registers and their configurations" : "Upravljajte svojim registrima podataka i njihovim konfiguracijama", + "Manage your data schemas and their properties" : "Upravljajte svojim shemama podataka i njihovim svojstvima", + "Manage your data sources and their configurations" : "Upravljajte svojim izvorima podataka i njihovim konfiguracijama", + "Manage your saved search configurations" : "Upravljajte svojim spremljenim konfiguracijama pretraživanja", + "Manage your system configurations and settings" : "Upravljajte svojim konfiguracijama i postavkama sustava", + "Managed" : "Upravljano", + "Max Files (0 = all)" : "Najviše datoteka (0 = sve)", + "Max Length" : "Najveća duljina", + "Max Length:" : "Najveća duljina:", + "Max Objects (0 = all)" : "Najviše objekata (0 = svi)", + "Max Retries" : "Najviše ponavljanja", + "Max Shards/Node" : "Najviše dijelova/čvoru", + "Max execution time (ms)" : "Najduže vrijeme izvršavanja (ms)", + "Max ms" : "Najviše ms", + "Max result count" : "Najveći broj rezultata", + "Max results" : "Najviše rezultata", + "Maximum" : "Maksimum", + "Maximum Nesting Depth" : "Najveća dubina ugnježđivanja", + "Maximum length constraint is missing" : "Nedostaje ograničenje najveće duljine", + "Maximum length is too restrictive" : "Najveća duljina je previše restriktivna", + "Maximum number of files to process. Set to 0 to process all files." : "Najveći broj datoteka za obradu. Postavite na 0 za obradu svih datoteka.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Najveći broj objekata za obradu. Postavite na 0 za obradu svih objekata.", + "Maximum number of retry attempts for failed deliveries" : "Najveći broj pokušaja ponavljanja za neuspjele isporuke", + "Maximum time to wait before committing changes" : "Najduže vrijeme čekanja prije predavanja promjena", + "Maximum tokens to generate" : "Najveći broj tokena za generiranje", + "Maximum value constraint is missing" : "Nedostaje ograničenje najveće vrijednosti", + "Maximum value is too restrictive" : "Najveća vrijednost je previše restriktivna", + "Medium" : "Srednje", + "Medium Confidence" : "Srednja pouzdanost", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Srednji upiti: pretraživanja s nekim filtriranjem ili više parametara (npr. rasponi datuma, specifični registri/sheme)", + "Members of selected groups can access this view" : "Članovi odabranih grupa mogu pristupiti ovom prikazu", + "Members:" : "Članovi:", + "Memory prediction calculated successfully" : "Predviđanje memorije uspješno izračunato", + "Message content is required" : "Sadržaj poruke je obavezan", + "Message does not belong to this conversation" : "Poruka ne pripada ovom razgovoru", + "Metadata Filters" : "Filtri metapodataka", + "Method" : "Metoda", + "Min Length" : "Najmanja duljina", + "Min execution time (ms)" : "Najkraće vrijeme izvršavanja (ms)", + "Min ms" : "Najmanje ms", + "Min result count" : "Najmanji broj rezultata", + "Min results" : "Najmanje rezultata", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Nedostaje ograničenje najmanje vrijednosti", + "Minimum value is too restrictive" : "Najmanja vrijednost je previše restriktivna", + "Missing object fields created successfully" : "Nedostajuća polja objekta uspješno stvorena", + "Mode:" : "Način:", + "Model" : "Model", + "Model Name" : "Naziv modela", + "Modified" : "Izmijenjeno", + "Monthly" : "Mjesečno", + "More sources (10-20):" : "Više izvora (10-20):", + "Most Active Objects" : "Najaktivniji objekti", + "Multiple Of" : "Višekratnik od", + "Name" : "Naziv", + "Names cache warmup completed" : "Zagrijavanje predmemorije naziva dovršeno", + "New Collection Name" : "Naziv nove zbirke", + "New Conversation" : "Novi razgovor", + "New Properties" : "Nova svojstva", + "New Property" : "Novo svojstvo", + "New Value" : "Nova vrijednost", + "New users without specific organisation membership will be automatically added to this organisation" : "Novi korisnici bez određenog članstva u organizaciji bit će automatski dodani ovoj organizaciji", + "Next" : "Sljedeće", + "No" : "Ne", + "No ConfigSets found" : "Nije pronađen nijedan ConfigSet", + "No GitHub token configured" : "Nijedan GitHub token nije konfiguriran", + "No GitHub token provided" : "Nijedan GitHub token nije naveden", + "No GitLab token provided" : "Nijedan GitLab token nije naveden", + "No Organisation" : "Nema organizacije", + "No active filters" : "Nema aktivnih filtara", + "No activity data available" : "Nema dostupnih podataka o aktivnosti", + "No agents available" : "Nema dostupnih agenata", + "No agents found" : "Nije pronađen nijedan agent", + "No app store cache exists yet" : "Predmemorija trgovine aplikacija još ne postoji", + "No applications are available." : "Nema dostupnih aplikacija.", + "No applications found" : "Nije pronađena nijedna aplikacija", + "No archived conversations" : "Nema arhiviranih razgovora", + "No audit trail entries found" : "Nije pronađen nijedan unos traga revizije", + "No changes recorded" : "Nisu zabilježene promjene", + "No chunks to vectorize" : "Nema dijelova za vektorizaciju", + "No collections found" : "Nije pronađena nijedna zbirka", + "No configuration data" : "Nema podataka konfiguracije", + "No configurations are available." : "Nema dostupnih konfiguracija.", + "No configurations found" : "Nije pronađena nijedna konfiguracija", + "No configurations found for this application." : "Za ovu aplikaciju nije pronađena nijedna konfiguracija.", + "No conversations yet. Create a new one to get started!" : "Još nema razgovora. Stvorite novi za početak!", + "No data available" : "Nema dostupnih podataka", + "No data available for chart" : "Nema dostupnih podataka za grafikon", + "No deleted items found" : "Nije pronađena nijedna izbrisana stavka", + "No deletion data available" : "Nema dostupnih podataka o brisanju", + "No description found" : "Nije pronađen nijedan opis", + "No description provided" : "Nije naveden nijedan opis", + "No entities detected in this file. Run text extraction first." : "U ovoj datoteci nije otkriven nijedan entitet. Prvo pokrenite izdvajanje teksta.", + "No entities found" : "Nije pronađen nijedan entitet", + "No entities have been detected yet" : "Još nije otkriven nijedan entitet", + "No events found" : "Nije pronađen nijedan događaj", + "No expired audit trails found to clear" : "Nije pronađen nijedan istekli trag revizije za čišćenje", + "No facetable fields available. Select a register and schema to see available filters." : "Nema dostupnih polja za fasetiranje. Odaberite registar i shemu kako biste vidjeli dostupne filtre.", + "No files found" : "Nije pronađena nijedna datoteka", + "No files have been extracted yet" : "Još nije izdvojena nijedna datoteka", + "No files to process" : "Nema datoteka za obradu", + "No files to reindex" : "Nema datoteka za ponovno indeksiranje", + "No filters are currently active. This will delete ALL audit trail entries!" : "Trenutno nema aktivnih filtara. Time će se izbrisati SVI unosi traga revizije!", + "No log entries found" : "Nije pronađen nijedan unos zapisnika", + "No logs are available for this configuration." : "Za ovu konfiguraciju nema dostupnih zapisnika.", + "No logs are available for this source." : "Za ovaj izvor nema dostupnih zapisnika.", + "No logs found" : "Nije pronađen nijedan zapisnik", + "No mismatched fields found - SOLR schema is properly configured" : "Nije pronađeno nijedno neusklađeno polje - SOLR shema je ispravno konfigurirana", + "No objects found" : "Nije pronađen nijedan objekt", + "No objects found to vectorize" : "Nije pronađen nijedan objekt za vektorizaciju", + "No objects selected for deletion" : "Nijedan objekt nije odabran za brisanje", + "No objects to delete" : "Nema objekata za brisanje", + "No objects were permanently deleted" : "Nijedan objekt nije trajno izbrisan", + "No properties found" : "Nije pronađeno nijedno svojstvo", + "No properties match your filters." : "Nijedno svojstvo ne odgovara vašim filtrima.", + "No purge date set" : "Nije postavljen datum čišćenja", + "No register data available" : "Nema dostupnih podataka registra", + "No registers found" : "Nije pronađen nijedan registar", + "No registers found for this application." : "Za ovu aplikaciju nije pronađen nijedan registar.", + "No relations found" : "Nije pronađena nijedna veza", + "No request body available" : "Nema dostupnog tijela zahtjeva", + "No response body available" : "Nema dostupnog tijela odgovora", + "No saved views yet. Create one in the Search tab!" : "Još nema spremljenih prikaza. Stvorite jedan na kartici Pretraživanje!", + "No schema data available" : "Nema dostupnih podataka sheme", + "No schema selected for exploration" : "Nije odabrana nijedna shema za istraživanje", + "No schemas found" : "Nije pronađena nijedna shema", + "No schemas found for this application." : "Za ovu aplikaciju nije pronađena nijedna shema.", + "No search terms data available" : "Nema dostupnih podataka o pojmovima za pretraživanje", + "No search trail entries found" : "Nije pronađen nijedan unos traga pretraživanja", + "No sources are available." : "Nema dostupnih izvora.", + "No sources found" : "Nije pronađen nijedan izvor", + "No templates found" : "Nije pronađen nijedan predložak", + "No templates have been created yet" : "Još nije stvoren nijedan predložak", + "No valid magic-mapped register+schema combinations found" : "Nije pronađena nijedna valjana čarobno-mapirana kombinacija registar+shema", + "No views found. Create views first before configuring vectorization." : "Nije pronađen nijedan prikaz. Prvo stvorite prikaze prije konfiguriranja vektorizacije.", + "No views match your search" : "Nijedan prikaz ne odgovara vašem pretraživanju", + "No webhooks found" : "Nije pronađen nijedan webhook", + "No webhooks have been configured yet" : "Još nije konfiguriran nijedan webhook", + "No workflows found in this project. Create workflows in the n8n editor." : "U ovom projektu nije pronađen nijedan tijek rada. Stvorite tijekove rada u n8n uređivaču.", + "None" : "Ništa", + "Not helpful" : "Nije korisno", + "Not in use" : "Nije u upotrebi", + "Number Constraints" : "Ograničenja broja", + "Number of chunks to vectorize in one API call (1-100)." : "Broj dijelova za vektorizaciju u jednom API pozivu (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Broj dijelova za vektorizaciju u jednom API pozivu. Veći = brže, ali više memorije. Preporučeno: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Broj objekata za obradu u svakoj seriji (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Broj objekata za vektorizaciju u jednom API pozivu. Veći = brže, ali više memorije. Preporučeno: 10-50.", + "Numeric Range:" : "Numerički raspon:", + "Object" : "Objekt", + "Object Collection" : "Zbirka objekata", + "Object Count Prediction" : "Predviđanje broja objekata", + "Object ID" : "ID objekta", + "Object Management" : "Upravljanje objektima", + "Object Vectorization" : "Vektorizacija objekata", + "Object Warmup" : "Zagrijavanje objekata", + "Object collection not configured" : "Zbirka objekata nije konfigurirana", + "Object not found" : "Objekt nije pronađen", + "Object permanently deleted" : "Objekt trajno izbrisan", + "Object restored successfully" : "Objekt uspješno vraćen", + "Object settings updated successfully" : "Postavke objekta uspješno ažurirane", + "Object sources" : "Izvori objekata", + "Object unlocked successfully" : "Objekt uspješno otključan", + "Object vectorization configuration saved successfully" : "Konfiguracija vektorizacije objekata uspješno spremljena", + "Object vectorized successfully" : "Objekt uspješno vektoriziran", + "Object:" : "Objekt:", + "Objects" : "Objekti", + "Objects Analyzed" : "Analizirani objekti", + "Objects Distribution" : "Raspodjela objekata", + "Objects being analyzed" : "Objekti koji se analiziraju", + "Objects by Register" : "Objekti po registru", + "Objects by Schema" : "Objekti po shemi", + "Objects deletion completed successfully" : "Brisanje objekata uspješno dovršeno", + "Objects in schema" : "Objekti u shemi", + "Objects to Process:" : "Objekti za obradu:", + "Objects to be analyzed" : "Objekti za analizu", + "Objects to be deleted" : "Objekti za brisanje", + "Objects to be validated" : "Objekti za provjeru valjanosti", + "Objects will be serialized as JSON text before vectorization" : "Objekti će biti serijalizirani kao JSON tekst prije vektorizacije", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekti će biti meko izbrisani (označeni kao izbrisani, ali zadržani u bazi podataka). Mogu se kasnije vratiti ako je potrebno.", + "Old Value" : "Stara vrijednost", + "Oldest Item (days)" : "Najstarija stavka (dani)", + "Ollama Chat Settings" : "Postavke razgovora Ollama", + "Ollama Embedding Configuration" : "Konfiguracija ugrađivanja Ollama", + "Ollama URL" : "Ollama URL", + "Only With Changes" : "Samo s promjenama", + "Open Register Objects" : "Open Register objekti", + "Open n8n Editor" : "Otvori n8n uređivač", + "OpenAI Chat Settings" : "Postavke razgovora OpenAI", + "OpenAI Embedding Configuration" : "Konfiguracija ugrađivanja OpenAI", + "OpenRegister Settings" : "OpenRegister postavke", + "Optional webhook secret for signature verification" : "Neobavezna tajna webhooka za provjeru potpisa", + "Order" : "Redoslijed", + "Organisation" : "Organizacija", + "Organisation ID" : "ID organizacije", + "Organisation Members" : "Članovi organizacije", + "Organisation Statistics" : "Statistika organizacije", + "Organisation created successfully" : "Organizacija uspješno stvorena", + "Organisation settings saved successfully" : "Postavke organizacije uspješno spremljene", + "Organisations" : "Organizacije", + "Organization" : "Organizacija", + "Organization ID (Optional)" : "ID organizacije (neobavezno)", + "Orphaned Items" : "Stavke siročad", + "Owner" : "Vlasnik", + "Owner:" : "Vlasnik:", + "Parallel Mode (Faster, more resource intensive)" : "Paralelni način (brže, zahtjevnije za resurse)", + "Parallel:" : "Paralelno:", + "Password" : "Lozinka", + "Password for SOLR authentication (optional)" : "Lozinka za SOLR autentifikaciju (neobavezno)", + "Path" : "Putanja", + "Path in repository" : "Putanja u repozitoriju", + "Path where the configuration file will be saved in the repository" : "Putanja na kojoj će se konfiguracijska datoteka spremiti u repozitoriju", + "Path where the register OAS file will be saved in the repository" : "Putanja na kojoj će se OAS datoteka registra spremiti u repozitoriju", + "Pattern (regex)" : "Uzorak (regex)", + "Pattern Issue" : "Problem s uzorkom", + "Pattern constraint is missing" : "Nedostaje ograničenje uzorka", + "Patterns:" : "Uzorci:", + "Pending" : "Na čekanju", + "Permanent Delete Mode" : "Način trajnog brisanja", + "Permanently Delete" : "Trajno izbriši", + "Permanently delete already soft-deleted objects" : "Trajno izbriši već meko izbrisane objekte", + "Permanently delete this conversation?" : "Trajno izbrisati ovaj razgovor?", + "Person" : "Osoba", + "Personal Data" : "Osobni podaci", + "Phone" : "Telefon", + "Photos" : "Fotografije", + "Please create an agent in the" : "Stvorite agenta u", + "Please select an agent to continue" : "Odaberite agenta za nastavak", + "Please select which register and schema to use for the new object" : "Odaberite koji registar i shemu koristiti za novi objekt", + "Please try again later." : "Pokušajte ponovno kasnije.", + "Please wait while we fetch your agents." : "Pričekajte dok dohvaćamo vaše agente.", + "Please wait while we fetch your applications." : "Pričekajte dok dohvaćamo vaše aplikacije.", + "Please wait while we fetch your configurations." : "Pričekajte dok dohvaćamo vaše konfiguracije.", + "Please wait while we fetch your deleted items." : "Pričekajte dok dohvaćamo vaše izbrisane stavke.", + "Please wait while we fetch your sources." : "Pričekajte dok dohvaćamo vaše izvore.", + "Polls" : "Ankete", + "Popular Search Terms" : "Popularni pojmovi za pretraživanje", + "Port" : "Vrata", + "Prerequisites" : "Preduvjeti", + "Press Enter to send, Shift+Enter for new line" : "Pritisnite Enter za slanje, Shift+Enter za novi redak", + "Previous" : "Prethodno", + "Private" : "Privatno", + "Processes chunks in batches with simulated parallelism." : "Obrađuje dijelove u serijama sa simuliranim paralelizmom.", + "Processes file chunks sequentially (safest)." : "Obrađuje dijelove datoteke sekvencijalno (najsigurnije).", + "Processes objects in chunks with simulated parallelism." : "Obrađuje objekte u dijelovima sa simuliranim paralelizmom.", + "Processes objects sequentially (safest)." : "Obrađuje objekte sekvencijalno (najsigurnije).", + "Processing" : "Obrada", + "Processing Limits" : "Ograničenja obrade", + "Processing..." : "Obrada...", + "Project initialized successfully" : "Projekt uspješno inicijaliziran", + "Project not found. Please initialize first." : "Projekt nije pronađen. Prvo inicijalizirajte.", + "Properties" : "Svojstva", + "Property Behaviors" : "Ponašanja svojstva", + "Property Configuration:" : "Konfiguracija svojstva:", + "Property Filters" : "Filtri svojstva", + "Property Title" : "Naslov svojstva", + "Property Type" : "Tip svojstva", + "Property can be improved" : "Svojstvo se može poboljšati", + "Protected" : "Zaštićeno", + "Provider" : "Pružatelj", + "Provider is required for testing" : "Pružatelj je obavezan za testiranje", + "Public" : "Javno", + "Public View" : "Javni prikaz", + "Public views can be accessed by anyone in the system" : "Javnim prikazima može pristupiti bilo tko u sustavu", + "Publish" : "Objavi", + "Publish Objects" : "Objavi objekte", + "Publish all objects in this schema" : "Objavi sve objekte u ovoj shemi", + "Published" : "Objavljeno", + "Published:" : "Objavljeno:", + "Publishing..." : "Objavljivanje...", + "Purge" : "Očisti", + "Purge Date" : "Datum čišćenja", + "Query Complexity Distribution" : "Raspodjela složenosti upita", + "Query parameter is required" : "Parametar upita je obavezan", + "RAG" : "RAG", + "RAG Configuration" : "RAG konfiguracija", + "Rate limits cleared successfully" : "Ograničenja brzine uspješno očišćena", + "Raw Changes Data" : "Sirovi podaci promjena", + "Re-vectorize on object update" : "Ponovno vektoriziraj pri ažuriranju objekta", + "Read" : "Čitaj", + "Recommendations:" : "Preporuke:", + "Recommended Type:" : "Preporučeni tip:", + "Recommended: 5 sources" : "Preporučeno: 5 izvora", + "Refresh" : "Osvježi", + "Refresh Data" : "Osvježi podatke", + "Refresh Stats" : "Osvježi statistiku", + "Refresh Workflows" : "Osvježi tijekove rada", + "Refresh dashboard" : "Osvježi nadzornu ploču", + "Refresh database info" : "Osvježi informacije o bazi podataka", + "Register" : "Registar", + "Register ID" : "ID registra", + "Register Statistics" : "Statistika registra", + "Register Totals" : "Ukupni iznosi registra", + "Register depublished successfully" : "Registar uspješno povučen iz objave", + "Register not found" : "Registar nije pronađen", + "Register objects deletion completed successfully" : "Brisanje objekata registra uspješno dovršeno", + "Register published successfully" : "Registar uspješno objavljen", + "Register/Schema Usage" : "Korištenje registra/sheme", + "Registers" : "Registri", + "Reindex" : "Ponovno indeksiraj", + "Reindex completed" : "Ponovno indeksiranje dovršeno", + "Reindex completed successfully" : "Ponovno indeksiranje uspješno dovršeno", + "Reindex failed" : "Ponovno indeksiranje nije uspjelo", + "Relations" : "Veze", + "Remote Version" : "Udaljena verzija", + "Remove" : "Ukloni", + "Remove filter" : "Ukloni filtar", + "Remove from favorites" : "Ukloni iz favorita", + "Remove group" : "Ukloni grupu", + "Removed" : "Uklonjeno", + "Removed from favorites" : "Uklonjeno iz favorita", + "Rename Conversation" : "Preimenuj razgovor", + "Rename conversation" : "Preimenuj razgovor", + "Replicas" : "Replike", + "Repository" : "Repozitorij", + "Request Body" : "Tijelo zahtjeva", + "Request Data" : "Podaci zahtjeva", + "Request Details" : "Pojedinosti zahtjeva", + "Request body must be JSON with a \"query\" field" : "Tijelo zahtjeva mora biti JSON s poljem \"query\"", + "Request timeout in seconds" : "Istek vremena zahtjeva u sekundama", + "Required field" : "Obavezno polje", + "Required status is inconsistent" : "Obavezni status je nedosljedan", + "Rerun Search" : "Ponovno pokreni pretraživanje", + "Reset Changes" : "Poništi promjene", + "Reset Filters" : "Poništi filtre", + "Response Body" : "Tijelo odgovora", + "Response Details" : "Pojedinosti odgovora", + "Response Time:" : "Vrijeme odgovora:", + "Restore" : "Vrati", + "Restore conversation" : "Vrati razgovor", + "Restore or permanently delete items" : "Vrati ili trajno izbriši stavke", + "Result Count Range" : "Raspon broja rezultata", + "Results" : "Rezultati", + "Retrieve all objects for this schema" : "Dohvati sve objekte za ovu shemu", + "Retry" : "Ponovi", + "Retry Failed Extractions" : "Ponovi neuspjela izdvajanja", + "Retry Policy" : "Pravila ponavljanja", + "Retry completed" : "Ponavljanje dovršeno", + "Retrying..." : "Ponavljanje...", + "Risk Level" : "Razina rizika", + "Roles updated successfully" : "Uloge uspješno ažurirane", + "SOLR Connection Settings" : "Postavke SOLR povezivanja", + "SOLR Version:" : "SOLR verzija:", + "SOLR actions menu" : "Izbornik SOLR radnji", + "SOLR base path (usually /solr)" : "Osnovna SOLR putanja (obično /solr)", + "SOLR is disabled" : "SOLR je onemogućen", + "SOLR is not available or not configured" : "SOLR nije dostupan ili nije konfiguriran", + "SOLR logging disabled" : "SOLR bilježenje onemogućeno", + "SOLR logging enabled" : "SOLR bilježenje omogućeno", + "SOLR search disabled" : "SOLR pretraživanje onemogućeno", + "SOLR search enabled" : "SOLR pretraživanje omogućeno", + "SOLR server hostname or IP address" : "Naziv domaćina ili IP adresa SOLR poslužitelja", + "SOLR server port number (optional, defaults to 8983)" : "Broj vrata SOLR poslužitelja (neobavezno, zadano 8983)", + "SOLR setup completed successfully" : "SOLR postavljanje uspješno dovršeno", + "SOLR setup failed" : "SOLR postavljanje nije uspjelo", + "SOLR setup failed - check logs" : "SOLR postavljanje nije uspjelo - provjerite zapisnike", + "Sample Values:" : "Primjeri vrijednosti:", + "Save" : "Spremi", + "Save Configuration" : "Spremi konfiguraciju", + "Save Connection Settings" : "Spremi postavke povezivanja", + "Save Roles" : "Spremi uloge", + "Save Settings" : "Spremi postavke", + "Save current search as view" : "Spremi trenutno pretraživanje kao prikaz", + "Saved Views" : "Spremljeni prikazi", + "Saving..." : "Spremanje...", + "Schema" : "Shema", + "Schema ID" : "ID sheme", + "Schema Name" : "Naziv sheme", + "Schema Statistics" : "Statistika sheme", + "Schema depublished successfully" : "Shema uspješno povučena iz objave", + "Schema objects deletion completed successfully" : "Brisanje objekata sheme uspješno dovršeno", + "Schema published successfully" : "Shema uspješno objavljena", + "Schema successfully created" : "Shema uspješno stvorena", + "Schema successfully updated" : "Shema uspješno ažurirana", + "Schemas" : "Sheme", + "Scheme" : "Shema", + "Search / Views" : "Pretraživanje / Prikazi", + "Search Activity" : "Aktivnost pretraživanja", + "Search Analytics" : "Analitika pretraživanja", + "Search Entities" : "Pretraži entitete", + "Search Files" : "Pretraži datoteke", + "Search GitHub" : "Pretraži GitHub", + "Search GitLab" : "Pretraži GitLab", + "Search Method" : "Metoda pretraživanja", + "Search Term" : "Pojam za pretraživanje", + "Search Trail Details" : "Pojedinosti traga pretraživanja", + "Search Trail Management" : "Upravljanje tragom pretraživanja", + "Search Trail Statistics" : "Statistika traga pretraživanja", + "Search Trails" : "Tragovi pretraživanja", + "Search Views" : "Pretraži prikaze", + "Search Webhooks" : "Pretraži webhookove", + "Search by file name or path" : "Pretraži po nazivu ili putanji datoteke", + "Search by name or URL" : "Pretraži po nazivu ili URL-u", + "Search by value" : "Pretraži po vrijednosti", + "Search in Files" : "Pretraži u datotekama", + "Search in Objects" : "Pretraži u objektima", + "Search property names..." : "Pretraži nazive svojstava...", + "Search trail deletion not implemented yet" : "Brisanje traga pretraživanja još nije implementirano", + "Search views..." : "Pretraži prikaze...", + "Secret" : "Tajna", + "Secret key for HMAC signature generation (optional)" : "Tajni ključ za generiranje HMAC potpisa (neobavezno)", + "Select AI Agent" : "Odaberi AI agenta", + "Select All" : "Odaberi sve", + "Select ConfigSet" : "Odaberi ConfigSet", + "Select File Types to Vectorize:" : "Odaberi tipove datoteka za vektorizaciju:", + "Select HTTP method" : "Odaberi HTTP metodu", + "Select Register and Schema" : "Odaberi registar i shemu", + "Select Views to Vectorize:" : "Odaberi prikaze za vektorizaciju:", + "Select a Nextcloud group to add" : "Odaberi Nextcloud grupu za dodavanje", + "Select a branch" : "Odaberi granu", + "Select a model or type a custom model name" : "Odaberite model ili upišite prilagođeni naziv modela", + "Select a repository" : "Odaberi repozitorij", + "Select a repository you have write access to" : "Odaberite repozitorij za koji imate pristup za pisanje", + "Select an AI Agent" : "Odaberi AI agenta", + "Select an AI agent to begin chatting with your data." : "Odaberite AI agenta za početak razgovora s vašim podacima.", + "Select backend" : "Odaberi pozadinski sustav", + "Select chat model" : "Odaberi model razgovora", + "Select collection for files" : "Odaberi zbirku za datoteke", + "Select collection for objects" : "Odaberi zbirku za objekte", + "Select default organisation" : "Odaberi zadanu organizaciju", + "Select event to listen to..." : "Odaberi događaj za slušanje...", + "Select model" : "Odaberi model", + "Select options..." : "Odaberi mogućnosti...", + "Select period" : "Odaberi razdoblje", + "Select property to send as payload" : "Odaberi svojstvo za slanje kao korisni teret", + "Select provider" : "Odaberi pružatelja", + "Select registers and schemas to save a view" : "Odaberite registre i sheme za spremanje prikaza", + "Select retry policy" : "Odaberi pravila ponavljanja", + "Select the branch to publish to" : "Odaberi granu za objavu", + "Select the event this webhook should listen to" : "Odaberi događaj koji bi ovaj webhook trebao slušati", + "Select views to vectorize:" : "Odaberi prikaze za vektorizaciju:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Odaberite koje su Nextcloud grupe dostupne za ovu organizaciju. Korisnici u tim grupama imat će pristup resursima organizacije.", + "Select which data views the AI can search" : "Odaberite koje prikaze podataka AI može pretraživati", + "Select which property from the event should be used as the webhook payload data" : "Odaberite koje svojstvo iz događaja treba koristiti kao podatke korisnog tereta webhooka", + "Select which tools the AI can use to perform actions" : "Odaberite koje alate AI može koristiti za izvođenje radnji", + "Selected" : "Odabrano", + "Selected Groups" : "Odabrane grupe", + "Selected audit trails deleted successfully" : "Odabrani tragovi revizije uspješno izbrisani", + "Selected search trails deleted successfully" : "Odabrani tragovi pretraživanja uspješno izbrisani", + "Selected users can access this view" : "Odabrani korisnici mogu pristupiti ovom prikazu", + "Send additional feedback" : "Pošalji dodatne povratne informacije", + "Send as CloudEvent" : "Pošalji kao CloudEvent", + "Sensitive PII" : "Osjetljivi osobni podaci", + "Serial Mode (Safer, slower)" : "Serijski način (sigurnije, sporije)", + "Serial:" : "Serijski:", + "Server Configuration" : "Konfiguracija poslužitelja", + "Server Information" : "Informacije o poslužitelju", + "Settings" : "Postavke", + "Shards" : "Dijelovi", + "Share" : "Dijeli", + "Share with Groups" : "Dijeli s grupama", + "Share with Users" : "Dijeli s korisnicima", + "Show Filters" : "Prikaži filtre", + "Show less" : "Prikaži manje", + "Show only entries with changes" : "Prikaži samo unose s promjenama", + "Simple" : "Jednostavno", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Jednostavni upiti: osnovna tekstualna pretraživanja s minimalnim parametrima (npr. jedan pojam za pretraživanje, bez naprednih filtara)", + "Size" : "Veličina", + "Slug" : "Slug", + "Soft Delete Mode" : "Način mekog brisanja", + "Soft Deleted Items" : "Meko izbrisane stavke", + "SolrCloud mode enabled" : "SolrCloud način omogućen", + "Source" : "Izvor", + "Sources" : "Izvori", + "Sources:" : "Izvori:", + "Standalone SOLR mode" : "Samostalni SOLR način", + "Start Conversation" : "Započni razgovor", + "Start Vectorization" : "Započni vektorizaciju", + "Start a conversation" : "Započni razgovor", + "Starting names cache warmup..." : "Pokretanje zagrijavanja predmemorije naziva...", + "Starting..." : "Pokretanje...", + "Statistics" : "Statistika", + "Stats calculation not yet implemented" : "Izračun statistike još nije implementiran", + "Status" : "Status", + "Status Code" : "Statusni kôd", + "String Constraints" : "Ograničenja niza", + "Success" : "Uspjeh", + "Success Rate" : "Stopa uspješnosti", + "Success Status" : "Status uspješnosti", + "Successful" : "Uspješno", + "Successfully joined organisation" : "Uspješno pridruženo organizaciji", + "Sync Table" : "Sinkroniziraj tablicu", + "Synchronization Settings" : "Postavke sinkronizacije", + "System Default" : "Zadano sustava", + "Technical Description" : "Tehnički opis", + "Technical description for developers and administrators" : "Tehnički opis za razvojne programere i administratore", + "Temperature" : "Temperatura", + "Templates" : "Predlošci", + "Test" : "Test", + "Test Chat" : "Testiraj razgovor", + "Test Connection" : "Testiraj povezivanje", + "Test Embedding" : "Testiraj ugrađivanje", + "Test endpoint executed successfully" : "Testna krajnja točka uspješno izvršena", + "Test webhook delivered successfully" : "Testni webhook uspješno isporučen", + "Test webhook delivery failed" : "Isporuka testnog webhooka nije uspjela", + "Test webhook sent successfully" : "Testni webhook uspješno poslan", + "Testing..." : "Testiranje...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Tekstualni dijelovi generiraju se tijekom izdvajanja datoteka i pohranjuju u bazu podataka. Vektorizacija čita te unaprijed podijeljene datoteke i pretvara ih u ugrađivanja.", + "Text extracted successfully" : "Tekst uspješno izdvojen", + "Text extraction disabled" : "Izdvajanje teksta onemogućeno", + "The URL where webhook events will be sent" : "URL na koji će se slati webhook događaji", + "The requested conversation does not exist" : "Traženi razgovor ne postoji", + "The saved view and all its search configuration" : "Spremljeni prikaz i sva njegova konfiguracija pretraživanja", + "There are no audit trail entries matching your current filters." : "Nema unosa traga revizije koji odgovaraju vašim trenutnim filtrima.", + "There are no deleted items matching your current filters." : "Nema izbrisanih stavki koje odgovaraju vašim trenutnim filtrima.", + "There are no search trail entries matching your current filters." : "Nema unosa traga pretraživanja koji odgovaraju vašim trenutnim filtrima.", + "There are no webhook log entries matching your filters." : "Nema unosa zapisnika webhooka koji odgovaraju vašim filtrima.", + "This action cannot be undone." : "Ova se radnja ne može poništiti.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Ova se radnja ne može poništiti. Provjerite da nijedna zbirka ne koristi ovaj ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Ovaj agent nema konfigurabilne prikaze ili alate, ali još uvijek možete prilagoditi RAG postavke u nastavku.", + "This analysis may take some time" : "Ova analiza može potrajati neko vrijeme", + "This audit trail entry does not contain any change information." : "Ovaj unos traga revizije ne sadrži nikakve informacije o promjeni.", + "This configuration has no data defined." : "Ova konfiguracija nema definiranih podataka.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ova je krajnja točka zastarjela. Umjesto toga koristite krajnje točke temeljene na dijelovima.", + "This entity has no relations to objects or files" : "Ovaj entitet nema veza s objektima ili datotekama", + "This is a test webhook from OpenRegister" : "Ovo je testni webhook iz OpenRegistera", + "This schema must use Magic Table configuration to sync" : "Ova shema mora koristiti konfiguraciju čarobne tablice za sinkronizaciju", + "This source has no associated registers." : "Ovaj izvor nema povezanih registara.", + "This will copy the _default ConfigSet with the new name" : "Time će se kopirati ConfigSet _default s novim nazivom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Time će se trajno izbrisati SVA ugrađivanja (vektori) iz baze podataka. Morat ćete ponovno vektorizirati sve objekte i datoteke. Ova se radnja ne može poništiti.\\n\\nJeste li sigurni da želite nastaviti?", + "This will permanently delete:" : "Time će se trajno izbrisati:", + "Timeout (seconds)" : "Istek vremena (sekunde)", + "Timestamp" : "Vremenska oznaka", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Savjet: omogućite samo prikaze kojima je potrebno semantičko pretraživanje kako biste smanjili troškove ugrađivanja. Jednostavne tablice za traženje rijetko trebaju vektorizaciju.", + "Title" : "Naslov", + "To Date" : "Do datuma", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Za prilagodbu veličine dijela ili strategije, idite na Konfiguracija datoteke → Ograničenja obrade.", + "To change the embedding provider or model, go to LLM Configuration." : "Za promjenu pružatelja ugrađivanja ili modela, idite na LLM konfiguracija.", + "To date" : "Do datuma", + "Toggle search sidebar" : "Uključi/isključi bočnu traku pretraživanja", + "Tools" : "Alati", + "Top Deleters" : "Najčešći brisači", + "Total" : "Ukupno", + "Total Audit Trails" : "Ukupno tragova revizije", + "Total Chunks Available:" : "Ukupno dostupnih dijelova:", + "Total Deleted Items" : "Ukupno izbrisanih stavki", + "Total Members" : "Ukupno članova", + "Total Objects" : "Ukupno objekata", + "Total Objects in Database:" : "Ukupno objekata u bazi podataka:", + "Total Organisations" : "Ukupno organizacija", + "Total Results" : "Ukupno rezultata", + "Total Searches" : "Ukupno pretraživanja", + "Total Size" : "Ukupna veličina", + "Total Storage" : "Ukupna pohrana", + "Total:" : "Ukupno:", + "Totals" : "Ukupni iznosi", + "Type" : "Tip", + "Type Issue" : "Problem s tipom", + "Type Variations:" : "Varijacije tipa:", + "Type must be \"positive\" or \"negative\"" : "Tip mora biti \"positive\" ili \"negative\"", + "Type to search for groups" : "Upišite za pretraživanje grupa", + "Type to search for users" : "Upišite za pretraživanje korisnika", + "Type to search groups..." : "Upišite za pretraživanje grupa...", + "Type to search users..." : "Upišite za pretraživanje korisnika...", + "URL" : "URL", + "URL where Ollama is running" : "URL na kojem se Ollama izvodi", + "UUID:" : "UUID:", + "Unchanged" : "Nepromijenjeno", + "Unique Search Terms" : "Jedinstveni pojmovi za pretraživanje", + "Unique Terms" : "Jedinstveni pojmovi", + "Unknown" : "Nepoznato", + "Unknown Register" : "Nepoznati registar", + "Unknown Schema" : "Nepoznata shema", + "Untitled View" : "Prikaz bez naslova", + "Update" : "Ažuriraj", + "Update Operations" : "Operacije ažuriranja", + "Update configuration: ..." : "Ažuriraj konfiguraciju: ...", + "Update register OAS: ..." : "Ažuriraj OAS registra: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Ažuriraj vektore kada se podaci objekta promijene (preporučeno za točno pretraživanje)", + "Updated" : "Ažurirano", + "Updated At" : "Ažurirano u", + "Updated:" : "Ažurirano:", + "Uptime:" : "Vrijeme rada:", + "Usage Count" : "Broj korištenja", + "Use SolrCloud with Zookeeper for distributed search" : "Koristi SolrCloud sa Zookeeperom za distribuirano pretraživanje", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Koristite filtre za sužavanje unosa traga revizije po registru, shemi, tipu radnje, korisniku, rasponu datuma ili ID-u objekta.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Koristite filtre za sužavanje izbrisanih stavki po registru, shemi, datumu brisanja ili korisniku koji ih je izbrisao.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Koristite filtre za sužavanje unosa traga pretraživanja po registru, shemi, statusu uspješnosti, korisniku, rasponu datuma, pojmovima za pretraživanje ili metrikama performansi.", + "Used By Collections" : "Koristi se u zbirkama", + "User" : "Korisnik", + "User Agent Statistics" : "Statistika korisničkog agenta", + "User Description" : "Opis korisnika", + "User rate limits cleared successfully" : "Ograničenja brzine korisnika uspješno očišćena", + "User removed successfully" : "Korisnik uspješno uklonjen", + "User-friendly description shown in forms and help text" : "Korisniku prilagođen opis prikazan u obrascima i tekstu pomoći", + "Username" : "Korisničko ime", + "Username for SOLR authentication (optional)" : "Korisničko ime za SOLR autentifikaciju (neobavezno)", + "Users" : "Korisnici", + "Using Pre-Generated Chunks" : "Korištenje unaprijed generiranih dijelova", + "Validate" : "Provjeri valjanost", + "Validate Objects" : "Provjeri valjanost objekata", + "Validation completed successfully" : "Provjera valjanosti uspješno dovršena", + "Validation failed" : "Provjera valjanosti nije uspjela", + "Value" : "Vrijednost", + "Vector Dimensions" : "Dimenzije vektora", + "Vector Search Backend" : "Pozadinski sustav vektorskog pretraživanja", + "Vector field: _embedding_" : "Vektorsko polje: _embedding_", + "Vectorization Triggers" : "Okidači vektorizacije", + "Vectorize All Files" : "Vektoriziraj sve datoteke", + "Vectorize All Objects" : "Vektoriziraj sve objekte", + "Vectorize all file types" : "Vektoriziraj sve tipove datoteka", + "Vectorize all views" : "Vektoriziraj sve prikaze", + "Vectorize on object creation" : "Vektoriziraj pri stvaranju objekta", + "Vectors will be stored in your existing object and file collections" : "Vektori će biti pohranjeni u vašim postojećim zbirkama objekata i datoteka", + "Version" : "Verzija", + "Very High" : "Vrlo visoko", + "View API Docs" : "Pregledaj API dokumentaciju", + "View Changes" : "Pregledaj promjene", + "View Details" : "Pregledaj pojedinosti", + "View Error" : "Pregledaj pogrešku", + "View File" : "Pregledaj datoteku", + "View Full Details" : "Pregledaj sve pojedinosti", + "View Logs" : "Pregledaj zapisnike", + "View Name" : "Naziv prikaza", + "View Object" : "Pregledaj objekt", + "View Parameters" : "Parametri prikaza", + "View Selection" : "Odabir prikaza", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Pregledajte i analizirajte zapisnike traga pretraživanja s naprednim mogućnostima filtriranja i analitike", + "View and analyze system audit trails with advanced filtering capabilities" : "Pregledajte i analizirajte tragove revizije sustava s naprednim mogućnostima filtriranja", + "View deleted successfully" : "Prikaz uspješno izbrisan", + "View entity details and manage relations" : "Pregledajte pojedinosti entiteta i upravljajte vezama", + "View name" : "Naziv prikaza", + "View name is required" : "Naziv prikaza je obavezan", + "View saved successfully!" : "Prikaz uspješno spremljen!", + "View search analytics and manage search logs" : "Pregledajte analitiku pretraživanja i upravljajte zapisnicima pretraživanja", + "View successfully deleted" : "Prikaz uspješno izbrisan", + "View successfully updated" : "Prikaz uspješno ažuriran", + "View updated successfully!" : "Prikaz uspješno ažuriran!", + "View webhook delivery logs and filter by webhook" : "Pregledajte zapisnike isporuke webhooka i filtrirajte po webhooku", + "Views" : "Prikazi", + "Visible to users" : "Vidljivo korisnicima", + "Wait for Response" : "Čekaj odgovor", + "Wait for webhook response before continuing (required for request/response flows)" : "Čekaj odgovor webhooka prije nastavka (obavezno za tijekove zahtjev/odgovor)", + "Warmup Names Cache" : "Zagrij predmemoriju naziva", + "Warning" : "Upozorenje", + "Warning:" : "Upozorenje:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Pojedinosti zapisnika webhooka", + "Webhook Logs" : "Zapisnici webhooka", + "Webhook created successfully" : "Webhook uspješno stvoren", + "Webhook deleted" : "Webhook izbrisan", + "Webhook retry delivered successfully" : "Ponovni pokušaj webhooka uspješno isporučen", + "Webhook retry delivery failed" : "Isporuka ponovnog pokušaja webhooka nije uspjela", + "Webhook updated" : "Webhook ažuriran", + "Webhook updated successfully" : "Webhook uspješno ažuriran", + "Webhooks" : "Webhookovi", + "Weekly" : "Tjedno", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Omotaj korisni teret webhooka u CloudEvents format za bolju interoperabilnost", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Da", + "You" : "Vi", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Možete stvoriti nove ConfigSetove na temelju predloška _default ili izravno prenijeti prilagođene na vaš SOLR poslužitelj.", + "You do not have access to this conversation" : "Nemate pristup ovom razgovoru", + "You do not have permission to delete this conversation" : "Nemate dopuštenje za brisanje ovog razgovora", + "You do not have permission to modify this conversation" : "Nemate dopuštenje za izmjenu ovog razgovora", + "You do not have permission to restore this conversation" : "Nemate dopuštenje za vraćanje ovog razgovora", + "You must be logged in to favorite views" : "Morate biti prijavljeni za dodavanje prikaza u favorite", + "You need an AI agent to start a conversation." : "Potreban vam je AI agent za pokretanje razgovora.", + "Your Fireworks AI API key. Get one at" : "Vaš Fireworks AI API ključ. Nabavite ga na", + "Your OpenAI API key. Get one at" : "Vaš OpenAI API ključ. Nabavite ga na", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Vaše povratne informacije su zabilježene. Po želji, ovdje možete navesti dodatne pojedinosti...", + "Zookeeper Hosts" : "Zookeeper domaćini", + "Zookeeper Port" : "Zookeeper vrata", + "Zookeeper Settings (SolrCloud)" : "Zookeeper postavke (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper niz povezivanja za SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Broj Zookeeper vrata (neobavezno, zadano 2181)", + "chunk" : "dio", + "chunks" : "dijelovi", + "chunks processed" : "obrađenih dijelova", + "configuration(s)" : "konfiguracija", + "e.g., lib/Settings/config.json" : "npr. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "npr. lib/Settings/register.json", + "failed" : "neuspjelo", + "file" : "datoteka", + "files" : "datoteke", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "članovi", + "menu or contact someone with permission to create agents." : "izborniku ili kontaktirajte nekoga s dopuštenjem za stvaranje agenata.", + "messages" : "poruke", + "n8n URL and API key are required" : "n8n URL i API ključ su obavezni", + "n8n configuration saved successfully" : "n8n konfiguracija uspješno spremljena", + "n8n connection not configured" : "n8n povezivanje nije konfigurirano", + "n8n connection successful" : "n8n povezivanje uspješno", + "n8n connection test successful" : "n8n test povezivanja uspješan", + "n8n integration disabled" : "n8n integracija onemogućena", + "n8n integration enabled" : "n8n integracija omogućena", + "n8n project initialized successfully" : "n8n projekt uspješno inicijaliziran", + "n8n settings saved successfully" : "n8n postavke uspješno spremljene", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekti", + "objects processed" : "obrađenih objekata", + "or pick a range" : "ili odaberite raspon", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "pruža dobru ravnotežu između brzine i točnosti.", + "register(s)" : "registar(i)", + "register(s) selected" : "registar(i) odabran(i)", + "required" : "obavezno", + "results" : "rezultati", + "schema(s)" : "shema(e)", + "schema(s) selected" : "shema(e) odabrana(e)", + "sk-..." : "sk-...", + "this application" : "ova aplikacija", + "to" : "do", + "vectorized" : "vektorizirano", + "views selected" : "prikaza odabrano", + "ℹ️ Current Configuration" : "ℹ️ Trenutna konfiguracija", + "⚡ Batch Processing" : "⚡ Serijska obrada", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Sveobuhvatan kontekst ✓ Manja vjerojatnost propuštanja pojedinosti ✗ Sporiji odgovori ✗ Može uključivati manje relevantne informacije", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Brži odgovori ✓ Usredotočeniji odgovori ✗ Može propustiti relevantne informacije", + "✨ AI Features" : "✨ AI značajke", + "All search trails cleared successfully" : "Svi tragovi pretraživanja uspješno očišćeni", + "Multiple search trail deletion not implemented yet" : "Višestruko brisanje traga pretraživanja još nije implementirano", + "No expired search trails found to clear" : "Nije pronađen nijedan istekli trag pretraživanja za čišćenje", + "Missing message" : "Nedostaje poruka", + "message content is required" : "sadržaj poruke je obavezan", + "Missing conversation or agentUuid" : "Nedostaje razgovor ili agentUuid", + "Access denied" : "Pristup odbijen", + "Conversation not found" : "Razgovor nije pronađen", + "AI service not configured" : "AI usluga nije konfigurirana", + "Failed to process message" : "Obrada poruke nije uspjela", + "Missing conversationId" : "Nedostaje conversationId", + "conversationId is required" : "conversationId je obavezan", + "Failed to fetch conversation history" : "Dohvaćanje povijesti razgovora nije uspjelo", + "Failed to clear conversation" : "Čišćenje razgovora nije uspjelo", + "Invalid feedback type" : "Nevaljan tip povratnih informacija", + "type must be \"positive\" or \"negative\"" : "tip mora biti \"positive\" ili \"negative\"", + "Message not found" : "Poruka nije pronađena", + "Failed to save feedback" : "Spremanje povratnih informacija nije uspjelo", + "Failed to get chat statistics" : "Dohvaćanje statistike razgovora nije uspjelo", + "Not Found" : "Nije pronađeno", + "Engine not found" : "Pogon nije pronađen", + "Consumer not found" : "Potrošač nije pronađen", + "Backend parameter is required" : "Parametar pozadinskog sustava je obavezan", + "Failed to get database information: %s" : "Dohvaćanje informacija o bazi podataka nije uspjelo: %s", + "SOLR setup error: %s" : "Pogreška SOLR postavljanja: %s", + "Reindex failed: %s" : "Ponovno indeksiranje nije uspjelo: %s", + "Anonymized" : "Anonimizirano", + "Entities detected" : "Otkriveni entiteti", + "Extract Now" : "Izdvoji sada", + "Extracted at" : "Izdvojeno u", + "Extraction" : "Izdvajanje", + "Failed to load extraction data" : "Učitavanje podataka izdvajanja nije uspjelo", + "Failed to load register data" : "Učitavanje podataka registra nije uspjelo", + "Failed to retrieve extraction status." : "Dohvaćanje statusa izdvajanja nije uspjelo.", + "Failed to retrieve objects for file." : "Dohvaćanje objekata za datoteku nije uspjelo.", + "No extraction data available for this file" : "Za ovu datoteku nema dostupnih podataka izdvajanja", + "No register objects reference this file" : "Nijedan objekt registra ne referencira ovu datoteku", + "Not extracted" : "Nije izdvojeno", + "Register Objects" : "Objekti registra", + "Risk level" : "Razina rizika", + "Text chunks" : "Tekstualni dijelovi", + "Unknown error" : "Nepoznata pogreška", + "Very high" : "Vrlo visoko", + "{title} in {register} / {schema}" : "{title} u {register} / {schema}", + "Push Notifications" : "Push obavijesti", + "Real-time push notification status via notify_push" : "Status push obavijesti u stvarnom vremenu putem notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push u stvarnom vremenu nije dostupan — aplikacija notify_push nije instalirana", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Instalirajte aplikaciju notify_push iz Nextcloud trgovine aplikacija za omogućavanje ažuriranja u stvarnom vremenu.", + "Open Nextcloud App Store" : "Otvori Nextcloud trgovinu aplikacija", + "notify_push is installed but not yet active" : "notify_push je instaliran, ali još nije aktivan", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push je instaliran, ali OpenRegister još nije potvrdio uspješan push. Pokrenite spremanje objekta za aktivaciju ili provjerite svoju notify_push konfiguraciju.", + "notify_push configuration guide" : "vodič za konfiguraciju notify_push", + "Realtime push active" : "Push u stvarnom vremenu aktivan", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push obavijesti u stvarnom vremenu su aktivne. Povezani klijenti primaju trenutna ažuriranja kada se objekti stvore, ažuriraju ili izbrišu.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +); diff --git a/l10n/hr.json b/l10n/hr.json new file mode 100644 index 0000000000..3986bff2b2 --- /dev/null +++ b/l10n/hr.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Potreban Dolphin)", + "(no subject)": "(bez predmeta)", + "(no title)": "(bez naslova)", + "(unnamed)": "(bez naziva)", + "(untitled card)": "(kartica bez naslova)", + "({count} selected)": "({count} odabrano)", + ", {failed} failed": ", {failed} neuspješno", + "0 = unlimited": "0 = neograničeno", + "A deactivation request is pending.": "Zahtjev za deaktivaciju je na čekanju.", + "AI Assistant": "AI asistent", + "AI Chat": "AI razgovor", + "AI service not configured": "AI usluga nije konfigurirana", + "API Key": "API ključ", + "API Request Quota (requests/day)": "Kvota API zahtjeva (zahtjeva/dan)", + "API Token Configuration": "Konfiguracija API tokena", + "API Tokens": "API tokeni", + "API tokens saved successfully": "API tokeni uspješno spremljeni", + "AVG / Verwerkingsregister": "AVG / Registar obrade", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG čl. 30 §4: ovo izvješće povezuje svaku aktivnost obrade s ukupnim brojem zapisa revizijskog traga koji joj se pripisuju. Revizori i Autoriteit Persoonsgegevens to koriste za nadzorni pregled.", + "About ConfigSets": "O ConfigSet stavkama", + "Access denied": "Pristup odbijen", + "Access organisation dashboard and analytics": "Pristup nadzornoj ploči i analitici organizacije", + "Account": "Račun", + "Action": "Radnja", + "Action Distribution": "Raspodjela radnji", + "Action:": "Radnja:", + "Actions": "Radnje", + "Active": "Aktivno", + "Active Collections": "Aktivne zbirke", + "Active Filters": "Aktivni filtri", + "Active Organisation:": "Aktivna organizacija:", + "Active Organisations": "Aktivne organizacije", + "Active filters:": "Aktivni filtri:", + "Active organisation changed successfully": "Aktivna organizacija uspješno promijenjena", + "Active organisation set successfully": "Aktivna organizacija uspješno postavljena", + "Activities": "Aktivnosti", + "Activity": "Aktivnost", + "Activity Period": "Razdoblje aktivnosti", + "Add": "Dodaj", + "Add Attachment": "Dodaj privitak", + "Add Endpoint": "Dodaj krajnju točku", + "Add Groups": "Dodaj grupe", + "Add Property to '{title}'": "Dodaj svojstvo u '{title}'", + "Add Register": "Dodaj registar", + "Add Schema": "Dodaj shemu", + "Add Source": "Dodaj izvor", + "Add User": "Dodaj korisnika", + "Add User to Organisation": "Dodaj korisnika u organizaciju", + "Add a contact from any of your address books to associate it with this object.": "Dodajte kontakt iz bilo kojeg svojeg adresara kako biste ga povezali s ovim objektom.", + "Add a file or files": "Dodajte datoteku ili datoteke", + "Add card": "Dodaj karticu", + "Add contact": "Dodaj kontakt", + "Add endpoint": "Dodaj krajnju točku", + "Add oneOf entry": "Dodaj oneOf unos", + "Add range": "Dodaj raspon", + "Add schema titles, descriptions, and register information to provide richer context for search": "Dodajte naslove shema, opise i informacije o registru kako biste pružili bogatiji kontekst za pretraživanje", + "Add to favorites": "Dodaj u favorite", + "Added": "Dodano", + "Added to favorites": "Dodano u favorite", + "Additional Information": "Dodatne informacije", + "Additional feedback saved. Thank you!": "Dodatne povratne informacije spremljene. Hvala vam!", + "Addresses": "Adrese", + "Admin": "Administrator", + "Advanced": "Napredno", + "Advanced Filters": "Napredni filtri", + "Advanced Options": "Napredne mogućnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Napredni filtri nisu dostupni pri korištenju izvora baze podataka. Prebacite na Auto ili SOLR indeks za mogućnosti filtriranja.", + "After": "Nakon", + "Agent Type": "Vrsta agenta", + "Agent deleted successfully": "Agent uspješno izbrisan", + "Agent successfully created": "Agent uspješno stvoren", + "Agent successfully updated": "Agent uspješno ažuriran", + "Agents": "Agenti", + "Aggregated across schemas": "Agregirano kroz sheme", + "All": "Sve", + "All Categories": "Sve kategorije", + "All Collections": "Sve zbirke", + "All Confidence Levels": "Sve razine pouzdanosti", + "All ConfigSets": "Svi ConfigSet-ovi", + "All Files": "Sve datoteke", + "All Levels": "Sve razine", + "All Types": "Sve vrste", + "All Webhooks": "Svi webhookovi", + "All actions": "Sve radnje", + "All audit trails cleared successfully": "Svi revizijski tragovi uspješno obrisani", + "All clear": "Sve čisto", + "All file types": "Sve vrste datoteka", + "All registers": "Svi registri", + "All schemas": "Sve sheme", + "All search trails cleared successfully": "Svi tragovi pretraživanja uspješno obrisani", + "All searches": "Sva pretraživanja", + "All users": "Svi korisnici", + "All-day events": "Cjelodnevni događaji", + "Allowed MIME Types": "Dopuštene MIME vrste", + "Already linked": "Već povezano", + "An error occurred while clearing audit trails": "Došlo je do pogreške prilikom brisanja revizijskih tragova", + "An error occurred while deleting the audit trail": "Došlo je do pogreške prilikom brisanja revizijskog traga", + "An error occurred while deleting the object": "Došlo je do pogreške prilikom brisanja objekta", + "An error occurred while deleting the view": "Došlo je do pogreške prilikom brisanja prikaza", + "An error occurred while permanently deleting the objects": "Došlo je do pogreške prilikom trajnog brisanja objekata", + "An error occurred while restoring the objects": "Došlo je do pogreške prilikom vraćanja objekata", + "An error occurred while saving the organisation": "Došlo je do pogreške prilikom spremanja organizacije", + "Analysis completed:": "Analiza dovršena:", + "Analysis steps:": "Koraci analize:", + "Analytics": "Analitika", + "Analyze Objects": "Analiziraj objekte", + "Analyze Properties": "Analiziraj svojstva", + "Analyze existing properties for improvement opportunities": "Analizirajte postojeća svojstva za mogućnosti poboljšanja", + "Analyzing...": "Analiziranje...", + "Anonymized": "Anonimizirano", + "Anonymous Group": "Anonimna grupa", + "Any favorites and sharing settings for this view": "Sve postavke favorita i dijeljenja za ovaj prikaz", + "Any user": "Bilo koji korisnik", + "App ID": "ID aplikacije", + "App store cache invalidated successfully": "Predmemorija trgovine aplikacija uspješno poništena", + "App store cache invalidated: {invalidated}": "Predmemorija trgovine aplikacija poništena: {invalidated}", + "Application deleted successfully": "Aplikacija uspješno izbrisana", + "Application identifier for this configuration (optional)": "Identifikator aplikacije za ovu konfiguraciju (neobavezno)", + "Application settings": "Postavke aplikacije", + "Application successfully created": "Aplikacija uspješno stvorena", + "Application successfully updated": "Aplikacija uspješno ažurirana", + "Applications": "Aplikacije", + "Apply": "Primijeni", + "Apply Changes": "Primijeni promjene", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Primijenite ulogu na sve sheme u ovom registru koje nemaju eksplicitna nadjačavanja autorizacije.", + "Approval Progress": "Napredak odobravanja", + "Approve": "Odobri", + "Approved": "Odobreno", + "Archive": "Arhiviraj", + "Archive conversation": "Arhiviraj razgovor", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arhivirati ovu aktivnost obrade? Reci revizijskog traga nastavit će je referencirati.", + "Archived conversations are hidden from your active list": "Arhivirani razgovori skriveni su s vašeg aktivnog popisa", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Jeste li sigurni da želite IZBRISATI zbirku \"{name}\"?\n\nOvo će:\n• Trajno izbrisati zbirku i sve njezine podatke\n• Ukloniti sve indeksirane dokumente\n• Ova se radnja ne može poništiti", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Jeste li sigurni da želite očistiti stare tragove pretraživanja? Ovo će izbrisati unose starije od 30 dana.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Jeste li sigurni da želite obrisati sve podatke iz zbirke \"{name}\"?\n\nOvo će:\n• Izbrisati sve indeksirane dokumente\n• Zadržati strukturu zbirke netaknutom\n• Ova se radnja ne može poništiti", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Jeste li sigurni da želite izbrisati aplikaciju \"{name}\"? Ova se radnja ne može poništiti.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Jeste li sigurni da želite izbrisati odabrane revizijske tragove? Ova se radnja ne može poništiti.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Jeste li sigurni da želite izbrisati odabrane tragove pretraživanja? Ova se radnja ne može poništiti.", + "Are you sure you want to delete this ConfigSet?": "Jeste li sigurni da želite izbrisati ovaj ConfigSet?", + "Are you sure you want to delete this configset?": "Jeste li sigurni da želite izbrisati ovaj configset?", + "Are you sure you want to leave '{name}'?": "Jeste li sigurni da želite napustiti '{name}'?", + "Are you sure you want to permanently delete": "Jeste li sigurni da želite trajno izbrisati", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Jeste li sigurni da želite ponovno indeksirati zbirku \"{name}\"?\n\nOvo će:\n• Ponovno izgraditi indeks sa svim objektima\n• Potrajati nekoliko minuta do dovršetka\n• Možda utjecati na performanse pretraživanja tijekom ponovnog indeksiranja", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Jeste li sigurni da želite ukloniti shemu \"{schema}\" iz registra \"{register}\"? Ova se radnja ne može poništiti.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Jeste li sigurni da želite meko izbrisati {count} aktivnih objekata za shemu \"{schema}\"?\n\nBit će označeni kao izbrisani, ali se mogu trajno ukloniti kasnije.", + "Array Object Configuration:": "Konfiguracija objekta polja:", + "Articles": "Članci", + "Ask a question...": "Postavite pitanje...", + "Ask questions about your data using natural language": "Postavljajte pitanja o svojim podacima koristeći prirodni jezik", + "Assigned collaborative tags": "Dodijeljene suradničke oznake", + "Assignment notifications": "Obavijesti o dodjeli", + "Attachment added to {name}": "Privitak dodan u {name}", + "Attempt": "Pokušaj", + "Audit Trail Changes": "Promjene revizijskog traga", + "Audit Trail Details": "Pojedinosti revizijskog traga", + "Audit Trail Management": "Upravljanje revizijskim tragom", + "Audit Trail Statistics": "Statistika revizijskog traga", + "Audit Trails": "Revizijski tragovi", + "Audit trail data copied to clipboard": "Podaci revizijskog traga kopirani u međuspremnik", + "Audit trail deleted successfully": "Revizijski trag uspješno izbrisan", + "Audit trail successfully deleted": "Revizijski trag uspješno izbrisan", + "Authentication": "Autentifikacija", + "Auto (e.g. Y for year, F Y for month)": "Auto (npr. Y za godinu, F Y za mjesec)", + "Auto share": "Automatsko dijeljenje", + "Auto-commit disabled": "Automatska potvrda onemogućena", + "Auto-commit enabled": "Automatska potvrda omogućena", + "Auto-create Default Organisation": "Automatski stvori zadanu organizaciju", + "Auto-retry failed vectorizations": "Automatski ponovno pokušaj neuspjele vektorizacije", + "Automatically commit changes to SOLR index": "Automatski potvrdi promjene u SOLR indeks", + "Automatically connect to the federation network.": "Automatski se poveži na federacijsku mrežu.", + "Automatically create a default organisation if none exists when the app is initialized": "Automatski stvori zadanu organizaciju ako nijedna ne postoji kada se aplikacija inicijalizira", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automatski generiraj vektorska ugrađivanja iz tekstualnih dijelova kada se datoteke prenesu i obrade", + "Automatically generate vector embeddings when objects are created or updated": "Automatski generiraj vektorska ugrađivanja kada se objekti stvore ili ažuriraju", + "Automatically retry failed vectorization attempts (max 3 retries)": "Automatski ponovno pokušaj neuspjele pokušaje vektorizacije (najviše 3 ponovna pokušaja)", + "Automatically update catalogues.": "Automatski ažuriraj kataloge.", + "Available Filters": "Dostupni filtri", + "Available Workflows": "Dostupni tijekovi rada", + "Available placeholders:": "Dostupni rezervirani znakovi:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Vaš pružatelj autentifikacije ne podržava promjene avatara.", + "Avatar removed": "Avatar uklonjen", + "Avatar updated successfully": "Avatar uspješno ažuriran", + "Avg Execution Time": "Prosj. vrijeme izvršavanja", + "Avg Members/Org": "Prosj. članova/org.", + "Avg Object Views/Session": "Prosj. pregleda objekata/sesiji", + "Avg Response Time": "Prosj. vrijeme odgovora", + "Avg Results/Search": "Prosj. rezultata/pretraživanju", + "Avg Searches/Session": "Prosj. pretraživanja/sesiji", + "Back": "Natrag", + "Back to Entities": "Natrag na entitete", + "Back to Registers": "Natrag na registre", + "Back to Webhooks": "Natrag na webhookove", + "Back to applications": "Natrag na aplikacije", + "Back to entities": "Natrag na entitete", + "Backend parameter is required": "Parametar pozadinskog sustava je obavezan", + "Backend updated successfully. Please reload the application.": "Pozadinski sustav uspješno ažuriran. Molimo ponovno učitajte aplikaciju.", + "Bandwidth Quota (MB/month)": "Kvota propusnosti (MB/mjesec)", + "Base URL (Optional)": "Osnovni URL (neobavezno)", + "Basic Information": "Osnovne informacije", + "Batch Size": "Veličina serije", + "Batch extraction completed": "Serijska ekstrakcija dovršena", + "Before": "Prije", + "Before object vectorization can work:": "Prije nego što vektorizacija objekata može raditi:", + "Behavior": "Ponašanje", + "Behavior Issue": "Problem s ponašanjem", + "Beschrijving": "Opis", + "Bewaartermijn": "Rok čuvanja", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Rok čuvanja (ISO-8601 trajanje, npr. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob pohrana je povučena. Svi objekti sada koriste magične tablice.", + "Bookmarks": "Knjižne oznake", + "Branch": "Grana", + "Bulk Role Assignment: {title}": "Skupna dodjela uloga: {title}", + "Bulk delete operation completed successfully": "Operacija skupnog brisanja uspješno dovršena", + "Bulk save operation completed successfully": "Operacija skupnog spremanja uspješno dovršena", + "Business Data": "Poslovni podaci", + "CSV": "CSV", + "Cache cleared successfully": "Predmemorija uspješno obrisana", + "Cache grew from {old} to {new} entries.": "Predmemorija je narasla s {old} na {new} unosa.", + "Cache shrunk from {old} to {new} entries.": "Predmemorija se smanjila s {old} na {new} unosa.", + "Cache stayed the same at {size} entries.": "Predmemorija je ostala ista na {size} unosa.", + "Calculate Sizes": "Izračunaj veličine", + "Calendar": "Kalendar", + "Calendar Provider Configuration": "Konfiguracija pružatelja kalendara", + "Calendar integration is not available": "Integracija kalendara nije dostupna", + "Calendar name": "Naziv kalendara", + "Cancel": "Odustani", + "Cancel deactivation request": "Otkaži zahtjev za deaktivaciju", + "Cannot delete: objects are still attached": "Nije moguće izbrisati: objekti su još uvijek priloženi", + "Cannot edit: This register is managed by external configuration {title}": "Nije moguće uređivati: ovim registrom upravlja vanjska konfiguracija {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nije moguće ukloniti shemu s postojećim objektima ({count} objekata)", + "Cannot remove schema {schema} because it contains {count} objects": "Nije moguće ukloniti shemu {schema} jer sadrži {count} objekata", + "Cards": "Kartice", + "Cascade delete": "Kaskadno brisanje", + "Categories of data subjects (one per line)": "Kategorije ispitanika (jedna po retku)", + "Categories of personal data (one per line)": "Kategorije osobnih podataka (jedna po retku)", + "Categorieën betrokkenen (één per regel)": "Kategorije ispitanika (jedna po retku)", + "Categorieën persoonsgegevens (één per regel)": "Kategorije osobnih podataka (jedna po retku)", + "Category": "Kategorija", + "Change Type": "Vrsta promjene", + "Change password": "Promijeni lozinku", + "Changes": "Promjene", + "Changes copied to clipboard": "Promjene kopirane u međuspremnik", + "Changes for Audit Trail #{id}": "Promjene za revizijski trag #{id}", + "Chat Model": "Model razgovora", + "Chat Provider": "Pružatelj razgovora", + "Chat Provider (RAG)": "Pružatelj razgovora (RAG)", + "Chat Settings": "Postavke razgovora", + "Chat provider connection successful!": "Veza s pružateljem razgovora uspješna!", + "Chat settings": "Postavke razgovora", + "Chat test failed: {error}": "Test razgovora neuspješan: {error}", + "Choose a register": "Odaberite registar", + "Choose a schema": "Odaberite shemu", + "Choose how vector similarity calculations are performed for semantic search": "Odaberite kako se izračuni vektorske sličnosti izvode za semantičko pretraživanje", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Odaberite koje vrste datoteka uključiti u proces vektorizacije. Obradit će se samo datoteke s izvučenim tekstom i dijelovima.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Odaberite koje prikaze uključiti u proces vektorizacije. Ostavite prazno za obradu svih prikaza na temelju vaše konfiguracije.", + "Chunk": "Dio", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Brisanje dijelova još nije implementirano. Koristite krajnje točke temeljene na dijelovima.", + "Chunks": "Dijelovi", + "Chunks to Vectorize:": "Dijelovi za vektorizaciju:", + "Cleanup Old Trails": "Očisti stare tragove", + "Cleanup completed": "Čišćenje dovršeno", + "Cleanup completed successfully. Deleted {count} entries.": "Čišćenje uspješno dovršeno. Izbrisano {count} unosa.", + "Cleanup failed: {error}": "Čišćenje neuspješno: {error}", + "Clear All Embeddings": "Obriši sva ugrađivanja", + "Clear Entries": "Obriši unose", + "Clear Filtered Audit Trails": "Obriši filtrirane revizijske tragove", + "Clear Filters": "Obriši filtre", + "Clear Index": "Obriši indeks", + "Clear Selection": "Obriši odabir", + "Clear all": "Obriši sve", + "Clear all filters": "Obriši sve filtre", + "Clear date range": "Obriši raspon datuma", + "Clear filters": "Obriši filtre", + "Clearing...": "Brisanje...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Kliknite Pokreni provjeru usklađenosti kako biste pronašli sheme u kojima je otkriven PII, ali ne postoji bilješka o aktivnosti obrade.", + "Click to subscribe to notifications": "Kliknite za pretplatu na obavijesti", + "Click to unsubscribe from notifications": "Kliknite za odjavu od obavijesti", + "Close": "Zatvori", + "Cluster name": "Naziv klastera", + "Code": "Kod", + "Code (short readable key, e.g. v-2026-001)": "Kod (kratki čitljivi ključ, npr. v-2026-001)", + "Collapse details": "Sažmi pojedinosti", + "Collection Management": "Upravljanje zbirkama", + "Collection Name": "Naziv zbirke", + "Collection assignments updated successfully": "Dodjele zbirki uspješno ažurirane", + "Collection cleared successfully": "Zbirka uspješno obrisana", + "Collection copied successfully": "Zbirka uspješno kopirana", + "Collection created successfully": "Zbirka uspješno stvorena", + "Collection deleted successfully": "Zbirka uspješno izbrisana", + "Collection used to store and index file metadata and content": "Zbirka koja se koristi za pohranu i indeksiranje metapodataka i sadržaja datoteka", + "Collection used to store and index object data": "Zbirka koja se koristi za pohranu i indeksiranje podataka objekata", + "Collections:": "Zbirke:", + "Color": "Boja", + "Comment...": "Komentar...", + "Commit Message": "Poruka potvrde", + "Commit Within (ms)": "Potvrdi unutar (ms)", + "Commit message": "Poruka potvrde", + "Compare current schema with real object data": "Usporedi trenutnu shemu sa stvarnim podacima objekata", + "Completed": "Dovršeno", + "Complex": "Složeno", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Složeni upiti: napredna pretraživanja s više filtara, operatora i složenih kombinacija parametara", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Složeni upiti: napredna pretraživanja s više filtara, operatora i složenih kombinacija parametara", + "Compliance": "Usklađenost", + "Confidence Level": "Razina pouzdanosti", + "Confidence Score": "Rezultat pouzdanosti", + "Confidence: {confidence}%": "Pouzdanost: {confidence}%", + "Config must be provided as an object": "Konfiguracija mora biti navedena kao objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Upravljanje ConfigSet stavkama", + "ConfigSet Name": "Naziv ConfigSet stavke", + "ConfigSet created successfully": "ConfigSet uspješno stvoren", + "ConfigSet deleted successfully": "ConfigSet uspješno izbrisan", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSet stavke definiraju shemu i konfiguraciju za vaše SOLR zbirke. Sadrže definicije polja, analizatore i druge postavke pretraživanja.", + "Configuration": "Konfiguracija", + "Configuration Data": "Podaci konfiguracije", + "Configuration is up to date": "Konfiguracija je ažurna", + "Configuration saved": "Konfiguracija spremljena", + "Configuration saved successfully.": "Konfiguracija uspješno spremljena.", + "Configuration type (default, application, etc.)": "Vrsta konfiguracije (zadana, aplikacija itd.)", + "Configurations": "Konfiguracije", + "Configure API tokens for external service integrations": "Konfigurirajte API tokene za integracije vanjskih usluga", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurirajte Apache SOLR tražilicu za napredne mogućnosti pretraživanja", + "Configure Facets": "Konfiguriraj fasete", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurirajte pružatelje velikih jezičnih modela (LLM) za značajke pokretane AI-jem uključujući semantičko pretraživanje, ugrađivanja i razgovor.", + "Configure SOLR Facets": "Konfiguriraj SOLR fasete", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurirajte osnovne postavke veze za vaš SOLR poslužitelj uključujući autentifikaciju i mrežne mogućnosti. Koristite zasebne dijaloge za upravljanje ConfigSet stavkama i zbirkama za upravljanje jezgrama i zbirkama.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurirajte osnovne postavke veze za vaš SOLR poslužitelj uključujući autentifikaciju i mrežne mogućnosti. Koristite zasebne dijaloge za upravljanje configset stavkama i zbirkama za upravljanje jezgrama i zbirkama.", + "Configure default organisation and organisation-related settings": "Konfigurirajte zadanu organizaciju i postavke vezane uz organizaciju", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurirajte kako se objekti baze podataka pretvaraju u vektorska ugrađivanja za semantičko pretraživanje. Objekti se izravno vektoriziraju bez potrebe za ekstrakcijom teksta.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurirajte kako se objekti pretvaraju u tekst prije vektorizacije. Ove postavke utječu na kvalitetu i kontekst pretraživanja.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurirajte pružatelje velikih jezičnih modela (LLM) za značajke pokretane AI-jem uključujući semantičko pretraživanje, ugrađivanja i razgovor.", + "Configure multi-organization support and tenant isolation": "Konfigurirajte podršku za više organizacija i izolaciju zakupaca", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurirajte parametre za vektorizaciju datoteka. Ovaj će proces generirati vektorska ugrađivanja za sve izvučene dijelove datoteka.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurirajte parametre za vektorizaciju objekata. Ovaj će proces generirati vektorska ugrađivanja za sve objekte koji odgovaraju filtrima vašeg prikaza.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurirajte ovu shemu da prikazuje objekte kao događaje u aplikaciji Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurirajte koje vrste podataka pretraživati i koliko izvora dohvatiti", + "Configuring field mappings...": "Konfiguriranje mapiranja polja...", + "Confirm Account Deactivation": "Potvrdi deaktivaciju računa", + "Confirm Object Deletion": "Potvrdi brisanje objekta", + "Confirm Object Restoration": "Potvrdi vraćanje objekta", + "Confirm Permanent Object Deletion": "Potvrdi trajno brisanje objekta", + "Confirm deactivation": "Potvrdi deaktivaciju", + "Connecting to SOLR cluster...": "Povezivanje s SOLR klasterom...", + "Connection Failed": "Veza neuspješna", + "Connection Settings": "Postavke veze", + "Connection Successful!": "Veza uspješna!", + "Connection failed": "Veza neuspješna", + "Connection protocol": "Protokol veze", + "Connection successful": "Veza uspješna", + "Connection timeout in seconds": "Istek veze u sekundama", + "Constant delay between retries (5 minutes)": "Stalni odgoda između ponovnih pokušaja (5 minuta)", + "Constraint Issue": "Problem s ograničenjem", + "Consumer not found": "Potrošač nije pronađen", + "Contacts": "Kontakti", + "Contacts integration is not available": "Integracija kontakata nije dostupna", + "Content Filters": "Filtri sadržaja", + "Continue to Properties": "Nastavi na svojstva", + "Contracts": "Ugovori", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontrolirajte koji se prikazi objekata trebaju vektorizirati radi smanjenja troškova API-ja. Vektorizirajte samo prikaze koji imaju koristi od semantičkog pretraživanja.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontrolirajte koje prikaze i alate AI može koristiti u ovom razgovoru. Prema zadanim postavkama, sve su mogućnosti agenta omogućene.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontrolira nasumičnost (0 = determinističko, 2 = vrlo kreativno)", + "Conversation ID is required": "ID razgovora je obavezan", + "Conversation archived": "Razgovor arhiviran", + "Conversation archived successfully": "Razgovor uspješno arhiviran", + "Conversation cleared successfully": "Razgovor uspješno obrisan", + "Conversation deleted": "Razgovor izbrisan", + "Conversation not found": "Razgovor nije pronađen", + "Conversation permanently deleted": "Razgovor trajno izbrisan", + "Conversation renamed": "Razgovor preimenovan", + "Conversation restored": "Razgovor vraćen", + "Conversation started with {agent}": "Razgovor započet s {agent}", + "Conversation title": "Naslov razgovora", + "Conversations": "Razgovori", + "Converting {schema} to blob storage...": "Pretvaranje {schema} u blob pohranu...", + "Converting {schema} to magic table...": "Pretvaranje {schema} u magičnu tablicu...", + "Copied!": "Kopirano!", + "Copy": "Kopiraj", + "Copy Changes": "Kopiraj promjene", + "Copy Collection": "Kopiraj zbirku", + "Copy Data": "Kopiraj podatke", + "Copy Full Data": "Kopiraj sve podatke", + "Copy of {name}": "Kopija od {name}", + "Copy to clipboard": "Kopiraj u međuspremnik", + "Copying...": "Kopiranje...", + "Costs": "Troškovi", + "Count": "Broj", + "Counting objects...": "Brojanje objekata...", + "Create": "Stvori", + "Create API Token": "Stvori API token", + "Create Agent": "Stvori agenta", + "Create Application": "Stvori aplikaciju", + "Create Collection": "Stvori zbirku", + "Create ConfigSet": "Stvori ConfigSet", + "Create New Collection": "Stvori novu zbirku", + "Create New ConfigSet": "Stvori novi ConfigSet", + "Create Operations": "Stvori operacije", + "Create Organisation": "Stvori organizaciju", + "Create Webhook": "Stvori webhook", + "Create a copy of collection:": "Stvori kopiju zbirke:", + "Create a new ConfigSet based on the _default template": "Stvori novi ConfigSet na temelju predloška _default", + "Create a new SOLR collection from an existing ConfigSet": "Stvori novu SOLR zbirku iz postojećeg ConfigSet-a", + "Create a new SOLR collection from an existing configset": "Stvori novu SOLR zbirku iz postojećeg configset-a", + "Create a new configset based on the _default template": "Stvori novi configset na temelju predloška _default", + "Create a new event or link an existing one from any of your calendars.": "Stvorite novi događaj ili povežite postojeći iz bilo kojeg svojeg kalendara.", + "Create another": "Stvori još jedan", + "Create copies of": "Stvori kopije od", + "Create event": "Stvori događaj", + "Create new token": "Stvori novi token", + "Create or link a Deck card to track work on this object.": "Stvorite ili povežite Deck karticu za praćenje rada na ovom objektu.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Stvorite prvu aktivnost obrade kako biste počeli označavati retke revizijskog traga njihovom AVG čl. 30 atribucijom.", + "Create your first AI agent to get started.": "Stvorite svojeg prvog AI agenta za početak.", + "Created": "Stvoreno", + "Created:": "Stvoreno:", + "Creating search collections...": "Stvaranje zbirki pretraživanja...", + "Creating...": "Stvaranje...", + "Current": "Trenutno", + "Current Filters": "Trenutni filtri", + "Current Type:": "Trenutna vrsta:", + "Current password": "Trenutna lozinka", + "Custom API endpoint if using a different region": "Prilagođena API krajnja točka ako koristite drugu regiju", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Prilagođena HTTP zaglavlja (jedno po retku, format: Header-Name: vrijednost)", + "Custom HTTP headers (one per line, format: header-name: value)": "Prilagođena HTTP zaglavlja (jedno po retku, format: header-name: vrijednost)", + "Custom Ranges:": "Prilagođeni rasponi:", + "Custom display title for this facet": "Prilagođeni prikazni naslov za ovu fasetu", + "Custom pattern:": "Prilagođeni uzorak:", + "DELETE COLLECTION": "IZBRIŠI ZBIRKU", + "DSAR": "DSAR", + "Daily": "Dnevno", + "Dashboard": "Nadzorna ploča", + "Dashboard not found": "Nadzorna ploča nije pronađena", + "Data": "Podaci", + "Data Source": "Izvor podataka", + "Data Sources": "Izvori podataka", + "Data Views": "Prikazi podataka", + "Data sources": "Izvori podataka", + "Data type does not match observed values": "Vrsta podataka ne odgovara promatranim vrijednostima", + "Data-subject access request": "Zahtjev ispitanika za pristup", + "Database URL": "URL baze podataka", + "Database information refreshed": "Informacije o bazi podataka osvježene", + "Date Faceting:": "Fasetiranje datuma:", + "Date Range": "Raspon datuma", + "Dates": "Datumi", + "Deactivation request cancelled": "Zahtjev za deaktivaciju otkazan", + "Deactivation request submitted": "Zahtjev za deaktivaciju podnesen", + "Deck": "Deck", + "Deck integration is not available": "Deck integracija nije dostupna", + "Default": "Zadano", + "Default Object Tenant": "Zadani zakupac objekta", + "Default Organisation": "Zadana organizacija", + "Default Owner": "Zadani vlasnik", + "Default User (for cron/background jobs)": "Zadani korisnik (za cron/pozadinske poslove)", + "Default User Tenant": "Zadani zakupac korisnika", + "Default value": "Zadana vrijednost", + "Default value (Email)": "Zadana vrijednost (e-pošta)", + "Default value (Password)": "Zadana vrijednost (lozinka)", + "Default value (Phone number)": "Zadana vrijednost (telefonski broj)", + "Default value (Regex)": "Zadana vrijednost (regularni izraz)", + "Default: 5": "Zadano: 5", + "Defaults to current user. Select a different user if needed.": "Zadano na trenutnog korisnika. Odaberite drugog korisnika ako je potrebno.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Odgode se udvostručuju sa svakim pokušajem (2, 4, 8 minuta...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Odgode se povećavaju linearno (5, 10, 15 minuta...)", + "Delete": "Izbriši", + "Delete ({count})": "Izbriši ({count})", + "Delete Application": "Izbriši aplikaciju", + "Delete Audit Trail": "Izbriši revizijski trag", + "Delete Collection": "Izbriši zbirku", + "Delete ConfigSet": "Izbriši ConfigSet", + "Delete Objects": "Izbriši objekte", + "Delete Operations": "Izbriši operacije", + "Delete SOLR Collection": "Izbriši SOLR zbirku", + "Delete View": "Izbriši prikaz", + "Delete all objects in this schema": "Izbriši sve objekte u ovoj shemi", + "Delete permanently": "Izbriši trajno", + "Delete this conversation?": "Izbrisati ovaj razgovor?", + "Delete view": "Izbriši prikaz", + "Delete {count} object": "Izbriši {count} objekt", + "Delete {count} objects": "Izbriši {count} objekata", + "Deleted": "Izbrisano", + "Deleted By": "Izbrisao", + "Deleted Date": "Datum brisanja", + "Deleted Items Management": "Upravljanje izbrisanim stavkama", + "Deleted This Week": "Izbrisano ovaj tjedan", + "Deleted Today": "Izbrisano danas", + "Deleted:": "Izbrisano:", + "Deleting SOLR Collection...": "Brisanje SOLR zbirke...", + "Deleting...": "Brisanje...", + "Deletion Date Range": "Raspon datuma brisanja", + "Deletion Statistics": "Statistika brisanja", + "Deprecated": "Zastarjelo", + "Depublish": "Poništi objavu", + "Description": "Opis", + "Description Template": "Predložak opisa", + "Description shown as tooltip": "Opis prikazan kao opis alata", + "Description:": "Opis:", + "Detect data types and patterns": "Otkrij vrste i uzorke podataka", + "Detected At": "Otkriveno u", + "Detected Format:": "Otkriveni format:", + "Detected Issues:": "Otkriveni problemi:", + "Detected: {date}": "Otkriveno: {date}", + "Direct permission": "Izravno dopuštenje", + "Disable": "Onemogući", + "Disable this to make the agent": "Onemogućite ovo kako bi agent", + "Disabled": "Onemogućeno", + "Discover Files": "Otkrij datoteke", + "Discovered Properties": "Otkrivena svojstva", + "Display Format": "Format prikaza", + "Display Name": "Prikazni naziv", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Želite li trajno izbrisati sve filtrirane unose revizijskog traga? Ova se radnja ne može poništiti.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Želite li trajno izbrisati ovaj unos revizijskog traga? Ova se radnja ne može poništiti.", + "Documents": "Dokumenti", + "Doelbinding *": "Vezanost svrhe *", + "Dolphin OCR enabled": "Dolphin OCR omogućen", + "Download API Spec": "Preuzmi API specifikaciju", + "Download API Specification": "Preuzmi API specifikaciju", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Preuzmite kopiju svih svojih osobnih podataka pohranjenih u OpenRegister (GDPR članak 20.).", + "Drag a file or files here": "Povucite datoteku ili datoteke ovdje", + "Dry run completed: {count} fields would be created": "Probno izvođenje dovršeno: {count} polja bilo bi stvoreno", + "Dry run completed: {count} fields would be fixed": "Probno izvođenje dovršeno: {count} polja bilo bi ispravljeno", + "Duration in seconds (optional)": "Trajanje u sekundama (neobavezno)", + "Edit": "Uredi", + "Edit Agent": "Uredi agenta", + "Edit Application": "Uredi aplikaciju", + "Edit Configuration": "Uredi konfiguraciju", + "Edit Endpoint": "Uredi krajnju točku", + "Edit Organisation": "Uredi organizaciju", + "Edit Property '{key}' of '{title}'": "Uredi svojstvo '{key}' od '{title}'", + "Edit Register": "Uredi registar", + "Edit Schema": "Uredi shemu", + "Edit Source": "Uredi izvor", + "Edit View": "Uredi prikaz", + "Edit Webhook": "Uredi webhook", + "Edit labels": "Uredi oznake", + "Edit processing activity": "Uredi aktivnost obrade", + "Edit tags for {name}": "Uredi oznake za {name}", + "Edit verwerkingsactiviteit": "Uredi aktivnost obrade", + "Edit view": "Uredi prikaz", + "Edit view details": "Uredi pojedinosti prikaza", + "Effectiveness": "Učinkovitost", + "Email": "E-pošta", + "Email Addresses": "Adrese e-pošte", + "Email digest frequency": "Učestalost sažetka e-pošte", + "Emails": "E-pošte", + "Embedding Model": "Model ugrađivanja", + "Embedding Provider": "Pružatelj ugrađivanja", + "Embedding generated successfully": "Ugrađivanje uspješno generirano", + "Embedding provider connection successful!": "Veza s pružateljem ugrađivanja uspješna!", + "Embedding test failed: {error}": "Test ugrađivanja neuspješan: {error}", + "Enable": "Omogući", + "Enable RAG": "Omogući RAG", + "Enable auto-creation": "Omogući automatsko stvaranje", + "Enable automatic file vectorization": "Omogući automatsku vektorizaciju datoteka", + "Enable automatic object vectorization": "Omogući automatsku vektorizaciju objekata", + "Enable automatic synchronization": "Omogući automatsku sinkronizaciju", + "Enable calendar provider": "Omogući pružatelja kalendara", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Omogući detaljno bilježenje za SOLR operacije (preporučeno za otklanjanje pogrešaka)", + "Enable faceting": "Omogući fasetiranje", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Omogućite ili onemogućite LLM značajke. Konfigurirajte pružatelje i modele koristeći gumb LLM konfiguracija iznad.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Omogućite ili onemogućite LLM značajke. Konfigurirajte pružatelje i modele koristeći gumb LLM konfiguracija iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Omogućite ili onemogućite integraciju SOLR pretraživanja. Konfigurirajte postavke veze koristeći gumb Postavke veze iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Omogućite ili onemogućite integraciju SOLR pretraživanja. Konfigurirajte postavke veze koristeći gumb postavke veze iznad.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Omogućite ili onemogućite integraciju n8n tijeka rada. Konfigurirajte postavke veze ispod.", + "Enable or disable this webhook": "Omogući ili onemogući ovaj webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Omogućite alate koji agentu omogućuju interakciju s podacima putem pozivanja funkcija.", + "Enable vectorization for all existing and future views (may increase costs)": "Omogući vektorizaciju za sve postojeće i buduće prikaze (može povećati troškove)", + "Enable write-back to target objects": "Omogući upisivanje natrag u ciljne objekte", + "Enabled": "Omogućeno", + "End Date Field": "Polje datuma završetka", + "Endpoint Path*": "Putanja krajnje točke*", + "Endpoints": "Krajnje točke", + "Engine not found": "Mehanizam nije pronađen", + "Enter ConfigSet name": "Unesite naziv ConfigSet stavke", + "Enter Nextcloud usernames to grant access to this private agent": "Unesite Nextcloud korisnička imena za dodjelu pristupa ovom privatnom agentu", + "Enter agent description (optional)": "Unesite opis agenta (neobavezno)", + "Enter agent name": "Unesite naziv agenta", + "Enter application description (optional)": "Unesite opis aplikacije (neobavezno)", + "Enter application name": "Unesite naziv aplikacije", + "Enter collection name": "Unesite naziv zbirke", + "Enter configuration description (optional)": "Unesite opis konfiguracije (neobavezno)", + "Enter configuration title": "Unesite naslov konfiguracije", + "Enter description (optional)...": "Unesite opis (neobavezno)...", + "Enter new collection name": "Unesite novi naziv zbirke", + "Enter object ID": "Unesite ID objekta", + "Enter organisation description (optional)": "Unesite opis organizacije (neobavezno)", + "Enter organisation name": "Unesite naziv organizacije", + "Enter search term": "Unesite pojam za pretraživanje", + "Enter search terms or leave empty to browse all": "Unesite pojmove za pretraživanje ili ostavite prazno za pregledavanje svega", + "Enter system prompt for the agent": "Unesite sistemski upit za agenta", + "Enter username and press Enter": "Unesite korisničko ime i pritisnite Enter", + "Enter view name...": "Unesite naziv prikaza...", + "Enter webhook name": "Unesite naziv webhooka", + "Enter your API key": "Unesite svoj API ključ", + "Enter your object here...": "Unesite svoj objekt ovdje...", + "Enter your schema here...": "Unesite svoju shemu ovdje...", + "Entities": "Entiteti", + "Entities detected": "Otkriveni entiteti", + "Entity ID: {id}": "ID entiteta: {id}", + "Entity Information": "Informacije o entitetu", + "Entity Recognition": "Prepoznavanje entiteta", + "Entity Recognition Method": "Metoda prepoznavanja entiteta", + "Entity deleted successfully": "Entitet je uspješno izbrisan", + "Entity not found": "Entitet nije pronađen", + "Entries to be deleted:": "Unosi koji će biti izbrisani:", + "Enum Issue": "Problem s enumeracijom", + "Enum constraint is missing": "Nedostaje ograničenje enumeracije", + "Erase (Art 17)": "Brisanje (čl. 17.)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Izbrisati {count} objekt(a) za ovaj subjekt? Ova radnja bilježi se u revizijskom tragu.", + "Erasure complete": "Brisanje dovršeno", + "Erasure preview": "Pregled brisanja", + "Error": "Pogreška", + "Error Details": "Pojedinosti o pogrešci", + "Error Information": "Informacije o pogrešci", + "Error Message": "Poruka o pogrešci", + "Error deleting audit trails: {error}": "Pogreška pri brisanju revizijskih tragova: {error}", + "Error deleting search trails: {error}": "Pogreška pri brisanju tragova pretraživanja: {error}", + "Error loading application": "Pogreška pri učitavanju aplikacije", + "Error loading audit trails": "Pogreška pri učitavanju revizijskih tragova", + "Error loading entity": "Pogreška pri učitavanju entiteta", + "Error loading search trails": "Pogreška pri učitavanju tragova pretraživanja", + "Estimated Batches:": "Procijenjeni paketi:", + "Estimated Cost:": "Procijenjeni trošak:", + "Estimated Duration:": "Procijenjeno trajanje:", + "Event": "Događaj", + "Event Property for Payload": "Svojstvo događaja za korisni teret", + "Events": "Događaji", + "Every schema with detected PII has a processing-activity annotation.": "Svaka shema s otkrivenim PII ima napomenu o aktivnosti obrade.", + "Example": "Primjer", + "Example Value": "Primjer vrijednosti", + "Example value for this property": "Primjer vrijednosti za ovo svojstvo", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Isključivi maksimum", + "Exclusive Minimum": "Isključivi minimum", + "Exclusive maximum": "Isključivi maksimum", + "Exclusive minimum": "Isključivi minimum", + "Execution Mode": "Način izvođenja", + "Execution Time": "Vrijeme izvođenja", + "Execution Time Range": "Raspon vremena izvođenja", + "Existing Improvements": "Postojeća poboljšanja", + "Expand details": "Proširi pojedinosti", + "Expiration": "Istek", + "Expires": "Istječe", + "Expires in (e.g., 90d)": "Istječe za (npr. 90d)", + "Exponential": "Eksponencijalno", + "Export": "Izvoz", + "Export \"{schema}\" objects from \"{register}\"": "Izvezi objekte \"{schema}\" iz \"{register}\"", + "Export Configuration": "Konfiguracija izvoza", + "Export Format:": "Format izvoza:", + "Export Objects": "Izvoz objekata", + "Export completed successfully": "Izvoz je uspješno dovršen", + "Export downloaded successfully": "Izvoz je uspješno preuzet", + "Export failed: {error}": "Izvoz nije uspio: {error}", + "Export is rate limited. Please try again later.": "Izvoz je ograničen po stopi. Pokušajte ponovno kasnije.", + "Export my data": "Izvezi moje podatke", + "Export, view, or delete audit trails": "Izvezite, pregledajte ili izbrišite revizijske tragove", + "Exporting...": "Izvoz...", + "Extend Schema": "Proširi shemu", + "Extended": "Prošireno", + "Extensions": "Proširenja", + "Extra Query Parameters": "Dodatni parametri upita", + "Extract Now": "Izdvoji sada", + "Extract Pending Files": "Izdvoji datoteke na čekanju", + "Extract now": "Izdvoji sada", + "Extract properties from each object": "Izdvoji svojstva iz svakog objekta", + "Extracted At": "Izdvojeno", + "Extracted at": "Izdvojeno", + "Extraction": "Izdvajanje", + "Extraction Mode": "Način izdvajanja", + "Extraction Scope": "Opseg izdvajanja", + "Extraction Status": "Status izdvajanja", + "Extraction error for {file}: {error}": "Pogreška izdvajanja za {file}: {error}", + "Facet Description": "Opis fasete", + "Facet Order": "Redoslijed fasete", + "Facet Title": "Naslov fasete", + "Facet Type": "Vrsta fasete", + "Facet configuration updated successfully": "Konfiguracija fasete uspješno ažurirana", + "Facetable": "Fasetabilno", + "Faceting Configuration:": "Konfiguracija faseta:", + "Facets discovered and configured successfully": "Fasete su uspješno otkrivene i konfigurirane", + "Facets discovered successfully": "Fasete su uspješno otkrivene", + "Failed": "Neuspjelo", + "Failed to add attachment to object": "Dodavanje privitka objektu nije uspjelo", + "Failed to add user to organisation": "Dodavanje korisnika u organizaciju nije uspjelo", + "Failed to analyze schema properties": "Analiza svojstava sheme nije uspjela", + "Failed to archive conversation": "Arhiviranje razgovora nije uspjelo", + "Failed to calculate sizes": "Izračun veličina nije uspio", + "Failed to cancel deactivation": "Otkazivanje deaktivacije nije uspjelo", + "Failed to change active organisation: {error}": "Promjena aktivne organizacije nije uspjela: {error}", + "Failed to change password": "Promjena lozinke nije uspjela", + "Failed to check version: {error}": "Provjera verzije nije uspjela: {error}", + "Failed to clear audit trails: {error}": "Brisanje revizijskih tragova nije uspjelo: {error}", + "Failed to clear blob objects: {error}": "Brisanje blob objekata nije uspjelo: {error}", + "Failed to clear cache: {error}": "Brisanje predmemorije nije uspjelo: {error}", + "Failed to clear collection": "Brisanje kolekcije nije uspjelo", + "Failed to clear conversation": "Brisanje razgovora nije uspjelo", + "Failed to clear embeddings: {error}": "Brisanje ugradbi nije uspjelo: {error}", + "Failed to clear search trails: {error}": "Brisanje tragova pretraživanja nije uspjelo: {error}", + "Failed to clear {type} cache: {error}": "Brisanje predmemorije {type} nije uspjelo: {error}", + "Failed to copy changes": "Kopiranje promjena nije uspjelo", + "Failed to copy data": "Kopiranje podataka nije uspjelo", + "Failed to copy data to clipboard": "Kopiranje podataka u međuspremnik nije uspjelo", + "Failed to copy to clipboard": "Kopiranje u međuspremnik nije uspjelo", + "Failed to copy token": "Kopiranje tokena nije uspjelo", + "Failed to create conversation": "Stvaranje razgovora nije uspjelo", + "Failed to create missing SOLR fields: {error}": "Stvaranje nedostajućih SOLR polja nije uspjelo: {error}", + "Failed to create or find project": "Stvaranje ili pronalaženje projekta nije uspjelo", + "Failed to create token": "Stvaranje tokena nije uspjelo", + "Failed to delete SOLR collection: {error}": "Brisanje SOLR kolekcije nije uspjelo: {error}", + "Failed to delete collection": "Brisanje kolekcije nije uspjelo", + "Failed to delete conversation": "Brisanje razgovora nije uspjelo", + "Failed to delete objects for {schema}: {error}": "Brisanje objekata za {schema} nije uspjelo: {error}", + "Failed to delete webhook": "Brisanje webhooka nije uspjelo", + "Failed to depublish register: {error}": "Depubliciranje registra nije uspjelo: {error}", + "Failed to depublish schema: {error}": "Depubliciranje sheme nije uspjelo: {error}", + "Failed to depublish: {error}": "Depubliciranje nije uspjelo: {error}", + "Failed to discover files: {error}": "Otkrivanje datoteka nije uspjelo: {error}", + "Failed to download API specification": "Preuzimanje API specifikacije nije uspjelo", + "Failed to export data": "Izvoz podataka nije uspio", + "Failed to export objects": "Izvoz objekata nije uspio", + "Failed to fetch conversation history": "Dohvaćanje povijesti razgovora nije uspjelo", + "Failed to fix mismatched SOLR fields: {error}": "Ispravljanje neusklađenih SOLR polja nije uspjelo: {error}", + "Failed to get SOLR field configuration": "Dohvaćanje konfiguracije SOLR polja nije uspjelo", + "Failed to get chat statistics": "Dohvaćanje statistike razgovora nije uspjelo", + "Failed to get database information: %s": "Dohvaćanje informacija o bazi podataka nije uspjelo: %s", + "Failed to get entity categories": "Dohvaćanje kategorija entiteta nije uspjelo", + "Failed to get entity statistics": "Dohvaćanje statistike entiteta nije uspjelo", + "Failed to get entity types": "Dohvaćanje vrsta entiteta nije uspjelo", + "Failed to get response: {error}": "Dohvaćanje odgovora nije uspjelo: {error}", + "Failed to invalidate app store cache: {error}": "Poništavanje predmemorije trgovine aplikacija nije uspjelo: {error}", + "Failed to leave organisation: {error}": "Napuštanje organizacije nije uspjelo: {error}", + "Failed to link object": "Povezivanje objekta nije uspjelo", + "Failed to load Deck cards": "Učitavanje Deck kartica nije uspjelo", + "Failed to load LLM configuration": "Učitavanje LLM konfiguracije nije uspjelo", + "Failed to load Nextcloud groups": "Učitavanje Nextcloud grupa nije uspjelo", + "Failed to load cache statistics: {error}": "Učitavanje statistike predmemorije nije uspjelo: {error}", + "Failed to load conversation": "Učitavanje razgovora nije uspjelo", + "Failed to load entities": "Učitavanje entiteta nije uspjelo", + "Failed to load entity": "Učitavanje entiteta nije uspjelo", + "Failed to load extraction data": "Učitavanje podataka o izdvajanju nije uspjelo", + "Failed to load files": "Učitavanje datoteka nije uspjelo", + "Failed to load linked contacts": "Učitavanje povezanih kontakata nije uspjelo", + "Failed to load linked emails": "Učitavanje povezanih e-pošta nije uspjelo", + "Failed to load linked events": "Učitavanje povezanih događaja nije uspjelo", + "Failed to load organisations": "Učitavanje organizacija nije uspjelo", + "Failed to load register data": "Učitavanje podataka registra nije uspjelo", + "Failed to load related entities": "Učitavanje povezanih entiteta nije uspjelo", + "Failed to load settings: {error}": "Učitavanje postavki nije uspjelo: {error}", + "Failed to load statistics: {error}": "Učitavanje statistike nije uspjelo: {error}", + "Failed to load templates": "Učitavanje predložaka nije uspjelo", + "Failed to load view: {error}": "Učitavanje prikaza nije uspjelo: {error}", + "Failed to load webhooks": "Učitavanje webhookova nije uspjelo", + "Failed to load workflows": "Učitavanje tijekova rada nije uspjelo", + "Failed to process message": "Obrada poruke nije uspjela", + "Failed to publish register: {error}": "Objavljivanje registra nije uspjelo: {error}", + "Failed to publish schema: {error}": "Objavljivanje sheme nije uspjelo: {error}", + "Failed to publish: {error}": "Objavljivanje nije uspjelo: {error}", + "Failed to refresh database information": "Osvježavanje informacija o bazi podataka nije uspjelo", + "Failed to reindex collection": "Ponovno indeksiranje kolekcije nije uspjelo", + "Failed to remove avatar": "Uklanjanje avatara nije uspjelo", + "Failed to remove link": "Uklanjanje veze nije uspjelo", + "Failed to remove member: {error}": "Uklanjanje člana nije uspjelo: {error}", + "Failed to remove schema {schema}: {error}": "Uklanjanje sheme {schema} nije uspjelo: {error}", + "Failed to remove user from organisation": "Uklanjanje korisnika iz organizacije nije uspjelo", + "Failed to rename conversation": "Preimenovanje razgovora nije uspjelo", + "Failed to request deactivation": "Zahtjev za deaktivaciju nije uspio", + "Failed to restore conversation": "Vraćanje razgovora nije uspjelo", + "Failed to retrieve extraction status.": "Dohvaćanje statusa izdvajanja nije uspjelo.", + "Failed to retrieve objects for file.": "Dohvaćanje objekata za datoteku nije uspjelo.", + "Failed to retry extraction": "Ponovni pokušaj izdvajanja nije uspio", + "Failed to retry webhook": "Ponovni pokušaj webhooka nije uspio", + "Failed to revoke token": "Opoziv tokena nije uspio", + "Failed to save GitHub token": "Spremanje GitHub tokena nije uspjelo", + "Failed to save GitLab URL": "Spremanje GitLab URL-a nije uspjelo", + "Failed to save GitLab token": "Spremanje GitLab tokena nije uspjelo", + "Failed to save LLM settings: {error}": "Spremanje LLM postavki nije uspjelo: {error}", + "Failed to save additional feedback": "Spremanje dodatnih povratnih informacija nije uspjelo", + "Failed to save configuration.": "Spremanje konfiguracije nije uspjelo.", + "Failed to save configuration: {error}": "Spremanje konfiguracije nije uspjelo: {error}", + "Failed to save facet configuration": "Spremanje konfiguracije fasete nije uspjelo", + "Failed to save feedback": "Spremanje povratnih informacija nije uspjelo", + "Failed to save file settings: {error}": "Spremanje postavki datoteke nije uspjelo: {error}", + "Failed to save n8n configuration": "Spremanje n8n konfiguracije nije uspjelo", + "Failed to save preferences": "Spremanje preferencija nije uspjelo", + "Failed to save roles": "Spremanje uloga nije uspjelo", + "Failed to save settings": "Spremanje postavki nije uspjelo", + "Failed to save settings: {error}": "Spremanje postavki nije uspjelo: {error}", + "Failed to save view: {error}": "Spremanje prikaza nije uspjelo: {error}", + "Failed to save warmup interval: {error}": "Spremanje intervala zagrijavanja nije uspjelo: {error}", + "Failed to save webhook": "Spremanje webhooka nije uspjelo", + "Failed to send feedback": "Slanje povratnih informacija nije uspjelo", + "Failed to set active organisation: {error}": "Postavljanje aktivne organizacije nije uspjelo: {error}", + "Failed to setup SOLR: {error}": "Postavljanje SOLR-a nije uspjelo: {error}", + "Failed to start file warmup: {error}": "Pokretanje zagrijavanja datoteka nije uspjelo: {error}", + "Failed to start processing {type} files: {error}": "Pokretanje obrade {type} datoteka nije uspjelo: {error}", + "Failed to start vectorization: {error}": "Pokretanje vektorizacije nije uspjelo: {error}", + "Failed to switch organisation: {error}": "Prebacivanje organizacije nije uspjelo: {error}", + "Failed to sync magic table for {schema}: {error}": "Sinkronizacija čarobne tablice za {schema} nije uspjela: {error}", + "Failed to test SOLR connection: {error}": "Testiranje SOLR veze nije uspjelo: {error}", + "Failed to test webhook": "Testiranje webhooka nije uspjelo", + "Failed to update LLM settings: {error}": "Ažuriranje LLM postavki nije uspjelo: {error}", + "Failed to update Multitenancy settings: {error}": "Ažuriranje postavki višestruke zakupništva nije uspjelo: {error}", + "Failed to update RBAC settings: {error}": "Ažuriranje RBAC postavki nije uspjelo: {error}", + "Failed to update Retention settings: {error}": "Ažuriranje postavki čuvanja nije uspjelo: {error}", + "Failed to update SOLR settings: {error}": "Ažuriranje SOLR postavki nije uspjelo: {error}", + "Failed to update favorite status": "Ažuriranje statusa favorita nije uspjelo", + "Failed to update schema configuration for {schema}: {error}": "Ažuriranje konfiguracije sheme za {schema} nije uspjelo: {error}", + "Failed to update schema properties": "Ažuriranje svojstava sheme nije uspjelo", + "Failed to update view": "Ažuriranje prikaza nije uspjelo", + "Failed to update view: {error}": "Ažuriranje prikaza nije uspjelo: {error}", + "Failed to update webhook": "Ažuriranje webhooka nije uspjelo", + "Failed to upload avatar": "Učitavanje avatara nije uspjelo", + "Failed to validate {schema}: {error}": "Provjera valjanosti {schema} nije uspjela: {error}", + "Failed to vectorize files: {error}": "Vektorizacija datoteka nije uspjela: {error}", + "Failed to warmup names cache: {error}": "Zagrijavanje predmemorije naziva nije uspjelo: {error}", + "Federation": "Federacija", + "Feedback recorded": "Povratne informacije zabilježene", + "Fewer sources (1-3):": "Manje izvora (1-3):", + "Field": "Polje", + "Fields to Display": "Polja za prikaz", + "File": "Datoteka", + "File #{id}": "Datoteka #{id}", + "File Chunk Prediction": "Predviđanje fragmenta datoteke", + "File Collection": "Kolekcija datoteka", + "File Handling": "Rukovanje datotekama", + "File Location": "Lokacija datoteke", + "File Management": "Upravljanje datotekama", + "File Name": "Naziv datoteke", + "File Path": "Putanja datoteke", + "File Type Selection": "Odabir vrste datoteke", + "File Types to Process": "Vrste datoteka za obradu", + "File Vectorization": "Vektorizacija datoteka", + "File Warmup": "Zagrijavanje datoteka", + "File actions menu": "Izbornik radnji datoteke", + "File anonymized successfully": "Datoteka je uspješno anonimizirana", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vektorizacija fragmenata datoteke još nije implementirana. Fragmenti su spremni i pohranjeni, ali je usluga vektorizacije u razvoju.", + "File collection not configured": "Kolekcija datoteka nije konfigurirana", + "File discovery completed": "Otkrivanje datoteka dovršeno", + "File extraction completed": "Izdvajanje datoteke dovršeno", + "File extraction queued": "Izdvajanje datoteke stavljeno u red čekanja", + "File indexed successfully": "Datoteka je uspješno indeksirana", + "File is already anonymized": "Datoteka je već anonimizirana", + "File name": "Naziv datoteke", + "File not found": "Datoteka nije pronađena", + "File settings saved successfully": "Postavke datoteke uspješno spremljene", + "File settings updated successfully": "Postavke datoteke uspješno ažurirane", + "File sources": "Izvori datoteka", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vektorizacija datoteka dovršena! {vectorized} fragmenata vektorizirano iz {files} datoteka. {failed} neuspjelo.", + "File vectorization configuration saved successfully": "Konfiguracija vektorizacije datoteka uspješno spremljena", + "File vectorization started. Check the statistics section for progress.": "Vektorizacija datoteka pokrenuta. Provjerite odjeljak statistike za napredak.", + "File warmup completed": "Zagrijavanje datoteka dovršeno", + "Files": "Datoteke", + "Files added successfully": "Datoteke uspješno dodane", + "Files with Completed Extraction:": "Datoteke s dovršenim izdvajanjem:", + "Files → fileCollection, Objects → objectCollection": "Datoteke → fileCollection, Objekti → objectCollection", + "Files → filecollection, objects → objectcollection": "Datoteke → filecollection, objekti → objectcollection", + "Filter Audit Trails": "Filtriraj revizijske tragove", + "Filter Deleted Items": "Filtriraj izbrisane stavke", + "Filter Objects": "Filtriraj objekte", + "Filter Properties": "Filtriraj svojstva", + "Filter Search Trails": "Filtriraj tragove pretraživanja", + "Filter Statistics": "Filtriraj statistiku", + "Filter and analyze search trail entries": "Filtrirajte i analizirajte unose tragova pretraživanja", + "Filter and manage audit trail entries": "Filtrirajte i upravljajte unosima revizijskog traga", + "Filter and manage soft deleted items": "Filtrirajte i upravljajte meko izbrisanim stavkama", + "Filter and search entities": "Filtrirajte i pretražujte entitete", + "Filter by object ID": "Filtriraj po ID-u objekta", + "Filter by search term": "Filtriraj po pojmu za pretraživanje", + "Filter by type": "Filtriraj po vrsti", + "Filter by webhook": "Filtriraj po webhooku", + "Filter data loaded automatically. Use the filters below to refine your search.": "Podaci filtera učitani automatski. Upotrijebite filtere u nastavku za pročišćavanje pretraživanja.", + "Filter fields...": "Filtriraj polja...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrirajte okidače webhooka po svojstvima korisnog tereta (jedno po retku, format: key: value)", + "Filtered": "Filtrirano", + "Filters": "Filteri", + "Finalizing setup...": "Dovršavanje postavljanja...", + "Fireworks AI Chat Settings": "Postavke razgovora Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfiguracija ugradbi Fireworks AI", + "First": "Prvo", + "Fixed": "Fiksno", + "For chat and retrieval-augmented generation": "Za razgovor i generiranje uz potporu dohvaćanja", + "For vector embeddings and semantic search": "Za vektorske ugradbe i semantičko pretraživanje", + "Format": "Format", + "Format Issue": "Problem s formatom", + "Format constraint is missing": "Nedostaje ograničenje formata", + "From (e.g. -7 days or 2025-01-01)": "Od (npr. -7 dana ili 2025-01-01)", + "From Date": "Od datuma", + "From date": "Od datuma", + "Full data copied to clipboard": "Potpuni podaci kopirani u međuspremnik", + "Function Tools": "Funkcijski alati", + "General Issue": "Općeniti problem", + "Generate recommendations and confidence scores": "Generiraj preporuke i ocjene pouzdanosti", + "Generate report": "Generiraj izvješće", + "Generate the verantwoordingsdocument": "Generiraj dokument o odgovornosti", + "Generate vectors immediately when new objects are created": "Generiraj vektore odmah kada se stvore novi objekti", + "Generated: {time}": "Generirano: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Dobijte uvide u stanje podataka vaše organizacije u stvarnom vremenu fokusiranjem na registre, definicije shema te pohranu i upotrebu objekata.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Dobijte uvide u stanje podataka vaše organizacije, trendove rasta i metrike kvalitete u stvarnom vremenu.", + "GitHub Personal Access Token": "GitHub osobni pristupni token", + "GitHub token is valid": "GitHub token je valjan", + "GitHub token is valid! Username: {username}": "GitHub token je valjan! Korisničko ime: {username}", + "GitHub token removed successfully": "GitHub token uspješno uklonjen", + "GitHub token saved successfully": "GitHub token uspješno spremljen", + "GitHub token test failed: {message}": "Testiranje GitHub tokena nije uspjelo: {message}", + "GitLab Personal Access Token": "GitLab osobni pristupni token", + "GitLab URL saved successfully": "GitLab URL uspješno spremljen", + "GitLab token is valid": "GitLab token je valjan", + "GitLab token is valid! Username: {username}": "GitLab token je valjan! Korisničko ime: {username}", + "GitLab token saved successfully": "GitLab token uspješno spremljen", + "GitLab token test failed: {message}": "Testiranje GitLab tokena nije uspjelo: {message}", + "Go to linked Register": "Idi na povezani registar", + "Go to linked Schema": "Idi na povezanu shemu", + "Go to organisation": "Idi na organizaciju", + "Grant additional permissions beyond standard CRUD operations": "Dodijeli dodatna dopuštenja izvan standardnih CRUD operacija", + "Group Quota": "Kvota grupe", + "Groups": "Grupe", + "HTML preview": "HTML pretpregled", + "HTTP Method": "HTTP metoda", + "HTTP method used to send webhook requests": "HTTP metoda koja se koristi za slanje zahtjeva webhooka", + "Headers": "Zaglavlja", + "Health": "Stanje", + "Heartbeat successful - connection kept alive": "Otkucaj uspješan - veza održana aktivnom", + "Helpful": "Korisno", + "Here you can configure the details for various connections.": "Ovdje možete konfigurirati pojedinosti za razne veze.", + "Here you can configure the details for your organisation.": "Ovdje možete konfigurirati pojedinosti za svoju organizaciju.", + "Hide": "Sakrij", + "Hide Filters": "Sakrij filtere", + "Hide in collection view": "Sakrij u prikazu kolekcije", + "Hide in forms": "Sakrij u obrascima", + "Hide in list view": "Sakrij u prikazu popisa", + "High": "Visoko", + "High Confidence": "Visoka pouzdanost", + "Host": "Poslužitelj", + "Hourly": "Svaki sat", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Koliko duboko prolaziti kroz ugniježđena svojstva objekta (1-20). Više vrijednosti obuhvaćaju više pojedinosti, ali povećavaju veličinu vektora.", + "How often to check for updates (1-168 hours)": "Koliko često provjeravati ažuriranja (1-168 sati)", + "How to handle retries for failed webhook deliveries": "Kako rukovati ponovnim pokušajima za neuspjele isporuke webhooka", + "IBANs": "IBAN-ovi", + "ID": "ID", + "ID of input mapping (optional)": "ID ulaznog mapiranja (neobavezno)", + "ID of output mapping (optional)": "ID izlaznog mapiranja (neobavezno)", + "ID of the target resource": "ID ciljnog resursa", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ograničenja IP stope uspješno izbrisana", + "Identify properties not in the schema": "Identificiraj svojstva koja nisu u shemi", + "Immutable": "Nepromjenjivo", + "Import": "Uvoz", + "Import Configuration": "Konfiguracija uvoza", + "Import Data into Register": "Uvezi podatke u registar", + "Import successful": "Uvoz uspješan", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Uvezite predložak report-bundle.json da biste dobili registar `reports`, zatim stvorite svoju prvu nadzornu ploču putem standardnog korisničkog sučelja za objekte. Nadzorne ploče deklariraju svoje widgete u JSON-u, a prikazivač svakom widgetu dostavlja podatke agregacije uživo.", + "Improved Property": "Poboljšano svojstvo", + "In use": "U upotrebi", + "Inactive": "Neaktivno", + "Inactive organisations cannot be used": "Neaktivne organizacije ne mogu se koristiti", + "Include IDs and names of related objects for better contextual search": "Uključi ID-ove i nazive povezanih objekata za bolje kontekstualno pretraživanje", + "Include ids and names of related objects for better contextual search": "Uključi id-ove i nazive povezanih objekata za bolje kontekstualno pretraživanje", + "Include related object references": "Uključi reference povezanih objekata", + "Include schema and register metadata": "Uključi metapodatke sheme i registra", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Informacije o trenutnoj OpenRegister instalaciji", + "Inherited from register": "Naslijeđeno od registra", + "Inherits permissions from register": "Nasljeđuje dopuštenja od registra", + "Initialization failed": "Inicijalizacija nije uspjela", + "Initialize Project": "Inicijaliziraj projekt", + "Initializing SOLR setup...": "Inicijalizacija SOLR postavljanja...", + "Input Mapping": "Ulazno mapiranje", + "Inspect Fields": "Pregledaj polja", + "Inspect Index": "Pregledaj indeks", + "Inspect SOLR Index": "Pregledaj SOLR indeks", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instalirajte aplikaciju notify_push iz trgovine aplikacija Nextcloud kako biste omogućili ažuriranja u stvarnom vremenu.", + "Integrations": "Integracije", + "Internet location (URL) of this installation": "Internetska lokacija (URL) ove instalacije", + "Interval": "Interval", + "Invalid": "Nevaljano", + "Invalid batch size. Must be between 1 and 5000": "Nevaljana veličina paketa. Mora biti između 1 i 5000", + "Invalid feedback type": "Nevaljana vrsta povratne informacije", + "Invalid field name provided": "Naveden je nevaljan naziv polja", + "Invalid maxObjects. Must be 0 (all) or positive number": "Nevaljan maxObjects. Mora biti 0 (sve) ili pozitivan broj", + "Invalid:": "Nevaljano:", + "Invite Users": "Pozovi korisnike", + "Invited Users": "Pozvani korisnici", + "Inzage (Art 15)": "Uvid (čl. 15.)", + "Inzage results": "Rezultati uvida", + "Issue": "Problem", + "Items per page:": "Stavki po stranici:", + "Just now": "Upravo sada", + "Key": "Ključ", + "LLM Configuration": "LLM konfiguracija", + "LLM actions menu": "Izbornik LLM radnji", + "LLM configuration saved successfully": "LLM konfiguracija uspješno spremljena", + "LLM features disabled": "LLM značajke onemogućene", + "LLM features enabled": "LLM značajke omogućene", + "LLM must be enabled with an embedding provider configured": "LLM mora biti omogućen uz konfiguriranog pružatelja ugradbi", + "LLM settings saved successfully": "LLM postavke uspješno spremljene", + "LLM settings updated successfully": "LLM postavke uspješno ažurirane", + "Label": "Oznaka", + "Labels": "Oznake", + "Languages": "Jezici", + "Last": "Zadnje", + "Last Triggered": "Zadnje aktivirano", + "Leave empty to allow all users access": "Ostavite prazno da biste dopustili pristup svim korisnicima", + "Leave off for auto-detection from property format.": "Ostavite isključeno za automatsko otkrivanje iz formata svojstva.", + "Left organisation successfully": "Uspješno napuštena organizacija", + "Legal basis *": "Pravna osnova *", + "Length Range:": "Raspon duljine:", + "Linear": "Linearno", + "Link": "Veza", + "Link an email from the Mail app sidebar to associate it with this object.": "Povežite e-poštu iz bočne trake aplikacije Mail kako biste je pridružili ovom objektu.", + "Link an object to see it here.": "Povežite objekt da biste ga ovdje vidjeli.", + "Link another object": "Poveži drugi objekt", + "Link existing event": "Poveži postojeći događaj", + "Link removed": "Veza uklonjena", + "Link to Object": "Veza na objekt", + "Link to {name}": "Veza na {name}", + "Linked Objects": "Povezani objekti", + "Linked by {user}": "Povezao {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Povezane e-pošte, kalendarski događaji, kontakti i Deck kartice pojavit će se ovdje.", + "Linked to {name}": "Povezano s {name}", + "Load Advanced Filters": "Učitaj napredne filtere", + "Load advanced filters with live data from your search index": "Učitaj napredne filtere s podacima uživo iz vašeg indeksa pretraživanja", + "Load more": "Učitaj više", + "Load view": "Učitaj prikaz", + "Loading API tokens...": "Učitavanje API tokena...", + "Loading ConfigSets...": "Učitavanje ConfigSetova...", + "Loading LLM configuration...": "Učitavanje LLM konfiguracije...", + "Loading RBAC settings...": "Učitavanje RBAC postavki...", + "Loading activity...": "Učitavanje aktivnosti...", + "Loading advanced filters...": "Učitavanje naprednih filtera...", + "Loading agents...": "Učitavanje agenata...", + "Loading application details...": "Učitavanje pojedinosti aplikacije...", + "Loading applications...": "Učitavanje aplikacija...", + "Loading archived conversations...": "Učitavanje arhiviranih razgovora...", + "Loading audit trails...": "Učitavanje revizijskih tragova...", + "Loading available tools...": "Učitavanje dostupnih alata...", + "Loading cache statistics...": "Učitavanje statistike predmemorije...", + "Loading collections...": "Učitavanje kolekcija...", + "Loading configurations...": "Učitavanje konfiguracija...", + "Loading conversation...": "Učitavanje razgovora...", + "Loading conversations...": "Učitavanje razgovora...", + "Loading deleted items...": "Učitavanje izbrisanih stavki...", + "Loading endpoints": "Učitavanje krajnjih točaka", + "Loading entities...": "Učitavanje entiteta...", + "Loading events...": "Učitavanje događaja...", + "Loading filter data...": "Učitavanje podataka filtera...", + "Loading groups...": "Učitavanje grupa...", + "Loading linked objects...": "Učitavanje povezanih objekata...", + "Loading log details...": "Učitavanje pojedinosti zapisnika...", + "Loading multitenancy settings...": "Učitavanje postavki višestruke zakupništva...", + "Loading n8n configuration...": "Učitavanje n8n konfiguracije...", + "Loading permission matrix...": "Učitavanje matrice dopuštenja...", + "Loading preferences...": "Učitavanje preferencija...", + "Loading register data...": "Učitavanje podataka registra...", + "Loading registers...": "Učitavanje registara...", + "Loading retention settings...": "Učitavanje postavki čuvanja...", + "Loading schemas...": "Učitavanje shema...", + "Loading search configuration...": "Učitavanje konfiguracije pretraživanja...", + "Loading search trails...": "Učitavanje tragova pretraživanja...", + "Loading sources...": "Učitavanje izvora...", + "Loading statistics...": "Učitavanje statistike...", + "Loading text extraction settings...": "Učitavanje postavki izdvajanja teksta...", + "Loading tokens...": "Učitavanje tokena...", + "Loading user groups...": "Učitavanje korisničkih grupa...", + "Loading users...": "Učitavanje korisnika...", + "Loading views...": "Učitavanje prikaza...", + "Loading...": "Učitavanje...", + "Local": "Lokalno", + "Local Version": "Lokalna verzija", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Pronađite svaki objekt koji referencira ispitanika (čl. 15. uvid), pregledajte brisanje (čl. 17. pravo na zaborav) ili izvezite njihove podatke (čl. 20. prenosivost).", + "Location": "Lokacija", + "Location Field": "Polje lokacije", + "Locations": "Lokacije", + "Locked": "Zaključano", + "Locked:": "Zaključano:", + "Login successful": "Prijava uspješna", + "Logs": "Zapisnici", + "Low": "Nisko", + "Low Confidence": "Niska pouzdanost", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Niži brojevi pojavljuju se prvi u bočnoj traci filtera. Ostavite prazno za automatski redoslijed.", + "Lucene Version:": "Lucene verzija:", + "Magic table sync completed for {schema}": "Sinkronizacija čarobne tablice dovršena za {schema}", + "Magic table synchronized successfully": "Čarobna tablica uspješno sinkronizirana", + "Mail Integration": "Integracija e-pošte", + "Mail integration is not available": "Integracija e-pošte nije dostupna", + "Make this installation discoverable within the federation network.": "Učinite ovu instalaciju pronalažljivom unutar mreže federacije.", + "Manage Organisation Roles": "Upravljaj ulogama organizacije", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Upravljajte SOLR kolekcijama (spremišta podataka) i dodijelite ih za objekte i datoteke.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Upravljajte SOLR ConfigSetovima (predlošci konfiguracije) za svoje kolekcije.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Upravljajte SOLR kolekcijama (spremišta podataka) i dodijelite ih za objekte i datoteke.", + "Manage SOLR configsets (configuration templates) for your collections.": "Upravljajte SOLR configsetovima (predlošci konfiguracije) za svoje kolekcije.", + "Manage and configure agents for automated tasks": "Upravljajte i konfigurirajte agente za automatizirane zadatke", + "Manage and monitor file text extraction status": "Upravljajte i nadzirite status izdvajanja teksta iz datoteka", + "Manage and restore soft deleted items from your registers": "Upravljajte i vraćajte meko izbrisane stavke iz svojih registara", + "Manage and view detected entities from files and objects": "Upravljajte i pregledajte otkrivene entitete iz datoteka i objekata", + "Manage document templates and themes": "Upravljajte predlošcima i temama dokumenata", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Upravljajte n8n tijekovima rada za OpenRegister automatizaciju. Tijekovi rada bit će pohranjeni u konfiguriranom projektu.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Upravljajte aktivnostima obrade, izvršavajte zahtjeve za pristup ispitanika i revidirajte usklađenost prema EU GDPR / nizozemskom AVG.", + "Manage webhooks for event-driven integrations": "Upravljajte webhookovima za integracije vođene događajima", + "Manage your account settings, security, and personal data.": "Upravljajte postavkama računa, sigurnošću i osobnim podacima.", + "Manage your applications and modules": "Upravljajte svojim aplikacijama i modulima", + "Manage your chat conversations": "Upravljajte svojim razgovorima", + "Manage your data registers and their configurations": "Upravljajte svojim registrima podataka i njihovim konfiguracijama", + "Manage your data schemas and their properties": "Upravljajte svojim shemama podataka i njihovim svojstvima", + "Manage your data sources and their configurations": "Upravljajte svojim izvorima podataka i njihovim konfiguracijama", + "Manage your organisations and switch between them": "Upravljajte svojim organizacijama i prebacujte se između njih", + "Manage your saved search configurations": "Upravljajte svojim spremljenim konfiguracijama pretraživanja", + "Manage your system configurations and settings": "Upravljajte svojim konfiguracijama i postavkama sustava", + "Managed": "Upravljano", + "Mappings": "Mapiranja", + "Mass validation completed successfully": "Masovna provjera valjanosti uspješno dovršena", + "Mass validation failed: {error}": "Masovna provjera valjanosti nije uspjela: {error}", + "Max Files (0 = all)": "Maks. datoteka (0 = sve)", + "Max Length": "Maks. duljina", + "Max Length:": "Maks. duljina:", + "Max Objects (0 = all)": "Maks. objekata (0 = sve)", + "Max Retries": "Maks. ponovnih pokušaja", + "Max Shards/Node": "Maks. shardova/čvor", + "Max Tokens": "Maks. tokena", + "Max execution time (ms)": "Maks. vrijeme izvođenja (ms)", + "Max ms": "Maks. ms", + "Max result count": "Maks. broj rezultata", + "Max results": "Maks. rezultata", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksimalna veličina datoteke (MB)", + "Maximum Nesting Depth": "Maksimalna dubina ugniježđivanja", + "Maximum length": "Maksimalna duljina", + "Maximum length constraint is missing": "Nedostaje ograničenje maksimalne duljine", + "Maximum length is too restrictive": "Maksimalna duljina previše je restriktivna", + "Maximum number of files to process. Set to 0 to process all files.": "Maksimalan broj datoteka za obradu. Postavite na 0 za obradu svih datoteka.", + "Maximum number of items": "Maksimalan broj stavki", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksimalan broj objekata za obradu. Postavite na 0 za obradu svih objekata.", + "Maximum number of retry attempts for failed deliveries": "Maksimalan broj ponovnih pokušaja za neuspjele isporuke", + "Maximum time to wait before committing changes": "Maksimalno vrijeme čekanja prije potvrđivanja promjena", + "Maximum tokens to generate": "Maksimalan broj tokena za generiranje", + "Maximum value": "Maksimalna vrijednost", + "Maximum value constraint is missing": "Nedostaje ograničenje maksimalne vrijednosti", + "Maximum value is too restrictive": "Maksimalna vrijednost previše je restriktivna", + "Medium": "Srednje", + "Medium Confidence": "Srednja pouzdanost", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Srednji upiti: Pretraživanja s nekim filtriranjem ili više parametara (npr. rasponi datuma, određeni registri/sheme)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Srednji upiti: pretraživanja s nekim filtriranjem ili više parametara (npr. rasponi datuma, određeni registri/sheme)", + "Member removed successfully": "Član uspješno uklonjen", + "Members": "Članovi", + "Members ({count})": "Članovi ({count})", + "Members of selected groups can access this view": "Članovi odabranih grupa mogu pristupiti ovom prikazu", + "Members:": "Članovi:", + "Memory prediction calculated successfully": "Predviđanje memorije uspješno izračunato", + "Message content is required": "Sadržaj poruke je obavezan", + "Message does not belong to this conversation": "Poruka ne pripada ovom razgovoru", + "Message not found": "Poruka nije pronađena", + "Metadata": "Metapodaci", + "Metadata Filters": "Filteri metapodataka", + "Method": "Metoda", + "Method*": "Metoda*", + "Method: {method}": "Metoda: {method}", + "Min Length": "Min. duljina", + "Min execution time (ms)": "Min. vrijeme izvođenja (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. broj rezultata", + "Min results": "Min. rezultata", + "Minimum": "Minimum", + "Minimum length": "Minimalna duljina", + "Minimum number of items": "Minimalan broj stavki", + "Minimum value": "Minimalna vrijednost", + "Minimum value constraint is missing": "Nedostaje ograničenje minimalne vrijednosti", + "Minimum value is too restrictive": "Minimalna vrijednost previše je restriktivna", + "Missing conversation or agentUuid": "Nedostaje razgovor ili agentUuid", + "Missing conversationId": "Nedostaje conversationId", + "Missing message": "Nedostaje poruka", + "Missing object fields created successfully": "Nedostajuća polja objekta uspješno stvorena", + "Mode:": "Način:", + "Model": "Model", + "Model Name": "Naziv modela", + "Modified": "Izmijenjeno", + "Monthly": "Mjesečno", + "More sources (10-20):": "Više izvora (10-20):", + "Most Active Objects": "Najaktivniji objekti", + "Multiple Of": "Višekratnik od", + "Multiple of": "Višekratnik od", + "Multiple search trail deletion not implemented yet": "Brisanje više tragova pretraživanja još nije implementirano", + "Multitenancy": "Višestruka zakupništvo", + "Multitenancy settings updated successfully": "Postavke višestruke zakupništva uspješno ažurirane", + "My Account": "Moj račun", + "Naam": "Naziv", + "Naam *": "Naziv *", + "Name": "Naziv", + "Name *": "Naziv *", + "Name*": "Naziv*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Predmemorija naziva uspješno je zagrijana: {count} naziva učitano za {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Predmemorija naziva uspješno je zagrijana: {count} naziva učitano za {time}. {message}", + "Names cache warmup completed": "Zagrijavanje predmemorije naziva dovršeno", + "Naming pattern for copies:": "Obrazac imenovanja za kopije:", + "New Collection Name": "Naziv nove zbirke", + "New Configuration": "Nova konfiguracija", + "New Conversation": "Novi razgovor", + "New Properties": "Nova svojstva", + "New Property": "Novo svojstvo", + "New User Group": "Nova korisnička grupa", + "New Value": "Nova vrijednost", + "New activity": "Nova aktivnost", + "New password": "Nova lozinka", + "New processing activity": "Nova aktivnost obrade", + "New users without specific organisation membership will be automatically added to this organisation": "Novi korisnici bez određenog članstva u organizaciji bit će automatski dodani u ovu organizaciju", + "New verwerkingsactiviteit": "Nova aktivnost obrade", + "Next": "Sljedeće", + "Nextcloud Groups": "Nextcloud grupe", + "No": "Ne", + "No API tokens.": "Nema API tokena.", + "No ConfigSets found": "Nisu pronađeni ConfigSet-ovi", + "No Deck cards linked to this object": "Nijedna Deck kartica nije povezana s ovim objektom", + "No GitHub token configured": "GitHub token nije konfiguriran", + "No GitHub token provided": "GitHub token nije naveden", + "No GitLab token provided": "GitLab token nije naveden", + "No Organisation": "Nema organizacije", + "No active filters": "Nema aktivnih filtara", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Nema aktivnih objekata za meko brisanje za shemu {schema}. Upotrijebite \"Trajno izbriši\" za uklanjanje meko izbrisanih objekata.", + "No activity data available": "Nema dostupnih podataka o aktivnosti", + "No activity found.": "Nije pronađena aktivnost.", + "No agents available": "Nema dostupnih agenata", + "No agents found": "Nisu pronađeni agenti", + "No app store cache exists yet": "Predmemorija trgovine aplikacijama još ne postoji", + "No applications are available.": "Nema dostupnih aplikacija.", + "No applications found": "Nisu pronađene aplikacije", + "No approval steps for this object.": "Nema koraka odobravanja za ovaj objekt.", + "No archived conversations": "Nema arhiviranih razgovora", + "No audit trail entries found": "Nisu pronađeni unosi revizijskog traga", + "No changes recorded": "Nisu zabilježene promjene", + "No chunks to vectorize": "Nema dijelova za vektorizaciju", + "No collections found": "Nisu pronađene zbirke", + "No configuration data": "Nema podataka o konfiguraciji", + "No configuration selected": "Nije odabrana konfiguracija", + "No configurations are available.": "Nema dostupnih konfiguracija.", + "No configurations found": "Nisu pronađene konfiguracije", + "No configurations found for this application.": "Nisu pronađene konfiguracije za ovu aplikaciju.", + "No contacts linked to this object": "Nijedan kontakt nije povezan s ovim objektom", + "No conversations yet. Create a new one to get started!": "Još nema razgovora. Stvorite novi za početak!", + "No dashboards yet": "Još nema nadzornih ploča", + "No data available": "Nema dostupnih podataka", + "No data available for chart": "Nema dostupnih podataka za grafikon", + "No deleted items found": "Nisu pronađene izbrisane stavke", + "No deletion data available": "Nema dostupnih podataka o brisanju", + "No description found": "Nije pronađen opis", + "No description provided": "Nije naveden opis", + "No emails linked to this object": "Nijedna e-pošta nije povezana s ovim objektom", + "No endpoints defined": "Nisu definirane krajnje točke", + "No entities detected for this email.": "Nisu otkriveni entiteti za ovu e-poštu.", + "No entities detected in this file. Run text extraction first.": "Nisu otkriveni entiteti u ovoj datoteci. Prvo pokrenite izvlačenje teksta.", + "No entities found": "Nisu pronađeni entiteti", + "No entities have been detected yet": "Još nisu otkriveni entiteti", + "No events found": "Nisu pronađeni događaji", + "No events linked to this object": "Nijedan događaj nije povezan s ovim objektom", + "No expired audit trails found to clear": "Nisu pronađeni istekli revizijski tragovi za brisanje", + "No expired search trails found to clear": "Nisu pronađeni istekli tragovi pretraživanja za brisanje", + "No extraction data available for this file": "Nema dostupnih podataka o izvlačenju za ovu datoteku", + "No facetable fields available. Select a register and schema to see available filters.": "Nema dostupnih polja za fasete. Odaberite registar i shemu da biste vidjeli dostupne filtre.", + "No files found": "Nisu pronađene datoteke", + "No files have been extracted yet": "Još nisu izvučene datoteke", + "No files selected": "Nisu odabrane datoteke", + "No files to process": "Nema datoteka za obradu", + "No files to reindex": "Nema datoteka za ponovno indeksiranje", + "No filters are currently active. This will delete ALL audit trail entries!": "Trenutno nije aktivan nijedan filtar. Time će se izbrisati SVI unosi revizijskog traga!", + "No groups found. Try a different search.": "Nisu pronađene grupe. Pokušajte s drugačijim pretraživanjem.", + "No label": "Nema oznake", + "No labels": "Nema oznaka", + "No linked objects": "Nema povezanih objekata", + "No log entries found": "Nisu pronađeni unosi zapisnika", + "No logs are available for this configuration.": "Nema dostupnih zapisnika za ovu konfiguraciju.", + "No logs are available for this source.": "Nema dostupnih zapisnika za ovaj izvor.", + "No logs found": "Nisu pronađeni zapisnici", + "No matches": "Nema podudaranja", + "No mismatched fields found - SOLR schema is properly configured": "Nisu pronađena nepodudarna polja - SOLR shema je ispravno konfigurirana", + "No objects are currently selected for deletion.": "Trenutno nije odabran nijedan objekt za brisanje.", + "No objects are currently selected for permanent deletion.": "Trenutno nije odabran nijedan objekt za trajno brisanje.", + "No objects are currently selected for restoration.": "Trenutno nije odabran nijedan objekt za vraćanje.", + "No objects found": "Nisu pronađeni objekti", + "No objects found to vectorize": "Nisu pronađeni objekti za vektorizaciju", + "No objects linked to this email": "Nijedan objekt nije povezan s ovom e-poštom", + "No objects selected": "Nije odabran nijedan objekt", + "No objects selected for deletion": "Nije odabran nijedan objekt za brisanje", + "No objects selected for restoration": "Nije odabran nijedan objekt za vraćanje", + "No objects to delete": "Nema objekata za brisanje", + "No objects to delete for schema {schema}": "Nema objekata za brisanje za shemu {schema}", + "No objects were permanently deleted": "Nijedan objekt nije trajno izbrisan", + "No organisations found": "Nisu pronađene organizacije", + "No personal data was found for this subject identifier.": "Nisu pronađeni osobni podaci za ovaj identifikator subjekta.", + "No processing activities yet": "Još nema aktivnosti obrade", + "No properties found": "Nisu pronađena svojstva", + "No properties match your filters.": "Nijedno svojstvo ne odgovara vašim filtrima.", + "No purge date set": "Datum čišćenja nije postavljen", + "No register data available": "Nema dostupnih podataka o registru", + "No register objects reference this file": "Nijedan objekt registra ne referencira ovu datoteku", + "No registers found": "Nisu pronađeni registri", + "No registers found for this application.": "Nisu pronađeni registri za ovu aplikaciju.", + "No registers found. Create a register to configure permissions.": "Nisu pronađeni registri. Stvorite registar za konfiguriranje dozvola.", + "No related cases found for this sender": "Nisu pronađeni povezani predmeti za ovog pošiljatelja", + "No related entities": "Nema povezanih entiteta", + "No relations found": "Nisu pronađene veze", + "No request body available": "Nema dostupnog tijela zahtjeva", + "No response body available": "Nema dostupnog tijela odgovora", + "No saved views yet. Create one in the Search tab!": "Još nema spremljenih prikaza. Stvorite jedan na kartici Pretraživanje!", + "No saved views yet. create one in the search tab!": "Još nema spremljenih prikaza. stvorite jedan na kartici pretraživanja!", + "No scan run yet": "Skeniranje još nije pokrenuto", + "No schema data available": "Nema dostupnih podataka o shemi", + "No schema selected for exploration": "Nije odabrana shema za istraživanje", + "No schemas configured for mail linking.": "Nijedna shema nije konfigurirana za povezivanje pošte.", + "No schemas found": "Nisu pronađene sheme", + "No schemas found for this application.": "Nisu pronađene sheme za ovu aplikaciju.", + "No search terms data available": "Nema dostupnih podataka o pojmovima pretraživanja", + "No search trail entries found": "Nisu pronađeni unosi traga pretraživanja", + "No sources are available.": "Nema dostupnih izvora.", + "No sources found": "Nisu pronađeni izvori", + "No synchronizations found": "Nisu pronađene sinkronizacije", + "No templates found": "Nisu pronađeni predlošci", + "No templates have been created yet": "Još nisu stvoreni predlošci", + "No tools available. Tools can be registered by installed apps.": "Nema dostupnih alata. Alate mogu registrirati instalirane aplikacije.", + "No users in this organisation.": "Nema korisnika u ovoj organizaciji.", + "No valid magic-mapped register+schema combinations found": "Nisu pronađene valjane magic-mapped kombinacije registra+sheme", + "No views found. Create views first before configuring vectorization.": "Nisu pronađeni prikazi. Prvo stvorite prikaze prije konfiguriranja vektorizacije.", + "No views match your search": "Nijedan prikaz ne odgovara vašem pretraživanju", + "No webhooks found": "Nisu pronađeni webhookovi", + "No webhooks have been configured yet": "Još nisu konfigurirani webhookovi", + "No workflows found in this project. Create workflows in the n8n editor.": "U ovom projektu nisu pronađeni tijekovi rada. Stvorite tijekove rada u n8n uređivaču.", + "None": "Nijedan", + "Not Found": "Nije pronađeno", + "Not extracted": "Nije izvučeno", + "Not helpful": "Nije korisno", + "Not in use": "Nije u upotrebi", + "Note:": "Napomena:", + "Notifications": "Obavijesti", + "Number Constraints": "Ograničenja broja", + "Number of Sources": "Broj izvora", + "Number of chunks to vectorize in one API call (1-100).": "Broj dijelova za vektorizaciju u jednom API pozivu (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Broj dijelova za vektorizaciju u jednom API pozivu. Više = brže, ali više memorije. Preporučeno: 10-50.", + "Number of objects to process in each batch (1-5000).": "Broj objekata za obradu u svakoj seriji (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Broj objekata za vektorizaciju u jednom API pozivu. Više = brže, ali više memorije. Preporučeno: 10-50.", + "Numeric Range:": "Brojčani raspon:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Zbirka objekata", + "Object Configuration:": "Konfiguracija objekta:", + "Object Count Prediction": "Predviđanje broja objekata", + "Object Extraction Mode": "Način izvlačenja objekata", + "Object ID": "ID objekta", + "Object Management": "Upravljanje objektima", + "Object Text Extraction": "Izvlačenje teksta objekta", + "Object Vectorization": "Vektorizacija objekata", + "Object Warmup": "Zagrijavanje objekata", + "Object changes in owned objects": "Promjene objekta u vlastitim objektima", + "Object collection not configured": "Zbirka objekata nije konfigurirana", + "Object metadata incomplete for file upload": "Metapodaci objekta nepotpuni za prijenos datoteke", + "Object not found": "Objekt nije pronađen", + "Object permanently deleted": "Objekt trajno izbrisan", + "Object restored successfully": "Objekt uspješno vraćen", + "Object settings updated successfully": "Postavke objekta uspješno ažurirane", + "Object sources": "Izvori objekata", + "Object successfully deleted": "Objekt uspješno izbrisan", + "Object successfully modified": "Objekt uspješno izmijenjen", + "Object unlocked successfully": "Objekt uspješno otključan", + "Object vectorization configuration saved successfully": "Konfiguracija vektorizacije objekata uspješno spremljena", + "Object vectorized successfully": "Objekt uspješno vektoriziran", + "Object:": "Objekt:", + "Objects": "Objekti", + "Objects Analyzed": "Analizirani objekti", + "Objects Distribution": "Raspodjela objekata", + "Objects being analyzed": "Objekti koji se analiziraju", + "Objects by Register": "Objekti po registru", + "Objects by Schema": "Objekti po shemi", + "Objects deletion completed for {schema}": "Brisanje objekata dovršeno za {schema}", + "Objects deletion completed successfully": "Brisanje objekata uspješno dovršeno", + "Objects in schema": "Objekti u shemi", + "Objects successfully deleted": "Objekti uspješno izbrisani", + "Objects to Process:": "Objekti za obradu:", + "Objects to be analyzed": "Objekti koji će biti analizirani", + "Objects to be deleted": "Objekti koji će biti izbrisani", + "Objects to be validated": "Objekti koji će biti provjereni", + "Objects will be serialized as JSON text before vectorization": "Objekti će biti serijalizirani kao JSON tekst prije vektorizacije", + "Objects will be soft deleted and moved to the": "Objekti će biti meko izbrisani i premješteni u", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekti će biti meko izbrisani (označeni kao izbrisani, ali zadržani u bazi podataka). Mogu se kasnije vratiti ako je potrebno.", + "Old Value": "Stara vrijednost", + "Oldest Item (days)": "Najstarija stavka (dani)", + "Ollama Chat Settings": "Postavke Ollama razgovora", + "Ollama Embedding Configuration": "Konfiguracija Ollama ugrađivanja", + "Ollama URL": "Ollama URL", + "Only With Changes": "Samo s promjenama", + "Only members of selected groups can access this organisation": "Samo članovi odabranih grupa mogu pristupiti ovoj organizaciji", + "Open Nextcloud App Store": "Otvori Nextcloud trgovinu aplikacija", + "Open OpenRegister sidebar": "Otvori OpenRegister bočnu traku", + "Open Register Objects": "Otvori objekte registra", + "Open folder": "Otvori mapu", + "Open in OpenRegister": "Otvori u OpenRegister", + "Open n8n Editor": "Otvori n8n uređivač", + "OpenAI Chat Settings": "Postavke OpenAI razgovora", + "OpenAI Embedding Configuration": "Konfiguracija OpenAI ugrađivanja", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister postavke", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Nadzorne ploče i zakazani izvještaji koje definira operater. Svaka nadzorna ploča je prvorazredni objekt u `reports` registru; widgeti se deklariraju u `widgets` polju nadzorne ploče i prikazuju uživo iz agregacija / GraphQL.", + "Optimizing search performance...": "Optimiziranje izvedbe pretraživanja...", + "Optional URL-friendly identifier": "Neobavezni identifikator prilagođen URL-u", + "Optional description for this facet": "Neobavezni opis za ovu fasetu", + "Optional end date property": "Neobavezno svojstvo datuma završetka", + "Optional event description template": "Neobavezni predložak opisa događaja", + "Optional location property": "Neobavezno svojstvo lokacije", + "Optional webhook secret for signature verification": "Neobavezna webhook tajna za provjeru potpisa", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Neobavezno: Dodajte parametre upita za filtriranje referencirane sheme (npr. status=active&type=public)", + "Or": "Ili", + "Order": "Redoslijed", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Poredane BCP 47 jezične oznake. Prvi jezik je zadani za registar i pokreće Accept-Language zamjenu za prevodiva svojstva.", + "Organisation": "Organizacija", + "Organisation Configuration": "Konfiguracija organizacije", + "Organisation ID": "ID organizacije", + "Organisation Members": "Članovi organizacije", + "Organisation Statistics": "Statistika organizacije", + "Organisation created successfully": "Organizacija uspješno stvorena", + "Organisation membership changes": "Promjene članstva u organizaciji", + "Organisation name is required": "Naziv organizacije je obavezan", + "Organisation settings saved successfully": "Postavke organizacije uspješno spremljene", + "Organisation successfully created": "Organizacija uspješno stvorena", + "Organisation successfully updated": "Organizacija uspješno ažurirana", + "Organisational measures": "Organizacijske mjere", + "Organisations": "Organizacije", + "Organisatorische maatregelen": "Organizacijske mjere", + "Organization": "Organizacija", + "Organization ID (Optional)": "ID organizacije (neobavezno)", + "Organizations": "Organizacije", + "Orphaned Items": "Stavke bez roditelja", + "Other": "Ostalo", + "Output Mapping": "Mapiranje izlaza", + "Overview": "Pregled", + "Owner": "Vlasnik", + "Owner:": "Vlasnik:", + "PDF": "PDF", + "PII hits": "PII pogodci", + "PII rows": "PII redci", + "Page {current} of {total}": "Stranica {current} od {total}", + "Parallel Mode (Faster, more resource intensive)": "Paralelni način (brže, zahtjevnije za resurse)", + "Parallel:": "Paralelno:", + "Password": "Lozinka", + "Password changes are not supported by your authentication provider.": "Vaš davatelj autentifikacije ne podržava promjene lozinke.", + "Password for SOLR authentication (optional)": "Lozinka za SOLR autentifikaciju (neobavezno)", + "Password updated successfully": "Lozinka uspješno ažurirana", + "Path": "Putanja", + "Path in repository": "Putanja u repozitoriju", + "Path where the configuration file will be saved in the repository": "Putanja na kojoj će konfiguracijska datoteka biti spremljena u repozitoriju", + "Path where the register OAS file will be saved in the repository": "Putanja na kojoj će OAS datoteka registra biti spremljena u repozitoriju", + "Pattern (regex)": "Obrazac (regex)", + "Pattern Issue": "Problem s obrascem", + "Pattern constraint is missing": "Nedostaje ograničenje obrasca", + "Patterns:": "Obrasci:", + "Pending": "Na čekanju", + "Permanent Delete Mode": "Način trajnog brisanja", + "Permanently Delete": "Trajno izbriši", + "Permanently Delete ({count})": "Trajno izbriši ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Trajno izbriši svih {count} meko izbrisanih objekata. To se ne može poništiti!", + "Permanently delete already soft-deleted objects": "Trajno izbriši već meko izbrisane objekte", + "Permanently delete this conversation?": "Trajno izbrisati ovaj razgovor?", + "Permission Matrix": "Matrica dozvola", + "Person": "Osoba", + "Personal Data": "Osobni podaci", + "Personal Data Export": "Izvoz osobnih podataka", + "Persons": "Osobe", + "Phone": "Telefon", + "Phone Numbers": "Telefonski brojevi", + "Photos": "Fotografije", + "Please create an agent in the": "Stvorite agenta u", + "Please select a user": "Odaberite korisnika", + "Please select an agent to continue": "Odaberite agenta za nastavak", + "Please select an organisation": "Odaberite organizaciju", + "Please select at least one item to import": "Odaberite barem jednu stavku za uvoz", + "Please select registers or schemas first": "Prvo odaberite registre ili sheme", + "Please select which register and schema to use for the new object": "Odaberite koji registar i shemu koristiti za novi objekt", + "Please try again later.": "Pokušajte ponovno kasnije.", + "Please wait while we fetch your agents.": "Pričekajte dok dohvaćamo vaše agente.", + "Please wait while we fetch your applications.": "Pričekajte dok dohvaćamo vaše aplikacije.", + "Please wait while we fetch your configurations.": "Pričekajte dok dohvaćamo vaše konfiguracije.", + "Please wait while we fetch your deleted items.": "Pričekajte dok dohvaćamo vaše izbrisane stavke.", + "Please wait while we fetch your sources.": "Pričekajte dok dohvaćamo vaše izvore.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Pričekajte dok trajno brišemo SOLR zbirku. To može potrajati nekoliko trenutaka.", + "Polls": "Ankete", + "Popular Search Terms": "Popularni pojmovi pretraživanja", + "Port": "Priključak", + "Portabiliteit (Art 20)": "Prenosivost (čl. 20)", + "Preferences saved": "Preferencije spremljene", + "Prerequisites": "Preduvjeti", + "Press Enter to send, Shift+Enter for new line": "Pritisnite Enter za slanje, Shift+Enter za novi redak", + "Press enter to send, shift+enter for new line": "Pritisnite enter za slanje, shift+enter za novi redak", + "Prettify": "Uljepšaj", + "Preview erasure": "Pretpregled brisanja", + "Preview:": "Pretpregled:", + "Previous": "Prethodno", + "Private": "Privatno", + "Private Agent (Default)": "Privatni agent (zadano)", + "Private agents": "Privatni agenti", + "Process Name (optional)": "Naziv procesa (neobavezno)", + "Processed {indexed} files, {failed} failed": "Obrađeno {indexed} datoteka, {failed} neuspješno", + "Processes chunks in batches with simulated parallelism.": "Obrađuje dijelove u serijama sa simuliranim paralelizmom.", + "Processes file chunks sequentially (safest).": "Obrađuje dijelove datoteke sekvencijalno (najsigurnije).", + "Processes objects in chunks with simulated parallelism.": "Obrađuje objekte u dijelovima sa simuliranim paralelizmom.", + "Processes objects sequentially (safest).": "Obrađuje objekte sekvencijalno (najsigurnije).", + "Processing": "Obrada", + "Processing Limits": "Ograničenja obrade", + "Processing...": "Obrada...", + "Project initialized successfully": "Projekt uspješno inicijaliziran", + "Project not found. Please initialize first.": "Projekt nije pronađen. Prvo inicijalizirajte.", + "Properties": "Svojstva", + "Property Behaviors": "Ponašanja svojstva", + "Property Configuration:": "Konfiguracija svojstva:", + "Property Filters": "Filtri svojstva", + "Property Title": "Naslov svojstva", + "Property Type": "Vrsta svojstva", + "Property can be improved": "Svojstvo se može poboljšati", + "Property could not be added": "Svojstvo se nije moglo dodati", + "Property could not be updated": "Svojstvo se nije moglo ažurirati", + "Property name of inversed relation": "Naziv svojstva obrnute veze", + "Property successfully added": "Svojstvo uspješno dodano", + "Property successfully updated": "Svojstvo uspješno ažurirano", + "Protected": "Zaštićeno", + "Provider": "Davatelj", + "Provider is required for testing": "Davatelj je obavezan za testiranje", + "Public": "Javno", + "Public View": "Javni prikaz", + "Public views can be accessed by anyone in the system": "Javnim prikazima može pristupiti bilo tko u sustavu", + "Publish": "Objavi", + "Publish Configuration to GitHub": "Objavi konfiguraciju na GitHub", + "Publish OAS": "Objavi OAS", + "Publish Objects": "Objavi objekte", + "Publish Register OAS to GitHub": "Objavi OAS registra na GitHub", + "Publish all objects in this schema": "Objavi sve objekte u ovoj shemi", + "Publish objects to make them publicly available": "Objavi objekte kako bi bili javno dostupni", + "Published": "Objavljeno", + "Published:": "Objavljeno:", + "Publishing...": "Objavljivanje...", + "Purge": "Očisti", + "Purge Date": "Datum čišćenja", + "Purge {count} object from database": "Očisti {count} objekt iz baze podataka", + "Purge {count} objects from database": "Očisti {count} objekata iz baze podataka", + "Purpose limitation *": "Ograničenje svrhe *", + "Push Notifications": "Push obavijesti", + "Query Complexity Distribution": "Raspodjela složenosti upita", + "Query Help": "Pomoć za upite", + "Query parameter is required": "Parametar upita je obavezan", + "Quota": "Kvota", + "RAG": "RAG", + "RAG Configuration": "RAG konfiguracija", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC postavke uspješno ažurirane", + "Rate limits cleared successfully": "Ograničenja brzine uspješno obrisana", + "Raw Changes Data": "Neobrađeni podaci o promjenama", + "Re-vectorize on object update": "Ponovno vektoriziraj pri ažuriranju objekta", + "Read": "Čitanje", + "Real-time push notification status via notify_push": "Status push obavijesti u stvarnom vremenu putem notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push obavijesti u stvarnom vremenu su aktivne. Povezani klijenti primaju trenutna ažuriranja kada se objekti stvore, ažuriraju ili izbrišu.", + "Realtime push active": "Push u stvarnom vremenu aktivan", + "Realtime push not available — the notify_push app is not installed": "Push u stvarnom vremenu nije dostupan — aplikacija notify_push nije instalirana", + "Reason": "Razlog", + "Reason (optional)": "Razlog (neobavezno)", + "Rebase operation completed successfully": "Operacija rebase uspješno dovršena", + "Rebase operation failed: {error}": "Operacija rebase nije uspjela: {error}", + "Rechtsgrond": "Pravna osnova", + "Recommendations:": "Preporuke:", + "Recommended Type:": "Preporučena vrsta:", + "Recommended: 5 sources": "Preporučeno: 5 izvora", + "Refresh": "Osvježi", + "Refresh Data": "Osvježi podatke", + "Refresh Stats": "Osvježi statistiku", + "Refresh Workflows": "Osvježi tijekove rada", + "Refresh dashboard": "Osvježi nadzornu ploču", + "Refresh database info": "Osvježi informacije o bazi podataka", + "Register": "Registar", + "Register #{id}": "Registar #{id}", + "Register / Schema": "Registar / Shema", + "Register ID": "ID registra", + "Register Objects": "Objekti registra", + "Register Overview": "Pregled registra", + "Register Statistics": "Statistika registra", + "Register Totals": "Ukupni iznosi registra", + "Register and schema are required": "Registar i shema su obavezni", + "Register annotation": "Anotacija registra", + "Register depublished successfully": "Registar uspješno povučen iz objave", + "Register not found": "Registar nije pronađen", + "Register objects": "Objekti registra", + "Register objects deletion completed successfully": "Brisanje objekata registra uspješno dovršeno", + "Register published successfully": "Registar uspješno objavljen", + "Register/Schema Usage": "Upotreba registra/sheme", + "Register:": "Registar:", + "Registers": "Registri", + "Reindex": "Ponovno indeksiraj", + "Reindex completed": "Ponovno indeksiranje dovršeno", + "Reindex completed successfully": "Ponovno indeksiranje uspješno dovršeno", + "Reindex completed! Processed {count} objects in {duration}s": "Ponovno indeksiranje dovršeno! Obrađeno {count} objekata za {duration}s", + "Reindex failed": "Ponovno indeksiranje nije uspjelo", + "Reindex failed: %s": "Ponovno indeksiranje nije uspjelo: %s", + "Reject": "Odbij", + "Rejected": "Odbijeno", + "Related Cases": "Povezani predmeti", + "Relations": "Veze", + "Remote Version": "Udaljena verzija", + "Remove": "Ukloni", + "Remove Deck card": "Ukloni Deck karticu", + "Remove avatar": "Ukloni avatar", + "Remove contact": "Ukloni kontakt", + "Remove filter": "Ukloni filtar", + "Remove from favorites": "Ukloni iz favorita", + "Remove from list": "Ukloni s popisa", + "Remove group": "Ukloni grupu", + "Remove link": "Ukloni vezu", + "Remove link to {name}": "Ukloni vezu na {name}", + "Remove link to {name}?": "Ukloniti vezu na {name}?", + "Remove link to {title}": "Ukloni vezu na {title}", + "Remove oneOf entry": "Ukloni oneOf unos", + "Remove property after write-back": "Ukloni svojstvo nakon povratnog upisa", + "Remove {title}": "Ukloni {title}", + "Remove {userId} from this organisation?": "Ukloniti {userId} iz ove organizacije?", + "Removed": "Uklonjeno", + "Removed from favorites": "Uklonjeno iz favorita", + "Removing schema {schema}...": "Uklanjanje sheme {schema}...", + "Rename Conversation": "Preimenuj razgovor", + "Rename conversation": "Preimenuj razgovor", + "Replicas": "Replike", + "Reports": "Izvještaji", + "Repository": "Repozitorij", + "Request Body": "Tijelo zahtjeva", + "Request Data": "Podaci zahtjeva", + "Request Details": "Pojedinosti zahtjeva", + "Request Quota (per day)": "Kvota zahtjeva (po danu)", + "Request account deactivation": "Zatraži deaktivaciju računa", + "Request account deactivation. This will notify administrators for review.": "Zatraži deaktivaciju računa. Time će administratori biti obaviješteni radi pregleda.", + "Request body must be JSON with a \"query\" field": "Tijelo zahtjeva mora biti JSON s poljem \"query\"", + "Request timeout in seconds": "Istek zahtjeva u sekundama", + "Requested at": "Zatraženo u", + "Required": "Obavezno", + "Required Token Scopes": "Obavezni opsezi tokena", + "Required field": "Obavezno polje", + "Required status is inconsistent": "Status obaveznosti je nedosljedan", + "Requires Dolphin AI for OCR text extraction": "Zahtijeva Dolphin AI za OCR izvlačenje teksta", + "Rerun Search": "Ponovno pokreni pretraživanje", + "Rerunning search: {searchTerm}": "Ponovno pokretanje pretraživanja: {searchTerm}", + "Reset Changes": "Poništi promjene", + "Reset Filters": "Poništi filtre", + "Resource Quotas": "Kvote resursa", + "Response Body": "Tijelo odgovora", + "Response Details": "Pojedinosti odgovora", + "Response Time:": "Vrijeme odgovora:", + "Restore": "Vrati", + "Restore conversation": "Vrati razgovor", + "Restore or permanently delete items": "Vrati ili trajno izbriši stavke", + "Restore {count} object": "Vrati {count} objekt", + "Restore {count} objects": "Vrati {count} objekata", + "Result Count Range": "Raspon broja rezultata", + "Results": "Rezultati", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Rok čuvanja (ISO-8601 trajanje, npr. P10Y, P30D)", + "Retention settings updated successfully": "Postavke čuvanja uspješno ažurirane", + "Retrieve all objects for this schema": "Dohvati sve objekte za ovu shemu", + "Retry": "Pokušaj ponovno", + "Retry Failed Extractions": "Ponovi neuspjela izvlačenja", + "Retry Policy": "Pravila ponovnog pokušaja", + "Retry completed": "Ponovni pokušaj dovršen", + "Retry upload": "Ponovi prijenos", + "Retrying...": "Ponovni pokušaj...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite izbrisati klikom na gumb za uklanjanje.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite uključiti.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite trajno izbrisati klikom na gumb za uklanjanje. Ova se radnja ne može poništiti.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite vratiti klikom na gumb za uklanjanje. Objekti će biti vraćeni na svoju izvornu lokaciju.", + "Revoke": "Opozovi", + "Right": "Desno", + "Risk Level": "Razina rizika", + "Risk level": "Razina rizika", + "Roles and Permissions": "Uloge i dozvole", + "Roles updated successfully": "Uloge uspješno ažurirane", + "Run compliance scan": "Pokreni skeniranje usklađenosti", + "SOLR Connection Settings": "Postavke SOLR veze", + "SOLR Index Warmup": "Zagrijavanje SOLR indeksa", + "SOLR Version:": "SOLR verzija:", + "SOLR actions menu": "Izbornik SOLR radnji", + "SOLR base path (usually /solr)": "SOLR osnovna putanja (obično /solr)", + "SOLR collection deleted successfully": "SOLR zbirka uspješno izbrisana", + "SOLR connection test failed: {error}": "Test SOLR veze nije uspio: {error}", + "SOLR connection test successful!": "Test SOLR veze uspješan!", + "SOLR index warmup completed successfully": "Zagrijavanje SOLR indeksa uspješno dovršeno", + "SOLR is disabled": "SOLR je onemogućen", + "SOLR is not available or not configured": "SOLR nije dostupan ili nije konfiguriran", + "SOLR logging disabled": "SOLR zapisivanje onemogućeno", + "SOLR logging enabled": "SOLR zapisivanje omogućeno", + "SOLR search disabled": "SOLR pretraživanje onemogućeno", + "SOLR search enabled": "SOLR pretraživanje omogućeno", + "SOLR server hostname or IP address": "SOLR naziv poslužitelja ili IP adresa", + "SOLR server port number (optional, defaults to 8983)": "Broj priključka SOLR poslužitelja (neobavezno, zadano 8983)", + "SOLR settings updated successfully": "SOLR postavke uspješno ažurirane", + "SOLR setup completed successfully": "SOLR postavljanje uspješno dovršeno", + "SOLR setup completed successfully!": "SOLR postavljanje uspješno dovršeno!", + "SOLR setup error: %s": "Pogreška SOLR postavljanja: %s", + "SOLR setup failed": "SOLR postavljanje nije uspjelo", + "SOLR setup failed - check logs": "SOLR postavljanje nije uspjelo - provjerite zapisnike", + "SOLR setup failed. Please check the configuration and try again.": "SOLR postavljanje nije uspjelo. Provjerite konfiguraciju i pokušajte ponovno.", + "SOLR setup failed: {error}": "SOLR postavljanje nije uspjelo: {error}", + "SOLR warmup failed: {error}": "Zagrijavanje SOLR nije uspjelo: {error}", + "Sample Values:": "Uzorci vrijednosti:", + "Save": "Spremi", + "Save Configuration": "Spremi konfiguraciju", + "Save Connection Settings": "Spremi postavke veze", + "Save Roles": "Spremi uloge", + "Save Settings": "Spremi postavke", + "Save changes": "Spremi promjene", + "Save current search as view": "Spremi trenutno pretraživanje kao prikaz", + "Save labels": "Spremi oznake", + "Save tags for {name}": "Spremi oznake za {name}", + "Save the organisation first to manage users.": "Prvo spremite organizaciju za upravljanje korisnicima.", + "Saved Views": "Spremljeni prikazi", + "Saving...": "Spremanje...", + "Scheduled": "Zakazano", + "Schema": "Shema", + "Schema ID": "ID sheme", + "Schema Name": "Naziv sheme", + "Schema Statistics": "Statistika sheme", + "Schema annotation": "Anotacija sheme", + "Schema configuration updated successfully for {schema}": "Konfiguracija sheme uspješno ažurirana za {schema}", + "Schema depublished successfully": "Shema uspješno povučena iz objave", + "Schema objects deletion completed successfully": "Brisanje objekata sheme uspješno dovršeno", + "Schema published successfully": "Shema uspješno objavljena", + "Schema reference ($ref)": "Referenca sheme ($ref)", + "Schema successfully created": "Shema uspješno stvorena", + "Schema successfully updated": "Shema uspješno ažurirana", + "Schema {schema} removed successfully": "Shema {schema} uspješno uklonjena", + "Schema:": "Shema:", + "Schemas": "Sheme", + "Scheme": "Shema", + "Search": "Pretraži", + "Search / Views": "Pretraživanje / Prikazi", + "Search / views": "Pretraživanje / prikazi", + "Search Activity": "Aktivnost pretraživanja", + "Search Analytics": "Analitika pretraživanja", + "Search Configuration": "Konfiguracija pretraživanja", + "Search Entities": "Pretraži entitete", + "Search Files": "Pretraži datoteke", + "Search GitHub": "Pretraži GitHub", + "Search GitLab": "Pretraži GitLab", + "Search Method": "Metoda pretraživanja", + "Search Mode": "Način pretraživanja", + "Search Query": "Upit pretraživanja", + "Search Results": "Rezultati pretraživanja", + "Search Term": "Pojam pretraživanja", + "Search Trail Details": "Pojedinosti traga pretraživanja", + "Search Trail Management": "Upravljanje tragom pretraživanja", + "Search Trail Statistics": "Statistika traga pretraživanja", + "Search Trails": "Tragovi pretraživanja", + "Search Views": "Prikazi pretraživanja", + "Search Webhooks": "Pretraži webhookove", + "Search agents...": "Pretraži agente...", + "Search applications...": "Pretraži aplikacije...", + "Search by file name or path": "Pretraži po nazivu datoteke ili putanji", + "Search by name or URL": "Pretraži po nazivu ili URL-u", + "Search by title or UUID...": "Pretraži po naslovu ili UUID...", + "Search by value": "Pretraži po vrijednosti", + "Search configurations": "Pretraži konfiguracije", + "Search data sources...": "Pretraži izvore podataka...", + "Search groups...": "Pretraži grupe...", + "Search in Database Objects": "Pretraži objekte u bazi podataka", + "Search in Files": "Pretraži datoteke", + "Search in Objects": "Pretraži objekte", + "Search objects": "Pretraži objekte", + "Search objects...": "Pretraži objekte...", + "Search property names...": "Pretraži nazive svojstava...", + "Search registers...": "Pretraži registre...", + "Search schemas...": "Pretraži sheme...", + "Search trail deletion not implemented yet": "Brisanje traga pretraživanja još nije implementirano", + "Search views...": "Pretraži prikaze...", + "Search {name}...": "Pretraži {name}...", + "Searching...": "Pretraživanje...", + "Secret": "Tajna", + "Secret key for HMAC signature generation (optional)": "Tajni ključ za generiranje HMAC potpisa (neobavezno)", + "Security": "Sigurnost", + "See organisations as a table": "Prikaži organizacije kao tablicu", + "See organisations as cards": "Prikaži organizacije kao kartice", + "Select AI Agent": "Odaberi AI agenta", + "Select Active Organisation": "Odaberi aktivnu organizaciju", + "Select All": "Odaberi sve", + "Select ConfigSet": "Odaberite ConfigSet", + "Select File Types to Vectorize:": "Odaberite vrste datoteka za vektorizaciju:", + "Select HTTP method": "Odaberite HTTP metodu", + "Select Register and Schema": "Odaberite Registar i Shemu", + "Select Views to Vectorize:": "Odaberite prikaze za vektorizaciju:", + "Select a Nextcloud group to add": "Odaberite Nextcloud grupu za dodavanje", + "Select a branch": "Odaberite granu", + "Select a date property": "Odaberite svojstvo datuma", + "Select a model or type a custom model name": "Odaberite model ili upišite prilagođeni naziv modela", + "Select a register": "Odaberite registar", + "Select a register...": "Odaberite registar...", + "Select a repository": "Odaberite repozitorij", + "Select a repository you have write access to": "Odaberite repozitorij na koji imate pravo pisanja", + "Select a schema": "Odaberite shemu", + "Select a schema...": "Odaberite shemu...", + "Select a user": "Odaberite korisnika", + "Select agent type": "Odaberite vrstu agenta", + "Select all entities": "Odaberite sve entitete", + "Select an AI Agent": "Odaberite AI agenta", + "Select an AI agent to begin chatting with your data.": "Odaberite AI agenta za početak razgovora s vašim podacima.", + "Select an email to see linked objects": "Odaberite e-poštu za prikaz povezanih objekata", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Odaberite organizaciju i korisnika da biste ih dodali kao člana. Pretražite organizacije po nazivu.", + "Select backend": "Odaberite pozadinski sustav", + "Select chat model": "Odaberite model razgovora", + "Select collection for files": "Odaberite kolekciju za datoteke", + "Select collection for objects": "Odaberite kolekciju za objekte", + "Select configuration type...": "Odaberite vrstu konfiguracije...", + "Select default organisation": "Odaberite zadanu organizaciju", + "Select entity {value}": "Odaberite entitet {value}", + "Select event to listen to...": "Odaberite događaj koji će se osluškivati...", + "Select fields to display": "Odaberite polja za prikaz", + "Select files with the correct extension": "Odaberite datoteke s ispravnom ekstenzijom", + "Select group": "Odaberite grupu", + "Select groups (optional)": "Odaberite grupe (neobavezno)", + "Select groups to notify...": "Odaberite grupe za obavještavanje...", + "Select groups with access to this agent": "Odaberite grupe s pristupom ovom agentu", + "Select groups...": "Odaberite grupe...", + "Select model": "Odaberite model", + "Select one or more registers": "Odaberite jedan ili više registara", + "Select one or more schemas": "Odaberite jednu ili više shema", + "Select options...": "Odaberite opcije...", + "Select or create labels, or select \"No label\" to add files": "Odaberite ili stvorite oznake ili odaberite \"Bez oznake\" za dodavanje datoteka", + "Select owner application (optional)...": "Odaberite vlasničku aplikaciju (neobavezno)...", + "Select period": "Odaberite razdoblje", + "Select property to send as payload": "Odaberite svojstvo za slanje kao korisni teret", + "Select provider": "Odaberite pružatelja usluge", + "Select registers and schemas to save a view": "Odaberite registre i sheme za spremanje prikaza", + "Select retry policy": "Odaberite pravilo ponovnog pokušaja", + "Select role": "Odaberite ulogu", + "Select schemas (empty = all schemas)": "Odaberite sheme (prazno = sve sheme)", + "Select schemas to warm up": "Odaberite sheme za zagrijavanje", + "Select search mode": "Odaberite način pretraživanja", + "Select source type...": "Odaberite vrstu izvora...", + "Select target type": "Odaberite ciljnu vrstu", + "Select the branch to publish to": "Odaberite granu za objavljivanje", + "Select the event this webhook should listen to": "Odaberite događaj koji bi ovaj webhook trebao osluškivati", + "Select views to filter data (optional)": "Odaberite prikaze za filtriranje podataka (neobavezno)", + "Select views to limit which data the agent can access": "Odaberite prikaze za ograničavanje podataka kojima agent može pristupiti", + "Select views to vectorize:": "Odaberite prikaze za vektorizaciju:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Odaberite koje su Nextcloud grupe dostupne za ovu organizaciju. Korisnici u tim grupama imat će pristup resursima organizacije.", + "Select which data views the AI can search": "Odaberite koje prikaze podataka AI može pretraživati", + "Select which property from the event should be used as the webhook payload data": "Odaberite koje svojstvo iz događaja treba koristiti kao podatke korisnog tereta webhooka", + "Select which tools the AI can use to perform actions": "Odaberite koje alate AI može koristiti za izvođenje radnji", + "Select {fieldName} values": "Odaberite vrijednosti {fieldName}", + "Selected": "Odabrano", + "Selected Groups": "Odabrane grupe", + "Selected Objects ({count})": "Odabrani objekti ({count})", + "Selected audit trails deleted successfully": "Odabrani revizijski tragovi uspješno su izbrisani", + "Selected search trails deleted successfully": "Odabrani tragovi pretraživanja uspješno su izbrisani", + "Selected users can access this view": "Odabrani korisnici mogu pristupiti ovom prikazu", + "Send additional feedback": "Pošaljite dodatne povratne informacije", + "Send as CloudEvent": "Pošalji kao CloudEvent", + "Send as cloudevent": "Pošalji kao cloudevent", + "Sensitive PII": "Osjetljivi PII", + "Serial Mode (Safer, slower)": "Serijski način rada (sigurniji, sporiji)", + "Serial:": "Serijski:", + "Server Configuration": "Konfiguracija poslužitelja", + "Server Information": "Informacije o poslužitelju", + "Set as Active": "Postavi kao aktivno", + "Set as active organisation": "Postavi kao aktivnu organizaciju", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Postavite ograničenja za korištenje API-ja i potrošnju tokena. Koristite 0 za neograničene resurse.", + "Settings": "Postavke", + "Settings updated successfully": "Postavke su uspješno ažurirane", + "Shards": "Krhotine", + "Share": "Dijeli", + "Share with Groups": "Dijeli s grupama", + "Share with Users": "Dijeli s korisnicima", + "Show": "Prikaži", + "Show Filters": "Prikaži filtre", + "Show less": "Prikaži manje", + "Show only entries with changes": "Prikaži samo unose s promjenama", + "Showing {count} dashboard(s)": "Prikazuje se {count} nadzornih ploča", + "Showing {count} processing activities": "Prikazuje se {count} aktivnosti obrade", + "Showing {showing} of {total} agents": "Prikazuje se {showing} od {total} agenata", + "Showing {showing} of {total} applications": "Prikazuje se {showing} od {total} aplikacija", + "Showing {showing} of {total} audit trail entries": "Prikazuje se {showing} od {total} unosa revizijskog traga", + "Showing {showing} of {total} configurations": "Prikazuje se {showing} od {total} konfiguracija", + "Showing {showing} of {total} deleted items": "Prikazuje se {showing} od {total} izbrisanih stavki", + "Showing {showing} of {total} entities": "Prikazuje se {showing} od {total} entiteta", + "Showing {showing} of {total} files": "Prikazuje se {showing} od {total} datoteka", + "Showing {showing} of {total} log entries": "Prikazuje se {showing} od {total} unosa zapisnika", + "Showing {showing} of {total} search trail entries": "Prikazuje se {showing} od {total} unosa traga pretraživanja", + "Showing {showing} of {total} sources": "Prikazuje se {showing} od {total} izvora", + "Showing {showing} of {total} templates": "Prikazuje se {showing} od {total} predložaka", + "Showing {showing} of {total} webhooks": "Prikazuje se {showing} od {total} webhookova", + "Showing {shown} of {total} organisations": "Prikazuje se {shown} od {total} organizacija", + "Simple": "Jednostavno", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednostavni upiti: osnovna pretraživanja teksta s minimalnim parametrima (npr. jedan pojam za pretraživanje, bez naprednih filtara)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednostavni upiti: osnovna pretraživanja teksta s minimalnim parametrima (npr. jedan pojam za pretraživanje, bez naprednih filtara)", + "Size": "Veličina", + "Slug": "Slug", + "Soft Delete Mode": "Način meke brisanja", + "Soft Deleted Items": "Meko izbrisane stavke", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Meko izbriši sve objekte za ovu shemu ({active} aktivnih, {deleted} već izbrisanih)", + "SolrCloud mode enabled": "SolrCloud način rada omogućen", + "Something went wrong while adding files": "Nešto je pošlo po zlu prilikom dodavanja datoteka", + "Source": "Izvor", + "Source successfully updated": "Izvor je uspješno ažuriran", + "Sources": "Izvori", + "Sources:": "Izvori:", + "Special Rights": "Posebna prava", + "Standalone SOLR mode": "Samostalni SOLR način rada", + "Start Conversation": "Započni razgovor", + "Start Date Field": "Polje datuma početka", + "Start Vectorization": "Pokreni vektorizaciju", + "Start a conversation": "Započni razgovor", + "Starting names cache warmup...": "Pokretanje zagrijavanja predmemorije naziva...", + "Starting validation for {schema}...": "Pokretanje provjere valjanosti za {schema}...", + "Starting {action} for {schema}...": "Pokretanje {action} za {schema}...", + "Starting...": "Pokretanje...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika i mjerni podaci", + "Statistics refreshed": "Statistika je osvježena", + "Stats calculation not yet implemented": "Izračun statistike još nije implementiran", + "Status": "Status", + "Status Code": "Statusni kod", + "Step": "Korak", + "Storage": "Pohrana", + "Storage Quota (MB)": "Kvota pohrane (MB)", + "String Constraints": "Ograničenja niza", + "Subject identifier (email, BSN, name, etc.)": "Identifikator subjekta (e-pošta, BSN, ime itd.)", + "Subscribe": "Pretplati se", + "Subscribed": "Pretplaćeno", + "Success": "Uspjeh", + "Success Rate": "Stopa uspjeha", + "Success Status": "Status uspjeha", + "Successful": "Uspješno", + "Successfully added user to organisation: {name}": "Korisnik je uspješno dodan u organizaciju: {name}", + "Successfully cleared {count} audit trails": "Uspješno je obrisano {count} revizijskih tragova", + "Successfully cleared {count} blob storage objects": "Uspješno je obrisano {count} objekata blob pohrane", + "Successfully cleared {count} search trails": "Uspješno je obrisano {count} tragova pretraživanja", + "Successfully created {count} SOLR fields": "Uspješno je stvoreno {count} SOLR polja", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Uspješno je izbrisano {count} ugrađivanja. Ponovno vektorizirajte svoje podatke.", + "Successfully deleted {count} objects for {schema}": "Uspješno je izbrisano {count} objekata za {schema}", + "Successfully fixed {count} SOLR field configurations": "Uspješno je popravljeno {count} konfiguracija SOLR polja", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Uspješno uvezeno: {registers} registara, {schemas} shema, {objects} objekata", + "Successfully joined organisation": "Uspješno ste se pridružili organizaciji", + "Successfully permanently deleted {count} objects": "Uspješno je trajno izbrisano {count} objekata", + "Successfully processed {count} files!": "Uspješno je obrađeno {count} datoteka!", + "Successfully restored {count} object": "Uspješno je obnovljen {count} objekt", + "Successfully restored {count} objects": "Uspješno je obnovljeno {count} objekata", + "Successfully saved configuration for {count} facets!": "Uspješno je spremljena konfiguracija za {count} faseta!", + "Supported File Types": "Podržane vrste datoteka", + "Switch Active Organisation": "Promijeni aktivnu organizaciju", + "Switch Organisation": "Promijeni organizaciju", + "Sync Table": "Sinkroniziraj tablicu", + "Synchronization Settings": "Postavke sinkronizacije", + "Syncing magic table for {schema}...": "Sinkronizacija magične tablice za {schema}...", + "Syncs": "Sinkronizacije", + "System Default": "Zadano sustavom", + "System Prompt": "Sistemski upit", + "System announcements": "Obavijesti sustava", + "Table": "Tablica", + "Target ID": "Ciljni ID", + "Target Type*": "Ciljna vrsta*", + "Technical Description": "Tehnički opis", + "Technical Details": "Tehnički detalji", + "Technical description for developers and administrators": "Tehnički opis za razvojne programere i administratore", + "Technical measures": "Tehničke mjere", + "Technische maatregelen": "Tehničke mjere", + "Temperature": "Temperatura", + "Templates": "Predlošci", + "Test": "Test", + "Test Chat": "Testni razgovor", + "Test Connection": "Testiraj vezu", + "Test Embedding": "Testiraj ugrađivanje", + "Test endpoint executed successfully": "Testna krajnja točka uspješno je izvršena", + "Test webhook delivered successfully": "Testni webhook uspješno je isporučen", + "Test webhook delivery failed": "Isporuka testnog webhooka nije uspjela", + "Test webhook sent successfully": "Testni webhook uspješno je poslan", + "Testing...": "Testiranje...", + "Text Chunk #{id}": "Tekstualni isječak #{id}", + "Text Extraction": "Izvlačenje teksta", + "Text Extraction Engine": "Mehanizam za izvlačenje teksta", + "Text chunks": "Tekstualni isječci", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Tekstualni isječci generiraju se tijekom izvlačenja datoteka i pohranjuju u bazu podataka. Vektorizacija čita te unaprijed isječene datoteke i pretvara ih u ugrađivanja.", + "Text extracted successfully": "Tekst je uspješno izvučen", + "Text extraction disabled": "Izvlačenje teksta onemogućeno", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplikacija Nextcloud Calendar nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplikacija Nextcloud Contacts nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplikacija Nextcloud Deck nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplikacija Nextcloud Mail nije instalirana ili omogućena na ovom poslužitelju.", + "The URL where webhook events will be sent": "URL na koji će se slati webhook događaji", + "The requested conversation does not exist": "Traženi razgovor ne postoji", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Tražena nadzorna ploča nije se mogla učitati. Provjerite URL ili odaberite drugu nadzornu ploču s popisa.", + "The saved view and all its search configuration": "Spremljeni prikaz i sva njegova konfiguracija pretraživanja", + "There are no audit trail entries matching your current filters.": "Nema unosa revizijskog traga koji odgovaraju vašim trenutnim filtrima.", + "There are no deleted items matching your current filters.": "Nema izbrisanih stavki koje odgovaraju vašim trenutnim filtrima.", + "There are no search trail entries matching your current filters.": "Nema unosa traga pretraživanja koji odgovaraju vašim trenutnim filtrima.", + "There are no webhook log entries matching your filters.": "Nema unosa zapisnika webhooka koji odgovaraju vašim filtrima.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Bit će zadržani u skladu s konfiguriranim rokom čuvanja njihove sheme i automatski trajno izbrisani kada rok čuvanja istekne. Rok čuvanja moguće je konfigurirati po shemi i nalazi se u postavkama sheme.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Bit će zadržani u skladu s konfiguriranim rokom čuvanja njihove sheme, a zatim trajno uklonjeni.", + "This action cannot be undone.": "Ova se radnja ne može poništiti.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Ova se radnja ne može poništiti. Provjerite da nijedna kolekcija ne koristi ovaj ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Ova se radnja ne može poništiti. Provjerite da nijedna kolekcija ne koristi ovaj configset.", + "This action will submit a deactivation request to your administrators.": "Ova će radnja poslati zahtjev za deaktivaciju vašim administratorima.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Ovaj agent nema konfigurabilne prikaze ili alate, ali još uvijek možete prilagoditi RAG postavke u nastavku.", + "This analysis may take some time": "Ova analiza može potrajati", + "This audit trail entry does not contain any change information.": "Ovaj unos revizijskog traga ne sadrži nikakve informacije o promjenama.", + "This configuration has no data defined.": "Ova konfiguracija nema definirane podatke.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ova je krajnja točka zastarjela. Umjesto toga koristite krajnje točke temeljene na isječcima.", + "This entity has no relations to objects or files": "Ovaj entitet nema veza s objektima ili datotekama", + "This is a test webhook from OpenRegister": "Ovo je testni webhook iz OpenRegister", + "This is not valid JSON": "Ovo nije valjani JSON", + "This key already exists on this schema": "Ovaj ključ već postoji u ovoj shemi", + "This schema must use Magic Table configuration to sync": "Ova shema mora koristiti konfiguraciju Magične tablice za sinkronizaciju", + "This schema must use magic table configuration to sync": "Ova shema mora koristiti konfiguraciju magične tablice za sinkronizaciju", + "This source has no associated registers.": "Ovaj izvor nema povezanih registara.", + "This token will only be shown once. Copy it now.": "Ovaj će se token prikazati samo jednom. Kopirajte ga sada.", + "This will copy the _default ConfigSet with the new name": "Ovo će kopirati _default ConfigSet s novim nazivom", + "This will copy the _default configset with the new name": "Ovo će kopirati _default configset s novim nazivom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Ovo će trajno izbrisati SVA ugrađivanja (vektore) iz baze podataka. Morat ćete ponovno vektorizirati sve objekte i datoteke. Ova se radnja ne može poništiti.\n\nJeste li sigurni da želite nastaviti?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Ovo će trajno izbrisati SVA ugrađivanja (vektore) iz baze podataka. Morat ćete ponovno vektorizirati sve objekte i datoteke. Ova se radnja ne može poništiti.\\n\\nJeste li sigurni da želite nastaviti?", + "This will permanently delete:": "Ovo će trajno izbrisati:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Ovo će trajno ukloniti {count} već meko izbrisanih objekata iz baze podataka. Ova se radnja ne može poništiti.", + "Timeout (seconds)": "Vremensko ograničenje (sekunde)", + "Timestamp": "Vremenska oznaka", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Savjet: Omogućite samo prikaze kojima je potrebno semantičko pretraživanje kako biste minimizirali troškove ugrađivanja. Jednostavne tablice za pretraživanje rijetko trebaju vektorizaciju.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Savjet: omogućite samo prikaze kojima je potrebno semantičko pretraživanje kako biste minimizirali troškove ugrađivanja. Jednostavne tablice za pretraživanje rijetko trebaju vektorizaciju.", + "Title": "Naslov", + "Title *": "Naslov *", + "Title Template": "Predložak naslova", + "Title*": "Naslov*", + "To (e.g. -1 year or 2025-12-31)": "Do (npr. -1 godina ili 2025-12-31)", + "To Date": "Do datuma", + "To add files larger than or equal to 512MB, go to the": "Za dodavanje datoteka većih ili jednakih 512 MB, idite na", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Za prilagodbu veličine isječka ili strategije, idite na Konfiguracija datoteke → Ograničenja obrade.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Za prilagodbu veličine isječka ili strategije, idite na konfiguracija datoteke → ograničenja obrade.", + "To change the embedding provider or model, go to LLM Configuration.": "Za promjenu pružatelja usluge ugrađivanja ili modela, idite na LLM konfiguraciju.", + "To change the embedding provider or model, go to LLM configuration.": "Za promjenu pružatelja usluge ugrađivanja ili modela, idite na LLM konfiguraciju.", + "To date": "Do datuma", + "Toggle search sidebar": "Uključi/isključi bočnu traku pretraživanja", + "Token Created": "Token stvoren", + "Token Quota (per request)": "Kvota tokena (po zahtjevu)", + "Token copied to clipboard": "Token kopiran u međuspremnik", + "Token name": "Naziv tokena", + "Token revoked": "Token opozvan", + "Tools": "Alati", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Alati se izvršavaju sa zadanim korisničkim ovlastima agenta kada nijedna korisnička sesija nije aktivna (npr. cron poslovi). Konfigurirajte zadanog korisnika na kartici Postavke.", + "Tools respect the agent's own role and organization boundaries.": "Alati poštuju vlastitu ulogu agenta i granice organizacije.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Alati poštuju prikaze, ovlasti i granice organizacije agenta.", + "Top Deleters": "Najčešći brisači", + "Total": "Ukupno", + "Total Audit Trails": "Ukupno revizijskih tragova", + "Total Chunks Available:": "Ukupno dostupnih isječaka:", + "Total Deleted Items": "Ukupno izbrisanih stavki", + "Total Members": "Ukupno članova", + "Total Objects": "Ukupno objekata", + "Total Objects in Database:": "Ukupno objekata u bazi podataka:", + "Total Organisations": "Ukupno organizacija", + "Total Results": "Ukupno rezultata", + "Total Searches": "Ukupno pretraživanja", + "Total Size": "Ukupna veličina", + "Total Storage": "Ukupna pohrana", + "Total events": "Ukupno događaja", + "Total:": "Ukupno:", + "Totals": "Ukupni iznosi", + "Try searching by UUID or with different keywords": "Pokušajte pretraživati po UUID-u ili s drugačijim ključnim riječima", + "Type": "Vrsta", + "Type \"DELETE COLLECTION\" to confirm:": "Upišite \"DELETE COLLECTION\" za potvrdu:", + "Type Issue": "Vrsta problema", + "Type Variations:": "Varijacije vrste:", + "Type filter (optional, e.g. email)": "Filtar vrste (neobavezno, npr. e-pošta)", + "Type must be \"positive\" or \"negative\"": "Vrsta mora biti \"positive\" ili \"negative\"", + "Type to search for groups": "Upišite za pretraživanje grupa", + "Type to search for objects...": "Upišite za pretraživanje objekata...", + "Type to search for organisations": "Upišite za pretraživanje organizacija", + "Type to search for users": "Upišite za pretraživanje korisnika", + "Type to search groups...": "Upišite za pretraživanje grupa...", + "Type to search users...": "Upišite za pretraživanje korisnika...", + "Type to search...": "Upišite za pretraživanje...", + "Type your username to confirm": "Upišite svoje korisničko ime za potvrdu", + "URL": "URL", + "URL where Ollama is running": "URL na kojem se izvodi Ollama", + "UUID copied to clipboard": "UUID kopiran u međuspremnik", + "UUID:": "UUID:", + "Unchanged": "Nepromijenjeno", + "Unique Search Terms": "Jedinstveni pojmovi za pretraživanje", + "Unique Terms": "Jedinstveni pojmovi", + "Unknown": "Nepoznato", + "Unknown Object": "Nepoznati objekt", + "Unknown Register": "Nepoznati registar", + "Unknown Schema": "Nepoznata shema", + "Unknown error": "Nepoznata pogreška", + "Unknown widget type: {type}": "Nepoznata vrsta widgeta: {type}", + "Unlink email": "Odveži e-poštu", + "Unlink event": "Odveži događaj", + "Unnamed Object": "Neimenovani objekt", + "Untitled": "Bez naslova", + "Untitled View": "Prikaz bez naslova", + "Update": "Ažuriraj", + "Update Operations": "Operacije ažuriranja", + "Update available: {local} → {remote}": "Ažuriranje dostupno: {local} → {remote}", + "Update configuration: ...": "Ažuriraj konfiguraciju: ...", + "Update register OAS: ...": "Ažuriraj OAS registra: ...", + "Update vectors when object data changes (recommended for accurate search)": "Ažuriraj vektore kada se podaci objekta promijene (preporučeno za točno pretraživanje)", + "Updated": "Ažurirano", + "Updated At": "Ažurirano u", + "Updated:": "Ažurirano:", + "Upload new avatar": "Prenesi novi avatar", + "Uploading configuration sets...": "Prenošenje skupova konfiguracije...", + "Uptime:": "Vrijeme rada:", + "Url": "Url", + "Usage Count": "Broj korištenja", + "Use AI agents for processing and analysis": "Koristite AI agente za obradu i analizu", + "Use Large Language Model features": "Koristite značajke velikog jezičnog modela", + "Use SolrCloud with Zookeeper for distributed search": "Koristite SolrCloud sa Zookeeperom za distribuirano pretraživanje", + "Use SolrCloud with zookeeper for distributed search": "Koristite SolrCloud sa zookeeperom za distribuirano pretraživanje", + "Use VNG APIs instead of MongoDB.": "Koristite VNG API-je umjesto MongoDB-a.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Koristite zadane raspone (posljednjih 7/30/90 dana, prošla godina, starije)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Koristite vanjsku pohranu (npr. MongoDB) umjesto interne Nextcloud pohrane.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Koristite filtre za sužavanje unosa revizijskog traga prema registru, shemi, vrsti radnje, korisniku, rasponu datuma ili ID-u objekta.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Koristite filtre za sužavanje izbrisanih stavki prema registru, shemi, datumu brisanja ili korisniku koji ih je izbrisao.", + "Use filters to narrow down entities by type or category.": "Koristite filtre za sužavanje entiteta prema vrsti ili kategoriji.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Koristite filtre za sužavanje unosa traga pretraživanja prema registru, shemi, statusu uspjeha, korisniku, rasponu datuma, pojmovima za pretraživanje ili mjernim podacima o izvedbi.", + "Used By Collections": "Korišteno od strane kolekcija", + "Used by": "Korišteno od strane", + "User": "Korisnik", + "User Agent Statistics": "Statistika korisničkog agenta", + "User Description": "Korisnički opis", + "User Quota": "Korisnička kvota", + "User rate limits cleared successfully": "Ograničenja brzine korisnika uspješno su obrisana", + "User removed successfully": "Korisnik je uspješno uklonjen", + "User-friendly description shown in forms and help text": "Korisniku prilagođen opis prikazan u obrascima i tekstu pomoći", + "Username": "Korisničko ime", + "Username for SOLR authentication (optional)": "Korisničko ime za SOLR autentifikaciju (neobavezno)", + "Users": "Korisnici", + "Uses": "Korištenja", + "Using Pre-Generated Chunks": "Korištenje unaprijed generiranih isječaka", + "Validate": "Provjeri valjanost", + "Validate Objects": "Provjeri valjanost objekata", + "Validation completed for {schema}": "Provjera valjanosti dovršena za {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Provjera valjanosti dovršena za {schema}: {processed} obrađeno, {updated} ažurirano, {failed} neuspješno", + "Validation completed successfully": "Provjera valjanosti uspješno je dovršena", + "Validation failed": "Provjera valjanosti nije uspjela", + "Value": "Vrijednost", + "Value list (split on ,)": "Popis vrijednosti (razdvojeno s ,)", + "Vector Dimensions": "Dimenzije vektora", + "Vector Search Backend": "Pozadinski sustav za vektorsko pretraživanje", + "Vector field: _embedding_": "Vektorsko polje: _embedding_", + "Vectorization Triggers": "Okidači vektorizacije", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorizacija dovršena: {vectorized} objekata vektorizirano, {failed} neuspješno", + "Vectorization failed: {error}": "Vektorizacija nije uspjela: {error}", + "Vectorize All Files": "Vektoriziraj sve datoteke", + "Vectorize All Objects": "Vektoriziraj sve objekte", + "Vectorize all file types": "Vektoriziraj sve vrste datoteka", + "Vectorize all views": "Vektoriziraj sve prikaze", + "Vectorize on object creation": "Vektoriziraj pri stvaranju objekta", + "Vectors will be stored in your existing object and file collections": "Vektori će biti pohranjeni u vašim postojećim kolekcijama objekata i datoteka", + "Verantwoording": "Obrazloženje", + "Verifying server connectivity...": "Provjera povezivosti poslužitelja...", + "Version": "Verzija", + "Version Information": "Informacije o verziji", + "Very High": "Vrlo visoko", + "Very high": "Vrlo visoko", + "View": "Prikaz", + "View API Docs": "Pregledaj API dokumentaciju", + "View API Documentation": "Pregledaj API dokumentaciju", + "View APIs": "Pregledaj API-je", + "View Changes": "Pregledaj promjene", + "View Details": "Pregledaj pojedinosti", + "View Error": "Pregledaj pogrešku", + "View File": "Pregledaj datoteku", + "View Full Details": "Pregledaj sve pojedinosti", + "View Logs": "Pregledaj zapisnike", + "View Name": "Naziv prikaza", + "View Object": "Pregledaj objekt", + "View Parameters": "Pregledaj parametre", + "View Selection": "Odabir prikaza", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Pregledajte i analizirajte zapisnike traga pretraživanja s naprednim mogućnostima filtriranja i analitike", + "View and analyze system audit trails with advanced filtering capabilities": "Pregledajte i analizirajte revizijske tragove sustava s naprednim mogućnostima filtriranja", + "View and manage authorization across registers and schemas": "Pregledajte i upravljajte ovlaštenjima u registrima i shemama", + "View deleted successfully": "Prikaz je uspješno izbrisan", + "View entity details and manage relations": "Pregledajte pojedinosti entiteta i upravljajte vezama", + "View name": "Naziv prikaza", + "View name is required": "Naziv prikaza je obavezan", + "View object": "Pregledaj objekt", + "View saved successfully!": "Prikaz je uspješno spremljen!", + "View search analytics and manage search logs": "Pregledajte analitiku pretraživanja i upravljajte zapisnicima pretraživanja", + "View successfully deleted": "Prikaz je uspješno izbrisan", + "View successfully updated": "Prikaz je uspješno ažuriran", + "View updated successfully!": "Prikaz je uspješno ažuriran!", + "View webhook delivery logs and filter by webhook": "Pregledajte zapisnike isporuke webhooka i filtrirajte po webhooku", + "View {count} more": "Pregledaj još {count}", + "Views": "Prikazi", + "Visible to end users": "Vidljivo krajnjim korisnicima", + "Visible to users": "Vidljivo korisnicima", + "Wait for Response": "Čekaj odgovor", + "Wait for webhook response before continuing (required for request/response flows)": "Čekaj odgovor webhooka prije nastavka (obavezno za tokove zahtjev/odgovor)", + "Waiting for cluster synchronization...": "Čekanje sinkronizacije klastera...", + "Warmup Names Cache": "Zagrij predmemoriju naziva", + "Warmup failed": "Zagrijavanje nije uspjelo", + "Warning": "Upozorenje", + "Warning:": "Upozorenje:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Upozorenje: Ovo svojstvo ima nedosljedne vrste: {types}. Razmislite trebaju li svi objekti imati istu vrstu.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Pregledat ćemo sve objekte koji pripadaju ovoj shemi kako bismo otkrili nova svojstva i analizirali postojeća svojstva za potencijalna poboljšanja. Proces uključuje ispitivanje strukture podataka svakog objekta, prepoznavanje svojstava koja nisu definirana u trenutnoj shemi i pronalaženje prilika za poboljšanje postojećih definicija svojstava boljim ograničenjima, formatima i pravilima provjere valjanosti.", + "Webhook": "Webhook", + "Webhook Log Details": "Pojedinosti zapisnika webhooka", + "Webhook Logs": "Zapisnici webhooka", + "Webhook created successfully": "Webhook je uspješno stvoren", + "Webhook deleted": "Webhook izbrisan", + "Webhook retry delivered successfully": "Ponovni pokušaj webhooka uspješno je isporučen", + "Webhook retry delivery failed": "Isporuka ponovnog pokušaja webhooka nije uspjela", + "Webhook updated": "Webhook ažuriran", + "Webhook updated successfully": "Webhook je uspješno ažuriran", + "Webhooks": "Webhookovi", + "Weekly": "Tjedno", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kada se agent izvodi bez korisničke sesije (npr. zakazani zadaci), koristit će se kontekst ovog korisnika", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kada se agent izvodi bez korisničke sesije (npr. zakazani zadaci), koristit će se kontekst ovog korisnika.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Kada je onemogućeno, ova će faseta prikazivati samo vrijednosti iz ove sheme i uključivat će filtar sheme kada je odabrana.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Kada je omogućeno, spremanje ovog objekta također će ažurirati ciljne objekte kako bi uključili referencu natrag na ovaj objekt.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Kada je omogućeno, ovo će se svojstvo ukloniti iz izvornog objekta nakon ažuriranja ciljnih objekata.", + "Workflow Management": "Upravljanje tijekom rada", + "Workflows": "Tijekovi rada", + "Wrap webhook payload in CloudEvents format for better interoperability": "Omotaj korisni teret webhooka u CloudEvents format za bolju interoperabilnost", + "Wrap webhook payload in cloudevents format for better interoperability": "Omotaj korisni teret webhooka u cloudevents format za bolju interoperabilnost", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Da", + "You": "Vi", + "You are not yet a member of any organisations.": "Još niste član nijedne organizacije.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Možete stvoriti nove ConfigSetove na temelju _default predloška ili izravno prenijeti prilagođene na svoj SOLR poslužitelj.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Možete stvoriti nove configsetove na temelju _default predloška ili izravno prenijeti prilagođene na svoj SOLR poslužitelj.", + "You do not have access to this conversation": "Nemate pristup ovom razgovoru", + "You do not have permission to delete this conversation": "Nemate ovlasti za brisanje ovog razgovora", + "You do not have permission to modify this conversation": "Nemate ovlasti za izmjenu ovog razgovora", + "You do not have permission to restore this conversation": "Nemate ovlasti za obnovu ovog razgovora", + "You do not have permission to view the permission matrix. Admin access is required.": "Nemate ovlasti za pregled matrice ovlasti. Potreban je administratorski pristup.", + "You have selected {count} properties to add to the schema.": "Odabrali ste {count} svojstava za dodavanje u shemu.", + "You must be logged in to favorite views": "Morate biti prijavljeni da biste prikaze označili kao omiljene", + "You need an AI agent to start a conversation.": "Potreban vam je AI agent za početak razgovora.", + "Your Fireworks AI API key. Get one at": "Vaš Fireworks AI API ključ. Nabavite ga na", + "Your OpenAI API key. Get one at": "Vaš OpenAI API ključ. Nabavite ga na", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Vaše povratne informacije su zabilježene. Po želji ovdje možete navesti dodatne pojedinosti...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Vaše povratne informacije su zabilježene. po želji ovdje možete navesti dodatne pojedinosti...", + "Zookeeper Hosts": "Zookeeper poslužitelji", + "Zookeeper Port": "Zookeeper priključak", + "Zookeeper Settings (SolrCloud)": "Zookeeper postavke (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper niz za povezivanje za SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Broj Zookeeper priključka (neobavezno, zadano 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORENJE O TRAJNOM BRISANJU \\u26A0\\uFE0F\n\nUpravo ćete TRAJNO izbrisati SVE objekte za shemu \"{schema}\":\n\n\\u2022 Aktivni objekti: {active}\n\\u2022 Meko izbrisani objekti: {deleted}\n\\u2022 Ukupno: {total}\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORENJE O TRAJNOM BRISANJU \\u26A0\\uFE0F\n\nUpravo ćete TRAJNO izbrisati {count} meko izbrisanih objekata za shemu \"{schema}\".\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "and accessible to all users in selected groups (or all users if no groups selected).": "i dostupno svim korisnicima u odabranim grupama (ili svim korisnicima ako nije odabrana nijedna grupa).", + "and add the files there.": "i ondje dodajte datoteke.", + "approved": "odobreno", + "are only accessible to invited users.": "dostupni su samo pozvanim korisnicima.", + "by": "od strane", + "chunk": "isječak", + "chunks": "isječci", + "chunks processed": "isječaka obrađeno", + "completed": "dovršeno", + "configuration(s)": "konfiguracija", + "conversationId is required": "conversationId je obavezan", + "create": "stvori", + "delete": "izbriši", + "deleted objects section": "odjeljak izbrisanih objekata", + "e.g., lib/Settings/config.json": "npr. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "npr. lib/Settings/register.json", + "e.g., lib/settings/config.json": "npr. lib/settings/config.json", + "e.g., lib/settings/register.json": "npr. lib/settings/register.json", + "execute": "izvrši", + "failed": "neuspješno", + "file": "datoteka", + "files": "datoteke", + "file{plural}": "datotek{plural}", + "folder": "mapa", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "u {register}", + "in_progress": "u tijeku", + "inherited": "naslijeđeno", + "list": "popis", + "log{plural}": "zapisni{plural}", + "manage": "upravljaj", + "members": "članovi", + "menu or contact someone with permission to create agents.": "izborniku ili se obratite nekome tko ima ovlasti za stvaranje agenata.", + "message content is required": "sadržaj poruke je obavezan", + "messages": "poruke", + "missing": "nedostaje", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL i API ključ su obavezni", + "n8n configuration saved successfully": "n8n konfiguracija uspješno je spremljena", + "n8n connection not configured": "n8n veza nije konfigurirana", + "n8n connection successful": "n8n veza uspješna", + "n8n connection test failed: {message}": "n8n test veze nije uspio: {message}", + "n8n connection test successful": "n8n test veze uspješan", + "n8n initialization failed: {message}": "n8n inicijalizacija nije uspjela: {message}", + "n8n integration disabled": "n8n integracija onemogućena", + "n8n integration enabled": "n8n integracija omogućena", + "n8n project initialized successfully": "n8n projekt uspješno je inicijaliziran", + "n8n settings saved successfully": "n8n postavke uspješno su spremljene", + "notify_push configuration guide": "vodič za konfiguraciju notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push je instaliran, ali OpenRegister još nije potvrdio uspješno guranje. Pokrenite spremanje objekta za aktivaciju ili provjerite svoju notify_push konfiguraciju.", + "notify_push is installed but not yet active": "notify_push je instaliran, ali još nije aktivan", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekti", + "objects processed": "objekata obrađeno", + "object{plural}": "objek{plural}", + "or pick a range": "ili odaberite raspon", + "org-...": "org-...", + "pending": "na čekanju", + "provides a good balance between speed and accuracy.": "pruža dobru ravnotežu između brzine i točnosti.", + "public": "javno", + "read": "čitaj", + "register(s)": "registar/registri", + "register(s) selected": "registar/registri odabran", + "register{plural}": "registar{plural}", + "rejected": "odbijeno", + "required": "obavezno", + "results": "rezultati", + "schema(s)": "shema/sheme", + "schema(s) selected": "shema/sheme odabrana", + "schema{plural}": "shem{plural}", + "selected objects": "odabrani objekti", + "set": "postavi", + "sk-...": "sk-...", + "this application": "ova aplikacija", + "to": "do", + "type must be \"positive\" or \"negative\"": "vrsta mora biti \"positive\" ili \"negative\"", + "update": "ažuriraj", + "vectorized": "vektorizirano", + "view": "prikaz", + "views selected": "prikaza odabrano", + "waiting": "čeka se", + "{active} active, {deleted} deleted": "{active} aktivnih, {deleted} izbrisanih", + "{count} audit trails cleared successfully": "{count} revizijskih tragova uspješno obrisano", + "{count} compliance issue(s) detected": "Otkriveno {count} problema usklađenosti", + "{count} deletions": "{count} brisanja", + "{count} documents found": "Pronađeno {count} dokumenata", + "{count} email": "{count} e-pošta", + "{count} emails": "{count} e-pošta", + "{count} entries": "{count} unosa", + "{count} issues": "{count} problema", + "{count} members": "{count} članova", + "{count} searches": "{count} pretraživanja", + "{count} selected": "{count} odabrano", + "{count} widget(s)": "{count} widgeta", + "{matched} object(s) matched. {erased} erased.": "{matched} objekata podudara se. {erased} izbrisano.", + "{minutes} minutes ago": "prije {minutes} minuta", + "{percentage}% of objects": "{percentage}% objekata", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} u {register} / {schema}", + "{type} cache cleared successfully": "{type} predmemorija uspješno obrisana", + "ℹ️ Current Configuration": "ℹ️ Trenutna konfiguracija", + "ℹ️ current configuration": "ℹ️ trenutna konfiguracija", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ OPASNOST: Svih {total} objekata bit će TRAJNO IZBRISANO iz baze podataka. Ova je radnja NEPOVRATNA i ne može se poništiti!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORENJE O TRAJNOM BRISANJU ⚠️\n\nUpravo ćete TRAJNO izbrisati SVE objekte za shemu \"{schema}\":\n\n• Aktivni objekti: {active}\n• Meko izbrisani objekti: {deleted}\n• Ukupno: {total}\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORENJE O TRAJNOM BRISANJU ⚠️\n\nUpravo ćete TRAJNO izbrisati {count} meko izbrisanih objekata za shemu \"{schema}\".\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "⚡ Batch Processing": "⚡ Skupna obrada", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR može obraditi milijune dokumenata s vremenom odziva pretraživanja kraćim od sekunde...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Sveobuhvatan kontekst ✓ Manja vjerojatnost izostavljanja pojedinosti ✗ Sporiji odgovori ✗ Može uključivati manje relevantne informacije", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Brži odgovori ✓ Usredotočeniji odgovori ✗ Može izostaviti relevantne informacije", + "✨ AI Features": "✨ AI značajke", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 U distribuiranom načinu rada, SOLR koristi ZooKeeper za koordinaciju klastera...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasetno pretraživanje omogućuje korisnicima dublje istraživanje rezultata po kategorijama...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Jeste li znali? SOLR pokreće pretraživanje za Netflix, Apple i mnoge druge velike stranice!", + "💰 View Selection (Cost Optimization)": "💰 Odabir prikaza (optimizacija troškova)", + "📄 Object Serialization": "📄 Serijalizacija objekta", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR pruža bogatu analitiku i statistiku o izvedbi pretraživanja...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSetovi sadrže shemu i konfiguracijske datoteke za vaš indeks pretraživanja...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikacija osigurava dostupnost vašeg indeksa pretraživanja čak i ako čvorovi zakažu...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR je moćna platforma za pretraživanje na razini poduzeća izgrađena na Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Postavke vektorizacije", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automatsko skaliranje može dinamički dodavati ili uklanjati čvorove na temelju opterećenja...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Sigurnosne značajke uključuju autentifikaciju, autorizaciju i SSL enkripciju...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-pošta", + "{count} e-pošte" + ], + "Purge {count} object from database": [ + "Trajno ukloni {count} objekt iz baze podataka", + "Trajno ukloni {count} objekata iz baze podataka" + ], + "Restore {count} object": [ + "Vrati {count} objekt", + "Vrati {count} objekata" + ], + "Successfully restored {count} object": [ + "Uspješno vraćen {count} objekt", + "Uspješno vraćeno {count} objekata" + ], + "Delete {count} object": [ + "Izbriši {count} objekt", + "Izbriši {count} objekata" + ], + "Object successfully deleted": [ + "Objekt je uspješno izbrisan", + "Objekti su uspješno izbrisani" + ] + } +} diff --git a/l10n/hu.js b/l10n/hu.js new file mode 100644 index 0000000000..3b2a5165d3 --- /dev/null +++ b/l10n/hu.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(nincs tárgy)", + "(no title)" : "(nincs cím)", + "(unnamed)" : "(névtelen)", + "(untitled card)" : "(cím nélküli kártya)", + "Activities" : "Tevékenységek", + "Add Endpoint" : "Végpont hozzáadása", + "Add Source" : "Forrás hozzáadása", + "Add card" : "Kártya hozzáadása", + "Add contact" : "Kapcsolat hozzáadása", + "All clear" : "Minden rendben", + "Articles" : "Cikkek", + "Code" : "Kód", + "Compliance" : "Megfelelőség", + "Contacts" : "Kapcsolatok", + "Create Agent" : "Ügynök létrehozása", + "Create Application" : "Alkalmazás létrehozása", + "Create event" : "Esemény létrehozása", + "Erasure complete" : "Törlés befejezve", + "Erasure preview" : "Törlés előnézete", + "Expand details" : "Részletek kibontása", + "Save changes" : "Módosítások mentése", + "📄 Object Serialization" : "📄 Objektumszerializálás", + "🔢 Vectorization Settings" : "🔢 Vektorizálási beállítások", + "💰 View Selection (Cost Optimization)" : "💰 Nézetválasztás (költségoptimalizálás)", + "AI Assistant" : "MI-asszisztens", + "AI Chat" : "MI-csevegés", + "API Key" : "API-kulcs", + "API tokens saved successfully" : "Az API-tokenek sikeresen mentve", + "About ConfigSets" : "A ConfigSet-ekről", + "Action" : "Művelet", + "Action Distribution" : "Műveletek megoszlása", + "Action:" : "Művelet:", + "Actions" : "Műveletek", + "Active" : "Aktív", + "Active Collections" : "Aktív gyűjtemények", + "Active Filters" : "Aktív szűrők", + "Active Organisations" : "Aktív szervezetek", + "Active filters:" : "Aktív szűrők:", + "Active organisation set successfully" : "Az aktív szervezet sikeresen beállítva", + "Activity Period" : "Tevékenységi időszak", + "Add Groups" : "Csoportok hozzáadása", + "Add Schema" : "Séma hozzáadása", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Sémacímek, leírások és nyilvántartási információk hozzáadása a keresés gazdagabb környezetének biztosításához", + "Add to favorites" : "Hozzáadás a kedvencekhez", + "Added" : "Hozzáadva", + "Added to favorites" : "Hozzáadva a kedvencekhez", + "Additional Information" : "További információk", + "Additional feedback saved. Thank you!" : "A további visszajelzés mentve. Köszönjük!", + "Admin" : "Adminisztrátor", + "Advanced" : "Speciális", + "Advanced Filters" : "Speciális szűrők", + "Advanced Options" : "Speciális beállítások", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "A speciális szűrők nem érhetők el adatbázisforrás használatakor. A szűrési beállításokhoz váltson Automatikus vagy SOLR-index lehetőségre.", + "After" : "Után", + "Agent deleted successfully" : "Az ügynök sikeresen törölve", + "Agents" : "Ügynökök", + "All" : "Mind", + "All Categories" : "Minden kategória", + "All Collections" : "Minden gyűjtemény", + "All Confidence Levels" : "Minden megbízhatósági szint", + "All ConfigSets" : "Minden ConfigSet", + "All Files" : "Minden fájl", + "All Levels" : "Minden szint", + "All Types" : "Minden típus", + "All Webhooks" : "Minden webhook", + "All actions" : "Minden művelet", + "All audit trails cleared successfully" : "Minden naplóbejegyzés sikeresen törölve", + "All registers" : "Minden nyilvántartás", + "All schemas" : "Minden séma", + "All searches" : "Minden keresés", + "All users" : "Minden felhasználó", + "An error occurred while clearing audit trails" : "Hiba történt a naplóbejegyzések törlése közben", + "An error occurred while deleting the audit trail" : "Hiba történt a naplóbejegyzés törlése közben", + "An error occurred while deleting the view" : "Hiba történt a nézet törlése közben", + "An error occurred while permanently deleting the objects" : "Hiba történt az objektumok végleges törlése közben", + "Analysis completed:" : "Az elemzés befejezve:", + "Analysis steps:" : "Elemzési lépések:", + "Analytics" : "Elemzések", + "Analyze Objects" : "Objektumok elemzése", + "Analyze Properties" : "Tulajdonságok elemzése", + "Analyze existing properties for improvement opportunities" : "Meglévő tulajdonságok elemzése a fejlesztési lehetőségek érdekében", + "Analyzing..." : "Elemzés…", + "Any favorites and sharing settings for this view" : "A nézet bármely kedvenc- és megosztási beállítása", + "Any user" : "Bármely felhasználó", + "App store cache invalidated successfully" : "Az alkalmazás-áruház gyorsítótára sikeresen érvénytelenítve", + "Application deleted successfully" : "Az alkalmazás sikeresen törölve", + "Applications" : "Alkalmazások", + "Apply Changes" : "Módosítások alkalmazása", + "Archive" : "Archiválás", + "Archive conversation" : "Beszélgetés archiválása", + "Archived conversations are hidden from your active list" : "Az archivált beszélgetések nem jelennek meg az aktív listában", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Biztosan megtisztítja a régi keresési naplókat? Ez törli a 30 napnál régebbi bejegyzéseket.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Biztosan törli a kiválasztott naplóbejegyzéseket? Ez a művelet nem vonható vissza.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Biztosan törli a kiválasztott keresési naplókat? Ez a művelet nem vonható vissza.", + "Are you sure you want to delete this ConfigSet?" : "Biztosan törli ezt a ConfigSet-et?", + "Are you sure you want to permanently delete" : "Biztosan véglegesen törli", + "Ask a question..." : "Tegyen fel egy kérdést…", + "Ask questions about your data using natural language" : "Tegyen fel kérdéseket az adatairól természetes nyelven", + "Attempt" : "Próbálkozás", + "Audit Trail Changes" : "Naplóbejegyzés-módosítások", + "Audit Trail Details" : "Naplóbejegyzés részletei", + "Audit Trail Management" : "Naplóbejegyzések kezelése", + "Audit Trail Statistics" : "Naplóbejegyzés-statisztikák", + "Audit Trails" : "Naplóbejegyzések", + "Audit trail data copied to clipboard" : "A naplóbejegyzés adatai a vágólapra másolva", + "Audit trail deleted successfully" : "A naplóbejegyzés sikeresen törölve", + "Audit trail successfully deleted" : "A naplóbejegyzés sikeresen törölve", + "Authentication" : "Hitelesítés", + "Auto-commit disabled" : "Az automatikus véglegesítés letiltva", + "Auto-commit enabled" : "Az automatikus véglegesítés engedélyezve", + "Auto-create Default Organisation" : "Alapértelmezett szervezet automatikus létrehozása", + "Auto-retry failed vectorizations" : "Sikertelen vektorizálások automatikus újrapróbálkozása", + "Automatically commit changes to SOLR index" : "Módosítások automatikus véglegesítése a SOLR-indexbe", + "Automatically create a default organisation if none exists when the app is initialized" : "Alapértelmezett szervezet automatikus létrehozása, ha az alkalmazás inicializálásakor még nem létezik", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Vektorbeágyazások automatikus létrehozása a szövegrészletekből a fájlok feltöltésekor és feldolgozásakor", + "Automatically generate vector embeddings when objects are created or updated" : "Vektorbeágyazások automatikus létrehozása az objektumok létrehozásakor vagy frissítésekor", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Sikertelen vektorizálási próbálkozások automatikus megismétlése (legfeljebb 3 alkalommal)", + "Available Filters" : "Elérhető szűrők", + "Available Workflows" : "Elérhető munkafolyamatok", + "Avg Execution Time" : "Átlagos végrehajtási idő", + "Avg Members/Org" : "Átlagos tagok/szervezet", + "Avg Object Views/Session" : "Átlagos objektummegtekintés/munkamenet", + "Avg Response Time" : "Átlagos válaszidő", + "Avg Results/Search" : "Átlagos találat/keresés", + "Avg Searches/Session" : "Átlagos keresés/munkamenet", + "Back" : "Vissza", + "Back to Entities" : "Vissza az entitásokhoz", + "Back to Registers" : "Vissza a nyilvántartásokhoz", + "Back to Webhooks" : "Vissza a webhookokhoz", + "Back to applications" : "Vissza az alkalmazásokhoz", + "Back to entities" : "Vissza az entitásokhoz", + "Backend updated successfully. Please reload the application." : "A háttérrendszer sikeresen frissítve. Töltse újra az alkalmazást.", + "Base URL (Optional)" : "Alap-URL (nem kötelező)", + "Basic Information" : "Alapvető információk", + "Batch Size" : "Kötegméret", + "Batch extraction completed" : "A kötegelt kinyerés befejeződött", + "Before" : "Előtt", + "Before object vectorization can work:" : "Mielőtt az objektumvektorizálás működhetne:", + "Behavior Issue" : "Viselkedési probléma", + "Bookmarks" : "Könyvjelzők", + "Blob storage has been retired. All objects now use magic tables." : "A blobtárolás megszűnt. Minden objektum mostantól magic táblákat használ.", + "Branch" : "Ág", + "Bulk delete operation completed successfully" : "A tömeges törlési művelet sikeresen befejeződött", + "Bulk save operation completed successfully" : "A tömeges mentési művelet sikeresen befejeződött", + "Business Data" : "Üzleti adatok", + "Cache cleared successfully" : "A gyorsítótár sikeresen törölve", + "Calculate Sizes" : "Méretek kiszámítása", + "Cancel" : "Mégse", + "Cannot delete: objects are still attached" : "Nem törölhető: objektumok vannak még csatolva", + "Category" : "Kategória", + "Change Type" : "Módosítás típusa", + "Changes" : "Módosítások", + "Changes copied to clipboard" : "A módosítások a vágólapra másolva", + "Chat Model" : "Csevegési modell", + "Chat Provider" : "Csevegési szolgáltató", + "Chat Provider (RAG)" : "Csevegési szolgáltató (RAG)", + "Chat Settings" : "Csevegési beállítások", + "Chat provider connection successful!" : "A csevegési szolgáltatóhoz való kapcsolódás sikeres!", + "Chat settings" : "Csevegési beállítások", + "Choose a register" : "Válasszon egy nyilvántartást", + "Choose a schema" : "Válasszon egy sémát", + "Choose how vector similarity calculations are performed for semantic search" : "Válassza ki, hogyan történjenek a vektorhasonlósági számítások a szemantikus keresésnél", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Válassza ki, mely fájltípusok kerüljenek be a vektorizálási folyamatba. Csak a kinyert szöveggel és részletekkel rendelkező fájlok kerülnek feldolgozásra.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Válassza ki, mely nézetek kerüljenek be a vektorizálási folyamatba. Hagyja üresen az összes nézet feldolgozásához a konfiguráció alapján.", + "Chunk" : "Részlet", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "A részlet törlése még nincs megvalósítva. Használjon részletalapú végpontokat.", + "Chunks" : "Részletek", + "Chunks to Vectorize:" : "Vektorizálandó részletek:", + "Cleanup Old Trails" : "Régi naplók megtisztítása", + "Cleanup completed" : "A megtisztítás befejeződött", + "Clear All Embeddings" : "Minden beágyazás törlése", + "Clear Entries" : "Bejegyzések törlése", + "Clear Filtered Audit Trails" : "Szűrt naplóbejegyzések törlése", + "Clear Filters" : "Szűrők törlése", + "Clear Index" : "Index törlése", + "Clear Selection" : "Kijelölés törlése", + "Clear all" : "Összes törlése", + "Clear all filters" : "Összes szűrő törlése", + "Clear date range" : "Dátumtartomány törlése", + "Clearing..." : "Törlés…", + "Close" : "Bezárás", + "Collection Management" : "Gyűjteménykezelés", + "Collection Name" : "Gyűjtemény neve", + "Collection assignments updated successfully" : "A gyűjtemény-hozzárendelések sikeresen frissítve", + "Collection cleared successfully" : "A gyűjtemény sikeresen törölve", + "Collection copied successfully" : "A gyűjtemény sikeresen másolva", + "Collection created successfully" : "A gyűjtemény sikeresen létrehozva", + "Collection deleted successfully" : "A gyűjtemény sikeresen törölve", + "Collection used to store and index file metadata and content" : "A fájlmetaadatok és -tartalom tárolására és indexelésére használt gyűjtemény", + "Collection used to store and index object data" : "Az objektumadatok tárolására és indexelésére használt gyűjtemény", + "Collections:" : "Gyűjtemények:", + "Commit Message" : "Véglegesítési üzenet", + "Commit Within (ms)" : "Véglegesítés ezen belül (ms)", + "Commit message" : "Véglegesítési üzenet", + "Compare current schema with real object data" : "Jelenlegi séma összehasonlítása a valós objektumadatokkal", + "Completed" : "Befejezve", + "Complex" : "Összetett", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Összetett lekérdezések: speciális keresések több szűrővel, operátorral és összetett paraméterkombinációkkal", + "Confidence Level" : "Megbízhatósági szint", + "Confidence Score" : "Megbízhatósági pontszám", + "Config must be provided as an object" : "A konfigurációt objektumként kell megadni", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSet-kezelés", + "ConfigSet Name" : "ConfigSet neve", + "ConfigSet created successfully" : "A ConfigSet sikeresen létrehozva", + "ConfigSet deleted successfully" : "A ConfigSet sikeresen törölve", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "A ConfigSet-ek határozzák meg a SOLR-gyűjtemények sémáját és konfigurációját. Meződefiníciókat, elemzőket és egyéb keresési beállításokat tartalmaznak.", + "Configuration" : "Konfiguráció", + "Configuration Data" : "Konfigurációs adatok", + "Configuration saved" : "A konfiguráció mentve", + "Configurations" : "Konfigurációk", + "Configure Facets" : "Aspektusok konfigurálása", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Nagy nyelvi modell (LLM) szolgáltatók konfigurálása az MI-alapú funkciókhoz, beleértve a szemantikus keresést, a beágyazásokat és a csevegést.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Adja meg a SOLR-kiszolgáló alapvető kapcsolati beállításait, beleértve a hitelesítést és a hálózati beállításokat. A magok és gyűjtemények kezeléséhez használja a külön ConfigSet- és gyűjteménykezelő párbeszédablakokat.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Állítsa be, hogyan alakuljanak át az adatbázis-objektumok vektorbeágyazásokká a szemantikus kereséshez. Az objektumok közvetlenül vektorizálódnak, szövegkinyerés nélkül.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Állítsa be, hogyan alakuljanak át az objektumok szöveggé a vektorizálás előtt. Ezek a beállítások hatással vannak a keresés minőségére és környezetére.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Adja meg a fájlvektorizálás paramétereit. Ez a folyamat vektorbeágyazásokat hoz létre az összes kinyert fájlrészlethez.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Adja meg az objektumvektorizálás paramétereit. Ez a folyamat vektorbeágyazásokat hoz létre minden olyan objektumhoz, amely megfelel a nézetszűrőknek.", + "Configure which types of data to search and how many sources to retrieve" : "Állítsa be, mely adattípusokban keressen, és hány forrást kérjen le", + "Connection Failed" : "A kapcsolódás sikertelen", + "Connection Settings" : "Kapcsolati beállítások", + "Connection Successful!" : "A kapcsolódás sikeres!", + "Connection failed" : "A kapcsolódás sikertelen", + "Connection protocol" : "Kapcsolati protokoll", + "Connection successful" : "A kapcsolódás sikeres", + "Connection timeout in seconds" : "Kapcsolat időkorlátja másodpercben", + "Constant delay between retries (5 minutes)" : "Állandó késleltetés az újrapróbálkozások között (5 perc)", + "Constraint Issue" : "Megszorítási probléma", + "Content Filters" : "Tartalomszűrők", + "Continue to Properties" : "Tovább a tulajdonságokhoz", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Szabályozza, mely objektumnézetek vektorizálódjanak az API-költségek csökkentése érdekében. Csak azokat a nézeteket vektorizálja, amelyek számára előnyös a szemantikus keresés.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Szabályozza, mely nézeteket és eszközöket használhatja az MI ebben a beszélgetésben. Alapértelmezés szerint minden ügynökképesség engedélyezve van.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Szabályozza a véletlenszerűséget (0 = determinisztikus, 2 = nagyon kreatív)", + "Conversation ID is required" : "A beszélgetés azonosítója kötelező", + "Conversation archived" : "A beszélgetés archiválva", + "Conversation archived successfully" : "A beszélgetés sikeresen archiválva", + "Conversation cleared successfully" : "A beszélgetés sikeresen törölve", + "Conversation deleted" : "A beszélgetés törölve", + "Conversation permanently deleted" : "A beszélgetés véglegesen törölve", + "Conversation renamed" : "A beszélgetés átnevezve", + "Conversation restored" : "A beszélgetés visszaállítva", + "Conversation title" : "A beszélgetés címe", + "Copied!" : "Másolva!", + "Copy" : "Másolás", + "Copy Changes" : "Módosítások másolása", + "Copy Collection" : "Gyűjtemény másolása", + "Copy Data" : "Adatok másolása", + "Copy Full Data" : "Teljes adatok másolása", + "Copying..." : "Másolás…", + "Count" : "Darabszám", + "Counting objects..." : "Objektumok számolása…", + "Costs" : "Költségek", + "Create" : "Létrehozás", + "Create Collection" : "Gyűjtemény létrehozása", + "Create ConfigSet" : "ConfigSet létrehozása", + "Create New Collection" : "Új gyűjtemény létrehozása", + "Create New ConfigSet" : "Új ConfigSet létrehozása", + "Create Operations" : "Létrehozási műveletek", + "Create Webhook" : "Webhook létrehozása", + "Create a copy of collection:" : "Gyűjtemény másolatának létrehozása:", + "Create a new ConfigSet based on the _default template" : "Új ConfigSet létrehozása a _default sablon alapján", + "Create a new SOLR collection from an existing ConfigSet" : "Új SOLR-gyűjtemény létrehozása egy meglévő ConfigSet alapján", + "Create your first AI agent to get started." : "A kezdéshez hozza létre az első MI-ügynökét.", + "Created" : "Létrehozva", + "Created:" : "Létrehozva:", + "Creating..." : "Létrehozás…", + "Current Filters" : "Jelenlegi szűrők", + "Current Type:" : "Jelenlegi típus:", + "Custom API endpoint if using a different region" : "Egyéni API-végpont eltérő régió használata esetén", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Egyéni HTTP-fejlécek (soronként egy, formátum: Fejlécnév: érték)", + "Daily" : "Naponta", + "Dashboard" : "Irányítópult", + "Data Source" : "Adatforrás", + "Data sources" : "Adatforrások", + "Data type does not match observed values" : "Az adattípus nem egyezik a megfigyelt értékekkel", + "Database URL" : "Adatbázis-URL", + "Database information refreshed" : "Az adatbázis-információk frissítve", + "Date Range" : "Dátumtartomány", + "Default" : "Alapértelmezett", + "Default Organisation" : "Alapértelmezett szervezet", + "Default: 5" : "Alapértelmezett: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "A késleltetés minden próbálkozással megduplázódik (2, 4, 8 perc…)", + "Delays increase linearly (5, 10, 15 minutes...)" : "A késleltetés lineárisan növekszik (5, 10, 15 perc…)", + "Delete" : "Törlés", + "Delete Application" : "Alkalmazás törlése", + "Delete Audit Trail" : "Naplóbejegyzés törlése", + "Delete Collection" : "Gyűjtemény törlése", + "Delete ConfigSet" : "ConfigSet törlése", + "Delete Objects" : "Objektumok törlése", + "Delete Operations" : "Törlési műveletek", + "Delete View" : "Nézet törlése", + "Delete all objects in this schema" : "A séma összes objektumának törlése", + "Delete permanently" : "Végleges törlés", + "Delete this conversation?" : "Törli ezt a beszélgetést?", + "Delete view" : "Nézet törlése", + "Deleted" : "Törölve", + "Deleted By" : "Törölte", + "Deleted Date" : "Törlés dátuma", + "Deleted Items Management" : "Törölt elemek kezelése", + "Deleted This Week" : "Ezen a héten törölve", + "Deleted Today" : "Ma törölve", + "Deleted:" : "Törölve:", + "Deleting..." : "Törlés…", + "Deletion Date Range" : "Törlési dátumtartomány", + "Deletion Statistics" : "Törlési statisztikák", + "Deprecated" : "Elavult", + "Description" : "Leírás", + "Description:" : "Leírás:", + "Detect data types and patterns" : "Adattípusok és minták felismerése", + "Detected At" : "Felismerés ideje", + "Detected Format:" : "Felismert formátum:", + "Detected Issues:" : "Felismert problémák:", + "Disable" : "Letiltás", + "Disabled" : "Letiltva", + "Discover Files" : "Fájlok felderítése", + "Discovered Properties" : "Felfedezett tulajdonságok", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Véglegesen törölni szeretné az összes szűrt naplóbejegyzést? Ez a művelet nem vonható vissza.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Véglegesen törölni szeretné ezt a naplóbejegyzést? Ez a művelet nem vonható vissza.", + "Documents" : "Dokumentumok", + "Download API Spec" : "API-specifikáció letöltése", + "Edit" : "Szerkesztés", + "Edit Register" : "Nyilvántartás szerkesztése", + "Edit Schema" : "Séma szerkesztése", + "Edit View" : "Nézet szerkesztése", + "Edit Webhook" : "Webhook szerkesztése", + "Edit view" : "Nézet szerkesztése", + "Edit view details" : "Nézet részleteinek szerkesztése", + "Effectiveness" : "Hatékonyság", + "Email" : "E-mail", + "Emails" : "E-mailek", + "Embedding Model" : "Beágyazási modell", + "Embedding Provider" : "Beágyazási szolgáltató", + "Embedding generated successfully" : "A beágyazás sikeresen létrehozva", + "Embedding provider connection successful!" : "A beágyazási szolgáltatóhoz való kapcsolódás sikeres!", + "Enable" : "Engedélyezés", + "Enable auto-creation" : "Automatikus létrehozás engedélyezése", + "Enable automatic file vectorization" : "Automatikus fájlvektorizálás engedélyezése", + "Enable automatic object vectorization" : "Automatikus objektumvektorizálás engedélyezése", + "Enable automatic synchronization" : "Automatikus szinkronizálás engedélyezése", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Részletes naplózás engedélyezése a SOLR-műveletekhez (hibakereséshez ajánlott)", + "Enable faceting" : "Aspektusok engedélyezése", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Az LLM-funkciók engedélyezése vagy letiltása. A szolgáltatókat és modelleket a fenti LLM-konfiguráció gombbal állíthatja be.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "A SOLR-keresési integráció engedélyezése vagy letiltása. A kapcsolati beállításokat a fenti Kapcsolati beállítások gombbal állíthatja be.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Az n8n-munkafolyamat-integráció engedélyezése vagy letiltása. A kapcsolati beállításokat alább állíthatja be.", + "Enable or disable this webhook" : "Ennek a webhooknak az engedélyezése vagy letiltása", + "Enable vectorization for all existing and future views (may increase costs)" : "Vektorizálás engedélyezése minden meglévő és jövőbeli nézethez (növelheti a költségeket)", + "Enabled" : "Engedélyezve", + "Endpoints" : "Végpontok", + "Enter ConfigSet name" : "Adja meg a ConfigSet nevét", + "Enter collection name" : "Adja meg a gyűjtemény nevét", + "Enter description (optional)..." : "Adja meg a leírást (nem kötelező)…", + "Enter new collection name" : "Adja meg az új gyűjtemény nevét", + "Enter object ID" : "Adja meg az objektumazonosítót", + "Enter search term" : "Adja meg a keresési kifejezést", + "Enter view name..." : "Adja meg a nézet nevét…", + "Enter webhook name" : "Adja meg a webhook nevét", + "Entities" : "Entitások", + "Entity Information" : "Entitásinformációk", + "Entity deleted successfully" : "Az entitás sikeresen törölve", + "Entity not found" : "Az entitás nem található", + "Entries to be deleted:" : "Törlendő bejegyzések:", + "Enum Issue" : "Felsorolási probléma", + "Enum constraint is missing" : "Hiányzik a felsorolási megszorítás", + "Error" : "Hiba", + "Error Details" : "Hiba részletei", + "Error Information" : "Hibainformációk", + "Error Message" : "Hibaüzenet", + "Error loading application" : "Hiba az alkalmazás betöltése közben", + "Error loading audit trails" : "Hiba a naplóbejegyzések betöltése közben", + "Error loading entity" : "Hiba az entitás betöltése közben", + "Error loading search trails" : "Hiba a keresési naplók betöltése közben", + "Estimated Batches:" : "Becsült kötegek:", + "Estimated Cost:" : "Becsült költség:", + "Estimated Duration:" : "Becsült időtartam:", + "Event" : "Esemény", + "Event Property for Payload" : "Eseménytulajdonság a hasznos adathoz", + "Events" : "Események", + "Example Value" : "Példaérték", + "Exclusive Maximum" : "Kizárólagos maximum", + "Exclusive Minimum" : "Kizárólagos minimum", + "Execution Mode" : "Végrehajtási mód", + "Execution Time" : "Végrehajtási idő", + "Execution Time Range" : "Végrehajtási időtartomány", + "Existing Improvements" : "Meglévő fejlesztések", + "Exponential" : "Exponenciális", + "Export" : "Exportálás", + "Export completed successfully" : "Az exportálás sikeresen befejeződött", + "Export, view, or delete audit trails" : "Naplóbejegyzések exportálása, megtekintése vagy törlése", + "Extend Schema" : "Séma kibővítése", + "Extended" : "Kibővített", + "Extensions" : "Bővítmények", + "Extract Pending Files" : "Függőben lévő fájlok kinyerése", + "Extract properties from each object" : "Tulajdonságok kinyerése minden egyes objektumból", + "Extracted At" : "Kinyerés ideje", + "Extraction Status" : "Kinyerés állapota", + "Facet configuration updated successfully" : "Az aspektuskonfiguráció sikeresen frissítve", + "Facets discovered and configured successfully" : "Az aspektusok sikeresen felfedezve és konfigurálva", + "Facets discovered successfully" : "Az aspektusok sikeresen felfedezve", + "Failed" : "Sikertelen", + "Failed to analyze schema properties" : "Nem sikerült elemezni a sématulajdonságokat", + "Failed to archive conversation" : "Nem sikerült archiválni a beszélgetést", + "Failed to calculate sizes" : "Nem sikerült kiszámítani a méreteket", + "Failed to clear collection" : "Nem sikerült törölni a gyűjteményt", + "Failed to copy changes" : "Nem sikerült másolni a módosításokat", + "Failed to copy data" : "Nem sikerült másolni az adatokat", + "Failed to copy data to clipboard" : "Nem sikerült az adatok vágólapra másolása", + "Failed to create conversation" : "Nem sikerült létrehozni a beszélgetést", + "Failed to create or find project" : "Nem sikerült létrehozni vagy megtalálni a projektet", + "Failed to delete collection" : "Nem sikerült törölni a gyűjteményt", + "Failed to delete conversation" : "Nem sikerült törölni a beszélgetést", + "Failed to delete webhook" : "Nem sikerült törölni a webhookot", + "Failed to download API specification" : "Nem sikerült letölteni az API-specifikációt", + "Failed to get SOLR field configuration" : "Nem sikerült lekérni a SOLR-mezőkonfigurációt", + "Failed to get entity categories" : "Nem sikerült lekérni az entitáskategóriákat", + "Failed to get entity statistics" : "Nem sikerült lekérni az entitásstatisztikákat", + "Failed to get entity types" : "Nem sikerült lekérni az entitástípusokat", + "Failed to load LLM configuration" : "Nem sikerült betölteni az LLM-konfigurációt", + "Failed to load Nextcloud groups" : "Nem sikerült betölteni a Nextcloud-csoportokat", + "Failed to load conversation" : "Nem sikerült betölteni a beszélgetést", + "Failed to load entities" : "Nem sikerült betölteni az entitásokat", + "Failed to load entity" : "Nem sikerült betölteni az entitást", + "Failed to load files" : "Nem sikerült betölteni a fájlokat", + "Failed to load organisations" : "Nem sikerült betölteni a szervezeteket", + "Failed to load templates" : "Nem sikerült betölteni a sablonokat", + "Failed to load webhooks" : "Nem sikerült betölteni a webhookokat", + "Failed to load workflows" : "Nem sikerült betölteni a munkafolyamatokat", + "Failed to refresh database information" : "Nem sikerült frissíteni az adatbázis-információkat", + "Failed to reindex collection" : "Nem sikerült újraindexelni a gyűjteményt", + "Failed to rename conversation" : "Nem sikerült átnevezni a beszélgetést", + "Failed to restore conversation" : "Nem sikerült visszaállítani a beszélgetést", + "Failed to retry extraction" : "Nem sikerült újrapróbálni a kinyerést", + "Failed to retry webhook" : "Nem sikerült újrapróbálni a webhookot", + "Failed to save GitHub token" : "Nem sikerült menteni a GitHub-tokent", + "Failed to save GitLab URL" : "Nem sikerült menteni a GitLab-URL-t", + "Failed to save GitLab token" : "Nem sikerült menteni a GitLab-tokent", + "Failed to save additional feedback" : "Nem sikerült menteni a további visszajelzést", + "Failed to save n8n configuration" : "Nem sikerült menteni az n8n-konfigurációt", + "Failed to save roles" : "Nem sikerült menteni a szerepköröket", + "Failed to save settings" : "Nem sikerült menteni a beállításokat", + "Failed to save webhook" : "Nem sikerült menteni a webhookot", + "Failed to send feedback" : "Nem sikerült elküldeni a visszajelzést", + "Failed to test webhook" : "Nem sikerült tesztelni a webhookot", + "Failed to update favorite status" : "Nem sikerült frissíteni a kedvenc állapotát", + "Failed to update schema properties" : "Nem sikerült frissíteni a sématulajdonságokat", + "Failed to update view" : "Nem sikerült frissíteni a nézetet", + "Failed to update webhook" : "Nem sikerült frissíteni a webhookot", + "Feedback recorded" : "A visszajelzés rögzítve", + "Fewer sources (1-3):" : "Kevesebb forrás (1–3):", + "Field" : "Mező", + "File" : "Fájl", + "File Chunk Prediction" : "Fájlrészlet-előrejelzés", + "File Collection" : "Fájlgyűjtemény", + "File Management" : "Fájlkezelés", + "File Name" : "Fájlnév", + "File Path" : "Fájl elérési útja", + "File Type Selection" : "Fájltípus kiválasztása", + "File Vectorization" : "Fájlvektorizálás", + "File Warmup" : "Fájlbemelegítés", + "File actions menu" : "Fájlműveletek menü", + "File anonymized successfully" : "A fájl sikeresen anonimizálva", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "A fájlrészletek vektorizálása még nincs megvalósítva. A részletek készen állnak és tárolva vannak, de a vektorizálási szolgáltatás fejlesztés alatt áll.", + "File collection not configured" : "A fájlgyűjtemény nincs konfigurálva", + "File discovery completed" : "A fájlfelderítés befejeződött", + "File extraction completed" : "A fájlkinyerés befejeződött", + "File extraction queued" : "A fájlkinyerés sorba állítva", + "File indexed successfully" : "A fájl sikeresen indexelve", + "File is already anonymized" : "A fájl már anonimizálva van", + "File not found" : "A fájl nem található", + "File settings updated successfully" : "A fájlbeállítások sikeresen frissítve", + "File sources" : "Fájlforrások", + "File vectorization configuration saved successfully" : "A fájlvektorizálási konfiguráció sikeresen mentve", + "File vectorization started. Check the statistics section for progress." : "A fájlvektorizálás elindult. A folyamatot a statisztikák szakaszban követheti nyomon.", + "File warmup completed" : "A fájlbemelegítés befejeződött", + "Files" : "Fájlok", + "Files with Completed Extraction:" : "Befejezett kinyeréssel rendelkező fájlok:", + "Files → fileCollection, Objects → objectCollection" : "Fájlok → fileCollection, objektumok → objectCollection", + "Filter Audit Trails" : "Naplóbejegyzések szűrése", + "Filter Deleted Items" : "Törölt elemek szűrése", + "Filter Objects" : "Objektumok szűrése", + "Filter Properties" : "Tulajdonságok szűrése", + "Filter Search Trails" : "Keresési naplók szűrése", + "Filter Statistics" : "Szűrési statisztikák", + "Filter and analyze search trail entries" : "Keresési naplóbejegyzések szűrése és elemzése", + "Filter and manage audit trail entries" : "Naplóbejegyzések szűrése és kezelése", + "Filter and manage soft deleted items" : "Lágyan törölt elemek szűrése és kezelése", + "Filter by object ID" : "Szűrés objektumazonosító szerint", + "Filter by search term" : "Szűrés keresési kifejezés szerint", + "Filter by webhook" : "Szűrés webhook szerint", + "Filter data loaded automatically. Use the filters below to refine your search." : "A szűrőadatok automatikusan betöltődtek. A keresés finomításához használja az alábbi szűrőket.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Webhook-eseményindítók szűrése a hasznos adat tulajdonságai szerint (soronként egy, formátum: kulcs: érték)", + "Filtered" : "Szűrve", + "Filters" : "Szűrők", + "Fireworks AI Chat Settings" : "Fireworks AI csevegési beállítások", + "Fireworks AI Embedding Configuration" : "Fireworks AI beágyazási konfiguráció", + "First" : "Első", + "Fixed" : "Rögzített", + "For chat and retrieval-augmented generation" : "Csevegéshez és visszakereséssel kiegészített generáláshoz", + "For vector embeddings and semantic search" : "Vektorbeágyazásokhoz és szemantikus kereséshez", + "Format" : "Formátum", + "Format Issue" : "Formátumprobléma", + "Format constraint is missing" : "Hiányzik a formátum-megszorítás", + "From Date" : "Kezdő dátum", + "From date" : "Kezdő dátum", + "Full data copied to clipboard" : "A teljes adatok a vágólapra másolva", + "General Issue" : "Általános probléma", + "Generate recommendations and confidence scores" : "Ajánlások és megbízhatósági pontszámok létrehozása", + "Generate vectors immediately when new objects are created" : "Vektorok azonnali létrehozása új objektumok létrehozásakor", + "GitHub token is valid" : "A GitHub-token érvényes", + "GitHub token removed successfully" : "A GitHub-token sikeresen eltávolítva", + "GitHub token saved successfully" : "A GitHub-token sikeresen mentve", + "GitLab URL saved successfully" : "A GitLab-URL sikeresen mentve", + "GitLab token is valid" : "A GitLab-token érvényes", + "GitLab token saved successfully" : "A GitLab-token sikeresen mentve", + "HTTP Method" : "HTTP-metódus", + "HTTP method used to send webhook requests" : "A webhook-kérések elküldéséhez használt HTTP-metódus", + "Headers" : "Fejlécek", + "Health" : "Állapot", + "Heartbeat successful - connection kept alive" : "A szívverés sikeres – a kapcsolat fennmaradt", + "Helpful" : "Hasznos", + "Hide Filters" : "Szűrők elrejtése", + "Hide in forms" : "Elrejtés az űrlapokon", + "Hide in list view" : "Elrejtés a listanézetben", + "High" : "Magas", + "High Confidence" : "Magas megbízhatóság", + "Host" : "Kiszolgáló", + "Hourly" : "Óránként", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Milyen mélyen járja be a beágyazott objektumtulajdonságokat (1–20). A magasabb értékek több részletet rögzítenek, de növelik a vektor méretét.", + "How often to check for updates (1-168 hours)" : "Milyen gyakran ellenőrizze a frissítéseket (1–168 óra)", + "How to handle retries for failed webhook deliveries" : "Hogyan kezelje az újrapróbálkozásokat a sikertelen webhook-kézbesítéseknél", + "ID" : "Azonosító", + "ID:" : "Azonosító:", + "IP rate limits cleared successfully" : "Az IP-sebességkorlátok sikeresen törölve", + "Identify properties not in the schema" : "A sémában nem szereplő tulajdonságok azonosítása", + "Immutable" : "Megváltoztathatatlan", + "Import" : "Importálás", + "Import successful" : "Az importálás sikeres", + "Improved Property" : "Javított tulajdonság", + "In use" : "Használatban", + "Inactive" : "Inaktív", + "Include IDs and names of related objects for better contextual search" : "A kapcsolódó objektumok azonosítóinak és neveinek belefoglalása a jobb környezeti kereséshez", + "Include related object references" : "Kapcsolódó objektumhivatkozások belefoglalása", + "Include schema and register metadata" : "Séma- és nyilvántartási metaadatok belefoglalása", + "Initialization failed" : "Az inicializálás sikertelen", + "Initialize Project" : "Projekt inicializálása", + "Inspect Fields" : "Mezők vizsgálata", + "Inspect Index" : "Index vizsgálata", + "Invalid" : "Érvénytelen", + "Invalid batch size. Must be between 1 and 5000" : "Érvénytelen kötegméret. 1 és 5000 között kell lennie", + "Invalid field name provided" : "Érvénytelen mezőnév lett megadva", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Érvénytelen maxObjects. 0 (mind) vagy pozitív számnak kell lennie", + "Invalid:" : "Érvénytelen:", + "Issue" : "Probléma", + "Items per page:" : "Elem oldalanként:", + "Just now" : "Épp most", + "LLM Configuration" : "LLM-konfiguráció", + "LLM actions menu" : "LLM-műveletek menü", + "LLM configuration saved successfully" : "Az LLM-konfiguráció sikeresen mentve", + "LLM features disabled" : "Az LLM-funkciók letiltva", + "LLM features enabled" : "Az LLM-funkciók engedélyezve", + "LLM must be enabled with an embedding provider configured" : "Az LLM-et engedélyezni kell, és be kell állítani egy beágyazási szolgáltatót", + "LLM settings updated successfully" : "Az LLM-beállítások sikeresen frissítve", + "Last" : "Utolsó", + "Last Triggered" : "Utoljára aktiválva", + "Length Range:" : "Hossztartomány:", + "Linear" : "Lineáris", + "Load Advanced Filters" : "Speciális szűrők betöltése", + "Load advanced filters with live data from your search index" : "Speciális szűrők betöltése élő adatokkal a keresési indexből", + "Load view" : "Nézet betöltése", + "Loading ConfigSets..." : "ConfigSet-ek betöltése…", + "Loading advanced filters..." : "Speciális szűrők betöltése…", + "Loading agents..." : "Ügynökök betöltése…", + "Loading application details..." : "Alkalmazás részleteinek betöltése…", + "Loading applications..." : "Alkalmazások betöltése…", + "Loading archived conversations..." : "Archivált beszélgetések betöltése…", + "Loading audit trails..." : "Naplóbejegyzések betöltése…", + "Loading collections..." : "Gyűjtemények betöltése…", + "Loading configurations..." : "Konfigurációk betöltése…", + "Loading conversation..." : "Beszélgetés betöltése…", + "Loading conversations..." : "Beszélgetések betöltése…", + "Loading deleted items..." : "Törölt elemek betöltése…", + "Loading events..." : "Események betöltése…", + "Loading filter data..." : "Szűrőadatok betöltése…", + "Loading groups..." : "Csoportok betöltése…", + "Loading log details..." : "Naplórészletek betöltése…", + "Loading registers..." : "Nyilvántartások betöltése…", + "Loading schemas..." : "Sémák betöltése…", + "Loading search trails..." : "Keresési naplók betöltése…", + "Loading sources..." : "Források betöltése…", + "Loading statistics..." : "Statisztikák betöltése…", + "Loading users..." : "Felhasználók betöltése…", + "Loading views..." : "Nézetek betöltése…", + "Loading..." : "Betöltés…", + "Local" : "Helyi", + "Local Version" : "Helyi verzió", + "Locked" : "Zárolva", + "Locked:" : "Zárolva:", + "Login successful" : "A bejelentkezés sikeres", + "Logs" : "Naplók", + "Low" : "Alacsony", + "Low Confidence" : "Alacsony megbízhatóság", + "Lucene Version:" : "Lucene-verzió:", + "Magic table synchronized successfully" : "A magic tábla sikeresen szinkronizálva", + "Manage Organisation Roles" : "Szervezeti szerepkörök kezelése", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "SOLR-gyűjtemények (adattárolók) kezelése és hozzárendelése objektumokhoz és fájlokhoz.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "A gyűjteményeihez tartozó SOLR ConfigSet-ek (konfigurációs sablonok) kezelése.", + "Manage and configure agents for automated tasks" : "Ügynökök kezelése és konfigurálása automatizált feladatokhoz", + "Manage and monitor file text extraction status" : "A fájlszövegkinyerés állapotának kezelése és figyelése", + "Manage and restore soft deleted items from your registers" : "Lágyan törölt elemek kezelése és visszaállítása a nyilvántartásokból", + "Manage and view detected entities from files and objects" : "Fájlokból és objektumokból felismert entitások kezelése és megtekintése", + "Manage document templates and themes" : "Dokumentumsablonok és témák kezelése", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Az n8n-munkafolyamatok kezelése az OpenRegister automatizálásához. A munkafolyamatok a konfigurált projektben lesznek tárolva.", + "Manage webhooks for event-driven integrations" : "Webhookok kezelése az eseményvezérelt integrációkhoz", + "Manage your applications and modules" : "Alkalmazások és modulok kezelése", + "Manage your data registers and their configurations" : "Adatnyilvántartások és konfigurációik kezelése", + "Manage your data schemas and their properties" : "Adatsémák és tulajdonságaik kezelése", + "Manage your data sources and their configurations" : "Adatforrások és konfigurációik kezelése", + "Manage your saved search configurations" : "Mentett keresési konfigurációk kezelése", + "Manage your system configurations and settings" : "Rendszerkonfigurációk és -beállítások kezelése", + "Managed" : "Felügyelt", + "Max Files (0 = all)" : "Maximális fájlok (0 = mind)", + "Max Length" : "Maximális hossz", + "Max Length:" : "Maximális hossz:", + "Max Objects (0 = all)" : "Maximális objektumok (0 = mind)", + "Max Retries" : "Maximális újrapróbálkozások", + "Max Shards/Node" : "Maximális szilánk/csomópont", + "Max execution time (ms)" : "Maximális végrehajtási idő (ms)", + "Max ms" : "Maximális ms", + "Max result count" : "Maximális találatszám", + "Max results" : "Maximális találatok", + "Maximum" : "Maximum", + "Maximum Nesting Depth" : "Maximális beágyazási mélység", + "Maximum length constraint is missing" : "Hiányzik a maximális hossz megszorítása", + "Maximum length is too restrictive" : "A maximális hossz túl szigorú", + "Maximum number of files to process. Set to 0 to process all files." : "A feldolgozandó fájlok maximális száma. Állítsa 0-ra az összes fájl feldolgozásához.", + "Maximum number of objects to process. Set to 0 to process all objects." : "A feldolgozandó objektumok maximális száma. Állítsa 0-ra az összes objektum feldolgozásához.", + "Maximum number of retry attempts for failed deliveries" : "A sikertelen kézbesítések újrapróbálkozási kísérleteinek maximális száma", + "Maximum time to wait before committing changes" : "A módosítások véglegesítése előtti maximális várakozási idő", + "Maximum tokens to generate" : "A létrehozandó tokenek maximális száma", + "Maximum value constraint is missing" : "Hiányzik a maximális érték megszorítása", + "Maximum value is too restrictive" : "A maximális érték túl szigorú", + "Medium" : "Közepes", + "Medium Confidence" : "Közepes megbízhatóság", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Közepes lekérdezések: keresések némi szűréssel vagy több paraméterrel (pl. dátumtartományok, adott nyilvántartások/sémák)", + "Members of selected groups can access this view" : "A kiválasztott csoportok tagjai hozzáférhetnek ehhez a nézethez", + "Members:" : "Tagok:", + "Memory prediction calculated successfully" : "A memória-előrejelzés sikeresen kiszámítva", + "Message content is required" : "Az üzenet tartalma kötelező", + "Message does not belong to this conversation" : "Az üzenet nem tartozik ehhez a beszélgetéshez", + "Metadata Filters" : "Metaadatszűrők", + "Method" : "Metódus", + "Min Length" : "Minimális hossz", + "Min execution time (ms)" : "Minimális végrehajtási idő (ms)", + "Min ms" : "Minimális ms", + "Min result count" : "Minimális találatszám", + "Min results" : "Minimális találatok", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Hiányzik a minimális érték megszorítása", + "Minimum value is too restrictive" : "A minimális érték túl szigorú", + "Missing object fields created successfully" : "A hiányzó objektummezők sikeresen létrehozva", + "Mode:" : "Mód:", + "Model" : "Modell", + "Model Name" : "Modell neve", + "Modified" : "Módosítva", + "Monthly" : "Havonta", + "More sources (10-20):" : "Több forrás (10–20):", + "Most Active Objects" : "Legaktívabb objektumok", + "Multiple Of" : "Többszöröse", + "Name" : "Név", + "Names cache warmup completed" : "A névgyorsítótár bemelegítése befejeződött", + "New Collection Name" : "Új gyűjtemény neve", + "New Conversation" : "Új beszélgetés", + "New Properties" : "Új tulajdonságok", + "New Property" : "Új tulajdonság", + "New Value" : "Új érték", + "New users without specific organisation membership will be automatically added to this organisation" : "Az adott szervezeti tagsággal nem rendelkező új felhasználók automatikusan hozzáadódnak ehhez a szervezethez", + "Next" : "Következő", + "No" : "Nem", + "No ConfigSets found" : "Nem található ConfigSet", + "No GitHub token configured" : "Nincs GitHub-token konfigurálva", + "No GitHub token provided" : "Nincs megadva GitHub-token", + "No GitLab token provided" : "Nincs megadva GitLab-token", + "No Organisation" : "Nincs szervezet", + "No active filters" : "Nincsenek aktív szűrők", + "No activity data available" : "Nincs elérhető tevékenységi adat", + "No agents available" : "Nincs elérhető ügynök", + "No agents found" : "Nem található ügynök", + "No app store cache exists yet" : "Még nem létezik alkalmazás-áruház gyorsítótár", + "No applications are available." : "Nincs elérhető alkalmazás.", + "No applications found" : "Nem található alkalmazás", + "No archived conversations" : "Nincsenek archivált beszélgetések", + "No audit trail entries found" : "Nem található naplóbejegyzés", + "No changes recorded" : "Nincs rögzített módosítás", + "No chunks to vectorize" : "Nincs vektorizálandó részlet", + "No collections found" : "Nem található gyűjtemény", + "No configuration data" : "Nincs konfigurációs adat", + "No configurations are available." : "Nincs elérhető konfiguráció.", + "No configurations found" : "Nem található konfiguráció", + "No configurations found for this application." : "Nem található konfiguráció ehhez az alkalmazáshoz.", + "No conversations yet. Create a new one to get started!" : "Még nincsenek beszélgetések. A kezdéshez hozzon létre egy újat!", + "No data available" : "Nincs elérhető adat", + "No data available for chart" : "Nincs elérhető adat a diagramhoz", + "No deleted items found" : "Nem található törölt elem", + "No deletion data available" : "Nincs elérhető törlési adat", + "No description found" : "Nem található leírás", + "No description provided" : "Nincs megadva leírás", + "No entities detected in this file. Run text extraction first." : "Nem észlelhető entitás ebben a fájlban. Először futtassa a szövegkinyerést.", + "No entities found" : "Nem található entitás", + "No entities have been detected yet" : "Még nem észleltek entitásokat", + "No events found" : "Nem található esemény", + "No expired audit trails found to clear" : "Nem található törlendő lejárt naplóbejegyzés", + "No facetable fields available. Select a register and schema to see available filters." : "Nincsenek elérhető aspektusozható mezők. Válasszon nyilvántartást és sémát az elérhető szűrők megtekintéséhez.", + "No files found" : "Nem található fájl", + "No files have been extracted yet" : "Még nem nyertek ki fájlokat", + "No files to process" : "Nincs feldolgozandó fájl", + "No files to reindex" : "Nincs újraindexelendő fájl", + "No filters are currently active. This will delete ALL audit trail entries!" : "Jelenleg nincs aktív szűrő. Ez az ÖSSZES naplóbejegyzést törölni fogja!", + "No log entries found" : "Nem található naplóbejegyzés", + "No logs are available for this configuration." : "Nincs elérhető napló ehhez a konfigurációhoz.", + "No logs are available for this source." : "Nincs elérhető napló ehhez a forráshoz.", + "No logs found" : "Nem található napló", + "No mismatched fields found - SOLR schema is properly configured" : "Nem található eltérő mező – a SOLR-séma megfelelően van konfigurálva", + "No objects found" : "Nem található objektum", + "No objects found to vectorize" : "Nem található vektorizálandó objektum", + "No objects selected for deletion" : "Nincs törlésre kiválasztott objektum", + "No objects to delete" : "Nincs törlendő objektum", + "No objects were permanently deleted" : "Nem lett véglegesen törölve objektum", + "No properties found" : "Nem található tulajdonság", + "No properties match your filters." : "Egyetlen tulajdonság sem felel meg a szűrőknek.", + "No purge date set" : "Nincs beállítva tisztítási dátum", + "No register data available" : "Nincs elérhető nyilvántartási adat", + "No registers found" : "Nem található nyilvántartás", + "No registers found for this application." : "Nem található nyilvántartás ehhez az alkalmazáshoz.", + "No relations found" : "Nem található kapcsolat", + "No request body available" : "Nincs elérhető kéréstörzs", + "No response body available" : "Nincs elérhető választörzs", + "No saved views yet. Create one in the Search tab!" : "Még nincsenek mentett nézetek. Hozzon létre egyet a Keresés lapon!", + "No schema data available" : "Nincs elérhető sémaadat", + "No schema selected for exploration" : "Nincs felfedezésre kiválasztott séma", + "No schemas found" : "Nem található séma", + "No schemas found for this application." : "Nem található séma ehhez az alkalmazáshoz.", + "No search terms data available" : "Nincs elérhető keresési kifejezés adat", + "No search trail entries found" : "Nem található keresési naplóbejegyzés", + "No sources are available." : "Nincs elérhető forrás.", + "No sources found" : "Nem található forrás", + "No templates found" : "Nem található sablon", + "No templates have been created yet" : "Még nem hoztak létre sablonokat", + "No valid magic-mapped register+schema combinations found" : "Nem található érvényes magic-leképezésű nyilvántartás+séma kombináció", + "No views found. Create views first before configuring vectorization." : "Nem található nézet. A vektorizálás konfigurálása előtt először hozzon létre nézeteket.", + "No views match your search" : "Egyetlen nézet sem felel meg a keresésnek", + "No webhooks found" : "Nem található webhook", + "No webhooks have been configured yet" : "Még nem konfiguráltak webhookokat", + "No workflows found in this project. Create workflows in the n8n editor." : "Nem található munkafolyamat ebben a projektben. Hozzon létre munkafolyamatokat az n8n-szerkesztőben.", + "None" : "Nincs", + "Not helpful" : "Nem hasznos", + "Not in use" : "Nincs használatban", + "Number Constraints" : "Számmegszorítások", + "Number of chunks to vectorize in one API call (1-100)." : "Egy API-hívásban vektorizálandó részletek száma (1–100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Egy API-hívásban vektorizálandó részletek száma. Magasabb = gyorsabb, de több memória. Ajánlott: 10–50.", + "Number of objects to process in each batch (1-5000)." : "Az egyes kötegekben feldolgozandó objektumok száma (1–5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Egy API-hívásban vektorizálandó objektumok száma. Magasabb = gyorsabb, de több memória. Ajánlott: 10–50.", + "Numeric Range:" : "Számtartomány:", + "Object" : "Objektum", + "Object Collection" : "Objektumgyűjtemény", + "Object Count Prediction" : "Objektumszám-előrejelzés", + "Object ID" : "Objektumazonosító", + "Object Management" : "Objektumkezelés", + "Object Vectorization" : "Objektumvektorizálás", + "Object Warmup" : "Objektumbemelegítés", + "Object collection not configured" : "Az objektumgyűjtemény nincs konfigurálva", + "Object not found" : "Az objektum nem található", + "Object permanently deleted" : "Az objektum véglegesen törölve", + "Object restored successfully" : "Az objektum sikeresen visszaállítva", + "Object settings updated successfully" : "Az objektumbeállítások sikeresen frissítve", + "Object sources" : "Objektumforrások", + "Object unlocked successfully" : "Az objektum zárolása sikeresen feloldva", + "Object vectorization configuration saved successfully" : "Az objektumvektorizálási konfiguráció sikeresen mentve", + "Object vectorized successfully" : "Az objektum sikeresen vektorizálva", + "Object:" : "Objektum:", + "Objects" : "Objektumok", + "Objects Analyzed" : "Elemzett objektumok", + "Objects Distribution" : "Objektumok megoszlása", + "Objects being analyzed" : "Elemzés alatt álló objektumok", + "Objects by Register" : "Objektumok nyilvántartás szerint", + "Objects by Schema" : "Objektumok séma szerint", + "Objects deletion completed successfully" : "Az objektumok törlése sikeresen befejeződött", + "Objects in schema" : "Objektumok a sémában", + "Objects to Process:" : "Feldolgozandó objektumok:", + "Objects to be analyzed" : "Elemzendő objektumok", + "Objects to be deleted" : "Törlendő objektumok", + "Objects to be validated" : "Érvényesítendő objektumok", + "Objects will be serialized as JSON text before vectorization" : "Az objektumok JSON-szövegként szerializálódnak a vektorizálás előtt", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Az objektumok lágyan törlődnek (töröltként megjelölve, de az adatbázisban maradnak). Szükség esetén később visszaállíthatók.", + "Old Value" : "Régi érték", + "Oldest Item (days)" : "Legrégebbi elem (nap)", + "Ollama Chat Settings" : "Ollama csevegési beállítások", + "Ollama Embedding Configuration" : "Ollama beágyazási konfiguráció", + "Ollama URL" : "Ollama URL", + "Only With Changes" : "Csak módosításokkal", + "Open Register Objects" : "Open Register objektumok", + "Open n8n Editor" : "n8n-szerkesztő megnyitása", + "OpenAI Chat Settings" : "OpenAI csevegési beállítások", + "OpenAI Embedding Configuration" : "OpenAI beágyazási konfiguráció", + "OpenRegister Settings" : "OpenRegister beállítások", + "Optional webhook secret for signature verification" : "Nem kötelező webhook-titok az aláírás-ellenőrzéshez", + "Order" : "Sorrend", + "Organisation" : "Szervezet", + "Organisation ID" : "Szervezetazonosító", + "Organisation Members" : "Szervezeti tagok", + "Organisation Statistics" : "Szervezeti statisztikák", + "Organisation created successfully" : "A szervezet sikeresen létrehozva", + "Organisation settings saved successfully" : "A szervezeti beállítások sikeresen mentve", + "Organisations" : "Szervezetek", + "Organization" : "Szervezet", + "Organization ID (Optional)" : "Szervezetazonosító (nem kötelező)", + "Orphaned Items" : "Árva elemek", + "Owner" : "Tulajdonos", + "Owner:" : "Tulajdonos:", + "Parallel Mode (Faster, more resource intensive)" : "Párhuzamos mód (gyorsabb, erőforrás-igényesebb)", + "Parallel:" : "Párhuzamos:", + "Password" : "Jelszó", + "Password for SOLR authentication (optional)" : "Jelszó a SOLR-hitelesítéshez (nem kötelező)", + "Path" : "Elérési út", + "Path in repository" : "Elérési út a tárolóban", + "Path where the configuration file will be saved in the repository" : "Elérési út, ahova a konfigurációs fájl mentve lesz a tárolóban", + "Path where the register OAS file will be saved in the repository" : "Elérési út, ahova a nyilvántartás OAS-fájlja mentve lesz a tárolóban", + "Pattern (regex)" : "Minta (reguláris kifejezés)", + "Pattern Issue" : "Mintaprobléma", + "Pattern constraint is missing" : "Hiányzik a minta-megszorítás", + "Patterns:" : "Minták:", + "Pending" : "Függőben", + "Permanent Delete Mode" : "Végleges törlési mód", + "Permanently Delete" : "Végleges törlés", + "Permanently delete already soft-deleted objects" : "A már lágyan törölt objektumok végleges törlése", + "Permanently delete this conversation?" : "Véglegesen törli ezt a beszélgetést?", + "Person" : "Személy", + "Personal Data" : "Személyes adatok", + "Phone" : "Telefon", + "Photos" : "Fényképek", + "Please create an agent in the" : "Hozzon létre egy ügynököt a", + "Please select an agent to continue" : "A folytatáshoz válasszon egy ügynököt", + "Please select which register and schema to use for the new object" : "Válassza ki, melyik nyilvántartást és sémát használja az új objektumhoz", + "Please try again later." : "Próbálja újra később.", + "Please wait while we fetch your agents." : "Kérjük, várjon, amíg lekérjük az ügynökeit.", + "Please wait while we fetch your applications." : "Kérjük, várjon, amíg lekérjük az alkalmazásait.", + "Please wait while we fetch your configurations." : "Kérjük, várjon, amíg lekérjük a konfigurációit.", + "Please wait while we fetch your deleted items." : "Kérjük, várjon, amíg lekérjük a törölt elemeit.", + "Please wait while we fetch your sources." : "Kérjük, várjon, amíg lekérjük a forrásait.", + "Polls" : "Szavazások", + "Popular Search Terms" : "Népszerű keresési kifejezések", + "Port" : "Port", + "Prerequisites" : "Előfeltételek", + "Press Enter to send, Shift+Enter for new line" : "Nyomja meg az Entert a küldéshez, a Shift+Entert az új sorhoz", + "Previous" : "Előző", + "Private" : "Privát", + "Processes chunks in batches with simulated parallelism." : "A részleteket kötegekben dolgozza fel szimulált párhuzamossággal.", + "Processes file chunks sequentially (safest)." : "A fájlrészleteket egymás után dolgozza fel (legbiztonságosabb).", + "Processes objects in chunks with simulated parallelism." : "Az objektumokat részletekben dolgozza fel szimulált párhuzamossággal.", + "Processes objects sequentially (safest)." : "Az objektumokat egymás után dolgozza fel (legbiztonságosabb).", + "Processing" : "Feldolgozás", + "Processing Limits" : "Feldolgozási korlátok", + "Processing..." : "Feldolgozás…", + "Project initialized successfully" : "A projekt sikeresen inicializálva", + "Project not found. Please initialize first." : "A projekt nem található. Először inicializálja.", + "Properties" : "Tulajdonságok", + "Property Behaviors" : "Tulajdonság viselkedései", + "Property Configuration:" : "Tulajdonságkonfiguráció:", + "Property Filters" : "Tulajdonságszűrők", + "Property Title" : "Tulajdonság címe", + "Property Type" : "Tulajdonság típusa", + "Property can be improved" : "A tulajdonság javítható", + "Protected" : "Védett", + "Provider" : "Szolgáltató", + "Provider is required for testing" : "A teszteléshez szolgáltató szükséges", + "Public" : "Nyilvános", + "Public View" : "Nyilvános nézet", + "Public views can be accessed by anyone in the system" : "A nyilvános nézetekhez a rendszer bárki hozzáférhet", + "Publish" : "Közzététel", + "Publish Objects" : "Objektumok közzététele", + "Publish all objects in this schema" : "A séma összes objektumának közzététele", + "Published" : "Közzétéve", + "Published:" : "Közzétéve:", + "Publishing..." : "Közzététel…", + "Purge" : "Tisztítás", + "Purge Date" : "Tisztítási dátum", + "Query Complexity Distribution" : "Lekérdezésösszetettség megoszlása", + "Query parameter is required" : "A lekérdezési paraméter kötelező", + "RAG" : "RAG", + "RAG Configuration" : "RAG-konfiguráció", + "Rate limits cleared successfully" : "A sebességkorlátok sikeresen törölve", + "Raw Changes Data" : "Nyers módosítási adatok", + "Re-vectorize on object update" : "Újravektorizálás objektumfrissítéskor", + "Read" : "Olvasás", + "Recommendations:" : "Ajánlások:", + "Recommended Type:" : "Ajánlott típus:", + "Recommended: 5 sources" : "Ajánlott: 5 forrás", + "Refresh" : "Frissítés", + "Refresh Data" : "Adatok frissítése", + "Refresh Stats" : "Statisztikák frissítése", + "Refresh Workflows" : "Munkafolyamatok frissítése", + "Refresh dashboard" : "Irányítópult frissítése", + "Refresh database info" : "Adatbázis-információk frissítése", + "Register" : "Nyilvántartás", + "Register ID" : "Nyilvántartás-azonosító", + "Register Statistics" : "Nyilvántartási statisztikák", + "Register Totals" : "Nyilvántartási összesítések", + "Register depublished successfully" : "A nyilvántartás közzététele sikeresen visszavonva", + "Register not found" : "A nyilvántartás nem található", + "Register objects deletion completed successfully" : "A nyilvántartás objektumainak törlése sikeresen befejeződött", + "Register published successfully" : "A nyilvántartás sikeresen közzétéve", + "Register/Schema Usage" : "Nyilvántartás-/sémahasználat", + "Registers" : "Nyilvántartások", + "Reindex" : "Újraindexelés", + "Reindex completed" : "Az újraindexelés befejeződött", + "Reindex completed successfully" : "Az újraindexelés sikeresen befejeződött", + "Reindex failed" : "Az újraindexelés sikertelen", + "Relations" : "Kapcsolatok", + "Remote Version" : "Távoli verzió", + "Remove" : "Eltávolítás", + "Remove filter" : "Szűrő eltávolítása", + "Remove from favorites" : "Eltávolítás a kedvencekből", + "Remove group" : "Csoport eltávolítása", + "Removed" : "Eltávolítva", + "Removed from favorites" : "Eltávolítva a kedvencekből", + "Rename Conversation" : "Beszélgetés átnevezése", + "Rename conversation" : "Beszélgetés átnevezése", + "Replicas" : "Replikák", + "Repository" : "Tároló", + "Request Body" : "Kéréstörzs", + "Request Data" : "Kérésadatok", + "Request Details" : "Kérés részletei", + "Request body must be JSON with a \"query\" field" : "A kéréstörzsnek JSON-nak kell lennie egy „query” mezővel", + "Request timeout in seconds" : "Kérés időkorlátja másodpercben", + "Required field" : "Kötelező mező", + "Required status is inconsistent" : "A kötelező állapot inkonzisztens", + "Rerun Search" : "Keresés újrafuttatása", + "Reset Changes" : "Módosítások visszaállítása", + "Reset Filters" : "Szűrők visszaállítása", + "Response Body" : "Választörzs", + "Response Details" : "Válasz részletei", + "Response Time:" : "Válaszidő:", + "Restore" : "Visszaállítás", + "Restore conversation" : "Beszélgetés visszaállítása", + "Restore or permanently delete items" : "Elemek visszaállítása vagy végleges törlése", + "Result Count Range" : "Találatszám-tartomány", + "Results" : "Találatok", + "Retrieve all objects for this schema" : "A séma összes objektumának lekérése", + "Retry" : "Újrapróbálkozás", + "Retry Failed Extractions" : "Sikertelen kinyerések újrapróbálása", + "Retry Policy" : "Újrapróbálkozási házirend", + "Retry completed" : "Az újrapróbálkozás befejeződött", + "Retrying..." : "Újrapróbálkozás…", + "Risk Level" : "Kockázati szint", + "Roles updated successfully" : "A szerepkörök sikeresen frissítve", + "SOLR Connection Settings" : "SOLR-kapcsolati beállítások", + "SOLR Version:" : "SOLR-verzió:", + "SOLR actions menu" : "SOLR-műveletek menü", + "SOLR base path (usually /solr)" : "SOLR alap-elérési út (általában /solr)", + "SOLR is disabled" : "A SOLR le van tiltva", + "SOLR is not available or not configured" : "A SOLR nem érhető el vagy nincs konfigurálva", + "SOLR logging disabled" : "A SOLR-naplózás letiltva", + "SOLR logging enabled" : "A SOLR-naplózás engedélyezve", + "SOLR search disabled" : "A SOLR-keresés letiltva", + "SOLR search enabled" : "A SOLR-keresés engedélyezve", + "SOLR server hostname or IP address" : "SOLR-kiszolgáló gazdaneve vagy IP-címe", + "SOLR server port number (optional, defaults to 8983)" : "SOLR-kiszolgáló portszáma (nem kötelező, alapértelmezés szerint 8983)", + "SOLR setup completed successfully" : "A SOLR beállítása sikeresen befejeződött", + "SOLR setup failed" : "A SOLR beállítása sikertelen", + "SOLR setup failed - check logs" : "A SOLR beállítása sikertelen – ellenőrizze a naplókat", + "Sample Values:" : "Mintaértékek:", + "Save" : "Mentés", + "Save Configuration" : "Konfiguráció mentése", + "Save Connection Settings" : "Kapcsolati beállítások mentése", + "Save Roles" : "Szerepkörök mentése", + "Save Settings" : "Beállítások mentése", + "Save current search as view" : "Jelenlegi keresés mentése nézetként", + "Saved Views" : "Mentett nézetek", + "Saving..." : "Mentés…", + "Schema" : "Séma", + "Schema ID" : "Sémaazonosító", + "Schema Name" : "Séma neve", + "Schema Statistics" : "Sémastatisztikák", + "Schema depublished successfully" : "A séma közzététele sikeresen visszavonva", + "Schema objects deletion completed successfully" : "A séma objektumainak törlése sikeresen befejeződött", + "Schema published successfully" : "A séma sikeresen közzétéve", + "Schema successfully created" : "A séma sikeresen létrehozva", + "Schema successfully updated" : "A séma sikeresen frissítve", + "Schemas" : "Sémák", + "Scheme" : "Séma", + "Search / Views" : "Keresés / nézetek", + "Search Activity" : "Keresési tevékenység", + "Search Analytics" : "Keresési elemzések", + "Search Entities" : "Entitások keresése", + "Search Files" : "Fájlok keresése", + "Search GitHub" : "Keresés a GitHubon", + "Search GitLab" : "Keresés a GitLabon", + "Search Method" : "Keresési módszer", + "Search Term" : "Keresési kifejezés", + "Search Trail Details" : "Keresési napló részletei", + "Search Trail Management" : "Keresési naplók kezelése", + "Search Trail Statistics" : "Keresési napló statisztikái", + "Search Trails" : "Keresési naplók", + "Search Views" : "Nézetek keresése", + "Search Webhooks" : "Webhookok keresése", + "Search by file name or path" : "Keresés fájlnév vagy elérési út szerint", + "Search by name or URL" : "Keresés név vagy URL szerint", + "Search by value" : "Keresés érték szerint", + "Search in Files" : "Keresés a fájlokban", + "Search in Objects" : "Keresés az objektumokban", + "Search property names..." : "Tulajdonságnevek keresése…", + "Search trail deletion not implemented yet" : "A keresési naplók törlése még nincs megvalósítva", + "Search views..." : "Nézetek keresése…", + "Secret" : "Titok", + "Secret key for HMAC signature generation (optional)" : "Titkos kulcs a HMAC-aláírás létrehozásához (nem kötelező)", + "Select AI Agent" : "MI-ügynök kiválasztása", + "Select All" : "Összes kijelölése", + "Select ConfigSet" : "ConfigSet kiválasztása", + "Select File Types to Vectorize:" : "Válassza ki a vektorizálandó fájltípusokat:", + "Select HTTP method" : "HTTP-metódus kiválasztása", + "Select Register and Schema" : "Nyilvántartás és séma kiválasztása", + "Select Views to Vectorize:" : "Válassza ki a vektorizálandó nézeteket:", + "Select a Nextcloud group to add" : "Válasszon egy hozzáadandó Nextcloud-csoportot", + "Select a branch" : "Válasszon egy ágat", + "Select a model or type a custom model name" : "Válasszon egy modellt, vagy adjon meg egy egyéni modellnevet", + "Select a repository" : "Válasszon egy tárolót", + "Select a repository you have write access to" : "Válasszon egy tárolót, amelyhez írási hozzáférése van", + "Select an AI Agent" : "Válasszon egy MI-ügynököt", + "Select an AI agent to begin chatting with your data." : "Válasszon egy MI-ügynököt az adataival való csevegés megkezdéséhez.", + "Select backend" : "Háttérrendszer kiválasztása", + "Select chat model" : "Csevegési modell kiválasztása", + "Select collection for files" : "Gyűjtemény kiválasztása a fájlokhoz", + "Select collection for objects" : "Gyűjtemény kiválasztása az objektumokhoz", + "Select default organisation" : "Alapértelmezett szervezet kiválasztása", + "Select event to listen to..." : "Válassza ki a figyelendő eseményt…", + "Select model" : "Modell kiválasztása", + "Select options..." : "Válasszon beállításokat…", + "Select period" : "Időszak kiválasztása", + "Select property to send as payload" : "Válassza ki a hasznos adatként küldendő tulajdonságot", + "Select provider" : "Szolgáltató kiválasztása", + "Select registers and schemas to save a view" : "Válasszon nyilvántartásokat és sémákat egy nézet mentéséhez", + "Select retry policy" : "Újrapróbálkozási házirend kiválasztása", + "Select the branch to publish to" : "Válassza ki az ágat, ahová közzéteszi", + "Select the event this webhook should listen to" : "Válassza ki az eseményt, amelyet ennek a webhooknak figyelnie kell", + "Select views to vectorize:" : "Válassza ki a vektorizálandó nézeteket:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Válassza ki, mely Nextcloud-csoportok érhetők el ehhez a szervezethez. Az ezekben a csoportokban lévő felhasználók hozzáférnek a szervezeti erőforrásokhoz.", + "Select which data views the AI can search" : "Válassza ki, mely adatnézetekben kereshet az MI", + "Select which property from the event should be used as the webhook payload data" : "Válassza ki, mely tulajdonságot kell az eseményből a webhook hasznos adataként használni", + "Select which tools the AI can use to perform actions" : "Válassza ki, mely eszközöket használhatja az MI a műveletek végrehajtásához", + "Selected" : "Kiválasztva", + "Selected Groups" : "Kiválasztott csoportok", + "Selected audit trails deleted successfully" : "A kiválasztott naplóbejegyzések sikeresen törölve", + "Selected search trails deleted successfully" : "A kiválasztott keresési naplók sikeresen törölve", + "Selected users can access this view" : "A kiválasztott felhasználók hozzáférhetnek ehhez a nézethez", + "Send additional feedback" : "További visszajelzés küldése", + "Send as CloudEvent" : "Küldés CloudEventként", + "Sensitive PII" : "Érzékeny személyazonosító adatok", + "Serial Mode (Safer, slower)" : "Soros mód (biztonságosabb, lassabb)", + "Serial:" : "Soros:", + "Server Configuration" : "Kiszolgáló-konfiguráció", + "Server Information" : "Kiszolgálóinformációk", + "Settings" : "Beállítások", + "Shards" : "Szilánkok", + "Share" : "Megosztás", + "Share with Groups" : "Megosztás csoportokkal", + "Share with Users" : "Megosztás felhasználókkal", + "Show Filters" : "Szűrők megjelenítése", + "Show less" : "Kevesebb megjelenítése", + "Show only entries with changes" : "Csak a módosításokkal rendelkező bejegyzések megjelenítése", + "Simple" : "Egyszerű", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Egyszerű lekérdezések: alapvető szövegkeresések minimális paraméterekkel (pl. egyetlen keresési kifejezés, speciális szűrők nélkül)", + "Size" : "Méret", + "Slug" : "Rövid azonosító", + "Soft Delete Mode" : "Lágy törlési mód", + "Soft Deleted Items" : "Lágyan törölt elemek", + "SolrCloud mode enabled" : "A SolrCloud mód engedélyezve", + "Source" : "Forrás", + "Sources" : "Források", + "Sources:" : "Források:", + "Standalone SOLR mode" : "Önálló SOLR mód", + "Start Conversation" : "Beszélgetés indítása", + "Start Vectorization" : "Vektorizálás indítása", + "Start a conversation" : "Beszélgetés indítása", + "Starting names cache warmup..." : "A névgyorsítótár bemelegítésének indítása…", + "Starting..." : "Indítás…", + "Statistics" : "Statisztikák", + "Stats calculation not yet implemented" : "A statisztikák kiszámítása még nincs megvalósítva", + "Status" : "Állapot", + "Status Code" : "Állapotkód", + "String Constraints" : "Karakterlánc-megszorítások", + "Success" : "Siker", + "Success Rate" : "Sikerességi arány", + "Success Status" : "Sikerállapot", + "Successful" : "Sikeres", + "Successfully joined organisation" : "Sikeres csatlakozás a szervezethez", + "Sync Table" : "Tábla szinkronizálása", + "Synchronization Settings" : "Szinkronizálási beállítások", + "System Default" : "Rendszer alapértelmezése", + "Technical Description" : "Műszaki leírás", + "Technical description for developers and administrators" : "Műszaki leírás fejlesztők és adminisztrátorok számára", + "Temperature" : "Hőmérséklet", + "Templates" : "Sablonok", + "Test" : "Teszt", + "Test Chat" : "Csevegés tesztelése", + "Test Connection" : "Kapcsolat tesztelése", + "Test Embedding" : "Beágyazás tesztelése", + "Test endpoint executed successfully" : "A tesztvégpont sikeresen végrehajtva", + "Test webhook delivered successfully" : "A tesztwebhook sikeresen kézbesítve", + "Test webhook delivery failed" : "A tesztwebhook kézbesítése sikertelen", + "Test webhook sent successfully" : "A tesztwebhook sikeresen elküldve", + "Testing..." : "Tesztelés…", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "A szövegrészletek a fájlkinyerés során jönnek létre, és az adatbázisban tárolódnak. A vektorizálás beolvassa ezeket az előre felosztott fájlokat, és beágyazásokká alakítja őket.", + "Text extracted successfully" : "A szöveg sikeresen kinyerve", + "Text extraction disabled" : "A szövegkinyerés letiltva", + "The URL where webhook events will be sent" : "Az URL, ahová a webhook-események elküldésre kerülnek", + "The requested conversation does not exist" : "A kért beszélgetés nem létezik", + "The saved view and all its search configuration" : "A mentett nézet és annak teljes keresési konfigurációja", + "There are no audit trail entries matching your current filters." : "Nincsenek a jelenlegi szűrőknek megfelelő naplóbejegyzések.", + "There are no deleted items matching your current filters." : "Nincsenek a jelenlegi szűrőknek megfelelő törölt elemek.", + "There are no search trail entries matching your current filters." : "Nincsenek a jelenlegi szűrőknek megfelelő keresési naplóbejegyzések.", + "There are no webhook log entries matching your filters." : "Nincsenek a szűrőknek megfelelő webhook-naplóbejegyzések.", + "This action cannot be undone." : "Ez a művelet nem vonható vissza.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Ez a művelet nem vonható vissza. Győződjön meg róla, hogy egyetlen gyűjtemény sem használja ezt a ConfigSet-et.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Ennek az ügynöknek nincsenek konfigurálható nézetei vagy eszközei, de a RAG-beállításokat alább módosíthatja.", + "This analysis may take some time" : "Ez az elemzés eltarthat egy ideig", + "This audit trail entry does not contain any change information." : "Ez a naplóbejegyzés nem tartalmaz módosítási információt.", + "This configuration has no data defined." : "Ehhez a konfigurációhoz nincs adat megadva.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ez a végpont elavult. Helyette használjon részletalapú végpontokat.", + "This entity has no relations to objects or files" : "Ennek az entitásnak nincsenek kapcsolatai objektumokkal vagy fájlokkal", + "This is a test webhook from OpenRegister" : "Ez egy tesztwebhook az OpenRegistertől", + "This schema must use Magic Table configuration to sync" : "Ennek a sémának a szinkronizáláshoz Magic Table konfigurációt kell használnia", + "This source has no associated registers." : "Ehhez a forráshoz nem tartoznak nyilvántartások.", + "This will copy the _default ConfigSet with the new name" : "Ez lemásolja a _default ConfigSet-et az új névvel", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Ez véglegesen törli az ÖSSZES beágyazást (vektort) az adatbázisból. Újra kell vektorizálnia az összes objektumot és fájlt. Ez a művelet nem vonható vissza.\\n\\nBiztosan folytatja?", + "This will permanently delete:" : "Ez véglegesen törli:", + "Timeout (seconds)" : "Időkorlát (másodperc)", + "Timestamp" : "Időbélyeg", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tipp: Csak azokat a nézeteket engedélyezze, amelyeknek szemantikus keresésre van szükségük, hogy minimalizálja a beágyazási költségeket. Az egyszerű kikeresési táblák ritkán igényelnek vektorizálást.", + "Title" : "Cím", + "To Date" : "Befejező dátum", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "A részletméret vagy a stratégia módosításához lépjen a Fájlkonfiguráció → Feldolgozási korlátok menüpontra.", + "To change the embedding provider or model, go to LLM Configuration." : "A beágyazási szolgáltató vagy modell módosításához lépjen az LLM-konfigurációhoz.", + "To date" : "Befejező dátum", + "Toggle search sidebar" : "Keresési oldalsáv be-/kikapcsolása", + "Tools" : "Eszközök", + "Top Deleters" : "Legtöbbet törlők", + "Total" : "Összesen", + "Total Audit Trails" : "Naplóbejegyzések összesen", + "Total Chunks Available:" : "Elérhető részletek összesen:", + "Total Deleted Items" : "Törölt elemek összesen", + "Total Members" : "Tagok összesen", + "Total Objects" : "Objektumok összesen", + "Total Objects in Database:" : "Objektumok összesen az adatbázisban:", + "Total Organisations" : "Szervezetek összesen", + "Total Results" : "Találatok összesen", + "Total Searches" : "Keresések összesen", + "Total Size" : "Teljes méret", + "Total Storage" : "Teljes tárhely", + "Total:" : "Összesen:", + "Totals" : "Összesítések", + "Type" : "Típus", + "Type Issue" : "Típusprobléma", + "Type Variations:" : "Típusváltozatok:", + "Type must be \"positive\" or \"negative\"" : "A típusnak „positive” vagy „negative” értékűnek kell lennie", + "Type to search for groups" : "Gépeljen a csoportok kereséséhez", + "Type to search for users" : "Gépeljen a felhasználók kereséséhez", + "Type to search groups..." : "Gépeljen a csoportok kereséséhez…", + "Type to search users..." : "Gépeljen a felhasználók kereséséhez…", + "URL" : "URL", + "URL where Ollama is running" : "Az URL, ahol az Ollama fut", + "UUID:" : "UUID:", + "Unchanged" : "Változatlan", + "Unique Search Terms" : "Egyedi keresési kifejezések", + "Unique Terms" : "Egyedi kifejezések", + "Unknown" : "Ismeretlen", + "Unknown Register" : "Ismeretlen nyilvántartás", + "Unknown Schema" : "Ismeretlen séma", + "Untitled View" : "Cím nélküli nézet", + "Update" : "Frissítés", + "Update Operations" : "Frissítési műveletek", + "Update configuration: ..." : "Konfiguráció frissítése: …", + "Update register OAS: ..." : "Nyilvántartás OAS frissítése: …", + "Update vectors when object data changes (recommended for accurate search)" : "Vektorok frissítése az objektumadatok módosulásakor (a pontos kereséshez ajánlott)", + "Updated" : "Frissítve", + "Updated At" : "Frissítés ideje", + "Updated:" : "Frissítve:", + "Uptime:" : "Üzemidő:", + "Usage Count" : "Használatok száma", + "Use SolrCloud with Zookeeper for distributed search" : "SolrCloud használata Zookeeperrel az elosztott kereséshez", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Használjon szűrőket a naplóbejegyzések szűkítéséhez nyilvántartás, séma, művelettípus, felhasználó, dátumtartomány vagy objektumazonosító szerint.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Használjon szűrőket a törölt elemek szűkítéséhez nyilvántartás, séma, törlési dátum vagy a törlést végző felhasználó szerint.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Használjon szűrőket a keresési naplóbejegyzések szűkítéséhez nyilvántartás, séma, sikerállapot, felhasználó, dátumtartomány, keresési kifejezések vagy teljesítménymutatók szerint.", + "Used By Collections" : "Használó gyűjtemények", + "User" : "Felhasználó", + "User Agent Statistics" : "Felhasználóügynök-statisztikák", + "User Description" : "Felhasználói leírás", + "User rate limits cleared successfully" : "A felhasználói sebességkorlátok sikeresen törölve", + "User removed successfully" : "A felhasználó sikeresen eltávolítva", + "User-friendly description shown in forms and help text" : "Az űrlapokon és súgószövegben megjelenő felhasználóbarát leírás", + "Username" : "Felhasználónév", + "Username for SOLR authentication (optional)" : "Felhasználónév a SOLR-hitelesítéshez (nem kötelező)", + "Users" : "Felhasználók", + "Using Pre-Generated Chunks" : "Előre létrehozott részletek használata", + "Validate" : "Érvényesítés", + "Validate Objects" : "Objektumok érvényesítése", + "Validation completed successfully" : "Az érvényesítés sikeresen befejeződött", + "Validation failed" : "Az érvényesítés sikertelen", + "Value" : "Érték", + "Vector Dimensions" : "Vektordimenziók", + "Vector Search Backend" : "Vektorkeresési háttérrendszer", + "Vector field: _embedding_" : "Vektormező: _embedding_", + "Vectorization Triggers" : "Vektorizálási eseményindítók", + "Vectorize All Files" : "Összes fájl vektorizálása", + "Vectorize All Objects" : "Összes objektum vektorizálása", + "Vectorize all file types" : "Összes fájltípus vektorizálása", + "Vectorize all views" : "Összes nézet vektorizálása", + "Vectorize on object creation" : "Vektorizálás objektum létrehozásakor", + "Vectors will be stored in your existing object and file collections" : "A vektorok a meglévő objektum- és fájlgyűjteményeiben lesznek tárolva", + "Version" : "Verzió", + "Very High" : "Nagyon magas", + "View API Docs" : "API-dokumentáció megtekintése", + "View Changes" : "Módosítások megtekintése", + "View Details" : "Részletek megtekintése", + "View Error" : "Hiba megtekintése", + "View File" : "Fájl megtekintése", + "View Full Details" : "Teljes részletek megtekintése", + "View Logs" : "Naplók megtekintése", + "View Name" : "Nézet neve", + "View Object" : "Objektum megtekintése", + "View Parameters" : "Paraméterek megtekintése", + "View Selection" : "Nézetválasztás", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Keresési naplók megtekintése és elemzése speciális szűrési és elemzési képességekkel", + "View and analyze system audit trails with advanced filtering capabilities" : "Rendszernaplók megtekintése és elemzése speciális szűrési képességekkel", + "View deleted successfully" : "A nézet sikeresen törölve", + "View entity details and manage relations" : "Entitás részleteinek megtekintése és kapcsolatok kezelése", + "View name" : "Nézet neve", + "View name is required" : "A nézet neve kötelező", + "View saved successfully!" : "A nézet sikeresen mentve!", + "View search analytics and manage search logs" : "Keresési elemzések megtekintése és keresési naplók kezelése", + "View successfully deleted" : "A nézet sikeresen törölve", + "View successfully updated" : "A nézet sikeresen frissítve", + "View updated successfully!" : "A nézet sikeresen frissítve!", + "View webhook delivery logs and filter by webhook" : "Webhook-kézbesítési naplók megtekintése és szűrés webhook szerint", + "Views" : "Nézetek", + "Visible to users" : "Látható a felhasználók számára", + "Wait for Response" : "Várakozás a válaszra", + "Wait for webhook response before continuing (required for request/response flows)" : "Várakozás a webhook válaszára a folytatás előtt (kérés/válasz folyamatokhoz szükséges)", + "Warmup Names Cache" : "Névgyorsítótár bemelegítése", + "Warning" : "Figyelmeztetés", + "Warning:" : "Figyelmeztetés:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Webhook-napló részletei", + "Webhook Logs" : "Webhook-naplók", + "Webhook created successfully" : "A webhook sikeresen létrehozva", + "Webhook deleted" : "A webhook törölve", + "Webhook retry delivered successfully" : "A webhook újrapróbálkozása sikeresen kézbesítve", + "Webhook retry delivery failed" : "A webhook újrapróbálkozásának kézbesítése sikertelen", + "Webhook updated" : "A webhook frissítve", + "Webhook updated successfully" : "A webhook sikeresen frissítve", + "Webhooks" : "Webhookok", + "Weekly" : "Hetente", + "Wrap webhook payload in CloudEvents format for better interoperability" : "A webhook hasznos adatainak becsomagolása CloudEvents formátumba a jobb együttműködés érdekében", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Igen", + "You" : "Ön", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Létrehozhat új ConfigSet-eket a _default sablon alapján, vagy feltölthet egyénieket közvetlenül a SOLR-kiszolgálóra.", + "You do not have access to this conversation" : "Nincs hozzáférése ehhez a beszélgetéshez", + "You do not have permission to delete this conversation" : "Nincs engedélye a beszélgetés törléséhez", + "You do not have permission to modify this conversation" : "Nincs engedélye a beszélgetés módosításához", + "You do not have permission to restore this conversation" : "Nincs engedélye a beszélgetés visszaállításához", + "You must be logged in to favorite views" : "A nézetek kedvencekhez adásához be kell jelentkeznie", + "You need an AI agent to start a conversation." : "Egy MI-ügynökre van szüksége a beszélgetés megkezdéséhez.", + "Your Fireworks AI API key. Get one at" : "Az Ön Fireworks AI API-kulcsa. Szerezzen be egyet itt:", + "Your OpenAI API key. Get one at" : "Az Ön OpenAI API-kulcsa. Szerezzen be egyet itt:", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "A visszajelzését rögzítettük. Igény szerint további részleteket adhat meg itt…", + "Zookeeper Hosts" : "Zookeeper-kiszolgálók", + "Zookeeper Port" : "Zookeeper-port", + "Zookeeper Settings (SolrCloud)" : "Zookeeper-beállítások (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper-kapcsolati karakterlánc a SolrCloudhoz", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper-portszám (nem kötelező, alapértelmezés szerint 2181)", + "chunk" : "részlet", + "chunks" : "részletek", + "chunks processed" : "részlet feldolgozva", + "configuration(s)" : "konfiguráció", + "e.g., lib/Settings/config.json" : "pl. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "pl. lib/Settings/register.json", + "failed" : "sikertelen", + "file" : "fájl", + "files" : "fájlok", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "tagok", + "menu or contact someone with permission to create agents." : "menüben, vagy forduljon valakihez, akinek engedélye van ügynökök létrehozására.", + "messages" : "üzenetek", + "n8n URL and API key are required" : "Az n8n URL és az API-kulcs kötelező", + "n8n configuration saved successfully" : "Az n8n-konfiguráció sikeresen mentve", + "n8n connection not configured" : "Az n8n-kapcsolat nincs konfigurálva", + "n8n connection successful" : "Az n8n-kapcsolódás sikeres", + "n8n connection test successful" : "Az n8n-kapcsolatteszt sikeres", + "n8n integration disabled" : "Az n8n-integráció letiltva", + "n8n integration enabled" : "Az n8n-integráció engedélyezve", + "n8n project initialized successfully" : "Az n8n-projekt sikeresen inicializálva", + "n8n settings saved successfully" : "Az n8n-beállítások sikeresen mentve", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objektumok", + "objects processed" : "objektum feldolgozva", + "or pick a range" : "vagy válasszon egy tartományt", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "jó egyensúlyt biztosít a sebesség és a pontosság között.", + "register(s)" : "nyilvántartás", + "register(s) selected" : "nyilvántartás kiválasztva", + "required" : "kötelező", + "results" : "találatok", + "schema(s)" : "séma", + "schema(s) selected" : "séma kiválasztva", + "sk-..." : "sk-...", + "this application" : "ez az alkalmazás", + "to" : "ig", + "vectorized" : "vektorizálva", + "views selected" : "nézet kiválasztva", + "ℹ️ Current Configuration" : "ℹ️ Jelenlegi konfiguráció", + "⚡ Batch Processing" : "⚡ Kötegelt feldolgozás", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Átfogó környezet ✓ Kevésbé valószínű, hogy lemarad részletekről ✗ Lassabb válaszok ✗ Kevésbé releváns információkat tartalmazhat", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Gyorsabb válaszok ✓ Fókuszáltabb válaszok ✗ Lemaradhat releváns információkról", + "✨ AI Features" : "✨ MI-funkciók", + "All search trails cleared successfully" : "Minden keresési napló sikeresen törölve", + "Multiple search trail deletion not implemented yet" : "Több keresési napló törlése még nincs megvalósítva", + "No expired search trails found to clear" : "Nem található törlendő lejárt keresési napló", + "Missing message" : "Hiányzó üzenet", + "message content is required" : "az üzenet tartalma kötelező", + "Missing conversation or agentUuid" : "Hiányzó beszélgetés vagy agentUuid", + "Access denied" : "Hozzáférés megtagadva", + "Conversation not found" : "A beszélgetés nem található", + "AI service not configured" : "Az MI-szolgáltatás nincs konfigurálva", + "Failed to process message" : "Nem sikerült feldolgozni az üzenetet", + "Missing conversationId" : "Hiányzó conversationId", + "conversationId is required" : "A conversationId kötelező", + "Failed to fetch conversation history" : "Nem sikerült lekérni a beszélgetési előzményeket", + "Failed to clear conversation" : "Nem sikerült törölni a beszélgetést", + "Invalid feedback type" : "Érvénytelen visszajelzéstípus", + "type must be \"positive\" or \"negative\"" : "a típusnak „positive” vagy „negative” értékűnek kell lennie", + "Message not found" : "Az üzenet nem található", + "Failed to save feedback" : "Nem sikerült menteni a visszajelzést", + "Failed to get chat statistics" : "Nem sikerült lekérni a csevegési statisztikákat", + "Not Found" : "Nem található", + "Engine not found" : "A motor nem található", + "Consumer not found" : "A fogyasztó nem található", + "Backend parameter is required" : "A háttérrendszer paramétere kötelező", + "Failed to get database information: %s" : "Nem sikerült lekérni az adatbázis-információkat: %s", + "SOLR setup error: %s" : "SOLR-beállítási hiba: %s", + "Reindex failed: %s" : "Az újraindexelés sikertelen: %s", + "Anonymized" : "Anonimizálva", + "Entities detected" : "Entitások felismerve", + "Extract Now" : "Kinyerés most", + "Extracted at" : "Kinyerés ideje", + "Extraction" : "Kinyerés", + "Failed to load extraction data" : "Nem sikerült betölteni a kinyerési adatokat", + "Failed to load register data" : "Nem sikerült betölteni a nyilvántartási adatokat", + "Failed to retrieve extraction status." : "Nem sikerült lekérni a kinyerés állapotát.", + "Failed to retrieve objects for file." : "Nem sikerült lekérni a fájlhoz tartozó objektumokat.", + "No extraction data available for this file" : "Nincs elérhető kinyerési adat ehhez a fájlhoz", + "No register objects reference this file" : "Egyetlen nyilvántartási objektum sem hivatkozik erre a fájlra", + "Not extracted" : "Nincs kinyerve", + "Register Objects" : "Nyilvántartási objektumok", + "Risk level" : "Kockázati szint", + "Text chunks" : "Szövegrészletek", + "Unknown error" : "Ismeretlen hiba", + "Very high" : "Nagyon magas", + "{title} in {register} / {schema}" : "{title} ebben: {register} / {schema}", + "Push Notifications" : "Leküldéses értesítések", + "Real-time push notification status via notify_push" : "Valós idejű leküldéses értesítés állapota a notify_push révén", + "Realtime push not available — the notify_push app is not installed" : "A valós idejű leküldés nem érhető el – a notify_push alkalmazás nincs telepítve", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Telepítse a notify_push alkalmazást a Nextcloud alkalmazás-áruházból a valós idejű frissítések engedélyezéséhez.", + "Open Nextcloud App Store" : "Nextcloud alkalmazás-áruház megnyitása", + "notify_push is installed but not yet active" : "A notify_push telepítve van, de még nem aktív", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "A notify_push telepítve van, de az OpenRegister még nem erősített meg sikeres leküldést. Az aktiváláshoz indítson el egy objektummentést, vagy ellenőrizze a notify_push konfigurációját.", + "notify_push configuration guide" : "notify_push konfigurációs útmutató", + "Realtime push active" : "A valós idejű leküldés aktív", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "A valós idejű leküldéses értesítések aktívak. A csatlakoztatott kliensek azonnali frissítéseket kapnak, amikor objektumok jönnek létre, frissülnek vagy törlődnek.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/hu.json b/l10n/hu.json new file mode 100644 index 0000000000..df77af4dce --- /dev/null +++ b/l10n/hu.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin szükséges)", + "(no subject)": "(nincs tárgy)", + "(no title)": "(nincs cím)", + "(unnamed)": "(névtelen)", + "(untitled card)": "(cím nélküli kártya)", + "({count} selected)": "({count} kiválasztva)", + ", {failed} failed": ", {failed} sikertelen", + "0 = unlimited": "0 = korlátlan", + "A deactivation request is pending.": "Egy deaktiválási kérelem függőben van.", + "AI Assistant": "AI asszisztens", + "AI Chat": "AI csevegés", + "AI service not configured": "Az AI szolgáltatás nincs konfigurálva", + "API Key": "API kulcs", + "API Request Quota (requests/day)": "API kérési kvóta (kérés/nap)", + "API Token Configuration": "API token konfiguráció", + "API Tokens": "API tokenek", + "API tokens saved successfully": "Az API tokenek sikeresen mentve", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: ez a jelentés minden verwerkingsactiviteit-et összekapcsol a hozzá tartozó teljes élettartamú auditnapló-számokkal. Az auditorok és az Autoriteit Persoonsgegevens ezt felügyeleti felülvizsgálatra használják.", + "About ConfigSets": "A ConfigSet-ekről", + "Access denied": "Hozzáférés megtagadva", + "Access organisation dashboard and analytics": "Szervezeti irányítópult és elemzések elérése", + "Account": "Fiók", + "Action": "Művelet", + "Action Distribution": "Műveletek eloszlása", + "Action:": "Művelet:", + "Actions": "Műveletek", + "Active": "Aktív", + "Active Collections": "Aktív gyűjtemények", + "Active Filters": "Aktív szűrők", + "Active Organisation:": "Aktív szervezet:", + "Active Organisations": "Aktív szervezetek", + "Active filters:": "Aktív szűrők:", + "Active organisation changed successfully": "Az aktív szervezet sikeresen módosítva", + "Active organisation set successfully": "Az aktív szervezet sikeresen beállítva", + "Activities": "Tevékenységek", + "Activity": "Tevékenység", + "Activity Period": "Tevékenységi időszak", + "Add": "Hozzáadás", + "Add Attachment": "Melléklet hozzáadása", + "Add Endpoint": "Végpont hozzáadása", + "Add Groups": "Csoportok hozzáadása", + "Add Property to '{title}'": "Tulajdonság hozzáadása ehhez: '{title}'", + "Add Register": "Nyilvántartás hozzáadása", + "Add Schema": "Séma hozzáadása", + "Add Source": "Forrás hozzáadása", + "Add User": "Felhasználó hozzáadása", + "Add User to Organisation": "Felhasználó hozzáadása a szervezethez", + "Add a contact from any of your address books to associate it with this object.": "Adjon hozzá egy névjegyet bármelyik címjegyzékéből, hogy összekapcsolja ezzel az objektummal.", + "Add a file or files": "Fájl vagy fájlok hozzáadása", + "Add card": "Kártya hozzáadása", + "Add contact": "Névjegy hozzáadása", + "Add endpoint": "Végpont hozzáadása", + "Add oneOf entry": "oneOf bejegyzés hozzáadása", + "Add range": "Tartomány hozzáadása", + "Add schema titles, descriptions, and register information to provide richer context for search": "Séma címek, leírások és nyilvántartási információk hozzáadása a gazdagabb keresési kontextus érdekében", + "Add to favorites": "Hozzáadás a kedvencekhez", + "Added": "Hozzáadva", + "Added to favorites": "Hozzáadva a kedvencekhez", + "Additional Information": "További információk", + "Additional feedback saved. Thank you!": "A további visszajelzés mentve. Köszönjük!", + "Addresses": "Címek", + "Admin": "Adminisztrátor", + "Advanced": "Speciális", + "Advanced Filters": "Speciális szűrők", + "Advanced Options": "Speciális beállítások", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "A speciális szűrők nem érhetők el adatbázis-forrás használatakor. A szűrési lehetőségekhez váltson Auto vagy SOLR index módra.", + "After": "Után", + "Agent Type": "Ügynök típusa", + "Agent deleted successfully": "Az ügynök sikeresen törölve", + "Agent successfully created": "Az ügynök sikeresen létrehozva", + "Agent successfully updated": "Az ügynök sikeresen frissítve", + "Agents": "Ügynökök", + "Aggregated across schemas": "Sémák között összesítve", + "All": "Összes", + "All Categories": "Összes kategória", + "All Collections": "Összes gyűjtemény", + "All Confidence Levels": "Összes megbízhatósági szint", + "All ConfigSets": "Összes ConfigSet", + "All Files": "Összes fájl", + "All Levels": "Összes szint", + "All Types": "Összes típus", + "All Webhooks": "Összes webhook", + "All actions": "Összes művelet", + "All audit trails cleared successfully": "Az összes naplóbejegyzés sikeresen törölve", + "All clear": "Minden rendben", + "All file types": "Összes fájltípus", + "All registers": "Összes nyilvántartás", + "All schemas": "Összes séma", + "All search trails cleared successfully": "Az összes keresési napló sikeresen törölve", + "All searches": "Összes keresés", + "All users": "Összes felhasználó", + "All-day events": "Egész napos események", + "Allowed MIME Types": "Engedélyezett MIME-típusok", + "Already linked": "Már összekapcsolva", + "An error occurred while clearing audit trails": "Hiba történt a naplóbejegyzések törlése közben", + "An error occurred while deleting the audit trail": "Hiba történt a naplóbejegyzés törlése közben", + "An error occurred while deleting the object": "Hiba történt az objektum törlése közben", + "An error occurred while deleting the view": "Hiba történt a nézet törlése közben", + "An error occurred while permanently deleting the objects": "Hiba történt az objektumok végleges törlése közben", + "An error occurred while restoring the objects": "Hiba történt az objektumok visszaállítása közben", + "An error occurred while saving the organisation": "Hiba történt a szervezet mentése közben", + "Analysis completed:": "Elemzés befejezve:", + "Analysis steps:": "Elemzési lépések:", + "Analytics": "Elemzések", + "Analyze Objects": "Objektumok elemzése", + "Analyze Properties": "Tulajdonságok elemzése", + "Analyze existing properties for improvement opportunities": "Meglévő tulajdonságok elemzése a fejlesztési lehetőségek érdekében", + "Analyzing...": "Elemzés...", + "Anonymized": "Anonimizálva", + "Anonymous Group": "Névtelen csoport", + "Any favorites and sharing settings for this view": "A nézethez tartozó kedvencek és megosztási beállítások", + "Any user": "Bármely felhasználó", + "App ID": "Alkalmazás ID", + "App store cache invalidated successfully": "Az alkalmazásbolt gyorsítótára sikeresen érvénytelenítve", + "App store cache invalidated: {invalidated}": "Az alkalmazásbolt gyorsítótára érvénytelenítve: {invalidated}", + "Application deleted successfully": "Az alkalmazás sikeresen törölve", + "Application identifier for this configuration (optional)": "Alkalmazásazonosító ehhez a konfigurációhoz (opcionális)", + "Application settings": "Alkalmazás beállításai", + "Application successfully created": "Az alkalmazás sikeresen létrehozva", + "Application successfully updated": "Az alkalmazás sikeresen frissítve", + "Applications": "Alkalmazások", + "Apply": "Alkalmaz", + "Apply Changes": "Módosítások alkalmazása", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Szerepkör alkalmazása a nyilvántartás összes olyan sémájára, amelynek nincs kifejezett jogosultsági felülbírálása.", + "Approval Progress": "Jóváhagyás folyamata", + "Approve": "Jóváhagyás", + "Approved": "Jóváhagyva", + "Archive": "Archiválás", + "Archive conversation": "Beszélgetés archiválása", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Archiválja ezt a verwerkingsactiviteit-et? Az auditnapló sorai továbbra is hivatkozni fognak rá.", + "Archived conversations are hidden from your active list": "Az archivált beszélgetések el vannak rejtve az aktív listáról", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Biztosan TÖRÖLNI szeretné a(z) \"{name}\" gyűjteményt?\n\nEz a következőket teszi:\n• Véglegesen törli a gyűjteményt és annak összes adatát\n• Eltávolítja az összes indexelt dokumentumot\n• Ezt a műveletet nem lehet visszavonni", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Biztosan meg szeretné tisztítani a régi keresési naplókat? Ez törli a 30 napnál régebbi bejegyzéseket.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Biztosan törölni szeretné az összes adatot a(z) \"{name}\" gyűjteményből?\n\nEz a következőket teszi:\n• Törli az összes indexelt dokumentumot\n• Megőrzi a gyűjtemény szerkezetét\n• Ezt a műveletet nem lehet visszavonni", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Biztosan törölni szeretné a(z) \"{name}\" alkalmazást? Ezt a műveletet nem lehet visszavonni.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Biztosan törölni szeretné a kiválasztott naplóbejegyzéseket? Ezt a műveletet nem lehet visszavonni.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Biztosan törölni szeretné a kiválasztott keresési naplókat? Ezt a műveletet nem lehet visszavonni.", + "Are you sure you want to delete this ConfigSet?": "Biztosan törölni szeretné ezt a ConfigSet-et?", + "Are you sure you want to delete this configset?": "Biztosan törölni szeretné ezt a configset-et?", + "Are you sure you want to leave '{name}'?": "Biztosan el szeretné hagyni a(z) '{name}' szervezetet?", + "Are you sure you want to permanently delete": "Biztosan véglegesen törölni szeretné", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Biztosan újra szeretné indexelni a(z) \"{name}\" gyűjteményt?\n\nEz a következőket teszi:\n• Újraépíti az indexet az összes objektummal\n• Több percig tart, amíg befejeződik\n• Befolyásolhatja a keresési teljesítményt az újraindexelés során", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Biztosan el szeretné távolítani a(z) \"{schema}\" sémát a(z) \"{register}\" nyilvántartásból? Ezt a műveletet nem lehet visszavonni.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Biztosan logikailag törölni szeretne {count} aktív objektumot a(z) \"{schema}\" sémához?\n\nTöröltként lesznek megjelölve, de később véglegesen eltávolíthatók.", + "Array Object Configuration:": "Tömbobjektum konfiguráció:", + "Articles": "Cikkek", + "Ask a question...": "Tegyen fel egy kérdést...", + "Ask questions about your data using natural language": "Tegyen fel kérdéseket az adatairól természetes nyelven", + "Assigned collaborative tags": "Hozzárendelt együttműködési címkék", + "Assignment notifications": "Hozzárendelési értesítések", + "Attachment added to {name}": "Melléklet hozzáadva ehhez: {name}", + "Attempt": "Próbálkozás", + "Audit Trail Changes": "Naplóbejegyzés módosításai", + "Audit Trail Details": "Naplóbejegyzés részletei", + "Audit Trail Management": "Naplóbejegyzések kezelése", + "Audit Trail Statistics": "Naplóbejegyzés-statisztikák", + "Audit Trails": "Naplóbejegyzések", + "Audit trail data copied to clipboard": "A naplóbejegyzés adatai a vágólapra másolva", + "Audit trail deleted successfully": "A naplóbejegyzés sikeresen törölve", + "Audit trail successfully deleted": "A naplóbejegyzés sikeresen törölve", + "Authentication": "Hitelesítés", + "Auto (e.g. Y for year, F Y for month)": "Auto (pl. Y az évhez, F Y a hónaphoz)", + "Auto share": "Automatikus megosztás", + "Auto-commit disabled": "Automatikus véglegesítés letiltva", + "Auto-commit enabled": "Automatikus véglegesítés engedélyezve", + "Auto-create Default Organisation": "Alapértelmezett szervezet automatikus létrehozása", + "Auto-retry failed vectorizations": "Sikertelen vektorizálások automatikus újrapróbálása", + "Automatically commit changes to SOLR index": "Módosítások automatikus véglegesítése a SOLR indexbe", + "Automatically connect to the federation network.": "Automatikus csatlakozás a föderációs hálózathoz.", + "Automatically create a default organisation if none exists when the app is initialized": "Automatikusan létrehoz egy alapértelmezett szervezetet, ha nincs ilyen az alkalmazás inicializálásakor", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Vektorbeágyazások automatikus generálása szövegrészletekből, amikor a fájlokat feltöltik és feldolgozzák", + "Automatically generate vector embeddings when objects are created or updated": "Vektorbeágyazások automatikus generálása az objektumok létrehozásakor vagy frissítésekor", + "Automatically retry failed vectorization attempts (max 3 retries)": "Sikertelen vektorizálási kísérletek automatikus újrapróbálása (legfeljebb 3 újrapróbálkozás)", + "Automatically update catalogues.": "Katalógusok automatikus frissítése.", + "Available Filters": "Elérhető szűrők", + "Available Workflows": "Elérhető munkafolyamatok", + "Available placeholders:": "Elérhető helykitöltők:", + "Avatar": "Avatár", + "Avatar changes are not supported by your authentication provider.": "Az avatár módosítását a hitelesítési szolgáltatója nem támogatja.", + "Avatar removed": "Avatár eltávolítva", + "Avatar updated successfully": "Az avatár sikeresen frissítve", + "Avg Execution Time": "Átl. végrehajtási idő", + "Avg Members/Org": "Átl. tagok/szervezet", + "Avg Object Views/Session": "Átl. objektumnézetek/munkamenet", + "Avg Response Time": "Átl. válaszidő", + "Avg Results/Search": "Átl. találatok/keresés", + "Avg Searches/Session": "Átl. keresések/munkamenet", + "Back": "Vissza", + "Back to Entities": "Vissza az entitásokhoz", + "Back to Registers": "Vissza a nyilvántartásokhoz", + "Back to Webhooks": "Vissza a webhookokhoz", + "Back to applications": "Vissza az alkalmazásokhoz", + "Back to entities": "Vissza az entitásokhoz", + "Backend parameter is required": "A backend paraméter kötelező", + "Backend updated successfully. Please reload the application.": "A backend sikeresen frissítve. Töltse újra az alkalmazást.", + "Bandwidth Quota (MB/month)": "Sávszélesség-kvóta (MB/hónap)", + "Base URL (Optional)": "Alap URL (opcionális)", + "Basic Information": "Alapvető információk", + "Batch Size": "Köteg mérete", + "Batch extraction completed": "A kötegelt kinyerés befejeződött", + "Before": "Előtt", + "Before object vectorization can work:": "Mielőtt az objektum-vektorizálás működne:", + "Behavior": "Viselkedés", + "Behavior Issue": "Viselkedési probléma", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 időtartam, pl. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "A blob tárolás megszűnt. Minden objektum már magic táblákat használ.", + "Bookmarks": "Könyvjelzők", + "Branch": "Ág", + "Bulk Role Assignment: {title}": "Tömeges szerepkör-hozzárendelés: {title}", + "Bulk delete operation completed successfully": "A tömeges törlési művelet sikeresen befejeződött", + "Bulk save operation completed successfully": "A tömeges mentési művelet sikeresen befejeződött", + "Business Data": "Üzleti adatok", + "CSV": "CSV", + "Cache cleared successfully": "A gyorsítótár sikeresen törölve", + "Cache grew from {old} to {new} entries.": "A gyorsítótár {old} bejegyzésről {new} bejegyzésre nőtt.", + "Cache shrunk from {old} to {new} entries.": "A gyorsítótár {old} bejegyzésről {new} bejegyzésre csökkent.", + "Cache stayed the same at {size} entries.": "A gyorsítótár {size} bejegyzésen maradt.", + "Calculate Sizes": "Méretek kiszámítása", + "Calendar": "Naptár", + "Calendar Provider Configuration": "Naptárszolgáltató konfiguráció", + "Calendar integration is not available": "A naptárintegráció nem érhető el", + "Calendar name": "Naptár neve", + "Cancel": "Mégse", + "Cancel deactivation request": "Deaktiválási kérelem visszavonása", + "Cannot delete: objects are still attached": "Nem törölhető: objektumok még csatolva vannak", + "Cannot edit: This register is managed by external configuration {title}": "Nem szerkeszthető: Ezt a nyilvántartást a(z) {title} külső konfiguráció kezeli", + "Cannot remove schema with existing objects ({count} objects)": "Nem távolítható el a séma meglévő objektumokkal ({count} objektum)", + "Cannot remove schema {schema} because it contains {count} objects": "A(z) {schema} séma nem távolítható el, mert {count} objektumot tartalmaz", + "Cards": "Kártyák", + "Cascade delete": "Kaszkádolt törlés", + "Categories of data subjects (one per line)": "Érintettek kategóriái (soronként egy)", + "Categories of personal data (one per line)": "Személyes adatok kategóriái (soronként egy)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategória", + "Change Type": "Módosítás típusa", + "Change password": "Jelszó módosítása", + "Changes": "Módosítások", + "Changes copied to clipboard": "A módosítások a vágólapra másolva", + "Changes for Audit Trail #{id}": "A #{id} naplóbejegyzés módosításai", + "Chat Model": "Csevegési modell", + "Chat Provider": "Csevegési szolgáltató", + "Chat Provider (RAG)": "Csevegési szolgáltató (RAG)", + "Chat Settings": "Csevegési beállítások", + "Chat provider connection successful!": "A csevegési szolgáltatóhoz való kapcsolódás sikeres!", + "Chat settings": "Csevegési beállítások", + "Chat test failed: {error}": "A csevegés tesztelése sikertelen: {error}", + "Choose a register": "Válasszon egy nyilvántartást", + "Choose a schema": "Válasszon egy sémát", + "Choose how vector similarity calculations are performed for semantic search": "Válassza ki, hogyan történjenek a vektorhasonlósági számítások a szemantikus kereséshez", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Válassza ki, mely fájltípusok szerepeljenek a vektorizálási folyamatban. Csak a kinyert szöveget és szövegrészleteket tartalmazó fájlok kerülnek feldolgozásra.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Válassza ki, mely nézetek szerepeljenek a vektorizálási folyamatban. Hagyja üresen az összes nézet feldolgozásához a konfigurációja alapján.", + "Chunk": "Szövegrészlet", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "A szövegrészlet törlése még nincs megvalósítva. Használjon szövegrészlet-alapú végpontokat.", + "Chunks": "Szövegrészletek", + "Chunks to Vectorize:": "Vektorizálandó szövegrészletek:", + "Cleanup Old Trails": "Régi naplók megtisztítása", + "Cleanup completed": "A megtisztítás befejeződött", + "Cleanup completed successfully. Deleted {count} entries.": "A megtisztítás sikeresen befejeződött. {count} bejegyzés törölve.", + "Cleanup failed: {error}": "A megtisztítás sikertelen: {error}", + "Clear All Embeddings": "Összes beágyazás törlése", + "Clear Entries": "Bejegyzések törlése", + "Clear Filtered Audit Trails": "Szűrt naplóbejegyzések törlése", + "Clear Filters": "Szűrők törlése", + "Clear Index": "Index törlése", + "Clear Selection": "Kijelölés törlése", + "Clear all": "Összes törlése", + "Clear all filters": "Összes szűrő törlése", + "Clear date range": "Dátumtartomány törlése", + "Clear filters": "Szűrők törlése", + "Clearing...": "Törlés...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Kattintson a Megfelelőségi vizsgálat futtatása gombra olyan sémák kereséséhez, ahol PII-t észleltek, de nincs feldolgozási tevékenység annotáció.", + "Click to subscribe to notifications": "Kattintson az értesítésekre való feliratkozáshoz", + "Click to unsubscribe from notifications": "Kattintson az értesítésekről való leiratkozáshoz", + "Close": "Bezárás", + "Cluster name": "Fürt neve", + "Code": "Kód", + "Code (short readable key, e.g. v-2026-001)": "Kód (rövid, olvasható kulcs, pl. v-2026-001)", + "Collapse details": "Részletek összecsukása", + "Collection Management": "Gyűjteménykezelés", + "Collection Name": "Gyűjtemény neve", + "Collection assignments updated successfully": "A gyűjtemény-hozzárendelések sikeresen frissítve", + "Collection cleared successfully": "A gyűjtemény sikeresen törölve", + "Collection copied successfully": "A gyűjtemény sikeresen másolva", + "Collection created successfully": "A gyűjtemény sikeresen létrehozva", + "Collection deleted successfully": "A gyűjtemény sikeresen törölve", + "Collection used to store and index file metadata and content": "Fájlok metaadatainak és tartalmának tárolására és indexelésére használt gyűjtemény", + "Collection used to store and index object data": "Objektumadatok tárolására és indexelésére használt gyűjtemény", + "Collections:": "Gyűjtemények:", + "Color": "Szín", + "Comment...": "Megjegyzés...", + "Commit Message": "Véglegesítési üzenet", + "Commit Within (ms)": "Véglegesítés ezen belül (ms)", + "Commit message": "Véglegesítési üzenet", + "Compare current schema with real object data": "Jelenlegi séma összehasonlítása valós objektumadatokkal", + "Completed": "Befejezve", + "Complex": "Összetett", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Összetett lekérdezések: Speciális keresések több szűrővel, operátorral és összetett paraméterkombinációkkal", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Összetett lekérdezések: speciális keresések több szűrővel, operátorral és összetett paraméterkombinációkkal", + "Compliance": "Megfelelőség", + "Confidence Level": "Megbízhatósági szint", + "Confidence Score": "Megbízhatósági pontszám", + "Confidence: {confidence}%": "Megbízhatóság: {confidence}%", + "Config must be provided as an object": "A konfigurációt objektumként kell megadni", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet kezelés", + "ConfigSet Name": "ConfigSet neve", + "ConfigSet created successfully": "A ConfigSet sikeresen létrehozva", + "ConfigSet deleted successfully": "A ConfigSet sikeresen törölve", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "A ConfigSet-ek határozzák meg a SOLR gyűjtemények sémáját és konfigurációját. Meződefiníciókat, elemzőket és egyéb keresési beállításokat tartalmaznak.", + "Configuration": "Konfiguráció", + "Configuration Data": "Konfigurációs adatok", + "Configuration is up to date": "A konfiguráció naprakész", + "Configuration saved": "A konfiguráció mentve", + "Configuration saved successfully.": "A konfiguráció sikeresen mentve.", + "Configuration type (default, application, etc.)": "Konfiguráció típusa (alapértelmezett, alkalmazás, stb.)", + "Configurations": "Konfigurációk", + "Configure API tokens for external service integrations": "API tokenek konfigurálása külső szolgáltatások integrációihoz", + "Configure Apache SOLR search engine for advanced search capabilities": "Az Apache SOLR keresőmotor konfigurálása a speciális keresési képességekhez", + "Configure Facets": "Facetek konfigurálása", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Nagy nyelvi modell (LLM) szolgáltatók konfigurálása az AI-alapú funkciókhoz, beleértve a szemantikus keresést, beágyazásokat és csevegést.", + "Configure SOLR Facets": "SOLR facetek konfigurálása", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Alapvető kapcsolati beállítások konfigurálása a SOLR szerverhez, beleértve a hitelesítési és hálózati lehetőségeket. A magok és gyűjtemények kezeléséhez használja a külön ConfigSet és Gyűjteménykezelés párbeszédpaneleket.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Alapvető kapcsolati beállítások konfigurálása a SOLR szerverhez, beleértve a hitelesítési és hálózati lehetőségeket. A magok és gyűjtemények kezeléséhez használja a külön configset és gyűjteménykezelés párbeszédpaneleket.", + "Configure default organisation and organisation-related settings": "Alapértelmezett szervezet és szervezethez kapcsolódó beállítások konfigurálása", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurálja, hogyan alakulnak át az adatbázis-objektumok vektorbeágyazásokká a szemantikus kereséshez. Az objektumok közvetlenül vektorizálódnak szövegkinyerés nélkül.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurálja, hogyan alakulnak át az objektumok szöveggé a vektorizálás előtt. Ezek a beállítások befolyásolják a keresés minőségét és kontextusát.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Nagy nyelvi modell (LLM) szolgáltatók konfigurálása az AI-alapú funkciókhoz, beleértve a szemantikus keresést, beágyazásokat és csevegést.", + "Configure multi-organization support and tenant isolation": "Több szervezet támogatásának és bérlői izolációnak a konfigurálása", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Paraméterek konfigurálása a fájlvektorizáláshoz. Ez a folyamat vektorbeágyazásokat generál az összes kinyert fájl-szövegrészlethez.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Paraméterek konfigurálása az objektum-vektorizáláshoz. Ez a folyamat vektorbeágyazásokat generál a nézetszűrőinek megfelelő összes objektumhoz.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurálja ezt a sémát, hogy az objektumok eseményekként jelenjenek meg a Nextcloud Naptár alkalmazásban.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurálja, mely adattípusokban keressen és hány forrást kérjen le", + "Configuring field mappings...": "Mezőleképezések konfigurálása...", + "Confirm Account Deactivation": "Fiók deaktiválásának megerősítése", + "Confirm Object Deletion": "Objektum törlésének megerősítése", + "Confirm Object Restoration": "Objektum visszaállításának megerősítése", + "Confirm Permanent Object Deletion": "Objektum végleges törlésének megerősítése", + "Confirm deactivation": "Deaktiválás megerősítése", + "Connecting to SOLR cluster...": "Csatlakozás a SOLR fürthöz...", + "Connection Failed": "A kapcsolat sikertelen", + "Connection Settings": "Kapcsolati beállítások", + "Connection Successful!": "A kapcsolat sikeres!", + "Connection failed": "A kapcsolat sikertelen", + "Connection protocol": "Kapcsolati protokoll", + "Connection successful": "A kapcsolat sikeres", + "Connection timeout in seconds": "Kapcsolati időtúllépés másodpercben", + "Constant delay between retries (5 minutes)": "Állandó késleltetés az újrapróbálkozások között (5 perc)", + "Constraint Issue": "Megszorítási probléma", + "Consumer not found": "A fogyasztó nem található", + "Contacts": "Névjegyek", + "Contacts integration is not available": "A névjegyintegráció nem érhető el", + "Content Filters": "Tartalomszűrők", + "Continue to Properties": "Tovább a tulajdonságokhoz", + "Contracts": "Szerződések", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Szabályozza, mely objektumnézeteket kell vektorizálni az API költségek csökkentése érdekében. Csak azokat a nézeteket vektorizálja, amelyek a szemantikus keresésből profitálnak.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Szabályozza, mely nézeteket és eszközöket használhat az AI ebben a beszélgetésben. Alapértelmezés szerint az összes ügynökképesség engedélyezve van.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Szabályozza a véletlenszerűséget (0 = determinisztikus, 2 = nagyon kreatív)", + "Conversation ID is required": "A beszélgetés ID kötelező", + "Conversation archived": "A beszélgetés archiválva", + "Conversation archived successfully": "A beszélgetés sikeresen archiválva", + "Conversation cleared successfully": "A beszélgetés sikeresen törölve", + "Conversation deleted": "A beszélgetés törölve", + "Conversation not found": "A beszélgetés nem található", + "Conversation permanently deleted": "A beszélgetés véglegesen törölve", + "Conversation renamed": "A beszélgetés átnevezve", + "Conversation restored": "A beszélgetés visszaállítva", + "Conversation started with {agent}": "Beszélgetés elkezdve a következővel: {agent}", + "Conversation title": "Beszélgetés címe", + "Conversations": "Beszélgetések", + "Converting {schema} to blob storage...": "A(z) {schema} átalakítása blob tárolóvá...", + "Converting {schema} to magic table...": "A(z) {schema} átalakítása magic táblává...", + "Copied!": "Másolva!", + "Copy": "Másolás", + "Copy Changes": "Módosítások másolása", + "Copy Collection": "Gyűjtemény másolása", + "Copy Data": "Adatok másolása", + "Copy Full Data": "Teljes adatok másolása", + "Copy of {name}": "A(z) {name} másolata", + "Copy to clipboard": "Másolás a vágólapra", + "Copying...": "Másolás...", + "Costs": "Költségek", + "Count": "Darabszám", + "Counting objects...": "Objektumok számolása...", + "Create": "Létrehozás", + "Create API Token": "API token létrehozása", + "Create Agent": "Ügynök létrehozása", + "Create Application": "Alkalmazás létrehozása", + "Create Collection": "Gyűjtemény létrehozása", + "Create ConfigSet": "ConfigSet létrehozása", + "Create New Collection": "Új gyűjtemény létrehozása", + "Create New ConfigSet": "Új ConfigSet létrehozása", + "Create Operations": "Műveletek létrehozása", + "Create Organisation": "Szervezet létrehozása", + "Create Webhook": "Webhook létrehozása", + "Create a copy of collection:": "Gyűjtemény másolatának létrehozása:", + "Create a new ConfigSet based on the _default template": "Új ConfigSet létrehozása a _default sablon alapján", + "Create a new SOLR collection from an existing ConfigSet": "Új SOLR gyűjtemény létrehozása meglévő ConfigSet alapján", + "Create a new SOLR collection from an existing configset": "Új SOLR gyűjtemény létrehozása meglévő configset alapján", + "Create a new configset based on the _default template": "Új configset létrehozása a _default sablon alapján", + "Create a new event or link an existing one from any of your calendars.": "Hozzon létre új eseményt, vagy kapcsoljon hozzá egy meglévőt bármelyik naptárából.", + "Create another": "Másik létrehozása", + "Create copies of": "Másolatok létrehozása", + "Create event": "Esemény létrehozása", + "Create new token": "Új token létrehozása", + "Create or link a Deck card to track work on this object.": "Hozzon létre vagy kapcsoljon egy Deck kártyát az objektumon végzett munka nyomon követéséhez.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Hozza létre az első verwerkingsactiviteit elemet, hogy elkezdhesse a naplóbejegyzés-sorok megjelölését az AVG Art 30 hozzárendelésükkel.", + "Create your first AI agent to get started.": "Hozza létre első AI ügynökét a kezdéshez.", + "Created": "Létrehozva", + "Created:": "Létrehozva:", + "Creating search collections...": "Keresési gyűjtemények létrehozása...", + "Creating...": "Létrehozás...", + "Current": "Jelenlegi", + "Current Filters": "Jelenlegi szűrők", + "Current Type:": "Jelenlegi típus:", + "Current password": "Jelenlegi jelszó", + "Custom API endpoint if using a different region": "Egyéni API végpont eltérő régió használata esetén", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Egyéni HTTP fejlécek (soronként egy, formátum: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Egyéni HTTP fejlécek (soronként egy, formátum: header-name: value)", + "Custom Ranges:": "Egyéni tartományok:", + "Custom display title for this facet": "Egyéni megjelenítési cím ehhez a facethez", + "Custom pattern:": "Egyéni minta:", + "DELETE COLLECTION": "GYŰJTEMÉNY TÖRLÉSE", + "DSAR": "DSAR", + "Daily": "Naponta", + "Dashboard": "Irányítópult", + "Dashboard not found": "Az irányítópult nem található", + "Data": "Adatok", + "Data Source": "Adatforrás", + "Data Sources": "Adatforrások", + "Data Views": "Adatnézetek", + "Data sources": "Adatforrások", + "Data type does not match observed values": "Az adattípus nem egyezik a megfigyelt értékekkel", + "Data-subject access request": "Érintetti hozzáférési kérelem", + "Database URL": "Adatbázis URL", + "Database information refreshed": "Az adatbázis-információk frissítve", + "Date Faceting:": "Dátum facetelés:", + "Date Range": "Dátumtartomány", + "Dates": "Dátumok", + "Deactivation request cancelled": "A deaktiválási kérelem visszavonva", + "Deactivation request submitted": "A deaktiválási kérelem elküldve", + "Deck": "Deck", + "Deck integration is not available": "A Deck integráció nem érhető el", + "Default": "Alapértelmezett", + "Default Object Tenant": "Alapértelmezett objektumbérlő", + "Default Organisation": "Alapértelmezett szervezet", + "Default Owner": "Alapértelmezett tulajdonos", + "Default User (for cron/background jobs)": "Alapértelmezett felhasználó (cron/háttérfeladatokhoz)", + "Default User Tenant": "Alapértelmezett felhasználói bérlő", + "Default value": "Alapértelmezett érték", + "Default value (Email)": "Alapértelmezett érték (e-mail)", + "Default value (Password)": "Alapértelmezett érték (jelszó)", + "Default value (Phone number)": "Alapértelmezett érték (telefonszám)", + "Default value (Regex)": "Alapértelmezett érték (regex)", + "Default: 5": "Alapértelmezett: 5", + "Defaults to current user. Select a different user if needed.": "Alapértelmezés szerint a jelenlegi felhasználó. Szükség esetén válasszon másik felhasználót.", + "Delays double with each attempt (2, 4, 8 minutes...)": "A késleltetés minden próbálkozással megduplázódik (2, 4, 8 perc...)", + "Delays increase linearly (5, 10, 15 minutes...)": "A késleltetés lineárisan növekszik (5, 10, 15 perc...)", + "Delete": "Törlés", + "Delete ({count})": "Törlés ({count})", + "Delete Application": "Alkalmazás törlése", + "Delete Audit Trail": "Naplóbejegyzés törlése", + "Delete Collection": "Gyűjtemény törlése", + "Delete ConfigSet": "ConfigSet törlése", + "Delete Objects": "Objektumok törlése", + "Delete Operations": "Műveletek törlése", + "Delete SOLR Collection": "SOLR gyűjtemény törlése", + "Delete View": "Nézet törlése", + "Delete all objects in this schema": "Az összes objektum törlése ebben a sémában", + "Delete permanently": "Végleges törlés", + "Delete this conversation?": "Törli ezt a beszélgetést?", + "Delete view": "Nézet törlése", + "Delete {count} object": "{count} objektum törlése", + "Delete {count} objects": "{count} objektum törlése", + "Deleted": "Törölve", + "Deleted By": "Törölte", + "Deleted Date": "Törlés dátuma", + "Deleted Items Management": "Törölt elemek kezelése", + "Deleted This Week": "Ezen a héten törölve", + "Deleted Today": "Ma törölve", + "Deleted:": "Törölve:", + "Deleting SOLR Collection...": "SOLR gyűjtemény törlése...", + "Deleting...": "Törlés...", + "Deletion Date Range": "Törlési dátumtartomány", + "Deletion Statistics": "Törlési statisztika", + "Deprecated": "Elavult", + "Depublish": "Depublish", + "Description": "Beschrijving", + "Description Template": "Leírássablon", + "Description shown as tooltip": "Eszköztipként megjelenített leírás", + "Description:": "Leírás:", + "Detect data types and patterns": "Adattípusok és minták észlelése", + "Detected At": "Észlelve ekkor", + "Detected Format:": "Észlelt formátum:", + "Detected Issues:": "Észlelt problémák:", + "Detected: {date}": "Észlelve: {date}", + "Direct permission": "Közvetlen engedély", + "Disable": "Letiltás", + "Disable this to make the agent": "Tiltsa le ezt, hogy az ügynök", + "Disabled": "Letiltva", + "Discover Files": "Fájlok felfedezése", + "Discovered Properties": "Felfedezett tulajdonságok", + "Display Format": "Megjelenítési formátum", + "Display Name": "Megjelenítendő név", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Véglegesen törölni szeretné az összes szűrt naplóbejegyzést? Ez a művelet nem vonható vissza.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Véglegesen törölni szeretné ezt a naplóbejegyzést? Ez a művelet nem vonható vissza.", + "Documents": "Dokumentumok", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR engedélyezve", + "Download API Spec": "API specifikáció letöltése", + "Download API Specification": "API specifikáció letöltése", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Töltse le az OpenRegisterben tárolt összes személyes adatának másolatát (GDPR 20. cikk).", + "Drag a file or files here": "Húzzon ide egy vagy több fájlt", + "Dry run completed: {count} fields would be created": "A próbafuttatás befejeződött: {count} mező jönne létre", + "Dry run completed: {count} fields would be fixed": "A próbafuttatás befejeződött: {count} mező kerülne javításra", + "Duration in seconds (optional)": "Időtartam másodpercben (opcionális)", + "Edit": "Szerkesztés", + "Edit Agent": "Ügynök szerkesztése", + "Edit Application": "Alkalmazás szerkesztése", + "Edit Configuration": "Konfiguráció szerkesztése", + "Edit Endpoint": "Végpont szerkesztése", + "Edit Organisation": "Szervezet szerkesztése", + "Edit Property '{key}' of '{title}'": "A(z) '{title}' '{key}' tulajdonságának szerkesztése", + "Edit Register": "Nyilvántartás szerkesztése", + "Edit Schema": "Séma szerkesztése", + "Edit Source": "Forrás szerkesztése", + "Edit View": "Nézet szerkesztése", + "Edit Webhook": "Webhook szerkesztése", + "Edit labels": "Címkék szerkesztése", + "Edit processing activity": "Feldolgozási tevékenység szerkesztése", + "Edit tags for {name}": "Címkék szerkesztése ehhez: {name}", + "Edit verwerkingsactiviteit": "verwerkingsactiviteit szerkesztése", + "Edit view": "Nézet szerkesztése", + "Edit view details": "Nézet részleteinek szerkesztése", + "Effectiveness": "Hatékonyság", + "Email": "E-mail", + "Email Addresses": "E-mail-címek", + "Email digest frequency": "E-mail-összefoglaló gyakorisága", + "Emails": "E-mailek", + "Embedding Model": "Beágyazási modell", + "Embedding Provider": "Beágyazási szolgáltató", + "Embedding generated successfully": "A beágyazás sikeresen létrejött", + "Embedding provider connection successful!": "A beágyazási szolgáltatóhoz való kapcsolódás sikeres!", + "Embedding test failed: {error}": "A beágyazási teszt sikertelen: {error}", + "Enable": "Engedélyezés", + "Enable RAG": "RAG engedélyezése", + "Enable auto-creation": "Automatikus létrehozás engedélyezése", + "Enable automatic file vectorization": "Automatikus fájlvektorizálás engedélyezése", + "Enable automatic object vectorization": "Automatikus objektumvektorizálás engedélyezése", + "Enable automatic synchronization": "Automatikus szinkronizálás engedélyezése", + "Enable calendar provider": "Naptárszolgáltató engedélyezése", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Részletes naplózás engedélyezése a SOLR műveletekhez (hibakereséshez ajánlott)", + "Enable faceting": "Facetelés engedélyezése", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Engedélyezze vagy tiltsa le az LLM-funkciókat. A szolgáltatókat és modelleket a fenti LLM-konfiguráció gombbal állíthatja be.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Engedélyezze vagy tiltsa le az LLM-funkciókat. A szolgáltatókat és modelleket a fenti LLM-konfiguráció gombbal állíthatja be.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Engedélyezze vagy tiltsa le a SOLR keresési integrációt. A kapcsolati beállításokat a fenti Kapcsolati beállítások gombbal állíthatja be.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Engedélyezze vagy tiltsa le a SOLR keresési integrációt. A kapcsolati beállításokat a fenti kapcsolati beállítások gombbal állíthatja be.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Engedélyezze vagy tiltsa le az n8n munkafolyamat-integrációt. A kapcsolati beállításokat alább konfigurálhatja.", + "Enable or disable this webhook": "Engedélyezze vagy tiltsa le ezt a webhookot", + "Enable tools that allow the agent to interact with data through function calling.": "Engedélyezzen olyan eszközöket, amelyek lehetővé teszik az ügynök számára, hogy függvényhívással lépjen kapcsolatba az adatokkal.", + "Enable vectorization for all existing and future views (may increase costs)": "Vektorizálás engedélyezése az összes meglévő és jövőbeli nézethez (növelheti a költségeket)", + "Enable write-back to target objects": "Visszaírás engedélyezése a célobjektumokba", + "Enabled": "Engedélyezve", + "End Date Field": "Befejező dátum mező", + "Endpoint Path*": "Végpont útvonala*", + "Endpoints": "Végpontok", + "Engine not found": "A motor nem található", + "Enter ConfigSet name": "Adja meg a ConfigSet nevét", + "Enter Nextcloud usernames to grant access to this private agent": "Adja meg a Nextcloud felhasználóneveket, hogy hozzáférést biztosítson ehhez a privát ügynökhöz", + "Enter agent description (optional)": "Adja meg az ügynök leírását (opcionális)", + "Enter agent name": "Adja meg az ügynök nevét", + "Enter application description (optional)": "Adja meg az alkalmazás leírását (opcionális)", + "Enter application name": "Adja meg az alkalmazás nevét", + "Enter collection name": "Adja meg a gyűjtemény nevét", + "Enter configuration description (optional)": "Adja meg a konfiguráció leírását (opcionális)", + "Enter configuration title": "Adja meg a konfiguráció címét", + "Enter description (optional)...": "Adja meg a leírást (opcionális)...", + "Enter new collection name": "Adja meg az új gyűjtemény nevét", + "Enter object ID": "Adja meg az objektum ID-jét", + "Enter organisation description (optional)": "Adja meg a szervezet leírását (opcionális)", + "Enter organisation name": "Adja meg a szervezet nevét", + "Enter search term": "Adja meg a keresési kifejezést", + "Enter search terms or leave empty to browse all": "Adjon meg keresési kifejezéseket, vagy hagyja üresen az összes böngészéséhez", + "Enter system prompt for the agent": "Adja meg az ügynök rendszerüzenetét", + "Enter username and press Enter": "Adja meg a felhasználónevet, és nyomja meg az Entert", + "Enter view name...": "Adja meg a nézet nevét...", + "Enter webhook name": "Adja meg a webhook nevét", + "Enter your API key": "Adja meg az API-kulcsát", + "Enter your object here...": "Adja meg itt az objektumát...", + "Enter your schema here...": "Adja meg itt a sémáját...", + "Entities": "Entitások", + "Entities detected": "Észlelt entitások", + "Entity ID: {id}": "Entitás ID: {id}", + "Entity Information": "Entitás-információk", + "Entity Recognition": "Entitásfelismerés", + "Entity Recognition Method": "Entitásfelismerési módszer", + "Entity deleted successfully": "Az entitás sikeresen törölve", + "Entity not found": "Az entitás nem található", + "Entries to be deleted:": "Törlendő bejegyzések:", + "Enum Issue": "Enum probléma", + "Enum constraint is missing": "Hiányzik az enum megszorítás", + "Erase (Art 17)": "Törlés (17. cikk)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Törli ennek az érintettnek a(z) {count} objektumát? Ez a művelet rögzítésre kerül a naplóbejegyzésben.", + "Erasure complete": "A törlés befejeződött", + "Erasure preview": "Törlés előnézete", + "Error": "Hiba", + "Error Details": "Hiba részletei", + "Error Information": "Hibainformációk", + "Error Message": "Hibaüzenet", + "Error deleting audit trails: {error}": "Hiba a naplóbejegyzések törlésekor: {error}", + "Error deleting search trails: {error}": "Hiba a keresési naplók törlésekor: {error}", + "Error loading application": "Hiba az alkalmazás betöltésekor", + "Error loading audit trails": "Hiba a naplóbejegyzések betöltésekor", + "Error loading entity": "Hiba az entitás betöltésekor", + "Error loading search trails": "Hiba a keresési naplók betöltésekor", + "Estimated Batches:": "Becsült kötegek:", + "Estimated Cost:": "Becsült költség:", + "Estimated Duration:": "Becsült időtartam:", + "Event": "Esemény", + "Event Property for Payload": "Esemény tulajdonsága a hasznos adathoz", + "Events": "Események", + "Every schema with detected PII has a processing-activity annotation.": "Minden séma, amelyben PII-t észleltek, rendelkezik feldolgozási tevékenység annotációval.", + "Example": "Példa", + "Example Value": "Példaérték", + "Example value for this property": "Példaérték ehhez a tulajdonsághoz", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Kizárólagos maximum", + "Exclusive Minimum": "Kizárólagos minimum", + "Exclusive maximum": "Kizárólagos maximum", + "Exclusive minimum": "Kizárólagos minimum", + "Execution Mode": "Végrehajtási mód", + "Execution Time": "Végrehajtási idő", + "Execution Time Range": "Végrehajtási időtartomány", + "Existing Improvements": "Meglévő fejlesztések", + "Expand details": "Részletek kibontása", + "Expiration": "Lejárat", + "Expires": "Lejár", + "Expires in (e.g., 90d)": "Lejár (pl. 90d)", + "Exponential": "Exponenciális", + "Export": "Exportálás", + "Export \"{schema}\" objects from \"{register}\"": "A(z) \"{schema}\" objektumok exportálása innen: \"{register}\"", + "Export Configuration": "Konfiguráció exportálása", + "Export Format:": "Exportálási formátum:", + "Export Objects": "Objektumok exportálása", + "Export completed successfully": "Az exportálás sikeresen befejeződött", + "Export downloaded successfully": "Az exportálás sikeresen letöltve", + "Export failed: {error}": "Az exportálás sikertelen: {error}", + "Export is rate limited. Please try again later.": "Az exportálás sebességkorlátozás alatt áll. Kérjük, próbálja újra később.", + "Export my data": "Adataim exportálása", + "Export, view, or delete audit trails": "Naplóbejegyzések exportálása, megtekintése vagy törlése", + "Exporting...": "Exportálás...", + "Extend Schema": "Séma kiterjesztése", + "Extended": "Kiterjesztett", + "Extensions": "Kiterjesztések", + "Extra Query Parameters": "További lekérdezési paraméterek", + "Extract Now": "Kinyerés most", + "Extract Pending Files": "Függőben lévő fájlok kinyerése", + "Extract now": "Kinyerés most", + "Extract properties from each object": "Tulajdonságok kinyerése minden objektumból", + "Extracted At": "Kinyerve ekkor", + "Extracted at": "Kinyerve ekkor", + "Extraction": "Kinyerés", + "Extraction Mode": "Kinyerési mód", + "Extraction Scope": "Kinyerési hatókör", + "Extraction Status": "Kinyerési állapot", + "Extraction error for {file}: {error}": "Kinyerési hiba ennél: {file}: {error}", + "Facet Description": "Facet leírása", + "Facet Order": "Facet sorrendje", + "Facet Title": "Facet címe", + "Facet Type": "Facet típusa", + "Facet configuration updated successfully": "A facet konfigurációja sikeresen frissítve", + "Facetable": "Facetelhető", + "Faceting Configuration:": "Facetelési konfiguráció:", + "Facets discovered and configured successfully": "A facetek sikeresen felfedezve és konfigurálva", + "Facets discovered successfully": "A facetek sikeresen felfedezve", + "Failed": "Sikertelen", + "Failed to add attachment to object": "Nem sikerült mellékletet hozzáadni az objektumhoz", + "Failed to add user to organisation": "Nem sikerült a felhasználót hozzáadni a szervezethez", + "Failed to analyze schema properties": "Nem sikerült elemezni a séma tulajdonságait", + "Failed to archive conversation": "Nem sikerült archiválni a beszélgetést", + "Failed to calculate sizes": "Nem sikerült kiszámítani a méreteket", + "Failed to cancel deactivation": "Nem sikerült visszavonni a deaktiválást", + "Failed to change active organisation: {error}": "Nem sikerült megváltoztatni az aktív szervezetet: {error}", + "Failed to change password": "Nem sikerült megváltoztatni a jelszót", + "Failed to check version: {error}": "Nem sikerült ellenőrizni a verziót: {error}", + "Failed to clear audit trails: {error}": "Nem sikerült törölni a naplóbejegyzéseket: {error}", + "Failed to clear blob objects: {error}": "Nem sikerült törölni a blob objektumokat: {error}", + "Failed to clear cache: {error}": "Nem sikerült törölni a gyorsítótárat: {error}", + "Failed to clear collection": "Nem sikerült törölni a gyűjteményt", + "Failed to clear conversation": "Nem sikerült törölni a beszélgetést", + "Failed to clear embeddings: {error}": "Nem sikerült törölni a beágyazásokat: {error}", + "Failed to clear search trails: {error}": "Nem sikerült törölni a keresési naplókat: {error}", + "Failed to clear {type} cache: {error}": "Nem sikerült törölni a(z) {type} gyorsítótárat: {error}", + "Failed to copy changes": "Nem sikerült másolni a módosításokat", + "Failed to copy data": "Nem sikerült másolni az adatokat", + "Failed to copy data to clipboard": "Nem sikerült az adatokat a vágólapra másolni", + "Failed to copy to clipboard": "Nem sikerült a vágólapra másolni", + "Failed to copy token": "Nem sikerült másolni a tokent", + "Failed to create conversation": "Nem sikerült létrehozni a beszélgetést", + "Failed to create missing SOLR fields: {error}": "Nem sikerült létrehozni a hiányzó SOLR mezőket: {error}", + "Failed to create or find project": "Nem sikerült létrehozni vagy megtalálni a projektet", + "Failed to create token": "Nem sikerült létrehozni a tokent", + "Failed to delete SOLR collection: {error}": "Nem sikerült törölni a SOLR gyűjteményt: {error}", + "Failed to delete collection": "Nem sikerült törölni a gyűjteményt", + "Failed to delete conversation": "Nem sikerült törölni a beszélgetést", + "Failed to delete objects for {schema}: {error}": "Nem sikerült törölni a(z) {schema} objektumait: {error}", + "Failed to delete webhook": "Nem sikerült törölni a webhookot", + "Failed to depublish register: {error}": "Nem sikerült a nyilvántartást depublish: {error}", + "Failed to depublish schema: {error}": "Nem sikerült a sémát depublish: {error}", + "Failed to depublish: {error}": "Nem sikerült a depublish: {error}", + "Failed to discover files: {error}": "Nem sikerült felfedezni a fájlokat: {error}", + "Failed to download API specification": "Nem sikerült letölteni az API specifikációt", + "Failed to export data": "Nem sikerült exportálni az adatokat", + "Failed to export objects": "Nem sikerült exportálni az objektumokat", + "Failed to fetch conversation history": "Nem sikerült lekérni a beszélgetési előzményeket", + "Failed to fix mismatched SOLR fields: {error}": "Nem sikerült javítani az eltérő SOLR mezőket: {error}", + "Failed to get SOLR field configuration": "Nem sikerült lekérni a SOLR mezőkonfigurációt", + "Failed to get chat statistics": "Nem sikerült lekérni a csevegési statisztikákat", + "Failed to get database information: %s": "Nem sikerült lekérni az adatbázis-információkat: %s", + "Failed to get entity categories": "Nem sikerült lekérni az entitáskategóriákat", + "Failed to get entity statistics": "Nem sikerült lekérni az entitásstatisztikákat", + "Failed to get entity types": "Nem sikerült lekérni az entitástípusokat", + "Failed to get response: {error}": "Nem sikerült választ kapni: {error}", + "Failed to invalidate app store cache: {error}": "Nem sikerült érvényteleníteni az alkalmazásbolt gyorsítótárát: {error}", + "Failed to leave organisation: {error}": "Nem sikerült elhagyni a szervezetet: {error}", + "Failed to link object": "Nem sikerült összekapcsolni az objektumot", + "Failed to load Deck cards": "Nem sikerült betölteni a Deck kártyákat", + "Failed to load LLM configuration": "Nem sikerült betölteni az LLM-konfigurációt", + "Failed to load Nextcloud groups": "Nem sikerült betölteni a Nextcloud csoportokat", + "Failed to load cache statistics: {error}": "Nem sikerült betölteni a gyorsítótár-statisztikákat: {error}", + "Failed to load conversation": "Nem sikerült betölteni a beszélgetést", + "Failed to load entities": "Nem sikerült betölteni az entitásokat", + "Failed to load entity": "Nem sikerült betölteni az entitást", + "Failed to load extraction data": "Nem sikerült betölteni a kinyerési adatokat", + "Failed to load files": "Nem sikerült betölteni a fájlokat", + "Failed to load linked contacts": "Nem sikerült betölteni az összekapcsolt kapcsolatokat", + "Failed to load linked emails": "Nem sikerült betölteni az összekapcsolt e-maileket", + "Failed to load linked events": "Nem sikerült betölteni az összekapcsolt eseményeket", + "Failed to load organisations": "Nem sikerült betölteni a szervezeteket", + "Failed to load register data": "Nem sikerült betölteni a nyilvántartás adatait", + "Failed to load related entities": "Nem sikerült betölteni a kapcsolódó entitásokat", + "Failed to load settings: {error}": "Nem sikerült betölteni a beállításokat: {error}", + "Failed to load statistics: {error}": "Nem sikerült betölteni a statisztikákat: {error}", + "Failed to load templates": "Nem sikerült betölteni a sablonokat", + "Failed to load view: {error}": "Nem sikerült betölteni a nézetet: {error}", + "Failed to load webhooks": "Nem sikerült betölteni a webhookokat", + "Failed to load workflows": "Nem sikerült betölteni a munkafolyamatokat", + "Failed to process message": "Nem sikerült feldolgozni az üzenetet", + "Failed to publish register: {error}": "Nem sikerült közzétenni a nyilvántartást: {error}", + "Failed to publish schema: {error}": "Nem sikerült közzétenni a sémát: {error}", + "Failed to publish: {error}": "Nem sikerült a közzététel: {error}", + "Failed to refresh database information": "Nem sikerült frissíteni az adatbázis-információkat", + "Failed to reindex collection": "Nem sikerült újraindexelni a gyűjteményt", + "Failed to remove avatar": "Nem sikerült eltávolítani az avatart", + "Failed to remove link": "Nem sikerült eltávolítani a hivatkozást", + "Failed to remove member: {error}": "Nem sikerült eltávolítani a tagot: {error}", + "Failed to remove schema {schema}: {error}": "Nem sikerült eltávolítani a(z) {schema} sémát: {error}", + "Failed to remove user from organisation": "Nem sikerült eltávolítani a felhasználót a szervezetből", + "Failed to rename conversation": "Nem sikerült átnevezni a beszélgetést", + "Failed to request deactivation": "Nem sikerült kérelmezni a deaktiválást", + "Failed to restore conversation": "Nem sikerült visszaállítani a beszélgetést", + "Failed to retrieve extraction status.": "Nem sikerült lekérni a kinyerési állapotot.", + "Failed to retrieve objects for file.": "Nem sikerült lekérni a fájlhoz tartozó objektumokat.", + "Failed to retry extraction": "Nem sikerült újrapróbálni a kinyerést", + "Failed to retry webhook": "Nem sikerült újrapróbálni a webhookot", + "Failed to revoke token": "Nem sikerült visszavonni a tokent", + "Failed to save GitHub token": "Nem sikerült menteni a GitHub tokent", + "Failed to save GitLab URL": "Nem sikerült menteni a GitLab URL-t", + "Failed to save GitLab token": "Nem sikerült menteni a GitLab tokent", + "Failed to save LLM settings: {error}": "Nem sikerült menteni az LLM-beállításokat: {error}", + "Failed to save additional feedback": "Nem sikerült menteni a további visszajelzést", + "Failed to save configuration.": "Nem sikerült menteni a konfigurációt.", + "Failed to save configuration: {error}": "Nem sikerült menteni a konfigurációt: {error}", + "Failed to save facet configuration": "Nem sikerült menteni a facet konfigurációt", + "Failed to save feedback": "Nem sikerült menteni a visszajelzést", + "Failed to save file settings: {error}": "Nem sikerült menteni a fájlbeállításokat: {error}", + "Failed to save n8n configuration": "Nem sikerült menteni az n8n konfigurációt", + "Failed to save preferences": "Nem sikerült menteni a beállításokat", + "Failed to save roles": "Nem sikerült menteni a szerepköröket", + "Failed to save settings": "Nem sikerült menteni a beállításokat", + "Failed to save settings: {error}": "Nem sikerült menteni a beállításokat: {error}", + "Failed to save view: {error}": "Nem sikerült menteni a nézetet: {error}", + "Failed to save warmup interval: {error}": "Nem sikerült menteni a bemelegítési időközt: {error}", + "Failed to save webhook": "Nem sikerült menteni a webhookot", + "Failed to send feedback": "A visszajelzés elküldése sikertelen", + "Failed to set active organisation: {error}": "Az aktív szervezet beállítása sikertelen: {error}", + "Failed to setup SOLR: {error}": "A SOLR beállítása sikertelen: {error}", + "Failed to start file warmup: {error}": "A fájl bemelegítésének indítása sikertelen: {error}", + "Failed to start processing {type} files: {error}": "A(z) {type} fájlok feldolgozásának indítása sikertelen: {error}", + "Failed to start vectorization: {error}": "A vektorizálás indítása sikertelen: {error}", + "Failed to switch organisation: {error}": "A szervezet váltása sikertelen: {error}", + "Failed to sync magic table for {schema}: {error}": "A magic tábla szinkronizálása sikertelen a(z) {schema} sémához: {error}", + "Failed to test SOLR connection: {error}": "A SOLR-kapcsolat tesztelése sikertelen: {error}", + "Failed to test webhook": "A webhook tesztelése sikertelen", + "Failed to update LLM settings: {error}": "Az LLM beállításainak frissítése sikertelen: {error}", + "Failed to update Multitenancy settings: {error}": "A többbérlős beállítások frissítése sikertelen: {error}", + "Failed to update RBAC settings: {error}": "Az RBAC beállításainak frissítése sikertelen: {error}", + "Failed to update Retention settings: {error}": "A megőrzési beállítások frissítése sikertelen: {error}", + "Failed to update SOLR settings: {error}": "A SOLR beállításainak frissítése sikertelen: {error}", + "Failed to update favorite status": "A kedvenc állapot frissítése sikertelen", + "Failed to update schema configuration for {schema}: {error}": "A(z) {schema} séma konfigurációjának frissítése sikertelen: {error}", + "Failed to update schema properties": "A séma tulajdonságainak frissítése sikertelen", + "Failed to update view": "A nézet frissítése sikertelen", + "Failed to update view: {error}": "A nézet frissítése sikertelen: {error}", + "Failed to update webhook": "A webhook frissítése sikertelen", + "Failed to upload avatar": "Az avatár feltöltése sikertelen", + "Failed to validate {schema}: {error}": "A(z) {schema} érvényesítése sikertelen: {error}", + "Failed to vectorize files: {error}": "A fájlok vektorizálása sikertelen: {error}", + "Failed to warmup names cache: {error}": "A nevek gyorsítótárának bemelegítése sikertelen: {error}", + "Federation": "Föderáció", + "Feedback recorded": "Visszajelzés rögzítve", + "Fewer sources (1-3):": "Kevesebb forrás (1–3):", + "Field": "Mező", + "Fields to Display": "Megjelenítendő mezők", + "File": "Fájl", + "File #{id}": "Fájl #{id}", + "File Chunk Prediction": "Fájlrészlet-előrejelzés", + "File Collection": "Fájlgyűjtemény", + "File Handling": "Fájlkezelés", + "File Location": "Fájl helye", + "File Management": "Fájlkezelés", + "File Name": "Fájlnév", + "File Path": "Fájl elérési útja", + "File Type Selection": "Fájltípus kiválasztása", + "File Types to Process": "Feldolgozandó fájltípusok", + "File Vectorization": "Fájl vektorizálása", + "File Warmup": "Fájl bemelegítése", + "File actions menu": "Fájlműveletek menüje", + "File anonymized successfully": "A fájl sikeresen anonimizálva", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "A fájlrészletek vektorizálása még nincs megvalósítva. A részletek készen állnak és tárolva vannak, de a vektorizálási szolgáltatás fejlesztés alatt áll.", + "File collection not configured": "A fájlgyűjtemény nincs konfigurálva", + "File discovery completed": "A fájlfelderítés befejeződött", + "File extraction completed": "A fájlkinyerés befejeződött", + "File extraction queued": "A fájlkinyerés sorba állítva", + "File indexed successfully": "A fájl sikeresen indexelve", + "File is already anonymized": "A fájl már anonimizálva van", + "File name": "Fájlnév", + "File not found": "A fájl nem található", + "File settings saved successfully": "A fájlbeállítások sikeresen mentve", + "File settings updated successfully": "A fájlbeállítások sikeresen frissítve", + "File sources": "Fájlforrások", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "A fájl vektorizálása befejeződött! {vectorized} részlet vektorizálva {files} fájlból. {failed} sikertelen.", + "File vectorization configuration saved successfully": "A fájl vektorizálási konfigurációja sikeresen mentve", + "File vectorization started. Check the statistics section for progress.": "A fájl vektorizálása elindult. A folyamatot a statisztikai részben ellenőrizheti.", + "File warmup completed": "A fájl bemelegítése befejeződött", + "Files": "Fájlok", + "Files added successfully": "A fájlok sikeresen hozzáadva", + "Files with Completed Extraction:": "Befejezett kinyeréssel rendelkező fájlok:", + "Files → fileCollection, Objects → objectCollection": "Fájlok → fileCollection, Objektumok → objectCollection", + "Files → filecollection, objects → objectcollection": "Fájlok → filecollection, objektumok → objectcollection", + "Filter Audit Trails": "Naplóbejegyzések szűrése", + "Filter Deleted Items": "Törölt elemek szűrése", + "Filter Objects": "Objektumok szűrése", + "Filter Properties": "Tulajdonságok szűrése", + "Filter Search Trails": "Keresési naplók szűrése", + "Filter Statistics": "Statisztikák szűrése", + "Filter and analyze search trail entries": "Keresési naplóbejegyzések szűrése és elemzése", + "Filter and manage audit trail entries": "Naplóbejegyzések szűrése és kezelése", + "Filter and manage soft deleted items": "Nem véglegesen törölt elemek szűrése és kezelése", + "Filter and search entities": "Entitások szűrése és keresése", + "Filter by object ID": "Szűrés objektum-ID szerint", + "Filter by search term": "Szűrés keresési kifejezés szerint", + "Filter by type": "Szűrés típus szerint", + "Filter by webhook": "Szűrés webhook szerint", + "Filter data loaded automatically. Use the filters below to refine your search.": "A szűrőadatok automatikusan betöltődtek. Használja az alábbi szűrőket a keresés finomításához.", + "Filter fields...": "Mezők szűrése...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Webhook-eseményindítók szűrése a payload tulajdonságai szerint (soronként egy, formátum: kulcs: érték)", + "Filtered": "Szűrt", + "Filters": "Szűrők", + "Finalizing setup...": "A beállítás véglegesítése...", + "Fireworks AI Chat Settings": "Fireworks AI csevegési beállítások", + "Fireworks AI Embedding Configuration": "Fireworks AI beágyazási konfiguráció", + "First": "Első", + "Fixed": "Javítva", + "For chat and retrieval-augmented generation": "Csevegéshez és RAG-hoz", + "For vector embeddings and semantic search": "Vektoros beágyazásokhoz és szemantikus kereséshez", + "Format": "Formátum", + "Format Issue": "Formátumprobléma", + "Format constraint is missing": "A formátumkorlátozás hiányzik", + "From (e.g. -7 days or 2025-01-01)": "Tól (pl. -7 days vagy 2025-01-01)", + "From Date": "Kezdő dátum", + "From date": "Kezdő dátum", + "Full data copied to clipboard": "A teljes adat a vágólapra másolva", + "Function Tools": "Funkcióeszközök", + "General Issue": "Általános probléma", + "Generate recommendations and confidence scores": "Ajánlások és megbízhatósági pontszámok generálása", + "Generate report": "Jelentés generálása", + "Generate the verantwoordingsdocument": "A verantwoordingsdocument generálása", + "Generate vectors immediately when new objects are created": "Vektorok azonnali generálása új objektumok létrehozásakor", + "Generated: {time}": "Generálva: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Szerezzen valós idejű betekintést szervezete adatainak állapotába a nyilvántartásokra, a sémadefiníciókra, valamint az objektumok tárolására és használatára összpontosítva.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Szerezzen valós idejű betekintést szervezete adatainak állapotába, növekedési trendjeibe és minőségi mutatóiba.", + "GitHub Personal Access Token": "GitHub személyes hozzáférési token", + "GitHub token is valid": "A GitHub token érvényes", + "GitHub token is valid! Username: {username}": "A GitHub token érvényes! Felhasználónév: {username}", + "GitHub token removed successfully": "A GitHub token sikeresen eltávolítva", + "GitHub token saved successfully": "A GitHub token sikeresen mentve", + "GitHub token test failed: {message}": "A GitHub token tesztelése sikertelen: {message}", + "GitLab Personal Access Token": "GitLab személyes hozzáférési token", + "GitLab URL saved successfully": "A GitLab URL sikeresen mentve", + "GitLab token is valid": "A GitLab token érvényes", + "GitLab token is valid! Username: {username}": "A GitLab token érvényes! Felhasználónév: {username}", + "GitLab token saved successfully": "A GitLab token sikeresen mentve", + "GitLab token test failed: {message}": "A GitLab token tesztelése sikertelen: {message}", + "Go to linked Register": "Ugrás a kapcsolt Nyilvántartáshoz", + "Go to linked Schema": "Ugrás a kapcsolt Sémához", + "Go to organisation": "Ugrás a szervezethez", + "Grant additional permissions beyond standard CRUD operations": "További jogosultságok megadása a szabványos CRUD-műveleteken túl", + "Group Quota": "Csoportkvóta", + "Groups": "Csoportok", + "HTML preview": "HTML-előnézet", + "HTTP Method": "HTTP-metódus", + "HTTP method used to send webhook requests": "A webhook-kérések küldéséhez használt HTTP-metódus", + "Headers": "Fejlécek", + "Health": "Állapot", + "Heartbeat successful - connection kept alive": "A heartbeat sikeres – a kapcsolat fennmaradt", + "Helpful": "Hasznos", + "Here you can configure the details for various connections.": "Itt konfigurálhatja a különböző kapcsolatok részleteit.", + "Here you can configure the details for your organisation.": "Itt konfigurálhatja szervezete részleteit.", + "Hide": "Elrejtés", + "Hide Filters": "Szűrők elrejtése", + "Hide in collection view": "Elrejtés a gyűjteménynézetben", + "Hide in forms": "Elrejtés az űrlapokon", + "Hide in list view": "Elrejtés a listanézetben", + "High": "Magas", + "High Confidence": "Magas megbízhatóság", + "Host": "Kiszolgáló", + "Hourly": "Óránként", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Milyen mélyen járja be a beágyazott objektumtulajdonságokat (1–20). A magasabb értékek több részletet rögzítenek, de növelik a vektor méretét.", + "How often to check for updates (1-168 hours)": "Milyen gyakran ellenőrizze a frissítéseket (1–168 óra)", + "How to handle retries for failed webhook deliveries": "Hogyan kezelje a sikertelen webhook-kézbesítések újrapróbálkozásait", + "IBANs": "IBAN-ok", + "ID": "ID", + "ID of input mapping (optional)": "A bemeneti leképezés ID-je (opcionális)", + "ID of output mapping (optional)": "A kimeneti leképezés ID-je (opcionális)", + "ID of the target resource": "A célerőforrás ID-je", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Az IP-sebességkorlátok sikeresen törölve", + "Identify properties not in the schema": "A sémában nem szereplő tulajdonságok azonosítása", + "Immutable": "Megváltoztathatatlan", + "Import": "Importálás", + "Import Configuration": "Konfiguráció importálása", + "Import Data into Register": "Adatok importálása a Nyilvántartásba", + "Import successful": "Az importálás sikeres", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importálja a report-bundle.json sablont a `reports` nyilvántartás létrehozásához, majd hozza létre első irányítópultját a szabványos objektum-felhasználói felületen keresztül. Az irányítópultok JSON-ban deklarálják widgetjeiket, és a renderelő minden widgetet élő aggregációs adatokkal lát el.", + "Improved Property": "Javított tulajdonság", + "In use": "Használatban", + "Inactive": "Inaktív", + "Inactive organisations cannot be used": "Az inaktív szervezetek nem használhatók", + "Include IDs and names of related objects for better contextual search": "A jobb kontextuális kereséshez vegye fel a kapcsolódó objektumok ID-jét és nevét", + "Include ids and names of related objects for better contextual search": "A jobb kontextuális kereséshez vegye fel a kapcsolódó objektumok azonosítóit és neveit", + "Include related object references": "Kapcsolódó objektumhivatkozások felvétele", + "Include schema and register metadata": "Séma- és nyilvántartás-metaadatok felvétele", + "Index": "Index", + "Information about the current OpenRegister installation": "Információk a jelenlegi OpenRegister-telepítésről", + "Inherited from register": "A nyilvántartástól örökölve", + "Inherits permissions from register": "A jogosultságokat a nyilvántartástól örökli", + "Initialization failed": "Az inicializálás sikertelen", + "Initialize Project": "Projekt inicializálása", + "Initializing SOLR setup...": "A SOLR beállításának inicializálása...", + "Input Mapping": "Bemeneti leképezés", + "Inspect Fields": "Mezők vizsgálata", + "Inspect Index": "Index vizsgálata", + "Inspect SOLR Index": "SOLR-index vizsgálata", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Telepítse a notify_push alkalmazást a Nextcloud App Store-ból a valós idejű frissítések engedélyezéséhez.", + "Integrations": "Integrációk", + "Internet location (URL) of this installation": "A telepítés internetes helye (URL)", + "Interval": "Időköz", + "Invalid": "Érvénytelen", + "Invalid batch size. Must be between 1 and 5000": "Érvénytelen kötegméret. 1 és 5000 között kell lennie", + "Invalid feedback type": "Érvénytelen visszajelzéstípus", + "Invalid field name provided": "Érvénytelen mezőnév lett megadva", + "Invalid maxObjects. Must be 0 (all) or positive number": "Érvénytelen maxObjects. 0-nak (összes) vagy pozitív számnak kell lennie", + "Invalid:": "Érvénytelen:", + "Invite Users": "Felhasználók meghívása", + "Invited Users": "Meghívott felhasználók", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Inzage eredmények", + "Issue": "Probléma", + "Items per page:": "Elem oldalanként:", + "Just now": "Épp most", + "Key": "Kulcs", + "LLM Configuration": "LLM-konfiguráció", + "LLM actions menu": "LLM-műveletek menüje", + "LLM configuration saved successfully": "Az LLM-konfiguráció sikeresen mentve", + "LLM features disabled": "Az LLM-funkciók letiltva", + "LLM features enabled": "Az LLM-funkciók engedélyezve", + "LLM must be enabled with an embedding provider configured": "Az LLM-et engedélyezni kell egy konfigurált beágyazási szolgáltatóval", + "LLM settings saved successfully": "Az LLM beállításai sikeresen mentve", + "LLM settings updated successfully": "Az LLM beállításai sikeresen frissítve", + "Label": "Címke", + "Labels": "Címkék", + "Languages": "Nyelvek", + "Last": "Utolsó", + "Last Triggered": "Utoljára aktiválva", + "Leave empty to allow all users access": "Hagyja üresen, hogy minden felhasználó hozzáférjen", + "Leave off for auto-detection from property format.": "Hagyja kikapcsolva a tulajdonság formátumából való automatikus felismeréshez.", + "Left organisation successfully": "A szervezet sikeresen elhagyva", + "Legal basis *": "Jogalap *", + "Length Range:": "Hossztartomány:", + "Linear": "Lineáris", + "Link": "Hivatkozás", + "Link an email from the Mail app sidebar to associate it with this object.": "Kapcsoljon egy e-mailt a Mail alkalmazás oldalsávjából, hogy ehhez az objektumhoz társítsa.", + "Link an object to see it here.": "Kapcsoljon egy objektumot, hogy itt megjelenjen.", + "Link another object": "Másik objektum kapcsolása", + "Link existing event": "Meglévő esemény kapcsolása", + "Link removed": "A kapcsolat eltávolítva", + "Link to Object": "Kapcsolás objektumhoz", + "Link to {name}": "Kapcsolás ehhez: {name}", + "Linked Objects": "Kapcsolt objektumok", + "Linked by {user}": "Kapcsolta: {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "A kapcsolt e-mailek, naptáresemények, kapcsolatok és Deck-kártyák itt jelennek meg.", + "Linked to {name}": "Kapcsolva ehhez: {name}", + "Load Advanced Filters": "Speciális szűrők betöltése", + "Load advanced filters with live data from your search index": "Speciális szűrők betöltése a keresési indexből származó élő adatokkal", + "Load more": "Több betöltése", + "Load view": "Nézet betöltése", + "Loading API tokens...": "API-tokenek betöltése...", + "Loading ConfigSets...": "ConfigSetek betöltése...", + "Loading LLM configuration...": "Az LLM-konfiguráció betöltése...", + "Loading RBAC settings...": "Az RBAC beállításainak betöltése...", + "Loading activity...": "Tevékenység betöltése...", + "Loading advanced filters...": "Speciális szűrők betöltése...", + "Loading agents...": "Ügynökök betöltése...", + "Loading application details...": "Az alkalmazás részleteinek betöltése...", + "Loading applications...": "Alkalmazások betöltése...", + "Loading archived conversations...": "Archivált beszélgetések betöltése...", + "Loading audit trails...": "Naplóbejegyzések betöltése...", + "Loading available tools...": "Elérhető eszközök betöltése...", + "Loading cache statistics...": "Gyorsítótár-statisztikák betöltése...", + "Loading collections...": "Gyűjtemények betöltése...", + "Loading configurations...": "Konfigurációk betöltése...", + "Loading conversation...": "Beszélgetés betöltése...", + "Loading conversations...": "Beszélgetések betöltése...", + "Loading deleted items...": "Törölt elemek betöltése...", + "Loading endpoints": "Végpontok betöltése", + "Loading entities...": "Entitások betöltése...", + "Loading events...": "Események betöltése...", + "Loading filter data...": "Szűrőadatok betöltése...", + "Loading groups...": "Csoportok betöltése...", + "Loading linked objects...": "Kapcsolt objektumok betöltése...", + "Loading log details...": "A naplórészletek betöltése...", + "Loading multitenancy settings...": "Többbérlős beállítások betöltése...", + "Loading n8n configuration...": "Az n8n-konfiguráció betöltése...", + "Loading permission matrix...": "Jogosultsági mátrix betöltése...", + "Loading preferences...": "Beállítások betöltése...", + "Loading register data...": "Nyilvántartás-adatok betöltése...", + "Loading registers...": "Nyilvántartások betöltése...", + "Loading retention settings...": "Megőrzési beállítások betöltése...", + "Loading schemas...": "Sémák betöltése...", + "Loading search configuration...": "A keresési konfiguráció betöltése...", + "Loading search trails...": "Keresési naplók betöltése...", + "Loading sources...": "Források betöltése...", + "Loading statistics...": "Statisztikák betöltése...", + "Loading text extraction settings...": "Szövegkinyerési beállítások betöltése...", + "Loading tokens...": "Tokenek betöltése...", + "Loading user groups...": "Felhasználói csoportok betöltése...", + "Loading users...": "Felhasználók betöltése...", + "Loading views...": "Nézetek betöltése...", + "Loading...": "Betöltés...", + "Local": "Helyi", + "Local Version": "Helyi verzió", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Keresse meg az érintettre hivatkozó minden objektumot (Art 15 inzage), tekintse meg egy törlés előnézetét (Art 17 vergetelheid), vagy exportálja az adataikat (Art 20 portabiliteit).", + "Location": "Hely", + "Location Field": "Helymező", + "Locations": "Helyek", + "Locked": "Zárolt", + "Locked:": "Zárolt:", + "Login successful": "A bejelentkezés sikeres", + "Logs": "Naplók", + "Low": "Alacsony", + "Low Confidence": "Alacsony megbízhatóság", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Az alacsonyabb számok jelennek meg először a szűrő-oldalsávban. Hagyja üresen az automatikus rendezéshez.", + "Lucene Version:": "Lucene-verzió:", + "Magic table sync completed for {schema}": "A magic tábla szinkronizálása befejeződött a(z) {schema} sémához", + "Magic table synchronized successfully": "A magic tábla sikeresen szinkronizálva", + "Mail Integration": "Mail-integráció", + "Mail integration is not available": "A Mail-integráció nem érhető el", + "Make this installation discoverable within the federation network.": "Tegye ezt a telepítést felfedezhetővé a föderációs hálózaton belül.", + "Manage Organisation Roles": "Szervezeti szerepkörök kezelése", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Kezelje a SOLR-gyűjteményeket (adattárakat), és rendelje hozzá őket objektumokhoz és fájlokhoz.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Kezelje a SOLR ConfigSeteket (konfigurációs sablonokat) a gyűjteményeihez.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Kezelje a SOLR-gyűjteményeket (adattárakat), és rendelje hozzá őket objektumokhoz és fájlokhoz.", + "Manage SOLR configsets (configuration templates) for your collections.": "Kezelje a SOLR-configseteket (konfigurációs sablonokat) a gyűjteményeihez.", + "Manage and configure agents for automated tasks": "Ügynökök kezelése és konfigurálása automatizált feladatokhoz", + "Manage and monitor file text extraction status": "A fájlok szövegkinyerési állapotának kezelése és nyomon követése", + "Manage and restore soft deleted items from your registers": "Nem véglegesen törölt elemek kezelése és visszaállítása a nyilvántartásaiból", + "Manage and view detected entities from files and objects": "A fájlokból és objektumokból észlelt entitások kezelése és megtekintése", + "Manage document templates and themes": "Dokumentumsablonok és témák kezelése", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Kezelje az n8n munkafolyamatokat az OpenRegister automatizálásához. A munkafolyamatok a konfigurált projektben lesznek tárolva.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Kezelje a verwerkingsactiviteit tevékenységeket, futtasson érintetti hozzáférési kérelmeket, és auditálja a megfelelőséget az EU GDPR / holland AVG szerint.", + "Manage webhooks for event-driven integrations": "Webhookok kezelése eseményvezérelt integrációkhoz", + "Manage your account settings, security, and personal data.": "Kezelje fiókbeállításait, biztonságát és személyes adatait.", + "Manage your applications and modules": "Alkalmazásai és moduljai kezelése", + "Manage your chat conversations": "Csevegési beszélgetései kezelése", + "Manage your data registers and their configurations": "Adatnyilvántartásai és azok konfigurációinak kezelése", + "Manage your data schemas and their properties": "Adatsémái és azok tulajdonságainak kezelése", + "Manage your data sources and their configurations": "Adatforrásai és azok konfigurációinak kezelése", + "Manage your organisations and switch between them": "Szervezetei kezelése és váltás közöttük", + "Manage your saved search configurations": "Mentett keresési konfigurációi kezelése", + "Manage your system configurations and settings": "Rendszerkonfigurációi és beállításai kezelése", + "Managed": "Felügyelt", + "Mappings": "Leképezések", + "Mass validation completed successfully": "A tömeges érvényesítés sikeresen befejeződött", + "Mass validation failed: {error}": "A tömeges érvényesítés sikertelen: {error}", + "Max Files (0 = all)": "Max. fájlok (0 = összes)", + "Max Length": "Max. hossz", + "Max Length:": "Max. hossz:", + "Max Objects (0 = all)": "Max. objektumok (0 = összes)", + "Max Retries": "Max. újrapróbálkozások", + "Max Shards/Node": "Max. shard/csomópont", + "Max Tokens": "Max. tokenek", + "Max execution time (ms)": "Max. végrehajtási idő (ms)", + "Max ms": "Max. ms", + "Max result count": "Max. találatszám", + "Max results": "Max. találatok", + "Maximum": "Maximum", + "Maximum File Size (MB)": "Maximális fájlméret (MB)", + "Maximum Nesting Depth": "Maximális beágyazási mélység", + "Maximum length": "Maximális hossz", + "Maximum length constraint is missing": "A maximális hossz korlátozása hiányzik", + "Maximum length is too restrictive": "A maximális hossz túl korlátozó", + "Maximum number of files to process. Set to 0 to process all files.": "A feldolgozandó fájlok maximális száma. Állítsa 0-ra az összes fájl feldolgozásához.", + "Maximum number of items": "Az elemek maximális száma", + "Maximum number of objects to process. Set to 0 to process all objects.": "A feldolgozandó objektumok maximális száma. Állítsa 0-ra az összes objektum feldolgozásához.", + "Maximum number of retry attempts for failed deliveries": "A sikertelen kézbesítések újrapróbálkozási kísérleteinek maximális száma", + "Maximum time to wait before committing changes": "A változtatások véglegesítése előtti maximális várakozási idő", + "Maximum tokens to generate": "A generálandó tokenek maximális száma", + "Maximum value": "Maximális érték", + "Maximum value constraint is missing": "A maximális érték korlátozása hiányzik", + "Maximum value is too restrictive": "A maximális érték túl korlátozó", + "Medium": "Közepes", + "Medium Confidence": "Közepes megbízhatóság", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Közepes lekérdezések: Némi szűréssel vagy több paraméterrel rendelkező keresések (pl. dátumtartományok, adott nyilvántartások/sémák)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Közepes lekérdezések: némi szűréssel vagy több paraméterrel rendelkező keresések (pl. dátumtartományok, adott nyilvántartások/sémák)", + "Member removed successfully": "A tag sikeresen eltávolítva", + "Members": "Tagok", + "Members ({count})": "Tagok ({count})", + "Members of selected groups can access this view": "A kiválasztott csoportok tagjai hozzáférhetnek ehhez a nézethez", + "Members:": "Tagok:", + "Memory prediction calculated successfully": "A memória-előrejelzés sikeresen kiszámítva", + "Message content is required": "Az üzenet tartalma kötelező", + "Message does not belong to this conversation": "Az üzenet nem ehhez a beszélgetéshez tartozik", + "Message not found": "Az üzenet nem található", + "Metadata": "Metaadatok", + "Metadata Filters": "Metaadat-szűrők", + "Method": "Metódus", + "Method*": "Metódus*", + "Method: {method}": "Metódus: {method}", + "Min Length": "Min. hossz", + "Min execution time (ms)": "Min. végrehajtási idő (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. találatszám", + "Min results": "Min. találatok", + "Minimum": "Minimum", + "Minimum length": "Minimális hossz", + "Minimum number of items": "Az elemek minimális száma", + "Minimum value": "Minimális érték", + "Minimum value constraint is missing": "A minimális érték korlátozása hiányzik", + "Minimum value is too restrictive": "A minimális érték túl korlátozó", + "Missing conversation or agentUuid": "Hiányzó conversation vagy agentUuid", + "Missing conversationId": "Hiányzó conversationId", + "Missing message": "Hiányzó message", + "Missing object fields created successfully": "A hiányzó objektummezők sikeresen létrehozva", + "Mode:": "Mód:", + "Model": "Modell", + "Model Name": "Modellnév", + "Modified": "Módosítva", + "Monthly": "Havonta", + "More sources (10-20):": "Több forrás (10–20):", + "Most Active Objects": "Legaktívabb objektumok", + "Multiple Of": "Többszöröse", + "Multiple of": "Többszöröse", + "Multiple search trail deletion not implemented yet": "Több keresési napló törlése még nincs megvalósítva", + "Multitenancy": "Többbérlősség", + "Multitenancy settings updated successfully": "A többbérlős beállítások sikeresen frissítve", + "My Account": "Saját fiók", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Név", + "Name *": "Név *", + "Name*": "Név*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "A nevek gyorsítótárának előmelegítése sikeresen befejeződött: {count} név betöltve {time} alatt. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "A nevek gyorsítótárának előmelegítése sikeresen befejeződött: {count} név betöltve {time} alatt. {message}", + "Names cache warmup completed": "A nevek gyorsítótárának előmelegítése befejeződött", + "Naming pattern for copies:": "Másolatok elnevezési mintája:", + "New Collection Name": "Új gyűjtemény neve", + "New Configuration": "Új konfiguráció", + "New Conversation": "Új beszélgetés", + "New Properties": "Új tulajdonságok", + "New Property": "Új tulajdonság", + "New User Group": "Új felhasználói csoport", + "New Value": "Új érték", + "New activity": "Új tevékenység", + "New password": "Új jelszó", + "New processing activity": "Új verwerkingsactiviteit", + "New users without specific organisation membership will be automatically added to this organisation": "A meghatározott szervezeti tagság nélküli új felhasználók automatikusan hozzáadódnak ehhez a szervezethez", + "New verwerkingsactiviteit": "Új verwerkingsactiviteit", + "Next": "Következő", + "Nextcloud Groups": "Nextcloud-csoportok", + "No": "Nem", + "No API tokens.": "Nincsenek API-tokenek.", + "No ConfigSets found": "Nem található ConfigSet", + "No Deck cards linked to this object": "Nincsenek Deck-kártyák ehhez az objektumhoz kapcsolva", + "No GitHub token configured": "Nincs GitHub-token beállítva", + "No GitHub token provided": "Nincs GitHub-token megadva", + "No GitLab token provided": "Nincs GitLab-token megadva", + "No Organisation": "Nincs szervezet", + "No active filters": "Nincsenek aktív szűrők", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Nincsenek aktív objektumok a(z) {schema} sémához, amelyeket finoman törölni lehetne. A finoman törölt objektumok eltávolításához használja a „Végleges törlés” lehetőséget.", + "No activity data available": "Nincsenek elérhető tevékenységadatok", + "No activity found.": "Nem található tevékenység.", + "No agents available": "Nincsenek elérhető ügynökök", + "No agents found": "Nem található ügynök", + "No app store cache exists yet": "Még nem létezik alkalmazásbolt-gyorsítótár", + "No applications are available.": "Nincsenek elérhető alkalmazások.", + "No applications found": "Nem található alkalmazás", + "No approval steps for this object.": "Nincsenek jóváhagyási lépések ehhez az objektumhoz.", + "No archived conversations": "Nincsenek archivált beszélgetések", + "No audit trail entries found": "Nem található naplóbejegyzés", + "No changes recorded": "Nincsenek rögzített módosítások", + "No chunks to vectorize": "Nincsenek vektorizálandó darabok", + "No collections found": "Nem található gyűjtemény", + "No configuration data": "Nincsenek konfigurációs adatok", + "No configuration selected": "Nincs konfiguráció kiválasztva", + "No configurations are available.": "Nincsenek elérhető konfigurációk.", + "No configurations found": "Nem található konfiguráció", + "No configurations found for this application.": "Nem található konfiguráció ehhez az alkalmazáshoz.", + "No contacts linked to this object": "Nincsenek kapcsolatok ehhez az objektumhoz kapcsolva", + "No conversations yet. Create a new one to get started!": "Még nincsenek beszélgetések. Hozzon létre egy újat a kezdéshez!", + "No dashboards yet": "Még nincsenek irányítópultok", + "No data available": "Nincsenek elérhető adatok", + "No data available for chart": "Nincsenek elérhető adatok a diagramhoz", + "No deleted items found": "Nem találhatók törölt elemek", + "No deletion data available": "Nincsenek elérhető törlési adatok", + "No description found": "Nem található leírás", + "No description provided": "Nincs leírás megadva", + "No emails linked to this object": "Nincsenek e-mailek ehhez az objektumhoz kapcsolva", + "No endpoints defined": "Nincsenek végpontok meghatározva", + "No entities detected for this email.": "Nem észlelhetők entitások ehhez az e-mailhez.", + "No entities detected in this file. Run text extraction first.": "Nem észlelhetők entitások ebben a fájlban. Először futtassa a szövegkinyerést.", + "No entities found": "Nem található entitás", + "No entities have been detected yet": "Még nem észleltek entitásokat", + "No events found": "Nem található esemény", + "No events linked to this object": "Nincsenek események ehhez az objektumhoz kapcsolva", + "No expired audit trails found to clear": "Nem találhatók lejárt naplóbejegyzések a törléshez", + "No expired search trails found to clear": "Nem találhatók lejárt keresési naplók a törléshez", + "No extraction data available for this file": "Nincsenek elérhető kinyerési adatok ehhez a fájlhoz", + "No facetable fields available. Select a register and schema to see available filters.": "Nincsenek facetelhető mezők. Válasszon egy nyilvántartást és sémát az elérhető szűrők megtekintéséhez.", + "No files found": "Nem található fájl", + "No files have been extracted yet": "Még nem nyertek ki fájlokat", + "No files selected": "Nincsenek fájlok kiválasztva", + "No files to process": "Nincsenek feldolgozandó fájlok", + "No files to reindex": "Nincsenek újraindexelendő fájlok", + "No filters are currently active. This will delete ALL audit trail entries!": "Jelenleg nincsenek aktív szűrők. Ez az ÖSSZES naplóbejegyzést törölni fogja!", + "No groups found. Try a different search.": "Nem található csoport. Próbáljon meg egy másik keresést.", + "No label": "Nincs címke", + "No labels": "Nincsenek címkék", + "No linked objects": "Nincsenek kapcsolt objektumok", + "No log entries found": "Nem található naplóbejegyzés", + "No logs are available for this configuration.": "Nincsenek elérhető naplók ehhez a konfigurációhoz.", + "No logs are available for this source.": "Nincsenek elérhető naplók ehhez a forráshoz.", + "No logs found": "Nem található napló", + "No matches": "Nincsenek találatok", + "No mismatched fields found - SOLR schema is properly configured": "Nem találhatók eltérő mezők – a SOLR-séma megfelelően van konfigurálva", + "No objects are currently selected for deletion.": "Jelenleg nincsenek objektumok kiválasztva törlésre.", + "No objects are currently selected for permanent deletion.": "Jelenleg nincsenek objektumok kiválasztva végleges törlésre.", + "No objects are currently selected for restoration.": "Jelenleg nincsenek objektumok kiválasztva visszaállításra.", + "No objects found": "Nem található objektum", + "No objects found to vectorize": "Nem találhatók vektorizálandó objektumok", + "No objects linked to this email": "Nincsenek objektumok ehhez az e-mailhez kapcsolva", + "No objects selected": "Nincsenek objektumok kiválasztva", + "No objects selected for deletion": "Nincsenek objektumok kiválasztva törlésre", + "No objects selected for restoration": "Nincsenek objektumok kiválasztva visszaállításra", + "No objects to delete": "Nincsenek törlendő objektumok", + "No objects to delete for schema {schema}": "Nincsenek törlendő objektumok a(z) {schema} sémához", + "No objects were permanently deleted": "Nem töröltek véglegesen objektumokat", + "No organisations found": "Nem található szervezet", + "No personal data was found for this subject identifier.": "Nem találtak személyes adatokat ehhez az alany-azonosítóhoz.", + "No processing activities yet": "Még nincsenek verwerkingsactiviteiten", + "No properties found": "Nem található tulajdonság", + "No properties match your filters.": "Egyetlen tulajdonság sem felel meg a szűrőknek.", + "No purge date set": "Nincs törlési dátum beállítva", + "No register data available": "Nincsenek elérhető nyilvántartási adatok", + "No register objects reference this file": "Egyetlen nyilvántartási objektum sem hivatkozik erre a fájlra", + "No registers found": "Nem található nyilvántartás", + "No registers found for this application.": "Nem található nyilvántartás ehhez az alkalmazáshoz.", + "No registers found. Create a register to configure permissions.": "Nem található nyilvántartás. Hozzon létre egy nyilvántartást a jogosultságok beállításához.", + "No related cases found for this sender": "Nem találhatók kapcsolódó ügyek ehhez a feladóhoz", + "No related entities": "Nincsenek kapcsolódó entitások", + "No relations found": "Nem található kapcsolat", + "No request body available": "Nincs elérhető kérelemtörzs", + "No response body available": "Nincs elérhető választörzs", + "No saved views yet. Create one in the Search tab!": "Még nincsenek mentett nézetek. Hozzon létre egyet a Keresés fülön!", + "No saved views yet. create one in the search tab!": "Még nincsenek mentett nézetek. hozzon létre egyet a keresés fülön!", + "No scan run yet": "Még nem futott vizsgálat", + "No schema data available": "Nincsenek elérhető sémaadatok", + "No schema selected for exploration": "Nincs séma kiválasztva felfedezésre", + "No schemas configured for mail linking.": "Nincsenek sémák beállítva e-mail-összekapcsoláshoz.", + "No schemas found": "Nem található séma", + "No schemas found for this application.": "Nem található séma ehhez az alkalmazáshoz.", + "No search terms data available": "Nincsenek elérhető keresési kifejezés adatok", + "No search trail entries found": "Nem található keresési naplóbejegyzés", + "No sources are available.": "Nincsenek elérhető források.", + "No sources found": "Nem található forrás", + "No synchronizations found": "Nem található szinkronizálás", + "No templates found": "Nem található sablon", + "No templates have been created yet": "Még nem hoztak létre sablonokat", + "No tools available. Tools can be registered by installed apps.": "Nincsenek elérhető eszközök. Eszközöket a telepített alkalmazások regisztrálhatnak.", + "No users in this organisation.": "Nincsenek felhasználók ebben a szervezetben.", + "No valid magic-mapped register+schema combinations found": "Nem találhatók érvényes magic-mapped nyilvántartás+séma kombinációk", + "No views found. Create views first before configuring vectorization.": "Nem található nézet. A vektorizálás beállítása előtt először hozzon létre nézeteket.", + "No views match your search": "Egyetlen nézet sem felel meg a keresésnek", + "No webhooks found": "Nem található webhook", + "No webhooks have been configured yet": "Még nem konfiguráltak webhookokat", + "No workflows found in this project. Create workflows in the n8n editor.": "Nem található munkafolyamat ebben a projektben. Hozzon létre munkafolyamatokat az n8n szerkesztőben.", + "None": "Nincs", + "Not Found": "Nem található", + "Not extracted": "Nincs kinyerve", + "Not helpful": "Nem hasznos", + "Not in use": "Nincs használatban", + "Note:": "Megjegyzés:", + "Notifications": "Értesítések", + "Number Constraints": "Számkorlátozások", + "Number of Sources": "Források száma", + "Number of chunks to vectorize in one API call (1-100).": "Egy API-hívásban vektorizálandó darabok száma (1–100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Egy API-hívásban vektorizálandó darabok száma. Magasabb = gyorsabb, de több memória. Ajánlott: 10–50.", + "Number of objects to process in each batch (1-5000).": "Egy kötegben feldolgozandó objektumok száma (1–5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Egy API-hívásban vektorizálandó objektumok száma. Magasabb = gyorsabb, de több memória. Ajánlott: 10–50.", + "Numeric Range:": "Számtartomány:", + "Object": "Objektum", + "Object #{id}": "Objektum #{id}", + "Object Collection": "Objektumgyűjtemény", + "Object Configuration:": "Objektumkonfiguráció:", + "Object Count Prediction": "Objektumszám-előrejelzés", + "Object Extraction Mode": "Objektumkinyerési mód", + "Object ID": "Objektumazonosító", + "Object Management": "Objektumkezelés", + "Object Text Extraction": "Objektum-szövegkinyerés", + "Object Vectorization": "Objektum-vektorizálás", + "Object Warmup": "Objektum-előmelegítés", + "Object changes in owned objects": "Objektumváltozások a saját objektumokban", + "Object collection not configured": "Az objektumgyűjtemény nincs konfigurálva", + "Object metadata incomplete for file upload": "Az objektum metaadatai hiányosak a fájlfeltöltéshez", + "Object not found": "Az objektum nem található", + "Object permanently deleted": "Az objektum véglegesen törölve", + "Object restored successfully": "Az objektum sikeresen visszaállítva", + "Object settings updated successfully": "Az objektum beállításai sikeresen frissítve", + "Object sources": "Objektumforrások", + "Object successfully deleted": "Az objektum sikeresen törölve", + "Object successfully modified": "Az objektum sikeresen módosítva", + "Object unlocked successfully": "Az objektum sikeresen feloldva", + "Object vectorization configuration saved successfully": "Az objektum-vektorizálási konfiguráció sikeresen mentve", + "Object vectorized successfully": "Az objektum sikeresen vektorizálva", + "Object:": "Objektum:", + "Objects": "Objektumok", + "Objects Analyzed": "Elemzett objektumok", + "Objects Distribution": "Objektumeloszlás", + "Objects being analyzed": "Elemzés alatt álló objektumok", + "Objects by Register": "Objektumok nyilvántartás szerint", + "Objects by Schema": "Objektumok séma szerint", + "Objects deletion completed for {schema}": "Az objektumok törlése befejeződött a(z) {schema} sémához", + "Objects deletion completed successfully": "Az objektumok törlése sikeresen befejeződött", + "Objects in schema": "Objektumok a sémában", + "Objects successfully deleted": "Az objektumok sikeresen törölve", + "Objects to Process:": "Feldolgozandó objektumok:", + "Objects to be analyzed": "Elemzendő objektumok", + "Objects to be deleted": "Törlendő objektumok", + "Objects to be validated": "Érvényesítendő objektumok", + "Objects will be serialized as JSON text before vectorization": "Az objektumok a vektorizálás előtt JSON-szövegként lesznek sorosítva", + "Objects will be soft deleted and moved to the": "Az objektumok finoman törlődnek, és átkerülnek ide:", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Az objektumok finoman törlődnek (töröltként megjelölve, de az adatbázisban megőrizve). Szükség esetén később visszaállíthatók.", + "Old Value": "Régi érték", + "Oldest Item (days)": "Legrégebbi elem (nap)", + "Ollama Chat Settings": "Ollama csevegési beállítások", + "Ollama Embedding Configuration": "Ollama beágyazási konfiguráció", + "Ollama URL": "Ollama URL", + "Only With Changes": "Csak módosításokkal", + "Only members of selected groups can access this organisation": "Csak a kiválasztott csoportok tagjai férhetnek hozzá ehhez a szervezethez", + "Open Nextcloud App Store": "Nextcloud alkalmazásbolt megnyitása", + "Open OpenRegister sidebar": "OpenRegister oldalsáv megnyitása", + "Open Register Objects": "Nyilvántartási objektumok megnyitása", + "Open folder": "Mappa megnyitása", + "Open in OpenRegister": "Megnyitás az OpenRegisterben", + "Open n8n Editor": "n8n szerkesztő megnyitása", + "OpenAI Chat Settings": "OpenAI csevegési beállítások", + "OpenAI Embedding Configuration": "OpenAI beágyazási konfiguráció", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister beállítások", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Üzemeltető által meghatározott irányítópultok és ütemezett jelentések. Minden irányítópult elsőrendű objektum a `reports` nyilvántartásban; a widgetek az irányítópult `widgets` tömbjében vannak deklarálva, és élőben jelennek meg az aggregációkból / GraphQL-ből.", + "Optimizing search performance...": "Keresési teljesítmény optimalizálása...", + "Optional URL-friendly identifier": "Opcionális, URL-barát azonosító", + "Optional description for this facet": "Opcionális leírás ehhez a facethez", + "Optional end date property": "Opcionális befejezési dátum tulajdonság", + "Optional event description template": "Opcionális eseményleírás-sablon", + "Optional location property": "Opcionális helyszín-tulajdonság", + "Optional webhook secret for signature verification": "Opcionális webhook-titok az aláírás-ellenőrzéshez", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Opcionális: Adjon hozzá lekérdezési paramétereket a hivatkozott séma szűréséhez (pl. status=active&type=public)", + "Or": "Vagy", + "Order": "Sorrend", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Rendezett BCP 47 nyelvi címkék. Az első nyelv a nyilvántartás alapértelmezett nyelve, és ez vezérli az Accept-Language tartalék mechanizmust a fordítható tulajdonságok esetében.", + "Organisation": "Szervezet", + "Organisation Configuration": "Szervezetkonfiguráció", + "Organisation ID": "Szervezetazonosító", + "Organisation Members": "Szervezeti tagok", + "Organisation Statistics": "Szervezeti statisztikák", + "Organisation created successfully": "A szervezet sikeresen létrehozva", + "Organisation membership changes": "Szervezeti tagsági változások", + "Organisation name is required": "A szervezet neve kötelező", + "Organisation settings saved successfully": "A szervezet beállításai sikeresen mentve", + "Organisation successfully created": "A szervezet sikeresen létrehozva", + "Organisation successfully updated": "A szervezet sikeresen frissítve", + "Organisational measures": "Szervezeti intézkedések", + "Organisations": "Szervezetek", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Szervezet", + "Organization ID (Optional)": "Szervezetazonosító (opcionális)", + "Organizations": "Szervezetek", + "Orphaned Items": "Árva elemek", + "Other": "Egyéb", + "Output Mapping": "Kimeneti leképezés", + "Overview": "Áttekintés", + "Owner": "Tulajdonos", + "Owner:": "Tulajdonos:", + "PDF": "PDF", + "PII hits": "PII-találatok", + "PII rows": "PII-sorok", + "Page {current} of {total}": "{current}. oldal a(z) {total} oldalból", + "Parallel Mode (Faster, more resource intensive)": "Párhuzamos mód (gyorsabb, erőforrás-igényesebb)", + "Parallel:": "Párhuzamos:", + "Password": "Jelszó", + "Password changes are not supported by your authentication provider.": "A jelszómódosítást nem támogatja az Ön hitelesítési szolgáltatója.", + "Password for SOLR authentication (optional)": "Jelszó a SOLR-hitelesítéshez (opcionális)", + "Password updated successfully": "A jelszó sikeresen frissítve", + "Path": "Útvonal", + "Path in repository": "Útvonal a tárolóban", + "Path where the configuration file will be saved in the repository": "Útvonal, ahová a konfigurációs fájl mentésre kerül a tárolóban", + "Path where the register OAS file will be saved in the repository": "Útvonal, ahová a nyilvántartás OAS-fájlja mentésre kerül a tárolóban", + "Pattern (regex)": "Minta (regex)", + "Pattern Issue": "Mintaprobléma", + "Pattern constraint is missing": "A mintakorlátozás hiányzik", + "Patterns:": "Minták:", + "Pending": "Függőben", + "Permanent Delete Mode": "Végleges törlési mód", + "Permanently Delete": "Végleges törlés", + "Permanently Delete ({count})": "Végleges törlés ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Mind a(z) {count} finoman törölt objektum végleges törlése. Ezt nem lehet visszavonni!", + "Permanently delete already soft-deleted objects": "A már finoman törölt objektumok végleges törlése", + "Permanently delete this conversation?": "Véglegesen törli ezt a beszélgetést?", + "Permission Matrix": "Jogosultsági mátrix", + "Person": "Személy", + "Personal Data": "Személyes adatok", + "Personal Data Export": "Személyes adatok exportálása", + "Persons": "Személyek", + "Phone": "Telefon", + "Phone Numbers": "Telefonszámok", + "Photos": "Fényképek", + "Please create an agent in the": "Kérjük, hozzon létre egy ügynököt itt:", + "Please select a user": "Kérjük, válasszon egy felhasználót", + "Please select an agent to continue": "Kérjük, válasszon egy ügynököt a folytatáshoz", + "Please select an organisation": "Kérjük, válasszon egy szervezetet", + "Please select at least one item to import": "Kérjük, válasszon ki legalább egy importálandó elemet", + "Please select registers or schemas first": "Kérjük, először válasszon nyilvántartásokat vagy sémákat", + "Please select which register and schema to use for the new object": "Kérjük, válassza ki, melyik nyilvántartást és sémát használja az új objektumhoz", + "Please try again later.": "Kérjük, próbálja meg később.", + "Please wait while we fetch your agents.": "Kérjük, várjon, amíg lekérjük az ügynökeit.", + "Please wait while we fetch your applications.": "Kérjük, várjon, amíg lekérjük az alkalmazásait.", + "Please wait while we fetch your configurations.": "Kérjük, várjon, amíg lekérjük a konfigurációit.", + "Please wait while we fetch your deleted items.": "Kérjük, várjon, amíg lekérjük a törölt elemeit.", + "Please wait while we fetch your sources.": "Kérjük, várjon, amíg lekérjük a forrásait.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Kérjük, várjon, amíg véglegesen töröljük a SOLR-gyűjteményt. Ez eltarthat néhány pillanatig.", + "Polls": "Szavazások", + "Popular Search Terms": "Népszerű keresési kifejezések", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "A beállítások mentve", + "Prerequisites": "Előfeltételek", + "Press Enter to send, Shift+Enter for new line": "Nyomja meg az Enter billentyűt a küldéshez, a Shift+Enter billentyűt az új sorhoz", + "Press enter to send, shift+enter for new line": "Nyomja meg az enter billentyűt a küldéshez, a shift+enter billentyűt az új sorhoz", + "Prettify": "Szépítés", + "Preview erasure": "Törlés előnézete", + "Preview:": "Előnézet:", + "Previous": "Előző", + "Private": "Privát", + "Private Agent (Default)": "Privát ügynök (alapértelmezett)", + "Private agents": "Privát ügynökök", + "Process Name (optional)": "Folyamat neve (opcionális)", + "Processed {indexed} files, {failed} failed": "{indexed} fájl feldolgozva, {failed} sikertelen", + "Processes chunks in batches with simulated parallelism.": "A darabokat kötegekben dolgozza fel szimulált párhuzamossággal.", + "Processes file chunks sequentially (safest).": "A fájldarabokat egymás után dolgozza fel (legbiztonságosabb).", + "Processes objects in chunks with simulated parallelism.": "Az objektumokat darabokban dolgozza fel szimulált párhuzamossággal.", + "Processes objects sequentially (safest).": "Az objektumokat egymás után dolgozza fel (legbiztonságosabb).", + "Processing": "Feldolgozás", + "Processing Limits": "Feldolgozási korlátok", + "Processing...": "Feldolgozás...", + "Project initialized successfully": "A projekt sikeresen inicializálva", + "Project not found. Please initialize first.": "A projekt nem található. Kérjük, először inicializálja.", + "Properties": "Tulajdonságok", + "Property Behaviors": "Tulajdonság-viselkedések", + "Property Configuration:": "Tulajdonságkonfiguráció:", + "Property Filters": "Tulajdonságszűrők", + "Property Title": "Tulajdonság címe", + "Property Type": "Tulajdonság típusa", + "Property can be improved": "A tulajdonság javítható", + "Property could not be added": "A tulajdonságot nem sikerült hozzáadni", + "Property could not be updated": "A tulajdonságot nem sikerült frissíteni", + "Property name of inversed relation": "A fordított kapcsolat tulajdonságneve", + "Property successfully added": "A tulajdonság sikeresen hozzáadva", + "Property successfully updated": "A tulajdonság sikeresen frissítve", + "Protected": "Védett", + "Provider": "Szolgáltató", + "Provider is required for testing": "A szolgáltató megadása kötelező a teszteléshez", + "Public": "Nyilvános", + "Public View": "Nyilvános nézet", + "Public views can be accessed by anyone in the system": "A nyilvános nézetekhez a rendszer bárki hozzáférhet", + "Publish": "Közzététel", + "Publish Configuration to GitHub": "Konfiguráció közzététele a GitHubon", + "Publish OAS": "OAS közzététele", + "Publish Objects": "Objektumok közzététele", + "Publish Register OAS to GitHub": "Nyilvántartás OAS közzététele a GitHubon", + "Publish all objects in this schema": "A séma összes objektumának közzététele", + "Publish objects to make them publicly available": "Tegye közzé az objektumokat, hogy nyilvánosan elérhetők legyenek", + "Published": "Közzétéve", + "Published:": "Közzétéve:", + "Publishing...": "Közzététel...", + "Purge": "Tisztítás", + "Purge Date": "Tisztítási dátum", + "Purge {count} object from database": "{count} objektum tisztítása az adatbázisból", + "Purge {count} objects from database": "{count} objektum tisztítása az adatbázisból", + "Purpose limitation *": "Doelbinding *", + "Push Notifications": "Push-értesítések", + "Query Complexity Distribution": "Lekérdezés-komplexitás eloszlása", + "Query Help": "Lekérdezési súgó", + "Query parameter is required": "A lekérdezési paraméter megadása kötelező", + "Quota": "Kvóta", + "RAG": "RAG", + "RAG Configuration": "RAG-konfiguráció", + "RBAC": "RBAC", + "RBAC settings updated successfully": "A RBAC-beállítások sikeresen frissítve", + "Rate limits cleared successfully": "A sebességkorlátok sikeresen törölve", + "Raw Changes Data": "Nyers módosítási adatok", + "Re-vectorize on object update": "Újravektorizálás objektumfrissítéskor", + "Read": "Olvasás", + "Real-time push notification status via notify_push": "Valós idejű push-értesítési állapot a notify_push segítségével", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "A valós idejű push-értesítések aktívak. A csatlakozott kliensek azonnali frissítéseket kapnak, amikor objektumokat hoznak létre, frissítenek vagy törölnek.", + "Realtime push active": "Valós idejű push aktív", + "Realtime push not available — the notify_push app is not installed": "A valós idejű push nem érhető el — a notify_push alkalmazás nincs telepítve", + "Reason": "Indok", + "Reason (optional)": "Indok (opcionális)", + "Rebase operation completed successfully": "A rebase művelet sikeresen befejeződött", + "Rebase operation failed: {error}": "A rebase művelet sikertelen: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Ajánlások:", + "Recommended Type:": "Ajánlott típus:", + "Recommended: 5 sources": "Ajánlott: 5 forrás", + "Refresh": "Frissítés", + "Refresh Data": "Adatok frissítése", + "Refresh Stats": "Statisztikák frissítése", + "Refresh Workflows": "Munkafolyamatok frissítése", + "Refresh dashboard": "Irányítópult frissítése", + "Refresh database info": "Adatbázis-információk frissítése", + "Register": "Nyilvántartás", + "Register #{id}": "Nyilvántartás #{id}", + "Register / Schema": "Nyilvántartás / Séma", + "Register ID": "Nyilvántartás-azonosító", + "Register Objects": "Nyilvántartási objektumok", + "Register Overview": "Nyilvántartás áttekintése", + "Register Statistics": "Nyilvántartási statisztikák", + "Register Totals": "Nyilvántartási összesítések", + "Register and schema are required": "A nyilvántartás és a séma megadása kötelező", + "Register annotation": "Nyilvántartási megjegyzés", + "Register depublished successfully": "A nyilvántartás sikeresen depublikálva", + "Register not found": "A nyilvántartás nem található", + "Register objects": "Nyilvántartás objektumai", + "Register objects deletion completed successfully": "A nyilvántartás objektumainak törlése sikeresen befejeződött", + "Register published successfully": "A nyilvántartás sikeresen közzétéve", + "Register/Schema Usage": "Nyilvántartás/séma használata", + "Register:": "Nyilvántartás:", + "Registers": "Nyilvántartások", + "Reindex": "Újraindexelés", + "Reindex completed": "Az újraindexelés befejeződött", + "Reindex completed successfully": "Az újraindexelés sikeresen befejeződött", + "Reindex completed! Processed {count} objects in {duration}s": "Az újraindexelés befejeződött! {count} objektum feldolgozva {duration} mp alatt", + "Reindex failed": "Az újraindexelés sikertelen", + "Reindex failed: %s": "Az újraindexelés sikertelen: %s", + "Reject": "Elutasítás", + "Rejected": "Elutasítva", + "Related Cases": "Kapcsolódó ügyek", + "Relations": "Kapcsolatok", + "Remote Version": "Távoli verzió", + "Remove": "Eltávolítás", + "Remove Deck card": "Deck-kártya eltávolítása", + "Remove avatar": "Profilkép eltávolítása", + "Remove contact": "Kapcsolat eltávolítása", + "Remove filter": "Szűrő eltávolítása", + "Remove from favorites": "Eltávolítás a kedvencek közül", + "Remove from list": "Eltávolítás a listáról", + "Remove group": "Csoport eltávolítása", + "Remove link": "Hivatkozás eltávolítása", + "Remove link to {name}": "A(z) {name} hivatkozásának eltávolítása", + "Remove link to {name}?": "Eltávolítja a(z) {name} hivatkozását?", + "Remove link to {title}": "A(z) {title} hivatkozásának eltávolítása", + "Remove oneOf entry": "A oneOf bejegyzés eltávolítása", + "Remove property after write-back": "Tulajdonság eltávolítása a visszaírás után", + "Remove {title}": "{title} eltávolítása", + "Remove {userId} from this organisation?": "Eltávolítja a(z) {userId} felhasználót ebből a szervezetből?", + "Removed": "Eltávolítva", + "Removed from favorites": "Eltávolítva a kedvencek közül", + "Removing schema {schema}...": "A(z) {schema} séma eltávolítása...", + "Rename Conversation": "Beszélgetés átnevezése", + "Rename conversation": "Beszélgetés átnevezése", + "Replicas": "Replikák", + "Reports": "Jelentések", + "Repository": "Adattár", + "Request Body": "Kéréstörzs", + "Request Data": "Kérési adatok", + "Request Details": "Kérés részletei", + "Request Quota (per day)": "Kérési kvóta (naponta)", + "Request account deactivation": "Fiók deaktiválásának kérése", + "Request account deactivation. This will notify administrators for review.": "Fiók deaktiválásának kérése. Ez értesíti az adminisztrátorokat a felülvizsgálat érdekében.", + "Request body must be JSON with a \"query\" field": "A kéréstörzsnek egy \"query\" mezőt tartalmazó JSON-nak kell lennie", + "Request timeout in seconds": "Kérés időtúllépése másodpercben", + "Requested at": "Kérve ekkor", + "Required": "Kötelező", + "Required Token Scopes": "Szükséges tokenhatókörök", + "Required field": "Kötelező mező", + "Required status is inconsistent": "A kötelező állapot következetlen", + "Requires Dolphin AI for OCR text extraction": "Dolphin AI szükséges az OCR szövegkinyeréshez", + "Rerun Search": "Keresés újrafuttatása", + "Rerunning search: {searchTerm}": "Keresés újrafuttatása: {searchTerm}", + "Reset Changes": "Módosítások visszaállítása", + "Reset Filters": "Szűrők visszaállítása", + "Resource Quotas": "Erőforráskvóták", + "Response Body": "Választörzs", + "Response Details": "Válasz részletei", + "Response Time:": "Válaszidő:", + "Restore": "Visszaállítás", + "Restore conversation": "Beszélgetés visszaállítása", + "Restore or permanently delete items": "Elemek visszaállítása vagy végleges törlése", + "Restore {count} object": "{count} objektum visszaállítása", + "Restore {count} objects": "{count} objektum visszaállítása", + "Result Count Range": "Eredményszám tartománya", + "Results": "Eredmények", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Megőrzési időszak (ISO-8601 időtartam, pl. P10Y, P30D)", + "Retention settings updated successfully": "A megőrzési beállítások sikeresen frissítve", + "Retrieve all objects for this schema": "Az összes objektum lekérése ehhez a sémához", + "Retry": "Újrapróbálkozás", + "Retry Failed Extractions": "Sikertelen kinyerések újrapróbálkozása", + "Retry Policy": "Újrapróbálkozási irányelv", + "Retry completed": "Az újrapróbálkozás befejeződött", + "Retry upload": "Feltöltés újrapróbálkozása", + "Retrying...": "Újrapróbálkozás...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Tekintse át az alább kiválasztott objektumokat. Az eltávolítás gombra kattintva eltávolíthatja azokat az objektumokat, amelyeket nem szeretne törölni.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Tekintse át az alább kiválasztott objektumokat. Eltávolíthatja azokat az objektumokat, amelyeket nem szeretne belefoglalni.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Tekintse át az alább kiválasztott objektumokat. Az eltávolítás gombra kattintva eltávolíthatja azokat az objektumokat, amelyeket nem szeretne véglegesen törölni. Ez a művelet nem vonható vissza.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Tekintse át az alább kiválasztott objektumokat. Az eltávolítás gombra kattintva eltávolíthatja azokat az objektumokat, amelyeket nem szeretne visszaállítani. Az objektumok visszaállnak az eredeti helyükre.", + "Revoke": "Visszavonás", + "Right": "Jobbra", + "Risk Level": "Kockázati szint", + "Risk level": "Kockázati szint", + "Roles and Permissions": "Szerepkörök és jogosultságok", + "Roles updated successfully": "A szerepkörök sikeresen frissítve", + "Run compliance scan": "Megfelelőségi vizsgálat futtatása", + "SOLR Connection Settings": "SOLR kapcsolati beállítások", + "SOLR Index Warmup": "SOLR indexbemelegítés", + "SOLR Version:": "SOLR verzió:", + "SOLR actions menu": "SOLR műveletek menü", + "SOLR base path (usually /solr)": "SOLR alapútvonal (általában /solr)", + "SOLR collection deleted successfully": "A SOLR gyűjtemény sikeresen törölve", + "SOLR connection test failed: {error}": "A SOLR kapcsolatteszt sikertelen: {error}", + "SOLR connection test successful!": "A SOLR kapcsolatteszt sikeres!", + "SOLR index warmup completed successfully": "A SOLR indexbemelegítés sikeresen befejeződött", + "SOLR is disabled": "A SOLR le van tiltva", + "SOLR is not available or not configured": "A SOLR nem érhető el, vagy nincs konfigurálva", + "SOLR logging disabled": "A SOLR naplózás letiltva", + "SOLR logging enabled": "A SOLR naplózás engedélyezve", + "SOLR search disabled": "A SOLR keresés letiltva", + "SOLR search enabled": "A SOLR keresés engedélyezve", + "SOLR server hostname or IP address": "SOLR kiszolgáló gazdaneve vagy IP-címe", + "SOLR server port number (optional, defaults to 8983)": "SOLR kiszolgáló portszáma (opcionális, alapértelmezés szerint 8983)", + "SOLR settings updated successfully": "A SOLR beállítások sikeresen frissítve", + "SOLR setup completed successfully": "A SOLR beállítás sikeresen befejeződött", + "SOLR setup completed successfully!": "A SOLR beállítás sikeresen befejeződött!", + "SOLR setup error: %s": "SOLR beállítási hiba: %s", + "SOLR setup failed": "A SOLR beállítás sikertelen", + "SOLR setup failed - check logs": "A SOLR beállítás sikertelen – ellenőrizze a naplókat", + "SOLR setup failed. Please check the configuration and try again.": "A SOLR beállítás sikertelen. Ellenőrizze a konfigurációt, és próbálja újra.", + "SOLR setup failed: {error}": "A SOLR beállítás sikertelen: {error}", + "SOLR warmup failed: {error}": "A SOLR bemelegítés sikertelen: {error}", + "Sample Values:": "Mintaértékek:", + "Save": "Mentés", + "Save Configuration": "Konfiguráció mentése", + "Save Connection Settings": "Kapcsolati beállítások mentése", + "Save Roles": "Szerepkörök mentése", + "Save Settings": "Beállítások mentése", + "Save changes": "Módosítások mentése", + "Save current search as view": "Aktuális keresés mentése nézetként", + "Save labels": "Címkék mentése", + "Save tags for {name}": "A(z) {name} címkéinek mentése", + "Save the organisation first to manage users.": "A felhasználók kezeléséhez először mentse a szervezetet.", + "Saved Views": "Mentett nézetek", + "Saving...": "Mentés...", + "Scheduled": "Ütemezve", + "Schema": "Séma", + "Schema ID": "Séma azonosító", + "Schema Name": "Séma neve", + "Schema Statistics": "Séma statisztikái", + "Schema annotation": "Séma megjegyzése", + "Schema configuration updated successfully for {schema}": "A(z) {schema} séma konfigurációja sikeresen frissítve", + "Schema depublished successfully": "A séma közzététele sikeresen visszavonva", + "Schema objects deletion completed successfully": "A séma objektumainak törlése sikeresen befejeződött", + "Schema published successfully": "A séma sikeresen közzétéve", + "Schema reference ($ref)": "Sémahivatkozás ($ref)", + "Schema successfully created": "A séma sikeresen létrehozva", + "Schema successfully updated": "A séma sikeresen frissítve", + "Schema {schema} removed successfully": "A(z) {schema} séma sikeresen eltávolítva", + "Schema:": "Séma:", + "Schemas": "Sémák", + "Scheme": "Séma", + "Search": "Keresés", + "Search / Views": "Keresés / nézetek", + "Search / views": "Keresés / nézetek", + "Search Activity": "Keresési tevékenység", + "Search Analytics": "Keresési analitika", + "Search Configuration": "Keresési konfiguráció", + "Search Entities": "Entitások keresése", + "Search Files": "Fájlok keresése", + "Search GitHub": "Keresés a GitHubon", + "Search GitLab": "Keresés a GitLabon", + "Search Method": "Keresési módszer", + "Search Mode": "Keresési mód", + "Search Query": "Keresési lekérdezés", + "Search Results": "Keresési eredmények", + "Search Term": "Keresési kifejezés", + "Search Trail Details": "Keresési napló részletei", + "Search Trail Management": "Keresési napló kezelése", + "Search Trail Statistics": "Keresési napló statisztikái", + "Search Trails": "Keresési naplók", + "Search Views": "Nézetek keresése", + "Search Webhooks": "Webhookok keresése", + "Search agents...": "Ügynökök keresése...", + "Search applications...": "Alkalmazások keresése...", + "Search by file name or path": "Keresés fájlnév vagy útvonal alapján", + "Search by name or URL": "Keresés név vagy URL alapján", + "Search by title or UUID...": "Keresés cím vagy UUID alapján...", + "Search by value": "Keresés érték alapján", + "Search configurations": "Konfigurációk keresése", + "Search data sources...": "Adatforrások keresése...", + "Search groups...": "Csoportok keresése...", + "Search in Database Objects": "Keresés az adatbázis-objektumokban", + "Search in Files": "Keresés a fájlokban", + "Search in Objects": "Keresés az objektumokban", + "Search objects": "Objektumok keresése", + "Search objects...": "Objektumok keresése...", + "Search property names...": "Tulajdonságnevek keresése...", + "Search registers...": "Nyilvántartások keresése...", + "Search schemas...": "Sémák keresése...", + "Search trail deletion not implemented yet": "A keresési napló törlése még nincs megvalósítva", + "Search views...": "Nézetek keresése...", + "Search {name}...": "{name} keresése...", + "Searching...": "Keresés...", + "Secret": "Titok", + "Secret key for HMAC signature generation (optional)": "Titkos kulcs HMAC-aláírás előállításához (opcionális)", + "Security": "Biztonság", + "See organisations as a table": "Szervezetek megtekintése táblázatként", + "See organisations as cards": "Szervezetek megtekintése kártyaként", + "Select AI Agent": "AI ügynök kiválasztása", + "Select Active Organisation": "Aktív szervezet kiválasztása", + "Select All": "Összes kiválasztása", + "Select ConfigSet": "ConfigSet kiválasztása", + "Select File Types to Vectorize:": "Vektorizálandó fájltípusok kiválasztása:", + "Select HTTP method": "HTTP metódus kiválasztása", + "Select Register and Schema": "Nyilvántartás és séma kiválasztása", + "Select Views to Vectorize:": "Vektorizálandó nézetek kiválasztása:", + "Select a Nextcloud group to add": "Válasszon egy hozzáadandó Nextcloud csoportot", + "Select a branch": "Válasszon egy ágat", + "Select a date property": "Válasszon egy dátumtulajdonságot", + "Select a model or type a custom model name": "Válasszon egy modellt, vagy adjon meg egyéni modellnevet", + "Select a register": "Válasszon egy nyilvántartást", + "Select a register...": "Válasszon egy nyilvántartást...", + "Select a repository": "Válasszon egy adattárat", + "Select a repository you have write access to": "Válasszon egy adattárat, amelyhez írási hozzáférése van", + "Select a schema": "Válasszon egy sémát", + "Select a schema...": "Válasszon egy sémát...", + "Select a user": "Válasszon egy felhasználót", + "Select agent type": "Ügynöktípus kiválasztása", + "Select all entities": "Összes entitás kiválasztása", + "Select an AI Agent": "Válasszon egy AI ügynököt", + "Select an AI agent to begin chatting with your data.": "Válasszon egy AI ügynököt, hogy elkezdhessen csevegni az adataival.", + "Select an email to see linked objects": "Válasszon egy e-mailt a kapcsolt objektumok megtekintéséhez", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Válasszon egy szervezetet és egy felhasználót, hogy tagként hozzáadja őket. Keressen szervezeteket név alapján.", + "Select backend": "Háttérrendszer kiválasztása", + "Select chat model": "Csevegési modell kiválasztása", + "Select collection for files": "Gyűjtemény kiválasztása a fájlokhoz", + "Select collection for objects": "Gyűjtemény kiválasztása az objektumokhoz", + "Select configuration type...": "Konfigurációtípus kiválasztása...", + "Select default organisation": "Alapértelmezett szervezet kiválasztása", + "Select entity {value}": "A(z) {value} entitás kiválasztása", + "Select event to listen to...": "Válassza ki a figyelendő eseményt...", + "Select fields to display": "Megjelenítendő mezők kiválasztása", + "Select files with the correct extension": "Válasszon megfelelő kiterjesztésű fájlokat", + "Select group": "Csoport kiválasztása", + "Select groups (optional)": "Csoportok kiválasztása (opcionális)", + "Select groups to notify...": "Válassza ki az értesítendő csoportokat...", + "Select groups with access to this agent": "Válassza ki az ehhez az ügynökhöz hozzáféréssel rendelkező csoportokat", + "Select groups...": "Csoportok kiválasztása...", + "Select model": "Modell kiválasztása", + "Select one or more registers": "Válasszon ki egy vagy több nyilvántartást", + "Select one or more schemas": "Válasszon ki egy vagy több sémát", + "Select options...": "Beállítások kiválasztása...", + "Select or create labels, or select \"No label\" to add files": "Válasszon ki vagy hozzon létre címkéket, vagy válassza a \"Nincs címke\" lehetőséget fájlok hozzáadásához", + "Select owner application (optional)...": "Válassza ki a tulajdonos alkalmazást (opcionális)...", + "Select period": "Időszak kiválasztása", + "Select property to send as payload": "Válassza ki a hasznos adatként küldendő tulajdonságot", + "Select provider": "Szolgáltató kiválasztása", + "Select registers and schemas to save a view": "Válasszon nyilvántartásokat és sémákat egy nézet mentéséhez", + "Select retry policy": "Újrapróbálkozási irányelv kiválasztása", + "Select role": "Szerepkör kiválasztása", + "Select schemas (empty = all schemas)": "Sémák kiválasztása (üres = összes séma)", + "Select schemas to warm up": "Válassza ki a bemelegítendő sémákat", + "Select search mode": "Keresési mód kiválasztása", + "Select source type...": "Forrástípus kiválasztása...", + "Select target type": "Céltípus kiválasztása", + "Select the branch to publish to": "Válassza ki a közzétételhez használandó ágat", + "Select the event this webhook should listen to": "Válassza ki azt az eseményt, amelyet ennek a webhooknak figyelnie kell", + "Select views to filter data (optional)": "Válasszon nézeteket az adatok szűréséhez (opcionális)", + "Select views to limit which data the agent can access": "Válasszon nézeteket annak korlátozásához, hogy az ügynök mely adatokhoz férhet hozzá", + "Select views to vectorize:": "Válassza ki a vektorizálandó nézeteket:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Válassza ki, hogy mely Nextcloud csoportok érhetők el ehhez a szervezethez. Az ezekben a csoportokban lévő felhasználók hozzáférnek a szervezet erőforrásaihoz.", + "Select which data views the AI can search": "Válassza ki, hogy mely adatnézetekben kereshet az AI", + "Select which property from the event should be used as the webhook payload data": "Válassza ki, hogy az esemény mely tulajdonságát kell webhook hasznos adatként használni", + "Select which tools the AI can use to perform actions": "Válassza ki, hogy az AI mely eszközöket használhatja műveletek végrehajtásához", + "Select {fieldName} values": "A(z) {fieldName} értékeinek kiválasztása", + "Selected": "Kiválasztva", + "Selected Groups": "Kiválasztott csoportok", + "Selected Objects ({count})": "Kiválasztott objektumok ({count})", + "Selected audit trails deleted successfully": "A kiválasztott naplóbejegyzések sikeresen törölve", + "Selected search trails deleted successfully": "A kiválasztott keresési naplók sikeresen törölve", + "Selected users can access this view": "A kiválasztott felhasználók hozzáférhetnek ehhez a nézethez", + "Send additional feedback": "További visszajelzés küldése", + "Send as CloudEvent": "Küldés CloudEventként", + "Send as cloudevent": "Küldés cloudeventként", + "Sensitive PII": "Érzékeny személyazonosító adatok", + "Serial Mode (Safer, slower)": "Soros mód (biztonságosabb, lassabb)", + "Serial:": "Soros:", + "Server Configuration": "Kiszolgáló konfigurációja", + "Server Information": "Kiszolgáló információi", + "Set as Active": "Beállítás aktívként", + "Set as active organisation": "Beállítás aktív szervezetként", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Állítson be korlátokat az API-használathoz és a tokenfogyasztáshoz. Korlátlan erőforrásokhoz használja a 0 értéket.", + "Settings": "Beállítások", + "Settings updated successfully": "A beállítások sikeresen frissítve", + "Shards": "Szilánkok", + "Share": "Megosztás", + "Share with Groups": "Megosztás csoportokkal", + "Share with Users": "Megosztás felhasználókkal", + "Show": "Megjelenítés", + "Show Filters": "Szűrők megjelenítése", + "Show less": "Kevesebb megjelenítése", + "Show only entries with changes": "Csak a módosításokat tartalmazó bejegyzések megjelenítése", + "Showing {count} dashboard(s)": "{count} irányítópult megjelenítése", + "Showing {count} processing activities": "{count} verwerkingsactiviteit megjelenítése", + "Showing {showing} of {total} agents": "{total} ügynökből {showing} megjelenítése", + "Showing {showing} of {total} applications": "{total} alkalmazásból {showing} megjelenítése", + "Showing {showing} of {total} audit trail entries": "{total} naplóbejegyzésből {showing} megjelenítése", + "Showing {showing} of {total} configurations": "{total} konfigurációból {showing} megjelenítése", + "Showing {showing} of {total} deleted items": "{total} törölt elemből {showing} megjelenítése", + "Showing {showing} of {total} entities": "{total} entitásból {showing} megjelenítése", + "Showing {showing} of {total} files": "{total} fájlból {showing} megjelenítése", + "Showing {showing} of {total} log entries": "{total} naplóbejegyzésből {showing} megjelenítése", + "Showing {showing} of {total} search trail entries": "{total} keresési naplóbejegyzésből {showing} megjelenítése", + "Showing {showing} of {total} sources": "{total} forrásból {showing} megjelenítése", + "Showing {showing} of {total} templates": "{total} sablonból {showing} megjelenítése", + "Showing {showing} of {total} webhooks": "{total} webhookból {showing} megjelenítése", + "Showing {shown} of {total} organisations": "{total} szervezetből {shown} megjelenítése", + "Simple": "Egyszerű", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Egyszerű lekérdezések: alapvető szövegkeresések minimális paraméterekkel (pl. egyetlen keresési kifejezés, speciális szűrők nélkül)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Egyszerű lekérdezések: alapvető szövegkeresések minimális paraméterekkel (pl. egyetlen keresési kifejezés, speciális szűrők nélkül)", + "Size": "Méret", + "Slug": "Slug", + "Soft Delete Mode": "Lágy törlési mód", + "Soft Deleted Items": "Lágyan törölt elemek", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "A séma összes objektumának lágy törlése ({active} aktív, {deleted} már törölve)", + "SolrCloud mode enabled": "SolrCloud mód engedélyezve", + "Something went wrong while adding files": "Hiba történt a fájlok hozzáadása közben", + "Source": "Forrás", + "Source successfully updated": "A forrás sikeresen frissítve", + "Sources": "Források", + "Sources:": "Források:", + "Special Rights": "Különleges jogok", + "Standalone SOLR mode": "Önálló SOLR mód", + "Start Conversation": "Beszélgetés indítása", + "Start Date Field": "Kezdődátum mező", + "Start Vectorization": "Vektorizálás indítása", + "Start a conversation": "Beszélgetés indítása", + "Starting names cache warmup...": "A nevek gyorsítótárának bemelegítése elkezdődött...", + "Starting validation for {schema}...": "A(z) {schema} érvényesítése elkezdődött...", + "Starting {action} for {schema}...": "A(z) {action} elkezdődött a(z) {schema} esetében...", + "Starting...": "Indítás...", + "Statistics": "Statisztikák", + "Statistics and Metrics": "Statisztikák és mérőszámok", + "Statistics refreshed": "A statisztikák frissítve", + "Stats calculation not yet implemented": "A statisztikaszámítás még nincs megvalósítva", + "Status": "Állapot", + "Status Code": "Állapotkód", + "Step": "Lépés", + "Storage": "Tárhely", + "Storage Quota (MB)": "Tárhelykvóta (MB)", + "String Constraints": "Karakterlánc-megszorítások", + "Subject identifier (email, BSN, name, etc.)": "Alany azonosítója (e-mail, BSN, név stb.)", + "Subscribe": "Feliratkozás", + "Subscribed": "Feliratkozva", + "Success": "Sikeres", + "Success Rate": "Sikerességi arány", + "Success Status": "Sikerállapot", + "Successful": "Sikeres", + "Successfully added user to organisation: {name}": "A felhasználó sikeresen hozzáadva a szervezethez: {name}", + "Successfully cleared {count} audit trails": "{count} naplóbejegyzés sikeresen törölve", + "Successfully cleared {count} blob storage objects": "{count} blobtároló-objektum sikeresen törölve", + "Successfully cleared {count} search trails": "{count} keresési napló sikeresen törölve", + "Successfully created {count} SOLR fields": "{count} SOLR mező sikeresen létrehozva", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "{count} beágyazás sikeresen törölve. Vektorizálja újra az adatait.", + "Successfully deleted {count} objects for {schema}": "{count} objektum sikeresen törölve a(z) {schema} esetében", + "Successfully fixed {count} SOLR field configurations": "{count} SOLR mezőkonfiguráció sikeresen javítva", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Sikeresen importálva: {registers} nyilvántartás, {schemas} séma, {objects} objektum", + "Successfully joined organisation": "Sikeresen csatlakozott a szervezethez", + "Successfully permanently deleted {count} objects": "{count} objektum sikeresen véglegesen törölve", + "Successfully processed {count} files!": "{count} fájl sikeresen feldolgozva!", + "Successfully restored {count} object": "{count} objektum sikeresen visszaállítva", + "Successfully restored {count} objects": "{count} objektum sikeresen visszaállítva", + "Successfully saved configuration for {count} facets!": "{count} facet konfigurációja sikeresen mentve!", + "Supported File Types": "Támogatott fájltípusok", + "Switch Active Organisation": "Aktív szervezet váltása", + "Switch Organisation": "Szervezet váltása", + "Sync Table": "Tábla szinkronizálása", + "Synchronization Settings": "Szinkronizálási beállítások", + "Syncing magic table for {schema}...": "A(z) {schema} magic táblájának szinkronizálása...", + "Syncs": "Szinkronizálások", + "System Default": "Rendszer alapértelmezése", + "System Prompt": "Rendszerprompt", + "System announcements": "Rendszerközlemények", + "Table": "Táblázat", + "Target ID": "Cél azonosítója", + "Target Type*": "Céltípus*", + "Technical Description": "Műszaki leírás", + "Technical Details": "Műszaki részletek", + "Technical description for developers and administrators": "Műszaki leírás fejlesztők és adminisztrátorok számára", + "Technical measures": "Műszaki intézkedések", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Hőmérséklet", + "Templates": "Sablonok", + "Test": "Teszt", + "Test Chat": "Csevegés tesztelése", + "Test Connection": "Kapcsolat tesztelése", + "Test Embedding": "Beágyazás tesztelése", + "Test endpoint executed successfully": "A tesztvégpont sikeresen lefutott", + "Test webhook delivered successfully": "A tesztwebhook sikeresen kézbesítve", + "Test webhook delivery failed": "A tesztwebhook kézbesítése sikertelen", + "Test webhook sent successfully": "A tesztwebhook sikeresen elküldve", + "Testing...": "Tesztelés...", + "Text Chunk #{id}": "Szövegrészlet #{id}", + "Text Extraction": "Szövegkinyerés", + "Text Extraction Engine": "Szövegkinyerő motor", + "Text chunks": "Szövegrészletek", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "A szövegrészletek a fájlkinyerés során jönnek létre, és az adatbázisban tárolódnak. A vektorizálás beolvassa ezeket az előre feldarabolt fájlokat, és beágyazásokká alakítja őket.", + "Text extracted successfully": "A szöveg sikeresen kinyerve", + "Text extraction disabled": "A szövegkinyerés letiltva", + "The Nextcloud Calendar app is not installed or enabled on this server.": "A Nextcloud Naptár alkalmazás nincs telepítve vagy engedélyezve ezen a kiszolgálón.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "A Nextcloud Névjegyek alkalmazás nincs telepítve vagy engedélyezve ezen a kiszolgálón.", + "The Nextcloud Deck app is not installed or enabled on this server.": "A Nextcloud Deck alkalmazás nincs telepítve vagy engedélyezve ezen a kiszolgálón.", + "The Nextcloud Mail app is not installed or enabled on this server.": "A Nextcloud Mail alkalmazás nincs telepítve vagy engedélyezve ezen a kiszolgálón.", + "The URL where webhook events will be sent": "Az URL, ahová a webhook események elküldésre kerülnek", + "The requested conversation does not exist": "A kért beszélgetés nem létezik", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "A kért irányítópult nem tölthető be. Ellenőrizze az URL-t, vagy válasszon másik irányítópultot a listából.", + "The saved view and all its search configuration": "A mentett Nézet és annak teljes keresési konfigurációja", + "There are no audit trail entries matching your current filters.": "Nincsenek a jelenlegi szűrőknek megfelelő naplóbejegyzések.", + "There are no deleted items matching your current filters.": "Nincsenek a jelenlegi szűrőknek megfelelő törölt elemek.", + "There are no search trail entries matching your current filters.": "Nincsenek a jelenlegi szűrőknek megfelelő keresési naplóbejegyzések.", + "There are no webhook log entries matching your filters.": "Nincsenek a szűrőknek megfelelő webhook naplóbejegyzések.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Megőrzésükre a Sémájuk beállított Bewaartermijn szerint kerül sor, és a megőrzési időszak lejártakor automatikusan, véglegesen törlődnek. A megőrzési időszak Sémánként konfigurálható, és a Séma beállításai között található.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Megőrzésükre a Sémájuk beállított Bewaartermijn szerint kerül sor, majd véglegesen eltávolításra kerülnek.", + "This action cannot be undone.": "Ez a művelet nem vonható vissza.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Ez a művelet nem vonható vissza. Győződjön meg arról, hogy egyetlen Gyűjtemény sem használja ezt a ConfigSetet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Ez a művelet nem vonható vissza. Győződjön meg arról, hogy egyetlen Gyűjtemény sem használja ezt a configsetet.", + "This action will submit a deactivation request to your administrators.": "Ez a művelet deaktiválási kérelmet küld a rendszergazdáknak.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Ennek az Ügynöknek nincsenek konfigurálható nézetei vagy eszközei, de a RAG beállításait továbbra is módosíthatja alább.", + "This analysis may take some time": "Ez az elemzés eltarthat egy ideig", + "This audit trail entry does not contain any change information.": "Ez a naplóbejegyzés nem tartalmaz változásinformációt.", + "This configuration has no data defined.": "Ehhez a konfigurációhoz nincs adat megadva.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ez a végpont elavult. Használjon helyette darabalapú (chunk) végpontokat.", + "This entity has no relations to objects or files": "Ennek az entitásnak nincsenek kapcsolatai objektumokhoz vagy fájlokhoz", + "This is a test webhook from OpenRegister": "Ez egy teszt webhook az OpenRegisterből", + "This is not valid JSON": "Ez nem érvényes JSON", + "This key already exists on this schema": "Ez a kulcs már létezik ezen a Sémán", + "This schema must use Magic Table configuration to sync": "Ennek a Sémának Magic Table konfigurációt kell használnia a szinkronizáláshoz", + "This schema must use magic table configuration to sync": "Ennek a Sémának magic table konfigurációt kell használnia a szinkronizáláshoz", + "This source has no associated registers.": "Ehhez a Forráshoz nem tartoznak Nyilvántartások.", + "This token will only be shown once. Copy it now.": "Ez a token csak egyszer jelenik meg. Másolja ki most.", + "This will copy the _default ConfigSet with the new name": "Ez lemásolja a _default ConfigSetet az új névvel", + "This will copy the _default configset with the new name": "Ez lemásolja a _default configsetet az új névvel", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Ez véglegesen törli az ÖSSZES Beágyazást (vektort) az adatbázisból. Újra kell vektorizálnia az összes objektumot és fájlt. Ez a művelet nem vonható vissza.\n\nBiztosan folytatja?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Ez véglegesen törli az ÖSSZES Beágyazást (vektort) az adatbázisból. Újra kell vektorizálnia az összes objektumot és fájlt. Ez a művelet nem vonható vissza.\\n\\nBiztosan folytatja?", + "This will permanently delete:": "Ez véglegesen törli a következőket:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Ez véglegesen eltávolít {count} már puhán törölt objektumot az adatbázisból. Ez a művelet nem vonható vissza.", + "Timeout (seconds)": "Időtúllépés (másodperc)", + "Timestamp": "Időbélyeg", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tipp: A beágyazási költségek minimalizálása érdekében csak azokat a nézeteket engedélyezze, amelyeknek szemantikus keresésre van szükségük. Az egyszerű kereső táblák ritkán igényelnek vektorizálást.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tipp: a beágyazási költségek minimalizálása érdekében csak azokat a nézeteket engedélyezze, amelyeknek szemantikus keresésre van szükségük. Az egyszerű kereső táblák ritkán igényelnek vektorizálást.", + "Title": "Cím", + "Title *": "Cím *", + "Title Template": "Címsablon", + "Title*": "Cím*", + "To (e.g. -1 year or 2025-12-31)": "Eddig (pl. -1 year vagy 2025-12-31)", + "To Date": "Eddigi dátum", + "To add files larger than or equal to 512MB, go to the": "Az 512 MB-os vagy annál nagyobb fájlok hozzáadásához lépjen a következőre:", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "A darabméret vagy a stratégia módosításához lépjen a Fájlkonfiguráció → Feldolgozási korlátok menüpontra.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "A darabméret vagy a stratégia módosításához lépjen a fájlkonfiguráció → feldolgozási korlátok menüpontra.", + "To change the embedding provider or model, go to LLM Configuration.": "A beágyazási szolgáltató vagy modell módosításához lépjen az LLM konfigurációhoz.", + "To change the embedding provider or model, go to LLM configuration.": "A beágyazási szolgáltató vagy modell módosításához lépjen az LLM konfigurációhoz.", + "To date": "Eddigi dátum", + "Toggle search sidebar": "Keresési oldalsáv be- és kikapcsolása", + "Token Created": "Token létrehozva", + "Token Quota (per request)": "Token kvóta (kérésenként)", + "Token copied to clipboard": "Token a vágólapra másolva", + "Token name": "Token neve", + "Token revoked": "Token visszavonva", + "Tools": "Eszközök", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Az eszközök az Ügynök alapértelmezett felhasználói jogosultságaival futnak, amikor nincs aktív felhasználói munkamenet (pl. cron feladatok). Az alapértelmezett felhasználót a Beállítások lapon konfigurálhatja.", + "Tools respect the agent's own role and organization boundaries.": "Az eszközök tiszteletben tartják az Ügynök saját szerepkörét és szervezeti határait.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Az eszközök tiszteletben tartják az Ügynök nézeteit, jogosultságait és szervezeti határait.", + "Top Deleters": "Legtöbbet törlők", + "Total": "Összesen", + "Total Audit Trails": "Összes naplóbejegyzés", + "Total Chunks Available:": "Összes elérhető darab:", + "Total Deleted Items": "Összes törölt elem", + "Total Members": "Összes tag", + "Total Objects": "Összes objektum", + "Total Objects in Database:": "Összes objektum az adatbázisban:", + "Total Organisations": "Összes Szervezet", + "Total Results": "Összes eredmény", + "Total Searches": "Összes keresés", + "Total Size": "Teljes méret", + "Total Storage": "Teljes tárhely", + "Total events": "Összes esemény", + "Total:": "Összesen:", + "Totals": "Összesítések", + "Try searching by UUID or with different keywords": "Próbáljon meg UUID alapján vagy más kulcsszavakkal keresni", + "Type": "Típus", + "Type \"DELETE COLLECTION\" to confirm:": "Írja be a \"DELETE COLLECTION\" szöveget a megerősítéshez:", + "Type Issue": "Típushiba", + "Type Variations:": "Típusváltozatok:", + "Type filter (optional, e.g. email)": "Típusszűrő (opcionális, pl. email)", + "Type must be \"positive\" or \"negative\"": "A típusnak \"positive\" vagy \"negative\" értékűnek kell lennie", + "Type to search for groups": "Gépeljen csoportok kereséséhez", + "Type to search for objects...": "Gépeljen objektumok kereséséhez...", + "Type to search for organisations": "Gépeljen Szervezetek kereséséhez", + "Type to search for users": "Gépeljen felhasználók kereséséhez", + "Type to search groups...": "Gépeljen csoportok kereséséhez...", + "Type to search users...": "Gépeljen felhasználók kereséséhez...", + "Type to search...": "Gépeljen a kereséshez...", + "Type your username to confirm": "Írja be a felhasználónevét a megerősítéshez", + "URL": "URL", + "URL where Ollama is running": "Az URL, ahol az Ollama fut", + "UUID copied to clipboard": "UUID a vágólapra másolva", + "UUID:": "UUID:", + "Unchanged": "Változatlan", + "Unique Search Terms": "Egyedi keresési kifejezések", + "Unique Terms": "Egyedi kifejezések", + "Unknown": "Ismeretlen", + "Unknown Object": "Ismeretlen objektum", + "Unknown Register": "Ismeretlen Nyilvántartás", + "Unknown Schema": "Ismeretlen Séma", + "Unknown error": "Ismeretlen hiba", + "Unknown widget type: {type}": "Ismeretlen widget típus: {type}", + "Unlink email": "E-mail leválasztása", + "Unlink event": "Esemény leválasztása", + "Unnamed Object": "Névtelen objektum", + "Untitled": "Cím nélküli", + "Untitled View": "Cím nélküli Nézet", + "Update": "Frissítés", + "Update Operations": "Frissítési műveletek", + "Update available: {local} → {remote}": "Frissítés érhető el: {local} → {remote}", + "Update configuration: ...": "Konfiguráció frissítése: ...", + "Update register OAS: ...": "Nyilvántartás OAS frissítése: ...", + "Update vectors when object data changes (recommended for accurate search)": "Vektorok frissítése, amikor az objektum adatai megváltoznak (pontos keresés esetén ajánlott)", + "Updated": "Frissítve", + "Updated At": "Frissítve ekkor", + "Updated:": "Frissítve:", + "Upload new avatar": "Új avatar feltöltése", + "Uploading configuration sets...": "Konfigurációs készletek feltöltése...", + "Uptime:": "Üzemidő:", + "Url": "Url", + "Usage Count": "Használati szám", + "Use AI agents for processing and analysis": "AI ügynökök használata feldolgozáshoz és elemzéshez", + "Use Large Language Model features": "Large Language Model (LLM) funkciók használata", + "Use SolrCloud with Zookeeper for distributed search": "SolrCloud használata Zookeeperrel az elosztott kereséshez", + "Use SolrCloud with zookeeper for distributed search": "SolrCloud használata zookeeperrel az elosztott kereséshez", + "Use VNG APIs instead of MongoDB.": "VNG API-k használata a MongoDB helyett.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Alapértelmezett tartományok használata (utolsó 7/30/90 nap, tavalyi év, régebbi)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Külső tároló (pl. MongoDB) használata a belső Nextcloud tároló helyett.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Szűrők használatával szűkítheti a naplóbejegyzéseket Nyilvántartás, Séma, művelettípus, felhasználó, dátumtartomány vagy objektum-ID szerint.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Szűrők használatával szűkítheti a törölt elemeket Nyilvántartás, Séma, törlési dátum vagy a törlést végző felhasználó szerint.", + "Use filters to narrow down entities by type or category.": "Szűrők használatával szűkítheti az entitásokat típus vagy kategória szerint.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Szűrők használatával szűkítheti a keresési naplóbejegyzéseket Nyilvántartás, Séma, sikerállapot, felhasználó, dátumtartomány, keresési kifejezések vagy teljesítménymutatók szerint.", + "Used By Collections": "Gyűjtemények használják", + "Used by": "Használja", + "User": "Felhasználó", + "User Agent Statistics": "User Agent statisztikák", + "User Description": "Felhasználói leírás", + "User Quota": "Felhasználói kvóta", + "User rate limits cleared successfully": "A felhasználói sebességkorlátok sikeresen törölve", + "User removed successfully": "A felhasználó sikeresen eltávolítva", + "User-friendly description shown in forms and help text": "Az űrlapokon és súgószövegekben megjelenő, felhasználóbarát leírás", + "Username": "Felhasználónév", + "Username for SOLR authentication (optional)": "Felhasználónév a SOLR hitelesítéshez (opcionális)", + "Users": "Felhasználók", + "Uses": "Használatok", + "Using Pre-Generated Chunks": "Előre generált darabok használata", + "Validate": "Érvényesítés", + "Validate Objects": "Objektumok érvényesítése", + "Validation completed for {schema}": "Az érvényesítés befejeződött a következőhöz: {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Az érvényesítés befejeződött a következőhöz: {schema}: {processed} feldolgozva, {updated} frissítve, {failed} sikertelen", + "Validation completed successfully": "Az érvényesítés sikeresen befejeződött", + "Validation failed": "Az érvényesítés sikertelen", + "Value": "Érték", + "Value list (split on ,)": "Értéklista (vesszővel elválasztva)", + "Vector Dimensions": "Vektordimenziók", + "Vector Search Backend": "Vektorkeresési háttér", + "Vector field: _embedding_": "Vektormező: _embedding_", + "Vectorization Triggers": "Vektorizálási kiváltók", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "A vektorizálás befejeződött: {vectorized} objektum vektorizálva, {failed} sikertelen", + "Vectorization failed: {error}": "A vektorizálás sikertelen: {error}", + "Vectorize All Files": "Összes fájl vektorizálása", + "Vectorize All Objects": "Összes objektum vektorizálása", + "Vectorize all file types": "Összes fájltípus vektorizálása", + "Vectorize all views": "Összes nézet vektorizálása", + "Vectorize on object creation": "Vektorizálás objektum létrehozásakor", + "Vectors will be stored in your existing object and file collections": "A vektorok a meglévő objektum- és fájlgyűjteményeiben kerülnek tárolásra", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Kiszolgáló kapcsolatának ellenőrzése...", + "Version": "Verzió", + "Version Information": "Verzióinformáció", + "Very High": "Nagyon magas", + "Very high": "Nagyon magas", + "View": "Nézet", + "View API Docs": "API dokumentáció megtekintése", + "View API Documentation": "API dokumentáció megtekintése", + "View APIs": "API-k megtekintése", + "View Changes": "Változások megtekintése", + "View Details": "Részletek megtekintése", + "View Error": "Hiba megtekintése", + "View File": "Fájl megtekintése", + "View Full Details": "Teljes részletek megtekintése", + "View Logs": "Naplók megtekintése", + "View Name": "Nézet neve", + "View Object": "Objektum megtekintése", + "View Parameters": "Paraméterek megtekintése", + "View Selection": "Nézet kiválasztása", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Keresési naplók megtekintése és elemzése speciális szűrési és analitikai képességekkel", + "View and analyze system audit trails with advanced filtering capabilities": "Rendszer-naplóbejegyzések megtekintése és elemzése speciális szűrési képességekkel", + "View and manage authorization across registers and schemas": "Jogosultságok megtekintése és kezelése Nyilvántartások és Sémák között", + "View deleted successfully": "A Nézet sikeresen törölve", + "View entity details and manage relations": "Entitás részleteinek megtekintése és kapcsolatok kezelése", + "View name": "Nézet neve", + "View name is required": "A Nézet neve kötelező", + "View object": "Objektum megtekintése", + "View saved successfully!": "A Nézet sikeresen mentve!", + "View search analytics and manage search logs": "Keresési analitika megtekintése és keresési naplók kezelése", + "View successfully deleted": "A Nézet sikeresen törölve", + "View successfully updated": "A Nézet sikeresen frissítve", + "View updated successfully!": "A Nézet sikeresen frissítve!", + "View webhook delivery logs and filter by webhook": "Webhook kézbesítési naplók megtekintése és szűrés webhook szerint", + "View {count} more": "Még {count} megtekintése", + "Views": "Nézetek", + "Visible to end users": "Látható a végfelhasználók számára", + "Visible to users": "Látható a felhasználók számára", + "Wait for Response": "Várakozás a válaszra", + "Wait for webhook response before continuing (required for request/response flows)": "Várakozás a webhook válaszára a folytatás előtt (kérés/válasz folyamatokhoz szükséges)", + "Waiting for cluster synchronization...": "Várakozás a fürt szinkronizálására...", + "Warmup Names Cache": "Nevek gyorsítótárának bemelegítése", + "Warmup failed": "A bemelegítés sikertelen", + "Warning": "Figyelmeztetés", + "Warning:": "Figyelmeztetés:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Figyelmeztetés: Ennek a Tulajdonságnak inkonzisztens típusai vannak: {types}. Fontolja meg, hogy minden objektumnak azonos típusúnak kell-e lennie.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Átvizsgáljuk az ehhez a Sémához tartozó összes objektumot, hogy felfedezzük az új Tulajdonságokat, és elemezzük a meglévő Tulajdonságokat a lehetséges fejlesztések érdekében. A folyamat magában foglalja az egyes objektumok adatstruktúrájának vizsgálatát, a jelenlegi Sémában nem definiált Tulajdonságok azonosítását, valamint a meglévő Tulajdonság-definíciók jobb megszorításokkal, formátumokkal és érvényesítési szabályokkal történő javítási lehetőségeinek megtalálását.", + "Webhook": "Webhook", + "Webhook Log Details": "Webhook napló részletei", + "Webhook Logs": "Webhook naplók", + "Webhook created successfully": "A Webhook sikeresen létrehozva", + "Webhook deleted": "Webhook törölve", + "Webhook retry delivered successfully": "A Webhook újrapróbálkozás sikeresen kézbesítve", + "Webhook retry delivery failed": "A Webhook újrapróbálkozás kézbesítése sikertelen", + "Webhook updated": "Webhook frissítve", + "Webhook updated successfully": "A Webhook sikeresen frissítve", + "Webhooks": "Webhookok", + "Weekly": "Hetente", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Amikor az Ügynök felhasználói munkamenet nélkül fut (pl. ütemezett feladatok), ennek a felhasználónak a kontextusa kerül felhasználásra", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Amikor az Ügynök felhasználói munkamenet nélkül fut (pl. ütemezett feladatok), ennek a felhasználónak a kontextusa kerül felhasználásra.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Ha le van tiltva, ez a Facet csak ennek a Sémának az értékeit jeleníti meg, és kiválasztáskor Séma szűrőt tartalmaz.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Ha engedélyezve van, ennek az objektumnak a mentése a cél objektumokat is frissíti, hogy azok visszahivatkozást tartalmazzanak erre az objektumra.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Ha engedélyezve van, ez a Tulajdonság eltávolításra kerül a Forrás objektumból a cél objektumok frissítése után.", + "Workflow Management": "Munkafolyamat-kezelés", + "Workflows": "Munkafolyamatok", + "Wrap webhook payload in CloudEvents format for better interoperability": "A webhook adattartalmának CloudEvents formátumba csomagolása a jobb interoperabilitás érdekében", + "Wrap webhook payload in cloudevents format for better interoperability": "A webhook adattartalmának cloudevents formátumba csomagolása a jobb interoperabilitás érdekében", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Igen", + "You": "Ön", + "You are not yet a member of any organisations.": "Ön még nem tagja egyetlen Szervezetnek sem.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Létrehozhat új ConfigSeteket a _default sablon alapján, vagy közvetlenül feltölthet egyedieket a SOLR kiszolgálóra.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Létrehozhat új configseteket a _default sablon alapján, vagy közvetlenül feltölthet egyedieket a SOLR kiszolgálóra.", + "You do not have access to this conversation": "Önnek nincs hozzáférése ehhez a beszélgetéshez", + "You do not have permission to delete this conversation": "Önnek nincs jogosultsága ennek a beszélgetésnek a törléséhez", + "You do not have permission to modify this conversation": "Önnek nincs jogosultsága ennek a beszélgetésnek a módosításához", + "You do not have permission to restore this conversation": "Önnek nincs jogosultsága ennek a beszélgetésnek a visszaállításához", + "You do not have permission to view the permission matrix. Admin access is required.": "Önnek nincs jogosultsága a jogosultsági mátrix megtekintéséhez. Rendszergazdai hozzáférés szükséges.", + "You have selected {count} properties to add to the schema.": "Ön {count} Tulajdonságot választott ki a Sémához való hozzáadáshoz.", + "You must be logged in to favorite views": "A nézetek kedvencként való megjelöléséhez be kell jelentkeznie", + "You need an AI agent to start a conversation.": "Beszélgetés indításához AI ügynökre van szüksége.", + "Your Fireworks AI API key. Get one at": "Az Ön Fireworks AI API kulcsa. Szerezzen be egyet itt:", + "Your OpenAI API key. Get one at": "Az Ön OpenAI API kulcsa. Szerezzen be egyet itt:", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Visszajelzését rögzítettük. Opcionálisan további részleteket adhat meg itt...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Visszajelzését rögzítettük. opcionálisan további részleteket adhat meg itt...", + "Zookeeper Hosts": "Zookeeper gazdagépek", + "Zookeeper Port": "Zookeeper port", + "Zookeeper Settings (SolrCloud)": "Zookeeper beállítások (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper kapcsolati karakterlánc a SolrCloudhoz", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper portszám (opcionális, alapértelmezett: 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F VÉGLEGES TÖRLÉSI FIGYELMEZTETÉS \\u26A0\\uFE0F\n\nÖn arra készül, hogy VÉGLEGESEN törölje a(z) \"{schema}\" Séma ÖSSZES objektumát:\n\n\\u2022 Aktív objektumok: {active}\n\\u2022 Puhán törölt objektumok: {deleted}\n\\u2022 Összesen: {total}\n\nEzek az objektumok teljesen eltávolításra kerülnek az adatbázisból, és NEM állíthatók vissza.\n\nBiztosan ezt szeretné?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F VÉGLEGES TÖRLÉSI FIGYELMEZTETÉS \\u26A0\\uFE0F\n\nÖn arra készül, hogy VÉGLEGESEN töröljön {count} puhán törölt objektumot a(z) \"{schema}\" Sémához.\n\nEzek az objektumok teljesen eltávolításra kerülnek az adatbázisból, és NEM állíthatók vissza.\n\nBiztosan ezt szeretné?", + "and accessible to all users in selected groups (or all users if no groups selected).": "és hozzáférhető a kiválasztott csoportok összes felhasználója számára (vagy minden felhasználó számára, ha nincs csoport kiválasztva).", + "and add the files there.": "és adja hozzá a fájlokat ott.", + "approved": "jóváhagyva", + "are only accessible to invited users.": "csak a meghívott felhasználók számára hozzáférhetők.", + "by": "készítette", + "chunk": "darab", + "chunks": "darabok", + "chunks processed": "darab feldolgozva", + "completed": "befejezve", + "configuration(s)": "konfiguráció", + "conversationId is required": "a conversationId kötelező", + "create": "létrehozás", + "delete": "törlés", + "deleted objects section": "törölt objektumok szakasz", + "e.g., lib/Settings/config.json": "pl. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "pl. lib/Settings/register.json", + "e.g., lib/settings/config.json": "pl. lib/settings/config.json", + "e.g., lib/settings/register.json": "pl. lib/settings/register.json", + "execute": "végrehajtás", + "failed": "sikertelen", + "file": "fájl", + "files": "fájlok", + "file{plural}": "fájl{plural}", + "folder": "mappa", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "ebben: {register}", + "in_progress": "folyamatban", + "inherited": "örökölt", + "list": "lista", + "log{plural}": "napló{plural}", + "manage": "kezelés", + "members": "tagok", + "menu or contact someone with permission to create agents.": "menüt, vagy forduljon valakihez, aki jogosult Ügynökök létrehozására.", + "message content is required": "az üzenet tartalma kötelező", + "messages": "üzenetek", + "missing": "hiányzik", + "myapp": "myapp", + "n8n URL and API key are required": "az n8n URL és az API kulcs kötelező", + "n8n configuration saved successfully": "az n8n konfiguráció sikeresen mentve", + "n8n connection not configured": "az n8n kapcsolat nincs konfigurálva", + "n8n connection successful": "az n8n kapcsolat sikeres", + "n8n connection test failed: {message}": "az n8n kapcsolat tesztje sikertelen: {message}", + "n8n connection test successful": "az n8n kapcsolat tesztje sikeres", + "n8n initialization failed: {message}": "az n8n inicializálása sikertelen: {message}", + "n8n integration disabled": "az n8n integráció letiltva", + "n8n integration enabled": "az n8n integráció engedélyezve", + "n8n project initialized successfully": "az n8n projekt sikeresen inicializálva", + "n8n settings saved successfully": "az n8n beállítások sikeresen mentve", + "notify_push configuration guide": "notify_push konfigurációs útmutató", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "A notify_push telepítve van, de az OpenRegister még nem erősített meg sikeres küldést. Aktiváláshoz indítson el egy objektummentést, vagy ellenőrizze a notify_push konfigurációját.", + "notify_push is installed but not yet active": "A notify_push telepítve van, de még nem aktív", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objektumok", + "objects processed": "objektum feldolgozva", + "object{plural}": "objektum{plural}", + "or pick a range": "vagy válasszon tartományt", + "org-...": "org-...", + "pending": "függőben", + "provides a good balance between speed and accuracy.": "jó egyensúlyt biztosít a sebesség és a pontosság között.", + "public": "nyilvános", + "read": "olvasás", + "register(s)": "Nyilvántartás", + "register(s) selected": "Nyilvántartás kiválasztva", + "register{plural}": "Nyilvántartás{plural}", + "rejected": "elutasítva", + "required": "kötelező", + "results": "eredmények", + "schema(s)": "Séma", + "schema(s) selected": "Séma kiválasztva", + "schema{plural}": "Séma{plural}", + "selected objects": "kiválasztott objektumok", + "set": "beállítás", + "sk-...": "sk-...", + "this application": "ez az alkalmazás", + "to": "eddig", + "type must be \"positive\" or \"negative\"": "a típusnak \"positive\" vagy \"negative\" értékűnek kell lennie", + "update": "frissítés", + "vectorized": "vektorizálva", + "view": "nézet", + "views selected": "nézet kiválasztva", + "waiting": "várakozás", + "{active} active, {deleted} deleted": "{active} aktív, {deleted} törölve", + "{count} audit trails cleared successfully": "{count} naplóbejegyzés sikeresen törölve", + "{count} compliance issue(s) detected": "{count} megfelelőségi probléma észlelve", + "{count} deletions": "{count} törlés", + "{count} documents found": "{count} dokumentum található", + "{count} email": "{count} e-mail", + "{count} emails": "{count} e-mail", + "{count} entries": "{count} bejegyzés", + "{count} issues": "{count} probléma", + "{count} members": "{count} tag", + "{count} searches": "{count} keresés", + "{count} selected": "{count} kiválasztva", + "{count} widget(s)": "{count} widget", + "{matched} object(s) matched. {erased} erased.": "{matched} objektum egyezett. {erased} törölve.", + "{minutes} minutes ago": "{minutes} perccel ezelőtt", + "{percentage}% of objects": "az objektumok {percentage}%-a", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} ebben: {register} / {schema}", + "{type} cache cleared successfully": "A(z) {type} gyorsítótár sikeresen törölve", + "ℹ️ Current Configuration": "ℹ️ Jelenlegi konfiguráció", + "ℹ️ current configuration": "ℹ️ jelenlegi konfiguráció", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ VESZÉLY: Mind a(z) {total} objektum VÉGLEGESEN TÖRLŐDIK az adatbázisból. Ez a művelet VISSZAÁLLÍTHATATLAN, és nem vonható vissza!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ VÉGLEGES TÖRLÉSI FIGYELMEZTETÉS ⚠️\n\nÖn arra készül, hogy VÉGLEGESEN törölje a(z) \"{schema}\" Séma ÖSSZES objektumát:\n\n• Aktív objektumok: {active}\n• Puhán törölt objektumok: {deleted}\n• Összesen: {total}\n\nEzek az objektumok teljesen eltávolításra kerülnek az adatbázisból, és NEM állíthatók vissza.\n\nBiztosan ezt szeretné?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ VÉGLEGES TÖRLÉSI FIGYELMEZTETÉS ⚠️\n\nÖn arra készül, hogy VÉGLEGESEN töröljön {count} puhán törölt objektumot a(z) \"{schema}\" Sémához.\n\nEzek az objektumok teljesen eltávolításra kerülnek az adatbázisból, és NEM állíthatók vissza.\n\nBiztosan ezt szeretné?", + "⚡ Batch Processing": "⚡ Kötegelt feldolgozás", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ A SOLR dokumentumok millióit képes kezelni egy másodpercnél rövidebb keresési válaszidővel...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Átfogó kontextus ✓ Kevésbé valószínű, hogy részletek kimaradnak ✗ Lassabb válaszok ✗ Kevésbé releváns információkat is tartalmazhat", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Gyorsabb válaszok ✓ Fókuszáltabb válaszok ✗ Releváns információk kimaradhatnak", + "✨ AI Features": "✨ AI funkciók", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Elosztott módban a SOLR a ZooKeepert használja a fürt koordinációjához...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 A facetes keresés lehetővé teszi a felhasználók számára, hogy kategóriák szerint mélyítsék el az eredményeket...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Tudta? A SOLR működteti a Netflix, az Apple és számos más nagy webhely keresését!", + "💰 View Selection (Cost Optimization)": "💰 Nézet kiválasztása (költségoptimalizálás)", + "📄 Object Serialization": "📄 Objektum-szerializálás", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 A SOLR gazdag analitikát és statisztikákat nyújt a keresési teljesítményről...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 A ConfigSetek tartalmazzák a keresési indexhez tartozó séma- és konfigurációs fájlokat...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 A replikáció biztosítja, hogy a keresési index akkor is elérhető legyen, ha a csomópontok meghibásodnak...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 A SOLR egy hatékony, vállalati szintű keresési platform, amely az Apache Lucene-re épül...", + "🔢 Vectorization Settings": "🔢 Vektorizálási beállítások", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Az automatikus skálázás a terhelés alapján dinamikusan adhat hozzá vagy távolíthat el csomópontokat...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ A biztonsági funkciók közé tartozik a hitelesítés, az engedélyezés és az SSL titkosítás...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-mail" + ], + "Purge {count} object from database": [ + "{count} objektum végleges törlése az adatbázisból", + "{count} objektum végleges törlése az adatbázisból" + ], + "Restore {count} object": [ + "{count} objektum visszaállítása", + "{count} objektum visszaállítása" + ], + "Successfully restored {count} object": [ + "{count} objektum sikeresen visszaállítva", + "{count} objektum sikeresen visszaállítva" + ], + "Delete {count} object": [ + "{count} objektum törlése", + "{count} objektum törlése" + ], + "Object successfully deleted": [ + "Az objektum sikeresen törölve", + "Az objektumok sikeresen törölve" + ] + } +} diff --git a/l10n/is.js b/l10n/is.js new file mode 100644 index 0000000000..3ac0271025 --- /dev/null +++ b/l10n/is.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(ekkert efni)", + "(no title)" : "(ekkert titill)", + "(unnamed)" : "(ónefnt)", + "(untitled card)" : "(kort án titils)", + "Activities" : "Starfsemi", + "Add Endpoint" : "Bæta við endapunkt", + "Add Source" : "Bæta við uppsprettu", + "Add card" : "Bæta við korti", + "Add contact" : "Bæta við tengilið", + "All clear" : "Allt í lagi", + "Articles" : "Greinar", + "Code" : "Kóði", + "Compliance" : "Fylgni", + "Contacts" : "Tengiliðir", + "Create Agent" : "Búa til umboðsmann", + "Create Application" : "Búa til forrit", + "Create event" : "Búa til viðburð", + "Erasure complete" : "Eyðing lokið", + "Erasure preview" : "Forskoðun eyðingar", + "Expand details" : "Þenjast út upplýsingar", + "Save changes" : "Vista breytingar", + "📄 Object Serialization" : "📄 Raðsetning hluta", + "🔢 Vectorization Settings" : "🔢 Stillingar vektórfærslu", + "💰 View Selection (Cost Optimization)" : "💰 Val yfirlits (kostnaðarbestun)", + "AI Assistant" : "AI aðstoðarmaður", + "AI Chat" : "AI spjall", + "API Key" : "API lykill", + "API tokens saved successfully" : "API tóken vistuð", + "About ConfigSets" : "Um ConfigSets", + "Action" : "Aðgerð", + "Action Distribution" : "Dreifing aðgerða", + "Action:" : "Aðgerð:", + "Actions" : "Aðgerðir", + "Active" : "Virkur", + "Active Collections" : "Virkar söfn", + "Active Filters" : "Virkir síar", + "Active Organisations" : "Virkar stofnanir", + "Active filters:" : "Virkir síar:", + "Active organisation set successfully" : "Virk stofnun stillt", + "Activity Period" : "Starfstímabil", + "Add Groups" : "Bæta við hópum", + "Add Schema" : "Bæta við skema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Bættu við titlum, lýsingum og skráningarupplýsingum í skema til að veita ríkari samhengi fyrir leit", + "Add to favorites" : "Bæta við í eftirlæti", + "Added" : "Bætt við", + "Added to favorites" : "Bætt við í eftirlæti", + "Additional Information" : "Viðbótarupplýsingar", + "Additional feedback saved. Thank you!" : "Viðbótargjöf vistuð. Takk!", + "Admin" : "Stjórnandi", + "Advanced" : "Ítarlegt", + "Advanced Filters" : "Ítarlegir síar", + "Advanced Options" : "Ítarlegar valkostir", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Ítarlegir síar eru ekki tiltækir þegar gagnagrunnsuppspretta er notuð. Skiptu yfir í Auto eða SOLR Index fyrir síavalkosti.", + "After" : "Eftir", + "Agent deleted successfully" : "Umboðsmaður eytt", + "Agents" : "Umboðsmenn", + "All" : "Allt", + "All Categories" : "Allar flokkar", + "All Collections" : "Öll söfn", + "All Confidence Levels" : "Öll öryggisstig", + "All ConfigSets" : "Öll ConfigSets", + "All Files" : "Allar skrár", + "All Levels" : "Öll stig", + "All Types" : "Allar tegundir", + "All Webhooks" : "Öll vefkrókur", + "All actions" : "Allar aðgerðir", + "All audit trails cleared successfully" : "Öll endurskoðunarferlatal hreinsað", + "All registers" : "Allar skrár", + "All schemas" : "Öll skemu", + "All searches" : "Allar leitir", + "All users" : "Allir notendur", + "An error occurred while clearing audit trails" : "Villa kom upp við hreinsun endurskoðunarferlatals", + "An error occurred while deleting the audit trail" : "Villa kom upp við eyðingu endurskoðunarferlatals", + "An error occurred while deleting the view" : "Villa kom upp við eyðingu yfirlitsins", + "An error occurred while permanently deleting the objects" : "Villa kom upp við varanlega eyðingu hlutanna", + "Analysis completed:" : "Greining lokið:", + "Analysis steps:" : "Greiningarskref:", + "Analytics" : "Greiningar", + "Analyze Objects" : "Greina hluti", + "Analyze Properties" : "Greina eiginleika", + "Analyze existing properties for improvement opportunities" : "Greina núverandi eiginleika með tilliti til endurbóta", + "Analyzing..." : "Greinir...", + "Any favorites and sharing settings for this view" : "Hvaða uppáhaldslistar og deildaaðstillingar eru fyrir þetta yfirlit", + "Any user" : "Hvaða notandi sem er", + "App store cache invalidated successfully" : "Forritsverslunarbuffer hreinsaður", + "Application deleted successfully" : "Forrit eytt", + "Applications" : "Forrit", + "Apply Changes" : "Nota breytingar", + "Archive" : "Safn", + "Archive conversation" : "Geyma samtal", + "Archived conversations are hidden from your active list" : "Geymd samtöl eru falin á virkum lista þínum", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Ertu viss um að þú viljir hreinsa gamla leitarferlatal? Þetta mun eyða færslum eldri en 30 daga.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Ertu viss um að þú viljir eyða völdum endurskoðunarferlatalum? Þessa aðgerð er ekki hægt að afturkalla.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Ertu viss um að þú viljir eyða völdum leitarferlatalum? Þessa aðgerð er ekki hægt að afturkalla.", + "Are you sure you want to delete this ConfigSet?" : "Ertu viss um að þú viljir eyða þessum ConfigSet?", + "Are you sure you want to permanently delete" : "Ertu viss um að þú viljir varanlega eyða", + "Ask a question..." : "Spyrðu spurningu...", + "Ask questions about your data using natural language" : "Spyrðu spurninga um gögnin þín með náttúrulegu máli", + "Attempt" : "Tilraun", + "Audit Trail Changes" : "Breytingar endurskoðunarferlatals", + "Audit Trail Details" : "Upplýsingar endurskoðunarferlatals", + "Audit Trail Management" : "Stjórnun endurskoðunarferlatals", + "Audit Trail Statistics" : "Tölfræði endurskoðunarferlatals", + "Audit Trails" : "Endurskoðunarferlatal", + "Audit trail data copied to clipboard" : "Gögn endurskoðunarferlatals afritaðar á klippiborð", + "Audit trail deleted successfully" : "Endurskoðunarferlatal eytt", + "Audit trail successfully deleted" : "Endurskoðunarferlatal eytt", + "Authentication" : "Auðkenning", + "Auto-commit disabled" : "Sjálfvirk staðfesting óvirkjuð", + "Auto-commit enabled" : "Sjálfvirk staðfesting virkjuð", + "Auto-create Default Organisation" : "Sjálfvirkt búa til sjálfgefna stofnun", + "Auto-retry failed vectorizations" : "Endurtilraun á misheppnaðar vektorfærslur", + "Automatically commit changes to SOLR index" : "Staðfesta breytingar sjálfvirkt í SOLR skrá", + "Automatically create a default organisation if none exists when the app is initialized" : "Búa sjálfvirkt til sjálfgefna stofnun ef engin er til þegar forritið er ræst", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Búa sjálfvirkt til vektórfellingar úr textabútum þegar skrár eru hlaðnar upp og unnar", + "Automatically generate vector embeddings when objects are created or updated" : "Búa sjálfvirkt til vektórfellingar þegar hlutir eru búnir til eða uppfærðir", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Endurtilraun sjálfvirkt á misheppnaðar vektórfærslur (hámark 3 tilraunir)", + "Available Filters" : "Tiltækir síar", + "Available Workflows" : "Tiltæk vinnuflæði", + "Avg Execution Time" : "Meðaltímaframkvæmd", + "Avg Members/Org" : "Meðalfjöldi meðlima/Stofnun", + "Avg Object Views/Session" : "Meðalfjöldi hlutasýna/Lota", + "Avg Response Time" : "Meðalsvartími", + "Avg Results/Search" : "Meðalfjöldi niðurstaðna/Leit", + "Avg Searches/Session" : "Meðalfjöldi leita/Lota", + "Back" : "Til baka", + "Back to Entities" : "Til baka í einingar", + "Back to Registers" : "Til baka í skrár", + "Back to Webhooks" : "Til baka í vefkrókar", + "Back to applications" : "Til baka í forrit", + "Back to entities" : "Til baka í einingar", + "Backend updated successfully. Please reload the application." : "Bakendi uppfærður. Vinsamlegast endurhladdu forritið.", + "Base URL (Optional)" : "Grunvefslóð (valkvætt)", + "Basic Information" : "Grunnupplýsingar", + "Batch Size" : "Lotustærð", + "Batch extraction completed" : "Lotuútdráttur lokið", + "Before" : "Áður", + "Before object vectorization can work:" : "Áður en hlutvektórfærsla getur virkað:", + "Behavior Issue" : "Hegðunarvandamál", + "Bookmarks" : "Bókamerki", + "Blob storage has been retired. All objects now use magic tables." : "Blob-geymsla hefur verið afnumin. Allir hlutir nota nú töfratöflur.", + "Branch" : "Grein", + "Bulk delete operation completed successfully" : "Magneyðing lokið", + "Bulk save operation completed successfully" : "Magnvistun lokið", + "Business Data" : "Viðskiptagögn", + "Cache cleared successfully" : "Skyndiminni hreinsað", + "Calculate Sizes" : "Reikna stærðir", + "Cancel" : "Hætta við", + "Cannot delete: objects are still attached" : "Get ekki eytt: hlutir eru enn tengdir", + "Category" : "Flokkur", + "Change Type" : "Breyta tegund", + "Changes" : "Breytingar", + "Changes copied to clipboard" : "Breytingar afritaðar á klippiborð", + "Chat Model" : "Spjallmódel", + "Chat Provider" : "Spjallveita", + "Chat Provider (RAG)" : "Spjallveita (RAG)", + "Chat Settings" : "Spjallstillingar", + "Chat provider connection successful!" : "Tenging við spjallveitu tókst!", + "Chat settings" : "Spjallstillingar", + "Choose a register" : "Veldu skrá", + "Choose a schema" : "Veldu skema", + "Choose how vector similarity calculations are performed for semantic search" : "Veldu hvernig vektórlíkindaútreikningar eru gerðir fyrir merkingarfræðilega leit", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Veldu hvaða skráartegundir á að taka með í vektórfærsluferlið. Aðeins skrár með dregnum texta og bútum verða unnar.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Veldu hvaða yfirlit á að taka með í vektórfærsluferlið. Skildu eftir tómt til að vinna öll yfirlit samkvæmt stillingum þínum.", + "Chunk" : "Bútur", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Eyðing búta er ekki enn innleidd. Notaðu bútatengda endapunkta.", + "Chunks" : "Bútar", + "Chunks to Vectorize:" : "Bútar til að vektórafræðilegar:", + "Cleanup Old Trails" : "Hreinsa gömul ferlatal", + "Cleanup completed" : "Hreinsun lokið", + "Clear All Embeddings" : "Hreinsa allar fellingar", + "Clear Entries" : "Hreinsa færslur", + "Clear Filtered Audit Trails" : "Hreinsa síuð endurskoðunarferlatal", + "Clear Filters" : "Hreinsa síar", + "Clear Index" : "Hreinsa skrá", + "Clear Selection" : "Hreinsa val", + "Clear all" : "Hreinsa allt", + "Clear all filters" : "Hreinsa alla síar", + "Clear date range" : "Hreinsa dagabilið", + "Clearing..." : "Hreinsun...", + "Close" : "Loka", + "Collection Management" : "Stjórnun safna", + "Collection Name" : "Nafn safns", + "Collection assignments updated successfully" : "Úthlutanir safns uppfærðar", + "Collection cleared successfully" : "Safn hreinsað", + "Collection copied successfully" : "Safn afritað", + "Collection created successfully" : "Safn búið til", + "Collection deleted successfully" : "Safni eytt", + "Collection used to store and index file metadata and content" : "Safn notað til að geyma og skrá lýsigögn og efni skráa", + "Collection used to store and index object data" : "Safn notað til að geyma og skrá hlutgögn", + "Collections:" : "Söfn:", + "Commit Message" : "Staðfestingarskilaboð", + "Commit Within (ms)" : "Staðfesta innan (ms)", + "Commit message" : "Staðfestingarskilaboð", + "Compare current schema with real object data" : "Bera saman núverandi skema við raunveruleg hlutgögn", + "Completed" : "Lokið", + "Complex" : "Flókið", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Flóknar fyrirspurnir: Ítarlegar leitir með mörgum síum, aðgerðum og flóknum færibreytusamsetningum", + "Confidence Level" : "Öryggisstig", + "Confidence Score" : "Öryggisskor", + "Config must be provided as an object" : "Stilling verður að vera gefin upp sem hlutur", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Stjórnun ConfigSets", + "ConfigSet Name" : "Nafn ConfigSet", + "ConfigSet created successfully" : "ConfigSet búið til", + "ConfigSet deleted successfully" : "ConfigSet eytt", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets skilgreina skema og stillingar fyrir SOLR söfn þín. Þær innihalda svæðisskilgreiningar, greinar og aðrar leitarstillingar.", + "Configuration" : "Stilling", + "Configuration Data" : "Stillingagögn", + "Configuration saved" : "Stilling vistuð", + "Configurations" : "Stillingar", + "Configure Facets" : "Stilla hliðar", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Stilla stórar tungumálalíkansveitu (LLM) fyrir AI-knúnar eiginleikar þar á meðal merkingarfræðileg leit, fellingar og spjall.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Stilla grunntengingarstillingar fyrir SOLR þjón þinn þar á meðal auðkenningu og netmöguleika. Notaðu sérstaka ConfigSet og Safnstjórnunargluggana til að stjórna kjörnum og söfnum.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Stilla hvernig gagnagrunnshlutar eru umbreyttir í vektórfellingar fyrir merkingarfræðilega leit. Hlutir eru vektóraðir beint án þess að þurfa textaútdrátt.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Stilla hvernig hlutir eru umbreyttir í texta fyrir vektórfærslu. Þessar stillingar hafa áhrif á leitargæði og samhengi.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Stilla færibreytur fyrir skráavektórfærslu. Þetta ferli mun búa til vektórfellingar fyrir alla dregna skráarbúta.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Stilla færibreytur fyrir hlutvektórfærslu. Þetta ferli mun búa til vektórfellingar fyrir alla hluti sem passa við yfirlitssíarnar þínar.", + "Configure which types of data to search and how many sources to retrieve" : "Stilla hvaða gagnategundir á að leita í og hve margar uppsprettur á að sækja", + "Connection Failed" : "Tenging mistókst", + "Connection Settings" : "Tengingarstillingar", + "Connection Successful!" : "Tenging tókst!", + "Connection failed" : "Tenging mistókst", + "Connection protocol" : "Tengingarskyldur", + "Connection successful" : "Tenging tókst", + "Connection timeout in seconds" : "Tengingarúrræðastími í sekúndum", + "Constant delay between retries (5 minutes)" : "Stöðug seinkun milli endurtilrauna (5 mínútur)", + "Constraint Issue" : "Takmörkunarvandamál", + "Content Filters" : "Efnissíar", + "Continue to Properties" : "Halda áfram í eiginleika", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Stjórna hvaða hlutayfirlit á að vektóra til að draga úr API kostnaði. Vektóra aðeins yfirlit sem hagnast á merkingarfræðilegri leit.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Stjórna hvaða yfirlit og verkfæri AI getur notað í þessu samtali. Sjálfgefið eru allar umboðsgæðar virkjaðar.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Stjórnar tilviljunarkenni (0 = ákveðið, 2 = mjög skapandi)", + "Conversation ID is required" : "Samtalsauðkenni er nauðsynlegt", + "Conversation archived" : "Samtal geymt", + "Conversation archived successfully" : "Samtal geymt", + "Conversation cleared successfully" : "Samtal hreinsað", + "Conversation deleted" : "Samtali eytt", + "Conversation permanently deleted" : "Samtali varanlega eytt", + "Conversation renamed" : "Samtal endurnefnt", + "Conversation restored" : "Samtal endurheimt", + "Conversation title" : "Titill samtals", + "Copied!" : "Afritað!", + "Copy" : "Afrita", + "Copy Changes" : "Afrita breytingar", + "Copy Collection" : "Afrita safn", + "Copy Data" : "Afrita gögn", + "Copy Full Data" : "Afrita öll gögn", + "Copying..." : "Afritar...", + "Count" : "Fjöldi", + "Counting objects..." : "Tel hluti...", + "Costs" : "Kostnaður", + "Create" : "Búa til", + "Create Collection" : "Búa til safn", + "Create ConfigSet" : "Búa til ConfigSet", + "Create New Collection" : "Búa til nýtt safn", + "Create New ConfigSet" : "Búa til nýtt ConfigSet", + "Create Operations" : "Búningaaðgerðir", + "Create Webhook" : "Búa til vefkrók", + "Create a copy of collection:" : "Búa til afrit af safni:", + "Create a new ConfigSet based on the _default template" : "Búa til nýtt ConfigSet byggt á _default sniðmáti", + "Create a new SOLR collection from an existing ConfigSet" : "Búa til nýtt SOLR safn úr núverandi ConfigSet", + "Create your first AI agent to get started." : "Búðu til fyrsta AI umboðsmann þinn til að byrja.", + "Created" : "Búið til", + "Created:" : "Búið til:", + "Creating..." : "Bý til...", + "Current Filters" : "Núverandi síar", + "Current Type:" : "Núverandi tegund:", + "Custom API endpoint if using a different region" : "Sérsniðin API endapunktur ef önnur svæði eru notuð", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Sérsniðnar HTTP hausar (ein á línu, snið: Hausheiti: gildi)", + "Daily" : "Daglega", + "Dashboard" : "Mælaborð", + "Data Source" : "Gagnaveita", + "Data sources" : "Gagnaveitu", + "Data type does not match observed values" : "Gagnategund passar ekki við fylgstu gildi", + "Database URL" : "Gagnagrunnsvefslóð", + "Database information refreshed" : "Gagnagrunnupplýsingar uppfærðar", + "Date Range" : "Dagsetningarbil", + "Default" : "Sjálfgefið", + "Default Organisation" : "Sjálfgefin stofnun", + "Default: 5" : "Sjálfgefið: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Seinkun tvöfaldast með hverri tilraun (2, 4, 8 mínútur...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Seinkun eykst línulega (5, 10, 15 mínútur...)", + "Delete" : "Eyða", + "Delete Application" : "Eyða forriti", + "Delete Audit Trail" : "Eyða endurskoðunarferlatal", + "Delete Collection" : "Eyða safni", + "Delete ConfigSet" : "Eyða ConfigSet", + "Delete Objects" : "Eyða hlutum", + "Delete Operations" : "Eyðingaaðgerðir", + "Delete View" : "Eyða yfirliti", + "Delete all objects in this schema" : "Eyða öllum hlutum í þessu skema", + "Delete permanently" : "Eyða varanlega", + "Delete this conversation?" : "Eyða þessu samtali?", + "Delete view" : "Eyða yfirliti", + "Deleted" : "Eytt", + "Deleted By" : "Eytt af", + "Deleted Date" : "Dagsetning eyðingar", + "Deleted Items Management" : "Stjórnun eyddra hluta", + "Deleted This Week" : "Eytt þessa viku", + "Deleted Today" : "Eytt í dag", + "Deleted:" : "Eytt:", + "Deleting..." : "Eyði...", + "Deletion Date Range" : "Dagsetningarbil eyðingar", + "Deletion Statistics" : "Tölfræði eyðingar", + "Deprecated" : "Úrelt", + "Description" : "Lýsing", + "Description:" : "Lýsing:", + "Detect data types and patterns" : "Greina gagnategundir og mynstur", + "Detected At" : "Greint við", + "Detected Format:" : "Greint snið:", + "Detected Issues:" : "Greind vandamál:", + "Disable" : "Gera óvirkt", + "Disabled" : "Óvirkt", + "Discover Files" : "Finna skrár", + "Discovered Properties" : "Fundnir eiginleikar", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Viltu varanlega eyða öllum síuðum endurskoðunarferslum? Þessa aðgerð er ekki hægt að afturkalla.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Viltu varanlega eyða þessari endurskoðunarferslu? Þessa aðgerð er ekki hægt að afturkalla.", + "Documents" : "Skjöl", + "Download API Spec" : "Sækja API forskrift", + "Edit" : "Breyta", + "Edit Register" : "Breyta skrá", + "Edit Schema" : "Breyta skema", + "Edit View" : "Breyta yfirliti", + "Edit Webhook" : "Breyta vefkrók", + "Edit view" : "Breyta yfirliti", + "Edit view details" : "Breyta upplýsingum yfirlits", + "Effectiveness" : "Skilvirkni", + "Email" : "Tölvupóstur", + "Emails" : "Tölvupóstar", + "Embedding Model" : "Fellingarlíkan", + "Embedding Provider" : "Fellingarveita", + "Embedding generated successfully" : "Felling búin til", + "Embedding provider connection successful!" : "Tenging við fellingarveitu tókst!", + "Enable" : "Virkja", + "Enable auto-creation" : "Virkja sjálfvirka búningu", + "Enable automatic file vectorization" : "Virkja sjálfvirka skráavektórfærslu", + "Enable automatic object vectorization" : "Virkja sjálfvirka hlutvektórfærslu", + "Enable automatic synchronization" : "Virkja sjálfvirka samstillingu", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Virkja ítarlega skráningu fyrir SOLR aðgerðir (mælt með við villuleit)", + "Enable faceting" : "Virkja hliðarflokkun", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Virkja eða gera óvirkt LLM eiginleika. Stilla veitur og líkön með LLM stillingarhnappinum að ofan.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Virkja eða gera óvirkt SOLR leitarsamþættingu. Stilla tengingarstillingar með Tengingarstillingar hnappinum að ofan.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Virkja eða gera óvirkt n8n vinnuflæðisamþættingu. Stilla tengingarstillingar hér að neðan.", + "Enable or disable this webhook" : "Virkja eða gera óvirkt þennan vefkrók", + "Enable vectorization for all existing and future views (may increase costs)" : "Virkja vektórfærslu fyrir öll núverandi og framtíðaryfirlit (getur aukið kostnað)", + "Enabled" : "Virkt", + "Endpoints" : "Endapunktar", + "Enter ConfigSet name" : "Sláðu inn nafn ConfigSet", + "Enter collection name" : "Sláðu inn nafn safns", + "Enter description (optional)..." : "Sláðu inn lýsingu (valkvætt)...", + "Enter new collection name" : "Sláðu inn nýtt nafn safns", + "Enter object ID" : "Sláðu inn hlutauðkenni", + "Enter search term" : "Sláðu inn leitarhugtök", + "Enter view name..." : "Sláðu inn nafn yfirlits...", + "Enter webhook name" : "Sláðu inn nafn vefkróks", + "Entities" : "Einingar", + "Entity Information" : "Upplýsingar um einingu", + "Entity deleted successfully" : "Einingu eytt", + "Entity not found" : "Eining ekki fundin", + "Entries to be deleted:" : "Færslur til að eyða:", + "Enum Issue" : "Upptalningarvandamál", + "Enum constraint is missing" : "Upptalningartakmörkun vantar", + "Error" : "Villa", + "Error Details" : "Villuupplýsingar", + "Error Information" : "Villuupplýsingar", + "Error Message" : "Villuskilaboð", + "Error loading application" : "Villa við hleðslu forrits", + "Error loading audit trails" : "Villa við hleðslu endurskoðunarferlatal", + "Error loading entity" : "Villa við hleðslu einingar", + "Error loading search trails" : "Villa við hleðslu leitarferlatal", + "Estimated Batches:" : "Áætlaðar lotur:", + "Estimated Cost:" : "Áætlaður kostnaður:", + "Estimated Duration:" : "Áætluð lengd:", + "Event" : "Viðburður", + "Event Property for Payload" : "Viðburðareiginleiki fyrir farminn", + "Events" : "Viðburðir", + "Example Value" : "Dæmigildi", + "Exclusive Maximum" : "Einhlýt hámark", + "Exclusive Minimum" : "Einhlýtt lágmark", + "Execution Mode" : "Framkvæmdaháttur", + "Execution Time" : "Framkvæmdatími", + "Execution Time Range" : "Bil framkvæmdatíma", + "Existing Improvements" : "Núverandi endurbætur", + "Exponential" : "Veldisvísir", + "Export" : "Flutningur út", + "Export completed successfully" : "Útflutningur lokið", + "Export, view, or delete audit trails" : "Flytja út, skoða eða eyða endurskoðunarferlatalum", + "Extend Schema" : "Stækka skema", + "Extended" : "Stækkað", + "Extensions" : "Viðbætur", + "Extract Pending Files" : "Draga út bíðandi skrár", + "Extract properties from each object" : "Draga eiginleika úr hverjum hlut", + "Extracted At" : "Dregið út við", + "Extraction Status" : "Staða útdrátturs", + "Facet configuration updated successfully" : "Hliðarstilling uppfærð", + "Facets discovered and configured successfully" : "Hliðar fundin og stillt", + "Facets discovered successfully" : "Hliðar fundin", + "Failed" : "Misheppnaðst", + "Failed to analyze schema properties" : "Mistókst að greina skemaeiginleika", + "Failed to archive conversation" : "Mistókst að geyma samtal", + "Failed to calculate sizes" : "Mistókst að reikna stærðir", + "Failed to clear collection" : "Mistókst að hreinsa safn", + "Failed to copy changes" : "Mistókst að afrita breytingar", + "Failed to copy data" : "Mistókst að afrita gögn", + "Failed to copy data to clipboard" : "Mistókst að afrita gögn á klippiborð", + "Failed to create conversation" : "Mistókst að búa til samtal", + "Failed to create or find project" : "Mistókst að búa til eða finna verkefni", + "Failed to delete collection" : "Mistókst að eyða safni", + "Failed to delete conversation" : "Mistókst að eyða samtali", + "Failed to delete webhook" : "Mistókst að eyða vefkrók", + "Failed to download API specification" : "Mistókst að sækja API forskrift", + "Failed to get SOLR field configuration" : "Mistókst að sækja SOLR svæðissttillingu", + "Failed to get entity categories" : "Mistókst að sækja einingaflokka", + "Failed to get entity statistics" : "Mistókst að sækja einingstölfræði", + "Failed to get entity types" : "Mistókst að sækja einingategundir", + "Failed to load LLM configuration" : "Mistókst að hlaða LLM stillingu", + "Failed to load Nextcloud groups" : "Mistókst að hlaða Nextcloud hópa", + "Failed to load conversation" : "Mistókst að hlaða samtali", + "Failed to load entities" : "Mistókst að hlaða einingum", + "Failed to load entity" : "Mistókst að hlaða einingu", + "Failed to load files" : "Mistókst að hlaða skrám", + "Failed to load organisations" : "Mistókst að hlaða stofnunum", + "Failed to load templates" : "Mistókst að hlaða sniðmátum", + "Failed to load webhooks" : "Mistókst að hlaða vefkrókum", + "Failed to load workflows" : "Mistókst að hlaða vinnuflæðum", + "Failed to refresh database information" : "Mistókst að uppfæra gagnagrunnupplýsingar", + "Failed to reindex collection" : "Mistókst að endurskrá safn", + "Failed to rename conversation" : "Mistókst að endurnefna samtal", + "Failed to restore conversation" : "Mistókst að endurheimta samtal", + "Failed to retry extraction" : "Mistókst að endurtaka útdrátt", + "Failed to retry webhook" : "Mistókst að endurtaka vefkrók", + "Failed to save GitHub token" : "Mistókst að vista GitHub tóken", + "Failed to save GitLab URL" : "Mistókst að vista GitLab vefslóð", + "Failed to save GitLab token" : "Mistókst að vista GitLab tóken", + "Failed to save additional feedback" : "Mistókst að vista viðbótargjöf", + "Failed to save n8n configuration" : "Mistókst að vista n8n stillingu", + "Failed to save roles" : "Mistókst að vista hlutverk", + "Failed to save settings" : "Mistókst að vista stillingar", + "Failed to save webhook" : "Mistókst að vista vefkrók", + "Failed to send feedback" : "Mistókst að senda gjöf", + "Failed to test webhook" : "Mistókst að prófa vefkrók", + "Failed to update favorite status" : "Mistókst að uppfæra eftirlætisástand", + "Failed to update schema properties" : "Mistókst að uppfæra skemaeiginleika", + "Failed to update view" : "Mistókst að uppfæra yfirlit", + "Failed to update webhook" : "Mistókst að uppfæra vefkrók", + "Feedback recorded" : "Gjöf skráð", + "Fewer sources (1-3):" : "Færri heimildir (1-3):", + "Field" : "Svæði", + "File" : "Skrá", + "File Chunk Prediction" : "Spá um skráarbút", + "File Collection" : "Skráasafn", + "File Management" : "Skráastjórnun", + "File Name" : "Nafn skráar", + "File Path" : "Slóð skráar", + "File Type Selection" : "Val skráartegundar", + "File Vectorization" : "Skráavektórfærsla", + "File Warmup" : "Hlýnun skráa", + "File actions menu" : "Aðgerðavalmynd skráar", + "File anonymized successfully" : "Skrá nafnlæg gerð", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Skráarbúta vektórfærsla er ekki enn innleidd. Bútarnir eru tilbúnir og geymdir, en vektórfærsluþjónustan er í þróun.", + "File collection not configured" : "Skráasafn ekki stillt", + "File discovery completed" : "Uppgötvun skráa lokið", + "File extraction completed" : "Útdráttur skráa lokið", + "File extraction queued" : "Útdráttur skráa í biðröð", + "File indexed successfully" : "Skrá skráð", + "File is already anonymized" : "Skrá er þegar nafnlæg", + "File not found" : "Skrá ekki fundin", + "File settings updated successfully" : "Skráastillingar uppfærðar", + "File sources" : "Skráaruppsprettur", + "File vectorization configuration saved successfully" : "Stilling skráavektórfærslu vistuð", + "File vectorization started. Check the statistics section for progress." : "Skráavektórfærsla hafin. Athugaðu tölfræðihlutann fyrir framvindu.", + "File warmup completed" : "Hlýnun skráa lokið", + "Files" : "Skrár", + "Files with Completed Extraction:" : "Skrár með lokinn útdrátt:", + "Files → fileCollection, Objects → objectCollection" : "Skrár → fileCollection, Hlutir → objectCollection", + "Filter Audit Trails" : "Sía endurskoðunarferlatal", + "Filter Deleted Items" : "Sía eydda hluti", + "Filter Objects" : "Sía hluti", + "Filter Properties" : "Sía eiginleika", + "Filter Search Trails" : "Sía leitarferlatal", + "Filter Statistics" : "Síutölfræði", + "Filter and analyze search trail entries" : "Sía og greina leitarferlatalfærslur", + "Filter and manage audit trail entries" : "Sía og stjórna endurskoðunarferlatalfærslum", + "Filter and manage soft deleted items" : "Sía og stjórna mjúkt eyddum hlutum", + "Filter by object ID" : "Sía eftir hlutauðkenni", + "Filter by search term" : "Sía eftir leitarhugtaki", + "Filter by webhook" : "Sía eftir vefkrók", + "Filter data loaded automatically. Use the filters below to refine your search." : "Síugögn hlaðin sjálfvirkt. Notaðu síarnar hér að neðan til að þrengja leitina þína.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Sía vefkrók kveikjur eftir farmeiginleikum (ein á línu, snið: lykill: gildi)", + "Filtered" : "Síað", + "Filters" : "Síar", + "Fireworks AI Chat Settings" : "Fireworks AI spjallstillingar", + "Fireworks AI Embedding Configuration" : "Fireworks AI fellingarstilling", + "First" : "Fyrst", + "Fixed" : "Lagað", + "For chat and retrieval-augmented generation" : "Fyrir spjall og endurheimtuaukna myndun", + "For vector embeddings and semantic search" : "Fyrir vektórfellingar og merkingarfræðilega leit", + "Format" : "Snið", + "Format Issue" : "Sniðvandamál", + "Format constraint is missing" : "Sniðtakmörkun vantar", + "From Date" : "Frá dagsetning", + "From date" : "Frá dagsetning", + "Full data copied to clipboard" : "Öll gögn afrituð á klippiborð", + "General Issue" : "Almennt vandamál", + "Generate recommendations and confidence scores" : "Búa til tillögur og öryggisskor", + "Generate vectors immediately when new objects are created" : "Búa til vektórar strax þegar nýir hlutir eru búnir til", + "GitHub token is valid" : "GitHub tóken er gilt", + "GitHub token removed successfully" : "GitHub tóken fjarlægt", + "GitHub token saved successfully" : "GitHub tóken vistað", + "GitLab URL saved successfully" : "GitLab vefslóð vistuð", + "GitLab token is valid" : "GitLab tóken er gilt", + "GitLab token saved successfully" : "GitLab tóken vistað", + "HTTP Method" : "HTTP aðferð", + "HTTP method used to send webhook requests" : "HTTP aðferð notuð til að senda vefkrókbeiðnir", + "Headers" : "Hausar", + "Health" : "Heilsa", + "Heartbeat successful - connection kept alive" : "Hjartsláttur tókst - tenging viðhaldið", + "Helpful" : "Gagnlegt", + "Hide Filters" : "Fela síar", + "Hide in forms" : "Fela í eyðublöðum", + "Hide in list view" : "Fela í listasýn", + "High" : "Hátt", + "High Confidence" : "Hátt öryggi", + "Host" : "Hýsill", + "Hourly" : "Á hverri klukkustund", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Hversu djúpt á að fara í hreiðraðar hlutaeiginleikar (1-20). Hærri gildi fanga meira en auka vektórstærð.", + "How often to check for updates (1-168 hours)" : "Hversu oft á að athuga uppfærslur (1-168 klukkustundir)", + "How to handle retries for failed webhook deliveries" : "Hvernig á að meðhöndla endurtilraunir á misheppnaðar vefkróklevranir", + "ID" : "Auðkenni", + "ID:" : "Auðkenni:", + "IP rate limits cleared successfully" : "IP hraðatakmarkanir hreinsaðar", + "Identify properties not in the schema" : "Greina eiginleika sem eru ekki í skemanu", + "Immutable" : "Óbreytanlegt", + "Import" : "Flutningur inn", + "Import successful" : "Innflutningur tókst", + "Improved Property" : "Endurbættur eiginleiki", + "In use" : "Í notkun", + "Inactive" : "Óvirkur", + "Include IDs and names of related objects for better contextual search" : "Taka með auðkenni og nöfn tengdra hluta fyrir betra samhengisleit", + "Include related object references" : "Taka með tilvísanir tengdra hluta", + "Include schema and register metadata" : "Taka með skema og skráarlýsigögn", + "Initialization failed" : "Frumstilling misheppnaðist", + "Initialize Project" : "Frumstilla verkefni", + "Inspect Fields" : "Skoða svæði", + "Inspect Index" : "Skoða skrá", + "Invalid" : "Ógilt", + "Invalid batch size. Must be between 1 and 5000" : "Ógild lotustærð. Verður að vera á milli 1 og 5000", + "Invalid field name provided" : "Ógilt svæðisnafn gefið upp", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Ógilt maxObjects. Verður að vera 0 (allt) eða jákvætt tala", + "Invalid:" : "Ógilt:", + "Issue" : "Vandamál", + "Items per page:" : "Hlutir á síðu:", + "Just now" : "Rétt í þessu", + "LLM Configuration" : "LLM stilling", + "LLM actions menu" : "LLM aðgerðavalmynd", + "LLM configuration saved successfully" : "LLM stilling vistuð", + "LLM features disabled" : "LLM eiginleikar óvirkjaðir", + "LLM features enabled" : "LLM eiginleikar virkjaðir", + "LLM must be enabled with an embedding provider configured" : "LLM verður að vera virkjað með fellingarveitu stillt", + "LLM settings updated successfully" : "LLM stillingar uppfærðar", + "Last" : "Síðast", + "Last Triggered" : "Síðast kveikt", + "Length Range:" : "Lengdarbil:", + "Linear" : "Línulegt", + "Load Advanced Filters" : "Hlaða ítarlegum síum", + "Load advanced filters with live data from your search index" : "Hlaða ítarlegum síum með lifandi gögnum úr leitarskránni þinni", + "Load view" : "Hlaða yfirliti", + "Loading ConfigSets..." : "Hleð ConfigSets...", + "Loading advanced filters..." : "Hleð ítarlegum síum...", + "Loading agents..." : "Hleð umboðsmenn...", + "Loading application details..." : "Hleð forritsupplýsingar...", + "Loading applications..." : "Hleð forrit...", + "Loading archived conversations..." : "Hleð geymd samtöl...", + "Loading audit trails..." : "Hleð endurskoðunarferlatal...", + "Loading collections..." : "Hleð söfn...", + "Loading configurations..." : "Hleð stillingar...", + "Loading conversation..." : "Hleð samtal...", + "Loading conversations..." : "Hleð samtöl...", + "Loading deleted items..." : "Hleð eydda hluti...", + "Loading events..." : "Hleð viðburði...", + "Loading filter data..." : "Hleð síugögn...", + "Loading groups..." : "Hleð hópa...", + "Loading log details..." : "Hleð skráningarupplýsingar...", + "Loading registers..." : "Hleð skrár...", + "Loading schemas..." : "Hleð skemu...", + "Loading search trails..." : "Hleð leitarferlatal...", + "Loading sources..." : "Hleð uppsprettur...", + "Loading statistics..." : "Hleð tölfræði...", + "Loading users..." : "Hleð notendur...", + "Loading views..." : "Hleð yfirlit...", + "Loading..." : "Hleð...", + "Local" : "Staðbundið", + "Local Version" : "Staðbundin útgáfa", + "Locked" : "Læst", + "Locked:" : "Læst:", + "Login successful" : "Innskráning tókst", + "Logs" : "Skráningar", + "Low" : "Lágt", + "Low Confidence" : "Lágt öryggi", + "Lucene Version:" : "Lucene útgáfa:", + "Magic table synchronized successfully" : "Töfratafla samstillt", + "Manage Organisation Roles" : "Stjórna hlutverkum stofnunar", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Stjórna SOLR söfnum (gagnageymslum) og úthluta þeim fyrir hluti og skrár.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Stjórna SOLR ConfigSets (stillingarsniðmátum) fyrir söfn þín.", + "Manage and configure agents for automated tasks" : "Stjórna og stilla umboðsmenn fyrir sjálfvirk verkefni", + "Manage and monitor file text extraction status" : "Stjórna og fylgjast með stöðu textaútdráttar skráa", + "Manage and restore soft deleted items from your registers" : "Stjórna og endurheimta mjúkt eydda hluti úr skránum þínum", + "Manage and view detected entities from files and objects" : "Stjórna og skoða greindar einingar úr skrám og hlutum", + "Manage document templates and themes" : "Stjórna skjalasniðmátum og þemum", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Stjórna n8n vinnuflæðum fyrir OpenRegister sjálfvirkni. Vinnuflæðin verða geymd í stillta verkefninu.", + "Manage webhooks for event-driven integrations" : "Stjórna vefkrókum fyrir viðburðadrifnar samþættingar", + "Manage your applications and modules" : "Stjórna forritunum þínum og einingum", + "Manage your data registers and their configurations" : "Stjórna gagnaskránum þínum og stillingum þeirra", + "Manage your data schemas and their properties" : "Stjórna gagnaskemum þínum og eiginleikum þeirra", + "Manage your data sources and their configurations" : "Stjórna gagnauppsprettum þínum og stillingum þeirra", + "Manage your saved search configurations" : "Stjórna vistaðum leitarstillingum þínum", + "Manage your system configurations and settings" : "Stjórna kerfissstillingum þínum og stillingum", + "Managed" : "Stjórnað", + "Max Files (0 = all)" : "Hámark skrár (0 = allar)", + "Max Length" : "Hámarkslengd", + "Max Length:" : "Hámarkslengd:", + "Max Objects (0 = all)" : "Hámark hlutir (0 = allir)", + "Max Retries" : "Hámark endurtilraunir", + "Max Shards/Node" : "Hámark brot/hnútur", + "Max execution time (ms)" : "Hámarks framkvæmdatími (ms)", + "Max ms" : "Hámarks ms", + "Max result count" : "Hámarks niðurstöðufjöldi", + "Max results" : "Hámarks niðurstöður", + "Maximum" : "Hámark", + "Maximum Nesting Depth" : "Hámarks hreiðrunardýpt", + "Maximum length constraint is missing" : "Hámarkslengdartakmörkun vantar", + "Maximum length is too restrictive" : "Hámarkslengd er of þröng", + "Maximum number of files to process. Set to 0 to process all files." : "Hámarksfjöldi skráa til að vinna. Stilltu á 0 til að vinna allar skrár.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Hámarksfjöldi hluta til að vinna. Stilltu á 0 til að vinna alla hluti.", + "Maximum number of retry attempts for failed deliveries" : "Hámarksfjöldi endurtilrauna fyrir misheppnaðar levranir", + "Maximum time to wait before committing changes" : "Hámarkstími til að bíða áður en breytingar eru staðfestar", + "Maximum tokens to generate" : "Hámarks tóken til að búa til", + "Maximum value constraint is missing" : "Hámarksgildistökmörkun vantar", + "Maximum value is too restrictive" : "Hámarksgildi er of þröngt", + "Medium" : "Miðlungs", + "Medium Confidence" : "Miðlungs öryggi", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Meðallegar fyrirspurnir: Leitir með nokkurri síun eða mörgum færibreytum (t.d. dagsetningarbil, sérstakar skrár/skemu)", + "Members of selected groups can access this view" : "Meðlimir valinna hópa geta fengið aðgang að þessu yfirliti", + "Members:" : "Meðlimir:", + "Memory prediction calculated successfully" : "Minnisspá reiknuð", + "Message content is required" : "Innihald skilaboða er nauðsynlegt", + "Message does not belong to this conversation" : "Skilaboð tilheyra ekki þessu samtali", + "Metadata Filters" : "Lýsigagnasíar", + "Method" : "Aðferð", + "Min Length" : "Lágmarkslengd", + "Min execution time (ms)" : "Lágmarks framkvæmdatími (ms)", + "Min ms" : "Lágmarks ms", + "Min result count" : "Lágmarks niðurstöðufjöldi", + "Min results" : "Lágmarks niðurstöður", + "Minimum" : "Lágmark", + "Minimum value constraint is missing" : "Lágmarksgildistökmörkun vantar", + "Minimum value is too restrictive" : "Lágmarksgildi er of þröngt", + "Missing object fields created successfully" : "Vantandi hlutasvæði búin til", + "Mode:" : "Háttur:", + "Model" : "Líkan", + "Model Name" : "Nafn líkans", + "Modified" : "Breytt", + "Monthly" : "Mánaðarlega", + "More sources (10-20):" : "Fleiri heimildir (10-20):", + "Most Active Objects" : "Virkustu hlutir", + "Multiple Of" : "Margfeldi af", + "Name" : "Nafn", + "Names cache warmup completed" : "Hlýnun nafnaskyndiminnis lokið", + "New Collection Name" : "Nýtt nafn safns", + "New Conversation" : "Nýtt samtal", + "New Properties" : "Nýir eiginleikar", + "New Property" : "Nýr eiginleiki", + "New Value" : "Nýtt gildi", + "New users without specific organisation membership will be automatically added to this organisation" : "Nýir notendur án sérstaks stofnanameðlimsskaps verða sjálfvirkt bætt við þessa stofnun", + "Next" : "Næst", + "No" : "Nei", + "No ConfigSets found" : "Engin ConfigSets fundin", + "No GitHub token configured" : "Ekkert GitHub tóken stillt", + "No GitHub token provided" : "Ekkert GitHub tóken gefið upp", + "No GitLab token provided" : "Ekkert GitLab tóken gefið upp", + "No Organisation" : "Engin stofnun", + "No active filters" : "Engir virkir síar", + "No activity data available" : "Engin starfsemigögn tiltæk", + "No agents available" : "Engir umboðsmenn tiltækir", + "No agents found" : "Engir umboðsmenn fundnir", + "No app store cache exists yet" : "Engin forritsverslunarskyndiminni til enn", + "No applications are available." : "Engin forrit eru tiltæk.", + "No applications found" : "Engin forrit fundin", + "No archived conversations" : "Engin geymd samtöl", + "No audit trail entries found" : "Engar endurskoðunarferslur fundnar", + "No changes recorded" : "Engar breytingar skráðar", + "No chunks to vectorize" : "Engir bútar til að vektóra", + "No collections found" : "Engin söfn fundin", + "No configuration data" : "Engin stillinggögn", + "No configurations are available." : "Engar stillingar eru tiltækar.", + "No configurations found" : "Engar stillingar fundnar", + "No configurations found for this application." : "Engar stillingar fundnar fyrir þetta forrit.", + "No conversations yet. Create a new one to get started!" : "Engin samtöl enn. Búðu til nýtt til að byrja!", + "No data available" : "Engin gögn tiltæk", + "No data available for chart" : "Engin gögn tiltæk fyrir graf", + "No deleted items found" : "Engir eyddir hlutir fundnir", + "No deletion data available" : "Engin eyðingargögn tiltæk", + "No description found" : "Engin lýsing fundin", + "No description provided" : "Engin lýsing gefin upp", + "No entities detected in this file. Run text extraction first." : "Engar einingar greindar í þessari skrá. Keyrðu textaútdrátt fyrst.", + "No entities found" : "Engar einingar fundnar", + "No entities have been detected yet" : "Engar einingar hafa verið greindar enn", + "No events found" : "Engir viðburðir fundnir", + "No expired audit trails found to clear" : "Engin útrunnin endurskoðunarferlatal fundin til að hreinsa", + "No facetable fields available. Select a register and schema to see available filters." : "Engin hliðarhæf svæði tiltæk. Veldu skrá og skema til að sjá tiltækar síar.", + "No files found" : "Engar skrár fundnar", + "No files have been extracted yet" : "Engar skrár hafa verið dregnar út enn", + "No files to process" : "Engar skrár til að vinna", + "No files to reindex" : "Engar skrár til að endurskrá", + "No filters are currently active. This will delete ALL audit trail entries!" : "Engir síar eru virkir núna. Þetta mun eyða ÖLLUM endurskoðunarferslum!", + "No log entries found" : "Engar skráningarfærslur fundnar", + "No logs are available for this configuration." : "Engar skráningar tiltækar fyrir þessa stillingu.", + "No logs are available for this source." : "Engar skráningar tiltækar fyrir þessa uppsprettu.", + "No logs found" : "Engar skráningar fundnar", + "No mismatched fields found - SOLR schema is properly configured" : "Engin ósamkvæm svæði fundin - SOLR skema er rétt stillt", + "No objects found" : "Engir hlutir fundnir", + "No objects found to vectorize" : "Engir hlutir fundnir til að vektóra", + "No objects selected for deletion" : "Engir hlutir valdir til eyðingar", + "No objects to delete" : "Engir hlutir til að eyða", + "No objects were permanently deleted" : "Engir hlutir voru varanlega eyddir", + "No properties found" : "Engir eiginleikar fundnir", + "No properties match your filters." : "Engir eiginleikar passa við síarnar þínar.", + "No purge date set" : "Engin hreinsunardag stillt", + "No register data available" : "Engin skrárgögn tiltæk", + "No registers found" : "Engar skrár fundnar", + "No registers found for this application." : "Engar skrár fundnar fyrir þetta forrit.", + "No relations found" : "Engar tengsl fundin", + "No request body available" : "Engin beiðnihluti tiltæk", + "No response body available" : "Engin svarhluti tiltæk", + "No saved views yet. Create one in the Search tab!" : "Engin vistuð yfirlit enn. Búðu til eitt í Leitarflipanum!", + "No schema data available" : "Engin skemugögn tiltæk", + "No schema selected for exploration" : "Ekkert skema valið til skoðunar", + "No schemas found" : "Engin skemu fundin", + "No schemas found for this application." : "Engin skemu fundin fyrir þetta forrit.", + "No search terms data available" : "Engin leitarhugtakagögn tiltæk", + "No search trail entries found" : "Engar leitarferslur fundnar", + "No sources are available." : "Engar uppsprettur eru tiltækar.", + "No sources found" : "Engar uppsprettur fundnar", + "No templates found" : "Engin sniðmát fundin", + "No templates have been created yet" : "Engin sniðmát hafa verið búin til enn", + "No valid magic-mapped register+schema combinations found" : "Engar gildar töfravarpaðar skrá+skemu samsetningar fundnar", + "No views found. Create views first before configuring vectorization." : "Engin yfirlit fundin. Búðu til yfirlit fyrst áður en vektórfærsla er stillt.", + "No views match your search" : "Engin yfirlit passa við leitina þína", + "No webhooks found" : "Engir vefkrókar fundnir", + "No webhooks have been configured yet" : "Engir vefkrókar hafa verið stilltir enn", + "No workflows found in this project. Create workflows in the n8n editor." : "Engin vinnuflæði fundin í þessu verkefni. Búðu til vinnuflæðin í n8n ritstjóranum.", + "None" : "Ekkert", + "Not helpful" : "Ekki gagnlegt", + "Not in use" : "Ekki í notkun", + "Number Constraints" : "Tölutakmörkunar", + "Number of chunks to vectorize in one API call (1-100)." : "Fjöldi búta til að vektóra í einu API kalli (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Fjöldi búta til að vektóra í einu API kalli. Hærra = hraðara en meira minni. Mælt: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Fjöldi hluta til að vinna í hverri lotu (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Fjöldi hluta til að vektóra í einu API kalli. Hærra = hraðara en meira minni. Mælt: 10-50.", + "Numeric Range:" : "Tölulegt bil:", + "Object" : "Hlutur", + "Object Collection" : "Hlutasafn", + "Object Count Prediction" : "Spá um hlutafjölda", + "Object ID" : "Hlutauðkenni", + "Object Management" : "Hlutastjórnun", + "Object Vectorization" : "Hlutvektórfærsla", + "Object Warmup" : "Hlýnun hluta", + "Object collection not configured" : "Hlutasafn ekki stillt", + "Object not found" : "Hlutur ekki fundinn", + "Object permanently deleted" : "Hlut varanlega eytt", + "Object restored successfully" : "Hlutur endurheimt", + "Object settings updated successfully" : "Hlutastillingar uppfærðar", + "Object sources" : "Hlutaruppsprettur", + "Object unlocked successfully" : "Hlut opnað", + "Object vectorization configuration saved successfully" : "Hlutvektórfærslustilling vistuð", + "Object vectorized successfully" : "Hlutur vektóraður", + "Object:" : "Hlutur:", + "Objects" : "Hlutir", + "Objects Analyzed" : "Hlutir greindir", + "Objects Distribution" : "Dreifing hluta", + "Objects being analyzed" : "Hlutir eru greindir", + "Objects by Register" : "Hlutir eftir skrá", + "Objects by Schema" : "Hlutir eftir skema", + "Objects deletion completed successfully" : "Eyðing hluta lokið", + "Objects in schema" : "Hlutir í skema", + "Objects to Process:" : "Hlutir til að vinna:", + "Objects to be analyzed" : "Hlutir til að greina", + "Objects to be deleted" : "Hlutir til að eyða", + "Objects to be validated" : "Hlutir til að staðfesta", + "Objects will be serialized as JSON text before vectorization" : "Hlutir verða raðaðar sem JSON texti fyrir vektórfærslu", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Hlutir verða mjúkt eyddir (merktir sem eyddir en geymdir í gagnagrunni). Hægt er að endurheimta þá síðar ef þörf krefur.", + "Old Value" : "Gamalt gildi", + "Oldest Item (days)" : "Elsti hlutur (dagar)", + "Ollama Chat Settings" : "Ollama spjallstillingar", + "Ollama Embedding Configuration" : "Ollama fellingarstilling", + "Ollama URL" : "Ollama vefslóð", + "Only With Changes" : "Aðeins með breytingar", + "Open Register Objects" : "Opna skrárhluti", + "Open n8n Editor" : "Opna n8n ritstjóra", + "OpenAI Chat Settings" : "OpenAI spjallstillingar", + "OpenAI Embedding Configuration" : "OpenAI fellingarstilling", + "OpenRegister Settings" : "OpenRegister stillingar", + "Optional webhook secret for signature verification" : "Valkvætt vefkrókleynd fyrir undirskriftarstaðfestingu", + "Order" : "Röð", + "Organisation" : "Stofnun", + "Organisation ID" : "Stofnunarauðkenni", + "Organisation Members" : "Meðlimir stofnunar", + "Organisation Statistics" : "Tölfræði stofnunar", + "Organisation created successfully" : "Stofnun búin til", + "Organisation settings saved successfully" : "Stillingar stofnunar vistaðar", + "Organisations" : "Stofnanir", + "Organization" : "Stofnun", + "Organization ID (Optional)" : "Stofnunarauðkenni (valkvætt)", + "Orphaned Items" : "Foreldralausir hlutir", + "Owner" : "Eigandi", + "Owner:" : "Eigandi:", + "Parallel Mode (Faster, more resource intensive)" : "Samhliða hamur (Hraðar, meira auðlindafrekt)", + "Parallel:" : "Samhliða:", + "Password" : "Lykilorð", + "Password for SOLR authentication (optional)" : "Lykilorð fyrir SOLR auðkenningu (valkvætt)", + "Path" : "Slóð", + "Path in repository" : "Slóð í geymslu", + "Path where the configuration file will be saved in the repository" : "Slóð þar sem stillingarskrá verður vistuð í geymslunni", + "Path where the register OAS file will be saved in the repository" : "Slóð þar sem skrár OAS skrá verður vistuð í geymslunni", + "Pattern (regex)" : "Mynstur (regex)", + "Pattern Issue" : "Mynsturnvandamál", + "Pattern constraint is missing" : "Mynsturtakmörkun vantar", + "Patterns:" : "Mynstur:", + "Pending" : "Í bið", + "Permanent Delete Mode" : "Varanlegur eyðingarhamur", + "Permanently Delete" : "Eyða varanlega", + "Permanently delete already soft-deleted objects" : "Eyða varanlega þegar mjúkt eyddum hlutum", + "Permanently delete this conversation?" : "Eyða þessu samtali varanlega?", + "Person" : "Einstaklingur", + "Personal Data" : "Persónuleg gögn", + "Phone" : "Sími", + "Photos" : "Myndir", + "Please create an agent in the" : "Vinsamlegast búðu til umboðsmann í", + "Please select an agent to continue" : "Vinsamlegast veldu umboðsmann til að halda áfram", + "Please select which register and schema to use for the new object" : "Vinsamlegast veldu hvaða skrá og skema á að nota fyrir nýja hlutinn", + "Please try again later." : "Vinsamlegast reyndu aftur síðar.", + "Please wait while we fetch your agents." : "Vinsamlegast bíddu meðan við sækjum umboðsmenn þína.", + "Please wait while we fetch your applications." : "Vinsamlegast bíddu meðan við sækjum forritin þín.", + "Please wait while we fetch your configurations." : "Vinsamlegast bíddu meðan við sækjum stillingar þínar.", + "Please wait while we fetch your deleted items." : "Vinsamlegast bíddu meðan við sækjum eyddar hlutir þínar.", + "Please wait while we fetch your sources." : "Vinsamlegast bíddu meðan við sækjum uppsprettur þínar.", + "Polls" : "Könnungar", + "Popular Search Terms" : "Vinsæl leitarhugtök", + "Port" : "Gátt", + "Prerequisites" : "Forskilyrði", + "Press Enter to send, Shift+Enter for new line" : "Ýttu á Enter til að senda, Shift+Enter fyrir nýja línu", + "Previous" : "Fyrri", + "Private" : "Einkamál", + "Processes chunks in batches with simulated parallelism." : "Vinnur búta í lotum með hermdri samhliðavinnslu.", + "Processes file chunks sequentially (safest)." : "Vinnur skráarbúta í röð (öruggast).", + "Processes objects in chunks with simulated parallelism." : "Vinnur hluti í búsum með hermdri samhliðavinnslu.", + "Processes objects sequentially (safest)." : "Vinnur hluti í röð (öruggast).", + "Processing" : "Vinnsla", + "Processing Limits" : "Vinnslutak", + "Processing..." : "Vinnur...", + "Project initialized successfully" : "Verkefni frumstillt", + "Project not found. Please initialize first." : "Verkefni ekki fundið. Vinsamlegast frumstilltu fyrst.", + "Properties" : "Eiginleikar", + "Property Behaviors" : "Eiginleikahegðun", + "Property Configuration:" : "Eiginleikastilling:", + "Property Filters" : "Eiginleikasíar", + "Property Title" : "Titill eiginleika", + "Property Type" : "Tegund eiginleika", + "Property can be improved" : "Eiginleika er hægt að bæta", + "Protected" : "Varið", + "Provider" : "Veita", + "Provider is required for testing" : "Veita er nauðsynleg til prófunar", + "Public" : "Almenningur", + "Public View" : "Opinbert yfirlit", + "Public views can be accessed by anyone in the system" : "Opinber yfirlit er hægt að fá aðgang að af hverjum sem er í kerfinu", + "Publish" : "Gefa út", + "Publish Objects" : "Gefa út hluti", + "Publish all objects in this schema" : "Gefa út alla hluti í þessu skema", + "Published" : "Birt", + "Published:" : "Birt:", + "Publishing..." : "Gefur út...", + "Purge" : "Hreinsa", + "Purge Date" : "Hreinsunardagur", + "Query Complexity Distribution" : "Dreifing flækni fyrirspurnar", + "Query parameter is required" : "Fyrirspurnafæribreyta er nauðsynleg", + "RAG" : "RAG", + "RAG Configuration" : "RAG stilling", + "Rate limits cleared successfully" : "Hraðatakmarkanir hreinsaðar", + "Raw Changes Data" : "Hrá breytingargögn", + "Re-vectorize on object update" : "Endur-vektóra við hlutauppfærslu", + "Read" : "Lesa", + "Recommendations:" : "Tillögur:", + "Recommended Type:" : "Mælt með tegund:", + "Recommended: 5 sources" : "Mælt með: 5 heimildir", + "Refresh" : "Uppfæra", + "Refresh Data" : "Uppfæra gögn", + "Refresh Stats" : "Uppfæra tölfræði", + "Refresh Workflows" : "Uppfæra vinnuflæði", + "Refresh dashboard" : "Uppfæra mælaborð", + "Refresh database info" : "Uppfæra gagnagrunnupplýsingar", + "Register" : "Skrá", + "Register ID" : "Skrárauðkenni", + "Register Statistics" : "Tölfræði skráar", + "Register Totals" : "Heildarfjöldi skráar", + "Register depublished successfully" : "Skrá afbirt", + "Register not found" : "Skrá ekki fundin", + "Register objects deletion completed successfully" : "Eyðing skrárhluta lokið", + "Register published successfully" : "Skrá birt", + "Register/Schema Usage" : "Notkun skráar/skema", + "Registers" : "Skrár", + "Reindex" : "Endurskrá", + "Reindex completed" : "Endurskráning lokið", + "Reindex completed successfully" : "Endurskráning lokið", + "Reindex failed" : "Endurskráning misheppnaðist", + "Relations" : "Tengsl", + "Remote Version" : "Fjarútgáfa", + "Remove" : "Fjarlægja", + "Remove filter" : "Fjarlægja sía", + "Remove from favorites" : "Fjarlægja úr eftirlæti", + "Remove group" : "Fjarlægja hóp", + "Removed" : "Fjarlægt", + "Removed from favorites" : "Fjarlægt úr eftirlæti", + "Rename Conversation" : "Endurnefna samtal", + "Rename conversation" : "Endurnefna samtal", + "Replicas" : "Eintök", + "Repository" : "Geymsla", + "Request Body" : "Beiðnihluti", + "Request Data" : "Beiðnigögn", + "Request Details" : "Upplýsingar um beiðni", + "Request body must be JSON with a \"query\" field" : "Beiðnihluti verður að vera JSON með \"query\" svæði", + "Request timeout in seconds" : "Beiðniúrræðastími í sekúndum", + "Required field" : "Nauðsynlegt svæði", + "Required status is inconsistent" : "Nauðsynlegur staðs er ósamkvæmur", + "Rerun Search" : "Endurtaka leit", + "Reset Changes" : "Endurstilla breytingar", + "Reset Filters" : "Endurstilla síar", + "Response Body" : "Svarhluti", + "Response Details" : "Upplýsingar um svar", + "Response Time:" : "Svartími:", + "Restore" : "Endurheimta", + "Restore conversation" : "Endurheimta samtal", + "Restore or permanently delete items" : "Endurheimta eða eyða hlutum varanlega", + "Result Count Range" : "Bil niðurstöðufjölda", + "Results" : "Niðurstöður", + "Retrieve all objects for this schema" : "Sækja alla hluti fyrir þetta skema", + "Retry" : "Endurtaka", + "Retry Failed Extractions" : "Endurtaka misheppnaðar útdráttur", + "Retry Policy" : "Endurtilraunaregla", + "Retry completed" : "Endurtilraun lokið", + "Retrying..." : "Endurtaki...", + "Risk Level" : "Áhættuflokk", + "Roles updated successfully" : "Hlutverk uppfærð", + "SOLR Connection Settings" : "SOLR tengingarstillingar", + "SOLR Version:" : "SOLR útgáfa:", + "SOLR actions menu" : "SOLR aðgerðavalmynd", + "SOLR base path (usually /solr)" : "SOLR grunnskrá (venjulega /solr)", + "SOLR is disabled" : "SOLR er óvirkt", + "SOLR is not available or not configured" : "SOLR er ekki tiltækt eða ekki stillt", + "SOLR logging disabled" : "SOLR skráning óvirkjuð", + "SOLR logging enabled" : "SOLR skráning virkjuð", + "SOLR search disabled" : "SOLR leit óvirkjuð", + "SOLR search enabled" : "SOLR leit virkjuð", + "SOLR server hostname or IP address" : "SOLR þjónaheiti eða IP-tala", + "SOLR server port number (optional, defaults to 8983)" : "SOLR þjónagáttarnúmer (valkvætt, sjálfgefið 8983)", + "SOLR setup completed successfully" : "SOLR uppsetning lokið", + "SOLR setup failed" : "SOLR uppsetning misheppnaðist", + "SOLR setup failed - check logs" : "SOLR uppsetning misheppnaðist - athugaðu skráningar", + "Sample Values:" : "Dæmigildi:", + "Save" : "Vista", + "Save Configuration" : "Vista stillingu", + "Save Connection Settings" : "Vista tengingarstillingar", + "Save Roles" : "Vista hlutverk", + "Save Settings" : "Vista stillingar", + "Save current search as view" : "Vista núverandi leit sem yfirlit", + "Saved Views" : "Vistuð yfirlit", + "Saving..." : "Vista...", + "Schema" : "Skema", + "Schema ID" : "Skemaauðkenni", + "Schema Name" : "Nafn skema", + "Schema Statistics" : "Tölfræði skema", + "Schema depublished successfully" : "Skema afbirt", + "Schema objects deletion completed successfully" : "Eyðing skemahluta lokið", + "Schema published successfully" : "Skema birt", + "Schema successfully created" : "Skema búið til", + "Schema successfully updated" : "Skema uppfært", + "Schemas" : "Skemu", + "Scheme" : "Kerfi", + "Search / Views" : "Leita / Yfirlit", + "Search Activity" : "Leitarstarfsemi", + "Search Analytics" : "Leitargreiningar", + "Search Entities" : "Leita að einingum", + "Search Files" : "Leita að skrám", + "Search GitHub" : "Leita á GitHub", + "Search GitLab" : "Leita á GitLab", + "Search Method" : "Leitaraðferð", + "Search Term" : "Leitarhugtök", + "Search Trail Details" : "Upplýsingar leitarferlatals", + "Search Trail Management" : "Stjórnun leitarferlatals", + "Search Trail Statistics" : "Tölfræði leitarferlatals", + "Search Trails" : "Leitarferlatal", + "Search Views" : "Leita yfirlit", + "Search Webhooks" : "Leita vefkrókar", + "Search by file name or path" : "Leita eftir nafni skráar eða slóð", + "Search by name or URL" : "Leita eftir nafni eða vefslóð", + "Search by value" : "Leita eftir gildi", + "Search in Files" : "Leita í skrám", + "Search in Objects" : "Leita í hlutum", + "Search property names..." : "Leita eiginleikaheiti...", + "Search trail deletion not implemented yet" : "Eyðing leitarferlatals er ekki enn innleidd", + "Search views..." : "Leita yfirlit...", + "Secret" : "Leynilykill", + "Secret key for HMAC signature generation (optional)" : "Leynigerðarlykill fyrir HMAC undirskriftarmyndun (valkvætt)", + "Select AI Agent" : "Veldu AI umboðsmann", + "Select All" : "Velja allt", + "Select ConfigSet" : "Veldu ConfigSet", + "Select File Types to Vectorize:" : "Veldu skráartegundir til að vektóra:", + "Select HTTP method" : "Veldu HTTP aðferð", + "Select Register and Schema" : "Veldu skrá og skema", + "Select Views to Vectorize:" : "Veldu yfirlit til að vektóra:", + "Select a Nextcloud group to add" : "Veldu Nextcloud hóp til að bæta við", + "Select a branch" : "Veldu grein", + "Select a model or type a custom model name" : "Veldu líkan eða sláðu inn sérsniðið líkansheiti", + "Select a repository" : "Veldu geymslu", + "Select a repository you have write access to" : "Veldu geymslu sem þú hefur skrivaðgang að", + "Select an AI Agent" : "Veldu AI umboðsmann", + "Select an AI agent to begin chatting with your data." : "Veldu AI umboðsmann til að byrja að spjalla um gögnin þín.", + "Select backend" : "Veldu bakenda", + "Select chat model" : "Veldu spjalllíkan", + "Select collection for files" : "Veldu safn fyrir skrár", + "Select collection for objects" : "Veldu safn fyrir hluti", + "Select default organisation" : "Veldu sjálfgefna stofnun", + "Select event to listen to..." : "Veldu viðburð til að hlusta á...", + "Select model" : "Veldu líkan", + "Select options..." : "Veldu valkosti...", + "Select period" : "Veldu tímabil", + "Select property to send as payload" : "Veldu eiginleika til að senda sem farm", + "Select provider" : "Veldu veitu", + "Select registers and schemas to save a view" : "Veldu skrár og skemu til að vista yfirlit", + "Select retry policy" : "Veldu endurtilraunareglu", + "Select the branch to publish to" : "Veldu grein til að birta í", + "Select the event this webhook should listen to" : "Veldu viðburð sem þessi vefkrókur á að hlusta á", + "Select views to vectorize:" : "Veldu yfirlit til að vektóra:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Veldu hvaða Nextcloud hópar eru tiltækir fyrir þessa stofnun. Notendur í þessum hópum munu hafa aðgang að auðlindum stofnunar.", + "Select which data views the AI can search" : "Veldu hvaða gagnasýn AI getur leitað í", + "Select which property from the event should be used as the webhook payload data" : "Veldu hvaða eiginleika úr viðburðinum á að nota sem farmgögn vefkróksins", + "Select which tools the AI can use to perform actions" : "Veldu hvaða verkfæri AI getur notað til að framkvæma aðgerðir", + "Selected" : "Valið", + "Selected Groups" : "Valdir hópar", + "Selected audit trails deleted successfully" : "Valdar endurskoðunarferslur eyddar", + "Selected search trails deleted successfully" : "Valdar leitarferslur eyddar", + "Selected users can access this view" : "Valdir notendur geta fengið aðgang að þessu yfirliti", + "Send additional feedback" : "Senda viðbótargjöf", + "Send as CloudEvent" : "Senda sem CloudEvent", + "Sensitive PII" : "Viðkvæmt PII", + "Serial Mode (Safer, slower)" : "Raðhamur (Öruggara, hægar)", + "Serial:" : "Raðhamur:", + "Server Configuration" : "Þjónastilling", + "Server Information" : "Þjónarupplýsingar", + "Settings" : "Stillingar", + "Shards" : "Brot", + "Share" : "Deila", + "Share with Groups" : "Deila með hópum", + "Share with Users" : "Deila með notendum", + "Show Filters" : "Sýna síar", + "Show less" : "Sýna minna", + "Show only entries with changes" : "Sýna aðeins færslur með breytingar", + "Simple" : "Einfalt", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Einfaldar fyrirspurnir: Grunnleitir með fáum færibreytum (t.d. eitt leitarhugtök, engar ítarlegar síar)", + "Size" : "Stærð", + "Slug" : "Auðkenni", + "Soft Delete Mode" : "Mjúkur eyðingarhamur", + "Soft Deleted Items" : "Mjúkt eyddir hlutir", + "SolrCloud mode enabled" : "SolrCloud hamur virkjað", + "Source" : "Uppspretta", + "Sources" : "Uppsprettur", + "Sources:" : "Uppsprettur:", + "Standalone SOLR mode" : "Sjálfstæður SOLR hamur", + "Start Conversation" : "Hefja samtal", + "Start Vectorization" : "Hefja vektórfærslu", + "Start a conversation" : "Hefja samtal", + "Starting names cache warmup..." : "Hefja hlýnun nafnaskyndiminnis...", + "Starting..." : "Hef...", + "Statistics" : "Tölfræði", + "Stats calculation not yet implemented" : "Tölfræðiútreikningur er ekki enn innleiddur", + "Status" : "Staða", + "Status Code" : "Stöðukóði", + "String Constraints" : "Strengjatakmörkunar", + "Success" : "Árangur", + "Success Rate" : "Hlutfall árangurs", + "Success Status" : "Staða árangurs", + "Successful" : "Tókst", + "Successfully joined organisation" : "Gengist í stofnun", + "Sync Table" : "Samstilla töflu", + "Synchronization Settings" : "Samstillingarstillingar", + "System Default" : "Kerfisjálfgefið", + "Technical Description" : "Tæknileg lýsing", + "Technical description for developers and administrators" : "Tæknileg lýsing fyrir þróunaraðila og stjórnendur", + "Temperature" : "Hitastig", + "Templates" : "Sniðmát", + "Test" : "Prófa", + "Test Chat" : "Prófa spjall", + "Test Connection" : "Prófa tengingu", + "Test Embedding" : "Prófa fellingu", + "Test endpoint executed successfully" : "Endapunkts prófun tókst", + "Test webhook delivered successfully" : "Vefkróksprófun afhent", + "Test webhook delivery failed" : "Afhending vefkróksprófunar misheppnaðist", + "Test webhook sent successfully" : "Vefkróksprófun send", + "Testing..." : "Prófa...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Textabútar eru búnir til við útdrátt skráa og geymdir í gagnagrunninum. Vektórfærsla les þessa forskornar skrár og umbreytir þeim í fellingar.", + "Text extracted successfully" : "Texti dreginn út", + "Text extraction disabled" : "Textaútdráttur óvirkjað", + "The URL where webhook events will be sent" : "Vefslóðin þar sem vefkrókur viðburðir verða sendir", + "The requested conversation does not exist" : "Umbeðið samtal er ekki til", + "The saved view and all its search configuration" : "Vistað yfirlit og allar leitarstillingar þess", + "There are no audit trail entries matching your current filters." : "Engar endurskoðunarferslur passa við núverandi síar.", + "There are no deleted items matching your current filters." : "Engir eyddir hlutir passa við núverandi síar.", + "There are no search trail entries matching your current filters." : "Engar leitarferslur passa við núverandi síar.", + "There are no webhook log entries matching your filters." : "Engar vefkrókurskráningarfærslur passa við síarnar þínar.", + "This action cannot be undone." : "Þessa aðgerð er ekki hægt að afturkalla.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Þessa aðgerð er ekki hægt að afturkalla. Gakktu úr skugga um að engin söfn noti þennan ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Þessi umboðsmaður hefur engin stillanleg yfirlit eða verkfæri, en þú getur ennþá stillt RAG stillingar hér að neðan.", + "This analysis may take some time" : "Þessi greining gæti tekið nokkurn tíma", + "This audit trail entry does not contain any change information." : "Þessi endurskoðunarfersla inniheldur engar breytingarupplýsingar.", + "This configuration has no data defined." : "Þessi stilling hefur engin gögn skilgreind.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Þessi endapunktur er úreltur. Notaðu bútatengda endapunkta í staðinn.", + "This entity has no relations to objects or files" : "Þessi eining hefur engin tengsl við hluti eða skrár", + "This is a test webhook from OpenRegister" : "Þetta er prófunarvefkrókur frá OpenRegister", + "This schema must use Magic Table configuration to sync" : "Þetta skema verður að nota Töfratöflustillingu til að samstilla", + "This source has no associated registers." : "Þessi uppspretta hefur engar tengdar skrár.", + "This will copy the _default ConfigSet with the new name" : "Þetta mun afrita _default ConfigSet með nýja nafninu", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Þetta mun varanlega eyða ÖLLUM fellingum (vektórum) úr gagnagrunninum. Þú munt þurfa að vektóra alla hluti og skrár aftur. Þessa aðgerð er ekki hægt að afturkalla.\\n\\nErtu viss um að þú viljir halda áfram?", + "This will permanently delete:" : "Þetta mun varanlega eyða:", + "Timeout (seconds)" : "Úrræðastími (sekúndur)", + "Timestamp" : "Tímastimpill", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Ráðlegging: Virkjaðu aðeins yfirlit sem þurfa merkingarfræðilega leit til að lágmarka fellingarkostnað. Einfaldar uppflettitöflur þurfa sjaldan vektórfærslu.", + "Title" : "Titill", + "To Date" : "Til dagsetning", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Til að stilla bútstærð eða stefnu, farðu í Skráastilling → Vinnslutak.", + "To change the embedding provider or model, go to LLM Configuration." : "Til að breyta fellingarveitu eða líkani, farðu í LLM Stilling.", + "To date" : "Til dagsetning", + "Toggle search sidebar" : "Víxla leitarhliðarborða", + "Tools" : "Verkfæri", + "Top Deleters" : "Helstu eyðarar", + "Total" : "Heildartala", + "Total Audit Trails" : "Heildartala endurskoðunarferlatals", + "Total Chunks Available:" : "Heildartala tiltækra búta:", + "Total Deleted Items" : "Heildartala eyddra hluta", + "Total Members" : "Heildartala meðlima", + "Total Objects" : "Heildartala hluta", + "Total Objects in Database:" : "Heildartala hluta í gagnagrunni:", + "Total Organisations" : "Heildartala stofnana", + "Total Results" : "Heildartala niðurstaðna", + "Total Searches" : "Heildartala leita", + "Total Size" : "Heildartala stærð", + "Total Storage" : "Heildartala geymsla", + "Total:" : "Heildartala:", + "Totals" : "Heildar", + "Type" : "Tegund", + "Type Issue" : "Tegundarvandamál", + "Type Variations:" : "Tegundarafbrigði:", + "Type must be \"positive\" or \"negative\"" : "Tegund verður að vera \"positive\" eða \"negative\"", + "Type to search for groups" : "Sláðu inn til að leita að hópum", + "Type to search for users" : "Sláðu inn til að leita að notendum", + "Type to search groups..." : "Sláðu inn til að leita að hópum...", + "Type to search users..." : "Sláðu inn til að leita að notendum...", + "URL" : "URL", + "URL where Ollama is running" : "URL þar sem Ollama keyrir", + "UUID:" : "UUID:", + "Unchanged" : "Óbreytt", + "Unique Search Terms" : "Einstök leitarhugtök", + "Unique Terms" : "Einstök hugtök", + "Unknown" : "Óþekkt", + "Unknown Register" : "Óþekkt skrá", + "Unknown Schema" : "Óþekkt skema", + "Untitled View" : "Yfirlit án titils", + "Update" : "Uppfæra", + "Update Operations" : "Uppfærslaaðgerðir", + "Update configuration: ..." : "Uppfæra stillingu: ...", + "Update register OAS: ..." : "Uppfæra skrár OAS: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Uppfæra vektóra þegar hlutgögn breytast (mælt með fyrir nákvæma leit)", + "Updated" : "Uppfært", + "Updated At" : "Uppfært við", + "Updated:" : "Uppfært:", + "Uptime:" : "Upptíma:", + "Usage Count" : "Notkunarfjöldi", + "Use SolrCloud with Zookeeper for distributed search" : "Nota SolrCloud með Zookeeper fyrir dreifða leit", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Notaðu síar til að þrengja endurskoðunarferslur eftir skrá, skema, aðgerðartegund, notanda, dagsetningarbil eða hlutauðkenni.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Notaðu síar til að þrengja eydda hluti eftir skrá, skema, eyðingardagsetning eða notandann sem eyðdi þeim.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Notaðu síar til að þrengja leitarferslur eftir skrá, skema, árangursstöðu, notanda, dagsetningarbil, leitarhugtök eða frammistöðumælikvarða.", + "Used By Collections" : "Notað af söfnum", + "User" : "Notandi", + "User Agent Statistics" : "Tölfræði notandaumboðsmanns", + "User Description" : "Notandalýsing", + "User rate limits cleared successfully" : "Notandahraðatakmarkanir hreinsaðar", + "User removed successfully" : "Notanda fjarlægt", + "User-friendly description shown in forms and help text" : "Notandavæn lýsing sýnd í eyðublöðum og hjálpartexta", + "Username" : "Notandanafn", + "Username for SOLR authentication (optional)" : "Notandanafn fyrir SOLR auðkenningu (valkvætt)", + "Users" : "Notendur", + "Using Pre-Generated Chunks" : "Nota forskornar bútar", + "Validate" : "Staðfesta", + "Validate Objects" : "Staðfesta hluti", + "Validation completed successfully" : "Staðfesting lokið", + "Validation failed" : "Staðfesting misheppnaðist", + "Value" : "Gildi", + "Vector Dimensions" : "Vektórvíddir", + "Vector Search Backend" : "Vektórleitarbakendi", + "Vector field: _embedding_" : "Vektórsvæði: _embedding_", + "Vectorization Triggers" : "Kveikjur vektórfærslu", + "Vectorize All Files" : "Vektóra allar skrár", + "Vectorize All Objects" : "Vektóra alla hluti", + "Vectorize all file types" : "Vektóra allar skráartegundir", + "Vectorize all views" : "Vektóra öll yfirlit", + "Vectorize on object creation" : "Vektóra við hlutabúningu", + "Vectors will be stored in your existing object and file collections" : "Vektórar verða geymdir í núverandi hluta- og skráasöfnum þínum", + "Version" : "Útgáfa", + "Very High" : "Mjög hátt", + "View API Docs" : "Skoða API skjöl", + "View Changes" : "Skoða breytingar", + "View Details" : "Skoða upplýsingar", + "View Error" : "Yfirlitvilla", + "View File" : "Skoða skrá", + "View Full Details" : "Skoða allar upplýsingar", + "View Logs" : "Skoða skráningar", + "View Name" : "Nafn yfirlits", + "View Object" : "Skoða hlut", + "View Parameters" : "Yfirlitsfæribreytur", + "View Selection" : "Val yfirlits", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Skoða og greina leitarferlatasskráningar með ítarlegum síum og greinunargæðum", + "View and analyze system audit trails with advanced filtering capabilities" : "Skoða og greina kerfisendurskoðunarferlatal með ítarlegum síum", + "View deleted successfully" : "Yfirliti eytt", + "View entity details and manage relations" : "Skoða upplýsingar einingar og stjórna tengslum", + "View name" : "Nafn yfirlits", + "View name is required" : "Nafn yfirlits er nauðsynlegt", + "View saved successfully!" : "Yfirlit vistað!", + "View search analytics and manage search logs" : "Skoða leitargreiningar og stjórna leitarskráningum", + "View successfully deleted" : "Yfirliti eytt", + "View successfully updated" : "Yfirlit uppfært", + "View updated successfully!" : "Yfirlit uppfært!", + "View webhook delivery logs and filter by webhook" : "Skoða afhendingarskráningar vefkróks og sía eftir vefkrók", + "Views" : "Yfirlit", + "Visible to users" : "Sýnilegt notendum", + "Wait for Response" : "Bíða eftir svari", + "Wait for webhook response before continuing (required for request/response flows)" : "Bíðu eftir svari vefkróks áður en haldið er áfram (nauðsynlegt fyrir beiðni/svar flæði)", + "Warmup Names Cache" : "Hlýna nafnaskyndiminni", + "Warning" : "Viðvörun", + "Warning:" : "Viðvörun:", + "Webhook" : "Vefkrókur", + "Webhook Log Details" : "Upplýsingar vefkrókarskráningar", + "Webhook Logs" : "Vefkrókarskráningar", + "Webhook created successfully" : "Vefkrókur búinn til", + "Webhook deleted" : "Vefkrókur eytt", + "Webhook retry delivered successfully" : "Endurtilraun vefkróks afhent", + "Webhook retry delivery failed" : "Afhending endurtilraunar vefkróks misheppnaðist", + "Webhook updated" : "Vefkrókur uppfærður", + "Webhook updated successfully" : "Vefkrókur uppfærður", + "Webhooks" : "Vefkrókar", + "Weekly" : "Vikulega", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Pakka farmur vefkróks í CloudEvents snið fyrir betri samvirkni", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: gildi\\nAuthorization: Bearer tóken", + "Yes" : "Já", + "You" : "Þú", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Þú getur búið til nýtt ConfigSets byggt á _default sniðmáti, eða hlaðið upp sérsniðnum beint á SOLR þjóninn þinn.", + "You do not have access to this conversation" : "Þú hefur ekki aðgang að þessu samtali", + "You do not have permission to delete this conversation" : "Þú hefur ekki leyfi til að eyða þessu samtali", + "You do not have permission to modify this conversation" : "Þú hefur ekki leyfi til að breyta þessu samtali", + "You do not have permission to restore this conversation" : "Þú hefur ekki leyfi til að endurheimta þetta samtal", + "You must be logged in to favorite views" : "Þú verður að vera innskráður til að geta valið eftirlætisyfirlit", + "You need an AI agent to start a conversation." : "Þú þarft AI umboðsmann til að hefja samtal.", + "Your Fireworks AI API key. Get one at" : "Fireworks AI API lykillinn þinn. Fáðu einn á", + "Your OpenAI API key. Get one at" : "OpenAI API lykillinn þinn. Fáðu einn á", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Gjöf þín hefur verið skráð. Valkvætt er að gefa viðbótarupplýsingar hér...", + "Zookeeper Hosts" : "Zookeeper hýslar", + "Zookeeper Port" : "Zookeeper gátt", + "Zookeeper Settings (SolrCloud)" : "Zookeeper stillingar (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper tengingastrengur fyrir SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper gáttarnúmer (valkvætt, sjálfgefið 2181)", + "chunk" : "bútur", + "chunks" : "bútar", + "chunks processed" : "bútar unnir", + "configuration(s)" : "stilling(ar)", + "e.g., lib/Settings/config.json" : "t.d., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "t.d., lib/Settings/register.json", + "failed" : "misheppnuðust", + "file" : "skrá", + "files" : "skrár", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "meðlimir", + "menu or contact someone with permission to create agents." : "valmyndinni eða hafðu samband við einhvern með leyfi til að búa til umboðsmenn.", + "messages" : "skilaboð", + "n8n URL and API key are required" : "n8n URL og API lykill eru nauðsynleg", + "n8n configuration saved successfully" : "n8n stilling vistuð", + "n8n connection not configured" : "n8n tenging ekki stillt", + "n8n connection successful" : "n8n tenging tókst", + "n8n connection test successful" : "n8n tengingaprófun tókst", + "n8n integration disabled" : "n8n samþætting óvirkjuð", + "n8n integration enabled" : "n8n samþætting virkjuð", + "n8n project initialized successfully" : "n8n verkefni frumstillt", + "n8n settings saved successfully" : "n8n stillingar vistaðar", + "objectType: object\\naction: created" : "objectType: hlutur\\naction: búinn til", + "objects" : "hlutir", + "objects processed" : "hlutir unnir", + "or pick a range" : "eða veldu bil", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "veitir góða jafnvægi milli hraða og nákvæmni.", + "register(s)" : "skrá(r)", + "register(s) selected" : "skrá(r) valin", + "required" : "nauðsynlegt", + "results" : "niðurstöður", + "schema(s)" : "skema(r)", + "schema(s) selected" : "skema(r) valin", + "sk-..." : "sk-...", + "this application" : "þetta forrit", + "to" : "til", + "vectorized" : "vektóraður", + "views selected" : "yfirlit valin", + "ℹ️ Current Configuration" : "ℹ️ Núverandi stilling", + "⚡ Batch Processing" : "⚡ Lotuúrvinnsla", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Yfirgripsmikið samhengi ✓ Minna líklegt til að missa af upplýsingum ✗ Hægari svör ✗ Gæti innihaldið minna viðeigandi upplýsingar", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Hraðari svör ✓ Meira einbeittar svör ✗ Gæti misst af viðeigandi upplýsingum", + "✨ AI Features" : "✨ AI eiginleikar", + "All search trails cleared successfully" : "Öll leitarferlatal hreinsað", + "Multiple search trail deletion not implemented yet" : "Margar leitarferlataleyðingar eru ekki enn innleiddar", + "No expired search trails found to clear" : "Engin útrunnin leitarferlatal fundin til að hreinsa", + "Missing message" : "Skilaboð vantar", + "message content is required" : "innihald skilaboða er nauðsynlegt", + "Missing conversation or agentUuid" : "Samtal eða agentUuid vantar", + "Access denied" : "Aðgangur hafnaður", + "Conversation not found" : "Samtal ekki fundið", + "AI service not configured" : "AI þjónusta ekki stillt", + "Failed to process message" : "Mistókst að vinna skilaboð", + "Missing conversationId" : "conversationId vantar", + "conversationId is required" : "conversationId er nauðsynlegt", + "Failed to fetch conversation history" : "Mistókst að sækja sögu samtals", + "Failed to clear conversation" : "Mistókst að hreinsa samtal", + "Invalid feedback type" : "Ógild gjafarstegund", + "type must be \"positive\" or \"negative\"" : "tegund verður að vera \"positive\" eða \"negative\"", + "Message not found" : "Skilaboð ekki fundin", + "Failed to save feedback" : "Mistókst að vista gjöf", + "Failed to get chat statistics" : "Mistókst að sækja spjallstat", + "Not Found" : "Ekki fundið", + "Engine not found" : "Vél ekki fundin", + "Consumer not found" : "Neytandi ekki fundinn", + "Backend parameter is required" : "Bakenda-færibreyta er nauðsynleg", + "Failed to get database information: %s" : "Mistókst að sækja gagnagrunnupplýsingar: %s", + "SOLR setup error: %s" : "SOLR uppsetningarvilla: %s", + "Reindex failed: %s" : "Endurskráning misheppnaðist: %s", + "Anonymized" : "Nafnlægt", + "Entities detected" : "Einingar greindar", + "Extract Now" : "Draga út núna", + "Extracted at" : "Dregið út við", + "Extraction" : "Útdráttur", + "Failed to load extraction data" : "Mistókst að hlaða útdráttursgögnum", + "Failed to load register data" : "Mistókst að hlaða skrárgögnum", + "Failed to retrieve extraction status." : "Mistókst að sækja stöðu útdrátturs.", + "Failed to retrieve objects for file." : "Mistókst að sækja hluti fyrir skrá.", + "No extraction data available for this file" : "Engin útdráttursgögn tiltæk fyrir þessa skrá", + "No register objects reference this file" : "Engin skrárhlutar vísa í þessa skrá", + "Not extracted" : "Ekki dregið út", + "Register Objects" : "Skrárhlutir", + "Risk level" : "Áhættuflokk", + "Text chunks" : "Textabútar", + "Unknown error" : "Óþekkt villa", + "Very high" : "Mjög hátt", + "{title} in {register} / {schema}" : "{title} í {register} / {schema}", + "Push Notifications" : "Ýtitilkynningar", + "Real-time push notification status via notify_push" : "Stav rauntíma ýtitilkynningar í gegnum notify_push", + "Realtime push not available — the notify_push app is not installed" : "Rauntíma ýti ekki tiltækt — notify_push forritið er ekki uppsett", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Settu upp notify_push forritið úr Nextcloud forritsverslun til að virkja rauntímauppfærslur.", + "Open Nextcloud App Store" : "Opna Nextcloud forritsverslun", + "notify_push is installed but not yet active" : "notify_push er uppsett en ekki enn virkt", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push er uppsett en OpenRegister hefur ekki enn staðfest farsælan ýti. Kveiktu á hlutavistun til að virkja, eða athugaðu notify_push stillinguna þína.", + "notify_push configuration guide" : "leiðbeiningar um notify_push stillingu", + "Realtime push active" : "Rauntíma ýti virkt", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Rauntíma ýtitilkynningar eru virkar. Tengdir viðskiptavinir fá tafarlausar uppfærslur þegar hlutir eru búnir til, uppfærðir eða eyddir.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n%10!=1 || n%100==11);" +); diff --git a/l10n/is.json b/l10n/is.json new file mode 100644 index 0000000000..a8862232c7 --- /dev/null +++ b/l10n/is.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin krafist)", + "(no subject)": "(ekkert efni)", + "(no title)": "(ekkert titill)", + "(unnamed)": "(ónefnt)", + "(untitled card)": "(kort án titils)", + "({count} selected)": "({count} valin)", + ", {failed} failed": ", {failed} mishepnuð", + "0 = unlimited": "0 = ótakmarkað", + "A deactivation request is pending.": "Beiðni um óvirkjun er í bið.", + "AI Assistant": "AI aðstoðarmaður", + "AI Chat": "AI spjall", + "AI service not configured": "AI þjónusta ekki stillt", + "API Key": "API lykill", + "API Request Quota (requests/day)": "API beiðnakvóti (beiðnir/dag)", + "API Token Configuration": "API tóken stilling", + "API Tokens": "API tóken", + "API tokens saved successfully": "API tóken vistuð", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG gr. 30 §4: þessi skýrsla tengir hverja vinnslustarfsemi við endurskoðunarferlatal hennar. Endurskoðendur og Autoriteit Persoonsgegevens nota þetta við eftirlit.", + "About ConfigSets": "Um ConfigSets", + "Access denied": "Aðgangur hafnaður", + "Access organisation dashboard and analytics": "Aðgangur að mælaborði og greiningu stofnunar", + "Account": "Reikningur", + "Action": "Aðgerð", + "Action Distribution": "Dreifing aðgerða", + "Action:": "Aðgerð:", + "Actions": "Aðgerðir", + "Active": "Virkur", + "Active Collections": "Virkar söfn", + "Active Filters": "Virkir síar", + "Active Organisation:": "Virk stofnun:", + "Active Organisations": "Virkar stofnanir", + "Active filters:": "Virkir síar:", + "Active organisation changed successfully": "Virk stofnun breytt", + "Active organisation set successfully": "Virk stofnun stillt", + "Activities": "Starfsemi", + "Activity": "Starfsemi", + "Activity Period": "Starfstímabil", + "Add": "Bæta við", + "Add Attachment": "Bæta við viðhengi", + "Add Endpoint": "Bæta við endapunkt", + "Add Groups": "Bæta við hópum", + "Add Property to '{title}'": "Bæta eiginleika við '{title}'", + "Add Register": "Bæta við skrá", + "Add Schema": "Bæta við skema", + "Add Source": "Bæta við uppsprettu", + "Add User": "Bæta við notanda", + "Add User to Organisation": "Bæta notanda við stofnun", + "Add a contact from any of your address books to associate it with this object.": "Bættu við tengilið úr einhverju af heimilisfangaskránum þínum til að tengja við þennan hlut.", + "Add a file or files": "Bæta við skrá eða skrám", + "Add card": "Bæta við korti", + "Add contact": "Bæta við tengilið", + "Add endpoint": "Bæta við endapunkt", + "Add oneOf entry": "Bæta við oneOf færslu", + "Add range": "Bæta við bili", + "Add schema titles, descriptions, and register information to provide richer context for search": "Bættu við titlum, lýsingum og skráningarupplýsingum í skema til að veita ríkari samhengi fyrir leit", + "Add to favorites": "Bæta við í eftirlæti", + "Added": "Bætt við", + "Added to favorites": "Bætt við í eftirlæti", + "Additional Information": "Viðbótarupplýsingar", + "Additional feedback saved. Thank you!": "Viðbótargjöf vistuð. Takk!", + "Addresses": "Heimilisföng", + "Admin": "Stjórnandi", + "Advanced": "Ítarlegt", + "Advanced Filters": "Ítarlegir síar", + "Advanced Options": "Ítarlegar valkostir", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Ítarlegir síar eru ekki tiltækir þegar gagnagrunnsuppspretta er notuð. Skiptu yfir í Auto eða SOLR Index fyrir síavalkosti.", + "After": "Eftir", + "Agent Type": "Tegund umboðsmanns", + "Agent deleted successfully": "Umboðsmaður eytt", + "Agent successfully created": "Umboðsmaður búinn til", + "Agent successfully updated": "Umboðsmaður uppfærður", + "Agents": "Umboðsmenn", + "Aggregated across schemas": "Samanlagt yfir skemu", + "All": "Allt", + "All Categories": "Allar flokkar", + "All Collections": "Öll söfn", + "All Confidence Levels": "Öll öryggisstig", + "All ConfigSets": "Öll ConfigSets", + "All Files": "Allar skrár", + "All Levels": "Öll stig", + "All Types": "Allar tegundir", + "All Webhooks": "Öll vefkrókur", + "All actions": "Allar aðgerðir", + "All audit trails cleared successfully": "Öll endurskoðunarferlatal hreinsað", + "All clear": "Allt í lagi", + "All file types": "Allar skráartegundir", + "All registers": "Allar skrár", + "All schemas": "Öll skemu", + "All search trails cleared successfully": "Öll leitarferlatal hreinsað", + "All searches": "Allar leitir", + "All users": "Allir notendur", + "All-day events": "Dagviðburðir", + "Allowed MIME Types": "Leyfðar MIME tegundir", + "Already linked": "Þegar tengt", + "An error occurred while clearing audit trails": "Villa kom upp við hreinsun endurskoðunarferlatals", + "An error occurred while deleting the audit trail": "Villa kom upp við eyðingu endurskoðunarferlatals", + "An error occurred while deleting the object": "Villa kom upp við eyðingu hlutarins", + "An error occurred while deleting the view": "Villa kom upp við eyðingu yfirlitsins", + "An error occurred while permanently deleting the objects": "Villa kom upp við varanlega eyðingu hlutanna", + "An error occurred while restoring the objects": "Villa kom upp við endurheimtu hlutanna", + "An error occurred while saving the organisation": "Villa kom upp við vistun stofnunarinnar", + "Analysis completed:": "Greining lokið:", + "Analysis steps:": "Greiningarskref:", + "Analytics": "Greiningar", + "Analyze Objects": "Greina hluti", + "Analyze Properties": "Greina eiginleika", + "Analyze existing properties for improvement opportunities": "Greina núverandi eiginleika með tilliti til endurbóta", + "Analyzing...": "Greinir...", + "Anonymized": "Nafnlægt", + "Anonymous Group": "Nafnlægur hópur", + "Any favorites and sharing settings for this view": "Hvaða uppáhaldslistar og deildaaðstillingar eru fyrir þetta yfirlit", + "Any user": "Hvaða notandi sem er", + "App ID": "Forritsauðkenni", + "App store cache invalidated successfully": "Forritsverslunarbuffer hreinsaður", + "App store cache invalidated: {invalidated}": "Forritsverslunarbuffer hreinsaður: {invalidated}", + "Application deleted successfully": "Forrit eytt", + "Application identifier for this configuration (optional)": "Forritsauðkenni fyrir þessa stillingu (valkvætt)", + "Application settings": "Stillingar forrits", + "Application successfully created": "Forrit búið til", + "Application successfully updated": "Forrit uppfært", + "Applications": "Forrit", + "Apply": "Nota", + "Apply Changes": "Nota breytingar", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Nota hlutverk á öll skemu í þessari skrá sem hafa ekki sérstakar heimildarhnekki.", + "Approval Progress": "Samþykktarframvindu", + "Approve": "Samþykkja", + "Approved": "Samþykkt", + "Archive": "Safn", + "Archive conversation": "Geyma samtal", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Geyma þessa verwerkingsactiviteit? Endurskoðunarferlatalraðir munu halda áfram að vísa í það.", + "Archived conversations are hidden from your active list": "Geymd samtöl eru falin á virkum lista þínum", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Ertu viss um að þú viljir EYÐA safni \"{name}\"?\n\nÞetta mun:\n• Varanlega eyða safni og öllum gögnum þess\n• Fjarlægja öll skráð skjöl\n• Þessa aðgerð er ekki hægt að afturkalla", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Ertu viss um að þú viljir hreinsa gamla leitarferlatal? Þetta mun eyða færslum eldri en 30 daga.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Ertu viss um að þú viljir hreinsa öll gögn úr safni \"{name}\"?\n\nÞetta mun:\n• Eyða öllum skráðum skjölum\n• Halda uppbyggingu safnsins óbreyttri\n• Þessa aðgerð er ekki hægt að afturkalla", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Ertu viss um að þú viljir eyða forriti \"{name}\"? Þessa aðgerð er ekki hægt að afturkalla.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Ertu viss um að þú viljir eyða völdum endurskoðunarferlatalum? Þessa aðgerð er ekki hægt að afturkalla.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Ertu viss um að þú viljir eyða völdum leitarferlatalum? Þessa aðgerð er ekki hægt að afturkalla.", + "Are you sure you want to delete this ConfigSet?": "Ertu viss um að þú viljir eyða þessum ConfigSet?", + "Are you sure you want to delete this configset?": "Ertu viss um að þú viljir eyða þessum configset?", + "Are you sure you want to leave '{name}'?": "Ertu viss um að þú viljir yfirgefa '{name}'?", + "Are you sure you want to permanently delete": "Ertu viss um að þú viljir varanlega eyða", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Ertu viss um að þú viljir endurskrá safn \"{name}\"?\n\nÞetta mun:\n• Endurbyggja skrána með öllum hlutum\n• Taka nokkrar mínútur\n• Geta haft áhrif á leitargæði við endurskráningu", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Ertu viss um að þú viljir fjarlægja skema \"{schema}\" úr skrá \"{register}\"? Þessa aðgerð er ekki hægt að afturkalla.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Ertu viss um að þú viljir mjúka-eyða {count} virkum hlutum fyrir skema \"{schema}\"?\n\nÞeir verða merktir sem eyddir en hægt er að eyða þeim varanlega síðar.", + "Array Object Configuration:": "Uppsetning fylkishluta:", + "Articles": "Greinar", + "Ask a question...": "Spyrðu spurningu...", + "Ask questions about your data using natural language": "Spyrðu spurninga um gögnin þín með náttúrulegu máli", + "Assigned collaborative tags": "Úthlutaðar samvinnutaggar", + "Assignment notifications": "Tilkynningar um úthlutun", + "Attachment added to {name}": "Viðhengi bætt við {name}", + "Attempt": "Tilraun", + "Audit Trail Changes": "Breytingar endurskoðunarferlatals", + "Audit Trail Details": "Upplýsingar endurskoðunarferlatals", + "Audit Trail Management": "Stjórnun endurskoðunarferlatals", + "Audit Trail Statistics": "Tölfræði endurskoðunarferlatals", + "Audit Trails": "Endurskoðunarferlatal", + "Audit trail data copied to clipboard": "Gögn endurskoðunarferlatals afritaðar á klippiborð", + "Audit trail deleted successfully": "Endurskoðunarferlatal eytt", + "Audit trail successfully deleted": "Endurskoðunarferlatal eytt", + "Authentication": "Auðkenning", + "Auto (e.g. Y for year, F Y for month)": "Sjálfvirkt (t.d. Y fyrir ár, F Y fyrir mánuð)", + "Auto share": "Sjálfvirk deiling", + "Auto-commit disabled": "Sjálfvirk staðfesting óvirkjuð", + "Auto-commit enabled": "Sjálfvirk staðfesting virkjuð", + "Auto-create Default Organisation": "Sjálfvirkt búa til sjálfgefna stofnun", + "Auto-retry failed vectorizations": "Endurtilraun á misheppnaðar vektorfærslur", + "Automatically commit changes to SOLR index": "Staðfesta breytingar sjálfvirkt í SOLR skrá", + "Automatically connect to the federation network.": "Tengja sjálfvirkt við sambandsnet.", + "Automatically create a default organisation if none exists when the app is initialized": "Búa sjálfvirkt til sjálfgefna stofnun ef engin er til þegar forritið er ræst", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Búa sjálfvirkt til vektórfellingar úr textabútum þegar skrár eru hlaðnar upp og unnar", + "Automatically generate vector embeddings when objects are created or updated": "Búa sjálfvirkt til vektórfellingar þegar hlutir eru búnir til eða uppfærðir", + "Automatically retry failed vectorization attempts (max 3 retries)": "Endurtilraun sjálfvirkt á misheppnaðar vektórfærslur (hámark 3 tilraunir)", + "Automatically update catalogues.": "Uppfæra vörulista sjálfvirkt.", + "Available Filters": "Tiltækir síar", + "Available Workflows": "Tiltæk vinnuflæði", + "Available placeholders:": "Tiltæk staðgengill:", + "Avatar": "Líkneskar", + "Avatar changes are not supported by your authentication provider.": "Auðkenningarveitan þín styður ekki breytingar á líkneskum.", + "Avatar removed": "Líkneskar fjarlægt", + "Avatar updated successfully": "Líkneskar uppfært", + "Avg Execution Time": "Meðaltímaframkvæmd", + "Avg Members/Org": "Meðalfjöldi meðlima/Stofnun", + "Avg Object Views/Session": "Meðalfjöldi hlutasýna/Lota", + "Avg Response Time": "Meðalsvartími", + "Avg Results/Search": "Meðalfjöldi niðurstaðna/Leit", + "Avg Searches/Session": "Meðalfjöldi leita/Lota", + "Back": "Til baka", + "Back to Entities": "Til baka í einingar", + "Back to Registers": "Til baka í skrár", + "Back to Webhooks": "Til baka í vefkrókar", + "Back to applications": "Til baka í forrit", + "Back to entities": "Til baka í einingar", + "Backend parameter is required": "Bakenda-færibreyta er nauðsynleg", + "Backend updated successfully. Please reload the application.": "Bakendi uppfærður. Vinsamlegast endurhladdu forritið.", + "Bandwidth Quota (MB/month)": "Bandbreiddarkvóti (MB/mánuður)", + "Base URL (Optional)": "Grunvefslóð (valkvætt)", + "Basic Information": "Grunnupplýsingar", + "Batch Size": "Lotustærð", + "Batch extraction completed": "Lotuútdráttur lokið", + "Before": "Áður", + "Before object vectorization can work:": "Áður en hlutvektórfærsla getur virkað:", + "Behavior": "Hegðun", + "Behavior Issue": "Hegðunarvandamál", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 tímalengd, t.d. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob-geymsla hefur verið afnumin. Allir hlutir nota nú töfratöflur.", + "Bookmarks": "Bókamerki", + "Branch": "Grein", + "Bulk Role Assignment: {title}": "Magnúthlutun hlutverka: {title}", + "Bulk delete operation completed successfully": "Magneyðing lokið", + "Bulk save operation completed successfully": "Magnvistun lokið", + "Business Data": "Viðskiptagögn", + "CSV": "CSV", + "Cache cleared successfully": "Skyndiminni hreinsað", + "Cache grew from {old} to {new} entries.": "Skyndiminni stækkaði úr {old} í {new} færslur.", + "Cache shrunk from {old} to {new} entries.": "Skyndiminni minnkaði úr {old} í {new} færslur.", + "Cache stayed the same at {size} entries.": "Skyndiminni hélst óbreytt með {size} færslur.", + "Calculate Sizes": "Reikna stærðir", + "Calendar": "Dagatal", + "Calendar Provider Configuration": "Stilling dagatalsveitu", + "Calendar integration is not available": "Dagatalssamþætting er ekki tiltæk", + "Calendar name": "Nafn dagataals", + "Cancel": "Hætta við", + "Cancel deactivation request": "Hætta við óvirkjunarbeiðni", + "Cannot delete: objects are still attached": "Get ekki eytt: hlutir eru enn tengdir", + "Cannot edit: This register is managed by external configuration {title}": "Get ekki breytt: Þessari skrá er stjórnað af ytri stillingu {title}", + "Cannot remove schema with existing objects ({count} objects)": "Get ekki fjarlægt skema með núverandi hlutum ({count} hlutir)", + "Cannot remove schema {schema} because it contains {count} objects": "Get ekki fjarlægt skema {schema} því það inniheldur {count} hluti", + "Cards": "Kort", + "Cascade delete": "Keðjueyðing", + "Categories of data subjects (one per line)": "Flokkar gagnasamninga (einn á línu)", + "Categories of personal data (one per line)": "Flokkar persónuupplýsinga (einn á línu)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Flokkur", + "Change Type": "Breyta tegund", + "Change password": "Breyta lykilorði", + "Changes": "Breytingar", + "Changes copied to clipboard": "Breytingar afritaðar á klippiborð", + "Changes for Audit Trail #{id}": "Breytingar fyrir endurskoðunarferlatal #{id}", + "Chat Model": "Spjallmódel", + "Chat Provider": "Spjallveita", + "Chat Provider (RAG)": "Spjallveita (RAG)", + "Chat Settings": "Spjallstillingar", + "Chat provider connection successful!": "Tenging við spjallveitu tókst!", + "Chat settings": "Spjallstillingar", + "Chat test failed: {error}": "Spjallprófun mistókst: {error}", + "Choose a register": "Veldu skrá", + "Choose a schema": "Veldu skema", + "Choose how vector similarity calculations are performed for semantic search": "Veldu hvernig vektórlíkindaútreikningar eru gerðir fyrir merkingarfræðilega leit", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Veldu hvaða skráartegundir á að taka með í vektórfærsluferlið. Aðeins skrár með dregnum texta og bútum verða unnar.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Veldu hvaða yfirlit á að taka með í vektórfærsluferlið. Skildu eftir tómt til að vinna öll yfirlit samkvæmt stillingum þínum.", + "Chunk": "Bútur", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Eyðing búta er ekki enn innleidd. Notaðu bútatengda endapunkta.", + "Chunks": "Bútar", + "Chunks to Vectorize:": "Bútar til að vektórafræðilegar:", + "Cleanup Old Trails": "Hreinsa gömul ferlatal", + "Cleanup completed": "Hreinsun lokið", + "Cleanup completed successfully. Deleted {count} entries.": "Hreinsun lokið. {count} færslur eyddar.", + "Cleanup failed: {error}": "Hreinsun misheppnaðist: {error}", + "Clear All Embeddings": "Hreinsa allar fellingar", + "Clear Entries": "Hreinsa færslur", + "Clear Filtered Audit Trails": "Hreinsa síuð endurskoðunarferlatal", + "Clear Filters": "Hreinsa síar", + "Clear Index": "Hreinsa skrá", + "Clear Selection": "Hreinsa val", + "Clear all": "Hreinsa allt", + "Clear all filters": "Hreinsa alla síar", + "Clear date range": "Hreinsa dagabilið", + "Clear filters": "Hreinsa síar", + "Clearing...": "Hreinsun...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Smelltu á Keyra fylgniskoðun til að finna skemu þar sem PII hefur fundist en engin vinnslustarfsemiskómerki eru til.", + "Click to subscribe to notifications": "Smelltu til að gerast áskrifandi að tilkynningum", + "Click to unsubscribe from notifications": "Smelltu til að hætta áskrift að tilkynningum", + "Close": "Loka", + "Cluster name": "Nafn þyrpingar", + "Code": "Kóði", + "Code (short readable key, e.g. v-2026-001)": "Kóði (stuttur læsilegur lykill, t.d. v-2026-001)", + "Collapse details": "Fella saman upplýsingar", + "Collection Management": "Stjórnun safna", + "Collection Name": "Nafn safns", + "Collection assignments updated successfully": "Úthlutanir safns uppfærðar", + "Collection cleared successfully": "Safn hreinsað", + "Collection copied successfully": "Safn afritað", + "Collection created successfully": "Safn búið til", + "Collection deleted successfully": "Safni eytt", + "Collection used to store and index file metadata and content": "Safn notað til að geyma og skrá lýsigögn og efni skráa", + "Collection used to store and index object data": "Safn notað til að geyma og skrá hlutgögn", + "Collections:": "Söfn:", + "Color": "Litur", + "Comment...": "Athugasemd...", + "Commit Message": "Staðfestingarskilaboð", + "Commit Within (ms)": "Staðfesta innan (ms)", + "Commit message": "Staðfestingarskilaboð", + "Compare current schema with real object data": "Bera saman núverandi skema við raunveruleg hlutgögn", + "Completed": "Lokið", + "Complex": "Flókið", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Flóknar fyrirspurnir: Ítarlegar leitir með mörgum síum, aðgerðum og flóknum færibreytusamsetningum", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Flóknar fyrirspurnir: ítarlegar leitir með mörgum síum, aðgerðum og flóknum færibreytusamsetningum", + "Compliance": "Fylgni", + "Confidence Level": "Öryggisstig", + "Confidence Score": "Öryggisskor", + "Confidence: {confidence}%": "Öryggi: {confidence}%", + "Config must be provided as an object": "Stilling verður að vera gefin upp sem hlutur", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Stjórnun ConfigSets", + "ConfigSet Name": "Nafn ConfigSet", + "ConfigSet created successfully": "ConfigSet búið til", + "ConfigSet deleted successfully": "ConfigSet eytt", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets skilgreina skema og stillingar fyrir SOLR söfn þín. Þær innihalda svæðisskilgreiningar, greinar og aðrar leitarstillingar.", + "Configuration": "Stilling", + "Configuration Data": "Stillingagögn", + "Configuration is up to date": "Stilling er uppfærð", + "Configuration saved": "Stilling vistuð", + "Configuration saved successfully.": "Stilling vistuð.", + "Configuration type (default, application, etc.)": "Tegund stillingar (sjálfgefin, forrit, o.fl.)", + "Configurations": "Stillingar", + "Configure API tokens for external service integrations": "Stilla API tóken fyrir ytri þjónustusamþætting", + "Configure Apache SOLR search engine for advanced search capabilities": "Stilla Apache SOLR leitarvél fyrir ítarlegar leitargæðar", + "Configure Facets": "Stilla hliðar", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Stilla stórar tungumálalíkansveitu (LLM) fyrir AI-knúnar eiginleikar þar á meðal merkingarfræðileg leit, fellingar og spjall.", + "Configure SOLR Facets": "Stilla SOLR hliðar", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Stilla grunntengingarstillingar fyrir SOLR þjón þinn þar á meðal auðkenningu og netmöguleika. Notaðu sérstaka ConfigSet og Safnstjórnunargluggana til að stjórna kjörnum og söfnum.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Stilla grunntengingarstillingar fyrir SOLR þjón þinn þar á meðal auðkenningu og netmöguleika. Notaðu sérstaka configset og safnstjórnunargluggana til að stjórna kjörnum og söfnum.", + "Configure default organisation and organisation-related settings": "Stilla sjálfgefna stofnun og stillingar tengdar stofnun", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Stilla hvernig gagnagrunnshlutar eru umbreyttir í vektórfellingar fyrir merkingarfræðilega leit. Hlutir eru vektóraðir beint án þess að þurfa textaútdrátt.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Stilla hvernig hlutir eru umbreyttir í texta fyrir vektórfærslu. Þessar stillingar hafa áhrif á leitargæði og samhengi.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Stilla stórar tungumálalíkansveitu (LLM) fyrir ai-knúnar eiginleikar þar á meðal merkingarfræðileg leit, fellingar og spjall.", + "Configure multi-organization support and tenant isolation": "Stilla stuðning við margar stofnanir og einangun leigjenda", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Stilla færibreytur fyrir skráavektórfærslu. Þetta ferli mun búa til vektórfellingar fyrir alla dregna skráarbúta.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Stilla færibreytur fyrir hlutvektórfærslu. Þetta ferli mun búa til vektórfellingar fyrir alla hluti sem passa við yfirlitssíarnar þínar.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Stilla þetta skema til að sýna hluti sem viðburði í Nextcloud Dagatal forriti.", + "Configure which types of data to search and how many sources to retrieve": "Stilla hvaða gagnategundir á að leita í og hve margar uppsprettur á að sækja", + "Configuring field mappings...": "Stilling svæðisvarpana...", + "Confirm Account Deactivation": "Staðfesta óvirkjun reiknings", + "Confirm Object Deletion": "Staðfesta eyðingu hlutarins", + "Confirm Object Restoration": "Staðfesta endurheimtu hlutarins", + "Confirm Permanent Object Deletion": "Staðfesta varanlega eyðingu hlutarins", + "Confirm deactivation": "Staðfesta óvirkjun", + "Connecting to SOLR cluster...": "Tengjast SOLR þyrpingu...", + "Connection Failed": "Tenging mistókst", + "Connection Settings": "Tengingarstillingar", + "Connection Successful!": "Tenging tókst!", + "Connection failed": "Tenging mistókst", + "Connection protocol": "Tengingarskyldur", + "Connection successful": "Tenging tókst", + "Connection timeout in seconds": "Tengingarúrræðastími í sekúndum", + "Constant delay between retries (5 minutes)": "Stöðug seinkun milli endurtilrauna (5 mínútur)", + "Constraint Issue": "Takmörkunarvandamál", + "Consumer not found": "Neytandi ekki fundinn", + "Contacts": "Tengiliðir", + "Contacts integration is not available": "Tengiliðasamþætting er ekki tiltæk", + "Content Filters": "Efnissíar", + "Continue to Properties": "Halda áfram í eiginleika", + "Contracts": "Samninga", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Stjórna hvaða hlutayfirlit á að vektóra til að draga úr API kostnaði. Vektóra aðeins yfirlit sem hagnast á merkingarfræðilegri leit.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Stjórna hvaða yfirlit og verkfæri AI getur notað í þessu samtali. Sjálfgefið eru allar umboðsgæðar virkjaðar.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Stjórnar tilviljunarkenni (0 = ákveðið, 2 = mjög skapandi)", + "Conversation ID is required": "Samtalsauðkenni er nauðsynlegt", + "Conversation archived": "Samtal geymt", + "Conversation archived successfully": "Samtal geymt", + "Conversation cleared successfully": "Samtal hreinsað", + "Conversation deleted": "Samtali eytt", + "Conversation not found": "Samtal ekki fundið", + "Conversation permanently deleted": "Samtali varanlega eytt", + "Conversation renamed": "Samtal endurnefnt", + "Conversation restored": "Samtal endurheimt", + "Conversation started with {agent}": "Samtal hafið við {agent}", + "Conversation title": "Titill samtals", + "Conversations": "Samtöl", + "Converting {schema} to blob storage...": "Umbreytir {schema} í blob-geymslu...", + "Converting {schema} to magic table...": "Umbreytir {schema} í töfratöflu...", + "Copied!": "Afritað!", + "Copy": "Afrita", + "Copy Changes": "Afrita breytingar", + "Copy Collection": "Afrita safn", + "Copy Data": "Afrita gögn", + "Copy Full Data": "Afrita öll gögn", + "Copy of {name}": "Afrit af {name}", + "Copy to clipboard": "Afrita á klippiborð", + "Copying...": "Afritar...", + "Costs": "Kostnaður", + "Count": "Fjöldi", + "Counting objects...": "Tel hluti...", + "Create": "Búa til", + "Create API Token": "Búa til API tóken", + "Create Agent": "Búa til umboðsmann", + "Create Application": "Búa til forrit", + "Create Collection": "Búa til safn", + "Create ConfigSet": "Búa til ConfigSet", + "Create New Collection": "Búa til nýtt safn", + "Create New ConfigSet": "Búa til nýtt ConfigSet", + "Create Operations": "Búningaaðgerðir", + "Create Organisation": "Búa til stofnun", + "Create Webhook": "Búa til vefkrók", + "Create a copy of collection:": "Búa til afrit af safni:", + "Create a new ConfigSet based on the _default template": "Búa til nýtt ConfigSet byggt á _default sniðmáti", + "Create a new SOLR collection from an existing ConfigSet": "Búa til nýtt SOLR safn úr núverandi ConfigSet", + "Create a new SOLR collection from an existing configset": "Búa til nýtt SOLR safn úr núverandi configset", + "Create a new configset based on the _default template": "Búa til nýtt configset byggt á _default sniðmáti", + "Create a new event or link an existing one from any of your calendars.": "Búa til nýjan viðburð eða tengja við núverandi úr einhverju af dagatalunum þínum.", + "Create another": "Búa til annað", + "Create copies of": "Búa til afrit af", + "Create event": "Búa til viðburð", + "Create new token": "Búa til nýtt tóken", + "Create or link a Deck card to track work on this object.": "Búa til eða tengja Deck kort til að rekja vinnu á þessum hlut.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Búðu til fyrstu verwerkingsactiviteit til að byrja að merkja endurskoðunarferlatalraðir með AVG gr. 30 tilvísun þeirra.", + "Create your first AI agent to get started.": "Búðu til fyrsta AI umboðsmann þinn til að byrja.", + "Created": "Búið til", + "Created:": "Búið til:", + "Creating search collections...": "Bý til leitarsöfn...", + "Creating...": "Bý til...", + "Current": "Núverandi", + "Current Filters": "Núverandi síar", + "Current Type:": "Núverandi tegund:", + "Current password": "Núverandi lykilorð", + "Custom API endpoint if using a different region": "Sérsniðin API endapunktur ef önnur svæði eru notuð", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Sérsniðnar HTTP hausar (ein á línu, snið: Hausheiti: gildi)", + "Custom HTTP headers (one per line, format: header-name: value)": "Sérsniðnar HTTP hausar (ein á línu, snið: hausheiti: gildi)", + "Custom Ranges:": "Sérsniðin bil:", + "Custom display title for this facet": "Sérsniðinn sýningartitill fyrir þessa hlið", + "Custom pattern:": "Sérsniðið mynstur:", + "DELETE COLLECTION": "EYÐA SAFNI", + "DSAR": "DSAR", + "Daily": "Daglega", + "Dashboard": "Mælaborð", + "Dashboard not found": "Mælaborð ekki fundið", + "Data": "Gögn", + "Data Source": "Gagnaveita", + "Data Sources": "Gagnaveitu", + "Data Views": "Gagnasýn", + "Data sources": "Gagnaveitu", + "Data type does not match observed values": "Gagnategund passar ekki við fylgstu gildi", + "Data-subject access request": "Beiðni um aðgang gagnaeiganda", + "Database URL": "Gagnagrunnsvefslóð", + "Database information refreshed": "Gagnagrunnupplýsingar uppfærðar", + "Date Faceting:": "Dagsetningarhliðar:", + "Date Range": "Dagsetningarbil", + "Dates": "Dagsetningar", + "Deactivation request cancelled": "Óvirkjunarbeiðni afturkölluð", + "Deactivation request submitted": "Óvirkjunarbeiðni send", + "Deck": "Deck", + "Deck integration is not available": "Deck samþætting er ekki tiltæk", + "Default": "Sjálfgefið", + "Default Object Tenant": "Sjálfgefinn hlutleigjandi", + "Default Organisation": "Sjálfgefin stofnun", + "Default Owner": "Sjálfgefinn eigandi", + "Default User (for cron/background jobs)": "Sjálfgefinn notandi (fyrir cron/bakgrunnsvinnu)", + "Default User Tenant": "Sjálfgefinn notandaleigjandi", + "Default value": "Sjálfgefið gildi", + "Default value (Email)": "Sjálfgefið gildi (Tölvupóstur)", + "Default value (Password)": "Sjálfgefið gildi (Lykilorð)", + "Default value (Phone number)": "Sjálfgefið gildi (Símanúmer)", + "Default value (Regex)": "Sjálfgefið gildi (Regex)", + "Default: 5": "Sjálfgefið: 5", + "Defaults to current user. Select a different user if needed.": "Sjálfgefið núverandi notandi. Veldu annan notanda ef þörf krefur.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Seinkun tvöfaldast með hverri tilraun (2, 4, 8 mínútur...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Seinkun eykst línulega (5, 10, 15 mínútur...)", + "Delete": "Eyða", + "Delete ({count})": "Eyða ({count})", + "Delete Application": "Eyða forriti", + "Delete Audit Trail": "Eyða endurskoðunarferlatal", + "Delete Collection": "Eyða safni", + "Delete ConfigSet": "Eyða ConfigSet", + "Delete Objects": "Eyða hlutum", + "Delete Operations": "Eyðingaaðgerðir", + "Delete SOLR Collection": "Eyða SOLR safni", + "Delete View": "Eyða yfirliti", + "Delete all objects in this schema": "Eyða öllum hlutum í þessu skema", + "Delete permanently": "Eyða varanlega", + "Delete this conversation?": "Eyða þessu samtali?", + "Delete view": "Eyða yfirliti", + "Delete {count} object": "Eyða {count} hlut", + "Delete {count} objects": "Eyða {count} hlutum", + "Deleted": "Eytt", + "Deleted By": "Eytt af", + "Deleted Date": "Dagsetning eyðingar", + "Deleted Items Management": "Stjórnun eyddra hluta", + "Deleted This Week": "Eytt þessa viku", + "Deleted Today": "Eytt í dag", + "Deleted:": "Eytt:", + "Deleting SOLR Collection...": "Eyði SOLR safni...", + "Deleting...": "Eyði...", + "Deletion Date Range": "Dagsetningarbil eyðingar", + "Deletion Statistics": "Tölfræði eyðingar", + "Deprecated": "Úrelt", + "Depublish": "Afútgáfa", + "Description": "Lýsing", + "Description Template": "Lýsingarsniðmát", + "Description shown as tooltip": "Lýsing sýnd sem ábending", + "Description:": "Lýsing:", + "Detect data types and patterns": "Greina gagnategundir og mynstur", + "Detected At": "Greint við", + "Detected Format:": "Greint snið:", + "Detected Issues:": "Greind vandamál:", + "Detected: {date}": "Greint: {date}", + "Direct permission": "Beint leyfi", + "Disable": "Gera óvirkt", + "Disable this to make the agent": "Gera þetta óvirkt til að gera umboðsmanninn", + "Disabled": "Óvirkt", + "Discover Files": "Finna skrár", + "Discovered Properties": "Fundnir eiginleikar", + "Display Format": "Sýningarsnið", + "Display Name": "Sýningarnafn", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Viltu varanlega eyða öllum síuðum endurskoðunarferslum? Þessa aðgerð er ekki hægt að afturkalla.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Viltu varanlega eyða þessari endurskoðunarferslu? Þessa aðgerð er ekki hægt að afturkalla.", + "Documents": "Skjöl", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR virkjað", + "Download API Spec": "Sækja API forskrift", + "Download API Specification": "Sækja API forskrift", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Sæktu afrit af öllum persónulegum gögnum þínum sem eru geymd í OpenRegister (GDPR gr. 20).", + "Drag a file or files here": "Dragðu skrá eða skrár hingað", + "Dry run completed: {count} fields would be created": "Prófkeyrsla lokið: {count} svæði myndu búast til", + "Dry run completed: {count} fields would be fixed": "Prófkeyrsla lokið: {count} svæði myndu leiðréttast", + "Duration in seconds (optional)": "Lengd í sekúndum (valkvætt)", + "Edit": "Breyta", + "Edit Agent": "Breyta umboðsmanni", + "Edit Application": "Breyta forriti", + "Edit Configuration": "Breyta stillingu", + "Edit Endpoint": "Breyta endapunkti", + "Edit Organisation": "Breyta stofnun", + "Edit Property '{key}' of '{title}'": "Breyta eiginleika '{key}' af '{title}'", + "Edit Register": "Breyta skrá", + "Edit Schema": "Breyta skema", + "Edit Source": "Breyta uppsprettu", + "Edit View": "Breyta yfirliti", + "Edit Webhook": "Breyta vefkrók", + "Edit labels": "Breyta merkimiðum", + "Edit processing activity": "Breyta vinnslustarfsemi", + "Edit tags for {name}": "Breyta taggum fyrir {name}", + "Edit verwerkingsactiviteit": "Breyta verwerkingsactiviteit", + "Edit view": "Breyta yfirliti", + "Edit view details": "Breyta upplýsingum yfirlits", + "Effectiveness": "Skilvirkni", + "Email": "Tölvupóstur", + "Email Addresses": "Tölvupóstfang", + "Email digest frequency": "Tíðni tölvupóststútdráttar", + "Emails": "Tölvupóstar", + "Embedding Model": "Fellingarlíkan", + "Embedding Provider": "Fellingarveita", + "Embedding generated successfully": "Felling búin til", + "Embedding provider connection successful!": "Tenging við fellingarveitu tókst!", + "Embedding test failed: {error}": "Fellingarprófun mistókst: {error}", + "Enable": "Virkja", + "Enable RAG": "Virkja RAG", + "Enable auto-creation": "Virkja sjálfvirka búningu", + "Enable automatic file vectorization": "Virkja sjálfvirka skráavektórfærslu", + "Enable automatic object vectorization": "Virkja sjálfvirka hlutvektórfærslu", + "Enable automatic synchronization": "Virkja sjálfvirka samstillingu", + "Enable calendar provider": "Virkja dagatalsveitu", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Virkja ítarlega skráningu fyrir SOLR aðgerðir (mælt með við villuleit)", + "Enable faceting": "Virkja hliðarflokkun", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Virkja eða gera óvirkt LLM eiginleika. Stilla veitur og líkön með LLM stillingarhnappinum að ofan.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Virkja eða gera óvirkt LLM eiginleika. Stilla veitur og líkön með LLM stillingarhnappinum að ofan.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Virkja eða gera óvirkt SOLR leitarsamþættingu. Stilla tengingarstillingar með Tengingarstillingar hnappinum að ofan.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Virkja eða gera óvirkt SOLR leitarsamþættingu. Stilla tengingarstillingar með tengingarstillingar hnappinum að ofan.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Virkja eða gera óvirkt n8n vinnuflæðisamþættingu. Stilla tengingarstillingar hér að neðan.", + "Enable or disable this webhook": "Virkja eða gera óvirkt þennan vefkrók", + "Enable tools that allow the agent to interact with data through function calling.": "Virkja verkfæri sem leyfa umboðsmanninum að hafa samskipti við gögn með fallakalli.", + "Enable vectorization for all existing and future views (may increase costs)": "Virkja vektórfærslu fyrir öll núverandi og framtíðaryfirlit (getur aukið kostnað)", + "Enable write-back to target objects": "Virkja endurskrift í markhluti", + "Enabled": "Virkt", + "End Date Field": "Lokadagsetningarsvæði", + "Endpoint Path*": "Endapunktsleiðin*", + "Endpoints": "Endapunktar", + "Engine not found": "Vél ekki fundin", + "Enter ConfigSet name": "Sláðu inn nafn ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Sláðu inn Nextcloud notandanöfn til að veita aðgang að þessum einka umboðsmanni", + "Enter agent description (optional)": "Sláðu inn lýsingu umboðsmanns (valkvætt)", + "Enter agent name": "Sláðu inn nafn umboðsmanns", + "Enter application description (optional)": "Sláðu inn lýsingu forrits (valkvætt)", + "Enter application name": "Sláðu inn nafn forrits", + "Enter collection name": "Sláðu inn nafn safns", + "Enter configuration description (optional)": "Sláðu inn lýsingu stillingar (valkvætt)", + "Enter configuration title": "Sláðu inn titil stillingar", + "Enter description (optional)...": "Sláðu inn lýsingu (valkvætt)...", + "Enter new collection name": "Sláðu inn nýtt nafn safns", + "Enter object ID": "Sláðu inn hlutauðkenni", + "Enter organisation description (optional)": "Sláðu inn lýsingu stofnunar (valkvætt)", + "Enter organisation name": "Sláðu inn nafn stofnunar", + "Enter search term": "Sláðu inn leitarhugtök", + "Enter search terms or leave empty to browse all": "Sláðu inn leitarhugtök eða skildu tómt til að skoða allt", + "Enter system prompt for the agent": "Sláðu inn kerfiseininguna fyrir umboðsmanninn", + "Enter username and press Enter": "Sláðu inn notandanafn og ýttu á Enter", + "Enter view name...": "Sláðu inn nafn yfirlits...", + "Enter webhook name": "Sláðu inn nafn vefkróks", + "Enter your API key": "Sláðu inn API lykil þinn", + "Enter your object here...": "Sláðu inn hlut þinn hér...", + "Enter your schema here...": "Sláðu inn skema þitt hér...", + "Entities": "Einingar", + "Entities detected": "Einingar greindar", + "Entity ID: {id}": "Einingarauðkenni: {id}", + "Entity Information": "Upplýsingar um einingu", + "Entity Recognition": "Einingagreining", + "Entity Recognition Method": "Aðferð einingagreiningar", + "Entity deleted successfully": "Einingu eytt", + "Entity not found": "Eining ekki fundin", + "Entries to be deleted:": "Færslur til að eyða:", + "Enum Issue": "Upptalningarvandamál", + "Enum constraint is missing": "Upptalningartakmörkun vantar", + "Erase (Art 17)": "Eyða (gr. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Eyða {count} hlut(um) fyrir þetta efni? Þessi aðgerð er skráð í endurskoðunarferlatal.", + "Erasure complete": "Eyðing lokið", + "Erasure preview": "Forskoðun eyðingar", + "Error": "Villa", + "Error Details": "Villuupplýsingar", + "Error Information": "Villuupplýsingar", + "Error Message": "Villuskilaboð", + "Error deleting audit trails: {error}": "Villa við eyðingu endurskoðunarferlatal: {error}", + "Error deleting search trails: {error}": "Villa við eyðingu leitarferlatal: {error}", + "Error loading application": "Villa við hleðslu forrits", + "Error loading audit trails": "Villa við hleðslu endurskoðunarferlatal", + "Error loading entity": "Villa við hleðslu einingar", + "Error loading search trails": "Villa við hleðslu leitarferlatal", + "Estimated Batches:": "Áætlaðar lotur:", + "Estimated Cost:": "Áætlaður kostnaður:", + "Estimated Duration:": "Áætluð lengd:", + "Event": "Viðburður", + "Event Property for Payload": "Viðburðareiginleiki fyrir farminn", + "Events": "Viðburðir", + "Every schema with detected PII has a processing-activity annotation.": "Hvert skema með greint PII hefur vinnslustarfsemiskómerki.", + "Example": "Dæmi", + "Example Value": "Dæmigildi", + "Example value for this property": "Dæmigildi fyrir þennan eiginleika", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Einhlýt hámark", + "Exclusive Minimum": "Einhlýtt lágmark", + "Exclusive maximum": "Einhlýt hámark", + "Exclusive minimum": "Einhlýtt lágmark", + "Execution Mode": "Framkvæmdaháttur", + "Execution Time": "Framkvæmdatími", + "Execution Time Range": "Bil framkvæmdatíma", + "Existing Improvements": "Núverandi endurbætur", + "Expand details": "Þenjast út upplýsingar", + "Expiration": "Lok", + "Expires": "Rennur út", + "Expires in (e.g., 90d)": "Rennur út eftir (t.d. 90d)", + "Exponential": "Veldisvísir", + "Export": "Flutningur út", + "Export \"{schema}\" objects from \"{register}\"": "Flytja út \"{schema}\" hluti úr \"{register}\"", + "Export Configuration": "Flytja út stillingu", + "Export Format:": "Útflutningssnið:", + "Export Objects": "Flytja út hluti", + "Export completed successfully": "Útflutningur lokið", + "Export downloaded successfully": "Útflutningur sóttur", + "Export failed: {error}": "Útflutningur misheppnaðist: {error}", + "Export is rate limited. Please try again later.": "Útflutningur er takmarkaður. Vinsamlegast reyndu aftur síðar.", + "Export my data": "Flytja út gögn mín", + "Export, view, or delete audit trails": "Flytja út, skoða eða eyða endurskoðunarferlatalum", + "Exporting...": "Flytur út...", + "Extend Schema": "Stækka skema", + "Extended": "Stækkað", + "Extensions": "Viðbætur", + "Extra Query Parameters": "Auka fyrirspurnafæribreytur", + "Extract Now": "Draga út núna", + "Extract Pending Files": "Draga út bíðandi skrár", + "Extract now": "Draga út núna", + "Extract properties from each object": "Draga eiginleika úr hverjum hlut", + "Extracted At": "Dregið út við", + "Extracted at": "Dregið út við", + "Extraction": "Útdráttur", + "Extraction Mode": "Útdrátturháttur", + "Extraction Scope": "Umfang útdrátturs", + "Extraction Status": "Staða útdrátturs", + "Extraction error for {file}: {error}": "Útdráttur villa fyrir {file}: {error}", + "Facet Description": "Lýsing hliðar", + "Facet Order": "Röðun hliðar", + "Facet Title": "Titill hliðar", + "Facet Type": "Tegund hliðar", + "Facet configuration updated successfully": "Hliðarstilling uppfærð", + "Facetable": "Hliðarhæft", + "Faceting Configuration:": "Hliðarflokkunarstilling:", + "Facets discovered and configured successfully": "Hliðar fundin og stillt", + "Facets discovered successfully": "Hliðar fundin", + "Failed": "Misheppnaðst", + "Failed to add attachment to object": "Mistókst að bæta viðhengi við hlut", + "Failed to add user to organisation": "Mistókst að bæta notanda við stofnun", + "Failed to analyze schema properties": "Mistókst að greina skemaeiginleika", + "Failed to archive conversation": "Mistókst að geyma samtal", + "Failed to calculate sizes": "Mistókst að reikna stærðir", + "Failed to cancel deactivation": "Mistókst að afturkalla óvirkjun", + "Failed to change active organisation: {error}": "Mistókst að breyta virkri stofnun: {error}", + "Failed to change password": "Mistókst að breyta lykilorði", + "Failed to check version: {error}": "Mistókst að athuga útgáfu: {error}", + "Failed to clear audit trails: {error}": "Mistókst að hreinsa endurskoðunarferlatal: {error}", + "Failed to clear blob objects: {error}": "Mistókst að hreinsa blob hluti: {error}", + "Failed to clear cache: {error}": "Mistókst að hreinsa skyndiminni: {error}", + "Failed to clear collection": "Mistókst að hreinsa safn", + "Failed to clear conversation": "Mistókst að hreinsa samtal", + "Failed to clear embeddings: {error}": "Mistókst að hreinsa fellingar: {error}", + "Failed to clear search trails: {error}": "Mistókst að hreinsa leitarferlatal: {error}", + "Failed to clear {type} cache: {error}": "Mistókst að hreinsa {type} skyndiminni: {error}", + "Failed to copy changes": "Mistókst að afrita breytingar", + "Failed to copy data": "Mistókst að afrita gögn", + "Failed to copy data to clipboard": "Mistókst að afrita gögn á klippiborð", + "Failed to copy to clipboard": "Mistókst að afrita á klippiborð", + "Failed to copy token": "Mistókst að afrita tóken", + "Failed to create conversation": "Mistókst að búa til samtal", + "Failed to create missing SOLR fields: {error}": "Mistókst að búa til vantandi SOLR svæði: {error}", + "Failed to create or find project": "Mistókst að búa til eða finna verkefni", + "Failed to create token": "Mistókst að búa til tóken", + "Failed to delete SOLR collection: {error}": "Mistókst að eyða SOLR safni: {error}", + "Failed to delete collection": "Mistókst að eyða safni", + "Failed to delete conversation": "Mistókst að eyða samtali", + "Failed to delete objects for {schema}: {error}": "Mistókst að eyða hlutum fyrir {schema}: {error}", + "Failed to delete webhook": "Mistókst að eyða vefkrók", + "Failed to depublish register: {error}": "Mistókst að afútgáfa skrá: {error}", + "Failed to depublish schema: {error}": "Mistókst að afútgáfa skema: {error}", + "Failed to depublish: {error}": "Mistókst að afútgáfa: {error}", + "Failed to discover files: {error}": "Mistókst að finna skrár: {error}", + "Failed to download API specification": "Mistókst að sækja API forskrift", + "Failed to export data": "Mistókst að flytja út gögn", + "Failed to export objects": "Mistókst að flytja út hluti", + "Failed to fetch conversation history": "Mistókst að sækja sögu samtals", + "Failed to fix mismatched SOLR fields: {error}": "Mistókst að laga ósamkvæm SOLR svæði: {error}", + "Failed to get SOLR field configuration": "Mistókst að sækja SOLR svæðissttillingu", + "Failed to get chat statistics": "Mistókst að sækja spjallstat", + "Failed to get database information: %s": "Mistókst að sækja gagnagrunnupplýsingar: %s", + "Failed to get entity categories": "Mistókst að sækja einingaflokka", + "Failed to get entity statistics": "Mistókst að sækja einingstölfræði", + "Failed to get entity types": "Mistókst að sækja einingategundir", + "Failed to get response: {error}": "Mistókst að fá svar: {error}", + "Failed to invalidate app store cache: {error}": "Mistókst að gera forritsverslunarbuffer ógildan: {error}", + "Failed to leave organisation: {error}": "Mistókst að yfirgefa stofnun: {error}", + "Failed to link object": "Mistókst að tengja hlut", + "Failed to load Deck cards": "Mistókst að hlaða Deck korti", + "Failed to load LLM configuration": "Mistókst að hlaða LLM stillingu", + "Failed to load Nextcloud groups": "Mistókst að hlaða Nextcloud hópa", + "Failed to load cache statistics: {error}": "Mistókst að hlaða skyndiminnistat: {error}", + "Failed to load conversation": "Mistókst að hlaða samtali", + "Failed to load entities": "Mistókst að hlaða einingum", + "Failed to load entity": "Mistókst að hlaða einingu", + "Failed to load extraction data": "Mistókst að hlaða útdráttursgögnum", + "Failed to load files": "Mistókst að hlaða skrám", + "Failed to load linked contacts": "Mistókst að hlaða tengda tengiliði", + "Failed to load linked emails": "Mistókst að hlaða tengda tölvupósta", + "Failed to load linked events": "Mistókst að hlaða tengda viðburði", + "Failed to load organisations": "Mistókst að hlaða stofnunum", + "Failed to load register data": "Mistókst að hlaða skrárgögnum", + "Failed to load related entities": "Mistókst að hlaða tengdar einingar", + "Failed to load settings: {error}": "Mistókst að hlaða stillingum: {error}", + "Failed to load statistics: {error}": "Mistókst að hlaða tölfræði: {error}", + "Failed to load templates": "Mistókst að hlaða sniðmátum", + "Failed to load view: {error}": "Mistókst að hlaða yfirliti: {error}", + "Failed to load webhooks": "Mistókst að hlaða vefkrókum", + "Failed to load workflows": "Mistókst að hlaða vinnuflæðum", + "Failed to process message": "Mistókst að vinna skilaboð", + "Failed to publish register: {error}": "Mistókst að gefa út skrá: {error}", + "Failed to publish schema: {error}": "Mistókst að gefa út skema: {error}", + "Failed to publish: {error}": "Mistókst að gefa út: {error}", + "Failed to refresh database information": "Mistókst að uppfæra gagnagrunnupplýsingar", + "Failed to reindex collection": "Mistókst að endurskrá safn", + "Failed to remove avatar": "Mistókst að fjarlægja líkneskar", + "Failed to remove link": "Mistókst að fjarlægja tengil", + "Failed to remove member: {error}": "Mistókst að fjarlægja meðlim: {error}", + "Failed to remove schema {schema}: {error}": "Mistókst að fjarlægja skema {schema}: {error}", + "Failed to remove user from organisation": "Mistókst að fjarlægja notanda úr stofnun", + "Failed to rename conversation": "Mistókst að endurnefna samtal", + "Failed to request deactivation": "Mistókst að biðja um óvirkjun", + "Failed to restore conversation": "Mistókst að endurheimta samtal", + "Failed to retrieve extraction status.": "Mistókst að sækja stöðu útdrátturs.", + "Failed to retrieve objects for file.": "Mistókst að sækja hluti fyrir skrá.", + "Failed to retry extraction": "Mistókst að endurtaka útdrátt", + "Failed to retry webhook": "Mistókst að endurtaka vefkrók", + "Failed to revoke token": "Mistókst að afturkalla tóken", + "Failed to save GitHub token": "Mistókst að vista GitHub tóken", + "Failed to save GitLab URL": "Mistókst að vista GitLab vefslóð", + "Failed to save GitLab token": "Mistókst að vista GitLab tóken", + "Failed to save LLM settings: {error}": "Mistókst að vista LLM stillingar: {error}", + "Failed to save additional feedback": "Mistókst að vista viðbótargjöf", + "Failed to save configuration.": "Mistókst að vista stillingu.", + "Failed to save configuration: {error}": "Mistókst að vista stillingu: {error}", + "Failed to save facet configuration": "Mistókst að vista hliðarstillingu", + "Failed to save feedback": "Mistókst að vista gjöf", + "Failed to save file settings: {error}": "Mistókst að vista skráastillingar: {error}", + "Failed to save n8n configuration": "Mistókst að vista n8n stillingu", + "Failed to save preferences": "Mistókst að vista kjörstillingar", + "Failed to save roles": "Mistókst að vista hlutverk", + "Failed to save settings": "Mistókst að vista stillingar", + "Failed to save settings: {error}": "Mistókst að vista stillingar: {error}", + "Failed to save view: {error}": "Mistókst að vista yfirlit: {error}", + "Failed to save warmup interval: {error}": "Mistókst að vista hlýnunarbil: {error}", + "Failed to save webhook": "Mistókst að vista vefkrók", + "Failed to send feedback": "Mistókst að senda gjöf", + "Failed to set active organisation: {error}": "Mistókst að stilla virka stofnun: {error}", + "Failed to setup SOLR: {error}": "Mistókst að setja upp SOLR: {error}", + "Failed to start file warmup: {error}": "Mistókst að hefja skráahlýnun: {error}", + "Failed to start processing {type} files: {error}": "Mistókst að hefja úrvinnslu {type} skráa: {error}", + "Failed to start vectorization: {error}": "Mistókst að hefja vektórfærslu: {error}", + "Failed to switch organisation: {error}": "Mistókst að skipta um stofnun: {error}", + "Failed to sync magic table for {schema}: {error}": "Mistókst að samstilla töfratöflu fyrir {schema}: {error}", + "Failed to test SOLR connection: {error}": "Mistókst að prófa SOLR tengingu: {error}", + "Failed to test webhook": "Mistókst að prófa vefkrók", + "Failed to update LLM settings: {error}": "Mistókst að uppfæra LLM stillingar: {error}", + "Failed to update Multitenancy settings: {error}": "Mistókst að uppfæra margleigjendastillingar: {error}", + "Failed to update RBAC settings: {error}": "Mistókst að uppfæra RBAC stillingar: {error}", + "Failed to update Retention settings: {error}": "Mistókst að uppfæra varðveislustillingar: {error}", + "Failed to update SOLR settings: {error}": "Mistókst að uppfæra SOLR stillingar: {error}", + "Failed to update favorite status": "Mistókst að uppfæra eftirlætisástand", + "Failed to update schema configuration for {schema}: {error}": "Mistókst að uppfæra skemastillingu fyrir {schema}: {error}", + "Failed to update schema properties": "Mistókst að uppfæra skemaeiginleika", + "Failed to update view": "Mistókst að uppfæra yfirlit", + "Failed to update view: {error}": "Mistókst að uppfæra yfirlit: {error}", + "Failed to update webhook": "Mistókst að uppfæra vefkrók", + "Failed to upload avatar": "Mistókst að hlaða upp líkneskum", + "Failed to validate {schema}: {error}": "Mistókst að staðfesta {schema}: {error}", + "Failed to vectorize files: {error}": "Mistókst að vektóra skrár: {error}", + "Failed to warmup names cache: {error}": "Mistókst að hlýna nafnaskyndiminni: {error}", + "Federation": "Samband", + "Feedback recorded": "Gjöf skráð", + "Fewer sources (1-3):": "Færri heimildir (1-3):", + "Field": "Svæði", + "Fields to Display": "Svæði til að sýna", + "File": "Skrá", + "File #{id}": "Skrá #{id}", + "File Chunk Prediction": "Spá um skráarbút", + "File Collection": "Skráasafn", + "File Handling": "Skráameðhöndlun", + "File Location": "Staðsetning skráar", + "File Management": "Skráastjórnun", + "File Name": "Nafn skráar", + "File Path": "Slóð skráar", + "File Type Selection": "Val skráartegundar", + "File Types to Process": "Skráartegundir til að vinna", + "File Vectorization": "Skráavektórfærsla", + "File Warmup": "Hlýnun skráa", + "File actions menu": "Aðgerðavalmynd skráar", + "File anonymized successfully": "Skrá nafnlæg gerð", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Skráarbúta vektórfærsla er ekki enn innleidd. Bútarnir eru tilbúnir og geymdir, en vektórfærsluþjónustan er í þróun.", + "File collection not configured": "Skráasafn ekki stillt", + "File discovery completed": "Uppgötvun skráa lokið", + "File extraction completed": "Útdráttur skráa lokið", + "File extraction queued": "Útdráttur skráa í biðröð", + "File indexed successfully": "Skrá skráð", + "File is already anonymized": "Skrá er þegar nafnlæg", + "File name": "Nafn skráar", + "File not found": "Skrá ekki fundin", + "File settings saved successfully": "Skráastillingar vistaðar", + "File settings updated successfully": "Skráastillingar uppfærðar", + "File sources": "Skráaruppsprettur", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Skráavektórfærsla lokið! {vectorized} bútar vektóraðir úr {files} skrám. {failed} misheppnuðust.", + "File vectorization configuration saved successfully": "Stilling skráavektórfærslu vistuð", + "File vectorization started. Check the statistics section for progress.": "Skráavektórfærsla hafin. Athugaðu tölfræðihlutann fyrir framvindu.", + "File warmup completed": "Hlýnun skráa lokið", + "Files": "Skrár", + "Files added successfully": "Skrár bættar við", + "Files with Completed Extraction:": "Skrár með lokinn útdrátt:", + "Files → fileCollection, Objects → objectCollection": "Skrár → fileCollection, Hlutir → objectCollection", + "Files → filecollection, objects → objectcollection": "Skrár → filecollection, hlutir → objectcollection", + "Filter Audit Trails": "Sía endurskoðunarferlatal", + "Filter Deleted Items": "Sía eydda hluti", + "Filter Objects": "Sía hluti", + "Filter Properties": "Sía eiginleika", + "Filter Search Trails": "Sía leitarferlatal", + "Filter Statistics": "Síutölfræði", + "Filter and analyze search trail entries": "Sía og greina leitarferlatalfærslur", + "Filter and manage audit trail entries": "Sía og stjórna endurskoðunarferlatalfærslum", + "Filter and manage soft deleted items": "Sía og stjórna mjúkt eyddum hlutum", + "Filter and search entities": "Sía og leita að einingum", + "Filter by object ID": "Sía eftir hlutauðkenni", + "Filter by search term": "Sía eftir leitarhugtaki", + "Filter by type": "Sía eftir tegund", + "Filter by webhook": "Sía eftir vefkrók", + "Filter data loaded automatically. Use the filters below to refine your search.": "Síugögn hlaðin sjálfvirkt. Notaðu síarnar hér að neðan til að þrengja leitina þína.", + "Filter fields...": "Sía svæði...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Sía vefkrók kveikjur eftir farmeiginleikum (ein á línu, snið: lykill: gildi)", + "Filtered": "Síað", + "Filters": "Síar", + "Finalizing setup...": "Loka uppsetningunni...", + "Fireworks AI Chat Settings": "Fireworks AI spjallstillingar", + "Fireworks AI Embedding Configuration": "Fireworks AI fellingarstilling", + "First": "Fyrst", + "Fixed": "Lagað", + "For chat and retrieval-augmented generation": "Fyrir spjall og endurheimtuaukna myndun", + "For vector embeddings and semantic search": "Fyrir vektórfellingar og merkingarfræðilega leit", + "Format": "Snið", + "Format Issue": "Sniðvandamál", + "Format constraint is missing": "Sniðtakmörkun vantar", + "From (e.g. -7 days or 2025-01-01)": "Frá (t.d. -7 dagar eða 2025-01-01)", + "From Date": "Frá dagsetning", + "From date": "Frá dagsetning", + "Full data copied to clipboard": "Öll gögn afrituð á klippiborð", + "Function Tools": "Fallverkfæri", + "General Issue": "Almennt vandamál", + "Generate recommendations and confidence scores": "Búa til tillögur og öryggisskor", + "Generate report": "Búa til skýrslu", + "Generate the verantwoordingsdocument": "Búa til verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Búa til vektórar strax þegar nýir hlutir eru búnir til", + "Generated: {time}": "Búið til: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Fáðu rauntímaupplýsingar um gæðaheilsu gagnanna þinna með áherslu á skrár, skemaskipulag og geymslu og notkun hluta.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Fáðu rauntímaupplýsingar um gæðaheilsu gagnanna þinna, vaxtarþróun og gæðamælikvarða.", + "GitHub Personal Access Token": "GitHub persónulegt aðgangstóken", + "GitHub token is valid": "GitHub tóken er gilt", + "GitHub token is valid! Username: {username}": "GitHub tóken er gilt! Notandanafn: {username}", + "GitHub token removed successfully": "GitHub tóken fjarlægt", + "GitHub token saved successfully": "GitHub tóken vistað", + "GitHub token test failed: {message}": "GitHub tókenprófun mistókst: {message}", + "GitLab Personal Access Token": "GitLab persónulegt aðgangstóken", + "GitLab URL saved successfully": "GitLab vefslóð vistuð", + "GitLab token is valid": "GitLab tóken er gilt", + "GitLab token is valid! Username: {username}": "GitLab tóken er gilt! Notandanafn: {username}", + "GitLab token saved successfully": "GitLab tóken vistað", + "GitLab token test failed: {message}": "GitLab tókenprófun mistókst: {message}", + "Go to linked Register": "Fara í tengda skrá", + "Go to linked Schema": "Fara í tengt skema", + "Go to organisation": "Fara í stofnun", + "Grant additional permissions beyond standard CRUD operations": "Veita viðbótarleyfi umfram staðlaðar CRUD aðgerðir", + "Group Quota": "Hópakvóti", + "Groups": "Hópar", + "HTML preview": "HTML forskoðun", + "HTTP Method": "HTTP aðferð", + "HTTP method used to send webhook requests": "HTTP aðferð notuð til að senda vefkrókbeiðnir", + "Headers": "Hausar", + "Health": "Heilsa", + "Heartbeat successful - connection kept alive": "Hjartsláttur tókst - tenging viðhaldið", + "Helpful": "Gagnlegt", + "Here you can configure the details for various connections.": "Hér getur þú stillt upplýsingar fyrir ýmsar tengingar.", + "Here you can configure the details for your organisation.": "Hér getur þú stillt upplýsingar fyrir stofnunina þína.", + "Hide": "Fela", + "Hide Filters": "Fela síar", + "Hide in collection view": "Fela í safnsýn", + "Hide in forms": "Fela í eyðublöðum", + "Hide in list view": "Fela í listasýn", + "High": "Hátt", + "High Confidence": "Hátt öryggi", + "Host": "Hýsill", + "Hourly": "Á hverri klukkustund", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Hversu djúpt á að fara í hreiðraðar hlutaeiginleikar (1-20). Hærri gildi fanga meira en auka vektórstærð.", + "How often to check for updates (1-168 hours)": "Hversu oft á að athuga uppfærslur (1-168 klukkustundir)", + "How to handle retries for failed webhook deliveries": "Hvernig á að meðhöndla endurtilraunir á misheppnaðar vefkróklevranir", + "IBANs": "IBAN númer", + "ID": "Auðkenni", + "ID of input mapping (optional)": "Auðkenni inntaksvörpunar (valkvætt)", + "ID of output mapping (optional)": "Auðkenni úttaksvörpunar (valkvætt)", + "ID of the target resource": "Auðkenni markauðlindanna", + "ID:": "Auðkenni:", + "ID: {id}": "Auðkenni: {id}", + "IP rate limits cleared successfully": "IP hraðatakmarkanir hreinsaðar", + "Identify properties not in the schema": "Greina eiginleika sem eru ekki í skemanu", + "Immutable": "Óbreytanlegt", + "Import": "Flutningur inn", + "Import Configuration": "Flytja inn stillingu", + "Import Data into Register": "Flytja gögn inn í skrá", + "Import successful": "Innflutningur tókst", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Flytja inn report-bundle.json sniðmátið til að fá `reports` skrána, síðan búa til fyrsta mælaborðið þitt í venjulega hlutaviðmótinu. Mælaborð tilkynna gluggana sína í JSON og kynningarforritið gefur hverjum glugga lifandi samantekin gögn.", + "Improved Property": "Endurbættur eiginleiki", + "In use": "Í notkun", + "Inactive": "Óvirkur", + "Inactive organisations cannot be used": "Ekki er hægt að nota óvirkar stofnanir", + "Include IDs and names of related objects for better contextual search": "Taka með auðkenni og nöfn tengdra hluta fyrir betra samhengisleit", + "Include ids and names of related objects for better contextual search": "Taka með auðkenni og nöfn tengdra hluta fyrir betra samhengisleit", + "Include related object references": "Taka með tilvísanir tengdra hluta", + "Include schema and register metadata": "Taka með skema og skráarlýsigögn", + "Index": "Skrá", + "Information about the current OpenRegister installation": "Upplýsingar um núverandi OpenRegister uppsetning", + "Inherited from register": "Erft úr skrá", + "Inherits permissions from register": "Erfir leyfi úr skrá", + "Initialization failed": "Frumstilling misheppnaðist", + "Initialize Project": "Frumstilla verkefni", + "Initializing SOLR setup...": "Frumstilli SOLR uppsetningu...", + "Input Mapping": "Inntaksvörpun", + "Inspect Fields": "Skoða svæði", + "Inspect Index": "Skoða skrá", + "Inspect SOLR Index": "Skoða SOLR skrá", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Settu upp notify_push forritið úr Nextcloud forritsverslun til að virkja rauntímauppfærslur.", + "Integrations": "Samþætting", + "Internet location (URL) of this installation": "Netstaðsetning (URL) þessarar uppsetningar", + "Interval": "Bil", + "Invalid": "Ógilt", + "Invalid batch size. Must be between 1 and 5000": "Ógild lotustærð. Verður að vera á milli 1 og 5000", + "Invalid feedback type": "Ógild gjafarstegund", + "Invalid field name provided": "Ógilt svæðisnafn gefið upp", + "Invalid maxObjects. Must be 0 (all) or positive number": "Ógilt maxObjects. Verður að vera 0 (allt) eða jákvætt tala", + "Invalid:": "Ógilt:", + "Invite Users": "Bjóða notendum", + "Invited Users": "Boðnir notendur", + "Inzage (Art 15)": "Inzage (gr. 15)", + "Inzage results": "Niðurstöður inzage", + "Issue": "Vandamál", + "Items per page:": "Hlutir á síðu:", + "Just now": "Rétt í þessu", + "Key": "Lykill", + "LLM Configuration": "LLM stilling", + "LLM actions menu": "LLM aðgerðavalmynd", + "LLM configuration saved successfully": "LLM stilling vistuð", + "LLM features disabled": "LLM eiginleikar óvirkjaðir", + "LLM features enabled": "LLM eiginleikar virkjaðir", + "LLM must be enabled with an embedding provider configured": "LLM verður að vera virkjað með fellingarveitu stillt", + "LLM settings saved successfully": "LLM stillingar vistaðar", + "LLM settings updated successfully": "LLM stillingar uppfærðar", + "Label": "Merkimiði", + "Labels": "Merkimiðar", + "Languages": "Tungumál", + "Last": "Síðast", + "Last Triggered": "Síðast kveikt", + "Leave empty to allow all users access": "Skildu tómt til að leyfa öllum notendum aðgang", + "Leave off for auto-detection from property format.": "Skildu af til að greina sjálfvirkt úr sniðmáti eiginleika.", + "Left organisation successfully": "Yfirgaf stofnun", + "Legal basis *": "Lagaleg grundvöllur *", + "Length Range:": "Lengdarbil:", + "Linear": "Línulegt", + "Link": "Tengill", + "Link an email from the Mail app sidebar to associate it with this object.": "Tengdu tölvupóst úr hliðarborða Póstsforritssins til að tengja við þennan hlut.", + "Link an object to see it here.": "Tengdu hlut til að sjá hér.", + "Link another object": "Tengja annan hlut", + "Link existing event": "Tengja núverandi viðburð", + "Link removed": "Tengill fjarlægður", + "Link to Object": "Tengill við hlut", + "Link to {name}": "Tengill við {name}", + "Linked Objects": "Tengdir hlutir", + "Linked by {user}": "Tengt af {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Tengdir tölvupóstar, dagatalsviðburðir, tengiliðir og Deck kort munu birtast hér.", + "Linked to {name}": "Tengt við {name}", + "Load Advanced Filters": "Hlaða ítarlegum síum", + "Load advanced filters with live data from your search index": "Hlaða ítarlegum síum með lifandi gögnum úr leitarskránni þinni", + "Load more": "Hlaða meira", + "Load view": "Hlaða yfirliti", + "Loading API tokens...": "Hleð API tóken...", + "Loading ConfigSets...": "Hleð ConfigSets...", + "Loading LLM configuration...": "Hleð LLM stillingu...", + "Loading RBAC settings...": "Hleð RBAC stillingar...", + "Loading activity...": "Hleð starfsemi...", + "Loading advanced filters...": "Hleð ítarlegum síum...", + "Loading agents...": "Hleð umboðsmenn...", + "Loading application details...": "Hleð forritsupplýsingar...", + "Loading applications...": "Hleð forrit...", + "Loading archived conversations...": "Hleð geymd samtöl...", + "Loading audit trails...": "Hleð endurskoðunarferlatal...", + "Loading available tools...": "Hleð tiltæk verkfæri...", + "Loading cache statistics...": "Hleð skyndiminnistat...", + "Loading collections...": "Hleð söfn...", + "Loading configurations...": "Hleð stillingar...", + "Loading conversation...": "Hleð samtal...", + "Loading conversations...": "Hleð samtöl...", + "Loading deleted items...": "Hleð eydda hluti...", + "Loading endpoints": "Hleð endapunkta", + "Loading entities...": "Hleð einingar...", + "Loading events...": "Hleð viðburði...", + "Loading filter data...": "Hleð síugögn...", + "Loading groups...": "Hleð hópa...", + "Loading linked objects...": "Hleð tengda hluti...", + "Loading log details...": "Hleð skráningarupplýsingar...", + "Loading multitenancy settings...": "Hleð margleigjendastillingar...", + "Loading n8n configuration...": "Hleð n8n stillingu...", + "Loading permission matrix...": "Hleð leyfismatrix...", + "Loading preferences...": "Hleð kjörstillingar...", + "Loading register data...": "Hleð skrárgögn...", + "Loading registers...": "Hleð skrár...", + "Loading retention settings...": "Hleð varðveislustillingar...", + "Loading schemas...": "Hleð skemu...", + "Loading search configuration...": "Hleð leitarstillingu...", + "Loading search trails...": "Hleð leitarferlatal...", + "Loading sources...": "Hleð uppsprettur...", + "Loading statistics...": "Hleð tölfræði...", + "Loading text extraction settings...": "Hleð textaútdráttarstillingar...", + "Loading tokens...": "Hleð tóken...", + "Loading user groups...": "Hleð notandahópa...", + "Loading users...": "Hleð notendur...", + "Loading views...": "Hleð yfirlit...", + "Loading...": "Hleð...", + "Local": "Staðbundið", + "Local Version": "Staðbundin útgáfa", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Finna hvern hlut sem vísar í gagnaeiganda (gr. 15 inzage), forskoða eyðingu (gr. 17 vergetelheid) eða flytja út gögn þeirra (gr. 20 portabiliteit).", + "Location": "Staðsetning", + "Location Field": "Staðsetningarsvæði", + "Locations": "Staðsetningar", + "Locked": "Læst", + "Locked:": "Læst:", + "Login successful": "Innskráning tókst", + "Logs": "Skráningar", + "Low": "Lágt", + "Low Confidence": "Lágt öryggi", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Lægri tölur birtast fyrst í síuhliðarborðanum. Skildu tómt fyrir sjálfvirka röðun.", + "Lucene Version:": "Lucene útgáfa:", + "Magic table sync completed for {schema}": "Samstilling töfratöflu lokið fyrir {schema}", + "Magic table synchronized successfully": "Töfratafla samstillt", + "Mail Integration": "Póstssamþætting", + "Mail integration is not available": "Póstssamþætting er ekki tiltæk", + "Make this installation discoverable within the federation network.": "Gera þessa uppsetning uppgötvanlega innan sambandsnetstins.", + "Manage Organisation Roles": "Stjórna hlutverkum stofnunar", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Stjórna SOLR söfnum (gagnageymslum) og úthluta þeim fyrir hluti og skrár.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Stjórna SOLR ConfigSets (stillingarsniðmátum) fyrir söfn þín.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Stjórna SOLR söfnum (gagnageymslum) og úthluta þeim fyrir hluti og skrár.", + "Manage SOLR configsets (configuration templates) for your collections.": "Stjórna SOLR configsets (stillingarsniðmátum) fyrir söfn þín.", + "Manage and configure agents for automated tasks": "Stjórna og stilla umboðsmenn fyrir sjálfvirk verkefni", + "Manage and monitor file text extraction status": "Stjórna og fylgjast með stöðu textaútdráttar skráa", + "Manage and restore soft deleted items from your registers": "Stjórna og endurheimta mjúkt eydda hluti úr skránum þínum", + "Manage and view detected entities from files and objects": "Stjórna og skoða greindar einingar úr skrám og hlutum", + "Manage document templates and themes": "Stjórna skjalasniðmátum og þemum", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Stjórna n8n vinnuflæðum fyrir OpenRegister sjálfvirkni. Vinnuflæðin verða geymd í stillta verkefninu.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Stjórna vinnslustarfsemi, keyra beiðnir um aðgang gagnaeigenda og endurskoða fylgni samkvæmt ESB GDPR / hollenskri AVG.", + "Manage webhooks for event-driven integrations": "Stjórna vefkrókum fyrir viðburðadrifnar samþættingar", + "Manage your account settings, security, and personal data.": "Stjórna reikningsstillingum þínum, öryggi og persónulegum gögnum.", + "Manage your applications and modules": "Stjórna forritunum þínum og einingum", + "Manage your chat conversations": "Stjórna spjallsamtölum þínum", + "Manage your data registers and their configurations": "Stjórna gagnaskránum þínum og stillingum þeirra", + "Manage your data schemas and their properties": "Stjórna gagnaskemum þínum og eiginleikum þeirra", + "Manage your data sources and their configurations": "Stjórna gagnauppsprettum þínum og stillingum þeirra", + "Manage your organisations and switch between them": "Stjórna stofnununum þínum og skipta á milli", + "Manage your saved search configurations": "Stjórna vistaðum leitarstillingum þínum", + "Manage your system configurations and settings": "Stjórna kerfissstillingum þínum og stillingum", + "Managed": "Stjórnað", + "Mappings": "Varpanir", + "Mass validation completed successfully": "Magnstaðfesting lokið", + "Mass validation failed: {error}": "Magnstaðfesting misheppnaðist: {error}", + "Max Files (0 = all)": "Hámark skrár (0 = allar)", + "Max Length": "Hámarkslengd", + "Max Length:": "Hámarkslengd:", + "Max Objects (0 = all)": "Hámark hlutir (0 = allir)", + "Max Retries": "Hámark endurtilraunir", + "Max Shards/Node": "Hámark brot/hnútur", + "Max Tokens": "Hámark tóken", + "Max execution time (ms)": "Hámarks framkvæmdatími (ms)", + "Max ms": "Hámarks ms", + "Max result count": "Hámarks niðurstöðufjöldi", + "Max results": "Hámarks niðurstöður", + "Maximum": "Hámark", + "Maximum File Size (MB)": "Hámarksskrárstærð (MB)", + "Maximum Nesting Depth": "Hámarks hreiðrunardýpt", + "Maximum length": "Hámarkslengd", + "Maximum length constraint is missing": "Hámarkslengdartakmörkun vantar", + "Maximum length is too restrictive": "Hámarkslengd er of þröng", + "Maximum number of files to process. Set to 0 to process all files.": "Hámarksfjöldi skráa til að vinna. Stilltu á 0 til að vinna allar skrár.", + "Maximum number of items": "Hámarksfjöldi hluta", + "Maximum number of objects to process. Set to 0 to process all objects.": "Hámarksfjöldi hluta til að vinna. Stilltu á 0 til að vinna alla hluti.", + "Maximum number of retry attempts for failed deliveries": "Hámarksfjöldi endurtilrauna fyrir misheppnaðar levranir", + "Maximum time to wait before committing changes": "Hámarkstími til að bíða áður en breytingar eru staðfestar", + "Maximum tokens to generate": "Hámarks tóken til að búa til", + "Maximum value": "Hámarksgildi", + "Maximum value constraint is missing": "Hámarksgildistökmörkun vantar", + "Maximum value is too restrictive": "Hámarksgildi er of þröngt", + "Medium": "Miðlungs", + "Medium Confidence": "Miðlungs öryggi", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Meðallegar fyrirspurnir: Leitir með nokkurri síun eða mörgum færibreytum (t.d. dagsetningarbil, sérstakar skrár/skemu)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Meðallegar fyrirspurnir: leitir með nokkurri síun eða mörgum færibreytum (t.d. dagsetningarbil, sérstakar skrár/skemu)", + "Member removed successfully": "Meðlimur fjarlægður", + "Members": "Meðlimir", + "Members ({count})": "Meðlimir ({count})", + "Members of selected groups can access this view": "Meðlimir valinna hópa geta fengið aðgang að þessu yfirliti", + "Members:": "Meðlimir:", + "Memory prediction calculated successfully": "Minnisspá reiknuð", + "Message content is required": "Innihald skilaboða er nauðsynlegt", + "Message does not belong to this conversation": "Skilaboð tilheyra ekki þessu samtali", + "Message not found": "Skilaboð ekki fundin", + "Metadata": "Lýsigögn", + "Metadata Filters": "Lýsigagnasíar", + "Method": "Aðferð", + "Method*": "Aðferð*", + "Method: {method}": "Aðferð: {method}", + "Min Length": "Lágmarkslengd", + "Min execution time (ms)": "Lágmarks framkvæmdatími (ms)", + "Min ms": "Lágmarks ms", + "Min result count": "Lágmarks niðurstöðufjöldi", + "Min results": "Lágmarks niðurstöður", + "Minimum": "Lágmark", + "Minimum length": "Lágmarkslengd", + "Minimum number of items": "Lágmarksfjöldi hluta", + "Minimum value": "Lágmarksgildi", + "Minimum value constraint is missing": "Lágmarksgildistökmörkun vantar", + "Minimum value is too restrictive": "Lágmarksgildi er of þröngt", + "Missing conversation or agentUuid": "Samtal eða agentUuid vantar", + "Missing conversationId": "conversationId vantar", + "Missing message": "Skilaboð vantar", + "Missing object fields created successfully": "Vantandi hlutasvæði búin til", + "Mode:": "Háttur:", + "Model": "Líkan", + "Model Name": "Nafn líkans", + "Modified": "Breytt", + "Monthly": "Mánaðarlega", + "More sources (10-20):": "Fleiri heimildir (10-20):", + "Most Active Objects": "Virkustu hlutir", + "Multiple Of": "Margfeldi af", + "Multiple of": "Margfeldi af", + "Multiple search trail deletion not implemented yet": "Margar leitarferlataleyðingar eru ekki enn innleiddar", + "Multitenancy": "Margleigjendur", + "Multitenancy settings updated successfully": "Margleigjendastillingar uppfærðar", + "My Account": "Reikningur minn", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Nafn", + "Name *": "Nafn *", + "Name*": "Nafn*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Nafnaskyndiminni hlýnt: {count} nöfn hlaðin á {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Nafnaskyndiminni hlýnt: {count} nöfn hlaðin á {time}. {message}", + "Names cache warmup completed": "Hlýnun nafnaskyndiminnis lokið", + "Naming pattern for copies:": "Nafnamynstur fyrir afrit:", + "New Collection Name": "Nýtt nafn safns", + "New Configuration": "Ný stilling", + "New Conversation": "Nýtt samtal", + "New Properties": "Nýir eiginleikar", + "New Property": "Nýr eiginleiki", + "New User Group": "Nýr notandahópur", + "New Value": "Nýtt gildi", + "New activity": "Ný starfsemi", + "New password": "Nýtt lykilorð", + "New processing activity": "Ný vinnslustarfsemi", + "New users without specific organisation membership will be automatically added to this organisation": "Nýir notendur án sérstaks stofnanameðlimsskaps verða sjálfvirkt bætt við þessa stofnun", + "New verwerkingsactiviteit": "Ný verwerkingsactiviteit", + "Next": "Næst", + "Nextcloud Groups": "Nextcloud hópar", + "No": "Nei", + "No API tokens.": "Engin API tóken.", + "No ConfigSets found": "Engin ConfigSets fundin", + "No Deck cards linked to this object": "Engin Deck kort tengd þessum hlut", + "No GitHub token configured": "Ekkert GitHub tóken stillt", + "No GitHub token provided": "Ekkert GitHub tóken gefið upp", + "No GitLab token provided": "Ekkert GitLab tóken gefið upp", + "No Organisation": "Engin stofnun", + "No active filters": "Engir virkir síar", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Engir virkir hlutir til mjúkeyðingar fyrir skema {schema}. Notaðu \"Eyða varanlega\" til að fjarlægja mjúkt eydda hluti.", + "No activity data available": "Engin starfsemigögn tiltæk", + "No activity found.": "Engin starfsemi fundin.", + "No agents available": "Engir umboðsmenn tiltækir", + "No agents found": "Engir umboðsmenn fundnir", + "No app store cache exists yet": "Engin forritsverslunarskyndiminni til enn", + "No applications are available.": "Engin forrit eru tiltæk.", + "No applications found": "Engin forrit fundin", + "No approval steps for this object.": "Engin samþykktarskref fyrir þennan hlut.", + "No archived conversations": "Engin geymd samtöl", + "No audit trail entries found": "Engar endurskoðunarferslur fundnar", + "No changes recorded": "Engar breytingar skráðar", + "No chunks to vectorize": "Engir bútar til að vektóra", + "No collections found": "Engin söfn fundin", + "No configuration data": "Engin stillinggögn", + "No configuration selected": "Engin stilling valin", + "No configurations are available.": "Engar stillingar eru tiltækar.", + "No configurations found": "Engar stillingar fundnar", + "No configurations found for this application.": "Engar stillingar fundnar fyrir þetta forrit.", + "No contacts linked to this object": "Engir tengiliðir tengdir þessum hlut", + "No conversations yet. Create a new one to get started!": "Engin samtöl enn. Búðu til nýtt til að byrja!", + "No dashboards yet": "Engin mælaborð enn", + "No data available": "Engin gögn tiltæk", + "No data available for chart": "Engin gögn tiltæk fyrir graf", + "No deleted items found": "Engir eyddir hlutir fundnir", + "No deletion data available": "Engin eyðingargögn tiltæk", + "No description found": "Engin lýsing fundin", + "No description provided": "Engin lýsing gefin upp", + "No emails linked to this object": "Engir tölvupóstar tengdir þessum hlut", + "No endpoints defined": "Engir endapunktar skilgreindir", + "No entities detected for this email.": "Engar einingar greindar fyrir þennan tölvupóst.", + "No entities detected in this file. Run text extraction first.": "Engar einingar greindar í þessari skrá. Keyrðu textaútdrátt fyrst.", + "No entities found": "Engar einingar fundnar", + "No entities have been detected yet": "Engar einingar hafa verið greindar enn", + "No events found": "Engir viðburðir fundnir", + "No events linked to this object": "Engir viðburðir tengdir þessum hlut", + "No expired audit trails found to clear": "Engin útrunnin endurskoðunarferlatal fundin til að hreinsa", + "No expired search trails found to clear": "Engin útrunnin leitarferlatal fundin til að hreinsa", + "No extraction data available for this file": "Engin útdráttursgögn tiltæk fyrir þessa skrá", + "No facetable fields available. Select a register and schema to see available filters.": "Engin hliðarhæf svæði tiltæk. Veldu skrá og skema til að sjá tiltækar síar.", + "No files found": "Engar skrár fundnar", + "No files have been extracted yet": "Engar skrár hafa verið dregnar út enn", + "No files selected": "Engar skrár valdar", + "No files to process": "Engar skrár til að vinna", + "No files to reindex": "Engar skrár til að endurskrá", + "No filters are currently active. This will delete ALL audit trail entries!": "Engir síar eru virkir núna. Þetta mun eyða ÖLLUM endurskoðunarferslum!", + "No groups found. Try a different search.": "Engir hópar fundnir. Reyndu aðra leit.", + "No label": "Enginn merkimiði", + "No labels": "Engir merkimiðar", + "No linked objects": "Engir tengdir hlutir", + "No log entries found": "Engar skráningarfærslur fundnar", + "No logs are available for this configuration.": "Engar skráningar tiltækar fyrir þessa stillingu.", + "No logs are available for this source.": "Engar skráningar tiltækar fyrir þessa uppsprettu.", + "No logs found": "Engar skráningar fundnar", + "No matches": "Engar samsvörunur", + "No mismatched fields found - SOLR schema is properly configured": "Engin ósamkvæm svæði fundin - SOLR skema er rétt stillt", + "No objects are currently selected for deletion.": "Engir hlutir eru valdir til eyðingar núna.", + "No objects are currently selected for permanent deletion.": "Engir hlutir eru valdir til varanlegrar eyðingar núna.", + "No objects are currently selected for restoration.": "Engir hlutir eru valdir til endurheimtu núna.", + "No objects found": "Engir hlutir fundnir", + "No objects found to vectorize": "Engir hlutir fundnir til að vektóra", + "No objects linked to this email": "Engir hlutir tengdir þessum tölvupósti", + "No objects selected": "Engir hlutir valdir", + "No objects selected for deletion": "Engir hlutir valdir til eyðingar", + "No objects selected for restoration": "Engir hlutir valdir til endurheimtu", + "No objects to delete": "Engir hlutir til að eyða", + "No objects to delete for schema {schema}": "Engir hlutir til að eyða fyrir skema {schema}", + "No objects were permanently deleted": "Engir hlutir voru varanlega eyddir", + "No organisations found": "Engar stofnanir fundnar", + "No personal data was found for this subject identifier.": "Engin persónuleg gögn fundust fyrir þetta efnisauðkenni.", + "No processing activities yet": "Engin vinnslustarfsemi enn", + "No properties found": "Engir eiginleikar fundnir", + "No properties match your filters.": "Engir eiginleikar passa við síarnar þínar.", + "No purge date set": "Engin hreinsunardag stillt", + "No register data available": "Engin skrárgögn tiltæk", + "No register objects reference this file": "Engin skrárhlutar vísa í þessa skrá", + "No registers found": "Engar skrár fundnar", + "No registers found for this application.": "Engar skrár fundnar fyrir þetta forrit.", + "No registers found. Create a register to configure permissions.": "Engar skrár fundnar. Búðu til skrá til að stilla leyfi.", + "No related cases found for this sender": "Engin tengd mál fundin fyrir þennan sendanda", + "No related entities": "Engar tengdar einingar", + "No relations found": "Engar tengsl fundin", + "No request body available": "Engin beiðnihluti tiltæk", + "No response body available": "Engin svarhluti tiltæk", + "No saved views yet. Create one in the Search tab!": "Engin vistuð yfirlit enn. Búðu til eitt í Leitarflipanum!", + "No saved views yet. create one in the search tab!": "Engin vistuð yfirlit enn. Búðu til eitt í leitarflipanum!", + "No scan run yet": "Engin skoðunarkeyrsla enn", + "No schema data available": "Engin skemugögn tiltæk", + "No schema selected for exploration": "Ekkert skema valið til skoðunar", + "No schemas configured for mail linking.": "Engin skemu stillt fyrir póstsytengingu.", + "No schemas found": "Engin skemu fundin", + "No schemas found for this application.": "Engin skemu fundin fyrir þetta forrit.", + "No search terms data available": "Engin leitarhugtakagögn tiltæk", + "No search trail entries found": "Engar leitarferslur fundnar", + "No sources are available.": "Engar uppsprettur eru tiltækar.", + "No sources found": "Engar uppsprettur fundnar", + "No synchronizations found": "Engar samstillingar fundnar", + "No templates found": "Engin sniðmát fundin", + "No templates have been created yet": "Engin sniðmát hafa verið búin til enn", + "No tools available. Tools can be registered by installed apps.": "Engin verkfæri tiltæk. Verkfæri geta verið skráð af uppsetttum forritum.", + "No users in this organisation.": "Engir notendur í þessari stofnun.", + "No valid magic-mapped register+schema combinations found": "Engar gildar töfravarpaðar skrá+skemu samsetningar fundnar", + "No views found. Create views first before configuring vectorization.": "Engin yfirlit fundin. Búðu til yfirlit fyrst áður en vektórfærsla er stillt.", + "No views match your search": "Engin yfirlit passa við leitina þína", + "No webhooks found": "Engir vefkrókar fundnir", + "No webhooks have been configured yet": "Engir vefkrókar hafa verið stilltir enn", + "No workflows found in this project. Create workflows in the n8n editor.": "Engin vinnuflæði fundin í þessu verkefni. Búðu til vinnuflæðin í n8n ritstjóranum.", + "None": "Ekkert", + "Not Found": "Ekki fundið", + "Not extracted": "Ekki dregið út", + "Not helpful": "Ekki gagnlegt", + "Not in use": "Ekki í notkun", + "Note:": "Athugasemd:", + "Notifications": "Tilkynningar", + "Number Constraints": "Tölutakmörkunar", + "Number of Sources": "Fjöldi heimilda", + "Number of chunks to vectorize in one API call (1-100).": "Fjöldi búta til að vektóra í einu API kalli (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Fjöldi búta til að vektóra í einu API kalli. Hærra = hraðara en meira minni. Mælt: 10-50.", + "Number of objects to process in each batch (1-5000).": "Fjöldi hluta til að vinna í hverri lotu (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Fjöldi hluta til að vektóra í einu API kalli. Hærra = hraðara en meira minni. Mælt: 10-50.", + "Numeric Range:": "Tölulegt bil:", + "Object": "Hlutur", + "Object #{id}": "Hlutur #{id}", + "Object Collection": "Hlutasafn", + "Object Configuration:": "Hlutastilling:", + "Object Count Prediction": "Spá um hlutafjölda", + "Object Extraction Mode": "Hlutaútdráttarhamur", + "Object ID": "Hlutauðkenni", + "Object Management": "Hlutastjórnun", + "Object Text Extraction": "Textaútdráttur úr hlutum", + "Object Vectorization": "Hlutvektórfærsla", + "Object Warmup": "Hlýnun hluta", + "Object changes in owned objects": "Hlutabreytingar í eigu hlutum", + "Object collection not configured": "Hlutasafn ekki stillt", + "Object metadata incomplete for file upload": "Hlutarlýsigögn ófullnægjandi fyrir skráaupphleðslu", + "Object not found": "Hlutur ekki fundinn", + "Object permanently deleted": "Hlut varanlega eytt", + "Object restored successfully": "Hlutur endurheimt", + "Object settings updated successfully": "Hlutastillingar uppfærðar", + "Object sources": "Hlutaruppsprettur", + "Object successfully deleted": "Hlut eytt", + "Object successfully modified": "Hlut breytt", + "Object unlocked successfully": "Hlut opnað", + "Object vectorization configuration saved successfully": "Hlutvektórfærslustilling vistuð", + "Object vectorized successfully": "Hlutur vektóraður", + "Object:": "Hlutur:", + "Objects": "Hlutir", + "Objects Analyzed": "Hlutir greindir", + "Objects Distribution": "Dreifing hluta", + "Objects being analyzed": "Hlutir eru greindir", + "Objects by Register": "Hlutir eftir skrá", + "Objects by Schema": "Hlutir eftir skema", + "Objects deletion completed for {schema}": "Eyðing hluta lokið fyrir {schema}", + "Objects deletion completed successfully": "Eyðing hluta lokið", + "Objects in schema": "Hlutir í skema", + "Objects successfully deleted": "Hlutum eytt", + "Objects to Process:": "Hlutir til að vinna:", + "Objects to be analyzed": "Hlutir til að greina", + "Objects to be deleted": "Hlutir til að eyða", + "Objects to be validated": "Hlutir til að staðfesta", + "Objects will be serialized as JSON text before vectorization": "Hlutir verða raðaðar sem JSON texti fyrir vektórfærslu", + "Objects will be soft deleted and moved to the": "Hlutir verða mjúkt eyddir og fluttir í", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Hlutir verða mjúkt eyddir (merktir sem eyddir en geymdir í gagnagrunni). Hægt er að endurheimta þá síðar ef þörf krefur.", + "Old Value": "Gamalt gildi", + "Oldest Item (days)": "Elsti hlutur (dagar)", + "Ollama Chat Settings": "Ollama spjallstillingar", + "Ollama Embedding Configuration": "Ollama fellingarstilling", + "Ollama URL": "Ollama vefslóð", + "Only With Changes": "Aðeins með breytingar", + "Only members of selected groups can access this organisation": "Aðeins meðlimir valinna hópa geta fengið aðgang að þessari stofnun", + "Open Nextcloud App Store": "Opna Nextcloud forritsverslun", + "Open OpenRegister sidebar": "Opna OpenRegister hliðarborða", + "Open Register Objects": "Opna skrárhluti", + "Open folder": "Opna möppu", + "Open in OpenRegister": "Opna í OpenRegister", + "Open n8n Editor": "Opna n8n ritstjóra", + "OpenAI Chat Settings": "OpenAI spjallstillingar", + "OpenAI Embedding Configuration": "OpenAI fellingarstilling", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister stillingar", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Rekstrarskilgreindar mælaborð og tímasettar skýrslur. Hvert mælaborð er fyrsta flokks hlutur í `reports` skránni; gluggarnir eru tilkynntir í `widgets` fylki mælaborðsins og kynntir lifandi úr samantektum / GraphQL.", + "Optimizing search performance...": "Besta leitargæðar...", + "Optional URL-friendly identifier": "Valkvætt URL-vinveitilegt auðkenni", + "Optional description for this facet": "Valkvæð lýsing fyrir þessa hlið", + "Optional end date property": "Valkvæður eiginleiki lokadagsetningar", + "Optional event description template": "Valkvætt sniðmát viðburðarlýsingar", + "Optional location property": "Valkvæður staðsetningareiginleiki", + "Optional webhook secret for signature verification": "Valkvætt vefkrókleynd fyrir undirskriftarstaðfestingu", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Valkvætt: Bættu við fyrirspurnafæribreytum til að sía tilvísaða skemað (t.d. status=active&type=public)", + "Or": "Eða", + "Order": "Röð", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Raðaðar BCP 47 tungumálataggar. Fyrsta tungumálið er sjálfgefið í skránni og knýr Accept-Language varafall fyrir þýðanlega eiginleika.", + "Organisation": "Stofnun", + "Organisation Configuration": "Stofnunastilling", + "Organisation ID": "Stofnunarauðkenni", + "Organisation Members": "Meðlimir stofnunar", + "Organisation Statistics": "Tölfræði stofnunar", + "Organisation created successfully": "Stofnun búin til", + "Organisation membership changes": "Breytingar á meðlimsskapi stofnunar", + "Organisation name is required": "Nafn stofnunar er nauðsynlegt", + "Organisation settings saved successfully": "Stillingar stofnunar vistaðar", + "Organisation successfully created": "Stofnun búin til", + "Organisation successfully updated": "Stofnun uppfærð", + "Organisational measures": "Skipulagslegar ráðstafanir", + "Organisations": "Stofnanir", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Stofnun", + "Organization ID (Optional)": "Stofnunarauðkenni (valkvætt)", + "Organizations": "Stofnanir", + "Orphaned Items": "Foreldralausir hlutir", + "Other": "Annað", + "Output Mapping": "Úttaksvörpun", + "Overview": "Yfirlit", + "Owner": "Eigandi", + "Owner:": "Eigandi:", + "PDF": "PDF", + "PII hits": "PII smellir", + "PII rows": "PII raðir", + "Page {current} of {total}": "Síða {current} af {total}", + "Parallel Mode (Faster, more resource intensive)": "Samhliða hamur (Hraðar, meira auðlindafrekt)", + "Parallel:": "Samhliða:", + "Password": "Lykilorð", + "Password changes are not supported by your authentication provider.": "Auðkenningarveitan þín styður ekki breytingar á lykilorði.", + "Password for SOLR authentication (optional)": "Lykilorð fyrir SOLR auðkenningu (valkvætt)", + "Password updated successfully": "Lykilorð uppfært", + "Path": "Slóð", + "Path in repository": "Slóð í geymslu", + "Path where the configuration file will be saved in the repository": "Slóð þar sem stillingarskrá verður vistuð í geymslunni", + "Path where the register OAS file will be saved in the repository": "Slóð þar sem skrár OAS skrá verður vistuð í geymslunni", + "Pattern (regex)": "Mynstur (regex)", + "Pattern Issue": "Mynsturnvandamál", + "Pattern constraint is missing": "Mynsturtakmörkun vantar", + "Patterns:": "Mynstur:", + "Pending": "Í bið", + "Permanent Delete Mode": "Varanlegur eyðingarhamur", + "Permanently Delete": "Eyða varanlega", + "Permanently Delete ({count})": "Eyða varanlega ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Eyða varanlega öllum {count} mjúkt eyddum hlutum. Þetta er ekki hægt að afturkalla!", + "Permanently delete already soft-deleted objects": "Eyða varanlega þegar mjúkt eyddum hlutum", + "Permanently delete this conversation?": "Eyða þessu samtali varanlega?", + "Permission Matrix": "Leyfismatrix", + "Person": "Einstaklingur", + "Personal Data": "Persónuleg gögn", + "Personal Data Export": "Útflutningur persónulegra gagna", + "Persons": "Einstaklingar", + "Phone": "Sími", + "Phone Numbers": "Símanúmer", + "Photos": "Myndir", + "Please create an agent in the": "Vinsamlegast búðu til umboðsmann í", + "Please select a user": "Vinsamlegast veldu notanda", + "Please select an agent to continue": "Vinsamlegast veldu umboðsmann til að halda áfram", + "Please select an organisation": "Vinsamlegast veldu stofnun", + "Please select at least one item to import": "Vinsamlegast veldu að minnsta kosti einn hlut til að flytja inn", + "Please select registers or schemas first": "Vinsamlegast veldu skrár eða skemu fyrst", + "Please select which register and schema to use for the new object": "Vinsamlegast veldu hvaða skrá og skema á að nota fyrir nýja hlutinn", + "Please try again later.": "Vinsamlegast reyndu aftur síðar.", + "Please wait while we fetch your agents.": "Vinsamlegast bíddu meðan við sækjum umboðsmenn þína.", + "Please wait while we fetch your applications.": "Vinsamlegast bíddu meðan við sækjum forritin þín.", + "Please wait while we fetch your configurations.": "Vinsamlegast bíddu meðan við sækjum stillingar þínar.", + "Please wait while we fetch your deleted items.": "Vinsamlegast bíddu meðan við sækjum eyddar hlutir þínar.", + "Please wait while we fetch your sources.": "Vinsamlegast bíddu meðan við sækjum uppsprettur þínar.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Vinsamlegast bíddu meðan við eyðum SOLR safninu varanlega. Þetta gæti tekið nokkrar mínútur.", + "Polls": "Könnungar", + "Popular Search Terms": "Vinsæl leitarhugtök", + "Port": "Gátt", + "Portabiliteit (Art 20)": "Portabiliteit (gr. 20)", + "Preferences saved": "Kjörstillingar vistaðar", + "Prerequisites": "Forskilyrði", + "Press Enter to send, Shift+Enter for new line": "Ýttu á Enter til að senda, Shift+Enter fyrir nýja línu", + "Press enter to send, shift+enter for new line": "Ýttu á enter til að senda, shift+enter fyrir nýja línu", + "Prettify": "Fegra", + "Preview erasure": "Forskoða eyðingu", + "Preview:": "Forskoðun:", + "Previous": "Fyrri", + "Private": "Einkamál", + "Private Agent (Default)": "Einkakalegur umboðsmaður (sjálfgefinn)", + "Private agents": "Einkamál umboðsmenn", + "Process Name (optional)": "Heiti ferlis (valkvætt)", + "Processed {indexed} files, {failed} failed": "Unnin {indexed} skrár, {failed} misheppnuðust", + "Processes chunks in batches with simulated parallelism.": "Vinnur búta í lotum með hermdri samhliðavinnslu.", + "Processes file chunks sequentially (safest).": "Vinnur skráarbúta í röð (öruggast).", + "Processes objects in chunks with simulated parallelism.": "Vinnur hluti í búsum með hermdri samhliðavinnslu.", + "Processes objects sequentially (safest).": "Vinnur hluti í röð (öruggast).", + "Processing": "Vinnsla", + "Processing Limits": "Vinnslutak", + "Processing...": "Vinnur...", + "Project initialized successfully": "Verkefni frumstillt", + "Project not found. Please initialize first.": "Verkefni ekki fundið. Vinsamlegast frumstilltu fyrst.", + "Properties": "Eiginleikar", + "Property Behaviors": "Eiginleikahegðun", + "Property Configuration:": "Eiginleikastilling:", + "Property Filters": "Eiginleikasíar", + "Property Title": "Titill eiginleika", + "Property Type": "Tegund eiginleika", + "Property can be improved": "Eiginleika er hægt að bæta", + "Property could not be added": "Eiginleika var ekki hægt að bæta við", + "Property could not be updated": "Eiginleika var ekki hægt að uppfæra", + "Property name of inversed relation": "Eiginleikaheiti andstæðrar tengsl", + "Property successfully added": "Eiginleiki bætt við", + "Property successfully updated": "Eiginleiki uppfærður", + "Protected": "Varið", + "Provider": "Veita", + "Provider is required for testing": "Veita er nauðsynleg til prófunar", + "Public": "Almenningur", + "Public View": "Opinbert yfirlit", + "Public views can be accessed by anyone in the system": "Opinber yfirlit er hægt að fá aðgang að af hverjum sem er í kerfinu", + "Publish": "Gefa út", + "Publish Configuration to GitHub": "Birta stillingu á GitHub", + "Publish OAS": "Gefa út OAS", + "Publish Objects": "Gefa út hluti", + "Publish Register OAS to GitHub": "Birta skrár OAS á GitHub", + "Publish all objects in this schema": "Gefa út alla hluti í þessu skema", + "Publish objects to make them publicly available": "Gefa út hluti til að gera þá aðgengilega almenningi", + "Published": "Birt", + "Published:": "Birt:", + "Publishing...": "Gefur út...", + "Purge": "Hreinsa", + "Purge Date": "Hreinsunardagur", + "Purge {count} object from database": "Hreinsa {count} hlut úr gagnagrunni", + "Purge {count} objects from database": "Hreinsa {count} hluti úr gagnagrunni", + "Purpose limitation *": "Takmarkanir á tilgangi *", + "Push Notifications": "Ýtitilkynningar", + "Query Complexity Distribution": "Dreifing flækni fyrirspurnar", + "Query Help": "Hjálp við fyrirspurn", + "Query parameter is required": "Fyrirspurnafæribreyta er nauðsynleg", + "Quota": "Kvóti", + "RAG": "RAG", + "RAG Configuration": "RAG stilling", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC stillingar uppfærðar", + "Rate limits cleared successfully": "Hraðatakmarkanir hreinsaðar", + "Raw Changes Data": "Hrá breytingargögn", + "Re-vectorize on object update": "Endur-vektóra við hlutauppfærslu", + "Read": "Lesa", + "Real-time push notification status via notify_push": "Stav rauntíma ýtitilkynningar í gegnum notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Rauntíma ýtitilkynningar eru virkar. Tengdir viðskiptavinir fá tafarlausar uppfærslur þegar hlutir eru búnir til, uppfærðir eða eyddir.", + "Realtime push active": "Rauntíma ýti virkt", + "Realtime push not available — the notify_push app is not installed": "Rauntíma ýti ekki tiltækt — notify_push forritið er ekki uppsett", + "Reason": "Ástæða", + "Reason (optional)": "Ástæða (valkvætt)", + "Rebase operation completed successfully": "Rebase aðgerð lokið", + "Rebase operation failed: {error}": "Rebase aðgerð misheppnaðist: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Tillögur:", + "Recommended Type:": "Mælt með tegund:", + "Recommended: 5 sources": "Mælt með: 5 heimildir", + "Refresh": "Uppfæra", + "Refresh Data": "Uppfæra gögn", + "Refresh Stats": "Uppfæra tölfræði", + "Refresh Workflows": "Uppfæra vinnuflæði", + "Refresh dashboard": "Uppfæra mælaborð", + "Refresh database info": "Uppfæra gagnagrunnupplýsingar", + "Register": "Skrá", + "Register #{id}": "Skrá #{id}", + "Register / Schema": "Skrá / Skema", + "Register ID": "Skrárauðkenni", + "Register Objects": "Skrárhlutir", + "Register Overview": "Yfirlit skráar", + "Register Statistics": "Tölfræði skráar", + "Register Totals": "Heildarfjöldi skráar", + "Register and schema are required": "Skrá og skema eru nauðsynleg", + "Register annotation": "Skrárkómerki", + "Register depublished successfully": "Skrá afbirt", + "Register not found": "Skrá ekki fundin", + "Register objects": "Skrárhlutir", + "Register objects deletion completed successfully": "Eyðing skrárhluta lokið", + "Register published successfully": "Skrá birt", + "Register/Schema Usage": "Notkun skráar/skema", + "Register:": "Skrá:", + "Registers": "Skrár", + "Reindex": "Endurskrá", + "Reindex completed": "Endurskráning lokið", + "Reindex completed successfully": "Endurskráning lokið", + "Reindex completed! Processed {count} objects in {duration}s": "Endurskráning lokið! Unnin {count} hlutir á {duration}s", + "Reindex failed": "Endurskráning misheppnaðist", + "Reindex failed: %s": "Endurskráning misheppnaðist: %s", + "Reject": "Hafna", + "Rejected": "Hafnað", + "Related Cases": "Tengd mál", + "Relations": "Tengsl", + "Remote Version": "Fjarútgáfa", + "Remove": "Fjarlægja", + "Remove Deck card": "Fjarlægja Deck kort", + "Remove avatar": "Fjarlægja líkneskar", + "Remove contact": "Fjarlægja tengilið", + "Remove filter": "Fjarlægja sía", + "Remove from favorites": "Fjarlægja úr eftirlæti", + "Remove from list": "Fjarlægja úr lista", + "Remove group": "Fjarlægja hóp", + "Remove link": "Fjarlægja tengil", + "Remove link to {name}": "Fjarlægja tengil við {name}", + "Remove link to {name}?": "Fjarlægja tengil við {name}?", + "Remove link to {title}": "Fjarlægja tengil við {title}", + "Remove oneOf entry": "Fjarlægja oneOf færslu", + "Remove property after write-back": "Fjarlægja eiginleika eftir endurskrift", + "Remove {title}": "Fjarlægja {title}", + "Remove {userId} from this organisation?": "Fjarlægja {userId} úr þessari stofnun?", + "Removed": "Fjarlægt", + "Removed from favorites": "Fjarlægt úr eftirlæti", + "Removing schema {schema}...": "Fjarlægi skema {schema}...", + "Rename Conversation": "Endurnefna samtal", + "Rename conversation": "Endurnefna samtal", + "Replicas": "Eintök", + "Reports": "Skýrslur", + "Repository": "Geymsla", + "Request Body": "Beiðnihluti", + "Request Data": "Beiðnigögn", + "Request Details": "Upplýsingar um beiðni", + "Request Quota (per day)": "Beiðnakvóti (á dag)", + "Request account deactivation": "Biðja um óvirkjun reiknings", + "Request account deactivation. This will notify administrators for review.": "Biðja um óvirkjun reiknings. Þetta mun tilkynna stjórnendum til yfirferðar.", + "Request body must be JSON with a \"query\" field": "Beiðnihluti verður að vera JSON með \"query\" svæði", + "Request timeout in seconds": "Beiðniúrræðastími í sekúndum", + "Requested at": "Beðið um kl.", + "Required": "Nauðsynlegt", + "Required Token Scopes": "Nauðsynlegar tóken-sviðar", + "Required field": "Nauðsynlegt svæði", + "Required status is inconsistent": "Nauðsynlegur staðs er ósamkvæmur", + "Requires Dolphin AI for OCR text extraction": "Krefst Dolphin AI fyrir OCR textaútdrátt", + "Rerun Search": "Endurtaka leit", + "Rerunning search: {searchTerm}": "Endurtaka leit: {searchTerm}", + "Reset Changes": "Endurstilla breytingar", + "Reset Filters": "Endurstilla síar", + "Resource Quotas": "Kvótar auðlinda", + "Response Body": "Svarhluti", + "Response Details": "Upplýsingar um svar", + "Response Time:": "Svartími:", + "Restore": "Endurheimta", + "Restore conversation": "Endurheimta samtal", + "Restore or permanently delete items": "Endurheimta eða eyða hlutum varanlega", + "Restore {count} object": "Endurheimta {count} hlut", + "Restore {count} objects": "Endurheimta {count} hluti", + "Result Count Range": "Bil niðurstöðufjölda", + "Results": "Niðurstöður", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Varðveislutímabil (ISO-8601 tímalengd, t.d. P10Y, P30D)", + "Retention settings updated successfully": "Varðveislustillingar uppfærðar", + "Retrieve all objects for this schema": "Sækja alla hluti fyrir þetta skema", + "Retry": "Endurtaka", + "Retry Failed Extractions": "Endurtaka misheppnaðar útdráttur", + "Retry Policy": "Endurtilraunaregla", + "Retry completed": "Endurtilraun lokið", + "Retry upload": "Endurtaka upphleðslu", + "Retrying...": "Endurtaki...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Skoðaðu valda hlutina hér að neðan. Þú getur fjarlægt hluti sem þú vilt ekki eyða með því að smella á fjarlægja hnappinn.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Skoðaðu valda hlutina hér að neðan. Þú getur fjarlægt hluti sem þú vilt ekki taka með.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Skoðaðu valda hlutina hér að neðan. Þú getur fjarlægt hluti sem þú vilt ekki eyða varanlega með því að smella á fjarlægja hnappinn. Þessa aðgerð er ekki hægt að afturkalla.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Skoðaðu valda hlutina hér að neðan. Þú getur fjarlægt hluti sem þú vilt ekki endurheimta með því að smella á fjarlægja hnappinn. Hlutir verða endurheimtir á upprunalegar staðsetning.", + "Revoke": "Afturkalla", + "Right": "Hægri", + "Risk Level": "Áhættuflokk", + "Risk level": "Áhættuflokk", + "Roles and Permissions": "Hlutverk og leyfi", + "Roles updated successfully": "Hlutverk uppfærð", + "Run compliance scan": "Keyra fylgniskoðun", + "SOLR Connection Settings": "SOLR tengingarstillingar", + "SOLR Index Warmup": "SOLR skráahlýnun", + "SOLR Version:": "SOLR útgáfa:", + "SOLR actions menu": "SOLR aðgerðavalmynd", + "SOLR base path (usually /solr)": "SOLR grunnskrá (venjulega /solr)", + "SOLR collection deleted successfully": "SOLR safni eytt", + "SOLR connection test failed: {error}": "SOLR tengingaprófun mistókst: {error}", + "SOLR connection test successful!": "SOLR tengingaprófun tókst!", + "SOLR index warmup completed successfully": "SOLR skráahlýnun lokið", + "SOLR is disabled": "SOLR er óvirkt", + "SOLR is not available or not configured": "SOLR er ekki tiltækt eða ekki stillt", + "SOLR logging disabled": "SOLR skráning óvirkjuð", + "SOLR logging enabled": "SOLR skráning virkjuð", + "SOLR search disabled": "SOLR leit óvirkjuð", + "SOLR search enabled": "SOLR leit virkjuð", + "SOLR server hostname or IP address": "SOLR þjónaheiti eða IP-tala", + "SOLR server port number (optional, defaults to 8983)": "SOLR þjónagáttarnúmer (valkvætt, sjálfgefið 8983)", + "SOLR settings updated successfully": "SOLR stillingar uppfærðar", + "SOLR setup completed successfully": "SOLR uppsetning lokið", + "SOLR setup completed successfully!": "SOLR uppsetning lokið!", + "SOLR setup error: %s": "SOLR uppsetningarvilla: %s", + "SOLR setup failed": "SOLR uppsetning misheppnaðist", + "SOLR setup failed - check logs": "SOLR uppsetning misheppnaðist - athugaðu skráningar", + "SOLR setup failed. Please check the configuration and try again.": "SOLR uppsetning misheppnaðist. Vinsamlegast athugaðu stillinguna og reyndu aftur.", + "SOLR setup failed: {error}": "SOLR uppsetning misheppnaðist: {error}", + "SOLR warmup failed: {error}": "SOLR hlýnun misheppnaðist: {error}", + "Sample Values:": "Dæmigildi:", + "Save": "Vista", + "Save Configuration": "Vista stillingu", + "Save Connection Settings": "Vista tengingarstillingar", + "Save Roles": "Vista hlutverk", + "Save Settings": "Vista stillingar", + "Save changes": "Vista breytingar", + "Save current search as view": "Vista núverandi leit sem yfirlit", + "Save labels": "Vista merkimiða", + "Save tags for {name}": "Vista taggar fyrir {name}", + "Save the organisation first to manage users.": "Vistaðu stofnunina fyrst til að stjórna notendum.", + "Saved Views": "Vistuð yfirlit", + "Saving...": "Vista...", + "Scheduled": "Tímasett", + "Schema": "Skema", + "Schema ID": "Skemaauðkenni", + "Schema Name": "Nafn skema", + "Schema Statistics": "Tölfræði skema", + "Schema annotation": "Skemakómerki", + "Schema configuration updated successfully for {schema}": "Skemastilling uppfærð fyrir {schema}", + "Schema depublished successfully": "Skema afbirt", + "Schema objects deletion completed successfully": "Eyðing skemahluta lokið", + "Schema published successfully": "Skema birt", + "Schema reference ($ref)": "Skematilvísun ($ref)", + "Schema successfully created": "Skema búið til", + "Schema successfully updated": "Skema uppfært", + "Schema {schema} removed successfully": "Skema {schema} fjarlægt", + "Schema:": "Skema:", + "Schemas": "Skemu", + "Scheme": "Kerfi", + "Search": "Leita", + "Search / Views": "Leita / Yfirlit", + "Search / views": "Leita / yfirlit", + "Search Activity": "Leitarstarfsemi", + "Search Analytics": "Leitargreiningar", + "Search Configuration": "Leitarstilling", + "Search Entities": "Leita að einingum", + "Search Files": "Leita að skrám", + "Search GitHub": "Leita á GitHub", + "Search GitLab": "Leita á GitLab", + "Search Method": "Leitaraðferð", + "Search Mode": "Leitarhamur", + "Search Query": "Leitarfyrirspurn", + "Search Results": "Leitarniðurstöður", + "Search Term": "Leitarhugtök", + "Search Trail Details": "Upplýsingar leitarferlatals", + "Search Trail Management": "Stjórnun leitarferlatals", + "Search Trail Statistics": "Tölfræði leitarferlatals", + "Search Trails": "Leitarferlatal", + "Search Views": "Leita yfirlit", + "Search Webhooks": "Leita vefkrókar", + "Search agents...": "Leita umboðsmenn...", + "Search applications...": "Leita forrit...", + "Search by file name or path": "Leita eftir nafni skráar eða slóð", + "Search by name or URL": "Leita eftir nafni eða vefslóð", + "Search by title or UUID...": "Leita eftir titli eða UUID...", + "Search by value": "Leita eftir gildi", + "Search configurations": "Leita stillingar", + "Search data sources...": "Leita gagnaveitu...", + "Search groups...": "Leita hópa...", + "Search in Database Objects": "Leita í gagnagrunnhlutum", + "Search in Files": "Leita í skrám", + "Search in Objects": "Leita í hlutum", + "Search objects": "Leita hluti", + "Search objects...": "Leita hluti...", + "Search property names...": "Leita eiginleikaheiti...", + "Search registers...": "Leita skrár...", + "Search schemas...": "Leita skemu...", + "Search trail deletion not implemented yet": "Eyðing leitarferlatals er ekki enn innleidd", + "Search views...": "Leita yfirlit...", + "Search {name}...": "Leita {name}...", + "Searching...": "Leitar...", + "Secret": "Leynilykill", + "Secret key for HMAC signature generation (optional)": "Leynigerðarlykill fyrir HMAC undirskriftarmyndun (valkvætt)", + "Security": "Öryggi", + "See organisations as a table": "Sjá stofnanir sem töflu", + "See organisations as cards": "Sjá stofnanir sem kort", + "Select AI Agent": "Veldu AI umboðsmann", + "Select Active Organisation": "Veldu virka stofnun", + "Select All": "Velja allt", + "Select ConfigSet": "Veldu ConfigSet", + "Select File Types to Vectorize:": "Veldu skráartegundir til að vektóra:", + "Select HTTP method": "Veldu HTTP aðferð", + "Select Register and Schema": "Veldu skrá og skema", + "Select Views to Vectorize:": "Veldu yfirlit til að vektóra:", + "Select a Nextcloud group to add": "Veldu Nextcloud hóp til að bæta við", + "Select a branch": "Veldu grein", + "Select a date property": "Veldu dagsetningareiginleika", + "Select a model or type a custom model name": "Veldu líkan eða sláðu inn sérsniðið líkansheiti", + "Select a register": "Veldu skrá", + "Select a register...": "Veldu skrá...", + "Select a repository": "Veldu geymslu", + "Select a repository you have write access to": "Veldu geymslu sem þú hefur skrivaðgang að", + "Select a schema": "Veldu skema", + "Select a schema...": "Veldu skema...", + "Select a user": "Veldu notanda", + "Select agent type": "Veldu tegund umboðsmanns", + "Select all entities": "Velja allar einingar", + "Select an AI Agent": "Veldu AI umboðsmann", + "Select an AI agent to begin chatting with your data.": "Veldu AI umboðsmann til að byrja að spjalla um gögnin þín.", + "Select an email to see linked objects": "Veldu tölvupóst til að sjá tengda hluti", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Veldu stofnun og notanda til að bæta þeim við sem meðlim. Leitaðu að stofnunum eftir nafni.", + "Select backend": "Veldu bakenda", + "Select chat model": "Veldu spjalllíkan", + "Select collection for files": "Veldu safn fyrir skrár", + "Select collection for objects": "Veldu safn fyrir hluti", + "Select configuration type...": "Veldu tegund stillingar...", + "Select default organisation": "Veldu sjálfgefna stofnun", + "Select entity {value}": "Veldu einingu {value}", + "Select event to listen to...": "Veldu viðburð til að hlusta á...", + "Select fields to display": "Veldu svæði til að sýna", + "Select files with the correct extension": "Veldu skrár með réttu viðskeytinu", + "Select group": "Veldu hóp", + "Select groups (optional)": "Veldu hópa (valkvætt)", + "Select groups to notify...": "Veldu hópa til að tilkynna...", + "Select groups with access to this agent": "Veldu hópa með aðgang að þessum umboðsmanni", + "Select groups...": "Veldu hópa...", + "Select model": "Veldu líkan", + "Select one or more registers": "Veldu eina eða fleiri skrár", + "Select one or more schemas": "Veldu eitt eða fleiri skemu", + "Select options...": "Veldu valkosti...", + "Select or create labels, or select \"No label\" to add files": "Veldu eða búðu til merkimiða, eða veldu \"Enginn merkimiði\" til að bæta við skrám", + "Select owner application (optional)...": "Veldu eigandaforrit (valkvætt)...", + "Select period": "Veldu tímabil", + "Select property to send as payload": "Veldu eiginleika til að senda sem farm", + "Select provider": "Veldu veitu", + "Select registers and schemas to save a view": "Veldu skrár og skemu til að vista yfirlit", + "Select retry policy": "Veldu endurtilraunareglu", + "Select role": "Veldu hlutverk", + "Select schemas (empty = all schemas)": "Veldu skemu (tómt = öll skemu)", + "Select schemas to warm up": "Veldu skemu til að hlýna", + "Select search mode": "Veldu leitarham", + "Select source type...": "Veldu tegund uppsprettu...", + "Select target type": "Veldu markstegund", + "Select the branch to publish to": "Veldu grein til að birta í", + "Select the event this webhook should listen to": "Veldu viðburð sem þessi vefkrókur á að hlusta á", + "Select views to filter data (optional)": "Veldu yfirlit til að sía gögn (valkvætt)", + "Select views to limit which data the agent can access": "Veldu yfirlit til að takmarka hvaða gögn umboðsmaðurinn getur fengið aðgang að", + "Select views to vectorize:": "Veldu yfirlit til að vektóra:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Veldu hvaða Nextcloud hópar eru tiltækir fyrir þessa stofnun. Notendur í þessum hópum munu hafa aðgang að auðlindum stofnunar.", + "Select which data views the AI can search": "Veldu hvaða gagnasýn AI getur leitað í", + "Select which property from the event should be used as the webhook payload data": "Veldu hvaða eiginleika úr viðburðinum á að nota sem farmgögn vefkróksins", + "Select which tools the AI can use to perform actions": "Veldu hvaða verkfæri AI getur notað til að framkvæma aðgerðir", + "Select {fieldName} values": "Veldu {fieldName} gildi", + "Selected": "Valið", + "Selected Groups": "Valdir hópar", + "Selected Objects ({count})": "Valdir hlutir ({count})", + "Selected audit trails deleted successfully": "Valdar endurskoðunarferslur eyddar", + "Selected search trails deleted successfully": "Valdar leitarferslur eyddar", + "Selected users can access this view": "Valdir notendur geta fengið aðgang að þessu yfirliti", + "Send additional feedback": "Senda viðbótargjöf", + "Send as CloudEvent": "Senda sem CloudEvent", + "Send as cloudevent": "Senda sem cloudevent", + "Sensitive PII": "Viðkvæmt PII", + "Serial Mode (Safer, slower)": "Raðhamur (Öruggara, hægar)", + "Serial:": "Raðhamur:", + "Server Configuration": "Þjónastilling", + "Server Information": "Þjónarupplýsingar", + "Set as Active": "Stilla sem virkt", + "Set as active organisation": "Stilla sem virka stofnun", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Stilla takmarkanir á API notkun og tókenneyslu. Notaðu 0 fyrir ótakmarkaðar auðlindir.", + "Settings": "Stillingar", + "Settings updated successfully": "Stillingar uppfærðar", + "Shards": "Brot", + "Share": "Deila", + "Share with Groups": "Deila með hópum", + "Share with Users": "Deila með notendum", + "Show": "Sýna", + "Show Filters": "Sýna síar", + "Show less": "Sýna minna", + "Show only entries with changes": "Sýna aðeins færslur með breytingar", + "Showing {count} dashboard(s)": "Sýni {count} mælaborð", + "Showing {count} processing activities": "Sýni {count} vinnslustarfsemi", + "Showing {showing} of {total} agents": "Sýni {showing} af {total} umboðsmönnum", + "Showing {showing} of {total} applications": "Sýni {showing} af {total} forritum", + "Showing {showing} of {total} audit trail entries": "Sýni {showing} af {total} endurskoðunarferslum", + "Showing {showing} of {total} configurations": "Sýni {showing} af {total} stillingum", + "Showing {showing} of {total} deleted items": "Sýni {showing} af {total} eyddum hlutum", + "Showing {showing} of {total} entities": "Sýni {showing} af {total} einingum", + "Showing {showing} of {total} files": "Sýni {showing} af {total} skrám", + "Showing {showing} of {total} log entries": "Sýni {showing} af {total} skráningarfærslum", + "Showing {showing} of {total} search trail entries": "Sýni {showing} af {total} leitarferslum", + "Showing {showing} of {total} sources": "Sýni {showing} af {total} uppsprettum", + "Showing {showing} of {total} templates": "Sýni {showing} af {total} sniðmátum", + "Showing {showing} of {total} webhooks": "Sýni {showing} af {total} vefkrókum", + "Showing {shown} of {total} organisations": "Sýni {shown} af {total} stofnunum", + "Simple": "Einfalt", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Einfaldar fyrirspurnir: Grunnleitir með fáum færibreytum (t.d. eitt leitarhugtök, engar ítarlegar síar)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Einfaldar fyrirspurnir: grunnleitir með fáum færibreytum (t.d. eitt leitarhugtök, engar ítarlegar síar)", + "Size": "Stærð", + "Slug": "Auðkenni", + "Soft Delete Mode": "Mjúkur eyðingarhamur", + "Soft Deleted Items": "Mjúkt eyddir hlutir", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Mjúkeyða öllum hlutum fyrir þetta skema ({active} virkir, {deleted} þegar eyddir)", + "SolrCloud mode enabled": "SolrCloud hamur virkjað", + "Something went wrong while adding files": "Eitthvað fór úrskeiðis við að bæta við skrám", + "Source": "Uppspretta", + "Source successfully updated": "Uppspretta uppfærð", + "Sources": "Uppsprettur", + "Sources:": "Uppsprettur:", + "Special Rights": "Sérstakar réttindi", + "Standalone SOLR mode": "Sjálfstæður SOLR hamur", + "Start Conversation": "Hefja samtal", + "Start Date Field": "Upphafsdagsetningarsvæði", + "Start Vectorization": "Hefja vektórfærslu", + "Start a conversation": "Hefja samtal", + "Starting names cache warmup...": "Hefja hlýnun nafnaskyndiminnis...", + "Starting validation for {schema}...": "Hefja staðfestingu fyrir {schema}...", + "Starting {action} for {schema}...": "Hefja {action} fyrir {schema}...", + "Starting...": "Hef...", + "Statistics": "Tölfræði", + "Statistics and Metrics": "Tölfræði og mælikvarðar", + "Statistics refreshed": "Tölfræði uppfærð", + "Stats calculation not yet implemented": "Tölfræðiútreikningur er ekki enn innleiddur", + "Status": "Staða", + "Status Code": "Stöðukóði", + "Step": "Skref", + "Storage": "Geymsla", + "Storage Quota (MB)": "Geymslukvóti (MB)", + "String Constraints": "Strengjatakmörkunar", + "Subject identifier (email, BSN, name, etc.)": "Efnisauðkenni (tölvupóstur, BSN, nafn, o.fl.)", + "Subscribe": "Gerast áskrifandi", + "Subscribed": "Áskrifandi", + "Success": "Árangur", + "Success Rate": "Hlutfall árangurs", + "Success Status": "Staða árangurs", + "Successful": "Tókst", + "Successfully added user to organisation: {name}": "Notanda bætt við stofnun: {name}", + "Successfully cleared {count} audit trails": "Hreinsaðar {count} endurskoðunarferslur", + "Successfully cleared {count} blob storage objects": "Hreinsaðir {count} blob-geymsluhlutar", + "Successfully cleared {count} search trails": "Hreinsaðar {count} leitarferslur", + "Successfully created {count} SOLR fields": "Búin til {count} SOLR svæði", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Eyddar {count} fellingar. Vinsamlegast vektóraðu gögnin þín aftur.", + "Successfully deleted {count} objects for {schema}": "Eytt {count} hlutum fyrir {schema}", + "Successfully fixed {count} SOLR field configurations": "Lagaðar {count} SOLR svæðisstillingar", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Flutt inn: {registers} skrár, {schemas} skemu, {objects} hlutir", + "Successfully joined organisation": "Gengist í stofnun", + "Successfully permanently deleted {count} objects": "Varanlega eytt {count} hlutum", + "Successfully processed {count} files!": "Unnin {count} skrár!", + "Successfully restored {count} object": "Endurheimt {count} hlut", + "Successfully restored {count} objects": "Endurheimt {count} hlutir", + "Successfully saved configuration for {count} facets!": "Stilling vistuð fyrir {count} hliðar!", + "Supported File Types": "Studdar skráartegundir", + "Switch Active Organisation": "Skipta um virka stofnun", + "Switch Organisation": "Skipta um stofnun", + "Sync Table": "Samstilla töflu", + "Synchronization Settings": "Samstillingarstillingar", + "Syncing magic table for {schema}...": "Samstilli töfratöflu fyrir {schema}...", + "Syncs": "Samstillingar", + "System Default": "Kerfisjálfgefið", + "System Prompt": "Kerfisboð", + "System announcements": "Kerfistilkynningar", + "Table": "Tafla", + "Target ID": "Markauðkenni", + "Target Type*": "Markstegund*", + "Technical Description": "Tæknileg lýsing", + "Technical Details": "Tæknilegar upplýsingar", + "Technical description for developers and administrators": "Tæknileg lýsing fyrir þróunaraðila og stjórnendur", + "Technical measures": "Tæknilegar ráðstafanir", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Hitastig", + "Templates": "Sniðmát", + "Test": "Prófa", + "Test Chat": "Prófa spjall", + "Test Connection": "Prófa tengingu", + "Test Embedding": "Prófa fellingu", + "Test endpoint executed successfully": "Endapunkts prófun tókst", + "Test webhook delivered successfully": "Vefkróksprófun afhent", + "Test webhook delivery failed": "Afhending vefkróksprófunar misheppnaðist", + "Test webhook sent successfully": "Vefkróksprófun send", + "Testing...": "Prófa...", + "Text Chunk #{id}": "Textabútur #{id}", + "Text Extraction": "Textaútdráttur", + "Text Extraction Engine": "Textaútdráttursvél", + "Text chunks": "Textabútar", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Textabútar eru búnir til við útdrátt skráa og geymdir í gagnagrunninum. Vektórfærsla les þessa forskornar skrár og umbreytir þeim í fellingar.", + "Text extracted successfully": "Texti dreginn út", + "Text extraction disabled": "Textaútdráttur óvirkjað", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloud Dagatal forritið er ekki uppsett eða virkjað á þessum þjóni.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloud Tengiliðir forritið er ekki uppsett eða virkjað á þessum þjóni.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloud Deck forritið er ekki uppsett eða virkjað á þessum þjóni.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloud Póstur forritið er ekki uppsett eða virkjað á þessum þjóni.", + "The URL where webhook events will be sent": "Vefslóðin þar sem vefkrókur viðburðir verða sendir", + "The requested conversation does not exist": "Umbeðið samtal er ekki til", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Ekki var hægt að hlaða umbeðið mælaborð. Staðfestu vefslóðina eða veldu annað mælaborð úr listanum.", + "The saved view and all its search configuration": "Vistað yfirlit og allar leitarstillingar þess", + "There are no audit trail entries matching your current filters.": "Engar endurskoðunarferslur passa við núverandi síar.", + "There are no deleted items matching your current filters.": "Engir eyddir hlutir passa við núverandi síar.", + "There are no search trail entries matching your current filters.": "Engar leitarferslur passa við núverandi síar.", + "There are no webhook log entries matching your filters.": "Engar vefkrókurskráningarfærslur passa við síarnar þínar.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Þær verða geymdar samkvæmt stilltum varðveislutímabili skemans og sjálfvirkt varanlega eyddar þegar varðveislutímabilið rennur út. Varðveislutímabilið er stilljanlegt per skema og er að finna í stillingum skemans.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Þær verða geymdar samkvæmt stilltum varðveislutímabili skemans og síðan varanlega eyddar.", + "This action cannot be undone.": "Þessa aðgerð er ekki hægt að afturkalla.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Þessa aðgerð er ekki hægt að afturkalla. Gakktu úr skugga um að engin söfn noti þennan ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Þessa aðgerð er ekki hægt að afturkalla. Gakktu úr skugga um að engin söfn noti þennan configset.", + "This action will submit a deactivation request to your administrators.": "Þessi aðgerð mun senda óvirkjunarbeiðni til stjórnenda þinna.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Þessi umboðsmaður hefur engin stillanleg yfirlit eða verkfæri, en þú getur ennþá stillt RAG stillingar hér að neðan.", + "This analysis may take some time": "Þessi greining gæti tekið nokkurn tíma", + "This audit trail entry does not contain any change information.": "Þessi endurskoðunarfersla inniheldur engar breytingarupplýsingar.", + "This configuration has no data defined.": "Þessi stilling hefur engin gögn skilgreind.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Þessi endapunktur er úreltur. Notaðu bútatengda endapunkta í staðinn.", + "This entity has no relations to objects or files": "Þessi eining hefur engin tengsl við hluti eða skrár", + "This is a test webhook from OpenRegister": "Þetta er prófunarvefkrókur frá OpenRegister", + "This is not valid JSON": "Þetta er ekki gilt JSON", + "This key already exists on this schema": "Þessi lykill er þegar til á þessu skema", + "This schema must use Magic Table configuration to sync": "Þetta skema verður að nota Töfratöflustillingu til að samstilla", + "This schema must use magic table configuration to sync": "Þetta skema verður að nota töfratöflustillingu til að samstilla", + "This source has no associated registers.": "Þessi uppspretta hefur engar tengdar skrár.", + "This token will only be shown once. Copy it now.": "Þetta tóken verður aðeins sýnt einu sinni. Afritaðu það núna.", + "This will copy the _default ConfigSet with the new name": "Þetta mun afrita _default ConfigSet með nýja nafninu", + "This will copy the _default configset with the new name": "Þetta mun afrita _default configset með nýja nafninu", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Þetta mun varanlega eyða ÖLLUM fellingum (vektórum) úr gagnagrunninum. Þú munt þurfa að vektóra alla hluti og skrár aftur. Þessa aðgerð er ekki hægt að afturkalla.\n\nErtu viss um að þú viljir halda áfram?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Þetta mun varanlega eyða ÖLLUM fellingum (vektórum) úr gagnagrunninum. Þú munt þurfa að vektóra alla hluti og skrár aftur. Þessa aðgerð er ekki hægt að afturkalla.\\n\\nErtu viss um að þú viljir halda áfram?", + "This will permanently delete:": "Þetta mun varanlega eyða:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Þetta mun varanlega fjarlægja {count} þegar mjúkt eydda hluti úr gagnagrunninum. Þessa aðgerð er ekki hægt að afturkalla.", + "Timeout (seconds)": "Úrræðastími (sekúndur)", + "Timestamp": "Tímastimpill", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Ráðlegging: Virkjaðu aðeins yfirlit sem þurfa merkingarfræðilega leit til að lágmarka fellingarkostnað. Einfaldar uppflettitöflur þurfa sjaldan vektórfærslu.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Ráðlegging: virkjaðu aðeins yfirlit sem þurfa merkingarfræðilega leit til að lágmarka fellingarkostnað. Einfaldar uppflettitöflur þurfa sjaldan vektórfærslu.", + "Title": "Titill", + "Title *": "Titill *", + "Title Template": "Titillsniðmát", + "Title*": "Titill*", + "To (e.g. -1 year or 2025-12-31)": "Til (t.d. -1 ár eða 2025-12-31)", + "To Date": "Til dagsetning", + "To add files larger than or equal to 512MB, go to the": "Til að bæta við skrám stærri en eða jafnri 512MB, farðu í", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Til að stilla bútstærð eða stefnu, farðu í Skráastilling → Vinnslutak.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Til að stilla bútstærð eða stefnu, farðu í skráastilling → vinnslutak.", + "To change the embedding provider or model, go to LLM Configuration.": "Til að breyta fellingarveitu eða líkani, farðu í LLM Stilling.", + "To change the embedding provider or model, go to LLM configuration.": "Til að breyta fellingarveitu eða líkani, farðu í LLM stilling.", + "To date": "Til dagsetning", + "Toggle search sidebar": "Víxla leitarhliðarborða", + "Token Created": "Tóken búið til", + "Token Quota (per request)": "Tókenkvóti (á beiðni)", + "Token copied to clipboard": "Tóken afritað á klippiborð", + "Token name": "Nafn tókens", + "Token revoked": "Tóken afturkallað", + "Tools": "Verkfæri", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Verkfæri keyra með sjálfgefnum notandaleyfi umboðsmannsins þegar engin notandalota er virk (t.d. cron verk). Stilltu sjálfgefna notandann í Stillingar flipanum.", + "Tools respect the agent's own role and organization boundaries.": "Verkfæri virða eigin hlutverk umboðsmannsins og stofnanarmörk.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Verkfæri virða yfirlit, leyfi og stofnanarmörk umboðsmannsins.", + "Top Deleters": "Helstu eyðarar", + "Total": "Heildartala", + "Total Audit Trails": "Heildartala endurskoðunarferlatals", + "Total Chunks Available:": "Heildartala tiltækra búta:", + "Total Deleted Items": "Heildartala eyddra hluta", + "Total Members": "Heildartala meðlima", + "Total Objects": "Heildartala hluta", + "Total Objects in Database:": "Heildartala hluta í gagnagrunni:", + "Total Organisations": "Heildartala stofnana", + "Total Results": "Heildartala niðurstaðna", + "Total Searches": "Heildartala leita", + "Total Size": "Heildartala stærð", + "Total Storage": "Heildartala geymsla", + "Total events": "Heildartala viðburða", + "Total:": "Heildartala:", + "Totals": "Heildar", + "Try searching by UUID or with different keywords": "Reyndu að leita eftir UUID eða með öðrum leitarorðum", + "Type": "Tegund", + "Type \"DELETE COLLECTION\" to confirm:": "Sláðu inn \"DELETE COLLECTION\" til að staðfesta:", + "Type Issue": "Tegundarvandamál", + "Type Variations:": "Tegundarafbrigði:", + "Type filter (optional, e.g. email)": "Tegundarsía (valkvætt, t.d. tölvupóstur)", + "Type must be \"positive\" or \"negative\"": "Tegund verður að vera \"positive\" eða \"negative\"", + "Type to search for groups": "Sláðu inn til að leita að hópum", + "Type to search for objects...": "Sláðu inn til að leita að hlutum...", + "Type to search for organisations": "Sláðu inn til að leita að stofnunum", + "Type to search for users": "Sláðu inn til að leita að notendum", + "Type to search groups...": "Sláðu inn til að leita að hópum...", + "Type to search users...": "Sláðu inn til að leita að notendum...", + "Type to search...": "Sláðu inn til að leita...", + "Type your username to confirm": "Sláðu inn notandanafnið þitt til að staðfesta", + "URL": "URL", + "URL where Ollama is running": "URL þar sem Ollama keyrir", + "UUID copied to clipboard": "UUID afritað á klippiborð", + "UUID:": "UUID:", + "Unchanged": "Óbreytt", + "Unique Search Terms": "Einstök leitarhugtök", + "Unique Terms": "Einstök hugtök", + "Unknown": "Óþekkt", + "Unknown Object": "Óþekktur hlutur", + "Unknown Register": "Óþekkt skrá", + "Unknown Schema": "Óþekkt skema", + "Unknown error": "Óþekkt villa", + "Unknown widget type: {type}": "Óþekkt gluggategund: {type}", + "Unlink email": "Aftengja tölvupóst", + "Unlink event": "Aftengja viðburð", + "Unnamed Object": "Ónefndur hlutur", + "Untitled": "Án titils", + "Untitled View": "Yfirlit án titils", + "Update": "Uppfæra", + "Update Operations": "Uppfærslaaðgerðir", + "Update available: {local} → {remote}": "Uppfærsla tiltæk: {local} → {remote}", + "Update configuration: ...": "Uppfæra stillingu: ...", + "Update register OAS: ...": "Uppfæra skrár OAS: ...", + "Update vectors when object data changes (recommended for accurate search)": "Uppfæra vektóra þegar hlutgögn breytast (mælt með fyrir nákvæma leit)", + "Updated": "Uppfært", + "Updated At": "Uppfært við", + "Updated:": "Uppfært:", + "Upload new avatar": "Hlaða upp nýjum líkneskum", + "Uploading configuration sets...": "Hleð upp stillingasettum...", + "Uptime:": "Upptíma:", + "Url": "Vefslóð", + "Usage Count": "Notkunarfjöldi", + "Use AI agents for processing and analysis": "Nota AI umboðsmenn til úrvinnslu og greiningar", + "Use Large Language Model features": "Nota stóra tungumálalíkanseiginleika", + "Use SolrCloud with Zookeeper for distributed search": "Nota SolrCloud með Zookeeper fyrir dreifða leit", + "Use SolrCloud with zookeeper for distributed search": "Nota SolrCloud með zookeeper fyrir dreifða leit", + "Use VNG APIs instead of MongoDB.": "Nota VNG APIs í stað MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Nota sjálfgefin bil (Síðustu 7/30/90 dagar, Síðasta ár, Eldri)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Nota ytri geymslu (t.d. MongoDB) í stað innri Nextcloud geymslu.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Notaðu síar til að þrengja endurskoðunarferslur eftir skrá, skema, aðgerðartegund, notanda, dagsetningarbil eða hlutauðkenni.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Notaðu síar til að þrengja eydda hluti eftir skrá, skema, eyðingardagsetning eða notandann sem eyðdi þeim.", + "Use filters to narrow down entities by type or category.": "Notaðu síar til að þrengja einingar eftir tegund eða flokki.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Notaðu síar til að þrengja leitarferslur eftir skrá, skema, árangursstöðu, notanda, dagsetningarbil, leitarhugtök eða frammistöðumælikvarða.", + "Used By Collections": "Notað af söfnum", + "Used by": "Notað af", + "User": "Notandi", + "User Agent Statistics": "Tölfræði notandaumboðsmanns", + "User Description": "Notandalýsing", + "User Quota": "Notandakvóti", + "User rate limits cleared successfully": "Notandahraðatakmarkanir hreinsaðar", + "User removed successfully": "Notanda fjarlægt", + "User-friendly description shown in forms and help text": "Notandavæn lýsing sýnd í eyðublöðum og hjálpartexta", + "Username": "Notandanafn", + "Username for SOLR authentication (optional)": "Notandanafn fyrir SOLR auðkenningu (valkvætt)", + "Users": "Notendur", + "Uses": "Notar", + "Using Pre-Generated Chunks": "Nota forskornar bútar", + "Validate": "Staðfesta", + "Validate Objects": "Staðfesta hluti", + "Validation completed for {schema}": "Staðfesting lokið fyrir {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Staðfesting lokið fyrir {schema}: {processed} unnin, {updated} uppfærð, {failed} misheppnuðust", + "Validation completed successfully": "Staðfesting lokið", + "Validation failed": "Staðfesting misheppnaðist", + "Value": "Gildi", + "Value list (split on ,)": "Gildalisti (skipt á ,)", + "Vector Dimensions": "Vektórvíddir", + "Vector Search Backend": "Vektórleitarbakendi", + "Vector field: _embedding_": "Vektórsvæði: _embedding_", + "Vectorization Triggers": "Kveikjur vektórfærslu", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektórfærsla lokið: {vectorized} hlutir vektóraðir, {failed} misheppnuðust", + "Vectorization failed: {error}": "Vektórfærsla misheppnaðist: {error}", + "Vectorize All Files": "Vektóra allar skrár", + "Vectorize All Objects": "Vektóra alla hluti", + "Vectorize all file types": "Vektóra allar skráartegundir", + "Vectorize all views": "Vektóra öll yfirlit", + "Vectorize on object creation": "Vektóra við hlutabúningu", + "Vectors will be stored in your existing object and file collections": "Vektórar verða geymdir í núverandi hluta- og skráasöfnum þínum", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Staðfesting þjónatengingar...", + "Version": "Útgáfa", + "Version Information": "Útgáfuupplýsingar", + "Very High": "Mjög hátt", + "Very high": "Mjög hátt", + "View": "Yfirlit", + "View API Docs": "Skoða API skjöl", + "View API Documentation": "Skoða API skjöl", + "View APIs": "Skoða APIs", + "View Changes": "Skoða breytingar", + "View Details": "Skoða upplýsingar", + "View Error": "Yfirlitvilla", + "View File": "Skoða skrá", + "View Full Details": "Skoða allar upplýsingar", + "View Logs": "Skoða skráningar", + "View Name": "Nafn yfirlits", + "View Object": "Skoða hlut", + "View Parameters": "Yfirlitsfæribreytur", + "View Selection": "Val yfirlits", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Skoða og greina leitarferlatasskráningar með ítarlegum síum og greinunargæðum", + "View and analyze system audit trails with advanced filtering capabilities": "Skoða og greina kerfisendurskoðunarferlatal með ítarlegum síum", + "View and manage authorization across registers and schemas": "Skoða og stjórna heimildum yfir skrár og skemu", + "View deleted successfully": "Yfirliti eytt", + "View entity details and manage relations": "Skoða upplýsingar einingar og stjórna tengslum", + "View name": "Nafn yfirlits", + "View name is required": "Nafn yfirlits er nauðsynlegt", + "View object": "Skoða hlut", + "View saved successfully!": "Yfirlit vistað!", + "View search analytics and manage search logs": "Skoða leitargreiningar og stjórna leitarskráningum", + "View successfully deleted": "Yfirliti eytt", + "View successfully updated": "Yfirlit uppfært", + "View updated successfully!": "Yfirlit uppfært!", + "View webhook delivery logs and filter by webhook": "Skoða afhendingarskráningar vefkróks og sía eftir vefkrók", + "View {count} more": "Skoða {count} fleiri", + "Views": "Yfirlit", + "Visible to end users": "Sýnilegt loknotendum", + "Visible to users": "Sýnilegt notendum", + "Wait for Response": "Bíða eftir svari", + "Wait for webhook response before continuing (required for request/response flows)": "Bíðu eftir svari vefkróks áður en haldið er áfram (nauðsynlegt fyrir beiðni/svar flæði)", + "Waiting for cluster synchronization...": "Bíð eftir samstillingu þyrpingar...", + "Warmup Names Cache": "Hlýna nafnaskyndiminni", + "Warmup failed": "Hlýnun misheppnaðist", + "Warning": "Viðvörun", + "Warning:": "Viðvörun:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Viðvörun: Þessi eiginleiki hefur ósamkvæmar tegundir: {types}. Ígrundaðu hvort allir hlutir eigi að hafa sömu tegund.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Við munum skanna alla hluti sem tilheyra þessu skema til að uppgötva nýja eiginleika og greina núverandi eiginleika með tilliti til mögulegra endurbóta. Ferlið felst í því að skoða gagnaskipulag hvers hlutar, finna eiginleika sem eru ekki skilgreindir í núverandi skema og finna tækifæri til að bæta núverandi eiginleikaskilgreiningar með betri takmörkunum, sniðum og staðfestingarreglum.", + "Webhook": "Vefkrókur", + "Webhook Log Details": "Upplýsingar vefkrókarskráningar", + "Webhook Logs": "Vefkrókarskráningar", + "Webhook created successfully": "Vefkrókur búinn til", + "Webhook deleted": "Vefkrókur eytt", + "Webhook retry delivered successfully": "Endurtilraun vefkróks afhent", + "Webhook retry delivery failed": "Afhending endurtilraunar vefkróks misheppnaðist", + "Webhook updated": "Vefkrókur uppfærður", + "Webhook updated successfully": "Vefkrókur uppfærður", + "Webhooks": "Vefkrókar", + "Weekly": "Vikulega", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Þegar umboðsmaður keyrir án notandalotu (t.d. tímasettar verkefni), verður samhengi þessa notanda notað", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Þegar umboðsmaður keyrir án notandalotu (t.d. tímasettar verkefni), verður samhengi þessa notanda notað.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Þegar óvirkt, mun þessi hlið aðeins sýna gildi úr þessu skema og mun innihalda skema síu þegar valið.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Þegar virkjað, mun vistun þessa hlutar einnig uppfæra markhlutina til að innihalda tilvísun aftur í þennan hlut.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Þegar virkjað, verður þessi eiginleiki fjarlægður úr upprunahlutnum eftir að markhlutar eru uppfærðir.", + "Workflow Management": "Stjórnun vinnuflæðis", + "Workflows": "Vinnuflæðin", + "Wrap webhook payload in CloudEvents format for better interoperability": "Pakka farmur vefkróks í CloudEvents snið fyrir betri samvirkni", + "Wrap webhook payload in cloudevents format for better interoperability": "Pakka farmur vefkróks í cloudevents snið fyrir betri samvirkni", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: gildi\nAuthorization: Bearer tóken", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: gildi\\nAuthorization: Bearer tóken", + "Yes": "Já", + "You": "Þú", + "You are not yet a member of any organisations.": "Þú ert ekki enn meðlimur í neinum stofnunum.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Þú getur búið til nýtt ConfigSets byggt á _default sniðmáti, eða hlaðið upp sérsniðnum beint á SOLR þjóninn þinn.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Þú getur búið til nýtt configsets byggt á _default sniðmáti, eða hlaðið upp sérsniðnum beint á SOLR þjóninn þinn.", + "You do not have access to this conversation": "Þú hefur ekki aðgang að þessu samtali", + "You do not have permission to delete this conversation": "Þú hefur ekki leyfi til að eyða þessu samtali", + "You do not have permission to modify this conversation": "Þú hefur ekki leyfi til að breyta þessu samtali", + "You do not have permission to restore this conversation": "Þú hefur ekki leyfi til að endurheimta þetta samtal", + "You do not have permission to view the permission matrix. Admin access is required.": "Þú hefur ekki leyfi til að skoða leyfismatrixinn. Stjórnandaaðgangur er nauðsynlegur.", + "You have selected {count} properties to add to the schema.": "Þú hefur valið {count} eiginleika til að bæta við skemanu.", + "You must be logged in to favorite views": "Þú verður að vera innskráður til að geta valið eftirlætisyfirlit", + "You need an AI agent to start a conversation.": "Þú þarft AI umboðsmann til að hefja samtal.", + "Your Fireworks AI API key. Get one at": "Fireworks AI API lykillinn þinn. Fáðu einn á", + "Your OpenAI API key. Get one at": "OpenAI API lykillinn þinn. Fáðu einn á", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Gjöf þín hefur verið skráð. Valkvætt er að gefa viðbótarupplýsingar hér...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Gjöf þín hefur verið skráð. valkvætt er að gefa viðbótarupplýsingar hér...", + "Zookeeper Hosts": "Zookeeper hýslar", + "Zookeeper Port": "Zookeeper gátt", + "Zookeeper Settings (SolrCloud)": "Zookeeper stillingar (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper tengingastrengur fyrir SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper gáttarnúmer (valkvætt, sjálfgefið 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F VIÐVÖRUN UM VARANLEGA EYÐINGU \\u26A0\\uFE0F\n\nÞú ert að fara að VARANLEGA eyða ÖLLUM hlutum fyrir skema \"{schema}\":\n\n\\u2022 Virkir hlutir: {active}\n\\u2022 Mjúkt eyddir hlutir: {deleted}\n\\u2022 Heildartala: {total}\n\nÞessir hlutir verða að fullu fjarlægðir úr gagnagrunninum og ER EKKI HÆGT AÐ ENDURHEIMTA þá.\n\nErtu algerlega viss?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F VIÐVÖRUN UM VARANLEGA EYÐINGU \\u26A0\\uFE0F\n\nÞú ert að fara að VARANLEGA eyða {count} mjúkt eyddum hlutum fyrir skema \"{schema}\".\n\nÞessir hlutir verða að fullu fjarlægðir úr gagnagrunninum og ER EKKI HÆGT AÐ ENDURHEIMTA þá.\n\nErtu algerlega viss?", + "and accessible to all users in selected groups (or all users if no groups selected).": "og aðgengilegt öllum notendum í völdum hópum (eða öllum notendum ef engir hópar valdir).", + "and add the files there.": "og bættu skrárnar við þar.", + "approved": "samþykkt", + "are only accessible to invited users.": "eru aðeins aðgengilegar boðnum notendum.", + "by": "af", + "chunk": "bútur", + "chunks": "bútar", + "chunks processed": "bútar unnir", + "completed": "lokið", + "configuration(s)": "stilling(ar)", + "conversationId is required": "conversationId er nauðsynlegt", + "create": "búa til", + "delete": "eyða", + "deleted objects section": "hluti eyddra hluta", + "e.g., lib/Settings/config.json": "t.d., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "t.d., lib/Settings/register.json", + "e.g., lib/settings/config.json": "t.d., lib/settings/config.json", + "e.g., lib/settings/register.json": "t.d., lib/settings/register.json", + "execute": "keyra", + "failed": "misheppnuðust", + "file": "skrá", + "files": "skrár", + "file{plural}": "skrá{plural}", + "folder": "mappa", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "í {register}", + "in_progress": "í gangi", + "inherited": "erft", + "list": "listi", + "log{plural}": "skráning{plural}", + "manage": "stjórna", + "members": "meðlimir", + "menu or contact someone with permission to create agents.": "valmyndinni eða hafðu samband við einhvern með leyfi til að búa til umboðsmenn.", + "message content is required": "innihald skilaboða er nauðsynlegt", + "messages": "skilaboð", + "missing": "vantar", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL og API lykill eru nauðsynleg", + "n8n configuration saved successfully": "n8n stilling vistuð", + "n8n connection not configured": "n8n tenging ekki stillt", + "n8n connection successful": "n8n tenging tókst", + "n8n connection test failed: {message}": "n8n tengingaprófun mistókst: {message}", + "n8n connection test successful": "n8n tengingaprófun tókst", + "n8n initialization failed: {message}": "n8n frumstilling misheppnaðist: {message}", + "n8n integration disabled": "n8n samþætting óvirkjuð", + "n8n integration enabled": "n8n samþætting virkjuð", + "n8n project initialized successfully": "n8n verkefni frumstillt", + "n8n settings saved successfully": "n8n stillingar vistaðar", + "notify_push configuration guide": "leiðbeiningar um notify_push stillingu", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push er uppsett en OpenRegister hefur ekki enn staðfest farsælan ýti. Kveiktu á hlutavistun til að virkja, eða athugaðu notify_push stillinguna þína.", + "notify_push is installed but not yet active": "notify_push er uppsett en ekki enn virkt", + "objectType: object\naction: created": "objectType: hlutur\naction: búinn til", + "objectType: object\\naction: created": "objectType: hlutur\\naction: búinn til", + "objects": "hlutir", + "objects processed": "hlutir unnir", + "object{plural}": "hlutur{plural}", + "or pick a range": "eða veldu bil", + "org-...": "org-...", + "pending": "í bið", + "provides a good balance between speed and accuracy.": "veitir góða jafnvægi milli hraða og nákvæmni.", + "public": "opinbert", + "read": "lesa", + "register(s)": "skrá(r)", + "register(s) selected": "skrá(r) valin", + "register{plural}": "skrá{plural}", + "rejected": "hafnað", + "required": "nauðsynlegt", + "results": "niðurstöður", + "schema(s)": "skema(r)", + "schema(s) selected": "skema(r) valin", + "schema{plural}": "skema{plural}", + "selected objects": "valdir hlutir", + "set": "stilla", + "sk-...": "sk-...", + "this application": "þetta forrit", + "to": "til", + "type must be \"positive\" or \"negative\"": "tegund verður að vera \"positive\" eða \"negative\"", + "update": "uppfæra", + "vectorized": "vektóraður", + "view": "yfirlit", + "views selected": "yfirlit valin", + "waiting": "í bið", + "{active} active, {deleted} deleted": "{active} virk, {deleted} eytt", + "{count} audit trails cleared successfully": "{count} endurskoðunarferslur hreinsaðar", + "{count} compliance issue(s) detected": "{count} fylgnivandamál greind", + "{count} deletions": "{count} eyðingar", + "{count} documents found": "{count} skjöl fundin", + "{count} email": "{count} tölvupóstur", + "{count} emails": "{count} tölvupóstar", + "{count} entries": "{count} færslur", + "{count} issues": "{count} vandamál", + "{count} members": "{count} meðlimir", + "{count} searches": "{count} leitir", + "{count} selected": "{count} valin", + "{count} widget(s)": "{count} gluggi(ar)", + "{matched} object(s) matched. {erased} erased.": "{matched} hlutur/hlutir fundinn/fundnir. {erased} eytt.", + "{minutes} minutes ago": "fyrir {minutes} mínútum", + "{percentage}% of objects": "{percentage}% hluta", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} í {register} / {schema}", + "{type} cache cleared successfully": "{type} skyndiminni hreinsað", + "ℹ️ Current Configuration": "ℹ️ Núverandi stilling", + "ℹ️ current configuration": "ℹ️ núverandi stilling", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ HÆ TTA: Allir {total} hlutir verða VARANLEGA EYDDIR úr gagnagrunninum. Þessa aðgerð er ÓENDURHEIMTANLEG og er ekki hægt að afturkalla!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ VIÐVÖRUN UM VARANLEGA EYÐINGU ⚠️\n\nÞú ert að fara að VARANLEGA eyða ÖLLUM hlutum fyrir skema \"{schema}\":\n\n• Virkir hlutir: {active}\n• Mjúkt eyddir hlutir: {deleted}\n• Heildartala: {total}\n\nÞessir hlutir verða að fullu fjarlægðir úr gagnagrunninum og ER EKKI HÆGT AÐ ENDURHEIMTA þá.\n\nErtu algerlega viss?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ VIÐVÖRUN UM VARANLEGA EYÐINGU ⚠️\n\nÞú ert að fara að VARANLEGA eyða {count} mjúkt eyddum hlutum fyrir skema \"{schema}\".\n\nÞessir hlutir verða að fullu fjarlægðir úr gagnagrunninum og ER EKKI HÆGT AÐ ENDURHEIMTA þá.\n\nErtu algerlega viss?", + "⚡ Batch Processing": "⚡ Lotuúrvinnsla", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR getur meðhöndlað milljónir skjala með leitarsvartíma undir sekúndu...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Yfirgripsmikið samhengi ✓ Minna líklegt til að missa af upplýsingum ✗ Hægari svör ✗ Gæti innihaldið minna viðeigandi upplýsingar", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Hraðari svör ✓ Meira einbeittar svör ✗ Gæti misst af viðeigandi upplýsingum", + "✨ AI Features": "✨ AI eiginleikar", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Í dreifðum ham, notar SOLR ZooKeeper fyrir samhæfingu þyrpinga...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Hliðarleit leyfir notendum að kafa ofan í niðurstöður eftir flokkum...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Vissir þú? SOLR knýr leit fyrir Netflix, Apple og margar aðrar helstu síður!", + "💰 View Selection (Cost Optimization)": "💰 Val yfirlits (kostnaðarbestun)", + "📄 Object Serialization": "📄 Raðsetning hluta", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR veitir ríkar greiningar og tölfræði um leitarframmistöðu...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets innihalda skema og stillingarskrár fyrir leitarskrána þína...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Fjölgun tryggir að leitarskráin þín er tiltæk jafnvel þótt hnútar bilni...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR er öflug leitarvettvangur fyrir fyrirtæki byggður á Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Stillingar vektórfærslu", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Sjálfvirk kvörðun getur bætt við eða fjarlægt hnúta gangvirkt eftir álagi...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Öryggiseiginleikar innihalda auðkenningu, heimild og SSL dulkóðun...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} tölvupóstur", + "{count} tölvupóstar" + ], + "Purge {count} object from database": [ + "Hreinsa {count} hlut úr gagnagrunni", + "Hreinsa {count} hluti úr gagnagrunni" + ], + "Restore {count} object": [ + "Endurheimta {count} hlut", + "Endurheimta {count} hluti" + ], + "Successfully restored {count} object": [ + "{count} hlutur endurheimtur", + "{count} hlutir endurheimtir" + ], + "Delete {count} object": [ + "Eyða {count} hlut", + "Eyða {count} hlutum" + ], + "Object successfully deleted": [ + "Hlutur eytt", + "Hlutir eyddir" + ] + } +} diff --git a/l10n/it.js b/l10n/it.js new file mode 100644 index 0000000000..28bb18a431 --- /dev/null +++ b/l10n/it.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(nessun oggetto)", + "(no title)" : "(nessun titolo)", + "(unnamed)" : "(senza nome)", + "(untitled card)" : "(scheda senza titolo)", + "Activities" : "Attività", + "Add Endpoint" : "Aggiungi endpoint", + "Add Source" : "Aggiungi origine", + "Add card" : "Aggiungi scheda", + "Add contact" : "Aggiungi contatto", + "All clear" : "Tutto a posto", + "Articles" : "Articoli", + "Code" : "Codice", + "Compliance" : "Conformità", + "Contacts" : "Contatti", + "Create Agent" : "Crea agente", + "Create Application" : "Crea applicazione", + "Create event" : "Crea evento", + "Erasure complete" : "Cancellazione completata", + "Erasure preview" : "Anteprima cancellazione", + "Expand details" : "Espandi dettagli", + "Save changes" : "Salva modifiche", + "📄 Object Serialization" : "📄 Serializzazione oggetto", + "🔢 Vectorization Settings" : "🔢 Impostazioni di vettorizzazione", + "💰 View Selection (Cost Optimization)" : "💰 Selezione vista (ottimizzazione costi)", + "AI Assistant" : "Assistente IA", + "AI Chat" : "Chat IA", + "API Key" : "Chiave API", + "API tokens saved successfully" : "Token API salvati correttamente", + "About ConfigSets" : "Informazioni sui ConfigSet", + "Action" : "Azione", + "Action Distribution" : "Distribuzione delle azioni", + "Action:" : "Azione:", + "Actions" : "Azioni", + "Active" : "Attivo", + "Active Collections" : "Raccolte attive", + "Active Filters" : "Filtri attivi", + "Active Organisations" : "Organizzazioni attive", + "Active filters:" : "Filtri attivi:", + "Active organisation set successfully" : "Organizzazione attiva impostata correttamente", + "Activity Period" : "Periodo di attività", + "Add Groups" : "Aggiungi gruppi", + "Add Schema" : "Aggiungi schema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Aggiungi titoli di schema, descrizioni e informazioni di registro per fornire un contesto più ricco alla ricerca", + "Add to favorites" : "Aggiungi ai preferiti", + "Added" : "Aggiunto", + "Added to favorites" : "Aggiunto ai preferiti", + "Additional Information" : "Informazioni aggiuntive", + "Additional feedback saved. Thank you!" : "Feedback aggiuntivo salvato. Grazie!", + "Admin" : "Amministratore", + "Advanced" : "Avanzate", + "Advanced Filters" : "Filtri avanzati", + "Advanced Options" : "Opzioni avanzate", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "I filtri avanzati non sono disponibili quando si utilizza l'origine database. Passare a Auto o all'indice SOLR per le opzioni di filtro.", + "After" : "Dopo", + "Agent deleted successfully" : "Agente eliminato correttamente", + "Agents" : "Agenti", + "All" : "Tutti", + "All Categories" : "Tutte le categorie", + "All Collections" : "Tutte le raccolte", + "All Confidence Levels" : "Tutti i livelli di confidenza", + "All ConfigSets" : "Tutti i ConfigSet", + "All Files" : "Tutti i file", + "All Levels" : "Tutti i livelli", + "All Types" : "Tutti i tipi", + "All Webhooks" : "Tutti i webhook", + "All actions" : "Tutte le azioni", + "All audit trails cleared successfully" : "Tutti i registri di controllo cancellati correttamente", + "All registers" : "Tutti i registri", + "All schemas" : "Tutti gli schemi", + "All searches" : "Tutte le ricerche", + "All users" : "Tutti gli utenti", + "An error occurred while clearing audit trails" : "Si è verificato un errore durante la cancellazione dei registri di controllo", + "An error occurred while deleting the audit trail" : "Si è verificato un errore durante l'eliminazione del registro di controllo", + "An error occurred while deleting the view" : "Si è verificato un errore durante l'eliminazione della vista", + "An error occurred while permanently deleting the objects" : "Si è verificato un errore durante l'eliminazione permanente degli oggetti", + "Analysis completed:" : "Analisi completata:", + "Analysis steps:" : "Passaggi dell'analisi:", + "Analytics" : "Analisi", + "Analyze Objects" : "Analizza oggetti", + "Analyze Properties" : "Analizza proprietà", + "Analyze existing properties for improvement opportunities" : "Analizza le proprietà esistenti per individuare opportunità di miglioramento", + "Analyzing..." : "Analisi in corso...", + "Any favorites and sharing settings for this view" : "Eventuali preferiti e impostazioni di condivisione per questa vista", + "Any user" : "Qualsiasi utente", + "App store cache invalidated successfully" : "Cache dell'App Store invalidata correttamente", + "Application deleted successfully" : "Applicazione eliminata correttamente", + "Applications" : "Applicazioni", + "Apply Changes" : "Applica modifiche", + "Archive" : "Archivia", + "Archive conversation" : "Archivia conversazione", + "Archived conversations are hidden from your active list" : "Le conversazioni archiviate sono nascoste dall'elenco attivo", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Pulire i vecchi registri di ricerca? Questo eliminerà le voci più vecchie di 30 giorni.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Eliminare i registri di controllo selezionati? Questa azione non può essere annullata.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Eliminare i registri di ricerca selezionati? Questa azione non può essere annullata.", + "Are you sure you want to delete this ConfigSet?" : "Eliminare questo ConfigSet?", + "Are you sure you want to permanently delete" : "Eliminare definitivamente", + "Ask a question..." : "Poni una domanda...", + "Ask questions about your data using natural language" : "Poni domande sui tuoi dati utilizzando il linguaggio naturale", + "Attempt" : "Tentativo", + "Audit Trail Changes" : "Modifiche del registro di controllo", + "Audit Trail Details" : "Dettagli del registro di controllo", + "Audit Trail Management" : "Gestione del registro di controllo", + "Audit Trail Statistics" : "Statistiche del registro di controllo", + "Audit Trails" : "Registri di controllo", + "Audit trail data copied to clipboard" : "Dati del registro di controllo copiati negli appunti", + "Audit trail deleted successfully" : "Registro di controllo eliminato correttamente", + "Audit trail successfully deleted" : "Registro di controllo eliminato correttamente", + "Authentication" : "Autenticazione", + "Auto-commit disabled" : "Commit automatico disabilitato", + "Auto-commit enabled" : "Commit automatico abilitato", + "Auto-create Default Organisation" : "Crea automaticamente organizzazione predefinita", + "Auto-retry failed vectorizations" : "Riprova automaticamente le vettorizzazioni non riuscite", + "Automatically commit changes to SOLR index" : "Esegui automaticamente il commit delle modifiche all'indice SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Crea automaticamente un'organizzazione predefinita se non ne esiste alcuna all'inizializzazione dell'app", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Genera automaticamente embedding vettoriali dai blocchi di testo quando i file vengono caricati ed elaborati", + "Automatically generate vector embeddings when objects are created or updated" : "Genera automaticamente embedding vettoriali quando gli oggetti vengono creati o aggiornati", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Riprova automaticamente i tentativi di vettorizzazione non riusciti (massimo 3 tentativi)", + "Available Filters" : "Filtri disponibili", + "Available Workflows" : "Flussi di lavoro disponibili", + "Avg Execution Time" : "Tempo medio di esecuzione", + "Avg Members/Org" : "Membri medi/org", + "Avg Object Views/Session" : "Visualizzazioni medie oggetti/sessione", + "Avg Response Time" : "Tempo medio di risposta", + "Avg Results/Search" : "Risultati medi/ricerca", + "Avg Searches/Session" : "Ricerche medie/sessione", + "Back" : "Indietro", + "Back to Entities" : "Torna alle entità", + "Back to Registers" : "Torna ai registri", + "Back to Webhooks" : "Torna ai webhook", + "Back to applications" : "Torna alle applicazioni", + "Back to entities" : "Torna alle entità", + "Backend updated successfully. Please reload the application." : "Backend aggiornato correttamente. Ricaricare l'applicazione.", + "Base URL (Optional)" : "URL di base (facoltativo)", + "Basic Information" : "Informazioni di base", + "Batch Size" : "Dimensione del lotto", + "Batch extraction completed" : "Estrazione in blocco completata", + "Before" : "Prima", + "Before object vectorization can work:" : "Prima che la vettorizzazione degli oggetti possa funzionare:", + "Behavior Issue" : "Problema di comportamento", + "Blob storage has been retired. All objects now use magic tables." : "L'archiviazione blob è stata dismessa. Tutti gli oggetti ora utilizzano le tabelle magiche.", + "Branch" : "Ramo", + "Bulk delete operation completed successfully" : "Operazione di eliminazione in blocco completata correttamente", + "Bulk save operation completed successfully" : "Operazione di salvataggio in blocco completata correttamente", + "Business Data" : "Dati aziendali", + "Cache cleared successfully" : "Cache svuotata correttamente", + "Calculate Sizes" : "Calcola dimensioni", + "Cancel" : "Annulla", + "Cannot delete: objects are still attached" : "Impossibile eliminare: ci sono ancora oggetti collegati", + "Category" : "Categoria", + "Change Type" : "Tipo di modifica", + "Changes" : "Modifiche", + "Changes copied to clipboard" : "Modifiche copiate negli appunti", + "Chat Model" : "Modello di chat", + "Chat Provider" : "Fornitore di chat", + "Chat Provider (RAG)" : "Fornitore di chat (RAG)", + "Chat Settings" : "Impostazioni della chat", + "Chat provider connection successful!" : "Connessione al fornitore di chat riuscita!", + "Chat settings" : "Impostazioni della chat", + "Choose a register" : "Scegli un registro", + "Choose a schema" : "Scegli uno schema", + "Choose how vector similarity calculations are performed for semantic search" : "Scegli come vengono eseguiti i calcoli di similarità vettoriale per la ricerca semantica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Scegli quali tipi di file includere nel processo di vettorizzazione. Verranno elaborati solo i file con testo e blocchi estratti.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Scegli quali viste includere nel processo di vettorizzazione. Lasciare vuoto per elaborare tutte le viste in base alla configurazione.", + "Chunk" : "Blocco", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Eliminazione dei blocchi non ancora implementata. Utilizzare gli endpoint basati sui blocchi.", + "Chunks" : "Blocchi", + "Chunks to Vectorize:" : "Blocchi da vettorizzare:", + "Cleanup Old Trails" : "Pulisci vecchi registri", + "Cleanup completed" : "Pulizia completata", + "Clear All Embeddings" : "Cancella tutti gli embedding", + "Clear Entries" : "Cancella voci", + "Clear Filtered Audit Trails" : "Cancella registri di controllo filtrati", + "Clear Filters" : "Cancella filtri", + "Clear Index" : "Cancella indice", + "Clear Selection" : "Cancella selezione", + "Clear all" : "Cancella tutto", + "Clear all filters" : "Cancella tutti i filtri", + "Clear date range" : "Cancella intervallo di date", + "Clearing..." : "Cancellazione in corso...", + "Close" : "Chiudi", + "Collection Management" : "Gestione raccolte", + "Collection Name" : "Nome raccolta", + "Collection assignments updated successfully" : "Assegnazioni delle raccolte aggiornate correttamente", + "Collection cleared successfully" : "Raccolta cancellata correttamente", + "Collection copied successfully" : "Raccolta copiata correttamente", + "Collection created successfully" : "Raccolta creata correttamente", + "Collection deleted successfully" : "Raccolta eliminata correttamente", + "Collection used to store and index file metadata and content" : "Raccolta utilizzata per archiviare e indicizzare i metadati e il contenuto dei file", + "Collection used to store and index object data" : "Raccolta utilizzata per archiviare e indicizzare i dati degli oggetti", + "Collections:" : "Raccolte:", + "Commit Message" : "Messaggio di commit", + "Commit Within (ms)" : "Commit entro (ms)", + "Commit message" : "Messaggio di commit", + "Compare current schema with real object data" : "Confronta lo schema corrente con i dati reali degli oggetti", + "Completed" : "Completato", + "Complex" : "Complesso", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Query complesse: ricerche avanzate con più filtri, operatori e combinazioni di parametri complesse", + "Confidence Level" : "Livello di confidenza", + "Confidence Score" : "Punteggio di confidenza", + "Config must be provided as an object" : "La configurazione deve essere fornita come oggetto", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Gestione ConfigSet", + "ConfigSet Name" : "Nome ConfigSet", + "ConfigSet created successfully" : "ConfigSet creato correttamente", + "ConfigSet deleted successfully" : "ConfigSet eliminato correttamente", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "I ConfigSet definiscono lo schema e la configurazione delle raccolte SOLR. Contengono definizioni di campi, analizzatori e altre impostazioni di ricerca.", + "Configuration" : "Configurazione", + "Configuration Data" : "Dati di configurazione", + "Configuration saved" : "Configurazione salvata", + "Configurations" : "Configurazioni", + "Configure Facets" : "Configura faccette", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Configura i fornitori di Large Language Model (LLM) per le funzionalità basate sull'IA, tra cui ricerca semantica, embedding e chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Configura le impostazioni di connessione di base per il server SOLR, comprese le opzioni di autenticazione e di rete. Utilizzare le finestre di dialogo separate di gestione ConfigSet e raccolte per gestire core e raccolte.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Configura come gli oggetti del database vengono convertiti in embedding vettoriali per la ricerca semantica. Gli oggetti vengono vettorizzati direttamente senza necessità di estrazione del testo.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Configura come gli oggetti vengono convertiti in testo prima della vettorizzazione. Queste impostazioni influiscono sulla qualità e sul contesto della ricerca.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Configura i parametri per la vettorizzazione dei file. Questo processo genererà embedding vettoriali per tutti i blocchi di file estratti.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Configura i parametri per la vettorizzazione degli oggetti. Questo processo genererà embedding vettoriali per tutti gli oggetti che corrispondono ai filtri della vista.", + "Configure which types of data to search and how many sources to retrieve" : "Configura quali tipi di dati cercare e quante origini recuperare", + "Connection Failed" : "Connessione non riuscita", + "Connection Settings" : "Impostazioni di connessione", + "Connection Successful!" : "Connessione riuscita!", + "Connection failed" : "Connessione non riuscita", + "Connection protocol" : "Protocollo di connessione", + "Connection successful" : "Connessione riuscita", + "Connection timeout in seconds" : "Timeout di connessione in secondi", + "Constant delay between retries (5 minutes)" : "Ritardo costante tra i tentativi (5 minuti)", + "Constraint Issue" : "Problema di vincolo", + "Content Filters" : "Filtri del contenuto", + "Continue to Properties" : "Continua con le proprietà", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Controlla quali viste oggetto devono essere vettorizzate per ridurre i costi delle API. Vettorizza solo le viste che traggono vantaggio dalla ricerca semantica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Controlla quali viste e strumenti l'IA può utilizzare in questa conversazione. Per impostazione predefinita, tutte le capacità dell'agente sono abilitate.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Controlla la casualità (0 = deterministico, 2 = molto creativo)", + "Conversation ID is required" : "L'ID della conversazione è obbligatorio", + "Conversation archived" : "Conversazione archiviata", + "Conversation archived successfully" : "Conversazione archiviata correttamente", + "Conversation cleared successfully" : "Conversazione cancellata correttamente", + "Conversation deleted" : "Conversazione eliminata", + "Conversation permanently deleted" : "Conversazione eliminata definitivamente", + "Conversation renamed" : "Conversazione rinominata", + "Conversation restored" : "Conversazione ripristinata", + "Conversation title" : "Titolo della conversazione", + "Copied!" : "Copiato!", + "Copy" : "Copia", + "Copy Changes" : "Copia modifiche", + "Copy Collection" : "Copia raccolta", + "Copy Data" : "Copia dati", + "Copy Full Data" : "Copia tutti i dati", + "Copying..." : "Copia in corso...", + "Count" : "Conteggio", + "Counting objects..." : "Conteggio degli oggetti in corso...", + "Costs" : "Costi", + "Create" : "Crea", + "Create Collection" : "Crea raccolta", + "Create ConfigSet" : "Crea ConfigSet", + "Create New Collection" : "Crea nuova raccolta", + "Create New ConfigSet" : "Crea nuovo ConfigSet", + "Create Operations" : "Operazioni di creazione", + "Create Webhook" : "Crea webhook", + "Create a copy of collection:" : "Crea una copia della raccolta:", + "Create a new ConfigSet based on the _default template" : "Crea un nuovo ConfigSet basato sul modello _default", + "Create a new SOLR collection from an existing ConfigSet" : "Crea una nuova raccolta SOLR da un ConfigSet esistente", + "Create your first AI agent to get started." : "Crea il tuo primo agente IA per iniziare.", + "Created" : "Creato", + "Created:" : "Creato:", + "Creating..." : "Creazione in corso...", + "Current Filters" : "Filtri attuali", + "Current Type:" : "Tipo attuale:", + "Custom API endpoint if using a different region" : "Endpoint API personalizzato se si utilizza una regione diversa", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Intestazioni HTTP personalizzate (una per riga, formato: Nome-Intestazione: valore)", + "Daily" : "Giornaliero", + "Dashboard" : "Pannello di controllo", + "Data Source" : "Origine dati", + "Data sources" : "Origini dati", + "Data type does not match observed values" : "Il tipo di dato non corrisponde ai valori osservati", + "Database URL" : "URL del database", + "Database information refreshed" : "Informazioni del database aggiornate", + "Date Range" : "Intervallo di date", + "Default" : "Predefinito", + "Default Organisation" : "Organizzazione predefinita", + "Default: 5" : "Predefinito: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "I ritardi raddoppiano a ogni tentativo (2, 4, 8 minuti...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "I ritardi aumentano linearmente (5, 10, 15 minuti...)", + "Delete" : "Elimina", + "Delete Application" : "Elimina applicazione", + "Delete Audit Trail" : "Elimina registro di controllo", + "Delete Collection" : "Elimina raccolta", + "Delete ConfigSet" : "Elimina ConfigSet", + "Delete Objects" : "Elimina oggetti", + "Delete Operations" : "Operazioni di eliminazione", + "Delete View" : "Elimina vista", + "Delete all objects in this schema" : "Elimina tutti gli oggetti in questo schema", + "Delete permanently" : "Elimina definitivamente", + "Delete this conversation?" : "Eliminare questa conversazione?", + "Delete view" : "Elimina vista", + "Deleted" : "Eliminato", + "Deleted By" : "Eliminato da", + "Deleted Date" : "Data di eliminazione", + "Deleted Items Management" : "Gestione elementi eliminati", + "Deleted This Week" : "Eliminati questa settimana", + "Deleted Today" : "Eliminati oggi", + "Deleted:" : "Eliminato:", + "Deleting..." : "Eliminazione in corso...", + "Deletion Date Range" : "Intervallo date di eliminazione", + "Deletion Statistics" : "Statistiche di eliminazione", + "Deprecated" : "Obsoleto", + "Description" : "Descrizione", + "Description:" : "Descrizione:", + "Detect data types and patterns" : "Rileva tipi di dati e schemi", + "Detected At" : "Rilevato il", + "Detected Format:" : "Formato rilevato:", + "Detected Issues:" : "Problemi rilevati:", + "Disable" : "Disabilita", + "Disabled" : "Disabilitato", + "Discover Files" : "Scopri file", + "Discovered Properties" : "Proprietà scoperte", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Eliminare definitivamente tutte le voci del registro di controllo filtrate? Questa azione non può essere annullata.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Eliminare definitivamente questa voce del registro di controllo? Questa azione non può essere annullata.", + "Documents" : "Documenti", + "Download API Spec" : "Scarica specifica API", + "Edit" : "Modifica", + "Edit Register" : "Modifica registro", + "Edit Schema" : "Modifica schema", + "Edit View" : "Modifica vista", + "Edit Webhook" : "Modifica webhook", + "Edit view" : "Modifica vista", + "Edit view details" : "Modifica dettagli vista", + "Effectiveness" : "Efficacia", + "Email" : "E-mail", + "Emails" : "E-mail", + "Embedding Model" : "Modello di embedding", + "Embedding Provider" : "Fornitore di embedding", + "Embedding generated successfully" : "Embedding generato correttamente", + "Embedding provider connection successful!" : "Connessione al fornitore di embedding riuscita!", + "Enable" : "Abilita", + "Enable auto-creation" : "Abilita creazione automatica", + "Enable automatic file vectorization" : "Abilita vettorizzazione automatica dei file", + "Enable automatic object vectorization" : "Abilita vettorizzazione automatica degli oggetti", + "Enable automatic synchronization" : "Abilita sincronizzazione automatica", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Abilita la registrazione dettagliata per le operazioni SOLR (consigliato per il debug)", + "Enable faceting" : "Abilita faccette", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Abilita o disabilita le funzionalità LLM. Configura fornitori e modelli utilizzando il pulsante Configurazione LLM in alto.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Abilita o disabilita l'integrazione della ricerca SOLR. Configura le impostazioni di connessione utilizzando il pulsante Impostazioni di connessione in alto.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Abilita o disabilita l'integrazione del flusso di lavoro n8n. Configura le impostazioni di connessione di seguito.", + "Enable or disable this webhook" : "Abilita o disabilita questo webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Abilita la vettorizzazione per tutte le viste esistenti e future (può aumentare i costi)", + "Enabled" : "Abilitato", + "Endpoints" : "Endpoint", + "Enter ConfigSet name" : "Inserisci il nome del ConfigSet", + "Enter collection name" : "Inserisci il nome della raccolta", + "Enter description (optional)..." : "Inserisci una descrizione (facoltativa)...", + "Enter new collection name" : "Inserisci il nuovo nome della raccolta", + "Enter object ID" : "Inserisci l'ID dell'oggetto", + "Enter search term" : "Inserisci il termine di ricerca", + "Enter view name..." : "Inserisci il nome della vista...", + "Enter webhook name" : "Inserisci il nome del webhook", + "Entities" : "Entità", + "Entity Information" : "Informazioni sull'entità", + "Entity deleted successfully" : "Entità eliminata correttamente", + "Entity not found" : "Entità non trovata", + "Entries to be deleted:" : "Voci da eliminare:", + "Enum Issue" : "Problema di enum", + "Enum constraint is missing" : "Il vincolo enum è mancante", + "Error" : "Errore", + "Error Details" : "Dettagli dell'errore", + "Error Information" : "Informazioni sull'errore", + "Error Message" : "Messaggio di errore", + "Error loading application" : "Errore nel caricamento dell'applicazione", + "Error loading audit trails" : "Errore nel caricamento dei registri di controllo", + "Error loading entity" : "Errore nel caricamento dell'entità", + "Error loading search trails" : "Errore nel caricamento dei registri di ricerca", + "Estimated Batches:" : "Lotti stimati:", + "Estimated Cost:" : "Costo stimato:", + "Estimated Duration:" : "Durata stimata:", + "Event" : "Evento", + "Event Property for Payload" : "Proprietà dell'evento per il payload", + "Events" : "Eventi", + "Example Value" : "Valore di esempio", + "Exclusive Maximum" : "Massimo esclusivo", + "Exclusive Minimum" : "Minimo esclusivo", + "Execution Mode" : "Modalità di esecuzione", + "Execution Time" : "Tempo di esecuzione", + "Execution Time Range" : "Intervallo del tempo di esecuzione", + "Existing Improvements" : "Miglioramenti esistenti", + "Exponential" : "Esponenziale", + "Export" : "Esporta", + "Export completed successfully" : "Esportazione completata correttamente", + "Export, view, or delete audit trails" : "Esporta, visualizza o elimina registri di controllo", + "Extend Schema" : "Estendi schema", + "Extended" : "Esteso", + "Extensions" : "Estensioni", + "Extract Pending Files" : "Estrai file in sospeso", + "Extract properties from each object" : "Estrai le proprietà da ogni oggetto", + "Extracted At" : "Estratto il", + "Extraction Status" : "Stato dell'estrazione", + "Facet configuration updated successfully" : "Configurazione delle faccette aggiornata correttamente", + "Facets discovered and configured successfully" : "Faccette scoperte e configurate correttamente", + "Facets discovered successfully" : "Faccette scoperte correttamente", + "Failed" : "Non riuscito", + "Failed to analyze schema properties" : "Impossibile analizzare le proprietà dello schema", + "Failed to archive conversation" : "Impossibile archiviare la conversazione", + "Failed to calculate sizes" : "Impossibile calcolare le dimensioni", + "Failed to clear collection" : "Impossibile cancellare la raccolta", + "Failed to copy changes" : "Impossibile copiare le modifiche", + "Failed to copy data" : "Impossibile copiare i dati", + "Failed to copy data to clipboard" : "Impossibile copiare i dati negli appunti", + "Failed to create conversation" : "Impossibile creare la conversazione", + "Failed to create or find project" : "Impossibile creare o trovare il progetto", + "Failed to delete collection" : "Impossibile eliminare la raccolta", + "Failed to delete conversation" : "Impossibile eliminare la conversazione", + "Failed to delete webhook" : "Impossibile eliminare il webhook", + "Failed to download API specification" : "Impossibile scaricare la specifica API", + "Failed to get SOLR field configuration" : "Impossibile ottenere la configurazione dei campi SOLR", + "Failed to get entity categories" : "Impossibile ottenere le categorie delle entità", + "Failed to get entity statistics" : "Impossibile ottenere le statistiche delle entità", + "Failed to get entity types" : "Impossibile ottenere i tipi di entità", + "Failed to load LLM configuration" : "Impossibile caricare la configurazione LLM", + "Failed to load Nextcloud groups" : "Impossibile caricare i gruppi Nextcloud", + "Failed to load conversation" : "Impossibile caricare la conversazione", + "Failed to load entities" : "Impossibile caricare le entità", + "Failed to load entity" : "Impossibile caricare l'entità", + "Failed to load files" : "Impossibile caricare i file", + "Failed to load organisations" : "Impossibile caricare le organizzazioni", + "Failed to load templates" : "Impossibile caricare i modelli", + "Failed to load webhooks" : "Impossibile caricare i webhook", + "Failed to load workflows" : "Impossibile caricare i flussi di lavoro", + "Failed to refresh database information" : "Impossibile aggiornare le informazioni del database", + "Failed to reindex collection" : "Impossibile reindicizzare la raccolta", + "Failed to rename conversation" : "Impossibile rinominare la conversazione", + "Failed to restore conversation" : "Impossibile ripristinare la conversazione", + "Failed to retry extraction" : "Impossibile riprovare l'estrazione", + "Failed to retry webhook" : "Impossibile riprovare il webhook", + "Failed to save GitHub token" : "Impossibile salvare il token GitHub", + "Failed to save GitLab URL" : "Impossibile salvare l'URL GitLab", + "Failed to save GitLab token" : "Impossibile salvare il token GitLab", + "Failed to save additional feedback" : "Impossibile salvare il feedback aggiuntivo", + "Failed to save n8n configuration" : "Impossibile salvare la configurazione n8n", + "Failed to save roles" : "Impossibile salvare i ruoli", + "Failed to save settings" : "Impossibile salvare le impostazioni", + "Failed to save webhook" : "Impossibile salvare il webhook", + "Failed to send feedback" : "Impossibile inviare il feedback", + "Failed to test webhook" : "Impossibile testare il webhook", + "Failed to update favorite status" : "Impossibile aggiornare lo stato dei preferiti", + "Failed to update schema properties" : "Impossibile aggiornare le proprietà dello schema", + "Failed to update view" : "Impossibile aggiornare la vista", + "Failed to update webhook" : "Impossibile aggiornare il webhook", + "Feedback recorded" : "Feedback registrato", + "Fewer sources (1-3):" : "Meno origini (1-3):", + "Field" : "Campo", + "File" : "File", + "File Chunk Prediction" : "Previsione dei blocchi di file", + "File Collection" : "Raccolta di file", + "File Management" : "Gestione file", + "File Name" : "Nome file", + "File Path" : "Percorso file", + "File Type Selection" : "Selezione del tipo di file", + "File Vectorization" : "Vettorizzazione dei file", + "File Warmup" : "Riscaldamento dei file", + "File actions menu" : "Menu azioni file", + "File anonymized successfully" : "File anonimizzato correttamente", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "La vettorizzazione dei blocchi di file non è ancora implementata. I blocchi sono pronti e archiviati, ma il servizio di vettorizzazione è in fase di sviluppo.", + "File collection not configured" : "Raccolta di file non configurata", + "File discovery completed" : "Individuazione dei file completata", + "File extraction completed" : "Estrazione dei file completata", + "File extraction queued" : "Estrazione dei file in coda", + "File indexed successfully" : "File indicizzato correttamente", + "File is already anonymized" : "Il file è già anonimizzato", + "File not found" : "File non trovato", + "File settings updated successfully" : "Impostazioni dei file aggiornate correttamente", + "File sources" : "Origini dei file", + "File vectorization configuration saved successfully" : "Configurazione della vettorizzazione dei file salvata correttamente", + "File vectorization started. Check the statistics section for progress." : "Vettorizzazione dei file avviata. Controlla la sezione statistiche per l'avanzamento.", + "File warmup completed" : "Riscaldamento dei file completato", + "Files" : "File", + "Files with Completed Extraction:" : "File con estrazione completata:", + "Files → fileCollection, Objects → objectCollection" : "File → fileCollection, Oggetti → objectCollection", + "Filter Audit Trails" : "Filtra registri di controllo", + "Filter Deleted Items" : "Filtra elementi eliminati", + "Filter Objects" : "Filtra oggetti", + "Filter Properties" : "Filtra proprietà", + "Filter Search Trails" : "Filtra registri di ricerca", + "Filter Statistics" : "Filtra statistiche", + "Filter and analyze search trail entries" : "Filtra e analizza le voci del registro di ricerca", + "Filter and manage audit trail entries" : "Filtra e gestisci le voci del registro di controllo", + "Filter and manage soft deleted items" : "Filtra e gestisci gli elementi eliminati in modo reversibile", + "Filter by object ID" : "Filtra per ID oggetto", + "Filter by search term" : "Filtra per termine di ricerca", + "Filter by webhook" : "Filtra per webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Dati dei filtri caricati automaticamente. Utilizza i filtri di seguito per affinare la ricerca.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtra gli attivatori del webhook per proprietà del payload (una per riga, formato: chiave: valore)", + "Filtered" : "Filtrato", + "Filters" : "Filtri", + "Fireworks AI Chat Settings" : "Impostazioni della chat Fireworks AI", + "Fireworks AI Embedding Configuration" : "Configurazione embedding Fireworks AI", + "First" : "Primo", + "Fixed" : "Fisso", + "For chat and retrieval-augmented generation" : "Per la chat e la generazione aumentata dal recupero (RAG)", + "For vector embeddings and semantic search" : "Per gli embedding vettoriali e la ricerca semantica", + "Format" : "Formato", + "Format Issue" : "Problema di formato", + "Format constraint is missing" : "Il vincolo di formato è mancante", + "From Date" : "Dalla data", + "From date" : "Dalla data", + "Full data copied to clipboard" : "Tutti i dati copiati negli appunti", + "General Issue" : "Problema generale", + "Generate recommendations and confidence scores" : "Genera raccomandazioni e punteggi di confidenza", + "Generate vectors immediately when new objects are created" : "Genera i vettori immediatamente quando vengono creati nuovi oggetti", + "GitHub token is valid" : "Il token GitHub è valido", + "GitHub token removed successfully" : "Token GitHub rimosso correttamente", + "GitHub token saved successfully" : "Token GitHub salvato correttamente", + "GitLab URL saved successfully" : "URL GitLab salvato correttamente", + "GitLab token is valid" : "Il token GitLab è valido", + "GitLab token saved successfully" : "Token GitLab salvato correttamente", + "HTTP Method" : "Metodo HTTP", + "HTTP method used to send webhook requests" : "Metodo HTTP utilizzato per inviare le richieste webhook", + "Headers" : "Intestazioni", + "Health" : "Stato di salute", + "Heartbeat successful - connection kept alive" : "Heartbeat riuscito - connessione mantenuta attiva", + "Helpful" : "Utile", + "Hide Filters" : "Nascondi filtri", + "Hide in forms" : "Nascondi nei moduli", + "Hide in list view" : "Nascondi nella vista elenco", + "High" : "Alto", + "High Confidence" : "Confidenza elevata", + "Host" : "Host", + "Hourly" : "Ogni ora", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Quanto in profondità attraversare le proprietà annidate degli oggetti (1-20). Valori più alti catturano più dettagli ma aumentano la dimensione del vettore.", + "How often to check for updates (1-168 hours)" : "Frequenza con cui verificare la presenza di aggiornamenti (1-168 ore)", + "How to handle retries for failed webhook deliveries" : "Come gestire i tentativi per le consegne webhook non riuscite", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Limiti di frequenza IP cancellati correttamente", + "Identify properties not in the schema" : "Identifica le proprietà non presenti nello schema", + "Immutable" : "Immutabile", + "Import" : "Importa", + "Import successful" : "Importazione riuscita", + "Improved Property" : "Proprietà migliorata", + "In use" : "In uso", + "Inactive" : "Inattivo", + "Include IDs and names of related objects for better contextual search" : "Includi gli ID e i nomi degli oggetti correlati per una migliore ricerca contestuale", + "Include related object references" : "Includi i riferimenti agli oggetti correlati", + "Include schema and register metadata" : "Includi i metadati di schema e registro", + "Initialization failed" : "Inizializzazione non riuscita", + "Initialize Project" : "Inizializza progetto", + "Inspect Fields" : "Ispeziona campi", + "Inspect Index" : "Ispeziona indice", + "Invalid" : "Non valido", + "Invalid batch size. Must be between 1 and 5000" : "Dimensione del lotto non valida. Deve essere compresa tra 1 e 5000", + "Invalid field name provided" : "Nome del campo fornito non valido", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects non valido. Deve essere 0 (tutti) o un numero positivo", + "Invalid:" : "Non valido:", + "Issue" : "Problema", + "Items per page:" : "Elementi per pagina:", + "Just now" : "Proprio ora", + "LLM Configuration" : "Configurazione LLM", + "LLM actions menu" : "Menu azioni LLM", + "LLM configuration saved successfully" : "Configurazione LLM salvata correttamente", + "LLM features disabled" : "Funzionalità LLM disabilitate", + "LLM features enabled" : "Funzionalità LLM abilitate", + "LLM must be enabled with an embedding provider configured" : "L'LLM deve essere abilitato con un fornitore di embedding configurato", + "LLM settings updated successfully" : "Impostazioni LLM aggiornate correttamente", + "Last" : "Ultimo", + "Last Triggered" : "Ultima attivazione", + "Length Range:" : "Intervallo di lunghezza:", + "Linear" : "Lineare", + "Load Advanced Filters" : "Carica filtri avanzati", + "Load advanced filters with live data from your search index" : "Carica filtri avanzati con dati in tempo reale dall'indice di ricerca", + "Load view" : "Carica vista", + "Loading ConfigSets..." : "Caricamento dei ConfigSet...", + "Loading advanced filters..." : "Caricamento dei filtri avanzati...", + "Loading agents..." : "Caricamento degli agenti...", + "Loading application details..." : "Caricamento dei dettagli dell'applicazione...", + "Loading applications..." : "Caricamento delle applicazioni...", + "Loading archived conversations..." : "Caricamento delle conversazioni archiviate...", + "Loading audit trails..." : "Caricamento dei registri di controllo...", + "Loading collections..." : "Caricamento delle raccolte...", + "Loading configurations..." : "Caricamento delle configurazioni...", + "Loading conversation..." : "Caricamento della conversazione...", + "Loading conversations..." : "Caricamento delle conversazioni...", + "Loading deleted items..." : "Caricamento degli elementi eliminati...", + "Loading events..." : "Caricamento degli eventi...", + "Loading filter data..." : "Caricamento dei dati dei filtri...", + "Loading groups..." : "Caricamento dei gruppi...", + "Loading log details..." : "Caricamento dei dettagli del log...", + "Loading registers..." : "Caricamento dei registri...", + "Loading schemas..." : "Caricamento degli schemi...", + "Loading search trails..." : "Caricamento dei registri di ricerca...", + "Loading sources..." : "Caricamento delle origini...", + "Loading statistics..." : "Caricamento delle statistiche...", + "Loading users..." : "Caricamento degli utenti...", + "Loading views..." : "Caricamento delle viste...", + "Loading..." : "Caricamento...", + "Local" : "Locale", + "Local Version" : "Versione locale", + "Locked" : "Bloccato", + "Locked:" : "Bloccato:", + "Login successful" : "Accesso riuscito", + "Logs" : "Log", + "Low" : "Basso", + "Low Confidence" : "Confidenza bassa", + "Lucene Version:" : "Versione Lucene:", + "Magic table synchronized successfully" : "Tabella magica sincronizzata correttamente", + "Manage Organisation Roles" : "Gestisci ruoli dell'organizzazione", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Gestisci le raccolte SOLR (archivi dati) e assegnale a oggetti e file.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Gestisci i ConfigSet SOLR (modelli di configurazione) per le tue raccolte.", + "Manage and configure agents for automated tasks" : "Gestisci e configura gli agenti per le attività automatizzate", + "Manage and monitor file text extraction status" : "Gestisci e monitora lo stato dell'estrazione del testo dai file", + "Manage and restore soft deleted items from your registers" : "Gestisci e ripristina gli elementi eliminati in modo reversibile dai tuoi registri", + "Manage and view detected entities from files and objects" : "Gestisci e visualizza le entità rilevate da file e oggetti", + "Manage document templates and themes" : "Gestisci modelli e temi dei documenti", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Gestisci i flussi di lavoro n8n per l'automazione di OpenRegister. I flussi di lavoro verranno archiviati nel progetto configurato.", + "Manage webhooks for event-driven integrations" : "Gestisci i webhook per le integrazioni basate sugli eventi", + "Manage your applications and modules" : "Gestisci le tue applicazioni e i tuoi moduli", + "Manage your data registers and their configurations" : "Gestisci i tuoi registri di dati e le loro configurazioni", + "Manage your data schemas and their properties" : "Gestisci i tuoi schemi di dati e le loro proprietà", + "Manage your data sources and their configurations" : "Gestisci le tue origini dati e le loro configurazioni", + "Manage your saved search configurations" : "Gestisci le tue configurazioni di ricerca salvate", + "Manage your system configurations and settings" : "Gestisci le configurazioni e le impostazioni del tuo sistema", + "Managed" : "Gestito", + "Max Files (0 = all)" : "Numero massimo di file (0 = tutti)", + "Max Length" : "Lunghezza massima", + "Max Length:" : "Lunghezza massima:", + "Max Objects (0 = all)" : "Numero massimo di oggetti (0 = tutti)", + "Max Retries" : "Numero massimo di tentativi", + "Max Shards/Node" : "Numero massimo di shard/nodo", + "Max execution time (ms)" : "Tempo massimo di esecuzione (ms)", + "Max ms" : "Max ms", + "Max result count" : "Numero massimo di risultati", + "Max results" : "Risultati massimi", + "Maximum" : "Massimo", + "Maximum Nesting Depth" : "Profondità massima di annidamento", + "Maximum length constraint is missing" : "Il vincolo di lunghezza massima è mancante", + "Maximum length is too restrictive" : "La lunghezza massima è troppo restrittiva", + "Maximum number of files to process. Set to 0 to process all files." : "Numero massimo di file da elaborare. Imposta a 0 per elaborare tutti i file.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Numero massimo di oggetti da elaborare. Imposta a 0 per elaborare tutti gli oggetti.", + "Maximum number of retry attempts for failed deliveries" : "Numero massimo di tentativi per le consegne non riuscite", + "Maximum time to wait before committing changes" : "Tempo massimo di attesa prima di eseguire il commit delle modifiche", + "Maximum tokens to generate" : "Numero massimo di token da generare", + "Maximum value constraint is missing" : "Il vincolo di valore massimo è mancante", + "Maximum value is too restrictive" : "Il valore massimo è troppo restrittivo", + "Medium" : "Medio", + "Medium Confidence" : "Confidenza media", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Query medie: ricerche con qualche filtro o parametri multipli (ad es. intervalli di date, registri/schemi specifici)", + "Members of selected groups can access this view" : "I membri dei gruppi selezionati possono accedere a questa vista", + "Members:" : "Membri:", + "Memory prediction calculated successfully" : "Previsione della memoria calcolata correttamente", + "Message content is required" : "Il contenuto del messaggio è obbligatorio", + "Message does not belong to this conversation" : "Il messaggio non appartiene a questa conversazione", + "Metadata Filters" : "Filtri dei metadati", + "Method" : "Metodo", + "Min Length" : "Lunghezza minima", + "Min execution time (ms)" : "Tempo minimo di esecuzione (ms)", + "Min ms" : "Min ms", + "Min result count" : "Numero minimo di risultati", + "Min results" : "Risultati minimi", + "Minimum" : "Minimo", + "Minimum value constraint is missing" : "Il vincolo di valore minimo è mancante", + "Minimum value is too restrictive" : "Il valore minimo è troppo restrittivo", + "Missing object fields created successfully" : "Campi oggetto mancanti creati correttamente", + "Mode:" : "Modalità:", + "Model" : "Modello", + "Model Name" : "Nome del modello", + "Modified" : "Modificato", + "Monthly" : "Mensile", + "More sources (10-20):" : "Più origini (10-20):", + "Most Active Objects" : "Oggetti più attivi", + "Multiple Of" : "Multiplo di", + "Name" : "Nome", + "Names cache warmup completed" : "Riscaldamento della cache dei nomi completato", + "New Collection Name" : "Nuovo nome della raccolta", + "New Conversation" : "Nuova conversazione", + "New Properties" : "Nuove proprietà", + "New Property" : "Nuova proprietà", + "New Value" : "Nuovo valore", + "New users without specific organisation membership will be automatically added to this organisation" : "I nuovi utenti senza un'appartenenza specifica a un'organizzazione verranno aggiunti automaticamente a questa organizzazione", + "Next" : "Successivo", + "No" : "No", + "No ConfigSets found" : "Nessun ConfigSet trovato", + "No GitHub token configured" : "Nessun token GitHub configurato", + "No GitHub token provided" : "Nessun token GitHub fornito", + "No GitLab token provided" : "Nessun token GitLab fornito", + "No Organisation" : "Nessuna organizzazione", + "No active filters" : "Nessun filtro attivo", + "No activity data available" : "Nessun dato di attività disponibile", + "No agents available" : "Nessun agente disponibile", + "No agents found" : "Nessun agente trovato", + "No app store cache exists yet" : "Non esiste ancora alcuna cache dell'App Store", + "No applications are available." : "Nessuna applicazione disponibile.", + "No applications found" : "Nessuna applicazione trovata", + "No archived conversations" : "Nessuna conversazione archiviata", + "No audit trail entries found" : "Nessuna voce del registro di controllo trovata", + "No changes recorded" : "Nessuna modifica registrata", + "No chunks to vectorize" : "Nessun blocco da vettorizzare", + "No collections found" : "Nessuna raccolta trovata", + "No configuration data" : "Nessun dato di configurazione", + "No configurations are available." : "Nessuna configurazione disponibile.", + "No configurations found" : "Nessuna configurazione trovata", + "No configurations found for this application." : "Nessuna configurazione trovata per questa applicazione.", + "No conversations yet. Create a new one to get started!" : "Ancora nessuna conversazione. Creane una nuova per iniziare!", + "No data available" : "Nessun dato disponibile", + "No data available for chart" : "Nessun dato disponibile per il grafico", + "No deleted items found" : "Nessun elemento eliminato trovato", + "No deletion data available" : "Nessun dato di eliminazione disponibile", + "No description found" : "Nessuna descrizione trovata", + "No description provided" : "Nessuna descrizione fornita", + "No entities detected in this file. Run text extraction first." : "Nessuna entità rilevata in questo file. Esegui prima l'estrazione del testo.", + "No entities found" : "Nessuna entità trovata", + "No entities have been detected yet" : "Non è stata ancora rilevata alcuna entità", + "No events found" : "Nessun evento trovato", + "No expired audit trails found to clear" : "Nessun registro di controllo scaduto da cancellare", + "No facetable fields available. Select a register and schema to see available filters." : "Nessun campo a faccette disponibile. Seleziona un registro e uno schema per visualizzare i filtri disponibili.", + "No files found" : "Nessun file trovato", + "No files have been extracted yet" : "Non è stato ancora estratto alcun file", + "No files to process" : "Nessun file da elaborare", + "No files to reindex" : "Nessun file da reindicizzare", + "No filters are currently active. This will delete ALL audit trail entries!" : "Nessun filtro attualmente attivo. Questo eliminerà TUTTE le voci del registro di controllo!", + "No log entries found" : "Nessuna voce di log trovata", + "No logs are available for this configuration." : "Nessun log disponibile per questa configurazione.", + "No logs are available for this source." : "Nessun log disponibile per questa origine.", + "No logs found" : "Nessun log trovato", + "No mismatched fields found - SOLR schema is properly configured" : "Nessun campo non corrispondente trovato - lo schema SOLR è configurato correttamente", + "No objects found" : "Nessun oggetto trovato", + "No objects found to vectorize" : "Nessun oggetto trovato da vettorizzare", + "No objects selected for deletion" : "Nessun oggetto selezionato per l'eliminazione", + "No objects to delete" : "Nessun oggetto da eliminare", + "No objects were permanently deleted" : "Nessun oggetto è stato eliminato definitivamente", + "No properties found" : "Nessuna proprietà trovata", + "No properties match your filters." : "Nessuna proprietà corrisponde ai filtri.", + "No purge date set" : "Nessuna data di eliminazione definitiva impostata", + "No register data available" : "Nessun dato di registro disponibile", + "No registers found" : "Nessun registro trovato", + "No registers found for this application." : "Nessun registro trovato per questa applicazione.", + "No relations found" : "Nessuna relazione trovata", + "No request body available" : "Nessun corpo della richiesta disponibile", + "No response body available" : "Nessun corpo della risposta disponibile", + "No saved views yet. Create one in the Search tab!" : "Ancora nessuna vista salvata. Creane una nella scheda Ricerca!", + "No schema data available" : "Nessun dato di schema disponibile", + "No schema selected for exploration" : "Nessuno schema selezionato per l'esplorazione", + "No schemas found" : "Nessuno schema trovato", + "No schemas found for this application." : "Nessuno schema trovato per questa applicazione.", + "No search terms data available" : "Nessun dato sui termini di ricerca disponibile", + "No search trail entries found" : "Nessuna voce del registro di ricerca trovata", + "No sources are available." : "Nessuna origine disponibile.", + "No sources found" : "Nessuna origine trovata", + "No templates found" : "Nessun modello trovato", + "No templates have been created yet" : "Non è stato ancora creato alcun modello", + "No valid magic-mapped register+schema combinations found" : "Nessuna combinazione valida di registro+schema mappata magicamente trovata", + "No views found. Create views first before configuring vectorization." : "Nessuna vista trovata. Crea prima le viste prima di configurare la vettorizzazione.", + "No views match your search" : "Nessuna vista corrisponde alla tua ricerca", + "No webhooks found" : "Nessun webhook trovato", + "No webhooks have been configured yet" : "Non è stato ancora configurato alcun webhook", + "No workflows found in this project. Create workflows in the n8n editor." : "Nessun flusso di lavoro trovato in questo progetto. Crea flussi di lavoro nell'editor n8n.", + "None" : "Nessuno", + "Not helpful" : "Non utile", + "Not in use" : "Non in uso", + "Number Constraints" : "Vincoli numerici", + "Number of chunks to vectorize in one API call (1-100)." : "Numero di blocchi da vettorizzare in una singola chiamata API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Numero di blocchi da vettorizzare in una singola chiamata API. Più alto = più veloce ma più memoria. Consigliato: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Numero di oggetti da elaborare in ogni lotto (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Numero di oggetti da vettorizzare in una singola chiamata API. Più alto = più veloce ma più memoria. Consigliato: 10-50.", + "Numeric Range:" : "Intervallo numerico:", + "Object" : "Oggetto", + "Object Collection" : "Raccolta di oggetti", + "Object Count Prediction" : "Previsione del conteggio degli oggetti", + "Object ID" : "ID oggetto", + "Object Management" : "Gestione oggetti", + "Object Vectorization" : "Vettorizzazione degli oggetti", + "Object Warmup" : "Riscaldamento degli oggetti", + "Object collection not configured" : "Raccolta di oggetti non configurata", + "Object not found" : "Oggetto non trovato", + "Object permanently deleted" : "Oggetto eliminato definitivamente", + "Object restored successfully" : "Oggetto ripristinato correttamente", + "Object settings updated successfully" : "Impostazioni dell'oggetto aggiornate correttamente", + "Object sources" : "Origini degli oggetti", + "Object unlocked successfully" : "Oggetto sbloccato correttamente", + "Object vectorization configuration saved successfully" : "Configurazione della vettorizzazione degli oggetti salvata correttamente", + "Object vectorized successfully" : "Oggetto vettorizzato correttamente", + "Object:" : "Oggetto:", + "Objects" : "Oggetti", + "Objects Analyzed" : "Oggetti analizzati", + "Objects Distribution" : "Distribuzione degli oggetti", + "Objects being analyzed" : "Oggetti in fase di analisi", + "Objects by Register" : "Oggetti per registro", + "Objects by Schema" : "Oggetti per schema", + "Objects deletion completed successfully" : "Eliminazione degli oggetti completata correttamente", + "Objects in schema" : "Oggetti nello schema", + "Objects to Process:" : "Oggetti da elaborare:", + "Objects to be analyzed" : "Oggetti da analizzare", + "Objects to be deleted" : "Oggetti da eliminare", + "Objects to be validated" : "Oggetti da convalidare", + "Objects will be serialized as JSON text before vectorization" : "Gli oggetti verranno serializzati come testo JSON prima della vettorizzazione", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Gli oggetti verranno eliminati in modo reversibile (contrassegnati come eliminati ma mantenuti nel database). Possono essere recuperati in seguito se necessario.", + "Old Value" : "Vecchio valore", + "Oldest Item (days)" : "Elemento più vecchio (giorni)", + "Ollama Chat Settings" : "Impostazioni della chat Ollama", + "Ollama Embedding Configuration" : "Configurazione embedding Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Solo con modifiche", + "Open Register Objects" : "Oggetti Open Register", + "Open n8n Editor" : "Apri editor n8n", + "OpenAI Chat Settings" : "Impostazioni della chat OpenAI", + "OpenAI Embedding Configuration" : "Configurazione embedding OpenAI", + "OpenRegister Settings" : "Impostazioni OpenRegister", + "Optional webhook secret for signature verification" : "Segreto webhook facoltativo per la verifica della firma", + "Order" : "Ordine", + "Organisation" : "Organizzazione", + "Organisation ID" : "ID organizzazione", + "Organisation Members" : "Membri dell'organizzazione", + "Organisation Statistics" : "Statistiche dell'organizzazione", + "Organisation created successfully" : "Organizzazione creata correttamente", + "Organisation settings saved successfully" : "Impostazioni dell'organizzazione salvate correttamente", + "Organisations" : "Organizzazioni", + "Organization" : "Organizzazione", + "Organization ID (Optional)" : "ID organizzazione (facoltativo)", + "Orphaned Items" : "Elementi orfani", + "Owner" : "Proprietario", + "Owner:" : "Proprietario:", + "Parallel Mode (Faster, more resource intensive)" : "Modalità parallela (più veloce, più dispendiosa di risorse)", + "Parallel:" : "Parallela:", + "Password" : "Password", + "Password for SOLR authentication (optional)" : "Password per l'autenticazione SOLR (facoltativa)", + "Path" : "Percorso", + "Path in repository" : "Percorso nel repository", + "Path where the configuration file will be saved in the repository" : "Percorso in cui il file di configurazione verrà salvato nel repository", + "Path where the register OAS file will be saved in the repository" : "Percorso in cui il file OAS del registro verrà salvato nel repository", + "Pattern (regex)" : "Schema (regex)", + "Pattern Issue" : "Problema di schema", + "Pattern constraint is missing" : "Il vincolo di schema è mancante", + "Patterns:" : "Schemi:", + "Pending" : "In sospeso", + "Permanent Delete Mode" : "Modalità eliminazione permanente", + "Permanently Delete" : "Elimina definitivamente", + "Permanently delete already soft-deleted objects" : "Elimina definitivamente gli oggetti già eliminati in modo reversibile", + "Permanently delete this conversation?" : "Eliminare definitivamente questa conversazione?", + "Person" : "Persona", + "Personal Data" : "Dati personali", + "Phone" : "Telefono", + "Photos" : "Foto", + "Please create an agent in the" : "Crea un agente nel", + "Please select an agent to continue" : "Seleziona un agente per continuare", + "Please select which register and schema to use for the new object" : "Seleziona quale registro e schema utilizzare per il nuovo oggetto", + "Please try again later." : "Riprova più tardi.", + "Please wait while we fetch your agents." : "Attendi mentre recuperiamo i tuoi agenti.", + "Please wait while we fetch your applications." : "Attendi mentre recuperiamo le tue applicazioni.", + "Please wait while we fetch your configurations." : "Attendi mentre recuperiamo le tue configurazioni.", + "Please wait while we fetch your deleted items." : "Attendi mentre recuperiamo i tuoi elementi eliminati.", + "Please wait while we fetch your sources." : "Attendi mentre recuperiamo le tue origini.", + "Polls" : "Sondaggi", + "Popular Search Terms" : "Termini di ricerca popolari", + "Port" : "Porta", + "Prerequisites" : "Prerequisiti", + "Press Enter to send, Shift+Enter for new line" : "Premi Invio per inviare, Maiusc+Invio per una nuova riga", + "Previous" : "Precedente", + "Private" : "Privato", + "Processes chunks in batches with simulated parallelism." : "Elabora i blocchi in lotti con parallelismo simulato.", + "Processes file chunks sequentially (safest)." : "Elabora i blocchi di file in sequenza (più sicuro).", + "Processes objects in chunks with simulated parallelism." : "Elabora gli oggetti in blocchi con parallelismo simulato.", + "Processes objects sequentially (safest)." : "Elabora gli oggetti in sequenza (più sicuro).", + "Processing" : "Elaborazione", + "Processing Limits" : "Limiti di elaborazione", + "Processing..." : "Elaborazione in corso...", + "Project initialized successfully" : "Progetto inizializzato correttamente", + "Project not found. Please initialize first." : "Progetto non trovato. Inizializza prima.", + "Properties" : "Proprietà", + "Property Behaviors" : "Comportamenti delle proprietà", + "Property Configuration:" : "Configurazione delle proprietà:", + "Property Filters" : "Filtri delle proprietà", + "Property Title" : "Titolo della proprietà", + "Property Type" : "Tipo di proprietà", + "Property can be improved" : "La proprietà può essere migliorata", + "Protected" : "Protetto", + "Provider" : "Fornitore", + "Provider is required for testing" : "Il fornitore è obbligatorio per il test", + "Public" : "Pubblico", + "Public View" : "Vista pubblica", + "Public views can be accessed by anyone in the system" : "Le viste pubbliche sono accessibili a chiunque nel sistema", + "Publish" : "Pubblica", + "Publish Objects" : "Pubblica oggetti", + "Publish all objects in this schema" : "Pubblica tutti gli oggetti in questo schema", + "Published" : "Pubblicato", + "Published:" : "Pubblicato:", + "Publishing..." : "Pubblicazione in corso...", + "Purge" : "Elimina definitivamente", + "Purge Date" : "Data di eliminazione definitiva", + "Query Complexity Distribution" : "Distribuzione della complessità delle query", + "Query parameter is required" : "Il parametro di query è obbligatorio", + "RAG" : "RAG", + "RAG Configuration" : "Configurazione RAG", + "Rate limits cleared successfully" : "Limiti di frequenza cancellati correttamente", + "Raw Changes Data" : "Dati grezzi delle modifiche", + "Re-vectorize on object update" : "Rivettorizza all'aggiornamento dell'oggetto", + "Read" : "Lettura", + "Recommendations:" : "Raccomandazioni:", + "Recommended Type:" : "Tipo consigliato:", + "Recommended: 5 sources" : "Consigliato: 5 origini", + "Refresh" : "Aggiorna", + "Refresh Data" : "Aggiorna dati", + "Refresh Stats" : "Aggiorna statistiche", + "Refresh Workflows" : "Aggiorna flussi di lavoro", + "Refresh dashboard" : "Aggiorna pannello di controllo", + "Refresh database info" : "Aggiorna informazioni del database", + "Register" : "Registro", + "Register ID" : "ID registro", + "Register Statistics" : "Statistiche del registro", + "Register Totals" : "Totali del registro", + "Register depublished successfully" : "Pubblicazione del registro annullata correttamente", + "Register not found" : "Registro non trovato", + "Register objects deletion completed successfully" : "Eliminazione degli oggetti del registro completata correttamente", + "Register published successfully" : "Registro pubblicato correttamente", + "Register/Schema Usage" : "Utilizzo registro/schema", + "Registers" : "Registri", + "Reindex" : "Reindicizza", + "Reindex completed" : "Reindicizzazione completata", + "Reindex completed successfully" : "Reindicizzazione completata correttamente", + "Reindex failed" : "Reindicizzazione non riuscita", + "Relations" : "Relazioni", + "Remote Version" : "Versione remota", + "Remove" : "Rimuovi", + "Remove filter" : "Rimuovi filtro", + "Remove from favorites" : "Rimuovi dai preferiti", + "Remove group" : "Rimuovi gruppo", + "Removed" : "Rimosso", + "Removed from favorites" : "Rimosso dai preferiti", + "Rename Conversation" : "Rinomina conversazione", + "Rename conversation" : "Rinomina conversazione", + "Replicas" : "Repliche", + "Repository" : "Repository", + "Request Body" : "Corpo della richiesta", + "Request Data" : "Dati della richiesta", + "Request Details" : "Dettagli della richiesta", + "Request body must be JSON with a \"query\" field" : "Il corpo della richiesta deve essere JSON con un campo \"query\"", + "Request timeout in seconds" : "Timeout della richiesta in secondi", + "Required field" : "Campo obbligatorio", + "Required status is inconsistent" : "Lo stato obbligatorio è incoerente", + "Rerun Search" : "Riesegui ricerca", + "Reset Changes" : "Ripristina modifiche", + "Reset Filters" : "Ripristina filtri", + "Response Body" : "Corpo della risposta", + "Response Details" : "Dettagli della risposta", + "Response Time:" : "Tempo di risposta:", + "Restore" : "Ripristina", + "Restore conversation" : "Ripristina conversazione", + "Restore or permanently delete items" : "Ripristina o elimina definitivamente gli elementi", + "Result Count Range" : "Intervallo del conteggio dei risultati", + "Results" : "Risultati", + "Retrieve all objects for this schema" : "Recupera tutti gli oggetti per questo schema", + "Retry" : "Riprova", + "Retry Failed Extractions" : "Riprova estrazioni non riuscite", + "Retry Policy" : "Criterio dei tentativi", + "Retry completed" : "Tentativo completato", + "Retrying..." : "Nuovo tentativo in corso...", + "Risk Level" : "Livello di rischio", + "Roles updated successfully" : "Ruoli aggiornati correttamente", + "SOLR Connection Settings" : "Impostazioni di connessione SOLR", + "SOLR Version:" : "Versione SOLR:", + "SOLR actions menu" : "Menu azioni SOLR", + "SOLR base path (usually /solr)" : "Percorso di base SOLR (di solito /solr)", + "SOLR is disabled" : "SOLR è disabilitato", + "SOLR is not available or not configured" : "SOLR non è disponibile o non configurato", + "SOLR logging disabled" : "Registrazione SOLR disabilitata", + "SOLR logging enabled" : "Registrazione SOLR abilitata", + "SOLR search disabled" : "Ricerca SOLR disabilitata", + "SOLR search enabled" : "Ricerca SOLR abilitata", + "SOLR server hostname or IP address" : "Nome host o indirizzo IP del server SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Numero di porta del server SOLR (facoltativo, predefinito 8983)", + "SOLR setup completed successfully" : "Configurazione SOLR completata correttamente", + "SOLR setup failed" : "Configurazione SOLR non riuscita", + "SOLR setup failed - check logs" : "Configurazione SOLR non riuscita - controlla i log", + "Sample Values:" : "Valori di esempio:", + "Save" : "Salva", + "Save Configuration" : "Salva configurazione", + "Save Connection Settings" : "Salva impostazioni di connessione", + "Save Roles" : "Salva ruoli", + "Save Settings" : "Salva impostazioni", + "Save current search as view" : "Salva la ricerca corrente come vista", + "Saved Views" : "Viste salvate", + "Saving..." : "Salvataggio in corso...", + "Schema" : "Schema", + "Schema ID" : "ID schema", + "Schema Name" : "Nome schema", + "Schema Statistics" : "Statistiche dello schema", + "Schema depublished successfully" : "Pubblicazione dello schema annullata correttamente", + "Schema objects deletion completed successfully" : "Eliminazione degli oggetti dello schema completata correttamente", + "Schema published successfully" : "Schema pubblicato correttamente", + "Schema successfully created" : "Schema creato correttamente", + "Schema successfully updated" : "Schema aggiornato correttamente", + "Schemas" : "Schemi", + "Scheme" : "Schema", + "Search / Views" : "Ricerca / Viste", + "Search Activity" : "Attività di ricerca", + "Search Analytics" : "Analisi delle ricerche", + "Search Entities" : "Cerca entità", + "Search Files" : "Cerca file", + "Search GitHub" : "Cerca su GitHub", + "Search GitLab" : "Cerca su GitLab", + "Search Method" : "Metodo di ricerca", + "Search Term" : "Termine di ricerca", + "Search Trail Details" : "Dettagli del registro di ricerca", + "Search Trail Management" : "Gestione del registro di ricerca", + "Search Trail Statistics" : "Statistiche del registro di ricerca", + "Search Trails" : "Registri di ricerca", + "Search Views" : "Cerca viste", + "Search Webhooks" : "Cerca webhook", + "Search by file name or path" : "Cerca per nome o percorso del file", + "Search by name or URL" : "Cerca per nome o URL", + "Search by value" : "Cerca per valore", + "Search in Files" : "Cerca nei file", + "Search in Objects" : "Cerca negli oggetti", + "Search property names..." : "Cerca nomi di proprietà...", + "Search trail deletion not implemented yet" : "Eliminazione del registro di ricerca non ancora implementata", + "Search views..." : "Cerca viste...", + "Secret" : "Segreto", + "Secret key for HMAC signature generation (optional)" : "Chiave segreta per la generazione della firma HMAC (facoltativa)", + "Select AI Agent" : "Seleziona agente IA", + "Select All" : "Seleziona tutto", + "Select ConfigSet" : "Seleziona ConfigSet", + "Select File Types to Vectorize:" : "Seleziona i tipi di file da vettorizzare:", + "Select HTTP method" : "Seleziona metodo HTTP", + "Select Register and Schema" : "Seleziona registro e schema", + "Select Views to Vectorize:" : "Seleziona le viste da vettorizzare:", + "Select a Nextcloud group to add" : "Seleziona un gruppo Nextcloud da aggiungere", + "Select a branch" : "Seleziona un ramo", + "Select a model or type a custom model name" : "Seleziona un modello o digita il nome di un modello personalizzato", + "Select a repository" : "Seleziona un repository", + "Select a repository you have write access to" : "Seleziona un repository a cui hai accesso in scrittura", + "Select an AI Agent" : "Seleziona un agente IA", + "Select an AI agent to begin chatting with your data." : "Seleziona un agente IA per iniziare a chattare con i tuoi dati.", + "Select backend" : "Seleziona backend", + "Select chat model" : "Seleziona modello di chat", + "Select collection for files" : "Seleziona raccolta per i file", + "Select collection for objects" : "Seleziona raccolta per gli oggetti", + "Select default organisation" : "Seleziona organizzazione predefinita", + "Select event to listen to..." : "Seleziona l'evento da ascoltare...", + "Select model" : "Seleziona modello", + "Select options..." : "Seleziona opzioni...", + "Select period" : "Seleziona periodo", + "Select property to send as payload" : "Seleziona la proprietà da inviare come payload", + "Select provider" : "Seleziona fornitore", + "Select registers and schemas to save a view" : "Seleziona registri e schemi per salvare una vista", + "Select retry policy" : "Seleziona criterio dei tentativi", + "Select the branch to publish to" : "Seleziona il ramo in cui pubblicare", + "Select the event this webhook should listen to" : "Seleziona l'evento che questo webhook deve ascoltare", + "Select views to vectorize:" : "Seleziona le viste da vettorizzare:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Seleziona quali gruppi Nextcloud sono disponibili per questa organizzazione. Gli utenti di questi gruppi avranno accesso alle risorse dell'organizzazione.", + "Select which data views the AI can search" : "Seleziona quali viste dati l'IA può cercare", + "Select which property from the event should be used as the webhook payload data" : "Seleziona quale proprietà dell'evento deve essere utilizzata come dati del payload del webhook", + "Select which tools the AI can use to perform actions" : "Seleziona quali strumenti l'IA può utilizzare per eseguire azioni", + "Selected" : "Selezionato", + "Selected Groups" : "Gruppi selezionati", + "Selected audit trails deleted successfully" : "Registri di controllo selezionati eliminati correttamente", + "Selected search trails deleted successfully" : "Registri di ricerca selezionati eliminati correttamente", + "Selected users can access this view" : "Gli utenti selezionati possono accedere a questa vista", + "Send additional feedback" : "Invia feedback aggiuntivo", + "Send as CloudEvent" : "Invia come CloudEvent", + "Sensitive PII" : "PII sensibili", + "Serial Mode (Safer, slower)" : "Modalità seriale (più sicura, più lenta)", + "Serial:" : "Seriale:", + "Server Configuration" : "Configurazione del server", + "Server Information" : "Informazioni del server", + "Settings" : "Impostazioni", + "Shards" : "Shard", + "Share" : "Condividi", + "Share with Groups" : "Condividi con gruppi", + "Share with Users" : "Condividi con utenti", + "Show Filters" : "Mostra filtri", + "Show less" : "Mostra meno", + "Show only entries with changes" : "Mostra solo le voci con modifiche", + "Simple" : "Semplice", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Query semplici: ricerche testuali di base con parametri minimi (ad es. termine di ricerca singolo, nessun filtro avanzato)", + "Size" : "Dimensione", + "Slug" : "Slug", + "Soft Delete Mode" : "Modalità eliminazione reversibile", + "Soft Deleted Items" : "Elementi eliminati in modo reversibile", + "SolrCloud mode enabled" : "Modalità SolrCloud abilitata", + "Source" : "Origine", + "Sources" : "Origini", + "Sources:" : "Origini:", + "Standalone SOLR mode" : "Modalità SOLR autonoma", + "Start Conversation" : "Avvia conversazione", + "Start Vectorization" : "Avvia vettorizzazione", + "Start a conversation" : "Avvia una conversazione", + "Starting names cache warmup..." : "Avvio del riscaldamento della cache dei nomi...", + "Starting..." : "Avvio in corso...", + "Statistics" : "Statistiche", + "Stats calculation not yet implemented" : "Calcolo delle statistiche non ancora implementato", + "Status" : "Stato", + "Status Code" : "Codice di stato", + "String Constraints" : "Vincoli delle stringhe", + "Success" : "Operazione riuscita", + "Success Rate" : "Tasso di successo", + "Success Status" : "Stato di successo", + "Successful" : "Riuscito", + "Successfully joined organisation" : "Iscrizione all'organizzazione riuscita", + "Sync Table" : "Sincronizza tabella", + "Synchronization Settings" : "Impostazioni di sincronizzazione", + "System Default" : "Predefinito del sistema", + "Technical Description" : "Descrizione tecnica", + "Technical description for developers and administrators" : "Descrizione tecnica per sviluppatori e amministratori", + "Temperature" : "Temperatura", + "Templates" : "Modelli", + "Test" : "Test", + "Test Chat" : "Prova chat", + "Test Connection" : "Prova connessione", + "Test Embedding" : "Prova embedding", + "Test endpoint executed successfully" : "Endpoint di test eseguito correttamente", + "Test webhook delivered successfully" : "Webhook di test consegnato correttamente", + "Test webhook delivery failed" : "Consegna del webhook di test non riuscita", + "Test webhook sent successfully" : "Webhook di test inviato correttamente", + "Testing..." : "Test in corso...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "I blocchi di testo vengono generati durante l'estrazione dei file e archiviati nel database. La vettorizzazione legge questi file già suddivisi in blocchi e li converte in embedding.", + "Text extracted successfully" : "Testo estratto correttamente", + "Text extraction disabled" : "Estrazione del testo disabilitata", + "The URL where webhook events will be sent" : "L'URL a cui verranno inviati gli eventi webhook", + "The requested conversation does not exist" : "La conversazione richiesta non esiste", + "The saved view and all its search configuration" : "La vista salvata e tutta la sua configurazione di ricerca", + "There are no audit trail entries matching your current filters." : "Non ci sono voci del registro di controllo che corrispondono ai filtri attuali.", + "There are no deleted items matching your current filters." : "Non ci sono elementi eliminati che corrispondono ai filtri attuali.", + "There are no search trail entries matching your current filters." : "Non ci sono voci del registro di ricerca che corrispondono ai filtri attuali.", + "There are no webhook log entries matching your filters." : "Non ci sono voci di log dei webhook che corrispondono ai filtri.", + "This action cannot be undone." : "Questa azione non può essere annullata.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Questa azione non può essere annullata. Assicurati che nessuna raccolta stia utilizzando questo ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Questo agente non ha viste o strumenti configurabili, ma è comunque possibile regolare le impostazioni RAG di seguito.", + "This analysis may take some time" : "Questa analisi potrebbe richiedere del tempo", + "This audit trail entry does not contain any change information." : "Questa voce del registro di controllo non contiene alcuna informazione sulle modifiche.", + "This configuration has no data defined." : "Questa configurazione non ha dati definiti.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Questo endpoint è obsoleto. Utilizzare invece gli endpoint basati sui blocchi.", + "This entity has no relations to objects or files" : "Questa entità non ha relazioni con oggetti o file", + "This is a test webhook from OpenRegister" : "Questo è un webhook di test da OpenRegister", + "This schema must use Magic Table configuration to sync" : "Questo schema deve utilizzare la configurazione Tabella magica per la sincronizzazione", + "This source has no associated registers." : "Questa origine non ha registri associati.", + "This will copy the _default ConfigSet with the new name" : "Questo copierà il ConfigSet _default con il nuovo nome", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Questo eliminerà definitivamente TUTTI gli embedding (vettori) dal database. Sarà necessario rivettorizzare tutti gli oggetti e i file. Questa azione non può essere annullata.\\n\\nContinuare?", + "This will permanently delete:" : "Questo eliminerà definitivamente:", + "Timeout (seconds)" : "Timeout (secondi)", + "Timestamp" : "Data e ora", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Suggerimento: abilita solo le viste che necessitano della ricerca semantica per ridurre al minimo i costi di embedding. Le semplici tabelle di consultazione raramente necessitano della vettorizzazione.", + "Title" : "Titolo", + "To Date" : "Alla data", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Per regolare la dimensione o la strategia dei blocchi, vai su Configurazione file → Limiti di elaborazione.", + "To change the embedding provider or model, go to LLM Configuration." : "Per cambiare il fornitore o il modello di embedding, vai su Configurazione LLM.", + "To date" : "Alla data", + "Toggle search sidebar" : "Attiva/disattiva barra laterale di ricerca", + "Tools" : "Strumenti", + "Top Deleters" : "Principali eliminatori", + "Total" : "Totale", + "Total Audit Trails" : "Totale registri di controllo", + "Total Chunks Available:" : "Totale blocchi disponibili:", + "Total Deleted Items" : "Totale elementi eliminati", + "Total Members" : "Totale membri", + "Total Objects" : "Totale oggetti", + "Total Objects in Database:" : "Totale oggetti nel database:", + "Total Organisations" : "Totale organizzazioni", + "Total Results" : "Totale risultati", + "Total Searches" : "Totale ricerche", + "Total Size" : "Dimensione totale", + "Total Storage" : "Archiviazione totale", + "Total:" : "Totale:", + "Totals" : "Totali", + "Type" : "Tipo", + "Type Issue" : "Problema di tipo", + "Type Variations:" : "Variazioni di tipo:", + "Type must be \"positive\" or \"negative\"" : "Il tipo deve essere \"positive\" o \"negative\"", + "Type to search for groups" : "Digita per cercare gruppi", + "Type to search for users" : "Digita per cercare utenti", + "Type to search groups..." : "Digita per cercare gruppi...", + "Type to search users..." : "Digita per cercare utenti...", + "URL" : "URL", + "URL where Ollama is running" : "URL su cui è in esecuzione Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Invariato", + "Unique Search Terms" : "Termini di ricerca univoci", + "Unique Terms" : "Termini univoci", + "Unknown" : "Sconosciuto", + "Unknown Register" : "Registro sconosciuto", + "Unknown Schema" : "Schema sconosciuto", + "Untitled View" : "Vista senza titolo", + "Update" : "Aggiorna", + "Update Operations" : "Operazioni di aggiornamento", + "Update configuration: ..." : "Aggiorna configurazione: ...", + "Update register OAS: ..." : "Aggiorna OAS del registro: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Aggiorna i vettori quando i dati dell'oggetto cambiano (consigliato per ricerche accurate)", + "Updated" : "Aggiornato", + "Updated At" : "Aggiornato il", + "Updated:" : "Aggiornato:", + "Uptime:" : "Tempo di attività:", + "Usage Count" : "Conteggio dell'utilizzo", + "Use SolrCloud with Zookeeper for distributed search" : "Usa SolrCloud con Zookeeper per la ricerca distribuita", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Usa i filtri per restringere le voci del registro di controllo per registro, schema, tipo di azione, utente, intervallo di date o ID oggetto.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Usa i filtri per restringere gli elementi eliminati per registro, schema, data di eliminazione o utente che li ha eliminati.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Usa i filtri per restringere le voci del registro di ricerca per registro, schema, stato di successo, utente, intervallo di date, termini di ricerca o metriche di prestazioni.", + "Used By Collections" : "Utilizzato dalle raccolte", + "User" : "Utente", + "User Agent Statistics" : "Statistiche degli user agent", + "User Description" : "Descrizione utente", + "User rate limits cleared successfully" : "Limiti di frequenza utente cancellati correttamente", + "User removed successfully" : "Utente rimosso correttamente", + "User-friendly description shown in forms and help text" : "Descrizione di facile comprensione mostrata nei moduli e nel testo di aiuto", + "Username" : "Nome utente", + "Username for SOLR authentication (optional)" : "Nome utente per l'autenticazione SOLR (facoltativo)", + "Users" : "Utenti", + "Using Pre-Generated Chunks" : "Utilizzo di blocchi pregenerati", + "Validate" : "Convalida", + "Validate Objects" : "Convalida oggetti", + "Validation completed successfully" : "Convalida completata correttamente", + "Validation failed" : "Convalida non riuscita", + "Value" : "Valore", + "Vector Dimensions" : "Dimensioni del vettore", + "Vector Search Backend" : "Backend di ricerca vettoriale", + "Vector field: _embedding_" : "Campo vettoriale: _embedding_", + "Vectorization Triggers" : "Attivatori di vettorizzazione", + "Vectorize All Files" : "Vettorizza tutti i file", + "Vectorize All Objects" : "Vettorizza tutti gli oggetti", + "Vectorize all file types" : "Vettorizza tutti i tipi di file", + "Vectorize all views" : "Vettorizza tutte le viste", + "Vectorize on object creation" : "Vettorizza alla creazione dell'oggetto", + "Vectors will be stored in your existing object and file collections" : "I vettori verranno archiviati nelle raccolte di oggetti e file esistenti", + "Version" : "Versione", + "Very High" : "Molto alto", + "View API Docs" : "Visualizza documentazione API", + "View Changes" : "Visualizza modifiche", + "View Details" : "Visualizza dettagli", + "View Error" : "Visualizza errore", + "View File" : "Visualizza file", + "View Full Details" : "Visualizza tutti i dettagli", + "View Logs" : "Visualizza log", + "View Name" : "Nome vista", + "View Object" : "Visualizza oggetto", + "View Parameters" : "Visualizza parametri", + "View Selection" : "Selezione vista", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Visualizza e analizza i log del registro di ricerca con funzionalità avanzate di filtro e analisi", + "View and analyze system audit trails with advanced filtering capabilities" : "Visualizza e analizza i registri di controllo del sistema con funzionalità avanzate di filtro", + "View deleted successfully" : "Vista eliminata correttamente", + "View entity details and manage relations" : "Visualizza i dettagli dell'entità e gestisci le relazioni", + "View name" : "Nome vista", + "View name is required" : "Il nome della vista è obbligatorio", + "View saved successfully!" : "Vista salvata correttamente!", + "View search analytics and manage search logs" : "Visualizza le analisi delle ricerche e gestisci i log delle ricerche", + "View successfully deleted" : "Vista eliminata correttamente", + "View successfully updated" : "Vista aggiornata correttamente", + "View updated successfully!" : "Vista aggiornata correttamente!", + "View webhook delivery logs and filter by webhook" : "Visualizza i log di consegna dei webhook e filtra per webhook", + "Views" : "Viste", + "Visible to users" : "Visibile agli utenti", + "Wait for Response" : "Attendi risposta", + "Wait for webhook response before continuing (required for request/response flows)" : "Attendi la risposta del webhook prima di continuare (necessario per i flussi richiesta/risposta)", + "Warmup Names Cache" : "Riscalda cache dei nomi", + "Warning" : "Avviso", + "Warning:" : "Avviso:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Dettagli del log del webhook", + "Webhook Logs" : "Log dei webhook", + "Webhook created successfully" : "Webhook creato correttamente", + "Webhook deleted" : "Webhook eliminato", + "Webhook retry delivered successfully" : "Tentativo di webhook consegnato correttamente", + "Webhook retry delivery failed" : "Consegna del tentativo di webhook non riuscita", + "Webhook updated" : "Webhook aggiornato", + "Webhook updated successfully" : "Webhook aggiornato correttamente", + "Webhooks" : "Webhook", + "Weekly" : "Settimanale", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Incapsula il payload del webhook nel formato CloudEvents per una migliore interoperabilità", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Sì", + "You" : "Tu", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Puoi creare nuovi ConfigSet basati sul modello _default oppure caricarne di personalizzati direttamente sul tuo server SOLR.", + "You do not have access to this conversation" : "Non hai accesso a questa conversazione", + "You do not have permission to delete this conversation" : "Non hai il permesso di eliminare questa conversazione", + "You do not have permission to modify this conversation" : "Non hai il permesso di modificare questa conversazione", + "You do not have permission to restore this conversation" : "Non hai il permesso di ripristinare questa conversazione", + "You must be logged in to favorite views" : "Devi aver effettuato l'accesso per aggiungere viste ai preferiti", + "You need an AI agent to start a conversation." : "Hai bisogno di un agente IA per avviare una conversazione.", + "Your Fireworks AI API key. Get one at" : "La tua chiave API Fireworks AI. Ottienine una su", + "Your OpenAI API key. Get one at" : "La tua chiave API OpenAI. Ottienine una su", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Il tuo feedback è stato registrato. Facoltativamente, puoi fornire ulteriori dettagli qui...", + "Zookeeper Hosts" : "Host Zookeeper", + "Zookeeper Port" : "Porta Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Impostazioni Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Stringa di connessione Zookeeper per SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Numero di porta Zookeeper (facoltativo, predefinito 2181)", + "chunk" : "blocco", + "chunks" : "blocchi", + "chunks processed" : "blocchi elaborati", + "configuration(s)" : "configurazione/i", + "e.g., lib/Settings/config.json" : "ad es. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "ad es. lib/Settings/register.json", + "failed" : "non riuscito", + "file" : "file", + "files" : "file", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "membri", + "menu or contact someone with permission to create agents." : "menu o contatta qualcuno con il permesso di creare agenti.", + "messages" : "messaggi", + "n8n URL and API key are required" : "L'URL e la chiave API di n8n sono obbligatori", + "n8n configuration saved successfully" : "Configurazione n8n salvata correttamente", + "n8n connection not configured" : "Connessione n8n non configurata", + "n8n connection successful" : "Connessione n8n riuscita", + "n8n connection test successful" : "Test di connessione n8n riuscito", + "n8n integration disabled" : "Integrazione n8n disabilitata", + "n8n integration enabled" : "Integrazione n8n abilitata", + "n8n project initialized successfully" : "Progetto n8n inizializzato correttamente", + "n8n settings saved successfully" : "Impostazioni n8n salvate correttamente", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "oggetti", + "objects processed" : "oggetti elaborati", + "or pick a range" : "oppure scegli un intervallo", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "offre un buon equilibrio tra velocità e accuratezza.", + "register(s)" : "registro/i", + "register(s) selected" : "registro/i selezionato/i", + "required" : "obbligatorio", + "results" : "risultati", + "schema(s)" : "schema/i", + "schema(s) selected" : "schema/i selezionato/i", + "sk-..." : "sk-...", + "this application" : "questa applicazione", + "to" : "a", + "vectorized" : "vettorizzato", + "views selected" : "viste selezionate", + "ℹ️ Current Configuration" : "ℹ️ Configurazione attuale", + "⚡ Batch Processing" : "⚡ Elaborazione in blocco", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Contesto completo ✓ Minore probabilità di perdere dettagli ✗ Risposte più lente ✗ Può includere informazioni meno rilevanti", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Risposte più veloci ✓ Risposte più mirate ✗ Può perdere informazioni rilevanti", + "✨ AI Features" : "✨ Funzionalità IA", + "All search trails cleared successfully" : "Tutti i registri di ricerca cancellati correttamente", + "Multiple search trail deletion not implemented yet" : "Eliminazione multipla dei registri di ricerca non ancora implementata", + "No expired search trails found to clear" : "Nessun registro di ricerca scaduto da cancellare", + "Missing message" : "Messaggio mancante", + "message content is required" : "il contenuto del messaggio è obbligatorio", + "Missing conversation or agentUuid" : "Conversazione o agentUuid mancante", + "Access denied" : "Accesso negato", + "Conversation not found" : "Conversazione non trovata", + "AI service not configured" : "Servizio IA non configurato", + "Failed to process message" : "Impossibile elaborare il messaggio", + "Missing conversationId" : "conversationId mancante", + "conversationId is required" : "conversationId è obbligatorio", + "Failed to fetch conversation history" : "Impossibile recuperare la cronologia della conversazione", + "Failed to clear conversation" : "Impossibile cancellare la conversazione", + "Invalid feedback type" : "Tipo di feedback non valido", + "type must be \"positive\" or \"negative\"" : "il tipo deve essere \"positive\" o \"negative\"", + "Message not found" : "Messaggio non trovato", + "Failed to save feedback" : "Impossibile salvare il feedback", + "Failed to get chat statistics" : "Impossibile ottenere le statistiche della chat", + "Not Found" : "Non trovato", + "Engine not found" : "Motore non trovato", + "Consumer not found" : "Consumer non trovato", + "Backend parameter is required" : "Il parametro backend è obbligatorio", + "Failed to get database information: %s" : "Impossibile ottenere le informazioni del database: %s", + "SOLR setup error: %s" : "Errore di configurazione SOLR: %s", + "Reindex failed: %s" : "Reindicizzazione non riuscita: %s", + "Anonymized" : "Anonimizzato", + "Entities detected" : "Entità rilevate", + "Extract Now" : "Estrai ora", + "Extracted at" : "Estratto il", + "Extraction" : "Estrazione", + "Failed to load extraction data" : "Impossibile caricare i dati di estrazione", + "Failed to load register data" : "Impossibile caricare i dati del registro", + "Failed to retrieve extraction status." : "Impossibile recuperare lo stato dell'estrazione.", + "Failed to retrieve objects for file." : "Impossibile recuperare gli oggetti per il file.", + "No extraction data available for this file" : "Nessun dato di estrazione disponibile per questo file", + "No register objects reference this file" : "Nessun oggetto del registro fa riferimento a questo file", + "Not extracted" : "Non estratto", + "Register Objects" : "Oggetti del registro", + "Risk level" : "Livello di rischio", + "Text chunks" : "Blocchi di testo", + "Unknown error" : "Errore sconosciuto", + "Very high" : "Molto alto", + "{title} in {register} / {schema}" : "{title} in {register} / {schema}", + "Push Notifications" : "Notifiche push", + "Real-time push notification status via notify_push" : "Stato delle notifiche push in tempo reale tramite notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push in tempo reale non disponibile — l'app notify_push non è installata", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installa l'app notify_push dall'App Store di Nextcloud per abilitare gli aggiornamenti in tempo reale.", + "Open Nextcloud App Store" : "Apri l'App Store di Nextcloud", + "notify_push is installed but not yet active" : "notify_push è installato ma non ancora attivo", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push è installato ma OpenRegister non ha ancora confermato un push riuscito. Attiva il salvataggio di un oggetto per attivarlo oppure controlla la configurazione di notify_push.", + "notify_push configuration guide" : "guida alla configurazione di notify_push", + "Realtime push active" : "Push in tempo reale attivo", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Le notifiche push in tempo reale sono attive. I client connessi ricevono aggiornamenti istantanei quando gli oggetti vengono creati, aggiornati o eliminati.", + "Bookmarks" : "Bookmarks", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/it.json b/l10n/it.json new file mode 100644 index 0000000000..ddd051e495 --- /dev/null +++ b/l10n/it.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(no subject)": "(nessun oggetto)", + "(no title)": "(nessun titolo)", + "(unnamed)": "(senza nome)", + "(untitled card)": "(scheda senza titolo)", + "Activities": "Attività", + "Add Endpoint": "Aggiungi endpoint", + "Add Source": "Aggiungi origine", + "Add card": "Aggiungi scheda", + "Add contact": "Aggiungi contatto", + "All clear": "Tutto a posto", + "Articles": "Articoli", + "Code": "Codice", + "Compliance": "Conformità", + "Contacts": "Contatti", + "Create Agent": "Crea agente", + "Create Application": "Crea applicazione", + "Create event": "Crea evento", + "Erasure complete": "Cancellazione completata", + "Erasure preview": "Anteprima cancellazione", + "Expand details": "Espandi dettagli", + "Save changes": "Salva modifiche", + "📄 Object Serialization": "📄 Serializzazione oggetto", + "🔢 Vectorization Settings": "🔢 Impostazioni di vettorializzazione", + "💰 View Selection (Cost Optimization)": "💰 Selezione vista (ottimizzazione dei costi)", + "AI Assistant": "Assistente IA", + "AI Chat": "Chat IA", + "API Key": "Chiave API", + "API tokens saved successfully": "Token API salvati correttamente", + "About ConfigSets": "Informazioni sui ConfigSet", + "Action": "Azione", + "Action Distribution": "Distribuzione delle azioni", + "Action:": "Azione:", + "Actions": "Azioni", + "Active": "Attivo", + "Active Collections": "Collezioni attive", + "Active Filters": "Filtri attivi", + "Active Organisations": "Organizzazioni attive", + "Active filters:": "Filtri attivi:", + "Active organisation set successfully": "Organizzazione attiva impostata correttamente", + "Activity Period": "Periodo di attività", + "Add Groups": "Aggiungi gruppi", + "Add Schema": "Aggiungi Schema", + "Add schema titles, descriptions, and register information to provide richer context for search": "Aggiungi titoli, descrizioni dello Schema e informazioni sul Registro per fornire un contesto più ricco alla ricerca", + "Add to favorites": "Aggiungi ai preferiti", + "Added": "Aggiunto", + "Added to favorites": "Aggiunto ai preferiti", + "Additional Information": "Informazioni aggiuntive", + "Additional feedback saved. Thank you!": "Feedback aggiuntivo salvato. Grazie!", + "Admin": "Amministratore", + "Advanced": "Avanzate", + "Advanced Filters": "Filtri avanzati", + "Advanced Options": "Opzioni avanzate", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "I filtri avanzati non sono disponibili quando si utilizza l'origine database. Passare ad Auto o all'indice SOLR per le opzioni di filtraggio.", + "After": "Dopo", + "Agent deleted successfully": "Agente eliminato correttamente", + "Agents": "Agenti", + "All": "Tutti", + "All Categories": "Tutte le categorie", + "All Collections": "Tutte le collezioni", + "All Confidence Levels": "Tutti i livelli di confidenza", + "All ConfigSets": "Tutti i ConfigSet", + "All Files": "Tutti i file", + "All Levels": "Tutti i livelli", + "All Types": "Tutti i tipi", + "All Webhooks": "Tutti i webhook", + "All actions": "Tutte le azioni", + "All audit trails cleared successfully": "Tutti i registri di controllo cancellati correttamente", + "All registers": "Tutti i registri", + "All schemas": "Tutti gli schema", + "All searches": "Tutte le ricerche", + "All users": "Tutti gli utenti", + "An error occurred while clearing audit trails": "Si è verificato un errore durante la cancellazione dei registri di controllo", + "An error occurred while deleting the audit trail": "Si è verificato un errore durante l'eliminazione del registro di controllo", + "An error occurred while deleting the view": "Si è verificato un errore durante l'eliminazione della vista", + "An error occurred while permanently deleting the objects": "Si è verificato un errore durante l'eliminazione permanente degli oggetti", + "Analysis completed:": "Analisi completata:", + "Analysis steps:": "Fasi dell'analisi:", + "Analytics": "Analisi", + "Analyze Objects": "Analizza oggetti", + "Analyze Properties": "Analizza proprietà", + "Analyze existing properties for improvement opportunities": "Analizza le proprietà esistenti per individuare opportunità di miglioramento", + "Analyzing...": "Analisi in corso...", + "Any favorites and sharing settings for this view": "Eventuali preferiti e impostazioni di condivisione di questa vista", + "Any user": "Qualsiasi utente", + "App store cache invalidated successfully": "Cache dell'App Store invalidata correttamente", + "Application deleted successfully": "Applicazione eliminata correttamente", + "Applications": "Applicazioni", + "Apply Changes": "Applica modifiche", + "Archive": "Archivia", + "Archive conversation": "Archivia conversazione", + "Archived conversations are hidden from your active list": "Le conversazioni archiviate sono nascoste dal tuo elenco attivo", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Vuoi davvero ripulire i vecchi registri di ricerca? Questa operazione eliminerà le voci più vecchie di 30 giorni.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Vuoi davvero eliminare i registri di controllo selezionati? Questa azione non può essere annullata.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Vuoi davvero eliminare i registri di ricerca selezionati? Questa azione non può essere annullata.", + "Are you sure you want to delete this ConfigSet?": "Vuoi davvero eliminare questo ConfigSet?", + "Are you sure you want to permanently delete": "Vuoi davvero eliminare permanentemente", + "Ask a question...": "Poni una domanda...", + "Ask questions about your data using natural language": "Poni domande sui tuoi dati utilizzando il linguaggio naturale", + "Attempt": "Tentativo", + "Audit Trail Changes": "Modifiche del registro di controllo", + "Audit Trail Details": "Dettagli del registro di controllo", + "Audit Trail Management": "Gestione del registro di controllo", + "Audit Trail Statistics": "Statistiche del registro di controllo", + "Audit Trails": "Registri di controllo", + "Audit trail data copied to clipboard": "Dati del registro di controllo copiati negli appunti", + "Audit trail deleted successfully": "Registro di controllo eliminato correttamente", + "Audit trail successfully deleted": "Registro di controllo eliminato correttamente", + "Authentication": "Autenticazione", + "Auto-commit disabled": "Commit automatico disabilitato", + "Auto-commit enabled": "Commit automatico abilitato", + "Auto-create Default Organisation": "Crea automaticamente organizzazione predefinita", + "Auto-retry failed vectorizations": "Riprova automaticamente le vettorializzazioni non riuscite", + "Automatically commit changes to SOLR index": "Esegui automaticamente il commit delle modifiche all'indice SOLR", + "Automatically create a default organisation if none exists when the app is initialized": "Crea automaticamente un'organizzazione predefinita se non ne esiste alcuna all'inizializzazione dell'app", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Genera automaticamente embedding vettoriali dai blocchi di testo quando i file vengono caricati ed elaborati", + "Automatically generate vector embeddings when objects are created or updated": "Genera automaticamente embedding vettoriali quando gli oggetti vengono creati o aggiornati", + "Automatically retry failed vectorization attempts (max 3 retries)": "Riprova automaticamente i tentativi di vettorializzazione non riusciti (massimo 3 tentativi)", + "Available Filters": "Filtri disponibili", + "Available Workflows": "Flussi di lavoro disponibili", + "Avg Execution Time": "Tempo medio di esecuzione", + "Avg Members/Org": "Membri medi/org.", + "Avg Object Views/Session": "Visualizzazioni medie oggetti/sessione", + "Avg Response Time": "Tempo medio di risposta", + "Avg Results/Search": "Risultati medi/ricerca", + "Avg Searches/Session": "Ricerche medie/sessione", + "Back": "Indietro", + "Back to Entities": "Torna alle entità", + "Back to Registers": "Torna ai registri", + "Back to Webhooks": "Torna ai webhook", + "Back to applications": "Torna alle applicazioni", + "Back to entities": "Torna alle entità", + "Backend updated successfully. Please reload the application.": "Backend aggiornato correttamente. Ricaricare l'applicazione.", + "Base URL (Optional)": "URL di base (facoltativo)", + "Basic Information": "Informazioni di base", + "Batch Size": "Dimensione del lotto", + "Batch extraction completed": "Estrazione in batch completata", + "Before": "Prima", + "Before object vectorization can work:": "Prima che la vettorializzazione degli oggetti possa funzionare:", + "Behavior Issue": "Problema di comportamento", + "Blob storage has been retired. All objects now use magic tables.": "L'archiviazione blob è stata dismessa. Tutti gli oggetti utilizzano ora le tabelle magiche.", + "Branch": "Ramo", + "Bulk delete operation completed successfully": "Operazione di eliminazione massiva completata correttamente", + "Bulk save operation completed successfully": "Operazione di salvataggio massivo completata correttamente", + "Business Data": "Dati aziendali", + "Cache cleared successfully": "Cache cancellata correttamente", + "Calculate Sizes": "Calcola dimensioni", + "Cancel": "Annulla", + "Cannot delete: objects are still attached": "Impossibile eliminare: sono ancora presenti oggetti collegati", + "Category": "Categoria", + "Change Type": "Tipo di modifica", + "Changes": "Modifiche", + "Changes copied to clipboard": "Modifiche copiate negli appunti", + "Chat Model": "Modello di chat", + "Chat Provider": "Provider di chat", + "Chat Provider (RAG)": "Provider di chat (RAG)", + "Chat Settings": "Impostazioni della chat", + "Chat provider connection successful!": "Connessione al provider di chat riuscita!", + "Chat settings": "Impostazioni della chat", + "Choose a register": "Scegli un Registro", + "Choose a schema": "Scegli uno Schema", + "Choose how vector similarity calculations are performed for semantic search": "Scegli come vengono eseguiti i calcoli di similarità vettoriale per la ricerca semantica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Scegli quali tipi di file includere nel processo di vettorializzazione. Verranno elaborati solo i file con testo e blocchi estratti.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Scegli quali viste includere nel processo di vettorializzazione. Lascia vuoto per elaborare tutte le viste in base alla tua configurazione.", + "Chunk": "Blocco", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Eliminazione dei blocchi non ancora implementata. Utilizzare gli endpoint basati su blocchi.", + "Chunks": "Blocchi", + "Chunks to Vectorize:": "Blocchi da vettorializzare:", + "Cleanup Old Trails": "Ripulisci vecchi registri", + "Cleanup completed": "Pulizia completata", + "Clear All Embeddings": "Cancella tutti gli embedding", + "Clear Entries": "Cancella voci", + "Clear Filtered Audit Trails": "Cancella registri di controllo filtrati", + "Clear Filters": "Cancella filtri", + "Clear Index": "Cancella indice", + "Clear Selection": "Cancella selezione", + "Clear all": "Cancella tutto", + "Clear all filters": "Cancella tutti i filtri", + "Clear date range": "Cancella intervallo di date", + "Clearing...": "Cancellazione in corso...", + "Close": "Chiudi", + "Collection Management": "Gestione collezioni", + "Collection Name": "Nome collezione", + "Collection assignments updated successfully": "Assegnazioni delle collezioni aggiornate correttamente", + "Collection cleared successfully": "Collezione cancellata correttamente", + "Collection copied successfully": "Collezione copiata correttamente", + "Collection created successfully": "Collezione creata correttamente", + "Collection deleted successfully": "Collezione eliminata correttamente", + "Collection used to store and index file metadata and content": "Collezione utilizzata per archiviare e indicizzare metadati e contenuti dei file", + "Collection used to store and index object data": "Collezione utilizzata per archiviare e indicizzare i dati degli oggetti", + "Collections:": "Collezioni:", + "Commit Message": "Messaggio di commit", + "Commit Within (ms)": "Commit entro (ms)", + "Commit message": "Messaggio di commit", + "Compare current schema with real object data": "Confronta lo Schema attuale con i dati reali degli oggetti", + "Completed": "Completato", + "Complex": "Complessa", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Query complesse: ricerche avanzate con più filtri, operatori e combinazioni complesse di parametri", + "Confidence Level": "Livello di confidenza", + "Confidence Score": "Punteggio di confidenza", + "Config must be provided as an object": "La configurazione deve essere fornita come oggetto", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Gestione ConfigSet", + "ConfigSet Name": "Nome ConfigSet", + "ConfigSet created successfully": "ConfigSet creato correttamente", + "ConfigSet deleted successfully": "ConfigSet eliminato correttamente", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "I ConfigSet definiscono lo schema e la configurazione delle tue collezioni SOLR. Contengono definizioni dei campi, analizzatori e altre impostazioni di ricerca.", + "Configuration": "Configurazione", + "Configuration Data": "Dati di configurazione", + "Configuration saved": "Configurazione salvata", + "Configurations": "Configurazioni", + "Configure Facets": "Configura faccette", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Configura i provider di Large Language Model (LLM) per le funzionalità basate sull'IA, tra cui ricerca semantica, embedding e chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Configura le impostazioni di base della connessione per il tuo server SOLR, comprese le opzioni di autenticazione e di rete. Utilizza le finestre di dialogo separate Gestione ConfigSet e Gestione collezioni per gestire core e collezioni.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Configura come gli oggetti del database vengono convertiti in embedding vettoriali per la ricerca semantica. Gli oggetti vengono vettorializzati direttamente senza necessità di estrazione del testo.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Configura come gli oggetti vengono convertiti in testo prima della vettorializzazione. Queste impostazioni influiscono sulla qualità e sul contesto della ricerca.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Configura i parametri per la vettorializzazione dei file. Questo processo genererà embedding vettoriali per tutti i blocchi di file estratti.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Configura i parametri per la vettorializzazione degli oggetti. Questo processo genererà embedding vettoriali per tutti gli oggetti corrispondenti ai filtri della tua vista.", + "Configure which types of data to search and how many sources to retrieve": "Configura quali tipi di dati cercare e quante origini recuperare", + "Connection Failed": "Connessione non riuscita", + "Connection Settings": "Impostazioni di connessione", + "Connection Successful!": "Connessione riuscita!", + "Connection failed": "Connessione non riuscita", + "Connection protocol": "Protocollo di connessione", + "Connection successful": "Connessione riuscita", + "Connection timeout in seconds": "Timeout della connessione in secondi", + "Constant delay between retries (5 minutes)": "Ritardo costante tra i tentativi (5 minuti)", + "Constraint Issue": "Problema di vincolo", + "Content Filters": "Filtri dei contenuti", + "Continue to Properties": "Continua con le proprietà", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Controlla quali viste degli oggetti devono essere vettorializzate per ridurre i costi delle API. Vettorializza solo le viste che traggono vantaggio dalla ricerca semantica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Controlla quali viste e strumenti l'IA può utilizzare in questa conversazione. Per impostazione predefinita, tutte le funzionalità dell'agente sono abilitate.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Controlla la casualità (0 = deterministico, 2 = molto creativo)", + "Conversation ID is required": "L'ID della conversazione è obbligatorio", + "Conversation archived": "Conversazione archiviata", + "Conversation archived successfully": "Conversazione archiviata correttamente", + "Conversation cleared successfully": "Conversazione cancellata correttamente", + "Conversation deleted": "Conversazione eliminata", + "Conversation permanently deleted": "Conversazione eliminata permanentemente", + "Conversation renamed": "Conversazione rinominata", + "Conversation restored": "Conversazione ripristinata", + "Conversation title": "Titolo della conversazione", + "Copied!": "Copiato!", + "Copy": "Copia", + "Copy Changes": "Copia modifiche", + "Copy Collection": "Copia collezione", + "Copy Data": "Copia dati", + "Copy Full Data": "Copia tutti i dati", + "Copying...": "Copia in corso...", + "Count": "Conteggio", + "Counting objects...": "Conteggio degli oggetti...", + "Costs": "Costi", + "Create": "Crea", + "Create Collection": "Crea collezione", + "Create ConfigSet": "Crea ConfigSet", + "Create New Collection": "Crea nuova collezione", + "Create New ConfigSet": "Crea nuovo ConfigSet", + "Create Operations": "Operazioni di creazione", + "Create Webhook": "Crea webhook", + "Create a copy of collection:": "Crea una copia della collezione:", + "Create a new ConfigSet based on the _default template": "Crea un nuovo ConfigSet basato sul modello _default", + "Create a new SOLR collection from an existing ConfigSet": "Crea una nuova collezione SOLR da un ConfigSet esistente", + "Create your first AI agent to get started.": "Crea il tuo primo agente IA per iniziare.", + "Created": "Creato", + "Created:": "Creato:", + "Creating...": "Creazione in corso...", + "Current Filters": "Filtri attuali", + "Current Type:": "Tipo attuale:", + "Custom API endpoint if using a different region": "Endpoint API personalizzato se si utilizza una regione diversa", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Intestazioni HTTP personalizzate (una per riga, formato: Nome-Intestazione: valore)", + "Daily": "Giornaliero", + "Dashboard": "Pannello di controllo", + "Data Source": "Origine dati", + "Data sources": "Origini dati", + "Data type does not match observed values": "Il tipo di dato non corrisponde ai valori osservati", + "Database URL": "URL del database", + "Database information refreshed": "Informazioni sul database aggiornate", + "Date Range": "Intervallo di date", + "Default": "Predefinito", + "Default Organisation": "Organizzazione predefinita", + "Default: 5": "Predefinito: 5", + "Delays double with each attempt (2, 4, 8 minutes...)": "I ritardi raddoppiano a ogni tentativo (2, 4, 8 minuti...)", + "Delays increase linearly (5, 10, 15 minutes...)": "I ritardi aumentano in modo lineare (5, 10, 15 minuti...)", + "Delete": "Elimina", + "Delete Application": "Elimina applicazione", + "Delete Audit Trail": "Elimina registro di controllo", + "Delete Collection": "Elimina collezione", + "Delete ConfigSet": "Elimina ConfigSet", + "Delete Objects": "Elimina oggetti", + "Delete Operations": "Operazioni di eliminazione", + "Delete View": "Elimina vista", + "Delete all objects in this schema": "Elimina tutti gli oggetti di questo Schema", + "Delete permanently": "Elimina permanentemente", + "Delete this conversation?": "Eliminare questa conversazione?", + "Delete view": "Elimina vista", + "Deleted": "Eliminato", + "Deleted By": "Eliminato da", + "Deleted Date": "Data di eliminazione", + "Deleted Items Management": "Gestione elementi eliminati", + "Deleted This Week": "Eliminati questa settimana", + "Deleted Today": "Eliminati oggi", + "Deleted:": "Eliminato:", + "Deleting...": "Eliminazione in corso...", + "Deletion Date Range": "Intervallo date di eliminazione", + "Deletion Statistics": "Statistiche di eliminazione", + "Deprecated": "Deprecato", + "Description": "Descrizione", + "Description:": "Descrizione:", + "Detect data types and patterns": "Rileva tipi di dati e schemi", + "Detected At": "Rilevato il", + "Detected Format:": "Formato rilevato:", + "Detected Issues:": "Problemi rilevati:", + "Disable": "Disabilita", + "Disabled": "Disabilitato", + "Discover Files": "Individua file", + "Discovered Properties": "Proprietà individuate", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Vuoi eliminare permanentemente tutte le voci filtrate del registro di controllo? Questa azione non può essere annullata.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Vuoi eliminare permanentemente questa voce del registro di controllo? Questa azione non può essere annullata.", + "Documents": "Documenti", + "Download API Spec": "Scarica specifica API", + "Edit": "Modifica", + "Edit Register": "Modifica Registro", + "Edit Schema": "Modifica Schema", + "Edit View": "Modifica vista", + "Edit Webhook": "Modifica webhook", + "Edit view": "Modifica vista", + "Edit view details": "Modifica dettagli della vista", + "Effectiveness": "Efficacia", + "Email": "Email", + "Emails": "Email", + "Embedding Model": "Modello di embedding", + "Embedding Provider": "Provider di embedding", + "Embedding generated successfully": "Embedding generato correttamente", + "Embedding provider connection successful!": "Connessione al provider di embedding riuscita!", + "Enable": "Abilita", + "Enable auto-creation": "Abilita creazione automatica", + "Enable automatic file vectorization": "Abilita la vettorializzazione automatica dei file", + "Enable automatic object vectorization": "Abilita la vettorializzazione automatica degli oggetti", + "Enable automatic synchronization": "Abilita la sincronizzazione automatica", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Abilita la registrazione dettagliata per le operazioni SOLR (consigliato per il debug)", + "Enable faceting": "Abilita le faccette", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Abilita o disabilita le funzionalità LLM. Configura provider e modelli utilizzando il pulsante Configurazione LLM in alto.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Abilita o disabilita l'integrazione della ricerca SOLR. Configura le impostazioni di connessione utilizzando il pulsante Impostazioni di connessione in alto.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Abilita o disabilita l'integrazione dei flussi di lavoro n8n. Configura le impostazioni di connessione di seguito.", + "Enable or disable this webhook": "Abilita o disabilita questo webhook", + "Enable vectorization for all existing and future views (may increase costs)": "Abilita la vettorializzazione per tutte le viste esistenti e future (potrebbe aumentare i costi)", + "Enabled": "Abilitato", + "Endpoints": "Endpoint", + "Enter ConfigSet name": "Inserisci il nome del ConfigSet", + "Enter collection name": "Inserisci il nome della collezione", + "Enter description (optional)...": "Inserisci una descrizione (facoltativo)...", + "Enter new collection name": "Inserisci il nuovo nome della collezione", + "Enter object ID": "Inserisci l'ID dell'oggetto", + "Enter search term": "Inserisci il termine di ricerca", + "Enter view name...": "Inserisci il nome della vista...", + "Enter webhook name": "Inserisci il nome del webhook", + "Entities": "Entità", + "Entity Information": "Informazioni sull'entità", + "Entity deleted successfully": "Entità eliminata correttamente", + "Entity not found": "Entità non trovata", + "Entries to be deleted:": "Voci da eliminare:", + "Enum Issue": "Problema di enumerazione", + "Enum constraint is missing": "Manca il vincolo di enumerazione", + "Error": "Errore", + "Error Details": "Dettagli dell'errore", + "Error Information": "Informazioni sull'errore", + "Error Message": "Messaggio di errore", + "Error loading application": "Errore durante il caricamento dell'applicazione", + "Error loading audit trails": "Errore durante il caricamento dei registri di controllo", + "Error loading entity": "Errore durante il caricamento dell'entità", + "Error loading search trails": "Errore durante il caricamento dei registri di ricerca", + "Estimated Batches:": "Lotti stimati:", + "Estimated Cost:": "Costo stimato:", + "Estimated Duration:": "Durata stimata:", + "Event": "Evento", + "Event Property for Payload": "Proprietà dell'evento per il payload", + "Events": "Eventi", + "Example Value": "Valore di esempio", + "Exclusive Maximum": "Massimo esclusivo", + "Exclusive Minimum": "Minimo esclusivo", + "Execution Mode": "Modalità di esecuzione", + "Execution Time": "Tempo di esecuzione", + "Execution Time Range": "Intervallo del tempo di esecuzione", + "Existing Improvements": "Miglioramenti esistenti", + "Exponential": "Esponenziale", + "Export": "Esporta", + "Export completed successfully": "Esportazione completata correttamente", + "Export, view, or delete audit trails": "Esporta, visualizza o elimina i registri di controllo", + "Extend Schema": "Estendi Schema", + "Extended": "Esteso", + "Extensions": "Estensioni", + "Extract Pending Files": "Estrai file in attesa", + "Extract properties from each object": "Estrai le proprietà da ciascun oggetto", + "Extracted At": "Estratto il", + "Extraction Status": "Stato dell'estrazione", + "Facet configuration updated successfully": "Configurazione delle faccette aggiornata correttamente", + "Facets discovered and configured successfully": "Faccette individuate e configurate correttamente", + "Facets discovered successfully": "Faccette individuate correttamente", + "Failed": "Non riuscito", + "Failed to analyze schema properties": "Impossibile analizzare le proprietà dello Schema", + "Failed to archive conversation": "Impossibile archiviare la conversazione", + "Failed to calculate sizes": "Impossibile calcolare le dimensioni", + "Failed to clear collection": "Impossibile cancellare la collezione", + "Failed to copy changes": "Impossibile copiare le modifiche", + "Failed to copy data": "Impossibile copiare i dati", + "Failed to copy data to clipboard": "Impossibile copiare i dati negli appunti", + "Failed to create conversation": "Impossibile creare la conversazione", + "Failed to create or find project": "Impossibile creare o trovare il progetto", + "Failed to delete collection": "Impossibile eliminare la collezione", + "Failed to delete conversation": "Impossibile eliminare la conversazione", + "Failed to delete webhook": "Impossibile eliminare il webhook", + "Failed to download API specification": "Impossibile scaricare la specifica API", + "Failed to get SOLR field configuration": "Impossibile ottenere la configurazione dei campi SOLR", + "Failed to get entity categories": "Impossibile ottenere le categorie delle entità", + "Failed to get entity statistics": "Impossibile ottenere le statistiche delle entità", + "Failed to get entity types": "Impossibile ottenere i tipi di entità", + "Failed to load LLM configuration": "Impossibile caricare la configurazione LLM", + "Failed to load Nextcloud groups": "Impossibile caricare i gruppi Nextcloud", + "Failed to load conversation": "Impossibile caricare la conversazione", + "Failed to load entities": "Impossibile caricare le entità", + "Failed to load entity": "Impossibile caricare l'entità", + "Failed to load files": "Impossibile caricare i file", + "Failed to load organisations": "Impossibile caricare le organizzazioni", + "Failed to load templates": "Impossibile caricare i modelli", + "Failed to load webhooks": "Impossibile caricare i webhook", + "Failed to load workflows": "Impossibile caricare i flussi di lavoro", + "Failed to refresh database information": "Impossibile aggiornare le informazioni sul database", + "Failed to reindex collection": "Impossibile reindicizzare la collezione", + "Failed to rename conversation": "Impossibile rinominare la conversazione", + "Failed to restore conversation": "Impossibile ripristinare la conversazione", + "Failed to retry extraction": "Impossibile riprovare l'estrazione", + "Failed to retry webhook": "Impossibile riprovare il webhook", + "Failed to save GitHub token": "Impossibile salvare il token GitHub", + "Failed to save GitLab URL": "Impossibile salvare l'URL GitLab", + "Failed to save GitLab token": "Impossibile salvare il token GitLab", + "Failed to save additional feedback": "Impossibile salvare il feedback aggiuntivo", + "Failed to save n8n configuration": "Impossibile salvare la configurazione n8n", + "Failed to save roles": "Impossibile salvare i ruoli", + "Failed to save settings": "Impossibile salvare le impostazioni", + "Failed to save webhook": "Impossibile salvare il webhook", + "Failed to send feedback": "Impossibile inviare il feedback", + "Failed to test webhook": "Impossibile testare il webhook", + "Failed to update favorite status": "Impossibile aggiornare lo stato dei preferiti", + "Failed to update schema properties": "Impossibile aggiornare le proprietà dello Schema", + "Failed to update view": "Impossibile aggiornare la vista", + "Failed to update webhook": "Impossibile aggiornare il webhook", + "Feedback recorded": "Feedback registrato", + "Fewer sources (1-3):": "Meno origini (1-3):", + "Field": "Campo", + "File": "File", + "File Chunk Prediction": "Previsione blocchi di file", + "File Collection": "Collezione di file", + "File Management": "Gestione file", + "File Name": "Nome del file", + "File Path": "Percorso del file", + "File Type Selection": "Selezione del tipo di file", + "File Vectorization": "Vettorializzazione dei file", + "File Warmup": "Preriscaldamento file", + "File actions menu": "Menu delle azioni sui file", + "File anonymized successfully": "File anonimizzato correttamente", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "La vettorializzazione dei blocchi di file non è ancora implementata. I blocchi sono pronti e archiviati, ma il servizio di vettorializzazione è in fase di sviluppo.", + "File collection not configured": "Collezione di file non configurata", + "File discovery completed": "Individuazione dei file completata", + "File extraction completed": "Estrazione del file completata", + "File extraction queued": "Estrazione del file in coda", + "File indexed successfully": "File indicizzato correttamente", + "File is already anonymized": "Il file è già anonimizzato", + "File not found": "File non trovato", + "File settings updated successfully": "Impostazioni del file aggiornate correttamente", + "File sources": "Origini dei file", + "File vectorization configuration saved successfully": "Configurazione della vettorializzazione dei file salvata correttamente", + "File vectorization started. Check the statistics section for progress.": "Vettorializzazione dei file avviata. Controlla la sezione delle statistiche per lo stato di avanzamento.", + "File warmup completed": "Preriscaldamento dei file completato", + "Files": "File", + "Files with Completed Extraction:": "File con estrazione completata:", + "Files → fileCollection, Objects → objectCollection": "File → fileCollection, Oggetti → objectCollection", + "Filter Audit Trails": "Filtra registri di controllo", + "Filter Deleted Items": "Filtra elementi eliminati", + "Filter Objects": "Filtra oggetti", + "Filter Properties": "Filtra proprietà", + "Filter Search Trails": "Filtra registri di ricerca", + "Filter Statistics": "Statistiche dei filtri", + "Filter and analyze search trail entries": "Filtra e analizza le voci del registro di ricerca", + "Filter and manage audit trail entries": "Filtra e gestisci le voci del registro di controllo", + "Filter and manage soft deleted items": "Filtra e gestisci gli elementi eliminati provvisoriamente", + "Filter by object ID": "Filtra per ID oggetto", + "Filter by search term": "Filtra per termine di ricerca", + "Filter by webhook": "Filtra per webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Dati dei filtri caricati automaticamente. Utilizza i filtri sottostanti per affinare la tua ricerca.", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtra gli attivatori dei webhook in base alle proprietà del payload (una per riga, formato: chiave: valore)", + "Filtered": "Filtrato", + "Filters": "Filtri", + "Fireworks AI Chat Settings": "Impostazioni della chat Fireworks AI", + "Fireworks AI Embedding Configuration": "Configurazione dell'embedding Fireworks AI", + "First": "Primo", + "Fixed": "Fisso", + "For chat and retrieval-augmented generation": "Per la chat e la generazione aumentata dal recupero", + "For vector embeddings and semantic search": "Per gli embedding vettoriali e la ricerca semantica", + "Format": "Formato", + "Format Issue": "Problema di formato", + "Format constraint is missing": "Manca il vincolo di formato", + "From Date": "Data di inizio", + "From date": "Data di inizio", + "Full data copied to clipboard": "Tutti i dati copiati negli appunti", + "General Issue": "Problema generale", + "Generate recommendations and confidence scores": "Genera raccomandazioni e punteggi di confidenza", + "Generate vectors immediately when new objects are created": "Genera vettori immediatamente quando vengono creati nuovi oggetti", + "GitHub token is valid": "Il token GitHub è valido", + "GitHub token removed successfully": "Token GitHub rimosso correttamente", + "GitHub token saved successfully": "Token GitHub salvato correttamente", + "GitLab URL saved successfully": "URL GitLab salvato correttamente", + "GitLab token is valid": "Il token GitLab è valido", + "GitLab token saved successfully": "Token GitLab salvato correttamente", + "HTTP Method": "Metodo HTTP", + "HTTP method used to send webhook requests": "Metodo HTTP utilizzato per inviare le richieste webhook", + "Headers": "Intestazioni", + "Health": "Stato", + "Heartbeat successful - connection kept alive": "Heartbeat riuscito - connessione mantenuta attiva", + "Helpful": "Utile", + "Hide Filters": "Nascondi filtri", + "Hide in forms": "Nascondi nei moduli", + "Hide in list view": "Nascondi nella vista elenco", + "High": "Alta", + "High Confidence": "Confidenza alta", + "Host": "Host", + "Hourly": "Ogni ora", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Quanto in profondità attraversare le proprietà annidate degli oggetti (1-20). Valori più alti catturano più dettagli ma aumentano la dimensione del vettore.", + "How often to check for updates (1-168 hours)": "Con quale frequenza verificare la disponibilità di aggiornamenti (1-168 ore)", + "How to handle retries for failed webhook deliveries": "Come gestire i nuovi tentativi per le consegne di webhook non riuscite", + "ID": "ID", + "ID:": "ID:", + "IP rate limits cleared successfully": "Limiti di frequenza IP cancellati correttamente", + "Identify properties not in the schema": "Identifica le proprietà non presenti nello Schema", + "Immutable": "Immutabile", + "Import": "Importa", + "Import successful": "Importazione riuscita", + "Improved Property": "Proprietà migliorata", + "In use": "In uso", + "Inactive": "Inattivo", + "Include IDs and names of related objects for better contextual search": "Includi gli ID e i nomi degli oggetti correlati per una migliore ricerca contestuale", + "Include related object references": "Includi i riferimenti agli oggetti correlati", + "Include schema and register metadata": "Includi i metadati di Schema e Registro", + "Initialization failed": "Inizializzazione non riuscita", + "Initialize Project": "Inizializza progetto", + "Inspect Fields": "Ispeziona campi", + "Inspect Index": "Ispeziona indice", + "Invalid": "Non valido", + "Invalid batch size. Must be between 1 and 5000": "Dimensione del lotto non valida. Deve essere compresa tra 1 e 5000", + "Invalid field name provided": "Nome del campo fornito non valido", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects non valido. Deve essere 0 (tutti) o un numero positivo", + "Invalid:": "Non valido:", + "Issue": "Problema", + "Items per page:": "Elementi per pagina:", + "Just now": "Adesso", + "LLM Configuration": "Configurazione LLM", + "LLM actions menu": "Menu delle azioni LLM", + "LLM configuration saved successfully": "Configurazione LLM salvata correttamente", + "LLM features disabled": "Funzionalità LLM disabilitate", + "LLM features enabled": "Funzionalità LLM abilitate", + "LLM must be enabled with an embedding provider configured": "L'LLM deve essere abilitato con un provider di embedding configurato", + "LLM settings updated successfully": "Impostazioni LLM aggiornate correttamente", + "Last": "Ultimo", + "Last Triggered": "Ultimo attivato", + "Length Range:": "Intervallo di lunghezza:", + "Linear": "Lineare", + "Load Advanced Filters": "Carica filtri avanzati", + "Load advanced filters with live data from your search index": "Carica i filtri avanzati con dati in tempo reale dal tuo indice di ricerca", + "Load view": "Carica vista", + "Loading ConfigSets...": "Caricamento dei ConfigSet...", + "Loading advanced filters...": "Caricamento dei filtri avanzati...", + "Loading agents...": "Caricamento degli agenti...", + "Loading application details...": "Caricamento dei dettagli dell'applicazione...", + "Loading applications...": "Caricamento delle applicazioni...", + "Loading archived conversations...": "Caricamento delle conversazioni archiviate...", + "Loading audit trails...": "Caricamento dei registri di controllo...", + "Loading collections...": "Caricamento delle collezioni...", + "Loading configurations...": "Caricamento delle configurazioni...", + "Loading conversation...": "Caricamento della conversazione...", + "Loading conversations...": "Caricamento delle conversazioni...", + "Loading deleted items...": "Caricamento degli elementi eliminati...", + "Loading events...": "Caricamento degli eventi...", + "Loading filter data...": "Caricamento dei dati dei filtri...", + "Loading groups...": "Caricamento dei gruppi...", + "Loading log details...": "Caricamento dei dettagli del log...", + "Loading registers...": "Caricamento dei registri...", + "Loading schemas...": "Caricamento degli schema...", + "Loading search trails...": "Caricamento dei registri di ricerca...", + "Loading sources...": "Caricamento delle origini...", + "Loading statistics...": "Caricamento delle statistiche...", + "Loading users...": "Caricamento degli utenti...", + "Loading views...": "Caricamento delle viste...", + "Loading...": "Caricamento...", + "Local": "Locale", + "Local Version": "Versione locale", + "Locked": "Bloccato", + "Locked:": "Bloccato:", + "Login successful": "Accesso riuscito", + "Logs": "Log", + "Low": "Bassa", + "Low Confidence": "Confidenza bassa", + "Lucene Version:": "Versione Lucene:", + "Magic table synchronized successfully": "Tabella magica sincronizzata correttamente", + "Manage Organisation Roles": "Gestisci ruoli dell'organizzazione", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Gestisci le collezioni SOLR (archivi dati) e assegnale a oggetti e file.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Gestisci i ConfigSet SOLR (modelli di configurazione) per le tue collezioni.", + "Manage and configure agents for automated tasks": "Gestisci e configura gli agenti per attività automatizzate", + "Manage and monitor file text extraction status": "Gestisci e monitora lo stato dell'estrazione del testo dai file", + "Manage and restore soft deleted items from your registers": "Gestisci e ripristina gli elementi eliminati provvisoriamente dai tuoi registri", + "Manage and view detected entities from files and objects": "Gestisci e visualizza le entità rilevate da file e oggetti", + "Manage document templates and themes": "Gestisci modelli e temi dei documenti", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Gestisci i flussi di lavoro n8n per l'automazione di OpenRegister. I flussi di lavoro saranno archiviati nel progetto configurato.", + "Manage webhooks for event-driven integrations": "Gestisci i webhook per integrazioni basate su eventi", + "Manage your applications and modules": "Gestisci le tue applicazioni e i tuoi moduli", + "Manage your data registers and their configurations": "Gestisci i tuoi registri di dati e le relative configurazioni", + "Manage your data schemas and their properties": "Gestisci i tuoi schema di dati e le relative proprietà", + "Manage your data sources and their configurations": "Gestisci le tue origini dati e le relative configurazioni", + "Manage your saved search configurations": "Gestisci le tue configurazioni di ricerca salvate", + "Manage your system configurations and settings": "Gestisci le configurazioni e le impostazioni di sistema", + "Managed": "Gestito", + "Max Files (0 = all)": "Numero massimo di file (0 = tutti)", + "Max Length": "Lunghezza massima", + "Max Length:": "Lunghezza massima:", + "Max Objects (0 = all)": "Numero massimo di oggetti (0 = tutti)", + "Max Retries": "Numero massimo di tentativi", + "Max Shards/Node": "Shard massimi/nodo", + "Max execution time (ms)": "Tempo massimo di esecuzione (ms)", + "Max ms": "Ms massimi", + "Max result count": "Conteggio massimo dei risultati", + "Max results": "Risultati massimi", + "Maximum": "Massimo", + "Maximum Nesting Depth": "Profondità massima di annidamento", + "Maximum length constraint is missing": "Manca il vincolo di lunghezza massima", + "Maximum length is too restrictive": "La lunghezza massima è troppo restrittiva", + "Maximum number of files to process. Set to 0 to process all files.": "Numero massimo di file da elaborare. Imposta su 0 per elaborare tutti i file.", + "Maximum number of objects to process. Set to 0 to process all objects.": "Numero massimo di oggetti da elaborare. Imposta su 0 per elaborare tutti gli oggetti.", + "Maximum number of retry attempts for failed deliveries": "Numero massimo di tentativi per le consegne non riuscite", + "Maximum time to wait before committing changes": "Tempo massimo di attesa prima di eseguire il commit delle modifiche", + "Maximum tokens to generate": "Numero massimo di token da generare", + "Maximum value constraint is missing": "Manca il vincolo di valore massimo", + "Maximum value is too restrictive": "Il valore massimo è troppo restrittivo", + "Medium": "Media", + "Medium Confidence": "Confidenza media", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Query medie: ricerche con qualche filtraggio o più parametri (es. intervalli di date, registri/schema specifici)", + "Members of selected groups can access this view": "I membri dei gruppi selezionati possono accedere a questa vista", + "Members:": "Membri:", + "Memory prediction calculated successfully": "Previsione della memoria calcolata correttamente", + "Message content is required": "Il contenuto del messaggio è obbligatorio", + "Message does not belong to this conversation": "Il messaggio non appartiene a questa conversazione", + "Metadata Filters": "Filtri dei metadati", + "Method": "Metodo", + "Min Length": "Lunghezza minima", + "Min execution time (ms)": "Tempo minimo di esecuzione (ms)", + "Min ms": "Ms minimi", + "Min result count": "Conteggio minimo dei risultati", + "Min results": "Risultati minimi", + "Minimum": "Minimo", + "Minimum value constraint is missing": "Manca il vincolo di valore minimo", + "Minimum value is too restrictive": "Il valore minimo è troppo restrittivo", + "Missing object fields created successfully": "Campi oggetto mancanti creati correttamente", + "Mode:": "Modalità:", + "Model": "Modello", + "Model Name": "Nome del modello", + "Modified": "Modificato", + "Monthly": "Mensile", + "More sources (10-20):": "Più origini (10-20):", + "Most Active Objects": "Oggetti più attivi", + "Multiple Of": "Multiplo di", + "Name": "Nome", + "Names cache warmup completed": "Preriscaldamento della cache dei nomi completato", + "New Collection Name": "Nome della nuova collezione", + "New Conversation": "Nuova conversazione", + "New Properties": "Nuove proprietà", + "New Property": "Nuova proprietà", + "New Value": "Nuovo valore", + "New users without specific organisation membership will be automatically added to this organisation": "I nuovi utenti senza un'appartenenza specifica a un'organizzazione verranno aggiunti automaticamente a questa organizzazione", + "Next": "Successivo", + "No": "No", + "No ConfigSets found": "Nessun ConfigSet trovato", + "No GitHub token configured": "Nessun token GitHub configurato", + "No GitHub token provided": "Nessun token GitHub fornito", + "No GitLab token provided": "Nessun token GitLab fornito", + "No Organisation": "Nessuna organizzazione", + "No active filters": "Nessun filtro attivo", + "No activity data available": "Nessun dato di attività disponibile", + "No agents available": "Nessun agente disponibile", + "No agents found": "Nessun agente trovato", + "No app store cache exists yet": "Non esiste ancora alcuna cache dell'App Store", + "No applications are available.": "Nessuna applicazione disponibile.", + "No applications found": "Nessuna applicazione trovata", + "No archived conversations": "Nessuna conversazione archiviata", + "No audit trail entries found": "Nessuna voce del registro di controllo trovata", + "No changes recorded": "Nessuna modifica registrata", + "No chunks to vectorize": "Nessun blocco da vettorializzare", + "No collections found": "Nessuna collezione trovata", + "No configuration data": "Nessun dato di configurazione", + "No configurations are available.": "Nessuna configurazione disponibile.", + "No configurations found": "Nessuna configurazione trovata", + "No configurations found for this application.": "Nessuna configurazione trovata per questa applicazione.", + "No conversations yet. Create a new one to get started!": "Ancora nessuna conversazione. Creane una nuova per iniziare!", + "No data available": "Nessun dato disponibile", + "No data available for chart": "Nessun dato disponibile per il grafico", + "No deleted items found": "Nessun elemento eliminato trovato", + "No deletion data available": "Nessun dato di eliminazione disponibile", + "No description found": "Nessuna descrizione trovata", + "No description provided": "Nessuna descrizione fornita", + "No entities detected in this file. Run text extraction first.": "Nessuna entità rilevata in questo file. Eseguire prima l'estrazione del testo.", + "No entities found": "Nessuna entità trovata", + "No entities have been detected yet": "Non è stata ancora rilevata alcuna entità", + "No events found": "Nessun evento trovato", + "No expired audit trails found to clear": "Nessun registro di controllo scaduto trovato da cancellare", + "No facetable fields available. Select a register and schema to see available filters.": "Nessun campo utilizzabile come faccetta disponibile. Seleziona un Registro e uno Schema per visualizzare i filtri disponibili.", + "No files found": "Nessun file trovato", + "No files have been extracted yet": "Non è stato ancora estratto alcun file", + "No files to process": "Nessun file da elaborare", + "No files to reindex": "Nessun file da reindicizzare", + "No filters are currently active. This will delete ALL audit trail entries!": "Nessun filtro è attualmente attivo. Questa operazione eliminerà TUTTE le voci del registro di controllo!", + "No log entries found": "Nessuna voce di log trovata", + "No logs are available for this configuration.": "Nessun log disponibile per questa configurazione.", + "No logs are available for this source.": "Nessun log disponibile per questa origine.", + "No logs found": "Nessun log trovato", + "No mismatched fields found - SOLR schema is properly configured": "Nessun campo non corrispondente trovato - lo schema SOLR è configurato correttamente", + "No objects found": "Nessun oggetto trovato", + "No objects found to vectorize": "Nessun oggetto trovato da vettorializzare", + "No objects selected for deletion": "Nessun oggetto selezionato per l'eliminazione", + "No objects to delete": "Nessun oggetto da eliminare", + "No objects were permanently deleted": "Nessun oggetto è stato eliminato permanentemente", + "No properties found": "Nessuna proprietà trovata", + "No properties match your filters.": "Nessuna proprietà corrisponde ai tuoi filtri.", + "No purge date set": "Nessuna data di eliminazione definitiva impostata", + "No register data available": "Nessun dato del Registro disponibile", + "No registers found": "Nessun Registro trovato", + "No registers found for this application.": "Nessun Registro trovato per questa applicazione.", + "No relations found": "Nessuna relazione trovata", + "No request body available": "Nessun corpo della richiesta disponibile", + "No response body available": "Nessun corpo della risposta disponibile", + "No saved views yet. Create one in the Search tab!": "Ancora nessuna vista salvata. Creane una nella scheda Ricerca!", + "No schema data available": "Nessun dato dello Schema disponibile", + "No schema selected for exploration": "Nessuno Schema selezionato per l'esplorazione", + "No schemas found": "Nessuno Schema trovato", + "No schemas found for this application.": "Nessuno Schema trovato per questa applicazione.", + "No search terms data available": "Nessun dato sui termini di ricerca disponibile", + "No search trail entries found": "Nessuna voce del registro di ricerca trovata", + "No sources are available.": "Nessuna origine disponibile.", + "No sources found": "Nessuna origine trovata", + "No templates found": "Nessun modello trovato", + "No templates have been created yet": "Non è stato ancora creato alcun modello", + "No valid magic-mapped register+schema combinations found": "Nessuna combinazione valida Registro+Schema mappata con magic table trovata", + "No views found. Create views first before configuring vectorization.": "Nessuna vista trovata. Crea prima delle viste prima di configurare la vettorializzazione.", + "No views match your search": "Nessuna vista corrisponde alla tua ricerca", + "No webhooks found": "Nessun webhook trovato", + "No webhooks have been configured yet": "Non è stato ancora configurato alcun webhook", + "No workflows found in this project. Create workflows in the n8n editor.": "Nessun flusso di lavoro trovato in questo progetto. Crea flussi di lavoro nell'editor n8n.", + "None": "Nessuno", + "Not helpful": "Non utile", + "Not in use": "Non in uso", + "Number Constraints": "Vincoli numerici", + "Number of chunks to vectorize in one API call (1-100).": "Numero di blocchi da vettorializzare in una singola chiamata API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Numero di blocchi da vettorializzare in una singola chiamata API. Più alto = più veloce ma più memoria. Consigliato: 10-50.", + "Number of objects to process in each batch (1-5000).": "Numero di oggetti da elaborare in ciascun lotto (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Numero di oggetti da vettorializzare in una singola chiamata API. Più alto = più veloce ma più memoria. Consigliato: 10-50.", + "Numeric Range:": "Intervallo numerico:", + "Object": "Oggetto", + "Object Collection": "Collezione di oggetti", + "Object Count Prediction": "Previsione del conteggio degli oggetti", + "Object ID": "ID oggetto", + "Object Management": "Gestione oggetti", + "Object Vectorization": "Vettorializzazione degli oggetti", + "Object Warmup": "Preriscaldamento oggetti", + "Object collection not configured": "Collezione di oggetti non configurata", + "Object not found": "Oggetto non trovato", + "Object permanently deleted": "Oggetto eliminato permanentemente", + "Object restored successfully": "Oggetto ripristinato correttamente", + "Object settings updated successfully": "Impostazioni dell'oggetto aggiornate correttamente", + "Object sources": "Origini degli oggetti", + "Object unlocked successfully": "Oggetto sbloccato correttamente", + "Object vectorization configuration saved successfully": "Configurazione della vettorializzazione degli oggetti salvata correttamente", + "Object vectorized successfully": "Oggetto vettorializzato correttamente", + "Object:": "Oggetto:", + "Objects": "Oggetti", + "Objects Analyzed": "Oggetti analizzati", + "Objects Distribution": "Distribuzione degli oggetti", + "Objects being analyzed": "Oggetti in fase di analisi", + "Objects by Register": "Oggetti per Registro", + "Objects by Schema": "Oggetti per Schema", + "Objects deletion completed successfully": "Eliminazione degli oggetti completata correttamente", + "Objects in schema": "Oggetti nello Schema", + "Objects to Process:": "Oggetti da elaborare:", + "Objects to be analyzed": "Oggetti da analizzare", + "Objects to be deleted": "Oggetti da eliminare", + "Objects to be validated": "Oggetti da convalidare", + "Objects will be serialized as JSON text before vectorization": "Gli oggetti verranno serializzati come testo JSON prima della vettorializzazione", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Gli oggetti verranno eliminati provvisoriamente (contrassegnati come eliminati ma mantenuti nel database). Possono essere recuperati in seguito se necessario.", + "Old Value": "Valore precedente", + "Oldest Item (days)": "Elemento più vecchio (giorni)", + "Ollama Chat Settings": "Impostazioni della chat Ollama", + "Ollama Embedding Configuration": "Configurazione dell'embedding Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Solo con modifiche", + "Open Register Objects": "Oggetti Open Register", + "Open n8n Editor": "Apri editor n8n", + "OpenAI Chat Settings": "Impostazioni della chat OpenAI", + "OpenAI Embedding Configuration": "Configurazione dell'embedding OpenAI", + "OpenRegister Settings": "Impostazioni di OpenRegister", + "Optional webhook secret for signature verification": "Segreto facoltativo del webhook per la verifica della firma", + "Order": "Ordine", + "Organisation": "Organizzazione", + "Organisation ID": "ID organizzazione", + "Organisation Members": "Membri dell'organizzazione", + "Organisation Statistics": "Statistiche dell'organizzazione", + "Organisation created successfully": "Organizzazione creata correttamente", + "Organisation settings saved successfully": "Impostazioni dell'organizzazione salvate correttamente", + "Organisations": "Organizzazioni", + "Organization": "Organizzazione", + "Organization ID (Optional)": "ID organizzazione (facoltativo)", + "Orphaned Items": "Elementi orfani", + "Owner": "Proprietario", + "Owner:": "Proprietario:", + "Parallel Mode (Faster, more resource intensive)": "Modalità parallela (più veloce, più dispendiosa in termini di risorse)", + "Parallel:": "Parallela:", + "Password": "Password", + "Password for SOLR authentication (optional)": "Password per l'autenticazione SOLR (facoltativa)", + "Path": "Percorso", + "Path in repository": "Percorso nel repository", + "Path where the configuration file will be saved in the repository": "Percorso in cui verrà salvato il file di configurazione nel repository", + "Path where the register OAS file will be saved in the repository": "Percorso in cui verrà salvato il file OAS del Registro nel repository", + "Pattern (regex)": "Pattern (regex)", + "Pattern Issue": "Problema di pattern", + "Pattern constraint is missing": "Manca il vincolo di pattern", + "Patterns:": "Pattern:", + "Pending": "In attesa", + "Permanent Delete Mode": "Modalità di eliminazione permanente", + "Permanently Delete": "Elimina permanentemente", + "Permanently delete already soft-deleted objects": "Elimina permanentemente gli oggetti già eliminati provvisoriamente", + "Permanently delete this conversation?": "Eliminare permanentemente questa conversazione?", + "Person": "Persona", + "Personal Data": "Dati personali", + "Phone": "Telefono", + "Photos": "Foto", + "Please create an agent in the": "Crea un agente nella", + "Please select an agent to continue": "Seleziona un agente per continuare", + "Please select which register and schema to use for the new object": "Seleziona quale Registro e quale Schema utilizzare per il nuovo oggetto", + "Please try again later.": "Riprova più tardi.", + "Please wait while we fetch your agents.": "Attendere mentre recuperiamo i tuoi agenti.", + "Please wait while we fetch your applications.": "Attendere mentre recuperiamo le tue applicazioni.", + "Please wait while we fetch your configurations.": "Attendere mentre recuperiamo le tue configurazioni.", + "Please wait while we fetch your deleted items.": "Attendere mentre recuperiamo i tuoi elementi eliminati.", + "Please wait while we fetch your sources.": "Attendere mentre recuperiamo le tue origini.", + "Polls": "Sondaggi", + "Popular Search Terms": "Termini di ricerca popolari", + "Port": "Porta", + "Prerequisites": "Prerequisiti", + "Press Enter to send, Shift+Enter for new line": "Premi Invio per inviare, Maiusc+Invio per andare a capo", + "Previous": "Precedente", + "Private": "Privato", + "Processes chunks in batches with simulated parallelism.": "Elabora i blocchi in lotti con parallelismo simulato.", + "Processes file chunks sequentially (safest).": "Elabora i blocchi di file in sequenza (più sicuro).", + "Processes objects in chunks with simulated parallelism.": "Elabora gli oggetti a blocchi con parallelismo simulato.", + "Processes objects sequentially (safest).": "Elabora gli oggetti in sequenza (più sicuro).", + "Processing": "Elaborazione", + "Processing Limits": "Limiti di elaborazione", + "Processing...": "Elaborazione in corso...", + "Project initialized successfully": "Progetto inizializzato correttamente", + "Project not found. Please initialize first.": "Progetto non trovato. Eseguire prima l'inizializzazione.", + "Properties": "Proprietà", + "Property Behaviors": "Comportamenti della proprietà", + "Property Configuration:": "Configurazione della proprietà:", + "Property Filters": "Filtri delle proprietà", + "Property Title": "Titolo della proprietà", + "Property Type": "Tipo di proprietà", + "Property can be improved": "La proprietà può essere migliorata", + "Protected": "Protetto", + "Provider": "Provider", + "Provider is required for testing": "Il provider è obbligatorio per il test", + "Public": "Pubblico", + "Public View": "Vista pubblica", + "Public views can be accessed by anyone in the system": "Le viste pubbliche sono accessibili a chiunque nel sistema", + "Publish": "Pubblica", + "Publish Objects": "Pubblica oggetti", + "Publish all objects in this schema": "Pubblica tutti gli oggetti di questo Schema", + "Published": "Pubblicato", + "Published:": "Pubblicato:", + "Publishing...": "Pubblicazione in corso...", + "Purge": "Elimina definitivamente", + "Purge Date": "Data di eliminazione definitiva", + "Query Complexity Distribution": "Distribuzione della complessità delle query", + "Query parameter is required": "Il parametro di query è obbligatorio", + "RAG": "RAG", + "RAG Configuration": "Configurazione RAG", + "Rate limits cleared successfully": "Limiti di frequenza cancellati correttamente", + "Raw Changes Data": "Dati grezzi delle modifiche", + "Re-vectorize on object update": "Rivettorializza all'aggiornamento dell'oggetto", + "Read": "Lettura", + "Recommendations:": "Raccomandazioni:", + "Recommended Type:": "Tipo consigliato:", + "Recommended: 5 sources": "Consigliato: 5 origini", + "Refresh": "Aggiorna", + "Refresh Data": "Aggiorna dati", + "Refresh Stats": "Aggiorna statistiche", + "Refresh Workflows": "Aggiorna flussi di lavoro", + "Refresh dashboard": "Aggiorna pannello di controllo", + "Refresh database info": "Aggiorna informazioni sul database", + "Register": "Registro", + "Register ID": "ID Registro", + "Register Statistics": "Statistiche del Registro", + "Register Totals": "Totali del Registro", + "Register depublished successfully": "Pubblicazione del Registro annullata correttamente", + "Register not found": "Registro non trovato", + "Register objects deletion completed successfully": "Eliminazione degli oggetti del Registro completata correttamente", + "Register published successfully": "Registro pubblicato correttamente", + "Register/Schema Usage": "Utilizzo Registro/Schema", + "Registers": "Registri", + "Reindex": "Reindicizza", + "Reindex completed": "Reindicizzazione completata", + "Reindex completed successfully": "Reindicizzazione completata correttamente", + "Reindex failed": "Reindicizzazione non riuscita", + "Relations": "Relazioni", + "Remote Version": "Versione remota", + "Remove": "Rimuovi", + "Remove filter": "Rimuovi filtro", + "Remove from favorites": "Rimuovi dai preferiti", + "Remove group": "Rimuovi gruppo", + "Removed": "Rimosso", + "Removed from favorites": "Rimosso dai preferiti", + "Rename Conversation": "Rinomina conversazione", + "Rename conversation": "Rinomina conversazione", + "Replicas": "Repliche", + "Repository": "Repository", + "Request Body": "Corpo della richiesta", + "Request Data": "Dati della richiesta", + "Request Details": "Dettagli della richiesta", + "Request body must be JSON with a \"query\" field": "Il corpo della richiesta deve essere JSON con un campo \"query\"", + "Request timeout in seconds": "Timeout della richiesta in secondi", + "Required field": "Campo obbligatorio", + "Required status is inconsistent": "Lo stato di obbligatorietà è incoerente", + "Rerun Search": "Riesegui ricerca", + "Reset Changes": "Ripristina modifiche", + "Reset Filters": "Reimposta filtri", + "Response Body": "Corpo della risposta", + "Response Details": "Dettagli della risposta", + "Response Time:": "Tempo di risposta:", + "Restore": "Ripristina", + "Restore conversation": "Ripristina conversazione", + "Restore or permanently delete items": "Ripristina o elimina permanentemente gli elementi", + "Result Count Range": "Intervallo del conteggio dei risultati", + "Results": "Risultati", + "Retrieve all objects for this schema": "Recupera tutti gli oggetti di questo Schema", + "Retry": "Riprova", + "Retry Failed Extractions": "Riprova estrazioni non riuscite", + "Retry Policy": "Criterio dei nuovi tentativi", + "Retry completed": "Nuovo tentativo completato", + "Retrying...": "Nuovo tentativo in corso...", + "Risk Level": "Livello di rischio", + "Roles updated successfully": "Ruoli aggiornati correttamente", + "SOLR Connection Settings": "Impostazioni di connessione SOLR", + "SOLR Version:": "Versione SOLR:", + "SOLR actions menu": "Menu delle azioni SOLR", + "SOLR base path (usually /solr)": "Percorso di base SOLR (di solito /solr)", + "SOLR is disabled": "SOLR è disabilitato", + "SOLR is not available or not configured": "SOLR non è disponibile o non è configurato", + "SOLR logging disabled": "Registrazione SOLR disabilitata", + "SOLR logging enabled": "Registrazione SOLR abilitata", + "SOLR search disabled": "Ricerca SOLR disabilitata", + "SOLR search enabled": "Ricerca SOLR abilitata", + "SOLR server hostname or IP address": "Hostname o indirizzo IP del server SOLR", + "SOLR server port number (optional, defaults to 8983)": "Numero di porta del server SOLR (facoltativo, predefinito 8983)", + "SOLR setup completed successfully": "Configurazione di SOLR completata correttamente", + "SOLR setup failed": "Configurazione di SOLR non riuscita", + "SOLR setup failed - check logs": "Configurazione di SOLR non riuscita - controllare i log", + "Sample Values:": "Valori di esempio:", + "Save": "Salva", + "Save Configuration": "Salva configurazione", + "Save Connection Settings": "Salva impostazioni di connessione", + "Save Roles": "Salva ruoli", + "Save Settings": "Salva impostazioni", + "Save current search as view": "Salva la ricerca attuale come vista", + "Saved Views": "Viste salvate", + "Saving...": "Salvataggio in corso...", + "Schema": "Schema", + "Schema ID": "ID Schema", + "Schema Name": "Nome Schema", + "Schema Statistics": "Statistiche dello Schema", + "Schema depublished successfully": "Pubblicazione dello Schema annullata correttamente", + "Schema objects deletion completed successfully": "Eliminazione degli oggetti dello Schema completata correttamente", + "Schema published successfully": "Schema pubblicato correttamente", + "Schema successfully created": "Schema creato correttamente", + "Schema successfully updated": "Schema aggiornato correttamente", + "Schemas": "Schema", + "Scheme": "Schema (protocollo)", + "Search / Views": "Ricerca / Viste", + "Search Activity": "Attività di ricerca", + "Search Analytics": "Analisi della ricerca", + "Search Entities": "Cerca entità", + "Search Files": "Cerca file", + "Search GitHub": "Cerca in GitHub", + "Search GitLab": "Cerca in GitLab", + "Search Method": "Metodo di ricerca", + "Search Term": "Termine di ricerca", + "Search Trail Details": "Dettagli del registro di ricerca", + "Search Trail Management": "Gestione del registro di ricerca", + "Search Trail Statistics": "Statistiche del registro di ricerca", + "Search Trails": "Registri di ricerca", + "Search Views": "Cerca viste", + "Search Webhooks": "Cerca webhook", + "Search by file name or path": "Cerca per nome o percorso del file", + "Search by name or URL": "Cerca per nome o URL", + "Search by value": "Cerca per valore", + "Search in Files": "Cerca nei file", + "Search in Objects": "Cerca negli oggetti", + "Search property names...": "Cerca nomi delle proprietà...", + "Search trail deletion not implemented yet": "Eliminazione del registro di ricerca non ancora implementata", + "Search views...": "Cerca viste...", + "Secret": "Segreto", + "Secret key for HMAC signature generation (optional)": "Chiave segreta per la generazione della firma HMAC (facoltativa)", + "Select AI Agent": "Seleziona agente IA", + "Select All": "Seleziona tutto", + "Select ConfigSet": "Seleziona ConfigSet", + "Select File Types to Vectorize:": "Seleziona i tipi di file da vettorializzare:", + "Select HTTP method": "Seleziona il metodo HTTP", + "Select Register and Schema": "Seleziona Registro e Schema", + "Select Views to Vectorize:": "Seleziona le viste da vettorializzare:", + "Select a Nextcloud group to add": "Seleziona un gruppo Nextcloud da aggiungere", + "Select a branch": "Seleziona un ramo", + "Select a model or type a custom model name": "Seleziona un modello o digita un nome di modello personalizzato", + "Select a repository": "Seleziona un repository", + "Select a repository you have write access to": "Seleziona un repository su cui disponi dell'accesso in scrittura", + "Select an AI Agent": "Seleziona un agente IA", + "Select an AI agent to begin chatting with your data.": "Seleziona un agente IA per iniziare a chattare con i tuoi dati.", + "Select backend": "Seleziona backend", + "Select chat model": "Seleziona il modello di chat", + "Select collection for files": "Seleziona la collezione per i file", + "Select collection for objects": "Seleziona la collezione per gli oggetti", + "Select default organisation": "Seleziona l'organizzazione predefinita", + "Select event to listen to...": "Seleziona l'evento da ascoltare...", + "Select model": "Seleziona modello", + "Select options...": "Seleziona opzioni...", + "Select period": "Seleziona periodo", + "Select property to send as payload": "Seleziona la proprietà da inviare come payload", + "Select provider": "Seleziona provider", + "Select registers and schemas to save a view": "Seleziona registri e schema per salvare una vista", + "Select retry policy": "Seleziona il criterio dei nuovi tentativi", + "Select the branch to publish to": "Seleziona il ramo su cui pubblicare", + "Select the event this webhook should listen to": "Seleziona l'evento che questo webhook deve ascoltare", + "Select views to vectorize:": "Seleziona le viste da vettorializzare:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Seleziona quali gruppi Nextcloud sono disponibili per questa organizzazione. Gli utenti di questi gruppi avranno accesso alle risorse dell'organizzazione.", + "Select which data views the AI can search": "Seleziona quali viste dei dati l'IA può cercare", + "Select which property from the event should be used as the webhook payload data": "Seleziona quale proprietà dell'evento deve essere utilizzata come dati del payload del webhook", + "Select which tools the AI can use to perform actions": "Seleziona quali strumenti l'IA può utilizzare per eseguire azioni", + "Selected": "Selezionato", + "Selected Groups": "Gruppi selezionati", + "Selected audit trails deleted successfully": "Registri di controllo selezionati eliminati correttamente", + "Selected search trails deleted successfully": "Registri di ricerca selezionati eliminati correttamente", + "Selected users can access this view": "Gli utenti selezionati possono accedere a questa vista", + "Send additional feedback": "Invia feedback aggiuntivo", + "Send as CloudEvent": "Invia come CloudEvent", + "Sensitive PII": "PII sensibili", + "Serial Mode (Safer, slower)": "Modalità seriale (più sicura, più lenta)", + "Serial:": "Seriale:", + "Server Configuration": "Configurazione del server", + "Server Information": "Informazioni sul server", + "Settings": "Impostazioni", + "Shards": "Shard", + "Share": "Condividi", + "Share with Groups": "Condividi con i gruppi", + "Share with Users": "Condividi con gli utenti", + "Show Filters": "Mostra filtri", + "Show less": "Mostra meno", + "Show only entries with changes": "Mostra solo le voci con modifiche", + "Simple": "Semplice", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Query semplici: ricerche testuali di base con parametri minimi (es. singolo termine di ricerca, nessun filtro avanzato)", + "Size": "Dimensione", + "Slug": "Slug", + "Soft Delete Mode": "Modalità di eliminazione provvisoria", + "Soft Deleted Items": "Elementi eliminati provvisoriamente", + "SolrCloud mode enabled": "Modalità SolrCloud abilitata", + "Source": "Origine", + "Sources": "Origini", + "Sources:": "Origini:", + "Standalone SOLR mode": "Modalità SOLR autonoma", + "Start Conversation": "Avvia conversazione", + "Start Vectorization": "Avvia vettorializzazione", + "Start a conversation": "Avvia una conversazione", + "Starting names cache warmup...": "Avvio del preriscaldamento della cache dei nomi...", + "Starting...": "Avvio in corso...", + "Statistics": "Statistiche", + "Stats calculation not yet implemented": "Calcolo delle statistiche non ancora implementato", + "Status": "Stato", + "Status Code": "Codice di stato", + "String Constraints": "Vincoli sulle stringhe", + "Success": "Successo", + "Success Rate": "Tasso di successo", + "Success Status": "Stato di successo", + "Successful": "Riuscito", + "Successfully joined organisation": "Adesione all'organizzazione riuscita", + "Sync Table": "Sincronizza tabella", + "Synchronization Settings": "Impostazioni di sincronizzazione", + "System Default": "Predefinito di sistema", + "Technical Description": "Descrizione tecnica", + "Technical description for developers and administrators": "Descrizione tecnica per sviluppatori e amministratori", + "Temperature": "Temperatura", + "Templates": "Modelli", + "Test": "Test", + "Test Chat": "Testa chat", + "Test Connection": "Testa connessione", + "Test Embedding": "Testa embedding", + "Test endpoint executed successfully": "Endpoint di test eseguito correttamente", + "Test webhook delivered successfully": "Webhook di test consegnato correttamente", + "Test webhook delivery failed": "Consegna del webhook di test non riuscita", + "Test webhook sent successfully": "Webhook di test inviato correttamente", + "Testing...": "Test in corso...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "I blocchi di testo vengono generati durante l'estrazione del file e archiviati nel database. La vettorializzazione legge questi file pre-suddivisi in blocchi e li converte in embedding.", + "Text extracted successfully": "Testo estratto correttamente", + "Text extraction disabled": "Estrazione del testo disabilitata", + "The URL where webhook events will be sent": "L'URL a cui verranno inviati gli eventi webhook", + "The requested conversation does not exist": "La conversazione richiesta non esiste", + "The saved view and all its search configuration": "La vista salvata e tutta la sua configurazione di ricerca", + "There are no audit trail entries matching your current filters.": "Non ci sono voci del registro di controllo corrispondenti ai tuoi filtri attuali.", + "There are no deleted items matching your current filters.": "Non ci sono elementi eliminati corrispondenti ai tuoi filtri attuali.", + "There are no search trail entries matching your current filters.": "Non ci sono voci del registro di ricerca corrispondenti ai tuoi filtri attuali.", + "There are no webhook log entries matching your filters.": "Non ci sono voci del log dei webhook corrispondenti ai tuoi filtri.", + "This action cannot be undone.": "Questa azione non può essere annullata.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Questa azione non può essere annullata. Assicurati che nessuna collezione stia utilizzando questo ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Questo agente non dispone di viste o strumenti configurabili, ma puoi comunque regolare le impostazioni RAG di seguito.", + "This analysis may take some time": "Questa analisi potrebbe richiedere del tempo", + "This audit trail entry does not contain any change information.": "Questa voce del registro di controllo non contiene alcuna informazione sulle modifiche.", + "This configuration has no data defined.": "Questa configurazione non ha dati definiti.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Questo endpoint è deprecato. Utilizzare invece gli endpoint basati su blocchi.", + "This entity has no relations to objects or files": "Questa entità non ha relazioni con oggetti o file", + "This is a test webhook from OpenRegister": "Questo è un webhook di test da OpenRegister", + "This schema must use Magic Table configuration to sync": "Questo Schema deve utilizzare la configurazione Magic Table per la sincronizzazione", + "This source has no associated registers.": "Questa origine non ha registri associati.", + "This will copy the _default ConfigSet with the new name": "Questa operazione copierà il ConfigSet _default con il nuovo nome", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Questa operazione eliminerà permanentemente TUTTI gli embedding (vettori) dal database. Sarà necessario rivettorializzare tutti gli oggetti e i file. Questa azione non può essere annullata.\\n\\nVuoi davvero continuare?", + "This will permanently delete:": "Questa operazione eliminerà permanentemente:", + "Timeout (seconds)": "Timeout (secondi)", + "Timestamp": "Marca temporale", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Suggerimento: abilita solo le viste che necessitano della ricerca semantica per ridurre al minimo i costi degli embedding. Le semplici tabelle di consultazione raramente necessitano della vettorializzazione.", + "Title": "Titolo", + "To Date": "Data di fine", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Per regolare la dimensione o la strategia dei blocchi, vai a Configurazione file → Limiti di elaborazione.", + "To change the embedding provider or model, go to LLM Configuration.": "Per modificare il provider o il modello di embedding, vai a Configurazione LLM.", + "To date": "Data di fine", + "Toggle search sidebar": "Attiva/disattiva barra laterale di ricerca", + "Tools": "Strumenti", + "Top Deleters": "Principali utenti per eliminazioni", + "Total": "Totale", + "Total Audit Trails": "Totale registri di controllo", + "Total Chunks Available:": "Totale blocchi disponibili:", + "Total Deleted Items": "Totale elementi eliminati", + "Total Members": "Totale membri", + "Total Objects": "Totale oggetti", + "Total Objects in Database:": "Totale oggetti nel database:", + "Total Organisations": "Totale organizzazioni", + "Total Results": "Totale risultati", + "Total Searches": "Totale ricerche", + "Total Size": "Dimensione totale", + "Total Storage": "Archiviazione totale", + "Total:": "Totale:", + "Totals": "Totali", + "Type": "Tipo", + "Type Issue": "Problema di tipo", + "Type Variations:": "Variazioni di tipo:", + "Type must be \"positive\" or \"negative\"": "Il tipo deve essere \"positive\" o \"negative\"", + "Type to search for groups": "Digita per cercare gruppi", + "Type to search for users": "Digita per cercare utenti", + "Type to search groups...": "Digita per cercare gruppi...", + "Type to search users...": "Digiti per cercare utenti...", + "URL": "URL", + "URL where Ollama is running": "URL su cui è in esecuzione Ollama", + "UUID:": "UUID:", + "Unchanged": "Invariato", + "Unique Search Terms": "Termini di ricerca univoci", + "Unique Terms": "Termini univoci", + "Unknown": "Sconosciuto", + "Unknown Register": "Registro sconosciuto", + "Unknown Schema": "Schema sconosciuto", + "Untitled View": "Vista senza titolo", + "Update": "Aggiorna", + "Update Operations": "Operazioni di aggiornamento", + "Update configuration: ...": "Aggiornamento configurazione: ...", + "Update register OAS: ...": "Aggiornamento OAS del registro: ...", + "Update vectors when object data changes (recommended for accurate search)": "Aggiorna i vettori quando i dati dell'oggetto cambiano (consigliato per una ricerca accurata)", + "Updated": "Aggiornato", + "Updated At": "Aggiornato il", + "Updated:": "Aggiornato:", + "Uptime:": "Tempo di attività:", + "Usage Count": "Conteggio utilizzi", + "Use SolrCloud with Zookeeper for distributed search": "Utilizzi SolrCloud con Zookeeper per la ricerca distribuita", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Utilizzi i filtri per restringere le voci del registro di controllo per registro, schema, tipo di azione, utente, intervallo di date o ID oggetto.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Utilizzi i filtri per restringere gli elementi eliminati per registro, schema, data di eliminazione o utente che li ha eliminati.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Utilizzi i filtri per restringere le voci del registro di ricerca per registro, schema, stato di esito, utente, intervallo di date, termini di ricerca o metriche delle prestazioni.", + "Used By Collections": "Utilizzato dalle raccolte", + "User": "Utente", + "User Agent Statistics": "Statistiche user agent", + "User Description": "Descrizione utente", + "User rate limits cleared successfully": "Limiti di frequenza dell'utente azzerati correttamente", + "User removed successfully": "Utente rimosso correttamente", + "User-friendly description shown in forms and help text": "Descrizione di facile comprensione mostrata nei moduli e nei testi di aiuto", + "Username": "Nome utente", + "Username for SOLR authentication (optional)": "Nome utente per l'autenticazione SOLR (facoltativo)", + "Users": "Utenti", + "Using Pre-Generated Chunks": "Utilizzo di blocchi pregenerati", + "Validate": "Convalida", + "Validate Objects": "Convalida oggetti", + "Validation completed successfully": "Convalida completata correttamente", + "Validation failed": "Convalida non riuscita", + "Value": "Valore", + "Vector Dimensions": "Dimensioni del vettore", + "Vector Search Backend": "Backend di ricerca vettoriale", + "Vector field: _embedding_": "Campo vettore: _embedding_", + "Vectorization Triggers": "Trigger di vettorizzazione", + "Vectorize All Files": "Vettorizza tutti i file", + "Vectorize All Objects": "Vettorizza tutti gli oggetti", + "Vectorize all file types": "Vettorizza tutti i tipi di file", + "Vectorize all views": "Vettorizza tutte le viste", + "Vectorize on object creation": "Vettorizza alla creazione dell'oggetto", + "Vectors will be stored in your existing object and file collections": "I vettori saranno memorizzati nelle raccolte esistenti di oggetti e file", + "Version": "Versione", + "Very High": "Molto alto", + "View API Docs": "Visualizza documentazione API", + "View Changes": "Visualizza modifiche", + "View Details": "Visualizza dettagli", + "View Error": "Visualizza errore", + "View File": "Visualizza file", + "View Full Details": "Visualizza dettagli completi", + "View Logs": "Visualizza registri", + "View Name": "Nome vista", + "View Object": "Visualizza oggetto", + "View Parameters": "Visualizza parametri", + "View Selection": "Visualizza selezione", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Visualizzi e analizzi i registri delle ricerche con funzionalità avanzate di filtro e analisi", + "View and analyze system audit trails with advanced filtering capabilities": "Visualizzi e analizzi i registri di controllo del sistema con funzionalità avanzate di filtro", + "View deleted successfully": "Vista eliminata correttamente", + "View entity details and manage relations": "Visualizzi i dettagli dell'entità e gestisca le relazioni", + "View name": "Nome vista", + "View name is required": "Il nome della vista è obbligatorio", + "View saved successfully!": "Vista salvata correttamente!", + "View search analytics and manage search logs": "Visualizzi le analisi di ricerca e gestisca i registri di ricerca", + "View successfully deleted": "Vista eliminata correttamente", + "View successfully updated": "Vista aggiornata correttamente", + "View updated successfully!": "Vista aggiornata correttamente!", + "View webhook delivery logs and filter by webhook": "Visualizzi i registri di consegna dei webhook e filtri per webhook", + "Views": "Viste", + "Visible to users": "Visibile agli utenti", + "Wait for Response": "Attendi risposta", + "Wait for webhook response before continuing (required for request/response flows)": "Attendi la risposta del webhook prima di continuare (richiesto per i flussi richiesta/risposta)", + "Warmup Names Cache": "Precarica cache dei nomi", + "Warning": "Avviso", + "Warning:": "Avviso:", + "Webhook": "Webhook", + "Webhook Log Details": "Dettagli registro webhook", + "Webhook Logs": "Registri webhook", + "Webhook created successfully": "Webhook creato correttamente", + "Webhook deleted": "Webhook eliminato", + "Webhook retry delivered successfully": "Nuovo tentativo del webhook consegnato correttamente", + "Webhook retry delivery failed": "Consegna del nuovo tentativo del webhook non riuscita", + "Webhook updated": "Webhook aggiornato", + "Webhook updated successfully": "Webhook aggiornato correttamente", + "Webhooks": "Webhook", + "Weekly": "Settimanale", + "Wrap webhook payload in CloudEvents format for better interoperability": "Incapsula il payload del webhook nel formato CloudEvents per una migliore interoperabilità", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Sì", + "You": "Lei", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Può creare nuovi ConfigSet basati sul modello _default o caricarne di personalizzati direttamente sul suo server SOLR.", + "You do not have access to this conversation": "Non ha accesso a questa conversazione", + "You do not have permission to delete this conversation": "Non dispone dell'autorizzazione per eliminare questa conversazione", + "You do not have permission to modify this conversation": "Non dispone dell'autorizzazione per modificare questa conversazione", + "You do not have permission to restore this conversation": "Non dispone dell'autorizzazione per ripristinare questa conversazione", + "You must be logged in to favorite views": "Deve aver effettuato l'accesso per aggiungere viste ai preferiti", + "You need an AI agent to start a conversation.": "Ha bisogno di un agente IA per avviare una conversazione.", + "Your Fireworks AI API key. Get one at": "La sua chiave API di Fireworks AI. Ne ottenga una su", + "Your OpenAI API key. Get one at": "La sua chiave API di OpenAI. Ne ottenga una su", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Il suo feedback è stato registrato. Facoltativamente, può fornire qui ulteriori dettagli...", + "Zookeeper Hosts": "Host Zookeeper", + "Zookeeper Port": "Porta Zookeeper", + "Zookeeper Settings (SolrCloud)": "Impostazioni Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Stringa di connessione Zookeeper per SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Numero di porta Zookeeper (facoltativo, predefinito 2181)", + "chunk": "blocco", + "chunks": "blocchi", + "chunks processed": "blocchi elaborati", + "configuration(s)": "configurazione/i", + "e.g., lib/Settings/config.json": "ad es., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "ad es., lib/Settings/register.json", + "failed": "non riuscito", + "file": "file", + "files": "file", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook": "https://example.com/webhook", + "members": "membri", + "menu or contact someone with permission to create agents.": "menu o contatti qualcuno con l'autorizzazione per creare agenti.", + "messages": "messaggi", + "n8n URL and API key are required": "L'URL di n8n e la chiave API sono obbligatori", + "n8n configuration saved successfully": "Configurazione di n8n salvata correttamente", + "n8n connection not configured": "Connessione n8n non configurata", + "n8n connection successful": "Connessione n8n riuscita", + "n8n connection test successful": "Test di connessione n8n riuscito", + "n8n integration disabled": "Integrazione n8n disabilitata", + "n8n integration enabled": "Integrazione n8n abilitata", + "n8n project initialized successfully": "Progetto n8n inizializzato correttamente", + "n8n settings saved successfully": "Impostazioni di n8n salvate correttamente", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "oggetti", + "objects processed": "oggetti elaborati", + "or pick a range": "oppure scelga un intervallo", + "org-...": "org-...", + "provides a good balance between speed and accuracy.": "offre un buon equilibrio tra velocità e precisione.", + "register(s)": "registro/i", + "register(s) selected": "registro/i selezionato/i", + "required": "obbligatorio", + "results": "risultati", + "schema(s)": "schema/i", + "schema(s) selected": "schema/i selezionato/i", + "sk-...": "sk-...", + "this application": "questa applicazione", + "to": "a", + "vectorized": "vettorizzato", + "views selected": "viste selezionate", + "ℹ️ Current Configuration": "ℹ️ Configurazione attuale", + "⚡ Batch Processing": "⚡ Elaborazione in batch", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Contesto completo ✓ Minore probabilità di tralasciare dettagli ✗ Risposte più lente ✗ Può includere informazioni meno pertinenti", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Risposte più rapide ✓ Risposte più mirate ✗ Può tralasciare informazioni pertinenti", + "✨ AI Features": "✨ Funzionalità IA", + "All search trails cleared successfully": "Tutti i registri di ricerca azzerati correttamente", + "Multiple search trail deletion not implemented yet": "L'eliminazione multipla dei registri di ricerca non è ancora implementata", + "No expired search trails found to clear": "Nessun registro di ricerca scaduto da azzerare", + "Missing message": "Messaggio mancante", + "message content is required": "il contenuto del messaggio è obbligatorio", + "Missing conversation or agentUuid": "Conversazione o agentUuid mancante", + "Access denied": "Accesso negato", + "Conversation not found": "Conversazione non trovata", + "AI service not configured": "Servizio IA non configurato", + "Failed to process message": "Elaborazione del messaggio non riuscita", + "Missing conversationId": "conversationId mancante", + "conversationId is required": "conversationId è obbligatorio", + "Failed to fetch conversation history": "Recupero della cronologia della conversazione non riuscito", + "Failed to clear conversation": "Azzeramento della conversazione non riuscito", + "Invalid feedback type": "Tipo di feedback non valido", + "type must be \"positive\" or \"negative\"": "il tipo deve essere \"positive\" o \"negative\"", + "Message not found": "Messaggio non trovato", + "Failed to save feedback": "Salvataggio del feedback non riuscito", + "Failed to get chat statistics": "Recupero delle statistiche della chat non riuscito", + "Not Found": "Non trovato", + "Engine not found": "Motore non trovato", + "Consumer not found": "Consumatore non trovato", + "Backend parameter is required": "Il parametro backend è obbligatorio", + "Failed to get database information: %s": "Recupero delle informazioni del database non riuscito: %s", + "SOLR setup error: %s": "Errore di configurazione SOLR: %s", + "Reindex failed: %s": "Reindicizzazione non riuscita: %s", + "Anonymized": "Anonimizzato", + "Entities detected": "Entità rilevate", + "Extract Now": "Estrai ora", + "Extracted at": "Estratto il", + "Extraction": "Estrazione", + "Failed to load extraction data": "Caricamento dei dati di estrazione non riuscito", + "Failed to load register data": "Caricamento dei dati del registro non riuscito", + "Failed to retrieve extraction status.": "Recupero dello stato di estrazione non riuscito.", + "Failed to retrieve objects for file.": "Recupero degli oggetti per il file non riuscito.", + "No extraction data available for this file": "Nessun dato di estrazione disponibile per questo file", + "No register objects reference this file": "Nessun oggetto del registro fa riferimento a questo file", + "Not extracted": "Non estratto", + "Register Objects": "Oggetti del registro", + "Risk level": "Livello di rischio", + "Text chunks": "Blocchi di testo", + "Unknown error": "Errore sconosciuto", + "Very high": "Molto alto", + "{title} in {register} / {schema}": "{title} in {register} / {schema}", + "Push Notifications": "Notifiche push", + "Real-time push notification status via notify_push": "Stato delle notifiche push in tempo reale tramite notify_push", + "Realtime push not available — the notify_push app is not installed": "Push in tempo reale non disponibile — l'app notify_push non è installata", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installi l'app notify_push dall'App Store di Nextcloud per abilitare gli aggiornamenti in tempo reale.", + "Open Nextcloud App Store": "Apri l'App Store di Nextcloud", + "notify_push is installed but not yet active": "notify_push è installato ma non ancora attivo", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push è installato ma OpenRegister non ha ancora confermato un push riuscito. Attivi un salvataggio di oggetto per attivarlo o verifichi la configurazione di notify_push.", + "notify_push configuration guide": "guida alla configurazione di notify_push", + "Realtime push active": "Push in tempo reale attivo", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Le notifiche push in tempo reale sono attive. I client connessi ricevono aggiornamenti istantanei quando gli oggetti vengono creati, aggiornati o eliminati.", + "(Dolphin required)": "(Dolphin richiesto)", + "({count} selected)": "({count} selezionati)", + ", {failed} failed": ", {failed} non riusciti", + "0 = unlimited": "0 = illimitato", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVVISO DI ELIMINAZIONE PERMANENTE ⚠️\n\nSta per eliminare PERMANENTEMENTE TUTTI gli oggetti per lo schema \"{schema}\":\n\n• Oggetti attivi: {active}\n• Oggetti eliminati provvisoriamente: {deleted}\n• Totale: {total}\n\nQuesti oggetti saranno rimossi completamente dal database e NON potranno essere recuperati.\n\nÈ assolutamente sicuro?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVVISO DI ELIMINAZIONE PERMANENTE ⚠️\n\nSta per eliminare PERMANENTEMENTE {count} oggetti eliminati provvisoriamente per lo schema \"{schema}\".\n\nQuesti oggetti saranno rimossi completamente dal database e NON potranno essere recuperati.\n\nÈ assolutamente sicuro?", + "A deactivation request is pending.": "È in sospeso una richiesta di disattivazione.", + "Access organisation dashboard and analytics": "Accedi alla dashboard e alle analisi dell'organizzazione", + "Account": "Account", + "Active organisation changed successfully": "Organizzazione attiva modificata correttamente", + "Active Organisation:": "Organizzazione attiva:", + "Activity": "Attività", + "Add": "Aggiungi", + "Add a file or files": "Aggiungi uno o più file", + "Add Attachment": "Aggiungi allegato", + "Add endpoint": "Aggiungi endpoint", + "Add oneOf entry": "Aggiungi voce oneOf", + "Add Property to '{title}'": "Aggiungi proprietà a '{title}'", + "Add range": "Aggiungi intervallo", + "Add Register": "Aggiungi registro", + "Add User": "Aggiungi utente", + "Add User to Organisation": "Aggiungi utente all'organizzazione", + "Addresses": "Indirizzi", + "Agent successfully updated": "Agente aggiornato correttamente", + "Agent Type": "Tipo di agente", + "Aggregated across schemas": "Aggregato tra gli schemi", + "All file types": "Tutti i tipi di file", + "All-day events": "Eventi per l'intera giornata", + "Allowed MIME Types": "Tipi MIME consentiti", + "Already linked": "Già collegato", + "An error occurred while deleting the object": "Si è verificato un errore durante l'eliminazione dell'oggetto", + "An error occurred while restoring the objects": "Si è verificato un errore durante il ripristino degli oggetti", + "An error occurred while saving the organisation": "Si è verificato un errore durante il salvataggio dell'organizzazione", + "and add the files there.": "e aggiunga i file lì.", + "Anonymous Group": "Gruppo anonimo", + "API Request Quota (requests/day)": "Quota richieste API (richieste/giorno)", + "API Token Configuration": "Configurazione token API", + "API Tokens": "Token API", + "App ID": "ID app", + "Application identifier for this configuration (optional)": "Identificatore dell'applicazione per questa configurazione (facoltativo)", + "Application settings": "Impostazioni dell'applicazione", + "Application successfully created": "Applicazione creata correttamente", + "Application successfully updated": "Applicazione aggiornata correttamente", + "Apply": "Applica", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Applica un ruolo a tutti gli schemi di questo registro che non hanno sostituzioni di autorizzazione esplicite.", + "Approval Progress": "Avanzamento approvazione", + "Approve": "Approva", + "Approved": "Approvato", + "approved": "approvato", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "È sicuro di voler azzerare tutti i dati dalla raccolta \"{name}\"?\n\nQuesta operazione:\n• Eliminerà tutti i documenti indicizzati\n• Manterrà intatta la struttura della raccolta\n• Non può essere annullata", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "È sicuro di voler ELIMINARE la raccolta \"{name}\"?\n\nQuesta operazione:\n• Eliminerà permanentemente la raccolta e tutti i suoi dati\n• Rimuoverà tutti i documenti indicizzati\n• Non può essere annullata", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "È sicuro di voler eliminare l'applicazione \"{name}\"? Questa azione non può essere annullata.", + "Are you sure you want to delete this configset?": "È sicuro di voler eliminare questo configset?", + "Are you sure you want to leave '{name}'?": "È sicuro di voler abbandonare '{name}'?", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "È sicuro di voler reindicizzare la raccolta \"{name}\"?\n\nQuesta operazione:\n• Ricostruirà l'indice con tutti gli oggetti\n• Richiederà diversi minuti per essere completata\n• Potrebbe influire sulle prestazioni di ricerca durante la reindicizzazione", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "È sicuro di voler rimuovere lo schema \"{schema}\" dal registro \"{register}\"? Questa azione non può essere annullata.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "È sicuro di voler eliminare provvisoriamente {count} oggetti attivi per lo schema \"{schema}\"?\n\nSaranno contrassegnati come eliminati ma potranno essere rimossi permanentemente in seguito.", + "Array Object Configuration:": "Configurazione oggetto array:", + "Assigned collaborative tags": "Etichette collaborative assegnate", + "Assignment notifications": "Notifiche di assegnazione", + "Auto (e.g. Y for year, F Y for month)": "Auto (ad es. Y per anno, F Y per mese)", + "Auto share": "Condivisione automatica", + "Automatically connect to the federation network.": "Connettiti automaticamente alla rete di federazione.", + "Automatically update catalogues.": "Aggiorna automaticamente i cataloghi.", + "Available placeholders:": "Segnaposto disponibili:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Le modifiche all'avatar non sono supportate dal suo provider di autenticazione.", + "Avatar removed": "Avatar rimosso", + "Avatar updated successfully": "Avatar aggiornato correttamente", + "Bandwidth Quota (MB/month)": "Quota di banda (MB/mese)", + "Behavior": "Comportamento", + "Bulk Role Assignment: {title}": "Assegnazione ruolo in blocco: {title}", + "Cache grew from {old} to {new} entries.": "La cache è cresciuta da {old} a {new} voci.", + "Cache shrunk from {old} to {new} entries.": "La cache si è ridotta da {old} a {new} voci.", + "Cache stayed the same at {size} entries.": "La cache è rimasta invariata a {size} voci.", + "Calendar": "Calendario", + "Calendar name": "Nome calendario", + "Calendar Provider Configuration": "Configurazione provider calendario", + "Cancel deactivation request": "Annulla richiesta di disattivazione", + "Cannot edit: This register is managed by external configuration {title}": "Impossibile modificare: questo registro è gestito dalla configurazione esterna {title}", + "Cannot remove schema with existing objects ({count} objects)": "Impossibile rimuovere uno schema con oggetti esistenti ({count} oggetti)", + "Cannot remove schema {schema} because it contains {count} objects": "Impossibile rimuovere lo schema {schema} perché contiene {count} oggetti", + "Cards": "Schede", + "Cascade delete": "Eliminazione a cascata", + "Change password": "Cambia password", + "Changes for Audit Trail #{id}": "Modifiche per il registro di controllo n. {id}", + "Chat test failed: {error}": "Test della chat non riuscito: {error}", + "Cleanup completed successfully. Deleted {count} entries.": "Pulizia completata correttamente. Eliminate {count} voci.", + "Cleanup failed: {error}": "Pulizia non riuscita: {error}", + "Clear filters": "Azzera filtri", + "Cluster name": "Nome cluster", + "Collapse details": "Comprimi dettagli", + "Color": "Colore", + "Comment...": "Commento...", + "completed": "completato", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Query complesse: ricerche avanzate con più filtri, operatori e combinazioni di parametri complesse", + "Confidence: {confidence}%": "Affidabilità: {confidence}%", + "Configuration is up to date": "La configurazione è aggiornata", + "Configuration saved successfully.": "Configurazione salvata correttamente.", + "Configuration type (default, application, etc.)": "Tipo di configurazione (predefinito, applicazione, ecc.)", + "Configure Apache SOLR search engine for advanced search capabilities": "Configuri il motore di ricerca Apache SOLR per funzionalità di ricerca avanzate", + "Configure API tokens for external service integrations": "Configuri i token API per le integrazioni di servizi esterni", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configuri le impostazioni di connessione di base per il suo server SOLR, comprese le opzioni di autenticazione e di rete. Utilizzi le finestre di dialogo separate di gestione di configset e raccolte per gestire i core e le raccolte.", + "Configure default organisation and organisation-related settings": "Configuri l'organizzazione predefinita e le impostazioni relative all'organizzazione", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configuri i provider di modelli linguistici di grandi dimensioni (LLM) per funzionalità basate sull'IA, tra cui ricerca semantica, embedding e chat.", + "Configure multi-organization support and tenant isolation": "Configuri il supporto multi-organizzazione e l'isolamento dei tenant", + "Configure SOLR Facets": "Configura faccette SOLR", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configuri questo schema per mostrare gli oggetti come eventi nell'app Calendario di Nextcloud.", + "Confirm Account Deactivation": "Conferma disattivazione account", + "Confirm deactivation": "Conferma disattivazione", + "Confirm Object Deletion": "Conferma eliminazione oggetto", + "Confirm Object Restoration": "Conferma ripristino oggetto", + "Confirm Permanent Object Deletion": "Conferma eliminazione permanente oggetto", + "Contracts": "Contratti", + "Conversation started with {agent}": "Conversazione avviata con {agent}", + "Conversations": "Conversazioni", + "Converting {schema} to blob storage...": "Conversione di {schema} in archiviazione blob...", + "Converting {schema} to magic table...": "Conversione di {schema} in tabella magica...", + "Copy of {name}": "Copia di {name}", + "Copy to clipboard": "Copia negli appunti", + "create": "crea", + "Create a new configset based on the _default template": "Crea un nuovo configset basato sul modello _default", + "Create a new SOLR collection from an existing configset": "Crea una nuova raccolta SOLR da un configset esistente", + "Create another": "Crea un altro", + "Create API Token": "Crea token API", + "Create copies of": "Crea copie di", + "Create new token": "Crea nuovo token", + "Create Organisation": "Crea organizzazione", + "Current": "Attuale", + "Current password": "Password attuale", + "Custom display title for this facet": "Titolo di visualizzazione personalizzato per questa faccetta", + "Custom HTTP headers (one per line, format: header-name: value)": "Intestazioni HTTP personalizzate (una per riga, formato: nome-intestazione: valore)", + "Custom pattern:": "Modello personalizzato:", + "Custom Ranges:": "Intervalli personalizzati:", + "Data": "Dati", + "Data Sources": "Origini dati", + "Data Views": "Viste dati", + "Date Faceting:": "Faccette per data:", + "Dates": "Date", + "Deactivation request cancelled": "Richiesta di disattivazione annullata", + "Deactivation request submitted": "Richiesta di disattivazione inviata", + "Default Object Tenant": "Tenant oggetto predefinito", + "Default Owner": "Proprietario predefinito", + "Default User (for cron/background jobs)": "Utente predefinito (per cron/processi in background)", + "Default User Tenant": "Tenant utente predefinito", + "Default value": "Valore predefinito", + "Default value (Email)": "Valore predefinito (Email)", + "Default value (Password)": "Valore predefinito (Password)", + "Default value (Phone number)": "Valore predefinito (Numero di telefono)", + "Default value (Regex)": "Valore predefinito (Regex)", + "Defaults to current user. Select a different user if needed.": "Per impostazione predefinita è l'utente attuale. Selezioni un utente diverso se necessario.", + "delete": "elimina", + "Delete ({count})": "Elimina ({count})", + "DELETE COLLECTION": "ELIMINA RACCOLTA", + "Delete SOLR Collection": "Elimina raccolta SOLR", + "deleted objects section": "sezione oggetti eliminati", + "Deleting SOLR Collection...": "Eliminazione della raccolta SOLR...", + "Depublish": "Annulla pubblicazione", + "Description shown as tooltip": "Descrizione mostrata come tooltip", + "Description Template": "Modello di descrizione", + "Detected: {date}": "Rilevato: {date}", + "Direct permission": "Autorizzazione diretta", + "Disable this to make the agent": "Disabiliti questa opzione per rendere l'agente", + "Display Format": "Formato di visualizzazione", + "Display Name": "Nome visualizzato", + "Dolphin OCR enabled": "OCR Dolphin abilitato", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Scarichi una copia di tutti i suoi dati personali archiviati in OpenRegister (RGPD Articolo 20).", + "Download API Specification": "Scarica specifica API", + "Drag a file or files here": "Trascini uno o più file qui", + "Dry run completed: {count} fields would be created": "Esecuzione di prova completata: verrebbero creati {count} campi", + "Dry run completed: {count} fields would be fixed": "Esecuzione di prova completata: verrebbero corretti {count} campi", + "Duration in seconds (optional)": "Durata in secondi (facoltativo)", + "e.g., lib/settings/config.json": "ad es., lib/settings/config.json", + "e.g., lib/settings/register.json": "ad es., lib/settings/register.json", + "Edit Agent": "Modifica agente", + "Edit Application": "Modifica applicazione", + "Edit Configuration": "Modifica configurazione", + "Edit Endpoint": "Modifica endpoint", + "Edit labels": "Modifica etichette", + "Edit Organisation": "Modifica organizzazione", + "Edit Property '{key}' of '{title}'": "Modifica proprietà '{key}' di '{title}'", + "Edit Source": "Modifica origine", + "Edit tags for {name}": "Modifica etichette per {name}", + "Email Addresses": "Indirizzi email", + "Email digest frequency": "Frequenza riepilogo email", + "Embedding test failed: {error}": "Test di embedding non riuscito: {error}", + "Enable calendar provider": "Abilita provider calendario", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Abiliti o disabiliti le funzionalità LLM. Configuri provider e modelli utilizzando il pulsante di configurazione LLM in alto.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Abiliti o disabiliti l'integrazione della ricerca SOLR. Configuri le impostazioni di connessione utilizzando il pulsante delle impostazioni di connessione in alto.", + "Enable RAG": "Abilita RAG", + "Enable tools that allow the agent to interact with data through function calling.": "Abiliti gli strumenti che consentono all'agente di interagire con i dati tramite chiamate di funzione.", + "Enable write-back to target objects": "Abilita la riscrittura sugli oggetti di destinazione", + "End Date Field": "Campo data di fine", + "Endpoint Path*": "Percorso endpoint*", + "Enter agent description (optional)": "Inserisca la descrizione dell'agente (facoltativo)", + "Enter agent name": "Inserisca il nome dell'agente", + "Enter application description (optional)": "Inserisca la descrizione dell'applicazione (facoltativo)", + "Enter application name": "Inserisca il nome dell'applicazione", + "Enter configuration description (optional)": "Inserisca la descrizione della configurazione (facoltativo)", + "Enter configuration title": "Inserisca il titolo della configurazione", + "Enter Nextcloud usernames to grant access to this private agent": "Inserisca i nomi utente di Nextcloud per concedere l'accesso a questo agente privato", + "Enter organisation description (optional)": "Inserisca la descrizione dell'organizzazione (facoltativo)", + "Enter organisation name": "Inserisca il nome dell'organizzazione", + "Enter search terms or leave empty to browse all": "Inserisca i termini di ricerca o lasci vuoto per sfogliare tutto", + "Enter system prompt for the agent": "Inserisca il prompt di sistema per l'agente", + "Enter username and press Enter": "Inserisca il nome utente e prema Invio", + "Enter your API key": "Inserisca la sua chiave API", + "Enter your object here...": "Inserisca qui il suo oggetto...", + "Enter your schema here...": "Inserisca qui il suo schema...", + "Entity ID: {id}": "ID entità: {id}", + "Entity Recognition": "Riconoscimento entità", + "Entity Recognition Method": "Metodo di riconoscimento entità", + "Error deleting audit trails: {error}": "Errore durante l'eliminazione dei registri di controllo: {error}", + "Error deleting search trails: {error}": "Errore durante l'eliminazione dei registri di ricerca: {error}", + "Example": "Esempio", + "Example value for this property": "Valore di esempio per questa proprietà", + "Exclusive maximum": "Massimo esclusivo", + "Exclusive minimum": "Minimo esclusivo", + "execute": "esegui", + "Expiration": "Scadenza", + "Expires": "Scade", + "Expires in (e.g., 90d)": "Scade tra (ad es., 90d)", + "Export \"{schema}\" objects from \"{register}\"": "Esporta gli oggetti \"{schema}\" da \"{register}\"", + "Export Configuration": "Esporta configurazione", + "Export downloaded successfully": "Esportazione scaricata correttamente", + "Export failed: {error}": "Esportazione non riuscita: {error}", + "Export Format:": "Formato di esportazione:", + "Export is rate limited. Please try again later.": "L'esportazione è soggetta a limite di frequenza. Riprovi più tardi.", + "Export my data": "Esporta i miei dati", + "Export Objects": "Esporta oggetti", + "Exporting...": "Esportazione...", + "Extra Query Parameters": "Parametri di query aggiuntivi", + "Extract now": "Estrai ora", + "Extraction error for {file}: {error}": "Errore di estrazione per {file}: {error}", + "Extraction Mode": "Modalità di estrazione", + "Extraction Scope": "Ambito di estrazione", + "Facet Description": "Descrizione faccetta", + "Facet Order": "Ordine faccette", + "Facet Title": "Titolo faccetta", + "Facet Type": "Tipo di faccetta", + "Facetable": "Sfaccettabile", + "Faceting Configuration:": "Configurazione faccette:", + "Failed to add user to organisation": "Aggiunta dell'utente all'organizzazione non riuscita", + "Failed to cancel deactivation": "Annullamento della disattivazione non riuscito", + "Failed to change active organisation: {error}": "Modifica dell'organizzazione attiva non riuscita: {error}", + "Failed to change password": "Modifica della password non riuscita", + "Failed to check version: {error}": "Verifica della versione non riuscita: {error}", + "Failed to clear embeddings: {error}": "Azzeramento degli embedding non riuscito: {error}", + "Failed to copy to clipboard": "Copia negli appunti non riuscita", + "Failed to copy token": "Copia del token non riuscita", + "Failed to create missing SOLR fields: {error}": "Creazione dei campi SOLR mancanti non riuscita: {error}", + "Failed to create token": "Creazione del token non riuscita", + "Failed to delete objects for {schema}: {error}": "Eliminazione degli oggetti per {schema} non riuscita: {error}", + "Failed to delete SOLR collection: {error}": "Eliminazione della raccolta SOLR non riuscita: {error}", + "Failed to depublish register: {error}": "Annullamento della pubblicazione del registro non riuscito: {error}", + "Failed to depublish schema: {error}": "Annullamento della pubblicazione dello schema non riuscito: {error}", + "Failed to depublish: {error}": "Annullamento della pubblicazione non riuscito: {error}", + "Failed to export data": "Esportazione dei dati non riuscita", + "Failed to export objects": "Esportazione degli oggetti non riuscita", + "Failed to fix mismatched SOLR fields: {error}": "Correzione dei campi SOLR non corrispondenti non riuscita: {error}", + "Failed to get response: {error}": "Recupero della risposta non riuscito: {error}", + "Failed to leave organisation: {error}": "Abbandono dell'organizzazione non riuscito: {error}", + "Failed to link object": "Collegamento dell'oggetto non riuscito", + "Failed to load view: {error}": "Caricamento della vista non riuscito: {error}", + "Failed to publish register: {error}": "Pubblicazione del registro non riuscita: {error}", + "Failed to publish schema: {error}": "Pubblicazione dello schema non riuscita: {error}", + "Failed to publish: {error}": "Pubblicazione non riuscita: {error}", + "Failed to remove avatar": "Rimozione dell'avatar non riuscita", + "Failed to remove link": "Rimozione del collegamento non riuscita", + "Failed to remove member: {error}": "Rimozione del membro non riuscita: {error}", + "Failed to remove schema {schema}: {error}": "Rimozione dello schema {schema} non riuscita: {error}", + "Failed to remove user from organisation": "Rimozione dell'utente dall'organizzazione non riuscita", + "Failed to request deactivation": "Richiesta di disattivazione non riuscita", + "Failed to revoke token": "Revoca del token non riuscita", + "Failed to save configuration.": "Salvataggio della configurazione non riuscito.", + "Failed to save configuration: {error}": "Salvataggio della configurazione non riuscito: {error}", + "Failed to save facet configuration": "Salvataggio della configurazione delle faccette non riuscito", + "Failed to save preferences": "Salvataggio delle preferenze non riuscito", + "Failed to save view: {error}": "Salvataggio della vista non riuscito: {error}", + "Failed to set active organisation: {error}": "Impostazione dell'organizzazione attiva non riuscita: {error}", + "Failed to start file warmup: {error}": "Avvio del precaricamento dei file non riuscito: {error}", + "Failed to start vectorization: {error}": "Avvio della vettorizzazione non riuscito: {error}", + "Failed to switch organisation: {error}": "Cambio dell'organizzazione non riuscito: {error}", + "Failed to sync magic table for {schema}: {error}": "Sincronizzazione della tabella magica per {schema} non riuscita: {error}", + "Failed to update schema configuration for {schema}: {error}": "Aggiornamento della configurazione dello schema per {schema} non riuscito: {error}", + "Failed to update view: {error}": "Aggiornamento della vista non riuscito: {error}", + "Failed to upload avatar": "Caricamento dell'avatar non riuscito", + "Failed to validate {schema}: {error}": "Convalida di {schema} non riuscita: {error}", + "Failed to vectorize files: {error}": "Vettorizzazione dei file non riuscita: {error}", + "Failed to warmup names cache: {error}": "Precaricamento della cache dei nomi non riuscito: {error}", + "Federation": "Federazione", + "Fields to Display": "Campi da visualizzare", + "File #{id}": "File n. {id}", + "File Handling": "Gestione file", + "File Location": "Posizione file", + "File name": "Nome file", + "File Types to Process": "Tipi di file da elaborare", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vettorizzazione dei file completata! {vectorized} blocchi vettorizzati da {files} file. {failed} non riusciti.", + "Files added successfully": "File aggiunti correttamente", + "Files → filecollection, objects → objectcollection": "File → filecollection, oggetti → objectcollection", + "file{plural}": "file{plural}", + "Filter by type": "Filtra per tipo", + "Filter fields...": "Filtra campi...", + "folder": "cartella", + "From (e.g. -7 days or 2025-01-01)": "Da (ad es. -7 days o 2025-01-01)", + "Function Tools": "Strumenti di funzione", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Ottenga informazioni in tempo reale sullo stato di salute dei dati della sua organizzazione, sulle tendenze di crescita e sulle metriche di qualità.", + "GitHub Personal Access Token": "Token di accesso personale GitHub", + "GitHub token is valid! Username: {username}": "Il token GitHub è valido! Nome utente: {username}", + "GitHub token test failed: {message}": "Test del token GitHub non riuscito: {message}", + "GitLab Personal Access Token": "Token di accesso personale GitLab", + "GitLab token is valid! Username: {username}": "Il token GitLab è valido! Nome utente: {username}", + "GitLab token test failed: {message}": "Test del token GitLab non riuscito: {message}", + "Go to linked Register": "Vai al registro collegato", + "Go to linked Schema": "Vai allo schema collegato", + "Go to organisation": "Vai all'organizzazione", + "Grant additional permissions beyond standard CRUD operations": "Concedi autorizzazioni aggiuntive oltre alle operazioni CRUD standard", + "Group Quota": "Quota gruppo", + "Groups": "Gruppi", + "Here you can configure the details for various connections.": "Qui può configurare i dettagli per le varie connessioni.", + "Here you can configure the details for your organisation.": "Qui può configurare i dettagli della sua organizzazione.", + "Hide": "Nascondi", + "Hide in collection view": "Nascondi nella vista raccolta", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "IBANs": "IBAN", + "ID of input mapping (optional)": "ID della mappatura di input (facoltativo)", + "ID of output mapping (optional)": "ID della mappatura di output (facoltativo)", + "ID of the target resource": "ID della risorsa di destinazione", + "ID: {id}": "ID: {id}", + "Import Configuration": "Importa configurazione", + "Import Data into Register": "Importa dati nel registro", + "in {register}": "in {register}", + "in_progress": "in corso", + "Inactive organisations cannot be used": "Le organizzazioni inattive non possono essere utilizzate", + "Include ids and names of related objects for better contextual search": "Includi gli ID e i nomi degli oggetti correlati per una migliore ricerca contestuale", + "Index": "Indice", + "Information about the current OpenRegister installation": "Informazioni sull'installazione attuale di OpenRegister", + "inherited": "ereditato", + "Inherited from register": "Ereditato dal registro", + "Inherits permissions from register": "Eredita le autorizzazioni dal registro", + "Input Mapping": "Mappatura di input", + "Inspect SOLR Index": "Ispeziona indice SOLR", + "Internet location (URL) of this installation": "Posizione Internet (URL) di questa installazione", + "Interval": "Intervallo", + "Invite Users": "Invita utenti", + "Invited Users": "Utenti invitati", + "Key": "Chiave", + "Label": "Etichetta", + "Labels": "Etichette", + "Leave empty to allow all users access": "Lasci vuoto per consentire l'accesso a tutti gli utenti", + "Leave off for auto-detection from property format.": "Lasci disattivato per il rilevamento automatico dal formato della proprietà.", + "Left organisation successfully": "Organizzazione abbandonata correttamente", + "Link": "Collega", + "Link an object to see it here.": "Colleghi un oggetto per visualizzarlo qui.", + "Link another object": "Collega un altro oggetto", + "Link removed": "Collegamento rimosso", + "Link to Object": "Collega all'oggetto", + "Link to {name}": "Collega a {name}", + "Linked by {user}": "Collegato da {user}", + "Linked Objects": "Oggetti collegati", + "Linked to {name}": "Collegato a {name}", + "list": "elenca", + "Load more": "Carica altro", + "Loading activity...": "Caricamento attività...", + "Loading API tokens...": "Caricamento token API...", + "Loading available tools...": "Caricamento strumenti disponibili...", + "Loading cache statistics...": "Caricamento statistiche cache...", + "Loading endpoints": "Caricamento endpoint", + "Loading entities...": "Caricamento entità...", + "Loading linked objects...": "Caricamento oggetti collegati...", + "Loading LLM configuration...": "Caricamento configurazione LLM...", + "Loading multitenancy settings...": "Caricamento impostazioni multitenancy...", + "Loading n8n configuration...": "Caricamento configurazione n8n...", + "Loading permission matrix...": "Caricamento matrice delle autorizzazioni...", + "Loading preferences...": "Caricamento preferenze...", + "Loading RBAC settings...": "Caricamento impostazioni RBAC...", + "Loading register data...": "Caricamento dati del registro...", + "Loading retention settings...": "Caricamento impostazioni di conservazione...", + "Loading search configuration...": "Caricamento configurazione di ricerca...", + "Loading text extraction settings...": "Caricamento impostazioni di estrazione del testo...", + "Loading tokens...": "Caricamento token...", + "Loading user groups...": "Caricamento gruppi di utenti...", + "Location": "Posizione", + "Location Field": "Campo posizione", + "Locations": "Posizioni", + "log{plural}": "log{plural}", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "I numeri più bassi appaiono per primi nella barra laterale dei filtri. Lasci vuoto per l'ordinamento automatico.", + "Magic table sync completed for {schema}": "Sincronizzazione della tabella magica completata per {schema}", + "Mail Integration": "Integrazione email", + "Make this installation discoverable within the federation network.": "Rendi questa installazione individuabile all'interno della rete di federazione.", + "manage": "gestisci", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Gestisca le raccolte SOLR (archivi dati) e le assegni a oggetti e file.", + "Manage SOLR configsets (configuration templates) for your collections.": "Gestisca i configset SOLR (modelli di configurazione) per le sue raccolte.", + "Manage your account settings, security, and personal data.": "Gestisca le impostazioni del suo account, la sicurezza e i dati personali.", + "Manage your chat conversations": "Gestisca le sue conversazioni di chat", + "Manage your organisations and switch between them": "Gestisca le sue organizzazioni e passi dall'una all'altra", + "Mappings": "Mappature", + "Max Tokens": "Token massimi", + "Maximum File Size (MB)": "Dimensione massima file (MB)", + "Maximum length": "Lunghezza massima", + "Maximum number of items": "Numero massimo di elementi", + "Maximum value": "Valore massimo", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Query medie: ricerche con qualche filtro o più parametri (ad es. intervalli di date, registri/schemi specifici)", + "Member removed successfully": "Membro rimosso correttamente", + "Members": "Membri", + "Members ({count})": "Membri ({count})", + "Metadata": "Metadati", + "Method*": "Metodo*", + "Method: {method}": "Metodo: {method}", + "Minimum length": "Lunghezza minima", + "Minimum number of items": "Numero minimo di elementi", + "Minimum value": "Valore minimo", + "Multiple of": "Multiplo di", + "Multitenancy": "Multitenancy", + "My Account": "Il mio account", + "myapp": "myapp", + "n8n connection test failed: {message}": "Test di connessione n8n non riuscito: {message}", + "n8n initialization failed: {message}": "Inizializzazione di n8n non riuscita: {message}", + "Name *": "Nome *", + "Name*": "Nome*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Cache dei nomi precaricata correttamente: {count} nomi caricati in {time}. {cache}", + "Naming pattern for copies:": "Modello di denominazione per le copie:", + "New password": "Nuova password", + "New User Group": "Nuovo gruppo di utenti", + "Nextcloud Groups": "Gruppi Nextcloud", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Nessun oggetto attivo da eliminare provvisoriamente per lo schema {schema}. Utilizzi \"Elimina permanentemente\" per rimuovere gli oggetti eliminati provvisoriamente.", + "No activity found.": "Nessuna attività trovata.", + "No API tokens.": "Nessun token API.", + "No approval steps for this object.": "Nessuna fase di approvazione per questo oggetto.", + "No configuration selected": "Nessuna configurazione selezionata", + "No endpoints defined": "Nessun endpoint definito", + "No entities detected for this email.": "Nessuna entità rilevata per questa email.", + "No files selected": "Nessun file selezionato", + "No groups found. Try a different search.": "Nessun gruppo trovato. Provi una ricerca diversa.", + "No label": "Nessuna etichetta", + "No labels": "Nessuna etichetta", + "No linked objects": "Nessun oggetto collegato", + "No objects are currently selected for deletion.": "Nessun oggetto è attualmente selezionato per l'eliminazione.", + "No objects are currently selected for permanent deletion.": "Nessun oggetto è attualmente selezionato per l'eliminazione permanente.", + "No objects are currently selected for restoration.": "Nessun oggetto è attualmente selezionato per il ripristino.", + "No objects linked to this email": "Nessun oggetto collegato a questa email", + "No objects selected": "Nessun oggetto selezionato", + "No objects selected for restoration": "Nessun oggetto selezionato per il ripristino", + "No objects to delete for schema {schema}": "Nessun oggetto da eliminare per lo schema {schema}", + "No organisations found": "Nessuna organizzazione trovata", + "No registers found. Create a register to configure permissions.": "Nessun registro trovato. Crei un registro per configurare le autorizzazioni.", + "No related cases found for this sender": "Nessun caso correlato trovato per questo mittente", + "No schemas configured for mail linking.": "Nessuno schema configurato per il collegamento email.", + "No synchronizations found": "Nessuna sincronizzazione trovata", + "No tools available. Tools can be registered by installed apps.": "Nessuno strumento disponibile. Gli strumenti possono essere registrati dalle app installate.", + "No users in this organisation.": "Nessun utente in questa organizzazione.", + "Note:": "Nota:", + "Notifications": "Notifiche", + "Number of Sources": "Numero di origini", + "Object #{id}": "Oggetto n. {id}", + "Object changes in owned objects": "Modifiche di oggetti negli oggetti posseduti", + "Object Configuration:": "Configurazione oggetto:", + "Object Extraction Mode": "Modalità di estrazione oggetti", + "Object successfully modified": "Oggetto modificato correttamente", + "Object Text Extraction": "Estrazione del testo dell'oggetto", + "Objects deletion completed for {schema}": "Eliminazione degli oggetti completata per {schema}", + "Objects will be soft deleted and moved to the": "Gli oggetti saranno eliminati provvisoriamente e spostati nella", + "objectType: object\naction: created": "objectType: object\naction: created", + "object{plural}": "object{plural}", + "Only members of selected groups can access this organisation": "Solo i membri dei gruppi selezionati possono accedere a questa organizzazione", + "Open folder": "Apri cartella", + "Open in OpenRegister": "Apri in OpenRegister", + "Open OpenRegister sidebar": "Apri la barra laterale di OpenRegister", + "OpenRegister": "OpenRegister", + "Optional description for this facet": "Descrizione facoltativa per questa faccetta", + "Optional end date property": "Proprietà data di fine facoltativa", + "Optional event description template": "Modello di descrizione dell'evento facoltativo", + "Optional location property": "Proprietà posizione facoltativa", + "Optional URL-friendly identifier": "Identificatore facoltativo compatibile con gli URL", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Facoltativo: aggiunga parametri di query per filtrare lo schema referenziato (ad es. status=active&type=public)", + "Or": "Oppure", + "Organisation Configuration": "Configurazione organizzazione", + "Organisation membership changes": "Modifiche all'appartenenza all'organizzazione", + "Organisation name is required": "Il nome dell'organizzazione è obbligatorio", + "Organisation successfully updated": "Organizzazione aggiornata correttamente", + "Organizations": "Organizzazioni", + "Other": "Altro", + "Output Mapping": "Mappatura di output", + "Overview": "Panoramica", + "Page {current} of {total}": "Pagina {current} di {total}", + "Password changes are not supported by your authentication provider.": "Le modifiche alla password non sono supportate dal suo provider di autenticazione.", + "Password updated successfully": "Password aggiornata correttamente", + "pending": "in sospeso", + "Permanently Delete ({count})": "Elimina permanentemente ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Elimina permanentemente tutti i {count} oggetti eliminati provvisoriamente. Questa operazione non può essere annullata!", + "Permission Matrix": "Matrice delle autorizzazioni", + "Personal Data Export": "Esportazione dei dati personali", + "Persons": "Persone", + "Phone Numbers": "Numeri di telefono", + "Please select a user": "Selezioni un utente", + "Please select an organisation": "Selezioni un'organizzazione", + "Please select at least one item to import": "Selezioni almeno un elemento da importare", + "Please select registers or schemas first": "Selezioni prima i registri o gli schemi", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Attenda mentre eliminiamo permanentemente la raccolta SOLR. Questa operazione potrebbe richiedere alcuni istanti.", + "Preferences saved": "Preferenze salvate", + "Press enter to send, shift+enter for new line": "Prema Invio per inviare, Maiusc+Invio per andare a capo", + "Prettify": "Formatta", + "Preview:": "Anteprima:", + "Private Agent (Default)": "Agente privato (predefinito)", + "Private agents": "Agenti privati", + "Process Name (optional)": "Nome processo (facoltativo)", + "Processed {indexed} files, {failed} failed": "Elaborati {indexed} file, {failed} non riusciti", + "Property could not be updated": "Impossibile aggiornare la proprietà", + "Property name of inversed relation": "Nome della proprietà della relazione inversa", + "Property successfully updated": "Proprietà aggiornata correttamente", + "Publish Configuration to GitHub": "Pubblica configurazione su GitHub", + "Publish OAS": "Pubblica OAS", + "Publish objects to make them publicly available": "Pubblica gli oggetti per renderli disponibili pubblicamente", + "Publish Register OAS to GitHub": "Pubblica OAS del registro su GitHub", + "Query Help": "Aiuto per le query", + "Quota": "Quota", + "RBAC": "RBAC", + "read": "leggi", + "Reason": "Motivo", + "Reason (optional)": "Motivo (facoltativo)", + "Register #{id}": "Registro n. {id}", + "Register / Schema": "Registro / Schema", + "Register and schema are required": "Il registro e lo schema sono obbligatori", + "Register Overview": "Panoramica registro", + "Register:": "Registro:", + "register{plural}": "register{plural}", + "Reindex completed! Processed {count} objects in {duration}s": "Reindicizzazione completata! Elaborati {count} oggetti in {duration}s", + "Reject": "Rifiuta", + "Rejected": "Rifiutato", + "rejected": "rifiutato", + "Related Cases": "Casi correlati", + "Remove avatar": "Rimuovi avatar", + "Remove from list": "Rimuovi dall'elenco", + "Remove link": "Rimuovi collegamento", + "Remove link to {name}": "Rimuovi collegamento a {name}", + "Remove link to {name}?": "Rimuovere il collegamento a {name}?", + "Remove link to {title}": "Rimuovi collegamento a {title}", + "Remove oneOf entry": "Rimuovi voce oneOf", + "Remove property after write-back": "Rimuovi la proprietà dopo la riscrittura", + "Remove {title}": "Rimuovi {title}", + "Remove {userId} from this organisation?": "Rimuovere {userId} da questa organizzazione?", + "Removing schema {schema}...": "Rimozione dello schema {schema}...", + "Request account deactivation": "Richiedi disattivazione account", + "Request account deactivation. This will notify administrators for review.": "Richieda la disattivazione dell'account. Questa operazione invierà una notifica agli amministratori per la revisione.", + "Request Quota (per day)": "Quota richieste (al giorno)", + "Requested at": "Richiesto il", + "Required": "Obbligatorio", + "Required Token Scopes": "Ambiti del token obbligatori", + "Requires Dolphin AI for OCR text extraction": "Richiede Dolphin AI per l'estrazione del testo OCR", + "Rerunning search: {searchTerm}": "Riesecuzione della ricerca: {searchTerm}", + "Resource Quotas": "Quote delle risorse", + "Retry upload": "Riprova caricamento", + "Review the selected objects below. You can remove any objects you don't want to include.": "Riveda gli oggetti selezionati di seguito. Può rimuovere gli oggetti che non desidera includere.", + "Revoke": "Revoca", + "Right": "Destra", + "Roles and Permissions": "Ruoli e autorizzazioni", + "Save labels": "Salva etichette", + "Save tags for {name}": "Salva etichette per {name}", + "Save the organisation first to manage users.": "Salvi prima l'organizzazione per gestire gli utenti.", + "Schema configuration updated successfully for {schema}": "Configurazione dello schema aggiornata correttamente per {schema}", + "Schema reference ($ref)": "Riferimento allo schema ($ref)", + "Schema {schema} removed successfully": "Schema {schema} rimosso correttamente", + "Schema:": "Schema:", + "schema{plural}": "schema{plural}", + "Search": "Cerca", + "Search / views": "Cerca / viste", + "Search agents...": "Cerca agenti...", + "Search applications...": "Cerca applicazioni...", + "Search by title or UUID...": "Cerca per titolo o UUID...", + "Search Configuration": "Configurazione di ricerca", + "Search configurations": "Cerca configurazioni", + "Search data sources...": "Cerca origini dati...", + "Search groups...": "Cerca gruppi...", + "Search in Database Objects": "Cerca negli oggetti del database", + "Search Mode": "Modalità di ricerca", + "Search objects": "Cerca oggetti", + "Search objects...": "Cerca oggetti...", + "Search Query": "Query di ricerca", + "Search registers...": "Cerca registri...", + "Search Results": "Risultati della ricerca", + "Search schemas...": "Cerca schemi...", + "Search {name}...": "Cerca {name}...", + "Searching...": "Ricerca...", + "Security": "Sicurezza", + "See organisations as a table": "Visualizza le organizzazioni come tabella", + "See organisations as cards": "Visualizza le organizzazioni come schede", + "Select a date property": "Selezioni una proprietà data", + "Select a register": "Selezioni un registro", + "Select a register...": "Selezioni un registro...", + "Select a schema": "Selezioni uno schema", + "Select a schema...": "Selezioni uno schema...", + "Select a user": "Selezioni un utente", + "Select Active Organisation": "Seleziona organizzazione attiva", + "Select agent type": "Selezioni il tipo di agente", + "Select an email to see linked objects": "Selezioni un'email per visualizzare gli oggetti collegati", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Selezioni un'organizzazione e un utente per aggiungerlo come membro. Cerchi le organizzazioni per nome.", + "Select configuration type...": "Selezioni il tipo di configurazione...", + "Select fields to display": "Selezioni i campi da visualizzare", + "Select files with the correct extension": "Selezioni i file con l'estensione corretta", + "Select group": "Selezioni un gruppo", + "Select groups (optional)": "Selezioni i gruppi (facoltativo)", + "Select groups to notify...": "Selezioni i gruppi da notificare...", + "Select groups with access to this agent": "Selezioni i gruppi con accesso a questo agente", + "Select groups...": "Selezioni i gruppi...", + "Select one or more registers": "Selezioni uno o più registri", + "Select one or more schemas": "Selezioni uno o più schemi", + "Select or create labels, or select \"No label\" to add files": "Selezioni o crei etichette, oppure selezioni \"Nessuna etichetta\" per aggiungere file", + "Select owner application (optional)...": "Selezioni l'applicazione proprietaria (facoltativo)...", + "Select role": "Selezioni il ruolo", + "Select schemas (empty = all schemas)": "Selezioni gli schemi (vuoto = tutti gli schemi)", + "Select schemas to warm up": "Selezioni gli schemi da precaricare", + "Select search mode": "Selezioni la modalità di ricerca", + "Select source type...": "Selezioni il tipo di origine...", + "Select target type": "Selezioni il tipo di destinazione", + "Select views to filter data (optional)": "Selezioni le viste per filtrare i dati (facoltativo)", + "Select views to limit which data the agent can access": "Selezioni le viste per limitare a quali dati l'agente può accedere", + "Select {fieldName} values": "Selezioni i valori di {fieldName}", + "Selected Objects ({count})": "Oggetti selezionati ({count})", + "Send as cloudevent": "Invia come cloudevent", + "Set as Active": "Imposta come attivo", + "Set as active organisation": "Imposta come organizzazione attiva", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Imposti i limiti per l'utilizzo delle API e il consumo dei token. Utilizzi 0 per risorse illimitate.", + "Showing {showing} of {total} agents": "Visualizzazione di {showing} di {total} agenti", + "Showing {showing} of {total} applications": "Visualizzazione di {showing} di {total} applicazioni", + "Showing {showing} of {total} audit trail entries": "Visualizzazione di {showing} di {total} voci del registro di controllo", + "Showing {showing} of {total} configurations": "Visualizzazione di {showing} di {total} configurazioni", + "Showing {showing} of {total} deleted items": "Visualizzazione di {showing} di {total} elementi eliminati", + "Showing {showing} of {total} entities": "Visualizzazione di {showing} di {total} entità", + "Showing {showing} of {total} files": "Visualizzazione di {showing} di {total} file", + "Showing {showing} of {total} log entries": "Visualizzazione di {showing} di {total} voci di registro", + "Showing {showing} of {total} search trail entries": "Visualizzazione di {showing} di {total} voci del registro di ricerca", + "Showing {showing} of {total} sources": "Visualizzazione di {showing} di {total} origini", + "Showing {showing} of {total} templates": "Visualizzazione di {showing} di {total} modelli", + "Showing {showing} of {total} webhooks": "Visualizzazione di {showing} di {total} webhook", + "Showing {shown} of {total} organisations": "Visualizzazione di {shown} di {total} organizzazioni", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Query semplici: ricerche di testo di base con parametri minimi (ad es. termine di ricerca singolo, nessun filtro avanzato)", + "SOLR collection deleted successfully": "Raccolta SOLR eliminata correttamente", + "SOLR Index Warmup": "Precaricamento indice SOLR", + "Something went wrong while adding files": "Si è verificato un problema durante l'aggiunta dei file", + "Source successfully updated": "Origine aggiornata correttamente", + "Special Rights": "Diritti speciali", + "Start Date Field": "Campo data di inizio", + "Starting validation for {schema}...": "Avvio della convalida per {schema}...", + "Starting {action} for {schema}...": "Avvio di {action} per {schema}...", + "Statistics and Metrics": "Statistiche e metriche", + "Statistics refreshed": "Statistiche aggiornate", + "Step": "Fase", + "Storage": "Archiviazione", + "Storage Quota (MB)": "Quota di archiviazione (MB)", + "Successfully added user to organisation: {name}": "Utente aggiunto correttamente all'organizzazione: {name}", + "Successfully created {count} SOLR fields": "Creati correttamente {count} campi SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Eliminati correttamente {count} embedding. Rivettorizzi i suoi dati.", + "Successfully deleted {count} objects for {schema}": "Eliminati correttamente {count} oggetti per {schema}", + "Successfully fixed {count} SOLR field configurations": "Corrette correttamente {count} configurazioni di campi SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importati correttamente: {registers} registri, {schemas} schemi, {objects} oggetti", + "Successfully permanently deleted {count} objects": "Eliminati permanentemente correttamente {count} oggetti", + "Successfully processed {count} files!": "Elaborati correttamente {count} file!", + "Successfully saved configuration for {count} facets!": "Configurazione salvata correttamente per {count} faccette!", + "Supported File Types": "Tipi di file supportati", + "Switch Active Organisation": "Cambia organizzazione attiva", + "Switch Organisation": "Cambia organizzazione", + "Syncing magic table for {schema}...": "Sincronizzazione della tabella magica per {schema}...", + "Syncs": "Sincronizzazioni", + "System announcements": "Annunci di sistema", + "System Prompt": "Prompt di sistema", + "Table": "Tabella", + "Target ID": "ID destinazione", + "Target Type*": "Tipo di destinazione*", + "Text Chunk #{id}": "Blocco di testo n. {id}", + "Text Extraction": "Estrazione del testo", + "Text Extraction Engine": "Motore di estrazione del testo", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Saranno conservati secondo il periodo di conservazione configurato nel relativo schema e successivamente rimossi permanentemente.", + "This action cannot be undone. Make sure no collections are using this configset.": "Questa azione non può essere annullata. Si assicuri che nessuna raccolta stia utilizzando questo configset.", + "This action will submit a deactivation request to your administrators.": "Questa azione invierà una richiesta di disattivazione ai suoi amministratori.", + "This is not valid JSON": "Questo non è un JSON valido", + "This key already exists on this schema": "Questa chiave esiste già in questo schema", + "This schema must use magic table configuration to sync": "Questo schema deve utilizzare la configurazione della tabella magica per sincronizzarsi", + "This token will only be shown once. Copy it now.": "Questo token sarà mostrato solo una volta. Lo copi ora.", + "This will copy the _default configset with the new name": "Questa operazione copierà il configset _default con il nuovo nome", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Questa operazione eliminerà permanentemente TUTTI gli embedding (vettori) dal database. Dovrà rivettorizzare tutti gli oggetti e i file. Questa azione non può essere annullata.\n\nÈ sicuro di voler continuare?", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Questa operazione rimuoverà permanentemente {count} oggetti già eliminati provvisoriamente dal database. Questa azione non può essere annullata.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Suggerimento: abiliti solo le viste che necessitano della ricerca semantica per ridurre al minimo i costi di embedding. Le semplici tabelle di consultazione raramente necessitano di vettorizzazione.", + "Title *": "Titolo *", + "Title Template": "Modello di titolo", + "Title*": "Titolo*", + "To (e.g. -1 year or 2025-12-31)": "A (ad es. -1 year o 2025-12-31)", + "To add files larger than or equal to 512MB, go to the": "Per aggiungere file di dimensioni maggiori o uguali a 512MB, vada alla", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Per regolare la dimensione dei blocchi o la strategia, vada alla configurazione dei file → limiti di elaborazione.", + "To change the embedding provider or model, go to LLM configuration.": "Per modificare il provider o il modello di embedding, vada alla configurazione LLM.", + "Token copied to clipboard": "Token copiato negli appunti", + "Token Created": "Token creato", + "Token name": "Nome token", + "Token Quota (per request)": "Quota token (per richiesta)", + "Token revoked": "Token revocato", + "Tools respect the agent's own role and organization boundaries.": "Gli strumenti rispettano il ruolo proprio dell'agente e i confini dell'organizzazione.", + "Try searching by UUID or with different keywords": "Provi a cercare per UUID o con parole chiave diverse", + "Type \"DELETE COLLECTION\" to confirm:": "Digiti \"ELIMINA RACCOLTA\" per confermare:", + "Type to search for objects...": "Digiti per cercare oggetti...", + "Type to search for organisations": "Digiti per cercare organizzazioni", + "Type to search...": "Digiti per cercare...", + "Type your username to confirm": "Digiti il suo nome utente per confermare", + "Unknown Object": "Oggetto sconosciuto", + "Unnamed Object": "Oggetto senza nome", + "update": "aggiorna", + "Update available: {local} → {remote}": "Aggiornamento disponibile: {local} → {remote}", + "Upload new avatar": "Carica nuovo avatar", + "Url": "Url", + "Use AI agents for processing and analysis": "Utilizza agenti IA per l'elaborazione e l'analisi", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Utilizza intervalli predefiniti (Ultimi 7/30/90 giorni, Ultimo anno, Più vecchio)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Utilizza un'archiviazione esterna (ad es. MongoDB) anziché l'archiviazione interna di Nextcloud.", + "Use Large Language Model features": "Utilizza le funzionalità dei modelli linguistici di grandi dimensioni", + "Use SolrCloud with zookeeper for distributed search": "Utilizzi SolrCloud con Zookeeper per la ricerca distribuita", + "Use VNG APIs instead of MongoDB.": "Utilizza le API VNG anziché MongoDB.", + "Used by": "Utilizzato da", + "User Quota": "Quota utente", + "Uses": "Utilizzi", + "UUID copied to clipboard": "UUID copiato negli appunti", + "Validation completed for {schema}": "Convalida completata per {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Convalida completata per {schema}: {processed} elaborati, {updated} aggiornati, {failed} non riusciti", + "Value list (split on ,)": "Elenco valori (separati da ,)", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vettorizzazione completata: {vectorized} oggetti vettorizzati, {failed} non riusciti", + "Vectorization failed: {error}": "Vettorizzazione non riuscita: {error}", + "Version Information": "Informazioni sulla versione", + "View": "Visualizza", + "view": "visualizza", + "View and manage authorization across registers and schemas": "Visualizzi e gestisca l'autorizzazione tra registri e schemi", + "View API Documentation": "Visualizza documentazione API", + "View APIs": "Visualizza API", + "View object": "Visualizza oggetto", + "View {count} more": "Visualizza altri {count}", + "Visible to end users": "Visibile agli utenti finali", + "waiting": "in attesa", + "Warmup failed": "Precaricamento non riuscito", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Quando l'agente viene eseguito senza una sessione utente (ad es. attività pianificate), verrà utilizzato il contesto di questo utente.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Quando disabilitata, questa faccetta mostrerà solo i valori di questo schema e includerà un filtro schema quando selezionata.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Quando abilitato, il salvataggio di questo oggetto aggiornerà anche gli oggetti di destinazione per includere un riferimento a questo oggetto.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Quando abilitato, questa proprietà sarà rimossa dall'oggetto di origine dopo l'aggiornamento degli oggetti di destinazione.", + "Workflow Management": "Gestione dei flussi di lavoro", + "Wrap webhook payload in cloudevents format for better interoperability": "Incapsula il payload del webhook nel formato cloudevents per una migliore interoperabilità", + "You are not yet a member of any organisations.": "Non è ancora membro di alcuna organizzazione.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Può creare nuovi configset basati sul modello _default o caricarne di personalizzati direttamente sul suo server SOLR.", + "You do not have permission to view the permission matrix. Admin access is required.": "Non dispone dell'autorizzazione per visualizzare la matrice delle autorizzazioni. È richiesto l'accesso come amministratore.", + "You have selected {count} properties to add to the schema.": "Ha selezionato {count} proprietà da aggiungere allo schema.", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Il suo feedback è stato registrato. Facoltativamente, può fornire qui ulteriori dettagli...", + "{active} active, {deleted} deleted": "{active} attivi, {deleted} eliminati", + "{count} audit trails cleared successfully": "{count} registri di controllo azzerati correttamente", + "{count} deletions": "{count} eliminazioni", + "{count} documents found": "{count} documenti trovati", + "{count} entries": "{count} voci", + "{count} issues": "{count} problemi", + "{count} members": "{count} membri", + "{count} searches": "{count} ricerche", + "{count} selected": "{count} selezionati", + "{minutes} minutes ago": "{minutes} minuti fa", + "{percentage}% of objects": "{percentage}% degli oggetti", + "{property} - {other}": "{property} - {other}", + "ℹ️ current configuration": "ℹ️ configurazione attuale", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ PERICOLO: Tutti i {total} oggetti saranno ELIMINATI PERMANENTEMENTE dal database. Questa azione è IRRECUPERABILE e non può essere annullata!", + "{count} compliance issue(s) detected": "{count} problema/i di conformità rilevato/i", + "{count} email": "{count} email", + "{count} emails": "{count} email", + "{count} widget(s)": "{count} widget", + "{matched} object(s) matched. {erased} erased.": "{matched} oggetto/i corrispondente/i. {erased} cancellato/i.", + "{type} cache cleared successfully": "Cache {type} azzerata correttamente", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVVISO DI ELIMINAZIONE PERMANENTE \\u26A0\\uFE0F\n\nSta per eliminare PERMANENTEMENTE {count} oggetti eliminati provvisoriamente per lo schema \"{schema}\".\n\nQuesti oggetti saranno rimossi completamente dal database e NON potranno essere recuperati.\n\nÈ assolutamente sicuro?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVVISO DI ELIMINAZIONE PERMANENTE \\u26A0\\uFE0F\n\nSta per eliminare PERMANENTEMENTE TUTTI gli oggetti per lo schema \"{schema}\":\n\n\\u2022 Oggetti attivi: {active}\n\\u2022 Oggetti eliminati provvisoriamente: {deleted}\n\\u2022 Totale: {total}\n\nQuesti oggetti saranno rimossi completamente dal database e NON potranno essere recuperati.\n\nÈ assolutamente sicuro?", + "Add a contact from any of your address books to associate it with this object.": "Aggiunga un contatto da una qualsiasi delle sue rubriche per associarlo a questo oggetto.", + "Agent successfully created": "Agente creato correttamente", + "and accessible to all users in selected groups (or all users if no groups selected).": "e accessibile a tutti gli utenti nei gruppi selezionati (o a tutti gli utenti se non è selezionato alcun gruppo).", + "App store cache invalidated: {invalidated}": "Cache dell'app store invalidata: {invalidated}", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Archiviare questa verwerkingsactiviteit? Le righe del registro di controllo continueranno a farvi riferimento.", + "are only accessible to invited users.": "sono accessibili solo agli utenti invitati.", + "Attachment added to {name}": "Allegato aggiunto a {name}", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: questo report associa ogni attività di trattamento ai conteggi del registro di controllo a essa attribuiti per l'intera durata. I revisori e l'Autoriteit Persoonsgegevens lo utilizzano per la revisione di vigilanza.", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (durata ISO-8601, ad es. P10Y, P30D)", + "by": "da", + "Calendar integration is not available": "L'integrazione del calendario non è disponibile", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Faccia clic su Esegui scansione di conformità per trovare gli schemi in cui sono stati rilevati dati personali identificabili (PII) ma non esiste alcuna annotazione di attività di trattamento.", + "Click to subscribe to notifications": "Faccia clic per iscriversi alle notifiche", + "Click to unsubscribe from notifications": "Faccia clic per annullare l'iscrizione alle notifiche", + "Code (short readable key, e.g. v-2026-001)": "Codice (chiave breve e leggibile, ad es. v-2026-001)", + "Contacts integration is not available": "L'integrazione dei contatti non è disponibile", + "Create a new event or link an existing one from any of your calendars.": "Crei un nuovo evento o ne colleghi uno esistente da uno qualsiasi dei suoi calendari.", + "Create or link a Deck card to track work on this object.": "Crei o colleghi una scheda Deck per tenere traccia del lavoro su questo oggetto.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Crei la prima verwerkingsactiviteit per iniziare a etichettare le righe del registro di controllo con la loro attribuzione AVG Art 30.", + "CSV": "CSV", + "Dashboard not found": "Dashboard non trovata", + "Data-subject access request": "Richiesta di accesso dell'interessato", + "Deck": "Deck", + "Deck integration is not available": "L'integrazione di Deck non è disponibile", + "Delete {count} object": "Elimina {count} oggetto", + "Delete {count} objects": "Elimina {count} oggetti", + "Doelbinding *": "Doelbinding *", + "DSAR": "DSAR", + "Edit verwerkingsactiviteit": "Modifica verwerkingsactiviteit", + "Erase (Art 17)": "Cancella (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Cancellare {count} oggetto/i per questo interessato? Questa azione viene registrata nel registro di controllo.", + "Every schema with detected PII has a processing-activity annotation.": "Ogni schema con dati personali identificabili (PII) rilevati dispone di un'annotazione di attività di trattamento.", + "Excel (.xlsx)": "Excel (.xlsx)", + "Failed to add attachment to object": "Aggiunta dell'allegato all'oggetto non riuscita", + "Failed to clear {type} cache: {error}": "Azzeramento della cache {type} non riuscito: {error}", + "Failed to clear audit trails: {error}": "Azzeramento dei registri di controllo non riuscito: {error}", + "Failed to clear blob objects: {error}": "Azzeramento degli oggetti blob non riuscito: {error}", + "Failed to clear cache: {error}": "Azzeramento della cache non riuscito: {error}", + "Failed to clear search trails: {error}": "Azzeramento dei registri di ricerca non riuscito: {error}", + "Failed to discover files: {error}": "Individuazione dei file non riuscita: {error}", + "Failed to invalidate app store cache: {error}": "Invalidazione della cache dell'app store non riuscita: {error}", + "Failed to load cache statistics: {error}": "Caricamento delle statistiche della cache non riuscito: {error}", + "Failed to load Deck cards": "Caricamento delle schede Deck non riuscito", + "Failed to load linked contacts": "Caricamento dei contatti collegati non riuscito", + "Failed to load linked emails": "Caricamento delle email collegate non riuscito", + "Failed to load linked events": "Caricamento degli eventi collegati non riuscito", + "Failed to load related entities": "Caricamento delle entità correlate non riuscito", + "Failed to load settings: {error}": "Caricamento delle impostazioni non riuscito: {error}", + "Failed to load statistics: {error}": "Caricamento delle statistiche non riuscito: {error}", + "Failed to save file settings: {error}": "Salvataggio delle impostazioni dei file non riuscito: {error}", + "Failed to save LLM settings: {error}": "Salvataggio delle impostazioni LLM non riuscito: {error}", + "Failed to save settings: {error}": "Salvataggio delle impostazioni non riuscito: {error}", + "Failed to save warmup interval: {error}": "Salvataggio dell'intervallo di precaricamento non riuscito: {error}", + "Failed to setup SOLR: {error}": "Configurazione di SOLR non riuscita: {error}", + "Failed to start processing {type} files: {error}": "Avvio dell'elaborazione dei file {type} non riuscito: {error}", + "Failed to test SOLR connection: {error}": "Test della connessione SOLR non riuscito: {error}", + "Failed to update LLM settings: {error}": "Aggiornamento delle impostazioni LLM non riuscito: {error}", + "Failed to update Multitenancy settings: {error}": "Aggiornamento delle impostazioni Multitenancy non riuscito: {error}", + "Failed to update RBAC settings: {error}": "Aggiornamento delle impostazioni RBAC non riuscito: {error}", + "Failed to update Retention settings: {error}": "Aggiornamento delle impostazioni di conservazione non riuscito: {error}", + "Failed to update SOLR settings: {error}": "Aggiornamento delle impostazioni SOLR non riuscito: {error}", + "File settings saved successfully": "Impostazioni dei file salvate correttamente", + "Filter and search entities": "Filtra e cerca entità", + "Generate report": "Genera report", + "Generate the verantwoordingsdocument": "Genera il verantwoordingsdocument", + "Generated: {time}": "Generato: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Ottenga informazioni in tempo reale sullo stato di salute dei dati della sua organizzazione concentrandosi su registri, definizioni di schema e archiviazione e utilizzo degli oggetti.", + "HTML preview": "Anteprima HTML", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importi il modello report-bundle.json per ottenere il registro `reports`, quindi crei la sua prima dashboard tramite l'interfaccia oggetto standard. Le dashboard dichiarano i loro widget in JSON e il renderer alimenta ogni widget con dati di aggregazione in tempo reale.", + "Integrations": "Integrazioni", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Risultati Inzage", + "Languages": "Lingue", + "Link an email from the Mail app sidebar to associate it with this object.": "Colleghi un'email dalla barra laterale dell'app Mail per associarla a questo oggetto.", + "Link existing event": "Collega evento esistente", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Le email collegate, gli eventi del calendario, i contatti e le schede Deck appariranno qui.", + "LLM settings saved successfully": "Impostazioni LLM salvate correttamente", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Individui ogni oggetto che fa riferimento a un interessato (Art 15 inzage), visualizzi l'anteprima di una cancellazione (Art 17 vergetelheid) o esporti i suoi dati (Art 20 portabiliteit).", + "Mail integration is not available": "L'integrazione email non è disponibile", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Gestisca le attività di trattamento, esegua le richieste di accesso degli interessati e verifichi la conformità ai sensi del RGPD UE / AVG olandese.", + "Mass validation completed successfully": "Convalida di massa completata correttamente", + "Mass validation failed: {error}": "Convalida di massa non riuscita: {error}", + "missing": "mancante", + "Multitenancy settings updated successfully": "Impostazioni Multitenancy aggiornate correttamente", + "Naam": "Naam", + "Naam *": "Naam *", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Cache dei nomi precaricata correttamente: {count} nomi caricati in {time}. {message}", + "New activity": "Nuova attività", + "New Configuration": "Nuova configurazione", + "New verwerkingsactiviteit": "Nuova verwerkingsactiviteit", + "No contacts linked to this object": "Nessun contatto collegato a questo oggetto", + "No dashboards yet": "Ancora nessuna dashboard", + "No Deck cards linked to this object": "Nessuna scheda Deck collegata a questo oggetto", + "No emails linked to this object": "Nessuna email collegata a questo oggetto", + "No events linked to this object": "Nessun evento collegato a questo oggetto", + "No matches": "Nessuna corrispondenza", + "No personal data was found for this subject identifier.": "Non sono stati trovati dati personali per questo identificatore dell'interessato.", + "No processing activities yet": "Ancora nessuna attività di trattamento", + "No related entities": "Nessuna entità correlata", + "No saved views yet. create one in the search tab!": "Ancora nessuna vista salvata. Ne crei una nella scheda di ricerca!", + "No scan run yet": "Ancora nessuna scansione eseguita", + "Object metadata incomplete for file upload": "Metadati dell'oggetto incompleti per il caricamento del file", + "Object successfully deleted": "Oggetto eliminato correttamente", + "Objects successfully deleted": "Oggetti eliminati correttamente", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Dashboard definite dall'operatore e report pianificati. Ogni dashboard è un oggetto di prima classe nel registro `reports`; i widget sono dichiarati nell'array `widgets` della dashboard e renderizzati in tempo reale a partire da aggregazioni / GraphQL.", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Tag di lingua BCP 47 ordinati. La prima lingua è quella predefinita del registro e determina il fallback Accept-Language per le proprietà traducibili.", + "Organisation successfully created": "Organizzazione creata correttamente", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "PDF": "PDF", + "PII hits": "Riscontri PII", + "PII rows": "Righe PII", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preview erasure": "Anteprima cancellazione", + "Property could not be added": "Impossibile aggiungere la proprietà", + "Property successfully added": "Proprietà aggiunta correttamente", + "public": "pubblico", + "Purge {count} object from database": "Elimina definitivamente {count} oggetto dal database", + "Purge {count} objects from database": "Elimina definitivamente {count} oggetti dal database", + "RBAC settings updated successfully": "Impostazioni RBAC aggiornate correttamente", + "Rebase operation completed successfully": "Operazione di rebase completata correttamente", + "Rebase operation failed: {error}": "Operazione di rebase non riuscita: {error}", + "Rechtsgrond": "Rechtsgrond", + "Register annotation": "Annotazione registro", + "Register objects": "Oggetti del registro", + "Remove contact": "Rimuovi contatto", + "Remove Deck card": "Rimuovi scheda Deck", + "Reports": "Report", + "Restore {count} object": "Ripristina {count} oggetto", + "Restore {count} objects": "Ripristina {count} oggetti", + "Retention settings updated successfully": "Impostazioni di conservazione aggiornate correttamente", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Riveda gli oggetti selezionati di seguito. Può rimuovere gli oggetti che non desidera eliminare facendo clic sul pulsante di rimozione.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Riveda gli oggetti selezionati di seguito. Può rimuovere gli oggetti che non desidera eliminare permanentemente facendo clic sul pulsante di rimozione. Questa azione non può essere annullata.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Riveda gli oggetti selezionati di seguito. Può rimuovere gli oggetti che non desidera ripristinare facendo clic sul pulsante di rimozione. Gli oggetti saranno ripristinati nella loro posizione originale.", + "Run compliance scan": "Esegui scansione di conformità", + "Scheduled": "Pianificato", + "Schema annotation": "Annotazione schema", + "selected objects": "oggetti selezionati", + "set": "imposta", + "Settings updated successfully": "Impostazioni aggiornate correttamente", + "Show": "Mostra", + "Showing {count} dashboard(s)": "Visualizzazione di {count} dashboard", + "Showing {count} processing activities": "Visualizzazione di {count} attività di trattamento", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Elimina provvisoriamente tutti gli oggetti per questo schema ({active} attivi, {deleted} già eliminati)", + "SOLR connection test failed: {error}": "Test della connessione SOLR non riuscito: {error}", + "SOLR connection test successful!": "Test della connessione SOLR riuscito!", + "SOLR index warmup completed successfully": "Precaricamento dell'indice SOLR completato correttamente", + "SOLR settings updated successfully": "Impostazioni SOLR aggiornate correttamente", + "SOLR setup completed successfully!": "Configurazione SOLR completata correttamente!", + "SOLR setup failed: {error}": "Configurazione SOLR non riuscita: {error}", + "SOLR warmup failed: {error}": "Precaricamento SOLR non riuscito: {error}", + "Subject identifier (email, BSN, name, etc.)": "Identificatore dell'interessato (email, BSN, nome, ecc.)", + "Subscribe": "Iscriviti", + "Subscribed": "Iscritto", + "Successfully cleared {count} audit trails": "{count} registri di controllo azzerati correttamente", + "Successfully cleared {count} blob storage objects": "{count} oggetti di archiviazione blob azzerati correttamente", + "Successfully cleared {count} search trails": "{count} registri di ricerca azzerati correttamente", + "Successfully restored {count} object": "{count} oggetto ripristinato correttamente", + "Successfully restored {count} objects": "{count} oggetti ripristinati correttamente", + "Technical Details": "Dettagli tecnici", + "Technische maatregelen": "Technische maatregelen", + "The Nextcloud Calendar app is not installed or enabled on this server.": "L'app Calendario di Nextcloud non è installata o abilitata su questo server.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "L'app Contatti di Nextcloud non è installata o abilitata su questo server.", + "The Nextcloud Deck app is not installed or enabled on this server.": "L'app Deck di Nextcloud non è installata o abilitata su questo server.", + "The Nextcloud Mail app is not installed or enabled on this server.": "L'app Mail di Nextcloud non è installata o abilitata su questo server.", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Impossibile caricare la dashboard richiesta. Verifichi l'URL o scelga un'altra dashboard dall'elenco.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Saranno conservati secondo il periodo di conservazione configurato nel relativo schema ed eliminati automaticamente in modo permanente alla scadenza del periodo di conservazione. Il periodo di conservazione è configurabile per ogni schema e può essere consultato nelle impostazioni dello schema.", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Gli strumenti vengono eseguiti con le autorizzazioni dell'utente predefinito dell'agente quando non è attiva alcuna sessione utente (ad es. processi cron). Configuri l'utente predefinito nella scheda Impostazioni.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Gli strumenti rispettano le viste, le autorizzazioni e i confini dell'organizzazione dell'agente.", + "Total events": "Eventi totali", + "Type filter (optional, e.g. email)": "Filtro tipo (facoltativo, ad es. email)", + "Unknown widget type: {type}": "Tipo di widget sconosciuto: {type}", + "Unlink email": "Scollega email", + "Unlink event": "Scollega evento", + "Untitled": "Senza titolo", + "Use filters to narrow down entities by type or category.": "Utilizzi i filtri per restringere le entità per tipo o categoria.", + "Verantwoording": "Verantwoording", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Avviso: questa proprietà ha tipi incoerenti: {types}. Valuti se tutti gli oggetti debbano avere lo stesso tipo.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Eseguiremo la scansione di tutti gli oggetti appartenenti a questo schema per scoprire nuove proprietà e analizzare le proprietà esistenti alla ricerca di potenziali miglioramenti. Il processo comporta l'esame della struttura dei dati di ogni oggetto, l'identificazione delle proprietà non definite nello schema attuale e l'individuazione di opportunità per migliorare le definizioni delle proprietà esistenti con vincoli, formati e regole di convalida migliori.", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Quando l'agente viene eseguito senza una sessione utente (ad es. attività pianificate), verrà utilizzato il contesto di questo utente", + "Workflows": "Flussi di lavoro", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "Bookmarks": "Bookmarks", + "Categories of data subjects (one per line)": "Categories of data subjects (one per line)", + "Categories of personal data (one per line)": "Categories of personal data (one per line)", + "Configuring field mappings...": "Configuring field mappings...", + "Connecting to SOLR cluster...": "Connecting to SOLR cluster...", + "Creating search collections...": "Creating search collections...", + "Edit processing activity": "Edit processing activity", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Finalizing setup...": "Finalizing setup...", + "Forbidden": "Forbidden", + "Initializing SOLR setup...": "Initializing SOLR setup...", + "Legal basis *": "Legal basis *", + "New processing activity": "New processing activity", + "Optimizing search performance...": "Optimizing search performance...", + "Organisational measures": "Organisational measures", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Purpose limitation *": "Purpose limitation *", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Retention period (ISO-8601 duration, e.g. P10Y, P30D)", + "SOLR setup failed. Please check the configuration and try again.": "SOLR setup failed. Please check the configuration and try again.", + "Select all entities": "Select all entities", + "Select entity {value}": "Select entity {value}", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "Technical measures": "Technical measures", + "Uploading configuration sets...": "Uploading configuration sets...", + "Verifying server connectivity...": "Verifying server connectivity...", + "Waiting for cluster synchronization...": "Waiting for cluster synchronization...", + "You do not have access to this object": "You do not have access to this object", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR can handle millions of documents with sub-second search response times...", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Faceted search allows users to drill down into results by categories...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR provides rich analytics and statistics about search performance...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets contain the schema and configuration files for your search index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replication ensures your search index is available even if nodes fail...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Auto-scaling can dynamically add or remove nodes based on load...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Security features include authentication, authorization, and SSL encryption...", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} email", + "{count} email" + ], + "Purge {count} object from database": [ + "Elimina definitivamente {count} oggetto dal database", + "Elimina definitivamente {count} oggetti dal database" + ], + "Restore {count} object": [ + "Ripristina {count} oggetto", + "Ripristina {count} oggetti" + ], + "Successfully restored {count} object": [ + "Ripristinato correttamente {count} oggetto", + "Ripristinati correttamente {count} oggetti" + ], + "Delete {count} object": [ + "Elimina {count} oggetto", + "Elimina {count} oggetti" + ], + "Object successfully deleted": [ + "Oggetto eliminato correttamente", + "Oggetti eliminati correttamente" + ] + } +} diff --git a/l10n/lb.js b/l10n/lb.js new file mode 100644 index 0000000000..64710bf01d --- /dev/null +++ b/l10n/lb.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(kee Sujet)", + "(no title)" : "(keen Titel)", + "(unnamed)" : "(ouni Numm)", + "(untitled card)" : "(Kaart ouni Titel)", + "Activities" : "Aktivitéiten", + "Add Endpoint" : "Endpunkt derbäisetzen", + "Add Source" : "Quell derbäisetzen", + "Add card" : "Kaart derbäisetzen", + "Add contact" : "Kontakt derbäisetzen", + "All clear" : "Alles a Rei", + "Articles" : "Artikelen", + "Code" : "Code", + "Compliance" : "Konformitéit", + "Contacts" : "Kontakter", + "Create Agent" : "Agent erstellen", + "Create Application" : "Applikatioun erstellen", + "Create event" : "Evenement erstellen", + "Erasure complete" : "Läschung ofgeschloss", + "Erasure preview" : "Läschungsvirschau", + "Expand details" : "Detailer opklappen", + "Save changes" : "Ännerungen späicheren", + "📄 Object Serialization" : "📄 Objet-Serialiséierung", + "🔢 Vectorization Settings" : "🔢 Vektoriséierungs-Astellungen", + "💰 View Selection (Cost Optimization)" : "💰 Usiicht-Auswiel (Käschtenoptiméierung)", + "AI Assistant" : "KI-Assistent", + "AI Chat" : "KI-Chat", + "API Key" : "API-Schlëssel", + "API tokens saved successfully" : "API-Tokens erfollegräich gespäichert", + "About ConfigSets" : "Iwwer ConfigSets", + "Action" : "Aktioun", + "Action Distribution" : "Aktiounsverdeelung", + "Action:" : "Aktioun:", + "Actions" : "Aktiounen", + "Active" : "Aktiv", + "Active Collections" : "Aktiv Sammlungen", + "Active Filters" : "Aktiv Filteren", + "Active Organisations" : "Aktiv Organisatiounen", + "Active filters:" : "Aktiv Filteren:", + "Active organisation set successfully" : "Aktiv Organisatioun erfollegräich gesat", + "Activity Period" : "Aktivitéitsperiod", + "Add Groups" : "Gruppen derbäisetzen", + "Add Schema" : "Schema derbäisetzen", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Setzt Schematitelen, Beschreiwungen a Registerinformatiounen derbäi, fir e räicheren Kontext fir d'Sich ze ginn", + "Add to favorites" : "Zu de Favoritten derbäisetzen", + "Added" : "Derbäigesat", + "Added to favorites" : "Zu de Favoritten derbäigesat", + "Additional Information" : "Zousätzlech Informatiounen", + "Additional feedback saved. Thank you!" : "Zousätzlecht Feedback gespäichert. Merci!", + "Admin" : "Administrateur", + "Advanced" : "Erweidert", + "Advanced Filters" : "Erweidert Filteren", + "Advanced Options" : "Erweidert Optiounen", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Erweidert Filteren sinn net verfügbar wann d'Datebankquell benotzt gëtt. Wiesselt op Auto oder SOLR-Index fir Filteroptiounen.", + "After" : "No", + "Agent deleted successfully" : "Agent erfollegräich geläscht", + "Agents" : "Agenten", + "All" : "All", + "All Categories" : "All Kategorien", + "All Collections" : "All Sammlungen", + "All Confidence Levels" : "All Vertrauensniveauen", + "All ConfigSets" : "All ConfigSets", + "All Files" : "All Dateien", + "All Levels" : "All Niveauen", + "All Types" : "All Typen", + "All Webhooks" : "All Webhooks", + "All actions" : "All Aktiounen", + "All audit trails cleared successfully" : "All Audit-Trails erfollegräich geläscht", + "All registers" : "All Registeren", + "All schemas" : "All Schemaen", + "All searches" : "All Sichen", + "All users" : "All Benotzer", + "An error occurred while clearing audit trails" : "E Feeler ass beim Läsche vun den Audit-Trails opgetrueden", + "An error occurred while deleting the audit trail" : "E Feeler ass beim Läsche vum Audit-Trail opgetrueden", + "An error occurred while deleting the view" : "E Feeler ass beim Läsche vun der Usiicht opgetrueden", + "An error occurred while permanently deleting the objects" : "E Feeler ass beim definitive Läsche vun den Objeten opgetrueden", + "Analysis completed:" : "Analyse ofgeschloss:", + "Analysis steps:" : "Analyseschrëtt:", + "Analytics" : "Analysen", + "Analyze Objects" : "Objeten analyséieren", + "Analyze Properties" : "Eegeschaften analyséieren", + "Analyze existing properties for improvement opportunities" : "Existéierend Eegeschaften op Verbesserungsméiglechkeeten analyséieren", + "Analyzing..." : "Gëtt analyséiert...", + "Any favorites and sharing settings for this view" : "All Favoritten- a Sharing-Astellunge fir dës Usiicht", + "Any user" : "All Benotzer", + "App store cache invalidated successfully" : "App-Store-Cache erfollegräich invalidéiert", + "Application deleted successfully" : "Applikatioun erfollegräich geläscht", + "Applications" : "Applikatiounen", + "Apply Changes" : "Ännerungen uwenden", + "Archive" : "Archiv", + "Archive conversation" : "Gespréich archivéieren", + "Archived conversations are hidden from your active list" : "Archivéiert Gespréicher si vun ärer aktiver Lëscht verstoppt", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Sidd Dir sécher, datt Dir al Sich-Trails opraume wëllt? Dëst läscht Andréi déi méi al wéi 30 Deeg sinn.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Sidd Dir sécher, datt Dir déi ausgewielten Audit-Trails läsche wëllt? Dës Aktioun kann net réckgängeg gemaach ginn.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Sidd Dir sécher, datt Dir déi ausgewielte Sich-Trails läsche wëllt? Dës Aktioun kann net réckgängeg gemaach ginn.", + "Are you sure you want to delete this ConfigSet?" : "Sidd Dir sécher, datt Dir dëse ConfigSet läsche wëllt?", + "Are you sure you want to permanently delete" : "Sidd Dir sécher, datt Dir definitiv läsche wëllt", + "Ask a question..." : "Eng Fro stellen...", + "Ask questions about your data using natural language" : "Stellt Froen iwwer Är Donnéeën a natierlecher Sprooch", + "Attempt" : "Versuch", + "Audit Trail Changes" : "Audit-Trail-Ännerungen", + "Audit Trail Details" : "Audit-Trail-Detailer", + "Audit Trail Management" : "Audit-Trail-Verwaltung", + "Audit Trail Statistics" : "Audit-Trail-Statistiken", + "Audit Trails" : "Audit-Trails", + "Audit trail data copied to clipboard" : "Audit-Trail-Donnéeën an d'Zwëschenoflag kopéiert", + "Audit trail deleted successfully" : "Audit-Trail erfollegräich geläscht", + "Audit trail successfully deleted" : "Audit-Trail erfollegräich geläscht", + "Authentication" : "Authentifizéierung", + "Auto-commit disabled" : "Auto-Commit desaktivéiert", + "Auto-commit enabled" : "Auto-Commit aktivéiert", + "Auto-create Default Organisation" : "Standardorganisatioun automatesch erstellen", + "Auto-retry failed vectorizations" : "Feelgeschloen Vektoriséierungen automatesch nei probéieren", + "Automatically commit changes to SOLR index" : "Ännerungen automatesch an de SOLR-Index committen", + "Automatically create a default organisation if none exists when the app is initialized" : "Automatesch eng Standardorganisatioun erstellen wann keng existéiert wann d'App initialiséiert gëtt", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Generéiert automatesch Vektor-Embeddings aus Textstécker wann Dateien eropgelueden a verschafft ginn", + "Automatically generate vector embeddings when objects are created or updated" : "Generéiert automatesch Vektor-Embeddings wann Objeten erstallt oder aktualiséiert ginn", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Feelgeschloen Vektoriséierungsversuche automatesch nei probéieren (max. 3 Versuche)", + "Available Filters" : "Verfügbar Filteren", + "Available Workflows" : "Verfügbar Workflows", + "Avg Execution Time" : "Duerchschnëttlech Ausféierungszäit", + "Avg Members/Org" : "Duerchschnëttlech Memberen/Org", + "Avg Object Views/Session" : "Duerchschnëttlech Objet-Usiichten/Sessioun", + "Avg Response Time" : "Duerchschnëttlech Äntwertzäit", + "Avg Results/Search" : "Duerchschnëttlech Resultater/Sich", + "Avg Searches/Session" : "Duerchschnëttlech Sichen/Sessioun", + "Back" : "Zréck", + "Back to Entities" : "Zréck zu den Entitéiten", + "Back to Registers" : "Zréck zu de Registeren", + "Back to Webhooks" : "Zréck zu de Webhooks", + "Back to applications" : "Zréck zu den Applikatiounen", + "Back to entities" : "Zréck zu den Entitéiten", + "Backend updated successfully. Please reload the application." : "Backend erfollegräich aktualiséiert. Lued d'Applikatioun w.e.g. nei.", + "Base URL (Optional)" : "Basis-URL (Optional)", + "Basic Information" : "Basisinformatiounen", + "Batch Size" : "Batch-Gréisst", + "Batch extraction completed" : "Batch-Extraktioun ofgeschloss", + "Before" : "Virdrun", + "Before object vectorization can work:" : "Ier d'Objet-Vektoriséierung funktionéiere kann:", + "Behavior Issue" : "Verhalensproblem", + "Bookmarks" : "Lieszeechen", + "Blob storage has been retired. All objects now use magic tables." : "Blob-Späicher gouf ofgeschaaft. All Objeten benotzen elo Magic-Tabellen.", + "Branch" : "Branche", + "Bulk delete operation completed successfully" : "Massel-Läschoperatioun erfollegräich ofgeschloss", + "Bulk save operation completed successfully" : "Massel-Späicheroperatioun erfollegräich ofgeschloss", + "Business Data" : "Geschäftsdonnéeën", + "Cache cleared successfully" : "Cache erfollegräich geläscht", + "Calculate Sizes" : "Gréissten berechnen", + "Cancel" : "Ofbriechen", + "Cannot delete: objects are still attached" : "Kann net läschen: Objeten sinn nach ugehaang", + "Category" : "Kategorie", + "Change Type" : "Ännerungstyp", + "Changes" : "Ännerungen", + "Changes copied to clipboard" : "Ännerungen an d'Zwëschenoflag kopéiert", + "Chat Model" : "Chat-Modell", + "Chat Provider" : "Chat-Provider", + "Chat Provider (RAG)" : "Chat-Provider (RAG)", + "Chat Settings" : "Chat-Astellungen", + "Chat provider connection successful!" : "Verbindung mam Chat-Provider erfollegräich!", + "Chat settings" : "Chat-Astellungen", + "Choose a register" : "E Register auswielen", + "Choose a schema" : "E Schema auswielen", + "Choose how vector similarity calculations are performed for semantic search" : "Wielt aus, wéi d'Vektorähnlechkeetsberechnunge fir d'semantesch Sich duerchgefouert ginn", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Wielt aus, wéi eng Dateitypen am Vektoriséierungsprozess abegraff ginn. Nëmmen Dateien mat extrahéiertem Text a Stécker ginn verschafft.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Wielt aus, wéi eng Usiichten am Vektoriséierungsprozess abegraff ginn. Loosst eidel fir all Usiichten op Basis vun ärer Konfiguratioun ze verschaffen.", + "Chunk" : "Stéck", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Stéck-Läschung nach net ëmgesat. Benotzt stéckbaséiert Endpunkten.", + "Chunks" : "Stécker", + "Chunks to Vectorize:" : "Stécker fir ze vektoriséieren:", + "Cleanup Old Trails" : "Al Trails opraumen", + "Cleanup completed" : "Opraumen ofgeschloss", + "Clear All Embeddings" : "All Embeddings läschen", + "Clear Entries" : "Andréi läschen", + "Clear Filtered Audit Trails" : "Gefiltert Audit-Trails läschen", + "Clear Filters" : "Filteren läschen", + "Clear Index" : "Index läschen", + "Clear Selection" : "Auswiel läschen", + "Clear all" : "Alles läschen", + "Clear all filters" : "All Filteren läschen", + "Clear date range" : "Datumsberäich läschen", + "Clearing..." : "Gëtt geläscht...", + "Close" : "Zoumaachen", + "Collection Management" : "Sammlungsverwaltung", + "Collection Name" : "Sammlungsnumm", + "Collection assignments updated successfully" : "Sammlungszouweisunge erfollegräich aktualiséiert", + "Collection cleared successfully" : "Sammlung erfollegräich geläscht", + "Collection copied successfully" : "Sammlung erfollegräich kopéiert", + "Collection created successfully" : "Sammlung erfollegräich erstallt", + "Collection deleted successfully" : "Sammlung erfollegräich geläscht", + "Collection used to store and index file metadata and content" : "Sammlung déi benotzt gëtt fir Datei-Metadonnéeën an -Inhalt ze späicheren an ze indexéieren", + "Collection used to store and index object data" : "Sammlung déi benotzt gëtt fir Objet-Donnéeën ze späicheren an ze indexéieren", + "Collections:" : "Sammlungen:", + "Commit Message" : "Commit-Message", + "Commit Within (ms)" : "Committen bannent (ms)", + "Commit message" : "Commit-Message", + "Compare current schema with real object data" : "Aktuelle Schema mat richtegen Objet-Donnéeë vergläichen", + "Completed" : "Ofgeschloss", + "Complex" : "Komplex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Komplex Ufroen: Erweidert Siche mat méi Filteren, Operatoren a komplexe Parameterkombinatiounen", + "Confidence Level" : "Vertrauensniveau", + "Confidence Score" : "Vertrauensscore", + "Config must be provided as an object" : "Config muss als Objet uginn ginn", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSet-Verwaltung", + "ConfigSet Name" : "ConfigSet-Numm", + "ConfigSet created successfully" : "ConfigSet erfollegräich erstallt", + "ConfigSet deleted successfully" : "ConfigSet erfollegräich geläscht", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets definéieren de Schema an d'Konfiguratioun fir Är SOLR-Sammlungen. Si enthalen Felddefinitiounen, Analyzeren an aner Sichastellungen.", + "Configuration" : "Konfiguratioun", + "Configuration Data" : "Konfiguratiounsdonnéeën", + "Configuration saved" : "Konfiguratioun gespäichert", + "Configurations" : "Konfiguratiounen", + "Configure Facets" : "Facetten konfiguréieren", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfiguréiert Large-Language-Model- (LLM-) Provider fir KI-gestäipt Funktiounen, dorënner semantesch Sich, Embeddings a Chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfiguréiert grondleeënd Verbindungsastellunge fir Äre SOLR-Server, dorënner Authentifizéierung an Netzoptiounen. Benotzt déi separat ConfigSet- a Sammlungsverwaltungsdialoge fir Cores a Sammlungen ze verwalten.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfiguréiert, wéi Datebank-Objeten an Vektor-Embeddings fir d'semantesch Sich ëmgewandelt ginn. Objeten ginn direkt vektoriséiert ouni datt eng Textextraktioun néideg ass.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfiguréiert, wéi Objeten viru der Vektoriséierung an Text ëmgewandelt ginn. Dës Astellunge beaflossen d'Sichqualitéit an de Kontext.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfiguréiert Parameter fir d'Datei-Vektoriséierung. Dëse Prozess generéiert Vektor-Embeddings fir all extrahéiert Dateistécker.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfiguréiert Parameter fir d'Objet-Vektoriséierung. Dëse Prozess generéiert Vektor-Embeddings fir all Objeten déi ären Usiicht-Filteren entspriechen.", + "Configure which types of data to search and how many sources to retrieve" : "Konfiguréiert wéi eng Zorte vun Donnéeën duerchsicht ginn a wéi vill Quellen ofgeruff ginn", + "Connection Failed" : "Verbindung feelgeschloen", + "Connection Settings" : "Verbindungsastellungen", + "Connection Successful!" : "Verbindung erfollegräich!", + "Connection failed" : "Verbindung feelgeschloen", + "Connection protocol" : "Verbindungsprotokoll", + "Connection successful" : "Verbindung erfollegräich", + "Connection timeout in seconds" : "Verbindungs-Timeout a Sekonnen", + "Constant delay between retries (5 minutes)" : "Konstant Verzögerung tëscht den Neiversuche (5 Minutten)", + "Constraint Issue" : "Restriktiounsproblem", + "Content Filters" : "Inhaltsfilteren", + "Continue to Properties" : "Weider zu den Eegeschaften", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontrolléiert wéi eng Objet-Usiichte solle vektoriséiert ginn, fir d'API-Käschten ze reduzéieren. Vektoriséiert nëmmen Usiichten déi vun der semantescher Sich profitéieren.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontrolléiert wéi eng Usiichten an Tools d'KI an dësem Gespréich benotze kann. Standardméisseg sinn all Agent-Fäegkeeten aktivéiert.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Kontrolléiert d'Zoufällegkeet (0 = deterministesch, 2 = ganz kreativ)", + "Conversation ID is required" : "Gespréichs-ID ass erfuerderlech", + "Conversation archived" : "Gespréich archivéiert", + "Conversation archived successfully" : "Gespréich erfollegräich archivéiert", + "Conversation cleared successfully" : "Gespréich erfollegräich geläscht", + "Conversation deleted" : "Gespréich geläscht", + "Conversation permanently deleted" : "Gespréich definitiv geläscht", + "Conversation renamed" : "Gespréich ëmbenannt", + "Conversation restored" : "Gespréich restauréiert", + "Conversation title" : "Gespréichstitel", + "Copied!" : "Kopéiert!", + "Copy" : "Kopéieren", + "Copy Changes" : "Ännerunge kopéieren", + "Copy Collection" : "Sammlung kopéieren", + "Copy Data" : "Donnéeë kopéieren", + "Copy Full Data" : "Vollstänneg Donnéeë kopéieren", + "Copying..." : "Gëtt kopéiert...", + "Count" : "Unzuel", + "Counting objects..." : "Objeten ginn gezielt...", + "Costs" : "Käschten", + "Create" : "Erstellen", + "Create Collection" : "Sammlung erstellen", + "Create ConfigSet" : "ConfigSet erstellen", + "Create New Collection" : "Nei Sammlung erstellen", + "Create New ConfigSet" : "Neie ConfigSet erstellen", + "Create Operations" : "Erstellungsoperatiounen", + "Create Webhook" : "Webhook erstellen", + "Create a copy of collection:" : "Eng Kopie vun der Sammlung erstellen:", + "Create a new ConfigSet based on the _default template" : "Engen neie ConfigSet op Basis vum _default-Template erstellen", + "Create a new SOLR collection from an existing ConfigSet" : "Eng nei SOLR-Sammlung aus engem existéierende ConfigSet erstellen", + "Create your first AI agent to get started." : "Erstellt Äre éischte KI-Agent fir unzefänken.", + "Created" : "Erstallt", + "Created:" : "Erstallt:", + "Creating..." : "Gëtt erstallt...", + "Current Filters" : "Aktuell Filteren", + "Current Type:" : "Aktuellen Typ:", + "Custom API endpoint if using a different region" : "Personaliséierten API-Endpunkt wann eng aner Regioun benotzt gëtt", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Personaliséiert HTTP-Headeren (een pro Linn, Format: Header-Numm: Wäert)", + "Daily" : "Deeglech", + "Dashboard" : "Dashboard", + "Data Source" : "Datequell", + "Data sources" : "Datequellen", + "Data type does not match observed values" : "Datentyp entsprécht net de beobachtete Wäerter", + "Database URL" : "Datebank-URL", + "Database information refreshed" : "Datebankinformatiounen aktualiséiert", + "Date Range" : "Datumsberäich", + "Default" : "Standard", + "Default Organisation" : "Standardorganisatioun", + "Default: 5" : "Standard: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "D'Verzögerunge verduebelen sech mat all Versuch (2, 4, 8 Minutten...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "D'Verzögerunge klamme linear (5, 10, 15 Minutten...)", + "Delete" : "Läschen", + "Delete Application" : "Applikatioun läschen", + "Delete Audit Trail" : "Audit-Trail läschen", + "Delete Collection" : "Sammlung läschen", + "Delete ConfigSet" : "ConfigSet läschen", + "Delete Objects" : "Objeten läschen", + "Delete Operations" : "Läschoperatiounen", + "Delete View" : "Usiicht läschen", + "Delete all objects in this schema" : "All Objeten an dësem Schema läschen", + "Delete permanently" : "Definitiv läschen", + "Delete this conversation?" : "Dëst Gespréich läschen?", + "Delete view" : "Usiicht läschen", + "Deleted" : "Geläscht", + "Deleted By" : "Geläscht vun", + "Deleted Date" : "Läschdatum", + "Deleted Items Management" : "Verwaltung vu geläschten Artikelen", + "Deleted This Week" : "Dës Woch geläscht", + "Deleted Today" : "Haut geläscht", + "Deleted:" : "Geläscht:", + "Deleting..." : "Gëtt geläscht...", + "Deletion Date Range" : "Läschdatumsberäich", + "Deletion Statistics" : "Läschstatistiken", + "Deprecated" : "Veroudert", + "Description" : "Beschreiwung", + "Description:" : "Beschreiwung:", + "Detect data types and patterns" : "Datentypen a Musteren erkennen", + "Detected At" : "Erkannt um", + "Detected Format:" : "Erkannte Format:", + "Detected Issues:" : "Erkannte Probleemer:", + "Disable" : "Desaktivéieren", + "Disabled" : "Desaktivéiert", + "Discover Files" : "Dateien entdecken", + "Discovered Properties" : "Entdeckt Eegeschaften", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Wëllt Dir all gefiltert Audit-Trail-Andréi definitiv läschen? Dës Aktioun kann net réckgängeg gemaach ginn.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Wëllt Dir dësen Audit-Trail-Andrag definitiv läschen? Dës Aktioun kann net réckgängeg gemaach ginn.", + "Documents" : "Dokumenter", + "Download API Spec" : "API-Spezifikatioun eroflueden", + "Edit" : "Beaarbechten", + "Edit Register" : "Register beaarbechten", + "Edit Schema" : "Schema beaarbechten", + "Edit View" : "Usiicht beaarbechten", + "Edit Webhook" : "Webhook beaarbechten", + "Edit view" : "Usiicht beaarbechten", + "Edit view details" : "Usiicht-Detailer beaarbechten", + "Effectiveness" : "Effektivitéit", + "Email" : "E-Mail", + "Emails" : "E-Mailen", + "Embedding Model" : "Embedding-Modell", + "Embedding Provider" : "Embedding-Provider", + "Embedding generated successfully" : "Embedding erfollegräich generéiert", + "Embedding provider connection successful!" : "Verbindung mam Embedding-Provider erfollegräich!", + "Enable" : "Aktivéieren", + "Enable auto-creation" : "Automatesch Erstellung aktivéieren", + "Enable automatic file vectorization" : "Automatesch Datei-Vektoriséierung aktivéieren", + "Enable automatic object vectorization" : "Automatesch Objet-Vektoriséierung aktivéieren", + "Enable automatic synchronization" : "Automatesch Synchroniséierung aktivéieren", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Detailléiert Protokolléierung fir SOLR-Operatiounen aktivéieren (recommandéiert fir d'Debugging)", + "Enable faceting" : "Facettéierung aktivéieren", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "LLM-Funktiounen aktivéieren oder desaktivéieren. Konfiguréiert Provider a Modeller mam LLM-Konfiguratiounsknäppchen hei uewen.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "SOLR-Sichintegratioun aktivéieren oder desaktivéieren. Konfiguréiert d'Verbindungsastellunge mam Verbindungsastellungs-Knäppchen hei uewen.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "n8n-Workflow-Integratioun aktivéieren oder desaktivéieren. Konfiguréiert d'Verbindungsastellungen hei drënner.", + "Enable or disable this webhook" : "Dëse Webhook aktivéieren oder desaktivéieren", + "Enable vectorization for all existing and future views (may increase costs)" : "Vektoriséierung fir all existéierend an zukënfteg Usiichten aktivéieren (kann d'Käschten erhéijen)", + "Enabled" : "Aktivéiert", + "Endpoints" : "Endpunkten", + "Enter ConfigSet name" : "ConfigSet-Numm aginn", + "Enter collection name" : "Sammlungsnumm aginn", + "Enter description (optional)..." : "Beschreiwung aginn (optional)...", + "Enter new collection name" : "Neie Sammlungsnumm aginn", + "Enter object ID" : "Objet-ID aginn", + "Enter search term" : "Sichbegrëff aginn", + "Enter view name..." : "Usiicht-Numm aginn...", + "Enter webhook name" : "Webhook-Numm aginn", + "Entities" : "Entitéiten", + "Entity Information" : "Entitéitsinformatiounen", + "Entity deleted successfully" : "Entitéit erfollegräich geläscht", + "Entity not found" : "Entitéit net fonnt", + "Entries to be deleted:" : "Andréi déi geläscht ginn:", + "Enum Issue" : "Enum-Problem", + "Enum constraint is missing" : "Enum-Restriktioun feelt", + "Error" : "Feeler", + "Error Details" : "Feeler-Detailer", + "Error Information" : "Feeler-Informatiounen", + "Error Message" : "Feeler-Message", + "Error loading application" : "Feeler beim Lueden vun der Applikatioun", + "Error loading audit trails" : "Feeler beim Lueden vun den Audit-Trails", + "Error loading entity" : "Feeler beim Lueden vun der Entitéit", + "Error loading search trails" : "Feeler beim Lueden vun de Sich-Trails", + "Estimated Batches:" : "Geschätzte Batchen:", + "Estimated Cost:" : "Geschätzte Käschten:", + "Estimated Duration:" : "Geschätzten Dauer:", + "Event" : "Evenement", + "Event Property for Payload" : "Evenement-Eegeschaft fir Payload", + "Events" : "Evenementer", + "Example Value" : "Beispillwäert", + "Exclusive Maximum" : "Exklusiivt Maximum", + "Exclusive Minimum" : "Exklusiivt Minimum", + "Execution Mode" : "Ausféierungsmodus", + "Execution Time" : "Ausféierungszäit", + "Execution Time Range" : "Ausféierungszäitberäich", + "Existing Improvements" : "Existéierend Verbesserungen", + "Exponential" : "Exponentiell", + "Export" : "Exportéieren", + "Export completed successfully" : "Export erfollegräich ofgeschloss", + "Export, view, or delete audit trails" : "Audit-Trails exportéieren, ukucken oder läschen", + "Extend Schema" : "Schema erweideren", + "Extended" : "Erweidert", + "Extensions" : "Erweiderungen", + "Extract Pending Files" : "Ausstoend Dateien extrahéieren", + "Extract properties from each object" : "Eegeschaften aus all Objet extrahéieren", + "Extracted At" : "Extrahéiert um", + "Extraction Status" : "Extraktiounsstatus", + "Facet configuration updated successfully" : "Facettekonfiguratioun erfollegräich aktualiséiert", + "Facets discovered and configured successfully" : "Facetten erfollegräich entdeckt a konfiguréiert", + "Facets discovered successfully" : "Facetten erfollegräich entdeckt", + "Failed" : "Feelgeschloen", + "Failed to analyze schema properties" : "Analyse vun de Schema-Eegeschafte feelgeschloen", + "Failed to archive conversation" : "Archivéiere vum Gespréich feelgeschloen", + "Failed to calculate sizes" : "Berechnung vun de Gréisste feelgeschloen", + "Failed to clear collection" : "Läsche vun der Sammlung feelgeschloen", + "Failed to copy changes" : "Kopéiere vun den Ännerunge feelgeschloen", + "Failed to copy data" : "Kopéiere vun den Donnéeë feelgeschloen", + "Failed to copy data to clipboard" : "Kopéiere vun den Donnéeën an d'Zwëschenoflag feelgeschloen", + "Failed to create conversation" : "Erstelle vum Gespréich feelgeschloen", + "Failed to create or find project" : "Erstellen oder Fanne vum Projet feelgeschloen", + "Failed to delete collection" : "Läsche vun der Sammlung feelgeschloen", + "Failed to delete conversation" : "Läsche vum Gespréich feelgeschloen", + "Failed to delete webhook" : "Läsche vum Webhook feelgeschloen", + "Failed to download API specification" : "Eroflueden vun der API-Spezifikatioun feelgeschloen", + "Failed to get SOLR field configuration" : "Ofruff vun der SOLR-Feldkonfiguratioun feelgeschloen", + "Failed to get entity categories" : "Ofruff vun den Entitéitskategorien feelgeschloen", + "Failed to get entity statistics" : "Ofruff vun den Entitéitsstatistike feelgeschloen", + "Failed to get entity types" : "Ofruff vun den Entitéitstypen feelgeschloen", + "Failed to load LLM configuration" : "Lueden vun der LLM-Konfiguratioun feelgeschloen", + "Failed to load Nextcloud groups" : "Lueden vun den Nextcloud-Gruppen feelgeschloen", + "Failed to load conversation" : "Lueden vum Gespréich feelgeschloen", + "Failed to load entities" : "Lueden vun den Entitéiten feelgeschloen", + "Failed to load entity" : "Lueden vun der Entitéit feelgeschloen", + "Failed to load files" : "Lueden vun den Dateien feelgeschloen", + "Failed to load organisations" : "Lueden vun den Organisatioune feelgeschloen", + "Failed to load templates" : "Lueden vun den Templater feelgeschloen", + "Failed to load webhooks" : "Lueden vun de Webhooks feelgeschloen", + "Failed to load workflows" : "Lueden vun de Workflows feelgeschloen", + "Failed to refresh database information" : "Aktualiséiere vun den Datebankinformatioune feelgeschloen", + "Failed to reindex collection" : "Nei-Indexéiere vun der Sammlung feelgeschloen", + "Failed to rename conversation" : "Ëmbenenne vum Gespréich feelgeschloen", + "Failed to restore conversation" : "Restauréiere vum Gespréich feelgeschloen", + "Failed to retry extraction" : "Neie Versuch vun der Extraktioun feelgeschloen", + "Failed to retry webhook" : "Neie Versuch vum Webhook feelgeschloen", + "Failed to save GitHub token" : "Späichere vum GitHub-Token feelgeschloen", + "Failed to save GitLab URL" : "Späichere vun der GitLab-URL feelgeschloen", + "Failed to save GitLab token" : "Späichere vum GitLab-Token feelgeschloen", + "Failed to save additional feedback" : "Späichere vum zousätzleche Feedback feelgeschloen", + "Failed to save n8n configuration" : "Späichere vun der n8n-Konfiguratioun feelgeschloen", + "Failed to save roles" : "Späichere vun de Rolle feelgeschloen", + "Failed to save settings" : "Späichere vun den Astellunge feelgeschloen", + "Failed to save webhook" : "Späichere vum Webhook feelgeschloen", + "Failed to send feedback" : "Schécke vum Feedback feelgeschloen", + "Failed to test webhook" : "Testen vum Webhook feelgeschloen", + "Failed to update favorite status" : "Aktualiséiere vum Favoritte-Status feelgeschloen", + "Failed to update schema properties" : "Aktualiséiere vun de Schema-Eegeschafte feelgeschloen", + "Failed to update view" : "Aktualiséiere vun der Usiicht feelgeschloen", + "Failed to update webhook" : "Aktualiséiere vum Webhook feelgeschloen", + "Feedback recorded" : "Feedback registréiert", + "Fewer sources (1-3):" : "Manner Quellen (1-3):", + "Field" : "Feld", + "File" : "Datei", + "File Chunk Prediction" : "Datei-Stéck-Prognos", + "File Collection" : "Datei-Sammlung", + "File Management" : "Datei-Verwaltung", + "File Name" : "Dateinumm", + "File Path" : "Dateipad", + "File Type Selection" : "Dateityp-Auswiel", + "File Vectorization" : "Datei-Vektoriséierung", + "File Warmup" : "Datei-Warmup", + "File actions menu" : "Datei-Aktiounsmenü", + "File anonymized successfully" : "Datei erfollegräich anonymiséiert", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "D'Datei-Stéck-Vektoriséierung ass nach net ëmgesat. D'Stécker si prett a gespäichert, awer de Vektoriséierungsservice ass an der Entwécklung.", + "File collection not configured" : "Datei-Sammlung net konfiguréiert", + "File discovery completed" : "Datei-Entdeckung ofgeschloss", + "File extraction completed" : "Datei-Extraktioun ofgeschloss", + "File extraction queued" : "Datei-Extraktioun an d'Schlaang gesat", + "File indexed successfully" : "Datei erfollegräich indexéiert", + "File is already anonymized" : "Datei ass scho anonymiséiert", + "File not found" : "Datei net fonnt", + "File settings updated successfully" : "Datei-Astellunge erfollegräich aktualiséiert", + "File sources" : "Datei-Quellen", + "File vectorization configuration saved successfully" : "Datei-Vektoriséierungskonfiguratioun erfollegräich gespäichert", + "File vectorization started. Check the statistics section for progress." : "Datei-Vektoriséierung gestart. Iwwerpréift den Statistik-Beräich fir de Fortschrëtt.", + "File warmup completed" : "Datei-Warmup ofgeschloss", + "Files" : "Dateien", + "Files with Completed Extraction:" : "Dateie mat ofgeschlossener Extraktioun:", + "Files → fileCollection, Objects → objectCollection" : "Dateien → fileCollection, Objeten → objectCollection", + "Filter Audit Trails" : "Audit-Trails filteren", + "Filter Deleted Items" : "Geläscht Artikele filteren", + "Filter Objects" : "Objeten filteren", + "Filter Properties" : "Eegeschafte filteren", + "Filter Search Trails" : "Sich-Trails filteren", + "Filter Statistics" : "Statistike filteren", + "Filter and analyze search trail entries" : "Sich-Trail-Andréi filteren an analyséieren", + "Filter and manage audit trail entries" : "Audit-Trail-Andréi filteren a verwalten", + "Filter and manage soft deleted items" : "Mëll geläscht Artikele filteren a verwalten", + "Filter by object ID" : "No Objet-ID filteren", + "Filter by search term" : "No Sichbegrëff filteren", + "Filter by webhook" : "No Webhook filteren", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filterdonnéeën automatesch gelueden. Benotzt d'Filteren hei drënner fir Är Sich ze verfeineren.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Webhook-Trigger no Payload-Eegeschafte filteren (een pro Linn, Format: Schlëssel: Wäert)", + "Filtered" : "Gefiltert", + "Filters" : "Filteren", + "Fireworks AI Chat Settings" : "Fireworks-AI-Chat-Astellungen", + "Fireworks AI Embedding Configuration" : "Fireworks-AI-Embedding-Konfiguratioun", + "First" : "Éischt", + "Fixed" : "Behuewen", + "For chat and retrieval-augmented generation" : "Fir Chat a Retrieval-Augmented Generation", + "For vector embeddings and semantic search" : "Fir Vektor-Embeddings a semantesch Sich", + "Format" : "Format", + "Format Issue" : "Formatproblem", + "Format constraint is missing" : "Format-Restriktioun feelt", + "From Date" : "Vum Datum", + "From date" : "Vum Datum", + "Full data copied to clipboard" : "Vollstänneg Donnéeën an d'Zwëschenoflag kopéiert", + "General Issue" : "Allgemenge Problem", + "Generate recommendations and confidence scores" : "Recommandatiounen a Vertrauensscoren generéieren", + "Generate vectors immediately when new objects are created" : "Generéiert direkt Vektoren wann nei Objeten erstallt ginn", + "GitHub token is valid" : "GitHub-Token ass gëlteg", + "GitHub token removed successfully" : "GitHub-Token erfollegräich ewechgeholl", + "GitHub token saved successfully" : "GitHub-Token erfollegräich gespäichert", + "GitLab URL saved successfully" : "GitLab-URL erfollegräich gespäichert", + "GitLab token is valid" : "GitLab-Token ass gëlteg", + "GitLab token saved successfully" : "GitLab-Token erfollegräich gespäichert", + "HTTP Method" : "HTTP-Method", + "HTTP method used to send webhook requests" : "HTTP-Method déi benotzt gëtt fir Webhook-Ufroen ze schécken", + "Headers" : "Headeren", + "Health" : "Gesondheet", + "Heartbeat successful - connection kept alive" : "Heartbeat erfollegräich - Verbindung um Liewen gehalen", + "Helpful" : "Hëllefräich", + "Hide Filters" : "Filteren verstoppen", + "Hide in forms" : "An de Formularen verstoppen", + "Hide in list view" : "An der Lëschtenusiicht verstoppen", + "High" : "Héich", + "High Confidence" : "Héicht Vertrauen", + "Host" : "Host", + "Hourly" : "Stënnlech", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Wéi déif verschachtelt Objet-Eegeschaften duerchlaf ginn (1-20). Méi héich Wäerter erfaassen méi Detail, awer erhéijen d'Vektorgréisst.", + "How often to check for updates (1-168 hours)" : "Wéi dacks no Aktualiséierungen iwwerpréift gëtt (1-168 Stonnen)", + "How to handle retries for failed webhook deliveries" : "Wéi mat den Neiversuche fir feelgeschloen Webhook-Liwwerunge fonctionéiert gëtt", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "IP-Rate-Limiten erfollegräich geläscht", + "Identify properties not in the schema" : "Eegeschaften déi net am Schema sinn identifizéieren", + "Immutable" : "Onverännerbar", + "Import" : "Importéieren", + "Import successful" : "Import erfollegräich", + "Improved Property" : "Verbessert Eegeschaft", + "In use" : "A Gebrauch", + "Inactive" : "Inaktiv", + "Include IDs and names of related objects for better contextual search" : "IDen an Nimm vu verbonnenen Objeten abezéie fir eng besser kontextuell Sich", + "Include related object references" : "Verbonne Objet-Referenzen abezéien", + "Include schema and register metadata" : "Schema- a Register-Metadonnéeën abezéien", + "Initialization failed" : "Initialiséierung feelgeschloen", + "Initialize Project" : "Projet initialiséieren", + "Inspect Fields" : "Felder inspizéieren", + "Inspect Index" : "Index inspizéieren", + "Invalid" : "Ongëlteg", + "Invalid batch size. Must be between 1 and 5000" : "Ongëlteg Batch-Gréisst. Muss tëscht 1 a 5000 sinn", + "Invalid field name provided" : "Ongëltege Feldnumm uginn", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Ongëlteg maxObjects. Muss 0 (all) oder eng positiv Zuel sinn", + "Invalid:" : "Ongëlteg:", + "Issue" : "Problem", + "Items per page:" : "Artikele pro Säit:", + "Just now" : "Eben elo", + "LLM Configuration" : "LLM-Konfiguratioun", + "LLM actions menu" : "LLM-Aktiounsmenü", + "LLM configuration saved successfully" : "LLM-Konfiguratioun erfollegräich gespäichert", + "LLM features disabled" : "LLM-Funktiounen desaktivéiert", + "LLM features enabled" : "LLM-Funktiounen aktivéiert", + "LLM must be enabled with an embedding provider configured" : "LLM muss aktivéiert sinn mat engem konfiguréierten Embedding-Provider", + "LLM settings updated successfully" : "LLM-Astellunge erfollegräich aktualiséiert", + "Last" : "Lescht", + "Last Triggered" : "Lescht ausgeléist", + "Length Range:" : "Längtberäich:", + "Linear" : "Linear", + "Load Advanced Filters" : "Erweidert Filtere lueden", + "Load advanced filters with live data from your search index" : "Erweidert Filtere mat Live-Donnéeën aus ärem Sichindex lueden", + "Load view" : "Usiicht lueden", + "Loading ConfigSets..." : "ConfigSets gi gelueden...", + "Loading advanced filters..." : "Erweidert Filtere gi gelueden...", + "Loading agents..." : "Agente gi gelueden...", + "Loading application details..." : "Applikatiouns-Detailer gi gelueden...", + "Loading applications..." : "Applikatioune gi gelueden...", + "Loading archived conversations..." : "Archivéiert Gespréicher gi gelueden...", + "Loading audit trails..." : "Audit-Trails gi gelueden...", + "Loading collections..." : "Sammlunge gi gelueden...", + "Loading configurations..." : "Konfiguratioune gi gelueden...", + "Loading conversation..." : "Gespréich gëtt gelueden...", + "Loading conversations..." : "Gespréicher gi gelueden...", + "Loading deleted items..." : "Geläscht Artikele gi gelueden...", + "Loading events..." : "Evenementer gi gelueden...", + "Loading filter data..." : "Filterdonnéeë gi gelueden...", + "Loading groups..." : "Gruppe gi gelueden...", + "Loading log details..." : "Protokoll-Detailer gi gelueden...", + "Loading registers..." : "Registere gi gelueden...", + "Loading schemas..." : "Schemaen gi gelueden...", + "Loading search trails..." : "Sich-Trails gi gelueden...", + "Loading sources..." : "Quellen gi gelueden...", + "Loading statistics..." : "Statistike gi gelueden...", + "Loading users..." : "Benotzer gi gelueden...", + "Loading views..." : "Usiichte gi gelueden...", + "Loading..." : "Gëtt gelueden...", + "Local" : "Lokal", + "Local Version" : "Lokal Versioun", + "Locked" : "Gespaart", + "Locked:" : "Gespaart:", + "Login successful" : "Umeldung erfollegräich", + "Logs" : "Protokoller", + "Low" : "Niddreg", + "Low Confidence" : "Niddregt Vertrauen", + "Lucene Version:" : "Lucene-Versioun:", + "Magic table synchronized successfully" : "Magic-Tabell erfollegräich synchroniséiert", + "Manage Organisation Roles" : "Organisatiounsrolle verwalten", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Verwalt SOLR-Sammlungen (Datespäicher) an deelt se fir Objeten an Dateien zou.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Verwalt SOLR-ConfigSets (Konfiguratiounstemplater) fir Är Sammlungen.", + "Manage and configure agents for automated tasks" : "Agente fir automatiséiert Aufgabe verwalten a konfiguréieren", + "Manage and monitor file text extraction status" : "Datei-Textextraktiounsstatus verwalten an iwwerwaachen", + "Manage and restore soft deleted items from your registers" : "Mëll geläscht Artikelen aus äre Registere verwalten a restauréieren", + "Manage and view detected entities from files and objects" : "Erkannt Entitéiten aus Dateien an Objete verwalten an ukucken", + "Manage document templates and themes" : "Dokument-Templater an Themen verwalten", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Verwalt n8n-Workflows fir d'OpenRegister-Automatiséierung. D'Workflows ginn am konfiguréierte Projet gespäichert.", + "Manage webhooks for event-driven integrations" : "Webhooks fir evenementgesteiert Integratioune verwalten", + "Manage your applications and modules" : "Verwalt Är Applikatiounen a Moduler", + "Manage your data registers and their configurations" : "Verwalt Är Dateregistere an hir Konfiguratiounen", + "Manage your data schemas and their properties" : "Verwalt Är Dateschemaen an hir Eegeschaften", + "Manage your data sources and their configurations" : "Verwalt Är Datequellen an hir Konfiguratiounen", + "Manage your saved search configurations" : "Verwalt Är gespäichert Sichkonfiguratiounen", + "Manage your system configurations and settings" : "Verwalt Är Systemkonfiguratiounen an -astellungen", + "Managed" : "Verwalt", + "Max Files (0 = all)" : "Max. Dateien (0 = all)", + "Max Length" : "Max. Längt", + "Max Length:" : "Max. Längt:", + "Max Objects (0 = all)" : "Max. Objeten (0 = all)", + "Max Retries" : "Max. Neiversuche", + "Max Shards/Node" : "Max. Shards/Node", + "Max execution time (ms)" : "Max. Ausféierungszäit (ms)", + "Max ms" : "Max. ms", + "Max result count" : "Max. Resultatzuel", + "Max results" : "Max. Resultater", + "Maximum" : "Maximum", + "Maximum Nesting Depth" : "Maximal Verschachtelungsdéift", + "Maximum length constraint is missing" : "Maximal Längt-Restriktioun feelt", + "Maximum length is too restrictive" : "Maximal Längt ass ze restriktiv", + "Maximum number of files to process. Set to 0 to process all files." : "Maximal Unzuel u Dateien déi verschafft ginn. Setzt op 0 fir all Dateien ze verschaffen.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maximal Unzuel un Objeten déi verschafft ginn. Setzt op 0 fir all Objeten ze verschaffen.", + "Maximum number of retry attempts for failed deliveries" : "Maximal Unzuel u Neiversuche fir feelgeschloen Liwwerungen", + "Maximum time to wait before committing changes" : "Maximal Zäit déi gewaart gëtt ier Ännerunge committet ginn", + "Maximum tokens to generate" : "Maximal Tokens déi generéiert ginn", + "Maximum value constraint is missing" : "Maximalwäert-Restriktioun feelt", + "Maximum value is too restrictive" : "Maximalwäert ass ze restriktiv", + "Medium" : "Mëttel", + "Medium Confidence" : "Mëttelt Vertrauen", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Mëttel Ufroen: Siche mat e bësse Filterung oder méi Parameter (z. B. Datumsberäicher, spezifesch Registeren/Schemaen)", + "Members of selected groups can access this view" : "Memberen vun den ausgewielte Gruppen hunn Zougang zu dëser Usiicht", + "Members:" : "Memberen:", + "Memory prediction calculated successfully" : "Späicherprognos erfollegräich berechent", + "Message content is required" : "Message-Inhalt ass erfuerderlech", + "Message does not belong to this conversation" : "Message gehéiert net zu dësem Gespréich", + "Metadata Filters" : "Metadonnéeën-Filteren", + "Method" : "Method", + "Min Length" : "Min. Längt", + "Min execution time (ms)" : "Min. Ausféierungszäit (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. Resultatzuel", + "Min results" : "Min. Resultater", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Minimalwäert-Restriktioun feelt", + "Minimum value is too restrictive" : "Minimalwäert ass ze restriktiv", + "Missing object fields created successfully" : "Feelend Objet-Felder erfollegräich erstallt", + "Mode:" : "Modus:", + "Model" : "Modell", + "Model Name" : "Modellnumm", + "Modified" : "Geännert", + "Monthly" : "Méintlech", + "More sources (10-20):" : "Méi Quellen (10-20):", + "Most Active Objects" : "Aktiivst Objeten", + "Multiple Of" : "Vilfacht vun", + "Name" : "Numm", + "Names cache warmup completed" : "Nimm-Cache-Warmup ofgeschloss", + "New Collection Name" : "Neie Sammlungsnumm", + "New Conversation" : "Neit Gespréich", + "New Properties" : "Nei Eegeschaften", + "New Property" : "Nei Eegeschaft", + "New Value" : "Neie Wäert", + "New users without specific organisation membership will be automatically added to this organisation" : "Nei Benotzer ouni spezifesch Organisatiounsmemberschaft ginn automatesch zu dëser Organisatioun derbäigesat", + "Next" : "Weider", + "No" : "Nee", + "No ConfigSets found" : "Keng ConfigSets fonnt", + "No GitHub token configured" : "Kee GitHub-Token konfiguréiert", + "No GitHub token provided" : "Kee GitHub-Token uginn", + "No GitLab token provided" : "Kee GitLab-Token uginn", + "No Organisation" : "Keng Organisatioun", + "No active filters" : "Keng aktiv Filteren", + "No activity data available" : "Keng Aktivitéitsdonnéeë verfügbar", + "No agents available" : "Keng Agente verfügbar", + "No agents found" : "Keng Agente fonnt", + "No app store cache exists yet" : "Et existéiert nach kee App-Store-Cache", + "No applications are available." : "Keng Applikatioune verfügbar.", + "No applications found" : "Keng Applikatioune fonnt", + "No archived conversations" : "Keng archivéiert Gespréicher", + "No audit trail entries found" : "Keng Audit-Trail-Andréi fonnt", + "No changes recorded" : "Keng Ännerunge registréiert", + "No chunks to vectorize" : "Keng Stécker fir ze vektoriséieren", + "No collections found" : "Keng Sammlunge fonnt", + "No configuration data" : "Keng Konfiguratiounsdonnéeën", + "No configurations are available." : "Keng Konfiguratioune verfügbar.", + "No configurations found" : "Keng Konfiguratioune fonnt", + "No configurations found for this application." : "Keng Konfiguratioune fir dës Applikatioun fonnt.", + "No conversations yet. Create a new one to get started!" : "Nach keng Gespréicher. Erstellt en neit fir unzefänken!", + "No data available" : "Keng Donnéeë verfügbar", + "No data available for chart" : "Keng Donnéeë fir d'Diagramm verfügbar", + "No deleted items found" : "Keng geläscht Artikele fonnt", + "No deletion data available" : "Keng Läschdonnéeë verfügbar", + "No description found" : "Keng Beschreiwung fonnt", + "No description provided" : "Keng Beschreiwung uginn", + "No entities detected in this file. Run text extraction first." : "Keng Entitéiten an dëser Datei erkannt. Féiert fir d'éischt d'Textextraktioun aus.", + "No entities found" : "Keng Entitéite fonnt", + "No entities have been detected yet" : "Et goufe nach keng Entitéite erkannt", + "No events found" : "Keng Evenementer fonnt", + "No expired audit trails found to clear" : "Keng ofgelaf Audit-Trails fonnt fir ze läschen", + "No facetable fields available. Select a register and schema to see available filters." : "Keng facettéierbar Felder verfügbar. Wielt e Register an e Schema fir verfügbar Filteren ze gesinn.", + "No files found" : "Keng Dateie fonnt", + "No files have been extracted yet" : "Et goufen nach keng Dateien extrahéiert", + "No files to process" : "Keng Dateie fir ze verschaffen", + "No files to reindex" : "Keng Dateie fir nei ze indexéieren", + "No filters are currently active. This will delete ALL audit trail entries!" : "Aktuell sinn keng Filteren aktiv. Dëst läscht ALL Audit-Trail-Andréi!", + "No log entries found" : "Keng Protokoll-Andréi fonnt", + "No logs are available for this configuration." : "Keng Protokoller fir dës Konfiguratioun verfügbar.", + "No logs are available for this source." : "Keng Protokoller fir dës Quell verfügbar.", + "No logs found" : "Keng Protokoller fonnt", + "No mismatched fields found - SOLR schema is properly configured" : "Keng net iwwereneestëmmend Felder fonnt - de SOLR-Schema ass richteg konfiguréiert", + "No objects found" : "Keng Objete fonnt", + "No objects found to vectorize" : "Keng Objete fir ze vektoriséiere fonnt", + "No objects selected for deletion" : "Keng Objete fir d'Läschung ausgewielt", + "No objects to delete" : "Keng Objete fir ze läschen", + "No objects were permanently deleted" : "Et goufe keng Objete definitiv geläscht", + "No properties found" : "Keng Eegeschafte fonnt", + "No properties match your filters." : "Keng Eegeschafte entspriechen äre Filteren.", + "No purge date set" : "Kee Purge-Datum gesat", + "No register data available" : "Keng Registerdonnéeë verfügbar", + "No registers found" : "Keng Registere fonnt", + "No registers found for this application." : "Keng Registere fir dës Applikatioun fonnt.", + "No relations found" : "Keng Relatioune fonnt", + "No request body available" : "Kee Ufro-Kierper verfügbar", + "No response body available" : "Keen Äntwert-Kierper verfügbar", + "No saved views yet. Create one in the Search tab!" : "Nach keng gespäichert Usiichten. Erstellt eng am Sich-Tab!", + "No schema data available" : "Keng Schemadonnéeë verfügbar", + "No schema selected for exploration" : "Kee Schema fir d'Exploratioun ausgewielt", + "No schemas found" : "Keng Schemae fonnt", + "No schemas found for this application." : "Keng Schemae fir dës Applikatioun fonnt.", + "No search terms data available" : "Keng Sichbegrëffer-Donnéeë verfügbar", + "No search trail entries found" : "Keng Sich-Trail-Andréi fonnt", + "No sources are available." : "Keng Quelle verfügbar.", + "No sources found" : "Keng Quelle fonnt", + "No templates found" : "Keng Templater fonnt", + "No templates have been created yet" : "Et goufen nach keng Templater erstallt", + "No valid magic-mapped register+schema combinations found" : "Keng gëlteg magic-mapped Register+Schema-Kombinatioune fonnt", + "No views found. Create views first before configuring vectorization." : "Keng Usiichte fonnt. Erstellt fir d'éischt Usiichte ier Dir d'Vektoriséierung konfiguréiert.", + "No views match your search" : "Keng Usiichte entspriechen ärer Sich", + "No webhooks found" : "Keng Webhooks fonnt", + "No webhooks have been configured yet" : "Et goufe nach keng Webhooks konfiguréiert", + "No workflows found in this project. Create workflows in the n8n editor." : "Keng Workflows an dësem Projet fonnt. Erstellt Workflows am n8n-Editor.", + "None" : "Keen", + "Not helpful" : "Net hëllefräich", + "Not in use" : "Net a Gebrauch", + "Number Constraints" : "Zuelerestriktiounen", + "Number of chunks to vectorize in one API call (1-100)." : "Unzuel u Stécker déi an engem API-Opruff vektoriséiert ginn (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Unzuel u Stécker déi an engem API-Opruff vektoriséiert ginn. Méi héich = méi séier awer méi Späicher. Recommandéiert: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Unzuel un Objeten déi an all Batch verschafft ginn (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Unzuel un Objeten déi an engem API-Opruff vektoriséiert ginn. Méi héich = méi séier awer méi Späicher. Recommandéiert: 10-50.", + "Numeric Range:" : "Numeresche Beräich:", + "Object" : "Objet", + "Object Collection" : "Objet-Sammlung", + "Object Count Prediction" : "Objet-Zuel-Prognos", + "Object ID" : "Objet-ID", + "Object Management" : "Objet-Verwaltung", + "Object Vectorization" : "Objet-Vektoriséierung", + "Object Warmup" : "Objet-Warmup", + "Object collection not configured" : "Objet-Sammlung net konfiguréiert", + "Object not found" : "Objet net fonnt", + "Object permanently deleted" : "Objet definitiv geläscht", + "Object restored successfully" : "Objet erfollegräich restauréiert", + "Object settings updated successfully" : "Objet-Astellunge erfollegräich aktualiséiert", + "Object sources" : "Objet-Quellen", + "Object unlocked successfully" : "Objet erfollegräich opgespaart", + "Object vectorization configuration saved successfully" : "Objet-Vektoriséierungskonfiguratioun erfollegräich gespäichert", + "Object vectorized successfully" : "Objet erfollegräich vektoriséiert", + "Object:" : "Objet:", + "Objects" : "Objeten", + "Objects Analyzed" : "Analyséiert Objeten", + "Objects Distribution" : "Objetverdeelung", + "Objects being analyzed" : "Objeten déi analyséiert ginn", + "Objects by Register" : "Objeten no Register", + "Objects by Schema" : "Objeten no Schema", + "Objects deletion completed successfully" : "Objet-Läschung erfollegräich ofgeschloss", + "Objects in schema" : "Objeten am Schema", + "Objects to Process:" : "Objeten déi ze verschaffe sinn:", + "Objects to be analyzed" : "Objeten déi analyséiert ginn", + "Objects to be deleted" : "Objeten déi geläscht ginn", + "Objects to be validated" : "Objeten déi validéiert ginn", + "Objects will be serialized as JSON text before vectorization" : "Objete ginn als JSON-Text serialiséiert virun der Vektoriséierung", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objete ginn mëll geläscht (als geläscht markéiert awer an der Datebank gehalen). Si kënne spéider erëmgewonne ginn wann néideg.", + "Old Value" : "Aalt Wäert", + "Oldest Item (days)" : "Eelsten Artikel (Deeg)", + "Ollama Chat Settings" : "Ollama-Chat-Astellungen", + "Ollama Embedding Configuration" : "Ollama-Embedding-Konfiguratioun", + "Ollama URL" : "Ollama-URL", + "Only With Changes" : "Nëmme mat Ännerungen", + "Open Register Objects" : "Open-Register-Objeten", + "Open n8n Editor" : "n8n-Editor opmaachen", + "OpenAI Chat Settings" : "OpenAI-Chat-Astellungen", + "OpenAI Embedding Configuration" : "OpenAI-Embedding-Konfiguratioun", + "OpenRegister Settings" : "OpenRegister-Astellungen", + "Optional webhook secret for signature verification" : "Optionalen Webhook-Geheimnis fir d'Signaturverifizéierung", + "Order" : "Reiefolleg", + "Organisation" : "Organisatioun", + "Organisation ID" : "Organisatiouns-ID", + "Organisation Members" : "Organisatiounsmemberen", + "Organisation Statistics" : "Organisatiounsstatistiken", + "Organisation created successfully" : "Organisatioun erfollegräich erstallt", + "Organisation settings saved successfully" : "Organisatiounsastellunge erfollegräich gespäichert", + "Organisations" : "Organisatiounen", + "Organization" : "Organisatioun", + "Organization ID (Optional)" : "Organisatiouns-ID (Optional)", + "Orphaned Items" : "Verloossen Artikelen", + "Owner" : "Besëtzer", + "Owner:" : "Besëtzer:", + "Parallel Mode (Faster, more resource intensive)" : "Parallelle Modus (Méi séier, méi ressourceintensiv)", + "Parallel:" : "Parallel:", + "Password" : "Passwuert", + "Password for SOLR authentication (optional)" : "Passwuert fir d'SOLR-Authentifizéierung (optional)", + "Path" : "Pad", + "Path in repository" : "Pad am Repository", + "Path where the configuration file will be saved in the repository" : "Pad wou d'Konfiguratiounsdatei am Repository gespäichert gëtt", + "Path where the register OAS file will be saved in the repository" : "Pad wou d'Register-OAS-Datei am Repository gespäichert gëtt", + "Pattern (regex)" : "Muster (Regex)", + "Pattern Issue" : "Musterproblem", + "Pattern constraint is missing" : "Muster-Restriktioun feelt", + "Patterns:" : "Musteren:", + "Pending" : "Ausstoend", + "Permanent Delete Mode" : "Definitive Läschmodus", + "Permanently Delete" : "Definitiv läschen", + "Permanently delete already soft-deleted objects" : "Scho mëll geläscht Objeten definitiv läschen", + "Permanently delete this conversation?" : "Dëst Gespréich definitiv läschen?", + "Person" : "Persoun", + "Personal Data" : "Perséinlech Donnéeën", + "Phone" : "Telefon", + "Photos" : "Fotoen", + "Please create an agent in the" : "Erstellt w.e.g. en Agent am", + "Please select an agent to continue" : "Wielt w.e.g. en Agent fir weiderzefueren", + "Please select which register and schema to use for the new object" : "Wielt w.e.g. aus wéi ee Register a Schema fir den neien Objet ze benotzen", + "Please try again later." : "Probéiert w.e.g. méi spéit erëm.", + "Please wait while we fetch your agents." : "Waart w.e.g. wärend mir Är Agenten ofruffen.", + "Please wait while we fetch your applications." : "Waart w.e.g. wärend mir Är Applikatiounen ofruffen.", + "Please wait while we fetch your configurations." : "Waart w.e.g. wärend mir Är Konfiguratiounen ofruffen.", + "Please wait while we fetch your deleted items." : "Waart w.e.g. wärend mir Är geläscht Artikelen ofruffen.", + "Please wait while we fetch your sources." : "Waart w.e.g. wärend mir Är Quellen ofruffen.", + "Polls" : "Ëmfroen", + "Popular Search Terms" : "Populär Sichbegrëffer", + "Port" : "Port", + "Prerequisites" : "Viraussetzungen", + "Press Enter to send, Shift+Enter for new line" : "Dréckt Enter fir ze schécken, Shift+Enter fir eng nei Linn", + "Previous" : "Virdrun", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Verschafft Stécker a Batche mat simuléierter Parallelitéit.", + "Processes file chunks sequentially (safest)." : "Verschafft Dateistécker sequenziell (am séchersten).", + "Processes objects in chunks with simulated parallelism." : "Verschafft Objeten a Stécker mat simuléierter Parallelitéit.", + "Processes objects sequentially (safest)." : "Verschafft Objete sequenziell (am séchersten).", + "Processing" : "Verschaffen", + "Processing Limits" : "Verschaffungslimiten", + "Processing..." : "Gëtt verschafft...", + "Project initialized successfully" : "Projet erfollegräich initialiséiert", + "Project not found. Please initialize first." : "Projet net fonnt. Initialiséiert w.e.g. fir d'éischt.", + "Properties" : "Eegeschaften", + "Property Behaviors" : "Eegeschaftsverhalen", + "Property Configuration:" : "Eegeschaftskonfiguratioun:", + "Property Filters" : "Eegeschaftsfilteren", + "Property Title" : "Eegeschaftstitel", + "Property Type" : "Eegeschaftstyp", + "Property can be improved" : "Eegeschaft kann verbessert ginn", + "Protected" : "Geschützt", + "Provider" : "Provider", + "Provider is required for testing" : "Provider ass fir den Test erfuerderlech", + "Public" : "Ëffentlech", + "Public View" : "Ëffentlech Usiicht", + "Public views can be accessed by anyone in the system" : "Op ëffentlech Usiichte kann jiddereen am System zougräifen", + "Publish" : "Verëffentlechen", + "Publish Objects" : "Objete verëffentlechen", + "Publish all objects in this schema" : "All Objeten an dësem Schema verëffentlechen", + "Published" : "Verëffentlecht", + "Published:" : "Verëffentlecht:", + "Publishing..." : "Gëtt verëffentlecht...", + "Purge" : "Purge", + "Purge Date" : "Purge-Datum", + "Query Complexity Distribution" : "Verdeelung vun der Ufrokomplexitéit", + "Query parameter is required" : "Ufroparameter ass erfuerderlech", + "RAG" : "RAG", + "RAG Configuration" : "RAG-Konfiguratioun", + "Rate limits cleared successfully" : "Rate-Limiten erfollegräich geläscht", + "Raw Changes Data" : "Réi Ännerungsdonnéeën", + "Re-vectorize on object update" : "Bei Objet-Aktualiséierung nei vektoriséieren", + "Read" : "Liesen", + "Recommendations:" : "Recommandatiounen:", + "Recommended Type:" : "Recommandéierten Typ:", + "Recommended: 5 sources" : "Recommandéiert: 5 Quellen", + "Refresh" : "Aktualiséieren", + "Refresh Data" : "Donnéeën aktualiséieren", + "Refresh Stats" : "Statistiken aktualiséieren", + "Refresh Workflows" : "Workflows aktualiséieren", + "Refresh dashboard" : "Dashboard aktualiséieren", + "Refresh database info" : "Datebankinformatiounen aktualiséieren", + "Register" : "Register", + "Register ID" : "Register-ID", + "Register Statistics" : "Registerstatistiken", + "Register Totals" : "Register-Totaler", + "Register depublished successfully" : "Register erfollegräich depublizéiert", + "Register not found" : "Register net fonnt", + "Register objects deletion completed successfully" : "Läschung vun de Register-Objeten erfollegräich ofgeschloss", + "Register published successfully" : "Register erfollegräich verëffentlecht", + "Register/Schema Usage" : "Register-/Schemagebrauch", + "Registers" : "Registeren", + "Reindex" : "Nei indexéieren", + "Reindex completed" : "Nei-Indexéierung ofgeschloss", + "Reindex completed successfully" : "Nei-Indexéierung erfollegräich ofgeschloss", + "Reindex failed" : "Nei-Indexéierung feelgeschloen", + "Relations" : "Relatiounen", + "Remote Version" : "Remote-Versioun", + "Remove" : "Ewechhuelen", + "Remove filter" : "Filter ewechhuelen", + "Remove from favorites" : "Aus de Favoritten ewechhuelen", + "Remove group" : "Grupp ewechhuelen", + "Removed" : "Ewechgeholl", + "Removed from favorites" : "Aus de Favoritten ewechgeholl", + "Rename Conversation" : "Gespréich ëmbenennen", + "Rename conversation" : "Gespréich ëmbenennen", + "Replicas" : "Repliken", + "Repository" : "Repository", + "Request Body" : "Ufro-Kierper", + "Request Data" : "Ufro-Donnéeën", + "Request Details" : "Ufro-Detailer", + "Request body must be JSON with a \"query\" field" : "Verzoekinhoud moet JSON zijn met een \"query\"-veld", + "Request timeout in seconds" : "Ufro-Timeout a Sekonnen", + "Required field" : "Erfuerderlecht Feld", + "Required status is inconsistent" : "Erfuerderleche Status ass inkonsistent", + "Rerun Search" : "Sich nei ausféieren", + "Reset Changes" : "Ännerunge zerécksetzen", + "Reset Filters" : "Filteren zerécksetzen", + "Response Body" : "Äntwert-Kierper", + "Response Details" : "Äntwert-Detailer", + "Response Time:" : "Äntwertzäit:", + "Restore" : "Restauréieren", + "Restore conversation" : "Gespréich restauréieren", + "Restore or permanently delete items" : "Artikele restauréieren oder definitiv läschen", + "Result Count Range" : "Resultatzuel-Beräich", + "Results" : "Resultater", + "Retrieve all objects for this schema" : "All Objete fir dëse Schema ofruffen", + "Retry" : "Nei probéieren", + "Retry Failed Extractions" : "Feelgeschloen Extraktiounen nei probéieren", + "Retry Policy" : "Neiversuchspolitik", + "Retry completed" : "Neie Versuch ofgeschloss", + "Retrying..." : "Gëtt nei probéiert...", + "Risk Level" : "Risikoniveau", + "Roles updated successfully" : "Rollen erfollegräich aktualiséiert", + "SOLR Connection Settings" : "SOLR-Verbindungsastellungen", + "SOLR Version:" : "SOLR-Versioun:", + "SOLR actions menu" : "SOLR-Aktiounsmenü", + "SOLR base path (usually /solr)" : "SOLR-Basispad (normalerweis /solr)", + "SOLR is disabled" : "SOLR ass desaktivéiert", + "SOLR is not available or not configured" : "SOLR ass net verfügbar oder net konfiguréiert", + "SOLR logging disabled" : "SOLR-Protokolléierung desaktivéiert", + "SOLR logging enabled" : "SOLR-Protokolléierung aktivéiert", + "SOLR search disabled" : "SOLR-Sich desaktivéiert", + "SOLR search enabled" : "SOLR-Sich aktivéiert", + "SOLR server hostname or IP address" : "SOLR-Server-Hostnumm oder IP-Adress", + "SOLR server port number (optional, defaults to 8983)" : "SOLR-Server-Portnummer (optional, Standard ass 8983)", + "SOLR setup completed successfully" : "SOLR-Asetzung erfollegräich ofgeschloss", + "SOLR setup failed" : "SOLR-Asetzung feelgeschloen", + "SOLR setup failed - check logs" : "SOLR-Asetzung feelgeschloen - iwwerpréift d'Protokoller", + "Sample Values:" : "Beispillwäerter:", + "Save" : "Späicheren", + "Save Configuration" : "Konfiguratioun späicheren", + "Save Connection Settings" : "Verbindungsastellunge späicheren", + "Save Roles" : "Rolle späicheren", + "Save Settings" : "Astellunge späicheren", + "Save current search as view" : "Aktuell Sich als Usiicht späicheren", + "Saved Views" : "Gespäichert Usiichten", + "Saving..." : "Gëtt gespäichert...", + "Schema" : "Schema", + "Schema ID" : "Schema-ID", + "Schema Name" : "Schemanumm", + "Schema Statistics" : "Schemastatistiken", + "Schema depublished successfully" : "Schema erfollegräich depublizéiert", + "Schema objects deletion completed successfully" : "Läschung vun de Schema-Objeten erfollegräich ofgeschloss", + "Schema published successfully" : "Schema erfollegräich verëffentlecht", + "Schema successfully created" : "Schema erfollegräich erstallt", + "Schema successfully updated" : "Schema erfollegräich aktualiséiert", + "Schemas" : "Schemaen", + "Scheme" : "Schema", + "Search / Views" : "Sich / Usiichten", + "Search Activity" : "Sichaktivitéit", + "Search Analytics" : "Sichanalysen", + "Search Entities" : "Entitéite sichen", + "Search Files" : "Dateie sichen", + "Search GitHub" : "GitHub duerchsichen", + "Search GitLab" : "GitLab duerchsichen", + "Search Method" : "Sichmethod", + "Search Term" : "Sichbegrëff", + "Search Trail Details" : "Sich-Trail-Detailer", + "Search Trail Management" : "Sich-Trail-Verwaltung", + "Search Trail Statistics" : "Sich-Trail-Statistiken", + "Search Trails" : "Sich-Trails", + "Search Views" : "Usiichte sichen", + "Search Webhooks" : "Webhooks duerchsichen", + "Search by file name or path" : "No Dateinumm oder Pad sichen", + "Search by name or URL" : "No Numm oder URL sichen", + "Search by value" : "No Wäert sichen", + "Search in Files" : "An Dateie sichen", + "Search in Objects" : "An Objete sichen", + "Search property names..." : "Eegeschaftsnimm sichen...", + "Search trail deletion not implemented yet" : "Sich-Trail-Läschung nach net ëmgesat", + "Search views..." : "Usiichte sichen...", + "Secret" : "Geheimnis", + "Secret key for HMAC signature generation (optional)" : "Geheime Schlëssel fir d'HMAC-Signaturgeneréierung (optional)", + "Select AI Agent" : "KI-Agent auswielen", + "Select All" : "Alles auswielen", + "Select ConfigSet" : "ConfigSet auswielen", + "Select File Types to Vectorize:" : "Dateitype fir ze vektoriséieren auswielen:", + "Select HTTP method" : "HTTP-Method auswielen", + "Select Register and Schema" : "Register a Schema auswielen", + "Select Views to Vectorize:" : "Usiichte fir ze vektoriséieren auswielen:", + "Select a Nextcloud group to add" : "En Nextcloud-Grupp auswielen fir derbäizesetzen", + "Select a branch" : "Eng Branche auswielen", + "Select a model or type a custom model name" : "E Modell auswielen oder e personaliséierte Modellnumm aginn", + "Select a repository" : "E Repository auswielen", + "Select a repository you have write access to" : "E Repository auswielen op deen Dir Schreifzougang hutt", + "Select an AI Agent" : "E KI-Agent auswielen", + "Select an AI agent to begin chatting with your data." : "Wielt e KI-Agent fir mat ären Donnéeën unzefänken ze chatten.", + "Select backend" : "Backend auswielen", + "Select chat model" : "Chat-Modell auswielen", + "Select collection for files" : "Sammlung fir Dateien auswielen", + "Select collection for objects" : "Sammlung fir Objeten auswielen", + "Select default organisation" : "Standardorganisatioun auswielen", + "Select event to listen to..." : "Evenement auswielen op deen nogelauschtert gëtt...", + "Select model" : "Modell auswielen", + "Select options..." : "Optiounen auswielen...", + "Select period" : "Period auswielen", + "Select property to send as payload" : "Eegeschaft auswielen déi als Payload geschéckt gëtt", + "Select provider" : "Provider auswielen", + "Select registers and schemas to save a view" : "Registeren a Schemae auswielen fir eng Usiicht ze späicheren", + "Select retry policy" : "Neiversuchspolitik auswielen", + "Select the branch to publish to" : "D'Branche auswielen op déi verëffentlecht gëtt", + "Select the event this webhook should listen to" : "D'Evenement auswielen op dat dëse Webhook soll nolauschteren", + "Select views to vectorize:" : "Usiichte fir ze vektoriséieren auswielen:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Wielt aus, wéi eng Nextcloud-Gruppe fir dës Organisatioun verfügbar sinn. Benotzer an dëse Gruppen hunn Zougang zu den Organisatiounsressourcen.", + "Select which data views the AI can search" : "Wielt aus, wéi eng Datenusiichten d'KI duerchsiche kann", + "Select which property from the event should be used as the webhook payload data" : "Wielt aus, wéi eng Eegeschaft vum Evenement als Webhook-Payload-Donnéeë benotzt soll ginn", + "Select which tools the AI can use to perform actions" : "Wielt aus, wéi eng Tools d'KI benotze kann fir Aktiounen auszeféieren", + "Selected" : "Ausgewielt", + "Selected Groups" : "Ausgewielt Gruppen", + "Selected audit trails deleted successfully" : "Ausgewielten Audit-Trails erfollegräich geläscht", + "Selected search trails deleted successfully" : "Ausgewielte Sich-Trails erfollegräich geläscht", + "Selected users can access this view" : "Ausgewielt Benotzer kënnen op dës Usiicht zougräifen", + "Send additional feedback" : "Zousätzlecht Feedback schécken", + "Send as CloudEvent" : "Als CloudEvent schécken", + "Sensitive PII" : "Sensibel PII", + "Serial Mode (Safer, slower)" : "Seriellen Modus (Méi sécher, méi lues)", + "Serial:" : "Seriell:", + "Server Configuration" : "Serverkonfiguratioun", + "Server Information" : "Serverinformatiounen", + "Settings" : "Astellungen", + "Shards" : "Shards", + "Share" : "Deelen", + "Share with Groups" : "Mat Gruppen deelen", + "Share with Users" : "Mat Benotzer deelen", + "Show Filters" : "Filteren uweisen", + "Show less" : "Manner uweisen", + "Show only entries with changes" : "Nëmmen Andréi mat Ännerungen uweisen", + "Simple" : "Einfach", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Einfach Ufroen: Grondleeënd Textsiche mat minimale Parameter (z. B. eenzele Sichbegrëff, keng erweidert Filteren)", + "Size" : "Gréisst", + "Slug" : "Slug", + "Soft Delete Mode" : "Mëlle Läschmodus", + "Soft Deleted Items" : "Mëll geläscht Artikelen", + "SolrCloud mode enabled" : "SolrCloud-Modus aktivéiert", + "Source" : "Quell", + "Sources" : "Quellen", + "Sources:" : "Quellen:", + "Standalone SOLR mode" : "Eegestännege SOLR-Modus", + "Start Conversation" : "Gespréich ufänken", + "Start Vectorization" : "Vektoriséierung starten", + "Start a conversation" : "E Gespréich ufänken", + "Starting names cache warmup..." : "Nimm-Cache-Warmup gëtt gestart...", + "Starting..." : "Start...", + "Statistics" : "Statistiken", + "Stats calculation not yet implemented" : "Statistikberechnung nach net ëmgesat", + "Status" : "Status", + "Status Code" : "Status-Code", + "String Constraints" : "String-Restriktiounen", + "Success" : "Erfolleg", + "Success Rate" : "Erfollegsquote", + "Success Status" : "Erfollegsstatus", + "Successful" : "Erfollegräich", + "Successfully joined organisation" : "Erfollegräich der Organisatioun bäigetrueden", + "Sync Table" : "Tabell synchroniséieren", + "Synchronization Settings" : "Synchroniséierungsastellungen", + "System Default" : "System-Standard", + "Technical Description" : "Technesch Beschreiwung", + "Technical description for developers and administrators" : "Technesch Beschreiwung fir Entwéckler an Administrateuren", + "Temperature" : "Temperatur", + "Templates" : "Templater", + "Test" : "Test", + "Test Chat" : "Chat testen", + "Test Connection" : "Verbindung testen", + "Test Embedding" : "Embedding testen", + "Test endpoint executed successfully" : "Test-Endpunkt erfollegräich ausgefouert", + "Test webhook delivered successfully" : "Test-Webhook erfollegräich geliwwert", + "Test webhook delivery failed" : "Test-Webhook-Liwwerung feelgeschloen", + "Test webhook sent successfully" : "Test-Webhook erfollegräich geschéckt", + "Testing..." : "Gëtt getest...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Textstécker ginn während der Datei-Extraktioun generéiert an an der Datebank gespäichert. D'Vektoriséierung liest dës virstéckelt Dateien an wandelt se an Embeddings ëm.", + "Text extracted successfully" : "Text erfollegräich extrahéiert", + "Text extraction disabled" : "Textextraktioun desaktivéiert", + "The URL where webhook events will be sent" : "D'URL wou d'Webhook-Evenementer higeschéckt ginn", + "The requested conversation does not exist" : "Dat ugefrote Gespréich existéiert net", + "The saved view and all its search configuration" : "Déi gespäichert Usiicht an all hir Sichkonfiguratioun", + "There are no audit trail entries matching your current filters." : "Et gi keng Audit-Trail-Andréi déi ären aktuelle Filteren entspriechen.", + "There are no deleted items matching your current filters." : "Et gi keng geläscht Artikelen déi ären aktuelle Filteren entspriechen.", + "There are no search trail entries matching your current filters." : "Et gi keng Sich-Trail-Andréi déi ären aktuelle Filteren entspriechen.", + "There are no webhook log entries matching your filters." : "Et gi keng Webhook-Protokoll-Andréi déi äre Filteren entspriechen.", + "This action cannot be undone." : "Dës Aktioun kann net réckgängeg gemaach ginn.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Dës Aktioun kann net réckgängeg gemaach ginn. Vergewëssert Iech, datt keng Sammlunge dëse ConfigSet benotzen.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Dësen Agent huet keng konfiguréierbar Usiichten oder Tools, awer Dir kënnt nach ëmmer d'RAG-Astellungen hei drënner upassen.", + "This analysis may take some time" : "Dës Analyse kann e bësse Zäit daueren", + "This audit trail entry does not contain any change information." : "Dësen Audit-Trail-Andrag enthält keng Ännerungsinformatiounen.", + "This configuration has no data defined." : "Dës Konfiguratioun huet keng Donnéeën definéiert.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Dësen Endpunkt ass veroudert. Benotzt amplaz stéckbaséiert Endpunkten.", + "This entity has no relations to objects or files" : "Dës Entitéit huet keng Relatiounen zu Objeten oder Dateien", + "This is a test webhook from OpenRegister" : "Dëst ass en Test-Webhook vun OpenRegister", + "This schema must use Magic Table configuration to sync" : "Dëse Schema muss d'Magic-Table-Konfiguratioun benotze fir ze synchroniséieren", + "This source has no associated registers." : "Dës Quell huet keng zougeuerdent Registeren.", + "This will copy the _default ConfigSet with the new name" : "Dëst kopéiert de _default-ConfigSet mam neien Numm", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Dit zal permanent verwijderen ALL embeddings (vectors) from the database. You will need to re-vectorize all objecten and bestands. Deze actie kan niet ongedaan worden gemaakt.\\n\\nWeet u zeker dat u wilt continue?", + "This will permanently delete:" : "Dëst läscht definitiv:", + "Timeout (seconds)" : "Timeout (Sekonnen)", + "Timestamp" : "Zäitstempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tipp: Aktivéiert nëmmen Usiichten déi semantesch Sich brauchen, fir d'Embedding-Käschten ze minimiséieren. Einfach Nokuck-Tabelle brauche selten eng Vektoriséierung.", + "Title" : "Titel", + "To Date" : "Bis zum Datum", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Fir d'Stéckgréisst oder d'Strategie unzepassen, gitt op Datei-Konfiguratioun → Verschaffungslimiten.", + "To change the embedding provider or model, go to LLM Configuration." : "Fir den Embedding-Provider oder de Modell z'änneren, gitt op LLM-Konfiguratioun.", + "To date" : "Bis zum Datum", + "Toggle search sidebar" : "Sich-Säiteleescht ëmschalten", + "Tools" : "Tools", + "Top Deleters" : "Top-Läscher", + "Total" : "Total", + "Total Audit Trails" : "Total Audit-Trails", + "Total Chunks Available:" : "Total verfügbar Stécker:", + "Total Deleted Items" : "Total geläscht Artikelen", + "Total Members" : "Total Memberen", + "Total Objects" : "Total Objeten", + "Total Objects in Database:" : "Total Objeten an der Datebank:", + "Total Organisations" : "Total Organisatiounen", + "Total Results" : "Total Resultater", + "Total Searches" : "Total Sichen", + "Total Size" : "Total Gréisst", + "Total Storage" : "Total Späicher", + "Total:" : "Total:", + "Totals" : "Totaler", + "Type" : "Typ", + "Type Issue" : "Typproblem", + "Type Variations:" : "Typvariatiounen:", + "Type must be \"positive\" or \"negative\"" : "Type moet \"positive\" of \"negative\" zijn", + "Type to search for groups" : "Tippt fir no Gruppen ze sichen", + "Type to search for users" : "Tippt fir no Benotzer ze sichen", + "Type to search groups..." : "Tippt fir Gruppen ze sichen...", + "Type to search users..." : "Tippt fir Benotzer ze sichen...", + "URL" : "URL", + "URL where Ollama is running" : "URL wou Ollama leeft", + "UUID:" : "UUID:", + "Unchanged" : "Onverännert", + "Unique Search Terms" : "Eenzegaarteg Sichbegrëffer", + "Unique Terms" : "Eenzegaarteg Begrëffer", + "Unknown" : "Onbekannt", + "Unknown Register" : "Onbekannte Register", + "Unknown Schema" : "Onbekannte Schema", + "Untitled View" : "Usiicht ouni Titel", + "Update" : "Aktualiséieren", + "Update Operations" : "Aktualiséierungsoperatiounen", + "Update configuration: ..." : "Konfiguratioun aktualiséieren: ...", + "Update register OAS: ..." : "Register-OAS aktualiséieren: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Vektore aktualiséiere wann d'Objet-Donnéeën änneren (recommandéiert fir eng korrekt Sich)", + "Updated" : "Aktualiséiert", + "Updated At" : "Aktualiséiert um", + "Updated:" : "Aktualiséiert:", + "Uptime:" : "Uptime:", + "Usage Count" : "Gebrauchszuel", + "Use SolrCloud with Zookeeper for distributed search" : "SolrCloud mat Zookeeper fir eng verdeelt Sich benotzen", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Benotzt Filteren fir Audit-Trail-Andréi no Register, Schema, Aktiounstyp, Benotzer, Datumsberäich oder Objet-ID anzegrenzen.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Benotzt Filteren fir geläscht Artikelen no Register, Schema, Läschdatum oder dem Benotzer deen se geläscht huet anzegrenzen.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Benotzt Filteren fir Sich-Trail-Andréi no Register, Schema, Erfollegsstatus, Benotzer, Datumsberäich, Sichbegrëffer oder Leeschtungsmetrike anzegrenzen.", + "Used By Collections" : "Benotzt vu Sammlungen", + "User" : "Benotzer", + "User Agent Statistics" : "User-Agent-Statistiken", + "User Description" : "Benotzerbeschreiwung", + "User rate limits cleared successfully" : "Benotzer-Rate-Limiten erfollegräich geläscht", + "User removed successfully" : "Benotzer erfollegräich ewechgeholl", + "User-friendly description shown in forms and help text" : "Benotzerfrëndlech Beschreiwung déi a Formularen an am Hëllefstext ugewise gëtt", + "Username" : "Benotzernumm", + "Username for SOLR authentication (optional)" : "Benotzernumm fir d'SOLR-Authentifizéierung (optional)", + "Users" : "Benotzer", + "Using Pre-Generated Chunks" : "Virgeneréiert Stécker benotzen", + "Validate" : "Validéieren", + "Validate Objects" : "Objeten validéieren", + "Validation completed successfully" : "Validéierung erfollegräich ofgeschloss", + "Validation failed" : "Validéierung feelgeschloen", + "Value" : "Wäert", + "Vector Dimensions" : "Vektordimensiounen", + "Vector Search Backend" : "Vektorsich-Backend", + "Vector field: _embedding_" : "Vektorfeld: _embedding_", + "Vectorization Triggers" : "Vektoriséierungs-Trigger", + "Vectorize All Files" : "All Dateie vektoriséieren", + "Vectorize All Objects" : "All Objete vektoriséieren", + "Vectorize all file types" : "All Dateitype vektoriséieren", + "Vectorize all views" : "All Usiichte vektoriséieren", + "Vectorize on object creation" : "Bei der Objet-Erstellung vektoriséieren", + "Vectors will be stored in your existing object and file collections" : "Vektore ginn an ären existéierenden Objet- an Datei-Sammlunge gespäichert", + "Version" : "Versioun", + "Very High" : "Ganz héich", + "View API Docs" : "API-Dokumentatioun ukucken", + "View Changes" : "Ännerungen ukucken", + "View Details" : "Detailer ukucken", + "View Error" : "Feeler ukucken", + "View File" : "Datei ukucken", + "View Full Details" : "Vollstänneg Detailer ukucken", + "View Logs" : "Protokoller ukucken", + "View Name" : "Usiicht-Numm", + "View Object" : "Objet ukucken", + "View Parameters" : "Usiicht-Parameteren", + "View Selection" : "Usiicht-Auswiel", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Sich-Trail-Protokoller mat erweiderten Filter- an Analysefäegkeeten ukucken an analyséieren", + "View and analyze system audit trails with advanced filtering capabilities" : "System-Audit-Trails mat erweiderte Filterfäegkeeten ukucken an analyséieren", + "View deleted successfully" : "Usiicht erfollegräich geläscht", + "View entity details and manage relations" : "Entitéitsdetailer ukucken a Relatioune verwalten", + "View name" : "Usiicht-Numm", + "View name is required" : "Usiicht-Numm ass erfuerderlech", + "View saved successfully!" : "Usiicht erfollegräich gespäichert!", + "View search analytics and manage search logs" : "Sichanalysen ukucken a Sichprotokoller verwalten", + "View successfully deleted" : "Usiicht erfollegräich geläscht", + "View successfully updated" : "Usiicht erfollegräich aktualiséiert", + "View updated successfully!" : "Usiicht erfollegräich aktualiséiert!", + "View webhook delivery logs and filter by webhook" : "Webhook-Liwwerungsprotokoller ukucken an no Webhook filteren", + "Views" : "Usiichten", + "Visible to users" : "Sichtbar fir Benotzer", + "Wait for Response" : "Op Äntwert waarden", + "Wait for webhook response before continuing (required for request/response flows)" : "Op d'Webhook-Äntwert waarde virum Weiderfueren (erfuerderlech fir Ufro-/Äntwertflëss)", + "Warmup Names Cache" : "Nimm-Cache opwiermen", + "Warning" : "Warnung", + "Warning:" : "Warnung:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Webhook-Protokoll-Detailer", + "Webhook Logs" : "Webhook-Protokoller", + "Webhook created successfully" : "Webhook erfollegräich erstallt", + "Webhook deleted" : "Webhook geläscht", + "Webhook retry delivered successfully" : "Webhook-Neiversuch erfollegräich geliwwert", + "Webhook retry delivery failed" : "Webhook-Neiversuch-Liwwerung feelgeschloen", + "Webhook updated" : "Webhook aktualiséiert", + "Webhook updated successfully" : "Webhook erfollegräich aktualiséiert", + "Webhooks" : "Webhooks", + "Weekly" : "Wëchentlech", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Webhook-Payload am CloudEvents-Format akapsele fir eng besser Interoperabilitéit", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Jo", + "You" : "Dir", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Dir kënnt nei ConfigSets op Basis vum _default-Template erstellen, oder personaliséiert direkt op Äre SOLR-Server eroplueden.", + "You do not have access to this conversation" : "Dir hutt kee Zougang zu dësem Gespréich", + "You do not have permission to delete this conversation" : "Dir hutt net d'Erlaabnis fir dëst Gespréich ze läschen", + "You do not have permission to modify this conversation" : "Dir hutt net d'Erlaabnis fir dëst Gespréich z'änneren", + "You do not have permission to restore this conversation" : "Dir hutt net d'Erlaabnis fir dëst Gespréich ze restauréieren", + "You must be logged in to favorite views" : "Dir musst ageloggt si fir Usiichten als Favoritt ze markéieren", + "You need an AI agent to start a conversation." : "Dir braucht e KI-Agent fir e Gespréich unzefänken.", + "Your Fireworks AI API key. Get one at" : "Äre Fireworks-AI-API-Schlëssel. Kritt een op", + "Your OpenAI API key. Get one at" : "Äre OpenAI-API-Schlëssel. Kritt een op", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Äre Feedback gouf registréiert. Optional kënnt Dir hei zousätzlech Detailer ginn...", + "Zookeeper Hosts" : "Zookeeper-Hosten", + "Zookeeper Port" : "Zookeeper-Port", + "Zookeeper Settings (SolrCloud)" : "Zookeeper-Astellungen (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper-Verbindungsstring fir SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper-Portnummer (optional, Standard ass 2181)", + "chunk" : "Stéck", + "chunks" : "Stécker", + "chunks processed" : "Stécker verschafft", + "configuration(s)" : "Konfiguratioun(en)", + "e.g., lib/Settings/config.json" : "z. B. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "z. B. lib/Settings/register.json", + "failed" : "feelgeschloen", + "file" : "Datei", + "files" : "Dateien", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "Memberen", + "menu or contact someone with permission to create agents." : "Menü oder kontaktéiert een mat der Erlaabnis fir Agenten ze erstellen.", + "messages" : "Messagen", + "n8n URL and API key are required" : "n8n-URL an API-Schlëssel sinn erfuerderlech", + "n8n configuration saved successfully" : "n8n-Konfiguratioun erfollegräich gespäichert", + "n8n connection not configured" : "n8n-Verbindung net konfiguréiert", + "n8n connection successful" : "n8n-Verbindung erfollegräich", + "n8n connection test successful" : "n8n-Verbindungstest erfollegräich", + "n8n integration disabled" : "n8n-Integratioun desaktivéiert", + "n8n integration enabled" : "n8n-Integratioun aktivéiert", + "n8n project initialized successfully" : "n8n-Projet erfollegräich initialiséiert", + "n8n settings saved successfully" : "n8n-Astellunge erfollegräich gespäichert", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "Objeten", + "objects processed" : "Objeten verschafft", + "or pick a range" : "oder e Beräich auswielen", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "bitt e gudde Gläichgewiicht tëscht Geschwindegkeet a Genauegkeet.", + "register(s)" : "Register", + "register(s) selected" : "Register ausgewielt", + "required" : "erfuerderlech", + "results" : "Resultater", + "schema(s)" : "Schema(en)", + "schema(s) selected" : "Schema(en) ausgewielt", + "sk-..." : "sk-...", + "this application" : "dës Applikatioun", + "to" : "bis", + "vectorized" : "vektoriséiert", + "views selected" : "Usiichten ausgewielt", + "ℹ️ Current Configuration" : "ℹ️ Aktuell Konfiguratioun", + "⚡ Batch Processing" : "⚡ Batch-Veraarbechtung", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Ëmfaassende Kontext ✓ Manner wahrscheinlech datt Detailer feelen ✗ Méi lues Äntwerten ✗ Kann manner relevant Informatiounen enthalen", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Méi séier Äntwerten ✓ Méi fokusséiert Äntwerten ✗ Kann relevant Informatioune verpassen", + "✨ AI Features" : "✨ KI-Funktiounen", + "All search trails cleared successfully" : "All Sich-Trails erfollegräich geläscht", + "Multiple search trail deletion not implemented yet" : "Méifach Sich-Trail-Läschung nach net ëmgesat", + "No expired search trails found to clear" : "Keng ofgelaf Sich-Trails fonnt fir ze läschen", + "Missing message" : "Feelende Message", + "message content is required" : "Message-Inhalt ass erfuerderlech", + "Missing conversation or agentUuid" : "Feelend Gespréich oder agentUuid", + "Access denied" : "Zougang verweigert", + "Conversation not found" : "Gespréich net fonnt", + "AI service not configured" : "KI-Service net konfiguréiert", + "Failed to process message" : "Verschaffe vum Message feelgeschloen", + "Missing conversationId" : "Feelend conversationId", + "conversationId is required" : "conversationId ass erfuerderlech", + "Failed to fetch conversation history" : "Ofruff vun der Gespréichshistorie feelgeschloen", + "Failed to clear conversation" : "Läsche vum Gespréich feelgeschloen", + "Invalid feedback type" : "Ongëltegen Feedback-Typ", + "type must be \"positive\" or \"negative\"" : "type moet \"positive\" of \"negative\" zijn", + "Message not found" : "Message net fonnt", + "Failed to save feedback" : "Späichere vum Feedback feelgeschloen", + "Failed to get chat statistics" : "Ofruff vun de Chat-Statistike feelgeschloen", + "Not Found" : "Net fonnt", + "Engine not found" : "Engine net fonnt", + "Consumer not found" : "Consumer net fonnt", + "Backend parameter is required" : "Backend-Parameter ass erfuerderlech", + "Failed to get database information: %s" : "Ofruff vun den Datebankinformatioune feelgeschloen: %s", + "SOLR setup error: %s" : "SOLR-Asetzungsfeeler: %s", + "Reindex failed: %s" : "Nei-Indexéierung feelgeschloen: %s", + "Anonymized" : "Anonymiséiert", + "Entities detected" : "Entitéiten erkannt", + "Extract Now" : "Elo extrahéieren", + "Extracted at" : "Extrahéiert um", + "Extraction" : "Extraktioun", + "Failed to load extraction data" : "Lueden vun den Extraktiounsdonnéeë feelgeschloen", + "Failed to load register data" : "Lueden vun den Registerdonnéeë feelgeschloen", + "Failed to retrieve extraction status." : "Ofruff vum Extraktiounsstatus feelgeschloen.", + "Failed to retrieve objects for file." : "Ofruff vun den Objete fir d'Datei feelgeschloen.", + "No extraction data available for this file" : "Keng Extraktiounsdonnéeë fir dës Datei verfügbar", + "No register objects reference this file" : "Keng Register-Objete referenzéieren dës Datei", + "Not extracted" : "Net extrahéiert", + "Register Objects" : "Register-Objeten", + "Risk level" : "Risikoniveau", + "Text chunks" : "Textstécker", + "Unknown error" : "Onbekannte Feeler", + "Very high" : "Ganz héich", + "{title} in {register} / {schema}" : "{title} am {register} / {schema}", + "Push Notifications" : "Push-Notifikatiounen", + "Real-time push notification status via notify_push" : "Echtzäit-Push-Notifikatiounsstatus iwwer notify_push", + "Realtime push not available — the notify_push app is not installed" : "Echtzäit-Push net verfügbar — d'notify_push-App ass net installéiert", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installéiert d'notify_push-App aus dem Nextcloud-App-Store fir Echtzäit-Aktualiséierungen z'aktivéieren.", + "Open Nextcloud App Store" : "Nextcloud-App-Store opmaachen", + "notify_push is installed but not yet active" : "notify_push ass installéiert awer nach net aktiv", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push ass installéiert awer OpenRegister huet nach keen erfollegräiche Push bestätegt. Léist eng Objet-Späicherung aus fir z'aktivéieren, oder iwwerpréift Är notify_push-Konfiguratioun.", + "notify_push configuration guide" : "notify_push-Konfiguratiounsguide", + "Realtime push active" : "Echtzäit-Push aktiv", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Echtzäit-Push-Notifikatioune sinn aktiv. Verbonne Clienten kréien direkt Aktualiséierunge wann Objeten erstallt, aktualiséiert oder geläscht ginn.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/lb.json b/l10n/lb.json new file mode 100644 index 0000000000..4440d90b05 --- /dev/null +++ b/l10n/lb.json @@ -0,0 +1,2665 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin vereist)", + "(no subject)": "(kee Sujet)", + "(no title)": "(keen Titel)", + "(unnamed)": "(ouni Numm)", + "(untitled card)": "(Kaart ouni Titel)", + "({count} selected)": "({count} geselecteerd)", + ", {failed} failed": ", {failed} mislukt", + "0 = unlimited": "0 = onbeperkt", + "A deactivation request is pending.": "Er is een deactiveringsverzoek in behandeling.", + "AI Assistant": "KI-Assistent", + "AI Chat": "KI-Chat", + "AI service not configured": "KI-Service net konfiguréiert", + "API Key": "API-Schlëssel", + "API Request Quota (requests/day)": "API-verzoeklimiet (verzoeken/dag)", + "API Token Configuration": "API-tokenconfiguratie", + "API Tokens": "API-tokens", + "API tokens saved successfully": "API-Tokens erfollegräich gespäichert", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.", + "About ConfigSets": "Iwwer ConfigSets", + "Access denied": "Zougang verweigert", + "Access organisation dashboard and analytics": "Toegang tot organisatiedashboard en analyses", + "Account": "Account", + "Action": "Aktioun", + "Action Distribution": "Aktiounsverdeelung", + "Action:": "Aktioun:", + "Actions": "Aktiounen", + "Active": "Aktiv", + "Active Collections": "Aktiv Sammlungen", + "Active Filters": "Aktiv Filteren", + "Active Organisation:": "Actieve Organisatie:", + "Active Organisations": "Aktiv Organisatiounen", + "Active filters:": "Aktiv Filteren:", + "Active organisation changed successfully": "Actieve organisatie succesvol gewijzigd", + "Active organisation set successfully": "Aktiv Organisatioun erfollegräich gesat", + "Activities": "Aktivitéiten", + "Activity": "Activiteit", + "Activity Period": "Aktivitéitsperiod", + "Add": "Toevoegen", + "Add Attachment": "Bijlage toevoegen", + "Add Endpoint": "Endpunkt derbäisetzen", + "Add Groups": "Gruppen derbäisetzen", + "Add Property to '{title}'": "Eigenschap toevoegen aan '{title}'", + "Add Register": "Register toevoegen", + "Add Schema": "Schema derbäisetzen", + "Add Source": "Quell derbäisetzen", + "Add User": "Gebruiker toevoegen", + "Add User to Organisation": "Gebruiker toevoegen aan organisatie", + "Add a contact from any of your address books to associate it with this object.": "Add a contact from any of your address books to associate it with this object.", + "Add a file or files": "Een of meerdere bestanden toevoegen", + "Add card": "Kaart derbäisetzen", + "Add contact": "Kontakt derbäisetzen", + "Add endpoint": "Eindpunt toevoegen", + "Add oneOf entry": "oneOf-invoer toevoegen", + "Add range": "Bereik toevoegen", + "Add schema titles, descriptions, and register information to provide richer context for search": "Setzt Schematitelen, Beschreiwungen a Registerinformatiounen derbäi, fir e räicheren Kontext fir d'Sich ze ginn", + "Add to favorites": "Zu de Favoritten derbäisetzen", + "Added": "Derbäigesat", + "Added to favorites": "Zu de Favoritten derbäigesat", + "Additional Information": "Zousätzlech Informatiounen", + "Additional feedback saved. Thank you!": "Zousätzlecht Feedback gespäichert. Merci!", + "Addresses": "Adressen", + "Admin": "Administrateur", + "Advanced": "Erweidert", + "Advanced Filters": "Erweidert Filteren", + "Advanced Options": "Erweidert Optiounen", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Erweidert Filteren sinn net verfügbar wann d'Datebankquell benotzt gëtt. Wiesselt op Auto oder SOLR-Index fir Filteroptiounen.", + "After": "No", + "Agent Type": "Agenttype", + "Agent deleted successfully": "Agent erfollegräich geläscht", + "Agent successfully created": "Agent successfully created", + "Agent successfully updated": "Agent succesvol bijgewerkt", + "Agents": "Agenten", + "Aggregated across schemas": "Geaggregeerd over schema's", + "All": "All", + "All Categories": "All Kategorien", + "All Collections": "All Sammlungen", + "All Confidence Levels": "All Vertrauensniveauen", + "All ConfigSets": "All ConfigSets", + "All Files": "All Dateien", + "All Levels": "All Niveauen", + "All Types": "All Typen", + "All Webhooks": "All Webhooks", + "All actions": "All Aktiounen", + "All audit trails cleared successfully": "All Audit-Trails erfollegräich geläscht", + "All clear": "Alles a Rei", + "All file types": "Alle bestandstypen", + "All registers": "All Registeren", + "All schemas": "All Schemaen", + "All search trails cleared successfully": "All Sich-Trails erfollegräich geläscht", + "All searches": "All Sichen", + "All users": "All Benotzer", + "All-day events": "Hele-dag-evenementen", + "Allowed MIME Types": "Toegestane MIME-typen", + "Already linked": "Al gekoppeld", + "An error occurred while clearing audit trails": "E Feeler ass beim Läsche vun den Audit-Trails opgetrueden", + "An error occurred while deleting the audit trail": "E Feeler ass beim Läsche vum Audit-Trail opgetrueden", + "An error occurred while deleting the object": "Er is een fout opgetreden bij het verwijderen van het object", + "An error occurred while deleting the view": "E Feeler ass beim Läsche vun der Usiicht opgetrueden", + "An error occurred while permanently deleting the objects": "E Feeler ass beim definitive Läsche vun den Objeten opgetrueden", + "An error occurred while restoring the objects": "Er is een fout opgetreden bij het herstellen van de objecten", + "An error occurred while saving the organisation": "Er is een fout opgetreden bij het opslaan van de organisatie", + "Analysis completed:": "Analyse ofgeschloss:", + "Analysis steps:": "Analyseschrëtt:", + "Analytics": "Analysen", + "Analyze Objects": "Objeten analyséieren", + "Analyze Properties": "Eegeschaften analyséieren", + "Analyze existing properties for improvement opportunities": "Existéierend Eegeschaften op Verbesserungsméiglechkeeten analyséieren", + "Analyzing...": "Gëtt analyséiert...", + "Anonymized": "Anonymiséiert", + "Anonymous Group": "Anonieme groep", + "Any favorites and sharing settings for this view": "All Favoritten- a Sharing-Astellunge fir dës Usiicht", + "Any user": "All Benotzer", + "App ID": "App-ID", + "App store cache invalidated successfully": "App-Store-Cache erfollegräich invalidéiert", + "App store cache invalidated: {invalidated}": "App store cache invalidated: {invalidated}", + "Application deleted successfully": "Applikatioun erfollegräich geläscht", + "Application identifier for this configuration (optional)": "Applicatie-identifier voor deze configuratie (optioneel)", + "Application settings": "Applicatie-instellingen", + "Application successfully created": "Applicatie succesvol aangemaakt", + "Application successfully updated": "Applicatie succesvol bijgewerkt", + "Applications": "Applikatiounen", + "Apply": "Toepassen", + "Apply Changes": "Ännerungen uwenden", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Wijs een rol toe aan alle schema's in dit register die geen expliciete autorisatie-overschrijvingen hebben.", + "Approval Progress": "Goedkeuringsvoortgang", + "Approve": "Goedkeuren", + "Approved": "Goedgekeurd", + "Archive": "Archiv", + "Archive conversation": "Gespréich archivéieren", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.", + "Archived conversations are hidden from your active list": "Archivéiert Gespréicher si vun ärer aktiver Lëscht verstoppt", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Weet u zeker dat u de collectie \"{name}\" wilt VERWIJDEREN?\n\nDit zal:\n• De collectie en alle bijbehorende gegevens permanent verwijderen\n• Alle geïndexeerde documenten verwijderen\n• Deze actie kan niet ongedaan worden gemaakt", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Sidd Dir sécher, datt Dir al Sich-Trails opraume wëllt? Dëst läscht Andréi déi méi al wéi 30 Deeg sinn.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Weet u zeker dat u alle gegevens uit collectie \"{name}\" wilt wissen?\n\nDit zal:\n• Alle geïndexeerde documenten verwijderen\n• De collectiestructuur intact houden\n• Deze actie kan niet ongedaan worden gemaakt", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Weet u zeker dat u de applicatie \"{name}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Sidd Dir sécher, datt Dir déi ausgewielten Audit-Trails läsche wëllt? Dës Aktioun kann net réckgängeg gemaach ginn.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Sidd Dir sécher, datt Dir déi ausgewielte Sich-Trails läsche wëllt? Dës Aktioun kann net réckgängeg gemaach ginn.", + "Are you sure you want to delete this ConfigSet?": "Sidd Dir sécher, datt Dir dëse ConfigSet läsche wëllt?", + "Are you sure you want to delete this configset?": "Weet u zeker dat u wilt verwijderen this ConfigSet?", + "Are you sure you want to leave '{name}'?": "Weet je zeker dat je '{name}' wilt verlaten?", + "Are you sure you want to permanently delete": "Sidd Dir sécher, datt Dir definitiv läsche wëllt", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Weet u zeker dat u collectie \"{name}\" opnieuw wilt indexeren?\n\nDit zal:\n• De index opnieuw opbouwen met alle objecten\n• Enkele minuten in beslag nemen\n• De zoekprestaties tijdens herindexering kunnen beïnvloeden", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Weet u zeker dat u het schema \"{schema}\" uit register \"{register}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Weet u zeker dat u {count} actieve objecten voor schema \"{schema}\" voorlopig wilt verwijderen?\n\nZe worden gemarkeerd als verwijderd, maar kunnen later permanent worden verwijderd.", + "Array Object Configuration:": "Array-objectconfiguratie:", + "Articles": "Artikelen", + "Ask a question...": "Eng Fro stellen...", + "Ask questions about your data using natural language": "Stellt Froen iwwer Är Donnéeën a natierlecher Sprooch", + "Assigned collaborative tags": "Toegewezen samenwerkingstags", + "Assignment notifications": "Toewijzingsmeldingen", + "Attachment added to {name}": "Attachment added to {name}", + "Attempt": "Versuch", + "Audit Trail Changes": "Audit-Trail-Ännerungen", + "Audit Trail Details": "Audit-Trail-Detailer", + "Audit Trail Management": "Audit-Trail-Verwaltung", + "Audit Trail Statistics": "Audit-Trail-Statistiken", + "Audit Trails": "Audit-Trails", + "Audit trail data copied to clipboard": "Audit-Trail-Donnéeën an d'Zwëschenoflag kopéiert", + "Audit trail deleted successfully": "Audit-Trail erfollegräich geläscht", + "Audit trail successfully deleted": "Audit-Trail erfollegräich geläscht", + "Authentication": "Authentifizéierung", + "Auto (e.g. Y for year, F Y for month)": "Auto (bijv. Y voor jaar, F Y voor maand)", + "Auto share": "Automatisch delen", + "Auto-commit disabled": "Auto-Commit desaktivéiert", + "Auto-commit enabled": "Auto-Commit aktivéiert", + "Auto-create Default Organisation": "Standardorganisatioun automatesch erstellen", + "Auto-retry failed vectorizations": "Feelgeschloen Vektoriséierungen automatesch nei probéieren", + "Automatically commit changes to SOLR index": "Ännerungen automatesch an de SOLR-Index committen", + "Automatically connect to the federation network.": "Automatisch verbinden met het federatienetwerk.", + "Automatically create a default organisation if none exists when the app is initialized": "Automatesch eng Standardorganisatioun erstellen wann keng existéiert wann d'App initialiséiert gëtt", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Generéiert automatesch Vektor-Embeddings aus Textstécker wann Dateien eropgelueden a verschafft ginn", + "Automatically generate vector embeddings when objects are created or updated": "Generéiert automatesch Vektor-Embeddings wann Objeten erstallt oder aktualiséiert ginn", + "Automatically retry failed vectorization attempts (max 3 retries)": "Feelgeschloen Vektoriséierungsversuche automatesch nei probéieren (max. 3 Versuche)", + "Automatically update catalogues.": "Catalogi automatisch bijwerken.", + "Available Filters": "Verfügbar Filteren", + "Available Workflows": "Verfügbar Workflows", + "Available placeholders:": "Beschikbare plaatshouders:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Avatarwijzigingen worden niet ondersteund door uw authenticatieprovider.", + "Avatar removed": "Avatar verwijderd", + "Avatar updated successfully": "Avatar succesvol bijgewerkt", + "Avg Execution Time": "Duerchschnëttlech Ausféierungszäit", + "Avg Members/Org": "Duerchschnëttlech Memberen/Org", + "Avg Object Views/Session": "Duerchschnëttlech Objet-Usiichten/Sessioun", + "Avg Response Time": "Duerchschnëttlech Äntwertzäit", + "Avg Results/Search": "Duerchschnëttlech Resultater/Sich", + "Avg Searches/Session": "Duerchschnëttlech Sichen/Sessioun", + "Back": "Zréck", + "Back to Entities": "Zréck zu den Entitéiten", + "Back to Registers": "Zréck zu de Registeren", + "Back to Webhooks": "Zréck zu de Webhooks", + "Back to applications": "Zréck zu den Applikatiounen", + "Back to entities": "Zréck zu den Entitéiten", + "Backend parameter is required": "Backend-Parameter ass erfuerderlech", + "Backend updated successfully. Please reload the application.": "Backend erfollegräich aktualiséiert. Lued d'Applikatioun w.e.g. nei.", + "Bandwidth Quota (MB/month)": "Bandbreedtequotum (MB/maand)", + "Base URL (Optional)": "Basis-URL (Optional)", + "Basic Information": "Basisinformatiounen", + "Batch Size": "Batch-Gréisst", + "Batch extraction completed": "Batch-Extraktioun ofgeschloss", + "Before": "Virdrun", + "Before object vectorization can work:": "Ier d'Objet-Vektoriséierung funktionéiere kann:", + "Behavior": "Gedrag", + "Behavior Issue": "Verhalensproblem", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob-Späicher gouf ofgeschaaft. All Objeten benotzen elo Magic-Tabellen.", + "Bookmarks": "Lieszeechen", + "Branch": "Branche", + "Bulk Role Assignment: {title}": "Bulkroltoewijzing: {title}", + "Bulk delete operation completed successfully": "Massel-Läschoperatioun erfollegräich ofgeschloss", + "Bulk save operation completed successfully": "Massel-Späicheroperatioun erfollegräich ofgeschloss", + "Business Data": "Geschäftsdonnéeën", + "CSV": "CSV", + "Cache cleared successfully": "Cache erfollegräich geläscht", + "Cache grew from {old} to {new} entries.": "Cache is gegroeid van {old} naar {new} vermeldingen.", + "Cache shrunk from {old} to {new} entries.": "Cache is gekrompen van {old} naar {new} vermeldingen.", + "Cache stayed the same at {size} entries.": "Cache is gelijk gebleven op {size} vermeldingen.", + "Calculate Sizes": "Gréissten berechnen", + "Calendar": "Agenda", + "Calendar Provider Configuration": "Agendaproviderconfiguratie", + "Calendar integration is not available": "Calendar integration is not available", + "Calendar name": "Agendanaam", + "Cancel": "Ofbriechen", + "Cancel deactivation request": "Deactiveringsverzoek annuleren", + "Cannot delete: objects are still attached": "Kann net läschen: Objeten sinn nach ugehaang", + "Cannot edit: This register is managed by external configuration {title}": "Kan niet bewerken: dit register wordt beheerd door externe configuratie {title}", + "Cannot remove schema with existing objects ({count} objects)": "Kan schema met bestaande objecten niet verwijderen ({count} objecten)", + "Cannot remove schema {schema} because it contains {count} objects": "Kan schema {schema} niet verwijderen omdat het {count} objecten bevat", + "Cards": "Kaarten", + "Cascade delete": "Cascade verwijderen", + "Categories of data subjects (one per line)": "Categorieën betrokkenen (één per regel)", + "Categories of personal data (one per line)": "Categorieën persoonsgegevens (één per regel)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategorie", + "Change Type": "Ännerungstyp", + "Change password": "Wachtwoord wijzigen", + "Changes": "Ännerungen", + "Changes copied to clipboard": "Ännerungen an d'Zwëschenoflag kopéiert", + "Changes for Audit Trail #{id}": "Wijzigingen voor auditspoor #{id}", + "Chat Model": "Chat-Modell", + "Chat Provider": "Chat-Provider", + "Chat Provider (RAG)": "Chat-Provider (RAG)", + "Chat Settings": "Chat-Astellungen", + "Chat provider connection successful!": "Verbindung mam Chat-Provider erfollegräich!", + "Chat settings": "Chat-Astellungen", + "Chat test failed: {error}": "Chattest mislukt: {error}", + "Choose a register": "E Register auswielen", + "Choose a schema": "E Schema auswielen", + "Choose how vector similarity calculations are performed for semantic search": "Wielt aus, wéi d'Vektorähnlechkeetsberechnunge fir d'semantesch Sich duerchgefouert ginn", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Wielt aus, wéi eng Dateitypen am Vektoriséierungsprozess abegraff ginn. Nëmmen Dateien mat extrahéiertem Text a Stécker ginn verschafft.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Wielt aus, wéi eng Usiichten am Vektoriséierungsprozess abegraff ginn. Loosst eidel fir all Usiichten op Basis vun ärer Konfiguratioun ze verschaffen.", + "Chunk": "Stéck", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Stéck-Läschung nach net ëmgesat. Benotzt stéckbaséiert Endpunkten.", + "Chunks": "Stécker", + "Chunks to Vectorize:": "Stécker fir ze vektoriséieren:", + "Cleanup Old Trails": "Al Trails opraumen", + "Cleanup completed": "Opraumen ofgeschloss", + "Cleanup completed successfully. Deleted {count} entries.": "Opruimen succesvol voltooid. {count} vermeldingen verwijderd.", + "Cleanup failed: {error}": "Opruimen mislukt: {error}", + "Clear All Embeddings": "All Embeddings läschen", + "Clear Entries": "Andréi läschen", + "Clear Filtered Audit Trails": "Gefiltert Audit-Trails läschen", + "Clear Filters": "Filteren läschen", + "Clear Index": "Index läschen", + "Clear Selection": "Auswiel läschen", + "Clear all": "Alles läschen", + "Clear all filters": "All Filteren läschen", + "Clear date range": "Datumsberäich läschen", + "Clear filters": "Filters wissen", + "Clearing...": "Gëtt geläscht...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.", + "Click to subscribe to notifications": "Click to subscribe to notifications", + "Click to unsubscribe from notifications": "Click to unsubscribe from notifications", + "Close": "Zoumaachen", + "Cluster name": "Clusternaam", + "Code": "Code", + "Code (short readable key, e.g. v-2026-001)": "Code (short readable key, e.g. v-2026-001)", + "Collapse details": "Details inklappen", + "Collection Management": "Sammlungsverwaltung", + "Collection Name": "Sammlungsnumm", + "Collection assignments updated successfully": "Sammlungszouweisunge erfollegräich aktualiséiert", + "Collection cleared successfully": "Sammlung erfollegräich geläscht", + "Collection copied successfully": "Sammlung erfollegräich kopéiert", + "Collection created successfully": "Sammlung erfollegräich erstallt", + "Collection deleted successfully": "Sammlung erfollegräich geläscht", + "Collection used to store and index file metadata and content": "Sammlung déi benotzt gëtt fir Datei-Metadonnéeën an -Inhalt ze späicheren an ze indexéieren", + "Collection used to store and index object data": "Sammlung déi benotzt gëtt fir Objet-Donnéeën ze späicheren an ze indexéieren", + "Collections:": "Sammlungen:", + "Color": "Kleur", + "Comment...": "Opmerking...", + "Commit Message": "Commit-Message", + "Commit Within (ms)": "Committen bannent (ms)", + "Commit message": "Commit-Message", + "Compare current schema with real object data": "Aktuelle Schema mat richtegen Objet-Donnéeë vergläichen", + "Completed": "Ofgeschloss", + "Complex": "Komplex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Komplex Ufroen: Erweidert Siche mat méi Filteren, Operatoren a komplexe Parameterkombinatiounen", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Complexe zoekopdrachten: geavanceerde zoekopdrachten met meerdere filters, operatoren en complexe parametercombinaties", + "Compliance": "Konformitéit", + "Confidence Level": "Vertrauensniveau", + "Confidence Score": "Vertrauensscore", + "Confidence: {confidence}%": "Betrouwbaarheid: {confidence}%", + "Config must be provided as an object": "Config muss als Objet uginn ginn", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet-Verwaltung", + "ConfigSet Name": "ConfigSet-Numm", + "ConfigSet created successfully": "ConfigSet erfollegräich erstallt", + "ConfigSet deleted successfully": "ConfigSet erfollegräich geläscht", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets definéieren de Schema an d'Konfiguratioun fir Är SOLR-Sammlungen. Si enthalen Felddefinitiounen, Analyzeren an aner Sichastellungen.", + "Configuration": "Konfiguratioun", + "Configuration Data": "Konfiguratiounsdonnéeën", + "Configuration is up to date": "Configuratie is actueel", + "Configuration saved": "Konfiguratioun gespäichert", + "Configuration saved successfully.": "Configuratie succesvol opgeslagen.", + "Configuration type (default, application, etc.)": "Configuratietype (standaard, applicatie, enz.)", + "Configurations": "Konfiguratiounen", + "Configure API tokens for external service integrations": "API-tokens configureren voor externe service-integraties", + "Configure Apache SOLR search engine for advanced search capabilities": "Apache SOLR-zoekmachine configureren voor geavanceerde zoekmogelijkheden", + "Configure Facets": "Facetten konfiguréieren", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfiguréiert Large-Language-Model- (LLM-) Provider fir KI-gestäipt Funktiounen, dorënner semantesch Sich, Embeddings a Chat.", + "Configure SOLR Facets": "SOLR-facetten configureren", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfiguréiert grondleeënd Verbindungsastellunge fir Äre SOLR-Server, dorënner Authentifizéierung an Netzoptiounen. Benotzt déi separat ConfigSet- a Sammlungsverwaltungsdialoge fir Cores a Sammlungen ze verwalten.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configure basic connection settings for your SOLR server including authentication and network options. use the separate configset and collection management dialogs to manage cores and collections.", + "Configure default organisation and organisation-related settings": "Standaardorganisatie en organisatiegerelateerde instellingen configureren", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfiguréiert, wéi Datebank-Objeten an Vektor-Embeddings fir d'semantesch Sich ëmgewandelt ginn. Objeten ginn direkt vektoriséiert ouni datt eng Textextraktioun néideg ass.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfiguréiert, wéi Objeten viru der Vektoriséierung an Text ëmgewandelt ginn. Dës Astellunge beaflossen d'Sichqualitéit an de Kontext.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Grote taalmodel (LLM)-providers configureren voor AI-functies inclusief semantisch zoeken, embeddings en chat.", + "Configure multi-organization support and tenant isolation": "Ondersteuning voor meerdere organisaties en tenantsisolatie configureren", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfiguréiert Parameter fir d'Datei-Vektoriséierung. Dëse Prozess generéiert Vektor-Embeddings fir all extrahéiert Dateistécker.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfiguréiert Parameter fir d'Objet-Vektoriséierung. Dëse Prozess generéiert Vektor-Embeddings fir all Objeten déi ären Usiicht-Filteren entspriechen.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Dit schema configureren om objecten als evenementen in de Nextcloud Agenda-app weer te geven.", + "Configure which types of data to search and how many sources to retrieve": "Konfiguréiert wéi eng Zorte vun Donnéeën duerchsicht ginn a wéi vill Quellen ofgeruff ginn", + "Configuring field mappings...": "Configuring field mappings...", + "Confirm Account Deactivation": "Accountdeactivering bevestigen", + "Confirm Object Deletion": "Objectverwijdering bevestigen", + "Confirm Object Restoration": "Objectherstel bevestigen", + "Confirm Permanent Object Deletion": "Permanente objectverwijdering bevestigen", + "Confirm deactivation": "Deactivering bevestigen", + "Connecting to SOLR cluster...": "Connecting to SOLR cluster...", + "Connection Failed": "Verbindung feelgeschloen", + "Connection Settings": "Verbindungsastellungen", + "Connection Successful!": "Verbindung erfollegräich!", + "Connection failed": "Verbindung feelgeschloen", + "Connection protocol": "Verbindungsprotokoll", + "Connection successful": "Verbindung erfollegräich", + "Connection timeout in seconds": "Verbindungs-Timeout a Sekonnen", + "Constant delay between retries (5 minutes)": "Konstant Verzögerung tëscht den Neiversuche (5 Minutten)", + "Constraint Issue": "Restriktiounsproblem", + "Consumer not found": "Consumer net fonnt", + "Contacts": "Kontakter", + "Contacts integration is not available": "Contacts integration is not available", + "Content Filters": "Inhaltsfilteren", + "Continue to Properties": "Weider zu den Eegeschaften", + "Contracts": "Contracten", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontrolléiert wéi eng Objet-Usiichte solle vektoriséiert ginn, fir d'API-Käschten ze reduzéieren. Vektoriséiert nëmmen Usiichten déi vun der semantescher Sich profitéieren.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontrolléiert wéi eng Usiichten an Tools d'KI an dësem Gespréich benotze kann. Standardméisseg sinn all Agent-Fäegkeeten aktivéiert.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontrolléiert d'Zoufällegkeet (0 = deterministesch, 2 = ganz kreativ)", + "Conversation ID is required": "Gespréichs-ID ass erfuerderlech", + "Conversation archived": "Gespréich archivéiert", + "Conversation archived successfully": "Gespréich erfollegräich archivéiert", + "Conversation cleared successfully": "Gespréich erfollegräich geläscht", + "Conversation deleted": "Gespréich geläscht", + "Conversation not found": "Gespréich net fonnt", + "Conversation permanently deleted": "Gespréich definitiv geläscht", + "Conversation renamed": "Gespréich ëmbenannt", + "Conversation restored": "Gespréich restauréiert", + "Conversation started with {agent}": "Gesprek gestart met {agent}", + "Conversation title": "Gespréichstitel", + "Conversations": "Gesprekken", + "Converting {schema} to blob storage...": "{schema} converteren naar blob-opslag...", + "Converting {schema} to magic table...": "{schema} converteren naar magische tabel...", + "Copied!": "Kopéiert!", + "Copy": "Kopéieren", + "Copy Changes": "Ännerunge kopéieren", + "Copy Collection": "Sammlung kopéieren", + "Copy Data": "Donnéeë kopéieren", + "Copy Full Data": "Vollstänneg Donnéeë kopéieren", + "Copy of {name}": "Kopie van {name}", + "Copy to clipboard": "Kopiëren naar klembord", + "Copying...": "Gëtt kopéiert...", + "Costs": "Käschten", + "Count": "Unzuel", + "Counting objects...": "Objeten ginn gezielt...", + "Create": "Erstellen", + "Create API Token": "API-token aanmaken", + "Create Agent": "Agent erstellen", + "Create Application": "Applikatioun erstellen", + "Create Collection": "Sammlung erstellen", + "Create ConfigSet": "ConfigSet erstellen", + "Create New Collection": "Nei Sammlung erstellen", + "Create New ConfigSet": "Neie ConfigSet erstellen", + "Create Operations": "Erstellungsoperatiounen", + "Create Organisation": "Organisatie aanmaken", + "Create Webhook": "Webhook erstellen", + "Create a copy of collection:": "Eng Kopie vun der Sammlung erstellen:", + "Create a new ConfigSet based on the _default template": "Engen neie ConfigSet op Basis vum _default-Template erstellen", + "Create a new SOLR collection from an existing ConfigSet": "Eng nei SOLR-Sammlung aus engem existéierende ConfigSet erstellen", + "Create a new SOLR collection from an existing configset": "Maak een nieuwe SOLR-collectie van een bestaande configset", + "Create a new configset based on the _default template": "Maak een nieuwe configset op basis van de _default-sjabloon", + "Create a new event or link an existing one from any of your calendars.": "Create a new event or link an existing one from any of your calendars.", + "Create another": "Nog een aanmaken", + "Create copies of": "Kopieën aanmaken van", + "Create event": "Evenement erstellen", + "Create new token": "Nieuw token aanmaken", + "Create or link a Deck card to track work on this object.": "Create or link a Deck card to track work on this object.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.", + "Create your first AI agent to get started.": "Erstellt Äre éischte KI-Agent fir unzefänken.", + "Created": "Erstallt", + "Created:": "Erstallt:", + "Creating search collections...": "Creating search collections...", + "Creating...": "Gëtt erstallt...", + "Current": "Huidig", + "Current Filters": "Aktuell Filteren", + "Current Type:": "Aktuellen Typ:", + "Current password": "Huidig wachtwoord", + "Custom API endpoint if using a different region": "Personaliséierten API-Endpunkt wann eng aner Regioun benotzt gëtt", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Personaliséiert HTTP-Headeren (een pro Linn, Format: Header-Numm: Wäert)", + "Custom HTTP headers (one per line, format: header-name: value)": "Aangepaste HTTP-headers (één per regel, formaat: header-naam: waarde)", + "Custom Ranges:": "Aangepaste bereiken:", + "Custom display title for this facet": "Aangepaste weergavetitel voor dit facet", + "Custom pattern:": "Aangepast patroon:", + "DELETE COLLECTION": "COLLECTIE VERWIJDEREN", + "DSAR": "DSAR", + "Daily": "Deeglech", + "Dashboard": "Dashboard", + "Dashboard not found": "Dashboard not found", + "Data": "Gegevens", + "Data Source": "Datequell", + "Data Sources": "Data brons", + "Data Views": "Gegevensweergaven", + "Data sources": "Datequellen", + "Data type does not match observed values": "Datentyp entsprécht net de beobachtete Wäerter", + "Data-subject access request": "Data-subject access request", + "Database URL": "Datebank-URL", + "Database information refreshed": "Datebankinformatiounen aktualiséiert", + "Date Faceting:": "Datumfacetten:", + "Date Range": "Datumsberäich", + "Dates": "Datums", + "Deactivation request cancelled": "Deactiveringsverzoek geannuleerd", + "Deactivation request submitted": "Deactiveringsverzoek ingediend", + "Deck": "Deck", + "Deck integration is not available": "Deck integration is not available", + "Default": "Standard", + "Default Object Tenant": "Standaard objecttenant", + "Default Organisation": "Standardorganisatioun", + "Default Owner": "Standaard eigenaar", + "Default User (for cron/background jobs)": "Standaard gebruiker (voor cron/achtergrondtaken)", + "Default User Tenant": "Standaard gebruikerstenant", + "Default value": "Standaardwaarde", + "Default value (Email)": "Standaardwaarde (E-mail)", + "Default value (Password)": "Standaardwaarde (Wachtwoord)", + "Default value (Phone number)": "Standaardwaarde (Telefoonnummer)", + "Default value (Regex)": "Standaardwaarde (Regex)", + "Default: 5": "Standard: 5", + "Defaults to current user. Select a different user if needed.": "Standaard de huidige gebruiker. Selecteer indien nodig een andere gebruiker.", + "Delays double with each attempt (2, 4, 8 minutes...)": "D'Verzögerunge verduebelen sech mat all Versuch (2, 4, 8 Minutten...)", + "Delays increase linearly (5, 10, 15 minutes...)": "D'Verzögerunge klamme linear (5, 10, 15 Minutten...)", + "Delete": "Läschen", + "Delete ({count})": "Verwijderen ({count})", + "Delete Application": "Applikatioun läschen", + "Delete Audit Trail": "Audit-Trail läschen", + "Delete Collection": "Sammlung läschen", + "Delete ConfigSet": "ConfigSet läschen", + "Delete Objects": "Objeten läschen", + "Delete Operations": "Läschoperatiounen", + "Delete SOLR Collection": "SOLR-collectie verwijderen", + "Delete View": "Usiicht läschen", + "Delete all objects in this schema": "All Objeten an dësem Schema läschen", + "Delete permanently": "Definitiv läschen", + "Delete this conversation?": "Dëst Gespréich läschen?", + "Delete view": "Usiicht läschen", + "Delete {count} object": "Delete {count} object", + "Delete {count} objects": "Delete {count} objects", + "Deleted": "Geläscht", + "Deleted By": "Geläscht vun", + "Deleted Date": "Läschdatum", + "Deleted Items Management": "Verwaltung vu geläschten Artikelen", + "Deleted This Week": "Dës Woch geläscht", + "Deleted Today": "Haut geläscht", + "Deleted:": "Geläscht:", + "Deleting SOLR Collection...": "SOLR-collectie verwijderen...", + "Deleting...": "Gëtt geläscht...", + "Deletion Date Range": "Läschdatumsberäich", + "Deletion Statistics": "Läschstatistiken", + "Deprecated": "Veroudert", + "Depublish": "Depubliceren", + "Description": "Beschreiwung", + "Description Template": "Beschrijvingssjabloon", + "Description shown as tooltip": "Beschrijving weergegeven als tooltip", + "Description:": "Beschreiwung:", + "Detect data types and patterns": "Datentypen a Musteren erkennen", + "Detected At": "Erkannt um", + "Detected Format:": "Erkannte Format:", + "Detected Issues:": "Erkannte Probleemer:", + "Detected: {date}": "Gedetecteerd: {date}", + "Direct permission": "Directe toestemming", + "Disable": "Desaktivéieren", + "Disable this to make the agent": "Schakel dit uit om de agent te maken", + "Disabled": "Desaktivéiert", + "Discover Files": "Dateien entdecken", + "Discovered Properties": "Entdeckt Eegeschaften", + "Display Format": "Weergaveformaat", + "Display Name": "Weergavenaam", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Wëllt Dir all gefiltert Audit-Trail-Andréi definitiv läschen? Dës Aktioun kann net réckgängeg gemaach ginn.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Wëllt Dir dësen Audit-Trail-Andrag definitiv läschen? Dës Aktioun kann net réckgängeg gemaach ginn.", + "Documents": "Dokumenter", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR ingeschakeld", + "Download API Spec": "API-Spezifikatioun eroflueden", + "Download API Specification": "API-specificatie downloaden", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Download een kopie van al uw persoonlijke gegevens opgeslagen in OpenRegister (AVG artikel 20).", + "Drag a file or files here": "Sleep een of meer bestanden hierheen", + "Dry run completed: {count} fields would be created": "Testrun voltooid: {count} velden zouden worden aangemaakt", + "Dry run completed: {count} fields would be fixed": "Testrun voltooid: {count} velden zouden worden hersteld", + "Duration in seconds (optional)": "Duur in seconden (optioneel)", + "Edit": "Beaarbechten", + "Edit Agent": "Agent bewerken", + "Edit Application": "Applicatie bewerken", + "Edit Configuration": "Configuratie bewerken", + "Edit Endpoint": "Eindpunt bewerken", + "Edit Organisation": "Organisatie bewerken", + "Edit Property '{key}' of '{title}'": "Eigenschap '{key}' van '{title}' bewerken", + "Edit Register": "Register beaarbechten", + "Edit Schema": "Schema beaarbechten", + "Edit Source": "Bron bewerken", + "Edit View": "Usiicht beaarbechten", + "Edit Webhook": "Webhook beaarbechten", + "Edit labels": "Labels bewerken", + "Edit processing activity": "Verwerkingsactiviteit bewerken", + "Edit tags for {name}": "Tags bewerken voor {name}", + "Edit verwerkingsactiviteit": "Edit verwerkingsactiviteit", + "Edit view": "Usiicht beaarbechten", + "Edit view details": "Usiicht-Detailer beaarbechten", + "Effectiveness": "Effektivitéit", + "Email": "E-Mail", + "Email Addresses": "E-mailadressen", + "Email digest frequency": "Frequentie e-maildigest", + "Emails": "E-Mailen", + "Embedding Model": "Embedding-Modell", + "Embedding Provider": "Embedding-Provider", + "Embedding generated successfully": "Embedding erfollegräich generéiert", + "Embedding provider connection successful!": "Verbindung mam Embedding-Provider erfollegräich!", + "Embedding test failed: {error}": "Embeddingtest mislukt: {error}", + "Enable": "Aktivéieren", + "Enable RAG": "RAG inschakelen", + "Enable auto-creation": "Automatesch Erstellung aktivéieren", + "Enable automatic file vectorization": "Automatesch Datei-Vektoriséierung aktivéieren", + "Enable automatic object vectorization": "Automatesch Objet-Vektoriséierung aktivéieren", + "Enable automatic synchronization": "Automatesch Synchroniséierung aktivéieren", + "Enable calendar provider": "Agendaprovider inschakelen", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Detailléiert Protokolléierung fir SOLR-Operatiounen aktivéieren (recommandéiert fir d'Debugging)", + "Enable faceting": "Facettéierung aktivéieren", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "LLM-Funktiounen aktivéieren oder desaktivéieren. Konfiguréiert Provider a Modeller mam LLM-Konfiguratiounsknäppchen hei uewen.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Enable or disable LLM features. configure providers and models using the LLM configuration button above.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "SOLR-Sichintegratioun aktivéieren oder desaktivéieren. Konfiguréiert d'Verbindungsastellunge mam Verbindungsastellungs-Knäppchen hei uewen.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Enable or disable SOLR search integration. configure connection settings using the connection settings button above.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "n8n-Workflow-Integratioun aktivéieren oder desaktivéieren. Konfiguréiert d'Verbindungsastellungen hei drënner.", + "Enable or disable this webhook": "Dëse Webhook aktivéieren oder desaktivéieren", + "Enable tools that allow the agent to interact with data through function calling.": "Schakel tools in waarmee de agent met gegevens kan werken via functieaanroepen.", + "Enable vectorization for all existing and future views (may increase costs)": "Vektoriséierung fir all existéierend an zukënfteg Usiichten aktivéieren (kann d'Käschten erhéijen)", + "Enable write-back to target objects": "Terugschrijven naar doelobjecten inschakelen", + "Enabled": "Aktivéiert", + "End Date Field": "Einddatumveld", + "Endpoint Path*": "Eindpuntpad*", + "Endpoints": "Endpunkten", + "Engine not found": "Engine net fonnt", + "Enter ConfigSet name": "ConfigSet-Numm aginn", + "Enter Nextcloud usernames to grant access to this private agent": "Voer Nextcloud-gebruikersnamen in om toegang te verlenen tot deze privéagent", + "Enter agent description (optional)": "Agentbeschrijving invoeren (optioneel)", + "Enter agent name": "Agentnaam invoeren", + "Enter application description (optional)": "Applicatiebeschrijving invoeren (optioneel)", + "Enter application name": "Applicatienaam invoeren", + "Enter collection name": "Sammlungsnumm aginn", + "Enter configuration description (optional)": "Configuratiebeschrijving invoeren (optioneel)", + "Enter configuration title": "Configuratietitel invoeren", + "Enter description (optional)...": "Beschreiwung aginn (optional)...", + "Enter new collection name": "Neie Sammlungsnumm aginn", + "Enter object ID": "Objet-ID aginn", + "Enter organisation description (optional)": "Organisatiebeschrijving invoeren (optioneel)", + "Enter organisation name": "Organisatienaam invoeren", + "Enter search term": "Sichbegrëff aginn", + "Enter search terms or leave empty to browse all": "Voer zoektermen in of laat leeg om alles te doorbladeren", + "Enter system prompt for the agent": "Systeemdprompt voor de agent invoeren", + "Enter username and press Enter": "Gebruikersnaam invoeren en op Enter drukken", + "Enter view name...": "Usiicht-Numm aginn...", + "Enter webhook name": "Webhook-Numm aginn", + "Enter your API key": "Voer uw API-sleutel in", + "Enter your object here...": "Voer uw object hier in...", + "Enter your schema here...": "Voer uw schema hier in...", + "Entities": "Entitéiten", + "Entities detected": "Entitéiten erkannt", + "Entity ID: {id}": "Entiteit-ID: {id}", + "Entity Information": "Entitéitsinformatiounen", + "Entity Recognition": "Entiteitsherkenning", + "Entity Recognition Method": "Methode voor entiteitsherkenning", + "Entity deleted successfully": "Entitéit erfollegräich geläscht", + "Entity not found": "Entitéit net fonnt", + "Entries to be deleted:": "Andréi déi geläscht ginn:", + "Enum Issue": "Enum-Problem", + "Enum constraint is missing": "Enum-Restriktioun feelt", + "Erase (Art 17)": "Erase (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Erase {count} object(s) for this subject? This action is logged in the audit trail.", + "Erasure complete": "Läschung ofgeschloss", + "Erasure preview": "Läschungsvirschau", + "Error": "Feeler", + "Error Details": "Feeler-Detailer", + "Error Information": "Feeler-Informatiounen", + "Error Message": "Feeler-Message", + "Error deleting audit trails: {error}": "Fout bij verwijderen auditsporen: {error}", + "Error deleting search trails: {error}": "Fout bij verwijderen zoeksporen: {error}", + "Error loading application": "Feeler beim Lueden vun der Applikatioun", + "Error loading audit trails": "Feeler beim Lueden vun den Audit-Trails", + "Error loading entity": "Feeler beim Lueden vun der Entitéit", + "Error loading search trails": "Feeler beim Lueden vun de Sich-Trails", + "Estimated Batches:": "Geschätzte Batchen:", + "Estimated Cost:": "Geschätzte Käschten:", + "Estimated Duration:": "Geschätzten Dauer:", + "Event": "Evenement", + "Event Property for Payload": "Evenement-Eegeschaft fir Payload", + "Events": "Evenementer", + "Every schema with detected PII has a processing-activity annotation.": "Every schema with detected PII has a processing-activity annotation.", + "Example": "Voorbeeld", + "Example Value": "Beispillwäert", + "Example value for this property": "Voorbeeldwaarde voor deze eigenschap", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Exklusiivt Maximum", + "Exclusive Minimum": "Exklusiivt Minimum", + "Exclusive maximum": "Exclusief maximum", + "Exclusive minimum": "Exclusief minimum", + "Execution Mode": "Ausféierungsmodus", + "Execution Time": "Ausféierungszäit", + "Execution Time Range": "Ausféierungszäitberäich", + "Existing Improvements": "Existéierend Verbesserungen", + "Expand details": "Detailer opklappen", + "Expiration": "Vervaldatum", + "Expires": "Verloopt", + "Expires in (e.g., 90d)": "Verloopt over (bijv. 90d)", + "Exponential": "Exponentiell", + "Export": "Exportéieren", + "Export \"{schema}\" objects from \"{register}\"": "Objecten van \"{schema}\" exporteren uit \"{register}\"", + "Export Configuration": "Configuratie exporteren", + "Export Format:": "Exportformaat:", + "Export Objects": "Objecten exporteren", + "Export completed successfully": "Export erfollegräich ofgeschloss", + "Export downloaded successfully": "Export succesvol gedownload", + "Export failed: {error}": "Export mislukt: {error}", + "Export is rate limited. Please try again later.": "Export is snelheidsbegrensd. Probeer het later opnieuw.", + "Export my data": "Mijn gegevens exporteren", + "Export, view, or delete audit trails": "Audit-Trails exportéieren, ukucken oder läschen", + "Exporting...": "Exporteren...", + "Extend Schema": "Schema erweideren", + "Extended": "Erweidert", + "Extensions": "Erweiderungen", + "Extra Query Parameters": "Extra queryparameters", + "Extract Now": "Elo extrahéieren", + "Extract Pending Files": "Ausstoend Dateien extrahéieren", + "Extract now": "Nu extraheren", + "Extract properties from each object": "Eegeschaften aus all Objet extrahéieren", + "Extracted At": "Extrahéiert um", + "Extracted at": "Extrahéiert um", + "Extraction": "Extraktioun", + "Extraction Mode": "Extractiemodus", + "Extraction Scope": "Extractiebereik", + "Extraction Status": "Extraktiounsstatus", + "Extraction error for {file}: {error}": "Extractiefout voor {file}: {error}", + "Facet Description": "Facetbeschrijving", + "Facet Order": "Facetvolgorde", + "Facet Title": "Facettitel", + "Facet Type": "Facettype", + "Facet configuration updated successfully": "Facettekonfiguratioun erfollegräich aktualiséiert", + "Facetable": "Facetteerbaar", + "Faceting Configuration:": "Facetconfiguratie:", + "Facets discovered and configured successfully": "Facetten erfollegräich entdeckt a konfiguréiert", + "Facets discovered successfully": "Facetten erfollegräich entdeckt", + "Failed": "Feelgeschloen", + "Failed to add attachment to object": "Failed to add attachment to object", + "Failed to add user to organisation": "Kan gebruiker niet toevoegen aan organisatie", + "Failed to analyze schema properties": "Analyse vun de Schema-Eegeschafte feelgeschloen", + "Failed to archive conversation": "Archivéiere vum Gespréich feelgeschloen", + "Failed to calculate sizes": "Berechnung vun de Gréisste feelgeschloen", + "Failed to cancel deactivation": "Kan deactivering niet annuleren", + "Failed to change active organisation: {error}": "Kan actieve organisatie niet wijzigen: {error}", + "Failed to change password": "Kan wachtwoord niet wijzigen", + "Failed to check version: {error}": "Kan versie niet controleren: {error}", + "Failed to clear audit trails: {error}": "Failed to clear audit trails: {error}", + "Failed to clear blob objects: {error}": "Failed to clear blob objects: {error}", + "Failed to clear cache: {error}": "Failed to clear cache: {error}", + "Failed to clear collection": "Läsche vun der Sammlung feelgeschloen", + "Failed to clear conversation": "Läsche vum Gespréich feelgeschloen", + "Failed to clear embeddings: {error}": "Kan embeddings niet wissen: {error}", + "Failed to clear search trails: {error}": "Failed to clear search trails: {error}", + "Failed to clear {type} cache: {error}": "Failed to clear {type} cache: {error}", + "Failed to copy changes": "Kopéiere vun den Ännerunge feelgeschloen", + "Failed to copy data": "Kopéiere vun den Donnéeë feelgeschloen", + "Failed to copy data to clipboard": "Kopéiere vun den Donnéeën an d'Zwëschenoflag feelgeschloen", + "Failed to copy to clipboard": "Kan niet naar klembord kopiëren", + "Failed to copy token": "Kan token niet kopiëren", + "Failed to create conversation": "Erstelle vum Gespréich feelgeschloen", + "Failed to create missing SOLR fields: {error}": "Kan ontbrekende SOLR-velden niet aanmaken: {error}", + "Failed to create or find project": "Erstellen oder Fanne vum Projet feelgeschloen", + "Failed to create token": "Kan token niet aanmaken", + "Failed to delete SOLR collection: {error}": "Kan SOLR-collectie niet verwijderen: {error}", + "Failed to delete collection": "Läsche vun der Sammlung feelgeschloen", + "Failed to delete conversation": "Läsche vum Gespréich feelgeschloen", + "Failed to delete objects for {schema}: {error}": "Kan objecten voor {schema} niet verwijderen: {error}", + "Failed to delete webhook": "Läsche vum Webhook feelgeschloen", + "Failed to depublish register: {error}": "Kan register niet depubliceren: {error}", + "Failed to depublish schema: {error}": "Kan schema niet depubliceren: {error}", + "Failed to depublish: {error}": "Kan niet depubliceren: {error}", + "Failed to discover files: {error}": "Failed to discover files: {error}", + "Failed to download API specification": "Eroflueden vun der API-Spezifikatioun feelgeschloen", + "Failed to export data": "Kan gegevens niet exporteren", + "Failed to export objects": "Kan objecten niet exporteren", + "Failed to fetch conversation history": "Ofruff vun der Gespréichshistorie feelgeschloen", + "Failed to fix mismatched SOLR fields: {error}": "Kan niet-overeenkomende SOLR-velden niet herstellen: {error}", + "Failed to get SOLR field configuration": "Ofruff vun der SOLR-Feldkonfiguratioun feelgeschloen", + "Failed to get chat statistics": "Ofruff vun de Chat-Statistike feelgeschloen", + "Failed to get database information: %s": "Ofruff vun den Datebankinformatioune feelgeschloen: %s", + "Failed to get entity categories": "Ofruff vun den Entitéitskategorien feelgeschloen", + "Failed to get entity statistics": "Ofruff vun den Entitéitsstatistike feelgeschloen", + "Failed to get entity types": "Ofruff vun den Entitéitstypen feelgeschloen", + "Failed to get response: {error}": "Kan geen antwoord ontvangen: {error}", + "Failed to invalidate app store cache: {error}": "Failed to invalidate app store cache: {error}", + "Failed to leave organisation: {error}": "Kan organisatie niet verlaten: {error}", + "Failed to link object": "Kan object niet koppelen", + "Failed to load Deck cards": "Failed to load Deck cards", + "Failed to load LLM configuration": "Lueden vun der LLM-Konfiguratioun feelgeschloen", + "Failed to load Nextcloud groups": "Lueden vun den Nextcloud-Gruppen feelgeschloen", + "Failed to load cache statistics: {error}": "Failed to load cache statistics: {error}", + "Failed to load conversation": "Lueden vum Gespréich feelgeschloen", + "Failed to load entities": "Lueden vun den Entitéiten feelgeschloen", + "Failed to load entity": "Lueden vun der Entitéit feelgeschloen", + "Failed to load extraction data": "Lueden vun den Extraktiounsdonnéeë feelgeschloen", + "Failed to load files": "Lueden vun den Dateien feelgeschloen", + "Failed to load linked contacts": "Failed to load linked contacts", + "Failed to load linked emails": "Failed to load linked emails", + "Failed to load linked events": "Failed to load linked events", + "Failed to load organisations": "Lueden vun den Organisatioune feelgeschloen", + "Failed to load register data": "Lueden vun den Registerdonnéeë feelgeschloen", + "Failed to load related entities": "Failed to load related entities", + "Failed to load settings: {error}": "Failed to load settings: {error}", + "Failed to load statistics: {error}": "Failed to load statistics: {error}", + "Failed to load templates": "Lueden vun den Templater feelgeschloen", + "Failed to load view: {error}": "Kan weergave niet laden: {error}", + "Failed to load webhooks": "Lueden vun de Webhooks feelgeschloen", + "Failed to load workflows": "Lueden vun de Workflows feelgeschloen", + "Failed to process message": "Verschaffe vum Message feelgeschloen", + "Failed to publish register: {error}": "Kan register niet publiceren: {error}", + "Failed to publish schema: {error}": "Kan schema niet publiceren: {error}", + "Failed to publish: {error}": "Kan niet publiceren: {error}", + "Failed to refresh database information": "Aktualiséiere vun den Datebankinformatioune feelgeschloen", + "Failed to reindex collection": "Nei-Indexéiere vun der Sammlung feelgeschloen", + "Failed to remove avatar": "Kan avatar niet verwijderen", + "Failed to remove link": "Kan koppeling niet verwijderen", + "Failed to remove member: {error}": "Kan lid niet verwijderen: {error}", + "Failed to remove schema {schema}: {error}": "Kan schema {schema} niet verwijderen: {error}", + "Failed to remove user from organisation": "Kan gebruiker niet uit organisatie verwijderen", + "Failed to rename conversation": "Ëmbenenne vum Gespréich feelgeschloen", + "Failed to request deactivation": "Kan geen deactiveringsverzoek indienen", + "Failed to restore conversation": "Restauréiere vum Gespréich feelgeschloen", + "Failed to retrieve extraction status.": "Ofruff vum Extraktiounsstatus feelgeschloen.", + "Failed to retrieve objects for file.": "Ofruff vun den Objete fir d'Datei feelgeschloen.", + "Failed to retry extraction": "Neie Versuch vun der Extraktioun feelgeschloen", + "Failed to retry webhook": "Neie Versuch vum Webhook feelgeschloen", + "Failed to revoke token": "Kan token niet intrekken", + "Failed to save GitHub token": "Späichere vum GitHub-Token feelgeschloen", + "Failed to save GitLab URL": "Späichere vun der GitLab-URL feelgeschloen", + "Failed to save GitLab token": "Späichere vum GitLab-Token feelgeschloen", + "Failed to save LLM settings: {error}": "Failed to save LLM settings: {error}", + "Failed to save additional feedback": "Späichere vum zousätzleche Feedback feelgeschloen", + "Failed to save configuration.": "Kan configuratie niet opslaan.", + "Failed to save configuration: {error}": "Kan configuratie niet opslaan: {error}", + "Failed to save facet configuration": "Kan facetconfiguratie niet opslaan", + "Failed to save feedback": "Späichere vum Feedback feelgeschloen", + "Failed to save file settings: {error}": "Failed to save file settings: {error}", + "Failed to save n8n configuration": "Späichere vun der n8n-Konfiguratioun feelgeschloen", + "Failed to save preferences": "Kan voorkeuren niet opslaan", + "Failed to save roles": "Späichere vun de Rolle feelgeschloen", + "Failed to save settings": "Späichere vun den Astellunge feelgeschloen", + "Failed to save settings: {error}": "Failed to save settings: {error}", + "Failed to save view: {error}": "Kan weergave niet opslaan: {error}", + "Failed to save warmup interval: {error}": "Failed to save warmup interval: {error}", + "Failed to save webhook": "Späichere vum Webhook feelgeschloen", + "Failed to send feedback": "Schécke vum Feedback feelgeschloen", + "Failed to set active organisation: {error}": "Kan actieve organisatie niet instellen: {error}", + "Failed to setup SOLR: {error}": "Failed to setup SOLR: {error}", + "Failed to start file warmup: {error}": "Kan bestandsopwarming niet starten: {error}", + "Failed to start processing {type} files: {error}": "Failed to start processing {type} files: {error}", + "Failed to start vectorization: {error}": "Kan vectorisatie niet starten: {error}", + "Failed to switch organisation: {error}": "Kan van organisatie niet wisselen: {error}", + "Failed to sync magic table for {schema}: {error}": "Kan magische tabel voor {schema} niet synchroniseren: {error}", + "Failed to test SOLR connection: {error}": "Failed to test SOLR connection: {error}", + "Failed to test webhook": "Testen vum Webhook feelgeschloen", + "Failed to update LLM settings: {error}": "Failed to update LLM settings: {error}", + "Failed to update Multitenancy settings: {error}": "Failed to update Multitenancy settings: {error}", + "Failed to update RBAC settings: {error}": "Failed to update RBAC settings: {error}", + "Failed to update Retention settings: {error}": "Failed to update Retention settings: {error}", + "Failed to update SOLR settings: {error}": "Failed to update SOLR settings: {error}", + "Failed to update favorite status": "Aktualiséiere vum Favoritte-Status feelgeschloen", + "Failed to update schema configuration for {schema}: {error}": "Kan schemaconfiguratie voor {schema} niet bijwerken: {error}", + "Failed to update schema properties": "Aktualiséiere vun de Schema-Eegeschafte feelgeschloen", + "Failed to update view": "Aktualiséiere vun der Usiicht feelgeschloen", + "Failed to update view: {error}": "Kan weergave niet bijwerken: {error}", + "Failed to update webhook": "Aktualiséiere vum Webhook feelgeschloen", + "Failed to upload avatar": "Kan avatar niet uploaden", + "Failed to validate {schema}: {error}": "Kan {schema} niet valideren: {error}", + "Failed to vectorize files: {error}": "Kan bestanden niet vectoriseren: {error}", + "Failed to warmup names cache: {error}": "Kan naamencache niet opwarmen: {error}", + "Federation": "Federatie", + "Feedback recorded": "Feedback registréiert", + "Fewer sources (1-3):": "Manner Quellen (1-3):", + "Field": "Feld", + "Fields to Display": "Weer te geven velden", + "File": "Datei", + "File #{id}": "Bestand #{id}", + "File Chunk Prediction": "Datei-Stéck-Prognos", + "File Collection": "Datei-Sammlung", + "File Handling": "Bestandsverwerking", + "File Location": "Bestandslocatie", + "File Management": "Datei-Verwaltung", + "File Name": "Dateinumm", + "File Path": "Dateipad", + "File Type Selection": "Dateityp-Auswiel", + "File Types to Process": "Te verwerken bestandstypen", + "File Vectorization": "Datei-Vektoriséierung", + "File Warmup": "Datei-Warmup", + "File actions menu": "Datei-Aktiounsmenü", + "File anonymized successfully": "Datei erfollegräich anonymiséiert", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "D'Datei-Stéck-Vektoriséierung ass nach net ëmgesat. D'Stécker si prett a gespäichert, awer de Vektoriséierungsservice ass an der Entwécklung.", + "File collection not configured": "Datei-Sammlung net konfiguréiert", + "File discovery completed": "Datei-Entdeckung ofgeschloss", + "File extraction completed": "Datei-Extraktioun ofgeschloss", + "File extraction queued": "Datei-Extraktioun an d'Schlaang gesat", + "File indexed successfully": "Datei erfollegräich indexéiert", + "File is already anonymized": "Datei ass scho anonymiséiert", + "File name": "Bestandsnaam", + "File not found": "Datei net fonnt", + "File settings saved successfully": "File settings saved successfully", + "File settings updated successfully": "Datei-Astellunge erfollegräich aktualiséiert", + "File sources": "Datei-Quellen", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Bestandsvectorisatie voltooid! {vectorized} fragmenten gevectoriseerd uit {files} bestanden. {failed} mislukt.", + "File vectorization configuration saved successfully": "Datei-Vektoriséierungskonfiguratioun erfollegräich gespäichert", + "File vectorization started. Check the statistics section for progress.": "Datei-Vektoriséierung gestart. Iwwerpréift den Statistik-Beräich fir de Fortschrëtt.", + "File warmup completed": "Datei-Warmup ofgeschloss", + "Files": "Dateien", + "Files added successfully": "Bestanden succesvol toegevoegd", + "Files with Completed Extraction:": "Dateie mat ofgeschlossener Extraktioun:", + "Files → fileCollection, Objects → objectCollection": "Dateien → fileCollection, Objeten → objectCollection", + "Files → filecollection, objects → objectcollection": "Files → bestandCollection, Objects → objectCollection", + "Filter Audit Trails": "Audit-Trails filteren", + "Filter Deleted Items": "Geläscht Artikele filteren", + "Filter Objects": "Objeten filteren", + "Filter Properties": "Eegeschafte filteren", + "Filter Search Trails": "Sich-Trails filteren", + "Filter Statistics": "Statistike filteren", + "Filter and analyze search trail entries": "Sich-Trail-Andréi filteren an analyséieren", + "Filter and manage audit trail entries": "Audit-Trail-Andréi filteren a verwalten", + "Filter and manage soft deleted items": "Mëll geläscht Artikele filteren a verwalten", + "Filter and search entities": "Filter and search entities", + "Filter by object ID": "No Objet-ID filteren", + "Filter by search term": "No Sichbegrëff filteren", + "Filter by type": "Filteren op type", + "Filter by webhook": "No Webhook filteren", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filterdonnéeën automatesch gelueden. Benotzt d'Filteren hei drënner fir Är Sich ze verfeineren.", + "Filter fields...": "Velden filteren...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Webhook-Trigger no Payload-Eegeschafte filteren (een pro Linn, Format: Schlëssel: Wäert)", + "Filtered": "Gefiltert", + "Filters": "Filteren", + "Finalizing setup...": "Finalizing setup...", + "Fireworks AI Chat Settings": "Fireworks-AI-Chat-Astellungen", + "Fireworks AI Embedding Configuration": "Fireworks-AI-Embedding-Konfiguratioun", + "First": "Éischt", + "Fixed": "Behuewen", + "For chat and retrieval-augmented generation": "Fir Chat a Retrieval-Augmented Generation", + "For vector embeddings and semantic search": "Fir Vektor-Embeddings a semantesch Sich", + "Format": "Format", + "Format Issue": "Formatproblem", + "Format constraint is missing": "Format-Restriktioun feelt", + "From (e.g. -7 days or 2025-01-01)": "Van (bijv. -7 dagen of 2025-01-01)", + "From Date": "Vum Datum", + "From date": "Vum Datum", + "Full data copied to clipboard": "Vollstänneg Donnéeën an d'Zwëschenoflag kopéiert", + "Function Tools": "Functietools", + "General Issue": "Allgemenge Problem", + "Generate recommendations and confidence scores": "Recommandatiounen a Vertrauensscoren generéieren", + "Generate report": "Generate report", + "Generate the verantwoordingsdocument": "Generate the verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generéiert direkt Vektoren wann nei Objeten erstallt ginn", + "Generated: {time}": "Generated: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Krijg realtime inzicht in de datakwaliteit, groeitrends en kwaliteitsmetingen van je organisatie.", + "GitHub Personal Access Token": "GitHub Persoonlijk Toegangstoken", + "GitHub token is valid": "GitHub-Token ass gëlteg", + "GitHub token is valid! Username: {username}": "GitHub-token is geldig! Gebruikersnaam: {username}", + "GitHub token removed successfully": "GitHub-Token erfollegräich ewechgeholl", + "GitHub token saved successfully": "GitHub-Token erfollegräich gespäichert", + "GitHub token test failed: {message}": "GitHub-tokentest mislukt: {message}", + "GitLab Personal Access Token": "GitLab Persoonlijk Toegangstoken", + "GitLab URL saved successfully": "GitLab-URL erfollegräich gespäichert", + "GitLab token is valid": "GitLab-Token ass gëlteg", + "GitLab token is valid! Username: {username}": "GitLab-token is geldig! Gebruikersnaam: {username}", + "GitLab token saved successfully": "GitLab-Token erfollegräich gespäichert", + "GitLab token test failed: {message}": "GitLab-tokentest mislukt: {message}", + "Go to linked Register": "Ga naar gekoppeld register", + "Go to linked Schema": "Ga naar gekoppeld schema", + "Go to organisation": "Ga naar organisatie", + "Grant additional permissions beyond standard CRUD operations": "Aanvullende machtigingen verlenen buiten standaard CRUD-bewerkingen", + "Group Quota": "Groepsquotum", + "Groups": "Groepen", + "HTML preview": "HTML preview", + "HTTP Method": "HTTP-Method", + "HTTP method used to send webhook requests": "HTTP-Method déi benotzt gëtt fir Webhook-Ufroen ze schécken", + "Headers": "Headeren", + "Health": "Gesondheet", + "Heartbeat successful - connection kept alive": "Heartbeat erfollegräich - Verbindung um Liewen gehalen", + "Helpful": "Hëllefräich", + "Here you can configure the details for various connections.": "Hier kunt u de details voor verschillende verbindingen configureren.", + "Here you can configure the details for your organisation.": "Hier kunt u de details voor uw organisatie configureren.", + "Hide": "Verbergen", + "Hide Filters": "Filteren verstoppen", + "Hide in collection view": "Verbergen in collectieweergave", + "Hide in forms": "An de Formularen verstoppen", + "Hide in list view": "An der Lëschtenusiicht verstoppen", + "High": "Héich", + "High Confidence": "Héicht Vertrauen", + "Host": "Host", + "Hourly": "Stënnlech", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Wéi déif verschachtelt Objet-Eegeschaften duerchlaf ginn (1-20). Méi héich Wäerter erfaassen méi Detail, awer erhéijen d'Vektorgréisst.", + "How often to check for updates (1-168 hours)": "Wéi dacks no Aktualiséierungen iwwerpréift gëtt (1-168 Stonnen)", + "How to handle retries for failed webhook deliveries": "Wéi mat den Neiversuche fir feelgeschloen Webhook-Liwwerunge fonctionéiert gëtt", + "IBANs": "IBAN's", + "ID": "ID", + "ID of input mapping (optional)": "ID van invoertoewijzing (optioneel)", + "ID of output mapping (optional)": "ID van uitvoertoewijzing (optioneel)", + "ID of the target resource": "ID van de doelresource", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "IP-Rate-Limiten erfollegräich geläscht", + "Identify properties not in the schema": "Eegeschaften déi net am Schema sinn identifizéieren", + "Immutable": "Onverännerbar", + "Import": "Importéieren", + "Import Configuration": "Configuratie importeren", + "Import Data into Register": "Gegevens importeren in register", + "Import successful": "Import erfollegräich", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.", + "Improved Property": "Verbessert Eegeschaft", + "In use": "A Gebrauch", + "Inactive": "Inaktiv", + "Inactive organisations cannot be used": "Inactieve organisaties kunnen niet worden gebruikt", + "Include IDs and names of related objects for better contextual search": "IDen an Nimm vu verbonnenen Objeten abezéie fir eng besser kontextuell Sich", + "Include ids and names of related objects for better contextual search": "Include IDs and names of related objecten for better contextual search", + "Include related object references": "Verbonne Objet-Referenzen abezéien", + "Include schema and register metadata": "Schema- a Register-Metadonnéeën abezéien", + "Index": "Index", + "Information about the current OpenRegister installation": "Informatie over de huidige OpenRegister-installatie", + "Inherited from register": "Overgeërfd van register", + "Inherits permissions from register": "Erft machtigingen van register", + "Initialization failed": "Initialiséierung feelgeschloen", + "Initialize Project": "Projet initialiséieren", + "Initializing SOLR setup...": "Initializing SOLR setup...", + "Input Mapping": "Invoertoewijzing", + "Inspect Fields": "Felder inspizéieren", + "Inspect Index": "Index inspizéieren", + "Inspect SOLR Index": "SOLR-index inspecteren", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installéiert d'notify_push-App aus dem Nextcloud-App-Store fir Echtzäit-Aktualiséierungen z'aktivéieren.", + "Integrations": "Integrations", + "Internet location (URL) of this installation": "Internetlocatie (URL) van deze installatie", + "Interval": "Interval", + "Invalid": "Ongëlteg", + "Invalid batch size. Must be between 1 and 5000": "Ongëlteg Batch-Gréisst. Muss tëscht 1 a 5000 sinn", + "Invalid feedback type": "Ongëltegen Feedback-Typ", + "Invalid field name provided": "Ongëltege Feldnumm uginn", + "Invalid maxObjects. Must be 0 (all) or positive number": "Ongëlteg maxObjects. Muss 0 (all) oder eng positiv Zuel sinn", + "Invalid:": "Ongëlteg:", + "Invite Users": "Gebruikers uitnodigen", + "Invited Users": "Uitgenodigde gebruikers", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Inzage results", + "Issue": "Problem", + "Items per page:": "Artikele pro Säit:", + "Just now": "Eben elo", + "Key": "Sleutel", + "LLM Configuration": "LLM-Konfiguratioun", + "LLM actions menu": "LLM-Aktiounsmenü", + "LLM configuration saved successfully": "LLM-Konfiguratioun erfollegräich gespäichert", + "LLM features disabled": "LLM-Funktiounen desaktivéiert", + "LLM features enabled": "LLM-Funktiounen aktivéiert", + "LLM must be enabled with an embedding provider configured": "LLM muss aktivéiert sinn mat engem konfiguréierten Embedding-Provider", + "LLM settings saved successfully": "LLM settings saved successfully", + "LLM settings updated successfully": "LLM-Astellunge erfollegräich aktualiséiert", + "Label": "Label", + "Labels": "Labels", + "Languages": "Languages", + "Last": "Lescht", + "Last Triggered": "Lescht ausgeléist", + "Leave empty to allow all users access": "Leeg laten om alle gebruikers toegang te geven", + "Leave off for auto-detection from property format.": "Uitschakelen voor automatische detectie vanuit eigenschapsformaat.", + "Left organisation successfully": "Organisatie succesvol verlaten", + "Legal basis *": "Rechtsgrond *", + "Length Range:": "Längtberäich:", + "Linear": "Linear", + "Link": "Koppeling", + "Link an email from the Mail app sidebar to associate it with this object.": "Link an email from the Mail app sidebar to associate it with this object.", + "Link an object to see it here.": "Koppel een object om het hier te zien.", + "Link another object": "Nog een object koppelen", + "Link existing event": "Link existing event", + "Link removed": "Koppeling verwijderd", + "Link to Object": "Koppeling naar object", + "Link to {name}": "Koppeling naar {name}", + "Linked Objects": "Gekoppelde objecten", + "Linked by {user}": "Gekoppeld door {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Linked emails, calendar events, contacts and Deck cards will appear here.", + "Linked to {name}": "Gekoppeld aan {name}", + "Load Advanced Filters": "Erweidert Filtere lueden", + "Load advanced filters with live data from your search index": "Erweidert Filtere mat Live-Donnéeën aus ärem Sichindex lueden", + "Load more": "Meer laden", + "Load view": "Usiicht lueden", + "Loading API tokens...": "API-tokens laden...", + "Loading ConfigSets...": "ConfigSets gi gelueden...", + "Loading LLM configuration...": "LLM-configuratie laden...", + "Loading RBAC settings...": "RBAC-instellingen laden...", + "Loading activity...": "Activiteit laden...", + "Loading advanced filters...": "Erweidert Filtere gi gelueden...", + "Loading agents...": "Agente gi gelueden...", + "Loading application details...": "Applikatiouns-Detailer gi gelueden...", + "Loading applications...": "Applikatioune gi gelueden...", + "Loading archived conversations...": "Archivéiert Gespréicher gi gelueden...", + "Loading audit trails...": "Audit-Trails gi gelueden...", + "Loading available tools...": "Beschikbare tools laden...", + "Loading cache statistics...": "Cache-statistieken laden...", + "Loading collections...": "Sammlunge gi gelueden...", + "Loading configurations...": "Konfiguratioune gi gelueden...", + "Loading conversation...": "Gespréich gëtt gelueden...", + "Loading conversations...": "Gespréicher gi gelueden...", + "Loading deleted items...": "Geläscht Artikele gi gelueden...", + "Loading endpoints": "Eindpunten laden", + "Loading entities...": "Entiteiten laden...", + "Loading events...": "Evenementer gi gelueden...", + "Loading filter data...": "Filterdonnéeë gi gelueden...", + "Loading groups...": "Gruppe gi gelueden...", + "Loading linked objects...": "Gekoppelde objecten laden...", + "Loading log details...": "Protokoll-Detailer gi gelueden...", + "Loading multitenancy settings...": "Multitenancy-instellingen laden...", + "Loading n8n configuration...": "n8n-configuratie laden...", + "Loading permission matrix...": "Machtigingsmatrix laden...", + "Loading preferences...": "Voorkeuren laden...", + "Loading register data...": "Registergegevens laden...", + "Loading registers...": "Registere gi gelueden...", + "Loading retention settings...": "Bewaarinstellingen laden...", + "Loading schemas...": "Schemaen gi gelueden...", + "Loading search configuration...": "Zoekconfiguratie laden...", + "Loading search trails...": "Sich-Trails gi gelueden...", + "Loading sources...": "Quellen gi gelueden...", + "Loading statistics...": "Statistike gi gelueden...", + "Loading text extraction settings...": "Instellingen voor tekstextractie laden...", + "Loading tokens...": "Tokens laden...", + "Loading user groups...": "Gebruikersgroepen laden...", + "Loading users...": "Benotzer gi gelueden...", + "Loading views...": "Usiichte gi gelueden...", + "Loading...": "Gëtt gelueden...", + "Local": "Lokal", + "Local Version": "Lokal Versioun", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).", + "Location": "Locatie", + "Location Field": "Locatieveld", + "Locations": "Locaties", + "Locked": "Gespaart", + "Locked:": "Gespaart:", + "Login successful": "Umeldung erfollegräich", + "Logs": "Protokoller", + "Low": "Niddreg", + "Low Confidence": "Niddregt Vertrauen", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Lagere nummers verschijnen eerst in de filterzijbalk. Leeg laten voor automatische volgorde.", + "Lucene Version:": "Lucene-Versioun:", + "Magic table sync completed for {schema}": "Synchronisatie magische tabel voltooid voor {schema}", + "Magic table synchronized successfully": "Magic-Tabell erfollegräich synchroniséiert", + "Mail Integration": "E-mailintegratie", + "Mail integration is not available": "Mail integration is not available", + "Make this installation discoverable within the federation network.": "Maak deze installatie vindbaar binnen het federatienetwerk.", + "Manage Organisation Roles": "Organisatiounsrolle verwalten", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Verwalt SOLR-Sammlungen (Datespäicher) an deelt se fir Objeten an Dateien zou.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Verwalt SOLR-ConfigSets (Konfiguratiounstemplater) fir Är Sammlungen.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Manage SOLR Collections (data stores) and assign them for objecten and bestands.", + "Manage SOLR configsets (configuration templates) for your collections.": "Manage SOLR ConfigSets (configuratie templates) for your collections.", + "Manage and configure agents for automated tasks": "Agente fir automatiséiert Aufgabe verwalten a konfiguréieren", + "Manage and monitor file text extraction status": "Datei-Textextraktiounsstatus verwalten an iwwerwaachen", + "Manage and restore soft deleted items from your registers": "Mëll geläscht Artikelen aus äre Registere verwalten a restauréieren", + "Manage and view detected entities from files and objects": "Erkannt Entitéiten aus Dateien an Objete verwalten an ukucken", + "Manage document templates and themes": "Dokument-Templater an Themen verwalten", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Verwalt n8n-Workflows fir d'OpenRegister-Automatiséierung. D'Workflows ginn am konfiguréierte Projet gespäichert.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.", + "Manage webhooks for event-driven integrations": "Webhooks fir evenementgesteiert Integratioune verwalten", + "Manage your account settings, security, and personal data.": "Beheer uw accountinstellingen, beveiliging en persoonlijke gegevens.", + "Manage your applications and modules": "Verwalt Är Applikatiounen a Moduler", + "Manage your chat conversations": "Uw chatgesprekken beheren", + "Manage your data registers and their configurations": "Verwalt Är Dateregistere an hir Konfiguratiounen", + "Manage your data schemas and their properties": "Verwalt Är Dateschemaen an hir Eegeschaften", + "Manage your data sources and their configurations": "Verwalt Är Datequellen an hir Konfiguratiounen", + "Manage your organisations and switch between them": "Uw organisaties beheren en ertussen wisselen", + "Manage your saved search configurations": "Verwalt Är gespäichert Sichkonfiguratiounen", + "Manage your system configurations and settings": "Verwalt Är Systemkonfiguratiounen an -astellungen", + "Managed": "Verwalt", + "Mappings": "Toewijzingen", + "Mass validation completed successfully": "Mass validation completed successfully", + "Mass validation failed: {error}": "Mass validation failed: {error}", + "Max Files (0 = all)": "Max. Dateien (0 = all)", + "Max Length": "Max. Längt", + "Max Length:": "Max. Längt:", + "Max Objects (0 = all)": "Max. Objeten (0 = all)", + "Max Retries": "Max. Neiversuche", + "Max Shards/Node": "Max. Shards/Node", + "Max Tokens": "Max. tokens", + "Max execution time (ms)": "Max. Ausféierungszäit (ms)", + "Max ms": "Max. ms", + "Max result count": "Max. Resultatzuel", + "Max results": "Max. Resultater", + "Maximum": "Maximum", + "Maximum File Size (MB)": "Maximale bestandsgrootte (MB)", + "Maximum Nesting Depth": "Maximal Verschachtelungsdéift", + "Maximum length": "Maximale lengte", + "Maximum length constraint is missing": "Maximal Längt-Restriktioun feelt", + "Maximum length is too restrictive": "Maximal Längt ass ze restriktiv", + "Maximum number of files to process. Set to 0 to process all files.": "Maximal Unzuel u Dateien déi verschafft ginn. Setzt op 0 fir all Dateien ze verschaffen.", + "Maximum number of items": "Maximum aantal items", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maximal Unzuel un Objeten déi verschafft ginn. Setzt op 0 fir all Objeten ze verschaffen.", + "Maximum number of retry attempts for failed deliveries": "Maximal Unzuel u Neiversuche fir feelgeschloen Liwwerungen", + "Maximum time to wait before committing changes": "Maximal Zäit déi gewaart gëtt ier Ännerunge committet ginn", + "Maximum tokens to generate": "Maximal Tokens déi generéiert ginn", + "Maximum value": "Maximumwaarde", + "Maximum value constraint is missing": "Maximalwäert-Restriktioun feelt", + "Maximum value is too restrictive": "Maximalwäert ass ze restriktiv", + "Medium": "Mëttel", + "Medium Confidence": "Mëttelt Vertrauen", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Mëttel Ufroen: Siche mat e bësse Filterung oder méi Parameter (z. B. Datumsberäicher, spezifesch Registeren/Schemaen)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Gemiddelde zoekopdrachten: zoekopdrachten met enige filtering of meerdere parameters (bijv. datumbereiken, specifieke registers/schema's)", + "Member removed successfully": "Lid succesvol verwijderd", + "Members": "Leden", + "Members ({count})": "Leden ({count})", + "Members of selected groups can access this view": "Memberen vun den ausgewielte Gruppen hunn Zougang zu dëser Usiicht", + "Members:": "Memberen:", + "Memory prediction calculated successfully": "Späicherprognos erfollegräich berechent", + "Message content is required": "Message-Inhalt ass erfuerderlech", + "Message does not belong to this conversation": "Message gehéiert net zu dësem Gespréich", + "Message not found": "Message net fonnt", + "Metadata": "Metadata", + "Metadata Filters": "Metadonnéeën-Filteren", + "Method": "Method", + "Method*": "Methode*", + "Method: {method}": "Methode: {method}", + "Min Length": "Min. Längt", + "Min execution time (ms)": "Min. Ausféierungszäit (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. Resultatzuel", + "Min results": "Min. Resultater", + "Minimum": "Minimum", + "Minimum length": "Minimale lengte", + "Minimum number of items": "Minimum aantal items", + "Minimum value": "Minimumwaarde", + "Minimum value constraint is missing": "Minimalwäert-Restriktioun feelt", + "Minimum value is too restrictive": "Minimalwäert ass ze restriktiv", + "Missing conversation or agentUuid": "Feelend Gespréich oder agentUuid", + "Missing conversationId": "Feelend conversationId", + "Missing message": "Feelende Message", + "Missing object fields created successfully": "Feelend Objet-Felder erfollegräich erstallt", + "Mode:": "Modus:", + "Model": "Modell", + "Model Name": "Modellnumm", + "Modified": "Geännert", + "Monthly": "Méintlech", + "More sources (10-20):": "Méi Quellen (10-20):", + "Most Active Objects": "Aktiivst Objeten", + "Multiple Of": "Vilfacht vun", + "Multiple of": "Meervoud van", + "Multiple search trail deletion not implemented yet": "Méifach Sich-Trail-Läschung nach net ëmgesat", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Multitenancy settings updated successfully", + "My Account": "Mijn account", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Numm", + "Name *": "Naam *", + "Name*": "Naam*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Naamencache succesvol opgewarmd: {count} namen geladen in {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Names cache warmed up successfully: {count} names loaded in {time}. {message}", + "Names cache warmup completed": "Nimm-Cache-Warmup ofgeschloss", + "Naming pattern for copies:": "Naamgevingspatroon voor kopieën:", + "New Collection Name": "Neie Sammlungsnumm", + "New Configuration": "New Configuration", + "New Conversation": "Neit Gespréich", + "New Properties": "Nei Eegeschaften", + "New Property": "Nei Eegeschaft", + "New User Group": "Nieuwe gebruikersgroep", + "New Value": "Neie Wäert", + "New activity": "New activity", + "New password": "Nieuw wachtwoord", + "New processing activity": "Nieuwe verwerkingsactiviteit", + "New users without specific organisation membership will be automatically added to this organisation": "Nei Benotzer ouni spezifesch Organisatiounsmemberschaft ginn automatesch zu dëser Organisatioun derbäigesat", + "New verwerkingsactiviteit": "New verwerkingsactiviteit", + "Next": "Weider", + "Nextcloud Groups": "Nextcloud-groepen", + "No": "Nee", + "No API tokens.": "Geen API-tokens.", + "No ConfigSets found": "Keng ConfigSets fonnt", + "No Deck cards linked to this object": "No Deck cards linked to this object", + "No GitHub token configured": "Kee GitHub-Token konfiguréiert", + "No GitHub token provided": "Kee GitHub-Token uginn", + "No GitLab token provided": "Kee GitLab-Token uginn", + "No Organisation": "Keng Organisatioun", + "No active filters": "Keng aktiv Filteren", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Geen actieve objecten om voorlopig te verwijderen voor schema {schema}. Gebruik \"Permanent verwijderen\" om voorlopig verwijderde objecten te verwijderen.", + "No activity data available": "Keng Aktivitéitsdonnéeë verfügbar", + "No activity found.": "Geen activiteit gevonden.", + "No agents available": "Keng Agente verfügbar", + "No agents found": "Keng Agente fonnt", + "No app store cache exists yet": "Et existéiert nach kee App-Store-Cache", + "No applications are available.": "Keng Applikatioune verfügbar.", + "No applications found": "Keng Applikatioune fonnt", + "No approval steps for this object.": "Geen goedkeuringsstappen voor dit object.", + "No archived conversations": "Keng archivéiert Gespréicher", + "No audit trail entries found": "Keng Audit-Trail-Andréi fonnt", + "No changes recorded": "Keng Ännerunge registréiert", + "No chunks to vectorize": "Keng Stécker fir ze vektoriséieren", + "No collections found": "Keng Sammlunge fonnt", + "No configuration data": "Keng Konfiguratiounsdonnéeën", + "No configuration selected": "Geen configuratie geselecteerd", + "No configurations are available.": "Keng Konfiguratioune verfügbar.", + "No configurations found": "Keng Konfiguratioune fonnt", + "No configurations found for this application.": "Keng Konfiguratioune fir dës Applikatioun fonnt.", + "No contacts linked to this object": "No contacts linked to this object", + "No conversations yet. Create a new one to get started!": "Nach keng Gespréicher. Erstellt en neit fir unzefänken!", + "No dashboards yet": "No dashboards yet", + "No data available": "Keng Donnéeë verfügbar", + "No data available for chart": "Keng Donnéeë fir d'Diagramm verfügbar", + "No deleted items found": "Keng geläscht Artikele fonnt", + "No deletion data available": "Keng Läschdonnéeë verfügbar", + "No description found": "Keng Beschreiwung fonnt", + "No description provided": "Keng Beschreiwung uginn", + "No emails linked to this object": "No emails linked to this object", + "No endpoints defined": "Geen eindpunten gedefinieerd", + "No entities detected for this email.": "Geen entiteiten gedetecteerd voor dit e-mailbericht.", + "No entities detected in this file. Run text extraction first.": "Keng Entitéiten an dëser Datei erkannt. Féiert fir d'éischt d'Textextraktioun aus.", + "No entities found": "Keng Entitéite fonnt", + "No entities have been detected yet": "Et goufe nach keng Entitéite erkannt", + "No events found": "Keng Evenementer fonnt", + "No events linked to this object": "No events linked to this object", + "No expired audit trails found to clear": "Keng ofgelaf Audit-Trails fonnt fir ze läschen", + "No expired search trails found to clear": "Keng ofgelaf Sich-Trails fonnt fir ze läschen", + "No extraction data available for this file": "Keng Extraktiounsdonnéeë fir dës Datei verfügbar", + "No facetable fields available. Select a register and schema to see available filters.": "Keng facettéierbar Felder verfügbar. Wielt e Register an e Schema fir verfügbar Filteren ze gesinn.", + "No files found": "Keng Dateie fonnt", + "No files have been extracted yet": "Et goufen nach keng Dateien extrahéiert", + "No files selected": "Geen bestanden geselecteerd", + "No files to process": "Keng Dateie fir ze verschaffen", + "No files to reindex": "Keng Dateie fir nei ze indexéieren", + "No filters are currently active. This will delete ALL audit trail entries!": "Aktuell sinn keng Filteren aktiv. Dëst läscht ALL Audit-Trail-Andréi!", + "No groups found. Try a different search.": "Geen groepen gevonden. Probeer een andere zoekopdracht.", + "No label": "Geen label", + "No labels": "Geen labels", + "No linked objects": "Geen gekoppelde objecten", + "No log entries found": "Keng Protokoll-Andréi fonnt", + "No logs are available for this configuration.": "Keng Protokoller fir dës Konfiguratioun verfügbar.", + "No logs are available for this source.": "Keng Protokoller fir dës Quell verfügbar.", + "No logs found": "Keng Protokoller fonnt", + "No matches": "No matches", + "No mismatched fields found - SOLR schema is properly configured": "Keng net iwwereneestëmmend Felder fonnt - de SOLR-Schema ass richteg konfiguréiert", + "No objects are currently selected for deletion.": "Er zijn momenteel geen objecten geselecteerd voor verwijdering.", + "No objects are currently selected for permanent deletion.": "Er zijn momenteel geen objecten geselecteerd voor permanente verwijdering.", + "No objects are currently selected for restoration.": "Er zijn momenteel geen objecten geselecteerd voor herstel.", + "No objects found": "Keng Objete fonnt", + "No objects found to vectorize": "Keng Objete fir ze vektoriséiere fonnt", + "No objects linked to this email": "Geen objecten gekoppeld aan dit e-mailbericht", + "No objects selected": "Geen objecten geselecteerd", + "No objects selected for deletion": "Keng Objete fir d'Läschung ausgewielt", + "No objects selected for restoration": "Geen objecten geselecteerd voor herstel", + "No objects to delete": "Keng Objete fir ze läschen", + "No objects to delete for schema {schema}": "Geen objecten om te verwijderen voor schema {schema}", + "No objects were permanently deleted": "Et goufe keng Objete definitiv geläscht", + "No organisations found": "Geen organisaties gevonden", + "No personal data was found for this subject identifier.": "No personal data was found for this subject identifier.", + "No processing activities yet": "No processing activities yet", + "No properties found": "Keng Eegeschafte fonnt", + "No properties match your filters.": "Keng Eegeschafte entspriechen äre Filteren.", + "No purge date set": "Kee Purge-Datum gesat", + "No register data available": "Keng Registerdonnéeë verfügbar", + "No register objects reference this file": "Keng Register-Objete referenzéieren dës Datei", + "No registers found": "Keng Registere fonnt", + "No registers found for this application.": "Keng Registere fir dës Applikatioun fonnt.", + "No registers found. Create a register to configure permissions.": "Geen registers gevonden. Maak een register aan om machtigingen te configureren.", + "No related cases found for this sender": "Geen gerelateerde zaken gevonden voor deze afzender", + "No related entities": "No related entities", + "No relations found": "Keng Relatioune fonnt", + "No request body available": "Kee Ufro-Kierper verfügbar", + "No response body available": "Keen Äntwert-Kierper verfügbar", + "No saved views yet. Create one in the Search tab!": "Nach keng gespäichert Usiichten. Erstellt eng am Sich-Tab!", + "No saved views yet. create one in the search tab!": "No saved views yet. create one in the search tab!", + "No scan run yet": "No scan run yet", + "No schema data available": "Keng Schemadonnéeë verfügbar", + "No schema selected for exploration": "Kee Schema fir d'Exploratioun ausgewielt", + "No schemas configured for mail linking.": "Geen schema's geconfigureerd voor e-mailkoppeling.", + "No schemas found": "Keng Schemae fonnt", + "No schemas found for this application.": "Keng Schemae fir dës Applikatioun fonnt.", + "No search terms data available": "Keng Sichbegrëffer-Donnéeë verfügbar", + "No search trail entries found": "Keng Sich-Trail-Andréi fonnt", + "No sources are available.": "Keng Quelle verfügbar.", + "No sources found": "Keng Quelle fonnt", + "No synchronizations found": "Geen synchronisaties gevonden", + "No templates found": "Keng Templater fonnt", + "No templates have been created yet": "Et goufen nach keng Templater erstallt", + "No tools available. Tools can be registered by installed apps.": "Geen tools beschikbaar. Tools kunnen worden geregistreerd door geïnstalleerde apps.", + "No users in this organisation.": "Geen gebruikers in deze organisatie.", + "No valid magic-mapped register+schema combinations found": "Keng gëlteg magic-mapped Register+Schema-Kombinatioune fonnt", + "No views found. Create views first before configuring vectorization.": "Keng Usiichte fonnt. Erstellt fir d'éischt Usiichte ier Dir d'Vektoriséierung konfiguréiert.", + "No views match your search": "Keng Usiichte entspriechen ärer Sich", + "No webhooks found": "Keng Webhooks fonnt", + "No webhooks have been configured yet": "Et goufe nach keng Webhooks konfiguréiert", + "No workflows found in this project. Create workflows in the n8n editor.": "Keng Workflows an dësem Projet fonnt. Erstellt Workflows am n8n-Editor.", + "None": "Keen", + "Not Found": "Net fonnt", + "Not extracted": "Net extrahéiert", + "Not helpful": "Net hëllefräich", + "Not in use": "Net a Gebrauch", + "Note:": "Opmerking:", + "Notifications": "Meldingen", + "Number Constraints": "Zuelerestriktiounen", + "Number of Sources": "Aantal bronnen", + "Number of chunks to vectorize in one API call (1-100).": "Unzuel u Stécker déi an engem API-Opruff vektoriséiert ginn (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Unzuel u Stécker déi an engem API-Opruff vektoriséiert ginn. Méi héich = méi séier awer méi Späicher. Recommandéiert: 10-50.", + "Number of objects to process in each batch (1-5000).": "Unzuel un Objeten déi an all Batch verschafft ginn (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Unzuel un Objeten déi an engem API-Opruff vektoriséiert ginn. Méi héich = méi séier awer méi Späicher. Recommandéiert: 10-50.", + "Numeric Range:": "Numeresche Beräich:", + "Object": "Objet", + "Object #{id}": "Object #{id}", + "Object Collection": "Objet-Sammlung", + "Object Configuration:": "Objectconfiguratie:", + "Object Count Prediction": "Objet-Zuel-Prognos", + "Object Extraction Mode": "Objectextractiemodus", + "Object ID": "Objet-ID", + "Object Management": "Objet-Verwaltung", + "Object Text Extraction": "Objecttekstextractie", + "Object Vectorization": "Objet-Vektoriséierung", + "Object Warmup": "Objet-Warmup", + "Object changes in owned objects": "Objectwijzigingen in eigendomsobjecten", + "Object collection not configured": "Objet-Sammlung net konfiguréiert", + "Object metadata incomplete for file upload": "Object metadata incomplete for file upload", + "Object not found": "Objet net fonnt", + "Object permanently deleted": "Objet definitiv geläscht", + "Object restored successfully": "Objet erfollegräich restauréiert", + "Object settings updated successfully": "Objet-Astellunge erfollegräich aktualiséiert", + "Object sources": "Objet-Quellen", + "Object successfully deleted": "Object successfully deleted", + "Object successfully modified": "Object succesvol gewijzigd", + "Object unlocked successfully": "Objet erfollegräich opgespaart", + "Object vectorization configuration saved successfully": "Objet-Vektoriséierungskonfiguratioun erfollegräich gespäichert", + "Object vectorized successfully": "Objet erfollegräich vektoriséiert", + "Object:": "Objet:", + "Objects": "Objeten", + "Objects Analyzed": "Analyséiert Objeten", + "Objects Distribution": "Objetverdeelung", + "Objects being analyzed": "Objeten déi analyséiert ginn", + "Objects by Register": "Objeten no Register", + "Objects by Schema": "Objeten no Schema", + "Objects deletion completed for {schema}": "Objectverwijdering voltooid voor {schema}", + "Objects deletion completed successfully": "Objet-Läschung erfollegräich ofgeschloss", + "Objects in schema": "Objeten am Schema", + "Objects successfully deleted": "Objects successfully deleted", + "Objects to Process:": "Objeten déi ze verschaffe sinn:", + "Objects to be analyzed": "Objeten déi analyséiert ginn", + "Objects to be deleted": "Objeten déi geläscht ginn", + "Objects to be validated": "Objeten déi validéiert ginn", + "Objects will be serialized as JSON text before vectorization": "Objete ginn als JSON-Text serialiséiert virun der Vektoriséierung", + "Objects will be soft deleted and moved to the": "Objecten worden voorlopig verwijderd en verplaatst naar de", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objete ginn mëll geläscht (als geläscht markéiert awer an der Datebank gehalen). Si kënne spéider erëmgewonne ginn wann néideg.", + "Old Value": "Aalt Wäert", + "Oldest Item (days)": "Eelsten Artikel (Deeg)", + "Ollama Chat Settings": "Ollama-Chat-Astellungen", + "Ollama Embedding Configuration": "Ollama-Embedding-Konfiguratioun", + "Ollama URL": "Ollama-URL", + "Only With Changes": "Nëmme mat Ännerungen", + "Only members of selected groups can access this organisation": "Alleen leden van geselecteerde groepen hebben toegang tot deze organisatie", + "Open Nextcloud App Store": "Nextcloud-App-Store opmaachen", + "Open OpenRegister sidebar": "OpenRegister-zijbalk openen", + "Open Register Objects": "Open-Register-Objeten", + "Open folder": "Map openen", + "Open in OpenRegister": "Openen in OpenRegister", + "Open n8n Editor": "n8n-Editor opmaachen", + "OpenAI Chat Settings": "OpenAI-Chat-Astellungen", + "OpenAI Embedding Configuration": "OpenAI-Embedding-Konfiguratioun", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister-Astellungen", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.", + "Optimizing search performance...": "Optimizing search performance...", + "Optional URL-friendly identifier": "Optionele URL-vriendelijke identifier", + "Optional description for this facet": "Optionele beschrijving voor dit facet", + "Optional end date property": "Optionele einddatumeigenschap", + "Optional event description template": "Optioneel evenementsbeschrijvingssjabloon", + "Optional location property": "Optionele locatie-eigenschap", + "Optional webhook secret for signature verification": "Optionalen Webhook-Geheimnis fir d'Signaturverifizéierung", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Optioneel: Voeg queryparameters toe om het gerefereerde schema te filteren (bijv. status=actief&type=openbaar)", + "Or": "Of", + "Order": "Reiefolleg", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.", + "Organisation": "Organisatioun", + "Organisation Configuration": "Organisatieconfiguratie", + "Organisation ID": "Organisatiouns-ID", + "Organisation Members": "Organisatiounsmemberen", + "Organisation Statistics": "Organisatiounsstatistiken", + "Organisation created successfully": "Organisatioun erfollegräich erstallt", + "Organisation membership changes": "Wijzigingen in organisatielidmaatschap", + "Organisation name is required": "Organisatienaam is vereist", + "Organisation settings saved successfully": "Organisatiounsastellunge erfollegräich gespäichert", + "Organisation successfully created": "Organisation successfully created", + "Organisation successfully updated": "Organisatie succesvol bijgewerkt", + "Organisational measures": "Organisatorische maatregelen", + "Organisations": "Organisatiounen", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organisatioun", + "Organization ID (Optional)": "Organisatiouns-ID (Optional)", + "Organizations": "Organisaties", + "Orphaned Items": "Verloossen Artikelen", + "Other": "Overige", + "Output Mapping": "Uitvoertoewijzing", + "Overview": "Overzicht", + "Owner": "Besëtzer", + "Owner:": "Besëtzer:", + "PDF": "PDF", + "PII hits": "PII hits", + "PII rows": "PII rows", + "Page {current} of {total}": "Pagina {current} van {total}", + "Parallel Mode (Faster, more resource intensive)": "Parallelle Modus (Méi séier, méi ressourceintensiv)", + "Parallel:": "Parallel:", + "Password": "Passwuert", + "Password changes are not supported by your authentication provider.": "Wachtwoordwijzigingen worden niet ondersteund door uw authenticatieprovider.", + "Password for SOLR authentication (optional)": "Passwuert fir d'SOLR-Authentifizéierung (optional)", + "Password updated successfully": "Wachtwoord succesvol bijgewerkt", + "Path": "Pad", + "Path in repository": "Pad am Repository", + "Path where the configuration file will be saved in the repository": "Pad wou d'Konfiguratiounsdatei am Repository gespäichert gëtt", + "Path where the register OAS file will be saved in the repository": "Pad wou d'Register-OAS-Datei am Repository gespäichert gëtt", + "Pattern (regex)": "Muster (Regex)", + "Pattern Issue": "Musterproblem", + "Pattern constraint is missing": "Muster-Restriktioun feelt", + "Patterns:": "Musteren:", + "Pending": "Ausstoend", + "Permanent Delete Mode": "Definitive Läschmodus", + "Permanently Delete": "Definitiv läschen", + "Permanently Delete ({count})": "Permanent verwijderen ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Alle {count} voorlopig verwijderde objecten permanent verwijderen. Dit kan niet ongedaan worden gemaakt!", + "Permanently delete already soft-deleted objects": "Scho mëll geläscht Objeten definitiv läschen", + "Permanently delete this conversation?": "Dëst Gespréich definitiv läschen?", + "Permission Matrix": "Machtigingsmatrix", + "Person": "Persoun", + "Personal Data": "Perséinlech Donnéeën", + "Personal Data Export": "Export persoonsgegevens", + "Persons": "Personen", + "Phone": "Telefon", + "Phone Numbers": "Telefoonnummers", + "Photos": "Fotoen", + "Please create an agent in the": "Erstellt w.e.g. en Agent am", + "Please select a user": "Selecteer een gebruiker", + "Please select an agent to continue": "Wielt w.e.g. en Agent fir weiderzefueren", + "Please select an organisation": "Selecteer een organisatie", + "Please select at least one item to import": "Selecteer minimaal één item om te importeren", + "Please select registers or schemas first": "Selecteer eerst registers of schema's", + "Please select which register and schema to use for the new object": "Wielt w.e.g. aus wéi ee Register a Schema fir den neien Objet ze benotzen", + "Please try again later.": "Probéiert w.e.g. méi spéit erëm.", + "Please wait while we fetch your agents.": "Waart w.e.g. wärend mir Är Agenten ofruffen.", + "Please wait while we fetch your applications.": "Waart w.e.g. wärend mir Är Applikatiounen ofruffen.", + "Please wait while we fetch your configurations.": "Waart w.e.g. wärend mir Är Konfiguratiounen ofruffen.", + "Please wait while we fetch your deleted items.": "Waart w.e.g. wärend mir Är geläscht Artikelen ofruffen.", + "Please wait while we fetch your sources.": "Waart w.e.g. wärend mir Är Quellen ofruffen.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Wacht terwijl de SOLR-collectie permanent wordt verwijderd. Dit kan even duren.", + "Polls": "Ëmfroen", + "Popular Search Terms": "Populär Sichbegrëffer", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Voorkeuren opgeslagen", + "Prerequisites": "Viraussetzungen", + "Press Enter to send, Shift+Enter for new line": "Dréckt Enter fir ze schécken, Shift+Enter fir eng nei Linn", + "Press enter to send, shift+enter for new line": "Druk op Enter om te verzenden, shift+Enter voor nieuwe regel", + "Prettify": "Opmaken", + "Preview erasure": "Preview erasure", + "Preview:": "Voorbeeld:", + "Previous": "Virdrun", + "Private": "Privat", + "Private Agent (Default)": "Privéagent (standaard)", + "Private agents": "Privéagenten", + "Process Name (optional)": "Procesnaam (optioneel)", + "Processed {indexed} files, {failed} failed": "{indexed} bestanden verwerkt, {failed} mislukt", + "Processes chunks in batches with simulated parallelism.": "Verschafft Stécker a Batche mat simuléierter Parallelitéit.", + "Processes file chunks sequentially (safest).": "Verschafft Dateistécker sequenziell (am séchersten).", + "Processes objects in chunks with simulated parallelism.": "Verschafft Objeten a Stécker mat simuléierter Parallelitéit.", + "Processes objects sequentially (safest).": "Verschafft Objete sequenziell (am séchersten).", + "Processing": "Verschaffen", + "Processing Limits": "Verschaffungslimiten", + "Processing...": "Gëtt verschafft...", + "Project initialized successfully": "Projet erfollegräich initialiséiert", + "Project not found. Please initialize first.": "Projet net fonnt. Initialiséiert w.e.g. fir d'éischt.", + "Properties": "Eegeschaften", + "Property Behaviors": "Eegeschaftsverhalen", + "Property Configuration:": "Eegeschaftskonfiguratioun:", + "Property Filters": "Eegeschaftsfilteren", + "Property Title": "Eegeschaftstitel", + "Property Type": "Eegeschaftstyp", + "Property can be improved": "Eegeschaft kann verbessert ginn", + "Property could not be added": "Property could not be added", + "Property could not be updated": "Eigenschap kon niet worden bijgewerkt", + "Property name of inversed relation": "Eigenschapsnaam van omgekeerde relatie", + "Property successfully added": "Property successfully added", + "Property successfully updated": "Eigenschap succesvol bijgewerkt", + "Protected": "Geschützt", + "Provider": "Provider", + "Provider is required for testing": "Provider ass fir den Test erfuerderlech", + "Public": "Ëffentlech", + "Public View": "Ëffentlech Usiicht", + "Public views can be accessed by anyone in the system": "Op ëffentlech Usiichte kann jiddereen am System zougräifen", + "Publish": "Verëffentlechen", + "Publish Configuration to GitHub": "Configuratie publiceren naar GitHub", + "Publish OAS": "OAS publiceren", + "Publish Objects": "Objete verëffentlechen", + "Publish Register OAS to GitHub": "Register OAS publiceren naar GitHub", + "Publish all objects in this schema": "All Objeten an dësem Schema verëffentlechen", + "Publish objects to make them publicly available": "Objecten publiceren om ze openbaar beschikbaar te maken", + "Published": "Verëffentlecht", + "Published:": "Verëffentlecht:", + "Publishing...": "Gëtt verëffentlecht...", + "Purge": "Purge", + "Purge Date": "Purge-Datum", + "Purge {count} object from database": "Purge {count} object from database", + "Purge {count} objects from database": "Purge {count} objects from database", + "Purpose limitation *": "Doelbinding *", + "Push Notifications": "Push-Notifikatiounen", + "Query Complexity Distribution": "Verdeelung vun der Ufrokomplexitéit", + "Query Help": "Zoekopdrachthulp", + "Query parameter is required": "Ufroparameter ass erfuerderlech", + "Quota": "Quotum", + "RAG": "RAG", + "RAG Configuration": "RAG-Konfiguratioun", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC settings updated successfully", + "Rate limits cleared successfully": "Rate-Limiten erfollegräich geläscht", + "Raw Changes Data": "Réi Ännerungsdonnéeën", + "Re-vectorize on object update": "Bei Objet-Aktualiséierung nei vektoriséieren", + "Read": "Liesen", + "Real-time push notification status via notify_push": "Echtzäit-Push-Notifikatiounsstatus iwwer notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Echtzäit-Push-Notifikatioune sinn aktiv. Verbonne Clienten kréien direkt Aktualiséierunge wann Objeten erstallt, aktualiséiert oder geläscht ginn.", + "Realtime push active": "Echtzäit-Push aktiv", + "Realtime push not available — the notify_push app is not installed": "Echtzäit-Push net verfügbar — d'notify_push-App ass net installéiert", + "Reason": "Reden", + "Reason (optional)": "Reden (optioneel)", + "Rebase operation completed successfully": "Rebase operation completed successfully", + "Rebase operation failed: {error}": "Rebase operation failed: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Recommandatiounen:", + "Recommended Type:": "Recommandéierten Typ:", + "Recommended: 5 sources": "Recommandéiert: 5 Quellen", + "Refresh": "Aktualiséieren", + "Refresh Data": "Donnéeën aktualiséieren", + "Refresh Stats": "Statistiken aktualiséieren", + "Refresh Workflows": "Workflows aktualiséieren", + "Refresh dashboard": "Dashboard aktualiséieren", + "Refresh database info": "Datebankinformatiounen aktualiséieren", + "Register": "Register", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Schema", + "Register ID": "Register-ID", + "Register Objects": "Register-Objeten", + "Register Overview": "Registeroverzicht", + "Register Statistics": "Registerstatistiken", + "Register Totals": "Register-Totaler", + "Register and schema are required": "Register en schema zijn vereist", + "Register annotation": "Register annotation", + "Register depublished successfully": "Register erfollegräich depublizéiert", + "Register not found": "Register net fonnt", + "Register objects": "Register objects", + "Register objects deletion completed successfully": "Läschung vun de Register-Objeten erfollegräich ofgeschloss", + "Register published successfully": "Register erfollegräich verëffentlecht", + "Register/Schema Usage": "Register-/Schemagebrauch", + "Register:": "Register:", + "Registers": "Registeren", + "Reindex": "Nei indexéieren", + "Reindex completed": "Nei-Indexéierung ofgeschloss", + "Reindex completed successfully": "Nei-Indexéierung erfollegräich ofgeschloss", + "Reindex completed! Processed {count} objects in {duration}s": "Herindexering voltooid! {count} objecten verwerkt in {duration}s", + "Reindex failed": "Nei-Indexéierung feelgeschloen", + "Reindex failed: %s": "Nei-Indexéierung feelgeschloen: %s", + "Reject": "Afwijzen", + "Rejected": "Afgewezen", + "Related Cases": "Gerelateerde zaken", + "Relations": "Relatiounen", + "Remote Version": "Remote-Versioun", + "Remove": "Ewechhuelen", + "Remove Deck card": "Remove Deck card", + "Remove avatar": "Avatar verwijderen", + "Remove contact": "Remove contact", + "Remove filter": "Filter ewechhuelen", + "Remove from favorites": "Aus de Favoritten ewechhuelen", + "Remove from list": "Uit lijst verwijderen", + "Remove group": "Grupp ewechhuelen", + "Remove link": "Koppeling verwijderen", + "Remove link to {name}": "Koppeling naar {name} verwijderen", + "Remove link to {name}?": "Koppeling naar {name} verwijderen?", + "Remove link to {title}": "Koppeling naar {title} verwijderen", + "Remove oneOf entry": "oneOf-invoer verwijderen", + "Remove property after write-back": "Eigenschap verwijderen na terugschrijven", + "Remove {title}": "{title} verwijderen", + "Remove {userId} from this organisation?": "{userId} uit deze organisatie verwijderen?", + "Removed": "Ewechgeholl", + "Removed from favorites": "Aus de Favoritten ewechgeholl", + "Removing schema {schema}...": "Schema {schema} verwijderen...", + "Rename Conversation": "Gespréich ëmbenennen", + "Rename conversation": "Gespréich ëmbenennen", + "Replicas": "Repliken", + "Reports": "Reports", + "Repository": "Repository", + "Request Body": "Ufro-Kierper", + "Request Data": "Ufro-Donnéeën", + "Request Details": "Ufro-Detailer", + "Request Quota (per day)": "Verzoekquotum (per dag)", + "Request account deactivation": "Accountdeactivering aanvragen", + "Request account deactivation. This will notify administrators for review.": "Accountdeactivering aanvragen. Hiermee worden beheerders op de hoogte gesteld voor beoordeling.", + "Request body must be JSON with a \"query\" field": "Verzoekinhoud moet JSON zijn met een \"query\"-veld", + "Request timeout in seconds": "Ufro-Timeout a Sekonnen", + "Requested at": "Aangevraagd op", + "Required": "Vereist", + "Required Token Scopes": "Vereiste tokenbereiken", + "Required field": "Erfuerderlecht Feld", + "Required status is inconsistent": "Erfuerderleche Status ass inkonsistent", + "Requires Dolphin AI for OCR text extraction": "Vereist Dolphin AI voor OCR-tekstextractie", + "Rerun Search": "Sich nei ausféieren", + "Rerunning search: {searchTerm}": "Zoekopdracht opnieuw uitvoeren: {searchTerm}", + "Reset Changes": "Ännerunge zerécksetzen", + "Reset Filters": "Filteren zerécksetzen", + "Resource Quotas": "Resourcequota's", + "Response Body": "Äntwert-Kierper", + "Response Details": "Äntwert-Detailer", + "Response Time:": "Äntwertzäit:", + "Restore": "Restauréieren", + "Restore conversation": "Gespréich restauréieren", + "Restore or permanently delete items": "Artikele restauréieren oder definitiv läschen", + "Restore {count} object": "Restore {count} object", + "Restore {count} objects": "Restore {count} objects", + "Result Count Range": "Resultatzuel-Beräich", + "Results": "Resultater", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 duration, bijv. P10Y, P30D)", + "Retention settings updated successfully": "Retention settings updated successfully", + "Retrieve all objects for this schema": "All Objete fir dëse Schema ofruffen", + "Retry": "Nei probéieren", + "Retry Failed Extractions": "Feelgeschloen Extraktiounen nei probéieren", + "Retry Policy": "Neiversuchspolitik", + "Retry completed": "Neie Versuch ofgeschloss", + "Retry upload": "Upload opnieuw proberen", + "Retrying...": "Gëtt nei probéiert...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Bekijk hieronder de geselecteerde objecten. Je kunt objecten verwijderen die je niet wilt opnemen.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.", + "Revoke": "Intrekken", + "Right": "Rechts", + "Risk Level": "Risikoniveau", + "Risk level": "Risikoniveau", + "Roles and Permissions": "Rollen en machtigingen", + "Roles updated successfully": "Rollen erfollegräich aktualiséiert", + "Run compliance scan": "Run compliance scan", + "SOLR Connection Settings": "SOLR-Verbindungsastellungen", + "SOLR Index Warmup": "SOLR-indexopwarming", + "SOLR Version:": "SOLR-Versioun:", + "SOLR actions menu": "SOLR-Aktiounsmenü", + "SOLR base path (usually /solr)": "SOLR-Basispad (normalerweis /solr)", + "SOLR collection deleted successfully": "SOLR-collectie succesvol verwijderd", + "SOLR connection test failed: {error}": "SOLR connection test failed: {error}", + "SOLR connection test successful!": "SOLR connection test successful!", + "SOLR index warmup completed successfully": "SOLR index warmup completed successfully", + "SOLR is disabled": "SOLR ass desaktivéiert", + "SOLR is not available or not configured": "SOLR ass net verfügbar oder net konfiguréiert", + "SOLR logging disabled": "SOLR-Protokolléierung desaktivéiert", + "SOLR logging enabled": "SOLR-Protokolléierung aktivéiert", + "SOLR search disabled": "SOLR-Sich desaktivéiert", + "SOLR search enabled": "SOLR-Sich aktivéiert", + "SOLR server hostname or IP address": "SOLR-Server-Hostnumm oder IP-Adress", + "SOLR server port number (optional, defaults to 8983)": "SOLR-Server-Portnummer (optional, Standard ass 8983)", + "SOLR settings updated successfully": "SOLR settings updated successfully", + "SOLR setup completed successfully": "SOLR-Asetzung erfollegräich ofgeschloss", + "SOLR setup completed successfully!": "SOLR setup completed successfully!", + "SOLR setup error: %s": "SOLR-Asetzungsfeeler: %s", + "SOLR setup failed": "SOLR-Asetzung feelgeschloen", + "SOLR setup failed - check logs": "SOLR-Asetzung feelgeschloen - iwwerpréift d'Protokoller", + "SOLR setup failed. Please check the configuration and try again.": "SOLR setup failed. Please check the configuration and try again.", + "SOLR setup failed: {error}": "SOLR setup failed: {error}", + "SOLR warmup failed: {error}": "SOLR warmup failed: {error}", + "Sample Values:": "Beispillwäerter:", + "Save": "Späicheren", + "Save Configuration": "Konfiguratioun späicheren", + "Save Connection Settings": "Verbindungsastellunge späicheren", + "Save Roles": "Rolle späicheren", + "Save Settings": "Astellunge späicheren", + "Save changes": "Ännerungen späicheren", + "Save current search as view": "Aktuell Sich als Usiicht späicheren", + "Save labels": "Labels opslaan", + "Save tags for {name}": "Tags opslaan voor {name}", + "Save the organisation first to manage users.": "Sla de organisatie eerst op om gebruikers te beheren.", + "Saved Views": "Gespäichert Usiichten", + "Saving...": "Gëtt gespäichert...", + "Scheduled": "Scheduled", + "Schema": "Schema", + "Schema ID": "Schema-ID", + "Schema Name": "Schemanumm", + "Schema Statistics": "Schemastatistiken", + "Schema annotation": "Schema annotation", + "Schema configuration updated successfully for {schema}": "Schemaconfiguratie succesvol bijgewerkt voor {schema}", + "Schema depublished successfully": "Schema erfollegräich depublizéiert", + "Schema objects deletion completed successfully": "Läschung vun de Schema-Objeten erfollegräich ofgeschloss", + "Schema published successfully": "Schema erfollegräich verëffentlecht", + "Schema reference ($ref)": "Schemareferentie ($ref)", + "Schema successfully created": "Schema erfollegräich erstallt", + "Schema successfully updated": "Schema erfollegräich aktualiséiert", + "Schema {schema} removed successfully": "Schema {schema} succesvol verwijderd", + "Schema:": "Schema:", + "Schemas": "Schemaen", + "Scheme": "Schema", + "Search": "Zoeken", + "Search / Views": "Sich / Usiichten", + "Search / views": "Zoeken / weergaven", + "Search Activity": "Sichaktivitéit", + "Search Analytics": "Sichanalysen", + "Search Configuration": "Zoekconfiguratie", + "Search Entities": "Entitéite sichen", + "Search Files": "Dateie sichen", + "Search GitHub": "GitHub duerchsichen", + "Search GitLab": "GitLab duerchsichen", + "Search Method": "Sichmethod", + "Search Mode": "Zoekmodus", + "Search Query": "Zoekopdracht", + "Search Results": "Zoekresultaten", + "Search Term": "Sichbegrëff", + "Search Trail Details": "Sich-Trail-Detailer", + "Search Trail Management": "Sich-Trail-Verwaltung", + "Search Trail Statistics": "Sich-Trail-Statistiken", + "Search Trails": "Sich-Trails", + "Search Views": "Usiichte sichen", + "Search Webhooks": "Webhooks duerchsichen", + "Search agents...": "Agenten zoeken...", + "Search applications...": "Applicaties zoeken...", + "Search by file name or path": "No Dateinumm oder Pad sichen", + "Search by name or URL": "No Numm oder URL sichen", + "Search by title or UUID...": "Zoeken op titel of UUID...", + "Search by value": "No Wäert sichen", + "Search configurations": "Configuraties zoeken", + "Search data sources...": "Gegevensbronnen zoeken...", + "Search groups...": "Groepen zoeken...", + "Search in Database Objects": "Zoeken in database-objecten", + "Search in Files": "An Dateie sichen", + "Search in Objects": "An Objete sichen", + "Search objects": "Objecten zoeken", + "Search objects...": "Objecten zoeken...", + "Search property names...": "Eegeschaftsnimm sichen...", + "Search registers...": "Registers zoeken...", + "Search schemas...": "Schema's zoeken...", + "Search trail deletion not implemented yet": "Sich-Trail-Läschung nach net ëmgesat", + "Search views...": "Usiichte sichen...", + "Search {name}...": "{name} zoeken...", + "Searching...": "Zoeken...", + "Secret": "Geheimnis", + "Secret key for HMAC signature generation (optional)": "Geheime Schlëssel fir d'HMAC-Signaturgeneréierung (optional)", + "Security": "Beveiliging", + "See organisations as a table": "Organisaties weergeven als tabel", + "See organisations as cards": "Organisaties weergeven als kaarten", + "Select AI Agent": "KI-Agent auswielen", + "Select Active Organisation": "Actieve organisatie selecteren", + "Select All": "Alles auswielen", + "Select ConfigSet": "ConfigSet auswielen", + "Select File Types to Vectorize:": "Dateitype fir ze vektoriséieren auswielen:", + "Select HTTP method": "HTTP-Method auswielen", + "Select Register and Schema": "Register a Schema auswielen", + "Select Views to Vectorize:": "Usiichte fir ze vektoriséieren auswielen:", + "Select a Nextcloud group to add": "En Nextcloud-Grupp auswielen fir derbäizesetzen", + "Select a branch": "Eng Branche auswielen", + "Select a date property": "Selecteer een datumeigenschap", + "Select a model or type a custom model name": "E Modell auswielen oder e personaliséierte Modellnumm aginn", + "Select a register": "Selecteer een register", + "Select a register...": "Selecteer een register...", + "Select a repository": "E Repository auswielen", + "Select a repository you have write access to": "E Repository auswielen op deen Dir Schreifzougang hutt", + "Select a schema": "Selecteer een schema", + "Select a schema...": "Selecteer een schema...", + "Select a user": "Selecteer een gebruiker", + "Select agent type": "Selecteer agenttype", + "Select all entities": "Select all entities", + "Select an AI Agent": "E KI-Agent auswielen", + "Select an AI agent to begin chatting with your data.": "Wielt e KI-Agent fir mat ären Donnéeën unzefänken ze chatten.", + "Select an email to see linked objects": "Selecteer een e-mail om gekoppelde objecten te zien", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Selecteer een organisatie en gebruiker om deze als lid toe te voegen. Zoek naar organisaties op naam.", + "Select backend": "Backend auswielen", + "Select chat model": "Chat-Modell auswielen", + "Select collection for files": "Sammlung fir Dateien auswielen", + "Select collection for objects": "Sammlung fir Objeten auswielen", + "Select configuration type...": "Selecteer configuratietype...", + "Select default organisation": "Standardorganisatioun auswielen", + "Select entity {value}": "Select entity {value}", + "Select event to listen to...": "Evenement auswielen op deen nogelauschtert gëtt...", + "Select fields to display": "Selecteer weer te geven velden", + "Select files with the correct extension": "Selecteer bestanden met de juiste extensie", + "Select group": "Selecteer groep", + "Select groups (optional)": "Selecteer groepen (optioneel)", + "Select groups to notify...": "Selecteer te informeren groepen...", + "Select groups with access to this agent": "Selecteer groepen met toegang tot deze agent", + "Select groups...": "Selecteer groepen...", + "Select model": "Modell auswielen", + "Select one or more registers": "Selecteer één of meer registers", + "Select one or more schemas": "Selecteer één of meer schema's", + "Select options...": "Optiounen auswielen...", + "Select or create labels, or select \"No label\" to add files": "Selecteer of maak labels, of selecteer \"Geen label\" om bestanden toe te voegen", + "Select owner application (optional)...": "Selecteer eigenaarssapplicatie (optioneel)...", + "Select period": "Period auswielen", + "Select property to send as payload": "Eegeschaft auswielen déi als Payload geschéckt gëtt", + "Select provider": "Provider auswielen", + "Select registers and schemas to save a view": "Registeren a Schemae auswielen fir eng Usiicht ze späicheren", + "Select retry policy": "Neiversuchspolitik auswielen", + "Select role": "Selecteer rol", + "Select schemas (empty = all schemas)": "Selecteer schema's (leeg = alle schema's)", + "Select schemas to warm up": "Selecteer op te warmen schema's", + "Select search mode": "Selecteer zoekmodus", + "Select source type...": "Selecteer brontype...", + "Select target type": "Selecteer doeltype", + "Select the branch to publish to": "D'Branche auswielen op déi verëffentlecht gëtt", + "Select the event this webhook should listen to": "D'Evenement auswielen op dat dëse Webhook soll nolauschteren", + "Select views to filter data (optional)": "Selecteer weergaven om gegevens te filteren (optioneel)", + "Select views to limit which data the agent can access": "Selecteer weergaven om te beperken welke gegevens de agent kan openen", + "Select views to vectorize:": "Usiichte fir ze vektoriséieren auswielen:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Wielt aus, wéi eng Nextcloud-Gruppe fir dës Organisatioun verfügbar sinn. Benotzer an dëse Gruppen hunn Zougang zu den Organisatiounsressourcen.", + "Select which data views the AI can search": "Wielt aus, wéi eng Datenusiichten d'KI duerchsiche kann", + "Select which property from the event should be used as the webhook payload data": "Wielt aus, wéi eng Eegeschaft vum Evenement als Webhook-Payload-Donnéeë benotzt soll ginn", + "Select which tools the AI can use to perform actions": "Wielt aus, wéi eng Tools d'KI benotze kann fir Aktiounen auszeféieren", + "Select {fieldName} values": "Selecteer {fieldName}-waarden", + "Selected": "Ausgewielt", + "Selected Groups": "Ausgewielt Gruppen", + "Selected Objects ({count})": "Geselecteerde objecten ({count})", + "Selected audit trails deleted successfully": "Ausgewielten Audit-Trails erfollegräich geläscht", + "Selected search trails deleted successfully": "Ausgewielte Sich-Trails erfollegräich geläscht", + "Selected users can access this view": "Ausgewielt Benotzer kënnen op dës Usiicht zougräifen", + "Send additional feedback": "Zousätzlecht Feedback schécken", + "Send as CloudEvent": "Als CloudEvent schécken", + "Send as cloudevent": "Verzenden als cloudevenement", + "Sensitive PII": "Sensibel PII", + "Serial Mode (Safer, slower)": "Seriellen Modus (Méi sécher, méi lues)", + "Serial:": "Seriell:", + "Server Configuration": "Serverkonfiguratioun", + "Server Information": "Serverinformatiounen", + "Set as Active": "Instellen als actief", + "Set as active organisation": "Instellen als actieve organisatie", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Limieten instellen voor API-gebruik en tokenverbruik. Gebruik 0 voor onbeperkte resources.", + "Settings": "Astellungen", + "Settings updated successfully": "Settings updated successfully", + "Shards": "Shards", + "Share": "Deelen", + "Share with Groups": "Mat Gruppen deelen", + "Share with Users": "Mat Benotzer deelen", + "Show": "Show", + "Show Filters": "Filteren uweisen", + "Show less": "Manner uweisen", + "Show only entries with changes": "Nëmmen Andréi mat Ännerungen uweisen", + "Showing {count} dashboard(s)": "Showing {count} dashboard(s)", + "Showing {count} processing activities": "Showing {count} processing activities", + "Showing {showing} of {total} agents": "{showing} van {total} agenten weergeven", + "Showing {showing} of {total} applications": "{showing} van {total} applicaties weergeven", + "Showing {showing} of {total} audit trail entries": "{showing} van {total} auditspoorvermeldingen weergeven", + "Showing {showing} of {total} configurations": "{showing} van {total} configuraties weergeven", + "Showing {showing} of {total} deleted items": "{showing} van {total} verwijderde items weergeven", + "Showing {showing} of {total} entities": "{showing} van {total} entiteiten weergeven", + "Showing {showing} of {total} files": "{showing} van {total} bestanden weergeven", + "Showing {showing} of {total} log entries": "{showing} van {total} logvermeldingen weergeven", + "Showing {showing} of {total} search trail entries": "{showing} van {total} zoekspoorvermeldingen weergeven", + "Showing {showing} of {total} sources": "{showing} van {total} bronnen weergeven", + "Showing {showing} of {total} templates": "{showing} van {total} sjablonen weergeven", + "Showing {showing} of {total} webhooks": "{showing} van {total} webhooks weergeven", + "Showing {shown} of {total} organisations": "{shown} van {total} organisaties weergeven", + "Simple": "Einfach", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Einfach Ufroen: Grondleeënd Textsiche mat minimale Parameter (z. B. eenzele Sichbegrëff, keng erweidert Filteren)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Eenvoudige zoekopdrachten: basistekstzoekopdrachten met minimale parameters (bijv. enkelvoudige zoekterm, geen geavanceerde filters)", + "Size": "Gréisst", + "Slug": "Slug", + "Soft Delete Mode": "Mëlle Läschmodus", + "Soft Deleted Items": "Mëll geläscht Artikelen", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Soft delete all objects for this schema ({active} active, {deleted} already deleted)", + "SolrCloud mode enabled": "SolrCloud-Modus aktivéiert", + "Something went wrong while adding files": "Er is iets misgegaan bij het toevoegen van bestanden", + "Source": "Quell", + "Source successfully updated": "Bron succesvol bijgewerkt", + "Sources": "Quellen", + "Sources:": "Quellen:", + "Special Rights": "Speciale rechten", + "Standalone SOLR mode": "Eegestännege SOLR-Modus", + "Start Conversation": "Gespréich ufänken", + "Start Date Field": "Begindatumveld", + "Start Vectorization": "Vektoriséierung starten", + "Start a conversation": "E Gespréich ufänken", + "Starting names cache warmup...": "Nimm-Cache-Warmup gëtt gestart...", + "Starting validation for {schema}...": "Validatie starten voor {schema}...", + "Starting {action} for {schema}...": "{action} starten voor {schema}...", + "Starting...": "Start...", + "Statistics": "Statistiken", + "Statistics and Metrics": "Statistieken en meetgegevens", + "Statistics refreshed": "Statistieken vernieuwd", + "Stats calculation not yet implemented": "Statistikberechnung nach net ëmgesat", + "Status": "Status", + "Status Code": "Status-Code", + "Step": "Stap", + "Storage": "Opslag", + "Storage Quota (MB)": "Opslagquotum (MB)", + "String Constraints": "String-Restriktiounen", + "Subject identifier (email, BSN, name, etc.)": "Subject identifier (email, BSN, name, etc.)", + "Subscribe": "Subscribe", + "Subscribed": "Subscribed", + "Success": "Erfolleg", + "Success Rate": "Erfollegsquote", + "Success Status": "Erfollegsstatus", + "Successful": "Erfollegräich", + "Successfully added user to organisation: {name}": "Gebruiker succesvol toegevoegd aan organisatie: {name}", + "Successfully cleared {count} audit trails": "Successfully cleared {count} audit trails", + "Successfully cleared {count} blob storage objects": "Successfully cleared {count} blob storage objects", + "Successfully cleared {count} search trails": "Successfully cleared {count} search trails", + "Successfully created {count} SOLR fields": "{count} SOLR-velden succesvol aangemaakt", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "{count} embeddings succesvol verwijderd. Vectoriseer uw gegevens opnieuw.", + "Successfully deleted {count} objects for {schema}": "{count} objecten succesvol verwijderd voor {schema}", + "Successfully fixed {count} SOLR field configurations": "{count} SOLR-veldconfiguraties succesvol hersteld", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Succesvol geïmporteerd: {registers} registers, {schemas} schema's, {objects} objecten", + "Successfully joined organisation": "Erfollegräich der Organisatioun bäigetrueden", + "Successfully permanently deleted {count} objects": "{count} objecten succesvol permanent verwijderd", + "Successfully processed {count} files!": "{count} bestanden succesvol verwerkt!", + "Successfully restored {count} object": "Successfully restored {count} object", + "Successfully restored {count} objects": "Successfully restored {count} objects", + "Successfully saved configuration for {count} facets!": "Configuratie voor {count} facetten succesvol opgeslagen!", + "Supported File Types": "Ondersteunde bestandstypen", + "Switch Active Organisation": "Actieve organisatie wisselen", + "Switch Organisation": "Van organisatie wisselen", + "Sync Table": "Tabell synchroniséieren", + "Synchronization Settings": "Synchroniséierungsastellungen", + "Syncing magic table for {schema}...": "Magische tabel synchroniseren voor {schema}...", + "Syncs": "Synchronisaties", + "System Default": "System-Standard", + "System Prompt": "Systeemprompt", + "System announcements": "Systeemmededelingen", + "Table": "Tabel", + "Target ID": "Doel-ID", + "Target Type*": "Doeltype*", + "Technical Description": "Technesch Beschreiwung", + "Technical Details": "Technical Details", + "Technical description for developers and administrators": "Technesch Beschreiwung fir Entwéckler an Administrateuren", + "Technical measures": "Technische maatregelen", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatur", + "Templates": "Templater", + "Test": "Test", + "Test Chat": "Chat testen", + "Test Connection": "Verbindung testen", + "Test Embedding": "Embedding testen", + "Test endpoint executed successfully": "Test-Endpunkt erfollegräich ausgefouert", + "Test webhook delivered successfully": "Test-Webhook erfollegräich geliwwert", + "Test webhook delivery failed": "Test-Webhook-Liwwerung feelgeschloen", + "Test webhook sent successfully": "Test-Webhook erfollegräich geschéckt", + "Testing...": "Gëtt getest...", + "Text Chunk #{id}": "Tekstfragment #{id}", + "Text Extraction": "Tekstextractie", + "Text Extraction Engine": "Tekstextractie-engine", + "Text chunks": "Textstécker", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Textstécker ginn während der Datei-Extraktioun generéiert an an der Datebank gespäichert. D'Vektoriséierung liest dës virstéckelt Dateien an wandelt se an Embeddings ëm.", + "Text extracted successfully": "Text erfollegräich extrahéiert", + "Text extraction disabled": "Textextraktioun desaktivéiert", + "The Nextcloud Calendar app is not installed or enabled on this server.": "The Nextcloud Calendar app is not installed or enabled on this server.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "The Nextcloud Contacts app is not installed or enabled on this server.", + "The Nextcloud Deck app is not installed or enabled on this server.": "The Nextcloud Deck app is not installed or enabled on this server.", + "The Nextcloud Mail app is not installed or enabled on this server.": "The Nextcloud Mail app is not installed or enabled on this server.", + "The URL where webhook events will be sent": "D'URL wou d'Webhook-Evenementer higeschéckt ginn", + "The requested conversation does not exist": "Dat ugefrote Gespréich existéiert net", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.", + "The saved view and all its search configuration": "Déi gespäichert Usiicht an all hir Sichkonfiguratioun", + "There are no audit trail entries matching your current filters.": "Et gi keng Audit-Trail-Andréi déi ären aktuelle Filteren entspriechen.", + "There are no deleted items matching your current filters.": "Et gi keng geläscht Artikelen déi ären aktuelle Filteren entspriechen.", + "There are no search trail entries matching your current filters.": "Et gi keng Sich-Trail-Andréi déi ären aktuelle Filteren entspriechen.", + "There are no webhook log entries matching your filters.": "Et gi keng Webhook-Protokoll-Andréi déi äre Filteren entspriechen.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Ze worden bewaard volgens de ingestelde bewaartermijn van hun schema en daarna permanent verwijderd.", + "This action cannot be undone.": "Dës Aktioun kann net réckgängeg gemaach ginn.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Dës Aktioun kann net réckgängeg gemaach ginn. Vergewëssert Iech, datt keng Sammlunge dëse ConfigSet benotzen.", + "This action cannot be undone. Make sure no collections are using this configset.": "Deze actie kan niet ongedaan worden gemaakt. Make sure no collections are using this ConfigSet.", + "This action will submit a deactivation request to your administrators.": "Deze actie dient een deactiveringsverzoek in bij uw beheerders.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Dësen Agent huet keng konfiguréierbar Usiichten oder Tools, awer Dir kënnt nach ëmmer d'RAG-Astellungen hei drënner upassen.", + "This analysis may take some time": "Dës Analyse kann e bësse Zäit daueren", + "This audit trail entry does not contain any change information.": "Dësen Audit-Trail-Andrag enthält keng Ännerungsinformatiounen.", + "This configuration has no data defined.": "Dës Konfiguratioun huet keng Donnéeën definéiert.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Dësen Endpunkt ass veroudert. Benotzt amplaz stéckbaséiert Endpunkten.", + "This entity has no relations to objects or files": "Dës Entitéit huet keng Relatiounen zu Objeten oder Dateien", + "This is a test webhook from OpenRegister": "Dëst ass en Test-Webhook vun OpenRegister", + "This is not valid JSON": "Dit is geen geldige JSON", + "This key already exists on this schema": "Deze sleutel bestaat al in dit schema", + "This schema must use Magic Table configuration to sync": "Dëse Schema muss d'Magic-Table-Konfiguratioun benotze fir ze synchroniséieren", + "This schema must use magic table configuration to sync": "This schema must use Magic Table configuratie to sync", + "This source has no associated registers.": "Dës Quell huet keng zougeuerdent Registeren.", + "This token will only be shown once. Copy it now.": "Dit token wordt slechts eenmalig weergegeven. Kopieer het nu.", + "This will copy the _default ConfigSet with the new name": "Dëst kopéiert de _default-ConfigSet mam neien Numm", + "This will copy the _default configset with the new name": "Dit kopieert de _default-configset met de nieuwe naam", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Dit verwijdert ALLE embeddings (vectoren) permanent uit de database. Je moet alle objecten en bestanden opnieuw vectoriseren. Deze actie kan niet ongedaan worden gemaakt.\n\nWeet je zeker dat je wilt doorgaan?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Dit zal permanent verwijderen ALL embeddings (vectors) from the database. You will need to re-vectorize all objecten and bestands. Deze actie kan niet ongedaan worden gemaakt.\\n\\nWeet u zeker dat u wilt continue?", + "This will permanently delete:": "Dëst läscht definitiv:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Hierdoor worden {count} reeds voorlopig verwijderde objecten permanent uit de database verwijderd. Deze actie kan niet ongedaan worden gemaakt.", + "Timeout (seconds)": "Timeout (Sekonnen)", + "Timestamp": "Zäitstempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tipp: Aktivéiert nëmmen Usiichten déi semantesch Sich brauchen, fir d'Embedding-Käschten ze minimiséieren. Einfach Nokuck-Tabelle brauche selten eng Vektoriséierung.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: Only enable weergaves that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.", + "Title": "Titel", + "Title *": "Titel *", + "Title Template": "Titelsjabloon", + "Title*": "Titel*", + "To (e.g. -1 year or 2025-12-31)": "Tot (bijv. -1 jaar of 2025-12-31)", + "To Date": "Bis zum Datum", + "To add files larger than or equal to 512MB, go to the": "Ga naar de volgende sectie om bestanden van 512 MB of groter toe te voegen:", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Fir d'Stéckgréisst oder d'Strategie unzepassen, gitt op Datei-Konfiguratioun → Verschaffungslimiten.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Ga naar bestandsconfiguratie → verwerkingslimieten om de fragmentgrootte of -strategie aan te passen.", + "To change the embedding provider or model, go to LLM Configuration.": "Fir den Embedding-Provider oder de Modell z'änneren, gitt op LLM-Konfiguratioun.", + "To change the embedding provider or model, go to LLM configuration.": "Ga naar LLM-configuratie om de embeddingprovider of het model te wijzigen.", + "To date": "Bis zum Datum", + "Toggle search sidebar": "Sich-Säiteleescht ëmschalten", + "Token Created": "Token aangemaakt", + "Token Quota (per request)": "Tokenquotum (per verzoek)", + "Token copied to clipboard": "Token gekopieerd naar klembord", + "Token name": "Tokennaam", + "Token revoked": "Token ingetrokken", + "Tools": "Tools", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.", + "Tools respect the agent's own role and organization boundaries.": "Tools respecteren de eigen rol en organisatiegrenzen van de agent.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Tools respect the agent's views, permissions, and organization boundaries.", + "Top Deleters": "Top-Läscher", + "Total": "Total", + "Total Audit Trails": "Total Audit-Trails", + "Total Chunks Available:": "Total verfügbar Stécker:", + "Total Deleted Items": "Total geläscht Artikelen", + "Total Members": "Total Memberen", + "Total Objects": "Total Objeten", + "Total Objects in Database:": "Total Objeten an der Datebank:", + "Total Organisations": "Total Organisatiounen", + "Total Results": "Total Resultater", + "Total Searches": "Total Sichen", + "Total Size": "Total Gréisst", + "Total Storage": "Total Späicher", + "Total events": "Total events", + "Total:": "Total:", + "Totals": "Totaler", + "Try searching by UUID or with different keywords": "Probeer te zoeken op UUID of met andere trefwoorden", + "Type": "Typ", + "Type \"DELETE COLLECTION\" to confirm:": "Typ \"COLLECTIE VERWIJDEREN\" om te bevestigen:", + "Type Issue": "Typproblem", + "Type Variations:": "Typvariatiounen:", + "Type filter (optional, e.g. email)": "Type filter (optional, e.g. email)", + "Type must be \"positive\" or \"negative\"": "Type moet \"positive\" of \"negative\" zijn", + "Type to search for groups": "Tippt fir no Gruppen ze sichen", + "Type to search for objects...": "Typ om objecten te zoeken...", + "Type to search for organisations": "Typ om organisaties te zoeken", + "Type to search for users": "Tippt fir no Benotzer ze sichen", + "Type to search groups...": "Tippt fir Gruppen ze sichen...", + "Type to search users...": "Tippt fir Benotzer ze sichen...", + "Type to search...": "Typ om te zoeken...", + "Type your username to confirm": "Typ uw gebruikersnaam ter bevestiging", + "URL": "URL", + "URL where Ollama is running": "URL wou Ollama leeft", + "UUID copied to clipboard": "UUID gekopieerd naar klembord", + "UUID:": "UUID:", + "Unchanged": "Onverännert", + "Unique Search Terms": "Eenzegaarteg Sichbegrëffer", + "Unique Terms": "Eenzegaarteg Begrëffer", + "Unknown": "Onbekannt", + "Unknown Object": "Onbekend object", + "Unknown Register": "Onbekannte Register", + "Unknown Schema": "Onbekannte Schema", + "Unknown error": "Onbekannte Feeler", + "Unknown widget type: {type}": "Unknown widget type: {type}", + "Unlink email": "Unlink email", + "Unlink event": "Unlink event", + "Unnamed Object": "Naamloos object", + "Untitled": "Untitled", + "Untitled View": "Usiicht ouni Titel", + "Update": "Aktualiséieren", + "Update Operations": "Aktualiséierungsoperatiounen", + "Update available: {local} → {remote}": "Update beschikbaar: {local} → {remote}", + "Update configuration: ...": "Konfiguratioun aktualiséieren: ...", + "Update register OAS: ...": "Register-OAS aktualiséieren: ...", + "Update vectors when object data changes (recommended for accurate search)": "Vektore aktualiséiere wann d'Objet-Donnéeën änneren (recommandéiert fir eng korrekt Sich)", + "Updated": "Aktualiséiert", + "Updated At": "Aktualiséiert um", + "Updated:": "Aktualiséiert:", + "Upload new avatar": "Nieuwe avatar uploaden", + "Uploading configuration sets...": "Uploading configuration sets...", + "Uptime:": "Uptime:", + "Url": "URL", + "Usage Count": "Gebrauchszuel", + "Use AI agents for processing and analysis": "Gebruik AI-agenten voor verwerking en analyse", + "Use Large Language Model features": "Gebruik groot taalmodel-functies", + "Use SolrCloud with Zookeeper for distributed search": "SolrCloud mat Zookeeper fir eng verdeelt Sich benotzen", + "Use SolrCloud with zookeeper for distributed search": "Gebruik SolrCloud met zookeeper voor gedistribueerd zoeken", + "Use VNG APIs instead of MongoDB.": "Gebruik VNG API's in plaats van MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Gebruik standaardbereiken (Afgelopen 7/30/90 dagen, Afgelopen jaar, Ouder)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Gebruik externe opslag (bijv. MongoDB) in plaats van de interne Nextcloud-opslag.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Benotzt Filteren fir Audit-Trail-Andréi no Register, Schema, Aktiounstyp, Benotzer, Datumsberäich oder Objet-ID anzegrenzen.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Benotzt Filteren fir geläscht Artikelen no Register, Schema, Läschdatum oder dem Benotzer deen se geläscht huet anzegrenzen.", + "Use filters to narrow down entities by type or category.": "Use filters to narrow down entities by type or category.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Benotzt Filteren fir Sich-Trail-Andréi no Register, Schema, Erfollegsstatus, Benotzer, Datumsberäich, Sichbegrëffer oder Leeschtungsmetrike anzegrenzen.", + "Used By Collections": "Benotzt vu Sammlungen", + "Used by": "Gebruikt door", + "User": "Benotzer", + "User Agent Statistics": "User-Agent-Statistiken", + "User Description": "Benotzerbeschreiwung", + "User Quota": "Gebruikersquotum", + "User rate limits cleared successfully": "Benotzer-Rate-Limiten erfollegräich geläscht", + "User removed successfully": "Benotzer erfollegräich ewechgeholl", + "User-friendly description shown in forms and help text": "Benotzerfrëndlech Beschreiwung déi a Formularen an am Hëllefstext ugewise gëtt", + "Username": "Benotzernumm", + "Username for SOLR authentication (optional)": "Benotzernumm fir d'SOLR-Authentifizéierung (optional)", + "Users": "Benotzer", + "Uses": "Gebruikt", + "Using Pre-Generated Chunks": "Virgeneréiert Stécker benotzen", + "Validate": "Validéieren", + "Validate Objects": "Objeten validéieren", + "Validation completed for {schema}": "Validatie voltooid voor {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validatie voltooid voor {schema}: {processed} verwerkt, {updated} bijgewerkt, {failed} mislukt", + "Validation completed successfully": "Validéierung erfollegräich ofgeschloss", + "Validation failed": "Validéierung feelgeschloen", + "Value": "Wäert", + "Value list (split on ,)": "Waardelijst (gesplitst op ,)", + "Vector Dimensions": "Vektordimensiounen", + "Vector Search Backend": "Vektorsich-Backend", + "Vector field: _embedding_": "Vektorfeld: _embedding_", + "Vectorization Triggers": "Vektoriséierungs-Trigger", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vectorisatie voltooid: {vectorized} objecten gevectoriseerd, {failed} mislukt", + "Vectorization failed: {error}": "Vectorisatie mislukt: {error}", + "Vectorize All Files": "All Dateie vektoriséieren", + "Vectorize All Objects": "All Objete vektoriséieren", + "Vectorize all file types": "All Dateitype vektoriséieren", + "Vectorize all views": "All Usiichte vektoriséieren", + "Vectorize on object creation": "Bei der Objet-Erstellung vektoriséieren", + "Vectors will be stored in your existing object and file collections": "Vektore ginn an ären existéierenden Objet- an Datei-Sammlunge gespäichert", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Verifying server connectivity...", + "Version": "Versioun", + "Version Information": "Versie-informatie", + "Very High": "Ganz héich", + "Very high": "Ganz héich", + "View": "Weergave", + "View API Docs": "API-Dokumentatioun ukucken", + "View API Documentation": "API-documentatie bekijken", + "View APIs": "API's bekijken", + "View Changes": "Ännerungen ukucken", + "View Details": "Detailer ukucken", + "View Error": "Feeler ukucken", + "View File": "Datei ukucken", + "View Full Details": "Vollstänneg Detailer ukucken", + "View Logs": "Protokoller ukucken", + "View Name": "Usiicht-Numm", + "View Object": "Objet ukucken", + "View Parameters": "Usiicht-Parameteren", + "View Selection": "Usiicht-Auswiel", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Sich-Trail-Protokoller mat erweiderten Filter- an Analysefäegkeeten ukucken an analyséieren", + "View and analyze system audit trails with advanced filtering capabilities": "System-Audit-Trails mat erweiderte Filterfäegkeeten ukucken an analyséieren", + "View and manage authorization across registers and schemas": "Autorisatie voor registers en schema's bekijken en beheren", + "View deleted successfully": "Usiicht erfollegräich geläscht", + "View entity details and manage relations": "Entitéitsdetailer ukucken a Relatioune verwalten", + "View name": "Usiicht-Numm", + "View name is required": "Usiicht-Numm ass erfuerderlech", + "View object": "Object bekijken", + "View saved successfully!": "Usiicht erfollegräich gespäichert!", + "View search analytics and manage search logs": "Sichanalysen ukucken a Sichprotokoller verwalten", + "View successfully deleted": "Usiicht erfollegräich geläscht", + "View successfully updated": "Usiicht erfollegräich aktualiséiert", + "View updated successfully!": "Usiicht erfollegräich aktualiséiert!", + "View webhook delivery logs and filter by webhook": "Webhook-Liwwerungsprotokoller ukucken an no Webhook filteren", + "View {count} more": "{count} meer bekijken", + "Views": "Usiichten", + "Visible to end users": "Zichtbaar voor eindgebruikers", + "Visible to users": "Sichtbar fir Benotzer", + "Wait for Response": "Op Äntwert waarden", + "Wait for webhook response before continuing (required for request/response flows)": "Op d'Webhook-Äntwert waarde virum Weiderfueren (erfuerderlech fir Ufro-/Äntwertflëss)", + "Waiting for cluster synchronization...": "Waiting for cluster synchronization...", + "Warmup Names Cache": "Nimm-Cache opwiermen", + "Warmup failed": "Opwarming mislukt", + "Warning": "Warnung", + "Warning:": "Warnung:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.", + "Webhook": "Webhook", + "Webhook Log Details": "Webhook-Protokoll-Detailer", + "Webhook Logs": "Webhook-Protokoller", + "Webhook created successfully": "Webhook erfollegräich erstallt", + "Webhook deleted": "Webhook geläscht", + "Webhook retry delivered successfully": "Webhook-Neiversuch erfollegräich geliwwert", + "Webhook retry delivery failed": "Webhook-Neiversuch-Liwwerung feelgeschloen", + "Webhook updated": "Webhook aktualiséiert", + "Webhook updated successfully": "Webhook erfollegräich aktualiséiert", + "Webhooks": "Webhooks", + "Weekly": "Wëchentlech", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Wanneer de agent zonder gebruikerssessie draait (bijv. geplande taken), wordt de context van deze gebruiker gebruikt.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Wanneer uitgeschakeld, toont dit facet alleen waarden uit dit schema en bevat het een schemafilter wanneer geselecteerd.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Wanneer ingeschakeld, worden bij het opslaan van dit object ook de doelobjecten bijgewerkt om een verwijzing terug naar dit object op te nemen.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Wanneer ingeschakeld, wordt deze eigenschap verwijderd uit het bronobject nadat de doelobjecten zijn bijgewerkt.", + "Workflow Management": "Workflowbeheer", + "Workflows": "Workflows", + "Wrap webhook payload in CloudEvents format for better interoperability": "Webhook-Payload am CloudEvents-Format akapsele fir eng besser Interoperabilitéit", + "Wrap webhook payload in cloudevents format for better interoperability": "Webhookpayload inpakken in cloudevents-formaat voor betere interoperabiliteit", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Jo", + "You": "Dir", + "You are not yet a member of any organisations.": "U bent nog geen lid van een organisatie.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Dir kënnt nei ConfigSets op Basis vum _default-Template erstellen, oder personaliséiert direkt op Äre SOLR-Server eroplueden.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "U kunt nieuwe configsets aanmaken op basis van de _default-sjabloon, of aangepaste configuraties rechtstreeks uploaden naar uw SOLR-server.", + "You do not have access to this conversation": "Dir hutt kee Zougang zu dësem Gespréich", + "You do not have permission to delete this conversation": "Dir hutt net d'Erlaabnis fir dëst Gespréich ze läschen", + "You do not have permission to modify this conversation": "Dir hutt net d'Erlaabnis fir dëst Gespréich z'änneren", + "You do not have permission to restore this conversation": "Dir hutt net d'Erlaabnis fir dëst Gespréich ze restauréieren", + "You do not have permission to view the permission matrix. Admin access is required.": "U hebt geen toestemming om de machtigingsmatrix te bekijken. Beheerderstoegang is vereist.", + "You have selected {count} properties to add to the schema.": "U hebt {count} eigenschappen geselecteerd om aan het schema toe te voegen.", + "You must be logged in to favorite views": "Dir musst ageloggt si fir Usiichten als Favoritt ze markéieren", + "You need an AI agent to start a conversation.": "Dir braucht e KI-Agent fir e Gespréich unzefänken.", + "Your Fireworks AI API key. Get one at": "Äre Fireworks-AI-API-Schlëssel. Kritt een op", + "Your OpenAI API key. Get one at": "Äre OpenAI-API-Schlëssel. Kritt een op", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Äre Feedback gouf registréiert. Optional kënnt Dir hei zousätzlech Detailer ginn...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Uw feedback is opgeslagen. Optioneel kunt u hier aanvullende details opgeven...", + "Zookeeper Hosts": "Zookeeper-Hosten", + "Zookeeper Port": "Zookeeper-Port", + "Zookeeper Settings (SolrCloud)": "Zookeeper-Astellungen (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper-Verbindungsstring fir SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper-Portnummer (optional, Standard ass 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?", + "and accessible to all users in selected groups (or all users if no groups selected).": "and accessible to all users in selected groups (or all users if no groups selected).", + "and add the files there.": "en voeg de bestanden daar toe.", + "approved": "goedgekeurd", + "are only accessible to invited users.": "are only accessible to invited users.", + "by": "by", + "chunk": "Stéck", + "chunks": "Stécker", + "chunks processed": "Stécker verschafft", + "completed": "voltooid", + "configuration(s)": "Konfiguratioun(en)", + "conversationId is required": "conversationId ass erfuerderlech", + "create": "aanmaken", + "delete": "verwijderen", + "deleted objects section": "sectie verwijderde objecten", + "e.g., lib/Settings/config.json": "z. B. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "z. B. lib/Settings/register.json", + "e.g., lib/settings/config.json": "bijv., lib/settings/config.json", + "e.g., lib/settings/register.json": "bijv., lib/settings/register.json", + "execute": "uitvoeren", + "failed": "feelgeschloen", + "file": "Datei", + "files": "Dateien", + "file{plural}": "bestand{plural}", + "folder": "map", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "in {register}", + "in_progress": "in behandeling", + "inherited": "overgeërfd", + "list": "lijst", + "log{plural}": "log{plural}", + "manage": "beheren", + "members": "Memberen", + "menu or contact someone with permission to create agents.": "Menü oder kontaktéiert een mat der Erlaabnis fir Agenten ze erstellen.", + "message content is required": "Message-Inhalt ass erfuerderlech", + "messages": "Messagen", + "missing": "missing", + "myapp": "mijnapp", + "n8n URL and API key are required": "n8n-URL an API-Schlëssel sinn erfuerderlech", + "n8n configuration saved successfully": "n8n-Konfiguratioun erfollegräich gespäichert", + "n8n connection not configured": "n8n-Verbindung net konfiguréiert", + "n8n connection successful": "n8n-Verbindung erfollegräich", + "n8n connection test failed: {message}": "n8n-verbindingstest mislukt: {message}", + "n8n connection test successful": "n8n-Verbindungstest erfollegräich", + "n8n initialization failed: {message}": "n8n-initialisatie mislukt: {message}", + "n8n integration disabled": "n8n-Integratioun desaktivéiert", + "n8n integration enabled": "n8n-Integratioun aktivéiert", + "n8n project initialized successfully": "n8n-Projet erfollegräich initialiséiert", + "n8n settings saved successfully": "n8n-Astellunge erfollegräich gespäichert", + "notify_push configuration guide": "notify_push-Konfiguratiounsguide", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push ass installéiert awer OpenRegister huet nach keen erfollegräiche Push bestätegt. Léist eng Objet-Späicherung aus fir z'aktivéieren, oder iwwerpréift Är notify_push-Konfiguratioun.", + "notify_push is installed but not yet active": "notify_push ass installéiert awer nach net aktiv", + "objectType: object\naction: created": "objectType: object\nactie: aangemaakt", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "Objeten", + "objects processed": "Objeten verschafft", + "object{plural}": "object{plural}", + "or pick a range": "oder e Beräich auswielen", + "org-...": "org-...", + "pending": "in afwachting", + "provides a good balance between speed and accuracy.": "bitt e gudde Gläichgewiicht tëscht Geschwindegkeet a Genauegkeet.", + "public": "public", + "read": "lezen", + "register(s)": "Register", + "register(s) selected": "Register ausgewielt", + "register{plural}": "register{plural}", + "rejected": "afgewezen", + "required": "erfuerderlech", + "results": "Resultater", + "schema(s)": "Schema(en)", + "schema(s) selected": "Schema(en) ausgewielt", + "schema{plural}": "schema{plural}", + "selected objects": "selected objects", + "set": "set", + "sk-...": "sk-...", + "this application": "dës Applikatioun", + "to": "bis", + "type must be \"positive\" or \"negative\"": "type moet \"positive\" of \"negative\" zijn", + "update": "bijwerken", + "vectorized": "vektoriséiert", + "view": "bekijken", + "views selected": "Usiichten ausgewielt", + "waiting": "wachtend", + "{active} active, {deleted} deleted": "{active} actief, {deleted} verwijderd", + "{count} audit trails cleared successfully": "{count} auditsporen succesvol gewist", + "{count} compliance issue(s) detected": "{count} compliance issue(s) detected", + "{count} deletions": "{count} verwijderingen", + "{count} documents found": "{count} documenten gevonden", + "{count} email": "{count} email", + "{count} emails": "{count} emails", + "{count} entries": "{count} vermeldingen", + "{count} issues": "{count} problemen", + "{count} members": "{count} leden", + "{count} searches": "{count} zoekopdrachten", + "{count} selected": "{count} geselecteerd", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} object(s) matched. {erased} erased.", + "{minutes} minutes ago": "{minutes} minuten geleden", + "{percentage}% of objects": "{percentage}% van objecten", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} am {register} / {schema}", + "{type} cache cleared successfully": "{type} cache cleared successfully", + "ℹ️ Current Configuration": "ℹ️ Aktuell Konfiguratioun", + "ℹ️ current configuration": "ℹ️ huidige configuratie", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ GEVAAR: Alle {total} objecten worden PERMANENT VERWIJDERD uit de database. Deze actie is ONHERSTELBAAR en kan niet ongedaan worden gemaakt!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ WAARSCHUWING PERMANENTE VERWIJDERING ⚠️\n\nU staat op het punt ALLE objecten voor schema \"{schema}\" PERMANENT te verwijderen:\n\n• Actieve objecten: {active}\n• Voorlopig verwijderde objecten: {deleted}\n• Totaal: {total}\n\nDeze objecten worden volledig verwijderd uit de database en KUNNEN NIET worden hersteld.\n\nWeet u het zeker?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ WAARSCHUWING PERMANENTE VERWIJDERING ⚠️\n\nU staat op het punt {count} voorlopig verwijderde objecten voor schema \"{schema}\" PERMANENT te verwijderen.\n\nDeze objecten worden volledig verwijderd uit de database en KUNNEN NIET worden hersteld.\n\nWeet u het zeker?", + "⚡ Batch Processing": "⚡ Batch-Veraarbechtung", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR can handle millions of documents with sub-second search response times...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Ëmfaassende Kontext ✓ Manner wahrscheinlech datt Detailer feelen ✗ Méi lues Äntwerten ✗ Kann manner relevant Informatiounen enthalen", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Méi séier Äntwerten ✓ Méi fokusséiert Äntwerten ✗ Kann relevant Informatioune verpassen", + "✨ AI Features": "✨ KI-Funktiounen", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Faceted search allows users to drill down into results by categories...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!", + "💰 View Selection (Cost Optimization)": "💰 Usiicht-Auswiel (Käschtenoptiméierung)", + "📄 Object Serialization": "📄 Objet-Serialiséierung", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR provides rich analytics and statistics about search performance...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets contain the schema and configuration files for your search index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replication ensures your search index is available even if nodes fail...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Vektoriséierungs-Astellungen", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Auto-scaling can dynamically add or remove nodes based on load...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Security features include authentication, authorization, and SSL encryption...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "pluralForm": "nplurals=2; plural=(n != 1);", + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-mails" + ], + "Purge {count} object from database": [ + "Verwijder {count} object uit de database", + "Verwijder {count} objecten uit de database" + ], + "Restore {count} object": [ + "Herstel {count} object", + "Herstel {count} objecten" + ], + "Successfully restored {count} object": [ + "{count} object succesvol hersteld", + "{count} objecten succesvol hersteld" + ], + "Delete {count} object": [ + "Verwijder {count} object", + "Verwijder {count} objecten" + ], + "Object successfully deleted": [ + "Object succesvol verwijderd", + "Objecten succesvol verwijderd" + ] + } +} diff --git a/l10n/lt.js b/l10n/lt.js new file mode 100644 index 0000000000..2051bfedfc --- /dev/null +++ b/l10n/lt.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(be temos)", + "(no title)" : "(be pavadinimo)", + "(unnamed)" : "(be vardo)", + "(untitled card)" : "(kortelė be pavadinimo)", + "Activities" : "Veikla", + "Add Endpoint" : "Pridėti galinį tašką", + "Add Source" : "Pridėti šaltinį", + "Add card" : "Pridėti kortelę", + "Add contact" : "Pridėti kontaktą", + "All clear" : "Viskas tvarkoje", + "Articles" : "Straipsniai", + "Code" : "Kodas", + "Compliance" : "Atitiktis", + "Contacts" : "Kontaktai", + "Create Agent" : "Sukurti agentą", + "Create Application" : "Sukurti programą", + "Create event" : "Sukurti įvykį", + "Erasure complete" : "Ištrynimas baigtas", + "Erasure preview" : "Ištrynimo peržiūra", + "Expand details" : "Išskleisti išsamią informaciją", + "Save changes" : "Įrašyti pakeitimus", + "📄 Object Serialization" : "📄 Objekto serializavimas", + "🔢 Vectorization Settings" : "🔢 Vektorizavimo nustatymai", + "💰 View Selection (Cost Optimization)" : "💰 Rodinių pasirinkimas (sąnaudų optimizavimas)", + "AI Assistant" : "DI asistentas", + "AI Chat" : "DI pokalbis", + "API Key" : "API raktas", + "API tokens saved successfully" : "API prieigos raktai sėkmingai įrašyti", + "About ConfigSets" : "Apie ConfigSets", + "Action" : "Veiksmas", + "Action Distribution" : "Veiksmų pasiskirstymas", + "Action:" : "Veiksmas:", + "Actions" : "Veiksmai", + "Active" : "Aktyvus", + "Active Collections" : "Aktyvios kolekcijos", + "Active Filters" : "Aktyvūs filtrai", + "Active Organisations" : "Aktyvios organizacijos", + "Active filters:" : "Aktyvūs filtrai:", + "Active organisation set successfully" : "Aktyvi organizacija sėkmingai nustatyta", + "Activity Period" : "Veiklos laikotarpis", + "Add Groups" : "Pridėti grupes", + "Add Schema" : "Pridėti schemą", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Pridėkite schemų pavadinimus, aprašymus ir registrų informaciją, kad paieška turėtų išsamesnį kontekstą", + "Add to favorites" : "Pridėti prie mėgstamų", + "Added" : "Pridėta", + "Added to favorites" : "Pridėta prie mėgstamų", + "Additional Information" : "Papildoma informacija", + "Additional feedback saved. Thank you!" : "Papildomas atsiliepimas įrašytas. Ačiū!", + "Admin" : "Administratorius", + "Advanced" : "Išplėstiniai", + "Advanced Filters" : "Išplėstiniai filtrai", + "Advanced Options" : "Išplėstinės parinktys", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Išplėstiniai filtrai negalimi naudojant duomenų bazės šaltinį. Filtravimo parinktims perjunkite į Auto arba SOLR indeksą.", + "After" : "Po", + "Agent deleted successfully" : "Agentas sėkmingai pašalintas", + "Agents" : "Agentai", + "All" : "Visi", + "All Categories" : "Visos kategorijos", + "All Collections" : "Visos kolekcijos", + "All Confidence Levels" : "Visi patikimumo lygiai", + "All ConfigSets" : "Visi ConfigSets", + "All Files" : "Visi failai", + "All Levels" : "Visi lygiai", + "All Types" : "Visi tipai", + "All Webhooks" : "Visi žiniatinklio kabliukai", + "All actions" : "Visi veiksmai", + "All audit trails cleared successfully" : "Visi audito žurnalai sėkmingai išvalyti", + "All registers" : "Visi registrai", + "All schemas" : "Visos schemos", + "All searches" : "Visos paieškos", + "All users" : "Visi naudotojai", + "An error occurred while clearing audit trails" : "Valant audito žurnalus įvyko klaida", + "An error occurred while deleting the audit trail" : "Šalinant audito žurnalą įvyko klaida", + "An error occurred while deleting the view" : "Šalinant rodinį įvyko klaida", + "An error occurred while permanently deleting the objects" : "Visam laikui šalinant objektus įvyko klaida", + "Analysis completed:" : "Analizė baigta:", + "Analysis steps:" : "Analizės žingsniai:", + "Analytics" : "Analitika", + "Analyze Objects" : "Analizuoti objektus", + "Analyze Properties" : "Analizuoti savybes", + "Analyze existing properties for improvement opportunities" : "Analizuoti esamas savybes ieškant tobulinimo galimybių", + "Analyzing..." : "Analizuojama...", + "Any favorites and sharing settings for this view" : "Visi šio rodinio mėgstamų ir bendrinimo nustatymai", + "Any user" : "Bet kuris naudotojas", + "App store cache invalidated successfully" : "Programų parduotuvės podėlis sėkmingai panaikintas", + "Application deleted successfully" : "Programa sėkmingai pašalinta", + "Applications" : "Programos", + "Apply Changes" : "Taikyti pakeitimus", + "Archive" : "Archyvas", + "Archive conversation" : "Archyvuoti pokalbį", + "Archived conversations are hidden from your active list" : "Archyvuoti pokalbiai paslepiami iš jūsų aktyvaus sąrašo", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Ar tikrai norite išvalyti senus paieškos žurnalus? Tai pašalins senesnius nei 30 dienų įrašus.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Ar tikrai norite pašalinti pasirinktus audito žurnalus? Šio veiksmo atšaukti negalima.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Ar tikrai norite pašalinti pasirinktus paieškos žurnalus? Šio veiksmo atšaukti negalima.", + "Are you sure you want to delete this ConfigSet?" : "Ar tikrai norite pašalinti šį ConfigSet?", + "Are you sure you want to permanently delete" : "Ar tikrai norite visam laikui pašalinti", + "Ask a question..." : "Užduokite klausimą...", + "Ask questions about your data using natural language" : "Užduokite klausimus apie savo duomenis naudodami natūraliąją kalbą", + "Attempt" : "Bandymas", + "Audit Trail Changes" : "Audito žurnalo pakeitimai", + "Audit Trail Details" : "Audito žurnalo išsami informacija", + "Audit Trail Management" : "Audito žurnalų valdymas", + "Audit Trail Statistics" : "Audito žurnalų statistika", + "Audit Trails" : "Audito žurnalai", + "Audit trail data copied to clipboard" : "Audito žurnalo duomenys nukopijuoti į iškarpinę", + "Audit trail deleted successfully" : "Audito žurnalas sėkmingai pašalintas", + "Audit trail successfully deleted" : "Audito žurnalas sėkmingai pašalintas", + "Authentication" : "Tapatybės nustatymas", + "Auto-commit disabled" : "Automatinis patvirtinimas išjungtas", + "Auto-commit enabled" : "Automatinis patvirtinimas įjungtas", + "Auto-create Default Organisation" : "Automatiškai sukurti numatytąją organizaciją", + "Auto-retry failed vectorizations" : "Automatiškai kartoti nepavykusius vektorizavimus", + "Automatically commit changes to SOLR index" : "Automatiškai patvirtinti pakeitimus SOLR indekse", + "Automatically create a default organisation if none exists when the app is initialized" : "Automatiškai sukurti numatytąją organizaciją, jei jokia neegzistuoja inicijuojant programą", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Automatiškai generuoti vektorinius įterpinius iš teksto fragmentų, kai failai įkeliami ir apdorojami", + "Automatically generate vector embeddings when objects are created or updated" : "Automatiškai generuoti vektorinius įterpinius, kai objektai sukuriami arba atnaujinami", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Automatiškai kartoti nepavykusius vektorizavimo bandymus (daugiausiai 3 kartus)", + "Available Filters" : "Galimi filtrai", + "Available Workflows" : "Galimi darbo srautai", + "Avg Execution Time" : "Vid. vykdymo laikas", + "Avg Members/Org" : "Vid. narių / organizacijoje", + "Avg Object Views/Session" : "Vid. objektų peržiūrų / seanse", + "Avg Response Time" : "Vid. atsako laikas", + "Avg Results/Search" : "Vid. rezultatų / paieškoje", + "Avg Searches/Session" : "Vid. paieškų / seanse", + "Back" : "Atgal", + "Back to Entities" : "Atgal į esybes", + "Back to Registers" : "Atgal į registrus", + "Back to Webhooks" : "Atgal į žiniatinklio kabliukus", + "Back to applications" : "Atgal į programas", + "Back to entities" : "Atgal į esybes", + "Backend updated successfully. Please reload the application." : "Vidinė sąsaja sėkmingai atnaujinta. Įkelkite programą iš naujo.", + "Base URL (Optional)" : "Bazinis URL (neprivaloma)", + "Basic Information" : "Pagrindinė informacija", + "Batch Size" : "Paketo dydis", + "Batch extraction completed" : "Paketinis ištraukimas baigtas", + "Before" : "Prieš", + "Before object vectorization can work:" : "Kad objektų vektorizavimas veiktų:", + "Behavior Issue" : "Elgsenos problema", + "Bookmarks" : "Žymės", + "Blob storage has been retired. All objects now use magic tables." : "Blob saugykla nebenaudojama. Visi objektai dabar naudoja stebuklingąsias lenteles.", + "Branch" : "Šaka", + "Bulk delete operation completed successfully" : "Masinio šalinimo operacija sėkmingai baigta", + "Bulk save operation completed successfully" : "Masinio įrašymo operacija sėkmingai baigta", + "Business Data" : "Verslo duomenys", + "Cache cleared successfully" : "Podėlis sėkmingai išvalytas", + "Calculate Sizes" : "Apskaičiuoti dydžius", + "Cancel" : "Atsisakyti", + "Cannot delete: objects are still attached" : "Negalima pašalinti: vis dar yra prikabintų objektų", + "Category" : "Kategorija", + "Change Type" : "Pakeitimo tipas", + "Changes" : "Pakeitimai", + "Changes copied to clipboard" : "Pakeitimai nukopijuoti į iškarpinę", + "Chat Model" : "Pokalbio modelis", + "Chat Provider" : "Pokalbio teikėjas", + "Chat Provider (RAG)" : "Pokalbio teikėjas (RAG)", + "Chat Settings" : "Pokalbio nustatymai", + "Chat provider connection successful!" : "Sėkmingai prisijungta prie pokalbio teikėjo!", + "Chat settings" : "Pokalbio nustatymai", + "Choose a register" : "Pasirinkite registrą", + "Choose a schema" : "Pasirinkite schemą", + "Choose how vector similarity calculations are performed for semantic search" : "Pasirinkite, kaip atliekami vektorių panašumo skaičiavimai semantinei paieškai", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Pasirinkite, kuriuos failų tipus įtraukti į vektorizavimo procesą. Bus apdorojami tik failai su ištrauktu tekstu ir fragmentais.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Pasirinkite, kuriuos rodinius įtraukti į vektorizavimo procesą. Palikite tuščią, kad būtų apdorojami visi rodiniai pagal jūsų konfigūraciją.", + "Chunk" : "Fragmentas", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Fragmentų šalinimas dar neįgyvendintas. Naudokite fragmentais pagrįstus galinius taškus.", + "Chunks" : "Fragmentai", + "Chunks to Vectorize:" : "Vektorizuotini fragmentai:", + "Cleanup Old Trails" : "Išvalyti senus žurnalus", + "Cleanup completed" : "Valymas baigtas", + "Clear All Embeddings" : "Išvalyti visus įterpinius", + "Clear Entries" : "Išvalyti įrašus", + "Clear Filtered Audit Trails" : "Išvalyti filtruotus audito žurnalus", + "Clear Filters" : "Išvalyti filtrus", + "Clear Index" : "Išvalyti indeksą", + "Clear Selection" : "Išvalyti pasirinkimą", + "Clear all" : "Išvalyti viską", + "Clear all filters" : "Išvalyti visus filtrus", + "Clear date range" : "Išvalyti datų intervalą", + "Clearing..." : "Valoma...", + "Close" : "Užverti", + "Collection Management" : "Kolekcijų valdymas", + "Collection Name" : "Kolekcijos pavadinimas", + "Collection assignments updated successfully" : "Kolekcijų priskyrimai sėkmingai atnaujinti", + "Collection cleared successfully" : "Kolekcija sėkmingai išvalyta", + "Collection copied successfully" : "Kolekcija sėkmingai nukopijuota", + "Collection created successfully" : "Kolekcija sėkmingai sukurta", + "Collection deleted successfully" : "Kolekcija sėkmingai pašalinta", + "Collection used to store and index file metadata and content" : "Kolekcija, naudojama failų metaduomenims ir turiniui saugoti bei indeksuoti", + "Collection used to store and index object data" : "Kolekcija, naudojama objektų duomenims saugoti bei indeksuoti", + "Collections:" : "Kolekcijos:", + "Commit Message" : "Patvirtinimo žinutė", + "Commit Within (ms)" : "Patvirtinti per (ms)", + "Commit message" : "Patvirtinimo žinutė", + "Compare current schema with real object data" : "Palyginti dabartinę schemą su tikrais objektų duomenimis", + "Completed" : "Baigta", + "Complex" : "Sudėtinga", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Sudėtingos užklausos: išplėstinės paieškos su keliais filtrais, operatoriais ir sudėtingais parametrų deriniais", + "Confidence Level" : "Patikimumo lygis", + "Confidence Score" : "Patikimumo įvertis", + "Config must be provided as an object" : "Konfigūracija turi būti pateikta kaip objektas", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSet valdymas", + "ConfigSet Name" : "ConfigSet pavadinimas", + "ConfigSet created successfully" : "ConfigSet sėkmingai sukurtas", + "ConfigSet deleted successfully" : "ConfigSet sėkmingai pašalintas", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets apibrėžia jūsų SOLR kolekcijų schemą ir konfigūraciją. Juose yra laukų apibrėžtys, analizatoriai ir kiti paieškos nustatymai.", + "Configuration" : "Konfigūracija", + "Configuration Data" : "Konfigūracijos duomenys", + "Configuration saved" : "Konfigūracija įrašyta", + "Configurations" : "Konfigūracijos", + "Configure Facets" : "Konfigūruoti aspektus", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigūruokite didžiųjų kalbos modelių (LLM) teikėjus DI funkcijoms, įskaitant semantinę paiešką, įterpinius ir pokalbį.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigūruokite pagrindinius SOLR serverio prisijungimo nustatymus, įskaitant tapatybės nustatymą ir tinklo parinktis. Branduoliams ir kolekcijoms valdyti naudokite atskirus ConfigSet ir Kolekcijų valdymo dialogus.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigūruokite, kaip duomenų bazės objektai konvertuojami į vektorinius įterpinius semantinei paieškai. Objektai vektorizuojami tiesiogiai, nereikalaujant teksto ištraukimo.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigūruokite, kaip objektai konvertuojami į tekstą prieš vektorizavimą. Šie nustatymai turi įtakos paieškos kokybei ir kontekstui.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigūruokite failų vektorizavimo parametrus. Šis procesas sugeneruos vektorinius įterpinius visiems ištrauktiems failų fragmentams.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigūruokite objektų vektorizavimo parametrus. Šis procesas sugeneruos vektorinius įterpinius visiems objektams, atitinkantiems jūsų rodinio filtrus.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigūruokite, kokių tipų duomenų ieškoti ir kiek šaltinių gauti", + "Connection Failed" : "Nepavyko prisijungti", + "Connection Settings" : "Prisijungimo nustatymai", + "Connection Successful!" : "Sėkmingai prisijungta!", + "Connection failed" : "Nepavyko prisijungti", + "Connection protocol" : "Prisijungimo protokolas", + "Connection successful" : "Sėkmingai prisijungta", + "Connection timeout in seconds" : "Prisijungimo skirtasis laikas sekundėmis", + "Constant delay between retries (5 minutes)" : "Pastovus delsimas tarp pakartojimų (5 minutės)", + "Constraint Issue" : "Apribojimo problema", + "Content Filters" : "Turinio filtrai", + "Continue to Properties" : "Tęsti į savybes", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Valdykite, kuriuos objektų rodinius reikia vektorizuoti, kad sumažintumėte API sąnaudas. Vektorizuokite tik tuos rodinius, kuriems naudinga semantinė paieška.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Valdykite, kuriuos rodinius ir įrankius DI gali naudoti šiame pokalbyje. Pagal numatymą įjungtos visos agento galimybės.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Valdo atsitiktinumą (0 = deterministinis, 2 = labai kūrybiškas)", + "Conversation ID is required" : "Reikia pokalbio ID", + "Conversation archived" : "Pokalbis archyvuotas", + "Conversation archived successfully" : "Pokalbis sėkmingai archyvuotas", + "Conversation cleared successfully" : "Pokalbis sėkmingai išvalytas", + "Conversation deleted" : "Pokalbis pašalintas", + "Conversation permanently deleted" : "Pokalbis pašalintas visam laikui", + "Conversation renamed" : "Pokalbis pervadintas", + "Conversation restored" : "Pokalbis atkurtas", + "Conversation title" : "Pokalbio pavadinimas", + "Copied!" : "Nukopijuota!", + "Copy" : "Kopijuoti", + "Copy Changes" : "Kopijuoti pakeitimus", + "Copy Collection" : "Kopijuoti kolekciją", + "Copy Data" : "Kopijuoti duomenis", + "Copy Full Data" : "Kopijuoti visus duomenis", + "Copying..." : "Kopijuojama...", + "Count" : "Kiekis", + "Counting objects..." : "Skaičiuojami objektai...", + "Costs" : "Sąnaudos", + "Create" : "Sukurti", + "Create Collection" : "Sukurti kolekciją", + "Create ConfigSet" : "Sukurti ConfigSet", + "Create New Collection" : "Sukurti naują kolekciją", + "Create New ConfigSet" : "Sukurti naują ConfigSet", + "Create Operations" : "Kūrimo operacijos", + "Create Webhook" : "Sukurti žiniatinklio kabliuką", + "Create a copy of collection:" : "Sukurti kolekcijos kopiją:", + "Create a new ConfigSet based on the _default template" : "Sukurti naują ConfigSet pagal _default šabloną", + "Create a new SOLR collection from an existing ConfigSet" : "Sukurti naują SOLR kolekciją iš esamo ConfigSet", + "Create your first AI agent to get started." : "Sukurkite savo pirmąjį DI agentą, kad pradėtumėte.", + "Created" : "Sukurta", + "Created:" : "Sukurta:", + "Creating..." : "Kuriama...", + "Current Filters" : "Dabartiniai filtrai", + "Current Type:" : "Dabartinis tipas:", + "Custom API endpoint if using a different region" : "Pasirinktinis API galinis taškas, jei naudojamas kitas regionas", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Pasirinktinės HTTP antraštės (po vieną eilutėje, formatas: Antraštės-Pavadinimas: reikšmė)", + "Daily" : "Kasdien", + "Dashboard" : "Skydelis", + "Data Source" : "Duomenų šaltinis", + "Data sources" : "Duomenų šaltiniai", + "Data type does not match observed values" : "Duomenų tipas neatitinka stebimų reikšmių", + "Database URL" : "Duomenų bazės URL", + "Database information refreshed" : "Duomenų bazės informacija atnaujinta", + "Date Range" : "Datų intervalas", + "Default" : "Numatytasis", + "Default Organisation" : "Numatytoji organizacija", + "Default: 5" : "Numatytasis: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Delsimas dvigubėja su kiekvienu bandymu (2, 4, 8 minutės...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Delsimas didėja tiesiškai (5, 10, 15 minučių...)", + "Delete" : "Pašalinti", + "Delete Application" : "Pašalinti programą", + "Delete Audit Trail" : "Pašalinti audito žurnalą", + "Delete Collection" : "Pašalinti kolekciją", + "Delete ConfigSet" : "Pašalinti ConfigSet", + "Delete Objects" : "Pašalinti objektus", + "Delete Operations" : "Šalinimo operacijos", + "Delete View" : "Pašalinti rodinį", + "Delete all objects in this schema" : "Pašalinti visus šios schemos objektus", + "Delete permanently" : "Pašalinti visam laikui", + "Delete this conversation?" : "Pašalinti šį pokalbį?", + "Delete view" : "Pašalinti rodinį", + "Deleted" : "Pašalinta", + "Deleted By" : "Pašalino", + "Deleted Date" : "Pašalinimo data", + "Deleted Items Management" : "Pašalintų elementų valdymas", + "Deleted This Week" : "Pašalinta šią savaitę", + "Deleted Today" : "Pašalinta šiandien", + "Deleted:" : "Pašalinta:", + "Deleting..." : "Šalinama...", + "Deletion Date Range" : "Pašalinimo datų intervalas", + "Deletion Statistics" : "Pašalinimo statistika", + "Deprecated" : "Nebenaudojama", + "Description" : "Aprašymas", + "Description:" : "Aprašymas:", + "Detect data types and patterns" : "Aptikti duomenų tipus ir šablonus", + "Detected At" : "Aptikta", + "Detected Format:" : "Aptiktas formatas:", + "Detected Issues:" : "Aptiktos problemos:", + "Disable" : "Išjungti", + "Disabled" : "Išjungta", + "Discover Files" : "Atrasti failus", + "Discovered Properties" : "Atrastos savybės", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Ar norite visam laikui pašalinti visus filtruotus audito žurnalo įrašus? Šio veiksmo atšaukti negalima.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Ar norite visam laikui pašalinti šį audito žurnalo įrašą? Šio veiksmo atšaukti negalima.", + "Documents" : "Dokumentai", + "Download API Spec" : "Atsisiųsti API specifikaciją", + "Edit" : "Redaguoti", + "Edit Register" : "Redaguoti registrą", + "Edit Schema" : "Redaguoti schemą", + "Edit View" : "Redaguoti rodinį", + "Edit Webhook" : "Redaguoti žiniatinklio kabliuką", + "Edit view" : "Redaguoti rodinį", + "Edit view details" : "Redaguoti rodinio išsamią informaciją", + "Effectiveness" : "Efektyvumas", + "Email" : "El. paštas", + "Emails" : "El. laiškai", + "Embedding Model" : "Įterpinių modelis", + "Embedding Provider" : "Įterpinių teikėjas", + "Embedding generated successfully" : "Įterpinys sėkmingai sugeneruotas", + "Embedding provider connection successful!" : "Sėkmingai prisijungta prie įterpinių teikėjo!", + "Enable" : "Įjungti", + "Enable auto-creation" : "Įjungti automatinį kūrimą", + "Enable automatic file vectorization" : "Įjungti automatinį failų vektorizavimą", + "Enable automatic object vectorization" : "Įjungti automatinį objektų vektorizavimą", + "Enable automatic synchronization" : "Įjungti automatinį sinchronizavimą", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Įjungti išsamų SOLR operacijų žurnalinimą (rekomenduojama derinimui)", + "Enable faceting" : "Įjungti aspektavimą", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Įjungti arba išjungti LLM funkcijas. Konfigūruokite teikėjus ir modelius naudodami LLM konfigūracijos mygtuką viršuje.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Įjungti arba išjungti SOLR paieškos integraciją. Konfigūruokite prisijungimo nustatymus naudodami Prisijungimo nustatymų mygtuką viršuje.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Įjungti arba išjungti n8n darbo srautų integraciją. Konfigūruokite prisijungimo nustatymus žemiau.", + "Enable or disable this webhook" : "Įjungti arba išjungti šį žiniatinklio kabliuką", + "Enable vectorization for all existing and future views (may increase costs)" : "Įjungti vektorizavimą visiems esamiems ir būsimiems rodiniams (gali padidinti sąnaudas)", + "Enabled" : "Įjungta", + "Endpoints" : "Galiniai taškai", + "Enter ConfigSet name" : "Įveskite ConfigSet pavadinimą", + "Enter collection name" : "Įveskite kolekcijos pavadinimą", + "Enter description (optional)..." : "Įveskite aprašymą (neprivaloma)...", + "Enter new collection name" : "Įveskite naują kolekcijos pavadinimą", + "Enter object ID" : "Įveskite objekto ID", + "Enter search term" : "Įveskite paieškos terminą", + "Enter view name..." : "Įveskite rodinio pavadinimą...", + "Enter webhook name" : "Įveskite žiniatinklio kabliuko pavadinimą", + "Entities" : "Esybės", + "Entity Information" : "Esybės informacija", + "Entity deleted successfully" : "Esybė sėkmingai pašalinta", + "Entity not found" : "Esybė nerasta", + "Entries to be deleted:" : "Pašalintini įrašai:", + "Enum Issue" : "Sąrašo (enum) problema", + "Enum constraint is missing" : "Trūksta sąrašo (enum) apribojimo", + "Error" : "Klaida", + "Error Details" : "Klaidos išsami informacija", + "Error Information" : "Klaidos informacija", + "Error Message" : "Klaidos pranešimas", + "Error loading application" : "Klaida įkeliant programą", + "Error loading audit trails" : "Klaida įkeliant audito žurnalus", + "Error loading entity" : "Klaida įkeliant esybę", + "Error loading search trails" : "Klaida įkeliant paieškos žurnalus", + "Estimated Batches:" : "Numatomi paketai:", + "Estimated Cost:" : "Numatomos sąnaudos:", + "Estimated Duration:" : "Numatoma trukmė:", + "Event" : "Įvykis", + "Event Property for Payload" : "Įvykio savybė naudingajai apkrovai", + "Events" : "Įvykiai", + "Example Value" : "Pavyzdinė reikšmė", + "Exclusive Maximum" : "Išskirtinis maksimumas", + "Exclusive Minimum" : "Išskirtinis minimumas", + "Execution Mode" : "Vykdymo režimas", + "Execution Time" : "Vykdymo laikas", + "Execution Time Range" : "Vykdymo laiko intervalas", + "Existing Improvements" : "Esami patobulinimai", + "Exponential" : "Eksponentinis", + "Export" : "Eksportuoti", + "Export completed successfully" : "Eksportavimas sėkmingai baigtas", + "Export, view, or delete audit trails" : "Eksportuoti, peržiūrėti arba pašalinti audito žurnalus", + "Extend Schema" : "Išplėsti schemą", + "Extended" : "Išplėsta", + "Extensions" : "Plėtiniai", + "Extract Pending Files" : "Ištraukti laukiančius failus", + "Extract properties from each object" : "Ištraukti savybes iš kiekvieno objekto", + "Extracted At" : "Ištraukta", + "Extraction Status" : "Ištraukimo būsena", + "Facet configuration updated successfully" : "Aspektų konfigūracija sėkmingai atnaujinta", + "Facets discovered and configured successfully" : "Aspektai sėkmingai atrasti ir sukonfigūruoti", + "Facets discovered successfully" : "Aspektai sėkmingai atrasti", + "Failed" : "Nepavyko", + "Failed to analyze schema properties" : "Nepavyko išanalizuoti schemos savybių", + "Failed to archive conversation" : "Nepavyko archyvuoti pokalbio", + "Failed to calculate sizes" : "Nepavyko apskaičiuoti dydžių", + "Failed to clear collection" : "Nepavyko išvalyti kolekcijos", + "Failed to copy changes" : "Nepavyko nukopijuoti pakeitimų", + "Failed to copy data" : "Nepavyko nukopijuoti duomenų", + "Failed to copy data to clipboard" : "Nepavyko nukopijuoti duomenų į iškarpinę", + "Failed to create conversation" : "Nepavyko sukurti pokalbio", + "Failed to create or find project" : "Nepavyko sukurti arba rasti projekto", + "Failed to delete collection" : "Nepavyko pašalinti kolekcijos", + "Failed to delete conversation" : "Nepavyko pašalinti pokalbio", + "Failed to delete webhook" : "Nepavyko pašalinti žiniatinklio kabliuko", + "Failed to download API specification" : "Nepavyko atsisiųsti API specifikacijos", + "Failed to get SOLR field configuration" : "Nepavyko gauti SOLR lauko konfigūracijos", + "Failed to get entity categories" : "Nepavyko gauti esybių kategorijų", + "Failed to get entity statistics" : "Nepavyko gauti esybių statistikos", + "Failed to get entity types" : "Nepavyko gauti esybių tipų", + "Failed to load LLM configuration" : "Nepavyko įkelti LLM konfigūracijos", + "Failed to load Nextcloud groups" : "Nepavyko įkelti Nextcloud grupių", + "Failed to load conversation" : "Nepavyko įkelti pokalbio", + "Failed to load entities" : "Nepavyko įkelti esybių", + "Failed to load entity" : "Nepavyko įkelti esybės", + "Failed to load files" : "Nepavyko įkelti failų", + "Failed to load organisations" : "Nepavyko įkelti organizacijų", + "Failed to load templates" : "Nepavyko įkelti šablonų", + "Failed to load webhooks" : "Nepavyko įkelti žiniatinklio kabliukų", + "Failed to load workflows" : "Nepavyko įkelti darbo srautų", + "Failed to refresh database information" : "Nepavyko atnaujinti duomenų bazės informacijos", + "Failed to reindex collection" : "Nepavyko iš naujo indeksuoti kolekcijos", + "Failed to rename conversation" : "Nepavyko pervadinti pokalbio", + "Failed to restore conversation" : "Nepavyko atkurti pokalbio", + "Failed to retry extraction" : "Nepavyko pakartoti ištraukimo", + "Failed to retry webhook" : "Nepavyko pakartoti žiniatinklio kabliuko", + "Failed to save GitHub token" : "Nepavyko įrašyti GitHub prieigos rakto", + "Failed to save GitLab URL" : "Nepavyko įrašyti GitLab URL", + "Failed to save GitLab token" : "Nepavyko įrašyti GitLab prieigos rakto", + "Failed to save additional feedback" : "Nepavyko įrašyti papildomo atsiliepimo", + "Failed to save n8n configuration" : "Nepavyko įrašyti n8n konfigūracijos", + "Failed to save roles" : "Nepavyko įrašyti rolių", + "Failed to save settings" : "Nepavyko įrašyti nustatymų", + "Failed to save webhook" : "Nepavyko įrašyti žiniatinklio kabliuko", + "Failed to send feedback" : "Nepavyko išsiųsti atsiliepimo", + "Failed to test webhook" : "Nepavyko išbandyti žiniatinklio kabliuko", + "Failed to update favorite status" : "Nepavyko atnaujinti mėgstamo būsenos", + "Failed to update schema properties" : "Nepavyko atnaujinti schemos savybių", + "Failed to update view" : "Nepavyko atnaujinti rodinio", + "Failed to update webhook" : "Nepavyko atnaujinti žiniatinklio kabliuko", + "Feedback recorded" : "Atsiliepimas užregistruotas", + "Fewer sources (1-3):" : "Mažiau šaltinių (1–3):", + "Field" : "Laukas", + "File" : "Failas", + "File Chunk Prediction" : "Failo fragmentų prognozė", + "File Collection" : "Failų kolekcija", + "File Management" : "Failų valdymas", + "File Name" : "Failo pavadinimas", + "File Path" : "Failo kelias", + "File Type Selection" : "Failų tipų pasirinkimas", + "File Vectorization" : "Failų vektorizavimas", + "File Warmup" : "Failų pradinis įkėlimas", + "File actions menu" : "Failo veiksmų meniu", + "File anonymized successfully" : "Failas sėkmingai nuasmenintas", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Failų fragmentų vektorizavimas dar neįgyvendintas. Fragmentai paruošti ir saugomi, tačiau vektorizavimo paslauga dar kuriama.", + "File collection not configured" : "Failų kolekcija nesukonfigūruota", + "File discovery completed" : "Failų atradimas baigtas", + "File extraction completed" : "Failų ištraukimas baigtas", + "File extraction queued" : "Failų ištraukimas įtrauktas į eilę", + "File indexed successfully" : "Failas sėkmingai suindeksuotas", + "File is already anonymized" : "Failas jau nuasmenintas", + "File not found" : "Failas nerastas", + "File settings updated successfully" : "Failo nustatymai sėkmingai atnaujinti", + "File sources" : "Failų šaltiniai", + "File vectorization configuration saved successfully" : "Failų vektorizavimo konfigūracija sėkmingai įrašyta", + "File vectorization started. Check the statistics section for progress." : "Failų vektorizavimas pradėtas. Eigą stebėkite statistikos skiltyje.", + "File warmup completed" : "Failų pradinis įkėlimas baigtas", + "Files" : "Failai", + "Files with Completed Extraction:" : "Failai su baigtu ištraukimu:", + "Files → fileCollection, Objects → objectCollection" : "Failai → fileCollection, Objektai → objectCollection", + "Filter Audit Trails" : "Filtruoti audito žurnalus", + "Filter Deleted Items" : "Filtruoti pašalintus elementus", + "Filter Objects" : "Filtruoti objektus", + "Filter Properties" : "Filtruoti savybes", + "Filter Search Trails" : "Filtruoti paieškos žurnalus", + "Filter Statistics" : "Filtravimo statistika", + "Filter and analyze search trail entries" : "Filtruoti ir analizuoti paieškos žurnalo įrašus", + "Filter and manage audit trail entries" : "Filtruoti ir valdyti audito žurnalo įrašus", + "Filter and manage soft deleted items" : "Filtruoti ir valdyti minkštai pašalintus elementus", + "Filter by object ID" : "Filtruoti pagal objekto ID", + "Filter by search term" : "Filtruoti pagal paieškos terminą", + "Filter by webhook" : "Filtruoti pagal žiniatinklio kabliuką", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filtravimo duomenys įkelti automatiškai. Naudokite žemiau esančius filtrus, kad patikslintumėte paiešką.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtruoti žiniatinklio kabliukų suaktyvinimus pagal naudingosios apkrovos savybes (po vieną eilutėje, formatas: raktas: reikšmė)", + "Filtered" : "Filtruota", + "Filters" : "Filtrai", + "Fireworks AI Chat Settings" : "Fireworks AI pokalbio nustatymai", + "Fireworks AI Embedding Configuration" : "Fireworks AI įterpinių konfigūracija", + "First" : "Pirmas", + "Fixed" : "Pataisyta", + "For chat and retrieval-augmented generation" : "Pokalbiui ir paieška papildytam generavimui", + "For vector embeddings and semantic search" : "Vektoriniams įterpiniams ir semantinei paieškai", + "Format" : "Formatas", + "Format Issue" : "Formato problema", + "Format constraint is missing" : "Trūksta formato apribojimo", + "From Date" : "Nuo datos", + "From date" : "Nuo datos", + "Full data copied to clipboard" : "Visi duomenys nukopijuoti į iškarpinę", + "General Issue" : "Bendra problema", + "Generate recommendations and confidence scores" : "Generuoti rekomendacijas ir patikimumo įverčius", + "Generate vectors immediately when new objects are created" : "Generuoti vektorius iškart, kai sukuriami nauji objektai", + "GitHub token is valid" : "GitHub prieigos raktas galioja", + "GitHub token removed successfully" : "GitHub prieigos raktas sėkmingai pašalintas", + "GitHub token saved successfully" : "GitHub prieigos raktas sėkmingai įrašytas", + "GitLab URL saved successfully" : "GitLab URL sėkmingai įrašytas", + "GitLab token is valid" : "GitLab prieigos raktas galioja", + "GitLab token saved successfully" : "GitLab prieigos raktas sėkmingai įrašytas", + "HTTP Method" : "HTTP metodas", + "HTTP method used to send webhook requests" : "HTTP metodas, naudojamas žiniatinklio kabliukų užklausoms siųsti", + "Headers" : "Antraštės", + "Health" : "Būklė", + "Heartbeat successful - connection kept alive" : "Širdies plakimo signalas sėkmingas – ryšys palaikomas", + "Helpful" : "Naudinga", + "Hide Filters" : "Slėpti filtrus", + "Hide in forms" : "Slėpti formose", + "Hide in list view" : "Slėpti sąrašo rodinyje", + "High" : "Aukštas", + "High Confidence" : "Aukštas patikimumas", + "Host" : "Pagrindinis kompiuteris", + "Hourly" : "Kas valandą", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Kaip giliai pereiti įdėtąsias objektų savybes (1–20). Didesnės reikšmės užfiksuoja daugiau detalių, bet padidina vektoriaus dydį.", + "How often to check for updates (1-168 hours)" : "Kaip dažnai tikrinti, ar yra atnaujinimų (1–168 valandos)", + "How to handle retries for failed webhook deliveries" : "Kaip tvarkyti pakartojimus dėl nepavykusių žiniatinklio kabliukų pristatymų", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "IP dažnio apribojimai sėkmingai išvalyti", + "Identify properties not in the schema" : "Nustatyti savybes, kurių nėra schemoje", + "Immutable" : "Nekintamas", + "Import" : "Importuoti", + "Import successful" : "Importavimas sėkmingas", + "Improved Property" : "Patobulinta savybė", + "In use" : "Naudojama", + "Inactive" : "Neaktyvus", + "Include IDs and names of related objects for better contextual search" : "Įtraukti susijusių objektų ID ir vardus geresnei kontekstinei paieškai", + "Include related object references" : "Įtraukti susijusių objektų nuorodas", + "Include schema and register metadata" : "Įtraukti schemos ir registro metaduomenis", + "Initialization failed" : "Inicijavimas nepavyko", + "Initialize Project" : "Inicijuoti projektą", + "Inspect Fields" : "Apžiūrėti laukus", + "Inspect Index" : "Apžiūrėti indeksą", + "Invalid" : "Negaliojantis", + "Invalid batch size. Must be between 1 and 5000" : "Negaliojantis paketo dydis. Turi būti tarp 1 ir 5000", + "Invalid field name provided" : "Pateiktas negaliojantis lauko pavadinimas", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Negaliojantis maxObjects. Turi būti 0 (visi) arba teigiamas skaičius", + "Invalid:" : "Negaliojantis:", + "Issue" : "Problema", + "Items per page:" : "Elementų puslapyje:", + "Just now" : "Ką tik", + "LLM Configuration" : "LLM konfigūracija", + "LLM actions menu" : "LLM veiksmų meniu", + "LLM configuration saved successfully" : "LLM konfigūracija sėkmingai įrašyta", + "LLM features disabled" : "LLM funkcijos išjungtos", + "LLM features enabled" : "LLM funkcijos įjungtos", + "LLM must be enabled with an embedding provider configured" : "LLM turi būti įjungtas su sukonfigūruotu įterpinių teikėju", + "LLM settings updated successfully" : "LLM nustatymai sėkmingai atnaujinti", + "Last" : "Paskutinis", + "Last Triggered" : "Paskutinį kartą suaktyvinta", + "Length Range:" : "Ilgio intervalas:", + "Linear" : "Tiesinis", + "Load Advanced Filters" : "Įkelti išplėstinius filtrus", + "Load advanced filters with live data from your search index" : "Įkelti išplėstinius filtrus su tiesioginiais duomenimis iš jūsų paieškos indekso", + "Load view" : "Įkelti rodinį", + "Loading ConfigSets..." : "Įkeliama ConfigSets...", + "Loading advanced filters..." : "Įkeliami išplėstiniai filtrai...", + "Loading agents..." : "Įkeliami agentai...", + "Loading application details..." : "Įkeliama išsami programos informacija...", + "Loading applications..." : "Įkeliamos programos...", + "Loading archived conversations..." : "Įkeliami archyvuoti pokalbiai...", + "Loading audit trails..." : "Įkeliami audito žurnalai...", + "Loading collections..." : "Įkeliamos kolekcijos...", + "Loading configurations..." : "Įkeliamos konfigūracijos...", + "Loading conversation..." : "Įkeliamas pokalbis...", + "Loading conversations..." : "Įkeliami pokalbiai...", + "Loading deleted items..." : "Įkeliami pašalinti elementai...", + "Loading events..." : "Įkeliami įvykiai...", + "Loading filter data..." : "Įkeliami filtravimo duomenys...", + "Loading groups..." : "Įkeliamos grupės...", + "Loading log details..." : "Įkeliama išsami žurnalo informacija...", + "Loading registers..." : "Įkeliami registrai...", + "Loading schemas..." : "Įkeliamos schemos...", + "Loading search trails..." : "Įkeliami paieškos žurnalai...", + "Loading sources..." : "Įkeliami šaltiniai...", + "Loading statistics..." : "Įkeliama statistika...", + "Loading users..." : "Įkeliami naudotojai...", + "Loading views..." : "Įkeliami rodiniai...", + "Loading..." : "Įkeliama...", + "Local" : "Vietinis", + "Local Version" : "Vietinė versija", + "Locked" : "Užrakinta", + "Locked:" : "Užrakinta:", + "Login successful" : "Prisijungimas sėkmingas", + "Logs" : "Žurnalai", + "Low" : "Žemas", + "Low Confidence" : "Žemas patikimumas", + "Lucene Version:" : "Lucene versija:", + "Magic table synchronized successfully" : "Stebuklingoji lentelė sėkmingai sinchronizuota", + "Manage Organisation Roles" : "Valdyti organizacijos roles", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Valdykite SOLR kolekcijas (duomenų saugyklas) ir priskirkite jas objektams bei failams.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Valdykite SOLR ConfigSets (konfigūracijos šablonus) savo kolekcijoms.", + "Manage and configure agents for automated tasks" : "Valdykite ir konfigūruokite agentus automatinėms užduotims", + "Manage and monitor file text extraction status" : "Valdykite ir stebėkite failų teksto ištraukimo būseną", + "Manage and restore soft deleted items from your registers" : "Valdykite ir atkurkite minkštai pašalintus elementus iš savo registrų", + "Manage and view detected entities from files and objects" : "Valdykite ir peržiūrėkite iš failų bei objektų aptiktas esybes", + "Manage document templates and themes" : "Valdykite dokumentų šablonus ir temas", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Valdykite n8n darbo srautus OpenRegister automatizavimui. Darbo srautai bus saugomi sukonfigūruotame projekte.", + "Manage webhooks for event-driven integrations" : "Valdykite žiniatinklio kabliukus įvykiais pagrįstoms integracijoms", + "Manage your applications and modules" : "Valdykite savo programas ir modulius", + "Manage your data registers and their configurations" : "Valdykite savo duomenų registrus ir jų konfigūracijas", + "Manage your data schemas and their properties" : "Valdykite savo duomenų schemas ir jų savybes", + "Manage your data sources and their configurations" : "Valdykite savo duomenų šaltinius ir jų konfigūracijas", + "Manage your saved search configurations" : "Valdykite savo įrašytas paieškos konfigūracijas", + "Manage your system configurations and settings" : "Valdykite savo sistemos konfigūracijas ir nustatymus", + "Managed" : "Valdoma", + "Max Files (0 = all)" : "Maks. failų (0 = visi)", + "Max Length" : "Maks. ilgis", + "Max Length:" : "Maks. ilgis:", + "Max Objects (0 = all)" : "Maks. objektų (0 = visi)", + "Max Retries" : "Maks. pakartojimų", + "Max Shards/Node" : "Maks. fragmentų / mazge", + "Max execution time (ms)" : "Maks. vykdymo laikas (ms)", + "Max ms" : "Maks. ms", + "Max result count" : "Maks. rezultatų kiekis", + "Max results" : "Maks. rezultatų", + "Maximum" : "Maksimumas", + "Maximum Nesting Depth" : "Maksimalus įdėjimo gylis", + "Maximum length constraint is missing" : "Trūksta maksimalaus ilgio apribojimo", + "Maximum length is too restrictive" : "Maksimalus ilgis pernelyg ribojantis", + "Maximum number of files to process. Set to 0 to process all files." : "Didžiausias apdorotinų failų skaičius. Nustatykite 0, kad būtų apdoroti visi failai.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Didžiausias apdorotinų objektų skaičius. Nustatykite 0, kad būtų apdoroti visi objektai.", + "Maximum number of retry attempts for failed deliveries" : "Didžiausias pakartojimo bandymų skaičius dėl nepavykusių pristatymų", + "Maximum time to wait before committing changes" : "Maksimalus laukimo laikas prieš patvirtinant pakeitimus", + "Maximum tokens to generate" : "Maksimalus generuotinų prieigos raktų skaičius", + "Maximum value constraint is missing" : "Trūksta maksimalios reikšmės apribojimo", + "Maximum value is too restrictive" : "Maksimali reikšmė pernelyg ribojanti", + "Medium" : "Vidutinis", + "Medium Confidence" : "Vidutinis patikimumas", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Vidutinės užklausos: paieškos su tam tikru filtravimu arba keliais parametrais (pvz., datų intervalai, konkretūs registrai / schemos)", + "Members of selected groups can access this view" : "Pasirinktų grupių nariai gali pasiekti šį rodinį", + "Members:" : "Nariai:", + "Memory prediction calculated successfully" : "Atminties prognozė sėkmingai apskaičiuota", + "Message content is required" : "Reikia žinutės turinio", + "Message does not belong to this conversation" : "Žinutė nepriklauso šiam pokalbiui", + "Metadata Filters" : "Metaduomenų filtrai", + "Method" : "Metodas", + "Min Length" : "Min. ilgis", + "Min execution time (ms)" : "Min. vykdymo laikas (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. rezultatų kiekis", + "Min results" : "Min. rezultatų", + "Minimum" : "Minimumas", + "Minimum value constraint is missing" : "Trūksta minimalios reikšmės apribojimo", + "Minimum value is too restrictive" : "Minimali reikšmė pernelyg ribojanti", + "Missing object fields created successfully" : "Trūkstami objekto laukai sėkmingai sukurti", + "Mode:" : "Režimas:", + "Model" : "Modelis", + "Model Name" : "Modelio pavadinimas", + "Modified" : "Pakeista", + "Monthly" : "Kas mėnesį", + "More sources (10-20):" : "Daugiau šaltinių (10–20):", + "Most Active Objects" : "Aktyviausi objektai", + "Multiple Of" : "Kartotinis", + "Name" : "Vardas", + "Names cache warmup completed" : "Vardų podėlio pradinis įkėlimas baigtas", + "New Collection Name" : "Naujos kolekcijos pavadinimas", + "New Conversation" : "Naujas pokalbis", + "New Properties" : "Naujos savybės", + "New Property" : "Nauja savybė", + "New Value" : "Nauja reikšmė", + "New users without specific organisation membership will be automatically added to this organisation" : "Nauji naudotojai be konkrečios organizacijos narystės bus automatiškai pridėti prie šios organizacijos", + "Next" : "Kitas", + "No" : "Ne", + "No ConfigSets found" : "ConfigSets nerasta", + "No GitHub token configured" : "Nesukonfigūruotas joks GitHub prieigos raktas", + "No GitHub token provided" : "Nepateiktas joks GitHub prieigos raktas", + "No GitLab token provided" : "Nepateiktas joks GitLab prieigos raktas", + "No Organisation" : "Nėra organizacijos", + "No active filters" : "Nėra aktyvių filtrų", + "No activity data available" : "Nėra veiklos duomenų", + "No agents available" : "Nėra agentų", + "No agents found" : "Agentų nerasta", + "No app store cache exists yet" : "Programų parduotuvės podėlio dar nėra", + "No applications are available." : "Nėra galimų programų.", + "No applications found" : "Programų nerasta", + "No archived conversations" : "Nėra archyvuotų pokalbių", + "No audit trail entries found" : "Audito žurnalo įrašų nerasta", + "No changes recorded" : "Pakeitimų neužregistruota", + "No chunks to vectorize" : "Nėra vektorizuotinų fragmentų", + "No collections found" : "Kolekcijų nerasta", + "No configuration data" : "Nėra konfigūracijos duomenų", + "No configurations are available." : "Nėra galimų konfigūracijų.", + "No configurations found" : "Konfigūracijų nerasta", + "No configurations found for this application." : "Šiai programai konfigūracijų nerasta.", + "No conversations yet. Create a new one to get started!" : "Dar nėra pokalbių. Sukurkite naują, kad pradėtumėte!", + "No data available" : "Nėra duomenų", + "No data available for chart" : "Nėra diagramai skirtų duomenų", + "No deleted items found" : "Pašalintų elementų nerasta", + "No deletion data available" : "Nėra pašalinimo duomenų", + "No description found" : "Aprašymo nerasta", + "No description provided" : "Nepateiktas joks aprašymas", + "No entities detected in this file. Run text extraction first." : "Šiame faile esybių neaptikta. Pirmiausia atlikite teksto ištraukimą.", + "No entities found" : "Esybių nerasta", + "No entities have been detected yet" : "Esybių dar neaptikta", + "No events found" : "Įvykių nerasta", + "No expired audit trails found to clear" : "Nerasta išvalytinų pasibaigusio galiojimo audito žurnalų", + "No facetable fields available. Select a register and schema to see available filters." : "Nėra aspektuojamų laukų. Pasirinkite registrą ir schemą, kad pamatytumėte galimus filtrus.", + "No files found" : "Failų nerasta", + "No files have been extracted yet" : "Failų dar neištraukta", + "No files to process" : "Nėra apdorotinų failų", + "No files to reindex" : "Nėra iš naujo indeksuotinų failų", + "No filters are currently active. This will delete ALL audit trail entries!" : "Šiuo metu nėra aktyvių filtrų. Tai pašalins VISUS audito žurnalo įrašus!", + "No log entries found" : "Žurnalo įrašų nerasta", + "No logs are available for this configuration." : "Šiai konfigūracijai žurnalų nėra.", + "No logs are available for this source." : "Šiam šaltiniui žurnalų nėra.", + "No logs found" : "Žurnalų nerasta", + "No mismatched fields found - SOLR schema is properly configured" : "Neatitinkančių laukų nerasta – SOLR schema tinkamai sukonfigūruota", + "No objects found" : "Objektų nerasta", + "No objects found to vectorize" : "Vektorizuotinų objektų nerasta", + "No objects selected for deletion" : "Nepasirinktas joks šalintinas objektas", + "No objects to delete" : "Nėra šalintinų objektų", + "No objects were permanently deleted" : "Jokie objektai nebuvo pašalinti visam laikui", + "No properties found" : "Savybių nerasta", + "No properties match your filters." : "Jokios savybės neatitinka jūsų filtrų.", + "No purge date set" : "Nenustatyta išvalymo data", + "No register data available" : "Nėra registro duomenų", + "No registers found" : "Registrų nerasta", + "No registers found for this application." : "Šiai programai registrų nerasta.", + "No relations found" : "Ryšių nerasta", + "No request body available" : "Nėra užklausos turinio", + "No response body available" : "Nėra atsako turinio", + "No saved views yet. Create one in the Search tab!" : "Dar nėra įrašytų rodinių. Sukurkite vieną paieškos kortelėje!", + "No schema data available" : "Nėra schemos duomenų", + "No schema selected for exploration" : "Nepasirinkta jokia tyrinėtina schema", + "No schemas found" : "Schemų nerasta", + "No schemas found for this application." : "Šiai programai schemų nerasta.", + "No search terms data available" : "Nėra paieškos terminų duomenų", + "No search trail entries found" : "Paieškos žurnalo įrašų nerasta", + "No sources are available." : "Nėra galimų šaltinių.", + "No sources found" : "Šaltinių nerasta", + "No templates found" : "Šablonų nerasta", + "No templates have been created yet" : "Šablonų dar nesukurta", + "No valid magic-mapped register+schema combinations found" : "Negaliojančių stebuklingai susietų registro + schemos derinių nerasta", + "No views found. Create views first before configuring vectorization." : "Rodinių nerasta. Prieš konfigūruodami vektorizavimą pirmiausia sukurkite rodinius.", + "No views match your search" : "Jokie rodiniai neatitinka jūsų paieškos", + "No webhooks found" : "Žiniatinklio kabliukų nerasta", + "No webhooks have been configured yet" : "Žiniatinklio kabliukų dar nesukonfigūruota", + "No workflows found in this project. Create workflows in the n8n editor." : "Šiame projekte darbo srautų nerasta. Sukurkite darbo srautus n8n redaktoriuje.", + "None" : "Nėra", + "Not helpful" : "Nenaudinga", + "Not in use" : "Nenaudojama", + "Number Constraints" : "Skaičių apribojimai", + "Number of chunks to vectorize in one API call (1-100)." : "Vienu API iškvietimu vektorizuotinų fragmentų skaičius (1–100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Vienu API iškvietimu vektorizuotinų fragmentų skaičius. Didesnis = greičiau, bet daugiau atminties. Rekomenduojama: 10–50.", + "Number of objects to process in each batch (1-5000)." : "Kiekviename pakete apdorotinų objektų skaičius (1–5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Vienu API iškvietimu vektorizuotinų objektų skaičius. Didesnis = greičiau, bet daugiau atminties. Rekomenduojama: 10–50.", + "Numeric Range:" : "Skaitinis intervalas:", + "Object" : "Objektas", + "Object Collection" : "Objektų kolekcija", + "Object Count Prediction" : "Objektų skaičiaus prognozė", + "Object ID" : "Objekto ID", + "Object Management" : "Objektų valdymas", + "Object Vectorization" : "Objektų vektorizavimas", + "Object Warmup" : "Objektų pradinis įkėlimas", + "Object collection not configured" : "Objektų kolekcija nesukonfigūruota", + "Object not found" : "Objektas nerastas", + "Object permanently deleted" : "Objektas pašalintas visam laikui", + "Object restored successfully" : "Objektas sėkmingai atkurtas", + "Object settings updated successfully" : "Objekto nustatymai sėkmingai atnaujinti", + "Object sources" : "Objektų šaltiniai", + "Object unlocked successfully" : "Objektas sėkmingai atrakintas", + "Object vectorization configuration saved successfully" : "Objektų vektorizavimo konfigūracija sėkmingai įrašyta", + "Object vectorized successfully" : "Objektas sėkmingai vektorizuotas", + "Object:" : "Objektas:", + "Objects" : "Objektai", + "Objects Analyzed" : "Išanalizuoti objektai", + "Objects Distribution" : "Objektų pasiskirstymas", + "Objects being analyzed" : "Analizuojami objektai", + "Objects by Register" : "Objektai pagal registrą", + "Objects by Schema" : "Objektai pagal schemą", + "Objects deletion completed successfully" : "Objektų šalinimas sėkmingai baigtas", + "Objects in schema" : "Objektai schemoje", + "Objects to Process:" : "Apdorotini objektai:", + "Objects to be analyzed" : "Analizuotini objektai", + "Objects to be deleted" : "Šalintini objektai", + "Objects to be validated" : "Patvirtintini objektai", + "Objects will be serialized as JSON text before vectorization" : "Objektai prieš vektorizavimą bus serializuoti kaip JSON tekstas", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objektai bus minkštai pašalinti (pažymėti kaip pašalinti, bet išsaugoti duomenų bazėje). Prireikus juos galima vėliau atkurti.", + "Old Value" : "Sena reikšmė", + "Oldest Item (days)" : "Seniausias elementas (dienomis)", + "Ollama Chat Settings" : "Ollama pokalbio nustatymai", + "Ollama Embedding Configuration" : "Ollama įterpinių konfigūracija", + "Ollama URL" : "Ollama URL", + "Only With Changes" : "Tik su pakeitimais", + "Open Register Objects" : "Open Register objektai", + "Open n8n Editor" : "Atverti n8n redaktorių", + "OpenAI Chat Settings" : "OpenAI pokalbio nustatymai", + "OpenAI Embedding Configuration" : "OpenAI įterpinių konfigūracija", + "OpenRegister Settings" : "OpenRegister nustatymai", + "Optional webhook secret for signature verification" : "Neprivalomas žiniatinklio kabliuko slaptasis raktas parašui patikrinti", + "Order" : "Tvarka", + "Organisation" : "Organizacija", + "Organisation ID" : "Organizacijos ID", + "Organisation Members" : "Organizacijos nariai", + "Organisation Statistics" : "Organizacijos statistika", + "Organisation created successfully" : "Organizacija sėkmingai sukurta", + "Organisation settings saved successfully" : "Organizacijos nustatymai sėkmingai įrašyti", + "Organisations" : "Organizacijos", + "Organization" : "Organizacija", + "Organization ID (Optional)" : "Organizacijos ID (neprivaloma)", + "Orphaned Items" : "Likę be ryšio elementai", + "Owner" : "Savininkas", + "Owner:" : "Savininkas:", + "Parallel Mode (Faster, more resource intensive)" : "Lygiagretusis režimas (greitesnis, imlesnis ištekliams)", + "Parallel:" : "Lygiagretusis:", + "Password" : "Slaptažodis", + "Password for SOLR authentication (optional)" : "Slaptažodis SOLR tapatybės nustatymui (neprivaloma)", + "Path" : "Kelias", + "Path in repository" : "Kelias saugykloje", + "Path where the configuration file will be saved in the repository" : "Kelias, kuriuo konfigūracijos failas bus įrašytas saugykloje", + "Path where the register OAS file will be saved in the repository" : "Kelias, kuriuo registro OAS failas bus įrašytas saugykloje", + "Pattern (regex)" : "Šablonas (regex)", + "Pattern Issue" : "Šablono problema", + "Pattern constraint is missing" : "Trūksta šablono apribojimo", + "Patterns:" : "Šablonai:", + "Pending" : "Laukiama", + "Permanent Delete Mode" : "Visam laikui šalinimo režimas", + "Permanently Delete" : "Pašalinti visam laikui", + "Permanently delete already soft-deleted objects" : "Visam laikui pašalinti jau minkštai pašalintus objektus", + "Permanently delete this conversation?" : "Pašalinti šį pokalbį visam laikui?", + "Person" : "Asmuo", + "Personal Data" : "Asmens duomenys", + "Phone" : "Telefonas", + "Photos" : "Nuotraukos", + "Please create an agent in the" : "Sukurkite agentą", + "Please select an agent to continue" : "Pasirinkite agentą, kad tęstumėte", + "Please select which register and schema to use for the new object" : "Pasirinkite, kurį registrą ir schemą naudoti naujam objektui", + "Please try again later." : "Bandykite dar kartą vėliau.", + "Please wait while we fetch your agents." : "Palaukite, kol gausime jūsų agentus.", + "Please wait while we fetch your applications." : "Palaukite, kol gausime jūsų programas.", + "Please wait while we fetch your configurations." : "Palaukite, kol gausime jūsų konfigūracijas.", + "Please wait while we fetch your deleted items." : "Palaukite, kol gausime jūsų pašalintus elementus.", + "Please wait while we fetch your sources." : "Palaukite, kol gausime jūsų šaltinius.", + "Polls" : "Apklausos", + "Popular Search Terms" : "Populiarūs paieškos terminai", + "Port" : "Prievadas", + "Prerequisites" : "Būtinosios sąlygos", + "Press Enter to send, Shift+Enter for new line" : "Spauskite Enter siųsti, Shift+Enter naujai eilutei", + "Previous" : "Ankstesnis", + "Private" : "Privatus", + "Processes chunks in batches with simulated parallelism." : "Apdoroja fragmentus paketais su modeliuojamu lygiagretumu.", + "Processes file chunks sequentially (safest)." : "Apdoroja failų fragmentus nuosekliai (saugiausia).", + "Processes objects in chunks with simulated parallelism." : "Apdoroja objektus fragmentais su modeliuojamu lygiagretumu.", + "Processes objects sequentially (safest)." : "Apdoroja objektus nuosekliai (saugiausia).", + "Processing" : "Apdorojama", + "Processing Limits" : "Apdorojimo ribos", + "Processing..." : "Apdorojama...", + "Project initialized successfully" : "Projektas sėkmingai inicijuotas", + "Project not found. Please initialize first." : "Projektas nerastas. Pirmiausia inicijuokite.", + "Properties" : "Savybės", + "Property Behaviors" : "Savybių elgsenos", + "Property Configuration:" : "Savybės konfigūracija:", + "Property Filters" : "Savybių filtrai", + "Property Title" : "Savybės pavadinimas", + "Property Type" : "Savybės tipas", + "Property can be improved" : "Savybę galima patobulinti", + "Protected" : "Apsaugota", + "Provider" : "Teikėjas", + "Provider is required for testing" : "Bandymui reikia teikėjo", + "Public" : "Viešas", + "Public View" : "Viešas rodinys", + "Public views can be accessed by anyone in the system" : "Viešus rodinius gali pasiekti bet kas sistemoje", + "Publish" : "Publikuoti", + "Publish Objects" : "Publikuoti objektus", + "Publish all objects in this schema" : "Publikuoti visus šios schemos objektus", + "Published" : "Publikuota", + "Published:" : "Publikuota:", + "Publishing..." : "Publikuojama...", + "Purge" : "Išvalyti", + "Purge Date" : "Išvalymo data", + "Query Complexity Distribution" : "Užklausų sudėtingumo pasiskirstymas", + "Query parameter is required" : "Reikia užklausos parametro", + "RAG" : "RAG", + "RAG Configuration" : "RAG konfigūracija", + "Rate limits cleared successfully" : "Dažnio apribojimai sėkmingai išvalyti", + "Raw Changes Data" : "Neapdoroti pakeitimų duomenys", + "Re-vectorize on object update" : "Iš naujo vektorizuoti objektą atnaujinus", + "Read" : "Skaityti", + "Recommendations:" : "Rekomendacijos:", + "Recommended Type:" : "Rekomenduojamas tipas:", + "Recommended: 5 sources" : "Rekomenduojama: 5 šaltiniai", + "Refresh" : "Įkelti iš naujo", + "Refresh Data" : "Įkelti duomenis iš naujo", + "Refresh Stats" : "Įkelti statistiką iš naujo", + "Refresh Workflows" : "Įkelti darbo srautus iš naujo", + "Refresh dashboard" : "Įkelti skydelį iš naujo", + "Refresh database info" : "Įkelti duomenų bazės informaciją iš naujo", + "Register" : "Registras", + "Register ID" : "Registro ID", + "Register Statistics" : "Registro statistika", + "Register Totals" : "Registro sumos", + "Register depublished successfully" : "Registras sėkmingai nupublikuotas", + "Register not found" : "Registras nerastas", + "Register objects deletion completed successfully" : "Registro objektų šalinimas sėkmingai baigtas", + "Register published successfully" : "Registras sėkmingai publikuotas", + "Register/Schema Usage" : "Registro / schemos naudojimas", + "Registers" : "Registrai", + "Reindex" : "Indeksuoti iš naujo", + "Reindex completed" : "Indeksavimas iš naujo baigtas", + "Reindex completed successfully" : "Indeksavimas iš naujo sėkmingai baigtas", + "Reindex failed" : "Indeksavimas iš naujo nepavyko", + "Relations" : "Ryšiai", + "Remote Version" : "Nuotolinė versija", + "Remove" : "Šalinti", + "Remove filter" : "Šalinti filtrą", + "Remove from favorites" : "Šalinti iš mėgstamų", + "Remove group" : "Šalinti grupę", + "Removed" : "Pašalinta", + "Removed from favorites" : "Pašalinta iš mėgstamų", + "Rename Conversation" : "Pervadinti pokalbį", + "Rename conversation" : "Pervadinti pokalbį", + "Replicas" : "Replikos", + "Repository" : "Saugykla", + "Request Body" : "Užklausos turinys", + "Request Data" : "Užklausos duomenys", + "Request Details" : "Užklausos išsami informacija", + "Request body must be JSON with a \"query\" field" : "Užklausos turinys turi būti JSON su lauku „query“", + "Request timeout in seconds" : "Užklausos skirtasis laikas sekundėmis", + "Required field" : "Privalomas laukas", + "Required status is inconsistent" : "Privalomumo būsena nenuosekli", + "Rerun Search" : "Pakartoti paiešką", + "Reset Changes" : "Atstatyti pakeitimus", + "Reset Filters" : "Atstatyti filtrus", + "Response Body" : "Atsako turinys", + "Response Details" : "Atsako išsami informacija", + "Response Time:" : "Atsako laikas:", + "Restore" : "Atkurti", + "Restore conversation" : "Atkurti pokalbį", + "Restore or permanently delete items" : "Atkurti arba visam laikui pašalinti elementus", + "Result Count Range" : "Rezultatų skaičiaus intervalas", + "Results" : "Rezultatai", + "Retrieve all objects for this schema" : "Gauti visus šios schemos objektus", + "Retry" : "Kartoti", + "Retry Failed Extractions" : "Kartoti nepavykusius ištraukimus", + "Retry Policy" : "Pakartojimų politika", + "Retry completed" : "Pakartojimas baigtas", + "Retrying..." : "Kartojama...", + "Risk Level" : "Rizikos lygis", + "Roles updated successfully" : "Rolės sėkmingai atnaujintos", + "SOLR Connection Settings" : "SOLR prisijungimo nustatymai", + "SOLR Version:" : "SOLR versija:", + "SOLR actions menu" : "SOLR veiksmų meniu", + "SOLR base path (usually /solr)" : "SOLR bazinis kelias (paprastai /solr)", + "SOLR is disabled" : "SOLR išjungtas", + "SOLR is not available or not configured" : "SOLR negalimas arba nesukonfigūruotas", + "SOLR logging disabled" : "SOLR žurnalinimas išjungtas", + "SOLR logging enabled" : "SOLR žurnalinimas įjungtas", + "SOLR search disabled" : "SOLR paieška išjungta", + "SOLR search enabled" : "SOLR paieška įjungta", + "SOLR server hostname or IP address" : "SOLR serverio pagrindinio kompiuterio vardas arba IP adresas", + "SOLR server port number (optional, defaults to 8983)" : "SOLR serverio prievado numeris (neprivaloma, numatytasis 8983)", + "SOLR setup completed successfully" : "SOLR sąranka sėkmingai baigta", + "SOLR setup failed" : "SOLR sąranka nepavyko", + "SOLR setup failed - check logs" : "SOLR sąranka nepavyko – patikrinkite žurnalus", + "Sample Values:" : "Pavyzdinės reikšmės:", + "Save" : "Įrašyti", + "Save Configuration" : "Įrašyti konfigūraciją", + "Save Connection Settings" : "Įrašyti prisijungimo nustatymus", + "Save Roles" : "Įrašyti roles", + "Save Settings" : "Įrašyti nustatymus", + "Save current search as view" : "Įrašyti dabartinę paiešką kaip rodinį", + "Saved Views" : "Įrašyti rodiniai", + "Saving..." : "Įrašoma...", + "Schema" : "Schema", + "Schema ID" : "Schemos ID", + "Schema Name" : "Schemos pavadinimas", + "Schema Statistics" : "Schemos statistika", + "Schema depublished successfully" : "Schema sėkmingai nupublikuota", + "Schema objects deletion completed successfully" : "Schemos objektų šalinimas sėkmingai baigtas", + "Schema published successfully" : "Schema sėkmingai publikuota", + "Schema successfully created" : "Schema sėkmingai sukurta", + "Schema successfully updated" : "Schema sėkmingai atnaujinta", + "Schemas" : "Schemos", + "Scheme" : "Schema", + "Search / Views" : "Paieška / rodiniai", + "Search Activity" : "Paieškos veikla", + "Search Analytics" : "Paieškos analitika", + "Search Entities" : "Ieškoti esybių", + "Search Files" : "Ieškoti failų", + "Search GitHub" : "Ieškoti GitHub", + "Search GitLab" : "Ieškoti GitLab", + "Search Method" : "Paieškos metodas", + "Search Term" : "Paieškos terminas", + "Search Trail Details" : "Paieškos žurnalo išsami informacija", + "Search Trail Management" : "Paieškos žurnalų valdymas", + "Search Trail Statistics" : "Paieškos žurnalų statistika", + "Search Trails" : "Paieškos žurnalai", + "Search Views" : "Ieškoti rodinių", + "Search Webhooks" : "Ieškoti žiniatinklio kabliukų", + "Search by file name or path" : "Ieškoti pagal failo pavadinimą arba kelią", + "Search by name or URL" : "Ieškoti pagal vardą arba URL", + "Search by value" : "Ieškoti pagal reikšmę", + "Search in Files" : "Ieškoti failuose", + "Search in Objects" : "Ieškoti objektuose", + "Search property names..." : "Ieškoti savybių pavadinimų...", + "Search trail deletion not implemented yet" : "Paieškos žurnalo šalinimas dar neįgyvendintas", + "Search views..." : "Ieškoti rodinių...", + "Secret" : "Slaptasis raktas", + "Secret key for HMAC signature generation (optional)" : "Slaptasis raktas HMAC parašui generuoti (neprivaloma)", + "Select AI Agent" : "Pasirinkti DI agentą", + "Select All" : "Pasirinkti visus", + "Select ConfigSet" : "Pasirinkti ConfigSet", + "Select File Types to Vectorize:" : "Pasirinkite vektorizuotinus failų tipus:", + "Select HTTP method" : "Pasirinkite HTTP metodą", + "Select Register and Schema" : "Pasirinkite registrą ir schemą", + "Select Views to Vectorize:" : "Pasirinkite vektorizuotinus rodinius:", + "Select a Nextcloud group to add" : "Pasirinkite pridėtiną Nextcloud grupę", + "Select a branch" : "Pasirinkite šaką", + "Select a model or type a custom model name" : "Pasirinkite modelį arba įveskite pasirinktinį modelio pavadinimą", + "Select a repository" : "Pasirinkite saugyklą", + "Select a repository you have write access to" : "Pasirinkite saugyklą, į kurią turite rašymo prieigą", + "Select an AI Agent" : "Pasirinkite DI agentą", + "Select an AI agent to begin chatting with your data." : "Pasirinkite DI agentą, kad pradėtumėte pokalbį su savo duomenimis.", + "Select backend" : "Pasirinkite vidinę sąsają", + "Select chat model" : "Pasirinkite pokalbio modelį", + "Select collection for files" : "Pasirinkite kolekciją failams", + "Select collection for objects" : "Pasirinkite kolekciją objektams", + "Select default organisation" : "Pasirinkite numatytąją organizaciją", + "Select event to listen to..." : "Pasirinkite klausomą įvykį...", + "Select model" : "Pasirinkite modelį", + "Select options..." : "Pasirinkite parinktis...", + "Select period" : "Pasirinkite laikotarpį", + "Select property to send as payload" : "Pasirinkite savybę, siunčiamą kaip naudingoji apkrova", + "Select provider" : "Pasirinkite teikėją", + "Select registers and schemas to save a view" : "Pasirinkite registrus ir schemas, kad įrašytumėte rodinį", + "Select retry policy" : "Pasirinkite pakartojimų politiką", + "Select the branch to publish to" : "Pasirinkite šaką, į kurią publikuoti", + "Select the event this webhook should listen to" : "Pasirinkite įvykį, kurio šis žiniatinklio kabliukas turėtų klausytis", + "Select views to vectorize:" : "Pasirinkite vektorizuotinus rodinius:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Pasirinkite, kurios Nextcloud grupės galimos šiai organizacijai. Šiose grupėse esantys naudotojai turės prieigą prie organizacijos išteklių.", + "Select which data views the AI can search" : "Pasirinkite, kuriuose duomenų rodiniuose DI gali ieškoti", + "Select which property from the event should be used as the webhook payload data" : "Pasirinkite, kuri įvykio savybė turėtų būti naudojama kaip žiniatinklio kabliuko naudingosios apkrovos duomenys", + "Select which tools the AI can use to perform actions" : "Pasirinkite, kuriuos įrankius DI gali naudoti veiksmams atlikti", + "Selected" : "Pasirinkta", + "Selected Groups" : "Pasirinktos grupės", + "Selected audit trails deleted successfully" : "Pasirinkti audito žurnalai sėkmingai pašalinti", + "Selected search trails deleted successfully" : "Pasirinkti paieškos žurnalai sėkmingai pašalinti", + "Selected users can access this view" : "Pasirinkti naudotojai gali pasiekti šį rodinį", + "Send additional feedback" : "Siųsti papildomą atsiliepimą", + "Send as CloudEvent" : "Siųsti kaip CloudEvent", + "Sensitive PII" : "Jautrūs asmens duomenys (PII)", + "Serial Mode (Safer, slower)" : "Nuoseklusis režimas (saugesnis, lėtesnis)", + "Serial:" : "Nuoseklusis:", + "Server Configuration" : "Serverio konfigūracija", + "Server Information" : "Serverio informacija", + "Settings" : "Nustatymai", + "Shards" : "Fragmentai", + "Share" : "Bendrinti", + "Share with Groups" : "Bendrinti su grupėmis", + "Share with Users" : "Bendrinti su naudotojais", + "Show Filters" : "Rodyti filtrus", + "Show less" : "Rodyti mažiau", + "Show only entries with changes" : "Rodyti tik įrašus su pakeitimais", + "Simple" : "Paprasta", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Paprastos užklausos: pagrindinės teksto paieškos su minimaliais parametrais (pvz., vienas paieškos terminas, be išplėstinių filtrų)", + "Size" : "Dydis", + "Slug" : "Slug", + "Soft Delete Mode" : "Minkšto šalinimo režimas", + "Soft Deleted Items" : "Minkštai pašalinti elementai", + "SolrCloud mode enabled" : "SolrCloud režimas įjungtas", + "Source" : "Šaltinis", + "Sources" : "Šaltiniai", + "Sources:" : "Šaltiniai:", + "Standalone SOLR mode" : "Atskirasis SOLR režimas", + "Start Conversation" : "Pradėti pokalbį", + "Start Vectorization" : "Pradėti vektorizavimą", + "Start a conversation" : "Pradėti pokalbį", + "Starting names cache warmup..." : "Pradedamas vardų podėlio pradinis įkėlimas...", + "Starting..." : "Pradedama...", + "Statistics" : "Statistika", + "Stats calculation not yet implemented" : "Statistikos skaičiavimas dar neįgyvendintas", + "Status" : "Būsena", + "Status Code" : "Būsenos kodas", + "String Constraints" : "Eilučių apribojimai", + "Success" : "Sėkmė", + "Success Rate" : "Sėkmės dažnis", + "Success Status" : "Sėkmės būsena", + "Successful" : "Sėkminga", + "Successfully joined organisation" : "Sėkmingai prisijungta prie organizacijos", + "Sync Table" : "Sinchronizuoti lentelę", + "Synchronization Settings" : "Sinchronizavimo nustatymai", + "System Default" : "Sistemos numatytasis", + "Technical Description" : "Techninis aprašymas", + "Technical description for developers and administrators" : "Techninis aprašymas, skirtas programuotojams ir administratoriams", + "Temperature" : "Temperatūra", + "Templates" : "Šablonai", + "Test" : "Bandyti", + "Test Chat" : "Bandyti pokalbį", + "Test Connection" : "Bandyti prisijungimą", + "Test Embedding" : "Bandyti įterpinį", + "Test endpoint executed successfully" : "Bandomasis galinis taškas sėkmingai įvykdytas", + "Test webhook delivered successfully" : "Bandomasis žiniatinklio kabliukas sėkmingai pristatytas", + "Test webhook delivery failed" : "Bandomojo žiniatinklio kabliuko pristatymas nepavyko", + "Test webhook sent successfully" : "Bandomasis žiniatinklio kabliukas sėkmingai išsiųstas", + "Testing..." : "Bandoma...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Teksto fragmentai generuojami failų ištraukimo metu ir saugomi duomenų bazėje. Vektorizavimas nuskaito šiuos iš anksto suskaidytus failus ir konvertuoja juos į įterpinius.", + "Text extracted successfully" : "Tekstas sėkmingai ištrauktas", + "Text extraction disabled" : "Teksto ištraukimas išjungtas", + "The URL where webhook events will be sent" : "URL, į kurį bus siunčiami žiniatinklio kabliukų įvykiai", + "The requested conversation does not exist" : "Prašomas pokalbis neegzistuoja", + "The saved view and all its search configuration" : "Įrašytas rodinys ir visa jo paieškos konfigūracija", + "There are no audit trail entries matching your current filters." : "Nėra audito žurnalo įrašų, atitinkančių jūsų dabartinius filtrus.", + "There are no deleted items matching your current filters." : "Nėra pašalintų elementų, atitinkančių jūsų dabartinius filtrus.", + "There are no search trail entries matching your current filters." : "Nėra paieškos žurnalo įrašų, atitinkančių jūsų dabartinius filtrus.", + "There are no webhook log entries matching your filters." : "Nėra žiniatinklio kabliukų žurnalo įrašų, atitinkančių jūsų filtrus.", + "This action cannot be undone." : "Šio veiksmo atšaukti negalima.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Šio veiksmo atšaukti negalima. Įsitikinkite, kad jokia kolekcija nenaudoja šio ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Šis agentas neturi konfigūruojamų rodinių ar įrankių, tačiau vis tiek galite koreguoti žemiau esančius RAG nustatymus.", + "This analysis may take some time" : "Ši analizė gali užtrukti", + "This audit trail entry does not contain any change information." : "Šiame audito žurnalo įraše nėra jokios informacijos apie pakeitimus.", + "This configuration has no data defined." : "Šiai konfigūracijai neapibrėžta jokių duomenų.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Šis galinis taškas nebenaudojamas. Vietoj jo naudokite fragmentais pagrįstus galinius taškus.", + "This entity has no relations to objects or files" : "Ši esybė neturi ryšių su objektais ar failais", + "This is a test webhook from OpenRegister" : "Tai bandomasis žiniatinklio kabliukas iš OpenRegister", + "This schema must use Magic Table configuration to sync" : "Norint sinchronizuoti, ši schema turi naudoti stebuklingosios lentelės konfigūraciją", + "This source has no associated registers." : "Šis šaltinis neturi susietų registrų.", + "This will copy the _default ConfigSet with the new name" : "Tai nukopijuos _default ConfigSet su nauju pavadinimu", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Tai visam laikui pašalins VISUS įterpinius (vektorius) iš duomenų bazės. Jums reikės iš naujo vektorizuoti visus objektus ir failus. Šio veiksmo atšaukti negalima.\\n\\nAr tikrai norite tęsti?", + "This will permanently delete:" : "Tai visam laikui pašalins:", + "Timeout (seconds)" : "Skirtasis laikas (sekundėmis)", + "Timestamp" : "Laiko žyma", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Patarimas: įjunkite tik tuos rodinius, kuriems reikia semantinės paieškos, kad sumažintumėte įterpinių sąnaudas. Paprastoms paieškos lentelėms vektorizavimo retai prireikia.", + "Title" : "Pavadinimas", + "To Date" : "Iki datos", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Norėdami koreguoti fragmentų dydį ar strategiją, eikite į Failų konfigūraciją → Apdorojimo ribos.", + "To change the embedding provider or model, go to LLM Configuration." : "Norėdami pakeisti įterpinių teikėją ar modelį, eikite į LLM konfigūraciją.", + "To date" : "Iki datos", + "Toggle search sidebar" : "Perjungti paieškos šoninę juostą", + "Tools" : "Įrankiai", + "Top Deleters" : "Daugiausiai šalinę", + "Total" : "Iš viso", + "Total Audit Trails" : "Iš viso audito žurnalų", + "Total Chunks Available:" : "Iš viso galimų fragmentų:", + "Total Deleted Items" : "Iš viso pašalintų elementų", + "Total Members" : "Iš viso narių", + "Total Objects" : "Iš viso objektų", + "Total Objects in Database:" : "Iš viso objektų duomenų bazėje:", + "Total Organisations" : "Iš viso organizacijų", + "Total Results" : "Iš viso rezultatų", + "Total Searches" : "Iš viso paieškų", + "Total Size" : "Bendras dydis", + "Total Storage" : "Bendra saugykla", + "Total:" : "Iš viso:", + "Totals" : "Sumos", + "Type" : "Tipas", + "Type Issue" : "Tipo problema", + "Type Variations:" : "Tipo variacijos:", + "Type must be \"positive\" or \"negative\"" : "Tipas turi būti „positive“ arba „negative“", + "Type to search for groups" : "Rašykite, kad ieškotumėte grupių", + "Type to search for users" : "Rašykite, kad ieškotumėte naudotojų", + "Type to search groups..." : "Rašykite, kad ieškotumėte grupių...", + "Type to search users..." : "Rašykite, kad ieškotumėte naudotojų...", + "URL" : "URL", + "URL where Ollama is running" : "URL, kuriuo veikia Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Nepakeista", + "Unique Search Terms" : "Unikalūs paieškos terminai", + "Unique Terms" : "Unikalūs terminai", + "Unknown" : "Nežinoma", + "Unknown Register" : "Nežinomas registras", + "Unknown Schema" : "Nežinoma schema", + "Untitled View" : "Rodinys be pavadinimo", + "Update" : "Atnaujinti", + "Update Operations" : "Atnaujinimo operacijos", + "Update configuration: ..." : "Atnaujinti konfigūraciją: ...", + "Update register OAS: ..." : "Atnaujinti registro OAS: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Atnaujinti vektorius, kai pasikeičia objekto duomenys (rekomenduojama tiksliai paieškai)", + "Updated" : "Atnaujinta", + "Updated At" : "Atnaujinta", + "Updated:" : "Atnaujinta:", + "Uptime:" : "Veikimo laikas:", + "Usage Count" : "Naudojimo kiekis", + "Use SolrCloud with Zookeeper for distributed search" : "Naudoti SolrCloud su Zookeeper paskirstytajai paieškai", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Naudokite filtrus, kad susiaurintumėte audito žurnalo įrašus pagal registrą, schemą, veiksmo tipą, naudotoją, datų intervalą arba objekto ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Naudokite filtrus, kad susiaurintumėte pašalintus elementus pagal registrą, schemą, pašalinimo datą arba juos pašalinusį naudotoją.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Naudokite filtrus, kad susiaurintumėte paieškos žurnalo įrašus pagal registrą, schemą, sėkmės būseną, naudotoją, datų intervalą, paieškos terminus arba našumo metriką.", + "Used By Collections" : "Naudojama kolekcijų", + "User" : "Naudotojas", + "User Agent Statistics" : "Naudotojo agento statistika", + "User Description" : "Naudotojo aprašymas", + "User rate limits cleared successfully" : "Naudotojo dažnio apribojimai sėkmingai išvalyti", + "User removed successfully" : "Naudotojas sėkmingai pašalintas", + "User-friendly description shown in forms and help text" : "Naudotojui patogus aprašymas, rodomas formose ir pagalbos tekste", + "Username" : "Naudotojo vardas", + "Username for SOLR authentication (optional)" : "Naudotojo vardas SOLR tapatybės nustatymui (neprivaloma)", + "Users" : "Naudotojai", + "Using Pre-Generated Chunks" : "Naudojami iš anksto sugeneruoti fragmentai", + "Validate" : "Patvirtinti", + "Validate Objects" : "Patvirtinti objektus", + "Validation completed successfully" : "Patvirtinimas sėkmingai baigtas", + "Validation failed" : "Patvirtinimas nepavyko", + "Value" : "Reikšmė", + "Vector Dimensions" : "Vektoriaus matmenys", + "Vector Search Backend" : "Vektorinės paieškos vidinė sąsaja", + "Vector field: _embedding_" : "Vektoriaus laukas: _embedding_", + "Vectorization Triggers" : "Vektorizavimo suaktyvinimai", + "Vectorize All Files" : "Vektorizuoti visus failus", + "Vectorize All Objects" : "Vektorizuoti visus objektus", + "Vectorize all file types" : "Vektorizuoti visus failų tipus", + "Vectorize all views" : "Vektorizuoti visus rodinius", + "Vectorize on object creation" : "Vektorizuoti sukuriant objektą", + "Vectors will be stored in your existing object and file collections" : "Vektoriai bus saugomi jūsų esamose objektų ir failų kolekcijose", + "Version" : "Versija", + "Very High" : "Labai aukštas", + "View API Docs" : "Peržiūrėti API dokumentaciją", + "View Changes" : "Peržiūrėti pakeitimus", + "View Details" : "Peržiūrėti išsamią informaciją", + "View Error" : "Peržiūrėti klaidą", + "View File" : "Peržiūrėti failą", + "View Full Details" : "Peržiūrėti visą išsamią informaciją", + "View Logs" : "Peržiūrėti žurnalus", + "View Name" : "Rodinio pavadinimas", + "View Object" : "Peržiūrėti objektą", + "View Parameters" : "Rodinio parametrai", + "View Selection" : "Rodinių pasirinkimas", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Peržiūrėkite ir analizuokite paieškos žurnalus su išplėstinio filtravimo ir analitikos galimybėmis", + "View and analyze system audit trails with advanced filtering capabilities" : "Peržiūrėkite ir analizuokite sistemos audito žurnalus su išplėstinio filtravimo galimybėmis", + "View deleted successfully" : "Rodinys sėkmingai pašalintas", + "View entity details and manage relations" : "Peržiūrėti esybės išsamią informaciją ir valdyti ryšius", + "View name" : "Rodinio pavadinimas", + "View name is required" : "Reikia rodinio pavadinimo", + "View saved successfully!" : "Rodinys sėkmingai įrašytas!", + "View search analytics and manage search logs" : "Peržiūrėti paieškos analitiką ir valdyti paieškos žurnalus", + "View successfully deleted" : "Rodinys sėkmingai pašalintas", + "View successfully updated" : "Rodinys sėkmingai atnaujintas", + "View updated successfully!" : "Rodinys sėkmingai atnaujintas!", + "View webhook delivery logs and filter by webhook" : "Peržiūrėti žiniatinklio kabliukų pristatymo žurnalus ir filtruoti pagal žiniatinklio kabliuką", + "Views" : "Rodiniai", + "Visible to users" : "Matoma naudotojams", + "Wait for Response" : "Laukti atsako", + "Wait for webhook response before continuing (required for request/response flows)" : "Prieš tęsiant laukti žiniatinklio kabliuko atsako (būtina užklausos / atsako srautams)", + "Warmup Names Cache" : "Iš anksto įkelti vardų podėlį", + "Warning" : "Įspėjimas", + "Warning:" : "Įspėjimas:", + "Webhook" : "Žiniatinklio kabliukas", + "Webhook Log Details" : "Žiniatinklio kabliuko žurnalo išsami informacija", + "Webhook Logs" : "Žiniatinklio kabliukų žurnalai", + "Webhook created successfully" : "Žiniatinklio kabliukas sėkmingai sukurtas", + "Webhook deleted" : "Žiniatinklio kabliukas pašalintas", + "Webhook retry delivered successfully" : "Žiniatinklio kabliuko pakartojimas sėkmingai pristatytas", + "Webhook retry delivery failed" : "Žiniatinklio kabliuko pakartojimo pristatymas nepavyko", + "Webhook updated" : "Žiniatinklio kabliukas atnaujintas", + "Webhook updated successfully" : "Žiniatinklio kabliukas sėkmingai atnaujintas", + "Webhooks" : "Žiniatinklio kabliukai", + "Weekly" : "Kas savaitę", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Įvilkti žiniatinklio kabliuko naudingąją apkrovą į CloudEvents formatą geresniam sąveikumui", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Taip", + "You" : "Jūs", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Galite kurti naujus ConfigSets pagal _default šabloną arba įkelti pasirinktinius tiesiai į savo SOLR serverį.", + "You do not have access to this conversation" : "Jūs neturite prieigos prie šio pokalbio", + "You do not have permission to delete this conversation" : "Jūs neturite leidimo pašalinti šį pokalbį", + "You do not have permission to modify this conversation" : "Jūs neturite leidimo keisti šį pokalbį", + "You do not have permission to restore this conversation" : "Jūs neturite leidimo atkurti šį pokalbį", + "You must be logged in to favorite views" : "Norėdami pažymėti rodinius kaip mėgstamus, turite būti prisijungę", + "You need an AI agent to start a conversation." : "Norėdami pradėti pokalbį, jums reikia DI agento.", + "Your Fireworks AI API key. Get one at" : "Jūsų Fireworks AI API raktas. Gaukite jį adresu", + "Your OpenAI API key. Get one at" : "Jūsų OpenAI API raktas. Gaukite jį adresu", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Jūsų atsiliepimas užregistruotas. Pageidaudami čia galite pateikti papildomos informacijos...", + "Zookeeper Hosts" : "Zookeeper pagrindiniai kompiuteriai", + "Zookeeper Port" : "Zookeeper prievadas", + "Zookeeper Settings (SolrCloud)" : "Zookeeper nustatymai (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper prisijungimo eilutė SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper prievado numeris (neprivaloma, numatytasis 2181)", + "chunk" : "fragmentas", + "chunks" : "fragmentai", + "chunks processed" : "apdorota fragmentų", + "configuration(s)" : "konfigūracija (-os)", + "e.g., lib/Settings/config.json" : "pvz., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "pvz., lib/Settings/register.json", + "failed" : "nepavyko", + "file" : "failas", + "files" : "failai", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "nariai", + "menu or contact someone with permission to create agents." : "meniu arba kreipkitės į asmenį, turintį leidimą kurti agentus.", + "messages" : "žinutės", + "n8n URL and API key are required" : "Reikia n8n URL ir API rakto", + "n8n configuration saved successfully" : "n8n konfigūracija sėkmingai įrašyta", + "n8n connection not configured" : "n8n ryšys nesukonfigūruotas", + "n8n connection successful" : "n8n ryšys sėkmingas", + "n8n connection test successful" : "n8n ryšio bandymas sėkmingas", + "n8n integration disabled" : "n8n integracija išjungta", + "n8n integration enabled" : "n8n integracija įjungta", + "n8n project initialized successfully" : "n8n projektas sėkmingai inicijuotas", + "n8n settings saved successfully" : "n8n nustatymai sėkmingai įrašyti", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objektai", + "objects processed" : "apdorota objektų", + "or pick a range" : "arba pasirinkite intervalą", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "užtikrina gerą pusiausvyrą tarp greičio ir tikslumo.", + "register(s)" : "registras (-ai)", + "register(s) selected" : "pasirinkta registrų", + "required" : "privaloma", + "results" : "rezultatai", + "schema(s)" : "schema (-os)", + "schema(s) selected" : "pasirinkta schemų", + "sk-..." : "sk-...", + "this application" : "šią programą", + "to" : "iki", + "vectorized" : "vektorizuota", + "views selected" : "pasirinkta rodinių", + "ℹ️ Current Configuration" : "ℹ️ Dabartinė konfigūracija", + "⚡ Batch Processing" : "⚡ Paketinis apdorojimas", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Išsamus kontekstas ✓ Mažesnė tikimybė praleisti detales ✗ Lėtesni atsakymai ✗ Gali įtraukti mažiau aktualios informacijos", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Greitesni atsakymai ✓ Tikslingesni atsakymai ✗ Gali praleisti aktualią informaciją", + "✨ AI Features" : "✨ DI funkcijos", + "All search trails cleared successfully" : "Visi paieškos žurnalai sėkmingai išvalyti", + "Multiple search trail deletion not implemented yet" : "Kelių paieškos žurnalų šalinimas dar neįgyvendintas", + "No expired search trails found to clear" : "Nerasta išvalytinų pasibaigusio galiojimo paieškos žurnalų", + "Missing message" : "Trūksta žinutės", + "message content is required" : "reikia žinutės turinio", + "Missing conversation or agentUuid" : "Trūksta pokalbio arba agentUuid", + "Access denied" : "Prieiga atmesta", + "Conversation not found" : "Pokalbis nerastas", + "AI service not configured" : "DI paslauga nesukonfigūruota", + "Failed to process message" : "Nepavyko apdoroti žinutės", + "Missing conversationId" : "Trūksta conversationId", + "conversationId is required" : "Reikia conversationId", + "Failed to fetch conversation history" : "Nepavyko gauti pokalbio istorijos", + "Failed to clear conversation" : "Nepavyko išvalyti pokalbio", + "Invalid feedback type" : "Negaliojantis atsiliepimo tipas", + "type must be \"positive\" or \"negative\"" : "tipas turi būti „positive“ arba „negative“", + "Message not found" : "Žinutė nerasta", + "Failed to save feedback" : "Nepavyko įrašyti atsiliepimo", + "Failed to get chat statistics" : "Nepavyko gauti pokalbio statistikos", + "Not Found" : "Nerasta", + "Engine not found" : "Variklis nerastas", + "Consumer not found" : "Vartotojas nerastas", + "Backend parameter is required" : "Reikia vidinės sąsajos parametro", + "Failed to get database information: %s" : "Nepavyko gauti duomenų bazės informacijos: %s", + "SOLR setup error: %s" : "SOLR sąrankos klaida: %s", + "Reindex failed: %s" : "Indeksavimas iš naujo nepavyko: %s", + "Anonymized" : "Nuasmeninta", + "Entities detected" : "Aptikta esybių", + "Extract Now" : "Ištraukti dabar", + "Extracted at" : "Ištraukta", + "Extraction" : "Ištraukimas", + "Failed to load extraction data" : "Nepavyko įkelti ištraukimo duomenų", + "Failed to load register data" : "Nepavyko įkelti registro duomenų", + "Failed to retrieve extraction status." : "Nepavyko gauti ištraukimo būsenos.", + "Failed to retrieve objects for file." : "Nepavyko gauti failo objektų.", + "No extraction data available for this file" : "Šiam failui nėra ištraukimo duomenų", + "No register objects reference this file" : "Jokie registro objektai nesiremia šiuo failu", + "Not extracted" : "Neištraukta", + "Register Objects" : "Registro objektai", + "Risk level" : "Rizikos lygis", + "Text chunks" : "Teksto fragmentai", + "Unknown error" : "Nežinoma klaida", + "Very high" : "Labai aukštas", + "{title} in {register} / {schema}" : "{title} registre {register} / {schema}", + "Push Notifications" : "Tiesioginiai pranešimai", + "Real-time push notification status via notify_push" : "Tiesioginių pranešimų būsena per notify_push", + "Realtime push not available — the notify_push app is not installed" : "Tiesioginiai pranešimai negalimi — programa notify_push neįdiegta", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Norėdami įjungti tiesioginius atnaujinimus, įdiekite programą notify_push iš Nextcloud programų parduotuvės.", + "Open Nextcloud App Store" : "Atverti Nextcloud programų parduotuvę", + "notify_push is installed but not yet active" : "notify_push įdiegta, bet dar neaktyvi", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push įdiegta, bet OpenRegister dar nepatvirtino sėkmingo pranešimo. Norėdami suaktyvinti, suaktyvinkite objekto įrašymą arba patikrinkite savo notify_push konfigūraciją.", + "notify_push configuration guide" : "notify_push konfigūracijos vadovas", + "Realtime push active" : "Tiesioginiai pranešimai aktyvūs", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Tiesioginiai pranešimai aktyvūs. Prisijungę klientai gauna momentinius atnaujinimus, kai objektai sukuriami, atnaujinami arba pašalinami.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);" +); diff --git a/l10n/lt.json b/l10n/lt.json new file mode 100644 index 0000000000..078a999221 --- /dev/null +++ b/l10n/lt.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(reikalingas Dolphin)", + "(no subject)": "(be temos)", + "(no title)": "(be pavadinimo)", + "(unnamed)": "(be vardo)", + "(untitled card)": "(kortelė be pavadinimo)", + "({count} selected)": "(pasirinkta: {count})", + ", {failed} failed": ", nepavyko: {failed}", + "0 = unlimited": "0 = neribota", + "A deactivation request is pending.": "Laukiama deaktyvavimo užklausa.", + "AI Assistant": "DI asistentas", + "AI Chat": "DI pokalbis", + "AI service not configured": "DI paslauga nesukonfigūruota", + "API Key": "API raktas", + "API Request Quota (requests/day)": "API užklausų kvota (užklausų per dieną)", + "API Token Configuration": "API prieigos rakto konfigūracija", + "API Tokens": "API prieigos raktai", + "API tokens saved successfully": "API prieigos raktai sėkmingai įrašyti", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: ši ataskaita susieja kiekvieną verwerkingsactiviteit su visos jos gyvavimo trukmės audito sekos skaičiais, priskirtais jai. Auditoriai ir Autoriteit Persoonsgegevens naudoja tai priežiūros peržiūrai.", + "About ConfigSets": "Apie ConfigSets", + "Access denied": "Prieiga atmesta", + "Access organisation dashboard and analytics": "Prieiga prie organizacijos skydelio ir analitikos", + "Account": "Paskyra", + "Action": "Veiksmas", + "Action Distribution": "Veiksmų pasiskirstymas", + "Action:": "Veiksmas:", + "Actions": "Veiksmai", + "Active": "Aktyvus", + "Active Collections": "Aktyvios kolekcijos", + "Active Filters": "Aktyvūs filtrai", + "Active Organisation:": "Aktyvi organizacija:", + "Active Organisations": "Aktyvios organizacijos", + "Active filters:": "Aktyvūs filtrai:", + "Active organisation changed successfully": "Aktyvi organizacija sėkmingai pakeista", + "Active organisation set successfully": "Aktyvi organizacija sėkmingai nustatyta", + "Activities": "Veikla", + "Activity": "Veikla", + "Activity Period": "Veiklos laikotarpis", + "Add": "Pridėti", + "Add Attachment": "Pridėti priedą", + "Add Endpoint": "Pridėti galinį tašką", + "Add Groups": "Pridėti grupes", + "Add Property to '{title}'": "Pridėti savybę į '{title}'", + "Add Register": "Pridėti registrą", + "Add Schema": "Pridėti schemą", + "Add Source": "Pridėti šaltinį", + "Add User": "Pridėti naudotoją", + "Add User to Organisation": "Pridėti naudotoją į organizaciją", + "Add a contact from any of your address books to associate it with this object.": "Pridėkite kontaktą iš bet kurios savo adresų knygos, kad susietumėte jį su šiuo objektu.", + "Add a file or files": "Pridėti failą arba failus", + "Add card": "Pridėti kortelę", + "Add contact": "Pridėti kontaktą", + "Add endpoint": "Pridėti galinį tašką", + "Add oneOf entry": "Pridėti oneOf įrašą", + "Add range": "Pridėti intervalą", + "Add schema titles, descriptions, and register information to provide richer context for search": "Pridėkite schemų pavadinimus, aprašymus ir registrų informaciją, kad paieška turėtų išsamesnį kontekstą", + "Add to favorites": "Pridėti prie mėgstamų", + "Added": "Pridėta", + "Added to favorites": "Pridėta prie mėgstamų", + "Additional Information": "Papildoma informacija", + "Additional feedback saved. Thank you!": "Papildomas atsiliepimas įrašytas. Ačiū!", + "Addresses": "Adresai", + "Admin": "Administratorius", + "Advanced": "Išplėstiniai", + "Advanced Filters": "Išplėstiniai filtrai", + "Advanced Options": "Išplėstinės parinktys", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Išplėstiniai filtrai negalimi naudojant duomenų bazės šaltinį. Filtravimo parinktims perjunkite į Auto arba SOLR indeksą.", + "After": "Po", + "Agent Type": "Agento tipas", + "Agent deleted successfully": "Agentas sėkmingai pašalintas", + "Agent successfully created": "Agentas sėkmingai sukurtas", + "Agent successfully updated": "Agentas sėkmingai atnaujintas", + "Agents": "Agentai", + "Aggregated across schemas": "Apibendrinta per visas schemas", + "All": "Visi", + "All Categories": "Visos kategorijos", + "All Collections": "Visos kolekcijos", + "All Confidence Levels": "Visi patikimumo lygiai", + "All ConfigSets": "Visi ConfigSets", + "All Files": "Visi failai", + "All Levels": "Visi lygiai", + "All Types": "Visi tipai", + "All Webhooks": "Visi žiniatinklio kabliukai", + "All actions": "Visi veiksmai", + "All audit trails cleared successfully": "Visi audito žurnalai sėkmingai išvalyti", + "All clear": "Viskas tvarkoje", + "All file types": "Visi failų tipai", + "All registers": "Visi registrai", + "All schemas": "Visos schemos", + "All search trails cleared successfully": "Visi paieškos žurnalai sėkmingai išvalyti", + "All searches": "Visos paieškos", + "All users": "Visi naudotojai", + "All-day events": "Visos dienos įvykiai", + "Allowed MIME Types": "Leidžiami MIME tipai", + "Already linked": "Jau susieta", + "An error occurred while clearing audit trails": "Valant audito žurnalus įvyko klaida", + "An error occurred while deleting the audit trail": "Šalinant audito žurnalą įvyko klaida", + "An error occurred while deleting the object": "Trinant objektą įvyko klaida", + "An error occurred while deleting the view": "Šalinant rodinį įvyko klaida", + "An error occurred while permanently deleting the objects": "Visam laikui šalinant objektus įvyko klaida", + "An error occurred while restoring the objects": "Atkuriant objektus įvyko klaida", + "An error occurred while saving the organisation": "Įrašant organizaciją įvyko klaida", + "Analysis completed:": "Analizė baigta:", + "Analysis steps:": "Analizės žingsniai:", + "Analytics": "Analitika", + "Analyze Objects": "Analizuoti objektus", + "Analyze Properties": "Analizuoti savybes", + "Analyze existing properties for improvement opportunities": "Analizuoti esamas savybes ieškant tobulinimo galimybių", + "Analyzing...": "Analizuojama...", + "Anonymized": "Nuasmeninta", + "Anonymous Group": "Anoniminė grupė", + "Any favorites and sharing settings for this view": "Visi šio rodinio mėgstamų ir bendrinimo nustatymai", + "Any user": "Bet kuris naudotojas", + "App ID": "Programos ID", + "App store cache invalidated successfully": "Programų parduotuvės podėlis sėkmingai panaikintas", + "App store cache invalidated: {invalidated}": "Programų parduotuvės talpykla negaliojanti: {invalidated}", + "Application deleted successfully": "Programa sėkmingai pašalinta", + "Application identifier for this configuration (optional)": "Šios konfigūracijos programos identifikatorius (neprivaloma)", + "Application settings": "Programos nustatymai", + "Application successfully created": "Programa sėkmingai sukurta", + "Application successfully updated": "Programa sėkmingai atnaujinta", + "Applications": "Programos", + "Apply": "Taikyti", + "Apply Changes": "Taikyti pakeitimus", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Taikyti vaidmenį visoms šio registro schemoms, kurios neturi aiškių autorizacijos nepaisymų.", + "Approval Progress": "Patvirtinimo eiga", + "Approve": "Patvirtinti", + "Approved": "Patvirtinta", + "Archive": "Archyvas", + "Archive conversation": "Archyvuoti pokalbį", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Archyvuoti šį verwerkingsactiviteit? Audito sekos eilutės ir toliau jį nurodys.", + "Archived conversations are hidden from your active list": "Archyvuoti pokalbiai paslepiami iš jūsų aktyvaus sąrašo", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Ar tikrai norite IŠTRINTI kolekciją \"{name}\"?\n\nTai:\n• Visam laikui ištrins kolekciją ir visus jos duomenis\n• Pašalins visus indeksuotus dokumentus\n• Šio veiksmo nebus galima atšaukti", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Ar tikrai norite išvalyti senus paieškos žurnalus? Tai pašalins senesnius nei 30 dienų įrašus.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Ar tikrai norite išvalyti visus duomenis iš kolekcijos \"{name}\"?\n\nTai:\n• Ištrins visus indeksuotus dokumentus\n• Išsaugos kolekcijos struktūrą nepakitusią\n• Šio veiksmo nebus galima atšaukti", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Ar tikrai norite ištrinti programą \"{name}\"? Šio veiksmo nebus galima atšaukti.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Ar tikrai norite pašalinti pasirinktus audito žurnalus? Šio veiksmo atšaukti negalima.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Ar tikrai norite pašalinti pasirinktus paieškos žurnalus? Šio veiksmo atšaukti negalima.", + "Are you sure you want to delete this ConfigSet?": "Ar tikrai norite pašalinti šį ConfigSet?", + "Are you sure you want to delete this configset?": "Ar tikrai norite ištrinti šį ConfigSet?", + "Are you sure you want to leave '{name}'?": "Ar tikrai norite palikti '{name}'?", + "Are you sure you want to permanently delete": "Ar tikrai norite visam laikui pašalinti", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Ar tikrai norite iš naujo indeksuoti kolekciją \"{name}\"?\n\nTai:\n• Atstatys indeksą su visais objektais\n• Užtruks kelias minutes\n• Pakartotinio indeksavimo metu gali paveikti paieškos našumą", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Ar tikrai norite pašalinti schemą \"{schema}\" iš registro \"{register}\"? Šio veiksmo nebus galima atšaukti.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Ar tikrai norite švelniai ištrinti {count} aktyvius objektus schemai \"{schema}\"?\n\nJie bus pažymėti kaip ištrinti, bet vėliau gali būti pašalinti visam laikui.", + "Array Object Configuration:": "Masyvo objekto konfigūracija:", + "Articles": "Straipsniai", + "Ask a question...": "Užduokite klausimą...", + "Ask questions about your data using natural language": "Užduokite klausimus apie savo duomenis naudodami natūraliąją kalbą", + "Assigned collaborative tags": "Priskirtos bendradarbiavimo žymės", + "Assignment notifications": "Priskyrimo pranešimai", + "Attachment added to {name}": "Priedas pridėtas prie {name}", + "Attempt": "Bandymas", + "Audit Trail Changes": "Audito žurnalo pakeitimai", + "Audit Trail Details": "Audito žurnalo išsami informacija", + "Audit Trail Management": "Audito žurnalų valdymas", + "Audit Trail Statistics": "Audito žurnalų statistika", + "Audit Trails": "Audito žurnalai", + "Audit trail data copied to clipboard": "Audito žurnalo duomenys nukopijuoti į iškarpinę", + "Audit trail deleted successfully": "Audito žurnalas sėkmingai pašalintas", + "Audit trail successfully deleted": "Audito žurnalas sėkmingai pašalintas", + "Authentication": "Tapatybės nustatymas", + "Auto (e.g. Y for year, F Y for month)": "Automatiškai (pvz., Y metams, F Y mėnesiui)", + "Auto share": "Automatinis bendrinimas", + "Auto-commit disabled": "Automatinis patvirtinimas išjungtas", + "Auto-commit enabled": "Automatinis patvirtinimas įjungtas", + "Auto-create Default Organisation": "Automatiškai sukurti numatytąją organizaciją", + "Auto-retry failed vectorizations": "Automatiškai kartoti nepavykusius vektorizavimus", + "Automatically commit changes to SOLR index": "Automatiškai patvirtinti pakeitimus SOLR indekse", + "Automatically connect to the federation network.": "Automatiškai prisijungti prie federacijos tinklo.", + "Automatically create a default organisation if none exists when the app is initialized": "Automatiškai sukurti numatytąją organizaciją, jei jokia neegzistuoja inicijuojant programą", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automatiškai generuoti vektorinius įterpinius iš teksto fragmentų, kai failai įkeliami ir apdorojami", + "Automatically generate vector embeddings when objects are created or updated": "Automatiškai generuoti vektorinius įterpinius, kai objektai sukuriami arba atnaujinami", + "Automatically retry failed vectorization attempts (max 3 retries)": "Automatiškai kartoti nepavykusius vektorizavimo bandymus (daugiausiai 3 kartus)", + "Automatically update catalogues.": "Automatiškai atnaujinti katalogus.", + "Available Filters": "Galimi filtrai", + "Available Workflows": "Galimi darbo srautai", + "Available placeholders:": "Galimi vietos rezervuotojai:", + "Avatar": "Avataras", + "Avatar changes are not supported by your authentication provider.": "Jūsų autentifikavimo tiekėjas nepalaiko avataro pakeitimų.", + "Avatar removed": "Avataras pašalintas", + "Avatar updated successfully": "Avataras sėkmingai atnaujintas", + "Avg Execution Time": "Vid. vykdymo laikas", + "Avg Members/Org": "Vid. narių / organizacijoje", + "Avg Object Views/Session": "Vid. objektų peržiūrų / seanse", + "Avg Response Time": "Vid. atsako laikas", + "Avg Results/Search": "Vid. rezultatų / paieškoje", + "Avg Searches/Session": "Vid. paieškų / seanse", + "Back": "Atgal", + "Back to Entities": "Atgal į esybes", + "Back to Registers": "Atgal į registrus", + "Back to Webhooks": "Atgal į žiniatinklio kabliukus", + "Back to applications": "Atgal į programas", + "Back to entities": "Atgal į esybes", + "Backend parameter is required": "Reikia vidinės sąsajos parametro", + "Backend updated successfully. Please reload the application.": "Vidinė sąsaja sėkmingai atnaujinta. Įkelkite programą iš naujo.", + "Bandwidth Quota (MB/month)": "Pralaidumo kvota (MB per mėnesį)", + "Base URL (Optional)": "Bazinis URL (neprivaloma)", + "Basic Information": "Pagrindinė informacija", + "Batch Size": "Paketo dydis", + "Batch extraction completed": "Paketinis ištraukimas baigtas", + "Before": "Prieš", + "Before object vectorization can work:": "Kad objektų vektorizavimas veiktų:", + "Behavior": "Elgsena", + "Behavior Issue": "Elgsenos problema", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 trukmė, pvz., P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob saugykla nebenaudojama. Visi objektai dabar naudoja stebuklingąsias lenteles.", + "Bookmarks": "Žymės", + "Branch": "Šaka", + "Bulk Role Assignment: {title}": "Masinis vaidmenų priskyrimas: {title}", + "Bulk delete operation completed successfully": "Masinio šalinimo operacija sėkmingai baigta", + "Bulk save operation completed successfully": "Masinio įrašymo operacija sėkmingai baigta", + "Business Data": "Verslo duomenys", + "CSV": "CSV", + "Cache cleared successfully": "Podėlis sėkmingai išvalytas", + "Cache grew from {old} to {new} entries.": "Talpykla padidėjo nuo {old} iki {new} įrašų.", + "Cache shrunk from {old} to {new} entries.": "Talpykla sumažėjo nuo {old} iki {new} įrašų.", + "Cache stayed the same at {size} entries.": "Talpykla liko ta pati – {size} įrašų.", + "Calculate Sizes": "Apskaičiuoti dydžius", + "Calendar": "Kalendorius", + "Calendar Provider Configuration": "Kalendoriaus tiekėjo konfigūracija", + "Calendar integration is not available": "Kalendoriaus integracija nepasiekiama", + "Calendar name": "Kalendoriaus pavadinimas", + "Cancel": "Atsisakyti", + "Cancel deactivation request": "Atšaukti deaktyvavimo užklausą", + "Cannot delete: objects are still attached": "Negalima pašalinti: vis dar yra prikabintų objektų", + "Cannot edit: This register is managed by external configuration {title}": "Negalima redaguoti: šį registrą valdo išorinė konfigūracija {title}", + "Cannot remove schema with existing objects ({count} objects)": "Negalima pašalinti schemos su esamais objektais ({count} objektai)", + "Cannot remove schema {schema} because it contains {count} objects": "Negalima pašalinti schemos {schema}, nes joje yra {count} objektai", + "Cards": "Kortelės", + "Cascade delete": "Pakopinis trynimas", + "Categories of data subjects (one per line)": "Categorieën betrokkenen (één per regel)", + "Categories of personal data (one per line)": "Categorieën persoonsgegevens (één per regel)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategorija", + "Change Type": "Pakeitimo tipas", + "Change password": "Keisti slaptažodį", + "Changes": "Pakeitimai", + "Changes copied to clipboard": "Pakeitimai nukopijuoti į iškarpinę", + "Changes for Audit Trail #{id}": "Audito sekos #{id} pakeitimai", + "Chat Model": "Pokalbio modelis", + "Chat Provider": "Pokalbio teikėjas", + "Chat Provider (RAG)": "Pokalbio teikėjas (RAG)", + "Chat Settings": "Pokalbio nustatymai", + "Chat provider connection successful!": "Sėkmingai prisijungta prie pokalbio teikėjo!", + "Chat settings": "Pokalbio nustatymai", + "Chat test failed: {error}": "Pokalbio testas nepavyko: {error}", + "Choose a register": "Pasirinkite registrą", + "Choose a schema": "Pasirinkite schemą", + "Choose how vector similarity calculations are performed for semantic search": "Pasirinkite, kaip atliekami vektorių panašumo skaičiavimai semantinei paieškai", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Pasirinkite, kuriuos failų tipus įtraukti į vektorizavimo procesą. Bus apdorojami tik failai su ištrauktu tekstu ir fragmentais.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Pasirinkite, kuriuos rodinius įtraukti į vektorizavimo procesą. Palikite tuščią, kad būtų apdorojami visi rodiniai pagal jūsų konfigūraciją.", + "Chunk": "Fragmentas", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Fragmentų šalinimas dar neįgyvendintas. Naudokite fragmentais pagrįstus galinius taškus.", + "Chunks": "Fragmentai", + "Chunks to Vectorize:": "Vektorizuotini fragmentai:", + "Cleanup Old Trails": "Išvalyti senus žurnalus", + "Cleanup completed": "Valymas baigtas", + "Cleanup completed successfully. Deleted {count} entries.": "Valymas sėkmingai baigtas. Ištrinta {count} įrašų.", + "Cleanup failed: {error}": "Valymas nepavyko: {error}", + "Clear All Embeddings": "Išvalyti visus įterpinius", + "Clear Entries": "Išvalyti įrašus", + "Clear Filtered Audit Trails": "Išvalyti filtruotus audito žurnalus", + "Clear Filters": "Išvalyti filtrus", + "Clear Index": "Išvalyti indeksą", + "Clear Selection": "Išvalyti pasirinkimą", + "Clear all": "Išvalyti viską", + "Clear all filters": "Išvalyti visus filtrus", + "Clear date range": "Išvalyti datų intervalą", + "Clear filters": "Išvalyti filtrus", + "Clearing...": "Valoma...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Spustelėkite „Vykdyti atitikties nuskaitymą“, kad rastumėte schemas, kuriose aptikta PII, bet nėra verwerkingsactiviteit anotacijos.", + "Click to subscribe to notifications": "Spustelėkite, kad užsiprenumeruotumėte pranešimus", + "Click to unsubscribe from notifications": "Spustelėkite, kad atsisakytumėte pranešimų", + "Close": "Užverti", + "Cluster name": "Klasterio pavadinimas", + "Code": "Kodas", + "Code (short readable key, e.g. v-2026-001)": "Kodas (trumpas skaitomas raktas, pvz., v-2026-001)", + "Collapse details": "Sutraukti išsamią informaciją", + "Collection Management": "Kolekcijų valdymas", + "Collection Name": "Kolekcijos pavadinimas", + "Collection assignments updated successfully": "Kolekcijų priskyrimai sėkmingai atnaujinti", + "Collection cleared successfully": "Kolekcija sėkmingai išvalyta", + "Collection copied successfully": "Kolekcija sėkmingai nukopijuota", + "Collection created successfully": "Kolekcija sėkmingai sukurta", + "Collection deleted successfully": "Kolekcija sėkmingai pašalinta", + "Collection used to store and index file metadata and content": "Kolekcija, naudojama failų metaduomenims ir turiniui saugoti bei indeksuoti", + "Collection used to store and index object data": "Kolekcija, naudojama objektų duomenims saugoti bei indeksuoti", + "Collections:": "Kolekcijos:", + "Color": "Spalva", + "Comment...": "Komentaras...", + "Commit Message": "Patvirtinimo žinutė", + "Commit Within (ms)": "Patvirtinti per (ms)", + "Commit message": "Patvirtinimo žinutė", + "Compare current schema with real object data": "Palyginti dabartinę schemą su tikrais objektų duomenimis", + "Completed": "Baigta", + "Complex": "Sudėtinga", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Sudėtingos užklausos: išplėstinės paieškos su keliais filtrais, operatoriais ir sudėtingais parametrų deriniais", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Sudėtingos užklausos: išplėstinės paieškos su keliais filtrais, operatoriais ir sudėtingais parametrų deriniais", + "Compliance": "Atitiktis", + "Confidence Level": "Patikimumo lygis", + "Confidence Score": "Patikimumo įvertis", + "Confidence: {confidence}%": "Pasitikėjimas: {confidence}%", + "Config must be provided as an object": "Konfigūracija turi būti pateikta kaip objektas", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet valdymas", + "ConfigSet Name": "ConfigSet pavadinimas", + "ConfigSet created successfully": "ConfigSet sėkmingai sukurtas", + "ConfigSet deleted successfully": "ConfigSet sėkmingai pašalintas", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets apibrėžia jūsų SOLR kolekcijų schemą ir konfigūraciją. Juose yra laukų apibrėžtys, analizatoriai ir kiti paieškos nustatymai.", + "Configuration": "Konfigūracija", + "Configuration Data": "Konfigūracijos duomenys", + "Configuration is up to date": "Konfigūracija yra naujausia", + "Configuration saved": "Konfigūracija įrašyta", + "Configuration saved successfully.": "Konfigūracija sėkmingai įrašyta.", + "Configuration type (default, application, etc.)": "Konfigūracijos tipas (numatytasis, programa ir kt.)", + "Configurations": "Konfigūracijos", + "Configure API tokens for external service integrations": "Konfigūruoti API prieigos raktus išorinių paslaugų integracijoms", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigūruoti Apache SOLR paieškos variklį išplėstinėms paieškos galimybėms", + "Configure Facets": "Konfigūruoti aspektus", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigūruokite didžiųjų kalbos modelių (LLM) teikėjus DI funkcijoms, įskaitant semantinę paiešką, įterpinius ir pokalbį.", + "Configure SOLR Facets": "Konfigūruoti SOLR aspektus", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigūruokite pagrindinius SOLR serverio prisijungimo nustatymus, įskaitant tapatybės nustatymą ir tinklo parinktis. Branduoliams ir kolekcijoms valdyti naudokite atskirus ConfigSet ir Kolekcijų valdymo dialogus.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigūruokite pagrindinius savo SOLR serverio ryšio nustatymus, įskaitant autentifikavimą ir tinklo parinktis. Naudokite atskirus ConfigSet ir kolekcijų valdymo dialogus, kad valdytumėte branduolius ir kolekcijas.", + "Configure default organisation and organisation-related settings": "Konfigūruoti numatytąją organizaciją ir su organizacija susijusius nustatymus", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigūruokite, kaip duomenų bazės objektai konvertuojami į vektorinius įterpinius semantinei paieškai. Objektai vektorizuojami tiesiogiai, nereikalaujant teksto ištraukimo.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigūruokite, kaip objektai konvertuojami į tekstą prieš vektorizavimą. Šie nustatymai turi įtakos paieškos kokybei ir kontekstui.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigūruokite didelių kalbos modelių (LLM) tiekėjus dirbtiniu intelektu paremtoms funkcijoms, įskaitant semantinę paiešką, įterpinius ir pokalbį.", + "Configure multi-organization support and tenant isolation": "Konfigūruoti kelių organizacijų palaikymą ir nuomininkų izoliaciją", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigūruokite failų vektorizavimo parametrus. Šis procesas sugeneruos vektorinius įterpinius visiems ištrauktiems failų fragmentams.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigūruokite objektų vektorizavimo parametrus. Šis procesas sugeneruos vektorinius įterpinius visiems objektams, atitinkantiems jūsų rodinio filtrus.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigūruokite šią schemą, kad objektai būtų rodomi kaip įvykiai Nextcloud Calendar programoje.", + "Configure which types of data to search and how many sources to retrieve": "Konfigūruokite, kokių tipų duomenų ieškoti ir kiek šaltinių gauti", + "Configuring field mappings...": "Konfigūruojami laukų susiejimai...", + "Confirm Account Deactivation": "Patvirtinkite paskyros deaktyvavimą", + "Confirm Object Deletion": "Patvirtinkite objekto ištrynimą", + "Confirm Object Restoration": "Patvirtinkite objekto atkūrimą", + "Confirm Permanent Object Deletion": "Patvirtinkite galutinį objekto ištrynimą", + "Confirm deactivation": "Patvirtinkite deaktyvavimą", + "Connecting to SOLR cluster...": "Jungiamasi prie SOLR klasterio...", + "Connection Failed": "Nepavyko prisijungti", + "Connection Settings": "Prisijungimo nustatymai", + "Connection Successful!": "Sėkmingai prisijungta!", + "Connection failed": "Nepavyko prisijungti", + "Connection protocol": "Prisijungimo protokolas", + "Connection successful": "Sėkmingai prisijungta", + "Connection timeout in seconds": "Prisijungimo skirtasis laikas sekundėmis", + "Constant delay between retries (5 minutes)": "Pastovus delsimas tarp pakartojimų (5 minutės)", + "Constraint Issue": "Apribojimo problema", + "Consumer not found": "Vartotojas nerastas", + "Contacts": "Kontaktai", + "Contacts integration is not available": "Kontaktų integracija nepasiekiama", + "Content Filters": "Turinio filtrai", + "Continue to Properties": "Tęsti į savybes", + "Contracts": "Sutartys", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Valdykite, kuriuos objektų rodinius reikia vektorizuoti, kad sumažintumėte API sąnaudas. Vektorizuokite tik tuos rodinius, kuriems naudinga semantinė paieška.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Valdykite, kuriuos rodinius ir įrankius DI gali naudoti šiame pokalbyje. Pagal numatymą įjungtos visos agento galimybės.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Valdo atsitiktinumą (0 = deterministinis, 2 = labai kūrybiškas)", + "Conversation ID is required": "Reikia pokalbio ID", + "Conversation archived": "Pokalbis archyvuotas", + "Conversation archived successfully": "Pokalbis sėkmingai archyvuotas", + "Conversation cleared successfully": "Pokalbis sėkmingai išvalytas", + "Conversation deleted": "Pokalbis pašalintas", + "Conversation not found": "Pokalbis nerastas", + "Conversation permanently deleted": "Pokalbis pašalintas visam laikui", + "Conversation renamed": "Pokalbis pervadintas", + "Conversation restored": "Pokalbis atkurtas", + "Conversation started with {agent}": "Pokalbis pradėtas su {agent}", + "Conversation title": "Pokalbio pavadinimas", + "Conversations": "Pokalbiai", + "Converting {schema} to blob storage...": "Konvertuojama {schema} į blob saugyklą...", + "Converting {schema} to magic table...": "Konvertuojama {schema} į magic lentelę...", + "Copied!": "Nukopijuota!", + "Copy": "Kopijuoti", + "Copy Changes": "Kopijuoti pakeitimus", + "Copy Collection": "Kopijuoti kolekciją", + "Copy Data": "Kopijuoti duomenis", + "Copy Full Data": "Kopijuoti visus duomenis", + "Copy of {name}": "{name} kopija", + "Copy to clipboard": "Kopijuoti į iškarpinę", + "Copying...": "Kopijuojama...", + "Costs": "Sąnaudos", + "Count": "Kiekis", + "Counting objects...": "Skaičiuojami objektai...", + "Create": "Sukurti", + "Create API Token": "Sukurti API prieigos raktą", + "Create Agent": "Sukurti agentą", + "Create Application": "Sukurti programą", + "Create Collection": "Sukurti kolekciją", + "Create ConfigSet": "Sukurti ConfigSet", + "Create New Collection": "Sukurti naują kolekciją", + "Create New ConfigSet": "Sukurti naują ConfigSet", + "Create Operations": "Kūrimo operacijos", + "Create Organisation": "Sukurti organizaciją", + "Create Webhook": "Sukurti žiniatinklio kabliuką", + "Create a copy of collection:": "Sukurti kolekcijos kopiją:", + "Create a new ConfigSet based on the _default template": "Sukurti naują ConfigSet pagal _default šabloną", + "Create a new SOLR collection from an existing ConfigSet": "Sukurti naują SOLR kolekciją iš esamo ConfigSet", + "Create a new SOLR collection from an existing configset": "Sukurti naują SOLR kolekciją iš esamo ConfigSet", + "Create a new configset based on the _default template": "Sukurti naują ConfigSet pagal _default šabloną", + "Create a new event or link an existing one from any of your calendars.": "Sukurkite naują įvykį arba susiekite esamą iš bet kurio savo kalendoriaus.", + "Create another": "Sukurti dar vieną", + "Create copies of": "Sukurti kopijas", + "Create event": "Sukurti įvykį", + "Create new token": "Sukurti naują prieigos raktą", + "Create or link a Deck card to track work on this object.": "Sukurkite arba susiekite Deck kortelę, kad galėtumėte sekti darbą su šiuo objektu.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Sukurkite pirmąją verwerkingsactiviteit, kad pradėtumėte žymėti audito sekos eilutes jų AVG 30 str. priskyrimu.", + "Create your first AI agent to get started.": "Sukurkite savo pirmąjį DI agentą, kad pradėtumėte.", + "Created": "Sukurta", + "Created:": "Sukurta:", + "Creating search collections...": "Kuriamos paieškos kolekcijos...", + "Creating...": "Kuriama...", + "Current": "Dabartinis", + "Current Filters": "Dabartiniai filtrai", + "Current Type:": "Dabartinis tipas:", + "Current password": "Dabartinis slaptažodis", + "Custom API endpoint if using a different region": "Pasirinktinis API galinis taškas, jei naudojamas kitas regionas", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Pasirinktinės HTTP antraštės (po vieną eilutėje, formatas: Antraštės-Pavadinimas: reikšmė)", + "Custom HTTP headers (one per line, format: header-name: value)": "Pasirinktinės HTTP antraštės (po vieną eilutėje, formatas: antraštės-pavadinimas: reikšmė)", + "Custom Ranges:": "Pasirinktiniai diapazonai:", + "Custom display title for this facet": "Pasirinktinis šios fasetės rodomas pavadinimas", + "Custom pattern:": "Pasirinktinis šablonas:", + "DELETE COLLECTION": "IŠTRINTI KOLEKCIJĄ", + "DSAR": "DSAR", + "Daily": "Kasdien", + "Dashboard": "Skydelis", + "Dashboard not found": "Skydelis nerastas", + "Data": "Duomenys", + "Data Source": "Duomenų šaltinis", + "Data Sources": "Duomenų šaltiniai", + "Data Views": "Duomenų rodiniai", + "Data sources": "Duomenų šaltiniai", + "Data type does not match observed values": "Duomenų tipas neatitinka stebimų reikšmių", + "Data-subject access request": "Duomenų subjekto prieigos prašymas", + "Database URL": "Duomenų bazės URL", + "Database information refreshed": "Duomenų bazės informacija atnaujinta", + "Date Faceting:": "Datų fasetavimas:", + "Date Range": "Datų intervalas", + "Dates": "Datos", + "Deactivation request cancelled": "Deaktyvavimo prašymas atšauktas", + "Deactivation request submitted": "Deaktyvavimo prašymas pateiktas", + "Deck": "Deck", + "Deck integration is not available": "Deck integracija nepasiekiama", + "Default": "Numatytasis", + "Default Object Tenant": "Numatytasis objekto nuomininkas", + "Default Organisation": "Numatytoji organizacija", + "Default Owner": "Numatytasis savininkas", + "Default User (for cron/background jobs)": "Numatytasis naudotojas (cron / foniniams darbams)", + "Default User Tenant": "Numatytasis naudotojo nuomininkas", + "Default value": "Numatytoji reikšmė", + "Default value (Email)": "Numatytoji reikšmė (el. paštas)", + "Default value (Password)": "Numatytoji reikšmė (slaptažodis)", + "Default value (Phone number)": "Numatytoji reikšmė (telefono numeris)", + "Default value (Regex)": "Numatytoji reikšmė (reguliarioji išraiška)", + "Default: 5": "Numatytasis: 5", + "Defaults to current user. Select a different user if needed.": "Pagal numatytuosius nustatymus naudojamas dabartinis naudotojas. Prireikus pasirinkite kitą naudotoją.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Delsimas dvigubėja su kiekvienu bandymu (2, 4, 8 minutės...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Delsimas didėja tiesiškai (5, 10, 15 minučių...)", + "Delete": "Pašalinti", + "Delete ({count})": "Ištrinti ({count})", + "Delete Application": "Pašalinti programą", + "Delete Audit Trail": "Pašalinti audito žurnalą", + "Delete Collection": "Pašalinti kolekciją", + "Delete ConfigSet": "Pašalinti ConfigSet", + "Delete Objects": "Pašalinti objektus", + "Delete Operations": "Šalinimo operacijos", + "Delete SOLR Collection": "Ištrinti SOLR kolekciją", + "Delete View": "Pašalinti rodinį", + "Delete all objects in this schema": "Pašalinti visus šios schemos objektus", + "Delete permanently": "Pašalinti visam laikui", + "Delete this conversation?": "Pašalinti šį pokalbį?", + "Delete view": "Pašalinti rodinį", + "Delete {count} object": "Ištrinti {count} objektą", + "Delete {count} objects": "Ištrinti {count} objektus", + "Deleted": "Pašalinta", + "Deleted By": "Pašalino", + "Deleted Date": "Pašalinimo data", + "Deleted Items Management": "Pašalintų elementų valdymas", + "Deleted This Week": "Pašalinta šią savaitę", + "Deleted Today": "Pašalinta šiandien", + "Deleted:": "Pašalinta:", + "Deleting SOLR Collection...": "Trinama SOLR kolekcija...", + "Deleting...": "Šalinama...", + "Deletion Date Range": "Pašalinimo datų intervalas", + "Deletion Statistics": "Pašalinimo statistika", + "Deprecated": "Nebenaudojama", + "Depublish": "Atšaukti publikavimą", + "Description": "Aprašymas", + "Description Template": "Aprašymo šablonas", + "Description shown as tooltip": "Aprašymas, rodomas kaip patarimas", + "Description:": "Aprašymas:", + "Detect data types and patterns": "Aptikti duomenų tipus ir šablonus", + "Detected At": "Aptikta", + "Detected Format:": "Aptiktas formatas:", + "Detected Issues:": "Aptiktos problemos:", + "Detected: {date}": "Aptikta: {date}", + "Direct permission": "Tiesioginis leidimas", + "Disable": "Išjungti", + "Disable this to make the agent": "Išjunkite tai, kad agentas", + "Disabled": "Išjungta", + "Discover Files": "Atrasti failus", + "Discovered Properties": "Atrastos savybės", + "Display Format": "Rodymo formatas", + "Display Name": "Rodomas vardas", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Ar norite visam laikui pašalinti visus filtruotus audito žurnalo įrašus? Šio veiksmo atšaukti negalima.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Ar norite visam laikui pašalinti šį audito žurnalo įrašą? Šio veiksmo atšaukti negalima.", + "Documents": "Dokumentai", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR įjungtas", + "Download API Spec": "Atsisiųsti API specifikaciją", + "Download API Specification": "Atsisiųsti API specifikaciją", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Atsisiųskite visų savo asmens duomenų, saugomų OpenRegister, kopiją (GDPR 20 straipsnis).", + "Drag a file or files here": "Vilkite failą arba failus čia", + "Dry run completed: {count} fields would be created": "Bandomasis vykdymas baigtas: būtų sukurta {count} laukų", + "Dry run completed: {count} fields would be fixed": "Bandomasis vykdymas baigtas: būtų ištaisyta {count} laukų", + "Duration in seconds (optional)": "Trukmė sekundėmis (neprivaloma)", + "Edit": "Redaguoti", + "Edit Agent": "Redaguoti agentą", + "Edit Application": "Redaguoti programą", + "Edit Configuration": "Redaguoti konfigūraciją", + "Edit Endpoint": "Redaguoti galinį tašką", + "Edit Organisation": "Redaguoti organizaciją", + "Edit Property '{key}' of '{title}'": "Redaguoti '{title}' savybę '{key}'", + "Edit Register": "Redaguoti registrą", + "Edit Schema": "Redaguoti schemą", + "Edit Source": "Redaguoti šaltinį", + "Edit View": "Redaguoti rodinį", + "Edit Webhook": "Redaguoti žiniatinklio kabliuką", + "Edit labels": "Redaguoti etiketes", + "Edit processing activity": "Redaguoti verwerkingsactiviteit", + "Edit tags for {name}": "Redaguoti {name} žymas", + "Edit verwerkingsactiviteit": "Redaguoti verwerkingsactiviteit", + "Edit view": "Redaguoti rodinį", + "Edit view details": "Redaguoti rodinio išsamią informaciją", + "Effectiveness": "Efektyvumas", + "Email": "El. paštas", + "Email Addresses": "El. pašto adresai", + "Email digest frequency": "El. pašto santraukos dažnumas", + "Emails": "El. laiškai", + "Embedding Model": "Įterpinių modelis", + "Embedding Provider": "Įterpinių teikėjas", + "Embedding generated successfully": "Įterpinys sėkmingai sugeneruotas", + "Embedding provider connection successful!": "Sėkmingai prisijungta prie įterpinių teikėjo!", + "Embedding test failed: {error}": "Įterpinio testas nepavyko: {error}", + "Enable": "Įjungti", + "Enable RAG": "Įjungti RAG", + "Enable auto-creation": "Įjungti automatinį kūrimą", + "Enable automatic file vectorization": "Įjungti automatinį failų vektorizavimą", + "Enable automatic object vectorization": "Įjungti automatinį objektų vektorizavimą", + "Enable automatic synchronization": "Įjungti automatinį sinchronizavimą", + "Enable calendar provider": "Įjungti kalendoriaus tiekėją", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Įjungti išsamų SOLR operacijų žurnalinimą (rekomenduojama derinimui)", + "Enable faceting": "Įjungti aspektavimą", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Įjungti arba išjungti LLM funkcijas. Konfigūruokite teikėjus ir modelius naudodami LLM konfigūracijos mygtuką viršuje.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Įjunkite arba išjunkite LLM funkcijas. Sukonfigūruokite tiekėjus ir modelius naudodami aukščiau esantį LLM konfigūracijos mygtuką.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Įjungti arba išjungti SOLR paieškos integraciją. Konfigūruokite prisijungimo nustatymus naudodami Prisijungimo nustatymų mygtuką viršuje.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Įjunkite arba išjunkite SOLR paieškos integraciją. Sukonfigūruokite ryšio nustatymus naudodami aukščiau esantį ryšio nustatymų mygtuką.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Įjungti arba išjungti n8n darbo srautų integraciją. Konfigūruokite prisijungimo nustatymus žemiau.", + "Enable or disable this webhook": "Įjungti arba išjungti šį žiniatinklio kabliuką", + "Enable tools that allow the agent to interact with data through function calling.": "Įjunkite įrankius, kurie leidžia agentui sąveikauti su duomenimis per funkcijų iškvietimą.", + "Enable vectorization for all existing and future views (may increase costs)": "Įjungti vektorizavimą visiems esamiems ir būsimiems rodiniams (gali padidinti sąnaudas)", + "Enable write-back to target objects": "Įjungti įrašymą atgal į tikslinius objektus", + "Enabled": "Įjungta", + "End Date Field": "Pabaigos datos laukas", + "Endpoint Path*": "Galinio taško kelias*", + "Endpoints": "Galiniai taškai", + "Engine not found": "Variklis nerastas", + "Enter ConfigSet name": "Įveskite ConfigSet pavadinimą", + "Enter Nextcloud usernames to grant access to this private agent": "Įveskite Nextcloud naudotojų vardus, kad suteiktumėte prieigą prie šio privataus agento", + "Enter agent description (optional)": "Įveskite agento aprašymą (neprivaloma)", + "Enter agent name": "Įveskite agento vardą", + "Enter application description (optional)": "Įveskite programos aprašymą (neprivaloma)", + "Enter application name": "Įveskite programos pavadinimą", + "Enter collection name": "Įveskite kolekcijos pavadinimą", + "Enter configuration description (optional)": "Įveskite konfigūracijos aprašymą (neprivaloma)", + "Enter configuration title": "Įveskite konfigūracijos pavadinimą", + "Enter description (optional)...": "Įveskite aprašymą (neprivaloma)...", + "Enter new collection name": "Įveskite naują kolekcijos pavadinimą", + "Enter object ID": "Įveskite objekto ID", + "Enter organisation description (optional)": "Įveskite organizacijos aprašymą (neprivaloma)", + "Enter organisation name": "Įveskite organizacijos pavadinimą", + "Enter search term": "Įveskite paieškos terminą", + "Enter search terms or leave empty to browse all": "Įveskite paieškos terminus arba palikite tuščią, kad naršytumėte viską", + "Enter system prompt for the agent": "Įveskite agento sistemos užklausą", + "Enter username and press Enter": "Įveskite naudotojo vardą ir paspauskite Enter", + "Enter view name...": "Įveskite rodinio pavadinimą...", + "Enter webhook name": "Įveskite žiniatinklio kabliuko pavadinimą", + "Enter your API key": "Įveskite savo API raktą", + "Enter your object here...": "Įveskite savo objektą čia...", + "Enter your schema here...": "Įveskite savo schema čia...", + "Entities": "Esybės", + "Entities detected": "Aptikta esybių", + "Entity ID: {id}": "Subjekto ID: {id}", + "Entity Information": "Esybės informacija", + "Entity Recognition": "Subjektų atpažinimas", + "Entity Recognition Method": "Subjektų atpažinimo metodas", + "Entity deleted successfully": "Esybė sėkmingai pašalinta", + "Entity not found": "Esybė nerasta", + "Entries to be deleted:": "Pašalintini įrašai:", + "Enum Issue": "Sąrašo (enum) problema", + "Enum constraint is missing": "Trūksta sąrašo (enum) apribojimo", + "Erase (Art 17)": "Ištrinti (17 str.)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Ištrinti {count} šio subjekto objektą (-us)? Šis veiksmas registruojamas audito sekoje.", + "Erasure complete": "Ištrynimas baigtas", + "Erasure preview": "Ištrynimo peržiūra", + "Error": "Klaida", + "Error Details": "Klaidos išsami informacija", + "Error Information": "Klaidos informacija", + "Error Message": "Klaidos pranešimas", + "Error deleting audit trails: {error}": "Klaida trinant audito sekas: {error}", + "Error deleting search trails: {error}": "Klaida trinant paieškos sekas: {error}", + "Error loading application": "Klaida įkeliant programą", + "Error loading audit trails": "Klaida įkeliant audito žurnalus", + "Error loading entity": "Klaida įkeliant esybę", + "Error loading search trails": "Klaida įkeliant paieškos žurnalus", + "Estimated Batches:": "Numatomi paketai:", + "Estimated Cost:": "Numatomos sąnaudos:", + "Estimated Duration:": "Numatoma trukmė:", + "Event": "Įvykis", + "Event Property for Payload": "Įvykio savybė naudingajai apkrovai", + "Events": "Įvykiai", + "Every schema with detected PII has a processing-activity annotation.": "Kiekviena schema su aptiktais asmens duomenimis turi verwerkingsactiviteit anotaciją.", + "Example": "Pavyzdys", + "Example Value": "Pavyzdinė reikšmė", + "Example value for this property": "Šios savybės pavyzdinė reikšmė", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Išskirtinis maksimumas", + "Exclusive Minimum": "Išskirtinis minimumas", + "Exclusive maximum": "Išskirtinis maksimumas", + "Exclusive minimum": "Išskirtinis minimumas", + "Execution Mode": "Vykdymo režimas", + "Execution Time": "Vykdymo laikas", + "Execution Time Range": "Vykdymo laiko intervalas", + "Existing Improvements": "Esami patobulinimai", + "Expand details": "Išskleisti išsamią informaciją", + "Expiration": "Galiojimo pabaiga", + "Expires": "Galioja iki", + "Expires in (e.g., 90d)": "Galioja (pvz., 90d)", + "Exponential": "Eksponentinis", + "Export": "Eksportuoti", + "Export \"{schema}\" objects from \"{register}\"": "Eksportuoti „{schema}“ objektus iš „{register}“", + "Export Configuration": "Eksporto konfigūracija", + "Export Format:": "Eksporto formatas:", + "Export Objects": "Eksportuoti objektus", + "Export completed successfully": "Eksportavimas sėkmingai baigtas", + "Export downloaded successfully": "Eksportas sėkmingai atsisiųstas", + "Export failed: {error}": "Eksportas nepavyko: {error}", + "Export is rate limited. Please try again later.": "Eksportui taikomas dažnio apribojimas. Bandykite vėliau.", + "Export my data": "Eksportuoti mano duomenis", + "Export, view, or delete audit trails": "Eksportuoti, peržiūrėti arba pašalinti audito žurnalus", + "Exporting...": "Eksportuojama...", + "Extend Schema": "Išplėsti schemą", + "Extended": "Išplėsta", + "Extensions": "Plėtiniai", + "Extra Query Parameters": "Papildomi užklausos parametrai", + "Extract Now": "Ištraukti dabar", + "Extract Pending Files": "Ištraukti laukiančius failus", + "Extract now": "Išskleisti dabar", + "Extract properties from each object": "Ištraukti savybes iš kiekvieno objekto", + "Extracted At": "Ištraukta", + "Extracted at": "Ištraukta", + "Extraction": "Ištraukimas", + "Extraction Mode": "Išskleidimo režimas", + "Extraction Scope": "Išskleidimo apimtis", + "Extraction Status": "Ištraukimo būsena", + "Extraction error for {file}: {error}": "Išskleidimo klaida failui {file}: {error}", + "Facet Description": "Aspekto aprašymas", + "Facet Order": "Aspektų tvarka", + "Facet Title": "Aspekto pavadinimas", + "Facet Type": "Aspekto tipas", + "Facet configuration updated successfully": "Aspektų konfigūracija sėkmingai atnaujinta", + "Facetable": "Aspektuojama", + "Faceting Configuration:": "Aspektavimo konfigūracija:", + "Facets discovered and configured successfully": "Aspektai sėkmingai atrasti ir sukonfigūruoti", + "Facets discovered successfully": "Aspektai sėkmingai atrasti", + "Failed": "Nepavyko", + "Failed to add attachment to object": "Nepavyko pridėti priedo prie objekto", + "Failed to add user to organisation": "Nepavyko pridėti naudotojo prie organizacijos", + "Failed to analyze schema properties": "Nepavyko išanalizuoti schemos savybių", + "Failed to archive conversation": "Nepavyko archyvuoti pokalbio", + "Failed to calculate sizes": "Nepavyko apskaičiuoti dydžių", + "Failed to cancel deactivation": "Nepavyko atšaukti deaktyvavimo", + "Failed to change active organisation: {error}": "Nepavyko pakeisti aktyvios organizacijos: {error}", + "Failed to change password": "Nepavyko pakeisti slaptažodžio", + "Failed to check version: {error}": "Nepavyko patikrinti versijos: {error}", + "Failed to clear audit trails: {error}": "Nepavyko išvalyti audito sekų: {error}", + "Failed to clear blob objects: {error}": "Nepavyko išvalyti blob objektų: {error}", + "Failed to clear cache: {error}": "Nepavyko išvalyti talpyklos: {error}", + "Failed to clear collection": "Nepavyko išvalyti kolekcijos", + "Failed to clear conversation": "Nepavyko išvalyti pokalbio", + "Failed to clear embeddings: {error}": "Nepavyko išvalyti įterpinių: {error}", + "Failed to clear search trails: {error}": "Nepavyko išvalyti paieškos sekų: {error}", + "Failed to clear {type} cache: {error}": "Nepavyko išvalyti {type} talpyklos: {error}", + "Failed to copy changes": "Nepavyko nukopijuoti pakeitimų", + "Failed to copy data": "Nepavyko nukopijuoti duomenų", + "Failed to copy data to clipboard": "Nepavyko nukopijuoti duomenų į iškarpinę", + "Failed to copy to clipboard": "Nepavyko nukopijuoti į iškarpinę", + "Failed to copy token": "Nepavyko nukopijuoti prieigos rakto", + "Failed to create conversation": "Nepavyko sukurti pokalbio", + "Failed to create missing SOLR fields: {error}": "Nepavyko sukurti trūkstamų SOLR laukų: {error}", + "Failed to create or find project": "Nepavyko sukurti arba rasti projekto", + "Failed to create token": "Nepavyko sukurti prieigos rakto", + "Failed to delete SOLR collection: {error}": "Nepavyko ištrinti SOLR kolekcijos: {error}", + "Failed to delete collection": "Nepavyko pašalinti kolekcijos", + "Failed to delete conversation": "Nepavyko pašalinti pokalbio", + "Failed to delete objects for {schema}: {error}": "Nepavyko ištrinti objektų schemai {schema}: {error}", + "Failed to delete webhook": "Nepavyko pašalinti žiniatinklio kabliuko", + "Failed to depublish register: {error}": "Nepavyko išpublikuoti registro: {error}", + "Failed to depublish schema: {error}": "Nepavyko išpublikuoti schemos: {error}", + "Failed to depublish: {error}": "Nepavyko išpublikuoti: {error}", + "Failed to discover files: {error}": "Nepavyko aptikti failų: {error}", + "Failed to download API specification": "Nepavyko atsisiųsti API specifikacijos", + "Failed to export data": "Nepavyko eksportuoti duomenų", + "Failed to export objects": "Nepavyko eksportuoti objektų", + "Failed to fetch conversation history": "Nepavyko gauti pokalbio istorijos", + "Failed to fix mismatched SOLR fields: {error}": "Nepavyko ištaisyti neatitinkančių SOLR laukų: {error}", + "Failed to get SOLR field configuration": "Nepavyko gauti SOLR lauko konfigūracijos", + "Failed to get chat statistics": "Nepavyko gauti pokalbio statistikos", + "Failed to get database information: %s": "Nepavyko gauti duomenų bazės informacijos: %s", + "Failed to get entity categories": "Nepavyko gauti esybių kategorijų", + "Failed to get entity statistics": "Nepavyko gauti esybių statistikos", + "Failed to get entity types": "Nepavyko gauti esybių tipų", + "Failed to get response: {error}": "Nepavyko gauti atsakymo: {error}", + "Failed to invalidate app store cache: {error}": "Nepavyko panaikinti programų parduotuvės talpyklos: {error}", + "Failed to leave organisation: {error}": "Nepavyko palikti organizacijos: {error}", + "Failed to link object": "Nepavyko susieti objekto", + "Failed to load Deck cards": "Nepavyko įkelti Deck kortelių", + "Failed to load LLM configuration": "Nepavyko įkelti LLM konfigūracijos", + "Failed to load Nextcloud groups": "Nepavyko įkelti Nextcloud grupių", + "Failed to load cache statistics: {error}": "Nepavyko įkelti talpyklos statistikos: {error}", + "Failed to load conversation": "Nepavyko įkelti pokalbio", + "Failed to load entities": "Nepavyko įkelti esybių", + "Failed to load entity": "Nepavyko įkelti esybės", + "Failed to load extraction data": "Nepavyko įkelti ištraukimo duomenų", + "Failed to load files": "Nepavyko įkelti failų", + "Failed to load linked contacts": "Nepavyko įkelti susietų kontaktų", + "Failed to load linked emails": "Nepavyko įkelti susietų el. laiškų", + "Failed to load linked events": "Nepavyko įkelti susietų įvykių", + "Failed to load organisations": "Nepavyko įkelti organizacijų", + "Failed to load register data": "Nepavyko įkelti registro duomenų", + "Failed to load related entities": "Nepavyko įkelti susijusių subjektų", + "Failed to load settings: {error}": "Nepavyko įkelti nustatymų: {error}", + "Failed to load statistics: {error}": "Nepavyko įkelti statistikos: {error}", + "Failed to load templates": "Nepavyko įkelti šablonų", + "Failed to load view: {error}": "Nepavyko įkelti rodinio: {error}", + "Failed to load webhooks": "Nepavyko įkelti žiniatinklio kabliukų", + "Failed to load workflows": "Nepavyko įkelti darbo srautų", + "Failed to process message": "Nepavyko apdoroti žinutės", + "Failed to publish register: {error}": "Nepavyko publikuoti registro: {error}", + "Failed to publish schema: {error}": "Nepavyko publikuoti schemos: {error}", + "Failed to publish: {error}": "Nepavyko publikuoti: {error}", + "Failed to refresh database information": "Nepavyko atnaujinti duomenų bazės informacijos", + "Failed to reindex collection": "Nepavyko iš naujo indeksuoti kolekcijos", + "Failed to remove avatar": "Nepavyko pašalinti avataro", + "Failed to remove link": "Nepavyko pašalinti saito", + "Failed to remove member: {error}": "Nepavyko pašalinti nario: {error}", + "Failed to remove schema {schema}: {error}": "Nepavyko pašalinti schemos {schema}: {error}", + "Failed to remove user from organisation": "Nepavyko pašalinti naudotojo iš organizacijos", + "Failed to rename conversation": "Nepavyko pervadinti pokalbio", + "Failed to request deactivation": "Nepavyko pateikti deaktyvavimo užklausos", + "Failed to restore conversation": "Nepavyko atkurti pokalbio", + "Failed to retrieve extraction status.": "Nepavyko gauti ištraukimo būsenos.", + "Failed to retrieve objects for file.": "Nepavyko gauti failo objektų.", + "Failed to retry extraction": "Nepavyko pakartoti ištraukimo", + "Failed to retry webhook": "Nepavyko pakartoti žiniatinklio kabliuko", + "Failed to revoke token": "Nepavyko atšaukti prieigos rakto", + "Failed to save GitHub token": "Nepavyko įrašyti GitHub prieigos rakto", + "Failed to save GitLab URL": "Nepavyko įrašyti GitLab URL", + "Failed to save GitLab token": "Nepavyko įrašyti GitLab prieigos rakto", + "Failed to save LLM settings: {error}": "Nepavyko išsaugoti LLM nustatymų: {error}", + "Failed to save additional feedback": "Nepavyko įrašyti papildomo atsiliepimo", + "Failed to save configuration.": "Nepavyko išsaugoti konfigūracijos.", + "Failed to save configuration: {error}": "Nepavyko išsaugoti konfigūracijos: {error}", + "Failed to save facet configuration": "Nepavyko išsaugoti aspekto konfigūracijos", + "Failed to save feedback": "Nepavyko įrašyti atsiliepimo", + "Failed to save file settings: {error}": "Nepavyko išsaugoti failų nustatymų: {error}", + "Failed to save n8n configuration": "Nepavyko įrašyti n8n konfigūracijos", + "Failed to save preferences": "Nepavyko išsaugoti nuostatų", + "Failed to save roles": "Nepavyko įrašyti rolių", + "Failed to save settings": "Nepavyko įrašyti nustatymų", + "Failed to save settings: {error}": "Nepavyko išsaugoti nustatymų: {error}", + "Failed to save view: {error}": "Nepavyko išsaugoti rodinio: {error}", + "Failed to save warmup interval: {error}": "Nepavyko išsaugoti pašildymo intervalo: {error}", + "Failed to save webhook": "Nepavyko įrašyti žiniatinklio kabliuko", + "Failed to send feedback": "Nepavyko išsiųsti atsiliepimo", + "Failed to set active organisation: {error}": "Nepavyko nustatyti aktyvios organizacijos: {error}", + "Failed to setup SOLR: {error}": "Nepavyko nustatyti SOLR: {error}", + "Failed to start file warmup: {error}": "Nepavyko pradėti failų pašildymo: {error}", + "Failed to start processing {type} files: {error}": "Nepavyko pradėti {type} failų apdorojimo: {error}", + "Failed to start vectorization: {error}": "Nepavyko pradėti vektorizavimo: {error}", + "Failed to switch organisation: {error}": "Nepavyko perjungti organizacijos: {error}", + "Failed to sync magic table for {schema}: {error}": "Nepavyko sinchronizuoti magiškosios lentelės schemai {schema}: {error}", + "Failed to test SOLR connection: {error}": "Nepavyko patikrinti SOLR ryšio: {error}", + "Failed to test webhook": "Nepavyko išbandyti žiniatinklio kabliuko", + "Failed to update LLM settings: {error}": "Nepavyko atnaujinti LLM nustatymų: {error}", + "Failed to update Multitenancy settings: {error}": "Nepavyko atnaujinti kelių nuomininkų nustatymų: {error}", + "Failed to update RBAC settings: {error}": "Nepavyko atnaujinti RBAC nustatymų: {error}", + "Failed to update Retention settings: {error}": "Nepavyko atnaujinti saugojimo nustatymų: {error}", + "Failed to update SOLR settings: {error}": "Nepavyko atnaujinti SOLR nustatymų: {error}", + "Failed to update favorite status": "Nepavyko atnaujinti mėgstamo būsenos", + "Failed to update schema configuration for {schema}: {error}": "Nepavyko atnaujinti schemos {schema} konfigūracijos: {error}", + "Failed to update schema properties": "Nepavyko atnaujinti schemos savybių", + "Failed to update view": "Nepavyko atnaujinti rodinio", + "Failed to update view: {error}": "Nepavyko atnaujinti rodinio: {error}", + "Failed to update webhook": "Nepavyko atnaujinti žiniatinklio kabliuko", + "Failed to upload avatar": "Nepavyko įkelti avataro", + "Failed to validate {schema}: {error}": "Nepavyko patvirtinti {schema}: {error}", + "Failed to vectorize files: {error}": "Nepavyko vektorizuoti failų: {error}", + "Failed to warmup names cache: {error}": "Nepavyko pašildyti vardų talpyklos: {error}", + "Federation": "Federacija", + "Feedback recorded": "Atsiliepimas užregistruotas", + "Fewer sources (1-3):": "Mažiau šaltinių (1–3):", + "Field": "Laukas", + "Fields to Display": "Rodytini laukai", + "File": "Failas", + "File #{id}": "Failas #{id}", + "File Chunk Prediction": "Failo fragmentų prognozė", + "File Collection": "Failų kolekcija", + "File Handling": "Failų tvarkymas", + "File Location": "Failo vieta", + "File Management": "Failų valdymas", + "File Name": "Failo pavadinimas", + "File Path": "Failo kelias", + "File Type Selection": "Failų tipų pasirinkimas", + "File Types to Process": "Apdorotini failų tipai", + "File Vectorization": "Failų vektorizavimas", + "File Warmup": "Failų pradinis įkėlimas", + "File actions menu": "Failo veiksmų meniu", + "File anonymized successfully": "Failas sėkmingai nuasmenintas", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Failų fragmentų vektorizavimas dar neįgyvendintas. Fragmentai paruošti ir saugomi, tačiau vektorizavimo paslauga dar kuriama.", + "File collection not configured": "Failų kolekcija nesukonfigūruota", + "File discovery completed": "Failų atradimas baigtas", + "File extraction completed": "Failų ištraukimas baigtas", + "File extraction queued": "Failų ištraukimas įtrauktas į eilę", + "File indexed successfully": "Failas sėkmingai suindeksuotas", + "File is already anonymized": "Failas jau nuasmenintas", + "File name": "Failo pavadinimas", + "File not found": "Failas nerastas", + "File settings saved successfully": "Failų nustatymai sėkmingai išsaugoti", + "File settings updated successfully": "Failo nustatymai sėkmingai atnaujinti", + "File sources": "Failų šaltiniai", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Failų vektorizavimas baigtas! {vectorized} fragmentų vektorizuota iš {files} failų. {failed} nepavyko.", + "File vectorization configuration saved successfully": "Failų vektorizavimo konfigūracija sėkmingai įrašyta", + "File vectorization started. Check the statistics section for progress.": "Failų vektorizavimas pradėtas. Eigą stebėkite statistikos skiltyje.", + "File warmup completed": "Failų pradinis įkėlimas baigtas", + "Files": "Failai", + "Files added successfully": "Failai sėkmingai pridėti", + "Files with Completed Extraction:": "Failai su baigtu ištraukimu:", + "Files → fileCollection, Objects → objectCollection": "Failai → fileCollection, Objektai → objectCollection", + "Files → filecollection, objects → objectcollection": "Failai → filecollection, objektai → objectcollection", + "Filter Audit Trails": "Filtruoti audito žurnalus", + "Filter Deleted Items": "Filtruoti pašalintus elementus", + "Filter Objects": "Filtruoti objektus", + "Filter Properties": "Filtruoti savybes", + "Filter Search Trails": "Filtruoti paieškos žurnalus", + "Filter Statistics": "Filtravimo statistika", + "Filter and analyze search trail entries": "Filtruoti ir analizuoti paieškos žurnalo įrašus", + "Filter and manage audit trail entries": "Filtruoti ir valdyti audito žurnalo įrašus", + "Filter and manage soft deleted items": "Filtruoti ir valdyti minkštai pašalintus elementus", + "Filter and search entities": "Filtruoti ir ieškoti subjektų", + "Filter by object ID": "Filtruoti pagal objekto ID", + "Filter by search term": "Filtruoti pagal paieškos terminą", + "Filter by type": "Filtruoti pagal tipą", + "Filter by webhook": "Filtruoti pagal žiniatinklio kabliuką", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filtravimo duomenys įkelti automatiškai. Naudokite žemiau esančius filtrus, kad patikslintumėte paiešką.", + "Filter fields...": "Filtruoti laukus...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtruoti žiniatinklio kabliukų suaktyvinimus pagal naudingosios apkrovos savybes (po vieną eilutėje, formatas: raktas: reikšmė)", + "Filtered": "Filtruota", + "Filters": "Filtrai", + "Finalizing setup...": "Baigiama sąranka...", + "Fireworks AI Chat Settings": "Fireworks AI pokalbio nustatymai", + "Fireworks AI Embedding Configuration": "Fireworks AI įterpinių konfigūracija", + "First": "Pirmas", + "Fixed": "Pataisyta", + "For chat and retrieval-augmented generation": "Pokalbiui ir paieška papildytam generavimui", + "For vector embeddings and semantic search": "Vektoriniams įterpiniams ir semantinei paieškai", + "Format": "Formatas", + "Format Issue": "Formato problema", + "Format constraint is missing": "Trūksta formato apribojimo", + "From (e.g. -7 days or 2025-01-01)": "Nuo (pvz., -7 dienos arba 2025-01-01)", + "From Date": "Nuo datos", + "From date": "Nuo datos", + "Full data copied to clipboard": "Visi duomenys nukopijuoti į iškarpinę", + "Function Tools": "Funkcijų įrankiai", + "General Issue": "Bendra problema", + "Generate recommendations and confidence scores": "Generuoti rekomendacijas ir patikimumo įverčius", + "Generate report": "Generuoti ataskaitą", + "Generate the verantwoordingsdocument": "Generuoti verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generuoti vektorius iškart, kai sukuriami nauji objektai", + "Generated: {time}": "Sugeneruota: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Gaukite realaus laiko įžvalgų apie savo organizacijos duomenų būklę, sutelkdami dėmesį į registrus, schemų apibrėžtis bei objektų saugojimą ir naudojimą.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Gaukite tikralaikių įžvalgų apie savo organizacijos duomenų būklę, augimo tendencijas ir kokybės rodiklius.", + "GitHub Personal Access Token": "GitHub asmeninės prieigos prieigos raktas", + "GitHub token is valid": "GitHub prieigos raktas galioja", + "GitHub token is valid! Username: {username}": "GitHub prieigos raktas galioja! Naudotojo vardas: {username}", + "GitHub token removed successfully": "GitHub prieigos raktas sėkmingai pašalintas", + "GitHub token saved successfully": "GitHub prieigos raktas sėkmingai įrašytas", + "GitHub token test failed: {message}": "GitHub prieigos rakto patikra nepavyko: {message}", + "GitLab Personal Access Token": "GitLab asmeninės prieigos prieigos raktas", + "GitLab URL saved successfully": "GitLab URL sėkmingai įrašytas", + "GitLab token is valid": "GitLab prieigos raktas galioja", + "GitLab token is valid! Username: {username}": "GitLab prieigos raktas galioja! Naudotojo vardas: {username}", + "GitLab token saved successfully": "GitLab prieigos raktas sėkmingai įrašytas", + "GitLab token test failed: {message}": "GitLab prieigos rakto patikra nepavyko: {message}", + "Go to linked Register": "Eiti į susietą Registrą", + "Go to linked Schema": "Eiti į susietą Schemą", + "Go to organisation": "Eiti į organizaciją", + "Grant additional permissions beyond standard CRUD operations": "Suteikti papildomus leidimus be standartinių CRUD operacijų", + "Group Quota": "Grupės kvota", + "Groups": "Grupės", + "HTML preview": "HTML peržiūra", + "HTTP Method": "HTTP metodas", + "HTTP method used to send webhook requests": "HTTP metodas, naudojamas žiniatinklio kabliukų užklausoms siųsti", + "Headers": "Antraštės", + "Health": "Būklė", + "Heartbeat successful - connection kept alive": "Širdies plakimo signalas sėkmingas – ryšys palaikomas", + "Helpful": "Naudinga", + "Here you can configure the details for various connections.": "Čia galite konfigūruoti įvairių ryšių išsamią informaciją.", + "Here you can configure the details for your organisation.": "Čia galite konfigūruoti savo organizacijos išsamią informaciją.", + "Hide": "Slėpti", + "Hide Filters": "Slėpti filtrus", + "Hide in collection view": "Slėpti kolekcijos rodinyje", + "Hide in forms": "Slėpti formose", + "Hide in list view": "Slėpti sąrašo rodinyje", + "High": "Aukštas", + "High Confidence": "Aukštas patikimumas", + "Host": "Pagrindinis kompiuteris", + "Hourly": "Kas valandą", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Kaip giliai pereiti įdėtąsias objektų savybes (1–20). Didesnės reikšmės užfiksuoja daugiau detalių, bet padidina vektoriaus dydį.", + "How often to check for updates (1-168 hours)": "Kaip dažnai tikrinti, ar yra atnaujinimų (1–168 valandos)", + "How to handle retries for failed webhook deliveries": "Kaip tvarkyti pakartojimus dėl nepavykusių žiniatinklio kabliukų pristatymų", + "IBANs": "IBAN", + "ID": "ID", + "ID of input mapping (optional)": "Įvesties susiejimo ID (neprivaloma)", + "ID of output mapping (optional)": "Išvesties susiejimo ID (neprivaloma)", + "ID of the target resource": "Tikslinio ištekliaus ID", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "IP dažnio apribojimai sėkmingai išvalyti", + "Identify properties not in the schema": "Nustatyti savybes, kurių nėra schemoje", + "Immutable": "Nekintamas", + "Import": "Importuoti", + "Import Configuration": "Importuoti konfigūraciją", + "Import Data into Register": "Importuoti duomenis į Registrą", + "Import successful": "Importavimas sėkmingas", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importuokite report-bundle.json šabloną, kad gautumėte `reports` registrą, tada sukurkite savo pirmąją suvestinę naudodami standartinę objektų naudotojo sąsają. Suvestinės deklaruoja savo valdiklius JSON formatu, o atvaizduoklis kiekvienam valdikliui teikia tikralaikius agregavimo duomenis.", + "Improved Property": "Patobulinta savybė", + "In use": "Naudojama", + "Inactive": "Neaktyvus", + "Inactive organisations cannot be used": "Neaktyvių organizacijų naudoti negalima", + "Include IDs and names of related objects for better contextual search": "Įtraukti susijusių objektų ID ir vardus geresnei kontekstinei paieškai", + "Include ids and names of related objects for better contextual search": "Įtraukti susijusių objektų ID ir pavadinimus, kad būtų geresnė kontekstinė paieška", + "Include related object references": "Įtraukti susijusių objektų nuorodas", + "Include schema and register metadata": "Įtraukti schemos ir registro metaduomenis", + "Index": "Indeksas", + "Information about the current OpenRegister installation": "Informacija apie dabartinį OpenRegister diegimą", + "Inherited from register": "Paveldėta iš registro", + "Inherits permissions from register": "Paveldi leidimus iš registro", + "Initialization failed": "Inicijavimas nepavyko", + "Initialize Project": "Inicijuoti projektą", + "Initializing SOLR setup...": "Inicijuojama SOLR sąranka...", + "Input Mapping": "Įvesties susiejimas", + "Inspect Fields": "Apžiūrėti laukus", + "Inspect Index": "Apžiūrėti indeksą", + "Inspect SOLR Index": "Patikrinti SOLR indeksą", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Norėdami įjungti tiesioginius atnaujinimus, įdiekite programą notify_push iš Nextcloud programų parduotuvės.", + "Integrations": "Integracijos", + "Internet location (URL) of this installation": "Šio diegimo interneto vieta (URL)", + "Interval": "Intervalas", + "Invalid": "Negaliojantis", + "Invalid batch size. Must be between 1 and 5000": "Negaliojantis paketo dydis. Turi būti tarp 1 ir 5000", + "Invalid feedback type": "Negaliojantis atsiliepimo tipas", + "Invalid field name provided": "Pateiktas negaliojantis lauko pavadinimas", + "Invalid maxObjects. Must be 0 (all) or positive number": "Negaliojantis maxObjects. Turi būti 0 (visi) arba teigiamas skaičius", + "Invalid:": "Negaliojantis:", + "Invite Users": "Pakviesti naudotojus", + "Invited Users": "Pakviesti naudotojai", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Inzage rezultatai", + "Issue": "Problema", + "Items per page:": "Elementų puslapyje:", + "Just now": "Ką tik", + "Key": "Raktas", + "LLM Configuration": "LLM konfigūracija", + "LLM actions menu": "LLM veiksmų meniu", + "LLM configuration saved successfully": "LLM konfigūracija sėkmingai įrašyta", + "LLM features disabled": "LLM funkcijos išjungtos", + "LLM features enabled": "LLM funkcijos įjungtos", + "LLM must be enabled with an embedding provider configured": "LLM turi būti įjungtas su sukonfigūruotu įterpinių teikėju", + "LLM settings saved successfully": "LLM nustatymai sėkmingai įrašyti", + "LLM settings updated successfully": "LLM nustatymai sėkmingai atnaujinti", + "Label": "Etiketė", + "Labels": "Etiketės", + "Languages": "Kalbos", + "Last": "Paskutinis", + "Last Triggered": "Paskutinį kartą suaktyvinta", + "Leave empty to allow all users access": "Palikite tuščią, kad visi naudotojai turėtų prieigą", + "Leave off for auto-detection from property format.": "Palikite išjungtą, kad būtų automatiškai aptikta iš savybės formato.", + "Left organisation successfully": "Sėkmingai išėjote iš organizacijos", + "Legal basis *": "Teisinis pagrindas *", + "Length Range:": "Ilgio intervalas:", + "Linear": "Tiesinis", + "Link": "Saitas", + "Link an email from the Mail app sidebar to associate it with this object.": "Susiekite el. laišką iš Mail programėlės šoninės juostos, kad jį susietumėte su šiuo objektu.", + "Link an object to see it here.": "Susiekite objektą, kad jį čia matytumėte.", + "Link another object": "Susieti kitą objektą", + "Link existing event": "Susieti esamą įvykį", + "Link removed": "Saitas pašalintas", + "Link to Object": "Susieti su objektu", + "Link to {name}": "Susieti su {name}", + "Linked Objects": "Susieti objektai", + "Linked by {user}": "Susiejo {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Susieti el. laiškai, kalendoriaus įvykiai, kontaktai ir Deck kortelės bus rodomi čia.", + "Linked to {name}": "Susieta su {name}", + "Load Advanced Filters": "Įkelti išplėstinius filtrus", + "Load advanced filters with live data from your search index": "Įkelti išplėstinius filtrus su tiesioginiais duomenimis iš jūsų paieškos indekso", + "Load more": "Įkelti daugiau", + "Load view": "Įkelti rodinį", + "Loading API tokens...": "Įkeliami API prieigos raktai...", + "Loading ConfigSets...": "Įkeliama ConfigSets...", + "Loading LLM configuration...": "Įkeliama LLM konfigūracija...", + "Loading RBAC settings...": "Įkeliami RBAC nustatymai...", + "Loading activity...": "Įkeliama veikla...", + "Loading advanced filters...": "Įkeliami išplėstiniai filtrai...", + "Loading agents...": "Įkeliami agentai...", + "Loading application details...": "Įkeliama išsami programos informacija...", + "Loading applications...": "Įkeliamos programos...", + "Loading archived conversations...": "Įkeliami archyvuoti pokalbiai...", + "Loading audit trails...": "Įkeliami audito žurnalai...", + "Loading available tools...": "Įkeliami galimi įrankiai...", + "Loading cache statistics...": "Įkeliama podėlio statistika...", + "Loading collections...": "Įkeliamos kolekcijos...", + "Loading configurations...": "Įkeliamos konfigūracijos...", + "Loading conversation...": "Įkeliamas pokalbis...", + "Loading conversations...": "Įkeliami pokalbiai...", + "Loading deleted items...": "Įkeliami pašalinti elementai...", + "Loading endpoints": "Įkeliami galiniai taškai", + "Loading entities...": "Įkeliami subjektai...", + "Loading events...": "Įkeliami įvykiai...", + "Loading filter data...": "Įkeliami filtravimo duomenys...", + "Loading groups...": "Įkeliamos grupės...", + "Loading linked objects...": "Įkeliami susieti objektai...", + "Loading log details...": "Įkeliama išsami žurnalo informacija...", + "Loading multitenancy settings...": "Įkeliami kelių nuomininkų nustatymai...", + "Loading n8n configuration...": "Įkeliama n8n konfigūracija...", + "Loading permission matrix...": "Įkeliama leidimų matrica...", + "Loading preferences...": "Įkeliamos nuostatos...", + "Loading register data...": "Įkeliami registro duomenys...", + "Loading registers...": "Įkeliami registrai...", + "Loading retention settings...": "Įkeliami saugojimo nustatymai...", + "Loading schemas...": "Įkeliamos schemos...", + "Loading search configuration...": "Įkeliama paieškos konfigūracija...", + "Loading search trails...": "Įkeliami paieškos žurnalai...", + "Loading sources...": "Įkeliami šaltiniai...", + "Loading statistics...": "Įkeliama statistika...", + "Loading text extraction settings...": "Įkeliami teksto išgavimo nustatymai...", + "Loading tokens...": "Įkeliami prieigos raktai...", + "Loading user groups...": "Įkeliamos naudotojų grupės...", + "Loading users...": "Įkeliami naudotojai...", + "Loading views...": "Įkeliami rodiniai...", + "Loading...": "Įkeliama...", + "Local": "Vietinis", + "Local Version": "Vietinė versija", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Raskite kiekvieną objektą, susijusį su duomenų subjektu (Art 15 inzage), peržiūrėkite ištrynimą (Art 17 vergetelheid) arba eksportuokite jų duomenis (Art 20 portabiliteit).", + "Location": "Vieta", + "Location Field": "Vietos laukas", + "Locations": "Vietos", + "Locked": "Užrakinta", + "Locked:": "Užrakinta:", + "Login successful": "Prisijungimas sėkmingas", + "Logs": "Žurnalai", + "Low": "Žemas", + "Low Confidence": "Žemas patikimumas", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Mažesni skaičiai filtro šoninėje juostoje rodomi pirmiau. Palikite tuščią, kad rikiavimas būtų automatinis.", + "Lucene Version:": "Lucene versija:", + "Magic table sync completed for {schema}": "Magic lentelės sinchronizavimas baigtas, schema: {schema}", + "Magic table synchronized successfully": "Stebuklingoji lentelė sėkmingai sinchronizuota", + "Mail Integration": "Mail integracija", + "Mail integration is not available": "Mail integracija negalima", + "Make this installation discoverable within the federation network.": "Padaryti šį diegimą aptinkamą federacijos tinkle.", + "Manage Organisation Roles": "Valdyti organizacijos roles", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Valdykite SOLR kolekcijas (duomenų saugyklas) ir priskirkite jas objektams bei failams.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Valdykite SOLR ConfigSets (konfigūracijos šablonus) savo kolekcijoms.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Tvarkykite SOLR kolekcijas (duomenų saugyklas) ir priskirkite jas objektams bei failams.", + "Manage SOLR configsets (configuration templates) for your collections.": "Tvarkykite savo kolekcijų SOLR ConfigSets (konfigūracijos šablonus).", + "Manage and configure agents for automated tasks": "Valdykite ir konfigūruokite agentus automatinėms užduotims", + "Manage and monitor file text extraction status": "Valdykite ir stebėkite failų teksto ištraukimo būseną", + "Manage and restore soft deleted items from your registers": "Valdykite ir atkurkite minkštai pašalintus elementus iš savo registrų", + "Manage and view detected entities from files and objects": "Valdykite ir peržiūrėkite iš failų bei objektų aptiktas esybes", + "Manage document templates and themes": "Valdykite dokumentų šablonus ir temas", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Valdykite n8n darbo srautus OpenRegister automatizavimui. Darbo srautai bus saugomi sukonfigūruotame projekte.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Tvarkykite verwerkingsactiviteit, vykdykite duomenų subjektų prieigos užklausas ir audituokite atitiktį pagal ES GDPR / Nyderlandų AVG.", + "Manage webhooks for event-driven integrations": "Valdykite žiniatinklio kabliukus įvykiais pagrįstoms integracijoms", + "Manage your account settings, security, and personal data.": "Tvarkykite savo paskyros nustatymus, saugą ir asmens duomenis.", + "Manage your applications and modules": "Valdykite savo programas ir modulius", + "Manage your chat conversations": "Tvarkykite savo pokalbius", + "Manage your data registers and their configurations": "Valdykite savo duomenų registrus ir jų konfigūracijas", + "Manage your data schemas and their properties": "Valdykite savo duomenų schemas ir jų savybes", + "Manage your data sources and their configurations": "Valdykite savo duomenų šaltinius ir jų konfigūracijas", + "Manage your organisations and switch between them": "Tvarkykite savo organizacijas ir perjunkite tarp jų", + "Manage your saved search configurations": "Valdykite savo įrašytas paieškos konfigūracijas", + "Manage your system configurations and settings": "Valdykite savo sistemos konfigūracijas ir nustatymus", + "Managed": "Valdoma", + "Mappings": "Susiejimai", + "Mass validation completed successfully": "Masinis tikrinimas sėkmingai baigtas", + "Mass validation failed: {error}": "Masinis tikrinimas nepavyko: {error}", + "Max Files (0 = all)": "Maks. failų (0 = visi)", + "Max Length": "Maks. ilgis", + "Max Length:": "Maks. ilgis:", + "Max Objects (0 = all)": "Maks. objektų (0 = visi)", + "Max Retries": "Maks. pakartojimų", + "Max Shards/Node": "Maks. fragmentų / mazge", + "Max Tokens": "Daugiausia atpažinimo ženklų", + "Max execution time (ms)": "Maks. vykdymo laikas (ms)", + "Max ms": "Maks. ms", + "Max result count": "Maks. rezultatų kiekis", + "Max results": "Maks. rezultatų", + "Maximum": "Maksimumas", + "Maximum File Size (MB)": "Didžiausias failo dydis (MB)", + "Maximum Nesting Depth": "Maksimalus įdėjimo gylis", + "Maximum length": "Didžiausias ilgis", + "Maximum length constraint is missing": "Trūksta maksimalaus ilgio apribojimo", + "Maximum length is too restrictive": "Maksimalus ilgis pernelyg ribojantis", + "Maximum number of files to process. Set to 0 to process all files.": "Didžiausias apdorotinų failų skaičius. Nustatykite 0, kad būtų apdoroti visi failai.", + "Maximum number of items": "Didžiausias elementų skaičius", + "Maximum number of objects to process. Set to 0 to process all objects.": "Didžiausias apdorotinų objektų skaičius. Nustatykite 0, kad būtų apdoroti visi objektai.", + "Maximum number of retry attempts for failed deliveries": "Didžiausias pakartojimo bandymų skaičius dėl nepavykusių pristatymų", + "Maximum time to wait before committing changes": "Maksimalus laukimo laikas prieš patvirtinant pakeitimus", + "Maximum tokens to generate": "Maksimalus generuotinų prieigos raktų skaičius", + "Maximum value": "Didžiausia reikšmė", + "Maximum value constraint is missing": "Trūksta maksimalios reikšmės apribojimo", + "Maximum value is too restrictive": "Maksimali reikšmė pernelyg ribojanti", + "Medium": "Vidutinis", + "Medium Confidence": "Vidutinis patikimumas", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Vidutinės užklausos: paieškos su tam tikru filtravimu arba keliais parametrais (pvz., datų intervalai, konkretūs registrai / schemos)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Vidutinės užklausos: paieškos su tam tikru filtravimu arba keliais parametrais (pvz., datų intervalais, konkrečiais registrais / schemomis)", + "Member removed successfully": "Narys sėkmingai pašalintas", + "Members": "Nariai", + "Members ({count})": "Nariai ({count})", + "Members of selected groups can access this view": "Pasirinktų grupių nariai gali pasiekti šį rodinį", + "Members:": "Nariai:", + "Memory prediction calculated successfully": "Atminties prognozė sėkmingai apskaičiuota", + "Message content is required": "Reikia žinutės turinio", + "Message does not belong to this conversation": "Žinutė nepriklauso šiam pokalbiui", + "Message not found": "Žinutė nerasta", + "Metadata": "Metaduomenys", + "Metadata Filters": "Metaduomenų filtrai", + "Method": "Metodas", + "Method*": "Metodas*", + "Method: {method}": "Metodas: {method}", + "Min Length": "Min. ilgis", + "Min execution time (ms)": "Min. vykdymo laikas (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. rezultatų kiekis", + "Min results": "Min. rezultatų", + "Minimum": "Minimumas", + "Minimum length": "Mažiausias ilgis", + "Minimum number of items": "Mažiausias elementų skaičius", + "Minimum value": "Mažiausia reikšmė", + "Minimum value constraint is missing": "Trūksta minimalios reikšmės apribojimo", + "Minimum value is too restrictive": "Minimali reikšmė pernelyg ribojanti", + "Missing conversation or agentUuid": "Trūksta pokalbio arba agentUuid", + "Missing conversationId": "Trūksta conversationId", + "Missing message": "Trūksta žinutės", + "Missing object fields created successfully": "Trūkstami objekto laukai sėkmingai sukurti", + "Mode:": "Režimas:", + "Model": "Modelis", + "Model Name": "Modelio pavadinimas", + "Modified": "Pakeista", + "Monthly": "Kas mėnesį", + "More sources (10-20):": "Daugiau šaltinių (10–20):", + "Most Active Objects": "Aktyviausi objektai", + "Multiple Of": "Kartotinis", + "Multiple of": "Kartotinis", + "Multiple search trail deletion not implemented yet": "Kelių paieškos žurnalų šalinimas dar neįgyvendintas", + "Multitenancy": "Keli nuomininkai", + "Multitenancy settings updated successfully": "Kelių nuomininkų nustatymai sėkmingai atnaujinti", + "My Account": "Mano paskyra", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Vardas", + "Name *": "Pavadinimas *", + "Name*": "Pavadinimas*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Pavadinimų talpykla sėkmingai paruošta: {count} pavadinimų įkelta per {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Pavadinimų talpykla sėkmingai paruošta: {count} pavadinimų įkelta per {time}. {message}", + "Names cache warmup completed": "Vardų podėlio pradinis įkėlimas baigtas", + "Naming pattern for copies:": "Kopijų pavadinimų šablonas:", + "New Collection Name": "Naujos kolekcijos pavadinimas", + "New Configuration": "Nauja konfigūracija", + "New Conversation": "Naujas pokalbis", + "New Properties": "Naujos savybės", + "New Property": "Nauja savybė", + "New User Group": "Nauja vartotojų grupė", + "New Value": "Nauja reikšmė", + "New activity": "Nauja veikla", + "New password": "Naujas slaptažodis", + "New processing activity": "Nauja tvarkymo veikla", + "New users without specific organisation membership will be automatically added to this organisation": "Nauji naudotojai be konkrečios organizacijos narystės bus automatiškai pridėti prie šios organizacijos", + "New verwerkingsactiviteit": "Nauja verwerkingsactiviteit", + "Next": "Kitas", + "Nextcloud Groups": "Nextcloud grupės", + "No": "Ne", + "No API tokens.": "Nėra API prieigos raktų.", + "No ConfigSets found": "ConfigSets nerasta", + "No Deck cards linked to this object": "Su šiuo objektu nėra susietų Deck kortelių", + "No GitHub token configured": "Nesukonfigūruotas joks GitHub prieigos raktas", + "No GitHub token provided": "Nepateiktas joks GitHub prieigos raktas", + "No GitLab token provided": "Nepateiktas joks GitLab prieigos raktas", + "No Organisation": "Nėra organizacijos", + "No active filters": "Nėra aktyvių filtrų", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Schemai {schema} nėra aktyvių objektų, kuriuos būtų galima švelniai pašalinti. Norėdami pašalinti švelniai pašalintus objektus, naudokite „Pašalinti visam laikui“.", + "No activity data available": "Nėra veiklos duomenų", + "No activity found.": "Veiklos nerasta.", + "No agents available": "Nėra agentų", + "No agents found": "Agentų nerasta", + "No app store cache exists yet": "Programų parduotuvės podėlio dar nėra", + "No applications are available.": "Nėra galimų programų.", + "No applications found": "Programų nerasta", + "No approval steps for this object.": "Šiam objektui nėra patvirtinimo žingsnių.", + "No archived conversations": "Nėra archyvuotų pokalbių", + "No audit trail entries found": "Audito žurnalo įrašų nerasta", + "No changes recorded": "Pakeitimų neužregistruota", + "No chunks to vectorize": "Nėra vektorizuotinų fragmentų", + "No collections found": "Kolekcijų nerasta", + "No configuration data": "Nėra konfigūracijos duomenų", + "No configuration selected": "Nepasirinkta jokia konfigūracija", + "No configurations are available.": "Nėra galimų konfigūracijų.", + "No configurations found": "Konfigūracijų nerasta", + "No configurations found for this application.": "Šiai programai konfigūracijų nerasta.", + "No contacts linked to this object": "Su šiuo objektu nėra susietų kontaktų", + "No conversations yet. Create a new one to get started!": "Dar nėra pokalbių. Sukurkite naują, kad pradėtumėte!", + "No dashboards yet": "Dar nėra skydelių", + "No data available": "Nėra duomenų", + "No data available for chart": "Nėra diagramai skirtų duomenų", + "No deleted items found": "Pašalintų elementų nerasta", + "No deletion data available": "Nėra pašalinimo duomenų", + "No description found": "Aprašymo nerasta", + "No description provided": "Nepateiktas joks aprašymas", + "No emails linked to this object": "Su šiuo objektu nėra susietų el. laiškų", + "No endpoints defined": "Nėra apibrėžtų galinių taškų", + "No entities detected for this email.": "Šiame el. laiške nenustatyta jokių objektų.", + "No entities detected in this file. Run text extraction first.": "Šiame faile esybių neaptikta. Pirmiausia atlikite teksto ištraukimą.", + "No entities found": "Esybių nerasta", + "No entities have been detected yet": "Esybių dar neaptikta", + "No events found": "Įvykių nerasta", + "No events linked to this object": "Su šiuo objektu nėra susietų įvykių", + "No expired audit trails found to clear": "Nerasta išvalytinų pasibaigusio galiojimo audito žurnalų", + "No expired search trails found to clear": "Nerasta išvalytinų pasibaigusio galiojimo paieškos žurnalų", + "No extraction data available for this file": "Šiam failui nėra ištraukimo duomenų", + "No facetable fields available. Select a register and schema to see available filters.": "Nėra aspektuojamų laukų. Pasirinkite registrą ir schemą, kad pamatytumėte galimus filtrus.", + "No files found": "Failų nerasta", + "No files have been extracted yet": "Failų dar neištraukta", + "No files selected": "Nepasirinkta jokių failų", + "No files to process": "Nėra apdorotinų failų", + "No files to reindex": "Nėra iš naujo indeksuotinų failų", + "No filters are currently active. This will delete ALL audit trail entries!": "Šiuo metu nėra aktyvių filtrų. Tai pašalins VISUS audito žurnalo įrašus!", + "No groups found. Try a different search.": "Grupių nerasta. Pabandykite kitą paiešką.", + "No label": "Nėra etiketės", + "No labels": "Nėra etikečių", + "No linked objects": "Nėra susietų objektų", + "No log entries found": "Žurnalo įrašų nerasta", + "No logs are available for this configuration.": "Šiai konfigūracijai žurnalų nėra.", + "No logs are available for this source.": "Šiam šaltiniui žurnalų nėra.", + "No logs found": "Žurnalų nerasta", + "No matches": "Nėra atitikmenų", + "No mismatched fields found - SOLR schema is properly configured": "Neatitinkančių laukų nerasta – SOLR schema tinkamai sukonfigūruota", + "No objects are currently selected for deletion.": "Šiuo metu nepasirinkta jokių objektų pašalinimui.", + "No objects are currently selected for permanent deletion.": "Šiuo metu nepasirinkta jokių objektų pašalinimui visam laikui.", + "No objects are currently selected for restoration.": "Šiuo metu nepasirinkta jokių objektų atkūrimui.", + "No objects found": "Objektų nerasta", + "No objects found to vectorize": "Vektorizuotinų objektų nerasta", + "No objects linked to this email": "Su šiuo el. laišku nėra susietų objektų", + "No objects selected": "Nepasirinkta jokių objektų", + "No objects selected for deletion": "Nepasirinktas joks šalintinas objektas", + "No objects selected for restoration": "Nepasirinkta jokių objektų atkūrimui", + "No objects to delete": "Nėra šalintinų objektų", + "No objects to delete for schema {schema}": "Schemai {schema} nėra objektų, kuriuos būtų galima pašalinti", + "No objects were permanently deleted": "Jokie objektai nebuvo pašalinti visam laikui", + "No organisations found": "Organizacijų nerasta", + "No personal data was found for this subject identifier.": "Šiam subjekto identifikatoriui nerasta jokių asmens duomenų.", + "No processing activities yet": "Dar nėra tvarkymo veiklų", + "No properties found": "Savybių nerasta", + "No properties match your filters.": "Jokios savybės neatitinka jūsų filtrų.", + "No purge date set": "Nenustatyta išvalymo data", + "No register data available": "Nėra registro duomenų", + "No register objects reference this file": "Jokie registro objektai nesiremia šiuo failu", + "No registers found": "Registrų nerasta", + "No registers found for this application.": "Šiai programai registrų nerasta.", + "No registers found. Create a register to configure permissions.": "Registrų nerasta. Sukurkite registrą, kad galėtumėte konfigūruoti leidimus.", + "No related cases found for this sender": "Šiam siuntėjui nerasta susijusių bylų", + "No related entities": "Nėra susijusių objektų", + "No relations found": "Ryšių nerasta", + "No request body available": "Nėra užklausos turinio", + "No response body available": "Nėra atsako turinio", + "No saved views yet. Create one in the Search tab!": "Dar nėra įrašytų rodinių. Sukurkite vieną paieškos kortelėje!", + "No saved views yet. create one in the search tab!": "Dar nėra išsaugotų rodinių. Sukurkite vieną paieškos skirtuke!", + "No scan run yet": "Dar nebuvo atlikta jokio nuskaitymo", + "No schema data available": "Nėra schemos duomenų", + "No schema selected for exploration": "Nepasirinkta jokia tyrinėtina schema", + "No schemas configured for mail linking.": "Nėra sukonfigūruotų schemų el. pašto susiejimui.", + "No schemas found": "Schemų nerasta", + "No schemas found for this application.": "Šiai programai schemų nerasta.", + "No search terms data available": "Nėra paieškos terminų duomenų", + "No search trail entries found": "Paieškos žurnalo įrašų nerasta", + "No sources are available.": "Nėra galimų šaltinių.", + "No sources found": "Šaltinių nerasta", + "No synchronizations found": "Sinchronizacijų nerasta", + "No templates found": "Šablonų nerasta", + "No templates have been created yet": "Šablonų dar nesukurta", + "No tools available. Tools can be registered by installed apps.": "Nėra prieinamų įrankių. Įrankius gali registruoti įdiegtos programos.", + "No users in this organisation.": "Šioje organizacijoje nėra vartotojų.", + "No valid magic-mapped register+schema combinations found": "Negaliojančių stebuklingai susietų registro + schemos derinių nerasta", + "No views found. Create views first before configuring vectorization.": "Rodinių nerasta. Prieš konfigūruodami vektorizavimą pirmiausia sukurkite rodinius.", + "No views match your search": "Jokie rodiniai neatitinka jūsų paieškos", + "No webhooks found": "Žiniatinklio kabliukų nerasta", + "No webhooks have been configured yet": "Žiniatinklio kabliukų dar nesukonfigūruota", + "No workflows found in this project. Create workflows in the n8n editor.": "Šiame projekte darbo srautų nerasta. Sukurkite darbo srautus n8n redaktoriuje.", + "None": "Nėra", + "Not Found": "Nerasta", + "Not extracted": "Neištraukta", + "Not helpful": "Nenaudinga", + "Not in use": "Nenaudojama", + "Note:": "Pastaba:", + "Notifications": "Pranešimai", + "Number Constraints": "Skaičių apribojimai", + "Number of Sources": "Šaltinių skaičius", + "Number of chunks to vectorize in one API call (1-100).": "Vienu API iškvietimu vektorizuotinų fragmentų skaičius (1–100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Vienu API iškvietimu vektorizuotinų fragmentų skaičius. Didesnis = greičiau, bet daugiau atminties. Rekomenduojama: 10–50.", + "Number of objects to process in each batch (1-5000).": "Kiekviename pakete apdorotinų objektų skaičius (1–5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Vienu API iškvietimu vektorizuotinų objektų skaičius. Didesnis = greičiau, bet daugiau atminties. Rekomenduojama: 10–50.", + "Numeric Range:": "Skaitinis intervalas:", + "Object": "Objektas", + "Object #{id}": "Objektas #{id}", + "Object Collection": "Objektų kolekcija", + "Object Configuration:": "Objekto konfigūracija:", + "Object Count Prediction": "Objektų skaičiaus prognozė", + "Object Extraction Mode": "Objektų išgavimo režimas", + "Object ID": "Objekto ID", + "Object Management": "Objektų valdymas", + "Object Text Extraction": "Objekto teksto išgavimas", + "Object Vectorization": "Objektų vektorizavimas", + "Object Warmup": "Objektų pradinis įkėlimas", + "Object changes in owned objects": "Objektų pakeitimai nuosavuose objektuose", + "Object collection not configured": "Objektų kolekcija nesukonfigūruota", + "Object metadata incomplete for file upload": "Objekto metaduomenys neišsamūs failo įkėlimui", + "Object not found": "Objektas nerastas", + "Object permanently deleted": "Objektas pašalintas visam laikui", + "Object restored successfully": "Objektas sėkmingai atkurtas", + "Object settings updated successfully": "Objekto nustatymai sėkmingai atnaujinti", + "Object sources": "Objektų šaltiniai", + "Object successfully deleted": "Objektas sėkmingai pašalintas", + "Object successfully modified": "Objektas sėkmingai pakeistas", + "Object unlocked successfully": "Objektas sėkmingai atrakintas", + "Object vectorization configuration saved successfully": "Objektų vektorizavimo konfigūracija sėkmingai įrašyta", + "Object vectorized successfully": "Objektas sėkmingai vektorizuotas", + "Object:": "Objektas:", + "Objects": "Objektai", + "Objects Analyzed": "Išanalizuoti objektai", + "Objects Distribution": "Objektų pasiskirstymas", + "Objects being analyzed": "Analizuojami objektai", + "Objects by Register": "Objektai pagal registrą", + "Objects by Schema": "Objektai pagal schemą", + "Objects deletion completed for {schema}": "Objektų pašalinimas baigtas schemai {schema}", + "Objects deletion completed successfully": "Objektų šalinimas sėkmingai baigtas", + "Objects in schema": "Objektai schemoje", + "Objects successfully deleted": "Objektai sėkmingai pašalinti", + "Objects to Process:": "Apdorotini objektai:", + "Objects to be analyzed": "Analizuotini objektai", + "Objects to be deleted": "Šalintini objektai", + "Objects to be validated": "Patvirtintini objektai", + "Objects will be serialized as JSON text before vectorization": "Objektai prieš vektorizavimą bus serializuoti kaip JSON tekstas", + "Objects will be soft deleted and moved to the": "Objektai bus švelniai pašalinti ir perkelti į", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objektai bus minkštai pašalinti (pažymėti kaip pašalinti, bet išsaugoti duomenų bazėje). Prireikus juos galima vėliau atkurti.", + "Old Value": "Sena reikšmė", + "Oldest Item (days)": "Seniausias elementas (dienomis)", + "Ollama Chat Settings": "Ollama pokalbio nustatymai", + "Ollama Embedding Configuration": "Ollama įterpinių konfigūracija", + "Ollama URL": "Ollama URL", + "Only With Changes": "Tik su pakeitimais", + "Only members of selected groups can access this organisation": "Tik pasirinktų grupių nariai gali pasiekti šią organizaciją", + "Open Nextcloud App Store": "Atverti Nextcloud programų parduotuvę", + "Open OpenRegister sidebar": "Atverti OpenRegister šoninę juostą", + "Open Register Objects": "Open Register objektai", + "Open folder": "Atverti aplanką", + "Open in OpenRegister": "Atverti OpenRegister", + "Open n8n Editor": "Atverti n8n redaktorių", + "OpenAI Chat Settings": "OpenAI pokalbio nustatymai", + "OpenAI Embedding Configuration": "OpenAI įterpinių konfigūracija", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister nustatymai", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operatoriaus apibrėžti skydeliai ir suplanuotos ataskaitos. Kiekvienas skydelis yra pirmos klasės objektas `reports` registre; valdikliai deklaruojami skydelio `widgets` masyve ir atvaizduojami realiu laiku iš agregacijų / GraphQL.", + "Optimizing search performance...": "Optimizuojamas paieškos našumas...", + "Optional URL-friendly identifier": "Neprivalomas URL pritaikytas identifikatorius", + "Optional description for this facet": "Neprivalomas šio aspekto aprašymas", + "Optional end date property": "Neprivaloma pabaigos datos savybė", + "Optional event description template": "Neprivalomas įvykio aprašymo šablonas", + "Optional location property": "Neprivaloma vietos savybė", + "Optional webhook secret for signature verification": "Neprivalomas žiniatinklio kabliuko slaptasis raktas parašui patikrinti", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Neprivaloma: pridėkite užklausos parametrus, kad filtruotumėte nurodytą schemą (pvz., status=active&type=public)", + "Or": "Arba", + "Order": "Tvarka", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Surūšiuotos BCP 47 kalbos žymos. Pirmoji kalba yra registro numatytoji ir lemia Accept-Language atsarginį variantą verčiamoms savybėms.", + "Organisation": "Organizacija", + "Organisation Configuration": "Organizacijos konfigūracija", + "Organisation ID": "Organizacijos ID", + "Organisation Members": "Organizacijos nariai", + "Organisation Statistics": "Organizacijos statistika", + "Organisation created successfully": "Organizacija sėkmingai sukurta", + "Organisation membership changes": "Organizacijos narystės pakeitimai", + "Organisation name is required": "Organizacijos pavadinimas yra privalomas", + "Organisation settings saved successfully": "Organizacijos nustatymai sėkmingai įrašyti", + "Organisation successfully created": "Organizacija sėkmingai sukurta", + "Organisation successfully updated": "Organizacija sėkmingai atnaujinta", + "Organisational measures": "Organizacinės priemonės", + "Organisations": "Organizacijos", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organizacija", + "Organization ID (Optional)": "Organizacijos ID (neprivaloma)", + "Organizations": "Organizacijos", + "Orphaned Items": "Likę be ryšio elementai", + "Other": "Kita", + "Output Mapping": "Išvesties susiejimas", + "Overview": "Apžvalga", + "Owner": "Savininkas", + "Owner:": "Savininkas:", + "PDF": "PDF", + "PII hits": "PII atitikmenys", + "PII rows": "PII eilutės", + "Page {current} of {total}": "Puslapis {current} iš {total}", + "Parallel Mode (Faster, more resource intensive)": "Lygiagretusis režimas (greitesnis, imlesnis ištekliams)", + "Parallel:": "Lygiagretusis:", + "Password": "Slaptažodis", + "Password changes are not supported by your authentication provider.": "Jūsų tapatybės nustatymo tiekėjas nepalaiko slaptažodžio keitimo.", + "Password for SOLR authentication (optional)": "Slaptažodis SOLR tapatybės nustatymui (neprivaloma)", + "Password updated successfully": "Slaptažodis sėkmingai atnaujintas", + "Path": "Kelias", + "Path in repository": "Kelias saugykloje", + "Path where the configuration file will be saved in the repository": "Kelias, kuriuo konfigūracijos failas bus įrašytas saugykloje", + "Path where the register OAS file will be saved in the repository": "Kelias, kuriuo registro OAS failas bus įrašytas saugykloje", + "Pattern (regex)": "Šablonas (regex)", + "Pattern Issue": "Šablono problema", + "Pattern constraint is missing": "Trūksta šablono apribojimo", + "Patterns:": "Šablonai:", + "Pending": "Laukiama", + "Permanent Delete Mode": "Visam laikui šalinimo režimas", + "Permanently Delete": "Pašalinti visam laikui", + "Permanently Delete ({count})": "Pašalinti visam laikui ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Visam laikui pašalinti visus {count} švelniai pašalintus objektus. To negalima atšaukti!", + "Permanently delete already soft-deleted objects": "Visam laikui pašalinti jau minkštai pašalintus objektus", + "Permanently delete this conversation?": "Pašalinti šį pokalbį visam laikui?", + "Permission Matrix": "Leidimų matrica", + "Person": "Asmuo", + "Personal Data": "Asmens duomenys", + "Personal Data Export": "Asmens duomenų eksportas", + "Persons": "Asmenys", + "Phone": "Telefonas", + "Phone Numbers": "Telefono numeriai", + "Photos": "Nuotraukos", + "Please create an agent in the": "Sukurkite agentą", + "Please select a user": "Pasirinkite vartotoją", + "Please select an agent to continue": "Pasirinkite agentą, kad tęstumėte", + "Please select an organisation": "Pasirinkite organizaciją", + "Please select at least one item to import": "Pasirinkite bent vieną elementą importavimui", + "Please select registers or schemas first": "Pirmiausia pasirinkite registrus arba schemas", + "Please select which register and schema to use for the new object": "Pasirinkite, kurį registrą ir schemą naudoti naujam objektui", + "Please try again later.": "Bandykite dar kartą vėliau.", + "Please wait while we fetch your agents.": "Palaukite, kol gausime jūsų agentus.", + "Please wait while we fetch your applications.": "Palaukite, kol gausime jūsų programas.", + "Please wait while we fetch your configurations.": "Palaukite, kol gausime jūsų konfigūracijas.", + "Please wait while we fetch your deleted items.": "Palaukite, kol gausime jūsų pašalintus elementus.", + "Please wait while we fetch your sources.": "Palaukite, kol gausime jūsų šaltinius.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Palaukite, kol visam laikui pašalinsime SOLR kolekciją. Tai gali užtrukti kelias akimirkas.", + "Polls": "Apklausos", + "Popular Search Terms": "Populiarūs paieškos terminai", + "Port": "Prievadas", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Nuostatos išsaugotos", + "Prerequisites": "Būtinosios sąlygos", + "Press Enter to send, Shift+Enter for new line": "Spauskite Enter siųsti, Shift+Enter naujai eilutei", + "Press enter to send, shift+enter for new line": "Paspauskite enter norėdami siųsti, shift+enter naujai eilutei", + "Prettify": "Sutvarkyti", + "Preview erasure": "Peržiūrėti ištrynimą", + "Preview:": "Peržiūra:", + "Previous": "Ankstesnis", + "Private": "Privatus", + "Private Agent (Default)": "Privatus agentas (numatytasis)", + "Private agents": "Privatūs agentai", + "Process Name (optional)": "Proceso pavadinimas (neprivaloma)", + "Processed {indexed} files, {failed} failed": "Apdorota {indexed} failų, {failed} nepavyko", + "Processes chunks in batches with simulated parallelism.": "Apdoroja fragmentus paketais su modeliuojamu lygiagretumu.", + "Processes file chunks sequentially (safest).": "Apdoroja failų fragmentus nuosekliai (saugiausia).", + "Processes objects in chunks with simulated parallelism.": "Apdoroja objektus fragmentais su modeliuojamu lygiagretumu.", + "Processes objects sequentially (safest).": "Apdoroja objektus nuosekliai (saugiausia).", + "Processing": "Apdorojama", + "Processing Limits": "Apdorojimo ribos", + "Processing...": "Apdorojama...", + "Project initialized successfully": "Projektas sėkmingai inicijuotas", + "Project not found. Please initialize first.": "Projektas nerastas. Pirmiausia inicijuokite.", + "Properties": "Savybės", + "Property Behaviors": "Savybių elgsenos", + "Property Configuration:": "Savybės konfigūracija:", + "Property Filters": "Savybių filtrai", + "Property Title": "Savybės pavadinimas", + "Property Type": "Savybės tipas", + "Property can be improved": "Savybę galima patobulinti", + "Property could not be added": "Nepavyko pridėti savybės", + "Property could not be updated": "Nepavyko atnaujinti savybės", + "Property name of inversed relation": "Atvirkštinio ryšio savybės pavadinimas", + "Property successfully added": "Savybė sėkmingai pridėta", + "Property successfully updated": "Savybė sėkmingai atnaujinta", + "Protected": "Apsaugota", + "Provider": "Teikėjas", + "Provider is required for testing": "Bandymui reikia teikėjo", + "Public": "Viešas", + "Public View": "Viešas rodinys", + "Public views can be accessed by anyone in the system": "Viešus rodinius gali pasiekti bet kas sistemoje", + "Publish": "Publikuoti", + "Publish Configuration to GitHub": "Paskelbti konfigūraciją į GitHub", + "Publish OAS": "Paskelbti OAS", + "Publish Objects": "Publikuoti objektus", + "Publish Register OAS to GitHub": "Publikuoti registro OAS į GitHub", + "Publish all objects in this schema": "Publikuoti visus šios schemos objektus", + "Publish objects to make them publicly available": "Publikuoti objektus, kad jie būtų viešai prieinami", + "Published": "Publikuota", + "Published:": "Publikuota:", + "Publishing...": "Publikuojama...", + "Purge": "Išvalyti", + "Purge Date": "Išvalymo data", + "Purge {count} object from database": "Pašalinti {count} objektą iš duomenų bazės", + "Purge {count} objects from database": "Pašalinti {count} objektų iš duomenų bazės", + "Purpose limitation *": "Doelbinding *", + "Push Notifications": "Tiesioginiai pranešimai", + "Query Complexity Distribution": "Užklausų sudėtingumo pasiskirstymas", + "Query Help": "Užklausų žinynas", + "Query parameter is required": "Reikia užklausos parametro", + "Quota": "Kvota", + "RAG": "RAG", + "RAG Configuration": "RAG konfigūracija", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC nustatymai sėkmingai atnaujinti", + "Rate limits cleared successfully": "Dažnio apribojimai sėkmingai išvalyti", + "Raw Changes Data": "Neapdoroti pakeitimų duomenys", + "Re-vectorize on object update": "Iš naujo vektorizuoti objektą atnaujinus", + "Read": "Skaityti", + "Real-time push notification status via notify_push": "Tiesioginių pranešimų būsena per notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Tiesioginiai pranešimai aktyvūs. Prisijungę klientai gauna momentinius atnaujinimus, kai objektai sukuriami, atnaujinami arba pašalinami.", + "Realtime push active": "Tiesioginiai pranešimai aktyvūs", + "Realtime push not available — the notify_push app is not installed": "Tiesioginiai pranešimai negalimi — programa notify_push neįdiegta", + "Reason": "Priežastis", + "Reason (optional)": "Priežastis (neprivaloma)", + "Rebase operation completed successfully": "Perbazavimo operacija sėkmingai baigta", + "Rebase operation failed: {error}": "Perbazavimo operacija nepavyko: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Rekomendacijos:", + "Recommended Type:": "Rekomenduojamas tipas:", + "Recommended: 5 sources": "Rekomenduojama: 5 šaltiniai", + "Refresh": "Įkelti iš naujo", + "Refresh Data": "Įkelti duomenis iš naujo", + "Refresh Stats": "Įkelti statistiką iš naujo", + "Refresh Workflows": "Įkelti darbo srautus iš naujo", + "Refresh dashboard": "Įkelti skydelį iš naujo", + "Refresh database info": "Įkelti duomenų bazės informaciją iš naujo", + "Register": "Registras", + "Register #{id}": "Registras #{id}", + "Register / Schema": "Registras / Schema", + "Register ID": "Registro ID", + "Register Objects": "Registro objektai", + "Register Overview": "Registro apžvalga", + "Register Statistics": "Registro statistika", + "Register Totals": "Registro sumos", + "Register and schema are required": "Registras ir schema yra privalomi", + "Register annotation": "Registro anotacija", + "Register depublished successfully": "Registras sėkmingai nupublikuotas", + "Register not found": "Registras nerastas", + "Register objects": "Registro objektai", + "Register objects deletion completed successfully": "Registro objektų šalinimas sėkmingai baigtas", + "Register published successfully": "Registras sėkmingai publikuotas", + "Register/Schema Usage": "Registro / schemos naudojimas", + "Register:": "Registras:", + "Registers": "Registrai", + "Reindex": "Indeksuoti iš naujo", + "Reindex completed": "Indeksavimas iš naujo baigtas", + "Reindex completed successfully": "Indeksavimas iš naujo sėkmingai baigtas", + "Reindex completed! Processed {count} objects in {duration}s": "Pakartotinis indeksavimas baigtas! Apdorota {count} objektų per {duration} s", + "Reindex failed": "Indeksavimas iš naujo nepavyko", + "Reindex failed: %s": "Indeksavimas iš naujo nepavyko: %s", + "Reject": "Atmesti", + "Rejected": "Atmesta", + "Related Cases": "Susijusios bylos", + "Relations": "Ryšiai", + "Remote Version": "Nuotolinė versija", + "Remove": "Šalinti", + "Remove Deck card": "Pašalinti Deck kortelę", + "Remove avatar": "Pašalinti avatarą", + "Remove contact": "Pašalinti kontaktą", + "Remove filter": "Šalinti filtrą", + "Remove from favorites": "Šalinti iš mėgstamų", + "Remove from list": "Pašalinti iš sąrašo", + "Remove group": "Šalinti grupę", + "Remove link": "Pašalinti nuorodą", + "Remove link to {name}": "Pašalinti nuorodą į {name}", + "Remove link to {name}?": "Pašalinti nuorodą į {name}?", + "Remove link to {title}": "Pašalinti nuorodą į {title}", + "Remove oneOf entry": "Pašalinti oneOf įrašą", + "Remove property after write-back": "Pašalinti savybę po įrašymo atgal", + "Remove {title}": "Pašalinti {title}", + "Remove {userId} from this organisation?": "Pašalinti {userId} iš šios organizacijos?", + "Removed": "Pašalinta", + "Removed from favorites": "Pašalinta iš mėgstamų", + "Removing schema {schema}...": "Šalinama schema {schema}...", + "Rename Conversation": "Pervadinti pokalbį", + "Rename conversation": "Pervadinti pokalbį", + "Replicas": "Replikos", + "Reports": "Ataskaitos", + "Repository": "Saugykla", + "Request Body": "Užklausos turinys", + "Request Data": "Užklausos duomenys", + "Request Details": "Užklausos išsami informacija", + "Request Quota (per day)": "Užklausų kvota (per dieną)", + "Request account deactivation": "Prašyti paskyros deaktyvavimo", + "Request account deactivation. This will notify administrators for review.": "Prašyti paskyros deaktyvavimo. Apie tai bus pranešta administratoriams peržiūrai.", + "Request body must be JSON with a \"query\" field": "Užklausos turinys turi būti JSON su lauku „query“", + "Request timeout in seconds": "Užklausos skirtasis laikas sekundėmis", + "Requested at": "Prašymo data", + "Required": "Privaloma", + "Required Token Scopes": "Privalomos žetono sritys", + "Required field": "Privalomas laukas", + "Required status is inconsistent": "Privalomumo būsena nenuosekli", + "Requires Dolphin AI for OCR text extraction": "Reikalauja Dolphin AI OCR teksto išgavimui", + "Rerun Search": "Pakartoti paiešką", + "Rerunning search: {searchTerm}": "Iš naujo vykdoma paieška: {searchTerm}", + "Reset Changes": "Atstatyti pakeitimus", + "Reset Filters": "Atstatyti filtrus", + "Resource Quotas": "Išteklių kvotos", + "Response Body": "Atsako turinys", + "Response Details": "Atsako išsami informacija", + "Response Time:": "Atsako laikas:", + "Restore": "Atkurti", + "Restore conversation": "Atkurti pokalbį", + "Restore or permanently delete items": "Atkurti arba visam laikui pašalinti elementus", + "Restore {count} object": "Atkurti {count} objektą", + "Restore {count} objects": "Atkurti {count} objektų", + "Result Count Range": "Rezultatų skaičiaus intervalas", + "Results": "Rezultatai", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 trukmė, pvz., P10Y, P30D)", + "Retention settings updated successfully": "Saugojimo nustatymai sėkmingai atnaujinti", + "Retrieve all objects for this schema": "Gauti visus šios schemos objektus", + "Retry": "Kartoti", + "Retry Failed Extractions": "Kartoti nepavykusius ištraukimus", + "Retry Policy": "Pakartojimų politika", + "Retry completed": "Pakartojimas baigtas", + "Retry upload": "Bandyti įkelti dar kartą", + "Retrying...": "Kartojama...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Peržiūrėkite pasirinktus objektus žemiau. Galite pašalinti bet kuriuos objektus, kurių nenorite ištrinti, spustelėdami šalinimo mygtuką.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Peržiūrėkite pasirinktus objektus žemiau. Galite pašalinti bet kuriuos objektus, kurių nenorite įtraukti.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Peržiūrėkite pasirinktus objektus žemiau. Galite pašalinti bet kuriuos objektus, kurių nenorite visam laikui ištrinti, spustelėdami šalinimo mygtuką. Šio veiksmo atšaukti negalima.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Peržiūrėkite pasirinktus objektus žemiau. Galite pašalinti bet kuriuos objektus, kurių nenorite atkurti, spustelėdami šalinimo mygtuką. Objektai bus atkurti į pradinę vietą.", + "Revoke": "Atšaukti", + "Right": "Dešinė", + "Risk Level": "Rizikos lygis", + "Risk level": "Rizikos lygis", + "Roles and Permissions": "Vaidmenys ir leidimai", + "Roles updated successfully": "Rolės sėkmingai atnaujintos", + "Run compliance scan": "Vykdyti atitikties patikrą", + "SOLR Connection Settings": "SOLR prisijungimo nustatymai", + "SOLR Index Warmup": "SOLR indekso įšildymas", + "SOLR Version:": "SOLR versija:", + "SOLR actions menu": "SOLR veiksmų meniu", + "SOLR base path (usually /solr)": "SOLR bazinis kelias (paprastai /solr)", + "SOLR collection deleted successfully": "SOLR kolekcija sėkmingai ištrinta", + "SOLR connection test failed: {error}": "SOLR ryšio testas nepavyko: {error}", + "SOLR connection test successful!": "SOLR ryšio testas sėkmingas!", + "SOLR index warmup completed successfully": "SOLR indekso įšildymas sėkmingai baigtas", + "SOLR is disabled": "SOLR išjungtas", + "SOLR is not available or not configured": "SOLR negalimas arba nesukonfigūruotas", + "SOLR logging disabled": "SOLR žurnalinimas išjungtas", + "SOLR logging enabled": "SOLR žurnalinimas įjungtas", + "SOLR search disabled": "SOLR paieška išjungta", + "SOLR search enabled": "SOLR paieška įjungta", + "SOLR server hostname or IP address": "SOLR serverio pagrindinio kompiuterio vardas arba IP adresas", + "SOLR server port number (optional, defaults to 8983)": "SOLR serverio prievado numeris (neprivaloma, numatytasis 8983)", + "SOLR settings updated successfully": "SOLR nustatymai sėkmingai atnaujinti", + "SOLR setup completed successfully": "SOLR sąranka sėkmingai baigta", + "SOLR setup completed successfully!": "SOLR sąranka sėkmingai baigta!", + "SOLR setup error: %s": "SOLR sąrankos klaida: %s", + "SOLR setup failed": "SOLR sąranka nepavyko", + "SOLR setup failed - check logs": "SOLR sąranka nepavyko – patikrinkite žurnalus", + "SOLR setup failed. Please check the configuration and try again.": "SOLR sąranka nepavyko. Patikrinkite konfigūraciją ir bandykite dar kartą.", + "SOLR setup failed: {error}": "SOLR sąranka nepavyko: {error}", + "SOLR warmup failed: {error}": "SOLR įšildymas nepavyko: {error}", + "Sample Values:": "Pavyzdinės reikšmės:", + "Save": "Įrašyti", + "Save Configuration": "Įrašyti konfigūraciją", + "Save Connection Settings": "Įrašyti prisijungimo nustatymus", + "Save Roles": "Įrašyti roles", + "Save Settings": "Įrašyti nustatymus", + "Save changes": "Įrašyti pakeitimus", + "Save current search as view": "Įrašyti dabartinę paiešką kaip rodinį", + "Save labels": "Išsaugoti etiketes", + "Save tags for {name}": "Išsaugoti žymas {name}", + "Save the organisation first to manage users.": "Pirmiausia išsaugokite organizaciją, kad galėtumėte valdyti naudotojus.", + "Saved Views": "Įrašyti rodiniai", + "Saving...": "Įrašoma...", + "Scheduled": "Suplanuota", + "Schema": "Schema", + "Schema ID": "Schemos ID", + "Schema Name": "Schemos pavadinimas", + "Schema Statistics": "Schemos statistika", + "Schema annotation": "Schemos anotacija", + "Schema configuration updated successfully for {schema}": "Schemos {schema} konfigūracija sėkmingai atnaujinta", + "Schema depublished successfully": "Schema sėkmingai nupublikuota", + "Schema objects deletion completed successfully": "Schemos objektų šalinimas sėkmingai baigtas", + "Schema published successfully": "Schema sėkmingai publikuota", + "Schema reference ($ref)": "Schemos nuoroda ($ref)", + "Schema successfully created": "Schema sėkmingai sukurta", + "Schema successfully updated": "Schema sėkmingai atnaujinta", + "Schema {schema} removed successfully": "Schema {schema} sėkmingai pašalinta", + "Schema:": "Schema:", + "Schemas": "Schemos", + "Scheme": "Schema", + "Search": "Paieška", + "Search / Views": "Paieška / rodiniai", + "Search / views": "Paieška / rodiniai", + "Search Activity": "Paieškos veikla", + "Search Analytics": "Paieškos analitika", + "Search Configuration": "Paieškos konfigūracija", + "Search Entities": "Ieškoti esybių", + "Search Files": "Ieškoti failų", + "Search GitHub": "Ieškoti GitHub", + "Search GitLab": "Ieškoti GitLab", + "Search Method": "Paieškos metodas", + "Search Mode": "Paieškos režimas", + "Search Query": "Paieškos užklausa", + "Search Results": "Paieškos rezultatai", + "Search Term": "Paieškos terminas", + "Search Trail Details": "Paieškos žurnalo išsami informacija", + "Search Trail Management": "Paieškos žurnalų valdymas", + "Search Trail Statistics": "Paieškos žurnalų statistika", + "Search Trails": "Paieškos žurnalai", + "Search Views": "Ieškoti rodinių", + "Search Webhooks": "Ieškoti žiniatinklio kabliukų", + "Search agents...": "Ieškoti agentų...", + "Search applications...": "Ieškoti programų...", + "Search by file name or path": "Ieškoti pagal failo pavadinimą arba kelią", + "Search by name or URL": "Ieškoti pagal vardą arba URL", + "Search by title or UUID...": "Ieškoti pagal pavadinimą arba UUID...", + "Search by value": "Ieškoti pagal reikšmę", + "Search configurations": "Ieškoti konfigūracijų", + "Search data sources...": "Ieškoti duomenų šaltinių...", + "Search groups...": "Ieškoti grupių...", + "Search in Database Objects": "Ieškoti duomenų bazės objektuose", + "Search in Files": "Ieškoti failuose", + "Search in Objects": "Ieškoti objektuose", + "Search objects": "Ieškoti objektų", + "Search objects...": "Ieškoti objektų...", + "Search property names...": "Ieškoti savybių pavadinimų...", + "Search registers...": "Ieškoti registrų...", + "Search schemas...": "Ieškoti schemų...", + "Search trail deletion not implemented yet": "Paieškos žurnalo šalinimas dar neįgyvendintas", + "Search views...": "Ieškoti rodinių...", + "Search {name}...": "Ieškoti {name}...", + "Searching...": "Ieškoma...", + "Secret": "Slaptasis raktas", + "Secret key for HMAC signature generation (optional)": "Slaptasis raktas HMAC parašui generuoti (neprivaloma)", + "Security": "Sauga", + "See organisations as a table": "Peržiūrėti organizacijas kaip lentelę", + "See organisations as cards": "Peržiūrėti organizacijas kaip korteles", + "Select AI Agent": "Pasirinkti DI agentą", + "Select Active Organisation": "Pasirinkti aktyvią organizaciją", + "Select All": "Pasirinkti visus", + "Select ConfigSet": "Pasirinkti ConfigSet", + "Select File Types to Vectorize:": "Pasirinkite vektorizuotinus failų tipus:", + "Select HTTP method": "Pasirinkite HTTP metodą", + "Select Register and Schema": "Pasirinkite registrą ir schemą", + "Select Views to Vectorize:": "Pasirinkite vektorizuotinus rodinius:", + "Select a Nextcloud group to add": "Pasirinkite pridėtiną Nextcloud grupę", + "Select a branch": "Pasirinkite šaką", + "Select a date property": "Pasirinkti datos savybę", + "Select a model or type a custom model name": "Pasirinkite modelį arba įveskite pasirinktinį modelio pavadinimą", + "Select a register": "Pasirinkti registrą", + "Select a register...": "Pasirinkti registrą...", + "Select a repository": "Pasirinkite saugyklą", + "Select a repository you have write access to": "Pasirinkite saugyklą, į kurią turite rašymo prieigą", + "Select a schema": "Pasirinkti schemą", + "Select a schema...": "Pasirinkti schemą...", + "Select a user": "Pasirinkti naudotoją", + "Select agent type": "Pasirinkti agento tipą", + "Select all entities": "Pasirinkti visus subjektus", + "Select an AI Agent": "Pasirinkite DI agentą", + "Select an AI agent to begin chatting with your data.": "Pasirinkite DI agentą, kad pradėtumėte pokalbį su savo duomenimis.", + "Select an email to see linked objects": "Pasirinkite el. laišką, kad pamatytumėte susietus objektus", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Pasirinkite organizaciją ir naudotoją, kad pridėtumėte jį kaip narį. Ieškokite organizacijų pagal pavadinimą.", + "Select backend": "Pasirinkite vidinę sąsają", + "Select chat model": "Pasirinkite pokalbio modelį", + "Select collection for files": "Pasirinkite kolekciją failams", + "Select collection for objects": "Pasirinkite kolekciją objektams", + "Select configuration type...": "Pasirinkti konfigūracijos tipą...", + "Select default organisation": "Pasirinkite numatytąją organizaciją", + "Select entity {value}": "Pasirinkti subjektą {value}", + "Select event to listen to...": "Pasirinkite klausomą įvykį...", + "Select fields to display": "Pasirinkti rodomus laukus", + "Select files with the correct extension": "Pasirinkti failus su teisingu plėtiniu", + "Select group": "Pasirinkti grupę", + "Select groups (optional)": "Pasirinkti grupes (neprivaloma)", + "Select groups to notify...": "Pasirinkti grupes, kurioms pranešti...", + "Select groups with access to this agent": "Pasirinkti grupes, turinčias prieigą prie šio agento", + "Select groups...": "Pasirinkti grupes...", + "Select model": "Pasirinkite modelį", + "Select one or more registers": "Pasirinkti vieną ar daugiau registrų", + "Select one or more schemas": "Pasirinkite vieną ar daugiau schemų", + "Select options...": "Pasirinkite parinktis...", + "Select or create labels, or select \"No label\" to add files": "Pasirinkite arba sukurkite etiketes arba pasirinkite „Be etiketės“, kad pridėtumėte failus", + "Select owner application (optional)...": "Pasirinkite savininko programą (neprivaloma)...", + "Select period": "Pasirinkite laikotarpį", + "Select property to send as payload": "Pasirinkite savybę, siunčiamą kaip naudingoji apkrova", + "Select provider": "Pasirinkite teikėją", + "Select registers and schemas to save a view": "Pasirinkite registrus ir schemas, kad įrašytumėte rodinį", + "Select retry policy": "Pasirinkite pakartojimų politiką", + "Select role": "Pasirinkite vaidmenį", + "Select schemas (empty = all schemas)": "Pasirinkite schemas (tuščia = visos schemos)", + "Select schemas to warm up": "Pasirinkite schemas pašildymui", + "Select search mode": "Pasirinkite paieškos režimą", + "Select source type...": "Pasirinkite šaltinio tipą...", + "Select target type": "Pasirinkite tikslo tipą", + "Select the branch to publish to": "Pasirinkite šaką, į kurią publikuoti", + "Select the event this webhook should listen to": "Pasirinkite įvykį, kurio šis žiniatinklio kabliukas turėtų klausytis", + "Select views to filter data (optional)": "Pasirinkite rodinius duomenims filtruoti (neprivaloma)", + "Select views to limit which data the agent can access": "Pasirinkite rodinius, kad apribotumėte, prie kokių duomenų agentas gali prieiti", + "Select views to vectorize:": "Pasirinkite vektorizuotinus rodinius:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Pasirinkite, kurios Nextcloud grupės galimos šiai organizacijai. Šiose grupėse esantys naudotojai turės prieigą prie organizacijos išteklių.", + "Select which data views the AI can search": "Pasirinkite, kuriuose duomenų rodiniuose DI gali ieškoti", + "Select which property from the event should be used as the webhook payload data": "Pasirinkite, kuri įvykio savybė turėtų būti naudojama kaip žiniatinklio kabliuko naudingosios apkrovos duomenys", + "Select which tools the AI can use to perform actions": "Pasirinkite, kuriuos įrankius DI gali naudoti veiksmams atlikti", + "Select {fieldName} values": "Pasirinkite {fieldName} reikšmes", + "Selected": "Pasirinkta", + "Selected Groups": "Pasirinktos grupės", + "Selected Objects ({count})": "Pasirinkti objektai ({count})", + "Selected audit trails deleted successfully": "Pasirinkti audito žurnalai sėkmingai pašalinti", + "Selected search trails deleted successfully": "Pasirinkti paieškos žurnalai sėkmingai pašalinti", + "Selected users can access this view": "Pasirinkti naudotojai gali pasiekti šį rodinį", + "Send additional feedback": "Siųsti papildomą atsiliepimą", + "Send as CloudEvent": "Siųsti kaip CloudEvent", + "Send as cloudevent": "Siųsti kaip cloudevent", + "Sensitive PII": "Jautrūs asmens duomenys (PII)", + "Serial Mode (Safer, slower)": "Nuoseklusis režimas (saugesnis, lėtesnis)", + "Serial:": "Nuoseklusis:", + "Server Configuration": "Serverio konfigūracija", + "Server Information": "Serverio informacija", + "Set as Active": "Nustatyti kaip aktyvų", + "Set as active organisation": "Nustatyti kaip aktyvią organizaciją", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Nustatykite API naudojimo ir žetonų sunaudojimo ribas. Naudokite 0 neribotiems ištekliams.", + "Settings": "Nustatymai", + "Settings updated successfully": "Nustatymai sėkmingai atnaujinti", + "Shards": "Fragmentai", + "Share": "Bendrinti", + "Share with Groups": "Bendrinti su grupėmis", + "Share with Users": "Bendrinti su naudotojais", + "Show": "Rodyti", + "Show Filters": "Rodyti filtrus", + "Show less": "Rodyti mažiau", + "Show only entries with changes": "Rodyti tik įrašus su pakeitimais", + "Showing {count} dashboard(s)": "Rodoma {count} skydelių", + "Showing {count} processing activities": "Rodoma {count} apdorojimo veiklų", + "Showing {showing} of {total} agents": "Rodoma {showing} iš {total} agentų", + "Showing {showing} of {total} applications": "Rodoma {showing} iš {total} programų", + "Showing {showing} of {total} audit trail entries": "Rodoma {showing} iš {total} audito sekos įrašų", + "Showing {showing} of {total} configurations": "Rodoma {showing} iš {total} konfigūracijų", + "Showing {showing} of {total} deleted items": "Rodoma {showing} iš {total} ištrintų elementų", + "Showing {showing} of {total} entities": "Rodoma {showing} iš {total} esybių", + "Showing {showing} of {total} files": "Rodoma {showing} iš {total} failų", + "Showing {showing} of {total} log entries": "Rodoma {showing} iš {total} žurnalo įrašų", + "Showing {showing} of {total} search trail entries": "Rodoma {showing} iš {total} paieškos sekos įrašų", + "Showing {showing} of {total} sources": "Rodoma {showing} iš {total} šaltinių", + "Showing {showing} of {total} templates": "Rodoma {showing} iš {total} šablonų", + "Showing {showing} of {total} webhooks": "Rodoma {showing} iš {total} webhook", + "Showing {shown} of {total} organisations": "Rodoma {shown} iš {total} organizacijų", + "Simple": "Paprasta", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Paprastos užklausos: pagrindinės teksto paieškos su minimaliais parametrais (pvz., vienas paieškos terminas, be išplėstinių filtrų)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Paprastos užklausos: pagrindinės teksto paieškos su minimaliais parametrais (pvz., vienas paieškos terminas, be išplėstinių filtrų)", + "Size": "Dydis", + "Slug": "Slug", + "Soft Delete Mode": "Minkšto šalinimo režimas", + "Soft Deleted Items": "Minkštai pašalinti elementai", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Švelniai ištrinti visus šios schemos objektus ({active} aktyvūs, {deleted} jau ištrinti)", + "SolrCloud mode enabled": "SolrCloud režimas įjungtas", + "Something went wrong while adding files": "Pridedant failus kažkas nepavyko", + "Source": "Šaltinis", + "Source successfully updated": "Šaltinis sėkmingai atnaujintas", + "Sources": "Šaltiniai", + "Sources:": "Šaltiniai:", + "Special Rights": "Specialios teisės", + "Standalone SOLR mode": "Atskirasis SOLR režimas", + "Start Conversation": "Pradėti pokalbį", + "Start Date Field": "Pradžios datos laukas", + "Start Vectorization": "Pradėti vektorizavimą", + "Start a conversation": "Pradėti pokalbį", + "Starting names cache warmup...": "Pradedamas vardų podėlio pradinis įkėlimas...", + "Starting validation for {schema}...": "Pradedamas {schema} patvirtinimas...", + "Starting {action} for {schema}...": "Pradedamas {action} schemai {schema}...", + "Starting...": "Pradedama...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika ir metrikos", + "Statistics refreshed": "Statistika atnaujinta", + "Stats calculation not yet implemented": "Statistikos skaičiavimas dar neįgyvendintas", + "Status": "Būsena", + "Status Code": "Būsenos kodas", + "Step": "Žingsnis", + "Storage": "Saugykla", + "Storage Quota (MB)": "Saugyklos kvota (MB)", + "String Constraints": "Eilučių apribojimai", + "Subject identifier (email, BSN, name, etc.)": "Subjekto identifikatorius (el. paštas, BSN, vardas ir kt.)", + "Subscribe": "Prenumeruoti", + "Subscribed": "Prenumeruota", + "Success": "Sėkmė", + "Success Rate": "Sėkmės dažnis", + "Success Status": "Sėkmės būsena", + "Successful": "Sėkminga", + "Successfully added user to organisation: {name}": "Naudotojas sėkmingai pridėtas prie organizacijos: {name}", + "Successfully cleared {count} audit trails": "Sėkmingai išvalyta {count} audito sekų", + "Successfully cleared {count} blob storage objects": "Sėkmingai išvalyta {count} blob saugyklos objektų", + "Successfully cleared {count} search trails": "Sėkmingai išvalyta {count} paieškos sekų", + "Successfully created {count} SOLR fields": "Sėkmingai sukurta {count} SOLR laukų", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Sėkmingai ištrinta {count} įterpinių. Prašome iš naujo vektorizuoti savo duomenis.", + "Successfully deleted {count} objects for {schema}": "Sėkmingai ištrinta {count} objektų schemai {schema}", + "Successfully fixed {count} SOLR field configurations": "Sėkmingai pataisyta {count} SOLR laukų konfigūracijų", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Sėkmingai importuota: {registers} registrų, {schemas} schemų, {objects} objektų", + "Successfully joined organisation": "Sėkmingai prisijungta prie organizacijos", + "Successfully permanently deleted {count} objects": "Sėkmingai visam laikui ištrinta {count} objektų", + "Successfully processed {count} files!": "Sėkmingai apdorota {count} failų!", + "Successfully restored {count} object": "Sėkmingai atkurtas {count} objektas", + "Successfully restored {count} objects": "Sėkmingai atkurta {count} objektų", + "Successfully saved configuration for {count} facets!": "Sėkmingai išsaugota {count} aspektų konfigūracija!", + "Supported File Types": "Palaikomi failų tipai", + "Switch Active Organisation": "Perjungti aktyvią organizaciją", + "Switch Organisation": "Perjungti organizaciją", + "Sync Table": "Sinchronizuoti lentelę", + "Synchronization Settings": "Sinchronizavimo nustatymai", + "Syncing magic table for {schema}...": "Sinchronizuojama magiška lentelė schemai {schema}...", + "Syncs": "Sinchronizavimai", + "System Default": "Sistemos numatytasis", + "System Prompt": "Sistemos užklausa", + "System announcements": "Sistemos pranešimai", + "Table": "Lentelė", + "Target ID": "Tikslo ID", + "Target Type*": "Tikslo tipas*", + "Technical Description": "Techninis aprašymas", + "Technical Details": "Techninė informacija", + "Technical description for developers and administrators": "Techninis aprašymas, skirtas programuotojams ir administratoriams", + "Technical measures": "Techninės priemonės", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatūra", + "Templates": "Šablonai", + "Test": "Bandyti", + "Test Chat": "Bandyti pokalbį", + "Test Connection": "Bandyti prisijungimą", + "Test Embedding": "Bandyti įterpinį", + "Test endpoint executed successfully": "Bandomasis galinis taškas sėkmingai įvykdytas", + "Test webhook delivered successfully": "Bandomasis žiniatinklio kabliukas sėkmingai pristatytas", + "Test webhook delivery failed": "Bandomojo žiniatinklio kabliuko pristatymas nepavyko", + "Test webhook sent successfully": "Bandomasis žiniatinklio kabliukas sėkmingai išsiųstas", + "Testing...": "Bandoma...", + "Text Chunk #{id}": "Teksto fragmentas #{id}", + "Text Extraction": "Teksto ištraukimas", + "Text Extraction Engine": "Teksto ištraukimo variklis", + "Text chunks": "Teksto fragmentai", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Teksto fragmentai generuojami failų ištraukimo metu ir saugomi duomenų bazėje. Vektorizavimas nuskaito šiuos iš anksto suskaidytus failus ir konvertuoja juos į įterpinius.", + "Text extracted successfully": "Tekstas sėkmingai ištrauktas", + "Text extraction disabled": "Teksto ištraukimas išjungtas", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloud Calendar programa neįdiegta arba neįjungta šiame serveryje.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloud Contacts programa neįdiegta arba neįjungta šiame serveryje.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloud Deck programa neįdiegta arba neįjungta šiame serveryje.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloud Mail programa neįdiegta arba neįjungta šiame serveryje.", + "The URL where webhook events will be sent": "URL, į kurį bus siunčiami žiniatinklio kabliukų įvykiai", + "The requested conversation does not exist": "Prašomas pokalbis neegzistuoja", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Prašomo skydelio nepavyko įkelti. Patikrinkite URL arba pasirinkite kitą skydelį iš sąrašo.", + "The saved view and all its search configuration": "Įrašytas rodinys ir visa jo paieškos konfigūracija", + "There are no audit trail entries matching your current filters.": "Nėra audito žurnalo įrašų, atitinkančių jūsų dabartinius filtrus.", + "There are no deleted items matching your current filters.": "Nėra pašalintų elementų, atitinkančių jūsų dabartinius filtrus.", + "There are no search trail entries matching your current filters.": "Nėra paieškos žurnalo įrašų, atitinkančių jūsų dabartinius filtrus.", + "There are no webhook log entries matching your filters.": "Nėra žiniatinklio kabliukų žurnalo įrašų, atitinkančių jūsų filtrus.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Jie bus saugomi pagal jų schemos sukonfigūruotą saugojimo laikotarpį ir automatiškai visam laikui ištrinti, kai saugojimo laikotarpis pasibaigs. Saugojimo laikotarpis konfigūruojamas kiekvienai schemai ir randamas schemos nustatymuose.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Jie bus saugomi pagal jų schemos sukonfigūruotą saugojimo laikotarpį ir vėliau visam laikui pašalinti.", + "This action cannot be undone.": "Šio veiksmo atšaukti negalima.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Šio veiksmo atšaukti negalima. Įsitikinkite, kad jokia kolekcija nenaudoja šio ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Šio veiksmo negalima atšaukti. Įsitikinkite, kad jokia kolekcija nenaudoja šio ConfigSet.", + "This action will submit a deactivation request to your administrators.": "Šis veiksmas pateiks deaktyvavimo užklausą jūsų administratoriams.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Šis agentas neturi konfigūruojamų rodinių ar įrankių, tačiau vis tiek galite koreguoti žemiau esančius RAG nustatymus.", + "This analysis may take some time": "Ši analizė gali užtrukti", + "This audit trail entry does not contain any change information.": "Šiame audito žurnalo įraše nėra jokios informacijos apie pakeitimus.", + "This configuration has no data defined.": "Šiai konfigūracijai neapibrėžta jokių duomenų.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Šis galinis taškas nebenaudojamas. Vietoj jo naudokite fragmentais pagrįstus galinius taškus.", + "This entity has no relations to objects or files": "Ši esybė neturi ryšių su objektais ar failais", + "This is a test webhook from OpenRegister": "Tai bandomasis žiniatinklio kabliukas iš OpenRegister", + "This is not valid JSON": "Tai netinkamas JSON", + "This key already exists on this schema": "Šis raktas jau yra šioje schemoje", + "This schema must use Magic Table configuration to sync": "Norint sinchronizuoti, ši schema turi naudoti stebuklingosios lentelės konfigūraciją", + "This schema must use magic table configuration to sync": "Ši schema, norint sinchronizuoti, turi naudoti magiškos lentelės konfigūraciją", + "This source has no associated registers.": "Šis šaltinis neturi susietų registrų.", + "This token will only be shown once. Copy it now.": "Šis žetonas bus parodytas tik vieną kartą. Nukopijuokite jį dabar.", + "This will copy the _default ConfigSet with the new name": "Tai nukopijuos _default ConfigSet su nauju pavadinimu", + "This will copy the _default configset with the new name": "Tai nukopijuos _default ConfigSet su nauju pavadinimu", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Tai visam laikui ištrins VISUS įterpinius (vektorius) iš duomenų bazės. Jums reikės iš naujo vektorizuoti visus objektus ir failus. Šio veiksmo negalima atšaukti.\n\nAr tikrai norite tęsti?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Tai visam laikui pašalins VISUS įterpinius (vektorius) iš duomenų bazės. Jums reikės iš naujo vektorizuoti visus objektus ir failus. Šio veiksmo atšaukti negalima.\\n\\nAr tikrai norite tęsti?", + "This will permanently delete:": "Tai visam laikui pašalins:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Tai visam laikui pašalins {count} jau švelniai ištrintų objektų iš duomenų bazės. Šio veiksmo negalima atšaukti.", + "Timeout (seconds)": "Skirtasis laikas (sekundėmis)", + "Timestamp": "Laiko žyma", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Patarimas: įjunkite tik tuos rodinius, kuriems reikia semantinės paieškos, kad sumažintumėte įterpinių sąnaudas. Paprastoms paieškos lentelėms vektorizavimo retai prireikia.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Patarimas: įjunkite tik tuos rodinius, kuriems reikia semantinės paieškos, kad sumažintumėte įterpinių sąnaudas. Paprastoms paieškos lentelėms vektorizavimo prireikia retai.", + "Title": "Pavadinimas", + "Title *": "Pavadinimas *", + "Title Template": "Pavadinimo šablonas", + "Title*": "Pavadinimas*", + "To (e.g. -1 year or 2025-12-31)": "Iki (pvz., -1 year arba 2025-12-31)", + "To Date": "Iki datos", + "To add files larger than or equal to 512MB, go to the": "Norėdami pridėti failus, kurių dydis 512MB ar didesnis, eikite į", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Norėdami koreguoti fragmentų dydį ar strategiją, eikite į Failų konfigūraciją → Apdorojimo ribos.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Norėdami pakeisti fragmento dydį ar strategiją, eikite į failo konfigūraciją → apdorojimo ribas.", + "To change the embedding provider or model, go to LLM Configuration.": "Norėdami pakeisti įterpinių teikėją ar modelį, eikite į LLM konfigūraciją.", + "To change the embedding provider or model, go to LLM configuration.": "Norėdami pakeisti įterpinių teikėją ar modelį, eikite į LLM konfigūraciją.", + "To date": "Iki datos", + "Toggle search sidebar": "Perjungti paieškos šoninę juostą", + "Token Created": "Žetonas sukurtas", + "Token Quota (per request)": "Žetonų kvota (vienai užklausai)", + "Token copied to clipboard": "Žetonas nukopijuotas į iškarpinę", + "Token name": "Žetono pavadinimas", + "Token revoked": "Žetonas atšauktas", + "Tools": "Įrankiai", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Įrankiai vykdomi su numatytaisiais agento naudotojo leidimais, kai jokia naudotojo sesija nėra aktyvi (pvz., cron užduotys). Numatytąjį naudotoją sukonfigūruokite skirtuke Nustatymai.", + "Tools respect the agent's own role and organization boundaries.": "Įrankiai paiso agento paties vaidmens ir organizacijos ribų.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Įrankiai paiso agento rodinių, leidimų ir organizacijos ribų.", + "Top Deleters": "Daugiausiai šalinę", + "Total": "Iš viso", + "Total Audit Trails": "Iš viso audito žurnalų", + "Total Chunks Available:": "Iš viso galimų fragmentų:", + "Total Deleted Items": "Iš viso pašalintų elementų", + "Total Members": "Iš viso narių", + "Total Objects": "Iš viso objektų", + "Total Objects in Database:": "Iš viso objektų duomenų bazėje:", + "Total Organisations": "Iš viso organizacijų", + "Total Results": "Iš viso rezultatų", + "Total Searches": "Iš viso paieškų", + "Total Size": "Bendras dydis", + "Total Storage": "Bendra saugykla", + "Total events": "Iš viso įvykių", + "Total:": "Iš viso:", + "Totals": "Sumos", + "Try searching by UUID or with different keywords": "Pabandykite ieškoti pagal UUID arba su kitais raktažodžiais", + "Type": "Tipas", + "Type \"DELETE COLLECTION\" to confirm:": "Įveskite „DELETE COLLECTION“, kad patvirtintumėte:", + "Type Issue": "Tipo problema", + "Type Variations:": "Tipo variacijos:", + "Type filter (optional, e.g. email)": "Tipo filtras (neprivaloma, pvz., el. paštas)", + "Type must be \"positive\" or \"negative\"": "Tipas turi būti „positive“ arba „negative“", + "Type to search for groups": "Rašykite, kad ieškotumėte grupių", + "Type to search for objects...": "Rašykite, kad ieškotumėte objektų...", + "Type to search for organisations": "Rašykite, kad ieškotumėte organizacijų", + "Type to search for users": "Rašykite, kad ieškotumėte naudotojų", + "Type to search groups...": "Rašykite, kad ieškotumėte grupių...", + "Type to search users...": "Rašykite, kad ieškotumėte naudotojų...", + "Type to search...": "Rašykite, kad ieškotumėte...", + "Type your username to confirm": "Įveskite savo naudotojo vardą, kad patvirtintumėte", + "URL": "URL", + "URL where Ollama is running": "URL, kuriuo veikia Ollama", + "UUID copied to clipboard": "UUID nukopijuotas į iškarpinę", + "UUID:": "UUID:", + "Unchanged": "Nepakeista", + "Unique Search Terms": "Unikalūs paieškos terminai", + "Unique Terms": "Unikalūs terminai", + "Unknown": "Nežinoma", + "Unknown Object": "Nežinomas objektas", + "Unknown Register": "Nežinomas registras", + "Unknown Schema": "Nežinoma schema", + "Unknown error": "Nežinoma klaida", + "Unknown widget type: {type}": "Nežinomas valdiklio tipas: {type}", + "Unlink email": "Atsieti el. laišką", + "Unlink event": "Atsieti įvykį", + "Unnamed Object": "Objektas be pavadinimo", + "Untitled": "Be pavadinimo", + "Untitled View": "Rodinys be pavadinimo", + "Update": "Atnaujinti", + "Update Operations": "Atnaujinimo operacijos", + "Update available: {local} → {remote}": "Yra atnaujinimas: {local} → {remote}", + "Update configuration: ...": "Atnaujinti konfigūraciją: ...", + "Update register OAS: ...": "Atnaujinti registro OAS: ...", + "Update vectors when object data changes (recommended for accurate search)": "Atnaujinti vektorius, kai pasikeičia objekto duomenys (rekomenduojama tiksliai paieškai)", + "Updated": "Atnaujinta", + "Updated At": "Atnaujinta", + "Updated:": "Atnaujinta:", + "Upload new avatar": "Įkelti naują avatarą", + "Uploading configuration sets...": "Įkeliami konfigūracijos rinkiniai...", + "Uptime:": "Veikimo laikas:", + "Url": "Url", + "Usage Count": "Naudojimo kiekis", + "Use AI agents for processing and analysis": "Naudoti DI agentus apdorojimui ir analizei", + "Use Large Language Model features": "Naudoti LLM funkcijas", + "Use SolrCloud with Zookeeper for distributed search": "Naudoti SolrCloud su Zookeeper paskirstytajai paieškai", + "Use SolrCloud with zookeeper for distributed search": "Naudoti SolrCloud su zookeeper paskirstytajai paieškai", + "Use VNG APIs instead of MongoDB.": "Naudoti VNG API vietoj MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Naudoti numatytuosius intervalus (Paskutinės 7/30/90 dienos, Paskutiniai metai, Senesni)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Naudoti išorinę saugyklą (pvz., MongoDB) vietoj vidinės Nextcloud saugyklos.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Naudokite filtrus, kad susiaurintumėte audito žurnalo įrašus pagal registrą, schemą, veiksmo tipą, naudotoją, datų intervalą arba objekto ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Naudokite filtrus, kad susiaurintumėte pašalintus elementus pagal registrą, schemą, pašalinimo datą arba juos pašalinusį naudotoją.", + "Use filters to narrow down entities by type or category.": "Naudokite filtrus, kad susiaurintumėte esybes pagal tipą ar kategoriją.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Naudokite filtrus, kad susiaurintumėte paieškos žurnalo įrašus pagal registrą, schemą, sėkmės būseną, naudotoją, datų intervalą, paieškos terminus arba našumo metriką.", + "Used By Collections": "Naudojama kolekcijų", + "Used by": "Naudoja", + "User": "Naudotojas", + "User Agent Statistics": "Naudotojo agento statistika", + "User Description": "Naudotojo aprašymas", + "User Quota": "Naudotojo kvota", + "User rate limits cleared successfully": "Naudotojo dažnio apribojimai sėkmingai išvalyti", + "User removed successfully": "Naudotojas sėkmingai pašalintas", + "User-friendly description shown in forms and help text": "Naudotojui patogus aprašymas, rodomas formose ir pagalbos tekste", + "Username": "Naudotojo vardas", + "Username for SOLR authentication (optional)": "Naudotojo vardas SOLR tapatybės nustatymui (neprivaloma)", + "Users": "Naudotojai", + "Uses": "Naudojimai", + "Using Pre-Generated Chunks": "Naudojami iš anksto sugeneruoti fragmentai", + "Validate": "Patvirtinti", + "Validate Objects": "Patvirtinti objektus", + "Validation completed for {schema}": "Patvirtinimas užbaigtas schemai {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Patvirtinimas užbaigtas schemai {schema}: {processed} apdorota, {updated} atnaujinta, {failed} nepavyko", + "Validation completed successfully": "Patvirtinimas sėkmingai baigtas", + "Validation failed": "Patvirtinimas nepavyko", + "Value": "Reikšmė", + "Value list (split on ,)": "Reikšmių sąrašas (atskirta kableliais ,)", + "Vector Dimensions": "Vektoriaus matmenys", + "Vector Search Backend": "Vektorinės paieškos vidinė sąsaja", + "Vector field: _embedding_": "Vektoriaus laukas: _embedding_", + "Vectorization Triggers": "Vektorizavimo suaktyvinimai", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorizavimas užbaigtas: {vectorized} objektų vektorizuota, {failed} nepavyko", + "Vectorization failed: {error}": "Vektorizavimas nepavyko: {error}", + "Vectorize All Files": "Vektorizuoti visus failus", + "Vectorize All Objects": "Vektorizuoti visus objektus", + "Vectorize all file types": "Vektorizuoti visus failų tipus", + "Vectorize all views": "Vektorizuoti visus rodinius", + "Vectorize on object creation": "Vektorizuoti sukuriant objektą", + "Vectors will be stored in your existing object and file collections": "Vektoriai bus saugomi jūsų esamose objektų ir failų kolekcijose", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Tikrinamas ryšys su serveriu...", + "Version": "Versija", + "Version Information": "Versijos informacija", + "Very High": "Labai aukštas", + "Very high": "Labai aukštas", + "View": "Rodinys", + "View API Docs": "Peržiūrėti API dokumentaciją", + "View API Documentation": "Peržiūrėti API dokumentaciją", + "View APIs": "Peržiūrėti API", + "View Changes": "Peržiūrėti pakeitimus", + "View Details": "Peržiūrėti išsamią informaciją", + "View Error": "Peržiūrėti klaidą", + "View File": "Peržiūrėti failą", + "View Full Details": "Peržiūrėti visą išsamią informaciją", + "View Logs": "Peržiūrėti žurnalus", + "View Name": "Rodinio pavadinimas", + "View Object": "Peržiūrėti objektą", + "View Parameters": "Rodinio parametrai", + "View Selection": "Rodinių pasirinkimas", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Peržiūrėkite ir analizuokite paieškos žurnalus su išplėstinio filtravimo ir analitikos galimybėmis", + "View and analyze system audit trails with advanced filtering capabilities": "Peržiūrėkite ir analizuokite sistemos audito žurnalus su išplėstinio filtravimo galimybėmis", + "View and manage authorization across registers and schemas": "Peržiūrėti ir valdyti įgaliojimus registruose ir schemose", + "View deleted successfully": "Rodinys sėkmingai pašalintas", + "View entity details and manage relations": "Peržiūrėti esybės išsamią informaciją ir valdyti ryšius", + "View name": "Rodinio pavadinimas", + "View name is required": "Reikia rodinio pavadinimo", + "View object": "Peržiūrėti objektą", + "View saved successfully!": "Rodinys sėkmingai įrašytas!", + "View search analytics and manage search logs": "Peržiūrėti paieškos analitiką ir valdyti paieškos žurnalus", + "View successfully deleted": "Rodinys sėkmingai pašalintas", + "View successfully updated": "Rodinys sėkmingai atnaujintas", + "View updated successfully!": "Rodinys sėkmingai atnaujintas!", + "View webhook delivery logs and filter by webhook": "Peržiūrėti žiniatinklio kabliukų pristatymo žurnalus ir filtruoti pagal žiniatinklio kabliuką", + "View {count} more": "Peržiūrėti dar {count}", + "Views": "Rodiniai", + "Visible to end users": "Matoma galutiniams naudotojams", + "Visible to users": "Matoma naudotojams", + "Wait for Response": "Laukti atsako", + "Wait for webhook response before continuing (required for request/response flows)": "Prieš tęsiant laukti žiniatinklio kabliuko atsako (būtina užklausos / atsako srautams)", + "Waiting for cluster synchronization...": "Laukiama klasterio sinchronizavimo...", + "Warmup Names Cache": "Iš anksto įkelti vardų podėlį", + "Warmup failed": "Įšildymas nepavyko", + "Warning": "Įspėjimas", + "Warning:": "Įspėjimas:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Įspėjimas: Ši savybė turi nenuoseklius tipus: {types}. Apsvarstykite, ar visi objektai turėtų turėti tą patį tipą.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Nuskaitysime visus šiai schemai priklausančius objektus, kad atrastume naujas savybes ir išanalizuotume esamas savybes dėl galimų patobulinimų. Procesas apima kiekvieno objekto duomenų struktūros nagrinėjimą, dabartinėje schemoje neapibrėžtų savybių nustatymą ir galimybių patobulinti esamus savybių apibrėžimus geresniais apribojimais, formatais bei patvirtinimo taisyklėmis paiešką.", + "Webhook": "Žiniatinklio kabliukas", + "Webhook Log Details": "Žiniatinklio kabliuko žurnalo išsami informacija", + "Webhook Logs": "Žiniatinklio kabliukų žurnalai", + "Webhook created successfully": "Žiniatinklio kabliukas sėkmingai sukurtas", + "Webhook deleted": "Žiniatinklio kabliukas pašalintas", + "Webhook retry delivered successfully": "Žiniatinklio kabliuko pakartojimas sėkmingai pristatytas", + "Webhook retry delivery failed": "Žiniatinklio kabliuko pakartojimo pristatymas nepavyko", + "Webhook updated": "Žiniatinklio kabliukas atnaujintas", + "Webhook updated successfully": "Žiniatinklio kabliukas sėkmingai atnaujintas", + "Webhooks": "Žiniatinklio kabliukai", + "Weekly": "Kas savaitę", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kai agentas veikia be naudotojo seanso (pvz., suplanuotos užduotys), bus naudojamas šio naudotojo kontekstas", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kai agentas veikia be naudotojo seanso (pvz., suplanuotos užduotys), bus naudojamas šio naudotojo kontekstas.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Kai išjungta, šis aspektas rodys tik šios schemos reikšmes ir pasirinkus įtrauks schemos filtrą.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Kai įjungta, išsaugant šį objektą bus atnaujinti ir tiksliniai objektai, įtraukiant nuorodą atgal į šį objektą.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Kai įjungta, ši savybė bus pašalinta iš šaltinio objekto atnaujinus tikslinius objektus.", + "Workflow Management": "Darbo eigų valdymas", + "Workflows": "Darbo eigos", + "Wrap webhook payload in CloudEvents format for better interoperability": "Įvilkti žiniatinklio kabliuko naudingąją apkrovą į CloudEvents formatą geresniam sąveikumui", + "Wrap webhook payload in cloudevents format for better interoperability": "Apvilkti Webhook naudingąją apkrovą cloudevents formatu geresniam sąveikumui", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Taip", + "You": "Jūs", + "You are not yet a member of any organisations.": "Dar nesate jokios organizacijos narys.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Galite kurti naujus ConfigSets pagal _default šabloną arba įkelti pasirinktinius tiesiai į savo SOLR serverį.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Galite kurti naujus ConfigSets pagal _default šabloną arba įkelti pasirinktinius tiesiai į savo SOLR serverį.", + "You do not have access to this conversation": "Jūs neturite prieigos prie šio pokalbio", + "You do not have permission to delete this conversation": "Jūs neturite leidimo pašalinti šį pokalbį", + "You do not have permission to modify this conversation": "Jūs neturite leidimo keisti šį pokalbį", + "You do not have permission to restore this conversation": "Jūs neturite leidimo atkurti šį pokalbį", + "You do not have permission to view the permission matrix. Admin access is required.": "Neturite leidimo peržiūrėti leidimų matricos. Reikalinga administratoriaus prieiga.", + "You have selected {count} properties to add to the schema.": "Pasirinkote {count} savybių, kurias norite pridėti prie schemos.", + "You must be logged in to favorite views": "Norėdami pažymėti rodinius kaip mėgstamus, turite būti prisijungę", + "You need an AI agent to start a conversation.": "Norėdami pradėti pokalbį, jums reikia DI agento.", + "Your Fireworks AI API key. Get one at": "Jūsų Fireworks AI API raktas. Gaukite jį adresu", + "Your OpenAI API key. Get one at": "Jūsų OpenAI API raktas. Gaukite jį adresu", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Jūsų atsiliepimas užregistruotas. Pageidaudami čia galite pateikti papildomos informacijos...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Jūsų atsiliepimas užfiksuotas. pasirinktinai čia galite pateikti papildomos informacijos...", + "Zookeeper Hosts": "Zookeeper pagrindiniai kompiuteriai", + "Zookeeper Port": "Zookeeper prievadas", + "Zookeeper Settings (SolrCloud)": "Zookeeper nustatymai (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper prisijungimo eilutė SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper prievado numeris (neprivaloma, numatytasis 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F NEGRĮŽTAMO IŠTRYNIMO ĮSPĖJIMAS \\u26A0\\uFE0F\n\nKetinate NEGRĮŽTAMAI ištrinti VISUS schemos \"{schema}\" objektus:\n\n\\u2022 Aktyvūs objektai: {active}\n\\u2022 Negrąžinamai ištrinti objektai: {deleted}\n\\u2022 Iš viso: {total}\n\nŠie objektai bus visiškai pašalinti iš duomenų bazės ir jų NEBUS GALIMA atkurti.\n\nAr tikrai esate įsitikinę?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F NEGRĮŽTAMO IŠTRYNIMO ĮSPĖJIMAS \\u26A0\\uFE0F\n\nKetinate NEGRĮŽTAMAI ištrinti {count} švelniai ištrintų schemos \"{schema}\" objektų.\n\nŠie objektai bus visiškai pašalinti iš duomenų bazės ir jų NEBUS GALIMA atkurti.\n\nAr tikrai esate įsitikinę?", + "and accessible to all users in selected groups (or all users if no groups selected).": "ir prieinami visiems naudotojams pasirinktose grupėse (arba visiems naudotojams, jei nepasirinkta jokios grupės).", + "and add the files there.": "ir pridėkite failus ten.", + "approved": "patvirtinta", + "are only accessible to invited users.": "prieinami tik pakviestiems naudotojams.", + "by": "pagal", + "chunk": "fragmentas", + "chunks": "fragmentai", + "chunks processed": "apdorota fragmentų", + "completed": "užbaigta", + "configuration(s)": "konfigūracija (-os)", + "conversationId is required": "Reikia conversationId", + "create": "kurti", + "delete": "ištrinti", + "deleted objects section": "ištrintų objektų skiltis", + "e.g., lib/Settings/config.json": "pvz., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "pvz., lib/Settings/register.json", + "e.g., lib/settings/config.json": "pvz., lib/settings/config.json", + "e.g., lib/settings/register.json": "pvz., lib/settings/register.json", + "execute": "vykdyti", + "failed": "nepavyko", + "file": "failas", + "files": "failai", + "file{plural}": "failas{plural}", + "folder": "aplankas", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "registre {register}", + "in_progress": "vykdoma", + "inherited": "paveldėta", + "list": "sąrašas", + "log{plural}": "žurnalas{plural}", + "manage": "valdyti", + "members": "nariai", + "menu or contact someone with permission to create agents.": "meniu arba kreipkitės į asmenį, turintį leidimą kurti agentus.", + "message content is required": "reikia žinutės turinio", + "messages": "žinutės", + "missing": "trūksta", + "myapp": "myapp", + "n8n URL and API key are required": "Reikia n8n URL ir API rakto", + "n8n configuration saved successfully": "n8n konfigūracija sėkmingai įrašyta", + "n8n connection not configured": "n8n ryšys nesukonfigūruotas", + "n8n connection successful": "n8n ryšys sėkmingas", + "n8n connection test failed: {message}": "n8n ryšio bandymas nepavyko: {message}", + "n8n connection test successful": "n8n ryšio bandymas sėkmingas", + "n8n initialization failed: {message}": "n8n inicijavimas nepavyko: {message}", + "n8n integration disabled": "n8n integracija išjungta", + "n8n integration enabled": "n8n integracija įjungta", + "n8n project initialized successfully": "n8n projektas sėkmingai inicijuotas", + "n8n settings saved successfully": "n8n nustatymai sėkmingai įrašyti", + "notify_push configuration guide": "notify_push konfigūracijos vadovas", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push įdiegta, bet OpenRegister dar nepatvirtino sėkmingo pranešimo. Norėdami suaktyvinti, suaktyvinkite objekto įrašymą arba patikrinkite savo notify_push konfigūraciją.", + "notify_push is installed but not yet active": "notify_push įdiegta, bet dar neaktyvi", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objektai", + "objects processed": "apdorota objektų", + "object{plural}": "objektas{plural}", + "or pick a range": "arba pasirinkite intervalą", + "org-...": "org-...", + "pending": "laukiama", + "provides a good balance between speed and accuracy.": "užtikrina gerą pusiausvyrą tarp greičio ir tikslumo.", + "public": "viešas", + "read": "skaityti", + "register(s)": "registras (-ai)", + "register(s) selected": "pasirinkta registrų", + "register{plural}": "registras{plural}", + "rejected": "atmesta", + "required": "privaloma", + "results": "rezultatai", + "schema(s)": "schema (-os)", + "schema(s) selected": "pasirinkta schemų", + "schema{plural}": "schema{plural}", + "selected objects": "pasirinkti objektai", + "set": "nustatyti", + "sk-...": "sk-...", + "this application": "šią programą", + "to": "iki", + "type must be \"positive\" or \"negative\"": "tipas turi būti „positive“ arba „negative“", + "update": "atnaujinti", + "vectorized": "vektorizuota", + "view": "peržiūrėti", + "views selected": "pasirinkta rodinių", + "waiting": "laukiama", + "{active} active, {deleted} deleted": "{active} aktyvūs, {deleted} ištrinti", + "{count} audit trails cleared successfully": "{count} audito sekos sėkmingai išvalytos", + "{count} compliance issue(s) detected": "Aptikta {count} atitikties problema (-os)", + "{count} deletions": "{count} ištrynimų", + "{count} documents found": "Rasta {count} dokumentų", + "{count} email": "{count} el. laiškas", + "{count} emails": "{count} el. laiškai", + "{count} entries": "{count} įrašai", + "{count} issues": "{count} problemos", + "{count} members": "{count} nariai", + "{count} searches": "{count} paieškos", + "{count} selected": "{count} pasirinkta", + "{count} widget(s)": "{count} valdiklis (-iai)", + "{matched} object(s) matched. {erased} erased.": "Atitiko {matched} objektas (-ai). Ištrinta {erased}.", + "{minutes} minutes ago": "Prieš {minutes} minučių", + "{percentage}% of objects": "{percentage}% objektų", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} registre {register} / {schema}", + "{type} cache cleared successfully": "{type} talpykla sėkmingai išvalyta", + "ℹ️ Current Configuration": "ℹ️ Dabartinė konfigūracija", + "ℹ️ current configuration": "ℹ️ dabartinė konfigūracija", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ PAVOJUS: Visi {total} objektai bus NEGRĮŽTAMAI IŠTRINTI iš duomenų bazės. Šis veiksmas yra NEATKURIAMAS ir negali būti atšauktas!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ NEGRĮŽTAMO IŠTRYNIMO ĮSPĖJIMAS ⚠️\n\nKetinate NEGRĮŽTAMAI ištrinti VISUS schemos \"{schema}\" objektus:\n\n• Aktyvūs objektai: {active}\n• Negrąžinamai ištrinti objektai: {deleted}\n• Iš viso: {total}\n\nŠie objektai bus visiškai pašalinti iš duomenų bazės ir jų NEBUS GALIMA atkurti.\n\nAr tikrai esate įsitikinę?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ NEGRĮŽTAMO IŠTRYNIMO ĮSPĖJIMAS ⚠️\n\nKetinate NEGRĮŽTAMAI ištrinti {count} švelniai ištrintų schemos \"{schema}\" objektų.\n\nŠie objektai bus visiškai pašalinti iš duomenų bazės ir jų NEBUS GALIMA atkurti.\n\nAr tikrai esate įsitikinę?", + "⚡ Batch Processing": "⚡ Paketinis apdorojimas", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR gali apdoroti milijonus dokumentų, kai paieškos atsako laikas trumpesnis nei sekundė...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Išsamus kontekstas ✓ Mažesnė tikimybė praleisti detales ✗ Lėtesni atsakymai ✗ Gali įtraukti mažiau aktualios informacijos", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Greitesni atsakymai ✓ Tikslingesni atsakymai ✗ Gali praleisti aktualią informaciją", + "✨ AI Features": "✨ DI funkcijos", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Paskirstytuoju režimu SOLR naudoja ZooKeeper klasterio koordinavimui...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Aspektinė paieška leidžia naudotojams gilintis į rezultatus pagal kategorijas...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Ar žinojote? SOLR užtikrina paiešką Netflix, Apple ir daugeliui kitų didelių svetainių!", + "💰 View Selection (Cost Optimization)": "💰 Rodinių pasirinkimas (sąnaudų optimizavimas)", + "📄 Object Serialization": "📄 Objekto serializavimas", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR teikia išsamią analitiką ir statistiką apie paieškos našumą...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets turi jūsų paieškos indekso schemos ir konfigūracijos failus...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikacija užtikrina, kad jūsų paieškos indeksas bus prieinamas net ir sutrikus mazgams...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR yra galinga įmonių paieškos platforma, sukurta Apache Lucene pagrindu...", + "🔢 Vectorization Settings": "🔢 Vektorizavimo nustatymai", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automatinis mastelis gali dinamiškai pridėti arba pašalinti mazgus pagal apkrovą...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Saugumo funkcijos apima autentifikavimą, įgaliojimą ir SSL šifravimą...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} el. laiškas", + "{count} el. laiškų" + ], + "Purge {count} object from database": [ + "Išvalyti {count} objektą iš duomenų bazės", + "Išvalyti {count} objektų iš duomenų bazės" + ], + "Restore {count} object": [ + "Atkurti {count} objektą", + "Atkurti {count} objektų" + ], + "Successfully restored {count} object": [ + "Sėkmingai atkurtas {count} objektas", + "Sėkmingai atkurta {count} objektų" + ], + "Delete {count} object": [ + "Ištrinti {count} objektą", + "Ištrinti {count} objektų" + ], + "Object successfully deleted": [ + "Objektas sėkmingai ištrintas", + "Objektai sėkmingai ištrinti" + ] + } +} diff --git a/l10n/lv.js b/l10n/lv.js new file mode 100644 index 0000000000..880685c77d --- /dev/null +++ b/l10n/lv.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(bez temata)", + "(no title)" : "(bez nosaukuma)", + "(unnamed)" : "(bez nosaukuma)", + "(untitled card)" : "(kartīte bez nosaukuma)", + "Activities" : "Aktivitātes", + "Add Endpoint" : "Pievienot galapunktu", + "Add Source" : "Pievienot avotu", + "Add card" : "Pievienot kartīti", + "Add contact" : "Pievienot kontaktu", + "All clear" : "Viss kārtībā", + "Articles" : "Raksti", + "Code" : "Kods", + "Compliance" : "Atbilstība", + "Contacts" : "Kontakti", + "Create Agent" : "Izveidot aģentu", + "Create Application" : "Izveidot lietotni", + "Create event" : "Izveidot notikumu", + "Erasure complete" : "Dzēšana pabeigta", + "Erasure preview" : "Dzēšanas priekšskatījums", + "Expand details" : "Izvērst detaļas", + "Save changes" : "Saglabāt izmaiņas", + "📄 Object Serialization" : "📄 Objekta serializācija", + "🔢 Vectorization Settings" : "🔢 Vektorizācijas iestatījumi", + "💰 View Selection (Cost Optimization)" : "💰 Skatu atlase (izmaksu optimizācija)", + "AI Assistant" : "MI asistents", + "AI Chat" : "MI tērzēšana", + "API Key" : "API atslēga", + "API tokens saved successfully" : "API marķieri veiksmīgi saglabāti", + "About ConfigSets" : "Par ConfigSet", + "Action" : "Darbība", + "Action Distribution" : "Darbību sadalījums", + "Action:" : "Darbība:", + "Actions" : "Darbības", + "Active" : "Aktīvs", + "Active Collections" : "Aktīvās kolekcijas", + "Active Filters" : "Aktīvie filtri", + "Active Organisations" : "Aktīvās organizācijas", + "Active filters:" : "Aktīvie filtri:", + "Active organisation set successfully" : "Aktīvā organizācija veiksmīgi iestatīta", + "Activity Period" : "Aktivitātes periods", + "Add Groups" : "Pievienot grupas", + "Add Schema" : "Pievienot shēmu", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Pievienojiet shēmu nosaukumus, aprakstus un reģistra informāciju, lai nodrošinātu bagātīgāku kontekstu meklēšanai", + "Add to favorites" : "Pievienot izlasei", + "Added" : "Pievienots", + "Added to favorites" : "Pievienots izlasei", + "Additional Information" : "Papildu informācija", + "Additional feedback saved. Thank you!" : "Papildu atsauksme saglabāta. Paldies!", + "Admin" : "Administrators", + "Advanced" : "Paplašināti", + "Advanced Filters" : "Paplašinātie filtri", + "Advanced Options" : "Paplašinātās opcijas", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Paplašinātie filtri nav pieejami, izmantojot datu bāzes avotu. Pārslēdzieties uz Auto vai SOLR indeksu, lai izmantotu filtrēšanas opcijas.", + "After" : "Pēc", + "Agent deleted successfully" : "Aģents veiksmīgi izdzēsts", + "Agents" : "Aģenti", + "All" : "Visi", + "All Categories" : "Visas kategorijas", + "All Collections" : "Visas kolekcijas", + "All Confidence Levels" : "Visi ticamības līmeņi", + "All ConfigSets" : "Visi ConfigSet", + "All Files" : "Visi faili", + "All Levels" : "Visi līmeņi", + "All Types" : "Visi veidi", + "All Webhooks" : "Visi tīmekļa āķi", + "All actions" : "Visas darbības", + "All audit trails cleared successfully" : "Visi audita pieraksti veiksmīgi notīrīti", + "All registers" : "Visi reģistri", + "All schemas" : "Visas shēmas", + "All searches" : "Visi meklējumi", + "All users" : "Visi lietotāji", + "An error occurred while clearing audit trails" : "Notīrot audita pierakstus, radās kļūda", + "An error occurred while deleting the audit trail" : "Dzēšot audita pierakstu, radās kļūda", + "An error occurred while deleting the view" : "Dzēšot skatu, radās kļūda", + "An error occurred while permanently deleting the objects" : "Neatgriezeniski dzēšot objektus, radās kļūda", + "Analysis completed:" : "Analīze pabeigta:", + "Analysis steps:" : "Analīzes soļi:", + "Analytics" : "Analītika", + "Analyze Objects" : "Analizēt objektus", + "Analyze Properties" : "Analizēt īpašības", + "Analyze existing properties for improvement opportunities" : "Analizēt esošās īpašības uzlabošanas iespēju nolūkā", + "Analyzing..." : "Notiek analīze...", + "Any favorites and sharing settings for this view" : "Jebkuri šī skata izlases un koplietošanas iestatījumi", + "Any user" : "Jebkurš lietotājs", + "App store cache invalidated successfully" : "Lietotņu veikala kešatmiņa veiksmīgi atspēkota", + "Application deleted successfully" : "Lietotne veiksmīgi izdzēsta", + "Applications" : "Lietotnes", + "Apply Changes" : "Lietot izmaiņas", + "Archive" : "Arhivēt", + "Archive conversation" : "Arhivēt sarunu", + "Archived conversations are hidden from your active list" : "Arhivētās sarunas tiek paslēptas no jūsu aktīvā saraksta", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Vai tiešām vēlaties iztīrīt vecos meklēšanas pierakstus? Tas izdzēsīs ierakstus, kas vecāki par 30 dienām.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Vai tiešām vēlaties izdzēst atlasītos audita pierakstus? Šo darbību nevar atsaukt.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Vai tiešām vēlaties izdzēst atlasītos meklēšanas pierakstus? Šo darbību nevar atsaukt.", + "Are you sure you want to delete this ConfigSet?" : "Vai tiešām vēlaties izdzēst šo ConfigSet?", + "Are you sure you want to permanently delete" : "Vai tiešām vēlaties neatgriezeniski izdzēst", + "Ask a question..." : "Uzdodiet jautājumu...", + "Ask questions about your data using natural language" : "Uzdodiet jautājumus par saviem datiem dabiskā valodā", + "Attempt" : "Mēģinājums", + "Audit Trail Changes" : "Audita pieraksta izmaiņas", + "Audit Trail Details" : "Audita pieraksta detaļas", + "Audit Trail Management" : "Audita pierakstu pārvaldība", + "Audit Trail Statistics" : "Audita pierakstu statistika", + "Audit Trails" : "Audita pieraksti", + "Audit trail data copied to clipboard" : "Audita pieraksta dati nokopēti starpliktuvē", + "Audit trail deleted successfully" : "Audita pieraksts veiksmīgi izdzēsts", + "Audit trail successfully deleted" : "Audita pieraksts veiksmīgi izdzēsts", + "Authentication" : "Autentifikācija", + "Auto-commit disabled" : "Automātiskā apstiprināšana atspējota", + "Auto-commit enabled" : "Automātiskā apstiprināšana iespējota", + "Auto-create Default Organisation" : "Automātiski izveidot noklusējuma organizāciju", + "Auto-retry failed vectorizations" : "Automātiski atkārtot neizdevušās vektorizācijas", + "Automatically commit changes to SOLR index" : "Automātiski apstiprināt izmaiņas SOLR indeksā", + "Automatically create a default organisation if none exists when the app is initialized" : "Automātiski izveidot noklusējuma organizāciju, ja lietotnes inicializēšanas laikā tāda nepastāv", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Automātiski ģenerēt vektoru iegultnes no teksta fragmentiem, kad faili tiek augšupielādēti un apstrādāti", + "Automatically generate vector embeddings when objects are created or updated" : "Automātiski ģenerēt vektoru iegultnes, kad objekti tiek izveidoti vai atjaunināti", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Automātiski atkārtot neizdevušos vektorizācijas mēģinājumus (maks. 3 atkārtojumi)", + "Available Filters" : "Pieejamie filtri", + "Available Workflows" : "Pieejamie darbplūsmas", + "Avg Execution Time" : "Vid. izpildes laiks", + "Avg Members/Org" : "Vid. dalībnieki/org.", + "Avg Object Views/Session" : "Vid. objektu skatījumi/sesijā", + "Avg Response Time" : "Vid. atbildes laiks", + "Avg Results/Search" : "Vid. rezultāti/meklējumā", + "Avg Searches/Session" : "Vid. meklējumi/sesijā", + "Back" : "Atpakaļ", + "Back to Entities" : "Atpakaļ uz entītijām", + "Back to Registers" : "Atpakaļ uz reģistriem", + "Back to Webhooks" : "Atpakaļ uz tīmekļa āķiem", + "Back to applications" : "Atpakaļ uz lietotnēm", + "Back to entities" : "Atpakaļ uz entītijām", + "Backend updated successfully. Please reload the application." : "Aizmugursistēma veiksmīgi atjaunināta. Lūdzu, pārlādējiet lietotni.", + "Base URL (Optional)" : "Bāzes URL (neobligāti)", + "Basic Information" : "Pamatinformācija", + "Batch Size" : "Pakas izmērs", + "Batch extraction completed" : "Pakas ekstrakcija pabeigta", + "Before" : "Pirms", + "Before object vectorization can work:" : "Pirms objektu vektorizācija var darboties:", + "Behavior Issue" : "Uzvedības problēma", + "Bookmarks" : "Grāmatzīmes", + "Blob storage has been retired. All objects now use magic tables." : "Blob krātuve ir izņemta no lietošanas. Visi objekti tagad izmanto magic tabulas.", + "Branch" : "Zars", + "Bulk delete operation completed successfully" : "Lielapjoma dzēšanas darbība veiksmīgi pabeigta", + "Bulk save operation completed successfully" : "Lielapjoma saglabāšanas darbība veiksmīgi pabeigta", + "Business Data" : "Biznesa dati", + "Cache cleared successfully" : "Kešatmiņa veiksmīgi notīrīta", + "Calculate Sizes" : "Aprēķināt izmērus", + "Cancel" : "Atcelt", + "Cannot delete: objects are still attached" : "Nevar izdzēst: objekti joprojām ir pievienoti", + "Category" : "Kategorija", + "Change Type" : "Izmaiņas veids", + "Changes" : "Izmaiņas", + "Changes copied to clipboard" : "Izmaiņas nokopētas starpliktuvē", + "Chat Model" : "Tērzēšanas modelis", + "Chat Provider" : "Tērzēšanas nodrošinātājs", + "Chat Provider (RAG)" : "Tērzēšanas nodrošinātājs (RAG)", + "Chat Settings" : "Tērzēšanas iestatījumi", + "Chat provider connection successful!" : "Savienojums ar tērzēšanas nodrošinātāju veiksmīgs!", + "Chat settings" : "Tērzēšanas iestatījumi", + "Choose a register" : "Izvēlieties reģistru", + "Choose a schema" : "Izvēlieties shēmu", + "Choose how vector similarity calculations are performed for semantic search" : "Izvēlieties, kā semantiskajai meklēšanai tiek veikti vektoru līdzības aprēķini", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Izvēlieties, kurus failu veidus iekļaut vektorizācijas procesā. Tiks apstrādāti tikai faili ar ekstraģētu tekstu un fragmentiem.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Izvēlieties, kurus skatus iekļaut vektorizācijas procesā. Atstājiet tukšu, lai apstrādātu visus skatus atbilstoši jūsu konfigurācijai.", + "Chunk" : "Fragments", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Fragmentu dzēšana vēl nav ieviesta. Izmantojiet uz fragmentiem balstītus galapunktus.", + "Chunks" : "Fragmenti", + "Chunks to Vectorize:" : "Vektorizējamie fragmenti:", + "Cleanup Old Trails" : "Iztīrīt vecos pierakstus", + "Cleanup completed" : "Iztīrīšana pabeigta", + "Clear All Embeddings" : "Notīrīt visas iegultnes", + "Clear Entries" : "Notīrīt ierakstus", + "Clear Filtered Audit Trails" : "Notīrīt filtrētos audita pierakstus", + "Clear Filters" : "Notīrīt filtrus", + "Clear Index" : "Notīrīt indeksu", + "Clear Selection" : "Notīrīt atlasi", + "Clear all" : "Notīrīt visu", + "Clear all filters" : "Notīrīt visus filtrus", + "Clear date range" : "Notīrīt datumu diapazonu", + "Clearing..." : "Notiek notīrīšana...", + "Close" : "Aizvērt", + "Collection Management" : "Kolekciju pārvaldība", + "Collection Name" : "Kolekcijas nosaukums", + "Collection assignments updated successfully" : "Kolekciju piešķīrumi veiksmīgi atjaunināti", + "Collection cleared successfully" : "Kolekcija veiksmīgi notīrīta", + "Collection copied successfully" : "Kolekcija veiksmīgi nokopēta", + "Collection created successfully" : "Kolekcija veiksmīgi izveidota", + "Collection deleted successfully" : "Kolekcija veiksmīgi izdzēsta", + "Collection used to store and index file metadata and content" : "Kolekcija, ko izmanto failu metadatu un satura glabāšanai un indeksēšanai", + "Collection used to store and index object data" : "Kolekcija, ko izmanto objektu datu glabāšanai un indeksēšanai", + "Collections:" : "Kolekcijas:", + "Commit Message" : "Apstiprināšanas ziņojums", + "Commit Within (ms)" : "Apstiprināt laikā (ms)", + "Commit message" : "Apstiprināšanas ziņojums", + "Compare current schema with real object data" : "Salīdzināt pašreizējo shēmu ar reāliem objektu datiem", + "Completed" : "Pabeigts", + "Complex" : "Sarežģīts", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Sarežģīti vaicājumi: paplašinātie meklējumi ar vairākiem filtriem, operatoriem un sarežģītām parametru kombinācijām", + "Confidence Level" : "Ticamības līmenis", + "Confidence Score" : "Ticamības vērtējums", + "Config must be provided as an object" : "Konfigurācija jānorāda kā objekts", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSet pārvaldība", + "ConfigSet Name" : "ConfigSet nosaukums", + "ConfigSet created successfully" : "ConfigSet veiksmīgi izveidots", + "ConfigSet deleted successfully" : "ConfigSet veiksmīgi izdzēsts", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSet definē jūsu SOLR kolekciju shēmu un konfigurāciju. Tie satur lauku definīcijas, analizatorus un citus meklēšanas iestatījumus.", + "Configuration" : "Konfigurācija", + "Configuration Data" : "Konfigurācijas dati", + "Configuration saved" : "Konfigurācija saglabāta", + "Configurations" : "Konfigurācijas", + "Configure Facets" : "Konfigurēt fasetes", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurējiet lielo valodu modeļu (LLM) nodrošinātājus ar MI darbinātām funkcijām, tostarp semantiskajai meklēšanai, iegultnēm un tērzēšanai.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurējiet sava SOLR servera pamata savienojuma iestatījumus, tostarp autentifikācijas un tīkla opcijas. Izmantojiet atsevišķos ConfigSet un kolekciju pārvaldības dialogus, lai pārvaldītu kodolus un kolekcijas.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurējiet, kā datu bāzes objekti tiek pārvērsti vektoru iegultnēs semantiskajai meklēšanai. Objekti tiek tieši vektorizēti, neprasot teksta ekstrakciju.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurējiet, kā objekti pirms vektorizācijas tiek pārvērsti tekstā. Šie iestatījumi ietekmē meklēšanas kvalitāti un kontekstu.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurējiet parametrus failu vektorizācijai. Šis process ģenerēs vektoru iegultnes visiem ekstraģētajiem failu fragmentiem.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurējiet parametrus objektu vektorizācijai. Šis process ģenerēs vektoru iegultnes visiem objektiem, kas atbilst jūsu skatu filtriem.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurējiet, kuru veidu datus meklēt un cik avotus izgūt", + "Connection Failed" : "Savienojums neizdevās", + "Connection Settings" : "Savienojuma iestatījumi", + "Connection Successful!" : "Savienojums veiksmīgs!", + "Connection failed" : "Savienojums neizdevās", + "Connection protocol" : "Savienojuma protokols", + "Connection successful" : "Savienojums veiksmīgs", + "Connection timeout in seconds" : "Savienojuma noildze sekundēs", + "Constant delay between retries (5 minutes)" : "Pastāvīga aizkave starp atkārtojumiem (5 minūtes)", + "Constraint Issue" : "Ierobežojuma problēma", + "Content Filters" : "Satura filtri", + "Continue to Properties" : "Turpināt uz īpašībām", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontrolējiet, kuri objektu skati jāvektorizē, lai samazinātu API izmaksas. Vektorizējiet tikai tos skatus, kuriem ir noderīga semantiskā meklēšana.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontrolējiet, kurus skatus un rīkus MI var izmantot šajā sarunā. Pēc noklusējuma visas aģenta iespējas ir iespējotas.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Kontrolē nejaušību (0 = deterministisks, 2 = ļoti radošs)", + "Conversation ID is required" : "Sarunas ID ir obligāts", + "Conversation archived" : "Saruna arhivēta", + "Conversation archived successfully" : "Saruna veiksmīgi arhivēta", + "Conversation cleared successfully" : "Saruna veiksmīgi notīrīta", + "Conversation deleted" : "Saruna izdzēsta", + "Conversation permanently deleted" : "Saruna neatgriezeniski izdzēsta", + "Conversation renamed" : "Saruna pārdēvēta", + "Conversation restored" : "Saruna atjaunota", + "Conversation title" : "Sarunas nosaukums", + "Copied!" : "Nokopēts!", + "Copy" : "Kopēt", + "Copy Changes" : "Kopēt izmaiņas", + "Copy Collection" : "Kopēt kolekciju", + "Copy Data" : "Kopēt datus", + "Copy Full Data" : "Kopēt visus datus", + "Copying..." : "Notiek kopēšana...", + "Count" : "Skaits", + "Counting objects..." : "Notiek objektu skaitīšana...", + "Costs" : "Izmaksas", + "Create" : "Izveidot", + "Create Collection" : "Izveidot kolekciju", + "Create ConfigSet" : "Izveidot ConfigSet", + "Create New Collection" : "Izveidot jaunu kolekciju", + "Create New ConfigSet" : "Izveidot jaunu ConfigSet", + "Create Operations" : "Izveidošanas darbības", + "Create Webhook" : "Izveidot tīmekļa āķi", + "Create a copy of collection:" : "Izveidot kolekcijas kopiju:", + "Create a new ConfigSet based on the _default template" : "Izveidot jaunu ConfigSet, balstoties uz _default veidni", + "Create a new SOLR collection from an existing ConfigSet" : "Izveidot jaunu SOLR kolekciju no esoša ConfigSet", + "Create your first AI agent to get started." : "Izveidojiet savu pirmo MI aģentu, lai sāktu darbu.", + "Created" : "Izveidots", + "Created:" : "Izveidots:", + "Creating..." : "Notiek izveide...", + "Current Filters" : "Pašreizējie filtri", + "Current Type:" : "Pašreizējais veids:", + "Custom API endpoint if using a different region" : "Pielāgots API galapunkts, ja izmantojat citu reģionu", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Pielāgotas HTTP galvenes (viena rindā, formāts: Header-Name: value)", + "Daily" : "Katru dienu", + "Dashboard" : "Informācijas panelis", + "Data Source" : "Datu avots", + "Data sources" : "Datu avoti", + "Data type does not match observed values" : "Datu tips neatbilst novērotajām vērtībām", + "Database URL" : "Datu bāzes URL", + "Database information refreshed" : "Datu bāzes informācija atsvaidzināta", + "Date Range" : "Datumu diapazons", + "Default" : "Noklusējums", + "Default Organisation" : "Noklusējuma organizācija", + "Default: 5" : "Noklusējums: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Aizkaves dubultojas ar katru mēģinājumu (2, 4, 8 minūtes...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Aizkaves palielinās lineāri (5, 10, 15 minūtes...)", + "Delete" : "Dzēst", + "Delete Application" : "Dzēst lietotni", + "Delete Audit Trail" : "Dzēst audita pierakstu", + "Delete Collection" : "Dzēst kolekciju", + "Delete ConfigSet" : "Dzēst ConfigSet", + "Delete Objects" : "Dzēst objektus", + "Delete Operations" : "Dzēšanas darbības", + "Delete View" : "Dzēst skatu", + "Delete all objects in this schema" : "Dzēst visus objektus šajā shēmā", + "Delete permanently" : "Dzēst neatgriezeniski", + "Delete this conversation?" : "Dzēst šo sarunu?", + "Delete view" : "Dzēst skatu", + "Deleted" : "Izdzēsts", + "Deleted By" : "Dzēsa", + "Deleted Date" : "Dzēšanas datums", + "Deleted Items Management" : "Dzēsto vienumu pārvaldība", + "Deleted This Week" : "Dzēsts šonedēļ", + "Deleted Today" : "Dzēsts šodien", + "Deleted:" : "Izdzēsts:", + "Deleting..." : "Notiek dzēšana...", + "Deletion Date Range" : "Dzēšanas datumu diapazons", + "Deletion Statistics" : "Dzēšanas statistika", + "Deprecated" : "Novecojis", + "Description" : "Apraksts", + "Description:" : "Apraksts:", + "Detect data types and patterns" : "Noteikt datu tipus un šablonus", + "Detected At" : "Noteikts", + "Detected Format:" : "Noteiktais formāts:", + "Detected Issues:" : "Noteiktās problēmas:", + "Disable" : "Atspējot", + "Disabled" : "Atspējots", + "Discover Files" : "Atklāt failus", + "Discovered Properties" : "Atklātās īpašības", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Vai vēlaties neatgriezeniski izdzēst visus filtrētos audita pierakstu ierakstus? Šo darbību nevar atsaukt.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Vai vēlaties neatgriezeniski izdzēst šo audita pieraksta ierakstu? Šo darbību nevar atsaukt.", + "Documents" : "Dokumenti", + "Download API Spec" : "Lejupielādēt API specifikāciju", + "Edit" : "Rediģēt", + "Edit Register" : "Rediģēt reģistru", + "Edit Schema" : "Rediģēt shēmu", + "Edit View" : "Rediģēt skatu", + "Edit Webhook" : "Rediģēt tīmekļa āķi", + "Edit view" : "Rediģēt skatu", + "Edit view details" : "Rediģēt skata detaļas", + "Effectiveness" : "Efektivitāte", + "Email" : "E-pasts", + "Emails" : "E-pasti", + "Embedding Model" : "Iegultnes modelis", + "Embedding Provider" : "Iegultnes nodrošinātājs", + "Embedding generated successfully" : "Iegultne veiksmīgi ģenerēta", + "Embedding provider connection successful!" : "Savienojums ar iegultnes nodrošinātāju veiksmīgs!", + "Enable" : "Iespējot", + "Enable auto-creation" : "Iespējot automātisko izveidi", + "Enable automatic file vectorization" : "Iespējot automātisko failu vektorizāciju", + "Enable automatic object vectorization" : "Iespējot automātisko objektu vektorizāciju", + "Enable automatic synchronization" : "Iespējot automātisko sinhronizāciju", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Iespējot detalizētu žurnālošanu SOLR darbībām (ieteicams atkļūdošanai)", + "Enable faceting" : "Iespējot fasetēšanu", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Iespējot vai atspējot LLM funkcijas. Konfigurējiet nodrošinātājus un modeļus, izmantojot iepriekš redzamo pogu LLM konfigurācija.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Iespējot vai atspējot SOLR meklēšanas integrāciju. Konfigurējiet savienojuma iestatījumus, izmantojot iepriekš redzamo pogu Savienojuma iestatījumi.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Iespējot vai atspējot n8n darbplūsmu integrāciju. Konfigurējiet savienojuma iestatījumus zemāk.", + "Enable or disable this webhook" : "Iespējot vai atspējot šo tīmekļa āķi", + "Enable vectorization for all existing and future views (may increase costs)" : "Iespējot vektorizāciju visiem esošajiem un nākotnes skatiem (var palielināt izmaksas)", + "Enabled" : "Iespējots", + "Endpoints" : "Galapunkti", + "Enter ConfigSet name" : "Ievadiet ConfigSet nosaukumu", + "Enter collection name" : "Ievadiet kolekcijas nosaukumu", + "Enter description (optional)..." : "Ievadiet aprakstu (neobligāti)...", + "Enter new collection name" : "Ievadiet jaunu kolekcijas nosaukumu", + "Enter object ID" : "Ievadiet objekta ID", + "Enter search term" : "Ievadiet meklēšanas terminu", + "Enter view name..." : "Ievadiet skata nosaukumu...", + "Enter webhook name" : "Ievadiet tīmekļa āķa nosaukumu", + "Entities" : "Entītijas", + "Entity Information" : "Entītijas informācija", + "Entity deleted successfully" : "Entītija veiksmīgi izdzēsta", + "Entity not found" : "Entītija nav atrasta", + "Entries to be deleted:" : "Dzēšamie ieraksti:", + "Enum Issue" : "Uzskaitījuma problēma", + "Enum constraint is missing" : "Trūkst uzskaitījuma ierobežojuma", + "Error" : "Kļūda", + "Error Details" : "Kļūdas detaļas", + "Error Information" : "Kļūdas informācija", + "Error Message" : "Kļūdas ziņojums", + "Error loading application" : "Kļūda, ielādējot lietotni", + "Error loading audit trails" : "Kļūda, ielādējot audita pierakstus", + "Error loading entity" : "Kļūda, ielādējot entītiju", + "Error loading search trails" : "Kļūda, ielādējot meklēšanas pierakstus", + "Estimated Batches:" : "Aplēstās pakas:", + "Estimated Cost:" : "Aplēstās izmaksas:", + "Estimated Duration:" : "Aplēstais ilgums:", + "Event" : "Notikums", + "Event Property for Payload" : "Notikuma īpašība lietderīgajai slodzei", + "Events" : "Notikumi", + "Example Value" : "Piemēra vērtība", + "Exclusive Maximum" : "Ekskluzīvs maksimums", + "Exclusive Minimum" : "Ekskluzīvs minimums", + "Execution Mode" : "Izpildes režīms", + "Execution Time" : "Izpildes laiks", + "Execution Time Range" : "Izpildes laika diapazons", + "Existing Improvements" : "Esošie uzlabojumi", + "Exponential" : "Eksponenciāls", + "Export" : "Eksportēt", + "Export completed successfully" : "Eksports veiksmīgi pabeigts", + "Export, view, or delete audit trails" : "Eksportēt, skatīt vai dzēst audita pierakstus", + "Extend Schema" : "Paplašināt shēmu", + "Extended" : "Paplašināts", + "Extensions" : "Paplašinājumi", + "Extract Pending Files" : "Ekstraģēt gaidošos failus", + "Extract properties from each object" : "Ekstraģēt īpašības no katra objekta", + "Extracted At" : "Ekstraģēts", + "Extraction Status" : "Ekstrakcijas statuss", + "Facet configuration updated successfully" : "Fašu konfigurācija veiksmīgi atjaunināta", + "Facets discovered and configured successfully" : "Fasetes veiksmīgi atklātas un konfigurētas", + "Facets discovered successfully" : "Fasetes veiksmīgi atklātas", + "Failed" : "Neizdevās", + "Failed to analyze schema properties" : "Neizdevās analizēt shēmas īpašības", + "Failed to archive conversation" : "Neizdevās arhivēt sarunu", + "Failed to calculate sizes" : "Neizdevās aprēķināt izmērus", + "Failed to clear collection" : "Neizdevās notīrīt kolekciju", + "Failed to copy changes" : "Neizdevās nokopēt izmaiņas", + "Failed to copy data" : "Neizdevās nokopēt datus", + "Failed to copy data to clipboard" : "Neizdevās nokopēt datus starpliktuvē", + "Failed to create conversation" : "Neizdevās izveidot sarunu", + "Failed to create or find project" : "Neizdevās izveidot vai atrast projektu", + "Failed to delete collection" : "Neizdevās izdzēst kolekciju", + "Failed to delete conversation" : "Neizdevās izdzēst sarunu", + "Failed to delete webhook" : "Neizdevās izdzēst tīmekļa āķi", + "Failed to download API specification" : "Neizdevās lejupielādēt API specifikāciju", + "Failed to get SOLR field configuration" : "Neizdevās iegūt SOLR lauka konfigurāciju", + "Failed to get entity categories" : "Neizdevās iegūt entītiju kategorijas", + "Failed to get entity statistics" : "Neizdevās iegūt entītiju statistiku", + "Failed to get entity types" : "Neizdevās iegūt entītiju veidus", + "Failed to load LLM configuration" : "Neizdevās ielādēt LLM konfigurāciju", + "Failed to load Nextcloud groups" : "Neizdevās ielādēt Nextcloud grupas", + "Failed to load conversation" : "Neizdevās ielādēt sarunu", + "Failed to load entities" : "Neizdevās ielādēt entītijas", + "Failed to load entity" : "Neizdevās ielādēt entītiju", + "Failed to load files" : "Neizdevās ielādēt failus", + "Failed to load organisations" : "Neizdevās ielādēt organizācijas", + "Failed to load templates" : "Neizdevās ielādēt veidnes", + "Failed to load webhooks" : "Neizdevās ielādēt tīmekļa āķus", + "Failed to load workflows" : "Neizdevās ielādēt darbplūsmas", + "Failed to refresh database information" : "Neizdevās atsvaidzināt datu bāzes informāciju", + "Failed to reindex collection" : "Neizdevās pārindeksēt kolekciju", + "Failed to rename conversation" : "Neizdevās pārdēvēt sarunu", + "Failed to restore conversation" : "Neizdevās atjaunot sarunu", + "Failed to retry extraction" : "Neizdevās atkārtot ekstrakciju", + "Failed to retry webhook" : "Neizdevās atkārtot tīmekļa āķi", + "Failed to save GitHub token" : "Neizdevās saglabāt GitHub marķieri", + "Failed to save GitLab URL" : "Neizdevās saglabāt GitLab URL", + "Failed to save GitLab token" : "Neizdevās saglabāt GitLab marķieri", + "Failed to save additional feedback" : "Neizdevās saglabāt papildu atsauksmi", + "Failed to save n8n configuration" : "Neizdevās saglabāt n8n konfigurāciju", + "Failed to save roles" : "Neizdevās saglabāt lomas", + "Failed to save settings" : "Neizdevās saglabāt iestatījumus", + "Failed to save webhook" : "Neizdevās saglabāt tīmekļa āķi", + "Failed to send feedback" : "Neizdevās nosūtīt atsauksmi", + "Failed to test webhook" : "Neizdevās testēt tīmekļa āķi", + "Failed to update favorite status" : "Neizdevās atjaunināt izlases statusu", + "Failed to update schema properties" : "Neizdevās atjaunināt shēmas īpašības", + "Failed to update view" : "Neizdevās atjaunināt skatu", + "Failed to update webhook" : "Neizdevās atjaunināt tīmekļa āķi", + "Feedback recorded" : "Atsauksme reģistrēta", + "Fewer sources (1-3):" : "Mazāk avotu (1-3):", + "Field" : "Lauks", + "File" : "Fails", + "File Chunk Prediction" : "Faila fragmentu prognoze", + "File Collection" : "Failu kolekcija", + "File Management" : "Failu pārvaldība", + "File Name" : "Faila nosaukums", + "File Path" : "Faila ceļš", + "File Type Selection" : "Failu veidu atlase", + "File Vectorization" : "Failu vektorizācija", + "File Warmup" : "Failu iesildīšana", + "File actions menu" : "Failu darbību izvēlne", + "File anonymized successfully" : "Fails veiksmīgi anonimizēts", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Faila fragmentu vektorizācija vēl nav ieviesta. Fragmenti ir gatavi un saglabāti, taču vektorizācijas pakalpojums tiek izstrādāts.", + "File collection not configured" : "Failu kolekcija nav konfigurēta", + "File discovery completed" : "Failu atklāšana pabeigta", + "File extraction completed" : "Failu ekstrakcija pabeigta", + "File extraction queued" : "Failu ekstrakcija ievietota rindā", + "File indexed successfully" : "Fails veiksmīgi indeksēts", + "File is already anonymized" : "Fails jau ir anonimizēts", + "File not found" : "Fails nav atrasts", + "File settings updated successfully" : "Failu iestatījumi veiksmīgi atjaunināti", + "File sources" : "Failu avoti", + "File vectorization configuration saved successfully" : "Failu vektorizācijas konfigurācija veiksmīgi saglabāta", + "File vectorization started. Check the statistics section for progress." : "Failu vektorizācija sākta. Skatiet statistikas sadaļu, lai uzzinātu progresu.", + "File warmup completed" : "Failu iesildīšana pabeigta", + "Files" : "Faili", + "Files with Completed Extraction:" : "Faili ar pabeigtu ekstrakciju:", + "Files → fileCollection, Objects → objectCollection" : "Faili → fileCollection, Objekti → objectCollection", + "Filter Audit Trails" : "Filtrēt audita pierakstus", + "Filter Deleted Items" : "Filtrēt dzēstos vienumus", + "Filter Objects" : "Filtrēt objektus", + "Filter Properties" : "Filtrēt īpašības", + "Filter Search Trails" : "Filtrēt meklēšanas pierakstus", + "Filter Statistics" : "Filtrēšanas statistika", + "Filter and analyze search trail entries" : "Filtrēt un analizēt meklēšanas pierakstu ierakstus", + "Filter and manage audit trail entries" : "Filtrēt un pārvaldīt audita pierakstu ierakstus", + "Filter and manage soft deleted items" : "Filtrēt un pārvaldīt maigi dzēstos vienumus", + "Filter by object ID" : "Filtrēt pēc objekta ID", + "Filter by search term" : "Filtrēt pēc meklēšanas termina", + "Filter by webhook" : "Filtrēt pēc tīmekļa āķa", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filtru dati ielādēti automātiski. Izmantojiet zemāk redzamos filtrus, lai precizētu meklēšanu.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrēt tīmekļa āķu aktivizatorus pēc lietderīgās slodzes īpašībām (viena rindā, formāts: key: value)", + "Filtered" : "Filtrēts", + "Filters" : "Filtri", + "Fireworks AI Chat Settings" : "Fireworks AI tērzēšanas iestatījumi", + "Fireworks AI Embedding Configuration" : "Fireworks AI iegultnes konfigurācija", + "First" : "Pirmais", + "Fixed" : "Fiksēts", + "For chat and retrieval-augmented generation" : "Tērzēšanai un izguves papildinātajai ģenerēšanai", + "For vector embeddings and semantic search" : "Vektoru iegultnēm un semantiskajai meklēšanai", + "Format" : "Formāts", + "Format Issue" : "Formāta problēma", + "Format constraint is missing" : "Trūkst formāta ierobežojuma", + "From Date" : "No datuma", + "From date" : "No datuma", + "Full data copied to clipboard" : "Visi dati nokopēti starpliktuvē", + "General Issue" : "Vispārēja problēma", + "Generate recommendations and confidence scores" : "Ģenerēt ieteikumus un ticamības vērtējumus", + "Generate vectors immediately when new objects are created" : "Ģenerēt vektorus uzreiz, kad tiek izveidoti jauni objekti", + "GitHub token is valid" : "GitHub marķieris ir derīgs", + "GitHub token removed successfully" : "GitHub marķieris veiksmīgi noņemts", + "GitHub token saved successfully" : "GitHub marķieris veiksmīgi saglabāts", + "GitLab URL saved successfully" : "GitLab URL veiksmīgi saglabāts", + "GitLab token is valid" : "GitLab marķieris ir derīgs", + "GitLab token saved successfully" : "GitLab marķieris veiksmīgi saglabāts", + "HTTP Method" : "HTTP metode", + "HTTP method used to send webhook requests" : "HTTP metode, ko izmanto tīmekļa āķu pieprasījumu sūtīšanai", + "Headers" : "Galvenes", + "Health" : "Veselīgums", + "Heartbeat successful - connection kept alive" : "Sirdspuksts veiksmīgs - savienojums tiek uzturēts aktīvs", + "Helpful" : "Noderīgs", + "Hide Filters" : "Paslēpt filtrus", + "Hide in forms" : "Paslēpt formās", + "Hide in list view" : "Paslēpt saraksta skatā", + "High" : "Augsts", + "High Confidence" : "Augsta ticamība", + "Host" : "Resursdators", + "Hourly" : "Katru stundu", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Cik dziļi šķērsot ligzdotās objektu īpašības (1-20). Augstākas vērtības tver vairāk detaļu, taču palielina vektora izmēru.", + "How often to check for updates (1-168 hours)" : "Cik bieži pārbaudīt atjauninājumus (1-168 stundas)", + "How to handle retries for failed webhook deliveries" : "Kā apstrādāt atkārtojumus neizdevušajām tīmekļa āķu piegādēm", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "IP ātruma ierobežojumi veiksmīgi notīrīti", + "Identify properties not in the schema" : "Identificēt īpašības, kas nav shēmā", + "Immutable" : "Nemainīgs", + "Import" : "Importēt", + "Import successful" : "Imports veiksmīgs", + "Improved Property" : "Uzlabota īpašība", + "In use" : "Tiek lietots", + "Inactive" : "Neaktīvs", + "Include IDs and names of related objects for better contextual search" : "Iekļaut saistīto objektu ID un nosaukumus labākai kontekstuālajai meklēšanai", + "Include related object references" : "Iekļaut saistīto objektu atsauces", + "Include schema and register metadata" : "Iekļaut shēmas un reģistra metadatus", + "Initialization failed" : "Inicializēšana neizdevās", + "Initialize Project" : "Inicializēt projektu", + "Inspect Fields" : "Pārbaudīt laukus", + "Inspect Index" : "Pārbaudīt indeksu", + "Invalid" : "Nederīgs", + "Invalid batch size. Must be between 1 and 5000" : "Nederīgs pakas izmērs. Jābūt no 1 līdz 5000", + "Invalid field name provided" : "Norādīts nederīgs lauka nosaukums", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Nederīgs maxObjects. Jābūt 0 (visi) vai pozitīvam skaitlim", + "Invalid:" : "Nederīgs:", + "Issue" : "Problēma", + "Items per page:" : "Vienumi lapā:", + "Just now" : "Tikko", + "LLM Configuration" : "LLM konfigurācija", + "LLM actions menu" : "LLM darbību izvēlne", + "LLM configuration saved successfully" : "LLM konfigurācija veiksmīgi saglabāta", + "LLM features disabled" : "LLM funkcijas atspējotas", + "LLM features enabled" : "LLM funkcijas iespējotas", + "LLM must be enabled with an embedding provider configured" : "LLM jābūt iespējotam ar konfigurētu iegultnes nodrošinātāju", + "LLM settings updated successfully" : "LLM iestatījumi veiksmīgi atjaunināti", + "Last" : "Pēdējais", + "Last Triggered" : "Pēdējoreiz aktivizēts", + "Length Range:" : "Garuma diapazons:", + "Linear" : "Lineārs", + "Load Advanced Filters" : "Ielādēt paplašinātos filtrus", + "Load advanced filters with live data from your search index" : "Ielādēt paplašinātos filtrus ar reāllaika datiem no jūsu meklēšanas indeksa", + "Load view" : "Ielādēt skatu", + "Loading ConfigSets..." : "Notiek ConfigSet ielāde...", + "Loading advanced filters..." : "Notiek paplašināto filtru ielāde...", + "Loading agents..." : "Notiek aģentu ielāde...", + "Loading application details..." : "Notiek lietotnes detaļu ielāde...", + "Loading applications..." : "Notiek lietotņu ielāde...", + "Loading archived conversations..." : "Notiek arhivēto sarunu ielāde...", + "Loading audit trails..." : "Notiek audita pierakstu ielāde...", + "Loading collections..." : "Notiek kolekciju ielāde...", + "Loading configurations..." : "Notiek konfigurāciju ielāde...", + "Loading conversation..." : "Notiek sarunas ielāde...", + "Loading conversations..." : "Notiek sarunu ielāde...", + "Loading deleted items..." : "Notiek dzēsto vienumu ielāde...", + "Loading events..." : "Notiek notikumu ielāde...", + "Loading filter data..." : "Notiek filtru datu ielāde...", + "Loading groups..." : "Notiek grupu ielāde...", + "Loading log details..." : "Notiek žurnāla detaļu ielāde...", + "Loading registers..." : "Notiek reģistru ielāde...", + "Loading schemas..." : "Notiek shēmu ielāde...", + "Loading search trails..." : "Notiek meklēšanas pierakstu ielāde...", + "Loading sources..." : "Notiek avotu ielāde...", + "Loading statistics..." : "Notiek statistikas ielāde...", + "Loading users..." : "Notiek lietotāju ielāde...", + "Loading views..." : "Notiek skatu ielāde...", + "Loading..." : "Notiek ielāde...", + "Local" : "Lokāls", + "Local Version" : "Lokālā versija", + "Locked" : "Bloķēts", + "Locked:" : "Bloķēts:", + "Login successful" : "Pieteikšanās veiksmīga", + "Logs" : "Žurnāli", + "Low" : "Zems", + "Low Confidence" : "Zema ticamība", + "Lucene Version:" : "Lucene versija:", + "Magic table synchronized successfully" : "Magic tabula veiksmīgi sinhronizēta", + "Manage Organisation Roles" : "Pārvaldīt organizācijas lomas", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Pārvaldiet SOLR kolekcijas (datu krātuves) un piešķiriet tās objektiem un failiem.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Pārvaldiet SOLR ConfigSet (konfigurācijas veidnes) savām kolekcijām.", + "Manage and configure agents for automated tasks" : "Pārvaldiet un konfigurējiet aģentus automatizētiem uzdevumiem", + "Manage and monitor file text extraction status" : "Pārvaldiet un pārraugiet failu teksta ekstrakcijas statusu", + "Manage and restore soft deleted items from your registers" : "Pārvaldiet un atjaunojiet maigi dzēstos vienumus no saviem reģistriem", + "Manage and view detected entities from files and objects" : "Pārvaldiet un skatiet no failiem un objektiem noteiktās entītijas", + "Manage document templates and themes" : "Pārvaldiet dokumentu veidnes un motīvus", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Pārvaldiet n8n darbplūsmas OpenRegister automatizācijai. Darbplūsmas tiks glabātas konfigurētajā projektā.", + "Manage webhooks for event-driven integrations" : "Pārvaldiet tīmekļa āķus uz notikumiem balstītām integrācijām", + "Manage your applications and modules" : "Pārvaldiet savas lietotnes un moduļus", + "Manage your data registers and their configurations" : "Pārvaldiet savus datu reģistrus un to konfigurācijas", + "Manage your data schemas and their properties" : "Pārvaldiet savas datu shēmas un to īpašības", + "Manage your data sources and their configurations" : "Pārvaldiet savus datu avotus un to konfigurācijas", + "Manage your saved search configurations" : "Pārvaldiet savas saglabātās meklēšanas konfigurācijas", + "Manage your system configurations and settings" : "Pārvaldiet savas sistēmas konfigurācijas un iestatījumus", + "Managed" : "Pārvaldīts", + "Max Files (0 = all)" : "Maks. faili (0 = visi)", + "Max Length" : "Maks. garums", + "Max Length:" : "Maks. garums:", + "Max Objects (0 = all)" : "Maks. objekti (0 = visi)", + "Max Retries" : "Maks. atkārtojumi", + "Max Shards/Node" : "Maks. lauskas/mezglā", + "Max execution time (ms)" : "Maks. izpildes laiks (ms)", + "Max ms" : "Maks. ms", + "Max result count" : "Maks. rezultātu skaits", + "Max results" : "Maks. rezultāti", + "Maximum" : "Maksimums", + "Maximum Nesting Depth" : "Maksimālais ligzdošanas dziļums", + "Maximum length constraint is missing" : "Trūkst maksimālā garuma ierobežojuma", + "Maximum length is too restrictive" : "Maksimālais garums ir pārāk ierobežojošs", + "Maximum number of files to process. Set to 0 to process all files." : "Maksimālais apstrādājamo failu skaits. Iestatiet uz 0, lai apstrādātu visus failus.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maksimālais apstrādājamo objektu skaits. Iestatiet uz 0, lai apstrādātu visus objektus.", + "Maximum number of retry attempts for failed deliveries" : "Maksimālais atkārtojuma mēģinājumu skaits neizdevušajām piegādēm", + "Maximum time to wait before committing changes" : "Maksimālais gaidīšanas laiks pirms izmaiņu apstiprināšanas", + "Maximum tokens to generate" : "Maksimālais ģenerējamo marķieru skaits", + "Maximum value constraint is missing" : "Trūkst maksimālās vērtības ierobežojuma", + "Maximum value is too restrictive" : "Maksimālā vērtība ir pārāk ierobežojoša", + "Medium" : "Vidējs", + "Medium Confidence" : "Vidēja ticamība", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Vidēji vaicājumi: meklējumi ar zināmu filtrēšanu vai vairākiem parametriem (piem., datumu diapazoni, konkrēti reģistri/shēmas)", + "Members of selected groups can access this view" : "Atlasīto grupu dalībnieki var piekļūt šim skatam", + "Members:" : "Dalībnieki:", + "Memory prediction calculated successfully" : "Atmiņas prognoze veiksmīgi aprēķināta", + "Message content is required" : "Ziņojuma saturs ir obligāts", + "Message does not belong to this conversation" : "Ziņojums nepieder šai sarunai", + "Metadata Filters" : "Metadatu filtri", + "Method" : "Metode", + "Min Length" : "Min. garums", + "Min execution time (ms)" : "Min. izpildes laiks (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. rezultātu skaits", + "Min results" : "Min. rezultāti", + "Minimum" : "Minimums", + "Minimum value constraint is missing" : "Trūkst minimālās vērtības ierobežojuma", + "Minimum value is too restrictive" : "Minimālā vērtība ir pārāk ierobežojoša", + "Missing object fields created successfully" : "Trūkstošie objekta lauki veiksmīgi izveidoti", + "Mode:" : "Režīms:", + "Model" : "Modelis", + "Model Name" : "Modeļa nosaukums", + "Modified" : "Modificēts", + "Monthly" : "Katru mēnesi", + "More sources (10-20):" : "Vairāk avotu (10-20):", + "Most Active Objects" : "Visaktīvākie objekti", + "Multiple Of" : "Daudzkārtnis no", + "Name" : "Nosaukums", + "Names cache warmup completed" : "Nosaukumu kešatmiņas iesildīšana pabeigta", + "New Collection Name" : "Jaunais kolekcijas nosaukums", + "New Conversation" : "Jauna saruna", + "New Properties" : "Jaunas īpašības", + "New Property" : "Jauna īpašība", + "New Value" : "Jauna vērtība", + "New users without specific organisation membership will be automatically added to this organisation" : "Jaunie lietotāji bez konkrētas organizācijas dalības tiks automātiski pievienoti šai organizācijai", + "Next" : "Nākamais", + "No" : "Nē", + "No ConfigSets found" : "Nav atrasts neviens ConfigSet", + "No GitHub token configured" : "Nav konfigurēts neviens GitHub marķieris", + "No GitHub token provided" : "Nav norādīts neviens GitHub marķieris", + "No GitLab token provided" : "Nav norādīts neviens GitLab marķieris", + "No Organisation" : "Nav organizācijas", + "No active filters" : "Nav aktīvu filtru", + "No activity data available" : "Nav pieejamu aktivitātes datu", + "No agents available" : "Nav pieejamu aģentu", + "No agents found" : "Nav atrasts neviens aģents", + "No app store cache exists yet" : "Vēl nepastāv lietotņu veikala kešatmiņa", + "No applications are available." : "Nav pieejama neviena lietotne.", + "No applications found" : "Nav atrasta neviena lietotne", + "No archived conversations" : "Nav arhivētu sarunu", + "No audit trail entries found" : "Nav atrasts neviens audita pieraksta ieraksts", + "No changes recorded" : "Nav reģistrētu izmaiņu", + "No chunks to vectorize" : "Nav vektorizējamu fragmentu", + "No collections found" : "Nav atrasta neviena kolekcija", + "No configuration data" : "Nav konfigurācijas datu", + "No configurations are available." : "Nav pieejama neviena konfigurācija.", + "No configurations found" : "Nav atrasta neviena konfigurācija", + "No configurations found for this application." : "Šai lietotnei nav atrasta neviena konfigurācija.", + "No conversations yet. Create a new one to get started!" : "Vēl nav sarunu. Izveidojiet jaunu, lai sāktu darbu!", + "No data available" : "Nav pieejamu datu", + "No data available for chart" : "Nav pieejamu datu diagrammai", + "No deleted items found" : "Nav atrasts neviens dzēsts vienums", + "No deletion data available" : "Nav pieejamu dzēšanas datu", + "No description found" : "Apraksts nav atrasts", + "No description provided" : "Apraksts nav norādīts", + "No entities detected in this file. Run text extraction first." : "Šajā failā nav noteiktas entītijas. Vispirms palaidiet teksta ekstrakciju.", + "No entities found" : "Nav atrasta neviena entītija", + "No entities have been detected yet" : "Vēl nav noteikta neviena entītija", + "No events found" : "Nav atrasts neviens notikums", + "No expired audit trails found to clear" : "Nav atrasts neviens notīrāms beidzies audita pieraksts", + "No facetable fields available. Select a register and schema to see available filters." : "Nav pieejamu fasetējamu lauku. Atlasiet reģistru un shēmu, lai redzētu pieejamos filtrus.", + "No files found" : "Nav atrasts neviens fails", + "No files have been extracted yet" : "Vēl nav ekstraģēts neviens fails", + "No files to process" : "Nav apstrādājamu failu", + "No files to reindex" : "Nav pārindeksējamu failu", + "No filters are currently active. This will delete ALL audit trail entries!" : "Pašlaik nav aktīvs neviens filtrs. Tas izdzēsīs VISUS audita pierakstu ierakstus!", + "No log entries found" : "Nav atrasts neviens žurnāla ieraksts", + "No logs are available for this configuration." : "Šai konfigurācijai nav pieejams neviens žurnāls.", + "No logs are available for this source." : "Šim avotam nav pieejams neviens žurnāls.", + "No logs found" : "Nav atrasts neviens žurnāls", + "No mismatched fields found - SOLR schema is properly configured" : "Nav atrasts neviens neatbilstošs lauks - SOLR shēma ir pareizi konfigurēta", + "No objects found" : "Nav atrasts neviens objekts", + "No objects found to vectorize" : "Nav atrasts neviens vektorizējams objekts", + "No objects selected for deletion" : "Nav atlasīts neviens objekts dzēšanai", + "No objects to delete" : "Nav dzēšamu objektu", + "No objects were permanently deleted" : "Neviens objekts netika neatgriezeniski izdzēsts", + "No properties found" : "Nav atrasta neviena īpašība", + "No properties match your filters." : "Neviena īpašība neatbilst jūsu filtriem.", + "No purge date set" : "Nav iestatīts iztīrīšanas datums", + "No register data available" : "Nav pieejamu reģistra datu", + "No registers found" : "Nav atrasts neviens reģistrs", + "No registers found for this application." : "Šai lietotnei nav atrasts neviens reģistrs.", + "No relations found" : "Nav atrasta neviena relācija", + "No request body available" : "Nav pieejama pieprasījuma pamatteksts", + "No response body available" : "Nav pieejama atbildes pamatteksts", + "No saved views yet. Create one in the Search tab!" : "Vēl nav saglabātu skatu. Izveidojiet vienu cilnē Meklēšana!", + "No schema data available" : "Nav pieejamu shēmas datu", + "No schema selected for exploration" : "Nav atlasīta neviena shēma izpētei", + "No schemas found" : "Nav atrasta neviena shēma", + "No schemas found for this application." : "Šai lietotnei nav atrasta neviena shēma.", + "No search terms data available" : "Nav pieejamu meklēšanas terminu datu", + "No search trail entries found" : "Nav atrasts neviens meklēšanas pieraksta ieraksts", + "No sources are available." : "Nav pieejams neviens avots.", + "No sources found" : "Nav atrasts neviens avots", + "No templates found" : "Nav atrasta neviena veidne", + "No templates have been created yet" : "Vēl nav izveidota neviena veidne", + "No valid magic-mapped register+schema combinations found" : "Nav atrasta neviena derīga magic-kartēta reģistra+shēmas kombinācija", + "No views found. Create views first before configuring vectorization." : "Nav atrasts neviens skats. Vispirms izveidojiet skatus, pirms konfigurējat vektorizāciju.", + "No views match your search" : "Neviens skats neatbilst jūsu meklējumam", + "No webhooks found" : "Nav atrasts neviens tīmekļa āķis", + "No webhooks have been configured yet" : "Vēl nav konfigurēts neviens tīmekļa āķis", + "No workflows found in this project. Create workflows in the n8n editor." : "Šajā projektā nav atrasta neviena darbplūsma. Izveidojiet darbplūsmas n8n redaktorā.", + "None" : "Nav", + "Not helpful" : "Nav noderīgs", + "Not in use" : "Netiek lietots", + "Number Constraints" : "Skaitļu ierobežojumi", + "Number of chunks to vectorize in one API call (1-100)." : "Vektorizējamo fragmentu skaits vienā API izsaukumā (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Vektorizējamo fragmentu skaits vienā API izsaukumā. Augstāks = ātrāks, bet vairāk atmiņas. Ieteicams: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Katrā pakā apstrādājamo objektu skaits (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Vektorizējamo objektu skaits vienā API izsaukumā. Augstāks = ātrāks, bet vairāk atmiņas. Ieteicams: 10-50.", + "Numeric Range:" : "Skaitliskais diapazons:", + "Object" : "Objekts", + "Object Collection" : "Objektu kolekcija", + "Object Count Prediction" : "Objektu skaita prognoze", + "Object ID" : "Objekta ID", + "Object Management" : "Objektu pārvaldība", + "Object Vectorization" : "Objektu vektorizācija", + "Object Warmup" : "Objektu iesildīšana", + "Object collection not configured" : "Objektu kolekcija nav konfigurēta", + "Object not found" : "Objekts nav atrasts", + "Object permanently deleted" : "Objekts neatgriezeniski izdzēsts", + "Object restored successfully" : "Objekts veiksmīgi atjaunots", + "Object settings updated successfully" : "Objektu iestatījumi veiksmīgi atjaunināti", + "Object sources" : "Objektu avoti", + "Object unlocked successfully" : "Objekts veiksmīgi atbloķēts", + "Object vectorization configuration saved successfully" : "Objektu vektorizācijas konfigurācija veiksmīgi saglabāta", + "Object vectorized successfully" : "Objekts veiksmīgi vektorizēts", + "Object:" : "Objekts:", + "Objects" : "Objekti", + "Objects Analyzed" : "Analizētie objekti", + "Objects Distribution" : "Objektu sadalījums", + "Objects being analyzed" : "Analizējamie objekti", + "Objects by Register" : "Objekti pēc reģistra", + "Objects by Schema" : "Objekti pēc shēmas", + "Objects deletion completed successfully" : "Objektu dzēšana veiksmīgi pabeigta", + "Objects in schema" : "Objekti shēmā", + "Objects to Process:" : "Apstrādājamie objekti:", + "Objects to be analyzed" : "Analizējamie objekti", + "Objects to be deleted" : "Dzēšamie objekti", + "Objects to be validated" : "Validējamie objekti", + "Objects will be serialized as JSON text before vectorization" : "Objekti pirms vektorizācijas tiks serializēti kā JSON teksts", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekti tiks maigi dzēsti (atzīmēti kā dzēsti, bet saglabāti datu bāzē). Tos vēlāk var atgūt, ja nepieciešams.", + "Old Value" : "Vecā vērtība", + "Oldest Item (days)" : "Vecākais vienums (dienas)", + "Ollama Chat Settings" : "Ollama tērzēšanas iestatījumi", + "Ollama Embedding Configuration" : "Ollama iegultnes konfigurācija", + "Ollama URL" : "Ollama URL", + "Only With Changes" : "Tikai ar izmaiņām", + "Open Register Objects" : "Open Register objekti", + "Open n8n Editor" : "Atvērt n8n redaktoru", + "OpenAI Chat Settings" : "OpenAI tērzēšanas iestatījumi", + "OpenAI Embedding Configuration" : "OpenAI iegultnes konfigurācija", + "OpenRegister Settings" : "OpenRegister iestatījumi", + "Optional webhook secret for signature verification" : "Neobligāts tīmekļa āķa noslēpums paraksta verifikācijai", + "Order" : "Secība", + "Organisation" : "Organizācija", + "Organisation ID" : "Organizācijas ID", + "Organisation Members" : "Organizācijas dalībnieki", + "Organisation Statistics" : "Organizācijas statistika", + "Organisation created successfully" : "Organizācija veiksmīgi izveidota", + "Organisation settings saved successfully" : "Organizācijas iestatījumi veiksmīgi saglabāti", + "Organisations" : "Organizācijas", + "Organization" : "Organizācija", + "Organization ID (Optional)" : "Organizācijas ID (neobligāti)", + "Orphaned Items" : "Bāreņu vienumi", + "Owner" : "Īpašnieks", + "Owner:" : "Īpašnieks:", + "Parallel Mode (Faster, more resource intensive)" : "Paralēlais režīms (ātrāks, resursietilpīgāks)", + "Parallel:" : "Paralēli:", + "Password" : "Parole", + "Password for SOLR authentication (optional)" : "Parole SOLR autentifikācijai (neobligāti)", + "Path" : "Ceļš", + "Path in repository" : "Ceļš repozitorijā", + "Path where the configuration file will be saved in the repository" : "Ceļš, kur repozitorijā tiks saglabāts konfigurācijas fails", + "Path where the register OAS file will be saved in the repository" : "Ceļš, kur repozitorijā tiks saglabāts reģistra OAS fails", + "Pattern (regex)" : "Šablons (regulārā izteiksme)", + "Pattern Issue" : "Šablona problēma", + "Pattern constraint is missing" : "Trūkst šablona ierobežojuma", + "Patterns:" : "Šabloni:", + "Pending" : "Gaida", + "Permanent Delete Mode" : "Neatgriezeniskās dzēšanas režīms", + "Permanently Delete" : "Dzēst neatgriezeniski", + "Permanently delete already soft-deleted objects" : "Neatgriezeniski dzēst jau maigi dzēstos objektus", + "Permanently delete this conversation?" : "Neatgriezeniski dzēst šo sarunu?", + "Person" : "Persona", + "Personal Data" : "Personas dati", + "Phone" : "Tālrunis", + "Photos" : "Fotoattēli", + "Please create an agent in the" : "Lūdzu, izveidojiet aģentu", + "Please select an agent to continue" : "Lūdzu, atlasiet aģentu, lai turpinātu", + "Please select which register and schema to use for the new object" : "Lūdzu, atlasiet, kuru reģistru un shēmu izmantot jaunajam objektam", + "Please try again later." : "Lūdzu, mēģiniet vēlreiz vēlāk.", + "Please wait while we fetch your agents." : "Lūdzu, uzgaidiet, kamēr ielādējam jūsu aģentus.", + "Please wait while we fetch your applications." : "Lūdzu, uzgaidiet, kamēr ielādējam jūsu lietotnes.", + "Please wait while we fetch your configurations." : "Lūdzu, uzgaidiet, kamēr ielādējam jūsu konfigurācijas.", + "Please wait while we fetch your deleted items." : "Lūdzu, uzgaidiet, kamēr ielādējam jūsu dzēstos vienumus.", + "Please wait while we fetch your sources." : "Lūdzu, uzgaidiet, kamēr ielādējam jūsu avotus.", + "Polls" : "Aptaujas", + "Popular Search Terms" : "Populārie meklēšanas termini", + "Port" : "Ports", + "Prerequisites" : "Priekšnosacījumi", + "Press Enter to send, Shift+Enter for new line" : "Nospiediet Enter, lai sūtītu, Shift+Enter jaunai rindai", + "Previous" : "Iepriekšējais", + "Private" : "Privāts", + "Processes chunks in batches with simulated parallelism." : "Apstrādā fragmentus pakās ar simulētu paralēlismu.", + "Processes file chunks sequentially (safest)." : "Apstrādā faila fragmentus secīgi (visdrošāk).", + "Processes objects in chunks with simulated parallelism." : "Apstrādā objektus fragmentos ar simulētu paralēlismu.", + "Processes objects sequentially (safest)." : "Apstrādā objektus secīgi (visdrošāk).", + "Processing" : "Notiek apstrāde", + "Processing Limits" : "Apstrādes ierobežojumi", + "Processing..." : "Notiek apstrāde...", + "Project initialized successfully" : "Projekts veiksmīgi inicializēts", + "Project not found. Please initialize first." : "Projekts nav atrasts. Lūdzu, vispirms inicializējiet.", + "Properties" : "Īpašības", + "Property Behaviors" : "Īpašību uzvedības", + "Property Configuration:" : "Īpašības konfigurācija:", + "Property Filters" : "Īpašību filtri", + "Property Title" : "Īpašības nosaukums", + "Property Type" : "Īpašības veids", + "Property can be improved" : "Īpašību var uzlabot", + "Protected" : "Aizsargāts", + "Provider" : "Nodrošinātājs", + "Provider is required for testing" : "Testēšanai ir obligāts nodrošinātājs", + "Public" : "Publisks", + "Public View" : "Publisks skats", + "Public views can be accessed by anyone in the system" : "Publiskiem skatiem var piekļūt jebkurš sistēmā", + "Publish" : "Publicēt", + "Publish Objects" : "Publicēt objektus", + "Publish all objects in this schema" : "Publicēt visus objektus šajā shēmā", + "Published" : "Publicēts", + "Published:" : "Publicēts:", + "Publishing..." : "Notiek publicēšana...", + "Purge" : "Iztīrīt", + "Purge Date" : "Iztīrīšanas datums", + "Query Complexity Distribution" : "Vaicājumu sarežģītības sadalījums", + "Query parameter is required" : "Vaicājuma parametrs ir obligāts", + "RAG" : "RAG", + "RAG Configuration" : "RAG konfigurācija", + "Rate limits cleared successfully" : "Ātruma ierobežojumi veiksmīgi notīrīti", + "Raw Changes Data" : "Neapstrādāti izmaiņu dati", + "Re-vectorize on object update" : "Atkārtoti vektorizēt objekta atjaunināšanas brīdī", + "Read" : "Lasīt", + "Recommendations:" : "Ieteikumi:", + "Recommended Type:" : "Ieteicamais veids:", + "Recommended: 5 sources" : "Ieteicams: 5 avoti", + "Refresh" : "Atsvaidzināt", + "Refresh Data" : "Atsvaidzināt datus", + "Refresh Stats" : "Atsvaidzināt statistiku", + "Refresh Workflows" : "Atsvaidzināt darbplūsmas", + "Refresh dashboard" : "Atsvaidzināt informācijas paneli", + "Refresh database info" : "Atsvaidzināt datu bāzes informāciju", + "Register" : "Reģistrs", + "Register ID" : "Reģistra ID", + "Register Statistics" : "Reģistra statistika", + "Register Totals" : "Reģistra kopsummas", + "Register depublished successfully" : "Reģistrs veiksmīgi atcelta publicēšana", + "Register not found" : "Reģistrs nav atrasts", + "Register objects deletion completed successfully" : "Reģistra objektu dzēšana veiksmīgi pabeigta", + "Register published successfully" : "Reģistrs veiksmīgi publicēts", + "Register/Schema Usage" : "Reģistra/shēmas lietojums", + "Registers" : "Reģistri", + "Reindex" : "Pārindeksēt", + "Reindex completed" : "Pārindeksēšana pabeigta", + "Reindex completed successfully" : "Pārindeksēšana veiksmīgi pabeigta", + "Reindex failed" : "Pārindeksēšana neizdevās", + "Relations" : "Relācijas", + "Remote Version" : "Attālā versija", + "Remove" : "Noņemt", + "Remove filter" : "Noņemt filtru", + "Remove from favorites" : "Noņemt no izlases", + "Remove group" : "Noņemt grupu", + "Removed" : "Noņemts", + "Removed from favorites" : "Noņemts no izlases", + "Rename Conversation" : "Pārdēvēt sarunu", + "Rename conversation" : "Pārdēvēt sarunu", + "Replicas" : "Replikas", + "Repository" : "Repozitorijs", + "Request Body" : "Pieprasījuma pamatteksts", + "Request Data" : "Pieprasījuma dati", + "Request Details" : "Pieprasījuma detaļas", + "Request body must be JSON with a \"query\" field" : "Pieprasījuma pamattekstam jābūt JSON ar lauku \"query\"", + "Request timeout in seconds" : "Pieprasījuma noildze sekundēs", + "Required field" : "Obligāts lauks", + "Required status is inconsistent" : "Obligātais statuss ir nekonsekvents", + "Rerun Search" : "Atkārtot meklēšanu", + "Reset Changes" : "Atiestatīt izmaiņas", + "Reset Filters" : "Atiestatīt filtrus", + "Response Body" : "Atbildes pamatteksts", + "Response Details" : "Atbildes detaļas", + "Response Time:" : "Atbildes laiks:", + "Restore" : "Atjaunot", + "Restore conversation" : "Atjaunot sarunu", + "Restore or permanently delete items" : "Atjaunot vai neatgriezeniski dzēst vienumus", + "Result Count Range" : "Rezultātu skaita diapazons", + "Results" : "Rezultāti", + "Retrieve all objects for this schema" : "Izgūt visus objektus šai shēmai", + "Retry" : "Mēģināt vēlreiz", + "Retry Failed Extractions" : "Atkārtot neizdevušās ekstrakcijas", + "Retry Policy" : "Atkārtošanas politika", + "Retry completed" : "Atkārtošana pabeigta", + "Retrying..." : "Notiek atkārtošana...", + "Risk Level" : "Riska līmenis", + "Roles updated successfully" : "Lomas veiksmīgi atjauninātas", + "SOLR Connection Settings" : "SOLR savienojuma iestatījumi", + "SOLR Version:" : "SOLR versija:", + "SOLR actions menu" : "SOLR darbību izvēlne", + "SOLR base path (usually /solr)" : "SOLR bāzes ceļš (parasti /solr)", + "SOLR is disabled" : "SOLR ir atspējots", + "SOLR is not available or not configured" : "SOLR nav pieejams vai nav konfigurēts", + "SOLR logging disabled" : "SOLR žurnālošana atspējota", + "SOLR logging enabled" : "SOLR žurnālošana iespējota", + "SOLR search disabled" : "SOLR meklēšana atspējota", + "SOLR search enabled" : "SOLR meklēšana iespējota", + "SOLR server hostname or IP address" : "SOLR servera resursdatora nosaukums vai IP adrese", + "SOLR server port number (optional, defaults to 8983)" : "SOLR servera porta numurs (neobligāti, noklusējums 8983)", + "SOLR setup completed successfully" : "SOLR iestatīšana veiksmīgi pabeigta", + "SOLR setup failed" : "SOLR iestatīšana neizdevās", + "SOLR setup failed - check logs" : "SOLR iestatīšana neizdevās - pārbaudiet žurnālus", + "Sample Values:" : "Paraugvērtības:", + "Save" : "Saglabāt", + "Save Configuration" : "Saglabāt konfigurāciju", + "Save Connection Settings" : "Saglabāt savienojuma iestatījumus", + "Save Roles" : "Saglabāt lomas", + "Save Settings" : "Saglabāt iestatījumus", + "Save current search as view" : "Saglabāt pašreizējo meklējumu kā skatu", + "Saved Views" : "Saglabātie skati", + "Saving..." : "Notiek saglabāšana...", + "Schema" : "Shēma", + "Schema ID" : "Shēmas ID", + "Schema Name" : "Shēmas nosaukums", + "Schema Statistics" : "Shēmas statistika", + "Schema depublished successfully" : "Shēmai veiksmīgi atcelta publicēšana", + "Schema objects deletion completed successfully" : "Shēmas objektu dzēšana veiksmīgi pabeigta", + "Schema published successfully" : "Shēma veiksmīgi publicēta", + "Schema successfully created" : "Shēma veiksmīgi izveidota", + "Schema successfully updated" : "Shēma veiksmīgi atjaunināta", + "Schemas" : "Shēmas", + "Scheme" : "Shēma", + "Search / Views" : "Meklēšana / skati", + "Search Activity" : "Meklēšanas aktivitāte", + "Search Analytics" : "Meklēšanas analītika", + "Search Entities" : "Meklēt entītijas", + "Search Files" : "Meklēt failus", + "Search GitHub" : "Meklēt GitHub", + "Search GitLab" : "Meklēt GitLab", + "Search Method" : "Meklēšanas metode", + "Search Term" : "Meklēšanas termins", + "Search Trail Details" : "Meklēšanas pieraksta detaļas", + "Search Trail Management" : "Meklēšanas pierakstu pārvaldība", + "Search Trail Statistics" : "Meklēšanas pierakstu statistika", + "Search Trails" : "Meklēšanas pieraksti", + "Search Views" : "Meklēt skatus", + "Search Webhooks" : "Meklēt tīmekļa āķus", + "Search by file name or path" : "Meklēt pēc faila nosaukuma vai ceļa", + "Search by name or URL" : "Meklēt pēc nosaukuma vai URL", + "Search by value" : "Meklēt pēc vērtības", + "Search in Files" : "Meklēt failos", + "Search in Objects" : "Meklēt objektos", + "Search property names..." : "Meklēt īpašību nosaukumus...", + "Search trail deletion not implemented yet" : "Meklēšanas pierakstu dzēšana vēl nav ieviesta", + "Search views..." : "Meklēt skatus...", + "Secret" : "Noslēpums", + "Secret key for HMAC signature generation (optional)" : "Slepenā atslēga HMAC paraksta ģenerēšanai (neobligāti)", + "Select AI Agent" : "Atlasīt MI aģentu", + "Select All" : "Atlasīt visus", + "Select ConfigSet" : "Atlasīt ConfigSet", + "Select File Types to Vectorize:" : "Atlasiet vektorizējamos failu veidus:", + "Select HTTP method" : "Atlasiet HTTP metodi", + "Select Register and Schema" : "Atlasīt reģistru un shēmu", + "Select Views to Vectorize:" : "Atlasiet vektorizējamos skatus:", + "Select a Nextcloud group to add" : "Atlasiet pievienojamo Nextcloud grupu", + "Select a branch" : "Atlasiet zaru", + "Select a model or type a custom model name" : "Atlasiet modeli vai ievadiet pielāgotu modeļa nosaukumu", + "Select a repository" : "Atlasiet repozitoriju", + "Select a repository you have write access to" : "Atlasiet repozitoriju, kuram jums ir rakstīšanas piekļuve", + "Select an AI Agent" : "Atlasiet MI aģentu", + "Select an AI agent to begin chatting with your data." : "Atlasiet MI aģentu, lai sāktu tērzēšanu ar saviem datiem.", + "Select backend" : "Atlasiet aizmugursistēmu", + "Select chat model" : "Atlasiet tērzēšanas modeli", + "Select collection for files" : "Atlasiet kolekciju failiem", + "Select collection for objects" : "Atlasiet kolekciju objektiem", + "Select default organisation" : "Atlasiet noklusējuma organizāciju", + "Select event to listen to..." : "Atlasiet notikumu, ko klausīties...", + "Select model" : "Atlasiet modeli", + "Select options..." : "Atlasiet opcijas...", + "Select period" : "Atlasiet periodu", + "Select property to send as payload" : "Atlasiet īpašību, ko sūtīt kā lietderīgo slodzi", + "Select provider" : "Atlasiet nodrošinātāju", + "Select registers and schemas to save a view" : "Atlasiet reģistrus un shēmas, lai saglabātu skatu", + "Select retry policy" : "Atlasiet atkārtošanas politiku", + "Select the branch to publish to" : "Atlasiet zaru, kurā publicēt", + "Select the event this webhook should listen to" : "Atlasiet notikumu, ko šim tīmekļa āķim jāklausās", + "Select views to vectorize:" : "Atlasiet vektorizējamos skatus:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Atlasiet, kuras Nextcloud grupas ir pieejamas šai organizācijai. Šo grupu lietotājiem būs piekļuve organizācijas resursiem.", + "Select which data views the AI can search" : "Atlasiet, kuros datu skatos MI var meklēt", + "Select which property from the event should be used as the webhook payload data" : "Atlasiet, kura notikuma īpašība jāizmanto kā tīmekļa āķa lietderīgās slodzes dati", + "Select which tools the AI can use to perform actions" : "Atlasiet, kurus rīkus MI var izmantot darbību veikšanai", + "Selected" : "Atlasīts", + "Selected Groups" : "Atlasītās grupas", + "Selected audit trails deleted successfully" : "Atlasītie audita pieraksti veiksmīgi izdzēsti", + "Selected search trails deleted successfully" : "Atlasītie meklēšanas pieraksti veiksmīgi izdzēsti", + "Selected users can access this view" : "Atlasītie lietotāji var piekļūt šim skatam", + "Send additional feedback" : "Nosūtīt papildu atsauksmi", + "Send as CloudEvent" : "Sūtīt kā CloudEvent", + "Sensitive PII" : "Sensitīva PII", + "Serial Mode (Safer, slower)" : "Secīgais režīms (drošāks, lēnāks)", + "Serial:" : "Secīgi:", + "Server Configuration" : "Servera konfigurācija", + "Server Information" : "Servera informācija", + "Settings" : "Iestatījumi", + "Shards" : "Lauskas", + "Share" : "Kopīgot", + "Share with Groups" : "Kopīgot ar grupām", + "Share with Users" : "Kopīgot ar lietotājiem", + "Show Filters" : "Rādīt filtrus", + "Show less" : "Rādīt mazāk", + "Show only entries with changes" : "Rādīt tikai ierakstus ar izmaiņām", + "Simple" : "Vienkāršs", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Vienkārši vaicājumi: pamata teksta meklējumi ar minimāliem parametriem (piem., viens meklēšanas termins, bez paplašinātiem filtriem)", + "Size" : "Izmērs", + "Slug" : "Slug", + "Soft Delete Mode" : "Maigās dzēšanas režīms", + "Soft Deleted Items" : "Maigi dzēstie vienumi", + "SolrCloud mode enabled" : "SolrCloud režīms iespējots", + "Source" : "Avots", + "Sources" : "Avoti", + "Sources:" : "Avoti:", + "Standalone SOLR mode" : "Atsevišķs SOLR režīms", + "Start Conversation" : "Sākt sarunu", + "Start Vectorization" : "Sākt vektorizāciju", + "Start a conversation" : "Sākt sarunu", + "Starting names cache warmup..." : "Notiek nosaukumu kešatmiņas iesildīšanas sākšana...", + "Starting..." : "Notiek sākšana...", + "Statistics" : "Statistika", + "Stats calculation not yet implemented" : "Statistikas aprēķins vēl nav ieviests", + "Status" : "Statuss", + "Status Code" : "Statusa kods", + "String Constraints" : "Virkņu ierobežojumi", + "Success" : "Veiksmīgi", + "Success Rate" : "Veiksmes rādītājs", + "Success Status" : "Veiksmes statuss", + "Successful" : "Veiksmīgs", + "Successfully joined organisation" : "Veiksmīgi pievienojies organizācijai", + "Sync Table" : "Sinhronizēt tabulu", + "Synchronization Settings" : "Sinhronizācijas iestatījumi", + "System Default" : "Sistēmas noklusējums", + "Technical Description" : "Tehniskais apraksts", + "Technical description for developers and administrators" : "Tehniskais apraksts izstrādātājiem un administratoriem", + "Temperature" : "Temperatūra", + "Templates" : "Veidnes", + "Test" : "Testēt", + "Test Chat" : "Testēt tērzēšanu", + "Test Connection" : "Testēt savienojumu", + "Test Embedding" : "Testēt iegultni", + "Test endpoint executed successfully" : "Testa galapunkts veiksmīgi izpildīts", + "Test webhook delivered successfully" : "Testa tīmekļa āķis veiksmīgi piegādāts", + "Test webhook delivery failed" : "Testa tīmekļa āķa piegāde neizdevās", + "Test webhook sent successfully" : "Testa tīmekļa āķis veiksmīgi nosūtīts", + "Testing..." : "Notiek testēšana...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Teksta fragmenti tiek ģenerēti faila ekstrakcijas laikā un glabāti datu bāzē. Vektorizācija nolasa šos iepriekš sadalītos failus un pārvērš tos iegultnēs.", + "Text extracted successfully" : "Teksts veiksmīgi ekstraģēts", + "Text extraction disabled" : "Teksta ekstrakcija atspējota", + "The URL where webhook events will be sent" : "URL, uz kuru tiks sūtīti tīmekļa āķa notikumi", + "The requested conversation does not exist" : "Pieprasītā saruna nepastāv", + "The saved view and all its search configuration" : "Saglabātais skats un visa tā meklēšanas konfigurācija", + "There are no audit trail entries matching your current filters." : "Nav audita pierakstu ierakstu, kas atbilst jūsu pašreizējiem filtriem.", + "There are no deleted items matching your current filters." : "Nav dzēsto vienumu, kas atbilst jūsu pašreizējiem filtriem.", + "There are no search trail entries matching your current filters." : "Nav meklēšanas pierakstu ierakstu, kas atbilst jūsu pašreizējiem filtriem.", + "There are no webhook log entries matching your filters." : "Nav tīmekļa āķu žurnāla ierakstu, kas atbilst jūsu filtriem.", + "This action cannot be undone." : "Šo darbību nevar atsaukt.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Šo darbību nevar atsaukt. Pārliecinieties, ka neviena kolekcija neizmanto šo ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Šim aģentam nav konfigurējamu skatu vai rīku, taču jūs joprojām varat pielāgot RAG iestatījumus zemāk.", + "This analysis may take some time" : "Šī analīze var aizņemt zināmu laiku", + "This audit trail entry does not contain any change information." : "Šis audita pieraksta ieraksts nesatur nekādu izmaiņu informāciju.", + "This configuration has no data defined." : "Šai konfigurācijai nav definēti dati.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Šis galapunkts ir novecojis. Tā vietā izmantojiet uz fragmentiem balstītus galapunktus.", + "This entity has no relations to objects or files" : "Šai entītijai nav relāciju ar objektiem vai failiem", + "This is a test webhook from OpenRegister" : "Šis ir testa tīmekļa āķis no OpenRegister", + "This schema must use Magic Table configuration to sync" : "Šai shēmai jāizmanto Magic Table konfigurācija, lai sinhronizētu", + "This source has no associated registers." : "Šim avotam nav saistītu reģistru.", + "This will copy the _default ConfigSet with the new name" : "Tas nokopēs _default ConfigSet ar jauno nosaukumu", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Tas neatgriezeniski izdzēsīs VISAS iegultnes (vektorus) no datu bāzes. Jums būs atkārtoti jāvektorizē visi objekti un faili. Šo darbību nevar atsaukt.\\n\\nVai tiešām vēlaties turpināt?", + "This will permanently delete:" : "Tas neatgriezeniski izdzēsīs:", + "Timeout (seconds)" : "Noildze (sekundes)", + "Timestamp" : "Laika zīmogs", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Padoms: iespējojiet tikai tos skatus, kuriem nepieciešama semantiskā meklēšana, lai minimizētu iegultņu izmaksas. Vienkāršām uzmeklēšanas tabulām vektorizācija reti ir nepieciešama.", + "Title" : "Nosaukums", + "To Date" : "Līdz datumam", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Lai pielāgotu fragmenta izmēru vai stratēģiju, dodieties uz Failu konfigurācija → Apstrādes ierobežojumi.", + "To change the embedding provider or model, go to LLM Configuration." : "Lai mainītu iegultnes nodrošinātāju vai modeli, dodieties uz LLM konfigurācija.", + "To date" : "Līdz datumam", + "Toggle search sidebar" : "Pārslēgt meklēšanas sānjoslu", + "Tools" : "Rīki", + "Top Deleters" : "Aktīvākie dzēsēji", + "Total" : "Kopā", + "Total Audit Trails" : "Kopējie audita pieraksti", + "Total Chunks Available:" : "Kopā pieejamie fragmenti:", + "Total Deleted Items" : "Kopējie dzēstie vienumi", + "Total Members" : "Kopējie dalībnieki", + "Total Objects" : "Kopējie objekti", + "Total Objects in Database:" : "Kopā objektu datu bāzē:", + "Total Organisations" : "Kopējās organizācijas", + "Total Results" : "Kopējie rezultāti", + "Total Searches" : "Kopējie meklējumi", + "Total Size" : "Kopējais izmērs", + "Total Storage" : "Kopējā krātuve", + "Total:" : "Kopā:", + "Totals" : "Kopsummas", + "Type" : "Veids", + "Type Issue" : "Veida problēma", + "Type Variations:" : "Veidu variācijas:", + "Type must be \"positive\" or \"negative\"" : "Veidam jābūt \"positive\" vai \"negative\"", + "Type to search for groups" : "Rakstiet, lai meklētu grupas", + "Type to search for users" : "Rakstiet, lai meklētu lietotājus", + "Type to search groups..." : "Rakstiet, lai meklētu grupas...", + "Type to search users..." : "Rakstiet, lai meklētu lietotājus...", + "URL" : "URL", + "URL where Ollama is running" : "URL, kurā darbojas Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Nemainīts", + "Unique Search Terms" : "Unikālie meklēšanas termini", + "Unique Terms" : "Unikālie termini", + "Unknown" : "Nezināms", + "Unknown Register" : "Nezināms reģistrs", + "Unknown Schema" : "Nezināma shēma", + "Untitled View" : "Skats bez nosaukuma", + "Update" : "Atjaunināt", + "Update Operations" : "Atjaunināšanas darbības", + "Update configuration: ..." : "Atjaunināt konfigurāciju: ...", + "Update register OAS: ..." : "Atjaunināt reģistra OAS: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Atjaunināt vektorus, kad mainās objekta dati (ieteicams precīzai meklēšanai)", + "Updated" : "Atjaunināts", + "Updated At" : "Atjaunināts", + "Updated:" : "Atjaunināts:", + "Uptime:" : "Darbības laiks:", + "Usage Count" : "Lietojuma skaits", + "Use SolrCloud with Zookeeper for distributed search" : "Izmantot SolrCloud ar Zookeeper sadalītai meklēšanai", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Izmantojiet filtrus, lai sašaurinātu audita pierakstu ierakstus pēc reģistra, shēmas, darbības veida, lietotāja, datumu diapazona vai objekta ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Izmantojiet filtrus, lai sašaurinātu dzēstos vienumus pēc reģistra, shēmas, dzēšanas datuma vai lietotāja, kurš tos dzēsa.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Izmantojiet filtrus, lai sašaurinātu meklēšanas pierakstu ierakstus pēc reģistra, shēmas, veiksmes statusa, lietotāja, datumu diapazona, meklēšanas terminiem vai veiktspējas rādītājiem.", + "Used By Collections" : "Izmanto kolekcijas", + "User" : "Lietotājs", + "User Agent Statistics" : "Lietotāja aģenta statistika", + "User Description" : "Lietotāja apraksts", + "User rate limits cleared successfully" : "Lietotāja ātruma ierobežojumi veiksmīgi notīrīti", + "User removed successfully" : "Lietotājs veiksmīgi noņemts", + "User-friendly description shown in forms and help text" : "Lietotājam draudzīgs apraksts, kas tiek rādīts formās un palīdzības tekstā", + "Username" : "Lietotājvārds", + "Username for SOLR authentication (optional)" : "Lietotājvārds SOLR autentifikācijai (neobligāti)", + "Users" : "Lietotāji", + "Using Pre-Generated Chunks" : "Izmanto iepriekš ģenerētus fragmentus", + "Validate" : "Validēt", + "Validate Objects" : "Validēt objektus", + "Validation completed successfully" : "Validācija veiksmīgi pabeigta", + "Validation failed" : "Validācija neizdevās", + "Value" : "Vērtība", + "Vector Dimensions" : "Vektora dimensijas", + "Vector Search Backend" : "Vektoru meklēšanas aizmugursistēma", + "Vector field: _embedding_" : "Vektora lauks: _embedding_", + "Vectorization Triggers" : "Vektorizācijas aktivizatori", + "Vectorize All Files" : "Vektorizēt visus failus", + "Vectorize All Objects" : "Vektorizēt visus objektus", + "Vectorize all file types" : "Vektorizēt visus failu veidus", + "Vectorize all views" : "Vektorizēt visus skatus", + "Vectorize on object creation" : "Vektorizēt objekta izveides brīdī", + "Vectors will be stored in your existing object and file collections" : "Vektori tiks glabāti jūsu esošajās objektu un failu kolekcijās", + "Version" : "Versija", + "Very High" : "Ļoti augsts", + "View API Docs" : "Skatīt API dokumentāciju", + "View Changes" : "Skatīt izmaiņas", + "View Details" : "Skatīt detaļas", + "View Error" : "Skatīt kļūdu", + "View File" : "Skatīt failu", + "View Full Details" : "Skatīt visas detaļas", + "View Logs" : "Skatīt žurnālus", + "View Name" : "Skata nosaukums", + "View Object" : "Skatīt objektu", + "View Parameters" : "Skatīt parametrus", + "View Selection" : "Skatu atlase", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Skatiet un analizējiet meklēšanas pierakstu žurnālus ar paplašinātām filtrēšanas un analītikas iespējām", + "View and analyze system audit trails with advanced filtering capabilities" : "Skatiet un analizējiet sistēmas audita pierakstus ar paplašinātām filtrēšanas iespējām", + "View deleted successfully" : "Skats veiksmīgi izdzēsts", + "View entity details and manage relations" : "Skatiet entītijas detaļas un pārvaldiet relācijas", + "View name" : "Skata nosaukums", + "View name is required" : "Skata nosaukums ir obligāts", + "View saved successfully!" : "Skats veiksmīgi saglabāts!", + "View search analytics and manage search logs" : "Skatiet meklēšanas analītiku un pārvaldiet meklēšanas žurnālus", + "View successfully deleted" : "Skats veiksmīgi izdzēsts", + "View successfully updated" : "Skats veiksmīgi atjaunināts", + "View updated successfully!" : "Skats veiksmīgi atjaunināts!", + "View webhook delivery logs and filter by webhook" : "Skatiet tīmekļa āķu piegādes žurnālus un filtrējiet pēc tīmekļa āķa", + "Views" : "Skati", + "Visible to users" : "Redzams lietotājiem", + "Wait for Response" : "Gaidīt atbildi", + "Wait for webhook response before continuing (required for request/response flows)" : "Gaidīt tīmekļa āķa atbildi pirms turpināšanas (nepieciešams pieprasījuma/atbildes plūsmām)", + "Warmup Names Cache" : "Iesildīt nosaukumu kešatmiņu", + "Warning" : "Brīdinājums", + "Warning:" : "Brīdinājums:", + "Webhook" : "Tīmekļa āķis", + "Webhook Log Details" : "Tīmekļa āķa žurnāla detaļas", + "Webhook Logs" : "Tīmekļa āķu žurnāli", + "Webhook created successfully" : "Tīmekļa āķis veiksmīgi izveidots", + "Webhook deleted" : "Tīmekļa āķis izdzēsts", + "Webhook retry delivered successfully" : "Tīmekļa āķa atkārtojums veiksmīgi piegādāts", + "Webhook retry delivery failed" : "Tīmekļa āķa atkārtojuma piegāde neizdevās", + "Webhook updated" : "Tīmekļa āķis atjaunināts", + "Webhook updated successfully" : "Tīmekļa āķis veiksmīgi atjaunināts", + "Webhooks" : "Tīmekļa āķi", + "Weekly" : "Katru nedēļu", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Ietvert tīmekļa āķa lietderīgo slodzi CloudEvents formātā labākai sadarbspējai", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Jā", + "You" : "Jūs", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Jūs varat izveidot jaunus ConfigSet, balstoties uz _default veidni, vai augšupielādēt pielāgotus tieši savā SOLR serverī.", + "You do not have access to this conversation" : "Jums nav piekļuves šai sarunai", + "You do not have permission to delete this conversation" : "Jums nav atļaujas izdzēst šo sarunu", + "You do not have permission to modify this conversation" : "Jums nav atļaujas modificēt šo sarunu", + "You do not have permission to restore this conversation" : "Jums nav atļaujas atjaunot šo sarunu", + "You must be logged in to favorite views" : "Lai pievienotu skatus izlasei, jums jābūt pieteiktam", + "You need an AI agent to start a conversation." : "Lai sāktu sarunu, jums nepieciešams MI aģents.", + "Your Fireworks AI API key. Get one at" : "Jūsu Fireworks AI API atslēga. Iegūstiet to vietnē", + "Your OpenAI API key. Get one at" : "Jūsu OpenAI API atslēga. Iegūstiet to vietnē", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Jūsu atsauksme ir reģistrēta. Pēc izvēles šeit varat sniegt papildu detaļas...", + "Zookeeper Hosts" : "Zookeeper resursdatori", + "Zookeeper Port" : "Zookeeper ports", + "Zookeeper Settings (SolrCloud)" : "Zookeeper iestatījumi (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper savienojuma virkne SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper porta numurs (neobligāti, noklusējums 2181)", + "chunk" : "fragments", + "chunks" : "fragmenti", + "chunks processed" : "apstrādāti fragmenti", + "configuration(s)" : "konfigurācija(s)", + "e.g., lib/Settings/config.json" : "piem., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "piem., lib/Settings/register.json", + "failed" : "neizdevās", + "file" : "fails", + "files" : "faili", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "dalībnieki", + "menu or contact someone with permission to create agents." : "izvēlnē vai sazinieties ar kādu, kuram ir atļauja izveidot aģentus.", + "messages" : "ziņojumi", + "n8n URL and API key are required" : "n8n URL un API atslēga ir obligāti", + "n8n configuration saved successfully" : "n8n konfigurācija veiksmīgi saglabāta", + "n8n connection not configured" : "n8n savienojums nav konfigurēts", + "n8n connection successful" : "n8n savienojums veiksmīgs", + "n8n connection test successful" : "n8n savienojuma tests veiksmīgs", + "n8n integration disabled" : "n8n integrācija atspējota", + "n8n integration enabled" : "n8n integrācija iespējota", + "n8n project initialized successfully" : "n8n projekts veiksmīgi inicializēts", + "n8n settings saved successfully" : "n8n iestatījumi veiksmīgi saglabāti", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekti", + "objects processed" : "apstrādāti objekti", + "or pick a range" : "vai izvēlieties diapazonu", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "nodrošina labu līdzsvaru starp ātrumu un precizitāti.", + "register(s)" : "reģistrs(i)", + "register(s) selected" : "atlasīts(i) reģistrs(i)", + "required" : "obligāts", + "results" : "rezultāti", + "schema(s)" : "shēma(s)", + "schema(s) selected" : "atlasīta(s) shēma(s)", + "sk-..." : "sk-...", + "this application" : "šī lietotne", + "to" : "līdz", + "vectorized" : "vektorizēts", + "views selected" : "atlasīti skati", + "ℹ️ Current Configuration" : "ℹ️ Pašreizējā konfigurācija", + "⚡ Batch Processing" : "⚡ Pakešu apstrāde", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Visaptverošs konteksts ✓ Mazāka iespēja palaist garām detaļas ✗ Lēnākas atbildes ✗ Var ietvert mazāk atbilstošu informāciju", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Ātrākas atbildes ✓ Mērķtiecīgākas atbildes ✗ Var palaist garām atbilstošu informāciju", + "✨ AI Features" : "✨ MI funkcijas", + "All search trails cleared successfully" : "Visi meklēšanas pieraksti veiksmīgi notīrīti", + "Multiple search trail deletion not implemented yet" : "Vairāku meklēšanas pierakstu dzēšana vēl nav ieviesta", + "No expired search trails found to clear" : "Nav atrasts neviens notīrāms beidzies meklēšanas pieraksts", + "Missing message" : "Trūkst ziņojuma", + "message content is required" : "ziņojuma saturs ir obligāts", + "Missing conversation or agentUuid" : "Trūkst sarunas vai agentUuid", + "Access denied" : "Piekļuve liegta", + "Conversation not found" : "Saruna nav atrasta", + "AI service not configured" : "MI pakalpojums nav konfigurēts", + "Failed to process message" : "Neizdevās apstrādāt ziņojumu", + "Missing conversationId" : "Trūkst conversationId", + "conversationId is required" : "conversationId ir obligāts", + "Failed to fetch conversation history" : "Neizdevās ielādēt sarunas vēsturi", + "Failed to clear conversation" : "Neizdevās notīrīt sarunu", + "Invalid feedback type" : "Nederīgs atsauksmes veids", + "type must be \"positive\" or \"negative\"" : "veidam jābūt \"positive\" vai \"negative\"", + "Message not found" : "Ziņojums nav atrasts", + "Failed to save feedback" : "Neizdevās saglabāt atsauksmi", + "Failed to get chat statistics" : "Neizdevās iegūt tērzēšanas statistiku", + "Not Found" : "Nav atrasts", + "Engine not found" : "Dzinējs nav atrasts", + "Consumer not found" : "Patērētājs nav atrasts", + "Backend parameter is required" : "Aizmugursistēmas parametrs ir obligāts", + "Failed to get database information: %s" : "Neizdevās iegūt datu bāzes informāciju: %s", + "SOLR setup error: %s" : "SOLR iestatīšanas kļūda: %s", + "Reindex failed: %s" : "Pārindeksēšana neizdevās: %s", + "Anonymized" : "Anonimizēts", + "Entities detected" : "Noteiktas entītijas", + "Extract Now" : "Ekstraģēt tagad", + "Extracted at" : "Ekstraģēts", + "Extraction" : "Ekstrakcija", + "Failed to load extraction data" : "Neizdevās ielādēt ekstrakcijas datus", + "Failed to load register data" : "Neizdevās ielādēt reģistra datus", + "Failed to retrieve extraction status." : "Neizdevās izgūt ekstrakcijas statusu.", + "Failed to retrieve objects for file." : "Neizdevās izgūt failam piesaistītos objektus.", + "No extraction data available for this file" : "Šim failam nav pieejamu ekstrakcijas datu", + "No register objects reference this file" : "Neviens reģistra objekts neatsaucas uz šo failu", + "Not extracted" : "Nav ekstraģēts", + "Register Objects" : "Reģistra objekti", + "Risk level" : "Riska līmenis", + "Text chunks" : "Teksta fragmenti", + "Unknown error" : "Nezināma kļūda", + "Very high" : "Ļoti augsts", + "{title} in {register} / {schema}" : "{title} reģistrā {register} / {schema}", + "Push Notifications" : "Tūlītējie paziņojumi", + "Real-time push notification status via notify_push" : "Reāllaika tūlītējo paziņojumu statuss, izmantojot notify_push", + "Realtime push not available — the notify_push app is not installed" : "Reāllaika tūlītējie paziņojumi nav pieejami — lietotne notify_push nav instalēta", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Instalējiet lietotni notify_push no Nextcloud lietotņu veikala, lai iespējotu reāllaika atjauninājumus.", + "Open Nextcloud App Store" : "Atvērt Nextcloud lietotņu veikalu", + "notify_push is installed but not yet active" : "notify_push ir instalēts, bet vēl nav aktīvs", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push ir instalēts, bet OpenRegister vēl nav apstiprinājis veiksmīgu tūlītējo paziņojumu. Aktivizēšanai veiciet objekta saglabāšanu vai pārbaudiet savu notify_push konfigurāciju.", + "notify_push configuration guide" : "notify_push konfigurācijas ceļvedis", + "Realtime push active" : "Reāllaika tūlītējie paziņojumi aktīvi", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Reāllaika tūlītējie paziņojumi ir aktīvi. Pievienotie klienti saņem tūlītējus atjauninājumus, kad objekti tiek izveidoti, atjaunināti vai dzēsti.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" +); diff --git a/l10n/lv.json b/l10n/lv.json new file mode 100644 index 0000000000..05f99f0e7f --- /dev/null +++ b/l10n/lv.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(nepieciešams Dolphin)", + "(no subject)": "(nav temata)", + "(no title)": "(nav nosaukuma)", + "(unnamed)": "(bez nosaukuma)", + "(untitled card)": "(kartīte bez nosaukuma)", + "({count} selected)": "({count} atlasīti)", + ", {failed} failed": ", {failed} neizdevās", + "0 = unlimited": "0 = neierobežots", + "A deactivation request is pending.": "Deaktivizācijas pieprasījums gaida apstrādi.", + "AI Assistant": "MI asistents", + "AI Chat": "MI tērzēšana", + "AI service not configured": "MI pakalpojums nav konfigurēts", + "API Key": "API atslēga", + "API Request Quota (requests/day)": "API pieprasījumu kvota (pieprasījumi dienā)", + "API Token Configuration": "API marķiera konfigurācija", + "API Tokens": "API marķieri", + "API tokens saved successfully": "API marķieri veiksmīgi saglabāti", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: šis pārskats sasaista katru verwerkingsactiviteit ar tai piešķirtajiem mūža audita pēdu skaitiem. Auditori un Autoriteit Persoonsgegevens to izmanto uzraudzības pārbaudei.", + "About ConfigSets": "Par ConfigSets", + "Access denied": "Piekļuve liegta", + "Access organisation dashboard and analytics": "Piekļūt organizācijas informācijas panelim un analītikai", + "Account": "Konts", + "Action": "Darbība", + "Action Distribution": "Darbību sadalījums", + "Action:": "Darbība:", + "Actions": "Darbības", + "Active": "Aktīvs", + "Active Collections": "Aktīvās kolekcijas", + "Active Filters": "Aktīvie filtri", + "Active Organisation:": "Aktīvā organizācija:", + "Active Organisations": "Aktīvās organizācijas", + "Active filters:": "Aktīvie filtri:", + "Active organisation changed successfully": "Aktīvā organizācija veiksmīgi mainīta", + "Active organisation set successfully": "Aktīvā organizācija veiksmīgi iestatīta", + "Activities": "Aktivitātes", + "Activity": "Aktivitāte", + "Activity Period": "Aktivitātes periods", + "Add": "Pievienot", + "Add Attachment": "Pievienot pielikumu", + "Add Endpoint": "Pievienot galapunktu", + "Add Groups": "Pievienot grupas", + "Add Property to '{title}'": "Pievienot īpašību objektam '{title}'", + "Add Register": "Pievienot reģistru", + "Add Schema": "Pievienot shēmu", + "Add Source": "Pievienot avotu", + "Add User": "Pievienot lietotāju", + "Add User to Organisation": "Pievienot lietotāju organizācijai", + "Add a contact from any of your address books to associate it with this object.": "Pievienojiet kontaktu no jebkuras savas adrešu grāmatas, lai to saistītu ar šo objektu.", + "Add a file or files": "Pievienot failu vai failus", + "Add card": "Pievienot kartīti", + "Add contact": "Pievienot kontaktu", + "Add endpoint": "Pievienot galapunktu", + "Add oneOf entry": "Pievienot oneOf ierakstu", + "Add range": "Pievienot diapazonu", + "Add schema titles, descriptions, and register information to provide richer context for search": "Pievienojiet shēmu nosaukumus, aprakstus un reģistra informāciju, lai nodrošinātu bagātīgāku meklēšanas kontekstu", + "Add to favorites": "Pievienot izlasei", + "Added": "Pievienots", + "Added to favorites": "Pievienots izlasei", + "Additional Information": "Papildu informācija", + "Additional feedback saved. Thank you!": "Papildu atsauksme saglabāta. Paldies!", + "Addresses": "Adreses", + "Admin": "Administrators", + "Advanced": "Paplašināti", + "Advanced Filters": "Paplašinātie filtri", + "Advanced Options": "Paplašinātās opcijas", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Paplašinātie filtri nav pieejami, izmantojot datubāzes avotu. Pārslēdzieties uz Auto vai SOLR indeksu, lai izmantotu filtrēšanas opcijas.", + "After": "Pēc", + "Agent Type": "Aģenta tips", + "Agent deleted successfully": "Aģents veiksmīgi dzēsts", + "Agent successfully created": "Aģents veiksmīgi izveidots", + "Agent successfully updated": "Aģents veiksmīgi atjaunināts", + "Agents": "Aģenti", + "Aggregated across schemas": "Apkopots pa shēmām", + "All": "Visi", + "All Categories": "Visas kategorijas", + "All Collections": "Visas kolekcijas", + "All Confidence Levels": "Visi ticamības līmeņi", + "All ConfigSets": "Visi ConfigSets", + "All Files": "Visi faili", + "All Levels": "Visi līmeņi", + "All Types": "Visi tipi", + "All Webhooks": "Visi tīmekļa āķi", + "All actions": "Visas darbības", + "All audit trails cleared successfully": "Visas audita pēdas veiksmīgi notīrītas", + "All clear": "Viss kārtībā", + "All file types": "Visi failu tipi", + "All registers": "Visi reģistri", + "All schemas": "Visas shēmas", + "All search trails cleared successfully": "Visas meklēšanas pēdas veiksmīgi notīrītas", + "All searches": "Visas meklēšanas", + "All users": "Visi lietotāji", + "All-day events": "Visas dienas notikumi", + "Allowed MIME Types": "Atļautie MIME tipi", + "Already linked": "Jau saistīts", + "An error occurred while clearing audit trails": "Notīrot audita pēdas, radās kļūda", + "An error occurred while deleting the audit trail": "Dzēšot audita pēdu, radās kļūda", + "An error occurred while deleting the object": "Dzēšot objektu, radās kļūda", + "An error occurred while deleting the view": "Dzēšot skatu, radās kļūda", + "An error occurred while permanently deleting the objects": "Neatgriezeniski dzēšot objektus, radās kļūda", + "An error occurred while restoring the objects": "Atjaunojot objektus, radās kļūda", + "An error occurred while saving the organisation": "Saglabājot organizāciju, radās kļūda", + "Analysis completed:": "Analīze pabeigta:", + "Analysis steps:": "Analīzes soļi:", + "Analytics": "Analītika", + "Analyze Objects": "Analizēt objektus", + "Analyze Properties": "Analizēt īpašības", + "Analyze existing properties for improvement opportunities": "Analizēt esošās īpašības, lai atrastu uzlabošanas iespējas", + "Analyzing...": "Notiek analīze...", + "Anonymized": "Anonimizēts", + "Anonymous Group": "Anonīmā grupa", + "Any favorites and sharing settings for this view": "Jebkuri šī skata izlases un kopīgošanas iestatījumi", + "Any user": "Jebkurš lietotājs", + "App ID": "Lietotnes ID", + "App store cache invalidated successfully": "Lietotņu veikala kešatmiņa veiksmīgi anulēta", + "App store cache invalidated: {invalidated}": "Lietotņu veikala kešatmiņa anulēta: {invalidated}", + "Application deleted successfully": "Lietojumprogramma veiksmīgi dzēsta", + "Application identifier for this configuration (optional)": "Lietojumprogrammas identifikators šai konfigurācijai (neobligāts)", + "Application settings": "Lietojumprogrammas iestatījumi", + "Application successfully created": "Lietojumprogramma veiksmīgi izveidota", + "Application successfully updated": "Lietojumprogramma veiksmīgi atjaunināta", + "Applications": "Lietojumprogrammas", + "Apply": "Lietot", + "Apply Changes": "Lietot izmaiņas", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Piešķirt lomu visām šī reģistra shēmām, kurām nav skaidru autorizācijas pārrakstījumu.", + "Approval Progress": "Apstiprināšanas progress", + "Approve": "Apstiprināt", + "Approved": "Apstiprināts", + "Archive": "Arhivēt", + "Archive conversation": "Arhivēt sarunu", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Vai arhivēt šo verwerkingsactiviteit? Audita pēdu rindas turpinās uz to atsaukties.", + "Archived conversations are hidden from your active list": "Arhivētās sarunas ir paslēptas no jūsu aktīvā saraksta", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Vai tiešām vēlaties DZĒST kolekciju \"{name}\"?\n\nTas:\n• Neatgriezeniski dzēsīs kolekciju un visus tās datus\n• Noņems visus indeksētos dokumentus\n• Šo darbību nevar atsaukt", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Vai tiešām vēlaties iztīrīt vecās meklēšanas pēdas? Tas dzēsīs ierakstus, kas vecāki par 30 dienām.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Vai tiešām vēlaties notīrīt visus datus no kolekcijas \"{name}\"?\n\nTas:\n• Dzēsīs visus indeksētos dokumentus\n• Saglabās kolekcijas struktūru neskartu\n• Šo darbību nevar atsaukt", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Vai tiešām vēlaties dzēst lietojumprogrammu \"{name}\"? Šo darbību nevar atsaukt.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Vai tiešām vēlaties dzēst atlasītās audita pēdas? Šo darbību nevar atsaukt.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Vai tiešām vēlaties dzēst atlasītās meklēšanas pēdas? Šo darbību nevar atsaukt.", + "Are you sure you want to delete this ConfigSet?": "Vai tiešām vēlaties dzēst šo ConfigSet?", + "Are you sure you want to delete this configset?": "Vai tiešām vēlaties dzēst šo configset?", + "Are you sure you want to leave '{name}'?": "Vai tiešām vēlaties pamest '{name}'?", + "Are you sure you want to permanently delete": "Vai tiešām vēlaties neatgriezeniski dzēst", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Vai tiešām vēlaties pārindeksēt kolekciju \"{name}\"?\n\nTas:\n• Pārbūvēs indeksu ar visiem objektiem\n• Pabeigšanai būs nepieciešamas vairākas minūtes\n• Pārindeksēšanas laikā var ietekmēt meklēšanas veiktspēju", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Vai tiešām vēlaties noņemt shēmu \"{schema}\" no reģistra \"{register}\"? Šo darbību nevar atsaukt.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Vai tiešām vēlaties mīksti dzēst {count} aktīvos objektus shēmai \"{schema}\"?\n\nTie tiks atzīmēti kā dzēsti, bet vēlāk tos var neatgriezeniski noņemt.", + "Array Object Configuration:": "Masīva objekta konfigurācija:", + "Articles": "Raksti", + "Ask a question...": "Uzdodiet jautājumu...", + "Ask questions about your data using natural language": "Uzdodiet jautājumus par saviem datiem dabiskā valodā", + "Assigned collaborative tags": "Piešķirtās sadarbības birkas", + "Assignment notifications": "Piešķiršanas paziņojumi", + "Attachment added to {name}": "Pielikums pievienots objektam {name}", + "Attempt": "Mēģinājums", + "Audit Trail Changes": "Audita pēdas izmaiņas", + "Audit Trail Details": "Audita pēdas detaļas", + "Audit Trail Management": "Audita pēdu pārvaldība", + "Audit Trail Statistics": "Audita pēdu statistika", + "Audit Trails": "Audita pēdas", + "Audit trail data copied to clipboard": "Audita pēdas dati kopēti starpliktuvē", + "Audit trail deleted successfully": "Audita pēda veiksmīgi dzēsta", + "Audit trail successfully deleted": "Audita pēda veiksmīgi dzēsta", + "Authentication": "Autentifikācija", + "Auto (e.g. Y for year, F Y for month)": "Auto (piem., Y gadam, F Y mēnesim)", + "Auto share": "Automātiska kopīgošana", + "Auto-commit disabled": "Automātiskā apstiprināšana atspējota", + "Auto-commit enabled": "Automātiskā apstiprināšana iespējota", + "Auto-create Default Organisation": "Automātiski izveidot noklusējuma organizāciju", + "Auto-retry failed vectorizations": "Automātiski atkārtot neizdevušās vektorizācijas", + "Automatically commit changes to SOLR index": "Automātiski apstiprināt izmaiņas SOLR indeksā", + "Automatically connect to the federation network.": "Automātiski savienoties ar federācijas tīklu.", + "Automatically create a default organisation if none exists when the app is initialized": "Automātiski izveidot noklusējuma organizāciju, ja lietotnes inicializēšanas brīdī tādas nav", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automātiski ģenerēt vektoru iegulumus no teksta fragmentiem, kad faili tiek augšupielādēti un apstrādāti", + "Automatically generate vector embeddings when objects are created or updated": "Automātiski ģenerēt vektoru iegulumus, kad objekti tiek izveidoti vai atjaunināti", + "Automatically retry failed vectorization attempts (max 3 retries)": "Automātiski atkārtot neizdevušos vektorizācijas mēģinājumus (maks. 3 atkārtojumi)", + "Automatically update catalogues.": "Automātiski atjaunināt katalogus.", + "Available Filters": "Pieejamie filtri", + "Available Workflows": "Pieejamās darbplūsmas", + "Available placeholders:": "Pieejamie vietturi:", + "Avatar": "Avatars", + "Avatar changes are not supported by your authentication provider.": "Jūsu autentifikācijas nodrošinātājs neatbalsta avatara izmaiņas.", + "Avatar removed": "Avatars noņemts", + "Avatar updated successfully": "Avatars veiksmīgi atjaunināts", + "Avg Execution Time": "Vid. izpildes laiks", + "Avg Members/Org": "Vid. dalībnieki/org.", + "Avg Object Views/Session": "Vid. objektu skatījumi/sesijā", + "Avg Response Time": "Vid. atbildes laiks", + "Avg Results/Search": "Vid. rezultāti/meklējumā", + "Avg Searches/Session": "Vid. meklējumi/sesijā", + "Back": "Atpakaļ", + "Back to Entities": "Atpakaļ uz entītijām", + "Back to Registers": "Atpakaļ uz reģistriem", + "Back to Webhooks": "Atpakaļ uz tīmekļa āķiem", + "Back to applications": "Atpakaļ uz lietojumprogrammām", + "Back to entities": "Atpakaļ uz entītijām", + "Backend parameter is required": "Aizmugursistēmas parametrs ir obligāts", + "Backend updated successfully. Please reload the application.": "Aizmugursistēma veiksmīgi atjaunināta. Lūdzu, pārlādējiet lietojumprogrammu.", + "Bandwidth Quota (MB/month)": "Joslas platuma kvota (MB/mēnesī)", + "Base URL (Optional)": "Bāzes URL (neobligāts)", + "Basic Information": "Pamatinformācija", + "Batch Size": "Partijas izmērs", + "Batch extraction completed": "Partijas izvilkšana pabeigta", + "Before": "Pirms", + "Before object vectorization can work:": "Pirms objektu vektorizācija var darboties:", + "Behavior": "Uzvedība", + "Behavior Issue": "Uzvedības problēma", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 ilgums, piem., P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob krātuve ir izņemta no lietošanas. Visi objekti tagad izmanto magic tabulas.", + "Bookmarks": "Grāmatzīmes", + "Branch": "Zars", + "Bulk Role Assignment: {title}": "Lomu masveida piešķiršana: {title}", + "Bulk delete operation completed successfully": "Masveida dzēšanas darbība veiksmīgi pabeigta", + "Bulk save operation completed successfully": "Masveida saglabāšanas darbība veiksmīgi pabeigta", + "Business Data": "Uzņēmējdarbības dati", + "CSV": "CSV", + "Cache cleared successfully": "Kešatmiņa veiksmīgi notīrīta", + "Cache grew from {old} to {new} entries.": "Kešatmiņa palielinājās no {old} līdz {new} ierakstiem.", + "Cache shrunk from {old} to {new} entries.": "Kešatmiņa samazinājās no {old} līdz {new} ierakstiem.", + "Cache stayed the same at {size} entries.": "Kešatmiņa palika nemainīga ar {size} ierakstiem.", + "Calculate Sizes": "Aprēķināt izmērus", + "Calendar": "Kalendārs", + "Calendar Provider Configuration": "Kalendāra nodrošinātāja konfigurācija", + "Calendar integration is not available": "Kalendāra integrācija nav pieejama", + "Calendar name": "Kalendāra nosaukums", + "Cancel": "Atcelt", + "Cancel deactivation request": "Atcelt deaktivizācijas pieprasījumu", + "Cannot delete: objects are still attached": "Nevar dzēst: objekti joprojām ir pievienoti", + "Cannot edit: This register is managed by external configuration {title}": "Nevar rediģēt: šo reģistru pārvalda ārēja konfigurācija {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nevar noņemt shēmu ar esošiem objektiem ({count} objekti)", + "Cannot remove schema {schema} because it contains {count} objects": "Nevar noņemt shēmu {schema}, jo tā satur {count} objektus", + "Cards": "Kartītes", + "Cascade delete": "Kaskādes dzēšana", + "Categories of data subjects (one per line)": "Datu subjektu kategorijas (viena rindā)", + "Categories of personal data (one per line)": "Personas datu kategorijas (viena rindā)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategorija", + "Change Type": "Mainīt tipu", + "Change password": "Mainīt paroli", + "Changes": "Izmaiņas", + "Changes copied to clipboard": "Izmaiņas kopētas starpliktuvē", + "Changes for Audit Trail #{id}": "Izmaiņas audita pēdai #{id}", + "Chat Model": "Tērzēšanas modelis", + "Chat Provider": "Tērzēšanas nodrošinātājs", + "Chat Provider (RAG)": "Tērzēšanas nodrošinātājs (RAG)", + "Chat Settings": "Tērzēšanas iestatījumi", + "Chat provider connection successful!": "Tērzēšanas nodrošinātāja savienojums veiksmīgs!", + "Chat settings": "Tērzēšanas iestatījumi", + "Chat test failed: {error}": "Tērzēšanas tests neizdevās: {error}", + "Choose a register": "Izvēlieties reģistru", + "Choose a schema": "Izvēlieties shēmu", + "Choose how vector similarity calculations are performed for semantic search": "Izvēlieties, kā tiek veikti vektoru līdzības aprēķini semantiskajai meklēšanai", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Izvēlieties, kurus failu tipus iekļaut vektorizācijas procesā. Tiks apstrādāti tikai faili ar izvilktu tekstu un fragmentiem.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Izvēlieties, kurus skatus iekļaut vektorizācijas procesā. Atstājiet tukšu, lai apstrādātu visus skatus atbilstoši jūsu konfigurācijai.", + "Chunk": "Fragments", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Fragmentu dzēšana vēl nav ieviesta. Izmantojiet uz fragmentiem balstītus galapunktus.", + "Chunks": "Fragmenti", + "Chunks to Vectorize:": "Vektorizējamie fragmenti:", + "Cleanup Old Trails": "Iztīrīt vecās pēdas", + "Cleanup completed": "Iztīrīšana pabeigta", + "Cleanup completed successfully. Deleted {count} entries.": "Iztīrīšana veiksmīgi pabeigta. Dzēsti {count} ieraksti.", + "Cleanup failed: {error}": "Iztīrīšana neizdevās: {error}", + "Clear All Embeddings": "Notīrīt visus iegulumus", + "Clear Entries": "Notīrīt ierakstus", + "Clear Filtered Audit Trails": "Notīrīt filtrētās audita pēdas", + "Clear Filters": "Notīrīt filtrus", + "Clear Index": "Notīrīt indeksu", + "Clear Selection": "Notīrīt atlasi", + "Clear all": "Notīrīt visu", + "Clear all filters": "Notīrīt visus filtrus", + "Clear date range": "Notīrīt datumu diapazonu", + "Clear filters": "Notīrīt filtrus", + "Clearing...": "Notiek notīrīšana...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Noklikšķiniet Palaist atbilstības skenēšanu, lai atrastu shēmas, kurās ir atklāts PII, bet nav verwerkingsactiviteit anotācijas.", + "Click to subscribe to notifications": "Noklikšķiniet, lai abonētu paziņojumus", + "Click to unsubscribe from notifications": "Noklikšķiniet, lai atteiktos no paziņojumiem", + "Close": "Aizvērt", + "Cluster name": "Klastera nosaukums", + "Code": "Kods", + "Code (short readable key, e.g. v-2026-001)": "Kods (īsa lasāma atslēga, piem., v-2026-001)", + "Collapse details": "Sakļaut detaļas", + "Collection Management": "Kolekciju pārvaldība", + "Collection Name": "Kolekcijas nosaukums", + "Collection assignments updated successfully": "Kolekcijas piešķīrumi veiksmīgi atjaunināti", + "Collection cleared successfully": "Kolekcija veiksmīgi notīrīta", + "Collection copied successfully": "Kolekcija veiksmīgi nokopēta", + "Collection created successfully": "Kolekcija veiksmīgi izveidota", + "Collection deleted successfully": "Kolekcija veiksmīgi dzēsta", + "Collection used to store and index file metadata and content": "Kolekcija, ko izmanto failu metadatu un satura glabāšanai un indeksēšanai", + "Collection used to store and index object data": "Kolekcija, ko izmanto objektu datu glabāšanai un indeksēšanai", + "Collections:": "Kolekcijas:", + "Color": "Krāsa", + "Comment...": "Komentārs...", + "Commit Message": "Apstiprināšanas ziņojums", + "Commit Within (ms)": "Apstiprināt laikā (ms)", + "Commit message": "Apstiprināšanas ziņojums", + "Compare current schema with real object data": "Salīdzināt pašreizējo shēmu ar reāliem objektu datiem", + "Completed": "Pabeigts", + "Complex": "Sarežģīts", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Sarežģīti vaicājumi: paplašinātas meklēšanas ar vairākiem filtriem, operatoriem un sarežģītām parametru kombinācijām", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Sarežģīti vaicājumi: paplašinātas meklēšanas ar vairākiem filtriem, operatoriem un sarežģītām parametru kombinācijām", + "Compliance": "Atbilstība", + "Confidence Level": "Ticamības līmenis", + "Confidence Score": "Ticamības rādītājs", + "Confidence: {confidence}%": "Ticamība: {confidence}%", + "Config must be provided as an object": "Konfigurācija ir jānorāda kā objekts", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet pārvaldība", + "ConfigSet Name": "ConfigSet nosaukums", + "ConfigSet created successfully": "ConfigSet veiksmīgi izveidots", + "ConfigSet deleted successfully": "ConfigSet veiksmīgi dzēsts", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets definē jūsu SOLR kolekciju shēmu un konfigurāciju. Tie satur lauku definīcijas, analizatorus un citus meklēšanas iestatījumus.", + "Configuration": "Konfigurācija", + "Configuration Data": "Konfigurācijas dati", + "Configuration is up to date": "Konfigurācija ir aktuāla", + "Configuration saved": "Konfigurācija saglabāta", + "Configuration saved successfully.": "Konfigurācija veiksmīgi saglabāta.", + "Configuration type (default, application, etc.)": "Konfigurācijas tips (noklusējums, lietojumprogramma utt.)", + "Configurations": "Konfigurācijas", + "Configure API tokens for external service integrations": "Konfigurēt API marķierus ārējo pakalpojumu integrācijām", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurēt Apache SOLR meklēšanas dzini paplašinātām meklēšanas iespējām", + "Configure Facets": "Konfigurēt aspektus", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurēt lielo valodas modeļu (LLM) nodrošinātājus MI darbinātām funkcijām, tostarp semantiskajai meklēšanai, iegulumiem un tērzēšanai.", + "Configure SOLR Facets": "Konfigurēt SOLR aspektus", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurējiet pamata savienojuma iestatījumus savam SOLR serverim, tostarp autentifikācijas un tīkla opcijas. Izmantojiet atsevišķos ConfigSet un kolekciju pārvaldības dialogus, lai pārvaldītu kodolus un kolekcijas.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurējiet pamata savienojuma iestatījumus savam SOLR serverim, tostarp autentifikācijas un tīkla opcijas. Izmantojiet atsevišķos configset un kolekciju pārvaldības dialogus, lai pārvaldītu kodolus un kolekcijas.", + "Configure default organisation and organisation-related settings": "Konfigurēt noklusējuma organizāciju un ar organizāciju saistītos iestatījumus", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurējiet, kā datubāzes objekti tiek pārveidoti vektoru iegulumos semantiskajai meklēšanai. Objekti tiek tieši vektorizēti bez teksta izvilkšanas nepieciešamības.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurējiet, kā objekti tiek pārveidoti tekstā pirms vektorizācijas. Šie iestatījumi ietekmē meklēšanas kvalitāti un kontekstu.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurēt lielo valodas modeļu (LLM) nodrošinātājus MI darbinātām funkcijām, tostarp semantiskajai meklēšanai, iegulumiem un tērzēšanai.", + "Configure multi-organization support and tenant isolation": "Konfigurēt vairāku organizāciju atbalstu un nomnieku izolāciju", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurējiet parametrus failu vektorizācijai. Šis process ģenerēs vektoru iegulumus visiem izvilktajiem failu fragmentiem.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurējiet parametrus objektu vektorizācijai. Šis process ģenerēs vektoru iegulumus visiem objektiem, kas atbilst jūsu skata filtriem.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurējiet šo shēmu, lai parādītu objektus kā notikumus Nextcloud Calendar lietotnē.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurējiet, kuru veidu datus meklēt un cik avotus iegūt", + "Configuring field mappings...": "Notiek lauku kartējumu konfigurēšana...", + "Confirm Account Deactivation": "Apstiprināt konta deaktivizāciju", + "Confirm Object Deletion": "Apstiprināt objekta dzēšanu", + "Confirm Object Restoration": "Apstiprināt objekta atjaunošanu", + "Confirm Permanent Object Deletion": "Apstiprināt objekta neatgriezenisku dzēšanu", + "Confirm deactivation": "Apstiprināt deaktivizāciju", + "Connecting to SOLR cluster...": "Notiek savienošanās ar SOLR klasteri...", + "Connection Failed": "Savienojums neizdevās", + "Connection Settings": "Savienojuma iestatījumi", + "Connection Successful!": "Savienojums veiksmīgs!", + "Connection failed": "Savienojums neizdevās", + "Connection protocol": "Savienojuma protokols", + "Connection successful": "Savienojums veiksmīgs", + "Connection timeout in seconds": "Savienojuma noildze sekundēs", + "Constant delay between retries (5 minutes)": "Nemainīga aizkave starp atkārtojumiem (5 minūtes)", + "Constraint Issue": "Ierobežojuma problēma", + "Consumer not found": "Patērētājs nav atrasts", + "Contacts": "Kontakti", + "Contacts integration is not available": "Kontaktu integrācija nav pieejama", + "Content Filters": "Satura filtri", + "Continue to Properties": "Turpināt uz īpašībām", + "Contracts": "Līgumi", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontrolējiet, kuri objektu skati ir jāvektorizē, lai samazinātu API izmaksas. Vektorizējiet tikai tos skatus, kuriem ir noderīga semantiskā meklēšana.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontrolējiet, kurus skatus un rīkus MI var izmantot šajā sarunā. Pēc noklusējuma ir iespējotas visas aģenta iespējas.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontrolē nejaušību (0 = determinēts, 2 = ļoti radošs)", + "Conversation ID is required": "Sarunas ID ir obligāts", + "Conversation archived": "Saruna arhivēta", + "Conversation archived successfully": "Saruna veiksmīgi arhivēta", + "Conversation cleared successfully": "Saruna veiksmīgi notīrīta", + "Conversation deleted": "Saruna dzēsta", + "Conversation not found": "Saruna nav atrasta", + "Conversation permanently deleted": "Saruna neatgriezeniski dzēsta", + "Conversation renamed": "Saruna pārdēvēta", + "Conversation restored": "Saruna atjaunota", + "Conversation started with {agent}": "Saruna sākta ar {agent}", + "Conversation title": "Sarunas nosaukums", + "Conversations": "Sarunas", + "Converting {schema} to blob storage...": "Notiek {schema} pārveidošana uz blob krātuvi...", + "Converting {schema} to magic table...": "Notiek {schema} pārveidošana uz magic tabulu...", + "Copied!": "Nokopēts!", + "Copy": "Kopēt", + "Copy Changes": "Kopēt izmaiņas", + "Copy Collection": "Kopēt kolekciju", + "Copy Data": "Kopēt datus", + "Copy Full Data": "Kopēt visus datus", + "Copy of {name}": "{name} kopija", + "Copy to clipboard": "Kopēt starpliktuvē", + "Copying...": "Notiek kopēšana...", + "Costs": "Izmaksas", + "Count": "Skaits", + "Counting objects...": "Notiek objektu skaitīšana...", + "Create": "Izveidot", + "Create API Token": "Izveidot API marķieri", + "Create Agent": "Izveidot aģentu", + "Create Application": "Izveidot lietojumprogrammu", + "Create Collection": "Izveidot kolekciju", + "Create ConfigSet": "Izveidot ConfigSet", + "Create New Collection": "Izveidot jaunu kolekciju", + "Create New ConfigSet": "Izveidot jaunu ConfigSet", + "Create Operations": "Izveides darbības", + "Create Organisation": "Izveidot organizāciju", + "Create Webhook": "Izveidot tīmekļa āķi", + "Create a copy of collection:": "Izveidot kolekcijas kopiju:", + "Create a new ConfigSet based on the _default template": "Izveidot jaunu ConfigSet, pamatojoties uz _default veidni", + "Create a new SOLR collection from an existing ConfigSet": "Izveidot jaunu SOLR kolekciju no esoša ConfigSet", + "Create a new SOLR collection from an existing configset": "Izveidot jaunu SOLR kolekciju no esoša configset", + "Create a new configset based on the _default template": "Izveidot jaunu configset, pamatojoties uz _default veidni", + "Create a new event or link an existing one from any of your calendars.": "Izveidojiet jaunu notikumu vai saistiet esošu no jebkura sava kalendāra.", + "Create another": "Izveidot vēl vienu", + "Create copies of": "Izveidot kopijas", + "Create event": "Izveidot notikumu", + "Create new token": "Izveidot jaunu marķieri", + "Create or link a Deck card to track work on this object.": "Izveidojiet vai saistiet Deck kartīti, lai izsekotu darbu pie šī objekta.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Izveidojiet pirmo verwerkingsactiviteit, lai sāktu audita pēdu rindas atzīmēt ar to AVG Art 30 piesaisti.", + "Create your first AI agent to get started.": "Izveidojiet savu pirmo MI aģentu, lai sāktu darbu.", + "Created": "Izveidots", + "Created:": "Izveidots:", + "Creating search collections...": "Notiek meklēšanas kolekciju izveide...", + "Creating...": "Notiek izveide...", + "Current": "Pašreizējais", + "Current Filters": "Pašreizējie filtri", + "Current Type:": "Pašreizējais tips:", + "Current password": "Pašreizējā parole", + "Custom API endpoint if using a different region": "Pielāgots API galapunkts, ja izmantojat citu reģionu", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Pielāgotas HTTP galvenes (viena rindā, formāts: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Pielāgotas HTTP galvenes (viena rindā, formāts: header-name: value)", + "Custom Ranges:": "Pielāgoti diapazoni:", + "Custom display title for this facet": "Pielāgots šī aspekta attēlojuma nosaukums", + "Custom pattern:": "Pielāgots raksts:", + "DELETE COLLECTION": "DZĒST KOLEKCIJU", + "DSAR": "DSAR", + "Daily": "Katru dienu", + "Dashboard": "Informācijas panelis", + "Dashboard not found": "Informācijas panelis nav atrasts", + "Data": "Dati", + "Data Source": "Datu avots", + "Data Sources": "Datu avoti", + "Data Views": "Datu skati", + "Data sources": "Datu avoti", + "Data type does not match observed values": "Datu tips neatbilst novērotajām vērtībām", + "Data-subject access request": "Datu subjekta piekļuves pieprasījums", + "Database URL": "Datubāzes URL", + "Database information refreshed": "Datubāzes informācija atsvaidzināta", + "Date Faceting:": "Datumu aspektēšana:", + "Date Range": "Datumu diapazons", + "Dates": "Datumi", + "Deactivation request cancelled": "Deaktivizācijas pieprasījums atcelts", + "Deactivation request submitted": "Deaktivizācijas pieprasījums iesniegts", + "Deck": "Deck", + "Deck integration is not available": "Deck integrācija nav pieejama", + "Default": "Noklusējums", + "Default Object Tenant": "Noklusējuma objekta nomnieks", + "Default Organisation": "Noklusējuma organizācija", + "Default Owner": "Noklusējuma īpašnieks", + "Default User (for cron/background jobs)": "Noklusējuma lietotājs (cron/fona uzdevumiem)", + "Default User Tenant": "Noklusējuma lietotāja nomnieks", + "Default value": "Noklusējuma vērtība", + "Default value (Email)": "Noklusējuma vērtība (e-pasts)", + "Default value (Password)": "Noklusējuma vērtība (parole)", + "Default value (Phone number)": "Noklusējuma vērtība (tālruņa numurs)", + "Default value (Regex)": "Noklusējuma vērtība (Regex)", + "Default: 5": "Noklusējums: 5", + "Defaults to current user. Select a different user if needed.": "Pēc noklusējuma pašreizējais lietotājs. Ja nepieciešams, atlasiet citu lietotāju.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Aizkaves dubultojas ar katru mēģinājumu (2, 4, 8 minūtes...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Aizkaves palielinās lineāri (5, 10, 15 minūtes...)", + "Delete": "Dzēst", + "Delete ({count})": "Dzēst ({count})", + "Delete Application": "Dzēst lietojumprogrammu", + "Delete Audit Trail": "Dzēst audita pēdu", + "Delete Collection": "Dzēst kolekciju", + "Delete ConfigSet": "Dzēst ConfigSet", + "Delete Objects": "Dzēst objektus", + "Delete Operations": "Dzēšanas darbības", + "Delete SOLR Collection": "Dzēst SOLR kolekciju", + "Delete View": "Dzēst skatu", + "Delete all objects in this schema": "Dzēst visus objektus šajā shēmā", + "Delete permanently": "Dzēst neatgriezeniski", + "Delete this conversation?": "Vai dzēst šo sarunu?", + "Delete view": "Dzēst skatu", + "Delete {count} object": "Dzēst {count} objektu", + "Delete {count} objects": "Dzēst {count} objektus", + "Deleted": "Dzēsts", + "Deleted By": "Dzēsis", + "Deleted Date": "Dzēšanas datums", + "Deleted Items Management": "Dzēsto vienumu pārvaldība", + "Deleted This Week": "Dzēsts šonedēļ", + "Deleted Today": "Dzēsts šodien", + "Deleted:": "Dzēsts:", + "Deleting SOLR Collection...": "Notiek SOLR kolekcijas dzēšana...", + "Deleting...": "Notiek dzēšana...", + "Deletion Date Range": "Dzēšanas datumu diapazons", + "Deletion Statistics": "Dzēšanas statistika", + "Deprecated": "Novecojis", + "Depublish": "Atsaukt publikāciju", + "Description": "Apraksts", + "Description Template": "Apraksta veidne", + "Description shown as tooltip": "Apraksts, kas tiek rādīts kā rīka padoms", + "Description:": "Apraksts:", + "Detect data types and patterns": "Atklāt datu tipus un rakstus", + "Detected At": "Atklāts", + "Detected Format:": "Atklātais formāts:", + "Detected Issues:": "Atklātās problēmas:", + "Detected: {date}": "Atklāts: {date}", + "Direct permission": "Tieša atļauja", + "Disable": "Atspējot", + "Disable this to make the agent": "Atspējojiet šo, lai padarītu aģentu", + "Disabled": "Atspējots", + "Discover Files": "Atklāt failus", + "Discovered Properties": "Atklātās īpašības", + "Display Format": "Attēlojuma formāts", + "Display Name": "Attēlojamais vārds", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Vai vēlaties neatgriezeniski dzēst visus filtrētos audita pēdu ierakstus? Šo darbību nevar atsaukt.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Vai vēlaties neatgriezeniski dzēst šo audita pēdas ierakstu? Šo darbību nevar atsaukt.", + "Documents": "Dokumenti", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR iespējots", + "Download API Spec": "Lejupielādēt API specifikāciju", + "Download API Specification": "Lejupielādēt API specifikāciju", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Lejupielādējiet visu savu OpenRegister glabāto personas datu kopiju (GDPR 20. pants).", + "Drag a file or files here": "Velciet failu vai failus šeit", + "Dry run completed: {count} fields would be created": "Izmēģinājuma palaišana pabeigta: tiktu izveidoti {count} lauki", + "Dry run completed: {count} fields would be fixed": "Izmēģinājuma palaišana pabeigta: tiktu izlaboti {count} lauki", + "Duration in seconds (optional)": "Ilgums sekundēs (neobligāts)", + "Edit": "Rediģēt", + "Edit Agent": "Rediģēt aģentu", + "Edit Application": "Rediģēt lietojumprogrammu", + "Edit Configuration": "Rediģēt konfigurāciju", + "Edit Endpoint": "Rediģēt galapunktu", + "Edit Organisation": "Rediģēt organizāciju", + "Edit Property '{key}' of '{title}'": "Rediģēt īpašību '{key}' objektam '{title}'", + "Edit Register": "Rediģēt reģistru", + "Edit Schema": "Rediģēt shēmu", + "Edit Source": "Rediģēt avotu", + "Edit View": "Rediģēt skatu", + "Edit Webhook": "Rediģēt tīmekļa āķi", + "Edit labels": "Rediģēt etiķetes", + "Edit processing activity": "Rediģēt verwerkingsactiviteit", + "Edit tags for {name}": "Rediģēt birkas objektam {name}", + "Edit verwerkingsactiviteit": "Rediģēt verwerkingsactiviteit", + "Edit view": "Rediģēt skatu", + "Edit view details": "Rediģēt skata detaļas", + "Effectiveness": "Efektivitāte", + "Email": "E-pasts", + "Email Addresses": "E-pasta adreses", + "Email digest frequency": "E-pasta kopsavilkuma biežums", + "Emails": "E-pasti", + "Embedding Model": "Iegulumu modelis", + "Embedding Provider": "Iegulumu nodrošinātājs", + "Embedding generated successfully": "Iegulums veiksmīgi ģenerēts", + "Embedding provider connection successful!": "Iegulumu nodrošinātāja savienojums veiksmīgs!", + "Embedding test failed: {error}": "Iegulumu tests neizdevās: {error}", + "Enable": "Iespējot", + "Enable RAG": "Iespējot RAG", + "Enable auto-creation": "Iespējot automātisku izveidi", + "Enable automatic file vectorization": "Iespējot automātisku failu vektorizāciju", + "Enable automatic object vectorization": "Iespējot automātisku objektu vektorizāciju", + "Enable automatic synchronization": "Iespējot automātisku sinhronizāciju", + "Enable calendar provider": "Iespējot kalendāra nodrošinātāju", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Iespējot detalizētu reģistrēšanu SOLR darbībām (ieteicams atkļūdošanai)", + "Enable faceting": "Iespējot aspektēšanu", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Iespējot vai atspējot LLM funkcijas. Konfigurējiet nodrošinātājus un modeļus, izmantojot iepriekš redzamo LLM konfigurācijas pogu.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Iespējot vai atspējot LLM funkcijas. Konfigurējiet nodrošinātājus un modeļus, izmantojot iepriekš redzamo LLM konfigurācijas pogu.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Iespējot vai atspējot SOLR meklēšanas integrāciju. Konfigurējiet savienojuma iestatījumus, izmantojot iepriekš redzamo savienojuma iestatījumu pogu.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Iespējot vai atspējot SOLR meklēšanas integrāciju. Konfigurējiet savienojuma iestatījumus, izmantojot iepriekš redzamo savienojuma iestatījumu pogu.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Iespējot vai atspējot n8n darbplūsmas integrāciju. Konfigurējiet savienojuma iestatījumus zemāk.", + "Enable or disable this webhook": "Iespējot vai atspējot šo tīmekļa āķi", + "Enable tools that allow the agent to interact with data through function calling.": "Iespējojiet rīkus, kas ļauj aģentam mijiedarboties ar datiem, izmantojot funkciju izsaukšanu.", + "Enable vectorization for all existing and future views (may increase costs)": "Iespējot vektorizāciju visiem esošajiem un nākotnes skatiem (var palielināt izmaksas)", + "Enable write-back to target objects": "Iespējot atpakaļrakstīšanu mērķa objektiem", + "Enabled": "Iespējots", + "End Date Field": "Beigu datuma lauks", + "Endpoint Path*": "Galapunkta ceļš*", + "Endpoints": "Galapunkti", + "Engine not found": "Dzinis nav atrasts", + "Enter ConfigSet name": "Ievadiet ConfigSet nosaukumu", + "Enter Nextcloud usernames to grant access to this private agent": "Ievadiet Nextcloud lietotājvārdus, lai piešķirtu piekļuvi šim privātajam aģentam", + "Enter agent description (optional)": "Ievadiet aģenta aprakstu (neobligāts)", + "Enter agent name": "Ievadiet aģenta nosaukumu", + "Enter application description (optional)": "Ievadiet lietojumprogrammas aprakstu (neobligāts)", + "Enter application name": "Ievadiet lietojumprogrammas nosaukumu", + "Enter collection name": "Ievadiet kolekcijas nosaukumu", + "Enter configuration description (optional)": "Ievadiet konfigurācijas aprakstu (neobligāts)", + "Enter configuration title": "Ievadiet konfigurācijas nosaukumu", + "Enter description (optional)...": "Ievadiet aprakstu (neobligāts)...", + "Enter new collection name": "Ievadiet jaunās kolekcijas nosaukumu", + "Enter object ID": "Ievadiet objekta ID", + "Enter organisation description (optional)": "Ievadiet organizācijas aprakstu (neobligāts)", + "Enter organisation name": "Ievadiet organizācijas nosaukumu", + "Enter search term": "Ievadiet meklēšanas vienumu", + "Enter search terms or leave empty to browse all": "Ievadiet meklēšanas vienumus vai atstājiet tukšu, lai pārlūkotu visus", + "Enter system prompt for the agent": "Ievadiet aģenta sistēmas uzvedni", + "Enter username and press Enter": "Ievadiet lietotājvārdu un nospiediet Enter", + "Enter view name...": "Ievadiet skata nosaukumu...", + "Enter webhook name": "Ievadiet tīmekļa āķa nosaukumu", + "Enter your API key": "Ievadiet savu API atslēgu", + "Enter your object here...": "Ievadiet savu objektu šeit...", + "Enter your schema here...": "Ievadiet savu shēmu šeit...", + "Entities": "Entītijas", + "Entities detected": "Atklātās entītijas", + "Entity ID: {id}": "Entītijas ID: {id}", + "Entity Information": "Entītijas informācija", + "Entity Recognition": "Entītiju atpazīšana", + "Entity Recognition Method": "Entītiju atpazīšanas metode", + "Entity deleted successfully": "Entītija veiksmīgi dzēsta", + "Entity not found": "Entītija nav atrasta", + "Entries to be deleted:": "Dzēšamie ieraksti:", + "Enum Issue": "Uzskaitījuma problēma", + "Enum constraint is missing": "Trūkst uzskaitījuma ierobežojuma", + "Erase (Art 17)": "Dzēst (17. pants)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Vai dzēst {count} objektu(s) šim subjektam? Šī darbība tiek reģistrēta audita pierakstā.", + "Erasure complete": "Dzēšana pabeigta", + "Erasure preview": "Dzēšanas priekšskatījums", + "Error": "Kļūda", + "Error Details": "Kļūdas detaļas", + "Error Information": "Kļūdas informācija", + "Error Message": "Kļūdas ziņojums", + "Error deleting audit trails: {error}": "Kļūda, dzēšot audita pierakstus: {error}", + "Error deleting search trails: {error}": "Kļūda, dzēšot meklēšanas pierakstus: {error}", + "Error loading application": "Kļūda, ielādējot lietojumprogrammu", + "Error loading audit trails": "Kļūda, ielādējot audita pierakstus", + "Error loading entity": "Kļūda, ielādējot entītiju", + "Error loading search trails": "Kļūda, ielādējot meklēšanas pierakstus", + "Estimated Batches:": "Aplēstās partijas:", + "Estimated Cost:": "Aplēstās izmaksas:", + "Estimated Duration:": "Aplēstais ilgums:", + "Event": "Notikums", + "Event Property for Payload": "Notikuma īpašība lietderīgajai slodzei", + "Events": "Notikumi", + "Every schema with detected PII has a processing-activity annotation.": "Katrai shēmai ar atklātu PII ir apstrādes darbības anotācija.", + "Example": "Piemērs", + "Example Value": "Piemēra vērtība", + "Example value for this property": "Piemēra vērtība šai īpašībai", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Ekskluzīvais maksimums", + "Exclusive Minimum": "Ekskluzīvais minimums", + "Exclusive maximum": "Ekskluzīvais maksimums", + "Exclusive minimum": "Ekskluzīvais minimums", + "Execution Mode": "Izpildes režīms", + "Execution Time": "Izpildes laiks", + "Execution Time Range": "Izpildes laika diapazons", + "Existing Improvements": "Esošie uzlabojumi", + "Expand details": "Izvērst detaļas", + "Expiration": "Derīguma termiņš", + "Expires": "Beidzas", + "Expires in (e.g., 90d)": "Beidzas pēc (piem., 90d)", + "Exponential": "Eksponenciāls", + "Export": "Eksportēt", + "Export \"{schema}\" objects from \"{register}\"": "Eksportēt \"{schema}\" objektus no \"{register}\"", + "Export Configuration": "Eksporta konfigurācija", + "Export Format:": "Eksporta formāts:", + "Export Objects": "Eksportēt objektus", + "Export completed successfully": "Eksports veiksmīgi pabeigts", + "Export downloaded successfully": "Eksports veiksmīgi lejupielādēts", + "Export failed: {error}": "Eksports neizdevās: {error}", + "Export is rate limited. Please try again later.": "Eksportam ir noteikts ātruma ierobežojums. Lūdzu, mēģiniet vēlreiz vēlāk.", + "Export my data": "Eksportēt manus datus", + "Export, view, or delete audit trails": "Eksportējiet, skatiet vai dzēsiet audita pierakstus", + "Exporting...": "Notiek eksportēšana...", + "Extend Schema": "Paplašināt shēmu", + "Extended": "Paplašināts", + "Extensions": "Paplašinājumi", + "Extra Query Parameters": "Papildu vaicājuma parametri", + "Extract Now": "Izvilkt tagad", + "Extract Pending Files": "Izvilkt gaidošos failus", + "Extract now": "Izvilkt tagad", + "Extract properties from each object": "Izvilkt īpašības no katra objekta", + "Extracted At": "Izvilkts", + "Extracted at": "Izvilkts", + "Extraction": "Izvilkšana", + "Extraction Mode": "Izvilkšanas režīms", + "Extraction Scope": "Izvilkšanas tvērums", + "Extraction Status": "Izvilkšanas statuss", + "Extraction error for {file}: {error}": "Izvilkšanas kļūda failam {file}: {error}", + "Facet Description": "Šķautnes apraksts", + "Facet Order": "Šķautnes secība", + "Facet Title": "Šķautnes nosaukums", + "Facet Type": "Šķautnes tips", + "Facet configuration updated successfully": "Šķautnes konfigurācija veiksmīgi atjaunināta", + "Facetable": "Šķautņojams", + "Faceting Configuration:": "Šķautņošanas konfigurācija:", + "Facets discovered and configured successfully": "Šķautnes veiksmīgi atklātas un konfigurētas", + "Facets discovered successfully": "Šķautnes veiksmīgi atklātas", + "Failed": "Neizdevās", + "Failed to add attachment to object": "Neizdevās pievienot pielikumu objektam", + "Failed to add user to organisation": "Neizdevās pievienot lietotāju organizācijai", + "Failed to analyze schema properties": "Neizdevās analizēt shēmas īpašības", + "Failed to archive conversation": "Neizdevās arhivēt sarunu", + "Failed to calculate sizes": "Neizdevās aprēķināt izmērus", + "Failed to cancel deactivation": "Neizdevās atcelt deaktivizāciju", + "Failed to change active organisation: {error}": "Neizdevās nomainīt aktīvo organizāciju: {error}", + "Failed to change password": "Neizdevās nomainīt paroli", + "Failed to check version: {error}": "Neizdevās pārbaudīt versiju: {error}", + "Failed to clear audit trails: {error}": "Neizdevās notīrīt audita pierakstus: {error}", + "Failed to clear blob objects: {error}": "Neizdevās notīrīt blob objektus: {error}", + "Failed to clear cache: {error}": "Neizdevās notīrīt kešatmiņu: {error}", + "Failed to clear collection": "Neizdevās notīrīt kolekciju", + "Failed to clear conversation": "Neizdevās notīrīt sarunu", + "Failed to clear embeddings: {error}": "Neizdevās notīrīt iegultos vektorus: {error}", + "Failed to clear search trails: {error}": "Neizdevās notīrīt meklēšanas pierakstus: {error}", + "Failed to clear {type} cache: {error}": "Neizdevās notīrīt {type} kešatmiņu: {error}", + "Failed to copy changes": "Neizdevās kopēt izmaiņas", + "Failed to copy data": "Neizdevās kopēt datus", + "Failed to copy data to clipboard": "Neizdevās kopēt datus uz starpliktuvi", + "Failed to copy to clipboard": "Neizdevās kopēt uz starpliktuvi", + "Failed to copy token": "Neizdevās kopēt pilnvaru", + "Failed to create conversation": "Neizdevās izveidot sarunu", + "Failed to create missing SOLR fields: {error}": "Neizdevās izveidot trūkstošos SOLR laukus: {error}", + "Failed to create or find project": "Neizdevās izveidot vai atrast projektu", + "Failed to create token": "Neizdevās izveidot pilnvaru", + "Failed to delete SOLR collection: {error}": "Neizdevās dzēst SOLR kolekciju: {error}", + "Failed to delete collection": "Neizdevās dzēst kolekciju", + "Failed to delete conversation": "Neizdevās dzēst sarunu", + "Failed to delete objects for {schema}: {error}": "Neizdevās dzēst objektus shēmai {schema}: {error}", + "Failed to delete webhook": "Neizdevās dzēst tīmekļa āķi", + "Failed to depublish register: {error}": "Neizdevās atpublicēt reģistru: {error}", + "Failed to depublish schema: {error}": "Neizdevās atpublicēt shēmu: {error}", + "Failed to depublish: {error}": "Neizdevās atpublicēt: {error}", + "Failed to discover files: {error}": "Neizdevās atklāt failus: {error}", + "Failed to download API specification": "Neizdevās lejupielādēt API specifikāciju", + "Failed to export data": "Neizdevās eksportēt datus", + "Failed to export objects": "Neizdevās eksportēt objektus", + "Failed to fetch conversation history": "Neizdevās ielādēt sarunu vēsturi", + "Failed to fix mismatched SOLR fields: {error}": "Neizdevās izlabot neatbilstošos SOLR laukus: {error}", + "Failed to get SOLR field configuration": "Neizdevās iegūt SOLR lauka konfigurāciju", + "Failed to get chat statistics": "Neizdevās iegūt tērzēšanas statistiku", + "Failed to get database information: %s": "Neizdevās iegūt datu bāzes informāciju: %s", + "Failed to get entity categories": "Neizdevās iegūt entītiju kategorijas", + "Failed to get entity statistics": "Neizdevās iegūt entītiju statistiku", + "Failed to get entity types": "Neizdevās iegūt entītiju tipus", + "Failed to get response: {error}": "Neizdevās iegūt atbildi: {error}", + "Failed to invalidate app store cache: {error}": "Neizdevās anulēt lietotņu veikala kešatmiņu: {error}", + "Failed to leave organisation: {error}": "Neizdevās pamest organizāciju: {error}", + "Failed to link object": "Neizdevās saistīt objektu", + "Failed to load Deck cards": "Neizdevās ielādēt Deck kartes", + "Failed to load LLM configuration": "Neizdevās ielādēt LLM konfigurāciju", + "Failed to load Nextcloud groups": "Neizdevās ielādēt Nextcloud grupas", + "Failed to load cache statistics: {error}": "Neizdevās ielādēt kešatmiņas statistiku: {error}", + "Failed to load conversation": "Neizdevās ielādēt sarunu", + "Failed to load entities": "Neizdevās ielādēt entītijas", + "Failed to load entity": "Neizdevās ielādēt entītiju", + "Failed to load extraction data": "Neizdevās ielādēt izvilkšanas datus", + "Failed to load files": "Neizdevās ielādēt failus", + "Failed to load linked contacts": "Neizdevās ielādēt saistītos kontaktus", + "Failed to load linked emails": "Neizdevās ielādēt saistītos e-pastus", + "Failed to load linked events": "Neizdevās ielādēt saistītos notikumus", + "Failed to load organisations": "Neizdevās ielādēt organizācijas", + "Failed to load register data": "Neizdevās ielādēt reģistra datus", + "Failed to load related entities": "Neizdevās ielādēt saistītās entītijas", + "Failed to load settings: {error}": "Neizdevās ielādēt iestatījumus: {error}", + "Failed to load statistics: {error}": "Neizdevās ielādēt statistiku: {error}", + "Failed to load templates": "Neizdevās ielādēt veidnes", + "Failed to load view: {error}": "Neizdevās ielādēt skatu: {error}", + "Failed to load webhooks": "Neizdevās ielādēt tīmekļa āķus", + "Failed to load workflows": "Neizdevās ielādēt darbplūsmas", + "Failed to process message": "Neizdevās apstrādāt ziņojumu", + "Failed to publish register: {error}": "Neizdevās publicēt reģistru: {error}", + "Failed to publish schema: {error}": "Neizdevās publicēt shēmu: {error}", + "Failed to publish: {error}": "Neizdevās publicēt: {error}", + "Failed to refresh database information": "Neizdevās atsvaidzināt datu bāzes informāciju", + "Failed to reindex collection": "Neizdevās atkārtoti indeksēt kolekciju", + "Failed to remove avatar": "Neizdevās noņemt avataru", + "Failed to remove link": "Neizdevās noņemt saiti", + "Failed to remove member: {error}": "Neizdevās noņemt dalībnieku: {error}", + "Failed to remove schema {schema}: {error}": "Neizdevās noņemt shēmu {schema}: {error}", + "Failed to remove user from organisation": "Neizdevās noņemt lietotāju no organizācijas", + "Failed to rename conversation": "Neizdevās pārdēvēt sarunu", + "Failed to request deactivation": "Neizdevās pieprasīt deaktivizāciju", + "Failed to restore conversation": "Neizdevās atjaunot sarunu", + "Failed to retrieve extraction status.": "Neizdevās iegūt izvilkšanas statusu.", + "Failed to retrieve objects for file.": "Neizdevās iegūt objektus failam.", + "Failed to retry extraction": "Neizdevās atkārtot izvilkšanu", + "Failed to retry webhook": "Neizdevās atkārtot tīmekļa āķi", + "Failed to revoke token": "Neizdevās atsaukt pilnvaru", + "Failed to save GitHub token": "Neizdevās saglabāt GitHub pilnvaru", + "Failed to save GitLab URL": "Neizdevās saglabāt GitLab URL", + "Failed to save GitLab token": "Neizdevās saglabāt GitLab pilnvaru", + "Failed to save LLM settings: {error}": "Neizdevās saglabāt LLM iestatījumus: {error}", + "Failed to save additional feedback": "Neizdevās saglabāt papildu atsauksmes", + "Failed to save configuration.": "Neizdevās saglabāt konfigurāciju.", + "Failed to save configuration: {error}": "Neizdevās saglabāt konfigurāciju: {error}", + "Failed to save facet configuration": "Neizdevās saglabāt šķautnes konfigurāciju", + "Failed to save feedback": "Neizdevās saglabāt atsauksmes", + "Failed to save file settings: {error}": "Neizdevās saglabāt failu iestatījumus: {error}", + "Failed to save n8n configuration": "Neizdevās saglabāt n8n konfigurāciju", + "Failed to save preferences": "Neizdevās saglabāt preferences", + "Failed to save roles": "Neizdevās saglabāt lomas", + "Failed to save settings": "Neizdevās saglabāt iestatījumus", + "Failed to save settings: {error}": "Neizdevās saglabāt iestatījumus: {error}", + "Failed to save view: {error}": "Neizdevās saglabāt skatu: {error}", + "Failed to save warmup interval: {error}": "Neizdevās saglabāt iesildīšanas intervālu: {error}", + "Failed to save webhook": "Neizdevās saglabāt tīmekļa āķi", + "Failed to send feedback": "Neizdevās nosūtīt atsauksmes", + "Failed to set active organisation: {error}": "Neizdevās iestatīt aktīvo organizāciju: {error}", + "Failed to setup SOLR: {error}": "Neizdevās iestatīt SOLR: {error}", + "Failed to start file warmup: {error}": "Neizdevās sākt failu iesildīšanu: {error}", + "Failed to start processing {type} files: {error}": "Neizdevās sākt {type} failu apstrādi: {error}", + "Failed to start vectorization: {error}": "Neizdevās sākt vektorizāciju: {error}", + "Failed to switch organisation: {error}": "Neizdevās pārslēgt organizāciju: {error}", + "Failed to sync magic table for {schema}: {error}": "Neizdevās sinhronizēt maģisko tabulu shēmai {schema}: {error}", + "Failed to test SOLR connection: {error}": "Neizdevās pārbaudīt SOLR savienojumu: {error}", + "Failed to test webhook": "Neizdevās pārbaudīt tīmekļa āķi", + "Failed to update LLM settings: {error}": "Neizdevās atjaunināt LLM iestatījumus: {error}", + "Failed to update Multitenancy settings: {error}": "Neizdevās atjaunināt vairāknomnieku iestatījumus: {error}", + "Failed to update RBAC settings: {error}": "Neizdevās atjaunināt RBAC iestatījumus: {error}", + "Failed to update Retention settings: {error}": "Neizdevās atjaunināt glabāšanas iestatījumus: {error}", + "Failed to update SOLR settings: {error}": "Neizdevās atjaunināt SOLR iestatījumus: {error}", + "Failed to update favorite status": "Neizdevās atjaunināt izlases statusu", + "Failed to update schema configuration for {schema}: {error}": "Neizdevās atjaunināt shēmas konfigurāciju shēmai {schema}: {error}", + "Failed to update schema properties": "Neizdevās atjaunināt shēmas īpašības", + "Failed to update view": "Neizdevās atjaunināt skatu", + "Failed to update view: {error}": "Neizdevās atjaunināt skatu: {error}", + "Failed to update webhook": "Neizdevās atjaunināt tīmekļa āķi", + "Failed to upload avatar": "Neizdevās augšupielādēt avataru", + "Failed to validate {schema}: {error}": "Neizdevās validēt {schema}: {error}", + "Failed to vectorize files: {error}": "Neizdevās vektorizēt failus: {error}", + "Failed to warmup names cache: {error}": "Neizdevās iesildīt nosaukumu kešatmiņu: {error}", + "Federation": "Federācija", + "Feedback recorded": "Atsauksmes reģistrētas", + "Fewer sources (1-3):": "Mazāk avotu (1-3):", + "Field": "Lauks", + "Fields to Display": "Attēlojamie lauki", + "File": "Fails", + "File #{id}": "Fails #{id}", + "File Chunk Prediction": "Faila fragmentu prognoze", + "File Collection": "Failu kolekcija", + "File Handling": "Failu apstrāde", + "File Location": "Faila atrašanās vieta", + "File Management": "Failu pārvaldība", + "File Name": "Faila nosaukums", + "File Path": "Faila ceļš", + "File Type Selection": "Faila tipa atlase", + "File Types to Process": "Apstrādājamie failu tipi", + "File Vectorization": "Failu vektorizācija", + "File Warmup": "Failu iesildīšana", + "File actions menu": "Faila darbību izvēlne", + "File anonymized successfully": "Fails veiksmīgi anonimizēts", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Faila fragmentu vektorizācija vēl nav ieviesta. Fragmenti ir sagatavoti un saglabāti, bet vektorizācijas pakalpojums ir izstrādes stadijā.", + "File collection not configured": "Failu kolekcija nav konfigurēta", + "File discovery completed": "Failu atklāšana pabeigta", + "File extraction completed": "Faila izvilkšana pabeigta", + "File extraction queued": "Faila izvilkšana ievietota rindā", + "File indexed successfully": "Fails veiksmīgi indeksēts", + "File is already anonymized": "Fails jau ir anonimizēts", + "File name": "Faila nosaukums", + "File not found": "Fails nav atrasts", + "File settings saved successfully": "Failu iestatījumi veiksmīgi saglabāti", + "File settings updated successfully": "Failu iestatījumi veiksmīgi atjaunināti", + "File sources": "Failu avoti", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Failu vektorizācija pabeigta! Vektorizēti {vectorized} fragmenti no {files} failiem. {failed} neizdevās.", + "File vectorization configuration saved successfully": "Failu vektorizācijas konfigurācija veiksmīgi saglabāta", + "File vectorization started. Check the statistics section for progress.": "Failu vektorizācija sākta. Pārbaudiet statistikas sadaļu, lai uzzinātu progresu.", + "File warmup completed": "Failu iesildīšana pabeigta", + "Files": "Faili", + "Files added successfully": "Faili veiksmīgi pievienoti", + "Files with Completed Extraction:": "Faili ar pabeigtu izvilkšanu:", + "Files → fileCollection, Objects → objectCollection": "Faili → fileCollection, Objekti → objectCollection", + "Files → filecollection, objects → objectcollection": "Faili → filecollection, objekti → objectcollection", + "Filter Audit Trails": "Filtrēt audita pierakstus", + "Filter Deleted Items": "Filtrēt dzēstos vienumus", + "Filter Objects": "Filtrēt objektus", + "Filter Properties": "Filtrēt īpašības", + "Filter Search Trails": "Filtrēt meklēšanas pierakstus", + "Filter Statistics": "Filtrēt statistiku", + "Filter and analyze search trail entries": "Filtrējiet un analizējiet meklēšanas pierakstu ierakstus", + "Filter and manage audit trail entries": "Filtrējiet un pārvaldiet audita pierakstu ierakstus", + "Filter and manage soft deleted items": "Filtrējiet un pārvaldiet mīksti dzēstos vienumus", + "Filter and search entities": "Filtrējiet un meklējiet entītijas", + "Filter by object ID": "Filtrēt pēc objekta ID", + "Filter by search term": "Filtrēt pēc meklēšanas termina", + "Filter by type": "Filtrēt pēc tipa", + "Filter by webhook": "Filtrēt pēc tīmekļa āķa", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filtra dati ielādēti automātiski. Izmantojiet tālāk esošos filtrus, lai precizētu meklēšanu.", + "Filter fields...": "Filtrēt laukus...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrējiet tīmekļa āķu izsaucējus pēc lietderīgās slodzes īpašībām (viena rindā, formāts: key: value)", + "Filtered": "Filtrēts", + "Filters": "Filtri", + "Finalizing setup...": "Notiek iestatīšanas pabeigšana...", + "Fireworks AI Chat Settings": "Fireworks AI tērzēšanas iestatījumi", + "Fireworks AI Embedding Configuration": "Fireworks AI iegulšanas konfigurācija", + "First": "Pirmais", + "Fixed": "Fiksēts", + "For chat and retrieval-augmented generation": "Tērzēšanai un izguves papildinātajai ģenerēšanai", + "For vector embeddings and semantic search": "Vektoru iegulšanai un semantiskajai meklēšanai", + "Format": "Formāts", + "Format Issue": "Formāta problēma", + "Format constraint is missing": "Trūkst formāta ierobežojuma", + "From (e.g. -7 days or 2025-01-01)": "No (piem., -7 days vai 2025-01-01)", + "From Date": "No datuma", + "From date": "No datuma", + "Full data copied to clipboard": "Pilnie dati nokopēti uz starpliktuvi", + "Function Tools": "Funkciju rīki", + "General Issue": "Vispārīga problēma", + "Generate recommendations and confidence scores": "Ģenerēt ieteikumus un ticamības vērtējumus", + "Generate report": "Ģenerēt pārskatu", + "Generate the verantwoordingsdocument": "Ģenerēt verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Ģenerēt vektorus uzreiz, kad tiek izveidoti jauni objekti", + "Generated: {time}": "Ģenerēts: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Iegūstiet reāllaika ieskatu savas organizācijas datu stāvoklī, pievēršoties reģistriem, shēmu definīcijām un objektu glabāšanai un lietošanai.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Iegūstiet reāllaika ieskatu savas organizācijas datu stāvoklī, izaugsmes tendencēs un kvalitātes rādītājos.", + "GitHub Personal Access Token": "GitHub personiskās piekļuves pilnvara", + "GitHub token is valid": "GitHub pilnvara ir derīga", + "GitHub token is valid! Username: {username}": "GitHub pilnvara ir derīga! Lietotājvārds: {username}", + "GitHub token removed successfully": "GitHub pilnvara veiksmīgi noņemta", + "GitHub token saved successfully": "GitHub pilnvara veiksmīgi saglabāta", + "GitHub token test failed: {message}": "GitHub pilnvaras pārbaude neizdevās: {message}", + "GitLab Personal Access Token": "GitLab personiskās piekļuves pilnvara", + "GitLab URL saved successfully": "GitLab URL veiksmīgi saglabāts", + "GitLab token is valid": "GitLab pilnvara ir derīga", + "GitLab token is valid! Username: {username}": "GitLab pilnvara ir derīga! Lietotājvārds: {username}", + "GitLab token saved successfully": "GitLab pilnvara veiksmīgi saglabāta", + "GitLab token test failed: {message}": "GitLab pilnvaras pārbaude neizdevās: {message}", + "Go to linked Register": "Doties uz saistīto Reģistru", + "Go to linked Schema": "Doties uz saistīto Shēmu", + "Go to organisation": "Doties uz organizāciju", + "Grant additional permissions beyond standard CRUD operations": "Piešķirt papildu atļaujas papildus standarta CRUD darbībām", + "Group Quota": "Grupas kvota", + "Groups": "Grupas", + "HTML preview": "HTML priekšskatījums", + "HTTP Method": "HTTP metode", + "HTTP method used to send webhook requests": "HTTP metode, ko izmanto tīmekļa āķa pieprasījumu sūtīšanai", + "Headers": "Galvenes", + "Health": "Stāvoklis", + "Heartbeat successful - connection kept alive": "Sirdspuksts veiksmīgs — savienojums uzturēts dzīvs", + "Helpful": "Noderīgs", + "Here you can configure the details for various connections.": "Šeit varat konfigurēt detaļas dažādiem savienojumiem.", + "Here you can configure the details for your organisation.": "Šeit varat konfigurēt savas organizācijas detaļas.", + "Hide": "Slēpt", + "Hide Filters": "Slēpt filtrus", + "Hide in collection view": "Slēpt kolekcijas skatā", + "Hide in forms": "Slēpt formās", + "Hide in list view": "Slēpt saraksta skatā", + "High": "Augsts", + "High Confidence": "Augsta ticamība", + "Host": "Resursdators", + "Hourly": "Reizi stundā", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Cik dziļi šķērsot ligzdotās objektu īpašības (1-20). Lielākas vērtības tver vairāk detaļu, bet palielina vektora izmēru.", + "How often to check for updates (1-168 hours)": "Cik bieži pārbaudīt atjauninājumus (1-168 stundas)", + "How to handle retries for failed webhook deliveries": "Kā apstrādāt atkārtotus mēģinājumus neizdevušamies tīmekļa āķu piegādēm", + "IBANs": "IBAN numuri", + "ID": "ID", + "ID of input mapping (optional)": "Ievades kartējuma ID (neobligāti)", + "ID of output mapping (optional)": "Izvades kartējuma ID (neobligāti)", + "ID of the target resource": "Mērķa resursa ID", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "IP ātruma ierobežojumi veiksmīgi notīrīti", + "Identify properties not in the schema": "Identificēt īpašības, kas nav shēmā", + "Immutable": "Nemainīgs", + "Import": "Importēt", + "Import Configuration": "Importa konfigurācija", + "Import Data into Register": "Importēt datus reģistrā", + "Import successful": "Imports veiksmīgs", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importējiet report-bundle.json veidni, lai iegūtu `reports` reģistru, pēc tam izveidojiet savu pirmo informācijas paneli, izmantojot standarta objektu lietotāja saskarni. Informācijas paneļi deklarē savus logrīkus JSON formātā, un renderētājs katram logrīkam piegādā reāllaika apkopošanas datus.", + "Improved Property": "Uzlabotā īpašība", + "In use": "Lietošanā", + "Inactive": "Neaktīvs", + "Inactive organisations cannot be used": "Neaktīvas organizācijas nevar izmantot", + "Include IDs and names of related objects for better contextual search": "Iekļaut saistīto objektu ID un nosaukumus labākai kontekstuālajai meklēšanai", + "Include ids and names of related objects for better contextual search": "Iekļaut saistīto objektu identifikatorus un nosaukumus labākai kontekstuālajai meklēšanai", + "Include related object references": "Iekļaut saistīto objektu atsauces", + "Include schema and register metadata": "Iekļaut shēmas un reģistra metadatus", + "Index": "Indekss", + "Information about the current OpenRegister installation": "Informācija par pašreizējo OpenRegister instalāciju", + "Inherited from register": "Mantots no reģistra", + "Inherits permissions from register": "Manto atļaujas no reģistra", + "Initialization failed": "Inicializācija neizdevās", + "Initialize Project": "Inicializēt projektu", + "Initializing SOLR setup...": "Notiek SOLR iestatīšanas inicializācija...", + "Input Mapping": "Ievades kartējums", + "Inspect Fields": "Pārbaudīt laukus", + "Inspect Index": "Pārbaudīt indeksu", + "Inspect SOLR Index": "Pārbaudīt SOLR indeksu", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instalējiet notify_push lietotni no Nextcloud lietotņu veikala, lai iespējotu reāllaika atjauninājumus.", + "Integrations": "Integrācijas", + "Internet location (URL) of this installation": "Šīs instalācijas atrašanās vieta internetā (URL)", + "Interval": "Intervāls", + "Invalid": "Nederīgs", + "Invalid batch size. Must be between 1 and 5000": "Nederīgs partijas izmērs. Jābūt no 1 līdz 5000", + "Invalid feedback type": "Nederīgs atsauksmju tips", + "Invalid field name provided": "Norādīts nederīgs lauka nosaukums", + "Invalid maxObjects. Must be 0 (all) or positive number": "Nederīgs maxObjects. Jābūt 0 (visi) vai pozitīvam skaitlim", + "Invalid:": "Nederīgs:", + "Invite Users": "Uzaicināt lietotājus", + "Invited Users": "Uzaicinātie lietotāji", + "Inzage (Art 15)": "Inzage (15. pants)", + "Inzage results": "Inzage rezultāti", + "Issue": "Problēma", + "Items per page:": "Vienumi lapā:", + "Just now": "Tikko", + "Key": "Atslēga", + "LLM Configuration": "LLM konfigurācija", + "LLM actions menu": "LLM darbību izvēlne", + "LLM configuration saved successfully": "LLM konfigurācija veiksmīgi saglabāta", + "LLM features disabled": "LLM funkcijas atspējotas", + "LLM features enabled": "LLM funkcijas iespējotas", + "LLM must be enabled with an embedding provider configured": "LLM jābūt iespējotam ar konfigurētu iegulšanas nodrošinātāju", + "LLM settings saved successfully": "LLM iestatījumi veiksmīgi saglabāti", + "LLM settings updated successfully": "LLM iestatījumi veiksmīgi atjaunināti", + "Label": "Etiķete", + "Labels": "Etiķetes", + "Languages": "Valodas", + "Last": "Pēdējais", + "Last Triggered": "Pēdējoreiz aktivizēts", + "Leave empty to allow all users access": "Atstājiet tukšu, lai atļautu piekļuvi visiem lietotājiem", + "Leave off for auto-detection from property format.": "Atstājiet izslēgtu automātiskai noteikšanai no īpašības formāta.", + "Left organisation successfully": "Organizācija veiksmīgi pamesta", + "Legal basis *": "Juridiskais pamats *", + "Length Range:": "Garuma diapazons:", + "Linear": "Lineārs", + "Link": "Saite", + "Link an email from the Mail app sidebar to associate it with this object.": "Saistiet e-pastu no Mail lietotnes sānjoslas, lai to saistītu ar šo objektu.", + "Link an object to see it here.": "Saistiet objektu, lai to redzētu šeit.", + "Link another object": "Saistīt citu objektu", + "Link existing event": "Saistīt esošu notikumu", + "Link removed": "Saite noņemta", + "Link to Object": "Saistīt ar objektu", + "Link to {name}": "Saite uz {name}", + "Linked Objects": "Saistītie objekti", + "Linked by {user}": "Saistīja {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Saistītie e-pasti, kalendāra notikumi, kontakti un Deck kartes parādīsies šeit.", + "Linked to {name}": "Saistīts ar {name}", + "Load Advanced Filters": "Ielādēt papildu filtrus", + "Load advanced filters with live data from your search index": "Ielādēt papildu filtrus ar reāllaika datiem no jūsu meklēšanas indeksa", + "Load more": "Ielādēt vairāk", + "Load view": "Ielādēt skatu", + "Loading API tokens...": "Notiek API pilnvaru ielāde...", + "Loading ConfigSets...": "Notiek ConfigSets ielāde...", + "Loading LLM configuration...": "Notiek LLM konfigurācijas ielāde...", + "Loading RBAC settings...": "Notiek RBAC iestatījumu ielāde...", + "Loading activity...": "Notiek aktivitātes ielāde...", + "Loading advanced filters...": "Notiek papildu filtru ielāde...", + "Loading agents...": "Notiek aģentu ielāde...", + "Loading application details...": "Notiek lietojumprogrammas detaļu ielāde...", + "Loading applications...": "Notiek lietojumprogrammu ielāde...", + "Loading archived conversations...": "Notiek arhivēto sarunu ielāde...", + "Loading audit trails...": "Notiek audita pierakstu ielāde...", + "Loading available tools...": "Notiek pieejamo rīku ielāde...", + "Loading cache statistics...": "Notiek kešatmiņas statistikas ielāde...", + "Loading collections...": "Notiek kolekciju ielāde...", + "Loading configurations...": "Notiek konfigurāciju ielāde...", + "Loading conversation...": "Notiek sarunas ielāde...", + "Loading conversations...": "Notiek sarunu ielāde...", + "Loading deleted items...": "Notiek dzēsto vienumu ielāde...", + "Loading endpoints": "Notiek galapunktu ielāde", + "Loading entities...": "Notiek entītiju ielāde...", + "Loading events...": "Notiek notikumu ielāde...", + "Loading filter data...": "Notiek filtra datu ielāde...", + "Loading groups...": "Notiek grupu ielāde...", + "Loading linked objects...": "Notiek saistīto objektu ielāde...", + "Loading log details...": "Notiek žurnāla detaļu ielāde...", + "Loading multitenancy settings...": "Notiek vairāknomnieku iestatījumu ielāde...", + "Loading n8n configuration...": "Notiek n8n konfigurācijas ielāde...", + "Loading permission matrix...": "Notiek atļauju matricas ielāde...", + "Loading preferences...": "Notiek preferenču ielāde...", + "Loading register data...": "Notiek reģistra datu ielāde...", + "Loading registers...": "Notiek reģistru ielāde...", + "Loading retention settings...": "Notiek glabāšanas iestatījumu ielāde...", + "Loading schemas...": "Notiek shēmu ielāde...", + "Loading search configuration...": "Notiek meklēšanas konfigurācijas ielāde...", + "Loading search trails...": "Notiek meklēšanas pierakstu ielāde...", + "Loading sources...": "Notiek avotu ielāde...", + "Loading statistics...": "Notiek statistikas ielāde...", + "Loading text extraction settings...": "Notiek teksta izvilkšanas iestatījumu ielāde...", + "Loading tokens...": "Notiek pilnvaru ielāde...", + "Loading user groups...": "Notiek lietotāju grupu ielāde...", + "Loading users...": "Notiek lietotāju ielāde...", + "Loading views...": "Notiek skatu ielāde...", + "Loading...": "Notiek ielāde...", + "Local": "Lokāls", + "Local Version": "Lokālā versija", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Atrodiet katru objektu, kas atsaucas uz datu subjektu (15. pants inzage), priekšskatiet dzēšanu (17. pants vergetelheid) vai eksportējiet to datus (20. pants portabiliteit).", + "Location": "Atrašanās vieta", + "Location Field": "Atrašanās vietas lauks", + "Locations": "Atrašanās vietas", + "Locked": "Bloķēts", + "Locked:": "Bloķēts:", + "Login successful": "Pieteikšanās veiksmīga", + "Logs": "Žurnāli", + "Low": "Zems", + "Low Confidence": "Zema ticamība", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Mazāki skaitļi parādās pirmie filtru sānjoslā. Atstājiet tukšu automātiskai kārtošanai.", + "Lucene Version:": "Lucene versija:", + "Magic table sync completed for {schema}": "Maģiskās tabulas sinhronizācija pabeigta shēmai {schema}", + "Magic table synchronized successfully": "Maģiskā tabula veiksmīgi sinhronizēta", + "Mail Integration": "Pasta integrācija", + "Mail integration is not available": "Pasta integrācija nav pieejama", + "Make this installation discoverable within the federation network.": "Padariet šo instalāciju atklājamu federācijas tīklā.", + "Manage Organisation Roles": "Pārvaldīt organizācijas lomas", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Pārvaldiet SOLR kolekcijas (datu krātuves) un piešķiriet tās objektiem un failiem.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Pārvaldiet SOLR ConfigSets (konfigurācijas veidnes) savām kolekcijām.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Pārvaldiet SOLR kolekcijas (datu krātuves) un piešķiriet tās objektiem un failiem.", + "Manage SOLR configsets (configuration templates) for your collections.": "Pārvaldiet SOLR konfigurācijas kopas (konfigurācijas veidnes) savām kolekcijām.", + "Manage and configure agents for automated tasks": "Pārvaldiet un konfigurējiet aģentus automatizētiem uzdevumiem", + "Manage and monitor file text extraction status": "Pārvaldiet un uzraugiet failu teksta izvilkšanas statusu", + "Manage and restore soft deleted items from your registers": "Pārvaldiet un atjaunojiet mīksti dzēstos vienumus no saviem reģistriem", + "Manage and view detected entities from files and objects": "Pārvaldiet un skatiet atklātās entītijas no failiem un objektiem", + "Manage document templates and themes": "Pārvaldiet dokumentu veidnes un tēmas", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Pārvaldiet n8n darbplūsmas OpenRegister automatizācijai. Darbplūsmas tiks glabātas konfigurētajā projektā.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Pārvaldiet apstrādes darbības, izpildiet datu subjektu piekļuves pieprasījumus un auditējiet atbilstību saskaņā ar ES GDPR / Nīderlandes AVG.", + "Manage webhooks for event-driven integrations": "Pārvaldiet tīmekļa āķus uz notikumiem balstītām integrācijām", + "Manage your account settings, security, and personal data.": "Pārvaldiet sava konta iestatījumus, drošību un personas datus.", + "Manage your applications and modules": "Pārvaldiet savas lietojumprogrammas un moduļus", + "Manage your chat conversations": "Pārvaldiet savas tērzēšanas sarunas", + "Manage your data registers and their configurations": "Pārvaldiet savus datu reģistrus un to konfigurācijas", + "Manage your data schemas and their properties": "Pārvaldiet savas datu shēmas un to īpašības", + "Manage your data sources and their configurations": "Pārvaldiet savus datu avotus un to konfigurācijas", + "Manage your organisations and switch between them": "Pārvaldiet savas organizācijas un pārslēdzieties starp tām", + "Manage your saved search configurations": "Pārvaldiet savas saglabātās meklēšanas konfigurācijas", + "Manage your system configurations and settings": "Pārvaldiet savas sistēmas konfigurācijas un iestatījumus", + "Managed": "Pārvaldīts", + "Mappings": "Kartējumi", + "Mass validation completed successfully": "Masveida validācija veiksmīgi pabeigta", + "Mass validation failed: {error}": "Masveida validācija neizdevās: {error}", + "Max Files (0 = all)": "Maks. faili (0 = visi)", + "Max Length": "Maks. garums", + "Max Length:": "Maks. garums:", + "Max Objects (0 = all)": "Maks. objekti (0 = visi)", + "Max Retries": "Maks. atkārtotie mēģinājumi", + "Max Shards/Node": "Maks. fragmenti/mezgls", + "Max Tokens": "Maks. marķieri", + "Max execution time (ms)": "Maks. izpildes laiks (ms)", + "Max ms": "Maks. ms", + "Max result count": "Maks. rezultātu skaits", + "Max results": "Maks. rezultāti", + "Maximum": "Maksimums", + "Maximum File Size (MB)": "Maksimālais faila izmērs (MB)", + "Maximum Nesting Depth": "Maksimālais ligzdošanas dziļums", + "Maximum length": "Maksimālais garums", + "Maximum length constraint is missing": "Trūkst maksimālā garuma ierobežojuma", + "Maximum length is too restrictive": "Maksimālais garums ir pārāk ierobežojošs", + "Maximum number of files to process. Set to 0 to process all files.": "Maksimālais apstrādājamo failu skaits. Iestatiet uz 0, lai apstrādātu visus failus.", + "Maximum number of items": "Maksimālais vienumu skaits", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksimālais apstrādājamo objektu skaits. Iestatiet uz 0, lai apstrādātu visus objektus.", + "Maximum number of retry attempts for failed deliveries": "Maksimālais atkārtoto mēģinājumu skaits neizdevušamies piegādēm", + "Maximum time to wait before committing changes": "Maksimālais gaidīšanas laiks pirms izmaiņu apstiprināšanas", + "Maximum tokens to generate": "Maksimālais ģenerējamo marķieru skaits", + "Maximum value": "Maksimālā vērtība", + "Maximum value constraint is missing": "Trūkst maksimālās vērtības ierobežojuma", + "Maximum value is too restrictive": "Maksimālā vērtība ir pārāk ierobežojoša", + "Medium": "Vidējs", + "Medium Confidence": "Vidēja ticamība", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Vidēja sarežģītība: meklējumi ar zināmu filtrēšanu vai vairākiem parametriem (piem., datumu diapazoni, konkrēti reģistri/shēmas)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Vidēja sarežģītība: meklējumi ar zināmu filtrēšanu vai vairākiem parametriem (piem., datumu diapazoni, konkrēti reģistri/shēmas)", + "Member removed successfully": "Dalībnieks veiksmīgi noņemts", + "Members": "Dalībnieki", + "Members ({count})": "Dalībnieki ({count})", + "Members of selected groups can access this view": "Atlasīto grupu dalībnieki var piekļūt šim skatam", + "Members:": "Dalībnieki:", + "Memory prediction calculated successfully": "Atmiņas prognoze veiksmīgi aprēķināta", + "Message content is required": "Ziņojuma saturs ir obligāts", + "Message does not belong to this conversation": "Ziņojums nepieder šai sarunai", + "Message not found": "Ziņojums nav atrasts", + "Metadata": "Metadati", + "Metadata Filters": "Metadatu filtri", + "Method": "Metode", + "Method*": "Metode*", + "Method: {method}": "Metode: {method}", + "Min Length": "Min. garums", + "Min execution time (ms)": "Min. izpildes laiks (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. rezultātu skaits", + "Min results": "Min. rezultāti", + "Minimum": "Minimums", + "Minimum length": "Minimālais garums", + "Minimum number of items": "Minimālais vienumu skaits", + "Minimum value": "Minimālā vērtība", + "Minimum value constraint is missing": "Trūkst minimālās vērtības ierobežojuma", + "Minimum value is too restrictive": "Minimālā vērtība ir pārāk ierobežojoša", + "Missing conversation or agentUuid": "Trūkst sarunas vai agentUuid", + "Missing conversationId": "Trūkst conversationId", + "Missing message": "Trūkst ziņojuma", + "Missing object fields created successfully": "Trūkstošie objekta lauki veiksmīgi izveidoti", + "Mode:": "Režīms:", + "Model": "Modelis", + "Model Name": "Modeļa nosaukums", + "Modified": "Modificēts", + "Monthly": "Reizi mēnesī", + "More sources (10-20):": "Vairāk avotu (10-20):", + "Most Active Objects": "Visaktīvākie objekti", + "Multiple Of": "Daudzkārtnis", + "Multiple of": "Daudzkārtnis", + "Multiple search trail deletion not implemented yet": "Vairāku meklēšanas pierakstu dzēšana vēl nav ieviesta", + "Multitenancy": "Vairāknomnieku režīms", + "Multitenancy settings updated successfully": "Vairāknomnieku iestatījumi veiksmīgi atjaunināti", + "My Account": "Mans konts", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Nosaukums", + "Name *": "Nosaukums *", + "Name*": "Nosaukums*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Nosaukumu kešatmiņa veiksmīgi uzsildīta: {count} nosaukumi ielādēti {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Nosaukumu kešatmiņa veiksmīgi uzsildīta: {count} nosaukumi ielādēti {time}. {message}", + "Names cache warmup completed": "Nosaukumu kešatmiņas uzsildīšana pabeigta", + "Naming pattern for copies:": "Nosaukumu shēma kopijām:", + "New Collection Name": "Jaunās kolekcijas nosaukums", + "New Configuration": "Jauna konfigurācija", + "New Conversation": "Jauna saruna", + "New Properties": "Jaunas īpašības", + "New Property": "Jauna īpašība", + "New User Group": "Jauna lietotāju grupa", + "New Value": "Jauna vērtība", + "New activity": "Jauna darbība", + "New password": "Jaunā parole", + "New processing activity": "Jauna apstrādes darbība", + "New users without specific organisation membership will be automatically added to this organisation": "Jaunie lietotāji bez konkrētas organizācijas piederības tiks automātiski pievienoti šai organizācijai", + "New verwerkingsactiviteit": "Jauna verwerkingsactiviteit", + "Next": "Tālāk", + "Nextcloud Groups": "Nextcloud grupas", + "No": "Nē", + "No API tokens.": "Nav API marķieru.", + "No ConfigSets found": "Nav atrasts neviens ConfigSets", + "No Deck cards linked to this object": "Šim objektam nav saistītu Deck kartīšu", + "No GitHub token configured": "Nav konfigurēts GitHub marķieris", + "No GitHub token provided": "Nav norādīts GitHub marķieris", + "No GitLab token provided": "Nav norādīts GitLab marķieris", + "No Organisation": "Nav organizācijas", + "No active filters": "Nav aktīvu filtru", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Shēmai {schema} nav aktīvu objektu, ko maigi dzēst. Lietojiet \"Neatgriezeniski dzēst\", lai noņemtu maigi dzēstos objektus.", + "No activity data available": "Nav pieejamu darbības datu", + "No activity found.": "Nav atrasta neviena darbība.", + "No agents available": "Nav pieejamu aģentu", + "No agents found": "Nav atrasts neviens aģents", + "No app store cache exists yet": "Lietotņu veikala kešatmiņa vēl nepastāv", + "No applications are available.": "Nav pieejamu lietojumprogrammu.", + "No applications found": "Nav atrasta neviena lietojumprogramma", + "No approval steps for this object.": "Šim objektam nav apstiprināšanas soļu.", + "No archived conversations": "Nav arhivētu sarunu", + "No audit trail entries found": "Nav atrasts neviens audita pieraksta ieraksts", + "No changes recorded": "Nav reģistrētu izmaiņu", + "No chunks to vectorize": "Nav fragmentu, ko vektorizēt", + "No collections found": "Nav atrasta neviena kolekcija", + "No configuration data": "Nav konfigurācijas datu", + "No configuration selected": "Nav atlasīta neviena konfigurācija", + "No configurations are available.": "Nav pieejamu konfigurāciju.", + "No configurations found": "Nav atrasta neviena konfigurācija", + "No configurations found for this application.": "Šai lietojumprogrammai nav atrasta neviena konfigurācija.", + "No contacts linked to this object": "Šim objektam nav saistītu kontaktu", + "No conversations yet. Create a new one to get started!": "Vēl nav nevienas sarunas. Izveidojiet jaunu, lai sāktu!", + "No dashboards yet": "Vēl nav neviena informācijas paneļa", + "No data available": "Nav pieejamu datu", + "No data available for chart": "Nav pieejamu datu diagrammai", + "No deleted items found": "Nav atrasts neviens dzēsts vienums", + "No deletion data available": "Nav pieejamu dzēšanas datu", + "No description found": "Nav atrasts apraksts", + "No description provided": "Apraksts nav norādīts", + "No emails linked to this object": "Šim objektam nav saistītu e-pasta ziņojumu", + "No endpoints defined": "Nav definēts neviens galapunkts", + "No entities detected for this email.": "Šim e-pasta ziņojumam nav atklāta neviena entītija.", + "No entities detected in this file. Run text extraction first.": "Šajā failā nav atklāta neviena entītija. Vispirms veiciet teksta izvilkšanu.", + "No entities found": "Nav atrasta neviena entītija", + "No entities have been detected yet": "Vēl nav atklāta neviena entītija", + "No events found": "Nav atrasts neviens notikums", + "No events linked to this object": "Šim objektam nav saistītu notikumu", + "No expired audit trails found to clear": "Nav atrasts neviens beidzies audita pieraksts, ko notīrīt", + "No expired search trails found to clear": "Nav atrasts neviens beidzies meklēšanas pieraksts, ko notīrīt", + "No extraction data available for this file": "Šim failam nav pieejamu izvilkšanas datu", + "No facetable fields available. Select a register and schema to see available filters.": "Nav pieejamu fasetējamu lauku. Atlasiet reģistru un shēmu, lai redzētu pieejamos filtrus.", + "No files found": "Nav atrasts neviens fails", + "No files have been extracted yet": "Vēl nav izvilkts neviens fails", + "No files selected": "Nav atlasīts neviens fails", + "No files to process": "Nav failu, ko apstrādāt", + "No files to reindex": "Nav failu, ko atkārtoti indeksēt", + "No filters are currently active. This will delete ALL audit trail entries!": "Pašlaik nav aktīvu filtru. Šādi tiks dzēsti VISI audita pieraksta ieraksti!", + "No groups found. Try a different search.": "Nav atrasta neviena grupa. Izmēģiniet citu meklēšanu.", + "No label": "Nav etiķetes", + "No labels": "Nav etiķešu", + "No linked objects": "Nav saistītu objektu", + "No log entries found": "Nav atrasts neviens žurnāla ieraksts", + "No logs are available for this configuration.": "Šai konfigurācijai nav pieejamu žurnālu.", + "No logs are available for this source.": "Šim avotam nav pieejamu žurnālu.", + "No logs found": "Nav atrasts neviens žurnāls", + "No matches": "Nav atbilstību", + "No mismatched fields found - SOLR schema is properly configured": "Nav atrasts neviens neatbilstošs lauks — SOLR shēma ir pareizi konfigurēta", + "No objects are currently selected for deletion.": "Pašlaik nav atlasīts neviens objekts dzēšanai.", + "No objects are currently selected for permanent deletion.": "Pašlaik nav atlasīts neviens objekts neatgriezeniskai dzēšanai.", + "No objects are currently selected for restoration.": "Pašlaik nav atlasīts neviens objekts atjaunošanai.", + "No objects found": "Nav atrasts neviens objekts", + "No objects found to vectorize": "Nav atrasts neviens objekts, ko vektorizēt", + "No objects linked to this email": "Šim e-pasta ziņojumam nav saistītu objektu", + "No objects selected": "Nav atlasīts neviens objekts", + "No objects selected for deletion": "Nav atlasīts neviens objekts dzēšanai", + "No objects selected for restoration": "Nav atlasīts neviens objekts atjaunošanai", + "No objects to delete": "Nav objektu, ko dzēst", + "No objects to delete for schema {schema}": "Shēmai {schema} nav objektu, ko dzēst", + "No objects were permanently deleted": "Neviens objekts netika neatgriezeniski dzēsts", + "No organisations found": "Nav atrasta neviena organizācija", + "No personal data was found for this subject identifier.": "Šim subjekta identifikatoram netika atrasti personas dati.", + "No processing activities yet": "Vēl nav nevienas apstrādes darbības", + "No properties found": "Nav atrasta neviena īpašība", + "No properties match your filters.": "Neviena īpašība neatbilst jūsu filtriem.", + "No purge date set": "Nav iestatīts iztīrīšanas datums", + "No register data available": "Nav pieejamu reģistra datu", + "No register objects reference this file": "Neviens reģistra objekts neatsaucas uz šo failu", + "No registers found": "Nav atrasts neviens reģistrs", + "No registers found for this application.": "Šai lietojumprogrammai nav atrasts neviens reģistrs.", + "No registers found. Create a register to configure permissions.": "Nav atrasts neviens reģistrs. Izveidojiet reģistru, lai konfigurētu atļaujas.", + "No related cases found for this sender": "Šim sūtītājam nav atrasta neviena saistīta lieta", + "No related entities": "Nav saistītu entītiju", + "No relations found": "Nav atrasta neviena saistība", + "No request body available": "Nav pieejama pieprasījuma pamatteksta", + "No response body available": "Nav pieejama atbildes pamatteksta", + "No saved views yet. Create one in the Search tab!": "Vēl nav neviena saglabāta skata. Izveidojiet to cilnē Meklēšana!", + "No saved views yet. create one in the search tab!": "Vēl nav neviena saglabāta skata. izveidojiet to meklēšanas cilnē!", + "No scan run yet": "Vēl nav veikta neviena skenēšana", + "No schema data available": "Nav pieejamu shēmas datu", + "No schema selected for exploration": "Nav atlasīta neviena shēma izpētei", + "No schemas configured for mail linking.": "Nav konfigurēta neviena shēma pasta saistīšanai.", + "No schemas found": "Nav atrasta neviena shēma", + "No schemas found for this application.": "Šai lietojumprogrammai nav atrasta neviena shēma.", + "No search terms data available": "Nav pieejamu meklēšanas terminu datu", + "No search trail entries found": "Nav atrasts neviens meklēšanas pieraksta ieraksts", + "No sources are available.": "Nav pieejamu avotu.", + "No sources found": "Nav atrasts neviens avots", + "No synchronizations found": "Nav atrasta neviena sinhronizācija", + "No templates found": "Nav atrasta neviena veidne", + "No templates have been created yet": "Vēl nav izveidota neviena veidne", + "No tools available. Tools can be registered by installed apps.": "Nav pieejamu rīku. Rīkus var reģistrēt instalētās lietotnes.", + "No users in this organisation.": "Šajā organizācijā nav lietotāju.", + "No valid magic-mapped register+schema combinations found": "Nav atrasta neviena derīga magic-mapped reģistra+shēmas kombinācija", + "No views found. Create views first before configuring vectorization.": "Nav atrasts neviens skats. Vispirms izveidojiet skatus, pirms konfigurējat vektorizāciju.", + "No views match your search": "Neviens skats neatbilst jūsu meklējumam", + "No webhooks found": "Nav atrasts neviens tīmekļa āķis", + "No webhooks have been configured yet": "Vēl nav konfigurēts neviens tīmekļa āķis", + "No workflows found in this project. Create workflows in the n8n editor.": "Šajā projektā nav atrasta neviena darbplūsma. Izveidojiet darbplūsmas n8n redaktorā.", + "None": "Nav", + "Not Found": "Nav atrasts", + "Not extracted": "Nav izvilkts", + "Not helpful": "Nav noderīgs", + "Not in use": "Netiek lietots", + "Note:": "Piezīme:", + "Notifications": "Paziņojumi", + "Number Constraints": "Skaitļa ierobežojumi", + "Number of Sources": "Avotu skaits", + "Number of chunks to vectorize in one API call (1-100).": "Fragmentu skaits, ko vektorizēt vienā API izsaukumā (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Fragmentu skaits, ko vektorizēt vienā API izsaukumā. Lielāks = ātrāks, bet vairāk atmiņas. Ieteicams: 10-50.", + "Number of objects to process in each batch (1-5000).": "Objektu skaits, ko apstrādāt katrā partijā (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Objektu skaits, ko vektorizēt vienā API izsaukumā. Lielāks = ātrāks, bet vairāk atmiņas. Ieteicams: 10-50.", + "Numeric Range:": "Skaitliskais diapazons:", + "Object": "Objekts", + "Object #{id}": "Objekts #{id}", + "Object Collection": "Objektu kolekcija", + "Object Configuration:": "Objekta konfigurācija:", + "Object Count Prediction": "Objektu skaita prognoze", + "Object Extraction Mode": "Objektu izvilkšanas režīms", + "Object ID": "Objekta ID", + "Object Management": "Objektu pārvaldība", + "Object Text Extraction": "Objekta teksta izvilkšana", + "Object Vectorization": "Objektu vektorizācija", + "Object Warmup": "Objektu uzsildīšana", + "Object changes in owned objects": "Objektu izmaiņas piederošajos objektos", + "Object collection not configured": "Objektu kolekcija nav konfigurēta", + "Object metadata incomplete for file upload": "Objekta metadati ir nepilnīgi faila augšupielādei", + "Object not found": "Objekts nav atrasts", + "Object permanently deleted": "Objekts neatgriezeniski dzēsts", + "Object restored successfully": "Objekts veiksmīgi atjaunots", + "Object settings updated successfully": "Objekta iestatījumi veiksmīgi atjaunināti", + "Object sources": "Objektu avoti", + "Object successfully deleted": "Objekts veiksmīgi dzēsts", + "Object successfully modified": "Objekts veiksmīgi mainīts", + "Object unlocked successfully": "Objekts veiksmīgi atbloķēts", + "Object vectorization configuration saved successfully": "Objektu vektorizācijas konfigurācija veiksmīgi saglabāta", + "Object vectorized successfully": "Objekts veiksmīgi vektorizēts", + "Object:": "Objekts:", + "Objects": "Objekti", + "Objects Analyzed": "Analizētie objekti", + "Objects Distribution": "Objektu sadalījums", + "Objects being analyzed": "Objekti tiek analizēti", + "Objects by Register": "Objekti pēc reģistra", + "Objects by Schema": "Objekti pēc shēmas", + "Objects deletion completed for {schema}": "Objektu dzēšana pabeigta shēmai {schema}", + "Objects deletion completed successfully": "Objektu dzēšana veiksmīgi pabeigta", + "Objects in schema": "Objekti shēmā", + "Objects successfully deleted": "Objekti veiksmīgi dzēsti", + "Objects to Process:": "Apstrādājamie objekti:", + "Objects to be analyzed": "Analizējamie objekti", + "Objects to be deleted": "Dzēšamie objekti", + "Objects to be validated": "Validējamie objekti", + "Objects will be serialized as JSON text before vectorization": "Objekti pirms vektorizācijas tiks serializēti kā JSON teksts", + "Objects will be soft deleted and moved to the": "Objekti tiks maigi dzēsti un pārvietoti uz", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekti tiks maigi dzēsti (atzīmēti kā dzēsti, bet saglabāti datu bāzē). Vēlāk tos var atjaunot, ja nepieciešams.", + "Old Value": "Vecā vērtība", + "Oldest Item (days)": "Vecākais vienums (dienas)", + "Ollama Chat Settings": "Ollama tērzēšanas iestatījumi", + "Ollama Embedding Configuration": "Ollama iegulšanas konfigurācija", + "Ollama URL": "Ollama URL", + "Only With Changes": "Tikai ar izmaiņām", + "Only members of selected groups can access this organisation": "Tikai atlasīto grupu dalībnieki var piekļūt šai organizācijai", + "Open Nextcloud App Store": "Atvērt Nextcloud lietotņu veikalu", + "Open OpenRegister sidebar": "Atvērt OpenRegister sānjoslu", + "Open Register Objects": "Atvērt reģistra objektus", + "Open folder": "Atvērt mapi", + "Open in OpenRegister": "Atvērt OpenRegister", + "Open n8n Editor": "Atvērt n8n redaktoru", + "OpenAI Chat Settings": "OpenAI tērzēšanas iestatījumi", + "OpenAI Embedding Configuration": "OpenAI iegulšanas konfigurācija", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister iestatījumi", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operatora definēti informācijas paneļi un ieplānoti pārskati. Katrs informācijas panelis ir pilntiesīgs objekts `reports` reģistrā; logrīki ir deklarēti informācijas paneļa `widgets` masīvā un tiek atveidoti reāllaikā no agregācijām / GraphQL.", + "Optimizing search performance...": "Optimizē meklēšanas veiktspēju...", + "Optional URL-friendly identifier": "Neobligāts URL draudzīgs identifikators", + "Optional description for this facet": "Neobligāts šīs fasetes apraksts", + "Optional end date property": "Neobligāta beigu datuma īpašība", + "Optional event description template": "Neobligāta notikuma apraksta veidne", + "Optional location property": "Neobligāta atrašanās vietas īpašība", + "Optional webhook secret for signature verification": "Neobligāts tīmekļa āķa noslēpums paraksta verifikācijai", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Neobligāti: pievienojiet vaicājuma parametrus, lai filtrētu atsaukto shēmu (piem., status=active&type=public)", + "Or": "Vai", + "Order": "Secība", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Sakārtotas BCP 47 valodu birkas. Pirmā valoda ir reģistra noklusējums un nosaka Accept-Language rezerves variantu tulkojamajām īpašībām.", + "Organisation": "Organizācija", + "Organisation Configuration": "Organizācijas konfigurācija", + "Organisation ID": "Organizācijas ID", + "Organisation Members": "Organizācijas dalībnieki", + "Organisation Statistics": "Organizācijas statistika", + "Organisation created successfully": "Organizācija veiksmīgi izveidota", + "Organisation membership changes": "Organizācijas piederības izmaiņas", + "Organisation name is required": "Organizācijas nosaukums ir obligāts", + "Organisation settings saved successfully": "Organizācijas iestatījumi veiksmīgi saglabāti", + "Organisation successfully created": "Organizācija veiksmīgi izveidota", + "Organisation successfully updated": "Organizācija veiksmīgi atjaunināta", + "Organisational measures": "Organizatoriskie pasākumi", + "Organisations": "Organizācijas", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organizācija", + "Organization ID (Optional)": "Organizācijas ID (neobligāts)", + "Organizations": "Organizācijas", + "Orphaned Items": "Bezsaimnieka vienumi", + "Other": "Cits", + "Output Mapping": "Izvades kartēšana", + "Overview": "Pārskats", + "Owner": "Īpašnieks", + "Owner:": "Īpašnieks:", + "PDF": "PDF", + "PII hits": "PII trāpījumi", + "PII rows": "PII rindas", + "Page {current} of {total}": "Lapa {current} no {total}", + "Parallel Mode (Faster, more resource intensive)": "Paralēlais režīms (ātrāks, resursietilpīgāks)", + "Parallel:": "Paralēli:", + "Password": "Parole", + "Password changes are not supported by your authentication provider.": "Paroles maiņu jūsu autentifikācijas pakalpojumu sniedzējs neatbalsta.", + "Password for SOLR authentication (optional)": "Parole SOLR autentifikācijai (neobligāta)", + "Password updated successfully": "Parole veiksmīgi atjaunināta", + "Path": "Ceļš", + "Path in repository": "Ceļš repozitorijā", + "Path where the configuration file will be saved in the repository": "Ceļš, kur konfigurācijas fails tiks saglabāts repozitorijā", + "Path where the register OAS file will be saved in the repository": "Ceļš, kur reģistra OAS fails tiks saglabāts repozitorijā", + "Pattern (regex)": "Raksts (regulārā izteiksme)", + "Pattern Issue": "Raksta problēma", + "Pattern constraint is missing": "Trūkst raksta ierobežojuma", + "Patterns:": "Raksti:", + "Pending": "Gaida", + "Permanent Delete Mode": "Neatgriezeniskās dzēšanas režīms", + "Permanently Delete": "Neatgriezeniski dzēst", + "Permanently Delete ({count})": "Neatgriezeniski dzēst ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Neatgriezeniski dzēst visus {count} maigi dzēstos objektus. To nevar atsaukt!", + "Permanently delete already soft-deleted objects": "Neatgriezeniski dzēst jau maigi dzēstos objektus", + "Permanently delete this conversation?": "Vai neatgriezeniski dzēst šo sarunu?", + "Permission Matrix": "Atļauju matrica", + "Person": "Persona", + "Personal Data": "Personas dati", + "Personal Data Export": "Personas datu eksports", + "Persons": "Personas", + "Phone": "Tālrunis", + "Phone Numbers": "Tālruņu numuri", + "Photos": "Fotoattēli", + "Please create an agent in the": "Lūdzu, izveidojiet aģentu", + "Please select a user": "Lūdzu, atlasiet lietotāju", + "Please select an agent to continue": "Lūdzu, atlasiet aģentu, lai turpinātu", + "Please select an organisation": "Lūdzu, atlasiet organizāciju", + "Please select at least one item to import": "Lūdzu, atlasiet vismaz vienu vienumu importēšanai", + "Please select registers or schemas first": "Lūdzu, vispirms atlasiet reģistrus vai shēmas", + "Please select which register and schema to use for the new object": "Lūdzu, atlasiet, kuru reģistru un shēmu lietot jaunajam objektam", + "Please try again later.": "Lūdzu, mēģiniet vēlreiz vēlāk.", + "Please wait while we fetch your agents.": "Lūdzu, uzgaidiet, kamēr mēs ielādējam jūsu aģentus.", + "Please wait while we fetch your applications.": "Lūdzu, uzgaidiet, kamēr mēs ielādējam jūsu lietojumprogrammas.", + "Please wait while we fetch your configurations.": "Lūdzu, uzgaidiet, kamēr mēs ielādējam jūsu konfigurācijas.", + "Please wait while we fetch your deleted items.": "Lūdzu, uzgaidiet, kamēr mēs ielādējam jūsu dzēstos vienumus.", + "Please wait while we fetch your sources.": "Lūdzu, uzgaidiet, kamēr mēs ielādējam jūsu avotus.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Lūdzu, uzgaidiet, kamēr mēs neatgriezeniski dzēšam SOLR kolekciju. Tas var aizņemt dažus mirkļus.", + "Polls": "Aptaujas", + "Popular Search Terms": "Populārie meklēšanas termini", + "Port": "Ports", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Preferences saglabātas", + "Prerequisites": "Priekšnoteikumi", + "Press Enter to send, Shift+Enter for new line": "Nospiediet Enter, lai sūtītu, Shift+Enter jaunai rindai", + "Press enter to send, shift+enter for new line": "Nospiediet enter, lai sūtītu, shift+enter jaunai rindai", + "Prettify": "Sakārtot", + "Preview erasure": "Priekšskatīt dzēšanu", + "Preview:": "Priekšskatījums:", + "Previous": "Iepriekšējais", + "Private": "Privāts", + "Private Agent (Default)": "Privāts aģents (noklusējums)", + "Private agents": "Privātie aģenti", + "Process Name (optional)": "Procesa nosaukums (neobligāts)", + "Processed {indexed} files, {failed} failed": "Apstrādāti {indexed} faili, {failed} neizdevās", + "Processes chunks in batches with simulated parallelism.": "Apstrādā fragmentus partijās ar simulētu paralēlismu.", + "Processes file chunks sequentially (safest).": "Apstrādā faila fragmentus secīgi (visdrošākais).", + "Processes objects in chunks with simulated parallelism.": "Apstrādā objektus fragmentos ar simulētu paralēlismu.", + "Processes objects sequentially (safest).": "Apstrādā objektus secīgi (visdrošākais).", + "Processing": "Apstrāde", + "Processing Limits": "Apstrādes ierobežojumi", + "Processing...": "Apstrādā...", + "Project initialized successfully": "Projekts veiksmīgi inicializēts", + "Project not found. Please initialize first.": "Projekts nav atrasts. Lūdzu, vispirms inicializējiet.", + "Properties": "Īpašības", + "Property Behaviors": "Īpašības uzvedības", + "Property Configuration:": "Īpašības konfigurācija:", + "Property Filters": "Īpašību filtri", + "Property Title": "Īpašības nosaukums", + "Property Type": "Īpašības tips", + "Property can be improved": "Īpašību var uzlabot", + "Property could not be added": "Īpašību nevarēja pievienot", + "Property could not be updated": "Īpašību nevarēja atjaunināt", + "Property name of inversed relation": "Apvērstās saistības īpašības nosaukums", + "Property successfully added": "Īpašība veiksmīgi pievienota", + "Property successfully updated": "Īpašība veiksmīgi atjaunināta", + "Protected": "Aizsargāts", + "Provider": "Pakalpojumu sniedzējs", + "Provider is required for testing": "Pakalpojumu sniedzējs ir obligāts testēšanai", + "Public": "Publisks", + "Public View": "Publisks skats", + "Public views can be accessed by anyone in the system": "Publiskiem skatiem var piekļūt ikviens sistēmā", + "Publish": "Publicēt", + "Publish Configuration to GitHub": "Publicēt konfigurāciju GitHub", + "Publish OAS": "Publicēt OAS", + "Publish Objects": "Publicēt objektus", + "Publish Register OAS to GitHub": "Publicēt reģistra OAS GitHub", + "Publish all objects in this schema": "Publicēt visus objektus šajā shēmā", + "Publish objects to make them publicly available": "Publicēt objektus, lai padarītu tos publiski pieejamus", + "Published": "Publicēts", + "Published:": "Publicēts:", + "Publishing...": "Publicē...", + "Purge": "Iztīrīt", + "Purge Date": "Iztīrīšanas datums", + "Purge {count} object from database": "Iztīrīt {count} objektu no datu bāzes", + "Purge {count} objects from database": "Iztīrīt {count} objektus no datu bāzes", + "Purpose limitation *": "Mērķa ierobežojums *", + "Push Notifications": "Tūlītējie paziņojumi", + "Query Complexity Distribution": "Vaicājumu sarežģītības sadalījums", + "Query Help": "Vaicājumu palīdzība", + "Query parameter is required": "Vaicājuma parametrs ir obligāts", + "Quota": "Kvota", + "RAG": "RAG", + "RAG Configuration": "RAG konfigurācija", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC iestatījumi veiksmīgi atjaunināti", + "Rate limits cleared successfully": "Pieprasījumu ātruma ierobežojumi veiksmīgi notīrīti", + "Raw Changes Data": "Neapstrādāti izmaiņu dati", + "Re-vectorize on object update": "Atkārtoti vektorizēt objekta atjaunināšanas brīdī", + "Read": "Lasīt", + "Real-time push notification status via notify_push": "Reāllaika tūlītējo paziņojumu statuss caur notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Reāllaika tūlītējie paziņojumi ir aktīvi. Pievienotie klienti saņem tūlītējus atjauninājumus, kad objekti tiek izveidoti, atjaunināti vai dzēsti.", + "Realtime push active": "Reāllaika tūlītējie paziņojumi aktīvi", + "Realtime push not available — the notify_push app is not installed": "Reāllaika tūlītējie paziņojumi nav pieejami — lietotne notify_push nav instalēta", + "Reason": "Iemesls", + "Reason (optional)": "Iemesls (neobligāts)", + "Rebase operation completed successfully": "Pārbāzēšanas darbība veiksmīgi pabeigta", + "Rebase operation failed: {error}": "Pārbāzēšanas darbība neizdevās: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Ieteikumi:", + "Recommended Type:": "Ieteicamais tips:", + "Recommended: 5 sources": "Ieteicams: 5 avoti", + "Refresh": "Atsvaidzināt", + "Refresh Data": "Atsvaidzināt datus", + "Refresh Stats": "Atsvaidzināt statistiku", + "Refresh Workflows": "Atsvaidzināt darbplūsmas", + "Refresh dashboard": "Atsvaidzināt informācijas paneli", + "Refresh database info": "Atsvaidzināt datu bāzes informāciju", + "Register": "Reģistrs", + "Register #{id}": "Reģistrs #{id}", + "Register / Schema": "Reģistrs / Shēma", + "Register ID": "Reģistra ID", + "Register Objects": "Reģistra objekti", + "Register Overview": "Reģistra pārskats", + "Register Statistics": "Reģistra statistika", + "Register Totals": "Reģistra kopsummas", + "Register and schema are required": "Reģistrs un shēma ir obligāti", + "Register annotation": "Reģistra anotācija", + "Register depublished successfully": "Reģistrs veiksmīgi atpublicēts", + "Register not found": "Reģistrs nav atrasts", + "Register objects": "Reģistra objekti", + "Register objects deletion completed successfully": "Reģistra objektu dzēšana veiksmīgi pabeigta", + "Register published successfully": "Reģistrs veiksmīgi publicēts", + "Register/Schema Usage": "Reģistra/Shēmas lietojums", + "Register:": "Reģistrs:", + "Registers": "Reģistri", + "Reindex": "Atkārtoti indeksēt", + "Reindex completed": "Atkārtotā indeksēšana pabeigta", + "Reindex completed successfully": "Atkārtotā indeksēšana veiksmīgi pabeigta", + "Reindex completed! Processed {count} objects in {duration}s": "Atkārtotā indeksēšana pabeigta! Apstrādāti {count} objekti {duration} s laikā", + "Reindex failed": "Atkārtotā indeksēšana neizdevās", + "Reindex failed: %s": "Atkārtotā indeksēšana neizdevās: %s", + "Reject": "Noraidīt", + "Rejected": "Noraidīts", + "Related Cases": "Saistītās lietas", + "Relations": "Saistības", + "Remote Version": "Attālā versija", + "Remove": "Noņemt", + "Remove Deck card": "Noņemt Deck kartīti", + "Remove avatar": "Noņemt avataru", + "Remove contact": "Noņemt kontaktu", + "Remove filter": "Noņemt filtru", + "Remove from favorites": "Noņemt no izlases", + "Remove from list": "Noņemt no saraksta", + "Remove group": "Noņemt grupu", + "Remove link": "Noņemt saiti", + "Remove link to {name}": "Noņemt saiti uz {name}", + "Remove link to {name}?": "Vai noņemt saiti uz {name}?", + "Remove link to {title}": "Noņemt saiti uz {title}", + "Remove oneOf entry": "Noņemt oneOf ierakstu", + "Remove property after write-back": "Noņemt īpašību pēc atpakaļrakstīšanas", + "Remove {title}": "Noņemt {title}", + "Remove {userId} from this organisation?": "Vai noņemt {userId} no šīs organizācijas?", + "Removed": "Noņemts", + "Removed from favorites": "Noņemts no izlases", + "Removing schema {schema}...": "Noņem shēmu {schema}...", + "Rename Conversation": "Pārdēvēt sarunu", + "Rename conversation": "Pārdēvēt sarunu", + "Replicas": "Replikas", + "Reports": "Pārskati", + "Repository": "Repozitorijs", + "Request Body": "Pieprasījuma pamatteksts", + "Request Data": "Pieprasījuma dati", + "Request Details": "Pieprasījuma detaļas", + "Request Quota (per day)": "Pieprasījumu kvota (dienā)", + "Request account deactivation": "Pieprasīt konta deaktivizēšanu", + "Request account deactivation. This will notify administrators for review.": "Pieprasīt konta deaktivizēšanu. Tas paziņos administratoriem izskatīšanai.", + "Request body must be JSON with a \"query\" field": "Pieprasījuma pamattekstam jābūt JSON ar lauku \"query\"", + "Request timeout in seconds": "Pieprasījuma noildze sekundēs", + "Requested at": "Pieprasīts", + "Required": "Obligāts", + "Required Token Scopes": "Nepieciešamie marķiera tvērumi", + "Required field": "Obligāts lauks", + "Required status is inconsistent": "Obligātuma statuss ir nekonsekvents", + "Requires Dolphin AI for OCR text extraction": "Nepieciešams Dolphin AI OCR teksta izvilkšanai", + "Rerun Search": "Atkārtoti palaist meklēšanu", + "Rerunning search: {searchTerm}": "Atkārtoti palaiž meklēšanu: {searchTerm}", + "Reset Changes": "Atiestatīt izmaiņas", + "Reset Filters": "Atiestatīt filtrus", + "Resource Quotas": "Resursu kvotas", + "Response Body": "Atbildes pamatteksts", + "Response Details": "Atbildes detaļas", + "Response Time:": "Atbildes laiks:", + "Restore": "Atjaunot", + "Restore conversation": "Atjaunot sarunu", + "Restore or permanently delete items": "Atjaunot vai neatgriezeniski dzēst vienumus", + "Restore {count} object": "Atjaunot {count} objektu", + "Restore {count} objects": "Atjaunot {count} objektus", + "Result Count Range": "Rezultātu skaita diapazons", + "Results": "Rezultāti", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Glabāšanas periods (ISO-8601 ilgums, piem., P10Y, P30D)", + "Retention settings updated successfully": "Glabāšanas iestatījumi veiksmīgi atjaunināti", + "Retrieve all objects for this schema": "Iegūt visus objektus šai shēmai", + "Retry": "Mēģināt vēlreiz", + "Retry Failed Extractions": "Atkārtot neizdevušās izvilkšanas", + "Retry Policy": "Atkārtošanas politika", + "Retry completed": "Atkārtošana pabeigta", + "Retry upload": "Atkārtot augšupielādi", + "Retrying...": "Mēģina vēlreiz...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Pārskatiet zemāk atlasītos objektus. Jūs varat noņemt jebkurus objektus, kurus nevēlaties dzēst, noklikšķinot uz noņemšanas pogas.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Pārskatiet zemāk atlasītos objektus. Jūs varat noņemt jebkurus objektus, kurus nevēlaties iekļaut.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Pārskatiet zemāk atlasītos objektus. Jūs varat noņemt jebkurus objektus, kurus nevēlaties neatgriezeniski dzēst, noklikšķinot uz noņemšanas pogas. Šo darbību nevar atsaukt.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Pārskatiet zemāk atlasītos objektus. Jūs varat noņemt jebkurus objektus, kurus nevēlaties atjaunot, noklikšķinot uz noņemšanas pogas. Objekti tiks atjaunoti to sākotnējā atrašanās vietā.", + "Revoke": "Atsaukt", + "Right": "Pa labi", + "Risk Level": "Riska līmenis", + "Risk level": "Riska līmenis", + "Roles and Permissions": "Lomas un atļaujas", + "Roles updated successfully": "Lomas veiksmīgi atjauninātas", + "Run compliance scan": "Palaist atbilstības skenēšanu", + "SOLR Connection Settings": "SOLR savienojuma iestatījumi", + "SOLR Index Warmup": "SOLR indeksa uzsildīšana", + "SOLR Version:": "SOLR versija:", + "SOLR actions menu": "SOLR darbību izvēlne", + "SOLR base path (usually /solr)": "SOLR bāzes ceļš (parasti /solr)", + "SOLR collection deleted successfully": "SOLR kolekcija veiksmīgi dzēsta", + "SOLR connection test failed: {error}": "SOLR savienojuma tests neizdevās: {error}", + "SOLR connection test successful!": "SOLR savienojuma tests veiksmīgs!", + "SOLR index warmup completed successfully": "SOLR indeksa uzsildīšana veiksmīgi pabeigta", + "SOLR is disabled": "SOLR ir atspējots", + "SOLR is not available or not configured": "SOLR nav pieejams vai nav konfigurēts", + "SOLR logging disabled": "SOLR reģistrēšana atspējota", + "SOLR logging enabled": "SOLR reģistrēšana iespējota", + "SOLR search disabled": "SOLR meklēšana atspējota", + "SOLR search enabled": "SOLR meklēšana iespējota", + "SOLR server hostname or IP address": "SOLR servera resursdatora nosaukums vai IP adrese", + "SOLR server port number (optional, defaults to 8983)": "SOLR servera porta numurs (neobligāts, noklusējums ir 8983)", + "SOLR settings updated successfully": "SOLR iestatījumi veiksmīgi atjaunināti", + "SOLR setup completed successfully": "SOLR iestatīšana veiksmīgi pabeigta", + "SOLR setup completed successfully!": "SOLR iestatīšana veiksmīgi pabeigta!", + "SOLR setup error: %s": "SOLR iestatīšanas kļūda: %s", + "SOLR setup failed": "SOLR iestatīšana neizdevās", + "SOLR setup failed - check logs": "SOLR iestatīšana neizdevās — pārbaudiet žurnālus", + "SOLR setup failed. Please check the configuration and try again.": "SOLR iestatīšana neizdevās. Lūdzu, pārbaudiet konfigurāciju un mēģiniet vēlreiz.", + "SOLR setup failed: {error}": "SOLR iestatīšana neizdevās: {error}", + "SOLR warmup failed: {error}": "SOLR uzsildīšana neizdevās: {error}", + "Sample Values:": "Paraugvērtības:", + "Save": "Saglabāt", + "Save Configuration": "Saglabāt konfigurāciju", + "Save Connection Settings": "Saglabāt savienojuma iestatījumus", + "Save Roles": "Saglabāt lomas", + "Save Settings": "Saglabāt iestatījumus", + "Save changes": "Saglabāt izmaiņas", + "Save current search as view": "Saglabāt pašreizējo meklējumu kā skatu", + "Save labels": "Saglabāt etiķetes", + "Save tags for {name}": "Saglabāt birkas {name}", + "Save the organisation first to manage users.": "Vispirms saglabājiet organizāciju, lai pārvaldītu lietotājus.", + "Saved Views": "Saglabātie skati", + "Saving...": "Saglabā...", + "Scheduled": "Ieplānots", + "Schema": "Shēma", + "Schema ID": "Shēmas ID", + "Schema Name": "Shēmas nosaukums", + "Schema Statistics": "Shēmas statistika", + "Schema annotation": "Shēmas anotācija", + "Schema configuration updated successfully for {schema}": "Shēmas konfigurācija veiksmīgi atjaunināta shēmai {schema}", + "Schema depublished successfully": "Shēma veiksmīgi atpublicēta", + "Schema objects deletion completed successfully": "Shēmas objektu dzēšana veiksmīgi pabeigta", + "Schema published successfully": "Shēma veiksmīgi publicēta", + "Schema reference ($ref)": "Shēmas atsauce ($ref)", + "Schema successfully created": "Shēma veiksmīgi izveidota", + "Schema successfully updated": "Shēma veiksmīgi atjaunināta", + "Schema {schema} removed successfully": "Shēma {schema} veiksmīgi noņemta", + "Schema:": "Shēma:", + "Schemas": "Shēmas", + "Scheme": "Shēma", + "Search": "Meklēt", + "Search / Views": "Meklēšana / Skati", + "Search / views": "Meklēšana / skati", + "Search Activity": "Meklēšanas darbība", + "Search Analytics": "Meklēšanas analītika", + "Search Configuration": "Meklēšanas konfigurācija", + "Search Entities": "Meklēt entītijas", + "Search Files": "Meklēt failus", + "Search GitHub": "Meklēt GitHub", + "Search GitLab": "Meklēt GitLab", + "Search Method": "Meklēšanas metode", + "Search Mode": "Meklēšanas režīms", + "Search Query": "Meklēšanas vaicājums", + "Search Results": "Meklēšanas rezultāti", + "Search Term": "Meklēšanas termins", + "Search Trail Details": "Meklēšanas pieraksta detaļas", + "Search Trail Management": "Meklēšanas pierakstu pārvaldība", + "Search Trail Statistics": "Meklēšanas pierakstu statistika", + "Search Trails": "Meklēšanas pieraksti", + "Search Views": "Meklēšanas skati", + "Search Webhooks": "Meklēt tīmekļa āķus", + "Search agents...": "Meklēt aģentus...", + "Search applications...": "Meklēt lietojumprogrammas...", + "Search by file name or path": "Meklēt pēc faila nosaukuma vai ceļa", + "Search by name or URL": "Meklēt pēc nosaukuma vai URL", + "Search by title or UUID...": "Meklēt pēc nosaukuma vai UUID...", + "Search by value": "Meklēt pēc vērtības", + "Search configurations": "Meklēt konfigurācijas", + "Search data sources...": "Meklēt datu avotus...", + "Search groups...": "Meklēt grupas...", + "Search in Database Objects": "Meklēt datu bāzes objektos", + "Search in Files": "Meklēt failos", + "Search in Objects": "Meklēt objektos", + "Search objects": "Meklēt objektus", + "Search objects...": "Meklēt objektus...", + "Search property names...": "Meklēt īpašību nosaukumus...", + "Search registers...": "Meklēt reģistrus...", + "Search schemas...": "Meklēt shēmas...", + "Search trail deletion not implemented yet": "Meklēšanas pierakstu dzēšana vēl nav ieviesta", + "Search views...": "Meklēt skatus...", + "Search {name}...": "Meklēt {name}...", + "Searching...": "Meklē...", + "Secret": "Noslēpums", + "Secret key for HMAC signature generation (optional)": "Slepenā atslēga HMAC paraksta ģenerēšanai (neobligāta)", + "Security": "Drošība", + "See organisations as a table": "Skatīt organizācijas kā tabulu", + "See organisations as cards": "Skatīt organizācijas kā kartītes", + "Select AI Agent": "Atlasīt AI aģentu", + "Select Active Organisation": "Atlasīt aktīvo organizāciju", + "Select All": "Atlasīt visu", + "Select ConfigSet": "Atlasiet ConfigSet", + "Select File Types to Vectorize:": "Atlasiet vektorizējamos failu tipus:", + "Select HTTP method": "Atlasiet HTTP metodi", + "Select Register and Schema": "Atlasiet Reģistru un Shēmu", + "Select Views to Vectorize:": "Atlasiet vektorizējamos skatus:", + "Select a Nextcloud group to add": "Atlasiet pievienojamo Nextcloud grupu", + "Select a branch": "Atlasiet zaru", + "Select a date property": "Atlasiet datuma īpašību", + "Select a model or type a custom model name": "Atlasiet modeli vai ievadiet pielāgota modeļa nosaukumu", + "Select a register": "Atlasiet reģistru", + "Select a register...": "Atlasiet reģistru...", + "Select a repository": "Atlasiet repozitoriju", + "Select a repository you have write access to": "Atlasiet repozitoriju, kuram jums ir rakstīšanas piekļuve", + "Select a schema": "Atlasiet shēmu", + "Select a schema...": "Atlasiet shēmu...", + "Select a user": "Atlasiet lietotāju", + "Select agent type": "Atlasiet aģenta tipu", + "Select all entities": "Atlasiet visas entītijas", + "Select an AI Agent": "Atlasiet MI aģentu", + "Select an AI agent to begin chatting with your data.": "Atlasiet MI aģentu, lai sāktu tērzēšanu ar saviem datiem.", + "Select an email to see linked objects": "Atlasiet e-pastu, lai redzētu saistītos objektus", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Atlasiet organizāciju un lietotāju, lai pievienotu tos kā dalībnieku. Meklējiet organizācijas pēc nosaukuma.", + "Select backend": "Atlasiet aizmugursistēmu", + "Select chat model": "Atlasiet tērzēšanas modeli", + "Select collection for files": "Atlasiet kolekciju failiem", + "Select collection for objects": "Atlasiet kolekciju objektiem", + "Select configuration type...": "Atlasiet konfigurācijas tipu...", + "Select default organisation": "Atlasiet noklusējuma organizāciju", + "Select entity {value}": "Atlasiet entītiju {value}", + "Select event to listen to...": "Atlasiet notikumu, ko klausīties...", + "Select fields to display": "Atlasiet attēlojamos laukus", + "Select files with the correct extension": "Atlasiet failus ar pareizo paplašinājumu", + "Select group": "Atlasiet grupu", + "Select groups (optional)": "Atlasiet grupas (neobligāti)", + "Select groups to notify...": "Atlasiet grupas, kurām paziņot...", + "Select groups with access to this agent": "Atlasiet grupas, kurām ir piekļuve šim aģentam", + "Select groups...": "Atlasiet grupas...", + "Select model": "Atlasiet modeli", + "Select one or more registers": "Atlasiet vienu vai vairākus reģistrus", + "Select one or more schemas": "Atlasiet vienu vai vairākas shēmas", + "Select options...": "Atlasiet opcijas...", + "Select or create labels, or select \"No label\" to add files": "Atlasiet vai izveidojiet etiķetes vai atlasiet \"Bez etiķetes\", lai pievienotu failus", + "Select owner application (optional)...": "Atlasiet īpašnieka lietojumprogrammu (neobligāti)...", + "Select period": "Atlasiet periodu", + "Select property to send as payload": "Atlasiet īpašību, ko nosūtīt kā lietderīgo slodzi", + "Select provider": "Atlasiet nodrošinātāju", + "Select registers and schemas to save a view": "Atlasiet reģistrus un shēmas, lai saglabātu skatu", + "Select retry policy": "Atlasiet atkārtošanas politiku", + "Select role": "Atlasiet lomu", + "Select schemas (empty = all schemas)": "Atlasiet shēmas (tukšs = visas shēmas)", + "Select schemas to warm up": "Atlasiet uzsildāmās shēmas", + "Select search mode": "Atlasiet meklēšanas režīmu", + "Select source type...": "Atlasiet avota tipu...", + "Select target type": "Atlasiet mērķa tipu", + "Select the branch to publish to": "Atlasiet zaru, kurā publicēt", + "Select the event this webhook should listen to": "Atlasiet notikumu, ko šim tīmekļa āķim klausīties", + "Select views to filter data (optional)": "Atlasiet skatus datu filtrēšanai (neobligāti)", + "Select views to limit which data the agent can access": "Atlasiet skatus, lai ierobežotu, kuriem datiem aģents var piekļūt", + "Select views to vectorize:": "Atlasiet vektorizējamos skatus:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Atlasiet, kuras Nextcloud grupas ir pieejamas šai organizācijai. Lietotājiem šajās grupās būs piekļuve organizācijas resursiem.", + "Select which data views the AI can search": "Atlasiet, kuros datu skatos MI var meklēt", + "Select which property from the event should be used as the webhook payload data": "Atlasiet, kura notikuma īpašība jāizmanto kā tīmekļa āķa lietderīgās slodzes dati", + "Select which tools the AI can use to perform actions": "Atlasiet, kurus rīkus MI var izmantot darbību veikšanai", + "Select {fieldName} values": "Atlasiet {fieldName} vērtības", + "Selected": "Atlasīts", + "Selected Groups": "Atlasītās grupas", + "Selected Objects ({count})": "Atlasītie objekti ({count})", + "Selected audit trails deleted successfully": "Atlasītie audita pieraksti veiksmīgi dzēsti", + "Selected search trails deleted successfully": "Atlasītie meklēšanas pieraksti veiksmīgi dzēsti", + "Selected users can access this view": "Atlasītie lietotāji var piekļūt šim skatam", + "Send additional feedback": "Nosūtīt papildu atsauksmi", + "Send as CloudEvent": "Nosūtīt kā CloudEvent", + "Send as cloudevent": "Nosūtīt kā cloudevent", + "Sensitive PII": "Sensitīva PII", + "Serial Mode (Safer, slower)": "Secīgais režīms (drošāks, lēnāks)", + "Serial:": "Secīgi:", + "Server Configuration": "Servera konfigurācija", + "Server Information": "Servera informācija", + "Set as Active": "Iestatīt kā aktīvu", + "Set as active organisation": "Iestatīt kā aktīvo organizāciju", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Iestatiet ierobežojumus API lietošanai un marķieru patēriņam. Izmantojiet 0 neierobežotiem resursiem.", + "Settings": "Iestatījumi", + "Settings updated successfully": "Iestatījumi veiksmīgi atjaunināti", + "Shards": "Šķembas", + "Share": "Kopīgot", + "Share with Groups": "Kopīgot ar grupām", + "Share with Users": "Kopīgot ar lietotājiem", + "Show": "Rādīt", + "Show Filters": "Rādīt filtrus", + "Show less": "Rādīt mazāk", + "Show only entries with changes": "Rādīt tikai ierakstus ar izmaiņām", + "Showing {count} dashboard(s)": "Tiek rādīts {count} informācijas panelis(-ļi)", + "Showing {count} processing activities": "Tiek rādītas {count} apstrādes darbības", + "Showing {showing} of {total} agents": "Tiek rādīti {showing} no {total} aģentiem", + "Showing {showing} of {total} applications": "Tiek rādītas {showing} no {total} lietojumprogrammām", + "Showing {showing} of {total} audit trail entries": "Tiek rādīti {showing} no {total} audita pierakstu ierakstiem", + "Showing {showing} of {total} configurations": "Tiek rādītas {showing} no {total} konfigurācijām", + "Showing {showing} of {total} deleted items": "Tiek rādīti {showing} no {total} dzēstajiem vienumiem", + "Showing {showing} of {total} entities": "Tiek rādītas {showing} no {total} entītijām", + "Showing {showing} of {total} files": "Tiek rādīti {showing} no {total} failiem", + "Showing {showing} of {total} log entries": "Tiek rādīti {showing} no {total} žurnāla ierakstiem", + "Showing {showing} of {total} search trail entries": "Tiek rādīti {showing} no {total} meklēšanas pierakstu ierakstiem", + "Showing {showing} of {total} sources": "Tiek rādīti {showing} no {total} avotiem", + "Showing {showing} of {total} templates": "Tiek rādītas {showing} no {total} veidnēm", + "Showing {showing} of {total} webhooks": "Tiek rādīti {showing} no {total} tīmekļa āķiem", + "Showing {shown} of {total} organisations": "Tiek rādītas {shown} no {total} organizācijām", + "Simple": "Vienkāršs", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Vienkārši vaicājumi: pamata teksta meklējumi ar minimāliem parametriem (piemēram, viens meklēšanas vārds, bez papildu filtriem)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Vienkārši vaicājumi: pamata teksta meklējumi ar minimāliem parametriem (piemēram, viens meklēšanas vārds, bez papildu filtriem)", + "Size": "Izmērs", + "Slug": "Identifikators", + "Soft Delete Mode": "Mīkstās dzēšanas režīms", + "Soft Deleted Items": "Mīksti dzēstie vienumi", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Mīksti dzēst visus šīs shēmas objektus ({active} aktīvi, {deleted} jau dzēsti)", + "SolrCloud mode enabled": "SolrCloud režīms iespējots", + "Something went wrong while adding files": "Pievienojot failus, radās kļūda", + "Source": "Avots", + "Source successfully updated": "Avots veiksmīgi atjaunināts", + "Sources": "Avoti", + "Sources:": "Avoti:", + "Special Rights": "Īpašās tiesības", + "Standalone SOLR mode": "Atsevišķs SOLR režīms", + "Start Conversation": "Sākt sarunu", + "Start Date Field": "Sākuma datuma lauks", + "Start Vectorization": "Sākt vektorizāciju", + "Start a conversation": "Sākt sarunu", + "Starting names cache warmup...": "Sākas nosaukumu kešatmiņas uzsildīšana...", + "Starting validation for {schema}...": "Sākas validācija shēmai {schema}...", + "Starting {action} for {schema}...": "Sākas {action} shēmai {schema}...", + "Starting...": "Sākas...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika un metrika", + "Statistics refreshed": "Statistika atsvaidzināta", + "Stats calculation not yet implemented": "Statistikas aprēķins vēl nav ieviests", + "Status": "Statuss", + "Status Code": "Statusa kods", + "Step": "Solis", + "Storage": "Krātuve", + "Storage Quota (MB)": "Krātuves kvota (MB)", + "String Constraints": "Virknes ierobežojumi", + "Subject identifier (email, BSN, name, etc.)": "Subjekta identifikators (e-pasts, BSN, vārds u. c.)", + "Subscribe": "Abonēt", + "Subscribed": "Abonēts", + "Success": "Veiksmīgi", + "Success Rate": "Veiksmes rādītājs", + "Success Status": "Veiksmes statuss", + "Successful": "Veiksmīgs", + "Successfully added user to organisation: {name}": "Lietotājs veiksmīgi pievienots organizācijai: {name}", + "Successfully cleared {count} audit trails": "Veiksmīgi notīrīti {count} audita pieraksti", + "Successfully cleared {count} blob storage objects": "Veiksmīgi notīrīti {count} blob krātuves objekti", + "Successfully cleared {count} search trails": "Veiksmīgi notīrīti {count} meklēšanas pieraksti", + "Successfully created {count} SOLR fields": "Veiksmīgi izveidoti {count} SOLR lauki", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Veiksmīgi dzēsti {count} iegulumi. Lūdzu, vēlreiz vektorizējiet savus datus.", + "Successfully deleted {count} objects for {schema}": "Veiksmīgi dzēsti {count} objekti shēmai {schema}", + "Successfully fixed {count} SOLR field configurations": "Veiksmīgi izlabotas {count} SOLR lauku konfigurācijas", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Veiksmīgi importēts: {registers} reģistri, {schemas} shēmas, {objects} objekti", + "Successfully joined organisation": "Veiksmīgi pievienojies organizācijai", + "Successfully permanently deleted {count} objects": "Veiksmīgi neatgriezeniski dzēsti {count} objekti", + "Successfully processed {count} files!": "Veiksmīgi apstrādāti {count} faili!", + "Successfully restored {count} object": "Veiksmīgi atjaunots {count} objekts", + "Successfully restored {count} objects": "Veiksmīgi atjaunoti {count} objekti", + "Successfully saved configuration for {count} facets!": "Veiksmīgi saglabāta konfigurācija {count} aspektiem!", + "Supported File Types": "Atbalstītie failu tipi", + "Switch Active Organisation": "Pārslēgt aktīvo organizāciju", + "Switch Organisation": "Pārslēgt organizāciju", + "Sync Table": "Sinhronizēt tabulu", + "Synchronization Settings": "Sinhronizācijas iestatījumi", + "Syncing magic table for {schema}...": "Tiek sinhronizēta maģiskā tabula shēmai {schema}...", + "Syncs": "Sinhronizācijas", + "System Default": "Sistēmas noklusējums", + "System Prompt": "Sistēmas uzvedne", + "System announcements": "Sistēmas paziņojumi", + "Table": "Tabula", + "Target ID": "Mērķa ID", + "Target Type*": "Mērķa tips*", + "Technical Description": "Tehniskais apraksts", + "Technical Details": "Tehniskā informācija", + "Technical description for developers and administrators": "Tehniskais apraksts izstrādātājiem un administratoriem", + "Technical measures": "Tehniskie pasākumi", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatūra", + "Templates": "Veidnes", + "Test": "Pārbaude", + "Test Chat": "Testa tērzēšana", + "Test Connection": "Pārbaudīt savienojumu", + "Test Embedding": "Pārbaudīt iegulumu", + "Test endpoint executed successfully": "Testa galapunkts veiksmīgi izpildīts", + "Test webhook delivered successfully": "Testa tīmekļa āķis veiksmīgi piegādāts", + "Test webhook delivery failed": "Testa tīmekļa āķa piegāde neizdevās", + "Test webhook sent successfully": "Testa tīmekļa āķis veiksmīgi nosūtīts", + "Testing...": "Notiek pārbaude...", + "Text Chunk #{id}": "Teksta fragments #{id}", + "Text Extraction": "Teksta izvilkšana", + "Text Extraction Engine": "Teksta izvilkšanas dzinis", + "Text chunks": "Teksta fragmenti", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Teksta fragmenti tiek ģenerēti faila izvilkšanas laikā un glabāti datubāzē. Vektorizācija nolasa šos iepriekš fragmentētos failus un pārvērš tos par iegulumiem.", + "Text extracted successfully": "Teksts veiksmīgi izvilkts", + "Text extraction disabled": "Teksta izvilkšana atspējota", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloud Kalendāra lietotne šajā serverī nav instalēta vai iespējota.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloud Kontaktu lietotne šajā serverī nav instalēta vai iespējota.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloud Deck lietotne šajā serverī nav instalēta vai iespējota.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloud Pasta lietotne šajā serverī nav instalēta vai iespējota.", + "The URL where webhook events will be sent": "URL, uz kuru tiks nosūtīti tīmekļa āķa notikumi", + "The requested conversation does not exist": "Pieprasītā saruna neeksistē", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Pieprasīto informācijas paneli nevarēja ielādēt. Pārbaudiet URL vai izvēlieties citu informācijas paneli no saraksta.", + "The saved view and all its search configuration": "Saglabātais skats un visa tā meklēšanas konfigurācija", + "There are no audit trail entries matching your current filters.": "Nav audita pierakstu ierakstu, kas atbilstu jūsu pašreizējiem filtriem.", + "There are no deleted items matching your current filters.": "Nav dzēsto vienumu, kas atbilstu jūsu pašreizējiem filtriem.", + "There are no search trail entries matching your current filters.": "Nav meklēšanas pierakstu ierakstu, kas atbilstu jūsu pašreizējiem filtriem.", + "There are no webhook log entries matching your filters.": "Nav tīmekļa āķa žurnāla ierakstu, kas atbilstu jūsu filtriem.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Tie tiks saglabāti atbilstoši to shēmas konfigurētajam glabāšanas periodam un automātiski neatgriezeniski dzēsti, kad glabāšanas periods beigsies. Glabāšanas periods ir konfigurējams katrai shēmai, un to var atrast shēmas iestatījumos.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Tie tiks saglabāti atbilstoši to shēmas konfigurētajam glabāšanas periodam un pēc tam neatgriezeniski noņemti.", + "This action cannot be undone.": "Šo darbību nevar atsaukt.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Šo darbību nevar atsaukt. Pārliecinieties, ka neviena kolekcija neizmanto šo ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Šo darbību nevar atsaukt. Pārliecinieties, ka neviena kolekcija neizmanto šo configset.", + "This action will submit a deactivation request to your administrators.": "Šī darbība iesniegs deaktivizēšanas pieprasījumu jūsu administratoriem.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Šim aģentam nav konfigurējamu skatu vai rīku, taču jūs joprojām varat pielāgot RAG iestatījumus zemāk.", + "This analysis may take some time": "Šī analīze var aizņemt zināmu laiku", + "This audit trail entry does not contain any change information.": "Šis audita pieraksta ieraksts nesatur informāciju par izmaiņām.", + "This configuration has no data defined.": "Šai konfigurācijai nav definētu datu.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Šis galapunkts ir novecojis. Tā vietā izmantojiet uz fragmentiem balstītus galapunktus.", + "This entity has no relations to objects or files": "Šai entītijai nav saistību ar objektiem vai failiem", + "This is a test webhook from OpenRegister": "Šis ir testa tīmekļa āķis no OpenRegister", + "This is not valid JSON": "Šis nav derīgs JSON", + "This key already exists on this schema": "Šī atslēga šajā shēmā jau eksistē", + "This schema must use Magic Table configuration to sync": "Šai shēmai sinhronizēšanai jāizmanto Maģiskās tabulas konfigurācija", + "This schema must use magic table configuration to sync": "Šai shēmai sinhronizēšanai jāizmanto maģiskās tabulas konfigurācija", + "This source has no associated registers.": "Šim avotam nav saistīto reģistru.", + "This token will only be shown once. Copy it now.": "Šis marķieris tiks parādīts tikai vienreiz. Kopējiet to tagad.", + "This will copy the _default ConfigSet with the new name": "Tas nokopēs _default ConfigSet ar jauno nosaukumu", + "This will copy the _default configset with the new name": "Tas nokopēs _default configset ar jauno nosaukumu", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Tas neatgriezeniski dzēsīs VISUS iegulumus (vektorus) no datubāzes. Jums būs atkārtoti jāvektorizē visi objekti un faili. Šo darbību nevar atsaukt.\n\nVai tiešām vēlaties turpināt?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Tas neatgriezeniski dzēsīs VISUS iegulumus (vektorus) no datubāzes. Jums būs atkārtoti jāvektorizē visi objekti un faili. Šo darbību nevar atsaukt.\\n\\nVai tiešām vēlaties turpināt?", + "This will permanently delete:": "Tas neatgriezeniski dzēsīs:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Tas neatgriezeniski noņems {count} jau mīksti dzēstos objektus no datubāzes. Šo darbību nevar atsaukt.", + "Timeout (seconds)": "Noildze (sekundes)", + "Timestamp": "Laikspiedols", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Padoms: iespējojiet tikai tos skatus, kuriem nepieciešama semantiskā meklēšana, lai samazinātu iegulumu izmaksas. Vienkāršām uzmeklēšanas tabulām vektorizācija reti ir nepieciešama.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Padoms: iespējojiet tikai tos skatus, kuriem nepieciešama semantiskā meklēšana, lai samazinātu iegulumu izmaksas. Vienkāršām uzmeklēšanas tabulām vektorizācija reti ir nepieciešama.", + "Title": "Nosaukums", + "Title *": "Nosaukums *", + "Title Template": "Nosaukuma veidne", + "Title*": "Nosaukums*", + "To (e.g. -1 year or 2025-12-31)": "Līdz (piemēram, -1 year vai 2025-12-31)", + "To Date": "Līdz datumam", + "To add files larger than or equal to 512MB, go to the": "Lai pievienotu failus, kas ir lielāki par vai vienādi ar 512 MB, dodieties uz", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Lai pielāgotu fragmenta izmēru vai stratēģiju, dodieties uz Faila konfigurācija → Apstrādes ierobežojumi.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Lai pielāgotu fragmenta izmēru vai stratēģiju, dodieties uz faila konfigurācija → apstrādes ierobežojumi.", + "To change the embedding provider or model, go to LLM Configuration.": "Lai mainītu ieguluma nodrošinātāju vai modeli, dodieties uz LLM konfigurāciju.", + "To change the embedding provider or model, go to LLM configuration.": "Lai mainītu ieguluma nodrošinātāju vai modeli, dodieties uz LLM konfigurāciju.", + "To date": "Līdz datumam", + "Toggle search sidebar": "Pārslēgt meklēšanas sānjoslu", + "Token Created": "Marķieris izveidots", + "Token Quota (per request)": "Marķieru kvota (uz pieprasījumu)", + "Token copied to clipboard": "Marķieris kopēts starpliktuvē", + "Token name": "Marķiera nosaukums", + "Token revoked": "Marķieris atsaukts", + "Tools": "Rīki", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Rīki tiek izpildīti ar aģenta noklusējuma lietotāja atļaujām, kad nav aktīvas lietotāja sesijas (piemēram, cron uzdevumi). Konfigurējiet noklusējuma lietotāju cilnē Iestatījumi.", + "Tools respect the agent's own role and organization boundaries.": "Rīki ievēro aģenta paša lomu un organizācijas robežas.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Rīki ievēro aģenta skatus, atļaujas un organizācijas robežas.", + "Top Deleters": "Galvenie dzēsēji", + "Total": "Kopā", + "Total Audit Trails": "Kopējie audita pieraksti", + "Total Chunks Available:": "Kopā pieejamie fragmenti:", + "Total Deleted Items": "Kopējie dzēstie vienumi", + "Total Members": "Kopējie dalībnieki", + "Total Objects": "Kopējie objekti", + "Total Objects in Database:": "Kopējie objekti datubāzē:", + "Total Organisations": "Kopējās organizācijas", + "Total Results": "Kopējie rezultāti", + "Total Searches": "Kopējie meklējumi", + "Total Size": "Kopējais izmērs", + "Total Storage": "Kopējā krātuve", + "Total events": "Kopējie notikumi", + "Total:": "Kopā:", + "Totals": "Kopsummas", + "Try searching by UUID or with different keywords": "Mēģiniet meklēt pēc UUID vai ar citiem atslēgvārdiem", + "Type": "Tips", + "Type \"DELETE COLLECTION\" to confirm:": "Ievadiet \"DELETE COLLECTION\", lai apstiprinātu:", + "Type Issue": "Tipa problēma", + "Type Variations:": "Tipa variācijas:", + "Type filter (optional, e.g. email)": "Tipa filtrs (neobligāts, piemēram, e-pasts)", + "Type must be \"positive\" or \"negative\"": "Tipam jābūt \"positive\" vai \"negative\"", + "Type to search for groups": "Ievadiet, lai meklētu grupas", + "Type to search for objects...": "Ievadiet, lai meklētu objektus...", + "Type to search for organisations": "Ievadiet, lai meklētu organizācijas", + "Type to search for users": "Ievadiet, lai meklētu lietotājus", + "Type to search groups...": "Ievadiet, lai meklētu grupas...", + "Type to search users...": "Ievadiet, lai meklētu lietotājus...", + "Type to search...": "Ievadiet, lai meklētu...", + "Type your username to confirm": "Ievadiet savu lietotājvārdu, lai apstiprinātu", + "URL": "URL", + "URL where Ollama is running": "URL, kurā darbojas Ollama", + "UUID copied to clipboard": "UUID kopēts starpliktuvē", + "UUID:": "UUID:", + "Unchanged": "Nemainīts", + "Unique Search Terms": "Unikāli meklēšanas vārdi", + "Unique Terms": "Unikāli vārdi", + "Unknown": "Nezināms", + "Unknown Object": "Nezināms objekts", + "Unknown Register": "Nezināms reģistrs", + "Unknown Schema": "Nezināma shēma", + "Unknown error": "Nezināma kļūda", + "Unknown widget type: {type}": "Nezināms logrīka tips: {type}", + "Unlink email": "Atsaistīt e-pastu", + "Unlink event": "Atsaistīt notikumu", + "Unnamed Object": "Nenosaukts objekts", + "Untitled": "Bez nosaukuma", + "Untitled View": "Bez nosaukuma skats", + "Update": "Atjaunināt", + "Update Operations": "Atjaunināšanas darbības", + "Update available: {local} → {remote}": "Pieejams atjauninājums: {local} → {remote}", + "Update configuration: ...": "Atjaunināt konfigurāciju: ...", + "Update register OAS: ...": "Atjaunināt reģistra OAS: ...", + "Update vectors when object data changes (recommended for accurate search)": "Atjaunināt vektorus, kad mainās objekta dati (ieteicams precīzai meklēšanai)", + "Updated": "Atjaunināts", + "Updated At": "Atjaunināts", + "Updated:": "Atjaunināts:", + "Upload new avatar": "Augšupielādēt jaunu avataru", + "Uploading configuration sets...": "Tiek augšupielādētas konfigurācijas kopas...", + "Uptime:": "Darbības laiks:", + "Url": "URL", + "Usage Count": "Lietojuma skaits", + "Use AI agents for processing and analysis": "Izmantojiet MI aģentus apstrādei un analīzei", + "Use Large Language Model features": "Izmantojiet lielo valodas modeļu funkcijas", + "Use SolrCloud with Zookeeper for distributed search": "Izmantojiet SolrCloud ar Zookeeper sadalītai meklēšanai", + "Use SolrCloud with zookeeper for distributed search": "Izmantojiet SolrCloud ar zookeeper sadalītai meklēšanai", + "Use VNG APIs instead of MongoDB.": "Izmantojiet VNG API MongoDB vietā.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Izmantot noklusējuma diapazonus (Pēdējās 7/30/90 dienas, Pēdējais gads, Vecāks)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Izmantojiet ārējo krātuvi (piemēram, MongoDB) iekšējās Nextcloud krātuves vietā.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Izmantojiet filtrus, lai sašaurinātu audita pierakstu ierakstus pēc reģistra, shēmas, darbības tipa, lietotāja, datumu diapazona vai objekta ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Izmantojiet filtrus, lai sašaurinātu dzēstos vienumus pēc reģistra, shēmas, dzēšanas datuma vai lietotāja, kas tos dzēsa.", + "Use filters to narrow down entities by type or category.": "Izmantojiet filtrus, lai sašaurinātu entītijas pēc tipa vai kategorijas.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Izmantojiet filtrus, lai sašaurinātu meklēšanas pierakstu ierakstus pēc reģistra, shēmas, veiksmes statusa, lietotāja, datumu diapazona, meklēšanas vārdiem vai veiktspējas metrikas.", + "Used By Collections": "Izmanto kolekcijas", + "Used by": "Izmanto", + "User": "Lietotājs", + "User Agent Statistics": "Lietotāja aģenta statistika", + "User Description": "Lietotāja apraksts", + "User Quota": "Lietotāja kvota", + "User rate limits cleared successfully": "Lietotāja ātruma ierobežojumi veiksmīgi notīrīti", + "User removed successfully": "Lietotājs veiksmīgi noņemts", + "User-friendly description shown in forms and help text": "Lietotājam draudzīgs apraksts, kas tiek rādīts veidlapās un palīdzības tekstā", + "Username": "Lietotājvārds", + "Username for SOLR authentication (optional)": "Lietotājvārds SOLR autentifikācijai (neobligāts)", + "Users": "Lietotāji", + "Uses": "Lietojumi", + "Using Pre-Generated Chunks": "Iepriekš ģenerēto fragmentu izmantošana", + "Validate": "Validēt", + "Validate Objects": "Validēt objektus", + "Validation completed for {schema}": "Validācija pabeigta shēmai {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validācija pabeigta shēmai {schema}: {processed} apstrādāti, {updated} atjaunināti, {failed} neizdevās", + "Validation completed successfully": "Validācija veiksmīgi pabeigta", + "Validation failed": "Validācija neizdevās", + "Value": "Vērtība", + "Value list (split on ,)": "Vērtību saraksts (atdalīts ar ,)", + "Vector Dimensions": "Vektora dimensijas", + "Vector Search Backend": "Vektoru meklēšanas aizmugursistēma", + "Vector field: _embedding_": "Vektora lauks: _embedding_", + "Vectorization Triggers": "Vektorizācijas izraisītāji", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorizācija pabeigta: {vectorized} objekti vektorizēti, {failed} neizdevās", + "Vectorization failed: {error}": "Vektorizācija neizdevās: {error}", + "Vectorize All Files": "Vektorizēt visus failus", + "Vectorize All Objects": "Vektorizēt visus objektus", + "Vectorize all file types": "Vektorizēt visus failu tipus", + "Vectorize all views": "Vektorizēt visus skatus", + "Vectorize on object creation": "Vektorizēt objekta izveides laikā", + "Vectors will be stored in your existing object and file collections": "Vektori tiks glabāti jūsu esošajās objektu un failu kolekcijās", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Tiek pārbaudīta servera savienojamība...", + "Version": "Versija", + "Version Information": "Versijas informācija", + "Very High": "Ļoti augsts", + "Very high": "Ļoti augsts", + "View": "Skats", + "View API Docs": "Skatīt API dokumentāciju", + "View API Documentation": "Skatīt API dokumentāciju", + "View APIs": "Skatīt API", + "View Changes": "Skatīt izmaiņas", + "View Details": "Skatīt detalizēti", + "View Error": "Skatīt kļūdu", + "View File": "Skatīt failu", + "View Full Details": "Skatīt pilnu informāciju", + "View Logs": "Skatīt žurnālus", + "View Name": "Skata nosaukums", + "View Object": "Skatīt objektu", + "View Parameters": "Skatīt parametrus", + "View Selection": "Skatu atlase", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Skatiet un analizējiet meklēšanas pierakstu žurnālus ar uzlabotām filtrēšanas un analītikas iespējām", + "View and analyze system audit trails with advanced filtering capabilities": "Skatiet un analizējiet sistēmas audita pierakstus ar uzlabotām filtrēšanas iespējām", + "View and manage authorization across registers and schemas": "Skatiet un pārvaldiet autorizāciju reģistros un shēmās", + "View deleted successfully": "Skats veiksmīgi dzēsts", + "View entity details and manage relations": "Skatiet entītijas detaļas un pārvaldiet saistības", + "View name": "Skata nosaukums", + "View name is required": "Skata nosaukums ir obligāts", + "View object": "Skatīt objektu", + "View saved successfully!": "Skats veiksmīgi saglabāts!", + "View search analytics and manage search logs": "Skatiet meklēšanas analītiku un pārvaldiet meklēšanas žurnālus", + "View successfully deleted": "Skats veiksmīgi dzēsts", + "View successfully updated": "Skats veiksmīgi atjaunināts", + "View updated successfully!": "Skats veiksmīgi atjaunināts!", + "View webhook delivery logs and filter by webhook": "Skatiet tīmekļa āķa piegādes žurnālus un filtrējiet pēc tīmekļa āķa", + "View {count} more": "Skatīt vēl {count}", + "Views": "Skati", + "Visible to end users": "Redzams gala lietotājiem", + "Visible to users": "Redzams lietotājiem", + "Wait for Response": "Gaidīt atbildi", + "Wait for webhook response before continuing (required for request/response flows)": "Pirms turpināšanas gaidiet tīmekļa āķa atbildi (nepieciešams pieprasījuma/atbildes plūsmām)", + "Waiting for cluster synchronization...": "Tiek gaidīta klastera sinhronizācija...", + "Warmup Names Cache": "Uzsildīt nosaukumu kešatmiņu", + "Warmup failed": "Uzsildīšana neizdevās", + "Warning": "Brīdinājums", + "Warning:": "Brīdinājums:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Brīdinājums: šai īpašībai ir nekonsekventi tipi: {types}. Apsveriet, vai visiem objektiem vajadzētu būt vienam tipam.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Mēs skenēsim visus šai shēmai piederošos objektus, lai atklātu jaunas īpašības un analizētu esošās īpašības iespējamiem uzlabojumiem. Process ietver katra objekta datu struktūras pārbaudi, pašreizējā shēmā nedefinētu īpašību identificēšanu un iespēju atrašanu esošo īpašību definīciju uzlabošanai ar labākiem ierobežojumiem, formātiem un validācijas noteikumiem.", + "Webhook": "Tīmekļa āķis", + "Webhook Log Details": "Tīmekļa āķa žurnāla detaļas", + "Webhook Logs": "Tīmekļa āķa žurnāli", + "Webhook created successfully": "Tīmekļa āķis veiksmīgi izveidots", + "Webhook deleted": "Tīmekļa āķis dzēsts", + "Webhook retry delivered successfully": "Tīmekļa āķa atkārtošana veiksmīgi piegādāta", + "Webhook retry delivery failed": "Tīmekļa āķa atkārtošanas piegāde neizdevās", + "Webhook updated": "Tīmekļa āķis atjaunināts", + "Webhook updated successfully": "Tīmekļa āķis veiksmīgi atjaunināts", + "Webhooks": "Tīmekļa āķi", + "Weekly": "Iknedēļas", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kad aģents darbojas bez lietotāja sesijas (piemēram, plānotie uzdevumi), tiks izmantots šī lietotāja konteksts", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kad aģents darbojas bez lietotāja sesijas (piemēram, plānotie uzdevumi), tiks izmantots šī lietotāja konteksts.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Kad atspējots, šis aspekts rādīs tikai šīs shēmas vērtības un, atlasot, iekļaus shēmas filtru.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Kad iespējots, šī objekta saglabāšana atjauninās arī mērķa objektus, iekļaujot atsauci atpakaļ uz šo objektu.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Kad iespējots, šī īpašība tiks noņemta no avota objekta pēc mērķa objektu atjaunināšanas.", + "Workflow Management": "Darbplūsmu pārvaldība", + "Workflows": "Darbplūsmas", + "Wrap webhook payload in CloudEvents format for better interoperability": "Iesaiņojiet tīmekļa āķa lietderīgo slodzi CloudEvents formātā labākai sadarbspējai", + "Wrap webhook payload in cloudevents format for better interoperability": "Iesaiņojiet tīmekļa āķa lietderīgo slodzi cloudevents formātā labākai sadarbspējai", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Jā", + "You": "Jūs", + "You are not yet a member of any organisations.": "Jūs vēl neesat nevienas organizācijas dalībnieks.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Jūs varat izveidot jaunus ConfigSet, balstoties uz _default veidni, vai augšupielādēt pielāgotus tieši savā SOLR serverī.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Jūs varat izveidot jaunus configset, balstoties uz _default veidni, vai augšupielādēt pielāgotus tieši savā SOLR serverī.", + "You do not have access to this conversation": "Jums nav piekļuves šai sarunai", + "You do not have permission to delete this conversation": "Jums nav atļaujas dzēst šo sarunu", + "You do not have permission to modify this conversation": "Jums nav atļaujas modificēt šo sarunu", + "You do not have permission to restore this conversation": "Jums nav atļaujas atjaunot šo sarunu", + "You do not have permission to view the permission matrix. Admin access is required.": "Jums nav atļaujas skatīt atļauju matricu. Nepieciešama administratora piekļuve.", + "You have selected {count} properties to add to the schema.": "Jūs esat atlasījis {count} īpašības pievienošanai shēmai.", + "You must be logged in to favorite views": "Jums jābūt pieteiktam, lai pievienotu skatus izlasei", + "You need an AI agent to start a conversation.": "Lai sāktu sarunu, jums nepieciešams MI aģents.", + "Your Fireworks AI API key. Get one at": "Jūsu Fireworks AI API atslēga. Iegūstiet to vietnē", + "Your OpenAI API key. Get one at": "Jūsu OpenAI API atslēga. Iegūstiet to vietnē", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Jūsu atsauksme ir reģistrēta. Pēc izvēles jūs varat sniegt papildu informāciju šeit...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Jūsu atsauksme ir reģistrēta. Pēc izvēles jūs varat sniegt papildu informāciju šeit...", + "Zookeeper Hosts": "Zookeeper saimnieki", + "Zookeeper Port": "Zookeeper ports", + "Zookeeper Settings (SolrCloud)": "Zookeeper iestatījumi (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper savienojuma virkne SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper porta numurs (neobligāts, noklusējuma vērtība 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F NEATGRIEZENISKAS DZĒŠANAS BRĪDINĀJUMS \\u26A0\\uFE0F\n\nJūs gatavojaties NEATGRIEZENISKI dzēst VISUS shēmas \"{schema}\" objektus:\n\n\\u2022 Aktīvie objekti: {active}\n\\u2022 Mīksti dzēstie objekti: {deleted}\n\\u2022 Kopā: {total}\n\nŠie objekti tiks pilnībā noņemti no datubāzes un NEVARĒS tikt atjaunoti.\n\nVai esat pilnīgi pārliecināts?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F NEATGRIEZENISKAS DZĒŠANAS BRĪDINĀJUMS \\u26A0\\uFE0F\n\nJūs gatavojaties NEATGRIEZENISKI dzēst {count} mīksti dzēstos shēmas \"{schema}\" objektus.\n\nŠie objekti tiks pilnībā noņemti no datubāzes un NEVARĒS tikt atjaunoti.\n\nVai esat pilnīgi pārliecināts?", + "and accessible to all users in selected groups (or all users if no groups selected).": "un pieejams visiem lietotājiem atlasītajās grupās (vai visiem lietotājiem, ja nav atlasīta neviena grupa).", + "and add the files there.": "un pievienojiet failus tur.", + "approved": "apstiprināts", + "are only accessible to invited users.": "ir pieejami tikai uzaicinātajiem lietotājiem.", + "by": "pēc", + "chunk": "fragments", + "chunks": "fragmenti", + "chunks processed": "fragmenti apstrādāti", + "completed": "pabeigts", + "configuration(s)": "konfigurācija(-s)", + "conversationId is required": "conversationId ir obligāts", + "create": "izveidot", + "delete": "dzēst", + "deleted objects section": "dzēsto objektu sadaļa", + "e.g., lib/Settings/config.json": "piemēram, lib/Settings/config.json", + "e.g., lib/Settings/register.json": "piemēram, lib/Settings/register.json", + "e.g., lib/settings/config.json": "piemēram, lib/settings/config.json", + "e.g., lib/settings/register.json": "piemēram, lib/settings/register.json", + "execute": "izpildīt", + "failed": "neizdevās", + "file": "fails", + "files": "faili", + "file{plural}": "fail{plural}", + "folder": "mape", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "reģistrā {register}", + "in_progress": "norisinās", + "inherited": "mantots", + "list": "saraksts", + "log{plural}": "žurnāl{plural}", + "manage": "pārvaldīt", + "members": "dalībnieki", + "menu or contact someone with permission to create agents.": "izvēlni vai sazinieties ar kādu, kam ir atļauja izveidot aģentus.", + "message content is required": "ziņojuma saturs ir obligāts", + "messages": "ziņojumi", + "missing": "trūkst", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL un API atslēga ir obligāti", + "n8n configuration saved successfully": "n8n konfigurācija veiksmīgi saglabāta", + "n8n connection not configured": "n8n savienojums nav konfigurēts", + "n8n connection successful": "n8n savienojums veiksmīgs", + "n8n connection test failed: {message}": "n8n savienojuma pārbaude neizdevās: {message}", + "n8n connection test successful": "n8n savienojuma pārbaude veiksmīga", + "n8n initialization failed: {message}": "n8n inicializācija neizdevās: {message}", + "n8n integration disabled": "n8n integrācija atspējota", + "n8n integration enabled": "n8n integrācija iespējota", + "n8n project initialized successfully": "n8n projekts veiksmīgi inicializēts", + "n8n settings saved successfully": "n8n iestatījumi veiksmīgi saglabāti", + "notify_push configuration guide": "notify_push konfigurācijas ceļvedis", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push ir instalēts, taču OpenRegister vēl nav apstiprinājis veiksmīgu nosūtīšanu. Aktivizējiet, izsaucot objekta saglabāšanu, vai pārbaudiet savu notify_push konfigurāciju.", + "notify_push is installed but not yet active": "notify_push ir instalēts, bet vēl nav aktīvs", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekti", + "objects processed": "objekti apstrādāti", + "object{plural}": "objekt{plural}", + "or pick a range": "vai izvēlieties diapazonu", + "org-...": "org-...", + "pending": "gaida", + "provides a good balance between speed and accuracy.": "nodrošina labu līdzsvaru starp ātrumu un precizitāti.", + "public": "publisks", + "read": "lasīt", + "register(s)": "reģistrs(-i)", + "register(s) selected": "reģistrs(-i) atlasīts(-i)", + "register{plural}": "reģistr{plural}", + "rejected": "noraidīts", + "required": "obligāts", + "results": "rezultāti", + "schema(s)": "shēma(-s)", + "schema(s) selected": "shēma(-s) atlasīta(-s)", + "schema{plural}": "shēm{plural}", + "selected objects": "atlasītie objekti", + "set": "iestatīt", + "sk-...": "sk-...", + "this application": "šī lietojumprogramma", + "to": "līdz", + "type must be \"positive\" or \"negative\"": "tipam jābūt \"positive\" vai \"negative\"", + "update": "atjaunināt", + "vectorized": "vektorizēts", + "view": "skats", + "views selected": "skati atlasīti", + "waiting": "gaida", + "{active} active, {deleted} deleted": "{active} aktīvi, {deleted} dzēsti", + "{count} audit trails cleared successfully": "{count} audita pieraksti veiksmīgi notīrīti", + "{count} compliance issue(s) detected": "Atklāta(-s) {count} atbilstības problēma(-s)", + "{count} deletions": "{count} dzēšanas", + "{count} documents found": "Atrasti {count} dokumenti", + "{count} email": "{count} e-pasts", + "{count} emails": "{count} e-pasti", + "{count} entries": "{count} ieraksti", + "{count} issues": "{count} problēmas", + "{count} members": "{count} dalībnieki", + "{count} searches": "{count} meklējumi", + "{count} selected": "{count} atlasīti", + "{count} widget(s)": "{count} logrīks(-i)", + "{matched} object(s) matched. {erased} erased.": "Atbilst {matched} objekts(-i). Izdzēsti {erased}.", + "{minutes} minutes ago": "pirms {minutes} minūtēm", + "{percentage}% of objects": "{percentage}% objektu", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} reģistrā {register} / {schema}", + "{type} cache cleared successfully": "{type} kešatmiņa veiksmīgi notīrīta", + "ℹ️ Current Configuration": "ℹ️ Pašreizējā konfigurācija", + "ℹ️ current configuration": "ℹ️ pašreizējā konfigurācija", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ BĪSTAMI: visi {total} objekti tiks NEATGRIEZENISKI DZĒSTI no datubāzes. Šī darbība ir NEATGRIEZENISKA, un to nevar atsaukt!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ NEATGRIEZENISKAS DZĒŠANAS BRĪDINĀJUMS ⚠️\n\nJūs gatavojaties NEATGRIEZENISKI dzēst VISUS shēmas \"{schema}\" objektus:\n\n• Aktīvie objekti: {active}\n• Mīksti dzēstie objekti: {deleted}\n• Kopā: {total}\n\nŠie objekti tiks pilnībā noņemti no datubāzes un NEVARĒS tikt atjaunoti.\n\nVai esat pilnīgi pārliecināts?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ NEATGRIEZENISKAS DZĒŠANAS BRĪDINĀJUMS ⚠️\n\nJūs gatavojaties NEATGRIEZENISKI dzēst {count} mīksti dzēstos shēmas \"{schema}\" objektus.\n\nŠie objekti tiks pilnībā noņemti no datubāzes un NEVARĒS tikt atjaunoti.\n\nVai esat pilnīgi pārliecināts?", + "⚡ Batch Processing": "⚡ Pakešapstrāde", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR var apstrādāt miljoniem dokumentu ar meklēšanas atbildes laiku, kas mazāks par sekundi...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Visaptverošs konteksts ✓ Mazāka iespēja palaist garām detaļas ✗ Lēnākas atbildes ✗ Var iekļaut mazāk būtisku informāciju", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Ātrākas atbildes ✓ Vairāk fokusētas atbildes ✗ Var palaist garām būtisku informāciju", + "✨ AI Features": "✨ MI funkcijas", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Sadalītajā režīmā SOLR izmanto ZooKeeper klastera koordinācijai...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Aspektu meklēšana ļauj lietotājiem detalizēti pārlūkot rezultātus pēc kategorijām...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Vai zinājāt? SOLR nodrošina meklēšanu Netflix, Apple un daudzām citām nozīmīgām vietnēm!", + "💰 View Selection (Cost Optimization)": "💰 Skatu atlase (izmaksu optimizācija)", + "📄 Object Serialization": "📄 Objektu serializācija", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR nodrošina bagātīgu analītiku un statistiku par meklēšanas veiktspēju...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSet satur shēmas un konfigurācijas failus jūsu meklēšanas indeksam...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikācija nodrošina, ka jūsu meklēšanas indekss ir pieejams pat tad, ja mezgli neizdodas...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR ir jaudīga uzņēmuma meklēšanas platforma, kas veidota uz Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Vektorizācijas iestatījumi", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automātiskā mērogošana var dinamiski pievienot vai noņemt mezglus atkarībā no slodzes...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Drošības funkcijas ietver autentifikāciju, autorizāciju un SSL šifrēšanu...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-pasts", + "{count} e-pasti" + ], + "Purge {count} object from database": [ + "Iztīrīt {count} objektu no datubāzes", + "Iztīrīt {count} objektus no datubāzes" + ], + "Restore {count} object": [ + "Atjaunot {count} objektu", + "Atjaunot {count} objektus" + ], + "Successfully restored {count} object": [ + "Veiksmīgi atjaunots {count} objekts", + "Veiksmīgi atjaunoti {count} objekti" + ], + "Delete {count} object": [ + "Dzēst {count} objektu", + "Dzēst {count} objektus" + ], + "Object successfully deleted": [ + "Objekts veiksmīgi dzēsts", + "Objekti veiksmīgi dzēsti" + ] + } +} diff --git a/l10n/mk.js b/l10n/mk.js new file mode 100644 index 0000000000..9e19113e96 --- /dev/null +++ b/l10n/mk.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(без тема)", + "(no title)" : "(без наслов)", + "(unnamed)" : "(неименувано)", + "(untitled card)" : "(картичка без наслов)", + "Activities" : "Активности", + "Add Endpoint" : "Додај крајна точка", + "Add Source" : "Додај извор", + "Add card" : "Додај картичка", + "Add contact" : "Додај контакт", + "All clear" : "Сè е во ред", + "Articles" : "Статии", + "Code" : "Код", + "Compliance" : "Сообразност", + "Contacts" : "Контакти", + "Create Agent" : "Создај агент", + "Create Application" : "Создај апликација", + "Create event" : "Создај настан", + "Erasure complete" : "Бришењето е завршено", + "Erasure preview" : "Преглед на бришење", + "Expand details" : "Прошири детали", + "Save changes" : "Зачувај промени", + "📄 Object Serialization" : "📄 Серијализација на објект", + "🔢 Vectorization Settings" : "🔢 Поставки за векторизација", + "💰 View Selection (Cost Optimization)" : "💰 Избор на приказ (Оптимизација на трошоци)", + "AI Assistant" : "ВИ-помошник", + "AI Chat" : "ВИ-разговор", + "API Key" : "API-клуч", + "API tokens saved successfully" : "API-токените се успешно зачувани", + "About ConfigSets" : "За ConfigSets", + "Action" : "Дејство", + "Action Distribution" : "Распределба на дејства", + "Action:" : "Дејство:", + "Actions" : "Дејства", + "Active" : "Активно", + "Active Collections" : "Активни колекции", + "Active Filters" : "Активни филтри", + "Active Organisations" : "Активни организации", + "Active filters:" : "Активни филтри:", + "Active organisation set successfully" : "Активната организација е успешно поставена", + "Activity Period" : "Период на активност", + "Add Groups" : "Додај групи", + "Add Schema" : "Додај Шема", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Додајте наслови на шеми, описи и информации за регистар за да обезбедите побогат контекст за пребарување", + "Add to favorites" : "Додај во омилени", + "Added" : "Додадено", + "Added to favorites" : "Додадено во омилени", + "Additional Information" : "Дополнителни информации", + "Additional feedback saved. Thank you!" : "Дополнителните повратни информации се зачувани. Ви благодариме!", + "Admin" : "Администратор", + "Advanced" : "Напредно", + "Advanced Filters" : "Напредни филтри", + "Advanced Options" : "Напредни опции", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Напредните филтри не се достапни при користење на базата на податоци како Извор. Префрлете на Авто или SOLR-индекс за опции за филтрирање.", + "After" : "По", + "Agent deleted successfully" : "Агентот е успешно избришан", + "Agents" : "Агенти", + "All" : "Сите", + "All Categories" : "Сите категории", + "All Collections" : "Сите колекции", + "All Confidence Levels" : "Сите нивоа на доверба", + "All ConfigSets" : "Сите ConfigSets", + "All Files" : "Сите Датотеки", + "All Levels" : "Сите нивоа", + "All Types" : "Сите типови", + "All Webhooks" : "Сите веб-кукички", + "All actions" : "Сите дејства", + "All audit trails cleared successfully" : "Сите ревизиски траги се успешно исчистени", + "All registers" : "Сите регистри", + "All schemas" : "Сите шеми", + "All searches" : "Сите пребарувања", + "All users" : "Сите корисници", + "An error occurred while clearing audit trails" : "Се појави грешка при чистењето на ревизиските траги", + "An error occurred while deleting the audit trail" : "Се појави грешка при бришењето на ревизиската трага", + "An error occurred while deleting the view" : "Се појави грешка при бришењето на приказот", + "An error occurred while permanently deleting the objects" : "Се појави грешка при трајното бришење на објектите", + "Analysis completed:" : "Анализата е завршена:", + "Analysis steps:" : "Чекори на анализа:", + "Analytics" : "Аналитика", + "Analyze Objects" : "Анализирај Објекти", + "Analyze Properties" : "Анализирај Својства", + "Analyze existing properties for improvement opportunities" : "Анализирајте ги постоечките својства за можности за подобрување", + "Analyzing..." : "Се анализира...", + "Any favorites and sharing settings for this view" : "Сите омилени и поставки за споделување за овој приказ", + "Any user" : "Кој било корисник", + "App store cache invalidated successfully" : "Кешот на App store е успешно поништен", + "Application deleted successfully" : "Апликацијата е успешно избришана", + "Applications" : "Апликации", + "Apply Changes" : "Примени промени", + "Archive" : "Архива", + "Archive conversation" : "Архивирај разговор", + "Archived conversations are hidden from your active list" : "Архивираните разговори се скриени од вашата активна листа", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Дали сте сигурни дека сакате да ги исчистите старите траги на пребарување? Ова ќе ги избрише записите постари од 30 дена.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Дали сте сигурни дека сакате да ги избришете избраните ревизиски траги? Ова дејство не може да се отповика.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Дали сте сигурни дека сакате да ги избришете избраните траги на пребарување? Ова дејство не може да се отповика.", + "Are you sure you want to delete this ConfigSet?" : "Дали сте сигурни дека сакате да го избришете овој ConfigSet?", + "Are you sure you want to permanently delete" : "Дали сте сигурни дека сакате трајно да избришете", + "Ask a question..." : "Поставете прашање...", + "Ask questions about your data using natural language" : "Поставувајте прашања за вашите податоци користејќи природен јазик", + "Attempt" : "Обид", + "Audit Trail Changes" : "Промени на ревизиска трага", + "Audit Trail Details" : "Детали за ревизиска трага", + "Audit Trail Management" : "Управување со ревизиска трага", + "Audit Trail Statistics" : "Статистика на ревизиска трага", + "Audit Trails" : "Ревизиски траги", + "Audit trail data copied to clipboard" : "Податоците за ревизиска трага се копирани во таблата со исечоци", + "Audit trail deleted successfully" : "Ревизиската трага е успешно избришана", + "Audit trail successfully deleted" : "Ревизиската трага е успешно избришана", + "Authentication" : "Автентикација", + "Auto-commit disabled" : "Авто-предавањето е оневозможено", + "Auto-commit enabled" : "Авто-предавањето е овозможено", + "Auto-create Default Organisation" : "Авто-создавање на стандардна организација", + "Auto-retry failed vectorizations" : "Авто-повторување на неуспешни векторизации", + "Automatically commit changes to SOLR index" : "Автоматски предавај ги промените во SOLR-индексот", + "Automatically create a default organisation if none exists when the app is initialized" : "Автоматски создај стандардна организација ако не постои при иницијализирање на апликацијата", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Автоматски генерирај векторски вградувања од текстуални делови кога Датотеките се прикачуваат и обработуваат", + "Automatically generate vector embeddings when objects are created or updated" : "Автоматски генерирај векторски вградувања кога објектите се создаваат или ажурираат", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Автоматски повторувај ги неуспешните обиди за векторизација (макс. 3 повторувања)", + "Available Filters" : "Достапни филтри", + "Available Workflows" : "Достапни работни текови", + "Avg Execution Time" : "Просечно време на извршување", + "Avg Members/Org" : "Просечно членови/орг.", + "Avg Object Views/Session" : "Просечно прегледи на Објекти/сесија", + "Avg Response Time" : "Просечно време на одговор", + "Avg Results/Search" : "Просечно резултати/пребарување", + "Avg Searches/Session" : "Просечно пребарувања/сесија", + "Back" : "Назад", + "Back to Entities" : "Назад кон ентитети", + "Back to Registers" : "Назад кон регистри", + "Back to Webhooks" : "Назад кон веб-кукички", + "Back to applications" : "Назад кон апликации", + "Back to entities" : "Назад кон ентитети", + "Backend updated successfully. Please reload the application." : "Заднинскиот дел е успешно ажуриран. Ве молиме повторно вчитајте ја апликацијата.", + "Base URL (Optional)" : "Основен URL (Опционално)", + "Basic Information" : "Основни информации", + "Batch Size" : "Големина на серија", + "Batch extraction completed" : "Серискoто извлекување е завршено", + "Before" : "Пред", + "Before object vectorization can work:" : "Пред да може да работи векторизацијата на објекти:", + "Behavior Issue" : "Проблем со однесување", + "Bookmarks" : "Обележувачи", + "Blob storage has been retired. All objects now use magic tables." : "Складирањето на blob е повлечено. Сите објекти сега користат магични табели.", + "Branch" : "Гранка", + "Bulk delete operation completed successfully" : "Масовната операција на бришење е успешно завршена", + "Bulk save operation completed successfully" : "Масовната операција на зачувување е успешно завршена", + "Business Data" : "Деловни податоци", + "Cache cleared successfully" : "Кешот е успешно исчистен", + "Calculate Sizes" : "Пресметај големини", + "Cancel" : "Откажи", + "Cannot delete: objects are still attached" : "Не може да се избрише: објектите се сè уште прикачени", + "Category" : "Категорија", + "Change Type" : "Тип на промена", + "Changes" : "Промени", + "Changes copied to clipboard" : "Промените се копирани во таблата со исечоци", + "Chat Model" : "Модел за разговор", + "Chat Provider" : "Давател за разговор", + "Chat Provider (RAG)" : "Давател за разговор (RAG)", + "Chat Settings" : "Поставки за разговор", + "Chat provider connection successful!" : "Поврзувањето со давателот за разговор е успешно!", + "Chat settings" : "Поставки за разговор", + "Choose a register" : "Изберете Регистар", + "Choose a schema" : "Изберете Шема", + "Choose how vector similarity calculations are performed for semantic search" : "Изберете како се вршат пресметките на векторска сличност за семантичко пребарување", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Изберете кои типови на Датотеки да се вклучат во процесот на векторизација. Ќе се обработат само Датотеки со извлечен текст и делови.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Изберете кои прикази да се вклучат во процесот на векторизација. Оставете празно за да се обработат сите прикази врз основа на вашата конфигурација.", + "Chunk" : "Дел", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Бришењето на делови сè уште не е имплементирано. Користете крајни точки засновани на делови.", + "Chunks" : "Делови", + "Chunks to Vectorize:" : "Делови за векторизирање:", + "Cleanup Old Trails" : "Исчисти стари траги", + "Cleanup completed" : "Чистењето е завршено", + "Clear All Embeddings" : "Исчисти ги сите вградувања", + "Clear Entries" : "Исчисти записи", + "Clear Filtered Audit Trails" : "Исчисти филтрирани ревизиски траги", + "Clear Filters" : "Исчисти филтри", + "Clear Index" : "Исчисти индекс", + "Clear Selection" : "Исчисти избор", + "Clear all" : "Исчисти сè", + "Clear all filters" : "Исчисти ги сите филтри", + "Clear date range" : "Исчисти опсег на датуми", + "Clearing..." : "Се чисти...", + "Close" : "Затвори", + "Collection Management" : "Управување со колекции", + "Collection Name" : "Име на колекција", + "Collection assignments updated successfully" : "Доделувањата на колекции се успешно ажурирани", + "Collection cleared successfully" : "Колекцијата е успешно исчистена", + "Collection copied successfully" : "Колекцијата е успешно копирана", + "Collection created successfully" : "Колекцијата е успешно создадена", + "Collection deleted successfully" : "Колекцијата е успешно избришана", + "Collection used to store and index file metadata and content" : "Колекција што се користи за складирање и индексирање на метаподатоци и содржина на Датотеки", + "Collection used to store and index object data" : "Колекција што се користи за складирање и индексирање на податоци за објекти", + "Collections:" : "Колекции:", + "Commit Message" : "Порака за предавање", + "Commit Within (ms)" : "Предади во рок од (ms)", + "Commit message" : "Порака за предавање", + "Compare current schema with real object data" : "Споредете ја тековната Шема со реалните податоци за објект", + "Completed" : "Завршено", + "Complex" : "Сложено", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Сложени барања: Напредни пребарувања со повеќе филтри, оператори и сложени комбинации на параметри", + "Confidence Level" : "Ниво на доверба", + "Confidence Score" : "Резултат на доверба", + "Config must be provided as an object" : "Config мора да биде обезбеден како објект", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Управување со ConfigSet", + "ConfigSet Name" : "Име на ConfigSet", + "ConfigSet created successfully" : "ConfigSet е успешно создаден", + "ConfigSet deleted successfully" : "ConfigSet е успешно избришан", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets ја дефинираат Шемата и конфигурацијата за вашите SOLR-колекции. Тие содржат дефиниции на полиња, анализатори и други поставки за пребарување.", + "Configuration" : "Конфигурација", + "Configuration Data" : "Податоци за конфигурација", + "Configuration saved" : "Конфигурацијата е зачувана", + "Configurations" : "Конфигурации", + "Configure Facets" : "Конфигурирај фасети", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Конфигурирајте даватели на големи јазични модели (LLM) за функции напојувани со ВИ, вклучувајќи семантичко пребарување, вградувања и разговор.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Конфигурирајте основни поставки за поврзување за вашиот SOLR-сервер, вклучувајќи автентикација и мрежни опции. Користете ги одделните дијалози за ConfigSet и Управување со колекции за управување со јадра и колекции.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Конфигурирајте како објектите од базата на податоци се претвораат во векторски вградувања за семантичко пребарување. Објектите се векторизираат директно без потреба од извлекување текст.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Конфигурирајте како објектите се претвораат во текст пред векторизацијата. Овие поставки влијаат на квалитетот и контекстот на пребарувањето.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Конфигурирајте параметри за векторизација на Датотеки. Овој процес ќе генерира векторски вградувања за сите извлечени делови на Датотеки.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Конфигурирајте параметри за векторизација на објекти. Овој процес ќе генерира векторски вградувања за сите објекти што се совпаѓаат со филтрите на вашиот приказ.", + "Configure which types of data to search and how many sources to retrieve" : "Конфигурирајте кои типови на податоци да се пребаруваат и колку извори да се преземат", + "Connection Failed" : "Поврзувањето не успеа", + "Connection Settings" : "Поставки за поврзување", + "Connection Successful!" : "Поврзувањето е успешно!", + "Connection failed" : "Поврзувањето не успеа", + "Connection protocol" : "Протокол за поврзување", + "Connection successful" : "Поврзувањето е успешно", + "Connection timeout in seconds" : "Истек на време за поврзување во секунди", + "Constant delay between retries (5 minutes)" : "Постојано задоцнување меѓу повторувањата (5 минути)", + "Constraint Issue" : "Проблем со ограничување", + "Content Filters" : "Филтри за содржина", + "Continue to Properties" : "Продолжи кон својства", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Контролирајте кои прикази на објекти треба да се векторизираат за да се намалат трошоците за API. Векторизирајте само прикази што имаат корист од семантичко пребарување.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Контролирајте кои прикази и алатки може да ги користи ВИ во овој разговор. Стандардно, сите способности на агентот се овозможени.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Ја контролира случајноста (0 = детерминистичко, 2 = многу креативно)", + "Conversation ID is required" : "ID на разговор е задолжителен", + "Conversation archived" : "Разговорот е архивиран", + "Conversation archived successfully" : "Разговорот е успешно архивиран", + "Conversation cleared successfully" : "Разговорот е успешно исчистен", + "Conversation deleted" : "Разговорот е избришан", + "Conversation permanently deleted" : "Разговорот е трајно избришан", + "Conversation renamed" : "Разговорот е преименуван", + "Conversation restored" : "Разговорот е обновен", + "Conversation title" : "Наслов на разговор", + "Copied!" : "Копирано!", + "Copy" : "Копирај", + "Copy Changes" : "Копирај промени", + "Copy Collection" : "Копирај колекција", + "Copy Data" : "Копирај податоци", + "Copy Full Data" : "Копирај целосни податоци", + "Copying..." : "Се копира...", + "Count" : "Број", + "Counting objects..." : "Се бројат објекти...", + "Costs" : "Трошоци", + "Create" : "Создај", + "Create Collection" : "Создај колекција", + "Create ConfigSet" : "Создај ConfigSet", + "Create New Collection" : "Создај нова колекција", + "Create New ConfigSet" : "Создај нов ConfigSet", + "Create Operations" : "Операции на создавање", + "Create Webhook" : "Создај веб-кукичка", + "Create a copy of collection:" : "Создај копија на колекцијата:", + "Create a new ConfigSet based on the _default template" : "Создај нов ConfigSet врз основа на шаблонот _default", + "Create a new SOLR collection from an existing ConfigSet" : "Создај нова SOLR-колекција од постоечки ConfigSet", + "Create your first AI agent to get started." : "Создајте го вашиот прв ВИ-агент за да започнете.", + "Created" : "Создадено", + "Created:" : "Создадено:", + "Creating..." : "Се создава...", + "Current Filters" : "Тековни филтри", + "Current Type:" : "Тековен тип:", + "Custom API endpoint if using a different region" : "Прилагодена API-крајна точка ако се користи различен регион", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Прилагодени HTTP-заглавја (едно по ред, формат: Header-Name: value)", + "Daily" : "Дневно", + "Dashboard" : "Контролна табла", + "Data Source" : "Извор на податоци", + "Data sources" : "Извори на податоци", + "Data type does not match observed values" : "Типот на податок не се совпаѓа со набљудуваните вредности", + "Database URL" : "URL на база на податоци", + "Database information refreshed" : "Информациите за базата на податоци се освежени", + "Date Range" : "Опсег на датуми", + "Default" : "Стандардно", + "Default Organisation" : "Стандардна организација", + "Default: 5" : "Стандардно: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Задоцнувањата се удвојуваат со секој обид (2, 4, 8 минути...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Задоцнувањата се зголемуваат линеарно (5, 10, 15 минути...)", + "Delete" : "Избриши", + "Delete Application" : "Избриши апликација", + "Delete Audit Trail" : "Избриши ревизиска трага", + "Delete Collection" : "Избриши колекција", + "Delete ConfigSet" : "Избриши ConfigSet", + "Delete Objects" : "Избриши Објекти", + "Delete Operations" : "Операции на бришење", + "Delete View" : "Избриши приказ", + "Delete all objects in this schema" : "Избриши ги сите објекти во оваа Шема", + "Delete permanently" : "Избриши трајно", + "Delete this conversation?" : "Да се избрише овој разговор?", + "Delete view" : "Избриши приказ", + "Deleted" : "Избришано", + "Deleted By" : "Избришано од", + "Deleted Date" : "Датум на бришење", + "Deleted Items Management" : "Управување со избришани ставки", + "Deleted This Week" : "Избришано оваа недела", + "Deleted Today" : "Избришано денес", + "Deleted:" : "Избришано:", + "Deleting..." : "Се брише...", + "Deletion Date Range" : "Опсег на датуми на бришење", + "Deletion Statistics" : "Статистика на бришење", + "Deprecated" : "Застарено", + "Description" : "Опис", + "Description:" : "Опис:", + "Detect data types and patterns" : "Откривај типови на податоци и обрасци", + "Detected At" : "Откриено на", + "Detected Format:" : "Откриен формат:", + "Detected Issues:" : "Откриени проблеми:", + "Disable" : "Оневозможи", + "Disabled" : "Оневозможено", + "Discover Files" : "Откриј Датотеки", + "Discovered Properties" : "Откриени својства", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Дали сакате трајно да ги избришете сите филтрирани записи на ревизиска трага? Ова дејство не може да се отповика.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Дали сакате трајно да го избришете овој запис на ревизиска трага? Ова дејство не може да се отповика.", + "Documents" : "Документи", + "Download API Spec" : "Преземи API-спецификација", + "Edit" : "Уреди", + "Edit Register" : "Уреди Регистар", + "Edit Schema" : "Уреди Шема", + "Edit View" : "Уреди приказ", + "Edit Webhook" : "Уреди веб-кукичка", + "Edit view" : "Уреди приказ", + "Edit view details" : "Уреди детали за приказ", + "Effectiveness" : "Ефективност", + "Email" : "Е-пошта", + "Emails" : "Е-пошти", + "Embedding Model" : "Модел за вградување", + "Embedding Provider" : "Давател за вградување", + "Embedding generated successfully" : "Вградувањето е успешно генерирано", + "Embedding provider connection successful!" : "Поврзувањето со давателот за вградување е успешно!", + "Enable" : "Овозможи", + "Enable auto-creation" : "Овозможи авто-создавање", + "Enable automatic file vectorization" : "Овозможи автоматска векторизација на Датотеки", + "Enable automatic object vectorization" : "Овозможи автоматска векторизација на објекти", + "Enable automatic synchronization" : "Овозможи автоматска синхронизација", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Овозможи детално евидентирање за SOLR-операции (препорачано за дебагирање)", + "Enable faceting" : "Овозможи фасетирање", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Овозможи или оневозможи LLM-функции. Конфигурирајте даватели и модели користејќи го копчето за LLM-конфигурација погоре.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Овозможи или оневозможи интеграција на SOLR-пребарување. Конфигурирајте поставки за поврзување користејќи го копчето за Поставки за поврзување погоре.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Овозможи или оневозможи интеграција на n8n работен тек. Конфигурирајте поставки за поврзување подолу.", + "Enable or disable this webhook" : "Овозможи или оневозможи ја оваа веб-кукичка", + "Enable vectorization for all existing and future views (may increase costs)" : "Овозможи векторизација за сите постоечки и идни прикази (може да ги зголеми трошоците)", + "Enabled" : "Овозможено", + "Endpoints" : "Крајни точки", + "Enter ConfigSet name" : "Внесете име на ConfigSet", + "Enter collection name" : "Внесете име на колекција", + "Enter description (optional)..." : "Внесете опис (опционално)...", + "Enter new collection name" : "Внесете ново име на колекција", + "Enter object ID" : "Внесете ID на објект", + "Enter search term" : "Внесете поим за пребарување", + "Enter view name..." : "Внесете име на приказ...", + "Enter webhook name" : "Внесете име на веб-кукичка", + "Entities" : "Ентитети", + "Entity Information" : "Информации за ентитет", + "Entity deleted successfully" : "Ентитетот е успешно избришан", + "Entity not found" : "Ентитетот не е пронајден", + "Entries to be deleted:" : "Записи за бришење:", + "Enum Issue" : "Проблем со набројување", + "Enum constraint is missing" : "Недостасува ограничување за набројување", + "Error" : "Грешка", + "Error Details" : "Детали за грешка", + "Error Information" : "Информации за грешка", + "Error Message" : "Порака за грешка", + "Error loading application" : "Грешка при вчитување на апликација", + "Error loading audit trails" : "Грешка при вчитување на ревизиски траги", + "Error loading entity" : "Грешка при вчитување на ентитет", + "Error loading search trails" : "Грешка при вчитување на траги на пребарување", + "Estimated Batches:" : "Проценети серии:", + "Estimated Cost:" : "Проценет трошок:", + "Estimated Duration:" : "Проценето траење:", + "Event" : "Настан", + "Event Property for Payload" : "Својство на настан за носивост", + "Events" : "Настани", + "Example Value" : "Пример вредност", + "Exclusive Maximum" : "Ексклузивен максимум", + "Exclusive Minimum" : "Ексклузивен минимум", + "Execution Mode" : "Режим на извршување", + "Execution Time" : "Време на извршување", + "Execution Time Range" : "Опсег на време на извршување", + "Existing Improvements" : "Постоечки подобрувања", + "Exponential" : "Експоненцијално", + "Export" : "Извези", + "Export completed successfully" : "Извезувањето е успешно завршено", + "Export, view, or delete audit trails" : "Извези, прегледај или избриши ревизиски траги", + "Extend Schema" : "Прошири Шема", + "Extended" : "Проширено", + "Extensions" : "Екстензии", + "Extract Pending Files" : "Извлечи Датотеки на чекање", + "Extract properties from each object" : "Извлечи својства од секој објект", + "Extracted At" : "Извлечено на", + "Extraction Status" : "Статус на извлекување", + "Facet configuration updated successfully" : "Конфигурацијата на фасети е успешно ажурирана", + "Facets discovered and configured successfully" : "Фасетите се успешно откриени и конфигурирани", + "Facets discovered successfully" : "Фасетите се успешно откриени", + "Failed" : "Неуспешно", + "Failed to analyze schema properties" : "Не успеа анализирањето на својствата на Шемата", + "Failed to archive conversation" : "Не успеа архивирањето на разговорот", + "Failed to calculate sizes" : "Не успеа пресметувањето на големините", + "Failed to clear collection" : "Не успеа чистењето на колекцијата", + "Failed to copy changes" : "Не успеа копирањето на промените", + "Failed to copy data" : "Не успеа копирањето на податоците", + "Failed to copy data to clipboard" : "Не успеа копирањето на податоците во таблата со исечоци", + "Failed to create conversation" : "Не успеа создавањето на разговорот", + "Failed to create or find project" : "Не успеа создавањето или наоѓањето на проектот", + "Failed to delete collection" : "Не успеа бришењето на колекцијата", + "Failed to delete conversation" : "Не успеа бришењето на разговорот", + "Failed to delete webhook" : "Не успеа бришењето на веб-кукичката", + "Failed to download API specification" : "Не успеа преземањето на API-спецификацијата", + "Failed to get SOLR field configuration" : "Не успеа добивањето на конфигурацијата на SOLR-поле", + "Failed to get entity categories" : "Не успеа добивањето на категориите на ентитети", + "Failed to get entity statistics" : "Не успеа добивањето на статистиката на ентитети", + "Failed to get entity types" : "Не успеа добивањето на типовите на ентитети", + "Failed to load LLM configuration" : "Не успеа вчитувањето на LLM-конфигурацијата", + "Failed to load Nextcloud groups" : "Не успеа вчитувањето на групите на Nextcloud", + "Failed to load conversation" : "Не успеа вчитувањето на разговорот", + "Failed to load entities" : "Не успеа вчитувањето на ентитетите", + "Failed to load entity" : "Не успеа вчитувањето на ентитетот", + "Failed to load files" : "Не успеа вчитувањето на Датотеките", + "Failed to load organisations" : "Не успеа вчитувањето на организациите", + "Failed to load templates" : "Не успеа вчитувањето на шаблоните", + "Failed to load webhooks" : "Не успеа вчитувањето на веб-кукичките", + "Failed to load workflows" : "Не успеа вчитувањето на работните текови", + "Failed to refresh database information" : "Не успеа освежувањето на информациите за базата на податоци", + "Failed to reindex collection" : "Не успеа повторното индексирање на колекцијата", + "Failed to rename conversation" : "Не успеа преименувањето на разговорот", + "Failed to restore conversation" : "Не успеа обновувањето на разговорот", + "Failed to retry extraction" : "Не успеа повторувањето на извлекувањето", + "Failed to retry webhook" : "Не успеа повторувањето на веб-кукичката", + "Failed to save GitHub token" : "Не успеа зачувувањето на GitHub-токенот", + "Failed to save GitLab URL" : "Не успеа зачувувањето на GitLab-URL", + "Failed to save GitLab token" : "Не успеа зачувувањето на GitLab-токенот", + "Failed to save additional feedback" : "Не успеа зачувувањето на дополнителните повратни информации", + "Failed to save n8n configuration" : "Не успеа зачувувањето на n8n-конфигурацијата", + "Failed to save roles" : "Не успеа зачувувањето на улогите", + "Failed to save settings" : "Не успеа зачувувањето на поставките", + "Failed to save webhook" : "Не успеа зачувувањето на веб-кукичката", + "Failed to send feedback" : "Не успеа испраќањето на повратните информации", + "Failed to test webhook" : "Не успеа тестирањето на веб-кукичката", + "Failed to update favorite status" : "Не успеа ажурирањето на статусот на омилено", + "Failed to update schema properties" : "Не успеа ажурирањето на својствата на Шемата", + "Failed to update view" : "Не успеа ажурирањето на приказот", + "Failed to update webhook" : "Не успеа ажурирањето на веб-кукичката", + "Feedback recorded" : "Повратните информации се забележани", + "Fewer sources (1-3):" : "Помалку извори (1-3):", + "Field" : "Поле", + "File" : "Датотека", + "File Chunk Prediction" : "Предвидување на делови на Датотека", + "File Collection" : "Колекција на Датотеки", + "File Management" : "Управување со Датотеки", + "File Name" : "Име на Датотека", + "File Path" : "Патека на Датотека", + "File Type Selection" : "Избор на тип на Датотека", + "File Vectorization" : "Векторизација на Датотеки", + "File Warmup" : "Загревање на Датотеки", + "File actions menu" : "Мени за дејства на Датотека", + "File anonymized successfully" : "Датотеката е успешно анонимизирана", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Векторизацијата на делови на Датотека сè уште не е имплементирана. Деловите се подготвени и складирани, но услугата за векторизација е во развој.", + "File collection not configured" : "Колекцијата на Датотеки не е конфигурирана", + "File discovery completed" : "Откривањето на Датотеки е завршено", + "File extraction completed" : "Извлекувањето на Датотека е завршено", + "File extraction queued" : "Извлекувањето на Датотека е во редица", + "File indexed successfully" : "Датотеката е успешно индексирана", + "File is already anonymized" : "Датотеката е веќе анонимизирана", + "File not found" : "Датотеката не е пронајдена", + "File settings updated successfully" : "Поставките на Датотеката се успешно ажурирани", + "File sources" : "Извори на Датотеки", + "File vectorization configuration saved successfully" : "Конфигурацијата за векторизација на Датотеки е успешно зачувана", + "File vectorization started. Check the statistics section for progress." : "Векторизацијата на Датотеки започна. Проверете го делот за статистика за напредок.", + "File warmup completed" : "Загревањето на Датотеки е завршено", + "Files" : "Датотеки", + "Files with Completed Extraction:" : "Датотеки со завршено извлекување:", + "Files → fileCollection, Objects → objectCollection" : "Датотеки → fileCollection, Објекти → objectCollection", + "Filter Audit Trails" : "Филтрирај ревизиски траги", + "Filter Deleted Items" : "Филтрирај избришани ставки", + "Filter Objects" : "Филтрирај Објекти", + "Filter Properties" : "Филтрирај својства", + "Filter Search Trails" : "Филтрирај траги на пребарување", + "Filter Statistics" : "Филтрирај статистика", + "Filter and analyze search trail entries" : "Филтрирај и анализирај записи на траги на пребарување", + "Filter and manage audit trail entries" : "Филтрирај и управувај со записи на ревизиска трага", + "Filter and manage soft deleted items" : "Филтрирај и управувај со меко избришани ставки", + "Filter by object ID" : "Филтрирај по ID на објект", + "Filter by search term" : "Филтрирај по поим за пребарување", + "Filter by webhook" : "Филтрирај по веб-кукичка", + "Filter data loaded automatically. Use the filters below to refine your search." : "Податоците за филтрирање се вчитани автоматски. Користете ги филтрите подолу за да го прецизирате вашето пребарување.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Филтрирај активирачи на веб-кукичка по својства на носивост (едно по ред, формат: key: value)", + "Filtered" : "Филтрирано", + "Filters" : "Филтри", + "Fireworks AI Chat Settings" : "Поставки за разговор на Fireworks AI", + "Fireworks AI Embedding Configuration" : "Конфигурација за вградување на Fireworks AI", + "First" : "Прво", + "Fixed" : "Поправено", + "For chat and retrieval-augmented generation" : "За разговор и генерирање потпомогнато со пребарување", + "For vector embeddings and semantic search" : "За векторски вградувања и семантичко пребарување", + "Format" : "Формат", + "Format Issue" : "Проблем со формат", + "Format constraint is missing" : "Недостасува ограничување за формат", + "From Date" : "Од датум", + "From date" : "Од датум", + "Full data copied to clipboard" : "Целосните податоци се копирани во таблата со исечоци", + "General Issue" : "Општ проблем", + "Generate recommendations and confidence scores" : "Генерирај препораки и резултати на доверба", + "Generate vectors immediately when new objects are created" : "Генерирај вектори веднаш кога се создаваат нови објекти", + "GitHub token is valid" : "GitHub-токенот е валиден", + "GitHub token removed successfully" : "GitHub-токенот е успешно отстранет", + "GitHub token saved successfully" : "GitHub-токенот е успешно зачуван", + "GitLab URL saved successfully" : "GitLab-URL е успешно зачуван", + "GitLab token is valid" : "GitLab-токенот е валиден", + "GitLab token saved successfully" : "GitLab-токенот е успешно зачуван", + "HTTP Method" : "HTTP-метод", + "HTTP method used to send webhook requests" : "HTTP-метод што се користи за испраќање барања за веб-кукичка", + "Headers" : "Заглавја", + "Health" : "Здравје", + "Heartbeat successful - connection kept alive" : "Отчукувањето е успешно - поврзувањето е одржано во живот", + "Helpful" : "Корисно", + "Hide Filters" : "Скриј филтри", + "Hide in forms" : "Скриј во формулари", + "Hide in list view" : "Скриј во приказ на листа", + "High" : "Високо", + "High Confidence" : "Висока доверба", + "Host" : "Хост", + "Hourly" : "На час", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Колку длабоко да се поминуваат вгнездените својства на објект (1-20). Повисоките вредности фаќаат повеќе детали, но ја зголемуваат големината на векторот.", + "How often to check for updates (1-168 hours)" : "Колку често да се проверуваат ажурирања (1-168 часа)", + "How to handle retries for failed webhook deliveries" : "Како да се ракува со повторувања за неуспешни испораки на веб-кукичка", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Ограничувањата на стапка по IP се успешно исчистени", + "Identify properties not in the schema" : "Идентификувај својства што не се во Шемата", + "Immutable" : "Непроменливо", + "Import" : "Увези", + "Import successful" : "Увезувањето е успешно", + "Improved Property" : "Подобрено Својство", + "In use" : "Во употреба", + "Inactive" : "Неактивно", + "Include IDs and names of related objects for better contextual search" : "Вклучи ID и имиња на поврзани објекти за подобро контекстуално пребарување", + "Include related object references" : "Вклучи референци на поврзани објекти", + "Include schema and register metadata" : "Вклучи метаподатоци за Шема и Регистар", + "Initialization failed" : "Иницијализацијата не успеа", + "Initialize Project" : "Иницијализирај проект", + "Inspect Fields" : "Прегледај полиња", + "Inspect Index" : "Прегледај индекс", + "Invalid" : "Невалидно", + "Invalid batch size. Must be between 1 and 5000" : "Невалидна големина на серија. Мора да биде меѓу 1 и 5000", + "Invalid field name provided" : "Обезбедено е невалидно име на поле", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Невалиден maxObjects. Мора да биде 0 (сите) или позитивен број", + "Invalid:" : "Невалидно:", + "Issue" : "Проблем", + "Items per page:" : "Ставки по страница:", + "Just now" : "Штотуку", + "LLM Configuration" : "LLM-конфигурација", + "LLM actions menu" : "Мени за LLM-дејства", + "LLM configuration saved successfully" : "LLM-конфигурацијата е успешно зачувана", + "LLM features disabled" : "LLM-функциите се оневозможени", + "LLM features enabled" : "LLM-функциите се овозможени", + "LLM must be enabled with an embedding provider configured" : "LLM мора да биде овозможено со конфигуриран давател за вградување", + "LLM settings updated successfully" : "LLM-поставките се успешно ажурирани", + "Last" : "Последно", + "Last Triggered" : "Последно активирано", + "Length Range:" : "Опсег на должина:", + "Linear" : "Линеарно", + "Load Advanced Filters" : "Вчитај напредни филтри", + "Load advanced filters with live data from your search index" : "Вчитај напредни филтри со податоци во живо од вашиот индекс за пребарување", + "Load view" : "Вчитај приказ", + "Loading ConfigSets..." : "Се вчитуваат ConfigSets...", + "Loading advanced filters..." : "Се вчитуваат напредни филтри...", + "Loading agents..." : "Се вчитуваат агенти...", + "Loading application details..." : "Се вчитуваат детали за апликација...", + "Loading applications..." : "Се вчитуваат апликации...", + "Loading archived conversations..." : "Се вчитуваат архивирани разговори...", + "Loading audit trails..." : "Се вчитуваат ревизиски траги...", + "Loading collections..." : "Се вчитуваат колекции...", + "Loading configurations..." : "Се вчитуваат конфигурации...", + "Loading conversation..." : "Се вчитува разговор...", + "Loading conversations..." : "Се вчитуваат разговори...", + "Loading deleted items..." : "Се вчитуваат избришани ставки...", + "Loading events..." : "Се вчитуваат настани...", + "Loading filter data..." : "Се вчитуваат податоци за филтрирање...", + "Loading groups..." : "Се вчитуваат групи...", + "Loading log details..." : "Се вчитуваат детали за лог...", + "Loading registers..." : "Се вчитуваат регистри...", + "Loading schemas..." : "Се вчитуваат шеми...", + "Loading search trails..." : "Се вчитуваат траги на пребарување...", + "Loading sources..." : "Се вчитуваат извори...", + "Loading statistics..." : "Се вчитува статистика...", + "Loading users..." : "Се вчитуваат корисници...", + "Loading views..." : "Се вчитуваат прикази...", + "Loading..." : "Се вчитува...", + "Local" : "Локално", + "Local Version" : "Локална верзија", + "Locked" : "Заклучено", + "Locked:" : "Заклучено:", + "Login successful" : "Најавувањето е успешно", + "Logs" : "Логови", + "Low" : "Ниско", + "Low Confidence" : "Ниска доверба", + "Lucene Version:" : "Lucene-верзија:", + "Magic table synchronized successfully" : "Магичната табела е успешно синхронизирана", + "Manage Organisation Roles" : "Управувај со улоги на организација", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Управувајте со SOLR-колекции (складишта на податоци) и доделете ги за објекти и Датотеки.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Управувајте со SOLR ConfigSets (шаблони за конфигурација) за вашите колекции.", + "Manage and configure agents for automated tasks" : "Управувај и конфигурирај агенти за автоматизирани задачи", + "Manage and monitor file text extraction status" : "Управувај и следи го статусот на извлекување текст од Датотеки", + "Manage and restore soft deleted items from your registers" : "Управувај и обнови меко избришани ставки од вашите регистри", + "Manage and view detected entities from files and objects" : "Управувај и прегледај откриени ентитети од Датотеки и објекти", + "Manage document templates and themes" : "Управувај со шаблони и теми за документи", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Управувај со n8n работни текови за автоматизација на OpenRegister. Работните текови ќе се складираат во конфигурираниот проект.", + "Manage webhooks for event-driven integrations" : "Управувај со веб-кукички за интеграции водени од настани", + "Manage your applications and modules" : "Управувајте со вашите апликации и модули", + "Manage your data registers and their configurations" : "Управувајте со вашите регистри на податоци и нивните конфигурации", + "Manage your data schemas and their properties" : "Управувајте со вашите шеми на податоци и нивните својства", + "Manage your data sources and their configurations" : "Управувајте со вашите Извори на податоци и нивните конфигурации", + "Manage your saved search configurations" : "Управувајте со вашите зачувани конфигурации за пребарување", + "Manage your system configurations and settings" : "Управувајте со вашите системски конфигурации и поставки", + "Managed" : "Управувано", + "Max Files (0 = all)" : "Макс. Датотеки (0 = сите)", + "Max Length" : "Макс. должина", + "Max Length:" : "Макс. должина:", + "Max Objects (0 = all)" : "Макс. Објекти (0 = сите)", + "Max Retries" : "Макс. повторувања", + "Max Shards/Node" : "Макс. делови/јазол", + "Max execution time (ms)" : "Макс. време на извршување (ms)", + "Max ms" : "Макс. ms", + "Max result count" : "Макс. број на резултати", + "Max results" : "Макс. резултати", + "Maximum" : "Максимум", + "Maximum Nesting Depth" : "Максимална длабочина на вгнездување", + "Maximum length constraint is missing" : "Недостасува ограничување за максимална должина", + "Maximum length is too restrictive" : "Максималната должина е премногу рестриктивна", + "Maximum number of files to process. Set to 0 to process all files." : "Максимален број на Датотеки за обработка. Поставете на 0 за да се обработат сите Датотеки.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Максимален број на објекти за обработка. Поставете на 0 за да се обработат сите објекти.", + "Maximum number of retry attempts for failed deliveries" : "Максимален број на обиди за повторување за неуспешни испораки", + "Maximum time to wait before committing changes" : "Максимално време за чекање пред предавање на промените", + "Maximum tokens to generate" : "Максимален број на токени за генерирање", + "Maximum value constraint is missing" : "Недостасува ограничување за максимална вредност", + "Maximum value is too restrictive" : "Максималната вредност е премногу рестриктивна", + "Medium" : "Средно", + "Medium Confidence" : "Средна доверба", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Средни барања: Пребарувања со одредено филтрирање или повеќе параметри (на пр., опсези на датуми, специфични регистри/шеми)", + "Members of selected groups can access this view" : "Членовите на избраните групи можат да пристапат до овој приказ", + "Members:" : "Членови:", + "Memory prediction calculated successfully" : "Предвидувањето на меморија е успешно пресметано", + "Message content is required" : "Содржината на пораката е задолжителна", + "Message does not belong to this conversation" : "Пораката не припаѓа на овој разговор", + "Metadata Filters" : "Филтри за метаподатоци", + "Method" : "Метод", + "Min Length" : "Мин. должина", + "Min execution time (ms)" : "Мин. време на извршување (ms)", + "Min ms" : "Мин. ms", + "Min result count" : "Мин. број на резултати", + "Min results" : "Мин. резултати", + "Minimum" : "Минимум", + "Minimum value constraint is missing" : "Недостасува ограничување за минимална вредност", + "Minimum value is too restrictive" : "Минималната вредност е премногу рестриктивна", + "Missing object fields created successfully" : "Полињата на објект што недостасуваа се успешно создадени", + "Mode:" : "Режим:", + "Model" : "Модел", + "Model Name" : "Име на модел", + "Modified" : "Изменето", + "Monthly" : "Месечно", + "More sources (10-20):" : "Повеќе извори (10-20):", + "Most Active Objects" : "Најактивни Објекти", + "Multiple Of" : "Множител на", + "Name" : "Име", + "Names cache warmup completed" : "Загревањето на кешот на имиња е завршено", + "New Collection Name" : "Ново име на колекција", + "New Conversation" : "Нов разговор", + "New Properties" : "Нови својства", + "New Property" : "Ново Својство", + "New Value" : "Нова вредност", + "New users without specific organisation membership will be automatically added to this organisation" : "Нови корисници без специфично членство во организација автоматски ќе бидат додадени во оваа организација", + "Next" : "Следно", + "No" : "Не", + "No ConfigSets found" : "Не се пронајдени ConfigSets", + "No GitHub token configured" : "Не е конфигуриран GitHub-токен", + "No GitHub token provided" : "Не е обезбеден GitHub-токен", + "No GitLab token provided" : "Не е обезбеден GitLab-токен", + "No Organisation" : "Нема организација", + "No active filters" : "Нема активни филтри", + "No activity data available" : "Нема достапни податоци за активност", + "No agents available" : "Нема достапни агенти", + "No agents found" : "Не се пронајдени агенти", + "No app store cache exists yet" : "Сè уште не постои кеш на App store", + "No applications are available." : "Нема достапни апликации.", + "No applications found" : "Не се пронајдени апликации", + "No archived conversations" : "Нема архивирани разговори", + "No audit trail entries found" : "Не се пронајдени записи на ревизиска трага", + "No changes recorded" : "Не се забележани промени", + "No chunks to vectorize" : "Нема делови за векторизирање", + "No collections found" : "Не се пронајдени колекции", + "No configuration data" : "Нема податоци за конфигурација", + "No configurations are available." : "Нема достапни конфигурации.", + "No configurations found" : "Не се пронајдени конфигурации", + "No configurations found for this application." : "Не се пронајдени конфигурации за оваа апликација.", + "No conversations yet. Create a new one to get started!" : "Сè уште нема разговори. Создајте нов за да започнете!", + "No data available" : "Нема достапни податоци", + "No data available for chart" : "Нема достапни податоци за графикон", + "No deleted items found" : "Не се пронајдени избришани ставки", + "No deletion data available" : "Нема достапни податоци за бришење", + "No description found" : "Не е пронајден опис", + "No description provided" : "Не е обезбеден опис", + "No entities detected in this file. Run text extraction first." : "Не се откриени ентитети во оваа Датотека. Прво извршете извлекување текст.", + "No entities found" : "Не се пронајдени ентитети", + "No entities have been detected yet" : "Сè уште не се откриени ентитети", + "No events found" : "Не се пронајдени настани", + "No expired audit trails found to clear" : "Не се пронајдени истечени ревизиски траги за чистење", + "No facetable fields available. Select a register and schema to see available filters." : "Нема достапни полиња за фасетирање. Изберете Регистар и Шема за да ги видите достапните филтри.", + "No files found" : "Не се пронајдени Датотеки", + "No files have been extracted yet" : "Сè уште не се извлечени Датотеки", + "No files to process" : "Нема Датотеки за обработка", + "No files to reindex" : "Нема Датотеки за повторно индексирање", + "No filters are currently active. This will delete ALL audit trail entries!" : "Во моментов не се активни филтри. Ова ќе ги избрише СИТЕ записи на ревизиска трага!", + "No log entries found" : "Не се пронајдени записи во логот", + "No logs are available for this configuration." : "Нема достапни логови за оваа конфигурација.", + "No logs are available for this source." : "Нема достапни логови за овој Извор.", + "No logs found" : "Не се пронајдени логови", + "No mismatched fields found - SOLR schema is properly configured" : "Не се пронајдени несовпаднати полиња - SOLR-Шемата е правилно конфигурирана", + "No objects found" : "Не се пронајдени објекти", + "No objects found to vectorize" : "Не се пронајдени објекти за векторизирање", + "No objects selected for deletion" : "Не се избрани објекти за бришење", + "No objects to delete" : "Нема објекти за бришење", + "No objects were permanently deleted" : "Ниту еден објект не беше трајно избришан", + "No properties found" : "Не се пронајдени својства", + "No properties match your filters." : "Ниту едно Својство не се совпаѓа со вашите филтри.", + "No purge date set" : "Не е поставен датум на прочистување", + "No register data available" : "Нема достапни податоци за Регистар", + "No registers found" : "Не се пронајдени регистри", + "No registers found for this application." : "Не се пронајдени регистри за оваа апликација.", + "No relations found" : "Не се пронајдени релации", + "No request body available" : "Нема достапно тело на барање", + "No response body available" : "Нема достапно тело на одговор", + "No saved views yet. Create one in the Search tab!" : "Сè уште нема зачувани прикази. Создајте еден во картичката Пребарување!", + "No schema data available" : "Нема достапни податоци за Шема", + "No schema selected for exploration" : "Не е избрана Шема за истражување", + "No schemas found" : "Не се пронајдени шеми", + "No schemas found for this application." : "Не се пронајдени шеми за оваа апликација.", + "No search terms data available" : "Нема достапни податоци за поими за пребарување", + "No search trail entries found" : "Не се пронајдени записи на траги на пребарување", + "No sources are available." : "Нема достапни извори.", + "No sources found" : "Не се пронајдени извори", + "No templates found" : "Не се пронајдени шаблони", + "No templates have been created yet" : "Сè уште не се создадени шаблони", + "No valid magic-mapped register+schema combinations found" : "Не се пронајдени валидни магично-мапирани комбинации Регистар+Шема", + "No views found. Create views first before configuring vectorization." : "Не се пронајдени прикази. Прво создајте прикази пред да ја конфигурирате векторизацијата.", + "No views match your search" : "Ниту еден приказ не се совпаѓа со вашето пребарување", + "No webhooks found" : "Не се пронајдени веб-кукички", + "No webhooks have been configured yet" : "Сè уште не се конфигурирани веб-кукички", + "No workflows found in this project. Create workflows in the n8n editor." : "Не се пронајдени работни текови во овој проект. Создајте работни текови во n8n-уредувачот.", + "None" : "Ниедно", + "Not helpful" : "Не е корисно", + "Not in use" : "Не е во употреба", + "Number Constraints" : "Ограничувања на броеви", + "Number of chunks to vectorize in one API call (1-100)." : "Број на делови за векторизирање во еден API-повик (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Број на делови за векторизирање во еден API-повик. Повисоко = побрзо, но повеќе меморија. Препорачано: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Број на објекти за обработка во секоја серија (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Број на објекти за векторизирање во еден API-повик. Повисоко = побрзо, но повеќе меморија. Препорачано: 10-50.", + "Numeric Range:" : "Нумерички опсег:", + "Object" : "Објект", + "Object Collection" : "Колекција на Објекти", + "Object Count Prediction" : "Предвидување на број на Објекти", + "Object ID" : "ID на Објект", + "Object Management" : "Управување со Објекти", + "Object Vectorization" : "Векторизација на Објекти", + "Object Warmup" : "Загревање на Објекти", + "Object collection not configured" : "Колекцијата на Објекти не е конфигурирана", + "Object not found" : "Објектот не е пронајден", + "Object permanently deleted" : "Објектот е трајно избришан", + "Object restored successfully" : "Објектот е успешно обновен", + "Object settings updated successfully" : "Поставките на Објектот се успешно ажурирани", + "Object sources" : "Извори на Објекти", + "Object unlocked successfully" : "Објектот е успешно отклучен", + "Object vectorization configuration saved successfully" : "Конфигурацијата за векторизација на Објекти е успешно зачувана", + "Object vectorized successfully" : "Објектот е успешно векторизиран", + "Object:" : "Објект:", + "Objects" : "Објекти", + "Objects Analyzed" : "Анализирани Објекти", + "Objects Distribution" : "Распределба на Објекти", + "Objects being analyzed" : "Објекти што се анализираат", + "Objects by Register" : "Објекти по Регистар", + "Objects by Schema" : "Објекти по Шема", + "Objects deletion completed successfully" : "Бришењето на Објекти е успешно завршено", + "Objects in schema" : "Објекти во Шема", + "Objects to Process:" : "Објекти за обработка:", + "Objects to be analyzed" : "Објекти што ќе се анализираат", + "Objects to be deleted" : "Објекти што ќе се избришат", + "Objects to be validated" : "Објекти што ќе се потврдат", + "Objects will be serialized as JSON text before vectorization" : "Објектите ќе бидат серијализирани како JSON-текст пред векторизацијата", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Објектите ќе бидат меко избришани (означени како избришани, но задржани во базата на податоци). Тие можат да се обноват подоцна ако е потребно.", + "Old Value" : "Стара вредност", + "Oldest Item (days)" : "Најстара ставка (денови)", + "Ollama Chat Settings" : "Поставки за разговор на Ollama", + "Ollama Embedding Configuration" : "Конфигурација за вградување на Ollama", + "Ollama URL" : "Ollama-URL", + "Only With Changes" : "Само со промени", + "Open Register Objects" : "Open Register Объекти", + "Open n8n Editor" : "Отвори n8n-уредувач", + "OpenAI Chat Settings" : "Поставки за разговор на OpenAI", + "OpenAI Embedding Configuration" : "Конфигурација за вградување на OpenAI", + "OpenRegister Settings" : "Поставки на OpenRegister", + "Optional webhook secret for signature verification" : "Опционална тајна на веб-кукичка за потврда на потпис", + "Order" : "Редослед", + "Organisation" : "Организација", + "Organisation ID" : "ID на организација", + "Organisation Members" : "Членови на организација", + "Organisation Statistics" : "Статистика на организација", + "Organisation created successfully" : "Организацијата е успешно создадена", + "Organisation settings saved successfully" : "Поставките на организацијата се успешно зачувани", + "Organisations" : "Организации", + "Organization" : "Организација", + "Organization ID (Optional)" : "ID на организација (Опционално)", + "Orphaned Items" : "Осиротени ставки", + "Owner" : "Сопственик", + "Owner:" : "Сопственик:", + "Parallel Mode (Faster, more resource intensive)" : "Паралелен режим (Побрз, поинтензивен за ресурси)", + "Parallel:" : "Паралелно:", + "Password" : "Лозинка", + "Password for SOLR authentication (optional)" : "Лозинка за SOLR-автентикација (опционално)", + "Path" : "Патека", + "Path in repository" : "Патека во складиштето", + "Path where the configuration file will be saved in the repository" : "Патека каде што конфигурациската Датотека ќе се зачува во складиштето", + "Path where the register OAS file will be saved in the repository" : "Патека каде што OAS-Датотеката на Регистарот ќе се зачува во складиштето", + "Pattern (regex)" : "Образец (regex)", + "Pattern Issue" : "Проблем со образец", + "Pattern constraint is missing" : "Недостасува ограничување за образец", + "Patterns:" : "Обрасци:", + "Pending" : "На чекање", + "Permanent Delete Mode" : "Режим на трајно бришење", + "Permanently Delete" : "Избриши трајно", + "Permanently delete already soft-deleted objects" : "Трајно избриши веќе меко избришани објекти", + "Permanently delete this conversation?" : "Да се избрише трајно овој разговор?", + "Person" : "Лице", + "Personal Data" : "Лични податоци", + "Phone" : "Телефон", + "Photos" : "Фотографии", + "Please create an agent in the" : "Ве молиме создајте агент во", + "Please select an agent to continue" : "Ве молиме изберете агент за да продолжите", + "Please select which register and schema to use for the new object" : "Ве молиме изберете кој Регистар и Шема да се користат за новиот објект", + "Please try again later." : "Ве молиме обидете се повторно подоцна.", + "Please wait while we fetch your agents." : "Ве молиме почекајте додека ги преземаме вашите агенти.", + "Please wait while we fetch your applications." : "Ве молиме почекајте додека ги преземаме вашите апликации.", + "Please wait while we fetch your configurations." : "Ве молиме почекајте додека ги преземаме вашите конфигурации.", + "Please wait while we fetch your deleted items." : "Ве молиме почекајте додека ги преземаме вашите избришани ставки.", + "Please wait while we fetch your sources." : "Ве молиме почекајте додека ги преземаме вашите извори.", + "Polls" : "Анкети", + "Popular Search Terms" : "Популарни поими за пребарување", + "Port" : "Порта", + "Prerequisites" : "Предуслови", + "Press Enter to send, Shift+Enter for new line" : "Притиснете Enter за испраќање, Shift+Enter за нов ред", + "Previous" : "Претходно", + "Private" : "Приватно", + "Processes chunks in batches with simulated parallelism." : "Обработува делови во серии со симулиран паралелизам.", + "Processes file chunks sequentially (safest)." : "Обработува делови на Датотека секвенцијално (најбезбедно).", + "Processes objects in chunks with simulated parallelism." : "Обработува објекти во делови со симулиран паралелизам.", + "Processes objects sequentially (safest)." : "Обработува објекти секвенцијално (најбезбедно).", + "Processing" : "Обработка", + "Processing Limits" : "Ограничувања на обработка", + "Processing..." : "Се обработува...", + "Project initialized successfully" : "Проектот е успешно иницијализиран", + "Project not found. Please initialize first." : "Проектот не е пронајден. Ве молиме прво иницијализирајте.", + "Properties" : "Својства", + "Property Behaviors" : "Однесувања на Својство", + "Property Configuration:" : "Конфигурација на Својство:", + "Property Filters" : "Филтри на Својство", + "Property Title" : "Наслов на Својство", + "Property Type" : "Тип на Својство", + "Property can be improved" : "Својството може да се подобри", + "Protected" : "Заштитено", + "Provider" : "Давател", + "Provider is required for testing" : "Давателот е задолжителен за тестирање", + "Public" : "Јавно", + "Public View" : "Јавен приказ", + "Public views can be accessed by anyone in the system" : "До јавните прикази може да пристапи секој во системот", + "Publish" : "Објави", + "Publish Objects" : "Објави Објекти", + "Publish all objects in this schema" : "Објави ги сите објекти во оваа Шема", + "Published" : "Објавено", + "Published:" : "Објавено:", + "Publishing..." : "Се објавува...", + "Purge" : "Прочисти", + "Purge Date" : "Датум на прочистување", + "Query Complexity Distribution" : "Распределба на сложеност на барања", + "Query parameter is required" : "Параметарот за барање е задолжителен", + "RAG" : "RAG", + "RAG Configuration" : "RAG-конфигурација", + "Rate limits cleared successfully" : "Ограничувањата на стапка се успешно исчистени", + "Raw Changes Data" : "Сурови податоци за промени", + "Re-vectorize on object update" : "Повторно векторизирај при ажурирање на објект", + "Read" : "Читај", + "Recommendations:" : "Препораки:", + "Recommended Type:" : "Препорачан тип:", + "Recommended: 5 sources" : "Препорачано: 5 извори", + "Refresh" : "Освежи", + "Refresh Data" : "Освежи податоци", + "Refresh Stats" : "Освежи статистика", + "Refresh Workflows" : "Освежи работни текови", + "Refresh dashboard" : "Освежи контролна табла", + "Refresh database info" : "Освежи информации за база на податоци", + "Register" : "Регистар", + "Register ID" : "ID на Регистар", + "Register Statistics" : "Статистика на Регистар", + "Register Totals" : "Вкупно за Регистар", + "Register depublished successfully" : "Регистарот е успешно одобјавен", + "Register not found" : "Регистарот не е пронајден", + "Register objects deletion completed successfully" : "Бришењето на објектите на Регистарот е успешно завршено", + "Register published successfully" : "Регистарот е успешно објавен", + "Register/Schema Usage" : "Употреба на Регистар/Шема", + "Registers" : "Регистри", + "Reindex" : "Повторно индексирај", + "Reindex completed" : "Повторното индексирање е завршено", + "Reindex completed successfully" : "Повторното индексирање е успешно завршено", + "Reindex failed" : "Повторното индексирање не успеа", + "Relations" : "Релации", + "Remote Version" : "Оддалечена верзија", + "Remove" : "Отстрани", + "Remove filter" : "Отстрани филтер", + "Remove from favorites" : "Отстрани од омилени", + "Remove group" : "Отстрани група", + "Removed" : "Отстрането", + "Removed from favorites" : "Отстрането од омилени", + "Rename Conversation" : "Преименувај разговор", + "Rename conversation" : "Преименувај разговор", + "Replicas" : "Реплики", + "Repository" : "Складиште", + "Request Body" : "Тело на барање", + "Request Data" : "Податоци за барање", + "Request Details" : "Детали за барање", + "Request body must be JSON with a \"query\" field" : "Телото на барањето мора да биде JSON со поле „query“", + "Request timeout in seconds" : "Истек на време за барање во секунди", + "Required field" : "Задолжително поле", + "Required status is inconsistent" : "Задолжителниот статус е недоследен", + "Rerun Search" : "Повторно изврши пребарување", + "Reset Changes" : "Ресетирај промени", + "Reset Filters" : "Ресетирај филтри", + "Response Body" : "Тело на одговор", + "Response Details" : "Детали за одговор", + "Response Time:" : "Време на одговор:", + "Restore" : "Обнови", + "Restore conversation" : "Обнови разговор", + "Restore or permanently delete items" : "Обнови или трајно избриши ставки", + "Result Count Range" : "Опсег на број на резултати", + "Results" : "Резултати", + "Retrieve all objects for this schema" : "Преземи ги сите објекти за оваа Шема", + "Retry" : "Повтори", + "Retry Failed Extractions" : "Повтори неуспешни извлекувања", + "Retry Policy" : "Политика на повторување", + "Retry completed" : "Повторувањето е завршено", + "Retrying..." : "Се повторува...", + "Risk Level" : "Ниво на ризик", + "Roles updated successfully" : "Улогите се успешно ажурирани", + "SOLR Connection Settings" : "Поставки за SOLR-поврзување", + "SOLR Version:" : "SOLR-верзија:", + "SOLR actions menu" : "Мени за SOLR-дејства", + "SOLR base path (usually /solr)" : "Основна патека на SOLR (обично /solr)", + "SOLR is disabled" : "SOLR е оневозможено", + "SOLR is not available or not configured" : "SOLR не е достапно или не е конфигурирано", + "SOLR logging disabled" : "SOLR-евидентирањето е оневозможено", + "SOLR logging enabled" : "SOLR-евидентирањето е овозможено", + "SOLR search disabled" : "SOLR-пребарувањето е оневозможено", + "SOLR search enabled" : "SOLR-пребарувањето е овозможено", + "SOLR server hostname or IP address" : "Име на хост или IP-адреса на SOLR-сервер", + "SOLR server port number (optional, defaults to 8983)" : "Број на порта на SOLR-сервер (опционално, стандардно 8983)", + "SOLR setup completed successfully" : "Поставувањето на SOLR е успешно завршено", + "SOLR setup failed" : "Поставувањето на SOLR не успеа", + "SOLR setup failed - check logs" : "Поставувањето на SOLR не успеа - проверете ги логовите", + "Sample Values:" : "Примерни вредности:", + "Save" : "Зачувај", + "Save Configuration" : "Зачувај конфигурација", + "Save Connection Settings" : "Зачувај поставки за поврзување", + "Save Roles" : "Зачувај улоги", + "Save Settings" : "Зачувај поставки", + "Save current search as view" : "Зачувај го тековното пребарување како приказ", + "Saved Views" : "Зачувани прикази", + "Saving..." : "Се зачувува...", + "Schema" : "Шема", + "Schema ID" : "ID на Шема", + "Schema Name" : "Име на Шема", + "Schema Statistics" : "Статистика на Шема", + "Schema depublished successfully" : "Шемата е успешно одобјавена", + "Schema objects deletion completed successfully" : "Бришењето на објектите на Шемата е успешно завршено", + "Schema published successfully" : "Шемата е успешно објавена", + "Schema successfully created" : "Шемата е успешно создадена", + "Schema successfully updated" : "Шемата е успешно ажурирана", + "Schemas" : "Шеми", + "Scheme" : "Шема", + "Search / Views" : "Пребарување / Прикази", + "Search Activity" : "Активност на пребарување", + "Search Analytics" : "Аналитика на пребарување", + "Search Entities" : "Пребарај ентитети", + "Search Files" : "Пребарај Датотеки", + "Search GitHub" : "Пребарај GitHub", + "Search GitLab" : "Пребарај GitLab", + "Search Method" : "Метод на пребарување", + "Search Term" : "Поим за пребарување", + "Search Trail Details" : "Детали за трага на пребарување", + "Search Trail Management" : "Управување со траги на пребарување", + "Search Trail Statistics" : "Статистика на траги на пребарување", + "Search Trails" : "Траги на пребарување", + "Search Views" : "Пребарај прикази", + "Search Webhooks" : "Пребарај веб-кукички", + "Search by file name or path" : "Пребарај по име или патека на Датотека", + "Search by name or URL" : "Пребарај по име или URL", + "Search by value" : "Пребарај по вредност", + "Search in Files" : "Пребарај во Датотеки", + "Search in Objects" : "Пребарај во Објекти", + "Search property names..." : "Пребарај имиња на својства...", + "Search trail deletion not implemented yet" : "Бришењето на траги на пребарување сè уште не е имплементирано", + "Search views..." : "Пребарај прикази...", + "Secret" : "Тајна", + "Secret key for HMAC signature generation (optional)" : "Таен клуч за генерирање HMAC-потпис (опционално)", + "Select AI Agent" : "Изберете ВИ-агент", + "Select All" : "Избери ги сите", + "Select ConfigSet" : "Изберете ConfigSet", + "Select File Types to Vectorize:" : "Изберете типови на Датотеки за векторизирање:", + "Select HTTP method" : "Изберете HTTP-метод", + "Select Register and Schema" : "Изберете Регистар и Шема", + "Select Views to Vectorize:" : "Изберете прикази за векторизирање:", + "Select a Nextcloud group to add" : "Изберете Nextcloud-група за додавање", + "Select a branch" : "Изберете гранка", + "Select a model or type a custom model name" : "Изберете модел или внесете прилагодено име на модел", + "Select a repository" : "Изберете складиште", + "Select a repository you have write access to" : "Изберете складиште за кое имате пристап за запишување", + "Select an AI Agent" : "Изберете ВИ-агент", + "Select an AI agent to begin chatting with your data." : "Изберете ВИ-агент за да започнете разговор со вашите податоци.", + "Select backend" : "Изберете заднински дел", + "Select chat model" : "Изберете модел за разговор", + "Select collection for files" : "Изберете колекција за Датотеки", + "Select collection for objects" : "Изберете колекција за објекти", + "Select default organisation" : "Изберете стандардна организација", + "Select event to listen to..." : "Изберете настан за слушање...", + "Select model" : "Изберете модел", + "Select options..." : "Изберете опции...", + "Select period" : "Изберете период", + "Select property to send as payload" : "Изберете Својство за испраќање како носивост", + "Select provider" : "Изберете давател", + "Select registers and schemas to save a view" : "Изберете регистри и шеми за да зачувате приказ", + "Select retry policy" : "Изберете политика на повторување", + "Select the branch to publish to" : "Изберете ја гранката на која ќе се објавува", + "Select the event this webhook should listen to" : "Изберете го настанот што оваа веб-кукичка треба да го слуша", + "Select views to vectorize:" : "Изберете прикази за векторизирање:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Изберете кои Nextcloud-групи се достапни за оваа организација. Корисниците во овие групи ќе имаат пристап до ресурсите на организацијата.", + "Select which data views the AI can search" : "Изберете кои прикази на податоци може да ги пребарува ВИ", + "Select which property from the event should be used as the webhook payload data" : "Изберете кое Својство од настанот треба да се користи како податоци за носивост на веб-кукичката", + "Select which tools the AI can use to perform actions" : "Изберете кои алатки може да ги користи ВИ за извршување дејства", + "Selected" : "Избрано", + "Selected Groups" : "Избрани групи", + "Selected audit trails deleted successfully" : "Избраните ревизиски траги се успешно избришани", + "Selected search trails deleted successfully" : "Избраните траги на пребарување се успешно избришани", + "Selected users can access this view" : "Избраните корисници можат да пристапат до овој приказ", + "Send additional feedback" : "Испрати дополнителни повратни информации", + "Send as CloudEvent" : "Испрати како CloudEvent", + "Sensitive PII" : "Чувствителни лични идентификациски информации", + "Serial Mode (Safer, slower)" : "Сериски режим (Побезбеден, побавен)", + "Serial:" : "Сериски:", + "Server Configuration" : "Конфигурација на сервер", + "Server Information" : "Информации за сервер", + "Settings" : "Поставки", + "Shards" : "Делови", + "Share" : "Сподели", + "Share with Groups" : "Сподели со групи", + "Share with Users" : "Сподели со корисници", + "Show Filters" : "Прикажи филтри", + "Show less" : "Прикажи помалку", + "Show only entries with changes" : "Прикажи само записи со промени", + "Simple" : "Едноставно", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Едноставни барања: Основни текстуални пребарувања со минимални параметри (на пр., еден поим за пребарување, без напредни филтри)", + "Size" : "Големина", + "Slug" : "Slug", + "Soft Delete Mode" : "Режим на меко бришење", + "Soft Deleted Items" : "Меко избришани ставки", + "SolrCloud mode enabled" : "Режимот SolrCloud е овозможен", + "Source" : "Извор", + "Sources" : "Извори", + "Sources:" : "Извори:", + "Standalone SOLR mode" : "Самостоен SOLR-режим", + "Start Conversation" : "Започни разговор", + "Start Vectorization" : "Започни векторизација", + "Start a conversation" : "Започни разговор", + "Starting names cache warmup..." : "Се започнува загревање на кешот на имиња...", + "Starting..." : "Се започнува...", + "Statistics" : "Статистика", + "Stats calculation not yet implemented" : "Пресметувањето на статистика сè уште не е имплементирано", + "Status" : "Статус", + "Status Code" : "Код на статус", + "String Constraints" : "Ограничувања на низа", + "Success" : "Успех", + "Success Rate" : "Стапка на успех", + "Success Status" : "Статус на успех", + "Successful" : "Успешно", + "Successfully joined organisation" : "Успешно се приклучивте на организацијата", + "Sync Table" : "Синхронизирај табела", + "Synchronization Settings" : "Поставки за синхронизација", + "System Default" : "Системски стандард", + "Technical Description" : "Технички опис", + "Technical description for developers and administrators" : "Технички опис за програмери и администратори", + "Temperature" : "Температура", + "Templates" : "Шаблони", + "Test" : "Тестирај", + "Test Chat" : "Тестирај разговор", + "Test Connection" : "Тестирај поврзување", + "Test Embedding" : "Тестирај вградување", + "Test endpoint executed successfully" : "Тестната крајна точка е успешно извршена", + "Test webhook delivered successfully" : "Тестната веб-кукичка е успешно испорачана", + "Test webhook delivery failed" : "Испораката на тестната веб-кукичка не успеа", + "Test webhook sent successfully" : "Тестната веб-кукичка е успешно испратена", + "Testing..." : "Се тестира...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Текстуалните делови се генерираат при извлекување на Датотека и се складираат во базата на податоци. Векторизацијата ги чита овие однапред поделени Датотеки и ги претвора во вградувања.", + "Text extracted successfully" : "Текстот е успешно извлечен", + "Text extraction disabled" : "Извлекувањето текст е оневозможено", + "The URL where webhook events will be sent" : "URL каде што ќе се испраќаат настаните на веб-кукичката", + "The requested conversation does not exist" : "Бараниот разговор не постои", + "The saved view and all its search configuration" : "Зачуваниот приказ и сета негова конфигурација за пребарување", + "There are no audit trail entries matching your current filters." : "Нема записи на ревизиска трага што се совпаѓаат со вашите тековни филтри.", + "There are no deleted items matching your current filters." : "Нема избришани ставки што се совпаѓаат со вашите тековни филтри.", + "There are no search trail entries matching your current filters." : "Нема записи на траги на пребарување што се совпаѓаат со вашите тековни филтри.", + "There are no webhook log entries matching your filters." : "Нема записи во логот на веб-кукичка што се совпаѓаат со вашите филтри.", + "This action cannot be undone." : "Ова дејство не може да се отповика.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Ова дејство не може да се отповика. Осигурете се дека ниту една колекција не го користи овој ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Овој агент нема конфигурабилни прикази или алатки, но сепак можете да ги прилагодите RAG-поставките подолу.", + "This analysis may take some time" : "Оваа анализа може да потрае одредено време", + "This audit trail entry does not contain any change information." : "Овој запис на ревизиска трага не содржи никакви информации за промена.", + "This configuration has no data defined." : "Оваа конфигурација нема дефинирани податоци.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Оваа крајна точка е застарена. Користете крајни точки засновани на делови наместо неа.", + "This entity has no relations to objects or files" : "Овој ентитет нема релации со објекти или Датотеки", + "This is a test webhook from OpenRegister" : "Ова е тестна веб-кукичка од OpenRegister", + "This schema must use Magic Table configuration to sync" : "Оваа Шема мора да користи конфигурација со Magic Table за синхронизација", + "This source has no associated registers." : "Овој Извор нема поврзани регистри.", + "This will copy the _default ConfigSet with the new name" : "Ова ќе го копира _default ConfigSet со новото име", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Ова ќе ги избрише трајно СИТЕ вградувања (вектори) од базата на податоци. Ќе треба повторно да ги векторизирате сите објекти и Датотеки. Ова дејство не може да се отповика.\\n\\nДали сте сигурни дека сакате да продолжите?", + "This will permanently delete:" : "Ова ќе избрише трајно:", + "Timeout (seconds)" : "Истек на време (секунди)", + "Timestamp" : "Временски печат", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Совет: Овозможете само прикази на кои им е потребно семантичко пребарување за да ги минимизирате трошоците за вградување. На едноставните табели за пребарување ретко им е потребна векторизација.", + "Title" : "Наслов", + "To Date" : "До датум", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "За да ја прилагодите големината или стратегијата на делови, одете на Конфигурација на Датотека → Ограничувања на обработка.", + "To change the embedding provider or model, go to LLM Configuration." : "За да го промените давателот или моделот за вградување, одете на LLM-конфигурација.", + "To date" : "До датум", + "Toggle search sidebar" : "Вклучи/исклучи странична лента за пребарување", + "Tools" : "Алатки", + "Top Deleters" : "Најголеми бришачи", + "Total" : "Вкупно", + "Total Audit Trails" : "Вкупно ревизиски траги", + "Total Chunks Available:" : "Вкупно достапни делови:", + "Total Deleted Items" : "Вкупно избришани ставки", + "Total Members" : "Вкупно членови", + "Total Objects" : "Вкупно Објекти", + "Total Objects in Database:" : "Вкупно Објекти во базата на податоци:", + "Total Organisations" : "Вкупно организации", + "Total Results" : "Вкупно резултати", + "Total Searches" : "Вкупно пребарувања", + "Total Size" : "Вкупна големина", + "Total Storage" : "Вкупно складирање", + "Total:" : "Вкупно:", + "Totals" : "Вкупно", + "Type" : "Тип", + "Type Issue" : "Проблем со тип", + "Type Variations:" : "Варијации на тип:", + "Type must be \"positive\" or \"negative\"" : "Типот мора да биде „positive“ или „negative“", + "Type to search for groups" : "Внесете за да пребарувате групи", + "Type to search for users" : "Внесете за да пребарувате корисници", + "Type to search groups..." : "Внесете за да пребарувате групи...", + "Type to search users..." : "Внесете за да пребарувате корисници...", + "URL" : "URL", + "URL where Ollama is running" : "URL каде што работи Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Непроменето", + "Unique Search Terms" : "Уникатни поими за пребарување", + "Unique Terms" : "Уникатни поими", + "Unknown" : "Непознато", + "Unknown Register" : "Непознат Регистар", + "Unknown Schema" : "Непозната Шема", + "Untitled View" : "Приказ без наслов", + "Update" : "Ажурирај", + "Update Operations" : "Операции на ажурирање", + "Update configuration: ..." : "Ажурирај конфигурација: ...", + "Update register OAS: ..." : "Ажурирај OAS на Регистар: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Ажурирај ги векторите кога податоците за објект се менуваат (препорачано за точно пребарување)", + "Updated" : "Ажурирано", + "Updated At" : "Ажурирано на", + "Updated:" : "Ажурирано:", + "Uptime:" : "Време на работа:", + "Usage Count" : "Број на употреба", + "Use SolrCloud with Zookeeper for distributed search" : "Користи SolrCloud со Zookeeper за дистрибуирано пребарување", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Користете филтри за да ги стесните записите на ревизиска трага по Регистар, Шема, тип на дејство, корисник, опсег на датуми или ID на објект.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Користете филтри за да ги стесните избришаните ставки по Регистар, Шема, датум на бришење или корисник што ги избришал.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Користете филтри за да ги стесните записите на траги на пребарување по Регистар, Шема, статус на успех, корисник, опсег на датуми, поими за пребарување или метрики на перформанси.", + "Used By Collections" : "Користено од колекции", + "User" : "Корисник", + "User Agent Statistics" : "Статистика на кориснички агент", + "User Description" : "Опис на корисник", + "User rate limits cleared successfully" : "Ограничувањата на стапка по корисник се успешно исчистени", + "User removed successfully" : "Корисникот е успешно отстранет", + "User-friendly description shown in forms and help text" : "Опис лесен за корисникот прикажан во формулари и текст за помош", + "Username" : "Корисничко име", + "Username for SOLR authentication (optional)" : "Корисничко име за SOLR-автентикација (опционално)", + "Users" : "Корисници", + "Using Pre-Generated Chunks" : "Користење на однапред генерирани делови", + "Validate" : "Потврди", + "Validate Objects" : "Потврди Објекти", + "Validation completed successfully" : "Потврдувањето е успешно завршено", + "Validation failed" : "Потврдувањето не успеа", + "Value" : "Вредност", + "Vector Dimensions" : "Векторски димензии", + "Vector Search Backend" : "Заднински дел за векторско пребарување", + "Vector field: _embedding_" : "Векторско поле: _embedding_", + "Vectorization Triggers" : "Активирачи на векторизација", + "Vectorize All Files" : "Векторизирај ги сите Датотеки", + "Vectorize All Objects" : "Векторизирај ги сите Објекти", + "Vectorize all file types" : "Векторизирај ги сите типови на Датотеки", + "Vectorize all views" : "Векторизирај ги сите прикази", + "Vectorize on object creation" : "Векторизирај при создавање на објект", + "Vectors will be stored in your existing object and file collections" : "Векторите ќе се складираат во вашите постоечки колекции на објекти и Датотеки", + "Version" : "Верзија", + "Very High" : "Многу високо", + "View API Docs" : "Прегледај API-документи", + "View Changes" : "Прегледај промени", + "View Details" : "Прегледај детали", + "View Error" : "Прегледај грешка", + "View File" : "Прегледај Датотека", + "View Full Details" : "Прегледај целосни детали", + "View Logs" : "Прегледај логови", + "View Name" : "Име на приказ", + "View Object" : "Прегледај Објект", + "View Parameters" : "Прегледај параметри", + "View Selection" : "Избор на приказ", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Прегледај и анализирај логови на траги на пребарување со напредни способности за филтрирање и аналитика", + "View and analyze system audit trails with advanced filtering capabilities" : "Прегледај и анализирај системски ревизиски траги со напредни способности за филтрирање", + "View deleted successfully" : "Приказот е успешно избришан", + "View entity details and manage relations" : "Прегледај детали за ентитет и управувај со релации", + "View name" : "Име на приказ", + "View name is required" : "Името на приказ е задолжително", + "View saved successfully!" : "Приказот е успешно зачуван!", + "View search analytics and manage search logs" : "Прегледај аналитика на пребарување и управувај со логови на пребарување", + "View successfully deleted" : "Приказот е успешно избришан", + "View successfully updated" : "Приказот е успешно ажуриран", + "View updated successfully!" : "Приказот е успешно ажуриран!", + "View webhook delivery logs and filter by webhook" : "Прегледај логови на испорака на веб-кукичка и филтрирај по веб-кукичка", + "Views" : "Прикази", + "Visible to users" : "Видливо за корисници", + "Wait for Response" : "Чекај одговор", + "Wait for webhook response before continuing (required for request/response flows)" : "Чекај одговор на веб-кукичка пред продолжување (потребно за текови барање/одговор)", + "Warmup Names Cache" : "Загреј кеш на имиња", + "Warning" : "Предупредување", + "Warning:" : "Предупредување:", + "Webhook" : "Веб-кукичка", + "Webhook Log Details" : "Детали за лог на веб-кукичка", + "Webhook Logs" : "Логови на веб-кукичка", + "Webhook created successfully" : "Веб-кукичката е успешно создадена", + "Webhook deleted" : "Веб-кукичката е избришана", + "Webhook retry delivered successfully" : "Повторувањето на веб-кукичката е успешно испорачано", + "Webhook retry delivery failed" : "Испораката на повторувањето на веб-кукичката не успеа", + "Webhook updated" : "Веб-кукичката е ажурирана", + "Webhook updated successfully" : "Веб-кукичката е успешно ажурирана", + "Webhooks" : "Веб-кукички", + "Weekly" : "Неделно", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Завиткај ја носивоста на веб-кукичката во CloudEvents-формат за подобра интероперабилност", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Да", + "You" : "Вие", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Можете да создадете нови ConfigSets врз основа на шаблонот _default или да прикачите прилагодени директно на вашиот SOLR-сервер.", + "You do not have access to this conversation" : "Немате пристап до овој разговор", + "You do not have permission to delete this conversation" : "Немате дозвола да го избришете овој разговор", + "You do not have permission to modify this conversation" : "Немате дозвола да го измените овој разговор", + "You do not have permission to restore this conversation" : "Немате дозвола да го обновите овој разговор", + "You must be logged in to favorite views" : "Мора да бидете најавени за да означувате прикази како омилени", + "You need an AI agent to start a conversation." : "Ви треба ВИ-агент за да започнете разговор.", + "Your Fireworks AI API key. Get one at" : "Вашиот API-клуч за Fireworks AI. Добијте го на", + "Your OpenAI API key. Get one at" : "Вашиот API-клуч за OpenAI. Добијте го на", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Вашите повратни информации се забележани. Опционално, можете да обезбедите дополнителни детали тука...", + "Zookeeper Hosts" : "Zookeeper-хостови", + "Zookeeper Port" : "Zookeeper-порта", + "Zookeeper Settings (SolrCloud)" : "Zookeeper-поставки (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Низа за поврзување на Zookeeper за SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Број на порта на Zookeeper (опционално, стандардно 2181)", + "chunk" : "дел", + "chunks" : "делови", + "chunks processed" : "обработени делови", + "configuration(s)" : "конфигурација(и)", + "e.g., lib/Settings/config.json" : "на пр., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "на пр., lib/Settings/register.json", + "failed" : "неуспешно", + "file" : "датотека", + "files" : "датотеки", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "членови", + "menu or contact someone with permission to create agents." : "менито или контактирајте некој со дозвола за создавање агенти.", + "messages" : "пораки", + "n8n URL and API key are required" : "URL и API-клуч за n8n се задолжителни", + "n8n configuration saved successfully" : "n8n-конфигурацијата е успешно зачувана", + "n8n connection not configured" : "n8n-поврзувањето не е конфигурирано", + "n8n connection successful" : "n8n-поврзувањето е успешно", + "n8n connection test successful" : "Тестот на n8n-поврзување е успешен", + "n8n integration disabled" : "n8n-интеграцијата е оневозможена", + "n8n integration enabled" : "n8n-интеграцијата е овозможена", + "n8n project initialized successfully" : "n8n-проектот е успешно иницијализиран", + "n8n settings saved successfully" : "n8n-поставките се успешно зачувани", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "објекти", + "objects processed" : "обработени објекти", + "or pick a range" : "или изберете опсег", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "обезбедува добра рамнотежа помеѓу брзината и точноста.", + "register(s)" : "регистар(и)", + "register(s) selected" : "регистар(и) избрани", + "required" : "задолжително", + "results" : "резултати", + "schema(s)" : "шема(и)", + "schema(s) selected" : "шема(и) избрани", + "sk-..." : "sk-...", + "this application" : "оваа апликација", + "to" : "до", + "vectorized" : "векторизирано", + "views selected" : "прикази избрани", + "ℹ️ Current Configuration" : "ℹ️ Тековна конфигурација", + "⚡ Batch Processing" : "⚡ Сериска обработка", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Сеопфатен контекст ✓ Помала веројатност да се пропуштат детали ✗ Побавни одговори ✗ Може да вклучи помалку релевантни информации", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Побрзи одговори ✓ Пофокусирани одговори ✗ Може да пропушти релевантни информации", + "✨ AI Features" : "✨ ВИ-функции", + "All search trails cleared successfully" : "Сите траги на пребарување се успешно исчистени", + "Multiple search trail deletion not implemented yet" : "Бришењето на повеќе траги на пребарување сè уште не е имплементирано", + "No expired search trails found to clear" : "Не се пронајдени истечени траги на пребарување за чистење", + "Missing message" : "Недостасува порака", + "message content is required" : "содржината на пораката е задолжителна", + "Missing conversation or agentUuid" : "Недостасува разговор или agentUuid", + "Access denied" : "Пристапот е одбиен", + "Conversation not found" : "Разговорот не е пронајден", + "AI service not configured" : "ВИ-услугата не е конфигурирана", + "Failed to process message" : "Не успеа обработката на пораката", + "Missing conversationId" : "Недостасува conversationId", + "conversationId is required" : "conversationId е задолжителен", + "Failed to fetch conversation history" : "Не успеа преземањето на историјата на разговор", + "Failed to clear conversation" : "Не успеа чистењето на разговорот", + "Invalid feedback type" : "Невалиден тип на повратна информација", + "type must be \"positive\" or \"negative\"" : "типот мора да биде „positive“ или „negative“", + "Message not found" : "Пораката не е пронајдена", + "Failed to save feedback" : "Не успеа зачувувањето на повратната информација", + "Failed to get chat statistics" : "Не успеа добивањето на статистиката на разговор", + "Not Found" : "Не е пронајдено", + "Engine not found" : "Машината не е пронајдена", + "Consumer not found" : "Потрошувачот не е пронајден", + "Backend parameter is required" : "Параметарот за заднински дел е задолжителен", + "Failed to get database information: %s" : "Не успеа добивањето на информации за базата на податоци: %s", + "SOLR setup error: %s" : "Грешка при поставување на SOLR: %s", + "Reindex failed: %s" : "Повторното индексирање не успеа: %s", + "Anonymized" : "Анонимизирано", + "Entities detected" : "Откриени ентитети", + "Extract Now" : "Извлечи сега", + "Extracted at" : "Извлечено на", + "Extraction" : "Извлекување", + "Failed to load extraction data" : "Не успеа вчитувањето на податоците за извлекување", + "Failed to load register data" : "Не успеа вчитувањето на податоците за Регистар", + "Failed to retrieve extraction status." : "Не успеа преземањето на статусот на извлекување.", + "Failed to retrieve objects for file." : "Не успеа преземањето на објектите за Датотеката.", + "No extraction data available for this file" : "Нема достапни податоци за извлекување за оваа Датотека", + "No register objects reference this file" : "Ниту еден објект на Регистар не упатува на оваа Датотека", + "Not extracted" : "Не е извлечено", + "Register Objects" : "Објекти на Регистар", + "Risk level" : "Ниво на ризик", + "Text chunks" : "Текстуални делови", + "Unknown error" : "Непозната грешка", + "Very high" : "Многу високо", + "{title} in {register} / {schema}" : "{title} во {register} / {schema}", + "Push Notifications" : "Push-известувања", + "Real-time push notification status via notify_push" : "Статус на push-известувања во реално време преку notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push во реално време не е достапно — апликацијата notify_push не е инсталирана", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Инсталирајте ја апликацијата notify_push од Nextcloud App Store за да овозможите ажурирања во реално време.", + "Open Nextcloud App Store" : "Отвори Nextcloud App Store", + "notify_push is installed but not yet active" : "notify_push е инсталирано, но сè уште не е активно", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push е инсталирано, но OpenRegister сè уште не потврдил успешен push. Активирајте зачувување на објект за да го активирате или проверете ја вашата конфигурација на notify_push.", + "notify_push configuration guide" : "водич за конфигурација на notify_push", + "Realtime push active" : "Push во реално време е активно", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push-известувањата во реално време се активни. Поврзаните клиенти добиваат моментални ажурирања кога објектите се создаваат, ажурираат или бришат.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);" +); diff --git a/l10n/mk.json b/l10n/mk.json new file mode 100644 index 0000000000..77c0f11ad6 --- /dev/null +++ b/l10n/mk.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Потребен Dolphin)", + "(no subject)": "(без предмет)", + "(no title)": "(без наслов)", + "(unnamed)": "(без име)", + "(untitled card)": "(картичка без наслов)", + "({count} selected)": "({count} избрани)", + ", {failed} failed": ", {failed} неуспешни", + "0 = unlimited": "0 = неограничено", + "A deactivation request is pending.": "Барањето за деактивација е на чекање.", + "AI Assistant": "AI асистент", + "AI Chat": "AI разговор", + "AI service not configured": "AI услугата не е конфигурирана", + "API Key": "API клуч", + "API Request Quota (requests/day)": "Квота на API барања (барања/ден)", + "API Token Configuration": "Конфигурација на API токен", + "API Tokens": "API токени", + "API tokens saved successfully": "API токените се зачувани успешно", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG чл. 30 §4: овој извештај ги поврзува секоја активност на обработка со вкупниот број записи во ревизорскиот траг. Ревизорите и Autoriteit Persoonsgegevens го користат ова за надзорен преглед.", + "About ConfigSets": "За ConfigSets", + "Access denied": "Пристапот е одбиен", + "Access organisation dashboard and analytics": "Пристап до контролната табла и аналитика на организацијата", + "Account": "Сметка", + "Action": "Акција", + "Action Distribution": "Распределба на акции", + "Action:": "Акција:", + "Actions": "Акции", + "Active": "Активно", + "Active Collections": "Активни колекции", + "Active Filters": "Активни филтри", + "Active Organisation:": "Активна организација:", + "Active Organisations": "Активни организации", + "Active filters:": "Активни филтри:", + "Active organisation changed successfully": "Активната организација е успешно сменета", + "Active organisation set successfully": "Активната организација е успешно поставена", + "Activities": "Активности", + "Activity": "Активност", + "Activity Period": "Период на активност", + "Add": "Додај", + "Add Attachment": "Додај прилог", + "Add Endpoint": "Додај крајна точка", + "Add Groups": "Додај групи", + "Add Property to '{title}'": "Додај својство на '{title}'", + "Add Register": "Додај регистар", + "Add Schema": "Додај шема", + "Add Source": "Додај извор", + "Add User": "Додај корисник", + "Add User to Organisation": "Додај корисник во организација", + "Add a contact from any of your address books to associate it with this object.": "Додајте контакт од која било адресна книга за да го поврзете со овој објект.", + "Add a file or files": "Додај датотека или датотеки", + "Add card": "Додај картичка", + "Add contact": "Додај контакт", + "Add endpoint": "Додај крајна точка", + "Add oneOf entry": "Додај oneOf запис", + "Add range": "Додај опсег", + "Add schema titles, descriptions, and register information to provide richer context for search": "Додајте наслови на шеми, описи и информации за регистар за побогат контекст при пребарување", + "Add to favorites": "Додај во омилени", + "Added": "Додадено", + "Added to favorites": "Додадено во омилени", + "Additional Information": "Дополнителни информации", + "Additional feedback saved. Thank you!": "Дополнителните повратни информации се зачувани. Благодарам!", + "Addresses": "Адреси", + "Admin": "Администратор", + "Advanced": "Напредно", + "Advanced Filters": "Напредни филтри", + "Advanced Options": "Напредни опции", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Напредните филтри не се достапни при користење на извор од база на податоци. Префрлете на Auto или SOLR Index за опции за филтрирање.", + "After": "По", + "Agent Type": "Тип на агент", + "Agent deleted successfully": "Агентот е успешно избришан", + "Agent successfully created": "Агентот е успешно создаден", + "Agent successfully updated": "Агентот е успешно ажуриран", + "Agents": "Агенти", + "Aggregated across schemas": "Агрегирано низ шеми", + "All": "Сите", + "All Categories": "Сите категории", + "All Collections": "Сите колекции", + "All Confidence Levels": "Сите нивоа на доверба", + "All ConfigSets": "Сите ConfigSets", + "All Files": "Сите датотеки", + "All Levels": "Сите нивоа", + "All Types": "Сите типови", + "All Webhooks": "Сите Webhooks", + "All actions": "Сите акции", + "All audit trails cleared successfully": "Сите ревизорски траги се успешно исчистени", + "All clear": "Сè е јасно", + "All file types": "Сите типови датотеки", + "All registers": "Сите регистри", + "All schemas": "Сите шеми", + "All search trails cleared successfully": "Сите траги на пребарување се успешно исчистени", + "All searches": "Сите пребарувања", + "All users": "Сите корисници", + "All-day events": "Целодневни настани", + "Allowed MIME Types": "Дозволени MIME типови", + "Already linked": "Веќе поврзано", + "An error occurred while clearing audit trails": "Се појави грешка при чистење на ревизорските траги", + "An error occurred while deleting the audit trail": "Се појави грешка при бришење на ревизорскиот траг", + "An error occurred while deleting the object": "Се појави грешка при бришење на објектот", + "An error occurred while deleting the view": "Се појави грешка при бришење на прегледот", + "An error occurred while permanently deleting the objects": "Се појави грешка при трајно бришење на објектите", + "An error occurred while restoring the objects": "Се појави грешка при враќање на објектите", + "An error occurred while saving the organisation": "Се појави грешка при зачувување на организацијата", + "Analysis completed:": "Анализата е завршена:", + "Analysis steps:": "Чекори на анализа:", + "Analytics": "Аналитика", + "Analyze Objects": "Анализирај објекти", + "Analyze Properties": "Анализирај својства", + "Analyze existing properties for improvement opportunities": "Анализирај постоечки својства за можности за подобрување", + "Analyzing...": "Анализирање...", + "Anonymized": "Анонимизирано", + "Anonymous Group": "Анонимна група", + "Any favorites and sharing settings for this view": "Сите омилени и поставки за споделување за овој преглед", + "Any user": "Кој било корисник", + "App ID": "App ID", + "App store cache invalidated successfully": "Кешот на продавницата за апликации е успешно поништен", + "App store cache invalidated: {invalidated}": "Кешот на продавницата за апликации е поништен: {invalidated}", + "Application deleted successfully": "Апликацијата е успешно избришана", + "Application identifier for this configuration (optional)": "Идентификатор на апликација за оваа конфигурација (опционално)", + "Application settings": "Поставки на апликацијата", + "Application successfully created": "Апликацијата е успешно создадена", + "Application successfully updated": "Апликацијата е успешно ажурирана", + "Applications": "Апликации", + "Apply": "Примени", + "Apply Changes": "Примени промени", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Примени улога на сите шеми во овој регистар кои немаат експлицитни отфрлувања на авторизацијата.", + "Approval Progress": "Напредок на одобрување", + "Approve": "Одобри", + "Approved": "Одобрено", + "Archive": "Архива", + "Archive conversation": "Архивирај разговор", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Архивирај ја оваа verwerkingsactiviteit? Редовите на ревизорскиот траг ќе продолжат да се однесуваат на неа.", + "Archived conversations are hidden from your active list": "Архивираните разговори се скриени од вашата активна листа", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Дали сте сигурни дека сакате да ја ИЗБРИШЕТЕ колекцијата \"{name}\"?\n\nОва ќе:\n• Трајно ја избрише колекцијата и сите нејзини податоци\n• Ги отстрани сите индексирани документи\n• Оваа акција не може да се поништи", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Дали сте сигурни дека сакате да ги исчистите старите траги на пребарување? Ова ќе ги избрише записите постари од 30 дена.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Дали сте сигурни дека сакате да ги исчистите сите податоци од колекцијата \"{name}\"?\n\nОва ќе:\n• Ги избрише сите индексирани документи\n• Ја зачува структурата на колекцијата непроменета\n• Оваа акција не може да се поништи", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Дали сте сигурни дека сакате да ја избришете апликацијата \"{name}\"? Оваа акција не може да се поништи.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Дали сте сигурни дека сакате да ги избришете избраните ревизорски траги? Оваа акција не може да се поништи.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Дали сте сигурни дека сакате да ги избришете избраните траги на пребарување? Оваа акција не може да се поништи.", + "Are you sure you want to delete this ConfigSet?": "Дали сте сигурни дека сакате да го избришете овој ConfigSet?", + "Are you sure you want to delete this configset?": "Дали сте сигурни дека сакате да го избришете овој configset?", + "Are you sure you want to leave '{name}'?": "Дали сте сигурни дека сакате да ја напуштите '{name}'?", + "Are you sure you want to permanently delete": "Дали сте сигурни дека сакате трајно да избришете", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Дали сте сигурни дека сакате повторно да ја индексирате колекцијата \"{name}\"?\n\nОва ќе:\n• Го реконструира индексот со сите објекти\n• Потрае неколку минути\n• Може да влијае на перформансите на пребарувањето при реиндексирање", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Дали сте сигурни дека сакате да ја отстраните шемата \"{schema}\" од регистарот \"{register}\"? Оваа акција не може да се поништи.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Дали сте сигурни дека сакате меко да избришете {count} активни објекти за шемата \"{schema}\"?\n\nТие ќе бидат означени како избришани, но можат трајно да се отстранат подоцна.", + "Array Object Configuration:": "Конфигурација на низа на објекти:", + "Articles": "Статии", + "Ask a question...": "Поставете прашање...", + "Ask questions about your data using natural language": "Поставувајте прашања за вашите податоци користејќи природен јазик", + "Assigned collaborative tags": "Доделени соработнички ознаки", + "Assignment notifications": "Известувања за доделување", + "Attachment added to {name}": "Прилогот е додаден на {name}", + "Attempt": "Обид", + "Audit Trail Changes": "Промени на ревизорскиот траг", + "Audit Trail Details": "Детали на ревизорскиот траг", + "Audit Trail Management": "Управување со ревизорскиот траг", + "Audit Trail Statistics": "Статистика на ревизорскиот траг", + "Audit Trails": "Ревизорски траги", + "Audit trail data copied to clipboard": "Податоците на ревизорскиот траг се копирани во клипборд", + "Audit trail deleted successfully": "Ревизорскиот траг е успешно избришан", + "Audit trail successfully deleted": "Ревизорскиот траг е успешно избришан", + "Authentication": "Автентикација", + "Auto (e.g. Y for year, F Y for month)": "Автоматски (на пр. Y за година, F Y за месец)", + "Auto share": "Автоматско споделување", + "Auto-commit disabled": "Автоматскиот commit е оневозможен", + "Auto-commit enabled": "Автоматскиот commit е овозможен", + "Auto-create Default Organisation": "Автоматски создај стандардна организација", + "Auto-retry failed vectorizations": "Автоматски повтори неуспешни векторизации", + "Automatically commit changes to SOLR index": "Автоматски потврди промени во SOLR индексот", + "Automatically connect to the federation network.": "Автоматски поврзи се со федеративната мрежа.", + "Automatically create a default organisation if none exists when the app is initialized": "Автоматски создај стандардна организација ако не постои при иницијализација на апликацијата", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Автоматски генерирај векторски вградувања од текстуални парчиња при прикачување и обработка на датотеки", + "Automatically generate vector embeddings when objects are created or updated": "Автоматски генерирај векторски вградувања при создавање или ажурирање на објекти", + "Automatically retry failed vectorization attempts (max 3 retries)": "Автоматски повтори неуспешни обиди за векторизација (макс. 3 повторувања)", + "Automatically update catalogues.": "Автоматски ажурирај каталози.", + "Available Filters": "Достапни филтри", + "Available Workflows": "Достапни работни текови", + "Available placeholders:": "Достапни заменски вредности:", + "Avatar": "Аватар", + "Avatar changes are not supported by your authentication provider.": "Промените на аватарот не се поддржани од вашиот давател на автентикација.", + "Avatar removed": "Аватарот е отстранет", + "Avatar updated successfully": "Аватарот е успешно ажуриран", + "Avg Execution Time": "Просечно време на извршување", + "Avg Members/Org": "Просечно членови/Орг", + "Avg Object Views/Session": "Просечни прегледи на објекти/Сесија", + "Avg Response Time": "Просечно време на одговор", + "Avg Results/Search": "Просечни резултати/Пребарување", + "Avg Searches/Session": "Просечни пребарувања/Сесија", + "Back": "Назад", + "Back to Entities": "Назад кон ентитети", + "Back to Registers": "Назад кон регистри", + "Back to Webhooks": "Назад кон Webhooks", + "Back to applications": "Назад кон апликации", + "Back to entities": "Назад кон ентитети", + "Backend parameter is required": "Параметарот за backend е задолжителен", + "Backend updated successfully. Please reload the application.": "Backend-от е успешно ажуриран. Ве молиме рестартирајте ја апликацијата.", + "Bandwidth Quota (MB/month)": "Квота на пропусност (MB/месец)", + "Base URL (Optional)": "Основна URL (Опционално)", + "Basic Information": "Основни информации", + "Batch Size": "Големина на серија", + "Batch extraction completed": "Серијското извлекување е завршено", + "Before": "Пред", + "Before object vectorization can work:": "Пред да може да работи векторизацијата на објекти:", + "Behavior": "Однесување", + "Behavior Issue": "Проблем со однесување", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 траење, на пр. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob складиштето е повлечено. Сите објекти сега користат магични табели.", + "Bookmarks": "Обележувачи", + "Branch": "Гранка", + "Bulk Role Assignment: {title}": "Масовно доделување улоги: {title}", + "Bulk delete operation completed successfully": "Операцијата за масовно бришење е успешно завршена", + "Bulk save operation completed successfully": "Операцијата за масовно зачувување е успешно завршена", + "Business Data": "Деловни податоци", + "CSV": "CSV", + "Cache cleared successfully": "Кешот е успешно исчистен", + "Cache grew from {old} to {new} entries.": "Кешот порасна од {old} на {new} записи.", + "Cache shrunk from {old} to {new} entries.": "Кешот се намали од {old} на {new} записи.", + "Cache stayed the same at {size} entries.": "Кешот остана ист со {size} записи.", + "Calculate Sizes": "Пресметај големини", + "Calendar": "Календар", + "Calendar Provider Configuration": "Конфигурација на давател на календар", + "Calendar integration is not available": "Интеграцијата со календарот не е достапна", + "Calendar name": "Име на календар", + "Cancel": "Откажи", + "Cancel deactivation request": "Откажи барање за деактивација", + "Cannot delete: objects are still attached": "Не може да се избрише: објекти сè уште се прикачени", + "Cannot edit: This register is managed by external configuration {title}": "Не може да се уредува: Овој регистар се управува со надворешна конфигурација {title}", + "Cannot remove schema with existing objects ({count} objects)": "Не може да се отстрани шема со постоечки објекти ({count} објекти)", + "Cannot remove schema {schema} because it contains {count} objects": "Не може да се отстрани шемата {schema} бидејќи содржи {count} објекти", + "Cards": "Картички", + "Cascade delete": "Каскадно бришење", + "Categories of data subjects (one per line)": "Категории на субјекти на податоци (по еден на ред)", + "Categories of personal data (one per line)": "Категории на лични податоци (по еден на ред)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Категорија", + "Change Type": "Тип на промена", + "Change password": "Промени лозинка", + "Changes": "Промени", + "Changes copied to clipboard": "Промените се копирани во клипборд", + "Changes for Audit Trail #{id}": "Промени за ревизорскиот траг #{id}", + "Chat Model": "Модел за разговор", + "Chat Provider": "Давател на разговор", + "Chat Provider (RAG)": "Давател на разговор (RAG)", + "Chat Settings": "Поставки за разговор", + "Chat provider connection successful!": "Врската со давателот на разговор е успешна!", + "Chat settings": "Поставки за разговор", + "Chat test failed: {error}": "Тестот на разговор не успеа: {error}", + "Choose a register": "Изберете регистар", + "Choose a schema": "Изберете шема", + "Choose how vector similarity calculations are performed for semantic search": "Изберете како се вршат пресметките на векторска сличност за семантичко пребарување", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Изберете кои типови датотеки да се вклучат во процесот на векторизација. Само датотеки со извлечен текст и парчиња ќе бидат обработени.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Изберете кои прегледи да се вклучат во процесот на векторизација. Оставете празно за да се обработат сите прегледи врз основа на вашата конфигурација.", + "Chunk": "Парче", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Бришењето на парчиња сè уште не е имплементирано. Користете крајни точки базирани на парчиња.", + "Chunks": "Парчиња", + "Chunks to Vectorize:": "Парчиња за векторизација:", + "Cleanup Old Trails": "Исчисти стари траги", + "Cleanup completed": "Чистењето е завршено", + "Cleanup completed successfully. Deleted {count} entries.": "Чистењето е успешно завршено. Избришани {count} записи.", + "Cleanup failed: {error}": "Чистењето не успеа: {error}", + "Clear All Embeddings": "Исчисти сите вградувања", + "Clear Entries": "Исчисти записи", + "Clear Filtered Audit Trails": "Исчисти филтрирани ревизорски траги", + "Clear Filters": "Исчисти филтри", + "Clear Index": "Исчисти индекс", + "Clear Selection": "Исчисти избор", + "Clear all": "Исчисти сè", + "Clear all filters": "Исчисти сите филтри", + "Clear date range": "Исчисти опсег на датум", + "Clear filters": "Исчисти филтри", + "Clearing...": "Чистење...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Кликнете Стартувај скенирање за усогласеност за да пронајдете шеми каде PII е детектиран, но не постои анотација за активност на обработка.", + "Click to subscribe to notifications": "Кликнете за да се претплатите на известувања", + "Click to unsubscribe from notifications": "Кликнете за да се отпретплатите од известувања", + "Close": "Затвори", + "Cluster name": "Име на кластер", + "Code": "Код", + "Code (short readable key, e.g. v-2026-001)": "Код (краток читлив клуч, на пр. v-2026-001)", + "Collapse details": "Собери детали", + "Collection Management": "Управување со колекции", + "Collection Name": "Име на колекција", + "Collection assignments updated successfully": "Доделувањата на колекцијата се успешно ажурирани", + "Collection cleared successfully": "Колекцијата е успешно исчистена", + "Collection copied successfully": "Колекцијата е успешно копирана", + "Collection created successfully": "Колекцијата е успешно создадена", + "Collection deleted successfully": "Колекцијата е успешно избришана", + "Collection used to store and index file metadata and content": "Колекција користена за складирање и индексирање на метаподатоци и содржина на датотеки", + "Collection used to store and index object data": "Колекција користена за складирање и индексирање на податоци за објекти", + "Collections:": "Колекции:", + "Color": "Боја", + "Comment...": "Коментар...", + "Commit Message": "Порака за commit", + "Commit Within (ms)": "Потврди во рок од (ms)", + "Commit message": "Порака за commit", + "Compare current schema with real object data": "Споредете ја тековната шема со реалните податоци за објект", + "Completed": "Завршено", + "Complex": "Сложено", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Сложени барања: Напредни пребарувања со повеќе филтри, оператори и сложени комбинации на параметри", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Сложени барања: напредни пребарувања со повеќе филтри, оператори и сложени комбинации на параметри", + "Compliance": "Усогласеност", + "Confidence Level": "Ниво на доверба", + "Confidence Score": "Оцена на доверба", + "Confidence: {confidence}%": "Доверба: {confidence}%", + "Config must be provided as an object": "Конфигурацијата мора да биде обезбедена како објект", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Управување со ConfigSet", + "ConfigSet Name": "Име на ConfigSet", + "ConfigSet created successfully": "ConfigSet е успешно создаден", + "ConfigSet deleted successfully": "ConfigSet е успешно избришан", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets ја дефинираат шемата и конфигурацијата за вашите SOLR колекции. Тие содржат дефиниции на полиња, анализатори и други поставки за пребарување.", + "Configuration": "Конфигурација", + "Configuration Data": "Податоци за конфигурација", + "Configuration is up to date": "Конфигурацијата е ажурирана", + "Configuration saved": "Конфигурацијата е зачувана", + "Configuration saved successfully.": "Конфигурацијата е успешно зачувана.", + "Configuration type (default, application, etc.)": "Тип на конфигурација (стандарден, апликација, итн.)", + "Configurations": "Конфигурации", + "Configure API tokens for external service integrations": "Конфигурирај API токени за интеграции на надворешни услуги", + "Configure Apache SOLR search engine for advanced search capabilities": "Конфигурирај Apache SOLR пребарувач за напредни можности за пребарување", + "Configure Facets": "Конфигурирај фасети", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Конфигурирај давателе на Голем јазичен модел (LLM) за AI-напојувани функции вклучувајќи семантичко пребарување, вградувања и разговор.", + "Configure SOLR Facets": "Конфигурирај SOLR фасети", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Конфигурирај основни поставки за поврзување за вашиот SOLR сервер вклучувајќи автентикација и мрежни опции. Користете ги посебните дијалози за управување со ConfigSet и колекции за управување со јадра и колекции.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Конфигурирај основни поставки за поврзување за вашиот SOLR сервер вклучувајќи автентикација и мрежни опции. Користете ги посебните дијалози за управување со configset и колекции за управување со јадра и колекции.", + "Configure default organisation and organisation-related settings": "Конфигурирај стандардна организација и поставки поврзани со организацијата", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Конфигурирај како базата на податоци ги конвертира објектите во векторски вградувања за семантичко пребарување. Објектите се директно векторизираат без потреба за извлекување на текст.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Конфигурирај како објектите се конвертираат во текст пред векторизација. Овие поставки влијаат на квалитетот на пребарувањето и контекстот.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Конфигурирај давателе на голем јазичен модел (LLM) за ai-напојувани функции вклучувајќи семантичко пребарување, вградувања и разговор.", + "Configure multi-organization support and tenant isolation": "Конфигурирај поддршка за повеќе организации и изолација на закупец", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Конфигурирај параметри за векторизација на датотеки. Овој процес ќе генерира векторски вградувања за сите извлечени парчиња датотеки.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Конфигурирај параметри за векторизација на објекти. Овој процес ќе генерира векторски вградувања за сите објекти кои одговараат на вашите филтри за преглед.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Конфигурирај ја оваа шема за прикажување на објекти како настани во апликацијата Nextcloud Календар.", + "Configure which types of data to search and how many sources to retrieve": "Конфигурирај кои типови податоци да се пребаруваат и колку извори да се превземат", + "Configuring field mappings...": "Конфигурирање на мапирања на полиња...", + "Confirm Account Deactivation": "Потврди деактивација на сметката", + "Confirm Object Deletion": "Потврди бришење на објект", + "Confirm Object Restoration": "Потврди враќање на објект", + "Confirm Permanent Object Deletion": "Потврди трајно бришење на објект", + "Confirm deactivation": "Потврди деактивација", + "Connecting to SOLR cluster...": "Поврзување со SOLR кластер...", + "Connection Failed": "Врската не успеа", + "Connection Settings": "Поставки за поврзување", + "Connection Successful!": "Врската е успешна!", + "Connection failed": "Врската не успеа", + "Connection protocol": "Протокол за поврзување", + "Connection successful": "Врската е успешна", + "Connection timeout in seconds": "Временско ограничување за поврзување во секунди", + "Constant delay between retries (5 minutes)": "Константно доцнење помеѓу повторувањата (5 минути)", + "Constraint Issue": "Проблем со ограничување", + "Consumer not found": "Потрошувачот не е пронајден", + "Contacts": "Контакти", + "Contacts integration is not available": "Интеграцијата со контактите не е достапна", + "Content Filters": "Филтри за содржина", + "Continue to Properties": "Продолжи кон својства", + "Contracts": "Договори", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Контролирајте кои прегледи на објекти треба да се векторизираат за да се намалат трошоците за API. Векторизирајте само прегледи кои имаат корист од семантичкото пребарување.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Контролирајте кои прегледи и алатки AI може да ги користи во овој разговор. Стандардно, сите можности на агентот се овозможени.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Контролира случајност (0 = детерминистичко, 2 = многу креативно)", + "Conversation ID is required": "ID на разговорот е задолжителен", + "Conversation archived": "Разговорот е архивиран", + "Conversation archived successfully": "Разговорот е успешно архивиран", + "Conversation cleared successfully": "Разговорот е успешно исчистен", + "Conversation deleted": "Разговорот е избришан", + "Conversation not found": "Разговорот не е пронајден", + "Conversation permanently deleted": "Разговорот е трајно избришан", + "Conversation renamed": "Разговорот е преименуван", + "Conversation restored": "Разговорот е вратен", + "Conversation started with {agent}": "Разговорот е започнат со {agent}", + "Conversation title": "Наслов на разговор", + "Conversations": "Разговори", + "Converting {schema} to blob storage...": "Конвертирање на {schema} во blob складиште...", + "Converting {schema} to magic table...": "Конвертирање на {schema} во магична табела...", + "Copied!": "Копирано!", + "Copy": "Копирај", + "Copy Changes": "Копирај промени", + "Copy Collection": "Копирај колекција", + "Copy Data": "Копирај податоци", + "Copy Full Data": "Копирај целосни податоци", + "Copy of {name}": "Копија на {name}", + "Copy to clipboard": "Копирај во клипборд", + "Copying...": "Копирање...", + "Costs": "Трошоци", + "Count": "Број", + "Counting objects...": "Броење објекти...", + "Create": "Создај", + "Create API Token": "Создај API токен", + "Create Agent": "Создај агент", + "Create Application": "Создај апликација", + "Create Collection": "Создај колекција", + "Create ConfigSet": "Создај ConfigSet", + "Create New Collection": "Создај нова колекција", + "Create New ConfigSet": "Создај нов ConfigSet", + "Create Operations": "Операции за создавање", + "Create Organisation": "Создај организација", + "Create Webhook": "Создај Webhook", + "Create a copy of collection:": "Создај копија на колекција:", + "Create a new ConfigSet based on the _default template": "Создај нов ConfigSet врз основа на шаблонот _default", + "Create a new SOLR collection from an existing ConfigSet": "Создај нова SOLR колекција од постоечки ConfigSet", + "Create a new SOLR collection from an existing configset": "Создај нова SOLR колекција од постоечки configset", + "Create a new configset based on the _default template": "Создај нов configset врз основа на шаблонот _default", + "Create a new event or link an existing one from any of your calendars.": "Создајте нов настан или поврзете постоечки од кој било ваш календар.", + "Create another": "Создај уште еден", + "Create copies of": "Создај копии на", + "Create event": "Создај настан", + "Create new token": "Создај нов токен", + "Create or link a Deck card to track work on this object.": "Создајте или поврзете картичка во Deck за следење на работата на овој објект.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Создајте ја првата verwerkingsactiviteit за да почнете да ги означувате редовите на ревизорскиот траг со нивното AVG чл. 30 припишување.", + "Create your first AI agent to get started.": "Создајте го вашиот прв AI агент за да започнете.", + "Created": "Создадено", + "Created:": "Создадено:", + "Creating search collections...": "Создавање колекции за пребарување...", + "Creating...": "Создавање...", + "Current": "Тековен", + "Current Filters": "Тековни филтри", + "Current Type:": "Тековен тип:", + "Current password": "Тековна лозинка", + "Custom API endpoint if using a different region": "Прилагодена API крајна точка ако користите друг регион", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Прилагодени HTTP заглавија (по едно на ред, формат: Header-Name: вредност)", + "Custom HTTP headers (one per line, format: header-name: value)": "Прилагодени HTTP заглавија (по едно на ред, формат: header-name: вредност)", + "Custom Ranges:": "Прилагодени опсези:", + "Custom display title for this facet": "Прилагоден наслов за прикажување за овој фасет", + "Custom pattern:": "Прилагоден шаблон:", + "DELETE COLLECTION": "ИЗБРИШИ КОЛЕКЦИЈА", + "DSAR": "DSAR", + "Daily": "Дневно", + "Dashboard": "Контролна табла", + "Dashboard not found": "Контролната табла не е пронајдена", + "Data": "Податоци", + "Data Source": "Извор на податоци", + "Data Sources": "Извори на податоци", + "Data Views": "Прегледи на податоци", + "Data sources": "Извори на податоци", + "Data type does not match observed values": "Типот на податоци не одговара на набљудуваните вредности", + "Data-subject access request": "Барање за пристап на субјект на податоци", + "Database URL": "URL на база на податоци", + "Database information refreshed": "Информациите за базата на податоци се освежени", + "Date Faceting:": "Фасетирање по датум:", + "Date Range": "Опсег на датуми", + "Dates": "Датуми", + "Deactivation request cancelled": "Барањето за деактивација е откажано", + "Deactivation request submitted": "Барањето за деактивација е поднесено", + "Deck": "Deck", + "Deck integration is not available": "Интеграцијата со Deck не е достапна", + "Default": "Стандарден", + "Default Object Tenant": "Стандарден закупец на објект", + "Default Organisation": "Стандардна организација", + "Default Owner": "Стандарден сопственик", + "Default User (for cron/background jobs)": "Стандарден корисник (за cron/позадински задачи)", + "Default User Tenant": "Стандарден закупец на корисник", + "Default value": "Стандардна вредност", + "Default value (Email)": "Стандардна вредност (Е-пошта)", + "Default value (Password)": "Стандардна вредност (Лозинка)", + "Default value (Phone number)": "Стандардна вредност (Телефонски број)", + "Default value (Regex)": "Стандардна вредност (Regex)", + "Default: 5": "Стандарден: 5", + "Defaults to current user. Select a different user if needed.": "Стандардно е тековниот корисник. Изберете друг корисник ако е потребно.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Доцнењата се удвојуваат со секој обид (2, 4, 8 минути...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Доцнењата растат линеарно (5, 10, 15 минути...)", + "Delete": "Избриши", + "Delete ({count})": "Избриши ({count})", + "Delete Application": "Избриши апликација", + "Delete Audit Trail": "Избриши ревизорски траг", + "Delete Collection": "Избриши колекција", + "Delete ConfigSet": "Избриши ConfigSet", + "Delete Objects": "Избриши објекти", + "Delete Operations": "Операции за бришење", + "Delete SOLR Collection": "Избриши SOLR колекција", + "Delete View": "Избриши преглед", + "Delete all objects in this schema": "Избриши ги сите објекти во оваа шема", + "Delete permanently": "Избриши трајно", + "Delete this conversation?": "Избриши го овој разговор?", + "Delete view": "Избриши преглед", + "Delete {count} object": "Избриши {count} објект", + "Delete {count} objects": "Избриши {count} објекти", + "Deleted": "Избришано", + "Deleted By": "Избришано од", + "Deleted Date": "Датум на бришење", + "Deleted Items Management": "Управување со избришани ставки", + "Deleted This Week": "Избришано оваа недела", + "Deleted Today": "Избришано денес", + "Deleted:": "Избришано:", + "Deleting SOLR Collection...": "Бришење на SOLR колекција...", + "Deleting...": "Бришење...", + "Deletion Date Range": "Опсег на датуми на бришење", + "Deletion Statistics": "Статистика на бришење", + "Deprecated": "Застарено", + "Depublish": "Отпубликувај", + "Description": "Опис", + "Description Template": "Шаблон за опис", + "Description shown as tooltip": "Описот е прикажан како tooltip", + "Description:": "Опис:", + "Detect data types and patterns": "Детектирај типови на податоци и шаблони", + "Detected At": "Детектирано на", + "Detected Format:": "Детектиран формат:", + "Detected Issues:": "Детектирани проблеми:", + "Detected: {date}": "Детектирано: {date}", + "Direct permission": "Директна дозвола", + "Disable": "Оневозможи", + "Disable this to make the agent": "Оневозможете го ова за да го направите агентот", + "Disabled": "Оневозможено", + "Discover Files": "Откривај датотеки", + "Discovered Properties": "Откриени својства", + "Display Format": "Формат на прикажување", + "Display Name": "Прикажано име", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Дали сакате трајно да ги избришете сите филтрирани записи на ревизорскиот траг? Оваа акција не може да се поништи.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Дали сакате трајно да го избришете овој запис на ревизорскиот траг? Оваа акција не може да се поништи.", + "Documents": "Документи", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR е овозможен", + "Download API Spec": "Преземи API спецификација", + "Download API Specification": "Преземи API спецификација", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Преземете копија на сите ваши лични податоци зачувани во OpenRegister (GDPR член 20).", + "Drag a file or files here": "Повлечете датотека или датотеки овде", + "Dry run completed: {count} fields would be created": "Тест-извршување завршено: {count} полиња ќе бидат создадени", + "Dry run completed: {count} fields would be fixed": "Тест-извршување завршено: {count} полиња ќе бидат поправени", + "Duration in seconds (optional)": "Траење во секунди (опционално)", + "Edit": "Уреди", + "Edit Agent": "Уреди агент", + "Edit Application": "Уреди апликација", + "Edit Configuration": "Уреди конфигурација", + "Edit Endpoint": "Уреди крајна точка", + "Edit Organisation": "Уреди организација", + "Edit Property '{key}' of '{title}'": "Уреди своство '{key}' на '{title}'", + "Edit Register": "Уреди регистар", + "Edit Schema": "Уреди шема", + "Edit Source": "Уреди извор", + "Edit View": "Уреди преглед", + "Edit Webhook": "Уреди Webhook", + "Edit labels": "Уреди ознаки", + "Edit processing activity": "Уреди активност на обработка", + "Edit tags for {name}": "Уреди ознаки за {name}", + "Edit verwerkingsactiviteit": "Уреди verwerkingsactiviteit", + "Edit view": "Уреди преглед", + "Edit view details": "Уреди детали на прегледот", + "Effectiveness": "Ефективност", + "Email": "Е-пошта", + "Email Addresses": "Адреси за е-пошта", + "Email digest frequency": "Фреквенција на е-пошта дајџест", + "Emails": "Е-пошти", + "Embedding Model": "Модел за вградување", + "Embedding Provider": "Давател на вградување", + "Embedding generated successfully": "Вградувањето е успешно генерирано", + "Embedding provider connection successful!": "Врската со давателот на вградување е успешна!", + "Embedding test failed: {error}": "Тестот на вградување не успеа: {error}", + "Enable": "Овозможи", + "Enable RAG": "Овозможи RAG", + "Enable auto-creation": "Овозможи автоматско создавање", + "Enable automatic file vectorization": "Овозможи автоматска векторизација на датотеки", + "Enable automatic object vectorization": "Овозможи автоматска векторизација на објекти", + "Enable automatic synchronization": "Овозможи автоматска синхронизација", + "Enable calendar provider": "Овозможи давател на календар", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Овозможи детално логирање за SOLR операции (препорачано за отстранување грешки)", + "Enable faceting": "Овозможи фасетирање", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Овозможете или оневозможете функции на LLM. Конфигурирајте давателе и модели користејќи го копчето LLM Конфигурација погоре.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Овозможете или оневозможете функции на LLM. Конфигурирајте давателе и модели користејќи го копчето за LLM конфигурација погоре.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Овозможете или оневозможете интеграција со SOLR пребарување. Конфигурирајте поставки за поврзување користејќи го копчето Поставки за поврзување погоре.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Овозможете или оневозможете интеграција со SOLR пребарување. Конфигурирајте поставки за поврзување користејќи го копчето за поставки за поврзување погоре.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Овозможете или оневозможете интеграција со n8n работниот тек. Конфигурирајте поставки за поврзување подолу.", + "Enable or disable this webhook": "Овозможете или оневозможете овој webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Овозможете алатки кои му дозволуваат на агентот да комуницира со податоци преку повикување функции.", + "Enable vectorization for all existing and future views (may increase costs)": "Овозможи векторизација за сите постоечки и идни прегледи (може да ги зголеми трошоците)", + "Enable write-back to target objects": "Овозможи запишување назад кон целните објекти", + "Enabled": "Овозможено", + "End Date Field": "Поле за краен датум", + "Endpoint Path*": "Патека на крајна точка*", + "Endpoints": "Крајни точки", + "Engine not found": "Моторот не е пронајден", + "Enter ConfigSet name": "Внесете го името на ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Внесете кориснички имиња на Nextcloud за да доделите пристап до овој приватен агент", + "Enter agent description (optional)": "Внесете опис на агентот (опционално)", + "Enter agent name": "Внесете го името на агентот", + "Enter application description (optional)": "Внесете опис на апликацијата (опционално)", + "Enter application name": "Внесете го името на апликацијата", + "Enter collection name": "Внесете го името на колекцијата", + "Enter configuration description (optional)": "Внесете опис на конфигурацијата (опционално)", + "Enter configuration title": "Внесете наслов на конфигурација", + "Enter description (optional)...": "Внесете опис (опционално)...", + "Enter new collection name": "Внесете го новото име на колекцијата", + "Enter object ID": "Внесете ID на објект", + "Enter organisation description (optional)": "Внесете опис на организацијата (опционално)", + "Enter organisation name": "Внесете го името на организацијата", + "Enter search term": "Внесете термин за пребарување", + "Enter search terms or leave empty to browse all": "Внесете термини за пребарување или оставете празно за да прегледате сè", + "Enter system prompt for the agent": "Внесете системска порака за агентот", + "Enter username and press Enter": "Внесете корисничко ime и притиснете Enter", + "Enter view name...": "Внесете го името на прегледот...", + "Enter webhook name": "Внесете го името на webhook", + "Enter your API key": "Внесете го вашиот API клуч", + "Enter your object here...": "Внесете го вашиот објект овде...", + "Enter your schema here...": "Внесете ја вашата шема овде...", + "Entities": "Ентитети", + "Entities detected": "Детектирани ентитети", + "Entity ID: {id}": "ID на ентитет: {id}", + "Entity Information": "Информации за ентитет", + "Entity Recognition": "Препознавање на ентитети", + "Entity Recognition Method": "Метод за препознавање на ентитети", + "Entity deleted successfully": "Ентитетот е успешно избришан", + "Entity not found": "Ентитетот не е пронајден", + "Entries to be deleted:": "Записи кои ќе се избришат:", + "Enum Issue": "Проблем со enum", + "Enum constraint is missing": "Ограничувањето enum недостасува", + "Erase (Art 17)": "Избриши (чл. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Да се избришат {count} објект(и) за овој субјект? Оваа акција се запишува во ревизорскиот траг.", + "Erasure complete": "Бришењето е завршено", + "Erasure preview": "Преглед на бришење", + "Error": "Грешка", + "Error Details": "Детали на грешка", + "Error Information": "Информации за грешка", + "Error Message": "Порака за грешка", + "Error deleting audit trails: {error}": "Грешка при бришење на ревизорски траги: {error}", + "Error deleting search trails: {error}": "Грешка при бришење на траги на пребарување: {error}", + "Error loading application": "Грешка при вчитување на апликацијата", + "Error loading audit trails": "Грешка при вчитување на ревизорски траги", + "Error loading entity": "Грешка при вчитување на ентитет", + "Error loading search trails": "Грешка при вчитување на траги на пребарување", + "Estimated Batches:": "Проценети серии:", + "Estimated Cost:": "Проценети трошоци:", + "Estimated Duration:": "Проценето траење:", + "Event": "Настан", + "Event Property for Payload": "Својство на настан за payload", + "Events": "Настани", + "Every schema with detected PII has a processing-activity annotation.": "Секоја шема со детектиран PII има анотација за активност на обработка.", + "Example": "Пример", + "Example Value": "Примерна вредност", + "Example value for this property": "Примерна вредност за ова својство", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Ексклузивен максимум", + "Exclusive Minimum": "Ексклузивен минимум", + "Exclusive maximum": "Ексклузивен максимум", + "Exclusive minimum": "Ексклузивен минимум", + "Execution Mode": "Режим на извршување", + "Execution Time": "Време на извршување", + "Execution Time Range": "Опсег на времиња на извршување", + "Existing Improvements": "Постоечки подобрувања", + "Expand details": "Прошири детали", + "Expiration": "Истекување", + "Expires": "Истекува", + "Expires in (e.g., 90d)": "Истекува за (на пр. 90d)", + "Exponential": "Експоненцијален", + "Export": "Извези", + "Export \"{schema}\" objects from \"{register}\"": "Извези објекти \"{schema}\" од \"{register}\"", + "Export Configuration": "Извези конфигурација", + "Export Format:": "Формат на извоз:", + "Export Objects": "Извези објекти", + "Export completed successfully": "Извозот е успешно завршен", + "Export downloaded successfully": "Извозот е успешно преземен", + "Export failed: {error}": "Извозот не успеа: {error}", + "Export is rate limited. Please try again later.": "Извозот е ограничен по стапка. Обидете се повторно подоцна.", + "Export my data": "Извези ги моите податоци", + "Export, view, or delete audit trails": "Извези, прегледај или избриши ревизорски траги", + "Exporting...": "Извезување...", + "Extend Schema": "Проширена шема", + "Extended": "Проширено", + "Extensions": "Екстензии", + "Extra Query Parameters": "Дополнителни параметри на барање", + "Extract Now": "Извлечи сега", + "Extract Pending Files": "Извлечи датотеки на чекање", + "Extract now": "Извлечи сега", + "Extract properties from each object": "Извлечи својства од секој објект", + "Extracted At": "Извлечено на", + "Extracted at": "Извлечено на", + "Extraction": "Извлекување", + "Extraction Mode": "Режим на извлекување", + "Extraction Scope": "Опфат на извлекување", + "Extraction Status": "Статус на извлекување", + "Extraction error for {file}: {error}": "Грешка при извлекување за {file}: {error}", + "Facet Description": "Опис на фасета", + "Facet Order": "Редослед на фасета", + "Facet Title": "Наслов на фасета", + "Facet Type": "Тип на фасета", + "Facet configuration updated successfully": "Конфигурацијата на фасета е успешно ажурирана", + "Facetable": "Фасетирачки", + "Faceting Configuration:": "Конфигурација на фасетирање:", + "Facets discovered and configured successfully": "Фасетите се успешно пронајдени и конфигурирани", + "Facets discovered successfully": "Фасетите се успешно пронајдени", + "Failed": "Неуспешно", + "Failed to add attachment to object": "Неуспешно додавање на прилог кон објект", + "Failed to add user to organisation": "Неуспешно додавање на корисник во организација", + "Failed to analyze schema properties": "Неуспешна анализа на својствата на шемата", + "Failed to archive conversation": "Неуспешно архивирање на разговорот", + "Failed to calculate sizes": "Неуспешно пресметување на големините", + "Failed to cancel deactivation": "Неуспешно откажување на деактивацијата", + "Failed to change active organisation: {error}": "Неуспешна промена на активната организација: {error}", + "Failed to change password": "Неуспешна промена на лозинката", + "Failed to check version: {error}": "Неуспешна проверка на верзијата: {error}", + "Failed to clear audit trails: {error}": "Неуспешно бришење на ревизиските записи: {error}", + "Failed to clear blob objects: {error}": "Неуспешно бришење на blob објекти: {error}", + "Failed to clear cache: {error}": "Неуспешно бришење на кешот: {error}", + "Failed to clear collection": "Неуспешно бришење на колекцијата", + "Failed to clear conversation": "Неуспешно бришење на разговорот", + "Failed to clear embeddings: {error}": "Неуспешно бришење на вградувањата: {error}", + "Failed to clear search trails: {error}": "Неуспешно бришење на записите за пребарување: {error}", + "Failed to clear {type} cache: {error}": "Неуспешно бришење на {type} кешот: {error}", + "Failed to copy changes": "Неуспешно копирање на промените", + "Failed to copy data": "Неуспешно копирање на податоци", + "Failed to copy data to clipboard": "Неуспешно копирање на податоците во клипборд", + "Failed to copy to clipboard": "Неуспешно копирање во клипборд", + "Failed to copy token": "Неуспешно копирање на токенот", + "Failed to create conversation": "Неуспешно создавање на разговор", + "Failed to create missing SOLR fields: {error}": "Неуспешно создавање на недостасувачките SOLR полиња: {error}", + "Failed to create or find project": "Неуспешно создавање или наоѓање на проект", + "Failed to create token": "Неуспешно создавање на токен", + "Failed to delete SOLR collection: {error}": "Неуспешно бришење на SOLR колекцијата: {error}", + "Failed to delete collection": "Неуспешно бришење на колекцијата", + "Failed to delete conversation": "Неуспешно бришење на разговорот", + "Failed to delete objects for {schema}: {error}": "Неуспешно бришење на објекти за {schema}: {error}", + "Failed to delete webhook": "Неуспешно бришење на Webhook", + "Failed to depublish register: {error}": "Неуспешно откажување на публикацијата на регистарот: {error}", + "Failed to depublish schema: {error}": "Неуспешно откажување на публикацијата на шемата: {error}", + "Failed to depublish: {error}": "Неуспешно откажување на публикацијата: {error}", + "Failed to discover files: {error}": "Неуспешно откривање на датотеки: {error}", + "Failed to download API specification": "Неуспешно преземање на API спецификацијата", + "Failed to export data": "Неуспешен извоз на податоци", + "Failed to export objects": "Неуспешен извоз на објекти", + "Failed to fetch conversation history": "Неуспешно преземање на историјата на разговорот", + "Failed to fix mismatched SOLR fields: {error}": "Неуспешно поправање на несовпаднатите SOLR полиња: {error}", + "Failed to get SOLR field configuration": "Неуспешно добивање на конфигурацијата на SOLR полиња", + "Failed to get chat statistics": "Неуспешно добивање на статистики за разговор", + "Failed to get database information: %s": "Неуспешно добивање на информации за базата на податоци: %s", + "Failed to get entity categories": "Неуспешно добивање на категории на ентитети", + "Failed to get entity statistics": "Неуспешно добивање на статистики за ентитети", + "Failed to get entity types": "Неуспешно добивање на типови на ентитети", + "Failed to get response: {error}": "Неуспешно добивање на одговор: {error}", + "Failed to invalidate app store cache: {error}": "Неуспешно поништување на кешот на продавницата за апликации: {error}", + "Failed to leave organisation: {error}": "Неуспешно напуштање на организацијата: {error}", + "Failed to link object": "Неуспешно поврзување на објект", + "Failed to load Deck cards": "Неуспешно вчитување на Deck картички", + "Failed to load LLM configuration": "Неуспешно вчитување на LLM конфигурацијата", + "Failed to load Nextcloud groups": "Неуспешно вчитување на Nextcloud групи", + "Failed to load cache statistics: {error}": "Неуспешно вчитување на статистики за кеш: {error}", + "Failed to load conversation": "Неуспешно вчитување на разговорот", + "Failed to load entities": "Неуспешно вчитување на ентитети", + "Failed to load entity": "Неуспешно вчитување на ентитет", + "Failed to load extraction data": "Неуспешно вчитување на податоците за екстракција", + "Failed to load files": "Неуспешно вчитување на датотеки", + "Failed to load linked contacts": "Неуспешно вчитување на поврзани контакти", + "Failed to load linked emails": "Неуспешно вчитување на поврзани е-пошти", + "Failed to load linked events": "Неуспешно вчитување на поврзани настани", + "Failed to load organisations": "Неуспешно вчитување на организации", + "Failed to load register data": "Неуспешно вчитување на податоците на регистарот", + "Failed to load related entities": "Неуспешно вчитување на поврзани ентитети", + "Failed to load settings: {error}": "Неуспешно вчитување на поставките: {error}", + "Failed to load statistics: {error}": "Неуспешно вчитување на статистики: {error}", + "Failed to load templates": "Неуспешно вчитување на шаблони", + "Failed to load view: {error}": "Неуспешно вчитување на приказот: {error}", + "Failed to load webhooks": "Неуспешно вчитување на Webhook-ови", + "Failed to load workflows": "Неуспешно вчитување на работни текови", + "Failed to process message": "Неуспешна обработка на пораката", + "Failed to publish register: {error}": "Неуспешно публикување на регистарот: {error}", + "Failed to publish schema: {error}": "Неуспешно публикување на шемата: {error}", + "Failed to publish: {error}": "Неуспешно публикување: {error}", + "Failed to refresh database information": "Неуспешно освежување на информациите за базата на податоци", + "Failed to reindex collection": "Неуспешна реиндексација на колекцијата", + "Failed to remove avatar": "Неуспешно отстранување на аватарот", + "Failed to remove link": "Неуспешно отстранување на врската", + "Failed to remove member: {error}": "Неуспешно отстранување на членот: {error}", + "Failed to remove schema {schema}: {error}": "Неуспешно отстранување на шемата {schema}: {error}", + "Failed to remove user from organisation": "Неуспешно отстранување на корисник од организација", + "Failed to rename conversation": "Неуспешно преименување на разговорот", + "Failed to request deactivation": "Неуспешно барање за деактивација", + "Failed to restore conversation": "Неуспешно враќање на разговорот", + "Failed to retrieve extraction status.": "Неуспешно добивање на статусот на екстракција.", + "Failed to retrieve objects for file.": "Неуспешно добивање на објекти за датотеката.", + "Failed to retry extraction": "Неуспешен повторен обид за екстракција", + "Failed to retry webhook": "Неуспешен повторен обид за Webhook", + "Failed to revoke token": "Неуспешно откажување на токен", + "Failed to save GitHub token": "Неуспешно зачувување на GitHub токен", + "Failed to save GitLab URL": "Неуспешно зачувување на GitLab URL", + "Failed to save GitLab token": "Неуспешно зачувување на GitLab токен", + "Failed to save LLM settings: {error}": "Неуспешно зачувување на LLM поставки: {error}", + "Failed to save additional feedback": "Неуспешно зачувување на дополнителни повратни информации", + "Failed to save configuration.": "Неуспешно зачувување на конфигурацијата.", + "Failed to save configuration: {error}": "Неуспешно зачувување на конфигурацијата: {error}", + "Failed to save facet configuration": "Неуспешно зачувување на конфигурацијата на фасета", + "Failed to save feedback": "Неуспешно зачувување на повратни информации", + "Failed to save file settings: {error}": "Неуспешно зачувување на поставките за датотека: {error}", + "Failed to save n8n configuration": "Неуспешно зачувување на n8n конфигурација", + "Failed to save preferences": "Неуспешно зачувување на преференции", + "Failed to save roles": "Неуспешно зачувување на улоги", + "Failed to save settings": "Неуспешно зачувување на поставки", + "Failed to save settings: {error}": "Неуспешно зачувување на поставките: {error}", + "Failed to save view: {error}": "Неуспешно зачувување на прегледот: {error}", + "Failed to save warmup interval: {error}": "Неуспешно зачувување на интервалот за загревање: {error}", + "Failed to save webhook": "Неуспешно зачувување на Webhook", + "Failed to send feedback": "Неуспешно испраќање на повратни информации", + "Failed to set active organisation: {error}": "Неуспешно поставување на активна организација: {error}", + "Failed to setup SOLR: {error}": "Неуспешно поставување на SOLR: {error}", + "Failed to start file warmup: {error}": "Неуспешно стартување на загревање на датотека: {error}", + "Failed to start processing {type} files: {error}": "Неуспешно стартување на обработка на {type} датотеки: {error}", + "Failed to start vectorization: {error}": "Неуспешно стартување на векторизација: {error}", + "Failed to switch organisation: {error}": "Неуспешна промена на организација: {error}", + "Failed to sync magic table for {schema}: {error}": "Неуспешна синхронизација на магична табела за {schema}: {error}", + "Failed to test SOLR connection: {error}": "Неуспешно тестирање на SOLR врска: {error}", + "Failed to test webhook": "Неуспешно тестирање на Webhook", + "Failed to update LLM settings: {error}": "Неуспешно ажурирање на LLM поставки: {error}", + "Failed to update Multitenancy settings: {error}": "Неуспешно ажурирање на Multitenancy поставки: {error}", + "Failed to update RBAC settings: {error}": "Неуспешно ажурирање на RBAC поставки: {error}", + "Failed to update Retention settings: {error}": "Неуспешно ажурирање на поставките за задржување: {error}", + "Failed to update SOLR settings: {error}": "Неуспешно ажурирање на SOLR поставки: {error}", + "Failed to update favorite status": "Неуспешно ажурирање на статусот на омилено", + "Failed to update schema configuration for {schema}: {error}": "Неуспешно ажурирање на конфигурацијата на шемата за {schema}: {error}", + "Failed to update schema properties": "Неуспешно ажурирање на својствата на шемата", + "Failed to update view": "Неуспешно ажурирање на приказот", + "Failed to update view: {error}": "Неуспешно ажурирање на приказот: {error}", + "Failed to update webhook": "Неуспешно ажурирање на Webhook", + "Failed to upload avatar": "Неуспешно поставување на аватар", + "Failed to validate {schema}: {error}": "Неуспешна валидација на {schema}: {error}", + "Failed to vectorize files: {error}": "Неуспешна векторизација на датотеки: {error}", + "Failed to warmup names cache: {error}": "Неуспешно загревање на кешот со имиња: {error}", + "Federation": "Федерација", + "Feedback recorded": "Повратните информации се снимени", + "Fewer sources (1-3):": "Помалку извори (1-3):", + "Field": "Поле", + "Fields to Display": "Полиња за прикажување", + "File": "Датотека", + "File #{id}": "Датотека #{id}", + "File Chunk Prediction": "Предвидување на парчиња датотека", + "File Collection": "Колекција на датотеки", + "File Handling": "Ракување со датотеки", + "File Location": "Локација на датотека", + "File Management": "Управување со датотеки", + "File Name": "Име на датотека", + "File Path": "Патека на датотека", + "File Type Selection": "Избор на тип на датотека", + "File Types to Process": "Типови датотеки за обработка", + "File Vectorization": "Векторизација на датотеки", + "File Warmup": "Загревање на датотека", + "File actions menu": "Мени со акции за датотека", + "File anonymized successfully": "Датотеката е успешно анонимизирана", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Векторизацијата на парчиња датотека сè уште не е имплементирана. Парчињата се подготвени и зачувани, но сервисот за векторизација е во развој.", + "File collection not configured": "Колекцијата на датотеки не е конфигурирана", + "File discovery completed": "Откривањето на датотеки е завршено", + "File extraction completed": "Извлекувањето на датотека е завршено", + "File extraction queued": "Извлекувањето на датотека е во ред на чекање", + "File indexed successfully": "Датотеката е успешно индексирана", + "File is already anonymized": "Датотеката е веќе анонимизирана", + "File name": "Име на датотека", + "File not found": "Датотеката не е пронајдена", + "File settings saved successfully": "Поставките за датотека се успешно зачувани", + "File settings updated successfully": "Поставките за датотека се успешно ажурирани", + "File sources": "Извори на датотеки", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Векторизацијата на датотеки е завршена! {vectorized} парчиња векторизирани од {files} датотеки. {failed} неуспешни.", + "File vectorization configuration saved successfully": "Конфигурацијата за векторизација на датотеки е успешно зачувана", + "File vectorization started. Check the statistics section for progress.": "Векторизацијата на датотеки е започната. Проверете го делот со статистики за напредокот.", + "File warmup completed": "Загревањето на датотека е завршено", + "Files": "Датотеки", + "Files added successfully": "Датотеките се успешно додадени", + "Files with Completed Extraction:": "Датотеки со завршено извлекување:", + "Files → fileCollection, Objects → objectCollection": "Files → fileCollection, Objects → objectCollection", + "Files → filecollection, objects → objectcollection": "Files → filecollection, objects → objectcollection", + "Filter Audit Trails": "Филтрирање на ревизиски патеки", + "Filter Deleted Items": "Филтрирање на избришани ставки", + "Filter Objects": "Филтрирање на објекти", + "Filter Properties": "Филтрирање на својства", + "Filter Search Trails": "Филтрирање на патеки за пребарување", + "Filter Statistics": "Филтрирање на статистики", + "Filter and analyze search trail entries": "Филтрирање и анализа на записи за патеки за пребарување", + "Filter and manage audit trail entries": "Филтрирање и управување со записи на ревизиски патеки", + "Filter and manage soft deleted items": "Филтрирање и управување со меко избришани ставки", + "Filter and search entities": "Филтрирање и пребарување на ентитети", + "Filter by object ID": "Филтрирање по ID на објект", + "Filter by search term": "Филтрирање по термин за пребарување", + "Filter by type": "Филтрирање по тип", + "Filter by webhook": "Филтрирање по Webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Податоците за филтер се вчитани автоматски. Користете ги филтрите подолу за да го прецизирате пребарувањето.", + "Filter fields...": "Филтрирање на полиња...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Филтрирање на Webhook активатори по својства на носивост (едно по ред, формат: клуч: вредност)", + "Filtered": "Филтрирано", + "Filters": "Филтри", + "Finalizing setup...": "Финализирање на поставувањето...", + "Fireworks AI Chat Settings": "Fireworks AI поставки за разговор", + "Fireworks AI Embedding Configuration": "Fireworks AI конфигурација за вградување", + "First": "Прв", + "Fixed": "Фиксно", + "For chat and retrieval-augmented generation": "За разговор и генерација збогатена со пронаоѓање", + "For vector embeddings and semantic search": "За векторски вградувања и семантичко пребарување", + "Format": "Формат", + "Format Issue": "Проблем со форматот", + "Format constraint is missing": "Недостасува ограничување на форматот", + "From (e.g. -7 days or 2025-01-01)": "Од (на пр. -7 дена или 2025-01-01)", + "From Date": "Од датум", + "From date": "Од датум", + "Full data copied to clipboard": "Целосните податоци се копирани во клипборд", + "Function Tools": "Алатки за функции", + "General Issue": "Општ проблем", + "Generate recommendations and confidence scores": "Генерирање препораки и резултати за доверба", + "Generate report": "Генерирање извештај", + "Generate the verantwoordingsdocument": "Генерирање на verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Генерирање вектори веднаш кога се создаваат нови објекти", + "Generated: {time}": "Генерирано: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Добијте увид во реално време за здравјето на податоците на вашата организација со фокус на регистри, дефиниции на шеми и складирање и употреба на објекти.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Добијте увид во реално време за здравјето на податоците на вашата организација, трендовите на раст и метриките за квалитет.", + "GitHub Personal Access Token": "GitHub личен пристапен токен", + "GitHub token is valid": "GitHub токенот е валиден", + "GitHub token is valid! Username: {username}": "GitHub токенот е валиден! Корисничко ime: {username}", + "GitHub token removed successfully": "GitHub токенот е успешно отстранет", + "GitHub token saved successfully": "GitHub токенот е успешно зачуван", + "GitHub token test failed: {message}": "Тестирањето на GitHub токен не успеа: {message}", + "GitLab Personal Access Token": "GitLab личен пристапен токен", + "GitLab URL saved successfully": "GitLab URL е успешно зачуван", + "GitLab token is valid": "GitLab токенот е валиден", + "GitLab token is valid! Username: {username}": "GitLab токенот е валиден! Корисничко ime: {username}", + "GitLab token saved successfully": "GitLab токенот е успешно зачуван", + "GitLab token test failed: {message}": "Тестирањето на GitLab токен не успеа: {message}", + "Go to linked Register": "Оди до поврзаниот регистар", + "Go to linked Schema": "Оди до поврзаната шема", + "Go to organisation": "Оди до организацијата", + "Grant additional permissions beyond standard CRUD operations": "Доделете дополнителни дозволи надвор од стандардните CRUD операции", + "Group Quota": "Квота за групи", + "Groups": "Групи", + "HTML preview": "HTML преглед", + "HTTP Method": "HTTP метод", + "HTTP method used to send webhook requests": "HTTP метод што се користи за испраќање на Webhook барања", + "Headers": "Заглавија", + "Health": "Здравје", + "Heartbeat successful - connection kept alive": "Пулсот е успешен - врската е одржана", + "Helpful": "Корисно", + "Here you can configure the details for various connections.": "Тука можете да ги конфигурирате деталите за различни врски.", + "Here you can configure the details for your organisation.": "Тука можете да ги конфигурирате деталите за вашата организација.", + "Hide": "Сокриј", + "Hide Filters": "Сокриј филтри", + "Hide in collection view": "Сокриј во приказот на колекцијата", + "Hide in forms": "Сокриј во формулари", + "Hide in list view": "Сокриј во приказот на листата", + "High": "Висок", + "High Confidence": "Висока доверба", + "Host": "Домаќин", + "Hourly": "Секој час", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Колку длабоко да се преминуваат вгнездените својства на објектот (1-20). Повисоките вредности зафаќаат повеќе детали, но го зголемуваат векторскиот размер.", + "How often to check for updates (1-168 hours)": "Колку често да се проверуваат ажурирања (1-168 часа)", + "How to handle retries for failed webhook deliveries": "Како да се справите со повторни обиди за неуспешни Webhook испораки", + "IBANs": "IBAN-и", + "ID": "ID", + "ID of input mapping (optional)": "ID на влезното мапирање (опционално)", + "ID of output mapping (optional)": "ID на излезното мапирање (опционално)", + "ID of the target resource": "ID на целниот ресурс", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ограничувањата на стапката за IP се успешно исчистени", + "Identify properties not in the schema": "Идентификувајте својства кои не се во шемата", + "Immutable": "Непроменлив", + "Import": "Увоз", + "Import Configuration": "Увоз на конфигурација", + "Import Data into Register": "Увоз на податоци во регистарот", + "Import successful": "Увозот е успешен", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Увезете го шаблонот report-bundle.json за да го добиете регистарот `reports`, потоа создадете ја вашата прва контролна табла преку стандардниот UI за објекти. Контролните табли ги декларираат своите виџети во JSON, а рендерерот ги снабдува со живи податоци за агрегација.", + "Improved Property": "Подобрено својство", + "In use": "Во употреба", + "Inactive": "Неактивен", + "Inactive organisations cannot be used": "Неактивните организации не можат да се користат", + "Include IDs and names of related objects for better contextual search": "Вклучете ID-а и имиња на поврзани објекти за подобро контекстуално пребарување", + "Include ids and names of related objects for better contextual search": "Вклучете id-а и имиња на поврзани објекти за подобро контекстуално пребарување", + "Include related object references": "Вклучете референци на поврзани објекти", + "Include schema and register metadata": "Вклучете метаподатоци на шемата и регистарот", + "Index": "Индекс", + "Information about the current OpenRegister installation": "Информации за тековната OpenRegister инсталација", + "Inherited from register": "Наследено од регистарот", + "Inherits permissions from register": "Наследува дозволи од регистарот", + "Initialization failed": "Иницијализацијата не успеа", + "Initialize Project": "Иницијализирање на проект", + "Initializing SOLR setup...": "Иницијализирање на SOLR поставување...", + "Input Mapping": "Влезно мапирање", + "Inspect Fields": "Прегледај полиња", + "Inspect Index": "Прегледај индекс", + "Inspect SOLR Index": "Прегледај SOLR индекс", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Инсталирајте ја апликацијата notify_push од Nextcloud продавницата за апликации за да овозможите ажурирања во реално време.", + "Integrations": "Интеграции", + "Internet location (URL) of this installation": "Интернет локација (URL) на оваа инсталација", + "Interval": "Интервал", + "Invalid": "Невалидно", + "Invalid batch size. Must be between 1 and 5000": "Невалидна големина на серија. Мора да биде помеѓу 1 и 5000", + "Invalid feedback type": "Невалиден тип на повратни информации", + "Invalid field name provided": "Наведено е невалидно ime на поле", + "Invalid maxObjects. Must be 0 (all) or positive number": "Невалидни maxObjects. Мора да биде 0 (сите) или позитивен број", + "Invalid:": "Невалидно:", + "Invite Users": "Поканете корисници", + "Invited Users": "Поканети корисници", + "Inzage (Art 15)": "Inzage (чл. 15)", + "Inzage results": "Inzage резултати", + "Issue": "Проблем", + "Items per page:": "Ставки по страница:", + "Just now": "Токму сега", + "Key": "Клуч", + "LLM Configuration": "LLM конфигурација", + "LLM actions menu": "Мени со акции за LLM", + "LLM configuration saved successfully": "LLM конфигурацијата е успешно зачувана", + "LLM features disabled": "LLM функциите се оневозможени", + "LLM features enabled": "LLM функциите се овозможени", + "LLM must be enabled with an embedding provider configured": "LLM мора да биде овозможен со конфигуриран провајдер за вградување", + "LLM settings saved successfully": "LLM поставките се успешно зачувани", + "LLM settings updated successfully": "LLM поставките се успешно ажурирани", + "Label": "Ознака", + "Labels": "Ознаки", + "Languages": "Јазици", + "Last": "Последен", + "Last Triggered": "Последно активирано", + "Leave empty to allow all users access": "Оставете празно за да дозволите пристап на сите корисници", + "Leave off for auto-detection from property format.": "Оставете исклучено за автоматско откривање од форматот на својството.", + "Left organisation successfully": "Успешно напуштање на организацијата", + "Legal basis *": "Правна основа *", + "Length Range:": "Опсег на должина:", + "Linear": "Линеарно", + "Link": "Врска", + "Link an email from the Mail app sidebar to associate it with this object.": "Поврзете е-пошта од страничната лента на апликацијата Mail за да ја поврзете со овој објект.", + "Link an object to see it here.": "Поврзете објект за да го видите тука.", + "Link another object": "Поврзете друг објект", + "Link existing event": "Поврзете постоен настан", + "Link removed": "Врската е отстранета", + "Link to Object": "Врска до објект", + "Link to {name}": "Врска до {name}", + "Linked Objects": "Поврзани објекти", + "Linked by {user}": "Поврзано од {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Поврзаните е-пошти, календарски настани, контакти и Deck картички ќе се прикажат тука.", + "Linked to {name}": "Поврзано со {name}", + "Load Advanced Filters": "Вчитај напредни филтри", + "Load advanced filters with live data from your search index": "Вчитај напредни филтри со живи податоци од вашиот индекс за пребарување", + "Load more": "Вчитај повеќе", + "Load view": "Вчитај приказ", + "Loading API tokens...": "Вчитување на API токени...", + "Loading ConfigSets...": "Вчитување на ConfigSets...", + "Loading LLM configuration...": "Вчитување на LLM конфигурација...", + "Loading RBAC settings...": "Вчитување на RBAC поставки...", + "Loading activity...": "Вчитување на активности...", + "Loading advanced filters...": "Вчитување на напредни филтри...", + "Loading agents...": "Вчитување на агенти...", + "Loading application details...": "Вчитување на детали за апликацијата...", + "Loading applications...": "Вчитување на апликации...", + "Loading archived conversations...": "Вчитување на архивирани разговори...", + "Loading audit trails...": "Вчитување на ревизиски патеки...", + "Loading available tools...": "Вчитување на достапни алатки...", + "Loading cache statistics...": "Вчитување на статистики за кеш...", + "Loading collections...": "Вчитување на колекции...", + "Loading configurations...": "Вчитување на конфигурации...", + "Loading conversation...": "Вчитување на разговор...", + "Loading conversations...": "Вчитување на разговори...", + "Loading deleted items...": "Вчитување на избришани ставки...", + "Loading endpoints": "Вчитување на крајни точки", + "Loading entities...": "Вчитување на ентитети...", + "Loading events...": "Вчитување на настани...", + "Loading filter data...": "Вчитување на податоци за филтер...", + "Loading groups...": "Вчитување на групи...", + "Loading linked objects...": "Вчитување на поврзани објекти...", + "Loading log details...": "Вчитување на детали за дневник...", + "Loading multitenancy settings...": "Вчитување на Multitenancy поставки...", + "Loading n8n configuration...": "Вчитување на n8n конфигурација...", + "Loading permission matrix...": "Вчитување на матрица на дозволи...", + "Loading preferences...": "Вчитување на преференции...", + "Loading register data...": "Вчитување на податоци од регистарот...", + "Loading registers...": "Вчитување на регистри...", + "Loading retention settings...": "Вчитување на поставките за задржување...", + "Loading schemas...": "Вчитување на шеми...", + "Loading search configuration...": "Вчитување на конфигурација за пребарување...", + "Loading search trails...": "Вчитување на патеки за пребарување...", + "Loading sources...": "Вчитување на извори...", + "Loading statistics...": "Вчитување на статистики...", + "Loading text extraction settings...": "Вчитување на поставките за извлекување текст...", + "Loading tokens...": "Вчитување на токени...", + "Loading user groups...": "Вчитување на корисничките групи...", + "Loading users...": "Вчитување на корисници...", + "Loading views...": "Вчитување на прикази...", + "Loading...": "Вчитување...", + "Local": "Локален", + "Local Version": "Локална верзија", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Лоцирајте секој објект кој упатува на субјект на податоци (чл. 15 inzage), прегледајте бришење (чл. 17 vergetelheid) или извезете ги нивните податоци (чл. 20 portabiliteit).", + "Location": "Локација", + "Location Field": "Поле за локација", + "Locations": "Локации", + "Locked": "Заклучено", + "Locked:": "Заклучено:", + "Login successful": "Најавувањето е успешно", + "Logs": "Логови", + "Low": "Низок", + "Low Confidence": "Ниска доверба", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Пониските броеви се прикажуваат прво во страничната лента за филтри. Оставете празно за автоматско подредување.", + "Lucene Version:": "Верзија на Lucene:", + "Magic table sync completed for {schema}": "Синхронизацијата на магичната табела е завршена за {schema}", + "Magic table synchronized successfully": "Магичната табела е успешно синхронизирана", + "Mail Integration": "Интеграција на пошта", + "Mail integration is not available": "Интеграцијата на пошта не е достапна", + "Make this installation discoverable within the federation network.": "Направете ја оваа инсталација откривлива во мрежата на федерацијата.", + "Manage Organisation Roles": "Управување со улоги на организацијата", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Управување со SOLR Колекции (складишта на податоци) и доделување за објекти и датотеки.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Управување со SOLR ConfigSets (конфигурациски шаблони) за вашите колекции.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Управување со SOLR колекции (складишта на податоци) и доделување за објекти и датотеки.", + "Manage SOLR configsets (configuration templates) for your collections.": "Управување со SOLR configsets (конфигурациски шаблони) за вашите колекции.", + "Manage and configure agents for automated tasks": "Управување и конфигурирање на агенти за автоматизирани задачи", + "Manage and monitor file text extraction status": "Управување и следење на статусот за извлекување текст од датотеки", + "Manage and restore soft deleted items from your registers": "Управување и обновување на меко избришани ставки од вашите регистри", + "Manage and view detected entities from files and objects": "Управување и прегледување на детектирани ентитети од датотеки и објекти", + "Manage document templates and themes": "Управување со документни шаблони и теми", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Управување со n8n работни текови за OpenRegister автоматизација. Работните текови ќе бидат зачувани во конфигурираниот проект.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Управување со активностите за обработка, извршување на барања за пристап на субјектите на податоци и ревизија на усогласеноста според EU GDPR / Dutch AVG.", + "Manage webhooks for event-driven integrations": "Управување со Webhooks за интеграции управувани од настани", + "Manage your account settings, security, and personal data.": "Управување со поставките на вашата сметка, безбедноста и личните податоци.", + "Manage your applications and modules": "Управување со вашите апликации и модули", + "Manage your chat conversations": "Управување со вашите разговори во чет", + "Manage your data registers and their configurations": "Управување со вашите регистри на податоци и нивните конфигурации", + "Manage your data schemas and their properties": "Управување со вашите шеми за податоци и нивните својства", + "Manage your data sources and their configurations": "Управување со вашите извори на податоци и нивните конфигурации", + "Manage your organisations and switch between them": "Управување со вашите организации и префрлање помеѓу нив", + "Manage your saved search configurations": "Управување со вашите зачувани конфигурации за пребарување", + "Manage your system configurations and settings": "Управување со системските конфигурации и поставки", + "Managed": "Управувано", + "Mappings": "Мапирања", + "Mass validation completed successfully": "Масовната валидација е успешно завршена", + "Mass validation failed: {error}": "Масовната валидација не успеа: {error}", + "Max Files (0 = all)": "Макс датотеки (0 = сите)", + "Max Length": "Максимална должина", + "Max Length:": "Максимална должина:", + "Max Objects (0 = all)": "Макс објекти (0 = сите)", + "Max Retries": "Макс повторни обиди", + "Max Shards/Node": "Макс шардови/јазол", + "Max Tokens": "Макс токени", + "Max execution time (ms)": "Макс време на извршување (ms)", + "Max ms": "Макс ms", + "Max result count": "Макс број резултати", + "Max results": "Макс резултати", + "Maximum": "Максимум", + "Maximum File Size (MB)": "Максимална големина на датотека (MB)", + "Maximum Nesting Depth": "Максимална длабочина на вгнездување", + "Maximum length": "Максимална должина", + "Maximum length constraint is missing": "Недостасува ограничување на максималната должина", + "Maximum length is too restrictive": "Максималната должина е премногу рестриктивна", + "Maximum number of files to process. Set to 0 to process all files.": "Максимален број на датотеки за обработка. Поставете на 0 за обработка на сите датотеки.", + "Maximum number of items": "Максимален број на ставки", + "Maximum number of objects to process. Set to 0 to process all objects.": "Максимален број на објекти за обработка. Поставете на 0 за обработка на сите објекти.", + "Maximum number of retry attempts for failed deliveries": "Максимален број на повторни обиди за неуспешни испораки", + "Maximum time to wait before committing changes": "Максимално време на чекање пред извршување на промените", + "Maximum tokens to generate": "Максимален број на токени за генерирање", + "Maximum value": "Максимална вредност", + "Maximum value constraint is missing": "Недостасува ограничување на максималната вредност", + "Maximum value is too restrictive": "Максималната вредност е премногу рестриктивна", + "Medium": "Среден", + "Medium Confidence": "Средна доверба", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средни прашања: Пребарувања со некое филтрирање или повеќе параметри (на пр., опсези на датуми, специфични регистри/шеми)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средни прашања: пребарувања со некое филтрирање или повеќе параметри (на пр., опсези на датуми, специфични регистри/шеми)", + "Member removed successfully": "Членот е успешно отстранет", + "Members": "Членови", + "Members ({count})": "Членови ({count})", + "Members of selected groups can access this view": "Членовите на избраните групи можат да пристапат до овој приказ", + "Members:": "Членови:", + "Memory prediction calculated successfully": "Предвидувањето на меморијата е успешно пресметано", + "Message content is required": "Потребна е содржина на пораката", + "Message does not belong to this conversation": "Пораката не припаѓа на овој разговор", + "Message not found": "Пораката не е пронајдена", + "Metadata": "Метаподатоци", + "Metadata Filters": "Филтри за метаподатоци", + "Method": "Метод", + "Method*": "Метод*", + "Method: {method}": "Метод: {method}", + "Min Length": "Минимална должина", + "Min execution time (ms)": "Мин. време на извршување (ms)", + "Min ms": "Мин ms", + "Min result count": "Мин број резултати", + "Min results": "Мин резултати", + "Minimum": "Минимум", + "Minimum length": "Минимална должина", + "Minimum number of items": "Минимален број на ставки", + "Minimum value": "Минимална вредност", + "Minimum value constraint is missing": "Недостасува ограничување на минималната вредност", + "Minimum value is too restrictive": "Минималната вредност е премногу рестриктивна", + "Missing conversation or agentUuid": "Недостасува разговор или agentUuid", + "Missing conversationId": "Недостасува conversationId", + "Missing message": "Недостасува порака", + "Missing object fields created successfully": "Недостасувачките полиња на објектот се успешно создадени", + "Mode:": "Режим:", + "Model": "Модел", + "Model Name": "Ime на модел", + "Modified": "Изменето", + "Monthly": "Месечно", + "More sources (10-20):": "Повеќе извори (10-20):", + "Most Active Objects": "Најактивни објекти", + "Multiple Of": "Кратно на", + "Multiple of": "Кратно на", + "Multiple search trail deletion not implemented yet": "Бришењето на повеќе патеки за пребарување сè уште не е имплементирано", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Multitenancy поставките се успешно ажурирани", + "My Account": "Мојата сметка", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Ime", + "Name *": "Ime *", + "Name*": "Ime*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Кешот со имиња е успешно загреан: {count} имиња вчитани за {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Кешот со имиња е успешно загреан: {count} имиња вчитани за {time}. {message}", + "Names cache warmup completed": "Загревањето на кешот со имиња е завршено", + "Naming pattern for copies:": "Шаблон за именување на копии:", + "New Collection Name": "Ново ime на колекција", + "New Configuration": "Нова конфигурација", + "New Conversation": "Нов разговор", + "New Properties": "Нови својства", + "New Property": "Ново својство", + "New User Group": "Нова корисничка група", + "New Value": "Нова вредност", + "New activity": "Нова активност", + "New password": "Нова лозинка", + "New processing activity": "Нова активност за обработка", + "New users without specific organisation membership will be automatically added to this organisation": "Новите корисници без специфично членство во организација автоматски ќе бидат додадени во оваа организација", + "New verwerkingsactiviteit": "Нова verwerkingsactiviteit", + "Next": "Следно", + "Nextcloud Groups": "Nextcloud групи", + "No": "Не", + "No API tokens.": "Нема API токени.", + "No ConfigSets found": "Не се пронајдени ConfigSets", + "No Deck cards linked to this object": "Нема Deck картички поврзани со овој објект", + "No GitHub token configured": "Не е конфигуриран GitHub токен", + "No GitHub token provided": "Не е обезбеден GitHub токен", + "No GitLab token provided": "Не е обезбеден GitLab токен", + "No Organisation": "Без организација", + "No active filters": "Нема активни филтри", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Нема активни објекти за меко бришење за шемата {schema}. Користете \"Трајно избриши\" за отстранување на меко избришани објекти.", + "No activity data available": "Нема достапни податоци за активност", + "No activity found.": "Не е пронајдена активност.", + "No agents available": "Нема достапни агенти", + "No agents found": "Не се пронајдени агенти", + "No app store cache exists yet": "Сè уште не постои кеш на продавницата за апликации", + "No applications are available.": "Нема достапни апликации.", + "No applications found": "Не се пронајдени апликации", + "No approval steps for this object.": "Нема чекори за одобрување за овој објект.", + "No archived conversations": "Нема архивирани разговори", + "No audit trail entries found": "Не се пронајдени записи на ревизорски траг", + "No changes recorded": "Не се забележани промени", + "No chunks to vectorize": "Нема парчиња за векторизација", + "No collections found": "Не се пронајдени колекции", + "No configuration data": "Нема податоци за конфигурација", + "No configuration selected": "Не е избрана конфигурација", + "No configurations are available.": "Нема достапни конфигурации.", + "No configurations found": "Не се пронајдени конфигурации", + "No configurations found for this application.": "Не се пронајдени конфигурации за оваа апликација.", + "No contacts linked to this object": "Нема контакти поврзани со овој објект", + "No conversations yet. Create a new one to get started!": "Сè уште нема разговори. Создадете нов за да започнете!", + "No dashboards yet": "Сè уште нема контролни табли", + "No data available": "Нема достапни податоци", + "No data available for chart": "Нема достапни податоци за графикон", + "No deleted items found": "Не се пронајдени избришани ставки", + "No deletion data available": "Нема достапни податоци за бришење", + "No description found": "Не е пронајден опис", + "No description provided": "Не е обезбеден опис", + "No emails linked to this object": "Нема е-пошти поврзани со овој објект", + "No endpoints defined": "Не се дефинирани крајни точки", + "No entities detected for this email.": "Не се детектирани ентитети за оваа е-пошта.", + "No entities detected in this file. Run text extraction first.": "Не се детектирани ентитети во оваа датотека. Прво стартувајте го извлекувањето на текст.", + "No entities found": "Не се пронајдени ентитети", + "No entities have been detected yet": "Сè уште не се детектирани ентитети", + "No events found": "Не се пронајдени настани", + "No events linked to this object": "Нема настани поврзани со овој објект", + "No expired audit trails found to clear": "Не се пронајдени истечени ревизорски траги за чистење", + "No expired search trails found to clear": "Не се пронајдени истечени траги на пребарување за чистење", + "No extraction data available for this file": "Нема достапни податоци за извлекување за оваа датотека", + "No facetable fields available. Select a register and schema to see available filters.": "Нема достапни фасетни полиња. Изберете регистар и шема за да ги видите достапните филтри.", + "No files found": "Не се пронајдени датотеки", + "No files have been extracted yet": "Сè уште не се извлечени датотеки", + "No files selected": "Не се избрани датотеки", + "No files to process": "Нема датотеки за обработка", + "No files to reindex": "Нема датотеки за реиндексирање", + "No filters are currently active. This will delete ALL audit trail entries!": "Моментално нема активни филтри. Ова ќе ги избрише СИТЕ записи на ревизорскиот траг!", + "No groups found. Try a different search.": "Не се пронајдени групи. Обидете се со различно пребарување.", + "No label": "Без ознака", + "No labels": "Без ознаки", + "No linked objects": "Нема поврзани објекти", + "No log entries found": "Не се пронајдени записи на лог", + "No logs are available for this configuration.": "Нема достапни логови за оваа конфигурација.", + "No logs are available for this source.": "Нема достапни логови за овој извор.", + "No logs found": "Не се пронајдени логови", + "No matches": "Нема совпаѓања", + "No mismatched fields found - SOLR schema is properly configured": "Не се пронајдени несовпаднати полиња - SOLR шемата е правилно конфигурирана", + "No objects are currently selected for deletion.": "Моментално не се избрани објекти за бришење.", + "No objects are currently selected for permanent deletion.": "Моментално не се избрани објекти за трајно бришење.", + "No objects are currently selected for restoration.": "Моментално не се избрани објекти за враќање.", + "No objects found": "Не се пронајдени објекти", + "No objects found to vectorize": "Не се пронајдени објекти за векторизација", + "No objects linked to this email": "Нема објекти поврзани со оваа е-пошта", + "No objects selected": "Не се избрани објекти", + "No objects selected for deletion": "Не се избрани објекти за бришење", + "No objects selected for restoration": "Не се избрани објекти за враќање", + "No objects to delete": "Нема објекти за бришење", + "No objects to delete for schema {schema}": "Нема објекти за бришење за шемата {schema}", + "No objects were permanently deleted": "Ниеден објект не е трајно избришан", + "No organisations found": "Не се пронајдени организации", + "No personal data was found for this subject identifier.": "Не се пронајдени лични податоци за овој идентификатор на субјект.", + "No processing activities yet": "Сè уште нема активности на обработка", + "No properties found": "Не се пронајдени својства", + "No properties match your filters.": "Ниедно својство не одговара на вашите филтри.", + "No purge date set": "Не е поставен датум на чистење", + "No register data available": "Нема достапни податоци за регистар", + "No register objects reference this file": "Ниеден објект на регистар не упатува на оваа датотека", + "No registers found": "Не се пронајдени регистри", + "No registers found for this application.": "Не се пронајдени регистри за оваа апликација.", + "No registers found. Create a register to configure permissions.": "Не се пронајдени регистри. Создадете регистар за конфигурирање на дозволите.", + "No related cases found for this sender": "Не се пронајдени поврзани случаи за овој испраќач", + "No related entities": "Нема поврзани ентитети", + "No relations found": "Не се пронајдени релации", + "No request body available": "Нема достапно тело на барање", + "No response body available": "Нема достапно тело на одговор", + "No saved views yet. Create one in the Search tab!": "Сè уште нема зачувани прегледи. Создадете еден во јазичето за пребарување!", + "No saved views yet. create one in the search tab!": "Сè уште нема зачувани прегледи. Создадете еден во јазичето за пребарување!", + "No scan run yet": "Сè уште не е стартувано скенирање", + "No schema data available": "Нема достапни податоци за шема", + "No schema selected for exploration": "Не е избрана шема за истражување", + "No schemas configured for mail linking.": "Не се конфигурирани шеми за поврзување со пошта.", + "No schemas found": "Не се пронајдени шеми", + "No schemas found for this application.": "Не се пронајдени шеми за оваа апликација.", + "No search terms data available": "Нема достапни податоци за термини на пребарување", + "No search trail entries found": "Не се пронајдени записи на траги на пребарување", + "No sources are available.": "Нема достапни извори.", + "No sources found": "Не се пронајдени извори", + "No synchronizations found": "Не се пронајдени синхронизации", + "No templates found": "Не се пронајдени шаблони", + "No templates have been created yet": "Сè уште не се создадени шаблони", + "No tools available. Tools can be registered by installed apps.": "Нема достапни алатки. Алатките можат да се регистрираат од инсталираните апликации.", + "No users in this organisation.": "Нема корисници во оваа организација.", + "No valid magic-mapped register+schema combinations found": "Не се пронајдени валидни магично-мапирани комбинации на регистар+шема", + "No views found. Create views first before configuring vectorization.": "Не се пронајдени прегледи. Прво создадете прегледи пред конфигурирање на векторизацијата.", + "No views match your search": "Ниеден преглед не одговара на вашето пребарување", + "No webhooks found": "Не се пронајдени webhooks", + "No webhooks have been configured yet": "Сè уште не се конфигурирани webhooks", + "No workflows found in this project. Create workflows in the n8n editor.": "Не се пронајдени работни текови во овој проект. Создадете работни текови во n8n уредникот.", + "None": "Ниедно", + "Not Found": "Не е пронајдено", + "Not extracted": "Не е извлечено", + "Not helpful": "Не е корисно", + "Not in use": "Не е во употреба", + "Note:": "Забелешка:", + "Notifications": "Известувања", + "Number Constraints": "Нумерички ограничувања", + "Number of Sources": "Број на извори", + "Number of chunks to vectorize in one API call (1-100).": "Број на парчиња за векторизација во едно API повикување (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Број на парчиња за векторизација во едно API повикување. Повисоко = побрзо, но повеќе меморија. Препорачано: 10-50.", + "Number of objects to process in each batch (1-5000).": "Број на објекти за обработка во секоја серија (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Број на објекти за векторизација во едно API повикување. Повисоко = побрзо, но повеќе меморија. Препорачано: 10-50.", + "Numeric Range:": "Нумерички опсег:", + "Object": "Објект", + "Object #{id}": "Објект #{id}", + "Object Collection": "Колекција на објекти", + "Object Configuration:": "Конфигурација на објект:", + "Object Count Prediction": "Предвидување на број на објекти", + "Object Extraction Mode": "Режим на извлекување на објекти", + "Object ID": "ID на објект", + "Object Management": "Управување со објекти", + "Object Text Extraction": "Извлекување на текст на објект", + "Object Vectorization": "Векторизација на објекти", + "Object Warmup": "Загревање на објекти", + "Object changes in owned objects": "Промени на објект во сопствени објекти", + "Object collection not configured": "Колекцијата на објекти не е конфигурирана", + "Object metadata incomplete for file upload": "Метаподатоците на објект се нецелосни за прикачување датотека", + "Object not found": "Објектот не е пронајден", + "Object permanently deleted": "Објектот е трајно избришан", + "Object restored successfully": "Објектот е успешно вратен", + "Object settings updated successfully": "Поставките на објектот се успешно ажурирани", + "Object sources": "Извори на објекти", + "Object successfully deleted": "Објектот е успешно избришан", + "Object successfully modified": "Објектот е успешно изменет", + "Object unlocked successfully": "Објектот е успешно отклучен", + "Object vectorization configuration saved successfully": "Конфигурацијата за векторизација на објект е успешно зачувана", + "Object vectorized successfully": "Објектот е успешно векторизиран", + "Object:": "Објект:", + "Objects": "Објекти", + "Objects Analyzed": "Анализирани објекти", + "Objects Distribution": "Распределба на објекти", + "Objects being analyzed": "Објекти во анализа", + "Objects by Register": "Објекти по регистар", + "Objects by Schema": "Објекти по шема", + "Objects deletion completed for {schema}": "Бришењето на објекти е завршено за {schema}", + "Objects deletion completed successfully": "Бришењето на објекти е успешно завршено", + "Objects in schema": "Објекти во шема", + "Objects successfully deleted": "Објектите се успешно избришани", + "Objects to Process:": "Објекти за обработка:", + "Objects to be analyzed": "Објекти за анализа", + "Objects to be deleted": "Објекти за бришење", + "Objects to be validated": "Објекти за валидација", + "Objects will be serialized as JSON text before vectorization": "Објектите ќе бидат сериализирани како JSON текст пред векторизација", + "Objects will be soft deleted and moved to the": "Објектите ќе бидат меко избришани и преместени во", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Објектите ќе бидат меко избришани (означени како избришани но задржани во базата на податоци). Тие можат да се обноват подоцна ако е потребно.", + "Old Value": "Стара вредност", + "Oldest Item (days)": "Најстара ставка (дена)", + "Ollama Chat Settings": "Поставки за Ollama разговор", + "Ollama Embedding Configuration": "Конфигурација за Ollama вградување", + "Ollama URL": "Ollama URL", + "Only With Changes": "Само со промени", + "Only members of selected groups can access this organisation": "Само членови на избраните групи можат да пристапат до оваа организација", + "Open Nextcloud App Store": "Отвори продавница за апликации Nextcloud", + "Open OpenRegister sidebar": "Отвори страничен панел на OpenRegister", + "Open Register Objects": "Отвори објекти на регистар", + "Open folder": "Отвори папка", + "Open in OpenRegister": "Отвори во OpenRegister", + "Open n8n Editor": "Отвори n8n уредник", + "OpenAI Chat Settings": "Поставки за OpenAI разговор", + "OpenAI Embedding Configuration": "Конфигурација за OpenAI вградување", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Поставки на OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Оператор-дефинирани контролни табли и закажани извештаи. Секоја контролна табла е прво-класен објект во регистарот `reports`; виџетите се декларираат во низата `widgets` на контролната табла и се рендерираат живо од агрегации / GraphQL.", + "Optimizing search performance...": "Оптимизирање на перформансите на пребарувањето...", + "Optional URL-friendly identifier": "Опционален URL-пријателски идентификатор", + "Optional description for this facet": "Опционален опис за овој фасет", + "Optional end date property": "Опционално својство за краен датум", + "Optional event description template": "Опционален шаблон за опис на настан", + "Optional location property": "Опционално својство за локација", + "Optional webhook secret for signature verification": "Опционална тајна за webhook за верификација на потпис", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Опционално: Додајте параметри на барање за филтрирање на наведената шема (на пр. status=active&type=public)", + "Or": "Или", + "Order": "Редослед", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Подредени BCP 47 јазични ознаки. Првиот јазик е стандардниот за регистарот и управува со Accept-Language резервна опција за преводливи својства.", + "Organisation": "Организација", + "Organisation Configuration": "Конфигурација на организација", + "Organisation ID": "ID на организација", + "Organisation Members": "Членови на организација", + "Organisation Statistics": "Статистика на организација", + "Organisation created successfully": "Организацијата е успешно создадена", + "Organisation membership changes": "Промени во членство во организација", + "Organisation name is required": "Името на организацијата е задолжително", + "Organisation settings saved successfully": "Поставките на организацијата се успешно зачувани", + "Organisation successfully created": "Организацијата е успешно создадена", + "Organisation successfully updated": "Организацијата е успешно ажурирана", + "Organisational measures": "Организациски мерки", + "Organisations": "Организации", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Организација", + "Organization ID (Optional)": "ID на организација (Опционално)", + "Organizations": "Организации", + "Orphaned Items": "Осиротени ставки", + "Other": "Друго", + "Output Mapping": "Излезно мапирање", + "Overview": "Преглед", + "Owner": "Сопственик", + "Owner:": "Сопственик:", + "PDF": "PDF", + "PII hits": "PII попадања", + "PII rows": "PII редови", + "Page {current} of {total}": "Страница {current} од {total}", + "Parallel Mode (Faster, more resource intensive)": "Паралелен режим (побрзо, повеќе ресурси)", + "Parallel:": "Паралелно:", + "Password": "Лозинка", + "Password changes are not supported by your authentication provider.": "Промените на лозинката не се поддржани од вашиот давател на автентикација.", + "Password for SOLR authentication (optional)": "Лозинка за автентикација со SOLR (опционално)", + "Password updated successfully": "Лозинката е успешно ажурирана", + "Path": "Патека", + "Path in repository": "Патека во репозиториум", + "Path where the configuration file will be saved in the repository": "Патека каде ќе биде зачуван конфигурацискиот фајл во репозиториумот", + "Path where the register OAS file will be saved in the repository": "Патека каде ќе биде зачуван OAS фајлот на регистарот во репозиториумот", + "Pattern (regex)": "Шаблон (regex)", + "Pattern Issue": "Проблем со шаблон", + "Pattern constraint is missing": "Ограничувањето за шаблон недостасува", + "Patterns:": "Шаблони:", + "Pending": "На чекање", + "Permanent Delete Mode": "Режим на трајно бришење", + "Permanently Delete": "Трајно избриши", + "Permanently Delete ({count})": "Трајно избриши ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Трајно избриши ги сите {count} меко избришани објекти. Ова не може да се поништи!", + "Permanently delete already soft-deleted objects": "Трајно избриши веќе меко избришани објекти", + "Permanently delete this conversation?": "Трајно да се избрише овој разговор?", + "Permission Matrix": "Матрица на дозволи", + "Person": "Лице", + "Personal Data": "Лични податоци", + "Personal Data Export": "Извоз на лични податоци", + "Persons": "Лица", + "Phone": "Телефон", + "Phone Numbers": "Телефонски броеви", + "Photos": "Фотографии", + "Please create an agent in the": "Ве молиме создадете агент во", + "Please select a user": "Ве молиме изберете корисник", + "Please select an agent to continue": "Ве молиме изберете агент за продолжување", + "Please select an organisation": "Ве молиме изберете организација", + "Please select at least one item to import": "Ве молиме изберете најмалку една ставка за увоз", + "Please select registers or schemas first": "Ве молиме прво изберете регистри или шеми", + "Please select which register and schema to use for the new object": "Ве молиме изберете кои регистар и шема да ги користите за новиот објект", + "Please try again later.": "Ве молиме обидете се повторно подоцна.", + "Please wait while we fetch your agents.": "Ве молиме почекајте додека ги превземаме вашите агенти.", + "Please wait while we fetch your applications.": "Ве молиме почекајте додека ги превземаме вашите апликации.", + "Please wait while we fetch your configurations.": "Ве молиме почекајте додека ги превземаме вашите конфигурации.", + "Please wait while we fetch your deleted items.": "Ве молиме почекајте додека ги превземаме вашите избришани ставки.", + "Please wait while we fetch your sources.": "Ве молиме почекајте додека ги превземаме вашите извори.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Ве молиме почекајте додека го трајно бришеме SOLR колекцијата. Ова може да потрае неколку моменти.", + "Polls": "Анкети", + "Popular Search Terms": "Популарни термини за пребарување", + "Port": "Порт", + "Portabiliteit (Art 20)": "Portabiliteit (чл. 20)", + "Preferences saved": "Преференциите се зачувани", + "Prerequisites": "Предуслови", + "Press Enter to send, Shift+Enter for new line": "Притиснете Enter за испраќање, Shift+Enter за нов ред", + "Press enter to send, shift+enter for new line": "Притиснете enter за испраќање, shift+enter за нов ред", + "Prettify": "Убави", + "Preview erasure": "Прегледај бришење", + "Preview:": "Преглед:", + "Previous": "Претходно", + "Private": "Приватно", + "Private Agent (Default)": "Приватен агент (стандарден)", + "Private agents": "Приватни агенти", + "Process Name (optional)": "Име на процес (опционално)", + "Processed {indexed} files, {failed} failed": "Обработени {indexed} датотеки, {failed} неуспешни", + "Processes chunks in batches with simulated parallelism.": "Обработува парчиња во серии со симулирана паралелност.", + "Processes file chunks sequentially (safest).": "Обработува парчиња датотека последователно (најбезбедно).", + "Processes objects in chunks with simulated parallelism.": "Обработува објекти во парчиња со симулирана паралелност.", + "Processes objects sequentially (safest).": "Обработува објекти последователно (најбезбедно).", + "Processing": "Обработка", + "Processing Limits": "Ограничувања на обработка", + "Processing...": "Обработување...", + "Project initialized successfully": "Проектот е успешно иницијализиран", + "Project not found. Please initialize first.": "Проектот не е пронајден. Ве молиме прво иницијализирајте.", + "Properties": "Својства", + "Property Behaviors": "Однесувања на својство", + "Property Configuration:": "Конфигурација на својство:", + "Property Filters": "Филтри за својства", + "Property Title": "Наслов на својство", + "Property Type": "Тип на својство", + "Property can be improved": "Својството може да се подобри", + "Property could not be added": "Својството не може да се додаде", + "Property could not be updated": "Својството не може да се ажурира", + "Property name of inversed relation": "Име на својство на инверзна релација", + "Property successfully added": "Својството е успешно додадено", + "Property successfully updated": "Својството е успешно ажурирано", + "Protected": "Заштитено", + "Provider": "Давател", + "Provider is required for testing": "Давателот е задолжителен за тестирање", + "Public": "Јавно", + "Public View": "Јавен преглед", + "Public views can be accessed by anyone in the system": "Јавните прегледи можат да им пристапат сите во системот", + "Publish": "Публикувај", + "Publish Configuration to GitHub": "Публикувај конфигурација на GitHub", + "Publish OAS": "Публикувај OAS", + "Publish Objects": "Публикувај објекти", + "Publish Register OAS to GitHub": "Публикувај OAS на регистар на GitHub", + "Publish all objects in this schema": "Публикувај ги сите објекти во оваа шема", + "Publish objects to make them publicly available": "Публикувај објекти за да ги направите јавно достапни", + "Published": "Публикувано", + "Published:": "Публикувано:", + "Publishing...": "Публикување...", + "Purge": "Прочисти", + "Purge Date": "Датум на прочистување", + "Purge {count} object from database": "Прочисти {count} објект од базата на податоци", + "Purge {count} objects from database": "Прочисти {count} објекти од базата на податоци", + "Purpose limitation *": "Ограничување на целта *", + "Push Notifications": "Push известувања", + "Query Complexity Distribution": "Распределба на сложеност на барање", + "Query Help": "Помош за барање", + "Query parameter is required": "Параметарот на барање е задолжителен", + "Quota": "Квота", + "RAG": "RAG", + "RAG Configuration": "RAG конфигурација", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Поставките на RBAC се успешно ажурирани", + "Rate limits cleared successfully": "Ограничувањата по стапка се успешно исчистени", + "Raw Changes Data": "Необработени податоци за промени", + "Re-vectorize on object update": "Повторно векторизирај при ажурирање на објект", + "Read": "Читај", + "Real-time push notification status via notify_push": "Статус на push известување во реално време преку notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push известувањата во реално време се активни. Поврзаните клиенти добиваат тренутни ажурирања кога објектите се создаваат, ажурираат или бришат.", + "Realtime push active": "Push во реално време е активен", + "Realtime push not available — the notify_push app is not installed": "Push во реално време не е достапен — апликацијата notify_push не е инсталирана", + "Reason": "Причина", + "Reason (optional)": "Причина (опционално)", + "Rebase operation completed successfully": "Операцијата rebase е успешно завршена", + "Rebase operation failed: {error}": "Операцијата rebase не успеа: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Препораки:", + "Recommended Type:": "Препорачан тип:", + "Recommended: 5 sources": "Препорачано: 5 извори", + "Refresh": "Освежи", + "Refresh Data": "Освежи податоци", + "Refresh Stats": "Освежи статистика", + "Refresh Workflows": "Освежи работни текови", + "Refresh dashboard": "Освежи контролна табла", + "Refresh database info": "Освежи информации за базата на податоци", + "Register": "Регистар", + "Register #{id}": "Регистар #{id}", + "Register / Schema": "Регистар / Шема", + "Register ID": "ID на регистар", + "Register Objects": "Објекти на регистар", + "Register Overview": "Преглед на регистар", + "Register Statistics": "Статистика на регистар", + "Register Totals": "Вкупно во регистар", + "Register and schema are required": "Регистарот и шемата се задолжителни", + "Register annotation": "Анотација на регистар", + "Register depublished successfully": "Регистарот е успешно отпубликуван", + "Register not found": "Регистарот не е пронајден", + "Register objects": "Објекти на регистар", + "Register objects deletion completed successfully": "Бришењето на објекти на регистар е успешно завршено", + "Register published successfully": "Регистарот е успешно публикуван", + "Register/Schema Usage": "Употреба на регистар/шема", + "Register:": "Регистар:", + "Registers": "Регистри", + "Reindex": "Реиндексирај", + "Reindex completed": "Реиндексирањето е завршено", + "Reindex completed successfully": "Реиндексирањето е успешно завршено", + "Reindex completed! Processed {count} objects in {duration}s": "Реиндексирањето е завршено! Обработени {count} објекти за {duration}s", + "Reindex failed": "Реиндексирањето не успеа", + "Reindex failed: %s": "Реиндексирањето не успеа: %s", + "Reject": "Одбиј", + "Rejected": "Одбиено", + "Related Cases": "Поврзани случаи", + "Relations": "Релации", + "Remote Version": "Далечинска верзија", + "Remove": "Отстрани", + "Remove Deck card": "Отстрани Deck картичка", + "Remove avatar": "Отстрани аватар", + "Remove contact": "Отстрани контакт", + "Remove filter": "Отстрани филтер", + "Remove from favorites": "Отстрани од омилени", + "Remove from list": "Отстрани од листа", + "Remove group": "Отстрани група", + "Remove link": "Отстрани врска", + "Remove link to {name}": "Отстрани врска кон {name}", + "Remove link to {name}?": "Отстрани врска кон {name}?", + "Remove link to {title}": "Отстрани врска кон {title}", + "Remove oneOf entry": "Отстрани oneOf запис", + "Remove property after write-back": "Отстрани својство по запишување назад", + "Remove {title}": "Отстрани {title}", + "Remove {userId} from this organisation?": "Отстрани {userId} од оваа организација?", + "Removed": "Отстрането", + "Removed from favorites": "Отстрането од омилени", + "Removing schema {schema}...": "Отстранување на шема {schema}...", + "Rename Conversation": "Преименувај разговор", + "Rename conversation": "Преименувај разговор", + "Replicas": "Реплики", + "Reports": "Извештаи", + "Repository": "Репозиториум", + "Request Body": "Тело на барање", + "Request Data": "Податоци за барање", + "Request Details": "Детали на барање", + "Request Quota (per day)": "Квота на барања (по ден)", + "Request account deactivation": "Побарај деактивација на сметката", + "Request account deactivation. This will notify administrators for review.": "Побарај деактивација на сметката. Ова ќе ги извести администраторите за преглед.", + "Request body must be JSON with a \"query\" field": "Телото на барањето мора да биде JSON со поле \"query\"", + "Request timeout in seconds": "Временско ограничување на барање во секунди", + "Requested at": "Побарано на", + "Required": "Задолжително", + "Required Token Scopes": "Задолжителни опфати на токен", + "Required field": "Задолжително поле", + "Required status is inconsistent": "Задолжителниот статус е неконзистентен", + "Requires Dolphin AI for OCR text extraction": "Потребен е Dolphin AI за OCR извлекување на текст", + "Rerun Search": "Повторно пребарај", + "Rerunning search: {searchTerm}": "Повторно пребарување: {searchTerm}", + "Reset Changes": "Ресетирај промени", + "Reset Filters": "Ресетирај филтри", + "Resource Quotas": "Квоти на ресурси", + "Response Body": "Тело на одговор", + "Response Details": "Детали на одговор", + "Response Time:": "Време на одговор:", + "Restore": "Врати", + "Restore conversation": "Врати разговор", + "Restore or permanently delete items": "Врати или трајно избриши ставки", + "Restore {count} object": "Врати {count} објект", + "Restore {count} objects": "Врати {count} објекти", + "Result Count Range": "Опсег на број на резултати", + "Results": "Резултати", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Период на задржување (ISO-8601 траење, на пр. P10Y, P30D)", + "Retention settings updated successfully": "Поставките за задржување се успешно ажурирани", + "Retrieve all objects for this schema": "Превземи ги сите објекти за оваа шема", + "Retry": "Обиди се повторно", + "Retry Failed Extractions": "Обиди се повторно со неуспешни извлекувања", + "Retry Policy": "Политика за повторување", + "Retry completed": "Повторувањето е завршено", + "Retry upload": "Обиди се повторно со прикачување", + "Retrying...": "Повторно обидување...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Прегледајте ги избраните објекти подолу. Можете да отстраните кои било објекти кои не сакате да ги избришете со кликање на копчето за отстранување.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Прегледајте ги избраните објекти подолу. Можете да отстраните кои било објекти кои не сакате да ги вклучите.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Прегледајте ги избраните објекти подолу. Можете да отстраните кои било објекти кои не сакате трајно да ги избришете со кликање на копчето за отстранување. Оваа акција не може да се поништи.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Прегледајте ги избраните објекти подолу. Можете да отстраните кои било објекти кои не сакате да ги вратите со кликање на копчето за отстранување. Објектите ќе бидат вратени на нивната оригинална локација.", + "Revoke": "Отповикај", + "Right": "Десно", + "Risk Level": "Ниво на ризик", + "Risk level": "Ниво на ризик", + "Roles and Permissions": "Улоги и дозволи", + "Roles updated successfully": "Улогите се успешно ажурирани", + "Run compliance scan": "Стартувај скенирање за усогласеност", + "SOLR Connection Settings": "SOLR поставки за поврзување", + "SOLR Index Warmup": "Загревање на SOLR индекс", + "SOLR Version:": "SOLR верзија:", + "SOLR actions menu": "Мени со акции за SOLR", + "SOLR base path (usually /solr)": "Основна патека за SOLR (обично /solr)", + "SOLR collection deleted successfully": "SOLR колекцијата е успешно избришана", + "SOLR connection test failed: {error}": "Тестот на SOLR врска не успеа: {error}", + "SOLR connection test successful!": "Тестот на SOLR врска е успешен!", + "SOLR index warmup completed successfully": "Загревањето на SOLR индексот е успешно завршено", + "SOLR is disabled": "SOLR е оневозможен", + "SOLR is not available or not configured": "SOLR не е достапен или не е конфигуриран", + "SOLR logging disabled": "SOLR логирањето е оневозможено", + "SOLR logging enabled": "SOLR логирањето е овозможено", + "SOLR search disabled": "SOLR пребарувањето е оневозможено", + "SOLR search enabled": "SOLR пребарувањето е овозможено", + "SOLR server hostname or IP address": "Hostname или IP адреса на SOLR серверот", + "SOLR server port number (optional, defaults to 8983)": "Број на порт на SOLR серверот (опционално, стандарден е 8983)", + "SOLR settings updated successfully": "Поставките на SOLR се успешно ажурирани", + "SOLR setup completed successfully": "Поставката на SOLR е успешно завршена", + "SOLR setup completed successfully!": "Поставката на SOLR е успешно завршена!", + "SOLR setup error: %s": "Грешка при поставка на SOLR: %s", + "SOLR setup failed": "Поставката на SOLR не успеа", + "SOLR setup failed - check logs": "Поставката на SOLR не успеа - проверете ги логовите", + "SOLR setup failed. Please check the configuration and try again.": "Поставката на SOLR не успеа. Ве молиме проверете ја конфигурацијата и обидете се повторно.", + "SOLR setup failed: {error}": "Поставката на SOLR не успеа: {error}", + "SOLR warmup failed: {error}": "Загревањето на SOLR не успеа: {error}", + "Sample Values:": "Примерни вредности:", + "Save": "Зачувај", + "Save Configuration": "Зачувај конфигурација", + "Save Connection Settings": "Зачувај поставки за поврзување", + "Save Roles": "Зачувај улоги", + "Save Settings": "Зачувај поставки", + "Save changes": "Зачувај промени", + "Save current search as view": "Зачувај тековно пребарување како преглед", + "Save labels": "Зачувај ознаки", + "Save tags for {name}": "Зачувај ознаки за {name}", + "Save the organisation first to manage users.": "Прво зачувајте ја организацијата за да управувате со корисниците.", + "Saved Views": "Зачувани прегледи", + "Saving...": "Зачувување...", + "Scheduled": "Закажано", + "Schema": "Шема", + "Schema ID": "ID на шема", + "Schema Name": "Име на шема", + "Schema Statistics": "Статистика на шема", + "Schema annotation": "Анотација на шема", + "Schema configuration updated successfully for {schema}": "Конфигурацијата на шемата е успешно ажурирана за {schema}", + "Schema depublished successfully": "Шемата е успешно отпубликувана", + "Schema objects deletion completed successfully": "Бришењето на објекти на шема е успешно завршено", + "Schema published successfully": "Шемата е успешно публикувана", + "Schema reference ($ref)": "Референца на шема ($ref)", + "Schema successfully created": "Шемата е успешно создадена", + "Schema successfully updated": "Шемата е успешно ажурирана", + "Schema {schema} removed successfully": "Шемата {schema} е успешно отстранета", + "Schema:": "Шема:", + "Schemas": "Шеми", + "Scheme": "Шема", + "Search": "Пребарај", + "Search / Views": "Пребарување / Прегледи", + "Search / views": "Пребарување / прегледи", + "Search Activity": "Активност на пребарување", + "Search Analytics": "Аналитика на пребарување", + "Search Configuration": "Конфигурација на пребарување", + "Search Entities": "Пребарувај ентитети", + "Search Files": "Пребарувај датотеки", + "Search GitHub": "Пребарувај GitHub", + "Search GitLab": "Пребарувај GitLab", + "Search Method": "Метод на пребарување", + "Search Mode": "Режим на пребарување", + "Search Query": "Барање за пребарување", + "Search Results": "Резултати на пребарување", + "Search Term": "Термин за пребарување", + "Search Trail Details": "Детали на траг на пребарување", + "Search Trail Management": "Управување со траги на пребарување", + "Search Trail Statistics": "Статистика на траги на пребарување", + "Search Trails": "Траги на пребарување", + "Search Views": "Пребарувај прегледи", + "Search Webhooks": "Пребарувај Webhooks", + "Search agents...": "Пребарувај агенти...", + "Search applications...": "Пребарувај апликации...", + "Search by file name or path": "Пребарувај по име на датотека или патека", + "Search by name or URL": "Пребарувај по име или URL", + "Search by title or UUID...": "Пребарувај по наслов или UUID...", + "Search by value": "Пребарувај по вредност", + "Search configurations": "Пребарувај конфигурации", + "Search data sources...": "Пребарувај извори на податоци...", + "Search groups...": "Пребарувај групи...", + "Search in Database Objects": "Пребарувај во објекти на база на податоци", + "Search in Files": "Пребарувај во датотеки", + "Search in Objects": "Пребарувај во објекти", + "Search objects": "Пребарувај објекти", + "Search objects...": "Пребарувај објекти...", + "Search property names...": "Пребарувај имиња на својства...", + "Search registers...": "Пребарувај регистри...", + "Search schemas...": "Пребарувај шеми...", + "Search trail deletion not implemented yet": "Бришењето на траги на пребарување сè уште не е имплементирано", + "Search views...": "Пребарувај прегледи...", + "Search {name}...": "Пребарувај {name}...", + "Searching...": "Пребарување...", + "Secret": "Тајна", + "Secret key for HMAC signature generation (optional)": "Таен клуч за генерирање на HMAC потпис (опционално)", + "Security": "Безбедност", + "See organisations as a table": "Погледни организации во табеларен преглед", + "See organisations as cards": "Погледни организации како картички", + "Select AI Agent": "Избери AI агент", + "Select Active Organisation": "Избери активна организација", + "Select All": "Избери сè", + "Select ConfigSet": "Избери ConfigSet", + "Select File Types to Vectorize:": "Избери типови датотеки за векторизација:", + "Select HTTP method": "Избери HTTP метод", + "Select Register and Schema": "Избери регистар и шема", + "Select Views to Vectorize:": "Избери прегледи за векторизација:", + "Select a Nextcloud group to add": "Избери Nextcloud група за додавање", + "Select a branch": "Избери гранка", + "Select a date property": "Избери својство за датум", + "Select a model or type a custom model name": "Изберете модел или внесете прилагодено име на модел", + "Select a register": "Избери регистар", + "Select a register...": "Избери регистар...", + "Select a repository": "Избери репозиториум", + "Select a repository you have write access to": "Изберете репозиториум до кој имате пристап за запишување", + "Select a schema": "Избери шема", + "Select a schema...": "Избери шема...", + "Select a user": "Избери корисник", + "Select agent type": "Избери тип на агент", + "Select all entities": "Избери ги сите ентитети", + "Select an AI Agent": "Избери AI агент", + "Select an AI agent to begin chatting with your data.": "Изберете AI агент за да започнете разговор со вашите податоци.", + "Select an email to see linked objects": "Изберете е-пошта за да ги видите поврзаните објекти", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Изберете организација и корисник за нивно додавање како член. Пребарувајте организации по име.", + "Select backend": "Избери backend", + "Select chat model": "Избери модел за разговор", + "Select collection for files": "Избери колекција за датотеки", + "Select collection for objects": "Избери колекција за објекти", + "Select configuration type...": "Избери тип на конфигурација...", + "Select default organisation": "Избери стандардна организација", + "Select entity {value}": "Избери ентитет {value}", + "Select event to listen to...": "Избери настан за слушање...", + "Select fields to display": "Избери полиња за прикажување", + "Select files with the correct extension": "Изберете датотеки со правилна екстензија", + "Select group": "Избери група", + "Select groups (optional)": "Избери групи (опционално)", + "Select groups to notify...": "Избери групи за известување...", + "Select groups with access to this agent": "Избери групи со пристап до овој агент", + "Select groups...": "Избери групи...", + "Select model": "Избери модел", + "Select one or more registers": "Избери еден или повеќе регистри", + "Select one or more schemas": "Избери една или повеќе шеми", + "Select options...": "Избери опции...", + "Select or create labels, or select \"No label\" to add files": "Изберете или создадете ознаки, или изберете \"Без ознака\" за додавање датотеки", + "Select owner application (optional)...": "Избери сопственичка апликација (опционално)...", + "Select period": "Избери период", + "Select property to send as payload": "Избери својство за испраќање како payload", + "Select provider": "Избери давател", + "Select registers and schemas to save a view": "Изберете регистри и шеми за зачувување на преглед", + "Select retry policy": "Избери политика за повторување", + "Select role": "Избери улога", + "Select schemas (empty = all schemas)": "Избери шеми (празно = сите шеми)", + "Select schemas to warm up": "Избери шеми за загревање", + "Select search mode": "Избери режим на пребарување", + "Select source type...": "Избери тип на извор...", + "Select target type": "Избери целен тип", + "Select the branch to publish to": "Избери гранка за публикување", + "Select the event this webhook should listen to": "Избери настан кој овој webhook треба да го слуша", + "Select views to filter data (optional)": "Избери прегледи за филтрирање на податоци (опционално)", + "Select views to limit which data the agent can access": "Избери прегледи за ограничување на кои податоци агентот може да пристапи", + "Select views to vectorize:": "Избери прегледи за векторизација:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Изберете кои Nextcloud групи се достапни за оваа организација. Корисниците во овие групи ќе имаат пристап до ресурсите на организацијата.", + "Select which data views the AI can search": "Избери кои прегледи на податоци AI може да ги пребарува", + "Select which property from the event should be used as the webhook payload data": "Изберете кое својство од настанот треба да се користи како payload податоци на webhook", + "Select which tools the AI can use to perform actions": "Изберете кои алатки AI може да ги користи за извршување акции", + "Select {fieldName} values": "Избери вредности на {fieldName}", + "Selected": "Избрано", + "Selected Groups": "Избрани групи", + "Selected Objects ({count})": "Избрани објекти ({count})", + "Selected audit trails deleted successfully": "Избраните ревизорски траги се успешно избришани", + "Selected search trails deleted successfully": "Избраните траги на пребарување се успешно избришани", + "Selected users can access this view": "Избраните корисници можат да пристапат до овој преглед", + "Send additional feedback": "Испрати дополнителни повратни информации", + "Send as CloudEvent": "Испрати како CloudEvent", + "Send as cloudevent": "Испрати како cloudevent", + "Sensitive PII": "Чувствително PII", + "Serial Mode (Safer, slower)": "Сериски режим (побезбеден, побавен)", + "Serial:": "Сериски:", + "Server Configuration": "Конфигурација на сервер", + "Server Information": "Информации за сервер", + "Set as Active": "Постави како активно", + "Set as active organisation": "Постави како активна организација", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Поставете ограничувања за употреба на API и трошење на токени. Користете 0 за неограничени ресурси.", + "Settings": "Поставки", + "Settings updated successfully": "Поставките се успешно ажурирани", + "Shards": "Шардови", + "Share": "Сподели", + "Share with Groups": "Сподели со групи", + "Share with Users": "Сподели со корисници", + "Show": "Прикажи", + "Show Filters": "Прикажи филтри", + "Show less": "Прикажи помалку", + "Show only entries with changes": "Прикажи само записи со промени", + "Showing {count} dashboard(s)": "Прикажување {count} контролна(и) табла(и)", + "Showing {count} processing activities": "Прикажување {count} активности на обработка", + "Showing {showing} of {total} agents": "Прикажување {showing} од {total} агенти", + "Showing {showing} of {total} applications": "Прикажување {showing} од {total} апликации", + "Showing {showing} of {total} audit trail entries": "Прикажување {showing} од {total} записи на ревизорски траг", + "Showing {showing} of {total} configurations": "Прикажување {showing} од {total} конфигурации", + "Showing {showing} of {total} deleted items": "Прикажување {showing} од {total} избришани ставки", + "Showing {showing} of {total} entities": "Прикажување {showing} од {total} ентитети", + "Showing {showing} of {total} files": "Прикажување {showing} од {total} датотеки", + "Showing {showing} of {total} log entries": "Прикажување {showing} од {total} записи на лог", + "Showing {showing} of {total} search trail entries": "Прикажување {showing} од {total} записи на траги на пребарување", + "Showing {showing} of {total} sources": "Прикажување {showing} од {total} извори", + "Showing {showing} of {total} templates": "Прикажување {showing} од {total} шаблони", + "Showing {showing} of {total} webhooks": "Прикажување {showing} од {total} webhooks", + "Showing {shown} of {total} organisations": "Прикажување {shown} од {total} организации", + "Simple": "Едноставно", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Едноставни барања: Основни текстуални пребарувања со минимални параметри (на пр. единечен термин за пребарување, без напредни филтри)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Едноставни барања: основни текстуални пребарувања со минимални параметри (на пр. единечен термин за пребарување, без напредни филтри)", + "Size": "Големина", + "Slug": "Slug", + "Soft Delete Mode": "Режим на меко бришење", + "Soft Deleted Items": "Меко избришани ставки", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Меко избриши ги сите објекти за оваа шема ({active} активни, {deleted} веќе избришани)", + "SolrCloud mode enabled": "SolrCloud режимот е овозможен", + "Something went wrong while adding files": "Нешто тргна наопаку при додавање датотеки", + "Source": "Извор", + "Source successfully updated": "Изворот е успешно ажуриран", + "Sources": "Извори", + "Sources:": "Извори:", + "Special Rights": "Специјални права", + "Standalone SOLR mode": "Самостоен SOLR режим", + "Start Conversation": "Започни разговор", + "Start Date Field": "Поле за почетен датум", + "Start Vectorization": "Започни векторизација", + "Start a conversation": "Започни разговор", + "Starting names cache warmup...": "Стартување на загревање на кешот за имиња...", + "Starting validation for {schema}...": "Стартување на валидација за {schema}...", + "Starting {action} for {schema}...": "Стартување на {action} за {schema}...", + "Starting...": "Стартување...", + "Statistics": "Статистика", + "Statistics and Metrics": "Статистика и метрики", + "Statistics refreshed": "Статистиката е освежена", + "Stats calculation not yet implemented": "Пресметката на статистика сè уште не е имплементирана", + "Status": "Статус", + "Status Code": "Код на статус", + "Step": "Чекор", + "Storage": "Складиште", + "Storage Quota (MB)": "Квота за складиште (MB)", + "String Constraints": "Стринг ограничувања", + "Subject identifier (email, BSN, name, etc.)": "Идентификатор на субјект (е-пошта, BSN, име, итн.)", + "Subscribe": "Претплати се", + "Subscribed": "Претплатено", + "Success": "Успех", + "Success Rate": "Стапка на успех", + "Success Status": "Статус на успех", + "Successful": "Успешно", + "Successfully added user to organisation: {name}": "Корисникот е успешно додаден во организацијата: {name}", + "Successfully cleared {count} audit trails": "Успешно исчистени {count} ревизорски траги", + "Successfully cleared {count} blob storage objects": "Успешно исчистени {count} blob складиштни објекти", + "Successfully cleared {count} search trails": "Успешно исчистени {count} траги на пребарување", + "Successfully created {count} SOLR fields": "Успешно создадени {count} SOLR полиња", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Успешно избришани {count} вградувања. Ве молиме повторно векторизирајте ги вашите податоци.", + "Successfully deleted {count} objects for {schema}": "Успешно избришани {count} објекти за {schema}", + "Successfully fixed {count} SOLR field configurations": "Успешно поправени {count} конфигурации на SOLR полиња", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Успешно увезено: {registers} регистри, {schemas} шеми, {objects} објекти", + "Successfully joined organisation": "Успешно зачленето во организацијата", + "Successfully permanently deleted {count} objects": "Успешно трајно избришани {count} објекти", + "Successfully processed {count} files!": "Успешно обработени {count} датотеки!", + "Successfully restored {count} object": "Успешно вратен {count} објект", + "Successfully restored {count} objects": "Успешно вратени {count} објекти", + "Successfully saved configuration for {count} facets!": "Успешно зачувана конфигурација за {count} фасети!", + "Supported File Types": "Поддржани типови датотеки", + "Switch Active Organisation": "Промени активна организација", + "Switch Organisation": "Промени организација", + "Sync Table": "Синхронизирај табела", + "Synchronization Settings": "Поставки за синхронизација", + "Syncing magic table for {schema}...": "Синхронизирање на магична табела за {schema}...", + "Syncs": "Синхронизации", + "System Default": "Системски стандард", + "System Prompt": "Системска порака", + "System announcements": "Системски известувања", + "Table": "Табела", + "Target ID": "ID на цел", + "Target Type*": "Целен тип*", + "Technical Description": "Техничка опис", + "Technical Details": "Технички детали", + "Technical description for developers and administrators": "Технички опис за програмери и администратори", + "Technical measures": "Технички мерки", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Температура", + "Templates": "Шаблони", + "Test": "Тест", + "Test Chat": "Тест разговор", + "Test Connection": "Тест врска", + "Test Embedding": "Тест вградување", + "Test endpoint executed successfully": "Тестот на крајна точка е успешно извршен", + "Test webhook delivered successfully": "Тест webhook е успешно доставен", + "Test webhook delivery failed": "Доставата на тест webhook не успеа", + "Test webhook sent successfully": "Тест webhook е успешно испратен", + "Testing...": "Тестирање...", + "Text Chunk #{id}": "Текстуално парче #{id}", + "Text Extraction": "Извлекување на текст", + "Text Extraction Engine": "Мотор за извлекување на текст", + "Text chunks": "Текстуални парчиња", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Текстуалните парчиња се генерираат при извлекување на датотека и се зачувуваат во базата на податоци. Векторизацијата ги чита овие претходно поделени датотеки и ги конвертира во вградувања.", + "Text extracted successfully": "Текстот е успешно извлечен", + "Text extraction disabled": "Извлекувањето на текст е оневозможено", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Апликацијата Nextcloud Календар не е инсталирана или овозможена на овој сервер.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Апликацијата Nextcloud Контакти не е инсталирана или овозможена на овој сервер.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Апликацијата Nextcloud Deck не е инсталирана или овозможена на овој сервер.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Апликацијата Nextcloud Пошта не е инсталирана или овозможена на овој сервер.", + "The URL where webhook events will be sent": "URL каде ќе бидат испратени настаните на webhook", + "The requested conversation does not exist": "Бараниот разговор не постои", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Бараната контролна табла не може да се вчита. Проверете ја URL или изберете друга контролна табла од листата.", + "The saved view and all its search configuration": "Зачуваниот преглед и целата негова конфигурација за пребарување", + "There are no audit trail entries matching your current filters.": "Нема записи на ревизорски траг кои одговараат на вашите тековни филтри.", + "There are no deleted items matching your current filters.": "Нема избришани ставки кои одговараат на вашите тековни филтри.", + "There are no search trail entries matching your current filters.": "Нема записи на траги на пребарување кои одговараат на вашите тековни филтри.", + "There are no webhook log entries matching your filters.": "Нема записи на лог на webhook кои одговараат на вашите филтри.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Тие ќе бидат задржани според конфигурираниот период на задржување на нивната шема и автоматски трајно избришани кога ќе истече периодот на задржување. Периодот на задржување е конфигурабилен по шема и може да се најде во поставките на шемата.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Тие ќе бидат задржани според конфигурираниот период на задржување на нивната шема, а потоа трајно отстранети.", + "This action cannot be undone.": "Оваа акција не може да се поништи.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Оваа акција не може да се поништи. Проверете дека ниедна колекција не го користи овој ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Оваа акција не може да се поништи. Проверете дека ниедна колекција не го користи овој configset.", + "This action will submit a deactivation request to your administrators.": "Оваа акција ќе поднесе барање за деактивација кон вашите администратори.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Овој агент нема конфигурабилни прегледи или алатки, но сè уште можете да ги прилагодите поставките за RAG подолу.", + "This analysis may take some time": "Оваа анализа може да потрае некое време", + "This audit trail entry does not contain any change information.": "Овој запис на ревизорски траг не содржи информации за промени.", + "This configuration has no data defined.": "Оваа конфигурација нема дефинирани податоци.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Оваа крајна точка е застарена. Користете крајни точки базирани на парчиња наместо тоа.", + "This entity has no relations to objects or files": "Овој ентитет нема релации кон објекти или датотеки", + "This is a test webhook from OpenRegister": "Ова е тест webhook од OpenRegister", + "This is not valid JSON": "Ова не е валиден JSON", + "This key already exists on this schema": "Овој клуч веќе постои во оваа шема", + "This schema must use Magic Table configuration to sync": "Оваа шема мора да користи конфигурација на магична табела за синхронизација", + "This schema must use magic table configuration to sync": "Оваа шема мора да користи конфигурација на магична табела за синхронизација", + "This source has no associated registers.": "Овој извор нема поврзани регистри.", + "This token will only be shown once. Copy it now.": "Овој токен ќе биде прикажан само еднаш. Копирајте го сега.", + "This will copy the _default ConfigSet with the new name": "Ова ќе го копира ConfigSet _default со новото име", + "This will copy the _default configset with the new name": "Ова ќе го копира configset _default со новото име", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Ова трајно ќе ги избрише СИТЕ вградувања (вектори) од базата на податоци. Ќе треба повторно да ги векторизирате сите објекти и датотеки. Оваа акција не може да се поништи.\n\nДали сте сигурни дека сакате да продолжите?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Ова трајно ќе ги избрише СИТЕ вградувања (вектори) од базата на податоци. Ќе треба повторно да ги векторизирате сите објекти и датотеки. Оваа акција не може да се поништи.\\n\\nДали сте сигурни дека сакате да продолжите?", + "This will permanently delete:": "Ова трајно ќе ги избрише:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Ова трајно ќе отстрани {count} веќе меко избришани објекти од базата на податоци. Оваа акција не може да се поништи.", + "Timeout (seconds)": "Временско ограничување (секунди)", + "Timestamp": "Временска ознака", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Совет: Овозможете само прегледи кои имаат потреба од семантичко пребарување за да ги минимизирате трошоците за вградување. Едноставните табели за пребарување ретко имаат потреба од векторизација.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Совет: овозможете само прегледи кои имаат потреба од семантичко пребарување за да ги минимизирате трошоците за вградување. Едноставните табели за пребарување ретко имаат потреба од векторизација.", + "Title": "Наслов", + "Title *": "Наслов *", + "Title Template": "Шаблон за наслов", + "Title*": "Наслов*", + "To (e.g. -1 year or 2025-12-31)": "До (на пр. -1 year или 2025-12-31)", + "To Date": "До датум", + "To add files larger than or equal to 512MB, go to the": "За да додадете датотеки поголеми или еднакви на 512MB, одете во", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "За да ги прилагодите големината на парчиња или стратегијата, одете во Конфигурација на датотека → Ограничувања на обработка.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "За да ги прилагодите големината на парчиња или стратегијата, одете во конфигурација на датотека → ограничувања на обработка.", + "To change the embedding provider or model, go to LLM Configuration.": "За да го промените давателот или моделот на вградување, одете во LLM конфигурација.", + "To change the embedding provider or model, go to LLM configuration.": "За да го промените давателот или моделот на вградување, одете во LLM конфигурација.", + "To date": "До датум", + "Toggle search sidebar": "Вклучи/исклучи страничен панел за пребарување", + "Token Created": "Токенот е создаден", + "Token Quota (per request)": "Квота на токени (по барање)", + "Token copied to clipboard": "Токенот е копиран во клипборд", + "Token name": "Име на токен", + "Token revoked": "Токенот е отповикан", + "Tools": "Алатки", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Алатките се извршуваат со стандардните кориснички дозволи на агентот кога нема активна кориснички сесија (на пр. cron задачи). Конфигурирајте го стандардниот корисник во јазичето Поставки.", + "Tools respect the agent's own role and organization boundaries.": "Алатките го почитуваат сопствената улога и границите на организацијата на агентот.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Алатките ги почитуваат прегледите, дозволите и границите на организацијата на агентот.", + "Top Deleters": "Врвни бришачи", + "Total": "Вкупно", + "Total Audit Trails": "Вкупни ревизорски траги", + "Total Chunks Available:": "Вкупни достапни парчиња:", + "Total Deleted Items": "Вкупно избришани ставки", + "Total Members": "Вкупни членови", + "Total Objects": "Вкупни објекти", + "Total Objects in Database:": "Вкупни објекти во базата на податоци:", + "Total Organisations": "Вкупни организации", + "Total Results": "Вкупни резултати", + "Total Searches": "Вкупни пребарувања", + "Total Size": "Вкупна големина", + "Total Storage": "Вкупно складиште", + "Total events": "Вкупни настани", + "Total:": "Вкупно:", + "Totals": "Вкупно", + "Try searching by UUID or with different keywords": "Обидете се да пребарувате по UUID или со различни клучни зборови", + "Type": "Тип", + "Type \"DELETE COLLECTION\" to confirm:": "Напишете \"DELETE COLLECTION\" за потврда:", + "Type Issue": "Проблем со тип", + "Type Variations:": "Варијации на тип:", + "Type filter (optional, e.g. email)": "Филтер по тип (опционално, на пр. е-пошта)", + "Type must be \"positive\" or \"negative\"": "Типот мора да биде \"позитивен\" или \"негативен\"", + "Type to search for groups": "Напишете за пребарување групи", + "Type to search for objects...": "Напишете за пребарување објекти...", + "Type to search for organisations": "Напишете за пребарување организации", + "Type to search for users": "Напишете за пребарување корисници", + "Type to search groups...": "Напишете за пребарување групи...", + "Type to search users...": "Напишете за пребарување корисници...", + "Type to search...": "Напишете за пребарување...", + "Type your username to confirm": "Напишете го вашето корисничко име за потврда", + "URL": "URL", + "URL where Ollama is running": "URL каде работи Ollama", + "UUID copied to clipboard": "UUID копиран во клипборд", + "UUID:": "UUID:", + "Unchanged": "Непроменето", + "Unique Search Terms": "Уникатни термини за пребарување", + "Unique Terms": "Уникатни термини", + "Unknown": "Непознато", + "Unknown Object": "Непознат објект", + "Unknown Register": "Непознат регистар", + "Unknown Schema": "Непозната шема", + "Unknown error": "Непозната грешка", + "Unknown widget type: {type}": "Непознат тип на виџет: {type}", + "Unlink email": "Одврзи е-пошта", + "Unlink event": "Одврзи настан", + "Unnamed Object": "Неименуван објект", + "Untitled": "Без наслов", + "Untitled View": "Поглед без наслов", + "Update": "Ажурирај", + "Update Operations": "Операции за ажурирање", + "Update available: {local} → {remote}": "Достапно ажурирање: {local} → {remote}", + "Update configuration: ...": "Ажурирај конфигурација: ...", + "Update register OAS: ...": "Ажурирај регистар OAS: ...", + "Update vectors when object data changes (recommended for accurate search)": "Ажурирај вектори кога се менуваат податоците на објектот (препорачано за прецизно пребарување)", + "Updated": "Ажурирано", + "Updated At": "Ажурирано на", + "Updated:": "Ажурирано:", + "Upload new avatar": "Прикачи нов аватар", + "Uploading configuration sets...": "Се прикачуваат конфигурациски множества...", + "Uptime:": "Работно време:", + "Url": "Url", + "Usage Count": "Број на употреби", + "Use AI agents for processing and analysis": "Користи AI агенти за обработка и анализа", + "Use Large Language Model features": "Користи функции на LLM", + "Use SolrCloud with Zookeeper for distributed search": "Користи SolrCloud со Zookeeper за дистрибуирано пребарување", + "Use SolrCloud with zookeeper for distributed search": "Користи SolrCloud со Zookeeper за дистрибуирано пребарување", + "Use VNG APIs instead of MongoDB.": "Користи VNG API-и наместо MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Користи стандардни опсези (Последните 7/30/90 дена, Минатата година, Постари)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Користи надворешно складиште (пр. MongoDB) наместо внатрешното Nextcloud складиште.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Користи филтри за да ги стесните записите на ревизиската патека по регистар, шема, тип на акција, корисник, опсег на датуми или ID на објект.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Користи филтри за да ги стесните избришаните ставки по регистар, шема, датум на бришење или корисник кој ги избришал.", + "Use filters to narrow down entities by type or category.": "Користи филтри за да ги стесните ентитетите по тип или категорија.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Користи филтри за да ги стесните записите на патеката за пребарување по регистар, шема, статус на успех, корисник, опсег на датуми, термини за пребарување или метрики за перформанси.", + "Used By Collections": "Користено од колекции", + "Used by": "Користено од", + "User": "Корисник", + "User Agent Statistics": "Статистики за корисничкиот агент", + "User Description": "Опис на корисник", + "User Quota": "Квота на корисник", + "User rate limits cleared successfully": "Ограничувањата на стапка на корисникот се успешно исчистени", + "User removed successfully": "Корисникот е успешно отстранет", + "User-friendly description shown in forms and help text": "Описот прифатлив за корисникот прикажан во формулари и текст за помош", + "Username": "Корисничко ime", + "Username for SOLR authentication (optional)": "Корисничко ime за SOLR автентикација (опционално)", + "Users": "Корисници", + "Uses": "Употреби", + "Using Pre-Generated Chunks": "Користење на претходно генерирани парчиња", + "Validate": "Валидирај", + "Validate Objects": "Валидирај објекти", + "Validation completed for {schema}": "Валидацијата е завршена за {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Валидацијата е завршена за {schema}: {processed} обработени, {updated} ажурирани, {failed} неуспешни", + "Validation completed successfully": "Валидацијата е успешно завршена", + "Validation failed": "Валидацијата не успеа", + "Value": "Вредност", + "Value list (split on ,)": "Листа на вредности (поделена со ,)", + "Vector Dimensions": "Векторски димензии", + "Vector Search Backend": "Позадина за векторско пребарување", + "Vector field: _embedding_": "Векторско поле: _embedding_", + "Vectorization Triggers": "Активатори за векторизација", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Векторизацијата е завршена: {vectorized} објекти векторизирани, {failed} неуспешни", + "Vectorization failed: {error}": "Векторизацијата не успеа: {error}", + "Vectorize All Files": "Векторизирај ги сите датотеки", + "Vectorize All Objects": "Векторизирај ги сите објекти", + "Vectorize all file types": "Векторизирај ги сите типови датотеки", + "Vectorize all views": "Векторизирај ги сите прегледи", + "Vectorize on object creation": "Векторизирај при создавање на објект", + "Vectors will be stored in your existing object and file collections": "Векторите ќе бидат зачувани во вашите постоечки колекции на објекти и датотеки", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Се верифицира поврзаноста со серверот...", + "Version": "Верзија", + "Version Information": "Информации за верзија", + "Very High": "Многу висок", + "Very high": "Многу висок", + "View": "Преглед", + "View API Docs": "Прегледај API документација", + "View API Documentation": "Прегледај API документација", + "View APIs": "Прегледај API-и", + "View Changes": "Прегледај промени", + "View Details": "Прегледај детали", + "View Error": "Прегледај грешка", + "View File": "Прегледај датотека", + "View Full Details": "Прегледај целосни детали", + "View Logs": "Прегледај логови", + "View Name": "Ime на преглед", + "View Object": "Прегледај објект", + "View Parameters": "Прегледај параметри", + "View Selection": "Избор на преглед", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Прегледај и анализирај логови на патеката за пребарување со напредни можности за филтрирање и аналитика", + "View and analyze system audit trails with advanced filtering capabilities": "Прегледај и анализирај системски ревизиски патеки со напредни можности за филтрирање", + "View and manage authorization across registers and schemas": "Прегледај и управувај со авторизација низ регистри и шеми", + "View deleted successfully": "Прегледот е успешно избришан", + "View entity details and manage relations": "Прегледај детали на ентитет и управувај со релации", + "View name": "Ime на преглед", + "View name is required": "Imeto на прегледот е задолжително", + "View object": "Прегледај објект", + "View saved successfully!": "Прегледот е успешно зачуван!", + "View search analytics and manage search logs": "Прегледај аналитика за пребарување и управувај со логови за пребарување", + "View successfully deleted": "Прегледот е успешно избришан", + "View successfully updated": "Прегледот е успешно ажуриран", + "View updated successfully!": "Прегледот е успешно ажуриран!", + "View webhook delivery logs and filter by webhook": "Прегледај логови за испорака на Webhook и филтрирај по Webhook", + "View {count} more": "Прегледај уште {count}", + "Views": "Прегледи", + "Visible to end users": "Видливо за крајните корисници", + "Visible to users": "Видливо за корисниците", + "Wait for Response": "Почекај одговор", + "Wait for webhook response before continuing (required for request/response flows)": "Почекај одговор на Webhook пред да продолжите (задолжително за текови на барање/одговор)", + "Waiting for cluster synchronization...": "Се чека синхронизација на кластерот...", + "Warmup Names Cache": "Загреј кеш на имиња", + "Warmup failed": "Загревањето не успеа", + "Warning": "Предупредување", + "Warning:": "Предупредување:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Предупредување: Оваа сопственост има неконзистентни типови: {types}. Размислете дали сите објекти треба да имаат ист тип.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Ќе ги скенираме сите објекти кои припаѓаат на оваа шема за да откријеме нови сопствености и да ги анализираме постоечките сопствености за потенцијални подобрувања. Процесот вклучува испитување на структурата на податоците на секој објект, идентификување на сопствености кои не се дефинирани во тековната шема и наоѓање можности за подобрување на постоечките дефиниции на сопствености со подобри ограничувања, формати и правила за валидација.", + "Webhook": "Webhook", + "Webhook Log Details": "Детали за лог на Webhook", + "Webhook Logs": "Логови на Webhook", + "Webhook created successfully": "Webhook е успешно создаден", + "Webhook deleted": "Webhook е избришан", + "Webhook retry delivered successfully": "Повторниот обид на Webhook е успешно испорачан", + "Webhook retry delivery failed": "Испораката на повторниот обид на Webhook не успеа", + "Webhook updated": "Webhook е ажуриран", + "Webhook updated successfully": "Webhook е успешно ажуриран", + "Webhooks": "Webhook-ови", + "Weekly": "Неделно", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Кога агентот работи без корисничка сесија (пр. закажани задачи), ќе се користи контекстот на овој корисник", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Кога агентот работи без корисничка сесија (пр. закажани задачи), ќе се користи контекстот на овој корисник.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Кога е оневозможено, оваа фасета ќе прикажува само вредности од оваа шема и ќе вклучи филтер за шема кога е избрана.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Кога е овозможено, зачувувањето на овој објект исто така ќе ги ажурира целните објекти за да вклучат референца назад кон овој објект.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Кога е овозможено, оваа сопственост ќе биде отстранета од изворниот објект по ажурирањето на целните објекти.", + "Workflow Management": "Управување со работни текови", + "Workflows": "Работни текови", + "Wrap webhook payload in CloudEvents format for better interoperability": "Завиткај содржина на Webhook во CloudEvents формат за подобра интероперабилност", + "Wrap webhook payload in cloudevents format for better interoperability": "Завиткај содржина на Webhook во CloudEvents формат за подобра интероперабилност", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Да", + "You": "Вие", + "You are not yet a member of any organisations.": "Сè уште не сте член на ниедна организација.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Можете да создадете нови ConfigSets врз основа на шаблонот _default или да прикачите прилагодени директно на вашиот SOLR сервер.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Можете да создадете нови ConfigSets врз основа на шаблонот _default или да прикачите прилагодени директно на вашиот SOLR сервер.", + "You do not have access to this conversation": "Немате пристап до овој разговор", + "You do not have permission to delete this conversation": "Немате дозвола да го избришете овој разговор", + "You do not have permission to modify this conversation": "Немате дозвола да го измените овој разговор", + "You do not have permission to restore this conversation": "Немате дозвола да го обновите овој разговор", + "You do not have permission to view the permission matrix. Admin access is required.": "Немате дозвола да ја прегледате матрицата на дозволи. Потребен е администраторски пристап.", + "You have selected {count} properties to add to the schema.": "Сте избрале {count} сопствености за додавање во шемата.", + "You must be logged in to favorite views": "Мора да сте најавени за да означувате прегледи како омилени", + "You need an AI agent to start a conversation.": "Потребен ви е AI агент за да започнете разговор.", + "Your Fireworks AI API key. Get one at": "Вашиот Fireworks AI API клуч. Добијте еден на", + "Your OpenAI API key. Get one at": "Вашиот OpenAI API клуч. Добијте еден на", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Вашата повратна информација е евидентирана. Опционално, можете да наведете дополнителни детали овде...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Вашата повратна информација е евидентирана. опционално, можете да наведете дополнителни детали овде...", + "Zookeeper Hosts": "Zookeeper домаќини", + "Zookeeper Port": "Zookeeper порта", + "Zookeeper Settings (SolrCloud)": "Zookeeper поставки (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper низа за поврзување за SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper број на порта (опционално, стандардно 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕДУВАЊЕ ЗА ТРАЈНО БРИШЕЊЕ ⚠️\n\nСте на пат да ТРАЈНО избришете СИТЕ објекти за шемата \"{schema}\":\n\n• Активни објекти: {active}\n• Меко избришани објекти: {deleted}\n• Вкупно: {total}\n\nОвие објекти ќе бидат целосно отстранети од базата на податоци и НЕ МОЖАТ да бидат обновени.\n\nДали сте апсолутно сигурни?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕДУВАЊЕ ЗА ТРАЈНО БРИШЕЊЕ ⚠️\n\nСте на пат да ТРАЈНО избришете {count} меко избришани објекти за шемата \"{schema}\".\n\nОвие објекти ќе бидат целосно отстранети од базата на податоци и НЕ МОЖАТ да бидат обновени.\n\nДали сте апсолутно сигурни?", + "and accessible to all users in selected groups (or all users if no groups selected).": "и достапно за сите корисници во избраните групи (или за сите корисници ако не се избрани групи).", + "and add the files there.": "и додајте ги датотеките таму.", + "approved": "approved", + "are only accessible to invited users.": "се достапни само за поканети корисници.", + "by": "by", + "chunk": "парче", + "chunks": "парчиња", + "chunks processed": "парчиња обработени", + "completed": "завршено", + "configuration(s)": "конфигурација(и)", + "conversationId is required": "conversationId е задолжително", + "create": "create", + "delete": "delete", + "deleted objects section": "дел за избришани објекти", + "e.g., lib/Settings/config.json": "e.g., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "e.g., lib/Settings/register.json", + "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", + "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", + "execute": "execute", + "failed": "failed", + "file": "датотека", + "files": "датотеки", + "file{plural}": "file{plural}", + "folder": "папка", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "во {register}", + "in_progress": "in_progress", + "inherited": "inherited", + "list": "list", + "log{plural}": "log{plural}", + "manage": "manage", + "members": "членови", + "menu or contact someone with permission to create agents.": "мени или контактирајте некого со дозвола за создавање агенти.", + "message content is required": "содржината на пораката е задолжителна", + "messages": "пораки", + "missing": "missing", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL и API клуч се задолжителни", + "n8n configuration saved successfully": "n8n конфигурацијата е успешно зачувана", + "n8n connection not configured": "n8n врската не е конфигурирана", + "n8n connection successful": "n8n врската е успешна", + "n8n connection test failed: {message}": "Тестот на n8n врската не успеа: {message}", + "n8n connection test successful": "Тестот на n8n врската е успешен", + "n8n initialization failed: {message}": "Иницијализацијата на n8n не успеа: {message}", + "n8n integration disabled": "n8n интеграцијата е оневозможена", + "n8n integration enabled": "n8n интеграцијата е овозможена", + "n8n project initialized successfully": "n8n проектот е успешно иницијализиран", + "n8n settings saved successfully": "n8n поставките се успешно зачувани", + "notify_push configuration guide": "водич за конфигурација на notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push е инсталиран но OpenRegister сè уште не потврдил успешно испраќање. Активирајте зачувување на објект за да активирате, или проверете ја вашата notify_push конфигурација.", + "notify_push is installed but not yet active": "notify_push е инсталиран но сè уште не е активен", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "објекти", + "objects processed": "обработени објекти", + "object{plural}": "object{plural}", + "or pick a range": "или изберете опсег", + "org-...": "org-...", + "pending": "pending", + "provides a good balance between speed and accuracy.": "обезбедува добра рамнотежа помеѓу брзина и точност.", + "public": "public", + "read": "read", + "register(s)": "регистар(и)", + "register(s) selected": "регистар(и) избрани", + "register{plural}": "register{plural}", + "rejected": "rejected", + "required": "required", + "results": "резултати", + "schema(s)": "шема(и)", + "schema(s) selected": "шема(и) избрани", + "schema{plural}": "schema{plural}", + "selected objects": "избрани објекти", + "set": "set", + "sk-...": "sk-...", + "this application": "оваа апликација", + "to": "to", + "type must be \"positive\" or \"negative\"": "типот мора да биде \"positive\" или \"negative\"", + "update": "update", + "vectorized": "векторизирано", + "view": "view", + "views selected": "избрани прегледи", + "waiting": "waiting", + "{active} active, {deleted} deleted": "{active} активни, {deleted} избришани", + "{count} audit trails cleared successfully": "{count} ревизиски патеки се успешно исчистени", + "{count} compliance issue(s) detected": "Откриени {count} проблем(и) со усогласеност", + "{count} deletions": "{count} бришења", + "{count} documents found": "Пронајдени {count} документи", + "{count} email": "{count} е-пошта", + "{count} emails": "{count} е-пошти", + "{count} entries": "{count} записи", + "{count} issues": "{count} проблеми", + "{count} members": "{count} членови", + "{count} searches": "{count} пребарувања", + "{count} selected": "{count} избрани", + "{count} widget(s)": "{count} виџет(и)", + "{matched} object(s) matched. {erased} erased.": "{matched} објект(и) се совпаднаа. {erased} избришани.", + "{minutes} minutes ago": "пред {minutes} минути", + "{percentage}% of objects": "{percentage}% од објектите", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} во {register} / {schema}", + "{type} cache cleared successfully": "{type} кешот е успешно исчистен", + "ℹ️ Current Configuration": "ℹ️ Тековна конфигурација", + "ℹ️ current configuration": "ℹ️ тековна конфигурација", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ ОПАСНОСТ: Сите {total} објекти ќе бидат ТРАЈНО ИЗБРИШАНИ од базата на податоци. Оваа акција е НЕПОВРАТНА и не може да се врати!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕДУВАЊЕ ЗА ТРАЈНО БРИШЕЊЕ ⚠️\n\nСте на пат да ТРАЈНО избришете СИТЕ објекти за шемата \"{schema}\":\n\n• Активни објекти: {active}\n• Меко избришани објекти: {deleted}\n• Вкупно: {total}\n\nОвие објекти ќе бидат целосно отстранети од базата на податоци и НЕ МОЖАТ да бидат обновени.\n\nДали сте апсолутно сигурни?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕДУВАЊЕ ЗА ТРАЈНО БРИШЕЊЕ ⚠️\n\nСте на пат да ТРАЈНО избришете {count} меко избришани објекти за шемата \"{schema}\".\n\nОвие објекти ќе бидат целосно отстранети од базата на податоци и НЕ МОЖАТ да бидат обновени.\n\nДали сте апсолутно сигурни?", + "⚡ Batch Processing": "⚡ Групна обработка", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR може да обработи милиони документи со времиња на одговор за пребарување под секунда...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Сеопфатен контекст ✓ Помала веројатност за пропуштање детали ✗ Побавни одговори ✗ Може да вклучи помалку релевантни информации", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Побрзи одговори ✓ Пофокусирани одговори ✗ Може да пропушти релевантни информации", + "✨ AI Features": "✨ AI Функции", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Во дистрибуиран режим, SOLR користи ZooKeeper за координација на кластерот...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Фасетираното пребарување им овозможува на корисниците да навлезат подлабоко во резултатите по категории...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Дали знаевте? SOLR го поддржува пребарувањето за Netflix, Apple и многу други главни сајтови!", + "💰 View Selection (Cost Optimization)": "💰 Избор на преглед (Оптимизација на трошоци)", + "📄 Object Serialization": "📄 Серијализација на објекти", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR обезбедува богата аналитика и статистики за перформансите на пребарувањето...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets ги содржат шемата и конфигурациските датотеки за вашиот индекс за пребарување...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Репликацијата обезбедува дека вашиот индекс за пребарување е достапен дури и ако јазлите не успеат...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR е моќна корпоративна платформа за пребарување изградена на Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Поставки за векторизација", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Автоматското скалирање може динамички да додава или отстранува јазли врз основа на оптоварување...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Безбедносните функции вклучуваат автентикација, авторизација и SSL шифрирање...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} е-пошта", + "{count} е-пошти" + ], + "Purge {count} object from database": [ + "Исчисти {count} објект од базата на податоци", + "Исчисти {count} објекти од базата на податоци" + ], + "Restore {count} object": [ + "Врати {count} објект", + "Врати {count} објекти" + ], + "Successfully restored {count} object": [ + "Успешно вратен {count} објект", + "Успешно вратени {count} објекти" + ], + "Delete {count} object": [ + "Избриши {count} објект", + "Избриши {count} објекти" + ], + "Object successfully deleted": [ + "Објектот е успешно избришан", + "Објектите се успешно избришани" + ] + } +} diff --git a/l10n/mt.js b/l10n/mt.js new file mode 100644 index 0000000000..346be82098 --- /dev/null +++ b/l10n/mt.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(l-ebda suġġett)", + "(no title)" : "(l-ebda titlu)", + "(unnamed)" : "(bla isem)", + "(untitled card)" : "(karta bla titlu)", + "Activities" : "Attivitajiet", + "Add Endpoint" : "Żid Endpoint", + "Add Source" : "Żid Sors", + "Add card" : "Żid karta", + "Add contact" : "Żid kuntatt", + "All clear" : "Kollox ċar", + "Articles" : "Artikli", + "Code" : "Kodiċi", + "Compliance" : "Konformità", + "Contacts" : "Kuntatti", + "Create Agent" : "Oħloq Aġent", + "Create Application" : "Oħloq Applikazzjoni", + "Create event" : "Oħloq avveniment", + "Erasure complete" : "It-tħassir tlesta", + "Erasure preview" : "Previżjoni tat-tħassir", + "Expand details" : "Espandi d-dettalji", + "Save changes" : "Issejvja l-bidliet", + "📄 Object Serialization" : "📄 Serializzazzjoni ta' Oġġetti", + "🔢 Vectorization Settings" : "🔢 Settings tal-Vettorizzazzjoni", + "💰 View Selection (Cost Optimization)" : "💰 Għażla tal-Vedute (Ottimizzazzjoni tal-Kost)", + "AI Assistant" : "Assistent tal-IA", + "AI Chat" : "Chat tal-IA", + "API Key" : "Ċavetta API", + "API tokens saved successfully" : "It-tokens tal-API ġew issejvjati b'suċċess", + "About ConfigSets" : "Dwar il-ConfigSets", + "Action" : "Azzjoni", + "Action Distribution" : "Distribuzzjoni tal-Azzjonijiet", + "Action:" : "Azzjoni:", + "Actions" : "Azzjonijiet", + "Active" : "Attiv", + "Active Collections" : "Ġabriet Attivi", + "Active Filters" : "Filtri Attivi", + "Active Organisations" : "Organizzazzjonijiet Attivi", + "Active filters:" : "Filtri attivi:", + "Active organisation set successfully" : "L-organizzazzjoni attiva ġiet stabbilita b'suċċess", + "Activity Period" : "Perjodu ta' Attività", + "Add Groups" : "Żid Gruppi", + "Add Schema" : "Żid Skema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Żid titli tal-iskema, deskrizzjonijiet, u informazzjoni dwar ir-Reġistru biex tipprovdi kuntest aktar għani għat-tfittxija", + "Add to favorites" : "Żid mal-favoriti", + "Added" : "Miżjud", + "Added to favorites" : "Miżjud mal-favoriti", + "Additional Information" : "Informazzjoni Addizzjonali", + "Additional feedback saved. Thank you!" : "Feedback addizzjonali ġie issejvjat. Grazzi!", + "Admin" : "Amministratur", + "Advanced" : "Avvanzat", + "Advanced Filters" : "Filtri Avvanzati", + "Advanced Options" : "Għażliet Avvanzati", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Il-filtri avvanzati mhumiex disponibbli meta tuża s-sors tad-database. Aqleb għal Auto jew SOLR Index għal għażliet ta' filtrazzjoni.", + "After" : "Wara", + "Agent deleted successfully" : "L-aġent tħassar b'suċċess", + "Agents" : "Aġenti", + "All" : "Kollha", + "All Categories" : "Il-Kategoriji Kollha", + "All Collections" : "Il-Ġabriet Kollha", + "All Confidence Levels" : "Il-Livelli ta' Kunfidenza Kollha", + "All ConfigSets" : "Il-ConfigSets Kollha", + "All Files" : "Il-Fajls Kollha", + "All Levels" : "Il-Livelli Kollha", + "All Types" : "It-Tipi Kollha", + "All Webhooks" : "Il-Webhooks Kollha", + "All actions" : "L-azzjonijiet kollha", + "All audit trails cleared successfully" : "It-traċċi tal-awditjar kollha tneħħew b'suċċess", + "All registers" : "Ir-Reġistri kollha", + "All schemas" : "L-iskemi kollha", + "All searches" : "It-tfittxijiet kollha", + "All users" : "L-utenti kollha", + "An error occurred while clearing audit trails" : "Seħħet żball waqt it-tneħħija tat-traċċi tal-awditjar", + "An error occurred while deleting the audit trail" : "Seħħet żball waqt it-tħassir tat-traċċa tal-awditjar", + "An error occurred while deleting the view" : "Seħħet żball waqt it-tħassir tal-veduta", + "An error occurred while permanently deleting the objects" : "Seħħet żball waqt it-tħassir permanenti tal-Oġġetti", + "Analysis completed:" : "L-analiżi tlestiet:", + "Analysis steps:" : "Passi tal-analiżi:", + "Analytics" : "Analitika", + "Analyze Objects" : "Analizza l-Oġġetti", + "Analyze Properties" : "Analizza l-Proprjetajiet", + "Analyze existing properties for improvement opportunities" : "Analizza l-Proprjetajiet eżistenti għal opportunitajiet ta' titjib", + "Analyzing..." : "Qed jiġi analizzat...", + "Any favorites and sharing settings for this view" : "Kwalunkwe favoriti u settings ta' qsim għal din il-veduta", + "Any user" : "Kwalunkwe utent", + "App store cache invalidated successfully" : "Il-cache tal-App Store ġiet invalidata b'suċċess", + "Application deleted successfully" : "L-applikazzjoni tħassret b'suċċess", + "Applications" : "Applikazzjonijiet", + "Apply Changes" : "Applika l-Bidliet", + "Archive" : "Arkivja", + "Archive conversation" : "Arkivja l-konversazzjoni", + "Archived conversations are hidden from your active list" : "Il-konversazzjonijiet arkivjati huma moħbija mil-lista attiva tiegħek", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Inti ċert li trid tnaddaf it-traċċi tat-tfittxija qodma? Dan ser iħassar l-annotazzjonijiet eqdem minn 30 jum.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Inti ċert li trid tħassar it-traċċi tal-awditjar magħżula? Din l-azzjoni ma tistax tiġi annullata.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Inti ċert li trid tħassar it-traċċi tat-tfittxija magħżula? Din l-azzjoni ma tistax tiġi annullata.", + "Are you sure you want to delete this ConfigSet?" : "Inti ċert li trid tħassar dan il-ConfigSet?", + "Are you sure you want to permanently delete" : "Inti ċert li trid tħassar permanentement", + "Ask a question..." : "Staqsi mistoqsija...", + "Ask questions about your data using natural language" : "Staqsi mistoqsijiet dwar id-data tiegħek bl-użu ta' lingwaġġ naturali", + "Attempt" : "Tentattiv", + "Audit Trail Changes" : "Bidliet fit-Traċċa tal-Awditjar", + "Audit Trail Details" : "Dettalji tat-Traċċa tal-Awditjar", + "Audit Trail Management" : "Ġestjoni tat-Traċċa tal-Awditjar", + "Audit Trail Statistics" : "Statistika tat-Traċċa tal-Awditjar", + "Audit Trails" : "Traċċi tal-Awditjar", + "Audit trail data copied to clipboard" : "Id-data tat-traċċa tal-awditjar ġiet ikkupjata fil-clipboard", + "Audit trail deleted successfully" : "It-traċċa tal-awditjar tħassret b'suċċess", + "Audit trail successfully deleted" : "It-traċċa tal-awditjar tħassret b'suċċess", + "Authentication" : "Awtentikazzjoni", + "Auto-commit disabled" : "Auto-commit diżattivat", + "Auto-commit enabled" : "Auto-commit attivat", + "Auto-create Default Organisation" : "Oħloq Awtomatikament Organizzazzjoni Awtomatika", + "Auto-retry failed vectorizations" : "Erġa' pprova awtomatikament il-vettorizzazzjonijiet falluti", + "Automatically commit changes to SOLR index" : "Issottometti awtomatikament il-bidliet għall-indiċi SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Oħloq awtomatikament organizzazzjoni awtomatika jekk ma teżisti l-ebda waħda meta l-app tiġi inizjalizzata", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Iġġenera awtomatikament embeddings tal-vetturi minn biċċiet ta' test meta l-Fajls jiġu mtellgħin u proċessati", + "Automatically generate vector embeddings when objects are created or updated" : "Iġġenera awtomatikament embeddings tal-vetturi meta l-Oġġetti jinħolqu jew jiġu aġġornati", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Erġa' pprova awtomatikament it-tentattivi ta' vettorizzazzjoni falluti (massimu 3 provi)", + "Available Filters" : "Filtri Disponibbli", + "Available Workflows" : "Workflows Disponibbli", + "Avg Execution Time" : "Ħin Medju ta' Eżekuzzjoni", + "Avg Members/Org" : "Medja Membri/Org", + "Avg Object Views/Session" : "Medja Vedute ta' Oġġetti/Sessjoni", + "Avg Response Time" : "Ħin Medju ta' Rispons", + "Avg Results/Search" : "Medja Riżultati/Tfittxija", + "Avg Searches/Session" : "Medja Tfittxijiet/Sessjoni", + "Back" : "Lura", + "Back to Entities" : "Lura għall-Entitajiet", + "Back to Registers" : "Lura għar-Reġistri", + "Back to Webhooks" : "Lura għall-Webhooks", + "Back to applications" : "Lura għall-applikazzjonijiet", + "Back to entities" : "Lura għall-entitajiet", + "Backend updated successfully. Please reload the application." : "Il-backend ġie aġġornat b'suċċess. Jekk jogħġbok erġa' tella' l-applikazzjoni.", + "Base URL (Optional)" : "URL Bażi (Fakultattiv)", + "Basic Information" : "Informazzjoni Bażika", + "Batch Size" : "Daqs tal-Batch", + "Batch extraction completed" : "L-estrazzjoni tal-batch tlestiet", + "Before" : "Qabel", + "Before object vectorization can work:" : "Qabel ma tista' taħdem il-vettorizzazzjoni tal-Oġġetti:", + "Behavior Issue" : "Kwistjoni ta' Mġiba", + "Bookmarks" : "Bukmarks", + "Blob storage has been retired. All objects now use magic tables." : "Il-ħażna Blob ġiet irtirata. L-Oġġetti kollha issa jużaw magic tables.", + "Branch" : "Branch", + "Bulk delete operation completed successfully" : "L-operazzjoni ta' tħassir bil-massa tlestiet b'suċċess", + "Bulk save operation completed successfully" : "L-operazzjoni ta' ssejvjar bil-massa tlestiet b'suċċess", + "Business Data" : "Data tan-Negozju", + "Cache cleared successfully" : "Il-cache tneħħiet b'suċċess", + "Calculate Sizes" : "Ikkalkula d-Daqsijiet", + "Cancel" : "Ikkanċella", + "Cannot delete: objects are still attached" : "Ma jistax jitħassar: l-Oġġetti għadhom mehmużin", + "Category" : "Kategorija", + "Change Type" : "Tip ta' Bidla", + "Changes" : "Bidliet", + "Changes copied to clipboard" : "Il-bidliet ġew ikkupjati fil-clipboard", + "Chat Model" : "Mudell tal-Chat", + "Chat Provider" : "Fornitur tal-Chat", + "Chat Provider (RAG)" : "Fornitur tal-Chat (RAG)", + "Chat Settings" : "Settings tal-Chat", + "Chat provider connection successful!" : "Il-konnessjoni mal-fornitur tal-chat irnexxiet!", + "Chat settings" : "Settings tal-chat", + "Choose a register" : "Agħżel Reġistru", + "Choose a schema" : "Agħżel skema", + "Choose how vector similarity calculations are performed for semantic search" : "Agħżel kif jitwettqu l-kalkoli tas-similarità tal-vetturi għat-tfittxija semantika", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Agħżel liema tipi ta' Fajls għandhom jiġu inklużi fil-proċess ta' vettorizzazzjoni. Huma biss il-Fajls b'test estratt u biċċiet li ser jiġu proċessati.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Agħżel liema vedute għandhom jiġu inklużi fil-proċess ta' vettorizzazzjoni. Ħalli vojt biex tipproċessa l-vedute kollha bbażati fuq il-konfigurazzjoni tiegħek.", + "Chunk" : "Biċċa", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "It-tħassir tal-biċċiet għadu mhux implimentat. Uża endpoints ibbażati fuq biċċiet.", + "Chunks" : "Biċċiet", + "Chunks to Vectorize:" : "Biċċiet għall-Vettorizzazzjoni:", + "Cleanup Old Trails" : "Naddaf it-Traċċi Qodma", + "Cleanup completed" : "It-tindif tlesta", + "Clear All Embeddings" : "Neħħi l-Embeddings Kollha", + "Clear Entries" : "Neħħi l-Annotazzjonijiet", + "Clear Filtered Audit Trails" : "Neħħi t-Traċċi tal-Awditjar Iffiltrati", + "Clear Filters" : "Neħħi l-Filtri", + "Clear Index" : "Neħħi l-Indiċi", + "Clear Selection" : "Neħħi l-Għażla", + "Clear all" : "Neħħi kollox", + "Clear all filters" : "Neħħi l-filtri kollha", + "Clear date range" : "Neħħi l-firxa ta' dati", + "Clearing..." : "Qed jitneħħa...", + "Close" : "Agħlaq", + "Collection Management" : "Ġestjoni tal-Ġabriet", + "Collection Name" : "Isem tal-Ġabra", + "Collection assignments updated successfully" : "L-assenjazzjonijiet tal-ġabriet ġew aġġornati b'suċċess", + "Collection cleared successfully" : "Il-ġabra tneħħiet b'suċċess", + "Collection copied successfully" : "Il-ġabra ġiet ikkupjata b'suċċess", + "Collection created successfully" : "Il-ġabra nħolqot b'suċċess", + "Collection deleted successfully" : "Il-ġabra tħassret b'suċċess", + "Collection used to store and index file metadata and content" : "Ġabra użata biex taħżen u tindiċja l-metadata u l-kontenut tal-Fajls", + "Collection used to store and index object data" : "Ġabra użata biex taħżen u tindiċja d-data tal-Oġġetti", + "Collections:" : "Ġabriet:", + "Commit Message" : "Messaġġ tal-Commit", + "Commit Within (ms)" : "Issottometti Fi Żmien (ms)", + "Commit message" : "Messaġġ tal-commit", + "Compare current schema with real object data" : "Qabbel l-iskema attwali mad-data reali tal-Oġġetti", + "Completed" : "Komplut", + "Complex" : "Kumpless", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Mistoqsijiet kumplessi: Tfittxijiet avvanzati b'filtri multipli, operaturi, u kombinazzjonijiet ta' parametri kumplessi", + "Confidence Level" : "Livell ta' Kunfidenza", + "Confidence Score" : "Punteġġ ta' Kunfidenza", + "Config must be provided as an object" : "Il-konfigurazzjoni trid tiġi pprovduta bħala Oġġett", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Ġestjoni tal-ConfigSet", + "ConfigSet Name" : "Isem tal-ConfigSet", + "ConfigSet created successfully" : "Il-ConfigSet inħoloq b'suċċess", + "ConfigSet deleted successfully" : "Il-ConfigSet tħassar b'suċċess", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Il-ConfigSets jiddefinixxu l-iskema u l-konfigurazzjoni għall-ġabriet SOLR tiegħek. Fihom definizzjonijiet ta' kampijiet, analizzaturi, u settings oħra tat-tfittxija.", + "Configuration" : "Konfigurazzjoni", + "Configuration Data" : "Data tal-Konfigurazzjoni", + "Configuration saved" : "Il-konfigurazzjoni ġiet issejvjata", + "Configurations" : "Konfigurazzjonijiet", + "Configure Facets" : "Ikkonfigura l-Faċċati", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Ikkonfigura l-fornituri tal-Large Language Model (LLM) għal funzjonalitajiet imħaddma mill-IA inkluż tfittxija semantika, embeddings, u chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Ikkonfigura s-settings bażiċi tal-konnessjoni għas-server SOLR tiegħek inkluż awtentikazzjoni u għażliet tan-netwerk. Uża d-djalogi separati tal-ConfigSet u tal-Ġestjoni tal-Ġabriet biex tamministra cores u ġabriet.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Ikkonfigura kif l-Oġġetti tad-database jinbidlu f'embeddings tal-vetturi għat-tfittxija semantika. L-Oġġetti jiġu vettorizzati direttament mingħajr bżonn ta' estrazzjoni ta' test.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Ikkonfigura kif l-Oġġetti jinbidlu f'test qabel il-vettorizzazzjoni. Dawn is-settings jaffettwaw il-kwalità u l-kuntest tat-tfittxija.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Ikkonfigura l-parametri għall-vettorizzazzjoni tal-Fajls. Dan il-proċess ser jiġġenera embeddings tal-vetturi għall-biċċiet kollha tal-Fajls estratti.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Ikkonfigura l-parametri għall-vettorizzazzjoni tal-Oġġetti. Dan il-proċess ser jiġġenera embeddings tal-vetturi għall-Oġġetti kollha li jaqblu mal-filtri tal-veduta tiegħek.", + "Configure which types of data to search and how many sources to retrieve" : "Ikkonfigura liema tipi ta' data għandhom jiġu mfittxa u kemm-il sors għandu jiġi rkuprat", + "Connection Failed" : "Il-Konnessjoni Falliet", + "Connection Settings" : "Settings tal-Konnessjoni", + "Connection Successful!" : "Il-Konnessjoni Rnexxiet!", + "Connection failed" : "Il-konnessjoni falliet", + "Connection protocol" : "Protokoll tal-konnessjoni", + "Connection successful" : "Il-konnessjoni rnexxiet", + "Connection timeout in seconds" : "Timeout tal-konnessjoni f'sekondi", + "Constant delay between retries (5 minutes)" : "Dewmien kostanti bejn il-provi (5 minuti)", + "Constraint Issue" : "Kwistjoni ta' Restrizzjoni", + "Content Filters" : "Filtri tal-Kontenut", + "Continue to Properties" : "Kompli għall-Proprjetajiet", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Ikkontrolla liema vedute ta' Oġġetti għandhom jiġu vettorizzati biex tnaqqas l-ispejjeż tal-API. Vettorizza biss vedute li jibbenefikaw mit-tfittxija semantika.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Ikkontrolla liema vedute u għodod tista' tuża l-IA f'din il-konversazzjoni. B'mod awtomatiku, il-kapaċitajiet kollha tal-aġent huma attivati.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Jikkontrolla l-każwalità (0 = deterministiku, 2 = kreattiv ħafna)", + "Conversation ID is required" : "L-ID tal-konversazzjoni huwa meħtieġ", + "Conversation archived" : "Il-konversazzjoni ġiet arkivjata", + "Conversation archived successfully" : "Il-konversazzjoni ġiet arkivjata b'suċċess", + "Conversation cleared successfully" : "Il-konversazzjoni tneħħiet b'suċċess", + "Conversation deleted" : "Il-konversazzjoni tħassret", + "Conversation permanently deleted" : "Il-konversazzjoni tħassret permanentement", + "Conversation renamed" : "Il-konversazzjoni ġiet imsemmija mill-ġdid", + "Conversation restored" : "Il-konversazzjoni ġiet restawrata", + "Conversation title" : "Titlu tal-konversazzjoni", + "Copied!" : "Ikkupjat!", + "Copy" : "Ikkupja", + "Copy Changes" : "Ikkupja l-Bidliet", + "Copy Collection" : "Ikkupja l-Ġabra", + "Copy Data" : "Ikkupja d-Data", + "Copy Full Data" : "Ikkupja d-Data Kollha", + "Copying..." : "Qed jiġi kkupjat...", + "Count" : "Għadd", + "Counting objects..." : "Qed jingħaddu l-Oġġetti...", + "Costs" : "Spejjeż", + "Create" : "Oħloq", + "Create Collection" : "Oħloq Ġabra", + "Create ConfigSet" : "Oħloq ConfigSet", + "Create New Collection" : "Oħloq Ġabra Ġdida", + "Create New ConfigSet" : "Oħloq ConfigSet Ġdid", + "Create Operations" : "Operazzjonijiet ta' Ħolqien", + "Create Webhook" : "Oħloq Webhook", + "Create a copy of collection:" : "Oħloq kopja tal-ġabra:", + "Create a new ConfigSet based on the _default template" : "Oħloq ConfigSet ġdid ibbażat fuq il-mudell _default", + "Create a new SOLR collection from an existing ConfigSet" : "Oħloq ġabra SOLR ġdida minn ConfigSet eżistenti", + "Create your first AI agent to get started." : "Oħloq l-ewwel aġent tal-IA tiegħek biex tibda.", + "Created" : "Maħluq", + "Created:" : "Maħluq:", + "Creating..." : "Qed jinħoloq...", + "Current Filters" : "Filtri Attwali", + "Current Type:" : "Tip Attwali:", + "Custom API endpoint if using a different region" : "Endpoint API personalizzat jekk tuża reġjun differenti", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Headers HTTP personalizzati (wieħed kull linja, format: Header-Name: value)", + "Daily" : "Kuljum", + "Dashboard" : "Dashboard", + "Data Source" : "Sors tad-Data", + "Data sources" : "Sorsi tad-data", + "Data type does not match observed values" : "It-tip ta' data ma jaqbilx mal-valuri osservati", + "Database URL" : "URL tad-Database", + "Database information refreshed" : "L-informazzjoni tad-database ġiet aġġornata", + "Date Range" : "Firxa ta' Dati", + "Default" : "Awtomatiku", + "Default Organisation" : "Organizzazzjoni Awtomatika", + "Default: 5" : "Awtomatiku: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Id-dewmien jirdoppja ma' kull tentattiv (2, 4, 8 minuti...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Id-dewmien jiżdied b'mod lineari (5, 10, 15 minuti...)", + "Delete" : "Ħassar", + "Delete Application" : "Ħassar l-Applikazzjoni", + "Delete Audit Trail" : "Ħassar it-Traċċa tal-Awditjar", + "Delete Collection" : "Ħassar il-Ġabra", + "Delete ConfigSet" : "Ħassar il-ConfigSet", + "Delete Objects" : "Ħassar l-Oġġetti", + "Delete Operations" : "Operazzjonijiet ta' Tħassir", + "Delete View" : "Ħassar il-Veduta", + "Delete all objects in this schema" : "Ħassar l-Oġġetti kollha f'din l-iskema", + "Delete permanently" : "Ħassar permanentement", + "Delete this conversation?" : "Tħassar din il-konversazzjoni?", + "Delete view" : "Ħassar il-veduta", + "Deleted" : "Imħassar", + "Deleted By" : "Imħassar Minn", + "Deleted Date" : "Data tat-Tħassir", + "Deleted Items Management" : "Ġestjoni tal-Oġġetti Mħassra", + "Deleted This Week" : "Imħassar Din il-Ġimgħa", + "Deleted Today" : "Imħassar Illum", + "Deleted:" : "Imħassar:", + "Deleting..." : "Qed jitħassar...", + "Deletion Date Range" : "Firxa ta' Dati tat-Tħassir", + "Deletion Statistics" : "Statistika tat-Tħassir", + "Deprecated" : "Skadut", + "Description" : "Deskrizzjoni", + "Description:" : "Deskrizzjoni:", + "Detect data types and patterns" : "Iskopri t-tipi ta' data u x-xejriet", + "Detected At" : "Skopert Fil", + "Detected Format:" : "Format Skopert:", + "Detected Issues:" : "Kwistjonijiet Skoperti:", + "Disable" : "Iddiżattiva", + "Disabled" : "Diżattivat", + "Discover Files" : "Skopri l-Fajls", + "Discovered Properties" : "Proprjetajiet Skoperti", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Trid tħassar permanentement l-annotazzjonijiet kollha ffiltrati tat-traċċa tal-awditjar? Din l-azzjoni ma tistax tiġi annullata.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Trid tħassar permanentement din l-annotazzjoni tat-traċċa tal-awditjar? Din l-azzjoni ma tistax tiġi annullata.", + "Documents" : "Dokumenti", + "Download API Spec" : "Niżżel l-Ispeċifikazzjoni tal-API", + "Edit" : "Editja", + "Edit Register" : "Editja r-Reġistru", + "Edit Schema" : "Editja l-Iskema", + "Edit View" : "Editja l-Veduta", + "Edit Webhook" : "Editja l-Webhook", + "Edit view" : "Editja l-veduta", + "Edit view details" : "Editja d-dettalji tal-veduta", + "Effectiveness" : "Effettività", + "Email" : "Email", + "Emails" : "Emails", + "Embedding Model" : "Mudell tal-Embedding", + "Embedding Provider" : "Fornitur tal-Embedding", + "Embedding generated successfully" : "L-embedding ġie ġġenerat b'suċċess", + "Embedding provider connection successful!" : "Il-konnessjoni mal-fornitur tal-embedding irnexxiet!", + "Enable" : "Attiva", + "Enable auto-creation" : "Attiva l-ħolqien awtomatiku", + "Enable automatic file vectorization" : "Attiva l-vettorizzazzjoni awtomatika tal-Fajls", + "Enable automatic object vectorization" : "Attiva l-vettorizzazzjoni awtomatika tal-Oġġetti", + "Enable automatic synchronization" : "Attiva s-sinkronizzazzjoni awtomatika", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Attiva logging dettaljat għall-operazzjonijiet SOLR (rakkomandat għad-debugging)", + "Enable faceting" : "Attiva l-faċċettar", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Attiva jew iddiżattiva l-funzjonalitajiet tal-LLM. Ikkonfigura l-fornituri u l-mudelli bl-użu tal-buttuna tal-Konfigurazzjoni tal-LLM hawn fuq.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Attiva jew iddiżattiva l-integrazzjoni tat-tfittxija SOLR. Ikkonfigura s-settings tal-konnessjoni bl-użu tal-buttuna tas-Settings tal-Konnessjoni hawn fuq.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Attiva jew iddiżattiva l-integrazzjoni tal-workflow n8n. Ikkonfigura s-settings tal-konnessjoni hawn taħt.", + "Enable or disable this webhook" : "Attiva jew iddiżattiva dan il-webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Attiva l-vettorizzazzjoni għall-vedute kollha eżistenti u futuri (jista' jżid l-ispejjeż)", + "Enabled" : "Attivat", + "Endpoints" : "Endpoints", + "Enter ConfigSet name" : "Daħħal isem tal-ConfigSet", + "Enter collection name" : "Daħħal isem tal-ġabra", + "Enter description (optional)..." : "Daħħal deskrizzjoni (fakultattiv)...", + "Enter new collection name" : "Daħħal isem ġdid tal-ġabra", + "Enter object ID" : "Daħħal l-ID tal-Oġġett", + "Enter search term" : "Daħħal terminu ta' tfittxija", + "Enter view name..." : "Daħħal isem tal-veduta...", + "Enter webhook name" : "Daħħal isem tal-webhook", + "Entities" : "Entitajiet", + "Entity Information" : "Informazzjoni dwar l-Entità", + "Entity deleted successfully" : "L-entità tħassret b'suċċess", + "Entity not found" : "L-entità ma nstabitx", + "Entries to be deleted:" : "Annotazzjonijiet li ser jitħassru:", + "Enum Issue" : "Kwistjoni ta' Enum", + "Enum constraint is missing" : "Ir-restrizzjoni tal-enum hija nieqsa", + "Error" : "Żball", + "Error Details" : "Dettalji tal-Iżball", + "Error Information" : "Informazzjoni dwar l-Iżball", + "Error Message" : "Messaġġ tal-Iżball", + "Error loading application" : "Żball fit-tagħbija tal-applikazzjoni", + "Error loading audit trails" : "Żball fit-tagħbija tat-traċċi tal-awditjar", + "Error loading entity" : "Żball fit-tagħbija tal-entità", + "Error loading search trails" : "Żball fit-tagħbija tat-traċċi tat-tfittxija", + "Estimated Batches:" : "Batches Stmati:", + "Estimated Cost:" : "Kost Stmat:", + "Estimated Duration:" : "Tul ta' Żmien Stmat:", + "Event" : "Avveniment", + "Event Property for Payload" : "Proprjetà tal-Avveniment għall-Payload", + "Events" : "Avvenimenti", + "Example Value" : "Valur ta' Eżempju", + "Exclusive Maximum" : "Massimu Esklussiv", + "Exclusive Minimum" : "Minimu Esklussiv", + "Execution Mode" : "Mod ta' Eżekuzzjoni", + "Execution Time" : "Ħin ta' Eżekuzzjoni", + "Execution Time Range" : "Firxa tal-Ħin ta' Eżekuzzjoni", + "Existing Improvements" : "Titjib Eżistenti", + "Exponential" : "Esponenzjali", + "Export" : "Esporta", + "Export completed successfully" : "L-esportazzjoni tlestiet b'suċċess", + "Export, view, or delete audit trails" : "Esporta, ara, jew ħassar it-traċċi tal-awditjar", + "Extend Schema" : "Estendi l-Iskema", + "Extended" : "Estiż", + "Extensions" : "Estensjonijiet", + "Extract Pending Files" : "Estratti l-Fajls Pendenti", + "Extract properties from each object" : "Estratti l-Proprjetajiet minn kull Oġġett", + "Extracted At" : "Estratti Fil", + "Extraction Status" : "Status tal-Estrazzjoni", + "Facet configuration updated successfully" : "Il-konfigurazzjoni tal-faċċata ġiet aġġornata b'suċċess", + "Facets discovered and configured successfully" : "Il-faċċati ġew skoperti u kkonfigurati b'suċċess", + "Facets discovered successfully" : "Il-faċċati ġew skoperti b'suċċess", + "Failed" : "Falla", + "Failed to analyze schema properties" : "Naqas milli janalizza l-Proprjetajiet tal-iskema", + "Failed to archive conversation" : "Naqas milli jarkivja l-konversazzjoni", + "Failed to calculate sizes" : "Naqas milli jikkalkula d-daqsijiet", + "Failed to clear collection" : "Naqas milli jneħħi l-ġabra", + "Failed to copy changes" : "Naqas milli jikkupja l-bidliet", + "Failed to copy data" : "Naqas milli jikkupja d-data", + "Failed to copy data to clipboard" : "Naqas milli jikkupja d-data fil-clipboard", + "Failed to create conversation" : "Naqas milli joħloq il-konversazzjoni", + "Failed to create or find project" : "Naqas milli joħloq jew isib il-proġett", + "Failed to delete collection" : "Naqas milli jħassar il-ġabra", + "Failed to delete conversation" : "Naqas milli jħassar il-konversazzjoni", + "Failed to delete webhook" : "Naqas milli jħassar il-webhook", + "Failed to download API specification" : "Naqas milli jniżżel l-ispeċifikazzjoni tal-API", + "Failed to get SOLR field configuration" : "Naqas milli jikseb il-konfigurazzjoni tal-kampijiet SOLR", + "Failed to get entity categories" : "Naqas milli jikseb il-kategoriji tal-entità", + "Failed to get entity statistics" : "Naqas milli jikseb l-istatistika tal-entità", + "Failed to get entity types" : "Naqas milli jikseb it-tipi tal-entità", + "Failed to load LLM configuration" : "Naqas milli jtella' l-konfigurazzjoni tal-LLM", + "Failed to load Nextcloud groups" : "Naqas milli jtella' l-gruppi ta' Nextcloud", + "Failed to load conversation" : "Naqas milli jtella' l-konversazzjoni", + "Failed to load entities" : "Naqas milli jtella' l-entitajiet", + "Failed to load entity" : "Naqas milli jtella' l-entità", + "Failed to load files" : "Naqas milli jtella' l-Fajls", + "Failed to load organisations" : "Naqas milli jtella' l-organizzazzjonijiet", + "Failed to load templates" : "Naqas milli jtella' l-mudelli", + "Failed to load webhooks" : "Naqas milli jtella' l-webhooks", + "Failed to load workflows" : "Naqas milli jtella' l-workflows", + "Failed to refresh database information" : "Naqas milli jaġġorna l-informazzjoni tad-database", + "Failed to reindex collection" : "Naqas milli jerġa' jindiċja l-ġabra", + "Failed to rename conversation" : "Naqas milli jsemmi mill-ġdid il-konversazzjoni", + "Failed to restore conversation" : "Naqas milli jirrestawra l-konversazzjoni", + "Failed to retry extraction" : "Naqas milli jerġa' jipprova l-estrazzjoni", + "Failed to retry webhook" : "Naqas milli jerġa' jipprova l-webhook", + "Failed to save GitHub token" : "Naqas milli jissejvja t-token ta' GitHub", + "Failed to save GitLab URL" : "Naqas milli jissejvja l-URL ta' GitLab", + "Failed to save GitLab token" : "Naqas milli jissejvja t-token ta' GitLab", + "Failed to save additional feedback" : "Naqas milli jissejvja feedback addizzjonali", + "Failed to save n8n configuration" : "Naqas milli jissejvja l-konfigurazzjoni n8n", + "Failed to save roles" : "Naqas milli jissejvja r-rwoli", + "Failed to save settings" : "Naqas milli jissejvja s-settings", + "Failed to save webhook" : "Naqas milli jissejvja l-webhook", + "Failed to send feedback" : "Naqas milli jibgħat il-feedback", + "Failed to test webhook" : "Naqas milli jittestja l-webhook", + "Failed to update favorite status" : "Naqas milli jaġġorna l-istatus tal-favorit", + "Failed to update schema properties" : "Naqas milli jaġġorna l-Proprjetajiet tal-iskema", + "Failed to update view" : "Naqas milli jaġġorna l-veduta", + "Failed to update webhook" : "Naqas milli jaġġorna l-webhook", + "Feedback recorded" : "Il-feedback ġie rreġistrat", + "Fewer sources (1-3):" : "Inqas sorsi (1-3):", + "Field" : "Kamp", + "File" : "Fajl", + "File Chunk Prediction" : "Tbassir tal-Biċċiet tal-Fajl", + "File Collection" : "Ġabra tal-Fajls", + "File Management" : "Ġestjoni tal-Fajls", + "File Name" : "Isem tal-Fajl", + "File Path" : "Path tal-Fajl", + "File Type Selection" : "Għażla tat-Tip ta' Fajl", + "File Vectorization" : "Vettorizzazzjoni tal-Fajls", + "File Warmup" : "Tisħin tal-Fajls", + "File actions menu" : "Menu tal-azzjonijiet tal-Fajls", + "File anonymized successfully" : "Il-Fajl ġie anonimizzat b'suċċess", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Il-vettorizzazzjoni tal-biċċiet tal-Fajls għadha mhux implimentata. Il-biċċiet huma lesti u maħżuna, iżda s-servizz tal-vettorizzazzjoni qiegħed jiġi żviluppat.", + "File collection not configured" : "Il-ġabra tal-Fajls mhix ikkonfigurata", + "File discovery completed" : "L-iskoperta tal-Fajls tlestiet", + "File extraction completed" : "L-estrazzjoni tal-Fajls tlestiet", + "File extraction queued" : "L-estrazzjoni tal-Fajls tqiegħdet fil-kju", + "File indexed successfully" : "Il-Fajl ġie indiċjat b'suċċess", + "File is already anonymized" : "Il-Fajl diġà huwa anonimizzat", + "File not found" : "Il-Fajl ma nstabx", + "File settings updated successfully" : "Is-settings tal-Fajl ġew aġġornati b'suċċess", + "File sources" : "Sorsi tal-Fajls", + "File vectorization configuration saved successfully" : "Il-konfigurazzjoni tal-vettorizzazzjoni tal-Fajls ġiet issejvjata b'suċċess", + "File vectorization started. Check the statistics section for progress." : "Il-vettorizzazzjoni tal-Fajls bdiet. Iċċekkja s-sezzjoni tal-istatistika għall-progress.", + "File warmup completed" : "It-tisħin tal-Fajls tlesta", + "Files" : "Fajls", + "Files with Completed Extraction:" : "Fajls b'Estrazzjoni Kompluta:", + "Files → fileCollection, Objects → objectCollection" : "Fajls → fileCollection, Oġġetti → objectCollection", + "Filter Audit Trails" : "Iffiltra t-Traċċi tal-Awditjar", + "Filter Deleted Items" : "Iffiltra l-Oġġetti Mħassra", + "Filter Objects" : "Iffiltra l-Oġġetti", + "Filter Properties" : "Iffiltra l-Proprjetajiet", + "Filter Search Trails" : "Iffiltra t-Traċċi tat-Tfittxija", + "Filter Statistics" : "Statistika tal-Filtri", + "Filter and analyze search trail entries" : "Iffiltra u analizza l-annotazzjonijiet tat-traċċa tat-tfittxija", + "Filter and manage audit trail entries" : "Iffiltra u amministra l-annotazzjonijiet tat-traċċa tal-awditjar", + "Filter and manage soft deleted items" : "Iffiltra u amministra l-Oġġetti soft-imħassra", + "Filter by object ID" : "Iffiltra skont l-ID tal-Oġġett", + "Filter by search term" : "Iffiltra skont it-terminu ta' tfittxija", + "Filter by webhook" : "Iffiltra skont il-webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Id-data tal-filtri tela' awtomatikament. Uża l-filtri hawn taħt biex tirfina t-tfittxija tiegħek.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Iffiltra t-triggers tal-webhook skont il-Proprjetajiet tal-payload (wieħed kull linja, format: key: value)", + "Filtered" : "Iffiltrat", + "Filters" : "Filtri", + "Fireworks AI Chat Settings" : "Settings tal-Chat Fireworks AI", + "Fireworks AI Embedding Configuration" : "Konfigurazzjoni tal-Embedding Fireworks AI", + "First" : "L-Ewwel", + "Fixed" : "Fiss", + "For chat and retrieval-augmented generation" : "Għall-chat u l-ġenerazzjoni miżjuda bl-irkupru", + "For vector embeddings and semantic search" : "Għall-embeddings tal-vetturi u t-tfittxija semantika", + "Format" : "Format", + "Format Issue" : "Kwistjoni ta' Format", + "Format constraint is missing" : "Ir-restrizzjoni tal-format hija nieqsa", + "From Date" : "Mid-Data", + "From date" : "Mid-data", + "Full data copied to clipboard" : "Id-data kollha ġiet ikkupjata fil-clipboard", + "General Issue" : "Kwistjoni Ġenerali", + "Generate recommendations and confidence scores" : "Iġġenera rakkomandazzjonijiet u punteġġi ta' kunfidenza", + "Generate vectors immediately when new objects are created" : "Iġġenera vetturi immedjatament meta jinħolqu Oġġetti ġodda", + "GitHub token is valid" : "It-token ta' GitHub huwa validu", + "GitHub token removed successfully" : "It-token ta' GitHub tneħħa b'suċċess", + "GitHub token saved successfully" : "It-token ta' GitHub ġie issejvjat b'suċċess", + "GitLab URL saved successfully" : "L-URL ta' GitLab ġiet issejvjata b'suċċess", + "GitLab token is valid" : "It-token ta' GitLab huwa validu", + "GitLab token saved successfully" : "It-token ta' GitLab ġie issejvjat b'suċċess", + "HTTP Method" : "Metodu HTTP", + "HTTP method used to send webhook requests" : "Metodu HTTP użat biex jintbagħtu t-talbiet tal-webhook", + "Headers" : "Headers", + "Health" : "Saħħa", + "Heartbeat successful - connection kept alive" : "Il-heartbeat irnexxa - il-konnessjoni nżammet ħajja", + "Helpful" : "Utli", + "Hide Filters" : "Aħbi l-Filtri", + "Hide in forms" : "Aħbi fil-formoli", + "Hide in list view" : "Aħbi fil-veduta tal-lista", + "High" : "Għoli", + "High Confidence" : "Kunfidenza Għolja", + "Host" : "Host", + "Hourly" : "Kull siegħa", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Kemm għandu jaqsam fil-fond fil-Proprjetajiet tal-Oġġetti mqassrin (1-20). Valuri ogħla jaqbdu aktar dettall iżda jżidu d-daqs tal-vettur.", + "How often to check for updates (1-168 hours)" : "Kemm-il darba għandu jiċċekkja għal aġġornamenti (1-168 siegħa)", + "How to handle retries for failed webhook deliveries" : "Kif għandhom jiġu mmaniġġjati l-provi għal kunsinni falluti tal-webhook", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Il-limiti tar-rata tal-IP tneħħew b'suċċess", + "Identify properties not in the schema" : "Identifika l-Proprjetajiet li mhumiex fl-iskema", + "Immutable" : "Immutabbli", + "Import" : "Importa", + "Import successful" : "L-importazzjoni rnexxiet", + "Improved Property" : "Proprjetà Imtejba", + "In use" : "Fl-użu", + "Inactive" : "Inattiv", + "Include IDs and names of related objects for better contextual search" : "Inkludi l-IDs u l-ismijiet tal-Oġġetti relatati għal tfittxija kuntestwali aħjar", + "Include related object references" : "Inkludi r-referenzi tal-Oġġetti relatati", + "Include schema and register metadata" : "Inkludi l-metadata tal-iskema u tar-Reġistru", + "Initialization failed" : "L-inizjalizzazzjoni falliet", + "Initialize Project" : "Inizjalizza l-Proġett", + "Inspect Fields" : "Ispezzjona l-Kampijiet", + "Inspect Index" : "Ispezzjona l-Indiċi", + "Invalid" : "Invalidu", + "Invalid batch size. Must be between 1 and 5000" : "Daqs ta' batch invalidu. Irid ikun bejn 1 u 5000", + "Invalid field name provided" : "Isem ta' kamp invalidu ġie pprovdut", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects invalidu. Irid ikun 0 (kollha) jew numru pożittiv", + "Invalid:" : "Invalidu:", + "Issue" : "Kwistjoni", + "Items per page:" : "Oġġetti kull paġna:", + "Just now" : "Issa stess", + "LLM Configuration" : "Konfigurazzjoni tal-LLM", + "LLM actions menu" : "Menu tal-azzjonijiet tal-LLM", + "LLM configuration saved successfully" : "Il-konfigurazzjoni tal-LLM ġiet issejvjata b'suċċess", + "LLM features disabled" : "Il-funzjonalitajiet tal-LLM ġew diżattivati", + "LLM features enabled" : "Il-funzjonalitajiet tal-LLM ġew attivati", + "LLM must be enabled with an embedding provider configured" : "Il-LLM irid ikun attivat b'fornitur tal-embedding ikkonfigurat", + "LLM settings updated successfully" : "Is-settings tal-LLM ġew aġġornati b'suċċess", + "Last" : "L-Aħħar", + "Last Triggered" : "L-Aħħar Imqabbad", + "Length Range:" : "Firxa tat-Tul:", + "Linear" : "Lineari", + "Load Advanced Filters" : "Tella' l-Filtri Avvanzati", + "Load advanced filters with live data from your search index" : "Tella' l-filtri avvanzati b'data live mill-indiċi tat-tfittxija tiegħek", + "Load view" : "Tella' l-veduta", + "Loading ConfigSets..." : "Qed jitilgħu l-ConfigSets...", + "Loading advanced filters..." : "Qed jitilgħu l-filtri avvanzati...", + "Loading agents..." : "Qed jitilgħu l-aġenti...", + "Loading application details..." : "Qed jitilgħu d-dettalji tal-applikazzjoni...", + "Loading applications..." : "Qed jitilgħu l-applikazzjonijiet...", + "Loading archived conversations..." : "Qed jitilgħu l-konversazzjonijiet arkivjati...", + "Loading audit trails..." : "Qed jitilgħu t-traċċi tal-awditjar...", + "Loading collections..." : "Qed jitilgħu l-ġabriet...", + "Loading configurations..." : "Qed jitilgħu l-konfigurazzjonijiet...", + "Loading conversation..." : "Qed titilgħa l-konversazzjoni...", + "Loading conversations..." : "Qed jitilgħu l-konversazzjonijiet...", + "Loading deleted items..." : "Qed jitilgħu l-Oġġetti mħassra...", + "Loading events..." : "Qed jitilgħu l-avvenimenti...", + "Loading filter data..." : "Qed titilgħa d-data tal-filtri...", + "Loading groups..." : "Qed jitilgħu l-gruppi...", + "Loading log details..." : "Qed jitilgħu d-dettalji tal-log...", + "Loading registers..." : "Qed jitilgħu r-Reġistri...", + "Loading schemas..." : "Qed jitilgħu l-iskemi...", + "Loading search trails..." : "Qed jitilgħu t-traċċi tat-tfittxija...", + "Loading sources..." : "Qed jitilgħu s-sorsi...", + "Loading statistics..." : "Qed titilgħa l-istatistika...", + "Loading users..." : "Qed jitilgħu l-utenti...", + "Loading views..." : "Qed jitilgħu l-vedute...", + "Loading..." : "Qed jitilgħa...", + "Local" : "Lokali", + "Local Version" : "Verżjoni Lokali", + "Locked" : "Imsakkar", + "Locked:" : "Imsakkar:", + "Login successful" : "Il-login irnexxa", + "Logs" : "Logs", + "Low" : "Baxx", + "Low Confidence" : "Kunfidenza Baxxa", + "Lucene Version:" : "Verżjoni Lucene:", + "Magic table synchronized successfully" : "Il-magic table ġiet sinkronizzata b'suċċess", + "Manage Organisation Roles" : "Amministra r-Rwoli tal-Organizzazzjoni", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Amministra l-Ġabriet SOLR (ħażniet ta' data) u assenjahom għall-Oġġetti u l-Fajls.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Amministra l-ConfigSets SOLR (mudelli ta' konfigurazzjoni) għall-ġabriet tiegħek.", + "Manage and configure agents for automated tasks" : "Amministra u kkonfigura aġenti għal kompiti awtomatizzati", + "Manage and monitor file text extraction status" : "Amministra u immonitorja l-istatus tal-estrazzjoni tat-test tal-Fajls", + "Manage and restore soft deleted items from your registers" : "Amministra u rrestawra l-Oġġetti soft-imħassra mir-Reġistri tiegħek", + "Manage and view detected entities from files and objects" : "Amministra u ara l-entitajiet skoperti mill-Fajls u l-Oġġetti", + "Manage document templates and themes" : "Amministra l-mudelli u t-temi tad-dokumenti", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Amministra l-workflows n8n għall-awtomatizzazzjoni ta' OpenRegister. Il-workflows ser jinħażnu fil-proġett ikkonfigurat.", + "Manage webhooks for event-driven integrations" : "Amministra l-webhooks għal integrazzjonijiet immexxija mill-avvenimenti", + "Manage your applications and modules" : "Amministra l-applikazzjonijiet u l-moduli tiegħek", + "Manage your data registers and their configurations" : "Amministra r-Reġistri tad-data tiegħek u l-konfigurazzjonijiet tagħhom", + "Manage your data schemas and their properties" : "Amministra l-iskemi tad-data tiegħek u l-Proprjetajiet tagħhom", + "Manage your data sources and their configurations" : "Amministra s-sorsi tad-data tiegħek u l-konfigurazzjonijiet tagħhom", + "Manage your saved search configurations" : "Amministra l-konfigurazzjonijiet tat-tfittxija ssejvjati tiegħek", + "Manage your system configurations and settings" : "Amministra l-konfigurazzjonijiet u s-settings tas-sistema tiegħek", + "Managed" : "Amministrat", + "Max Files (0 = all)" : "Massimu ta' Fajls (0 = kollha)", + "Max Length" : "Tul Massimu", + "Max Length:" : "Tul Massimu:", + "Max Objects (0 = all)" : "Massimu ta' Oġġetti (0 = kollha)", + "Max Retries" : "Massimu ta' Provi", + "Max Shards/Node" : "Massimu Shards/Node", + "Max execution time (ms)" : "Ħin massimu ta' eżekuzzjoni (ms)", + "Max ms" : "Massimu ms", + "Max result count" : "Għadd massimu ta' riżultati", + "Max results" : "Riżultati massimi", + "Maximum" : "Massimu", + "Maximum Nesting Depth" : "Fond Massimu tat-Tqassir", + "Maximum length constraint is missing" : "Ir-restrizzjoni tat-tul massimu hija nieqsa", + "Maximum length is too restrictive" : "It-tul massimu huwa restrittiv wisq", + "Maximum number of files to process. Set to 0 to process all files." : "L-għadd massimu ta' Fajls li għandhom jiġu proċessati. Issettja għal 0 biex tipproċessa l-Fajls kollha.", + "Maximum number of objects to process. Set to 0 to process all objects." : "L-għadd massimu ta' Oġġetti li għandhom jiġu proċessati. Issettja għal 0 biex tipproċessa l-Oġġetti kollha.", + "Maximum number of retry attempts for failed deliveries" : "L-għadd massimu ta' tentattivi ta' prova għal kunsinni falluti", + "Maximum time to wait before committing changes" : "Il-ħin massimu ta' stennija qabel ma jiġu sottomessi l-bidliet", + "Maximum tokens to generate" : "Massimu ta' tokens li għandhom jiġu ġġenerati", + "Maximum value constraint is missing" : "Ir-restrizzjoni tal-valur massimu hija nieqsa", + "Maximum value is too restrictive" : "Il-valur massimu huwa restrittiv wisq", + "Medium" : "Medju", + "Medium Confidence" : "Kunfidenza Medja", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Mistoqsijiet medji: Tfittxijiet b'ftit filtrazzjoni jew parametri multipli (eż., firxiet ta' dati, Reġistri/skemi speċifiċi)", + "Members of selected groups can access this view" : "Il-membri tal-gruppi magħżula jistgħu jaċċessaw din il-veduta", + "Members:" : "Membri:", + "Memory prediction calculated successfully" : "It-tbassir tal-memorja ġie kkalkulat b'suċċess", + "Message content is required" : "Il-kontenut tal-messaġġ huwa meħtieġ", + "Message does not belong to this conversation" : "Il-messaġġ ma jappartjenix għal din il-konversazzjoni", + "Metadata Filters" : "Filtri tal-Metadata", + "Method" : "Metodu", + "Min Length" : "Tul Minimu", + "Min execution time (ms)" : "Ħin minimu ta' eżekuzzjoni (ms)", + "Min ms" : "Minimu ms", + "Min result count" : "Għadd minimu ta' riżultati", + "Min results" : "Riżultati minimi", + "Minimum" : "Minimu", + "Minimum value constraint is missing" : "Ir-restrizzjoni tal-valur minimu hija nieqsa", + "Minimum value is too restrictive" : "Il-valur minimu huwa restrittiv wisq", + "Missing object fields created successfully" : "Il-kampijiet nieqsa tal-Oġġetti nħolqu b'suċċess", + "Mode:" : "Mod:", + "Model" : "Mudell", + "Model Name" : "Isem tal-Mudell", + "Modified" : "Modifikat", + "Monthly" : "Kull xahar", + "More sources (10-20):" : "Aktar sorsi (10-20):", + "Most Active Objects" : "L-Aktar Oġġetti Attivi", + "Multiple Of" : "Multiplu Ta'", + "Name" : "Isem", + "Names cache warmup completed" : "It-tisħin tal-cache tal-ismijiet tlesta", + "New Collection Name" : "Isem ta' Ġabra Ġdida", + "New Conversation" : "Konversazzjoni Ġdida", + "New Properties" : "Proprjetajiet Ġodda", + "New Property" : "Proprjetà Ġdida", + "New Value" : "Valur Ġdid", + "New users without specific organisation membership will be automatically added to this organisation" : "L-utenti ġodda mingħajr sħubija f'organizzazzjoni speċifika ser jiġu miżjuda awtomatikament ma' din l-organizzazzjoni", + "Next" : "Li Jmiss", + "No" : "Le", + "No ConfigSets found" : "Ma nstabu l-ebda ConfigSets", + "No GitHub token configured" : "L-ebda token ta' GitHub mhux ikkonfigurat", + "No GitHub token provided" : "L-ebda token ta' GitHub ma ġie pprovdut", + "No GitLab token provided" : "L-ebda token ta' GitLab ma ġie pprovdut", + "No Organisation" : "L-ebda Organizzazzjoni", + "No active filters" : "L-ebda filtri attivi", + "No activity data available" : "L-ebda data ta' attività disponibbli", + "No agents available" : "L-ebda aġent disponibbli", + "No agents found" : "Ma nstabu l-ebda aġenti", + "No app store cache exists yet" : "Għadu ma teżisti l-ebda cache tal-App Store", + "No applications are available." : "L-ebda applikazzjoni mhux disponibbli.", + "No applications found" : "Ma nstabu l-ebda applikazzjonijiet", + "No archived conversations" : "L-ebda konversazzjonijiet arkivjati", + "No audit trail entries found" : "Ma nstabu l-ebda annotazzjonijiet tat-traċċa tal-awditjar", + "No changes recorded" : "L-ebda bidliet ġew irreġistrati", + "No chunks to vectorize" : "L-ebda biċċiet biex jiġu vettorizzati", + "No collections found" : "Ma nstabu l-ebda ġabriet", + "No configuration data" : "L-ebda data ta' konfigurazzjoni", + "No configurations are available." : "L-ebda konfigurazzjoni mhux disponibbli.", + "No configurations found" : "Ma nstabu l-ebda konfigurazzjonijiet", + "No configurations found for this application." : "Ma nstabu l-ebda konfigurazzjonijiet għal din l-applikazzjoni.", + "No conversations yet. Create a new one to get started!" : "Għadu l-ebda konversazzjoni. Oħloq waħda ġdida biex tibda!", + "No data available" : "L-ebda data disponibbli", + "No data available for chart" : "L-ebda data disponibbli għaċ-chart", + "No deleted items found" : "Ma nstabu l-ebda Oġġetti mħassra", + "No deletion data available" : "L-ebda data ta' tħassir disponibbli", + "No description found" : "Ma nstabet l-ebda deskrizzjoni", + "No description provided" : "L-ebda deskrizzjoni ma ġiet ipprovduta", + "No entities detected in this file. Run text extraction first." : "Ma ġew skoperti l-ebda entitajiet f'dan il-Fajl. Mexxi l-estrazzjoni tat-test l-ewwel.", + "No entities found" : "Ma nstabu l-ebda entitajiet", + "No entities have been detected yet" : "Għadhom ma ġew skoperti l-ebda entitajiet", + "No events found" : "Ma nstabu l-ebda avvenimenti", + "No expired audit trails found to clear" : "Ma nstabu l-ebda traċċi tal-awditjar skaduti biex jitneħħew", + "No facetable fields available. Select a register and schema to see available filters." : "L-ebda kampijiet faċċettabbli disponibbli. Agħżel Reġistru u skema biex tara l-filtri disponibbli.", + "No files found" : "Ma nstabu l-ebda Fajls", + "No files have been extracted yet" : "Għadhom ma ġew estratti l-ebda Fajls", + "No files to process" : "L-ebda Fajls biex jiġu proċessati", + "No files to reindex" : "L-ebda Fajls biex jerġgħu jiġu indiċjati", + "No filters are currently active. This will delete ALL audit trail entries!" : "Bħalissa l-ebda filtri mhumiex attivi. Dan ser iħassar l-annotazzjonijiet KOLLHA tat-traċċa tal-awditjar!", + "No log entries found" : "Ma nstabu l-ebda annotazzjonijiet tal-log", + "No logs are available for this configuration." : "L-ebda logs mhumiex disponibbli għal din il-konfigurazzjoni.", + "No logs are available for this source." : "L-ebda logs mhumiex disponibbli għal dan is-Sors.", + "No logs found" : "Ma nstabu l-ebda logs", + "No mismatched fields found - SOLR schema is properly configured" : "Ma nstabu l-ebda kampijiet inkonsistenti - l-iskema SOLR hija kkonfigurata sew", + "No objects found" : "Ma nstabu l-ebda Oġġetti", + "No objects found to vectorize" : "Ma nstabu l-ebda Oġġetti biex jiġu vettorizzati", + "No objects selected for deletion" : "L-ebda Oġġetti magħżula għat-tħassir", + "No objects to delete" : "L-ebda Oġġetti biex jitħassru", + "No objects were permanently deleted" : "L-ebda Oġġetti ma tħassru permanentement", + "No properties found" : "Ma nstabu l-ebda Proprjetajiet", + "No properties match your filters." : "L-ebda Proprjetajiet ma jaqblu mal-filtri tiegħek.", + "No purge date set" : "L-ebda data ta' purga mhux issettjata", + "No register data available" : "L-ebda data tar-Reġistru disponibbli", + "No registers found" : "Ma nstabu l-ebda Reġistri", + "No registers found for this application." : "Ma nstabu l-ebda Reġistri għal din l-applikazzjoni.", + "No relations found" : "Ma nstabu l-ebda relazzjonijiet", + "No request body available" : "L-ebda korp ta' talba disponibbli", + "No response body available" : "L-ebda korp ta' rispons disponibbli", + "No saved views yet. Create one in the Search tab!" : "Għadu l-ebda veduta ssejvjata. Oħloq waħda fit-tab tat-Tfittxija!", + "No schema data available" : "L-ebda data tal-iskema disponibbli", + "No schema selected for exploration" : "L-ebda skema magħżula għall-esplorazzjoni", + "No schemas found" : "Ma nstabu l-ebda skemi", + "No schemas found for this application." : "Ma nstabu l-ebda skemi għal din l-applikazzjoni.", + "No search terms data available" : "L-ebda data ta' termini ta' tfittxija disponibbli", + "No search trail entries found" : "Ma nstabu l-ebda annotazzjonijiet tat-traċċa tat-tfittxija", + "No sources are available." : "L-ebda sorsi mhumiex disponibbli.", + "No sources found" : "Ma nstabu l-ebda sorsi", + "No templates found" : "Ma nstabu l-ebda mudelli", + "No templates have been created yet" : "Għadhom ma nħolqu l-ebda mudelli", + "No valid magic-mapped register+schema combinations found" : "Ma nstabu l-ebda kombinazzjonijiet validi ta' Reġistru+skema magic-mapped", + "No views found. Create views first before configuring vectorization." : "Ma nstabu l-ebda vedute. Oħloq vedute l-ewwel qabel ma tikkonfigura l-vettorizzazzjoni.", + "No views match your search" : "L-ebda vedute ma jaqblu mat-tfittxija tiegħek", + "No webhooks found" : "Ma nstabu l-ebda webhooks", + "No webhooks have been configured yet" : "Għadhom ma ġew ikkonfigurati l-ebda webhooks", + "No workflows found in this project. Create workflows in the n8n editor." : "Ma nstabu l-ebda workflows f'dan il-proġett. Oħloq workflows fl-editur n8n.", + "None" : "L-ebda", + "Not helpful" : "Mhux utli", + "Not in use" : "Mhux fl-użu", + "Number Constraints" : "Restrizzjonijiet tan-Numri", + "Number of chunks to vectorize in one API call (1-100)." : "L-għadd ta' biċċiet li għandhom jiġu vettorizzati f'sejħa waħda tal-API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "L-għadd ta' biċċiet li għandhom jiġu vettorizzati f'sejħa waħda tal-API. Ogħla = aktar mgħaġġel iżda aktar memorja. Rakkomandat: 10-50.", + "Number of objects to process in each batch (1-5000)." : "L-għadd ta' Oġġetti li għandhom jiġu proċessati f'kull batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "L-għadd ta' Oġġetti li għandhom jiġu vettorizzati f'sejħa waħda tal-API. Ogħla = aktar mgħaġġel iżda aktar memorja. Rakkomandat: 10-50.", + "Numeric Range:" : "Firxa Numerika:", + "Object" : "Oġġett", + "Object Collection" : "Ġabra tal-Oġġetti", + "Object Count Prediction" : "Tbassir tal-Għadd tal-Oġġetti", + "Object ID" : "ID tal-Oġġett", + "Object Management" : "Ġestjoni tal-Oġġetti", + "Object Vectorization" : "Vettorizzazzjoni tal-Oġġetti", + "Object Warmup" : "Tisħin tal-Oġġetti", + "Object collection not configured" : "Il-ġabra tal-Oġġetti mhix ikkonfigurata", + "Object not found" : "L-Oġġett ma nstabx", + "Object permanently deleted" : "L-Oġġett tħassar permanentement", + "Object restored successfully" : "L-Oġġett ġie restawrat b'suċċess", + "Object settings updated successfully" : "Is-settings tal-Oġġett ġew aġġornati b'suċċess", + "Object sources" : "Sorsi tal-Oġġetti", + "Object unlocked successfully" : "L-Oġġett ġie żblukkat b'suċċess", + "Object vectorization configuration saved successfully" : "Il-konfigurazzjoni tal-vettorizzazzjoni tal-Oġġetti ġiet issejvjata b'suċċess", + "Object vectorized successfully" : "L-Oġġett ġie vettorizzat b'suċċess", + "Object:" : "Oġġett:", + "Objects" : "Oġġetti", + "Objects Analyzed" : "Oġġetti Analizzati", + "Objects Distribution" : "Distribuzzjoni tal-Oġġetti", + "Objects being analyzed" : "Oġġetti qed jiġu analizzati", + "Objects by Register" : "Oġġetti skont ir-Reġistru", + "Objects by Schema" : "Oġġetti skont l-Iskema", + "Objects deletion completed successfully" : "It-tħassir tal-Oġġetti tlesta b'suċċess", + "Objects in schema" : "Oġġetti fl-iskema", + "Objects to Process:" : "Oġġetti biex Jiġu Proċessati:", + "Objects to be analyzed" : "Oġġetti li ser jiġu analizzati", + "Objects to be deleted" : "Oġġetti li ser jitħassru", + "Objects to be validated" : "Oġġetti li ser jiġu validati", + "Objects will be serialized as JSON text before vectorization" : "L-Oġġetti ser jiġu serializzati bħala test JSON qabel il-vettorizzazzjoni", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "L-Oġġetti ser jiġu soft-imħassra (immarkati bħala mħassra iżda miżmuma fid-database). Jistgħu jiġu rkuprati aktar tard jekk ikun meħtieġ.", + "Old Value" : "Valur Qadim", + "Oldest Item (days)" : "L-Eqdem Oġġett (jiem)", + "Ollama Chat Settings" : "Settings tal-Chat Ollama", + "Ollama Embedding Configuration" : "Konfigurazzjoni tal-Embedding Ollama", + "Ollama URL" : "URL ta' Ollama", + "Only With Changes" : "Biss b'Bidliet", + "Open Register Objects" : "Oġġetti Open Register", + "Open n8n Editor" : "Iftaħ l-Editur n8n", + "OpenAI Chat Settings" : "Settings tal-Chat OpenAI", + "OpenAI Embedding Configuration" : "Konfigurazzjoni tal-Embedding OpenAI", + "OpenRegister Settings" : "Settings ta' OpenRegister", + "Optional webhook secret for signature verification" : "Sigriet fakultattiv tal-webhook għall-verifika tal-firma", + "Order" : "Ordni", + "Organisation" : "Organizzazzjoni", + "Organisation ID" : "ID tal-Organizzazzjoni", + "Organisation Members" : "Membri tal-Organizzazzjoni", + "Organisation Statistics" : "Statistika tal-Organizzazzjoni", + "Organisation created successfully" : "L-organizzazzjoni nħolqot b'suċċess", + "Organisation settings saved successfully" : "Is-settings tal-organizzazzjoni ġew issejvjati b'suċċess", + "Organisations" : "Organizzazzjonijiet", + "Organization" : "Organizzazzjoni", + "Organization ID (Optional)" : "ID tal-Organizzazzjoni (Fakultattiv)", + "Orphaned Items" : "Oġġetti Orfni", + "Owner" : "Sid", + "Owner:" : "Sid:", + "Parallel Mode (Faster, more resource intensive)" : "Mod Parallel (Aktar mgħaġġel, jeħtieġ aktar riżorsi)", + "Parallel:" : "Parallel:", + "Password" : "Password", + "Password for SOLR authentication (optional)" : "Password għall-awtentikazzjoni SOLR (fakultattiv)", + "Path" : "Path", + "Path in repository" : "Path fir-repository", + "Path where the configuration file will be saved in the repository" : "Path fejn il-Fajl tal-konfigurazzjoni ser jiġi issejvjat fir-repository", + "Path where the register OAS file will be saved in the repository" : "Path fejn il-Fajl OAS tar-Reġistru ser jiġi issejvjat fir-repository", + "Pattern (regex)" : "Xejra (regex)", + "Pattern Issue" : "Kwistjoni ta' Xejra", + "Pattern constraint is missing" : "Ir-restrizzjoni tax-xejra hija nieqsa", + "Patterns:" : "Xejriet:", + "Pending" : "Pendenti", + "Permanent Delete Mode" : "Mod ta' Tħassir Permanenti", + "Permanently Delete" : "Ħassar Permanentement", + "Permanently delete already soft-deleted objects" : "Ħassar permanentement l-Oġġetti diġà soft-imħassra", + "Permanently delete this conversation?" : "Tħassar permanentement din il-konversazzjoni?", + "Person" : "Persuna", + "Personal Data" : "Data Personali", + "Phone" : "Telefon", + "Photos" : "Ritratti", + "Please create an agent in the" : "Jekk jogħġbok oħloq aġent fil-", + "Please select an agent to continue" : "Jekk jogħġbok agħżel aġent biex tkompli", + "Please select which register and schema to use for the new object" : "Jekk jogħġbok agħżel liema Reġistru u skema għandhom jintużaw għall-Oġġett il-ġdid", + "Please try again later." : "Jekk jogħġbok erġa' pprova aktar tard.", + "Please wait while we fetch your agents." : "Jekk jogħġbok stenna waqt li nġibu l-aġenti tiegħek.", + "Please wait while we fetch your applications." : "Jekk jogħġbok stenna waqt li nġibu l-applikazzjonijiet tiegħek.", + "Please wait while we fetch your configurations." : "Jekk jogħġbok stenna waqt li nġibu l-konfigurazzjonijiet tiegħek.", + "Please wait while we fetch your deleted items." : "Jekk jogħġbok stenna waqt li nġibu l-Oġġetti mħassra tiegħek.", + "Please wait while we fetch your sources." : "Jekk jogħġbok stenna waqt li nġibu s-sorsi tiegħek.", + "Polls" : "Stħarriġ", + "Popular Search Terms" : "Termini ta' Tfittxija Popolari", + "Port" : "Port", + "Prerequisites" : "Prerekwiżiti", + "Press Enter to send, Shift+Enter for new line" : "Agħfas Enter biex tibgħat, Shift+Enter għal linja ġdida", + "Previous" : "Ta' Qabel", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Jipproċessa l-biċċiet f'batches b'parallelliżmu simulat.", + "Processes file chunks sequentially (safest)." : "Jipproċessa l-biċċiet tal-Fajls b'mod sekwenzjali (l-aktar sikur).", + "Processes objects in chunks with simulated parallelism." : "Jipproċessa l-Oġġetti f'biċċiet b'parallelliżmu simulat.", + "Processes objects sequentially (safest)." : "Jipproċessa l-Oġġetti b'mod sekwenzjali (l-aktar sikur).", + "Processing" : "Qed jiġi proċessat", + "Processing Limits" : "Limiti ta' Proċessar", + "Processing..." : "Qed jiġi proċessat...", + "Project initialized successfully" : "Il-proġett ġie inizjalizzat b'suċċess", + "Project not found. Please initialize first." : "Il-proġett ma nstabx. Jekk jogħġbok inizjalizza l-ewwel.", + "Properties" : "Proprjetajiet", + "Property Behaviors" : "Imġibiet tal-Proprjetà", + "Property Configuration:" : "Konfigurazzjoni tal-Proprjetà:", + "Property Filters" : "Filtri tal-Proprjetà", + "Property Title" : "Titlu tal-Proprjetà", + "Property Type" : "Tip ta' Proprjetà", + "Property can be improved" : "Il-Proprjetà tista' tittejjeb", + "Protected" : "Protett", + "Provider" : "Fornitur", + "Provider is required for testing" : "Il-fornitur huwa meħtieġ għall-ittestjar", + "Public" : "Pubbliku", + "Public View" : "Veduta Pubblika", + "Public views can be accessed by anyone in the system" : "Il-vedute pubbliċi jistgħu jiġu aċċessati minn kulħadd fis-sistema", + "Publish" : "Ippubblika", + "Publish Objects" : "Ippubblika l-Oġġetti", + "Publish all objects in this schema" : "Ippubblika l-Oġġetti kollha f'din l-iskema", + "Published" : "Ippubblikat", + "Published:" : "Ippubblikat:", + "Publishing..." : "Qed jiġi ppubblikat...", + "Purge" : "Purga", + "Purge Date" : "Data tal-Purga", + "Query Complexity Distribution" : "Distribuzzjoni tal-Kumplessità tal-Mistoqsijiet", + "Query parameter is required" : "Il-parametru tal-mistoqsija huwa meħtieġ", + "RAG" : "RAG", + "RAG Configuration" : "Konfigurazzjoni RAG", + "Rate limits cleared successfully" : "Il-limiti tar-rata tneħħew b'suċċess", + "Raw Changes Data" : "Data Mhux Ipproċessata tal-Bidliet", + "Re-vectorize on object update" : "Erġa' vettorizza mal-aġġornament tal-Oġġett", + "Read" : "Aqra", + "Recommendations:" : "Rakkomandazzjonijiet:", + "Recommended Type:" : "Tip Rakkomandat:", + "Recommended: 5 sources" : "Rakkomandat: 5 sorsi", + "Refresh" : "Aġġorna", + "Refresh Data" : "Aġġorna d-Data", + "Refresh Stats" : "Aġġorna l-Istatistika", + "Refresh Workflows" : "Aġġorna l-Workflows", + "Refresh dashboard" : "Aġġorna d-dashboard", + "Refresh database info" : "Aġġorna l-info tad-database", + "Register" : "Reġistru", + "Register ID" : "ID tar-Reġistru", + "Register Statistics" : "Statistika tar-Reġistru", + "Register Totals" : "Totali tar-Reġistru", + "Register depublished successfully" : "Ir-Reġistru ġie depubblikat b'suċċess", + "Register not found" : "Ir-Reġistru ma nstabx", + "Register objects deletion completed successfully" : "It-tħassir tal-Oġġetti tar-Reġistru tlesta b'suċċess", + "Register published successfully" : "Ir-Reġistru ġie ppubblikat b'suċċess", + "Register/Schema Usage" : "Użu tar-Reġistru/Skema", + "Registers" : "Reġistri", + "Reindex" : "Erġa' indiċja", + "Reindex completed" : "L-indiċjar mill-ġdid tlesta", + "Reindex completed successfully" : "L-indiċjar mill-ġdid tlesta b'suċċess", + "Reindex failed" : "L-indiċjar mill-ġdid falla", + "Relations" : "Relazzjonijiet", + "Remote Version" : "Verżjoni Remota", + "Remove" : "Neħħi", + "Remove filter" : "Neħħi l-filtru", + "Remove from favorites" : "Neħħi mill-favoriti", + "Remove group" : "Neħħi l-grupp", + "Removed" : "Imneħħi", + "Removed from favorites" : "Imneħħi mill-favoriti", + "Rename Conversation" : "Semmi mill-Ġdid il-Konversazzjoni", + "Rename conversation" : "Semmi mill-ġdid il-konversazzjoni", + "Replicas" : "Repliki", + "Repository" : "Repository", + "Request Body" : "Korp tat-Talba", + "Request Data" : "Data tat-Talba", + "Request Details" : "Dettalji tat-Talba", + "Request body must be JSON with a \"query\" field" : "Il-korp tat-talba jrid ikun JSON b'kamp \"query\"", + "Request timeout in seconds" : "Timeout tat-talba f'sekondi", + "Required field" : "Kamp meħtieġ", + "Required status is inconsistent" : "L-istatus meħtieġ huwa inkonsistenti", + "Rerun Search" : "Erġa' mexxi t-Tfittxija", + "Reset Changes" : "Irrisettja l-Bidliet", + "Reset Filters" : "Irrisettja l-Filtri", + "Response Body" : "Korp tar-Rispons", + "Response Details" : "Dettalji tar-Rispons", + "Response Time:" : "Ħin tar-Rispons:", + "Restore" : "Irrestawra", + "Restore conversation" : "Irrestawra l-konversazzjoni", + "Restore or permanently delete items" : "Irrestawra jew ħassar permanentement l-Oġġetti", + "Result Count Range" : "Firxa tal-Għadd ta' Riżultati", + "Results" : "Riżultati", + "Retrieve all objects for this schema" : "Irkupra l-Oġġetti kollha għal din l-iskema", + "Retry" : "Erġa' pprova", + "Retry Failed Extractions" : "Erġa' pprova l-Estrazzjonijiet Falluti", + "Retry Policy" : "Politika tal-Provi", + "Retry completed" : "Il-prova tlestiet", + "Retrying..." : "Qed jerġa' jipprova...", + "Risk Level" : "Livell ta' Riskju", + "Roles updated successfully" : "Ir-rwoli ġew aġġornati b'suċċess", + "SOLR Connection Settings" : "Settings tal-Konnessjoni SOLR", + "SOLR Version:" : "Verżjoni SOLR:", + "SOLR actions menu" : "Menu tal-azzjonijiet SOLR", + "SOLR base path (usually /solr)" : "Path bażi SOLR (normalment /solr)", + "SOLR is disabled" : "SOLR huwa diżattivat", + "SOLR is not available or not configured" : "SOLR mhux disponibbli jew mhux ikkonfigurat", + "SOLR logging disabled" : "Il-logging SOLR ġie diżattivat", + "SOLR logging enabled" : "Il-logging SOLR ġie attivat", + "SOLR search disabled" : "It-tfittxija SOLR ġiet diżattivata", + "SOLR search enabled" : "It-tfittxija SOLR ġiet attivata", + "SOLR server hostname or IP address" : "Hostname tas-server SOLR jew indirizz IP", + "SOLR server port number (optional, defaults to 8983)" : "Numru tal-port tas-server SOLR (fakultattiv, awtomatiku 8983)", + "SOLR setup completed successfully" : "L-issettjar SOLR tlesta b'suċċess", + "SOLR setup failed" : "L-issettjar SOLR falla", + "SOLR setup failed - check logs" : "L-issettjar SOLR falla - iċċekkja l-logs", + "Sample Values:" : "Valuri ta' Kampjun:", + "Save" : "Issejvja", + "Save Configuration" : "Issejvja l-Konfigurazzjoni", + "Save Connection Settings" : "Issejvja s-Settings tal-Konnessjoni", + "Save Roles" : "Issejvja r-Rwoli", + "Save Settings" : "Issejvja s-Settings", + "Save current search as view" : "Issejvja t-tfittxija attwali bħala veduta", + "Saved Views" : "Vedute Ssejvjati", + "Saving..." : "Qed jiġi ssejvjat...", + "Schema" : "Skema", + "Schema ID" : "ID tal-Iskema", + "Schema Name" : "Isem tal-Iskema", + "Schema Statistics" : "Statistika tal-Iskema", + "Schema depublished successfully" : "L-iskema ġiet depubblikata b'suċċess", + "Schema objects deletion completed successfully" : "It-tħassir tal-Oġġetti tal-iskema tlesta b'suċċess", + "Schema published successfully" : "L-iskema ġiet ippubblikata b'suċċess", + "Schema successfully created" : "L-iskema nħolqot b'suċċess", + "Schema successfully updated" : "L-iskema ġiet aġġornata b'suċċess", + "Schemas" : "Skemi", + "Scheme" : "Skema", + "Search / Views" : "Tfittxija / Vedute", + "Search Activity" : "Attività tat-Tfittxija", + "Search Analytics" : "Analitika tat-Tfittxija", + "Search Entities" : "Fittex l-Entitajiet", + "Search Files" : "Fittex il-Fajls", + "Search GitHub" : "Fittex f'GitHub", + "Search GitLab" : "Fittex f'GitLab", + "Search Method" : "Metodu ta' Tfittxija", + "Search Term" : "Terminu ta' Tfittxija", + "Search Trail Details" : "Dettalji tat-Traċċa tat-Tfittxija", + "Search Trail Management" : "Ġestjoni tat-Traċċa tat-Tfittxija", + "Search Trail Statistics" : "Statistika tat-Traċċa tat-Tfittxija", + "Search Trails" : "Traċċi tat-Tfittxija", + "Search Views" : "Fittex il-Vedute", + "Search Webhooks" : "Fittex il-Webhooks", + "Search by file name or path" : "Fittex skont l-isem tal-Fajl jew il-path", + "Search by name or URL" : "Fittex skont l-isem jew l-URL", + "Search by value" : "Fittex skont il-valur", + "Search in Files" : "Fittex fil-Fajls", + "Search in Objects" : "Fittex fl-Oġġetti", + "Search property names..." : "Fittex l-ismijiet tal-Proprjetajiet...", + "Search trail deletion not implemented yet" : "It-tħassir tat-traċċa tat-tfittxija għadu mhux implimentat", + "Search views..." : "Fittex il-vedute...", + "Secret" : "Sigriet", + "Secret key for HMAC signature generation (optional)" : "Ċavetta sigrieta għall-ġenerazzjoni tal-firma HMAC (fakultattiv)", + "Select AI Agent" : "Agħżel Aġent tal-IA", + "Select All" : "Agħżel Kollha", + "Select ConfigSet" : "Agħżel ConfigSet", + "Select File Types to Vectorize:" : "Agħżel it-Tipi ta' Fajls biex Jiġu Vettorizzati:", + "Select HTTP method" : "Agħżel metodu HTTP", + "Select Register and Schema" : "Agħżel Reġistru u Skema", + "Select Views to Vectorize:" : "Agħżel il-Vedute biex Jiġu Vettorizzati:", + "Select a Nextcloud group to add" : "Agħżel grupp ta' Nextcloud biex iżżid", + "Select a branch" : "Agħżel branch", + "Select a model or type a custom model name" : "Agħżel mudell jew ittajpja isem ta' mudell personalizzat", + "Select a repository" : "Agħżel repository", + "Select a repository you have write access to" : "Agħżel repository li għandek aċċess ta' kitba għalih", + "Select an AI Agent" : "Agħżel Aġent tal-IA", + "Select an AI agent to begin chatting with your data." : "Agħżel aġent tal-IA biex tibda titkellem mad-data tiegħek.", + "Select backend" : "Agħżel backend", + "Select chat model" : "Agħżel mudell tal-chat", + "Select collection for files" : "Agħżel ġabra għall-Fajls", + "Select collection for objects" : "Agħżel ġabra għall-Oġġetti", + "Select default organisation" : "Agħżel organizzazzjoni awtomatika", + "Select event to listen to..." : "Agħżel avveniment biex tisma'...", + "Select model" : "Agħżel mudell", + "Select options..." : "Agħżel għażliet...", + "Select period" : "Agħżel perjodu", + "Select property to send as payload" : "Agħżel Proprjetà biex tintbagħat bħala payload", + "Select provider" : "Agħżel fornitur", + "Select registers and schemas to save a view" : "Agħżel Reġistri u skemi biex tissejvja veduta", + "Select retry policy" : "Agħżel politika tal-provi", + "Select the branch to publish to" : "Agħżel il-branch fejn tippubblika", + "Select the event this webhook should listen to" : "Agħżel l-avveniment li dan il-webhook għandu jisma'", + "Select views to vectorize:" : "Agħżel il-vedute biex jiġu vettorizzati:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Agħżel liema gruppi ta' Nextcloud huma disponibbli għal din l-organizzazzjoni. L-utenti f'dawn il-gruppi ser ikollhom aċċess għar-riżorsi tal-organizzazzjoni.", + "Select which data views the AI can search" : "Agħżel liema vedute tad-data tista' tfittex l-IA", + "Select which property from the event should be used as the webhook payload data" : "Agħżel liema Proprjetà mill-avveniment għandha tintuża bħala d-data tal-payload tal-webhook", + "Select which tools the AI can use to perform actions" : "Agħżel liema għodod tista' tuża l-IA biex twettaq azzjonijiet", + "Selected" : "Magħżul", + "Selected Groups" : "Gruppi Magħżula", + "Selected audit trails deleted successfully" : "It-traċċi tal-awditjar magħżula tħassru b'suċċess", + "Selected search trails deleted successfully" : "It-traċċi tat-tfittxija magħżula tħassru b'suċċess", + "Selected users can access this view" : "L-utenti magħżula jistgħu jaċċessaw din il-veduta", + "Send additional feedback" : "Ibgħat feedback addizzjonali", + "Send as CloudEvent" : "Ibgħat bħala CloudEvent", + "Sensitive PII" : "PII Sensittiv", + "Serial Mode (Safer, slower)" : "Mod Serial (Aktar sikur, aktar bil-mod)", + "Serial:" : "Serial:", + "Server Configuration" : "Konfigurazzjoni tas-Server", + "Server Information" : "Informazzjoni tas-Server", + "Settings" : "Settings", + "Shards" : "Shards", + "Share" : "Aqsam", + "Share with Groups" : "Aqsam mal-Gruppi", + "Share with Users" : "Aqsam mal-Utenti", + "Show Filters" : "Uri l-Filtri", + "Show less" : "Uri inqas", + "Show only entries with changes" : "Uri biss l-annotazzjonijiet b'bidliet", + "Simple" : "Sempliċi", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Mistoqsijiet sempliċi: Tfittxijiet bażiċi ta' test b'parametri minimi (eż., terminu ta' tfittxija wieħed, l-ebda filtri avvanzati)", + "Size" : "Daqs", + "Slug" : "Slug", + "Soft Delete Mode" : "Mod ta' Soft Delete", + "Soft Deleted Items" : "Oġġetti Soft-Imħassra", + "SolrCloud mode enabled" : "Il-mod SolrCloud ġie attivat", + "Source" : "Sors", + "Sources" : "Sorsi", + "Sources:" : "Sorsi:", + "Standalone SOLR mode" : "Mod SOLR standalone", + "Start Conversation" : "Ibda Konversazzjoni", + "Start Vectorization" : "Ibda l-Vettorizzazzjoni", + "Start a conversation" : "Ibda konversazzjoni", + "Starting names cache warmup..." : "Qed jibda t-tisħin tal-cache tal-ismijiet...", + "Starting..." : "Qed jibda...", + "Statistics" : "Statistika", + "Stats calculation not yet implemented" : "Il-kalkolu tal-istatistika għadu mhux implimentat", + "Status" : "Status", + "Status Code" : "Kodiċi tal-Istatus", + "String Constraints" : "Restrizzjonijiet tal-Istring", + "Success" : "Suċċess", + "Success Rate" : "Rata ta' Suċċess", + "Success Status" : "Status ta' Suċċess", + "Successful" : "Suċċess", + "Successfully joined organisation" : "Ingħaqadt mal-organizzazzjoni b'suċċess", + "Sync Table" : "Sinkronizza t-Tabella", + "Synchronization Settings" : "Settings tas-Sinkronizzazzjoni", + "System Default" : "Awtomatiku tas-Sistema", + "Technical Description" : "Deskrizzjoni Teknika", + "Technical description for developers and administrators" : "Deskrizzjoni teknika għall-iżviluppaturi u l-amministraturi", + "Temperature" : "Temperatura", + "Templates" : "Mudelli", + "Test" : "Ittestja", + "Test Chat" : "Ittestja l-Chat", + "Test Connection" : "Ittestja l-Konnessjoni", + "Test Embedding" : "Ittestja l-Embedding", + "Test endpoint executed successfully" : "L-endpoint tat-test ġie eżegwit b'suċċess", + "Test webhook delivered successfully" : "Il-webhook tat-test ġie kkunsinnat b'suċċess", + "Test webhook delivery failed" : "Il-kunsinna tal-webhook tat-test falliet", + "Test webhook sent successfully" : "Il-webhook tat-test intbagħat b'suċċess", + "Testing..." : "Qed jiġi ttestjat...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Il-biċċiet tat-test jiġu ġġenerati waqt l-estrazzjoni tal-Fajls u maħżuna fid-database. Il-vettorizzazzjoni taqra dawn il-Fajls maqsuma minn qabel u tibdilhom f'embeddings.", + "Text extracted successfully" : "It-test ġie estratt b'suċċess", + "Text extraction disabled" : "L-estrazzjoni tat-test ġiet diżattivata", + "The URL where webhook events will be sent" : "L-URL fejn ser jintbagħtu l-avvenimenti tal-webhook", + "The requested conversation does not exist" : "Il-konversazzjoni mitluba ma teżistix", + "The saved view and all its search configuration" : "Il-veduta ssejvjata u l-konfigurazzjoni kollha tat-tfittxija tagħha", + "There are no audit trail entries matching your current filters." : "M'hemm l-ebda annotazzjonijiet tat-traċċa tal-awditjar li jaqblu mal-filtri attwali tiegħek.", + "There are no deleted items matching your current filters." : "M'hemm l-ebda Oġġetti mħassra li jaqblu mal-filtri attwali tiegħek.", + "There are no search trail entries matching your current filters." : "M'hemm l-ebda annotazzjonijiet tat-traċċa tat-tfittxija li jaqblu mal-filtri attwali tiegħek.", + "There are no webhook log entries matching your filters." : "M'hemm l-ebda annotazzjonijiet tal-log tal-webhook li jaqblu mal-filtri tiegħek.", + "This action cannot be undone." : "Din l-azzjoni ma tistax tiġi annullata.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Din l-azzjoni ma tistax tiġi annullata. Kun żgur li l-ebda ġabriet mhumiex jużaw dan il-ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Dan l-aġent ma għandux vedute jew għodod konfigurabbli, iżda xorta tista' taġġusta s-settings RAG hawn taħt.", + "This analysis may take some time" : "Din l-analiżi tista' tieħu ftit ħin", + "This audit trail entry does not contain any change information." : "Din l-annotazzjoni tat-traċċa tal-awditjar ma fihiex informazzjoni dwar xi bidla.", + "This configuration has no data defined." : "Din il-konfigurazzjoni ma għandha l-ebda data definita.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Dan l-endpoint huwa skadut. Uża endpoints ibbażati fuq biċċiet minflok.", + "This entity has no relations to objects or files" : "Din l-entità ma għandha l-ebda relazzjonijiet ma' Oġġetti jew Fajls", + "This is a test webhook from OpenRegister" : "Dan huwa webhook ta' test minn OpenRegister", + "This schema must use Magic Table configuration to sync" : "Din l-iskema trid tuża l-konfigurazzjoni Magic Table biex tissinkronizza", + "This source has no associated registers." : "Dan is-Sors ma għandu l-ebda Reġistri assoċjati.", + "This will copy the _default ConfigSet with the new name" : "Dan ser jikkupja l-ConfigSet _default bl-isem il-ġdid", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Dan ser iħassar permanentement l-embeddings KOLLHA (vetturi) mid-database. Ser ikollok terġa' vettorizza l-Oġġetti u l-Fajls kollha. Din l-azzjoni ma tistax tiġi annullata.\\n\\nInti ċert li trid tkompli?", + "This will permanently delete:" : "Dan ser iħassar permanentement:", + "Timeout (seconds)" : "Timeout (sekondi)", + "Timestamp" : "Timestamp", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tip: Attiva biss vedute li jeħtieġu tfittxija semantika biex tnaqqas l-ispejjeż tal-embedding. It-tabelli sempliċi ta' tiftix rari jeħtieġu vettorizzazzjoni.", + "Title" : "Titlu", + "To Date" : "Sad-Data", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Biex taġġusta d-daqs tal-biċċa jew l-istrateġija, mur fil-Konfigurazzjoni tal-Fajls → Limiti ta' Proċessar.", + "To change the embedding provider or model, go to LLM Configuration." : "Biex tibdel il-fornitur jew il-mudell tal-embedding, mur fil-Konfigurazzjoni tal-LLM.", + "To date" : "Sad-data", + "Toggle search sidebar" : "Aqleb is-sidebar tat-tfittxija", + "Tools" : "Għodod", + "Top Deleters" : "L-Aqwa Ħassara", + "Total" : "Total", + "Total Audit Trails" : "Total Traċċi tal-Awditjar", + "Total Chunks Available:" : "Total Biċċiet Disponibbli:", + "Total Deleted Items" : "Total Oġġetti Mħassra", + "Total Members" : "Total Membri", + "Total Objects" : "Total Oġġetti", + "Total Objects in Database:" : "Total Oġġetti fid-Database:", + "Total Organisations" : "Total Organizzazzjonijiet", + "Total Results" : "Total Riżultati", + "Total Searches" : "Total Tfittxijiet", + "Total Size" : "Daqs Totali", + "Total Storage" : "Ħażna Totali", + "Total:" : "Total:", + "Totals" : "Totali", + "Type" : "Tip", + "Type Issue" : "Kwistjoni ta' Tip", + "Type Variations:" : "Varjazzjonijiet tat-Tip:", + "Type must be \"positive\" or \"negative\"" : "It-tip irid ikun \"positive\" jew \"negative\"", + "Type to search for groups" : "Ittajpja biex tfittex il-gruppi", + "Type to search for users" : "Ittajpja biex tfittex l-utenti", + "Type to search groups..." : "Ittajpja biex tfittex il-gruppi...", + "Type to search users..." : "Ittajpja biex tfittex l-utenti...", + "URL" : "URL", + "URL where Ollama is running" : "URL fejn qed jaħdem Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Mhux mibdul", + "Unique Search Terms" : "Termini ta' Tfittxija Uniċi", + "Unique Terms" : "Termini Uniċi", + "Unknown" : "Mhux magħruf", + "Unknown Register" : "Reġistru Mhux Magħruf", + "Unknown Schema" : "Skema Mhux Magħrufa", + "Untitled View" : "Veduta Bla Titlu", + "Update" : "Aġġorna", + "Update Operations" : "Operazzjonijiet ta' Aġġornament", + "Update configuration: ..." : "Aġġorna l-konfigurazzjoni: ...", + "Update register OAS: ..." : "Aġġorna l-OAS tar-Reġistru: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Aġġorna l-vetturi meta d-data tal-Oġġett tinbidel (rakkomandat għal tfittxija preċiża)", + "Updated" : "Aġġornat", + "Updated At" : "Aġġornat Fil", + "Updated:" : "Aġġornat:", + "Uptime:" : "Uptime:", + "Usage Count" : "Għadd tal-Użu", + "Use SolrCloud with Zookeeper for distributed search" : "Uża SolrCloud ma' Zookeeper għal tfittxija distribwita", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Uża l-filtri biex tnaqqas l-annotazzjonijiet tat-traċċa tal-awditjar skont ir-Reġistru, l-iskema, it-tip ta' azzjoni, l-utent, il-firxa ta' dati, jew l-ID tal-Oġġett.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Uża l-filtri biex tnaqqas l-Oġġetti mħassra skont ir-Reġistru, l-iskema, id-data tat-tħassir, jew l-utent li ħassarhom.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Uża l-filtri biex tnaqqas l-annotazzjonijiet tat-traċċa tat-tfittxija skont ir-Reġistru, l-iskema, l-istatus ta' suċċess, l-utent, il-firxa ta' dati, it-termini ta' tfittxija, jew il-metriċi tal-prestazzjoni.", + "Used By Collections" : "Użat Minn Ġabriet", + "User" : "Utent", + "User Agent Statistics" : "Statistika tal-User Agent", + "User Description" : "Deskrizzjoni tal-Utent", + "User rate limits cleared successfully" : "Il-limiti tar-rata tal-utent tneħħew b'suċċess", + "User removed successfully" : "L-utent tneħħa b'suċċess", + "User-friendly description shown in forms and help text" : "Deskrizzjoni faċli għall-utent murija fil-formoli u t-test tal-għajnuna", + "Username" : "Username", + "Username for SOLR authentication (optional)" : "Username għall-awtentikazzjoni SOLR (fakultattiv)", + "Users" : "Utenti", + "Using Pre-Generated Chunks" : "Bl-Użu ta' Biċċiet Iġġenerati Minn Qabel", + "Validate" : "Valida", + "Validate Objects" : "Valida l-Oġġetti", + "Validation completed successfully" : "Il-validazzjoni tlestiet b'suċċess", + "Validation failed" : "Il-validazzjoni falliet", + "Value" : "Valur", + "Vector Dimensions" : "Dimensjonijiet tal-Vettur", + "Vector Search Backend" : "Backend tat-Tfittxija tal-Vetturi", + "Vector field: _embedding_" : "Kamp tal-vettur: _embedding_", + "Vectorization Triggers" : "Triggers tal-Vettorizzazzjoni", + "Vectorize All Files" : "Vettorizza l-Fajls Kollha", + "Vectorize All Objects" : "Vettorizza l-Oġġetti Kollha", + "Vectorize all file types" : "Vettorizza t-tipi kollha ta' Fajls", + "Vectorize all views" : "Vettorizza l-vedute kollha", + "Vectorize on object creation" : "Vettorizza mal-ħolqien tal-Oġġett", + "Vectors will be stored in your existing object and file collections" : "Il-vetturi ser jinħażnu fil-ġabriet eżistenti tal-Oġġetti u l-Fajls tiegħek", + "Version" : "Verżjoni", + "Very High" : "Għoli Ħafna", + "View API Docs" : "Ara d-Dokumenti tal-API", + "View Changes" : "Ara l-Bidliet", + "View Details" : "Ara d-Dettalji", + "View Error" : "Ara l-Iżball", + "View File" : "Ara l-Fajl", + "View Full Details" : "Ara d-Dettalji Kollha", + "View Logs" : "Ara l-Logs", + "View Name" : "Isem tal-Veduta", + "View Object" : "Ara l-Oġġett", + "View Parameters" : "Ara l-Parametri", + "View Selection" : "Għażla tal-Vedute", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Ara u analizza l-logs tat-traċċa tat-tfittxija b'kapaċitajiet avvanzati ta' filtrazzjoni u analitika", + "View and analyze system audit trails with advanced filtering capabilities" : "Ara u analizza t-traċċi tal-awditjar tas-sistema b'kapaċitajiet avvanzati ta' filtrazzjoni", + "View deleted successfully" : "Il-veduta tħassret b'suċċess", + "View entity details and manage relations" : "Ara d-dettalji tal-entità u amministra r-relazzjonijiet", + "View name" : "Isem tal-veduta", + "View name is required" : "L-isem tal-veduta huwa meħtieġ", + "View saved successfully!" : "Il-veduta ġiet issejvjata b'suċċess!", + "View search analytics and manage search logs" : "Ara l-analitika tat-tfittxija u amministra l-logs tat-tfittxija", + "View successfully deleted" : "Il-veduta tħassret b'suċċess", + "View successfully updated" : "Il-veduta ġiet aġġornata b'suċċess", + "View updated successfully!" : "Il-veduta ġiet aġġornata b'suċċess!", + "View webhook delivery logs and filter by webhook" : "Ara l-logs tal-kunsinna tal-webhook u ffiltra skont il-webhook", + "Views" : "Vedute", + "Visible to users" : "Viżibbli għall-utenti", + "Wait for Response" : "Stenna għar-Rispons", + "Wait for webhook response before continuing (required for request/response flows)" : "Stenna għar-rispons tal-webhook qabel ma tkompli (meħtieġ għall-flussi ta' talba/rispons)", + "Warmup Names Cache" : "Saħħan il-Cache tal-Ismijiet", + "Warning" : "Twissija", + "Warning:" : "Twissija:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Dettalji tal-Log tal-Webhook", + "Webhook Logs" : "Logs tal-Webhook", + "Webhook created successfully" : "Il-webhook inħoloq b'suċċess", + "Webhook deleted" : "Il-webhook tħassar", + "Webhook retry delivered successfully" : "Il-prova tal-webhook ġiet ikkunsinnata b'suċċess", + "Webhook retry delivery failed" : "Il-kunsinna tal-prova tal-webhook falliet", + "Webhook updated" : "Il-webhook ġie aġġornat", + "Webhook updated successfully" : "Il-webhook ġie aġġornat b'suċċess", + "Webhooks" : "Webhooks", + "Weekly" : "Kull ġimgħa", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Geżwer il-payload tal-webhook fil-format CloudEvents għal interoperabbiltà aħjar", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Iva", + "You" : "Inti", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Tista' toħloq ConfigSets ġodda bbażati fuq il-mudell _default, jew ittella' dawk personalizzati direttament fis-server SOLR tiegħek.", + "You do not have access to this conversation" : "Ma għandekx aċċess għal din il-konversazzjoni", + "You do not have permission to delete this conversation" : "Ma għandekx permess biex tħassar din il-konversazzjoni", + "You do not have permission to modify this conversation" : "Ma għandekx permess biex timmodifika din il-konversazzjoni", + "You do not have permission to restore this conversation" : "Ma għandekx permess biex tirrestawra din il-konversazzjoni", + "You must be logged in to favorite views" : "Trid tkun illoggjat biex tagħmel vedute favoriti", + "You need an AI agent to start a conversation." : "Teħtieġ aġent tal-IA biex tibda konversazzjoni.", + "Your Fireworks AI API key. Get one at" : "Iċ-ċavetta API Fireworks AI tiegħek. Ikseb waħda fuq", + "Your OpenAI API key. Get one at" : "Iċ-ċavetta API OpenAI tiegħek. Ikseb waħda fuq", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Il-feedback tiegħek ġie rreġistrat. B'mod fakultattiv, tista' tipprovdi dettalji addizzjonali hawn...", + "Zookeeper Hosts" : "Hosts ta' Zookeeper", + "Zookeeper Port" : "Port ta' Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Settings ta' Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "String tal-konnessjoni Zookeeper għal SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Numru tal-port ta' Zookeeper (fakultattiv, awtomatiku 2181)", + "chunk" : "biċċa", + "chunks" : "biċċiet", + "chunks processed" : "biċċiet ipproċessati", + "configuration(s)" : "konfigurazzjoni(jiet)", + "e.g., lib/Settings/config.json" : "eż., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "eż., lib/Settings/register.json", + "failed" : "falla", + "file" : "fajl", + "files" : "fajls", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "membri", + "menu or contact someone with permission to create agents." : "menu jew ikkuntattja lil xi ħadd b'permess biex joħloq aġenti.", + "messages" : "messaġġi", + "n8n URL and API key are required" : "L-URL n8n u ċ-ċavetta API huma meħtieġa", + "n8n configuration saved successfully" : "Il-konfigurazzjoni n8n ġiet issejvjata b'suċċess", + "n8n connection not configured" : "Il-konnessjoni n8n mhix ikkonfigurata", + "n8n connection successful" : "Il-konnessjoni n8n irnexxiet", + "n8n connection test successful" : "It-test tal-konnessjoni n8n irnexxa", + "n8n integration disabled" : "L-integrazzjoni n8n ġiet diżattivata", + "n8n integration enabled" : "L-integrazzjoni n8n ġiet attivata", + "n8n project initialized successfully" : "Il-proġett n8n ġie inizjalizzat b'suċċess", + "n8n settings saved successfully" : "Is-settings n8n ġew issejvjati b'suċċess", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "oġġetti", + "objects processed" : "oġġetti ipproċessati", + "or pick a range" : "jew agħżel firxa", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "jipprovdi bilanċ tajjeb bejn il-veloċità u l-preċiżjoni.", + "register(s)" : "Reġistru/i", + "register(s) selected" : "Reġistru/i magħżula", + "required" : "meħtieġ", + "results" : "riżultati", + "schema(s)" : "skema/i", + "schema(s) selected" : "skema/i magħżula", + "sk-..." : "sk-...", + "this application" : "din l-applikazzjoni", + "to" : "sa", + "vectorized" : "vettorizzat", + "views selected" : "vedute magħżula", + "ℹ️ Current Configuration" : "ℹ️ Konfigurazzjoni Attwali", + "⚡ Batch Processing" : "⚡ Proċessar f'Batch", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Kuntest komprensiv ✓ Inqas probabbli li jitlef dettalji ✗ Risposti aktar bil-mod ✗ Jista' jinkludi informazzjoni inqas rilevanti", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Risposti aktar mgħaġġla ✓ Tweġibiet aktar iffukati ✗ Jista' jitlef informazzjoni rilevanti", + "✨ AI Features" : "✨ Funzjonalitajiet tal-IA", + "All search trails cleared successfully" : "It-traċċi tat-tfittxija kollha tneħħew b'suċċess", + "Multiple search trail deletion not implemented yet" : "It-tħassir ta' diversi traċċi tat-tfittxija għadu mhux implimentat", + "No expired search trails found to clear" : "Ma nstabu l-ebda traċċi tat-tfittxija skaduti biex jitneħħew", + "Missing message" : "Messaġġ nieqes", + "message content is required" : "il-kontenut tal-messaġġ huwa meħtieġ", + "Missing conversation or agentUuid" : "Konversazzjoni jew agentUuid nieqes", + "Access denied" : "Aċċess miċħud", + "Conversation not found" : "Il-konversazzjoni ma nstabitx", + "AI service not configured" : "Is-servizz tal-IA mhux ikkonfigurat", + "Failed to process message" : "Naqas milli jipproċessa l-messaġġ", + "Missing conversationId" : "conversationId nieqes", + "conversationId is required" : "conversationId huwa meħtieġ", + "Failed to fetch conversation history" : "Naqas milli jġib l-istorja tal-konversazzjoni", + "Failed to clear conversation" : "Naqas milli jneħħi l-konversazzjoni", + "Invalid feedback type" : "Tip ta' feedback invalidu", + "type must be \"positive\" or \"negative\"" : "it-tip irid ikun \"positive\" jew \"negative\"", + "Message not found" : "Il-messaġġ ma nstabx", + "Failed to save feedback" : "Naqas milli jissejvja l-feedback", + "Failed to get chat statistics" : "Naqas milli jikseb l-istatistika tal-chat", + "Not Found" : "Ma nstabx", + "Engine not found" : "L-engine ma nstabx", + "Consumer not found" : "Il-consumer ma nstabx", + "Backend parameter is required" : "Il-parametru tal-backend huwa meħtieġ", + "Failed to get database information: %s" : "Naqas milli jikseb l-informazzjoni tad-database: %s", + "SOLR setup error: %s" : "Żball fl-issettjar SOLR: %s", + "Reindex failed: %s" : "L-indiċjar mill-ġdid falla: %s", + "Anonymized" : "Anonimizzat", + "Entities detected" : "Entitajiet skoperti", + "Extract Now" : "Estratti Issa", + "Extracted at" : "Estratti fil", + "Extraction" : "Estrazzjoni", + "Failed to load extraction data" : "Naqas milli jtella' d-data tal-estrazzjoni", + "Failed to load register data" : "Naqas milli jtella' d-data tar-Reġistru", + "Failed to retrieve extraction status." : "Naqas milli jirkupra l-istatus tal-estrazzjoni.", + "Failed to retrieve objects for file." : "Naqas milli jirkupra l-Oġġetti għall-Fajl.", + "No extraction data available for this file" : "L-ebda data ta' estrazzjoni disponibbli għal dan il-Fajl", + "No register objects reference this file" : "L-ebda Oġġetti tar-Reġistru ma jirreferixxu għal dan il-Fajl", + "Not extracted" : "Mhux estratt", + "Register Objects" : "Oġġetti tar-Reġistru", + "Risk level" : "Livell ta' riskju", + "Text chunks" : "Biċċiet ta' test", + "Unknown error" : "Żball mhux magħruf", + "Very high" : "Għoli ħafna", + "{title} in {register} / {schema}" : "{title} fi {register} / {schema}", + "Push Notifications" : "Notifiki Push", + "Real-time push notification status via notify_push" : "Status tan-notifika push f'ħin reali permezz ta' notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push f'ħin reali mhux disponibbli — l-app notify_push mhux installata", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installa l-app notify_push mill-App Store ta' Nextcloud biex tattiva l-aġġornamenti f'ħin reali.", + "Open Nextcloud App Store" : "Iftaħ l-App Store ta' Nextcloud", + "notify_push is installed but not yet active" : "notify_push huwa installat iżda għadu mhux attiv", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push huwa installat iżda OpenRegister għadu ma kkonfermax push b'suċċess. Qabbad ssejvjar ta' Oġġett biex tattiva, jew iċċekkja l-konfigurazzjoni notify_push tiegħek.", + "notify_push configuration guide" : "gwida tal-konfigurazzjoni notify_push", + "Realtime push active" : "Push f'ħin reali attiv", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "In-notifiki push f'ħin reali huma attivi. Il-klijenti konnessi jirċievu aġġornamenti istantanji meta l-Oġġetti jinħolqu, jiġu aġġornati, jew jitħassru.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);" +); diff --git a/l10n/mt.json b/l10n/mt.json new file mode 100644 index 0000000000..c7ce11ec41 --- /dev/null +++ b/l10n/mt.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin meħtieġ)", + "(no subject)": "(l-ebda suġġett)", + "(no title)": "(l-ebda titlu)", + "(unnamed)": "(bla isem)", + "(untitled card)": "(karta bla titlu)", + "({count} selected)": "({count} magħżula)", + ", {failed} failed": ", {failed} fallew", + "0 = unlimited": "0 = bla limitu", + "A deactivation request is pending.": "Talba għad-diżattivazzjoni qiegħda pendenti.", + "AI Assistant": "Assistent tal-IA", + "AI Chat": "Chat tal-IA", + "AI service not configured": "Is-servizz tal-IA mhux ikkonfigurat", + "API Key": "Ċavetta tal-API", + "API Request Quota (requests/day)": "Kwota ta' Talbiet tal-API (talbiet/jum)", + "API Token Configuration": "Konfigurazzjoni tat-Token tal-API", + "API Tokens": "Tokens tal-API", + "API tokens saved successfully": "It-tokens tal-API ġew salvati b'suċċess", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: dan ir-rapport jgħaqqad kull verwerkingsactiviteit mal-għadd tal-audit-trail tul il-ħajja attribwit lilu. L-awdituri u l-Autoriteit Persoonsgegevens jużaw dan għar-reviżjoni superviżorja.", + "About ConfigSets": "Dwar il-ConfigSets", + "Access denied": "Aċċess miċħud", + "Access organisation dashboard and analytics": "Aċċessa d-dashboard u l-analitika tal-organizzazzjoni", + "Account": "Kont", + "Action": "Azzjoni", + "Action Distribution": "Distribuzzjoni tal-Azzjonijiet", + "Action:": "Azzjoni:", + "Actions": "Azzjonijiet", + "Active": "Attiv", + "Active Collections": "Ġabriet Attivi", + "Active Filters": "Filtri Attivi", + "Active Organisation:": "Organizzazzjoni Attiva:", + "Active Organisations": "Organizzazzjonijiet Attivi", + "Active filters:": "Filtri attivi:", + "Active organisation changed successfully": "L-organizzazzjoni attiva nbidlet b'suċċess", + "Active organisation set successfully": "L-organizzazzjoni attiva ġiet stabbilita b'suċċess", + "Activities": "Attivitajiet", + "Activity": "Attività", + "Activity Period": "Perjodu tal-Attività", + "Add": "Żid", + "Add Attachment": "Żid Mehmuż", + "Add Endpoint": "Żid Endpoint", + "Add Groups": "Żid Gruppi", + "Add Property to '{title}'": "Żid Proprjetà ma' '{title}'", + "Add Register": "Żid Reġistru", + "Add Schema": "Żid Skema", + "Add Source": "Żid Sors", + "Add User": "Żid Utent", + "Add User to Organisation": "Żid Utent mal-Organizzazzjoni", + "Add a contact from any of your address books to associate it with this object.": "Żid kuntatt minn kwalunkwe wieħed mill-kotba tal-indirizzi tiegħek biex tassoċjah ma' dan l-oġġett.", + "Add a file or files": "Żid fajl jew fajls", + "Add card": "Żid karta", + "Add contact": "Żid kuntatt", + "Add endpoint": "Żid endpoint", + "Add oneOf entry": "Żid entrata oneOf", + "Add range": "Żid medda", + "Add schema titles, descriptions, and register information to provide richer context for search": "Żid titli tal-iskema, deskrizzjonijiet u informazzjoni dwar ir-reġistru biex tipprovdi kuntest aktar għani għat-tiftix", + "Add to favorites": "Żid mal-favoriti", + "Added": "Miżjud", + "Added to favorites": "Miżjud mal-favoriti", + "Additional Information": "Informazzjoni Addizzjonali", + "Additional feedback saved. Thank you!": "Feedback addizzjonali ġie salvat. Grazzi!", + "Addresses": "Indirizzi", + "Admin": "Amministratur", + "Advanced": "Avvanzat", + "Advanced Filters": "Filtri Avvanzati", + "Advanced Options": "Għażliet Avvanzati", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Il-filtri avvanzati mhumiex disponibbli meta tuża s-sors tad-database. Aqleb għal Auto jew Indiċi SOLR għall-għażliet ta' filtraġġ.", + "After": "Wara", + "Agent Type": "Tip ta' Aġent", + "Agent deleted successfully": "L-aġent tħassar b'suċċess", + "Agent successfully created": "L-aġent inħoloq b'suċċess", + "Agent successfully updated": "L-aġent ġie aġġornat b'suċċess", + "Agents": "Aġenti", + "Aggregated across schemas": "Aggregat fl-iskemi kollha", + "All": "Kollox", + "All Categories": "Il-Kategoriji Kollha", + "All Collections": "Il-Ġabriet Kollha", + "All Confidence Levels": "Il-Livelli ta' Fiduċja Kollha", + "All ConfigSets": "Il-ConfigSets Kollha", + "All Files": "Il-Fajls Kollha", + "All Levels": "Il-Livelli Kollha", + "All Types": "It-Tipi Kollha", + "All Webhooks": "Il-Webhooks Kollha", + "All actions": "L-azzjonijiet kollha", + "All audit trails cleared successfully": "L-audit trails kollha tneħħew b'suċċess", + "All clear": "Kollox indaf", + "All file types": "It-tipi ta' fajls kollha", + "All registers": "Ir-reġistri kollha", + "All schemas": "L-iskemi kollha", + "All search trails cleared successfully": "It-search trails kollha tneħħew b'suċċess", + "All searches": "It-tiftixiet kollha", + "All users": "L-utenti kollha", + "All-day events": "Avvenimenti tul il-ġurnata kollha", + "Allowed MIME Types": "Tipi MIME Permessi", + "Already linked": "Diġà marbut", + "An error occurred while clearing audit trails": "Seħħ żball waqt it-tneħħija tal-audit trails", + "An error occurred while deleting the audit trail": "Seħħ żball waqt it-tħassir tal-audit trail", + "An error occurred while deleting the object": "Seħħ żball waqt it-tħassir tal-oġġett", + "An error occurred while deleting the view": "Seħħ żball waqt it-tħassir tal-veduta", + "An error occurred while permanently deleting the objects": "Seħħ żball waqt it-tħassir permanenti tal-oġġetti", + "An error occurred while restoring the objects": "Seħħ żball waqt ir-restawr tal-oġġetti", + "An error occurred while saving the organisation": "Seħħ żball waqt is-salvar tal-organizzazzjoni", + "Analysis completed:": "L-analiżi tlestiet:", + "Analysis steps:": "Passi tal-analiżi:", + "Analytics": "Analitika", + "Analyze Objects": "Analizza l-Oġġetti", + "Analyze Properties": "Analizza l-Proprjetajiet", + "Analyze existing properties for improvement opportunities": "Analizza l-proprjetajiet eżistenti għal opportunitajiet ta' titjib", + "Analyzing...": "Qed janalizza...", + "Anonymized": "Anonimizzat", + "Anonymous Group": "Grupp Anonimu", + "Any favorites and sharing settings for this view": "Kwalunkwe favoriti u settings ta' kondiviżjoni għal din il-veduta", + "Any user": "Kwalunkwe utent", + "App ID": "ID tal-App", + "App store cache invalidated successfully": "Il-cache tal-app store ġie invalidat b'suċċess", + "App store cache invalidated: {invalidated}": "Il-cache tal-app store ġie invalidat: {invalidated}", + "Application deleted successfully": "L-applikazzjoni tħassret b'suċċess", + "Application identifier for this configuration (optional)": "Identifikatur tal-applikazzjoni għal din il-konfigurazzjoni (fakultattiv)", + "Application settings": "Settings tal-applikazzjoni", + "Application successfully created": "L-applikazzjoni nħolqot b'suċċess", + "Application successfully updated": "L-applikazzjoni ġiet aġġornata b'suċċess", + "Applications": "Applikazzjonijiet", + "Apply": "Applika", + "Apply Changes": "Applika l-Bidliet", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Applika rwol lill-iskemi kollha f'dan ir-reġistru li m'għandhomx overrides ta' awtorizzazzjoni espliċiti.", + "Approval Progress": "Progress tal-Approvazzjoni", + "Approve": "Approva", + "Approved": "Approvat", + "Archive": "Arkivja", + "Archive conversation": "Arkivja l-konverżazzjoni", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Tarkivja din il-verwerkingsactiviteit? Ir-ringieli tal-audit-trail se jibqgħu jirreferu għaliha.", + "Archived conversations are hidden from your active list": "Il-konverżazzjonijiet arkivjati huma moħbija mil-lista attiva tiegħek", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Żgur li trid TĦASSAR il-ġabra \"{name}\"?\n\nDan se:\n• Iħassar permanentement il-ġabra u d-data kollha tagħha\n• Ineħħi d-dokumenti kollha indiċjati\n• Din l-azzjoni ma tistax titreġġa' lura", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Żgur li trid tnaddaf is-search trails qodma? Dan se jħassar l-entrati li huma eqdem minn 30 jum.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Żgur li trid tnaddaf id-data kollha mill-ġabra \"{name}\"?\n\nDan se:\n• Iħassar id-dokumenti kollha indiċjati\n• Iżomm l-istruttura tal-ġabra intatta\n• Din l-azzjoni ma tistax titreġġa' lura", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Żgur li trid tħassar l-applikazzjoni \"{name}\"? Din l-azzjoni ma tistax titreġġa' lura.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Żgur li trid tħassar l-audit trails magħżula? Din l-azzjoni ma tistax titreġġa' lura.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Żgur li trid tħassar is-search trails magħżula? Din l-azzjoni ma tistax titreġġa' lura.", + "Are you sure you want to delete this ConfigSet?": "Żgur li trid tħassar dan il-ConfigSet?", + "Are you sure you want to delete this configset?": "Żgur li trid tħassar dan il-configset?", + "Are you sure you want to leave '{name}'?": "Żgur li trid titlaq minn '{name}'?", + "Are you sure you want to permanently delete": "Żgur li trid tħassar permanentement", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Żgur li trid terġa' tindiċja l-ġabra \"{name}\"?\n\nDan se:\n• Jerġa' jibni l-indiċi bl-oġġetti kollha\n• Jieħu diversi minuti biex jitlesta\n• Jista' jaffettwa l-prestazzjoni tat-tiftix waqt l-indiċjar mill-ġdid", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Żgur li trid tneħħi l-iskema \"{schema}\" mir-reġistru \"{register}\"? Din l-azzjoni ma tistax titreġġa' lura.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Żgur li trid tagħmel soft-delete ta' {count} oġġetti attivi għall-iskema \"{schema}\"?\n\nDawn se jiġu mmarkati bħala mħassra iżda jistgħu jitneħħew permanentement aktar tard.", + "Array Object Configuration:": "Konfigurazzjoni tal-Oġġett Array:", + "Articles": "Artikli", + "Ask a question...": "Staqsi mistoqsija...", + "Ask questions about your data using natural language": "Staqsi mistoqsijiet dwar id-data tiegħek bl-użu ta' lingwaġġ naturali", + "Assigned collaborative tags": "Tags kollaborattivi assenjati", + "Assignment notifications": "Notifiki ta' assenjazzjoni", + "Attachment added to {name}": "Mehmuż miżjud ma' {name}", + "Attempt": "Tentattiv", + "Audit Trail Changes": "Bidliet fl-Audit Trail", + "Audit Trail Details": "Dettalji tal-Audit Trail", + "Audit Trail Management": "Ġestjoni tal-Audit Trail", + "Audit Trail Statistics": "Statistika tal-Audit Trail", + "Audit Trails": "Audit Trails", + "Audit trail data copied to clipboard": "Id-data tal-audit trail ġiet ikkupjata fil-clipboard", + "Audit trail deleted successfully": "L-audit trail tħassar b'suċċess", + "Audit trail successfully deleted": "L-audit trail tħassar b'suċċess", + "Authentication": "Awtentikazzjoni", + "Auto (e.g. Y for year, F Y for month)": "Auto (eż. Y għas-sena, F Y għax-xahar)", + "Auto share": "Kondiviżjoni awtomatika", + "Auto-commit disabled": "Auto-commit diżattivat", + "Auto-commit enabled": "Auto-commit attivat", + "Auto-create Default Organisation": "Oħloq Awtomatikament Organizzazzjoni Default", + "Auto-retry failed vectorizations": "Erġa' pprova awtomatikament il-vettorizzazzjonijiet li fallew", + "Automatically commit changes to SOLR index": "Wettaq awtomatikament il-bidliet fl-indiċi SOLR", + "Automatically connect to the federation network.": "Qabbad awtomatikament man-netwerk tal-federazzjoni.", + "Automatically create a default organisation if none exists when the app is initialized": "Oħloq awtomatikament organizzazzjoni default jekk ma teżisti l-ebda waħda meta l-app tiġi inizjalizzata", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Iġġenera awtomatikament vector embeddings mit-text chunks meta l-fajls jittellgħu u jiġu pproċessati", + "Automatically generate vector embeddings when objects are created or updated": "Iġġenera awtomatikament vector embeddings meta l-oġġetti jinħolqu jew jiġu aġġornati", + "Automatically retry failed vectorization attempts (max 3 retries)": "Erġa' pprova awtomatikament it-tentattivi ta' vettorizzazzjoni li fallew (massimu 3 tentattivi)", + "Automatically update catalogues.": "Aġġorna awtomatikament il-katalgi.", + "Available Filters": "Filtri Disponibbli", + "Available Workflows": "Workflows Disponibbli", + "Available placeholders:": "Placeholders disponibbli:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Il-bidliet fl-avatar mhumiex appoġġjati mill-fornitur tal-awtentikazzjoni tiegħek.", + "Avatar removed": "L-avatar tneħħa", + "Avatar updated successfully": "L-avatar ġie aġġornat b'suċċess", + "Avg Execution Time": "Ħin Medju ta' Eżekuzzjoni", + "Avg Members/Org": "Membri Medji/Org", + "Avg Object Views/Session": "Veduti Medji tal-Oġġetti/Sessjoni", + "Avg Response Time": "Ħin Medju ta' Rispons", + "Avg Results/Search": "Riżultati Medji/Tiftixa", + "Avg Searches/Session": "Tiftixiet Medji/Sessjoni", + "Back": "Lura", + "Back to Entities": "Lura għall-Entitajiet", + "Back to Registers": "Lura għar-Reġistri", + "Back to Webhooks": "Lura għall-Webhooks", + "Back to applications": "Lura għall-applikazzjonijiet", + "Back to entities": "Lura għall-entitajiet", + "Backend parameter is required": "Il-parametru tal-backend huwa meħtieġ", + "Backend updated successfully. Please reload the application.": "Il-backend ġie aġġornat b'suċċess. Jekk jogħġbok erġa' tella' l-applikazzjoni.", + "Bandwidth Quota (MB/month)": "Kwota tal-Bandwidth (MB/xahar)", + "Base URL (Optional)": "URL Bażi (Fakultattiv)", + "Basic Information": "Informazzjoni Bażika", + "Batch Size": "Daqs tal-Batch", + "Batch extraction completed": "L-estrazzjoni tal-batch tlestiet", + "Before": "Qabel", + "Before object vectorization can work:": "Qabel ma l-vettorizzazzjoni tal-oġġetti tista' taħdem:", + "Behavior": "Imġiba", + "Behavior Issue": "Problema fl-Imġiba", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (durata ISO-8601, eż. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Il-ħażna Blob ġiet irtirata. L-oġġetti kollha issa jużaw magic tables.", + "Bookmarks": "Bookmarks", + "Branch": "Fergħa", + "Bulk Role Assignment: {title}": "Assenjazzjoni ta' Rwol bil-Massa: {title}", + "Bulk delete operation completed successfully": "L-operazzjoni ta' tħassir bil-massa tlestiet b'suċċess", + "Bulk save operation completed successfully": "L-operazzjoni ta' salvar bil-massa tlestiet b'suċċess", + "Business Data": "Data tan-Negozju", + "CSV": "CSV", + "Cache cleared successfully": "Il-cache tneħħa b'suċċess", + "Cache grew from {old} to {new} entries.": "Il-cache kiber minn {old} għal {new} entrati.", + "Cache shrunk from {old} to {new} entries.": "Il-cache naqas minn {old} għal {new} entrati.", + "Cache stayed the same at {size} entries.": "Il-cache baqa' l-istess fuq {size} entrati.", + "Calculate Sizes": "Ikkalkula d-Daqsijiet", + "Calendar": "Kalendarju", + "Calendar Provider Configuration": "Konfigurazzjoni tal-Fornitur tal-Kalendarju", + "Calendar integration is not available": "L-integrazzjoni tal-kalendarju mhix disponibbli", + "Calendar name": "Isem il-kalendarju", + "Cancel": "Ikkanċella", + "Cancel deactivation request": "Ikkanċella t-talba għad-diżattivazzjoni", + "Cannot delete: objects are still attached": "Ma jistax jitħassar: l-oġġetti għadhom mehmuża", + "Cannot edit: This register is managed by external configuration {title}": "Ma jistax jiġi editjat: Dan ir-reġistru huwa ġestit minn konfigurazzjoni esterna {title}", + "Cannot remove schema with existing objects ({count} objects)": "Ma tistax titneħħa skema b'oġġetti eżistenti ({count} oġġetti)", + "Cannot remove schema {schema} because it contains {count} objects": "Ma tistax titneħħa l-iskema {schema} għax fiha {count} oġġetti", + "Cards": "Karti", + "Cascade delete": "Tħassir kaskata", + "Categories of data subjects (one per line)": "Kategoriji ta' suġġetti tad-data (waħda kull linja)", + "Categories of personal data (one per line)": "Kategoriji ta' data personali (waħda kull linja)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategorija", + "Change Type": "Ibdel it-Tip", + "Change password": "Ibdel il-password", + "Changes": "Bidliet", + "Changes copied to clipboard": "Il-bidliet ġew ikkupjati fil-clipboard", + "Changes for Audit Trail #{id}": "Bidliet għall-Audit Trail #{id}", + "Chat Model": "Mudell tal-Chat", + "Chat Provider": "Fornitur tal-Chat", + "Chat Provider (RAG)": "Fornitur tal-Chat (RAG)", + "Chat Settings": "Settings tal-Chat", + "Chat provider connection successful!": "Il-konnessjoni mal-fornitur tal-chat irnexxiet!", + "Chat settings": "Settings tal-chat", + "Chat test failed: {error}": "It-test tal-chat falla: {error}", + "Choose a register": "Agħżel reġistru", + "Choose a schema": "Agħżel skema", + "Choose how vector similarity calculations are performed for semantic search": "Agħżel kif jitwettqu l-kalkoli tas-similarità tal-vetturi għat-tiftix semantiku", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Agħżel liema tipi ta' fajls għandhom jiġu inklużi fil-proċess ta' vettorizzazzjoni. Se jiġu pproċessati biss il-fajls bit-test estratt u chunks.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Agħżel liema veduti għandhom jiġu inklużi fil-proċess ta' vettorizzazzjoni. Ħalli vojt biex tipproċessa l-veduti kollha skont il-konfigurazzjoni tiegħek.", + "Chunk": "Chunk", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "It-tħassir tal-chunks għadu mhux implimentat. Uża endpoints ibbażati fuq chunks.", + "Chunks": "Chunks", + "Chunks to Vectorize:": "Chunks li għandhom jiġu Vettorizzati:", + "Cleanup Old Trails": "Naddaf it-Trails Qodma", + "Cleanup completed": "It-tindif tlesta", + "Cleanup completed successfully. Deleted {count} entries.": "It-tindif tlesta b'suċċess. Tħassru {count} entrati.", + "Cleanup failed: {error}": "It-tindif falla: {error}", + "Clear All Embeddings": "Naddaf l-Embeddings Kollha", + "Clear Entries": "Naddaf l-Entrati", + "Clear Filtered Audit Trails": "Naddaf l-Audit Trails Iffiltrati", + "Clear Filters": "Naddaf il-Filtri", + "Clear Index": "Naddaf l-Indiċi", + "Clear Selection": "Naddaf l-Għażla", + "Clear all": "Naddaf kollox", + "Clear all filters": "Naddaf il-filtri kollha", + "Clear date range": "Naddaf il-medda tad-dati", + "Clear filters": "Naddaf il-filtri", + "Clearing...": "Qed inaddaf...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Ikklikkja Mexxi l-iskan tal-konformità biex issib l-iskemi fejn ġie identifikat PII iżda ma teżisti l-ebda annotazzjoni ta' verwerkingsactiviteit.", + "Click to subscribe to notifications": "Ikklikkja biex tabbona għan-notifiki", + "Click to unsubscribe from notifications": "Ikklikkja biex tikkanċella l-abbonament għan-notifiki", + "Close": "Agħlaq", + "Cluster name": "Isem il-cluster", + "Code": "Kodiċi", + "Code (short readable key, e.g. v-2026-001)": "Kodiċi (ċavetta qasira li tinqara, eż. v-2026-001)", + "Collapse details": "Aqfel id-dettalji", + "Collection Management": "Ġestjoni tal-Ġabriet", + "Collection Name": "Isem il-Ġabra", + "Collection assignments updated successfully": "L-assenjazzjonijiet tal-ġabra ġew aġġornati b'suċċess", + "Collection cleared successfully": "Il-ġabra tneħħiet b'suċċess", + "Collection copied successfully": "Il-ġabra ġiet ikkupjata b'suċċess", + "Collection created successfully": "Il-ġabra nħolqot b'suċċess", + "Collection deleted successfully": "Il-ġabra tħassret b'suċċess", + "Collection used to store and index file metadata and content": "Ġabra użata biex taħżen u tindiċja l-metadata u l-kontenut tal-fajls", + "Collection used to store and index object data": "Ġabra użata biex taħżen u tindiċja d-data tal-oġġetti", + "Collections:": "Ġabriet:", + "Color": "Kulur", + "Comment...": "Kumment...", + "Commit Message": "Messaġġ tal-Commit", + "Commit Within (ms)": "Commit Fi (ms)", + "Commit message": "Messaġġ tal-commit", + "Compare current schema with real object data": "Qabbel l-iskema attwali mad-data reali tal-oġġetti", + "Completed": "Tlesta", + "Complex": "Kumpless", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Mistoqsijiet kumplessi: Tiftixiet avvanzati b'filtri multipli, operaturi u kombinazzjonijiet kumplessi ta' parametri", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Mistoqsijiet kumplessi: tiftixiet avvanzati b'filtri multipli, operaturi u kombinazzjonijiet kumplessi ta' parametri", + "Compliance": "Konformità", + "Confidence Level": "Livell ta' Fiduċja", + "Confidence Score": "Punteġġ ta' Fiduċja", + "Confidence: {confidence}%": "Fiduċja: {confidence}%", + "Config must be provided as an object": "Il-config għandu jiġi pprovdut bħala oġġett", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Ġestjoni tal-ConfigSet", + "ConfigSet Name": "Isem il-ConfigSet", + "ConfigSet created successfully": "Il-ConfigSet inħoloq b'suċċess", + "ConfigSet deleted successfully": "Il-ConfigSet tħassar b'suċċess", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Il-ConfigSets jiddefinixxu l-iskema u l-konfigurazzjoni għall-ġabriet SOLR tiegħek. Fihom id-definizzjonijiet tal-kampijiet, analizzaturi u settings oħra tat-tiftix.", + "Configuration": "Konfigurazzjoni", + "Configuration Data": "Data tal-Konfigurazzjoni", + "Configuration is up to date": "Il-konfigurazzjoni hija aġġornata", + "Configuration saved": "Il-konfigurazzjoni ġiet salvata", + "Configuration saved successfully.": "Il-konfigurazzjoni ġiet salvata b'suċċess.", + "Configuration type (default, application, etc.)": "Tip ta' konfigurazzjoni (default, applikazzjoni, eċċ.)", + "Configurations": "Konfigurazzjonijiet", + "Configure API tokens for external service integrations": "Ikkonfigura t-tokens tal-API għall-integrazzjonijiet ta' servizzi esterni", + "Configure Apache SOLR search engine for advanced search capabilities": "Ikkonfigura l-magna tat-tiftix Apache SOLR għal kapaċitajiet ta' tiftix avvanzati", + "Configure Facets": "Ikkonfigura l-Facets", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Ikkonfigura l-fornituri tal-Large Language Model (LLM) għal funzjonijiet imħaddma mill-IA inkluż tiftix semantiku, embeddings u chat.", + "Configure SOLR Facets": "Ikkonfigura l-Facets SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Ikkonfigura s-settings bażiċi tal-konnessjoni għas-server SOLR tiegħek inkluż l-awtentikazzjoni u l-għażliet tan-netwerk. Uża d-djalogi separati tal-ConfigSet u l-Ġestjoni tal-Ġabriet biex tiġġestixxi l-cores u l-ġabriet.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Ikkonfigura s-settings bażiċi tal-konnessjoni għas-server SOLR tiegħek inkluż l-awtentikazzjoni u l-għażliet tan-netwerk. Uża d-djalogi separati tal-configset u l-ġestjoni tal-ġabriet biex tiġġestixxi l-cores u l-ġabriet.", + "Configure default organisation and organisation-related settings": "Ikkonfigura l-organizzazzjoni default u s-settings relatati mal-organizzazzjoni", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Ikkonfigura kif l-oġġetti tad-database jiġu konvertiti f'vector embeddings għat-tiftix semantiku. L-oġġetti jiġu vettorizzati direttament mingħajr ma tkun meħtieġa estrazzjoni ta' test.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Ikkonfigura kif l-oġġetti jiġu konvertiti f'test qabel il-vettorizzazzjoni. Dawn is-settings jaffettwaw il-kwalità u l-kuntest tat-tiftix.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Ikkonfigura l-fornituri tal-large language model (LLM) għal funzjonijiet imħaddma mill-IA inkluż tiftix semantiku, embeddings u chat.", + "Configure multi-organization support and tenant isolation": "Ikkonfigura l-appoġġ għal organizzazzjonijiet multipli u l-iżolament tat-tenants", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Ikkonfigura l-parametri għall-vettorizzazzjoni tal-fajls. Dan il-proċess se jiġġenera vector embeddings għall-chunks kollha tal-fajls estratti.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Ikkonfigura l-parametri għall-vettorizzazzjoni tal-oġġetti. Dan il-proċess se jiġġenera vector embeddings għall-oġġetti kollha li jaqblu mal-filtri tal-veduta tiegħek.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Ikkonfigura din l-iskema biex turi l-oġġetti bħala avvenimenti fl-app tal-Kalendarju ta' Nextcloud.", + "Configure which types of data to search and how many sources to retrieve": "Ikkonfigura liema tipi ta' data għandhom jiġu mfittxija u kemm-il sors għandu jinġieb", + "Configuring field mappings...": "Qed jikkonfigura l-mappings tal-kampijiet...", + "Confirm Account Deactivation": "Ikkonferma d-Diżattivazzjoni tal-Kont", + "Confirm Object Deletion": "Ikkonferma t-Tħassir tal-Oġġett", + "Confirm Object Restoration": "Ikkonferma r-Restawr tal-Oġġett", + "Confirm Permanent Object Deletion": "Ikkonferma t-Tħassir Permanenti tal-Oġġett", + "Confirm deactivation": "Ikkonferma d-diżattivazzjoni", + "Connecting to SOLR cluster...": "Qed jaqbad mal-cluster SOLR...", + "Connection Failed": "Il-Konnessjoni Falliet", + "Connection Settings": "Settings tal-Konnessjoni", + "Connection Successful!": "Il-Konnessjoni rnexxiet!", + "Connection failed": "Il-konnessjoni falliet", + "Connection protocol": "Protokoll tal-konnessjoni", + "Connection successful": "Il-konnessjoni rnexxiet", + "Connection timeout in seconds": "Timeout tal-konnessjoni f'sekondi", + "Constant delay between retries (5 minutes)": "Dewmien kostanti bejn it-tentattivi (5 minuti)", + "Constraint Issue": "Problema ta' Restrizzjoni", + "Consumer not found": "Il-konsumatur ma nstabx", + "Contacts": "Kuntatti", + "Contacts integration is not available": "L-integrazzjoni tal-kuntatti mhix disponibbli", + "Content Filters": "Filtri tal-Kontenut", + "Continue to Properties": "Kompli għall-Proprjetajiet", + "Contracts": "Kuntratti", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Ikkontrolla liema veduti tal-oġġetti għandhom jiġu vettorizzati biex titnaqqas l-ispiża tal-API. Vettorizza biss il-veduti li jibbenefikaw mit-tiftix semantiku.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Ikkontrolla liema veduti u għodod l-IA tista' tuża f'din il-konverżazzjoni. B'mod default, il-kapaċitajiet kollha tal-aġent huma attivati.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Jikkontrolla l-każwalità (0 = deterministiku, 2 = kreattiv ħafna)", + "Conversation ID is required": "L-ID tal-konverżazzjoni huwa meħtieġ", + "Conversation archived": "Il-konverżazzjoni ġiet arkivjata", + "Conversation archived successfully": "Il-konverżazzjoni ġiet arkivjata b'suċċess", + "Conversation cleared successfully": "Il-konverżazzjoni tneħħiet b'suċċess", + "Conversation deleted": "Il-konverżazzjoni tħassret", + "Conversation not found": "Il-konverżazzjoni ma nstabitx", + "Conversation permanently deleted": "Il-konverżazzjoni tħassret permanentement", + "Conversation renamed": "Il-konverżazzjoni ġiet imsemmija mill-ġdid", + "Conversation restored": "Il-konverżazzjoni ġiet restawrata", + "Conversation started with {agent}": "Il-konverżazzjoni nbdiet ma' {agent}", + "Conversation title": "Titlu tal-konverżazzjoni", + "Conversations": "Konverżazzjonijiet", + "Converting {schema} to blob storage...": "Qed jikkonverti {schema} għall-ħażna blob...", + "Converting {schema} to magic table...": "Qed jikkonverti {schema} għal magic table...", + "Copied!": "Ikkupjat!", + "Copy": "Ikkopja", + "Copy Changes": "Ikkopja l-Bidliet", + "Copy Collection": "Ikkopja l-Ġabra", + "Copy Data": "Ikkopja d-Data", + "Copy Full Data": "Ikkopja d-Data Sħiħa", + "Copy of {name}": "Kopja ta' {name}", + "Copy to clipboard": "Ikkopja fil-clipboard", + "Copying...": "Qed jikkopja...", + "Costs": "Spejjeż", + "Count": "Għadd", + "Counting objects...": "Qed jgħodd l-oġġetti...", + "Create": "Oħloq", + "Create API Token": "Oħloq Token tal-API", + "Create Agent": "Oħloq Aġent", + "Create Application": "Oħloq Applikazzjoni", + "Create Collection": "Oħloq Ġabra", + "Create ConfigSet": "Oħloq ConfigSet", + "Create New Collection": "Oħloq Ġabra Ġdida", + "Create New ConfigSet": "Oħloq ConfigSet Ġdid", + "Create Operations": "Operazzjonijiet ta' Ħolqien", + "Create Organisation": "Oħloq Organizzazzjoni", + "Create Webhook": "Oħloq Webhook", + "Create a copy of collection:": "Oħloq kopja tal-ġabra:", + "Create a new ConfigSet based on the _default template": "Oħloq ConfigSet ġdid ibbażat fuq il-mudell _default", + "Create a new SOLR collection from an existing ConfigSet": "Oħloq ġabra SOLR ġdida minn ConfigSet eżistenti", + "Create a new SOLR collection from an existing configset": "Oħloq ġabra SOLR ġdida minn configset eżistenti", + "Create a new configset based on the _default template": "Oħloq configset ġdid ibbażat fuq il-mudell _default", + "Create a new event or link an existing one from any of your calendars.": "Oħloq avveniment ġdid jew orbot wieħed eżistenti minn kwalunkwe wieħed mill-kalendarji tiegħek.", + "Create another": "Oħloq ieħor", + "Create copies of": "Oħloq kopji ta'", + "Create event": "Oħloq avveniment", + "Create new token": "Oħloq token ġdid", + "Create or link a Deck card to track work on this object.": "Oħloq jew orbot karta Deck biex issegwi x-xogħol fuq dan l-oġġett.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Oħloq l-ewwel verwerkingsactiviteit biex tibda tittaggja r-ringieli tal-audit-trail bl-attribuzzjoni tagħhom skont l-AVG Art 30.", + "Create your first AI agent to get started.": "Oħloq l-ewwel aġent tal-IA tiegħek biex tibda.", + "Created": "Maħluq", + "Created:": "Maħluq:", + "Creating search collections...": "Qed joħloq il-ġabriet tat-tiftix...", + "Creating...": "Qed joħloq...", + "Current": "Attwali", + "Current Filters": "Filtri Attwali", + "Current Type:": "Tip Attwali:", + "Current password": "Password attwali", + "Custom API endpoint if using a different region": "Endpoint tal-API personalizzat jekk tuża reġjun differenti", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Headers HTTP personalizzati (wieħed kull linja, format: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Headers HTTP personalizzati (wieħed kull linja, format: header-name: value)", + "Custom Ranges:": "Meddiet Personalizzati:", + "Custom display title for this facet": "Titlu ta' wiri personalizzat għal dan il-facet", + "Custom pattern:": "Mudell personalizzat:", + "DELETE COLLECTION": "ĦASSAR IL-ĠABRA", + "DSAR": "DSAR", + "Daily": "Kuljum", + "Dashboard": "Dashboard", + "Dashboard not found": "Id-dashboard ma nstabx", + "Data": "Data", + "Data Source": "Sors tad-Data", + "Data Sources": "Sorsi tad-Data", + "Data Views": "Veduti tad-Data", + "Data sources": "Sorsi tad-data", + "Data type does not match observed values": "It-tip ta' data ma jaqbilx mal-valuri osservati", + "Data-subject access request": "Talba għall-aċċess minn suġġett tad-data", + "Database URL": "URL tad-Database", + "Database information refreshed": "L-informazzjoni tad-database ġiet aġġornata", + "Date Faceting:": "Faceting tad-Dati:", + "Date Range": "Medda tad-Dati", + "Dates": "Dati", + "Deactivation request cancelled": "It-talba għad-diżattivazzjoni ġiet ikkanċellata", + "Deactivation request submitted": "It-talba għad-diżattivazzjoni ġiet sottomessa", + "Deck": "Deck", + "Deck integration is not available": "L-integrazzjoni ma' Deck mhix disponibbli", + "Default": "Predefinit", + "Default Object Tenant": "Tenant tal-Oġġett Predefinit", + "Default Organisation": "Organizzazzjoni Predefinita", + "Default Owner": "Sid Predefinit", + "Default User (for cron/background jobs)": "Utent Predefinit (għax-xogħlijiet cron/fl-isfond)", + "Default User Tenant": "Tenant tal-Utent Predefinit", + "Default value": "Valur predefinit", + "Default value (Email)": "Valur predefinit (Email)", + "Default value (Password)": "Valur predefinit (Password)", + "Default value (Phone number)": "Valur predefinit (Numru tat-telefon)", + "Default value (Regex)": "Valur predefinit (Regex)", + "Default: 5": "Predefinit: 5", + "Defaults to current user. Select a different user if needed.": "Jistabbilixxi bħala predefinit l-utent attwali. Agħżel utent differenti jekk meħtieġ.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Id-dewmien jirdoppja ma' kull tentattiv (2, 4, 8 minuti...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Id-dewmien jiżdied b'mod lineari (5, 10, 15 minuti...)", + "Delete": "Ħassar", + "Delete ({count})": "Ħassar ({count})", + "Delete Application": "Ħassar l-Applikazzjoni", + "Delete Audit Trail": "Ħassar it-Traċċa tal-Verifika", + "Delete Collection": "Ħassar il-Ġabra", + "Delete ConfigSet": "Ħassar il-ConfigSet", + "Delete Objects": "Ħassar l-Oġġetti", + "Delete Operations": "Ħassar l-Operazzjonijiet", + "Delete SOLR Collection": "Ħassar il-Ġabra SOLR", + "Delete View": "Ħassar il-Veduta", + "Delete all objects in this schema": "Ħassar l-oġġetti kollha f'din l-iSkema", + "Delete permanently": "Ħassar b'mod permanenti", + "Delete this conversation?": "Tħassar din il-konverżazzjoni?", + "Delete view": "Ħassar il-veduta", + "Delete {count} object": "Ħassar {count} oġġett", + "Delete {count} objects": "Ħassar {count} oġġetti", + "Deleted": "Imħassar", + "Deleted By": "Imħassar Minn", + "Deleted Date": "Data tat-Tħassir", + "Deleted Items Management": "Ġestjoni tal-Elementi Mħassra", + "Deleted This Week": "Imħassar Din il-Ġimgħa", + "Deleted Today": "Imħassar Illum", + "Deleted:": "Imħassar:", + "Deleting SOLR Collection...": "Qed titħassar il-Ġabra SOLR...", + "Deleting...": "Qed jitħassar...", + "Deletion Date Range": "Medda ta' Dati tat-Tħassir", + "Deletion Statistics": "Statistika tat-Tħassir", + "Deprecated": "Skadut", + "Depublish": "Neħħi mill-pubblikazzjoni", + "Description": "Deskrizzjoni", + "Description Template": "Mudell tad-Deskrizzjoni", + "Description shown as tooltip": "Deskrizzjoni murija bħala tooltip", + "Description:": "Deskrizzjoni:", + "Detect data types and patterns": "Identifika t-tipi ta' data u x-xejriet", + "Detected At": "Identifikat Fl-", + "Detected Format:": "Format Identifikat:", + "Detected Issues:": "Problemi Identifikati:", + "Detected: {date}": "Identifikat: {date}", + "Direct permission": "Permess dirett", + "Disable": "Diżattiva", + "Disable this to make the agent": "Diżattiva dan biex tagħmel l-aġent", + "Disabled": "Diżattivat", + "Discover Files": "Skopri l-Fajls", + "Discovered Properties": "Proprjetajiet Skoperti", + "Display Format": "Format tal-Wiri", + "Display Name": "Isem tal-Wiri", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Trid tħassar b'mod permanenti l-entrati kollha ffiltrati tat-traċċa tal-verifika? Din l-azzjoni ma tistax titħassar.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Trid tħassar b'mod permanenti din l-entrata tat-traċċa tal-verifika? Din l-azzjoni ma tistax titħassar.", + "Documents": "Dokumenti", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR attivat", + "Download API Spec": "Niżżel l-Ispeċifikazzjoni API", + "Download API Specification": "Niżżel l-Ispeċifikazzjoni API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Niżżel kopja tad-data personali kollha tiegħek maħżuna f'OpenRegister (GDPR Artikolu 20).", + "Drag a file or files here": "Iġbed fajl jew fajls hawn", + "Dry run completed: {count} fields would be created": "Il-prova niexfa tlestiet: {count} oqsma jinħolqu", + "Dry run completed: {count} fields would be fixed": "Il-prova niexfa tlestiet: {count} oqsma jiġu rranġati", + "Duration in seconds (optional)": "Tul ta' żmien f'sekondi (mhux obbligatorju)", + "Edit": "Editja", + "Edit Agent": "Editja l-Aġent", + "Edit Application": "Editja l-Applikazzjoni", + "Edit Configuration": "Editja l-Konfigurazzjoni", + "Edit Endpoint": "Editja l-Endpoint", + "Edit Organisation": "Editja l-Organizzazzjoni", + "Edit Property '{key}' of '{title}'": "Editja l-Proprjetà '{key}' ta' '{title}'", + "Edit Register": "Editja r-Reġistru", + "Edit Schema": "Editja l-iSkema", + "Edit Source": "Editja s-Sors", + "Edit View": "Editja l-Veduta", + "Edit Webhook": "Editja l-Webhook", + "Edit labels": "Editja t-tikketti", + "Edit processing activity": "Editja l-attività ta' pproċessar", + "Edit tags for {name}": "Editja t-tags għal {name}", + "Edit verwerkingsactiviteit": "Editja l-verwerkingsactiviteit", + "Edit view": "Editja l-veduta", + "Edit view details": "Editja d-dettalji tal-veduta", + "Effectiveness": "Effettività", + "Email": "Email", + "Email Addresses": "Indirizzi tal-Email", + "Email digest frequency": "Frekwenza tas-sommarju bl-email", + "Emails": "Emails", + "Embedding Model": "Mudell tal-Embedding", + "Embedding Provider": "Fornitur tal-Embedding", + "Embedding generated successfully": "L-embedding inħoloq b'suċċess", + "Embedding provider connection successful!": "Il-konnessjoni mal-fornitur tal-embedding irnexxiet!", + "Embedding test failed: {error}": "Il-prova tal-embedding falliet: {error}", + "Enable": "Attiva", + "Enable RAG": "Attiva RAG", + "Enable auto-creation": "Attiva l-ħolqien awtomatiku", + "Enable automatic file vectorization": "Attiva l-vettorizzazzjoni awtomatika tal-fajls", + "Enable automatic object vectorization": "Attiva l-vettorizzazzjoni awtomatika tal-oġġetti", + "Enable automatic synchronization": "Attiva s-sinkronizzazzjoni awtomatika", + "Enable calendar provider": "Attiva l-fornitur tal-kalendarju", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Attiva l-illoggjar dettaljat għall-operazzjonijiet SOLR (rakkomandat għad-debugging)", + "Enable faceting": "Attiva l-faceting", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Attiva jew diżattiva l-funzjonijiet LLM. Ikkonfigura l-fornituri u l-mudelli bl-użu tal-buttuna LLM Configuration hawn fuq.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Attiva jew diżattiva l-funzjonijiet LLM. Ikkonfigura l-fornituri u l-mudelli bl-użu tal-buttuna ta' konfigurazzjoni LLM hawn fuq.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Attiva jew diżattiva l-integrazzjoni tat-tiftix SOLR. Ikkonfigura s-settings tal-konnessjoni bl-użu tal-buttuna Connection Settings hawn fuq.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Attiva jew diżattiva l-integrazzjoni tat-tiftix SOLR. Ikkonfigura s-settings tal-konnessjoni bl-użu tal-buttuna tas-settings tal-konnessjoni hawn fuq.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Attiva jew diżattiva l-integrazzjoni tal-workflow n8n. Ikkonfigura s-settings tal-konnessjoni hawn taħt.", + "Enable or disable this webhook": "Attiva jew diżattiva dan il-webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Attiva l-għodod li jippermettu lill-aġent jinteraġixxi mad-data permezz tas-sejħa ta' funzjonijiet.", + "Enable vectorization for all existing and future views (may increase costs)": "Attiva l-vettorizzazzjoni għall-veduti kollha eżistenti u futuri (jista' jżid l-ispejjeż)", + "Enable write-back to target objects": "Attiva l-kitba lura lejn l-oġġetti fil-mira", + "Enabled": "Attivat", + "End Date Field": "Qasam tad-Data tat-Tmiem", + "Endpoint Path*": "Mogħdija tal-Endpoint*", + "Endpoints": "Endpoints", + "Engine not found": "Il-magna ma nstabitx", + "Enter ConfigSet name": "Daħħal l-isem tal-ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Daħħal l-ismijiet tal-utenti ta' Nextcloud biex tagħti aċċess għal dan l-aġent privat", + "Enter agent description (optional)": "Daħħal id-deskrizzjoni tal-aġent (mhux obbligatorju)", + "Enter agent name": "Daħħal l-isem tal-aġent", + "Enter application description (optional)": "Daħħal id-deskrizzjoni tal-applikazzjoni (mhux obbligatorju)", + "Enter application name": "Daħħal l-isem tal-applikazzjoni", + "Enter collection name": "Daħħal l-isem tal-ġabra", + "Enter configuration description (optional)": "Daħħal id-deskrizzjoni tal-konfigurazzjoni (mhux obbligatorju)", + "Enter configuration title": "Daħħal it-titlu tal-konfigurazzjoni", + "Enter description (optional)...": "Daħħal id-deskrizzjoni (mhux obbligatorju)...", + "Enter new collection name": "Daħħal l-isem ġdid tal-ġabra", + "Enter object ID": "Daħħal l-ID tal-oġġett", + "Enter organisation description (optional)": "Daħħal id-deskrizzjoni tal-organizzazzjoni (mhux obbligatorju)", + "Enter organisation name": "Daħħal l-isem tal-organizzazzjoni", + "Enter search term": "Daħħal it-terminu tat-tiftix", + "Enter search terms or leave empty to browse all": "Daħħal it-termini tat-tiftix jew ħalli vojt biex tfittex fil-kollox", + "Enter system prompt for the agent": "Daħħal il-prompt tas-sistema għall-aġent", + "Enter username and press Enter": "Daħħal l-isem tal-utent u agħfas Enter", + "Enter view name...": "Daħħal l-isem tal-veduta...", + "Enter webhook name": "Daħħal l-isem tal-webhook", + "Enter your API key": "Daħħal iċ-ċavetta API tiegħek", + "Enter your object here...": "Daħħal l-oġġett tiegħek hawn...", + "Enter your schema here...": "Daħħal l-iSkema tiegħek hawn...", + "Entities": "Entitajiet", + "Entities detected": "Entitajiet identifikati", + "Entity ID: {id}": "ID tal-Entità: {id}", + "Entity Information": "Informazzjoni dwar l-Entità", + "Entity Recognition": "Rikonoxximent ta' Entitajiet", + "Entity Recognition Method": "Metodu ta' Rikonoxximent ta' Entitajiet", + "Entity deleted successfully": "L-entità tħassret b'suċċess", + "Entity not found": "L-entità ma nstabitx", + "Entries to be deleted:": "Entrati li jridu jitħassru:", + "Enum Issue": "Problema tal-Enum", + "Enum constraint is missing": "Il-limitazzjoni tal-enum hija nieqsa", + "Erase (Art 17)": "Ħassar (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Tħassar {count} oġġett(i) għal dan is-suġġett? Din l-azzjoni tiġi rreġistrata fit-traċċa tal-verifika.", + "Erasure complete": "It-tħassir tlesta", + "Erasure preview": "Previżjoni tat-tħassir", + "Error": "Żball", + "Error Details": "Dettalji tal-Iżball", + "Error Information": "Informazzjoni dwar l-Iżball", + "Error Message": "Messaġġ tal-Iżball", + "Error deleting audit trails: {error}": "Żball waqt it-tħassir tat-traċċi tal-verifika: {error}", + "Error deleting search trails: {error}": "Żball waqt it-tħassir tat-traċċi tat-tiftix: {error}", + "Error loading application": "Żball waqt it-tagħbija tal-applikazzjoni", + "Error loading audit trails": "Żball waqt it-tagħbija tat-traċċi tal-verifika", + "Error loading entity": "Żball waqt it-tagħbija tal-entità", + "Error loading search trails": "Żball waqt it-tagħbija tat-traċċi tat-tiftix", + "Estimated Batches:": "Lottijiet Stmati:", + "Estimated Cost:": "Spiża Stmata:", + "Estimated Duration:": "Tul ta' Żmien Stmat:", + "Event": "Avveniment", + "Event Property for Payload": "Proprjetà tal-Avveniment għall-Payload", + "Events": "Avvenimenti", + "Every schema with detected PII has a processing-activity annotation.": "Kull Skema b'PII identifikat għandha annotazzjoni ta' attività ta' pproċessar.", + "Example": "Eżempju", + "Example Value": "Valur ta' Eżempju", + "Example value for this property": "Valur ta' eżempju għal din il-Proprjetà", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Massimu Esklussiv", + "Exclusive Minimum": "Minimu Esklussiv", + "Exclusive maximum": "Massimu esklussiv", + "Exclusive minimum": "Minimu esklussiv", + "Execution Mode": "Modalità ta' Eżekuzzjoni", + "Execution Time": "Ħin ta' Eżekuzzjoni", + "Execution Time Range": "Medda tal-Ħin ta' Eżekuzzjoni", + "Existing Improvements": "Titjib Eżistenti", + "Expand details": "Espandi d-dettalji", + "Expiration": "Skadenza", + "Expires": "Jiskadi", + "Expires in (e.g., 90d)": "Jiskadi fi (eż., 90d)", + "Exponential": "Esponenzjali", + "Export": "Esporta", + "Export \"{schema}\" objects from \"{register}\"": "Esporta l-oġġetti ta' \"{schema}\" minn \"{register}\"", + "Export Configuration": "Esporta l-Konfigurazzjoni", + "Export Format:": "Format tal-Esportazzjoni:", + "Export Objects": "Esporta l-Oġġetti", + "Export completed successfully": "L-esportazzjoni tlestiet b'suċċess", + "Export downloaded successfully": "L-esportazzjoni ġiet imniżżla b'suċċess", + "Export failed: {error}": "L-esportazzjoni falliet: {error}", + "Export is rate limited. Please try again later.": "L-esportazzjoni hija limitata bir-rata. Jekk jogħġbok erġa' pprova aktar tard.", + "Export my data": "Esporta d-data tiegħi", + "Export, view, or delete audit trails": "Esporta, ara, jew ħassar it-traċċi tal-verifika", + "Exporting...": "Qed tiġi esportata...", + "Extend Schema": "Estendi l-iSkema", + "Extended": "Estiż", + "Extensions": "Estensjonijiet", + "Extra Query Parameters": "Parametri ta' Query Addizzjonali", + "Extract Now": "Estratti Issa", + "Extract Pending Files": "Estratti l-Fajls Pendenti", + "Extract now": "Estratti issa", + "Extract properties from each object": "Estratti l-proprjetajiet minn kull oġġett", + "Extracted At": "Estratt Fl-", + "Extracted at": "Estratt fl-", + "Extraction": "Estrazzjoni", + "Extraction Mode": "Modalità ta' Estrazzjoni", + "Extraction Scope": "Ambitu ta' Estrazzjoni", + "Extraction Status": "Status tal-Estrazzjoni", + "Extraction error for {file}: {error}": "Żball ta' estrazzjoni għal {file}: {error}", + "Facet Description": "Deskrizzjoni tal-Facet", + "Facet Order": "Ordni tal-Facet", + "Facet Title": "Titlu tal-Facet", + "Facet Type": "Tip ta' Facet", + "Facet configuration updated successfully": "Il-konfigurazzjoni tal-facet ġiet aġġornata b'suċċess", + "Facetable": "Facetable", + "Faceting Configuration:": "Konfigurazzjoni tal-Faceting:", + "Facets discovered and configured successfully": "Il-facets ġew skoperti u kkonfigurati b'suċċess", + "Facets discovered successfully": "Il-facets ġew skoperti b'suċċess", + "Failed": "Falliet", + "Failed to add attachment to object": "Falliet iż-żieda tal-mehmuż mal-oġġett", + "Failed to add user to organisation": "Falliet iż-żieda tal-utent mal-organizzazzjoni", + "Failed to analyze schema properties": "Falliet l-analiżi tal-proprjetajiet tal-iSkema", + "Failed to archive conversation": "Falliet l-arkivjar tal-konverżazzjoni", + "Failed to calculate sizes": "Falliet il-kalkolazzjoni tad-daqsijiet", + "Failed to cancel deactivation": "Falliet il-kanċellazzjoni tad-diżattivazzjoni", + "Failed to change active organisation: {error}": "Falliet il-bidla tal-organizzazzjoni attiva: {error}", + "Failed to change password": "Falliet il-bidla tal-password", + "Failed to check version: {error}": "Falliet il-verifika tal-verżjoni: {error}", + "Failed to clear audit trails: {error}": "Falliet it-tneħħija tat-traċċi tal-verifika: {error}", + "Failed to clear blob objects: {error}": "Falliet it-tneħħija tal-oġġetti blob: {error}", + "Failed to clear cache: {error}": "Falliet it-tneħħija tal-cache: {error}", + "Failed to clear collection": "Falliet it-tneħħija tal-ġabra", + "Failed to clear conversation": "Falliet it-tneħħija tal-konverżazzjoni", + "Failed to clear embeddings: {error}": "Falliet it-tneħħija tal-embeddings: {error}", + "Failed to clear search trails: {error}": "Falliet it-tneħħija tat-traċċi tat-tiftix: {error}", + "Failed to clear {type} cache: {error}": "Falliet it-tneħħija tal-cache {type}: {error}", + "Failed to copy changes": "Falliet l-ikkupjar tal-bidliet", + "Failed to copy data": "Falliet l-ikkupjar tad-data", + "Failed to copy data to clipboard": "Falliet l-ikkupjar tad-data fil-clipboard", + "Failed to copy to clipboard": "Falliet l-ikkupjar fil-clipboard", + "Failed to copy token": "Falliet l-ikkupjar tat-token", + "Failed to create conversation": "Falliet il-ħolqien tal-konverżazzjoni", + "Failed to create missing SOLR fields: {error}": "Falliet il-ħolqien tal-oqsma SOLR nieqsa: {error}", + "Failed to create or find project": "Falliet il-ħolqien jew is-sib tal-proġett", + "Failed to create token": "Falliet il-ħolqien tat-token", + "Failed to delete SOLR collection: {error}": "Falliet it-tħassir tal-ġabra SOLR: {error}", + "Failed to delete collection": "Falliet it-tħassir tal-ġabra", + "Failed to delete conversation": "Falliet it-tħassir tal-konverżazzjoni", + "Failed to delete objects for {schema}: {error}": "Falliet it-tħassir tal-oġġetti għal {schema}: {error}", + "Failed to delete webhook": "Falliet it-tħassir tal-webhook", + "Failed to depublish register: {error}": "Falliet it-tneħħija mill-pubblikazzjoni tar-Reġistru: {error}", + "Failed to depublish schema: {error}": "Falliet it-tneħħija mill-pubblikazzjoni tal-iSkema: {error}", + "Failed to depublish: {error}": "Falliet it-tneħħija mill-pubblikazzjoni: {error}", + "Failed to discover files: {error}": "Falliet l-iskoperta tal-fajls: {error}", + "Failed to download API specification": "Falliet l-iskariku tal-ispeċifikazzjoni API", + "Failed to export data": "Falliet l-esportazzjoni tad-data", + "Failed to export objects": "Falliet l-esportazzjoni tal-oġġetti", + "Failed to fetch conversation history": "Falliet il-ġbir tal-istorja tal-konverżazzjoni", + "Failed to fix mismatched SOLR fields: {error}": "Falliet l-irranġar tal-oqsma SOLR mhux maqbula: {error}", + "Failed to get SOLR field configuration": "Falliet il-kisba tal-konfigurazzjoni tal-oqsma SOLR", + "Failed to get chat statistics": "Falliet il-kisba tal-istatistika tal-chat", + "Failed to get database information: %s": "Falliet il-kisba tal-informazzjoni tad-database: %s", + "Failed to get entity categories": "Falliet il-kisba tal-kategoriji tal-entitajiet", + "Failed to get entity statistics": "Falliet il-kisba tal-istatistika tal-entitajiet", + "Failed to get entity types": "Falliet il-kisba tat-tipi ta' entitajiet", + "Failed to get response: {error}": "Falliet il-kisba tar-rispons: {error}", + "Failed to invalidate app store cache: {error}": "Falliet l-invalidazzjoni tal-cache tal-app store: {error}", + "Failed to leave organisation: {error}": "Falliet il-ħruġ mill-organizzazzjoni: {error}", + "Failed to link object": "Falliet ir-rabta tal-oġġett", + "Failed to load Deck cards": "Falliet it-tagħbija tal-karti ta' Deck", + "Failed to load LLM configuration": "Falliet it-tagħbija tal-konfigurazzjoni LLM", + "Failed to load Nextcloud groups": "Falliet it-tagħbija tal-gruppi ta' Nextcloud", + "Failed to load cache statistics: {error}": "Falliet it-tagħbija tal-istatistika tal-cache: {error}", + "Failed to load conversation": "Falliet it-tagħbija tal-konverżazzjoni", + "Failed to load entities": "Falliet it-tagħbija tal-entitajiet", + "Failed to load entity": "Falliet it-tagħbija tal-entità", + "Failed to load extraction data": "Falliet it-tagħbija tad-data tal-estrazzjoni", + "Failed to load files": "Falliet it-tagħbija tal-fajls", + "Failed to load linked contacts": "Falliet it-tagħbija tal-kuntatti marbuta", + "Failed to load linked emails": "Falliet it-tagħbija tal-emails marbuta", + "Failed to load linked events": "Falliet it-tagħbija tal-avvenimenti marbuta", + "Failed to load organisations": "Falliet it-tagħbija tal-organizzazzjonijiet", + "Failed to load register data": "Falliet it-tagħbija tad-data tar-Reġistru", + "Failed to load related entities": "Falliet it-tagħbija tal-entitajiet relatati", + "Failed to load settings: {error}": "Falliet it-tagħbija tas-settings: {error}", + "Failed to load statistics: {error}": "Falliet it-tagħbija tal-istatistika: {error}", + "Failed to load templates": "Falliet it-tagħbija tal-mudelli", + "Failed to load view: {error}": "Falliet it-tagħbija tal-veduta: {error}", + "Failed to load webhooks": "Falliet it-tagħbija tal-webhooks", + "Failed to load workflows": "Falliet it-tagħbija tal-workflows", + "Failed to process message": "Falliet l-ipproċessar tal-messaġġ", + "Failed to publish register: {error}": "Falliet il-pubblikazzjoni tar-Reġistru: {error}", + "Failed to publish schema: {error}": "Falliet il-pubblikazzjoni tal-iSkema: {error}", + "Failed to publish: {error}": "Falliet il-pubblikazzjoni: {error}", + "Failed to refresh database information": "Falliet l-aġġornament tal-informazzjoni tad-database", + "Failed to reindex collection": "Falliet ir-reindiċjar tal-ġabra", + "Failed to remove avatar": "Falliet it-tneħħija tal-avatar", + "Failed to remove link": "Falliet it-tneħħija tar-rabta", + "Failed to remove member: {error}": "Falliet it-tneħħija tal-membru: {error}", + "Failed to remove schema {schema}: {error}": "Falliet it-tneħħija tal-iSkema {schema}: {error}", + "Failed to remove user from organisation": "Falliet it-tneħħija tal-utent mill-organizzazzjoni", + "Failed to rename conversation": "Falliet il-bidla tal-isem tal-konverżazzjoni", + "Failed to request deactivation": "Falliet it-talba għad-diżattivazzjoni", + "Failed to restore conversation": "Falliet ir-restawr tal-konverżazzjoni", + "Failed to retrieve extraction status.": "Falliet il-kisba tal-istatus tal-estrazzjoni.", + "Failed to retrieve objects for file.": "Falliet il-kisba tal-oġġetti għall-fajl.", + "Failed to retry extraction": "Falliet il-prova mill-ġdid tal-estrazzjoni", + "Failed to retry webhook": "Falliet il-prova mill-ġdid tal-webhook", + "Failed to revoke token": "Falliet ir-revoka tat-token", + "Failed to save GitHub token": "Falliet il-ħażna tat-token ta' GitHub", + "Failed to save GitLab URL": "Falliet il-ħażna tal-URL ta' GitLab", + "Failed to save GitLab token": "Falliet il-ħażna tat-token ta' GitLab", + "Failed to save LLM settings: {error}": "Falliet il-ħażna tas-settings LLM: {error}", + "Failed to save additional feedback": "Falliet il-ħażna tal-feedback addizzjonali", + "Failed to save configuration.": "Falliet il-ħażna tal-konfigurazzjoni.", + "Failed to save configuration: {error}": "Falliet il-ħażna tal-konfigurazzjoni: {error}", + "Failed to save facet configuration": "Falliet il-ħażna tal-konfigurazzjoni tal-facet", + "Failed to save feedback": "Falliet il-ħażna tal-feedback", + "Failed to save file settings: {error}": "Falliet il-ħażna tas-settings tal-fajl: {error}", + "Failed to save n8n configuration": "Falliet il-ħażna tal-konfigurazzjoni n8n", + "Failed to save preferences": "Falliet il-ħażna tal-preferenzi", + "Failed to save roles": "Falliet il-ħażna tar-rwoli", + "Failed to save settings": "Falliet il-ħażna tas-settings", + "Failed to save settings: {error}": "Falliet il-ħażna tas-settings: {error}", + "Failed to save view: {error}": "Falliet il-ħażna tal-veduta: {error}", + "Failed to save warmup interval: {error}": "Falliet il-ħażna tal-intervall tal-warmup: {error}", + "Failed to save webhook": "Falliet il-ħażna tal-webhook", + "Failed to send feedback": "Falliet il-bagħt tal-feedback", + "Failed to set active organisation: {error}": "Falliet l-issettjar tal-organizzazzjoni attiva: {error}", + "Failed to setup SOLR: {error}": "Falliet l-issettjar ta' SOLR: {error}", + "Failed to start file warmup: {error}": "Falliet il-bidu tal-warmup tal-fajl: {error}", + "Failed to start processing {type} files: {error}": "Falliet il-bidu tal-ipproċessar tal-fajls {type}: {error}", + "Failed to start vectorization: {error}": "Falliet il-bidu tal-vettorizzazzjoni: {error}", + "Failed to switch organisation: {error}": "Falliet il-qlib tal-organizzazzjoni: {error}", + "Failed to sync magic table for {schema}: {error}": "Falliet is-sinkronizzazzjoni tal-magic table għal {schema}: {error}", + "Failed to test SOLR connection: {error}": "Falliet il-prova tal-konnessjoni SOLR: {error}", + "Failed to test webhook": "Falliet il-prova tal-webhook", + "Failed to update LLM settings: {error}": "Falliet l-aġġornament tas-settings LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Falliet l-aġġornament tas-settings tal-Multitenancy: {error}", + "Failed to update RBAC settings: {error}": "Falliet l-aġġornament tas-settings RBAC: {error}", + "Failed to update Retention settings: {error}": "Falliet l-aġġornament tas-settings tar-Retention: {error}", + "Failed to update SOLR settings: {error}": "Falliet l-aġġornament tas-settings SOLR: {error}", + "Failed to update favorite status": "Falliet l-aġġornament tal-istatus favorit", + "Failed to update schema configuration for {schema}: {error}": "Falliet l-aġġornament tal-konfigurazzjoni tal-iSkema għal {schema}: {error}", + "Failed to update schema properties": "Falliet l-aġġornament tal-proprjetajiet tal-iSkema", + "Failed to update view": "Falliet l-aġġornament tal-veduta", + "Failed to update view: {error}": "Falliet l-aġġornament tal-veduta: {error}", + "Failed to update webhook": "Falliet l-aġġornament tal-webhook", + "Failed to upload avatar": "Falliet it-tella' tal-avatar", + "Failed to validate {schema}: {error}": "Falliet il-validazzjoni ta' {schema}: {error}", + "Failed to vectorize files: {error}": "Falliet il-vettorizzazzjoni tal-fajls: {error}", + "Failed to warmup names cache: {error}": "Falliet il-warmup tal-cache tal-ismijiet: {error}", + "Federation": "Federazzjoni", + "Feedback recorded": "Il-feedback ġie rreġistrat", + "Fewer sources (1-3):": "Inqas sorsi (1-3):", + "Field": "Qasam", + "Fields to Display": "Oqsma li jridu Jintwerew", + "File": "Fajl", + "File #{id}": "Fajl #{id}", + "File Chunk Prediction": "Previżjoni tal-Chunk tal-Fajl", + "File Collection": "Ġabra ta' Fajls", + "File Handling": "Ġestjoni tal-Fajls", + "File Location": "Post tal-Fajl", + "File Management": "Ġestjoni tal-Fajls", + "File Name": "Isem tal-Fajl", + "File Path": "Mogħdija tal-Fajl", + "File Type Selection": "Għażla tat-Tip ta' Fajl", + "File Types to Process": "Tipi ta' Fajls li jridu Jiġu Pproċessati", + "File Vectorization": "Vettorizzazzjoni tal-Fajls", + "File Warmup": "Warmup tal-Fajls", + "File actions menu": "Menu tal-azzjonijiet tal-fajl", + "File anonymized successfully": "Il-fajl ġie anonimizzat b'suċċess", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Il-vettorizzazzjoni tal-chunks tal-fajl għadha mhix implimentata. Il-chunks huma lesti u maħżuna, iżda s-servizz tal-vettorizzazzjoni qiegħed taħt żvilupp.", + "File collection not configured": "Il-Ġabra ta' Fajls mhix ikkonfigurata", + "File discovery completed": "L-iskoperta tal-fajls tlestiet", + "File extraction completed": "L-estrazzjoni tal-fajl tlestiet", + "File extraction queued": "L-estrazzjoni tal-fajl tqiegħdet fil-kju", + "File indexed successfully": "Il-fajl ġie indiċjat b'suċċess", + "File is already anonymized": "Il-fajl huwa diġà anonimizzat", + "File name": "Isem tal-fajl", + "File not found": "Il-fajl ma nstabx", + "File settings saved successfully": "Is-settings tal-fajl inħażnu b'suċċess", + "File settings updated successfully": "Is-settings tal-fajl ġew aġġornati b'suċċess", + "File sources": "Sorsi ta' fajls", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Il-vettorizzazzjoni tal-fajls tlestiet! {vectorized} chunks ġew vettorizzati minn {files} fajls. {failed} fallew.", + "File vectorization configuration saved successfully": "Il-konfigurazzjoni tal-vettorizzazzjoni tal-fajls ġiet salvata b'suċċess", + "File vectorization started. Check the statistics section for progress.": "Il-vettorizzazzjoni tal-fajls bdiet. Iċċekkja t-taqsima tal-istatistika għall-progress.", + "File warmup completed": "It-tisħin tal-fajl tlesta", + "Files": "Fajls", + "Files added successfully": "Il-fajls ġew miżjuda b'suċċess", + "Files with Completed Extraction:": "Fajls bl-estrazzjoni mlestija:", + "Files → fileCollection, Objects → objectCollection": "Fajls → fileCollection, Oġġetti → objectCollection", + "Files → filecollection, objects → objectcollection": "Fajls → filecollection, oġġetti → objectcollection", + "Filter Audit Trails": "Iffiltra t-Traċċi tal-Verifika", + "Filter Deleted Items": "Iffiltra l-Oġġetti Mħassra", + "Filter Objects": "Iffiltra l-Oġġetti", + "Filter Properties": "Iffiltra l-Proprjetajiet", + "Filter Search Trails": "Iffiltra t-Traċċi tat-Tiftix", + "Filter Statistics": "Iffiltra l-Istatistika", + "Filter and analyze search trail entries": "Iffiltra u analizza l-entrati tat-traċċi tat-tiftix", + "Filter and manage audit trail entries": "Iffiltra u ġestixxi l-entrati tat-traċċi tal-verifika", + "Filter and manage soft deleted items": "Iffiltra u ġestixxi l-oġġetti mħassra bil-mod artab", + "Filter and search entities": "Iffiltra u fittex l-entitajiet", + "Filter by object ID": "Iffiltra skont l-ID tal-oġġett", + "Filter by search term": "Iffiltra skont it-terminu tat-tiftix", + "Filter by type": "Iffiltra skont it-tip", + "Filter by webhook": "Iffiltra skont il-webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Id-data tal-filtru ġiet mgħobbija awtomatikament. Uża l-filtri ta' hawn taħt biex tirfina t-tiftix tiegħek.", + "Filter fields...": "Iffiltra l-oqsma...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Iffiltra l-grilji tal-webhook skont il-proprjetajiet tal-payload (waħda f'kull linja, format: key: value)", + "Filtered": "Iffiltrat", + "Filters": "Filtri", + "Finalizing setup...": "Qed jiġi ffinalizzat l-issettjar...", + "Fireworks AI Chat Settings": "Settings tal-Chat ta' Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfigurazzjoni tal-Embedding ta' Fireworks AI", + "First": "L-ewwel", + "Fixed": "Fiss", + "For chat and retrieval-augmented generation": "Għaċ-chat u l-ġenerazzjoni miżjuda bl-irkupru", + "For vector embeddings and semantic search": "Għall-embeddings tal-vetturi u t-tiftix semantiku", + "Format": "Format", + "Format Issue": "Problema tal-Format", + "Format constraint is missing": "Ir-restrizzjoni tal-format hija nieqsa", + "From (e.g. -7 days or 2025-01-01)": "Minn (eż. -7 days jew 2025-01-01)", + "From Date": "Mid-Data", + "From date": "Mid-data", + "Full data copied to clipboard": "Id-data sħiħa ġiet ikkupjata fil-clipboard", + "Function Tools": "Għodod tal-Funzjonijiet", + "General Issue": "Problema Ġenerali", + "Generate recommendations and confidence scores": "Iġġenera rakkomandazzjonijiet u punteġġi ta' kunfidenza", + "Generate report": "Iġġenera rapport", + "Generate the verantwoordingsdocument": "Iġġenera l-verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Iġġenera l-vetturi minnufih meta jinħolqu oġġetti ġodda", + "Generated: {time}": "Iġġenerat: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Ikseb għarfien f'ħin reali dwar is-saħħa tad-data tal-organizzazzjoni tiegħek billi tiffoka fuq ir-reġistri, id-definizzjonijiet tal-iskemi, u l-ħżin u l-użu tal-oġġetti.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Ikseb għarfien f'ħin reali dwar is-saħħa tad-data tal-organizzazzjoni tiegħek, ix-xejriet tat-tkabbir, u l-metriċi tal-kwalità.", + "GitHub Personal Access Token": "Token ta' Aċċess Personali ta' GitHub", + "GitHub token is valid": "It-token ta' GitHub huwa validu", + "GitHub token is valid! Username: {username}": "It-token ta' GitHub huwa validu! Username: {username}", + "GitHub token removed successfully": "It-token ta' GitHub tneħħa b'suċċess", + "GitHub token saved successfully": "It-token ta' GitHub ġie salvat b'suċċess", + "GitHub token test failed: {message}": "It-test tat-token ta' GitHub falla: {message}", + "GitLab Personal Access Token": "Token ta' Aċċess Personali ta' GitLab", + "GitLab URL saved successfully": "L-URL ta' GitLab ġie salvat b'suċċess", + "GitLab token is valid": "It-token ta' GitLab huwa validu", + "GitLab token is valid! Username: {username}": "It-token ta' GitLab huwa validu! Username: {username}", + "GitLab token saved successfully": "It-token ta' GitLab ġie salvat b'suċċess", + "GitLab token test failed: {message}": "It-test tat-token ta' GitLab falla: {message}", + "Go to linked Register": "Mur għar-Reġistru llinkjat", + "Go to linked Schema": "Mur għall-Iskema llinkjata", + "Go to organisation": "Mur għall-organizzazzjoni", + "Grant additional permissions beyond standard CRUD operations": "Agħti permessi addizzjonali lil hinn mill-operazzjonijiet CRUD standard", + "Group Quota": "Kwota tal-Grupp", + "Groups": "Gruppi", + "HTML preview": "Previżjoni HTML", + "HTTP Method": "Metodu HTTP", + "HTTP method used to send webhook requests": "Il-metodu HTTP użat biex jintbagħtu t-talbiet tal-webhook", + "Headers": "Headers", + "Health": "Saħħa", + "Heartbeat successful - connection kept alive": "Heartbeat b'suċċess - il-konnessjoni nżammet ħajja", + "Helpful": "Ta' għajnuna", + "Here you can configure the details for various connections.": "Hawnhekk tista' tikkonfigura d-dettalji għal diversi konnessjonijiet.", + "Here you can configure the details for your organisation.": "Hawnhekk tista' tikkonfigura d-dettalji għall-organizzazzjoni tiegħek.", + "Hide": "Aħbi", + "Hide Filters": "Aħbi l-Filtri", + "Hide in collection view": "Aħbi fil-veduta tal-kollezzjoni", + "Hide in forms": "Aħbi fil-formoli", + "Hide in list view": "Aħbi fil-veduta tal-lista", + "High": "Għoli", + "High Confidence": "Kunfidenza Għolja", + "Host": "Host", + "Hourly": "Kull siegħa", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Kemm fil-fond tinżel fil-proprjetajiet tal-oġġetti mdaħħla (1-20). Valuri ogħla jaqbdu aktar dettall iżda jżidu d-daqs tal-vettur.", + "How often to check for updates (1-168 hours)": "Kemm-il darba tiċċekkja għall-aġġornamenti (1-168 siegħa)", + "How to handle retries for failed webhook deliveries": "Kif tittratta l-provi mill-ġdid għall-konsenji tal-webhook li fallew", + "IBANs": "IBANs", + "ID": "ID", + "ID of input mapping (optional)": "ID tal-immappjar tal-input (mhux obbligatorju)", + "ID of output mapping (optional)": "ID tal-immappjar tal-output (mhux obbligatorju)", + "ID of the target resource": "ID tar-riżorsa fil-mira", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Il-limiti tar-rata tal-IP ġew imnaddfa b'suċċess", + "Identify properties not in the schema": "Identifika l-proprjetajiet li mhumiex fl-iskema", + "Immutable": "Immutabbli", + "Import": "Importa", + "Import Configuration": "Importa l-Konfigurazzjoni", + "Import Data into Register": "Importa Data fir-Reġistru", + "Import successful": "L-importazzjoni rnexxiet", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importa l-mudell report-bundle.json biex tikseb ir-reġistru `reports`, imbagħad oħloq l-ewwel dashboard tiegħek permezz tal-UI standard tal-oġġetti. Id-dashboards jiddikjaraw il-widgets tagħhom f'JSON u r-renderer jipprovdi lil kull widget data ta' aggregazzjoni f'ħin reali.", + "Improved Property": "Proprjetà Mtejba", + "In use": "Fl-użu", + "Inactive": "Inattiv", + "Inactive organisations cannot be used": "L-organizzazzjonijiet inattivi ma jistgħux jintużaw", + "Include IDs and names of related objects for better contextual search": "Inkludi l-IDs u l-ismijiet tal-oġġetti relatati għal tiftix kontestwali aħjar", + "Include ids and names of related objects for better contextual search": "Inkludi l-ids u l-ismijiet tal-oġġetti relatati għal tiftix kontestwali aħjar", + "Include related object references": "Inkludi r-referenzi tal-oġġetti relatati", + "Include schema and register metadata": "Inkludi l-metadata tal-iskema u tar-reġistru", + "Index": "Indiċi", + "Information about the current OpenRegister installation": "Informazzjoni dwar l-installazzjoni attwali ta' OpenRegister", + "Inherited from register": "Miret mir-reġistru", + "Inherits permissions from register": "Jiret il-permessi mir-reġistru", + "Initialization failed": "L-inizjalizzazzjoni falliet", + "Initialize Project": "Inizjalizza l-Proġett", + "Initializing SOLR setup...": "Qed jiġi inizjalizzat l-issettjar ta' SOLR...", + "Input Mapping": "Immappjar tal-Input", + "Inspect Fields": "Ispezzjona l-Oqsma", + "Inspect Index": "Ispezzjona l-Indiċi", + "Inspect SOLR Index": "Ispezzjona l-Indiċi SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installa l-app notify_push mill-App Store ta' Nextcloud biex tippermetti aġġornamenti f'ħin reali.", + "Integrations": "Integrazzjonijiet", + "Internet location (URL) of this installation": "Il-post fuq l-internet (URL) ta' din l-installazzjoni", + "Interval": "Intervall", + "Invalid": "Invalidu", + "Invalid batch size. Must be between 1 and 5000": "Daqs tal-batch invalidu. Irid ikun bejn 1 u 5000", + "Invalid feedback type": "Tip ta' feedback invalidu", + "Invalid field name provided": "L-isem tal-qasam ipprovdut huwa invalidu", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects invalidu. Irid ikun 0 (kollha) jew numru pożittiv", + "Invalid:": "Invalidu:", + "Invite Users": "Stieden Utenti", + "Invited Users": "Utenti Mistiedna", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Riżultati tal-Inzage", + "Issue": "Problema", + "Items per page:": "Oġġetti f'kull paġna:", + "Just now": "Issa stess", + "Key": "Ċavetta", + "LLM Configuration": "Konfigurazzjoni tal-LLM", + "LLM actions menu": "Menu tal-azzjonijiet tal-LLM", + "LLM configuration saved successfully": "Il-konfigurazzjoni tal-LLM ġiet salvata b'suċċess", + "LLM features disabled": "Il-karatteristiċi tal-LLM ġew diżattivati", + "LLM features enabled": "Il-karatteristiċi tal-LLM ġew attivati", + "LLM must be enabled with an embedding provider configured": "L-LLM irid ikun attivat b'fornitur tal-embedding konfigurat", + "LLM settings saved successfully": "Is-settings tal-LLM ġew salvati b'suċċess", + "LLM settings updated successfully": "Is-settings tal-LLM ġew aġġornati b'suċċess", + "Label": "Tikketta", + "Labels": "Tikketti", + "Languages": "Lingwi", + "Last": "L-aħħar", + "Last Triggered": "L-Aħħar Skattat", + "Leave empty to allow all users access": "Ħalli vojt biex tippermetti aċċess lill-utenti kollha", + "Leave off for auto-detection from property format.": "Ħallih spjuġat għall-iskoperta awtomatika mill-format tal-proprjetà.", + "Left organisation successfully": "Telaq mill-organizzazzjoni b'suċċess", + "Legal basis *": "Bażi legali *", + "Length Range:": "Firxa tat-Tul:", + "Linear": "Lineari", + "Link": "Link", + "Link an email from the Mail app sidebar to associate it with this object.": "Illinkja email mill-bar laterali tal-app Mail biex tassoċjaha ma' dan l-oġġett.", + "Link an object to see it here.": "Illinkja oġġett biex tarah hawn.", + "Link another object": "Illinkja oġġett ieħor", + "Link existing event": "Illinkja avveniment eżistenti", + "Link removed": "Il-link tneħħa", + "Link to Object": "Illinkja mal-Oġġett", + "Link to {name}": "Illinkja ma' {name}", + "Linked Objects": "Oġġetti Llinkjati", + "Linked by {user}": "Illinkjat minn {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "L-emails, l-avvenimenti tal-kalendarju, il-kuntatti u l-karti tad-Deck illinkjati jidhru hawn.", + "Linked to {name}": "Illinkjat ma' {name}", + "Load Advanced Filters": "Għabbi l-Filtri Avvanzati", + "Load advanced filters with live data from your search index": "Għabbi l-filtri avvanzati b'data ħajja mill-indiċi tat-tiftix tiegħek", + "Load more": "Għabbi aktar", + "Load view": "Għabbi l-veduta", + "Loading API tokens...": "Qed jiġu mgħobbija t-tokens tal-API...", + "Loading ConfigSets...": "Qed jiġu mgħobbija l-ConfigSets...", + "Loading LLM configuration...": "Qed tiġi mgħobbija l-konfigurazzjoni tal-LLM...", + "Loading RBAC settings...": "Qed jiġu mgħobbija s-settings tar-RBAC...", + "Loading activity...": "Qed tiġi mgħobbija l-attività...", + "Loading advanced filters...": "Qed jiġu mgħobbija l-filtri avvanzati...", + "Loading agents...": "Qed jiġu mgħobbija l-aġenti...", + "Loading application details...": "Qed jiġu mgħobbija d-dettalji tal-applikazzjoni...", + "Loading applications...": "Qed jiġu mgħobbija l-applikazzjonijiet...", + "Loading archived conversations...": "Qed jiġu mgħobbija l-konverżazzjonijiet arkivjati...", + "Loading audit trails...": "Qed jiġu mgħobbija t-traċċi tal-verifika...", + "Loading available tools...": "Qed jiġu mgħobbija l-għodod disponibbli...", + "Loading cache statistics...": "Qed tiġi mgħobbija l-istatistika tal-cache...", + "Loading collections...": "Qed jiġu mgħobbija l-kollezzjonijiet...", + "Loading configurations...": "Qed jiġu mgħobbija l-konfigurazzjonijiet...", + "Loading conversation...": "Qed tiġi mgħobbija l-konverżazzjoni...", + "Loading conversations...": "Qed jiġu mgħobbija l-konverżazzjonijiet...", + "Loading deleted items...": "Qed jiġu mgħobbija l-oġġetti mħassra...", + "Loading endpoints": "Qed jiġu mgħobbija l-endpoints", + "Loading entities...": "Qed jiġu mgħobbija l-entitajiet...", + "Loading events...": "Qed jiġu mgħobbija l-avvenimenti...", + "Loading filter data...": "Qed tiġi mgħobbija d-data tal-filtru...", + "Loading groups...": "Qed jiġu mgħobbija l-gruppi...", + "Loading linked objects...": "Qed jiġu mgħobbija l-oġġetti llinkjati...", + "Loading log details...": "Qed jiġu mgħobbija d-dettalji tal-log...", + "Loading multitenancy settings...": "Qed jiġu mgħobbija s-settings tal-multitenancy...", + "Loading n8n configuration...": "Qed tiġi mgħobbija l-konfigurazzjoni ta' n8n...", + "Loading permission matrix...": "Qed tiġi mgħobbija l-matriċi tal-permessi...", + "Loading preferences...": "Qed jiġu mgħobbija l-preferenzi...", + "Loading register data...": "Qed tiġi mgħobbija d-data tar-reġistru...", + "Loading registers...": "Qed jiġu mgħobbija r-reġistri...", + "Loading retention settings...": "Qed jiġu mgħobbija s-settings taż-żamma...", + "Loading schemas...": "Qed jiġu mgħobbija l-iskemi...", + "Loading search configuration...": "Qed tiġi mgħobbija l-konfigurazzjoni tat-tiftix...", + "Loading search trails...": "Qed jiġu mgħobbija t-traċċi tat-tiftix...", + "Loading sources...": "Qed jiġu mgħobbija s-sorsi...", + "Loading statistics...": "Qed tiġi mgħobbija l-istatistika...", + "Loading text extraction settings...": "Qed jiġu mgħobbija s-settings tal-estrazzjoni tat-test...", + "Loading tokens...": "Qed jiġu mgħobbija t-tokens...", + "Loading user groups...": "Qed jiġu mgħobbija l-gruppi tal-utenti...", + "Loading users...": "Qed jiġu mgħobbija l-utenti...", + "Loading views...": "Qed jiġu mgħobbija l-veduti...", + "Loading...": "Qed jitgħabba...", + "Local": "Lokali", + "Local Version": "Verżjoni Lokali", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Sib kull oġġett li jirreferi għal suġġett tad-data (Art 15 inzage), ara minn qabel tħassir (Art 17 vergetelheid), jew esporta d-data tagħhom (Art 20 portabiliteit).", + "Location": "Post", + "Location Field": "Qasam tal-Post", + "Locations": "Postijiet", + "Locked": "Imsakkar", + "Locked:": "Imsakkar:", + "Login successful": "Il-login irnexxa", + "Logs": "Logs", + "Low": "Baxx", + "Low Confidence": "Kunfidenza Baxxa", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Numri aktar baxxi jidhru l-ewwel fil-bar laterali tal-filtri. Ħalli vojt għall-ordni awtomatika.", + "Lucene Version:": "Verżjoni ta' Lucene:", + "Magic table sync completed for {schema}": "Is-sinkronizzazzjoni tat-tabella magic tlestiet għal {schema}", + "Magic table synchronized successfully": "It-tabella magic ġiet sinkronizzata b'suċċess", + "Mail Integration": "Integrazzjoni tal-Mail", + "Mail integration is not available": "L-integrazzjoni tal-Mail mhix disponibbli", + "Make this installation discoverable within the federation network.": "Agħmel din l-installazzjoni skopribbli fi ħdan in-netwerk tal-federazzjoni.", + "Manage Organisation Roles": "Ġestixxi r-Rwoli tal-Organizzazzjoni", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Ġestixxi l-Kollezzjonijiet SOLR (ħażniet tad-data) u assenjahom għall-oġġetti u l-fajls.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Ġestixxi l-ConfigSets SOLR (mudelli ta' konfigurazzjoni) għall-kollezzjonijiet tiegħek.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Ġestixxi l-kollezzjonijiet SOLR (ħażniet tad-data) u assenjahom għall-oġġetti u l-fajls.", + "Manage SOLR configsets (configuration templates) for your collections.": "Ġestixxi l-configsets SOLR (mudelli ta' konfigurazzjoni) għall-kollezzjonijiet tiegħek.", + "Manage and configure agents for automated tasks": "Ġestixxi u kkonfigura l-aġenti għal kompiti awtomatizzati", + "Manage and monitor file text extraction status": "Ġestixxi u immonitorja l-istatus tal-estrazzjoni tat-test mill-fajls", + "Manage and restore soft deleted items from your registers": "Ġestixxi u rkupra oġġetti mħassra bil-mod artab mir-reġistri tiegħek", + "Manage and view detected entities from files and objects": "Ġestixxi u ara l-entitajiet identifikati mill-fajls u l-oġġetti", + "Manage document templates and themes": "Ġestixxi l-mudelli u t-temi tad-dokumenti", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Ġestixxi l-workflows ta' n8n għall-awtomazzjoni ta' OpenRegister. Il-workflows jinħażnu fil-proġett konfigurat.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Ġestixxi l-attivitajiet tal-ipproċessar, mexxi t-talbiet ta' aċċess tas-suġġetti tad-data, u verifika l-konformità taħt il-GDPR tal-UE / l-AVG Olandiż.", + "Manage webhooks for event-driven integrations": "Ġestixxi l-webhooks għal integrazzjonijiet immexxija mill-avvenimenti", + "Manage your account settings, security, and personal data.": "Ġestixxi s-settings tal-kont, is-sigurtà, u d-data personali tiegħek.", + "Manage your applications and modules": "Ġestixxi l-applikazzjonijiet u l-moduli tiegħek", + "Manage your chat conversations": "Ġestixxi l-konverżazzjonijiet taċ-chat tiegħek", + "Manage your data registers and their configurations": "Ġestixxi r-reġistri tad-data tiegħek u l-konfigurazzjonijiet tagħhom", + "Manage your data schemas and their properties": "Ġestixxi l-iskemi tad-data tiegħek u l-proprjetajiet tagħhom", + "Manage your data sources and their configurations": "Ġestixxi s-sorsi tad-data tiegħek u l-konfigurazzjonijiet tagħhom", + "Manage your organisations and switch between them": "Ġestixxi l-organizzazzjonijiet tiegħek u aqleb bejniethom", + "Manage your saved search configurations": "Ġestixxi l-konfigurazzjonijiet tat-tiftix salvati tiegħek", + "Manage your system configurations and settings": "Ġestixxi l-konfigurazzjonijiet u s-settings tas-sistema tiegħek", + "Managed": "Ġestit", + "Mappings": "Immappjar", + "Mass validation completed successfully": "Il-validazzjoni tal-massa tlestiet b'suċċess", + "Mass validation failed: {error}": "Il-validazzjoni tal-massa falliet: {error}", + "Max Files (0 = all)": "Massimu ta' Fajls (0 = kollha)", + "Max Length": "Tul Massimu", + "Max Length:": "Tul Massimu:", + "Max Objects (0 = all)": "Massimu ta' Oġġetti (0 = kollha)", + "Max Retries": "Provi mill-Ġdid Massimi", + "Max Shards/Node": "Massimu ta' Shards/Node", + "Max Tokens": "Tokens Massimi", + "Max execution time (ms)": "Ħin massimu ta' eżekuzzjoni (ms)", + "Max ms": "Massimu ms", + "Max result count": "Għadd massimu ta' riżultati", + "Max results": "Riżultati massimi", + "Maximum": "Massimu", + "Maximum File Size (MB)": "Daqs Massimu tal-Fajl (MB)", + "Maximum Nesting Depth": "Fond Massimu tat-Tnaqqix", + "Maximum length": "Tul massimu", + "Maximum length constraint is missing": "Ir-restrizzjoni tat-tul massimu hija nieqsa", + "Maximum length is too restrictive": "It-tul massimu huwa restrittiv wisq", + "Maximum number of files to process. Set to 0 to process all files.": "Numru massimu ta' fajls li jiġu pproċessati. Issettja għal 0 biex tipproċessa l-fajls kollha.", + "Maximum number of items": "Numru massimu ta' oġġetti", + "Maximum number of objects to process. Set to 0 to process all objects.": "Numru massimu ta' oġġetti li jiġu pproċessati. Issettja għal 0 biex tipproċessa l-oġġetti kollha.", + "Maximum number of retry attempts for failed deliveries": "Numru massimu ta' provi mill-ġdid għall-konsenji li fallew", + "Maximum time to wait before committing changes": "Ħin massimu ta' stennija qabel ma jiġu kkommettjati l-bidliet", + "Maximum tokens to generate": "Tokens massimi li jiġu ġġenerati", + "Maximum value": "Valur massimu", + "Maximum value constraint is missing": "Ir-restrizzjoni tal-valur massimu hija nieqsa", + "Maximum value is too restrictive": "Il-valur massimu huwa restrittiv wisq", + "Medium": "Medju", + "Medium Confidence": "Kunfidenza Medja", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Mistoqsijiet medji: Tiftixiet b'xi filtraġġ jew parametri multipli (eż., firxiet ta' dati, reġistri/skemi speċifiċi)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Mistoqsijiet medji: tiftixiet b'xi filtraġġ jew parametri multipli (eż., firxiet ta' dati, reġistri/skemi speċifiċi)", + "Member removed successfully": "Il-membru tneħħa b'suċċess", + "Members": "Membri", + "Members ({count})": "Membri ({count})", + "Members of selected groups can access this view": "Il-membri tal-gruppi magħżula jistgħu jaċċessaw din il-veduta", + "Members:": "Membri:", + "Memory prediction calculated successfully": "It-tbassir tal-memorja ġie kkalkulat b'suċċess", + "Message content is required": "Il-kontenut tal-messaġġ huwa meħtieġ", + "Message does not belong to this conversation": "Il-messaġġ ma jappartjenix għal din il-konverżazzjoni", + "Message not found": "Il-messaġġ ma nstabx", + "Metadata": "Metadata", + "Metadata Filters": "Filtri tal-Metadata", + "Method": "Metodu", + "Method*": "Metodu*", + "Method: {method}": "Metodu: {method}", + "Min Length": "Tul Minimu", + "Min execution time (ms)": "Ħin minimu ta' eżekuzzjoni (ms)", + "Min ms": "Minimu ms", + "Min result count": "Għadd minimu ta' riżultati", + "Min results": "Riżultati minimi", + "Minimum": "Minimu", + "Minimum length": "Tul minimu", + "Minimum number of items": "Numru minimu ta' oġġetti", + "Minimum value": "Valur minimu", + "Minimum value constraint is missing": "Ir-restrizzjoni tal-valur minimu hija nieqsa", + "Minimum value is too restrictive": "Il-valur minimu huwa restrittiv wisq", + "Missing conversation or agentUuid": "Konverżazzjoni jew agentUuid nieqes", + "Missing conversationId": "conversationId nieqes", + "Missing message": "Messaġġ nieqes", + "Missing object fields created successfully": "L-oqsma tal-oġġett neqsin inħolqu b'suċċess", + "Mode:": "Modalità:", + "Model": "Mudell", + "Model Name": "Isem tal-Mudell", + "Modified": "Modifikat", + "Monthly": "Kull xahar", + "More sources (10-20):": "Aktar sorsi (10-20):", + "Most Active Objects": "L-Aktar Oġġetti Attivi", + "Multiple Of": "Multiplu Ta'", + "Multiple of": "Multiplu ta'", + "Multiple search trail deletion not implemented yet": "It-tħassir ta' diversi traċċi tat-tiftix għadu mhux implimentat", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Is-settings tal-multitenancy ġew aġġornati b'suċċess", + "My Account": "Il-Kont Tiegħi", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Isem", + "Name *": "Isem *", + "Name*": "Isem*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Il-cache tal-ismijiet issaħħan b'suċċess: {count} ismijiet ġew mgħobbija f'{time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Il-cache tal-ismijiet issaħħan b'suċċess: {count} ismijiet ġew mgħobbija f'{time}. {message}", + "Names cache warmup completed": "It-tisħin tal-cache tal-ismijiet tlesta", + "Naming pattern for copies:": "Mudell tal-ismijiet għall-kopji:", + "New Collection Name": "Isem ta' Kollezzjoni Ġdida", + "New Configuration": "Konfigurazzjoni Ġdida", + "New Conversation": "Konverżazzjoni Ġdida", + "New Properties": "Proprjetajiet Ġodda", + "New Property": "Proprjetà Ġdida", + "New User Group": "Grupp ta' Utenti Ġdid", + "New Value": "Valur Ġdid", + "New activity": "Attività ġdida", + "New password": "Password ġdida", + "New processing activity": "Attività tal-ipproċessar ġdida", + "New users without specific organisation membership will be automatically added to this organisation": "L-utenti l-ġodda mingħajr sħubija f'organizzazzjoni speċifika jiġu miżjuda awtomatikament ma' din l-organizzazzjoni", + "New verwerkingsactiviteit": "verwerkingsactiviteit ġdida", + "Next": "Li jmiss", + "Nextcloud Groups": "Gruppi ta' Nextcloud", + "No": "Le", + "No API tokens.": "L-ebda token tal-API.", + "No ConfigSets found": "Ma nstabu l-ebda ConfigSets", + "No Deck cards linked to this object": "L-ebda karta tad-Deck ma hija llinkjata ma' dan l-oġġett", + "No GitHub token configured": "L-ebda token ta' GitHub mhux konfigurat", + "No GitHub token provided": "Ma ġie pprovdut l-ebda token ta' GitHub", + "No GitLab token provided": "Ma ġie pprovdut l-ebda token ta' GitLab", + "No Organisation": "L-ebda Organizzazzjoni", + "No active filters": "L-ebda filtru attiv", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "L-ebda oġġett attiv biex jitħassar bil-mod artab għall-iskema {schema}. Uża \"Ħassar Permanentement\" biex tneħħi l-oġġetti mħassra bil-mod artab.", + "No activity data available": "L-ebda data tal-attività mhix disponibbli", + "No activity found.": "Ma nstabet l-ebda attività.", + "No agents available": "L-ebda aġent mhux disponibbli", + "No agents found": "Ma nstabu l-ebda aġenti", + "No app store cache exists yet": "Għadu ma jeżisti l-ebda cache tal-app store", + "No applications are available.": "L-ebda applikazzjoni mhix disponibbli.", + "No applications found": "Ma nstabet l-ebda applikazzjoni", + "No approval steps for this object.": "L-ebda pass ta' approvazzjoni għal dan l-oġġett.", + "No archived conversations": "L-ebda konverżazzjoni arkivjata", + "No audit trail entries found": "Ma nstabet l-ebda entrata ta' traċċa ta' verifika", + "No changes recorded": "L-ebda bidla ma ġiet irreġistrata", + "No chunks to vectorize": "L-ebda chunk biex jiġi vettorizzat", + "No collections found": "Ma nstabet l-ebda kollezzjoni", + "No configuration data": "L-ebda data ta' konfigurazzjoni", + "No configuration selected": "L-ebda konfigurazzjoni magħżula", + "No configurations are available.": "L-ebda konfigurazzjoni mhix disponibbli.", + "No configurations found": "Ma nstabet l-ebda konfigurazzjoni", + "No configurations found for this application.": "Ma nstabet l-ebda konfigurazzjoni għal din l-applikazzjoni.", + "No contacts linked to this object": "L-ebda kuntatt ma huwa llinkjat ma' dan l-oġġett", + "No conversations yet. Create a new one to get started!": "Għad l-ebda konverżazzjoni. Oħloq waħda ġdida biex tibda!", + "No dashboards yet": "Għad l-ebda dashboard", + "No data available": "L-ebda data mhix disponibbli", + "No data available for chart": "L-ebda data mhix disponibbli għall-grafika", + "No deleted items found": "Ma nstab l-ebda oġġett imħassar", + "No deletion data available": "L-ebda data ta' tħassir mhix disponibbli", + "No description found": "Ma nstabet l-ebda deskrizzjoni", + "No description provided": "Ma ġiet ipprovduta l-ebda deskrizzjoni", + "No emails linked to this object": "L-ebda email ma hija llinkjata ma' dan l-oġġett", + "No endpoints defined": "L-ebda endpoint mhux definit", + "No entities detected for this email.": "L-ebda entità ma ġiet identifikata għal din l-email.", + "No entities detected in this file. Run text extraction first.": "L-ebda entità ma ġiet identifikata f'dan il-fajl. Mexxi l-estrazzjoni tat-test l-ewwel.", + "No entities found": "L-ebda entità ma nstabet", + "No entities have been detected yet": "L-ebda entità għadha ma ġiet skoperta", + "No events found": "L-ebda avveniment ma nstab", + "No events linked to this object": "L-ebda avveniment mhux marbut ma' dan l-Oġġett", + "No expired audit trails found to clear": "L-ebda audit trail skadut ma nstab biex jitnaddaf", + "No expired search trails found to clear": "L-ebda search trail skadut ma nstab biex jitnaddaf", + "No extraction data available for this file": "L-ebda data ta' estrazzjoni mhux disponibbli għal dan il-Fajl", + "No facetable fields available. Select a register and schema to see available filters.": "L-ebda kamp faċettabbli mhux disponibbli. Agħżel Reġistru u Skema biex tara l-filtri disponibbli.", + "No files found": "L-ebda Fajl ma nstab", + "No files have been extracted yet": "L-ebda Fajl għadu ma ġie estratt", + "No files selected": "L-ebda Fajl ma ntgħażel", + "No files to process": "L-ebda Fajl x'jiġi pproċessat", + "No files to reindex": "L-ebda Fajl x'jiġi indiċjat mill-ġdid", + "No filters are currently active. This will delete ALL audit trail entries!": "L-ebda filtru mhux attiv bħalissa. Dan se jħassar id-daħliet KOLLHA tal-audit trail!", + "No groups found. Try a different search.": "L-ebda grupp ma nstab. Ipprova tfittxija differenti.", + "No label": "L-ebda tikketta", + "No labels": "L-ebda tikketta", + "No linked objects": "L-ebda Oġġett marbut", + "No log entries found": "L-ebda daħla ta' log ma nstabet", + "No logs are available for this configuration.": "L-ebda log mhux disponibbli għal din il-konfigurazzjoni.", + "No logs are available for this source.": "L-ebda log mhux disponibbli għal dan is-Sors.", + "No logs found": "L-ebda log ma nstab", + "No matches": "L-ebda riżultat", + "No mismatched fields found - SOLR schema is properly configured": "L-ebda kamp diskordanti ma nstab - l-iskema SOLR hija kkonfigurata sewwa", + "No objects are currently selected for deletion.": "L-ebda Oġġett mhux magħżul bħalissa għat-tħassir.", + "No objects are currently selected for permanent deletion.": "L-ebda Oġġett mhux magħżul bħalissa għat-tħassir permanenti.", + "No objects are currently selected for restoration.": "L-ebda Oġġett mhux magħżul bħalissa għar-restawr.", + "No objects found": "L-ebda Oġġett ma nstab", + "No objects found to vectorize": "L-ebda Oġġett ma nstab biex jiġi vettorizzat", + "No objects linked to this email": "L-ebda Oġġett marbut ma' dan l-email", + "No objects selected": "L-ebda Oġġett ma ntgħażel", + "No objects selected for deletion": "L-ebda Oġġett ma ntgħażel għat-tħassir", + "No objects selected for restoration": "L-ebda Oġġett ma ntgħażel għar-restawr", + "No objects to delete": "L-ebda Oġġett x'jitħassar", + "No objects to delete for schema {schema}": "L-ebda Oġġett x'jitħassar għall-Iskema {schema}", + "No objects were permanently deleted": "L-ebda Oġġett ma tħassar b'mod permanenti", + "No organisations found": "L-ebda organizzazzjoni ma nstabet", + "No personal data was found for this subject identifier.": "L-ebda data personali ma nstabet għal dan l-identifikatur tas-suġġett.", + "No processing activities yet": "L-ebda attività ta' pproċessar s'issa", + "No properties found": "L-ebda Proprjetà ma nstabet", + "No properties match your filters.": "L-ebda Proprjetà ma taqbel mal-filtri tiegħek.", + "No purge date set": "L-ebda data ta' purge mhix stabbilita", + "No register data available": "L-ebda data tar-Reġistru mhix disponibbli", + "No register objects reference this file": "L-ebda Oġġett tar-Reġistru ma jirreferi għal dan il-Fajl", + "No registers found": "L-ebda Reġistru ma nstab", + "No registers found for this application.": "L-ebda Reġistru ma nstab għal din l-applikazzjoni.", + "No registers found. Create a register to configure permissions.": "L-ebda Reġistru ma nstab. Oħloq Reġistru biex tikkonfigura l-permessi.", + "No related cases found for this sender": "L-ebda każ relatat ma nstab għal dan il-mittent", + "No related entities": "L-ebda entità relatata", + "No relations found": "L-ebda relazzjoni ma nstabet", + "No request body available": "L-ebda body tat-talba mhux disponibbli", + "No response body available": "L-ebda body tar-rispons mhux disponibbli", + "No saved views yet. Create one in the Search tab!": "L-ebda vista mibgħuta s'issa. Oħloq waħda fit-tab tat-Tfittxija!", + "No saved views yet. create one in the search tab!": "L-ebda vista mibgħuta s'issa. oħloq waħda fit-tab tat-tfittxija!", + "No scan run yet": "L-ebda scan ma sar s'issa", + "No schema data available": "L-ebda data tal-Iskema mhix disponibbli", + "No schema selected for exploration": "L-ebda Skema ma ntgħażlet għall-esplorazzjoni", + "No schemas configured for mail linking.": "L-ebda Skema mhix ikkonfigurata għar-rabta tal-mail.", + "No schemas found": "L-ebda Skema ma nstabet", + "No schemas found for this application.": "L-ebda Skema ma nstabet għal din l-applikazzjoni.", + "No search terms data available": "L-ebda data tat-termini ta' tfittxija mhix disponibbli", + "No search trail entries found": "L-ebda daħla ta' search trail ma nstabet", + "No sources are available.": "L-ebda Sors mhux disponibbli.", + "No sources found": "L-ebda Sors ma nstab", + "No synchronizations found": "L-ebda sinkronizzazzjoni ma nstabet", + "No templates found": "L-ebda mudell ma nstab", + "No templates have been created yet": "L-ebda mudell għadu ma nħoloq", + "No tools available. Tools can be registered by installed apps.": "L-ebda għodda mhix disponibbli. L-għodod jistgħu jiġu rreġistrati mill-applikazzjonijiet installati.", + "No users in this organisation.": "L-ebda utent f'din l-organizzazzjoni.", + "No valid magic-mapped register+schema combinations found": "L-ebda kombinazzjoni valida magic-mapped ta' Reġistru+Skema ma nstabet", + "No views found. Create views first before configuring vectorization.": "L-ebda vista ma nstabet. Oħloq visti l-ewwel qabel ma tikkonfigura l-vettorizzazzjoni.", + "No views match your search": "L-ebda vista ma taqbel mat-tfittxija tiegħek", + "No webhooks found": "L-ebda webhook ma nstab", + "No webhooks have been configured yet": "L-ebda webhook għadu ma ġie kkonfigurat", + "No workflows found in this project. Create workflows in the n8n editor.": "L-ebda workflow ma nstab f'dan il-proġett. Oħloq workflows fl-editur n8n.", + "None": "L-ebda", + "Not Found": "Ma nstabx", + "Not extracted": "Mhux estratt", + "Not helpful": "Mhux ta' għajnuna", + "Not in use": "Mhux fl-użu", + "Note:": "Nota:", + "Notifications": "Notifiki", + "Number Constraints": "Restrizzjonijiet tan-Numri", + "Number of Sources": "Numru ta' Sorsi", + "Number of chunks to vectorize in one API call (1-100).": "Numru ta' chunks li jiġu vettorizzati f'sejħa waħda tal-API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Numru ta' chunks li jiġu vettorizzati f'sejħa waħda tal-API. Ogħla = aktar mgħaġġel imma aktar memorja. Rakkomandat: 10-50.", + "Number of objects to process in each batch (1-5000).": "Numru ta' Oġġetti li jiġu pproċessati f'kull batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Numru ta' Oġġetti li jiġu vettorizzati f'sejħa waħda tal-API. Ogħla = aktar mgħaġġel imma aktar memorja. Rakkomandat: 10-50.", + "Numeric Range:": "Firxa Numerika:", + "Object": "Oġġett", + "Object #{id}": "Oġġett #{id}", + "Object Collection": "Ġabra ta' Oġġetti", + "Object Configuration:": "Konfigurazzjoni tal-Oġġett:", + "Object Count Prediction": "Tbassir tal-Għadd ta' Oġġetti", + "Object Extraction Mode": "Modalità tal-Estrazzjoni tal-Oġġetti", + "Object ID": "ID tal-Oġġett", + "Object Management": "Ġestjoni tal-Oġġetti", + "Object Text Extraction": "Estrazzjoni tat-Test tal-Oġġetti", + "Object Vectorization": "Vettorizzazzjoni tal-Oġġetti", + "Object Warmup": "Warmup tal-Oġġetti", + "Object changes in owned objects": "Tibdil fl-Oġġetti fl-Oġġetti proprjetarji", + "Object collection not configured": "Il-ġabra ta' Oġġetti mhix ikkonfigurata", + "Object metadata incomplete for file upload": "Il-metadata tal-Oġġett mhix kompluta għall-upload tal-Fajl", + "Object not found": "L-Oġġett ma nstabx", + "Object permanently deleted": "L-Oġġett tħassar b'mod permanenti", + "Object restored successfully": "L-Oġġett ġie restawrat b'suċċess", + "Object settings updated successfully": "L-issettjar tal-Oġġett ġie aġġornat b'suċċess", + "Object sources": "Sorsi tal-Oġġett", + "Object successfully deleted": "L-Oġġett tħassar b'suċċess", + "Object successfully modified": "L-Oġġett ġie mmodifikat b'suċċess", + "Object unlocked successfully": "L-Oġġett ġie żblukkat b'suċċess", + "Object vectorization configuration saved successfully": "Il-konfigurazzjoni tal-vettorizzazzjoni tal-Oġġetti ġiet salvata b'suċċess", + "Object vectorized successfully": "L-Oġġett ġie vettorizzat b'suċċess", + "Object:": "Oġġett:", + "Objects": "Oġġetti", + "Objects Analyzed": "Oġġetti Analizzati", + "Objects Distribution": "Distribuzzjoni tal-Oġġetti", + "Objects being analyzed": "Oġġetti li qed jiġu analizzati", + "Objects by Register": "Oġġetti skont ir-Reġistru", + "Objects by Schema": "Oġġetti skont l-Iskema", + "Objects deletion completed for {schema}": "It-tħassir tal-Oġġetti tlesta għal {schema}", + "Objects deletion completed successfully": "It-tħassir tal-Oġġetti tlesta b'suċċess", + "Objects in schema": "Oġġetti fl-Iskema", + "Objects successfully deleted": "L-Oġġetti tħassru b'suċċess", + "Objects to Process:": "Oġġetti x'jiġu Pproċessati:", + "Objects to be analyzed": "Oġġetti li jridu jiġu analizzati", + "Objects to be deleted": "Oġġetti li jridu jitħassru", + "Objects to be validated": "Oġġetti li jridu jiġu validati", + "Objects will be serialized as JSON text before vectorization": "L-Oġġetti se jiġu serializzati bħala test JSON qabel il-vettorizzazzjoni", + "Objects will be soft deleted and moved to the": "L-Oġġetti se jitħassru b'mod soft u jiġu mċaqilqa fil-", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "L-Oġġetti se jitħassru b'mod soft (immarkati bħala mħassra imma jinżammu fid-database). Jistgħu jiġu rkuprati aktar tard jekk ikun meħtieġ.", + "Old Value": "Valur Antik", + "Oldest Item (days)": "L-Eqdem Element (jiem)", + "Ollama Chat Settings": "Issettjar tal-Chat Ollama", + "Ollama Embedding Configuration": "Konfigurazzjoni tal-Embedding Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Biss b'Tibdiliet", + "Only members of selected groups can access this organisation": "Biss il-membri tal-gruppi magħżula jistgħu jaċċessaw din l-organizzazzjoni", + "Open Nextcloud App Store": "Iftaħ l-App Store ta' Nextcloud", + "Open OpenRegister sidebar": "Iftaħ il-barra ġenba ta' OpenRegister", + "Open Register Objects": "Iftaħ l-Oġġetti tar-Reġistru", + "Open folder": "Iftaħ il-folder", + "Open in OpenRegister": "Iftaħ f'OpenRegister", + "Open n8n Editor": "Iftaħ l-Editur n8n", + "OpenAI Chat Settings": "Issettjar tal-Chat OpenAI", + "OpenAI Embedding Configuration": "Konfigurazzjoni tal-Embedding OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Issettjar ta' OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Dashboards definiti mill-operatur u rapporti skedati. Kull dashboard huwa Oġġett ta' klassi ewlenija fir-Reġistru `reports`; il-widgets huma ddikjarati fl-array `widgets` tad-dashboard u jiġu renderjati live mill-aggregazzjonijiet / GraphQL.", + "Optimizing search performance...": "Qed tiġi ottimizzata l-prestazzjoni tat-tfittxija...", + "Optional URL-friendly identifier": "Identifikatur faċli għall-URL fakultattiv", + "Optional description for this facet": "Deskrizzjoni fakultattiva għal din il-faċetta", + "Optional end date property": "Proprjetà tad-data tat-tmiem fakultattiva", + "Optional event description template": "Mudell tad-deskrizzjoni tal-avveniment fakultattiv", + "Optional location property": "Proprjetà tal-post fakultattiva", + "Optional webhook secret for signature verification": "Secret tal-webhook fakultattiv għall-verifika tal-firma", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Fakultattiv: Żid parametri tal-query biex tiffiltra l-Iskema referenzjata (eż., status=active&type=public)", + "Or": "Jew", + "Order": "Ordni", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Tikketti tal-lingwa BCP 47 ordnati. L-ewwel lingwa hija d-default tar-Reġistru u tmexxi l-fallback Accept-Language għall-Proprjetajiet traduċibbli.", + "Organisation": "Organizzazzjoni", + "Organisation Configuration": "Konfigurazzjoni tal-Organizzazzjoni", + "Organisation ID": "ID tal-Organizzazzjoni", + "Organisation Members": "Membri tal-Organizzazzjoni", + "Organisation Statistics": "Statistika tal-Organizzazzjoni", + "Organisation created successfully": "L-organizzazzjoni nħolqot b'suċċess", + "Organisation membership changes": "Tibdil fis-sħubija tal-organizzazzjoni", + "Organisation name is required": "L-isem tal-organizzazzjoni huwa meħtieġ", + "Organisation settings saved successfully": "L-issettjar tal-organizzazzjoni ġie salvat b'suċċess", + "Organisation successfully created": "L-organizzazzjoni nħolqot b'suċċess", + "Organisation successfully updated": "L-organizzazzjoni ġiet aġġornata b'suċċess", + "Organisational measures": "Miżuri organizzattivi", + "Organisations": "Organizzazzjonijiet", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organizzazzjoni", + "Organization ID (Optional)": "ID tal-Organizzazzjoni (Fakultattiv)", + "Organizations": "Organizzazzjonijiet", + "Orphaned Items": "Elementi Orfni", + "Other": "Oħrajn", + "Output Mapping": "Mappar tal-Output", + "Overview": "Ħarsa Ġenerali", + "Owner": "Sid", + "Owner:": "Sid:", + "PDF": "PDF", + "PII hits": "Hits PII", + "PII rows": "Filieri PII", + "Page {current} of {total}": "Paġna {current} minn {total}", + "Parallel Mode (Faster, more resource intensive)": "Modalità Parallela (Aktar mgħaġġla, aktar intensiva fir-riżorsi)", + "Parallel:": "Parallel:", + "Password": "Password", + "Password changes are not supported by your authentication provider.": "It-tibdil tal-password mhux appoġġjat mill-fornitur tal-awtentikazzjoni tiegħek.", + "Password for SOLR authentication (optional)": "Password għall-awtentikazzjoni SOLR (fakultattiv)", + "Password updated successfully": "Il-password ġiet aġġornata b'suċċess", + "Path": "Path", + "Path in repository": "Path fir-repository", + "Path where the configuration file will be saved in the repository": "Path fejn il-Fajl tal-konfigurazzjoni se jiġi salvat fir-repository", + "Path where the register OAS file will be saved in the repository": "Path fejn il-Fajl OAS tar-Reġistru se jiġi salvat fir-repository", + "Pattern (regex)": "Pattern (regex)", + "Pattern Issue": "Problema fil-Pattern", + "Pattern constraint is missing": "Ir-restrizzjoni tal-pattern hija nieqsa", + "Patterns:": "Patterns:", + "Pending": "Pendenti", + "Permanent Delete Mode": "Modalità ta' Tħassir Permanenti", + "Permanently Delete": "Ħassar b'mod Permanenti", + "Permanently Delete ({count})": "Ħassar b'mod Permanenti ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Ħassar b'mod permanenti l-{count} Oġġetti kollha mħassra b'mod soft. Dan ma jistax jitreġġa' lura!", + "Permanently delete already soft-deleted objects": "Ħassar b'mod permanenti l-Oġġetti diġà mħassra b'mod soft", + "Permanently delete this conversation?": "Tħassar din il-konversazzjoni b'mod permanenti?", + "Permission Matrix": "Matriċi tal-Permessi", + "Person": "Persuna", + "Personal Data": "Data Personali", + "Personal Data Export": "Esportazzjoni tad-Data Personali", + "Persons": "Persuni", + "Phone": "Telefown", + "Phone Numbers": "Numri tat-Telefown", + "Photos": "Ritratti", + "Please create an agent in the": "Jekk jogħġbok oħloq aġent fil-", + "Please select a user": "Jekk jogħġbok agħżel utent", + "Please select an agent to continue": "Jekk jogħġbok agħżel aġent biex tkompli", + "Please select an organisation": "Jekk jogħġbok agħżel organizzazzjoni", + "Please select at least one item to import": "Jekk jogħġbok agħżel mill-inqas element wieħed biex timporta", + "Please select registers or schemas first": "Jekk jogħġbok agħżel Reġistri jew Skemi l-ewwel", + "Please select which register and schema to use for the new object": "Jekk jogħġbok agħżel liema Reġistru u Skema tuża għall-Oġġett il-ġdid", + "Please try again later.": "Jekk jogħġbok erġa' pprova aktar tard.", + "Please wait while we fetch your agents.": "Jekk jogħġbok stenna sakemm niġbdu l-aġenti tiegħek.", + "Please wait while we fetch your applications.": "Jekk jogħġbok stenna sakemm niġbdu l-applikazzjonijiet tiegħek.", + "Please wait while we fetch your configurations.": "Jekk jogħġbok stenna sakemm niġbdu l-konfigurazzjonijiet tiegħek.", + "Please wait while we fetch your deleted items.": "Jekk jogħġbok stenna sakemm niġbdu l-elementi mħassra tiegħek.", + "Please wait while we fetch your sources.": "Jekk jogħġbok stenna sakemm niġbdu s-Sorsi tiegħek.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Jekk jogħġbok stenna sakemm inħassru l-ġabra SOLR b'mod permanenti. Dan jista' jieħu ftit ħin.", + "Polls": "Stħarriġ", + "Popular Search Terms": "Termini ta' Tfittxija Popolari", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Il-preferenzi ġew salvati", + "Prerequisites": "Prerekwiżiti", + "Press Enter to send, Shift+Enter for new line": "Agħfas Enter biex tibgħat, Shift+Enter għal linja ġdida", + "Press enter to send, shift+enter for new line": "Agħfas enter biex tibgħat, shift+enter għal linja ġdida", + "Prettify": "Prettify", + "Preview erasure": "Previżwalizza t-tħassir", + "Preview:": "Previżwalizzazzjoni:", + "Previous": "Preċedenti", + "Private": "Privat", + "Private Agent (Default)": "Aġent Privat (Default)", + "Private agents": "Aġenti privati", + "Process Name (optional)": "Isem tal-Proċess (fakultattiv)", + "Processed {indexed} files, {failed} failed": "{indexed} Fajls ġew ipproċessati, {failed} fallew", + "Processes chunks in batches with simulated parallelism.": "Jipproċessa chunks f'batches b'parallelliżmu simulat.", + "Processes file chunks sequentially (safest).": "Jipproċessa l-chunks tal-Fajl b'mod sekwenzjali (l-aktar sikur).", + "Processes objects in chunks with simulated parallelism.": "Jipproċessa l-Oġġetti f'chunks b'parallelliżmu simulat.", + "Processes objects sequentially (safest).": "Jipproċessa l-Oġġetti b'mod sekwenzjali (l-aktar sikur).", + "Processing": "Qed jiġi pproċessat", + "Processing Limits": "Limiti tal-Ipproċessar", + "Processing...": "Qed jiġi pproċessat...", + "Project initialized successfully": "Il-proġett ġie inizjalizzat b'suċċess", + "Project not found. Please initialize first.": "Il-proġett ma nstabx. Jekk jogħġbok inizjalizza l-ewwel.", + "Properties": "Proprjetajiet", + "Property Behaviors": "Imġiba tal-Proprjetà", + "Property Configuration:": "Konfigurazzjoni tal-Proprjetà:", + "Property Filters": "Filtri tal-Proprjetà", + "Property Title": "Titlu tal-Proprjetà", + "Property Type": "Tip tal-Proprjetà", + "Property can be improved": "Il-Proprjetà tista' tittejjeb", + "Property could not be added": "Il-Proprjetà ma setgħetx tiżdied", + "Property could not be updated": "Il-Proprjetà ma setgħetx tiġi aġġornata", + "Property name of inversed relation": "Isem tal-Proprjetà tar-relazzjoni invertita", + "Property successfully added": "Il-Proprjetà żdiedet b'suċċess", + "Property successfully updated": "Il-Proprjetà ġiet aġġornata b'suċċess", + "Protected": "Protett", + "Provider": "Fornitur", + "Provider is required for testing": "Il-fornitur huwa meħtieġ għall-ittestjar", + "Public": "Pubbliku", + "Public View": "Vista Pubblika", + "Public views can be accessed by anyone in the system": "Il-visti pubbliċi jistgħu jiġu aċċessati minn kulħadd fis-sistema", + "Publish": "Ippubblika", + "Publish Configuration to GitHub": "Ippubblika l-Konfigurazzjoni fuq GitHub", + "Publish OAS": "Ippubblika l-OAS", + "Publish Objects": "Ippubblika l-Oġġetti", + "Publish Register OAS to GitHub": "Ippubblika l-OAS tar-Reġistru fuq GitHub", + "Publish all objects in this schema": "Ippubblika l-Oġġetti kollha f'din l-Iskema", + "Publish objects to make them publicly available": "Ippubblika l-Oġġetti biex tagħmilhom disponibbli pubblikament", + "Published": "Ippubblikat", + "Published:": "Ippubblikat:", + "Publishing...": "Qed jiġi ppubblikat...", + "Purge": "Purge", + "Purge Date": "Data tal-Purge", + "Purge {count} object from database": "Purge {count} Oġġett mid-database", + "Purge {count} objects from database": "Purge {count} Oġġetti mid-database", + "Purpose limitation *": "Limitazzjoni tal-iskop *", + "Push Notifications": "Notifiki Push", + "Query Complexity Distribution": "Distribuzzjoni tal-Kumplessità tal-Query", + "Query Help": "Għajnuna dwar il-Query", + "Query parameter is required": "Il-parametru tal-query huwa meħtieġ", + "Quota": "Kwota", + "RAG": "RAG", + "RAG Configuration": "Konfigurazzjoni RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "L-issettjar RBAC ġie aġġornat b'suċċess", + "Rate limits cleared successfully": "Il-limiti tar-rata tneħħew b'suċċess", + "Raw Changes Data": "Data Mhux Ipproċessata tat-Tibdil", + "Re-vectorize on object update": "Erġa' vettorizza fl-aġġornament tal-Oġġett", + "Read": "Aqra", + "Real-time push notification status via notify_push": "Status tan-notifika push f'ħin reali permezz ta' notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "In-notifiki push f'ħin reali huma attivi. Il-klijenti konnessi jirċievu aġġornamenti istantanji meta l-Oġġetti jinħolqu, jiġu aġġornati, jew jitħassru.", + "Realtime push active": "Push f'ħin reali attiv", + "Realtime push not available — the notify_push app is not installed": "Push f'ħin reali mhux disponibbli — l-applikazzjoni notify_push mhix installata", + "Reason": "Raġuni", + "Reason (optional)": "Raġuni (fakultattiv)", + "Rebase operation completed successfully": "L-operazzjoni ta' rebase tlestiet b'suċċess", + "Rebase operation failed: {error}": "L-operazzjoni ta' rebase falliet: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Rakkomandazzjonijiet:", + "Recommended Type:": "Tip Rakkomandat:", + "Recommended: 5 sources": "Rakkomandat: 5 Sorsi", + "Refresh": "Aġġorna", + "Refresh Data": "Aġġorna d-Data", + "Refresh Stats": "Aġġorna l-Istatistika", + "Refresh Workflows": "Aġġorna l-Workflows", + "Refresh dashboard": "Aġġorna d-dashboard", + "Refresh database info": "Aġġorna l-informazzjoni tad-database", + "Register": "Reġistru", + "Register #{id}": "Reġistru #{id}", + "Register / Schema": "Reġistru / Skema", + "Register ID": "ID tar-Reġistru", + "Register Objects": "Oġġetti tar-Reġistru", + "Register Overview": "Ħarsa Ġenerali tar-Reġistru", + "Register Statistics": "Statistika tar-Reġistru", + "Register Totals": "Totali tar-Reġistru", + "Register and schema are required": "Ir-Reġistru u l-Iskema huma meħtieġa", + "Register annotation": "Annotazzjoni tar-Reġistru", + "Register depublished successfully": "Ir-Reġistru ġie depubblikat b'suċċess", + "Register not found": "Ir-Reġistru ma nstabx", + "Register objects": "Oġġetti tar-Reġistru", + "Register objects deletion completed successfully": "It-tħassir tal-Oġġetti tar-Reġistru tlesta b'suċċess", + "Register published successfully": "Ir-Reġistru ġie ppubblikat b'suċċess", + "Register/Schema Usage": "Użu tar-Reġistru/Skema", + "Register:": "Reġistru:", + "Registers": "Reġistri", + "Reindex": "Indiċja mill-ġdid", + "Reindex completed": "L-indiċjar mill-ġdid tlesta", + "Reindex completed successfully": "L-indiċjar mill-ġdid tlesta b'suċċess", + "Reindex completed! Processed {count} objects in {duration}s": "L-indiċjar mill-ġdid tlesta! {count} Oġġetti ġew ipproċessati f'{duration}s", + "Reindex failed": "L-indiċjar mill-ġdid falla", + "Reindex failed: %s": "L-indiċjar mill-ġdid falla: %s", + "Reject": "Irrifjuta", + "Rejected": "Irrifjutat", + "Related Cases": "Każijiet Relatati", + "Relations": "Relazzjonijiet", + "Remote Version": "Verżjoni Remota", + "Remove": "Neħħi", + "Remove Deck card": "Neħħi l-karta tad-Deck", + "Remove avatar": "Neħħi l-avatar", + "Remove contact": "Neħħi l-kuntatt", + "Remove filter": "Neħħi l-filtru", + "Remove from favorites": "Neħħi mill-favoriti", + "Remove from list": "Neħħi mil-lista", + "Remove group": "Neħħi l-grupp", + "Remove link": "Neħħi r-rabta", + "Remove link to {name}": "Neħħi r-rabta ma' {name}", + "Remove link to {name}?": "Tneħħi r-rabta ma' {name}?", + "Remove link to {title}": "Neħħi r-rabta ma' {title}", + "Remove oneOf entry": "Neħħi d-daħla oneOf", + "Remove property after write-back": "Neħħi l-Proprjetà wara l-write-back", + "Remove {title}": "Neħħi {title}", + "Remove {userId} from this organisation?": "Tneħħi {userId} minn din l-organizzazzjoni?", + "Removed": "Imneħħi", + "Removed from favorites": "Imneħħi mill-favoriti", + "Removing schema {schema}...": "Qed titneħħa l-Iskema {schema}...", + "Rename Conversation": "Ibdel l-Isem tal-Konversazzjoni", + "Rename conversation": "Ibdel l-isem tal-konversazzjoni", + "Replicas": "Replicas", + "Reports": "Rapporti", + "Repository": "Repository", + "Request Body": "Body tat-Talba", + "Request Data": "Data tat-Talba", + "Request Details": "Dettalji tat-Talba", + "Request Quota (per day)": "Kwota tat-Talbiet (kuljum)", + "Request account deactivation": "Itlob id-diżattivazzjoni tal-kont", + "Request account deactivation. This will notify administrators for review.": "Itlob id-diżattivazzjoni tal-kont. Dan se jinnotifika lill-amministraturi għar-rieżami.", + "Request body must be JSON with a \"query\" field": "Il-body tat-talba jrid ikun JSON b'kamp \"query\"", + "Request timeout in seconds": "Timeout tat-talba f'sekondi", + "Requested at": "Mitlub fil-", + "Required": "Meħtieġ", + "Required Token Scopes": "Scopes tat-Token Meħtieġa", + "Required field": "Kamp meħtieġ", + "Required status is inconsistent": "L-istatus meħtieġ huwa inkonsistenti", + "Requires Dolphin AI for OCR text extraction": "Jeħtieġ Dolphin AI għall-estrazzjoni tat-test OCR", + "Rerun Search": "Erġa' Mexxi t-Tfittxija", + "Rerunning search: {searchTerm}": "Qed terġa' titmexxa t-tfittxija: {searchTerm}", + "Reset Changes": "Irrisettja t-Tibdiliet", + "Reset Filters": "Irrisettja l-Filtri", + "Resource Quotas": "Kwoti tar-Riżorsi", + "Response Body": "Body tar-Rispons", + "Response Details": "Dettalji tar-Rispons", + "Response Time:": "Ħin tar-Rispons:", + "Restore": "Restawra", + "Restore conversation": "Restawra l-konversazzjoni", + "Restore or permanently delete items": "Restawra jew ħassar l-elementi b'mod permanenti", + "Restore {count} object": "Restawra {count} Oġġett", + "Restore {count} objects": "Irrestawra {count} oġġetti", + "Result Count Range": "Firxa tal-Għadd tar-Riżultati", + "Results": "Riżultati", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Perjodu ta' żamma (tul ISO-8601, eż. P10Y, P30D)", + "Retention settings updated successfully": "L-issettjar taż-żamma ġie aġġornat b'suċċess", + "Retrieve all objects for this schema": "Irkupra l-oġġetti kollha għal din l-iskema", + "Retry": "Erġa' pprova", + "Retry Failed Extractions": "Erġa' pprova l-Estrazzjonijiet li Fallew", + "Retry Policy": "Politika ta' Ripetizzjoni", + "Retry completed": "Ir-ripetizzjoni tlestiet", + "Retry upload": "Erġa' pprova t-tlugħ", + "Retrying...": "Qed jerġa' jipprova...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Irrevedi l-oġġetti magħżula hawn taħt. Tista' tneħħi kwalunkwe oġġett li ma tridx tħassar billi tikklikkja l-buttuna tat-tneħħija.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Irrevedi l-oġġetti magħżula hawn taħt. Tista' tneħħi kwalunkwe oġġett li ma tridx tinkludi.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Irrevedi l-oġġetti magħżula hawn taħt. Tista' tneħħi kwalunkwe oġġett li ma tridx tħassar permanentement billi tikklikkja l-buttuna tat-tneħħija. Din l-azzjoni ma tistax titreġġa' lura.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Irrevedi l-oġġetti magħżula hawn taħt. Tista' tneħħi kwalunkwe oġġett li ma tridx tirrestawra billi tikklikkja l-buttuna tat-tneħħija. L-oġġetti se jiġu rrestawrati fil-post oriġinali tagħhom.", + "Revoke": "Irrevoka", + "Right": "Lemin", + "Risk Level": "Livell ta' Riskju", + "Risk level": "Livell ta' riskju", + "Roles and Permissions": "Rwoli u Permessi", + "Roles updated successfully": "Ir-rwoli ġew aġġornati b'suċċess", + "Run compliance scan": "Mexxi skan tal-konformità", + "SOLR Connection Settings": "Issettjar tal-Konnessjoni SOLR", + "SOLR Index Warmup": "Tisħin tal-Indiċi SOLR", + "SOLR Version:": "Verżjoni SOLR:", + "SOLR actions menu": "Menu tal-azzjonijiet SOLR", + "SOLR base path (usually /solr)": "Triq bażi SOLR (ġeneralment /solr)", + "SOLR collection deleted successfully": "Il-ġabra SOLR tħassret b'suċċess", + "SOLR connection test failed: {error}": "It-test tal-konnessjoni SOLR falla: {error}", + "SOLR connection test successful!": "It-test tal-konnessjoni SOLR irnexxa!", + "SOLR index warmup completed successfully": "It-tisħin tal-indiċi SOLR tlesta b'suċċess", + "SOLR is disabled": "SOLR huwa diżattivat", + "SOLR is not available or not configured": "SOLR mhuwiex disponibbli jew mhuwiex ikkonfigurat", + "SOLR logging disabled": "Ir-reġistrazzjoni SOLR ġiet diżattivata", + "SOLR logging enabled": "Ir-reġistrazzjoni SOLR ġiet attivata", + "SOLR search disabled": "It-tiftix SOLR ġie diżattivat", + "SOLR search enabled": "It-tiftix SOLR ġie attivat", + "SOLR server hostname or IP address": "Isem tal-host jew indirizz IP tas-server SOLR", + "SOLR server port number (optional, defaults to 8983)": "Numru tal-port tas-server SOLR (mhux obbligatorju, b'mod awtomatiku 8983)", + "SOLR settings updated successfully": "L-issettjar SOLR ġie aġġornat b'suċċess", + "SOLR setup completed successfully": "L-issettjar ta' SOLR tlesta b'suċċess", + "SOLR setup completed successfully!": "L-issettjar ta' SOLR tlesta b'suċċess!", + "SOLR setup error: %s": "Żball fl-issettjar ta' SOLR: %s", + "SOLR setup failed": "L-issettjar ta' SOLR falla", + "SOLR setup failed - check logs": "L-issettjar ta' SOLR falla - iċċekkja r-reġistri", + "SOLR setup failed. Please check the configuration and try again.": "L-issettjar ta' SOLR falla. Jekk jogħġbok iċċekkja l-konfigurazzjoni u erġa' pprova.", + "SOLR setup failed: {error}": "L-issettjar ta' SOLR falla: {error}", + "SOLR warmup failed: {error}": "It-tisħin ta' SOLR falla: {error}", + "Sample Values:": "Valuri ta' Kampjun:", + "Save": "Issejvja", + "Save Configuration": "Issejvja l-Konfigurazzjoni", + "Save Connection Settings": "Issejvja l-Issettjar tal-Konnessjoni", + "Save Roles": "Issejvja r-Rwoli", + "Save Settings": "Issejvja l-Issettjar", + "Save changes": "Issejvja l-bidliet", + "Save current search as view": "Issejvja t-tiftix attwali bħala veduta", + "Save labels": "Issejvja t-tikketti", + "Save tags for {name}": "Issejvja t-tikketti għal {name}", + "Save the organisation first to manage users.": "Issejvja l-organizzazzjoni l-ewwel biex timmaniġġja l-utenti.", + "Saved Views": "Veduti Salvati", + "Saving...": "Qed jissejvja...", + "Scheduled": "Skedat", + "Schema": "Skema", + "Schema ID": "ID tal-Iskema", + "Schema Name": "Isem tal-Iskema", + "Schema Statistics": "Statistika tal-Iskema", + "Schema annotation": "Annotazzjoni tal-iskema", + "Schema configuration updated successfully for {schema}": "Il-konfigurazzjoni tal-iskema ġiet aġġornata b'suċċess għal {schema}", + "Schema depublished successfully": "L-iskema ġiet ippubblikata lura b'suċċess", + "Schema objects deletion completed successfully": "It-tħassir tal-oġġetti tal-iskema tlesta b'suċċess", + "Schema published successfully": "L-iskema ġiet ippubblikata b'suċċess", + "Schema reference ($ref)": "Referenza tal-iskema ($ref)", + "Schema successfully created": "L-iskema nħolqot b'suċċess", + "Schema successfully updated": "L-iskema ġiet aġġornata b'suċċess", + "Schema {schema} removed successfully": "L-iskema {schema} tneħħiet b'suċċess", + "Schema:": "Skema:", + "Schemas": "Skemi", + "Scheme": "Skema", + "Search": "Fittex", + "Search / Views": "Tiftix / Veduti", + "Search / views": "Tiftix / veduti", + "Search Activity": "Attività tat-Tiftix", + "Search Analytics": "Analitika tat-Tiftix", + "Search Configuration": "Konfigurazzjoni tat-Tiftix", + "Search Entities": "Fittex Entitajiet", + "Search Files": "Fittex Fajls", + "Search GitHub": "Fittex f'GitHub", + "Search GitLab": "Fittex f'GitLab", + "Search Method": "Metodu ta' Tiftix", + "Search Mode": "Modalità ta' Tiftix", + "Search Query": "Mistoqsija ta' Tiftix", + "Search Results": "Riżultati tat-Tiftix", + "Search Term": "Terminu ta' Tiftix", + "Search Trail Details": "Dettalji tat-Traċċa tat-Tiftix", + "Search Trail Management": "Ġestjoni tat-Traċċa tat-Tiftix", + "Search Trail Statistics": "Statistika tat-Traċċa tat-Tiftix", + "Search Trails": "Traċċi tat-Tiftix", + "Search Views": "Veduti tat-Tiftix", + "Search Webhooks": "Fittex Webhooks", + "Search agents...": "Fittex aġenti...", + "Search applications...": "Fittex applikazzjonijiet...", + "Search by file name or path": "Fittex skont l-isem jew it-triq tal-fajl", + "Search by name or URL": "Fittex skont l-isem jew l-URL", + "Search by title or UUID...": "Fittex skont it-titlu jew UUID...", + "Search by value": "Fittex skont il-valur", + "Search configurations": "Fittex konfigurazzjonijiet", + "Search data sources...": "Fittex sorsi tad-data...", + "Search groups...": "Fittex gruppi...", + "Search in Database Objects": "Fittex fl-Oġġetti tad-Database", + "Search in Files": "Fittex fil-Fajls", + "Search in Objects": "Fittex fl-Oġġetti", + "Search objects": "Fittex oġġetti", + "Search objects...": "Fittex oġġetti...", + "Search property names...": "Fittex ismijiet ta' proprjetajiet...", + "Search registers...": "Fittex reġistri...", + "Search schemas...": "Fittex skemi...", + "Search trail deletion not implemented yet": "It-tħassir tat-traċċa tat-tiftix għadu mhux implimentat", + "Search views...": "Fittex veduti...", + "Search {name}...": "Fittex {name}...", + "Searching...": "Qed ifittex...", + "Secret": "Sigriet", + "Secret key for HMAC signature generation (optional)": "Ċavetta sigrieta għall-ġenerazzjoni tal-firma HMAC (mhux obbligatorja)", + "Security": "Sigurtà", + "See organisations as a table": "Ara l-organizzazzjonijiet bħala tabella", + "See organisations as cards": "Ara l-organizzazzjonijiet bħala kards", + "Select AI Agent": "Agħżel Aġent AI", + "Select Active Organisation": "Agħżel Organizzazzjoni Attiva", + "Select All": "Agħżel Kollox", + "Select ConfigSet": "Agħżel ConfigSet", + "Select File Types to Vectorize:": "Agħżel it-Tipi ta' Fajls biex Tivvettorizza:", + "Select HTTP method": "Agħżel metodu HTTP", + "Select Register and Schema": "Agħżel Reġistru u Skema", + "Select Views to Vectorize:": "Agħżel il-Veduti biex Tivvettorizza:", + "Select a Nextcloud group to add": "Agħżel grupp Nextcloud biex iżżid", + "Select a branch": "Agħżel fergħa", + "Select a date property": "Agħżel proprjetà ta' data", + "Select a model or type a custom model name": "Agħżel mudell jew ittajpja isem ta' mudell personalizzat", + "Select a register": "Agħżel reġistru", + "Select a register...": "Agħżel reġistru...", + "Select a repository": "Agħżel repożitorju", + "Select a repository you have write access to": "Agħżel repożitorju li għandek aċċess għall-kitba fih", + "Select a schema": "Agħżel skema", + "Select a schema...": "Agħżel skema...", + "Select a user": "Agħżel utent", + "Select agent type": "Agħżel it-tip ta' aġent", + "Select all entities": "Agħżel l-entitajiet kollha", + "Select an AI Agent": "Agħżel Aġent AI", + "Select an AI agent to begin chatting with your data.": "Agħżel aġent AI biex tibda tikkjattja mad-data tiegħek.", + "Select an email to see linked objects": "Agħżel email biex tara l-oġġetti marbuta", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Agħżel organizzazzjoni u utent biex iżżidhom bħala membru. Fittex organizzazzjonijiet skont l-isem.", + "Select backend": "Agħżel backend", + "Select chat model": "Agħżel mudell ta' chat", + "Select collection for files": "Agħżel ġabra għall-fajls", + "Select collection for objects": "Agħżel ġabra għall-oġġetti", + "Select configuration type...": "Agħżel it-tip ta' konfigurazzjoni...", + "Select default organisation": "Agħżel organizzazzjoni awtomatika", + "Select entity {value}": "Agħżel l-entità {value}", + "Select event to listen to...": "Agħżel avveniment biex tismagħ...", + "Select fields to display": "Agħżel l-oqsma li trid turi", + "Select files with the correct extension": "Agħżel fajls bl-estensjoni korretta", + "Select group": "Agħżel grupp", + "Select groups (optional)": "Agħżel gruppi (mhux obbligatorju)", + "Select groups to notify...": "Agħżel gruppi biex tinnotifika...", + "Select groups with access to this agent": "Agħżel gruppi b'aċċess għal dan l-aġent", + "Select groups...": "Agħżel gruppi...", + "Select model": "Agħżel mudell", + "Select one or more registers": "Agħżel reġistru wieħed jew aktar", + "Select one or more schemas": "Agħżel skema waħda jew aktar", + "Select options...": "Agħżel l-għażliet...", + "Select or create labels, or select \"No label\" to add files": "Agħżel jew oħloq tikketti, jew agħżel \"L-ebda tikketta\" biex iżżid fajls", + "Select owner application (optional)...": "Agħżel l-applikazzjoni proprjetarja (mhux obbligatorju)...", + "Select period": "Agħżel perjodu", + "Select property to send as payload": "Agħżel proprjetà biex tintbagħat bħala payload", + "Select provider": "Agħżel fornitur", + "Select registers and schemas to save a view": "Agħżel reġistri u skemi biex tissejvja veduta", + "Select retry policy": "Agħżel politika ta' ripetizzjoni", + "Select role": "Agħżel rwol", + "Select schemas (empty = all schemas)": "Agħżel skemi (vojt = l-iskemi kollha)", + "Select schemas to warm up": "Agħżel skemi biex issaħħan", + "Select search mode": "Agħżel modalità ta' tiftix", + "Select source type...": "Agħżel it-tip ta' sors...", + "Select target type": "Agħżel it-tip fil-mira", + "Select the branch to publish to": "Agħżel il-fergħa biex tippubblika fiha", + "Select the event this webhook should listen to": "Agħżel l-avveniment li dan il-webhook għandu jismagħ", + "Select views to filter data (optional)": "Agħżel veduti biex tiffiltra d-data (mhux obbligatorju)", + "Select views to limit which data the agent can access": "Agħżel veduti biex tillimita liema data l-aġent jista' jaċċessa", + "Select views to vectorize:": "Agħżel veduti biex tivvettorizza:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Agħżel liema gruppi Nextcloud huma disponibbli għal din l-organizzazzjoni. L-utenti f'dawn il-gruppi se jkollhom aċċess għar-riżorsi tal-organizzazzjoni.", + "Select which data views the AI can search": "Agħżel liema veduti ta' data l-AI jista' jfittex", + "Select which property from the event should be used as the webhook payload data": "Agħżel liema proprjetà mill-avveniment għandha tintuża bħala d-data tal-payload tal-webhook", + "Select which tools the AI can use to perform actions": "Agħżel liema għodod l-AI jista' juża biex iwettaq azzjonijiet", + "Select {fieldName} values": "Agħżel il-valuri {fieldName}", + "Selected": "Magħżul", + "Selected Groups": "Gruppi Magħżula", + "Selected Objects ({count})": "Oġġetti Magħżula ({count})", + "Selected audit trails deleted successfully": "It-traċċi tal-verifika magħżula tħassru b'suċċess", + "Selected search trails deleted successfully": "It-traċċi tat-tiftix magħżula tħassru b'suċċess", + "Selected users can access this view": "L-utenti magħżula jistgħu jaċċessaw din il-veduta", + "Send additional feedback": "Ibgħat feedback addizzjonali", + "Send as CloudEvent": "Ibgħat bħala CloudEvent", + "Send as cloudevent": "Ibgħat bħala cloudevent", + "Sensitive PII": "PII Sensittiv", + "Serial Mode (Safer, slower)": "Modalità Serjali (Aktar sikura, aktar bil-mod)", + "Serial:": "Serjali:", + "Server Configuration": "Konfigurazzjoni tas-Server", + "Server Information": "Informazzjoni tas-Server", + "Set as Active": "Issettja bħala Attiv", + "Set as active organisation": "Issettja bħala organizzazzjoni attiva", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Issettja limiti għall-użu tal-API u l-konsum tat-token. Uża 0 għal riżorsi mingħajr limitu.", + "Settings": "Issettjar", + "Settings updated successfully": "L-issettjar ġie aġġornat b'suċċess", + "Shards": "Shards", + "Share": "Ikkondividi", + "Share with Groups": "Ikkondividi ma' Gruppi", + "Share with Users": "Ikkondividi ma' Utenti", + "Show": "Uri", + "Show Filters": "Uri l-Filtri", + "Show less": "Uri inqas", + "Show only entries with changes": "Uri biss l-entrati bil-bidliet", + "Showing {count} dashboard(s)": "Qed juri {count} dashboard(s)", + "Showing {count} processing activities": "Qed juri {count} attivitajiet ta' pproċessar", + "Showing {showing} of {total} agents": "Qed juri {showing} minn {total} aġenti", + "Showing {showing} of {total} applications": "Qed juri {showing} minn {total} applikazzjonijiet", + "Showing {showing} of {total} audit trail entries": "Qed juri {showing} minn {total} entrati tat-traċċa tal-verifika", + "Showing {showing} of {total} configurations": "Qed juri {showing} minn {total} konfigurazzjonijiet", + "Showing {showing} of {total} deleted items": "Qed juri {showing} minn {total} oġġetti mħassra", + "Showing {showing} of {total} entities": "Qed juri {showing} minn {total} entitajiet", + "Showing {showing} of {total} files": "Qed juri {showing} minn {total} fajls", + "Showing {showing} of {total} log entries": "Qed juri {showing} minn {total} entrati tar-reġistru", + "Showing {showing} of {total} search trail entries": "Qed juri {showing} minn {total} entrati tat-traċċa tat-tiftix", + "Showing {showing} of {total} sources": "Qed juri {showing} minn {total} sorsi", + "Showing {showing} of {total} templates": "Qed juri {showing} minn {total} mudelli", + "Showing {showing} of {total} webhooks": "Qed juri {showing} minn {total} webhooks", + "Showing {shown} of {total} organisations": "Qed juri {shown} minn {total} organizzazzjonijiet", + "Simple": "Sempliċi", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Mistoqsijiet sempliċi: Tiftixiet bażiċi ta' test b'parametri minimi (eż. terminu ta' tiftix wieħed, l-ebda filtri avvanzati)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Mistoqsijiet sempliċi: tiftixiet bażiċi ta' test b'parametri minimi (eż. terminu ta' tiftix wieħed, l-ebda filtri avvanzati)", + "Size": "Daqs", + "Slug": "Slug", + "Soft Delete Mode": "Modalità ta' Tħassir Artab", + "Soft Deleted Items": "Oġġetti Mħassra b'Mod Artab", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Ħassar b'mod artab l-oġġetti kollha għal din l-iskema ({active} attivi, {deleted} diġà mħassra)", + "SolrCloud mode enabled": "Il-modalità SolrCloud ġiet attivata", + "Something went wrong while adding files": "Xi ħaġa marret ħażin waqt iż-żieda tal-fajls", + "Source": "Sors", + "Source successfully updated": "Is-sors ġie aġġornat b'suċċess", + "Sources": "Sorsi", + "Sources:": "Sorsi:", + "Special Rights": "Drittijiet Speċjali", + "Standalone SOLR mode": "Modalità SOLR awtonoma", + "Start Conversation": "Ibda Konversazzjoni", + "Start Date Field": "Qasam tad-Data tal-Bidu", + "Start Vectorization": "Ibda l-Vettorizzazzjoni", + "Start a conversation": "Ibda konversazzjoni", + "Starting names cache warmup...": "Qed jibda t-tisħin tal-cache tal-ismijiet...", + "Starting validation for {schema}...": "Qed tibda l-validazzjoni għal {schema}...", + "Starting {action} for {schema}...": "Qed jibda {action} għal {schema}...", + "Starting...": "Qed jibda...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika u Metriċi", + "Statistics refreshed": "L-istatistika ġiet aġġornata", + "Stats calculation not yet implemented": "Il-kalkolu tal-istatistika għadu mhux implimentat", + "Status": "Status", + "Status Code": "Kodiċi tal-Istatus", + "Step": "Pass", + "Storage": "Ħażna", + "Storage Quota (MB)": "Kwota tal-Ħażna (MB)", + "String Constraints": "Restrizzjonijiet tal-Istring", + "Subject identifier (email, BSN, name, etc.)": "Identifikatur tas-suġġett (email, BSN, isem, eċċ.)", + "Subscribe": "Abbona", + "Subscribed": "Abbonat", + "Success": "Suċċess", + "Success Rate": "Rata ta' Suċċess", + "Success Status": "Status ta' Suċċess", + "Successful": "B'suċċess", + "Successfully added user to organisation: {name}": "L-utent ġie miżjud b'suċċess mal-organizzazzjoni: {name}", + "Successfully cleared {count} audit trails": "{count} traċċi tal-verifika tneħħew b'suċċess", + "Successfully cleared {count} blob storage objects": "{count} oġġetti tal-ħażna blob tneħħew b'suċċess", + "Successfully cleared {count} search trails": "{count} traċċi tat-tiftix tneħħew b'suċċess", + "Successfully created {count} SOLR fields": "Inħolqu b'suċċess {count} oqsma SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Tħassru b'suċċess {count} embeddings. Jekk jogħġbok erġa' vvettorizza d-data tiegħek.", + "Successfully deleted {count} objects for {schema}": "Tħassru b'suċċess {count} oġġetti għal {schema}", + "Successfully fixed {count} SOLR field configurations": "Ġew irranġati b'suċċess {count} konfigurazzjonijiet tal-oqsma SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importati b'suċċess: {registers} reġistri, {schemas} skemi, {objects} oġġetti", + "Successfully joined organisation": "Ingħaqadt mal-organizzazzjoni b'suċċess", + "Successfully permanently deleted {count} objects": "Tħassru permanentement b'suċċess {count} oġġetti", + "Successfully processed {count} files!": "Ġew ipproċessati b'suċċess {count} fajls!", + "Successfully restored {count} object": "Ġie rrestawrat b'suċċess {count} oġġett", + "Successfully restored {count} objects": "Ġew irrestawrati b'suċċess {count} oġġetti", + "Successfully saved configuration for {count} facets!": "Il-konfigurazzjoni ġiet salvata b'suċċess għal {count} facets!", + "Supported File Types": "Tipi ta' Fajls Appoġġati", + "Switch Active Organisation": "Ibdel l-Organizzazzjoni Attiva", + "Switch Organisation": "Ibdel l-Organizzazzjoni", + "Sync Table": "Issinkronizza t-Tabella", + "Synchronization Settings": "Issettjar tas-Sinkronizzazzjoni", + "Syncing magic table for {schema}...": "Qed tissinkronizza l-magic table għal {schema}...", + "Syncs": "Sinkronizzazzjonijiet", + "System Default": "Awtomatiku tas-Sistema", + "System Prompt": "Prompt tas-Sistema", + "System announcements": "Avviżi tas-sistema", + "Table": "Tabella", + "Target ID": "ID fil-Mira", + "Target Type*": "Tip fil-Mira*", + "Technical Description": "Deskrizzjoni Teknika", + "Technical Details": "Dettalji Tekniċi", + "Technical description for developers and administrators": "Deskrizzjoni teknika għall-iżviluppaturi u l-amministraturi", + "Technical measures": "Miżuri tekniċi", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatura", + "Templates": "Mudelli", + "Test": "Test", + "Test Chat": "Ittestja l-Chat", + "Test Connection": "Ittestja l-Konnessjoni", + "Test Embedding": "Ittestja l-Embedding", + "Test endpoint executed successfully": "L-endpoint tat-test ġie esegwit b'suċċess", + "Test webhook delivered successfully": "Il-webhook tat-test ġie kkonsenjat b'suċċess", + "Test webhook delivery failed": "Il-konsenja tal-webhook tat-test falliet", + "Test webhook sent successfully": "Il-webhook tat-test intbagħat b'suċċess", + "Testing...": "Qed jittestja...", + "Text Chunk #{id}": "Biċċa ta' Test #{id}", + "Text Extraction": "Estrazzjoni tat-Test", + "Text Extraction Engine": "Magna tal-Estrazzjoni tat-Test", + "Text chunks": "Biċċiet ta' test", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Il-biċċiet tat-test jiġu ġġenerati waqt l-estrazzjoni tal-fajl u maħżuna fid-database. Il-vettorizzazzjoni taqra dawn il-fajls maqsuma minn qabel u tikkonvertihom f'embeddings.", + "Text extracted successfully": "It-test ġie estratt b'suċċess", + "Text extraction disabled": "L-estrazzjoni tat-test ġiet diżattivata", + "The Nextcloud Calendar app is not installed or enabled on this server.": "L-app Nextcloud Calendar mhijiex installata jew attivata fuq dan is-server.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "L-app Nextcloud Contacts mhijiex installata jew attivata fuq dan is-server.", + "The Nextcloud Deck app is not installed or enabled on this server.": "L-app Nextcloud Deck mhijiex installata jew attivata fuq dan is-server.", + "The Nextcloud Mail app is not installed or enabled on this server.": "L-app Nextcloud Mail mhijiex installata jew attivata fuq dan is-server.", + "The URL where webhook events will be sent": "L-URL fejn jintbagħtu l-avvenimenti tal-webhook", + "The requested conversation does not exist": "Il-konversazzjoni mitluba ma teżistix", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Id-dashboard mitlub ma setax jiġi mtella'. Ivverifika l-URL jew agħżel dashboard ieħor mil-lista.", + "The saved view and all its search configuration": "Il-veduta salvata u l-konfigurazzjoni kollha tat-tiftix tagħha", + "There are no audit trail entries matching your current filters.": "M'hemm l-ebda entrati tat-traċċa tal-verifika li jaqblu mal-filtri attwali tiegħek.", + "There are no deleted items matching your current filters.": "M'hemm l-ebda oġġetti mħassra li jaqblu mal-filtri attwali tiegħek.", + "There are no search trail entries matching your current filters.": "M'hemm l-ebda entrati tat-traċċa tat-tiftix li jaqblu mal-filtri attwali tiegħek.", + "There are no webhook log entries matching your filters.": "M'hemm l-ebda entrati tar-reġistru tal-webhook li jaqblu mal-filtri tiegħek.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Dawn se jinżammu skont il-perjodu ta' żamma kkonfigurat tal-iskema tagħhom u jitħassru permanentement b'mod awtomatiku meta jiskadi l-perjodu ta' żamma. Il-perjodu ta' żamma jista' jiġi kkonfigurat għal kull skema u jinsab fl-issettjar tal-iskema.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Dawn se jinżammu skont il-perjodu ta' żamma kkonfigurat tal-iskema tagħhom u mbagħad jitneħħew permanentement.", + "This action cannot be undone.": "Din l-azzjoni ma tistax titreġġa' lura.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Din l-azzjoni ma tistax titreġġa' lura. Kun żgur li l-ebda ġabra mhi qed tuża dan il-ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Din l-azzjoni ma tistax titreġġa' lura. Kun żgur li l-ebda ġabra mhi qed tuża dan il-configset.", + "This action will submit a deactivation request to your administrators.": "Din l-azzjoni se tibgħat talba ta' diżattivazzjoni lill-amministraturi tiegħek.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Dan l-aġent m'għandux veduti jew għodod konfigurabbli, iżda xorta tista' taġġusta l-issettjar RAG hawn taħt.", + "This analysis may take some time": "Din l-analiżi tista' tieħu xi ħin", + "This audit trail entry does not contain any change information.": "Din l-entrata tat-traċċa tal-verifika ma fihiex informazzjoni dwar bidliet.", + "This configuration has no data defined.": "Din il-konfigurazzjoni m'għandha l-ebda data definita.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Dan l-endpoint huwa skadut. Uża endpoints ibbażati fuq biċċiet minflok.", + "This entity has no relations to objects or files": "Din l-entità m'għandha l-ebda relazzjonijiet ma' oġġetti jew fajls", + "This is a test webhook from OpenRegister": "Dan huwa webhook ta' test minn OpenRegister", + "This is not valid JSON": "Dan mhuwiex JSON validu", + "This key already exists on this schema": "Din iċ-ċavetta diġà teżisti fuq din l-iskema", + "This schema must use Magic Table configuration to sync": "Din l-iskema trid tuża l-konfigurazzjoni Magic Table biex tissinkronizza", + "This schema must use magic table configuration to sync": "Din l-iskema trid tuża l-konfigurazzjoni magic table biex tissinkronizza", + "This source has no associated registers.": "Dan is-sors m'għandux reġistri assoċjati.", + "This token will only be shown once. Copy it now.": "Dan it-token se jintwera darba biss. Ikkupjah issa.", + "This will copy the _default ConfigSet with the new name": "Dan se jikkopja l-ConfigSet _default bl-isem il-ġdid", + "This will copy the _default configset with the new name": "Dan se jikkopja l-configset _default bl-isem il-ġdid", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Dan se jħassar permanentement l-embeddings (vetturi) KOLLHA mid-database. Se jkollok bżonn terġa' tivvettorizza l-oġġetti u l-fajls kollha. Din l-azzjoni ma tistax titreġġa' lura.\n\nInti ċert li trid tkompli?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Dan se jħassar permanentement l-embeddings (vetturi) KOLLHA mid-database. Se jkollok bżonn terġa' tivvettorizza l-oġġetti u l-fajls kollha. Din l-azzjoni ma tistax titreġġa' lura.\\n\\nInti ċert li trid tkompli?", + "This will permanently delete:": "Dan se jħassar permanentement:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Dan se jneħħi permanentement {count} oġġetti diġà mħassra b'mod artab mid-database. Din l-azzjoni ma tistax titreġġa' lura.", + "Timeout (seconds)": "Timeout (sekondi)", + "Timestamp": "Timestamp", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Parir: Attiva biss il-veduti li jeħtieġu tiftix semantiku biex timminimizza l-ispejjeż tal-embedding. It-tabelli sempliċi ta' tiftix rari jeħtieġu vettorizzazzjoni.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Parir: attiva biss il-veduti li jeħtieġu tiftix semantiku biex timminimizza l-ispejjeż tal-embedding. It-tabelli sempliċi ta' tiftix rari jeħtieġu vettorizzazzjoni.", + "Title": "Titlu", + "Title *": "Titlu *", + "Title Template": "Mudell tat-Titlu", + "Title*": "Titlu*", + "To (e.g. -1 year or 2025-12-31)": "Sa (eż. -1 sena jew 2025-12-31)", + "To Date": "Sad-Data", + "To add files larger than or equal to 512MB, go to the": "Biex iżżid fajls akbar minn jew ugwali għal 512MB, mur fil-", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Biex taġġusta d-daqs jew l-istrateġija tal-biċċa, mur fil-Konfigurazzjoni tal-Fajl → Limiti tal-Ipproċessar.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Biex taġġusta d-daqs jew l-istrateġija tal-biċċa, mur fil-konfigurazzjoni tal-fajl → limiti tal-ipproċessar.", + "To change the embedding provider or model, go to LLM Configuration.": "Biex tibdel il-fornitur jew il-mudell tal-embedding, mur fil-Konfigurazzjoni LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Biex tibdel il-fornitur jew il-mudell tal-embedding, mur fil-konfigurazzjoni LLM.", + "To date": "Sad-data", + "Toggle search sidebar": "Ibdel il-barra tal-ġenb tat-tiftix", + "Token Created": "It-Token Inħoloq", + "Token Quota (per request)": "Kwota tat-Token (kull talba)", + "Token copied to clipboard": "It-token ġie kkupjat fil-clipboard", + "Token name": "Isem tat-token", + "Token revoked": "It-token ġie revokat", + "Tools": "Għodod", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "L-għodod jiġu esegwiti bil-permessi awtomatiċi tal-utent tal-aġent meta l-ebda sessjoni tal-utent ma tkun attiva (eż. cron jobs). Ikkonfigura l-utent awtomatiku fit-tab tal-Issettjar.", + "Tools respect the agent's own role and organization boundaries.": "L-għodod jirrispettaw ir-rwol stess tal-aġent u l-limiti tal-organizzazzjoni.", + "Tools respect the agent's views, permissions, and organization boundaries.": "L-għodod jirrispettaw il-veduti, il-permessi, u l-limiti tal-organizzazzjoni tal-aġent.", + "Top Deleters": "L-Ogħla Tħassara", + "Total": "Total", + "Total Audit Trails": "Total tat-Traċċi tal-Verifika", + "Total Chunks Available:": "Total tal-Biċċiet Disponibbli:", + "Total Deleted Items": "Total tal-Oġġetti Mħassra", + "Total Members": "Total tal-Membri", + "Total Objects": "Total tal-Oġġetti", + "Total Objects in Database:": "Total tal-Oġġetti fid-Database:", + "Total Organisations": "Total tal-Organizzazzjonijiet", + "Total Results": "Total tar-Riżultati", + "Total Searches": "Total ta' Tfittxijiet", + "Total Size": "Daqs Totali", + "Total Storage": "Ħażna Totali", + "Total events": "Total ta' avvenimenti", + "Total:": "Total:", + "Totals": "Totali", + "Try searching by UUID or with different keywords": "Ipprova fittex bl-UUID jew bi kliem ċavetta differenti", + "Type": "Tip", + "Type \"DELETE COLLECTION\" to confirm:": "Ittajpja \"DELETE COLLECTION\" biex tikkonferma:", + "Type Issue": "Problema tat-Tip", + "Type Variations:": "Varjazzjonijiet tat-Tip:", + "Type filter (optional, e.g. email)": "Filtru tat-tip (mhux obbligatorju, eż. email)", + "Type must be \"positive\" or \"negative\"": "It-tip irid ikun \"positive\" jew \"negative\"", + "Type to search for groups": "Ittajpja biex tfittex gruppi", + "Type to search for objects...": "Ittajpja biex tfittex oġġetti...", + "Type to search for organisations": "Ittajpja biex tfittex organizzazzjonijiet", + "Type to search for users": "Ittajpja biex tfittex utenti", + "Type to search groups...": "Ittajpja biex tfittex gruppi...", + "Type to search users...": "Ittajpja biex tfittex utenti...", + "Type to search...": "Ittajpja biex tfittex...", + "Type your username to confirm": "Ittajpja l-isem tal-utent tiegħek biex tikkonferma", + "URL": "URL", + "URL where Ollama is running": "URL fejn qiegħed jaħdem Ollama", + "UUID copied to clipboard": "L-UUID ġie kkupjat fil-clipboard", + "UUID:": "UUID:", + "Unchanged": "Mhux mibdul", + "Unique Search Terms": "Termini ta' Tfittxija Uniċi", + "Unique Terms": "Termini Uniċi", + "Unknown": "Mhux magħruf", + "Unknown Object": "Oġġett Mhux Magħruf", + "Unknown Register": "Reġistru Mhux Magħruf", + "Unknown Schema": "Skema Mhux Magħrufa", + "Unknown error": "Żball mhux magħruf", + "Unknown widget type: {type}": "Tip ta' widget mhux magħruf: {type}", + "Unlink email": "Aqla' r-rabta tal-email", + "Unlink event": "Aqla' r-rabta tal-avveniment", + "Unnamed Object": "Oġġett Mingħajr Isem", + "Untitled": "Mingħajr Titlu", + "Untitled View": "Veduta Mingħajr Titlu", + "Update": "Aġġorna", + "Update Operations": "Operazzjonijiet ta' Aġġornament", + "Update available: {local} → {remote}": "Aġġornament disponibbli: {local} → {remote}", + "Update configuration: ...": "Aġġorna l-konfigurazzjoni: ...", + "Update register OAS: ...": "Aġġorna l-OAS tar-Reġistru: ...", + "Update vectors when object data changes (recommended for accurate search)": "Aġġorna l-vetturi meta tinbidel id-data tal-oġġett (rakkomandat għal tfittxija preċiża)", + "Updated": "Aġġornat", + "Updated At": "Aġġornat Fi", + "Updated:": "Aġġornat:", + "Upload new avatar": "Tella' avatar ġdid", + "Uploading configuration sets...": "Qed jittellgħu s-settijiet ta' konfigurazzjoni...", + "Uptime:": "Ħin imqabbad:", + "Url": "Url", + "Usage Count": "Għadd ta' Użu", + "Use AI agents for processing and analysis": "Uża aġenti tal-AI għall-ipproċessar u l-analiżi", + "Use Large Language Model features": "Uża l-funzjonijiet tal-Large Language Model", + "Use SolrCloud with Zookeeper for distributed search": "Uża SolrCloud ma' Zookeeper għal tfittxija distribwita", + "Use SolrCloud with zookeeper for distributed search": "Uża SolrCloud ma' zookeeper għal tfittxija distribwita", + "Use VNG APIs instead of MongoDB.": "Uża l-APIs tal-VNG minflok MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Uża l-firxiet awtomatiċi (L-aħħar 7/30/90 jum, Is-sena li għaddiet, Eqdem)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Uża ħażna esterna (eż. MongoDB) minflok il-ħażna interna ta' Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Uża l-filtri biex tnaqqas l-entrati tar-rendikont tal-verifika skont ir-reġistru, l-iskema, it-tip ta' azzjoni, l-utent, il-firxa tad-dati, jew l-ID tal-oġġett.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Uża l-filtri biex tnaqqas l-oġġetti mħassra skont ir-reġistru, l-iskema, id-data tat-tħassir, jew l-utent li ħassarhom.", + "Use filters to narrow down entities by type or category.": "Uża l-filtri biex tnaqqas l-entitajiet skont it-tip jew il-kategorija.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Uża l-filtri biex tnaqqas l-entrati tar-rendikont tat-tfittxija skont ir-reġistru, l-iskema, l-istat ta' suċċess, l-utent, il-firxa tad-dati, it-termini tat-tfittxija, jew il-metriċi tal-prestazzjoni.", + "Used By Collections": "Użat Minn Kollezzjonijiet", + "Used by": "Użat minn", + "User": "Utent", + "User Agent Statistics": "Statistika tal-User Agent", + "User Description": "Deskrizzjoni tal-Utent", + "User Quota": "Kwota tal-Utent", + "User rate limits cleared successfully": "Il-limiti tar-rata tal-utent tneħħew b'suċċess", + "User removed successfully": "L-utent tneħħa b'suċċess", + "User-friendly description shown in forms and help text": "Deskrizzjoni faċli għall-utent murija fil-formoli u fit-test tal-għajnuna", + "Username": "Isem tal-utent", + "Username for SOLR authentication (optional)": "Isem tal-utent għall-awtentikazzjoni SOLR (mhux obbligatorju)", + "Users": "Utenti", + "Uses": "Użi", + "Using Pre-Generated Chunks": "Qed jintużaw Chunks Iġġenerati Minn Qabel", + "Validate": "Ivvalida", + "Validate Objects": "Ivvalida l-Oġġetti", + "Validation completed for {schema}": "Il-validazzjoni tlestiet għal {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Il-validazzjoni tlestiet għal {schema}: {processed} ipproċessati, {updated} aġġornati, {failed} fallew", + "Validation completed successfully": "Il-validazzjoni tlestiet b'suċċess", + "Validation failed": "Il-validazzjoni falliet", + "Value": "Valur", + "Value list (split on ,)": "Lista ta' valuri (maqsuma fuq ,)", + "Vector Dimensions": "Dimensjonijiet tal-Vetturi", + "Vector Search Backend": "Backend tat-Tfittxija bil-Vetturi", + "Vector field: _embedding_": "Qasam tal-vettur: _embedding_", + "Vectorization Triggers": "Skattaturi tal-Vettorizzazzjoni", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Il-vettorizzazzjoni tlestiet: {vectorized} oġġetti vettorizzati, {failed} fallew", + "Vectorization failed: {error}": "Il-vettorizzazzjoni falliet: {error}", + "Vectorize All Files": "Ivvettorizza l-Fajls Kollha", + "Vectorize All Objects": "Ivvettorizza l-Oġġetti Kollha", + "Vectorize all file types": "Ivvettorizza t-tipi kollha ta' fajls", + "Vectorize all views": "Ivvettorizza l-veduti kollha", + "Vectorize on object creation": "Ivvettorizza mal-ħolqien tal-oġġett", + "Vectors will be stored in your existing object and file collections": "Il-vetturi se jinħażnu fil-kollezzjonijiet eżistenti tal-oġġetti u tal-fajls tiegħek", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Qed tiġi vverifikata l-konnettività tas-server...", + "Version": "Verżjoni", + "Version Information": "Informazzjoni dwar il-Verżjoni", + "Very High": "Għoli Ħafna", + "Very high": "Għoli ħafna", + "View": "Veduta", + "View API Docs": "Ara d-Dokumenti tal-API", + "View API Documentation": "Ara d-Dokumentazzjoni tal-API", + "View APIs": "Ara l-APIs", + "View Changes": "Ara l-Bidliet", + "View Details": "Ara d-Dettalji", + "View Error": "Ara l-Iżball", + "View File": "Ara l-Fajl", + "View Full Details": "Ara d-Dettalji Sħaħ", + "View Logs": "Ara r-Reġistri", + "View Name": "Isem tal-Veduta", + "View Object": "Ara l-Oġġett", + "View Parameters": "Ara l-Parametri", + "View Selection": "Għażla tal-Veduta", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Ara u analizza r-reġistri tar-rendikont tat-tfittxija b'kapaċitajiet avvanzati ta' filtraġġ u analitika", + "View and analyze system audit trails with advanced filtering capabilities": "Ara u analizza r-rendikonti tal-verifika tas-sistema b'kapaċitajiet avvanzati ta' filtraġġ", + "View and manage authorization across registers and schemas": "Ara u amministra l-awtorizzazzjoni fir-reġistri u l-iskemi", + "View deleted successfully": "Il-veduta tħassret b'suċċess", + "View entity details and manage relations": "Ara d-dettalji tal-entità u amministra r-relazzjonijiet", + "View name": "Isem tal-veduta", + "View name is required": "L-isem tal-veduta huwa meħtieġ", + "View object": "Ara l-oġġett", + "View saved successfully!": "Il-veduta ġiet salvata b'suċċess!", + "View search analytics and manage search logs": "Ara l-analitika tat-tfittxija u amministra r-reġistri tat-tfittxija", + "View successfully deleted": "Il-veduta tħassret b'suċċess", + "View successfully updated": "Il-veduta ġiet aġġornata b'suċċess", + "View updated successfully!": "Il-veduta ġiet aġġornata b'suċċess!", + "View webhook delivery logs and filter by webhook": "Ara r-reġistri tal-konsenja tal-webhook u ffiltra skont il-webhook", + "View {count} more": "Ara {count} oħra", + "Views": "Veduti", + "Visible to end users": "Viżibbli għall-utenti finali", + "Visible to users": "Viżibbli għall-utenti", + "Wait for Response": "Stenna għar-Rispons", + "Wait for webhook response before continuing (required for request/response flows)": "Stenna r-rispons tal-webhook qabel ma tkompli (meħtieġ għall-flussi ta' talba/rispons)", + "Waiting for cluster synchronization...": "Qed jistennew is-sinkronizzazzjoni tal-cluster...", + "Warmup Names Cache": "Issaħħan il-Cache tal-Ismijiet", + "Warmup failed": "It-tisħin falla", + "Warning": "Twissija", + "Warning:": "Twissija:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Twissija: Din il-proprjetà għandha tipi inkonsistenti: {types}. Ikkunsidra jekk l-oġġetti kollha għandhomx ikollhom l-istess tip.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Aħna se niskennjaw l-oġġetti kollha li jappartjenu lil din l-iskema biex niskopru proprjetajiet ġodda u nanalizzaw il-proprjetajiet eżistenti għal titjib potenzjali. Il-proċess jinvolvi l-eżami tal-istruttura tad-data ta' kull oġġett, l-identifikazzjoni ta' proprjetajiet mhux iddefiniti fl-iskema attwali, u s-sejbien ta' opportunitajiet biex itejbu d-definizzjonijiet eżistenti tal-proprjetajiet b'restrizzjonijiet, formati u regoli ta' validazzjoni aħjar.", + "Webhook": "Webhook", + "Webhook Log Details": "Dettalji tar-Reġistru tal-Webhook", + "Webhook Logs": "Reġistri tal-Webhook", + "Webhook created successfully": "Il-webhook inħoloq b'suċċess", + "Webhook deleted": "Il-webhook tħassar", + "Webhook retry delivered successfully": "Il-konsenja mill-ġdid tal-webhook saret b'suċċess", + "Webhook retry delivery failed": "Il-konsenja mill-ġdid tal-webhook falliet", + "Webhook updated": "Il-webhook ġie aġġornat", + "Webhook updated successfully": "Il-webhook ġie aġġornat b'suċċess", + "Webhooks": "Webhooks", + "Weekly": "Kull ġimgħa", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Meta l-aġent jaħdem mingħajr sessjoni ta' utent (eż. kompiti skedati), se jintuża l-kuntest ta' dan l-utent", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Meta l-aġent jaħdem mingħajr sessjoni ta' utent (eż. kompiti skedati), se jintuża l-kuntest ta' dan l-utent.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Meta jkun diżattivat, dan il-faċett se juri biss valuri minn din l-iskema u se jinkludi filtru tal-iskema meta jintgħażel.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Meta jkun attivat, is-salvataġġ ta' dan l-oġġett se jaġġorna wkoll l-oġġetti fil-mira biex jinkludu referenza lura għal dan l-oġġett.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Meta jkun attivat, din il-proprjetà se titneħħa mill-oġġett tas-sors wara li jiġu aġġornati l-oġġetti fil-mira.", + "Workflow Management": "Ġestjoni tal-Workflow", + "Workflows": "Workflows", + "Wrap webhook payload in CloudEvents format for better interoperability": "Iġbor il-payload tal-webhook fil-format CloudEvents għal interoperabbiltà aħjar", + "Wrap webhook payload in cloudevents format for better interoperability": "Iġbor il-payload tal-webhook fil-format cloudevents għal interoperabbiltà aħjar", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Iva", + "You": "Int", + "You are not yet a member of any organisations.": "Int għadek mhux membru ta' ebda organizzazzjoni.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Tista' toħloq ConfigSets ġodda bbażati fuq il-mudell _default, jew ittella' oħrajn personalizzati direttament fis-server SOLR tiegħek.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Tista' toħloq configsets ġodda bbażati fuq il-mudell _default, jew ittella' oħrajn personalizzati direttament fis-server SOLR tiegħek.", + "You do not have access to this conversation": "Int m'għandekx aċċess għal din il-konverżazzjoni", + "You do not have permission to delete this conversation": "Int m'għandekx permess biex tħassar din il-konverżazzjoni", + "You do not have permission to modify this conversation": "Int m'għandekx permess biex timmodifika din il-konverżazzjoni", + "You do not have permission to restore this conversation": "Int m'għandekx permess biex tirrestawra din il-konverżazzjoni", + "You do not have permission to view the permission matrix. Admin access is required.": "Int m'għandekx permess biex tara l-matriċi tal-permessi. Huwa meħtieġ aċċess ta' amministratur.", + "You have selected {count} properties to add to the schema.": "Int għażilt {count} proprjetajiet biex iżżid mal-iskema.", + "You must be logged in to favorite views": "Trid tkun illoggjat biex tagħmel veduti favoriti", + "You need an AI agent to start a conversation.": "Għandek bżonn aġent tal-AI biex tibda konverżazzjoni.", + "Your Fireworks AI API key. Get one at": "Iċ-ċavetta tal-API ta' Fireworks AI tiegħek. Ikseb waħda fuq", + "Your OpenAI API key. Get one at": "Iċ-ċavetta tal-API ta' OpenAI tiegħek. Ikseb waħda fuq", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Il-feedback tiegħek ġie rreġistrat. B'mod fakultattiv, tista' tipprovdi dettalji addizzjonali hawn...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Il-feedback tiegħek ġie rreġistrat. b'mod fakultattiv, tista' tipprovdi dettalji addizzjonali hawn...", + "Zookeeper Hosts": "Hosts ta' Zookeeper", + "Zookeeper Port": "Port ta' Zookeeper", + "Zookeeper Settings (SolrCloud)": "Settings ta' Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "String tal-konnessjoni ta' Zookeeper għal SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Numru tal-port ta' Zookeeper (mhux obbligatorju, l-istandard huwa 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F TWISSIJA TA' TĦASSIR PERMANENTI \\u26A0\\uFE0F\n\nInti wasalt biex tħassar B'MOD PERMANENTI l-oġġetti KOLLHA għall-iskema \"{schema}\":\n\n\\u2022 Oġġetti attivi: {active}\n\\u2022 Oġġetti mħassra b'mod artab: {deleted}\n\\u2022 Total: {total}\n\nDawn l-oġġetti se jitneħħew kompletament mid-database u MA JISTGĦUX jiġu rkuprati.\n\nInti assolutament ċert?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F TWISSIJA TA' TĦASSIR PERMANENTI \\u26A0\\uFE0F\n\nInti wasalt biex tħassar B'MOD PERMANENTI {count} oġġetti mħassra b'mod artab għall-iskema \"{schema}\".\n\nDawn l-oġġetti se jitneħħew kompletament mid-database u MA JISTGĦUX jiġu rkuprati.\n\nInti assolutament ċert?", + "and accessible to all users in selected groups (or all users if no groups selected).": "u aċċessibbli għall-utenti kollha fil-gruppi magħżula (jew l-utenti kollha jekk ma jintgħażel ebda grupp).", + "and add the files there.": "u żid il-fajls hemmhekk.", + "approved": "approvat", + "are only accessible to invited users.": "huma aċċessibbli biss għall-utenti mistiedna.", + "by": "minn", + "chunk": "chunk", + "chunks": "chunks", + "chunks processed": "chunks ipproċessati", + "completed": "tlesta", + "configuration(s)": "konfigurazzjoni(jiet)", + "conversationId is required": "conversationId huwa meħtieġ", + "create": "oħloq", + "delete": "ħassar", + "deleted objects section": "sezzjoni tal-oġġetti mħassra", + "e.g., lib/Settings/config.json": "eż., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "eż., lib/Settings/register.json", + "e.g., lib/settings/config.json": "eż., lib/settings/config.json", + "e.g., lib/settings/register.json": "eż., lib/settings/register.json", + "execute": "esegwi", + "failed": "falla", + "file": "fajl", + "files": "fajls", + "file{plural}": "fajl{plural}", + "folder": "kartella", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "fi {register}", + "in_progress": "għaddej", + "inherited": "miret", + "list": "lista", + "log{plural}": "log{plural}", + "manage": "amministra", + "members": "membri", + "menu or contact someone with permission to create agents.": "menu jew ikkuntattja lil xi ħadd b'permess biex joħloq aġenti.", + "message content is required": "il-kontenut tal-messaġġ huwa meħtieġ", + "messages": "messaġġi", + "missing": "nieqes", + "myapp": "myapp", + "n8n URL and API key are required": "L-URL u ċ-ċavetta tal-API ta' n8n huma meħtieġa", + "n8n configuration saved successfully": "Il-konfigurazzjoni ta' n8n ġiet salvata b'suċċess", + "n8n connection not configured": "Il-konnessjoni ta' n8n mhux ikkonfigurata", + "n8n connection successful": "Il-konnessjoni ta' n8n irnexxiet", + "n8n connection test failed: {message}": "It-test tal-konnessjoni ta' n8n falla: {message}", + "n8n connection test successful": "It-test tal-konnessjoni ta' n8n irnexxa", + "n8n initialization failed: {message}": "L-inizjalizzazzjoni ta' n8n falliet: {message}", + "n8n integration disabled": "L-integrazzjoni ta' n8n ġiet diżattivata", + "n8n integration enabled": "L-integrazzjoni ta' n8n ġiet attivata", + "n8n project initialized successfully": "Il-proġett ta' n8n ġie inizjalizzat b'suċċess", + "n8n settings saved successfully": "Is-settings ta' n8n ġew salvati b'suċċess", + "notify_push configuration guide": "gwida għall-konfigurazzjoni ta' notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push huwa installat iżda OpenRegister għadu ma kkonfermax push b'suċċess. Iskatta salvataġġ ta' oġġett biex tattiva, jew iċċekkja l-konfigurazzjoni ta' notify_push tiegħek.", + "notify_push is installed but not yet active": "notify_push huwa installat iżda għadu mhux attiv", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "oġġetti", + "objects processed": "oġġetti pproċessati", + "object{plural}": "oġġett{plural}", + "or pick a range": "jew agħżel firxa", + "org-...": "org-...", + "pending": "pendenti", + "provides a good balance between speed and accuracy.": "jipprovdi bilanċ tajjeb bejn il-veloċità u l-preċiżjoni.", + "public": "pubbliku", + "read": "aqra", + "register(s)": "reġistru(i)", + "register(s) selected": "reġistru(i) magħżula", + "register{plural}": "reġistru{plural}", + "rejected": "miċħud", + "required": "meħtieġ", + "results": "riżultati", + "schema(s)": "skema/i", + "schema(s) selected": "skema/i magħżula", + "schema{plural}": "skema{plural}", + "selected objects": "oġġetti magħżula", + "set": "issettja", + "sk-...": "sk-...", + "this application": "din l-applikazzjoni", + "to": "sa", + "type must be \"positive\" or \"negative\"": "it-tip irid ikun \"positive\" jew \"negative\"", + "update": "aġġorna", + "vectorized": "vettorizzat", + "view": "ara", + "views selected": "veduti magħżula", + "waiting": "qed jistenna", + "{active} active, {deleted} deleted": "{active} attivi, {deleted} imħassra", + "{count} audit trails cleared successfully": "{count} rendikonti ta' verifika tneħħew b'suċċess", + "{count} compliance issue(s) detected": "{count} problema/i ta' konformità misjuba", + "{count} deletions": "{count} tħassiriet", + "{count} documents found": "{count} dokumenti misjuba", + "{count} email": "{count} email", + "{count} emails": "{count} emails", + "{count} entries": "{count} entrati", + "{count} issues": "{count} problemi", + "{count} members": "{count} membri", + "{count} searches": "{count} tfittxijiet", + "{count} selected": "{count} magħżula", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} oġġett(i) qablu. {erased} tħassru.", + "{minutes} minutes ago": "{minutes} minuti ilu", + "{percentage}% of objects": "{percentage}% tal-oġġetti", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} fi {register} / {schema}", + "{type} cache cleared successfully": "Il-cache ta' {type} tneħħa b'suċċess", + "ℹ️ Current Configuration": "ℹ️ Konfigurazzjoni Attwali", + "ℹ️ current configuration": "ℹ️ konfigurazzjoni attwali", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ PERIKLU: L-oġġetti kollha {total} se jitħassru B'MOD PERMANENTI mid-database. Din l-azzjoni MA TISTAX TIĠI RKUPRATA u ma tistax titreġġa' lura!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ TWISSIJA TA' TĦASSIR PERMANENTI ⚠️\n\nInti wasalt biex tħassar B'MOD PERMANENTI l-oġġetti KOLLHA għall-iskema \"{schema}\":\n\n• Oġġetti attivi: {active}\n• Oġġetti mħassra b'mod artab: {deleted}\n• Total: {total}\n\nDawn l-oġġetti se jitneħħew kompletament mid-database u MA JISTGĦUX jiġu rkuprati.\n\nInti assolutament ċert?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ TWISSIJA TA' TĦASSIR PERMANENTI ⚠️\n\nInti wasalt biex tħassar B'MOD PERMANENTI {count} oġġetti mħassra b'mod artab għall-iskema \"{schema}\".\n\nDawn l-oġġetti se jitneħħew kompletament mid-database u MA JISTGĦUX jiġu rkuprati.\n\nInti assolutament ċert?", + "⚡ Batch Processing": "⚡ Ipproċessar f'Lottijiet", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR jista' jimmaniġġja miljuni ta' dokumenti b'ħinijiet ta' rispons għat-tfittxija ta' inqas minn sekonda...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Kuntest komprensiv ✓ Inqas probabbli li jitlef dettalji ✗ Risponsi aktar bil-mod ✗ Jista' jinkludi informazzjoni inqas rilevanti", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Risponsi aktar mgħaġġla ✓ Tweġibiet aktar iffukati ✗ Jista' jitlef informazzjoni rilevanti", + "✨ AI Features": "✨ Funzjonijiet tal-AI", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Fil-mod distribwit, SOLR juża ZooKeeper għall-koordinazzjoni tal-cluster...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 It-tfittxija b'faċetti tippermetti lill-utenti jniżżlu fil-fond fir-riżultati skont il-kategoriji...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Kont taf? SOLR jħaddem it-tfittxija għal Netflix, Apple, u ħafna siti ewlenin oħra!", + "💰 View Selection (Cost Optimization)": "💰 Għażla tal-Veduta (Ottimizzazzjoni tal-Ispiża)", + "📄 Object Serialization": "📄 Serjalizzazzjoni tal-Oġġetti", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR jipprovdi analitika u statistika rikka dwar il-prestazzjoni tat-tfittxija...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Il-ConfigSets fihom l-iskema u l-fajls ta' konfigurazzjoni għall-indiċi tat-tfittxija tiegħek...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Ir-replikazzjoni tiżgura li l-indiċi tat-tfittxija tiegħek ikun disponibbli anke jekk in-nodi jfallu...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR huwa pjattaforma ta' tfittxija qawwija għall-intrapriżi mibnija fuq Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Settings tal-Vettorizzazzjoni", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 L-iskalar awtomatiku jista' jżid jew ineħħi nodi b'mod dinamiku skont it-tagħbija...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Il-funzjonijiet tas-sigurtà jinkludu awtentikazzjoni, awtorizzazzjoni, u kriptaġġ SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} email", + "{count} emails" + ], + "Purge {count} object from database": [ + "Neħħi {count} oġġett mid-database", + "Neħħi {count} oġġetti mid-database" + ], + "Restore {count} object": [ + "Irrestawra {count} oġġett", + "Irrestawra {count} oġġetti" + ], + "Successfully restored {count} object": [ + "{count} oġġett ġie rrestawrat b'suċċess", + "{count} oġġetti ġew irrestawrati b'suċċess" + ], + "Delete {count} object": [ + "Ħassar {count} oġġett", + "Ħassar {count} oġġetti" + ], + "Object successfully deleted": [ + "L-oġġett tħassar b'suċċess", + "L-oġġetti tħassru b'suċċess" + ] + } +} diff --git a/l10n/nb.js b/l10n/nb.js new file mode 100644 index 0000000000..8874c175ec --- /dev/null +++ b/l10n/nb.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(uten emne)", + "(no title)" : "(uten tittel)", + "(unnamed)" : "(uten navn)", + "(untitled card)" : "(kort uten tittel)", + "Activities" : "Aktiviteter", + "Add Endpoint" : "Legg til endepunkt", + "Add Source" : "Legg til kilde", + "Add card" : "Legg til kort", + "Add contact" : "Legg til kontakt", + "All clear" : "Alt i orden", + "Articles" : "Artikler", + "Code" : "Kode", + "Compliance" : "Samsvar", + "Contacts" : "Kontakter", + "Create Agent" : "Opprett agent", + "Create Application" : "Opprett applikasjon", + "Create event" : "Opprett hendelse", + "Erasure complete" : "Sletting fullført", + "Erasure preview" : "Forhåndsvisning av sletting", + "Expand details" : "Utvid detaljer", + "Save changes" : "Lagre endringer", + "📄 Object Serialization" : "📄 Objektserialisering", + "🔢 Vectorization Settings" : "🔢 Vektoriseringsinnstillinger", + "💰 View Selection (Cost Optimization)" : "💰 Visningsvalg (kostnadsoptimalisering)", + "AI Assistant" : "KI-assistent", + "AI Chat" : "KI-chat", + "API Key" : "API-nøkkel", + "API tokens saved successfully" : "API-tokenene ble lagret", + "About ConfigSets" : "Om ConfigSets", + "Action" : "Handling", + "Action Distribution" : "Handlingsfordeling", + "Action:" : "Handling:", + "Actions" : "Handlinger", + "Active" : "Aktiv", + "Active Collections" : "Aktive samlinger", + "Active Filters" : "Aktive filtre", + "Active Organisations" : "Aktive organisasjoner", + "Active filters:" : "Aktive filtre:", + "Active organisation set successfully" : "Aktiv organisasjon ble angitt", + "Activity Period" : "Aktivitetsperiode", + "Add Groups" : "Legg til grupper", + "Add Schema" : "Legg til skjema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Legg til skjematitler, beskrivelser og registerinformasjon for å gi rikere kontekst for søk", + "Add to favorites" : "Legg til i favoritter", + "Added" : "Lagt til", + "Added to favorites" : "Lagt til i favoritter", + "Additional Information" : "Tilleggsinformasjon", + "Additional feedback saved. Thank you!" : "Ytterligere tilbakemelding lagret. Takk!", + "Admin" : "Administrator", + "Advanced" : "Avansert", + "Advanced Filters" : "Avanserte filtre", + "Advanced Options" : "Avanserte alternativer", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Avanserte filtre er ikke tilgjengelige ved bruk av databasekilde. Bytt til Auto eller SOLR-indeks for filtreringsalternativer.", + "After" : "Etter", + "Agent deleted successfully" : "Agenten ble slettet", + "Agents" : "Agenter", + "All" : "Alle", + "All Categories" : "Alle kategorier", + "All Collections" : "Alle samlinger", + "All Confidence Levels" : "Alle konfidensnivåer", + "All ConfigSets" : "Alle ConfigSets", + "All Files" : "Alle filer", + "All Levels" : "Alle nivåer", + "All Types" : "Alle typer", + "All Webhooks" : "Alle webhooks", + "All actions" : "Alle handlinger", + "All audit trails cleared successfully" : "Alle revisjonsspor ble tømt", + "All registers" : "Alle registre", + "All schemas" : "Alle skjemaer", + "All searches" : "Alle søk", + "All users" : "Alle brukere", + "An error occurred while clearing audit trails" : "Det oppstod en feil under tømming av revisjonsspor", + "An error occurred while deleting the audit trail" : "Det oppstod en feil under sletting av revisjonssporet", + "An error occurred while deleting the view" : "Det oppstod en feil under sletting av visningen", + "An error occurred while permanently deleting the objects" : "Det oppstod en feil under permanent sletting av objektene", + "Analysis completed:" : "Analyse fullført:", + "Analysis steps:" : "Analysetrinn:", + "Analytics" : "Analyse", + "Analyze Objects" : "Analyser objekter", + "Analyze Properties" : "Analyser egenskaper", + "Analyze existing properties for improvement opportunities" : "Analyser eksisterende egenskaper for forbedringsmuligheter", + "Analyzing..." : "Analyserer ...", + "Any favorites and sharing settings for this view" : "Eventuelle favoritter og delingsinnstillinger for denne visningen", + "Any user" : "Enhver bruker", + "App store cache invalidated successfully" : "Appbutikk-bufferen ble ugyldiggjort", + "Application deleted successfully" : "Applikasjonen ble slettet", + "Applications" : "Applikasjoner", + "Apply Changes" : "Bruk endringer", + "Archive" : "Arkiver", + "Archive conversation" : "Arkiver samtale", + "Archived conversations are hidden from your active list" : "Arkiverte samtaler skjules fra den aktive listen din", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Er du sikker på at du vil rydde opp i gamle søkespor? Dette vil slette oppføringer eldre enn 30 dager.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Er du sikker på at du vil slette de valgte revisjonssporene? Denne handlingen kan ikke angres.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Er du sikker på at du vil slette de valgte søkesporene? Denne handlingen kan ikke angres.", + "Are you sure you want to delete this ConfigSet?" : "Er du sikker på at du vil slette denne ConfigSet?", + "Are you sure you want to permanently delete" : "Er du sikker på at du vil slette permanent", + "Ask a question..." : "Still et spørsmål ...", + "Ask questions about your data using natural language" : "Still spørsmål om dataene dine ved hjelp av naturlig språk", + "Attempt" : "Forsøk", + "Audit Trail Changes" : "Endringer i revisjonsspor", + "Audit Trail Details" : "Detaljer om revisjonsspor", + "Audit Trail Management" : "Administrasjon av revisjonsspor", + "Audit Trail Statistics" : "Statistikk for revisjonsspor", + "Audit Trails" : "Revisjonsspor", + "Audit trail data copied to clipboard" : "Revisjonsspordata kopiert til utklippstavlen", + "Audit trail deleted successfully" : "Revisjonssporet ble slettet", + "Audit trail successfully deleted" : "Revisjonssporet ble slettet", + "Authentication" : "Autentisering", + "Auto-commit disabled" : "Automatisk commit deaktivert", + "Auto-commit enabled" : "Automatisk commit aktivert", + "Auto-create Default Organisation" : "Opprett standardorganisasjon automatisk", + "Auto-retry failed vectorizations" : "Prøv mislykkede vektoriseringer på nytt automatisk", + "Automatically commit changes to SOLR index" : "Commit endringer til SOLR-indeksen automatisk", + "Automatically create a default organisation if none exists when the app is initialized" : "Opprett en standardorganisasjon automatisk hvis ingen finnes når appen initialiseres", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Generer vektorinnbygginger automatisk fra tekstdeler når filer lastes opp og behandles", + "Automatically generate vector embeddings when objects are created or updated" : "Generer vektorinnbygginger automatisk når objekter opprettes eller oppdateres", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Prøv mislykkede vektoriseringsforsøk på nytt automatisk (maks 3 forsøk)", + "Available Filters" : "Tilgjengelige filtre", + "Available Workflows" : "Tilgjengelige arbeidsflyter", + "Avg Execution Time" : "Gjennomsnittlig utførelsestid", + "Avg Members/Org" : "Gjennomsnittlig medlemmer/org", + "Avg Object Views/Session" : "Gjennomsnittlig objektvisninger/økt", + "Avg Response Time" : "Gjennomsnittlig responstid", + "Avg Results/Search" : "Gjennomsnittlig resultater/søk", + "Avg Searches/Session" : "Gjennomsnittlig søk/økt", + "Back" : "Tilbake", + "Back to Entities" : "Tilbake til entiteter", + "Back to Registers" : "Tilbake til registre", + "Back to Webhooks" : "Tilbake til webhooks", + "Back to applications" : "Tilbake til applikasjoner", + "Back to entities" : "Tilbake til entiteter", + "Backend updated successfully. Please reload the application." : "Backend ble oppdatert. Last inn applikasjonen på nytt.", + "Base URL (Optional)" : "Base-URL (valgfritt)", + "Basic Information" : "Grunnleggende informasjon", + "Batch Size" : "Partistørrelse", + "Batch extraction completed" : "Partiuttrekking fullført", + "Before" : "Før", + "Before object vectorization can work:" : "Før objektvektorisering kan fungere:", + "Behavior Issue" : "Atferdsproblem", + "Bookmarks" : "Bokmerker", + "Blob storage has been retired. All objects now use magic tables." : "Blob-lagring er avviklet. Alle objekter bruker nå magiske tabeller.", + "Branch" : "Gren", + "Bulk delete operation completed successfully" : "Masseslettingsoperasjonen ble fullført", + "Bulk save operation completed successfully" : "Masselagringsoperasjonen ble fullført", + "Business Data" : "Forretningsdata", + "Cache cleared successfully" : "Bufferen ble tømt", + "Calculate Sizes" : "Beregn størrelser", + "Cancel" : "Avbryt", + "Cannot delete: objects are still attached" : "Kan ikke slette: objekter er fortsatt tilknyttet", + "Category" : "Kategori", + "Change Type" : "Endringstype", + "Changes" : "Endringer", + "Changes copied to clipboard" : "Endringer kopiert til utklippstavlen", + "Chat Model" : "Chat-modell", + "Chat Provider" : "Chat-leverandør", + "Chat Provider (RAG)" : "Chat-leverandør (RAG)", + "Chat Settings" : "Chat-innstillinger", + "Chat provider connection successful!" : "Tilkobling til chat-leverandør vellykket!", + "Chat settings" : "Chat-innstillinger", + "Choose a register" : "Velg et register", + "Choose a schema" : "Velg et skjema", + "Choose how vector similarity calculations are performed for semantic search" : "Velg hvordan vektorlikhetsberegninger utføres for semantisk søk", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Velg hvilke filtyper som skal inkluderes i vektoriseringsprosessen. Bare filer med uttrukket tekst og deler vil bli behandlet.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Velg hvilke visninger som skal inkluderes i vektoriseringsprosessen. La stå tomt for å behandle alle visninger basert på konfigurasjonen din.", + "Chunk" : "Del", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Sletting av del er ikke implementert ennå. Bruk delbaserte endepunkter.", + "Chunks" : "Deler", + "Chunks to Vectorize:" : "Deler som skal vektoriseres:", + "Cleanup Old Trails" : "Rydd opp i gamle spor", + "Cleanup completed" : "Opprydding fullført", + "Clear All Embeddings" : "Tøm alle innbygginger", + "Clear Entries" : "Tøm oppføringer", + "Clear Filtered Audit Trails" : "Tøm filtrerte revisjonsspor", + "Clear Filters" : "Tøm filtre", + "Clear Index" : "Tøm indeks", + "Clear Selection" : "Tøm utvalg", + "Clear all" : "Tøm alt", + "Clear all filters" : "Tøm alle filtre", + "Clear date range" : "Tøm datointervall", + "Clearing..." : "Tømmer ...", + "Close" : "Lukk", + "Collection Management" : "Samlingsadministrasjon", + "Collection Name" : "Samlingsnavn", + "Collection assignments updated successfully" : "Samlingstilordninger ble oppdatert", + "Collection cleared successfully" : "Samlingen ble tømt", + "Collection copied successfully" : "Samlingen ble kopiert", + "Collection created successfully" : "Samlingen ble opprettet", + "Collection deleted successfully" : "Samlingen ble slettet", + "Collection used to store and index file metadata and content" : "Samling brukt til å lagre og indeksere filmetadata og innhold", + "Collection used to store and index object data" : "Samling brukt til å lagre og indeksere objektdata", + "Collections:" : "Samlinger:", + "Commit Message" : "Commit-melding", + "Commit Within (ms)" : "Commit innen (ms)", + "Commit message" : "Commit-melding", + "Compare current schema with real object data" : "Sammenlign gjeldende skjema med reelle objektdata", + "Completed" : "Fullført", + "Complex" : "Kompleks", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Komplekse spørringer: Avanserte søk med flere filtre, operatorer og komplekse parameterkombinasjoner", + "Confidence Level" : "Konfidensnivå", + "Confidence Score" : "Konfidensscore", + "Config must be provided as an object" : "Konfigurasjon må oppgis som et objekt", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSet-administrasjon", + "ConfigSet Name" : "ConfigSet-navn", + "ConfigSet created successfully" : "ConfigSet ble opprettet", + "ConfigSet deleted successfully" : "ConfigSet ble slettet", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets definerer skjemaet og konfigurasjonen for SOLR-samlingene dine. De inneholder feltdefinisjoner, analysatorer og andre søkeinnstillinger.", + "Configuration" : "Konfigurasjon", + "Configuration Data" : "Konfigurasjonsdata", + "Configuration saved" : "Konfigurasjon lagret", + "Configurations" : "Konfigurasjoner", + "Configure Facets" : "Konfigurer fasetter", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurer leverandører av store språkmodeller (LLM) for KI-drevne funksjoner inkludert semantisk søk, innbygginger og chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurer grunnleggende tilkoblingsinnstillinger for SOLR-serveren din inkludert autentisering og nettverksalternativer. Bruk de separate dialogene for ConfigSet- og samlingsadministrasjon for å administrere kjerner og samlinger.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurer hvordan databaseobjekter konverteres til vektorinnbygginger for semantisk søk. Objekter vektoriseres direkte uten behov for tekstuttrekking.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurer hvordan objekter konverteres til tekst før vektorisering. Disse innstillingene påvirker søkekvalitet og kontekst.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurer parametere for filvektorisering. Denne prosessen vil generere vektorinnbygginger for alle uttrukne fildeler.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurer parametere for objektvektorisering. Denne prosessen vil generere vektorinnbygginger for alle objekter som samsvarer med visningsfiltrene dine.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurer hvilke datatyper som skal søkes i og hvor mange kilder som skal hentes", + "Connection Failed" : "Tilkobling mislyktes", + "Connection Settings" : "Tilkoblingsinnstillinger", + "Connection Successful!" : "Tilkobling vellykket!", + "Connection failed" : "Tilkobling mislyktes", + "Connection protocol" : "Tilkoblingsprotokoll", + "Connection successful" : "Tilkobling vellykket", + "Connection timeout in seconds" : "Tilkoblingstidsavbrudd i sekunder", + "Constant delay between retries (5 minutes)" : "Konstant forsinkelse mellom forsøk (5 minutter)", + "Constraint Issue" : "Begrensningsproblem", + "Content Filters" : "Innholdsfiltre", + "Continue to Properties" : "Fortsett til egenskaper", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontroller hvilke objektvisninger som skal vektoriseres for å redusere API-kostnader. Vektoriser bare visninger som drar nytte av semantisk søk.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontroller hvilke visninger og verktøy KI-en kan bruke i denne samtalen. Som standard er alle agentfunksjoner aktivert.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Kontrollerer tilfeldighet (0 = deterministisk, 2 = svært kreativ)", + "Conversation ID is required" : "Samtale-ID er påkrevd", + "Conversation archived" : "Samtale arkivert", + "Conversation archived successfully" : "Samtalen ble arkivert", + "Conversation cleared successfully" : "Samtalen ble tømt", + "Conversation deleted" : "Samtale slettet", + "Conversation permanently deleted" : "Samtale permanent slettet", + "Conversation renamed" : "Samtale gitt nytt navn", + "Conversation restored" : "Samtale gjenopprettet", + "Conversation title" : "Samtaletittel", + "Copied!" : "Kopiert!", + "Copy" : "Kopier", + "Copy Changes" : "Kopier endringer", + "Copy Collection" : "Kopier samling", + "Copy Data" : "Kopier data", + "Copy Full Data" : "Kopier alle data", + "Copying..." : "Kopierer ...", + "Count" : "Antall", + "Counting objects..." : "Teller objekter ...", + "Costs" : "Kostnader", + "Create" : "Opprett", + "Create Collection" : "Opprett samling", + "Create ConfigSet" : "Opprett ConfigSet", + "Create New Collection" : "Opprett ny samling", + "Create New ConfigSet" : "Opprett ny ConfigSet", + "Create Operations" : "Opprett-operasjoner", + "Create Webhook" : "Opprett webhook", + "Create a copy of collection:" : "Opprett en kopi av samlingen:", + "Create a new ConfigSet based on the _default template" : "Opprett en ny ConfigSet basert på _default-malen", + "Create a new SOLR collection from an existing ConfigSet" : "Opprett en ny SOLR-samling fra en eksisterende ConfigSet", + "Create your first AI agent to get started." : "Opprett din første KI-agent for å komme i gang.", + "Created" : "Opprettet", + "Created:" : "Opprettet:", + "Creating..." : "Oppretter ...", + "Current Filters" : "Gjeldende filtre", + "Current Type:" : "Gjeldende type:", + "Custom API endpoint if using a different region" : "Tilpasset API-endepunkt ved bruk av en annen region", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Tilpassede HTTP-hoder (én per linje, format: Header-Name: value)", + "Daily" : "Daglig", + "Dashboard" : "Dashbord", + "Data Source" : "Datakilde", + "Data sources" : "Datakilder", + "Data type does not match observed values" : "Datatypen samsvarer ikke med observerte verdier", + "Database URL" : "Database-URL", + "Database information refreshed" : "Databaseinformasjon oppdatert", + "Date Range" : "Datointervall", + "Default" : "Standard", + "Default Organisation" : "Standardorganisasjon", + "Default: 5" : "Standard: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Forsinkelsene dobles for hvert forsøk (2, 4, 8 minutter ...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Forsinkelsene øker lineært (5, 10, 15 minutter ...)", + "Delete" : "Slett", + "Delete Application" : "Slett applikasjon", + "Delete Audit Trail" : "Slett revisjonsspor", + "Delete Collection" : "Slett samling", + "Delete ConfigSet" : "Slett ConfigSet", + "Delete Objects" : "Slett objekter", + "Delete Operations" : "Slett-operasjoner", + "Delete View" : "Slett visning", + "Delete all objects in this schema" : "Slett alle objekter i dette skjemaet", + "Delete permanently" : "Slett permanent", + "Delete this conversation?" : "Slette denne samtalen?", + "Delete view" : "Slett visning", + "Deleted" : "Slettet", + "Deleted By" : "Slettet av", + "Deleted Date" : "Slettedato", + "Deleted Items Management" : "Administrasjon av slettede elementer", + "Deleted This Week" : "Slettet denne uken", + "Deleted Today" : "Slettet i dag", + "Deleted:" : "Slettet:", + "Deleting..." : "Sletter ...", + "Deletion Date Range" : "Datointervall for sletting", + "Deletion Statistics" : "Slettestatistikk", + "Deprecated" : "Avviklet", + "Description" : "Beskrivelse", + "Description:" : "Beskrivelse:", + "Detect data types and patterns" : "Oppdag datatyper og mønstre", + "Detected At" : "Oppdaget den", + "Detected Format:" : "Oppdaget format:", + "Detected Issues:" : "Oppdagede problemer:", + "Disable" : "Deaktiver", + "Disabled" : "Deaktivert", + "Discover Files" : "Oppdag filer", + "Discovered Properties" : "Oppdagede egenskaper", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Vil du permanent slette alle filtrerte revisjonssporoppføringer? Denne handlingen kan ikke angres.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Vil du permanent slette denne revisjonssporoppføringen? Denne handlingen kan ikke angres.", + "Documents" : "Dokumenter", + "Download API Spec" : "Last ned API-spesifikasjon", + "Edit" : "Rediger", + "Edit Register" : "Rediger register", + "Edit Schema" : "Rediger skjema", + "Edit View" : "Rediger visning", + "Edit Webhook" : "Rediger webhook", + "Edit view" : "Rediger visning", + "Edit view details" : "Rediger visningsdetaljer", + "Effectiveness" : "Effektivitet", + "Email" : "E-post", + "Emails" : "E-poster", + "Embedding Model" : "Innbyggingsmodell", + "Embedding Provider" : "Innbyggingsleverandør", + "Embedding generated successfully" : "Innbygging ble generert", + "Embedding provider connection successful!" : "Tilkobling til innbyggingsleverandør vellykket!", + "Enable" : "Aktiver", + "Enable auto-creation" : "Aktiver automatisk opprettelse", + "Enable automatic file vectorization" : "Aktiver automatisk filvektorisering", + "Enable automatic object vectorization" : "Aktiver automatisk objektvektorisering", + "Enable automatic synchronization" : "Aktiver automatisk synkronisering", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Aktiver detaljert logging for SOLR-operasjoner (anbefales for feilsøking)", + "Enable faceting" : "Aktiver fasettering", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Aktiver eller deaktiver LLM-funksjoner. Konfigurer leverandører og modeller ved hjelp av LLM-konfigurasjonsknappen ovenfor.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Aktiver eller deaktiver SOLR-søkeintegrasjon. Konfigurer tilkoblingsinnstillinger ved hjelp av knappen for tilkoblingsinnstillinger ovenfor.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Aktiver eller deaktiver n8n-arbeidsflytintegrasjon. Konfigurer tilkoblingsinnstillinger nedenfor.", + "Enable or disable this webhook" : "Aktiver eller deaktiver denne webhooken", + "Enable vectorization for all existing and future views (may increase costs)" : "Aktiver vektorisering for alle eksisterende og fremtidige visninger (kan øke kostnadene)", + "Enabled" : "Aktivert", + "Endpoints" : "Endepunkter", + "Enter ConfigSet name" : "Skriv inn ConfigSet-navn", + "Enter collection name" : "Skriv inn samlingsnavn", + "Enter description (optional)..." : "Skriv inn beskrivelse (valgfritt) ...", + "Enter new collection name" : "Skriv inn nytt samlingsnavn", + "Enter object ID" : "Skriv inn objekt-ID", + "Enter search term" : "Skriv inn søkeord", + "Enter view name..." : "Skriv inn visningsnavn ...", + "Enter webhook name" : "Skriv inn webhook-navn", + "Entities" : "Entiteter", + "Entity Information" : "Entitetsinformasjon", + "Entity deleted successfully" : "Entiteten ble slettet", + "Entity not found" : "Entiteten ble ikke funnet", + "Entries to be deleted:" : "Oppføringer som skal slettes:", + "Enum Issue" : "Enum-problem", + "Enum constraint is missing" : "Enum-begrensning mangler", + "Error" : "Feil", + "Error Details" : "Feildetaljer", + "Error Information" : "Feilinformasjon", + "Error Message" : "Feilmelding", + "Error loading application" : "Feil under innlasting av applikasjon", + "Error loading audit trails" : "Feil under innlasting av revisjonsspor", + "Error loading entity" : "Feil under innlasting av entitet", + "Error loading search trails" : "Feil under innlasting av søkespor", + "Estimated Batches:" : "Estimerte partier:", + "Estimated Cost:" : "Estimert kostnad:", + "Estimated Duration:" : "Estimert varighet:", + "Event" : "Hendelse", + "Event Property for Payload" : "Hendelsesegenskap for nyttelast", + "Events" : "Hendelser", + "Example Value" : "Eksempelverdi", + "Exclusive Maximum" : "Eksklusivt maksimum", + "Exclusive Minimum" : "Eksklusivt minimum", + "Execution Mode" : "Utførelsesmodus", + "Execution Time" : "Utførelsestid", + "Execution Time Range" : "Intervall for utførelsestid", + "Existing Improvements" : "Eksisterende forbedringer", + "Exponential" : "Eksponentiell", + "Export" : "Eksporter", + "Export completed successfully" : "Eksport fullført", + "Export, view, or delete audit trails" : "Eksporter, vis eller slett revisjonsspor", + "Extend Schema" : "Utvid skjema", + "Extended" : "Utvidet", + "Extensions" : "Utvidelser", + "Extract Pending Files" : "Trekk ut ventende filer", + "Extract properties from each object" : "Trekk ut egenskaper fra hvert objekt", + "Extracted At" : "Uttrukket den", + "Extraction Status" : "Uttrekkingsstatus", + "Facet configuration updated successfully" : "Fasettkonfigurasjonen ble oppdatert", + "Facets discovered and configured successfully" : "Fasetter ble oppdaget og konfigurert", + "Facets discovered successfully" : "Fasetter ble oppdaget", + "Failed" : "Mislyktes", + "Failed to analyze schema properties" : "Kunne ikke analysere skjemaegenskaper", + "Failed to archive conversation" : "Kunne ikke arkivere samtale", + "Failed to calculate sizes" : "Kunne ikke beregne størrelser", + "Failed to clear collection" : "Kunne ikke tømme samling", + "Failed to copy changes" : "Kunne ikke kopiere endringer", + "Failed to copy data" : "Kunne ikke kopiere data", + "Failed to copy data to clipboard" : "Kunne ikke kopiere data til utklippstavlen", + "Failed to create conversation" : "Kunne ikke opprette samtale", + "Failed to create or find project" : "Kunne ikke opprette eller finne prosjekt", + "Failed to delete collection" : "Kunne ikke slette samling", + "Failed to delete conversation" : "Kunne ikke slette samtale", + "Failed to delete webhook" : "Kunne ikke slette webhook", + "Failed to download API specification" : "Kunne ikke laste ned API-spesifikasjon", + "Failed to get SOLR field configuration" : "Kunne ikke hente SOLR-feltkonfigurasjon", + "Failed to get entity categories" : "Kunne ikke hente entitetskategorier", + "Failed to get entity statistics" : "Kunne ikke hente entitetsstatistikk", + "Failed to get entity types" : "Kunne ikke hente entitetstyper", + "Failed to load LLM configuration" : "Kunne ikke laste inn LLM-konfigurasjon", + "Failed to load Nextcloud groups" : "Kunne ikke laste inn Nextcloud-grupper", + "Failed to load conversation" : "Kunne ikke laste inn samtale", + "Failed to load entities" : "Kunne ikke laste inn entiteter", + "Failed to load entity" : "Kunne ikke laste inn entitet", + "Failed to load files" : "Kunne ikke laste inn filer", + "Failed to load organisations" : "Kunne ikke laste inn organisasjoner", + "Failed to load templates" : "Kunne ikke laste inn maler", + "Failed to load webhooks" : "Kunne ikke laste inn webhooks", + "Failed to load workflows" : "Kunne ikke laste inn arbeidsflyter", + "Failed to refresh database information" : "Kunne ikke oppdatere databaseinformasjon", + "Failed to reindex collection" : "Kunne ikke reindeksere samling", + "Failed to rename conversation" : "Kunne ikke gi nytt navn til samtale", + "Failed to restore conversation" : "Kunne ikke gjenopprette samtale", + "Failed to retry extraction" : "Kunne ikke prøve uttrekking på nytt", + "Failed to retry webhook" : "Kunne ikke prøve webhook på nytt", + "Failed to save GitHub token" : "Kunne ikke lagre GitHub-token", + "Failed to save GitLab URL" : "Kunne ikke lagre GitLab-URL", + "Failed to save GitLab token" : "Kunne ikke lagre GitLab-token", + "Failed to save additional feedback" : "Kunne ikke lagre ytterligere tilbakemelding", + "Failed to save n8n configuration" : "Kunne ikke lagre n8n-konfigurasjon", + "Failed to save roles" : "Kunne ikke lagre roller", + "Failed to save settings" : "Kunne ikke lagre innstillinger", + "Failed to save webhook" : "Kunne ikke lagre webhook", + "Failed to send feedback" : "Kunne ikke sende tilbakemelding", + "Failed to test webhook" : "Kunne ikke teste webhook", + "Failed to update favorite status" : "Kunne ikke oppdatere favorittstatus", + "Failed to update schema properties" : "Kunne ikke oppdatere skjemaegenskaper", + "Failed to update view" : "Kunne ikke oppdatere visning", + "Failed to update webhook" : "Kunne ikke oppdatere webhook", + "Feedback recorded" : "Tilbakemelding registrert", + "Fewer sources (1-3):" : "Færre kilder (1–3):", + "Field" : "Felt", + "File" : "Fil", + "File Chunk Prediction" : "Fildelprediksjon", + "File Collection" : "Filsamling", + "File Management" : "Filadministrasjon", + "File Name" : "Filnavn", + "File Path" : "Filbane", + "File Type Selection" : "Valg av filtype", + "File Vectorization" : "Filvektorisering", + "File Warmup" : "Filoppvarming", + "File actions menu" : "Filhandlingsmeny", + "File anonymized successfully" : "Filen ble anonymisert", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vektorisering av fildeler er ikke implementert ennå. Delene er klare og lagret, men vektoriseringstjenesten er under utvikling.", + "File collection not configured" : "Filsamling ikke konfigurert", + "File discovery completed" : "Filoppdagelse fullført", + "File extraction completed" : "Filuttrekking fullført", + "File extraction queued" : "Filuttrekking i kø", + "File indexed successfully" : "Filen ble indeksert", + "File is already anonymized" : "Filen er allerede anonymisert", + "File not found" : "Filen ble ikke funnet", + "File settings updated successfully" : "Filinnstillingene ble oppdatert", + "File sources" : "Filkilder", + "File vectorization configuration saved successfully" : "Konfigurasjonen for filvektorisering ble lagret", + "File vectorization started. Check the statistics section for progress." : "Filvektorisering startet. Sjekk statistikkdelen for fremdrift.", + "File warmup completed" : "Filoppvarming fullført", + "Files" : "Filer", + "Files with Completed Extraction:" : "Filer med fullført uttrekking:", + "Files → fileCollection, Objects → objectCollection" : "Filer → fileCollection, Objekter → objectCollection", + "Filter Audit Trails" : "Filtrer revisjonsspor", + "Filter Deleted Items" : "Filtrer slettede elementer", + "Filter Objects" : "Filtrer objekter", + "Filter Properties" : "Filtrer egenskaper", + "Filter Search Trails" : "Filtrer søkespor", + "Filter Statistics" : "Filtrer statistikk", + "Filter and analyze search trail entries" : "Filtrer og analyser søkesporoppføringer", + "Filter and manage audit trail entries" : "Filtrer og administrer revisjonssporoppføringer", + "Filter and manage soft deleted items" : "Filtrer og administrer myk-slettede elementer", + "Filter by object ID" : "Filtrer etter objekt-ID", + "Filter by search term" : "Filtrer etter søkeord", + "Filter by webhook" : "Filtrer etter webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filterdata lastet inn automatisk. Bruk filtrene nedenfor for å avgrense søket.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrer webhook-utløsere etter nyttelastegenskaper (én per linje, format: key: value)", + "Filtered" : "Filtrert", + "Filters" : "Filtre", + "Fireworks AI Chat Settings" : "Fireworks AI chat-innstillinger", + "Fireworks AI Embedding Configuration" : "Fireworks AI innbyggingskonfigurasjon", + "First" : "Første", + "Fixed" : "Fast", + "For chat and retrieval-augmented generation" : "For chat og gjenfinningsforsterket generering", + "For vector embeddings and semantic search" : "For vektorinnbygginger og semantisk søk", + "Format" : "Format", + "Format Issue" : "Formatproblem", + "Format constraint is missing" : "Formatbegrensning mangler", + "From Date" : "Fra dato", + "From date" : "Fra dato", + "Full data copied to clipboard" : "Alle data kopiert til utklippstavlen", + "General Issue" : "Generelt problem", + "Generate recommendations and confidence scores" : "Generer anbefalinger og konfidensscorer", + "Generate vectors immediately when new objects are created" : "Generer vektorer umiddelbart når nye objekter opprettes", + "GitHub token is valid" : "GitHub-token er gyldig", + "GitHub token removed successfully" : "GitHub-token ble fjernet", + "GitHub token saved successfully" : "GitHub-token ble lagret", + "GitLab URL saved successfully" : "GitLab-URL ble lagret", + "GitLab token is valid" : "GitLab-token er gyldig", + "GitLab token saved successfully" : "GitLab-token ble lagret", + "HTTP Method" : "HTTP-metode", + "HTTP method used to send webhook requests" : "HTTP-metode brukt til å sende webhook-forespørsler", + "Headers" : "Hoder", + "Health" : "Helse", + "Heartbeat successful - connection kept alive" : "Hjerteslag vellykket – tilkobling holdt i live", + "Helpful" : "Nyttig", + "Hide Filters" : "Skjul filtre", + "Hide in forms" : "Skjul i skjemaer", + "Hide in list view" : "Skjul i listevisning", + "High" : "Høy", + "High Confidence" : "Høy konfidens", + "Host" : "Vert", + "Hourly" : "Hver time", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Hvor dypt man skal traversere nestede objektegenskaper (1–20). Høyere verdier fanger opp mer detalj, men øker vektorstørrelsen.", + "How often to check for updates (1-168 hours)" : "Hvor ofte man skal sjekke for oppdateringer (1–168 timer)", + "How to handle retries for failed webhook deliveries" : "Hvordan håndtere nye forsøk for mislykkede webhook-leveringer", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "IP-hastighetsgrenser ble tømt", + "Identify properties not in the schema" : "Identifiser egenskaper som ikke er i skjemaet", + "Immutable" : "Uforanderlig", + "Import" : "Importer", + "Import successful" : "Import vellykket", + "Improved Property" : "Forbedret egenskap", + "In use" : "I bruk", + "Inactive" : "Inaktiv", + "Include IDs and names of related objects for better contextual search" : "Inkluder ID-er og navn på relaterte objekter for bedre kontekstuelt søk", + "Include related object references" : "Inkluder relaterte objektreferanser", + "Include schema and register metadata" : "Inkluder skjema- og registermetadata", + "Initialization failed" : "Initialisering mislyktes", + "Initialize Project" : "Initialiser prosjekt", + "Inspect Fields" : "Inspiser felter", + "Inspect Index" : "Inspiser indeks", + "Invalid" : "Ugyldig", + "Invalid batch size. Must be between 1 and 5000" : "Ugyldig partistørrelse. Må være mellom 1 og 5000", + "Invalid field name provided" : "Ugyldig feltnavn oppgitt", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Ugyldig maxObjects. Må være 0 (alle) eller positivt tall", + "Invalid:" : "Ugyldig:", + "Issue" : "Problem", + "Items per page:" : "Elementer per side:", + "Just now" : "Akkurat nå", + "LLM Configuration" : "LLM-konfigurasjon", + "LLM actions menu" : "LLM-handlingsmeny", + "LLM configuration saved successfully" : "LLM-konfigurasjonen ble lagret", + "LLM features disabled" : "LLM-funksjoner deaktivert", + "LLM features enabled" : "LLM-funksjoner aktivert", + "LLM must be enabled with an embedding provider configured" : "LLM må være aktivert med en innbyggingsleverandør konfigurert", + "LLM settings updated successfully" : "LLM-innstillingene ble oppdatert", + "Last" : "Siste", + "Last Triggered" : "Sist utløst", + "Length Range:" : "Lengdeintervall:", + "Linear" : "Lineær", + "Load Advanced Filters" : "Last inn avanserte filtre", + "Load advanced filters with live data from your search index" : "Last inn avanserte filtre med sanntidsdata fra søkeindeksen din", + "Load view" : "Last inn visning", + "Loading ConfigSets..." : "Laster inn ConfigSets ...", + "Loading advanced filters..." : "Laster inn avanserte filtre ...", + "Loading agents..." : "Laster inn agenter ...", + "Loading application details..." : "Laster inn applikasjonsdetaljer ...", + "Loading applications..." : "Laster inn applikasjoner ...", + "Loading archived conversations..." : "Laster inn arkiverte samtaler ...", + "Loading audit trails..." : "Laster inn revisjonsspor ...", + "Loading collections..." : "Laster inn samlinger ...", + "Loading configurations..." : "Laster inn konfigurasjoner ...", + "Loading conversation..." : "Laster inn samtale ...", + "Loading conversations..." : "Laster inn samtaler ...", + "Loading deleted items..." : "Laster inn slettede elementer ...", + "Loading events..." : "Laster inn hendelser ...", + "Loading filter data..." : "Laster inn filterdata ...", + "Loading groups..." : "Laster inn grupper ...", + "Loading log details..." : "Laster inn loggdetaljer ...", + "Loading registers..." : "Laster inn registre ...", + "Loading schemas..." : "Laster inn skjemaer ...", + "Loading search trails..." : "Laster inn søkespor ...", + "Loading sources..." : "Laster inn kilder ...", + "Loading statistics..." : "Laster inn statistikk ...", + "Loading users..." : "Laster inn brukere ...", + "Loading views..." : "Laster inn visninger ...", + "Loading..." : "Laster inn ...", + "Local" : "Lokal", + "Local Version" : "Lokal versjon", + "Locked" : "Låst", + "Locked:" : "Låst:", + "Login successful" : "Innlogging vellykket", + "Logs" : "Logger", + "Low" : "Lav", + "Low Confidence" : "Lav konfidens", + "Lucene Version:" : "Lucene-versjon:", + "Magic table synchronized successfully" : "Den magiske tabellen ble synkronisert", + "Manage Organisation Roles" : "Administrer organisasjonsroller", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Administrer SOLR-samlinger (datalagre) og tildel dem for objekter og filer.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Administrer SOLR ConfigSets (konfigurasjonsmaler) for samlingene dine.", + "Manage and configure agents for automated tasks" : "Administrer og konfigurer agenter for automatiserte oppgaver", + "Manage and monitor file text extraction status" : "Administrer og overvåk status for tekstuttrekking fra filer", + "Manage and restore soft deleted items from your registers" : "Administrer og gjenopprett myk-slettede elementer fra registrene dine", + "Manage and view detected entities from files and objects" : "Administrer og vis oppdagede entiteter fra filer og objekter", + "Manage document templates and themes" : "Administrer dokumentmaler og temaer", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Administrer n8n-arbeidsflyter for OpenRegister-automatisering. Arbeidsflyter vil bli lagret i det konfigurerte prosjektet.", + "Manage webhooks for event-driven integrations" : "Administrer webhooks for hendelsesdrevne integrasjoner", + "Manage your applications and modules" : "Administrer applikasjonene og modulene dine", + "Manage your data registers and their configurations" : "Administrer dataregistrene dine og konfigurasjonene deres", + "Manage your data schemas and their properties" : "Administrer dataskjemaene dine og egenskapene deres", + "Manage your data sources and their configurations" : "Administrer datakildene dine og konfigurasjonene deres", + "Manage your saved search configurations" : "Administrer de lagrede søkekonfigurasjonene dine", + "Manage your system configurations and settings" : "Administrer systemkonfigurasjonene og -innstillingene dine", + "Managed" : "Administrert", + "Max Files (0 = all)" : "Maks filer (0 = alle)", + "Max Length" : "Maks lengde", + "Max Length:" : "Maks lengde:", + "Max Objects (0 = all)" : "Maks objekter (0 = alle)", + "Max Retries" : "Maks forsøk", + "Max Shards/Node" : "Maks shards/node", + "Max execution time (ms)" : "Maks utførelsestid (ms)", + "Max ms" : "Maks ms", + "Max result count" : "Maks resultatantall", + "Max results" : "Maks resultater", + "Maximum" : "Maksimum", + "Maximum Nesting Depth" : "Maksimal nestingsdybde", + "Maximum length constraint is missing" : "Begrensning for maksimal lengde mangler", + "Maximum length is too restrictive" : "Maksimal lengde er for restriktiv", + "Maximum number of files to process. Set to 0 to process all files." : "Maksimalt antall filer som skal behandles. Sett til 0 for å behandle alle filer.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maksimalt antall objekter som skal behandles. Sett til 0 for å behandle alle objekter.", + "Maximum number of retry attempts for failed deliveries" : "Maksimalt antall nye forsøk for mislykkede leveringer", + "Maximum time to wait before committing changes" : "Maksimal tid å vente før commit av endringer", + "Maximum tokens to generate" : "Maksimalt antall tokens som skal genereres", + "Maximum value constraint is missing" : "Begrensning for maksimal verdi mangler", + "Maximum value is too restrictive" : "Maksimal verdi er for restriktiv", + "Medium" : "Middels", + "Medium Confidence" : "Middels konfidens", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Middels spørringer: Søk med noe filtrering eller flere parametere (f.eks. datointervaller, spesifikke registre/skjemaer)", + "Members of selected groups can access this view" : "Medlemmer av valgte grupper har tilgang til denne visningen", + "Members:" : "Medlemmer:", + "Memory prediction calculated successfully" : "Minneprediksjon ble beregnet", + "Message content is required" : "Meldingsinnhold er påkrevd", + "Message does not belong to this conversation" : "Meldingen tilhører ikke denne samtalen", + "Metadata Filters" : "Metadatafiltre", + "Method" : "Metode", + "Min Length" : "Min lengde", + "Min execution time (ms)" : "Min utførelsestid (ms)", + "Min ms" : "Min ms", + "Min result count" : "Min resultatantall", + "Min results" : "Min resultater", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Begrensning for minimumsverdi mangler", + "Minimum value is too restrictive" : "Minimumsverdi er for restriktiv", + "Missing object fields created successfully" : "Manglende objektfelter ble opprettet", + "Mode:" : "Modus:", + "Model" : "Modell", + "Model Name" : "Modellnavn", + "Modified" : "Endret", + "Monthly" : "Månedlig", + "More sources (10-20):" : "Flere kilder (10–20):", + "Most Active Objects" : "Mest aktive objekter", + "Multiple Of" : "Multiplum av", + "Name" : "Navn", + "Names cache warmup completed" : "Oppvarming av navnebuffer fullført", + "New Collection Name" : "Nytt samlingsnavn", + "New Conversation" : "Ny samtale", + "New Properties" : "Nye egenskaper", + "New Property" : "Ny egenskap", + "New Value" : "Ny verdi", + "New users without specific organisation membership will be automatically added to this organisation" : "Nye brukere uten spesifikt organisasjonsmedlemskap vil automatisk bli lagt til i denne organisasjonen", + "Next" : "Neste", + "No" : "Nei", + "No ConfigSets found" : "Ingen ConfigSets funnet", + "No GitHub token configured" : "Ingen GitHub-token konfigurert", + "No GitHub token provided" : "Ingen GitHub-token oppgitt", + "No GitLab token provided" : "Ingen GitLab-token oppgitt", + "No Organisation" : "Ingen organisasjon", + "No active filters" : "Ingen aktive filtre", + "No activity data available" : "Ingen aktivitetsdata tilgjengelig", + "No agents available" : "Ingen agenter tilgjengelig", + "No agents found" : "Ingen agenter funnet", + "No app store cache exists yet" : "Ingen appbutikk-buffer finnes ennå", + "No applications are available." : "Ingen applikasjoner er tilgjengelige.", + "No applications found" : "Ingen applikasjoner funnet", + "No archived conversations" : "Ingen arkiverte samtaler", + "No audit trail entries found" : "Ingen revisjonssporoppføringer funnet", + "No changes recorded" : "Ingen endringer registrert", + "No chunks to vectorize" : "Ingen deler å vektorisere", + "No collections found" : "Ingen samlinger funnet", + "No configuration data" : "Ingen konfigurasjonsdata", + "No configurations are available." : "Ingen konfigurasjoner er tilgjengelige.", + "No configurations found" : "Ingen konfigurasjoner funnet", + "No configurations found for this application." : "Ingen konfigurasjoner funnet for denne applikasjonen.", + "No conversations yet. Create a new one to get started!" : "Ingen samtaler ennå. Opprett en ny for å komme i gang!", + "No data available" : "Ingen data tilgjengelig", + "No data available for chart" : "Ingen data tilgjengelig for diagram", + "No deleted items found" : "Ingen slettede elementer funnet", + "No deletion data available" : "Ingen slettedata tilgjengelig", + "No description found" : "Ingen beskrivelse funnet", + "No description provided" : "Ingen beskrivelse oppgitt", + "No entities detected in this file. Run text extraction first." : "Ingen entiteter oppdaget i denne filen. Kjør tekstuttrekking først.", + "No entities found" : "Ingen entiteter funnet", + "No entities have been detected yet" : "Ingen entiteter har blitt oppdaget ennå", + "No events found" : "Ingen hendelser funnet", + "No expired audit trails found to clear" : "Ingen utløpte revisjonsspor funnet å tømme", + "No facetable fields available. Select a register and schema to see available filters." : "Ingen fasetterbare felter tilgjengelig. Velg et register og skjema for å se tilgjengelige filtre.", + "No files found" : "Ingen filer funnet", + "No files have been extracted yet" : "Ingen filer har blitt uttrukket ennå", + "No files to process" : "Ingen filer å behandle", + "No files to reindex" : "Ingen filer å reindeksere", + "No filters are currently active. This will delete ALL audit trail entries!" : "Ingen filtre er for øyeblikket aktive. Dette vil slette ALLE revisjonssporoppføringer!", + "No log entries found" : "Ingen loggoppføringer funnet", + "No logs are available for this configuration." : "Ingen logger er tilgjengelige for denne konfigurasjonen.", + "No logs are available for this source." : "Ingen logger er tilgjengelige for denne kilden.", + "No logs found" : "Ingen logger funnet", + "No mismatched fields found - SOLR schema is properly configured" : "Ingen feilmatchede felter funnet – SOLR-skjemaet er riktig konfigurert", + "No objects found" : "Ingen objekter funnet", + "No objects found to vectorize" : "Ingen objekter funnet å vektorisere", + "No objects selected for deletion" : "Ingen objekter valgt for sletting", + "No objects to delete" : "Ingen objekter å slette", + "No objects were permanently deleted" : "Ingen objekter ble permanent slettet", + "No properties found" : "Ingen egenskaper funnet", + "No properties match your filters." : "Ingen egenskaper samsvarer med filtrene dine.", + "No purge date set" : "Ingen utrenskingsdato angitt", + "No register data available" : "Ingen registerdata tilgjengelig", + "No registers found" : "Ingen registre funnet", + "No registers found for this application." : "Ingen registre funnet for denne applikasjonen.", + "No relations found" : "Ingen relasjoner funnet", + "No request body available" : "Ingen forespørselskropp tilgjengelig", + "No response body available" : "Ingen responskropp tilgjengelig", + "No saved views yet. Create one in the Search tab!" : "Ingen lagrede visninger ennå. Opprett en i Søk-fanen!", + "No schema data available" : "Ingen skjemadata tilgjengelig", + "No schema selected for exploration" : "Ingen skjema valgt for utforskning", + "No schemas found" : "Ingen skjemaer funnet", + "No schemas found for this application." : "Ingen skjemaer funnet for denne applikasjonen.", + "No search terms data available" : "Ingen søkeorddata tilgjengelig", + "No search trail entries found" : "Ingen søkesporoppføringer funnet", + "No sources are available." : "Ingen kilder er tilgjengelige.", + "No sources found" : "Ingen kilder funnet", + "No templates found" : "Ingen maler funnet", + "No templates have been created yet" : "Ingen maler har blitt opprettet ennå", + "No valid magic-mapped register+schema combinations found" : "Ingen gyldige magisk-mappede kombinasjoner av register+skjema funnet", + "No views found. Create views first before configuring vectorization." : "Ingen visninger funnet. Opprett visninger før du konfigurerer vektorisering.", + "No views match your search" : "Ingen visninger samsvarer med søket ditt", + "No webhooks found" : "Ingen webhooks funnet", + "No webhooks have been configured yet" : "Ingen webhooks har blitt konfigurert ennå", + "No workflows found in this project. Create workflows in the n8n editor." : "Ingen arbeidsflyter funnet i dette prosjektet. Opprett arbeidsflyter i n8n-editoren.", + "None" : "Ingen", + "Not helpful" : "Ikke nyttig", + "Not in use" : "Ikke i bruk", + "Number Constraints" : "Tallbegrensninger", + "Number of chunks to vectorize in one API call (1-100)." : "Antall deler som skal vektoriseres i ett API-kall (1–100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Antall deler som skal vektoriseres i ett API-kall. Høyere = raskere, men mer minne. Anbefalt: 10–50.", + "Number of objects to process in each batch (1-5000)." : "Antall objekter som skal behandles i hvert parti (1–5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Antall objekter som skal vektoriseres i ett API-kall. Høyere = raskere, men mer minne. Anbefalt: 10–50.", + "Numeric Range:" : "Tallintervall:", + "Object" : "Objekt", + "Object Collection" : "Objektsamling", + "Object Count Prediction" : "Prediksjon av objektantall", + "Object ID" : "Objekt-ID", + "Object Management" : "Objektadministrasjon", + "Object Vectorization" : "Objektvektorisering", + "Object Warmup" : "Objektoppvarming", + "Object collection not configured" : "Objektsamling ikke konfigurert", + "Object not found" : "Objektet ble ikke funnet", + "Object permanently deleted" : "Objektet ble permanent slettet", + "Object restored successfully" : "Objektet ble gjenopprettet", + "Object settings updated successfully" : "Objektinnstillingene ble oppdatert", + "Object sources" : "Objektkilder", + "Object unlocked successfully" : "Objektet ble låst opp", + "Object vectorization configuration saved successfully" : "Konfigurasjonen for objektvektorisering ble lagret", + "Object vectorized successfully" : "Objektet ble vektorisert", + "Object:" : "Objekt:", + "Objects" : "Objekter", + "Objects Analyzed" : "Objekter analysert", + "Objects Distribution" : "Objektfordeling", + "Objects being analyzed" : "Objekter som analyseres", + "Objects by Register" : "Objekter etter register", + "Objects by Schema" : "Objekter etter skjema", + "Objects deletion completed successfully" : "Sletting av objekter ble fullført", + "Objects in schema" : "Objekter i skjema", + "Objects to Process:" : "Objekter som skal behandles:", + "Objects to be analyzed" : "Objekter som skal analyseres", + "Objects to be deleted" : "Objekter som skal slettes", + "Objects to be validated" : "Objekter som skal valideres", + "Objects will be serialized as JSON text before vectorization" : "Objekter vil bli serialisert som JSON-tekst før vektorisering", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekter vil bli myk-slettet (merket som slettet, men beholdt i databasen). De kan gjenopprettes senere ved behov.", + "Old Value" : "Gammel verdi", + "Oldest Item (days)" : "Eldste element (dager)", + "Ollama Chat Settings" : "Ollama chat-innstillinger", + "Ollama Embedding Configuration" : "Ollama innbyggingskonfigurasjon", + "Ollama URL" : "Ollama-URL", + "Only With Changes" : "Bare med endringer", + "Open Register Objects" : "Open Register-objekter", + "Open n8n Editor" : "Åpne n8n-editor", + "OpenAI Chat Settings" : "OpenAI chat-innstillinger", + "OpenAI Embedding Configuration" : "OpenAI innbyggingskonfigurasjon", + "OpenRegister Settings" : "OpenRegister-innstillinger", + "Optional webhook secret for signature verification" : "Valgfri webhook-hemmelighet for signaturverifisering", + "Order" : "Rekkefølge", + "Organisation" : "Organisasjon", + "Organisation ID" : "Organisasjons-ID", + "Organisation Members" : "Organisasjonsmedlemmer", + "Organisation Statistics" : "Organisasjonsstatistikk", + "Organisation created successfully" : "Organisasjonen ble opprettet", + "Organisation settings saved successfully" : "Organisasjonsinnstillingene ble lagret", + "Organisations" : "Organisasjoner", + "Organization" : "Organisasjon", + "Organization ID (Optional)" : "Organisasjons-ID (valgfritt)", + "Orphaned Items" : "Foreldreløse elementer", + "Owner" : "Eier", + "Owner:" : "Eier:", + "Parallel Mode (Faster, more resource intensive)" : "Parallellmodus (raskere, mer ressurskrevende)", + "Parallel:" : "Parallell:", + "Password" : "Passord", + "Password for SOLR authentication (optional)" : "Passord for SOLR-autentisering (valgfritt)", + "Path" : "Bane", + "Path in repository" : "Bane i depot", + "Path where the configuration file will be saved in the repository" : "Bane der konfigurasjonsfilen vil bli lagret i depotet", + "Path where the register OAS file will be saved in the repository" : "Bane der register-OAS-filen vil bli lagret i depotet", + "Pattern (regex)" : "Mønster (regulært uttrykk)", + "Pattern Issue" : "Mønsterproblem", + "Pattern constraint is missing" : "Mønsterbegrensning mangler", + "Patterns:" : "Mønstre:", + "Pending" : "Venter", + "Permanent Delete Mode" : "Permanent slettemodus", + "Permanently Delete" : "Slett permanent", + "Permanently delete already soft-deleted objects" : "Slett permanent allerede myk-slettede objekter", + "Permanently delete this conversation?" : "Slette denne samtalen permanent?", + "Person" : "Person", + "Personal Data" : "Personopplysninger", + "Phone" : "Telefon", + "Photos" : "Bilder", + "Please create an agent in the" : "Opprett en agent i", + "Please select an agent to continue" : "Velg en agent for å fortsette", + "Please select which register and schema to use for the new object" : "Velg hvilket register og skjema som skal brukes for det nye objektet", + "Please try again later." : "Prøv igjen senere.", + "Please wait while we fetch your agents." : "Vent mens vi henter agentene dine.", + "Please wait while we fetch your applications." : "Vent mens vi henter applikasjonene dine.", + "Please wait while we fetch your configurations." : "Vent mens vi henter konfigurasjonene dine.", + "Please wait while we fetch your deleted items." : "Vent mens vi henter de slettede elementene dine.", + "Please wait while we fetch your sources." : "Vent mens vi henter kildene dine.", + "Polls" : "Avstemninger", + "Popular Search Terms" : "Populære søkeord", + "Port" : "Port", + "Prerequisites" : "Forutsetninger", + "Press Enter to send, Shift+Enter for new line" : "Trykk Enter for å sende, Shift+Enter for ny linje", + "Previous" : "Forrige", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Behandler deler i partier med simulert parallellisme.", + "Processes file chunks sequentially (safest)." : "Behandler fildeler sekvensielt (sikrest).", + "Processes objects in chunks with simulated parallelism." : "Behandler objekter i deler med simulert parallellisme.", + "Processes objects sequentially (safest)." : "Behandler objekter sekvensielt (sikrest).", + "Processing" : "Behandler", + "Processing Limits" : "Behandlingsgrenser", + "Processing..." : "Behandler ...", + "Project initialized successfully" : "Prosjektet ble initialisert", + "Project not found. Please initialize first." : "Prosjektet ble ikke funnet. Initialiser først.", + "Properties" : "Egenskaper", + "Property Behaviors" : "Egenskapsatferd", + "Property Configuration:" : "Egenskapskonfigurasjon:", + "Property Filters" : "Egenskapsfiltre", + "Property Title" : "Egenskapstittel", + "Property Type" : "Egenskapstype", + "Property can be improved" : "Egenskapen kan forbedres", + "Protected" : "Beskyttet", + "Provider" : "Leverandør", + "Provider is required for testing" : "Leverandør er påkrevd for testing", + "Public" : "Offentlig", + "Public View" : "Offentlig visning", + "Public views can be accessed by anyone in the system" : "Offentlige visninger kan nås av alle i systemet", + "Publish" : "Publiser", + "Publish Objects" : "Publiser objekter", + "Publish all objects in this schema" : "Publiser alle objekter i dette skjemaet", + "Published" : "Publisert", + "Published:" : "Publisert:", + "Publishing..." : "Publiserer ...", + "Purge" : "Rensk ut", + "Purge Date" : "Utrenskingsdato", + "Query Complexity Distribution" : "Fordeling av spørringskompleksitet", + "Query parameter is required" : "Spørringsparameter er påkrevd", + "RAG" : "RAG", + "RAG Configuration" : "RAG-konfigurasjon", + "Rate limits cleared successfully" : "Hastighetsgrenser ble tømt", + "Raw Changes Data" : "Rådata for endringer", + "Re-vectorize on object update" : "Revektoriser ved objektoppdatering", + "Read" : "Les", + "Recommendations:" : "Anbefalinger:", + "Recommended Type:" : "Anbefalt type:", + "Recommended: 5 sources" : "Anbefalt: 5 kilder", + "Refresh" : "Oppdater", + "Refresh Data" : "Oppdater data", + "Refresh Stats" : "Oppdater statistikk", + "Refresh Workflows" : "Oppdater arbeidsflyter", + "Refresh dashboard" : "Oppdater dashbord", + "Refresh database info" : "Oppdater databaseinfo", + "Register" : "Register", + "Register ID" : "Register-ID", + "Register Statistics" : "Registerstatistikk", + "Register Totals" : "Registertotaler", + "Register depublished successfully" : "Registeret ble avpublisert", + "Register not found" : "Registeret ble ikke funnet", + "Register objects deletion completed successfully" : "Sletting av registerobjekter ble fullført", + "Register published successfully" : "Registeret ble publisert", + "Register/Schema Usage" : "Register/skjema-bruk", + "Registers" : "Registre", + "Reindex" : "Reindekser", + "Reindex completed" : "Reindeksering fullført", + "Reindex completed successfully" : "Reindeksering ble fullført", + "Reindex failed" : "Reindeksering mislyktes", + "Relations" : "Relasjoner", + "Remote Version" : "Ekstern versjon", + "Remove" : "Fjern", + "Remove filter" : "Fjern filter", + "Remove from favorites" : "Fjern fra favoritter", + "Remove group" : "Fjern gruppe", + "Removed" : "Fjernet", + "Removed from favorites" : "Fjernet fra favoritter", + "Rename Conversation" : "Gi nytt navn til samtale", + "Rename conversation" : "Gi nytt navn til samtale", + "Replicas" : "Replikaer", + "Repository" : "Depot", + "Request Body" : "Forespørselskropp", + "Request Data" : "Forespørselsdata", + "Request Details" : "Forespørselsdetaljer", + "Request body must be JSON with a \"query\" field" : "Forespørselskroppen må være JSON med et \"query\"-felt", + "Request timeout in seconds" : "Forespørselstidsavbrudd i sekunder", + "Required field" : "Påkrevd felt", + "Required status is inconsistent" : "Påkrevd-status er inkonsekvent", + "Rerun Search" : "Kjør søk på nytt", + "Reset Changes" : "Tilbakestill endringer", + "Reset Filters" : "Tilbakestill filtre", + "Response Body" : "Responskropp", + "Response Details" : "Responsdetaljer", + "Response Time:" : "Responstid:", + "Restore" : "Gjenopprett", + "Restore conversation" : "Gjenopprett samtale", + "Restore or permanently delete items" : "Gjenopprett eller slett elementer permanent", + "Result Count Range" : "Intervall for resultatantall", + "Results" : "Resultater", + "Retrieve all objects for this schema" : "Hent alle objekter for dette skjemaet", + "Retry" : "Prøv på nytt", + "Retry Failed Extractions" : "Prøv mislykkede uttrekkinger på nytt", + "Retry Policy" : "Retningslinjer for nye forsøk", + "Retry completed" : "Nytt forsøk fullført", + "Retrying..." : "Prøver på nytt ...", + "Risk Level" : "Risikonivå", + "Roles updated successfully" : "Rollene ble oppdatert", + "SOLR Connection Settings" : "SOLR-tilkoblingsinnstillinger", + "SOLR Version:" : "SOLR-versjon:", + "SOLR actions menu" : "SOLR-handlingsmeny", + "SOLR base path (usually /solr)" : "SOLR-basebane (vanligvis /solr)", + "SOLR is disabled" : "SOLR er deaktivert", + "SOLR is not available or not configured" : "SOLR er ikke tilgjengelig eller ikke konfigurert", + "SOLR logging disabled" : "SOLR-logging deaktivert", + "SOLR logging enabled" : "SOLR-logging aktivert", + "SOLR search disabled" : "SOLR-søk deaktivert", + "SOLR search enabled" : "SOLR-søk aktivert", + "SOLR server hostname or IP address" : "SOLR-serverens vertsnavn eller IP-adresse", + "SOLR server port number (optional, defaults to 8983)" : "SOLR-serverens portnummer (valgfritt, standard er 8983)", + "SOLR setup completed successfully" : "SOLR-oppsett ble fullført", + "SOLR setup failed" : "SOLR-oppsett mislyktes", + "SOLR setup failed - check logs" : "SOLR-oppsett mislyktes – sjekk logger", + "Sample Values:" : "Eksempelverdier:", + "Save" : "Lagre", + "Save Configuration" : "Lagre konfigurasjon", + "Save Connection Settings" : "Lagre tilkoblingsinnstillinger", + "Save Roles" : "Lagre roller", + "Save Settings" : "Lagre innstillinger", + "Save current search as view" : "Lagre gjeldende søk som visning", + "Saved Views" : "Lagrede visninger", + "Saving..." : "Lagrer ...", + "Schema" : "Skjema", + "Schema ID" : "Skjema-ID", + "Schema Name" : "Skjemanavn", + "Schema Statistics" : "Skjemastatistikk", + "Schema depublished successfully" : "Skjemaet ble avpublisert", + "Schema objects deletion completed successfully" : "Sletting av skjemaobjekter ble fullført", + "Schema published successfully" : "Skjemaet ble publisert", + "Schema successfully created" : "Skjemaet ble opprettet", + "Schema successfully updated" : "Skjemaet ble oppdatert", + "Schemas" : "Skjemaer", + "Scheme" : "Skjema", + "Search / Views" : "Søk / visninger", + "Search Activity" : "Søkeaktivitet", + "Search Analytics" : "Søkeanalyse", + "Search Entities" : "Søk i entiteter", + "Search Files" : "Søk i filer", + "Search GitHub" : "Søk i GitHub", + "Search GitLab" : "Søk i GitLab", + "Search Method" : "Søkemetode", + "Search Term" : "Søkeord", + "Search Trail Details" : "Detaljer om søkespor", + "Search Trail Management" : "Administrasjon av søkespor", + "Search Trail Statistics" : "Statistikk for søkespor", + "Search Trails" : "Søkespor", + "Search Views" : "Søk i visninger", + "Search Webhooks" : "Søk i webhooks", + "Search by file name or path" : "Søk etter filnavn eller bane", + "Search by name or URL" : "Søk etter navn eller URL", + "Search by value" : "Søk etter verdi", + "Search in Files" : "Søk i filer", + "Search in Objects" : "Søk i objekter", + "Search property names..." : "Søk i egenskapsnavn ...", + "Search trail deletion not implemented yet" : "Sletting av søkespor er ikke implementert ennå", + "Search views..." : "Søk i visninger ...", + "Secret" : "Hemmelighet", + "Secret key for HMAC signature generation (optional)" : "Hemmelig nøkkel for HMAC-signaturgenerering (valgfritt)", + "Select AI Agent" : "Velg KI-agent", + "Select All" : "Velg alle", + "Select ConfigSet" : "Velg ConfigSet", + "Select File Types to Vectorize:" : "Velg filtyper som skal vektoriseres:", + "Select HTTP method" : "Velg HTTP-metode", + "Select Register and Schema" : "Velg register og skjema", + "Select Views to Vectorize:" : "Velg visninger som skal vektoriseres:", + "Select a Nextcloud group to add" : "Velg en Nextcloud-gruppe å legge til", + "Select a branch" : "Velg en gren", + "Select a model or type a custom model name" : "Velg en modell eller skriv inn et tilpasset modellnavn", + "Select a repository" : "Velg et depot", + "Select a repository you have write access to" : "Velg et depot du har skrivetilgang til", + "Select an AI Agent" : "Velg en KI-agent", + "Select an AI agent to begin chatting with your data." : "Velg en KI-agent for å begynne å chatte med dataene dine.", + "Select backend" : "Velg backend", + "Select chat model" : "Velg chat-modell", + "Select collection for files" : "Velg samling for filer", + "Select collection for objects" : "Velg samling for objekter", + "Select default organisation" : "Velg standardorganisasjon", + "Select event to listen to..." : "Velg hendelse å lytte til ...", + "Select model" : "Velg modell", + "Select options..." : "Velg alternativer ...", + "Select period" : "Velg periode", + "Select property to send as payload" : "Velg egenskap som skal sendes som nyttelast", + "Select provider" : "Velg leverandør", + "Select registers and schemas to save a view" : "Velg registre og skjemaer for å lagre en visning", + "Select retry policy" : "Velg retningslinjer for nye forsøk", + "Select the branch to publish to" : "Velg grenen det skal publiseres til", + "Select the event this webhook should listen to" : "Velg hendelsen denne webhooken skal lytte til", + "Select views to vectorize:" : "Velg visninger som skal vektoriseres:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Velg hvilke Nextcloud-grupper som er tilgjengelige for denne organisasjonen. Brukere i disse gruppene vil ha tilgang til organisasjonsressurser.", + "Select which data views the AI can search" : "Velg hvilke datavisninger KI-en kan søke i", + "Select which property from the event should be used as the webhook payload data" : "Velg hvilken egenskap fra hendelsen som skal brukes som nyttelastdata for webhooken", + "Select which tools the AI can use to perform actions" : "Velg hvilke verktøy KI-en kan bruke til å utføre handlinger", + "Selected" : "Valgt", + "Selected Groups" : "Valgte grupper", + "Selected audit trails deleted successfully" : "De valgte revisjonssporene ble slettet", + "Selected search trails deleted successfully" : "De valgte søkesporene ble slettet", + "Selected users can access this view" : "Valgte brukere har tilgang til denne visningen", + "Send additional feedback" : "Send ytterligere tilbakemelding", + "Send as CloudEvent" : "Send som CloudEvent", + "Sensitive PII" : "Sensitive personopplysninger", + "Serial Mode (Safer, slower)" : "Seriemodus (sikrere, tregere)", + "Serial:" : "Serie:", + "Server Configuration" : "Serverkonfigurasjon", + "Server Information" : "Serverinformasjon", + "Settings" : "Innstillinger", + "Shards" : "Shards", + "Share" : "Del", + "Share with Groups" : "Del med grupper", + "Share with Users" : "Del med brukere", + "Show Filters" : "Vis filtre", + "Show less" : "Vis mindre", + "Show only entries with changes" : "Vis bare oppføringer med endringer", + "Simple" : "Enkel", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Enkle spørringer: Grunnleggende tekstsøk med minimale parametere (f.eks. enkelt søkeord, ingen avanserte filtre)", + "Size" : "Størrelse", + "Slug" : "Slug", + "Soft Delete Mode" : "Myk slettemodus", + "Soft Deleted Items" : "Myk-slettede elementer", + "SolrCloud mode enabled" : "SolrCloud-modus aktivert", + "Source" : "Kilde", + "Sources" : "Kilder", + "Sources:" : "Kilder:", + "Standalone SOLR mode" : "Frittstående SOLR-modus", + "Start Conversation" : "Start samtale", + "Start Vectorization" : "Start vektorisering", + "Start a conversation" : "Start en samtale", + "Starting names cache warmup..." : "Starter oppvarming av navnebuffer ...", + "Starting..." : "Starter ...", + "Statistics" : "Statistikk", + "Stats calculation not yet implemented" : "Statistikkberegning er ikke implementert ennå", + "Status" : "Status", + "Status Code" : "Statuskode", + "String Constraints" : "Strengbegrensninger", + "Success" : "Vellykket", + "Success Rate" : "Suksessrate", + "Success Status" : "Suksessstatus", + "Successful" : "Vellykket", + "Successfully joined organisation" : "Ble med i organisasjonen", + "Sync Table" : "Synkroniser tabell", + "Synchronization Settings" : "Synkroniseringsinnstillinger", + "System Default" : "Systemstandard", + "Technical Description" : "Teknisk beskrivelse", + "Technical description for developers and administrators" : "Teknisk beskrivelse for utviklere og administratorer", + "Temperature" : "Temperatur", + "Templates" : "Maler", + "Test" : "Test", + "Test Chat" : "Test chat", + "Test Connection" : "Test tilkobling", + "Test Embedding" : "Test innbygging", + "Test endpoint executed successfully" : "Testendepunktet ble utført", + "Test webhook delivered successfully" : "Testwebhooken ble levert", + "Test webhook delivery failed" : "Levering av testwebhook mislyktes", + "Test webhook sent successfully" : "Testwebhooken ble sendt", + "Testing..." : "Tester ...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Tekstdeler genereres under filuttrekking og lagres i databasen. Vektorisering leser disse forhåndsdelte filene og konverterer dem til innbygginger.", + "Text extracted successfully" : "Teksten ble uttrukket", + "Text extraction disabled" : "Tekstuttrekking deaktivert", + "The URL where webhook events will be sent" : "URL-en der webhook-hendelser vil bli sendt", + "The requested conversation does not exist" : "Den forespurte samtalen finnes ikke", + "The saved view and all its search configuration" : "Den lagrede visningen og all dens søkekonfigurasjon", + "There are no audit trail entries matching your current filters." : "Det finnes ingen revisjonssporoppføringer som samsvarer med de gjeldende filtrene dine.", + "There are no deleted items matching your current filters." : "Det finnes ingen slettede elementer som samsvarer med de gjeldende filtrene dine.", + "There are no search trail entries matching your current filters." : "Det finnes ingen søkesporoppføringer som samsvarer med de gjeldende filtrene dine.", + "There are no webhook log entries matching your filters." : "Det finnes ingen webhook-loggoppføringer som samsvarer med filtrene dine.", + "This action cannot be undone." : "Denne handlingen kan ikke angres.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Denne handlingen kan ikke angres. Sørg for at ingen samlinger bruker denne ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Denne agenten har ingen konfigurerbare visninger eller verktøy, men du kan fortsatt justere RAG-innstillingene nedenfor.", + "This analysis may take some time" : "Denne analysen kan ta litt tid", + "This audit trail entry does not contain any change information." : "Denne revisjonssporoppføringen inneholder ingen endringsinformasjon.", + "This configuration has no data defined." : "Denne konfigurasjonen har ingen data definert.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Dette endepunktet er avviklet. Bruk delbaserte endepunkter i stedet.", + "This entity has no relations to objects or files" : "Denne entiteten har ingen relasjoner til objekter eller filer", + "This is a test webhook from OpenRegister" : "Dette er en testwebhook fra OpenRegister", + "This schema must use Magic Table configuration to sync" : "Dette skjemaet må bruke Magic Table-konfigurasjon for å synkronisere", + "This source has no associated registers." : "Denne kilden har ingen tilknyttede registre.", + "This will copy the _default ConfigSet with the new name" : "Dette vil kopiere _default-ConfigSet med det nye navnet", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Dette vil permanent slette ALLE innbygginger (vektorer) fra databasen. Du vil måtte revektorisere alle objekter og filer. Denne handlingen kan ikke angres.\\n\\nEr du sikker på at du vil fortsette?", + "This will permanently delete:" : "Dette vil permanent slette:", + "Timeout (seconds)" : "Tidsavbrudd (sekunder)", + "Timestamp" : "Tidsstempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tips: Aktiver bare visninger som trenger semantisk søk for å minimere innbyggingskostnader. Enkle oppslagstabeller trenger sjelden vektorisering.", + "Title" : "Tittel", + "To Date" : "Til dato", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "For å justere delstørrelse eller strategi, gå til Filkonfigurasjon → Behandlingsgrenser.", + "To change the embedding provider or model, go to LLM Configuration." : "For å endre innbyggingsleverandøren eller -modellen, gå til LLM-konfigurasjon.", + "To date" : "Til dato", + "Toggle search sidebar" : "Veksle søkesidepanel", + "Tools" : "Verktøy", + "Top Deleters" : "Toppslettere", + "Total" : "Totalt", + "Total Audit Trails" : "Totalt antall revisjonsspor", + "Total Chunks Available:" : "Totalt antall tilgjengelige deler:", + "Total Deleted Items" : "Totalt antall slettede elementer", + "Total Members" : "Totalt antall medlemmer", + "Total Objects" : "Totalt antall objekter", + "Total Objects in Database:" : "Totalt antall objekter i databasen:", + "Total Organisations" : "Totalt antall organisasjoner", + "Total Results" : "Totalt antall resultater", + "Total Searches" : "Totalt antall søk", + "Total Size" : "Total størrelse", + "Total Storage" : "Total lagringsplass", + "Total:" : "Totalt:", + "Totals" : "Totaler", + "Type" : "Type", + "Type Issue" : "Typeproblem", + "Type Variations:" : "Typevariasjoner:", + "Type must be \"positive\" or \"negative\"" : "Type må være \"positive\" eller \"negative\"", + "Type to search for groups" : "Skriv for å søke etter grupper", + "Type to search for users" : "Skriv for å søke etter brukere", + "Type to search groups..." : "Skriv for å søke i grupper ...", + "Type to search users..." : "Skriv for å søke i brukere ...", + "URL" : "URL", + "URL where Ollama is running" : "URL der Ollama kjører", + "UUID:" : "UUID:", + "Unchanged" : "Uendret", + "Unique Search Terms" : "Unike søkeord", + "Unique Terms" : "Unike termer", + "Unknown" : "Ukjent", + "Unknown Register" : "Ukjent register", + "Unknown Schema" : "Ukjent skjema", + "Untitled View" : "Visning uten tittel", + "Update" : "Oppdater", + "Update Operations" : "Oppdater-operasjoner", + "Update configuration: ..." : "Oppdater konfigurasjon: ...", + "Update register OAS: ..." : "Oppdater register-OAS: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Oppdater vektorer når objektdata endres (anbefales for nøyaktig søk)", + "Updated" : "Oppdatert", + "Updated At" : "Oppdatert den", + "Updated:" : "Oppdatert:", + "Uptime:" : "Oppetid:", + "Usage Count" : "Bruksantall", + "Use SolrCloud with Zookeeper for distributed search" : "Bruk SolrCloud med Zookeeper for distribuert søk", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Bruk filtre for å avgrense revisjonssporoppføringer etter register, skjema, handlingstype, bruker, datointervall eller objekt-ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Bruk filtre for å avgrense slettede elementer etter register, skjema, slettedato eller bruker som slettet dem.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Bruk filtre for å avgrense søkesporoppføringer etter register, skjema, suksessstatus, bruker, datointervall, søkeord eller ytelsesmålinger.", + "Used By Collections" : "Brukt av samlinger", + "User" : "Bruker", + "User Agent Statistics" : "Statistikk for brukeragent", + "User Description" : "Brukerbeskrivelse", + "User rate limits cleared successfully" : "Brukerens hastighetsgrenser ble tømt", + "User removed successfully" : "Brukeren ble fjernet", + "User-friendly description shown in forms and help text" : "Brukervennlig beskrivelse som vises i skjemaer og hjelpetekst", + "Username" : "Brukernavn", + "Username for SOLR authentication (optional)" : "Brukernavn for SOLR-autentisering (valgfritt)", + "Users" : "Brukere", + "Using Pre-Generated Chunks" : "Bruker forhåndsgenererte deler", + "Validate" : "Valider", + "Validate Objects" : "Valider objekter", + "Validation completed successfully" : "Valideringen ble fullført", + "Validation failed" : "Validering mislyktes", + "Value" : "Verdi", + "Vector Dimensions" : "Vektordimensjoner", + "Vector Search Backend" : "Vektorsøk-backend", + "Vector field: _embedding_" : "Vektorfelt: _embedding_", + "Vectorization Triggers" : "Vektoriseringsutløsere", + "Vectorize All Files" : "Vektoriser alle filer", + "Vectorize All Objects" : "Vektoriser alle objekter", + "Vectorize all file types" : "Vektoriser alle filtyper", + "Vectorize all views" : "Vektoriser alle visninger", + "Vectorize on object creation" : "Vektoriser ved objektopprettelse", + "Vectors will be stored in your existing object and file collections" : "Vektorer vil bli lagret i de eksisterende objekt- og filsamlingene dine", + "Version" : "Versjon", + "Very High" : "Svært høy", + "View API Docs" : "Vis API-dokumentasjon", + "View Changes" : "Vis endringer", + "View Details" : "Vis detaljer", + "View Error" : "Vis feil", + "View File" : "Vis fil", + "View Full Details" : "Vis alle detaljer", + "View Logs" : "Vis logger", + "View Name" : "Visningsnavn", + "View Object" : "Vis objekt", + "View Parameters" : "Vis parametere", + "View Selection" : "Visningsvalg", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Vis og analyser søkesporlogger med avanserte filtrerings- og analysefunksjoner", + "View and analyze system audit trails with advanced filtering capabilities" : "Vis og analyser systemrevisjonsspor med avanserte filtreringsfunksjoner", + "View deleted successfully" : "Visningen ble slettet", + "View entity details and manage relations" : "Vis entitetsdetaljer og administrer relasjoner", + "View name" : "Visningsnavn", + "View name is required" : "Visningsnavn er påkrevd", + "View saved successfully!" : "Visningen ble lagret!", + "View search analytics and manage search logs" : "Vis søkeanalyse og administrer søkelogger", + "View successfully deleted" : "Visningen ble slettet", + "View successfully updated" : "Visningen ble oppdatert", + "View updated successfully!" : "Visningen ble oppdatert!", + "View webhook delivery logs and filter by webhook" : "Vis webhook-leveringslogger og filtrer etter webhook", + "Views" : "Visninger", + "Visible to users" : "Synlig for brukere", + "Wait for Response" : "Vent på respons", + "Wait for webhook response before continuing (required for request/response flows)" : "Vent på webhook-respons før du fortsetter (påkrevd for forespørsel/respons-flyter)", + "Warmup Names Cache" : "Varm opp navnebuffer", + "Warning" : "Advarsel", + "Warning:" : "Advarsel:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detaljer om webhook-logg", + "Webhook Logs" : "Webhook-logger", + "Webhook created successfully" : "Webhooken ble opprettet", + "Webhook deleted" : "Webhook slettet", + "Webhook retry delivered successfully" : "Nytt forsøk for webhook ble levert", + "Webhook retry delivery failed" : "Levering av nytt forsøk for webhook mislyktes", + "Webhook updated" : "Webhook oppdatert", + "Webhook updated successfully" : "Webhooken ble oppdatert", + "Webhooks" : "Webhooks", + "Weekly" : "Ukentlig", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Pakk webhook-nyttelast i CloudEvents-format for bedre interoperabilitet", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Ja", + "You" : "Du", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Du kan opprette nye ConfigSets basert på _default-malen, eller laste opp tilpassede direkte til SOLR-serveren din.", + "You do not have access to this conversation" : "Du har ikke tilgang til denne samtalen", + "You do not have permission to delete this conversation" : "Du har ikke tillatelse til å slette denne samtalen", + "You do not have permission to modify this conversation" : "Du har ikke tillatelse til å endre denne samtalen", + "You do not have permission to restore this conversation" : "Du har ikke tillatelse til å gjenopprette denne samtalen", + "You must be logged in to favorite views" : "Du må være logget inn for å markere visninger som favoritter", + "You need an AI agent to start a conversation." : "Du trenger en KI-agent for å starte en samtale.", + "Your Fireworks AI API key. Get one at" : "Din Fireworks AI API-nøkkel. Skaff deg en på", + "Your OpenAI API key. Get one at" : "Din OpenAI API-nøkkel. Skaff deg en på", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Tilbakemeldingen din er registrert. Du kan eventuelt oppgi ytterligere detaljer her ...", + "Zookeeper Hosts" : "Zookeeper-verter", + "Zookeeper Port" : "Zookeeper-port", + "Zookeeper Settings (SolrCloud)" : "Zookeeper-innstillinger (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper-tilkoblingsstreng for SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper-portnummer (valgfritt, standard er 2181)", + "chunk" : "del", + "chunks" : "deler", + "chunks processed" : "deler behandlet", + "configuration(s)" : "konfigurasjon(er)", + "e.g., lib/Settings/config.json" : "f.eks. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "f.eks. lib/Settings/register.json", + "failed" : "mislyktes", + "file" : "fil", + "files" : "filer", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "medlemmer", + "menu or contact someone with permission to create agents." : "menyen, eller kontakt noen med tillatelse til å opprette agenter.", + "messages" : "meldinger", + "n8n URL and API key are required" : "n8n-URL og API-nøkkel er påkrevd", + "n8n configuration saved successfully" : "n8n-konfigurasjonen ble lagret", + "n8n connection not configured" : "n8n-tilkobling ikke konfigurert", + "n8n connection successful" : "n8n-tilkobling vellykket", + "n8n connection test successful" : "n8n-tilkoblingstest vellykket", + "n8n integration disabled" : "n8n-integrasjon deaktivert", + "n8n integration enabled" : "n8n-integrasjon aktivert", + "n8n project initialized successfully" : "n8n-prosjektet ble initialisert", + "n8n settings saved successfully" : "n8n-innstillingene ble lagret", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekter", + "objects processed" : "objekter behandlet", + "or pick a range" : "eller velg et intervall", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "gir en god balanse mellom hastighet og nøyaktighet.", + "register(s)" : "register(e)", + "register(s) selected" : "register(e) valgt", + "required" : "påkrevd", + "results" : "resultater", + "schema(s)" : "skjema(er)", + "schema(s) selected" : "skjema(er) valgt", + "sk-..." : "sk-...", + "this application" : "denne applikasjonen", + "to" : "til", + "vectorized" : "vektorisert", + "views selected" : "visninger valgt", + "ℹ️ Current Configuration" : "ℹ️ Gjeldende konfigurasjon", + "⚡ Batch Processing" : "⚡ Partibehandling", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Omfattende kontekst ✓ Mindre sannsynlig å gå glipp av detaljer ✗ Tregere responser ✗ Kan inkludere mindre relevant informasjon", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Raskere responser ✓ Mer fokuserte svar ✗ Kan gå glipp av relevant informasjon", + "✨ AI Features" : "✨ KI-funksjoner", + "All search trails cleared successfully" : "Alle søkespor ble tømt", + "Multiple search trail deletion not implemented yet" : "Sletting av flere søkespor er ikke implementert ennå", + "No expired search trails found to clear" : "Ingen utløpte søkespor funnet å tømme", + "Missing message" : "Mangler melding", + "message content is required" : "meldingsinnhold er påkrevd", + "Missing conversation or agentUuid" : "Mangler samtale eller agentUuid", + "Access denied" : "Tilgang nektet", + "Conversation not found" : "Samtalen ble ikke funnet", + "AI service not configured" : "KI-tjeneste ikke konfigurert", + "Failed to process message" : "Kunne ikke behandle melding", + "Missing conversationId" : "Mangler conversationId", + "conversationId is required" : "conversationId er påkrevd", + "Failed to fetch conversation history" : "Kunne ikke hente samtalehistorikk", + "Failed to clear conversation" : "Kunne ikke tømme samtale", + "Invalid feedback type" : "Ugyldig tilbakemeldingstype", + "type must be \"positive\" or \"negative\"" : "type må være \"positive\" eller \"negative\"", + "Message not found" : "Meldingen ble ikke funnet", + "Failed to save feedback" : "Kunne ikke lagre tilbakemelding", + "Failed to get chat statistics" : "Kunne ikke hente chat-statistikk", + "Not Found" : "Ikke funnet", + "Engine not found" : "Motoren ble ikke funnet", + "Consumer not found" : "Forbrukeren ble ikke funnet", + "Backend parameter is required" : "Backend-parameter er påkrevd", + "Failed to get database information: %s" : "Kunne ikke hente databaseinformasjon: %s", + "SOLR setup error: %s" : "SOLR-oppsettsfeil: %s", + "Reindex failed: %s" : "Reindeksering mislyktes: %s", + "Anonymized" : "Anonymisert", + "Entities detected" : "Entiteter oppdaget", + "Extract Now" : "Trekk ut nå", + "Extracted at" : "Uttrukket den", + "Extraction" : "Uttrekking", + "Failed to load extraction data" : "Kunne ikke laste inn uttrekkingsdata", + "Failed to load register data" : "Kunne ikke laste inn registerdata", + "Failed to retrieve extraction status." : "Kunne ikke hente uttrekkingsstatus.", + "Failed to retrieve objects for file." : "Kunne ikke hente objekter for fil.", + "No extraction data available for this file" : "Ingen uttrekkingsdata tilgjengelig for denne filen", + "No register objects reference this file" : "Ingen registerobjekter refererer til denne filen", + "Not extracted" : "Ikke uttrukket", + "Register Objects" : "Registerobjekter", + "Risk level" : "Risikonivå", + "Text chunks" : "Tekstdeler", + "Unknown error" : "Ukjent feil", + "Very high" : "Svært høy", + "{title} in {register} / {schema}" : "{title} i {register} / {schema}", + "Push Notifications" : "Push-varsler", + "Real-time push notification status via notify_push" : "Status for push-varsler i sanntid via notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push i sanntid ikke tilgjengelig – appen notify_push er ikke installert", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installer appen notify_push fra Nextcloud App Store for å aktivere sanntidsoppdateringer.", + "Open Nextcloud App Store" : "Åpne Nextcloud App Store", + "notify_push is installed but not yet active" : "notify_push er installert, men ikke aktiv ennå", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push er installert, men OpenRegister har ennå ikke bekreftet en vellykket push. Utløs en objektlagring for å aktivere, eller sjekk notify_push-konfigurasjonen din.", + "notify_push configuration guide" : "Konfigurasjonsveiledning for notify_push", + "Realtime push active" : "Push i sanntid aktiv", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push-varsler i sanntid er aktive. Tilkoblede klienter mottar umiddelbare oppdateringer når objekter opprettes, oppdateres eller slettes.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/nb.json b/l10n/nb.json new file mode 100644 index 0000000000..a182bc4654 --- /dev/null +++ b/l10n/nb.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin kreves)", + "(no subject)": "(ingen emne)", + "(no title)": "(ingen tittel)", + "(unnamed)": "(uten navn)", + "(untitled card)": "(kort uten tittel)", + "({count} selected)": "({count} valgt)", + ", {failed} failed": ", {failed} mislyktes", + "0 = unlimited": "0 = ubegrenset", + "A deactivation request is pending.": "En deaktiveringsforespørsel venter.", + "AI Assistant": "AI-assistent", + "AI Chat": "AI-chat", + "AI service not configured": "AI-tjeneste ikke konfigurert", + "API Key": "API-nøkkel", + "API Request Quota (requests/day)": "API-forespørselskvote (forespørsler/dag)", + "API Token Configuration": "API-tokenkonfigurasjon", + "API Tokens": "API-tokener", + "API tokens saved successfully": "API-tokener lagret", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG art. 30 §4: denne rapporten kobler hver behandlingsaktivitet med levetidsrevisjonssporantall tilskrevet den. Revisorer og Autoriteit Persoonsgegevens bruker dette til tilsynsgjennomgang.", + "About ConfigSets": "Om ConfigSets", + "Access denied": "Tilgang nektet", + "Access organisation dashboard and analytics": "Tilgang til organisasjonens dashboard og analyse", + "Account": "Konto", + "Action": "Handling", + "Action Distribution": "Handlingsfordeling", + "Action:": "Handling:", + "Actions": "Handlinger", + "Active": "Aktiv", + "Active Collections": "Aktive samlinger", + "Active Filters": "Aktive filtre", + "Active Organisation:": "Aktiv organisasjon:", + "Active Organisations": "Aktive organisasjoner", + "Active filters:": "Aktive filtre:", + "Active organisation changed successfully": "Aktiv organisasjon endret", + "Active organisation set successfully": "Aktiv organisasjon angitt", + "Activities": "Aktiviteter", + "Activity": "Aktivitet", + "Activity Period": "Aktivitetsperiode", + "Add": "Legg til", + "Add Attachment": "Legg til vedlegg", + "Add Endpoint": "Legg til endepunkt", + "Add Groups": "Legg til grupper", + "Add Property to '{title}'": "Legg til egenskap i '{title}'", + "Add Register": "Legg til register", + "Add Schema": "Legg til skjema", + "Add Source": "Legg til kilde", + "Add User": "Legg til bruker", + "Add User to Organisation": "Legg til bruker i organisasjon", + "Add a contact from any of your address books to associate it with this object.": "Legg til en kontakt fra en av adressebøkene dine for å knytte den til dette objektet.", + "Add a file or files": "Legg til en fil eller filer", + "Add card": "Legg til kort", + "Add contact": "Legg til kontakt", + "Add endpoint": "Legg til endepunkt", + "Add oneOf entry": "Legg til oneOf-oppføring", + "Add range": "Legg til område", + "Add schema titles, descriptions, and register information to provide richer context for search": "Legg til skjematitler, beskrivelser og registerinformasjon for å gi rikere kontekst for søk", + "Add to favorites": "Legg til i favoritter", + "Added": "Lagt til", + "Added to favorites": "Lagt til i favoritter", + "Additional Information": "Tilleggsinformasjon", + "Additional feedback saved. Thank you!": "Tilbakemelding lagret. Takk!", + "Addresses": "Adresser", + "Admin": "Admin", + "Advanced": "Avansert", + "Advanced Filters": "Avanserte filtre", + "Advanced Options": "Avanserte valg", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Avanserte filtre er ikke tilgjengelig ved bruk av databasekilde. Bytt til Auto eller SOLR-indeks for filtreringsalternativer.", + "After": "Etter", + "Agent Type": "Agenttype", + "Agent deleted successfully": "Agent slettet", + "Agent successfully created": "Agent opprettet", + "Agent successfully updated": "Agent oppdatert", + "Agents": "Agenter", + "Aggregated across schemas": "Aggregert på tvers av skjemaer", + "All": "Alle", + "All Categories": "Alle kategorier", + "All Collections": "Alle samlinger", + "All Confidence Levels": "Alle konfidensnivåer", + "All ConfigSets": "Alle ConfigSets", + "All Files": "Alle filer", + "All Levels": "Alle nivåer", + "All Types": "Alle typer", + "All Webhooks": "Alle webhooks", + "All actions": "Alle handlinger", + "All audit trails cleared successfully": "Alle revisjonsspor tømt", + "All clear": "Alt klart", + "All file types": "Alle filtyper", + "All registers": "Alle registre", + "All schemas": "Alle skjemaer", + "All search trails cleared successfully": "Alle søkespor tømt", + "All searches": "Alle søk", + "All users": "Alle brukere", + "All-day events": "Heldagsarrangementer", + "Allowed MIME Types": "Tillatte MIME-typer", + "Already linked": "Allerede koblet", + "An error occurred while clearing audit trails": "En feil oppstod ved tømming av revisjonsspor", + "An error occurred while deleting the audit trail": "En feil oppstod ved sletting av revisjonssporet", + "An error occurred while deleting the object": "En feil oppstod ved sletting av objektet", + "An error occurred while deleting the view": "En feil oppstod ved sletting av visningen", + "An error occurred while permanently deleting the objects": "En feil oppstod ved permanent sletting av objektene", + "An error occurred while restoring the objects": "En feil oppstod ved gjenoppretting av objektene", + "An error occurred while saving the organisation": "En feil oppstod ved lagring av organisasjonen", + "Analysis completed:": "Analyse fullført:", + "Analysis steps:": "Analysetrinn:", + "Analytics": "Analyse", + "Analyze Objects": "Analyser objekter", + "Analyze Properties": "Analyser egenskaper", + "Analyze existing properties for improvement opportunities": "Analyser eksisterende egenskaper for forbedringsmuligheter", + "Analyzing...": "Analyserer...", + "Anonymized": "Anonymisert", + "Anonymous Group": "Anonym gruppe", + "Any favorites and sharing settings for this view": "Eventuelle favoritter og delingsinnstillinger for denne visningen", + "Any user": "Alle brukere", + "App ID": "App-ID", + "App store cache invalidated successfully": "App Store-cache ugyldiggjort", + "App store cache invalidated: {invalidated}": "App Store-cache ugyldiggjort: {invalidated}", + "Application deleted successfully": "Applikasjon slettet", + "Application identifier for this configuration (optional)": "Applikasjonsidentifikator for denne konfigurasjonen (valgfritt)", + "Application settings": "Applikasjonsinnstillinger", + "Application successfully created": "Applikasjon opprettet", + "Application successfully updated": "Applikasjon oppdatert", + "Applications": "Applikasjoner", + "Apply": "Bruk", + "Apply Changes": "Bruk endringer", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Bruk en rolle på alle skjemaer i dette registeret som ikke har eksplisitte autorisasjonsoverstyringer.", + "Approval Progress": "Godkjenningsfremgang", + "Approve": "Godkjenn", + "Approved": "Godkjent", + "Archive": "Arkiver", + "Archive conversation": "Arkiver samtale", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arkivere denne verwerkingsactiviteit? Revisjonssporradene vil fortsatt referere til den.", + "Archived conversations are hidden from your active list": "Arkiverte samtaler er skjult fra den aktive listen", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Er du sikker på at du vil SLETTE samlingen \"{name}\"?\n\nDette vil:\n• Permanent slette samlingen og alle dens data\n• Fjerne alle indekserte dokumenter\n• Denne handlingen kan ikke angres", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Er du sikker på at du vil rydde opp gamle søkespor? Dette vil slette oppføringer eldre enn 30 dager.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Er du sikker på at du vil slette alle data fra samlingen \"{name}\"?\n\nDette vil:\n• Slette alle indekserte dokumenter\n• Beholde samlingsstrukturen intakt\n• Denne handlingen kan ikke angres", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Er du sikker på at du vil slette applikasjonen \"{name}\"? Denne handlingen kan ikke angres.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Er du sikker på at du vil slette de valgte revisjonssporene? Denne handlingen kan ikke angres.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Er du sikker på at du vil slette de valgte søkesporene? Denne handlingen kan ikke angres.", + "Are you sure you want to delete this ConfigSet?": "Er du sikker på at du vil slette denne ConfigSet?", + "Are you sure you want to delete this configset?": "Er du sikker på at du vil slette denne configset?", + "Are you sure you want to leave '{name}'?": "Er du sikker på at du vil forlate '{name}'?", + "Are you sure you want to permanently delete": "Er du sikker på at du vil permanent slette", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Er du sikker på at du vil reindeksere samlingen \"{name}\"?\n\nDette vil:\n• Gjenoppbygge indeksen med alle objekter\n• Ta flere minutter å fullføre\n• Kan påvirke søkeytelsen under reindeksering", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Er du sikker på at du vil fjerne skjemaet \"{schema}\" fra registeret \"{register}\"? Denne handlingen kan ikke angres.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Er du sikker på at du vil myk-slette {count} aktive objekter for skjemaet \"{schema}\"?\n\nDe vil bli merket som slettet, men kan permanent fjernes senere.", + "Array Object Configuration:": "Arrayobjektkonfigurasjon:", + "Articles": "Artikler", + "Ask a question...": "Still et spørsmål...", + "Ask questions about your data using natural language": "Still spørsmål om dataene dine på naturlig språk", + "Assigned collaborative tags": "Tildelte samarbeidsetiketter", + "Assignment notifications": "Tildelingsvarslinger", + "Attachment added to {name}": "Vedlegg lagt til i {name}", + "Attempt": "Forsøk", + "Audit Trail Changes": "Revisjonssporendringer", + "Audit Trail Details": "Revisjonsspordetaljer", + "Audit Trail Management": "Revisjonssporhåndtering", + "Audit Trail Statistics": "Revisjonssporstatistikk", + "Audit Trails": "Revisjonsspor", + "Audit trail data copied to clipboard": "Revisjonsspordata kopiert til utklippstavlen", + "Audit trail deleted successfully": "Revisjonsspor slettet", + "Audit trail successfully deleted": "Revisjonsspor slettet", + "Authentication": "Autentisering", + "Auto (e.g. Y for year, F Y for month)": "Auto (f.eks. Y for år, F Y for måned)", + "Auto share": "Automatisk deling", + "Auto-commit disabled": "Auto-commit deaktivert", + "Auto-commit enabled": "Auto-commit aktivert", + "Auto-create Default Organisation": "Opprett standard organisasjon automatisk", + "Auto-retry failed vectorizations": "Prøv automatisk mislykkede vektoriseringer på nytt", + "Automatically commit changes to SOLR index": "Bekreft automatisk endringer til SOLR-indeksen", + "Automatically connect to the federation network.": "Koble automatisk til føderasjonsnettverket.", + "Automatically create a default organisation if none exists when the app is initialized": "Opprett automatisk en standardorganisasjon hvis ingen finnes når appen initialiseres", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Generer automatisk vektorinnbygginger fra tekstbiter når filer lastes opp og behandles", + "Automatically generate vector embeddings when objects are created or updated": "Generer automatisk vektorinnbygginger når objekter opprettes eller oppdateres", + "Automatically retry failed vectorization attempts (max 3 retries)": "Prøv automatisk mislykkede vektoriseringsforsøk på nytt (maks 3 forsøk)", + "Automatically update catalogues.": "Oppdater kataloger automatisk.", + "Available Filters": "Tilgjengelige filtre", + "Available Workflows": "Tilgjengelige arbeidsflyter", + "Available placeholders:": "Tilgjengelige plassholdere:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Avatarjusteringer støttes ikke av autentiseringsleverandøren din.", + "Avatar removed": "Avatar fjernet", + "Avatar updated successfully": "Avatar oppdatert", + "Avg Execution Time": "Gj.sn. utføringstid", + "Avg Members/Org": "Gj.sn. medlemmer/org", + "Avg Object Views/Session": "Gj.sn. objektvisninger/økt", + "Avg Response Time": "Gj.sn. responstid", + "Avg Results/Search": "Gj.sn. resultater/søk", + "Avg Searches/Session": "Gj.sn. søk/økt", + "Back": "Tilbake", + "Back to Entities": "Tilbake til enheter", + "Back to Registers": "Tilbake til registre", + "Back to Webhooks": "Tilbake til webhooks", + "Back to applications": "Tilbake til applikasjoner", + "Back to entities": "Tilbake til enheter", + "Backend parameter is required": "Backend-parameter er påkrevd", + "Backend updated successfully. Please reload the application.": "Backend oppdatert. Vennligst last inn applikasjonen på nytt.", + "Bandwidth Quota (MB/month)": "Båndbreddekvote (MB/måned)", + "Base URL (Optional)": "Basis-URL (valgfritt)", + "Basic Information": "Grunnleggende informasjon", + "Batch Size": "Batchstørrelse", + "Batch extraction completed": "Batchuttrekking fullført", + "Before": "Før", + "Before object vectorization can work:": "Før objektvektorisering kan fungere:", + "Behavior": "Atferd", + "Behavior Issue": "Atferdsproblem", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 varighet, f.eks. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob-lagring er avviklet. Alle objekter bruker nå magiske tabeller.", + "Bookmarks": "Bokmerker", + "Branch": "Gren", + "Bulk Role Assignment: {title}": "Massetildeling av roller: {title}", + "Bulk delete operation completed successfully": "Masseslettingsoperasjon fullført", + "Bulk save operation completed successfully": "Masselagringsoperasjon fullført", + "Business Data": "Forretningsdata", + "CSV": "CSV", + "Cache cleared successfully": "Cache tømt", + "Cache grew from {old} to {new} entries.": "Cache vokste fra {old} til {new} oppføringer.", + "Cache shrunk from {old} to {new} entries.": "Cache krympet fra {old} til {new} oppføringer.", + "Cache stayed the same at {size} entries.": "Cache forble uendret med {size} oppføringer.", + "Calculate Sizes": "Beregn størrelser", + "Calendar": "Kalender", + "Calendar Provider Configuration": "Kalenderleverandørkonfigurasjon", + "Calendar integration is not available": "Kalenderintegrasjon er ikke tilgjengelig", + "Calendar name": "Kalendernavn", + "Cancel": "Avbryt", + "Cancel deactivation request": "Avbryt deaktiveringsforespørsel", + "Cannot delete: objects are still attached": "Kan ikke slette: objekter er fortsatt tilknyttet", + "Cannot edit: This register is managed by external configuration {title}": "Kan ikke redigere: Dette registeret administreres av ekstern konfigurasjon {title}", + "Cannot remove schema with existing objects ({count} objects)": "Kan ikke fjerne skjema med eksisterende objekter ({count} objekter)", + "Cannot remove schema {schema} because it contains {count} objects": "Kan ikke fjerne skjemaet {schema} fordi det inneholder {count} objekter", + "Cards": "Kort", + "Cascade delete": "Kaskadesletting", + "Categories of data subjects (one per line)": "Kategorier av registrerte (én per linje)", + "Categories of personal data (one per line)": "Kategorier av personopplysninger (én per linje)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategori", + "Change Type": "Endringstype", + "Change password": "Endre passord", + "Changes": "Endringer", + "Changes copied to clipboard": "Endringer kopiert til utklippstavlen", + "Changes for Audit Trail #{id}": "Endringer for revisjonsspor #{id}", + "Chat Model": "Chatmodell", + "Chat Provider": "Chatleverandør", + "Chat Provider (RAG)": "Chatleverandør (RAG)", + "Chat Settings": "Chatinnstillinger", + "Chat provider connection successful!": "Tilkobling til chatleverandør vellykket!", + "Chat settings": "Chatinnstillinger", + "Chat test failed: {error}": "Chattest mislyktes: {error}", + "Choose a register": "Velg et register", + "Choose a schema": "Velg et skjema", + "Choose how vector similarity calculations are performed for semantic search": "Velg hvordan vektorlikhetberegninger utføres for semantisk søk", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Velg hvilke filtyper som skal inkluderes i vektoriseringsprosessen. Bare filer med ekstrahert tekst og biter vil bli behandlet.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Velg hvilke visninger som skal inkluderes i vektoriseringsprosessen. La stå tomt for å behandle alle visninger basert på konfigurasjonen din.", + "Chunk": "Bit", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Sletting av biter er ikke implementert ennå. Bruk bitbaserte endepunkter.", + "Chunks": "Biter", + "Chunks to Vectorize:": "Biter å vektorisere:", + "Cleanup Old Trails": "Rydd opp gamle spor", + "Cleanup completed": "Opprydding fullført", + "Cleanup completed successfully. Deleted {count} entries.": "Opprydding fullført. Slettet {count} oppføringer.", + "Cleanup failed: {error}": "Opprydding mislyktes: {error}", + "Clear All Embeddings": "Tøm alle innbygginger", + "Clear Entries": "Tøm oppføringer", + "Clear Filtered Audit Trails": "Tøm filtrerte revisjonsspor", + "Clear Filters": "Tøm filtre", + "Clear Index": "Tøm indeks", + "Clear Selection": "Tøm utvalg", + "Clear all": "Tøm alle", + "Clear all filters": "Tøm alle filtre", + "Clear date range": "Tøm datoområde", + "Clear filters": "Tøm filtre", + "Clearing...": "Tømmer...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Klikk på Kjør samsvarsskanning for å finne skjemaer der personopplysninger er oppdaget men ingen behandlingsaktivitetsannotering finnes.", + "Click to subscribe to notifications": "Klikk for å abonnere på varsler", + "Click to unsubscribe from notifications": "Klikk for å avslutte abonnement på varsler", + "Close": "Lukk", + "Cluster name": "Klusternavn", + "Code": "Kode", + "Code (short readable key, e.g. v-2026-001)": "Kode (kort lesbar nøkkel, f.eks. v-2026-001)", + "Collapse details": "Skjul detaljer", + "Collection Management": "Samlingsadministrasjon", + "Collection Name": "Samlingsnavn", + "Collection assignments updated successfully": "Samlingstilordninger oppdatert", + "Collection cleared successfully": "Samling tømt", + "Collection copied successfully": "Samling kopiert", + "Collection created successfully": "Samling opprettet", + "Collection deleted successfully": "Samling slettet", + "Collection used to store and index file metadata and content": "Samling brukt til å lagre og indeksere filmetadata og innhold", + "Collection used to store and index object data": "Samling brukt til å lagre og indeksere objektdata", + "Collections:": "Samlinger:", + "Color": "Farge", + "Comment...": "Kommentar...", + "Commit Message": "Commit-melding", + "Commit Within (ms)": "Commit innen (ms)", + "Commit message": "Commit-melding", + "Compare current schema with real object data": "Sammenlign gjeldende skjema med reelle objektdata", + "Completed": "Fullført", + "Complex": "Kompleks", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Komplekse spørringer: Avanserte søk med flere filtre, operatorer og komplekse parameterkombnasjoner", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Komplekse spørringer: avanserte søk med flere filtre, operatorer og komplekse parameterkombnasjoner", + "Compliance": "Samsvar", + "Confidence Level": "Konfidensnivå", + "Confidence Score": "Konfidenspoeng", + "Confidence: {confidence}%": "Konfidens: {confidence}%", + "Config must be provided as an object": "Konfigurasjon må angis som et objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet-administrasjon", + "ConfigSet Name": "ConfigSet-navn", + "ConfigSet created successfully": "ConfigSet opprettet", + "ConfigSet deleted successfully": "ConfigSet slettet", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets definerer skjemaet og konfigurasjonen for SOLR-samlingene dine. De inneholder feltdefinisjoner, analysatorer og andre søkeinnstillinger.", + "Configuration": "Konfigurasjon", + "Configuration Data": "Konfigurasjonsdata", + "Configuration is up to date": "Konfigurasjonen er oppdatert", + "Configuration saved": "Konfigurasjon lagret", + "Configuration saved successfully.": "Konfigurasjon lagret.", + "Configuration type (default, application, etc.)": "Konfigurasjonstype (standard, applikasjon osv.)", + "Configurations": "Konfigurasjoner", + "Configure API tokens for external service integrations": "Konfigurer API-tokener for ekstern tjenesteintegrasjon", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurer Apache SOLR søkemotor for avanserte søkefunksjoner", + "Configure Facets": "Konfigurer fasetter", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurer store språkmodell-leverandører (LLM) for AI-drevne funksjoner inkludert semantisk søk, innbygginger og chat.", + "Configure SOLR Facets": "Konfigurer SOLR-fasetter", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurer grunnleggende tilkoblingsinnstillinger for SOLR-serveren din inkludert autentisering og nettverksalternativer. Bruk de separate ConfigSet- og samlingsadministrasjonsdialogene for å administrere kjerner og samlinger.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurer grunnleggende tilkoblingsinnstillinger for SOLR-serveren din inkludert autentisering og nettverksalternativer. Bruk de separate configset- og samlingsadministrasjonsdialogene for å administrere kjerner og samlinger.", + "Configure default organisation and organisation-related settings": "Konfigurer standardorganisasjon og organisasjonsrelaterte innstillinger", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurer hvordan databaseobjekter konverteres til vektorinnbygginger for semantisk søk. Objekter vektoriseres direkte uten behov for tekstuttrekking.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurer hvordan objekter konverteres til tekst før vektorisering. Disse innstillingene påvirker søkekvalitet og kontekst.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurer store språkmodell-leverandører (LLM) for AI-drevne funksjoner inkludert semantisk søk, innbygginger og chat.", + "Configure multi-organization support and tenant isolation": "Konfigurer støtte for flere organisasjoner og leietakerisolasjon", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurer parametere for filovektorisering. Denne prosessen vil generere vektorinnbygginger for alle ekstraherte filbiter.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurer parametere for objektvektorisering. Denne prosessen vil generere vektorinnbygginger for alle objekter som samsvarer med visningsfiltrene dine.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurer dette skjemaet for å vise objekter som hendelser i Nextcloud Kalender-appen.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurer hvilke datatyper som skal søkes og hvor mange kilder som skal hentes", + "Configuring field mappings...": "Konfigurerer felttilordninger...", + "Confirm Account Deactivation": "Bekreft kontodeaktivering", + "Confirm Object Deletion": "Bekreft objektsletting", + "Confirm Object Restoration": "Bekreft objektgjenoppretting", + "Confirm Permanent Object Deletion": "Bekreft permanent objektsletting", + "Confirm deactivation": "Bekreft deaktivering", + "Connecting to SOLR cluster...": "Kobler til SOLR-kluster...", + "Connection Failed": "Tilkobling mislyktes", + "Connection Settings": "Tilkoblingsinnstillinger", + "Connection Successful!": "Tilkobling vellykket!", + "Connection failed": "Tilkobling mislyktes", + "Connection protocol": "Tilkoblingsprotokoll", + "Connection successful": "Tilkobling vellykket", + "Connection timeout in seconds": "Tilkoblingstidsavbrudd i sekunder", + "Constant delay between retries (5 minutes)": "Konstant forsinkelse mellom forsøk (5 minutter)", + "Constraint Issue": "Begrensningsproblem", + "Consumer not found": "Forbruker ikke funnet", + "Contacts": "Kontakter", + "Contacts integration is not available": "Kontaktintegrasjon er ikke tilgjengelig", + "Content Filters": "Innholdsfiltre", + "Continue to Properties": "Fortsett til egenskaper", + "Contracts": "Kontrakter", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontroller hvilke objektvisninger som skal vektoriseres for å redusere API-kostnader. Vektoriser bare visninger som drar nytte av semantisk søk.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontroller hvilke visninger og verktøy AI-en kan bruke i denne samtalen. Som standard er alle agentfunksjoner aktivert.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontrollerer tilfeldighet (0 = deterministisk, 2 = svært kreativ)", + "Conversation ID is required": "Samtale-ID er påkrevd", + "Conversation archived": "Samtale arkivert", + "Conversation archived successfully": "Samtale arkivert", + "Conversation cleared successfully": "Samtale tømt", + "Conversation deleted": "Samtale slettet", + "Conversation not found": "Samtale ikke funnet", + "Conversation permanently deleted": "Samtale permanent slettet", + "Conversation renamed": "Samtale omdøpt", + "Conversation restored": "Samtale gjenopprettet", + "Conversation started with {agent}": "Samtale startet med {agent}", + "Conversation title": "Samtaletittel", + "Conversations": "Samtaler", + "Converting {schema} to blob storage...": "Konverterer {schema} til blob-lagring...", + "Converting {schema} to magic table...": "Konverterer {schema} til magisk tabell...", + "Copied!": "Kopiert!", + "Copy": "Kopier", + "Copy Changes": "Kopier endringer", + "Copy Collection": "Kopier samling", + "Copy Data": "Kopier data", + "Copy Full Data": "Kopier alle data", + "Copy of {name}": "Kopi av {name}", + "Copy to clipboard": "Kopier til utklippstavlen", + "Copying...": "Kopierer...", + "Costs": "Kostnader", + "Count": "Antall", + "Counting objects...": "Teller objekter...", + "Create": "Opprett", + "Create API Token": "Opprett API-token", + "Create Agent": "Opprett agent", + "Create Application": "Opprett applikasjon", + "Create Collection": "Opprett samling", + "Create ConfigSet": "Opprett ConfigSet", + "Create New Collection": "Opprett ny samling", + "Create New ConfigSet": "Opprett ny ConfigSet", + "Create Operations": "Opprettelsesoperasjoner", + "Create Organisation": "Opprett organisasjon", + "Create Webhook": "Opprett webhook", + "Create a copy of collection:": "Opprett en kopi av samling:", + "Create a new ConfigSet based on the _default template": "Opprett en ny ConfigSet basert på _default-malen", + "Create a new SOLR collection from an existing ConfigSet": "Opprett en ny SOLR-samling fra en eksisterende ConfigSet", + "Create a new SOLR collection from an existing configset": "Opprett en ny SOLR-samling fra en eksisterende configset", + "Create a new configset based on the _default template": "Opprett en ny configset basert på _default-malen", + "Create a new event or link an existing one from any of your calendars.": "Opprett en ny hendelse eller koble til en eksisterende fra en av kalenderene dine.", + "Create another": "Opprett en til", + "Create copies of": "Opprett kopier av", + "Create event": "Opprett hendelse", + "Create new token": "Opprett nytt token", + "Create or link a Deck card to track work on this object.": "Opprett eller koble til et Deck-kort for å spore arbeid på dette objektet.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Opprett den første verwerkingsactiviteit for å begynne å merke revisjonssporradene med AVG art. 30-attribusjon.", + "Create your first AI agent to get started.": "Opprett din første AI-agent for å komme i gang.", + "Created": "Opprettet", + "Created:": "Opprettet:", + "Creating search collections...": "Oppretter søkesamlinger...", + "Creating...": "Oppretter...", + "Current": "Gjeldende", + "Current Filters": "Gjeldende filtre", + "Current Type:": "Gjeldende type:", + "Current password": "Gjeldende passord", + "Custom API endpoint if using a different region": "Egendefinert API-endepunkt ved bruk av en annen region", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Egendefinerte HTTP-hoder (én per linje, format: Header-Name: verdi)", + "Custom HTTP headers (one per line, format: header-name: value)": "Egendefinerte HTTP-hoder (én per linje, format: header-name: verdi)", + "Custom Ranges:": "Egendefinerte områder:", + "Custom display title for this facet": "Egendefinert visningstitel for denne fasetten", + "Custom pattern:": "Egendefinert mønster:", + "DELETE COLLECTION": "SLETT SAMLING", + "DSAR": "DSAR", + "Daily": "Daglig", + "Dashboard": "Dashboard", + "Dashboard not found": "Dashboard ikke funnet", + "Data": "Data", + "Data Source": "Datakilde", + "Data Sources": "Datakilder", + "Data Views": "Datavisninger", + "Data sources": "Datakilder", + "Data type does not match observed values": "Datatype samsvarer ikke med observerte verdier", + "Data-subject access request": "Registrertes tilgangsforespørsel", + "Database URL": "Database-URL", + "Database information refreshed": "Databaseinformasjon oppdatert", + "Date Faceting:": "Datofasettering:", + "Date Range": "Datoområde", + "Dates": "Datoer", + "Deactivation request cancelled": "Deaktiveringsforespørsel avbrutt", + "Deactivation request submitted": "Deaktiveringsforespørsel sendt", + "Deck": "Deck", + "Deck integration is not available": "Deck-integrasjon er ikke tilgjengelig", + "Default": "Standard", + "Default Object Tenant": "Standard objektleietaker", + "Default Organisation": "Standardorganisasjon", + "Default Owner": "Standardeier", + "Default User (for cron/background jobs)": "Standardbruker (for cron/bakgrunnsjobber)", + "Default User Tenant": "Standard brukerleietaker", + "Default value": "Standardverdi", + "Default value (Email)": "Standardverdi (e-post)", + "Default value (Password)": "Standardverdi (passord)", + "Default value (Phone number)": "Standardverdi (telefonnummer)", + "Default value (Regex)": "Standardverdi (regulært uttrykk)", + "Default: 5": "Standard: 5", + "Defaults to current user. Select a different user if needed.": "Standard er gjeldende bruker. Velg en annen bruker ved behov.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Forsinkelsene dobles med hvert forsøk (2, 4, 8 minutter...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Forsinkelsene øker lineært (5, 10, 15 minutter...)", + "Delete": "Slett", + "Delete ({count})": "Slett ({count})", + "Delete Application": "Slett applikasjon", + "Delete Audit Trail": "Slett revisjonsspor", + "Delete Collection": "Slett samling", + "Delete ConfigSet": "Slett ConfigSet", + "Delete Objects": "Slett objekter", + "Delete Operations": "Slettingsoperasjoner", + "Delete SOLR Collection": "Slett SOLR-samling", + "Delete View": "Slett visning", + "Delete all objects in this schema": "Slett alle objekter i dette skjemaet", + "Delete permanently": "Slett permanent", + "Delete this conversation?": "Slette denne samtalen?", + "Delete view": "Slett visning", + "Delete {count} object": "Slett {count} objekt", + "Delete {count} objects": "Slett {count} objekter", + "Deleted": "Slettet", + "Deleted By": "Slettet av", + "Deleted Date": "Slettedato", + "Deleted Items Management": "Administrasjon av slettede elementer", + "Deleted This Week": "Slettet denne uken", + "Deleted Today": "Slettet i dag", + "Deleted:": "Slettet:", + "Deleting SOLR Collection...": "Sletter SOLR-samling...", + "Deleting...": "Sletter...", + "Deletion Date Range": "Datoområde for sletting", + "Deletion Statistics": "Slettingsstatistikk", + "Deprecated": "Avviklet", + "Depublish": "Avpubliser", + "Description": "Beskrivelse", + "Description Template": "Beskrivelsemal", + "Description shown as tooltip": "Beskrivelse vist som verktøytips", + "Description:": "Beskrivelse:", + "Detect data types and patterns": "Oppdag datatyper og mønstre", + "Detected At": "Oppdaget ved", + "Detected Format:": "Oppdaget format:", + "Detected Issues:": "Oppdagede problemer:", + "Detected: {date}": "Oppdaget: {date}", + "Direct permission": "Direkte tillatelse", + "Disable": "Deaktiver", + "Disable this to make the agent": "Deaktiver dette for å gjøre agenten", + "Disabled": "Deaktivert", + "Discover Files": "Oppdag filer", + "Discovered Properties": "Oppdagede egenskaper", + "Display Format": "Visningsformat", + "Display Name": "Visningsnavn", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Vil du permanent slette alle filtrerte revisjonssporoppføringer? Denne handlingen kan ikke angres.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Vil du permanent slette denne revisjonssporoppføringen? Denne handlingen kan ikke angres.", + "Documents": "Dokumenter", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR aktivert", + "Download API Spec": "Last ned API-spesifikasjon", + "Download API Specification": "Last ned API-spesifikasjon", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Last ned en kopi av alle personopplysningene dine lagret i OpenRegister (GDPR artikkel 20).", + "Drag a file or files here": "Dra en fil eller filer hit", + "Dry run completed: {count} fields would be created": "Tørrkjøring fullført: {count} felt ville bli opprettet", + "Dry run completed: {count} fields would be fixed": "Tørrkjøring fullført: {count} felt ville bli rettet", + "Duration in seconds (optional)": "Varighet i sekunder (valgfritt)", + "Edit": "Rediger", + "Edit Agent": "Rediger agent", + "Edit Application": "Rediger applikasjon", + "Edit Configuration": "Rediger konfigurasjon", + "Edit Endpoint": "Rediger endepunkt", + "Edit Organisation": "Rediger organisasjon", + "Edit Property '{key}' of '{title}'": "Rediger egenskap '{key}' til '{title}'", + "Edit Register": "Rediger register", + "Edit Schema": "Rediger skjema", + "Edit Source": "Rediger kilde", + "Edit View": "Rediger visning", + "Edit Webhook": "Rediger webhook", + "Edit labels": "Rediger etiketter", + "Edit processing activity": "Rediger behandlingsaktivitet", + "Edit tags for {name}": "Rediger etiketter for {name}", + "Edit verwerkingsactiviteit": "Rediger verwerkingsactiviteit", + "Edit view": "Rediger visning", + "Edit view details": "Rediger visningsdetaljer", + "Effectiveness": "Effektivitet", + "Email": "E-post", + "Email Addresses": "E-postadresser", + "Email digest frequency": "Frekvens for e-postsammendrag", + "Emails": "E-poster", + "Embedding Model": "Innbyggingsmodell", + "Embedding Provider": "Innbyggingsleverandør", + "Embedding generated successfully": "Innbyggingen generert", + "Embedding provider connection successful!": "Tilkobling til innbyggingsleverandør vellykket!", + "Embedding test failed: {error}": "Innbyggingstest mislyktes: {error}", + "Enable": "Aktiver", + "Enable RAG": "Aktiver RAG", + "Enable auto-creation": "Aktiver automatisk opprettelse", + "Enable automatic file vectorization": "Aktiver automatisk filovektorisering", + "Enable automatic object vectorization": "Aktiver automatisk objektvektorisering", + "Enable automatic synchronization": "Aktiver automatisk synkronisering", + "Enable calendar provider": "Aktiver kalenderleverandør", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Aktiver detaljert logging for SOLR-operasjoner (anbefalt for feilsøking)", + "Enable faceting": "Aktiver fasettering", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Aktiver eller deaktiver LLM-funksjoner. Konfigurer leverandører og modeller ved hjelp av LLM-konfigurasjonsknappen ovenfor.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Aktiver eller deaktiver LLM-funksjoner. Konfigurer leverandører og modeller ved hjelp av LLM-konfigurasjonsknappen ovenfor.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Aktiver eller deaktiver SOLR-søkeintegrasjon. Konfigurer tilkoblingsinnstillinger ved hjelp av Tilkoblingsinnstillinger-knappen ovenfor.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Aktiver eller deaktiver SOLR-søkeintegrasjon. Konfigurer tilkoblingsinnstillinger ved hjelp av tilkoblingsinnstillinger-knappen ovenfor.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Aktiver eller deaktiver n8n arbeidsflytintegrasjon. Konfigurer tilkoblingsinnstillinger nedenfor.", + "Enable or disable this webhook": "Aktiver eller deaktiver denne webhooken", + "Enable tools that allow the agent to interact with data through function calling.": "Aktiver verktøy som lar agenten samhandle med data gjennom funksjonskall.", + "Enable vectorization for all existing and future views (may increase costs)": "Aktiver vektorisering for alle eksisterende og fremtidige visninger (kan øke kostnader)", + "Enable write-back to target objects": "Aktiver tilbakeskrivning til målobjekter", + "Enabled": "Aktivert", + "End Date Field": "Sluttdatofelt", + "Endpoint Path*": "Endepunktsti*", + "Endpoints": "Endepunkter", + "Engine not found": "Motor ikke funnet", + "Enter ConfigSet name": "Skriv inn ConfigSet-navn", + "Enter Nextcloud usernames to grant access to this private agent": "Skriv inn Nextcloud-brukernavn for å gi tilgang til denne private agenten", + "Enter agent description (optional)": "Skriv inn agentbeskrivelse (valgfritt)", + "Enter agent name": "Skriv inn agentnavn", + "Enter application description (optional)": "Skriv inn applikasjonsbeskrivelse (valgfritt)", + "Enter application name": "Skriv inn applikasjonsnavn", + "Enter collection name": "Skriv inn samlingsnavn", + "Enter configuration description (optional)": "Skriv inn konfigurasjonsbeskrivelse (valgfritt)", + "Enter configuration title": "Skriv inn konfigurasjonstittel", + "Enter description (optional)...": "Skriv inn beskrivelse (valgfritt)...", + "Enter new collection name": "Skriv inn nytt samlingsnavn", + "Enter object ID": "Skriv inn objekt-ID", + "Enter organisation description (optional)": "Skriv inn organisasjonsbeskrivelse (valgfritt)", + "Enter organisation name": "Skriv inn organisasjonsnavn", + "Enter search term": "Skriv inn søketerm", + "Enter search terms or leave empty to browse all": "Skriv inn søketermer eller la stå tomt for å bla gjennom alle", + "Enter system prompt for the agent": "Skriv inn systemprompt for agenten", + "Enter username and press Enter": "Skriv inn brukernavn og trykk Enter", + "Enter view name...": "Skriv inn visningsnavn...", + "Enter webhook name": "Skriv inn webhook-navn", + "Enter your API key": "Skriv inn API-nøkkelen din", + "Enter your object here...": "Skriv inn objektet ditt her...", + "Enter your schema here...": "Skriv inn skjemaet ditt her...", + "Entities": "Enheter", + "Entities detected": "Enheter oppdaget", + "Entity ID: {id}": "Enhets-ID: {id}", + "Entity Information": "Enhetsinformasjon", + "Entity Recognition": "Enhetsgjenkjenning", + "Entity Recognition Method": "Metode for enhetsgjenkjenning", + "Entity deleted successfully": "Enhet slettet", + "Entity not found": "Enhet ikke funnet", + "Entries to be deleted:": "Oppføringer som skal slettes:", + "Enum Issue": "Enumereringsproblem", + "Enum constraint is missing": "Enumereringsbegrensning mangler", + "Erase (Art 17)": "Slett (art. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Slette {count} objekt(er) for dette emnet? Denne handlingen logges i revisjonssporet.", + "Erasure complete": "Sletting fullført", + "Erasure preview": "Forhåndsvisning av sletting", + "Error": "Feil", + "Error Details": "Feildetaljer", + "Error Information": "Feilinformasjon", + "Error Message": "Feilmelding", + "Error deleting audit trails: {error}": "Feil ved sletting av revisjonsspor: {error}", + "Error deleting search trails: {error}": "Feil ved sletting av søkespor: {error}", + "Error loading application": "Feil ved lasting av applikasjon", + "Error loading audit trails": "Feil ved lasting av revisjonsspor", + "Error loading entity": "Feil ved lasting av enhet", + "Error loading search trails": "Feil ved lasting av søkespor", + "Estimated Batches:": "Estimerte batcher:", + "Estimated Cost:": "Estimert kostnad:", + "Estimated Duration:": "Estimert varighet:", + "Event": "Hendelse", + "Event Property for Payload": "Hendelsesegensskap for nyttelast", + "Events": "Hendelser", + "Every schema with detected PII has a processing-activity annotation.": "Hvert skjema med oppdagede personopplysninger har en behandlingsaktivitetsannotering.", + "Example": "Eksempel", + "Example Value": "Eksempelverdi", + "Example value for this property": "Eksempelverdi for denne egenskapen", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Eksklusivt maksimum", + "Exclusive Minimum": "Eksklusivt minimum", + "Exclusive maximum": "Eksklusivt maksimum", + "Exclusive minimum": "Eksklusivt minimum", + "Execution Mode": "Utførelsesmodus", + "Execution Time": "Utførelsestid", + "Execution Time Range": "Utførelsestidsområde", + "Existing Improvements": "Eksisterende forbedringer", + "Expand details": "Vis detaljer", + "Expiration": "Utløp", + "Expires": "Utløper", + "Expires in (e.g., 90d)": "Utløper om (f.eks. 90d)", + "Exponential": "Eksponentiell", + "Export": "Eksporter", + "Export \"{schema}\" objects from \"{register}\"": "Eksporter \"{schema}\"-objekter fra \"{register}\"", + "Export Configuration": "Eksporterkonfigurasjon", + "Export Format:": "Eksportformat:", + "Export Objects": "Eksporter objekter", + "Export completed successfully": "Eksport fullført", + "Export downloaded successfully": "Eksport lastet ned", + "Export failed: {error}": "Eksport mislyktes: {error}", + "Export is rate limited. Please try again later.": "Eksport er hastighetsbegrenset. Vennligst prøv igjen senere.", + "Export my data": "Eksporter dataene mine", + "Export, view, or delete audit trails": "Eksporter, vis eller slett revisjonsspor", + "Exporting...": "Eksporterer...", + "Extend Schema": "Utvid skjema", + "Extended": "Utvidet", + "Extensions": "Utvidelser", + "Extra Query Parameters": "Ekstra søkeparametere", + "Extract Now": "Uttrekk nå", + "Extract Pending Files": "Uttrekk ventende filer", + "Extract now": "Uttrekk nå", + "Extract properties from each object": "Uttrekk egenskaper fra hvert objekt", + "Extracted At": "Uttrekket ved", + "Extracted at": "Uttrekket ved", + "Extraction": "Uttrekking", + "Extraction Mode": "Uttrektingsmodus", + "Extraction Scope": "Uttrektingsomfang", + "Extraction Status": "Uttrektingsstatus", + "Extraction error for {file}: {error}": "Uttrekkingsfeil for {file}: {error}", + "Facet Description": "Fasettbeskrivelse", + "Facet Order": "Fasettrekkefølge", + "Facet Title": "Fasetttittel", + "Facet Type": "Fasetttype", + "Facet configuration updated successfully": "Fasettkonfigurasjon oppdatert", + "Facetable": "Fasettbar", + "Faceting Configuration:": "Fasetteringskonfigurasjon:", + "Facets discovered and configured successfully": "Fasetter oppdaget og konfigurert", + "Facets discovered successfully": "Fasetter oppdaget", + "Failed": "Mislyktes", + "Failed to add attachment to object": "Kunne ikke legge til vedlegg i objekt", + "Failed to add user to organisation": "Kunne ikke legge til bruker i organisasjon", + "Failed to analyze schema properties": "Kunne ikke analysere skjegmaegenskaper", + "Failed to archive conversation": "Kunne ikke arkivere samtale", + "Failed to calculate sizes": "Kunne ikke beregne størrelser", + "Failed to cancel deactivation": "Kunne ikke avbryte deaktivering", + "Failed to change active organisation: {error}": "Kunne ikke endre aktiv organisasjon: {error}", + "Failed to change password": "Kunne ikke endre passord", + "Failed to check version: {error}": "Kunne ikke sjekke versjon: {error}", + "Failed to clear audit trails: {error}": "Kunne ikke tømme revisjonsspor: {error}", + "Failed to clear blob objects: {error}": "Kunne ikke tømme blob-objekter: {error}", + "Failed to clear cache: {error}": "Kunne ikke tømme cache: {error}", + "Failed to clear collection": "Kunne ikke tømme samling", + "Failed to clear conversation": "Kunne ikke tømme samtale", + "Failed to clear embeddings: {error}": "Kunne ikke tømme innbygginger: {error}", + "Failed to clear search trails: {error}": "Kunne ikke tømme søkespor: {error}", + "Failed to clear {type} cache: {error}": "Kunne ikke tømme {type}-cache: {error}", + "Failed to copy changes": "Kunne ikke kopiere endringer", + "Failed to copy data": "Kunne ikke kopiere data", + "Failed to copy data to clipboard": "Kunne ikke kopiere data til utklippstavlen", + "Failed to copy to clipboard": "Kunne ikke kopiere til utklippstavlen", + "Failed to copy token": "Kunne ikke kopiere token", + "Failed to create conversation": "Kunne ikke opprette samtale", + "Failed to create missing SOLR fields: {error}": "Kunne ikke opprette manglende SOLR-felt: {error}", + "Failed to create or find project": "Kunne ikke opprette eller finne prosjekt", + "Failed to create token": "Kunne ikke opprette token", + "Failed to delete SOLR collection: {error}": "Kunne ikke slette SOLR-samling: {error}", + "Failed to delete collection": "Kunne ikke slette samling", + "Failed to delete conversation": "Kunne ikke slette samtale", + "Failed to delete objects for {schema}: {error}": "Kunne ikke slette objekter for {schema}: {error}", + "Failed to delete webhook": "Kunne ikke slette webhook", + "Failed to depublish register: {error}": "Kunne ikke avpublisere register: {error}", + "Failed to depublish schema: {error}": "Kunne ikke avpublisere skjema: {error}", + "Failed to depublish: {error}": "Kunne ikke avpublisere: {error}", + "Failed to discover files: {error}": "Kunne ikke oppdage filer: {error}", + "Failed to download API specification": "Kunne ikke laste ned API-spesifikasjon", + "Failed to export data": "Kunne ikke eksportere data", + "Failed to export objects": "Kunne ikke eksportere objekter", + "Failed to fetch conversation history": "Kunne ikke hente samtalhistorikk", + "Failed to fix mismatched SOLR fields: {error}": "Kunne ikke rette uoverensstemmende SOLR-felt: {error}", + "Failed to get SOLR field configuration": "Kunne ikke hente SOLR-feltkonfigurasjon", + "Failed to get chat statistics": "Kunne ikke hente chatstatistikk", + "Failed to get database information: %s": "Kunne ikke hente databaseinformasjon: %s", + "Failed to get entity categories": "Kunne ikke hente enhetskategorier", + "Failed to get entity statistics": "Kunne ikke hente enhetsstatistikk", + "Failed to get entity types": "Kunne ikke hente enhetstyper", + "Failed to get response: {error}": "Kunne ikke hente svar: {error}", + "Failed to invalidate app store cache: {error}": "Kunne ikke ugyldiggjøre App Store-cache: {error}", + "Failed to leave organisation: {error}": "Kunne ikke forlate organisasjon: {error}", + "Failed to link object": "Kunne ikke koble til objekt", + "Failed to load Deck cards": "Kunne ikke laste inn Deck-kort", + "Failed to load LLM configuration": "Kunne ikke laste inn LLM-konfigurasjon", + "Failed to load Nextcloud groups": "Kunne ikke laste inn Nextcloud-grupper", + "Failed to load cache statistics: {error}": "Kunne ikke laste inn cachestatistikk: {error}", + "Failed to load conversation": "Kunne ikke laste inn samtale", + "Failed to load entities": "Kunne ikke laste inn enheter", + "Failed to load entity": "Kunne ikke laste inn enhet", + "Failed to load extraction data": "Kunde ikke laste inn uttrekkingsdata", + "Failed to load files": "Kunne ikke laste inn filer", + "Failed to load linked contacts": "Kunne ikke laste inn koblede kontakter", + "Failed to load linked emails": "Kunne ikke laste inn koblede e-poster", + "Failed to load linked events": "Kunne ikke laste inn koblede hendelser", + "Failed to load organisations": "Kunne ikke laste inn organisasjoner", + "Failed to load register data": "Kunne ikke laste inn registerdata", + "Failed to load related entities": "Kunne ikke laste inn relaterte enheter", + "Failed to load settings: {error}": "Kunne ikke laste inn innstillinger: {error}", + "Failed to load statistics: {error}": "Kunne ikke laste inn statistikk: {error}", + "Failed to load templates": "Kunne ikke laste inn maler", + "Failed to load view: {error}": "Kunne ikke laste inn visning: {error}", + "Failed to load webhooks": "Kunne ikke laste inn webhooks", + "Failed to load workflows": "Kunne ikke laste inn arbeidsflyter", + "Failed to process message": "Kunne ikke behandle melding", + "Failed to publish register: {error}": "Kunne ikke publisere register: {error}", + "Failed to publish schema: {error}": "Kunne ikke publisere skjema: {error}", + "Failed to publish: {error}": "Kunne ikke publisere: {error}", + "Failed to refresh database information": "Kunne ikke oppdatere databaseinformasjon", + "Failed to reindex collection": "Kunne ikke reindeksere samling", + "Failed to remove avatar": "Kunne ikke fjerne avatar", + "Failed to remove link": "Kunne ikke fjerne kobling", + "Failed to remove member: {error}": "Kunne ikke fjerne medlem: {error}", + "Failed to remove schema {schema}: {error}": "Kunne ikke fjerne skjema {schema}: {error}", + "Failed to remove user from organisation": "Kunne ikke fjerne bruker fra organisasjon", + "Failed to rename conversation": "Kunne ikke omdøpe samtale", + "Failed to request deactivation": "Kunne ikke be om deaktivering", + "Failed to restore conversation": "Kunne ikke gjenopprette samtale", + "Failed to retrieve extraction status.": "Kunne ikke hente uttrekkingsstatus.", + "Failed to retrieve objects for file.": "Kunne ikke hente objekter for fil.", + "Failed to retry extraction": "Kunne ikke prøve uttrekking på nytt", + "Failed to retry webhook": "Kunne ikke prøve webhook på nytt", + "Failed to revoke token": "Kunne ikke tilbakekalle token", + "Failed to save GitHub token": "Kunne ikke lagre GitHub-token", + "Failed to save GitLab URL": "Kunne ikke lagre GitLab-URL", + "Failed to save GitLab token": "Kunne ikke lagre GitLab-token", + "Failed to save LLM settings: {error}": "Kunne ikke lagre LLM-innstillinger: {error}", + "Failed to save additional feedback": "Kunne ikke lagre tilleggstilbakemelding", + "Failed to save configuration.": "Kunne ikke lagre konfigurasjon.", + "Failed to save configuration: {error}": "Kunne ikke lagre konfigurasjon: {error}", + "Failed to save facet configuration": "Kunne ikke lagre fasettkonfigurasjon", + "Failed to save feedback": "Kunne ikke lagre tilbakemelding", + "Failed to save file settings: {error}": "Kunne ikke lagre filinnstillinger: {error}", + "Failed to save n8n configuration": "Kunne ikke lagre n8n-konfigurasjon", + "Failed to save preferences": "Kunne ikke lagre preferanser", + "Failed to save roles": "Kunne ikke lagre roller", + "Failed to save settings": "Kunne ikke lagre innstillinger", + "Failed to save settings: {error}": "Kunne ikke lagre innstillinger: {error}", + "Failed to save view: {error}": "Kunne ikke lagre visning: {error}", + "Failed to save warmup interval: {error}": "Kunne ikke lagre oppvarmingsintervall: {error}", + "Failed to save webhook": "Kunne ikke lagre webhook", + "Failed to send feedback": "Kunne ikke sende tilbakemelding", + "Failed to set active organisation: {error}": "Kunne ikke angi aktiv organisasjon: {error}", + "Failed to setup SOLR: {error}": "Kunne ikke konfigurere SOLR: {error}", + "Failed to start file warmup: {error}": "Kunne ikke starte filoppvarming: {error}", + "Failed to start processing {type} files: {error}": "Kunne ikke starte behandling av {type}-filer: {error}", + "Failed to start vectorization: {error}": "Kunne ikke starte vektorisering: {error}", + "Failed to switch organisation: {error}": "Kunne ikke bytte organisasjon: {error}", + "Failed to sync magic table for {schema}: {error}": "Kunne ikke synkronisere magisk tabell for {schema}: {error}", + "Failed to test SOLR connection: {error}": "Kunne ikke teste SOLR-tilkobling: {error}", + "Failed to test webhook": "Kunne ikke teste webhook", + "Failed to update LLM settings: {error}": "Kunne ikke oppdatere LLM-innstillinger: {error}", + "Failed to update Multitenancy settings: {error}": "Kunne ikke oppdatere flerleietaker-innstillinger: {error}", + "Failed to update RBAC settings: {error}": "Kunne ikke oppdatere RBAC-innstillinger: {error}", + "Failed to update Retention settings: {error}": "Kunne ikke oppdatere lagringsinnstillinger: {error}", + "Failed to update SOLR settings: {error}": "Kunne ikke oppdatere SOLR-innstillinger: {error}", + "Failed to update favorite status": "Kunne ikke oppdatere favorittstatus", + "Failed to update schema configuration for {schema}: {error}": "Kunne ikke oppdatere skjemakonfigurasjon for {schema}: {error}", + "Failed to update schema properties": "Kunne ikke oppdatere skjemaegenskaper", + "Failed to update view": "Kunne ikke oppdatere visning", + "Failed to update view: {error}": "Kunne ikke oppdatere visning: {error}", + "Failed to update webhook": "Kunne ikke oppdatere webhook", + "Failed to upload avatar": "Kunne ikke laste opp avatar", + "Failed to validate {schema}: {error}": "Kunne ikke validere {schema}: {error}", + "Failed to vectorize files: {error}": "Kunne ikke vektorisere filer: {error}", + "Failed to warmup names cache: {error}": "Kunne ikke varme opp navnecache: {error}", + "Federation": "Føderasjon", + "Feedback recorded": "Tilbakemelding registrert", + "Fewer sources (1-3):": "Færre kilder (1-3):", + "Field": "Felt", + "Fields to Display": "Felt som skal vises", + "File": "Fil", + "File #{id}": "Fil #{id}", + "File Chunk Prediction": "Filbitprediksjon", + "File Collection": "Filsamling", + "File Handling": "Filhåndtering", + "File Location": "Filplassering", + "File Management": "Filadministrasjon", + "File Name": "Filnavn", + "File Path": "Filsti", + "File Type Selection": "Filtypevelger", + "File Types to Process": "Filtyper å behandle", + "File Vectorization": "Filovektorisering", + "File Warmup": "Filoppvarming", + "File actions menu": "Filhandlingsmeny", + "File anonymized successfully": "Fil anonymisert", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Filbitvektorisering er ikke implementert ennå. Bitene er klare og lagret, men vektoriseringstjenesten er under utvikling.", + "File collection not configured": "Filsamling ikke konfigurert", + "File discovery completed": "Filoppdagelse fullført", + "File extraction completed": "Filuttrekking fullført", + "File extraction queued": "Filuttrekking satt i kø", + "File indexed successfully": "Fil indeksert", + "File is already anonymized": "Fil er allerede anonymisert", + "File name": "Filnavn", + "File not found": "Fil ikke funnet", + "File settings saved successfully": "Filinnstillinger lagret", + "File settings updated successfully": "Filinnstillinger oppdatert", + "File sources": "Filkilder", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Filovektorisering fullført! {vectorized} biter vektorisert fra {files} filer. {failed} mislyktes.", + "File vectorization configuration saved successfully": "Filovektoriseringskonfigurasjon lagret", + "File vectorization started. Check the statistics section for progress.": "Filovektorisering startet. Sjekk statistikkdelen for fremgang.", + "File warmup completed": "Filoppvarming fullført", + "Files": "Filer", + "Files added successfully": "Filer lagt til", + "Files with Completed Extraction:": "Filer med fullført uttrekking:", + "Files → fileCollection, Objects → objectCollection": "Filer → fileCollection, Objekter → objectCollection", + "Files → filecollection, objects → objectcollection": "Filer → filecollection, objekter → objectcollection", + "Filter Audit Trails": "Filtrer revisjonsspor", + "Filter Deleted Items": "Filtrer slettede elementer", + "Filter Objects": "Filtrer objekter", + "Filter Properties": "Filtrer egenskaper", + "Filter Search Trails": "Filtrer søkespor", + "Filter Statistics": "Filtrer statistikk", + "Filter and analyze search trail entries": "Filtrer og analyser søkesporoppføringer", + "Filter and manage audit trail entries": "Filtrer og administrer revisjonssporoppføringer", + "Filter and manage soft deleted items": "Filtrer og administrer myk-slettede elementer", + "Filter and search entities": "Filtrer og søk etter enheter", + "Filter by object ID": "Filtrer etter objekt-ID", + "Filter by search term": "Filtrer etter søketerm", + "Filter by type": "Filtrer etter type", + "Filter by webhook": "Filtrer etter webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filterdata lastet automatisk. Bruk filtrene nedenfor for å avgrense søket.", + "Filter fields...": "Filtrer felt...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrer webhook-utløsere etter nyttelastegenskaper (én per linje, format: nøkkel: verdi)", + "Filtered": "Filtrert", + "Filters": "Filtre", + "Finalizing setup...": "Fullfører oppsett...", + "Fireworks AI Chat Settings": "Fireworks AI-chatinnstillinger", + "Fireworks AI Embedding Configuration": "Fireworks AI-innbyggingskonfigurasjon", + "First": "Første", + "Fixed": "Fast", + "For chat and retrieval-augmented generation": "For chat og hentingsforsterket generering", + "For vector embeddings and semantic search": "For vektorinnbygginger og semantisk søk", + "Format": "Format", + "Format Issue": "Formatproblem", + "Format constraint is missing": "Formatbegrensning mangler", + "From (e.g. -7 days or 2025-01-01)": "Fra (f.eks. -7 dager eller 2025-01-01)", + "From Date": "Fra dato", + "From date": "Fra dato", + "Full data copied to clipboard": "Alle data kopiert til utklippstavlen", + "Function Tools": "Funksjonsverktøy", + "General Issue": "Generelt problem", + "Generate recommendations and confidence scores": "Generer anbefalinger og konfidenspoeng", + "Generate report": "Generer rapport", + "Generate the verantwoordingsdocument": "Generer verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generer vektorer umiddelbart når nye objekter opprettes", + "Generated: {time}": "Generert: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Få sanntidsinnsikt i organisasjonens datahelse med fokus på registre, skjemadefinisjoner og objektlagring og -bruk.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Få sanntidsinnsikt i organisasjonens datahelse, veksttrender og kvalitetsmålinger.", + "GitHub Personal Access Token": "GitHub personlig tilgangstoken", + "GitHub token is valid": "GitHub-token er gyldig", + "GitHub token is valid! Username: {username}": "GitHub-token er gyldig! Brukernavn: {username}", + "GitHub token removed successfully": "GitHub-token fjernet", + "GitHub token saved successfully": "GitHub-token lagret", + "GitHub token test failed: {message}": "GitHub-tokentest mislyktes: {message}", + "GitLab Personal Access Token": "GitLab personlig tilgangstoken", + "GitLab URL saved successfully": "GitLab-URL lagret", + "GitLab token is valid": "GitLab-token er gyldig", + "GitLab token is valid! Username: {username}": "GitLab-token er gyldig! Brukernavn: {username}", + "GitLab token saved successfully": "GitLab-token lagret", + "GitLab token test failed: {message}": "GitLab-tokentest mislyktes: {message}", + "Go to linked Register": "Gå til koblet register", + "Go to linked Schema": "Gå til koblet skjema", + "Go to organisation": "Gå til organisasjon", + "Grant additional permissions beyond standard CRUD operations": "Gi ytterligere tillatelser utover standard CRUD-operasjoner", + "Group Quota": "Gruppekvote", + "Groups": "Grupper", + "HTML preview": "HTML-forhåndsvisning", + "HTTP Method": "HTTP-metode", + "HTTP method used to send webhook requests": "HTTP-metode brukt for å sende webhook-forespørsler", + "Headers": "Hoder", + "Health": "Helse", + "Heartbeat successful - connection kept alive": "Hjerteslaget vellykket – tilkobling holdes i live", + "Helpful": "Nyttig", + "Here you can configure the details for various connections.": "Her kan du konfigurere detaljer for ulike tilkoblinger.", + "Here you can configure the details for your organisation.": "Her kan du konfigurere detaljer for organisasjonen din.", + "Hide": "Skjul", + "Hide Filters": "Skjul filtre", + "Hide in collection view": "Skjul i samlingsvisning", + "Hide in forms": "Skjul i skjemaer", + "Hide in list view": "Skjul i listevisning", + "High": "Høy", + "High Confidence": "Høy konfidens", + "Host": "Vert", + "Hourly": "Timesvis", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Hvor dypt nestede objektegenskaper skal traverseres (1-20). Høyere verdier fanger mer detalj men øker vektorstørrelsen.", + "How often to check for updates (1-168 hours)": "Hvor ofte etter oppdateringer skal sjekkes (1-168 timer)", + "How to handle retries for failed webhook deliveries": "Hvordan håndtere nye forsøk for mislykkede webhookleveringer", + "IBANs": "IBAN-er", + "ID": "ID", + "ID of input mapping (optional)": "ID for innmatingstilordning (valgfritt)", + "ID of output mapping (optional)": "ID for utmatinginstilordning (valgfritt)", + "ID of the target resource": "ID for målressursen", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "IP-hastighetsbegrensninger tømt", + "Identify properties not in the schema": "Identifiser egenskaper som ikke er i skjemaet", + "Immutable": "Uforanderlig", + "Import": "Importer", + "Import Configuration": "Importerkonfigurasjon", + "Import Data into Register": "Importer data til register", + "Import successful": "Import vellykket", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importer report-bundle.json-malen for å få `reports`-registeret, og opprett deretter ditt første dashboard via standard objekt-UI. Dashboards deklarerer widgetene sine i JSON, og rendereren mater hvert widget live aggregeringsdata.", + "Improved Property": "Forbedret egenskap", + "In use": "I bruk", + "Inactive": "Inaktiv", + "Inactive organisations cannot be used": "Inaktive organisasjoner kan ikke brukes", + "Include IDs and names of related objects for better contextual search": "Inkluder ID-er og navn på relaterte objekter for bedre kontekstsøk", + "Include ids and names of related objects for better contextual search": "Inkluder ID-er og navn på relaterte objekter for bedre kontekstsøk", + "Include related object references": "Inkluder relaterte objektreferanser", + "Include schema and register metadata": "Inkluder skjema- og registermetadata", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Informasjon om gjeldende OpenRegister-installasjon", + "Inherited from register": "Arvet fra register", + "Inherits permissions from register": "Arver tillatelser fra register", + "Initialization failed": "Initialisering mislyktes", + "Initialize Project": "Initialiser prosjekt", + "Initializing SOLR setup...": "Initialiserer SOLR-oppsett...", + "Input Mapping": "Innmatingstilordning", + "Inspect Fields": "Inspiser felt", + "Inspect Index": "Inspiser indeks", + "Inspect SOLR Index": "Inspiser SOLR-indeks", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installer notify_push-appen fra Nextcloud App Store for å aktivere sanntidsoppdateringer.", + "Integrations": "Integrasjoner", + "Internet location (URL) of this installation": "Internettplassering (URL) for denne installasjonen", + "Interval": "Intervall", + "Invalid": "Ugyldig", + "Invalid batch size. Must be between 1 and 5000": "Ugyldig batchstørrelse. Må være mellom 1 og 5000", + "Invalid feedback type": "Ugyldig tilbakemeldingstype", + "Invalid field name provided": "Ugyldig feltnavn angitt", + "Invalid maxObjects. Must be 0 (all) or positive number": "Ugyldig maxObjects. Må være 0 (alle) eller positivt tall", + "Invalid:": "Ugyldig:", + "Invite Users": "Inviter brukere", + "Invited Users": "Inviterte brukere", + "Inzage (Art 15)": "Inzage (art. 15)", + "Inzage results": "Inzage-resultater", + "Issue": "Problem", + "Items per page:": "Elementer per side:", + "Just now": "Akkurat nå", + "Key": "Nøkkel", + "LLM Configuration": "LLM-konfigurasjon", + "LLM actions menu": "LLM-handlingsmeny", + "LLM configuration saved successfully": "LLM-konfigurasjon lagret", + "LLM features disabled": "LLM-funksjoner deaktivert", + "LLM features enabled": "LLM-funksjoner aktivert", + "LLM must be enabled with an embedding provider configured": "LLM må aktiveres med en innbyggingsleverandør konfigurert", + "LLM settings saved successfully": "LLM-innstillinger lagret", + "LLM settings updated successfully": "LLM-innstillinger oppdatert", + "Label": "Etikett", + "Labels": "Etiketter", + "Languages": "Språk", + "Last": "Siste", + "Last Triggered": "Sist utløst", + "Leave empty to allow all users access": "La stå tomt for å gi alle brukere tilgang", + "Leave off for auto-detection from property format.": "La stå av for automatisk registrering fra egenskapsformat.", + "Left organisation successfully": "Forlot organisasjon", + "Legal basis *": "Rettslig grunnlag *", + "Length Range:": "Lengdeområde:", + "Linear": "Lineær", + "Link": "Koble", + "Link an email from the Mail app sidebar to associate it with this object.": "Koble en e-post fra Mail-appens sidefeltet for å knytte den til dette objektet.", + "Link an object to see it here.": "Koble et objekt for å se det her.", + "Link another object": "Koble et annet objekt", + "Link existing event": "Koble eksisterende hendelse", + "Link removed": "Kobling fjernet", + "Link to Object": "Koble til objekt", + "Link to {name}": "Koble til {name}", + "Linked Objects": "Koblede objekter", + "Linked by {user}": "Koblet av {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Koblede e-poster, kalenderhendelser, kontakter og Deck-kort vil vises her.", + "Linked to {name}": "Koblet til {name}", + "Load Advanced Filters": "Last inn avanserte filtre", + "Load advanced filters with live data from your search index": "Last inn avanserte filtre med live-data fra søkeindeksen din", + "Load more": "Last inn mer", + "Load view": "Last inn visning", + "Loading API tokens...": "Laster inn API-tokener...", + "Loading ConfigSets...": "Laster inn ConfigSets...", + "Loading LLM configuration...": "Laster inn LLM-konfigurasjon...", + "Loading RBAC settings...": "Laster inn RBAC-innstillinger...", + "Loading activity...": "Laster inn aktivitet...", + "Loading advanced filters...": "Laster inn avanserte filtre...", + "Loading agents...": "Laster inn agenter...", + "Loading application details...": "Laster inn applikasjonsdetaljer...", + "Loading applications...": "Laster inn applikasjoner...", + "Loading archived conversations...": "Laster inn arkiverte samtaler...", + "Loading audit trails...": "Laster inn revisjonsspor...", + "Loading available tools...": "Laster inn tilgjengelige verktøy...", + "Loading cache statistics...": "Laster inn cachestatistikk...", + "Loading collections...": "Laster inn samlinger...", + "Loading configurations...": "Laster inn konfigurasjoner...", + "Loading conversation...": "Laster inn samtale...", + "Loading conversations...": "Laster inn samtaler...", + "Loading deleted items...": "Laster inn slettede elementer...", + "Loading endpoints": "Laster inn endepunkter", + "Loading entities...": "Laster inn enheter...", + "Loading events...": "Laster inn hendelser...", + "Loading filter data...": "Laster inn filterdata...", + "Loading groups...": "Laster inn grupper...", + "Loading linked objects...": "Laster inn koblede objekter...", + "Loading log details...": "Laster inn loggdetaljer...", + "Loading multitenancy settings...": "Laster inn flerleietaker-innstillinger...", + "Loading n8n configuration...": "Laster inn n8n-konfigurasjon...", + "Loading permission matrix...": "Laster inn tillatelsesmatrise...", + "Loading preferences...": "Laster inn preferanser...", + "Loading register data...": "Laster inn registerdata...", + "Loading registers...": "Laster inn registre...", + "Loading retention settings...": "Laster inn lagringsinnstillinger...", + "Loading schemas...": "Laster inn skjemaer...", + "Loading search configuration...": "Laster inn søkekonfigurasjon...", + "Loading search trails...": "Laster inn søkespor...", + "Loading sources...": "Laster inn kilder...", + "Loading statistics...": "Laster inn statistikk...", + "Loading text extraction settings...": "Laster inn tekstuttrekkingsinnstillinger...", + "Loading tokens...": "Laster inn tokener...", + "Loading user groups...": "Laster inn brukergrupper...", + "Loading users...": "Laster inn brukere...", + "Loading views...": "Laster inn visninger...", + "Loading...": "Laster inn...", + "Local": "Lokal", + "Local Version": "Lokal versjon", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Finn alle objekter som refererer til en registrert (art. 15 inzage), forhåndsvis en sletting (art. 17 vergetelheid) eller eksporter dataene deres (art. 20 portabiliteit).", + "Location": "Plassering", + "Location Field": "Plasseringsfelt", + "Locations": "Plasseringer", + "Locked": "Låst", + "Locked:": "Låst:", + "Login successful": "Innlogging vellykket", + "Logs": "Logger", + "Low": "Lav", + "Low Confidence": "Lav konfidens", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Lavere tall vises først i filterfeltet. La stå tomt for automatisk rekkefølge.", + "Lucene Version:": "Lucene-versjon:", + "Magic table sync completed for {schema}": "Magisk tabellsynkronisering fullført for {schema}", + "Magic table synchronized successfully": "Magisk tabell synkronisert", + "Mail Integration": "E-postintegrasjon", + "Mail integration is not available": "E-postintegrasjon er ikke tilgjengelig", + "Make this installation discoverable within the federation network.": "Gjør denne installasjonen oppdagbar i føderasjonsnettverket.", + "Manage Organisation Roles": "Administrer organisasjonsroller", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Administrer SOLR-samlinger (datalagre) og tildel dem for objekter og filer.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Administrer SOLR ConfigSets (konfigurasjonsmal) for samlingene dine.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Administrer SOLR-samlinger (datalagre) og tildel dem for objekter og filer.", + "Manage SOLR configsets (configuration templates) for your collections.": "Administrer SOLR configsets (konfigurasjonsmal) for samlingene dine.", + "Manage and configure agents for automated tasks": "Administrer og konfigurer agenter for automatiserte oppgaver", + "Manage and monitor file text extraction status": "Administrer og overvåk filuttrekkingsstatus", + "Manage and restore soft deleted items from your registers": "Administrer og gjenopprett myk-slettede elementer fra registrene dine", + "Manage and view detected entities from files and objects": "Administrer og vis oppdagede enheter fra filer og objekter", + "Manage document templates and themes": "Administrer dokumentmaler og temaer", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Administrer n8n-arbeidsflyter for OpenRegister-automatisering. Arbeidsflyter lagres i det konfigurerte prosjektet.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Administrer behandlingsaktiviteter, kjør tilgangsforespørsler fra registrerte, og revisjonssamsvar under EU GDPR / nederlandsk AVG.", + "Manage webhooks for event-driven integrations": "Administrer webhooks for hendelsesdrevne integrasjoner", + "Manage your account settings, security, and personal data.": "Administrer kontoinnstillinger, sikkerhet og personopplysninger.", + "Manage your applications and modules": "Administrer applikasjoner og moduler", + "Manage your chat conversations": "Administrer chatsamtaler", + "Manage your data registers and their configurations": "Administrer dataregistre og konfigurasjoner", + "Manage your data schemas and their properties": "Administrer dataskjemaer og egenskaper", + "Manage your data sources and their configurations": "Administrer datakilder og konfigurasjoner", + "Manage your organisations and switch between them": "Administrer organisasjoner og bytt mellom dem", + "Manage your saved search configurations": "Administrer lagrede søkekonfigurasjoner", + "Manage your system configurations and settings": "Administrer systemkonfigurasjoner og innstillinger", + "Managed": "Administrert", + "Mappings": "Tilordninger", + "Mass validation completed successfully": "Massevalidering fullført", + "Mass validation failed: {error}": "Massevalidering mislyktes: {error}", + "Max Files (0 = all)": "Maks filer (0 = alle)", + "Max Length": "Makslengde", + "Max Length:": "Makslengde:", + "Max Objects (0 = all)": "Maks objekter (0 = alle)", + "Max Retries": "Maks forsøk", + "Max Shards/Node": "Maks skiver/node", + "Max Tokens": "Maks tokener", + "Max execution time (ms)": "Maks utførelsestid (ms)", + "Max ms": "Maks ms", + "Max result count": "Maks antall resultater", + "Max results": "Maks resultater", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksimal filstørrelse (MB)", + "Maximum Nesting Depth": "Maksimal nestingsdybde", + "Maximum length": "Makslengde", + "Maximum length constraint is missing": "Makslengdebegrensning mangler", + "Maximum length is too restrictive": "Makslengde er for restriktiv", + "Maximum number of files to process. Set to 0 to process all files.": "Maksimalt antall filer å behandle. Sett til 0 for å behandle alle filer.", + "Maximum number of items": "Maksimalt antall elementer", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksimalt antall objekter å behandle. Sett til 0 for å behandle alle objekter.", + "Maximum number of retry attempts for failed deliveries": "Maksimalt antall forsøk på nytt for mislykkede leveringer", + "Maximum time to wait before committing changes": "Maksimal ventetid før endringer bekreftes", + "Maximum tokens to generate": "Maksimalt antall tokener å generere", + "Maximum value": "Maksimumsverdi", + "Maximum value constraint is missing": "Maksimumsverdi-begrensning mangler", + "Maximum value is too restrictive": "Maksimumsverdi er for restriktiv", + "Medium": "Middels", + "Medium Confidence": "Middels konfidens", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Middels spørringer: Søk med noe filtrering eller flere parametere (f.eks. datoområder, spesifikke registre/skjemaer)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Middels spørringer: søk med noe filtrering eller flere parametere (f.eks. datoområder, spesifikke registre/skjemaer)", + "Member removed successfully": "Medlem fjernet", + "Members": "Medlemmer", + "Members ({count})": "Medlemmer ({count})", + "Members of selected groups can access this view": "Medlemmer av valgte grupper kan få tilgang til denne visningen", + "Members:": "Medlemmer:", + "Memory prediction calculated successfully": "Minneprediksjon beregnet", + "Message content is required": "Meldingsinnhold er påkrevd", + "Message does not belong to this conversation": "Meldingen tilhører ikke denne samtalen", + "Message not found": "Melding ikke funnet", + "Metadata": "Metadata", + "Metadata Filters": "Metadatafiltre", + "Method": "Metode", + "Method*": "Metode*", + "Method: {method}": "Metode: {method}", + "Min Length": "Minlengde", + "Min execution time (ms)": "Min utførelsestid (ms)", + "Min ms": "Min ms", + "Min result count": "Min antall resultater", + "Min results": "Min resultater", + "Minimum": "Minimum", + "Minimum length": "Minlengde", + "Minimum number of items": "Minimalt antall elementer", + "Minimum value": "Minimumsverdi", + "Minimum value constraint is missing": "Minimumsverdi-begrensning mangler", + "Minimum value is too restrictive": "Minimumsverdi er for restriktiv", + "Missing conversation or agentUuid": "Mangler samtale eller agentUuid", + "Missing conversationId": "Mangler conversationId", + "Missing message": "Mangler melding", + "Missing object fields created successfully": "Manglende objektfelt opprettet", + "Mode:": "Modus:", + "Model": "Modell", + "Model Name": "Modellnavn", + "Modified": "Endret", + "Monthly": "Månedlig", + "More sources (10-20):": "Flere kilder (10-20):", + "Most Active Objects": "Mest aktive objekter", + "Multiple Of": "Multiplum av", + "Multiple of": "Multiplum av", + "Multiple search trail deletion not implemented yet": "Sletting av flere søkespor er ikke implementert ennå", + "Multitenancy": "Flerleietaker", + "Multitenancy settings updated successfully": "Flerleietaker-innstillinger oppdatert", + "My Account": "Min konto", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Navn", + "Name *": "Navn *", + "Name*": "Navn*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Navnecache varmet opp: {count} navn lastet på {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Navnecache varmet opp: {count} navn lastet på {time}. {message}", + "Names cache warmup completed": "Navnecache-oppvarming fullført", + "Naming pattern for copies:": "Navngivningsmønster for kopier:", + "New Collection Name": "Nytt samlingsnavn", + "New Configuration": "Ny konfigurasjon", + "New Conversation": "Ny samtale", + "New Properties": "Nye egenskaper", + "New Property": "Ny egenskap", + "New User Group": "Ny brukergruppe", + "New Value": "Ny verdi", + "New activity": "Ny aktivitet", + "New password": "Nytt passord", + "New processing activity": "Ny behandlingsaktivitet", + "New users without specific organisation membership will be automatically added to this organisation": "Nye brukere uten spesifikt organisasjonsmedlemskap vil automatisk bli lagt til denne organisasjonen", + "New verwerkingsactiviteit": "Ny verwerkingsactiviteit", + "Next": "Neste", + "Nextcloud Groups": "Nextcloud-grupper", + "No": "Nei", + "No API tokens.": "Ingen API-tokener.", + "No ConfigSets found": "Ingen ConfigSets funnet", + "No Deck cards linked to this object": "Ingen Deck-kort koblet til dette objektet", + "No GitHub token configured": "Ingen GitHub-token konfigurert", + "No GitHub token provided": "Ingen GitHub-token angitt", + "No GitLab token provided": "Ingen GitLab-token angitt", + "No Organisation": "Ingen organisasjon", + "No active filters": "Ingen aktive filtre", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Ingen aktive objekter å myk-slette for skjema {schema}. Bruk «Slett permanent» for å fjerne myk-slettede objekter.", + "No activity data available": "Ingen aktivitetsdata tilgjengelig", + "No activity found.": "Ingen aktivitet funnet.", + "No agents available": "Ingen agenter tilgjengelig", + "No agents found": "Ingen agenter funnet", + "No app store cache exists yet": "Ingen App Store-cache finnes ennå", + "No applications are available.": "Ingen applikasjoner er tilgjengelige.", + "No applications found": "Ingen applikasjoner funnet", + "No approval steps for this object.": "Ingen godkjenningstrinn for dette objektet.", + "No archived conversations": "Ingen arkiverte samtaler", + "No audit trail entries found": "Ingen revisjonssporoppføringer funnet", + "No changes recorded": "Ingen endringer registrert", + "No chunks to vectorize": "Ingen biter å vektorisere", + "No collections found": "Ingen samlinger funnet", + "No configuration data": "Ingen konfigurasjonsdata", + "No configuration selected": "Ingen konfigurasjon valgt", + "No configurations are available.": "Ingen konfigurasjoner er tilgjengelige.", + "No configurations found": "Ingen konfigurasjoner funnet", + "No configurations found for this application.": "Ingen konfigurasjoner funnet for denne applikasjonen.", + "No contacts linked to this object": "Ingen kontakter koblet til dette objektet", + "No conversations yet. Create a new one to get started!": "Ingen samtaler ennå. Opprett en ny for å komme i gang!", + "No dashboards yet": "Ingen dashboards ennå", + "No data available": "Ingen data tilgjengelig", + "No data available for chart": "Ingen data tilgjengelig for diagram", + "No deleted items found": "Ingen slettede elementer funnet", + "No deletion data available": "Ingen slettingsdata tilgjengelig", + "No description found": "Ingen beskrivelse funnet", + "No description provided": "Ingen beskrivelse angitt", + "No emails linked to this object": "Ingen e-poster koblet til dette objektet", + "No endpoints defined": "Ingen endepunkter definert", + "No entities detected for this email.": "Ingen enheter oppdaget for denne e-posten.", + "No entities detected in this file. Run text extraction first.": "Ingen enheter oppdaget i denne filen. Kjør tekstuttrekking først.", + "No entities found": "Ingen enheter funnet", + "No entities have been detected yet": "Ingen enheter er oppdaget ennå", + "No events found": "Ingen hendelser funnet", + "No events linked to this object": "Ingen hendelser koblet til dette objektet", + "No expired audit trails found to clear": "Ingen utløpte revisjonsspor funnet å tømme", + "No expired search trails found to clear": "Ingen utløpte søkespor funnet å tømme", + "No extraction data available for this file": "Ingen uttrekkingsdata tilgjengelig for denne filen", + "No facetable fields available. Select a register and schema to see available filters.": "Ingen fasettbare felt tilgjengelig. Velg et register og skjema for å se tilgjengelige filtre.", + "No files found": "Ingen filer funnet", + "No files have been extracted yet": "Ingen filer er uttrekket ennå", + "No files selected": "Ingen filer valgt", + "No files to process": "Ingen filer å behandle", + "No files to reindex": "Ingen filer å reindeksere", + "No filters are currently active. This will delete ALL audit trail entries!": "Ingen filtre er aktive for øyeblikket. Dette vil slette ALLE revisjonssporoppføringer!", + "No groups found. Try a different search.": "Ingen grupper funnet. Prøv et annet søk.", + "No label": "Ingen etikett", + "No labels": "Ingen etiketter", + "No linked objects": "Ingen koblede objekter", + "No log entries found": "Ingen loggoppføringer funnet", + "No logs are available for this configuration.": "Ingen logger er tilgjengelig for denne konfigurasjonen.", + "No logs are available for this source.": "Ingen logger er tilgjengelig for denne kilden.", + "No logs found": "Ingen logger funnet", + "No matches": "Ingen treff", + "No mismatched fields found - SOLR schema is properly configured": "Ingen uoverensstemmende felt funnet – SOLR-skjemaet er riktig konfigurert", + "No objects are currently selected for deletion.": "Ingen objekter er valgt for sletting.", + "No objects are currently selected for permanent deletion.": "Ingen objekter er valgt for permanent sletting.", + "No objects are currently selected for restoration.": "Ingen objekter er valgt for gjenoppretting.", + "No objects found": "Ingen objekter funnet", + "No objects found to vectorize": "Ingen objekter funnet å vektorisere", + "No objects linked to this email": "Ingen objekter koblet til denne e-posten", + "No objects selected": "Ingen objekter valgt", + "No objects selected for deletion": "Ingen objekter valgt for sletting", + "No objects selected for restoration": "Ingen objekter valgt for gjenoppretting", + "No objects to delete": "Ingen objekter å slette", + "No objects to delete for schema {schema}": "Ingen objekter å slette for skjema {schema}", + "No objects were permanently deleted": "Ingen objekter ble permanent slettet", + "No organisations found": "Ingen organisasjoner funnet", + "No personal data was found for this subject identifier.": "Ingen personopplysninger ble funnet for denne emneidentifikatoren.", + "No processing activities yet": "Ingen behandlingsaktiviteter ennå", + "No properties found": "Ingen egenskaper funnet", + "No properties match your filters.": "Ingen egenskaper samsvarer med filtrene dine.", + "No purge date set": "Ingen slettefrist angitt", + "No register data available": "Ingen registerdata tilgjengelig", + "No register objects reference this file": "Ingen registerobjekter refererer til denne filen", + "No registers found": "Ingen registre funnet", + "No registers found for this application.": "Ingen registre funnet for denne applikasjonen.", + "No registers found. Create a register to configure permissions.": "Ingen registre funnet. Opprett et register for å konfigurere tillatelser.", + "No related cases found for this sender": "Ingen relaterte saker funnet for denne avsenderen", + "No related entities": "Ingen relaterte enheter", + "No relations found": "Ingen relasjoner funnet", + "No request body available": "Ingen forespørselstekst tilgjengelig", + "No response body available": "Ingen svartekst tilgjengelig", + "No saved views yet. Create one in the Search tab!": "Ingen lagrede visninger ennå. Opprett en i Søk-fanen!", + "No saved views yet. create one in the search tab!": "Ingen lagrede visninger ennå. opprett en i søk-fanen!", + "No scan run yet": "Ingen skanning kjørt ennå", + "No schema data available": "Ingen skjemadata tilgjengelig", + "No schema selected for exploration": "Ingen skjema valgt for utforsking", + "No schemas configured for mail linking.": "Ingen skjemaer konfigurert for e-postkobling.", + "No schemas found": "Ingen skjemaer funnet", + "No schemas found for this application.": "Ingen skjemaer funnet for denne applikasjonen.", + "No search terms data available": "Ingen søketermdata tilgjengelig", + "No search trail entries found": "Ingen søkesporoppføringer funnet", + "No sources are available.": "Ingen kilder er tilgjengelige.", + "No sources found": "Ingen kilder funnet", + "No synchronizations found": "Ingen synkroniseringer funnet", + "No templates found": "Ingen maler funnet", + "No templates have been created yet": "Ingen maler er opprettet ennå", + "No tools available. Tools can be registered by installed apps.": "Ingen verktøy tilgjengelig. Verktøy kan registreres av installerte apper.", + "No users in this organisation.": "Ingen brukere i denne organisasjonen.", + "No valid magic-mapped register+schema combinations found": "Ingen gyldige magisk-tilordnede register+skjema-kombinasjoner funnet", + "No views found. Create views first before configuring vectorization.": "Ingen visninger funnet. Opprett visninger først før du konfigurerer vektorisering.", + "No views match your search": "Ingen visninger samsvarer med søket ditt", + "No webhooks found": "Ingen webhooks funnet", + "No webhooks have been configured yet": "Ingen webhooks er konfigurert ennå", + "No workflows found in this project. Create workflows in the n8n editor.": "Ingen arbeidsflyter funnet i dette prosjektet. Opprett arbeidsflyter i n8n-editoren.", + "None": "Ingen", + "Not Found": "Ikke funnet", + "Not extracted": "Ikke uttrekket", + "Not helpful": "Ikke nyttig", + "Not in use": "Ikke i bruk", + "Note:": "Merk:", + "Notifications": "Varsler", + "Number Constraints": "Tallbegrensninger", + "Number of Sources": "Antall kilder", + "Number of chunks to vectorize in one API call (1-100).": "Antall biter å vektorisere i ett API-kall (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Antall biter å vektorisere i ett API-kall. Høyere = raskere men mer minne. Anbefalt: 10-50.", + "Number of objects to process in each batch (1-5000).": "Antall objekter å behandle i hver batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Antall objekter å vektorisere i ett API-kall. Høyere = raskere men mer minne. Anbefalt: 10-50.", + "Numeric Range:": "Tallområde:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Objektsamling", + "Object Configuration:": "Objektkonfigurasjon:", + "Object Count Prediction": "Objektantallsprediksjon", + "Object Extraction Mode": "Objektuttrekkingsmodus", + "Object ID": "Objekt-ID", + "Object Management": "Objektadministrasjon", + "Object Text Extraction": "Objekttekstuttrekking", + "Object Vectorization": "Objektvektorisering", + "Object Warmup": "Objektoppvarming", + "Object changes in owned objects": "Objektendringer i eide objekter", + "Object collection not configured": "Objektsamling ikke konfigurert", + "Object metadata incomplete for file upload": "Objektmetadata ufullstendig for filopplasting", + "Object not found": "Objekt ikke funnet", + "Object permanently deleted": "Objekt permanent slettet", + "Object restored successfully": "Objekt gjenopprettet", + "Object settings updated successfully": "Objektinnstillinger oppdatert", + "Object sources": "Objektkilder", + "Object successfully deleted": "Objekt slettet", + "Object successfully modified": "Objekt endret", + "Object unlocked successfully": "Objekt låst opp", + "Object vectorization configuration saved successfully": "Objektvektoriseringskonfigurasjon lagret", + "Object vectorized successfully": "Objekt vektorisert", + "Object:": "Objekt:", + "Objects": "Objekter", + "Objects Analyzed": "Objekter analysert", + "Objects Distribution": "Objektfordeling", + "Objects being analyzed": "Objekter som analyseres", + "Objects by Register": "Objekter etter register", + "Objects by Schema": "Objekter etter skjema", + "Objects deletion completed for {schema}": "Objektsletting fullført for {schema}", + "Objects deletion completed successfully": "Objektsletting fullført", + "Objects in schema": "Objekter i skjema", + "Objects successfully deleted": "Objekter slettet", + "Objects to Process:": "Objekter å behandle:", + "Objects to be analyzed": "Objekter som skal analyseres", + "Objects to be deleted": "Objekter som skal slettes", + "Objects to be validated": "Objekter som skal valideres", + "Objects will be serialized as JSON text before vectorization": "Objekter vil bli serialisert som JSON-tekst før vektorisering", + "Objects will be soft deleted and moved to the": "Objekter vil bli myk-slettet og flyttet til", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekter vil bli myk-slettet (merket som slettet men beholdt i databasen). De kan gjenopprettes senere ved behov.", + "Old Value": "Gammel verdi", + "Oldest Item (days)": "Eldste element (dager)", + "Ollama Chat Settings": "Ollama-chatinnstillinger", + "Ollama Embedding Configuration": "Ollama-innbyggingskonfigurasjon", + "Ollama URL": "Ollama-URL", + "Only With Changes": "Bare med endringer", + "Only members of selected groups can access this organisation": "Bare medlemmer av valgte grupper kan få tilgang til denne organisasjonen", + "Open Nextcloud App Store": "Åpne Nextcloud App Store", + "Open OpenRegister sidebar": "Åpne OpenRegister-sidefeltet", + "Open Register Objects": "Åpne registerobjekter", + "Open folder": "Åpne mappe", + "Open in OpenRegister": "Åpne i OpenRegister", + "Open n8n Editor": "Åpne n8n-editor", + "OpenAI Chat Settings": "OpenAI-chatinnstillinger", + "OpenAI Embedding Configuration": "OpenAI-innbyggingskonfigurasjon", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister-innstillinger", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operatørdefinerte dashboards og planlagte rapporter. Hvert dashboard er et førsteklasses objekt i `reports`-registeret; widgets deklareres i dashboardets `widgets`-array og gjengis live fra aggregeringer / GraphQL.", + "Optimizing search performance...": "Optimaliserer søkeytelse...", + "Optional URL-friendly identifier": "Valgfri URL-vennlig identifikator", + "Optional description for this facet": "Valgfri beskrivelse for denne fasetten", + "Optional end date property": "Valgfri sluttdatoegenskap", + "Optional event description template": "Valgfri hendelsesbeskrivelsesmal", + "Optional location property": "Valgfri plasseringsegenskap", + "Optional webhook secret for signature verification": "Valgfri webhook-hemmelighet for signaturverifisering", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Valgfritt: Legg til søkeparametere for å filtrere det refererte skjemaet (f.eks. status=active&type=public)", + "Or": "Eller", + "Order": "Rekkefølge", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Ordnede BCP 47-språktagger. Det første språket er registerstandarden og driver Accept-Language-tilbakefall for oversettbare egenskaper.", + "Organisation": "Organisasjon", + "Organisation Configuration": "Organisasjonskonfigurasjon", + "Organisation ID": "Organisasjons-ID", + "Organisation Members": "Organisasjonsmedlemmer", + "Organisation Statistics": "Organisasjonsstatistikk", + "Organisation created successfully": "Organisasjon opprettet", + "Organisation membership changes": "Endringer i organisasjonsmedlemskap", + "Organisation name is required": "Organisasjonsnavn er påkrevd", + "Organisation settings saved successfully": "Organisasjonsinnstillinger lagret", + "Organisation successfully created": "Organisasjon opprettet", + "Organisation successfully updated": "Organisasjon oppdatert", + "Organisational measures": "Organisatoriske tiltak", + "Organisations": "Organisasjoner", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organisasjon", + "Organization ID (Optional)": "Organisasjons-ID (valgfritt)", + "Organizations": "Organisasjoner", + "Orphaned Items": "Foreldreløse elementer", + "Other": "Annet", + "Output Mapping": "Utmatingtilordning", + "Overview": "Oversikt", + "Owner": "Eier", + "Owner:": "Eier:", + "PDF": "PDF", + "PII hits": "Personopplysningsforekomster", + "PII rows": "Personopplysningsrader", + "Page {current} of {total}": "Side {current} av {total}", + "Parallel Mode (Faster, more resource intensive)": "Parallellmodus (raskere, mer ressurskrevende)", + "Parallel:": "Parallell:", + "Password": "Passord", + "Password changes are not supported by your authentication provider.": "Passordendringer støttes ikke av autentiseringsleverandøren din.", + "Password for SOLR authentication (optional)": "Passord for SOLR-autentisering (valgfritt)", + "Password updated successfully": "Passord oppdatert", + "Path": "Sti", + "Path in repository": "Sti i repository", + "Path where the configuration file will be saved in the repository": "Sti der konfigurasjonsfilen vil bli lagret i repository", + "Path where the register OAS file will be saved in the repository": "Sti der register-OAS-filen vil bli lagret i repository", + "Pattern (regex)": "Mønster (regulært uttrykk)", + "Pattern Issue": "Mønsterproblem", + "Pattern constraint is missing": "Mønsterbegrensning mangler", + "Patterns:": "Mønstre:", + "Pending": "Venter", + "Permanent Delete Mode": "Permanent slettingsmodus", + "Permanently Delete": "Slett permanent", + "Permanently Delete ({count})": "Slett permanent ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Slett permanent alle {count} myk-slettede objekter. Dette kan ikke angres!", + "Permanently delete already soft-deleted objects": "Slett permanent allerede myk-slettede objekter", + "Permanently delete this conversation?": "Slette denne samtalen permanent?", + "Permission Matrix": "Tillatelsesmatrise", + "Person": "Person", + "Personal Data": "Personopplysninger", + "Personal Data Export": "Eksport av personopplysninger", + "Persons": "Personer", + "Phone": "Telefon", + "Phone Numbers": "Telefonnumre", + "Photos": "Bilder", + "Please create an agent in the": "Vennligst opprett en agent i", + "Please select a user": "Velg en bruker", + "Please select an agent to continue": "Velg en agent for å fortsette", + "Please select an organisation": "Velg en organisasjon", + "Please select at least one item to import": "Velg minst ett element å importere", + "Please select registers or schemas first": "Velg registre eller skjemaer først", + "Please select which register and schema to use for the new object": "Velg register og skjema for det nye objektet", + "Please try again later.": "Vennligst prøv igjen senere.", + "Please wait while we fetch your agents.": "Vent mens vi henter agentene dine.", + "Please wait while we fetch your applications.": "Vent mens vi henter applikasjonene dine.", + "Please wait while we fetch your configurations.": "Vent mens vi henter konfigurasjonene dine.", + "Please wait while we fetch your deleted items.": "Vent mens vi henter de slettede elementene dine.", + "Please wait while we fetch your sources.": "Vent mens vi henter kildene dine.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Vent mens vi permanent sletter SOLR-samlingen. Dette kan ta litt tid.", + "Polls": "Avstemninger", + "Popular Search Terms": "Populære søketermer", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (art. 20)", + "Preferences saved": "Preferanser lagret", + "Prerequisites": "Forutsetninger", + "Press Enter to send, Shift+Enter for new line": "Trykk Enter for å sende, Shift+Enter for ny linje", + "Press enter to send, shift+enter for new line": "Trykk enter for å sende, shift+enter for ny linje", + "Prettify": "Formater", + "Preview erasure": "Forhåndsvis sletting", + "Preview:": "Forhåndsvisning:", + "Previous": "Forrige", + "Private": "Privat", + "Private Agent (Default)": "Privat agent (standard)", + "Private agents": "Private agenter", + "Process Name (optional)": "Prosessnavn (valgfritt)", + "Processed {indexed} files, {failed} failed": "Behandlet {indexed} filer, {failed} mislyktes", + "Processes chunks in batches with simulated parallelism.": "Behandler biter i batcher med simulert parallellisme.", + "Processes file chunks sequentially (safest).": "Behandler filbiter sekvensielt (sikreste).", + "Processes objects in chunks with simulated parallelism.": "Behandler objekter i biter med simulert parallellisme.", + "Processes objects sequentially (safest).": "Behandler objekter sekvensielt (sikreste).", + "Processing": "Behandler", + "Processing Limits": "Behandlingsgrenser", + "Processing...": "Behandler...", + "Project initialized successfully": "Prosjekt initialisert", + "Project not found. Please initialize first.": "Prosjekt ikke funnet. Vennligst initialiser først.", + "Properties": "Egenskaper", + "Property Behaviors": "Egenskapsatferd", + "Property Configuration:": "Egenskapskonfigurasjon:", + "Property Filters": "Egenskapsfiltre", + "Property Title": "Egenskapstittel", + "Property Type": "Egenskapstype", + "Property can be improved": "Egenskap kan forbedres", + "Property could not be added": "Egenskap kunne ikke legges til", + "Property could not be updated": "Egenskap kunne ikke oppdateres", + "Property name of inversed relation": "Egenskapsnavn for invertert relasjon", + "Property successfully added": "Egenskap lagt til", + "Property successfully updated": "Egenskap oppdatert", + "Protected": "Beskyttet", + "Provider": "Leverandør", + "Provider is required for testing": "Leverandør er påkrevd for testing", + "Public": "Offentlig", + "Public View": "Offentlig visning", + "Public views can be accessed by anyone in the system": "Offentlige visninger er tilgjengelige for alle i systemet", + "Publish": "Publiser", + "Publish Configuration to GitHub": "Publiser konfigurasjon til GitHub", + "Publish OAS": "Publiser OAS", + "Publish Objects": "Publiser objekter", + "Publish Register OAS to GitHub": "Publiser register-OAS til GitHub", + "Publish all objects in this schema": "Publiser alle objekter i dette skjemaet", + "Publish objects to make them publicly available": "Publiser objekter for å gjøre dem offentlig tilgjengelige", + "Published": "Publisert", + "Published:": "Publisert:", + "Publishing...": "Publiserer...", + "Purge": "Tøm", + "Purge Date": "Slettedato", + "Purge {count} object from database": "Tøm {count} objekt fra database", + "Purge {count} objects from database": "Tøm {count} objekter fra database", + "Purpose limitation *": "Formålsbegrensning *", + "Push Notifications": "Push-varsler", + "Query Complexity Distribution": "Spørringskompleksitetsfordeling", + "Query Help": "Søkehjelp", + "Query parameter is required": "Søkeparameter er påkrevd", + "Quota": "Kvote", + "RAG": "RAG", + "RAG Configuration": "RAG-konfigurasjon", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC-innstillinger oppdatert", + "Rate limits cleared successfully": "Hastighetsbegrensninger tømt", + "Raw Changes Data": "Råe endringsdata", + "Re-vectorize on object update": "Revektoriser ved objektoppdatering", + "Read": "Les", + "Real-time push notification status via notify_push": "Sanntids push-varselstatus via notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Sanntids push-varsler er aktive. Tilkoblede klienter mottar umiddelbare oppdateringer når objekter opprettes, oppdateres eller slettes.", + "Realtime push active": "Sanntids push aktiv", + "Realtime push not available — the notify_push app is not installed": "Sanntids push ikke tilgjengelig – notify_push-appen er ikke installert", + "Reason": "Årsak", + "Reason (optional)": "Årsak (valgfritt)", + "Rebase operation completed successfully": "Rebase-operasjon fullført", + "Rebase operation failed: {error}": "Rebase-operasjon mislyktes: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Anbefalinger:", + "Recommended Type:": "Anbefalt type:", + "Recommended: 5 sources": "Anbefalt: 5 kilder", + "Refresh": "Oppdater", + "Refresh Data": "Oppdater data", + "Refresh Stats": "Oppdater statistikk", + "Refresh Workflows": "Oppdater arbeidsflyter", + "Refresh dashboard": "Oppdater dashboard", + "Refresh database info": "Oppdater databaseinfo", + "Register": "Register", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Skjema", + "Register ID": "Register-ID", + "Register Objects": "Registerobjekter", + "Register Overview": "Registeroversikt", + "Register Statistics": "Registerstatistikk", + "Register Totals": "Registertotaler", + "Register and schema are required": "Register og skjema er påkrevd", + "Register annotation": "Registerannotering", + "Register depublished successfully": "Register avpublisert", + "Register not found": "Register ikke funnet", + "Register objects": "Registerobjekter", + "Register objects deletion completed successfully": "Sletting av registerobjekter fullført", + "Register published successfully": "Register publisert", + "Register/Schema Usage": "Register/Skjema-bruk", + "Register:": "Register:", + "Registers": "Registre", + "Reindex": "Reindekser", + "Reindex completed": "Reindeksering fullført", + "Reindex completed successfully": "Reindeksering fullført", + "Reindex completed! Processed {count} objects in {duration}s": "Reindeksering fullført! Behandlet {count} objekter på {duration}s", + "Reindex failed": "Reindeksering mislyktes", + "Reindex failed: %s": "Reindeksering mislyktes: %s", + "Reject": "Avvis", + "Rejected": "Avvist", + "Related Cases": "Relaterte saker", + "Relations": "Relasjoner", + "Remote Version": "Ekstern versjon", + "Remove": "Fjern", + "Remove Deck card": "Fjern Deck-kort", + "Remove avatar": "Fjern avatar", + "Remove contact": "Fjern kontakt", + "Remove filter": "Fjern filter", + "Remove from favorites": "Fjern fra favoritter", + "Remove from list": "Fjern fra liste", + "Remove group": "Fjern gruppe", + "Remove link": "Fjern kobling", + "Remove link to {name}": "Fjern kobling til {name}", + "Remove link to {name}?": "Fjerne kobling til {name}?", + "Remove link to {title}": "Fjern kobling til {title}", + "Remove oneOf entry": "Fjern oneOf-oppføring", + "Remove property after write-back": "Fjern egenskap etter tilbakeskrivning", + "Remove {title}": "Fjern {title}", + "Remove {userId} from this organisation?": "Fjerne {userId} fra denne organisasjonen?", + "Removed": "Fjernet", + "Removed from favorites": "Fjernet fra favoritter", + "Removing schema {schema}...": "Fjerner skjema {schema}...", + "Rename Conversation": "Omdøp samtale", + "Rename conversation": "Omdøp samtale", + "Replicas": "Kopier", + "Reports": "Rapporter", + "Repository": "Repository", + "Request Body": "Forespørselstekst", + "Request Data": "Forespørselsdata", + "Request Details": "Forespørselsdetaljer", + "Request Quota (per day)": "Forespørselskvote (per dag)", + "Request account deactivation": "Be om kontodeaktivering", + "Request account deactivation. This will notify administrators for review.": "Be om kontodeaktivering. Dette varsler administratorer for gjennomgang.", + "Request body must be JSON with a \"query\" field": "Forespørselstekst må være JSON med et «query»-felt", + "Request timeout in seconds": "Forespørselstidsavbrudd i sekunder", + "Requested at": "Forespurt ved", + "Required": "Påkrevd", + "Required Token Scopes": "Påkrevde tokenomfang", + "Required field": "Påkrevd felt", + "Required status is inconsistent": "Påkrevd-status er inkonsistent", + "Requires Dolphin AI for OCR text extraction": "Krever Dolphin AI for OCR-tekstuttrekking", + "Rerun Search": "Kjør søk på nytt", + "Rerunning search: {searchTerm}": "Kjører søk på nytt: {searchTerm}", + "Reset Changes": "Tilbakestill endringer", + "Reset Filters": "Tilbakestill filtre", + "Resource Quotas": "Ressurskvoter", + "Response Body": "Svartekst", + "Response Details": "Svardetaljer", + "Response Time:": "Responstid:", + "Restore": "Gjenopprett", + "Restore conversation": "Gjenopprett samtale", + "Restore or permanently delete items": "Gjenopprett eller slett elementer permanent", + "Restore {count} object": "Gjenopprett {count} objekt", + "Restore {count} objects": "Gjenopprett {count} objekter", + "Result Count Range": "Antall resultatområde", + "Results": "Resultater", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Lagringsperiode (ISO-8601 varighet, f.eks. P10Y, P30D)", + "Retention settings updated successfully": "Lagringsinnstillinger oppdatert", + "Retrieve all objects for this schema": "Hent alle objekter for dette skjemaet", + "Retry": "Prøv igjen", + "Retry Failed Extractions": "Prøv mislykkede uttrekkinger på nytt", + "Retry Policy": "Policy for ny forsøk", + "Retry completed": "Nytt forsøk fullført", + "Retry upload": "Prøv opplasting på nytt", + "Retrying...": "Prøver igjen...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Gjennomgå de valgte objektene nedenfor. Du kan fjerne objekter du ikke vil slette ved å klikke på fjern-knappen.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Gjennomgå de valgte objektene nedenfor. Du kan fjerne objekter du ikke vil inkludere.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Gjennomgå de valgte objektene nedenfor. Du kan fjerne objekter du ikke vil slette permanent ved å klikke på fjern-knappen. Denne handlingen kan ikke angres.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Gjennomgå de valgte objektene nedenfor. Du kan fjerne objekter du ikke vil gjenopprette ved å klikke på fjern-knappen. Objekter gjenopprettes til sin opprinnelige plassering.", + "Revoke": "Tilbakekall", + "Right": "Høyre", + "Risk Level": "Risikonivå", + "Risk level": "Risikonivå", + "Roles and Permissions": "Roller og tillatelser", + "Roles updated successfully": "Roller oppdatert", + "Run compliance scan": "Kjør samsvarsskanning", + "SOLR Connection Settings": "SOLR-tilkoblingsinnstillinger", + "SOLR Index Warmup": "SOLR-indeksoppvarming", + "SOLR Version:": "SOLR-versjon:", + "SOLR actions menu": "SOLR-handlingsmeny", + "SOLR base path (usually /solr)": "SOLR-grunnsti (vanligvis /solr)", + "SOLR collection deleted successfully": "SOLR-samling slettet", + "SOLR connection test failed: {error}": "SOLR-tilkoblingstest mislyktes: {error}", + "SOLR connection test successful!": "SOLR-tilkoblingstest vellykket!", + "SOLR index warmup completed successfully": "SOLR-indeksoppvarming fullført", + "SOLR is disabled": "SOLR er deaktivert", + "SOLR is not available or not configured": "SOLR er ikke tilgjengelig eller ikke konfigurert", + "SOLR logging disabled": "SOLR-logging deaktivert", + "SOLR logging enabled": "SOLR-logging aktivert", + "SOLR search disabled": "SOLR-søk deaktivert", + "SOLR search enabled": "SOLR-søk aktivert", + "SOLR server hostname or IP address": "SOLR-servertjenestenavn eller IP-adresse", + "SOLR server port number (optional, defaults to 8983)": "SOLR-serverportnummer (valgfritt, standard er 8983)", + "SOLR settings updated successfully": "SOLR-innstillinger oppdatert", + "SOLR setup completed successfully": "SOLR-oppsett fullført", + "SOLR setup completed successfully!": "SOLR-oppsett fullført!", + "SOLR setup error: %s": "SOLR-oppsettfeil: %s", + "SOLR setup failed": "SOLR-oppsett mislyktes", + "SOLR setup failed - check logs": "SOLR-oppsett mislyktes – sjekk logger", + "SOLR setup failed. Please check the configuration and try again.": "SOLR-oppsett mislyktes. Kontroller konfigurasjonen og prøv igjen.", + "SOLR setup failed: {error}": "SOLR-oppsett mislyktes: {error}", + "SOLR warmup failed: {error}": "SOLR-oppvarming mislyktes: {error}", + "Sample Values:": "Eksempelverdier:", + "Save": "Lagre", + "Save Configuration": "Lagre konfigurasjon", + "Save Connection Settings": "Lagre tilkoblingsinnstillinger", + "Save Roles": "Lagre roller", + "Save Settings": "Lagre innstillinger", + "Save changes": "Lagre endringer", + "Save current search as view": "Lagre gjeldende søk som visning", + "Save labels": "Lagre etiketter", + "Save tags for {name}": "Lagre etiketter for {name}", + "Save the organisation first to manage users.": "Lagre organisasjonen først for å administrere brukere.", + "Saved Views": "Lagrede visninger", + "Saving...": "Lagrer...", + "Scheduled": "Planlagt", + "Schema": "Skjema", + "Schema ID": "Skjema-ID", + "Schema Name": "Skjemanavn", + "Schema Statistics": "Skjemastatistikk", + "Schema annotation": "Skjemaannotering", + "Schema configuration updated successfully for {schema}": "Skjemakonfigurasjon oppdatert for {schema}", + "Schema depublished successfully": "Skjema avpublisert", + "Schema objects deletion completed successfully": "Sletting av skjemaobjekter fullført", + "Schema published successfully": "Skjema publisert", + "Schema reference ($ref)": "Skjemareferanse ($ref)", + "Schema successfully created": "Skjema opprettet", + "Schema successfully updated": "Skjema oppdatert", + "Schema {schema} removed successfully": "Skjema {schema} fjernet", + "Schema:": "Skjema:", + "Schemas": "Skjemaer", + "Scheme": "Skjema", + "Search": "Søk", + "Search / Views": "Søk / Visninger", + "Search / views": "Søk / visninger", + "Search Activity": "Søkesaktivitet", + "Search Analytics": "Søkeanalyse", + "Search Configuration": "Søkekonfigurasjon", + "Search Entities": "Søk etter enheter", + "Search Files": "Søk i filer", + "Search GitHub": "Søk på GitHub", + "Search GitLab": "Søk på GitLab", + "Search Method": "Søkemetode", + "Search Mode": "Søkemodus", + "Search Query": "Søkespørring", + "Search Results": "Søkeresultater", + "Search Term": "Søketerm", + "Search Trail Details": "Søkespordetaljer", + "Search Trail Management": "Søkesporadministrasjon", + "Search Trail Statistics": "Søkesporstatistikk", + "Search Trails": "Søkespor", + "Search Views": "Søk i visninger", + "Search Webhooks": "Søk i webhooks", + "Search agents...": "Søk etter agenter...", + "Search applications...": "Søk etter applikasjoner...", + "Search by file name or path": "Søk etter filnavn eller sti", + "Search by name or URL": "Søk etter navn eller URL", + "Search by title or UUID...": "Søk etter tittel eller UUID...", + "Search by value": "Søk etter verdi", + "Search configurations": "Søkekonfigurasjoner", + "Search data sources...": "Søk etter datakilder...", + "Search groups...": "Søk etter grupper...", + "Search in Database Objects": "Søk i databaseobjekter", + "Search in Files": "Søk i filer", + "Search in Objects": "Søk i objekter", + "Search objects": "Søk i objekter", + "Search objects...": "Søk etter objekter...", + "Search property names...": "Søk etter egenskapsnavn...", + "Search registers...": "Søk etter registre...", + "Search schemas...": "Søk etter skjemaer...", + "Search trail deletion not implemented yet": "Søkespor-sletting er ikke implementert ennå", + "Search views...": "Søk i visninger...", + "Search {name}...": "Søk etter {name}...", + "Searching...": "Søker...", + "Secret": "Hemmelighet", + "Secret key for HMAC signature generation (optional)": "Hemmelig nøkkel for HMAC-signaturgenerering (valgfritt)", + "Security": "Sikkerhet", + "See organisations as a table": "Se organisasjoner som tabell", + "See organisations as cards": "Se organisasjoner som kort", + "Select AI Agent": "Velg AI-agent", + "Select Active Organisation": "Velg aktiv organisasjon", + "Select All": "Velg alle", + "Select ConfigSet": "Velg ConfigSet", + "Select File Types to Vectorize:": "Velg filtyper å vektorisere:", + "Select HTTP method": "Velg HTTP-metode", + "Select Register and Schema": "Velg register og skjema", + "Select Views to Vectorize:": "Velg visninger å vektorisere:", + "Select a Nextcloud group to add": "Velg en Nextcloud-gruppe å legge til", + "Select a branch": "Velg en gren", + "Select a date property": "Velg en datoegenskap", + "Select a model or type a custom model name": "Velg en modell eller skriv inn et egendefinert modellnavn", + "Select a register": "Velg et register", + "Select a register...": "Velg et register...", + "Select a repository": "Velg et repository", + "Select a repository you have write access to": "Velg et repository du har skrivetilgang til", + "Select a schema": "Velg et skjema", + "Select a schema...": "Velg et skjema...", + "Select a user": "Velg en bruker", + "Select agent type": "Velg agenttype", + "Select all entities": "Velg alle enheter", + "Select an AI Agent": "Velg en AI-agent", + "Select an AI agent to begin chatting with your data.": "Velg en AI-agent for å begynne å chatte med dataene dine.", + "Select an email to see linked objects": "Velg en e-post for å se koblede objekter", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Velg en organisasjon og bruker for å legge dem til som medlem. Søk etter organisasjoner etter navn.", + "Select backend": "Velg backend", + "Select chat model": "Velg chatmodell", + "Select collection for files": "Velg samling for filer", + "Select collection for objects": "Velg samling for objekter", + "Select configuration type...": "Velg konfigurasjonstype...", + "Select default organisation": "Velg standardorganisasjon", + "Select entity {value}": "Velg enhet {value}", + "Select event to listen to...": "Velg hendelse å lytte til...", + "Select fields to display": "Velg felt som skal vises", + "Select files with the correct extension": "Velg filer med riktig utvidelse", + "Select group": "Velg gruppe", + "Select groups (optional)": "Velg grupper (valgfritt)", + "Select groups to notify...": "Velg grupper å varsle...", + "Select groups with access to this agent": "Velg grupper med tilgang til denne agenten", + "Select groups...": "Velg grupper...", + "Select model": "Velg modell", + "Select one or more registers": "Velg ett eller flere registre", + "Select one or more schemas": "Velg ett eller flere skjemaer", + "Select options...": "Velg alternativer...", + "Select or create labels, or select \"No label\" to add files": "Velg eller opprett etiketter, eller velg «Ingen etikett» for å legge til filer", + "Select owner application (optional)...": "Velg eierapplikasjon (valgfritt)...", + "Select period": "Velg periode", + "Select property to send as payload": "Velg egenskap å sende som nyttelast", + "Select provider": "Velg leverandør", + "Select registers and schemas to save a view": "Velg registre og skjemaer for å lagre en visning", + "Select retry policy": "Velg policy for nytt forsøk", + "Select role": "Velg rolle", + "Select schemas (empty = all schemas)": "Velg skjemaer (tomt = alle skjemaer)", + "Select schemas to warm up": "Velg skjemaer å varme opp", + "Select search mode": "Velg søkemodus", + "Select source type...": "Velg kildetype...", + "Select target type": "Velg måltype", + "Select the branch to publish to": "Velg grenen å publisere til", + "Select the event this webhook should listen to": "Velg hendelsen denne webhooken skal lytte til", + "Select views to filter data (optional)": "Velg visninger for å filtrere data (valgfritt)", + "Select views to limit which data the agent can access": "Velg visninger for å begrense hvilke data agenten kan få tilgang til", + "Select views to vectorize:": "Velg visninger å vektorisere:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Velg hvilke Nextcloud-grupper som er tilgjengelige for denne organisasjonen. Brukere i disse gruppene vil ha tilgang til organisasjonsressurser.", + "Select which data views the AI can search": "Velg hvilke datavisninger AI-en kan søke i", + "Select which property from the event should be used as the webhook payload data": "Velg hvilken egenskap fra hendelsen som skal brukes som webhook-nyttelastdata", + "Select which tools the AI can use to perform actions": "Velg hvilke verktøy AI-en kan bruke til å utføre handlinger", + "Select {fieldName} values": "Velg {fieldName}-verdier", + "Selected": "Valgt", + "Selected Groups": "Valgte grupper", + "Selected Objects ({count})": "Valgte objekter ({count})", + "Selected audit trails deleted successfully": "Valgte revisjonsspor slettet", + "Selected search trails deleted successfully": "Valgte søkespor slettet", + "Selected users can access this view": "Valgte brukere kan få tilgang til denne visningen", + "Send additional feedback": "Send tilleggstilbakemelding", + "Send as CloudEvent": "Send som CloudEvent", + "Send as cloudevent": "Send som cloudevent", + "Sensitive PII": "Sensitive personopplysninger", + "Serial Mode (Safer, slower)": "Seriellmodus (sikrere, tregere)", + "Serial:": "Seriell:", + "Server Configuration": "Serverkonfigurasjon", + "Server Information": "Serverinformasjon", + "Set as Active": "Sett som aktiv", + "Set as active organisation": "Sett som aktiv organisasjon", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Angi grenser for API-bruk og tokenforbruk. Bruk 0 for ubegrenset ressurser.", + "Settings": "Innstillinger", + "Settings updated successfully": "Innstillinger oppdatert", + "Shards": "Skiver", + "Share": "Del", + "Share with Groups": "Del med grupper", + "Share with Users": "Del med brukere", + "Show": "Vis", + "Show Filters": "Vis filtre", + "Show less": "Vis mindre", + "Show only entries with changes": "Vis bare oppføringer med endringer", + "Showing {count} dashboard(s)": "Viser {count} dashboard(s)", + "Showing {count} processing activities": "Viser {count} behandlingsaktiviteter", + "Showing {showing} of {total} agents": "Viser {showing} av {total} agenter", + "Showing {showing} of {total} applications": "Viser {showing} av {total} applikasjoner", + "Showing {showing} of {total} audit trail entries": "Viser {showing} av {total} revisjonssporoppføringer", + "Showing {showing} of {total} configurations": "Viser {showing} av {total} konfigurasjoner", + "Showing {showing} of {total} deleted items": "Viser {showing} av {total} slettede elementer", + "Showing {showing} of {total} entities": "Viser {showing} av {total} enheter", + "Showing {showing} of {total} files": "Viser {showing} av {total} filer", + "Showing {showing} of {total} log entries": "Viser {showing} av {total} loggoppføringer", + "Showing {showing} of {total} search trail entries": "Viser {showing} av {total} søkesporoppføringer", + "Showing {showing} of {total} sources": "Viser {showing} av {total} kilder", + "Showing {showing} of {total} templates": "Viser {showing} av {total} maler", + "Showing {showing} of {total} webhooks": "Viser {showing} av {total} webhooks", + "Showing {shown} of {total} organisations": "Viser {shown} av {total} organisasjoner", + "Simple": "Enkel", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Enkle spørringer: Grunnleggende tekstsøk med minimale parametere (f.eks. enkelt søketerm, ingen avanserte filtre)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Enkle spørringer: grunnleggende tekstsøk med minimale parametere (f.eks. enkelt søketerm, ingen avanserte filtre)", + "Size": "Størrelse", + "Slug": "Slug", + "Soft Delete Mode": "Myk-slettingsmodus", + "Soft Deleted Items": "Myk-slettede elementer", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Myk-slett alle objekter for dette skjemaet ({active} aktive, {deleted} allerede slettet)", + "SolrCloud mode enabled": "SolrCloud-modus aktivert", + "Something went wrong while adding files": "Noe gikk galt ved tillegg av filer", + "Source": "Kilde", + "Source successfully updated": "Kilde oppdatert", + "Sources": "Kilder", + "Sources:": "Kilder:", + "Special Rights": "Spesielle rettigheter", + "Standalone SOLR mode": "Frittstående SOLR-modus", + "Start Conversation": "Start samtale", + "Start Date Field": "Startdatofelt", + "Start Vectorization": "Start vektorisering", + "Start a conversation": "Start en samtale", + "Starting names cache warmup...": "Starter oppvarming av navnecache...", + "Starting validation for {schema}...": "Starter validering for {schema}...", + "Starting {action} for {schema}...": "Starter {action} for {schema}...", + "Starting...": "Starter...", + "Statistics": "Statistikk", + "Statistics and Metrics": "Statistikk og målinger", + "Statistics refreshed": "Statistikk oppdatert", + "Stats calculation not yet implemented": "Statistikkberegning er ikke implementert ennå", + "Status": "Status", + "Status Code": "Statuskode", + "Step": "Trinn", + "Storage": "Lagring", + "Storage Quota (MB)": "Lagringskvote (MB)", + "String Constraints": "Strengbegrensninger", + "Subject identifier (email, BSN, name, etc.)": "Emneidentifikator (e-post, BSN, navn osv.)", + "Subscribe": "Abonner", + "Subscribed": "Abonnert", + "Success": "Suksess", + "Success Rate": "Suksessrate", + "Success Status": "Suksessstatus", + "Successful": "Vellykket", + "Successfully added user to organisation: {name}": "Bruker lagt til i organisasjon: {name}", + "Successfully cleared {count} audit trails": "Tømt {count} revisjonsspor", + "Successfully cleared {count} blob storage objects": "Tømt {count} blob-lagringsobjekter", + "Successfully cleared {count} search trails": "Tømt {count} søkespor", + "Successfully created {count} SOLR fields": "Opprettet {count} SOLR-felt", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Slettet {count} innbygginger. Vennligst revektoriser dataene dine.", + "Successfully deleted {count} objects for {schema}": "Slettet {count} objekter for {schema}", + "Successfully fixed {count} SOLR field configurations": "Rettet {count} SOLR-feltkonfigurasjoner", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importert: {registers} registre, {schemas} skjemaer, {objects} objekter", + "Successfully joined organisation": "Ble med i organisasjon", + "Successfully permanently deleted {count} objects": "Slettet permanent {count} objekter", + "Successfully processed {count} files!": "Behandlet {count} filer!", + "Successfully restored {count} object": "Gjenopprettet {count} objekt", + "Successfully restored {count} objects": "Gjenopprettet {count} objekter", + "Successfully saved configuration for {count} facets!": "Lagret konfigurasjon for {count} fasetter!", + "Supported File Types": "Støttede filtyper", + "Switch Active Organisation": "Bytt aktiv organisasjon", + "Switch Organisation": "Bytt organisasjon", + "Sync Table": "Synkroniser tabell", + "Synchronization Settings": "Synkroniseringsinnstillinger", + "Syncing magic table for {schema}...": "Synkroniserer magisk tabell for {schema}...", + "Syncs": "Synkroniseringer", + "System Default": "Systemstandard", + "System Prompt": "Systemprompt", + "System announcements": "Systemkunngjøringer", + "Table": "Tabell", + "Target ID": "Mål-ID", + "Target Type*": "Måltype*", + "Technical Description": "Teknisk beskrivelse", + "Technical Details": "Tekniske detaljer", + "Technical description for developers and administrators": "Teknisk beskrivelse for utviklere og administratorer", + "Technical measures": "Tekniske tiltak", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatur", + "Templates": "Maler", + "Test": "Test", + "Test Chat": "Testchat", + "Test Connection": "Test tilkobling", + "Test Embedding": "Test innbyggingen", + "Test endpoint executed successfully": "Testendepunkt utført", + "Test webhook delivered successfully": "Testwebhook levert", + "Test webhook delivery failed": "Testwebhookleveranse mislyktes", + "Test webhook sent successfully": "Testwebhook sendt", + "Testing...": "Tester...", + "Text Chunk #{id}": "Tekstbit #{id}", + "Text Extraction": "Tekstuttrekking", + "Text Extraction Engine": "Tekstuttrekkingsmotor", + "Text chunks": "Tekstbiter", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Tekstbiter genereres under filuttrekking og lagres i databasen. Vektorisering leser disse forhåndsbitede filene og konverterer dem til innbygginger.", + "Text extracted successfully": "Tekst uttrekket", + "Text extraction disabled": "Tekstuttrekking deaktivert", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloud Kalender-appen er ikke installert eller aktivert på denne serveren.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloud Kontakter-appen er ikke installert eller aktivert på denne serveren.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloud Deck-appen er ikke installert eller aktivert på denne serveren.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloud E-post-appen er ikke installert eller aktivert på denne serveren.", + "The URL where webhook events will be sent": "URL-en der webhook-hendelser vil bli sendt", + "The requested conversation does not exist": "Den forespurte samtalen finnes ikke", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Det forespurte dashboardet kunne ikke lastes inn. Bekreft URL-en eller velg et annet dashboard fra listen.", + "The saved view and all its search configuration": "Den lagrede visningen og all søkekonfigurasjon", + "There are no audit trail entries matching your current filters.": "Det er ingen revisjonssporoppføringer som samsvarer med gjeldende filtre.", + "There are no deleted items matching your current filters.": "Det er ingen slettede elementer som samsvarer med gjeldende filtre.", + "There are no search trail entries matching your current filters.": "Det er ingen søkesporoppføringer som samsvarer med gjeldende filtre.", + "There are no webhook log entries matching your filters.": "Det er ingen webhook-loggoppføringer som samsvarer med filtrene dine.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "De vil bli beholdt i henhold til skjemaets konfigurerte lagringsperiode og automatisk permanent slettet når lagringsperioden utløper. Lagringsperioden er konfigurerbar per skjema og finnes i skjemaets innstillinger.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "De vil bli beholdt i henhold til skjemaets konfigurerte lagringsperiode og deretter permanent fjernet.", + "This action cannot be undone.": "Denne handlingen kan ikke angres.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Denne handlingen kan ikke angres. Sørg for at ingen samlinger bruker denne ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Denne handlingen kan ikke angres. Sørg for at ingen samlinger bruker denne configset.", + "This action will submit a deactivation request to your administrators.": "Denne handlingen sender en deaktiveringsforespørsel til administratorene dine.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Denne agenten har ingen konfigurerbare visninger eller verktøy, men du kan fortsatt justere RAG-innstillinger nedenfor.", + "This analysis may take some time": "Denne analysen kan ta litt tid", + "This audit trail entry does not contain any change information.": "Denne revisjonssporoppføringen inneholder ingen endringsinformasjon.", + "This configuration has no data defined.": "Denne konfigurasjonen har ingen definerte data.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Dette endepunktet er avviklet. Bruk bitbaserte endepunkter i stedet.", + "This entity has no relations to objects or files": "Denne enheten har ingen relasjoner til objekter eller filer", + "This is a test webhook from OpenRegister": "Dette er en testwebhook fra OpenRegister", + "This is not valid JSON": "Dette er ikke gyldig JSON", + "This key already exists on this schema": "Denne nøkkelen finnes allerede i dette skjemaet", + "This schema must use Magic Table configuration to sync": "Dette skjemaet må bruke Magic Table-konfigurasjon for å synkronisere", + "This schema must use magic table configuration to sync": "Dette skjemaet må bruke magic table-konfigurasjon for å synkronisere", + "This source has no associated registers.": "Denne kilden har ingen tilknyttede registre.", + "This token will only be shown once. Copy it now.": "Dette tokenet vises bare én gang. Kopier det nå.", + "This will copy the _default ConfigSet with the new name": "Dette vil kopiere _default ConfigSet med det nye navnet", + "This will copy the _default configset with the new name": "Dette vil kopiere _default configset med det nye navnet", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Dette vil permanent slette ALLE innbygginger (vektorer) fra databasen. Du må revektorisere alle objekter og filer. Denne handlingen kan ikke angres.\n\nEr du sikker på at du vil fortsette?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Dette vil permanent slette ALLE innbygginger (vektorer) fra databasen. Du må revektorisere alle objekter og filer. Denne handlingen kan ikke angres.\\n\\nEr du sikker på at du vil fortsette?", + "This will permanently delete:": "Dette vil permanent slette:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Dette vil permanent fjerne {count} allerede myk-slettede objekter fra databasen. Denne handlingen kan ikke angres.", + "Timeout (seconds)": "Tidsavbrudd (sekunder)", + "Timestamp": "Tidsstempel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tips: Aktiver bare visninger som trenger semantisk søk for å minimere innbyggingskostnader. Enkle oppslagstabeller trenger sjelden vektorisering.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tips: aktiver bare visninger som trenger semantisk søk for å minimere innbyggingskostnader. Enkle oppslagstabeller trenger sjelden vektorisering.", + "Title": "Tittel", + "Title *": "Tittel *", + "Title Template": "Titelmal", + "Title*": "Tittel*", + "To (e.g. -1 year or 2025-12-31)": "Til (f.eks. -1 år eller 2025-12-31)", + "To Date": "Til dato", + "To add files larger than or equal to 512MB, go to the": "For å legge til filer større enn eller lik 512MB, gå til", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "For å justere bitstørrelse eller strategi, gå til Filkonfigurasjon → Behandlingsgrenser.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "For å justere bitstørrelse eller strategi, gå til filkonfigurasjon → behandlingsgrenser.", + "To change the embedding provider or model, go to LLM Configuration.": "For å endre innbyggingsleverandør eller modell, gå til LLM-konfigurasjon.", + "To change the embedding provider or model, go to LLM configuration.": "For å endre innbyggingsleverandør eller modell, gå til LLM-konfigurasjon.", + "To date": "Til dato", + "Toggle search sidebar": "Veksle søkesidefelt", + "Token Created": "Token opprettet", + "Token Quota (per request)": "Tokenkvote (per forespørsel)", + "Token copied to clipboard": "Token kopiert til utklippstavlen", + "Token name": "Tokennavn", + "Token revoked": "Token tilbakekalt", + "Tools": "Verktøy", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Verktøy utfører med agentens standardbrukertillatelser når ingen brukerøkt er aktiv (f.eks. cron-jobber). Konfigurer standardbrukeren i Innstillinger-fanen.", + "Tools respect the agent's own role and organization boundaries.": "Verktøy respekterer agentens egen rolle og organisasjonsgrenser.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Verktøy respekterer agentens visninger, tillatelser og organisasjonsgrenser.", + "Top Deleters": "Topp-slettere", + "Total": "Total", + "Total Audit Trails": "Totale revisjonsspor", + "Total Chunks Available:": "Totale biter tilgjengelig:", + "Total Deleted Items": "Totale slettede elementer", + "Total Members": "Totale medlemmer", + "Total Objects": "Totale objekter", + "Total Objects in Database:": "Totale objekter i database:", + "Total Organisations": "Totale organisasjoner", + "Total Results": "Totale resultater", + "Total Searches": "Totale søk", + "Total Size": "Total størrelse", + "Total Storage": "Total lagring", + "Total events": "Totale hendelser", + "Total:": "Total:", + "Totals": "Totaler", + "Try searching by UUID or with different keywords": "Prøv å søke etter UUID eller med andre nøkkelord", + "Type": "Type", + "Type \"DELETE COLLECTION\" to confirm:": "Skriv «SLETT SAMLING» for å bekrefte:", + "Type Issue": "Typeproblem", + "Type Variations:": "Typevarianter:", + "Type filter (optional, e.g. email)": "Typefilter (valgfritt, f.eks. e-post)", + "Type must be \"positive\" or \"negative\"": "Type må være «positiv» eller «negativ»", + "Type to search for groups": "Skriv for å søke etter grupper", + "Type to search for objects...": "Skriv for å søke etter objekter...", + "Type to search for organisations": "Skriv for å søke etter organisasjoner", + "Type to search for users": "Skriv for å søke etter brukere", + "Type to search groups...": "Skriv for å søke etter grupper...", + "Type to search users...": "Skriv for å søke etter brukere...", + "Type to search...": "Skriv for å søke...", + "Type your username to confirm": "Skriv inn brukernavnet ditt for å bekrefte", + "URL": "URL", + "URL where Ollama is running": "URL der Ollama kjører", + "UUID copied to clipboard": "UUID kopiert til utklippstavlen", + "UUID:": "UUID:", + "Unchanged": "Uendret", + "Unique Search Terms": "Unike søketermer", + "Unique Terms": "Unike termer", + "Unknown": "Ukjent", + "Unknown Object": "Ukjent objekt", + "Unknown Register": "Ukjent register", + "Unknown Schema": "Ukjent skjema", + "Unknown error": "Ukjent feil", + "Unknown widget type: {type}": "Ukjent widgettype: {type}", + "Unlink email": "Koble fra e-post", + "Unlink event": "Koble fra hendelse", + "Unnamed Object": "Objekt uten navn", + "Untitled": "Uten tittel", + "Untitled View": "Visning uten tittel", + "Update": "Oppdater", + "Update Operations": "Oppdateringsoperasjoner", + "Update available: {local} → {remote}": "Oppdatering tilgjengelig: {local} → {remote}", + "Update configuration: ...": "Oppdater konfigurasjon: ...", + "Update register OAS: ...": "Oppdater register-OAS: ...", + "Update vectors when object data changes (recommended for accurate search)": "Oppdater vektorer når objektdata endres (anbefalt for nøyaktig søk)", + "Updated": "Oppdatert", + "Updated At": "Oppdatert ved", + "Updated:": "Oppdatert:", + "Upload new avatar": "Last opp ny avatar", + "Uploading configuration sets...": "Laster opp konfigurasjonsett...", + "Uptime:": "Oppetid:", + "Url": "Url", + "Usage Count": "Brukstelling", + "Use AI agents for processing and analysis": "Bruk AI-agenter for behandling og analyse", + "Use Large Language Model features": "Bruk store språkmodell-funksjoner", + "Use SolrCloud with Zookeeper for distributed search": "Bruk SolrCloud med Zookeeper for distribuert søk", + "Use SolrCloud with zookeeper for distributed search": "Bruk SolrCloud med zookeeper for distribuert søk", + "Use VNG APIs instead of MongoDB.": "Bruk VNG API-er i stedet for MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Bruk standardområder (Siste 7/30/90 dager, Siste år, Eldre)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Bruk ekstern lagring (f.eks. MongoDB) i stedet for intern Nextcloud-lagring.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Bruk filtre for å begrense revisjonssporoppføringer etter register, skjema, handlingstype, bruker, datoområde eller objekt-ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Bruk filtre for å begrense slettede elementer etter register, skjema, slettedato eller bruker som slettet dem.", + "Use filters to narrow down entities by type or category.": "Bruk filtre for å begrense enheter etter type eller kategori.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Bruk filtre for å begrense søkesporoppføringer etter register, skjema, suksessstatus, bruker, datoområde, søketermer eller ytelsesmålinger.", + "Used By Collections": "Brukt av samlinger", + "Used by": "Brukt av", + "User": "Bruker", + "User Agent Statistics": "Brukeragentstatistikk", + "User Description": "Brukerbeskrivelse", + "User Quota": "Brukerkvote", + "User rate limits cleared successfully": "Brukerhastighetsbegrensninger tømt", + "User removed successfully": "Bruker fjernet", + "User-friendly description shown in forms and help text": "Brukervennlig beskrivelse vist i skjemaer og hjelpetekst", + "Username": "Brukernavn", + "Username for SOLR authentication (optional)": "Brukernavn for SOLR-autentisering (valgfritt)", + "Users": "Brukere", + "Uses": "Bruker", + "Using Pre-Generated Chunks": "Bruker forhåndsgenererte biter", + "Validate": "Valider", + "Validate Objects": "Valider objekter", + "Validation completed for {schema}": "Validering fullført for {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validering fullført for {schema}: {processed} behandlet, {updated} oppdatert, {failed} mislyktes", + "Validation completed successfully": "Validering fullført", + "Validation failed": "Validering mislyktes", + "Value": "Verdi", + "Value list (split on ,)": "Verdiliste (delt på ,)", + "Vector Dimensions": "Vektordimensjoner", + "Vector Search Backend": "Vektorsøk-backend", + "Vector field: _embedding_": "Vektorfelt: _embedding_", + "Vectorization Triggers": "Vektoriseringsutløsere", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorisering fullført: {vectorized} objekter vektorisert, {failed} mislyktes", + "Vectorization failed: {error}": "Vektorisering mislyktes: {error}", + "Vectorize All Files": "Vektoriser alle filer", + "Vectorize All Objects": "Vektoriser alle objekter", + "Vectorize all file types": "Vektoriser alle filtyper", + "Vectorize all views": "Vektoriser alle visninger", + "Vectorize on object creation": "Vektoriser ved objektopprettelse", + "Vectors will be stored in your existing object and file collections": "Vektorer lagres i eksisterende objekt- og filsamlinger", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Bekrefter servertilkobling...", + "Version": "Versjon", + "Version Information": "Versjonsinformasjon", + "Very High": "Svært høy", + "Very high": "Svært høy", + "View": "Visning", + "View API Docs": "Vis API-dokumentasjon", + "View API Documentation": "Vis API-dokumentasjon", + "View APIs": "Vis API-er", + "View Changes": "Vis endringer", + "View Details": "Vis detaljer", + "View Error": "Visningsfeil", + "View File": "Vis fil", + "View Full Details": "Vis alle detaljer", + "View Logs": "Vis logger", + "View Name": "Visningsnavn", + "View Object": "Vis objekt", + "View Parameters": "Visningsparametere", + "View Selection": "Visningsutvalg", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Vis og analyser søkesporlogger med avansert filtrering og analyser", + "View and analyze system audit trails with advanced filtering capabilities": "Vis og analyser systemrevisjonsspor med avanserte filtreringsmuligheter", + "View and manage authorization across registers and schemas": "Vis og administrer autorisasjon på tvers av registre og skjemaer", + "View deleted successfully": "Visning slettet", + "View entity details and manage relations": "Vis enhetsdetaljer og administrer relasjoner", + "View name": "Visningsnavn", + "View name is required": "Visningsnavn er påkrevd", + "View object": "Vis objekt", + "View saved successfully!": "Visning lagret!", + "View search analytics and manage search logs": "Vis søkeanalyse og administrer søkelogger", + "View successfully deleted": "Visning slettet", + "View successfully updated": "Visning oppdatert", + "View updated successfully!": "Visning oppdatert!", + "View webhook delivery logs and filter by webhook": "Vis webhook-leveringslogger og filtrer etter webhook", + "View {count} more": "Vis {count} til", + "Views": "Visninger", + "Visible to end users": "Synlig for sluttbrukere", + "Visible to users": "Synlig for brukere", + "Wait for Response": "Vent på svar", + "Wait for webhook response before continuing (required for request/response flows)": "Vent på webhook-svar før fortsetting (kreves for forespørsel/svar-flyter)", + "Waiting for cluster synchronization...": "Venter på klustersynkronisering...", + "Warmup Names Cache": "Varm opp navnecache", + "Warmup failed": "Oppvarming mislyktes", + "Warning": "Advarsel", + "Warning:": "Advarsel:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Advarsel: Denne egenskapen har inkonsistente typer: {types}. Vurder om alle objekter skal ha samme type.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Vi vil skanne alle objekter som tilhører dette skjemaet for å oppdage nye egenskaper og analysere eksisterende egenskaper for potensielle forbedringer. Prosessen innebærer å undersøke hvert objekts datastruktur, identifisere egenskaper som ikke er definert i gjeldende skjema, og finne muligheter for å forbedre eksisterende egenskapsdefinisjoner med bedre begrensninger, formater og valideringsregler.", + "Webhook": "Webhook", + "Webhook Log Details": "Webhook-loggdetaljer", + "Webhook Logs": "Webhook-logger", + "Webhook created successfully": "Webhook opprettet", + "Webhook deleted": "Webhook slettet", + "Webhook retry delivered successfully": "Webhook-nytt forsøk levert", + "Webhook retry delivery failed": "Webhook-nytt forsøk mislyktes", + "Webhook updated": "Webhook oppdatert", + "Webhook updated successfully": "Webhook oppdatert", + "Webhooks": "Webhooks", + "Weekly": "Ukentlig", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Når agenten kjører uten en brukerøkt (f.eks. planlagte oppgaver), vil denne brukerens kontekst bli brukt", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Når agenten kjører uten en brukerøkt (f.eks. planlagte oppgaver), vil denne brukerens kontekst bli brukt.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Når deaktivert, vil denne fasetten bare vise verdier fra dette skjemaet og vil inkludere et skjemafilter når valgt.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Når aktivert, vil lagring av dette objektet også oppdatere målobjektene for å inkludere en referanse tilbake til dette objektet.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Når aktivert, vil denne egenskapen fjernes fra kildeobjektet etter oppdatering av målobjektene.", + "Workflow Management": "Arbeidsflytadministrasjon", + "Workflows": "Arbeidsflyter", + "Wrap webhook payload in CloudEvents format for better interoperability": "Pakk webhook-nyttelast i CloudEvents-format for bedre interoperabilitet", + "Wrap webhook payload in cloudevents format for better interoperability": "Pakk webhook-nyttelast i cloudevents-format for bedre interoperabilitet", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Ja", + "You": "Du", + "You are not yet a member of any organisations.": "Du er ikke ennå medlem av noen organisasjoner.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Du kan opprette nye ConfigSets basert på _default-malen, eller laste opp egendefinerte direkte til SOLR-serveren din.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Du kan opprette nye configsets basert på _default-malen, eller laste opp egendefinerte direkte til SOLR-serveren din.", + "You do not have access to this conversation": "Du har ikke tilgang til denne samtalen", + "You do not have permission to delete this conversation": "Du har ikke tillatelse til å slette denne samtalen", + "You do not have permission to modify this conversation": "Du har ikke tillatelse til å endre denne samtalen", + "You do not have permission to restore this conversation": "Du har ikke tillatelse til å gjenopprette denne samtalen", + "You do not have permission to view the permission matrix. Admin access is required.": "Du har ikke tillatelse til å se tillatelsesmatrisen. Admin-tilgang er påkrevd.", + "You have selected {count} properties to add to the schema.": "Du har valgt {count} egenskaper å legge til i skjemaet.", + "You must be logged in to favorite views": "Du må være innlogget for å sette visninger som favoritter", + "You need an AI agent to start a conversation.": "Du trenger en AI-agent for å starte en samtale.", + "Your Fireworks AI API key. Get one at": "Din Fireworks AI API-nøkkel. Skaff en på", + "Your OpenAI API key. Get one at": "Din OpenAI API-nøkkel. Skaff en på", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Tilbakemeldingen din er registrert. Du kan eventuelt oppgi ytterligere detaljer her...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Tilbakemeldingen din er registrert. du kan eventuelt oppgi ytterligere detaljer her...", + "Zookeeper Hosts": "Zookeeper-verter", + "Zookeeper Port": "Zookeeper-port", + "Zookeeper Settings (SolrCloud)": "Zookeeper-innstillinger (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper-tilkoblingsstreng for SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper-portnummer (valgfritt, standard er 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ADVARSEL OM PERMANENT SLETTING \\u26A0\\uFE0F\n\nDu er i ferd med å PERMANENT slette ALLE objekter for skjema \"{schema}\":\n\n\\u2022 Aktive objekter: {active}\n\\u2022 Myk-slettede objekter: {deleted}\n\\u2022 Total: {total}\n\nDisse objektene vil bli fullstendig fjernet fra databasen og KAN IKKE gjenopprettes.\n\nEr du helt sikker?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ADVARSEL OM PERMANENT SLETTING \\u26A0\\uFE0F\n\nDu er i ferd med å PERMANENT slette {count} myk-slettede objekter for skjema \"{schema}\".\n\nDisse objektene vil bli fullstendig fjernet fra databasen og KAN IKKE gjenopprettes.\n\nEr du helt sikker?", + "and accessible to all users in selected groups (or all users if no groups selected).": "og tilgjengelig for alle brukere i valgte grupper (eller alle brukere hvis ingen grupper er valgt).", + "and add the files there.": "og legg til filene der.", + "approved": "godkjent", + "are only accessible to invited users.": "er bare tilgjengelige for inviterte brukere.", + "by": "av", + "chunk": "bit", + "chunks": "biter", + "chunks processed": "biter behandlet", + "completed": "fullført", + "configuration(s)": "konfigurasjon(er)", + "conversationId is required": "conversationId er påkrevd", + "create": "opprett", + "delete": "slett", + "deleted objects section": "seksjonen for slettede objekter", + "e.g., lib/Settings/config.json": "f.eks. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "f.eks. lib/Settings/register.json", + "e.g., lib/settings/config.json": "f.eks. lib/settings/config.json", + "e.g., lib/settings/register.json": "f.eks. lib/settings/register.json", + "execute": "utfør", + "failed": "mislyktes", + "file": "fil", + "files": "filer", + "file{plural}": "fil{plural}", + "folder": "mappe", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "i {register}", + "in_progress": "pågår", + "inherited": "arvet", + "list": "liste", + "log{plural}": "logg{plural}", + "manage": "administrer", + "members": "medlemmer", + "menu or contact someone with permission to create agents.": "meny eller kontakt noen med tillatelse til å opprette agenter.", + "message content is required": "meldingsinnhold er påkrevd", + "messages": "meldinger", + "missing": "mangler", + "myapp": "myapp", + "n8n URL and API key are required": "n8n-URL og API-nøkkel er påkrevd", + "n8n configuration saved successfully": "n8n-konfigurasjon lagret", + "n8n connection not configured": "n8n-tilkobling ikke konfigurert", + "n8n connection successful": "n8n-tilkobling vellykket", + "n8n connection test failed: {message}": "n8n-tilkoblingstest mislyktes: {message}", + "n8n connection test successful": "n8n-tilkoblingstest vellykket", + "n8n initialization failed: {message}": "n8n-initialisering mislyktes: {message}", + "n8n integration disabled": "n8n-integrasjon deaktivert", + "n8n integration enabled": "n8n-integrasjon aktivert", + "n8n project initialized successfully": "n8n-prosjekt initialisert", + "n8n settings saved successfully": "n8n-innstillinger lagret", + "notify_push configuration guide": "Konfigurasjonsguide for notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push er installert, men OpenRegister har ennå ikke bekreftet en vellykket push. Utløs en objektlagring for å aktivere, eller sjekk notify_push-konfigurasjonen din.", + "notify_push is installed but not yet active": "notify_push er installert men ikke aktiv ennå", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekter", + "objects processed": "objekter behandlet", + "object{plural}": "objekt{plural}", + "or pick a range": "eller velg et område", + "org-...": "org-...", + "pending": "venter", + "provides a good balance between speed and accuracy.": "gir en god balanse mellom hastighet og nøyaktighet.", + "public": "offentlig", + "read": "les", + "register(s)": "register(e)", + "register(s) selected": "register(e) valgt", + "register{plural}": "register{plural}", + "rejected": "avvist", + "required": "påkrevd", + "results": "resultater", + "schema(s)": "skjema(er)", + "schema(s) selected": "skjema(er) valgt", + "schema{plural}": "skjema{plural}", + "selected objects": "valgte objekter", + "set": "angi", + "sk-...": "sk-...", + "this application": "denne applikasjonen", + "to": "til", + "type must be \"positive\" or \"negative\"": "type må være «positiv» eller «negativ»", + "update": "oppdater", + "vectorized": "vektorisert", + "view": "visning", + "views selected": "visninger valgt", + "waiting": "venter", + "{active} active, {deleted} deleted": "{active} aktive, {deleted} slettet", + "{count} audit trails cleared successfully": "{count} revisjonsspor tømt", + "{count} compliance issue(s) detected": "{count} samsvarproblem(er) oppdaget", + "{count} deletions": "{count} slettinger", + "{count} documents found": "{count} dokumenter funnet", + "{count} email": "{count} e-post", + "{count} emails": "{count} e-poster", + "{count} entries": "{count} oppføringer", + "{count} issues": "{count} problemer", + "{count} members": "{count} medlemmer", + "{count} searches": "{count} søk", + "{count} selected": "{count} valgt", + "{count} widget(s)": "{count} widget(er)", + "{matched} object(s) matched. {erased} erased.": "{matched} objekt(er) samsvarte. {erased} slettet.", + "{minutes} minutes ago": "for {minutes} minutter siden", + "{percentage}% of objects": "{percentage}% av objekter", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} i {register} / {schema}", + "{type} cache cleared successfully": "{type}-cache tømt", + "ℹ️ Current Configuration": "ℹ️ Gjeldende konfigurasjon", + "ℹ️ current configuration": "ℹ️ gjeldende konfigurasjon", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ FARE: Alle {total} objekter vil bli PERMANENT SLETTET fra databasen. Denne handlingen er UGJENKALLELIG og kan ikke angres!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ADVARSEL OM PERMANENT SLETTING ⚠️\n\nDu er i ferd med å PERMANENT slette ALLE objekter for skjema \"{schema}\":\n\n• Aktive objekter: {active}\n• Myk-slettede objekter: {deleted}\n• Total: {total}\n\nDisse objektene vil bli fullstendig fjernet fra databasen og KAN IKKE gjenopprettes.\n\nEr du helt sikker?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ADVARSEL OM PERMANENT SLETTING ⚠️\n\nDu er i ferd med å PERMANENT slette {count} myk-slettede objekter for skjema \"{schema}\".\n\nDisse objektene vil bli fullstendig fjernet fra databasen og KAN IKKE gjenopprettes.\n\nEr du helt sikker?", + "⚡ Batch Processing": "⚡ Batchbehandling", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR kan håndtere millioner av dokumenter med søkeresponstider under ett sekund...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Omfattende kontekst ✓ Mindre sannsynlig å miste detaljer ✗ Tregere svar ✗ Kan inkludere mindre relevant informasjon", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Raskere svar ✓ Mer fokuserte svar ✗ Kan miste relevant informasjon", + "✨ AI Features": "✨ AI-funksjoner", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 I distribuert modus bruker SOLR ZooKeeper for klusterkoordinering...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasettert søk lar brukere bore ned i resultater etter kategorier...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Visste du det? SOLR driver søk for Netflix, Apple og mange andre store nettsteder!", + "💰 View Selection (Cost Optimization)": "💰 Visningsutvalg (kostnadsoptimalisering)", + "📄 Object Serialization": "📄 Objektserialisering", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR gir rik analyse og statistikk om søkeytelse...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets inneholder skjema- og konfigurasjonsfilene for søkeindeksen din...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikering sikrer at søkeindeksen er tilgjengelig selv om noder feiler...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR er en kraftig bedriftssøkeplattform bygget på Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Vektoriseringsinnstillinger", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Autoskalering kan dynamisk legge til eller fjerne noder basert på belastning...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Sikkerhetsfunksjoner inkluderer autentisering, autorisasjon og SSL-kryptering...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-post", + "{count} e-poster" + ], + "Purge {count} object from database": [ + "Tøm {count} objekt fra database", + "Tøm {count} objekter fra database" + ], + "Restore {count} object": [ + "Gjenopprett {count} objekt", + "Gjenopprett {count} objekter" + ], + "Successfully restored {count} object": [ + "Vellykket gjenopprettet {count} objekt", + "Vellykket gjenopprettet {count} objekter" + ], + "Delete {count} object": [ + "Slett {count} objekt", + "Slett {count} objekter" + ], + "Object successfully deleted": [ + "Objekt vellykket slettet", + "Objekter vellykket slettet" + ] + } +} diff --git a/l10n/nl.js b/l10n/nl.js index 365c55217b..44431816c3 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -1,6 +1,67 @@ OC.L10N.register( "openregister", { + "3" : "3", + "30" : "30", + "PERSON" : "PERSOON", + "ORGANIZATION" : "ORGANISATIE", + "LOCATION" : "LOCATIE", + "EMAIL" : "E-MAILADRES", + "PHONE" : "TELEFOONNUMMER", + "ADDRESS" : "ADRES", + "DATE" : "DATUM", + "IBAN" : "IBAN", + "SSN" : "BSN", + "IP_ADDRESS" : "IP-ADRES", + "Detection backends" : "Detectie-backends", + "OpenAnonymiser source" : "OpenAnonymiser-bron", + "Use the built-in OpenAnonymiser (recommended)" : "Gebruik de ingebouwde OpenAnonymiser (aanbevolen)", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed." : "Roept de geïnstalleerde OpenAnonymiser ExApp rechtstreeks aan via AppAPI — geen endpoint nodig.", + "Use an external OpenAnonymiser endpoint" : "Gebruik een externe OpenAnonymiser-endpoint", + "OpenAnonymiser API endpoint" : "OpenAnonymiser API-endpoint", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)" : "URL naar een externe OpenAnonymiser-instantie (Nederlandstalige PII-detectie)", + "Active method: {active}" : "Actieve methode: {active}", + "(auto-selected)" : "(automatisch gekozen)", + "Effective: {effective}" : "Effectief: {effective}", + "falling back" : "valt terug", + "Active" : "Actief", + "Reachable" : "Bereikbaar", + "Unreachable" : "Niet bereikbaar", + "Recommended" : "Aanbevolen", + "Available" : "Beschikbaar", + "Unavailable" : "Niet beschikbaar", + "Not configured" : "Niet geconfigureerd", + "Test connection" : "Verbinding testen", + "Test connection for {backend}" : "Verbinding testen voor {backend}", + "Install OpenAnonymiser" : "OpenAnonymiser installeren", + "Auto (recommended)" : "Automatisch (aanbevolen)", + "Connection successful" : "Connection successful", + "Connection failed: {error}" : "Verbinding mislukt: {error}", + "Connection test failed" : "Verbindingstest mislukt", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection." : "AppAPI is niet geïnstalleerd. Installeer AppAPI en de OpenAnonymiser ExApp om Nederlandse PII-detectie in te schakelen.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection." : "De OpenAnonymiser ExApp is niet geïnstalleerd. Installeer deze om Nederlandse PII-detectie in te schakelen.", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition." : "De OpenAnonymiser ExApp is geïnstalleerd maar uitgeschakeld. Schakel deze in om te gebruiken voor entiteitsherkenning.", + "(no subject)" : "(geen onderwerp)", + "(no title)" : "(geen titel)", + "(unnamed)" : "(naamloos)", + "(untitled card)" : "(naamloze kaart)", + "Activities" : "Activiteiten", + "Add Endpoint" : "Endpoint toevoegen", + "Add Source" : "Bron toevoegen", + "Add card" : "Kaart toevoegen", + "Add contact" : "Contact toevoegen", + "All clear" : "Alles in orde", + "Articles" : "Artikelen", + "Code" : "Code", + "Compliance" : "Naleving", + "Contacts" : "Contacten", + "Create Agent" : "Agent aanmaken", + "Create Application" : "Applicatie aanmaken", + "Create event" : "Afspraak aanmaken", + "Erasure complete" : "Wissen voltooid", + "Erasure preview" : "Voorbeeld van wissen", + "Expand details" : "Details uitklappen", + "Save changes" : "Wijzigingen opslaan", "Object \"%1$s\" created in register \"%2$s\"" : "Object \"%1$s\" aangemaakt in register \"%2$s\"", "Object \"%1$s\" updated in register \"%2$s\"" : "Object \"%1$s\" bijgewerkt in register \"%2$s\"", "Object \"%1$s\" assigned to you in register \"%2$s\"" : "Object \"%1$s\" aan je toegewezen in register \"%2$s\"", @@ -8,8 +69,6 @@ OC.L10N.register( "📄 Object Serialization" : "📄 Objectserialisatie", "🔢 Vectorization Settings" : "🔢 Vectorisatie-instellingen", "💰 View Selection (Cost Optimization)" : "💰 Weergaveselectie (kostenoptimalisatie)", - "3" : "3", - "30" : "30", "AI Assistant" : "AI-assistent", "AI Chat" : "AI-chat", "API Key" : "API-sleutel", @@ -19,7 +78,6 @@ OC.L10N.register( "Action Distribution" : "Actieverdeling", "Action:" : "Actie:", "Actions" : "Acties", - "Active" : "Actief", "Active Collections" : "Actieve collecties", "Active Filters" : "Actieve filters", "Active Organisations" : "Actieve organisaties", @@ -210,7 +268,6 @@ OC.L10N.register( "Connection Successful!" : "Connection Successful!", "Connection failed" : "Connection failed", "Connection protocol" : "Connection protocol", - "Connection successful" : "Connection successful", "Connection timeout in seconds" : "Connection timeout in seconds", "Constant delay between retries (5 minutes)" : "Constant delay between retries (5 minutes)", "Constraint Issue" : "Constraint Issue", @@ -237,6 +294,7 @@ OC.L10N.register( "Copying..." : "Copying...", "Count" : "Aantal", "Counting objects..." : "Counting objects...", + "Costs" : "Kosten", "Create" : "Aanmaken", "Create Collection" : "Create Collection", "Create ConfigSet" : "Create ConfigSet", @@ -314,6 +372,7 @@ OC.L10N.register( "Edit view details" : "Edit weergave details", "Effectiveness" : "Effectiveness", "Email" : "E-mail", + "Emails" : "E-mails", "Embedding Model" : "Embedding Model", "Embedding Provider" : "Embedding Provider", "Embedding generated successfully" : "Embedding succesvol gegenereerd", @@ -821,6 +880,7 @@ OC.L10N.register( "Person" : "Person", "Personal Data" : "Personal Data", "Phone" : "Phone", + "Photos" : "Foto's", "Please create an agent in the" : "Please create an agent in the", "Please select an agent to continue" : "Please select an agent to continue", "Please select which register and schema to use for the new object" : "Please select which register and schema to use for the new object", @@ -830,6 +890,7 @@ OC.L10N.register( "Please wait while we fetch your configurations." : "Even geduld while we fetch your configuraties.", "Please wait while we fetch your deleted items." : "Even geduld while we fetch your verwijderd items.", "Please wait while we fetch your sources." : "Even geduld while we fetch your brons.", + "Polls" : "Polls", "Popular Search Terms" : "Popular Search Terms", "Port" : "Poort", "Prerequisites" : "Prerequisites", @@ -1360,7 +1421,34 @@ OC.L10N.register( "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push is geïnstalleerd, maar OpenRegister heeft nog geen succesvolle push bevestigd. Sla een object op om te activeren, of controleer uw notify_push-configuratie.", "notify_push configuration guide" : "Configuratiegids notify_push", "Realtime push active" : "Realtime push actief", - "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Realtime pushmeldingen zijn actief. Verbonden clients ontvangen onmiddellijke updates wanneer objecten worden aangemaakt, bijgewerkt of verwijderd." + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Realtime pushmeldingen zijn actief. Verbonden clients ontvangen onmiddellijke updates wanneer objecten worden aangemaakt, bijgewerkt of verwijderd.", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser-webpush is geconfigureerd", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "Er is een VAPID-sleutelpaar geconfigureerd. Gebruikers kunnen zich via hun persoonlijke instellingen aanmelden voor browsermeldingen, die ook worden bezorgd wanneer het browsertabblad gesloten is.", + "VAPID public key" : "VAPID publieke sleutel", + "Browser web push is not configured" : "Browser-webpush is niet geconfigureerd", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "Er is nog geen VAPID-sleutelpaar geconfigureerd. Genereer er een met de volgende occ-opdracht om browsermeldingen in te schakelen:", + "Checking browser web push configuration …" : "Browser-webpushconfiguratie controleren …", + "Browser notifications" : "Browsermeldingen", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Ontvang OpenRegister-meldingen als native browsermeldingen, ook wanneer het tabblad gesloten is.", + "This browser does not support web push notifications." : "Deze browser ondersteunt geen webpushmeldingen.", + "Enable browser notifications" : "Browsermeldingen inschakelen", + "Notification permission: granted" : "Meldingstoestemming: verleend", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Meldingstoestemming: geweigerd. Schakel meldingen voor deze site opnieuw in via je browserinstellingen.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Meldingstoestemming: nog niet aangevraagd. Door de schakelaar in te schakelen wordt om toestemming gevraagd.", + "Could not change browser notification settings." : "Kon de instellingen voor browsermeldingen niet wijzigen.", + "Bookmarks" : "Bookmarks", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/nl.json b/l10n/nl.json index 5b7edf4699..5ac26c8cd5 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1,14 +1,69 @@ { "translations": { - "Object \"%1$s\" created in register \"%2$s\"": "Object \"%1$s\" aangemaakt in register \"%2$s\"", - "Object \"%1$s\" updated in register \"%2$s\"": "Object \"%1$s\" bijgewerkt in register \"%2$s\"", - "Object \"%1$s\" assigned to you in register \"%2$s\"": "Object \"%1$s\" aan je toegewezen in register \"%2$s\"", - "object": "object", + "3": "3", + "30": "30", + "PERSON": "PERSOON", + "ORGANIZATION": "ORGANISATIE", + "LOCATION": "LOCATIE", + "EMAIL": "E-MAILADRES", + "PHONE": "TELEFOONNUMMER", + "ADDRESS": "ADRES", + "DATE": "DATUM", + "IBAN": "IBAN", + "SSN": "BSN", + "IP_ADDRESS": "IP-ADRES", + "Detection backends": "Detectie-backends", + "OpenAnonymiser source": "OpenAnonymiser-bron", + "Use the built-in OpenAnonymiser (recommended)": "Gebruik de ingebouwde OpenAnonymiser (aanbevolen)", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Roept de geïnstalleerde OpenAnonymiser ExApp rechtstreeks aan via AppAPI — geen endpoint nodig.", + "Use an external OpenAnonymiser endpoint": "Gebruik een externe OpenAnonymiser-endpoint", + "OpenAnonymiser API endpoint": "OpenAnonymiser API-endpoint", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL naar een externe OpenAnonymiser-instantie (Nederlandstalige PII-detectie)", + "Active method: {active}": "Actieve methode: {active}", + "(auto-selected)": "(automatisch gekozen)", + "Effective: {effective}": "Effectief: {effective}", + "falling back": "valt terug", + "Active": "Actief", + "Reachable": "Bereikbaar", + "Unreachable": "Niet bereikbaar", + "Recommended": "Aanbevolen", + "Available": "Beschikbaar", + "Unavailable": "Niet beschikbaar", + "Not configured": "Niet geconfigureerd", + "Test connection": "Verbinding testen", + "Test connection for {backend}": "Verbinding testen voor {backend}", + "Install OpenAnonymiser": "OpenAnonymiser installeren", + "Auto (recommended)": "Automatisch (aanbevolen)", + "Connection successful": "Verbinding geslaagd", + "Connection failed: {error}": "Verbinding mislukt: {error}", + "Connection test failed": "Verbindingstest mislukt", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is niet geïnstalleerd. Installeer AppAPI en de OpenAnonymiser ExApp om Nederlandse PII-detectie in te schakelen.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "De OpenAnonymiser ExApp is niet geïnstalleerd. Installeer deze om Nederlandse PII-detectie in te schakelen.", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "De OpenAnonymiser ExApp is geïnstalleerd maar uitgeschakeld. Schakel deze in om te gebruiken voor entiteitsherkenning.", + "(no subject)": "(geen onderwerp)", + "(no title)": "(geen titel)", + "(unnamed)": "(naamloos)", + "(untitled card)": "(naamloze kaart)", + "Activities": "Activiteiten", + "Add Endpoint": "Endpoint toevoegen", + "Add Source": "Bron toevoegen", + "Add card": "Kaart toevoegen", + "Add contact": "Contact toevoegen", + "All clear": "Alles in orde", + "Articles": "Artikelen", + "Code": "Code", + "Compliance": "Naleving", + "Contacts": "Contacten", + "Create Agent": "Agent aanmaken", + "Create Application": "Applicatie aanmaken", + "Create event": "Afspraak aanmaken", + "Erasure complete": "Wissen voltooid", + "Erasure preview": "Voorbeeld van wissen", + "Expand details": "Details uitklappen", + "Save changes": "Wijzigingen opslaan", "📄 Object Serialization": "📄 Objectserialisatie", "🔢 Vectorization Settings": "🔢 Vectorisatie-instellingen", "💰 View Selection (Cost Optimization)": "💰 Weergaveselectie (kostenoptimalisatie)", - "3": "3", - "30": "30", "AI Assistant": "AI-assistent", "AI Chat": "AI-chat", "API Key": "API-sleutel", @@ -18,7 +73,6 @@ "Action Distribution": "Actieverdeling", "Action:": "Actie:", "Actions": "Acties", - "Active": "Actief", "Active Collections": "Actieve collecties", "Active Filters": "Actieve filters", "Active Organisations": "Actieve organisaties", @@ -209,7 +263,6 @@ "Connection Successful!": "Verbinding geslaagd!", "Connection failed": "Verbinding mislukt", "Connection protocol": "Verbindingsprotocol", - "Connection successful": "Verbinding geslaagd", "Connection timeout in seconds": "Verbindingstime-out in seconden", "Constant delay between retries (5 minutes)": "Constant delay between retries (5 minutes)", "Constraint Issue": "Beperkingsprobleem", @@ -236,6 +289,7 @@ "Copying...": "Kopiëren...", "Count": "Aantal", "Counting objects...": "Objecten tellen...", + "Costs": "Kosten", "Create": "Aanmaken", "Create Collection": "Create Collection", "Create ConfigSet": "ConfigSet aanmaken", @@ -313,6 +367,7 @@ "Edit view details": "Edit weergave details", "Effectiveness": "Effectiviteit", "Email": "E-mail", + "Emails": "E-mails", "Embedding Model": "Embeddingmodel", "Embedding Provider": "Embeddingprovider", "Embedding generated successfully": "Embedding succesvol gegenereerd", @@ -820,6 +875,7 @@ "Person": "Persoon", "Personal Data": "Persoonsgegevens", "Phone": "Telefoon", + "Photos": "Foto's", "Please create an agent in the": "Maak een agent aan in de", "Please select an agent to continue": "Selecteer een agent om door te gaan", "Please select which register and schema to use for the new object": "Selecteer welk register en schema u wilt gebruiken voor het nieuwe object", @@ -829,6 +885,7 @@ "Please wait while we fetch your configurations.": "Even geduld while we fetch your configuraties.", "Please wait while we fetch your deleted items.": "Even geduld while we fetch your verwijderd items.", "Please wait while we fetch your sources.": "Even geduld while we fetch your brons.", + "Polls": "Polls", "Popular Search Terms": "Populaire zoektermen", "Port": "Poort", "Prerequisites": "Vereisten", @@ -1360,6 +1417,21 @@ "notify_push configuration guide": "Configuratiegids notify_push", "Realtime push active": "Realtime push actief", "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Realtime pushmeldingen zijn actief. Verbonden clients ontvangen onmiddellijke updates wanneer objecten worden aangemaakt, bijgewerkt of verwijderd.", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser-webpush is geconfigureerd", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "Er is een VAPID-sleutelpaar geconfigureerd. Gebruikers kunnen zich via hun persoonlijke instellingen aanmelden voor browsermeldingen, die ook worden bezorgd wanneer het browsertabblad gesloten is.", + "VAPID public key": "VAPID publieke sleutel", + "Browser web push is not configured": "Browser-webpush is niet geconfigureerd", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "Er is nog geen VAPID-sleutelpaar geconfigureerd. Genereer er een met de volgende occ-opdracht om browsermeldingen in te schakelen:", + "Checking browser web push configuration …": "Browser-webpushconfiguratie controleren …", + "Browser notifications": "Browsermeldingen", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Ontvang OpenRegister-meldingen als native browsermeldingen, ook wanneer het tabblad gesloten is.", + "This browser does not support web push notifications.": "Deze browser ondersteunt geen webpushmeldingen.", + "Enable browser notifications": "Browsermeldingen inschakelen", + "Notification permission: granted": "Meldingstoestemming: verleend", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Meldingstoestemming: geweigerd. Schakel meldingen voor deze site opnieuw in via je browserinstellingen.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Meldingstoestemming: nog niet aangevraagd. Door de schakelaar in te schakelen wordt om toestemming gevraagd.", + "Could not change browser notification settings.": "Kon de instellingen voor browsermeldingen niet wijzigen.", "(Dolphin required)": "(Dolphin vereist)", "({count} selected)": "({count} geselecteerd)", ", {failed} failed": ", {failed} mislukt", @@ -2117,33 +2189,488 @@ "{percentage}% of objects": "{percentage}% van objecten", "{property} - {other}": "{property} - {other}", "ℹ️ current configuration": "ℹ️ huidige configuratie", - "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ GEVAAR: Alle {total} objecten worden PERMANENT VERWIJDERD uit de database. Deze actie is ONHERSTELBAAR en kan niet ongedaan worden gemaakt!" + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ GEVAAR: Alle {total} objecten worden PERMANENT VERWIJDERD uit de database. Deze actie is ONHERSTELBAAR en kan niet ongedaan worden gemaakt!", + "{count} compliance issue(s) detected": "{count} compliance-probleem(en) gedetecteerd", + "{count} email": "{count} e-mail", + "{count} emails": "{count} e-mails", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} object(en) overeengekomen. {erased} gewist.", + "{type} cache cleared successfully": "{type}-cache succesvol geleegd", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F WAARSCHUWING PERMANENTE VERWIJDERING \\u26A0\\uFE0F\n\nU staat op het punt om {count} zacht-verwijderde objecten voor schema \"{schema}\" PERMANENT te verwijderen.\n\nDeze objecten worden volledig uit de database verwijderd en kunnen NIET worden hersteld.\n\nWeet u het absoluut zeker?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F WAARSCHUWING PERMANENTE VERWIJDERING \\u26A0\\uFE0F\n\nU staat op het punt om ALLE objecten voor schema \"{schema}\" PERMANENT te verwijderen:\n\n\\u2022 Actieve objecten: {active}\n\\u2022 Zacht-verwijderde objecten: {deleted}\n\\u2022 Totaal: {total}\n\nDeze objecten worden volledig uit de database verwijderd en kunnen NIET worden hersteld.\n\nWeet u het absoluut zeker?", + "Add a contact from any of your address books to associate it with this object.": "Voeg een contact toe uit een van uw adresboeken om het aan dit object te koppelen.", + "Agent successfully created": "Agent succesvol aangemaakt", + "and accessible to all users in selected groups (or all users if no groups selected).": "en toegankelijk voor alle gebruikers in geselecteerde groepen (of alle gebruikers als er geen groepen zijn geselecteerd).", + "App store cache invalidated: {invalidated}": "App store-cache ongeldig gemaakt: {invalidated}", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Deze verwerkingsactiviteit archiveren? Audit-trailregels blijven ernaar verwijzen.", + "are only accessible to invited users.": "zijn alleen toegankelijk voor uitgenodigde gebruikers.", + "Attachment added to {name}": "Bijlage toegevoegd aan {name}", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art. 30 §4: dit rapport koppelt elke verwerkingsactiviteit aan de totale audit-trailtellingen die eraan zijn toegeschreven. Auditors en de Autoriteit Persoonsgegevens gebruiken dit voor toezichtcontrole.", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601-duur, bijv. P10Y, P30D)", + "by": "door", + "Calendar integration is not available": "Agenda-integratie is niet beschikbaar", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Klik op Compliancescan uitvoeren om schema's te vinden waar PII is gedetecteerd maar geen verwerkingsactiviteit-annotatie bestaat.", + "Click to subscribe to notifications": "Klik om u te abonneren op meldingen", + "Click to unsubscribe from notifications": "Klik om u af te melden voor meldingen", + "Code (short readable key, e.g. v-2026-001)": "Code (korte leesbare sleutel, bijv. v-2026-001)", + "Contacts integration is not available": "Contacten-integratie is niet beschikbaar", + "Create a new event or link an existing one from any of your calendars.": "Maak een nieuwe gebeurtenis aan of koppel een bestaande gebeurtenis uit een van uw agenda's.", + "Create or link a Deck card to track work on this object.": "Maak een Deck-kaart aan of koppel er een om het werk aan dit object te volgen.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Maak de eerste verwerkingsactiviteit aan om audit-trailregels te taggen met hun AVG Art. 30-toeschrijving.", + "CSV": "CSV", + "Dashboard not found": "Dashboard niet gevonden", + "Data-subject access request": "Inzageverzoek betrokkene", + "Deck": "Deck", + "Deck integration is not available": "Deck-integratie is niet beschikbaar", + "Delete {count} object": "{count} object verwijderen", + "Delete {count} objects": "{count} objecten verwijderen", + "Doelbinding *": "Doelbinding *", + "DSAR": "DSAR", + "Edit verwerkingsactiviteit": "Verwerkingsactiviteit bewerken", + "Erase (Art 17)": "Wissen (Art. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "{count} object(en) voor deze betrokkene wissen? Deze actie wordt vastgelegd in de audit trail.", + "Every schema with detected PII has a processing-activity annotation.": "Elk schema met gedetecteerde PII heeft een verwerkingsactiviteit-annotatie.", + "Excel (.xlsx)": "Excel (.xlsx)", + "Failed to add attachment to object": "Bijlage toevoegen aan object mislukt", + "Failed to clear {type} cache: {error}": "Legen van {type}-cache mislukt: {error}", + "Failed to clear audit trails: {error}": "Wissen van audit trails mislukt: {error}", + "Failed to clear blob objects: {error}": "Wissen van blob-objecten mislukt: {error}", + "Failed to clear cache: {error}": "Legen van cache mislukt: {error}", + "Failed to clear search trails: {error}": "Wissen van zoek-trails mislukt: {error}", + "Failed to discover files: {error}": "Ontdekken van bestanden mislukt: {error}", + "Failed to invalidate app store cache: {error}": "Ongeldig maken van app store-cache mislukt: {error}", + "Failed to load cache statistics: {error}": "Laden van cachestatistieken mislukt: {error}", + "Failed to load Deck cards": "Laden van Deck-kaarten mislukt", + "Failed to load linked contacts": "Laden van gekoppelde contacten mislukt", + "Failed to load linked emails": "Laden van gekoppelde e-mails mislukt", + "Failed to load linked events": "Laden van gekoppelde gebeurtenissen mislukt", + "Failed to load related entities": "Laden van gerelateerde entiteiten mislukt", + "Failed to load settings: {error}": "Laden van instellingen mislukt: {error}", + "Failed to load statistics: {error}": "Laden van statistieken mislukt: {error}", + "Failed to save file settings: {error}": "Opslaan van bestandsinstellingen mislukt: {error}", + "Failed to save LLM settings: {error}": "Opslaan van LLM-instellingen mislukt: {error}", + "Failed to save settings: {error}": "Opslaan van instellingen mislukt: {error}", + "Failed to save warmup interval: {error}": "Opslaan van opwarm-interval mislukt: {error}", + "Failed to setup SOLR: {error}": "Instellen van SOLR mislukt: {error}", + "Failed to start processing {type} files: {error}": "Starten van verwerken van {type}-bestanden mislukt: {error}", + "Failed to test SOLR connection: {error}": "Testen van SOLR-verbinding mislukt: {error}", + "Failed to update LLM settings: {error}": "Bijwerken van LLM-instellingen mislukt: {error}", + "Failed to update Multitenancy settings: {error}": "Bijwerken van Multitenancy-instellingen mislukt: {error}", + "Failed to update RBAC settings: {error}": "Bijwerken van RBAC-instellingen mislukt: {error}", + "Failed to update Retention settings: {error}": "Bijwerken van bewaarinstellingen mislukt: {error}", + "Failed to update SOLR settings: {error}": "Bijwerken van SOLR-instellingen mislukt: {error}", + "File settings saved successfully": "Bestandsinstellingen succesvol opgeslagen", + "Filter and search entities": "Entiteiten filteren en zoeken", + "Generate report": "Rapport genereren", + "Generate the verantwoordingsdocument": "Het verantwoordingsdocument genereren", + "Generated: {time}": "Gegenereerd: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Krijg realtime inzicht in de datakwaliteit van uw organisatie door te focussen op registers, schemadefinities en objectopslag en -gebruik.", + "HTML preview": "HTML-voorbeeld", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importeer het report-bundle.json-sjabloon om het `reports`-register te krijgen en maak vervolgens uw eerste dashboard aan via de standaard object-UI. Dashboards declareren hun widgets in JSON en de renderer voedt elke widget met live aggregatiegegevens.", + "Integrations": "Integraties", + "Inzage (Art 15)": "Inzage (Art. 15)", + "Inzage results": "Inzageresultaten", + "Languages": "Talen", + "Link an email from the Mail app sidebar to associate it with this object.": "Koppel een e-mail vanuit de zijbalk van de Mail-app om deze aan dit object te koppelen.", + "Link existing event": "Bestaande gebeurtenis koppelen", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Gekoppelde e-mails, agendagebeurtenissen, contacten en Deck-kaarten verschijnen hier.", + "LLM settings saved successfully": "LLM-instellingen succesvol opgeslagen", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Lokaliseer elk object dat naar een betrokkene verwijst (Art. 15 inzage), bekijk een voorbeeld van een wissing (Art. 17 vergetelheid) of exporteer hun gegevens (Art. 20 portabiliteit).", + "Mail integration is not available": "Mail-integratie is niet beschikbaar", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Beheer verwerkingsactiviteiten, voer inzageverzoeken van betrokkenen uit en controleer compliance onder de EU-AVG / Nederlandse AVG.", + "Mass validation completed successfully": "Massavalidatie succesvol voltooid", + "Mass validation failed: {error}": "Massavalidatie mislukt: {error}", + "missing": "ontbreekt", + "Multitenancy settings updated successfully": "Multitenancy-instellingen succesvol bijgewerkt", + "Naam": "Naam", + "Naam *": "Naam *", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Namen-cache succesvol opgewarmd: {count} namen geladen in {time}. {message}", + "New activity": "Nieuwe activiteit", + "New Configuration": "Nieuwe configuratie", + "New verwerkingsactiviteit": "Nieuwe verwerkingsactiviteit", + "No contacts linked to this object": "Geen contacten gekoppeld aan dit object", + "No dashboards yet": "Nog geen dashboards", + "No Deck cards linked to this object": "Geen Deck-kaarten gekoppeld aan dit object", + "No emails linked to this object": "Geen e-mails gekoppeld aan dit object", + "No events linked to this object": "Geen gebeurtenissen gekoppeld aan dit object", + "No matches": "Geen overeenkomsten", + "No personal data was found for this subject identifier.": "Er zijn geen persoonsgegevens gevonden voor deze betrokkene-identificatie.", + "No processing activities yet": "Nog geen verwerkingsactiviteiten", + "No related entities": "Geen gerelateerde entiteiten", + "No saved views yet. create one in the search tab!": "Nog geen opgeslagen weergaven. Maak er een aan op het tabblad Zoeken!", + "No scan run yet": "Nog geen scan uitgevoerd", + "Object metadata incomplete for file upload": "Objectmetadata onvolledig voor bestandsupload", + "Object successfully deleted": "Object succesvol verwijderd", + "Objects successfully deleted": "Objecten succesvol verwijderd", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Door de beheerder gedefinieerde dashboards en geplande rapporten. Elk dashboard is een eersteklas object in het `reports`-register; widgets worden gedeclareerd in de `widgets`-array van het dashboard en live gerenderd vanuit aggregaties / GraphQL.", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Geordende BCP 47-taaltags. De eerste taal is de standaard van het register en stuurt de Accept-Language-fallback voor vertaalbare eigenschappen aan.", + "Organisation successfully created": "Organisatie succesvol aangemaakt", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "PDF": "PDF", + "PII hits": "PII-treffers", + "PII rows": "PII-regels", + "Portabiliteit (Art 20)": "Portabiliteit (Art. 20)", + "Preview erasure": "Voorbeeld van wissing", + "Property could not be added": "Eigenschap kon niet worden toegevoegd", + "Property successfully added": "Eigenschap succesvol toegevoegd", + "public": "openbaar", + "Purge {count} object from database": "{count} object opschonen uit database", + "Purge {count} objects from database": "{count} objecten opschonen uit database", + "RBAC settings updated successfully": "RBAC-instellingen succesvol bijgewerkt", + "Rebase operation completed successfully": "Rebase-bewerking succesvol voltooid", + "Rebase operation failed: {error}": "Rebase-bewerking mislukt: {error}", + "Rechtsgrond": "Rechtsgrond", + "Register annotation": "Register-annotatie", + "Register objects": "Register-objecten", + "Remove contact": "Contact verwijderen", + "Remove Deck card": "Deck-kaart verwijderen", + "Reports": "Rapporten", + "Restore {count} object": "{count} object herstellen", + "Restore {count} objects": "{count} objecten herstellen", + "Retention settings updated successfully": "Bewaarinstellingen succesvol bijgewerkt", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Bekijk de geselecteerde objecten hieronder. U kunt objecten die u niet wilt verwijderen weghalen door op de verwijderknop te klikken.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Bekijk de geselecteerde objecten hieronder. U kunt objecten die u niet permanent wilt verwijderen weghalen door op de verwijderknop te klikken. Deze actie kan niet ongedaan worden gemaakt.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Bekijk de geselecteerde objecten hieronder. U kunt objecten die u niet wilt herstellen weghalen door op de verwijderknop te klikken. Objecten worden hersteld naar hun oorspronkelijke locatie.", + "Run compliance scan": "Compliancescan uitvoeren", + "Scheduled": "Gepland", + "Schema annotation": "Schema-annotatie", + "selected objects": "geselecteerde objecten", + "set": "ingesteld", + "Settings updated successfully": "Instellingen succesvol bijgewerkt", + "Show": "Tonen", + "Showing {count} dashboard(s)": "{count} dashboard(s) worden getoond", + "Showing {count} processing activities": "{count} verwerkingsactiviteiten worden getoond", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Alle objecten voor dit schema zacht verwijderen ({active} actief, {deleted} al verwijderd)", + "SOLR connection test failed: {error}": "SOLR-verbindingstest mislukt: {error}", + "SOLR connection test successful!": "SOLR-verbindingstest geslaagd!", + "SOLR index warmup completed successfully": "Opwarmen van SOLR-index succesvol voltooid", + "SOLR settings updated successfully": "SOLR-instellingen succesvol bijgewerkt", + "SOLR setup completed successfully!": "SOLR-instelling succesvol voltooid!", + "SOLR setup failed: {error}": "SOLR-instelling mislukt: {error}", + "SOLR warmup failed: {error}": "SOLR-opwarming mislukt: {error}", + "Subject identifier (email, BSN, name, etc.)": "Betrokkene-identificatie (e-mail, BSN, naam, enz.)", + "Subscribe": "Abonneren", + "Subscribed": "Geabonneerd", + "Successfully cleared {count} audit trails": "{count} audit trails succesvol gewist", + "Successfully cleared {count} blob storage objects": "{count} blob-opslagobjecten succesvol gewist", + "Successfully cleared {count} search trails": "{count} zoek-trails succesvol gewist", + "Successfully restored {count} object": "{count} object succesvol hersteld", + "Successfully restored {count} objects": "{count} objecten succesvol hersteld", + "Technical Details": "Technische details", + "Technische maatregelen": "Technische maatregelen", + "The Nextcloud Calendar app is not installed or enabled on this server.": "De Nextcloud Agenda-app is niet geïnstalleerd of ingeschakeld op deze server.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "De Nextcloud Contacten-app is niet geïnstalleerd of ingeschakeld op deze server.", + "The Nextcloud Deck app is not installed or enabled on this server.": "De Nextcloud Deck-app is niet geïnstalleerd of ingeschakeld op deze server.", + "The Nextcloud Mail app is not installed or enabled on this server.": "De Nextcloud Mail-app is niet geïnstalleerd of ingeschakeld op deze server.", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Het gevraagde dashboard kon niet worden geladen. Controleer de URL of kies een ander dashboard uit de lijst.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Ze worden bewaard volgens de geconfigureerde bewaartermijn van hun schema en automatisch permanent verwijderd wanneer de bewaartermijn verloopt. De bewaartermijn is per schema configureerbaar en is te vinden in de instellingen van het schema.", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Tools worden uitgevoerd met de standaard gebruikersrechten van de agent wanneer er geen gebruikerssessie actief is (bijv. cron-taken). Configureer de standaardgebruiker op het tabblad Instellingen.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Tools respecteren de weergaven, rechten en organisatiegrenzen van de agent.", + "Total events": "Totaal gebeurtenissen", + "Type filter (optional, e.g. email)": "Typefilter (optioneel, bijv. e-mail)", + "Unknown widget type: {type}": "Onbekend widget-type: {type}", + "Unlink email": "E-mail ontkoppelen", + "Unlink event": "Gebeurtenis ontkoppelen", + "Untitled": "Naamloos", + "Use filters to narrow down entities by type or category.": "Gebruik filters om entiteiten te beperken op type of categorie.", + "Verantwoording": "Verantwoording", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Waarschuwing: Deze eigenschap heeft inconsistente typen: {types}. Overweeg of alle objecten hetzelfde type moeten hebben.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "We scannen alle objecten die bij dit schema horen om nieuwe eigenschappen te ontdekken en bestaande eigenschappen te analyseren op mogelijke verbeteringen. Het proces omvat het onderzoeken van de datastructuur van elk object, het identificeren van eigenschappen die niet in het huidige schema zijn gedefinieerd, en het vinden van mogelijkheden om bestaande eigenschapdefinities te verbeteren met betere beperkingen, formaten en validatieregels.", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Wanneer de agent zonder gebruikerssessie draait (bijv. geplande taken), wordt de context van deze gebruiker gebruikt", + "Workflows": "Workflows", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "Bookmarks": "Bookmarks", + "Categories of data subjects (one per line)": "Categories of data subjects (one per line)", + "Categories of personal data (one per line)": "Categories of personal data (one per line)", + "Configuring field mappings...": "Configuring field mappings...", + "Connecting to SOLR cluster...": "Connecting to SOLR cluster...", + "Creating search collections...": "Creating search collections...", + "Edit processing activity": "Edit processing activity", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Finalizing setup...": "Finalizing setup...", + "Forbidden": "Forbidden", + "Initializing SOLR setup...": "Initializing SOLR setup...", + "Legal basis *": "Legal basis *", + "New processing activity": "New processing activity", + "Optimizing search performance...": "Optimizing search performance...", + "Organisational measures": "Organisational measures", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Purpose limitation *": "Purpose limitation *", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Retention period (ISO-8601 duration, e.g. P10Y, P30D)", + "SOLR setup failed. Please check the configuration and try again.": "SOLR setup failed. Please check the configuration and try again.", + "Select all entities": "Select all entities", + "Select entity {value}": "Select entity {value}", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "Technical measures": "Technical measures", + "Uploading configuration sets...": "Uploading configuration sets...", + "Verifying server connectivity...": "Verifying server connectivity...", + "Waiting for cluster synchronization...": "Waiting for cluster synchronization...", + "You do not have access to this object": "You do not have access to this object", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR can handle millions of documents with sub-second search response times...", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Faceted search allows users to drill down into results by categories...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR provides rich analytics and statistics about search performance...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets contain the schema and configuration files for your search index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replication ensures your search index is available even if nodes fail...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Auto-scaling can dynamically add or remove nodes based on load...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Security features include authentication, authorization, and SSL encryption...", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Allow private/loopback targets": "Allow private/loopback targets", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Change date range": "Change date range", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Text searches only": "Text searches only", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." }, - "pluralForm": "nplurals=2; plural=(n != 1);", "plurals": { "{count} email": [ - "{count} e-mail", - "{count} e-mails" + "{count} email", + "{count} emails" ], "Purge {count} object from database": [ - "Verwijder {count} object uit de database", - "Verwijder {count} objecten uit de database" + "Purge {count} object from database", + "Purge {count} objects from database" ], "Restore {count} object": [ - "Herstel {count} object", - "Herstel {count} objecten" + "Restore {count} object", + "Restore {count} objects" ], "Successfully restored {count} object": [ - "{count} object succesvol hersteld", - "{count} objecten succesvol hersteld" + "Successfully restored {count} object", + "Successfully restored {count} objects" ], "Delete {count} object": [ - "Verwijder {count} object", - "Verwijder {count} objecten" + "Delete {count} object", + "Delete {count} objects" ], "Object successfully deleted": [ - "Object succesvol verwijderd", - "Objecten succesvol verwijderd" + "Object successfully deleted", + "Objects successfully deleted" ] } } diff --git a/l10n/pl.js b/l10n/pl.js new file mode 100644 index 0000000000..2f6768f369 --- /dev/null +++ b/l10n/pl.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(brak tematu)", + "(no title)" : "(brak tytułu)", + "(unnamed)" : "(bez nazwy)", + "(untitled card)" : "(karta bez tytułu)", + "Activities" : "Aktywności", + "Add Endpoint" : "Dodaj punkt końcowy", + "Add Source" : "Dodaj źródło", + "Add card" : "Dodaj kartę", + "Add contact" : "Dodaj kontakt", + "All clear" : "Wszystko w porządku", + "Articles" : "Artykuły", + "Code" : "Kod", + "Compliance" : "Zgodność", + "Contacts" : "Kontakty", + "Create Agent" : "Utwórz agenta", + "Create Application" : "Utwórz aplikację", + "Create event" : "Utwórz wydarzenie", + "Erasure complete" : "Usuwanie zakończone", + "Erasure preview" : "Podgląd usuwania", + "Expand details" : "Rozwiń szczegóły", + "Save changes" : "Zapisz zmiany", + "📄 Object Serialization" : "📄 Serializacja obiektu", + "🔢 Vectorization Settings" : "🔢 Ustawienia wektoryzacji", + "💰 View Selection (Cost Optimization)" : "💰 Wybór widoku (optymalizacja kosztów)", + "AI Assistant" : "Asystent AI", + "AI Chat" : "Czat AI", + "API Key" : "Klucz API", + "API tokens saved successfully" : "Tokeny API zostały pomyślnie zapisane", + "About ConfigSets" : "O ConfigSets", + "Action" : "Akcja", + "Action Distribution" : "Rozkład akcji", + "Action:" : "Akcja:", + "Actions" : "Akcje", + "Active" : "Aktywne", + "Active Collections" : "Aktywne kolekcje", + "Active Filters" : "Aktywne filtry", + "Active Organisations" : "Aktywne organizacje", + "Active filters:" : "Aktywne filtry:", + "Active organisation set successfully" : "Aktywna organizacja została pomyślnie ustawiona", + "Activity Period" : "Okres aktywności", + "Add Groups" : "Dodaj grupy", + "Add Schema" : "Dodaj Schemat", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Dodaj tytuły Schematów, opisy i informacje o Rejestrze, aby zapewnić bogatszy kontekst dla wyszukiwania", + "Add to favorites" : "Dodaj do ulubionych", + "Added" : "Dodano", + "Added to favorites" : "Dodano do ulubionych", + "Additional Information" : "Dodatkowe informacje", + "Additional feedback saved. Thank you!" : "Dodatkowa opinia została zapisana. Dziękujemy!", + "Admin" : "Administrator", + "Advanced" : "Zaawansowane", + "Advanced Filters" : "Filtry zaawansowane", + "Advanced Options" : "Opcje zaawansowane", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Filtry zaawansowane nie są dostępne podczas korzystania ze źródła bazy danych. Przełącz na Auto lub indeks SOLR, aby uzyskać opcje filtrowania.", + "After" : "Po", + "Agent deleted successfully" : "Agent został pomyślnie usunięty", + "Agents" : "Agenci", + "All" : "Wszystkie", + "All Categories" : "Wszystkie kategorie", + "All Collections" : "Wszystkie kolekcje", + "All Confidence Levels" : "Wszystkie poziomy pewności", + "All ConfigSets" : "Wszystkie ConfigSets", + "All Files" : "Wszystkie Pliki", + "All Levels" : "Wszystkie poziomy", + "All Types" : "Wszystkie typy", + "All Webhooks" : "Wszystkie webhooki", + "All actions" : "Wszystkie akcje", + "All audit trails cleared successfully" : "Wszystkie ścieżki audytu zostały pomyślnie wyczyszczone", + "All registers" : "Wszystkie Rejestry", + "All schemas" : "Wszystkie Schematy", + "All searches" : "Wszystkie wyszukiwania", + "All users" : "Wszyscy użytkownicy", + "An error occurred while clearing audit trails" : "Wystąpił błąd podczas czyszczenia ścieżek audytu", + "An error occurred while deleting the audit trail" : "Wystąpił błąd podczas usuwania ścieżki audytu", + "An error occurred while deleting the view" : "Wystąpił błąd podczas usuwania widoku", + "An error occurred while permanently deleting the objects" : "Wystąpił błąd podczas trwałego usuwania Obiektów", + "Analysis completed:" : "Analiza zakończona:", + "Analysis steps:" : "Kroki analizy:", + "Analytics" : "Analityka", + "Analyze Objects" : "Analizuj Obiekty", + "Analyze Properties" : "Analizuj Właściwości", + "Analyze existing properties for improvement opportunities" : "Przeanalizuj istniejące Właściwości pod kątem możliwości ulepszenia", + "Analyzing..." : "Analizowanie...", + "Any favorites and sharing settings for this view" : "Wszelkie ulubione i ustawienia udostępniania dla tego widoku", + "Any user" : "Dowolny użytkownik", + "App store cache invalidated successfully" : "Pamięć podręczna sklepu z aplikacjami została pomyślnie unieważniona", + "Application deleted successfully" : "Aplikacja została pomyślnie usunięta", + "Applications" : "Aplikacje", + "Apply Changes" : "Zastosuj zmiany", + "Archive" : "Archiwizuj", + "Archive conversation" : "Archiwizuj rozmowę", + "Archived conversations are hidden from your active list" : "Zarchiwizowane rozmowy są ukryte na liście aktywnych", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Czy na pewno chcesz wyczyścić stare ścieżki wyszukiwania? Spowoduje to usunięcie wpisów starszych niż 30 dni.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Czy na pewno chcesz usunąć wybrane ścieżki audytu? Tej operacji nie można cofnąć.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Czy na pewno chcesz usunąć wybrane ścieżki wyszukiwania? Tej operacji nie można cofnąć.", + "Are you sure you want to delete this ConfigSet?" : "Czy na pewno chcesz usunąć ten ConfigSet?", + "Are you sure you want to permanently delete" : "Czy na pewno chcesz trwale usunąć", + "Ask a question..." : "Zadaj pytanie...", + "Ask questions about your data using natural language" : "Zadawaj pytania o swoje dane, używając języka naturalnego", + "Attempt" : "Próba", + "Audit Trail Changes" : "Zmiany ścieżki audytu", + "Audit Trail Details" : "Szczegóły ścieżki audytu", + "Audit Trail Management" : "Zarządzanie ścieżką audytu", + "Audit Trail Statistics" : "Statystyki ścieżki audytu", + "Audit Trails" : "Ścieżki audytu", + "Audit trail data copied to clipboard" : "Dane ścieżki audytu skopiowano do schowka", + "Audit trail deleted successfully" : "Ścieżka audytu została pomyślnie usunięta", + "Audit trail successfully deleted" : "Ścieżka audytu została pomyślnie usunięta", + "Authentication" : "Uwierzytelnianie", + "Auto-commit disabled" : "Automatyczne zatwierdzanie wyłączone", + "Auto-commit enabled" : "Automatyczne zatwierdzanie włączone", + "Auto-create Default Organisation" : "Automatycznie utwórz domyślną organizację", + "Auto-retry failed vectorizations" : "Automatycznie ponawiaj nieudane wektoryzacje", + "Automatically commit changes to SOLR index" : "Automatycznie zatwierdzaj zmiany w indeksie SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Automatycznie utwórz domyślną organizację, jeśli żadna nie istnieje podczas inicjalizacji aplikacji", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Automatycznie generuj osadzenia wektorowe z fragmentów tekstu, gdy Pliki są przesyłane i przetwarzane", + "Automatically generate vector embeddings when objects are created or updated" : "Automatycznie generuj osadzenia wektorowe, gdy Obiekty są tworzone lub aktualizowane", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Automatycznie ponawiaj nieudane próby wektoryzacji (maks. 3 próby)", + "Available Filters" : "Dostępne filtry", + "Available Workflows" : "Dostępne przepływy pracy", + "Avg Execution Time" : "Śr. czas wykonania", + "Avg Members/Org" : "Śr. członków/org.", + "Avg Object Views/Session" : "Śr. wyświetleń Obiektów/sesję", + "Avg Response Time" : "Śr. czas odpowiedzi", + "Avg Results/Search" : "Śr. wyników/wyszukiwanie", + "Avg Searches/Session" : "Śr. wyszukiwań/sesję", + "Back" : "Wstecz", + "Back to Entities" : "Powrót do encji", + "Back to Registers" : "Powrót do Rejestrów", + "Back to Webhooks" : "Powrót do webhooków", + "Back to applications" : "Powrót do aplikacji", + "Back to entities" : "Powrót do encji", + "Backend updated successfully. Please reload the application." : "Backend został pomyślnie zaktualizowany. Załaduj ponownie aplikację.", + "Base URL (Optional)" : "Bazowy adres URL (opcjonalnie)", + "Basic Information" : "Podstawowe informacje", + "Batch Size" : "Rozmiar partii", + "Batch extraction completed" : "Ekstrakcja wsadowa zakończona", + "Before" : "Przed", + "Before object vectorization can work:" : "Zanim wektoryzacja Obiektów zacznie działać:", + "Behavior Issue" : "Problem z zachowaniem", + "Bookmarks" : "Zakładki", + "Blob storage has been retired. All objects now use magic tables." : "Pamięć blob została wycofana. Wszystkie Obiekty używają teraz tabel magicznych.", + "Branch" : "Gałąź", + "Bulk delete operation completed successfully" : "Operacja masowego usuwania zakończona pomyślnie", + "Bulk save operation completed successfully" : "Operacja masowego zapisu zakończona pomyślnie", + "Business Data" : "Dane biznesowe", + "Cache cleared successfully" : "Pamięć podręczna została pomyślnie wyczyszczona", + "Calculate Sizes" : "Oblicz rozmiary", + "Cancel" : "Anuluj", + "Cannot delete: objects are still attached" : "Nie można usunąć: Obiekty są nadal dołączone", + "Category" : "Kategoria", + "Change Type" : "Typ zmiany", + "Changes" : "Zmiany", + "Changes copied to clipboard" : "Zmiany skopiowano do schowka", + "Chat Model" : "Model czatu", + "Chat Provider" : "Dostawca czatu", + "Chat Provider (RAG)" : "Dostawca czatu (RAG)", + "Chat Settings" : "Ustawienia czatu", + "Chat provider connection successful!" : "Połączenie z dostawcą czatu udane!", + "Chat settings" : "Ustawienia czatu", + "Choose a register" : "Wybierz Rejestr", + "Choose a schema" : "Wybierz Schemat", + "Choose how vector similarity calculations are performed for semantic search" : "Wybierz, w jaki sposób obliczane jest podobieństwo wektorów dla wyszukiwania semantycznego", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Wybierz, które typy Plików uwzględnić w procesie wektoryzacji. Przetwarzane będą tylko Pliki z wyodrębnionym tekstem i fragmentami.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Wybierz, które widoki uwzględnić w procesie wektoryzacji. Pozostaw puste, aby przetworzyć wszystkie widoki na podstawie konfiguracji.", + "Chunk" : "Fragment", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Usuwanie fragmentów nie zostało jeszcze zaimplementowane. Użyj punktów końcowych opartych na fragmentach.", + "Chunks" : "Fragmenty", + "Chunks to Vectorize:" : "Fragmenty do wektoryzacji:", + "Cleanup Old Trails" : "Wyczyść stare ścieżki", + "Cleanup completed" : "Czyszczenie zakończone", + "Clear All Embeddings" : "Wyczyść wszystkie osadzenia", + "Clear Entries" : "Wyczyść wpisy", + "Clear Filtered Audit Trails" : "Wyczyść filtrowane ścieżki audytu", + "Clear Filters" : "Wyczyść filtry", + "Clear Index" : "Wyczyść indeks", + "Clear Selection" : "Wyczyść zaznaczenie", + "Clear all" : "Wyczyść wszystko", + "Clear all filters" : "Wyczyść wszystkie filtry", + "Clear date range" : "Wyczyść zakres dat", + "Clearing..." : "Czyszczenie...", + "Close" : "Zamknij", + "Collection Management" : "Zarządzanie kolekcjami", + "Collection Name" : "Nazwa kolekcji", + "Collection assignments updated successfully" : "Przypisania kolekcji zostały pomyślnie zaktualizowane", + "Collection cleared successfully" : "Kolekcja została pomyślnie wyczyszczona", + "Collection copied successfully" : "Kolekcja została pomyślnie skopiowana", + "Collection created successfully" : "Kolekcja została pomyślnie utworzona", + "Collection deleted successfully" : "Kolekcja została pomyślnie usunięta", + "Collection used to store and index file metadata and content" : "Kolekcja używana do przechowywania i indeksowania metadanych oraz zawartości Plików", + "Collection used to store and index object data" : "Kolekcja używana do przechowywania i indeksowania danych Obiektów", + "Collections:" : "Kolekcje:", + "Commit Message" : "Komunikat zatwierdzenia", + "Commit Within (ms)" : "Zatwierdź w ciągu (ms)", + "Commit message" : "Komunikat zatwierdzenia", + "Compare current schema with real object data" : "Porównaj bieżący Schemat z rzeczywistymi danymi Obiektów", + "Completed" : "Zakończone", + "Complex" : "Złożone", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Zapytania złożone: zaawansowane wyszukiwania z wieloma filtrami, operatorami i złożonymi kombinacjami parametrów", + "Confidence Level" : "Poziom pewności", + "Confidence Score" : "Wynik pewności", + "Config must be provided as an object" : "Konfiguracja musi być podana jako obiekt", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Zarządzanie ConfigSet", + "ConfigSet Name" : "Nazwa ConfigSet", + "ConfigSet created successfully" : "ConfigSet został pomyślnie utworzony", + "ConfigSet deleted successfully" : "ConfigSet został pomyślnie usunięty", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets definiują Schemat i konfigurację kolekcji SOLR. Zawierają definicje pól, analizatory i inne ustawienia wyszukiwania.", + "Configuration" : "Konfiguracja", + "Configuration Data" : "Dane konfiguracji", + "Configuration saved" : "Konfiguracja zapisana", + "Configurations" : "Konfiguracje", + "Configure Facets" : "Skonfiguruj fasety", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Skonfiguruj dostawców dużych modeli językowych (LLM) dla funkcji opartych na AI, w tym wyszukiwania semantycznego, osadzeń i czatu.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Skonfiguruj podstawowe ustawienia połączenia dla serwera SOLR, w tym uwierzytelnianie i opcje sieciowe. Użyj osobnych okien zarządzania ConfigSet i kolekcjami, aby zarządzać rdzeniami i kolekcjami.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Skonfiguruj, w jaki sposób Obiekty bazy danych są konwertowane na osadzenia wektorowe dla wyszukiwania semantycznego. Obiekty są wektoryzowane bezpośrednio, bez potrzeby ekstrakcji tekstu.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Skonfiguruj, w jaki sposób Obiekty są konwertowane na tekst przed wektoryzacją. Te ustawienia wpływają na jakość i kontekst wyszukiwania.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Skonfiguruj parametry wektoryzacji Plików. Ten proces wygeneruje osadzenia wektorowe dla wszystkich wyodrębnionych fragmentów Plików.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Skonfiguruj parametry wektoryzacji Obiektów. Ten proces wygeneruje osadzenia wektorowe dla wszystkich Obiektów pasujących do filtrów widoku.", + "Configure which types of data to search and how many sources to retrieve" : "Skonfiguruj, które typy danych przeszukiwać i ile źródeł pobierać", + "Connection Failed" : "Połączenie nieudane", + "Connection Settings" : "Ustawienia połączenia", + "Connection Successful!" : "Połączenie udane!", + "Connection failed" : "Połączenie nieudane", + "Connection protocol" : "Protokół połączenia", + "Connection successful" : "Połączenie udane", + "Connection timeout in seconds" : "Limit czasu połączenia w sekundach", + "Constant delay between retries (5 minutes)" : "Stałe opóźnienie między próbami (5 minut)", + "Constraint Issue" : "Problem z ograniczeniem", + "Content Filters" : "Filtry zawartości", + "Continue to Properties" : "Przejdź do Właściwości", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontroluj, które widoki Obiektów mają być wektoryzowane, aby zmniejszyć koszty API. Wektoryzuj tylko widoki, które korzystają z wyszukiwania semantycznego.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontroluj, które widoki i narzędzia AI może wykorzystać w tej rozmowie. Domyślnie wszystkie możliwości agenta są włączone.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Kontroluje losowość (0 = deterministyczny, 2 = bardzo kreatywny)", + "Conversation ID is required" : "Identyfikator rozmowy jest wymagany", + "Conversation archived" : "Rozmowa zarchiwizowana", + "Conversation archived successfully" : "Rozmowa została pomyślnie zarchiwizowana", + "Conversation cleared successfully" : "Rozmowa została pomyślnie wyczyszczona", + "Conversation deleted" : "Rozmowa usunięta", + "Conversation permanently deleted" : "Rozmowa trwale usunięta", + "Conversation renamed" : "Rozmowa przemianowana", + "Conversation restored" : "Rozmowa przywrócona", + "Conversation title" : "Tytuł rozmowy", + "Copied!" : "Skopiowano!", + "Copy" : "Kopiuj", + "Copy Changes" : "Kopiuj zmiany", + "Copy Collection" : "Kopiuj kolekcję", + "Copy Data" : "Kopiuj dane", + "Copy Full Data" : "Kopiuj pełne dane", + "Copying..." : "Kopiowanie...", + "Count" : "Liczba", + "Counting objects..." : "Liczenie Obiektów...", + "Costs" : "Koszty", + "Create" : "Utwórz", + "Create Collection" : "Utwórz kolekcję", + "Create ConfigSet" : "Utwórz ConfigSet", + "Create New Collection" : "Utwórz nową kolekcję", + "Create New ConfigSet" : "Utwórz nowy ConfigSet", + "Create Operations" : "Operacje tworzenia", + "Create Webhook" : "Utwórz webhook", + "Create a copy of collection:" : "Utwórz kopię kolekcji:", + "Create a new ConfigSet based on the _default template" : "Utwórz nowy ConfigSet na podstawie szablonu _default", + "Create a new SOLR collection from an existing ConfigSet" : "Utwórz nową kolekcję SOLR z istniejącego ConfigSet", + "Create your first AI agent to get started." : "Utwórz swojego pierwszego agenta AI, aby rozpocząć.", + "Created" : "Utworzono", + "Created:" : "Utworzono:", + "Creating..." : "Tworzenie...", + "Current Filters" : "Bieżące filtry", + "Current Type:" : "Bieżący typ:", + "Custom API endpoint if using a different region" : "Niestandardowy punkt końcowy API w przypadku korzystania z innego regionu", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Niestandardowe nagłówki HTTP (jeden na linię, format: Nazwa-Nagłówka: wartość)", + "Daily" : "Codziennie", + "Dashboard" : "Pulpit", + "Data Source" : "Źródło danych", + "Data sources" : "Źródła danych", + "Data type does not match observed values" : "Typ danych nie pasuje do zaobserwowanych wartości", + "Database URL" : "Adres URL bazy danych", + "Database information refreshed" : "Informacje o bazie danych odświeżone", + "Date Range" : "Zakres dat", + "Default" : "Domyślne", + "Default Organisation" : "Domyślna organizacja", + "Default: 5" : "Domyślnie: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Opóźnienia podwajają się z każdą próbą (2, 4, 8 minut...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Opóźnienia rosną liniowo (5, 10, 15 minut...)", + "Delete" : "Usuń", + "Delete Application" : "Usuń aplikację", + "Delete Audit Trail" : "Usuń ścieżkę audytu", + "Delete Collection" : "Usuń kolekcję", + "Delete ConfigSet" : "Usuń ConfigSet", + "Delete Objects" : "Usuń Obiekty", + "Delete Operations" : "Operacje usuwania", + "Delete View" : "Usuń widok", + "Delete all objects in this schema" : "Usuń wszystkie Obiekty w tym Schemacie", + "Delete permanently" : "Usuń trwale", + "Delete this conversation?" : "Usunąć tę rozmowę?", + "Delete view" : "Usuń widok", + "Deleted" : "Usunięto", + "Deleted By" : "Usunięte przez", + "Deleted Date" : "Data usunięcia", + "Deleted Items Management" : "Zarządzanie usuniętymi elementami", + "Deleted This Week" : "Usunięto w tym tygodniu", + "Deleted Today" : "Usunięto dzisiaj", + "Deleted:" : "Usunięto:", + "Deleting..." : "Usuwanie...", + "Deletion Date Range" : "Zakres dat usunięcia", + "Deletion Statistics" : "Statystyki usuwania", + "Deprecated" : "Przestarzałe", + "Description" : "Opis", + "Description:" : "Opis:", + "Detect data types and patterns" : "Wykryj typy danych i wzorce", + "Detected At" : "Wykryto", + "Detected Format:" : "Wykryty format:", + "Detected Issues:" : "Wykryte problemy:", + "Disable" : "Wyłącz", + "Disabled" : "Wyłączone", + "Discover Files" : "Odkryj Pliki", + "Discovered Properties" : "Odkryte Właściwości", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Czy chcesz trwale usunąć wszystkie filtrowane wpisy ścieżki audytu? Tej operacji nie można cofnąć.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Czy chcesz trwale usunąć ten wpis ścieżki audytu? Tej operacji nie można cofnąć.", + "Documents" : "Dokumenty", + "Download API Spec" : "Pobierz specyfikację API", + "Edit" : "Edytuj", + "Edit Register" : "Edytuj Rejestr", + "Edit Schema" : "Edytuj Schemat", + "Edit View" : "Edytuj widok", + "Edit Webhook" : "Edytuj webhook", + "Edit view" : "Edytuj widok", + "Edit view details" : "Edytuj szczegóły widoku", + "Effectiveness" : "Skuteczność", + "Email" : "E-mail", + "Emails" : "E-maile", + "Embedding Model" : "Model osadzania", + "Embedding Provider" : "Dostawca osadzania", + "Embedding generated successfully" : "Osadzenie zostało pomyślnie wygenerowane", + "Embedding provider connection successful!" : "Połączenie z dostawcą osadzania udane!", + "Enable" : "Włącz", + "Enable auto-creation" : "Włącz automatyczne tworzenie", + "Enable automatic file vectorization" : "Włącz automatyczną wektoryzację Plików", + "Enable automatic object vectorization" : "Włącz automatyczną wektoryzację Obiektów", + "Enable automatic synchronization" : "Włącz automatyczną synchronizację", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Włącz szczegółowe rejestrowanie operacji SOLR (zalecane do debugowania)", + "Enable faceting" : "Włącz fasetowanie", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Włącz lub wyłącz funkcje LLM. Skonfiguruj dostawców i modele za pomocą przycisku Konfiguracja LLM powyżej.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Włącz lub wyłącz integrację wyszukiwania SOLR. Skonfiguruj ustawienia połączenia za pomocą przycisku Ustawienia połączenia powyżej.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Włącz lub wyłącz integrację przepływów pracy n8n. Skonfiguruj ustawienia połączenia poniżej.", + "Enable or disable this webhook" : "Włącz lub wyłącz ten webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Włącz wektoryzację dla wszystkich istniejących i przyszłych widoków (może zwiększyć koszty)", + "Enabled" : "Włączone", + "Endpoints" : "Punkty końcowe", + "Enter ConfigSet name" : "Wprowadź nazwę ConfigSet", + "Enter collection name" : "Wprowadź nazwę kolekcji", + "Enter description (optional)..." : "Wprowadź opis (opcjonalnie)...", + "Enter new collection name" : "Wprowadź nową nazwę kolekcji", + "Enter object ID" : "Wprowadź identyfikator Obiektu", + "Enter search term" : "Wprowadź termin wyszukiwania", + "Enter view name..." : "Wprowadź nazwę widoku...", + "Enter webhook name" : "Wprowadź nazwę webhooka", + "Entities" : "Encje", + "Entity Information" : "Informacje o encji", + "Entity deleted successfully" : "Encja została pomyślnie usunięta", + "Entity not found" : "Nie znaleziono encji", + "Entries to be deleted:" : "Wpisy do usunięcia:", + "Enum Issue" : "Problem z wartościami enum", + "Enum constraint is missing" : "Brak ograniczenia enum", + "Error" : "Błąd", + "Error Details" : "Szczegóły błędu", + "Error Information" : "Informacje o błędzie", + "Error Message" : "Komunikat błędu", + "Error loading application" : "Błąd podczas ładowania aplikacji", + "Error loading audit trails" : "Błąd podczas ładowania ścieżek audytu", + "Error loading entity" : "Błąd podczas ładowania encji", + "Error loading search trails" : "Błąd podczas ładowania ścieżek wyszukiwania", + "Estimated Batches:" : "Szacowane partie:", + "Estimated Cost:" : "Szacowany koszt:", + "Estimated Duration:" : "Szacowany czas trwania:", + "Event" : "Wydarzenie", + "Event Property for Payload" : "Właściwość wydarzenia dla ładunku", + "Events" : "Wydarzenia", + "Example Value" : "Przykładowa wartość", + "Exclusive Maximum" : "Maksimum wyłączne", + "Exclusive Minimum" : "Minimum wyłączne", + "Execution Mode" : "Tryb wykonania", + "Execution Time" : "Czas wykonania", + "Execution Time Range" : "Zakres czasu wykonania", + "Existing Improvements" : "Istniejące ulepszenia", + "Exponential" : "Wykładnicze", + "Export" : "Eksportuj", + "Export completed successfully" : "Eksport zakończony pomyślnie", + "Export, view, or delete audit trails" : "Eksportuj, przeglądaj lub usuwaj ścieżki audytu", + "Extend Schema" : "Rozszerz Schemat", + "Extended" : "Rozszerzone", + "Extensions" : "Rozszerzenia", + "Extract Pending Files" : "Wyodrębnij oczekujące Pliki", + "Extract properties from each object" : "Wyodrębnij Właściwości z każdego Obiektu", + "Extracted At" : "Wyodrębniono", + "Extraction Status" : "Status ekstrakcji", + "Facet configuration updated successfully" : "Konfiguracja faset została pomyślnie zaktualizowana", + "Facets discovered and configured successfully" : "Fasety zostały pomyślnie odkryte i skonfigurowane", + "Facets discovered successfully" : "Fasety zostały pomyślnie odkryte", + "Failed" : "Nieudane", + "Failed to analyze schema properties" : "Nie udało się przeanalizować Właściwości Schematu", + "Failed to archive conversation" : "Nie udało się zarchiwizować rozmowy", + "Failed to calculate sizes" : "Nie udało się obliczyć rozmiarów", + "Failed to clear collection" : "Nie udało się wyczyścić kolekcji", + "Failed to copy changes" : "Nie udało się skopiować zmian", + "Failed to copy data" : "Nie udało się skopiować danych", + "Failed to copy data to clipboard" : "Nie udało się skopiować danych do schowka", + "Failed to create conversation" : "Nie udało się utworzyć rozmowy", + "Failed to create or find project" : "Nie udało się utworzyć lub znaleźć projektu", + "Failed to delete collection" : "Nie udało się usunąć kolekcji", + "Failed to delete conversation" : "Nie udało się usunąć rozmowy", + "Failed to delete webhook" : "Nie udało się usunąć webhooka", + "Failed to download API specification" : "Nie udało się pobrać specyfikacji API", + "Failed to get SOLR field configuration" : "Nie udało się pobrać konfiguracji pól SOLR", + "Failed to get entity categories" : "Nie udało się pobrać kategorii encji", + "Failed to get entity statistics" : "Nie udało się pobrać statystyk encji", + "Failed to get entity types" : "Nie udało się pobrać typów encji", + "Failed to load LLM configuration" : "Nie udało się załadować konfiguracji LLM", + "Failed to load Nextcloud groups" : "Nie udało się załadować grup Nextcloud", + "Failed to load conversation" : "Nie udało się załadować rozmowy", + "Failed to load entities" : "Nie udało się załadować encji", + "Failed to load entity" : "Nie udało się załadować encji", + "Failed to load files" : "Nie udało się załadować Plików", + "Failed to load organisations" : "Nie udało się załadować organizacji", + "Failed to load templates" : "Nie udało się załadować szablonów", + "Failed to load webhooks" : "Nie udało się załadować webhooków", + "Failed to load workflows" : "Nie udało się załadować przepływów pracy", + "Failed to refresh database information" : "Nie udało się odświeżyć informacji o bazie danych", + "Failed to reindex collection" : "Nie udało się ponownie zindeksować kolekcji", + "Failed to rename conversation" : "Nie udało się przemianować rozmowy", + "Failed to restore conversation" : "Nie udało się przywrócić rozmowy", + "Failed to retry extraction" : "Nie udało się ponowić ekstrakcji", + "Failed to retry webhook" : "Nie udało się ponowić webhooka", + "Failed to save GitHub token" : "Nie udało się zapisać tokenu GitHub", + "Failed to save GitLab URL" : "Nie udało się zapisać adresu URL GitLab", + "Failed to save GitLab token" : "Nie udało się zapisać tokenu GitLab", + "Failed to save additional feedback" : "Nie udało się zapisać dodatkowej opinii", + "Failed to save n8n configuration" : "Nie udało się zapisać konfiguracji n8n", + "Failed to save roles" : "Nie udało się zapisać ról", + "Failed to save settings" : "Nie udało się zapisać ustawień", + "Failed to save webhook" : "Nie udało się zapisać webhooka", + "Failed to send feedback" : "Nie udało się wysłać opinii", + "Failed to test webhook" : "Nie udało się przetestować webhooka", + "Failed to update favorite status" : "Nie udało się zaktualizować statusu ulubionego", + "Failed to update schema properties" : "Nie udało się zaktualizować Właściwości Schematu", + "Failed to update view" : "Nie udało się zaktualizować widoku", + "Failed to update webhook" : "Nie udało się zaktualizować webhooka", + "Feedback recorded" : "Opinia zarejestrowana", + "Fewer sources (1-3):" : "Mniej źródeł (1-3):", + "Field" : "Pole", + "File" : "Plik", + "File Chunk Prediction" : "Predykcja fragmentów Pliku", + "File Collection" : "Kolekcja Plików", + "File Management" : "Zarządzanie Plikami", + "File Name" : "Nazwa Pliku", + "File Path" : "Ścieżka Pliku", + "File Type Selection" : "Wybór typu Pliku", + "File Vectorization" : "Wektoryzacja Plików", + "File Warmup" : "Rozgrzewka Plików", + "File actions menu" : "Menu akcji Pliku", + "File anonymized successfully" : "Plik został pomyślnie zanonimizowany", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Wektoryzacja fragmentów Pliku nie została jeszcze zaimplementowana. Fragmenty są gotowe i przechowywane, ale usługa wektoryzacji jest w trakcie opracowywania.", + "File collection not configured" : "Kolekcja Plików nie została skonfigurowana", + "File discovery completed" : "Odkrywanie Plików zakończone", + "File extraction completed" : "Ekstrakcja Plików zakończona", + "File extraction queued" : "Ekstrakcja Plików w kolejce", + "File indexed successfully" : "Plik został pomyślnie zindeksowany", + "File is already anonymized" : "Plik jest już zanonimizowany", + "File not found" : "Nie znaleziono Pliku", + "File settings updated successfully" : "Ustawienia Pliku zostały pomyślnie zaktualizowane", + "File sources" : "Źródła Plików", + "File vectorization configuration saved successfully" : "Konfiguracja wektoryzacji Plików została pomyślnie zapisana", + "File vectorization started. Check the statistics section for progress." : "Wektoryzacja Plików rozpoczęta. Sprawdź sekcję statystyk, aby śledzić postęp.", + "File warmup completed" : "Rozgrzewka Plików zakończona", + "Files" : "Pliki", + "Files with Completed Extraction:" : "Pliki z zakończoną ekstrakcją:", + "Files → fileCollection, Objects → objectCollection" : "Pliki → fileCollection, Obiekty → objectCollection", + "Filter Audit Trails" : "Filtruj ścieżki audytu", + "Filter Deleted Items" : "Filtruj usunięte elementy", + "Filter Objects" : "Filtruj Obiekty", + "Filter Properties" : "Filtruj Właściwości", + "Filter Search Trails" : "Filtruj ścieżki wyszukiwania", + "Filter Statistics" : "Filtruj statystyki", + "Filter and analyze search trail entries" : "Filtruj i analizuj wpisy ścieżek wyszukiwania", + "Filter and manage audit trail entries" : "Filtruj i zarządzaj wpisami ścieżek audytu", + "Filter and manage soft deleted items" : "Filtruj i zarządzaj miękko usuniętymi elementami", + "Filter by object ID" : "Filtruj według identyfikatora Obiektu", + "Filter by search term" : "Filtruj według terminu wyszukiwania", + "Filter by webhook" : "Filtruj według webhooka", + "Filter data loaded automatically. Use the filters below to refine your search." : "Dane filtra załadowane automatycznie. Użyj filtrów poniżej, aby zawęzić wyszukiwanie.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtruj wyzwalacze webhooków według właściwości ładunku (jeden na linię, format: klucz: wartość)", + "Filtered" : "Filtrowane", + "Filters" : "Filtry", + "Fireworks AI Chat Settings" : "Ustawienia czatu Fireworks AI", + "Fireworks AI Embedding Configuration" : "Konfiguracja osadzania Fireworks AI", + "First" : "Pierwsza", + "Fixed" : "Naprawione", + "For chat and retrieval-augmented generation" : "Do czatu i generowania wspomaganego wyszukiwaniem", + "For vector embeddings and semantic search" : "Do osadzeń wektorowych i wyszukiwania semantycznego", + "Format" : "Format", + "Format Issue" : "Problem z formatem", + "Format constraint is missing" : "Brak ograniczenia formatu", + "From Date" : "Data od", + "From date" : "Data od", + "Full data copied to clipboard" : "Pełne dane skopiowano do schowka", + "General Issue" : "Ogólny problem", + "Generate recommendations and confidence scores" : "Generuj rekomendacje i wyniki pewności", + "Generate vectors immediately when new objects are created" : "Generuj wektory natychmiast po utworzeniu nowych Obiektów", + "GitHub token is valid" : "Token GitHub jest prawidłowy", + "GitHub token removed successfully" : "Token GitHub został pomyślnie usunięty", + "GitHub token saved successfully" : "Token GitHub został pomyślnie zapisany", + "GitLab URL saved successfully" : "Adres URL GitLab został pomyślnie zapisany", + "GitLab token is valid" : "Token GitLab jest prawidłowy", + "GitLab token saved successfully" : "Token GitLab został pomyślnie zapisany", + "HTTP Method" : "Metoda HTTP", + "HTTP method used to send webhook requests" : "Metoda HTTP używana do wysyłania żądań webhooków", + "Headers" : "Nagłówki", + "Health" : "Stan", + "Heartbeat successful - connection kept alive" : "Sygnał kontrolny udany - połączenie podtrzymane", + "Helpful" : "Pomocne", + "Hide Filters" : "Ukryj filtry", + "Hide in forms" : "Ukryj w formularzach", + "Hide in list view" : "Ukryj w widoku listy", + "High" : "Wysoki", + "High Confidence" : "Wysoka pewność", + "Host" : "Host", + "Hourly" : "Co godzinę", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Jak głęboko przeszukiwać zagnieżdżone Właściwości Obiektów (1-20). Wyższe wartości obejmują więcej szczegółów, ale zwiększają rozmiar wektora.", + "How often to check for updates (1-168 hours)" : "Jak często sprawdzać aktualizacje (1-168 godzin)", + "How to handle retries for failed webhook deliveries" : "Jak obsługiwać ponowne próby nieudanych dostaw webhooków", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Limity szybkości IP zostały pomyślnie wyczyszczone", + "Identify properties not in the schema" : "Zidentyfikuj Właściwości spoza Schematu", + "Immutable" : "Niezmienne", + "Import" : "Importuj", + "Import successful" : "Import udany", + "Improved Property" : "Ulepszona Właściwość", + "In use" : "W użyciu", + "Inactive" : "Nieaktywne", + "Include IDs and names of related objects for better contextual search" : "Uwzględnij identyfikatory i nazwy powiązanych Obiektów dla lepszego wyszukiwania kontekstowego", + "Include related object references" : "Uwzględnij odniesienia do powiązanych Obiektów", + "Include schema and register metadata" : "Uwzględnij metadane Schematu i Rejestru", + "Initialization failed" : "Inicjalizacja nieudana", + "Initialize Project" : "Zainicjuj projekt", + "Inspect Fields" : "Sprawdź pola", + "Inspect Index" : "Sprawdź indeks", + "Invalid" : "Nieprawidłowe", + "Invalid batch size. Must be between 1 and 5000" : "Nieprawidłowy rozmiar partii. Musi być między 1 a 5000", + "Invalid field name provided" : "Podano nieprawidłową nazwę pola", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Nieprawidłowa wartość maxObjects. Musi wynosić 0 (wszystkie) lub być liczbą dodatnią", + "Invalid:" : "Nieprawidłowe:", + "Issue" : "Problem", + "Items per page:" : "Elementów na stronę:", + "Just now" : "Przed chwilą", + "LLM Configuration" : "Konfiguracja LLM", + "LLM actions menu" : "Menu akcji LLM", + "LLM configuration saved successfully" : "Konfiguracja LLM została pomyślnie zapisana", + "LLM features disabled" : "Funkcje LLM wyłączone", + "LLM features enabled" : "Funkcje LLM włączone", + "LLM must be enabled with an embedding provider configured" : "LLM musi być włączony z skonfigurowanym dostawcą osadzania", + "LLM settings updated successfully" : "Ustawienia LLM zostały pomyślnie zaktualizowane", + "Last" : "Ostatnia", + "Last Triggered" : "Ostatnio wyzwolone", + "Length Range:" : "Zakres długości:", + "Linear" : "Liniowe", + "Load Advanced Filters" : "Załaduj filtry zaawansowane", + "Load advanced filters with live data from your search index" : "Załaduj filtry zaawansowane z danymi na żywo z indeksu wyszukiwania", + "Load view" : "Załaduj widok", + "Loading ConfigSets..." : "Ładowanie ConfigSets...", + "Loading advanced filters..." : "Ładowanie filtrów zaawansowanych...", + "Loading agents..." : "Ładowanie agentów...", + "Loading application details..." : "Ładowanie szczegółów aplikacji...", + "Loading applications..." : "Ładowanie aplikacji...", + "Loading archived conversations..." : "Ładowanie zarchiwizowanych rozmów...", + "Loading audit trails..." : "Ładowanie ścieżek audytu...", + "Loading collections..." : "Ładowanie kolekcji...", + "Loading configurations..." : "Ładowanie konfiguracji...", + "Loading conversation..." : "Ładowanie rozmowy...", + "Loading conversations..." : "Ładowanie rozmów...", + "Loading deleted items..." : "Ładowanie usuniętych elementów...", + "Loading events..." : "Ładowanie wydarzeń...", + "Loading filter data..." : "Ładowanie danych filtra...", + "Loading groups..." : "Ładowanie grup...", + "Loading log details..." : "Ładowanie szczegółów dziennika...", + "Loading registers..." : "Ładowanie Rejestrów...", + "Loading schemas..." : "Ładowanie Schematów...", + "Loading search trails..." : "Ładowanie ścieżek wyszukiwania...", + "Loading sources..." : "Ładowanie źródeł...", + "Loading statistics..." : "Ładowanie statystyk...", + "Loading users..." : "Ładowanie użytkowników...", + "Loading views..." : "Ładowanie widoków...", + "Loading..." : "Ładowanie...", + "Local" : "Lokalne", + "Local Version" : "Wersja lokalna", + "Locked" : "Zablokowane", + "Locked:" : "Zablokowane:", + "Login successful" : "Logowanie udane", + "Logs" : "Dzienniki", + "Low" : "Niski", + "Low Confidence" : "Niska pewność", + "Lucene Version:" : "Wersja Lucene:", + "Magic table synchronized successfully" : "Tabela magiczna została pomyślnie zsynchronizowana", + "Manage Organisation Roles" : "Zarządzaj rolami organizacji", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Zarządzaj kolekcjami SOLR (magazynami danych) i przypisuj je do Obiektów i Plików.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Zarządzaj ConfigSets SOLR (szablonami konfiguracji) dla swoich kolekcji.", + "Manage and configure agents for automated tasks" : "Zarządzaj i konfiguruj agentów do zautomatyzowanych zadań", + "Manage and monitor file text extraction status" : "Zarządzaj i monitoruj status ekstrakcji tekstu z Plików", + "Manage and restore soft deleted items from your registers" : "Zarządzaj i przywracaj miękko usunięte elementy z Rejestrów", + "Manage and view detected entities from files and objects" : "Zarządzaj i przeglądaj wykryte encje z Plików i Obiektów", + "Manage document templates and themes" : "Zarządzaj szablonami dokumentów i motywami", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Zarządzaj przepływami pracy n8n dla automatyzacji OpenRegister. Przepływy pracy będą przechowywane w skonfigurowanym projekcie.", + "Manage webhooks for event-driven integrations" : "Zarządzaj webhookami dla integracji opartych na wydarzeniach", + "Manage your applications and modules" : "Zarządzaj swoimi aplikacjami i modułami", + "Manage your data registers and their configurations" : "Zarządzaj swoimi Rejestrami danych i ich konfiguracjami", + "Manage your data schemas and their properties" : "Zarządzaj swoimi Schematami danych i ich Właściwościami", + "Manage your data sources and their configurations" : "Zarządzaj swoimi Źródłami danych i ich konfiguracjami", + "Manage your saved search configurations" : "Zarządzaj zapisanymi konfiguracjami wyszukiwania", + "Manage your system configurations and settings" : "Zarządzaj konfiguracjami i ustawieniami systemu", + "Managed" : "Zarządzane", + "Max Files (0 = all)" : "Maks. Plików (0 = wszystkie)", + "Max Length" : "Maks. długość", + "Max Length:" : "Maks. długość:", + "Max Objects (0 = all)" : "Maks. Obiektów (0 = wszystkie)", + "Max Retries" : "Maks. ponowień", + "Max Shards/Node" : "Maks. fragmentów/węzeł", + "Max execution time (ms)" : "Maks. czas wykonania (ms)", + "Max ms" : "Maks. ms", + "Max result count" : "Maks. liczba wyników", + "Max results" : "Maks. wyników", + "Maximum" : "Maksimum", + "Maximum Nesting Depth" : "Maksymalna głębokość zagnieżdżenia", + "Maximum length constraint is missing" : "Brak ograniczenia maksymalnej długości", + "Maximum length is too restrictive" : "Maksymalna długość jest zbyt restrykcyjna", + "Maximum number of files to process. Set to 0 to process all files." : "Maksymalna liczba Plików do przetworzenia. Ustaw na 0, aby przetworzyć wszystkie Pliki.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maksymalna liczba Obiektów do przetworzenia. Ustaw na 0, aby przetworzyć wszystkie Obiekty.", + "Maximum number of retry attempts for failed deliveries" : "Maksymalna liczba prób ponowienia dla nieudanych dostaw", + "Maximum time to wait before committing changes" : "Maksymalny czas oczekiwania przed zatwierdzeniem zmian", + "Maximum tokens to generate" : "Maksymalna liczba tokenów do wygenerowania", + "Maximum value constraint is missing" : "Brak ograniczenia wartości maksymalnej", + "Maximum value is too restrictive" : "Wartość maksymalna jest zbyt restrykcyjna", + "Medium" : "Średni", + "Medium Confidence" : "Średnia pewność", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Zapytania średnie: wyszukiwania z pewnym filtrowaniem lub wieloma parametrami (np. zakresy dat, konkretne Rejestry/Schematy)", + "Members of selected groups can access this view" : "Członkowie wybranych grup mają dostęp do tego widoku", + "Members:" : "Członkowie:", + "Memory prediction calculated successfully" : "Predykcja pamięci została pomyślnie obliczona", + "Message content is required" : "Treść wiadomości jest wymagana", + "Message does not belong to this conversation" : "Wiadomość nie należy do tej rozmowy", + "Metadata Filters" : "Filtry metadanych", + "Method" : "Metoda", + "Min Length" : "Min. długość", + "Min execution time (ms)" : "Min. czas wykonania (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. liczba wyników", + "Min results" : "Min. wyników", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Brak ograniczenia wartości minimalnej", + "Minimum value is too restrictive" : "Wartość minimalna jest zbyt restrykcyjna", + "Missing object fields created successfully" : "Brakujące pola Obiektów zostały pomyślnie utworzone", + "Mode:" : "Tryb:", + "Model" : "Model", + "Model Name" : "Nazwa modelu", + "Modified" : "Zmodyfikowane", + "Monthly" : "Co miesiąc", + "More sources (10-20):" : "Więcej źródeł (10-20):", + "Most Active Objects" : "Najbardziej aktywne Obiekty", + "Multiple Of" : "Wielokrotność", + "Name" : "Nazwa", + "Names cache warmup completed" : "Rozgrzewka pamięci podręcznej nazw zakończona", + "New Collection Name" : "Nazwa nowej kolekcji", + "New Conversation" : "Nowa rozmowa", + "New Properties" : "Nowe Właściwości", + "New Property" : "Nowa Właściwość", + "New Value" : "Nowa wartość", + "New users without specific organisation membership will be automatically added to this organisation" : "Nowi użytkownicy bez przynależności do konkretnej organizacji zostaną automatycznie dodani do tej organizacji", + "Next" : "Następna", + "No" : "Nie", + "No ConfigSets found" : "Nie znaleziono ConfigSets", + "No GitHub token configured" : "Nie skonfigurowano tokenu GitHub", + "No GitHub token provided" : "Nie podano tokenu GitHub", + "No GitLab token provided" : "Nie podano tokenu GitLab", + "No Organisation" : "Brak organizacji", + "No active filters" : "Brak aktywnych filtrów", + "No activity data available" : "Brak dostępnych danych o aktywności", + "No agents available" : "Brak dostępnych agentów", + "No agents found" : "Nie znaleziono agentów", + "No app store cache exists yet" : "Pamięć podręczna sklepu z aplikacjami jeszcze nie istnieje", + "No applications are available." : "Brak dostępnych aplikacji.", + "No applications found" : "Nie znaleziono aplikacji", + "No archived conversations" : "Brak zarchiwizowanych rozmów", + "No audit trail entries found" : "Nie znaleziono wpisów ścieżki audytu", + "No changes recorded" : "Nie zarejestrowano żadnych zmian", + "No chunks to vectorize" : "Brak fragmentów do wektoryzacji", + "No collections found" : "Nie znaleziono kolekcji", + "No configuration data" : "Brak danych konfiguracji", + "No configurations are available." : "Brak dostępnych konfiguracji.", + "No configurations found" : "Nie znaleziono konfiguracji", + "No configurations found for this application." : "Nie znaleziono konfiguracji dla tej aplikacji.", + "No conversations yet. Create a new one to get started!" : "Brak rozmów. Utwórz nową, aby rozpocząć!", + "No data available" : "Brak dostępnych danych", + "No data available for chart" : "Brak dostępnych danych do wykresu", + "No deleted items found" : "Nie znaleziono usuniętych elementów", + "No deletion data available" : "Brak dostępnych danych o usuwaniu", + "No description found" : "Nie znaleziono opisu", + "No description provided" : "Nie podano opisu", + "No entities detected in this file. Run text extraction first." : "Nie wykryto encji w tym Pliku. Najpierw uruchom ekstrakcję tekstu.", + "No entities found" : "Nie znaleziono encji", + "No entities have been detected yet" : "Nie wykryto jeszcze żadnych encji", + "No events found" : "Nie znaleziono wydarzeń", + "No expired audit trails found to clear" : "Nie znaleziono wygasłych ścieżek audytu do wyczyszczenia", + "No facetable fields available. Select a register and schema to see available filters." : "Brak dostępnych pól do fasetowania. Wybierz Rejestr i Schemat, aby zobaczyć dostępne filtry.", + "No files found" : "Nie znaleziono Plików", + "No files have been extracted yet" : "Nie wyodrębniono jeszcze żadnych Plików", + "No files to process" : "Brak Plików do przetworzenia", + "No files to reindex" : "Brak Plików do ponownego zindeksowania", + "No filters are currently active. This will delete ALL audit trail entries!" : "Obecnie nie są aktywne żadne filtry. Spowoduje to usunięcie WSZYSTKICH wpisów ścieżki audytu!", + "No log entries found" : "Nie znaleziono wpisów dziennika", + "No logs are available for this configuration." : "Brak dostępnych dzienników dla tej konfiguracji.", + "No logs are available for this source." : "Brak dostępnych dzienników dla tego Źródła.", + "No logs found" : "Nie znaleziono dzienników", + "No mismatched fields found - SOLR schema is properly configured" : "Nie znaleziono niezgodnych pól - Schemat SOLR jest poprawnie skonfigurowany", + "No objects found" : "Nie znaleziono Obiektów", + "No objects found to vectorize" : "Nie znaleziono Obiektów do wektoryzacji", + "No objects selected for deletion" : "Nie wybrano Obiektów do usunięcia", + "No objects to delete" : "Brak Obiektów do usunięcia", + "No objects were permanently deleted" : "Żaden Obiekt nie został trwale usunięty", + "No properties found" : "Nie znaleziono Właściwości", + "No properties match your filters." : "Żadna Właściwość nie pasuje do filtrów.", + "No purge date set" : "Nie ustawiono daty czyszczenia", + "No register data available" : "Brak dostępnych danych Rejestru", + "No registers found" : "Nie znaleziono Rejestrów", + "No registers found for this application." : "Nie znaleziono Rejestrów dla tej aplikacji.", + "No relations found" : "Nie znaleziono relacji", + "No request body available" : "Brak dostępnej treści żądania", + "No response body available" : "Brak dostępnej treści odpowiedzi", + "No saved views yet. Create one in the Search tab!" : "Brak zapisanych widoków. Utwórz jeden w zakładce Wyszukiwanie!", + "No schema data available" : "Brak dostępnych danych Schematu", + "No schema selected for exploration" : "Nie wybrano Schematu do eksploracji", + "No schemas found" : "Nie znaleziono Schematów", + "No schemas found for this application." : "Nie znaleziono Schematów dla tej aplikacji.", + "No search terms data available" : "Brak dostępnych danych o terminach wyszukiwania", + "No search trail entries found" : "Nie znaleziono wpisów ścieżki wyszukiwania", + "No sources are available." : "Brak dostępnych Źródeł.", + "No sources found" : "Nie znaleziono Źródeł", + "No templates found" : "Nie znaleziono szablonów", + "No templates have been created yet" : "Nie utworzono jeszcze żadnych szablonów", + "No valid magic-mapped register+schema combinations found" : "Nie znaleziono prawidłowych kombinacji Rejestr+Schemat mapowanych magicznie", + "No views found. Create views first before configuring vectorization." : "Nie znaleziono widoków. Najpierw utwórz widoki przed skonfigurowaniem wektoryzacji.", + "No views match your search" : "Żaden widok nie pasuje do wyszukiwania", + "No webhooks found" : "Nie znaleziono webhooków", + "No webhooks have been configured yet" : "Nie skonfigurowano jeszcze żadnych webhooków", + "No workflows found in this project. Create workflows in the n8n editor." : "Nie znaleziono przepływów pracy w tym projekcie. Utwórz przepływy pracy w edytorze n8n.", + "None" : "Brak", + "Not helpful" : "Niepomocne", + "Not in use" : "Nieużywane", + "Number Constraints" : "Ograniczenia liczbowe", + "Number of chunks to vectorize in one API call (1-100)." : "Liczba fragmentów do wektoryzacji w jednym wywołaniu API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Liczba fragmentów do wektoryzacji w jednym wywołaniu API. Więcej = szybciej, ale więcej pamięci. Zalecane: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Liczba Obiektów do przetworzenia w każdej partii (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Liczba Obiektów do wektoryzacji w jednym wywołaniu API. Więcej = szybciej, ale więcej pamięci. Zalecane: 10-50.", + "Numeric Range:" : "Zakres liczbowy:", + "Object" : "Obiekt", + "Object Collection" : "Kolekcja Obiektów", + "Object Count Prediction" : "Predykcja liczby Obiektów", + "Object ID" : "Identyfikator Obiektu", + "Object Management" : "Zarządzanie Obiektami", + "Object Vectorization" : "Wektoryzacja Obiektów", + "Object Warmup" : "Rozgrzewka Obiektów", + "Object collection not configured" : "Kolekcja Obiektów nie została skonfigurowana", + "Object not found" : "Nie znaleziono Obiektu", + "Object permanently deleted" : "Obiekt trwale usunięty", + "Object restored successfully" : "Obiekt został pomyślnie przywrócony", + "Object settings updated successfully" : "Ustawienia Obiektu zostały pomyślnie zaktualizowane", + "Object sources" : "Źródła Obiektów", + "Object unlocked successfully" : "Obiekt został pomyślnie odblokowany", + "Object vectorization configuration saved successfully" : "Konfiguracja wektoryzacji Obiektów została pomyślnie zapisana", + "Object vectorized successfully" : "Obiekt został pomyślnie zwektoryzowany", + "Object:" : "Obiekt:", + "Objects" : "Obiekty", + "Objects Analyzed" : "Przeanalizowane Obiekty", + "Objects Distribution" : "Rozkład Obiektów", + "Objects being analyzed" : "Analizowane Obiekty", + "Objects by Register" : "Obiekty według Rejestru", + "Objects by Schema" : "Obiekty według Schematu", + "Objects deletion completed successfully" : "Usuwanie Obiektów zakończone pomyślnie", + "Objects in schema" : "Obiekty w Schemacie", + "Objects to Process:" : "Obiekty do przetworzenia:", + "Objects to be analyzed" : "Obiekty do przeanalizowania", + "Objects to be deleted" : "Obiekty do usunięcia", + "Objects to be validated" : "Obiekty do zwalidowania", + "Objects will be serialized as JSON text before vectorization" : "Obiekty zostaną zserializowane jako tekst JSON przed wektoryzacją", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Obiekty zostaną miękko usunięte (oznaczone jako usunięte, ale zachowane w bazie danych). W razie potrzeby można je później odzyskać.", + "Old Value" : "Stara wartość", + "Oldest Item (days)" : "Najstarszy element (dni)", + "Ollama Chat Settings" : "Ustawienia czatu Ollama", + "Ollama Embedding Configuration" : "Konfiguracja osadzania Ollama", + "Ollama URL" : "Adres URL Ollama", + "Only With Changes" : "Tylko ze zmianami", + "Open Register Objects" : "Obiekty Open Register", + "Open n8n Editor" : "Otwórz edytor n8n", + "OpenAI Chat Settings" : "Ustawienia czatu OpenAI", + "OpenAI Embedding Configuration" : "Konfiguracja osadzania OpenAI", + "OpenRegister Settings" : "Ustawienia OpenRegister", + "Optional webhook secret for signature verification" : "Opcjonalny sekret webhooka do weryfikacji podpisu", + "Order" : "Kolejność", + "Organisation" : "Organizacja", + "Organisation ID" : "Identyfikator organizacji", + "Organisation Members" : "Członkowie organizacji", + "Organisation Statistics" : "Statystyki organizacji", + "Organisation created successfully" : "Organizacja została pomyślnie utworzona", + "Organisation settings saved successfully" : "Ustawienia organizacji zostały pomyślnie zapisane", + "Organisations" : "Organizacje", + "Organization" : "Organizacja", + "Organization ID (Optional)" : "Identyfikator organizacji (opcjonalnie)", + "Orphaned Items" : "Osierocone elementy", + "Owner" : "Właściciel", + "Owner:" : "Właściciel:", + "Parallel Mode (Faster, more resource intensive)" : "Tryb równoległy (szybszy, bardziej zasobożerny)", + "Parallel:" : "Równolegle:", + "Password" : "Hasło", + "Password for SOLR authentication (optional)" : "Hasło do uwierzytelniania SOLR (opcjonalnie)", + "Path" : "Ścieżka", + "Path in repository" : "Ścieżka w repozytorium", + "Path where the configuration file will be saved in the repository" : "Ścieżka, w której Plik konfiguracji zostanie zapisany w repozytorium", + "Path where the register OAS file will be saved in the repository" : "Ścieżka, w której Plik OAS Rejestru zostanie zapisany w repozytorium", + "Pattern (regex)" : "Wzorzec (regex)", + "Pattern Issue" : "Problem ze wzorcem", + "Pattern constraint is missing" : "Brak ograniczenia wzorca", + "Patterns:" : "Wzorce:", + "Pending" : "Oczekujące", + "Permanent Delete Mode" : "Tryb trwałego usuwania", + "Permanently Delete" : "Usuń trwale", + "Permanently delete already soft-deleted objects" : "Trwale usuń już miękko usunięte Obiekty", + "Permanently delete this conversation?" : "Trwale usunąć tę rozmowę?", + "Person" : "Osoba", + "Personal Data" : "Dane osobowe", + "Phone" : "Telefon", + "Photos" : "Zdjęcia", + "Please create an agent in the" : "Utwórz agenta w", + "Please select an agent to continue" : "Wybierz agenta, aby kontynuować", + "Please select which register and schema to use for the new object" : "Wybierz, który Rejestr i Schemat ma zostać użyty dla nowego Obiektu", + "Please try again later." : "Spróbuj ponownie później.", + "Please wait while we fetch your agents." : "Poczekaj, aż pobierzemy Twoich agentów.", + "Please wait while we fetch your applications." : "Poczekaj, aż pobierzemy Twoje aplikacje.", + "Please wait while we fetch your configurations." : "Poczekaj, aż pobierzemy Twoje konfiguracje.", + "Please wait while we fetch your deleted items." : "Poczekaj, aż pobierzemy Twoje usunięte elementy.", + "Please wait while we fetch your sources." : "Poczekaj, aż pobierzemy Twoje Źródła.", + "Polls" : "Ankiety", + "Popular Search Terms" : "Popularne terminy wyszukiwania", + "Port" : "Port", + "Prerequisites" : "Wymagania wstępne", + "Press Enter to send, Shift+Enter for new line" : "Naciśnij Enter, aby wysłać, Shift+Enter, aby przejść do nowej linii", + "Previous" : "Poprzednia", + "Private" : "Prywatne", + "Processes chunks in batches with simulated parallelism." : "Przetwarza fragmenty partiami z symulowaną równoległością.", + "Processes file chunks sequentially (safest)." : "Przetwarza fragmenty Plików sekwencyjnie (najbezpieczniej).", + "Processes objects in chunks with simulated parallelism." : "Przetwarza Obiekty fragmentami z symulowaną równoległością.", + "Processes objects sequentially (safest)." : "Przetwarza Obiekty sekwencyjnie (najbezpieczniej).", + "Processing" : "Przetwarzanie", + "Processing Limits" : "Limity przetwarzania", + "Processing..." : "Przetwarzanie...", + "Project initialized successfully" : "Projekt został pomyślnie zainicjowany", + "Project not found. Please initialize first." : "Nie znaleziono projektu. Najpierw zainicjuj.", + "Properties" : "Właściwości", + "Property Behaviors" : "Zachowania Właściwości", + "Property Configuration:" : "Konfiguracja Właściwości:", + "Property Filters" : "Filtry Właściwości", + "Property Title" : "Tytuł Właściwości", + "Property Type" : "Typ Właściwości", + "Property can be improved" : "Właściwość można ulepszyć", + "Protected" : "Chronione", + "Provider" : "Dostawca", + "Provider is required for testing" : "Dostawca jest wymagany do testowania", + "Public" : "Publiczne", + "Public View" : "Widok publiczny", + "Public views can be accessed by anyone in the system" : "Widoki publiczne są dostępne dla każdego w systemie", + "Publish" : "Opublikuj", + "Publish Objects" : "Opublikuj Obiekty", + "Publish all objects in this schema" : "Opublikuj wszystkie Obiekty w tym Schemacie", + "Published" : "Opublikowane", + "Published:" : "Opublikowano:", + "Publishing..." : "Publikowanie...", + "Purge" : "Wyczyść", + "Purge Date" : "Data czyszczenia", + "Query Complexity Distribution" : "Rozkład złożoności zapytań", + "Query parameter is required" : "Parametr zapytania jest wymagany", + "RAG" : "RAG", + "RAG Configuration" : "Konfiguracja RAG", + "Rate limits cleared successfully" : "Limity szybkości zostały pomyślnie wyczyszczone", + "Raw Changes Data" : "Surowe dane zmian", + "Re-vectorize on object update" : "Ponowna wektoryzacja przy aktualizacji Obiektu", + "Read" : "Odczyt", + "Recommendations:" : "Rekomendacje:", + "Recommended Type:" : "Zalecany typ:", + "Recommended: 5 sources" : "Zalecane: 5 źródeł", + "Refresh" : "Odśwież", + "Refresh Data" : "Odśwież dane", + "Refresh Stats" : "Odśwież statystyki", + "Refresh Workflows" : "Odśwież przepływy pracy", + "Refresh dashboard" : "Odśwież pulpit", + "Refresh database info" : "Odśwież informacje o bazie danych", + "Register" : "Rejestr", + "Register ID" : "Identyfikator Rejestru", + "Register Statistics" : "Statystyki Rejestru", + "Register Totals" : "Sumy Rejestru", + "Register depublished successfully" : "Rejestr został pomyślnie wycofany z publikacji", + "Register not found" : "Nie znaleziono Rejestru", + "Register objects deletion completed successfully" : "Usuwanie Obiektów Rejestru zakończone pomyślnie", + "Register published successfully" : "Rejestr został pomyślnie opublikowany", + "Register/Schema Usage" : "Wykorzystanie Rejestru/Schematu", + "Registers" : "Rejestry", + "Reindex" : "Ponownie zindeksuj", + "Reindex completed" : "Ponowne indeksowanie zakończone", + "Reindex completed successfully" : "Ponowne indeksowanie zakończone pomyślnie", + "Reindex failed" : "Ponowne indeksowanie nieudane", + "Relations" : "Relacje", + "Remote Version" : "Wersja zdalna", + "Remove" : "Usuń", + "Remove filter" : "Usuń filtr", + "Remove from favorites" : "Usuń z ulubionych", + "Remove group" : "Usuń grupę", + "Removed" : "Usunięto", + "Removed from favorites" : "Usunięto z ulubionych", + "Rename Conversation" : "Przemianuj rozmowę", + "Rename conversation" : "Przemianuj rozmowę", + "Replicas" : "Repliki", + "Repository" : "Repozytorium", + "Request Body" : "Treść żądania", + "Request Data" : "Dane żądania", + "Request Details" : "Szczegóły żądania", + "Request body must be JSON with a \"query\" field" : "Treść żądania musi być w formacie JSON z polem \"query\"", + "Request timeout in seconds" : "Limit czasu żądania w sekundach", + "Required field" : "Pole wymagane", + "Required status is inconsistent" : "Status wymagalności jest niespójny", + "Rerun Search" : "Uruchom ponownie wyszukiwanie", + "Reset Changes" : "Zresetuj zmiany", + "Reset Filters" : "Zresetuj filtry", + "Response Body" : "Treść odpowiedzi", + "Response Details" : "Szczegóły odpowiedzi", + "Response Time:" : "Czas odpowiedzi:", + "Restore" : "Przywróć", + "Restore conversation" : "Przywróć rozmowę", + "Restore or permanently delete items" : "Przywróć lub trwale usuń elementy", + "Result Count Range" : "Zakres liczby wyników", + "Results" : "Wyniki", + "Retrieve all objects for this schema" : "Pobierz wszystkie Obiekty dla tego Schematu", + "Retry" : "Ponów", + "Retry Failed Extractions" : "Ponów nieudane ekstrakcje", + "Retry Policy" : "Zasady ponawiania", + "Retry completed" : "Ponawianie zakończone", + "Retrying..." : "Ponawianie...", + "Risk Level" : "Poziom ryzyka", + "Roles updated successfully" : "Role zostały pomyślnie zaktualizowane", + "SOLR Connection Settings" : "Ustawienia połączenia SOLR", + "SOLR Version:" : "Wersja SOLR:", + "SOLR actions menu" : "Menu akcji SOLR", + "SOLR base path (usually /solr)" : "Ścieżka bazowa SOLR (zwykle /solr)", + "SOLR is disabled" : "SOLR jest wyłączony", + "SOLR is not available or not configured" : "SOLR jest niedostępny lub nie został skonfigurowany", + "SOLR logging disabled" : "Rejestrowanie SOLR wyłączone", + "SOLR logging enabled" : "Rejestrowanie SOLR włączone", + "SOLR search disabled" : "Wyszukiwanie SOLR wyłączone", + "SOLR search enabled" : "Wyszukiwanie SOLR włączone", + "SOLR server hostname or IP address" : "Nazwa hosta lub adres IP serwera SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Numer portu serwera SOLR (opcjonalnie, domyślnie 8983)", + "SOLR setup completed successfully" : "Konfiguracja SOLR zakończona pomyślnie", + "SOLR setup failed" : "Konfiguracja SOLR nieudana", + "SOLR setup failed - check logs" : "Konfiguracja SOLR nieudana - sprawdź dzienniki", + "Sample Values:" : "Przykładowe wartości:", + "Save" : "Zapisz", + "Save Configuration" : "Zapisz konfigurację", + "Save Connection Settings" : "Zapisz ustawienia połączenia", + "Save Roles" : "Zapisz role", + "Save Settings" : "Zapisz ustawienia", + "Save current search as view" : "Zapisz bieżące wyszukiwanie jako widok", + "Saved Views" : "Zapisane widoki", + "Saving..." : "Zapisywanie...", + "Schema" : "Schemat", + "Schema ID" : "Identyfikator Schematu", + "Schema Name" : "Nazwa Schematu", + "Schema Statistics" : "Statystyki Schematu", + "Schema depublished successfully" : "Schemat został pomyślnie wycofany z publikacji", + "Schema objects deletion completed successfully" : "Usuwanie Obiektów Schematu zakończone pomyślnie", + "Schema published successfully" : "Schemat został pomyślnie opublikowany", + "Schema successfully created" : "Schemat został pomyślnie utworzony", + "Schema successfully updated" : "Schemat został pomyślnie zaktualizowany", + "Schemas" : "Schematy", + "Scheme" : "Schemat", + "Search / Views" : "Wyszukiwanie / Widoki", + "Search Activity" : "Aktywność wyszukiwania", + "Search Analytics" : "Analityka wyszukiwania", + "Search Entities" : "Przeszukaj encje", + "Search Files" : "Przeszukaj Pliki", + "Search GitHub" : "Przeszukaj GitHub", + "Search GitLab" : "Przeszukaj GitLab", + "Search Method" : "Metoda wyszukiwania", + "Search Term" : "Termin wyszukiwania", + "Search Trail Details" : "Szczegóły ścieżki wyszukiwania", + "Search Trail Management" : "Zarządzanie ścieżką wyszukiwania", + "Search Trail Statistics" : "Statystyki ścieżki wyszukiwania", + "Search Trails" : "Ścieżki wyszukiwania", + "Search Views" : "Przeszukaj widoki", + "Search Webhooks" : "Przeszukaj webhooki", + "Search by file name or path" : "Szukaj według nazwy lub ścieżki Pliku", + "Search by name or URL" : "Szukaj według nazwy lub adresu URL", + "Search by value" : "Szukaj według wartości", + "Search in Files" : "Szukaj w Plikach", + "Search in Objects" : "Szukaj w Obiektach", + "Search property names..." : "Szukaj nazw Właściwości...", + "Search trail deletion not implemented yet" : "Usuwanie ścieżki wyszukiwania nie zostało jeszcze zaimplementowane", + "Search views..." : "Przeszukaj widoki...", + "Secret" : "Sekret", + "Secret key for HMAC signature generation (optional)" : "Klucz sekretny do generowania podpisu HMAC (opcjonalnie)", + "Select AI Agent" : "Wybierz agenta AI", + "Select All" : "Zaznacz wszystko", + "Select ConfigSet" : "Wybierz ConfigSet", + "Select File Types to Vectorize:" : "Wybierz typy Plików do wektoryzacji:", + "Select HTTP method" : "Wybierz metodę HTTP", + "Select Register and Schema" : "Wybierz Rejestr i Schemat", + "Select Views to Vectorize:" : "Wybierz widoki do wektoryzacji:", + "Select a Nextcloud group to add" : "Wybierz grupę Nextcloud do dodania", + "Select a branch" : "Wybierz gałąź", + "Select a model or type a custom model name" : "Wybierz model lub wpisz niestandardową nazwę modelu", + "Select a repository" : "Wybierz repozytorium", + "Select a repository you have write access to" : "Wybierz repozytorium, do którego masz dostęp zapisu", + "Select an AI Agent" : "Wybierz agenta AI", + "Select an AI agent to begin chatting with your data." : "Wybierz agenta AI, aby rozpocząć czat ze swoimi danymi.", + "Select backend" : "Wybierz backend", + "Select chat model" : "Wybierz model czatu", + "Select collection for files" : "Wybierz kolekcję dla Plików", + "Select collection for objects" : "Wybierz kolekcję dla Obiektów", + "Select default organisation" : "Wybierz domyślną organizację", + "Select event to listen to..." : "Wybierz wydarzenie do nasłuchiwania...", + "Select model" : "Wybierz model", + "Select options..." : "Wybierz opcje...", + "Select period" : "Wybierz okres", + "Select property to send as payload" : "Wybierz Właściwość do wysłania jako ładunek", + "Select provider" : "Wybierz dostawcę", + "Select registers and schemas to save a view" : "Wybierz Rejestry i Schematy, aby zapisać widok", + "Select retry policy" : "Wybierz zasady ponawiania", + "Select the branch to publish to" : "Wybierz gałąź, do której opublikować", + "Select the event this webhook should listen to" : "Wybierz wydarzenie, którego ten webhook ma nasłuchiwać", + "Select views to vectorize:" : "Wybierz widoki do wektoryzacji:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Wybierz, które grupy Nextcloud są dostępne dla tej organizacji. Użytkownicy w tych grupach będą mieli dostęp do zasobów organizacji.", + "Select which data views the AI can search" : "Wybierz, które widoki danych AI może przeszukiwać", + "Select which property from the event should be used as the webhook payload data" : "Wybierz, która Właściwość z wydarzenia ma zostać użyta jako dane ładunku webhooka", + "Select which tools the AI can use to perform actions" : "Wybierz, których narzędzi AI może użyć do wykonywania akcji", + "Selected" : "Wybrane", + "Selected Groups" : "Wybrane grupy", + "Selected audit trails deleted successfully" : "Wybrane ścieżki audytu zostały pomyślnie usunięte", + "Selected search trails deleted successfully" : "Wybrane ścieżki wyszukiwania zostały pomyślnie usunięte", + "Selected users can access this view" : "Wybrani użytkownicy mają dostęp do tego widoku", + "Send additional feedback" : "Wyślij dodatkową opinię", + "Send as CloudEvent" : "Wyślij jako CloudEvent", + "Sensitive PII" : "Wrażliwe dane osobowe", + "Serial Mode (Safer, slower)" : "Tryb szeregowy (bezpieczniejszy, wolniejszy)", + "Serial:" : "Szeregowo:", + "Server Configuration" : "Konfiguracja serwera", + "Server Information" : "Informacje o serwerze", + "Settings" : "Ustawienia", + "Shards" : "Fragmenty", + "Share" : "Udostępnij", + "Share with Groups" : "Udostępnij grupom", + "Share with Users" : "Udostępnij użytkownikom", + "Show Filters" : "Pokaż filtry", + "Show less" : "Pokaż mniej", + "Show only entries with changes" : "Pokaż tylko wpisy ze zmianami", + "Simple" : "Proste", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Zapytania proste: podstawowe wyszukiwania tekstowe z minimalnymi parametrami (np. pojedynczy termin wyszukiwania, bez filtrów zaawansowanych)", + "Size" : "Rozmiar", + "Slug" : "Slug", + "Soft Delete Mode" : "Tryb miękkiego usuwania", + "Soft Deleted Items" : "Miękko usunięte elementy", + "SolrCloud mode enabled" : "Tryb SolrCloud włączony", + "Source" : "Źródło", + "Sources" : "Źródła", + "Sources:" : "Źródła:", + "Standalone SOLR mode" : "Tryb autonomiczny SOLR", + "Start Conversation" : "Rozpocznij rozmowę", + "Start Vectorization" : "Rozpocznij wektoryzację", + "Start a conversation" : "Rozpocznij rozmowę", + "Starting names cache warmup..." : "Rozpoczynanie rozgrzewki pamięci podręcznej nazw...", + "Starting..." : "Rozpoczynanie...", + "Statistics" : "Statystyki", + "Stats calculation not yet implemented" : "Obliczanie statystyk nie zostało jeszcze zaimplementowane", + "Status" : "Status", + "Status Code" : "Kod statusu", + "String Constraints" : "Ograniczenia ciągów znaków", + "Success" : "Sukces", + "Success Rate" : "Wskaźnik powodzenia", + "Success Status" : "Status powodzenia", + "Successful" : "Udane", + "Successfully joined organisation" : "Pomyślnie dołączono do organizacji", + "Sync Table" : "Synchronizuj tabelę", + "Synchronization Settings" : "Ustawienia synchronizacji", + "System Default" : "Domyślne systemowe", + "Technical Description" : "Opis techniczny", + "Technical description for developers and administrators" : "Opis techniczny dla deweloperów i administratorów", + "Temperature" : "Temperatura", + "Templates" : "Szablony", + "Test" : "Testuj", + "Test Chat" : "Testuj czat", + "Test Connection" : "Testuj połączenie", + "Test Embedding" : "Testuj osadzanie", + "Test endpoint executed successfully" : "Punkt końcowy testowy został pomyślnie wykonany", + "Test webhook delivered successfully" : "Testowy webhook został pomyślnie dostarczony", + "Test webhook delivery failed" : "Dostarczenie testowego webhooka nieudane", + "Test webhook sent successfully" : "Testowy webhook został pomyślnie wysłany", + "Testing..." : "Testowanie...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Fragmenty tekstu są generowane podczas ekstrakcji Plików i przechowywane w bazie danych. Wektoryzacja odczytuje te wstępnie pofragmentowane Pliki i konwertuje je na osadzenia.", + "Text extracted successfully" : "Tekst został pomyślnie wyodrębniony", + "Text extraction disabled" : "Ekstrakcja tekstu wyłączona", + "The URL where webhook events will be sent" : "Adres URL, na który będą wysyłane wydarzenia webhooka", + "The requested conversation does not exist" : "Żądana rozmowa nie istnieje", + "The saved view and all its search configuration" : "Zapisany widok i cała jego konfiguracja wyszukiwania", + "There are no audit trail entries matching your current filters." : "Brak wpisów ścieżki audytu pasujących do bieżących filtrów.", + "There are no deleted items matching your current filters." : "Brak usuniętych elementów pasujących do bieżących filtrów.", + "There are no search trail entries matching your current filters." : "Brak wpisów ścieżki wyszukiwania pasujących do bieżących filtrów.", + "There are no webhook log entries matching your filters." : "Brak wpisów dziennika webhooków pasujących do filtrów.", + "This action cannot be undone." : "Tej operacji nie można cofnąć.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Tej operacji nie można cofnąć. Upewnij się, że żadna kolekcja nie używa tego ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Ten agent nie ma konfigurowalnych widoków ani narzędzi, ale nadal możesz dostosować ustawienia RAG poniżej.", + "This analysis may take some time" : "Ta analiza może zająć trochę czasu", + "This audit trail entry does not contain any change information." : "Ten wpis ścieżki audytu nie zawiera żadnych informacji o zmianach.", + "This configuration has no data defined." : "Ta konfiguracja nie ma zdefiniowanych danych.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ten punkt końcowy jest przestarzały. Zamiast tego użyj punktów końcowych opartych na fragmentach.", + "This entity has no relations to objects or files" : "Ta encja nie ma relacji z Obiektami ani Plikami", + "This is a test webhook from OpenRegister" : "To jest testowy webhook z OpenRegister", + "This schema must use Magic Table configuration to sync" : "Ten Schemat musi używać konfiguracji tabeli magicznej do synchronizacji", + "This source has no associated registers." : "To Źródło nie ma powiązanych Rejestrów.", + "This will copy the _default ConfigSet with the new name" : "Spowoduje to skopiowanie ConfigSet _default z nową nazwą", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Spowoduje to trwałe usunięcie WSZYSTKICH osadzeń (wektorów) z bazy danych. Konieczne będzie ponowne zwektoryzowanie wszystkich Obiektów i Plików. Tej operacji nie można cofnąć.\\n\\nCzy na pewno chcesz kontynuować?", + "This will permanently delete:" : "Spowoduje to trwałe usunięcie:", + "Timeout (seconds)" : "Limit czasu (sekundy)", + "Timestamp" : "Znacznik czasu", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Wskazówka: Włączaj tylko widoki, które potrzebują wyszukiwania semantycznego, aby zminimalizować koszty osadzania. Proste tabele wyszukiwania rzadko wymagają wektoryzacji.", + "Title" : "Tytuł", + "To Date" : "Data do", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Aby dostosować rozmiar fragmentu lub strategię, przejdź do Konfiguracja Plików → Limity przetwarzania.", + "To change the embedding provider or model, go to LLM Configuration." : "Aby zmienić dostawcę lub model osadzania, przejdź do Konfiguracja LLM.", + "To date" : "Data do", + "Toggle search sidebar" : "Przełącz pasek boczny wyszukiwania", + "Tools" : "Narzędzia", + "Top Deleters" : "Najczęściej usuwający", + "Total" : "Łącznie", + "Total Audit Trails" : "Łącznie ścieżek audytu", + "Total Chunks Available:" : "Łącznie dostępnych fragmentów:", + "Total Deleted Items" : "Łącznie usuniętych elementów", + "Total Members" : "Łącznie członków", + "Total Objects" : "Łącznie Obiektów", + "Total Objects in Database:" : "Łącznie Obiektów w bazie danych:", + "Total Organisations" : "Łącznie organizacji", + "Total Results" : "Łącznie wyników", + "Total Searches" : "Łącznie wyszukiwań", + "Total Size" : "Łączny rozmiar", + "Total Storage" : "Łączna pamięć", + "Total:" : "Łącznie:", + "Totals" : "Sumy", + "Type" : "Typ", + "Type Issue" : "Problem z typem", + "Type Variations:" : "Warianty typu:", + "Type must be \"positive\" or \"negative\"" : "Typ musi być \"positive\" lub \"negative\"", + "Type to search for groups" : "Wpisz, aby wyszukać grupy", + "Type to search for users" : "Wpisz, aby wyszukać użytkowników", + "Type to search groups..." : "Wpisz, aby wyszukać grupy...", + "Type to search users..." : "Wpisz, aby wyszukać użytkowników...", + "URL" : "URL", + "URL where Ollama is running" : "Adres URL, pod którym działa Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Niezmienione", + "Unique Search Terms" : "Unikalne terminy wyszukiwania", + "Unique Terms" : "Unikalne terminy", + "Unknown" : "Nieznane", + "Unknown Register" : "Nieznany Rejestr", + "Unknown Schema" : "Nieznany Schemat", + "Untitled View" : "Widok bez tytułu", + "Update" : "Aktualizuj", + "Update Operations" : "Operacje aktualizacji", + "Update configuration: ..." : "Aktualizuj konfigurację: ...", + "Update register OAS: ..." : "Aktualizuj OAS Rejestru: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Aktualizuj wektory, gdy zmieniają się dane Obiektu (zalecane dla dokładnego wyszukiwania)", + "Updated" : "Zaktualizowano", + "Updated At" : "Zaktualizowano", + "Updated:" : "Zaktualizowano:", + "Uptime:" : "Czas działania:", + "Usage Count" : "Liczba użyć", + "Use SolrCloud with Zookeeper for distributed search" : "Użyj SolrCloud z Zookeeper do wyszukiwania rozproszonego", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Użyj filtrów, aby zawęzić wpisy ścieżki audytu według Rejestru, Schematu, typu akcji, użytkownika, zakresu dat lub identyfikatora Obiektu.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Użyj filtrów, aby zawęzić usunięte elementy według Rejestru, Schematu, daty usunięcia lub użytkownika, który je usunął.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Użyj filtrów, aby zawęzić wpisy ścieżki wyszukiwania według Rejestru, Schematu, statusu powodzenia, użytkownika, zakresu dat, terminów wyszukiwania lub metryk wydajności.", + "Used By Collections" : "Używane przez kolekcje", + "User" : "Użytkownik", + "User Agent Statistics" : "Statystyki klienta użytkownika", + "User Description" : "Opis użytkownika", + "User rate limits cleared successfully" : "Limity szybkości użytkownika zostały pomyślnie wyczyszczone", + "User removed successfully" : "Użytkownik został pomyślnie usunięty", + "User-friendly description shown in forms and help text" : "Przyjazny dla użytkownika opis wyświetlany w formularzach i tekście pomocy", + "Username" : "Nazwa użytkownika", + "Username for SOLR authentication (optional)" : "Nazwa użytkownika do uwierzytelniania SOLR (opcjonalnie)", + "Users" : "Użytkownicy", + "Using Pre-Generated Chunks" : "Używanie wstępnie wygenerowanych fragmentów", + "Validate" : "Zwaliduj", + "Validate Objects" : "Zwaliduj Obiekty", + "Validation completed successfully" : "Walidacja zakończona pomyślnie", + "Validation failed" : "Walidacja nieudana", + "Value" : "Wartość", + "Vector Dimensions" : "Wymiary wektora", + "Vector Search Backend" : "Backend wyszukiwania wektorowego", + "Vector field: _embedding_" : "Pole wektora: _embedding_", + "Vectorization Triggers" : "Wyzwalacze wektoryzacji", + "Vectorize All Files" : "Wektoryzuj wszystkie Pliki", + "Vectorize All Objects" : "Wektoryzuj wszystkie Obiekty", + "Vectorize all file types" : "Wektoryzuj wszystkie typy Plików", + "Vectorize all views" : "Wektoryzuj wszystkie widoki", + "Vectorize on object creation" : "Wektoryzuj przy tworzeniu Obiektu", + "Vectors will be stored in your existing object and file collections" : "Wektory będą przechowywane w istniejących kolekcjach Obiektów i Plików", + "Version" : "Wersja", + "Very High" : "Bardzo wysoki", + "View API Docs" : "Zobacz dokumentację API", + "View Changes" : "Zobacz zmiany", + "View Details" : "Zobacz szczegóły", + "View Error" : "Zobacz błąd", + "View File" : "Zobacz Plik", + "View Full Details" : "Zobacz pełne szczegóły", + "View Logs" : "Zobacz dzienniki", + "View Name" : "Nazwa widoku", + "View Object" : "Zobacz Obiekt", + "View Parameters" : "Zobacz parametry", + "View Selection" : "Wybór widoku", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Przeglądaj i analizuj dzienniki ścieżek wyszukiwania z zaawansowanymi możliwościami filtrowania i analizy", + "View and analyze system audit trails with advanced filtering capabilities" : "Przeglądaj i analizuj systemowe ścieżki audytu z zaawansowanymi możliwościami filtrowania", + "View deleted successfully" : "Widok został pomyślnie usunięty", + "View entity details and manage relations" : "Zobacz szczegóły encji i zarządzaj relacjami", + "View name" : "Nazwa widoku", + "View name is required" : "Nazwa widoku jest wymagana", + "View saved successfully!" : "Widok został pomyślnie zapisany!", + "View search analytics and manage search logs" : "Przeglądaj analitykę wyszukiwania i zarządzaj dziennikami wyszukiwania", + "View successfully deleted" : "Widok został pomyślnie usunięty", + "View successfully updated" : "Widok został pomyślnie zaktualizowany", + "View updated successfully!" : "Widok został pomyślnie zaktualizowany!", + "View webhook delivery logs and filter by webhook" : "Przeglądaj dzienniki dostaw webhooków i filtruj według webhooka", + "Views" : "Widoki", + "Visible to users" : "Widoczne dla użytkowników", + "Wait for Response" : "Czekaj na odpowiedź", + "Wait for webhook response before continuing (required for request/response flows)" : "Czekaj na odpowiedź webhooka przed kontynuowaniem (wymagane dla przepływów żądanie/odpowiedź)", + "Warmup Names Cache" : "Rozgrzej pamięć podręczną nazw", + "Warning" : "Ostrzeżenie", + "Warning:" : "Ostrzeżenie:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Szczegóły dziennika webhooka", + "Webhook Logs" : "Dzienniki webhooków", + "Webhook created successfully" : "Webhook został pomyślnie utworzony", + "Webhook deleted" : "Webhook usunięty", + "Webhook retry delivered successfully" : "Ponowna próba webhooka dostarczona pomyślnie", + "Webhook retry delivery failed" : "Ponowna dostawa webhooka nieudana", + "Webhook updated" : "Webhook zaktualizowany", + "Webhook updated successfully" : "Webhook został pomyślnie zaktualizowany", + "Webhooks" : "Webhooki", + "Weekly" : "Co tydzień", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Opakuj ładunek webhooka w formacie CloudEvents dla lepszej interoperacyjności", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Tak", + "You" : "Ty", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Możesz tworzyć nowe ConfigSets na podstawie szablonu _default lub przesyłać niestandardowe bezpośrednio na serwer SOLR.", + "You do not have access to this conversation" : "Nie masz dostępu do tej rozmowy", + "You do not have permission to delete this conversation" : "Nie masz uprawnień do usunięcia tej rozmowy", + "You do not have permission to modify this conversation" : "Nie masz uprawnień do modyfikacji tej rozmowy", + "You do not have permission to restore this conversation" : "Nie masz uprawnień do przywrócenia tej rozmowy", + "You must be logged in to favorite views" : "Musisz być zalogowany, aby dodawać widoki do ulubionych", + "You need an AI agent to start a conversation." : "Potrzebujesz agenta AI, aby rozpocząć rozmowę.", + "Your Fireworks AI API key. Get one at" : "Twój klucz API Fireworks AI. Pobierz go na", + "Your OpenAI API key. Get one at" : "Twój klucz API OpenAI. Pobierz go na", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Twoja opinia została zarejestrowana. Opcjonalnie możesz podać dodatkowe szczegóły tutaj...", + "Zookeeper Hosts" : "Hosty Zookeeper", + "Zookeeper Port" : "Port Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Ustawienia Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Ciąg połączenia Zookeeper dla SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Numer portu Zookeeper (opcjonalnie, domyślnie 2181)", + "chunk" : "fragment", + "chunks" : "fragmenty", + "chunks processed" : "przetworzonych fragmentów", + "configuration(s)" : "konfiguracja(e)", + "e.g., lib/Settings/config.json" : "np. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "np. lib/Settings/register.json", + "failed" : "nieudane", + "file" : "plik", + "files" : "pliki", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "członkowie", + "menu or contact someone with permission to create agents." : "menu lub skontaktuj się z kimś, kto ma uprawnienia do tworzenia agentów.", + "messages" : "wiadomości", + "n8n URL and API key are required" : "Adres URL i klucz API n8n są wymagane", + "n8n configuration saved successfully" : "Konfiguracja n8n została pomyślnie zapisana", + "n8n connection not configured" : "Połączenie n8n nie zostało skonfigurowane", + "n8n connection successful" : "Połączenie n8n udane", + "n8n connection test successful" : "Test połączenia n8n udany", + "n8n integration disabled" : "Integracja n8n wyłączona", + "n8n integration enabled" : "Integracja n8n włączona", + "n8n project initialized successfully" : "Projekt n8n został pomyślnie zainicjowany", + "n8n settings saved successfully" : "Ustawienia n8n zostały pomyślnie zapisane", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "obiekty", + "objects processed" : "przetworzonych obiektów", + "or pick a range" : "lub wybierz zakres", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "zapewnia dobrą równowagę między szybkością a dokładnością.", + "register(s)" : "rejestr(y)", + "register(s) selected" : "wybrano rejestr(y)", + "required" : "wymagane", + "results" : "wyniki", + "schema(s)" : "schemat(y)", + "schema(s) selected" : "wybrano schemat(y)", + "sk-..." : "sk-...", + "this application" : "ta aplikacja", + "to" : "do", + "vectorized" : "zwektoryzowane", + "views selected" : "wybrano widoki", + "ℹ️ Current Configuration" : "ℹ️ Bieżąca konfiguracja", + "⚡ Batch Processing" : "⚡ Przetwarzanie wsadowe", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Kompleksowy kontekst ✓ Mniejsze ryzyko pominięcia szczegółów ✗ Wolniejsze odpowiedzi ✗ Może zawierać mniej istotne informacje", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Szybsze odpowiedzi ✓ Bardziej skoncentrowane odpowiedzi ✗ Może pominąć istotne informacje", + "✨ AI Features" : "✨ Funkcje AI", + "All search trails cleared successfully" : "Wszystkie ścieżki wyszukiwania zostały pomyślnie wyczyszczone", + "Multiple search trail deletion not implemented yet" : "Usuwanie wielu ścieżek wyszukiwania nie zostało jeszcze zaimplementowane", + "No expired search trails found to clear" : "Nie znaleziono wygasłych ścieżek wyszukiwania do wyczyszczenia", + "Missing message" : "Brak wiadomości", + "message content is required" : "treść wiadomości jest wymagana", + "Missing conversation or agentUuid" : "Brak rozmowy lub agentUuid", + "Access denied" : "Odmowa dostępu", + "Conversation not found" : "Nie znaleziono rozmowy", + "AI service not configured" : "Usługa AI nie została skonfigurowana", + "Failed to process message" : "Nie udało się przetworzyć wiadomości", + "Missing conversationId" : "Brak conversationId", + "conversationId is required" : "conversationId jest wymagany", + "Failed to fetch conversation history" : "Nie udało się pobrać historii rozmowy", + "Failed to clear conversation" : "Nie udało się wyczyścić rozmowy", + "Invalid feedback type" : "Nieprawidłowy typ opinii", + "type must be \"positive\" or \"negative\"" : "typ musi być \"positive\" lub \"negative\"", + "Message not found" : "Nie znaleziono wiadomości", + "Failed to save feedback" : "Nie udało się zapisać opinii", + "Failed to get chat statistics" : "Nie udało się pobrać statystyk czatu", + "Not Found" : "Nie znaleziono", + "Engine not found" : "Nie znaleziono silnika", + "Consumer not found" : "Nie znaleziono konsumenta", + "Backend parameter is required" : "Parametr backend jest wymagany", + "Failed to get database information: %s" : "Nie udało się pobrać informacji o bazie danych: %s", + "SOLR setup error: %s" : "Błąd konfiguracji SOLR: %s", + "Reindex failed: %s" : "Ponowne indeksowanie nieudane: %s", + "Anonymized" : "Zanonimizowane", + "Entities detected" : "Wykryte encje", + "Extract Now" : "Wyodrębnij teraz", + "Extracted at" : "Wyodrębniono", + "Extraction" : "Ekstrakcja", + "Failed to load extraction data" : "Nie udało się załadować danych ekstrakcji", + "Failed to load register data" : "Nie udało się załadować danych Rejestru", + "Failed to retrieve extraction status." : "Nie udało się pobrać statusu ekstrakcji.", + "Failed to retrieve objects for file." : "Nie udało się pobrać Obiektów dla Pliku.", + "No extraction data available for this file" : "Brak dostępnych danych ekstrakcji dla tego Pliku", + "No register objects reference this file" : "Żadne Obiekty Rejestru nie odwołują się do tego Pliku", + "Not extracted" : "Niewyodrębnione", + "Register Objects" : "Obiekty Rejestru", + "Risk level" : "Poziom ryzyka", + "Text chunks" : "Fragmenty tekstu", + "Unknown error" : "Nieznany błąd", + "Very high" : "Bardzo wysoki", + "{title} in {register} / {schema}" : "{title} w {register} / {schema}", + "Push Notifications" : "Powiadomienia push", + "Real-time push notification status via notify_push" : "Status powiadomień push w czasie rzeczywistym przez notify_push", + "Realtime push not available — the notify_push app is not installed" : "Powiadomienia push w czasie rzeczywistym niedostępne — aplikacja notify_push nie jest zainstalowana", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Zainstaluj aplikację notify_push ze sklepu Nextcloud App Store, aby włączyć aktualizacje w czasie rzeczywistym.", + "Open Nextcloud App Store" : "Otwórz Nextcloud App Store", + "notify_push is installed but not yet active" : "notify_push jest zainstalowany, ale jeszcze nieaktywny", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push jest zainstalowany, ale OpenRegister nie potwierdził jeszcze udanego push. Wyzwól zapis Obiektu, aby aktywować, lub sprawdź konfigurację notify_push.", + "notify_push configuration guide" : "przewodnik konfiguracji notify_push", + "Realtime push active" : "Powiadomienia push w czasie rzeczywistym aktywne", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Powiadomienia push w czasie rzeczywistym są aktywne. Połączeni klienci otrzymują natychmiastowe aktualizacje, gdy Obiekty są tworzone, aktualizowane lub usuwane.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +); diff --git a/l10n/pl.json b/l10n/pl.json new file mode 100644 index 0000000000..494e6e2600 --- /dev/null +++ b/l10n/pl.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Wymagany Dolphin)", + "(no subject)": "(brak tematu)", + "(no title)": "(brak tytułu)", + "(unnamed)": "(bez nazwy)", + "(untitled card)": "(karta bez tytułu)", + "({count} selected)": "(wybrano: {count})", + ", {failed} failed": ", nieudane: {failed}", + "0 = unlimited": "0 = bez ograniczeń", + "A deactivation request is pending.": "Oczekuje wniosek o dezaktywację.", + "AI Assistant": "Asystent AI", + "AI Chat": "Czat AI", + "AI service not configured": "Usługa AI nie została skonfigurowana", + "API Key": "Klucz API", + "API Request Quota (requests/day)": "Limit żądań API (żądań/dzień)", + "API Token Configuration": "Konfiguracja tokenów API", + "API Tokens": "Tokeny API", + "API tokens saved successfully": "Tokeny API zostały pomyślnie zapisane", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: niniejszy raport łączy każdą verwerkingsactiviteit z przypisanymi do niej całkowitymi licznikami ścieżki audytu. Audytorzy oraz Autoriteit Persoonsgegevens wykorzystują go do przeglądu nadzorczego.", + "About ConfigSets": "Informacje o ConfigSets", + "Access denied": "Odmowa dostępu", + "Access organisation dashboard and analytics": "Dostęp do pulpitu i analityki organizacji", + "Account": "Konto", + "Action": "Działanie", + "Action Distribution": "Rozkład działań", + "Action:": "Działanie:", + "Actions": "Działania", + "Active": "Aktywny", + "Active Collections": "Aktywne kolekcje", + "Active Filters": "Aktywne filtry", + "Active Organisation:": "Aktywna organizacja:", + "Active Organisations": "Aktywne organizacje", + "Active filters:": "Aktywne filtry:", + "Active organisation changed successfully": "Aktywna organizacja została pomyślnie zmieniona", + "Active organisation set successfully": "Aktywna organizacja została pomyślnie ustawiona", + "Activities": "Aktywności", + "Activity": "Aktywność", + "Activity Period": "Okres aktywności", + "Add": "Dodaj", + "Add Attachment": "Dodaj załącznik", + "Add Endpoint": "Dodaj punkt końcowy", + "Add Groups": "Dodaj grupy", + "Add Property to '{title}'": "Dodaj Właściwość do „{title}”", + "Add Register": "Dodaj Rejestr", + "Add Schema": "Dodaj Schemat", + "Add Source": "Dodaj Źródło", + "Add User": "Dodaj użytkownika", + "Add User to Organisation": "Dodaj użytkownika do organizacji", + "Add a contact from any of your address books to associate it with this object.": "Dodaj kontakt z dowolnej książki adresowej, aby powiązać go z tym Obiektem.", + "Add a file or files": "Dodaj Plik lub Pliki", + "Add card": "Dodaj kartę", + "Add contact": "Dodaj kontakt", + "Add endpoint": "Dodaj punkt końcowy", + "Add oneOf entry": "Dodaj wpis oneOf", + "Add range": "Dodaj zakres", + "Add schema titles, descriptions, and register information to provide richer context for search": "Dodaj tytuły Schematów, opisy oraz informacje o Rejestrze, aby zapewnić bogatszy kontekst wyszukiwania", + "Add to favorites": "Dodaj do ulubionych", + "Added": "Dodano", + "Added to favorites": "Dodano do ulubionych", + "Additional Information": "Informacje dodatkowe", + "Additional feedback saved. Thank you!": "Dodatkowa opinia została zapisana. Dziękujemy!", + "Addresses": "Adresy", + "Admin": "Administrator", + "Advanced": "Zaawansowane", + "Advanced Filters": "Filtry zaawansowane", + "Advanced Options": "Opcje zaawansowane", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Filtry zaawansowane nie są dostępne podczas korzystania ze Źródła bazy danych. Przełącz na Auto lub indeks SOLR, aby uzyskać opcje filtrowania.", + "After": "Po", + "Agent Type": "Typ agenta", + "Agent deleted successfully": "Agent został pomyślnie usunięty", + "Agent successfully created": "Agent został pomyślnie utworzony", + "Agent successfully updated": "Agent został pomyślnie zaktualizowany", + "Agents": "Agenci", + "Aggregated across schemas": "Zagregowane dla wszystkich Schematów", + "All": "Wszystko", + "All Categories": "Wszystkie kategorie", + "All Collections": "Wszystkie kolekcje", + "All Confidence Levels": "Wszystkie poziomy pewności", + "All ConfigSets": "Wszystkie ConfigSets", + "All Files": "Wszystkie Pliki", + "All Levels": "Wszystkie poziomy", + "All Types": "Wszystkie typy", + "All Webhooks": "Wszystkie webhooki", + "All actions": "Wszystkie działania", + "All audit trails cleared successfully": "Wszystkie ścieżki audytu zostały pomyślnie wyczyszczone", + "All clear": "Wszystko wyczyszczone", + "All file types": "Wszystkie typy Plików", + "All registers": "Wszystkie Rejestry", + "All schemas": "Wszystkie Schematy", + "All search trails cleared successfully": "Wszystkie ścieżki wyszukiwania zostały pomyślnie wyczyszczone", + "All searches": "Wszystkie wyszukiwania", + "All users": "Wszyscy użytkownicy", + "All-day events": "Wydarzenia całodniowe", + "Allowed MIME Types": "Dozwolone typy MIME", + "Already linked": "Już powiązano", + "An error occurred while clearing audit trails": "Wystąpił błąd podczas czyszczenia ścieżek audytu", + "An error occurred while deleting the audit trail": "Wystąpił błąd podczas usuwania ścieżki audytu", + "An error occurred while deleting the object": "Wystąpił błąd podczas usuwania Obiektu", + "An error occurred while deleting the view": "Wystąpił błąd podczas usuwania widoku", + "An error occurred while permanently deleting the objects": "Wystąpił błąd podczas trwałego usuwania Obiektów", + "An error occurred while restoring the objects": "Wystąpił błąd podczas przywracania Obiektów", + "An error occurred while saving the organisation": "Wystąpił błąd podczas zapisywania organizacji", + "Analysis completed:": "Analiza zakończona:", + "Analysis steps:": "Kroki analizy:", + "Analytics": "Analityka", + "Analyze Objects": "Analizuj Obiekty", + "Analyze Properties": "Analizuj Właściwości", + "Analyze existing properties for improvement opportunities": "Analizuj istniejące Właściwości pod kątem możliwości ulepszeń", + "Analyzing...": "Analizowanie...", + "Anonymized": "Zanonimizowane", + "Anonymous Group": "Grupa anonimowa", + "Any favorites and sharing settings for this view": "Wszelkie ulubione i ustawienia udostępniania tego widoku", + "Any user": "Dowolny użytkownik", + "App ID": "Identyfikator aplikacji", + "App store cache invalidated successfully": "Pamięć podręczna sklepu z aplikacjami została pomyślnie unieważniona", + "App store cache invalidated: {invalidated}": "Pamięć podręczna sklepu z aplikacjami unieważniona: {invalidated}", + "Application deleted successfully": "Aplikacja została pomyślnie usunięta", + "Application identifier for this configuration (optional)": "Identyfikator aplikacji dla tej konfiguracji (opcjonalnie)", + "Application settings": "Ustawienia aplikacji", + "Application successfully created": "Aplikacja została pomyślnie utworzona", + "Application successfully updated": "Aplikacja została pomyślnie zaktualizowana", + "Applications": "Aplikacje", + "Apply": "Zastosuj", + "Apply Changes": "Zastosuj zmiany", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Zastosuj rolę do wszystkich Schematów w tym Rejestrze, które nie mają jawnych nadpisań autoryzacji.", + "Approval Progress": "Postęp zatwierdzania", + "Approve": "Zatwierdź", + "Approved": "Zatwierdzono", + "Archive": "Archiwizuj", + "Archive conversation": "Archiwizuj rozmowę", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Zarchiwizować tę verwerkingsactiviteit? Wiersze ścieżki audytu nadal będą się do niej odwoływać.", + "Archived conversations are hidden from your active list": "Zarchiwizowane rozmowy są ukryte na liście aktywnych", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Czy na pewno chcą Państwo USUNĄĆ kolekcję „{name}”?\n\nSpowoduje to:\n• Trwałe usunięcie kolekcji i wszystkich jej danych\n• Usunięcie wszystkich zindeksowanych dokumentów\n• Tej operacji nie można cofnąć", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Czy na pewno chcą Państwo wyczyścić stare ścieżki wyszukiwania? Spowoduje to usunięcie wpisów starszych niż 30 dni.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Czy na pewno chcą Państwo wyczyścić wszystkie dane z kolekcji „{name}”?\n\nSpowoduje to:\n• Usunięcie wszystkich zindeksowanych dokumentów\n• Zachowanie nienaruszonej struktury kolekcji\n• Tej operacji nie można cofnąć", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Czy na pewno chcą Państwo usunąć aplikację „{name}”? Tej operacji nie można cofnąć.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Czy na pewno chcą Państwo usunąć wybrane ścieżki audytu? Tej operacji nie można cofnąć.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Czy na pewno chcą Państwo usunąć wybrane ścieżki wyszukiwania? Tej operacji nie można cofnąć.", + "Are you sure you want to delete this ConfigSet?": "Czy na pewno chcą Państwo usunąć ten ConfigSet?", + "Are you sure you want to delete this configset?": "Czy na pewno chcą Państwo usunąć ten configset?", + "Are you sure you want to leave '{name}'?": "Czy na pewno chcą Państwo opuścić „{name}”?", + "Are you sure you want to permanently delete": "Czy na pewno chcą Państwo trwale usunąć", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Czy na pewno chcą Państwo ponownie zindeksować kolekcję „{name}”?\n\nSpowoduje to:\n• Przebudowę indeksu ze wszystkimi Obiektami\n• Zajmie kilka minut\n• Może wpłynąć na wydajność wyszukiwania podczas ponownego indeksowania", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Czy na pewno chcą Państwo usunąć Schemat „{schema}” z Rejestru „{register}”? Tej operacji nie można cofnąć.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Czy na pewno chcą Państwo miękko usunąć {count} aktywnych Obiektów dla Schematu „{schema}”?\n\nZostaną oznaczone jako usunięte, ale będzie można je trwale usunąć później.", + "Array Object Configuration:": "Konfiguracja Obiektu tablicowego:", + "Articles": "Artykuły", + "Ask a question...": "Zadaj pytanie...", + "Ask questions about your data using natural language": "Zadawaj pytania o swoje dane, używając języka naturalnego", + "Assigned collaborative tags": "Przypisane tagi współdzielone", + "Assignment notifications": "Powiadomienia o przypisaniach", + "Attachment added to {name}": "Załącznik dodany do {name}", + "Attempt": "Próba", + "Audit Trail Changes": "Zmiany ścieżki audytu", + "Audit Trail Details": "Szczegóły ścieżki audytu", + "Audit Trail Management": "Zarządzanie ścieżką audytu", + "Audit Trail Statistics": "Statystyki ścieżki audytu", + "Audit Trails": "Ścieżki audytu", + "Audit trail data copied to clipboard": "Dane ścieżki audytu skopiowane do schowka", + "Audit trail deleted successfully": "Ścieżka audytu została pomyślnie usunięta", + "Audit trail successfully deleted": "Ścieżka audytu została pomyślnie usunięta", + "Authentication": "Uwierzytelnianie", + "Auto (e.g. Y for year, F Y for month)": "Auto (np. Y dla roku, F Y dla miesiąca)", + "Auto share": "Udostępnianie automatyczne", + "Auto-commit disabled": "Automatyczne zatwierdzanie wyłączone", + "Auto-commit enabled": "Automatyczne zatwierdzanie włączone", + "Auto-create Default Organisation": "Automatyczne tworzenie organizacji domyślnej", + "Auto-retry failed vectorizations": "Automatyczne ponawianie nieudanych wektoryzacji", + "Automatically commit changes to SOLR index": "Automatycznie zatwierdzaj zmiany w indeksie SOLR", + "Automatically connect to the federation network.": "Automatycznie łącz z siecią federacyjną.", + "Automatically create a default organisation if none exists when the app is initialized": "Automatycznie utwórz organizację domyślną, jeśli żadna nie istnieje podczas inicjalizacji aplikacji", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automatycznie generuj osadzenia wektorowe z fragmentów tekstu, gdy Pliki są przesyłane i przetwarzane", + "Automatically generate vector embeddings when objects are created or updated": "Automatycznie generuj osadzenia wektorowe, gdy Obiekty są tworzone lub aktualizowane", + "Automatically retry failed vectorization attempts (max 3 retries)": "Automatycznie ponawiaj nieudane próby wektoryzacji (maks. 3 ponowienia)", + "Automatically update catalogues.": "Automatycznie aktualizuj katalogi.", + "Available Filters": "Dostępne filtry", + "Available Workflows": "Dostępne przepływy pracy", + "Available placeholders:": "Dostępne symbole zastępcze:", + "Avatar": "Awatar", + "Avatar changes are not supported by your authentication provider.": "Zmiany awatara nie są obsługiwane przez Państwa dostawcę uwierzytelniania.", + "Avatar removed": "Awatar usunięty", + "Avatar updated successfully": "Awatar został pomyślnie zaktualizowany", + "Avg Execution Time": "Śr. czas wykonania", + "Avg Members/Org": "Śr. członków/org.", + "Avg Object Views/Session": "Śr. wyświetleń Obiektów/sesję", + "Avg Response Time": "Śr. czas odpowiedzi", + "Avg Results/Search": "Śr. wyników/wyszukiwanie", + "Avg Searches/Session": "Śr. wyszukiwań/sesję", + "Back": "Wstecz", + "Back to Entities": "Powrót do encji", + "Back to Registers": "Powrót do Rejestrów", + "Back to Webhooks": "Powrót do webhooków", + "Back to applications": "Powrót do aplikacji", + "Back to entities": "Powrót do encji", + "Backend parameter is required": "Parametr backendu jest wymagany", + "Backend updated successfully. Please reload the application.": "Backend został pomyślnie zaktualizowany. Proszę ponownie załadować aplikację.", + "Bandwidth Quota (MB/month)": "Limit przepustowości (MB/miesiąc)", + "Base URL (Optional)": "Bazowy URL (opcjonalnie)", + "Basic Information": "Informacje podstawowe", + "Batch Size": "Rozmiar partii", + "Batch extraction completed": "Ekstrakcja wsadowa zakończona", + "Before": "Przed", + "Before object vectorization can work:": "Aby wektoryzacja Obiektów mogła działać:", + "Behavior": "Zachowanie", + "Behavior Issue": "Problem z zachowaniem", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (czas trwania ISO-8601, np. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Magazyn blob został wycofany. Wszystkie Obiekty korzystają teraz z tabel magic.", + "Bookmarks": "Zakładki", + "Branch": "Gałąź", + "Bulk Role Assignment: {title}": "Masowe przypisanie roli: {title}", + "Bulk delete operation completed successfully": "Operacja masowego usuwania zakończona pomyślnie", + "Bulk save operation completed successfully": "Operacja masowego zapisu zakończona pomyślnie", + "Business Data": "Dane biznesowe", + "CSV": "CSV", + "Cache cleared successfully": "Pamięć podręczna została pomyślnie wyczyszczona", + "Cache grew from {old} to {new} entries.": "Pamięć podręczna wzrosła z {old} do {new} wpisów.", + "Cache shrunk from {old} to {new} entries.": "Pamięć podręczna zmniejszyła się z {old} do {new} wpisów.", + "Cache stayed the same at {size} entries.": "Pamięć podręczna pozostała bez zmian na poziomie {size} wpisów.", + "Calculate Sizes": "Oblicz rozmiary", + "Calendar": "Kalendarz", + "Calendar Provider Configuration": "Konfiguracja dostawcy kalendarza", + "Calendar integration is not available": "Integracja z kalendarzem jest niedostępna", + "Calendar name": "Nazwa kalendarza", + "Cancel": "Anuluj", + "Cancel deactivation request": "Anuluj wniosek o dezaktywację", + "Cannot delete: objects are still attached": "Nie można usunąć: Obiekty są nadal dołączone", + "Cannot edit: This register is managed by external configuration {title}": "Nie można edytować: ten Rejestr jest zarządzany przez konfigurację zewnętrzną {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nie można usunąć Schematu z istniejącymi Obiektami ({count} Obiektów)", + "Cannot remove schema {schema} because it contains {count} objects": "Nie można usunąć Schematu {schema}, ponieważ zawiera {count} Obiektów", + "Cards": "Karty", + "Cascade delete": "Usuwanie kaskadowe", + "Categories of data subjects (one per line)": "Categorieën betrokkenen (po jednej w wierszu)", + "Categories of personal data (one per line)": "Categorieën persoonsgegevens (po jednej w wierszu)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategoria", + "Change Type": "Typ zmiany", + "Change password": "Zmień hasło", + "Changes": "Zmiany", + "Changes copied to clipboard": "Zmiany skopiowane do schowka", + "Changes for Audit Trail #{id}": "Zmiany dla ścieżki audytu #{id}", + "Chat Model": "Model czatu", + "Chat Provider": "Dostawca czatu", + "Chat Provider (RAG)": "Dostawca czatu (RAG)", + "Chat Settings": "Ustawienia czatu", + "Chat provider connection successful!": "Połączenie z dostawcą czatu udane!", + "Chat settings": "Ustawienia czatu", + "Chat test failed: {error}": "Test czatu nie powiódł się: {error}", + "Choose a register": "Wybierz Rejestr", + "Choose a schema": "Wybierz Schemat", + "Choose how vector similarity calculations are performed for semantic search": "Wybierz sposób wykonywania obliczeń podobieństwa wektorów dla wyszukiwania semantycznego", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Wybierz, które typy Plików uwzględnić w procesie wektoryzacji. Przetwarzane będą tylko Pliki z wyodrębnionym tekstem i fragmentami.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Wybierz, które widoki uwzględnić w procesie wektoryzacji. Pozostaw puste, aby przetworzyć wszystkie widoki zgodnie z konfiguracją.", + "Chunk": "Fragment", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Usuwanie fragmentów nie zostało jeszcze zaimplementowane. Użyj punktów końcowych opartych na fragmentach.", + "Chunks": "Fragmenty", + "Chunks to Vectorize:": "Fragmenty do wektoryzacji:", + "Cleanup Old Trails": "Wyczyść stare ścieżki", + "Cleanup completed": "Czyszczenie zakończone", + "Cleanup completed successfully. Deleted {count} entries.": "Czyszczenie zakończone pomyślnie. Usunięto {count} wpisów.", + "Cleanup failed: {error}": "Czyszczenie nie powiodło się: {error}", + "Clear All Embeddings": "Wyczyść wszystkie osadzenia", + "Clear Entries": "Wyczyść wpisy", + "Clear Filtered Audit Trails": "Wyczyść przefiltrowane ścieżki audytu", + "Clear Filters": "Wyczyść filtry", + "Clear Index": "Wyczyść indeks", + "Clear Selection": "Wyczyść zaznaczenie", + "Clear all": "Wyczyść wszystko", + "Clear all filters": "Wyczyść wszystkie filtry", + "Clear date range": "Wyczyść zakres dat", + "Clear filters": "Wyczyść filtry", + "Clearing...": "Czyszczenie...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Kliknij Uruchom skanowanie zgodności, aby znaleźć Schematy, w których wykryto dane osobowe (PII), ale nie istnieje adnotacja verwerkingsactiviteit.", + "Click to subscribe to notifications": "Kliknij, aby zasubskrybować powiadomienia", + "Click to unsubscribe from notifications": "Kliknij, aby zrezygnować z powiadomień", + "Close": "Zamknij", + "Cluster name": "Nazwa klastra", + "Code": "Kod", + "Code (short readable key, e.g. v-2026-001)": "Kod (krótki czytelny klucz, np. v-2026-001)", + "Collapse details": "Zwiń szczegóły", + "Collection Management": "Zarządzanie kolekcją", + "Collection Name": "Nazwa kolekcji", + "Collection assignments updated successfully": "Przypisania kolekcji zostały pomyślnie zaktualizowane", + "Collection cleared successfully": "Kolekcja została pomyślnie wyczyszczona", + "Collection copied successfully": "Kolekcja została pomyślnie skopiowana", + "Collection created successfully": "Kolekcja została pomyślnie utworzona", + "Collection deleted successfully": "Kolekcja została pomyślnie usunięta", + "Collection used to store and index file metadata and content": "Kolekcja używana do przechowywania i indeksowania metadanych oraz treści Plików", + "Collection used to store and index object data": "Kolekcja używana do przechowywania i indeksowania danych Obiektów", + "Collections:": "Kolekcje:", + "Color": "Kolor", + "Comment...": "Komentarz...", + "Commit Message": "Komunikat zatwierdzenia", + "Commit Within (ms)": "Zatwierdź w ciągu (ms)", + "Commit message": "Komunikat zatwierdzenia", + "Compare current schema with real object data": "Porównaj bieżący Schemat z rzeczywistymi danymi Obiektów", + "Completed": "Zakończono", + "Complex": "Złożone", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Zapytania złożone: zaawansowane wyszukiwania z wieloma filtrami, operatorami i złożonymi kombinacjami parametrów", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Zapytania złożone: zaawansowane wyszukiwania z wieloma filtrami, operatorami i złożonymi kombinacjami parametrów", + "Compliance": "Zgodność", + "Confidence Level": "Poziom pewności", + "Confidence Score": "Wynik pewności", + "Confidence: {confidence}%": "Pewność: {confidence}%", + "Config must be provided as an object": "Konfiguracja musi zostać podana jako obiekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Zarządzanie ConfigSet", + "ConfigSet Name": "Nazwa ConfigSet", + "ConfigSet created successfully": "ConfigSet został pomyślnie utworzony", + "ConfigSet deleted successfully": "ConfigSet został pomyślnie usunięty", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets definiują schemat i konfigurację kolekcji SOLR. Zawierają definicje pól, analizatory oraz inne ustawienia wyszukiwania.", + "Configuration": "Konfiguracja", + "Configuration Data": "Dane konfiguracji", + "Configuration is up to date": "Konfiguracja jest aktualna", + "Configuration saved": "Konfiguracja zapisana", + "Configuration saved successfully.": "Konfiguracja została pomyślnie zapisana.", + "Configuration type (default, application, etc.)": "Typ konfiguracji (default, application itp.)", + "Configurations": "Konfiguracje", + "Configure API tokens for external service integrations": "Skonfiguruj tokeny API do integracji z usługami zewnętrznymi", + "Configure Apache SOLR search engine for advanced search capabilities": "Skonfiguruj wyszukiwarkę Apache SOLR dla zaawansowanych możliwości wyszukiwania", + "Configure Facets": "Skonfiguruj fasety", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Skonfiguruj dostawców LLM dla funkcji opartych na AI, w tym wyszukiwania semantycznego, osadzeń i czatu.", + "Configure SOLR Facets": "Skonfiguruj fasety SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Skonfiguruj podstawowe ustawienia połączenia serwera SOLR, w tym uwierzytelnianie i opcje sieciowe. Użyj osobnych okien zarządzania ConfigSet i kolekcjami, aby zarządzać rdzeniami i kolekcjami.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Skonfiguruj podstawowe ustawienia połączenia serwera SOLR, w tym uwierzytelnianie i opcje sieciowe. Użyj osobnych okien zarządzania configset i kolekcjami, aby zarządzać rdzeniami i kolekcjami.", + "Configure default organisation and organisation-related settings": "Skonfiguruj organizację domyślną oraz ustawienia związane z organizacją", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Skonfiguruj sposób konwertowania Obiektów bazy danych na osadzenia wektorowe dla wyszukiwania semantycznego. Obiekty są wektoryzowane bezpośrednio, bez potrzeby ekstrakcji tekstu.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Skonfiguruj sposób konwertowania Obiektów na tekst przed wektoryzacją. Te ustawienia wpływają na jakość i kontekst wyszukiwania.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Skonfiguruj dostawców LLM dla funkcji opartych na AI, w tym wyszukiwania semantycznego, osadzeń i czatu.", + "Configure multi-organization support and tenant isolation": "Skonfiguruj obsługę wielu organizacji oraz izolację dzierżawców", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Skonfiguruj parametry wektoryzacji Plików. Ten proces wygeneruje osadzenia wektorowe dla wszystkich wyodrębnionych fragmentów Plików.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Skonfiguruj parametry wektoryzacji Obiektów. Ten proces wygeneruje osadzenia wektorowe dla wszystkich Obiektów pasujących do filtrów widoku.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Skonfiguruj ten Schemat, aby udostępniać Obiekty jako wydarzenia w aplikacji Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Skonfiguruj, które typy danych przeszukiwać i ile Źródeł pobierać", + "Configuring field mappings...": "Konfigurowanie mapowań pól...", + "Confirm Account Deactivation": "Potwierdź dezaktywację konta", + "Confirm Object Deletion": "Potwierdź usunięcie Obiektu", + "Confirm Object Restoration": "Potwierdź przywrócenie Obiektu", + "Confirm Permanent Object Deletion": "Potwierdź trwałe usunięcie Obiektu", + "Confirm deactivation": "Potwierdź dezaktywację", + "Connecting to SOLR cluster...": "Łączenie z klastrem SOLR...", + "Connection Failed": "Połączenie nie powiodło się", + "Connection Settings": "Ustawienia połączenia", + "Connection Successful!": "Połączenie udane!", + "Connection failed": "Połączenie nie powiodło się", + "Connection protocol": "Protokół połączenia", + "Connection successful": "Połączenie udane", + "Connection timeout in seconds": "Limit czasu połączenia w sekundach", + "Constant delay between retries (5 minutes)": "Stałe opóźnienie między ponowieniami (5 minut)", + "Constraint Issue": "Problem z ograniczeniem", + "Consumer not found": "Nie znaleziono konsumenta", + "Contacts": "Kontakty", + "Contacts integration is not available": "Integracja z kontaktami jest niedostępna", + "Content Filters": "Filtry treści", + "Continue to Properties": "Przejdź do Właściwości", + "Contracts": "Umowy", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontroluj, które widoki Obiektów powinny być wektoryzowane, aby zmniejszyć koszty API. Wektoryzuj tylko widoki, które korzystają z wyszukiwania semantycznego.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontroluj, których widoków i narzędzi AI może używać w tej rozmowie. Domyślnie wszystkie możliwości agenta są włączone.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontroluje losowość (0 = deterministyczne, 2 = bardzo kreatywne)", + "Conversation ID is required": "Identyfikator rozmowy jest wymagany", + "Conversation archived": "Rozmowa zarchiwizowana", + "Conversation archived successfully": "Rozmowa została pomyślnie zarchiwizowana", + "Conversation cleared successfully": "Rozmowa została pomyślnie wyczyszczona", + "Conversation deleted": "Rozmowa usunięta", + "Conversation not found": "Nie znaleziono rozmowy", + "Conversation permanently deleted": "Rozmowa została trwale usunięta", + "Conversation renamed": "Rozmowa zmieniła nazwę", + "Conversation restored": "Rozmowa przywrócona", + "Conversation started with {agent}": "Rozmowa rozpoczęta z {agent}", + "Conversation title": "Tytuł rozmowy", + "Conversations": "Rozmowy", + "Converting {schema} to blob storage...": "Konwertowanie {schema} na magazyn blob...", + "Converting {schema} to magic table...": "Konwertowanie {schema} na tabelę magic...", + "Copied!": "Skopiowano!", + "Copy": "Kopiuj", + "Copy Changes": "Kopiuj zmiany", + "Copy Collection": "Kopiuj kolekcję", + "Copy Data": "Kopiuj dane", + "Copy Full Data": "Kopiuj pełne dane", + "Copy of {name}": "Kopia {name}", + "Copy to clipboard": "Kopiuj do schowka", + "Copying...": "Kopiowanie...", + "Costs": "Koszty", + "Count": "Liczba", + "Counting objects...": "Liczenie Obiektów...", + "Create": "Utwórz", + "Create API Token": "Utwórz token API", + "Create Agent": "Utwórz agenta", + "Create Application": "Utwórz aplikację", + "Create Collection": "Utwórz kolekcję", + "Create ConfigSet": "Utwórz ConfigSet", + "Create New Collection": "Utwórz nową kolekcję", + "Create New ConfigSet": "Utwórz nowy ConfigSet", + "Create Operations": "Operacje tworzenia", + "Create Organisation": "Utwórz organizację", + "Create Webhook": "Utwórz webhook", + "Create a copy of collection:": "Utwórz kopię kolekcji:", + "Create a new ConfigSet based on the _default template": "Utwórz nowy ConfigSet na podstawie szablonu _default", + "Create a new SOLR collection from an existing ConfigSet": "Utwórz nową kolekcję SOLR z istniejącego ConfigSet", + "Create a new SOLR collection from an existing configset": "Utwórz nową kolekcję SOLR z istniejącego ConfigSet", + "Create a new configset based on the _default template": "Utwórz nowy ConfigSet na podstawie szablonu _default", + "Create a new event or link an existing one from any of your calendars.": "Utwórz nowe wydarzenie lub połącz istniejące z dowolnego ze swoich kalendarzy.", + "Create another": "Utwórz kolejny", + "Create copies of": "Utwórz kopie", + "Create event": "Utwórz wydarzenie", + "Create new token": "Utwórz nowy token", + "Create or link a Deck card to track work on this object.": "Utwórz lub połącz kartę Deck, aby śledzić pracę nad tym Obiektem.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Utwórz pierwszą verwerkingsactiviteit, aby rozpocząć oznaczanie wierszy ścieżki audytu ich przypisaniem do AVG Art 30.", + "Create your first AI agent to get started.": "Utwórz swojego pierwszego agenta AI, aby rozpocząć.", + "Created": "Utworzono", + "Created:": "Utworzono:", + "Creating search collections...": "Tworzenie kolekcji wyszukiwania...", + "Creating...": "Tworzenie...", + "Current": "Bieżący", + "Current Filters": "Bieżące filtry", + "Current Type:": "Bieżący typ:", + "Current password": "Bieżące hasło", + "Custom API endpoint if using a different region": "Niestandardowy punkt końcowy API, jeśli używany jest inny region", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Niestandardowe nagłówki HTTP (jeden na wiersz, format: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Niestandardowe nagłówki HTTP (jeden na wiersz, format: header-name: value)", + "Custom Ranges:": "Niestandardowe zakresy:", + "Custom display title for this facet": "Niestandardowy tytuł wyświetlania dla tego facetu", + "Custom pattern:": "Niestandardowy wzorzec:", + "DELETE COLLECTION": "USUŃ KOLEKCJĘ", + "DSAR": "DSAR", + "Daily": "Codziennie", + "Dashboard": "Pulpit", + "Dashboard not found": "Nie znaleziono pulpitu", + "Data": "Dane", + "Data Source": "Źródło danych", + "Data Sources": "Źródła danych", + "Data Views": "Widoki danych", + "Data sources": "Źródła danych", + "Data type does not match observed values": "Typ danych nie pasuje do zaobserwowanych wartości", + "Data-subject access request": "Wniosek o dostęp osoby, której dane dotyczą", + "Database URL": "Adres URL bazy danych", + "Database information refreshed": "Informacje o bazie danych zostały odświeżone", + "Date Faceting:": "Facetowanie według dat:", + "Date Range": "Zakres dat", + "Dates": "Daty", + "Deactivation request cancelled": "Wniosek o dezaktywację został anulowany", + "Deactivation request submitted": "Wniosek o dezaktywację został złożony", + "Deck": "Deck", + "Deck integration is not available": "Integracja z Deck jest niedostępna", + "Default": "Domyślne", + "Default Object Tenant": "Domyślny dzierżawca Obiektu", + "Default Organisation": "Domyślna organizacja", + "Default Owner": "Domyślny właściciel", + "Default User (for cron/background jobs)": "Domyślny użytkownik (dla zadań cron/w tle)", + "Default User Tenant": "Domyślny dzierżawca użytkownika", + "Default value": "Wartość domyślna", + "Default value (Email)": "Wartość domyślna (Email)", + "Default value (Password)": "Wartość domyślna (Hasło)", + "Default value (Phone number)": "Wartość domyślna (Numer telefonu)", + "Default value (Regex)": "Wartość domyślna (Regex)", + "Default: 5": "Domyślnie: 5", + "Defaults to current user. Select a different user if needed.": "Domyślnie bieżący użytkownik. W razie potrzeby wybierz innego użytkownika.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Opóźnienia podwajają się przy każdej próbie (2, 4, 8 minut...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Opóźnienia rosną liniowo (5, 10, 15 minut...)", + "Delete": "Usuń", + "Delete ({count})": "Usuń ({count})", + "Delete Application": "Usuń aplikację", + "Delete Audit Trail": "Usuń ścieżkę audytu", + "Delete Collection": "Usuń kolekcję", + "Delete ConfigSet": "Usuń ConfigSet", + "Delete Objects": "Usuń Obiekty", + "Delete Operations": "Operacje usuwania", + "Delete SOLR Collection": "Usuń kolekcję SOLR", + "Delete View": "Usuń widok", + "Delete all objects in this schema": "Usuń wszystkie Obiekty w tym Schemacie", + "Delete permanently": "Usuń trwale", + "Delete this conversation?": "Usunąć tę rozmowę?", + "Delete view": "Usuń widok", + "Delete {count} object": "Usuń {count} Obiekt", + "Delete {count} objects": "Usuń {count} Obiektów", + "Deleted": "Usunięto", + "Deleted By": "Usunięte przez", + "Deleted Date": "Data usunięcia", + "Deleted Items Management": "Zarządzanie usuniętymi elementami", + "Deleted This Week": "Usunięte w tym tygodniu", + "Deleted Today": "Usunięte dzisiaj", + "Deleted:": "Usunięto:", + "Deleting SOLR Collection...": "Usuwanie kolekcji SOLR...", + "Deleting...": "Usuwanie...", + "Deletion Date Range": "Zakres dat usunięcia", + "Deletion Statistics": "Statystyki usuwania", + "Deprecated": "Przestarzałe", + "Depublish": "Depublish", + "Description": "Opis", + "Description Template": "Szablon opisu", + "Description shown as tooltip": "Opis wyświetlany jako podpowiedź", + "Description:": "Opis:", + "Detect data types and patterns": "Wykryj typy danych i wzorce", + "Detected At": "Wykryto o", + "Detected Format:": "Wykryty format:", + "Detected Issues:": "Wykryte problemy:", + "Detected: {date}": "Wykryto: {date}", + "Direct permission": "Uprawnienie bezpośrednie", + "Disable": "Wyłącz", + "Disable this to make the agent": "Wyłącz tę opcję, aby agent", + "Disabled": "Wyłączone", + "Discover Files": "Wykryj Pliki", + "Discovered Properties": "Wykryte Właściwości", + "Display Format": "Format wyświetlania", + "Display Name": "Nazwa wyświetlana", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Czy chcą Państwo trwale usunąć wszystkie przefiltrowane wpisy ścieżki audytu? Tej operacji nie można cofnąć.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Czy chcą Państwo trwale usunąć ten wpis ścieżki audytu? Tej operacji nie można cofnąć.", + "Documents": "Dokumenty", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR włączony", + "Download API Spec": "Pobierz specyfikację API", + "Download API Specification": "Pobierz specyfikację API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Pobierz kopię wszystkich swoich danych osobowych przechowywanych w OpenRegister (GDPR Artykuł 20).", + "Drag a file or files here": "Przeciągnij Plik lub Pliki tutaj", + "Dry run completed: {count} fields would be created": "Próbne uruchomienie zakończone: utworzono by {count} pól", + "Dry run completed: {count} fields would be fixed": "Próbne uruchomienie zakończone: naprawiono by {count} pól", + "Duration in seconds (optional)": "Czas trwania w sekundach (opcjonalnie)", + "Edit": "Edytuj", + "Edit Agent": "Edytuj agenta", + "Edit Application": "Edytuj aplikację", + "Edit Configuration": "Edytuj konfigurację", + "Edit Endpoint": "Edytuj punkt końcowy", + "Edit Organisation": "Edytuj organizację", + "Edit Property '{key}' of '{title}'": "Edytuj Właściwość „{key}” Obiektu „{title}”", + "Edit Register": "Edytuj Rejestr", + "Edit Schema": "Edytuj Schemat", + "Edit Source": "Edytuj Źródło", + "Edit View": "Edytuj widok", + "Edit Webhook": "Edytuj webhook", + "Edit labels": "Edytuj etykiety", + "Edit processing activity": "Edytuj verwerkingsactiviteit", + "Edit tags for {name}": "Edytuj tagi dla {name}", + "Edit verwerkingsactiviteit": "Edytuj verwerkingsactiviteit", + "Edit view": "Edytuj widok", + "Edit view details": "Edytuj szczegóły widoku", + "Effectiveness": "Skuteczność", + "Email": "Email", + "Email Addresses": "Adresy email", + "Email digest frequency": "Częstotliwość podsumowań email", + "Emails": "Wiadomości email", + "Embedding Model": "Model osadzania", + "Embedding Provider": "Dostawca osadzania", + "Embedding generated successfully": "Osadzanie zostało wygenerowane pomyślnie", + "Embedding provider connection successful!": "Połączenie z dostawcą osadzania powiodło się!", + "Embedding test failed: {error}": "Test osadzania nie powiódł się: {error}", + "Enable": "Włącz", + "Enable RAG": "Włącz RAG", + "Enable auto-creation": "Włącz automatyczne tworzenie", + "Enable automatic file vectorization": "Włącz automatyczną wektoryzację Plików", + "Enable automatic object vectorization": "Włącz automatyczną wektoryzację Obiektów", + "Enable automatic synchronization": "Włącz automatyczną synchronizację", + "Enable calendar provider": "Włącz dostawcę kalendarza", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Włącz szczegółowe rejestrowanie operacji SOLR (zalecane do debugowania)", + "Enable faceting": "Włącz facetowanie", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Włącz lub wyłącz funkcje LLM. Skonfiguruj dostawców i modele za pomocą przycisku Konfiguracja LLM powyżej.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Włącz lub wyłącz funkcje LLM. Skonfiguruj dostawców i modele za pomocą przycisku konfiguracji LLM powyżej.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Włącz lub wyłącz integrację wyszukiwania SOLR. Skonfiguruj ustawienia połączenia za pomocą przycisku Ustawienia połączenia powyżej.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Włącz lub wyłącz integrację wyszukiwania SOLR. Skonfiguruj ustawienia połączenia za pomocą przycisku ustawień połączenia powyżej.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Włącz lub wyłącz integrację przepływów pracy n8n. Skonfiguruj ustawienia połączenia poniżej.", + "Enable or disable this webhook": "Włącz lub wyłącz ten webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Włącz narzędzia umożliwiające agentowi interakcję z danymi poprzez wywoływanie funkcji.", + "Enable vectorization for all existing and future views (may increase costs)": "Włącz wektoryzację dla wszystkich istniejących i przyszłych widoków (może zwiększyć koszty)", + "Enable write-back to target objects": "Włącz zapis zwrotny do docelowych Obiektów", + "Enabled": "Włączone", + "End Date Field": "Pole daty zakończenia", + "Endpoint Path*": "Ścieżka punktu końcowego*", + "Endpoints": "Punkty końcowe", + "Engine not found": "Nie znaleziono silnika", + "Enter ConfigSet name": "Wprowadź nazwę ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Wprowadź nazwy użytkowników Nextcloud, aby przyznać dostęp do tego prywatnego agenta", + "Enter agent description (optional)": "Wprowadź opis agenta (opcjonalnie)", + "Enter agent name": "Wprowadź nazwę agenta", + "Enter application description (optional)": "Wprowadź opis aplikacji (opcjonalnie)", + "Enter application name": "Wprowadź nazwę aplikacji", + "Enter collection name": "Wprowadź nazwę kolekcji", + "Enter configuration description (optional)": "Wprowadź opis konfiguracji (opcjonalnie)", + "Enter configuration title": "Wprowadź tytuł konfiguracji", + "Enter description (optional)...": "Wprowadź opis (opcjonalnie)...", + "Enter new collection name": "Wprowadź nową nazwę kolekcji", + "Enter object ID": "Wprowadź identyfikator Obiektu", + "Enter organisation description (optional)": "Wprowadź opis organizacji (opcjonalnie)", + "Enter organisation name": "Wprowadź nazwę organizacji", + "Enter search term": "Wprowadź wyszukiwany termin", + "Enter search terms or leave empty to browse all": "Wprowadź wyszukiwane terminy lub pozostaw puste, aby przeglądać wszystkie", + "Enter system prompt for the agent": "Wprowadź monit systemowy dla agenta", + "Enter username and press Enter": "Wprowadź nazwę użytkownika i naciśnij Enter", + "Enter view name...": "Wprowadź nazwę widoku...", + "Enter webhook name": "Wprowadź nazwę webhooka", + "Enter your API key": "Wprowadź swój klucz API", + "Enter your object here...": "Wprowadź swój Obiekt tutaj...", + "Enter your schema here...": "Wprowadź swój Schemat tutaj...", + "Entities": "Encje", + "Entities detected": "Wykryto encje", + "Entity ID: {id}": "Identyfikator encji: {id}", + "Entity Information": "Informacje o encji", + "Entity Recognition": "Rozpoznawanie encji", + "Entity Recognition Method": "Metoda rozpoznawania encji", + "Entity deleted successfully": "Encja została usunięta pomyślnie", + "Entity not found": "Nie znaleziono encji", + "Entries to be deleted:": "Wpisy do usunięcia:", + "Enum Issue": "Problem z Enum", + "Enum constraint is missing": "Brak ograniczenia Enum", + "Erase (Art 17)": "Usuń (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Usunąć {count} Obiekt(ów) dla tego podmiotu? Ta operacja jest rejestrowana w ścieżce audytu.", + "Erasure complete": "Usuwanie zakończone", + "Erasure preview": "Podgląd usuwania", + "Error": "Błąd", + "Error Details": "Szczegóły błędu", + "Error Information": "Informacje o błędzie", + "Error Message": "Komunikat o błędzie", + "Error deleting audit trails: {error}": "Błąd podczas usuwania ścieżek audytu: {error}", + "Error deleting search trails: {error}": "Błąd podczas usuwania ścieżek wyszukiwania: {error}", + "Error loading application": "Błąd podczas ładowania aplikacji", + "Error loading audit trails": "Błąd podczas ładowania ścieżek audytu", + "Error loading entity": "Błąd podczas ładowania encji", + "Error loading search trails": "Błąd podczas ładowania ścieżek wyszukiwania", + "Estimated Batches:": "Szacowane partie:", + "Estimated Cost:": "Szacowany koszt:", + "Estimated Duration:": "Szacowany czas trwania:", + "Event": "Wydarzenie", + "Event Property for Payload": "Właściwość zdarzenia dla ładunku", + "Events": "Wydarzenia", + "Every schema with detected PII has a processing-activity annotation.": "Każdy Schemat z wykrytymi danymi PII ma adnotację verwerkingsactiviteit.", + "Example": "Przykład", + "Example Value": "Przykładowa wartość", + "Example value for this property": "Przykładowa wartość dla tej Właściwości", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Maksimum wyłączne", + "Exclusive Minimum": "Minimum wyłączne", + "Exclusive maximum": "Maksimum wyłączne", + "Exclusive minimum": "Minimum wyłączne", + "Execution Mode": "Tryb wykonywania", + "Execution Time": "Czas wykonywania", + "Execution Time Range": "Zakres czasu wykonywania", + "Existing Improvements": "Istniejące ulepszenia", + "Expand details": "Rozwiń szczegóły", + "Expiration": "Wygaśnięcie", + "Expires": "Wygasa", + "Expires in (e.g., 90d)": "Wygasa za (np. 90d)", + "Exponential": "Wykładnicze", + "Export": "Eksportuj", + "Export \"{schema}\" objects from \"{register}\"": "Eksportuj Obiekty „{schema}” z „{register}”", + "Export Configuration": "Eksportuj konfigurację", + "Export Format:": "Format eksportu:", + "Export Objects": "Eksportuj Obiekty", + "Export completed successfully": "Eksport zakończony pomyślnie", + "Export downloaded successfully": "Eksport pobrany pomyślnie", + "Export failed: {error}": "Eksport nie powiódł się: {error}", + "Export is rate limited. Please try again later.": "Eksport jest ograniczony liczbą żądań. Proszę spróbować ponownie później.", + "Export my data": "Eksportuj moje dane", + "Export, view, or delete audit trails": "Eksportuj, przeglądaj lub usuwaj ścieżki audytu", + "Exporting...": "Eksportowanie...", + "Extend Schema": "Rozszerz Schemat", + "Extended": "Rozszerzone", + "Extensions": "Rozszerzenia", + "Extra Query Parameters": "Dodatkowe parametry zapytania", + "Extract Now": "Wyodrębnij teraz", + "Extract Pending Files": "Wyodrębnij oczekujące Pliki", + "Extract now": "Wyodrębnij teraz", + "Extract properties from each object": "Wyodrębnij Właściwości z każdego Obiektu", + "Extracted At": "Wyodrębniono o", + "Extracted at": "Wyodrębniono o", + "Extraction": "Wyodrębnianie", + "Extraction Mode": "Tryb wyodrębniania", + "Extraction Scope": "Zakres wyodrębniania", + "Extraction Status": "Status wyodrębniania", + "Extraction error for {file}: {error}": "Błąd wyodrębniania dla {file}: {error}", + "Facet Description": "Opis facetu", + "Facet Order": "Kolejność facetów", + "Facet Title": "Tytuł facetu", + "Facet Type": "Typ facetu", + "Facet configuration updated successfully": "Konfiguracja facetu została zaktualizowana pomyślnie", + "Facetable": "Możliwy do facetowania", + "Faceting Configuration:": "Konfiguracja facetowania:", + "Facets discovered and configured successfully": "Facety zostały wykryte i skonfigurowane pomyślnie", + "Facets discovered successfully": "Facety zostały wykryte pomyślnie", + "Failed": "Niepowodzenie", + "Failed to add attachment to object": "Nie udało się dodać załącznika do Obiektu", + "Failed to add user to organisation": "Nie udało się dodać użytkownika do organizacji", + "Failed to analyze schema properties": "Nie udało się przeanalizować Właściwości Schematu", + "Failed to archive conversation": "Nie udało się zarchiwizować rozmowy", + "Failed to calculate sizes": "Nie udało się obliczyć rozmiarów", + "Failed to cancel deactivation": "Nie udało się anulować dezaktywacji", + "Failed to change active organisation: {error}": "Nie udało się zmienić aktywnej organizacji: {error}", + "Failed to change password": "Nie udało się zmienić hasła", + "Failed to check version: {error}": "Nie udało się sprawdzić wersji: {error}", + "Failed to clear audit trails: {error}": "Nie udało się wyczyścić ścieżek audytu: {error}", + "Failed to clear blob objects: {error}": "Nie udało się wyczyścić Obiektów blob: {error}", + "Failed to clear cache: {error}": "Nie udało się wyczyścić pamięci podręcznej: {error}", + "Failed to clear collection": "Nie udało się wyczyścić kolekcji", + "Failed to clear conversation": "Nie udało się wyczyścić rozmowy", + "Failed to clear embeddings: {error}": "Nie udało się wyczyścić osadzeń: {error}", + "Failed to clear search trails: {error}": "Nie udało się wyczyścić ścieżek wyszukiwania: {error}", + "Failed to clear {type} cache: {error}": "Nie udało się wyczyścić pamięci podręcznej {type}: {error}", + "Failed to copy changes": "Nie udało się skopiować zmian", + "Failed to copy data": "Nie udało się skopiować danych", + "Failed to copy data to clipboard": "Nie udało się skopiować danych do schowka", + "Failed to copy to clipboard": "Nie udało się skopiować do schowka", + "Failed to copy token": "Nie udało się skopiować tokena", + "Failed to create conversation": "Nie udało się utworzyć rozmowy", + "Failed to create missing SOLR fields: {error}": "Nie udało się utworzyć brakujących pól SOLR: {error}", + "Failed to create or find project": "Nie udało się utworzyć ani znaleźć projektu", + "Failed to create token": "Nie udało się utworzyć tokena", + "Failed to delete SOLR collection: {error}": "Nie udało się usunąć kolekcji SOLR: {error}", + "Failed to delete collection": "Nie udało się usunąć kolekcji", + "Failed to delete conversation": "Nie udało się usunąć rozmowy", + "Failed to delete objects for {schema}: {error}": "Nie udało się usunąć Obiektów dla {schema}: {error}", + "Failed to delete webhook": "Nie udało się usunąć webhooka", + "Failed to depublish register: {error}": "Nie udało się wykonać depublish Rejestru: {error}", + "Failed to depublish schema: {error}": "Nie udało się wykonać depublish Schematu: {error}", + "Failed to depublish: {error}": "Nie udało się wykonać depublish: {error}", + "Failed to discover files: {error}": "Nie udało się wykryć Plików: {error}", + "Failed to download API specification": "Nie udało się pobrać specyfikacji API", + "Failed to export data": "Nie udało się wyeksportować danych", + "Failed to export objects": "Nie udało się wyeksportować Obiektów", + "Failed to fetch conversation history": "Nie udało się pobrać historii rozmów", + "Failed to fix mismatched SOLR fields: {error}": "Nie udało się naprawić niezgodnych pól SOLR: {error}", + "Failed to get SOLR field configuration": "Nie udało się pobrać konfiguracji pól SOLR", + "Failed to get chat statistics": "Nie udało się pobrać statystyk czatu", + "Failed to get database information: %s": "Nie udało się pobrać informacji o bazie danych: %s", + "Failed to get entity categories": "Nie udało się pobrać kategorii encji", + "Failed to get entity statistics": "Nie udało się pobrać statystyk encji", + "Failed to get entity types": "Nie udało się pobrać typów encji", + "Failed to get response: {error}": "Nie udało się uzyskać odpowiedzi: {error}", + "Failed to invalidate app store cache: {error}": "Nie udało się unieważnić pamięci podręcznej sklepu z aplikacjami: {error}", + "Failed to leave organisation: {error}": "Nie udało się opuścić organizacji: {error}", + "Failed to link object": "Nie udało się połączyć Obiektu", + "Failed to load Deck cards": "Nie udało się załadować kart Deck", + "Failed to load LLM configuration": "Nie udało się załadować konfiguracji LLM", + "Failed to load Nextcloud groups": "Nie udało się załadować grup Nextcloud", + "Failed to load cache statistics: {error}": "Nie udało się załadować statystyk pamięci podręcznej: {error}", + "Failed to load conversation": "Nie udało się załadować rozmowy", + "Failed to load entities": "Nie udało się załadować encji", + "Failed to load entity": "Nie udało się załadować encji", + "Failed to load extraction data": "Nie udało się załadować danych wyodrębniania", + "Failed to load files": "Nie udało się załadować Plików", + "Failed to load linked contacts": "Nie udało się załadować połączonych kontaktów", + "Failed to load linked emails": "Nie udało się załadować połączonych wiadomości email", + "Failed to load linked events": "Nie udało się załadować połączonych wydarzeń", + "Failed to load organisations": "Nie udało się załadować organizacji", + "Failed to load register data": "Nie udało się załadować danych Rejestru", + "Failed to load related entities": "Nie udało się załadować powiązanych encji", + "Failed to load settings: {error}": "Nie udało się załadować ustawień: {error}", + "Failed to load statistics: {error}": "Nie udało się załadować statystyk: {error}", + "Failed to load templates": "Nie udało się załadować szablonów", + "Failed to load view: {error}": "Nie udało się załadować widoku: {error}", + "Failed to load webhooks": "Nie udało się załadować webhooków", + "Failed to load workflows": "Nie udało się załadować przepływów pracy", + "Failed to process message": "Nie udało się przetworzyć wiadomości", + "Failed to publish register: {error}": "Nie udało się opublikować Rejestru: {error}", + "Failed to publish schema: {error}": "Nie udało się opublikować Schematu: {error}", + "Failed to publish: {error}": "Nie udało się opublikować: {error}", + "Failed to refresh database information": "Nie udało się odświeżyć informacji o bazie danych", + "Failed to reindex collection": "Nie udało się ponownie zindeksować kolekcji", + "Failed to remove avatar": "Nie udało się usunąć awatara", + "Failed to remove link": "Nie udało się usunąć połączenia", + "Failed to remove member: {error}": "Nie udało się usunąć członka: {error}", + "Failed to remove schema {schema}: {error}": "Nie udało się usunąć Schematu {schema}: {error}", + "Failed to remove user from organisation": "Nie udało się usunąć użytkownika z organizacji", + "Failed to rename conversation": "Nie udało się zmienić nazwy rozmowy", + "Failed to request deactivation": "Nie udało się zażądać dezaktywacji", + "Failed to restore conversation": "Nie udało się przywrócić rozmowy", + "Failed to retrieve extraction status.": "Nie udało się pobrać statusu wyodrębniania.", + "Failed to retrieve objects for file.": "Nie udało się pobrać Obiektów dla Pliku.", + "Failed to retry extraction": "Nie udało się ponowić wyodrębniania", + "Failed to retry webhook": "Nie udało się ponowić webhooka", + "Failed to revoke token": "Nie udało się unieważnić tokena", + "Failed to save GitHub token": "Nie udało się zapisać tokena GitHub", + "Failed to save GitLab URL": "Nie udało się zapisać adresu URL GitLab", + "Failed to save GitLab token": "Nie udało się zapisać tokena GitLab", + "Failed to save LLM settings: {error}": "Nie udało się zapisać ustawień LLM: {error}", + "Failed to save additional feedback": "Nie udało się zapisać dodatkowej opinii", + "Failed to save configuration.": "Nie udało się zapisać konfiguracji.", + "Failed to save configuration: {error}": "Nie udało się zapisać konfiguracji: {error}", + "Failed to save facet configuration": "Nie udało się zapisać konfiguracji facetu", + "Failed to save feedback": "Nie udało się zapisać opinii", + "Failed to save file settings: {error}": "Nie udało się zapisać ustawień Plików: {error}", + "Failed to save n8n configuration": "Nie udało się zapisać konfiguracji n8n", + "Failed to save preferences": "Nie udało się zapisać preferencji", + "Failed to save roles": "Nie udało się zapisać ról", + "Failed to save settings": "Nie udało się zapisać ustawień", + "Failed to save settings: {error}": "Nie udało się zapisać ustawień: {error}", + "Failed to save view: {error}": "Nie udało się zapisać widoku: {error}", + "Failed to save warmup interval: {error}": "Nie udało się zapisać interwału rozgrzewki: {error}", + "Failed to save webhook": "Nie udało się zapisać webhooka", + "Failed to send feedback": "Nie udało się wysłać opinii", + "Failed to set active organisation: {error}": "Nie udało się ustawić aktywnej organizacji: {error}", + "Failed to setup SOLR: {error}": "Nie udało się skonfigurować SOLR: {error}", + "Failed to start file warmup: {error}": "Nie udało się rozpocząć rozgrzewania plików: {error}", + "Failed to start processing {type} files: {error}": "Nie udało się rozpocząć przetwarzania plików {type}: {error}", + "Failed to start vectorization: {error}": "Nie udało się rozpocząć wektoryzacji: {error}", + "Failed to switch organisation: {error}": "Nie udało się przełączyć organizacji: {error}", + "Failed to sync magic table for {schema}: {error}": "Nie udało się zsynchronizować tabeli magic dla {schema}: {error}", + "Failed to test SOLR connection: {error}": "Nie udało się przetestować połączenia z SOLR: {error}", + "Failed to test webhook": "Nie udało się przetestować webhooka", + "Failed to update LLM settings: {error}": "Nie udało się zaktualizować ustawień LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Nie udało się zaktualizować ustawień Multitenancy: {error}", + "Failed to update RBAC settings: {error}": "Nie udało się zaktualizować ustawień RBAC: {error}", + "Failed to update Retention settings: {error}": "Nie udało się zaktualizować ustawień przechowywania: {error}", + "Failed to update SOLR settings: {error}": "Nie udało się zaktualizować ustawień SOLR: {error}", + "Failed to update favorite status": "Nie udało się zaktualizować statusu ulubionego", + "Failed to update schema configuration for {schema}: {error}": "Nie udało się zaktualizować konfiguracji schematu dla {schema}: {error}", + "Failed to update schema properties": "Nie udało się zaktualizować właściwości schematu", + "Failed to update view": "Nie udało się zaktualizować widoku", + "Failed to update view: {error}": "Nie udało się zaktualizować widoku: {error}", + "Failed to update webhook": "Nie udało się zaktualizować webhooka", + "Failed to upload avatar": "Nie udało się przesłać awatara", + "Failed to validate {schema}: {error}": "Nie udało się zwalidować {schema}: {error}", + "Failed to vectorize files: {error}": "Nie udało się zwektoryzować plików: {error}", + "Failed to warmup names cache: {error}": "Nie udało się rozgrzać pamięci podręcznej nazw: {error}", + "Federation": "Federacja", + "Feedback recorded": "Opinia zapisana", + "Fewer sources (1-3):": "Mniej źródeł (1-3):", + "Field": "Pole", + "Fields to Display": "Pola do wyświetlenia", + "File": "Plik", + "File #{id}": "Plik #{id}", + "File Chunk Prediction": "Predykcja fragmentów pliku", + "File Collection": "Kolekcja plików", + "File Handling": "Obsługa plików", + "File Location": "Lokalizacja pliku", + "File Management": "Zarządzanie plikami", + "File Name": "Nazwa pliku", + "File Path": "Ścieżka pliku", + "File Type Selection": "Wybór typu pliku", + "File Types to Process": "Typy plików do przetworzenia", + "File Vectorization": "Wektoryzacja plików", + "File Warmup": "Rozgrzewanie plików", + "File actions menu": "Menu akcji pliku", + "File anonymized successfully": "Plik został pomyślnie zanonimizowany", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Wektoryzacja fragmentów pliku nie jest jeszcze zaimplementowana. Fragmenty są gotowe i przechowywane, ale usługa wektoryzacji jest w fazie rozwoju.", + "File collection not configured": "Kolekcja plików nie została skonfigurowana", + "File discovery completed": "Wykrywanie plików zakończone", + "File extraction completed": "Ekstrakcja pliku zakończona", + "File extraction queued": "Ekstrakcja pliku w kolejce", + "File indexed successfully": "Plik został pomyślnie zindeksowany", + "File is already anonymized": "Plik jest już zanonimizowany", + "File name": "Nazwa pliku", + "File not found": "Nie znaleziono pliku", + "File settings saved successfully": "Ustawienia plików zostały pomyślnie zapisane", + "File settings updated successfully": "Ustawienia plików zostały pomyślnie zaktualizowane", + "File sources": "Źródła plików", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Wektoryzacja plików zakończona! Zwektoryzowano {vectorized} fragmentów z {files} plików. {failed} nie powiodło się.", + "File vectorization configuration saved successfully": "Konfiguracja wektoryzacji plików została pomyślnie zapisana", + "File vectorization started. Check the statistics section for progress.": "Wektoryzacja plików rozpoczęta. Postęp można sprawdzić w sekcji statystyk.", + "File warmup completed": "Rozgrzewanie plików zakończone", + "Files": "Pliki", + "Files added successfully": "Pliki zostały pomyślnie dodane", + "Files with Completed Extraction:": "Pliki z zakończoną ekstrakcją:", + "Files → fileCollection, Objects → objectCollection": "Pliki → fileCollection, Obiekty → objectCollection", + "Files → filecollection, objects → objectcollection": "Pliki → filecollection, obiekty → objectcollection", + "Filter Audit Trails": "Filtruj ślady audytu", + "Filter Deleted Items": "Filtruj usunięte elementy", + "Filter Objects": "Filtruj obiekty", + "Filter Properties": "Filtruj właściwości", + "Filter Search Trails": "Filtruj ślady wyszukiwania", + "Filter Statistics": "Filtruj statystyki", + "Filter and analyze search trail entries": "Filtruj i analizuj wpisy śladów wyszukiwania", + "Filter and manage audit trail entries": "Filtruj i zarządzaj wpisami śladów audytu", + "Filter and manage soft deleted items": "Filtruj i zarządzaj elementami usuniętymi nietrwale", + "Filter and search entities": "Filtruj i wyszukuj jednostki", + "Filter by object ID": "Filtruj według identyfikatora obiektu", + "Filter by search term": "Filtruj według terminu wyszukiwania", + "Filter by type": "Filtruj według typu", + "Filter by webhook": "Filtruj według webhooka", + "Filter data loaded automatically. Use the filters below to refine your search.": "Dane filtrów załadowano automatycznie. Należy użyć poniższych filtrów, aby zawęzić wyszukiwanie.", + "Filter fields...": "Filtruj pola...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtruj wyzwalacze webhooka według właściwości ładunku (jeden w wierszu, format: klucz: wartość)", + "Filtered": "Przefiltrowano", + "Filters": "Filtry", + "Finalizing setup...": "Finalizowanie konfiguracji...", + "Fireworks AI Chat Settings": "Ustawienia czatu Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfiguracja osadzeń Fireworks AI", + "First": "Pierwszy", + "Fixed": "Naprawiono", + "For chat and retrieval-augmented generation": "Do czatu i generowania wspomaganego wyszukiwaniem", + "For vector embeddings and semantic search": "Do osadzeń wektorowych i wyszukiwania semantycznego", + "Format": "Format", + "Format Issue": "Problem z formatem", + "Format constraint is missing": "Brakuje ograniczenia formatu", + "From (e.g. -7 days or 2025-01-01)": "Od (np. -7 dni lub 2025-01-01)", + "From Date": "Data od", + "From date": "Data od", + "Full data copied to clipboard": "Pełne dane skopiowano do schowka", + "Function Tools": "Narzędzia funkcji", + "General Issue": "Ogólny problem", + "Generate recommendations and confidence scores": "Generuj rekomendacje i wyniki pewności", + "Generate report": "Generuj raport", + "Generate the verantwoordingsdocument": "Generuj verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generuj wektory natychmiast po utworzeniu nowych obiektów", + "Generated: {time}": "Wygenerowano: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Uzyskaj wgląd w czasie rzeczywistym w kondycję danych swojej organizacji, koncentrując się na rejestrach, definicjach schematów oraz przechowywaniu i wykorzystaniu obiektów.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Uzyskaj wgląd w czasie rzeczywistym w kondycję danych swojej organizacji, trendy wzrostu i wskaźniki jakości.", + "GitHub Personal Access Token": "Osobisty token dostępu GitHub", + "GitHub token is valid": "Token GitHub jest prawidłowy", + "GitHub token is valid! Username: {username}": "Token GitHub jest prawidłowy! Nazwa użytkownika: {username}", + "GitHub token removed successfully": "Token GitHub został pomyślnie usunięty", + "GitHub token saved successfully": "Token GitHub został pomyślnie zapisany", + "GitHub token test failed: {message}": "Test tokena GitHub nie powiódł się: {message}", + "GitLab Personal Access Token": "Osobisty token dostępu GitLab", + "GitLab URL saved successfully": "Adres URL GitLab został pomyślnie zapisany", + "GitLab token is valid": "Token GitLab jest prawidłowy", + "GitLab token is valid! Username: {username}": "Token GitLab jest prawidłowy! Nazwa użytkownika: {username}", + "GitLab token saved successfully": "Token GitLab został pomyślnie zapisany", + "GitLab token test failed: {message}": "Test tokena GitLab nie powiódł się: {message}", + "Go to linked Register": "Przejdź do powiązanego Rejestru", + "Go to linked Schema": "Przejdź do powiązanego Schematu", + "Go to organisation": "Przejdź do organizacji", + "Grant additional permissions beyond standard CRUD operations": "Przyznaj dodatkowe uprawnienia wykraczające poza standardowe operacje CRUD", + "Group Quota": "Limit grupy", + "Groups": "Grupy", + "HTML preview": "Podgląd HTML", + "HTTP Method": "Metoda HTTP", + "HTTP method used to send webhook requests": "Metoda HTTP używana do wysyłania żądań webhooka", + "Headers": "Nagłówki", + "Health": "Kondycja", + "Heartbeat successful - connection kept alive": "Sygnał kontrolny pomyślny - połączenie utrzymane", + "Helpful": "Pomocne", + "Here you can configure the details for various connections.": "Tutaj można skonfigurować szczegóły różnych połączeń.", + "Here you can configure the details for your organisation.": "Tutaj można skonfigurować szczegóły swojej organizacji.", + "Hide": "Ukryj", + "Hide Filters": "Ukryj filtry", + "Hide in collection view": "Ukryj w widoku kolekcji", + "Hide in forms": "Ukryj w formularzach", + "Hide in list view": "Ukryj w widoku listy", + "High": "Wysoki", + "High Confidence": "Wysoka pewność", + "Host": "Host", + "Hourly": "Co godzinę", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Jak głęboko przeszukiwać zagnieżdżone właściwości obiektu (1-20). Wyższe wartości obejmują więcej szczegółów, ale zwiększają rozmiar wektora.", + "How often to check for updates (1-168 hours)": "Jak często sprawdzać aktualizacje (1-168 godzin)", + "How to handle retries for failed webhook deliveries": "Jak obsługiwać ponowne próby nieudanych dostaw webhooka", + "IBANs": "Numery IBAN", + "ID": "Identyfikator", + "ID of input mapping (optional)": "Identyfikator mapowania wejściowego (opcjonalnie)", + "ID of output mapping (optional)": "Identyfikator mapowania wyjściowego (opcjonalnie)", + "ID of the target resource": "Identyfikator zasobu docelowego", + "ID:": "Identyfikator:", + "ID: {id}": "Identyfikator: {id}", + "IP rate limits cleared successfully": "Limity szybkości IP zostały pomyślnie wyczyszczone", + "Identify properties not in the schema": "Zidentyfikuj właściwości spoza schematu", + "Immutable": "Niezmienny", + "Import": "Importuj", + "Import Configuration": "Importuj konfigurację", + "Import Data into Register": "Importuj dane do Rejestru", + "Import successful": "Import zakończony pomyślnie", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Zaimportuj szablon report-bundle.json, aby uzyskać rejestr `reports`, a następnie utwórz swój pierwszy pulpit za pomocą standardowego interfejsu obiektów. Pulpity deklarują swoje widżety w formacie JSON, a renderer dostarcza każdemu widżetowi dane agregacji na żywo.", + "Improved Property": "Ulepszona właściwość", + "In use": "W użyciu", + "Inactive": "Nieaktywny", + "Inactive organisations cannot be used": "Nieaktywne organizacje nie mogą być używane", + "Include IDs and names of related objects for better contextual search": "Uwzględnij identyfikatory i nazwy powiązanych obiektów dla lepszego wyszukiwania kontekstowego", + "Include ids and names of related objects for better contextual search": "Uwzględnij identyfikatory i nazwy powiązanych obiektów dla lepszego wyszukiwania kontekstowego", + "Include related object references": "Uwzględnij odniesienia do powiązanych obiektów", + "Include schema and register metadata": "Uwzględnij metadane schematu i rejestru", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Informacje o bieżącej instalacji OpenRegister", + "Inherited from register": "Odziedziczone z rejestru", + "Inherits permissions from register": "Dziedziczy uprawnienia z rejestru", + "Initialization failed": "Inicjalizacja nie powiodła się", + "Initialize Project": "Zainicjuj projekt", + "Initializing SOLR setup...": "Inicjowanie konfiguracji SOLR...", + "Input Mapping": "Mapowanie wejściowe", + "Inspect Fields": "Sprawdź pola", + "Inspect Index": "Sprawdź indeks", + "Inspect SOLR Index": "Sprawdź indeks SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Zainstaluj aplikację notify_push ze sklepu Nextcloud App Store, aby włączyć aktualizacje w czasie rzeczywistym.", + "Integrations": "Integracje", + "Internet location (URL) of this installation": "Lokalizacja internetowa (URL) tej instalacji", + "Interval": "Interwał", + "Invalid": "Nieprawidłowy", + "Invalid batch size. Must be between 1 and 5000": "Nieprawidłowy rozmiar partii. Musi mieścić się w zakresie od 1 do 5000", + "Invalid feedback type": "Nieprawidłowy typ opinii", + "Invalid field name provided": "Podano nieprawidłową nazwę pola", + "Invalid maxObjects. Must be 0 (all) or positive number": "Nieprawidłowa wartość maxObjects. Musi wynosić 0 (wszystkie) lub być liczbą dodatnią", + "Invalid:": "Nieprawidłowy:", + "Invite Users": "Zaproś użytkowników", + "Invited Users": "Zaproszeni użytkownicy", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Wyniki Inzage", + "Issue": "Problem", + "Items per page:": "Elementów na stronę:", + "Just now": "Przed chwilą", + "Key": "Klucz", + "LLM Configuration": "Konfiguracja LLM", + "LLM actions menu": "Menu akcji LLM", + "LLM configuration saved successfully": "Konfiguracja LLM została pomyślnie zapisana", + "LLM features disabled": "Funkcje LLM wyłączone", + "LLM features enabled": "Funkcje LLM włączone", + "LLM must be enabled with an embedding provider configured": "LLM musi być włączony ze skonfigurowanym dostawcą osadzeń", + "LLM settings saved successfully": "Ustawienia LLM zostały pomyślnie zapisane", + "LLM settings updated successfully": "Ustawienia LLM zostały pomyślnie zaktualizowane", + "Label": "Etykieta", + "Labels": "Etykiety", + "Languages": "Języki", + "Last": "Ostatni", + "Last Triggered": "Ostatnio wyzwolono", + "Leave empty to allow all users access": "Pozostaw puste, aby umożliwić dostęp wszystkim użytkownikom", + "Leave off for auto-detection from property format.": "Pozostaw wyłączone, aby automatycznie wykrywać z formatu właściwości.", + "Left organisation successfully": "Pomyślnie opuszczono organizację", + "Legal basis *": "Podstawa prawna *", + "Length Range:": "Zakres długości:", + "Linear": "Liniowy", + "Link": "Łącze", + "Link an email from the Mail app sidebar to associate it with this object.": "Połącz wiadomość e-mail z panelu bocznego aplikacji Mail, aby powiązać ją z tym obiektem.", + "Link an object to see it here.": "Połącz obiekt, aby zobaczyć go tutaj.", + "Link another object": "Połącz inny obiekt", + "Link existing event": "Połącz istniejące wydarzenie", + "Link removed": "Łącze usunięte", + "Link to Object": "Łącze do obiektu", + "Link to {name}": "Łącze do {name}", + "Linked Objects": "Powiązane obiekty", + "Linked by {user}": "Powiązane przez {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Powiązane wiadomości e-mail, wydarzenia w kalendarzu, kontakty i karty Deck pojawią się tutaj.", + "Linked to {name}": "Powiązane z {name}", + "Load Advanced Filters": "Załaduj filtry zaawansowane", + "Load advanced filters with live data from your search index": "Załaduj filtry zaawansowane z danymi na żywo z indeksu wyszukiwania", + "Load more": "Załaduj więcej", + "Load view": "Załaduj widok", + "Loading API tokens...": "Ładowanie tokenów API...", + "Loading ConfigSets...": "Ładowanie ConfigSets...", + "Loading LLM configuration...": "Ładowanie konfiguracji LLM...", + "Loading RBAC settings...": "Ładowanie ustawień RBAC...", + "Loading activity...": "Ładowanie aktywności...", + "Loading advanced filters...": "Ładowanie filtrów zaawansowanych...", + "Loading agents...": "Ładowanie agentów...", + "Loading application details...": "Ładowanie szczegółów aplikacji...", + "Loading applications...": "Ładowanie aplikacji...", + "Loading archived conversations...": "Ładowanie zarchiwizowanych konwersacji...", + "Loading audit trails...": "Ładowanie śladów audytu...", + "Loading available tools...": "Ładowanie dostępnych narzędzi...", + "Loading cache statistics...": "Ładowanie statystyk pamięci podręcznej...", + "Loading collections...": "Ładowanie kolekcji...", + "Loading configurations...": "Ładowanie konfiguracji...", + "Loading conversation...": "Ładowanie konwersacji...", + "Loading conversations...": "Ładowanie konwersacji...", + "Loading deleted items...": "Ładowanie usuniętych elementów...", + "Loading endpoints": "Ładowanie punktów końcowych", + "Loading entities...": "Ładowanie jednostek...", + "Loading events...": "Ładowanie wydarzeń...", + "Loading filter data...": "Ładowanie danych filtrów...", + "Loading groups...": "Ładowanie grup...", + "Loading linked objects...": "Ładowanie powiązanych obiektów...", + "Loading log details...": "Ładowanie szczegółów dziennika...", + "Loading multitenancy settings...": "Ładowanie ustawień multitenancy...", + "Loading n8n configuration...": "Ładowanie konfiguracji n8n...", + "Loading permission matrix...": "Ładowanie macierzy uprawnień...", + "Loading preferences...": "Ładowanie preferencji...", + "Loading register data...": "Ładowanie danych rejestru...", + "Loading registers...": "Ładowanie rejestrów...", + "Loading retention settings...": "Ładowanie ustawień przechowywania...", + "Loading schemas...": "Ładowanie schematów...", + "Loading search configuration...": "Ładowanie konfiguracji wyszukiwania...", + "Loading search trails...": "Ładowanie śladów wyszukiwania...", + "Loading sources...": "Ładowanie źródeł...", + "Loading statistics...": "Ładowanie statystyk...", + "Loading text extraction settings...": "Ładowanie ustawień ekstrakcji tekstu...", + "Loading tokens...": "Ładowanie tokenów...", + "Loading user groups...": "Ładowanie grup użytkowników...", + "Loading users...": "Ładowanie użytkowników...", + "Loading views...": "Ładowanie widoków...", + "Loading...": "Ładowanie...", + "Local": "Lokalny", + "Local Version": "Wersja lokalna", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Zlokalizuj każdy obiekt odwołujący się do osoby, której dane dotyczą (Art 15 inzage), wyświetl podgląd usunięcia (Art 17 vergetelheid) lub wyeksportuj jej dane (Art 20 portabiliteit).", + "Location": "Lokalizacja", + "Location Field": "Pole lokalizacji", + "Locations": "Lokalizacje", + "Locked": "Zablokowany", + "Locked:": "Zablokowany:", + "Login successful": "Logowanie pomyślne", + "Logs": "Dzienniki", + "Low": "Niski", + "Low Confidence": "Niska pewność", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Niższe liczby pojawiają się jako pierwsze w panelu bocznym filtrów. Pozostaw puste dla automatycznego porządkowania.", + "Lucene Version:": "Wersja Lucene:", + "Magic table sync completed for {schema}": "Synchronizacja tabeli magic zakończona dla {schema}", + "Magic table synchronized successfully": "Tabela magic została pomyślnie zsynchronizowana", + "Mail Integration": "Integracja Mail", + "Mail integration is not available": "Integracja Mail jest niedostępna", + "Make this installation discoverable within the federation network.": "Uczyń tę instalację wykrywalną w sieci federacji.", + "Manage Organisation Roles": "Zarządzaj rolami organizacji", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Zarządzaj kolekcjami SOLR (magazynami danych) i przypisuj je do obiektów i plików.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Zarządzaj zestawami SOLR ConfigSets (szablonami konfiguracji) dla swoich kolekcji.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Zarządzaj kolekcjami SOLR (magazynami danych) i przypisuj je do obiektów i plików.", + "Manage SOLR configsets (configuration templates) for your collections.": "Zarządzaj zestawami SOLR configsets (szablonami konfiguracji) dla swoich kolekcji.", + "Manage and configure agents for automated tasks": "Zarządzaj i konfiguruj agentów do zadań zautomatyzowanych", + "Manage and monitor file text extraction status": "Zarządzaj i monitoruj status ekstrakcji tekstu z plików", + "Manage and restore soft deleted items from your registers": "Zarządzaj i przywracaj nietrwale usunięte elementy ze swoich rejestrów", + "Manage and view detected entities from files and objects": "Zarządzaj i przeglądaj wykryte jednostki z plików i obiektów", + "Manage document templates and themes": "Zarządzaj szablonami i motywami dokumentów", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Zarządzaj przepływami pracy n8n dla automatyzacji OpenRegister. Przepływy pracy będą przechowywane w skonfigurowanym projekcie.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Zarządzaj verwerkingsactiviteit, realizuj żądania dostępu osób, których dane dotyczą, oraz audytuj zgodność z unijnym GDPR / holenderskim AVG.", + "Manage webhooks for event-driven integrations": "Zarządzaj webhookami dla integracji sterowanych zdarzeniami", + "Manage your account settings, security, and personal data.": "Zarządzaj ustawieniami swojego konta, bezpieczeństwem i danymi osobowymi.", + "Manage your applications and modules": "Zarządzaj swoimi aplikacjami i modułami", + "Manage your chat conversations": "Zarządzaj swoimi konwersacjami czatu", + "Manage your data registers and their configurations": "Zarządzaj swoimi rejestrami danych i ich konfiguracjami", + "Manage your data schemas and their properties": "Zarządzaj swoimi schematami danych i ich właściwościami", + "Manage your data sources and their configurations": "Zarządzaj swoimi źródłami danych i ich konfiguracjami", + "Manage your organisations and switch between them": "Zarządzaj swoimi organizacjami i przełączaj się między nimi", + "Manage your saved search configurations": "Zarządzaj swoimi zapisanymi konfiguracjami wyszukiwania", + "Manage your system configurations and settings": "Zarządzaj swoimi konfiguracjami i ustawieniami systemu", + "Managed": "Zarządzany", + "Mappings": "Mapowania", + "Mass validation completed successfully": "Walidacja masowa zakończona pomyślnie", + "Mass validation failed: {error}": "Walidacja masowa nie powiodła się: {error}", + "Max Files (0 = all)": "Maks. plików (0 = wszystkie)", + "Max Length": "Maks. długość", + "Max Length:": "Maks. długość:", + "Max Objects (0 = all)": "Maks. obiektów (0 = wszystkie)", + "Max Retries": "Maks. ponownych prób", + "Max Shards/Node": "Maks. fragmentów/węzeł", + "Max Tokens": "Maks. tokenów", + "Max execution time (ms)": "Maks. czas wykonania (ms)", + "Max ms": "Maks. ms", + "Max result count": "Maks. liczba wyników", + "Max results": "Maks. wyników", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksymalny rozmiar pliku (MB)", + "Maximum Nesting Depth": "Maksymalna głębokość zagnieżdżenia", + "Maximum length": "Maksymalna długość", + "Maximum length constraint is missing": "Brakuje ograniczenia maksymalnej długości", + "Maximum length is too restrictive": "Maksymalna długość jest zbyt restrykcyjna", + "Maximum number of files to process. Set to 0 to process all files.": "Maksymalna liczba plików do przetworzenia. Ustaw 0, aby przetworzyć wszystkie pliki.", + "Maximum number of items": "Maksymalna liczba elementów", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksymalna liczba obiektów do przetworzenia. Ustaw 0, aby przetworzyć wszystkie obiekty.", + "Maximum number of retry attempts for failed deliveries": "Maksymalna liczba prób ponowienia dla nieudanych dostaw", + "Maximum time to wait before committing changes": "Maksymalny czas oczekiwania przed zatwierdzeniem zmian", + "Maximum tokens to generate": "Maksymalna liczba tokenów do wygenerowania", + "Maximum value": "Maksymalna wartość", + "Maximum value constraint is missing": "Brakuje ograniczenia maksymalnej wartości", + "Maximum value is too restrictive": "Maksymalna wartość jest zbyt restrykcyjna", + "Medium": "Średni", + "Medium Confidence": "Średnia pewność", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Zapytania średnie: Wyszukiwania z pewnym filtrowaniem lub wieloma parametrami (np. zakresy dat, konkretne rejestry/schematy)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Zapytania średnie: wyszukiwania z pewnym filtrowaniem lub wieloma parametrami (np. zakresy dat, konkretne rejestry/schematy)", + "Member removed successfully": "Członek został pomyślnie usunięty", + "Members": "Członkowie", + "Members ({count})": "Członkowie ({count})", + "Members of selected groups can access this view": "Członkowie wybranych grup mogą uzyskać dostęp do tego widoku", + "Members:": "Członkowie:", + "Memory prediction calculated successfully": "Predykcja pamięci została pomyślnie obliczona", + "Message content is required": "Treść wiadomości jest wymagana", + "Message does not belong to this conversation": "Wiadomość nie należy do tej konwersacji", + "Message not found": "Nie znaleziono wiadomości", + "Metadata": "Metadane", + "Metadata Filters": "Filtry metadanych", + "Method": "Metoda", + "Method*": "Metoda*", + "Method: {method}": "Metoda: {method}", + "Min Length": "Min. długość", + "Min execution time (ms)": "Min. czas wykonania (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. liczba wyników", + "Min results": "Min. wyników", + "Minimum": "Minimum", + "Minimum length": "Minimalna długość", + "Minimum number of items": "Minimalna liczba elementów", + "Minimum value": "Minimalna wartość", + "Minimum value constraint is missing": "Brakuje ograniczenia minimalnej wartości", + "Minimum value is too restrictive": "Minimalna wartość jest zbyt restrykcyjna", + "Missing conversation or agentUuid": "Brak konwersacji lub agentUuid", + "Missing conversationId": "Brak conversationId", + "Missing message": "Brak wiadomości", + "Missing object fields created successfully": "Brakujące pola obiektu zostały pomyślnie utworzone", + "Mode:": "Tryb:", + "Model": "Model", + "Model Name": "Nazwa modelu", + "Modified": "Zmodyfikowano", + "Monthly": "Co miesiąc", + "More sources (10-20):": "Więcej źródeł (10-20):", + "Most Active Objects": "Najbardziej aktywne obiekty", + "Multiple Of": "Wielokrotność", + "Multiple of": "Wielokrotność", + "Multiple search trail deletion not implemented yet": "Usuwanie wielu śladów wyszukiwania nie zostało jeszcze zaimplementowane", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Ustawienia Multitenancy zostały pomyślnie zaktualizowane", + "My Account": "Moje konto", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Nazwa", + "Name *": "Nazwa *", + "Name*": "Nazwa*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Pomyślnie rozgrzano pamięć podręczną nazw: załadowano nazwy w liczbie {count} w czasie {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Pomyślnie rozgrzano pamięć podręczną nazw: załadowano nazwy w liczbie {count} w czasie {time}. {message}", + "Names cache warmup completed": "Rozgrzewanie pamięci podręcznej nazw zakończone", + "Naming pattern for copies:": "Wzorzec nazewnictwa kopii:", + "New Collection Name": "Nazwa nowej kolekcji", + "New Configuration": "Nowa konfiguracja", + "New Conversation": "Nowa rozmowa", + "New Properties": "Nowe właściwości", + "New Property": "Nowa właściwość", + "New User Group": "Nowa grupa użytkowników", + "New Value": "Nowa wartość", + "New activity": "Nowa aktywność", + "New password": "Nowe hasło", + "New processing activity": "Nowa czynność przetwarzania", + "New users without specific organisation membership will be automatically added to this organisation": "Nowi użytkownicy bez przynależności do konkretnej organizacji zostaną automatycznie dodani do tej organizacji", + "New verwerkingsactiviteit": "Nowa verwerkingsactiviteit", + "Next": "Dalej", + "Nextcloud Groups": "Grupy Nextcloud", + "No": "Nie", + "No API tokens.": "Brak tokenów API.", + "No ConfigSets found": "Nie znaleziono ConfigSets", + "No Deck cards linked to this object": "Brak kart Deck powiązanych z tym obiektem", + "No GitHub token configured": "Nie skonfigurowano tokenu GitHub", + "No GitHub token provided": "Nie podano tokenu GitHub", + "No GitLab token provided": "Nie podano tokenu GitLab", + "No Organisation": "Brak organizacji", + "No active filters": "Brak aktywnych filtrów", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Brak aktywnych obiektów do miękkiego usunięcia dla schematu {schema}. Użyj opcji „Trwale usuń”, aby usunąć obiekty usunięte w sposób miękki.", + "No activity data available": "Brak dostępnych danych o aktywności", + "No activity found.": "Nie znaleziono aktywności.", + "No agents available": "Brak dostępnych agentów", + "No agents found": "Nie znaleziono agentów", + "No app store cache exists yet": "Pamięć podręczna sklepu z aplikacjami jeszcze nie istnieje", + "No applications are available.": "Brak dostępnych aplikacji.", + "No applications found": "Nie znaleziono aplikacji", + "No approval steps for this object.": "Brak kroków zatwierdzania dla tego obiektu.", + "No archived conversations": "Brak zarchiwizowanych rozmów", + "No audit trail entries found": "Nie znaleziono wpisów ścieżki audytu", + "No changes recorded": "Nie zarejestrowano żadnych zmian", + "No chunks to vectorize": "Brak fragmentów do wektoryzacji", + "No collections found": "Nie znaleziono kolekcji", + "No configuration data": "Brak danych konfiguracji", + "No configuration selected": "Nie wybrano konfiguracji", + "No configurations are available.": "Brak dostępnych konfiguracji.", + "No configurations found": "Nie znaleziono konfiguracji", + "No configurations found for this application.": "Nie znaleziono konfiguracji dla tej aplikacji.", + "No contacts linked to this object": "Brak kontaktów powiązanych z tym obiektem", + "No conversations yet. Create a new one to get started!": "Brak rozmów. Utwórz nową, aby rozpocząć!", + "No dashboards yet": "Brak pulpitów nawigacyjnych", + "No data available": "Brak dostępnych danych", + "No data available for chart": "Brak dostępnych danych dla wykresu", + "No deleted items found": "Nie znaleziono usuniętych elementów", + "No deletion data available": "Brak dostępnych danych o usuwaniu", + "No description found": "Nie znaleziono opisu", + "No description provided": "Nie podano opisu", + "No emails linked to this object": "Brak wiadomości e-mail powiązanych z tym obiektem", + "No endpoints defined": "Nie zdefiniowano żadnych punktów końcowych", + "No entities detected for this email.": "Nie wykryto żadnych encji dla tej wiadomości e-mail.", + "No entities detected in this file. Run text extraction first.": "Nie wykryto żadnych encji w tym pliku. Najpierw uruchom ekstrakcję tekstu.", + "No entities found": "Nie znaleziono encji", + "No entities have been detected yet": "Nie wykryto jeszcze żadnych encji", + "No events found": "Nie znaleziono wydarzeń", + "No events linked to this object": "Brak wydarzeń powiązanych z tym obiektem", + "No expired audit trails found to clear": "Nie znaleziono wygasłych ścieżek audytu do wyczyszczenia", + "No expired search trails found to clear": "Nie znaleziono wygasłych ścieżek wyszukiwania do wyczyszczenia", + "No extraction data available for this file": "Brak dostępnych danych ekstrakcji dla tego pliku", + "No facetable fields available. Select a register and schema to see available filters.": "Brak dostępnych pól fasetowych. Wybierz Rejestr i Schemat, aby zobaczyć dostępne filtry.", + "No files found": "Nie znaleziono plików", + "No files have been extracted yet": "Nie wyodrębniono jeszcze żadnych plików", + "No files selected": "Nie wybrano plików", + "No files to process": "Brak plików do przetworzenia", + "No files to reindex": "Brak plików do ponownego zindeksowania", + "No filters are currently active. This will delete ALL audit trail entries!": "Obecnie nie są aktywne żadne filtry. Spowoduje to usunięcie WSZYSTKICH wpisów ścieżki audytu!", + "No groups found. Try a different search.": "Nie znaleziono grup. Spróbuj innego wyszukiwania.", + "No label": "Brak etykiety", + "No labels": "Brak etykiet", + "No linked objects": "Brak powiązanych obiektów", + "No log entries found": "Nie znaleziono wpisów dziennika", + "No logs are available for this configuration.": "Brak dostępnych dzienników dla tej konfiguracji.", + "No logs are available for this source.": "Brak dostępnych dzienników dla tego źródła.", + "No logs found": "Nie znaleziono dzienników", + "No matches": "Brak dopasowań", + "No mismatched fields found - SOLR schema is properly configured": "Nie znaleziono niezgodnych pól — Schemat SOLR jest poprawnie skonfigurowany", + "No objects are currently selected for deletion.": "Obecnie nie wybrano żadnych obiektów do usunięcia.", + "No objects are currently selected for permanent deletion.": "Obecnie nie wybrano żadnych obiektów do trwałego usunięcia.", + "No objects are currently selected for restoration.": "Obecnie nie wybrano żadnych obiektów do przywrócenia.", + "No objects found": "Nie znaleziono obiektów", + "No objects found to vectorize": "Nie znaleziono obiektów do wektoryzacji", + "No objects linked to this email": "Brak obiektów powiązanych z tą wiadomością e-mail", + "No objects selected": "Nie wybrano obiektów", + "No objects selected for deletion": "Nie wybrano obiektów do usunięcia", + "No objects selected for restoration": "Nie wybrano obiektów do przywrócenia", + "No objects to delete": "Brak obiektów do usunięcia", + "No objects to delete for schema {schema}": "Brak obiektów do usunięcia dla schematu {schema}", + "No objects were permanently deleted": "Żaden obiekt nie został trwale usunięty", + "No organisations found": "Nie znaleziono organizacji", + "No personal data was found for this subject identifier.": "Nie znaleziono żadnych danych osobowych dla tego identyfikatora podmiotu.", + "No processing activities yet": "Brak czynności przetwarzania", + "No properties found": "Nie znaleziono właściwości", + "No properties match your filters.": "Żadna właściwość nie pasuje do Twoich filtrów.", + "No purge date set": "Nie ustawiono daty przeczyszczenia", + "No register data available": "Brak dostępnych danych Rejestru", + "No register objects reference this file": "Żaden obiekt Rejestru nie odwołuje się do tego pliku", + "No registers found": "Nie znaleziono rejestrów", + "No registers found for this application.": "Nie znaleziono rejestrów dla tej aplikacji.", + "No registers found. Create a register to configure permissions.": "Nie znaleziono rejestrów. Utwórz Rejestr, aby skonfigurować uprawnienia.", + "No related cases found for this sender": "Nie znaleziono powiązanych spraw dla tego nadawcy", + "No related entities": "Brak powiązanych encji", + "No relations found": "Nie znaleziono relacji", + "No request body available": "Brak dostępnej treści żądania", + "No response body available": "Brak dostępnej treści odpowiedzi", + "No saved views yet. Create one in the Search tab!": "Brak zapisanych widoków. Utwórz jeden w karcie Wyszukiwanie!", + "No saved views yet. create one in the search tab!": "Brak zapisanych widoków. utwórz jeden w karcie wyszukiwania!", + "No scan run yet": "Nie uruchomiono jeszcze żadnego skanowania", + "No schema data available": "Brak dostępnych danych Schematu", + "No schema selected for exploration": "Nie wybrano Schematu do eksploracji", + "No schemas configured for mail linking.": "Nie skonfigurowano żadnych schematów do łączenia poczty.", + "No schemas found": "Nie znaleziono schematów", + "No schemas found for this application.": "Nie znaleziono schematów dla tej aplikacji.", + "No search terms data available": "Brak dostępnych danych o wyszukiwanych hasłach", + "No search trail entries found": "Nie znaleziono wpisów ścieżki wyszukiwania", + "No sources are available.": "Brak dostępnych źródeł.", + "No sources found": "Nie znaleziono źródeł", + "No synchronizations found": "Nie znaleziono synchronizacji", + "No templates found": "Nie znaleziono szablonów", + "No templates have been created yet": "Nie utworzono jeszcze żadnych szablonów", + "No tools available. Tools can be registered by installed apps.": "Brak dostępnych narzędzi. Narzędzia mogą być rejestrowane przez zainstalowane aplikacje.", + "No users in this organisation.": "Brak użytkowników w tej organizacji.", + "No valid magic-mapped register+schema combinations found": "Nie znaleziono prawidłowych kombinacji Rejestr+Schemat z mapowaniem magic", + "No views found. Create views first before configuring vectorization.": "Nie znaleziono widoków. Najpierw utwórz widoki przed skonfigurowaniem wektoryzacji.", + "No views match your search": "Żaden widok nie pasuje do Twojego wyszukiwania", + "No webhooks found": "Nie znaleziono webhooków", + "No webhooks have been configured yet": "Nie skonfigurowano jeszcze żadnych webhooków", + "No workflows found in this project. Create workflows in the n8n editor.": "Nie znaleziono przepływów pracy w tym projekcie. Utwórz przepływy pracy w edytorze n8n.", + "None": "Brak", + "Not Found": "Nie znaleziono", + "Not extracted": "Nie wyodrębniono", + "Not helpful": "Nieprzydatne", + "Not in use": "Nieużywane", + "Note:": "Uwaga:", + "Notifications": "Powiadomienia", + "Number Constraints": "Ograniczenia liczbowe", + "Number of Sources": "Liczba źródeł", + "Number of chunks to vectorize in one API call (1-100).": "Liczba fragmentów do wektoryzacji w jednym wywołaniu API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Liczba fragmentów do wektoryzacji w jednym wywołaniu API. Więcej = szybciej, ale więcej pamięci. Zalecane: 10-50.", + "Number of objects to process in each batch (1-5000).": "Liczba obiektów do przetworzenia w każdej partii (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Liczba obiektów do wektoryzacji w jednym wywołaniu API. Więcej = szybciej, ale więcej pamięci. Zalecane: 10-50.", + "Numeric Range:": "Zakres liczbowy:", + "Object": "Obiekt", + "Object #{id}": "Obiekt #{id}", + "Object Collection": "Kolekcja obiektów", + "Object Configuration:": "Konfiguracja obiektu:", + "Object Count Prediction": "Prognoza liczby obiektów", + "Object Extraction Mode": "Tryb ekstrakcji obiektów", + "Object ID": "Identyfikator obiektu", + "Object Management": "Zarządzanie obiektami", + "Object Text Extraction": "Ekstrakcja tekstu obiektu", + "Object Vectorization": "Wektoryzacja obiektów", + "Object Warmup": "Rozgrzewanie obiektów", + "Object changes in owned objects": "Zmiany obiektu w posiadanych obiektach", + "Object collection not configured": "Kolekcja obiektów nie jest skonfigurowana", + "Object metadata incomplete for file upload": "Metadane obiektu są niekompletne do przesłania pliku", + "Object not found": "Nie znaleziono obiektu", + "Object permanently deleted": "Obiekt trwale usunięty", + "Object restored successfully": "Obiekt pomyślnie przywrócony", + "Object settings updated successfully": "Ustawienia obiektu pomyślnie zaktualizowane", + "Object sources": "Źródła obiektu", + "Object successfully deleted": "Obiekt pomyślnie usunięty", + "Object successfully modified": "Obiekt pomyślnie zmodyfikowany", + "Object unlocked successfully": "Obiekt pomyślnie odblokowany", + "Object vectorization configuration saved successfully": "Konfiguracja wektoryzacji obiektów pomyślnie zapisana", + "Object vectorized successfully": "Obiekt pomyślnie zwektoryzowany", + "Object:": "Obiekt:", + "Objects": "Obiekty", + "Objects Analyzed": "Przeanalizowane obiekty", + "Objects Distribution": "Rozkład obiektów", + "Objects being analyzed": "Analizowane obiekty", + "Objects by Register": "Obiekty według Rejestru", + "Objects by Schema": "Obiekty według Schematu", + "Objects deletion completed for {schema}": "Usuwanie obiektów zakończone dla {schema}", + "Objects deletion completed successfully": "Usuwanie obiektów pomyślnie zakończone", + "Objects in schema": "Obiekty w Schemacie", + "Objects successfully deleted": "Obiekty pomyślnie usunięte", + "Objects to Process:": "Obiekty do przetworzenia:", + "Objects to be analyzed": "Obiekty do przeanalizowania", + "Objects to be deleted": "Obiekty do usunięcia", + "Objects to be validated": "Obiekty do zweryfikowania", + "Objects will be serialized as JSON text before vectorization": "Obiekty zostaną zserializowane jako tekst JSON przed wektoryzacją", + "Objects will be soft deleted and moved to the": "Obiekty zostaną usunięte w sposób miękki i przeniesione do", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Obiekty zostaną usunięte w sposób miękki (oznaczone jako usunięte, ale zachowane w bazie danych). W razie potrzeby można je później odzyskać.", + "Old Value": "Stara wartość", + "Oldest Item (days)": "Najstarszy element (dni)", + "Ollama Chat Settings": "Ustawienia czatu Ollama", + "Ollama Embedding Configuration": "Konfiguracja osadzania Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Tylko ze zmianami", + "Only members of selected groups can access this organisation": "Tylko członkowie wybranych grup mogą uzyskać dostęp do tej organizacji", + "Open Nextcloud App Store": "Otwórz sklep z aplikacjami Nextcloud", + "Open OpenRegister sidebar": "Otwórz panel boczny OpenRegister", + "Open Register Objects": "Otwórz obiekty Rejestru", + "Open folder": "Otwórz folder", + "Open in OpenRegister": "Otwórz w OpenRegister", + "Open n8n Editor": "Otwórz edytor n8n", + "OpenAI Chat Settings": "Ustawienia czatu OpenAI", + "OpenAI Embedding Configuration": "Konfiguracja osadzania OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Ustawienia OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Pulpity nawigacyjne i zaplanowane raporty zdefiniowane przez operatora. Każdy pulpit nawigacyjny jest pełnoprawnym obiektem w Rejestrze `reports`; widżety są zadeklarowane w tablicy `widgets` pulpitu i renderowane na żywo z agregacji / GraphQL.", + "Optimizing search performance...": "Optymalizowanie wydajności wyszukiwania...", + "Optional URL-friendly identifier": "Opcjonalny identyfikator przyjazny dla URL", + "Optional description for this facet": "Opcjonalny opis tej fasety", + "Optional end date property": "Opcjonalna Właściwość daty zakończenia", + "Optional event description template": "Opcjonalny szablon opisu wydarzenia", + "Optional location property": "Opcjonalna Właściwość lokalizacji", + "Optional webhook secret for signature verification": "Opcjonalny sekret webhooka do weryfikacji podpisu", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Opcjonalnie: Dodaj parametry zapytania, aby filtrować przywoływany Schemat (np. status=active&type=public)", + "Or": "Lub", + "Order": "Kolejność", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Uporządkowane znaczniki języka BCP 47. Pierwszy język jest domyślnym dla Rejestru i steruje awaryjnym mechanizmem Accept-Language dla właściwości przetłumaczalnych.", + "Organisation": "Organizacja", + "Organisation Configuration": "Konfiguracja organizacji", + "Organisation ID": "Identyfikator organizacji", + "Organisation Members": "Członkowie organizacji", + "Organisation Statistics": "Statystyki organizacji", + "Organisation created successfully": "Organizacja pomyślnie utworzona", + "Organisation membership changes": "Zmiany w przynależności do organizacji", + "Organisation name is required": "Nazwa organizacji jest wymagana", + "Organisation settings saved successfully": "Ustawienia organizacji pomyślnie zapisane", + "Organisation successfully created": "Organizacja pomyślnie utworzona", + "Organisation successfully updated": "Organizacja pomyślnie zaktualizowana", + "Organisational measures": "Środki organizacyjne", + "Organisations": "Organizacje", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organizacja", + "Organization ID (Optional)": "Identyfikator organizacji (opcjonalnie)", + "Organizations": "Organizacje", + "Orphaned Items": "Osierocone elementy", + "Other": "Inne", + "Output Mapping": "Mapowanie wyjścia", + "Overview": "Przegląd", + "Owner": "Właściciel", + "Owner:": "Właściciel:", + "PDF": "PDF", + "PII hits": "Trafienia PII", + "PII rows": "Wiersze PII", + "Page {current} of {total}": "Strona {current} z {total}", + "Parallel Mode (Faster, more resource intensive)": "Tryb równoległy (szybszy, bardziej zasobożerny)", + "Parallel:": "Równolegle:", + "Password": "Hasło", + "Password changes are not supported by your authentication provider.": "Zmiany hasła nie są obsługiwane przez Państwa dostawcę uwierzytelniania.", + "Password for SOLR authentication (optional)": "Hasło do uwierzytelniania SOLR (opcjonalnie)", + "Password updated successfully": "Hasło pomyślnie zaktualizowane", + "Path": "Ścieżka", + "Path in repository": "Ścieżka w repozytorium", + "Path where the configuration file will be saved in the repository": "Ścieżka, w której plik konfiguracyjny zostanie zapisany w repozytorium", + "Path where the register OAS file will be saved in the repository": "Ścieżka, w której plik OAS Rejestru zostanie zapisany w repozytorium", + "Pattern (regex)": "Wzorzec (regex)", + "Pattern Issue": "Problem ze wzorcem", + "Pattern constraint is missing": "Brakuje ograniczenia wzorca", + "Patterns:": "Wzorce:", + "Pending": "Oczekujące", + "Permanent Delete Mode": "Tryb trwałego usuwania", + "Permanently Delete": "Trwale usuń", + "Permanently Delete ({count})": "Trwale usuń ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Trwale usuń wszystkie obiekty usunięte w sposób miękki w liczbie {count}. Tej operacji nie można cofnąć!", + "Permanently delete already soft-deleted objects": "Trwale usuń obiekty już usunięte w sposób miękki", + "Permanently delete this conversation?": "Trwale usunąć tę rozmowę?", + "Permission Matrix": "Macierz uprawnień", + "Person": "Osoba", + "Personal Data": "Dane osobowe", + "Personal Data Export": "Eksport danych osobowych", + "Persons": "Osoby", + "Phone": "Telefon", + "Phone Numbers": "Numery telefonów", + "Photos": "Zdjęcia", + "Please create an agent in the": "Proszę utworzyć agenta w", + "Please select a user": "Proszę wybrać użytkownika", + "Please select an agent to continue": "Proszę wybrać agenta, aby kontynuować", + "Please select an organisation": "Proszę wybrać organizację", + "Please select at least one item to import": "Proszę wybrać co najmniej jeden element do zaimportowania", + "Please select registers or schemas first": "Proszę najpierw wybrać rejestry lub schematy", + "Please select which register and schema to use for the new object": "Proszę wybrać, który Rejestr i Schemat ma zostać użyty dla nowego obiektu", + "Please try again later.": "Proszę spróbować ponownie później.", + "Please wait while we fetch your agents.": "Proszę poczekać, aż pobierzemy Państwa agentów.", + "Please wait while we fetch your applications.": "Proszę poczekać, aż pobierzemy Państwa aplikacje.", + "Please wait while we fetch your configurations.": "Proszę poczekać, aż pobierzemy Państwa konfiguracje.", + "Please wait while we fetch your deleted items.": "Proszę poczekać, aż pobierzemy Państwa usunięte elementy.", + "Please wait while we fetch your sources.": "Proszę poczekać, aż pobierzemy Państwa źródła.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Proszę poczekać, aż trwale usuniemy kolekcję SOLR. Może to potrwać kilka chwil.", + "Polls": "Ankiety", + "Popular Search Terms": "Popularne wyszukiwane hasła", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Preferencje zapisane", + "Prerequisites": "Wymagania wstępne", + "Press Enter to send, Shift+Enter for new line": "Naciśnij Enter, aby wysłać, Shift+Enter dla nowego wiersza", + "Press enter to send, shift+enter for new line": "Naciśnij enter, aby wysłać, shift+enter dla nowego wiersza", + "Prettify": "Sformatuj", + "Preview erasure": "Podgląd usuwania", + "Preview:": "Podgląd:", + "Previous": "Wstecz", + "Private": "Prywatne", + "Private Agent (Default)": "Agent prywatny (domyślny)", + "Private agents": "Agenci prywatni", + "Process Name (optional)": "Nazwa procesu (opcjonalnie)", + "Processed {indexed} files, {failed} failed": "Przetworzono pliki w liczbie {indexed}, {failed} nieudane", + "Processes chunks in batches with simulated parallelism.": "Przetwarza fragmenty partiami z symulowaną równoległością.", + "Processes file chunks sequentially (safest).": "Przetwarza fragmenty plików sekwencyjnie (najbezpieczniej).", + "Processes objects in chunks with simulated parallelism.": "Przetwarza obiekty fragmentami z symulowaną równoległością.", + "Processes objects sequentially (safest).": "Przetwarza obiekty sekwencyjnie (najbezpieczniej).", + "Processing": "Przetwarzanie", + "Processing Limits": "Limity przetwarzania", + "Processing...": "Przetwarzanie...", + "Project initialized successfully": "Projekt pomyślnie zainicjowany", + "Project not found. Please initialize first.": "Nie znaleziono projektu. Proszę najpierw zainicjować.", + "Properties": "Właściwości", + "Property Behaviors": "Zachowania właściwości", + "Property Configuration:": "Konfiguracja właściwości:", + "Property Filters": "Filtry właściwości", + "Property Title": "Tytuł właściwości", + "Property Type": "Typ właściwości", + "Property can be improved": "Właściwość można ulepszyć", + "Property could not be added": "Nie można było dodać właściwości", + "Property could not be updated": "Nie można było zaktualizować właściwości", + "Property name of inversed relation": "Nazwa właściwości relacji odwróconej", + "Property successfully added": "Właściwość pomyślnie dodana", + "Property successfully updated": "Właściwość pomyślnie zaktualizowana", + "Protected": "Chronione", + "Provider": "Dostawca", + "Provider is required for testing": "Dostawca jest wymagany do testowania", + "Public": "Publiczne", + "Public View": "Widok publiczny", + "Public views can be accessed by anyone in the system": "Do widoków publicznych może uzyskać dostęp każdy w systemie", + "Publish": "Opublikuj", + "Publish Configuration to GitHub": "Opublikuj konfigurację w GitHub", + "Publish OAS": "Opublikuj OAS", + "Publish Objects": "Opublikuj obiekty", + "Publish Register OAS to GitHub": "Opublikuj OAS Rejestru w GitHub", + "Publish all objects in this schema": "Opublikuj wszystkie obiekty w tym Schemacie", + "Publish objects to make them publicly available": "Opublikuj obiekty, aby udostępnić je publicznie", + "Published": "Opublikowane", + "Published:": "Opublikowano:", + "Publishing...": "Publikowanie...", + "Purge": "Przeczyść", + "Purge Date": "Data przeczyszczenia", + "Purge {count} object from database": "Przeczyść {count} obiekt z bazy danych", + "Purge {count} objects from database": "Przeczyść obiekty w liczbie {count} z bazy danych", + "Purpose limitation *": "Ograniczenie celu *", + "Push Notifications": "Powiadomienia push", + "Query Complexity Distribution": "Rozkład złożoności zapytań", + "Query Help": "Pomoc dotycząca zapytań", + "Query parameter is required": "Parametr zapytania jest wymagany", + "Quota": "Limit", + "RAG": "RAG", + "RAG Configuration": "Konfiguracja RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Ustawienia RBAC pomyślnie zaktualizowane", + "Rate limits cleared successfully": "Limity szybkości pomyślnie wyczyszczone", + "Raw Changes Data": "Surowe dane zmian", + "Re-vectorize on object update": "Ponowna wektoryzacja przy aktualizacji obiektu", + "Read": "Odczyt", + "Real-time push notification status via notify_push": "Status powiadomień push w czasie rzeczywistym za pośrednictwem notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Powiadomienia push w czasie rzeczywistym są aktywne. Połączeni klienci otrzymują natychmiastowe aktualizacje, gdy obiekty są tworzone, aktualizowane lub usuwane.", + "Realtime push active": "Push w czasie rzeczywistym aktywny", + "Realtime push not available — the notify_push app is not installed": "Push w czasie rzeczywistym niedostępny — aplikacja notify_push nie jest zainstalowana", + "Reason": "Powód", + "Reason (optional)": "Powód (opcjonalnie)", + "Rebase operation completed successfully": "Operacja rebase pomyślnie zakończona", + "Rebase operation failed: {error}": "Operacja rebase nie powiodła się: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Zalecenia:", + "Recommended Type:": "Zalecany typ:", + "Recommended: 5 sources": "Zalecane: 5 źródeł", + "Refresh": "Odśwież", + "Refresh Data": "Odśwież dane", + "Refresh Stats": "Odśwież statystyki", + "Refresh Workflows": "Odśwież przepływy pracy", + "Refresh dashboard": "Odśwież pulpit nawigacyjny", + "Refresh database info": "Odśwież informacje o bazie danych", + "Register": "Rejestr", + "Register #{id}": "Rejestr #{id}", + "Register / Schema": "Rejestr / Schemat", + "Register ID": "Identyfikator Rejestru", + "Register Objects": "Obiekty Rejestru", + "Register Overview": "Przegląd Rejestru", + "Register Statistics": "Statystyki Rejestru", + "Register Totals": "Sumy Rejestru", + "Register and schema are required": "Rejestr i Schemat są wymagane", + "Register annotation": "Adnotacja Rejestru", + "Register depublished successfully": "Rejestr pomyślnie depublikowany", + "Register not found": "Nie znaleziono rejestru", + "Register objects": "Obiekty rejestru", + "Register objects deletion completed successfully": "Usuwanie obiektów rejestru zakończone pomyślnie", + "Register published successfully": "Rejestr opublikowany pomyślnie", + "Register/Schema Usage": "Wykorzystanie rejestru/schematu", + "Register:": "Rejestr:", + "Registers": "Rejestry", + "Reindex": "Ponowne indeksowanie", + "Reindex completed": "Ponowne indeksowanie zakończone", + "Reindex completed successfully": "Ponowne indeksowanie zakończone pomyślnie", + "Reindex completed! Processed {count} objects in {duration}s": "Ponowne indeksowanie zakończone! Przetworzono {count} obiektów w {duration}s", + "Reindex failed": "Ponowne indeksowanie nie powiodło się", + "Reindex failed: %s": "Ponowne indeksowanie nie powiodło się: %s", + "Reject": "Odrzuć", + "Rejected": "Odrzucono", + "Related Cases": "Powiązane sprawy", + "Relations": "Relacje", + "Remote Version": "Wersja zdalna", + "Remove": "Usuń", + "Remove Deck card": "Usuń kartę Deck", + "Remove avatar": "Usuń awatar", + "Remove contact": "Usuń kontakt", + "Remove filter": "Usuń filtr", + "Remove from favorites": "Usuń z ulubionych", + "Remove from list": "Usuń z listy", + "Remove group": "Usuń grupę", + "Remove link": "Usuń łącze", + "Remove link to {name}": "Usuń łącze do {name}", + "Remove link to {name}?": "Usunąć łącze do {name}?", + "Remove link to {title}": "Usuń łącze do {title}", + "Remove oneOf entry": "Usuń wpis oneOf", + "Remove property after write-back": "Usuń właściwość po zapisie zwrotnym", + "Remove {title}": "Usuń {title}", + "Remove {userId} from this organisation?": "Usunąć {userId} z tej organizacji?", + "Removed": "Usunięto", + "Removed from favorites": "Usunięto z ulubionych", + "Removing schema {schema}...": "Usuwanie schematu {schema}...", + "Rename Conversation": "Zmień nazwę rozmowy", + "Rename conversation": "Zmień nazwę rozmowy", + "Replicas": "Repliki", + "Reports": "Raporty", + "Repository": "Repozytorium", + "Request Body": "Treść żądania", + "Request Data": "Dane żądania", + "Request Details": "Szczegóły żądania", + "Request Quota (per day)": "Limit żądań (dziennie)", + "Request account deactivation": "Zażądaj dezaktywacji konta", + "Request account deactivation. This will notify administrators for review.": "Zażądaj dezaktywacji konta. Spowoduje to powiadomienie administratorów w celu sprawdzenia.", + "Request body must be JSON with a \"query\" field": "Treść żądania musi być w formacie JSON z polem \"query\"", + "Request timeout in seconds": "Limit czasu żądania w sekundach", + "Requested at": "Zażądano o", + "Required": "Wymagane", + "Required Token Scopes": "Wymagane zakresy tokenów", + "Required field": "Pole wymagane", + "Required status is inconsistent": "Status wymagania jest niespójny", + "Requires Dolphin AI for OCR text extraction": "Wymaga Dolphin AI do wyodrębniania tekstu OCR", + "Rerun Search": "Uruchom wyszukiwanie ponownie", + "Rerunning search: {searchTerm}": "Ponowne uruchamianie wyszukiwania: {searchTerm}", + "Reset Changes": "Resetuj zmiany", + "Reset Filters": "Resetuj filtry", + "Resource Quotas": "Limity zasobów", + "Response Body": "Treść odpowiedzi", + "Response Details": "Szczegóły odpowiedzi", + "Response Time:": "Czas odpowiedzi:", + "Restore": "Przywróć", + "Restore conversation": "Przywróć rozmowę", + "Restore or permanently delete items": "Przywróć lub trwale usuń elementy", + "Restore {count} object": "Przywróć {count} obiekt", + "Restore {count} objects": "Przywróć {count} obiektów", + "Result Count Range": "Zakres liczby wyników", + "Results": "Wyniki", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Okres przechowywania (czas trwania ISO-8601, np. P10Y, P30D)", + "Retention settings updated successfully": "Ustawienia przechowywania zaktualizowane pomyślnie", + "Retrieve all objects for this schema": "Pobierz wszystkie obiekty dla tego schematu", + "Retry": "Ponów", + "Retry Failed Extractions": "Ponów nieudane wyodrębnienia", + "Retry Policy": "Zasady ponawiania", + "Retry completed": "Ponawianie zakończone", + "Retry upload": "Ponów przesyłanie", + "Retrying...": "Ponawianie...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Sprawdź wybrane obiekty poniżej. Można usunąć dowolne obiekty, których nie chcą Państwo usuwać, klikając przycisk usuwania.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Sprawdź wybrane obiekty poniżej. Można usunąć dowolne obiekty, których nie chcą Państwo uwzględniać.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Sprawdź wybrane obiekty poniżej. Można usunąć dowolne obiekty, których nie chcą Państwo trwale usuwać, klikając przycisk usuwania. Tej operacji nie można cofnąć.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Sprawdź wybrane obiekty poniżej. Można usunąć dowolne obiekty, których nie chcą Państwo przywracać, klikając przycisk usuwania. Obiekty zostaną przywrócone do ich pierwotnej lokalizacji.", + "Revoke": "Cofnij", + "Right": "Prawo", + "Risk Level": "Poziom ryzyka", + "Risk level": "Poziom ryzyka", + "Roles and Permissions": "Role i uprawnienia", + "Roles updated successfully": "Role zaktualizowane pomyślnie", + "Run compliance scan": "Uruchom skanowanie zgodności", + "SOLR Connection Settings": "Ustawienia połączenia SOLR", + "SOLR Index Warmup": "Rozgrzewanie indeksu SOLR", + "SOLR Version:": "Wersja SOLR:", + "SOLR actions menu": "Menu działań SOLR", + "SOLR base path (usually /solr)": "Ścieżka bazowa SOLR (zwykle /solr)", + "SOLR collection deleted successfully": "Kolekcja SOLR usunięta pomyślnie", + "SOLR connection test failed: {error}": "Test połączenia SOLR nie powiódł się: {error}", + "SOLR connection test successful!": "Test połączenia SOLR zakończony pomyślnie!", + "SOLR index warmup completed successfully": "Rozgrzewanie indeksu SOLR zakończone pomyślnie", + "SOLR is disabled": "SOLR jest wyłączony", + "SOLR is not available or not configured": "SOLR jest niedostępny lub nieskonfigurowany", + "SOLR logging disabled": "Rejestrowanie SOLR wyłączone", + "SOLR logging enabled": "Rejestrowanie SOLR włączone", + "SOLR search disabled": "Wyszukiwanie SOLR wyłączone", + "SOLR search enabled": "Wyszukiwanie SOLR włączone", + "SOLR server hostname or IP address": "Nazwa hosta lub adres IP serwera SOLR", + "SOLR server port number (optional, defaults to 8983)": "Numer portu serwera SOLR (opcjonalnie, domyślnie 8983)", + "SOLR settings updated successfully": "Ustawienia SOLR zaktualizowane pomyślnie", + "SOLR setup completed successfully": "Konfiguracja SOLR zakończona pomyślnie", + "SOLR setup completed successfully!": "Konfiguracja SOLR zakończona pomyślnie!", + "SOLR setup error: %s": "Błąd konfiguracji SOLR: %s", + "SOLR setup failed": "Konfiguracja SOLR nie powiodła się", + "SOLR setup failed - check logs": "Konfiguracja SOLR nie powiodła się - sprawdź dzienniki", + "SOLR setup failed. Please check the configuration and try again.": "Konfiguracja SOLR nie powiodła się. Proszę sprawdzić konfigurację i spróbować ponownie.", + "SOLR setup failed: {error}": "Konfiguracja SOLR nie powiodła się: {error}", + "SOLR warmup failed: {error}": "Rozgrzewanie SOLR nie powiodło się: {error}", + "Sample Values:": "Przykładowe wartości:", + "Save": "Zapisz", + "Save Configuration": "Zapisz konfigurację", + "Save Connection Settings": "Zapisz ustawienia połączenia", + "Save Roles": "Zapisz role", + "Save Settings": "Zapisz ustawienia", + "Save changes": "Zapisz zmiany", + "Save current search as view": "Zapisz bieżące wyszukiwanie jako widok", + "Save labels": "Zapisz etykiety", + "Save tags for {name}": "Zapisz tagi dla {name}", + "Save the organisation first to manage users.": "Aby zarządzać użytkownikami, najpierw zapisz organizację.", + "Saved Views": "Zapisane widoki", + "Saving...": "Zapisywanie...", + "Scheduled": "Zaplanowano", + "Schema": "Schemat", + "Schema ID": "Identyfikator schematu", + "Schema Name": "Nazwa schematu", + "Schema Statistics": "Statystyki schematu", + "Schema annotation": "Adnotacja schematu", + "Schema configuration updated successfully for {schema}": "Konfiguracja schematu zaktualizowana pomyślnie dla {schema}", + "Schema depublished successfully": "Schemat wycofany z publikacji pomyślnie", + "Schema objects deletion completed successfully": "Usuwanie obiektów schematu zakończone pomyślnie", + "Schema published successfully": "Schemat opublikowany pomyślnie", + "Schema reference ($ref)": "Odwołanie do schematu ($ref)", + "Schema successfully created": "Schemat pomyślnie utworzony", + "Schema successfully updated": "Schemat pomyślnie zaktualizowany", + "Schema {schema} removed successfully": "Schemat {schema} usunięty pomyślnie", + "Schema:": "Schemat:", + "Schemas": "Schematy", + "Scheme": "Schemat", + "Search": "Szukaj", + "Search / Views": "Wyszukiwanie / widoki", + "Search / views": "Wyszukiwanie / widoki", + "Search Activity": "Aktywność wyszukiwania", + "Search Analytics": "Analityka wyszukiwania", + "Search Configuration": "Konfiguracja wyszukiwania", + "Search Entities": "Wyszukaj encje", + "Search Files": "Wyszukaj pliki", + "Search GitHub": "Przeszukaj GitHub", + "Search GitLab": "Przeszukaj GitLab", + "Search Method": "Metoda wyszukiwania", + "Search Mode": "Tryb wyszukiwania", + "Search Query": "Zapytanie wyszukiwania", + "Search Results": "Wyniki wyszukiwania", + "Search Term": "Wyszukiwane hasło", + "Search Trail Details": "Szczegóły śladu wyszukiwania", + "Search Trail Management": "Zarządzanie śladem wyszukiwania", + "Search Trail Statistics": "Statystyki śladu wyszukiwania", + "Search Trails": "Ślady wyszukiwania", + "Search Views": "Widoki wyszukiwania", + "Search Webhooks": "Wyszukaj webhooki", + "Search agents...": "Wyszukaj agentów...", + "Search applications...": "Wyszukaj aplikacje...", + "Search by file name or path": "Wyszukaj według nazwy pliku lub ścieżki", + "Search by name or URL": "Wyszukaj według nazwy lub URL", + "Search by title or UUID...": "Wyszukaj według tytułu lub UUID...", + "Search by value": "Wyszukaj według wartości", + "Search configurations": "Wyszukaj konfiguracje", + "Search data sources...": "Wyszukaj źródła danych...", + "Search groups...": "Wyszukaj grupy...", + "Search in Database Objects": "Wyszukaj w obiektach bazy danych", + "Search in Files": "Wyszukaj w plikach", + "Search in Objects": "Wyszukaj w obiektach", + "Search objects": "Wyszukaj obiekty", + "Search objects...": "Wyszukaj obiekty...", + "Search property names...": "Wyszukaj nazwy właściwości...", + "Search registers...": "Wyszukaj rejestry...", + "Search schemas...": "Wyszukaj schematy...", + "Search trail deletion not implemented yet": "Usuwanie śladu wyszukiwania nie zostało jeszcze zaimplementowane", + "Search views...": "Wyszukaj widoki...", + "Search {name}...": "Wyszukaj {name}...", + "Searching...": "Wyszukiwanie...", + "Secret": "Sekret", + "Secret key for HMAC signature generation (optional)": "Klucz tajny do generowania podpisu HMAC (opcjonalnie)", + "Security": "Bezpieczeństwo", + "See organisations as a table": "Wyświetl organizacje jako tabelę", + "See organisations as cards": "Wyświetl organizacje jako karty", + "Select AI Agent": "Wybierz agenta AI", + "Select Active Organisation": "Wybierz aktywną organizację", + "Select All": "Wybierz wszystko", + "Select ConfigSet": "Wybierz ConfigSet", + "Select File Types to Vectorize:": "Wybierz typy plików do wektoryzacji:", + "Select HTTP method": "Wybierz metodę HTTP", + "Select Register and Schema": "Wybierz rejestr i schemat", + "Select Views to Vectorize:": "Wybierz widoki do wektoryzacji:", + "Select a Nextcloud group to add": "Wybierz grupę Nextcloud do dodania", + "Select a branch": "Wybierz gałąź", + "Select a date property": "Wybierz właściwość daty", + "Select a model or type a custom model name": "Wybierz model lub wpisz niestandardową nazwę modelu", + "Select a register": "Wybierz rejestr", + "Select a register...": "Wybierz rejestr...", + "Select a repository": "Wybierz repozytorium", + "Select a repository you have write access to": "Wybierz repozytorium, do którego mają Państwo dostęp do zapisu", + "Select a schema": "Wybierz schemat", + "Select a schema...": "Wybierz schemat...", + "Select a user": "Wybierz użytkownika", + "Select agent type": "Wybierz typ agenta", + "Select all entities": "Wybierz wszystkie encje", + "Select an AI Agent": "Wybierz agenta AI", + "Select an AI agent to begin chatting with your data.": "Wybierz agenta AI, aby rozpocząć rozmowę z danymi.", + "Select an email to see linked objects": "Wybierz wiadomość e-mail, aby zobaczyć powiązane obiekty", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Wybierz organizację i użytkownika, aby dodać go jako członka. Wyszukaj organizacje według nazwy.", + "Select backend": "Wybierz backend", + "Select chat model": "Wybierz model czatu", + "Select collection for files": "Wybierz kolekcję dla plików", + "Select collection for objects": "Wybierz kolekcję dla obiektów", + "Select configuration type...": "Wybierz typ konfiguracji...", + "Select default organisation": "Wybierz domyślną organizację", + "Select entity {value}": "Wybierz encję {value}", + "Select event to listen to...": "Wybierz zdarzenie do nasłuchiwania...", + "Select fields to display": "Wybierz pola do wyświetlenia", + "Select files with the correct extension": "Wybierz pliki z poprawnym rozszerzeniem", + "Select group": "Wybierz grupę", + "Select groups (optional)": "Wybierz grupy (opcjonalnie)", + "Select groups to notify...": "Wybierz grupy do powiadomienia...", + "Select groups with access to this agent": "Wybierz grupy z dostępem do tego agenta", + "Select groups...": "Wybierz grupy...", + "Select model": "Wybierz model", + "Select one or more registers": "Wybierz jeden lub więcej rejestrów", + "Select one or more schemas": "Wybierz jeden lub więcej schematów", + "Select options...": "Wybierz opcje...", + "Select or create labels, or select \"No label\" to add files": "Wybierz lub utwórz etykiety albo wybierz \"Brak etykiety\", aby dodać pliki", + "Select owner application (optional)...": "Wybierz aplikację właściciela (opcjonalnie)...", + "Select period": "Wybierz okres", + "Select property to send as payload": "Wybierz właściwość do wysłania jako ładunek", + "Select provider": "Wybierz dostawcę", + "Select registers and schemas to save a view": "Wybierz rejestry i schematy, aby zapisać widok", + "Select retry policy": "Wybierz zasady ponawiania", + "Select role": "Wybierz rolę", + "Select schemas (empty = all schemas)": "Wybierz schematy (puste = wszystkie schematy)", + "Select schemas to warm up": "Wybierz schematy do rozgrzania", + "Select search mode": "Wybierz tryb wyszukiwania", + "Select source type...": "Wybierz typ źródła...", + "Select target type": "Wybierz typ celu", + "Select the branch to publish to": "Wybierz gałąź do publikacji", + "Select the event this webhook should listen to": "Wybierz zdarzenie, którego ten webhook powinien nasłuchiwać", + "Select views to filter data (optional)": "Wybierz widoki do filtrowania danych (opcjonalnie)", + "Select views to limit which data the agent can access": "Wybierz widoki, aby ograniczyć dane, do których agent może mieć dostęp", + "Select views to vectorize:": "Wybierz widoki do wektoryzacji:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Wybierz, które grupy Nextcloud są dostępne dla tej organizacji. Użytkownicy w tych grupach będą mieli dostęp do zasobów organizacji.", + "Select which data views the AI can search": "Wybierz, które widoki danych może przeszukiwać AI", + "Select which property from the event should be used as the webhook payload data": "Wybierz, która właściwość zdarzenia powinna zostać użyta jako dane ładunku webhooka", + "Select which tools the AI can use to perform actions": "Wybierz, których narzędzi może użyć AI do wykonywania działań", + "Select {fieldName} values": "Wybierz wartości {fieldName}", + "Selected": "Wybrano", + "Selected Groups": "Wybrane grupy", + "Selected Objects ({count})": "Wybrane obiekty ({count})", + "Selected audit trails deleted successfully": "Wybrane ślady audytu usunięte pomyślnie", + "Selected search trails deleted successfully": "Wybrane ślady wyszukiwania usunięte pomyślnie", + "Selected users can access this view": "Wybrani użytkownicy mogą uzyskać dostęp do tego widoku", + "Send additional feedback": "Wyślij dodatkową opinię", + "Send as CloudEvent": "Wyślij jako CloudEvent", + "Send as cloudevent": "Wyślij jako cloudevent", + "Sensitive PII": "Wrażliwe dane osobowe (PII)", + "Serial Mode (Safer, slower)": "Tryb szeregowy (bezpieczniejszy, wolniejszy)", + "Serial:": "Szeregowy:", + "Server Configuration": "Konfiguracja serwera", + "Server Information": "Informacje o serwerze", + "Set as Active": "Ustaw jako aktywne", + "Set as active organisation": "Ustaw jako aktywną organizację", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Ustaw limity wykorzystania API i zużycia tokenów. Użyj 0 dla nieograniczonych zasobów.", + "Settings": "Ustawienia", + "Settings updated successfully": "Ustawienia zaktualizowane pomyślnie", + "Shards": "Fragmenty (shards)", + "Share": "Udostępnij", + "Share with Groups": "Udostępnij grupom", + "Share with Users": "Udostępnij użytkownikom", + "Show": "Pokaż", + "Show Filters": "Pokaż filtry", + "Show less": "Pokaż mniej", + "Show only entries with changes": "Pokaż tylko wpisy ze zmianami", + "Showing {count} dashboard(s)": "Wyświetlanie {count} pulpitów", + "Showing {count} processing activities": "Wyświetlanie {count} verwerkingsactiviteit", + "Showing {showing} of {total} agents": "Wyświetlanie {showing} z {total} agentów", + "Showing {showing} of {total} applications": "Wyświetlanie {showing} z {total} aplikacji", + "Showing {showing} of {total} audit trail entries": "Wyświetlanie {showing} z {total} wpisów śladu audytu", + "Showing {showing} of {total} configurations": "Wyświetlanie {showing} z {total} konfiguracji", + "Showing {showing} of {total} deleted items": "Wyświetlanie {showing} z {total} usuniętych elementów", + "Showing {showing} of {total} entities": "Wyświetlanie {showing} z {total} encji", + "Showing {showing} of {total} files": "Wyświetlanie {showing} z {total} plików", + "Showing {showing} of {total} log entries": "Wyświetlanie {showing} z {total} wpisów dziennika", + "Showing {showing} of {total} search trail entries": "Wyświetlanie {showing} z {total} wpisów śladu wyszukiwania", + "Showing {showing} of {total} sources": "Wyświetlanie {showing} z {total} źródeł", + "Showing {showing} of {total} templates": "Wyświetlanie {showing} z {total} szablonów", + "Showing {showing} of {total} webhooks": "Wyświetlanie {showing} z {total} webhooków", + "Showing {shown} of {total} organisations": "Wyświetlanie {shown} z {total} organizacji", + "Simple": "Proste", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Proste zapytania: podstawowe wyszukiwania tekstowe z minimalnymi parametrami (np. pojedyncze hasło wyszukiwania, bez zaawansowanych filtrów)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Proste zapytania: podstawowe wyszukiwania tekstowe z minimalnymi parametrami (np. pojedyncze hasło wyszukiwania, bez zaawansowanych filtrów)", + "Size": "Rozmiar", + "Slug": "Slug", + "Soft Delete Mode": "Tryb miękkiego usuwania", + "Soft Deleted Items": "Elementy miękko usunięte", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Miękko usuń wszystkie obiekty dla tego schematu ({active} aktywnych, {deleted} już usuniętych)", + "SolrCloud mode enabled": "Tryb SolrCloud włączony", + "Something went wrong while adding files": "Coś poszło nie tak podczas dodawania plików", + "Source": "Źródło", + "Source successfully updated": "Źródło pomyślnie zaktualizowane", + "Sources": "Źródła", + "Sources:": "Źródła:", + "Special Rights": "Specjalne uprawnienia", + "Standalone SOLR mode": "Samodzielny tryb SOLR", + "Start Conversation": "Rozpocznij rozmowę", + "Start Date Field": "Pole daty rozpoczęcia", + "Start Vectorization": "Rozpocznij wektoryzację", + "Start a conversation": "Rozpocznij rozmowę", + "Starting names cache warmup...": "Rozpoczynanie rozgrzewania pamięci podręcznej nazw...", + "Starting validation for {schema}...": "Rozpoczynanie walidacji dla {schema}...", + "Starting {action} for {schema}...": "Rozpoczynanie {action} dla {schema}...", + "Starting...": "Rozpoczynanie...", + "Statistics": "Statystyki", + "Statistics and Metrics": "Statystyki i metryki", + "Statistics refreshed": "Statystyki odświeżone", + "Stats calculation not yet implemented": "Obliczanie statystyk nie zostało jeszcze zaimplementowane", + "Status": "Status", + "Status Code": "Kod statusu", + "Step": "Krok", + "Storage": "Pamięć masowa", + "Storage Quota (MB)": "Limit pamięci masowej (MB)", + "String Constraints": "Ograniczenia ciągu znaków", + "Subject identifier (email, BSN, name, etc.)": "Identyfikator podmiotu (e-mail, BSN, nazwa itp.)", + "Subscribe": "Subskrybuj", + "Subscribed": "Zasubskrybowano", + "Success": "Sukces", + "Success Rate": "Wskaźnik powodzenia", + "Success Status": "Status powodzenia", + "Successful": "Powodzenie", + "Successfully added user to organisation: {name}": "Pomyślnie dodano użytkownika do organizacji: {name}", + "Successfully cleared {count} audit trails": "Pomyślnie wyczyszczono {count} śladów audytu", + "Successfully cleared {count} blob storage objects": "Pomyślnie wyczyszczono {count} obiektów pamięci blob", + "Successfully cleared {count} search trails": "Pomyślnie wyczyszczono {count} śladów wyszukiwania", + "Successfully created {count} SOLR fields": "Pomyślnie utworzono {count} pól SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Pomyślnie usunięto {count} osadzeń. Proszę ponownie zwektoryzować dane.", + "Successfully deleted {count} objects for {schema}": "Pomyślnie usunięto {count} obiektów dla {schema}", + "Successfully fixed {count} SOLR field configurations": "Pomyślnie naprawiono {count} konfiguracji pól SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Pomyślnie zaimportowano: {registers} rejestrów, {schemas} schematów, {objects} obiektów", + "Successfully joined organisation": "Pomyślnie dołączono do organizacji", + "Successfully permanently deleted {count} objects": "Pomyślnie trwale usunięto {count} obiektów", + "Successfully processed {count} files!": "Pomyślnie przetworzono {count} plików!", + "Successfully restored {count} object": "Pomyślnie przywrócono {count} obiekt", + "Successfully restored {count} objects": "Pomyślnie przywrócono {count} obiektów", + "Successfully saved configuration for {count} facets!": "Pomyślnie zapisano konfigurację dla {count} aspektów!", + "Supported File Types": "Obsługiwane typy plików", + "Switch Active Organisation": "Przełącz aktywną organizację", + "Switch Organisation": "Przełącz organizację", + "Sync Table": "Synchronizuj tabelę", + "Synchronization Settings": "Ustawienia synchronizacji", + "Syncing magic table for {schema}...": "Synchronizowanie tabeli magic dla {schema}...", + "Syncs": "Synchronizacje", + "System Default": "Domyślne systemowe", + "System Prompt": "Monit systemowy", + "System announcements": "Ogłoszenia systemowe", + "Table": "Tabela", + "Target ID": "Identyfikator celu", + "Target Type*": "Typ celu*", + "Technical Description": "Opis techniczny", + "Technical Details": "Szczegóły techniczne", + "Technical description for developers and administrators": "Opis techniczny dla programistów i administratorów", + "Technical measures": "Środki techniczne", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatura", + "Templates": "Szablony", + "Test": "Test", + "Test Chat": "Czat testowy", + "Test Connection": "Przetestuj połączenie", + "Test Embedding": "Przetestuj osadzanie", + "Test endpoint executed successfully": "Punkt końcowy testu wykonany pomyślnie", + "Test webhook delivered successfully": "Testowy webhook dostarczony pomyślnie", + "Test webhook delivery failed": "Dostarczenie testowego webhooka nie powiodło się", + "Test webhook sent successfully": "Testowy webhook wysłany pomyślnie", + "Testing...": "Testowanie...", + "Text Chunk #{id}": "Fragment tekstu #{id}", + "Text Extraction": "Wyodrębnianie tekstu", + "Text Extraction Engine": "Silnik wyodrębniania tekstu", + "Text chunks": "Fragmenty tekstu", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Fragmenty tekstu są generowane podczas wyodrębniania plików i przechowywane w bazie danych. Wektoryzacja odczytuje te wstępnie podzielone pliki i konwertuje je na osadzenia.", + "Text extracted successfully": "Tekst wyodrębniony pomyślnie", + "Text extraction disabled": "Wyodrębnianie tekstu wyłączone", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplikacja Nextcloud Calendar nie jest zainstalowana ani włączona na tym serwerze.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplikacja Nextcloud Contacts nie jest zainstalowana ani włączona na tym serwerze.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplikacja Nextcloud Deck nie jest zainstalowana ani włączona na tym serwerze.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplikacja Nextcloud Mail nie jest zainstalowana ani włączona na tym serwerze.", + "The URL where webhook events will be sent": "URL, pod który będą wysyłane zdarzenia webhooka", + "The requested conversation does not exist": "Żądana konwersacja nie istnieje", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Nie można było załadować żądanego pulpitu. Proszę zweryfikować URL lub wybrać inny pulpit z listy.", + "The saved view and all its search configuration": "Zapisany widok i cała jego konfiguracja wyszukiwania", + "There are no audit trail entries matching your current filters.": "Brak wpisów dziennika audytu pasujących do bieżących filtrów.", + "There are no deleted items matching your current filters.": "Brak usuniętych elementów pasujących do bieżących filtrów.", + "There are no search trail entries matching your current filters.": "Brak wpisów dziennika wyszukiwania pasujących do bieżących filtrów.", + "There are no webhook log entries matching your filters.": "Brak wpisów dziennika webhooków pasujących do filtrów.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Zostaną zachowane zgodnie ze skonfigurowanym dla ich Schematu okresem przechowywania i automatycznie trwale usunięte po upływie tego okresu. Okres przechowywania można skonfigurować dla każdego Schematu i można go znaleźć w ustawieniach Schematu.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Zostaną zachowane zgodnie ze skonfigurowanym dla ich Schematu okresem przechowywania, a następnie trwale usunięte.", + "This action cannot be undone.": "Tej operacji nie można cofnąć.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Tej operacji nie można cofnąć. Proszę upewnić się, że żadne kolekcje nie używają tego ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Tej operacji nie można cofnąć. Proszę upewnić się, że żadne kolekcje nie używają tego ConfigSet.", + "This action will submit a deactivation request to your administrators.": "Ta operacja wyśle żądanie dezaktywacji do administratorów.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Ten agent nie ma konfigurowalnych widoków ani narzędzi, ale nadal można dostosować ustawienia RAG poniżej.", + "This analysis may take some time": "Ta analiza może zająć trochę czasu", + "This audit trail entry does not contain any change information.": "Ten wpis dziennika audytu nie zawiera żadnych informacji o zmianach.", + "This configuration has no data defined.": "Ta konfiguracja nie ma zdefiniowanych danych.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ten punkt końcowy jest przestarzały. Zamiast tego należy używać punktów końcowych opartych na fragmentach.", + "This entity has no relations to objects or files": "Ten obiekt nie ma relacji do Obiektów ani Plików", + "This is a test webhook from OpenRegister": "To jest testowy webhook z OpenRegister", + "This is not valid JSON": "To nie jest prawidłowy JSON", + "This key already exists on this schema": "Ten klucz już istnieje w tym Schemacie", + "This schema must use Magic Table configuration to sync": "Ten Schemat musi używać konfiguracji Magic Table, aby się synchronizować", + "This schema must use magic table configuration to sync": "Ten Schemat musi używać konfiguracji Magic Table, aby się synchronizować", + "This source has no associated registers.": "To Źródło nie ma powiązanych Rejestrów.", + "This token will only be shown once. Copy it now.": "Ten token zostanie wyświetlony tylko raz. Proszę go teraz skopiować.", + "This will copy the _default ConfigSet with the new name": "Spowoduje to skopiowanie ConfigSet _default pod nową nazwą", + "This will copy the _default configset with the new name": "Spowoduje to skopiowanie ConfigSet _default pod nową nazwą", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Spowoduje to trwałe usunięcie WSZYSTKICH osadzeń (wektorów) z bazy danych. Konieczne będzie ponowne zwektoryzowanie wszystkich Obiektów i Plików. Tej operacji nie można cofnąć.\n\nCzy na pewno chcą Państwo kontynuować?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Spowoduje to trwałe usunięcie WSZYSTKICH osadzeń (wektorów) z bazy danych. Konieczne będzie ponowne zwektoryzowanie wszystkich Obiektów i Plików. Tej operacji nie można cofnąć.\\n\\nCzy na pewno chcą Państwo kontynuować?", + "This will permanently delete:": "Spowoduje to trwałe usunięcie:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Spowoduje to trwałe usunięcie {count} już miękko usuniętych Obiektów z bazy danych. Tej operacji nie można cofnąć.", + "Timeout (seconds)": "Limit czasu (sekundy)", + "Timestamp": "Znacznik czasu", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Wskazówka: Proszę włączać tylko widoki, które wymagają wyszukiwania semantycznego, aby zminimalizować koszty osadzania. Proste tabele wyszukiwania rzadko wymagają wektoryzacji.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Wskazówka: proszę włączać tylko widoki, które wymagają wyszukiwania semantycznego, aby zminimalizować koszty osadzania. Proste tabele wyszukiwania rzadko wymagają wektoryzacji.", + "Title": "Tytuł", + "Title *": "Tytuł *", + "Title Template": "Szablon tytułu", + "Title*": "Tytuł*", + "To (e.g. -1 year or 2025-12-31)": "Do (np. -1 year lub 2025-12-31)", + "To Date": "Do daty", + "To add files larger than or equal to 512MB, go to the": "Aby dodać Pliki o rozmiarze większym lub równym 512 MB, należy przejść do", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Aby dostosować rozmiar fragmentu lub strategię, należy przejść do Konfiguracja Plików → Limity przetwarzania.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Aby dostosować rozmiar fragmentu lub strategię, należy przejść do Konfiguracja Plików → Limity przetwarzania.", + "To change the embedding provider or model, go to LLM Configuration.": "Aby zmienić dostawcę osadzania lub model, należy przejść do Konfiguracja LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Aby zmienić dostawcę osadzania lub model, należy przejść do Konfiguracja LLM.", + "To date": "Do daty", + "Toggle search sidebar": "Przełącz panel boczny wyszukiwania", + "Token Created": "Token utworzony", + "Token Quota (per request)": "Limit tokenów (na żądanie)", + "Token copied to clipboard": "Token skopiowany do schowka", + "Token name": "Nazwa tokena", + "Token revoked": "Token unieważniony", + "Tools": "Narzędzia", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Narzędzia wykonują się z domyślnymi uprawnieniami użytkownika agenta, gdy żadna sesja użytkownika nie jest aktywna (np. zadania cron). Domyślnego użytkownika można skonfigurować w karcie Ustawienia.", + "Tools respect the agent's own role and organization boundaries.": "Narzędzia respektują własną rolę agenta oraz granice organizacji.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Narzędzia respektują widoki, uprawnienia oraz granice organizacji agenta.", + "Top Deleters": "Najczęściej usuwający", + "Total": "Razem", + "Total Audit Trails": "Łączna liczba dzienników audytu", + "Total Chunks Available:": "Łączna liczba dostępnych fragmentów:", + "Total Deleted Items": "Łączna liczba usuniętych elementów", + "Total Members": "Łączna liczba członków", + "Total Objects": "Łączna liczba Obiektów", + "Total Objects in Database:": "Łączna liczba Obiektów w bazie danych:", + "Total Organisations": "Łączna liczba organizacji", + "Total Results": "Łączna liczba wyników", + "Total Searches": "Łączna liczba wyszukiwań", + "Total Size": "Łączny rozmiar", + "Total Storage": "Łączna pamięć masowa", + "Total events": "Łączna liczba zdarzeń", + "Total:": "Razem:", + "Totals": "Sumy", + "Try searching by UUID or with different keywords": "Proszę spróbować wyszukać według UUID lub z użyciem innych słów kluczowych", + "Type": "Typ", + "Type \"DELETE COLLECTION\" to confirm:": "Proszę wpisać \"DELETE COLLECTION\", aby potwierdzić:", + "Type Issue": "Problem z typem", + "Type Variations:": "Wariacje typu:", + "Type filter (optional, e.g. email)": "Filtr typu (opcjonalny, np. email)", + "Type must be \"positive\" or \"negative\"": "Typ musi być \"positive\" lub \"negative\"", + "Type to search for groups": "Proszę pisać, aby wyszukać grupy", + "Type to search for objects...": "Proszę pisać, aby wyszukać Obiekty...", + "Type to search for organisations": "Proszę pisać, aby wyszukać organizacje", + "Type to search for users": "Proszę pisać, aby wyszukać użytkowników", + "Type to search groups...": "Proszę pisać, aby wyszukać grupy...", + "Type to search users...": "Proszę pisać, aby wyszukać użytkowników...", + "Type to search...": "Proszę pisać, aby wyszukać...", + "Type your username to confirm": "Proszę wpisać swoją nazwę użytkownika, aby potwierdzić", + "URL": "URL", + "URL where Ollama is running": "URL, pod którym działa Ollama", + "UUID copied to clipboard": "UUID skopiowany do schowka", + "UUID:": "UUID:", + "Unchanged": "Niezmieniony", + "Unique Search Terms": "Unikalne wyszukiwane terminy", + "Unique Terms": "Unikalne terminy", + "Unknown": "Nieznany", + "Unknown Object": "Nieznany Obiekt", + "Unknown Register": "Nieznany Rejestr", + "Unknown Schema": "Nieznany Schemat", + "Unknown error": "Nieznany błąd", + "Unknown widget type: {type}": "Nieznany typ widżetu: {type}", + "Unlink email": "Odłącz email", + "Unlink event": "Odłącz wydarzenie", + "Unnamed Object": "Obiekt bez nazwy", + "Untitled": "Bez tytułu", + "Untitled View": "Widok bez tytułu", + "Update": "Aktualizuj", + "Update Operations": "Operacje aktualizacji", + "Update available: {local} → {remote}": "Dostępna aktualizacja: {local} → {remote}", + "Update configuration: ...": "Aktualizuj konfigurację: ...", + "Update register OAS: ...": "Aktualizuj OAS Rejestru: ...", + "Update vectors when object data changes (recommended for accurate search)": "Aktualizuj wektory, gdy zmieniają się dane Obiektu (zalecane dla dokładnego wyszukiwania)", + "Updated": "Zaktualizowano", + "Updated At": "Zaktualizowano dnia", + "Updated:": "Zaktualizowano:", + "Upload new avatar": "Prześlij nowy awatar", + "Uploading configuration sets...": "Przesyłanie zestawów konfiguracji...", + "Uptime:": "Czas działania:", + "Url": "Url", + "Usage Count": "Liczba użyć", + "Use AI agents for processing and analysis": "Używaj agentów AI do przetwarzania i analizy", + "Use Large Language Model features": "Używaj funkcji LLM", + "Use SolrCloud with Zookeeper for distributed search": "Używaj SolrCloud z ZooKeeper do wyszukiwania rozproszonego", + "Use SolrCloud with zookeeper for distributed search": "Używaj SolrCloud z ZooKeeper do wyszukiwania rozproszonego", + "Use VNG APIs instead of MongoDB.": "Używaj API VNG zamiast MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Używaj domyślnych zakresów (Ostatnie 7/30/90 dni, Ostatni rok, Starsze)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Używaj zewnętrznej pamięci masowej (np. MongoDB) zamiast wewnętrznej pamięci masowej Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Proszę używać filtrów, aby zawęzić wpisy dziennika audytu według Rejestru, Schematu, typu akcji, użytkownika, zakresu dat lub identyfikatora Obiektu.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Proszę używać filtrów, aby zawęzić usunięte elementy według Rejestru, Schematu, daty usunięcia lub użytkownika, który je usunął.", + "Use filters to narrow down entities by type or category.": "Proszę używać filtrów, aby zawęzić obiekty według typu lub kategorii.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Proszę używać filtrów, aby zawęzić wpisy dziennika wyszukiwania według Rejestru, Schematu, statusu powodzenia, użytkownika, zakresu dat, wyszukiwanych terminów lub metryk wydajności.", + "Used By Collections": "Używane przez kolekcje", + "Used by": "Używane przez", + "User": "Użytkownik", + "User Agent Statistics": "Statystyki klienta użytkownika", + "User Description": "Opis użytkownika", + "User Quota": "Limit użytkownika", + "User rate limits cleared successfully": "Limity szybkości użytkownika wyczyszczone pomyślnie", + "User removed successfully": "Użytkownik usunięty pomyślnie", + "User-friendly description shown in forms and help text": "Przyjazny dla użytkownika opis wyświetlany w formularzach i tekście pomocy", + "Username": "Nazwa użytkownika", + "Username for SOLR authentication (optional)": "Nazwa użytkownika do uwierzytelniania SOLR (opcjonalnie)", + "Users": "Użytkownicy", + "Uses": "Użycia", + "Using Pre-Generated Chunks": "Używanie wstępnie wygenerowanych fragmentów", + "Validate": "Sprawdź poprawność", + "Validate Objects": "Sprawdź poprawność Obiektów", + "Validation completed for {schema}": "Sprawdzanie poprawności zakończone dla {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Sprawdzanie poprawności zakończone dla {schema}: {processed} przetworzono, {updated} zaktualizowano, {failed} nieudanych", + "Validation completed successfully": "Sprawdzanie poprawności zakończone pomyślnie", + "Validation failed": "Sprawdzanie poprawności nie powiodło się", + "Value": "Wartość", + "Value list (split on ,)": "Lista wartości (rozdzielana ,)", + "Vector Dimensions": "Wymiary wektora", + "Vector Search Backend": "Backend wyszukiwania wektorowego", + "Vector field: _embedding_": "Pole wektorowe: _embedding_", + "Vectorization Triggers": "Wyzwalacze wektoryzacji", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Wektoryzacja zakończona: {vectorized} Obiektów zwektoryzowano, {failed} nieudanych", + "Vectorization failed: {error}": "Wektoryzacja nie powiodła się: {error}", + "Vectorize All Files": "Zwektoryzuj wszystkie Pliki", + "Vectorize All Objects": "Zwektoryzuj wszystkie Obiekty", + "Vectorize all file types": "Zwektoryzuj wszystkie typy Plików", + "Vectorize all views": "Zwektoryzuj wszystkie widoki", + "Vectorize on object creation": "Wektoryzuj przy tworzeniu Obiektu", + "Vectors will be stored in your existing object and file collections": "Wektory będą przechowywane w istniejących kolekcjach Obiektów i Plików", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Weryfikowanie łączności z serwerem...", + "Version": "Wersja", + "Version Information": "Informacje o wersji", + "Very High": "Bardzo wysoki", + "Very high": "Bardzo wysoki", + "View": "Widok", + "View API Docs": "Zobacz dokumentację API", + "View API Documentation": "Zobacz dokumentację API", + "View APIs": "Zobacz API", + "View Changes": "Zobacz zmiany", + "View Details": "Zobacz szczegóły", + "View Error": "Zobacz błąd", + "View File": "Zobacz Plik", + "View Full Details": "Zobacz pełne szczegóły", + "View Logs": "Zobacz dzienniki", + "View Name": "Nazwa widoku", + "View Object": "Zobacz Obiekt", + "View Parameters": "Zobacz parametry", + "View Selection": "Wybór widoku", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Przeglądaj i analizuj dzienniki wyszukiwania z zaawansowanymi możliwościami filtrowania i analityki", + "View and analyze system audit trails with advanced filtering capabilities": "Przeglądaj i analizuj systemowe dzienniki audytu z zaawansowanymi możliwościami filtrowania", + "View and manage authorization across registers and schemas": "Przeglądaj i zarządzaj autoryzacją w Rejestrach i Schematach", + "View deleted successfully": "Widok usunięty pomyślnie", + "View entity details and manage relations": "Przeglądaj szczegóły obiektu i zarządzaj relacjami", + "View name": "Nazwa widoku", + "View name is required": "Nazwa widoku jest wymagana", + "View object": "Zobacz Obiekt", + "View saved successfully!": "Widok zapisany pomyślnie!", + "View search analytics and manage search logs": "Przeglądaj analitykę wyszukiwania i zarządzaj dziennikami wyszukiwania", + "View successfully deleted": "Widok usunięty pomyślnie", + "View successfully updated": "Widok zaktualizowany pomyślnie", + "View updated successfully!": "Widok zaktualizowany pomyślnie!", + "View webhook delivery logs and filter by webhook": "Przeglądaj dzienniki dostarczania webhooków i filtruj według webhooka", + "View {count} more": "Zobacz {count} więcej", + "Views": "Widoki", + "Visible to end users": "Widoczne dla użytkowników końcowych", + "Visible to users": "Widoczne dla użytkowników", + "Wait for Response": "Czekaj na odpowiedź", + "Wait for webhook response before continuing (required for request/response flows)": "Czekaj na odpowiedź webhooka przed kontynuowaniem (wymagane dla przepływów żądanie/odpowiedź)", + "Waiting for cluster synchronization...": "Oczekiwanie na synchronizację klastra...", + "Warmup Names Cache": "Rozgrzej pamięć podręczną nazw", + "Warmup failed": "Rozgrzewanie nie powiodło się", + "Warning": "Ostrzeżenie", + "Warning:": "Ostrzeżenie:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Ostrzeżenie: Ta Właściwość ma niespójne typy: {types}. Proszę rozważyć, czy wszystkie Obiekty powinny mieć ten sam typ.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Przeskanujemy wszystkie Obiekty należące do tego Schematu, aby odkryć nowe Właściwości oraz przeanalizować istniejące Właściwości pod kątem potencjalnych ulepszeń. Proces ten obejmuje badanie struktury danych każdego Obiektu, identyfikowanie Właściwości niezdefiniowanych w bieżącym Schemacie oraz znajdowanie możliwości ulepszenia istniejących definicji Właściwości za pomocą lepszych ograniczeń, formatów i reguł sprawdzania poprawności.", + "Webhook": "Webhook", + "Webhook Log Details": "Szczegóły dziennika webhooka", + "Webhook Logs": "Dzienniki webhooków", + "Webhook created successfully": "Webhook utworzony pomyślnie", + "Webhook deleted": "Webhook usunięty", + "Webhook retry delivered successfully": "Ponowna próba webhooka dostarczona pomyślnie", + "Webhook retry delivery failed": "Dostarczenie ponownej próby webhooka nie powiodło się", + "Webhook updated": "Webhook zaktualizowany", + "Webhook updated successfully": "Webhook zaktualizowany pomyślnie", + "Webhooks": "Webhooki", + "Weekly": "Tygodniowo", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Gdy agent działa bez sesji użytkownika (np. zaplanowane zadania), zostanie użyty kontekst tego użytkownika", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Gdy agent działa bez sesji użytkownika (np. zaplanowane zadania), zostanie użyty kontekst tego użytkownika.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Gdy wyłączone, ten aspekt będzie pokazywał tylko wartości z tego Schematu i będzie zawierał filtr Schematu po wybraniu.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Gdy włączone, zapisanie tego Obiektu zaktualizuje również Obiekty docelowe, aby zawierały odwołanie zwrotne do tego Obiektu.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Gdy włączone, ta Właściwość zostanie usunięta z Obiektu źródłowego po zaktualizowaniu Obiektów docelowych.", + "Workflow Management": "Zarządzanie przepływem pracy", + "Workflows": "Przepływy pracy", + "Wrap webhook payload in CloudEvents format for better interoperability": "Opakuj ładunek webhooka w format CloudEvents dla lepszej interoperacyjności", + "Wrap webhook payload in cloudevents format for better interoperability": "Opakuj ładunek webhooka w format CloudEvents dla lepszej interoperacyjności", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Tak", + "You": "Państwo", + "You are not yet a member of any organisations.": "Nie są Państwo jeszcze członkiem żadnej organizacji.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Mogą Państwo tworzyć nowe ConfigSets na podstawie szablonu _default lub przesyłać własne bezpośrednio na swój serwer SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Mogą Państwo tworzyć nowe ConfigSets na podstawie szablonu _default lub przesyłać własne bezpośrednio na swój serwer SOLR.", + "You do not have access to this conversation": "Nie mają Państwo dostępu do tej konwersacji", + "You do not have permission to delete this conversation": "Nie mają Państwo uprawnień do usunięcia tej konwersacji", + "You do not have permission to modify this conversation": "Nie mają Państwo uprawnień do modyfikowania tej konwersacji", + "You do not have permission to restore this conversation": "Nie mają Państwo uprawnień do przywrócenia tej konwersacji", + "You do not have permission to view the permission matrix. Admin access is required.": "Nie mają Państwo uprawnień do wyświetlenia macierzy uprawnień. Wymagany jest dostęp administratora.", + "You have selected {count} properties to add to the schema.": "Wybrali Państwo {count} Właściwości do dodania do Schematu.", + "You must be logged in to favorite views": "Aby dodać widoki do ulubionych, trzeba być zalogowanym", + "You need an AI agent to start a conversation.": "Aby rozpocząć konwersację, potrzebny jest agent AI.", + "Your Fireworks AI API key. Get one at": "Państwa klucz API Fireworks AI. Można go uzyskać na", + "Your OpenAI API key. Get one at": "Państwa klucz API OpenAI. Można go uzyskać na", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Państwa opinia została zapisana. Opcjonalnie można podać dodatkowe szczegóły tutaj...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Państwa opinia została zapisana. opcjonalnie można podać dodatkowe szczegóły tutaj...", + "Zookeeper Hosts": "Hosty ZooKeeper", + "Zookeeper Port": "Port ZooKeeper", + "Zookeeper Settings (SolrCloud)": "Ustawienia ZooKeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Ciąg połączenia ZooKeeper dla SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Numer portu ZooKeeper (opcjonalny, domyślnie 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F OSTRZEŻENIE O TRWAŁYM USUNIĘCIU \\u26A0\\uFE0F\n\nZamierzają Państwo TRWALE usunąć WSZYSTKIE Obiekty dla Schematu \"{schema}\":\n\n\\u2022 Aktywne Obiekty: {active}\n\\u2022 Miękko usunięte Obiekty: {deleted}\n\\u2022 Razem: {total}\n\nTe Obiekty zostaną całkowicie usunięte z bazy danych i NIE BĘDZIE można ich odzyskać.\n\nCzy są Państwo absolutnie pewni?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F OSTRZEŻENIE O TRWAŁYM USUNIĘCIU \\u26A0\\uFE0F\n\nZamierzają Państwo TRWALE usunąć {count} miękko usuniętych Obiektów dla Schematu \"{schema}\".\n\nTe Obiekty zostaną całkowicie usunięte z bazy danych i NIE BĘDZIE można ich odzyskać.\n\nCzy są Państwo absolutnie pewni?", + "and accessible to all users in selected groups (or all users if no groups selected).": "i dostępne dla wszystkich użytkowników w wybranych grupach (lub wszystkich użytkowników, jeśli nie wybrano grup).", + "and add the files there.": "i dodać tam Pliki.", + "approved": "zatwierdzono", + "are only accessible to invited users.": "są dostępne tylko dla zaproszonych użytkowników.", + "by": "przez", + "chunk": "fragment", + "chunks": "fragmenty", + "chunks processed": "fragmentów przetworzono", + "completed": "zakończono", + "configuration(s)": "konfiguracja(e)", + "conversationId is required": "conversationId jest wymagane", + "create": "utwórz", + "delete": "usuń", + "deleted objects section": "sekcja usuniętych Obiektów", + "e.g., lib/Settings/config.json": "np. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "np. lib/Settings/register.json", + "e.g., lib/settings/config.json": "np. lib/settings/config.json", + "e.g., lib/settings/register.json": "np. lib/settings/register.json", + "execute": "wykonaj", + "failed": "nieudane", + "file": "plik", + "files": "pliki", + "file{plural}": "plik{plural}", + "folder": "folder", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "w {register}", + "in_progress": "w toku", + "inherited": "odziedziczone", + "list": "lista", + "log{plural}": "log{plural}", + "manage": "zarządzaj", + "members": "członkowie", + "menu or contact someone with permission to create agents.": "menu lub skontaktować się z kimś, kto ma uprawnienia do tworzenia agentów.", + "message content is required": "treść wiadomości jest wymagana", + "messages": "wiadomości", + "missing": "brakujące", + "myapp": "myapp", + "n8n URL and API key are required": "URL n8n i klucz API są wymagane", + "n8n configuration saved successfully": "Konfiguracja n8n zapisana pomyślnie", + "n8n connection not configured": "Połączenie n8n nie jest skonfigurowane", + "n8n connection successful": "Połączenie n8n udane", + "n8n connection test failed: {message}": "Test połączenia n8n nie powiódł się: {message}", + "n8n connection test successful": "Test połączenia n8n udany", + "n8n initialization failed: {message}": "Inicjalizacja n8n nie powiodła się: {message}", + "n8n integration disabled": "Integracja n8n wyłączona", + "n8n integration enabled": "Integracja n8n włączona", + "n8n project initialized successfully": "Projekt n8n zainicjowany pomyślnie", + "n8n settings saved successfully": "Ustawienia n8n zapisane pomyślnie", + "notify_push configuration guide": "przewodnik konfiguracji notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push jest zainstalowany, ale OpenRegister nie potwierdził jeszcze udanego pushu. Proszę wyzwolić zapis Obiektu, aby aktywować, lub sprawdzić konfigurację notify_push.", + "notify_push is installed but not yet active": "notify_push jest zainstalowany, ale nie jest jeszcze aktywny", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "obiekty", + "objects processed": "obiektów przetworzono", + "object{plural}": "obiekt{plural}", + "or pick a range": "lub wybrać zakres", + "org-...": "org-...", + "pending": "oczekujące", + "provides a good balance between speed and accuracy.": "zapewnia dobrą równowagę między szybkością a dokładnością.", + "public": "publiczne", + "read": "odczyt", + "register(s)": "rejestr(y)", + "register(s) selected": "rejestr(y) wybrano", + "register{plural}": "rejestr{plural}", + "rejected": "odrzucono", + "required": "wymagane", + "results": "wyniki", + "schema(s)": "schemat(y)", + "schema(s) selected": "schemat(y) wybrano", + "schema{plural}": "schemat{plural}", + "selected objects": "wybrane Obiekty", + "set": "ustaw", + "sk-...": "sk-...", + "this application": "ta aplikacja", + "to": "do", + "type must be \"positive\" or \"negative\"": "typ musi być \"positive\" lub \"negative\"", + "update": "aktualizuj", + "vectorized": "zwektoryzowano", + "view": "widok", + "views selected": "widoków wybrano", + "waiting": "oczekiwanie", + "{active} active, {deleted} deleted": "{active} aktywnych, {deleted} usuniętych", + "{count} audit trails cleared successfully": "{count} dzienników audytu wyczyszczono pomyślnie", + "{count} compliance issue(s) detected": "Wykryto {count} problem(ów) zgodności", + "{count} deletions": "{count} usunięć", + "{count} documents found": "Znaleziono {count} dokumentów", + "{count} email": "{count} email", + "{count} emails": "{count} emaili", + "{count} entries": "{count} wpisów", + "{count} issues": "{count} problemów", + "{count} members": "{count} członków", + "{count} searches": "{count} wyszukiwań", + "{count} selected": "{count} wybrano", + "{count} widget(s)": "{count} widżet(ów)", + "{matched} object(s) matched. {erased} erased.": "Dopasowano {matched} Obiekt(ów). Wymazano {erased}.", + "{minutes} minutes ago": "{minutes} minut temu", + "{percentage}% of objects": "{percentage}% Obiektów", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} w {register} / {schema}", + "{type} cache cleared successfully": "Pamięć podręczna {type} wyczyszczona pomyślnie", + "ℹ️ Current Configuration": "ℹ️ Bieżąca konfiguracja", + "ℹ️ current configuration": "ℹ️ bieżąca konfiguracja", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ NIEBEZPIECZEŃSTWO: Wszystkie {total} Obiektów zostanie TRWALE USUNIĘTYCH z bazy danych. Ta operacja jest NIEODWRACALNA i nie można jej cofnąć!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ OSTRZEŻENIE O TRWAŁYM USUNIĘCIU ⚠️\n\nZamierzają Państwo TRWALE usunąć WSZYSTKIE Obiekty dla Schematu \"{schema}\":\n\n• Aktywne Obiekty: {active}\n• Miękko usunięte Obiekty: {deleted}\n• Razem: {total}\n\nTe Obiekty zostaną całkowicie usunięte z bazy danych i NIE BĘDZIE można ich odzyskać.\n\nCzy są Państwo absolutnie pewni?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ OSTRZEŻENIE O TRWAŁYM USUNIĘCIU ⚠️\n\nZamierzają Państwo TRWALE usunąć {count} miękko usuniętych Obiektów dla Schematu \"{schema}\".\n\nTe Obiekty zostaną całkowicie usunięte z bazy danych i NIE BĘDZIE można ich odzyskać.\n\nCzy są Państwo absolutnie pewni?", + "⚡ Batch Processing": "⚡ Przetwarzanie wsadowe", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR może obsłużyć miliony dokumentów z czasem odpowiedzi wyszukiwania poniżej sekundy...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Wszechstronny kontekst ✓ Mniejsze prawdopodobieństwo pominięcia szczegółów ✗ Wolniejsze odpowiedzi ✗ Może zawierać mniej istotne informacje", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Szybsze odpowiedzi ✓ Bardziej ukierunkowane odpowiedzi ✗ Może pominąć istotne informacje", + "✨ AI Features": "✨ Funkcje AI", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 W trybie rozproszonym SOLR używa ZooKeeper do koordynacji klastra...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Wyszukiwanie fasetowe pozwala użytkownikom zagłębiać się w wyniki według kategorii...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Czy wiedzą Państwo? SOLR napędza wyszukiwanie dla Netflix, Apple i wielu innych dużych witryn!", + "💰 View Selection (Cost Optimization)": "💰 Wybór widoku (Optymalizacja kosztów)", + "📄 Object Serialization": "📄 Serializacja Obiektu", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR zapewnia bogatą analitykę i statystyki dotyczące wydajności wyszukiwania...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets zawierają pliki schematu i konfiguracji dla Państwa indeksu wyszukiwania...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikacja zapewnia dostępność Państwa indeksu wyszukiwania nawet w przypadku awarii węzłów...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR to potężna korporacyjna platforma wyszukiwania zbudowana na Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Ustawienia wektoryzacji", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automatyczne skalowanie może dynamicznie dodawać lub usuwać węzły w zależności od obciążenia...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Funkcje bezpieczeństwa obejmują uwierzytelnianie, autoryzację i szyfrowanie SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-maili" + ], + "Purge {count} object from database": [ + "Wyczyść {count} obiekt z bazy danych", + "Wyczyść {count} obiektów z bazy danych" + ], + "Restore {count} object": [ + "Przywróć {count} obiekt", + "Przywróć {count} obiektów" + ], + "Successfully restored {count} object": [ + "Pomyślnie przywrócono {count} obiekt", + "Pomyślnie przywrócono {count} obiektów" + ], + "Delete {count} object": [ + "Usuń {count} obiekt", + "Usuń {count} obiektów" + ], + "Object successfully deleted": [ + "Obiekt został pomyślnie usunięty", + "Obiekty zostały pomyślnie usunięte" + ] + } +} diff --git a/l10n/pt.js b/l10n/pt.js new file mode 100644 index 0000000000..60b81f770d --- /dev/null +++ b/l10n/pt.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(sem assunto)", + "(no title)" : "(sem título)", + "(unnamed)" : "(sem nome)", + "(untitled card)" : "(cartão sem título)", + "Activities" : "Atividades", + "Add Endpoint" : "Adicionar Endpoint", + "Add Source" : "Adicionar Fonte", + "Add card" : "Adicionar cartão", + "Add contact" : "Adicionar contacto", + "All clear" : "Tudo em ordem", + "Articles" : "Artigos", + "Code" : "Código", + "Compliance" : "Conformidade", + "Contacts" : "Contactos", + "Create Agent" : "Criar Agente", + "Create Application" : "Criar Aplicação", + "Create event" : "Criar evento", + "Erasure complete" : "Eliminação concluída", + "Erasure preview" : "Pré-visualização da eliminação", + "Expand details" : "Expandir detalhes", + "Save changes" : "Guardar alterações", + "📄 Object Serialization" : "📄 Serialização de Objetos", + "🔢 Vectorization Settings" : "🔢 Definições de Vetorização", + "💰 View Selection (Cost Optimization)" : "💰 Seleção de Vistas (Otimização de Custos)", + "AI Assistant" : "Assistente de IA", + "AI Chat" : "Conversa de IA", + "API Key" : "Chave de API", + "API tokens saved successfully" : "Tokens de API guardados com sucesso", + "About ConfigSets" : "Acerca dos ConfigSets", + "Action" : "Ação", + "Action Distribution" : "Distribuição de Ações", + "Action:" : "Ação:", + "Actions" : "Ações", + "Active" : "Ativo", + "Active Collections" : "Coleções Ativas", + "Active Filters" : "Filtros Ativos", + "Active Organisations" : "Organizações Ativas", + "Active filters:" : "Filtros ativos:", + "Active organisation set successfully" : "Organização ativa definida com sucesso", + "Activity Period" : "Período de Atividade", + "Add Groups" : "Adicionar Grupos", + "Add Schema" : "Adicionar Esquema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Adicione títulos de esquemas, descrições e informações de registo para fornecer um contexto mais rico para a pesquisa", + "Add to favorites" : "Adicionar aos favoritos", + "Added" : "Adicionado", + "Added to favorites" : "Adicionado aos favoritos", + "Additional Information" : "Informação Adicional", + "Additional feedback saved. Thank you!" : "Comentários adicionais guardados. Obrigado!", + "Admin" : "Administração", + "Advanced" : "Avançado", + "Advanced Filters" : "Filtros Avançados", + "Advanced Options" : "Opções Avançadas", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Os filtros avançados não estão disponíveis ao utilizar a fonte de base de dados. Mude para Auto ou Índice SOLR para obter opções de filtragem.", + "After" : "Depois", + "Agent deleted successfully" : "Agente eliminado com sucesso", + "Agents" : "Agentes", + "All" : "Todos", + "All Categories" : "Todas as Categorias", + "All Collections" : "Todas as Coleções", + "All Confidence Levels" : "Todos os Níveis de Confiança", + "All ConfigSets" : "Todos os ConfigSets", + "All Files" : "Todos os Ficheiros", + "All Levels" : "Todos os Níveis", + "All Types" : "Todos os Tipos", + "All Webhooks" : "Todos os Webhooks", + "All actions" : "Todas as ações", + "All audit trails cleared successfully" : "Todos os registos de auditoria foram limpos com sucesso", + "All registers" : "Todos os registos", + "All schemas" : "Todos os esquemas", + "All searches" : "Todas as pesquisas", + "All users" : "Todos os utilizadores", + "An error occurred while clearing audit trails" : "Ocorreu um erro ao limpar os registos de auditoria", + "An error occurred while deleting the audit trail" : "Ocorreu um erro ao eliminar o registo de auditoria", + "An error occurred while deleting the view" : "Ocorreu um erro ao eliminar a vista", + "An error occurred while permanently deleting the objects" : "Ocorreu um erro ao eliminar permanentemente os objetos", + "Analysis completed:" : "Análise concluída:", + "Analysis steps:" : "Passos da análise:", + "Analytics" : "Análises", + "Analyze Objects" : "Analisar Objetos", + "Analyze Properties" : "Analisar Propriedades", + "Analyze existing properties for improvement opportunities" : "Analisar propriedades existentes em busca de oportunidades de melhoria", + "Analyzing..." : "A analisar...", + "Any favorites and sharing settings for this view" : "Quaisquer favoritos e definições de partilha para esta vista", + "Any user" : "Qualquer utilizador", + "App store cache invalidated successfully" : "Cache da App Store invalidada com sucesso", + "Application deleted successfully" : "Aplicação eliminada com sucesso", + "Applications" : "Aplicações", + "Apply Changes" : "Aplicar Alterações", + "Archive" : "Arquivar", + "Archive conversation" : "Arquivar conversa", + "Archived conversations are hidden from your active list" : "As conversas arquivadas ficam ocultas da sua lista ativa", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Tem a certeza de que pretende limpar os registos de pesquisa antigos? Isto eliminará as entradas com mais de 30 dias.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Tem a certeza de que pretende eliminar os registos de auditoria selecionados? Esta ação não pode ser anulada.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Tem a certeza de que pretende eliminar os registos de pesquisa selecionados? Esta ação não pode ser anulada.", + "Are you sure you want to delete this ConfigSet?" : "Tem a certeza de que pretende eliminar este ConfigSet?", + "Are you sure you want to permanently delete" : "Tem a certeza de que pretende eliminar permanentemente", + "Ask a question..." : "Faça uma pergunta...", + "Ask questions about your data using natural language" : "Faça perguntas sobre os seus dados utilizando linguagem natural", + "Attempt" : "Tentativa", + "Audit Trail Changes" : "Alterações do Registo de Auditoria", + "Audit Trail Details" : "Detalhes do Registo de Auditoria", + "Audit Trail Management" : "Gestão do Registo de Auditoria", + "Audit Trail Statistics" : "Estatísticas do Registo de Auditoria", + "Audit Trails" : "Registos de Auditoria", + "Audit trail data copied to clipboard" : "Dados do registo de auditoria copiados para a área de transferência", + "Audit trail deleted successfully" : "Registo de auditoria eliminado com sucesso", + "Audit trail successfully deleted" : "Registo de auditoria eliminado com sucesso", + "Authentication" : "Autenticação", + "Auto-commit disabled" : "Confirmação automática desativada", + "Auto-commit enabled" : "Confirmação automática ativada", + "Auto-create Default Organisation" : "Criar Automaticamente Organização Predefinida", + "Auto-retry failed vectorizations" : "Repetir automaticamente vetorizações falhadas", + "Automatically commit changes to SOLR index" : "Confirmar automaticamente as alterações no índice SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Criar automaticamente uma organização predefinida se não existir nenhuma quando a aplicação é inicializada", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Gerar automaticamente embeddings vetoriais a partir de blocos de texto quando os ficheiros são carregados e processados", + "Automatically generate vector embeddings when objects are created or updated" : "Gerar automaticamente embeddings vetoriais quando os objetos são criados ou atualizados", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Repetir automaticamente as tentativas de vetorização falhadas (máx. 3 tentativas)", + "Available Filters" : "Filtros Disponíveis", + "Available Workflows" : "Fluxos de Trabalho Disponíveis", + "Avg Execution Time" : "Tempo Médio de Execução", + "Avg Members/Org" : "Média de Membros/Org", + "Avg Object Views/Session" : "Média de Visualizações de Objetos/Sessão", + "Avg Response Time" : "Tempo Médio de Resposta", + "Avg Results/Search" : "Média de Resultados/Pesquisa", + "Avg Searches/Session" : "Média de Pesquisas/Sessão", + "Back" : "Voltar", + "Back to Entities" : "Voltar às Entidades", + "Back to Registers" : "Voltar aos Registos", + "Back to Webhooks" : "Voltar aos Webhooks", + "Back to applications" : "Voltar às aplicações", + "Back to entities" : "Voltar às entidades", + "Backend updated successfully. Please reload the application." : "Backend atualizado com sucesso. Recarregue a aplicação.", + "Base URL (Optional)" : "URL Base (Opcional)", + "Basic Information" : "Informação Básica", + "Batch Size" : "Tamanho do Lote", + "Batch extraction completed" : "Extração em lote concluída", + "Before" : "Antes", + "Before object vectorization can work:" : "Antes de a vetorização de objetos poder funcionar:", + "Behavior Issue" : "Problema de Comportamento", + "Bookmarks" : "Marcadores", + "Blob storage has been retired. All objects now use magic tables." : "O armazenamento blob foi descontinuado. Todos os objetos utilizam agora tabelas mágicas.", + "Branch" : "Ramo", + "Bulk delete operation completed successfully" : "Operação de eliminação em massa concluída com sucesso", + "Bulk save operation completed successfully" : "Operação de gravação em massa concluída com sucesso", + "Business Data" : "Dados Empresariais", + "Cache cleared successfully" : "Cache limpa com sucesso", + "Calculate Sizes" : "Calcular Tamanhos", + "Cancel" : "Cancelar", + "Cannot delete: objects are still attached" : "Não é possível eliminar: ainda existem objetos anexados", + "Category" : "Categoria", + "Change Type" : "Tipo de Alteração", + "Changes" : "Alterações", + "Changes copied to clipboard" : "Alterações copiadas para a área de transferência", + "Chat Model" : "Modelo de Conversa", + "Chat Provider" : "Fornecedor de Conversa", + "Chat Provider (RAG)" : "Fornecedor de Conversa (RAG)", + "Chat Settings" : "Definições de Conversa", + "Chat provider connection successful!" : "Ligação ao fornecedor de conversa bem-sucedida!", + "Chat settings" : "Definições de conversa", + "Choose a register" : "Escolha um registo", + "Choose a schema" : "Escolha um esquema", + "Choose how vector similarity calculations are performed for semantic search" : "Escolha como os cálculos de semelhança vetorial são realizados para a pesquisa semântica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Escolha que tipos de ficheiro incluir no processo de vetorização. Apenas os ficheiros com texto extraído e blocos serão processados.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Escolha que vistas incluir no processo de vetorização. Deixe vazio para processar todas as vistas com base na sua configuração.", + "Chunk" : "Bloco", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "A eliminação de blocos ainda não foi implementada. Utilize endpoints baseados em blocos.", + "Chunks" : "Blocos", + "Chunks to Vectorize:" : "Blocos a Vetorizar:", + "Cleanup Old Trails" : "Limpar Registos Antigos", + "Cleanup completed" : "Limpeza concluída", + "Clear All Embeddings" : "Limpar Todos os Embeddings", + "Clear Entries" : "Limpar Entradas", + "Clear Filtered Audit Trails" : "Limpar Registos de Auditoria Filtrados", + "Clear Filters" : "Limpar Filtros", + "Clear Index" : "Limpar Índice", + "Clear Selection" : "Limpar Seleção", + "Clear all" : "Limpar tudo", + "Clear all filters" : "Limpar todos os filtros", + "Clear date range" : "Limpar intervalo de datas", + "Clearing..." : "A limpar...", + "Close" : "Fechar", + "Collection Management" : "Gestão de Coleções", + "Collection Name" : "Nome da Coleção", + "Collection assignments updated successfully" : "Atribuições de coleção atualizadas com sucesso", + "Collection cleared successfully" : "Coleção limpa com sucesso", + "Collection copied successfully" : "Coleção copiada com sucesso", + "Collection created successfully" : "Coleção criada com sucesso", + "Collection deleted successfully" : "Coleção eliminada com sucesso", + "Collection used to store and index file metadata and content" : "Coleção utilizada para armazenar e indexar metadados e conteúdo de ficheiros", + "Collection used to store and index object data" : "Coleção utilizada para armazenar e indexar dados de objetos", + "Collections:" : "Coleções:", + "Commit Message" : "Mensagem de Commit", + "Commit Within (ms)" : "Confirmar Dentro De (ms)", + "Commit message" : "Mensagem de commit", + "Compare current schema with real object data" : "Comparar o esquema atual com dados reais de objetos", + "Completed" : "Concluído", + "Complex" : "Complexo", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Consultas complexas: pesquisas avançadas com vários filtros, operadores e combinações complexas de parâmetros", + "Confidence Level" : "Nível de Confiança", + "Confidence Score" : "Pontuação de Confiança", + "Config must be provided as an object" : "A configuração tem de ser fornecida como um objeto", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Gestão de ConfigSets", + "ConfigSet Name" : "Nome do ConfigSet", + "ConfigSet created successfully" : "ConfigSet criado com sucesso", + "ConfigSet deleted successfully" : "ConfigSet eliminado com sucesso", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Os ConfigSets definem o esquema e a configuração das suas coleções SOLR. Contêm definições de campos, analisadores e outras definições de pesquisa.", + "Configuration" : "Configuração", + "Configuration Data" : "Dados de Configuração", + "Configuration saved" : "Configuração guardada", + "Configurations" : "Configurações", + "Configure Facets" : "Configurar Facetas", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Configure os fornecedores de Modelos de Linguagem de Grande Dimensão (LLM) para funcionalidades baseadas em IA, incluindo pesquisa semântica, embeddings e conversa.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Configure as definições básicas de ligação ao seu servidor SOLR, incluindo autenticação e opções de rede. Utilize os diálogos separados de Gestão de ConfigSets e Coleções para gerir cores e coleções.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Configure como os objetos da base de dados são convertidos em embeddings vetoriais para a pesquisa semântica. Os objetos são vetorizados diretamente sem necessidade de extração de texto.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Configure como os objetos são convertidos em texto antes da vetorização. Estas definições afetam a qualidade e o contexto da pesquisa.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Configure os parâmetros para a vetorização de ficheiros. Este processo gerará embeddings vetoriais para todos os blocos de ficheiros extraídos.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Configure os parâmetros para a vetorização de objetos. Este processo gerará embeddings vetoriais para todos os objetos que correspondam aos filtros da sua vista.", + "Configure which types of data to search and how many sources to retrieve" : "Configure que tipos de dados pesquisar e quantas fontes obter", + "Connection Failed" : "Ligação Falhada", + "Connection Settings" : "Definições de Ligação", + "Connection Successful!" : "Ligação Bem-Sucedida!", + "Connection failed" : "A ligação falhou", + "Connection protocol" : "Protocolo de ligação", + "Connection successful" : "Ligação bem-sucedida", + "Connection timeout in seconds" : "Tempo limite de ligação em segundos", + "Constant delay between retries (5 minutes)" : "Atraso constante entre tentativas (5 minutos)", + "Constraint Issue" : "Problema de Restrição", + "Content Filters" : "Filtros de Conteúdo", + "Continue to Properties" : "Continuar para as Propriedades", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Controle que vistas de objetos devem ser vetorizadas para reduzir os custos de API. Vetorize apenas as vistas que beneficiam da pesquisa semântica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Controle que vistas e ferramentas a IA pode utilizar nesta conversa. Por predefinição, todas as capacidades do agente estão ativadas.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Controla a aleatoriedade (0 = determinístico, 2 = muito criativo)", + "Conversation ID is required" : "O ID da conversa é obrigatório", + "Conversation archived" : "Conversa arquivada", + "Conversation archived successfully" : "Conversa arquivada com sucesso", + "Conversation cleared successfully" : "Conversa limpa com sucesso", + "Conversation deleted" : "Conversa eliminada", + "Conversation permanently deleted" : "Conversa eliminada permanentemente", + "Conversation renamed" : "Conversa renomeada", + "Conversation restored" : "Conversa restaurada", + "Conversation title" : "Título da conversa", + "Copied!" : "Copiado!", + "Copy" : "Copiar", + "Copy Changes" : "Copiar Alterações", + "Copy Collection" : "Copiar Coleção", + "Copy Data" : "Copiar Dados", + "Copy Full Data" : "Copiar Todos os Dados", + "Copying..." : "A copiar...", + "Count" : "Contagem", + "Counting objects..." : "A contar objetos...", + "Costs" : "Custos", + "Create" : "Criar", + "Create Collection" : "Criar Coleção", + "Create ConfigSet" : "Criar ConfigSet", + "Create New Collection" : "Criar Nova Coleção", + "Create New ConfigSet" : "Criar Novo ConfigSet", + "Create Operations" : "Operações de Criação", + "Create Webhook" : "Criar Webhook", + "Create a copy of collection:" : "Criar uma cópia da coleção:", + "Create a new ConfigSet based on the _default template" : "Criar um novo ConfigSet com base no modelo _default", + "Create a new SOLR collection from an existing ConfigSet" : "Criar uma nova coleção SOLR a partir de um ConfigSet existente", + "Create your first AI agent to get started." : "Crie o seu primeiro agente de IA para começar.", + "Created" : "Criado", + "Created:" : "Criado:", + "Creating..." : "A criar...", + "Current Filters" : "Filtros Atuais", + "Current Type:" : "Tipo Atual:", + "Custom API endpoint if using a different region" : "Endpoint de API personalizado se utilizar uma região diferente", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Cabeçalhos HTTP personalizados (um por linha, formato: Nome-Cabeçalho: valor)", + "Daily" : "Diariamente", + "Dashboard" : "Painel", + "Data Source" : "Fonte de Dados", + "Data sources" : "Fontes de dados", + "Data type does not match observed values" : "O tipo de dados não corresponde aos valores observados", + "Database URL" : "URL da Base de Dados", + "Database information refreshed" : "Informação da base de dados atualizada", + "Date Range" : "Intervalo de Datas", + "Default" : "Predefinição", + "Default Organisation" : "Organização Predefinida", + "Default: 5" : "Predefinição: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Os atrasos duplicam a cada tentativa (2, 4, 8 minutos...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Os atrasos aumentam linearmente (5, 10, 15 minutos...)", + "Delete" : "Eliminar", + "Delete Application" : "Eliminar Aplicação", + "Delete Audit Trail" : "Eliminar Registo de Auditoria", + "Delete Collection" : "Eliminar Coleção", + "Delete ConfigSet" : "Eliminar ConfigSet", + "Delete Objects" : "Eliminar Objetos", + "Delete Operations" : "Operações de Eliminação", + "Delete View" : "Eliminar Vista", + "Delete all objects in this schema" : "Eliminar todos os objetos neste esquema", + "Delete permanently" : "Eliminar permanentemente", + "Delete this conversation?" : "Eliminar esta conversa?", + "Delete view" : "Eliminar vista", + "Deleted" : "Eliminado", + "Deleted By" : "Eliminado Por", + "Deleted Date" : "Data de Eliminação", + "Deleted Items Management" : "Gestão de Itens Eliminados", + "Deleted This Week" : "Eliminados Esta Semana", + "Deleted Today" : "Eliminados Hoje", + "Deleted:" : "Eliminado:", + "Deleting..." : "A eliminar...", + "Deletion Date Range" : "Intervalo de Datas de Eliminação", + "Deletion Statistics" : "Estatísticas de Eliminação", + "Deprecated" : "Descontinuado", + "Description" : "Descrição", + "Description:" : "Descrição:", + "Detect data types and patterns" : "Detetar tipos de dados e padrões", + "Detected At" : "Detetado Em", + "Detected Format:" : "Formato Detetado:", + "Detected Issues:" : "Problemas Detetados:", + "Disable" : "Desativar", + "Disabled" : "Desativado", + "Discover Files" : "Descobrir Ficheiros", + "Discovered Properties" : "Propriedades Descobertas", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Pretende eliminar permanentemente todas as entradas de registo de auditoria filtradas? Esta ação não pode ser anulada.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Pretende eliminar permanentemente esta entrada de registo de auditoria? Esta ação não pode ser anulada.", + "Documents" : "Documentos", + "Download API Spec" : "Transferir Especificação da API", + "Edit" : "Editar", + "Edit Register" : "Editar Registo", + "Edit Schema" : "Editar Esquema", + "Edit View" : "Editar Vista", + "Edit Webhook" : "Editar Webhook", + "Edit view" : "Editar vista", + "Edit view details" : "Editar detalhes da vista", + "Effectiveness" : "Eficácia", + "Email" : "Email", + "Emails" : "Emails", + "Embedding Model" : "Modelo de Embedding", + "Embedding Provider" : "Fornecedor de Embedding", + "Embedding generated successfully" : "Embedding gerado com sucesso", + "Embedding provider connection successful!" : "Ligação ao fornecedor de embedding bem-sucedida!", + "Enable" : "Ativar", + "Enable auto-creation" : "Ativar criação automática", + "Enable automatic file vectorization" : "Ativar vetorização automática de ficheiros", + "Enable automatic object vectorization" : "Ativar vetorização automática de objetos", + "Enable automatic synchronization" : "Ativar sincronização automática", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Ativar registo detalhado para operações SOLR (recomendado para depuração)", + "Enable faceting" : "Ativar facetação", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Ativar ou desativar as funcionalidades LLM. Configure os fornecedores e modelos utilizando o botão Configuração LLM acima.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Ativar ou desativar a integração de pesquisa SOLR. Configure as definições de ligação utilizando o botão Definições de Ligação acima.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Ativar ou desativar a integração de fluxos de trabalho n8n. Configure as definições de ligação abaixo.", + "Enable or disable this webhook" : "Ativar ou desativar este webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Ativar vetorização para todas as vistas existentes e futuras (pode aumentar os custos)", + "Enabled" : "Ativado", + "Endpoints" : "Endpoints", + "Enter ConfigSet name" : "Introduza o nome do ConfigSet", + "Enter collection name" : "Introduza o nome da coleção", + "Enter description (optional)..." : "Introduza a descrição (opcional)...", + "Enter new collection name" : "Introduza o novo nome da coleção", + "Enter object ID" : "Introduza o ID do objeto", + "Enter search term" : "Introduza o termo de pesquisa", + "Enter view name..." : "Introduza o nome da vista...", + "Enter webhook name" : "Introduza o nome do webhook", + "Entities" : "Entidades", + "Entity Information" : "Informação da Entidade", + "Entity deleted successfully" : "Entidade eliminada com sucesso", + "Entity not found" : "Entidade não encontrada", + "Entries to be deleted:" : "Entradas a eliminar:", + "Enum Issue" : "Problema de Enumeração", + "Enum constraint is missing" : "Falta a restrição de enumeração", + "Error" : "Erro", + "Error Details" : "Detalhes do Erro", + "Error Information" : "Informação do Erro", + "Error Message" : "Mensagem de Erro", + "Error loading application" : "Erro ao carregar a aplicação", + "Error loading audit trails" : "Erro ao carregar os registos de auditoria", + "Error loading entity" : "Erro ao carregar a entidade", + "Error loading search trails" : "Erro ao carregar os registos de pesquisa", + "Estimated Batches:" : "Lotes Estimados:", + "Estimated Cost:" : "Custo Estimado:", + "Estimated Duration:" : "Duração Estimada:", + "Event" : "Evento", + "Event Property for Payload" : "Propriedade de Evento para o Payload", + "Events" : "Eventos", + "Example Value" : "Valor de Exemplo", + "Exclusive Maximum" : "Máximo Exclusivo", + "Exclusive Minimum" : "Mínimo Exclusivo", + "Execution Mode" : "Modo de Execução", + "Execution Time" : "Tempo de Execução", + "Execution Time Range" : "Intervalo de Tempo de Execução", + "Existing Improvements" : "Melhorias Existentes", + "Exponential" : "Exponencial", + "Export" : "Exportar", + "Export completed successfully" : "Exportação concluída com sucesso", + "Export, view, or delete audit trails" : "Exportar, ver ou eliminar registos de auditoria", + "Extend Schema" : "Estender Esquema", + "Extended" : "Estendido", + "Extensions" : "Extensões", + "Extract Pending Files" : "Extrair Ficheiros Pendentes", + "Extract properties from each object" : "Extrair propriedades de cada objeto", + "Extracted At" : "Extraído Em", + "Extraction Status" : "Estado da Extração", + "Facet configuration updated successfully" : "Configuração de facetas atualizada com sucesso", + "Facets discovered and configured successfully" : "Facetas descobertas e configuradas com sucesso", + "Facets discovered successfully" : "Facetas descobertas com sucesso", + "Failed" : "Falhado", + "Failed to analyze schema properties" : "Falha ao analisar as propriedades do esquema", + "Failed to archive conversation" : "Falha ao arquivar a conversa", + "Failed to calculate sizes" : "Falha ao calcular os tamanhos", + "Failed to clear collection" : "Falha ao limpar a coleção", + "Failed to copy changes" : "Falha ao copiar as alterações", + "Failed to copy data" : "Falha ao copiar os dados", + "Failed to copy data to clipboard" : "Falha ao copiar os dados para a área de transferência", + "Failed to create conversation" : "Falha ao criar a conversa", + "Failed to create or find project" : "Falha ao criar ou encontrar o projeto", + "Failed to delete collection" : "Falha ao eliminar a coleção", + "Failed to delete conversation" : "Falha ao eliminar a conversa", + "Failed to delete webhook" : "Falha ao eliminar o webhook", + "Failed to download API specification" : "Falha ao transferir a especificação da API", + "Failed to get SOLR field configuration" : "Falha ao obter a configuração de campos SOLR", + "Failed to get entity categories" : "Falha ao obter as categorias de entidades", + "Failed to get entity statistics" : "Falha ao obter as estatísticas de entidades", + "Failed to get entity types" : "Falha ao obter os tipos de entidades", + "Failed to load LLM configuration" : "Falha ao carregar a configuração LLM", + "Failed to load Nextcloud groups" : "Falha ao carregar os grupos do Nextcloud", + "Failed to load conversation" : "Falha ao carregar a conversa", + "Failed to load entities" : "Falha ao carregar as entidades", + "Failed to load entity" : "Falha ao carregar a entidade", + "Failed to load files" : "Falha ao carregar os ficheiros", + "Failed to load organisations" : "Falha ao carregar as organizações", + "Failed to load templates" : "Falha ao carregar os modelos", + "Failed to load webhooks" : "Falha ao carregar os webhooks", + "Failed to load workflows" : "Falha ao carregar os fluxos de trabalho", + "Failed to refresh database information" : "Falha ao atualizar a informação da base de dados", + "Failed to reindex collection" : "Falha ao reindexar a coleção", + "Failed to rename conversation" : "Falha ao renomear a conversa", + "Failed to restore conversation" : "Falha ao restaurar a conversa", + "Failed to retry extraction" : "Falha ao repetir a extração", + "Failed to retry webhook" : "Falha ao repetir o webhook", + "Failed to save GitHub token" : "Falha ao guardar o token do GitHub", + "Failed to save GitLab URL" : "Falha ao guardar o URL do GitLab", + "Failed to save GitLab token" : "Falha ao guardar o token do GitLab", + "Failed to save additional feedback" : "Falha ao guardar os comentários adicionais", + "Failed to save n8n configuration" : "Falha ao guardar a configuração do n8n", + "Failed to save roles" : "Falha ao guardar os papéis", + "Failed to save settings" : "Falha ao guardar as definições", + "Failed to save webhook" : "Falha ao guardar o webhook", + "Failed to send feedback" : "Falha ao enviar os comentários", + "Failed to test webhook" : "Falha ao testar o webhook", + "Failed to update favorite status" : "Falha ao atualizar o estado de favorito", + "Failed to update schema properties" : "Falha ao atualizar as propriedades do esquema", + "Failed to update view" : "Falha ao atualizar a vista", + "Failed to update webhook" : "Falha ao atualizar o webhook", + "Feedback recorded" : "Comentários registados", + "Fewer sources (1-3):" : "Menos fontes (1-3):", + "Field" : "Campo", + "File" : "Ficheiro", + "File Chunk Prediction" : "Previsão de Blocos de Ficheiro", + "File Collection" : "Coleção de Ficheiros", + "File Management" : "Gestão de Ficheiros", + "File Name" : "Nome do Ficheiro", + "File Path" : "Caminho do Ficheiro", + "File Type Selection" : "Seleção de Tipo de Ficheiro", + "File Vectorization" : "Vetorização de Ficheiros", + "File Warmup" : "Aquecimento de Ficheiros", + "File actions menu" : "Menu de ações do ficheiro", + "File anonymized successfully" : "Ficheiro anonimizado com sucesso", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "A vetorização de blocos de ficheiro ainda não foi implementada. Os blocos estão prontos e armazenados, mas o serviço de vetorização está em desenvolvimento.", + "File collection not configured" : "Coleção de ficheiros não configurada", + "File discovery completed" : "Descoberta de ficheiros concluída", + "File extraction completed" : "Extração de ficheiros concluída", + "File extraction queued" : "Extração de ficheiros em fila de espera", + "File indexed successfully" : "Ficheiro indexado com sucesso", + "File is already anonymized" : "O ficheiro já está anonimizado", + "File not found" : "Ficheiro não encontrado", + "File settings updated successfully" : "Definições do ficheiro atualizadas com sucesso", + "File sources" : "Fontes de ficheiros", + "File vectorization configuration saved successfully" : "Configuração de vetorização de ficheiros guardada com sucesso", + "File vectorization started. Check the statistics section for progress." : "Vetorização de ficheiros iniciada. Consulte a secção de estatísticas para acompanhar o progresso.", + "File warmup completed" : "Aquecimento de ficheiros concluído", + "Files" : "Ficheiros", + "Files with Completed Extraction:" : "Ficheiros com Extração Concluída:", + "Files → fileCollection, Objects → objectCollection" : "Ficheiros → fileCollection, Objetos → objectCollection", + "Filter Audit Trails" : "Filtrar Registos de Auditoria", + "Filter Deleted Items" : "Filtrar Itens Eliminados", + "Filter Objects" : "Filtrar Objetos", + "Filter Properties" : "Filtrar Propriedades", + "Filter Search Trails" : "Filtrar Registos de Pesquisa", + "Filter Statistics" : "Filtrar Estatísticas", + "Filter and analyze search trail entries" : "Filtrar e analisar as entradas de registo de pesquisa", + "Filter and manage audit trail entries" : "Filtrar e gerir as entradas de registo de auditoria", + "Filter and manage soft deleted items" : "Filtrar e gerir itens eliminados de forma reversível", + "Filter by object ID" : "Filtrar por ID do objeto", + "Filter by search term" : "Filtrar por termo de pesquisa", + "Filter by webhook" : "Filtrar por webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Dados de filtro carregados automaticamente. Utilize os filtros abaixo para refinar a sua pesquisa.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrar acionadores de webhook por propriedades do payload (um por linha, formato: chave: valor)", + "Filtered" : "Filtrado", + "Filters" : "Filtros", + "Fireworks AI Chat Settings" : "Definições de Conversa do Fireworks AI", + "Fireworks AI Embedding Configuration" : "Configuração de Embedding do Fireworks AI", + "First" : "Primeiro", + "Fixed" : "Fixo", + "For chat and retrieval-augmented generation" : "Para conversa e geração aumentada por recuperação", + "For vector embeddings and semantic search" : "Para embeddings vetoriais e pesquisa semântica", + "Format" : "Formato", + "Format Issue" : "Problema de Formato", + "Format constraint is missing" : "Falta a restrição de formato", + "From Date" : "Data de Início", + "From date" : "Data de início", + "Full data copied to clipboard" : "Todos os dados copiados para a área de transferência", + "General Issue" : "Problema Geral", + "Generate recommendations and confidence scores" : "Gerar recomendações e pontuações de confiança", + "Generate vectors immediately when new objects are created" : "Gerar vetores imediatamente quando são criados novos objetos", + "GitHub token is valid" : "O token do GitHub é válido", + "GitHub token removed successfully" : "Token do GitHub removido com sucesso", + "GitHub token saved successfully" : "Token do GitHub guardado com sucesso", + "GitLab URL saved successfully" : "URL do GitLab guardado com sucesso", + "GitLab token is valid" : "O token do GitLab é válido", + "GitLab token saved successfully" : "Token do GitLab guardado com sucesso", + "HTTP Method" : "Método HTTP", + "HTTP method used to send webhook requests" : "Método HTTP utilizado para enviar pedidos de webhook", + "Headers" : "Cabeçalhos", + "Health" : "Estado de Saúde", + "Heartbeat successful - connection kept alive" : "Pulsação bem-sucedida - ligação mantida ativa", + "Helpful" : "Útil", + "Hide Filters" : "Ocultar Filtros", + "Hide in forms" : "Ocultar nos formulários", + "Hide in list view" : "Ocultar na vista de lista", + "High" : "Alto", + "High Confidence" : "Confiança Alta", + "Host" : "Anfitrião", + "Hourly" : "De hora a hora", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Quão profundamente percorrer as propriedades de objetos aninhados (1-20). Valores mais altos capturam mais detalhe mas aumentam o tamanho do vetor.", + "How often to check for updates (1-168 hours)" : "Com que frequência verificar atualizações (1-168 horas)", + "How to handle retries for failed webhook deliveries" : "Como tratar as tentativas para entregas de webhook falhadas", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Limites de taxa de IP limpos com sucesso", + "Identify properties not in the schema" : "Identificar propriedades que não estão no esquema", + "Immutable" : "Imutável", + "Import" : "Importar", + "Import successful" : "Importação bem-sucedida", + "Improved Property" : "Propriedade Melhorada", + "In use" : "Em utilização", + "Inactive" : "Inativo", + "Include IDs and names of related objects for better contextual search" : "Incluir IDs e nomes de objetos relacionados para uma melhor pesquisa contextual", + "Include related object references" : "Incluir referências de objetos relacionados", + "Include schema and register metadata" : "Incluir metadados do esquema e do registo", + "Initialization failed" : "A inicialização falhou", + "Initialize Project" : "Inicializar Projeto", + "Inspect Fields" : "Inspecionar Campos", + "Inspect Index" : "Inspecionar Índice", + "Invalid" : "Inválido", + "Invalid batch size. Must be between 1 and 5000" : "Tamanho de lote inválido. Tem de estar entre 1 e 5000", + "Invalid field name provided" : "Nome de campo fornecido inválido", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects inválido. Tem de ser 0 (todos) ou um número positivo", + "Invalid:" : "Inválido:", + "Issue" : "Problema", + "Items per page:" : "Itens por página:", + "Just now" : "Agora mesmo", + "LLM Configuration" : "Configuração LLM", + "LLM actions menu" : "Menu de ações LLM", + "LLM configuration saved successfully" : "Configuração LLM guardada com sucesso", + "LLM features disabled" : "Funcionalidades LLM desativadas", + "LLM features enabled" : "Funcionalidades LLM ativadas", + "LLM must be enabled with an embedding provider configured" : "O LLM tem de estar ativado com um fornecedor de embedding configurado", + "LLM settings updated successfully" : "Definições LLM atualizadas com sucesso", + "Last" : "Último", + "Last Triggered" : "Último Acionamento", + "Length Range:" : "Intervalo de Comprimento:", + "Linear" : "Linear", + "Load Advanced Filters" : "Carregar Filtros Avançados", + "Load advanced filters with live data from your search index" : "Carregar filtros avançados com dados em tempo real do seu índice de pesquisa", + "Load view" : "Carregar vista", + "Loading ConfigSets..." : "A carregar ConfigSets...", + "Loading advanced filters..." : "A carregar filtros avançados...", + "Loading agents..." : "A carregar agentes...", + "Loading application details..." : "A carregar detalhes da aplicação...", + "Loading applications..." : "A carregar aplicações...", + "Loading archived conversations..." : "A carregar conversas arquivadas...", + "Loading audit trails..." : "A carregar registos de auditoria...", + "Loading collections..." : "A carregar coleções...", + "Loading configurations..." : "A carregar configurações...", + "Loading conversation..." : "A carregar conversa...", + "Loading conversations..." : "A carregar conversas...", + "Loading deleted items..." : "A carregar itens eliminados...", + "Loading events..." : "A carregar eventos...", + "Loading filter data..." : "A carregar dados de filtro...", + "Loading groups..." : "A carregar grupos...", + "Loading log details..." : "A carregar detalhes do registo...", + "Loading registers..." : "A carregar registos...", + "Loading schemas..." : "A carregar esquemas...", + "Loading search trails..." : "A carregar registos de pesquisa...", + "Loading sources..." : "A carregar fontes...", + "Loading statistics..." : "A carregar estatísticas...", + "Loading users..." : "A carregar utilizadores...", + "Loading views..." : "A carregar vistas...", + "Loading..." : "A carregar...", + "Local" : "Local", + "Local Version" : "Versão Local", + "Locked" : "Bloqueado", + "Locked:" : "Bloqueado:", + "Login successful" : "Início de sessão bem-sucedido", + "Logs" : "Registos", + "Low" : "Baixo", + "Low Confidence" : "Confiança Baixa", + "Lucene Version:" : "Versão do Lucene:", + "Magic table synchronized successfully" : "Tabela mágica sincronizada com sucesso", + "Manage Organisation Roles" : "Gerir Papéis da Organização", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Gerir Coleções SOLR (armazenamentos de dados) e atribuí-las a objetos e ficheiros.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Gerir ConfigSets SOLR (modelos de configuração) para as suas coleções.", + "Manage and configure agents for automated tasks" : "Gerir e configurar agentes para tarefas automatizadas", + "Manage and monitor file text extraction status" : "Gerir e monitorizar o estado da extração de texto de ficheiros", + "Manage and restore soft deleted items from your registers" : "Gerir e restaurar itens eliminados de forma reversível dos seus registos", + "Manage and view detected entities from files and objects" : "Gerir e ver entidades detetadas a partir de ficheiros e objetos", + "Manage document templates and themes" : "Gerir modelos e temas de documentos", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Gerir fluxos de trabalho n8n para a automatização do OpenRegister. Os fluxos de trabalho serão armazenados no projeto configurado.", + "Manage webhooks for event-driven integrations" : "Gerir webhooks para integrações orientadas por eventos", + "Manage your applications and modules" : "Gira as suas aplicações e módulos", + "Manage your data registers and their configurations" : "Gira os seus registos de dados e as respetivas configurações", + "Manage your data schemas and their properties" : "Gira os seus esquemas de dados e as respetivas propriedades", + "Manage your data sources and their configurations" : "Gira as suas fontes de dados e as respetivas configurações", + "Manage your saved search configurations" : "Gira as suas configurações de pesquisa guardadas", + "Manage your system configurations and settings" : "Gira as configurações e definições do seu sistema", + "Managed" : "Gerido", + "Max Files (0 = all)" : "Máx. de Ficheiros (0 = todos)", + "Max Length" : "Comprimento Máx.", + "Max Length:" : "Comprimento Máx.:", + "Max Objects (0 = all)" : "Máx. de Objetos (0 = todos)", + "Max Retries" : "Máx. de Tentativas", + "Max Shards/Node" : "Máx. de Shards/Nó", + "Max execution time (ms)" : "Tempo máximo de execução (ms)", + "Max ms" : "Máx. ms", + "Max result count" : "Contagem máxima de resultados", + "Max results" : "Máx. de resultados", + "Maximum" : "Máximo", + "Maximum Nesting Depth" : "Profundidade Máxima de Aninhamento", + "Maximum length constraint is missing" : "Falta a restrição de comprimento máximo", + "Maximum length is too restrictive" : "O comprimento máximo é demasiado restritivo", + "Maximum number of files to process. Set to 0 to process all files." : "Número máximo de ficheiros a processar. Defina como 0 para processar todos os ficheiros.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Número máximo de objetos a processar. Defina como 0 para processar todos os objetos.", + "Maximum number of retry attempts for failed deliveries" : "Número máximo de tentativas para entregas falhadas", + "Maximum time to wait before committing changes" : "Tempo máximo de espera antes de confirmar as alterações", + "Maximum tokens to generate" : "Número máximo de tokens a gerar", + "Maximum value constraint is missing" : "Falta a restrição de valor máximo", + "Maximum value is too restrictive" : "O valor máximo é demasiado restritivo", + "Medium" : "Médio", + "Medium Confidence" : "Confiança Média", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Consultas médias: pesquisas com alguma filtragem ou vários parâmetros (por exemplo, intervalos de datas, registos/esquemas específicos)", + "Members of selected groups can access this view" : "Os membros dos grupos selecionados podem aceder a esta vista", + "Members:" : "Membros:", + "Memory prediction calculated successfully" : "Previsão de memória calculada com sucesso", + "Message content is required" : "O conteúdo da mensagem é obrigatório", + "Message does not belong to this conversation" : "A mensagem não pertence a esta conversa", + "Metadata Filters" : "Filtros de Metadados", + "Method" : "Método", + "Min Length" : "Comprimento Mín.", + "Min execution time (ms)" : "Tempo mínimo de execução (ms)", + "Min ms" : "Mín. ms", + "Min result count" : "Contagem mínima de resultados", + "Min results" : "Mín. de resultados", + "Minimum" : "Mínimo", + "Minimum value constraint is missing" : "Falta a restrição de valor mínimo", + "Minimum value is too restrictive" : "O valor mínimo é demasiado restritivo", + "Missing object fields created successfully" : "Campos de objeto em falta criados com sucesso", + "Mode:" : "Modo:", + "Model" : "Modelo", + "Model Name" : "Nome do Modelo", + "Modified" : "Modificado", + "Monthly" : "Mensalmente", + "More sources (10-20):" : "Mais fontes (10-20):", + "Most Active Objects" : "Objetos Mais Ativos", + "Multiple Of" : "Múltiplo De", + "Name" : "Nome", + "Names cache warmup completed" : "Aquecimento da cache de nomes concluído", + "New Collection Name" : "Nome da Nova Coleção", + "New Conversation" : "Nova Conversa", + "New Properties" : "Novas Propriedades", + "New Property" : "Nova Propriedade", + "New Value" : "Novo Valor", + "New users without specific organisation membership will be automatically added to this organisation" : "Os novos utilizadores sem associação a uma organização específica serão automaticamente adicionados a esta organização", + "Next" : "Seguinte", + "No" : "Não", + "No ConfigSets found" : "Nenhum ConfigSet encontrado", + "No GitHub token configured" : "Nenhum token do GitHub configurado", + "No GitHub token provided" : "Nenhum token do GitHub fornecido", + "No GitLab token provided" : "Nenhum token do GitLab fornecido", + "No Organisation" : "Sem Organização", + "No active filters" : "Sem filtros ativos", + "No activity data available" : "Não existem dados de atividade disponíveis", + "No agents available" : "Nenhum agente disponível", + "No agents found" : "Nenhum agente encontrado", + "No app store cache exists yet" : "Ainda não existe cache da App Store", + "No applications are available." : "Não existem aplicações disponíveis.", + "No applications found" : "Nenhuma aplicação encontrada", + "No archived conversations" : "Sem conversas arquivadas", + "No audit trail entries found" : "Nenhuma entrada de registo de auditoria encontrada", + "No changes recorded" : "Nenhuma alteração registada", + "No chunks to vectorize" : "Sem blocos para vetorizar", + "No collections found" : "Nenhuma coleção encontrada", + "No configuration data" : "Sem dados de configuração", + "No configurations are available." : "Não existem configurações disponíveis.", + "No configurations found" : "Nenhuma configuração encontrada", + "No configurations found for this application." : "Nenhuma configuração encontrada para esta aplicação.", + "No conversations yet. Create a new one to get started!" : "Ainda não existem conversas. Crie uma nova para começar!", + "No data available" : "Não existem dados disponíveis", + "No data available for chart" : "Não existem dados disponíveis para o gráfico", + "No deleted items found" : "Nenhum item eliminado encontrado", + "No deletion data available" : "Não existem dados de eliminação disponíveis", + "No description found" : "Nenhuma descrição encontrada", + "No description provided" : "Nenhuma descrição fornecida", + "No entities detected in this file. Run text extraction first." : "Não foram detetadas entidades neste ficheiro. Execute primeiro a extração de texto.", + "No entities found" : "Nenhuma entidade encontrada", + "No entities have been detected yet" : "Ainda não foram detetadas entidades", + "No events found" : "Nenhum evento encontrado", + "No expired audit trails found to clear" : "Não foram encontrados registos de auditoria expirados para limpar", + "No facetable fields available. Select a register and schema to see available filters." : "Não existem campos facetáveis disponíveis. Selecione um registo e um esquema para ver os filtros disponíveis.", + "No files found" : "Nenhum ficheiro encontrado", + "No files have been extracted yet" : "Ainda não foram extraídos ficheiros", + "No files to process" : "Sem ficheiros para processar", + "No files to reindex" : "Sem ficheiros para reindexar", + "No filters are currently active. This will delete ALL audit trail entries!" : "Não existem filtros ativos atualmente. Isto eliminará TODAS as entradas de registo de auditoria!", + "No log entries found" : "Nenhuma entrada de registo encontrada", + "No logs are available for this configuration." : "Não existem registos disponíveis para esta configuração.", + "No logs are available for this source." : "Não existem registos disponíveis para esta fonte.", + "No logs found" : "Nenhum registo encontrado", + "No mismatched fields found - SOLR schema is properly configured" : "Não foram encontrados campos incompatíveis - o esquema SOLR está corretamente configurado", + "No objects found" : "Nenhum objeto encontrado", + "No objects found to vectorize" : "Nenhum objeto encontrado para vetorizar", + "No objects selected for deletion" : "Nenhum objeto selecionado para eliminação", + "No objects to delete" : "Sem objetos para eliminar", + "No objects were permanently deleted" : "Nenhum objeto foi eliminado permanentemente", + "No properties found" : "Nenhuma propriedade encontrada", + "No properties match your filters." : "Nenhuma propriedade corresponde aos seus filtros.", + "No purge date set" : "Nenhuma data de expurgo definida", + "No register data available" : "Não existem dados de registo disponíveis", + "No registers found" : "Nenhum registo encontrado", + "No registers found for this application." : "Nenhum registo encontrado para esta aplicação.", + "No relations found" : "Nenhuma relação encontrada", + "No request body available" : "Não existe corpo de pedido disponível", + "No response body available" : "Não existe corpo de resposta disponível", + "No saved views yet. Create one in the Search tab!" : "Ainda não existem vistas guardadas. Crie uma no separador Pesquisa!", + "No schema data available" : "Não existem dados de esquema disponíveis", + "No schema selected for exploration" : "Nenhum esquema selecionado para exploração", + "No schemas found" : "Nenhum esquema encontrado", + "No schemas found for this application." : "Nenhum esquema encontrado para esta aplicação.", + "No search terms data available" : "Não existem dados de termos de pesquisa disponíveis", + "No search trail entries found" : "Nenhuma entrada de registo de pesquisa encontrada", + "No sources are available." : "Não existem fontes disponíveis.", + "No sources found" : "Nenhuma fonte encontrada", + "No templates found" : "Nenhum modelo encontrado", + "No templates have been created yet" : "Ainda não foram criados modelos", + "No valid magic-mapped register+schema combinations found" : "Não foram encontradas combinações válidas de registo+esquema mapeados magicamente", + "No views found. Create views first before configuring vectorization." : "Nenhuma vista encontrada. Crie vistas primeiro antes de configurar a vetorização.", + "No views match your search" : "Nenhuma vista corresponde à sua pesquisa", + "No webhooks found" : "Nenhum webhook encontrado", + "No webhooks have been configured yet" : "Ainda não foram configurados webhooks", + "No workflows found in this project. Create workflows in the n8n editor." : "Nenhum fluxo de trabalho encontrado neste projeto. Crie fluxos de trabalho no editor n8n.", + "None" : "Nenhum", + "Not helpful" : "Não útil", + "Not in use" : "Não está em utilização", + "Number Constraints" : "Restrições Numéricas", + "Number of chunks to vectorize in one API call (1-100)." : "Número de blocos a vetorizar numa chamada de API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Número de blocos a vetorizar numa chamada de API. Maior = mais rápido mas mais memória. Recomendado: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Número de objetos a processar em cada lote (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Número de objetos a vetorizar numa chamada de API. Maior = mais rápido mas mais memória. Recomendado: 10-50.", + "Numeric Range:" : "Intervalo Numérico:", + "Object" : "Objeto", + "Object Collection" : "Coleção de Objetos", + "Object Count Prediction" : "Previsão de Contagem de Objetos", + "Object ID" : "ID do Objeto", + "Object Management" : "Gestão de Objetos", + "Object Vectorization" : "Vetorização de Objetos", + "Object Warmup" : "Aquecimento de Objetos", + "Object collection not configured" : "Coleção de objetos não configurada", + "Object not found" : "Objeto não encontrado", + "Object permanently deleted" : "Objeto eliminado permanentemente", + "Object restored successfully" : "Objeto restaurado com sucesso", + "Object settings updated successfully" : "Definições do objeto atualizadas com sucesso", + "Object sources" : "Fontes de objetos", + "Object unlocked successfully" : "Objeto desbloqueado com sucesso", + "Object vectorization configuration saved successfully" : "Configuração de vetorização de objetos guardada com sucesso", + "Object vectorized successfully" : "Objeto vetorizado com sucesso", + "Object:" : "Objeto:", + "Objects" : "Objetos", + "Objects Analyzed" : "Objetos Analisados", + "Objects Distribution" : "Distribuição de Objetos", + "Objects being analyzed" : "Objetos em análise", + "Objects by Register" : "Objetos por Registo", + "Objects by Schema" : "Objetos por Esquema", + "Objects deletion completed successfully" : "Eliminação de objetos concluída com sucesso", + "Objects in schema" : "Objetos no esquema", + "Objects to Process:" : "Objetos a Processar:", + "Objects to be analyzed" : "Objetos a analisar", + "Objects to be deleted" : "Objetos a eliminar", + "Objects to be validated" : "Objetos a validar", + "Objects will be serialized as JSON text before vectorization" : "Os objetos serão serializados como texto JSON antes da vetorização", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Os objetos serão eliminados de forma reversível (marcados como eliminados mas mantidos na base de dados). Podem ser recuperados mais tarde, se necessário.", + "Old Value" : "Valor Antigo", + "Oldest Item (days)" : "Item Mais Antigo (dias)", + "Ollama Chat Settings" : "Definições de Conversa do Ollama", + "Ollama Embedding Configuration" : "Configuração de Embedding do Ollama", + "Ollama URL" : "URL do Ollama", + "Only With Changes" : "Apenas Com Alterações", + "Open Register Objects" : "Objetos do Open Register", + "Open n8n Editor" : "Abrir Editor n8n", + "OpenAI Chat Settings" : "Definições de Conversa do OpenAI", + "OpenAI Embedding Configuration" : "Configuração de Embedding do OpenAI", + "OpenRegister Settings" : "Definições do OpenRegister", + "Optional webhook secret for signature verification" : "Segredo de webhook opcional para verificação de assinatura", + "Order" : "Ordem", + "Organisation" : "Organização", + "Organisation ID" : "ID da Organização", + "Organisation Members" : "Membros da Organização", + "Organisation Statistics" : "Estatísticas da Organização", + "Organisation created successfully" : "Organização criada com sucesso", + "Organisation settings saved successfully" : "Definições da organização guardadas com sucesso", + "Organisations" : "Organizações", + "Organization" : "Organização", + "Organization ID (Optional)" : "ID da Organização (Opcional)", + "Orphaned Items" : "Itens Órfãos", + "Owner" : "Proprietário", + "Owner:" : "Proprietário:", + "Parallel Mode (Faster, more resource intensive)" : "Modo Paralelo (Mais rápido, mais intensivo em recursos)", + "Parallel:" : "Paralelo:", + "Password" : "Palavra-passe", + "Password for SOLR authentication (optional)" : "Palavra-passe para autenticação SOLR (opcional)", + "Path" : "Caminho", + "Path in repository" : "Caminho no repositório", + "Path where the configuration file will be saved in the repository" : "Caminho onde o ficheiro de configuração será guardado no repositório", + "Path where the register OAS file will be saved in the repository" : "Caminho onde o ficheiro OAS do registo será guardado no repositório", + "Pattern (regex)" : "Padrão (regex)", + "Pattern Issue" : "Problema de Padrão", + "Pattern constraint is missing" : "Falta a restrição de padrão", + "Patterns:" : "Padrões:", + "Pending" : "Pendente", + "Permanent Delete Mode" : "Modo de Eliminação Permanente", + "Permanently Delete" : "Eliminar Permanentemente", + "Permanently delete already soft-deleted objects" : "Eliminar permanentemente objetos já eliminados de forma reversível", + "Permanently delete this conversation?" : "Eliminar permanentemente esta conversa?", + "Person" : "Pessoa", + "Personal Data" : "Dados Pessoais", + "Phone" : "Telefone", + "Photos" : "Fotografias", + "Please create an agent in the" : "Crie um agente em", + "Please select an agent to continue" : "Selecione um agente para continuar", + "Please select which register and schema to use for the new object" : "Selecione que registo e esquema utilizar para o novo objeto", + "Please try again later." : "Tente novamente mais tarde.", + "Please wait while we fetch your agents." : "Aguarde enquanto obtemos os seus agentes.", + "Please wait while we fetch your applications." : "Aguarde enquanto obtemos as suas aplicações.", + "Please wait while we fetch your configurations." : "Aguarde enquanto obtemos as suas configurações.", + "Please wait while we fetch your deleted items." : "Aguarde enquanto obtemos os seus itens eliminados.", + "Please wait while we fetch your sources." : "Aguarde enquanto obtemos as suas fontes.", + "Polls" : "Sondagens", + "Popular Search Terms" : "Termos de Pesquisa Populares", + "Port" : "Porta", + "Prerequisites" : "Pré-requisitos", + "Press Enter to send, Shift+Enter for new line" : "Prima Enter para enviar, Shift+Enter para nova linha", + "Previous" : "Anterior", + "Private" : "Privado", + "Processes chunks in batches with simulated parallelism." : "Processa blocos em lotes com paralelismo simulado.", + "Processes file chunks sequentially (safest)." : "Processa os blocos de ficheiro sequencialmente (mais seguro).", + "Processes objects in chunks with simulated parallelism." : "Processa objetos em blocos com paralelismo simulado.", + "Processes objects sequentially (safest)." : "Processa os objetos sequencialmente (mais seguro).", + "Processing" : "A processar", + "Processing Limits" : "Limites de Processamento", + "Processing..." : "A processar...", + "Project initialized successfully" : "Projeto inicializado com sucesso", + "Project not found. Please initialize first." : "Projeto não encontrado. Inicialize primeiro.", + "Properties" : "Propriedades", + "Property Behaviors" : "Comportamentos da Propriedade", + "Property Configuration:" : "Configuração da Propriedade:", + "Property Filters" : "Filtros de Propriedades", + "Property Title" : "Título da Propriedade", + "Property Type" : "Tipo de Propriedade", + "Property can be improved" : "A propriedade pode ser melhorada", + "Protected" : "Protegido", + "Provider" : "Fornecedor", + "Provider is required for testing" : "O fornecedor é obrigatório para o teste", + "Public" : "Público", + "Public View" : "Vista Pública", + "Public views can be accessed by anyone in the system" : "As vistas públicas podem ser acedidas por qualquer pessoa no sistema", + "Publish" : "Publicar", + "Publish Objects" : "Publicar Objetos", + "Publish all objects in this schema" : "Publicar todos os objetos neste esquema", + "Published" : "Publicado", + "Published:" : "Publicado:", + "Publishing..." : "A publicar...", + "Purge" : "Expurgar", + "Purge Date" : "Data de Expurgo", + "Query Complexity Distribution" : "Distribuição da Complexidade das Consultas", + "Query parameter is required" : "O parâmetro de consulta é obrigatório", + "RAG" : "RAG", + "RAG Configuration" : "Configuração RAG", + "Rate limits cleared successfully" : "Limites de taxa limpos com sucesso", + "Raw Changes Data" : "Dados de Alterações em Bruto", + "Re-vectorize on object update" : "Voltar a vetorizar na atualização do objeto", + "Read" : "Ler", + "Recommendations:" : "Recomendações:", + "Recommended Type:" : "Tipo Recomendado:", + "Recommended: 5 sources" : "Recomendado: 5 fontes", + "Refresh" : "Atualizar", + "Refresh Data" : "Atualizar Dados", + "Refresh Stats" : "Atualizar Estatísticas", + "Refresh Workflows" : "Atualizar Fluxos de Trabalho", + "Refresh dashboard" : "Atualizar painel", + "Refresh database info" : "Atualizar informação da base de dados", + "Register" : "Registo", + "Register ID" : "ID do Registo", + "Register Statistics" : "Estatísticas do Registo", + "Register Totals" : "Totais do Registo", + "Register depublished successfully" : "Publicação do registo retirada com sucesso", + "Register not found" : "Registo não encontrado", + "Register objects deletion completed successfully" : "Eliminação dos objetos do registo concluída com sucesso", + "Register published successfully" : "Registo publicado com sucesso", + "Register/Schema Usage" : "Utilização de Registo/Esquema", + "Registers" : "Registos", + "Reindex" : "Reindexar", + "Reindex completed" : "Reindexação concluída", + "Reindex completed successfully" : "Reindexação concluída com sucesso", + "Reindex failed" : "A reindexação falhou", + "Relations" : "Relações", + "Remote Version" : "Versão Remota", + "Remove" : "Remover", + "Remove filter" : "Remover filtro", + "Remove from favorites" : "Remover dos favoritos", + "Remove group" : "Remover grupo", + "Removed" : "Removido", + "Removed from favorites" : "Removido dos favoritos", + "Rename Conversation" : "Renomear Conversa", + "Rename conversation" : "Renomear conversa", + "Replicas" : "Réplicas", + "Repository" : "Repositório", + "Request Body" : "Corpo do Pedido", + "Request Data" : "Dados do Pedido", + "Request Details" : "Detalhes do Pedido", + "Request body must be JSON with a \"query\" field" : "O corpo do pedido tem de ser JSON com um campo \"query\"", + "Request timeout in seconds" : "Tempo limite do pedido em segundos", + "Required field" : "Campo obrigatório", + "Required status is inconsistent" : "O estado de obrigatório é inconsistente", + "Rerun Search" : "Repetir Pesquisa", + "Reset Changes" : "Repor Alterações", + "Reset Filters" : "Repor Filtros", + "Response Body" : "Corpo da Resposta", + "Response Details" : "Detalhes da Resposta", + "Response Time:" : "Tempo de Resposta:", + "Restore" : "Restaurar", + "Restore conversation" : "Restaurar conversa", + "Restore or permanently delete items" : "Restaurar ou eliminar permanentemente itens", + "Result Count Range" : "Intervalo de Contagem de Resultados", + "Results" : "Resultados", + "Retrieve all objects for this schema" : "Obter todos os objetos para este esquema", + "Retry" : "Repetir", + "Retry Failed Extractions" : "Repetir Extrações Falhadas", + "Retry Policy" : "Política de Repetição", + "Retry completed" : "Repetição concluída", + "Retrying..." : "A repetir...", + "Risk Level" : "Nível de Risco", + "Roles updated successfully" : "Papéis atualizados com sucesso", + "SOLR Connection Settings" : "Definições de Ligação SOLR", + "SOLR Version:" : "Versão do SOLR:", + "SOLR actions menu" : "Menu de ações SOLR", + "SOLR base path (usually /solr)" : "Caminho base do SOLR (normalmente /solr)", + "SOLR is disabled" : "O SOLR está desativado", + "SOLR is not available or not configured" : "O SOLR não está disponível ou não está configurado", + "SOLR logging disabled" : "Registo SOLR desativado", + "SOLR logging enabled" : "Registo SOLR ativado", + "SOLR search disabled" : "Pesquisa SOLR desativada", + "SOLR search enabled" : "Pesquisa SOLR ativada", + "SOLR server hostname or IP address" : "Nome de anfitrião ou endereço IP do servidor SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Número de porta do servidor SOLR (opcional, predefinição 8983)", + "SOLR setup completed successfully" : "Configuração SOLR concluída com sucesso", + "SOLR setup failed" : "A configuração SOLR falhou", + "SOLR setup failed - check logs" : "A configuração SOLR falhou - verifique os registos", + "Sample Values:" : "Valores de Amostra:", + "Save" : "Guardar", + "Save Configuration" : "Guardar Configuração", + "Save Connection Settings" : "Guardar Definições de Ligação", + "Save Roles" : "Guardar Papéis", + "Save Settings" : "Guardar Definições", + "Save current search as view" : "Guardar a pesquisa atual como vista", + "Saved Views" : "Vistas Guardadas", + "Saving..." : "A guardar...", + "Schema" : "Esquema", + "Schema ID" : "ID do Esquema", + "Schema Name" : "Nome do Esquema", + "Schema Statistics" : "Estatísticas do Esquema", + "Schema depublished successfully" : "Publicação do esquema retirada com sucesso", + "Schema objects deletion completed successfully" : "Eliminação dos objetos do esquema concluída com sucesso", + "Schema published successfully" : "Esquema publicado com sucesso", + "Schema successfully created" : "Esquema criado com sucesso", + "Schema successfully updated" : "Esquema atualizado com sucesso", + "Schemas" : "Esquemas", + "Scheme" : "Esquema", + "Search / Views" : "Pesquisa / Vistas", + "Search Activity" : "Atividade de Pesquisa", + "Search Analytics" : "Análises de Pesquisa", + "Search Entities" : "Pesquisar Entidades", + "Search Files" : "Pesquisar Ficheiros", + "Search GitHub" : "Pesquisar no GitHub", + "Search GitLab" : "Pesquisar no GitLab", + "Search Method" : "Método de Pesquisa", + "Search Term" : "Termo de Pesquisa", + "Search Trail Details" : "Detalhes do Registo de Pesquisa", + "Search Trail Management" : "Gestão do Registo de Pesquisa", + "Search Trail Statistics" : "Estatísticas do Registo de Pesquisa", + "Search Trails" : "Registos de Pesquisa", + "Search Views" : "Pesquisar Vistas", + "Search Webhooks" : "Pesquisar Webhooks", + "Search by file name or path" : "Pesquisar por nome ou caminho do ficheiro", + "Search by name or URL" : "Pesquisar por nome ou URL", + "Search by value" : "Pesquisar por valor", + "Search in Files" : "Pesquisar em Ficheiros", + "Search in Objects" : "Pesquisar em Objetos", + "Search property names..." : "Pesquisar nomes de propriedades...", + "Search trail deletion not implemented yet" : "A eliminação de registos de pesquisa ainda não foi implementada", + "Search views..." : "Pesquisar vistas...", + "Secret" : "Segredo", + "Secret key for HMAC signature generation (optional)" : "Chave secreta para a geração de assinatura HMAC (opcional)", + "Select AI Agent" : "Selecionar Agente de IA", + "Select All" : "Selecionar Tudo", + "Select ConfigSet" : "Selecionar ConfigSet", + "Select File Types to Vectorize:" : "Selecionar Tipos de Ficheiro a Vetorizar:", + "Select HTTP method" : "Selecionar método HTTP", + "Select Register and Schema" : "Selecionar Registo e Esquema", + "Select Views to Vectorize:" : "Selecionar Vistas a Vetorizar:", + "Select a Nextcloud group to add" : "Selecione um grupo do Nextcloud para adicionar", + "Select a branch" : "Selecione um ramo", + "Select a model or type a custom model name" : "Selecione um modelo ou escreva um nome de modelo personalizado", + "Select a repository" : "Selecione um repositório", + "Select a repository you have write access to" : "Selecione um repositório ao qual tem acesso de escrita", + "Select an AI Agent" : "Selecionar um Agente de IA", + "Select an AI agent to begin chatting with your data." : "Selecione um agente de IA para começar a conversar com os seus dados.", + "Select backend" : "Selecionar backend", + "Select chat model" : "Selecionar modelo de conversa", + "Select collection for files" : "Selecionar coleção para ficheiros", + "Select collection for objects" : "Selecionar coleção para objetos", + "Select default organisation" : "Selecionar organização predefinida", + "Select event to listen to..." : "Selecionar evento a monitorizar...", + "Select model" : "Selecionar modelo", + "Select options..." : "Selecionar opções...", + "Select period" : "Selecionar período", + "Select property to send as payload" : "Selecionar propriedade a enviar como payload", + "Select provider" : "Selecionar fornecedor", + "Select registers and schemas to save a view" : "Selecione registos e esquemas para guardar uma vista", + "Select retry policy" : "Selecionar política de repetição", + "Select the branch to publish to" : "Selecione o ramo para o qual publicar", + "Select the event this webhook should listen to" : "Selecione o evento que este webhook deve monitorizar", + "Select views to vectorize:" : "Selecionar vistas a vetorizar:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Selecione que grupos do Nextcloud estão disponíveis para esta organização. Os utilizadores destes grupos terão acesso aos recursos da organização.", + "Select which data views the AI can search" : "Selecione que vistas de dados a IA pode pesquisar", + "Select which property from the event should be used as the webhook payload data" : "Selecione que propriedade do evento deve ser utilizada como dados do payload do webhook", + "Select which tools the AI can use to perform actions" : "Selecione que ferramentas a IA pode utilizar para realizar ações", + "Selected" : "Selecionado", + "Selected Groups" : "Grupos Selecionados", + "Selected audit trails deleted successfully" : "Registos de auditoria selecionados eliminados com sucesso", + "Selected search trails deleted successfully" : "Registos de pesquisa selecionados eliminados com sucesso", + "Selected users can access this view" : "Os utilizadores selecionados podem aceder a esta vista", + "Send additional feedback" : "Enviar comentários adicionais", + "Send as CloudEvent" : "Enviar como CloudEvent", + "Sensitive PII" : "IIP Sensível", + "Serial Mode (Safer, slower)" : "Modo Sequencial (Mais seguro, mais lento)", + "Serial:" : "Sequencial:", + "Server Configuration" : "Configuração do Servidor", + "Server Information" : "Informação do Servidor", + "Settings" : "Definições", + "Shards" : "Shards", + "Share" : "Partilhar", + "Share with Groups" : "Partilhar com Grupos", + "Share with Users" : "Partilhar com Utilizadores", + "Show Filters" : "Mostrar Filtros", + "Show less" : "Mostrar menos", + "Show only entries with changes" : "Mostrar apenas entradas com alterações", + "Simple" : "Simples", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Consultas simples: pesquisas de texto básicas com parâmetros mínimos (por exemplo, um único termo de pesquisa, sem filtros avançados)", + "Size" : "Tamanho", + "Slug" : "Slug", + "Soft Delete Mode" : "Modo de Eliminação Reversível", + "Soft Deleted Items" : "Itens Eliminados de Forma Reversível", + "SolrCloud mode enabled" : "Modo SolrCloud ativado", + "Source" : "Fonte", + "Sources" : "Fontes", + "Sources:" : "Fontes:", + "Standalone SOLR mode" : "Modo SOLR autónomo", + "Start Conversation" : "Iniciar Conversa", + "Start Vectorization" : "Iniciar Vetorização", + "Start a conversation" : "Iniciar uma conversa", + "Starting names cache warmup..." : "A iniciar o aquecimento da cache de nomes...", + "Starting..." : "A iniciar...", + "Statistics" : "Estatísticas", + "Stats calculation not yet implemented" : "O cálculo de estatísticas ainda não foi implementado", + "Status" : "Estado", + "Status Code" : "Código de Estado", + "String Constraints" : "Restrições de Cadeia de Caracteres", + "Success" : "Sucesso", + "Success Rate" : "Taxa de Sucesso", + "Success Status" : "Estado de Sucesso", + "Successful" : "Bem-sucedido", + "Successfully joined organisation" : "Aderiu à organização com sucesso", + "Sync Table" : "Sincronizar Tabela", + "Synchronization Settings" : "Definições de Sincronização", + "System Default" : "Predefinição do Sistema", + "Technical Description" : "Descrição Técnica", + "Technical description for developers and administrators" : "Descrição técnica para programadores e administradores", + "Temperature" : "Temperatura", + "Templates" : "Modelos", + "Test" : "Testar", + "Test Chat" : "Testar Conversa", + "Test Connection" : "Testar Ligação", + "Test Embedding" : "Testar Embedding", + "Test endpoint executed successfully" : "Endpoint de teste executado com sucesso", + "Test webhook delivered successfully" : "Webhook de teste entregue com sucesso", + "Test webhook delivery failed" : "A entrega do webhook de teste falhou", + "Test webhook sent successfully" : "Webhook de teste enviado com sucesso", + "Testing..." : "A testar...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Os blocos de texto são gerados durante a extração de ficheiros e armazenados na base de dados. A vetorização lê estes ficheiros pré-divididos em blocos e converte-os em embeddings.", + "Text extracted successfully" : "Texto extraído com sucesso", + "Text extraction disabled" : "Extração de texto desativada", + "The URL where webhook events will be sent" : "O URL para onde os eventos de webhook serão enviados", + "The requested conversation does not exist" : "A conversa solicitada não existe", + "The saved view and all its search configuration" : "A vista guardada e toda a sua configuração de pesquisa", + "There are no audit trail entries matching your current filters." : "Não existem entradas de registo de auditoria que correspondam aos seus filtros atuais.", + "There are no deleted items matching your current filters." : "Não existem itens eliminados que correspondam aos seus filtros atuais.", + "There are no search trail entries matching your current filters." : "Não existem entradas de registo de pesquisa que correspondam aos seus filtros atuais.", + "There are no webhook log entries matching your filters." : "Não existem entradas de registo de webhook que correspondam aos seus filtros.", + "This action cannot be undone." : "Esta ação não pode ser anulada.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Esta ação não pode ser anulada. Certifique-se de que nenhuma coleção está a utilizar este ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Este agente não tem vistas ou ferramentas configuráveis, mas pode ainda assim ajustar as definições de RAG abaixo.", + "This analysis may take some time" : "Esta análise pode demorar algum tempo", + "This audit trail entry does not contain any change information." : "Esta entrada de registo de auditoria não contém qualquer informação de alteração.", + "This configuration has no data defined." : "Esta configuração não tem dados definidos.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Este endpoint está descontinuado. Utilize antes endpoints baseados em blocos.", + "This entity has no relations to objects or files" : "Esta entidade não tem relações com objetos ou ficheiros", + "This is a test webhook from OpenRegister" : "Este é um webhook de teste do OpenRegister", + "This schema must use Magic Table configuration to sync" : "Este esquema tem de utilizar a configuração de Tabela Mágica para sincronizar", + "This source has no associated registers." : "Esta fonte não tem registos associados.", + "This will copy the _default ConfigSet with the new name" : "Isto copiará o ConfigSet _default com o novo nome", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Isto eliminará permanentemente TODOS os embeddings (vetores) da base de dados. Terá de voltar a vetorizar todos os objetos e ficheiros. Esta ação não pode ser anulada.\\n\\nTem a certeza de que pretende continuar?", + "This will permanently delete:" : "Isto eliminará permanentemente:", + "Timeout (seconds)" : "Tempo limite (segundos)", + "Timestamp" : "Data e hora", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Dica: ative apenas as vistas que necessitam de pesquisa semântica para minimizar os custos de embedding. As tabelas de consulta simples raramente necessitam de vetorização.", + "Title" : "Título", + "To Date" : "Data de Fim", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Para ajustar o tamanho ou a estratégia dos blocos, vá a Configuração de Ficheiros → Limites de Processamento.", + "To change the embedding provider or model, go to LLM Configuration." : "Para alterar o fornecedor ou o modelo de embedding, vá a Configuração LLM.", + "To date" : "Data de fim", + "Toggle search sidebar" : "Alternar barra lateral de pesquisa", + "Tools" : "Ferramentas", + "Top Deleters" : "Principais Autores de Eliminações", + "Total" : "Total", + "Total Audit Trails" : "Total de Registos de Auditoria", + "Total Chunks Available:" : "Total de Blocos Disponíveis:", + "Total Deleted Items" : "Total de Itens Eliminados", + "Total Members" : "Total de Membros", + "Total Objects" : "Total de Objetos", + "Total Objects in Database:" : "Total de Objetos na Base de Dados:", + "Total Organisations" : "Total de Organizações", + "Total Results" : "Total de Resultados", + "Total Searches" : "Total de Pesquisas", + "Total Size" : "Tamanho Total", + "Total Storage" : "Armazenamento Total", + "Total:" : "Total:", + "Totals" : "Totais", + "Type" : "Tipo", + "Type Issue" : "Problema de Tipo", + "Type Variations:" : "Variações de Tipo:", + "Type must be \"positive\" or \"negative\"" : "O tipo tem de ser \"positivo\" ou \"negativo\"", + "Type to search for groups" : "Escreva para pesquisar grupos", + "Type to search for users" : "Escreva para pesquisar utilizadores", + "Type to search groups..." : "Escreva para pesquisar grupos...", + "Type to search users..." : "Escreva para pesquisar utilizadores...", + "URL" : "URL", + "URL where Ollama is running" : "URL onde o Ollama está em execução", + "UUID:" : "UUID:", + "Unchanged" : "Inalterado", + "Unique Search Terms" : "Termos de Pesquisa Únicos", + "Unique Terms" : "Termos Únicos", + "Unknown" : "Desconhecido", + "Unknown Register" : "Registo Desconhecido", + "Unknown Schema" : "Esquema Desconhecido", + "Untitled View" : "Vista Sem Título", + "Update" : "Atualizar", + "Update Operations" : "Operações de Atualização", + "Update configuration: ..." : "Atualizar configuração: ...", + "Update register OAS: ..." : "Atualizar OAS do registo: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Atualizar os vetores quando os dados do objeto mudam (recomendado para uma pesquisa precisa)", + "Updated" : "Atualizado", + "Updated At" : "Atualizado Em", + "Updated:" : "Atualizado:", + "Uptime:" : "Tempo de atividade:", + "Usage Count" : "Contagem de Utilização", + "Use SolrCloud with Zookeeper for distributed search" : "Utilizar SolrCloud com Zookeeper para pesquisa distribuída", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Utilize filtros para restringir as entradas de registo de auditoria por registo, esquema, tipo de ação, utilizador, intervalo de datas ou ID do objeto.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Utilize filtros para restringir os itens eliminados por registo, esquema, data de eliminação ou utilizador que os eliminou.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Utilize filtros para restringir as entradas de registo de pesquisa por registo, esquema, estado de sucesso, utilizador, intervalo de datas, termos de pesquisa ou métricas de desempenho.", + "Used By Collections" : "Utilizado Por Coleções", + "User" : "Utilizador", + "User Agent Statistics" : "Estatísticas de User Agent", + "User Description" : "Descrição do Utilizador", + "User rate limits cleared successfully" : "Limites de taxa de utilizador limpos com sucesso", + "User removed successfully" : "Utilizador removido com sucesso", + "User-friendly description shown in forms and help text" : "Descrição amigável apresentada nos formulários e no texto de ajuda", + "Username" : "Nome de utilizador", + "Username for SOLR authentication (optional)" : "Nome de utilizador para autenticação SOLR (opcional)", + "Users" : "Utilizadores", + "Using Pre-Generated Chunks" : "A Utilizar Blocos Pré-Gerados", + "Validate" : "Validar", + "Validate Objects" : "Validar Objetos", + "Validation completed successfully" : "Validação concluída com sucesso", + "Validation failed" : "A validação falhou", + "Value" : "Valor", + "Vector Dimensions" : "Dimensões do Vetor", + "Vector Search Backend" : "Backend de Pesquisa Vetorial", + "Vector field: _embedding_" : "Campo vetorial: _embedding_", + "Vectorization Triggers" : "Acionadores de Vetorização", + "Vectorize All Files" : "Vetorizar Todos os Ficheiros", + "Vectorize All Objects" : "Vetorizar Todos os Objetos", + "Vectorize all file types" : "Vetorizar todos os tipos de ficheiro", + "Vectorize all views" : "Vetorizar todas as vistas", + "Vectorize on object creation" : "Vetorizar na criação do objeto", + "Vectors will be stored in your existing object and file collections" : "Os vetores serão armazenados nas suas coleções de objetos e ficheiros existentes", + "Version" : "Versão", + "Very High" : "Muito Alto", + "View API Docs" : "Ver Documentação da API", + "View Changes" : "Ver Alterações", + "View Details" : "Ver Detalhes", + "View Error" : "Ver Erro", + "View File" : "Ver Ficheiro", + "View Full Details" : "Ver Todos os Detalhes", + "View Logs" : "Ver Registos", + "View Name" : "Nome da Vista", + "View Object" : "Ver Objeto", + "View Parameters" : "Ver Parâmetros", + "View Selection" : "Seleção de Vistas", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Ver e analisar os registos de pesquisa com capacidades avançadas de filtragem e análise", + "View and analyze system audit trails with advanced filtering capabilities" : "Ver e analisar os registos de auditoria do sistema com capacidades avançadas de filtragem", + "View deleted successfully" : "Vista eliminada com sucesso", + "View entity details and manage relations" : "Ver detalhes da entidade e gerir relações", + "View name" : "Nome da vista", + "View name is required" : "O nome da vista é obrigatório", + "View saved successfully!" : "Vista guardada com sucesso!", + "View search analytics and manage search logs" : "Ver análises de pesquisa e gerir os registos de pesquisa", + "View successfully deleted" : "Vista eliminada com sucesso", + "View successfully updated" : "Vista atualizada com sucesso", + "View updated successfully!" : "Vista atualizada com sucesso!", + "View webhook delivery logs and filter by webhook" : "Ver os registos de entrega de webhook e filtrar por webhook", + "Views" : "Vistas", + "Visible to users" : "Visível para os utilizadores", + "Wait for Response" : "Aguardar Resposta", + "Wait for webhook response before continuing (required for request/response flows)" : "Aguardar pela resposta do webhook antes de continuar (necessário para fluxos de pedido/resposta)", + "Warmup Names Cache" : "Aquecer Cache de Nomes", + "Warning" : "Aviso", + "Warning:" : "Aviso:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detalhes do Registo de Webhook", + "Webhook Logs" : "Registos de Webhook", + "Webhook created successfully" : "Webhook criado com sucesso", + "Webhook deleted" : "Webhook eliminado", + "Webhook retry delivered successfully" : "Repetição do webhook entregue com sucesso", + "Webhook retry delivery failed" : "A entrega da repetição do webhook falhou", + "Webhook updated" : "Webhook atualizado", + "Webhook updated successfully" : "Webhook atualizado com sucesso", + "Webhooks" : "Webhooks", + "Weekly" : "Semanalmente", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Envolver o payload do webhook no formato CloudEvents para uma melhor interoperabilidade", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Sim", + "You" : "Você", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Pode criar novos ConfigSets com base no modelo _default, ou carregar ConfigSets personalizados diretamente para o seu servidor SOLR.", + "You do not have access to this conversation" : "Não tem acesso a esta conversa", + "You do not have permission to delete this conversation" : "Não tem permissão para eliminar esta conversa", + "You do not have permission to modify this conversation" : "Não tem permissão para modificar esta conversa", + "You do not have permission to restore this conversation" : "Não tem permissão para restaurar esta conversa", + "You must be logged in to favorite views" : "Tem de ter sessão iniciada para marcar vistas como favoritas", + "You need an AI agent to start a conversation." : "Necessita de um agente de IA para iniciar uma conversa.", + "Your Fireworks AI API key. Get one at" : "A sua chave de API do Fireworks AI. Obtenha uma em", + "Your OpenAI API key. Get one at" : "A sua chave de API do OpenAI. Obtenha uma em", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Os seus comentários foram registados. Opcionalmente, pode fornecer detalhes adicionais aqui...", + "Zookeeper Hosts" : "Anfitriões do Zookeeper", + "Zookeeper Port" : "Porta do Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Definições do Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Cadeia de ligação do Zookeeper para SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Número de porta do Zookeeper (opcional, predefinição 2181)", + "chunk" : "bloco", + "chunks" : "blocos", + "chunks processed" : "blocos processados", + "configuration(s)" : "configuração(ões)", + "e.g., lib/Settings/config.json" : "por exemplo, lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "por exemplo, lib/Settings/register.json", + "failed" : "falhado", + "file" : "ficheiro", + "files" : "ficheiros", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "membros", + "menu or contact someone with permission to create agents." : "ou contacte alguém com permissão para criar agentes.", + "messages" : "mensagens", + "n8n URL and API key are required" : "O URL e a chave de API do n8n são obrigatórios", + "n8n configuration saved successfully" : "Configuração do n8n guardada com sucesso", + "n8n connection not configured" : "Ligação ao n8n não configurada", + "n8n connection successful" : "Ligação ao n8n bem-sucedida", + "n8n connection test successful" : "Teste de ligação ao n8n bem-sucedido", + "n8n integration disabled" : "Integração do n8n desativada", + "n8n integration enabled" : "Integração do n8n ativada", + "n8n project initialized successfully" : "Projeto n8n inicializado com sucesso", + "n8n settings saved successfully" : "Definições do n8n guardadas com sucesso", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objetos", + "objects processed" : "objetos processados", + "or pick a range" : "ou escolha um intervalo", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "proporciona um bom equilíbrio entre velocidade e precisão.", + "register(s)" : "registo(s)", + "register(s) selected" : "registo(s) selecionado(s)", + "required" : "obrigatório", + "results" : "resultados", + "schema(s)" : "esquema(s)", + "schema(s) selected" : "esquema(s) selecionado(s)", + "sk-..." : "sk-...", + "this application" : "esta aplicação", + "to" : "até", + "vectorized" : "vetorizado", + "views selected" : "vistas selecionadas", + "ℹ️ Current Configuration" : "ℹ️ Configuração Atual", + "⚡ Batch Processing" : "⚡ Processamento em Lote", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Contexto abrangente ✓ Menor probabilidade de perder detalhes ✗ Respostas mais lentas ✗ Pode incluir informação menos relevante", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Respostas mais rápidas ✓ Respostas mais focadas ✗ Pode perder informação relevante", + "✨ AI Features" : "✨ Funcionalidades de IA", + "All search trails cleared successfully" : "Todos os registos de pesquisa foram limpos com sucesso", + "Multiple search trail deletion not implemented yet" : "A eliminação de vários registos de pesquisa ainda não foi implementada", + "No expired search trails found to clear" : "Não foram encontrados registos de pesquisa expirados para limpar", + "Missing message" : "Mensagem em falta", + "message content is required" : "o conteúdo da mensagem é obrigatório", + "Missing conversation or agentUuid" : "Conversa ou agentUuid em falta", + "Access denied" : "Acesso negado", + "Conversation not found" : "Conversa não encontrada", + "AI service not configured" : "Serviço de IA não configurado", + "Failed to process message" : "Falha ao processar a mensagem", + "Missing conversationId" : "conversationId em falta", + "conversationId is required" : "conversationId é obrigatório", + "Failed to fetch conversation history" : "Falha ao obter o histórico da conversa", + "Failed to clear conversation" : "Falha ao limpar a conversa", + "Invalid feedback type" : "Tipo de comentário inválido", + "type must be \"positive\" or \"negative\"" : "o tipo tem de ser \"positivo\" ou \"negativo\"", + "Message not found" : "Mensagem não encontrada", + "Failed to save feedback" : "Falha ao guardar os comentários", + "Failed to get chat statistics" : "Falha ao obter as estatísticas da conversa", + "Not Found" : "Não Encontrado", + "Engine not found" : "Motor não encontrado", + "Consumer not found" : "Consumidor não encontrado", + "Backend parameter is required" : "O parâmetro de backend é obrigatório", + "Failed to get database information: %s" : "Falha ao obter a informação da base de dados: %s", + "SOLR setup error: %s" : "Erro de configuração SOLR: %s", + "Reindex failed: %s" : "A reindexação falhou: %s", + "Anonymized" : "Anonimizado", + "Entities detected" : "Entidades detetadas", + "Extract Now" : "Extrair Agora", + "Extracted at" : "Extraído em", + "Extraction" : "Extração", + "Failed to load extraction data" : "Falha ao carregar os dados de extração", + "Failed to load register data" : "Falha ao carregar os dados do registo", + "Failed to retrieve extraction status." : "Falha ao obter o estado da extração.", + "Failed to retrieve objects for file." : "Falha ao obter os objetos para o ficheiro.", + "No extraction data available for this file" : "Não existem dados de extração disponíveis para este ficheiro", + "No register objects reference this file" : "Nenhum objeto de registo referencia este ficheiro", + "Not extracted" : "Não extraído", + "Register Objects" : "Objetos do Registo", + "Risk level" : "Nível de risco", + "Text chunks" : "Blocos de texto", + "Unknown error" : "Erro desconhecido", + "Very high" : "Muito alto", + "{title} in {register} / {schema}" : "{title} em {register} / {schema}", + "Push Notifications" : "Notificações Push", + "Real-time push notification status via notify_push" : "Estado das notificações push em tempo real via notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push em tempo real não disponível — a aplicação notify_push não está instalada", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Instale a aplicação notify_push a partir da App Store do Nextcloud para ativar as atualizações em tempo real.", + "Open Nextcloud App Store" : "Abrir App Store do Nextcloud", + "notify_push is installed but not yet active" : "O notify_push está instalado mas ainda não está ativo", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "O notify_push está instalado mas o OpenRegister ainda não confirmou um push bem-sucedido. Acione uma gravação de objeto para ativar, ou verifique a configuração do notify_push.", + "notify_push configuration guide" : "guia de configuração do notify_push", + "Realtime push active" : "Push em tempo real ativo", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "As notificações push em tempo real estão ativas. Os clientes ligados recebem atualizações instantâneas quando os objetos são criados, atualizados ou eliminados.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/pt.json b/l10n/pt.json new file mode 100644 index 0000000000..d1f9df5852 --- /dev/null +++ b/l10n/pt.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin necessário)", + "(no subject)": "(sem assunto)", + "(no title)": "(sem título)", + "(unnamed)": "(sem nome)", + "(untitled card)": "(cartão sem título)", + "({count} selected)": "({count} selecionados)", + ", {failed} failed": ", {failed} falharam", + "0 = unlimited": "0 = ilimitado", + "A deactivation request is pending.": "Existe um pedido de desativação pendente.", + "AI Assistant": "Assistente de IA", + "AI Chat": "Conversa de IA", + "AI service not configured": "Serviço de IA não configurado", + "API Key": "Chave de API", + "API Request Quota (requests/day)": "Quota de pedidos de API (pedidos/dia)", + "API Token Configuration": "Configuração de tokens de API", + "API Tokens": "Tokens de API", + "API tokens saved successfully": "Tokens de API guardados com sucesso", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: este relatório associa cada verwerkingsactiviteit às contagens vitalícias de trilhos de auditoria que lhe são atribuídas. Os auditores e a Autoriteit Persoonsgegevens utilizam-no para revisão de supervisão.", + "About ConfigSets": "Acerca dos ConfigSets", + "Access denied": "Acesso negado", + "Access organisation dashboard and analytics": "Aceder ao painel e às análises da organização", + "Account": "Conta", + "Action": "Ação", + "Action Distribution": "Distribuição de ações", + "Action:": "Ação:", + "Actions": "Ações", + "Active": "Ativo", + "Active Collections": "Coleções ativas", + "Active Filters": "Filtros ativos", + "Active Organisation:": "Organização ativa:", + "Active Organisations": "Organizações ativas", + "Active filters:": "Filtros ativos:", + "Active organisation changed successfully": "Organização ativa alterada com sucesso", + "Active organisation set successfully": "Organização ativa definida com sucesso", + "Activities": "Atividades", + "Activity": "Atividade", + "Activity Period": "Período de atividade", + "Add": "Adicionar", + "Add Attachment": "Adicionar anexo", + "Add Endpoint": "Adicionar ponto de extremidade", + "Add Groups": "Adicionar grupos", + "Add Property to '{title}'": "Adicionar propriedade a '{title}'", + "Add Register": "Adicionar registo", + "Add Schema": "Adicionar esquema", + "Add Source": "Adicionar fonte", + "Add User": "Adicionar utilizador", + "Add User to Organisation": "Adicionar utilizador à organização", + "Add a contact from any of your address books to associate it with this object.": "Adicione um contacto de qualquer um dos seus livros de endereços para o associar a este objeto.", + "Add a file or files": "Adicionar um ficheiro ou ficheiros", + "Add card": "Adicionar cartão", + "Add contact": "Adicionar contacto", + "Add endpoint": "Adicionar ponto de extremidade", + "Add oneOf entry": "Adicionar entrada oneOf", + "Add range": "Adicionar intervalo", + "Add schema titles, descriptions, and register information to provide richer context for search": "Adicione títulos de esquema, descrições e informações de registo para fornecer um contexto mais rico para a pesquisa", + "Add to favorites": "Adicionar aos favoritos", + "Added": "Adicionado", + "Added to favorites": "Adicionado aos favoritos", + "Additional Information": "Informações adicionais", + "Additional feedback saved. Thank you!": "Comentários adicionais guardados. Obrigado!", + "Addresses": "Endereços", + "Admin": "Administrador", + "Advanced": "Avançado", + "Advanced Filters": "Filtros avançados", + "Advanced Options": "Opções avançadas", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Os filtros avançados não estão disponíveis ao utilizar a fonte de base de dados. Mude para Auto ou índice SOLR para obter opções de filtragem.", + "After": "Depois", + "Agent Type": "Tipo de agente", + "Agent deleted successfully": "Agente eliminado com sucesso", + "Agent successfully created": "Agente criado com sucesso", + "Agent successfully updated": "Agente atualizado com sucesso", + "Agents": "Agentes", + "Aggregated across schemas": "Agregado entre esquemas", + "All": "Todos", + "All Categories": "Todas as categorias", + "All Collections": "Todas as coleções", + "All Confidence Levels": "Todos os níveis de confiança", + "All ConfigSets": "Todos os ConfigSets", + "All Files": "Todos os ficheiros", + "All Levels": "Todos os níveis", + "All Types": "Todos os tipos", + "All Webhooks": "Todos os webhooks", + "All actions": "Todas as ações", + "All audit trails cleared successfully": "Todos os trilhos de auditoria limpos com sucesso", + "All clear": "Tudo limpo", + "All file types": "Todos os tipos de ficheiro", + "All registers": "Todos os registos", + "All schemas": "Todos os esquemas", + "All search trails cleared successfully": "Todos os trilhos de pesquisa limpos com sucesso", + "All searches": "Todas as pesquisas", + "All users": "Todos os utilizadores", + "All-day events": "Eventos de dia inteiro", + "Allowed MIME Types": "Tipos MIME permitidos", + "Already linked": "Já associado", + "An error occurred while clearing audit trails": "Ocorreu um erro ao limpar os trilhos de auditoria", + "An error occurred while deleting the audit trail": "Ocorreu um erro ao eliminar o trilho de auditoria", + "An error occurred while deleting the object": "Ocorreu um erro ao eliminar o objeto", + "An error occurred while deleting the view": "Ocorreu um erro ao eliminar a vista", + "An error occurred while permanently deleting the objects": "Ocorreu um erro ao eliminar permanentemente os objetos", + "An error occurred while restoring the objects": "Ocorreu um erro ao restaurar os objetos", + "An error occurred while saving the organisation": "Ocorreu um erro ao guardar a organização", + "Analysis completed:": "Análise concluída:", + "Analysis steps:": "Passos da análise:", + "Analytics": "Análises", + "Analyze Objects": "Analisar objetos", + "Analyze Properties": "Analisar propriedades", + "Analyze existing properties for improvement opportunities": "Analisar propriedades existentes para encontrar oportunidades de melhoria", + "Analyzing...": "A analisar...", + "Anonymized": "Anonimizado", + "Anonymous Group": "Grupo anónimo", + "Any favorites and sharing settings for this view": "Quaisquer favoritos e definições de partilha desta vista", + "Any user": "Qualquer utilizador", + "App ID": "ID da aplicação", + "App store cache invalidated successfully": "Cache da App Store invalidada com sucesso", + "App store cache invalidated: {invalidated}": "Cache da App Store invalidada: {invalidated}", + "Application deleted successfully": "Aplicação eliminada com sucesso", + "Application identifier for this configuration (optional)": "Identificador de aplicação para esta configuração (opcional)", + "Application settings": "Definições da aplicação", + "Application successfully created": "Aplicação criada com sucesso", + "Application successfully updated": "Aplicação atualizada com sucesso", + "Applications": "Aplicações", + "Apply": "Aplicar", + "Apply Changes": "Aplicar alterações", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Aplicar uma função a todos os esquemas neste registo que não tenham substituições de autorização explícitas.", + "Approval Progress": "Progresso da aprovação", + "Approve": "Aprovar", + "Approved": "Aprovado", + "Archive": "Arquivar", + "Archive conversation": "Arquivar conversa", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arquivar esta verwerkingsactiviteit? As linhas do trilho de auditoria continuarão a referenciá-la.", + "Archived conversations are hidden from your active list": "As conversas arquivadas ficam ocultas da sua lista ativa", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Tem a certeza de que pretende ELIMINAR a coleção \"{name}\"?\n\nIsto irá:\n• Eliminar permanentemente a coleção e todos os seus dados\n• Remover todos os documentos indexados\n• Esta ação não pode ser anulada", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Tem a certeza de que pretende limpar os trilhos de pesquisa antigos? Isto irá eliminar as entradas com mais de 30 dias.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Tem a certeza de que pretende limpar todos os dados da coleção \"{name}\"?\n\nIsto irá:\n• Eliminar todos os documentos indexados\n• Manter a estrutura da coleção intacta\n• Esta ação não pode ser anulada", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Tem a certeza de que pretende eliminar a aplicação \"{name}\"? Esta ação não pode ser anulada.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Tem a certeza de que pretende eliminar os trilhos de auditoria selecionados? Esta ação não pode ser anulada.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Tem a certeza de que pretende eliminar os trilhos de pesquisa selecionados? Esta ação não pode ser anulada.", + "Are you sure you want to delete this ConfigSet?": "Tem a certeza de que pretende eliminar este ConfigSet?", + "Are you sure you want to delete this configset?": "Tem a certeza de que pretende eliminar este configset?", + "Are you sure you want to leave '{name}'?": "Tem a certeza de que pretende sair de '{name}'?", + "Are you sure you want to permanently delete": "Tem a certeza de que pretende eliminar permanentemente", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Tem a certeza de que pretende reindexar a coleção \"{name}\"?\n\nIsto irá:\n• Reconstruir o índice com todos os objetos\n• Demorar vários minutos a concluir\n• Pode afetar o desempenho da pesquisa durante a reindexação", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Tem a certeza de que pretende remover o esquema \"{schema}\" do registo \"{register}\"? Esta ação não pode ser anulada.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Tem a certeza de que pretende eliminar de forma reversível {count} objetos ativos do esquema \"{schema}\"?\n\nSerão marcados como eliminados, mas podem ser removidos permanentemente mais tarde.", + "Array Object Configuration:": "Configuração de objeto de matriz:", + "Articles": "Artigos", + "Ask a question...": "Faça uma pergunta...", + "Ask questions about your data using natural language": "Faça perguntas sobre os seus dados utilizando linguagem natural", + "Assigned collaborative tags": "Etiquetas colaborativas atribuídas", + "Assignment notifications": "Notificações de atribuição", + "Attachment added to {name}": "Anexo adicionado a {name}", + "Attempt": "Tentativa", + "Audit Trail Changes": "Alterações do trilho de auditoria", + "Audit Trail Details": "Detalhes do trilho de auditoria", + "Audit Trail Management": "Gestão de trilhos de auditoria", + "Audit Trail Statistics": "Estatísticas do trilho de auditoria", + "Audit Trails": "Trilhos de auditoria", + "Audit trail data copied to clipboard": "Dados do trilho de auditoria copiados para a área de transferência", + "Audit trail deleted successfully": "Trilho de auditoria eliminado com sucesso", + "Audit trail successfully deleted": "Trilho de auditoria eliminado com sucesso", + "Authentication": "Autenticação", + "Auto (e.g. Y for year, F Y for month)": "Auto (por exemplo, Y para ano, F Y para mês)", + "Auto share": "Partilha automática", + "Auto-commit disabled": "Confirmação automática desativada", + "Auto-commit enabled": "Confirmação automática ativada", + "Auto-create Default Organisation": "Criar automaticamente a organização predefinida", + "Auto-retry failed vectorizations": "Repetir automaticamente as vetorizações falhadas", + "Automatically commit changes to SOLR index": "Confirmar automaticamente as alterações no índice SOLR", + "Automatically connect to the federation network.": "Ligar automaticamente à rede de federação.", + "Automatically create a default organisation if none exists when the app is initialized": "Criar automaticamente uma organização predefinida se não existir nenhuma quando a aplicação é inicializada", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Gerar automaticamente vetores de incorporação a partir de blocos de texto quando os ficheiros são carregados e processados", + "Automatically generate vector embeddings when objects are created or updated": "Gerar automaticamente vetores de incorporação quando os objetos são criados ou atualizados", + "Automatically retry failed vectorization attempts (max 3 retries)": "Repetir automaticamente as tentativas de vetorização falhadas (máximo de 3 tentativas)", + "Automatically update catalogues.": "Atualizar automaticamente os catálogos.", + "Available Filters": "Filtros disponíveis", + "Available Workflows": "Fluxos de trabalho disponíveis", + "Available placeholders:": "Marcadores de posição disponíveis:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "As alterações de avatar não são suportadas pelo seu fornecedor de autenticação.", + "Avatar removed": "Avatar removido", + "Avatar updated successfully": "Avatar atualizado com sucesso", + "Avg Execution Time": "Tempo médio de execução", + "Avg Members/Org": "Média de membros/org.", + "Avg Object Views/Session": "Média de visualizações de objetos/sessão", + "Avg Response Time": "Tempo médio de resposta", + "Avg Results/Search": "Média de resultados/pesquisa", + "Avg Searches/Session": "Média de pesquisas/sessão", + "Back": "Voltar", + "Back to Entities": "Voltar às entidades", + "Back to Registers": "Voltar aos registos", + "Back to Webhooks": "Voltar aos webhooks", + "Back to applications": "Voltar às aplicações", + "Back to entities": "Voltar às entidades", + "Backend parameter is required": "O parâmetro de backend é obrigatório", + "Backend updated successfully. Please reload the application.": "Backend atualizado com sucesso. Recarregue a aplicação.", + "Bandwidth Quota (MB/month)": "Quota de largura de banda (MB/mês)", + "Base URL (Optional)": "URL base (opcional)", + "Basic Information": "Informações básicas", + "Batch Size": "Tamanho do lote", + "Batch extraction completed": "Extração em lote concluída", + "Before": "Antes", + "Before object vectorization can work:": "Antes de a vetorização de objetos poder funcionar:", + "Behavior": "Comportamento", + "Behavior Issue": "Problema de comportamento", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (duração ISO-8601, por exemplo, P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "O armazenamento de blobs foi descontinuado. Todos os objetos utilizam agora tabelas mágicas.", + "Bookmarks": "Marcadores", + "Branch": "Ramo", + "Bulk Role Assignment: {title}": "Atribuição de funções em massa: {title}", + "Bulk delete operation completed successfully": "Operação de eliminação em massa concluída com sucesso", + "Bulk save operation completed successfully": "Operação de gravação em massa concluída com sucesso", + "Business Data": "Dados empresariais", + "CSV": "CSV", + "Cache cleared successfully": "Cache limpa com sucesso", + "Cache grew from {old} to {new} entries.": "A cache cresceu de {old} para {new} entradas.", + "Cache shrunk from {old} to {new} entries.": "A cache reduziu-se de {old} para {new} entradas.", + "Cache stayed the same at {size} entries.": "A cache manteve-se igual, com {size} entradas.", + "Calculate Sizes": "Calcular tamanhos", + "Calendar": "Calendário", + "Calendar Provider Configuration": "Configuração do fornecedor de calendário", + "Calendar integration is not available": "A integração de calendário não está disponível", + "Calendar name": "Nome do calendário", + "Cancel": "Cancelar", + "Cancel deactivation request": "Cancelar pedido de desativação", + "Cannot delete: objects are still attached": "Não é possível eliminar: ainda existem objetos anexados", + "Cannot edit: This register is managed by external configuration {title}": "Não é possível editar: este registo é gerido pela configuração externa {title}", + "Cannot remove schema with existing objects ({count} objects)": "Não é possível remover o esquema com objetos existentes ({count} objetos)", + "Cannot remove schema {schema} because it contains {count} objects": "Não é possível remover o esquema {schema} porque contém {count} objetos", + "Cards": "Cartões", + "Cascade delete": "Eliminação em cascata", + "Categories of data subjects (one per line)": "Categorias de titulares dos dados (uma por linha)", + "Categories of personal data (one per line)": "Categorias de dados pessoais (uma por linha)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Categoria", + "Change Type": "Tipo de alteração", + "Change password": "Alterar palavra-passe", + "Changes": "Alterações", + "Changes copied to clipboard": "Alterações copiadas para a área de transferência", + "Changes for Audit Trail #{id}": "Alterações do trilho de auditoria #{id}", + "Chat Model": "Modelo de conversa", + "Chat Provider": "Fornecedor de conversa", + "Chat Provider (RAG)": "Fornecedor de conversa (RAG)", + "Chat Settings": "Definições de conversa", + "Chat provider connection successful!": "Ligação ao fornecedor de conversa estabelecida com sucesso!", + "Chat settings": "Definições de conversa", + "Chat test failed: {error}": "O teste de conversa falhou: {error}", + "Choose a register": "Escolha um registo", + "Choose a schema": "Escolha um esquema", + "Choose how vector similarity calculations are performed for semantic search": "Escolha como são realizados os cálculos de semelhança de vetores para a pesquisa semântica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Escolha que tipos de ficheiro incluir no processo de vetorização. Apenas serão processados os ficheiros com texto e blocos extraídos.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Escolha que vistas incluir no processo de vetorização. Deixe vazio para processar todas as vistas com base na sua configuração.", + "Chunk": "Bloco", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "A eliminação de blocos ainda não está implementada. Utilize os pontos de extremidade baseados em blocos.", + "Chunks": "Blocos", + "Chunks to Vectorize:": "Blocos a vetorizar:", + "Cleanup Old Trails": "Limpar trilhos antigos", + "Cleanup completed": "Limpeza concluída", + "Cleanup completed successfully. Deleted {count} entries.": "Limpeza concluída com sucesso. {count} entradas eliminadas.", + "Cleanup failed: {error}": "A limpeza falhou: {error}", + "Clear All Embeddings": "Limpar todas as incorporações", + "Clear Entries": "Limpar entradas", + "Clear Filtered Audit Trails": "Limpar trilhos de auditoria filtrados", + "Clear Filters": "Limpar filtros", + "Clear Index": "Limpar índice", + "Clear Selection": "Limpar seleção", + "Clear all": "Limpar tudo", + "Clear all filters": "Limpar todos os filtros", + "Clear date range": "Limpar intervalo de datas", + "Clear filters": "Limpar filtros", + "Clearing...": "A limpar...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Clique em Executar análise de conformidade para encontrar esquemas onde foram detetados dados pessoais (PII) mas não existe nenhuma anotação de verwerkingsactiviteit.", + "Click to subscribe to notifications": "Clique para subscrever as notificações", + "Click to unsubscribe from notifications": "Clique para anular a subscrição das notificações", + "Close": "Fechar", + "Cluster name": "Nome do cluster", + "Code": "Código", + "Code (short readable key, e.g. v-2026-001)": "Código (chave curta legível, por exemplo, v-2026-001)", + "Collapse details": "Reduzir detalhes", + "Collection Management": "Gestão de coleções", + "Collection Name": "Nome da coleção", + "Collection assignments updated successfully": "Atribuições de coleção atualizadas com sucesso", + "Collection cleared successfully": "Coleção limpa com sucesso", + "Collection copied successfully": "Coleção copiada com sucesso", + "Collection created successfully": "Coleção criada com sucesso", + "Collection deleted successfully": "Coleção eliminada com sucesso", + "Collection used to store and index file metadata and content": "Coleção utilizada para armazenar e indexar metadados e conteúdo de ficheiros", + "Collection used to store and index object data": "Coleção utilizada para armazenar e indexar dados de objetos", + "Collections:": "Coleções:", + "Color": "Cor", + "Comment...": "Comentário...", + "Commit Message": "Mensagem de confirmação", + "Commit Within (ms)": "Confirmar dentro de (ms)", + "Commit message": "Mensagem de confirmação", + "Compare current schema with real object data": "Comparar o esquema atual com dados de objetos reais", + "Completed": "Concluído", + "Complex": "Complexo", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Consultas complexas: pesquisas avançadas com múltiplos filtros, operadores e combinações de parâmetros complexas", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Consultas complexas: pesquisas avançadas com múltiplos filtros, operadores e combinações de parâmetros complexas", + "Compliance": "Conformidade", + "Confidence Level": "Nível de confiança", + "Confidence Score": "Pontuação de confiança", + "Confidence: {confidence}%": "Confiança: {confidence}%", + "Config must be provided as an object": "A configuração tem de ser fornecida como um objeto", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Gestão de ConfigSets", + "ConfigSet Name": "Nome do ConfigSet", + "ConfigSet created successfully": "ConfigSet criado com sucesso", + "ConfigSet deleted successfully": "ConfigSet eliminado com sucesso", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Os ConfigSets definem o esquema e a configuração das suas coleções SOLR. Contêm definições de campos, analisadores e outras definições de pesquisa.", + "Configuration": "Configuração", + "Configuration Data": "Dados de configuração", + "Configuration is up to date": "A configuração está atualizada", + "Configuration saved": "Configuração guardada", + "Configuration saved successfully.": "Configuração guardada com sucesso.", + "Configuration type (default, application, etc.)": "Tipo de configuração (default, application, etc.)", + "Configurations": "Configurações", + "Configure API tokens for external service integrations": "Configurar tokens de API para integrações de serviços externos", + "Configure Apache SOLR search engine for advanced search capabilities": "Configurar o motor de pesquisa Apache SOLR para capacidades de pesquisa avançadas", + "Configure Facets": "Configurar facetas", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Configurar fornecedores de LLM (Large Language Model) para funcionalidades baseadas em IA, incluindo pesquisa semântica, incorporações e conversa.", + "Configure SOLR Facets": "Configurar facetas SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Configure as definições básicas de ligação ao seu servidor SOLR, incluindo as opções de autenticação e de rede. Utilize as caixas de diálogo separadas de gestão de ConfigSets e coleções para gerir os cores e as coleções.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configure as definições básicas de ligação ao seu servidor SOLR, incluindo as opções de autenticação e de rede. Utilize as caixas de diálogo separadas de gestão de configset e coleções para gerir os cores e as coleções.", + "Configure default organisation and organisation-related settings": "Configurar a organização predefinida e as definições relacionadas com a organização", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Configure como os objetos da base de dados são convertidos em vetores de incorporação para a pesquisa semântica. Os objetos são vetorizados diretamente sem necessidade de extração de texto.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Configure como os objetos são convertidos em texto antes da vetorização. Estas definições afetam a qualidade e o contexto da pesquisa.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configurar fornecedores de LLM (large language model) para funcionalidades baseadas em IA, incluindo pesquisa semântica, incorporações e conversa.", + "Configure multi-organization support and tenant isolation": "Configurar o suporte de múltiplas organizações e o isolamento de inquilinos", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Configure os parâmetros para a vetorização de ficheiros. Este processo irá gerar vetores de incorporação para todos os blocos de ficheiros extraídos.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Configure os parâmetros para a vetorização de objetos. Este processo irá gerar vetores de incorporação para todos os objetos que correspondam aos filtros da sua vista.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configure este esquema para apresentar objetos como eventos na aplicação Calendário do Nextcloud.", + "Configure which types of data to search and how many sources to retrieve": "Configure que tipos de dados pesquisar e quantas fontes obter", + "Configuring field mappings...": "A configurar os mapeamentos de campos...", + "Confirm Account Deactivation": "Confirmar a desativação da conta", + "Confirm Object Deletion": "Confirmar a eliminação do objeto", + "Confirm Object Restoration": "Confirmar o restauro do objeto", + "Confirm Permanent Object Deletion": "Confirmar a eliminação permanente do objeto", + "Confirm deactivation": "Confirmar desativação", + "Connecting to SOLR cluster...": "A ligar ao cluster SOLR...", + "Connection Failed": "Ligação falhada", + "Connection Settings": "Definições de ligação", + "Connection Successful!": "Ligação estabelecida com sucesso!", + "Connection failed": "Ligação falhada", + "Connection protocol": "Protocolo de ligação", + "Connection successful": "Ligação estabelecida com sucesso", + "Connection timeout in seconds": "Tempo limite de ligação em segundos", + "Constant delay between retries (5 minutes)": "Atraso constante entre tentativas (5 minutos)", + "Constraint Issue": "Problema de restrição", + "Consumer not found": "Consumidor não encontrado", + "Contacts": "Contactos", + "Contacts integration is not available": "A integração de contactos não está disponível", + "Content Filters": "Filtros de conteúdo", + "Continue to Properties": "Continuar para as propriedades", + "Contracts": "Contratos", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Controle que vistas de objetos devem ser vetorizadas para reduzir os custos de API. Vetorize apenas as vistas que beneficiam da pesquisa semântica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Controle que vistas e ferramentas a IA pode utilizar nesta conversa. Por predefinição, todas as capacidades do agente estão ativadas.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Controla a aleatoriedade (0 = determinístico, 2 = muito criativo)", + "Conversation ID is required": "O ID da conversa é obrigatório", + "Conversation archived": "Conversa arquivada", + "Conversation archived successfully": "Conversa arquivada com sucesso", + "Conversation cleared successfully": "Conversa limpa com sucesso", + "Conversation deleted": "Conversa eliminada", + "Conversation not found": "Conversa não encontrada", + "Conversation permanently deleted": "Conversa eliminada permanentemente", + "Conversation renamed": "Conversa renomeada", + "Conversation restored": "Conversa restaurada", + "Conversation started with {agent}": "Conversa iniciada com {agent}", + "Conversation title": "Título da conversa", + "Conversations": "Conversas", + "Converting {schema} to blob storage...": "A converter {schema} para armazenamento de blobs...", + "Converting {schema} to magic table...": "A converter {schema} para tabela mágica...", + "Copied!": "Copiado!", + "Copy": "Copiar", + "Copy Changes": "Copiar alterações", + "Copy Collection": "Copiar coleção", + "Copy Data": "Copiar dados", + "Copy Full Data": "Copiar todos os dados", + "Copy of {name}": "Cópia de {name}", + "Copy to clipboard": "Copiar para a área de transferência", + "Copying...": "A copiar...", + "Costs": "Custos", + "Count": "Contagem", + "Counting objects...": "A contar objetos...", + "Create": "Criar", + "Create API Token": "Criar token de API", + "Create Agent": "Criar agente", + "Create Application": "Criar aplicação", + "Create Collection": "Criar coleção", + "Create ConfigSet": "Criar ConfigSet", + "Create New Collection": "Criar nova coleção", + "Create New ConfigSet": "Criar novo ConfigSet", + "Create Operations": "Operações de criação", + "Create Organisation": "Criar organização", + "Create Webhook": "Criar webhook", + "Create a copy of collection:": "Criar uma cópia da coleção:", + "Create a new ConfigSet based on the _default template": "Criar um novo ConfigSet baseado no modelo _default", + "Create a new SOLR collection from an existing ConfigSet": "Criar uma nova coleção SOLR a partir de um ConfigSet existente", + "Create a new SOLR collection from an existing configset": "Criar uma nova coleção SOLR a partir de um configset existente", + "Create a new configset based on the _default template": "Criar um novo configset baseado no modelo _default", + "Create a new event or link an existing one from any of your calendars.": "Crie um novo evento ou associe um existente de qualquer um dos seus calendários.", + "Create another": "Criar outro", + "Create copies of": "Criar cópias de", + "Create event": "Criar evento", + "Create new token": "Criar novo token", + "Create or link a Deck card to track work on this object.": "Crie ou associe um cartão Deck para acompanhar o trabalho neste objeto.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Crie a primeira verwerkingsactiviteit para começar a etiquetar as linhas do trilho de auditoria com a respetiva atribuição AVG Art 30.", + "Create your first AI agent to get started.": "Crie o seu primeiro agente de IA para começar.", + "Created": "Criado", + "Created:": "Criado:", + "Creating search collections...": "A criar coleções de pesquisa...", + "Creating...": "A criar...", + "Current": "Atual", + "Current Filters": "Filtros atuais", + "Current Type:": "Tipo atual:", + "Current password": "Palavra-passe atual", + "Custom API endpoint if using a different region": "Ponto de extremidade de API personalizado se utilizar uma região diferente", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Cabeçalhos HTTP personalizados (um por linha, formato: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Cabeçalhos HTTP personalizados (um por linha, formato: header-name: value)", + "Custom Ranges:": "Intervalos personalizados:", + "Custom display title for this facet": "Título de apresentação personalizado para esta faceta", + "Custom pattern:": "Padrão personalizado:", + "DELETE COLLECTION": "DELETE COLLECTION", + "DSAR": "DSAR", + "Daily": "Diariamente", + "Dashboard": "Painel", + "Dashboard not found": "Painel não encontrado", + "Data": "Dados", + "Data Source": "Fonte de dados", + "Data Sources": "Fontes de dados", + "Data Views": "Vistas de dados", + "Data sources": "Fontes de dados", + "Data type does not match observed values": "O tipo de dados não corresponde aos valores observados", + "Data-subject access request": "Pedido de acesso do titular dos dados", + "Database URL": "URL da base de dados", + "Database information refreshed": "Informações da base de dados atualizadas", + "Date Faceting:": "Facetação por data:", + "Date Range": "Intervalo de datas", + "Dates": "Datas", + "Deactivation request cancelled": "Pedido de desativação cancelado", + "Deactivation request submitted": "Pedido de desativação submetido", + "Deck": "Deck", + "Deck integration is not available": "A integração do Deck não está disponível", + "Default": "Predefinição", + "Default Object Tenant": "Inquilino de objeto predefinido", + "Default Organisation": "Organização predefinida", + "Default Owner": "Proprietário predefinido", + "Default User (for cron/background jobs)": "Utilizador predefinido (para tarefas cron/em segundo plano)", + "Default User Tenant": "Inquilino de utilizador predefinido", + "Default value": "Valor predefinido", + "Default value (Email)": "Valor predefinido (E-mail)", + "Default value (Password)": "Valor predefinido (Palavra-passe)", + "Default value (Phone number)": "Valor predefinido (Número de telefone)", + "Default value (Regex)": "Valor predefinido (Regex)", + "Default: 5": "Predefinição: 5", + "Defaults to current user. Select a different user if needed.": "A predefinição é o utilizador atual. Selecione um utilizador diferente, se necessário.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Os atrasos duplicam a cada tentativa (2, 4, 8 minutos...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Os atrasos aumentam linearmente (5, 10, 15 minutos...)", + "Delete": "Eliminar", + "Delete ({count})": "Eliminar ({count})", + "Delete Application": "Eliminar aplicação", + "Delete Audit Trail": "Eliminar trilho de auditoria", + "Delete Collection": "Eliminar coleção", + "Delete ConfigSet": "Eliminar ConfigSet", + "Delete Objects": "Eliminar objetos", + "Delete Operations": "Operações de eliminação", + "Delete SOLR Collection": "Eliminar coleção SOLR", + "Delete View": "Eliminar vista", + "Delete all objects in this schema": "Eliminar todos os objetos neste esquema", + "Delete permanently": "Eliminar permanentemente", + "Delete this conversation?": "Eliminar esta conversa?", + "Delete view": "Eliminar vista", + "Delete {count} object": "Eliminar {count} objeto", + "Delete {count} objects": "Eliminar {count} objetos", + "Deleted": "Eliminado", + "Deleted By": "Eliminado por", + "Deleted Date": "Data de eliminação", + "Deleted Items Management": "Gestão de itens eliminados", + "Deleted This Week": "Eliminados esta semana", + "Deleted Today": "Eliminados hoje", + "Deleted:": "Eliminado:", + "Deleting SOLR Collection...": "A eliminar coleção SOLR...", + "Deleting...": "A eliminar...", + "Deletion Date Range": "Intervalo de datas de eliminação", + "Deletion Statistics": "Estatísticas de eliminação", + "Deprecated": "Obsoleto", + "Depublish": "Despublicar", + "Description": "Descrição", + "Description Template": "Modelo de descrição", + "Description shown as tooltip": "Descrição apresentada como dica de ferramenta", + "Description:": "Descrição:", + "Detect data types and patterns": "Detetar tipos de dados e padrões", + "Detected At": "Detetado em", + "Detected Format:": "Formato detetado:", + "Detected Issues:": "Problemas detetados:", + "Detected: {date}": "Detetado: {date}", + "Direct permission": "Permissão direta", + "Disable": "Desativar", + "Disable this to make the agent": "Desative isto para tornar o agente", + "Disabled": "Desativado", + "Discover Files": "Descobrir ficheiros", + "Discovered Properties": "Propriedades descobertas", + "Display Format": "Formato de apresentação", + "Display Name": "Nome de apresentação", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Pretende eliminar permanentemente todas as entradas de trilho de auditoria filtradas? Esta ação não pode ser anulada.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Pretende eliminar permanentemente esta entrada de trilho de auditoria? Esta ação não pode ser anulada.", + "Documents": "Documentos", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR ativado", + "Download API Spec": "Transferir especificação da API", + "Download API Specification": "Transferir especificação da API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Transfira uma cópia de todos os seus dados pessoais armazenados no OpenRegister (GDPR Artigo 20.º).", + "Drag a file or files here": "Arraste um ficheiro ou ficheiros para aqui", + "Dry run completed: {count} fields would be created": "Execução de teste concluída: seriam criados {count} campos", + "Dry run completed: {count} fields would be fixed": "Execução de teste concluída: seriam corrigidos {count} campos", + "Duration in seconds (optional)": "Duração em segundos (opcional)", + "Edit": "Editar", + "Edit Agent": "Editar agente", + "Edit Application": "Editar aplicação", + "Edit Configuration": "Editar configuração", + "Edit Endpoint": "Editar ponto de extremidade", + "Edit Organisation": "Editar organização", + "Edit Property '{key}' of '{title}'": "Editar propriedade '{key}' de '{title}'", + "Edit Register": "Editar registo", + "Edit Schema": "Editar esquema", + "Edit Source": "Editar fonte", + "Edit View": "Editar vista", + "Edit Webhook": "Editar webhook", + "Edit labels": "Editar etiquetas", + "Edit processing activity": "Editar verwerkingsactiviteit", + "Edit tags for {name}": "Editar etiquetas de {name}", + "Edit verwerkingsactiviteit": "Editar verwerkingsactiviteit", + "Edit view": "Editar vista", + "Edit view details": "Editar detalhes da vista", + "Effectiveness": "Eficácia", + "Email": "E-mail", + "Email Addresses": "Endereços de e-mail", + "Email digest frequency": "Frequência do resumo por e-mail", + "Emails": "E-mails", + "Embedding Model": "Modelo de incorporação", + "Embedding Provider": "Fornecedor de incorporação", + "Embedding generated successfully": "Incorporação gerada com sucesso", + "Embedding provider connection successful!": "Ligação ao fornecedor de incorporação estabelecida com sucesso!", + "Embedding test failed: {error}": "O teste de incorporação falhou: {error}", + "Enable": "Ativar", + "Enable RAG": "Ativar RAG", + "Enable auto-creation": "Ativar criação automática", + "Enable automatic file vectorization": "Ativar vetorização automática de ficheiros", + "Enable automatic object vectorization": "Ativar vetorização automática de objetos", + "Enable automatic synchronization": "Ativar sincronização automática", + "Enable calendar provider": "Ativar fornecedor de calendário", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Ativar registo detalhado para operações SOLR (recomendado para depuração)", + "Enable faceting": "Ativar facetação", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Ative ou desative as funcionalidades de LLM. Configure os fornecedores e modelos utilizando o botão Configuração de LLM acima.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Ative ou desative as funcionalidades de LLM. Configure os fornecedores e modelos utilizando o botão de configuração de LLM acima.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Ative ou desative a integração de pesquisa SOLR. Configure as definições de ligação utilizando o botão Definições de ligação acima.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Ative ou desative a integração de pesquisa SOLR. Configure as definições de ligação utilizando o botão de definições de ligação acima.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Ative ou desative a integração de fluxos de trabalho n8n. Configure as definições de ligação abaixo.", + "Enable or disable this webhook": "Ativar ou desativar este webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Ative ferramentas que permitem ao agente interagir com os dados através de chamadas de função.", + "Enable vectorization for all existing and future views (may increase costs)": "Ativar a vetorização para todas as vistas existentes e futuras (pode aumentar os custos)", + "Enable write-back to target objects": "Ativar a regravação nos objetos de destino", + "Enabled": "Ativado", + "End Date Field": "Campo de data de fim", + "Endpoint Path*": "Caminho do ponto de extremidade*", + "Endpoints": "Pontos de extremidade", + "Engine not found": "Motor não encontrado", + "Enter ConfigSet name": "Introduza o nome do ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Introduza os nomes de utilizador do Nextcloud para conceder acesso a este agente privado", + "Enter agent description (optional)": "Introduza a descrição do agente (opcional)", + "Enter agent name": "Introduza o nome do agente", + "Enter application description (optional)": "Introduza a descrição da aplicação (opcional)", + "Enter application name": "Introduza o nome da aplicação", + "Enter collection name": "Introduza o nome da coleção", + "Enter configuration description (optional)": "Introduza a descrição da configuração (opcional)", + "Enter configuration title": "Introduza o título da configuração", + "Enter description (optional)...": "Introduza a descrição (opcional)...", + "Enter new collection name": "Introduza o nome da nova coleção", + "Enter object ID": "Introduza o ID do objeto", + "Enter organisation description (optional)": "Introduza a descrição da organização (opcional)", + "Enter organisation name": "Introduza o nome da organização", + "Enter search term": "Introduza o termo de pesquisa", + "Enter search terms or leave empty to browse all": "Introduza termos de pesquisa ou deixe vazio para navegar por todos", + "Enter system prompt for the agent": "Introduza o prompt de sistema para o agente", + "Enter username and press Enter": "Introduza o nome de utilizador e prima Enter", + "Enter view name...": "Introduza o nome da vista...", + "Enter webhook name": "Introduza o nome do webhook", + "Enter your API key": "Introduza a sua chave de API", + "Enter your object here...": "Introduza o seu objeto aqui...", + "Enter your schema here...": "Introduza o seu esquema aqui...", + "Entities": "Entidades", + "Entities detected": "Entidades detetadas", + "Entity ID: {id}": "ID da entidade: {id}", + "Entity Information": "Informações da entidade", + "Entity Recognition": "Reconhecimento de entidades", + "Entity Recognition Method": "Método de reconhecimento de entidades", + "Entity deleted successfully": "Entidade eliminada com sucesso", + "Entity not found": "Entidade não encontrada", + "Entries to be deleted:": "Entradas a eliminar:", + "Enum Issue": "Problema de enumeração", + "Enum constraint is missing": "A restrição de enumeração está em falta", + "Erase (Art 17)": "Apagar (Art. 17.º)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Apagar {count} objeto(s) deste titular? Esta ação é registada no trilho de auditoria.", + "Erasure complete": "Apagamento concluído", + "Erasure preview": "Pré-visualização do apagamento", + "Error": "Erro", + "Error Details": "Detalhes do erro", + "Error Information": "Informações do erro", + "Error Message": "Mensagem de erro", + "Error deleting audit trails: {error}": "Erro ao eliminar trilhos de auditoria: {error}", + "Error deleting search trails: {error}": "Erro ao eliminar trilhos de pesquisa: {error}", + "Error loading application": "Erro ao carregar a aplicação", + "Error loading audit trails": "Erro ao carregar os trilhos de auditoria", + "Error loading entity": "Erro ao carregar a entidade", + "Error loading search trails": "Erro ao carregar os trilhos de pesquisa", + "Estimated Batches:": "Lotes estimados:", + "Estimated Cost:": "Custo estimado:", + "Estimated Duration:": "Duração estimada:", + "Event": "Evento", + "Event Property for Payload": "Propriedade de evento para o payload", + "Events": "Eventos", + "Every schema with detected PII has a processing-activity annotation.": "Todos os esquemas com dados pessoais (PII) detetados têm uma anotação de verwerkingsactiviteit.", + "Example": "Exemplo", + "Example Value": "Valor de exemplo", + "Example value for this property": "Valor de exemplo para esta propriedade", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Máximo Exclusivo", + "Exclusive Minimum": "Mínimo Exclusivo", + "Exclusive maximum": "Máximo exclusivo", + "Exclusive minimum": "Mínimo exclusivo", + "Execution Mode": "Modo de Execução", + "Execution Time": "Tempo de Execução", + "Execution Time Range": "Intervalo de Tempo de Execução", + "Existing Improvements": "Melhorias Existentes", + "Expand details": "Expandir detalhes", + "Expiration": "Expiração", + "Expires": "Expira", + "Expires in (e.g., 90d)": "Expira em (por exemplo, 90d)", + "Exponential": "Exponencial", + "Export": "Exportar", + "Export \"{schema}\" objects from \"{register}\"": "Exportar objetos de \"{schema}\" do \"{register}\"", + "Export Configuration": "Exportar Configuração", + "Export Format:": "Formato de Exportação:", + "Export Objects": "Exportar Objetos", + "Export completed successfully": "Exportação concluída com sucesso", + "Export downloaded successfully": "Exportação transferida com sucesso", + "Export failed: {error}": "A exportação falhou: {error}", + "Export is rate limited. Please try again later.": "A exportação está limitada por taxa. Tente novamente mais tarde.", + "Export my data": "Exportar os meus dados", + "Export, view, or delete audit trails": "Exportar, visualizar ou eliminar registos de auditoria", + "Exporting...": "A exportar...", + "Extend Schema": "Estender Esquema", + "Extended": "Estendido", + "Extensions": "Extensões", + "Extra Query Parameters": "Parâmetros de Consulta Adicionais", + "Extract Now": "Extrair Agora", + "Extract Pending Files": "Extrair Ficheiros Pendentes", + "Extract now": "Extrair agora", + "Extract properties from each object": "Extrair propriedades de cada objeto", + "Extracted At": "Extraído Em", + "Extracted at": "Extraído em", + "Extraction": "Extração", + "Extraction Mode": "Modo de Extração", + "Extraction Scope": "Âmbito de Extração", + "Extraction Status": "Estado da Extração", + "Extraction error for {file}: {error}": "Erro de extração para {file}: {error}", + "Facet Description": "Descrição da Faceta", + "Facet Order": "Ordem da Faceta", + "Facet Title": "Título da Faceta", + "Facet Type": "Tipo de Faceta", + "Facet configuration updated successfully": "Configuração de faceta atualizada com sucesso", + "Facetable": "Facetável", + "Faceting Configuration:": "Configuração de Facetas:", + "Facets discovered and configured successfully": "Facetas descobertas e configuradas com sucesso", + "Facets discovered successfully": "Facetas descobertas com sucesso", + "Failed": "Falhou", + "Failed to add attachment to object": "Falha ao adicionar anexo ao objeto", + "Failed to add user to organisation": "Falha ao adicionar utilizador à organização", + "Failed to analyze schema properties": "Falha ao analisar propriedades do esquema", + "Failed to archive conversation": "Falha ao arquivar conversa", + "Failed to calculate sizes": "Falha ao calcular tamanhos", + "Failed to cancel deactivation": "Falha ao cancelar a desativação", + "Failed to change active organisation: {error}": "Falha ao alterar a organização ativa: {error}", + "Failed to change password": "Falha ao alterar a palavra-passe", + "Failed to check version: {error}": "Falha ao verificar a versão: {error}", + "Failed to clear audit trails: {error}": "Falha ao limpar registos de auditoria: {error}", + "Failed to clear blob objects: {error}": "Falha ao limpar objetos blob: {error}", + "Failed to clear cache: {error}": "Falha ao limpar a cache: {error}", + "Failed to clear collection": "Falha ao limpar a coleção", + "Failed to clear conversation": "Falha ao limpar a conversa", + "Failed to clear embeddings: {error}": "Falha ao limpar embeddings: {error}", + "Failed to clear search trails: {error}": "Falha ao limpar registos de pesquisa: {error}", + "Failed to clear {type} cache: {error}": "Falha ao limpar a cache de {type}: {error}", + "Failed to copy changes": "Falha ao copiar alterações", + "Failed to copy data": "Falha ao copiar dados", + "Failed to copy data to clipboard": "Falha ao copiar dados para a área de transferência", + "Failed to copy to clipboard": "Falha ao copiar para a área de transferência", + "Failed to copy token": "Falha ao copiar token", + "Failed to create conversation": "Falha ao criar conversa", + "Failed to create missing SOLR fields: {error}": "Falha ao criar campos SOLR em falta: {error}", + "Failed to create or find project": "Falha ao criar ou encontrar projeto", + "Failed to create token": "Falha ao criar token", + "Failed to delete SOLR collection: {error}": "Falha ao eliminar a coleção SOLR: {error}", + "Failed to delete collection": "Falha ao eliminar a coleção", + "Failed to delete conversation": "Falha ao eliminar a conversa", + "Failed to delete objects for {schema}: {error}": "Falha ao eliminar objetos de {schema}: {error}", + "Failed to delete webhook": "Falha ao eliminar webhook", + "Failed to depublish register: {error}": "Falha ao despublicar o registo: {error}", + "Failed to depublish schema: {error}": "Falha ao despublicar o esquema: {error}", + "Failed to depublish: {error}": "Falha ao despublicar: {error}", + "Failed to discover files: {error}": "Falha ao descobrir ficheiros: {error}", + "Failed to download API specification": "Falha ao transferir a especificação da API", + "Failed to export data": "Falha ao exportar dados", + "Failed to export objects": "Falha ao exportar objetos", + "Failed to fetch conversation history": "Falha ao obter o histórico da conversa", + "Failed to fix mismatched SOLR fields: {error}": "Falha ao corrigir campos SOLR incompatíveis: {error}", + "Failed to get SOLR field configuration": "Falha ao obter a configuração de campos SOLR", + "Failed to get chat statistics": "Falha ao obter estatísticas de conversa", + "Failed to get database information: %s": "Falha ao obter informações da base de dados: %s", + "Failed to get entity categories": "Falha ao obter categorias de entidades", + "Failed to get entity statistics": "Falha ao obter estatísticas de entidades", + "Failed to get entity types": "Falha ao obter tipos de entidades", + "Failed to get response: {error}": "Falha ao obter resposta: {error}", + "Failed to invalidate app store cache: {error}": "Falha ao invalidar a cache da App Store: {error}", + "Failed to leave organisation: {error}": "Falha ao sair da organização: {error}", + "Failed to link object": "Falha ao associar objeto", + "Failed to load Deck cards": "Falha ao carregar cartões do Deck", + "Failed to load LLM configuration": "Falha ao carregar a configuração do LLM", + "Failed to load Nextcloud groups": "Falha ao carregar grupos do Nextcloud", + "Failed to load cache statistics: {error}": "Falha ao carregar estatísticas da cache: {error}", + "Failed to load conversation": "Falha ao carregar a conversa", + "Failed to load entities": "Falha ao carregar entidades", + "Failed to load entity": "Falha ao carregar entidade", + "Failed to load extraction data": "Falha ao carregar dados de extração", + "Failed to load files": "Falha ao carregar ficheiros", + "Failed to load linked contacts": "Falha ao carregar contactos associados", + "Failed to load linked emails": "Falha ao carregar emails associados", + "Failed to load linked events": "Falha ao carregar eventos associados", + "Failed to load organisations": "Falha ao carregar organizações", + "Failed to load register data": "Falha ao carregar dados do registo", + "Failed to load related entities": "Falha ao carregar entidades relacionadas", + "Failed to load settings: {error}": "Falha ao carregar definições: {error}", + "Failed to load statistics: {error}": "Falha ao carregar estatísticas: {error}", + "Failed to load templates": "Falha ao carregar modelos", + "Failed to load view: {error}": "Falha ao carregar a vista: {error}", + "Failed to load webhooks": "Falha ao carregar webhooks", + "Failed to load workflows": "Falha ao carregar fluxos de trabalho", + "Failed to process message": "Falha ao processar mensagem", + "Failed to publish register: {error}": "Falha ao publicar o registo: {error}", + "Failed to publish schema: {error}": "Falha ao publicar o esquema: {error}", + "Failed to publish: {error}": "Falha ao publicar: {error}", + "Failed to refresh database information": "Falha ao atualizar informações da base de dados", + "Failed to reindex collection": "Falha ao reindexar a coleção", + "Failed to remove avatar": "Falha ao remover o avatar", + "Failed to remove link": "Falha ao remover a associação", + "Failed to remove member: {error}": "Falha ao remover membro: {error}", + "Failed to remove schema {schema}: {error}": "Falha ao remover o esquema {schema}: {error}", + "Failed to remove user from organisation": "Falha ao remover utilizador da organização", + "Failed to rename conversation": "Falha ao renomear a conversa", + "Failed to request deactivation": "Falha ao solicitar a desativação", + "Failed to restore conversation": "Falha ao restaurar a conversa", + "Failed to retrieve extraction status.": "Falha ao obter o estado da extração.", + "Failed to retrieve objects for file.": "Falha ao obter objetos do ficheiro.", + "Failed to retry extraction": "Falha ao repetir a extração", + "Failed to retry webhook": "Falha ao repetir o webhook", + "Failed to revoke token": "Falha ao revogar token", + "Failed to save GitHub token": "Falha ao guardar o token do GitHub", + "Failed to save GitLab URL": "Falha ao guardar o URL do GitLab", + "Failed to save GitLab token": "Falha ao guardar o token do GitLab", + "Failed to save LLM settings: {error}": "Falha ao guardar definições do LLM: {error}", + "Failed to save additional feedback": "Falha ao guardar feedback adicional", + "Failed to save configuration.": "Falha ao guardar a configuração.", + "Failed to save configuration: {error}": "Falha ao guardar a configuração: {error}", + "Failed to save facet configuration": "Falha ao guardar a configuração de facetas", + "Failed to save feedback": "Falha ao guardar feedback", + "Failed to save file settings: {error}": "Falha ao guardar definições de ficheiros: {error}", + "Failed to save n8n configuration": "Falha ao guardar a configuração do n8n", + "Failed to save preferences": "Falha ao guardar preferências", + "Failed to save roles": "Falha ao guardar funções", + "Failed to save settings": "Falha ao guardar definições", + "Failed to save settings: {error}": "Falha ao guardar definições: {error}", + "Failed to save view: {error}": "Falha ao guardar a vista: {error}", + "Failed to save warmup interval: {error}": "Falha ao guardar o intervalo de aquecimento: {error}", + "Failed to save webhook": "Falha ao guardar webhook", + "Failed to send feedback": "Falha ao enviar feedback", + "Failed to set active organisation: {error}": "Falha ao definir a organização ativa: {error}", + "Failed to setup SOLR: {error}": "Falha ao configurar o SOLR: {error}", + "Failed to start file warmup: {error}": "Falha ao iniciar o aquecimento de ficheiros: {error}", + "Failed to start processing {type} files: {error}": "Falha ao iniciar o processamento de ficheiros {type}: {error}", + "Failed to start vectorization: {error}": "Falha ao iniciar a vetorização: {error}", + "Failed to switch organisation: {error}": "Falha ao mudar de organização: {error}", + "Failed to sync magic table for {schema}: {error}": "Falha ao sincronizar a tabela mágica para {schema}: {error}", + "Failed to test SOLR connection: {error}": "Falha ao testar a ligação SOLR: {error}", + "Failed to test webhook": "Falha ao testar o webhook", + "Failed to update LLM settings: {error}": "Falha ao atualizar definições do LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Falha ao atualizar definições de Multilocação: {error}", + "Failed to update RBAC settings: {error}": "Falha ao atualizar definições de RBAC: {error}", + "Failed to update Retention settings: {error}": "Falha ao atualizar definições de Retenção: {error}", + "Failed to update SOLR settings: {error}": "Falha ao atualizar definições do SOLR: {error}", + "Failed to update favorite status": "Falha ao atualizar o estado de favorito", + "Failed to update schema configuration for {schema}: {error}": "Falha ao atualizar a configuração do esquema para {schema}: {error}", + "Failed to update schema properties": "Falha ao atualizar propriedades do esquema", + "Failed to update view": "Falha ao atualizar a vista", + "Failed to update view: {error}": "Falha ao atualizar a vista: {error}", + "Failed to update webhook": "Falha ao atualizar o webhook", + "Failed to upload avatar": "Falha ao carregar o avatar", + "Failed to validate {schema}: {error}": "Falha ao validar {schema}: {error}", + "Failed to vectorize files: {error}": "Falha ao vetorizar ficheiros: {error}", + "Failed to warmup names cache: {error}": "Falha ao aquecer a cache de nomes: {error}", + "Federation": "Federação", + "Feedback recorded": "Feedback registado", + "Fewer sources (1-3):": "Menos fontes (1-3):", + "Field": "Campo", + "Fields to Display": "Campos a Apresentar", + "File": "Ficheiro", + "File #{id}": "Ficheiro #{id}", + "File Chunk Prediction": "Previsão de Fragmentos de Ficheiro", + "File Collection": "Coleção de Ficheiros", + "File Handling": "Tratamento de Ficheiros", + "File Location": "Localização do Ficheiro", + "File Management": "Gestão de Ficheiros", + "File Name": "Nome do Ficheiro", + "File Path": "Caminho do Ficheiro", + "File Type Selection": "Seleção do Tipo de Ficheiro", + "File Types to Process": "Tipos de Ficheiro a Processar", + "File Vectorization": "Vetorização de Ficheiros", + "File Warmup": "Aquecimento de Ficheiros", + "File actions menu": "Menu de ações do ficheiro", + "File anonymized successfully": "Ficheiro anonimizado com sucesso", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "A vetorização de fragmentos de ficheiro ainda não está implementada. Os fragmentos estão prontos e armazenados, mas o serviço de vetorização está em desenvolvimento.", + "File collection not configured": "Coleção de ficheiros não configurada", + "File discovery completed": "Descoberta de ficheiros concluída", + "File extraction completed": "Extração de ficheiros concluída", + "File extraction queued": "Extração de ficheiros em fila", + "File indexed successfully": "Ficheiro indexado com sucesso", + "File is already anonymized": "O ficheiro já está anonimizado", + "File name": "Nome do ficheiro", + "File not found": "Ficheiro não encontrado", + "File settings saved successfully": "Definições de ficheiros guardadas com sucesso", + "File settings updated successfully": "Definições de ficheiros atualizadas com sucesso", + "File sources": "Fontes de ficheiros", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vetorização de ficheiros concluída! {vectorized} fragmentos vetorizados de {files} ficheiros. {failed} falharam.", + "File vectorization configuration saved successfully": "Configuração de vetorização de ficheiros guardada com sucesso", + "File vectorization started. Check the statistics section for progress.": "Vetorização de ficheiros iniciada. Consulte a secção de estatísticas para acompanhar o progresso.", + "File warmup completed": "Aquecimento de ficheiros concluído", + "Files": "Ficheiros", + "Files added successfully": "Ficheiros adicionados com sucesso", + "Files with Completed Extraction:": "Ficheiros com Extração Concluída:", + "Files → fileCollection, Objects → objectCollection": "Ficheiros → fileCollection, Objetos → objectCollection", + "Files → filecollection, objects → objectcollection": "Ficheiros → filecollection, objetos → objectcollection", + "Filter Audit Trails": "Filtrar Registos de Auditoria", + "Filter Deleted Items": "Filtrar Itens Eliminados", + "Filter Objects": "Filtrar Objetos", + "Filter Properties": "Filtrar Propriedades", + "Filter Search Trails": "Filtrar Registos de Pesquisa", + "Filter Statistics": "Filtrar Estatísticas", + "Filter and analyze search trail entries": "Filtrar e analisar entradas de registos de pesquisa", + "Filter and manage audit trail entries": "Filtrar e gerir entradas de registos de auditoria", + "Filter and manage soft deleted items": "Filtrar e gerir itens eliminados de forma reversível", + "Filter and search entities": "Filtrar e pesquisar entidades", + "Filter by object ID": "Filtrar por ID do objeto", + "Filter by search term": "Filtrar por termo de pesquisa", + "Filter by type": "Filtrar por tipo", + "Filter by webhook": "Filtrar por webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Dados de filtro carregados automaticamente. Utilize os filtros abaixo para refinar a sua pesquisa.", + "Filter fields...": "Filtrar campos...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrar acionadores de webhook por propriedades do payload (uma por linha, formato: chave: valor)", + "Filtered": "Filtrado", + "Filters": "Filtros", + "Finalizing setup...": "A finalizar a configuração...", + "Fireworks AI Chat Settings": "Definições de Conversa do Fireworks AI", + "Fireworks AI Embedding Configuration": "Configuração de Embedding do Fireworks AI", + "First": "Primeiro", + "Fixed": "Fixo", + "For chat and retrieval-augmented generation": "Para conversa e geração aumentada por recuperação", + "For vector embeddings and semantic search": "Para embeddings vetoriais e pesquisa semântica", + "Format": "Formato", + "Format Issue": "Problema de Formato", + "Format constraint is missing": "Falta a restrição de formato", + "From (e.g. -7 days or 2025-01-01)": "De (por exemplo, -7 days ou 2025-01-01)", + "From Date": "Data Inicial", + "From date": "Data inicial", + "Full data copied to clipboard": "Dados completos copiados para a área de transferência", + "Function Tools": "Ferramentas de Função", + "General Issue": "Problema Geral", + "Generate recommendations and confidence scores": "Gerar recomendações e pontuações de confiança", + "Generate report": "Gerar relatório", + "Generate the verantwoordingsdocument": "Gerar o verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Gerar vetores imediatamente quando novos objetos são criados", + "Generated: {time}": "Gerado: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Obtenha informações em tempo real sobre a saúde dos dados da sua organização, focando-se em registos, definições de esquema e armazenamento e utilização de objetos.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Obtenha informações em tempo real sobre a saúde dos dados da sua organização, tendências de crescimento e métricas de qualidade.", + "GitHub Personal Access Token": "Token de Acesso Pessoal do GitHub", + "GitHub token is valid": "O token do GitHub é válido", + "GitHub token is valid! Username: {username}": "O token do GitHub é válido! Nome de utilizador: {username}", + "GitHub token removed successfully": "Token do GitHub removido com sucesso", + "GitHub token saved successfully": "Token do GitHub guardado com sucesso", + "GitHub token test failed: {message}": "O teste do token do GitHub falhou: {message}", + "GitLab Personal Access Token": "Token de Acesso Pessoal do GitLab", + "GitLab URL saved successfully": "URL do GitLab guardado com sucesso", + "GitLab token is valid": "O token do GitLab é válido", + "GitLab token is valid! Username: {username}": "O token do GitLab é válido! Nome de utilizador: {username}", + "GitLab token saved successfully": "Token do GitLab guardado com sucesso", + "GitLab token test failed: {message}": "O teste do token do GitLab falhou: {message}", + "Go to linked Register": "Ir para o Registo associado", + "Go to linked Schema": "Ir para o Esquema associado", + "Go to organisation": "Ir para a organização", + "Grant additional permissions beyond standard CRUD operations": "Conceder permissões adicionais além das operações CRUD padrão", + "Group Quota": "Quota do Grupo", + "Groups": "Grupos", + "HTML preview": "Pré-visualização HTML", + "HTTP Method": "Método HTTP", + "HTTP method used to send webhook requests": "Método HTTP utilizado para enviar pedidos de webhook", + "Headers": "Cabeçalhos", + "Health": "Saúde", + "Heartbeat successful - connection kept alive": "Heartbeat bem-sucedido - ligação mantida ativa", + "Helpful": "Útil", + "Here you can configure the details for various connections.": "Aqui pode configurar os detalhes de várias ligações.", + "Here you can configure the details for your organisation.": "Aqui pode configurar os detalhes da sua organização.", + "Hide": "Ocultar", + "Hide Filters": "Ocultar Filtros", + "Hide in collection view": "Ocultar na vista de coleção", + "Hide in forms": "Ocultar nos formulários", + "Hide in list view": "Ocultar na vista de lista", + "High": "Alto", + "High Confidence": "Confiança Elevada", + "Host": "Anfitrião", + "Hourly": "De hora a hora", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Quão profundamente percorrer propriedades de objetos aninhados (1-20). Valores mais elevados captam mais detalhe, mas aumentam o tamanho do vetor.", + "How often to check for updates (1-168 hours)": "Com que frequência verificar atualizações (1-168 horas)", + "How to handle retries for failed webhook deliveries": "Como tratar as repetições de entregas de webhook falhadas", + "IBANs": "IBANs", + "ID": "ID", + "ID of input mapping (optional)": "ID do mapeamento de entrada (opcional)", + "ID of output mapping (optional)": "ID do mapeamento de saída (opcional)", + "ID of the target resource": "ID do recurso de destino", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Limites de taxa de IP limpos com sucesso", + "Identify properties not in the schema": "Identificar propriedades que não estão no esquema", + "Immutable": "Imutável", + "Import": "Importar", + "Import Configuration": "Importar Configuração", + "Import Data into Register": "Importar Dados para o Registo", + "Import successful": "Importação bem-sucedida", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importe o modelo report-bundle.json para obter o registo `reports` e, em seguida, crie o seu primeiro painel através da interface de objetos padrão. Os painéis declaram os seus widgets em JSON e o renderizador alimenta cada widget com dados de agregação em tempo real.", + "Improved Property": "Propriedade Melhorada", + "In use": "Em utilização", + "Inactive": "Inativo", + "Inactive organisations cannot be used": "As organizações inativas não podem ser utilizadas", + "Include IDs and names of related objects for better contextual search": "Incluir IDs e nomes de objetos relacionados para melhor pesquisa contextual", + "Include ids and names of related objects for better contextual search": "Incluir ids e nomes de objetos relacionados para melhor pesquisa contextual", + "Include related object references": "Incluir referências de objetos relacionados", + "Include schema and register metadata": "Incluir metadados de esquema e registo", + "Index": "Índice", + "Information about the current OpenRegister installation": "Informações sobre a instalação atual do OpenRegister", + "Inherited from register": "Herdado do registo", + "Inherits permissions from register": "Herda permissões do registo", + "Initialization failed": "A inicialização falhou", + "Initialize Project": "Inicializar Projeto", + "Initializing SOLR setup...": "A inicializar a configuração do SOLR...", + "Input Mapping": "Mapeamento de Entrada", + "Inspect Fields": "Inspecionar Campos", + "Inspect Index": "Inspecionar Índice", + "Inspect SOLR Index": "Inspecionar Índice SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instale a aplicação notify_push a partir da Nextcloud App Store para ativar atualizações em tempo real.", + "Integrations": "Integrações", + "Internet location (URL) of this installation": "Localização na Internet (URL) desta instalação", + "Interval": "Intervalo", + "Invalid": "Inválido", + "Invalid batch size. Must be between 1 and 5000": "Tamanho de lote inválido. Deve estar entre 1 e 5000", + "Invalid feedback type": "Tipo de feedback inválido", + "Invalid field name provided": "Nome de campo inválido fornecido", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects inválido. Deve ser 0 (todos) ou um número positivo", + "Invalid:": "Inválido:", + "Invite Users": "Convidar Utilizadores", + "Invited Users": "Utilizadores Convidados", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Resultados de Inzage", + "Issue": "Problema", + "Items per page:": "Itens por página:", + "Just now": "Agora mesmo", + "Key": "Chave", + "LLM Configuration": "Configuração do LLM", + "LLM actions menu": "Menu de ações do LLM", + "LLM configuration saved successfully": "Configuração do LLM guardada com sucesso", + "LLM features disabled": "Funcionalidades do LLM desativadas", + "LLM features enabled": "Funcionalidades do LLM ativadas", + "LLM must be enabled with an embedding provider configured": "O LLM deve estar ativado com um fornecedor de embedding configurado", + "LLM settings saved successfully": "Definições do LLM guardadas com sucesso", + "LLM settings updated successfully": "Definições do LLM atualizadas com sucesso", + "Label": "Etiqueta", + "Labels": "Etiquetas", + "Languages": "Idiomas", + "Last": "Último", + "Last Triggered": "Último Acionamento", + "Leave empty to allow all users access": "Deixe vazio para permitir o acesso a todos os utilizadores", + "Leave off for auto-detection from property format.": "Deixe desativado para deteção automática a partir do formato da propriedade.", + "Left organisation successfully": "Saiu da organização com sucesso", + "Legal basis *": "Base legal *", + "Length Range:": "Intervalo de Comprimento:", + "Linear": "Linear", + "Link": "Associar", + "Link an email from the Mail app sidebar to associate it with this object.": "Associe um email a partir da barra lateral da aplicação Mail para o ligar a este objeto.", + "Link an object to see it here.": "Associe um objeto para o ver aqui.", + "Link another object": "Associar outro objeto", + "Link existing event": "Associar evento existente", + "Link removed": "Associação removida", + "Link to Object": "Associar a Objeto", + "Link to {name}": "Associar a {name}", + "Linked Objects": "Objetos Associados", + "Linked by {user}": "Associado por {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Emails, eventos de calendário, contactos e cartões do Deck associados aparecerão aqui.", + "Linked to {name}": "Associado a {name}", + "Load Advanced Filters": "Carregar Filtros Avançados", + "Load advanced filters with live data from your search index": "Carregar filtros avançados com dados em tempo real do seu índice de pesquisa", + "Load more": "Carregar mais", + "Load view": "Carregar vista", + "Loading API tokens...": "A carregar tokens da API...", + "Loading ConfigSets...": "A carregar ConfigSets...", + "Loading LLM configuration...": "A carregar a configuração do LLM...", + "Loading RBAC settings...": "A carregar definições de RBAC...", + "Loading activity...": "A carregar atividade...", + "Loading advanced filters...": "A carregar filtros avançados...", + "Loading agents...": "A carregar agentes...", + "Loading application details...": "A carregar detalhes da aplicação...", + "Loading applications...": "A carregar aplicações...", + "Loading archived conversations...": "A carregar conversas arquivadas...", + "Loading audit trails...": "A carregar registos de auditoria...", + "Loading available tools...": "A carregar ferramentas disponíveis...", + "Loading cache statistics...": "A carregar estatísticas da cache...", + "Loading collections...": "A carregar coleções...", + "Loading configurations...": "A carregar configurações...", + "Loading conversation...": "A carregar conversa...", + "Loading conversations...": "A carregar conversas...", + "Loading deleted items...": "A carregar itens eliminados...", + "Loading endpoints": "A carregar endpoints", + "Loading entities...": "A carregar entidades...", + "Loading events...": "A carregar eventos...", + "Loading filter data...": "A carregar dados de filtro...", + "Loading groups...": "A carregar grupos...", + "Loading linked objects...": "A carregar objetos associados...", + "Loading log details...": "A carregar detalhes do registo...", + "Loading multitenancy settings...": "A carregar definições de multilocação...", + "Loading n8n configuration...": "A carregar a configuração do n8n...", + "Loading permission matrix...": "A carregar matriz de permissões...", + "Loading preferences...": "A carregar preferências...", + "Loading register data...": "A carregar dados do registo...", + "Loading registers...": "A carregar registos...", + "Loading retention settings...": "A carregar definições de retenção...", + "Loading schemas...": "A carregar esquemas...", + "Loading search configuration...": "A carregar a configuração de pesquisa...", + "Loading search trails...": "A carregar registos de pesquisa...", + "Loading sources...": "A carregar fontes...", + "Loading statistics...": "A carregar estatísticas...", + "Loading text extraction settings...": "A carregar definições de extração de texto...", + "Loading tokens...": "A carregar tokens...", + "Loading user groups...": "A carregar grupos de utilizadores...", + "Loading users...": "A carregar utilizadores...", + "Loading views...": "A carregar vistas...", + "Loading...": "A carregar...", + "Local": "Local", + "Local Version": "Versão Local", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Localize todos os objetos que referenciam um titular de dados (Art 15 inzage), pré-visualize uma eliminação (Art 17 vergetelheid) ou exporte os seus dados (Art 20 portabiliteit).", + "Location": "Localização", + "Location Field": "Campo de Localização", + "Locations": "Localizações", + "Locked": "Bloqueado", + "Locked:": "Bloqueado:", + "Login successful": "Início de sessão bem-sucedido", + "Logs": "Registos", + "Low": "Baixo", + "Low Confidence": "Confiança Baixa", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Números mais baixos aparecem primeiro na barra lateral de filtros. Deixe vazio para ordenação automática.", + "Lucene Version:": "Versão do Lucene:", + "Magic table sync completed for {schema}": "Sincronização da tabela mágica concluída para {schema}", + "Magic table synchronized successfully": "Tabela mágica sincronizada com sucesso", + "Mail Integration": "Integração com Mail", + "Mail integration is not available": "A integração com Mail não está disponível", + "Make this installation discoverable within the federation network.": "Tornar esta instalação detetável dentro da rede de federação.", + "Manage Organisation Roles": "Gerir Funções da Organização", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Faça a gestão das Coleções SOLR (armazenamentos de dados) e atribua-as a objetos e ficheiros.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Faça a gestão dos ConfigSets SOLR (modelos de configuração) para as suas coleções.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Faça a gestão das coleções SOLR (armazenamentos de dados) e atribua-as a objetos e ficheiros.", + "Manage SOLR configsets (configuration templates) for your collections.": "Faça a gestão dos configsets SOLR (modelos de configuração) para as suas coleções.", + "Manage and configure agents for automated tasks": "Faça a gestão e configure agentes para tarefas automatizadas", + "Manage and monitor file text extraction status": "Faça a gestão e monitorize o estado da extração de texto de ficheiros", + "Manage and restore soft deleted items from your registers": "Faça a gestão e restaure itens eliminados de forma reversível dos seus registos", + "Manage and view detected entities from files and objects": "Faça a gestão e visualize entidades detetadas em ficheiros e objetos", + "Manage document templates and themes": "Faça a gestão de modelos e temas de documentos", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Faça a gestão dos fluxos de trabalho do n8n para a automação do OpenRegister. Os fluxos de trabalho serão armazenados no projeto configurado.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Faça a gestão de atividades de tratamento, execute pedidos de acesso de titulares de dados e audite a conformidade ao abrigo do GDPR da UE / AVG neerlandês.", + "Manage webhooks for event-driven integrations": "Faça a gestão de webhooks para integrações orientadas por eventos", + "Manage your account settings, security, and personal data.": "Faça a gestão das definições da sua conta, segurança e dados pessoais.", + "Manage your applications and modules": "Faça a gestão das suas aplicações e módulos", + "Manage your chat conversations": "Faça a gestão das suas conversas", + "Manage your data registers and their configurations": "Faça a gestão dos seus registos de dados e respetivas configurações", + "Manage your data schemas and their properties": "Faça a gestão dos seus esquemas de dados e respetivas propriedades", + "Manage your data sources and their configurations": "Faça a gestão das suas fontes de dados e respetivas configurações", + "Manage your organisations and switch between them": "Faça a gestão das suas organizações e alterne entre elas", + "Manage your saved search configurations": "Faça a gestão das suas configurações de pesquisa guardadas", + "Manage your system configurations and settings": "Faça a gestão das configurações e definições do seu sistema", + "Managed": "Gerido", + "Mappings": "Mapeamentos", + "Mass validation completed successfully": "Validação em massa concluída com sucesso", + "Mass validation failed: {error}": "A validação em massa falhou: {error}", + "Max Files (0 = all)": "Máximo de Ficheiros (0 = todos)", + "Max Length": "Comprimento Máximo", + "Max Length:": "Comprimento Máximo:", + "Max Objects (0 = all)": "Máximo de Objetos (0 = todos)", + "Max Retries": "Máximo de Repetições", + "Max Shards/Node": "Máximo de Shards/Nó", + "Max Tokens": "Máximo de Tokens", + "Max execution time (ms)": "Tempo máximo de execução (ms)", + "Max ms": "Máximo de ms", + "Max result count": "Contagem máxima de resultados", + "Max results": "Máximo de resultados", + "Maximum": "Máximo", + "Maximum File Size (MB)": "Tamanho Máximo de Ficheiro (MB)", + "Maximum Nesting Depth": "Profundidade Máxima de Aninhamento", + "Maximum length": "Comprimento máximo", + "Maximum length constraint is missing": "Falta a restrição de comprimento máximo", + "Maximum length is too restrictive": "O comprimento máximo é demasiado restritivo", + "Maximum number of files to process. Set to 0 to process all files.": "Número máximo de ficheiros a processar. Defina como 0 para processar todos os ficheiros.", + "Maximum number of items": "Número máximo de itens", + "Maximum number of objects to process. Set to 0 to process all objects.": "Número máximo de objetos a processar. Defina como 0 para processar todos os objetos.", + "Maximum number of retry attempts for failed deliveries": "Número máximo de tentativas de repetição para entregas falhadas", + "Maximum time to wait before committing changes": "Tempo máximo de espera antes de confirmar alterações", + "Maximum tokens to generate": "Máximo de tokens a gerar", + "Maximum value": "Valor máximo", + "Maximum value constraint is missing": "Falta a restrição de valor máximo", + "Maximum value is too restrictive": "O valor máximo é demasiado restritivo", + "Medium": "Médio", + "Medium Confidence": "Confiança Média", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Consultas médias: pesquisas com alguma filtragem ou múltiplos parâmetros (por exemplo, intervalos de datas, registos/esquemas específicos)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Consultas médias: pesquisas com alguma filtragem ou múltiplos parâmetros (por exemplo, intervalos de datas, registos/esquemas específicos)", + "Member removed successfully": "Membro removido com sucesso", + "Members": "Membros", + "Members ({count})": "Membros ({count})", + "Members of selected groups can access this view": "Os membros dos grupos selecionados podem aceder a esta vista", + "Members:": "Membros:", + "Memory prediction calculated successfully": "Previsão de memória calculada com sucesso", + "Message content is required": "O conteúdo da mensagem é obrigatório", + "Message does not belong to this conversation": "A mensagem não pertence a esta conversa", + "Message not found": "Mensagem não encontrada", + "Metadata": "Metadados", + "Metadata Filters": "Filtros de Metadados", + "Method": "Método", + "Method*": "Método*", + "Method: {method}": "Método: {method}", + "Min Length": "Comprimento Mínimo", + "Min execution time (ms)": "Tempo mínimo de execução (ms)", + "Min ms": "Mínimo de ms", + "Min result count": "Contagem mínima de resultados", + "Min results": "Mínimo de resultados", + "Minimum": "Mínimo", + "Minimum length": "Comprimento mínimo", + "Minimum number of items": "Número mínimo de itens", + "Minimum value": "Valor mínimo", + "Minimum value constraint is missing": "Falta a restrição de valor mínimo", + "Minimum value is too restrictive": "O valor mínimo é demasiado restritivo", + "Missing conversation or agentUuid": "Falta conversa ou agentUuid", + "Missing conversationId": "Falta conversationId", + "Missing message": "Falta mensagem", + "Missing object fields created successfully": "Campos de objeto em falta criados com sucesso", + "Mode:": "Modo:", + "Model": "Modelo", + "Model Name": "Nome do Modelo", + "Modified": "Modificado", + "Monthly": "Mensalmente", + "More sources (10-20):": "Mais fontes (10-20):", + "Most Active Objects": "Objetos Mais Ativos", + "Multiple Of": "Múltiplo De", + "Multiple of": "Múltiplo de", + "Multiple search trail deletion not implemented yet": "A eliminação de múltiplos registos de pesquisa ainda não está implementada", + "Multitenancy": "Multilocação", + "Multitenancy settings updated successfully": "Definições de multilocação atualizadas com sucesso", + "My Account": "A Minha Conta", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Nome", + "Name *": "Nome *", + "Name*": "Nome*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Cache de nomes aquecida com sucesso: {count} nomes carregados em {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Cache de nomes aquecida com sucesso: {count} nomes carregados em {time}. {message}", + "Names cache warmup completed": "Aquecimento da cache de nomes concluído", + "Naming pattern for copies:": "Padrão de nomenclatura para cópias:", + "New Collection Name": "Nome da Nova Coleção", + "New Configuration": "Nova Configuração", + "New Conversation": "Nova Conversa", + "New Properties": "Novas Propriedades", + "New Property": "Nova Propriedade", + "New User Group": "Novo Grupo de Utilizadores", + "New Value": "Novo Valor", + "New activity": "Nova atividade", + "New password": "Nova palavra-passe", + "New processing activity": "Nova atividade de tratamento", + "New users without specific organisation membership will be automatically added to this organisation": "Os novos utilizadores sem associação específica a uma organização serão adicionados automaticamente a esta organização", + "New verwerkingsactiviteit": "Nova verwerkingsactiviteit", + "Next": "Seguinte", + "Nextcloud Groups": "Grupos do Nextcloud", + "No": "Não", + "No API tokens.": "Sem tokens da API.", + "No ConfigSets found": "Nenhum ConfigSet encontrado", + "No Deck cards linked to this object": "Nenhum cartão do Deck associado a este objeto", + "No GitHub token configured": "Nenhum token do GitHub configurado", + "No GitHub token provided": "Nenhum token do GitHub fornecido", + "No GitLab token provided": "Nenhum token do GitLab fornecido", + "No Organisation": "Sem Organização", + "No active filters": "Sem filtros ativos", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Não há objetos ativos para eliminar de forma reversível no esquema {schema}. Utilize \"Eliminar Permanentemente\" para remover objetos eliminados de forma reversível.", + "No activity data available": "Não há dados de atividade disponíveis", + "No activity found.": "Nenhuma atividade encontrada.", + "No agents available": "Nenhum agente disponível", + "No agents found": "Nenhum agente encontrado", + "No app store cache exists yet": "Ainda não existe cache da App Store", + "No applications are available.": "Não há aplicações disponíveis.", + "No applications found": "Nenhuma aplicação encontrada", + "No approval steps for this object.": "Sem etapas de aprovação para este objeto.", + "No archived conversations": "Sem conversas arquivadas", + "No audit trail entries found": "Nenhuma entrada de registo de auditoria encontrada", + "No changes recorded": "Nenhuma alteração registada", + "No chunks to vectorize": "Nenhum fragmento para vetorizar", + "No collections found": "Nenhuma coleção encontrada", + "No configuration data": "Sem dados de configuração", + "No configuration selected": "Nenhuma configuração selecionada", + "No configurations are available.": "Não há configurações disponíveis.", + "No configurations found": "Nenhuma configuração encontrada", + "No configurations found for this application.": "Nenhuma configuração encontrada para esta aplicação.", + "No contacts linked to this object": "Nenhum contacto associado a este objeto", + "No conversations yet. Create a new one to get started!": "Ainda não há conversas. Crie uma nova para começar!", + "No dashboards yet": "Ainda não há painéis", + "No data available": "Sem dados disponíveis", + "No data available for chart": "Sem dados disponíveis para o gráfico", + "No deleted items found": "Não foram encontrados itens eliminados", + "No deletion data available": "Sem dados de eliminação disponíveis", + "No description found": "Nenhuma descrição encontrada", + "No description provided": "Nenhuma descrição fornecida", + "No emails linked to this object": "Nenhum e-mail associado a este objeto", + "No endpoints defined": "Nenhum endpoint definido", + "No entities detected for this email.": "Não foram detetadas entidades para este e-mail.", + "No entities detected in this file. Run text extraction first.": "Não foram detetadas entidades neste ficheiro. Execute primeiro a extração de texto.", + "No entities found": "Não foram encontradas entidades", + "No entities have been detected yet": "Ainda não foram detetadas entidades", + "No events found": "Não foram encontrados eventos", + "No events linked to this object": "Nenhum evento associado a este objeto", + "No expired audit trails found to clear": "Não foram encontrados registos de auditoria expirados para limpar", + "No expired search trails found to clear": "Não foram encontrados registos de pesquisa expirados para limpar", + "No extraction data available for this file": "Sem dados de extração disponíveis para este ficheiro", + "No facetable fields available. Select a register and schema to see available filters.": "Não há campos facetáveis disponíveis. Selecione um registo e um esquema para ver os filtros disponíveis.", + "No files found": "Não foram encontrados ficheiros", + "No files have been extracted yet": "Ainda não foram extraídos ficheiros", + "No files selected": "Nenhum ficheiro selecionado", + "No files to process": "Não há ficheiros para processar", + "No files to reindex": "Não há ficheiros para reindexar", + "No filters are currently active. This will delete ALL audit trail entries!": "Não há filtros ativos de momento. Isto irá eliminar TODAS as entradas do registo de auditoria!", + "No groups found. Try a different search.": "Não foram encontrados grupos. Experimente uma pesquisa diferente.", + "No label": "Sem etiqueta", + "No labels": "Sem etiquetas", + "No linked objects": "Nenhum objeto associado", + "No log entries found": "Não foram encontradas entradas de registo", + "No logs are available for this configuration.": "Não há registos disponíveis para esta configuração.", + "No logs are available for this source.": "Não há registos disponíveis para esta fonte.", + "No logs found": "Não foram encontrados registos", + "No matches": "Sem correspondências", + "No mismatched fields found - SOLR schema is properly configured": "Não foram encontrados campos incompatíveis - o esquema SOLR está corretamente configurado", + "No objects are currently selected for deletion.": "Não há objetos selecionados para eliminação de momento.", + "No objects are currently selected for permanent deletion.": "Não há objetos selecionados para eliminação permanente de momento.", + "No objects are currently selected for restoration.": "Não há objetos selecionados para restauro de momento.", + "No objects found": "Não foram encontrados objetos", + "No objects found to vectorize": "Não foram encontrados objetos para vetorizar", + "No objects linked to this email": "Nenhum objeto associado a este e-mail", + "No objects selected": "Nenhum objeto selecionado", + "No objects selected for deletion": "Nenhum objeto selecionado para eliminação", + "No objects selected for restoration": "Nenhum objeto selecionado para restauro", + "No objects to delete": "Não há objetos para eliminar", + "No objects to delete for schema {schema}": "Não há objetos para eliminar para o esquema {schema}", + "No objects were permanently deleted": "Não foram eliminados objetos permanentemente", + "No organisations found": "Não foram encontradas organizações", + "No personal data was found for this subject identifier.": "Não foram encontrados dados pessoais para este identificador de titular.", + "No processing activities yet": "Ainda não há atividades de tratamento", + "No properties found": "Não foram encontradas propriedades", + "No properties match your filters.": "Nenhuma propriedade corresponde aos seus filtros.", + "No purge date set": "Nenhuma data de expurgo definida", + "No register data available": "Sem dados de registo disponíveis", + "No register objects reference this file": "Nenhum objeto de registo referencia este ficheiro", + "No registers found": "Não foram encontrados registos", + "No registers found for this application.": "Não foram encontrados registos para esta aplicação.", + "No registers found. Create a register to configure permissions.": "Não foram encontrados registos. Crie um registo para configurar as permissões.", + "No related cases found for this sender": "Não foram encontrados processos relacionados para este remetente", + "No related entities": "Sem entidades relacionadas", + "No relations found": "Não foram encontradas relações", + "No request body available": "Sem corpo de pedido disponível", + "No response body available": "Sem corpo de resposta disponível", + "No saved views yet. Create one in the Search tab!": "Ainda não há vistas guardadas. Crie uma no separador Pesquisa!", + "No saved views yet. create one in the search tab!": "Ainda não há vistas guardadas. crie uma no separador de pesquisa!", + "No scan run yet": "Ainda não foi executada nenhuma análise", + "No schema data available": "Sem dados de esquema disponíveis", + "No schema selected for exploration": "Nenhum esquema selecionado para exploração", + "No schemas configured for mail linking.": "Não há esquemas configurados para associação de correio.", + "No schemas found": "Não foram encontrados esquemas", + "No schemas found for this application.": "Não foram encontrados esquemas para esta aplicação.", + "No search terms data available": "Sem dados de termos de pesquisa disponíveis", + "No search trail entries found": "Não foram encontradas entradas de registo de pesquisa", + "No sources are available.": "Não há fontes disponíveis.", + "No sources found": "Não foram encontradas fontes", + "No synchronizations found": "Não foram encontradas sincronizações", + "No templates found": "Não foram encontrados modelos", + "No templates have been created yet": "Ainda não foram criados modelos", + "No tools available. Tools can be registered by installed apps.": "Não há ferramentas disponíveis. As ferramentas podem ser registadas por aplicações instaladas.", + "No users in this organisation.": "Não há utilizadores nesta organização.", + "No valid magic-mapped register+schema combinations found": "Não foram encontradas combinações válidas de registo+esquema com mapeamento mágico", + "No views found. Create views first before configuring vectorization.": "Não foram encontradas vistas. Crie primeiro vistas antes de configurar a vetorização.", + "No views match your search": "Nenhuma vista corresponde à sua pesquisa", + "No webhooks found": "Não foram encontrados webhooks", + "No webhooks have been configured yet": "Ainda não foram configurados webhooks", + "No workflows found in this project. Create workflows in the n8n editor.": "Não foram encontrados fluxos de trabalho neste projeto. Crie fluxos de trabalho no editor n8n.", + "None": "Nenhum", + "Not Found": "Não Encontrado", + "Not extracted": "Não extraído", + "Not helpful": "Não útil", + "Not in use": "Não está em utilização", + "Note:": "Nota:", + "Notifications": "Notificações", + "Number Constraints": "Restrições Numéricas", + "Number of Sources": "Número de Fontes", + "Number of chunks to vectorize in one API call (1-100).": "Número de blocos a vetorizar numa chamada à API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Número de blocos a vetorizar numa chamada à API. Mais alto = mais rápido mas mais memória. Recomendado: 10-50.", + "Number of objects to process in each batch (1-5000).": "Número de objetos a processar em cada lote (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Número de objetos a vetorizar numa chamada à API. Mais alto = mais rápido mas mais memória. Recomendado: 10-50.", + "Numeric Range:": "Intervalo Numérico:", + "Object": "Objeto", + "Object #{id}": "Objeto #{id}", + "Object Collection": "Coleção de Objetos", + "Object Configuration:": "Configuração de Objeto:", + "Object Count Prediction": "Previsão de Contagem de Objetos", + "Object Extraction Mode": "Modo de Extração de Objetos", + "Object ID": "ID de Objeto", + "Object Management": "Gestão de Objetos", + "Object Text Extraction": "Extração de Texto de Objetos", + "Object Vectorization": "Vetorização de Objetos", + "Object Warmup": "Aquecimento de Objetos", + "Object changes in owned objects": "Alterações de objetos em objetos próprios", + "Object collection not configured": "Coleção de objetos não configurada", + "Object metadata incomplete for file upload": "Metadados do objeto incompletos para carregamento de ficheiro", + "Object not found": "Objeto não encontrado", + "Object permanently deleted": "Objeto eliminado permanentemente", + "Object restored successfully": "Objeto restaurado com sucesso", + "Object settings updated successfully": "Definições do objeto atualizadas com sucesso", + "Object sources": "Fontes do objeto", + "Object successfully deleted": "Objeto eliminado com sucesso", + "Object successfully modified": "Objeto modificado com sucesso", + "Object unlocked successfully": "Objeto desbloqueado com sucesso", + "Object vectorization configuration saved successfully": "Configuração de vetorização de objetos guardada com sucesso", + "Object vectorized successfully": "Objeto vetorizado com sucesso", + "Object:": "Objeto:", + "Objects": "Objetos", + "Objects Analyzed": "Objetos Analisados", + "Objects Distribution": "Distribuição de Objetos", + "Objects being analyzed": "Objetos a serem analisados", + "Objects by Register": "Objetos por Registo", + "Objects by Schema": "Objetos por Esquema", + "Objects deletion completed for {schema}": "Eliminação de objetos concluída para {schema}", + "Objects deletion completed successfully": "Eliminação de objetos concluída com sucesso", + "Objects in schema": "Objetos no esquema", + "Objects successfully deleted": "Objetos eliminados com sucesso", + "Objects to Process:": "Objetos a Processar:", + "Objects to be analyzed": "Objetos a serem analisados", + "Objects to be deleted": "Objetos a serem eliminados", + "Objects to be validated": "Objetos a serem validados", + "Objects will be serialized as JSON text before vectorization": "Os objetos serão serializados como texto JSON antes da vetorização", + "Objects will be soft deleted and moved to the": "Os objetos serão eliminados de forma reversível e movidos para o", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Os objetos serão eliminados de forma reversível (marcados como eliminados mas mantidos na base de dados). Podem ser recuperados mais tarde, se necessário.", + "Old Value": "Valor Antigo", + "Oldest Item (days)": "Item Mais Antigo (dias)", + "Ollama Chat Settings": "Definições de Chat do Ollama", + "Ollama Embedding Configuration": "Configuração de Embedding do Ollama", + "Ollama URL": "URL do Ollama", + "Only With Changes": "Apenas Com Alterações", + "Only members of selected groups can access this organisation": "Apenas os membros dos grupos selecionados podem aceder a esta organização", + "Open Nextcloud App Store": "Abrir a App Store do Nextcloud", + "Open OpenRegister sidebar": "Abrir a barra lateral do OpenRegister", + "Open Register Objects": "Abrir Objetos do Registo", + "Open folder": "Abrir pasta", + "Open in OpenRegister": "Abrir no OpenRegister", + "Open n8n Editor": "Abrir o Editor n8n", + "OpenAI Chat Settings": "Definições de Chat do OpenAI", + "OpenAI Embedding Configuration": "Configuração de Embedding do OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Definições do OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Painéis definidos pelo operador e relatórios agendados. Cada painel é um objeto de primeira classe no registo `reports`; os widgets são declarados no array `widgets` do painel e renderizados em tempo real a partir de agregações / GraphQL.", + "Optimizing search performance...": "A otimizar o desempenho da pesquisa...", + "Optional URL-friendly identifier": "Identificador opcional compatível com URL", + "Optional description for this facet": "Descrição opcional para esta faceta", + "Optional end date property": "Propriedade opcional de data de fim", + "Optional event description template": "Modelo opcional de descrição de evento", + "Optional location property": "Propriedade opcional de localização", + "Optional webhook secret for signature verification": "Segredo opcional do webhook para verificação de assinatura", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Opcional: Adicione parâmetros de consulta para filtrar o esquema referenciado (por exemplo, status=active&type=public)", + "Or": "Ou", + "Order": "Ordem", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Etiquetas de idioma BCP 47 ordenadas. O primeiro idioma é o predefinido do registo e determina o recurso de reserva Accept-Language para propriedades traduzíveis.", + "Organisation": "Organização", + "Organisation Configuration": "Configuração da Organização", + "Organisation ID": "ID da Organização", + "Organisation Members": "Membros da Organização", + "Organisation Statistics": "Estatísticas da Organização", + "Organisation created successfully": "Organização criada com sucesso", + "Organisation membership changes": "Alterações de adesão à organização", + "Organisation name is required": "O nome da organização é obrigatório", + "Organisation settings saved successfully": "Definições da organização guardadas com sucesso", + "Organisation successfully created": "Organização criada com sucesso", + "Organisation successfully updated": "Organização atualizada com sucesso", + "Organisational measures": "Medidas organizacionais", + "Organisations": "Organizações", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organização", + "Organization ID (Optional)": "ID da Organização (Opcional)", + "Organizations": "Organizações", + "Orphaned Items": "Itens Órfãos", + "Other": "Outro", + "Output Mapping": "Mapeamento de Saída", + "Overview": "Visão Geral", + "Owner": "Proprietário", + "Owner:": "Proprietário:", + "PDF": "PDF", + "PII hits": "ocorrências de PII", + "PII rows": "linhas de PII", + "Page {current} of {total}": "Página {current} de {total}", + "Parallel Mode (Faster, more resource intensive)": "Modo Paralelo (Mais rápido, mais intensivo em recursos)", + "Parallel:": "Paralelo:", + "Password": "Palavra-passe", + "Password changes are not supported by your authentication provider.": "As alterações de palavra-passe não são suportadas pelo seu fornecedor de autenticação.", + "Password for SOLR authentication (optional)": "Palavra-passe para autenticação SOLR (opcional)", + "Password updated successfully": "Palavra-passe atualizada com sucesso", + "Path": "Caminho", + "Path in repository": "Caminho no repositório", + "Path where the configuration file will be saved in the repository": "Caminho onde o ficheiro de configuração será guardado no repositório", + "Path where the register OAS file will be saved in the repository": "Caminho onde o ficheiro OAS do registo será guardado no repositório", + "Pattern (regex)": "Padrão (regex)", + "Pattern Issue": "Problema de Padrão", + "Pattern constraint is missing": "Falta a restrição de padrão", + "Patterns:": "Padrões:", + "Pending": "Pendente", + "Permanent Delete Mode": "Modo de Eliminação Permanente", + "Permanently Delete": "Eliminar Permanentemente", + "Permanently Delete ({count})": "Eliminar Permanentemente ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Eliminar permanentemente todos os {count} objetos eliminados de forma reversível. Esta ação não pode ser anulada!", + "Permanently delete already soft-deleted objects": "Eliminar permanentemente objetos já eliminados de forma reversível", + "Permanently delete this conversation?": "Eliminar permanentemente esta conversa?", + "Permission Matrix": "Matriz de Permissões", + "Person": "Pessoa", + "Personal Data": "Dados Pessoais", + "Personal Data Export": "Exportação de Dados Pessoais", + "Persons": "Pessoas", + "Phone": "Telefone", + "Phone Numbers": "Números de Telefone", + "Photos": "Fotografias", + "Please create an agent in the": "Crie um agente no", + "Please select a user": "Selecione um utilizador", + "Please select an agent to continue": "Selecione um agente para continuar", + "Please select an organisation": "Selecione uma organização", + "Please select at least one item to import": "Selecione pelo menos um item para importar", + "Please select registers or schemas first": "Selecione primeiro registos ou esquemas", + "Please select which register and schema to use for the new object": "Selecione que registo e esquema utilizar para o novo objeto", + "Please try again later.": "Tente novamente mais tarde.", + "Please wait while we fetch your agents.": "Aguarde enquanto obtemos os seus agentes.", + "Please wait while we fetch your applications.": "Aguarde enquanto obtemos as suas aplicações.", + "Please wait while we fetch your configurations.": "Aguarde enquanto obtemos as suas configurações.", + "Please wait while we fetch your deleted items.": "Aguarde enquanto obtemos os seus itens eliminados.", + "Please wait while we fetch your sources.": "Aguarde enquanto obtemos as suas fontes.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Aguarde enquanto eliminamos permanentemente a coleção SOLR. Isto pode demorar alguns momentos.", + "Polls": "Sondagens", + "Popular Search Terms": "Termos de Pesquisa Populares", + "Port": "Porta", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Preferências guardadas", + "Prerequisites": "Pré-requisitos", + "Press Enter to send, Shift+Enter for new line": "Prima Enter para enviar, Shift+Enter para nova linha", + "Press enter to send, shift+enter for new line": "Prima enter para enviar, shift+enter para nova linha", + "Prettify": "Embelezar", + "Preview erasure": "Pré-visualizar apagamento", + "Preview:": "Pré-visualização:", + "Previous": "Anterior", + "Private": "Privado", + "Private Agent (Default)": "Agente Privado (Predefinição)", + "Private agents": "Agentes privados", + "Process Name (optional)": "Nome do Processo (opcional)", + "Processed {indexed} files, {failed} failed": "Processados {indexed} ficheiros, {failed} falharam", + "Processes chunks in batches with simulated parallelism.": "Processa blocos em lotes com paralelismo simulado.", + "Processes file chunks sequentially (safest).": "Processa blocos de ficheiros sequencialmente (mais seguro).", + "Processes objects in chunks with simulated parallelism.": "Processa objetos em blocos com paralelismo simulado.", + "Processes objects sequentially (safest).": "Processa objetos sequencialmente (mais seguro).", + "Processing": "A processar", + "Processing Limits": "Limites de Processamento", + "Processing...": "A processar...", + "Project initialized successfully": "Projeto inicializado com sucesso", + "Project not found. Please initialize first.": "Projeto não encontrado. Inicialize primeiro.", + "Properties": "Propriedades", + "Property Behaviors": "Comportamentos da Propriedade", + "Property Configuration:": "Configuração da Propriedade:", + "Property Filters": "Filtros de Propriedade", + "Property Title": "Título da Propriedade", + "Property Type": "Tipo de Propriedade", + "Property can be improved": "A propriedade pode ser melhorada", + "Property could not be added": "Não foi possível adicionar a propriedade", + "Property could not be updated": "Não foi possível atualizar a propriedade", + "Property name of inversed relation": "Nome da propriedade da relação inversa", + "Property successfully added": "Propriedade adicionada com sucesso", + "Property successfully updated": "Propriedade atualizada com sucesso", + "Protected": "Protegido", + "Provider": "Fornecedor", + "Provider is required for testing": "O fornecedor é obrigatório para o teste", + "Public": "Público", + "Public View": "Vista Pública", + "Public views can be accessed by anyone in the system": "As vistas públicas podem ser acedidas por qualquer pessoa no sistema", + "Publish": "Publicar", + "Publish Configuration to GitHub": "Publicar Configuração no GitHub", + "Publish OAS": "Publicar OAS", + "Publish Objects": "Publicar Objetos", + "Publish Register OAS to GitHub": "Publicar OAS do Registo no GitHub", + "Publish all objects in this schema": "Publicar todos os objetos neste esquema", + "Publish objects to make them publicly available": "Publicar objetos para os tornar disponíveis publicamente", + "Published": "Publicado", + "Published:": "Publicado:", + "Publishing...": "A publicar...", + "Purge": "Expurgar", + "Purge Date": "Data de Expurgo", + "Purge {count} object from database": "Expurgar {count} objeto da base de dados", + "Purge {count} objects from database": "Expurgar {count} objetos da base de dados", + "Purpose limitation *": "Limitação da finalidade *", + "Push Notifications": "Notificações Push", + "Query Complexity Distribution": "Distribuição de Complexidade de Consultas", + "Query Help": "Ajuda de Consulta", + "Query parameter is required": "O parâmetro de consulta é obrigatório", + "Quota": "Quota", + "RAG": "RAG", + "RAG Configuration": "Configuração RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Definições RBAC atualizadas com sucesso", + "Rate limits cleared successfully": "Limites de taxa limpos com sucesso", + "Raw Changes Data": "Dados de Alterações em Bruto", + "Re-vectorize on object update": "Voltar a vetorizar na atualização do objeto", + "Read": "Ler", + "Real-time push notification status via notify_push": "Estado das notificações push em tempo real via notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "As notificações push em tempo real estão ativas. Os clientes ligados recebem atualizações instantâneas quando os objetos são criados, atualizados ou eliminados.", + "Realtime push active": "Push em tempo real ativo", + "Realtime push not available — the notify_push app is not installed": "Push em tempo real não disponível — a aplicação notify_push não está instalada", + "Reason": "Motivo", + "Reason (optional)": "Motivo (opcional)", + "Rebase operation completed successfully": "Operação de rebase concluída com sucesso", + "Rebase operation failed: {error}": "A operação de rebase falhou: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Recomendações:", + "Recommended Type:": "Tipo Recomendado:", + "Recommended: 5 sources": "Recomendado: 5 fontes", + "Refresh": "Atualizar", + "Refresh Data": "Atualizar Dados", + "Refresh Stats": "Atualizar Estatísticas", + "Refresh Workflows": "Atualizar Fluxos de Trabalho", + "Refresh dashboard": "Atualizar painel", + "Refresh database info": "Atualizar informações da base de dados", + "Register": "Registo", + "Register #{id}": "Registo #{id}", + "Register / Schema": "Registo / Esquema", + "Register ID": "ID do Registo", + "Register Objects": "Objetos do Registo", + "Register Overview": "Visão Geral do Registo", + "Register Statistics": "Estatísticas do Registo", + "Register Totals": "Totais do Registo", + "Register and schema are required": "O registo e o esquema são obrigatórios", + "Register annotation": "Anotação do registo", + "Register depublished successfully": "Registo despublicado com sucesso", + "Register not found": "Registo não encontrado", + "Register objects": "Objetos do registo", + "Register objects deletion completed successfully": "Eliminação dos objetos do registo concluída com sucesso", + "Register published successfully": "Registo publicado com sucesso", + "Register/Schema Usage": "Utilização de Registo/Esquema", + "Register:": "Registo:", + "Registers": "Registos", + "Reindex": "Reindexar", + "Reindex completed": "Reindexação concluída", + "Reindex completed successfully": "Reindexação concluída com sucesso", + "Reindex completed! Processed {count} objects in {duration}s": "Reindexação concluída! Processados {count} objetos em {duration}s", + "Reindex failed": "A reindexação falhou", + "Reindex failed: %s": "A reindexação falhou: %s", + "Reject": "Rejeitar", + "Rejected": "Rejeitado", + "Related Cases": "Processos Relacionados", + "Relations": "Relações", + "Remote Version": "Versão Remota", + "Remove": "Remover", + "Remove Deck card": "Remover cartão do Deck", + "Remove avatar": "Remover avatar", + "Remove contact": "Remover contacto", + "Remove filter": "Remover filtro", + "Remove from favorites": "Remover dos favoritos", + "Remove from list": "Remover da lista", + "Remove group": "Remover grupo", + "Remove link": "Remover ligação", + "Remove link to {name}": "Remover ligação a {name}", + "Remove link to {name}?": "Remover ligação a {name}?", + "Remove link to {title}": "Remover ligação a {title}", + "Remove oneOf entry": "Remover entrada oneOf", + "Remove property after write-back": "Remover propriedade após escrita de retorno", + "Remove {title}": "Remover {title}", + "Remove {userId} from this organisation?": "Remover {userId} desta organização?", + "Removed": "Removido", + "Removed from favorites": "Removido dos favoritos", + "Removing schema {schema}...": "A remover o esquema {schema}...", + "Rename Conversation": "Mudar o Nome da Conversa", + "Rename conversation": "Mudar o nome da conversa", + "Replicas": "Réplicas", + "Reports": "Relatórios", + "Repository": "Repositório", + "Request Body": "Corpo do Pedido", + "Request Data": "Dados do Pedido", + "Request Details": "Detalhes do Pedido", + "Request Quota (per day)": "Quota de Pedidos (por dia)", + "Request account deactivation": "Solicitar desativação da conta", + "Request account deactivation. This will notify administrators for review.": "Solicitar a desativação da conta. Isto notificará os administradores para revisão.", + "Request body must be JSON with a \"query\" field": "O corpo do pedido tem de ser JSON com um campo \"query\"", + "Request timeout in seconds": "Tempo limite do pedido em segundos", + "Requested at": "Solicitado em", + "Required": "Obrigatório", + "Required Token Scopes": "Âmbitos de Token Obrigatórios", + "Required field": "Campo obrigatório", + "Required status is inconsistent": "O estado obrigatório é inconsistente", + "Requires Dolphin AI for OCR text extraction": "Requer o Dolphin AI para extração de texto por OCR", + "Rerun Search": "Voltar a Executar Pesquisa", + "Rerunning search: {searchTerm}": "A voltar a executar a pesquisa: {searchTerm}", + "Reset Changes": "Repor Alterações", + "Reset Filters": "Repor Filtros", + "Resource Quotas": "Quotas de Recursos", + "Response Body": "Corpo da Resposta", + "Response Details": "Detalhes da Resposta", + "Response Time:": "Tempo de Resposta:", + "Restore": "Restaurar", + "Restore conversation": "Restaurar conversa", + "Restore or permanently delete items": "Restaurar ou eliminar permanentemente itens", + "Restore {count} object": "Restaurar {count} objeto", + "Restore {count} objects": "Restaurar {count} objetos", + "Result Count Range": "Intervalo de Contagem de Resultados", + "Results": "Resultados", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Período de retenção (duração ISO-8601, por exemplo, P10Y, P30D)", + "Retention settings updated successfully": "Definições de retenção atualizadas com sucesso", + "Retrieve all objects for this schema": "Obter todos os objetos para este esquema", + "Retry": "Tentar novamente", + "Retry Failed Extractions": "Tentar Novamente Extrações Falhadas", + "Retry Policy": "Política de Tentativas", + "Retry completed": "Nova tentativa concluída", + "Retry upload": "Tentar novamente o carregamento", + "Retrying...": "A tentar novamente...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Reveja os objetos selecionados abaixo. Pode remover quaisquer objetos que não pretenda eliminar clicando no botão remover.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Reveja os objetos selecionados abaixo. Pode remover quaisquer objetos que não pretenda incluir.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Reveja os objetos selecionados abaixo. Pode remover quaisquer objetos que não pretenda eliminar permanentemente clicando no botão remover. Esta ação não pode ser anulada.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Reveja os objetos selecionados abaixo. Pode remover quaisquer objetos que não pretenda restaurar clicando no botão remover. Os objetos serão restaurados para a sua localização original.", + "Revoke": "Revogar", + "Right": "Direita", + "Risk Level": "Nível de Risco", + "Risk level": "Nível de risco", + "Roles and Permissions": "Funções e Permissões", + "Roles updated successfully": "Funções atualizadas com sucesso", + "Run compliance scan": "Executar análise de conformidade", + "SOLR Connection Settings": "Definições de Ligação SOLR", + "SOLR Index Warmup": "Aquecimento do Índice SOLR", + "SOLR Version:": "Versão SOLR:", + "SOLR actions menu": "Menu de ações SOLR", + "SOLR base path (usually /solr)": "Caminho base SOLR (normalmente /solr)", + "SOLR collection deleted successfully": "Coleção SOLR eliminada com sucesso", + "SOLR connection test failed: {error}": "O teste de ligação SOLR falhou: {error}", + "SOLR connection test successful!": "Teste de ligação SOLR bem-sucedido!", + "SOLR index warmup completed successfully": "Aquecimento do índice SOLR concluído com sucesso", + "SOLR is disabled": "O SOLR está desativado", + "SOLR is not available or not configured": "O SOLR não está disponível ou não está configurado", + "SOLR logging disabled": "Registo SOLR desativado", + "SOLR logging enabled": "Registo SOLR ativado", + "SOLR search disabled": "Pesquisa SOLR desativada", + "SOLR search enabled": "Pesquisa SOLR ativada", + "SOLR server hostname or IP address": "Nome de anfitrião ou endereço IP do servidor SOLR", + "SOLR server port number (optional, defaults to 8983)": "Número de porta do servidor SOLR (opcional, predefinição 8983)", + "SOLR settings updated successfully": "Definições SOLR atualizadas com sucesso", + "SOLR setup completed successfully": "Configuração SOLR concluída com sucesso", + "SOLR setup completed successfully!": "Configuração SOLR concluída com sucesso!", + "SOLR setup error: %s": "Erro de configuração SOLR: %s", + "SOLR setup failed": "A configuração SOLR falhou", + "SOLR setup failed - check logs": "A configuração SOLR falhou - verifique os registos", + "SOLR setup failed. Please check the configuration and try again.": "A configuração SOLR falhou. Verifique a configuração e tente novamente.", + "SOLR setup failed: {error}": "A configuração SOLR falhou: {error}", + "SOLR warmup failed: {error}": "O aquecimento SOLR falhou: {error}", + "Sample Values:": "Valores de Amostra:", + "Save": "Guardar", + "Save Configuration": "Guardar Configuração", + "Save Connection Settings": "Guardar Definições de Ligação", + "Save Roles": "Guardar Funções", + "Save Settings": "Guardar Definições", + "Save changes": "Guardar alterações", + "Save current search as view": "Guardar a pesquisa atual como vista", + "Save labels": "Guardar etiquetas", + "Save tags for {name}": "Guardar etiquetas para {name}", + "Save the organisation first to manage users.": "Guarde primeiro a organização para gerir utilizadores.", + "Saved Views": "Vistas Guardadas", + "Saving...": "A guardar...", + "Scheduled": "Agendado", + "Schema": "Esquema", + "Schema ID": "ID do Esquema", + "Schema Name": "Nome do Esquema", + "Schema Statistics": "Estatísticas do Esquema", + "Schema annotation": "Anotação do esquema", + "Schema configuration updated successfully for {schema}": "Configuração do esquema atualizada com sucesso para {schema}", + "Schema depublished successfully": "Esquema despublicado com sucesso", + "Schema objects deletion completed successfully": "Eliminação dos objetos do esquema concluída com sucesso", + "Schema published successfully": "Esquema publicado com sucesso", + "Schema reference ($ref)": "Referência do esquema ($ref)", + "Schema successfully created": "Esquema criado com sucesso", + "Schema successfully updated": "Esquema atualizado com sucesso", + "Schema {schema} removed successfully": "Esquema {schema} removido com sucesso", + "Schema:": "Esquema:", + "Schemas": "Esquemas", + "Scheme": "Esquema", + "Search": "Pesquisar", + "Search / Views": "Pesquisa / Vistas", + "Search / views": "Pesquisa / vistas", + "Search Activity": "Atividade de Pesquisa", + "Search Analytics": "Análise de Pesquisa", + "Search Configuration": "Configuração de Pesquisa", + "Search Entities": "Pesquisar Entidades", + "Search Files": "Pesquisar Ficheiros", + "Search GitHub": "Pesquisar no GitHub", + "Search GitLab": "Pesquisar no GitLab", + "Search Method": "Método de Pesquisa", + "Search Mode": "Modo de Pesquisa", + "Search Query": "Consulta de Pesquisa", + "Search Results": "Resultados da Pesquisa", + "Search Term": "Termo de Pesquisa", + "Search Trail Details": "Detalhes do Registo de Pesquisa", + "Search Trail Management": "Gestão de Registos de Pesquisa", + "Search Trail Statistics": "Estatísticas de Registos de Pesquisa", + "Search Trails": "Registos de Pesquisa", + "Search Views": "Pesquisar Vistas", + "Search Webhooks": "Pesquisar Webhooks", + "Search agents...": "Pesquisar agentes...", + "Search applications...": "Pesquisar aplicações...", + "Search by file name or path": "Pesquisar por nome de ficheiro ou caminho", + "Search by name or URL": "Pesquisar por nome ou URL", + "Search by title or UUID...": "Pesquisar por título ou UUID...", + "Search by value": "Pesquisar por valor", + "Search configurations": "Pesquisar configurações", + "Search data sources...": "Pesquisar fontes de dados...", + "Search groups...": "Pesquisar grupos...", + "Search in Database Objects": "Pesquisar em Objetos da Base de Dados", + "Search in Files": "Pesquisar em Ficheiros", + "Search in Objects": "Pesquisar em Objetos", + "Search objects": "Pesquisar objetos", + "Search objects...": "Pesquisar objetos...", + "Search property names...": "Pesquisar nomes de propriedades...", + "Search registers...": "Pesquisar registos...", + "Search schemas...": "Pesquisar esquemas...", + "Search trail deletion not implemented yet": "Eliminação de registo de pesquisa ainda não implementada", + "Search views...": "Pesquisar vistas...", + "Search {name}...": "Pesquisar {name}...", + "Searching...": "A pesquisar...", + "Secret": "Segredo", + "Secret key for HMAC signature generation (optional)": "Chave secreta para geração de assinatura HMAC (opcional)", + "Security": "Segurança", + "See organisations as a table": "Ver organizações como uma tabela", + "See organisations as cards": "Ver organizações como cartões", + "Select AI Agent": "Selecionar Agente de IA", + "Select Active Organisation": "Selecionar Organização Ativa", + "Select All": "Selecionar Tudo", + "Select ConfigSet": "Selecionar ConfigSet", + "Select File Types to Vectorize:": "Selecionar Tipos de Ficheiro a Vetorizar:", + "Select HTTP method": "Selecionar método HTTP", + "Select Register and Schema": "Selecionar Registo e Esquema", + "Select Views to Vectorize:": "Selecionar Vistas a Vetorizar:", + "Select a Nextcloud group to add": "Selecione um grupo do Nextcloud para adicionar", + "Select a branch": "Selecione um ramo", + "Select a date property": "Selecione uma propriedade de data", + "Select a model or type a custom model name": "Selecione um modelo ou escreva um nome de modelo personalizado", + "Select a register": "Selecione um registo", + "Select a register...": "Selecione um registo...", + "Select a repository": "Selecione um repositório", + "Select a repository you have write access to": "Selecione um repositório ao qual tenha acesso de escrita", + "Select a schema": "Selecione um esquema", + "Select a schema...": "Selecione um esquema...", + "Select a user": "Selecione um utilizador", + "Select agent type": "Selecionar tipo de agente", + "Select all entities": "Selecionar todas as entidades", + "Select an AI Agent": "Selecionar um Agente de IA", + "Select an AI agent to begin chatting with your data.": "Selecione um agente de IA para começar a conversar com os seus dados.", + "Select an email to see linked objects": "Selecione um e-mail para ver os objetos associados", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Selecione uma organização e um utilizador para os adicionar como membro. Pesquise organizações por nome.", + "Select backend": "Selecionar backend", + "Select chat model": "Selecionar modelo de chat", + "Select collection for files": "Selecionar coleção para ficheiros", + "Select collection for objects": "Selecionar coleção para objetos", + "Select configuration type...": "Selecionar tipo de configuração...", + "Select default organisation": "Selecionar organização predefinida", + "Select entity {value}": "Selecionar entidade {value}", + "Select event to listen to...": "Selecionar evento a escutar...", + "Select fields to display": "Selecionar campos a apresentar", + "Select files with the correct extension": "Selecionar ficheiros com a extensão correta", + "Select group": "Selecionar grupo", + "Select groups (optional)": "Selecionar grupos (opcional)", + "Select groups to notify...": "Selecionar grupos a notificar...", + "Select groups with access to this agent": "Selecionar grupos com acesso a este agente", + "Select groups...": "Selecionar grupos...", + "Select model": "Selecionar modelo", + "Select one or more registers": "Selecione um ou mais registos", + "Select one or more schemas": "Selecione um ou mais esquemas", + "Select options...": "Selecionar opções...", + "Select or create labels, or select \"No label\" to add files": "Selecione ou crie etiquetas, ou selecione \"Sem etiqueta\" para adicionar ficheiros", + "Select owner application (optional)...": "Selecionar aplicação proprietária (opcional)...", + "Select period": "Selecionar período", + "Select property to send as payload": "Selecionar propriedade a enviar como payload", + "Select provider": "Selecionar fornecedor", + "Select registers and schemas to save a view": "Selecione registos e esquemas para guardar uma vista", + "Select retry policy": "Selecionar política de tentativas", + "Select role": "Selecionar função", + "Select schemas (empty = all schemas)": "Selecionar esquemas (vazio = todos os esquemas)", + "Select schemas to warm up": "Selecionar esquemas a aquecer", + "Select search mode": "Selecionar modo de pesquisa", + "Select source type...": "Selecionar tipo de fonte...", + "Select target type": "Selecionar tipo de destino", + "Select the branch to publish to": "Selecione o ramo para onde publicar", + "Select the event this webhook should listen to": "Selecione o evento que este webhook deve escutar", + "Select views to filter data (optional)": "Selecionar vistas para filtrar dados (opcional)", + "Select views to limit which data the agent can access": "Selecionar vistas para limitar os dados a que o agente pode aceder", + "Select views to vectorize:": "Selecionar vistas a vetorizar:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Selecione quais os grupos do Nextcloud disponíveis para esta organização. Os utilizadores destes grupos terão acesso aos recursos da organização.", + "Select which data views the AI can search": "Selecione quais as vistas de dados que a IA pode pesquisar", + "Select which property from the event should be used as the webhook payload data": "Selecione qual a propriedade do evento que deve ser utilizada como dados de payload do webhook", + "Select which tools the AI can use to perform actions": "Selecione quais as ferramentas que a IA pode utilizar para executar ações", + "Select {fieldName} values": "Selecionar valores de {fieldName}", + "Selected": "Selecionado", + "Selected Groups": "Grupos Selecionados", + "Selected Objects ({count})": "Objetos Selecionados ({count})", + "Selected audit trails deleted successfully": "Registos de auditoria selecionados eliminados com sucesso", + "Selected search trails deleted successfully": "Registos de pesquisa selecionados eliminados com sucesso", + "Selected users can access this view": "Os utilizadores selecionados podem aceder a esta vista", + "Send additional feedback": "Enviar comentários adicionais", + "Send as CloudEvent": "Enviar como CloudEvent", + "Send as cloudevent": "Enviar como CloudEvent", + "Sensitive PII": "PII sensível", + "Serial Mode (Safer, slower)": "Modo em série (mais seguro, mais lento)", + "Serial:": "Em série:", + "Server Configuration": "Configuração do servidor", + "Server Information": "Informação do servidor", + "Set as Active": "Definir como ativo", + "Set as active organisation": "Definir como organização ativa", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Defina limites para a utilização da API e o consumo de tokens. Utilize 0 para recursos ilimitados.", + "Settings": "Definições", + "Settings updated successfully": "Definições atualizadas com sucesso", + "Shards": "Shards", + "Share": "Partilhar", + "Share with Groups": "Partilhar com grupos", + "Share with Users": "Partilhar com utilizadores", + "Show": "Mostrar", + "Show Filters": "Mostrar filtros", + "Show less": "Mostrar menos", + "Show only entries with changes": "Mostrar apenas entradas com alterações", + "Showing {count} dashboard(s)": "A mostrar {count} painel(éis)", + "Showing {count} processing activities": "A mostrar {count} atividades de processamento", + "Showing {showing} of {total} agents": "A mostrar {showing} de {total} agentes", + "Showing {showing} of {total} applications": "A mostrar {showing} de {total} aplicações", + "Showing {showing} of {total} audit trail entries": "A mostrar {showing} de {total} entradas de registo de auditoria", + "Showing {showing} of {total} configurations": "A mostrar {showing} de {total} configurações", + "Showing {showing} of {total} deleted items": "A mostrar {showing} de {total} itens eliminados", + "Showing {showing} of {total} entities": "A mostrar {showing} de {total} entidades", + "Showing {showing} of {total} files": "A mostrar {showing} de {total} ficheiros", + "Showing {showing} of {total} log entries": "A mostrar {showing} de {total} entradas de registo", + "Showing {showing} of {total} search trail entries": "A mostrar {showing} de {total} entradas de registo de pesquisa", + "Showing {showing} of {total} sources": "A mostrar {showing} de {total} fontes", + "Showing {showing} of {total} templates": "A mostrar {showing} de {total} modelos", + "Showing {showing} of {total} webhooks": "A mostrar {showing} de {total} webhooks", + "Showing {shown} of {total} organisations": "A mostrar {shown} de {total} organizações", + "Simple": "Simples", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Consultas simples: pesquisas de texto básicas com parâmetros mínimos (por exemplo, um único termo de pesquisa, sem filtros avançados)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Consultas simples: pesquisas de texto básicas com parâmetros mínimos (por exemplo, um único termo de pesquisa, sem filtros avançados)", + "Size": "Tamanho", + "Slug": "Slug", + "Soft Delete Mode": "Modo de eliminação reversível", + "Soft Deleted Items": "Itens eliminados de forma reversível", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Eliminar de forma reversível todos os objetos deste esquema ({active} ativos, {deleted} já eliminados)", + "SolrCloud mode enabled": "Modo SolrCloud ativado", + "Something went wrong while adding files": "Ocorreu um problema ao adicionar ficheiros", + "Source": "Fonte", + "Source successfully updated": "Fonte atualizada com sucesso", + "Sources": "Fontes", + "Sources:": "Fontes:", + "Special Rights": "Direitos especiais", + "Standalone SOLR mode": "Modo SOLR autónomo", + "Start Conversation": "Iniciar conversa", + "Start Date Field": "Campo da data de início", + "Start Vectorization": "Iniciar vetorização", + "Start a conversation": "Iniciar uma conversa", + "Starting names cache warmup...": "A iniciar o aquecimento da cache de nomes...", + "Starting validation for {schema}...": "A iniciar a validação para {schema}...", + "Starting {action} for {schema}...": "A iniciar {action} para {schema}...", + "Starting...": "A iniciar...", + "Statistics": "Estatísticas", + "Statistics and Metrics": "Estatísticas e métricas", + "Statistics refreshed": "Estatísticas atualizadas", + "Stats calculation not yet implemented": "O cálculo de estatísticas ainda não está implementado", + "Status": "Estado", + "Status Code": "Código de estado", + "Step": "Passo", + "Storage": "Armazenamento", + "Storage Quota (MB)": "Quota de armazenamento (MB)", + "String Constraints": "Restrições de cadeia de carateres", + "Subject identifier (email, BSN, name, etc.)": "Identificador do titular (email, BSN, nome, etc.)", + "Subscribe": "Subscrever", + "Subscribed": "Subscrito", + "Success": "Sucesso", + "Success Rate": "Taxa de sucesso", + "Success Status": "Estado de sucesso", + "Successful": "Bem-sucedido", + "Successfully added user to organisation: {name}": "Utilizador adicionado com sucesso à organização: {name}", + "Successfully cleared {count} audit trails": "Foram limpos com sucesso {count} registos de auditoria", + "Successfully cleared {count} blob storage objects": "Foram limpos com sucesso {count} objetos de armazenamento blob", + "Successfully cleared {count} search trails": "Foram limpos com sucesso {count} registos de pesquisa", + "Successfully created {count} SOLR fields": "Foram criados com sucesso {count} campos SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Foram eliminados com sucesso {count} embeddings. Volte a vetorizar os seus dados.", + "Successfully deleted {count} objects for {schema}": "Foram eliminados com sucesso {count} objetos para {schema}", + "Successfully fixed {count} SOLR field configurations": "Foram corrigidas com sucesso {count} configurações de campos SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importado com sucesso: {registers} registos, {schemas} esquemas, {objects} objetos", + "Successfully joined organisation": "Aderiu à organização com sucesso", + "Successfully permanently deleted {count} objects": "Foram eliminados permanentemente com sucesso {count} objetos", + "Successfully processed {count} files!": "Foram processados com sucesso {count} ficheiros!", + "Successfully restored {count} object": "Foi restaurado com sucesso {count} objeto", + "Successfully restored {count} objects": "Foram restaurados com sucesso {count} objetos", + "Successfully saved configuration for {count} facets!": "Configuração guardada com sucesso para {count} facetas!", + "Supported File Types": "Tipos de ficheiro suportados", + "Switch Active Organisation": "Mudar de organização ativa", + "Switch Organisation": "Mudar de organização", + "Sync Table": "Sincronizar tabela", + "Synchronization Settings": "Definições de sincronização", + "Syncing magic table for {schema}...": "A sincronizar a tabela mágica para {schema}...", + "Syncs": "Sincronizações", + "System Default": "Predefinição do sistema", + "System Prompt": "Prompt do sistema", + "System announcements": "Anúncios do sistema", + "Table": "Tabela", + "Target ID": "ID do destino", + "Target Type*": "Tipo de destino*", + "Technical Description": "Descrição técnica", + "Technical Details": "Detalhes técnicos", + "Technical description for developers and administrators": "Descrição técnica para programadores e administradores", + "Technical measures": "Medidas técnicas", + "Technische maatregelen": "Medidas técnicas", + "Temperature": "Temperatura", + "Templates": "Modelos", + "Test": "Testar", + "Test Chat": "Testar conversa", + "Test Connection": "Testar ligação", + "Test Embedding": "Testar embedding", + "Test endpoint executed successfully": "Endpoint de teste executado com sucesso", + "Test webhook delivered successfully": "Webhook de teste entregue com sucesso", + "Test webhook delivery failed": "Falha na entrega do webhook de teste", + "Test webhook sent successfully": "Webhook de teste enviado com sucesso", + "Testing...": "A testar...", + "Text Chunk #{id}": "Bloco de texto n.º {id}", + "Text Extraction": "Extração de texto", + "Text Extraction Engine": "Motor de extração de texto", + "Text chunks": "Blocos de texto", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Os blocos de texto são gerados durante a extração de ficheiros e armazenados na base de dados. A vetorização lê estes ficheiros previamente divididos em blocos e converte-os em embeddings.", + "Text extracted successfully": "Texto extraído com sucesso", + "Text extraction disabled": "Extração de texto desativada", + "The Nextcloud Calendar app is not installed or enabled on this server.": "A aplicação Nextcloud Calendar não está instalada nem ativada neste servidor.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "A aplicação Nextcloud Contacts não está instalada nem ativada neste servidor.", + "The Nextcloud Deck app is not installed or enabled on this server.": "A aplicação Nextcloud Deck não está instalada nem ativada neste servidor.", + "The Nextcloud Mail app is not installed or enabled on this server.": "A aplicação Nextcloud Mail não está instalada nem ativada neste servidor.", + "The URL where webhook events will be sent": "O URL para onde os eventos do webhook serão enviados", + "The requested conversation does not exist": "A conversa solicitada não existe", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Não foi possível carregar o painel solicitado. Verifique o URL ou escolha outro painel da lista.", + "The saved view and all its search configuration": "A vista guardada e toda a sua configuração de pesquisa", + "There are no audit trail entries matching your current filters.": "Não existem entradas de registo de auditoria que correspondam aos seus filtros atuais.", + "There are no deleted items matching your current filters.": "Não existem itens eliminados que correspondam aos seus filtros atuais.", + "There are no search trail entries matching your current filters.": "Não existem entradas de registo de pesquisa que correspondam aos seus filtros atuais.", + "There are no webhook log entries matching your filters.": "Não existem entradas de registo de webhook que correspondam aos seus filtros.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Serão retidos de acordo com o período de retenção configurado no respetivo esquema e eliminados permanentemente de forma automática quando o período de retenção expirar. O período de retenção é configurável por esquema e pode ser encontrado nas definições do esquema.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Serão retidos de acordo com o período de retenção configurado no respetivo esquema e depois removidos permanentemente.", + "This action cannot be undone.": "Esta ação não pode ser anulada.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Esta ação não pode ser anulada. Certifique-se de que nenhuma coleção está a utilizar este ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Esta ação não pode ser anulada. Certifique-se de que nenhuma coleção está a utilizar este ConfigSet.", + "This action will submit a deactivation request to your administrators.": "Esta ação irá submeter um pedido de desativação aos seus administradores.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Este agente não tem vistas ou ferramentas configuráveis, mas pode ainda assim ajustar as definições de RAG abaixo.", + "This analysis may take some time": "Esta análise pode demorar algum tempo", + "This audit trail entry does not contain any change information.": "Esta entrada de registo de auditoria não contém qualquer informação de alteração.", + "This configuration has no data defined.": "Esta configuração não tem dados definidos.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Este endpoint está descontinuado. Utilize antes endpoints baseados em blocos.", + "This entity has no relations to objects or files": "Esta entidade não tem relações com objetos ou ficheiros", + "This is a test webhook from OpenRegister": "Este é um webhook de teste do OpenRegister", + "This is not valid JSON": "Este JSON não é válido", + "This key already exists on this schema": "Esta chave já existe neste esquema", + "This schema must use Magic Table configuration to sync": "Este esquema tem de utilizar a configuração de tabela mágica para sincronizar", + "This schema must use magic table configuration to sync": "Este esquema tem de utilizar a configuração de tabela mágica para sincronizar", + "This source has no associated registers.": "Esta fonte não tem registos associados.", + "This token will only be shown once. Copy it now.": "Este token só será mostrado uma vez. Copie-o agora.", + "This will copy the _default ConfigSet with the new name": "Isto irá copiar o ConfigSet _default com o novo nome", + "This will copy the _default configset with the new name": "Isto irá copiar o ConfigSet _default com o novo nome", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Isto irá eliminar permanentemente TODOS os embeddings (vetores) da base de dados. Terá de voltar a vetorizar todos os objetos e ficheiros. Esta ação não pode ser anulada.\n\nTem a certeza de que pretende continuar?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Isto irá eliminar permanentemente TODOS os embeddings (vetores) da base de dados. Terá de voltar a vetorizar todos os objetos e ficheiros. Esta ação não pode ser anulada.\\n\\nTem a certeza de que pretende continuar?", + "This will permanently delete:": "Isto irá eliminar permanentemente:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Isto irá remover permanentemente {count} objetos já eliminados de forma reversível da base de dados. Esta ação não pode ser anulada.", + "Timeout (seconds)": "Tempo limite (segundos)", + "Timestamp": "Carimbo de data/hora", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Sugestão: ative apenas as vistas que necessitam de pesquisa semântica para minimizar os custos de embedding. As tabelas de consulta simples raramente necessitam de vetorização.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Sugestão: ative apenas as vistas que necessitam de pesquisa semântica para minimizar os custos de embedding. As tabelas de consulta simples raramente necessitam de vetorização.", + "Title": "Título", + "Title *": "Título *", + "Title Template": "Modelo de título", + "Title*": "Título*", + "To (e.g. -1 year or 2025-12-31)": "Até (por exemplo, -1 year ou 2025-12-31)", + "To Date": "Até à data", + "To add files larger than or equal to 512MB, go to the": "Para adicionar ficheiros maiores ou iguais a 512 MB, aceda a", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Para ajustar o tamanho ou a estratégia dos blocos, aceda a Configuração de Ficheiros → Limites de Processamento.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Para ajustar o tamanho ou a estratégia dos blocos, aceda a configuração de ficheiros → limites de processamento.", + "To change the embedding provider or model, go to LLM Configuration.": "Para alterar o fornecedor ou modelo de embedding, aceda a Configuração de LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Para alterar o fornecedor ou modelo de embedding, aceda a configuração de LLM.", + "To date": "Até à data", + "Toggle search sidebar": "Alternar barra lateral de pesquisa", + "Token Created": "Token criado", + "Token Quota (per request)": "Quota de tokens (por pedido)", + "Token copied to clipboard": "Token copiado para a área de transferência", + "Token name": "Nome do token", + "Token revoked": "Token revogado", + "Tools": "Ferramentas", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "As ferramentas são executadas com as permissões do utilizador predefinido do agente quando não existe uma sessão de utilizador ativa (por exemplo, tarefas cron). Configure o utilizador predefinido no separador Definições.", + "Tools respect the agent's own role and organization boundaries.": "As ferramentas respeitam o próprio papel do agente e os limites da organização.", + "Tools respect the agent's views, permissions, and organization boundaries.": "As ferramentas respeitam as vistas, permissões e limites da organização do agente.", + "Top Deleters": "Principais autores de eliminações", + "Total": "Total", + "Total Audit Trails": "Total de registos de auditoria", + "Total Chunks Available:": "Total de blocos disponíveis:", + "Total Deleted Items": "Total de itens eliminados", + "Total Members": "Total de membros", + "Total Objects": "Total de objetos", + "Total Objects in Database:": "Total de objetos na base de dados:", + "Total Organisations": "Total de organizações", + "Total Results": "Total de resultados", + "Total Searches": "Total de pesquisas", + "Total Size": "Tamanho total", + "Total Storage": "Armazenamento total", + "Total events": "Total de eventos", + "Total:": "Total:", + "Totals": "Totais", + "Try searching by UUID or with different keywords": "Tente pesquisar por UUID ou com palavras-chave diferentes", + "Type": "Tipo", + "Type \"DELETE COLLECTION\" to confirm:": "Escreva \"DELETE COLLECTION\" para confirmar:", + "Type Issue": "Problema de tipo", + "Type Variations:": "Variações de tipo:", + "Type filter (optional, e.g. email)": "Filtro de tipo (opcional, por exemplo, email)", + "Type must be \"positive\" or \"negative\"": "O tipo tem de ser \"positive\" ou \"negative\"", + "Type to search for groups": "Escreva para pesquisar grupos", + "Type to search for objects...": "Escreva para pesquisar objetos...", + "Type to search for organisations": "Escreva para pesquisar organizações", + "Type to search for users": "Escreva para pesquisar utilizadores", + "Type to search groups...": "Escreva para pesquisar grupos...", + "Type to search users...": "Escreva para pesquisar utilizadores...", + "Type to search...": "Escreva para pesquisar...", + "Type your username to confirm": "Escreva o seu nome de utilizador para confirmar", + "URL": "URL", + "URL where Ollama is running": "URL onde o Ollama está em execução", + "UUID copied to clipboard": "UUID copiado para a área de transferência", + "UUID:": "UUID:", + "Unchanged": "Inalterado", + "Unique Search Terms": "Termos de pesquisa únicos", + "Unique Terms": "Termos únicos", + "Unknown": "Desconhecido", + "Unknown Object": "Objeto desconhecido", + "Unknown Register": "Registo desconhecido", + "Unknown Schema": "Esquema desconhecido", + "Unknown error": "Erro desconhecido", + "Unknown widget type: {type}": "Tipo de widget desconhecido: {type}", + "Unlink email": "Desassociar email", + "Unlink event": "Desassociar evento", + "Unnamed Object": "Objeto sem nome", + "Untitled": "Sem título", + "Untitled View": "Vista sem título", + "Update": "Atualizar", + "Update Operations": "Operações de atualização", + "Update available: {local} → {remote}": "Atualização disponível: {local} → {remote}", + "Update configuration: ...": "Atualizar configuração: ...", + "Update register OAS: ...": "Atualizar OAS do registo: ...", + "Update vectors when object data changes (recommended for accurate search)": "Atualizar vetores quando os dados do objeto forem alterados (recomendado para uma pesquisa precisa)", + "Updated": "Atualizado", + "Updated At": "Atualizado em", + "Updated:": "Atualizado:", + "Upload new avatar": "Carregar novo avatar", + "Uploading configuration sets...": "A carregar conjuntos de configuração...", + "Uptime:": "Tempo de atividade:", + "Url": "Url", + "Usage Count": "Contagem de utilizações", + "Use AI agents for processing and analysis": "Utilizar agentes de IA para processamento e análise", + "Use Large Language Model features": "Utilizar funcionalidades de Large Language Model", + "Use SolrCloud with Zookeeper for distributed search": "Utilizar o SolrCloud com o Zookeeper para pesquisa distribuída", + "Use SolrCloud with zookeeper for distributed search": "Utilizar o SolrCloud com o Zookeeper para pesquisa distribuída", + "Use VNG APIs instead of MongoDB.": "Utilizar as APIs da VNG em vez do MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Utilizar intervalos predefinidos (Últimos 7/30/90 dias, Último ano, Mais antigos)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Utilizar armazenamento externo (por exemplo, MongoDB) em vez do armazenamento interno do Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Utilize filtros para restringir as entradas de registo de auditoria por registo, esquema, tipo de ação, utilizador, intervalo de datas ou ID do objeto.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Utilize filtros para restringir os itens eliminados por registo, esquema, data de eliminação ou utilizador que os eliminou.", + "Use filters to narrow down entities by type or category.": "Utilize filtros para restringir as entidades por tipo ou categoria.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Utilize filtros para restringir as entradas de registo de pesquisa por registo, esquema, estado de sucesso, utilizador, intervalo de datas, termos de pesquisa ou métricas de desempenho.", + "Used By Collections": "Utilizado por coleções", + "Used by": "Utilizado por", + "User": "Utilizador", + "User Agent Statistics": "Estatísticas de agente de utilizador", + "User Description": "Descrição do utilizador", + "User Quota": "Quota do utilizador", + "User rate limits cleared successfully": "Limites de taxa do utilizador limpos com sucesso", + "User removed successfully": "Utilizador removido com sucesso", + "User-friendly description shown in forms and help text": "Descrição acessível mostrada em formulários e textos de ajuda", + "Username": "Nome de utilizador", + "Username for SOLR authentication (optional)": "Nome de utilizador para autenticação SOLR (opcional)", + "Users": "Utilizadores", + "Uses": "Utilizações", + "Using Pre-Generated Chunks": "A utilizar blocos pré-gerados", + "Validate": "Validar", + "Validate Objects": "Validar objetos", + "Validation completed for {schema}": "Validação concluída para {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validação concluída para {schema}: {processed} processados, {updated} atualizados, {failed} falhados", + "Validation completed successfully": "Validação concluída com sucesso", + "Validation failed": "A validação falhou", + "Value": "Valor", + "Value list (split on ,)": "Lista de valores (separada por ,)", + "Vector Dimensions": "Dimensões do vetor", + "Vector Search Backend": "Backend de pesquisa vetorial", + "Vector field: _embedding_": "Campo de vetor: _embedding_", + "Vectorization Triggers": "Acionadores de vetorização", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vetorização concluída: {vectorized} objetos vetorizados, {failed} falhados", + "Vectorization failed: {error}": "A vetorização falhou: {error}", + "Vectorize All Files": "Vetorizar todos os ficheiros", + "Vectorize All Objects": "Vetorizar todos os objetos", + "Vectorize all file types": "Vetorizar todos os tipos de ficheiro", + "Vectorize all views": "Vetorizar todas as vistas", + "Vectorize on object creation": "Vetorizar na criação do objeto", + "Vectors will be stored in your existing object and file collections": "Os vetores serão armazenados nas suas coleções existentes de objetos e ficheiros", + "Verantwoording": "Justificação", + "Verifying server connectivity...": "A verificar a conetividade com o servidor...", + "Version": "Versão", + "Version Information": "Informação da versão", + "Very High": "Muito alto", + "Very high": "Muito alto", + "View": "Vista", + "View API Docs": "Ver documentação da API", + "View API Documentation": "Ver documentação da API", + "View APIs": "Ver APIs", + "View Changes": "Ver alterações", + "View Details": "Ver detalhes", + "View Error": "Ver erro", + "View File": "Ver ficheiro", + "View Full Details": "Ver todos os detalhes", + "View Logs": "Ver registos", + "View Name": "Nome da vista", + "View Object": "Ver objeto", + "View Parameters": "Ver parâmetros", + "View Selection": "Seleção de vistas", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Veja e analise os registos de pesquisa com filtragem avançada e capacidades analíticas", + "View and analyze system audit trails with advanced filtering capabilities": "Veja e analise os registos de auditoria do sistema com capacidades de filtragem avançada", + "View and manage authorization across registers and schemas": "Veja e faça a gestão da autorização nos registos e esquemas", + "View deleted successfully": "Vista eliminada com sucesso", + "View entity details and manage relations": "Ver detalhes da entidade e fazer a gestão das relações", + "View name": "Nome da vista", + "View name is required": "O nome da vista é obrigatório", + "View object": "Ver objeto", + "View saved successfully!": "Vista guardada com sucesso!", + "View search analytics and manage search logs": "Veja as análises de pesquisa e faça a gestão dos registos de pesquisa", + "View successfully deleted": "Vista eliminada com sucesso", + "View successfully updated": "Vista atualizada com sucesso", + "View updated successfully!": "Vista atualizada com sucesso!", + "View webhook delivery logs and filter by webhook": "Veja os registos de entrega de webhooks e filtre por webhook", + "View {count} more": "Ver mais {count}", + "Views": "Vistas", + "Visible to end users": "Visível para os utilizadores finais", + "Visible to users": "Visível para os utilizadores", + "Wait for Response": "Aguardar resposta", + "Wait for webhook response before continuing (required for request/response flows)": "Aguardar a resposta do webhook antes de continuar (obrigatório para fluxos de pedido/resposta)", + "Waiting for cluster synchronization...": "A aguardar pela sincronização do cluster...", + "Warmup Names Cache": "Aquecer cache de nomes", + "Warmup failed": "O aquecimento falhou", + "Warning": "Aviso", + "Warning:": "Aviso:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Aviso: esta propriedade tem tipos inconsistentes: {types}. Considere se todos os objetos devem ter o mesmo tipo.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Iremos analisar todos os objetos pertencentes a este esquema para descobrir novas propriedades e analisar as propriedades existentes em busca de potenciais melhorias. O processo envolve examinar a estrutura de dados de cada objeto, identificar propriedades não definidas no esquema atual e encontrar oportunidades para melhorar as definições das propriedades existentes com melhores restrições, formatos e regras de validação.", + "Webhook": "Webhook", + "Webhook Log Details": "Detalhes do registo do webhook", + "Webhook Logs": "Registos de webhooks", + "Webhook created successfully": "Webhook criado com sucesso", + "Webhook deleted": "Webhook eliminado", + "Webhook retry delivered successfully": "Nova tentativa de webhook entregue com sucesso", + "Webhook retry delivery failed": "Falha na entrega da nova tentativa de webhook", + "Webhook updated": "Webhook atualizado", + "Webhook updated successfully": "Webhook atualizado com sucesso", + "Webhooks": "Webhooks", + "Weekly": "Semanalmente", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Quando o agente é executado sem uma sessão de utilizador (por exemplo, tarefas agendadas), será utilizado o contexto deste utilizador", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Quando o agente é executado sem uma sessão de utilizador (por exemplo, tarefas agendadas), será utilizado o contexto deste utilizador.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Quando desativada, esta faceta mostrará apenas valores deste esquema e incluirá um filtro de esquema quando selecionada.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Quando ativado, ao guardar este objeto também serão atualizados os objetos de destino para incluir uma referência de volta a este objeto.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Quando ativado, esta propriedade será removida do objeto de origem após a atualização dos objetos de destino.", + "Workflow Management": "Gestão de fluxos de trabalho", + "Workflows": "Fluxos de trabalho", + "Wrap webhook payload in CloudEvents format for better interoperability": "Encapsular o payload do webhook no formato CloudEvents para melhor interoperabilidade", + "Wrap webhook payload in cloudevents format for better interoperability": "Encapsular o payload do webhook no formato CloudEvents para melhor interoperabilidade", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Sim", + "You": "Você", + "You are not yet a member of any organisations.": "Ainda não é membro de nenhuma organização.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Pode criar novos ConfigSets baseados no modelo _default, ou carregar configurações personalizadas diretamente para o seu servidor SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Pode criar novos ConfigSets baseados no modelo _default, ou carregar configurações personalizadas diretamente para o seu servidor SOLR.", + "You do not have access to this conversation": "Não tem acesso a esta conversa", + "You do not have permission to delete this conversation": "Não tem permissão para eliminar esta conversa", + "You do not have permission to modify this conversation": "Não tem permissão para modificar esta conversa", + "You do not have permission to restore this conversation": "Não tem permissão para restaurar esta conversa", + "You do not have permission to view the permission matrix. Admin access is required.": "Não tem permissão para ver a matriz de permissões. É necessário acesso de administrador.", + "You have selected {count} properties to add to the schema.": "Selecionou {count} propriedades para adicionar ao esquema.", + "You must be logged in to favorite views": "Tem de ter sessão iniciada para marcar vistas como favoritas", + "You need an AI agent to start a conversation.": "Necessita de um agente de IA para iniciar uma conversa.", + "Your Fireworks AI API key. Get one at": "A sua chave de API da Fireworks AI. Obtenha uma em", + "Your OpenAI API key. Get one at": "A sua chave de API da OpenAI. Obtenha uma em", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "O seu comentário foi registado. Opcionalmente, pode fornecer detalhes adicionais aqui...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "O seu comentário foi registado. Opcionalmente, pode fornecer detalhes adicionais aqui...", + "Zookeeper Hosts": "Anfitriões do Zookeeper", + "Zookeeper Port": "Porta do Zookeeper", + "Zookeeper Settings (SolrCloud)": "Definições do Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Cadeia de ligação do Zookeeper para o SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Número da porta do Zookeeper (opcional, predefinição 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVISO DE ELIMINAÇÃO PERMANENTE \\u26A0\\uFE0F\n\nEstá prestes a eliminar PERMANENTEMENTE TODOS os objetos do esquema \"{schema}\":\n\n\\u2022 Objetos ativos: {active}\n\\u2022 Objetos eliminados de forma reversível: {deleted}\n\\u2022 Total: {total}\n\nEstes objetos serão completamente removidos da base de dados e NÃO PODEM ser recuperados.\n\nTem a certeza absoluta?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVISO DE ELIMINAÇÃO PERMANENTE \\u26A0\\uFE0F\n\nEstá prestes a eliminar PERMANENTEMENTE {count} objetos eliminados de forma reversível do esquema \"{schema}\".\n\nEstes objetos serão completamente removidos da base de dados e NÃO PODEM ser recuperados.\n\nTem a certeza absoluta?", + "and accessible to all users in selected groups (or all users if no groups selected).": "e acessível a todos os utilizadores nos grupos selecionados (ou a todos os utilizadores se nenhum grupo for selecionado).", + "and add the files there.": "e adicione os ficheiros aí.", + "approved": "aprovado", + "are only accessible to invited users.": "são apenas acessíveis a utilizadores convidados.", + "by": "por", + "chunk": "bloco", + "chunks": "blocos", + "chunks processed": "blocos processados", + "completed": "concluído", + "configuration(s)": "configuração(ões)", + "conversationId is required": "conversationId é obrigatório", + "create": "criar", + "delete": "eliminar", + "deleted objects section": "secção de objetos eliminados", + "e.g., lib/Settings/config.json": "e.g., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "e.g., lib/Settings/register.json", + "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", + "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", + "execute": "executar", + "failed": "falhado", + "file": "ficheiro", + "files": "ficheiros", + "file{plural}": "ficheiro{plural}", + "folder": "pasta", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "em {register}", + "in_progress": "em curso", + "inherited": "herdado", + "list": "listar", + "log{plural}": "registo{plural}", + "manage": "gerir", + "members": "membros", + "menu or contact someone with permission to create agents.": "menu ou contacte alguém com permissão para criar agentes.", + "message content is required": "o conteúdo da mensagem é obrigatório", + "messages": "mensagens", + "missing": "em falta", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL and API key are required", + "n8n configuration saved successfully": "Configuração do n8n guardada com sucesso", + "n8n connection not configured": "Ligação do n8n não configurada", + "n8n connection successful": "Ligação do n8n bem-sucedida", + "n8n connection test failed: {message}": "O teste de ligação do n8n falhou: {message}", + "n8n connection test successful": "Teste de ligação do n8n bem-sucedido", + "n8n initialization failed: {message}": "A inicialização do n8n falhou: {message}", + "n8n integration disabled": "Integração do n8n desativada", + "n8n integration enabled": "Integração do n8n ativada", + "n8n project initialized successfully": "Projeto n8n inicializado com sucesso", + "n8n settings saved successfully": "Definições do n8n guardadas com sucesso", + "notify_push configuration guide": "guia de configuração do notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "O notify_push está instalado, mas o OpenRegister ainda não confirmou um push bem-sucedido. Acione a gravação de um objeto para ativar, ou verifique a sua configuração do notify_push.", + "notify_push is installed but not yet active": "O notify_push está instalado, mas ainda não está ativo", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objetos", + "objects processed": "objetos processados", + "object{plural}": "objeto{plural}", + "or pick a range": "ou escolha um intervalo", + "org-...": "org-...", + "pending": "pendente", + "provides a good balance between speed and accuracy.": "proporciona um bom equilíbrio entre velocidade e precisão.", + "public": "público", + "read": "ler", + "register(s)": "registo(s)", + "register(s) selected": "registo(s) selecionado(s)", + "register{plural}": "registo{plural}", + "rejected": "rejeitado", + "required": "obrigatório", + "results": "resultados", + "schema(s)": "esquema(s)", + "schema(s) selected": "esquema(s) selecionado(s)", + "schema{plural}": "esquema{plural}", + "selected objects": "objetos selecionados", + "set": "definir", + "sk-...": "sk-...", + "this application": "esta aplicação", + "to": "para", + "type must be \"positive\" or \"negative\"": "o tipo tem de ser \"positive\" ou \"negative\"", + "update": "atualizar", + "vectorized": "vetorizado", + "view": "ver", + "views selected": "vistas selecionadas", + "waiting": "em espera", + "{active} active, {deleted} deleted": "{active} ativos, {deleted} eliminados", + "{count} audit trails cleared successfully": "{count} registos de auditoria limpos com sucesso", + "{count} compliance issue(s) detected": "{count} problema(s) de conformidade detetado(s)", + "{count} deletions": "{count} eliminações", + "{count} documents found": "{count} documentos encontrados", + "{count} email": "{count} email", + "{count} emails": "{count} emails", + "{count} entries": "{count} entradas", + "{count} issues": "{count} problemas", + "{count} members": "{count} membros", + "{count} searches": "{count} pesquisas", + "{count} selected": "{count} selecionado(s)", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} objeto(s) correspondido(s). {erased} apagado(s).", + "{minutes} minutes ago": "há {minutes} minutos", + "{percentage}% of objects": "{percentage}% dos objetos", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} em {register} / {schema}", + "{type} cache cleared successfully": "Cache {type} limpa com sucesso", + "ℹ️ Current Configuration": "ℹ️ Configuração atual", + "ℹ️ current configuration": "ℹ️ configuração atual", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ PERIGO: Todos os {total} objetos serão ELIMINADOS PERMANENTEMENTE da base de dados. Esta ação é IRRECUPERÁVEL e não pode ser anulada!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVISO DE ELIMINAÇÃO PERMANENTE ⚠️\n\nEstá prestes a eliminar PERMANENTEMENTE TODOS os objetos do esquema \"{schema}\":\n\n• Objetos ativos: {active}\n• Objetos eliminados de forma reversível: {deleted}\n• Total: {total}\n\nEstes objetos serão completamente removidos da base de dados e NÃO PODEM ser recuperados.\n\nTem a certeza absoluta?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVISO DE ELIMINAÇÃO PERMANENTE ⚠️\n\nEstá prestes a eliminar PERMANENTEMENTE {count} objetos eliminados de forma reversível do esquema \"{schema}\".\n\nEstes objetos serão completamente removidos da base de dados e NÃO PODEM ser recuperados.\n\nTem a certeza absoluta?", + "⚡ Batch Processing": "⚡ Processamento em lote", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ O SOLR consegue processar milhões de documentos com tempos de resposta de pesquisa inferiores a um segundo...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Contexto abrangente ✓ Menos probabilidade de perder detalhes ✗ Respostas mais lentas ✗ Pode incluir informação menos relevante", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Respostas mais rápidas ✓ Respostas mais focadas ✗ Pode perder informação relevante", + "✨ AI Features": "✨ Funcionalidades de IA", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 No modo distribuído, o SOLR utiliza o ZooKeeper para a coordenação do cluster...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 A pesquisa por facetas permite aos utilizadores aprofundar os resultados por categorias...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Sabia que? O SOLR alimenta a pesquisa da Netflix, da Apple e de muitos outros grandes sites!", + "💰 View Selection (Cost Optimization)": "💰 Seleção de vistas (otimização de custos)", + "📄 Object Serialization": "📄 Serialização de objetos", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 O SOLR fornece análises e estatísticas ricas sobre o desempenho da pesquisa...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Os ConfigSets contêm o esquema e os ficheiros de configuração do seu índice de pesquisa...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 A replicação garante que o seu índice de pesquisa está disponível mesmo que os nós falhem...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 O SOLR é uma poderosa plataforma de pesquisa empresarial construída sobre o Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Definições de vetorização", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 O escalonamento automático pode adicionar ou remover nós dinamicamente com base na carga...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ As funcionalidades de segurança incluem autenticação, autorização e encriptação SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-mails" + ], + "Purge {count} object from database": [ + "Expurgar {count} objeto da base de dados", + "Expurgar {count} objetos da base de dados" + ], + "Restore {count} object": [ + "Restaurar {count} objeto", + "Restaurar {count} objetos" + ], + "Successfully restored {count} object": [ + "{count} objeto restaurado com sucesso", + "{count} objetos restaurados com sucesso" + ], + "Delete {count} object": [ + "Eliminar {count} objeto", + "Eliminar {count} objetos" + ], + "Object successfully deleted": [ + "Objeto eliminado com sucesso", + "Objetos eliminados com sucesso" + ] + } +} diff --git a/l10n/rm.js b/l10n/rm.js new file mode 100644 index 0000000000..4f0231604e --- /dev/null +++ b/l10n/rm.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(nagin object)", + "(no title)" : "(nagin titel)", + "(unnamed)" : "(nundenominà)", + "(untitled card)" : "(charta senza titel)", + "Activities" : "Activitads", + "Add Endpoint" : "Agiuntar endpoint", + "Add Source" : "Agiuntar funtauna", + "Add card" : "Agiuntar charta", + "Add contact" : "Agiuntar contact", + "All clear" : "Tut en urden", + "Articles" : "Artitgels", + "Code" : "Code", + "Compliance" : "Conformitad", + "Contacts" : "Contacts", + "Create Agent" : "Crear agent", + "Create Application" : "Crear applicaziun", + "Create event" : "Crear eveniment", + "Erasure complete" : "Stizzada cumpletada", + "Erasure preview" : "Prevista da la stizzada", + "Expand details" : "Mussar ils detagls", + "Save changes" : "Memorisar las midadas", + "📄 Object Serialization" : "📄 Serialisaziun da l'object", + "🔢 Vectorization Settings" : "🔢 Configuraziuns da vectorisaziun", + "💰 View Selection (Cost Optimization)" : "💰 Selecziun da vistas (optimaziun dals custs)", + "AI Assistant" : "Assistent IA", + "AI Chat" : "Chat IA", + "API Key" : "Clav API", + "API tokens saved successfully" : "Ils tokens API èn vegnids memorisads cun success", + "About ConfigSets" : "Davart ils ConfigSets", + "Action" : "Acziun", + "Action Distribution" : "Distribuziun da las acziuns", + "Action:" : "Acziun:", + "Actions" : "Acziuns", + "Active" : "Activ", + "Active Collections" : "Collecziuns activas", + "Active Filters" : "Filters activs", + "Active Organisations" : "Organisaziuns activas", + "Active filters:" : "Filters activs:", + "Active organisation set successfully" : "L'organisaziun activa è vegnida fixada cun success", + "Activity Period" : "Perioda d'activitad", + "Add Groups" : "Agiuntar gruppas", + "Add Schema" : "Agiuntar Schema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Agiuntar titels da Schema, descripziuns ed infurmaziuns dal Register per porscher in context pli ritg per la tschertga", + "Add to favorites" : "Agiuntar als favorits", + "Added" : "Agiuntà", + "Added to favorites" : "Agiuntà als favorits", + "Additional Information" : "Infurmaziuns supplementaras", + "Additional feedback saved. Thank you!" : "Il feedback supplementar è vegnì memorisà. Grazia fitg!", + "Admin" : "Administraziun", + "Advanced" : "Avanzà", + "Advanced Filters" : "Filters avanzads", + "Advanced Options" : "Opziuns avanzadas", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Ils filters avanzads na stattan betg a disposiziun cur ch'i vegn duvrada la funtauna da banca da datas. Midai sin Auto u SOLR Index per opziuns da filtrar.", + "After" : "Suenter", + "Agent deleted successfully" : "L'agent è vegnì stizzà cun success", + "Agents" : "Agents", + "All" : "Tut", + "All Categories" : "Tut las categorias", + "All Collections" : "Tut las collecziuns", + "All Confidence Levels" : "Tut ils nivels da confidenza", + "All ConfigSets" : "Tut ils ConfigSets", + "All Files" : "Tut las Datotecas", + "All Levels" : "Tut ils nivels", + "All Types" : "Tut ils tips", + "All Webhooks" : "Tut ils webhooks", + "All actions" : "Tut las acziuns", + "All audit trails cleared successfully" : "Tut las colliaziuns d'audit èn vegnidas stizzadas cun success", + "All registers" : "Tut ils Registers", + "All schemas" : "Tut ils Schemas", + "All searches" : "Tut las tschertgas", + "All users" : "Tut ils utilisaders", + "An error occurred while clearing audit trails" : "Igl è succedida ina errur durant il stizzar da las colliaziuns d'audit", + "An error occurred while deleting the audit trail" : "Igl è succedida ina errur durant il stizzar da la colliaziun d'audit", + "An error occurred while deleting the view" : "Igl è succedida ina errur durant il stizzar da la vista", + "An error occurred while permanently deleting the objects" : "Igl è succedida ina errur durant il stizzar permanent dals objects", + "Analysis completed:" : "Analisa cumpletada:", + "Analysis steps:" : "Pass da l'analisa:", + "Analytics" : "Analisa", + "Analyze Objects" : "Analisar ils objects", + "Analyze Properties" : "Analisar las caracteristicas", + "Analyze existing properties for improvement opportunities" : "Analisar las caracteristicas existentas per pussaivladads da meglieraziun", + "Analyzing..." : "Analisar...", + "Any favorites and sharing settings for this view" : "Tut ils favorits e las configuraziuns da cumpartir per questa vista", + "Any user" : "Mintga utilisader", + "App store cache invalidated successfully" : "Il cache da l'App Store è vegnì invalidà cun success", + "Application deleted successfully" : "L'applicaziun è vegnida stizzada cun success", + "Applications" : "Applicaziuns", + "Apply Changes" : "Applitgar las midadas", + "Archive" : "Archiv", + "Archive conversation" : "Archivar la conversaziun", + "Archived conversations are hidden from your active list" : "Las conversaziuns archivadas èn zuppentadas en Vossa glista activa", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Essas Vus segir che Vus vulais rumir las colliaziuns da tschertga veglias? Quai stizza las endataziuns pli veglias che 30 dis.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Essas Vus segir che Vus vulais stizzar las colliaziuns d'audit tschernidas? Questa acziun na po betg vegnir revocada.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Essas Vus segir che Vus vulais stizzar las colliaziuns da tschertga tschernidas? Questa acziun na po betg vegnir revocada.", + "Are you sure you want to delete this ConfigSet?" : "Essas Vus segir che Vus vulais stizzar quest ConfigSet?", + "Are you sure you want to permanently delete" : "Essas Vus segir che Vus vulais stizzar permanentamain", + "Ask a question..." : "Far ina dumonda...", + "Ask questions about your data using natural language" : "Far dumondas davart Vossas datas cun lingua natirala", + "Attempt" : "Emprova", + "Audit Trail Changes" : "Midadas da la colliaziun d'audit", + "Audit Trail Details" : "Detagls da la colliaziun d'audit", + "Audit Trail Management" : "Administraziun da las colliaziuns d'audit", + "Audit Trail Statistics" : "Statistica da las colliaziuns d'audit", + "Audit Trails" : "Colliaziuns d'audit", + "Audit trail data copied to clipboard" : "Las datas da la colliaziun d'audit èn vegnidas copiadas en l'archivet provisoric", + "Audit trail deleted successfully" : "La colliaziun d'audit è vegnida stizzada cun success", + "Audit trail successfully deleted" : "La colliaziun d'audit è vegnida stizzada cun success", + "Authentication" : "Autentificaziun", + "Auto-commit disabled" : "Auto-commit deactivà", + "Auto-commit enabled" : "Auto-commit activà", + "Auto-create Default Organisation" : "Crear automaticamain l'organisaziun standard", + "Auto-retry failed vectorizations" : "Reemprovar automaticamain las vectorisaziuns fallidas", + "Automatically commit changes to SOLR index" : "Cummetter automaticamain las midadas a l'index SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Crear automaticamain ina organisaziun standard sche nagina exista cur che l'applicaziun vegn inizialisada", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Generar automaticamain embeddings da vectur dals chunks da text cur che las Datotecas vegnan transferidas ed elavuradas", + "Automatically generate vector embeddings when objects are created or updated" : "Generar automaticamain embeddings da vectur cur che ils objects vegnan creads u actualisads", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Reemprovar automaticamain las empruvas da vectorisaziun fallidas (max 3 empruvas)", + "Available Filters" : "Filters disponibels", + "Available Workflows" : "Process da lavur disponibels", + "Avg Execution Time" : "Temp d'execuziun media", + "Avg Members/Org" : "Commembers media/org", + "Avg Object Views/Session" : "Vistas d'object media/sessiun", + "Avg Response Time" : "Temp da resposta media", + "Avg Results/Search" : "Resultats media/tschertga", + "Avg Searches/Session" : "Tschertgas media/sessiun", + "Back" : "Enavos", + "Back to Entities" : "Enavos a las entitads", + "Back to Registers" : "Enavos als Registers", + "Back to Webhooks" : "Enavos als webhooks", + "Back to applications" : "Enavos a las applicaziuns", + "Back to entities" : "Enavos a las entitads", + "Backend updated successfully. Please reload the application." : "Il backend è vegnì actualisà cun success. Chargiai per plaschair danovamain l'applicaziun.", + "Base URL (Optional)" : "URL da basa (opziunal)", + "Basic Information" : "Infurmaziuns da basa", + "Batch Size" : "Grondezza dal batch", + "Batch extraction completed" : "L'extracziun da batch è cumpletada", + "Before" : "Avant", + "Before object vectorization can work:" : "Avant che la vectorisaziun d'object po funcziunar:", + "Behavior Issue" : "Problem da cumportament", + "Bookmarks" : "Segnapaginas", + "Blob storage has been retired. All objects now use magic tables." : "Il deposit blob è vegnì retratg. Tut ils objects dovran ussa tabellas magicas.", + "Branch" : "Branch", + "Bulk delete operation completed successfully" : "L'operaziun da stizzar en massa è vegnida cumpletada cun success", + "Bulk save operation completed successfully" : "L'operaziun da memorisar en massa è vegnida cumpletada cun success", + "Business Data" : "Datas d'affar", + "Cache cleared successfully" : "Il cache è vegnì stizzà cun success", + "Calculate Sizes" : "Calcular las grondezzas", + "Cancel" : "Annullar", + "Cannot delete: objects are still attached" : "Impussibel da stizzar: objects èn anc agiuntads", + "Category" : "Categoria", + "Change Type" : "Tip da midada", + "Changes" : "Midadas", + "Changes copied to clipboard" : "Las midadas èn vegnidas copiadas en l'archivet provisoric", + "Chat Model" : "Model da chat", + "Chat Provider" : "Purschider da chat", + "Chat Provider (RAG)" : "Purschider da chat (RAG)", + "Chat Settings" : "Configuraziuns da chat", + "Chat provider connection successful!" : "La colliaziun cun il purschider da chat è reussida!", + "Chat settings" : "Configuraziuns da chat", + "Choose a register" : "Tscherner in Register", + "Choose a schema" : "Tscherner in Schema", + "Choose how vector similarity calculations are performed for semantic search" : "Tscherner co ils calculs da similitad da vectur vegnan exequids per la tschertga semantica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Tscherner quals tips da Datoteca includer en il process da vectorisaziun. Mo Datotecas cun text e chunks extratgs vegnan elavuradas.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Tscherner qualas vistas includer en il process da vectorisaziun. Laschai vid per elavurar tut las vistas a basa da Vossa configuraziun.", + "Chunk" : "Chunk", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Il stizzar da chunks n'è anc betg implementà. Duvrai endpoints basads sin chunks.", + "Chunks" : "Chunks", + "Chunks to Vectorize:" : "Chunks per vectorisar:", + "Cleanup Old Trails" : "Rumir las colliaziuns veglias", + "Cleanup completed" : "La rumida è cumpletada", + "Clear All Embeddings" : "Stizzar tut ils embeddings", + "Clear Entries" : "Stizzar las endataziuns", + "Clear Filtered Audit Trails" : "Stizzar las colliaziuns d'audit filtradas", + "Clear Filters" : "Stizzar ils filters", + "Clear Index" : "Stizzar l'index", + "Clear Selection" : "Stizzar la selecziun", + "Clear all" : "Stizzar tut", + "Clear all filters" : "Stizzar tut ils filters", + "Clear date range" : "Stizzar la perioda da data", + "Clearing..." : "Stizzar...", + "Close" : "Serrar", + "Collection Management" : "Administraziun da las collecziuns", + "Collection Name" : "Num da la collecziun", + "Collection assignments updated successfully" : "Las attribuziuns da collecziun èn vegnidas actualisadas cun success", + "Collection cleared successfully" : "La collecziun è vegnida stizzada cun success", + "Collection copied successfully" : "La collecziun è vegnida copiada cun success", + "Collection created successfully" : "La collecziun è vegnida creada cun success", + "Collection deleted successfully" : "La collecziun è vegnida stizzada cun success", + "Collection used to store and index file metadata and content" : "Collecziun duvrada per memorisar ed indexar metadatas e cuntegn da Datotecas", + "Collection used to store and index object data" : "Collecziun duvrada per memorisar ed indexar datas d'object", + "Collections:" : "Collecziuns:", + "Commit Message" : "Messadi da commit", + "Commit Within (ms)" : "Cummetter entaifer (ms)", + "Commit message" : "Messadi da commit", + "Compare current schema with real object data" : "Cumparegliar il Schema actual cun datas d'object realas", + "Completed" : "Cumpletà", + "Complex" : "Cumplex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Dumondas cumplexas: tschertgas avanzadas cun plirs filters, operaturs e cumbinaziuns da parameters cumplexas", + "Confidence Level" : "Nivel da confidenza", + "Confidence Score" : "Punctuaziun da confidenza", + "Config must be provided as an object" : "La configuraziun sto vegnir furnida sco object", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Administraziun dals ConfigSets", + "ConfigSet Name" : "Num dal ConfigSet", + "ConfigSet created successfully" : "Il ConfigSet è vegnì creà cun success", + "ConfigSet deleted successfully" : "Il ConfigSet è vegnì stizzà cun success", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Ils ConfigSets defineschan il Schema e la configuraziun per Vossas collecziuns SOLR. Els cuntegnan definiziuns da chomps, analisaturs ed autras configuraziuns da tschertga.", + "Configuration" : "Configuraziun", + "Configuration Data" : "Datas da configuraziun", + "Configuration saved" : "La configuraziun è vegnida memorisada", + "Configurations" : "Configuraziuns", + "Configure Facets" : "Configurar las facettas", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Configurar purschiders da Large Language Model (LLM) per funcziuns basadas sin IA, inclus tschertga semantica, embeddings e chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Configurar las configuraziuns da basa da la colliaziun per Voss server SOLR, inclus l'autentificaziun e las opziuns da rait. Duvrai ils dialogs separads d'administraziun da ConfigSet e collecziuns per administrar cores e collecziuns.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Configurar co ils objects da la banca da datas vegnan convertids en embeddings da vectur per la tschertga semantica. Ils objects vegnan vectorisads directamain senza basegnar in'extracziun da text.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Configurar co ils objects vegnan convertids en text avant la vectorisaziun. Questas configuraziuns influenzeschan la qualitad e il context da la tschertga.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Configurar ils parameters per la vectorisaziun da Datotecas. Quest process generescha embeddings da vectur per tut ils chunks da Datoteca extratgs.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Configurar ils parameters per la vectorisaziun d'objects. Quest process generescha embeddings da vectur per tut ils objects che correspundan a Voss filters da vista.", + "Configure which types of data to search and how many sources to retrieve" : "Configurar quals tips da datas tschertgar e quantas funtaunas recuperar", + "Connection Failed" : "La colliaziun è fallida", + "Connection Settings" : "Configuraziuns da la colliaziun", + "Connection Successful!" : "La colliaziun è reussida!", + "Connection failed" : "La colliaziun è fallida", + "Connection protocol" : "Protocol da colliaziun", + "Connection successful" : "La colliaziun è reussida", + "Connection timeout in seconds" : "Timeout da la colliaziun en secundas", + "Constant delay between retries (5 minutes)" : "Retardada constanta tranter las empruvas (5 minutas)", + "Constraint Issue" : "Problem da restricziun", + "Content Filters" : "Filters da cuntegn", + "Continue to Properties" : "Cuntinuar a las caracteristicas", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Controllar qualas vistas d'object duessan vegnir vectorisadas per reducir ils custs API. Vectorisai mo vistas che profiteschan da la tschertga semantica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Controllar qualas vistas ed utensils l'IA po duvrar en questa conversaziun. Sco standard èn tut las abilitads da l'agent activadas.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Controllescha la casualitad (0 = deterministic, 2 = fitg creativ)", + "Conversation ID is required" : "L'ID da conversaziun è obligatoric", + "Conversation archived" : "La conversaziun è archivada", + "Conversation archived successfully" : "La conversaziun è vegnida archivada cun success", + "Conversation cleared successfully" : "La conversaziun è vegnida stizzada cun success", + "Conversation deleted" : "La conversaziun è stizzada", + "Conversation permanently deleted" : "La conversaziun è stizzada permanentamain", + "Conversation renamed" : "La conversaziun è renumnada", + "Conversation restored" : "La conversaziun è restituida", + "Conversation title" : "Titel da la conversaziun", + "Copied!" : "Copià!", + "Copy" : "Copiar", + "Copy Changes" : "Copiar las midadas", + "Copy Collection" : "Copiar la collecziun", + "Copy Data" : "Copiar las datas", + "Copy Full Data" : "Copiar tut las datas", + "Copying..." : "Copiar...", + "Count" : "Dumber", + "Counting objects..." : "Dumbrar ils objects...", + "Costs" : "Custs", + "Create" : "Crear", + "Create Collection" : "Crear collecziun", + "Create ConfigSet" : "Crear ConfigSet", + "Create New Collection" : "Crear nova collecziun", + "Create New ConfigSet" : "Crear nov ConfigSet", + "Create Operations" : "Operaziuns da crear", + "Create Webhook" : "Crear webhook", + "Create a copy of collection:" : "Crear ina copia da la collecziun:", + "Create a new ConfigSet based on the _default template" : "Crear in nov ConfigSet a basa dal model _default", + "Create a new SOLR collection from an existing ConfigSet" : "Crear ina nova collecziun SOLR ord in ConfigSet existent", + "Create your first AI agent to get started." : "Creai Voss emprim agent IA per cumenzar.", + "Created" : "Creà", + "Created:" : "Creà:", + "Creating..." : "Crear...", + "Current Filters" : "Filters actuals", + "Current Type:" : "Tip actual:", + "Custom API endpoint if using a different region" : "Endpoint API persunalisà sche Vus duvrais ina autra regiun", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Headers HTTP persunalisads (in per lingia, format: Header-Name: value)", + "Daily" : "Mintga di", + "Dashboard" : "Panel", + "Data Source" : "Funtauna da datas", + "Data sources" : "Funtaunas da datas", + "Data type does not match observed values" : "Il tip da datas na correspunda betg als valurs observads", + "Database URL" : "URL da la banca da datas", + "Database information refreshed" : "Las infurmaziuns da la banca da datas èn vegnidas actualisadas", + "Date Range" : "Perioda da data", + "Default" : "Standard", + "Default Organisation" : "Organisaziun standard", + "Default: 5" : "Standard: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Las retardadas dobleschan cun mintga emprova (2, 4, 8 minutas...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Las retardadas creschan linearmain (5, 10, 15 minutas...)", + "Delete" : "Stizzar", + "Delete Application" : "Stizzar l'applicaziun", + "Delete Audit Trail" : "Stizzar la colliaziun d'audit", + "Delete Collection" : "Stizzar la collecziun", + "Delete ConfigSet" : "Stizzar il ConfigSet", + "Delete Objects" : "Stizzar ils objects", + "Delete Operations" : "Operaziuns da stizzar", + "Delete View" : "Stizzar la vista", + "Delete all objects in this schema" : "Stizzar tut ils objects en quest Schema", + "Delete permanently" : "Stizzar permanentamain", + "Delete this conversation?" : "Stizzar questa conversaziun?", + "Delete view" : "Stizzar la vista", + "Deleted" : "Stizzà", + "Deleted By" : "Stizzà da", + "Deleted Date" : "Data da stizzada", + "Deleted Items Management" : "Administraziun dals elements stizzads", + "Deleted This Week" : "Stizzà questa emna", + "Deleted Today" : "Stizzà oz", + "Deleted:" : "Stizzà:", + "Deleting..." : "Stizzar...", + "Deletion Date Range" : "Perioda da data da stizzada", + "Deletion Statistics" : "Statistica da stizzada", + "Deprecated" : "Antiquà", + "Description" : "Descripziun", + "Description:" : "Descripziun:", + "Detect data types and patterns" : "Identifitgar tips da datas e models", + "Detected At" : "Identifitgà la", + "Detected Format:" : "Format identifitgà:", + "Detected Issues:" : "Problems identifitgads:", + "Disable" : "Deactivar", + "Disabled" : "Deactivà", + "Discover Files" : "Scuvrir las Datotecas", + "Discovered Properties" : "Caracteristicas scuvertas", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Vulais Vus stizzar permanentamain tut las endataziuns da colliaziuns d'audit filtradas? Questa acziun na po betg vegnir revocada.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Vulais Vus stizzar permanentamain questa endataziun da colliaziun d'audit? Questa acziun na po betg vegnir revocada.", + "Documents" : "Documents", + "Download API Spec" : "Telechargiar la specificaziun API", + "Edit" : "Modifitgar", + "Edit Register" : "Modifitgar il Register", + "Edit Schema" : "Modifitgar il Schema", + "Edit View" : "Modifitgar la vista", + "Edit Webhook" : "Modifitgar il webhook", + "Edit view" : "Modifitgar la vista", + "Edit view details" : "Modifitgar ils detagls da la vista", + "Effectiveness" : "Effectivitad", + "Email" : "E-mail", + "Emails" : "E-mails", + "Embedding Model" : "Model d'embedding", + "Embedding Provider" : "Purschider d'embedding", + "Embedding generated successfully" : "L'embedding è vegnì generà cun success", + "Embedding provider connection successful!" : "La colliaziun cun il purschider d'embedding è reussida!", + "Enable" : "Activar", + "Enable auto-creation" : "Activar la creaziun automatica", + "Enable automatic file vectorization" : "Activar la vectorisaziun automatica da Datotecas", + "Enable automatic object vectorization" : "Activar la vectorisaziun automatica d'objects", + "Enable automatic synchronization" : "Activar la sincronisaziun automatica", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Activar il protocollar detaglià per las operaziuns SOLR (recumandà per il debugging)", + "Enable faceting" : "Activar las facettas", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Activar u deactivar las funcziuns LLM. Configurai ils purschiders e models cun il buttun da configuraziun LLM survart.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Activar u deactivar l'integraziun da tschertga SOLR. Configurai las configuraziuns da la colliaziun cun il buttun da configuraziuns da la colliaziun survart.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Activar u deactivar l'integraziun dal process da lavur n8n. Configurai las configuraziuns da la colliaziun sutvart.", + "Enable or disable this webhook" : "Activar u deactivar quest webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Activar la vectorisaziun per tut las vistas existentas e futuras (po augmentar ils custs)", + "Enabled" : "Activà", + "Endpoints" : "Endpoints", + "Enter ConfigSet name" : "Endatar il num dal ConfigSet", + "Enter collection name" : "Endatar il num da la collecziun", + "Enter description (optional)..." : "Endatar la descripziun (opziunal)...", + "Enter new collection name" : "Endatar il nov num da la collecziun", + "Enter object ID" : "Endatar l'ID da l'object", + "Enter search term" : "Endatar in term da tschertga", + "Enter view name..." : "Endatar il num da la vista...", + "Enter webhook name" : "Endatar il num dal webhook", + "Entities" : "Entitads", + "Entity Information" : "Infurmaziuns da l'entitad", + "Entity deleted successfully" : "L'entitad è vegnida stizzada cun success", + "Entity not found" : "L'entitad n'è betg vegnida chattada", + "Entries to be deleted:" : "Endataziuns che vegnan stizzadas:", + "Enum Issue" : "Problem d'enum", + "Enum constraint is missing" : "La restricziun d'enum manca", + "Error" : "Errur", + "Error Details" : "Detagls da l'errur", + "Error Information" : "Infurmaziuns da l'errur", + "Error Message" : "Messadi d'errur", + "Error loading application" : "Errur durant il chargiar da l'applicaziun", + "Error loading audit trails" : "Errur durant il chargiar da las colliaziuns d'audit", + "Error loading entity" : "Errur durant il chargiar da l'entitad", + "Error loading search trails" : "Errur durant il chargiar da las colliaziuns da tschertga", + "Estimated Batches:" : "Batches estimads:", + "Estimated Cost:" : "Custs estimads:", + "Estimated Duration:" : "Durada estimada:", + "Event" : "Eveniment", + "Event Property for Payload" : "Caracteristica d'eveniment per il payload", + "Events" : "Eveniments", + "Example Value" : "Valur d'exempel", + "Exclusive Maximum" : "Maximum exclusiv", + "Exclusive Minimum" : "Minimum exclusiv", + "Execution Mode" : "Modus d'execuziun", + "Execution Time" : "Temp d'execuziun", + "Execution Time Range" : "Perioda dal temp d'execuziun", + "Existing Improvements" : "Meglieraziuns existentas", + "Exponential" : "Exponenzial", + "Export" : "Exportar", + "Export completed successfully" : "L'export è vegnì cumpletà cun success", + "Export, view, or delete audit trails" : "Exportar, vesair u stizzar las colliaziuns d'audit", + "Extend Schema" : "Extender il Schema", + "Extended" : "Extendì", + "Extensions" : "Extensiuns", + "Extract Pending Files" : "Extrair las Datotecas pendentas", + "Extract properties from each object" : "Extrair las caracteristicas da mintga object", + "Extracted At" : "Extratg la", + "Extraction Status" : "Status da l'extracziun", + "Facet configuration updated successfully" : "La configuraziun da las facettas è vegnida actualisada cun success", + "Facets discovered and configured successfully" : "Las facettas èn vegnidas scuvertas e configuradas cun success", + "Facets discovered successfully" : "Las facettas èn vegnidas scuvertas cun success", + "Failed" : "Fallì", + "Failed to analyze schema properties" : "Betg reussì d'analisar las caracteristicas dal Schema", + "Failed to archive conversation" : "Betg reussì d'archivar la conversaziun", + "Failed to calculate sizes" : "Betg reussì da calcular las grondezzas", + "Failed to clear collection" : "Betg reussì da stizzar la collecziun", + "Failed to copy changes" : "Betg reussì da copiar las midadas", + "Failed to copy data" : "Betg reussì da copiar las datas", + "Failed to copy data to clipboard" : "Betg reussì da copiar las datas en l'archivet provisoric", + "Failed to create conversation" : "Betg reussì da crear la conversaziun", + "Failed to create or find project" : "Betg reussì da crear u chattar il project", + "Failed to delete collection" : "Betg reussì da stizzar la collecziun", + "Failed to delete conversation" : "Betg reussì da stizzar la conversaziun", + "Failed to delete webhook" : "Betg reussì da stizzar il webhook", + "Failed to download API specification" : "Betg reussì da telechargiar la specificaziun API", + "Failed to get SOLR field configuration" : "Betg reussì da retschaiver la configuraziun dals chomps SOLR", + "Failed to get entity categories" : "Betg reussì da retschaiver las categorias d'entitad", + "Failed to get entity statistics" : "Betg reussì da retschaiver la statistica d'entitad", + "Failed to get entity types" : "Betg reussì da retschaiver ils tips d'entitad", + "Failed to load LLM configuration" : "Betg reussì da chargiar la configuraziun LLM", + "Failed to load Nextcloud groups" : "Betg reussì da chargiar las gruppas da Nextcloud", + "Failed to load conversation" : "Betg reussì da chargiar la conversaziun", + "Failed to load entities" : "Betg reussì da chargiar las entitads", + "Failed to load entity" : "Betg reussì da chargiar l'entitad", + "Failed to load files" : "Betg reussì da chargiar las Datotecas", + "Failed to load organisations" : "Betg reussì da chargiar las organisaziuns", + "Failed to load templates" : "Betg reussì da chargiar ils models", + "Failed to load webhooks" : "Betg reussì da chargiar ils webhooks", + "Failed to load workflows" : "Betg reussì da chargiar ils process da lavur", + "Failed to refresh database information" : "Betg reussì d'actualisar las infurmaziuns da la banca da datas", + "Failed to reindex collection" : "Betg reussì da reindexar la collecziun", + "Failed to rename conversation" : "Betg reussì da renumnar la conversaziun", + "Failed to restore conversation" : "Betg reussì da restituir la conversaziun", + "Failed to retry extraction" : "Betg reussì da reemprovar l'extracziun", + "Failed to retry webhook" : "Betg reussì da reemprovar il webhook", + "Failed to save GitHub token" : "Betg reussì da memorisar il token da GitHub", + "Failed to save GitLab URL" : "Betg reussì da memorisar l'URL da GitLab", + "Failed to save GitLab token" : "Betg reussì da memorisar il token da GitLab", + "Failed to save additional feedback" : "Betg reussì da memorisar il feedback supplementar", + "Failed to save n8n configuration" : "Betg reussì da memorisar la configuraziun n8n", + "Failed to save roles" : "Betg reussì da memorisar ils rolls", + "Failed to save settings" : "Betg reussì da memorisar las configuraziuns", + "Failed to save webhook" : "Betg reussì da memorisar il webhook", + "Failed to send feedback" : "Betg reussì da trametter il feedback", + "Failed to test webhook" : "Betg reussì da testar il webhook", + "Failed to update favorite status" : "Betg reussì d'actualisar il status da favorit", + "Failed to update schema properties" : "Betg reussì d'actualisar las caracteristicas dal Schema", + "Failed to update view" : "Betg reussì d'actualisar la vista", + "Failed to update webhook" : "Betg reussì d'actualisar il webhook", + "Feedback recorded" : "Il feedback è registrà", + "Fewer sources (1-3):" : "Damain funtaunas (1-3):", + "Field" : "Chomp", + "File" : "Datoteca", + "File Chunk Prediction" : "Prognosa da chunks da Datoteca", + "File Collection" : "Collecziun da Datotecas", + "File Management" : "Administraziun da Datotecas", + "File Name" : "Num da la Datoteca", + "File Path" : "Percurs da la Datoteca", + "File Type Selection" : "Selecziun dal tip da Datoteca", + "File Vectorization" : "Vectorisaziun da Datotecas", + "File Warmup" : "Prechauffament da Datotecas", + "File actions menu" : "Menu d'acziuns da Datoteca", + "File anonymized successfully" : "La Datoteca è vegnida anonimisada cun success", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "La vectorisaziun da chunks da Datoteca n'è anc betg implementada. Ils chunks èn pronts e memorisads, ma il servetsch da vectorisaziun è en svilup.", + "File collection not configured" : "La collecziun da Datotecas n'è betg configurada", + "File discovery completed" : "La scuverta da Datotecas è cumpletada", + "File extraction completed" : "L'extracziun da la Datoteca è cumpletada", + "File extraction queued" : "L'extracziun da la Datoteca è en la lingia d'spetga", + "File indexed successfully" : "La Datoteca è vegnida indexada cun success", + "File is already anonymized" : "La Datoteca è gia anonimisada", + "File not found" : "La Datoteca n'è betg vegnida chattada", + "File settings updated successfully" : "Las configuraziuns da la Datoteca èn vegnidas actualisadas cun success", + "File sources" : "Funtaunas da Datotecas", + "File vectorization configuration saved successfully" : "La configuraziun da vectorisaziun da Datotecas è vegnida memorisada cun success", + "File vectorization started. Check the statistics section for progress." : "La vectorisaziun da Datotecas è cumenzada. Controllai la secziun da statistica per il progress.", + "File warmup completed" : "Il prechauffament da Datotecas è cumpletà", + "Files" : "Datotecas", + "Files with Completed Extraction:" : "Datotecas cun extracziun cumpletada:", + "Files → fileCollection, Objects → objectCollection" : "Datotecas → fileCollection, Objects → objectCollection", + "Filter Audit Trails" : "Filtrar las colliaziuns d'audit", + "Filter Deleted Items" : "Filtrar ils elements stizzads", + "Filter Objects" : "Filtrar ils objects", + "Filter Properties" : "Filtrar las caracteristicas", + "Filter Search Trails" : "Filtrar las colliaziuns da tschertga", + "Filter Statistics" : "Filtrar la statistica", + "Filter and analyze search trail entries" : "Filtrar ed analisar las endataziuns da colliaziuns da tschertga", + "Filter and manage audit trail entries" : "Filtrar ed administrar las endataziuns da colliaziuns d'audit", + "Filter and manage soft deleted items" : "Filtrar ed administrar ils elements stizzads mollamain", + "Filter by object ID" : "Filtrar tenor l'ID da l'object", + "Filter by search term" : "Filtrar tenor il term da tschertga", + "Filter by webhook" : "Filtrar tenor il webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Las datas da filter èn vegnidas chargiadas automaticamain. Duvrai ils filters sutvart per rafinar Vossa tschertga.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrar ils triggers da webhook tenor las caracteristicas dal payload (in per lingia, format: key: value)", + "Filtered" : "Filtrà", + "Filters" : "Filters", + "Fireworks AI Chat Settings" : "Configuraziuns da chat Fireworks AI", + "Fireworks AI Embedding Configuration" : "Configuraziun d'embedding Fireworks AI", + "First" : "Emprim", + "Fixed" : "Fixà", + "For chat and retrieval-augmented generation" : "Per chat e generaziun augmentada cun recuperaziun", + "For vector embeddings and semantic search" : "Per embeddings da vectur e tschertga semantica", + "Format" : "Format", + "Format Issue" : "Problem da format", + "Format constraint is missing" : "La restricziun da format manca", + "From Date" : "Da la data", + "From date" : "Da la data", + "Full data copied to clipboard" : "Tut las datas èn vegnidas copiadas en l'archivet provisoric", + "General Issue" : "Problem general", + "Generate recommendations and confidence scores" : "Generar recumandaziuns e punctuaziuns da confidenza", + "Generate vectors immediately when new objects are created" : "Generar immediatamain vecturs cur che novs objects vegnan creads", + "GitHub token is valid" : "Il token da GitHub è valaivel", + "GitHub token removed successfully" : "Il token da GitHub è vegnì allontanà cun success", + "GitHub token saved successfully" : "Il token da GitHub è vegnì memorisà cun success", + "GitLab URL saved successfully" : "L'URL da GitLab è vegnì memorisà cun success", + "GitLab token is valid" : "Il token da GitLab è valaivel", + "GitLab token saved successfully" : "Il token da GitLab è vegnì memorisà cun success", + "HTTP Method" : "Metoda HTTP", + "HTTP method used to send webhook requests" : "Metoda HTTP duvrada per trametter dumondas da webhook", + "Headers" : "Headers", + "Health" : "Sanadad", + "Heartbeat successful - connection kept alive" : "Heartbeat reussì - la colliaziun vegn mantegnida activa", + "Helpful" : "Util", + "Hide Filters" : "Zuppentar ils filters", + "Hide in forms" : "Zuppentar en ils formulars", + "Hide in list view" : "Zuppentar en la vista da glista", + "High" : "Aut", + "High Confidence" : "Auta confidenza", + "Host" : "Host", + "Hourly" : "Mintga ura", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Quant profund traversar las caracteristicas d'object inglubadas (1-20). Valurs pli auts chattan dapli detagls ma augmentan la grondezza dal vectur.", + "How often to check for updates (1-168 hours)" : "Quant savens controllar per actualisaziuns (1-168 uras)", + "How to handle retries for failed webhook deliveries" : "Co tractar las empruvas per furniziuns da webhook fallidas", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Las limitas da tariffa IP èn vegnidas stizzadas cun success", + "Identify properties not in the schema" : "Identifitgar las caracteristicas che n'èn betg en il Schema", + "Immutable" : "Immutabel", + "Import" : "Importar", + "Import successful" : "L'import è reussì", + "Improved Property" : "Caracteristica meglierada", + "In use" : "En diever", + "Inactive" : "Inactiv", + "Include IDs and names of related objects for better contextual search" : "Includer IDs e nums dals objects relaziunads per ina meglra tschertga contextuala", + "Include related object references" : "Includer las referenzas dals objects relaziunads", + "Include schema and register metadata" : "Includer las metadatas dal Schema e dal Register", + "Initialization failed" : "L'inizialisaziun è fallida", + "Initialize Project" : "Inizialisar il project", + "Inspect Fields" : "Inspecziunar ils chomps", + "Inspect Index" : "Inspecziunar l'index", + "Invalid" : "Nunvalaivel", + "Invalid batch size. Must be between 1 and 5000" : "Grondezza da batch nunvalaivla. Sto esser tranter 1 e 5000", + "Invalid field name provided" : "Num da chomp nunvalaivel furnì", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects nunvalaivel. Sto esser 0 (tut) u in dumber positiv", + "Invalid:" : "Nunvalaivel:", + "Issue" : "Problem", + "Items per page:" : "Elements per pagina:", + "Just now" : "Pir ussa", + "LLM Configuration" : "Configuraziun LLM", + "LLM actions menu" : "Menu d'acziuns LLM", + "LLM configuration saved successfully" : "La configuraziun LLM è vegnida memorisada cun success", + "LLM features disabled" : "Las funcziuns LLM èn deactivadas", + "LLM features enabled" : "Las funcziuns LLM èn activadas", + "LLM must be enabled with an embedding provider configured" : "LLM sto esser activà cun in purschider d'embedding configurà", + "LLM settings updated successfully" : "Las configuraziuns LLM èn vegnidas actualisadas cun success", + "Last" : "Ultim", + "Last Triggered" : "Davos declanschà", + "Length Range:" : "Perioda da lunghezza:", + "Linear" : "Linear", + "Load Advanced Filters" : "Chargiar ils filters avanzads", + "Load advanced filters with live data from your search index" : "Chargiar ils filters avanzads cun datas en direct da Voss index da tschertga", + "Load view" : "Chargiar la vista", + "Loading ConfigSets..." : "Chargiar ils ConfigSets...", + "Loading advanced filters..." : "Chargiar ils filters avanzads...", + "Loading agents..." : "Chargiar ils agents...", + "Loading application details..." : "Chargiar ils detagls da l'applicaziun...", + "Loading applications..." : "Chargiar las applicaziuns...", + "Loading archived conversations..." : "Chargiar las conversaziuns archivadas...", + "Loading audit trails..." : "Chargiar las colliaziuns d'audit...", + "Loading collections..." : "Chargiar las collecziuns...", + "Loading configurations..." : "Chargiar las configuraziuns...", + "Loading conversation..." : "Chargiar la conversaziun...", + "Loading conversations..." : "Chargiar las conversaziuns...", + "Loading deleted items..." : "Chargiar ils elements stizzads...", + "Loading events..." : "Chargiar ils eveniments...", + "Loading filter data..." : "Chargiar las datas da filter...", + "Loading groups..." : "Chargiar las gruppas...", + "Loading log details..." : "Chargiar ils detagls dal protocol...", + "Loading registers..." : "Chargiar ils Registers...", + "Loading schemas..." : "Chargiar ils Schemas...", + "Loading search trails..." : "Chargiar las colliaziuns da tschertga...", + "Loading sources..." : "Chargiar las funtaunas...", + "Loading statistics..." : "Chargiar la statistica...", + "Loading users..." : "Chargiar ils utilisaders...", + "Loading views..." : "Chargiar las vistas...", + "Loading..." : "Chargiar...", + "Local" : "Local", + "Local Version" : "Versiun locala", + "Locked" : "Bloccà", + "Locked:" : "Bloccà:", + "Login successful" : "L'annunzia è reussida", + "Logs" : "Protocols", + "Low" : "Bass", + "Low Confidence" : "Bassa confidenza", + "Lucene Version:" : "Versiun da Lucene:", + "Magic table synchronized successfully" : "La tabella magica è vegnida sincronisada cun success", + "Manage Organisation Roles" : "Administrar ils rolls da l'organisaziun", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Administrar las collecziuns SOLR (deposits da datas) ed attribuir las per objects e Datotecas.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Administrar ils ConfigSets SOLR (models da configuraziun) per Vossas collecziuns.", + "Manage and configure agents for automated tasks" : "Administrar e configurar agents per incumbensas automatisadas", + "Manage and monitor file text extraction status" : "Administrar e survegliar il status da l'extracziun da text da Datotecas", + "Manage and restore soft deleted items from your registers" : "Administrar e restituir ils elements stizzads mollamain da Voss Registers", + "Manage and view detected entities from files and objects" : "Administrar e vesair las entitads identifitgadas da Datotecas ed objects", + "Manage document templates and themes" : "Administrar ils models e thaibers da documents", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Administrar ils process da lavur n8n per l'automatisaziun da OpenRegister. Ils process da lavur vegnan memorisads en il project configurà.", + "Manage webhooks for event-driven integrations" : "Administrar ils webhooks per integraziuns manadas dad eveniments", + "Manage your applications and modules" : "Administrar Vossas applicaziuns e moduls", + "Manage your data registers and their configurations" : "Administrar Voss Registers da datas e lur configuraziuns", + "Manage your data schemas and their properties" : "Administrar Voss Schemas da datas e lur caracteristicas", + "Manage your data sources and their configurations" : "Administrar Vossas funtaunas da datas e lur configuraziuns", + "Manage your saved search configurations" : "Administrar Vossas configuraziuns da tschertga memorisadas", + "Manage your system configurations and settings" : "Administrar Vossas configuraziuns e parameters dal sistem", + "Managed" : "Administrà", + "Max Files (0 = all)" : "Max Datotecas (0 = tut)", + "Max Length" : "Lunghezza max", + "Max Length:" : "Lunghezza max:", + "Max Objects (0 = all)" : "Max objects (0 = tut)", + "Max Retries" : "Max empruvas", + "Max Shards/Node" : "Max Shards/Nodi", + "Max execution time (ms)" : "Temp d'execuziun max (ms)", + "Max ms" : "Max ms", + "Max result count" : "Dumber max da resultats", + "Max results" : "Resultats max", + "Maximum" : "Maximum", + "Maximum Nesting Depth" : "Profunditad maximala d'inglubaziun", + "Maximum length constraint is missing" : "La restricziun da lunghezza maximala manca", + "Maximum length is too restrictive" : "La lunghezza maximala è memia restrictiva", + "Maximum number of files to process. Set to 0 to process all files." : "Dumber maximal da Datotecas dad elavurar. Mettai sin 0 per elavurar tut las Datotecas.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Dumber maximal d'objects dad elavurar. Mettai sin 0 per elavurar tut ils objects.", + "Maximum number of retry attempts for failed deliveries" : "Dumber maximal d'empruvas per furniziuns fallidas", + "Maximum time to wait before committing changes" : "Temp maximal dad spetgar avant da cummetter las midadas", + "Maximum tokens to generate" : "Dumber maximal da tokens da generar", + "Maximum value constraint is missing" : "La restricziun da valur maximala manca", + "Maximum value is too restrictive" : "La valur maximala è memia restrictiva", + "Medium" : "Mesaun", + "Medium Confidence" : "Confidenza mesauna", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Dumondas mesaunas: tschertgas cun in tschert filtrar u plirs parameters (p.ex. periodas da data, Registers/Schemas specifics)", + "Members of selected groups can access this view" : "Ils commembers da las gruppas tschernidas pon acceder a questa vista", + "Members:" : "Commembers:", + "Memory prediction calculated successfully" : "La prognosa da memoria è vegnida calculada cun success", + "Message content is required" : "Il cuntegn dal messadi è obligatoric", + "Message does not belong to this conversation" : "Il messadi n'appartegna betg a questa conversaziun", + "Metadata Filters" : "Filters da metadatas", + "Method" : "Metoda", + "Min Length" : "Lunghezza min", + "Min execution time (ms)" : "Temp d'execuziun min (ms)", + "Min ms" : "Min ms", + "Min result count" : "Dumber min da resultats", + "Min results" : "Resultats min", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "La restricziun da valur minimala manca", + "Minimum value is too restrictive" : "La valur minimala è memia restrictiva", + "Missing object fields created successfully" : "Ils chomps d'object mancants èn vegnids creads cun success", + "Mode:" : "Modus:", + "Model" : "Model", + "Model Name" : "Num dal model", + "Modified" : "Modifitgà", + "Monthly" : "Mintga mais", + "More sources (10-20):" : "Dapli funtaunas (10-20):", + "Most Active Objects" : "Ils objects ils pli activs", + "Multiple Of" : "Multipel da", + "Name" : "Num", + "Names cache warmup completed" : "Il prechauffament dal cache da nums è cumpletà", + "New Collection Name" : "Num da la nova collecziun", + "New Conversation" : "Nova conversaziun", + "New Properties" : "Novas caracteristicas", + "New Property" : "Nova caracteristica", + "New Value" : "Nova valur", + "New users without specific organisation membership will be automatically added to this organisation" : "Novs utilisaders senza commembranza specifica d'organisaziun vegnan agiuntads automaticamain a questa organisaziun", + "Next" : "Proxim", + "No" : "Na", + "No ConfigSets found" : "Nagins ConfigSets chattads", + "No GitHub token configured" : "Nagin token da GitHub configurà", + "No GitHub token provided" : "Nagin token da GitHub furnì", + "No GitLab token provided" : "Nagin token da GitLab furnì", + "No Organisation" : "Nagina organisaziun", + "No active filters" : "Nagins filters activs", + "No activity data available" : "Naginas datas d'activitad disponiblas", + "No agents available" : "Nagins agents disponibels", + "No agents found" : "Nagins agents chattads", + "No app store cache exists yet" : "Anc nagin cache da l'App Store exista", + "No applications are available." : "Naginas applicaziuns èn disponiblas.", + "No applications found" : "Naginas applicaziuns chattadas", + "No archived conversations" : "Naginas conversaziuns archivadas", + "No audit trail entries found" : "Naginas endataziuns da colliaziuns d'audit chattadas", + "No changes recorded" : "Naginas midadas registradas", + "No chunks to vectorize" : "Nagins chunks da vectorisar", + "No collections found" : "Naginas collecziuns chattadas", + "No configuration data" : "Naginas datas da configuraziun", + "No configurations are available." : "Naginas configuraziuns èn disponiblas.", + "No configurations found" : "Naginas configuraziuns chattadas", + "No configurations found for this application." : "Naginas configuraziuns chattadas per questa applicaziun.", + "No conversations yet. Create a new one to get started!" : "Anc naginas conversaziuns. Creai ina nova per cumenzar!", + "No data available" : "Naginas datas disponiblas", + "No data available for chart" : "Naginas datas disponiblas per il diagram", + "No deleted items found" : "Nagins elements stizzads chattads", + "No deletion data available" : "Naginas datas da stizzada disponiblas", + "No description found" : "Nagina descripziun chattada", + "No description provided" : "Nagina descripziun furnida", + "No entities detected in this file. Run text extraction first." : "Naginas entitads identifitgadas en questa Datoteca. Exequì l'extracziun da text avant.", + "No entities found" : "Naginas entitads chattadas", + "No entities have been detected yet" : "Anc naginas entitads èn vegnidas identifitgadas", + "No events found" : "Nagins eveniments chattads", + "No expired audit trails found to clear" : "Naginas colliaziuns d'audit scadidas chattadas da stizzar", + "No facetable fields available. Select a register and schema to see available filters." : "Nagins chomps facettabels disponibels. Tscherni in Register ed in Schema per vesair ils filters disponibels.", + "No files found" : "Naginas Datotecas chattadas", + "No files have been extracted yet" : "Anc naginas Datotecas èn vegnidas extratgas", + "No files to process" : "Naginas Datotecas dad elavurar", + "No files to reindex" : "Naginas Datotecas da reindexar", + "No filters are currently active. This will delete ALL audit trail entries!" : "Actualmain n'èn nagins filters activs. Quai stizza TUT las endataziuns da colliaziuns d'audit!", + "No log entries found" : "Naginas endataziuns da protocol chattadas", + "No logs are available for this configuration." : "Nagins protocols èn disponibels per questa configuraziun.", + "No logs are available for this source." : "Nagins protocols èn disponibels per questa funtauna.", + "No logs found" : "Nagins protocols chattads", + "No mismatched fields found - SOLR schema is properly configured" : "Nagins chomps betg correspundents chattads - il Schema SOLR è configurà correctamain", + "No objects found" : "Nagins objects chattads", + "No objects found to vectorize" : "Nagins objects chattads da vectorisar", + "No objects selected for deletion" : "Nagins objects tschernids per la stizzada", + "No objects to delete" : "Nagins objects da stizzar", + "No objects were permanently deleted" : "Nagins objects èn vegnids stizzads permanentamain", + "No properties found" : "Naginas caracteristicas chattadas", + "No properties match your filters." : "Naginas caracteristicas correspundan a Voss filters.", + "No purge date set" : "Nagina data da purgar fixada", + "No register data available" : "Naginas datas da Register disponiblas", + "No registers found" : "Nagins Registers chattads", + "No registers found for this application." : "Nagins Registers chattads per questa applicaziun.", + "No relations found" : "Naginas relaziuns chattadas", + "No request body available" : "Nagin corp da dumonda disponibel", + "No response body available" : "Nagin corp da resposta disponibel", + "No saved views yet. Create one in the Search tab!" : "Anc naginas vistas memorisadas. Creai ina en il tab da tschertga!", + "No schema data available" : "Naginas datas da Schema disponiblas", + "No schema selected for exploration" : "Nagin Schema tschernì per l'exploraziun", + "No schemas found" : "Nagins Schemas chattads", + "No schemas found for this application." : "Nagins Schemas chattads per questa applicaziun.", + "No search terms data available" : "Naginas datas da terms da tschertga disponiblas", + "No search trail entries found" : "Naginas endataziuns da colliaziuns da tschertga chattadas", + "No sources are available." : "Naginas funtaunas èn disponiblas.", + "No sources found" : "Naginas funtaunas chattadas", + "No templates found" : "Nagins models chattads", + "No templates have been created yet" : "Anc nagins models èn vegnids creads", + "No valid magic-mapped register+schema combinations found" : "Naginas cumbinaziuns valaivlas Register+Schema magic-mapped chattadas", + "No views found. Create views first before configuring vectorization." : "Naginas vistas chattadas. Creai vistas avant da configurar la vectorisaziun.", + "No views match your search" : "Naginas vistas correspundan a Vossa tschertga", + "No webhooks found" : "Nagins webhooks chattads", + "No webhooks have been configured yet" : "Anc nagins webhooks èn vegnids configurads", + "No workflows found in this project. Create workflows in the n8n editor." : "Nagins process da lavur chattads en quest project. Creai process da lavur en l'editur n8n.", + "None" : "Nagin", + "Not helpful" : "Betg util", + "Not in use" : "Betg en diever", + "Number Constraints" : "Restricziuns da dumber", + "Number of chunks to vectorize in one API call (1-100)." : "Dumber da chunks da vectorisar en ina clamada API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Dumber da chunks da vectorisar en ina clamada API. Pli aut = pli svelt ma dapli memoria. Recumandà: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Dumber d'objects dad elavurar en mintga batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Dumber d'objects da vectorisar en ina clamada API. Pli aut = pli svelt ma dapli memoria. Recumandà: 10-50.", + "Numeric Range:" : "Perioda numerica:", + "Object" : "Object", + "Object Collection" : "Collecziun d'objects", + "Object Count Prediction" : "Prognosa dal dumber d'objects", + "Object ID" : "ID da l'object", + "Object Management" : "Administraziun dals objects", + "Object Vectorization" : "Vectorisaziun d'objects", + "Object Warmup" : "Prechauffament d'objects", + "Object collection not configured" : "La collecziun d'objects n'è betg configurada", + "Object not found" : "L'object n'è betg vegnì chattà", + "Object permanently deleted" : "L'object è vegnì stizzà permanentamain", + "Object restored successfully" : "L'object è vegnì restituì cun success", + "Object settings updated successfully" : "Las configuraziuns da l'object èn vegnidas actualisadas cun success", + "Object sources" : "Funtaunas d'objects", + "Object unlocked successfully" : "L'object è vegnì debloccà cun success", + "Object vectorization configuration saved successfully" : "La configuraziun da vectorisaziun d'objects è vegnida memorisada cun success", + "Object vectorized successfully" : "L'object è vegnì vectorisà cun success", + "Object:" : "Object:", + "Objects" : "Objects", + "Objects Analyzed" : "Objects analisads", + "Objects Distribution" : "Distribuziun dals objects", + "Objects being analyzed" : "Objects che vegnan analisads", + "Objects by Register" : "Objects tenor Register", + "Objects by Schema" : "Objects tenor Schema", + "Objects deletion completed successfully" : "La stizzada dals objects è vegnida cumpletada cun success", + "Objects in schema" : "Objects en il Schema", + "Objects to Process:" : "Objects dad elavurar:", + "Objects to be analyzed" : "Objects che vegnan analisads", + "Objects to be deleted" : "Objects che vegnan stizzads", + "Objects to be validated" : "Objects che vegnan validads", + "Objects will be serialized as JSON text before vectorization" : "Ils objects vegnan serialisads sco text JSON avant la vectorisaziun", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Ils objects vegnan stizzads mollamain (marcads sco stizzads ma mantegnids en la banca da datas). Els pon vegnir recuperads pli tard sche necessari.", + "Old Value" : "Veglia valur", + "Oldest Item (days)" : "L'element il pli vegl (dis)", + "Ollama Chat Settings" : "Configuraziuns da chat Ollama", + "Ollama Embedding Configuration" : "Configuraziun d'embedding Ollama", + "Ollama URL" : "URL da Ollama", + "Only With Changes" : "Mo cun midadas", + "Open Register Objects" : "Avrir ils objects da Register", + "Open n8n Editor" : "Avrir l'editur n8n", + "OpenAI Chat Settings" : "Configuraziuns da chat OpenAI", + "OpenAI Embedding Configuration" : "Configuraziun d'embedding OpenAI", + "OpenRegister Settings" : "Configuraziuns da OpenRegister", + "Optional webhook secret for signature verification" : "Secret da webhook opziunal per la verificaziun da la signatura", + "Order" : "Urden", + "Organisation" : "Organisaziun", + "Organisation ID" : "ID da l'organisaziun", + "Organisation Members" : "Commembers da l'organisaziun", + "Organisation Statistics" : "Statistica da l'organisaziun", + "Organisation created successfully" : "L'organisaziun è vegnida creada cun success", + "Organisation settings saved successfully" : "Las configuraziuns da l'organisaziun èn vegnidas memorisadas cun success", + "Organisations" : "Organisaziuns", + "Organization" : "Organisaziun", + "Organization ID (Optional)" : "ID d'organisaziun (opziunal)", + "Orphaned Items" : "Elements orfens", + "Owner" : "Possessur", + "Owner:" : "Possessur:", + "Parallel Mode (Faster, more resource intensive)" : "Modus parallel (pli svelt, dapli intensiv en resursas)", + "Parallel:" : "Parallel:", + "Password" : "Pled-clav", + "Password for SOLR authentication (optional)" : "Pled-clav per l'autentificaziun SOLR (opziunal)", + "Path" : "Percurs", + "Path in repository" : "Percurs en il repositori", + "Path where the configuration file will be saved in the repository" : "Percurs nua che la Datoteca da configuraziun vegn memorisada en il repositori", + "Path where the register OAS file will be saved in the repository" : "Percurs nua che la Datoteca OAS dal Register vegn memorisada en il repositori", + "Pattern (regex)" : "Model (regex)", + "Pattern Issue" : "Problem da model", + "Pattern constraint is missing" : "La restricziun da model manca", + "Patterns:" : "Models:", + "Pending" : "Pendent", + "Permanent Delete Mode" : "Modus da stizzar permanent", + "Permanently Delete" : "Stizzar permanentamain", + "Permanently delete already soft-deleted objects" : "Stizzar permanentamain ils objects gia stizzads mollamain", + "Permanently delete this conversation?" : "Stizzar permanentamain questa conversaziun?", + "Person" : "Persuna", + "Personal Data" : "Datas persunalas", + "Phone" : "Telefon", + "Photos" : "Fotografias", + "Please create an agent in the" : "Creai per plaschair in agent en il", + "Please select an agent to continue" : "Tscherni per plaschair in agent per cuntinuar", + "Please select which register and schema to use for the new object" : "Tscherni per plaschair qual Register e Schema duvrar per il nov object", + "Please try again later." : "Empruvai per plaschair pli tard danovamain.", + "Please wait while we fetch your agents." : "Spetgai per plaschair entant che nus retschavain Voss agents.", + "Please wait while we fetch your applications." : "Spetgai per plaschair entant che nus retschavain Vossas applicaziuns.", + "Please wait while we fetch your configurations." : "Spetgai per plaschair entant che nus retschavain Vossas configuraziuns.", + "Please wait while we fetch your deleted items." : "Spetgai per plaschair entant che nus retschavain Voss elements stizzads.", + "Please wait while we fetch your sources." : "Spetgai per plaschair entant che nus retschavain Vossas funtaunas.", + "Polls" : "Sondaggis", + "Popular Search Terms" : "Terms da tschertga populars", + "Port" : "Port", + "Prerequisites" : "Premissas", + "Press Enter to send, Shift+Enter for new line" : "Smatgai Enter per trametter, Shift+Enter per ina nova lingia", + "Previous" : "Precedent", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Elavurescha ils chunks en batches cun parallelissem simulà.", + "Processes file chunks sequentially (safest)." : "Elavurescha ils chunks da Datoteca sequenzialmain (il pli segir).", + "Processes objects in chunks with simulated parallelism." : "Elavurescha ils objects en chunks cun parallelissem simulà.", + "Processes objects sequentially (safest)." : "Elavurescha ils objects sequenzialmain (il pli segir).", + "Processing" : "Elavuraziun", + "Processing Limits" : "Limitas d'elavuraziun", + "Processing..." : "Elavurar...", + "Project initialized successfully" : "Il project è vegnì inizialisà cun success", + "Project not found. Please initialize first." : "Il project n'è betg vegnì chattà. Inizialisai per plaschair avant.", + "Properties" : "Caracteristicas", + "Property Behaviors" : "Cumportaments da la caracteristica", + "Property Configuration:" : "Configuraziun da la caracteristica:", + "Property Filters" : "Filters da caracteristicas", + "Property Title" : "Titel da la caracteristica", + "Property Type" : "Tip da caracteristica", + "Property can be improved" : "La caracteristica po vegnir meglierada", + "Protected" : "Protegì", + "Provider" : "Purschider", + "Provider is required for testing" : "Il purschider è obligatoric per testar", + "Public" : "Public", + "Public View" : "Vista publica", + "Public views can be accessed by anyone in the system" : "Las vistas publicas pon vegnir accedidas da scadin en il sistem", + "Publish" : "Publitgar", + "Publish Objects" : "Publitgar ils objects", + "Publish all objects in this schema" : "Publitgar tut ils objects en quest Schema", + "Published" : "Publitgà", + "Published:" : "Publitgà:", + "Publishing..." : "Publitgar...", + "Purge" : "Purgar", + "Purge Date" : "Data da purgar", + "Query Complexity Distribution" : "Distribuziun da la cumplexitad da las dumondas", + "Query parameter is required" : "Il parameter da dumonda è obligatoric", + "RAG" : "RAG", + "RAG Configuration" : "Configuraziun RAG", + "Rate limits cleared successfully" : "Las limitas da tariffa èn vegnidas stizzadas cun success", + "Raw Changes Data" : "Datas da midadas brutas", + "Re-vectorize on object update" : "Revectorisar cur ch'in object vegn actualisà", + "Read" : "Leger", + "Recommendations:" : "Recumandaziuns:", + "Recommended Type:" : "Tip recumandà:", + "Recommended: 5 sources" : "Recumandà: 5 funtaunas", + "Refresh" : "Actualisar", + "Refresh Data" : "Actualisar las datas", + "Refresh Stats" : "Actualisar la statistica", + "Refresh Workflows" : "Actualisar ils process da lavur", + "Refresh dashboard" : "Actualisar il panel", + "Refresh database info" : "Actualisar l'infurmaziun da la banca da datas", + "Register" : "Register", + "Register ID" : "ID dal Register", + "Register Statistics" : "Statistica dal Register", + "Register Totals" : "Totals dal Register", + "Register depublished successfully" : "Il Register è vegnì depublitgà cun success", + "Register not found" : "Il Register n'è betg vegnì chattà", + "Register objects deletion completed successfully" : "La stizzada dals objects dal Register è vegnida cumpletada cun success", + "Register published successfully" : "Il Register è vegnì publitgà cun success", + "Register/Schema Usage" : "Diever da Register/Schema", + "Registers" : "Registers", + "Reindex" : "Reindexar", + "Reindex completed" : "Il reindexar è cumpletà", + "Reindex completed successfully" : "Il reindexar è vegnì cumpletà cun success", + "Reindex failed" : "Il reindexar è fallì", + "Relations" : "Relaziuns", + "Remote Version" : "Versiun a distanza", + "Remove" : "Allontanar", + "Remove filter" : "Allontanar il filter", + "Remove from favorites" : "Allontanar dals favorits", + "Remove group" : "Allontanar la gruppa", + "Removed" : "Allontanà", + "Removed from favorites" : "Allontanà dals favorits", + "Rename Conversation" : "Renumnar la conversaziun", + "Rename conversation" : "Renumnar la conversaziun", + "Replicas" : "Replicas", + "Repository" : "Repositori", + "Request Body" : "Corp da la dumonda", + "Request Data" : "Datas da la dumonda", + "Request Details" : "Detagls da la dumonda", + "Request body must be JSON with a \"query\" field" : "O corpo do pedido tem de ser JSON com um campo \"query\"", + "Request timeout in seconds" : "Timeout da la dumonda en secundas", + "Required field" : "Chomp obligatoric", + "Required status is inconsistent" : "Il status obligatoric è incoerent", + "Rerun Search" : "Reexequir la tschertga", + "Reset Changes" : "Reverter las midadas", + "Reset Filters" : "Reverter ils filters", + "Response Body" : "Corp da la resposta", + "Response Details" : "Detagls da la resposta", + "Response Time:" : "Temp da resposta:", + "Restore" : "Restituir", + "Restore conversation" : "Restituir la conversaziun", + "Restore or permanently delete items" : "Restituir u stizzar permanentamain ils elements", + "Result Count Range" : "Perioda dal dumber da resultats", + "Results" : "Resultats", + "Retrieve all objects for this schema" : "Recuperar tut ils objects per quest Schema", + "Retry" : "Reemprovar", + "Retry Failed Extractions" : "Reemprovar las extracziuns fallidas", + "Retry Policy" : "Reglament d'empruvas", + "Retry completed" : "L'emprova è cumpletada", + "Retrying..." : "Reemprovar...", + "Risk Level" : "Nivel da ristg", + "Roles updated successfully" : "Ils rolls èn vegnids actualisads cun success", + "SOLR Connection Settings" : "Configuraziuns da la colliaziun SOLR", + "SOLR Version:" : "Versiun da SOLR:", + "SOLR actions menu" : "Menu d'acziuns SOLR", + "SOLR base path (usually /solr)" : "Percurs da basa SOLR (savens /solr)", + "SOLR is disabled" : "SOLR è deactivà", + "SOLR is not available or not configured" : "SOLR n'è betg disponibel u betg configurà", + "SOLR logging disabled" : "Il protocollar SOLR è deactivà", + "SOLR logging enabled" : "Il protocollar SOLR è activà", + "SOLR search disabled" : "La tschertga SOLR è deactivada", + "SOLR search enabled" : "La tschertga SOLR è activada", + "SOLR server hostname or IP address" : "Hostname dal server SOLR u adressa IP", + "SOLR server port number (optional, defaults to 8983)" : "Dumber da port dal server SOLR (opziunal, standard 8983)", + "SOLR setup completed successfully" : "La configuraziun SOLR è vegnida cumpletada cun success", + "SOLR setup failed" : "La configuraziun SOLR è fallida", + "SOLR setup failed - check logs" : "La configuraziun SOLR è fallida - controllai ils protocols", + "Sample Values:" : "Valurs d'exempel:", + "Save" : "Memorisar", + "Save Configuration" : "Memorisar la configuraziun", + "Save Connection Settings" : "Memorisar las configuraziuns da la colliaziun", + "Save Roles" : "Memorisar ils rolls", + "Save Settings" : "Memorisar las configuraziuns", + "Save current search as view" : "Memorisar la tschertga actuala sco vista", + "Saved Views" : "Vistas memorisadas", + "Saving..." : "Memorisar...", + "Schema" : "Schema", + "Schema ID" : "ID dal Schema", + "Schema Name" : "Num dal Schema", + "Schema Statistics" : "Statistica dal Schema", + "Schema depublished successfully" : "Il Schema è vegnì depublitgà cun success", + "Schema objects deletion completed successfully" : "La stizzada dals objects dal Schema è vegnida cumpletada cun success", + "Schema published successfully" : "Il Schema è vegnì publitgà cun success", + "Schema successfully created" : "Il Schema è vegnì creà cun success", + "Schema successfully updated" : "Il Schema è vegnì actualisà cun success", + "Schemas" : "Schemas", + "Scheme" : "Schema", + "Search / Views" : "Tschertga / Vistas", + "Search Activity" : "Activitad da tschertga", + "Search Analytics" : "Analisa da tschertga", + "Search Entities" : "Tschertgar entitads", + "Search Files" : "Tschertgar Datotecas", + "Search GitHub" : "Tschertgar en GitHub", + "Search GitLab" : "Tschertgar en GitLab", + "Search Method" : "Metoda da tschertga", + "Search Term" : "Term da tschertga", + "Search Trail Details" : "Detagls da la colliaziun da tschertga", + "Search Trail Management" : "Administraziun da las colliaziuns da tschertga", + "Search Trail Statistics" : "Statistica da las colliaziuns da tschertga", + "Search Trails" : "Colliaziuns da tschertga", + "Search Views" : "Tschertgar vistas", + "Search Webhooks" : "Tschertgar webhooks", + "Search by file name or path" : "Tschertgar tenor il num u il percurs da la Datoteca", + "Search by name or URL" : "Tschertgar tenor num u URL", + "Search by value" : "Tschertgar tenor valur", + "Search in Files" : "Tschertgar en Datotecas", + "Search in Objects" : "Tschertgar en objects", + "Search property names..." : "Tschertgar nums da caracteristicas...", + "Search trail deletion not implemented yet" : "Il stizzar da colliaziuns da tschertga n'è anc betg implementà", + "Search views..." : "Tschertgar vistas...", + "Secret" : "Secret", + "Secret key for HMAC signature generation (optional)" : "Clav secreta per la generaziun da la signatura HMAC (opziunal)", + "Select AI Agent" : "Tscherner l'agent IA", + "Select All" : "Tscherner tut", + "Select ConfigSet" : "Tscherner il ConfigSet", + "Select File Types to Vectorize:" : "Tscherner ils tips da Datoteca da vectorisar:", + "Select HTTP method" : "Tscherner la metoda HTTP", + "Select Register and Schema" : "Tscherner il Register ed il Schema", + "Select Views to Vectorize:" : "Tscherner las vistas da vectorisar:", + "Select a Nextcloud group to add" : "Tscherner ina gruppa da Nextcloud per agiuntar", + "Select a branch" : "Tscherner in branch", + "Select a model or type a custom model name" : "Tscherner in model u endatar in num da model persunalisà", + "Select a repository" : "Tscherner in repositori", + "Select a repository you have write access to" : "Tscherner in repositori sin il qual Vus avais access da scriver", + "Select an AI Agent" : "Tscherner in agent IA", + "Select an AI agent to begin chatting with your data." : "Tscherner in agent IA per cumenzar a chattar cun Vossas datas.", + "Select backend" : "Tscherner il backend", + "Select chat model" : "Tscherner il model da chat", + "Select collection for files" : "Tscherner la collecziun per las Datotecas", + "Select collection for objects" : "Tscherner la collecziun per ils objects", + "Select default organisation" : "Tscherner l'organisaziun standard", + "Select event to listen to..." : "Tscherner l'eveniment dad ascutar...", + "Select model" : "Tscherner il model", + "Select options..." : "Tscherner las opziuns...", + "Select period" : "Tscherner la perioda", + "Select property to send as payload" : "Tscherner la caracteristica da trametter sco payload", + "Select provider" : "Tscherner il purschider", + "Select registers and schemas to save a view" : "Tscherner Registers e Schemas per memorisar ina vista", + "Select retry policy" : "Tscherner il reglament d'empruvas", + "Select the branch to publish to" : "Tscherner il branch sin il qual publitgar", + "Select the event this webhook should listen to" : "Tscherner l'eveniment ch'il webhook duai ascutar", + "Select views to vectorize:" : "Tscherner las vistas da vectorisar:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Tscherner qualas gruppas da Nextcloud èn disponiblas per questa organisaziun. Ils utilisaders en questas gruppas han access a las resursas da l'organisaziun.", + "Select which data views the AI can search" : "Tscherner qualas vistas da datas l'IA po tschertgar", + "Select which property from the event should be used as the webhook payload data" : "Tscherner qual caracteristica da l'eveniment duai vegnir duvrada sco datas da payload dal webhook", + "Select which tools the AI can use to perform actions" : "Tscherner quals utensils l'IA po duvrar per exequir acziuns", + "Selected" : "Tschernì", + "Selected Groups" : "Gruppas tschernidas", + "Selected audit trails deleted successfully" : "Las colliaziuns d'audit tschernidas èn vegnidas stizzadas cun success", + "Selected search trails deleted successfully" : "Las colliaziuns da tschertga tschernidas èn vegnidas stizzadas cun success", + "Selected users can access this view" : "Ils utilisaders tschernids pon acceder a questa vista", + "Send additional feedback" : "Trametter feedback supplementar", + "Send as CloudEvent" : "Trametter sco CloudEvent", + "Sensitive PII" : "PII sensibel", + "Serial Mode (Safer, slower)" : "Modus serial (pli segir, pli plaun)", + "Serial:" : "Serial:", + "Server Configuration" : "Configuraziun dal server", + "Server Information" : "Infurmaziuns dal server", + "Settings" : "Configuraziuns", + "Shards" : "Shards", + "Share" : "Cumpartir", + "Share with Groups" : "Cumpartir cun gruppas", + "Share with Users" : "Cumpartir cun utilisaders", + "Show Filters" : "Mussar ils filters", + "Show less" : "Mussar damain", + "Show only entries with changes" : "Mussar mo las endataziuns cun midadas", + "Simple" : "Simpel", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Dumondas simplas: tschertgas da text da basa cun parameters minimals (p.ex. in sulet term da tschertga, nagins filters avanzads)", + "Size" : "Grondezza", + "Slug" : "Slug", + "Soft Delete Mode" : "Modus da stizzar mollamain", + "Soft Deleted Items" : "Elements stizzads mollamain", + "SolrCloud mode enabled" : "Il modus SolrCloud è activà", + "Source" : "Funtauna", + "Sources" : "Funtaunas", + "Sources:" : "Funtaunas:", + "Standalone SOLR mode" : "Modus SOLR autonom", + "Start Conversation" : "Cumenzar la conversaziun", + "Start Vectorization" : "Cumenzar la vectorisaziun", + "Start a conversation" : "Cumenzar ina conversaziun", + "Starting names cache warmup..." : "Cumenzar il prechauffament dal cache da nums...", + "Starting..." : "Cumenzar...", + "Statistics" : "Statistica", + "Stats calculation not yet implemented" : "Il calcul da la statistica n'è anc betg implementà", + "Status" : "Status", + "Status Code" : "Code da status", + "String Constraints" : "Restricziuns da string", + "Success" : "Success", + "Success Rate" : "Quota da success", + "Success Status" : "Status da success", + "Successful" : "Reussì", + "Successfully joined organisation" : "Sa participà cun success a l'organisaziun", + "Sync Table" : "Sincronisar la tabella", + "Synchronization Settings" : "Configuraziuns da sincronisaziun", + "System Default" : "Standard dal sistem", + "Technical Description" : "Descripziun tecnica", + "Technical description for developers and administrators" : "Descripziun tecnica per sviluppaders ed administraturs", + "Temperature" : "Temperatura", + "Templates" : "Models", + "Test" : "Test", + "Test Chat" : "Testar il chat", + "Test Connection" : "Testar la colliaziun", + "Test Embedding" : "Testar l'embedding", + "Test endpoint executed successfully" : "L'endpoint da test è vegnì exequì cun success", + "Test webhook delivered successfully" : "Il webhook da test è vegnì furnì cun success", + "Test webhook delivery failed" : "La furniziun dal webhook da test è fallida", + "Test webhook sent successfully" : "Il webhook da test è vegnì tramess cun success", + "Testing..." : "Testar...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Ils chunks da text vegnan generads durant l'extracziun da la Datoteca e memorisads en la banca da datas. La vectorisaziun lescha questas Datotecas prechunkadas e las converta en embeddings.", + "Text extracted successfully" : "Il text è vegnì extratg cun success", + "Text extraction disabled" : "L'extracziun da text è deactivada", + "The URL where webhook events will be sent" : "L'URL nua ch'ils eveniments da webhook vegnan tramess", + "The requested conversation does not exist" : "La conversaziun dumandada n'exista betg", + "The saved view and all its search configuration" : "La vista memorisada e tut sia configuraziun da tschertga", + "There are no audit trail entries matching your current filters." : "I na dat naginas endataziuns da colliaziuns d'audit che correspundan a Voss filters actuals.", + "There are no deleted items matching your current filters." : "I na dat nagins elements stizzads che correspundan a Voss filters actuals.", + "There are no search trail entries matching your current filters." : "I na dat naginas endataziuns da colliaziuns da tschertga che correspundan a Voss filters actuals.", + "There are no webhook log entries matching your filters." : "I na dat naginas endataziuns da protocol da webhook che correspundan a Voss filters.", + "This action cannot be undone." : "Questa acziun na po betg vegnir revocada.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Questa acziun na po betg vegnir revocada. Garantì che naginas collecziuns dovran quest ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Quest agent n'ha naginas vistas u utensils configurabels, ma Vus pudais tuttina adattar las configuraziuns RAG sutvart.", + "This analysis may take some time" : "Questa analisa po durar in tschert temp", + "This audit trail entry does not contain any change information." : "Questa endataziun da colliaziun d'audit na cuntegna naginas infurmaziuns da midada.", + "This configuration has no data defined." : "Questa configuraziun n'ha naginas datas definidas.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Quest endpoint è antiquà. Duvrai enstagl endpoints basads sin chunks.", + "This entity has no relations to objects or files" : "Questa entitad n'ha naginas relaziuns cun objects u Datotecas", + "This is a test webhook from OpenRegister" : "Quai è in webhook da test da OpenRegister", + "This schema must use Magic Table configuration to sync" : "Quest Schema sto duvrar la configuraziun Magic Table per sincronisar", + "This source has no associated registers." : "Questa funtauna n'ha nagins Registers associads.", + "This will copy the _default ConfigSet with the new name" : "Quai copia il ConfigSet _default cun il nov num", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Isto irá eliminar permanentemente TODOS os embeddings (vetores) da base de dados. Terá de voltar a vetorizar todos os objetos e ficheiros. Esta ação não pode ser anulada.\\n\\nTem a certeza de que pretende continuar?", + "This will permanently delete:" : "Quai stizza permanentamain:", + "Timeout (seconds)" : "Timeout (secundas)", + "Timestamp" : "Marca da temp", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Conegl: activai mo vistas che basegnan la tschertga semantica per minimar ils custs d'embedding. Tabellas da consultaziun simplas basegnan raramain vectorisaziun.", + "Title" : "Titel", + "To Date" : "Fin la data", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Per adattar la grondezza dal chunk u la strategia, mai a la configuraziun da Datoteca → Limitas d'elavuraziun.", + "To change the embedding provider or model, go to LLM Configuration." : "Per midar il purschider u il model d'embedding, mai a la configuraziun LLM.", + "To date" : "Fin la data", + "Toggle search sidebar" : "Cumutar la trav laterala da tschertga", + "Tools" : "Utensils", + "Top Deleters" : "Ils principals che stizzan", + "Total" : "Total", + "Total Audit Trails" : "Total da colliaziuns d'audit", + "Total Chunks Available:" : "Total da chunks disponibels:", + "Total Deleted Items" : "Total d'elements stizzads", + "Total Members" : "Total da commembers", + "Total Objects" : "Total d'objects", + "Total Objects in Database:" : "Total d'objects en la banca da datas:", + "Total Organisations" : "Total d'organisaziuns", + "Total Results" : "Total da resultats", + "Total Searches" : "Total da tschertgas", + "Total Size" : "Grondezza totala", + "Total Storage" : "Deposit total", + "Total:" : "Total:", + "Totals" : "Totals", + "Type" : "Tip", + "Type Issue" : "Problem da tip", + "Type Variations:" : "Variaziuns da tip:", + "Type must be \"positive\" or \"negative\"" : "O tipo tem de ser \"positive\" ou \"negative\"", + "Type to search for groups" : "Tippai per tschertgar gruppas", + "Type to search for users" : "Tippai per tschertgar utilisaders", + "Type to search groups..." : "Tippai per tschertgar gruppas...", + "Type to search users..." : "Tippai per tschertgar utilisaders...", + "URL" : "URL", + "URL where Ollama is running" : "URL nua ch'Ollama funcziunescha", + "UUID:" : "UUID:", + "Unchanged" : "Nunmidà", + "Unique Search Terms" : "Terms da tschertga unics", + "Unique Terms" : "Terms unics", + "Unknown" : "Nunenconuschent", + "Unknown Register" : "Register nunenconuschent", + "Unknown Schema" : "Schema nunenconuschent", + "Untitled View" : "Vista senza titel", + "Update" : "Actualisar", + "Update Operations" : "Operaziuns d'actualisaziun", + "Update configuration: ..." : "Actualisar la configuraziun: ...", + "Update register OAS: ..." : "Actualisar l'OAS dal Register: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Actualisar ils vecturs cur che las datas d'object midan (recumandà per ina tschertga precisa)", + "Updated" : "Actualisà", + "Updated At" : "Actualisà la", + "Updated:" : "Actualisà:", + "Uptime:" : "Temp en funcziun:", + "Usage Count" : "Dumber da diever", + "Use SolrCloud with Zookeeper for distributed search" : "Duvrar SolrCloud cun Zookeeper per la tschertga distribuida", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Duvrar filters per restrenscher las endataziuns da colliaziuns d'audit tenor Register, Schema, tip d'acziun, utilisader, perioda da data u ID d'object.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Duvrar filters per restrenscher ils elements stizzads tenor Register, Schema, data da stizzada u l'utilisader che ha stizzà els.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Duvrar filters per restrenscher las endataziuns da colliaziuns da tschertga tenor Register, Schema, status da success, utilisader, perioda da data, terms da tschertga u metricas da prestaziun.", + "Used By Collections" : "Duvrà da collecziuns", + "User" : "Utilisader", + "User Agent Statistics" : "Statistica dal User Agent", + "User Description" : "Descripziun da l'utilisader", + "User rate limits cleared successfully" : "Las limitas da tariffa d'utilisader èn vegnidas stizzadas cun success", + "User removed successfully" : "L'utilisader è vegnì allontanà cun success", + "User-friendly description shown in forms and help text" : "Descripziun adattada a l'utilisader mussada en ils formulars ed il text d'agid", + "Username" : "Num d'utilisader", + "Username for SOLR authentication (optional)" : "Num d'utilisader per l'autentificaziun SOLR (opziunal)", + "Users" : "Utilisaders", + "Using Pre-Generated Chunks" : "Diever da chunks pregenerads", + "Validate" : "Validar", + "Validate Objects" : "Validar ils objects", + "Validation completed successfully" : "La validaziun è vegnida cumpletada cun success", + "Validation failed" : "La validaziun è fallida", + "Value" : "Valur", + "Vector Dimensions" : "Dimensiuns dal vectur", + "Vector Search Backend" : "Backend da tschertga da vectur", + "Vector field: _embedding_" : "Chomp da vectur: _embedding_", + "Vectorization Triggers" : "Triggers da vectorisaziun", + "Vectorize All Files" : "Vectorisar tut las Datotecas", + "Vectorize All Objects" : "Vectorisar tut ils objects", + "Vectorize all file types" : "Vectorisar tut ils tips da Datoteca", + "Vectorize all views" : "Vectorisar tut las vistas", + "Vectorize on object creation" : "Vectorisar cur ch'in object vegn creà", + "Vectors will be stored in your existing object and file collections" : "Ils vecturs vegnan memorisads en Vossas collecziuns existentas d'objects e Datotecas", + "Version" : "Versiun", + "Very High" : "Fitg aut", + "View API Docs" : "Vesair la documentaziun API", + "View Changes" : "Vesair las midadas", + "View Details" : "Vesair ils detagls", + "View Error" : "Vesair l'errur", + "View File" : "Vesair la Datoteca", + "View Full Details" : "Vesair tut ils detagls", + "View Logs" : "Vesair ils protocols", + "View Name" : "Num da la vista", + "View Object" : "Vesair l'object", + "View Parameters" : "Vesair ils parameters", + "View Selection" : "Selecziun da vistas", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Vesair ed analisar ils protocols da colliaziuns da tschertga cun capacitads avanzadas da filtrar ed analisar", + "View and analyze system audit trails with advanced filtering capabilities" : "Vesair ed analisar las colliaziuns d'audit dal sistem cun capacitads avanzadas da filtrar", + "View deleted successfully" : "La vista è vegnida stizzada cun success", + "View entity details and manage relations" : "Vesair ils detagls da l'entitad ed administrar las relaziuns", + "View name" : "Num da la vista", + "View name is required" : "Il num da la vista è obligatoric", + "View saved successfully!" : "La vista è vegnida memorisada cun success!", + "View search analytics and manage search logs" : "Vesair l'analisa da tschertga ed administrar ils protocols da tschertga", + "View successfully deleted" : "La vista è vegnida stizzada cun success", + "View successfully updated" : "La vista è vegnida actualisada cun success", + "View updated successfully!" : "La vista è vegnida actualisada cun success!", + "View webhook delivery logs and filter by webhook" : "Vesair ils protocols da furniziun da webhook e filtrar tenor il webhook", + "Views" : "Vistas", + "Visible to users" : "Visibel per ils utilisaders", + "Wait for Response" : "Spetgar la resposta", + "Wait for webhook response before continuing (required for request/response flows)" : "Spetgar la resposta dal webhook avant da cuntinuar (obligatoric per process da dumonda/resposta)", + "Warmup Names Cache" : "Prechauffar il cache da nums", + "Warning" : "Avertiment", + "Warning:" : "Avertiment:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detagls dal protocol da webhook", + "Webhook Logs" : "Protocols da webhook", + "Webhook created successfully" : "Il webhook è vegnì creà cun success", + "Webhook deleted" : "Il webhook è stizzà", + "Webhook retry delivered successfully" : "L'emprova dal webhook è vegnida furnida cun success", + "Webhook retry delivery failed" : "La furniziun da l'emprova dal webhook è fallida", + "Webhook updated" : "Il webhook è actualisà", + "Webhook updated successfully" : "Il webhook è vegnì actualisà cun success", + "Webhooks" : "Webhooks", + "Weekly" : "Mintga emna", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Embellar il payload dal webhook en il format CloudEvents per ina meglra interoperabilitad", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Gea", + "You" : "Vus", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Vus pudais crear novs ConfigSets a basa dal model _default, u transferir tals persunalisads directamain sin Voss server SOLR.", + "You do not have access to this conversation" : "Vus n'avais betg access a questa conversaziun", + "You do not have permission to delete this conversation" : "Vus n'avais betg il dretg da stizzar questa conversaziun", + "You do not have permission to modify this conversation" : "Vus n'avais betg il dretg da modifitgar questa conversaziun", + "You do not have permission to restore this conversation" : "Vus n'avais betg il dretg da restituir questa conversaziun", + "You must be logged in to favorite views" : "Vus stuais esser annunzià per marcar vistas sco favorits", + "You need an AI agent to start a conversation." : "Vus basegnais in agent IA per cumenzar ina conversaziun.", + "Your Fireworks AI API key. Get one at" : "Vossa clav API da Fireworks AI. Procurai Vus ina sin", + "Your OpenAI API key. Get one at" : "Vossa clav API da OpenAI. Procurai Vus ina sin", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Voss feedback è vegnì registrà. Opziunalmain pudais Vus furnir qua detagls supplementars...", + "Zookeeper Hosts" : "Hosts da Zookeeper", + "Zookeeper Port" : "Port da Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Configuraziuns da Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "String da colliaziun da Zookeeper per SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Dumber da port da Zookeeper (opziunal, standard 2181)", + "chunk" : "chunk", + "chunks" : "chunks", + "chunks processed" : "chunks elavurads", + "configuration(s)" : "configuraziun(s)", + "e.g., lib/Settings/config.json" : "p.ex. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "p.ex. lib/Settings/register.json", + "failed" : "fallì", + "file" : "datoteca", + "files" : "datotecas", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "commembers", + "menu or contact someone with permission to create agents." : "menu u contactai insatgi cun il dretg da crear agents.", + "messages" : "messadis", + "n8n URL and API key are required" : "L'URL n8n e la clav API èn obligatorics", + "n8n configuration saved successfully" : "La configuraziun n8n è vegnida memorisada cun success", + "n8n connection not configured" : "La colliaziun n8n n'è betg configurada", + "n8n connection successful" : "La colliaziun n8n è reussida", + "n8n connection test successful" : "Il test da colliaziun n8n è reussì", + "n8n integration disabled" : "L'integraziun n8n è deactivada", + "n8n integration enabled" : "L'integraziun n8n è activada", + "n8n project initialized successfully" : "Il project n8n è vegnì inizialisà cun success", + "n8n settings saved successfully" : "Las configuraziuns n8n èn vegnidas memorisadas cun success", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objects", + "objects processed" : "objects elavurads", + "or pick a range" : "u tscherni ina perioda", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "porscha in bun equiliber tranter sveltezza e precisiun.", + "register(s)" : "Register(s)", + "register(s) selected" : "Register(s) tschernì(s)", + "required" : "obligatoric", + "results" : "resultats", + "schema(s)" : "Schema(s)", + "schema(s) selected" : "Schema(s) tschernì(s)", + "sk-..." : "sk-...", + "this application" : "questa applicaziun", + "to" : "a", + "vectorized" : "vectorisà", + "views selected" : "vistas tschernidas", + "ℹ️ Current Configuration" : "ℹ️ Configuraziun actuala", + "⚡ Batch Processing" : "⚡ Elavuraziun en batch", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Context cumplet ✓ Damain probabel da mancar detagls ✗ Respostas pli plaunas ✗ Po includer infurmaziuns main relevantas", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Respostas pli sveltas ✓ Respostas pli focusadas ✗ Po mancar infurmaziuns relevantas", + "✨ AI Features" : "✨ Funcziuns IA", + "All search trails cleared successfully" : "Tut las colliaziuns da tschertga èn vegnidas stizzadas cun success", + "Multiple search trail deletion not implemented yet" : "Il stizzar da pliras colliaziuns da tschertga n'è anc betg implementà", + "No expired search trails found to clear" : "Naginas colliaziuns da tschertga scadidas chattadas da stizzar", + "Missing message" : "Messadi mancant", + "message content is required" : "Il cuntegn dal messadi è obligatoric", + "Missing conversation or agentUuid" : "Conversaziun u agentUuid mancant", + "Access denied" : "Access refusà", + "Conversation not found" : "La conversaziun n'è betg vegnida chattada", + "AI service not configured" : "Il servetsch IA n'è betg configurà", + "Failed to process message" : "Betg reussì dad elavurar il messadi", + "Missing conversationId" : "conversationId mancant", + "conversationId is required" : "conversationId è obligatoric", + "Failed to fetch conversation history" : "Betg reussì da retschaiver la cronologia da la conversaziun", + "Failed to clear conversation" : "Betg reussì da stizzar la conversaziun", + "Invalid feedback type" : "Tip da feedback nunvalaivel", + "type must be \"positive\" or \"negative\"" : "o tipo tem de ser \"positive\" ou \"negative\"", + "Message not found" : "Il messadi n'è betg vegnì chattà", + "Failed to save feedback" : "Betg reussì da memorisar il feedback", + "Failed to get chat statistics" : "Betg reussì da retschaiver la statistica da chat", + "Not Found" : "Betg chattà", + "Engine not found" : "Il motor n'è betg vegnì chattà", + "Consumer not found" : "Il consument n'è betg vegnì chattà", + "Backend parameter is required" : "Il parameter da backend è obligatoric", + "Failed to get database information: %s" : "Betg reussì da retschaiver l'infurmaziun da la banca da datas: %s", + "SOLR setup error: %s" : "Errur da configuraziun SOLR: %s", + "Reindex failed: %s" : "Il reindexar è fallì: %s", + "Anonymized" : "Anonimisà", + "Entities detected" : "Entitads identifitgadas", + "Extract Now" : "Extrair ussa", + "Extracted at" : "Extratg la", + "Extraction" : "Extracziun", + "Failed to load extraction data" : "Betg reussì da chargiar las datas d'extracziun", + "Failed to load register data" : "Betg reussì da chargiar las datas dal Register", + "Failed to retrieve extraction status." : "Betg reussì da recuperar il status da l'extracziun.", + "Failed to retrieve objects for file." : "Betg reussì da recuperar ils objects per la Datoteca.", + "No extraction data available for this file" : "Naginas datas d'extracziun disponiblas per questa Datoteca", + "No register objects reference this file" : "Nagins objects dal Register fan referenza a questa Datoteca", + "Not extracted" : "Betg extratg", + "Register Objects" : "Objects dal Register", + "Risk level" : "Nivel da ristg", + "Text chunks" : "Chunks da text", + "Unknown error" : "Errur nunenconuschenta", + "Very high" : "Fitg aut", + "{title} in {register} / {schema}" : "{title} en {register} / {schema}", + "Push Notifications" : "Communicaziuns push", + "Real-time push notification status via notify_push" : "Status da las communicaziuns push en temp real via notify_push", + "Realtime push not available — the notify_push app is not installed" : "Il push en temp real n'è betg disponibel — l'applicaziun notify_push n'è betg installada", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installai l'applicaziun notify_push da l'App Store da Nextcloud per activar las actualisaziuns en temp real.", + "Open Nextcloud App Store" : "Avrir l'App Store da Nextcloud", + "notify_push is installed but not yet active" : "notify_push è installà ma anc betg activ", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push è installà ma OpenRegister n'ha anc betg confermà in push reussì. Declanschai in memorisar d'object per activar, u controllai Vossa configuraziun da notify_push.", + "notify_push configuration guide" : "Guida da configuraziun da notify_push", + "Realtime push active" : "Push en temp real activ", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Las communicaziuns push en temp real èn activas. Ils clients connectads survegnan actualisaziuns immediatas cur ch'ils objects vegnan creads, actualisads u stizzads.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/rm.json b/l10n/rm.json new file mode 100644 index 0000000000..2b89c69a58 --- /dev/null +++ b/l10n/rm.json @@ -0,0 +1,2665 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin necessário)", + "(no subject)": "(nagin object)", + "(no title)": "(nagin titel)", + "(unnamed)": "(nundenominà)", + "(untitled card)": "(charta senza titel)", + "({count} selected)": "({count} selecionados)", + ", {failed} failed": ", {failed} falharam", + "0 = unlimited": "0 = ilimitado", + "A deactivation request is pending.": "Existe um pedido de desativação pendente.", + "AI Assistant": "Assistent IA", + "AI Chat": "Chat IA", + "AI service not configured": "Il servetsch IA n'è betg configurà", + "API Key": "Clav API", + "API Request Quota (requests/day)": "Quota de pedidos de API (pedidos/dia)", + "API Token Configuration": "Configuração de tokens de API", + "API Tokens": "Tokens de API", + "API tokens saved successfully": "Ils tokens API èn vegnids memorisads cun success", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: este relatório associa cada verwerkingsactiviteit às contagens vitalícias de trilhos de auditoria que lhe são atribuídas. Os auditores e a Autoriteit Persoonsgegevens utilizam-no para revisão de supervisão.", + "About ConfigSets": "Davart ils ConfigSets", + "Access denied": "Access refusà", + "Access organisation dashboard and analytics": "Aceder ao painel e às análises da organização", + "Account": "Conta", + "Action": "Acziun", + "Action Distribution": "Distribuziun da las acziuns", + "Action:": "Acziun:", + "Actions": "Acziuns", + "Active": "Activ", + "Active Collections": "Collecziuns activas", + "Active Filters": "Filters activs", + "Active Organisation:": "Organização ativa:", + "Active Organisations": "Organisaziuns activas", + "Active filters:": "Filters activs:", + "Active organisation changed successfully": "Organização ativa alterada com sucesso", + "Active organisation set successfully": "L'organisaziun activa è vegnida fixada cun success", + "Activities": "Activitads", + "Activity": "Atividade", + "Activity Period": "Perioda d'activitad", + "Add": "Adicionar", + "Add Attachment": "Adicionar anexo", + "Add Endpoint": "Agiuntar endpoint", + "Add Groups": "Agiuntar gruppas", + "Add Property to '{title}'": "Adicionar propriedade a '{title}'", + "Add Register": "Adicionar registo", + "Add Schema": "Agiuntar Schema", + "Add Source": "Agiuntar funtauna", + "Add User": "Adicionar utilizador", + "Add User to Organisation": "Adicionar utilizador à organização", + "Add a contact from any of your address books to associate it with this object.": "Adicione um contacto de qualquer um dos seus livros de endereços para o associar a este objeto.", + "Add a file or files": "Adicionar um ficheiro ou ficheiros", + "Add card": "Agiuntar charta", + "Add contact": "Agiuntar contact", + "Add endpoint": "Adicionar ponto de extremidade", + "Add oneOf entry": "Adicionar entrada oneOf", + "Add range": "Adicionar intervalo", + "Add schema titles, descriptions, and register information to provide richer context for search": "Agiuntar titels da Schema, descripziuns ed infurmaziuns dal Register per porscher in context pli ritg per la tschertga", + "Add to favorites": "Agiuntar als favorits", + "Added": "Agiuntà", + "Added to favorites": "Agiuntà als favorits", + "Additional Information": "Infurmaziuns supplementaras", + "Additional feedback saved. Thank you!": "Il feedback supplementar è vegnì memorisà. Grazia fitg!", + "Addresses": "Endereços", + "Admin": "Administraziun", + "Advanced": "Avanzà", + "Advanced Filters": "Filters avanzads", + "Advanced Options": "Opziuns avanzadas", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Ils filters avanzads na stattan betg a disposiziun cur ch'i vegn duvrada la funtauna da banca da datas. Midai sin Auto u SOLR Index per opziuns da filtrar.", + "After": "Suenter", + "Agent Type": "Tipo de agente", + "Agent deleted successfully": "L'agent è vegnì stizzà cun success", + "Agent successfully created": "Agente criado com sucesso", + "Agent successfully updated": "Agente atualizado com sucesso", + "Agents": "Agents", + "Aggregated across schemas": "Agregado entre esquemas", + "All": "Tut", + "All Categories": "Tut las categorias", + "All Collections": "Tut las collecziuns", + "All Confidence Levels": "Tut ils nivels da confidenza", + "All ConfigSets": "Tut ils ConfigSets", + "All Files": "Tut las Datotecas", + "All Levels": "Tut ils nivels", + "All Types": "Tut ils tips", + "All Webhooks": "Tut ils webhooks", + "All actions": "Tut las acziuns", + "All audit trails cleared successfully": "Tut las colliaziuns d'audit èn vegnidas stizzadas cun success", + "All clear": "Tut en urden", + "All file types": "Todos os tipos de ficheiro", + "All registers": "Tut ils Registers", + "All schemas": "Tut ils Schemas", + "All search trails cleared successfully": "Tut las colliaziuns da tschertga èn vegnidas stizzadas cun success", + "All searches": "Tut las tschertgas", + "All users": "Tut ils utilisaders", + "All-day events": "Eventos de dia inteiro", + "Allowed MIME Types": "Tipos MIME permitidos", + "Already linked": "Já associado", + "An error occurred while clearing audit trails": "Igl è succedida ina errur durant il stizzar da las colliaziuns d'audit", + "An error occurred while deleting the audit trail": "Igl è succedida ina errur durant il stizzar da la colliaziun d'audit", + "An error occurred while deleting the object": "Ocorreu um erro ao eliminar o objeto", + "An error occurred while deleting the view": "Igl è succedida ina errur durant il stizzar da la vista", + "An error occurred while permanently deleting the objects": "Igl è succedida ina errur durant il stizzar permanent dals objects", + "An error occurred while restoring the objects": "Ocorreu um erro ao restaurar os objetos", + "An error occurred while saving the organisation": "Ocorreu um erro ao guardar a organização", + "Analysis completed:": "Analisa cumpletada:", + "Analysis steps:": "Pass da l'analisa:", + "Analytics": "Analisa", + "Analyze Objects": "Analisar ils objects", + "Analyze Properties": "Analisar las caracteristicas", + "Analyze existing properties for improvement opportunities": "Analisar las caracteristicas existentas per pussaivladads da meglieraziun", + "Analyzing...": "Analisar...", + "Anonymized": "Anonimisà", + "Anonymous Group": "Grupo anónimo", + "Any favorites and sharing settings for this view": "Tut ils favorits e las configuraziuns da cumpartir per questa vista", + "Any user": "Mintga utilisader", + "App ID": "ID da aplicação", + "App store cache invalidated successfully": "Il cache da l'App Store è vegnì invalidà cun success", + "App store cache invalidated: {invalidated}": "Cache da App Store invalidada: {invalidated}", + "Application deleted successfully": "L'applicaziun è vegnida stizzada cun success", + "Application identifier for this configuration (optional)": "Identificador de aplicação para esta configuração (opcional)", + "Application settings": "Definições da aplicação", + "Application successfully created": "Aplicação criada com sucesso", + "Application successfully updated": "Aplicação atualizada com sucesso", + "Applications": "Applicaziuns", + "Apply": "Aplicar", + "Apply Changes": "Applitgar las midadas", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Aplicar uma função a todos os esquemas neste registo que não tenham substituições de autorização explícitas.", + "Approval Progress": "Progresso da aprovação", + "Approve": "Aprovar", + "Approved": "Aprovado", + "Archive": "Archiv", + "Archive conversation": "Archivar la conversaziun", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arquivar esta verwerkingsactiviteit? As linhas do trilho de auditoria continuarão a referenciá-la.", + "Archived conversations are hidden from your active list": "Las conversaziuns archivadas èn zuppentadas en Vossa glista activa", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Tem a certeza de que pretende ELIMINAR a coleção \"{name}\"?\n\nIsto irá:\n• Eliminar permanentemente a coleção e todos os seus dados\n• Remover todos os documentos indexados\n• Esta ação não pode ser anulada", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Essas Vus segir che Vus vulais rumir las colliaziuns da tschertga veglias? Quai stizza las endataziuns pli veglias che 30 dis.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Tem a certeza de que pretende limpar todos os dados da coleção \"{name}\"?\n\nIsto irá:\n• Eliminar todos os documentos indexados\n• Manter a estrutura da coleção intacta\n• Esta ação não pode ser anulada", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Tem a certeza de que pretende eliminar a aplicação \"{name}\"? Esta ação não pode ser anulada.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Essas Vus segir che Vus vulais stizzar las colliaziuns d'audit tschernidas? Questa acziun na po betg vegnir revocada.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Essas Vus segir che Vus vulais stizzar las colliaziuns da tschertga tschernidas? Questa acziun na po betg vegnir revocada.", + "Are you sure you want to delete this ConfigSet?": "Essas Vus segir che Vus vulais stizzar quest ConfigSet?", + "Are you sure you want to delete this configset?": "Tem a certeza de que pretende eliminar este configset?", + "Are you sure you want to leave '{name}'?": "Tem a certeza de que pretende sair de '{name}'?", + "Are you sure you want to permanently delete": "Essas Vus segir che Vus vulais stizzar permanentamain", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Tem a certeza de que pretende reindexar a coleção \"{name}\"?\n\nIsto irá:\n• Reconstruir o índice com todos os objetos\n• Demorar vários minutos a concluir\n• Pode afetar o desempenho da pesquisa durante a reindexação", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Tem a certeza de que pretende remover o esquema \"{schema}\" do registo \"{register}\"? Esta ação não pode ser anulada.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Tem a certeza de que pretende eliminar de forma reversível {count} objetos ativos do esquema \"{schema}\"?\n\nSerão marcados como eliminados, mas podem ser removidos permanentemente mais tarde.", + "Array Object Configuration:": "Configuração de objeto de matriz:", + "Articles": "Artitgels", + "Ask a question...": "Far ina dumonda...", + "Ask questions about your data using natural language": "Far dumondas davart Vossas datas cun lingua natirala", + "Assigned collaborative tags": "Etiquetas colaborativas atribuídas", + "Assignment notifications": "Notificações de atribuição", + "Attachment added to {name}": "Anexo adicionado a {name}", + "Attempt": "Emprova", + "Audit Trail Changes": "Midadas da la colliaziun d'audit", + "Audit Trail Details": "Detagls da la colliaziun d'audit", + "Audit Trail Management": "Administraziun da las colliaziuns d'audit", + "Audit Trail Statistics": "Statistica da las colliaziuns d'audit", + "Audit Trails": "Colliaziuns d'audit", + "Audit trail data copied to clipboard": "Las datas da la colliaziun d'audit èn vegnidas copiadas en l'archivet provisoric", + "Audit trail deleted successfully": "La colliaziun d'audit è vegnida stizzada cun success", + "Audit trail successfully deleted": "La colliaziun d'audit è vegnida stizzada cun success", + "Authentication": "Autentificaziun", + "Auto (e.g. Y for year, F Y for month)": "Auto (por exemplo, Y para ano, F Y para mês)", + "Auto share": "Partilha automática", + "Auto-commit disabled": "Auto-commit deactivà", + "Auto-commit enabled": "Auto-commit activà", + "Auto-create Default Organisation": "Crear automaticamain l'organisaziun standard", + "Auto-retry failed vectorizations": "Reemprovar automaticamain las vectorisaziuns fallidas", + "Automatically commit changes to SOLR index": "Cummetter automaticamain las midadas a l'index SOLR", + "Automatically connect to the federation network.": "Ligar automaticamente à rede de federação.", + "Automatically create a default organisation if none exists when the app is initialized": "Crear automaticamain ina organisaziun standard sche nagina exista cur che l'applicaziun vegn inizialisada", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Generar automaticamain embeddings da vectur dals chunks da text cur che las Datotecas vegnan transferidas ed elavuradas", + "Automatically generate vector embeddings when objects are created or updated": "Generar automaticamain embeddings da vectur cur che ils objects vegnan creads u actualisads", + "Automatically retry failed vectorization attempts (max 3 retries)": "Reemprovar automaticamain las empruvas da vectorisaziun fallidas (max 3 empruvas)", + "Automatically update catalogues.": "Atualizar automaticamente os catálogos.", + "Available Filters": "Filters disponibels", + "Available Workflows": "Process da lavur disponibels", + "Available placeholders:": "Marcadores de posição disponíveis:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "As alterações de avatar não são suportadas pelo seu fornecedor de autenticação.", + "Avatar removed": "Avatar removido", + "Avatar updated successfully": "Avatar atualizado com sucesso", + "Avg Execution Time": "Temp d'execuziun media", + "Avg Members/Org": "Commembers media/org", + "Avg Object Views/Session": "Vistas d'object media/sessiun", + "Avg Response Time": "Temp da resposta media", + "Avg Results/Search": "Resultats media/tschertga", + "Avg Searches/Session": "Tschertgas media/sessiun", + "Back": "Enavos", + "Back to Entities": "Enavos a las entitads", + "Back to Registers": "Enavos als Registers", + "Back to Webhooks": "Enavos als webhooks", + "Back to applications": "Enavos a las applicaziuns", + "Back to entities": "Enavos a las entitads", + "Backend parameter is required": "Il parameter da backend è obligatoric", + "Backend updated successfully. Please reload the application.": "Il backend è vegnì actualisà cun success. Chargiai per plaschair danovamain l'applicaziun.", + "Bandwidth Quota (MB/month)": "Quota de largura de banda (MB/mês)", + "Base URL (Optional)": "URL da basa (opziunal)", + "Basic Information": "Infurmaziuns da basa", + "Batch Size": "Grondezza dal batch", + "Batch extraction completed": "L'extracziun da batch è cumpletada", + "Before": "Avant", + "Before object vectorization can work:": "Avant che la vectorisaziun d'object po funcziunar:", + "Behavior": "Comportamento", + "Behavior Issue": "Problem da cumportament", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (duração ISO-8601, por exemplo, P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Il deposit blob è vegnì retratg. Tut ils objects dovran ussa tabellas magicas.", + "Bookmarks": "Segnapaginas", + "Branch": "Branch", + "Bulk Role Assignment: {title}": "Atribuição de funções em massa: {title}", + "Bulk delete operation completed successfully": "L'operaziun da stizzar en massa è vegnida cumpletada cun success", + "Bulk save operation completed successfully": "L'operaziun da memorisar en massa è vegnida cumpletada cun success", + "Business Data": "Datas d'affar", + "CSV": "CSV", + "Cache cleared successfully": "Il cache è vegnì stizzà cun success", + "Cache grew from {old} to {new} entries.": "A cache cresceu de {old} para {new} entradas.", + "Cache shrunk from {old} to {new} entries.": "A cache reduziu-se de {old} para {new} entradas.", + "Cache stayed the same at {size} entries.": "A cache manteve-se igual, com {size} entradas.", + "Calculate Sizes": "Calcular las grondezzas", + "Calendar": "Calendário", + "Calendar Provider Configuration": "Configuração do fornecedor de calendário", + "Calendar integration is not available": "A integração de calendário não está disponível", + "Calendar name": "Nome do calendário", + "Cancel": "Annullar", + "Cancel deactivation request": "Cancelar pedido de desativação", + "Cannot delete: objects are still attached": "Impussibel da stizzar: objects èn anc agiuntads", + "Cannot edit: This register is managed by external configuration {title}": "Não é possível editar: este registo é gerido pela configuração externa {title}", + "Cannot remove schema with existing objects ({count} objects)": "Não é possível remover o esquema com objetos existentes ({count} objetos)", + "Cannot remove schema {schema} because it contains {count} objects": "Não é possível remover o esquema {schema} porque contém {count} objetos", + "Cards": "Cartões", + "Cascade delete": "Eliminação em cascata", + "Categories of data subjects (one per line)": "Categorias de titulares dos dados (uma por linha)", + "Categories of personal data (one per line)": "Categorias de dados pessoais (uma por linha)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Categoria", + "Change Type": "Tip da midada", + "Change password": "Alterar palavra-passe", + "Changes": "Midadas", + "Changes copied to clipboard": "Las midadas èn vegnidas copiadas en l'archivet provisoric", + "Changes for Audit Trail #{id}": "Alterações do trilho de auditoria #{id}", + "Chat Model": "Model da chat", + "Chat Provider": "Purschider da chat", + "Chat Provider (RAG)": "Purschider da chat (RAG)", + "Chat Settings": "Configuraziuns da chat", + "Chat provider connection successful!": "La colliaziun cun il purschider da chat è reussida!", + "Chat settings": "Configuraziuns da chat", + "Chat test failed: {error}": "O teste de conversa falhou: {error}", + "Choose a register": "Tscherner in Register", + "Choose a schema": "Tscherner in Schema", + "Choose how vector similarity calculations are performed for semantic search": "Tscherner co ils calculs da similitad da vectur vegnan exequids per la tschertga semantica", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Tscherner quals tips da Datoteca includer en il process da vectorisaziun. Mo Datotecas cun text e chunks extratgs vegnan elavuradas.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Tscherner qualas vistas includer en il process da vectorisaziun. Laschai vid per elavurar tut las vistas a basa da Vossa configuraziun.", + "Chunk": "Chunk", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Il stizzar da chunks n'è anc betg implementà. Duvrai endpoints basads sin chunks.", + "Chunks": "Chunks", + "Chunks to Vectorize:": "Chunks per vectorisar:", + "Cleanup Old Trails": "Rumir las colliaziuns veglias", + "Cleanup completed": "La rumida è cumpletada", + "Cleanup completed successfully. Deleted {count} entries.": "Limpeza concluída com sucesso. {count} entradas eliminadas.", + "Cleanup failed: {error}": "A limpeza falhou: {error}", + "Clear All Embeddings": "Stizzar tut ils embeddings", + "Clear Entries": "Stizzar las endataziuns", + "Clear Filtered Audit Trails": "Stizzar las colliaziuns d'audit filtradas", + "Clear Filters": "Stizzar ils filters", + "Clear Index": "Stizzar l'index", + "Clear Selection": "Stizzar la selecziun", + "Clear all": "Stizzar tut", + "Clear all filters": "Stizzar tut ils filters", + "Clear date range": "Stizzar la perioda da data", + "Clear filters": "Limpar filtros", + "Clearing...": "Stizzar...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Clique em Executar análise de conformidade para encontrar esquemas onde foram detetados dados pessoais (PII) mas não existe nenhuma anotação de verwerkingsactiviteit.", + "Click to subscribe to notifications": "Clique para subscrever as notificações", + "Click to unsubscribe from notifications": "Clique para anular a subscrição das notificações", + "Close": "Serrar", + "Cluster name": "Nome do cluster", + "Code": "Code", + "Code (short readable key, e.g. v-2026-001)": "Código (chave curta legível, por exemplo, v-2026-001)", + "Collapse details": "Reduzir detalhes", + "Collection Management": "Administraziun da las collecziuns", + "Collection Name": "Num da la collecziun", + "Collection assignments updated successfully": "Las attribuziuns da collecziun èn vegnidas actualisadas cun success", + "Collection cleared successfully": "La collecziun è vegnida stizzada cun success", + "Collection copied successfully": "La collecziun è vegnida copiada cun success", + "Collection created successfully": "La collecziun è vegnida creada cun success", + "Collection deleted successfully": "La collecziun è vegnida stizzada cun success", + "Collection used to store and index file metadata and content": "Collecziun duvrada per memorisar ed indexar metadatas e cuntegn da Datotecas", + "Collection used to store and index object data": "Collecziun duvrada per memorisar ed indexar datas d'object", + "Collections:": "Collecziuns:", + "Color": "Cor", + "Comment...": "Comentário...", + "Commit Message": "Messadi da commit", + "Commit Within (ms)": "Cummetter entaifer (ms)", + "Commit message": "Messadi da commit", + "Compare current schema with real object data": "Cumparegliar il Schema actual cun datas d'object realas", + "Completed": "Cumpletà", + "Complex": "Cumplex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Dumondas cumplexas: tschertgas avanzadas cun plirs filters, operaturs e cumbinaziuns da parameters cumplexas", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Consultas complexas: pesquisas avançadas com múltiplos filtros, operadores e combinações de parâmetros complexas", + "Compliance": "Conformitad", + "Confidence Level": "Nivel da confidenza", + "Confidence Score": "Punctuaziun da confidenza", + "Confidence: {confidence}%": "Confiança: {confidence}%", + "Config must be provided as an object": "La configuraziun sto vegnir furnida sco object", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Administraziun dals ConfigSets", + "ConfigSet Name": "Num dal ConfigSet", + "ConfigSet created successfully": "Il ConfigSet è vegnì creà cun success", + "ConfigSet deleted successfully": "Il ConfigSet è vegnì stizzà cun success", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Ils ConfigSets defineschan il Schema e la configuraziun per Vossas collecziuns SOLR. Els cuntegnan definiziuns da chomps, analisaturs ed autras configuraziuns da tschertga.", + "Configuration": "Configuraziun", + "Configuration Data": "Datas da configuraziun", + "Configuration is up to date": "A configuração está atualizada", + "Configuration saved": "La configuraziun è vegnida memorisada", + "Configuration saved successfully.": "Configuração guardada com sucesso.", + "Configuration type (default, application, etc.)": "Tipo de configuração (default, application, etc.)", + "Configurations": "Configuraziuns", + "Configure API tokens for external service integrations": "Configurar tokens de API para integrações de serviços externos", + "Configure Apache SOLR search engine for advanced search capabilities": "Configurar o motor de pesquisa Apache SOLR para capacidades de pesquisa avançadas", + "Configure Facets": "Configurar las facettas", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Configurar purschiders da Large Language Model (LLM) per funcziuns basadas sin IA, inclus tschertga semantica, embeddings e chat.", + "Configure SOLR Facets": "Configurar facetas SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Configurar las configuraziuns da basa da la colliaziun per Voss server SOLR, inclus l'autentificaziun e las opziuns da rait. Duvrai ils dialogs separads d'administraziun da ConfigSet e collecziuns per administrar cores e collecziuns.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configure as definições básicas de ligação ao seu servidor SOLR, incluindo as opções de autenticação e de rede. Utilize as caixas de diálogo separadas de gestão de configset e coleções para gerir os cores e as coleções.", + "Configure default organisation and organisation-related settings": "Configurar a organização predefinida e as definições relacionadas com a organização", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Configurar co ils objects da la banca da datas vegnan convertids en embeddings da vectur per la tschertga semantica. Ils objects vegnan vectorisads directamain senza basegnar in'extracziun da text.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Configurar co ils objects vegnan convertids en text avant la vectorisaziun. Questas configuraziuns influenzeschan la qualitad e il context da la tschertga.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configurar fornecedores de LLM (large language model) para funcionalidades baseadas em IA, incluindo pesquisa semântica, incorporações e conversa.", + "Configure multi-organization support and tenant isolation": "Configurar o suporte de múltiplas organizações e o isolamento de inquilinos", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Configurar ils parameters per la vectorisaziun da Datotecas. Quest process generescha embeddings da vectur per tut ils chunks da Datoteca extratgs.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Configurar ils parameters per la vectorisaziun d'objects. Quest process generescha embeddings da vectur per tut ils objects che correspundan a Voss filters da vista.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configure este esquema para apresentar objetos como eventos na aplicação Calendário do Nextcloud.", + "Configure which types of data to search and how many sources to retrieve": "Configurar quals tips da datas tschertgar e quantas funtaunas recuperar", + "Configuring field mappings...": "A configurar os mapeamentos de campos...", + "Confirm Account Deactivation": "Confirmar a desativação da conta", + "Confirm Object Deletion": "Confirmar a eliminação do objeto", + "Confirm Object Restoration": "Confirmar o restauro do objeto", + "Confirm Permanent Object Deletion": "Confirmar a eliminação permanente do objeto", + "Confirm deactivation": "Confirmar desativação", + "Connecting to SOLR cluster...": "A ligar ao cluster SOLR...", + "Connection Failed": "La colliaziun è fallida", + "Connection Settings": "Configuraziuns da la colliaziun", + "Connection Successful!": "La colliaziun è reussida!", + "Connection failed": "La colliaziun è fallida", + "Connection protocol": "Protocol da colliaziun", + "Connection successful": "La colliaziun è reussida", + "Connection timeout in seconds": "Timeout da la colliaziun en secundas", + "Constant delay between retries (5 minutes)": "Retardada constanta tranter las empruvas (5 minutas)", + "Constraint Issue": "Problem da restricziun", + "Consumer not found": "Il consument n'è betg vegnì chattà", + "Contacts": "Contacts", + "Contacts integration is not available": "A integração de contactos não está disponível", + "Content Filters": "Filters da cuntegn", + "Continue to Properties": "Cuntinuar a las caracteristicas", + "Contracts": "Contratos", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Controllar qualas vistas d'object duessan vegnir vectorisadas per reducir ils custs API. Vectorisai mo vistas che profiteschan da la tschertga semantica.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Controllar qualas vistas ed utensils l'IA po duvrar en questa conversaziun. Sco standard èn tut las abilitads da l'agent activadas.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Controllescha la casualitad (0 = deterministic, 2 = fitg creativ)", + "Conversation ID is required": "L'ID da conversaziun è obligatoric", + "Conversation archived": "La conversaziun è archivada", + "Conversation archived successfully": "La conversaziun è vegnida archivada cun success", + "Conversation cleared successfully": "La conversaziun è vegnida stizzada cun success", + "Conversation deleted": "La conversaziun è stizzada", + "Conversation not found": "La conversaziun n'è betg vegnida chattada", + "Conversation permanently deleted": "La conversaziun è stizzada permanentamain", + "Conversation renamed": "La conversaziun è renumnada", + "Conversation restored": "La conversaziun è restituida", + "Conversation started with {agent}": "Conversa iniciada com {agent}", + "Conversation title": "Titel da la conversaziun", + "Conversations": "Conversas", + "Converting {schema} to blob storage...": "A converter {schema} para armazenamento de blobs...", + "Converting {schema} to magic table...": "A converter {schema} para tabela mágica...", + "Copied!": "Copià!", + "Copy": "Copiar", + "Copy Changes": "Copiar las midadas", + "Copy Collection": "Copiar la collecziun", + "Copy Data": "Copiar las datas", + "Copy Full Data": "Copiar tut las datas", + "Copy of {name}": "Cópia de {name}", + "Copy to clipboard": "Copiar para a área de transferência", + "Copying...": "Copiar...", + "Costs": "Custs", + "Count": "Dumber", + "Counting objects...": "Dumbrar ils objects...", + "Create": "Crear", + "Create API Token": "Criar token de API", + "Create Agent": "Crear agent", + "Create Application": "Crear applicaziun", + "Create Collection": "Crear collecziun", + "Create ConfigSet": "Crear ConfigSet", + "Create New Collection": "Crear nova collecziun", + "Create New ConfigSet": "Crear nov ConfigSet", + "Create Operations": "Operaziuns da crear", + "Create Organisation": "Criar organização", + "Create Webhook": "Crear webhook", + "Create a copy of collection:": "Crear ina copia da la collecziun:", + "Create a new ConfigSet based on the _default template": "Crear in nov ConfigSet a basa dal model _default", + "Create a new SOLR collection from an existing ConfigSet": "Crear ina nova collecziun SOLR ord in ConfigSet existent", + "Create a new SOLR collection from an existing configset": "Criar uma nova coleção SOLR a partir de um configset existente", + "Create a new configset based on the _default template": "Criar um novo configset baseado no modelo _default", + "Create a new event or link an existing one from any of your calendars.": "Crie um novo evento ou associe um existente de qualquer um dos seus calendários.", + "Create another": "Criar outro", + "Create copies of": "Criar cópias de", + "Create event": "Crear eveniment", + "Create new token": "Criar novo token", + "Create or link a Deck card to track work on this object.": "Crie ou associe um cartão Deck para acompanhar o trabalho neste objeto.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Crie a primeira verwerkingsactiviteit para começar a etiquetar as linhas do trilho de auditoria com a respetiva atribuição AVG Art 30.", + "Create your first AI agent to get started.": "Creai Voss emprim agent IA per cumenzar.", + "Created": "Creà", + "Created:": "Creà:", + "Creating search collections...": "A criar coleções de pesquisa...", + "Creating...": "Crear...", + "Current": "Atual", + "Current Filters": "Filters actuals", + "Current Type:": "Tip actual:", + "Current password": "Palavra-passe atual", + "Custom API endpoint if using a different region": "Endpoint API persunalisà sche Vus duvrais ina autra regiun", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Headers HTTP persunalisads (in per lingia, format: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Cabeçalhos HTTP personalizados (um por linha, formato: header-name: value)", + "Custom Ranges:": "Intervalos personalizados:", + "Custom display title for this facet": "Título de apresentação personalizado para esta faceta", + "Custom pattern:": "Padrão personalizado:", + "DELETE COLLECTION": "DELETE COLLECTION", + "DSAR": "DSAR", + "Daily": "Mintga di", + "Dashboard": "Panel", + "Dashboard not found": "Painel não encontrado", + "Data": "Dados", + "Data Source": "Funtauna da datas", + "Data Sources": "Fontes de dados", + "Data Views": "Vistas de dados", + "Data sources": "Funtaunas da datas", + "Data type does not match observed values": "Il tip da datas na correspunda betg als valurs observads", + "Data-subject access request": "Pedido de acesso do titular dos dados", + "Database URL": "URL da la banca da datas", + "Database information refreshed": "Las infurmaziuns da la banca da datas èn vegnidas actualisadas", + "Date Faceting:": "Facetação por data:", + "Date Range": "Perioda da data", + "Dates": "Datas", + "Deactivation request cancelled": "Pedido de desativação cancelado", + "Deactivation request submitted": "Pedido de desativação submetido", + "Deck": "Deck", + "Deck integration is not available": "A integração do Deck não está disponível", + "Default": "Standard", + "Default Object Tenant": "Inquilino de objeto predefinido", + "Default Organisation": "Organisaziun standard", + "Default Owner": "Proprietário predefinido", + "Default User (for cron/background jobs)": "Utilizador predefinido (para tarefas cron/em segundo plano)", + "Default User Tenant": "Inquilino de utilizador predefinido", + "Default value": "Valor predefinido", + "Default value (Email)": "Valor predefinido (E-mail)", + "Default value (Password)": "Valor predefinido (Palavra-passe)", + "Default value (Phone number)": "Valor predefinido (Número de telefone)", + "Default value (Regex)": "Valor predefinido (Regex)", + "Default: 5": "Standard: 5", + "Defaults to current user. Select a different user if needed.": "A predefinição é o utilizador atual. Selecione um utilizador diferente, se necessário.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Las retardadas dobleschan cun mintga emprova (2, 4, 8 minutas...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Las retardadas creschan linearmain (5, 10, 15 minutas...)", + "Delete": "Stizzar", + "Delete ({count})": "Eliminar ({count})", + "Delete Application": "Stizzar l'applicaziun", + "Delete Audit Trail": "Stizzar la colliaziun d'audit", + "Delete Collection": "Stizzar la collecziun", + "Delete ConfigSet": "Stizzar il ConfigSet", + "Delete Objects": "Stizzar ils objects", + "Delete Operations": "Operaziuns da stizzar", + "Delete SOLR Collection": "Eliminar coleção SOLR", + "Delete View": "Stizzar la vista", + "Delete all objects in this schema": "Stizzar tut ils objects en quest Schema", + "Delete permanently": "Stizzar permanentamain", + "Delete this conversation?": "Stizzar questa conversaziun?", + "Delete view": "Stizzar la vista", + "Delete {count} object": "Eliminar {count} objeto", + "Delete {count} objects": "Eliminar {count} objetos", + "Deleted": "Stizzà", + "Deleted By": "Stizzà da", + "Deleted Date": "Data da stizzada", + "Deleted Items Management": "Administraziun dals elements stizzads", + "Deleted This Week": "Stizzà questa emna", + "Deleted Today": "Stizzà oz", + "Deleted:": "Stizzà:", + "Deleting SOLR Collection...": "A eliminar coleção SOLR...", + "Deleting...": "Stizzar...", + "Deletion Date Range": "Perioda da data da stizzada", + "Deletion Statistics": "Statistica da stizzada", + "Deprecated": "Antiquà", + "Depublish": "Despublicar", + "Description": "Descripziun", + "Description Template": "Modelo de descrição", + "Description shown as tooltip": "Descrição apresentada como dica de ferramenta", + "Description:": "Descripziun:", + "Detect data types and patterns": "Identifitgar tips da datas e models", + "Detected At": "Identifitgà la", + "Detected Format:": "Format identifitgà:", + "Detected Issues:": "Problems identifitgads:", + "Detected: {date}": "Detetado: {date}", + "Direct permission": "Permissão direta", + "Disable": "Deactivar", + "Disable this to make the agent": "Desative isto para tornar o agente", + "Disabled": "Deactivà", + "Discover Files": "Scuvrir las Datotecas", + "Discovered Properties": "Caracteristicas scuvertas", + "Display Format": "Formato de apresentação", + "Display Name": "Nome de apresentação", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Vulais Vus stizzar permanentamain tut las endataziuns da colliaziuns d'audit filtradas? Questa acziun na po betg vegnir revocada.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Vulais Vus stizzar permanentamain questa endataziun da colliaziun d'audit? Questa acziun na po betg vegnir revocada.", + "Documents": "Documents", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR ativado", + "Download API Spec": "Telechargiar la specificaziun API", + "Download API Specification": "Transferir especificação da API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Transfira uma cópia de todos os seus dados pessoais armazenados no OpenRegister (GDPR Artigo 20.º).", + "Drag a file or files here": "Arraste um ficheiro ou ficheiros para aqui", + "Dry run completed: {count} fields would be created": "Execução de teste concluída: seriam criados {count} campos", + "Dry run completed: {count} fields would be fixed": "Execução de teste concluída: seriam corrigidos {count} campos", + "Duration in seconds (optional)": "Duração em segundos (opcional)", + "Edit": "Modifitgar", + "Edit Agent": "Editar agente", + "Edit Application": "Editar aplicação", + "Edit Configuration": "Editar configuração", + "Edit Endpoint": "Editar ponto de extremidade", + "Edit Organisation": "Editar organização", + "Edit Property '{key}' of '{title}'": "Editar propriedade '{key}' de '{title}'", + "Edit Register": "Modifitgar il Register", + "Edit Schema": "Modifitgar il Schema", + "Edit Source": "Editar fonte", + "Edit View": "Modifitgar la vista", + "Edit Webhook": "Modifitgar il webhook", + "Edit labels": "Editar etiquetas", + "Edit processing activity": "Editar verwerkingsactiviteit", + "Edit tags for {name}": "Editar etiquetas de {name}", + "Edit verwerkingsactiviteit": "Editar verwerkingsactiviteit", + "Edit view": "Modifitgar la vista", + "Edit view details": "Modifitgar ils detagls da la vista", + "Effectiveness": "Effectivitad", + "Email": "E-mail", + "Email Addresses": "Endereços de e-mail", + "Email digest frequency": "Frequência do resumo por e-mail", + "Emails": "E-mails", + "Embedding Model": "Model d'embedding", + "Embedding Provider": "Purschider d'embedding", + "Embedding generated successfully": "L'embedding è vegnì generà cun success", + "Embedding provider connection successful!": "La colliaziun cun il purschider d'embedding è reussida!", + "Embedding test failed: {error}": "O teste de incorporação falhou: {error}", + "Enable": "Activar", + "Enable RAG": "Ativar RAG", + "Enable auto-creation": "Activar la creaziun automatica", + "Enable automatic file vectorization": "Activar la vectorisaziun automatica da Datotecas", + "Enable automatic object vectorization": "Activar la vectorisaziun automatica d'objects", + "Enable automatic synchronization": "Activar la sincronisaziun automatica", + "Enable calendar provider": "Ativar fornecedor de calendário", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Activar il protocollar detaglià per las operaziuns SOLR (recumandà per il debugging)", + "Enable faceting": "Activar las facettas", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Activar u deactivar las funcziuns LLM. Configurai ils purschiders e models cun il buttun da configuraziun LLM survart.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Ative ou desative as funcionalidades de LLM. Configure os fornecedores e modelos utilizando o botão de configuração de LLM acima.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Activar u deactivar l'integraziun da tschertga SOLR. Configurai las configuraziuns da la colliaziun cun il buttun da configuraziuns da la colliaziun survart.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Ative ou desative a integração de pesquisa SOLR. Configure as definições de ligação utilizando o botão de definições de ligação acima.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Activar u deactivar l'integraziun dal process da lavur n8n. Configurai las configuraziuns da la colliaziun sutvart.", + "Enable or disable this webhook": "Activar u deactivar quest webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Ative ferramentas que permitem ao agente interagir com os dados através de chamadas de função.", + "Enable vectorization for all existing and future views (may increase costs)": "Activar la vectorisaziun per tut las vistas existentas e futuras (po augmentar ils custs)", + "Enable write-back to target objects": "Ativar a regravação nos objetos de destino", + "Enabled": "Activà", + "End Date Field": "Campo de data de fim", + "Endpoint Path*": "Caminho do ponto de extremidade*", + "Endpoints": "Endpoints", + "Engine not found": "Il motor n'è betg vegnì chattà", + "Enter ConfigSet name": "Endatar il num dal ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Introduza os nomes de utilizador do Nextcloud para conceder acesso a este agente privado", + "Enter agent description (optional)": "Introduza a descrição do agente (opcional)", + "Enter agent name": "Introduza o nome do agente", + "Enter application description (optional)": "Introduza a descrição da aplicação (opcional)", + "Enter application name": "Introduza o nome da aplicação", + "Enter collection name": "Endatar il num da la collecziun", + "Enter configuration description (optional)": "Introduza a descrição da configuração (opcional)", + "Enter configuration title": "Introduza o título da configuração", + "Enter description (optional)...": "Endatar la descripziun (opziunal)...", + "Enter new collection name": "Endatar il nov num da la collecziun", + "Enter object ID": "Endatar l'ID da l'object", + "Enter organisation description (optional)": "Introduza a descrição da organização (opcional)", + "Enter organisation name": "Introduza o nome da organização", + "Enter search term": "Endatar in term da tschertga", + "Enter search terms or leave empty to browse all": "Introduza termos de pesquisa ou deixe vazio para navegar por todos", + "Enter system prompt for the agent": "Introduza o prompt de sistema para o agente", + "Enter username and press Enter": "Introduza o nome de utilizador e prima Enter", + "Enter view name...": "Endatar il num da la vista...", + "Enter webhook name": "Endatar il num dal webhook", + "Enter your API key": "Introduza a sua chave de API", + "Enter your object here...": "Introduza o seu objeto aqui...", + "Enter your schema here...": "Introduza o seu esquema aqui...", + "Entities": "Entitads", + "Entities detected": "Entitads identifitgadas", + "Entity ID: {id}": "ID da entidade: {id}", + "Entity Information": "Infurmaziuns da l'entitad", + "Entity Recognition": "Reconhecimento de entidades", + "Entity Recognition Method": "Método de reconhecimento de entidades", + "Entity deleted successfully": "L'entitad è vegnida stizzada cun success", + "Entity not found": "L'entitad n'è betg vegnida chattada", + "Entries to be deleted:": "Endataziuns che vegnan stizzadas:", + "Enum Issue": "Problem d'enum", + "Enum constraint is missing": "La restricziun d'enum manca", + "Erase (Art 17)": "Apagar (Art. 17.º)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Apagar {count} objeto(s) deste titular? Esta ação é registada no trilho de auditoria.", + "Erasure complete": "Stizzada cumpletada", + "Erasure preview": "Prevista da la stizzada", + "Error": "Errur", + "Error Details": "Detagls da l'errur", + "Error Information": "Infurmaziuns da l'errur", + "Error Message": "Messadi d'errur", + "Error deleting audit trails: {error}": "Erro ao eliminar trilhos de auditoria: {error}", + "Error deleting search trails: {error}": "Erro ao eliminar trilhos de pesquisa: {error}", + "Error loading application": "Errur durant il chargiar da l'applicaziun", + "Error loading audit trails": "Errur durant il chargiar da las colliaziuns d'audit", + "Error loading entity": "Errur durant il chargiar da l'entitad", + "Error loading search trails": "Errur durant il chargiar da las colliaziuns da tschertga", + "Estimated Batches:": "Batches estimads:", + "Estimated Cost:": "Custs estimads:", + "Estimated Duration:": "Durada estimada:", + "Event": "Eveniment", + "Event Property for Payload": "Caracteristica d'eveniment per il payload", + "Events": "Eveniments", + "Every schema with detected PII has a processing-activity annotation.": "Todos os esquemas com dados pessoais (PII) detetados têm uma anotação de verwerkingsactiviteit.", + "Example": "Exemplo", + "Example Value": "Valur d'exempel", + "Example value for this property": "Valor de exemplo para esta propriedade", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Maximum exclusiv", + "Exclusive Minimum": "Minimum exclusiv", + "Exclusive maximum": "Máximo exclusivo", + "Exclusive minimum": "Mínimo exclusivo", + "Execution Mode": "Modus d'execuziun", + "Execution Time": "Temp d'execuziun", + "Execution Time Range": "Perioda dal temp d'execuziun", + "Existing Improvements": "Meglieraziuns existentas", + "Expand details": "Mussar ils detagls", + "Expiration": "Expiração", + "Expires": "Expira", + "Expires in (e.g., 90d)": "Expira em (por exemplo, 90d)", + "Exponential": "Exponenzial", + "Export": "Exportar", + "Export \"{schema}\" objects from \"{register}\"": "Exportar objetos de \"{schema}\" do \"{register}\"", + "Export Configuration": "Exportar Configuração", + "Export Format:": "Formato de Exportação:", + "Export Objects": "Exportar Objetos", + "Export completed successfully": "L'export è vegnì cumpletà cun success", + "Export downloaded successfully": "Exportação transferida com sucesso", + "Export failed: {error}": "A exportação falhou: {error}", + "Export is rate limited. Please try again later.": "A exportação está limitada por taxa. Tente novamente mais tarde.", + "Export my data": "Exportar os meus dados", + "Export, view, or delete audit trails": "Exportar, vesair u stizzar las colliaziuns d'audit", + "Exporting...": "A exportar...", + "Extend Schema": "Extender il Schema", + "Extended": "Extendì", + "Extensions": "Extensiuns", + "Extra Query Parameters": "Parâmetros de Consulta Adicionais", + "Extract Now": "Extrair ussa", + "Extract Pending Files": "Extrair las Datotecas pendentas", + "Extract now": "Extrair agora", + "Extract properties from each object": "Extrair las caracteristicas da mintga object", + "Extracted At": "Extratg la", + "Extracted at": "Extratg la", + "Extraction": "Extracziun", + "Extraction Mode": "Modo de Extração", + "Extraction Scope": "Âmbito de Extração", + "Extraction Status": "Status da l'extracziun", + "Extraction error for {file}: {error}": "Erro de extração para {file}: {error}", + "Facet Description": "Descrição da Faceta", + "Facet Order": "Ordem da Faceta", + "Facet Title": "Título da Faceta", + "Facet Type": "Tipo de Faceta", + "Facet configuration updated successfully": "La configuraziun da las facettas è vegnida actualisada cun success", + "Facetable": "Facetável", + "Faceting Configuration:": "Configuração de Facetas:", + "Facets discovered and configured successfully": "Las facettas èn vegnidas scuvertas e configuradas cun success", + "Facets discovered successfully": "Las facettas èn vegnidas scuvertas cun success", + "Failed": "Fallì", + "Failed to add attachment to object": "Falha ao adicionar anexo ao objeto", + "Failed to add user to organisation": "Falha ao adicionar utilizador à organização", + "Failed to analyze schema properties": "Betg reussì d'analisar las caracteristicas dal Schema", + "Failed to archive conversation": "Betg reussì d'archivar la conversaziun", + "Failed to calculate sizes": "Betg reussì da calcular las grondezzas", + "Failed to cancel deactivation": "Falha ao cancelar a desativação", + "Failed to change active organisation: {error}": "Falha ao alterar a organização ativa: {error}", + "Failed to change password": "Falha ao alterar a palavra-passe", + "Failed to check version: {error}": "Falha ao verificar a versão: {error}", + "Failed to clear audit trails: {error}": "Falha ao limpar registos de auditoria: {error}", + "Failed to clear blob objects: {error}": "Falha ao limpar objetos blob: {error}", + "Failed to clear cache: {error}": "Falha ao limpar a cache: {error}", + "Failed to clear collection": "Betg reussì da stizzar la collecziun", + "Failed to clear conversation": "Betg reussì da stizzar la conversaziun", + "Failed to clear embeddings: {error}": "Falha ao limpar embeddings: {error}", + "Failed to clear search trails: {error}": "Falha ao limpar registos de pesquisa: {error}", + "Failed to clear {type} cache: {error}": "Falha ao limpar a cache de {type}: {error}", + "Failed to copy changes": "Betg reussì da copiar las midadas", + "Failed to copy data": "Betg reussì da copiar las datas", + "Failed to copy data to clipboard": "Betg reussì da copiar las datas en l'archivet provisoric", + "Failed to copy to clipboard": "Falha ao copiar para a área de transferência", + "Failed to copy token": "Falha ao copiar token", + "Failed to create conversation": "Betg reussì da crear la conversaziun", + "Failed to create missing SOLR fields: {error}": "Falha ao criar campos SOLR em falta: {error}", + "Failed to create or find project": "Betg reussì da crear u chattar il project", + "Failed to create token": "Falha ao criar token", + "Failed to delete SOLR collection: {error}": "Falha ao eliminar a coleção SOLR: {error}", + "Failed to delete collection": "Betg reussì da stizzar la collecziun", + "Failed to delete conversation": "Betg reussì da stizzar la conversaziun", + "Failed to delete objects for {schema}: {error}": "Falha ao eliminar objetos de {schema}: {error}", + "Failed to delete webhook": "Betg reussì da stizzar il webhook", + "Failed to depublish register: {error}": "Falha ao despublicar o registo: {error}", + "Failed to depublish schema: {error}": "Falha ao despublicar o esquema: {error}", + "Failed to depublish: {error}": "Falha ao despublicar: {error}", + "Failed to discover files: {error}": "Falha ao descobrir ficheiros: {error}", + "Failed to download API specification": "Betg reussì da telechargiar la specificaziun API", + "Failed to export data": "Falha ao exportar dados", + "Failed to export objects": "Falha ao exportar objetos", + "Failed to fetch conversation history": "Betg reussì da retschaiver la cronologia da la conversaziun", + "Failed to fix mismatched SOLR fields: {error}": "Falha ao corrigir campos SOLR incompatíveis: {error}", + "Failed to get SOLR field configuration": "Betg reussì da retschaiver la configuraziun dals chomps SOLR", + "Failed to get chat statistics": "Betg reussì da retschaiver la statistica da chat", + "Failed to get database information: %s": "Betg reussì da retschaiver l'infurmaziun da la banca da datas: %s", + "Failed to get entity categories": "Betg reussì da retschaiver las categorias d'entitad", + "Failed to get entity statistics": "Betg reussì da retschaiver la statistica d'entitad", + "Failed to get entity types": "Betg reussì da retschaiver ils tips d'entitad", + "Failed to get response: {error}": "Falha ao obter resposta: {error}", + "Failed to invalidate app store cache: {error}": "Falha ao invalidar a cache da App Store: {error}", + "Failed to leave organisation: {error}": "Falha ao sair da organização: {error}", + "Failed to link object": "Falha ao associar objeto", + "Failed to load Deck cards": "Falha ao carregar cartões do Deck", + "Failed to load LLM configuration": "Betg reussì da chargiar la configuraziun LLM", + "Failed to load Nextcloud groups": "Betg reussì da chargiar las gruppas da Nextcloud", + "Failed to load cache statistics: {error}": "Falha ao carregar estatísticas da cache: {error}", + "Failed to load conversation": "Betg reussì da chargiar la conversaziun", + "Failed to load entities": "Betg reussì da chargiar las entitads", + "Failed to load entity": "Betg reussì da chargiar l'entitad", + "Failed to load extraction data": "Betg reussì da chargiar las datas d'extracziun", + "Failed to load files": "Betg reussì da chargiar las Datotecas", + "Failed to load linked contacts": "Falha ao carregar contactos associados", + "Failed to load linked emails": "Falha ao carregar emails associados", + "Failed to load linked events": "Falha ao carregar eventos associados", + "Failed to load organisations": "Betg reussì da chargiar las organisaziuns", + "Failed to load register data": "Betg reussì da chargiar las datas dal Register", + "Failed to load related entities": "Falha ao carregar entidades relacionadas", + "Failed to load settings: {error}": "Falha ao carregar definições: {error}", + "Failed to load statistics: {error}": "Falha ao carregar estatísticas: {error}", + "Failed to load templates": "Betg reussì da chargiar ils models", + "Failed to load view: {error}": "Falha ao carregar a vista: {error}", + "Failed to load webhooks": "Betg reussì da chargiar ils webhooks", + "Failed to load workflows": "Betg reussì da chargiar ils process da lavur", + "Failed to process message": "Betg reussì dad elavurar il messadi", + "Failed to publish register: {error}": "Falha ao publicar o registo: {error}", + "Failed to publish schema: {error}": "Falha ao publicar o esquema: {error}", + "Failed to publish: {error}": "Falha ao publicar: {error}", + "Failed to refresh database information": "Betg reussì d'actualisar las infurmaziuns da la banca da datas", + "Failed to reindex collection": "Betg reussì da reindexar la collecziun", + "Failed to remove avatar": "Falha ao remover o avatar", + "Failed to remove link": "Falha ao remover a associação", + "Failed to remove member: {error}": "Falha ao remover membro: {error}", + "Failed to remove schema {schema}: {error}": "Falha ao remover o esquema {schema}: {error}", + "Failed to remove user from organisation": "Falha ao remover utilizador da organização", + "Failed to rename conversation": "Betg reussì da renumnar la conversaziun", + "Failed to request deactivation": "Falha ao solicitar a desativação", + "Failed to restore conversation": "Betg reussì da restituir la conversaziun", + "Failed to retrieve extraction status.": "Betg reussì da recuperar il status da l'extracziun.", + "Failed to retrieve objects for file.": "Betg reussì da recuperar ils objects per la Datoteca.", + "Failed to retry extraction": "Betg reussì da reemprovar l'extracziun", + "Failed to retry webhook": "Betg reussì da reemprovar il webhook", + "Failed to revoke token": "Falha ao revogar token", + "Failed to save GitHub token": "Betg reussì da memorisar il token da GitHub", + "Failed to save GitLab URL": "Betg reussì da memorisar l'URL da GitLab", + "Failed to save GitLab token": "Betg reussì da memorisar il token da GitLab", + "Failed to save LLM settings: {error}": "Falha ao guardar definições do LLM: {error}", + "Failed to save additional feedback": "Betg reussì da memorisar il feedback supplementar", + "Failed to save configuration.": "Falha ao guardar a configuração.", + "Failed to save configuration: {error}": "Falha ao guardar a configuração: {error}", + "Failed to save facet configuration": "Falha ao guardar a configuração de facetas", + "Failed to save feedback": "Betg reussì da memorisar il feedback", + "Failed to save file settings: {error}": "Falha ao guardar definições de ficheiros: {error}", + "Failed to save n8n configuration": "Betg reussì da memorisar la configuraziun n8n", + "Failed to save preferences": "Falha ao guardar preferências", + "Failed to save roles": "Betg reussì da memorisar ils rolls", + "Failed to save settings": "Betg reussì da memorisar las configuraziuns", + "Failed to save settings: {error}": "Falha ao guardar definições: {error}", + "Failed to save view: {error}": "Falha ao guardar a vista: {error}", + "Failed to save warmup interval: {error}": "Falha ao guardar o intervalo de aquecimento: {error}", + "Failed to save webhook": "Betg reussì da memorisar il webhook", + "Failed to send feedback": "Betg reussì da trametter il feedback", + "Failed to set active organisation: {error}": "Falha ao definir a organização ativa: {error}", + "Failed to setup SOLR: {error}": "Falha ao configurar o SOLR: {error}", + "Failed to start file warmup: {error}": "Falha ao iniciar o aquecimento de ficheiros: {error}", + "Failed to start processing {type} files: {error}": "Falha ao iniciar o processamento de ficheiros {type}: {error}", + "Failed to start vectorization: {error}": "Falha ao iniciar a vetorização: {error}", + "Failed to switch organisation: {error}": "Falha ao mudar de organização: {error}", + "Failed to sync magic table for {schema}: {error}": "Falha ao sincronizar a tabela mágica para {schema}: {error}", + "Failed to test SOLR connection: {error}": "Falha ao testar a ligação SOLR: {error}", + "Failed to test webhook": "Betg reussì da testar il webhook", + "Failed to update LLM settings: {error}": "Falha ao atualizar definições do LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Falha ao atualizar definições de Multilocação: {error}", + "Failed to update RBAC settings: {error}": "Falha ao atualizar definições de RBAC: {error}", + "Failed to update Retention settings: {error}": "Falha ao atualizar definições de Retenção: {error}", + "Failed to update SOLR settings: {error}": "Falha ao atualizar definições do SOLR: {error}", + "Failed to update favorite status": "Betg reussì d'actualisar il status da favorit", + "Failed to update schema configuration for {schema}: {error}": "Falha ao atualizar a configuração do esquema para {schema}: {error}", + "Failed to update schema properties": "Betg reussì d'actualisar las caracteristicas dal Schema", + "Failed to update view": "Betg reussì d'actualisar la vista", + "Failed to update view: {error}": "Falha ao atualizar a vista: {error}", + "Failed to update webhook": "Betg reussì d'actualisar il webhook", + "Failed to upload avatar": "Falha ao carregar o avatar", + "Failed to validate {schema}: {error}": "Falha ao validar {schema}: {error}", + "Failed to vectorize files: {error}": "Falha ao vetorizar ficheiros: {error}", + "Failed to warmup names cache: {error}": "Falha ao aquecer a cache de nomes: {error}", + "Federation": "Federação", + "Feedback recorded": "Il feedback è registrà", + "Fewer sources (1-3):": "Damain funtaunas (1-3):", + "Field": "Chomp", + "Fields to Display": "Campos a Apresentar", + "File": "Datoteca", + "File #{id}": "Ficheiro #{id}", + "File Chunk Prediction": "Prognosa da chunks da Datoteca", + "File Collection": "Collecziun da Datotecas", + "File Handling": "Tratamento de Ficheiros", + "File Location": "Localização do Ficheiro", + "File Management": "Administraziun da Datotecas", + "File Name": "Num da la Datoteca", + "File Path": "Percurs da la Datoteca", + "File Type Selection": "Selecziun dal tip da Datoteca", + "File Types to Process": "Tipos de Ficheiro a Processar", + "File Vectorization": "Vectorisaziun da Datotecas", + "File Warmup": "Prechauffament da Datotecas", + "File actions menu": "Menu d'acziuns da Datoteca", + "File anonymized successfully": "La Datoteca è vegnida anonimisada cun success", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "La vectorisaziun da chunks da Datoteca n'è anc betg implementada. Ils chunks èn pronts e memorisads, ma il servetsch da vectorisaziun è en svilup.", + "File collection not configured": "La collecziun da Datotecas n'è betg configurada", + "File discovery completed": "La scuverta da Datotecas è cumpletada", + "File extraction completed": "L'extracziun da la Datoteca è cumpletada", + "File extraction queued": "L'extracziun da la Datoteca è en la lingia d'spetga", + "File indexed successfully": "La Datoteca è vegnida indexada cun success", + "File is already anonymized": "La Datoteca è gia anonimisada", + "File name": "Nome do ficheiro", + "File not found": "La Datoteca n'è betg vegnida chattada", + "File settings saved successfully": "Definições de ficheiros guardadas com sucesso", + "File settings updated successfully": "Las configuraziuns da la Datoteca èn vegnidas actualisadas cun success", + "File sources": "Funtaunas da Datotecas", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vetorização de ficheiros concluída! {vectorized} fragmentos vetorizados de {files} ficheiros. {failed} falharam.", + "File vectorization configuration saved successfully": "La configuraziun da vectorisaziun da Datotecas è vegnida memorisada cun success", + "File vectorization started. Check the statistics section for progress.": "La vectorisaziun da Datotecas è cumenzada. Controllai la secziun da statistica per il progress.", + "File warmup completed": "Il prechauffament da Datotecas è cumpletà", + "Files": "Datotecas", + "Files added successfully": "Ficheiros adicionados com sucesso", + "Files with Completed Extraction:": "Datotecas cun extracziun cumpletada:", + "Files → fileCollection, Objects → objectCollection": "Datotecas → fileCollection, Objects → objectCollection", + "Files → filecollection, objects → objectcollection": "Ficheiros → filecollection, objetos → objectcollection", + "Filter Audit Trails": "Filtrar las colliaziuns d'audit", + "Filter Deleted Items": "Filtrar ils elements stizzads", + "Filter Objects": "Filtrar ils objects", + "Filter Properties": "Filtrar las caracteristicas", + "Filter Search Trails": "Filtrar las colliaziuns da tschertga", + "Filter Statistics": "Filtrar la statistica", + "Filter and analyze search trail entries": "Filtrar ed analisar las endataziuns da colliaziuns da tschertga", + "Filter and manage audit trail entries": "Filtrar ed administrar las endataziuns da colliaziuns d'audit", + "Filter and manage soft deleted items": "Filtrar ed administrar ils elements stizzads mollamain", + "Filter and search entities": "Filtrar e pesquisar entidades", + "Filter by object ID": "Filtrar tenor l'ID da l'object", + "Filter by search term": "Filtrar tenor il term da tschertga", + "Filter by type": "Filtrar por tipo", + "Filter by webhook": "Filtrar tenor il webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Las datas da filter èn vegnidas chargiadas automaticamain. Duvrai ils filters sutvart per rafinar Vossa tschertga.", + "Filter fields...": "Filtrar campos...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrar ils triggers da webhook tenor las caracteristicas dal payload (in per lingia, format: key: value)", + "Filtered": "Filtrà", + "Filters": "Filters", + "Finalizing setup...": "A finalizar a configuração...", + "Fireworks AI Chat Settings": "Configuraziuns da chat Fireworks AI", + "Fireworks AI Embedding Configuration": "Configuraziun d'embedding Fireworks AI", + "First": "Emprim", + "Fixed": "Fixà", + "For chat and retrieval-augmented generation": "Per chat e generaziun augmentada cun recuperaziun", + "For vector embeddings and semantic search": "Per embeddings da vectur e tschertga semantica", + "Format": "Format", + "Format Issue": "Problem da format", + "Format constraint is missing": "La restricziun da format manca", + "From (e.g. -7 days or 2025-01-01)": "De (por exemplo, -7 days ou 2025-01-01)", + "From Date": "Da la data", + "From date": "Da la data", + "Full data copied to clipboard": "Tut las datas èn vegnidas copiadas en l'archivet provisoric", + "Function Tools": "Ferramentas de Função", + "General Issue": "Problem general", + "Generate recommendations and confidence scores": "Generar recumandaziuns e punctuaziuns da confidenza", + "Generate report": "Gerar relatório", + "Generate the verantwoordingsdocument": "Gerar o verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generar immediatamain vecturs cur che novs objects vegnan creads", + "Generated: {time}": "Gerado: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Obtenha informações em tempo real sobre a saúde dos dados da sua organização, focando-se em registos, definições de esquema e armazenamento e utilização de objetos.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Obtenha informações em tempo real sobre a saúde dos dados da sua organização, tendências de crescimento e métricas de qualidade.", + "GitHub Personal Access Token": "Token de Acesso Pessoal do GitHub", + "GitHub token is valid": "Il token da GitHub è valaivel", + "GitHub token is valid! Username: {username}": "O token do GitHub é válido! Nome de utilizador: {username}", + "GitHub token removed successfully": "Il token da GitHub è vegnì allontanà cun success", + "GitHub token saved successfully": "Il token da GitHub è vegnì memorisà cun success", + "GitHub token test failed: {message}": "O teste do token do GitHub falhou: {message}", + "GitLab Personal Access Token": "Token de Acesso Pessoal do GitLab", + "GitLab URL saved successfully": "L'URL da GitLab è vegnì memorisà cun success", + "GitLab token is valid": "Il token da GitLab è valaivel", + "GitLab token is valid! Username: {username}": "O token do GitLab é válido! Nome de utilizador: {username}", + "GitLab token saved successfully": "Il token da GitLab è vegnì memorisà cun success", + "GitLab token test failed: {message}": "O teste do token do GitLab falhou: {message}", + "Go to linked Register": "Ir para o Registo associado", + "Go to linked Schema": "Ir para o Esquema associado", + "Go to organisation": "Ir para a organização", + "Grant additional permissions beyond standard CRUD operations": "Conceder permissões adicionais além das operações CRUD padrão", + "Group Quota": "Quota do Grupo", + "Groups": "Grupos", + "HTML preview": "Pré-visualização HTML", + "HTTP Method": "Metoda HTTP", + "HTTP method used to send webhook requests": "Metoda HTTP duvrada per trametter dumondas da webhook", + "Headers": "Headers", + "Health": "Sanadad", + "Heartbeat successful - connection kept alive": "Heartbeat reussì - la colliaziun vegn mantegnida activa", + "Helpful": "Util", + "Here you can configure the details for various connections.": "Aqui pode configurar os detalhes de várias ligações.", + "Here you can configure the details for your organisation.": "Aqui pode configurar os detalhes da sua organização.", + "Hide": "Ocultar", + "Hide Filters": "Zuppentar ils filters", + "Hide in collection view": "Ocultar na vista de coleção", + "Hide in forms": "Zuppentar en ils formulars", + "Hide in list view": "Zuppentar en la vista da glista", + "High": "Aut", + "High Confidence": "Auta confidenza", + "Host": "Host", + "Hourly": "Mintga ura", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Quant profund traversar las caracteristicas d'object inglubadas (1-20). Valurs pli auts chattan dapli detagls ma augmentan la grondezza dal vectur.", + "How often to check for updates (1-168 hours)": "Quant savens controllar per actualisaziuns (1-168 uras)", + "How to handle retries for failed webhook deliveries": "Co tractar las empruvas per furniziuns da webhook fallidas", + "IBANs": "IBANs", + "ID": "ID", + "ID of input mapping (optional)": "ID do mapeamento de entrada (opcional)", + "ID of output mapping (optional)": "ID do mapeamento de saída (opcional)", + "ID of the target resource": "ID do recurso de destino", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Las limitas da tariffa IP èn vegnidas stizzadas cun success", + "Identify properties not in the schema": "Identifitgar las caracteristicas che n'èn betg en il Schema", + "Immutable": "Immutabel", + "Import": "Importar", + "Import Configuration": "Importar Configuração", + "Import Data into Register": "Importar Dados para o Registo", + "Import successful": "L'import è reussì", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importe o modelo report-bundle.json para obter o registo `reports` e, em seguida, crie o seu primeiro painel através da interface de objetos padrão. Os painéis declaram os seus widgets em JSON e o renderizador alimenta cada widget com dados de agregação em tempo real.", + "Improved Property": "Caracteristica meglierada", + "In use": "En diever", + "Inactive": "Inactiv", + "Inactive organisations cannot be used": "As organizações inativas não podem ser utilizadas", + "Include IDs and names of related objects for better contextual search": "Includer IDs e nums dals objects relaziunads per ina meglra tschertga contextuala", + "Include ids and names of related objects for better contextual search": "Incluir ids e nomes de objetos relacionados para melhor pesquisa contextual", + "Include related object references": "Includer las referenzas dals objects relaziunads", + "Include schema and register metadata": "Includer las metadatas dal Schema e dal Register", + "Index": "Índice", + "Information about the current OpenRegister installation": "Informações sobre a instalação atual do OpenRegister", + "Inherited from register": "Herdado do registo", + "Inherits permissions from register": "Herda permissões do registo", + "Initialization failed": "L'inizialisaziun è fallida", + "Initialize Project": "Inizialisar il project", + "Initializing SOLR setup...": "A inicializar a configuração do SOLR...", + "Input Mapping": "Mapeamento de Entrada", + "Inspect Fields": "Inspecziunar ils chomps", + "Inspect Index": "Inspecziunar l'index", + "Inspect SOLR Index": "Inspecionar Índice SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installai l'applicaziun notify_push da l'App Store da Nextcloud per activar las actualisaziuns en temp real.", + "Integrations": "Integrações", + "Internet location (URL) of this installation": "Localização na Internet (URL) desta instalação", + "Interval": "Intervalo", + "Invalid": "Nunvalaivel", + "Invalid batch size. Must be between 1 and 5000": "Grondezza da batch nunvalaivla. Sto esser tranter 1 e 5000", + "Invalid feedback type": "Tip da feedback nunvalaivel", + "Invalid field name provided": "Num da chomp nunvalaivel furnì", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects nunvalaivel. Sto esser 0 (tut) u in dumber positiv", + "Invalid:": "Nunvalaivel:", + "Invite Users": "Convidar Utilizadores", + "Invited Users": "Utilizadores Convidados", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Resultados de Inzage", + "Issue": "Problem", + "Items per page:": "Elements per pagina:", + "Just now": "Pir ussa", + "Key": "Chave", + "LLM Configuration": "Configuraziun LLM", + "LLM actions menu": "Menu d'acziuns LLM", + "LLM configuration saved successfully": "La configuraziun LLM è vegnida memorisada cun success", + "LLM features disabled": "Las funcziuns LLM èn deactivadas", + "LLM features enabled": "Las funcziuns LLM èn activadas", + "LLM must be enabled with an embedding provider configured": "LLM sto esser activà cun in purschider d'embedding configurà", + "LLM settings saved successfully": "Definições do LLM guardadas com sucesso", + "LLM settings updated successfully": "Las configuraziuns LLM èn vegnidas actualisadas cun success", + "Label": "Etiqueta", + "Labels": "Etiquetas", + "Languages": "Idiomas", + "Last": "Ultim", + "Last Triggered": "Davos declanschà", + "Leave empty to allow all users access": "Deixe vazio para permitir o acesso a todos os utilizadores", + "Leave off for auto-detection from property format.": "Deixe desativado para deteção automática a partir do formato da propriedade.", + "Left organisation successfully": "Saiu da organização com sucesso", + "Legal basis *": "Base legal *", + "Length Range:": "Perioda da lunghezza:", + "Linear": "Linear", + "Link": "Associar", + "Link an email from the Mail app sidebar to associate it with this object.": "Associe um email a partir da barra lateral da aplicação Mail para o ligar a este objeto.", + "Link an object to see it here.": "Associe um objeto para o ver aqui.", + "Link another object": "Associar outro objeto", + "Link existing event": "Associar evento existente", + "Link removed": "Associação removida", + "Link to Object": "Associar a Objeto", + "Link to {name}": "Associar a {name}", + "Linked Objects": "Objetos Associados", + "Linked by {user}": "Associado por {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Emails, eventos de calendário, contactos e cartões do Deck associados aparecerão aqui.", + "Linked to {name}": "Associado a {name}", + "Load Advanced Filters": "Chargiar ils filters avanzads", + "Load advanced filters with live data from your search index": "Chargiar ils filters avanzads cun datas en direct da Voss index da tschertga", + "Load more": "Carregar mais", + "Load view": "Chargiar la vista", + "Loading API tokens...": "A carregar tokens da API...", + "Loading ConfigSets...": "Chargiar ils ConfigSets...", + "Loading LLM configuration...": "A carregar a configuração do LLM...", + "Loading RBAC settings...": "A carregar definições de RBAC...", + "Loading activity...": "A carregar atividade...", + "Loading advanced filters...": "Chargiar ils filters avanzads...", + "Loading agents...": "Chargiar ils agents...", + "Loading application details...": "Chargiar ils detagls da l'applicaziun...", + "Loading applications...": "Chargiar las applicaziuns...", + "Loading archived conversations...": "Chargiar las conversaziuns archivadas...", + "Loading audit trails...": "Chargiar las colliaziuns d'audit...", + "Loading available tools...": "A carregar ferramentas disponíveis...", + "Loading cache statistics...": "A carregar estatísticas da cache...", + "Loading collections...": "Chargiar las collecziuns...", + "Loading configurations...": "Chargiar las configuraziuns...", + "Loading conversation...": "Chargiar la conversaziun...", + "Loading conversations...": "Chargiar las conversaziuns...", + "Loading deleted items...": "Chargiar ils elements stizzads...", + "Loading endpoints": "A carregar endpoints", + "Loading entities...": "A carregar entidades...", + "Loading events...": "Chargiar ils eveniments...", + "Loading filter data...": "Chargiar las datas da filter...", + "Loading groups...": "Chargiar las gruppas...", + "Loading linked objects...": "A carregar objetos associados...", + "Loading log details...": "Chargiar ils detagls dal protocol...", + "Loading multitenancy settings...": "A carregar definições de multilocação...", + "Loading n8n configuration...": "A carregar a configuração do n8n...", + "Loading permission matrix...": "A carregar matriz de permissões...", + "Loading preferences...": "A carregar preferências...", + "Loading register data...": "A carregar dados do registo...", + "Loading registers...": "Chargiar ils Registers...", + "Loading retention settings...": "A carregar definições de retenção...", + "Loading schemas...": "Chargiar ils Schemas...", + "Loading search configuration...": "A carregar a configuração de pesquisa...", + "Loading search trails...": "Chargiar las colliaziuns da tschertga...", + "Loading sources...": "Chargiar las funtaunas...", + "Loading statistics...": "Chargiar la statistica...", + "Loading text extraction settings...": "A carregar definições de extração de texto...", + "Loading tokens...": "A carregar tokens...", + "Loading user groups...": "A carregar grupos de utilizadores...", + "Loading users...": "Chargiar ils utilisaders...", + "Loading views...": "Chargiar las vistas...", + "Loading...": "Chargiar...", + "Local": "Local", + "Local Version": "Versiun locala", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Localize todos os objetos que referenciam um titular de dados (Art 15 inzage), pré-visualize uma eliminação (Art 17 vergetelheid) ou exporte os seus dados (Art 20 portabiliteit).", + "Location": "Localização", + "Location Field": "Campo de Localização", + "Locations": "Localizações", + "Locked": "Bloccà", + "Locked:": "Bloccà:", + "Login successful": "L'annunzia è reussida", + "Logs": "Protocols", + "Low": "Bass", + "Low Confidence": "Bassa confidenza", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Números mais baixos aparecem primeiro na barra lateral de filtros. Deixe vazio para ordenação automática.", + "Lucene Version:": "Versiun da Lucene:", + "Magic table sync completed for {schema}": "Sincronização da tabela mágica concluída para {schema}", + "Magic table synchronized successfully": "La tabella magica è vegnida sincronisada cun success", + "Mail Integration": "Integração com Mail", + "Mail integration is not available": "A integração com Mail não está disponível", + "Make this installation discoverable within the federation network.": "Tornar esta instalação detetável dentro da rede de federação.", + "Manage Organisation Roles": "Administrar ils rolls da l'organisaziun", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Administrar las collecziuns SOLR (deposits da datas) ed attribuir las per objects e Datotecas.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Administrar ils ConfigSets SOLR (models da configuraziun) per Vossas collecziuns.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Faça a gestão das coleções SOLR (armazenamentos de dados) e atribua-as a objetos e ficheiros.", + "Manage SOLR configsets (configuration templates) for your collections.": "Faça a gestão dos configsets SOLR (modelos de configuração) para as suas coleções.", + "Manage and configure agents for automated tasks": "Administrar e configurar agents per incumbensas automatisadas", + "Manage and monitor file text extraction status": "Administrar e survegliar il status da l'extracziun da text da Datotecas", + "Manage and restore soft deleted items from your registers": "Administrar e restituir ils elements stizzads mollamain da Voss Registers", + "Manage and view detected entities from files and objects": "Administrar e vesair las entitads identifitgadas da Datotecas ed objects", + "Manage document templates and themes": "Administrar ils models e thaibers da documents", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Administrar ils process da lavur n8n per l'automatisaziun da OpenRegister. Ils process da lavur vegnan memorisads en il project configurà.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Faça a gestão de atividades de tratamento, execute pedidos de acesso de titulares de dados e audite a conformidade ao abrigo do GDPR da UE / AVG neerlandês.", + "Manage webhooks for event-driven integrations": "Administrar ils webhooks per integraziuns manadas dad eveniments", + "Manage your account settings, security, and personal data.": "Faça a gestão das definições da sua conta, segurança e dados pessoais.", + "Manage your applications and modules": "Administrar Vossas applicaziuns e moduls", + "Manage your chat conversations": "Faça a gestão das suas conversas", + "Manage your data registers and their configurations": "Administrar Voss Registers da datas e lur configuraziuns", + "Manage your data schemas and their properties": "Administrar Voss Schemas da datas e lur caracteristicas", + "Manage your data sources and their configurations": "Administrar Vossas funtaunas da datas e lur configuraziuns", + "Manage your organisations and switch between them": "Faça a gestão das suas organizações e alterne entre elas", + "Manage your saved search configurations": "Administrar Vossas configuraziuns da tschertga memorisadas", + "Manage your system configurations and settings": "Administrar Vossas configuraziuns e parameters dal sistem", + "Managed": "Administrà", + "Mappings": "Mapeamentos", + "Mass validation completed successfully": "Validação em massa concluída com sucesso", + "Mass validation failed: {error}": "A validação em massa falhou: {error}", + "Max Files (0 = all)": "Max Datotecas (0 = tut)", + "Max Length": "Lunghezza max", + "Max Length:": "Lunghezza max:", + "Max Objects (0 = all)": "Max objects (0 = tut)", + "Max Retries": "Max empruvas", + "Max Shards/Node": "Max Shards/Nodi", + "Max Tokens": "Máximo de Tokens", + "Max execution time (ms)": "Temp d'execuziun max (ms)", + "Max ms": "Max ms", + "Max result count": "Dumber max da resultats", + "Max results": "Resultats max", + "Maximum": "Maximum", + "Maximum File Size (MB)": "Tamanho Máximo de Ficheiro (MB)", + "Maximum Nesting Depth": "Profunditad maximala d'inglubaziun", + "Maximum length": "Comprimento máximo", + "Maximum length constraint is missing": "La restricziun da lunghezza maximala manca", + "Maximum length is too restrictive": "La lunghezza maximala è memia restrictiva", + "Maximum number of files to process. Set to 0 to process all files.": "Dumber maximal da Datotecas dad elavurar. Mettai sin 0 per elavurar tut las Datotecas.", + "Maximum number of items": "Número máximo de itens", + "Maximum number of objects to process. Set to 0 to process all objects.": "Dumber maximal d'objects dad elavurar. Mettai sin 0 per elavurar tut ils objects.", + "Maximum number of retry attempts for failed deliveries": "Dumber maximal d'empruvas per furniziuns fallidas", + "Maximum time to wait before committing changes": "Temp maximal dad spetgar avant da cummetter las midadas", + "Maximum tokens to generate": "Dumber maximal da tokens da generar", + "Maximum value": "Valor máximo", + "Maximum value constraint is missing": "La restricziun da valur maximala manca", + "Maximum value is too restrictive": "La valur maximala è memia restrictiva", + "Medium": "Mesaun", + "Medium Confidence": "Confidenza mesauna", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Dumondas mesaunas: tschertgas cun in tschert filtrar u plirs parameters (p.ex. periodas da data, Registers/Schemas specifics)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Consultas médias: pesquisas com alguma filtragem ou múltiplos parâmetros (por exemplo, intervalos de datas, registos/esquemas específicos)", + "Member removed successfully": "Membro removido com sucesso", + "Members": "Membros", + "Members ({count})": "Membros ({count})", + "Members of selected groups can access this view": "Ils commembers da las gruppas tschernidas pon acceder a questa vista", + "Members:": "Commembers:", + "Memory prediction calculated successfully": "La prognosa da memoria è vegnida calculada cun success", + "Message content is required": "Il cuntegn dal messadi è obligatoric", + "Message does not belong to this conversation": "Il messadi n'appartegna betg a questa conversaziun", + "Message not found": "Il messadi n'è betg vegnì chattà", + "Metadata": "Metadados", + "Metadata Filters": "Filters da metadatas", + "Method": "Metoda", + "Method*": "Método*", + "Method: {method}": "Método: {method}", + "Min Length": "Lunghezza min", + "Min execution time (ms)": "Temp d'execuziun min (ms)", + "Min ms": "Min ms", + "Min result count": "Dumber min da resultats", + "Min results": "Resultats min", + "Minimum": "Minimum", + "Minimum length": "Comprimento mínimo", + "Minimum number of items": "Número mínimo de itens", + "Minimum value": "Valor mínimo", + "Minimum value constraint is missing": "La restricziun da valur minimala manca", + "Minimum value is too restrictive": "La valur minimala è memia restrictiva", + "Missing conversation or agentUuid": "Conversaziun u agentUuid mancant", + "Missing conversationId": "conversationId mancant", + "Missing message": "Messadi mancant", + "Missing object fields created successfully": "Ils chomps d'object mancants èn vegnids creads cun success", + "Mode:": "Modus:", + "Model": "Model", + "Model Name": "Num dal model", + "Modified": "Modifitgà", + "Monthly": "Mintga mais", + "More sources (10-20):": "Dapli funtaunas (10-20):", + "Most Active Objects": "Ils objects ils pli activs", + "Multiple Of": "Multipel da", + "Multiple of": "Múltiplo de", + "Multiple search trail deletion not implemented yet": "Il stizzar da pliras colliaziuns da tschertga n'è anc betg implementà", + "Multitenancy": "Multilocação", + "Multitenancy settings updated successfully": "Definições de multilocação atualizadas com sucesso", + "My Account": "A Minha Conta", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Num", + "Name *": "Nome *", + "Name*": "Nome*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Cache de nomes aquecida com sucesso: {count} nomes carregados em {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Cache de nomes aquecida com sucesso: {count} nomes carregados em {time}. {message}", + "Names cache warmup completed": "Il prechauffament dal cache da nums è cumpletà", + "Naming pattern for copies:": "Padrão de nomenclatura para cópias:", + "New Collection Name": "Num da la nova collecziun", + "New Configuration": "Nova Configuração", + "New Conversation": "Nova conversaziun", + "New Properties": "Novas caracteristicas", + "New Property": "Nova caracteristica", + "New User Group": "Novo Grupo de Utilizadores", + "New Value": "Nova valur", + "New activity": "Nova atividade", + "New password": "Nova palavra-passe", + "New processing activity": "Nova atividade de tratamento", + "New users without specific organisation membership will be automatically added to this organisation": "Novs utilisaders senza commembranza specifica d'organisaziun vegnan agiuntads automaticamain a questa organisaziun", + "New verwerkingsactiviteit": "Nova verwerkingsactiviteit", + "Next": "Proxim", + "Nextcloud Groups": "Grupos do Nextcloud", + "No": "Na", + "No API tokens.": "Sem tokens da API.", + "No ConfigSets found": "Nagins ConfigSets chattads", + "No Deck cards linked to this object": "Nenhum cartão do Deck associado a este objeto", + "No GitHub token configured": "Nagin token da GitHub configurà", + "No GitHub token provided": "Nagin token da GitHub furnì", + "No GitLab token provided": "Nagin token da GitLab furnì", + "No Organisation": "Nagina organisaziun", + "No active filters": "Nagins filters activs", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Não há objetos ativos para eliminar de forma reversível no esquema {schema}. Utilize \"Eliminar Permanentemente\" para remover objetos eliminados de forma reversível.", + "No activity data available": "Naginas datas d'activitad disponiblas", + "No activity found.": "Nenhuma atividade encontrada.", + "No agents available": "Nagins agents disponibels", + "No agents found": "Nagins agents chattads", + "No app store cache exists yet": "Anc nagin cache da l'App Store exista", + "No applications are available.": "Naginas applicaziuns èn disponiblas.", + "No applications found": "Naginas applicaziuns chattadas", + "No approval steps for this object.": "Sem etapas de aprovação para este objeto.", + "No archived conversations": "Naginas conversaziuns archivadas", + "No audit trail entries found": "Naginas endataziuns da colliaziuns d'audit chattadas", + "No changes recorded": "Naginas midadas registradas", + "No chunks to vectorize": "Nagins chunks da vectorisar", + "No collections found": "Naginas collecziuns chattadas", + "No configuration data": "Naginas datas da configuraziun", + "No configuration selected": "Nenhuma configuração selecionada", + "No configurations are available.": "Naginas configuraziuns èn disponiblas.", + "No configurations found": "Naginas configuraziuns chattadas", + "No configurations found for this application.": "Naginas configuraziuns chattadas per questa applicaziun.", + "No contacts linked to this object": "Nenhum contacto associado a este objeto", + "No conversations yet. Create a new one to get started!": "Anc naginas conversaziuns. Creai ina nova per cumenzar!", + "No dashboards yet": "Ainda não há painéis", + "No data available": "Naginas datas disponiblas", + "No data available for chart": "Naginas datas disponiblas per il diagram", + "No deleted items found": "Nagins elements stizzads chattads", + "No deletion data available": "Naginas datas da stizzada disponiblas", + "No description found": "Nagina descripziun chattada", + "No description provided": "Nagina descripziun furnida", + "No emails linked to this object": "Nenhum e-mail associado a este objeto", + "No endpoints defined": "Nenhum endpoint definido", + "No entities detected for this email.": "Não foram detetadas entidades para este e-mail.", + "No entities detected in this file. Run text extraction first.": "Naginas entitads identifitgadas en questa Datoteca. Exequì l'extracziun da text avant.", + "No entities found": "Naginas entitads chattadas", + "No entities have been detected yet": "Anc naginas entitads èn vegnidas identifitgadas", + "No events found": "Nagins eveniments chattads", + "No events linked to this object": "Nenhum evento associado a este objeto", + "No expired audit trails found to clear": "Naginas colliaziuns d'audit scadidas chattadas da stizzar", + "No expired search trails found to clear": "Naginas colliaziuns da tschertga scadidas chattadas da stizzar", + "No extraction data available for this file": "Naginas datas d'extracziun disponiblas per questa Datoteca", + "No facetable fields available. Select a register and schema to see available filters.": "Nagins chomps facettabels disponibels. Tscherni in Register ed in Schema per vesair ils filters disponibels.", + "No files found": "Naginas Datotecas chattadas", + "No files have been extracted yet": "Anc naginas Datotecas èn vegnidas extratgas", + "No files selected": "Nenhum ficheiro selecionado", + "No files to process": "Naginas Datotecas dad elavurar", + "No files to reindex": "Naginas Datotecas da reindexar", + "No filters are currently active. This will delete ALL audit trail entries!": "Actualmain n'èn nagins filters activs. Quai stizza TUT las endataziuns da colliaziuns d'audit!", + "No groups found. Try a different search.": "Não foram encontrados grupos. Experimente uma pesquisa diferente.", + "No label": "Sem etiqueta", + "No labels": "Sem etiquetas", + "No linked objects": "Nenhum objeto associado", + "No log entries found": "Naginas endataziuns da protocol chattadas", + "No logs are available for this configuration.": "Nagins protocols èn disponibels per questa configuraziun.", + "No logs are available for this source.": "Nagins protocols èn disponibels per questa funtauna.", + "No logs found": "Nagins protocols chattads", + "No matches": "Sem correspondências", + "No mismatched fields found - SOLR schema is properly configured": "Nagins chomps betg correspundents chattads - il Schema SOLR è configurà correctamain", + "No objects are currently selected for deletion.": "Não há objetos selecionados para eliminação de momento.", + "No objects are currently selected for permanent deletion.": "Não há objetos selecionados para eliminação permanente de momento.", + "No objects are currently selected for restoration.": "Não há objetos selecionados para restauro de momento.", + "No objects found": "Nagins objects chattads", + "No objects found to vectorize": "Nagins objects chattads da vectorisar", + "No objects linked to this email": "Nenhum objeto associado a este e-mail", + "No objects selected": "Nenhum objeto selecionado", + "No objects selected for deletion": "Nagins objects tschernids per la stizzada", + "No objects selected for restoration": "Nenhum objeto selecionado para restauro", + "No objects to delete": "Nagins objects da stizzar", + "No objects to delete for schema {schema}": "Não há objetos para eliminar para o esquema {schema}", + "No objects were permanently deleted": "Nagins objects èn vegnids stizzads permanentamain", + "No organisations found": "Não foram encontradas organizações", + "No personal data was found for this subject identifier.": "Não foram encontrados dados pessoais para este identificador de titular.", + "No processing activities yet": "Ainda não há atividades de tratamento", + "No properties found": "Naginas caracteristicas chattadas", + "No properties match your filters.": "Naginas caracteristicas correspundan a Voss filters.", + "No purge date set": "Nagina data da purgar fixada", + "No register data available": "Naginas datas da Register disponiblas", + "No register objects reference this file": "Nagins objects dal Register fan referenza a questa Datoteca", + "No registers found": "Nagins Registers chattads", + "No registers found for this application.": "Nagins Registers chattads per questa applicaziun.", + "No registers found. Create a register to configure permissions.": "Não foram encontrados registos. Crie um registo para configurar as permissões.", + "No related cases found for this sender": "Não foram encontrados processos relacionados para este remetente", + "No related entities": "Sem entidades relacionadas", + "No relations found": "Naginas relaziuns chattadas", + "No request body available": "Nagin corp da dumonda disponibel", + "No response body available": "Nagin corp da resposta disponibel", + "No saved views yet. Create one in the Search tab!": "Anc naginas vistas memorisadas. Creai ina en il tab da tschertga!", + "No saved views yet. create one in the search tab!": "Ainda não há vistas guardadas. crie uma no separador de pesquisa!", + "No scan run yet": "Ainda não foi executada nenhuma análise", + "No schema data available": "Naginas datas da Schema disponiblas", + "No schema selected for exploration": "Nagin Schema tschernì per l'exploraziun", + "No schemas configured for mail linking.": "Não há esquemas configurados para associação de correio.", + "No schemas found": "Nagins Schemas chattads", + "No schemas found for this application.": "Nagins Schemas chattads per questa applicaziun.", + "No search terms data available": "Naginas datas da terms da tschertga disponiblas", + "No search trail entries found": "Naginas endataziuns da colliaziuns da tschertga chattadas", + "No sources are available.": "Naginas funtaunas èn disponiblas.", + "No sources found": "Naginas funtaunas chattadas", + "No synchronizations found": "Não foram encontradas sincronizações", + "No templates found": "Nagins models chattads", + "No templates have been created yet": "Anc nagins models èn vegnids creads", + "No tools available. Tools can be registered by installed apps.": "Não há ferramentas disponíveis. As ferramentas podem ser registadas por aplicações instaladas.", + "No users in this organisation.": "Não há utilizadores nesta organização.", + "No valid magic-mapped register+schema combinations found": "Naginas cumbinaziuns valaivlas Register+Schema magic-mapped chattadas", + "No views found. Create views first before configuring vectorization.": "Naginas vistas chattadas. Creai vistas avant da configurar la vectorisaziun.", + "No views match your search": "Naginas vistas correspundan a Vossa tschertga", + "No webhooks found": "Nagins webhooks chattads", + "No webhooks have been configured yet": "Anc nagins webhooks èn vegnids configurads", + "No workflows found in this project. Create workflows in the n8n editor.": "Nagins process da lavur chattads en quest project. Creai process da lavur en l'editur n8n.", + "None": "Nagin", + "Not Found": "Betg chattà", + "Not extracted": "Betg extratg", + "Not helpful": "Betg util", + "Not in use": "Betg en diever", + "Note:": "Nota:", + "Notifications": "Notificações", + "Number Constraints": "Restricziuns da dumber", + "Number of Sources": "Número de Fontes", + "Number of chunks to vectorize in one API call (1-100).": "Dumber da chunks da vectorisar en ina clamada API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Dumber da chunks da vectorisar en ina clamada API. Pli aut = pli svelt ma dapli memoria. Recumandà: 10-50.", + "Number of objects to process in each batch (1-5000).": "Dumber d'objects dad elavurar en mintga batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Dumber d'objects da vectorisar en ina clamada API. Pli aut = pli svelt ma dapli memoria. Recumandà: 10-50.", + "Numeric Range:": "Perioda numerica:", + "Object": "Object", + "Object #{id}": "Objeto #{id}", + "Object Collection": "Collecziun d'objects", + "Object Configuration:": "Configuração de Objeto:", + "Object Count Prediction": "Prognosa dal dumber d'objects", + "Object Extraction Mode": "Modo de Extração de Objetos", + "Object ID": "ID da l'object", + "Object Management": "Administraziun dals objects", + "Object Text Extraction": "Extração de Texto de Objetos", + "Object Vectorization": "Vectorisaziun d'objects", + "Object Warmup": "Prechauffament d'objects", + "Object changes in owned objects": "Alterações de objetos em objetos próprios", + "Object collection not configured": "La collecziun d'objects n'è betg configurada", + "Object metadata incomplete for file upload": "Metadados do objeto incompletos para carregamento de ficheiro", + "Object not found": "L'object n'è betg vegnì chattà", + "Object permanently deleted": "L'object è vegnì stizzà permanentamain", + "Object restored successfully": "L'object è vegnì restituì cun success", + "Object settings updated successfully": "Las configuraziuns da l'object èn vegnidas actualisadas cun success", + "Object sources": "Funtaunas d'objects", + "Object successfully deleted": "Objeto eliminado com sucesso", + "Object successfully modified": "Objeto modificado com sucesso", + "Object unlocked successfully": "L'object è vegnì debloccà cun success", + "Object vectorization configuration saved successfully": "La configuraziun da vectorisaziun d'objects è vegnida memorisada cun success", + "Object vectorized successfully": "L'object è vegnì vectorisà cun success", + "Object:": "Object:", + "Objects": "Objects", + "Objects Analyzed": "Objects analisads", + "Objects Distribution": "Distribuziun dals objects", + "Objects being analyzed": "Objects che vegnan analisads", + "Objects by Register": "Objects tenor Register", + "Objects by Schema": "Objects tenor Schema", + "Objects deletion completed for {schema}": "Eliminação de objetos concluída para {schema}", + "Objects deletion completed successfully": "La stizzada dals objects è vegnida cumpletada cun success", + "Objects in schema": "Objects en il Schema", + "Objects successfully deleted": "Objetos eliminados com sucesso", + "Objects to Process:": "Objects dad elavurar:", + "Objects to be analyzed": "Objects che vegnan analisads", + "Objects to be deleted": "Objects che vegnan stizzads", + "Objects to be validated": "Objects che vegnan validads", + "Objects will be serialized as JSON text before vectorization": "Ils objects vegnan serialisads sco text JSON avant la vectorisaziun", + "Objects will be soft deleted and moved to the": "Os objetos serão eliminados de forma reversível e movidos para o", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Ils objects vegnan stizzads mollamain (marcads sco stizzads ma mantegnids en la banca da datas). Els pon vegnir recuperads pli tard sche necessari.", + "Old Value": "Veglia valur", + "Oldest Item (days)": "L'element il pli vegl (dis)", + "Ollama Chat Settings": "Configuraziuns da chat Ollama", + "Ollama Embedding Configuration": "Configuraziun d'embedding Ollama", + "Ollama URL": "URL da Ollama", + "Only With Changes": "Mo cun midadas", + "Only members of selected groups can access this organisation": "Apenas os membros dos grupos selecionados podem aceder a esta organização", + "Open Nextcloud App Store": "Avrir l'App Store da Nextcloud", + "Open OpenRegister sidebar": "Abrir a barra lateral do OpenRegister", + "Open Register Objects": "Avrir ils objects da Register", + "Open folder": "Abrir pasta", + "Open in OpenRegister": "Abrir no OpenRegister", + "Open n8n Editor": "Avrir l'editur n8n", + "OpenAI Chat Settings": "Configuraziuns da chat OpenAI", + "OpenAI Embedding Configuration": "Configuraziun d'embedding OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Configuraziuns da OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Painéis definidos pelo operador e relatórios agendados. Cada painel é um objeto de primeira classe no registo `reports`; os widgets são declarados no array `widgets` do painel e renderizados em tempo real a partir de agregações / GraphQL.", + "Optimizing search performance...": "A otimizar o desempenho da pesquisa...", + "Optional URL-friendly identifier": "Identificador opcional compatível com URL", + "Optional description for this facet": "Descrição opcional para esta faceta", + "Optional end date property": "Propriedade opcional de data de fim", + "Optional event description template": "Modelo opcional de descrição de evento", + "Optional location property": "Propriedade opcional de localização", + "Optional webhook secret for signature verification": "Secret da webhook opziunal per la verificaziun da la signatura", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Opcional: Adicione parâmetros de consulta para filtrar o esquema referenciado (por exemplo, status=active&type=public)", + "Or": "Ou", + "Order": "Urden", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Etiquetas de idioma BCP 47 ordenadas. O primeiro idioma é o predefinido do registo e determina o recurso de reserva Accept-Language para propriedades traduzíveis.", + "Organisation": "Organisaziun", + "Organisation Configuration": "Configuração da Organização", + "Organisation ID": "ID da l'organisaziun", + "Organisation Members": "Commembers da l'organisaziun", + "Organisation Statistics": "Statistica da l'organisaziun", + "Organisation created successfully": "L'organisaziun è vegnida creada cun success", + "Organisation membership changes": "Alterações de adesão à organização", + "Organisation name is required": "O nome da organização é obrigatório", + "Organisation settings saved successfully": "Las configuraziuns da l'organisaziun èn vegnidas memorisadas cun success", + "Organisation successfully created": "Organização criada com sucesso", + "Organisation successfully updated": "Organização atualizada com sucesso", + "Organisational measures": "Medidas organizacionais", + "Organisations": "Organisaziuns", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organisaziun", + "Organization ID (Optional)": "ID d'organisaziun (opziunal)", + "Organizations": "Organizações", + "Orphaned Items": "Elements orfens", + "Other": "Outro", + "Output Mapping": "Mapeamento de Saída", + "Overview": "Visão Geral", + "Owner": "Possessur", + "Owner:": "Possessur:", + "PDF": "PDF", + "PII hits": "ocorrências de PII", + "PII rows": "linhas de PII", + "Page {current} of {total}": "Página {current} de {total}", + "Parallel Mode (Faster, more resource intensive)": "Modus parallel (pli svelt, dapli intensiv en resursas)", + "Parallel:": "Parallel:", + "Password": "Pled-clav", + "Password changes are not supported by your authentication provider.": "As alterações de palavra-passe não são suportadas pelo seu fornecedor de autenticação.", + "Password for SOLR authentication (optional)": "Pled-clav per l'autentificaziun SOLR (opziunal)", + "Password updated successfully": "Palavra-passe atualizada com sucesso", + "Path": "Percurs", + "Path in repository": "Percurs en il repositori", + "Path where the configuration file will be saved in the repository": "Percurs nua che la Datoteca da configuraziun vegn memorisada en il repositori", + "Path where the register OAS file will be saved in the repository": "Percurs nua che la Datoteca OAS dal Register vegn memorisada en il repositori", + "Pattern (regex)": "Model (regex)", + "Pattern Issue": "Problem da model", + "Pattern constraint is missing": "La restricziun da model manca", + "Patterns:": "Models:", + "Pending": "Pendent", + "Permanent Delete Mode": "Modus da stizzar permanent", + "Permanently Delete": "Stizzar permanentamain", + "Permanently Delete ({count})": "Eliminar Permanentemente ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Eliminar permanentemente todos os {count} objetos eliminados de forma reversível. Esta ação não pode ser anulada!", + "Permanently delete already soft-deleted objects": "Stizzar permanentamain ils objects gia stizzads mollamain", + "Permanently delete this conversation?": "Stizzar permanentamain questa conversaziun?", + "Permission Matrix": "Matriz de Permissões", + "Person": "Persuna", + "Personal Data": "Datas persunalas", + "Personal Data Export": "Exportação de Dados Pessoais", + "Persons": "Pessoas", + "Phone": "Telefon", + "Phone Numbers": "Números de Telefone", + "Photos": "Fotografias", + "Please create an agent in the": "Creai per plaschair in agent en il", + "Please select a user": "Selecione um utilizador", + "Please select an agent to continue": "Tscherni per plaschair in agent per cuntinuar", + "Please select an organisation": "Selecione uma organização", + "Please select at least one item to import": "Selecione pelo menos um item para importar", + "Please select registers or schemas first": "Selecione primeiro registos ou esquemas", + "Please select which register and schema to use for the new object": "Tscherni per plaschair qual Register e Schema duvrar per il nov object", + "Please try again later.": "Empruvai per plaschair pli tard danovamain.", + "Please wait while we fetch your agents.": "Spetgai per plaschair entant che nus retschavain Voss agents.", + "Please wait while we fetch your applications.": "Spetgai per plaschair entant che nus retschavain Vossas applicaziuns.", + "Please wait while we fetch your configurations.": "Spetgai per plaschair entant che nus retschavain Vossas configuraziuns.", + "Please wait while we fetch your deleted items.": "Spetgai per plaschair entant che nus retschavain Voss elements stizzads.", + "Please wait while we fetch your sources.": "Spetgai per plaschair entant che nus retschavain Vossas funtaunas.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Aguarde enquanto eliminamos permanentemente a coleção SOLR. Isto pode demorar alguns momentos.", + "Polls": "Sondaggis", + "Popular Search Terms": "Terms da tschertga populars", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Preferências guardadas", + "Prerequisites": "Premissas", + "Press Enter to send, Shift+Enter for new line": "Smatgai Enter per trametter, Shift+Enter per ina nova lingia", + "Press enter to send, shift+enter for new line": "Prima enter para enviar, shift+enter para nova linha", + "Prettify": "Embelezar", + "Preview erasure": "Pré-visualizar apagamento", + "Preview:": "Pré-visualização:", + "Previous": "Precedent", + "Private": "Privat", + "Private Agent (Default)": "Agente Privado (Predefinição)", + "Private agents": "Agentes privados", + "Process Name (optional)": "Nome do Processo (opcional)", + "Processed {indexed} files, {failed} failed": "Processados {indexed} ficheiros, {failed} falharam", + "Processes chunks in batches with simulated parallelism.": "Elavurescha ils chunks en batches cun parallelissem simulà.", + "Processes file chunks sequentially (safest).": "Elavurescha ils chunks da Datoteca sequenzialmain (il pli segir).", + "Processes objects in chunks with simulated parallelism.": "Elavurescha ils objects en chunks cun parallelissem simulà.", + "Processes objects sequentially (safest).": "Elavurescha ils objects sequenzialmain (il pli segir).", + "Processing": "Elavuraziun", + "Processing Limits": "Limitas d'elavuraziun", + "Processing...": "Elavurar...", + "Project initialized successfully": "Il project è vegnì inizialisà cun success", + "Project not found. Please initialize first.": "Il project n'è betg vegnì chattà. Inizialisai per plaschair avant.", + "Properties": "Caracteristicas", + "Property Behaviors": "Cumportaments da la caracteristica", + "Property Configuration:": "Configuraziun da la caracteristica:", + "Property Filters": "Filters da caracteristicas", + "Property Title": "Titel da la caracteristica", + "Property Type": "Tip da caracteristica", + "Property can be improved": "La caracteristica po vegnir meglierada", + "Property could not be added": "Não foi possível adicionar a propriedade", + "Property could not be updated": "Não foi possível atualizar a propriedade", + "Property name of inversed relation": "Nome da propriedade da relação inversa", + "Property successfully added": "Propriedade adicionada com sucesso", + "Property successfully updated": "Propriedade atualizada com sucesso", + "Protected": "Protegì", + "Provider": "Purschider", + "Provider is required for testing": "Il purschider è obligatoric per testar", + "Public": "Public", + "Public View": "Vista publica", + "Public views can be accessed by anyone in the system": "Las vistas publicas pon vegnir accedidas da scadin en il sistem", + "Publish": "Publitgar", + "Publish Configuration to GitHub": "Publicar Configuração no GitHub", + "Publish OAS": "Publicar OAS", + "Publish Objects": "Publitgar ils objects", + "Publish Register OAS to GitHub": "Publicar OAS do Registo no GitHub", + "Publish all objects in this schema": "Publitgar tut ils objects en quest Schema", + "Publish objects to make them publicly available": "Publicar objetos para os tornar disponíveis publicamente", + "Published": "Publitgà", + "Published:": "Publitgà:", + "Publishing...": "Publitgar...", + "Purge": "Purgar", + "Purge Date": "Data da purgar", + "Purge {count} object from database": "Expurgar {count} objeto da base de dados", + "Purge {count} objects from database": "Expurgar {count} objetos da base de dados", + "Purpose limitation *": "Limitação da finalidade *", + "Push Notifications": "Communicaziuns push", + "Query Complexity Distribution": "Distribuziun da la cumplexitad da las dumondas", + "Query Help": "Ajuda de Consulta", + "Query parameter is required": "Il parameter da dumonda è obligatoric", + "Quota": "Quota", + "RAG": "RAG", + "RAG Configuration": "Configuraziun RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Definições RBAC atualizadas com sucesso", + "Rate limits cleared successfully": "Las limitas da tariffa èn vegnidas stizzadas cun success", + "Raw Changes Data": "Datas da midadas brutas", + "Re-vectorize on object update": "Revectorisar cur ch'in object vegn actualisà", + "Read": "Leger", + "Real-time push notification status via notify_push": "Status da las communicaziuns push en temp real via notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Las communicaziuns push en temp real èn activas. Ils clients connectads survegnan actualisaziuns immediatas cur ch'ils objects vegnan creads, actualisads u stizzads.", + "Realtime push active": "Push en temp real activ", + "Realtime push not available — the notify_push app is not installed": "Il push en temp real n'è betg disponibel — l'applicaziun notify_push n'è betg installada", + "Reason": "Motivo", + "Reason (optional)": "Motivo (opcional)", + "Rebase operation completed successfully": "Operação de rebase concluída com sucesso", + "Rebase operation failed: {error}": "A operação de rebase falhou: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Recumandaziuns:", + "Recommended Type:": "Tip recumandà:", + "Recommended: 5 sources": "Recumandà: 5 funtaunas", + "Refresh": "Actualisar", + "Refresh Data": "Actualisar las datas", + "Refresh Stats": "Actualisar la statistica", + "Refresh Workflows": "Actualisar ils process da lavur", + "Refresh dashboard": "Actualisar il panel", + "Refresh database info": "Actualisar l'infurmaziun da la banca da datas", + "Register": "Register", + "Register #{id}": "Registo #{id}", + "Register / Schema": "Registo / Esquema", + "Register ID": "ID dal Register", + "Register Objects": "Objects dal Register", + "Register Overview": "Visão Geral do Registo", + "Register Statistics": "Statistica dal Register", + "Register Totals": "Totals dal Register", + "Register and schema are required": "O registo e o esquema são obrigatórios", + "Register annotation": "Anotação do registo", + "Register depublished successfully": "Il Register è vegnì depublitgà cun success", + "Register not found": "Il Register n'è betg vegnì chattà", + "Register objects": "Objetos do registo", + "Register objects deletion completed successfully": "La stizzada dals objects dal Register è vegnida cumpletada cun success", + "Register published successfully": "Il Register è vegnì publitgà cun success", + "Register/Schema Usage": "Diever da Register/Schema", + "Register:": "Registo:", + "Registers": "Registers", + "Reindex": "Reindexar", + "Reindex completed": "Il reindexar è cumpletà", + "Reindex completed successfully": "Il reindexar è vegnì cumpletà cun success", + "Reindex completed! Processed {count} objects in {duration}s": "Reindexação concluída! Processados {count} objetos em {duration}s", + "Reindex failed": "Il reindexar è fallì", + "Reindex failed: %s": "Il reindexar è fallì: %s", + "Reject": "Rejeitar", + "Rejected": "Rejeitado", + "Related Cases": "Processos Relacionados", + "Relations": "Relaziuns", + "Remote Version": "Versiun a distanza", + "Remove": "Allontanar", + "Remove Deck card": "Remover cartão do Deck", + "Remove avatar": "Remover avatar", + "Remove contact": "Remover contacto", + "Remove filter": "Allontanar il filter", + "Remove from favorites": "Allontanar dals favorits", + "Remove from list": "Remover da lista", + "Remove group": "Allontanar la gruppa", + "Remove link": "Remover ligação", + "Remove link to {name}": "Remover ligação a {name}", + "Remove link to {name}?": "Remover ligação a {name}?", + "Remove link to {title}": "Remover ligação a {title}", + "Remove oneOf entry": "Remover entrada oneOf", + "Remove property after write-back": "Remover propriedade após escrita de retorno", + "Remove {title}": "Remover {title}", + "Remove {userId} from this organisation?": "Remover {userId} desta organização?", + "Removed": "Allontanà", + "Removed from favorites": "Allontanà dals favorits", + "Removing schema {schema}...": "A remover o esquema {schema}...", + "Rename Conversation": "Renumnar la conversaziun", + "Rename conversation": "Renumnar la conversaziun", + "Replicas": "Replicas", + "Reports": "Relatórios", + "Repository": "Repositori", + "Request Body": "Corp da la dumonda", + "Request Data": "Datas da la dumonda", + "Request Details": "Detagls da la dumonda", + "Request Quota (per day)": "Quota de Pedidos (por dia)", + "Request account deactivation": "Solicitar desativação da conta", + "Request account deactivation. This will notify administrators for review.": "Solicitar a desativação da conta. Isto notificará os administradores para revisão.", + "Request body must be JSON with a \"query\" field": "O corpo do pedido tem de ser JSON com um campo \"query\"", + "Request timeout in seconds": "Timeout da la dumonda en secundas", + "Requested at": "Solicitado em", + "Required": "Obrigatório", + "Required Token Scopes": "Âmbitos de Token Obrigatórios", + "Required field": "Chomp obligatoric", + "Required status is inconsistent": "Il status obligatoric è incoerent", + "Requires Dolphin AI for OCR text extraction": "Requer o Dolphin AI para extração de texto por OCR", + "Rerun Search": "Reexequir la tschertga", + "Rerunning search: {searchTerm}": "A voltar a executar a pesquisa: {searchTerm}", + "Reset Changes": "Reverter las midadas", + "Reset Filters": "Reverter ils filters", + "Resource Quotas": "Quotas de Recursos", + "Response Body": "Corp da la resposta", + "Response Details": "Detagls da la resposta", + "Response Time:": "Temp da resposta:", + "Restore": "Restituir", + "Restore conversation": "Restituir la conversaziun", + "Restore or permanently delete items": "Restituir u stizzar permanentamain ils elements", + "Restore {count} object": "Restaurar {count} objeto", + "Restore {count} objects": "Restaurar {count} objetos", + "Result Count Range": "Perioda dal dumber da resultats", + "Results": "Resultats", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Período de retenção (duração ISO-8601, por exemplo, P10Y, P30D)", + "Retention settings updated successfully": "Definições de retenção atualizadas com sucesso", + "Retrieve all objects for this schema": "Recuperar tut ils objects per quest Schema", + "Retry": "Reemprovar", + "Retry Failed Extractions": "Reemprovar las extracziuns fallidas", + "Retry Policy": "Reglament d'empruvas", + "Retry completed": "L'emprova è cumpletada", + "Retry upload": "Tentar novamente o carregamento", + "Retrying...": "Reemprovar...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Reveja os objetos selecionados abaixo. Pode remover quaisquer objetos que não pretenda eliminar clicando no botão remover.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Reveja os objetos selecionados abaixo. Pode remover quaisquer objetos que não pretenda incluir.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Reveja os objetos selecionados abaixo. Pode remover quaisquer objetos que não pretenda eliminar permanentemente clicando no botão remover. Esta ação não pode ser anulada.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Reveja os objetos selecionados abaixo. Pode remover quaisquer objetos que não pretenda restaurar clicando no botão remover. Os objetos serão restaurados para a sua localização original.", + "Revoke": "Revogar", + "Right": "Direita", + "Risk Level": "Nivel da ristg", + "Risk level": "Nivel da ristg", + "Roles and Permissions": "Funções e Permissões", + "Roles updated successfully": "Ils rolls èn vegnids actualisads cun success", + "Run compliance scan": "Executar análise de conformidade", + "SOLR Connection Settings": "Configuraziuns da la colliaziun SOLR", + "SOLR Index Warmup": "Aquecimento do Índice SOLR", + "SOLR Version:": "Versiun da SOLR:", + "SOLR actions menu": "Menu d'acziuns SOLR", + "SOLR base path (usually /solr)": "Percurs da basa SOLR (savens /solr)", + "SOLR collection deleted successfully": "Coleção SOLR eliminada com sucesso", + "SOLR connection test failed: {error}": "O teste de ligação SOLR falhou: {error}", + "SOLR connection test successful!": "Teste de ligação SOLR bem-sucedido!", + "SOLR index warmup completed successfully": "Aquecimento do índice SOLR concluído com sucesso", + "SOLR is disabled": "SOLR è deactivà", + "SOLR is not available or not configured": "SOLR n'è betg disponibel u betg configurà", + "SOLR logging disabled": "Il protocollar SOLR è deactivà", + "SOLR logging enabled": "Il protocollar SOLR è activà", + "SOLR search disabled": "La tschertga SOLR è deactivada", + "SOLR search enabled": "La tschertga SOLR è activada", + "SOLR server hostname or IP address": "Hostname dal server SOLR u adressa IP", + "SOLR server port number (optional, defaults to 8983)": "Dumber da port dal server SOLR (opziunal, standard 8983)", + "SOLR settings updated successfully": "Definições SOLR atualizadas com sucesso", + "SOLR setup completed successfully": "La configuraziun SOLR è vegnida cumpletada cun success", + "SOLR setup completed successfully!": "Configuração SOLR concluída com sucesso!", + "SOLR setup error: %s": "Errur da configuraziun SOLR: %s", + "SOLR setup failed": "La configuraziun SOLR è fallida", + "SOLR setup failed - check logs": "La configuraziun SOLR è fallida - controllai ils protocols", + "SOLR setup failed. Please check the configuration and try again.": "A configuração SOLR falhou. Verifique a configuração e tente novamente.", + "SOLR setup failed: {error}": "A configuração SOLR falhou: {error}", + "SOLR warmup failed: {error}": "O aquecimento SOLR falhou: {error}", + "Sample Values:": "Valurs d'exempel:", + "Save": "Memorisar", + "Save Configuration": "Memorisar la configuraziun", + "Save Connection Settings": "Memorisar las configuraziuns da la colliaziun", + "Save Roles": "Memorisar ils rolls", + "Save Settings": "Memorisar las configuraziuns", + "Save changes": "Memorisar las midadas", + "Save current search as view": "Memorisar la tschertga actuala sco vista", + "Save labels": "Guardar etiquetas", + "Save tags for {name}": "Guardar etiquetas para {name}", + "Save the organisation first to manage users.": "Guarde primeiro a organização para gerir utilizadores.", + "Saved Views": "Vistas memorisadas", + "Saving...": "Memorisar...", + "Scheduled": "Agendado", + "Schema": "Schema", + "Schema ID": "ID dal Schema", + "Schema Name": "Num dal Schema", + "Schema Statistics": "Statistica dal Schema", + "Schema annotation": "Anotação do esquema", + "Schema configuration updated successfully for {schema}": "Configuração do esquema atualizada com sucesso para {schema}", + "Schema depublished successfully": "Il Schema è vegnì depublitgà cun success", + "Schema objects deletion completed successfully": "La stizzada dals objects dal Schema è vegnida cumpletada cun success", + "Schema published successfully": "Il Schema è vegnì publitgà cun success", + "Schema reference ($ref)": "Referência do esquema ($ref)", + "Schema successfully created": "Il Schema è vegnì creà cun success", + "Schema successfully updated": "Il Schema è vegnì actualisà cun success", + "Schema {schema} removed successfully": "Esquema {schema} removido com sucesso", + "Schema:": "Esquema:", + "Schemas": "Schemas", + "Scheme": "Schema", + "Search": "Pesquisar", + "Search / Views": "Tschertga / Vistas", + "Search / views": "Pesquisa / vistas", + "Search Activity": "Activitad da tschertga", + "Search Analytics": "Analisa da tschertga", + "Search Configuration": "Configuração de Pesquisa", + "Search Entities": "Tschertgar entitads", + "Search Files": "Tschertgar Datotecas", + "Search GitHub": "Tschertgar en GitHub", + "Search GitLab": "Tschertgar en GitLab", + "Search Method": "Metoda da tschertga", + "Search Mode": "Modo de Pesquisa", + "Search Query": "Consulta de Pesquisa", + "Search Results": "Resultados da Pesquisa", + "Search Term": "Term da tschertga", + "Search Trail Details": "Detagls da la colliaziun da tschertga", + "Search Trail Management": "Administraziun da las colliaziuns da tschertga", + "Search Trail Statistics": "Statistica da las colliaziuns da tschertga", + "Search Trails": "Colliaziuns da tschertga", + "Search Views": "Tschertgar vistas", + "Search Webhooks": "Tschertgar webhooks", + "Search agents...": "Pesquisar agentes...", + "Search applications...": "Pesquisar aplicações...", + "Search by file name or path": "Tschertgar tenor il num u il percurs da la Datoteca", + "Search by name or URL": "Tschertgar tenor num u URL", + "Search by title or UUID...": "Pesquisar por título ou UUID...", + "Search by value": "Tschertgar tenor valur", + "Search configurations": "Pesquisar configurações", + "Search data sources...": "Pesquisar fontes de dados...", + "Search groups...": "Pesquisar grupos...", + "Search in Database Objects": "Pesquisar em Objetos da Base de Dados", + "Search in Files": "Tschertgar en Datotecas", + "Search in Objects": "Tschertgar en objects", + "Search objects": "Pesquisar objetos", + "Search objects...": "Pesquisar objetos...", + "Search property names...": "Tschertgar nums da caracteristicas...", + "Search registers...": "Pesquisar registos...", + "Search schemas...": "Pesquisar esquemas...", + "Search trail deletion not implemented yet": "Il stizzar da colliaziuns da tschertga n'è anc betg implementà", + "Search views...": "Tschertgar vistas...", + "Search {name}...": "Pesquisar {name}...", + "Searching...": "A pesquisar...", + "Secret": "Secret", + "Secret key for HMAC signature generation (optional)": "Clav secreta per la generaziun da la signatura HMAC (opziunal)", + "Security": "Segurança", + "See organisations as a table": "Ver organizações como uma tabela", + "See organisations as cards": "Ver organizações como cartões", + "Select AI Agent": "Tscherner l'agent IA", + "Select Active Organisation": "Selecionar Organização Ativa", + "Select All": "Tscherner tut", + "Select ConfigSet": "Tscherner il ConfigSet", + "Select File Types to Vectorize:": "Tscherner ils tips da Datoteca da vectorisar:", + "Select HTTP method": "Tscherner la metoda HTTP", + "Select Register and Schema": "Tscherner il Register ed il Schema", + "Select Views to Vectorize:": "Tscherner las vistas da vectorisar:", + "Select a Nextcloud group to add": "Tscherner ina gruppa da Nextcloud per agiuntar", + "Select a branch": "Tscherner in branch", + "Select a date property": "Selecione uma propriedade de data", + "Select a model or type a custom model name": "Tscherner in model u endatar in num da model persunalisà", + "Select a register": "Selecione um registo", + "Select a register...": "Selecione um registo...", + "Select a repository": "Tscherner in repositori", + "Select a repository you have write access to": "Tscherner in repositori sin il qual Vus avais access da scriver", + "Select a schema": "Selecione um esquema", + "Select a schema...": "Selecione um esquema...", + "Select a user": "Selecione um utilizador", + "Select agent type": "Selecionar tipo de agente", + "Select all entities": "Selecionar todas as entidades", + "Select an AI Agent": "Tscherner in agent IA", + "Select an AI agent to begin chatting with your data.": "Tscherner in agent IA per cumenzar a chattar cun Vossas datas.", + "Select an email to see linked objects": "Selecione um e-mail para ver os objetos associados", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Selecione uma organização e um utilizador para os adicionar como membro. Pesquise organizações por nome.", + "Select backend": "Tscherner il backend", + "Select chat model": "Tscherner il model da chat", + "Select collection for files": "Tscherner la collecziun per las Datotecas", + "Select collection for objects": "Tscherner la collecziun per ils objects", + "Select configuration type...": "Selecionar tipo de configuração...", + "Select default organisation": "Tscherner l'organisaziun standard", + "Select entity {value}": "Selecionar entidade {value}", + "Select event to listen to...": "Tscherner l'eveniment dad ascutar...", + "Select fields to display": "Selecionar campos a apresentar", + "Select files with the correct extension": "Selecionar ficheiros com a extensão correta", + "Select group": "Selecionar grupo", + "Select groups (optional)": "Selecionar grupos (opcional)", + "Select groups to notify...": "Selecionar grupos a notificar...", + "Select groups with access to this agent": "Selecionar grupos com acesso a este agente", + "Select groups...": "Selecionar grupos...", + "Select model": "Tscherner il model", + "Select one or more registers": "Selecione um ou mais registos", + "Select one or more schemas": "Selecione um ou mais esquemas", + "Select options...": "Tscherner las opziuns...", + "Select or create labels, or select \"No label\" to add files": "Selecione ou crie etiquetas, ou selecione \"Sem etiqueta\" para adicionar ficheiros", + "Select owner application (optional)...": "Selecionar aplicação proprietária (opcional)...", + "Select period": "Tscherner la perioda", + "Select property to send as payload": "Tscherner la caracteristica da trametter sco payload", + "Select provider": "Tscherner il purschider", + "Select registers and schemas to save a view": "Tscherner Registers e Schemas per memorisar ina vista", + "Select retry policy": "Tscherner il reglament d'empruvas", + "Select role": "Selecionar função", + "Select schemas (empty = all schemas)": "Selecionar esquemas (vazio = todos os esquemas)", + "Select schemas to warm up": "Selecionar esquemas a aquecer", + "Select search mode": "Selecionar modo de pesquisa", + "Select source type...": "Selecionar tipo de fonte...", + "Select target type": "Selecionar tipo de destino", + "Select the branch to publish to": "Tscherner il branch sin il qual publitgar", + "Select the event this webhook should listen to": "Tscherner l'eveniment ch'il webhook duai ascutar", + "Select views to filter data (optional)": "Selecionar vistas para filtrar dados (opcional)", + "Select views to limit which data the agent can access": "Selecionar vistas para limitar os dados a que o agente pode aceder", + "Select views to vectorize:": "Tscherner las vistas da vectorisar:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Tscherner qualas gruppas da Nextcloud èn disponiblas per questa organisaziun. Ils utilisaders en questas gruppas han access a las resursas da l'organisaziun.", + "Select which data views the AI can search": "Tscherner qualas vistas da datas l'IA po tschertgar", + "Select which property from the event should be used as the webhook payload data": "Tscherner qual caracteristica da l'eveniment duai vegnir duvrada sco datas da payload dal webhook", + "Select which tools the AI can use to perform actions": "Tscherner quals utensils l'IA po duvrar per exequir acziuns", + "Select {fieldName} values": "Selecionar valores de {fieldName}", + "Selected": "Tschernì", + "Selected Groups": "Gruppas tschernidas", + "Selected Objects ({count})": "Objetos Selecionados ({count})", + "Selected audit trails deleted successfully": "Las colliaziuns d'audit tschernidas èn vegnidas stizzadas cun success", + "Selected search trails deleted successfully": "Las colliaziuns da tschertga tschernidas èn vegnidas stizzadas cun success", + "Selected users can access this view": "Ils utilisaders tschernids pon acceder a questa vista", + "Send additional feedback": "Trametter feedback supplementar", + "Send as CloudEvent": "Trametter sco CloudEvent", + "Send as cloudevent": "Enviar como CloudEvent", + "Sensitive PII": "PII sensibel", + "Serial Mode (Safer, slower)": "Modus serial (pli segir, pli plaun)", + "Serial:": "Serial:", + "Server Configuration": "Configuraziun dal server", + "Server Information": "Infurmaziuns dal server", + "Set as Active": "Definir como ativo", + "Set as active organisation": "Definir como organização ativa", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Defina limites para a utilização da API e o consumo de tokens. Utilize 0 para recursos ilimitados.", + "Settings": "Configuraziuns", + "Settings updated successfully": "Definições atualizadas com sucesso", + "Shards": "Shards", + "Share": "Cumpartir", + "Share with Groups": "Cumpartir cun gruppas", + "Share with Users": "Cumpartir cun utilisaders", + "Show": "Mostrar", + "Show Filters": "Mussar ils filters", + "Show less": "Mussar damain", + "Show only entries with changes": "Mussar mo las endataziuns cun midadas", + "Showing {count} dashboard(s)": "A mostrar {count} painel(éis)", + "Showing {count} processing activities": "A mostrar {count} atividades de processamento", + "Showing {showing} of {total} agents": "A mostrar {showing} de {total} agentes", + "Showing {showing} of {total} applications": "A mostrar {showing} de {total} aplicações", + "Showing {showing} of {total} audit trail entries": "A mostrar {showing} de {total} entradas de registo de auditoria", + "Showing {showing} of {total} configurations": "A mostrar {showing} de {total} configurações", + "Showing {showing} of {total} deleted items": "A mostrar {showing} de {total} itens eliminados", + "Showing {showing} of {total} entities": "A mostrar {showing} de {total} entidades", + "Showing {showing} of {total} files": "A mostrar {showing} de {total} ficheiros", + "Showing {showing} of {total} log entries": "A mostrar {showing} de {total} entradas de registo", + "Showing {showing} of {total} search trail entries": "A mostrar {showing} de {total} entradas de registo de pesquisa", + "Showing {showing} of {total} sources": "A mostrar {showing} de {total} fontes", + "Showing {showing} of {total} templates": "A mostrar {showing} de {total} modelos", + "Showing {showing} of {total} webhooks": "A mostrar {showing} de {total} webhooks", + "Showing {shown} of {total} organisations": "A mostrar {shown} de {total} organizações", + "Simple": "Simpel", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Dumondas simplas: tschertgas da text da basa cun parameters minimals (p.ex. in sulet term da tschertga, nagins filters avanzads)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Consultas simples: pesquisas de texto básicas com parâmetros mínimos (por exemplo, um único termo de pesquisa, sem filtros avançados)", + "Size": "Grondezza", + "Slug": "Slug", + "Soft Delete Mode": "Modus da stizzar mollamain", + "Soft Deleted Items": "Elements stizzads mollamain", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Eliminar de forma reversível todos os objetos deste esquema ({active} ativos, {deleted} já eliminados)", + "SolrCloud mode enabled": "Il modus SolrCloud è activà", + "Something went wrong while adding files": "Ocorreu um problema ao adicionar ficheiros", + "Source": "Funtauna", + "Source successfully updated": "Fonte atualizada com sucesso", + "Sources": "Funtaunas", + "Sources:": "Funtaunas:", + "Special Rights": "Direitos especiais", + "Standalone SOLR mode": "Modus SOLR autonom", + "Start Conversation": "Cumenzar la conversaziun", + "Start Date Field": "Campo da data de início", + "Start Vectorization": "Cumenzar la vectorisaziun", + "Start a conversation": "Cumenzar ina conversaziun", + "Starting names cache warmup...": "Cumenzar il prechauffament dal cache da nums...", + "Starting validation for {schema}...": "A iniciar a validação para {schema}...", + "Starting {action} for {schema}...": "A iniciar {action} para {schema}...", + "Starting...": "Cumenzar...", + "Statistics": "Statistica", + "Statistics and Metrics": "Estatísticas e métricas", + "Statistics refreshed": "Estatísticas atualizadas", + "Stats calculation not yet implemented": "Il calcul da la statistica n'è anc betg implementà", + "Status": "Status", + "Status Code": "Code da status", + "Step": "Passo", + "Storage": "Armazenamento", + "Storage Quota (MB)": "Quota de armazenamento (MB)", + "String Constraints": "Restricziuns da string", + "Subject identifier (email, BSN, name, etc.)": "Identificador do titular (email, BSN, nome, etc.)", + "Subscribe": "Subscrever", + "Subscribed": "Subscrito", + "Success": "Success", + "Success Rate": "Quota da success", + "Success Status": "Status da success", + "Successful": "Reussì", + "Successfully added user to organisation: {name}": "Utilizador adicionado com sucesso à organização: {name}", + "Successfully cleared {count} audit trails": "Foram limpos com sucesso {count} registos de auditoria", + "Successfully cleared {count} blob storage objects": "Foram limpos com sucesso {count} objetos de armazenamento blob", + "Successfully cleared {count} search trails": "Foram limpos com sucesso {count} registos de pesquisa", + "Successfully created {count} SOLR fields": "Foram criados com sucesso {count} campos SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Foram eliminados com sucesso {count} embeddings. Volte a vetorizar os seus dados.", + "Successfully deleted {count} objects for {schema}": "Foram eliminados com sucesso {count} objetos para {schema}", + "Successfully fixed {count} SOLR field configurations": "Foram corrigidas com sucesso {count} configurações de campos SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importado com sucesso: {registers} registos, {schemas} esquemas, {objects} objetos", + "Successfully joined organisation": "Sa participà cun success a l'organisaziun", + "Successfully permanently deleted {count} objects": "Foram eliminados permanentemente com sucesso {count} objetos", + "Successfully processed {count} files!": "Foram processados com sucesso {count} ficheiros!", + "Successfully restored {count} object": "Foi restaurado com sucesso {count} objeto", + "Successfully restored {count} objects": "Foram restaurados com sucesso {count} objetos", + "Successfully saved configuration for {count} facets!": "Configuração guardada com sucesso para {count} facetas!", + "Supported File Types": "Tipos de ficheiro suportados", + "Switch Active Organisation": "Mudar de organização ativa", + "Switch Organisation": "Mudar de organização", + "Sync Table": "Sincronisar la tabella", + "Synchronization Settings": "Configuraziuns da sincronisaziun", + "Syncing magic table for {schema}...": "A sincronizar a tabela mágica para {schema}...", + "Syncs": "Sincronizações", + "System Default": "Standard dal sistem", + "System Prompt": "Prompt do sistema", + "System announcements": "Anúncios do sistema", + "Table": "Tabela", + "Target ID": "ID do destino", + "Target Type*": "Tipo de destino*", + "Technical Description": "Descripziun tecnica", + "Technical Details": "Detalhes técnicos", + "Technical description for developers and administrators": "Descripziun tecnica per sviluppaders ed administraturs", + "Technical measures": "Medidas técnicas", + "Technische maatregelen": "Medidas técnicas", + "Temperature": "Temperatura", + "Templates": "Models", + "Test": "Test", + "Test Chat": "Testar il chat", + "Test Connection": "Testar la colliaziun", + "Test Embedding": "Testar l'embedding", + "Test endpoint executed successfully": "L'endpoint da test è vegnì exequì cun success", + "Test webhook delivered successfully": "Il webhook da test è vegnì furnì cun success", + "Test webhook delivery failed": "La furniziun dal webhook da test è fallida", + "Test webhook sent successfully": "Il webhook da test è vegnì tramess cun success", + "Testing...": "Testar...", + "Text Chunk #{id}": "Bloco de texto n.º {id}", + "Text Extraction": "Extração de texto", + "Text Extraction Engine": "Motor de extração de texto", + "Text chunks": "Chunks da text", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Ils chunks da text vegnan generads durant l'extracziun da la Datoteca e memorisads en la banca da datas. La vectorisaziun lescha questas Datotecas prechunkadas e las converta en embeddings.", + "Text extracted successfully": "Il text è vegnì extratg cun success", + "Text extraction disabled": "L'extracziun da text è deactivada", + "The Nextcloud Calendar app is not installed or enabled on this server.": "A aplicação Nextcloud Calendar não está instalada nem ativada neste servidor.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "A aplicação Nextcloud Contacts não está instalada nem ativada neste servidor.", + "The Nextcloud Deck app is not installed or enabled on this server.": "A aplicação Nextcloud Deck não está instalada nem ativada neste servidor.", + "The Nextcloud Mail app is not installed or enabled on this server.": "A aplicação Nextcloud Mail não está instalada nem ativada neste servidor.", + "The URL where webhook events will be sent": "L'URL nua ch'ils eveniments da webhook vegnan tramess", + "The requested conversation does not exist": "La conversaziun dumandada n'exista betg", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Não foi possível carregar o painel solicitado. Verifique o URL ou escolha outro painel da lista.", + "The saved view and all its search configuration": "La vista memorisada e tut sia configuraziun da tschertga", + "There are no audit trail entries matching your current filters.": "I na dat naginas endataziuns da colliaziuns d'audit che correspundan a Voss filters actuals.", + "There are no deleted items matching your current filters.": "I na dat nagins elements stizzads che correspundan a Voss filters actuals.", + "There are no search trail entries matching your current filters.": "I na dat naginas endataziuns da colliaziuns da tschertga che correspundan a Voss filters actuals.", + "There are no webhook log entries matching your filters.": "I na dat naginas endataziuns da protocol da webhook che correspundan a Voss filters.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Serão retidos de acordo com o período de retenção configurado no respetivo esquema e eliminados permanentemente de forma automática quando o período de retenção expirar. O período de retenção é configurável por esquema e pode ser encontrado nas definições do esquema.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Serão retidos de acordo com o período de retenção configurado no respetivo esquema e depois removidos permanentemente.", + "This action cannot be undone.": "Questa acziun na po betg vegnir revocada.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Questa acziun na po betg vegnir revocada. Garantì che naginas collecziuns dovran quest ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Esta ação não pode ser anulada. Certifique-se de que nenhuma coleção está a utilizar este ConfigSet.", + "This action will submit a deactivation request to your administrators.": "Esta ação irá submeter um pedido de desativação aos seus administradores.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Quest agent n'ha naginas vistas u utensils configurabels, ma Vus pudais tuttina adattar las configuraziuns RAG sutvart.", + "This analysis may take some time": "Questa analisa po durar in tschert temp", + "This audit trail entry does not contain any change information.": "Questa endataziun da colliaziun d'audit na cuntegna naginas infurmaziuns da midada.", + "This configuration has no data defined.": "Questa configuraziun n'ha naginas datas definidas.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Quest endpoint è antiquà. Duvrai enstagl endpoints basads sin chunks.", + "This entity has no relations to objects or files": "Questa entitad n'ha naginas relaziuns cun objects u Datotecas", + "This is a test webhook from OpenRegister": "Quai è in webhook da test da OpenRegister", + "This is not valid JSON": "Este JSON não é válido", + "This key already exists on this schema": "Esta chave já existe neste esquema", + "This schema must use Magic Table configuration to sync": "Quest Schema sto duvrar la configuraziun Magic Table per sincronisar", + "This schema must use magic table configuration to sync": "Este esquema tem de utilizar a configuração de tabela mágica para sincronizar", + "This source has no associated registers.": "Questa funtauna n'ha nagins Registers associads.", + "This token will only be shown once. Copy it now.": "Este token só será mostrado uma vez. Copie-o agora.", + "This will copy the _default ConfigSet with the new name": "Quai copia il ConfigSet _default cun il nov num", + "This will copy the _default configset with the new name": "Isto irá copiar o ConfigSet _default com o novo nome", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Isto irá eliminar permanentemente TODOS os embeddings (vetores) da base de dados. Terá de voltar a vetorizar todos os objetos e ficheiros. Esta ação não pode ser anulada.\n\nTem a certeza de que pretende continuar?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Isto irá eliminar permanentemente TODOS os embeddings (vetores) da base de dados. Terá de voltar a vetorizar todos os objetos e ficheiros. Esta ação não pode ser anulada.\\n\\nTem a certeza de que pretende continuar?", + "This will permanently delete:": "Quai stizza permanentamain:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Isto irá remover permanentemente {count} objetos já eliminados de forma reversível da base de dados. Esta ação não pode ser anulada.", + "Timeout (seconds)": "Timeout (secundas)", + "Timestamp": "Marca da temp", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Conegl: activai mo vistas che basegnan la tschertga semantica per minimar ils custs d'embedding. Tabellas da consultaziun simplas basegnan raramain vectorisaziun.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Sugestão: ative apenas as vistas que necessitam de pesquisa semântica para minimizar os custos de embedding. As tabelas de consulta simples raramente necessitam de vetorização.", + "Title": "Titel", + "Title *": "Título *", + "Title Template": "Modelo de título", + "Title*": "Título*", + "To (e.g. -1 year or 2025-12-31)": "Até (por exemplo, -1 year ou 2025-12-31)", + "To Date": "Fin la data", + "To add files larger than or equal to 512MB, go to the": "Para adicionar ficheiros maiores ou iguais a 512 MB, aceda a", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Per adattar la grondezza dal chunk u la strategia, mai a la configuraziun da Datoteca → Limitas d'elavuraziun.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Para ajustar o tamanho ou a estratégia dos blocos, aceda a configuração de ficheiros → limites de processamento.", + "To change the embedding provider or model, go to LLM Configuration.": "Per midar il purschider u il model d'embedding, mai a la configuraziun LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Para alterar o fornecedor ou modelo de embedding, aceda a configuração de LLM.", + "To date": "Fin la data", + "Toggle search sidebar": "Cumutar la trav laterala da tschertga", + "Token Created": "Token criado", + "Token Quota (per request)": "Quota de tokens (por pedido)", + "Token copied to clipboard": "Token copiado para a área de transferência", + "Token name": "Nome do token", + "Token revoked": "Token revogado", + "Tools": "Utensils", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "As ferramentas são executadas com as permissões do utilizador predefinido do agente quando não existe uma sessão de utilizador ativa (por exemplo, tarefas cron). Configure o utilizador predefinido no separador Definições.", + "Tools respect the agent's own role and organization boundaries.": "As ferramentas respeitam o próprio papel do agente e os limites da organização.", + "Tools respect the agent's views, permissions, and organization boundaries.": "As ferramentas respeitam as vistas, permissões e limites da organização do agente.", + "Top Deleters": "Ils principals che stizzan", + "Total": "Total", + "Total Audit Trails": "Total da colliaziuns d'audit", + "Total Chunks Available:": "Total da chunks disponibels:", + "Total Deleted Items": "Total d'elements stizzads", + "Total Members": "Total da commembers", + "Total Objects": "Total d'objects", + "Total Objects in Database:": "Total d'objects en la banca da datas:", + "Total Organisations": "Total d'organisaziuns", + "Total Results": "Total da resultats", + "Total Searches": "Total da tschertgas", + "Total Size": "Grondezza totala", + "Total Storage": "Deposit total", + "Total events": "Total de eventos", + "Total:": "Total:", + "Totals": "Totals", + "Try searching by UUID or with different keywords": "Tente pesquisar por UUID ou com palavras-chave diferentes", + "Type": "Tip", + "Type \"DELETE COLLECTION\" to confirm:": "Escreva \"DELETE COLLECTION\" para confirmar:", + "Type Issue": "Problem da tip", + "Type Variations:": "Variaziuns da tip:", + "Type filter (optional, e.g. email)": "Filtro de tipo (opcional, por exemplo, email)", + "Type must be \"positive\" or \"negative\"": "O tipo tem de ser \"positive\" ou \"negative\"", + "Type to search for groups": "Tippai per tschertgar gruppas", + "Type to search for objects...": "Escreva para pesquisar objetos...", + "Type to search for organisations": "Escreva para pesquisar organizações", + "Type to search for users": "Tippai per tschertgar utilisaders", + "Type to search groups...": "Tippai per tschertgar gruppas...", + "Type to search users...": "Tippai per tschertgar utilisaders...", + "Type to search...": "Escreva para pesquisar...", + "Type your username to confirm": "Escreva o seu nome de utilizador para confirmar", + "URL": "URL", + "URL where Ollama is running": "URL nua ch'Ollama funcziunescha", + "UUID copied to clipboard": "UUID copiado para a área de transferência", + "UUID:": "UUID:", + "Unchanged": "Nunmidà", + "Unique Search Terms": "Terms da tschertga unics", + "Unique Terms": "Terms unics", + "Unknown": "Nunenconuschent", + "Unknown Object": "Objeto desconhecido", + "Unknown Register": "Register nunenconuschent", + "Unknown Schema": "Schema nunenconuschent", + "Unknown error": "Errur nunenconuschenta", + "Unknown widget type: {type}": "Tipo de widget desconhecido: {type}", + "Unlink email": "Desassociar email", + "Unlink event": "Desassociar evento", + "Unnamed Object": "Objeto sem nome", + "Untitled": "Sem título", + "Untitled View": "Vista senza titel", + "Update": "Actualisar", + "Update Operations": "Operaziuns d'actualisaziun", + "Update available: {local} → {remote}": "Atualização disponível: {local} → {remote}", + "Update configuration: ...": "Actualisar la configuraziun: ...", + "Update register OAS: ...": "Actualisar l'OAS dal Register: ...", + "Update vectors when object data changes (recommended for accurate search)": "Actualisar ils vecturs cur che las datas d'object midan (recumandà per ina tschertga precisa)", + "Updated": "Actualisà", + "Updated At": "Actualisà la", + "Updated:": "Actualisà:", + "Upload new avatar": "Carregar novo avatar", + "Uploading configuration sets...": "A carregar conjuntos de configuração...", + "Uptime:": "Temp en funcziun:", + "Url": "Url", + "Usage Count": "Dumber da diever", + "Use AI agents for processing and analysis": "Utilizar agentes de IA para processamento e análise", + "Use Large Language Model features": "Utilizar funcionalidades de Large Language Model", + "Use SolrCloud with Zookeeper for distributed search": "Duvrar SolrCloud cun Zookeeper per la tschertga distribuida", + "Use SolrCloud with zookeeper for distributed search": "Utilizar o SolrCloud com o Zookeeper para pesquisa distribuída", + "Use VNG APIs instead of MongoDB.": "Utilizar as APIs da VNG em vez do MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Utilizar intervalos predefinidos (Últimos 7/30/90 dias, Último ano, Mais antigos)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Utilizar armazenamento externo (por exemplo, MongoDB) em vez do armazenamento interno do Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Duvrar filters per restrenscher las endataziuns da colliaziuns d'audit tenor Register, Schema, tip d'acziun, utilisader, perioda da data u ID d'object.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Duvrar filters per restrenscher ils elements stizzads tenor Register, Schema, data da stizzada u l'utilisader che ha stizzà els.", + "Use filters to narrow down entities by type or category.": "Utilize filtros para restringir as entidades por tipo ou categoria.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Duvrar filters per restrenscher las endataziuns da colliaziuns da tschertga tenor Register, Schema, status da success, utilisader, perioda da data, terms da tschertga u metricas da prestaziun.", + "Used By Collections": "Duvrà da collecziuns", + "Used by": "Utilizado por", + "User": "Utilisader", + "User Agent Statistics": "Statistica dal User Agent", + "User Description": "Descripziun da l'utilisader", + "User Quota": "Quota do utilizador", + "User rate limits cleared successfully": "Las limitas da tariffa d'utilisader èn vegnidas stizzadas cun success", + "User removed successfully": "L'utilisader è vegnì allontanà cun success", + "User-friendly description shown in forms and help text": "Descripziun adattada a l'utilisader mussada en ils formulars ed il text d'agid", + "Username": "Num d'utilisader", + "Username for SOLR authentication (optional)": "Num d'utilisader per l'autentificaziun SOLR (opziunal)", + "Users": "Utilisaders", + "Uses": "Utilizações", + "Using Pre-Generated Chunks": "Diever da chunks pregenerads", + "Validate": "Validar", + "Validate Objects": "Validar ils objects", + "Validation completed for {schema}": "Validação concluída para {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validação concluída para {schema}: {processed} processados, {updated} atualizados, {failed} falhados", + "Validation completed successfully": "La validaziun è vegnida cumpletada cun success", + "Validation failed": "La validaziun è fallida", + "Value": "Valur", + "Value list (split on ,)": "Lista de valores (separada por ,)", + "Vector Dimensions": "Dimensiuns dal vectur", + "Vector Search Backend": "Backend da tschertga da vectur", + "Vector field: _embedding_": "Chomp da vectur: _embedding_", + "Vectorization Triggers": "Triggers da vectorisaziun", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vetorização concluída: {vectorized} objetos vetorizados, {failed} falhados", + "Vectorization failed: {error}": "A vetorização falhou: {error}", + "Vectorize All Files": "Vectorisar tut las Datotecas", + "Vectorize All Objects": "Vectorisar tut ils objects", + "Vectorize all file types": "Vectorisar tut ils tips da Datoteca", + "Vectorize all views": "Vectorisar tut las vistas", + "Vectorize on object creation": "Vectorisar cur ch'in object vegn creà", + "Vectors will be stored in your existing object and file collections": "Ils vecturs vegnan memorisads en Vossas collecziuns existentas d'objects e Datotecas", + "Verantwoording": "Justificação", + "Verifying server connectivity...": "A verificar a conetividade com o servidor...", + "Version": "Versiun", + "Version Information": "Informação da versão", + "Very High": "Fitg aut", + "Very high": "Fitg aut", + "View": "Vista", + "View API Docs": "Vesair la documentaziun API", + "View API Documentation": "Ver documentação da API", + "View APIs": "Ver APIs", + "View Changes": "Vesair las midadas", + "View Details": "Vesair ils detagls", + "View Error": "Vesair l'errur", + "View File": "Vesair la Datoteca", + "View Full Details": "Vesair tut ils detagls", + "View Logs": "Vesair ils protocols", + "View Name": "Num da la vista", + "View Object": "Vesair l'object", + "View Parameters": "Vesair ils parameters", + "View Selection": "Selecziun da vistas", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Vesair ed analisar ils protocols da colliaziuns da tschertga cun capacitads avanzadas da filtrar ed analisar", + "View and analyze system audit trails with advanced filtering capabilities": "Vesair ed analisar las colliaziuns d'audit dal sistem cun capacitads avanzadas da filtrar", + "View and manage authorization across registers and schemas": "Veja e faça a gestão da autorização nos registos e esquemas", + "View deleted successfully": "La vista è vegnida stizzada cun success", + "View entity details and manage relations": "Vesair ils detagls da l'entitad ed administrar las relaziuns", + "View name": "Num da la vista", + "View name is required": "Il num da la vista è obligatoric", + "View object": "Ver objeto", + "View saved successfully!": "La vista è vegnida memorisada cun success!", + "View search analytics and manage search logs": "Vesair l'analisa da tschertga ed administrar ils protocols da tschertga", + "View successfully deleted": "La vista è vegnida stizzada cun success", + "View successfully updated": "La vista è vegnida actualisada cun success", + "View updated successfully!": "La vista è vegnida actualisada cun success!", + "View webhook delivery logs and filter by webhook": "Vesair ils protocols da furniziun da webhook e filtrar tenor il webhook", + "View {count} more": "Ver mais {count}", + "Views": "Vistas", + "Visible to end users": "Visível para os utilizadores finais", + "Visible to users": "Visibel per ils utilisaders", + "Wait for Response": "Spetgar la resposta", + "Wait for webhook response before continuing (required for request/response flows)": "Spetgar la resposta dal webhook avant da cuntinuar (obligatoric per process da dumonda/resposta)", + "Waiting for cluster synchronization...": "A aguardar pela sincronização do cluster...", + "Warmup Names Cache": "Prechauffar il cache da nums", + "Warmup failed": "O aquecimento falhou", + "Warning": "Avertiment", + "Warning:": "Avertiment:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Aviso: esta propriedade tem tipos inconsistentes: {types}. Considere se todos os objetos devem ter o mesmo tipo.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Iremos analisar todos os objetos pertencentes a este esquema para descobrir novas propriedades e analisar as propriedades existentes em busca de potenciais melhorias. O processo envolve examinar a estrutura de dados de cada objeto, identificar propriedades não definidas no esquema atual e encontrar oportunidades para melhorar as definições das propriedades existentes com melhores restrições, formatos e regras de validação.", + "Webhook": "Webhook", + "Webhook Log Details": "Detagls dal protocol da webhook", + "Webhook Logs": "Protocols da webhook", + "Webhook created successfully": "Il webhook è vegnì creà cun success", + "Webhook deleted": "Il webhook è stizzà", + "Webhook retry delivered successfully": "L'emprova dal webhook è vegnida furnida cun success", + "Webhook retry delivery failed": "La furniziun da l'emprova dal webhook è fallida", + "Webhook updated": "Il webhook è actualisà", + "Webhook updated successfully": "Il webhook è vegnì actualisà cun success", + "Webhooks": "Webhooks", + "Weekly": "Mintga emna", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Quando o agente é executado sem uma sessão de utilizador (por exemplo, tarefas agendadas), será utilizado o contexto deste utilizador", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Quando o agente é executado sem uma sessão de utilizador (por exemplo, tarefas agendadas), será utilizado o contexto deste utilizador.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Quando desativada, esta faceta mostrará apenas valores deste esquema e incluirá um filtro de esquema quando selecionada.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Quando ativado, ao guardar este objeto também serão atualizados os objetos de destino para incluir uma referência de volta a este objeto.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Quando ativado, esta propriedade será removida do objeto de origem após a atualização dos objetos de destino.", + "Workflow Management": "Gestão de fluxos de trabalho", + "Workflows": "Fluxos de trabalho", + "Wrap webhook payload in CloudEvents format for better interoperability": "Embellar il payload dal webhook en il format CloudEvents per ina meglra interoperabilitad", + "Wrap webhook payload in cloudevents format for better interoperability": "Encapsular o payload do webhook no formato CloudEvents para melhor interoperabilidade", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Gea", + "You": "Vus", + "You are not yet a member of any organisations.": "Ainda não é membro de nenhuma organização.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Vus pudais crear novs ConfigSets a basa dal model _default, u transferir tals persunalisads directamain sin Voss server SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Pode criar novos ConfigSets baseados no modelo _default, ou carregar configurações personalizadas diretamente para o seu servidor SOLR.", + "You do not have access to this conversation": "Vus n'avais betg access a questa conversaziun", + "You do not have permission to delete this conversation": "Vus n'avais betg il dretg da stizzar questa conversaziun", + "You do not have permission to modify this conversation": "Vus n'avais betg il dretg da modifitgar questa conversaziun", + "You do not have permission to restore this conversation": "Vus n'avais betg il dretg da restituir questa conversaziun", + "You do not have permission to view the permission matrix. Admin access is required.": "Não tem permissão para ver a matriz de permissões. É necessário acesso de administrador.", + "You have selected {count} properties to add to the schema.": "Selecionou {count} propriedades para adicionar ao esquema.", + "You must be logged in to favorite views": "Vus stuais esser annunzià per marcar vistas sco favorits", + "You need an AI agent to start a conversation.": "Vus basegnais in agent IA per cumenzar ina conversaziun.", + "Your Fireworks AI API key. Get one at": "Vossa clav API da Fireworks AI. Procurai Vus ina sin", + "Your OpenAI API key. Get one at": "Vossa clav API da OpenAI. Procurai Vus ina sin", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Voss feedback è vegnì registrà. Opziunalmain pudais Vus furnir qua detagls supplementars...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "O seu comentário foi registado. Opcionalmente, pode fornecer detalhes adicionais aqui...", + "Zookeeper Hosts": "Hosts da Zookeeper", + "Zookeeper Port": "Port da Zookeeper", + "Zookeeper Settings (SolrCloud)": "Configuraziuns da Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "String da colliaziun da Zookeeper per SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Dumber da port da Zookeeper (opziunal, standard 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVISO DE ELIMINAÇÃO PERMANENTE \\u26A0\\uFE0F\n\nEstá prestes a eliminar PERMANENTEMENTE TODOS os objetos do esquema \"{schema}\":\n\n\\u2022 Objetos ativos: {active}\n\\u2022 Objetos eliminados de forma reversível: {deleted}\n\\u2022 Total: {total}\n\nEstes objetos serão completamente removidos da base de dados e NÃO PODEM ser recuperados.\n\nTem a certeza absoluta?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVISO DE ELIMINAÇÃO PERMANENTE \\u26A0\\uFE0F\n\nEstá prestes a eliminar PERMANENTEMENTE {count} objetos eliminados de forma reversível do esquema \"{schema}\".\n\nEstes objetos serão completamente removidos da base de dados e NÃO PODEM ser recuperados.\n\nTem a certeza absoluta?", + "and accessible to all users in selected groups (or all users if no groups selected).": "e acessível a todos os utilizadores nos grupos selecionados (ou a todos os utilizadores se nenhum grupo for selecionado).", + "and add the files there.": "e adicione os ficheiros aí.", + "approved": "aprovado", + "are only accessible to invited users.": "são apenas acessíveis a utilizadores convidados.", + "by": "por", + "chunk": "chunk", + "chunks": "chunks", + "chunks processed": "chunks elavurads", + "completed": "concluído", + "configuration(s)": "configuraziun(s)", + "conversationId is required": "conversationId è obligatoric", + "create": "criar", + "delete": "eliminar", + "deleted objects section": "secção de objetos eliminados", + "e.g., lib/Settings/config.json": "p.ex. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "p.ex. lib/Settings/register.json", + "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", + "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", + "execute": "executar", + "failed": "fallì", + "file": "datoteca", + "files": "datotecas", + "file{plural}": "ficheiro{plural}", + "folder": "pasta", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "em {register}", + "in_progress": "em curso", + "inherited": "herdado", + "list": "listar", + "log{plural}": "registo{plural}", + "manage": "gerir", + "members": "commembers", + "menu or contact someone with permission to create agents.": "menu u contactai insatgi cun il dretg da crear agents.", + "message content is required": "Il cuntegn dal messadi è obligatoric", + "messages": "messadis", + "missing": "em falta", + "myapp": "myapp", + "n8n URL and API key are required": "L'URL n8n e la clav API èn obligatorics", + "n8n configuration saved successfully": "La configuraziun n8n è vegnida memorisada cun success", + "n8n connection not configured": "La colliaziun n8n n'è betg configurada", + "n8n connection successful": "La colliaziun n8n è reussida", + "n8n connection test failed: {message}": "O teste de ligação do n8n falhou: {message}", + "n8n connection test successful": "Il test da colliaziun n8n è reussì", + "n8n initialization failed: {message}": "A inicialização do n8n falhou: {message}", + "n8n integration disabled": "L'integraziun n8n è deactivada", + "n8n integration enabled": "L'integraziun n8n è activada", + "n8n project initialized successfully": "Il project n8n è vegnì inizialisà cun success", + "n8n settings saved successfully": "Las configuraziuns n8n èn vegnidas memorisadas cun success", + "notify_push configuration guide": "Guida da configuraziun da notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push è installà ma OpenRegister n'ha anc betg confermà in push reussì. Declanschai in memorisar d'object per activar, u controllai Vossa configuraziun da notify_push.", + "notify_push is installed but not yet active": "notify_push è installà ma anc betg activ", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objects", + "objects processed": "objects elavurads", + "object{plural}": "objeto{plural}", + "or pick a range": "u tscherni ina perioda", + "org-...": "org-...", + "pending": "pendente", + "provides a good balance between speed and accuracy.": "porscha in bun equiliber tranter sveltezza e precisiun.", + "public": "público", + "read": "ler", + "register(s)": "Register(s)", + "register(s) selected": "Register(s) tschernì(s)", + "register{plural}": "registo{plural}", + "rejected": "rejeitado", + "required": "obligatoric", + "results": "resultats", + "schema(s)": "Schema(s)", + "schema(s) selected": "Schema(s) tschernì(s)", + "schema{plural}": "esquema{plural}", + "selected objects": "objetos selecionados", + "set": "definir", + "sk-...": "sk-...", + "this application": "questa applicaziun", + "to": "a", + "type must be \"positive\" or \"negative\"": "o tipo tem de ser \"positive\" ou \"negative\"", + "update": "atualizar", + "vectorized": "vectorisà", + "view": "ver", + "views selected": "vistas tschernidas", + "waiting": "em espera", + "{active} active, {deleted} deleted": "{active} ativos, {deleted} eliminados", + "{count} audit trails cleared successfully": "{count} registos de auditoria limpos com sucesso", + "{count} compliance issue(s) detected": "{count} problema(s) de conformidade detetado(s)", + "{count} deletions": "{count} eliminações", + "{count} documents found": "{count} documentos encontrados", + "{count} email": "{count} email", + "{count} emails": "{count} emails", + "{count} entries": "{count} entradas", + "{count} issues": "{count} problemas", + "{count} members": "{count} membros", + "{count} searches": "{count} pesquisas", + "{count} selected": "{count} selecionado(s)", + "{count} widget(s)": "{count} widget(s)", + "{matched} object(s) matched. {erased} erased.": "{matched} objeto(s) correspondido(s). {erased} apagado(s).", + "{minutes} minutes ago": "há {minutes} minutos", + "{percentage}% of objects": "{percentage}% dos objetos", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} en {register} / {schema}", + "{type} cache cleared successfully": "Cache {type} limpa com sucesso", + "ℹ️ Current Configuration": "ℹ️ Configuraziun actuala", + "ℹ️ current configuration": "ℹ️ configuração atual", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ PERIGO: Todos os {total} objetos serão ELIMINADOS PERMANENTEMENTE da base de dados. Esta ação é IRRECUPERÁVEL e não pode ser anulada!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVISO DE ELIMINAÇÃO PERMANENTE ⚠️\n\nEstá prestes a eliminar PERMANENTEMENTE TODOS os objetos do esquema \"{schema}\":\n\n• Objetos ativos: {active}\n• Objetos eliminados de forma reversível: {deleted}\n• Total: {total}\n\nEstes objetos serão completamente removidos da base de dados e NÃO PODEM ser recuperados.\n\nTem a certeza absoluta?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVISO DE ELIMINAÇÃO PERMANENTE ⚠️\n\nEstá prestes a eliminar PERMANENTEMENTE {count} objetos eliminados de forma reversível do esquema \"{schema}\".\n\nEstes objetos serão completamente removidos da base de dados e NÃO PODEM ser recuperados.\n\nTem a certeza absoluta?", + "⚡ Batch Processing": "⚡ Elavuraziun en batch", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ O SOLR consegue processar milhões de documentos com tempos de resposta de pesquisa inferiores a um segundo...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Context cumplet ✓ Damain probabel da mancar detagls ✗ Respostas pli plaunas ✗ Po includer infurmaziuns main relevantas", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Respostas pli sveltas ✓ Respostas pli focusadas ✗ Po mancar infurmaziuns relevantas", + "✨ AI Features": "✨ Funcziuns IA", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 No modo distribuído, o SOLR utiliza o ZooKeeper para a coordenação do cluster...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 A pesquisa por facetas permite aos utilizadores aprofundar os resultados por categorias...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Sabia que? O SOLR alimenta a pesquisa da Netflix, da Apple e de muitos outros grandes sites!", + "💰 View Selection (Cost Optimization)": "💰 Selecziun da vistas (optimaziun dals custs)", + "📄 Object Serialization": "📄 Serialisaziun da l'object", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 O SOLR fornece análises e estatísticas ricas sobre o desempenho da pesquisa...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Os ConfigSets contêm o esquema e os ficheiros de configuração do seu índice de pesquisa...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 A replicação garante que o seu índice de pesquisa está disponível mesmo que os nós falhem...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 O SOLR é uma poderosa plataforma de pesquisa empresarial construída sobre o Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Configuraziuns da vectorisaziun", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 O escalonamento automático pode adicionar ou remover nós dinamicamente com base na carga...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ As funcionalidades de segurança incluem autenticação, autorização e encriptação SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "pluralForm": "nplurals=2; plural=(n != 1);", + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-mails" + ], + "Purge {count} object from database": [ + "Expurgar {count} objeto da base de dados", + "Expurgar {count} objetos da base de dados" + ], + "Restore {count} object": [ + "Restaurar {count} objeto", + "Restaurar {count} objetos" + ], + "Successfully restored {count} object": [ + "{count} objeto restaurado com sucesso", + "{count} objetos restaurados com sucesso" + ], + "Delete {count} object": [ + "Eliminar {count} objeto", + "Eliminar {count} objetos" + ], + "Object successfully deleted": [ + "Objeto eliminado com sucesso", + "Objetos eliminados com sucesso" + ] + } +} diff --git a/l10n/ro.js b/l10n/ro.js new file mode 100644 index 0000000000..1181f7e81a --- /dev/null +++ b/l10n/ro.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(fără subiect)", + "(no title)" : "(fără titlu)", + "(unnamed)" : "(fără nume)", + "(untitled card)" : "(card fără titlu)", + "Activities" : "Activități", + "Add Endpoint" : "Adăugare endpoint", + "Add Source" : "Adăugare sursă", + "Add card" : "Adăugare card", + "Add contact" : "Adăugare contact", + "All clear" : "Totul în regulă", + "Articles" : "Articole", + "Code" : "Cod", + "Compliance" : "Conformitate", + "Contacts" : "Contacte", + "Create Agent" : "Creare agent", + "Create Application" : "Creare aplicație", + "Create event" : "Creare eveniment", + "Erasure complete" : "Ștergere finalizată", + "Erasure preview" : "Previzualizare ștergere", + "Expand details" : "Extindere detalii", + "Save changes" : "Salvare modificări", + "📄 Object Serialization" : "📄 Serializare obiect", + "🔢 Vectorization Settings" : "🔢 Setări vectorizare", + "💰 View Selection (Cost Optimization)" : "💰 Selectare vizualizare (optimizare costuri)", + "AI Assistant" : "Asistent AI", + "AI Chat" : "Chat AI", + "API Key" : "Cheie API", + "API tokens saved successfully" : "Token-urile API au fost salvate cu succes", + "About ConfigSets" : "Despre ConfigSets", + "Action" : "Acțiune", + "Action Distribution" : "Distribuție acțiuni", + "Action:" : "Acțiune:", + "Actions" : "Acțiuni", + "Active" : "Activ", + "Active Collections" : "Colecții active", + "Active Filters" : "Filtre active", + "Active Organisations" : "Organizații active", + "Active filters:" : "Filtre active:", + "Active organisation set successfully" : "Organizația activă a fost setată cu succes", + "Activity Period" : "Perioadă de activitate", + "Add Groups" : "Adăugare grupuri", + "Add Schema" : "Adăugare Schemă", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Adăugați titluri de schemă, descrieri și informații despre registru pentru a oferi un context mai bogat pentru căutare", + "Add to favorites" : "Adăugare la favorite", + "Added" : "Adăugat", + "Added to favorites" : "Adăugat la favorite", + "Additional Information" : "Informații suplimentare", + "Additional feedback saved. Thank you!" : "Feedback-ul suplimentar a fost salvat. Vă mulțumim!", + "Admin" : "Administrator", + "Advanced" : "Avansat", + "Advanced Filters" : "Filtre avansate", + "Advanced Options" : "Opțiuni avansate", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Filtrele avansate nu sunt disponibile când se utilizează sursa bazei de date. Comutați la Auto sau SOLR Index pentru opțiuni de filtrare.", + "After" : "După", + "Agent deleted successfully" : "Agentul a fost șters cu succes", + "Agents" : "Agenți", + "All" : "Toate", + "All Categories" : "Toate categoriile", + "All Collections" : "Toate colecțiile", + "All Confidence Levels" : "Toate nivelurile de încredere", + "All ConfigSets" : "Toate ConfigSets", + "All Files" : "Toate fișierele", + "All Levels" : "Toate nivelurile", + "All Types" : "Toate tipurile", + "All Webhooks" : "Toate webhook-urile", + "All actions" : "Toate acțiunile", + "All audit trails cleared successfully" : "Toate jurnalele de audit au fost șterse cu succes", + "All registers" : "Toate registrele", + "All schemas" : "Toate schemele", + "All searches" : "Toate căutările", + "All users" : "Toți utilizatorii", + "An error occurred while clearing audit trails" : "A apărut o eroare la ștergerea jurnalelor de audit", + "An error occurred while deleting the audit trail" : "A apărut o eroare la ștergerea jurnalului de audit", + "An error occurred while deleting the view" : "A apărut o eroare la ștergerea vizualizării", + "An error occurred while permanently deleting the objects" : "A apărut o eroare la ștergerea definitivă a obiectelor", + "Analysis completed:" : "Analiză finalizată:", + "Analysis steps:" : "Pași de analiză:", + "Analytics" : "Analize", + "Analyze Objects" : "Analizare obiecte", + "Analyze Properties" : "Analizare proprietăți", + "Analyze existing properties for improvement opportunities" : "Analizați proprietățile existente pentru oportunități de îmbunătățire", + "Analyzing..." : "Se analizează...", + "Any favorites and sharing settings for this view" : "Orice setări de favorite și partajare pentru această vizualizare", + "Any user" : "Orice utilizator", + "App store cache invalidated successfully" : "Cache-ul App Store a fost invalidat cu succes", + "Application deleted successfully" : "Aplicația a fost ștearsă cu succes", + "Applications" : "Aplicații", + "Apply Changes" : "Aplicare modificări", + "Archive" : "Arhivare", + "Archive conversation" : "Arhivare conversație", + "Archived conversations are hidden from your active list" : "Conversațiile arhivate sunt ascunse din lista dumneavoastră activă", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Sigur doriți să curățați jurnalele de căutare vechi? Aceasta va șterge intrările mai vechi de 30 de zile.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Sigur doriți să ștergeți jurnalele de audit selectate? Această acțiune nu poate fi anulată.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Sigur doriți să ștergeți jurnalele de căutare selectate? Această acțiune nu poate fi anulată.", + "Are you sure you want to delete this ConfigSet?" : "Sigur doriți să ștergeți acest ConfigSet?", + "Are you sure you want to permanently delete" : "Sigur doriți să ștergeți definitiv", + "Ask a question..." : "Adresați o întrebare...", + "Ask questions about your data using natural language" : "Adresați întrebări despre datele dumneavoastră folosind limbaj natural", + "Attempt" : "Încercare", + "Audit Trail Changes" : "Modificări jurnal de audit", + "Audit Trail Details" : "Detalii jurnal de audit", + "Audit Trail Management" : "Gestionare jurnal de audit", + "Audit Trail Statistics" : "Statistici jurnal de audit", + "Audit Trails" : "Jurnale de audit", + "Audit trail data copied to clipboard" : "Datele jurnalului de audit au fost copiate în clipboard", + "Audit trail deleted successfully" : "Jurnalul de audit a fost șters cu succes", + "Audit trail successfully deleted" : "Jurnalul de audit a fost șters cu succes", + "Authentication" : "Autentificare", + "Auto-commit disabled" : "Auto-commit dezactivat", + "Auto-commit enabled" : "Auto-commit activat", + "Auto-create Default Organisation" : "Creare automată organizație implicită", + "Auto-retry failed vectorizations" : "Reîncercare automată a vectorizărilor eșuate", + "Automatically commit changes to SOLR index" : "Aplicarea automată a modificărilor în indexul SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Creează automat o organizație implicită dacă nu există niciuna la inițializarea aplicației", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Generează automat embeddings vectoriale din fragmente de text când fișierele sunt încărcate și procesate", + "Automatically generate vector embeddings when objects are created or updated" : "Generează automat embeddings vectoriale când obiectele sunt create sau actualizate", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Reîncearcă automat încercările de vectorizare eșuate (maximum 3 reîncercări)", + "Available Filters" : "Filtre disponibile", + "Available Workflows" : "Fluxuri de lucru disponibile", + "Avg Execution Time" : "Timp mediu de execuție", + "Avg Members/Org" : "Membri medii/organizație", + "Avg Object Views/Session" : "Vizualizări medii de obiecte/sesiune", + "Avg Response Time" : "Timp mediu de răspuns", + "Avg Results/Search" : "Rezultate medii/căutare", + "Avg Searches/Session" : "Căutări medii/sesiune", + "Back" : "Înapoi", + "Back to Entities" : "Înapoi la entități", + "Back to Registers" : "Înapoi la registre", + "Back to Webhooks" : "Înapoi la webhook-uri", + "Back to applications" : "Înapoi la aplicații", + "Back to entities" : "Înapoi la entități", + "Backend updated successfully. Please reload the application." : "Backend-ul a fost actualizat cu succes. Vă rugăm să reîncărcați aplicația.", + "Base URL (Optional)" : "URL de bază (opțional)", + "Basic Information" : "Informații de bază", + "Batch Size" : "Dimensiune lot", + "Batch extraction completed" : "Extragerea în lot a fost finalizată", + "Before" : "Înainte", + "Before object vectorization can work:" : "Înainte ca vectorizarea obiectelor să poată funcționa:", + "Behavior Issue" : "Problemă de comportament", + "Bookmarks" : "Marcaje", + "Blob storage has been retired. All objects now use magic tables." : "Stocarea blob a fost retrasă. Toate obiectele folosesc acum tabele magice.", + "Branch" : "Ramură", + "Bulk delete operation completed successfully" : "Operațiunea de ștergere în masă a fost finalizată cu succes", + "Bulk save operation completed successfully" : "Operațiunea de salvare în masă a fost finalizată cu succes", + "Business Data" : "Date de afaceri", + "Cache cleared successfully" : "Cache-ul a fost șters cu succes", + "Calculate Sizes" : "Calculare dimensiuni", + "Cancel" : "Anulare", + "Cannot delete: objects are still attached" : "Nu se poate șterge: obiectele sunt încă atașate", + "Category" : "Categorie", + "Change Type" : "Tip modificare", + "Changes" : "Modificări", + "Changes copied to clipboard" : "Modificările au fost copiate în clipboard", + "Chat Model" : "Model de chat", + "Chat Provider" : "Furnizor de chat", + "Chat Provider (RAG)" : "Furnizor de chat (RAG)", + "Chat Settings" : "Setări chat", + "Chat provider connection successful!" : "Conexiunea cu furnizorul de chat a reușit!", + "Chat settings" : "Setări chat", + "Choose a register" : "Alegeți un registru", + "Choose a schema" : "Alegeți o schemă", + "Choose how vector similarity calculations are performed for semantic search" : "Alegeți modul în care sunt efectuate calculele de similaritate vectorială pentru căutarea semantică", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Alegeți ce tipuri de fișiere să includeți în procesul de vectorizare. Vor fi procesate doar fișierele cu text și fragmente extrase.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Alegeți ce vizualizări să includeți în procesul de vectorizare. Lăsați gol pentru a procesa toate vizualizările pe baza configurației dumneavoastră.", + "Chunk" : "Fragment", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Ștergerea fragmentelor nu este încă implementată. Utilizați endpoint-urile bazate pe fragmente.", + "Chunks" : "Fragmente", + "Chunks to Vectorize:" : "Fragmente de vectorizat:", + "Cleanup Old Trails" : "Curățare jurnale vechi", + "Cleanup completed" : "Curățare finalizată", + "Clear All Embeddings" : "Ștergere toate embeddings", + "Clear Entries" : "Ștergere intrări", + "Clear Filtered Audit Trails" : "Ștergere jurnale de audit filtrate", + "Clear Filters" : "Ștergere filtre", + "Clear Index" : "Ștergere index", + "Clear Selection" : "Ștergere selecție", + "Clear all" : "Ștergere tot", + "Clear all filters" : "Ștergere toate filtrele", + "Clear date range" : "Ștergere interval de date", + "Clearing..." : "Se șterge...", + "Close" : "Închidere", + "Collection Management" : "Gestionare colecții", + "Collection Name" : "Nume colecție", + "Collection assignments updated successfully" : "Atribuirile colecției au fost actualizate cu succes", + "Collection cleared successfully" : "Colecția a fost ștearsă cu succes", + "Collection copied successfully" : "Colecția a fost copiată cu succes", + "Collection created successfully" : "Colecția a fost creată cu succes", + "Collection deleted successfully" : "Colecția a fost ștearsă cu succes", + "Collection used to store and index file metadata and content" : "Colecție utilizată pentru a stoca și indexa metadatele și conținutul fișierelor", + "Collection used to store and index object data" : "Colecție utilizată pentru a stoca și indexa datele obiectelor", + "Collections:" : "Colecții:", + "Commit Message" : "Mesaj commit", + "Commit Within (ms)" : "Commit în interval de (ms)", + "Commit message" : "Mesaj commit", + "Compare current schema with real object data" : "Comparați schema curentă cu datele reale ale obiectelor", + "Completed" : "Finalizat", + "Complex" : "Complex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Interogări complexe: căutări avansate cu filtre multiple, operatori și combinații complexe de parametri", + "Confidence Level" : "Nivel de încredere", + "Confidence Score" : "Scor de încredere", + "Config must be provided as an object" : "Configurația trebuie furnizată ca obiect", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Gestionare ConfigSet", + "ConfigSet Name" : "Nume ConfigSet", + "ConfigSet created successfully" : "ConfigSet creat cu succes", + "ConfigSet deleted successfully" : "ConfigSet șters cu succes", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets definesc schema și configurația pentru colecțiile dumneavoastră SOLR. Acestea conțin definiții de câmpuri, analizoare și alte setări de căutare.", + "Configuration" : "Configurație", + "Configuration Data" : "Date de configurație", + "Configuration saved" : "Configurație salvată", + "Configurations" : "Configurații", + "Configure Facets" : "Configurare fațete", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Configurați furnizorii de modele lingvistice mari (LLM) pentru funcționalități bazate pe AI, inclusiv căutare semantică, embeddings și chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Configurați setările de conexiune de bază pentru serverul dumneavoastră SOLR, inclusiv autentificarea și opțiunile de rețea. Utilizați dialogurile separate de gestionare ConfigSet și colecții pentru a gestiona nucleele și colecțiile.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Configurați modul în care obiectele din baza de date sunt convertite în embeddings vectoriale pentru căutarea semantică. Obiectele sunt vectorizate direct fără a necesita extragerea textului.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Configurați modul în care obiectele sunt convertite în text înainte de vectorizare. Aceste setări afectează calitatea și contextul căutării.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Configurați parametrii pentru vectorizarea fișierelor. Acest proces va genera embeddings vectoriale pentru toate fragmentele de fișiere extrase.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Configurați parametrii pentru vectorizarea obiectelor. Acest proces va genera embeddings vectoriale pentru toate obiectele care corespund filtrelor vizualizării dumneavoastră.", + "Configure which types of data to search and how many sources to retrieve" : "Configurați ce tipuri de date să căutați și câte surse să recuperați", + "Connection Failed" : "Conexiune eșuată", + "Connection Settings" : "Setări conexiune", + "Connection Successful!" : "Conexiune reușită!", + "Connection failed" : "Conexiune eșuată", + "Connection protocol" : "Protocol de conexiune", + "Connection successful" : "Conexiune reușită", + "Connection timeout in seconds" : "Timeout conexiune în secunde", + "Constant delay between retries (5 minutes)" : "Întârziere constantă între reîncercări (5 minute)", + "Constraint Issue" : "Problemă de constrângere", + "Content Filters" : "Filtre de conținut", + "Continue to Properties" : "Continuați la proprietăți", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Controlați ce vizualizări de obiecte ar trebui vectorizate pentru a reduce costurile API. Vectorizați doar vizualizările care beneficiază de căutarea semantică.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Controlați ce vizualizări și instrumente poate utiliza AI în această conversație. În mod implicit, toate capacitățile agentului sunt activate.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Controlează aleatoriul (0 = determinist, 2 = foarte creativ)", + "Conversation ID is required" : "ID-ul conversației este obligatoriu", + "Conversation archived" : "Conversație arhivată", + "Conversation archived successfully" : "Conversația a fost arhivată cu succes", + "Conversation cleared successfully" : "Conversația a fost ștearsă cu succes", + "Conversation deleted" : "Conversație ștearsă", + "Conversation permanently deleted" : "Conversație ștearsă definitiv", + "Conversation renamed" : "Conversație redenumită", + "Conversation restored" : "Conversație restaurată", + "Conversation title" : "Titlu conversație", + "Copied!" : "Copiat!", + "Copy" : "Copiere", + "Copy Changes" : "Copiere modificări", + "Copy Collection" : "Copiere colecție", + "Copy Data" : "Copiere date", + "Copy Full Data" : "Copiere date complete", + "Copying..." : "Se copiază...", + "Count" : "Număr", + "Counting objects..." : "Se numără obiectele...", + "Costs" : "Costuri", + "Create" : "Creare", + "Create Collection" : "Creare colecție", + "Create ConfigSet" : "Creare ConfigSet", + "Create New Collection" : "Creare colecție nouă", + "Create New ConfigSet" : "Creare ConfigSet nou", + "Create Operations" : "Operațiuni de creare", + "Create Webhook" : "Creare webhook", + "Create a copy of collection:" : "Creați o copie a colecției:", + "Create a new ConfigSet based on the _default template" : "Creați un ConfigSet nou pe baza șablonului _default", + "Create a new SOLR collection from an existing ConfigSet" : "Creați o colecție SOLR nouă dintr-un ConfigSet existent", + "Create your first AI agent to get started." : "Creați primul dumneavoastră agent AI pentru a începe.", + "Created" : "Creat", + "Created:" : "Creat:", + "Creating..." : "Se creează...", + "Current Filters" : "Filtre curente", + "Current Type:" : "Tip curent:", + "Custom API endpoint if using a different region" : "Endpoint API personalizat dacă se utilizează o regiune diferită", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Anteturi HTTP personalizate (unul pe linie, format: Nume-Antet: valoare)", + "Daily" : "Zilnic", + "Dashboard" : "Tablou de bord", + "Data Source" : "Sursă de date", + "Data sources" : "Surse de date", + "Data type does not match observed values" : "Tipul de date nu corespunde valorilor observate", + "Database URL" : "URL bază de date", + "Database information refreshed" : "Informațiile despre baza de date au fost reîmprospătate", + "Date Range" : "Interval de date", + "Default" : "Implicit", + "Default Organisation" : "Organizație implicită", + "Default: 5" : "Implicit: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Întârzierile se dublează cu fiecare încercare (2, 4, 8 minute...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Întârzierile cresc liniar (5, 10, 15 minute...)", + "Delete" : "Ștergere", + "Delete Application" : "Ștergere aplicație", + "Delete Audit Trail" : "Ștergere jurnal de audit", + "Delete Collection" : "Ștergere colecție", + "Delete ConfigSet" : "Ștergere ConfigSet", + "Delete Objects" : "Ștergere obiecte", + "Delete Operations" : "Operațiuni de ștergere", + "Delete View" : "Ștergere vizualizare", + "Delete all objects in this schema" : "Ștergeți toate obiectele din această schemă", + "Delete permanently" : "Ștergere definitivă", + "Delete this conversation?" : "Ștergeți această conversație?", + "Delete view" : "Ștergere vizualizare", + "Deleted" : "Șters", + "Deleted By" : "Șters de", + "Deleted Date" : "Data ștergerii", + "Deleted Items Management" : "Gestionare elemente șterse", + "Deleted This Week" : "Șterse săptămâna aceasta", + "Deleted Today" : "Șterse astăzi", + "Deleted:" : "Șters:", + "Deleting..." : "Se șterge...", + "Deletion Date Range" : "Interval de date al ștergerii", + "Deletion Statistics" : "Statistici de ștergere", + "Deprecated" : "Învechit", + "Description" : "Descriere", + "Description:" : "Descriere:", + "Detect data types and patterns" : "Detectare tipuri de date și modele", + "Detected At" : "Detectat la", + "Detected Format:" : "Format detectat:", + "Detected Issues:" : "Probleme detectate:", + "Disable" : "Dezactivare", + "Disabled" : "Dezactivat", + "Discover Files" : "Descoperire fișiere", + "Discovered Properties" : "Proprietăți descoperite", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Doriți să ștergeți definitiv toate intrările filtrate ale jurnalului de audit? Această acțiune nu poate fi anulată.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Doriți să ștergeți definitiv această intrare a jurnalului de audit? Această acțiune nu poate fi anulată.", + "Documents" : "Documente", + "Download API Spec" : "Descărcare specificație API", + "Edit" : "Editare", + "Edit Register" : "Editare Registru", + "Edit Schema" : "Editare Schemă", + "Edit View" : "Editare vizualizare", + "Edit Webhook" : "Editare webhook", + "Edit view" : "Editare vizualizare", + "Edit view details" : "Editare detalii vizualizare", + "Effectiveness" : "Eficacitate", + "Email" : "E-mail", + "Emails" : "E-mailuri", + "Embedding Model" : "Model de embedding", + "Embedding Provider" : "Furnizor de embedding", + "Embedding generated successfully" : "Embedding-ul a fost generat cu succes", + "Embedding provider connection successful!" : "Conexiunea cu furnizorul de embedding a reușit!", + "Enable" : "Activare", + "Enable auto-creation" : "Activare creare automată", + "Enable automatic file vectorization" : "Activare vectorizare automată a fișierelor", + "Enable automatic object vectorization" : "Activare vectorizare automată a obiectelor", + "Enable automatic synchronization" : "Activare sincronizare automată", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Activare jurnalizare detaliată pentru operațiunile SOLR (recomandat pentru depanare)", + "Enable faceting" : "Activare fațetare", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Activați sau dezactivați funcționalitățile LLM. Configurați furnizorii și modelele folosind butonul Configurație LLM de mai sus.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Activați sau dezactivați integrarea căutării SOLR. Configurați setările de conexiune folosind butonul Setări conexiune de mai sus.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Activați sau dezactivați integrarea fluxurilor de lucru n8n. Configurați setările de conexiune mai jos.", + "Enable or disable this webhook" : "Activați sau dezactivați acest webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Activați vectorizarea pentru toate vizualizările existente și viitoare (poate crește costurile)", + "Enabled" : "Activat", + "Endpoints" : "Endpoint-uri", + "Enter ConfigSet name" : "Introduceți numele ConfigSet", + "Enter collection name" : "Introduceți numele colecției", + "Enter description (optional)..." : "Introduceți descrierea (opțional)...", + "Enter new collection name" : "Introduceți numele noii colecții", + "Enter object ID" : "Introduceți ID-ul obiectului", + "Enter search term" : "Introduceți termenul de căutare", + "Enter view name..." : "Introduceți numele vizualizării...", + "Enter webhook name" : "Introduceți numele webhook-ului", + "Entities" : "Entități", + "Entity Information" : "Informații entitate", + "Entity deleted successfully" : "Entitatea a fost ștearsă cu succes", + "Entity not found" : "Entitatea nu a fost găsită", + "Entries to be deleted:" : "Intrări care vor fi șterse:", + "Enum Issue" : "Problemă enum", + "Enum constraint is missing" : "Constrângerea enum lipsește", + "Error" : "Eroare", + "Error Details" : "Detalii eroare", + "Error Information" : "Informații eroare", + "Error Message" : "Mesaj de eroare", + "Error loading application" : "Eroare la încărcarea aplicației", + "Error loading audit trails" : "Eroare la încărcarea jurnalelor de audit", + "Error loading entity" : "Eroare la încărcarea entității", + "Error loading search trails" : "Eroare la încărcarea jurnalelor de căutare", + "Estimated Batches:" : "Loturi estimate:", + "Estimated Cost:" : "Cost estimat:", + "Estimated Duration:" : "Durată estimată:", + "Event" : "Eveniment", + "Event Property for Payload" : "Proprietate eveniment pentru payload", + "Events" : "Evenimente", + "Example Value" : "Valoare exemplu", + "Exclusive Maximum" : "Maxim exclusiv", + "Exclusive Minimum" : "Minim exclusiv", + "Execution Mode" : "Mod de execuție", + "Execution Time" : "Timp de execuție", + "Execution Time Range" : "Interval de timp de execuție", + "Existing Improvements" : "Îmbunătățiri existente", + "Exponential" : "Exponențial", + "Export" : "Export", + "Export completed successfully" : "Exportul a fost finalizat cu succes", + "Export, view, or delete audit trails" : "Exportați, vizualizați sau ștergeți jurnale de audit", + "Extend Schema" : "Extindere Schemă", + "Extended" : "Extins", + "Extensions" : "Extensii", + "Extract Pending Files" : "Extragere fișiere în așteptare", + "Extract properties from each object" : "Extragere proprietăți din fiecare obiect", + "Extracted At" : "Extras la", + "Extraction Status" : "Stare extragere", + "Facet configuration updated successfully" : "Configurația fațetelor a fost actualizată cu succes", + "Facets discovered and configured successfully" : "Fațetele au fost descoperite și configurate cu succes", + "Facets discovered successfully" : "Fațetele au fost descoperite cu succes", + "Failed" : "Eșuat", + "Failed to analyze schema properties" : "Analiza proprietăților schemei a eșuat", + "Failed to archive conversation" : "Arhivarea conversației a eșuat", + "Failed to calculate sizes" : "Calcularea dimensiunilor a eșuat", + "Failed to clear collection" : "Ștergerea colecției a eșuat", + "Failed to copy changes" : "Copierea modificărilor a eșuat", + "Failed to copy data" : "Copierea datelor a eșuat", + "Failed to copy data to clipboard" : "Copierea datelor în clipboard a eșuat", + "Failed to create conversation" : "Crearea conversației a eșuat", + "Failed to create or find project" : "Crearea sau găsirea proiectului a eșuat", + "Failed to delete collection" : "Ștergerea colecției a eșuat", + "Failed to delete conversation" : "Ștergerea conversației a eșuat", + "Failed to delete webhook" : "Ștergerea webhook-ului a eșuat", + "Failed to download API specification" : "Descărcarea specificației API a eșuat", + "Failed to get SOLR field configuration" : "Obținerea configurației câmpului SOLR a eșuat", + "Failed to get entity categories" : "Obținerea categoriilor de entități a eșuat", + "Failed to get entity statistics" : "Obținerea statisticilor entităților a eșuat", + "Failed to get entity types" : "Obținerea tipurilor de entități a eșuat", + "Failed to load LLM configuration" : "Încărcarea configurației LLM a eșuat", + "Failed to load Nextcloud groups" : "Încărcarea grupurilor Nextcloud a eșuat", + "Failed to load conversation" : "Încărcarea conversației a eșuat", + "Failed to load entities" : "Încărcarea entităților a eșuat", + "Failed to load entity" : "Încărcarea entității a eșuat", + "Failed to load files" : "Încărcarea fișierelor a eșuat", + "Failed to load organisations" : "Încărcarea organizațiilor a eșuat", + "Failed to load templates" : "Încărcarea șabloanelor a eșuat", + "Failed to load webhooks" : "Încărcarea webhook-urilor a eșuat", + "Failed to load workflows" : "Încărcarea fluxurilor de lucru a eșuat", + "Failed to refresh database information" : "Reîmprospătarea informațiilor despre baza de date a eșuat", + "Failed to reindex collection" : "Reindexarea colecției a eșuat", + "Failed to rename conversation" : "Redenumirea conversației a eșuat", + "Failed to restore conversation" : "Restaurarea conversației a eșuat", + "Failed to retry extraction" : "Reîncercarea extragerii a eșuat", + "Failed to retry webhook" : "Reîncercarea webhook-ului a eșuat", + "Failed to save GitHub token" : "Salvarea token-ului GitHub a eșuat", + "Failed to save GitLab URL" : "Salvarea URL-ului GitLab a eșuat", + "Failed to save GitLab token" : "Salvarea token-ului GitLab a eșuat", + "Failed to save additional feedback" : "Salvarea feedback-ului suplimentar a eșuat", + "Failed to save n8n configuration" : "Salvarea configurației n8n a eșuat", + "Failed to save roles" : "Salvarea rolurilor a eșuat", + "Failed to save settings" : "Salvarea setărilor a eșuat", + "Failed to save webhook" : "Salvarea webhook-ului a eșuat", + "Failed to send feedback" : "Trimiterea feedback-ului a eșuat", + "Failed to test webhook" : "Testarea webhook-ului a eșuat", + "Failed to update favorite status" : "Actualizarea stării de favorit a eșuat", + "Failed to update schema properties" : "Actualizarea proprietăților schemei a eșuat", + "Failed to update view" : "Actualizarea vizualizării a eșuat", + "Failed to update webhook" : "Actualizarea webhook-ului a eșuat", + "Feedback recorded" : "Feedback înregistrat", + "Fewer sources (1-3):" : "Mai puține surse (1-3):", + "Field" : "Câmp", + "File" : "Fișier", + "File Chunk Prediction" : "Predicție fragmente fișier", + "File Collection" : "Colecție de fișiere", + "File Management" : "Gestionare fișiere", + "File Name" : "Nume fișier", + "File Path" : "Cale fișier", + "File Type Selection" : "Selectare tip fișier", + "File Vectorization" : "Vectorizare fișiere", + "File Warmup" : "Preîncălzire fișiere", + "File actions menu" : "Meniu acțiuni fișier", + "File anonymized successfully" : "Fișierul a fost anonimizat cu succes", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vectorizarea fragmentelor de fișiere nu este încă implementată. Fragmentele sunt pregătite și stocate, dar serviciul de vectorizare este în curs de dezvoltare.", + "File collection not configured" : "Colecția de fișiere nu este configurată", + "File discovery completed" : "Descoperirea fișierelor a fost finalizată", + "File extraction completed" : "Extragerea fișierelor a fost finalizată", + "File extraction queued" : "Extragerea fișierelor a fost pusă în coadă", + "File indexed successfully" : "Fișierul a fost indexat cu succes", + "File is already anonymized" : "Fișierul este deja anonimizat", + "File not found" : "Fișierul nu a fost găsit", + "File settings updated successfully" : "Setările fișierului au fost actualizate cu succes", + "File sources" : "Surse de fișiere", + "File vectorization configuration saved successfully" : "Configurația vectorizării fișierelor a fost salvată cu succes", + "File vectorization started. Check the statistics section for progress." : "Vectorizarea fișierelor a început. Verificați secțiunea de statistici pentru progres.", + "File warmup completed" : "Preîncălzirea fișierelor a fost finalizată", + "Files" : "Fișiere", + "Files with Completed Extraction:" : "Fișiere cu extragere finalizată:", + "Files → fileCollection, Objects → objectCollection" : "Fișiere → fileCollection, Obiecte → objectCollection", + "Filter Audit Trails" : "Filtrare jurnale de audit", + "Filter Deleted Items" : "Filtrare elemente șterse", + "Filter Objects" : "Filtrare obiecte", + "Filter Properties" : "Filtrare proprietăți", + "Filter Search Trails" : "Filtrare jurnale de căutare", + "Filter Statistics" : "Filtrare statistici", + "Filter and analyze search trail entries" : "Filtrați și analizați intrările jurnalului de căutare", + "Filter and manage audit trail entries" : "Filtrați și gestionați intrările jurnalului de audit", + "Filter and manage soft deleted items" : "Filtrați și gestionați elementele șterse temporar", + "Filter by object ID" : "Filtrare după ID obiect", + "Filter by search term" : "Filtrare după termen de căutare", + "Filter by webhook" : "Filtrare după webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Datele de filtrare s-au încărcat automat. Utilizați filtrele de mai jos pentru a vă rafina căutarea.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrați declanșatoarele webhook după proprietățile payload (unul pe linie, format: cheie: valoare)", + "Filtered" : "Filtrat", + "Filters" : "Filtre", + "Fireworks AI Chat Settings" : "Setări chat Fireworks AI", + "Fireworks AI Embedding Configuration" : "Configurație embedding Fireworks AI", + "First" : "Primul", + "Fixed" : "Fix", + "For chat and retrieval-augmented generation" : "Pentru chat și generare augmentată prin recuperare", + "For vector embeddings and semantic search" : "Pentru embeddings vectoriale și căutare semantică", + "Format" : "Format", + "Format Issue" : "Problemă de format", + "Format constraint is missing" : "Constrângerea de format lipsește", + "From Date" : "De la data", + "From date" : "De la data", + "Full data copied to clipboard" : "Datele complete au fost copiate în clipboard", + "General Issue" : "Problemă generală", + "Generate recommendations and confidence scores" : "Generare recomandări și scoruri de încredere", + "Generate vectors immediately when new objects are created" : "Generează vectori imediat când sunt create obiecte noi", + "GitHub token is valid" : "Token-ul GitHub este valid", + "GitHub token removed successfully" : "Token-ul GitHub a fost eliminat cu succes", + "GitHub token saved successfully" : "Token-ul GitHub a fost salvat cu succes", + "GitLab URL saved successfully" : "URL-ul GitLab a fost salvat cu succes", + "GitLab token is valid" : "Token-ul GitLab este valid", + "GitLab token saved successfully" : "Token-ul GitLab a fost salvat cu succes", + "HTTP Method" : "Metodă HTTP", + "HTTP method used to send webhook requests" : "Metoda HTTP utilizată pentru a trimite cereri webhook", + "Headers" : "Anteturi", + "Health" : "Stare de sănătate", + "Heartbeat successful - connection kept alive" : "Heartbeat reușit - conexiunea menținută activă", + "Helpful" : "Util", + "Hide Filters" : "Ascundere filtre", + "Hide in forms" : "Ascundere în formulare", + "Hide in list view" : "Ascundere în vizualizarea listă", + "High" : "Ridicat", + "High Confidence" : "Încredere ridicată", + "Host" : "Gazdă", + "Hourly" : "Orar", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Cât de adânc să parcurgeți proprietățile imbricate ale obiectului (1-20). Valorile mai mari captează mai multe detalii, dar cresc dimensiunea vectorului.", + "How often to check for updates (1-168 hours)" : "Cât de des să se verifice actualizările (1-168 ore)", + "How to handle retries for failed webhook deliveries" : "Cum să se gestioneze reîncercările pentru livrările de webhook eșuate", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Limitele de rată IP au fost șterse cu succes", + "Identify properties not in the schema" : "Identificare proprietăți care nu sunt în schemă", + "Immutable" : "Imutabil", + "Import" : "Import", + "Import successful" : "Import reușit", + "Improved Property" : "Proprietate îmbunătățită", + "In use" : "În uz", + "Inactive" : "Inactiv", + "Include IDs and names of related objects for better contextual search" : "Includeți ID-urile și numele obiectelor asociate pentru o căutare contextuală mai bună", + "Include related object references" : "Includere referințe la obiecte asociate", + "Include schema and register metadata" : "Includere metadate schemă și registru", + "Initialization failed" : "Inițializarea a eșuat", + "Initialize Project" : "Inițializare proiect", + "Inspect Fields" : "Inspectare câmpuri", + "Inspect Index" : "Inspectare index", + "Invalid" : "Nevalid", + "Invalid batch size. Must be between 1 and 5000" : "Dimensiune lot nevalidă. Trebuie să fie între 1 și 5000", + "Invalid field name provided" : "Nume de câmp nevalid furnizat", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects nevalid. Trebuie să fie 0 (toate) sau un număr pozitiv", + "Invalid:" : "Nevalid:", + "Issue" : "Problemă", + "Items per page:" : "Elemente pe pagină:", + "Just now" : "Chiar acum", + "LLM Configuration" : "Configurație LLM", + "LLM actions menu" : "Meniu acțiuni LLM", + "LLM configuration saved successfully" : "Configurația LLM a fost salvată cu succes", + "LLM features disabled" : "Funcționalitățile LLM au fost dezactivate", + "LLM features enabled" : "Funcționalitățile LLM au fost activate", + "LLM must be enabled with an embedding provider configured" : "LLM trebuie activat cu un furnizor de embedding configurat", + "LLM settings updated successfully" : "Setările LLM au fost actualizate cu succes", + "Last" : "Ultimul", + "Last Triggered" : "Ultima declanșare", + "Length Range:" : "Interval de lungime:", + "Linear" : "Liniar", + "Load Advanced Filters" : "Încărcare filtre avansate", + "Load advanced filters with live data from your search index" : "Încărcați filtre avansate cu date în timp real din indexul dumneavoastră de căutare", + "Load view" : "Încărcare vizualizare", + "Loading ConfigSets..." : "Se încarcă ConfigSets...", + "Loading advanced filters..." : "Se încarcă filtrele avansate...", + "Loading agents..." : "Se încarcă agenții...", + "Loading application details..." : "Se încarcă detaliile aplicației...", + "Loading applications..." : "Se încarcă aplicațiile...", + "Loading archived conversations..." : "Se încarcă conversațiile arhivate...", + "Loading audit trails..." : "Se încarcă jurnalele de audit...", + "Loading collections..." : "Se încarcă colecțiile...", + "Loading configurations..." : "Se încarcă configurațiile...", + "Loading conversation..." : "Se încarcă conversația...", + "Loading conversations..." : "Se încarcă conversațiile...", + "Loading deleted items..." : "Se încarcă elementele șterse...", + "Loading events..." : "Se încarcă evenimentele...", + "Loading filter data..." : "Se încarcă datele de filtrare...", + "Loading groups..." : "Se încarcă grupurile...", + "Loading log details..." : "Se încarcă detaliile jurnalului...", + "Loading registers..." : "Se încarcă registrele...", + "Loading schemas..." : "Se încarcă schemele...", + "Loading search trails..." : "Se încarcă jurnalele de căutare...", + "Loading sources..." : "Se încarcă sursele...", + "Loading statistics..." : "Se încarcă statisticile...", + "Loading users..." : "Se încarcă utilizatorii...", + "Loading views..." : "Se încarcă vizualizările...", + "Loading..." : "Se încarcă...", + "Local" : "Local", + "Local Version" : "Versiune locală", + "Locked" : "Blocat", + "Locked:" : "Blocat:", + "Login successful" : "Autentificare reușită", + "Logs" : "Jurnale", + "Low" : "Scăzut", + "Low Confidence" : "Încredere scăzută", + "Lucene Version:" : "Versiune Lucene:", + "Magic table synchronized successfully" : "Tabelul magic a fost sincronizat cu succes", + "Manage Organisation Roles" : "Gestionare roluri organizație", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Gestionați colecțiile SOLR (depozite de date) și atribuiți-le pentru obiecte și fișiere.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Gestionați ConfigSets SOLR (șabloane de configurație) pentru colecțiile dumneavoastră.", + "Manage and configure agents for automated tasks" : "Gestionați și configurați agenți pentru sarcini automatizate", + "Manage and monitor file text extraction status" : "Gestionați și monitorizați starea extragerii textului din fișiere", + "Manage and restore soft deleted items from your registers" : "Gestionați și restaurați elementele șterse temporar din registrele dumneavoastră", + "Manage and view detected entities from files and objects" : "Gestionați și vizualizați entitățile detectate din fișiere și obiecte", + "Manage document templates and themes" : "Gestionați șabloanele și temele de documente", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Gestionați fluxurile de lucru n8n pentru automatizarea OpenRegister. Fluxurile de lucru vor fi stocate în proiectul configurat.", + "Manage webhooks for event-driven integrations" : "Gestionați webhook-uri pentru integrări bazate pe evenimente", + "Manage your applications and modules" : "Gestionați aplicațiile și modulele dumneavoastră", + "Manage your data registers and their configurations" : "Gestionați registrele dumneavoastră de date și configurațiile acestora", + "Manage your data schemas and their properties" : "Gestionați schemele dumneavoastră de date și proprietățile acestora", + "Manage your data sources and their configurations" : "Gestionați sursele dumneavoastră de date și configurațiile acestora", + "Manage your saved search configurations" : "Gestionați configurațiile dumneavoastră de căutare salvate", + "Manage your system configurations and settings" : "Gestionați configurațiile și setările dumneavoastră de sistem", + "Managed" : "Gestionat", + "Max Files (0 = all)" : "Fișiere maxime (0 = toate)", + "Max Length" : "Lungime maximă", + "Max Length:" : "Lungime maximă:", + "Max Objects (0 = all)" : "Obiecte maxime (0 = toate)", + "Max Retries" : "Reîncercări maxime", + "Max Shards/Node" : "Shard-uri maxime/nod", + "Max execution time (ms)" : "Timp maxim de execuție (ms)", + "Max ms" : "ms maxim", + "Max result count" : "Număr maxim de rezultate", + "Max results" : "Rezultate maxime", + "Maximum" : "Maxim", + "Maximum Nesting Depth" : "Adâncime maximă de imbricare", + "Maximum length constraint is missing" : "Constrângerea de lungime maximă lipsește", + "Maximum length is too restrictive" : "Lungimea maximă este prea restrictivă", + "Maximum number of files to process. Set to 0 to process all files." : "Numărul maxim de fișiere de procesat. Setați la 0 pentru a procesa toate fișierele.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Numărul maxim de obiecte de procesat. Setați la 0 pentru a procesa toate obiectele.", + "Maximum number of retry attempts for failed deliveries" : "Numărul maxim de încercări de reîncercare pentru livrările eșuate", + "Maximum time to wait before committing changes" : "Timpul maxim de așteptare înainte de aplicarea modificărilor", + "Maximum tokens to generate" : "Numărul maxim de token-uri de generat", + "Maximum value constraint is missing" : "Constrângerea de valoare maximă lipsește", + "Maximum value is too restrictive" : "Valoarea maximă este prea restrictivă", + "Medium" : "Mediu", + "Medium Confidence" : "Încredere medie", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Interogări medii: căutări cu o anumită filtrare sau parametri multipli (de exemplu, intervale de date, registre/scheme specifice)", + "Members of selected groups can access this view" : "Membrii grupurilor selectate pot accesa această vizualizare", + "Members:" : "Membri:", + "Memory prediction calculated successfully" : "Predicția de memorie a fost calculată cu succes", + "Message content is required" : "Conținutul mesajului este obligatoriu", + "Message does not belong to this conversation" : "Mesajul nu aparține acestei conversații", + "Metadata Filters" : "Filtre metadate", + "Method" : "Metodă", + "Min Length" : "Lungime minimă", + "Min execution time (ms)" : "Timp minim de execuție (ms)", + "Min ms" : "ms minim", + "Min result count" : "Număr minim de rezultate", + "Min results" : "Rezultate minime", + "Minimum" : "Minim", + "Minimum value constraint is missing" : "Constrângerea de valoare minimă lipsește", + "Minimum value is too restrictive" : "Valoarea minimă este prea restrictivă", + "Missing object fields created successfully" : "Câmpurile de obiect lipsă au fost create cu succes", + "Mode:" : "Mod:", + "Model" : "Model", + "Model Name" : "Nume model", + "Modified" : "Modificat", + "Monthly" : "Lunar", + "More sources (10-20):" : "Mai multe surse (10-20):", + "Most Active Objects" : "Cele mai active obiecte", + "Multiple Of" : "Multiplu de", + "Name" : "Nume", + "Names cache warmup completed" : "Preîncălzirea cache-ului de nume a fost finalizată", + "New Collection Name" : "Nume colecție nouă", + "New Conversation" : "Conversație nouă", + "New Properties" : "Proprietăți noi", + "New Property" : "Proprietate nouă", + "New Value" : "Valoare nouă", + "New users without specific organisation membership will be automatically added to this organisation" : "Utilizatorii noi fără apartenență la o organizație specifică vor fi adăugați automat la această organizație", + "Next" : "Următorul", + "No" : "Nu", + "No ConfigSets found" : "Nu s-au găsit ConfigSets", + "No GitHub token configured" : "Niciun token GitHub configurat", + "No GitHub token provided" : "Niciun token GitHub furnizat", + "No GitLab token provided" : "Niciun token GitLab furnizat", + "No Organisation" : "Nicio organizație", + "No active filters" : "Niciun filtru activ", + "No activity data available" : "Nu există date de activitate disponibile", + "No agents available" : "Niciun agent disponibil", + "No agents found" : "Nu s-au găsit agenți", + "No app store cache exists yet" : "Nu există încă niciun cache App Store", + "No applications are available." : "Nu sunt disponibile aplicații.", + "No applications found" : "Nu s-au găsit aplicații", + "No archived conversations" : "Nicio conversație arhivată", + "No audit trail entries found" : "Nu s-au găsit intrări în jurnalul de audit", + "No changes recorded" : "Nu au fost înregistrate modificări", + "No chunks to vectorize" : "Niciun fragment de vectorizat", + "No collections found" : "Nu s-au găsit colecții", + "No configuration data" : "Nu există date de configurație", + "No configurations are available." : "Nu sunt disponibile configurații.", + "No configurations found" : "Nu s-au găsit configurații", + "No configurations found for this application." : "Nu s-au găsit configurații pentru această aplicație.", + "No conversations yet. Create a new one to get started!" : "Nicio conversație încă. Creați una nouă pentru a începe!", + "No data available" : "Nu există date disponibile", + "No data available for chart" : "Nu există date disponibile pentru grafic", + "No deleted items found" : "Nu s-au găsit elemente șterse", + "No deletion data available" : "Nu există date de ștergere disponibile", + "No description found" : "Nu s-a găsit nicio descriere", + "No description provided" : "Nu a fost furnizată nicio descriere", + "No entities detected in this file. Run text extraction first." : "Nicio entitate detectată în acest fișier. Rulați mai întâi extragerea textului.", + "No entities found" : "Nu s-au găsit entități", + "No entities have been detected yet" : "Nu au fost detectate încă entități", + "No events found" : "Nu s-au găsit evenimente", + "No expired audit trails found to clear" : "Nu s-au găsit jurnale de audit expirate de șters", + "No facetable fields available. Select a register and schema to see available filters." : "Nu există câmpuri fațetabile disponibile. Selectați un registru și o schemă pentru a vedea filtrele disponibile.", + "No files found" : "Nu s-au găsit fișiere", + "No files have been extracted yet" : "Nu au fost extrase încă fișiere", + "No files to process" : "Niciun fișier de procesat", + "No files to reindex" : "Niciun fișier de reindexat", + "No filters are currently active. This will delete ALL audit trail entries!" : "Niciun filtru nu este activ în prezent. Aceasta va șterge TOATE intrările jurnalului de audit!", + "No log entries found" : "Nu s-au găsit intrări în jurnal", + "No logs are available for this configuration." : "Nu sunt disponibile jurnale pentru această configurație.", + "No logs are available for this source." : "Nu sunt disponibile jurnale pentru această sursă.", + "No logs found" : "Nu s-au găsit jurnale", + "No mismatched fields found - SOLR schema is properly configured" : "Nu s-au găsit câmpuri nepotrivite - schema SOLR este configurată corect", + "No objects found" : "Nu s-au găsit obiecte", + "No objects found to vectorize" : "Nu s-au găsit obiecte de vectorizat", + "No objects selected for deletion" : "Niciun obiect selectat pentru ștergere", + "No objects to delete" : "Niciun obiect de șters", + "No objects were permanently deleted" : "Niciun obiect nu a fost șters definitiv", + "No properties found" : "Nu s-au găsit proprietăți", + "No properties match your filters." : "Nicio proprietate nu corespunde filtrelor dumneavoastră.", + "No purge date set" : "Nicio dată de epurare setată", + "No register data available" : "Nu există date de registru disponibile", + "No registers found" : "Nu s-au găsit registre", + "No registers found for this application." : "Nu s-au găsit registre pentru această aplicație.", + "No relations found" : "Nu s-au găsit relații", + "No request body available" : "Nu există corp de cerere disponibil", + "No response body available" : "Nu există corp de răspuns disponibil", + "No saved views yet. Create one in the Search tab!" : "Nicio vizualizare salvată încă. Creați una în fila Căutare!", + "No schema data available" : "Nu există date de schemă disponibile", + "No schema selected for exploration" : "Nicio schemă selectată pentru explorare", + "No schemas found" : "Nu s-au găsit scheme", + "No schemas found for this application." : "Nu s-au găsit scheme pentru această aplicație.", + "No search terms data available" : "Nu există date despre termeni de căutare disponibile", + "No search trail entries found" : "Nu s-au găsit intrări în jurnalul de căutare", + "No sources are available." : "Nu sunt disponibile surse.", + "No sources found" : "Nu s-au găsit surse", + "No templates found" : "Nu s-au găsit șabloane", + "No templates have been created yet" : "Nu au fost create încă șabloane", + "No valid magic-mapped register+schema combinations found" : "Nu s-au găsit combinații valide registru+schemă mapate magic", + "No views found. Create views first before configuring vectorization." : "Nu s-au găsit vizualizări. Creați mai întâi vizualizări înainte de a configura vectorizarea.", + "No views match your search" : "Nicio vizualizare nu corespunde căutării dumneavoastră", + "No webhooks found" : "Nu s-au găsit webhook-uri", + "No webhooks have been configured yet" : "Nu au fost configurate încă webhook-uri", + "No workflows found in this project. Create workflows in the n8n editor." : "Nu s-au găsit fluxuri de lucru în acest proiect. Creați fluxuri de lucru în editorul n8n.", + "None" : "Niciunul", + "Not helpful" : "Inutil", + "Not in use" : "Nu este în uz", + "Number Constraints" : "Constrângeri numerice", + "Number of chunks to vectorize in one API call (1-100)." : "Numărul de fragmente de vectorizat într-un apel API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Numărul de fragmente de vectorizat într-un apel API. Mai mare = mai rapid, dar mai multă memorie. Recomandat: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Numărul de obiecte de procesat în fiecare lot (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Numărul de obiecte de vectorizat într-un apel API. Mai mare = mai rapid, dar mai multă memorie. Recomandat: 10-50.", + "Numeric Range:" : "Interval numeric:", + "Object" : "Obiect", + "Object Collection" : "Colecție de obiecte", + "Object Count Prediction" : "Predicție număr de obiecte", + "Object ID" : "ID obiect", + "Object Management" : "Gestionare obiecte", + "Object Vectorization" : "Vectorizare obiecte", + "Object Warmup" : "Preîncălzire obiecte", + "Object collection not configured" : "Colecția de obiecte nu este configurată", + "Object not found" : "Obiectul nu a fost găsit", + "Object permanently deleted" : "Obiectul a fost șters definitiv", + "Object restored successfully" : "Obiectul a fost restaurat cu succes", + "Object settings updated successfully" : "Setările obiectului au fost actualizate cu succes", + "Object sources" : "Surse de obiecte", + "Object unlocked successfully" : "Obiectul a fost deblocat cu succes", + "Object vectorization configuration saved successfully" : "Configurația vectorizării obiectelor a fost salvată cu succes", + "Object vectorized successfully" : "Obiectul a fost vectorizat cu succes", + "Object:" : "Obiect:", + "Objects" : "Obiecte", + "Objects Analyzed" : "Obiecte analizate", + "Objects Distribution" : "Distribuție obiecte", + "Objects being analyzed" : "Obiecte în curs de analizare", + "Objects by Register" : "Obiecte după registru", + "Objects by Schema" : "Obiecte după schemă", + "Objects deletion completed successfully" : "Ștergerea obiectelor a fost finalizată cu succes", + "Objects in schema" : "Obiecte în schemă", + "Objects to Process:" : "Obiecte de procesat:", + "Objects to be analyzed" : "Obiecte care vor fi analizate", + "Objects to be deleted" : "Obiecte care vor fi șterse", + "Objects to be validated" : "Obiecte care vor fi validate", + "Objects will be serialized as JSON text before vectorization" : "Obiectele vor fi serializate ca text JSON înainte de vectorizare", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Obiectele vor fi șterse temporar (marcate ca șterse, dar păstrate în baza de date). Pot fi recuperate ulterior dacă este necesar.", + "Old Value" : "Valoare veche", + "Oldest Item (days)" : "Cel mai vechi element (zile)", + "Ollama Chat Settings" : "Setări chat Ollama", + "Ollama Embedding Configuration" : "Configurație embedding Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Doar cu modificări", + "Open Register Objects" : "Obiecte Open Register", + "Open n8n Editor" : "Deschidere editor n8n", + "OpenAI Chat Settings" : "Setări chat OpenAI", + "OpenAI Embedding Configuration" : "Configurație embedding OpenAI", + "OpenRegister Settings" : "Setări OpenRegister", + "Optional webhook secret for signature verification" : "Secret webhook opțional pentru verificarea semnăturii", + "Order" : "Ordine", + "Organisation" : "Organizație", + "Organisation ID" : "ID organizație", + "Organisation Members" : "Membri organizație", + "Organisation Statistics" : "Statistici organizație", + "Organisation created successfully" : "Organizația a fost creată cu succes", + "Organisation settings saved successfully" : "Setările organizației au fost salvate cu succes", + "Organisations" : "Organizații", + "Organization" : "Organizație", + "Organization ID (Optional)" : "ID organizație (opțional)", + "Orphaned Items" : "Elemente orfane", + "Owner" : "Proprietar", + "Owner:" : "Proprietar:", + "Parallel Mode (Faster, more resource intensive)" : "Mod paralel (mai rapid, mai intensiv în resurse)", + "Parallel:" : "Paralel:", + "Password" : "Parolă", + "Password for SOLR authentication (optional)" : "Parolă pentru autentificarea SOLR (opțional)", + "Path" : "Cale", + "Path in repository" : "Cale în depozit", + "Path where the configuration file will be saved in the repository" : "Calea unde fișierul de configurație va fi salvat în depozit", + "Path where the register OAS file will be saved in the repository" : "Calea unde fișierul OAS al registrului va fi salvat în depozit", + "Pattern (regex)" : "Model (regex)", + "Pattern Issue" : "Problemă de model", + "Pattern constraint is missing" : "Constrângerea de model lipsește", + "Patterns:" : "Modele:", + "Pending" : "În așteptare", + "Permanent Delete Mode" : "Mod ștergere definitivă", + "Permanently Delete" : "Ștergere definitivă", + "Permanently delete already soft-deleted objects" : "Ștergeți definitiv obiectele deja șterse temporar", + "Permanently delete this conversation?" : "Ștergeți definitiv această conversație?", + "Person" : "Persoană", + "Personal Data" : "Date personale", + "Phone" : "Telefon", + "Photos" : "Fotografii", + "Please create an agent in the" : "Vă rugăm să creați un agent în", + "Please select an agent to continue" : "Vă rugăm să selectați un agent pentru a continua", + "Please select which register and schema to use for the new object" : "Vă rugăm să selectați ce registru și schemă să utilizați pentru noul obiect", + "Please try again later." : "Vă rugăm să încercați din nou mai târziu.", + "Please wait while we fetch your agents." : "Vă rugăm să așteptați în timp ce preluăm agenții dumneavoastră.", + "Please wait while we fetch your applications." : "Vă rugăm să așteptați în timp ce preluăm aplicațiile dumneavoastră.", + "Please wait while we fetch your configurations." : "Vă rugăm să așteptați în timp ce preluăm configurațiile dumneavoastră.", + "Please wait while we fetch your deleted items." : "Vă rugăm să așteptați în timp ce preluăm elementele dumneavoastră șterse.", + "Please wait while we fetch your sources." : "Vă rugăm să așteptați în timp ce preluăm sursele dumneavoastră.", + "Polls" : "Sondaje", + "Popular Search Terms" : "Termeni de căutare populari", + "Port" : "Port", + "Prerequisites" : "Cerințe prealabile", + "Press Enter to send, Shift+Enter for new line" : "Apăsați Enter pentru a trimite, Shift+Enter pentru linie nouă", + "Previous" : "Anterior", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Procesează fragmentele în loturi cu paralelism simulat.", + "Processes file chunks sequentially (safest)." : "Procesează fragmentele de fișiere secvențial (cel mai sigur).", + "Processes objects in chunks with simulated parallelism." : "Procesează obiectele în fragmente cu paralelism simulat.", + "Processes objects sequentially (safest)." : "Procesează obiectele secvențial (cel mai sigur).", + "Processing" : "Procesare", + "Processing Limits" : "Limite de procesare", + "Processing..." : "Se procesează...", + "Project initialized successfully" : "Proiectul a fost inițializat cu succes", + "Project not found. Please initialize first." : "Proiectul nu a fost găsit. Vă rugăm să inițializați mai întâi.", + "Properties" : "Proprietăți", + "Property Behaviors" : "Comportamente proprietate", + "Property Configuration:" : "Configurație proprietate:", + "Property Filters" : "Filtre proprietate", + "Property Title" : "Titlu proprietate", + "Property Type" : "Tip proprietate", + "Property can be improved" : "Proprietatea poate fi îmbunătățită", + "Protected" : "Protejat", + "Provider" : "Furnizor", + "Provider is required for testing" : "Furnizorul este obligatoriu pentru testare", + "Public" : "Public", + "Public View" : "Vizualizare publică", + "Public views can be accessed by anyone in the system" : "Vizualizările publice pot fi accesate de oricine din sistem", + "Publish" : "Publicare", + "Publish Objects" : "Publicare obiecte", + "Publish all objects in this schema" : "Publicați toate obiectele din această schemă", + "Published" : "Publicat", + "Published:" : "Publicat:", + "Publishing..." : "Se publică...", + "Purge" : "Epurare", + "Purge Date" : "Dată epurare", + "Query Complexity Distribution" : "Distribuție complexitate interogări", + "Query parameter is required" : "Parametrul de interogare este obligatoriu", + "RAG" : "RAG", + "RAG Configuration" : "Configurație RAG", + "Rate limits cleared successfully" : "Limitele de rată au fost șterse cu succes", + "Raw Changes Data" : "Date modificări brute", + "Re-vectorize on object update" : "Re-vectorizare la actualizarea obiectului", + "Read" : "Citire", + "Recommendations:" : "Recomandări:", + "Recommended Type:" : "Tip recomandat:", + "Recommended: 5 sources" : "Recomandat: 5 surse", + "Refresh" : "Reîmprospătare", + "Refresh Data" : "Reîmprospătare date", + "Refresh Stats" : "Reîmprospătare statistici", + "Refresh Workflows" : "Reîmprospătare fluxuri de lucru", + "Refresh dashboard" : "Reîmprospătare tablou de bord", + "Refresh database info" : "Reîmprospătare informații bază de date", + "Register" : "Registru", + "Register ID" : "ID registru", + "Register Statistics" : "Statistici registru", + "Register Totals" : "Totaluri registru", + "Register depublished successfully" : "Registrul a fost depublicat cu succes", + "Register not found" : "Registrul nu a fost găsit", + "Register objects deletion completed successfully" : "Ștergerea obiectelor din registru a fost finalizată cu succes", + "Register published successfully" : "Registrul a fost publicat cu succes", + "Register/Schema Usage" : "Utilizare registru/schemă", + "Registers" : "Registre", + "Reindex" : "Reindexare", + "Reindex completed" : "Reindexare finalizată", + "Reindex completed successfully" : "Reindexarea a fost finalizată cu succes", + "Reindex failed" : "Reindexarea a eșuat", + "Relations" : "Relații", + "Remote Version" : "Versiune la distanță", + "Remove" : "Eliminare", + "Remove filter" : "Eliminare filtru", + "Remove from favorites" : "Eliminare din favorite", + "Remove group" : "Eliminare grup", + "Removed" : "Eliminat", + "Removed from favorites" : "Eliminat din favorite", + "Rename Conversation" : "Redenumire conversație", + "Rename conversation" : "Redenumire conversație", + "Replicas" : "Replici", + "Repository" : "Depozit", + "Request Body" : "Corp cerere", + "Request Data" : "Date cerere", + "Request Details" : "Detalii cerere", + "Request body must be JSON with a \"query\" field" : "Corpul cererii trebuie să fie JSON cu un câmp \"query\"", + "Request timeout in seconds" : "Timeout cerere în secunde", + "Required field" : "Câmp obligatoriu", + "Required status is inconsistent" : "Starea obligatorie este inconsecventă", + "Rerun Search" : "Reexecutare căutare", + "Reset Changes" : "Resetare modificări", + "Reset Filters" : "Resetare filtre", + "Response Body" : "Corp răspuns", + "Response Details" : "Detalii răspuns", + "Response Time:" : "Timp de răspuns:", + "Restore" : "Restaurare", + "Restore conversation" : "Restaurare conversație", + "Restore or permanently delete items" : "Restaurați sau ștergeți definitiv elemente", + "Result Count Range" : "Interval număr de rezultate", + "Results" : "Rezultate", + "Retrieve all objects for this schema" : "Recuperați toate obiectele pentru această schemă", + "Retry" : "Reîncercare", + "Retry Failed Extractions" : "Reîncercare extrageri eșuate", + "Retry Policy" : "Politică de reîncercare", + "Retry completed" : "Reîncercare finalizată", + "Retrying..." : "Se reîncearcă...", + "Risk Level" : "Nivel de risc", + "Roles updated successfully" : "Rolurile au fost actualizate cu succes", + "SOLR Connection Settings" : "Setări conexiune SOLR", + "SOLR Version:" : "Versiune SOLR:", + "SOLR actions menu" : "Meniu acțiuni SOLR", + "SOLR base path (usually /solr)" : "Cale de bază SOLR (de obicei /solr)", + "SOLR is disabled" : "SOLR este dezactivat", + "SOLR is not available or not configured" : "SOLR nu este disponibil sau nu este configurat", + "SOLR logging disabled" : "Jurnalizare SOLR dezactivată", + "SOLR logging enabled" : "Jurnalizare SOLR activată", + "SOLR search disabled" : "Căutare SOLR dezactivată", + "SOLR search enabled" : "Căutare SOLR activată", + "SOLR server hostname or IP address" : "Nume gazdă sau adresă IP server SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Număr port server SOLR (opțional, implicit 8983)", + "SOLR setup completed successfully" : "Configurarea SOLR a fost finalizată cu succes", + "SOLR setup failed" : "Configurarea SOLR a eșuat", + "SOLR setup failed - check logs" : "Configurarea SOLR a eșuat - verificați jurnalele", + "Sample Values:" : "Valori eșantion:", + "Save" : "Salvare", + "Save Configuration" : "Salvare configurație", + "Save Connection Settings" : "Salvare setări conexiune", + "Save Roles" : "Salvare roluri", + "Save Settings" : "Salvare setări", + "Save current search as view" : "Salvare căutare curentă ca vizualizare", + "Saved Views" : "Vizualizări salvate", + "Saving..." : "Se salvează...", + "Schema" : "Schemă", + "Schema ID" : "ID schemă", + "Schema Name" : "Nume schemă", + "Schema Statistics" : "Statistici schemă", + "Schema depublished successfully" : "Schema a fost depublicată cu succes", + "Schema objects deletion completed successfully" : "Ștergerea obiectelor din schemă a fost finalizată cu succes", + "Schema published successfully" : "Schema a fost publicată cu succes", + "Schema successfully created" : "Schema a fost creată cu succes", + "Schema successfully updated" : "Schema a fost actualizată cu succes", + "Schemas" : "Scheme", + "Scheme" : "Schemă", + "Search / Views" : "Căutare / Vizualizări", + "Search Activity" : "Activitate de căutare", + "Search Analytics" : "Analize căutare", + "Search Entities" : "Căutare entități", + "Search Files" : "Căutare fișiere", + "Search GitHub" : "Căutare GitHub", + "Search GitLab" : "Căutare GitLab", + "Search Method" : "Metodă de căutare", + "Search Term" : "Termen de căutare", + "Search Trail Details" : "Detalii jurnal de căutare", + "Search Trail Management" : "Gestionare jurnal de căutare", + "Search Trail Statistics" : "Statistici jurnal de căutare", + "Search Trails" : "Jurnale de căutare", + "Search Views" : "Căutare vizualizări", + "Search Webhooks" : "Căutare webhook-uri", + "Search by file name or path" : "Căutare după nume fișier sau cale", + "Search by name or URL" : "Căutare după nume sau URL", + "Search by value" : "Căutare după valoare", + "Search in Files" : "Căutare în fișiere", + "Search in Objects" : "Căutare în obiecte", + "Search property names..." : "Căutare nume proprietăți...", + "Search trail deletion not implemented yet" : "Ștergerea jurnalului de căutare nu este încă implementată", + "Search views..." : "Căutare vizualizări...", + "Secret" : "Secret", + "Secret key for HMAC signature generation (optional)" : "Cheie secretă pentru generarea semnăturii HMAC (opțional)", + "Select AI Agent" : "Selectare agent AI", + "Select All" : "Selectare tot", + "Select ConfigSet" : "Selectare ConfigSet", + "Select File Types to Vectorize:" : "Selectați tipurile de fișiere de vectorizat:", + "Select HTTP method" : "Selectare metodă HTTP", + "Select Register and Schema" : "Selectare Registru și Schemă", + "Select Views to Vectorize:" : "Selectați vizualizările de vectorizat:", + "Select a Nextcloud group to add" : "Selectați un grup Nextcloud de adăugat", + "Select a branch" : "Selectați o ramură", + "Select a model or type a custom model name" : "Selectați un model sau introduceți un nume de model personalizat", + "Select a repository" : "Selectați un depozit", + "Select a repository you have write access to" : "Selectați un depozit la care aveți acces de scriere", + "Select an AI Agent" : "Selectați un agent AI", + "Select an AI agent to begin chatting with your data." : "Selectați un agent AI pentru a începe să conversați cu datele dumneavoastră.", + "Select backend" : "Selectare backend", + "Select chat model" : "Selectare model de chat", + "Select collection for files" : "Selectare colecție pentru fișiere", + "Select collection for objects" : "Selectare colecție pentru obiecte", + "Select default organisation" : "Selectare organizație implicită", + "Select event to listen to..." : "Selectați evenimentul de ascultat...", + "Select model" : "Selectare model", + "Select options..." : "Selectați opțiuni...", + "Select period" : "Selectare perioadă", + "Select property to send as payload" : "Selectați proprietatea de trimis ca payload", + "Select provider" : "Selectare furnizor", + "Select registers and schemas to save a view" : "Selectați registre și scheme pentru a salva o vizualizare", + "Select retry policy" : "Selectare politică de reîncercare", + "Select the branch to publish to" : "Selectați ramura în care să publicați", + "Select the event this webhook should listen to" : "Selectați evenimentul pe care acest webhook ar trebui să îl asculte", + "Select views to vectorize:" : "Selectați vizualizările de vectorizat:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Selectați ce grupuri Nextcloud sunt disponibile pentru această organizație. Utilizatorii din aceste grupuri vor avea acces la resursele organizației.", + "Select which data views the AI can search" : "Selectați ce vizualizări de date poate căuta AI", + "Select which property from the event should be used as the webhook payload data" : "Selectați ce proprietate din eveniment ar trebui utilizată ca date payload ale webhook-ului", + "Select which tools the AI can use to perform actions" : "Selectați ce instrumente poate utiliza AI pentru a efectua acțiuni", + "Selected" : "Selectat", + "Selected Groups" : "Grupuri selectate", + "Selected audit trails deleted successfully" : "Jurnalele de audit selectate au fost șterse cu succes", + "Selected search trails deleted successfully" : "Jurnalele de căutare selectate au fost șterse cu succes", + "Selected users can access this view" : "Utilizatorii selectați pot accesa această vizualizare", + "Send additional feedback" : "Trimitere feedback suplimentar", + "Send as CloudEvent" : "Trimitere ca CloudEvent", + "Sensitive PII" : "PII sensibile", + "Serial Mode (Safer, slower)" : "Mod serial (mai sigur, mai lent)", + "Serial:" : "Serial:", + "Server Configuration" : "Configurație server", + "Server Information" : "Informații server", + "Settings" : "Setări", + "Shards" : "Shard-uri", + "Share" : "Partajare", + "Share with Groups" : "Partajare cu grupuri", + "Share with Users" : "Partajare cu utilizatori", + "Show Filters" : "Afișare filtre", + "Show less" : "Afișare mai puțin", + "Show only entries with changes" : "Afișare doar intrări cu modificări", + "Simple" : "Simplu", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Interogări simple: căutări de text de bază cu parametri minimi (de exemplu, un singur termen de căutare, fără filtre avansate)", + "Size" : "Dimensiune", + "Slug" : "Slug", + "Soft Delete Mode" : "Mod ștergere temporară", + "Soft Deleted Items" : "Elemente șterse temporar", + "SolrCloud mode enabled" : "Mod SolrCloud activat", + "Source" : "Sursă", + "Sources" : "Surse", + "Sources:" : "Surse:", + "Standalone SOLR mode" : "Mod SOLR autonom", + "Start Conversation" : "Începere conversație", + "Start Vectorization" : "Începere vectorizare", + "Start a conversation" : "Începeți o conversație", + "Starting names cache warmup..." : "Se începe preîncălzirea cache-ului de nume...", + "Starting..." : "Se începe...", + "Statistics" : "Statistici", + "Stats calculation not yet implemented" : "Calcularea statisticilor nu este încă implementată", + "Status" : "Stare", + "Status Code" : "Cod de stare", + "String Constraints" : "Constrângeri șir", + "Success" : "Succes", + "Success Rate" : "Rată de succes", + "Success Status" : "Stare de succes", + "Successful" : "Reușit", + "Successfully joined organisation" : "V-ați alăturat cu succes organizației", + "Sync Table" : "Sincronizare tabel", + "Synchronization Settings" : "Setări sincronizare", + "System Default" : "Implicit sistem", + "Technical Description" : "Descriere tehnică", + "Technical description for developers and administrators" : "Descriere tehnică pentru dezvoltatori și administratori", + "Temperature" : "Temperatură", + "Templates" : "Șabloane", + "Test" : "Testare", + "Test Chat" : "Testare chat", + "Test Connection" : "Testare conexiune", + "Test Embedding" : "Testare embedding", + "Test endpoint executed successfully" : "Endpoint-ul de testare a fost executat cu succes", + "Test webhook delivered successfully" : "Webhook-ul de testare a fost livrat cu succes", + "Test webhook delivery failed" : "Livrarea webhook-ului de testare a eșuat", + "Test webhook sent successfully" : "Webhook-ul de testare a fost trimis cu succes", + "Testing..." : "Se testează...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Fragmentele de text sunt generate în timpul extragerii fișierelor și stocate în baza de date. Vectorizarea citește aceste fișiere prefragmentate și le convertește în embeddings.", + "Text extracted successfully" : "Textul a fost extras cu succes", + "Text extraction disabled" : "Extragerea textului dezactivată", + "The URL where webhook events will be sent" : "URL-ul unde vor fi trimise evenimentele webhook", + "The requested conversation does not exist" : "Conversația solicitată nu există", + "The saved view and all its search configuration" : "Vizualizarea salvată și întreaga sa configurație de căutare", + "There are no audit trail entries matching your current filters." : "Nu există intrări în jurnalul de audit care să corespundă filtrelor dumneavoastră curente.", + "There are no deleted items matching your current filters." : "Nu există elemente șterse care să corespundă filtrelor dumneavoastră curente.", + "There are no search trail entries matching your current filters." : "Nu există intrări în jurnalul de căutare care să corespundă filtrelor dumneavoastră curente.", + "There are no webhook log entries matching your filters." : "Nu există intrări în jurnalul webhook care să corespundă filtrelor dumneavoastră.", + "This action cannot be undone." : "Această acțiune nu poate fi anulată.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Această acțiune nu poate fi anulată. Asigurați-vă că nicio colecție nu utilizează acest ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Acest agent nu are vizualizări sau instrumente configurabile, dar puteți ajusta în continuare setările RAG de mai jos.", + "This analysis may take some time" : "Această analiză poate dura ceva timp", + "This audit trail entry does not contain any change information." : "Această intrare a jurnalului de audit nu conține nicio informație despre modificări.", + "This configuration has no data defined." : "Această configurație nu are date definite.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Acest endpoint este învechit. Utilizați în schimb endpoint-urile bazate pe fragmente.", + "This entity has no relations to objects or files" : "Această entitate nu are relații cu obiecte sau fișiere", + "This is a test webhook from OpenRegister" : "Acesta este un webhook de testare de la OpenRegister", + "This schema must use Magic Table configuration to sync" : "Această schemă trebuie să utilizeze configurația Magic Table pentru a se sincroniza", + "This source has no associated registers." : "Această sursă nu are registre asociate.", + "This will copy the _default ConfigSet with the new name" : "Aceasta va copia ConfigSet-ul _default cu noul nume", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Aceasta va șterge definitiv TOATE embeddings (vectorii) din baza de date. Va trebui să re-vectorizați toate obiectele și fișierele. Această acțiune nu poate fi anulată.\\n\\nSigur doriți să continuați?", + "This will permanently delete:" : "Aceasta va șterge definitiv:", + "Timeout (seconds)" : "Timeout (secunde)", + "Timestamp" : "Marcaj temporal", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Sfat: activați doar vizualizările care necesită căutare semantică pentru a minimiza costurile de embedding. Tabelele simple de căutare rareori necesită vectorizare.", + "Title" : "Titlu", + "To Date" : "Până la data", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Pentru a ajusta dimensiunea sau strategia fragmentelor, accesați Configurație fișier → Limite de procesare.", + "To change the embedding provider or model, go to LLM Configuration." : "Pentru a schimba furnizorul sau modelul de embedding, accesați Configurație LLM.", + "To date" : "Până la data", + "Toggle search sidebar" : "Comutare bară laterală de căutare", + "Tools" : "Instrumente", + "Top Deleters" : "Cei mai activi în ștergeri", + "Total" : "Total", + "Total Audit Trails" : "Total jurnale de audit", + "Total Chunks Available:" : "Total fragmente disponibile:", + "Total Deleted Items" : "Total elemente șterse", + "Total Members" : "Total membri", + "Total Objects" : "Total obiecte", + "Total Objects in Database:" : "Total obiecte în baza de date:", + "Total Organisations" : "Total organizații", + "Total Results" : "Total rezultate", + "Total Searches" : "Total căutări", + "Total Size" : "Dimensiune totală", + "Total Storage" : "Stocare totală", + "Total:" : "Total:", + "Totals" : "Totaluri", + "Type" : "Tip", + "Type Issue" : "Problemă de tip", + "Type Variations:" : "Variații de tip:", + "Type must be \"positive\" or \"negative\"" : "Tipul trebuie să fie \"positive\" sau \"negative\"", + "Type to search for groups" : "Tastați pentru a căuta grupuri", + "Type to search for users" : "Tastați pentru a căuta utilizatori", + "Type to search groups..." : "Tastați pentru a căuta grupuri...", + "Type to search users..." : "Tastați pentru a căuta utilizatori...", + "URL" : "URL", + "URL where Ollama is running" : "URL-ul unde rulează Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Nemodificat", + "Unique Search Terms" : "Termeni de căutare unici", + "Unique Terms" : "Termeni unici", + "Unknown" : "Necunoscut", + "Unknown Register" : "Registru necunoscut", + "Unknown Schema" : "Schemă necunoscută", + "Untitled View" : "Vizualizare fără titlu", + "Update" : "Actualizare", + "Update Operations" : "Operațiuni de actualizare", + "Update configuration: ..." : "Actualizare configurație: ...", + "Update register OAS: ..." : "Actualizare OAS registru: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Actualizează vectorii când datele obiectului se modifică (recomandat pentru o căutare exactă)", + "Updated" : "Actualizat", + "Updated At" : "Actualizat la", + "Updated:" : "Actualizat:", + "Uptime:" : "Timp de funcționare:", + "Usage Count" : "Număr de utilizări", + "Use SolrCloud with Zookeeper for distributed search" : "Utilizați SolrCloud cu Zookeeper pentru căutare distribuită", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Utilizați filtre pentru a restrânge intrările jurnalului de audit după registru, schemă, tip de acțiune, utilizator, interval de date sau ID obiect.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Utilizați filtre pentru a restrânge elementele șterse după registru, schemă, dată de ștergere sau utilizatorul care le-a șters.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Utilizați filtre pentru a restrânge intrările jurnalului de căutare după registru, schemă, stare de succes, utilizator, interval de date, termeni de căutare sau metrici de performanță.", + "Used By Collections" : "Utilizat de colecții", + "User" : "Utilizator", + "User Agent Statistics" : "Statistici agent utilizator", + "User Description" : "Descriere utilizator", + "User rate limits cleared successfully" : "Limitele de rată ale utilizatorului au fost șterse cu succes", + "User removed successfully" : "Utilizatorul a fost eliminat cu succes", + "User-friendly description shown in forms and help text" : "Descriere prietenoasă pentru utilizator afișată în formulare și textul de ajutor", + "Username" : "Nume utilizator", + "Username for SOLR authentication (optional)" : "Nume utilizator pentru autentificarea SOLR (opțional)", + "Users" : "Utilizatori", + "Using Pre-Generated Chunks" : "Utilizare fragmente pregenerate", + "Validate" : "Validare", + "Validate Objects" : "Validare obiecte", + "Validation completed successfully" : "Validarea a fost finalizată cu succes", + "Validation failed" : "Validarea a eșuat", + "Value" : "Valoare", + "Vector Dimensions" : "Dimensiuni vector", + "Vector Search Backend" : "Backend căutare vectorială", + "Vector field: _embedding_" : "Câmp vector: _embedding_", + "Vectorization Triggers" : "Declanșatoare de vectorizare", + "Vectorize All Files" : "Vectorizare toate fișierele", + "Vectorize All Objects" : "Vectorizare toate obiectele", + "Vectorize all file types" : "Vectorizare toate tipurile de fișiere", + "Vectorize all views" : "Vectorizare toate vizualizările", + "Vectorize on object creation" : "Vectorizare la crearea obiectului", + "Vectors will be stored in your existing object and file collections" : "Vectorii vor fi stocați în colecțiile dumneavoastră existente de obiecte și fișiere", + "Version" : "Versiune", + "Very High" : "Foarte ridicat", + "View API Docs" : "Vizualizare documentație API", + "View Changes" : "Vizualizare modificări", + "View Details" : "Vizualizare detalii", + "View Error" : "Vizualizare eroare", + "View File" : "Vizualizare fișier", + "View Full Details" : "Vizualizare detalii complete", + "View Logs" : "Vizualizare jurnale", + "View Name" : "Nume vizualizare", + "View Object" : "Vizualizare obiect", + "View Parameters" : "Parametri vizualizare", + "View Selection" : "Selectare vizualizare", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Vizualizați și analizați jurnalele de căutare cu capacități avansate de filtrare și analiză", + "View and analyze system audit trails with advanced filtering capabilities" : "Vizualizați și analizați jurnalele de audit ale sistemului cu capacități avansate de filtrare", + "View deleted successfully" : "Vizualizarea a fost ștearsă cu succes", + "View entity details and manage relations" : "Vizualizați detaliile entității și gestionați relațiile", + "View name" : "Nume vizualizare", + "View name is required" : "Numele vizualizării este obligatoriu", + "View saved successfully!" : "Vizualizarea a fost salvată cu succes!", + "View search analytics and manage search logs" : "Vizualizați analizele de căutare și gestionați jurnalele de căutare", + "View successfully deleted" : "Vizualizarea a fost ștearsă cu succes", + "View successfully updated" : "Vizualizarea a fost actualizată cu succes", + "View updated successfully!" : "Vizualizarea a fost actualizată cu succes!", + "View webhook delivery logs and filter by webhook" : "Vizualizați jurnalele de livrare a webhook-urilor și filtrați după webhook", + "Views" : "Vizualizări", + "Visible to users" : "Vizibil pentru utilizatori", + "Wait for Response" : "Așteptare răspuns", + "Wait for webhook response before continuing (required for request/response flows)" : "Așteptați răspunsul webhook-ului înainte de a continua (necesar pentru fluxurile cerere/răspuns)", + "Warmup Names Cache" : "Preîncălzire cache de nume", + "Warning" : "Avertisment", + "Warning:" : "Avertisment:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detalii jurnal webhook", + "Webhook Logs" : "Jurnale webhook", + "Webhook created successfully" : "Webhook-ul a fost creat cu succes", + "Webhook deleted" : "Webhook șters", + "Webhook retry delivered successfully" : "Reîncercarea webhook-ului a fost livrată cu succes", + "Webhook retry delivery failed" : "Livrarea reîncercării webhook-ului a eșuat", + "Webhook updated" : "Webhook actualizat", + "Webhook updated successfully" : "Webhook-ul a fost actualizat cu succes", + "Webhooks" : "Webhook-uri", + "Weekly" : "Săptămânal", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Încadrați payload-ul webhook în format CloudEvents pentru o interoperabilitate mai bună", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Da", + "You" : "Dumneavoastră", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Puteți crea ConfigSets noi pe baza șablonului _default sau puteți încărca unele personalizate direct pe serverul dumneavoastră SOLR.", + "You do not have access to this conversation" : "Nu aveți acces la această conversație", + "You do not have permission to delete this conversation" : "Nu aveți permisiunea de a șterge această conversație", + "You do not have permission to modify this conversation" : "Nu aveți permisiunea de a modifica această conversație", + "You do not have permission to restore this conversation" : "Nu aveți permisiunea de a restaura această conversație", + "You must be logged in to favorite views" : "Trebuie să fiți autentificat pentru a adăuga vizualizări la favorite", + "You need an AI agent to start a conversation." : "Aveți nevoie de un agent AI pentru a începe o conversație.", + "Your Fireworks AI API key. Get one at" : "Cheia dumneavoastră API Fireworks AI. Obțineți una la", + "Your OpenAI API key. Get one at" : "Cheia dumneavoastră API OpenAI. Obțineți una la", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Feedback-ul dumneavoastră a fost înregistrat. Opțional, puteți furniza detalii suplimentare aici...", + "Zookeeper Hosts" : "Gazde Zookeeper", + "Zookeeper Port" : "Port Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Setări Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Șir de conexiune Zookeeper pentru SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Număr port Zookeeper (opțional, implicit 2181)", + "chunk" : "fragment", + "chunks" : "fragmente", + "chunks processed" : "fragmente procesate", + "configuration(s)" : "configurație(i)", + "e.g., lib/Settings/config.json" : "de exemplu, lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "de exemplu, lib/Settings/register.json", + "failed" : "eșuat", + "file" : "fișier", + "files" : "fișiere", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "membri", + "menu or contact someone with permission to create agents." : "meniul sau contactați pe cineva cu permisiunea de a crea agenți.", + "messages" : "mesaje", + "n8n URL and API key are required" : "URL-ul n8n și cheia API sunt obligatorii", + "n8n configuration saved successfully" : "Configurația n8n a fost salvată cu succes", + "n8n connection not configured" : "Conexiunea n8n nu este configurată", + "n8n connection successful" : "Conexiune n8n reușită", + "n8n connection test successful" : "Testul de conexiune n8n a reușit", + "n8n integration disabled" : "Integrarea n8n dezactivată", + "n8n integration enabled" : "Integrarea n8n activată", + "n8n project initialized successfully" : "Proiectul n8n a fost inițializat cu succes", + "n8n settings saved successfully" : "Setările n8n au fost salvate cu succes", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "obiecte", + "objects processed" : "obiecte procesate", + "or pick a range" : "sau alegeți un interval", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "oferă un echilibru bun între viteză și acuratețe.", + "register(s)" : "registru(e)", + "register(s) selected" : "registru(e) selectat(e)", + "required" : "obligatoriu", + "results" : "rezultate", + "schema(s)" : "schemă(e)", + "schema(s) selected" : "schemă(e) selectată(e)", + "sk-..." : "sk-...", + "this application" : "această aplicație", + "to" : "până la", + "vectorized" : "vectorizat", + "views selected" : "vizualizări selectate", + "ℹ️ Current Configuration" : "ℹ️ Configurație curentă", + "⚡ Batch Processing" : "⚡ Procesare în lot", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Context cuprinzător ✓ Mai puțin probabil să rateze detalii ✗ Răspunsuri mai lente ✗ Poate include informații mai puțin relevante", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Răspunsuri mai rapide ✓ Răspunsuri mai concentrate ✗ Poate rata informații relevante", + "✨ AI Features" : "✨ Funcționalități AI", + "All search trails cleared successfully" : "Toate jurnalele de căutare au fost șterse cu succes", + "Multiple search trail deletion not implemented yet" : "Ștergerea mai multor jurnale de căutare nu este încă implementată", + "No expired search trails found to clear" : "Nu s-au găsit jurnale de căutare expirate de șters", + "Missing message" : "Mesaj lipsă", + "message content is required" : "conținutul mesajului este obligatoriu", + "Missing conversation or agentUuid" : "Lipsește conversația sau agentUuid", + "Access denied" : "Acces refuzat", + "Conversation not found" : "Conversația nu a fost găsită", + "AI service not configured" : "Serviciul AI nu este configurat", + "Failed to process message" : "Procesarea mesajului a eșuat", + "Missing conversationId" : "Lipsește conversationId", + "conversationId is required" : "conversationId este obligatoriu", + "Failed to fetch conversation history" : "Preluarea istoricului conversației a eșuat", + "Failed to clear conversation" : "Ștergerea conversației a eșuat", + "Invalid feedback type" : "Tip de feedback nevalid", + "type must be \"positive\" or \"negative\"" : "tipul trebuie să fie \"positive\" sau \"negative\"", + "Message not found" : "Mesajul nu a fost găsit", + "Failed to save feedback" : "Salvarea feedback-ului a eșuat", + "Failed to get chat statistics" : "Obținerea statisticilor de chat a eșuat", + "Not Found" : "Negăsit", + "Engine not found" : "Motorul nu a fost găsit", + "Consumer not found" : "Consumatorul nu a fost găsit", + "Backend parameter is required" : "Parametrul backend este obligatoriu", + "Failed to get database information: %s" : "Obținerea informațiilor despre baza de date a eșuat: %s", + "SOLR setup error: %s" : "Eroare configurare SOLR: %s", + "Reindex failed: %s" : "Reindexarea a eșuat: %s", + "Anonymized" : "Anonimizat", + "Entities detected" : "Entități detectate", + "Extract Now" : "Extragere acum", + "Extracted at" : "Extras la", + "Extraction" : "Extragere", + "Failed to load extraction data" : "Încărcarea datelor de extragere a eșuat", + "Failed to load register data" : "Încărcarea datelor registrului a eșuat", + "Failed to retrieve extraction status." : "Recuperarea stării de extragere a eșuat.", + "Failed to retrieve objects for file." : "Recuperarea obiectelor pentru fișier a eșuat.", + "No extraction data available for this file" : "Nu există date de extragere disponibile pentru acest fișier", + "No register objects reference this file" : "Niciun obiect din registru nu face referire la acest fișier", + "Not extracted" : "Neextras", + "Register Objects" : "Obiecte registru", + "Risk level" : "Nivel de risc", + "Text chunks" : "Fragmente de text", + "Unknown error" : "Eroare necunoscută", + "Very high" : "Foarte ridicat", + "{title} in {register} / {schema}" : "{title} în {register} / {schema}", + "Push Notifications" : "Notificări push", + "Real-time push notification status via notify_push" : "Stare notificări push în timp real prin notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push în timp real indisponibil — aplicația notify_push nu este instalată", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Instalați aplicația notify_push din Nextcloud App Store pentru a activa actualizările în timp real.", + "Open Nextcloud App Store" : "Deschidere Nextcloud App Store", + "notify_push is installed but not yet active" : "notify_push este instalat, dar nu este încă activ", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push este instalat, dar OpenRegister nu a confirmat încă un push reușit. Declanșați o salvare de obiect pentru a-l activa sau verificați configurația notify_push.", + "notify_push configuration guide" : "ghid de configurare notify_push", + "Realtime push active" : "Push în timp real activ", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Notificările push în timp real sunt active. Clienții conectați primesc actualizări instantanee când obiectele sunt create, actualizate sau șterse.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);" +); diff --git a/l10n/ro.json b/l10n/ro.json new file mode 100644 index 0000000000..bdb0a60512 --- /dev/null +++ b/l10n/ro.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin necesar)", + "(no subject)": "(fără subiect)", + "(no title)": "(fără titlu)", + "(unnamed)": "(fără nume)", + "(untitled card)": "(card fără titlu)", + "({count} selected)": "({count} selectate)", + ", {failed} failed": ", {failed} eșuate", + "0 = unlimited": "0 = nelimitat", + "A deactivation request is pending.": "O cerere de dezactivare este în așteptare.", + "AI Assistant": "Asistent AI", + "AI Chat": "Conversație AI", + "AI service not configured": "Serviciul AI nu este configurat", + "API Key": "Cheie API", + "API Request Quota (requests/day)": "Cotă cereri API (cereri/zi)", + "API Token Configuration": "Configurare token API", + "API Tokens": "Token-uri API", + "API tokens saved successfully": "Token-urile API au fost salvate cu succes", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: acest raport asociază fiecare verwerkingsactiviteit cu numărul total de înregistrări din pista de audit atribuite acesteia. Auditorii și Autoriteit Persoonsgegevens utilizează acest lucru pentru revizuirea de supraveghere.", + "About ConfigSets": "Despre ConfigSets", + "Access denied": "Acces refuzat", + "Access organisation dashboard and analytics": "Accesați tabloul de bord și analizele organizației", + "Account": "Cont", + "Action": "Acțiune", + "Action Distribution": "Distribuția acțiunilor", + "Action:": "Acțiune:", + "Actions": "Acțiuni", + "Active": "Activ", + "Active Collections": "Colecții active", + "Active Filters": "Filtre active", + "Active Organisation:": "Organizație activă:", + "Active Organisations": "Organizații active", + "Active filters:": "Filtre active:", + "Active organisation changed successfully": "Organizația activă a fost modificată cu succes", + "Active organisation set successfully": "Organizația activă a fost setată cu succes", + "Activities": "Activități", + "Activity": "Activitate", + "Activity Period": "Perioada de activitate", + "Add": "Adăugați", + "Add Attachment": "Adăugați atașament", + "Add Endpoint": "Adăugați punct final", + "Add Groups": "Adăugați grupuri", + "Add Property to '{title}'": "Adăugați proprietate la '{title}'", + "Add Register": "Adăugați registru", + "Add Schema": "Adăugați schemă", + "Add Source": "Adăugați sursă", + "Add User": "Adăugați utilizator", + "Add User to Organisation": "Adăugați utilizator în organizație", + "Add a contact from any of your address books to associate it with this object.": "Adăugați un contact din oricare dintre agendele dumneavoastră pentru a-l asocia cu acest obiect.", + "Add a file or files": "Adăugați un fișier sau mai multe fișiere", + "Add card": "Adăugați card", + "Add contact": "Adăugați contact", + "Add endpoint": "Adăugați punct final", + "Add oneOf entry": "Adăugați intrare oneOf", + "Add range": "Adăugați interval", + "Add schema titles, descriptions, and register information to provide richer context for search": "Adăugați titluri de schemă, descrieri și informații despre registru pentru a oferi un context mai bogat căutării", + "Add to favorites": "Adăugați la favorite", + "Added": "Adăugat", + "Added to favorites": "Adăugat la favorite", + "Additional Information": "Informații suplimentare", + "Additional feedback saved. Thank you!": "Feedback-ul suplimentar a fost salvat. Vă mulțumim!", + "Addresses": "Adrese", + "Admin": "Administrator", + "Advanced": "Avansat", + "Advanced Filters": "Filtre avansate", + "Advanced Options": "Opțiuni avansate", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Filtrele avansate nu sunt disponibile când utilizați sursa baze de date. Comutați la Auto sau SOLR Index pentru opțiuni de filtrare.", + "After": "După", + "Agent Type": "Tip de agent", + "Agent deleted successfully": "Agentul a fost șters cu succes", + "Agent successfully created": "Agentul a fost creat cu succes", + "Agent successfully updated": "Agentul a fost actualizat cu succes", + "Agents": "Agenți", + "Aggregated across schemas": "Agregat pe toate schemele", + "All": "Toate", + "All Categories": "Toate categoriile", + "All Collections": "Toate colecțiile", + "All Confidence Levels": "Toate nivelurile de încredere", + "All ConfigSets": "Toate ConfigSets", + "All Files": "Toate fișierele", + "All Levels": "Toate nivelurile", + "All Types": "Toate tipurile", + "All Webhooks": "Toate webhook-urile", + "All actions": "Toate acțiunile", + "All audit trails cleared successfully": "Toate pistele de audit au fost șterse cu succes", + "All clear": "Totul în regulă", + "All file types": "Toate tipurile de fișiere", + "All registers": "Toate registrele", + "All schemas": "Toate schemele", + "All search trails cleared successfully": "Toate pistele de căutare au fost șterse cu succes", + "All searches": "Toate căutările", + "All users": "Toți utilizatorii", + "All-day events": "Evenimente de toată ziua", + "Allowed MIME Types": "Tipuri MIME permise", + "Already linked": "Deja asociat", + "An error occurred while clearing audit trails": "A apărut o eroare la ștergerea pistelor de audit", + "An error occurred while deleting the audit trail": "A apărut o eroare la ștergerea pistei de audit", + "An error occurred while deleting the object": "A apărut o eroare la ștergerea obiectului", + "An error occurred while deleting the view": "A apărut o eroare la ștergerea vizualizării", + "An error occurred while permanently deleting the objects": "A apărut o eroare la ștergerea permanentă a obiectelor", + "An error occurred while restoring the objects": "A apărut o eroare la restaurarea obiectelor", + "An error occurred while saving the organisation": "A apărut o eroare la salvarea organizației", + "Analysis completed:": "Analiză finalizată:", + "Analysis steps:": "Pașii analizei:", + "Analytics": "Analize", + "Analyze Objects": "Analizați obiecte", + "Analyze Properties": "Analizați proprietăți", + "Analyze existing properties for improvement opportunities": "Analizați proprietățile existente pentru oportunități de îmbunătățire", + "Analyzing...": "Se analizează...", + "Anonymized": "Anonimizat", + "Anonymous Group": "Grup anonim", + "Any favorites and sharing settings for this view": "Orice setări de favorite și de partajare pentru această vizualizare", + "Any user": "Orice utilizator", + "App ID": "ID aplicație", + "App store cache invalidated successfully": "Cache-ul magazinului de aplicații a fost invalidat cu succes", + "App store cache invalidated: {invalidated}": "Cache-ul magazinului de aplicații a fost invalidat: {invalidated}", + "Application deleted successfully": "Aplicația a fost ștearsă cu succes", + "Application identifier for this configuration (optional)": "Identificator de aplicație pentru această configurație (opțional)", + "Application settings": "Setări aplicație", + "Application successfully created": "Aplicația a fost creată cu succes", + "Application successfully updated": "Aplicația a fost actualizată cu succes", + "Applications": "Aplicații", + "Apply": "Aplicați", + "Apply Changes": "Aplicați modificările", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Aplicați un rol tuturor schemelor din acest registru care nu au suprascrieri explicite de autorizare.", + "Approval Progress": "Progresul aprobării", + "Approve": "Aprobați", + "Approved": "Aprobat", + "Archive": "Arhivați", + "Archive conversation": "Arhivați conversația", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arhivați această verwerkingsactiviteit? Rândurile din pista de audit vor continua să facă referire la aceasta.", + "Archived conversations are hidden from your active list": "Conversațiile arhivate sunt ascunse din lista dumneavoastră activă", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Sigur doriți să ȘTERGEȚI colecția \"{name}\"?\n\nAceasta va:\n• Șterge permanent colecția și toate datele sale\n• Elimina toate documentele indexate\n• Această acțiune nu poate fi anulată", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Sigur doriți să curățați pistele de căutare vechi? Aceasta va șterge intrările mai vechi de 30 de zile.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Sigur doriți să ștergeți toate datele din colecția \"{name}\"?\n\nAceasta va:\n• Șterge toate documentele indexate\n• Păstra structura colecției intactă\n• Această acțiune nu poate fi anulată", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Sigur doriți să ștergeți aplicația \"{name}\"? Această acțiune nu poate fi anulată.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Sigur doriți să ștergeți pistele de audit selectate? Această acțiune nu poate fi anulată.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Sigur doriți să ștergeți pistele de căutare selectate? Această acțiune nu poate fi anulată.", + "Are you sure you want to delete this ConfigSet?": "Sigur doriți să ștergeți acest ConfigSet?", + "Are you sure you want to delete this configset?": "Sigur doriți să ștergeți acest configset?", + "Are you sure you want to leave '{name}'?": "Sigur doriți să părăsiți '{name}'?", + "Are you sure you want to permanently delete": "Sigur doriți să ștergeți permanent", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Sigur doriți să reindexați colecția \"{name}\"?\n\nAceasta va:\n• Reconstrui indexul cu toate obiectele\n• Dura câteva minute până la finalizare\n• Putea afecta performanța căutării în timpul reindexării", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Sigur doriți să eliminați schema \"{schema}\" din registrul \"{register}\"? Această acțiune nu poate fi anulată.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Sigur doriți să ștergeți temporar {count} obiecte active pentru schema \"{schema}\"?\n\nAcestea vor fi marcate ca șterse, dar pot fi eliminate permanent ulterior.", + "Array Object Configuration:": "Configurare obiect tablou:", + "Articles": "Articole", + "Ask a question...": "Puneți o întrebare...", + "Ask questions about your data using natural language": "Puneți întrebări despre datele dumneavoastră utilizând limbaj natural", + "Assigned collaborative tags": "Etichete colaborative atribuite", + "Assignment notifications": "Notificări de atribuire", + "Attachment added to {name}": "Atașament adăugat la {name}", + "Attempt": "Încercare", + "Audit Trail Changes": "Modificări ale pistei de audit", + "Audit Trail Details": "Detalii pistă de audit", + "Audit Trail Management": "Gestionarea pistei de audit", + "Audit Trail Statistics": "Statistici pistă de audit", + "Audit Trails": "Piste de audit", + "Audit trail data copied to clipboard": "Datele pistei de audit au fost copiate în clipboard", + "Audit trail deleted successfully": "Pista de audit a fost ștearsă cu succes", + "Audit trail successfully deleted": "Pista de audit a fost ștearsă cu succes", + "Authentication": "Autentificare", + "Auto (e.g. Y for year, F Y for month)": "Auto (e.g. Y pentru an, F Y pentru lună)", + "Auto share": "Partajare automată", + "Auto-commit disabled": "Confirmare automată dezactivată", + "Auto-commit enabled": "Confirmare automată activată", + "Auto-create Default Organisation": "Creare automată a organizației implicite", + "Auto-retry failed vectorizations": "Reîncercare automată a vectorizărilor eșuate", + "Automatically commit changes to SOLR index": "Confirmați automat modificările în indexul SOLR", + "Automatically connect to the federation network.": "Conectați-vă automat la rețeaua de federație.", + "Automatically create a default organisation if none exists when the app is initialized": "Creați automat o organizație implicită dacă nu există niciuna la inițializarea aplicației", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Generați automat încorporări vectoriale din fragmente de text la încărcarea și procesarea fișierelor", + "Automatically generate vector embeddings when objects are created or updated": "Generați automat încorporări vectoriale la crearea sau actualizarea obiectelor", + "Automatically retry failed vectorization attempts (max 3 retries)": "Reîncercați automat tentativele de vectorizare eșuate (maximum 3 reîncercări)", + "Automatically update catalogues.": "Actualizați automat cataloagele.", + "Available Filters": "Filtre disponibile", + "Available Workflows": "Fluxuri de lucru disponibile", + "Available placeholders:": "Substituenți disponibili:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Modificările de avatar nu sunt acceptate de furnizorul dumneavoastră de autentificare.", + "Avatar removed": "Avatar eliminat", + "Avatar updated successfully": "Avatarul a fost actualizat cu succes", + "Avg Execution Time": "Timp mediu de execuție", + "Avg Members/Org": "Membri medii/Org", + "Avg Object Views/Session": "Vizualizări medii de obiecte/Sesiune", + "Avg Response Time": "Timp mediu de răspuns", + "Avg Results/Search": "Rezultate medii/Căutare", + "Avg Searches/Session": "Căutări medii/Sesiune", + "Back": "Înapoi", + "Back to Entities": "Înapoi la entități", + "Back to Registers": "Înapoi la registre", + "Back to Webhooks": "Înapoi la webhook-uri", + "Back to applications": "Înapoi la aplicații", + "Back to entities": "Înapoi la entități", + "Backend parameter is required": "Parametrul backend este necesar", + "Backend updated successfully. Please reload the application.": "Backend-ul a fost actualizat cu succes. Vă rugăm să reîncărcați aplicația.", + "Bandwidth Quota (MB/month)": "Cotă de lățime de bandă (MB/lună)", + "Base URL (Optional)": "URL de bază (Opțional)", + "Basic Information": "Informații de bază", + "Batch Size": "Dimensiunea lotului", + "Batch extraction completed": "Extragerea în lot a fost finalizată", + "Before": "Înainte", + "Before object vectorization can work:": "Înainte ca vectorizarea obiectelor să poată funcționa:", + "Behavior": "Comportament", + "Behavior Issue": "Problemă de comportament", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (durată ISO-8601, e.g. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Stocarea blob a fost retrasă. Toate obiectele utilizează acum tabele magice.", + "Bookmarks": "Marcaje", + "Branch": "Ramură", + "Bulk Role Assignment: {title}": "Atribuire de roluri în masă: {title}", + "Bulk delete operation completed successfully": "Operațiunea de ștergere în masă a fost finalizată cu succes", + "Bulk save operation completed successfully": "Operațiunea de salvare în masă a fost finalizată cu succes", + "Business Data": "Date de afaceri", + "CSV": "CSV", + "Cache cleared successfully": "Cache-ul a fost șters cu succes", + "Cache grew from {old} to {new} entries.": "Cache-ul a crescut de la {old} la {new} intrări.", + "Cache shrunk from {old} to {new} entries.": "Cache-ul s-a redus de la {old} la {new} intrări.", + "Cache stayed the same at {size} entries.": "Cache-ul a rămas la fel, la {size} intrări.", + "Calculate Sizes": "Calculați dimensiunile", + "Calendar": "Calendar", + "Calendar Provider Configuration": "Configurarea furnizorului de calendar", + "Calendar integration is not available": "Integrarea cu calendarul nu este disponibilă", + "Calendar name": "Numele calendarului", + "Cancel": "Anulați", + "Cancel deactivation request": "Anulați cererea de dezactivare", + "Cannot delete: objects are still attached": "Nu se poate șterge: obiectele sunt încă atașate", + "Cannot edit: This register is managed by external configuration {title}": "Nu se poate edita: Acest registru este gestionat de configurația externă {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nu se poate elimina schema cu obiecte existente ({count} obiecte)", + "Cannot remove schema {schema} because it contains {count} objects": "Nu se poate elimina schema {schema} deoarece conține {count} obiecte", + "Cards": "Carduri", + "Cascade delete": "Ștergere în cascadă", + "Categories of data subjects (one per line)": "Categorii de persoane vizate (câte una pe rând)", + "Categories of personal data (one per line)": "Categorii de date cu caracter personal (câte una pe rând)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Categorie", + "Change Type": "Tip de modificare", + "Change password": "Schimbați parola", + "Changes": "Modificări", + "Changes copied to clipboard": "Modificările au fost copiate în clipboard", + "Changes for Audit Trail #{id}": "Modificări pentru pista de audit #{id}", + "Chat Model": "Model de conversație", + "Chat Provider": "Furnizor de conversație", + "Chat Provider (RAG)": "Furnizor de conversație (RAG)", + "Chat Settings": "Setări conversație", + "Chat provider connection successful!": "Conexiunea cu furnizorul de conversație a reușit!", + "Chat settings": "Setări conversație", + "Chat test failed: {error}": "Testul de conversație a eșuat: {error}", + "Choose a register": "Alegeți un registru", + "Choose a schema": "Alegeți o schemă", + "Choose how vector similarity calculations are performed for semantic search": "Alegeți modul în care sunt efectuate calculele de similaritate vectorială pentru căutarea semantică", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Alegeți ce tipuri de fișiere să includeți în procesul de vectorizare. Vor fi procesate doar fișierele cu text extras și fragmente.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Alegeți ce vizualizări să includeți în procesul de vectorizare. Lăsați gol pentru a procesa toate vizualizările pe baza configurației dumneavoastră.", + "Chunk": "Fragment", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Ștergerea fragmentelor nu este încă implementată. Utilizați puncte finale bazate pe fragmente.", + "Chunks": "Fragmente", + "Chunks to Vectorize:": "Fragmente de vectorizat:", + "Cleanup Old Trails": "Curățați pistele vechi", + "Cleanup completed": "Curățarea a fost finalizată", + "Cleanup completed successfully. Deleted {count} entries.": "Curățarea a fost finalizată cu succes. S-au șters {count} intrări.", + "Cleanup failed: {error}": "Curățarea a eșuat: {error}", + "Clear All Embeddings": "Ștergeți toate încorporările", + "Clear Entries": "Ștergeți intrările", + "Clear Filtered Audit Trails": "Ștergeți pistele de audit filtrate", + "Clear Filters": "Ștergeți filtrele", + "Clear Index": "Ștergeți indexul", + "Clear Selection": "Ștergeți selecția", + "Clear all": "Ștergeți tot", + "Clear all filters": "Ștergeți toate filtrele", + "Clear date range": "Ștergeți intervalul de date", + "Clear filters": "Ștergeți filtrele", + "Clearing...": "Se șterge...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Faceți clic pe Rulați scanarea de conformitate pentru a găsi schemele în care au fost detectate PII, dar nu există nicio adnotare de verwerkingsactiviteit.", + "Click to subscribe to notifications": "Faceți clic pentru a vă abona la notificări", + "Click to unsubscribe from notifications": "Faceți clic pentru a vă dezabona de la notificări", + "Close": "Închideți", + "Cluster name": "Numele clusterului", + "Code": "Cod", + "Code (short readable key, e.g. v-2026-001)": "Cod (cheie scurtă lizibilă, e.g. v-2026-001)", + "Collapse details": "Restrângeți detaliile", + "Collection Management": "Gestionarea colecțiilor", + "Collection Name": "Numele colecției", + "Collection assignments updated successfully": "Atribuirile colecției au fost actualizate cu succes", + "Collection cleared successfully": "Colecția a fost ștearsă cu succes", + "Collection copied successfully": "Colecția a fost copiată cu succes", + "Collection created successfully": "Colecția a fost creată cu succes", + "Collection deleted successfully": "Colecția a fost ștearsă cu succes", + "Collection used to store and index file metadata and content": "Colecție utilizată pentru a stoca și indexa metadatele și conținutul fișierelor", + "Collection used to store and index object data": "Colecție utilizată pentru a stoca și indexa datele obiectelor", + "Collections:": "Colecții:", + "Color": "Culoare", + "Comment...": "Comentariu...", + "Commit Message": "Mesaj de confirmare", + "Commit Within (ms)": "Confirmare în interval de (ms)", + "Commit message": "Mesaj de confirmare", + "Compare current schema with real object data": "Comparați schema curentă cu datele reale ale obiectelor", + "Completed": "Finalizat", + "Complex": "Complex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Interogări complexe: Căutări avansate cu filtre multiple, operatori și combinații complexe de parametri", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Interogări complexe: căutări avansate cu filtre multiple, operatori și combinații complexe de parametri", + "Compliance": "Conformitate", + "Confidence Level": "Nivel de încredere", + "Confidence Score": "Scor de încredere", + "Confidence: {confidence}%": "Încredere: {confidence}%", + "Config must be provided as an object": "Configurația trebuie furnizată ca un obiect", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Gestionarea ConfigSet", + "ConfigSet Name": "Numele ConfigSet", + "ConfigSet created successfully": "ConfigSet creat cu succes", + "ConfigSet deleted successfully": "ConfigSet șters cu succes", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets definesc schema și configurația pentru colecțiile dumneavoastră SOLR. Acestea conțin definiții de câmpuri, analizoare și alte setări de căutare.", + "Configuration": "Configurație", + "Configuration Data": "Date de configurație", + "Configuration is up to date": "Configurația este actualizată", + "Configuration saved": "Configurația a fost salvată", + "Configuration saved successfully.": "Configurația a fost salvată cu succes.", + "Configuration type (default, application, etc.)": "Tip de configurație (implicit, aplicație etc.)", + "Configurations": "Configurații", + "Configure API tokens for external service integrations": "Configurați token-uri API pentru integrările cu servicii externe", + "Configure Apache SOLR search engine for advanced search capabilities": "Configurați motorul de căutare Apache SOLR pentru capabilități de căutare avansate", + "Configure Facets": "Configurați fațetele", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Configurați furnizorii de modele lingvistice mari (LLM) pentru funcții bazate pe AI, inclusiv căutare semantică, încorporări și conversație.", + "Configure SOLR Facets": "Configurați fațetele SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Configurați setările de bază ale conexiunii pentru serverul dumneavoastră SOLR, inclusiv opțiunile de autentificare și de rețea. Utilizați casetele de dialog separate de gestionare ConfigSet și Colecții pentru a gestiona nucleele și colecțiile.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Configurați setările de bază ale conexiunii pentru serverul dumneavoastră SOLR, inclusiv opțiunile de autentificare și de rețea. Utilizați casetele de dialog separate de gestionare a configset-urilor și colecțiilor pentru a gestiona nucleele și colecțiile.", + "Configure default organisation and organisation-related settings": "Configurați organizația implicită și setările legate de organizație", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Configurați modul în care obiectele din baza de date sunt convertite în încorporări vectoriale pentru căutarea semantică. Obiectele sunt vectorizate direct, fără a fi nevoie de extragerea textului.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Configurați modul în care obiectele sunt convertite în text înainte de vectorizare. Aceste setări afectează calitatea și contextul căutării.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Configurați furnizorii de modele lingvistice mari (LLM) pentru funcții bazate pe AI, inclusiv căutare semantică, încorporări și conversație.", + "Configure multi-organization support and tenant isolation": "Configurați suportul pentru organizații multiple și izolarea chiriașilor", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Configurați parametrii pentru vectorizarea fișierelor. Acest proces va genera încorporări vectoriale pentru toate fragmentele de fișier extrase.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Configurați parametrii pentru vectorizarea obiectelor. Acest proces va genera încorporări vectoriale pentru toate obiectele care corespund filtrelor vizualizării dumneavoastră.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Configurați această schemă pentru a afișa obiectele ca evenimente în aplicația Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Configurați ce tipuri de date să căutați și câte surse să recuperați", + "Configuring field mappings...": "Se configurează maparea câmpurilor...", + "Confirm Account Deactivation": "Confirmați dezactivarea contului", + "Confirm Object Deletion": "Confirmați ștergerea obiectului", + "Confirm Object Restoration": "Confirmați restaurarea obiectului", + "Confirm Permanent Object Deletion": "Confirmați ștergerea permanentă a obiectului", + "Confirm deactivation": "Confirmați dezactivarea", + "Connecting to SOLR cluster...": "Se conectează la clusterul SOLR...", + "Connection Failed": "Conexiune eșuată", + "Connection Settings": "Setări de conexiune", + "Connection Successful!": "Conexiune reușită!", + "Connection failed": "Conexiune eșuată", + "Connection protocol": "Protocol de conexiune", + "Connection successful": "Conexiune reușită", + "Connection timeout in seconds": "Timp de expirare a conexiunii în secunde", + "Constant delay between retries (5 minutes)": "Întârziere constantă între reîncercări (5 minute)", + "Constraint Issue": "Problemă de constrângere", + "Consumer not found": "Consumatorul nu a fost găsit", + "Contacts": "Contacte", + "Contacts integration is not available": "Integrarea cu contactele nu este disponibilă", + "Content Filters": "Filtre de conținut", + "Continue to Properties": "Continuați la proprietăți", + "Contracts": "Contracte", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Controlați ce vizualizări de obiecte ar trebui vectorizate pentru a reduce costurile API. Vectorizați doar vizualizările care beneficiază de căutare semantică.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Controlați ce vizualizări și instrumente poate utiliza AI în această conversație. În mod implicit, toate capabilitățile agentului sunt activate.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Controlează caracterul aleatoriu (0 = determinist, 2 = foarte creativ)", + "Conversation ID is required": "ID-ul conversației este necesar", + "Conversation archived": "Conversație arhivată", + "Conversation archived successfully": "Conversația a fost arhivată cu succes", + "Conversation cleared successfully": "Conversația a fost ștearsă cu succes", + "Conversation deleted": "Conversație ștearsă", + "Conversation not found": "Conversația nu a fost găsită", + "Conversation permanently deleted": "Conversația a fost ștearsă permanent", + "Conversation renamed": "Conversație redenumită", + "Conversation restored": "Conversație restaurată", + "Conversation started with {agent}": "Conversație începută cu {agent}", + "Conversation title": "Titlul conversației", + "Conversations": "Conversații", + "Converting {schema} to blob storage...": "Se convertește {schema} în stocare blob...", + "Converting {schema} to magic table...": "Se convertește {schema} în tabel magic...", + "Copied!": "Copiat!", + "Copy": "Copiați", + "Copy Changes": "Copiați modificările", + "Copy Collection": "Copiați colecția", + "Copy Data": "Copiați datele", + "Copy Full Data": "Copiați toate datele", + "Copy of {name}": "Copie a {name}", + "Copy to clipboard": "Copiați în clipboard", + "Copying...": "Se copiază...", + "Costs": "Costuri", + "Count": "Număr", + "Counting objects...": "Se numără obiectele...", + "Create": "Creați", + "Create API Token": "Creați token API", + "Create Agent": "Creați agent", + "Create Application": "Creați aplicație", + "Create Collection": "Creați colecție", + "Create ConfigSet": "Creați ConfigSet", + "Create New Collection": "Creați o colecție nouă", + "Create New ConfigSet": "Creați un ConfigSet nou", + "Create Operations": "Operațiuni de creare", + "Create Organisation": "Creați organizație", + "Create Webhook": "Creați webhook", + "Create a copy of collection:": "Creați o copie a colecției:", + "Create a new ConfigSet based on the _default template": "Creați un ConfigSet nou pe baza șablonului _default", + "Create a new SOLR collection from an existing ConfigSet": "Creați o colecție SOLR nouă dintr-un ConfigSet existent", + "Create a new SOLR collection from an existing configset": "Creați o colecție SOLR nouă dintr-un configset existent", + "Create a new configset based on the _default template": "Creați un configset nou pe baza șablonului _default", + "Create a new event or link an existing one from any of your calendars.": "Creați un eveniment nou sau asociați unul existent din oricare dintre calendarele dumneavoastră.", + "Create another": "Creați altul", + "Create copies of": "Creați copii ale", + "Create event": "Creați eveniment", + "Create new token": "Creați token nou", + "Create or link a Deck card to track work on this object.": "Creați sau asociați un card Deck pentru a urmări activitatea legată de acest obiect.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Creați prima verwerkingsactiviteit pentru a începe etichetarea rândurilor din pista de audit cu atribuirea lor conform AVG Art 30.", + "Create your first AI agent to get started.": "Creați primul dumneavoastră agent AI pentru a începe.", + "Created": "Creat", + "Created:": "Creat:", + "Creating search collections...": "Se creează colecțiile de căutare...", + "Creating...": "Se creează...", + "Current": "Curent", + "Current Filters": "Filtre curente", + "Current Type:": "Tip curent:", + "Current password": "Parola curentă", + "Custom API endpoint if using a different region": "Punct final API personalizat dacă utilizați o regiune diferită", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Anteturi HTTP personalizate (câte unul pe rând, format: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Anteturi HTTP personalizate (câte unul pe rând, format: header-name: value)", + "Custom Ranges:": "Intervale personalizate:", + "Custom display title for this facet": "Titlu de afișare personalizat pentru această fațetă", + "Custom pattern:": "Tipar personalizat:", + "DELETE COLLECTION": "ȘTERGEȚI COLECȚIA", + "DSAR": "DSAR", + "Daily": "Zilnic", + "Dashboard": "Tablou de bord", + "Dashboard not found": "Tabloul de bord nu a fost găsit", + "Data": "Date", + "Data Source": "Sursă de date", + "Data Sources": "Surse de date", + "Data Views": "Vizualizări de date", + "Data sources": "Surse de date", + "Data type does not match observed values": "Tipul de date nu corespunde valorilor observate", + "Data-subject access request": "Cerere de acces a persoanei vizate", + "Database URL": "URL bază de date", + "Database information refreshed": "Informațiile despre baza de date au fost reîmprospătate", + "Date Faceting:": "Fațetare după dată:", + "Date Range": "Interval de date", + "Dates": "Date", + "Deactivation request cancelled": "Cererea de dezactivare a fost anulată", + "Deactivation request submitted": "Cererea de dezactivare a fost trimisă", + "Deck": "Deck", + "Deck integration is not available": "Integrarea cu Deck nu este disponibilă", + "Default": "Implicit", + "Default Object Tenant": "Chiriaș implicit al obiectelor", + "Default Organisation": "Organizație implicită", + "Default Owner": "Proprietar implicit", + "Default User (for cron/background jobs)": "Utilizator implicit (pentru sarcini cron/de fundal)", + "Default User Tenant": "Chiriaș implicit al utilizatorilor", + "Default value": "Valoare implicită", + "Default value (Email)": "Valoare implicită (Email)", + "Default value (Password)": "Valoare implicită (Parolă)", + "Default value (Phone number)": "Valoare implicită (Număr de telefon)", + "Default value (Regex)": "Valoare implicită (Regex)", + "Default: 5": "Implicit: 5", + "Defaults to current user. Select a different user if needed.": "Implicit este utilizatorul curent. Selectați un alt utilizator dacă este necesar.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Întârzierile se dublează la fiecare încercare (2, 4, 8 minute...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Întârzierile cresc liniar (5, 10, 15 minute...)", + "Delete": "Ștergeți", + "Delete ({count})": "Ștergeți ({count})", + "Delete Application": "Ștergeți aplicația", + "Delete Audit Trail": "Ștergeți pista de audit", + "Delete Collection": "Ștergeți colecția", + "Delete ConfigSet": "Ștergeți ConfigSet", + "Delete Objects": "Ștergeți obiectele", + "Delete Operations": "Operațiuni de ștergere", + "Delete SOLR Collection": "Ștergeți colecția SOLR", + "Delete View": "Ștergeți vizualizarea", + "Delete all objects in this schema": "Ștergeți toate obiectele din această schemă", + "Delete permanently": "Ștergeți permanent", + "Delete this conversation?": "Ștergeți această conversație?", + "Delete view": "Ștergeți vizualizarea", + "Delete {count} object": "Ștergeți {count} obiect", + "Delete {count} objects": "Ștergeți {count} obiecte", + "Deleted": "Șters", + "Deleted By": "Șters de", + "Deleted Date": "Data ștergerii", + "Deleted Items Management": "Gestionarea elementelor șterse", + "Deleted This Week": "Șterse săptămâna aceasta", + "Deleted Today": "Șterse astăzi", + "Deleted:": "Șters:", + "Deleting SOLR Collection...": "Se șterge colecția SOLR...", + "Deleting...": "Se șterge...", + "Deletion Date Range": "Interval de date al ștergerii", + "Deletion Statistics": "Statistici de ștergere", + "Deprecated": "Învechit", + "Depublish": "Depublicați", + "Description": "Descriere", + "Description Template": "Șablon de descriere", + "Description shown as tooltip": "Descriere afișată ca sfat", + "Description:": "Descriere:", + "Detect data types and patterns": "Detectați tipurile de date și tiparele", + "Detected At": "Detectat la", + "Detected Format:": "Format detectat:", + "Detected Issues:": "Probleme detectate:", + "Detected: {date}": "Detectat: {date}", + "Direct permission": "Permisiune directă", + "Disable": "Dezactivați", + "Disable this to make the agent": "Dezactivați aceasta pentru a face agentul", + "Disabled": "Dezactivat", + "Discover Files": "Descoperiți fișiere", + "Discovered Properties": "Proprietăți descoperite", + "Display Format": "Format de afișare", + "Display Name": "Nume afișat", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Doriți să ștergeți permanent toate intrările filtrate din pista de audit? Această acțiune nu poate fi anulată.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Doriți să ștergeți permanent această intrare din pista de audit? Această acțiune nu poate fi anulată.", + "Documents": "Documente", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "OCR Dolphin activat", + "Download API Spec": "Descărcați specificația API", + "Download API Specification": "Descărcați specificația API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Descărcați o copie a tuturor datelor dumneavoastră cu caracter personal stocate în OpenRegister (GDPR Articolul 20).", + "Drag a file or files here": "Trageți un fișier sau mai multe fișiere aici", + "Dry run completed: {count} fields would be created": "Rulare de probă finalizată: {count} câmpuri ar fi create", + "Dry run completed: {count} fields would be fixed": "Rulare de probă finalizată: {count} câmpuri ar fi reparate", + "Duration in seconds (optional)": "Durată în secunde (opțional)", + "Edit": "Editați", + "Edit Agent": "Editați agentul", + "Edit Application": "Editați aplicația", + "Edit Configuration": "Editați configurația", + "Edit Endpoint": "Editați punctul final", + "Edit Organisation": "Editați organizația", + "Edit Property '{key}' of '{title}'": "Editați proprietatea '{key}' a '{title}'", + "Edit Register": "Editați registrul", + "Edit Schema": "Editați schema", + "Edit Source": "Editați sursa", + "Edit View": "Editați vizualizarea", + "Edit Webhook": "Editați webhook-ul", + "Edit labels": "Editați etichetele", + "Edit processing activity": "Editați verwerkingsactiviteit", + "Edit tags for {name}": "Editați etichetele pentru {name}", + "Edit verwerkingsactiviteit": "Editați verwerkingsactiviteit", + "Edit view": "Editați vizualizarea", + "Edit view details": "Editați detaliile vizualizării", + "Effectiveness": "Eficacitate", + "Email": "E-mail", + "Email Addresses": "Adrese de e-mail", + "Email digest frequency": "Frecvența rezumatului prin e-mail", + "Emails": "E-mailuri", + "Embedding Model": "Model de embedding", + "Embedding Provider": "Furnizor de embedding", + "Embedding generated successfully": "Embedding generat cu succes", + "Embedding provider connection successful!": "Conexiunea cu furnizorul de embedding a reușit!", + "Embedding test failed: {error}": "Testul de embedding a eșuat: {error}", + "Enable": "Activează", + "Enable RAG": "Activează RAG", + "Enable auto-creation": "Activează crearea automată", + "Enable automatic file vectorization": "Activează vectorizarea automată a fișierelor", + "Enable automatic object vectorization": "Activează vectorizarea automată a obiectelor", + "Enable automatic synchronization": "Activează sincronizarea automată", + "Enable calendar provider": "Activează furnizorul de calendar", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Activează jurnalizarea detaliată pentru operațiunile SOLR (recomandat pentru depanare)", + "Enable faceting": "Activează fațetarea", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Activează sau dezactivează funcțiile LLM. Configurați furnizorii și modelele folosind butonul LLM Configuration de mai sus.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Activează sau dezactivează funcțiile LLM. Configurați furnizorii și modelele folosind butonul de configurare LLM de mai sus.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Activează sau dezactivează integrarea căutării SOLR. Configurați setările de conexiune folosind butonul Connection Settings de mai sus.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Activează sau dezactivează integrarea căutării SOLR. Configurați setările de conexiune folosind butonul de setări de conexiune de mai sus.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Activează sau dezactivează integrarea fluxurilor de lucru n8n. Configurați setările de conexiune mai jos.", + "Enable or disable this webhook": "Activează sau dezactivează acest webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Activează instrumentele care permit agentului să interacționeze cu datele prin apeluri de funcții.", + "Enable vectorization for all existing and future views (may increase costs)": "Activează vectorizarea pentru toate vizualizările existente și viitoare (poate crește costurile)", + "Enable write-back to target objects": "Activează scrierea înapoi în obiectele țintă", + "Enabled": "Activat", + "End Date Field": "Câmp dată de sfârșit", + "Endpoint Path*": "Cale endpoint*", + "Endpoints": "Endpoint-uri", + "Engine not found": "Motorul nu a fost găsit", + "Enter ConfigSet name": "Introduceți numele ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Introduceți numele de utilizator Nextcloud pentru a acorda acces la acest agent privat", + "Enter agent description (optional)": "Introduceți descrierea agentului (opțional)", + "Enter agent name": "Introduceți numele agentului", + "Enter application description (optional)": "Introduceți descrierea aplicației (opțional)", + "Enter application name": "Introduceți numele aplicației", + "Enter collection name": "Introduceți numele colecției", + "Enter configuration description (optional)": "Introduceți descrierea configurației (opțional)", + "Enter configuration title": "Introduceți titlul configurației", + "Enter description (optional)...": "Introduceți descrierea (opțional)...", + "Enter new collection name": "Introduceți numele noii colecții", + "Enter object ID": "Introduceți ID-ul obiectului", + "Enter organisation description (optional)": "Introduceți descrierea organizației (opțional)", + "Enter organisation name": "Introduceți numele organizației", + "Enter search term": "Introduceți termenul de căutare", + "Enter search terms or leave empty to browse all": "Introduceți termenii de căutare sau lăsați gol pentru a răsfoi tot", + "Enter system prompt for the agent": "Introduceți prompterul de sistem pentru agent", + "Enter username and press Enter": "Introduceți numele de utilizator și apăsați Enter", + "Enter view name...": "Introduceți numele vizualizării...", + "Enter webhook name": "Introduceți numele webhook-ului", + "Enter your API key": "Introduceți cheia dvs. API", + "Enter your object here...": "Introduceți obiectul dvs. aici...", + "Enter your schema here...": "Introduceți schema dvs. aici...", + "Entities": "Entități", + "Entities detected": "Entități detectate", + "Entity ID: {id}": "ID entitate: {id}", + "Entity Information": "Informații despre entitate", + "Entity Recognition": "Recunoaștere a entităților", + "Entity Recognition Method": "Metodă de recunoaștere a entităților", + "Entity deleted successfully": "Entitate ștearsă cu succes", + "Entity not found": "Entitatea nu a fost găsită", + "Entries to be deleted:": "Intrări care vor fi șterse:", + "Enum Issue": "Problemă enum", + "Enum constraint is missing": "Constrângerea enum lipsește", + "Erase (Art 17)": "Ștergere (Art. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Ștergeți {count} obiect(e) pentru acest subiect? Această acțiune este înregistrată în pista de audit.", + "Erasure complete": "Ștergere finalizată", + "Erasure preview": "Previzualizare ștergere", + "Error": "Eroare", + "Error Details": "Detalii eroare", + "Error Information": "Informații despre eroare", + "Error Message": "Mesaj de eroare", + "Error deleting audit trails: {error}": "Eroare la ștergerea pistelor de audit: {error}", + "Error deleting search trails: {error}": "Eroare la ștergerea pistelor de căutare: {error}", + "Error loading application": "Eroare la încărcarea aplicației", + "Error loading audit trails": "Eroare la încărcarea pistelor de audit", + "Error loading entity": "Eroare la încărcarea entității", + "Error loading search trails": "Eroare la încărcarea pistelor de căutare", + "Estimated Batches:": "Loturi estimate:", + "Estimated Cost:": "Cost estimat:", + "Estimated Duration:": "Durată estimată:", + "Event": "Eveniment", + "Event Property for Payload": "Proprietate eveniment pentru sarcina utilă", + "Events": "Evenimente", + "Every schema with detected PII has a processing-activity annotation.": "Fiecare schemă cu PII detectat are o adnotare de activitate de prelucrare.", + "Example": "Exemplu", + "Example Value": "Valoare exemplu", + "Example value for this property": "Valoare exemplu pentru această proprietate", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Maxim exclusiv", + "Exclusive Minimum": "Minim exclusiv", + "Exclusive maximum": "Maxim exclusiv", + "Exclusive minimum": "Minim exclusiv", + "Execution Mode": "Mod de execuție", + "Execution Time": "Timp de execuție", + "Execution Time Range": "Interval de timp de execuție", + "Existing Improvements": "Îmbunătățiri existente", + "Expand details": "Extinde detaliile", + "Expiration": "Expirare", + "Expires": "Expiră", + "Expires in (e.g., 90d)": "Expiră în (de ex., 90d)", + "Exponential": "Exponențial", + "Export": "Exportă", + "Export \"{schema}\" objects from \"{register}\"": "Exportă obiectele „{schema}” din „{register}”", + "Export Configuration": "Configurație export", + "Export Format:": "Format export:", + "Export Objects": "Exportă obiecte", + "Export completed successfully": "Export finalizat cu succes", + "Export downloaded successfully": "Export descărcat cu succes", + "Export failed: {error}": "Exportul a eșuat: {error}", + "Export is rate limited. Please try again later.": "Exportul este limitat ca rată. Vă rugăm să încercați din nou mai târziu.", + "Export my data": "Exportă datele mele", + "Export, view, or delete audit trails": "Exportați, vizualizați sau ștergeți pistele de audit", + "Exporting...": "Se exportă...", + "Extend Schema": "Extinde schema", + "Extended": "Extins", + "Extensions": "Extensii", + "Extra Query Parameters": "Parametri de interogare suplimentari", + "Extract Now": "Extrage acum", + "Extract Pending Files": "Extrage fișierele în așteptare", + "Extract now": "Extrage acum", + "Extract properties from each object": "Extrage proprietățile din fiecare obiect", + "Extracted At": "Extras la", + "Extracted at": "Extras la", + "Extraction": "Extragere", + "Extraction Mode": "Mod de extragere", + "Extraction Scope": "Domeniu de extragere", + "Extraction Status": "Stare extragere", + "Extraction error for {file}: {error}": "Eroare de extragere pentru {file}: {error}", + "Facet Description": "Descriere fațetă", + "Facet Order": "Ordine fațetă", + "Facet Title": "Titlu fațetă", + "Facet Type": "Tip fațetă", + "Facet configuration updated successfully": "Configurația fațetei a fost actualizată cu succes", + "Facetable": "Fațetabil", + "Faceting Configuration:": "Configurație fațetare:", + "Facets discovered and configured successfully": "Fațetele au fost descoperite și configurate cu succes", + "Facets discovered successfully": "Fațetele au fost descoperite cu succes", + "Failed": "Eșuat", + "Failed to add attachment to object": "Adăugarea atașamentului la obiect a eșuat", + "Failed to add user to organisation": "Adăugarea utilizatorului în organizație a eșuat", + "Failed to analyze schema properties": "Analiza proprietăților schemei a eșuat", + "Failed to archive conversation": "Arhivarea conversației a eșuat", + "Failed to calculate sizes": "Calcularea dimensiunilor a eșuat", + "Failed to cancel deactivation": "Anularea dezactivării a eșuat", + "Failed to change active organisation: {error}": "Schimbarea organizației active a eșuat: {error}", + "Failed to change password": "Schimbarea parolei a eșuat", + "Failed to check version: {error}": "Verificarea versiunii a eșuat: {error}", + "Failed to clear audit trails: {error}": "Golirea pistelor de audit a eșuat: {error}", + "Failed to clear blob objects: {error}": "Golirea obiectelor blob a eșuat: {error}", + "Failed to clear cache: {error}": "Golirea cache-ului a eșuat: {error}", + "Failed to clear collection": "Golirea colecției a eșuat", + "Failed to clear conversation": "Golirea conversației a eșuat", + "Failed to clear embeddings: {error}": "Golirea embedding-urilor a eșuat: {error}", + "Failed to clear search trails: {error}": "Golirea pistelor de căutare a eșuat: {error}", + "Failed to clear {type} cache: {error}": "Golirea cache-ului {type} a eșuat: {error}", + "Failed to copy changes": "Copierea modificărilor a eșuat", + "Failed to copy data": "Copierea datelor a eșuat", + "Failed to copy data to clipboard": "Copierea datelor în clipboard a eșuat", + "Failed to copy to clipboard": "Copierea în clipboard a eșuat", + "Failed to copy token": "Copierea tokenului a eșuat", + "Failed to create conversation": "Crearea conversației a eșuat", + "Failed to create missing SOLR fields: {error}": "Crearea câmpurilor SOLR lipsă a eșuat: {error}", + "Failed to create or find project": "Crearea sau găsirea proiectului a eșuat", + "Failed to create token": "Crearea tokenului a eșuat", + "Failed to delete SOLR collection: {error}": "Ștergerea colecției SOLR a eșuat: {error}", + "Failed to delete collection": "Ștergerea colecției a eșuat", + "Failed to delete conversation": "Ștergerea conversației a eșuat", + "Failed to delete objects for {schema}: {error}": "Ștergerea obiectelor pentru {schema} a eșuat: {error}", + "Failed to delete webhook": "Ștergerea webhook-ului a eșuat", + "Failed to depublish register: {error}": "Depublicarea registrului a eșuat: {error}", + "Failed to depublish schema: {error}": "Depublicarea schemei a eșuat: {error}", + "Failed to depublish: {error}": "Depublicarea a eșuat: {error}", + "Failed to discover files: {error}": "Descoperirea fișierelor a eșuat: {error}", + "Failed to download API specification": "Descărcarea specificației API a eșuat", + "Failed to export data": "Exportul datelor a eșuat", + "Failed to export objects": "Exportul obiectelor a eșuat", + "Failed to fetch conversation history": "Preluarea istoricului conversației a eșuat", + "Failed to fix mismatched SOLR fields: {error}": "Corectarea câmpurilor SOLR neconcordante a eșuat: {error}", + "Failed to get SOLR field configuration": "Obținerea configurației câmpurilor SOLR a eșuat", + "Failed to get chat statistics": "Obținerea statisticilor de chat a eșuat", + "Failed to get database information: %s": "Obținerea informațiilor despre baza de date a eșuat: %s", + "Failed to get entity categories": "Obținerea categoriilor de entități a eșuat", + "Failed to get entity statistics": "Obținerea statisticilor de entități a eșuat", + "Failed to get entity types": "Obținerea tipurilor de entități a eșuat", + "Failed to get response: {error}": "Obținerea răspunsului a eșuat: {error}", + "Failed to invalidate app store cache: {error}": "Invalidarea cache-ului magazinului de aplicații a eșuat: {error}", + "Failed to leave organisation: {error}": "Părăsirea organizației a eșuat: {error}", + "Failed to link object": "Legarea obiectului a eșuat", + "Failed to load Deck cards": "Încărcarea cardurilor Deck a eșuat", + "Failed to load LLM configuration": "Încărcarea configurației LLM a eșuat", + "Failed to load Nextcloud groups": "Încărcarea grupurilor Nextcloud a eșuat", + "Failed to load cache statistics: {error}": "Încărcarea statisticilor cache-ului a eșuat: {error}", + "Failed to load conversation": "Încărcarea conversației a eșuat", + "Failed to load entities": "Încărcarea entităților a eșuat", + "Failed to load entity": "Încărcarea entității a eșuat", + "Failed to load extraction data": "Încărcarea datelor de extragere a eșuat", + "Failed to load files": "Încărcarea fișierelor a eșuat", + "Failed to load linked contacts": "Încărcarea contactelor legate a eșuat", + "Failed to load linked emails": "Încărcarea e-mailurilor legate a eșuat", + "Failed to load linked events": "Încărcarea evenimentelor legate a eșuat", + "Failed to load organisations": "Încărcarea organizațiilor a eșuat", + "Failed to load register data": "Încărcarea datelor registrului a eșuat", + "Failed to load related entities": "Încărcarea entităților asociate a eșuat", + "Failed to load settings: {error}": "Încărcarea setărilor a eșuat: {error}", + "Failed to load statistics: {error}": "Încărcarea statisticilor a eșuat: {error}", + "Failed to load templates": "Încărcarea șabloanelor a eșuat", + "Failed to load view: {error}": "Încărcarea vizualizării a eșuat: {error}", + "Failed to load webhooks": "Încărcarea webhook-urilor a eșuat", + "Failed to load workflows": "Încărcarea fluxurilor de lucru a eșuat", + "Failed to process message": "Procesarea mesajului a eșuat", + "Failed to publish register: {error}": "Publicarea registrului a eșuat: {error}", + "Failed to publish schema: {error}": "Publicarea schemei a eșuat: {error}", + "Failed to publish: {error}": "Publicarea a eșuat: {error}", + "Failed to refresh database information": "Reîmprospătarea informațiilor despre baza de date a eșuat", + "Failed to reindex collection": "Reindexarea colecției a eșuat", + "Failed to remove avatar": "Eliminarea avatarului a eșuat", + "Failed to remove link": "Eliminarea legăturii a eșuat", + "Failed to remove member: {error}": "Eliminarea membrului a eșuat: {error}", + "Failed to remove schema {schema}: {error}": "Eliminarea schemei {schema} a eșuat: {error}", + "Failed to remove user from organisation": "Eliminarea utilizatorului din organizație a eșuat", + "Failed to rename conversation": "Redenumirea conversației a eșuat", + "Failed to request deactivation": "Solicitarea dezactivării a eșuat", + "Failed to restore conversation": "Restaurarea conversației a eșuat", + "Failed to retrieve extraction status.": "Preluarea stării de extragere a eșuat.", + "Failed to retrieve objects for file.": "Preluarea obiectelor pentru fișier a eșuat.", + "Failed to retry extraction": "Reîncercarea extragerii a eșuat", + "Failed to retry webhook": "Reîncercarea webhook-ului a eșuat", + "Failed to revoke token": "Revocarea tokenului a eșuat", + "Failed to save GitHub token": "Salvarea tokenului GitHub a eșuat", + "Failed to save GitLab URL": "Salvarea URL-ului GitLab a eșuat", + "Failed to save GitLab token": "Salvarea tokenului GitLab a eșuat", + "Failed to save LLM settings: {error}": "Salvarea setărilor LLM a eșuat: {error}", + "Failed to save additional feedback": "Salvarea feedbackului suplimentar a eșuat", + "Failed to save configuration.": "Salvarea configurației a eșuat.", + "Failed to save configuration: {error}": "Salvarea configurației a eșuat: {error}", + "Failed to save facet configuration": "Salvarea configurației fațetei a eșuat", + "Failed to save feedback": "Salvarea feedbackului a eșuat", + "Failed to save file settings: {error}": "Salvarea setărilor fișierelor a eșuat: {error}", + "Failed to save n8n configuration": "Salvarea configurației n8n a eșuat", + "Failed to save preferences": "Salvarea preferințelor a eșuat", + "Failed to save roles": "Salvarea rolurilor a eșuat", + "Failed to save settings": "Salvarea setărilor a eșuat", + "Failed to save settings: {error}": "Salvarea setărilor a eșuat: {error}", + "Failed to save view: {error}": "Salvarea vizualizării a eșuat: {error}", + "Failed to save warmup interval: {error}": "Salvarea intervalului de încălzire a eșuat: {error}", + "Failed to save webhook": "Salvarea webhook-ului a eșuat", + "Failed to send feedback": "Trimiterea feedbackului a eșuat", + "Failed to set active organisation: {error}": "Setarea organizației active a eșuat: {error}", + "Failed to setup SOLR: {error}": "Configurarea SOLR a eșuat: {error}", + "Failed to start file warmup: {error}": "Pornirea încălzirii fișierelor a eșuat: {error}", + "Failed to start processing {type} files: {error}": "Pornirea procesării fișierelor {type} a eșuat: {error}", + "Failed to start vectorization: {error}": "Pornirea vectorizării a eșuat: {error}", + "Failed to switch organisation: {error}": "Comutarea organizației a eșuat: {error}", + "Failed to sync magic table for {schema}: {error}": "Sincronizarea tabelului magic pentru {schema} a eșuat: {error}", + "Failed to test SOLR connection: {error}": "Testarea conexiunii SOLR a eșuat: {error}", + "Failed to test webhook": "Testarea webhook-ului a eșuat", + "Failed to update LLM settings: {error}": "Actualizarea setărilor LLM a eșuat: {error}", + "Failed to update Multitenancy settings: {error}": "Actualizarea setărilor Multitenancy a eșuat: {error}", + "Failed to update RBAC settings: {error}": "Actualizarea setărilor RBAC a eșuat: {error}", + "Failed to update Retention settings: {error}": "Actualizarea setărilor de retenție a eșuat: {error}", + "Failed to update SOLR settings: {error}": "Actualizarea setărilor SOLR a eșuat: {error}", + "Failed to update favorite status": "Actualizarea stării de favorit a eșuat", + "Failed to update schema configuration for {schema}: {error}": "Actualizarea configurației schemei pentru {schema} a eșuat: {error}", + "Failed to update schema properties": "Actualizarea proprietăților schemei a eșuat", + "Failed to update view": "Actualizarea vizualizării a eșuat", + "Failed to update view: {error}": "Actualizarea vizualizării a eșuat: {error}", + "Failed to update webhook": "Actualizarea webhook-ului a eșuat", + "Failed to upload avatar": "Încărcarea avatarului a eșuat", + "Failed to validate {schema}: {error}": "Validarea {schema} a eșuat: {error}", + "Failed to vectorize files: {error}": "Vectorizarea fișierelor a eșuat: {error}", + "Failed to warmup names cache: {error}": "Încălzirea cache-ului de nume a eșuat: {error}", + "Federation": "Federație", + "Feedback recorded": "Feedback înregistrat", + "Fewer sources (1-3):": "Mai puține surse (1-3):", + "Field": "Câmp", + "Fields to Display": "Câmpuri de afișat", + "File": "Fișier", + "File #{id}": "Fișier #{id}", + "File Chunk Prediction": "Predicție fragment de fișier", + "File Collection": "Colecție de fișiere", + "File Handling": "Gestionarea fișierelor", + "File Location": "Locația fișierului", + "File Management": "Gestionarea fișierelor", + "File Name": "Nume fișier", + "File Path": "Cale fișier", + "File Type Selection": "Selectarea tipului de fișier", + "File Types to Process": "Tipuri de fișiere de procesat", + "File Vectorization": "Vectorizare fișiere", + "File Warmup": "Încălzire fișiere", + "File actions menu": "Meniu acțiuni fișier", + "File anonymized successfully": "Fișier anonimizat cu succes", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vectorizarea fragmentelor de fișiere nu este încă implementată. Fragmentele sunt pregătite și stocate, dar serviciul de vectorizare este în dezvoltare.", + "File collection not configured": "Colecția de fișiere nu este configurată", + "File discovery completed": "Descoperirea fișierelor a fost finalizată", + "File extraction completed": "Extragerea fișierelor a fost finalizată", + "File extraction queued": "Extragerea fișierelor a fost pusă în coadă", + "File indexed successfully": "Fișier indexat cu succes", + "File is already anonymized": "Fișierul este deja anonimizat", + "File name": "Nume fișier", + "File not found": "Fișierul nu a fost găsit", + "File settings saved successfully": "Setările fișierelor au fost salvate cu succes", + "File settings updated successfully": "Setările fișierelor au fost actualizate cu succes", + "File sources": "Surse fișiere", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vectorizarea fișierelor a fost finalizată! {vectorized} fragmente vectorizate din {files} fișiere. {failed} eșuate.", + "File vectorization configuration saved successfully": "Configurația de vectorizare a fișierelor a fost salvată cu succes", + "File vectorization started. Check the statistics section for progress.": "Vectorizarea fișierelor a început. Verificați secțiunea de statistici pentru progres.", + "File warmup completed": "Încălzirea fișierelor a fost finalizată", + "Files": "Fișiere", + "Files added successfully": "Fișiere adăugate cu succes", + "Files with Completed Extraction:": "Fișiere cu extragere finalizată:", + "Files → fileCollection, Objects → objectCollection": "Fișiere → fileCollection, Obiecte → objectCollection", + "Files → filecollection, objects → objectcollection": "Fișiere → filecollection, obiecte → objectcollection", + "Filter Audit Trails": "Filtrează pistele de audit", + "Filter Deleted Items": "Filtrează elementele șterse", + "Filter Objects": "Filtrează obiectele", + "Filter Properties": "Filtrează proprietățile", + "Filter Search Trails": "Filtrează pistele de căutare", + "Filter Statistics": "Filtrează statisticile", + "Filter and analyze search trail entries": "Filtrați și analizați intrările din pista de căutare", + "Filter and manage audit trail entries": "Filtrați și gestionați intrările din pista de audit", + "Filter and manage soft deleted items": "Filtrați și gestionați elementele șterse temporar", + "Filter and search entities": "Filtrați și căutați entități", + "Filter by object ID": "Filtrează după ID-ul obiectului", + "Filter by search term": "Filtrează după termenul de căutare", + "Filter by type": "Filtrează după tip", + "Filter by webhook": "Filtrează după webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Datele de filtrare au fost încărcate automat. Folosiți filtrele de mai jos pentru a vă rafina căutarea.", + "Filter fields...": "Filtrează câmpurile...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrează declanșatoarele webhook după proprietățile sarcinii utile (una pe linie, format: key: value)", + "Filtered": "Filtrat", + "Filters": "Filtre", + "Finalizing setup...": "Se finalizează configurarea...", + "Fireworks AI Chat Settings": "Setări chat Fireworks AI", + "Fireworks AI Embedding Configuration": "Configurație embedding Fireworks AI", + "First": "Primul", + "Fixed": "Fix", + "For chat and retrieval-augmented generation": "Pentru chat și generare augmentată prin recuperare", + "For vector embeddings and semantic search": "Pentru embedding-uri vectoriale și căutare semantică", + "Format": "Format", + "Format Issue": "Problemă de format", + "Format constraint is missing": "Constrângerea de format lipsește", + "From (e.g. -7 days or 2025-01-01)": "De la (de ex. -7 days sau 2025-01-01)", + "From Date": "De la data", + "From date": "De la data", + "Full data copied to clipboard": "Datele complete au fost copiate în clipboard", + "Function Tools": "Instrumente de funcții", + "General Issue": "Problemă generală", + "Generate recommendations and confidence scores": "Generează recomandări și scoruri de încredere", + "Generate report": "Generează raport", + "Generate the verantwoordingsdocument": "Generează verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generează vectori imediat ce sunt create obiecte noi", + "Generated: {time}": "Generat: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Obțineți informații în timp real despre starea de sănătate a datelor organizației dvs. concentrându-vă pe registre, definiții de scheme și stocarea și utilizarea obiectelor.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Obțineți informații în timp real despre starea de sănătate a datelor organizației dvs., tendințele de creștere și metricile de calitate.", + "GitHub Personal Access Token": "Token de acces personal GitHub", + "GitHub token is valid": "Tokenul GitHub este valid", + "GitHub token is valid! Username: {username}": "Tokenul GitHub este valid! Nume utilizator: {username}", + "GitHub token removed successfully": "Tokenul GitHub a fost eliminat cu succes", + "GitHub token saved successfully": "Tokenul GitHub a fost salvat cu succes", + "GitHub token test failed: {message}": "Testarea tokenului GitHub a eșuat: {message}", + "GitLab Personal Access Token": "Token de acces personal GitLab", + "GitLab URL saved successfully": "URL-ul GitLab a fost salvat cu succes", + "GitLab token is valid": "Tokenul GitLab este valid", + "GitLab token is valid! Username: {username}": "Tokenul GitLab este valid! Nume utilizator: {username}", + "GitLab token saved successfully": "Tokenul GitLab a fost salvat cu succes", + "GitLab token test failed: {message}": "Testarea tokenului GitLab a eșuat: {message}", + "Go to linked Register": "Mergi la Registrul legat", + "Go to linked Schema": "Mergi la Schema legată", + "Go to organisation": "Mergi la organizație", + "Grant additional permissions beyond standard CRUD operations": "Acordă permisiuni suplimentare dincolo de operațiunile CRUD standard", + "Group Quota": "Cotă de grup", + "Groups": "Grupuri", + "HTML preview": "Previzualizare HTML", + "HTTP Method": "Metodă HTTP", + "HTTP method used to send webhook requests": "Metoda HTTP folosită pentru a trimite cererile webhook", + "Headers": "Anteturi", + "Health": "Sănătate", + "Heartbeat successful - connection kept alive": "Heartbeat reușit - conexiunea menținută activă", + "Helpful": "Util", + "Here you can configure the details for various connections.": "Aici puteți configura detaliile pentru diverse conexiuni.", + "Here you can configure the details for your organisation.": "Aici puteți configura detaliile pentru organizația dvs.", + "Hide": "Ascunde", + "Hide Filters": "Ascunde filtrele", + "Hide in collection view": "Ascunde în vizualizarea de colecție", + "Hide in forms": "Ascunde în formulare", + "Hide in list view": "Ascunde în vizualizarea de listă", + "High": "Ridicat", + "High Confidence": "Încredere ridicată", + "Host": "Gazdă", + "Hourly": "Orar", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Cât de adânc să se traverseze proprietățile imbricate ale obiectelor (1-20). Valorile mai mari captează mai multe detalii, dar cresc dimensiunea vectorului.", + "How often to check for updates (1-168 hours)": "Cât de des să se verifice actualizările (1-168 de ore)", + "How to handle retries for failed webhook deliveries": "Cum să se gestioneze reîncercările pentru livrările webhook eșuate", + "IBANs": "IBAN-uri", + "ID": "ID", + "ID of input mapping (optional)": "ID-ul mapării de intrare (opțional)", + "ID of output mapping (optional)": "ID-ul mapării de ieșire (opțional)", + "ID of the target resource": "ID-ul resursei țintă", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Limitele de rată IP au fost golite cu succes", + "Identify properties not in the schema": "Identifică proprietățile care nu se află în schemă", + "Immutable": "Imutabil", + "Import": "Importă", + "Import Configuration": "Configurație import", + "Import Data into Register": "Importă date în Registru", + "Import successful": "Import reușit", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importați șablonul report-bundle.json pentru a obține registrul `reports`, apoi creați primul dvs. tablou de bord prin interfața standard a obiectelor. Tablourile de bord își declară widget-urile în JSON, iar randarea alimentează fiecare widget cu date de agregare în timp real.", + "Improved Property": "Proprietate îmbunătățită", + "In use": "În uz", + "Inactive": "Inactiv", + "Inactive organisations cannot be used": "Organizațiile inactive nu pot fi folosite", + "Include IDs and names of related objects for better contextual search": "Include ID-urile și numele obiectelor asociate pentru o căutare contextuală mai bună", + "Include ids and names of related objects for better contextual search": "Include ID-urile și numele obiectelor asociate pentru o căutare contextuală mai bună", + "Include related object references": "Include referințele obiectelor asociate", + "Include schema and register metadata": "Include metadatele schemei și registrului", + "Index": "Index", + "Information about the current OpenRegister installation": "Informații despre instalarea curentă OpenRegister", + "Inherited from register": "Moștenit din registru", + "Inherits permissions from register": "Moștenește permisiunile din registru", + "Initialization failed": "Inițializarea a eșuat", + "Initialize Project": "Inițializează proiectul", + "Initializing SOLR setup...": "Se inițializează configurarea SOLR...", + "Input Mapping": "Mapare de intrare", + "Inspect Fields": "Inspectează câmpurile", + "Inspect Index": "Inspectează indexul", + "Inspect SOLR Index": "Inspectează indexul SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instalați aplicația notify_push din Nextcloud App Store pentru a activa actualizările în timp real.", + "Integrations": "Integrări", + "Internet location (URL) of this installation": "Locația pe internet (URL) a acestei instalări", + "Interval": "Interval", + "Invalid": "Nevalid", + "Invalid batch size. Must be between 1 and 5000": "Dimensiune a lotului nevalidă. Trebuie să fie între 1 și 5000", + "Invalid feedback type": "Tip de feedback nevalid", + "Invalid field name provided": "Nume de câmp furnizat nevalid", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects nevalid. Trebuie să fie 0 (toate) sau un număr pozitiv", + "Invalid:": "Nevalid:", + "Invite Users": "Invitați utilizatori", + "Invited Users": "Utilizatori invitați", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Rezultate Inzage", + "Issue": "Problemă", + "Items per page:": "Elemente pe pagină:", + "Just now": "Chiar acum", + "Key": "Cheie", + "LLM Configuration": "Configurare LLM", + "LLM actions menu": "Meniu de acțiuni LLM", + "LLM configuration saved successfully": "Configurarea LLM a fost salvată cu succes", + "LLM features disabled": "Funcționalitățile LLM au fost dezactivate", + "LLM features enabled": "Funcționalitățile LLM au fost activate", + "LLM must be enabled with an embedding provider configured": "LLM trebuie să fie activat cu un furnizor de embedding configurat", + "LLM settings saved successfully": "Setările LLM au fost salvate cu succes", + "LLM settings updated successfully": "Setările LLM au fost actualizate cu succes", + "Label": "Etichetă", + "Labels": "Etichete", + "Languages": "Limbi", + "Last": "Ultimul", + "Last Triggered": "Ultima declanșare", + "Leave empty to allow all users access": "Lăsați gol pentru a permite accesul tuturor utilizatorilor", + "Leave off for auto-detection from property format.": "Lăsați dezactivat pentru detectarea automată din formatul proprietății.", + "Left organisation successfully": "Ați părăsit organizația cu succes", + "Legal basis *": "Temei juridic *", + "Length Range:": "Interval de lungime:", + "Linear": "Liniar", + "Link": "Legătură", + "Link an email from the Mail app sidebar to associate it with this object.": "Legați un e-mail din bara laterală a aplicației Mail pentru a-l asocia cu acest obiect.", + "Link an object to see it here.": "Legați un obiect pentru a-l vedea aici.", + "Link another object": "Legați un alt obiect", + "Link existing event": "Legați un eveniment existent", + "Link removed": "Legătura a fost eliminată", + "Link to Object": "Legătură către obiect", + "Link to {name}": "Legătură către {name}", + "Linked Objects": "Obiecte legate", + "Linked by {user}": "Legat de {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "E-mailurile, evenimentele din calendar, contactele și cardurile Deck legate vor apărea aici.", + "Linked to {name}": "Legat de {name}", + "Load Advanced Filters": "Încărcați filtrele avansate", + "Load advanced filters with live data from your search index": "Încărcați filtrele avansate cu date live din indexul dumneavoastră de căutare", + "Load more": "Încărcați mai multe", + "Load view": "Încărcați vizualizarea", + "Loading API tokens...": "Se încarcă tokenurile API...", + "Loading ConfigSets...": "Se încarcă ConfigSets...", + "Loading LLM configuration...": "Se încarcă configurarea LLM...", + "Loading RBAC settings...": "Se încarcă setările RBAC...", + "Loading activity...": "Se încarcă activitatea...", + "Loading advanced filters...": "Se încarcă filtrele avansate...", + "Loading agents...": "Se încarcă agenții...", + "Loading application details...": "Se încarcă detaliile aplicației...", + "Loading applications...": "Se încarcă aplicațiile...", + "Loading archived conversations...": "Se încarcă conversațiile arhivate...", + "Loading audit trails...": "Se încarcă pistele de audit...", + "Loading available tools...": "Se încarcă instrumentele disponibile...", + "Loading cache statistics...": "Se încarcă statisticile cache...", + "Loading collections...": "Se încarcă colecțiile...", + "Loading configurations...": "Se încarcă configurările...", + "Loading conversation...": "Se încarcă conversația...", + "Loading conversations...": "Se încarcă conversațiile...", + "Loading deleted items...": "Se încarcă elementele șterse...", + "Loading endpoints": "Se încarcă punctele finale", + "Loading entities...": "Se încarcă entitățile...", + "Loading events...": "Se încarcă evenimentele...", + "Loading filter data...": "Se încarcă datele de filtrare...", + "Loading groups...": "Se încarcă grupurile...", + "Loading linked objects...": "Se încarcă obiectele legate...", + "Loading log details...": "Se încarcă detaliile jurnalului...", + "Loading multitenancy settings...": "Se încarcă setările de multitenancy...", + "Loading n8n configuration...": "Se încarcă configurarea n8n...", + "Loading permission matrix...": "Se încarcă matricea de permisiuni...", + "Loading preferences...": "Se încarcă preferințele...", + "Loading register data...": "Se încarcă datele registrului...", + "Loading registers...": "Se încarcă registrele...", + "Loading retention settings...": "Se încarcă setările de retenție...", + "Loading schemas...": "Se încarcă schemele...", + "Loading search configuration...": "Se încarcă configurarea căutării...", + "Loading search trails...": "Se încarcă pistele de căutare...", + "Loading sources...": "Se încarcă sursele...", + "Loading statistics...": "Se încarcă statisticile...", + "Loading text extraction settings...": "Se încarcă setările de extragere a textului...", + "Loading tokens...": "Se încarcă tokenurile...", + "Loading user groups...": "Se încarcă grupurile de utilizatori...", + "Loading users...": "Se încarcă utilizatorii...", + "Loading views...": "Se încarcă vizualizările...", + "Loading...": "Se încarcă...", + "Local": "Local", + "Local Version": "Versiune locală", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Localizați fiecare obiect care face referire la o persoană vizată (Art 15 inzage), previzualizați o ștergere (Art 17 vergetelheid) sau exportați datele acesteia (Art 20 portabiliteit).", + "Location": "Locație", + "Location Field": "Câmp de locație", + "Locations": "Locații", + "Locked": "Blocat", + "Locked:": "Blocat:", + "Login successful": "Autentificare reușită", + "Logs": "Jurnale", + "Low": "Scăzut", + "Low Confidence": "Încredere scăzută", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Numerele mai mici apar primele în bara laterală de filtrare. Lăsați gol pentru ordonare automată.", + "Lucene Version:": "Versiune Lucene:", + "Magic table sync completed for {schema}": "Sincronizarea tabelului magic a fost finalizată pentru {schema}", + "Magic table synchronized successfully": "Tabelul magic a fost sincronizat cu succes", + "Mail Integration": "Integrare Mail", + "Mail integration is not available": "Integrarea Mail nu este disponibilă", + "Make this installation discoverable within the federation network.": "Faceți această instalare descoperibilă în cadrul rețelei de federație.", + "Manage Organisation Roles": "Gestionați rolurile organizației", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Gestionați colecțiile SOLR (depozite de date) și atribuiți-le pentru obiecte și fișiere.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Gestionați ConfigSets SOLR (șabloane de configurare) pentru colecțiile dumneavoastră.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Gestionați colecțiile SOLR (depozite de date) și atribuiți-le pentru obiecte și fișiere.", + "Manage SOLR configsets (configuration templates) for your collections.": "Gestionați configsets SOLR (șabloane de configurare) pentru colecțiile dumneavoastră.", + "Manage and configure agents for automated tasks": "Gestionați și configurați agenții pentru sarcini automatizate", + "Manage and monitor file text extraction status": "Gestionați și monitorizați starea extragerii textului din fișiere", + "Manage and restore soft deleted items from your registers": "Gestionați și restaurați elementele șterse temporar din registrele dumneavoastră", + "Manage and view detected entities from files and objects": "Gestionați și vizualizați entitățile detectate din fișiere și obiecte", + "Manage document templates and themes": "Gestionați șabloanele și temele de documente", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Gestionați fluxurile de lucru n8n pentru automatizarea OpenRegister. Fluxurile de lucru vor fi stocate în proiectul configurat.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Gestionați activitățile de prelucrare, executați cereri de acces ale persoanelor vizate și auditați conformitatea în temeiul GDPR al UE / AVG olandez.", + "Manage webhooks for event-driven integrations": "Gestionați webhook-urile pentru integrări bazate pe evenimente", + "Manage your account settings, security, and personal data.": "Gestionați setările contului, securitatea și datele personale.", + "Manage your applications and modules": "Gestionați aplicațiile și modulele dumneavoastră", + "Manage your chat conversations": "Gestionați conversațiile dumneavoastră de chat", + "Manage your data registers and their configurations": "Gestionați registrele dumneavoastră de date și configurările acestora", + "Manage your data schemas and their properties": "Gestionați schemele dumneavoastră de date și proprietățile acestora", + "Manage your data sources and their configurations": "Gestionați sursele dumneavoastră de date și configurările acestora", + "Manage your organisations and switch between them": "Gestionați organizațiile dumneavoastră și comutați între ele", + "Manage your saved search configurations": "Gestionați configurările dumneavoastră de căutare salvate", + "Manage your system configurations and settings": "Gestionați configurările și setările dumneavoastră de sistem", + "Managed": "Gestionat", + "Mappings": "Mapări", + "Mass validation completed successfully": "Validarea în masă a fost finalizată cu succes", + "Mass validation failed: {error}": "Validarea în masă a eșuat: {error}", + "Max Files (0 = all)": "Maxim fișiere (0 = toate)", + "Max Length": "Lungime maximă", + "Max Length:": "Lungime maximă:", + "Max Objects (0 = all)": "Maxim obiecte (0 = toate)", + "Max Retries": "Maxim reîncercări", + "Max Shards/Node": "Maxim Shards/Nod", + "Max Tokens": "Maxim Tokenuri", + "Max execution time (ms)": "Timp maxim de execuție (ms)", + "Max ms": "Maxim ms", + "Max result count": "Număr maxim de rezultate", + "Max results": "Rezultate maxime", + "Maximum": "Maxim", + "Maximum File Size (MB)": "Dimensiune maximă a fișierului (MB)", + "Maximum Nesting Depth": "Adâncime maximă de imbricare", + "Maximum length": "Lungime maximă", + "Maximum length constraint is missing": "Constrângerea de lungime maximă lipsește", + "Maximum length is too restrictive": "Lungimea maximă este prea restrictivă", + "Maximum number of files to process. Set to 0 to process all files.": "Numărul maxim de fișiere de procesat. Setați la 0 pentru a procesa toate fișierele.", + "Maximum number of items": "Numărul maxim de elemente", + "Maximum number of objects to process. Set to 0 to process all objects.": "Numărul maxim de obiecte de procesat. Setați la 0 pentru a procesa toate obiectele.", + "Maximum number of retry attempts for failed deliveries": "Numărul maxim de încercări de reîncercare pentru livrările eșuate", + "Maximum time to wait before committing changes": "Timpul maxim de așteptare înainte de confirmarea modificărilor", + "Maximum tokens to generate": "Numărul maxim de tokenuri de generat", + "Maximum value": "Valoare maximă", + "Maximum value constraint is missing": "Constrângerea de valoare maximă lipsește", + "Maximum value is too restrictive": "Valoarea maximă este prea restrictivă", + "Medium": "Mediu", + "Medium Confidence": "Încredere medie", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Interogări medii: Căutări cu unele filtrări sau mai mulți parametri (de exemplu, intervale de date, registre/scheme specifice)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Interogări medii: căutări cu unele filtrări sau mai mulți parametri (de exemplu, intervale de date, registre/scheme specifice)", + "Member removed successfully": "Membrul a fost eliminat cu succes", + "Members": "Membri", + "Members ({count})": "Membri ({count})", + "Members of selected groups can access this view": "Membrii grupurilor selectate pot accesa această vizualizare", + "Members:": "Membri:", + "Memory prediction calculated successfully": "Predicția de memorie a fost calculată cu succes", + "Message content is required": "Conținutul mesajului este obligatoriu", + "Message does not belong to this conversation": "Mesajul nu aparține acestei conversații", + "Message not found": "Mesajul nu a fost găsit", + "Metadata": "Metadate", + "Metadata Filters": "Filtre de metadate", + "Method": "Metodă", + "Method*": "Metodă*", + "Method: {method}": "Metodă: {method}", + "Min Length": "Lungime minimă", + "Min execution time (ms)": "Timp minim de execuție (ms)", + "Min ms": "Minim ms", + "Min result count": "Număr minim de rezultate", + "Min results": "Rezultate minime", + "Minimum": "Minim", + "Minimum length": "Lungime minimă", + "Minimum number of items": "Numărul minim de elemente", + "Minimum value": "Valoare minimă", + "Minimum value constraint is missing": "Constrângerea de valoare minimă lipsește", + "Minimum value is too restrictive": "Valoarea minimă este prea restrictivă", + "Missing conversation or agentUuid": "Lipsește conversația sau agentUuid", + "Missing conversationId": "Lipsește conversationId", + "Missing message": "Lipsește mesajul", + "Missing object fields created successfully": "Câmpurile lipsă ale obiectului au fost create cu succes", + "Mode:": "Mod:", + "Model": "Model", + "Model Name": "Nume model", + "Modified": "Modificat", + "Monthly": "Lunar", + "More sources (10-20):": "Mai multe surse (10-20):", + "Most Active Objects": "Cele mai active obiecte", + "Multiple Of": "Multiplu de", + "Multiple of": "Multiplu de", + "Multiple search trail deletion not implemented yet": "Ștergerea mai multor piste de căutare nu este încă implementată", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Setările de multitenancy au fost actualizate cu succes", + "My Account": "Contul meu", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Nume", + "Name *": "Nume *", + "Name*": "Nume*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Memoria cache a numelor a fost încălzită cu succes: {count} nume încărcate în {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Memoria cache a numelor a fost încălzită cu succes: {count} nume încărcate în {time}. {message}", + "Names cache warmup completed": "Încălzirea memoriei cache a numelor a fost finalizată", + "Naming pattern for copies:": "Model de denumire pentru copii:", + "New Collection Name": "Nume colecție nouă", + "New Configuration": "Configurare nouă", + "New Conversation": "Conversație nouă", + "New Properties": "Proprietăți noi", + "New Property": "Proprietate nouă", + "New User Group": "Grup de utilizatori nou", + "New Value": "Valoare nouă", + "New activity": "Activitate nouă", + "New password": "Parolă nouă", + "New processing activity": "Activitate de prelucrare nouă", + "New users without specific organisation membership will be automatically added to this organisation": "Utilizatorii noi fără apartenență specifică la o organizație vor fi adăugați automat la această organizație", + "New verwerkingsactiviteit": "New verwerkingsactiviteit", + "Next": "Următorul", + "Nextcloud Groups": "Grupuri Nextcloud", + "No": "Nu", + "No API tokens.": "Niciun token API.", + "No ConfigSets found": "Nu s-au găsit ConfigSets", + "No Deck cards linked to this object": "Niciun card Deck legat de acest obiect", + "No GitHub token configured": "Niciun token GitHub configurat", + "No GitHub token provided": "Niciun token GitHub furnizat", + "No GitLab token provided": "Niciun token GitLab furnizat", + "No Organisation": "Nicio organizație", + "No active filters": "Niciun filtru activ", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Niciun obiect activ de șters temporar pentru schema {schema}. Utilizați „Ștergeți permanent” pentru a elimina obiectele șterse temporar.", + "No activity data available": "Nu sunt disponibile date de activitate", + "No activity found.": "Nu s-a găsit nicio activitate.", + "No agents available": "Niciun agent disponibil", + "No agents found": "Nu s-au găsit agenți", + "No app store cache exists yet": "Nu există încă o memorie cache pentru magazinul de aplicații", + "No applications are available.": "Nu este disponibilă nicio aplicație.", + "No applications found": "Nu s-au găsit aplicații", + "No approval steps for this object.": "Niciun pas de aprobare pentru acest obiect.", + "No archived conversations": "Nicio conversație arhivată", + "No audit trail entries found": "Nu s-au găsit înregistrări în pista de audit", + "No changes recorded": "Nicio modificare înregistrată", + "No chunks to vectorize": "Niciun fragment de vectorizat", + "No collections found": "Nu s-au găsit colecții", + "No configuration data": "Nicio dată de configurare", + "No configuration selected": "Nicio configurare selectată", + "No configurations are available.": "Nu este disponibilă nicio configurare.", + "No configurations found": "Nu s-au găsit configurări", + "No configurations found for this application.": "Nu s-au găsit configurări pentru această aplicație.", + "No contacts linked to this object": "Niciun contact legat de acest obiect", + "No conversations yet. Create a new one to get started!": "Încă nicio conversație. Creați una nouă pentru a începe!", + "No dashboards yet": "Încă niciun tablou de bord", + "No data available": "Nu sunt disponibile date", + "No data available for chart": "Nu sunt disponibile date pentru grafic", + "No deleted items found": "Nu s-au găsit elemente șterse", + "No deletion data available": "Nu sunt disponibile date de ștergere", + "No description found": "Nu s-a găsit nicio descriere", + "No description provided": "Nu a fost furnizată nicio descriere", + "No emails linked to this object": "Niciun e-mail legat de acest obiect", + "No endpoints defined": "Niciun punct final definit", + "No entities detected for this email.": "Nicio entitate detectată pentru acest e-mail.", + "No entities detected in this file. Run text extraction first.": "Nicio entitate detectată în acest fișier. Executați mai întâi extragerea textului.", + "No entities found": "Nu s-au găsit entități", + "No entities have been detected yet": "Nu a fost detectată încă nicio entitate", + "No events found": "Nu s-au găsit evenimente", + "No events linked to this object": "Niciun eveniment legat de acest obiect", + "No expired audit trails found to clear": "Nu s-au găsit piste de audit expirate de șters", + "No expired search trails found to clear": "Nu s-au găsit piste de căutare expirate de șters", + "No extraction data available for this file": "Nu sunt disponibile date de extragere pentru acest fișier", + "No facetable fields available. Select a register and schema to see available filters.": "Niciun câmp facetabil disponibil. Selectați un registru și o schemă pentru a vedea filtrele disponibile.", + "No files found": "Nu s-au găsit fișiere", + "No files have been extracted yet": "Nu a fost extras încă niciun fișier", + "No files selected": "Niciun fișier selectat", + "No files to process": "Niciun fișier de procesat", + "No files to reindex": "Niciun fișier de reindexat", + "No filters are currently active. This will delete ALL audit trail entries!": "Niciun filtru nu este activ în prezent. Aceasta va șterge TOATE înregistrările din pista de audit!", + "No groups found. Try a different search.": "Nu s-au găsit grupuri. Încercați o căutare diferită.", + "No label": "Nicio etichetă", + "No labels": "Nicio etichetă", + "No linked objects": "Niciun obiect legat", + "No log entries found": "Nu s-au găsit înregistrări în jurnal", + "No logs are available for this configuration.": "Nu sunt disponibile jurnale pentru această configurare.", + "No logs are available for this source.": "Nu sunt disponibile jurnale pentru această sursă.", + "No logs found": "Nu s-au găsit jurnale", + "No matches": "Nicio potrivire", + "No mismatched fields found - SOLR schema is properly configured": "Nu s-au găsit câmpuri nepotrivite - schema SOLR este configurată corect", + "No objects are currently selected for deletion.": "Niciun obiect nu este selectat în prezent pentru ștergere.", + "No objects are currently selected for permanent deletion.": "Niciun obiect nu este selectat în prezent pentru ștergere permanentă.", + "No objects are currently selected for restoration.": "Niciun obiect nu este selectat în prezent pentru restaurare.", + "No objects found": "Nu s-au găsit obiecte", + "No objects found to vectorize": "Nu s-au găsit obiecte de vectorizat", + "No objects linked to this email": "Niciun obiect legat de acest e-mail", + "No objects selected": "Niciun obiect selectat", + "No objects selected for deletion": "Niciun obiect selectat pentru ștergere", + "No objects selected for restoration": "Niciun obiect selectat pentru restaurare", + "No objects to delete": "Niciun obiect de șters", + "No objects to delete for schema {schema}": "Niciun obiect de șters pentru schema {schema}", + "No objects were permanently deleted": "Niciun obiect nu a fost șters permanent", + "No organisations found": "Nu s-au găsit organizații", + "No personal data was found for this subject identifier.": "Nu s-au găsit date personale pentru acest identificator de persoană vizată.", + "No processing activities yet": "Încă nicio activitate de prelucrare", + "No properties found": "Nu s-au găsit proprietăți", + "No properties match your filters.": "Nicio proprietate nu corespunde filtrelor dumneavoastră.", + "No purge date set": "Nicio dată de epurare setată", + "No register data available": "Nu sunt disponibile date de registru", + "No register objects reference this file": "Niciun obiect de registru nu face referire la acest fișier", + "No registers found": "Nu s-au găsit registre", + "No registers found for this application.": "Nu s-au găsit registre pentru această aplicație.", + "No registers found. Create a register to configure permissions.": "Nu s-au găsit registre. Creați un registru pentru a configura permisiunile.", + "No related cases found for this sender": "Nu s-au găsit cazuri conexe pentru acest expeditor", + "No related entities": "Nicio entitate conexă", + "No relations found": "Nu s-au găsit relații", + "No request body available": "Niciun corp de cerere disponibil", + "No response body available": "Niciun corp de răspuns disponibil", + "No saved views yet. Create one in the Search tab!": "Încă nicio vizualizare salvată. Creați una în fila Căutare!", + "No saved views yet. create one in the search tab!": "Încă nicio vizualizare salvată. creați una în fila de căutare!", + "No scan run yet": "Nicio scanare executată încă", + "No schema data available": "Nu sunt disponibile date de schemă", + "No schema selected for exploration": "Nicio schemă selectată pentru explorare", + "No schemas configured for mail linking.": "Nicio schemă configurată pentru legarea e-mailurilor.", + "No schemas found": "Nu s-au găsit scheme", + "No schemas found for this application.": "Nu s-au găsit scheme pentru această aplicație.", + "No search terms data available": "Nu sunt disponibile date despre termenii de căutare", + "No search trail entries found": "Nu s-au găsit înregistrări în pista de căutare", + "No sources are available.": "Nu este disponibilă nicio sursă.", + "No sources found": "Nu s-au găsit surse", + "No synchronizations found": "Nu s-au găsit sincronizări", + "No templates found": "Nu s-au găsit șabloane", + "No templates have been created yet": "Nu a fost creat încă niciun șablon", + "No tools available. Tools can be registered by installed apps.": "Niciun instrument disponibil. Instrumentele pot fi înregistrate de aplicațiile instalate.", + "No users in this organisation.": "Niciun utilizator în această organizație.", + "No valid magic-mapped register+schema combinations found": "Nu s-au găsit combinații valide de registru+schemă mapate magic", + "No views found. Create views first before configuring vectorization.": "Nu s-au găsit vizualizări. Creați mai întâi vizualizări înainte de a configura vectorizarea.", + "No views match your search": "Nicio vizualizare nu corespunde căutării dumneavoastră", + "No webhooks found": "Nu s-au găsit webhook-uri", + "No webhooks have been configured yet": "Nu a fost configurat încă niciun webhook", + "No workflows found in this project. Create workflows in the n8n editor.": "Nu s-au găsit fluxuri de lucru în acest proiect. Creați fluxuri de lucru în editorul n8n.", + "None": "Niciunul", + "Not Found": "Nu a fost găsit", + "Not extracted": "Neextras", + "Not helpful": "Neutil", + "Not in use": "Neutilizat", + "Note:": "Notă:", + "Notifications": "Notificări", + "Number Constraints": "Constrângeri numerice", + "Number of Sources": "Număr de surse", + "Number of chunks to vectorize in one API call (1-100).": "Numărul de fragmente de vectorizat într-un singur apel API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Numărul de fragmente de vectorizat într-un singur apel API. Mai mare = mai rapid, dar mai multă memorie. Recomandat: 10-50.", + "Number of objects to process in each batch (1-5000).": "Numărul de obiecte de procesat în fiecare lot (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Numărul de obiecte de vectorizat într-un singur apel API. Mai mare = mai rapid, dar mai multă memorie. Recomandat: 10-50.", + "Numeric Range:": "Interval numeric:", + "Object": "Obiect", + "Object #{id}": "Obiect #{id}", + "Object Collection": "Colecție de obiecte", + "Object Configuration:": "Configurare obiect:", + "Object Count Prediction": "Predicția numărului de obiecte", + "Object Extraction Mode": "Mod de extragere a obiectelor", + "Object ID": "ID obiect", + "Object Management": "Gestionarea obiectelor", + "Object Text Extraction": "Extragerea textului din obiect", + "Object Vectorization": "Vectorizarea obiectelor", + "Object Warmup": "Încălzirea obiectelor", + "Object changes in owned objects": "Modificări ale obiectelor în obiectele deținute", + "Object collection not configured": "Colecția de obiecte nu este configurată", + "Object metadata incomplete for file upload": "Metadatele obiectului sunt incomplete pentru încărcarea fișierului", + "Object not found": "Obiectul nu a fost găsit", + "Object permanently deleted": "Obiectul a fost șters permanent", + "Object restored successfully": "Obiectul a fost restaurat cu succes", + "Object settings updated successfully": "Setările obiectului au fost actualizate cu succes", + "Object sources": "Sursele obiectului", + "Object successfully deleted": "Obiectul a fost șters cu succes", + "Object successfully modified": "Obiectul a fost modificat cu succes", + "Object unlocked successfully": "Obiectul a fost deblocat cu succes", + "Object vectorization configuration saved successfully": "Configurarea vectorizării obiectelor a fost salvată cu succes", + "Object vectorized successfully": "Obiectul a fost vectorizat cu succes", + "Object:": "Obiect:", + "Objects": "Obiecte", + "Objects Analyzed": "Obiecte analizate", + "Objects Distribution": "Distribuția obiectelor", + "Objects being analyzed": "Obiecte în curs de analiză", + "Objects by Register": "Obiecte după registru", + "Objects by Schema": "Obiecte după schemă", + "Objects deletion completed for {schema}": "Ștergerea obiectelor a fost finalizată pentru {schema}", + "Objects deletion completed successfully": "Ștergerea obiectelor a fost finalizată cu succes", + "Objects in schema": "Obiecte în schemă", + "Objects successfully deleted": "Obiectele au fost șterse cu succes", + "Objects to Process:": "Obiecte de procesat:", + "Objects to be analyzed": "Obiecte de analizat", + "Objects to be deleted": "Obiecte de șters", + "Objects to be validated": "Obiecte de validat", + "Objects will be serialized as JSON text before vectorization": "Obiectele vor fi serializate ca text JSON înainte de vectorizare", + "Objects will be soft deleted and moved to the": "Obiectele vor fi șterse temporar și mutate în", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Obiectele vor fi șterse temporar (marcate ca șterse, dar păstrate în baza de date). Acestea pot fi recuperate ulterior dacă este necesar.", + "Old Value": "Valoare veche", + "Oldest Item (days)": "Cel mai vechi element (zile)", + "Ollama Chat Settings": "Setări de chat Ollama", + "Ollama Embedding Configuration": "Configurare embedding Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Numai cu modificări", + "Only members of selected groups can access this organisation": "Numai membrii grupurilor selectate pot accesa această organizație", + "Open Nextcloud App Store": "Deschideți Nextcloud App Store", + "Open OpenRegister sidebar": "Deschideți bara laterală OpenRegister", + "Open Register Objects": "Deschideți obiectele registrului", + "Open folder": "Deschideți dosarul", + "Open in OpenRegister": "Deschideți în OpenRegister", + "Open n8n Editor": "Deschideți editorul n8n", + "OpenAI Chat Settings": "Setări de chat OpenAI", + "OpenAI Embedding Configuration": "Configurare embedding OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Setări OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Tablouri de bord definite de operator și rapoarte programate. Fiecare tablou de bord este un obiect de primă clasă în registrul `reports`; widget-urile sunt declarate în matricea `widgets` a tabloului de bord și sunt randate live din agregări / GraphQL.", + "Optimizing search performance...": "Se optimizează performanța căutării...", + "Optional URL-friendly identifier": "Identificator opțional prietenos cu URL-ul", + "Optional description for this facet": "Descriere opțională pentru această fațetă", + "Optional end date property": "Proprietate opțională de dată de sfârșit", + "Optional event description template": "Șablon opțional de descriere a evenimentului", + "Optional location property": "Proprietate opțională de locație", + "Optional webhook secret for signature verification": "Secret opțional de webhook pentru verificarea semnăturii", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Opțional: Adăugați parametri de interogare pentru a filtra schema referită (de exemplu, status=active&type=public)", + "Or": "Sau", + "Order": "Ordine", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Etichete de limbă BCP 47 ordonate. Prima limbă este implicită pentru registru și determină rezerva Accept-Language pentru proprietățile traductibile.", + "Organisation": "Organizație", + "Organisation Configuration": "Configurare organizație", + "Organisation ID": "ID organizație", + "Organisation Members": "Membrii organizației", + "Organisation Statistics": "Statistici organizație", + "Organisation created successfully": "Organizația a fost creată cu succes", + "Organisation membership changes": "Modificări ale apartenenței la organizație", + "Organisation name is required": "Numele organizației este obligatoriu", + "Organisation settings saved successfully": "Setările organizației au fost salvate cu succes", + "Organisation successfully created": "Organizația a fost creată cu succes", + "Organisation successfully updated": "Organizația a fost actualizată cu succes", + "Organisational measures": "Măsuri organizaționale", + "Organisations": "Organizații", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organizație", + "Organization ID (Optional)": "ID organizație (opțional)", + "Organizations": "Organizații", + "Orphaned Items": "Elemente orfane", + "Other": "Altul", + "Output Mapping": "Mapare de ieșire", + "Overview": "Prezentare generală", + "Owner": "Proprietar", + "Owner:": "Proprietar:", + "PDF": "PDF", + "PII hits": "Potriviri PII", + "PII rows": "Rânduri PII", + "Page {current} of {total}": "Pagina {current} din {total}", + "Parallel Mode (Faster, more resource intensive)": "Mod paralel (mai rapid, mai intensiv în resurse)", + "Parallel:": "Paralel:", + "Password": "Parolă", + "Password changes are not supported by your authentication provider.": "Modificările parolei nu sunt acceptate de furnizorul dumneavoastră de autentificare.", + "Password for SOLR authentication (optional)": "Parolă pentru autentificarea SOLR (opțional)", + "Password updated successfully": "Parola a fost actualizată cu succes", + "Path": "Cale", + "Path in repository": "Cale în depozit", + "Path where the configuration file will be saved in the repository": "Calea unde fișierul de configurare va fi salvat în depozit", + "Path where the register OAS file will be saved in the repository": "Calea unde fișierul OAS al registrului va fi salvat în depozit", + "Pattern (regex)": "Model (regex)", + "Pattern Issue": "Problemă de model", + "Pattern constraint is missing": "Constrângerea de model lipsește", + "Patterns:": "Modele:", + "Pending": "În așteptare", + "Permanent Delete Mode": "Mod de ștergere permanentă", + "Permanently Delete": "Ștergeți permanent", + "Permanently Delete ({count})": "Ștergeți permanent ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Ștergeți permanent toate cele {count} obiecte șterse temporar. Această acțiune nu poate fi anulată!", + "Permanently delete already soft-deleted objects": "Ștergeți permanent obiectele deja șterse temporar", + "Permanently delete this conversation?": "Ștergeți permanent această conversație?", + "Permission Matrix": "Matrice de permisiuni", + "Person": "Persoană", + "Personal Data": "Date personale", + "Personal Data Export": "Export de date personale", + "Persons": "Persoane", + "Phone": "Telefon", + "Phone Numbers": "Numere de telefon", + "Photos": "Fotografii", + "Please create an agent in the": "Vă rugăm să creați un agent în", + "Please select a user": "Vă rugăm să selectați un utilizator", + "Please select an agent to continue": "Vă rugăm să selectați un agent pentru a continua", + "Please select an organisation": "Vă rugăm să selectați o organizație", + "Please select at least one item to import": "Vă rugăm să selectați cel puțin un element pentru import", + "Please select registers or schemas first": "Vă rugăm să selectați mai întâi registre sau scheme", + "Please select which register and schema to use for the new object": "Vă rugăm să selectați ce registru și ce schemă să fie utilizate pentru noul obiect", + "Please try again later.": "Vă rugăm să încercați din nou mai târziu.", + "Please wait while we fetch your agents.": "Vă rugăm să așteptați în timp ce vă preluăm agenții.", + "Please wait while we fetch your applications.": "Vă rugăm să așteptați în timp ce vă preluăm aplicațiile.", + "Please wait while we fetch your configurations.": "Vă rugăm să așteptați în timp ce vă preluăm configurațiile.", + "Please wait while we fetch your deleted items.": "Vă rugăm să așteptați în timp ce vă preluăm elementele șterse.", + "Please wait while we fetch your sources.": "Vă rugăm să așteptați în timp ce vă preluăm sursele.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Vă rugăm să așteptați în timp ce ștergem definitiv colecția SOLR. Aceasta poate dura câteva momente.", + "Polls": "Sondaje", + "Popular Search Terms": "Termeni de căutare populari", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Preferințe salvate", + "Prerequisites": "Cerințe prealabile", + "Press Enter to send, Shift+Enter for new line": "Apăsați Enter pentru a trimite, Shift+Enter pentru un rând nou", + "Press enter to send, shift+enter for new line": "Apăsați enter pentru a trimite, shift+enter pentru un rând nou", + "Prettify": "Formatare", + "Preview erasure": "Previzualizare ștergere", + "Preview:": "Previzualizare:", + "Previous": "Anterior", + "Private": "Privat", + "Private Agent (Default)": "Agent privat (Implicit)", + "Private agents": "Agenți privați", + "Process Name (optional)": "Nume proces (opțional)", + "Processed {indexed} files, {failed} failed": "S-au procesat {indexed} fișiere, {failed} eșuate", + "Processes chunks in batches with simulated parallelism.": "Procesează fragmente în loturi cu paralelism simulat.", + "Processes file chunks sequentially (safest).": "Procesează fragmentele de fișier secvențial (cel mai sigur).", + "Processes objects in chunks with simulated parallelism.": "Procesează obiectele în fragmente cu paralelism simulat.", + "Processes objects sequentially (safest).": "Procesează obiectele secvențial (cel mai sigur).", + "Processing": "Se procesează", + "Processing Limits": "Limite de procesare", + "Processing...": "Se procesează...", + "Project initialized successfully": "Proiect inițializat cu succes", + "Project not found. Please initialize first.": "Proiectul nu a fost găsit. Vă rugăm să îl inițializați mai întâi.", + "Properties": "Proprietăți", + "Property Behaviors": "Comportamente ale proprietății", + "Property Configuration:": "Configurarea proprietății:", + "Property Filters": "Filtre de proprietate", + "Property Title": "Titlul proprietății", + "Property Type": "Tipul proprietății", + "Property can be improved": "Proprietatea poate fi îmbunătățită", + "Property could not be added": "Proprietatea nu a putut fi adăugată", + "Property could not be updated": "Proprietatea nu a putut fi actualizată", + "Property name of inversed relation": "Numele proprietății relației inversate", + "Property successfully added": "Proprietate adăugată cu succes", + "Property successfully updated": "Proprietate actualizată cu succes", + "Protected": "Protejat", + "Provider": "Furnizor", + "Provider is required for testing": "Furnizorul este necesar pentru testare", + "Public": "Public", + "Public View": "Vizualizare publică", + "Public views can be accessed by anyone in the system": "Vizualizările publice pot fi accesate de oricine din sistem", + "Publish": "Publicare", + "Publish Configuration to GitHub": "Publicați configurația pe GitHub", + "Publish OAS": "Publicați OAS", + "Publish Objects": "Publicați obiecte", + "Publish Register OAS to GitHub": "Publicați OAS-ul registrului pe GitHub", + "Publish all objects in this schema": "Publicați toate obiectele din această schemă", + "Publish objects to make them publicly available": "Publicați obiecte pentru a le face disponibile public", + "Published": "Publicat", + "Published:": "Publicat:", + "Publishing...": "Se publică...", + "Purge": "Epurare", + "Purge Date": "Data epurării", + "Purge {count} object from database": "Epurați {count} obiect din baza de date", + "Purge {count} objects from database": "Epurați {count} obiecte din baza de date", + "Purpose limitation *": "Limitarea scopului *", + "Push Notifications": "Notificări push", + "Query Complexity Distribution": "Distribuția complexității interogărilor", + "Query Help": "Ajutor pentru interogări", + "Query parameter is required": "Parametrul de interogare este obligatoriu", + "Quota": "Cotă", + "RAG": "RAG", + "RAG Configuration": "Configurare RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Setările RBAC au fost actualizate cu succes", + "Rate limits cleared successfully": "Limitele de rată au fost șterse cu succes", + "Raw Changes Data": "Date brute ale modificărilor", + "Re-vectorize on object update": "Re-vectorizare la actualizarea obiectului", + "Read": "Citire", + "Real-time push notification status via notify_push": "Starea notificărilor push în timp real prin notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Notificările push în timp real sunt active. Clienții conectați primesc actualizări instantanee atunci când obiectele sunt create, actualizate sau șterse.", + "Realtime push active": "Push în timp real activ", + "Realtime push not available — the notify_push app is not installed": "Push-ul în timp real nu este disponibil — aplicația notify_push nu este instalată", + "Reason": "Motiv", + "Reason (optional)": "Motiv (opțional)", + "Rebase operation completed successfully": "Operațiunea de rebazare s-a finalizat cu succes", + "Rebase operation failed: {error}": "Operațiunea de rebazare a eșuat: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Recomandări:", + "Recommended Type:": "Tip recomandat:", + "Recommended: 5 sources": "Recomandat: 5 surse", + "Refresh": "Reîmprospătare", + "Refresh Data": "Reîmprospătați datele", + "Refresh Stats": "Reîmprospătați statisticile", + "Refresh Workflows": "Reîmprospătați fluxurile de lucru", + "Refresh dashboard": "Reîmprospătați panoul de control", + "Refresh database info": "Reîmprospătați informațiile despre baza de date", + "Register": "Registru", + "Register #{id}": "Registru #{id}", + "Register / Schema": "Registru / Schemă", + "Register ID": "ID registru", + "Register Objects": "Obiectele registrului", + "Register Overview": "Prezentare generală a registrului", + "Register Statistics": "Statistici ale registrului", + "Register Totals": "Totaluri ale registrului", + "Register and schema are required": "Registrul și schema sunt obligatorii", + "Register annotation": "Adnotare registru", + "Register depublished successfully": "Registru depublicat cu succes", + "Register not found": "Registrul nu a fost găsit", + "Register objects": "Obiectele registrului", + "Register objects deletion completed successfully": "Ștergerea obiectelor registrului s-a finalizat cu succes", + "Register published successfully": "Registru publicat cu succes", + "Register/Schema Usage": "Utilizarea registrului/schemei", + "Register:": "Registru:", + "Registers": "Registre", + "Reindex": "Reindexare", + "Reindex completed": "Reindexare finalizată", + "Reindex completed successfully": "Reindexare finalizată cu succes", + "Reindex completed! Processed {count} objects in {duration}s": "Reindexare finalizată! S-au procesat {count} obiecte în {duration}s", + "Reindex failed": "Reindexarea a eșuat", + "Reindex failed: %s": "Reindexarea a eșuat: %s", + "Reject": "Respingere", + "Rejected": "Respins", + "Related Cases": "Cazuri conexe", + "Relations": "Relații", + "Remote Version": "Versiune la distanță", + "Remove": "Eliminare", + "Remove Deck card": "Eliminați cardul Deck", + "Remove avatar": "Eliminați avatarul", + "Remove contact": "Eliminați contactul", + "Remove filter": "Eliminați filtrul", + "Remove from favorites": "Eliminați din favorite", + "Remove from list": "Eliminați din listă", + "Remove group": "Eliminați grupul", + "Remove link": "Eliminați legătura", + "Remove link to {name}": "Eliminați legătura către {name}", + "Remove link to {name}?": "Eliminați legătura către {name}?", + "Remove link to {title}": "Eliminați legătura către {title}", + "Remove oneOf entry": "Eliminați intrarea oneOf", + "Remove property after write-back": "Eliminați proprietatea după rescriere", + "Remove {title}": "Eliminați {title}", + "Remove {userId} from this organisation?": "Eliminați {userId} din această organizație?", + "Removed": "Eliminat", + "Removed from favorites": "Eliminat din favorite", + "Removing schema {schema}...": "Se elimină schema {schema}...", + "Rename Conversation": "Redenumiți conversația", + "Rename conversation": "Redenumiți conversația", + "Replicas": "Replici", + "Reports": "Rapoarte", + "Repository": "Depozit", + "Request Body": "Corpul cererii", + "Request Data": "Datele cererii", + "Request Details": "Detaliile cererii", + "Request Quota (per day)": "Cota de cereri (pe zi)", + "Request account deactivation": "Solicitați dezactivarea contului", + "Request account deactivation. This will notify administrators for review.": "Solicitați dezactivarea contului. Aceasta va notifica administratorii pentru revizuire.", + "Request body must be JSON with a \"query\" field": "Corpul cererii trebuie să fie JSON cu un câmp \"query\"", + "Request timeout in seconds": "Timpul de expirare a cererii în secunde", + "Requested at": "Solicitat la", + "Required": "Obligatoriu", + "Required Token Scopes": "Domenii de aplicare ale tokenului necesare", + "Required field": "Câmp obligatoriu", + "Required status is inconsistent": "Starea obligatorie este inconsecventă", + "Requires Dolphin AI for OCR text extraction": "Necesită Dolphin AI pentru extragerea textului OCR", + "Rerun Search": "Reexecutați căutarea", + "Rerunning search: {searchTerm}": "Se reexecută căutarea: {searchTerm}", + "Reset Changes": "Resetați modificările", + "Reset Filters": "Resetați filtrele", + "Resource Quotas": "Cote de resurse", + "Response Body": "Corpul răspunsului", + "Response Details": "Detaliile răspunsului", + "Response Time:": "Timp de răspuns:", + "Restore": "Restaurare", + "Restore conversation": "Restaurați conversația", + "Restore or permanently delete items": "Restaurați sau ștergeți definitiv elemente", + "Restore {count} object": "Restaurați {count} obiect", + "Restore {count} objects": "Restaurați {count} obiecte", + "Result Count Range": "Intervalul numărului de rezultate", + "Results": "Rezultate", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Perioada de retenție (durată ISO-8601, de ex. P10Y, P30D)", + "Retention settings updated successfully": "Setările de retenție au fost actualizate cu succes", + "Retrieve all objects for this schema": "Preluați toate obiectele pentru această schemă", + "Retry": "Reîncercare", + "Retry Failed Extractions": "Reîncercați extragerile eșuate", + "Retry Policy": "Politică de reîncercare", + "Retry completed": "Reîncercare finalizată", + "Retry upload": "Reîncercați încărcarea", + "Retrying...": "Se reîncearcă...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Examinați obiectele selectate de mai jos. Puteți elimina orice obiect pe care nu doriți să îl ștergeți făcând clic pe butonul de eliminare.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Examinați obiectele selectate de mai jos. Puteți elimina orice obiect pe care nu doriți să îl includeți.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Examinați obiectele selectate de mai jos. Puteți elimina orice obiect pe care nu doriți să îl ștergeți definitiv făcând clic pe butonul de eliminare. Această acțiune nu poate fi anulată.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Examinați obiectele selectate de mai jos. Puteți elimina orice obiect pe care nu doriți să îl restaurați făcând clic pe butonul de eliminare. Obiectele vor fi restaurate în locația lor originală.", + "Revoke": "Revocare", + "Right": "Dreapta", + "Risk Level": "Nivel de risc", + "Risk level": "Nivel de risc", + "Roles and Permissions": "Roluri și permisiuni", + "Roles updated successfully": "Roluri actualizate cu succes", + "Run compliance scan": "Executați scanarea de conformitate", + "SOLR Connection Settings": "Setări de conexiune SOLR", + "SOLR Index Warmup": "Preîncălzirea indexului SOLR", + "SOLR Version:": "Versiune SOLR:", + "SOLR actions menu": "Meniu de acțiuni SOLR", + "SOLR base path (usually /solr)": "Calea de bază SOLR (de obicei /solr)", + "SOLR collection deleted successfully": "Colecția SOLR a fost ștearsă cu succes", + "SOLR connection test failed: {error}": "Testul de conexiune SOLR a eșuat: {error}", + "SOLR connection test successful!": "Testul de conexiune SOLR a reușit!", + "SOLR index warmup completed successfully": "Preîncălzirea indexului SOLR s-a finalizat cu succes", + "SOLR is disabled": "SOLR este dezactivat", + "SOLR is not available or not configured": "SOLR nu este disponibil sau nu este configurat", + "SOLR logging disabled": "Înregistrarea SOLR dezactivată", + "SOLR logging enabled": "Înregistrarea SOLR activată", + "SOLR search disabled": "Căutarea SOLR dezactivată", + "SOLR search enabled": "Căutarea SOLR activată", + "SOLR server hostname or IP address": "Numele de gazdă sau adresa IP a serverului SOLR", + "SOLR server port number (optional, defaults to 8983)": "Numărul portului serverului SOLR (opțional, implicit 8983)", + "SOLR settings updated successfully": "Setările SOLR au fost actualizate cu succes", + "SOLR setup completed successfully": "Configurarea SOLR s-a finalizat cu succes", + "SOLR setup completed successfully!": "Configurarea SOLR s-a finalizat cu succes!", + "SOLR setup error: %s": "Eroare la configurarea SOLR: %s", + "SOLR setup failed": "Configurarea SOLR a eșuat", + "SOLR setup failed - check logs": "Configurarea SOLR a eșuat - verificați jurnalele", + "SOLR setup failed. Please check the configuration and try again.": "Configurarea SOLR a eșuat. Vă rugăm să verificați configurația și să încercați din nou.", + "SOLR setup failed: {error}": "Configurarea SOLR a eșuat: {error}", + "SOLR warmup failed: {error}": "Preîncălzirea SOLR a eșuat: {error}", + "Sample Values:": "Valori de exemplu:", + "Save": "Salvare", + "Save Configuration": "Salvați configurația", + "Save Connection Settings": "Salvați setările de conexiune", + "Save Roles": "Salvați rolurile", + "Save Settings": "Salvați setările", + "Save changes": "Salvați modificările", + "Save current search as view": "Salvați căutarea curentă ca vizualizare", + "Save labels": "Salvați etichetele", + "Save tags for {name}": "Salvați etichetele pentru {name}", + "Save the organisation first to manage users.": "Salvați mai întâi organizația pentru a gestiona utilizatorii.", + "Saved Views": "Vizualizări salvate", + "Saving...": "Se salvează...", + "Scheduled": "Programat", + "Schema": "Schemă", + "Schema ID": "ID schemă", + "Schema Name": "Numele schemei", + "Schema Statistics": "Statistici ale schemei", + "Schema annotation": "Adnotare schemă", + "Schema configuration updated successfully for {schema}": "Configurația schemei a fost actualizată cu succes pentru {schema}", + "Schema depublished successfully": "Schemă depublicată cu succes", + "Schema objects deletion completed successfully": "Ștergerea obiectelor schemei s-a finalizat cu succes", + "Schema published successfully": "Schemă publicată cu succes", + "Schema reference ($ref)": "Referință de schemă ($ref)", + "Schema successfully created": "Schemă creată cu succes", + "Schema successfully updated": "Schemă actualizată cu succes", + "Schema {schema} removed successfully": "Schema {schema} a fost eliminată cu succes", + "Schema:": "Schemă:", + "Schemas": "Scheme", + "Scheme": "Schemă", + "Search": "Căutare", + "Search / Views": "Căutare / Vizualizări", + "Search / views": "Căutare / vizualizări", + "Search Activity": "Activitate de căutare", + "Search Analytics": "Analize de căutare", + "Search Configuration": "Configurare căutare", + "Search Entities": "Căutați entități", + "Search Files": "Căutați fișiere", + "Search GitHub": "Căutați pe GitHub", + "Search GitLab": "Căutați pe GitLab", + "Search Method": "Metodă de căutare", + "Search Mode": "Mod de căutare", + "Search Query": "Interogare de căutare", + "Search Results": "Rezultatele căutării", + "Search Term": "Termen de căutare", + "Search Trail Details": "Detaliile urmei de căutare", + "Search Trail Management": "Gestionarea urmelor de căutare", + "Search Trail Statistics": "Statistici ale urmelor de căutare", + "Search Trails": "Urme de căutare", + "Search Views": "Vizualizări de căutare", + "Search Webhooks": "Căutați webhook-uri", + "Search agents...": "Căutați agenți...", + "Search applications...": "Căutați aplicații...", + "Search by file name or path": "Căutați după numele sau calea fișierului", + "Search by name or URL": "Căutați după nume sau URL", + "Search by title or UUID...": "Căutați după titlu sau UUID...", + "Search by value": "Căutați după valoare", + "Search configurations": "Căutați configurații", + "Search data sources...": "Căutați surse de date...", + "Search groups...": "Căutați grupuri...", + "Search in Database Objects": "Căutați în obiectele bazei de date", + "Search in Files": "Căutați în fișiere", + "Search in Objects": "Căutați în obiecte", + "Search objects": "Căutați obiecte", + "Search objects...": "Căutați obiecte...", + "Search property names...": "Căutați nume de proprietăți...", + "Search registers...": "Căutați registre...", + "Search schemas...": "Căutați scheme...", + "Search trail deletion not implemented yet": "Ștergerea urmelor de căutare nu este încă implementată", + "Search views...": "Căutați vizualizări...", + "Search {name}...": "Căutați {name}...", + "Searching...": "Se caută...", + "Secret": "Secret", + "Secret key for HMAC signature generation (optional)": "Cheie secretă pentru generarea semnăturii HMAC (opțional)", + "Security": "Securitate", + "See organisations as a table": "Vizualizați organizațiile ca tabel", + "See organisations as cards": "Vizualizați organizațiile ca fișe", + "Select AI Agent": "Selectați agentul AI", + "Select Active Organisation": "Selectați organizația activă", + "Select All": "Selectați tot", + "Select ConfigSet": "Selectați ConfigSet", + "Select File Types to Vectorize:": "Selectați tipurile de fișiere de vectorizat:", + "Select HTTP method": "Selectați metoda HTTP", + "Select Register and Schema": "Selectați registrul și schema", + "Select Views to Vectorize:": "Selectați vizualizările de vectorizat:", + "Select a Nextcloud group to add": "Selectați un grup Nextcloud de adăugat", + "Select a branch": "Selectați o ramură", + "Select a date property": "Selectați o proprietate de tip dată", + "Select a model or type a custom model name": "Selectați un model sau introduceți un nume de model personalizat", + "Select a register": "Selectați un registru", + "Select a register...": "Selectați un registru...", + "Select a repository": "Selectați un depozit", + "Select a repository you have write access to": "Selectați un depozit la care aveți acces de scriere", + "Select a schema": "Selectați o schemă", + "Select a schema...": "Selectați o schemă...", + "Select a user": "Selectați un utilizator", + "Select agent type": "Selectați tipul de agent", + "Select all entities": "Selectați toate entitățile", + "Select an AI Agent": "Selectați un agent AI", + "Select an AI agent to begin chatting with your data.": "Selectați un agent AI pentru a începe să conversați cu datele dumneavoastră.", + "Select an email to see linked objects": "Selectați un e-mail pentru a vedea obiectele asociate", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Selectați o organizație și un utilizator pentru a-i adăuga ca membru. Căutați organizații după nume.", + "Select backend": "Selectați backendul", + "Select chat model": "Selectați modelul de conversație", + "Select collection for files": "Selectați colecția pentru fișiere", + "Select collection for objects": "Selectați colecția pentru obiecte", + "Select configuration type...": "Selectați tipul de configurație...", + "Select default organisation": "Selectați organizația implicită", + "Select entity {value}": "Selectați entitatea {value}", + "Select event to listen to...": "Selectați evenimentul de ascultat...", + "Select fields to display": "Selectați câmpurile de afișat", + "Select files with the correct extension": "Selectați fișiere cu extensia corectă", + "Select group": "Selectați grupul", + "Select groups (optional)": "Selectați grupuri (opțional)", + "Select groups to notify...": "Selectați grupurile de notificat...", + "Select groups with access to this agent": "Selectați grupurile cu acces la acest agent", + "Select groups...": "Selectați grupuri...", + "Select model": "Selectați modelul", + "Select one or more registers": "Selectați unul sau mai multe registre", + "Select one or more schemas": "Selectați una sau mai multe scheme", + "Select options...": "Selectați opțiuni...", + "Select or create labels, or select \"No label\" to add files": "Selectați sau creați etichete sau selectați \"No label\" pentru a adăuga fișiere", + "Select owner application (optional)...": "Selectați aplicația proprietară (opțional)...", + "Select period": "Selectați perioada", + "Select property to send as payload": "Selectați proprietatea de trimis ca payload", + "Select provider": "Selectați furnizorul", + "Select registers and schemas to save a view": "Selectați registre și scheme pentru a salva o vizualizare", + "Select retry policy": "Selectați politica de reîncercare", + "Select role": "Selectați rolul", + "Select schemas (empty = all schemas)": "Selectați scheme (gol = toate schemele)", + "Select schemas to warm up": "Selectați schemele de preîncălzit", + "Select search mode": "Selectați modul de căutare", + "Select source type...": "Selectați tipul sursei...", + "Select target type": "Selectați tipul țintă", + "Select the branch to publish to": "Selectați ramura în care să publicați", + "Select the event this webhook should listen to": "Selectați evenimentul pe care acest webhook ar trebui să îl asculte", + "Select views to filter data (optional)": "Selectați vizualizări pentru a filtra datele (opțional)", + "Select views to limit which data the agent can access": "Selectați vizualizări pentru a limita datele la care poate accesa agentul", + "Select views to vectorize:": "Selectați vizualizările de vectorizat:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Selectați ce grupuri Nextcloud sunt disponibile pentru această organizație. Utilizatorii din aceste grupuri vor avea acces la resursele organizației.", + "Select which data views the AI can search": "Selectați în ce vizualizări de date poate căuta AI-ul", + "Select which property from the event should be used as the webhook payload data": "Selectați ce proprietate din eveniment ar trebui utilizată ca date de payload pentru webhook", + "Select which tools the AI can use to perform actions": "Selectați ce instrumente poate utiliza AI-ul pentru a efectua acțiuni", + "Select {fieldName} values": "Selectați valorile {fieldName}", + "Selected": "Selectat", + "Selected Groups": "Grupuri selectate", + "Selected Objects ({count})": "Obiecte selectate ({count})", + "Selected audit trails deleted successfully": "Urmele de audit selectate au fost șterse cu succes", + "Selected search trails deleted successfully": "Urmele de căutare selectate au fost șterse cu succes", + "Selected users can access this view": "Utilizatorii selectați pot accesa această vizualizare", + "Send additional feedback": "Trimiteți feedback suplimentar", + "Send as CloudEvent": "Trimiteți ca CloudEvent", + "Send as cloudevent": "Trimiteți ca cloudevent", + "Sensitive PII": "PII sensibile", + "Serial Mode (Safer, slower)": "Mod serial (Mai sigur, mai lent)", + "Serial:": "Serial:", + "Server Configuration": "Configurarea serverului", + "Server Information": "Informații despre server", + "Set as Active": "Setați ca activ", + "Set as active organisation": "Setați ca organizație activă", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Setați limite pentru utilizarea API și consumul de tokenuri. Utilizați 0 pentru resurse nelimitate.", + "Settings": "Setări", + "Settings updated successfully": "Setări actualizate cu succes", + "Shards": "Fragmente", + "Share": "Partajare", + "Share with Groups": "Partajare cu grupuri", + "Share with Users": "Partajare cu utilizatori", + "Show": "Afișare", + "Show Filters": "Afișare filtre", + "Show less": "Afișare mai puțin", + "Show only entries with changes": "Afișează doar intrările cu modificări", + "Showing {count} dashboard(s)": "Se afișează {count} tablou(ri) de bord", + "Showing {count} processing activities": "Se afișează {count} activități de prelucrare", + "Showing {showing} of {total} agents": "Se afișează {showing} din {total} agenți", + "Showing {showing} of {total} applications": "Se afișează {showing} din {total} aplicații", + "Showing {showing} of {total} audit trail entries": "Se afișează {showing} din {total} intrări din jurnalul de audit", + "Showing {showing} of {total} configurations": "Se afișează {showing} din {total} configurații", + "Showing {showing} of {total} deleted items": "Se afișează {showing} din {total} elemente șterse", + "Showing {showing} of {total} entities": "Se afișează {showing} din {total} entități", + "Showing {showing} of {total} files": "Se afișează {showing} din {total} fișiere", + "Showing {showing} of {total} log entries": "Se afișează {showing} din {total} intrări de jurnal", + "Showing {showing} of {total} search trail entries": "Se afișează {showing} din {total} intrări din jurnalul de căutare", + "Showing {showing} of {total} sources": "Se afișează {showing} din {total} surse", + "Showing {showing} of {total} templates": "Se afișează {showing} din {total} șabloane", + "Showing {showing} of {total} webhooks": "Se afișează {showing} din {total} webhook-uri", + "Showing {shown} of {total} organisations": "Se afișează {shown} din {total} organizații", + "Simple": "Simplu", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Interogări simple: căutări de text de bază cu parametri minimi (de ex. un singur termen de căutare, fără filtre avansate)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Interogări simple: căutări de text de bază cu parametri minimi (de ex. un singur termen de căutare, fără filtre avansate)", + "Size": "Dimensiune", + "Slug": "Slug", + "Soft Delete Mode": "Mod ștergere temporară", + "Soft Deleted Items": "Elemente șterse temporar", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Șterge temporar toate obiectele pentru această schemă ({active} active, {deleted} deja șterse)", + "SolrCloud mode enabled": "Mod SolrCloud activat", + "Something went wrong while adding files": "A apărut o eroare la adăugarea fișierelor", + "Source": "Sursă", + "Source successfully updated": "Sursă actualizată cu succes", + "Sources": "Surse", + "Sources:": "Surse:", + "Special Rights": "Drepturi speciale", + "Standalone SOLR mode": "Mod SOLR autonom", + "Start Conversation": "Începe conversația", + "Start Date Field": "Câmp dată de început", + "Start Vectorization": "Începe vectorizarea", + "Start a conversation": "Începe o conversație", + "Starting names cache warmup...": "Se inițiază preîncălzirea memoriei cache a numelor...", + "Starting validation for {schema}...": "Se inițiază validarea pentru {schema}...", + "Starting {action} for {schema}...": "Se inițiază {action} pentru {schema}...", + "Starting...": "Se inițiază...", + "Statistics": "Statistici", + "Statistics and Metrics": "Statistici și metrici", + "Statistics refreshed": "Statistici reîmprospătate", + "Stats calculation not yet implemented": "Calculul statisticilor nu este încă implementat", + "Status": "Stare", + "Status Code": "Cod de stare", + "Step": "Pas", + "Storage": "Stocare", + "Storage Quota (MB)": "Cotă de stocare (MB)", + "String Constraints": "Constrângeri pentru șiruri de caractere", + "Subject identifier (email, BSN, name, etc.)": "Identificator subiect (e-mail, BSN, nume etc.)", + "Subscribe": "Abonare", + "Subscribed": "Abonat", + "Success": "Succes", + "Success Rate": "Rată de succes", + "Success Status": "Stare de succes", + "Successful": "Reușit", + "Successfully added user to organisation: {name}": "Utilizator adăugat cu succes la organizația: {name}", + "Successfully cleared {count} audit trails": "S-au șters cu succes {count} jurnale de audit", + "Successfully cleared {count} blob storage objects": "S-au șters cu succes {count} obiecte de stocare blob", + "Successfully cleared {count} search trails": "S-au șters cu succes {count} jurnale de căutare", + "Successfully created {count} SOLR fields": "S-au creat cu succes {count} câmpuri SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "S-au șters cu succes {count} încorporări. Vă rugăm să revectorizați datele.", + "Successfully deleted {count} objects for {schema}": "S-au șters cu succes {count} obiecte pentru {schema}", + "Successfully fixed {count} SOLR field configurations": "S-au remediat cu succes {count} configurații de câmp SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Import reușit: {registers} registre, {schemas} scheme, {objects} obiecte", + "Successfully joined organisation": "V-ați alăturat cu succes organizației", + "Successfully permanently deleted {count} objects": "S-au șters definitiv cu succes {count} obiecte", + "Successfully processed {count} files!": "S-au prelucrat cu succes {count} fișiere!", + "Successfully restored {count} object": "S-a restaurat cu succes {count} obiect", + "Successfully restored {count} objects": "S-au restaurat cu succes {count} obiecte", + "Successfully saved configuration for {count} facets!": "S-a salvat cu succes configurația pentru {count} fațete!", + "Supported File Types": "Tipuri de fișiere acceptate", + "Switch Active Organisation": "Schimbă organizația activă", + "Switch Organisation": "Schimbă organizația", + "Sync Table": "Sincronizare tabel", + "Synchronization Settings": "Setări de sincronizare", + "Syncing magic table for {schema}...": "Se sincronizează tabelul magic pentru {schema}...", + "Syncs": "Sincronizări", + "System Default": "Implicit de sistem", + "System Prompt": "Prompt de sistem", + "System announcements": "Anunțuri de sistem", + "Table": "Tabel", + "Target ID": "ID țintă", + "Target Type*": "Tip țintă*", + "Technical Description": "Descriere tehnică", + "Technical Details": "Detalii tehnice", + "Technical description for developers and administrators": "Descriere tehnică pentru dezvoltatori și administratori", + "Technical measures": "Măsuri tehnice", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatură", + "Templates": "Șabloane", + "Test": "Testare", + "Test Chat": "Testare chat", + "Test Connection": "Testare conexiune", + "Test Embedding": "Testare încorporare", + "Test endpoint executed successfully": "Punct final de test executat cu succes", + "Test webhook delivered successfully": "Webhook de test livrat cu succes", + "Test webhook delivery failed": "Livrarea webhook-ului de test a eșuat", + "Test webhook sent successfully": "Webhook de test trimis cu succes", + "Testing...": "Se testează...", + "Text Chunk #{id}": "Fragment de text #{id}", + "Text Extraction": "Extragere text", + "Text Extraction Engine": "Motor de extragere text", + "Text chunks": "Fragmente de text", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Fragmentele de text sunt generate în timpul extragerii fișierelor și stocate în baza de date. Vectorizarea citește aceste fișiere prefragmentate și le convertește în încorporări.", + "Text extracted successfully": "Text extras cu succes", + "Text extraction disabled": "Extragere text dezactivată", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplicația Nextcloud Calendar nu este instalată sau activată pe acest server.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplicația Nextcloud Contacts nu este instalată sau activată pe acest server.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplicația Nextcloud Deck nu este instalată sau activată pe acest server.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplicația Nextcloud Mail nu este instalată sau activată pe acest server.", + "The URL where webhook events will be sent": "URL-ul către care vor fi trimise evenimentele webhook", + "The requested conversation does not exist": "Conversația solicitată nu există", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Tabloul de bord solicitat nu a putut fi încărcat. Verificați URL-ul sau alegeți alt tablou de bord din listă.", + "The saved view and all its search configuration": "Vizualizarea salvată și întreaga sa configurație de căutare", + "There are no audit trail entries matching your current filters.": "Nu există intrări în jurnalul de audit care să corespundă filtrelor curente.", + "There are no deleted items matching your current filters.": "Nu există elemente șterse care să corespundă filtrelor curente.", + "There are no search trail entries matching your current filters.": "Nu există intrări în jurnalul de căutare care să corespundă filtrelor curente.", + "There are no webhook log entries matching your filters.": "Nu există intrări de jurnal webhook care să corespundă filtrelor.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Acestea vor fi păstrate conform perioadei de retenție configurate a schemei lor și vor fi șterse automat definitiv la expirarea perioadei de retenție. Perioada de retenție este configurabilă per schemă și poate fi găsită în setările schemei.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Acestea vor fi păstrate conform perioadei de retenție configurate a schemei lor și apoi eliminate definitiv.", + "This action cannot be undone.": "Această acțiune nu poate fi anulată.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Această acțiune nu poate fi anulată. Asigurați-vă că nicio colecție nu folosește acest ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Această acțiune nu poate fi anulată. Asigurați-vă că nicio colecție nu folosește acest configset.", + "This action will submit a deactivation request to your administrators.": "Această acțiune va trimite o solicitare de dezactivare către administratorii dumneavoastră.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Acest agent nu are vizualizări sau instrumente configurabile, dar puteți ajusta totuși setările RAG de mai jos.", + "This analysis may take some time": "Această analiză poate dura ceva timp", + "This audit trail entry does not contain any change information.": "Această intrare din jurnalul de audit nu conține informații despre modificări.", + "This configuration has no data defined.": "Această configurație nu are date definite.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Acest punct final este învechit. Folosiți în schimb puncte finale bazate pe fragmente.", + "This entity has no relations to objects or files": "Această entitate nu are relații cu obiecte sau fișiere", + "This is a test webhook from OpenRegister": "Acesta este un webhook de test de la OpenRegister", + "This is not valid JSON": "Acesta nu este JSON valid", + "This key already exists on this schema": "Această cheie există deja în această schemă", + "This schema must use Magic Table configuration to sync": "Această schemă trebuie să folosească configurația Magic Table pentru a se sincroniza", + "This schema must use magic table configuration to sync": "Această schemă trebuie să folosească configurația magic table pentru a se sincroniza", + "This source has no associated registers.": "Această sursă nu are registre asociate.", + "This token will only be shown once. Copy it now.": "Acest token va fi afișat o singură dată. Copiați-l acum.", + "This will copy the _default ConfigSet with the new name": "Acest lucru va copia ConfigSet-ul _default cu noul nume", + "This will copy the _default configset with the new name": "Acest lucru va copia configset-ul _default cu noul nume", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Acest lucru va șterge definitiv TOATE încorporările (vectorii) din baza de date. Va trebui să revectorizați toate obiectele și fișierele. Această acțiune nu poate fi anulată.\n\nSunteți sigur că doriți să continuați?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Acest lucru va șterge definitiv TOATE încorporările (vectorii) din baza de date. Va trebui să revectorizați toate obiectele și fișierele. Această acțiune nu poate fi anulată.\\n\\nSunteți sigur că doriți să continuați?", + "This will permanently delete:": "Acest lucru va șterge definitiv:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Acest lucru va elimina definitiv {count} obiecte deja șterse temporar din baza de date. Această acțiune nu poate fi anulată.", + "Timeout (seconds)": "Expirare (secunde)", + "Timestamp": "Marcaj temporal", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Sfat: Activați doar vizualizările care necesită căutare semantică pentru a minimiza costurile de încorporare. Tabelele de căutare simple rareori necesită vectorizare.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Sfat: activați doar vizualizările care necesită căutare semantică pentru a minimiza costurile de încorporare. Tabelele de căutare simple rareori necesită vectorizare.", + "Title": "Titlu", + "Title *": "Titlu *", + "Title Template": "Șablon de titlu", + "Title*": "Titlu*", + "To (e.g. -1 year or 2025-12-31)": "Până la (de ex. -1 year sau 2025-12-31)", + "To Date": "Până la data", + "To add files larger than or equal to 512MB, go to the": "Pentru a adăuga fișiere mai mari sau egale cu 512 MB, accesați", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Pentru a ajusta dimensiunea fragmentelor sau strategia, accesați Configurare fișiere → Limite de prelucrare.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Pentru a ajusta dimensiunea fragmentelor sau strategia, accesați configurare fișiere → limite de prelucrare.", + "To change the embedding provider or model, go to LLM Configuration.": "Pentru a schimba furnizorul sau modelul de încorporare, accesați Configurare LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Pentru a schimba furnizorul sau modelul de încorporare, accesați configurare LLM.", + "To date": "Până la data", + "Toggle search sidebar": "Comută bara laterală de căutare", + "Token Created": "Token creat", + "Token Quota (per request)": "Cotă de token-uri (per solicitare)", + "Token copied to clipboard": "Token copiat în clipboard", + "Token name": "Nume token", + "Token revoked": "Token revocat", + "Tools": "Instrumente", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Instrumentele se execută cu permisiunile utilizatorului implicit al agentului atunci când nicio sesiune de utilizator nu este activă (de ex. sarcini cron). Configurați utilizatorul implicit în fila Setări.", + "Tools respect the agent's own role and organization boundaries.": "Instrumentele respectă rolul propriu al agentului și limitele organizației.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Instrumentele respectă vizualizările, permisiunile și limitele organizației ale agentului.", + "Top Deleters": "Cei care șterg cel mai mult", + "Total": "Total", + "Total Audit Trails": "Total jurnale de audit", + "Total Chunks Available:": "Total fragmente disponibile:", + "Total Deleted Items": "Total elemente șterse", + "Total Members": "Total membri", + "Total Objects": "Total obiecte", + "Total Objects in Database:": "Total obiecte în baza de date:", + "Total Organisations": "Total organizații", + "Total Results": "Total rezultate", + "Total Searches": "Total căutări", + "Total Size": "Dimensiune totală", + "Total Storage": "Stocare totală", + "Total events": "Total evenimente", + "Total:": "Total:", + "Totals": "Totaluri", + "Try searching by UUID or with different keywords": "Încercați să căutați după UUID sau cu alte cuvinte cheie", + "Type": "Tip", + "Type \"DELETE COLLECTION\" to confirm:": "Tastați \"DELETE COLLECTION\" pentru a confirma:", + "Type Issue": "Problemă de tip", + "Type Variations:": "Variații de tip:", + "Type filter (optional, e.g. email)": "Filtru de tip (opțional, de ex. e-mail)", + "Type must be \"positive\" or \"negative\"": "Tipul trebuie să fie \"positive\" sau \"negative\"", + "Type to search for groups": "Tastați pentru a căuta grupuri", + "Type to search for objects...": "Tastați pentru a căuta obiecte...", + "Type to search for organisations": "Tastați pentru a căuta organizații", + "Type to search for users": "Tastați pentru a căuta utilizatori", + "Type to search groups...": "Tastați pentru a căuta grupuri...", + "Type to search users...": "Tastați pentru a căuta utilizatori...", + "Type to search...": "Tastați pentru a căuta...", + "Type your username to confirm": "Tastați numele de utilizator pentru a confirma", + "URL": "URL", + "URL where Ollama is running": "URL-ul unde rulează Ollama", + "UUID copied to clipboard": "UUID copiat în clipboard", + "UUID:": "UUID:", + "Unchanged": "Nemodificat", + "Unique Search Terms": "Termeni de căutare unici", + "Unique Terms": "Termeni unici", + "Unknown": "Necunoscut", + "Unknown Object": "Obiect necunoscut", + "Unknown Register": "Registru necunoscut", + "Unknown Schema": "Schemă necunoscută", + "Unknown error": "Eroare necunoscută", + "Unknown widget type: {type}": "Tip de widget necunoscut: {type}", + "Unlink email": "Deconectează e-mailul", + "Unlink event": "Deconectează evenimentul", + "Unnamed Object": "Obiect fără nume", + "Untitled": "Fără titlu", + "Untitled View": "Vizualizare fără titlu", + "Update": "Actualizare", + "Update Operations": "Operațiuni de actualizare", + "Update available: {local} → {remote}": "Actualizare disponibilă: {local} → {remote}", + "Update configuration: ...": "Actualizare configurație: ...", + "Update register OAS: ...": "Actualizare OAS registru: ...", + "Update vectors when object data changes (recommended for accurate search)": "Actualizează vectorii când datele obiectului se modifică (recomandat pentru căutare precisă)", + "Updated": "Actualizat", + "Updated At": "Actualizat la", + "Updated:": "Actualizat:", + "Upload new avatar": "Încarcă un avatar nou", + "Uploading configuration sets...": "Se încarcă seturile de configurare...", + "Uptime:": "Timp de funcționare:", + "Url": "Url", + "Usage Count": "Număr de utilizări", + "Use AI agents for processing and analysis": "Folosiți agenți AI pentru prelucrare și analiză", + "Use Large Language Model features": "Folosiți funcțiile de model lingvistic mare", + "Use SolrCloud with Zookeeper for distributed search": "Folosiți SolrCloud cu ZooKeeper pentru căutare distribuită", + "Use SolrCloud with zookeeper for distributed search": "Folosiți SolrCloud cu ZooKeeper pentru căutare distribuită", + "Use VNG APIs instead of MongoDB.": "Folosiți API-urile VNG în loc de MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Folosiți intervalele implicite (Ultimele 7/30/90 de zile, Ultimul an, Mai vechi)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Folosiți stocare externă (de ex. MongoDB) în loc de stocarea internă Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Folosiți filtre pentru a restrânge intrările din jurnalul de audit după registru, schemă, tip de acțiune, utilizator, interval de date sau ID obiect.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Folosiți filtre pentru a restrânge elementele șterse după registru, schemă, dată de ștergere sau utilizatorul care le-a șters.", + "Use filters to narrow down entities by type or category.": "Folosiți filtre pentru a restrânge entitățile după tip sau categorie.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Folosiți filtre pentru a restrânge intrările din jurnalul de căutare după registru, schemă, stare de succes, utilizator, interval de date, termeni de căutare sau metrici de performanță.", + "Used By Collections": "Folosit de colecții", + "Used by": "Folosit de", + "User": "Utilizator", + "User Agent Statistics": "Statistici User Agent", + "User Description": "Descriere utilizator", + "User Quota": "Cotă utilizator", + "User rate limits cleared successfully": "Limitele de rată ale utilizatorului au fost șterse cu succes", + "User removed successfully": "Utilizator eliminat cu succes", + "User-friendly description shown in forms and help text": "Descriere prietenoasă afișată în formulare și textul de ajutor", + "Username": "Nume de utilizator", + "Username for SOLR authentication (optional)": "Nume de utilizator pentru autentificarea SOLR (opțional)", + "Users": "Utilizatori", + "Uses": "Utilizări", + "Using Pre-Generated Chunks": "Se folosesc fragmente pregenerate", + "Validate": "Validare", + "Validate Objects": "Validare obiecte", + "Validation completed for {schema}": "Validare finalizată pentru {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validare finalizată pentru {schema}: {processed} prelucrate, {updated} actualizate, {failed} eșuate", + "Validation completed successfully": "Validare finalizată cu succes", + "Validation failed": "Validare eșuată", + "Value": "Valoare", + "Value list (split on ,)": "Listă de valori (separate prin ,)", + "Vector Dimensions": "Dimensiuni vector", + "Vector Search Backend": "Backend de căutare vectorială", + "Vector field: _embedding_": "Vector field: _embedding_", + "Vectorization Triggers": "Declanșatoare de vectorizare", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vectorizare finalizată: {vectorized} obiecte vectorizate, {failed} eșuate", + "Vectorization failed: {error}": "Vectorizarea a eșuat: {error}", + "Vectorize All Files": "Vectorizează toate fișierele", + "Vectorize All Objects": "Vectorizează toate obiectele", + "Vectorize all file types": "Vectorizează toate tipurile de fișiere", + "Vectorize all views": "Vectorizează toate vizualizările", + "Vectorize on object creation": "Vectorizează la crearea obiectului", + "Vectors will be stored in your existing object and file collections": "Vectorii vor fi stocați în colecțiile dumneavoastră existente de obiecte și fișiere", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Se verifică conectivitatea serverului...", + "Version": "Versiune", + "Version Information": "Informații despre versiune", + "Very High": "Foarte ridicat", + "Very high": "Foarte ridicat", + "View": "Vizualizare", + "View API Docs": "Vezi documentația API", + "View API Documentation": "Vezi documentația API", + "View APIs": "Vezi API-urile", + "View Changes": "Vezi modificările", + "View Details": "Vezi detaliile", + "View Error": "Vezi eroarea", + "View File": "Vezi fișierul", + "View Full Details": "Vezi toate detaliile", + "View Logs": "Vezi jurnalele", + "View Name": "Nume vizualizare", + "View Object": "Vezi obiectul", + "View Parameters": "Vezi parametrii", + "View Selection": "Selecție vizualizare", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Vizualizați și analizați jurnalele de căutare cu capacități avansate de filtrare și analiză", + "View and analyze system audit trails with advanced filtering capabilities": "Vizualizați și analizați jurnalele de audit ale sistemului cu capacități avansate de filtrare", + "View and manage authorization across registers and schemas": "Vizualizați și gestionați autorizarea pentru registre și scheme", + "View deleted successfully": "Vizualizare ștearsă cu succes", + "View entity details and manage relations": "Vizualizați detaliile entității și gestionați relațiile", + "View name": "Nume vizualizare", + "View name is required": "Numele vizualizării este obligatoriu", + "View object": "Vizualizează obiectul", + "View saved successfully!": "Vizualizare salvată cu succes!", + "View search analytics and manage search logs": "Vizualizați analizele de căutare și gestionați jurnalele de căutare", + "View successfully deleted": "Vizualizare ștearsă cu succes", + "View successfully updated": "Vizualizare actualizată cu succes", + "View updated successfully!": "Vizualizare actualizată cu succes!", + "View webhook delivery logs and filter by webhook": "Vizualizați jurnalele de livrare webhook și filtrați după webhook", + "View {count} more": "Vezi încă {count}", + "Views": "Vizualizări", + "Visible to end users": "Vizibil pentru utilizatorii finali", + "Visible to users": "Vizibil pentru utilizatori", + "Wait for Response": "Așteaptă răspunsul", + "Wait for webhook response before continuing (required for request/response flows)": "Așteaptă răspunsul webhook înainte de a continua (obligatoriu pentru fluxurile cerere/răspuns)", + "Waiting for cluster synchronization...": "Se așteaptă sincronizarea clusterului...", + "Warmup Names Cache": "Preîncălzire memorie cache nume", + "Warmup failed": "Preîncălzirea a eșuat", + "Warning": "Avertisment", + "Warning:": "Avertisment:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Avertisment: Această proprietate are tipuri inconsecvente: {types}. Luați în considerare dacă toate obiectele ar trebui să aibă același tip.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Vom scana toate obiectele care aparțin acestei scheme pentru a descoperi proprietăți noi și a analiza proprietățile existente pentru posibile îmbunătățiri. Procesul implică examinarea structurii de date a fiecărui obiect, identificarea proprietăților nedefinite în schema curentă și găsirea oportunităților de a îmbunătăți definițiile proprietăților existente cu constrângeri, formate și reguli de validare mai bune.", + "Webhook": "Webhook", + "Webhook Log Details": "Detalii jurnal webhook", + "Webhook Logs": "Jurnale webhook", + "Webhook created successfully": "Webhook creat cu succes", + "Webhook deleted": "Webhook șters", + "Webhook retry delivered successfully": "Reîncercarea webhook-ului a fost livrată cu succes", + "Webhook retry delivery failed": "Livrarea reîncercării webhook-ului a eșuat", + "Webhook updated": "Webhook actualizat", + "Webhook updated successfully": "Webhook actualizat cu succes", + "Webhooks": "Webhook-uri", + "Weekly": "Săptămânal", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Când agentul rulează fără o sesiune de utilizator (de ex. sarcini programate), va fi folosit contextul acestui utilizator", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Când agentul rulează fără o sesiune de utilizator (de ex. sarcini programate), va fi folosit contextul acestui utilizator.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Când este dezactivat, această fațetă va afișa doar valori din această schemă și va include un filtru de schemă atunci când este selectată.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Când este activat, salvarea acestui obiect va actualiza și obiectele țintă pentru a include o referință înapoi la acest obiect.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Când este activat, această proprietate va fi eliminată din obiectul sursă după actualizarea obiectelor țintă.", + "Workflow Management": "Gestionarea fluxurilor de lucru", + "Workflows": "Fluxuri de lucru", + "Wrap webhook payload in CloudEvents format for better interoperability": "Încapsulează încărcătura utilă a webhook-ului în format CloudEvents pentru o mai bună interoperabilitate", + "Wrap webhook payload in cloudevents format for better interoperability": "Încapsulează încărcătura utilă a webhook-ului în format CloudEvents pentru o mai bună interoperabilitate", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Da", + "You": "Dumneavoastră", + "You are not yet a member of any organisations.": "Nu sunteți încă membru al niciunei organizații.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Puteți crea ConfigSet-uri noi pe baza șablonului _default sau puteți încărca unele personalizate direct pe serverul dumneavoastră SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Puteți crea configset-uri noi pe baza șablonului _default sau puteți încărca unele personalizate direct pe serverul dumneavoastră SOLR.", + "You do not have access to this conversation": "Nu aveți acces la această conversație", + "You do not have permission to delete this conversation": "Nu aveți permisiunea de a șterge această conversație", + "You do not have permission to modify this conversation": "Nu aveți permisiunea de a modifica această conversație", + "You do not have permission to restore this conversation": "Nu aveți permisiunea de a restaura această conversație", + "You do not have permission to view the permission matrix. Admin access is required.": "Nu aveți permisiunea de a vizualiza matricea de permisiuni. Este necesar acces de administrator.", + "You have selected {count} properties to add to the schema.": "Ați selectat {count} proprietăți de adăugat la schemă.", + "You must be logged in to favorite views": "Trebuie să fiți autentificat pentru a adăuga vizualizări la favorite", + "You need an AI agent to start a conversation.": "Aveți nevoie de un agent AI pentru a începe o conversație.", + "Your Fireworks AI API key. Get one at": "Cheia dumneavoastră API Fireworks AI. Obțineți una la", + "Your OpenAI API key. Get one at": "Cheia dumneavoastră API OpenAI. Obțineți una la", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Feedbackul dumneavoastră a fost înregistrat. Opțional, puteți furniza detalii suplimentare aici...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Feedbackul dumneavoastră a fost înregistrat. opțional, puteți furniza detalii suplimentare aici...", + "Zookeeper Hosts": "Gazde ZooKeeper", + "Zookeeper Port": "Port ZooKeeper", + "Zookeeper Settings (SolrCloud)": "Setări ZooKeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Șir de conexiune ZooKeeper pentru SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Numărul portului ZooKeeper (opțional, implicit 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVERTISMENT DE ȘTERGERE DEFINITIVĂ \\u26A0\\uFE0F\n\nSunteți pe cale să ștergeți DEFINITIV TOATE obiectele pentru schema \"{schema}\":\n\n\\u2022 Obiecte active: {active}\n\\u2022 Obiecte șterse temporar: {deleted}\n\\u2022 Total: {total}\n\nAceste obiecte vor fi eliminate complet din baza de date și NU pot fi recuperate.\n\nSunteți absolut sigur?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F AVERTISMENT DE ȘTERGERE DEFINITIVĂ \\u26A0\\uFE0F\n\nSunteți pe cale să ștergeți DEFINITIV {count} obiecte șterse temporar pentru schema \"{schema}\".\n\nAceste obiecte vor fi eliminate complet din baza de date și NU pot fi recuperate.\n\nSunteți absolut sigur?", + "and accessible to all users in selected groups (or all users if no groups selected).": "și accesibil tuturor utilizatorilor din grupurile selectate (sau tuturor utilizatorilor dacă nu este selectat niciun grup).", + "and add the files there.": "și adăugați fișierele acolo.", + "approved": "aprobat", + "are only accessible to invited users.": "sunt accesibile doar utilizatorilor invitați.", + "by": "de către", + "chunk": "fragment", + "chunks": "fragmente", + "chunks processed": "fragmente prelucrate", + "completed": "finalizat", + "configuration(s)": "configurație(ii)", + "conversationId is required": "conversationId este obligatoriu", + "create": "creare", + "delete": "ștergere", + "deleted objects section": "secțiunea obiectelor șterse", + "e.g., lib/Settings/config.json": "e.g., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "e.g., lib/Settings/register.json", + "e.g., lib/settings/config.json": "e.g., lib/settings/config.json", + "e.g., lib/settings/register.json": "e.g., lib/settings/register.json", + "execute": "executare", + "failed": "eșuat", + "file": "fișier", + "files": "fișiere", + "file{plural}": "fișier{plural}", + "folder": "dosar", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "în {register}", + "in_progress": "în curs", + "inherited": "moștenit", + "list": "listare", + "log{plural}": "jurnal{plural}", + "manage": "gestionare", + "members": "membri", + "menu or contact someone with permission to create agents.": "meniu sau contactați pe cineva cu permisiunea de a crea agenți.", + "message content is required": "conținutul mesajului este obligatoriu", + "messages": "mesaje", + "missing": "lipsă", + "myapp": "myapp", + "n8n URL and API key are required": "URL-ul n8n și cheia API sunt obligatorii", + "n8n configuration saved successfully": "Configurație n8n salvată cu succes", + "n8n connection not configured": "Conexiune n8n neconfigurată", + "n8n connection successful": "Conexiune n8n reușită", + "n8n connection test failed: {message}": "Testul conexiunii n8n a eșuat: {message}", + "n8n connection test successful": "Test de conexiune n8n reușit", + "n8n initialization failed: {message}": "Inițializarea n8n a eșuat: {message}", + "n8n integration disabled": "Integrare n8n dezactivată", + "n8n integration enabled": "Integrare n8n activată", + "n8n project initialized successfully": "Proiect n8n inițializat cu succes", + "n8n settings saved successfully": "Setări n8n salvate cu succes", + "notify_push configuration guide": "ghid de configurare notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push este instalat, dar OpenRegister nu a confirmat încă un push reușit. Declanșați o salvare de obiect pentru a-l activa sau verificați configurația notify_push.", + "notify_push is installed but not yet active": "notify_push este instalat, dar nu este încă activ", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "obiecte", + "objects processed": "obiecte prelucrate", + "object{plural}": "obiect{plural}", + "or pick a range": "sau alegeți un interval", + "org-...": "org-...", + "pending": "în așteptare", + "provides a good balance between speed and accuracy.": "oferă un echilibru bun între viteză și precizie.", + "public": "public", + "read": "citire", + "register(s)": "registru(re)", + "register(s) selected": "registru(re) selectat(e)", + "register{plural}": "registru{plural}", + "rejected": "respins", + "required": "obligatoriu", + "results": "rezultate", + "schema(s)": "schemă(e)", + "schema(s) selected": "schemă(e) selectată(e)", + "schema{plural}": "schemă{plural}", + "selected objects": "obiecte selectate", + "set": "setare", + "sk-...": "sk-...", + "this application": "această aplicație", + "to": "până la", + "type must be \"positive\" or \"negative\"": "tipul trebuie să fie \"positive\" sau \"negative\"", + "update": "actualizare", + "vectorized": "vectorizat", + "view": "vizualizare", + "views selected": "vizualizări selectate", + "waiting": "în așteptare", + "{active} active, {deleted} deleted": "{active} active, {deleted} șterse", + "{count} audit trails cleared successfully": "{count} jurnale de audit șterse cu succes", + "{count} compliance issue(s) detected": "{count} problemă(e) de conformitate detectată(e)", + "{count} deletions": "{count} ștergeri", + "{count} documents found": "{count} documente găsite", + "{count} email": "{count} e-mail", + "{count} emails": "{count} e-mailuri", + "{count} entries": "{count} intrări", + "{count} issues": "{count} probleme", + "{count} members": "{count} membri", + "{count} searches": "{count} căutări", + "{count} selected": "{count} selectat(e)", + "{count} widget(s)": "{count} widget(uri)", + "{matched} object(s) matched. {erased} erased.": "{matched} obiect(e) corespunzătoare. {erased} șterse.", + "{minutes} minutes ago": "acum {minutes} minute", + "{percentage}% of objects": "{percentage}% din obiecte", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} în {register} / {schema}", + "{type} cache cleared successfully": "Memoria cache {type} a fost ștearsă cu succes", + "ℹ️ Current Configuration": "ℹ️ Configurația curentă", + "ℹ️ current configuration": "ℹ️ configurația curentă", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ PERICOL: Toate cele {total} obiecte vor fi ȘTERSE DEFINITIV din baza de date. Această acțiune este IREVERSIBILĂ și nu poate fi anulată!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVERTISMENT DE ȘTERGERE DEFINITIVĂ ⚠️\n\nSunteți pe cale să ștergeți DEFINITIV TOATE obiectele pentru schema \"{schema}\":\n\n• Obiecte active: {active}\n• Obiecte șterse temporar: {deleted}\n• Total: {total}\n\nAceste obiecte vor fi eliminate complet din baza de date și NU pot fi recuperate.\n\nSunteți absolut sigur?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ AVERTISMENT DE ȘTERGERE DEFINITIVĂ ⚠️\n\nSunteți pe cale să ștergeți DEFINITIV {count} obiecte șterse temporar pentru schema \"{schema}\".\n\nAceste obiecte vor fi eliminate complet din baza de date și NU pot fi recuperate.\n\nSunteți absolut sigur?", + "⚡ Batch Processing": "⚡ Prelucrare în lot", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR poate gestiona milioane de documente cu timpi de răspuns la căutare sub o secundă...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Context cuprinzător ✓ Mai puțin probabil să rateze detalii ✗ Răspunsuri mai lente ✗ Poate include informații mai puțin relevante", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Răspunsuri mai rapide ✓ Răspunsuri mai concentrate ✗ Poate rata informații relevante", + "✨ AI Features": "✨ Funcții AI", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 În modul distribuit, SOLR folosește ZooKeeper pentru coordonarea clusterului...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Căutarea fațetată le permite utilizatorilor să exploreze rezultatele pe categorii...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Știați că? SOLR alimentează căutarea pentru Netflix, Apple și multe alte site-uri majore!", + "💰 View Selection (Cost Optimization)": "💰 Selecție vizualizare (Optimizarea costurilor)", + "📄 Object Serialization": "📄 Serializare obiect", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR oferă analize și statistici bogate despre performanța căutării...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSet-urile conțin fișierele de schemă și de configurare pentru indexul dumneavoastră de căutare...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replicarea asigură disponibilitatea indexului dumneavoastră de căutare chiar dacă nodurile eșuează...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR este o platformă de căutare enterprise puternică construită pe Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Setări de vectorizare", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Scalarea automată poate adăuga sau elimina dinamic noduri în funcție de încărcare...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Funcțiile de securitate includ autentificare, autorizare și criptare SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-mail", + "{count} e-mailuri" + ], + "Purge {count} object from database": [ + "Epurați {count} obiect din baza de date", + "Epurați {count} obiecte din baza de date" + ], + "Restore {count} object": [ + "Restaurați {count} obiect", + "Restaurați {count} obiecte" + ], + "Successfully restored {count} object": [ + "S-a restaurat cu succes {count} obiect", + "S-au restaurat cu succes {count} obiecte" + ], + "Delete {count} object": [ + "Ștergeți {count} obiect", + "Ștergeți {count} obiecte" + ], + "Object successfully deleted": [ + "Obiectul a fost șters cu succes", + "Obiectele au fost șterse cu succes" + ] + } +} diff --git a/l10n/ru.js b/l10n/ru.js new file mode 100644 index 0000000000..a3f0d1983f --- /dev/null +++ b/l10n/ru.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(без темы)", + "(no title)" : "(без названия)", + "(unnamed)" : "(без имени)", + "(untitled card)" : "(карточка без названия)", + "Activities" : "Действия", + "Add Endpoint" : "Добавить конечную точку", + "Add Source" : "Добавить источник", + "Add card" : "Добавить карточку", + "Add contact" : "Добавить контакт", + "All clear" : "Всё в порядке", + "Articles" : "Статьи", + "Code" : "Код", + "Compliance" : "Соответствие требованиям", + "Contacts" : "Контакты", + "Create Agent" : "Создать агента", + "Create Application" : "Создать приложение", + "Create event" : "Создать событие", + "Erasure complete" : "Стирание завершено", + "Erasure preview" : "Предпросмотр стирания", + "Expand details" : "Развернуть детали", + "Save changes" : "Сохранить изменения", + "📄 Object Serialization" : "📄 Сериализация объектов", + "🔢 Vectorization Settings" : "🔢 Настройки векторизации", + "💰 View Selection (Cost Optimization)" : "💰 Выбор представлений (оптимизация затрат)", + "AI Assistant" : "ИИ-помощник", + "AI Chat" : "ИИ-чат", + "API Key" : "API-ключ", + "API tokens saved successfully" : "API-токены успешно сохранены", + "About ConfigSets" : "О наборах конфигурации (ConfigSets)", + "Action" : "Действие", + "Action Distribution" : "Распределение действий", + "Action:" : "Действие:", + "Actions" : "Действия", + "Active" : "Активный", + "Active Collections" : "Активные коллекции", + "Active Filters" : "Активные фильтры", + "Active Organisations" : "Активные организации", + "Active filters:" : "Активные фильтры:", + "Active organisation set successfully" : "Активная организация успешно установлена", + "Activity Period" : "Период активности", + "Add Groups" : "Добавить группы", + "Add Schema" : "Добавить схему", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Добавьте заголовки, описания схем и информацию реестра для более богатого контекста поиска", + "Add to favorites" : "Добавить в избранное", + "Added" : "Добавлено", + "Added to favorites" : "Добавлено в избранное", + "Additional Information" : "Дополнительная информация", + "Additional feedback saved. Thank you!" : "Дополнительный отзыв сохранён. Спасибо!", + "Admin" : "Администратор", + "Advanced" : "Дополнительно", + "Advanced Filters" : "Расширенные фильтры", + "Advanced Options" : "Расширенные параметры", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Расширенные фильтры недоступны при использовании источника базы данных. Переключитесь на Auto или SOLR Index для использования фильтров.", + "After" : "После", + "Agent deleted successfully" : "Агент успешно удалён", + "Agents" : "Агенты", + "All" : "Все", + "All Categories" : "Все категории", + "All Collections" : "Все коллекции", + "All Confidence Levels" : "Все уровни достоверности", + "All ConfigSets" : "Все наборы конфигурации", + "All Files" : "Все файлы", + "All Levels" : "Все уровни", + "All Types" : "Все типы", + "All Webhooks" : "Все веб-хуки", + "All actions" : "Все действия", + "All audit trails cleared successfully" : "Все журналы аудита успешно очищены", + "All registers" : "Все реестры", + "All schemas" : "Все схемы", + "All searches" : "Все поисковые запросы", + "All users" : "Все пользователи", + "An error occurred while clearing audit trails" : "Произошла ошибка при очистке журналов аудита", + "An error occurred while deleting the audit trail" : "Произошла ошибка при удалении записи журнала аудита", + "An error occurred while deleting the view" : "Произошла ошибка при удалении представления", + "An error occurred while permanently deleting the objects" : "Произошла ошибка при безвозвратном удалении объектов", + "Analysis completed:" : "Анализ завершён:", + "Analysis steps:" : "Шаги анализа:", + "Analytics" : "Аналитика", + "Analyze Objects" : "Анализировать объекты", + "Analyze Properties" : "Анализировать свойства", + "Analyze existing properties for improvement opportunities" : "Анализировать существующие свойства для поиска возможностей улучшения", + "Analyzing..." : "Анализ...", + "Any favorites and sharing settings for this view" : "Все настройки избранного и совместного доступа для этого представления", + "Any user" : "Любой пользователь", + "App store cache invalidated successfully" : "Кэш магазина приложений успешно аннулирован", + "Application deleted successfully" : "Приложение успешно удалено", + "Applications" : "Приложения", + "Apply Changes" : "Применить изменения", + "Archive" : "Архив", + "Archive conversation" : "Архивировать переписку", + "Archived conversations are hidden from your active list" : "Архивированные переписки скрыты из активного списка", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Вы уверены, что хотите очистить старые журналы поиска? Это удалит записи старше 30 дней.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Вы уверены, что хотите удалить выбранные записи журнала аудита? Это действие нельзя отменить.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Вы уверены, что хотите удалить выбранные записи журнала поиска? Это действие нельзя отменить.", + "Are you sure you want to delete this ConfigSet?" : "Вы уверены, что хотите удалить этот набор конфигурации?", + "Are you sure you want to permanently delete" : "Вы уверены, что хотите безвозвратно удалить", + "Ask a question..." : "Задайте вопрос...", + "Ask questions about your data using natural language" : "Задавайте вопросы о ваших данных на естественном языке", + "Attempt" : "Попытка", + "Audit Trail Changes" : "Изменения в журнале аудита", + "Audit Trail Details" : "Детали записи журнала аудита", + "Audit Trail Management" : "Управление журналом аудита", + "Audit Trail Statistics" : "Статистика журнала аудита", + "Audit Trails" : "Журналы аудита", + "Audit trail data copied to clipboard" : "Данные журнала аудита скопированы в буфер обмена", + "Audit trail deleted successfully" : "Запись журнала аудита успешно удалена", + "Audit trail successfully deleted" : "Запись журнала аудита успешно удалена", + "Authentication" : "Аутентификация", + "Auto-commit disabled" : "Автоматическая фиксация отключена", + "Auto-commit enabled" : "Автоматическая фиксация включена", + "Auto-create Default Organisation" : "Автоматически создавать организацию по умолчанию", + "Auto-retry failed vectorizations" : "Автоматически повторять неудачные векторизации", + "Automatically commit changes to SOLR index" : "Автоматически фиксировать изменения в индексе SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Автоматически создавать организацию по умолчанию, если она отсутствует при инициализации приложения", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Автоматически создавать векторные представления из текстовых фрагментов при загрузке и обработке файлов", + "Automatically generate vector embeddings when objects are created or updated" : "Автоматически создавать векторные представления при создании или обновлении объектов", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Автоматически повторять неудачные попытки векторизации (максимум 3 попытки)", + "Available Filters" : "Доступные фильтры", + "Available Workflows" : "Доступные рабочие процессы", + "Avg Execution Time" : "Среднее время выполнения", + "Avg Members/Org" : "Среднее число участников/организация", + "Avg Object Views/Session" : "Среднее количество просмотров объектов/сессия", + "Avg Response Time" : "Среднее время ответа", + "Avg Results/Search" : "Среднее число результатов/поиск", + "Avg Searches/Session" : "Среднее число поисков/сессия", + "Back" : "Назад", + "Back to Entities" : "Назад к объектам", + "Back to Registers" : "Назад к реестрам", + "Back to Webhooks" : "Назад к веб-хукам", + "Back to applications" : "Назад к приложениям", + "Back to entities" : "Назад к объектам", + "Backend updated successfully. Please reload the application." : "Backend успешно обновлён. Пожалуйста, перезагрузите приложение.", + "Base URL (Optional)" : "Базовый URL (необязательно)", + "Basic Information" : "Основная информация", + "Batch Size" : "Размер пакета", + "Batch extraction completed" : "Пакетное извлечение завершено", + "Before" : "До", + "Before object vectorization can work:" : "Перед тем как начать векторизацию объектов:", + "Behavior Issue" : "Проблема поведения", + "Bookmarks" : "Закладки", + "Blob storage has been retired. All objects now use magic tables." : "Хранилище Blob устарело. Все объекты теперь используют magic tables.", + "Branch" : "Ветка", + "Bulk delete operation completed successfully" : "Операция массового удаления успешно завершена", + "Bulk save operation completed successfully" : "Операция массового сохранения успешно завершена", + "Business Data" : "Бизнес-данные", + "Cache cleared successfully" : "Кэш успешно очищен", + "Calculate Sizes" : "Вычислить размеры", + "Cancel" : "Отмена", + "Cannot delete: objects are still attached" : "Невозможно удалить: объекты всё ещё прикреплены", + "Category" : "Категория", + "Change Type" : "Тип изменения", + "Changes" : "Изменения", + "Changes copied to clipboard" : "Изменения скопированы в буфер обмена", + "Chat Model" : "Модель чата", + "Chat Provider" : "Провайдер чата", + "Chat Provider (RAG)" : "Провайдер чата (RAG)", + "Chat Settings" : "Настройки чата", + "Chat provider connection successful!" : "Подключение к провайдеру чата успешно!", + "Chat settings" : "Настройки чата", + "Choose a register" : "Выберите реестр", + "Choose a schema" : "Выберите схему", + "Choose how vector similarity calculations are performed for semantic search" : "Выберите метод вычисления векторного сходства для семантического поиска", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Выберите типы файлов для включения в процесс векторизации. Будут обработаны только файлы с извлечённым текстом и фрагментами.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Выберите представления для включения в процесс векторизации. Оставьте пустым для обработки всех представлений согласно вашей конфигурации.", + "Chunk" : "Фрагмент", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Удаление фрагментов ещё не реализовано. Используйте конечные точки на основе фрагментов.", + "Chunks" : "Фрагменты", + "Chunks to Vectorize:" : "Фрагменты для векторизации:", + "Cleanup Old Trails" : "Очистить старые журналы", + "Cleanup completed" : "Очистка завершена", + "Clear All Embeddings" : "Очистить все векторные представления", + "Clear Entries" : "Очистить записи", + "Clear Filtered Audit Trails" : "Очистить отфильтрованные журналы аудита", + "Clear Filters" : "Сбросить фильтры", + "Clear Index" : "Очистить индекс", + "Clear Selection" : "Снять выделение", + "Clear all" : "Очистить всё", + "Clear all filters" : "Сбросить все фильтры", + "Clear date range" : "Очистить диапазон дат", + "Clearing..." : "Очистка...", + "Close" : "Закрыть", + "Collection Management" : "Управление коллекциями", + "Collection Name" : "Название коллекции", + "Collection assignments updated successfully" : "Назначения коллекции успешно обновлены", + "Collection cleared successfully" : "Коллекция успешно очищена", + "Collection copied successfully" : "Коллекция успешно скопирована", + "Collection created successfully" : "Коллекция успешно создана", + "Collection deleted successfully" : "Коллекция успешно удалена", + "Collection used to store and index file metadata and content" : "Коллекция для хранения и индексации метаданных и содержимого файлов", + "Collection used to store and index object data" : "Коллекция для хранения и индексации данных объектов", + "Collections:" : "Коллекции:", + "Commit Message" : "Сообщение коммита", + "Commit Within (ms)" : "Фиксация в течение (мс)", + "Commit message" : "Сообщение коммита", + "Compare current schema with real object data" : "Сравнить текущую схему с реальными данными объектов", + "Completed" : "Завершено", + "Complex" : "Сложный", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Сложные запросы: расширенный поиск с несколькими фильтрами, операторами и сложными комбинациями параметров", + "Confidence Level" : "Уровень достоверности", + "Confidence Score" : "Оценка достоверности", + "Config must be provided as an object" : "Конфигурация должна быть предоставлена как объект", + "ConfigSet" : "Набор конфигурации", + "ConfigSet Management" : "Управление наборами конфигурации", + "ConfigSet Name" : "Название набора конфигурации", + "ConfigSet created successfully" : "Набор конфигурации успешно создан", + "ConfigSet deleted successfully" : "Набор конфигурации успешно удалён", + "ConfigSet:" : "Набор конфигурации:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Наборы конфигурации определяют схему и настройки для ваших коллекций SOLR. Они содержат определения полей, анализаторы и другие настройки поиска.", + "Configuration" : "Конфигурация", + "Configuration Data" : "Данные конфигурации", + "Configuration saved" : "Конфигурация сохранена", + "Configurations" : "Конфигурации", + "Configure Facets" : "Настроить фасеты", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Настроить провайдеров больших языковых моделей (LLM) для функций на основе ИИ, включая семантический поиск, векторные представления и чат.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Настроить основные параметры подключения к серверу SOLR, включая аутентификацию и сетевые параметры. Используйте отдельные диалоги управления наборами конфигурации и коллекциями для управления ядрами и коллекциями.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Настроить преобразование объектов базы данных в векторные представления для семантического поиска. Объекты векторизуются напрямую без необходимости извлечения текста.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Настроить преобразование объектов в текст перед векторизацией. Эти настройки влияют на качество поиска и контекст.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Настроить параметры векторизации файлов. Этот процесс создаст векторные представления для всех извлечённых фрагментов файлов.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Настроить параметры векторизации объектов. Этот процесс создаст векторные представления для всех объектов, соответствующих фильтрам представления.", + "Configure which types of data to search and how many sources to retrieve" : "Настроить типы данных для поиска и количество извлекаемых источников", + "Connection Failed" : "Подключение не удалось", + "Connection Settings" : "Настройки подключения", + "Connection Successful!" : "Подключение успешно!", + "Connection failed" : "Подключение не удалось", + "Connection protocol" : "Протокол подключения", + "Connection successful" : "Подключение успешно", + "Connection timeout in seconds" : "Тайм-аут подключения в секундах", + "Constant delay between retries (5 minutes)" : "Постоянная задержка между повторными попытками (5 минут)", + "Constraint Issue" : "Проблема ограничения", + "Content Filters" : "Фильтры содержимого", + "Continue to Properties" : "Перейти к свойствам", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Управляйте тем, какие представления объектов следует векторизовать, чтобы снизить расходы на API. Векторизуйте только те представления, которые выигрывают от семантического поиска.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Управляйте тем, какие представления и инструменты ИИ может использовать в этой переписке. По умолчанию все возможности агента включены.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Управляет случайностью (0 = детерминировано, 2 = очень творчески)", + "Conversation ID is required" : "Требуется ID переписки", + "Conversation archived" : "Переписка архивирована", + "Conversation archived successfully" : "Переписка успешно архивирована", + "Conversation cleared successfully" : "Переписка успешно очищена", + "Conversation deleted" : "Переписка удалена", + "Conversation permanently deleted" : "Переписка безвозвратно удалена", + "Conversation renamed" : "Переписка переименована", + "Conversation restored" : "Переписка восстановлена", + "Conversation title" : "Название переписки", + "Copied!" : "Скопировано!", + "Copy" : "Копировать", + "Copy Changes" : "Копировать изменения", + "Copy Collection" : "Копировать коллекцию", + "Copy Data" : "Копировать данные", + "Copy Full Data" : "Копировать все данные", + "Copying..." : "Копирование...", + "Count" : "Количество", + "Counting objects..." : "Подсчёт объектов...", + "Costs" : "Затраты", + "Create" : "Создать", + "Create Collection" : "Создать коллекцию", + "Create ConfigSet" : "Создать набор конфигурации", + "Create New Collection" : "Создать новую коллекцию", + "Create New ConfigSet" : "Создать новый набор конфигурации", + "Create Operations" : "Операции создания", + "Create Webhook" : "Создать веб-хук", + "Create a copy of collection:" : "Создать копию коллекции:", + "Create a new ConfigSet based on the _default template" : "Создать новый набор конфигурации на основе шаблона _default", + "Create a new SOLR collection from an existing ConfigSet" : "Создать новую коллекцию SOLR из существующего набора конфигурации", + "Create your first AI agent to get started." : "Создайте своего первого ИИ-агента для начала работы.", + "Created" : "Создано", + "Created:" : "Создано:", + "Creating..." : "Создание...", + "Current Filters" : "Текущие фильтры", + "Current Type:" : "Текущий тип:", + "Custom API endpoint if using a different region" : "Пользовательская конечная точка API для другого региона", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Пользовательские HTTP-заголовки (по одному на строку, формат: Header-Name: value)", + "Daily" : "Ежедневно", + "Dashboard" : "Дашборд", + "Data Source" : "Источник данных", + "Data sources" : "Источники данных", + "Data type does not match observed values" : "Тип данных не соответствует наблюдаемым значениям", + "Database URL" : "URL базы данных", + "Database information refreshed" : "Информация о базе данных обновлена", + "Date Range" : "Диапазон дат", + "Default" : "По умолчанию", + "Default Organisation" : "Организация по умолчанию", + "Default: 5" : "По умолчанию: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Задержка удваивается с каждой попыткой (2, 4, 8 минут...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Задержка увеличивается линейно (5, 10, 15 минут...)", + "Delete" : "Удалить", + "Delete Application" : "Удалить приложение", + "Delete Audit Trail" : "Удалить запись журнала аудита", + "Delete Collection" : "Удалить коллекцию", + "Delete ConfigSet" : "Удалить набор конфигурации", + "Delete Objects" : "Удалить объекты", + "Delete Operations" : "Операции удаления", + "Delete View" : "Удалить представление", + "Delete all objects in this schema" : "Удалить все объекты в этой схеме", + "Delete permanently" : "Удалить навсегда", + "Delete this conversation?" : "Удалить эту переписку?", + "Delete view" : "Удалить представление", + "Deleted" : "Удалено", + "Deleted By" : "Удалено пользователем", + "Deleted Date" : "Дата удаления", + "Deleted Items Management" : "Управление удалёнными элементами", + "Deleted This Week" : "Удалено на этой неделе", + "Deleted Today" : "Удалено сегодня", + "Deleted:" : "Удалено:", + "Deleting..." : "Удаление...", + "Deletion Date Range" : "Диапазон дат удаления", + "Deletion Statistics" : "Статистика удалений", + "Deprecated" : "Устаревший", + "Description" : "Описание", + "Description:" : "Описание:", + "Detect data types and patterns" : "Определить типы данных и шаблоны", + "Detected At" : "Обнаружено в", + "Detected Format:" : "Обнаруженный формат:", + "Detected Issues:" : "Обнаруженные проблемы:", + "Disable" : "Отключить", + "Disabled" : "Отключено", + "Discover Files" : "Обнаружить файлы", + "Discovered Properties" : "Обнаруженные свойства", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Вы хотите безвозвратно удалить все отфильтрованные записи журнала аудита? Это действие нельзя отменить.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Вы хотите безвозвратно удалить эту запись журнала аудита? Это действие нельзя отменить.", + "Documents" : "Документы", + "Download API Spec" : "Скачать спецификацию API", + "Edit" : "Редактировать", + "Edit Register" : "Редактировать реестр", + "Edit Schema" : "Редактировать схему", + "Edit View" : "Редактировать представление", + "Edit Webhook" : "Редактировать веб-хук", + "Edit view" : "Редактировать представление", + "Edit view details" : "Редактировать детали представления", + "Effectiveness" : "Эффективность", + "Email" : "Электронная почта", + "Emails" : "Письма", + "Embedding Model" : "Модель векторных представлений", + "Embedding Provider" : "Провайдер векторных представлений", + "Embedding generated successfully" : "Векторное представление успешно создано", + "Embedding provider connection successful!" : "Подключение к провайдеру векторных представлений успешно!", + "Enable" : "Включить", + "Enable auto-creation" : "Включить автосоздание", + "Enable automatic file vectorization" : "Включить автоматическую векторизацию файлов", + "Enable automatic object vectorization" : "Включить автоматическую векторизацию объектов", + "Enable automatic synchronization" : "Включить автоматическую синхронизацию", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Включить подробное логирование операций SOLR (рекомендуется для отладки)", + "Enable faceting" : "Включить фасетирование", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Включить или отключить функции LLM. Настройте провайдеров и модели с помощью кнопки «Настройка LLM» выше.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Включить или отключить интеграцию поиска SOLR. Настройте параметры подключения с помощью кнопки «Настройки подключения» выше.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Включить или отключить интеграцию с рабочими процессами n8n. Настройте параметры подключения ниже.", + "Enable or disable this webhook" : "Включить или отключить этот веб-хук", + "Enable vectorization for all existing and future views (may increase costs)" : "Включить векторизацию для всех существующих и будущих представлений (может увеличить затраты)", + "Enabled" : "Включено", + "Endpoints" : "Конечные точки", + "Enter ConfigSet name" : "Введите название набора конфигурации", + "Enter collection name" : "Введите название коллекции", + "Enter description (optional)..." : "Введите описание (необязательно)...", + "Enter new collection name" : "Введите новое название коллекции", + "Enter object ID" : "Введите ID объекта", + "Enter search term" : "Введите поисковый запрос", + "Enter view name..." : "Введите название представления...", + "Enter webhook name" : "Введите название веб-хука", + "Entities" : "Объекты", + "Entity Information" : "Информация об объекте", + "Entity deleted successfully" : "Объект успешно удалён", + "Entity not found" : "Объект не найден", + "Entries to be deleted:" : "Записи для удаления:", + "Enum Issue" : "Проблема с перечислением", + "Enum constraint is missing" : "Ограничение перечисления отсутствует", + "Error" : "Ошибка", + "Error Details" : "Детали ошибки", + "Error Information" : "Информация об ошибке", + "Error Message" : "Сообщение об ошибке", + "Error loading application" : "Ошибка при загрузке приложения", + "Error loading audit trails" : "Ошибка при загрузке журналов аудита", + "Error loading entity" : "Ошибка при загрузке объекта", + "Error loading search trails" : "Ошибка при загрузке журналов поиска", + "Estimated Batches:" : "Расчётное количество пакетов:", + "Estimated Cost:" : "Расчётная стоимость:", + "Estimated Duration:" : "Расчётная длительность:", + "Event" : "Событие", + "Event Property for Payload" : "Свойство события для полезной нагрузки", + "Events" : "События", + "Example Value" : "Пример значения", + "Exclusive Maximum" : "Строгий максимум", + "Exclusive Minimum" : "Строгий минимум", + "Execution Mode" : "Режим выполнения", + "Execution Time" : "Время выполнения", + "Execution Time Range" : "Диапазон времени выполнения", + "Existing Improvements" : "Существующие улучшения", + "Exponential" : "Экспоненциальный", + "Export" : "Экспорт", + "Export completed successfully" : "Экспорт успешно завершён", + "Export, view, or delete audit trails" : "Экспортировать, просматривать или удалять журналы аудита", + "Extend Schema" : "Расширить схему", + "Extended" : "Расширенный", + "Extensions" : "Расширения", + "Extract Pending Files" : "Извлечь ожидающие файлы", + "Extract properties from each object" : "Извлечь свойства из каждого объекта", + "Extracted At" : "Извлечено в", + "Extraction Status" : "Статус извлечения", + "Facet configuration updated successfully" : "Конфигурация фасетов успешно обновлена", + "Facets discovered and configured successfully" : "Фасеты успешно обнаружены и настроены", + "Facets discovered successfully" : "Фасеты успешно обнаружены", + "Failed" : "Не удалось", + "Failed to analyze schema properties" : "Не удалось проанализировать свойства схемы", + "Failed to archive conversation" : "Не удалось архивировать переписку", + "Failed to calculate sizes" : "Не удалось вычислить размеры", + "Failed to clear collection" : "Не удалось очистить коллекцию", + "Failed to copy changes" : "Не удалось скопировать изменения", + "Failed to copy data" : "Не удалось скопировать данные", + "Failed to copy data to clipboard" : "Не удалось скопировать данные в буфер обмена", + "Failed to create conversation" : "Не удалось создать переписку", + "Failed to create or find project" : "Не удалось создать или найти проект", + "Failed to delete collection" : "Не удалось удалить коллекцию", + "Failed to delete conversation" : "Не удалось удалить переписку", + "Failed to delete webhook" : "Не удалось удалить веб-хук", + "Failed to download API specification" : "Не удалось скачать спецификацию API", + "Failed to get SOLR field configuration" : "Не удалось получить конфигурацию полей SOLR", + "Failed to get entity categories" : "Не удалось получить категории объектов", + "Failed to get entity statistics" : "Не удалось получить статистику объектов", + "Failed to get entity types" : "Не удалось получить типы объектов", + "Failed to load LLM configuration" : "Не удалось загрузить конфигурацию LLM", + "Failed to load Nextcloud groups" : "Не удалось загрузить группы Nextcloud", + "Failed to load conversation" : "Не удалось загрузить переписку", + "Failed to load entities" : "Не удалось загрузить объекты", + "Failed to load entity" : "Не удалось загрузить объект", + "Failed to load files" : "Не удалось загрузить файлы", + "Failed to load organisations" : "Не удалось загрузить организации", + "Failed to load templates" : "Не удалось загрузить шаблоны", + "Failed to load webhooks" : "Не удалось загрузить веб-хуки", + "Failed to load workflows" : "Не удалось загрузить рабочие процессы", + "Failed to refresh database information" : "Не удалось обновить информацию о базе данных", + "Failed to reindex collection" : "Не удалось переиндексировать коллекцию", + "Failed to rename conversation" : "Не удалось переименовать переписку", + "Failed to restore conversation" : "Не удалось восстановить переписку", + "Failed to retry extraction" : "Не удалось повторить извлечение", + "Failed to retry webhook" : "Не удалось повторить веб-хук", + "Failed to save GitHub token" : "Не удалось сохранить токен GitHub", + "Failed to save GitLab URL" : "Не удалось сохранить URL GitLab", + "Failed to save GitLab token" : "Не удалось сохранить токен GitLab", + "Failed to save additional feedback" : "Не удалось сохранить дополнительный отзыв", + "Failed to save n8n configuration" : "Не удалось сохранить конфигурацию n8n", + "Failed to save roles" : "Не удалось сохранить роли", + "Failed to save settings" : "Не удалось сохранить настройки", + "Failed to save webhook" : "Не удалось сохранить веб-хук", + "Failed to send feedback" : "Не удалось отправить отзыв", + "Failed to test webhook" : "Не удалось проверить веб-хук", + "Failed to update favorite status" : "Не удалось обновить статус избранного", + "Failed to update schema properties" : "Не удалось обновить свойства схемы", + "Failed to update view" : "Не удалось обновить представление", + "Failed to update webhook" : "Не удалось обновить веб-хук", + "Feedback recorded" : "Отзыв записан", + "Fewer sources (1-3):" : "Меньше источников (1-3):", + "Field" : "Поле", + "File" : "Файл", + "File Chunk Prediction" : "Прогноз фрагментов файла", + "File Collection" : "Коллекция файлов", + "File Management" : "Управление файлами", + "File Name" : "Имя файла", + "File Path" : "Путь к файлу", + "File Type Selection" : "Выбор типа файла", + "File Vectorization" : "Векторизация файлов", + "File Warmup" : "Прогрев файлов", + "File actions menu" : "Меню действий с файлом", + "File anonymized successfully" : "Файл успешно анонимизирован", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Векторизация фрагментов файлов ещё не реализована. Фрагменты готовы и сохранены, но сервис векторизации находится в разработке.", + "File collection not configured" : "Коллекция файлов не настроена", + "File discovery completed" : "Обнаружение файлов завершено", + "File extraction completed" : "Извлечение файлов завершено", + "File extraction queued" : "Извлечение файлов поставлено в очередь", + "File indexed successfully" : "Файл успешно проиндексирован", + "File is already anonymized" : "Файл уже анонимизирован", + "File not found" : "Файл не найден", + "File settings updated successfully" : "Настройки файлов успешно обновлены", + "File sources" : "Источники файлов", + "File vectorization configuration saved successfully" : "Конфигурация векторизации файлов успешно сохранена", + "File vectorization started. Check the statistics section for progress." : "Векторизация файлов запущена. Прогресс см. в разделе статистики.", + "File warmup completed" : "Прогрев файлов завершён", + "Files" : "Файлы", + "Files with Completed Extraction:" : "Файлы с завершённым извлечением:", + "Files → fileCollection, Objects → objectCollection" : "Файлы → fileCollection, Объекты → objectCollection", + "Filter Audit Trails" : "Фильтровать журналы аудита", + "Filter Deleted Items" : "Фильтровать удалённые элементы", + "Filter Objects" : "Фильтровать объекты", + "Filter Properties" : "Фильтровать свойства", + "Filter Search Trails" : "Фильтровать журналы поиска", + "Filter Statistics" : "Статистика фильтров", + "Filter and analyze search trail entries" : "Фильтровать и анализировать записи журнала поиска", + "Filter and manage audit trail entries" : "Фильтровать и управлять записями журнала аудита", + "Filter and manage soft deleted items" : "Фильтровать и управлять программно удалёнными элементами", + "Filter by object ID" : "Фильтровать по ID объекта", + "Filter by search term" : "Фильтровать по поисковому запросу", + "Filter by webhook" : "Фильтровать по веб-хуку", + "Filter data loaded automatically. Use the filters below to refine your search." : "Данные фильтров загружены автоматически. Используйте фильтры ниже для уточнения поиска.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Фильтровать триггеры веб-хука по свойствам полезной нагрузки (по одному на строку, формат: key: value)", + "Filtered" : "Отфильтровано", + "Filters" : "Фильтры", + "Fireworks AI Chat Settings" : "Настройки чата Fireworks AI", + "Fireworks AI Embedding Configuration" : "Конфигурация векторных представлений Fireworks AI", + "First" : "Первый", + "Fixed" : "Исправлено", + "For chat and retrieval-augmented generation" : "Для чата и генерации с расширенным поиском", + "For vector embeddings and semantic search" : "Для векторных представлений и семантического поиска", + "Format" : "Формат", + "Format Issue" : "Проблема формата", + "Format constraint is missing" : "Ограничение формата отсутствует", + "From Date" : "Дата начала", + "From date" : "Дата начала", + "Full data copied to clipboard" : "Полные данные скопированы в буфер обмена", + "General Issue" : "Общая проблема", + "Generate recommendations and confidence scores" : "Сгенерировать рекомендации и оценки достоверности", + "Generate vectors immediately when new objects are created" : "Генерировать векторы сразу при создании новых объектов", + "GitHub token is valid" : "Токен GitHub действителен", + "GitHub token removed successfully" : "Токен GitHub успешно удалён", + "GitHub token saved successfully" : "Токен GitHub успешно сохранён", + "GitLab URL saved successfully" : "URL GitLab успешно сохранён", + "GitLab token is valid" : "Токен GitLab действителен", + "GitLab token saved successfully" : "Токен GitLab успешно сохранён", + "HTTP Method" : "Метод HTTP", + "HTTP method used to send webhook requests" : "Метод HTTP для отправки запросов веб-хука", + "Headers" : "Заголовки", + "Health" : "Состояние", + "Heartbeat successful - connection kept alive" : "Сигнал жизнеспособности успешен — соединение поддерживается", + "Helpful" : "Полезно", + "Hide Filters" : "Скрыть фильтры", + "Hide in forms" : "Скрыть в формах", + "Hide in list view" : "Скрыть в списковом представлении", + "High" : "Высокий", + "High Confidence" : "Высокая достоверность", + "Host" : "Хост", + "Hourly" : "Каждый час", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Глубина обхода вложенных свойств объекта (1-20). Большие значения захватывают больше деталей, но увеличивают размер вектора.", + "How often to check for updates (1-168 hours)" : "Как часто проверять обновления (1-168 часов)", + "How to handle retries for failed webhook deliveries" : "Как обрабатывать повторные попытки при неудачной доставке веб-хука", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Ограничения частоты запросов IP успешно очищены", + "Identify properties not in the schema" : "Определить свойства, отсутствующие в схеме", + "Immutable" : "Неизменяемый", + "Import" : "Импорт", + "Import successful" : "Импорт успешно выполнен", + "Improved Property" : "Улучшенное свойство", + "In use" : "Используется", + "Inactive" : "Неактивный", + "Include IDs and names of related objects for better contextual search" : "Включить идентификаторы и имена связанных объектов для улучшения контекстного поиска", + "Include related object references" : "Включить ссылки на связанные объекты", + "Include schema and register metadata" : "Включить метаданные схемы и реестра", + "Initialization failed" : "Инициализация не удалась", + "Initialize Project" : "Инициализировать проект", + "Inspect Fields" : "Проверить поля", + "Inspect Index" : "Проверить индекс", + "Invalid" : "Недействительный", + "Invalid batch size. Must be between 1 and 5000" : "Неверный размер пакета. Должен быть от 1 до 5000", + "Invalid field name provided" : "Указано недействительное имя поля", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Неверное значение maxObjects. Должно быть 0 (все) или положительное число", + "Invalid:" : "Недействительный:", + "Issue" : "Проблема", + "Items per page:" : "Элементов на странице:", + "Just now" : "Только что", + "LLM Configuration" : "Настройка LLM", + "LLM actions menu" : "Меню действий LLM", + "LLM configuration saved successfully" : "Конфигурация LLM успешно сохранена", + "LLM features disabled" : "Функции LLM отключены", + "LLM features enabled" : "Функции LLM включены", + "LLM must be enabled with an embedding provider configured" : "LLM должен быть включён с настроенным провайдером векторных представлений", + "LLM settings updated successfully" : "Настройки LLM успешно обновлены", + "Last" : "Последний", + "Last Triggered" : "Последнее срабатывание", + "Length Range:" : "Диапазон длины:", + "Linear" : "Линейный", + "Load Advanced Filters" : "Загрузить расширенные фильтры", + "Load advanced filters with live data from your search index" : "Загрузить расширенные фильтры с живыми данными из вашего поискового индекса", + "Load view" : "Загрузить представление", + "Loading ConfigSets..." : "Загрузка наборов конфигурации...", + "Loading advanced filters..." : "Загрузка расширенных фильтров...", + "Loading agents..." : "Загрузка агентов...", + "Loading application details..." : "Загрузка сведений о приложении...", + "Loading applications..." : "Загрузка приложений...", + "Loading archived conversations..." : "Загрузка архивированных переписок...", + "Loading audit trails..." : "Загрузка журналов аудита...", + "Loading collections..." : "Загрузка коллекций...", + "Loading configurations..." : "Загрузка конфигураций...", + "Loading conversation..." : "Загрузка переписки...", + "Loading conversations..." : "Загрузка переписок...", + "Loading deleted items..." : "Загрузка удалённых элементов...", + "Loading events..." : "Загрузка событий...", + "Loading filter data..." : "Загрузка данных фильтров...", + "Loading groups..." : "Загрузка групп...", + "Loading log details..." : "Загрузка деталей журнала...", + "Loading registers..." : "Загрузка реестров...", + "Loading schemas..." : "Загрузка схем...", + "Loading search trails..." : "Загрузка журналов поиска...", + "Loading sources..." : "Загрузка источников...", + "Loading statistics..." : "Загрузка статистики...", + "Loading users..." : "Загрузка пользователей...", + "Loading views..." : "Загрузка представлений...", + "Loading..." : "Загрузка...", + "Local" : "Локальный", + "Local Version" : "Локальная версия", + "Locked" : "Заблокировано", + "Locked:" : "Заблокировано:", + "Login successful" : "Вход выполнен успешно", + "Logs" : "Журналы", + "Low" : "Низкий", + "Low Confidence" : "Низкая достоверность", + "Lucene Version:" : "Версия Lucene:", + "Magic table synchronized successfully" : "Magic table успешно синхронизирована", + "Manage Organisation Roles" : "Управление ролями организации", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Управление коллекциями SOLR (хранилищами данных) и их назначение для объектов и файлов.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Управление наборами конфигурации SOLR (шаблонами конфигурации) для ваших коллекций.", + "Manage and configure agents for automated tasks" : "Управление и настройка агентов для автоматизированных задач", + "Manage and monitor file text extraction status" : "Управление и мониторинг статуса извлечения текста из файлов", + "Manage and restore soft deleted items from your registers" : "Управление и восстановление программно удалённых элементов из ваших реестров", + "Manage and view detected entities from files and objects" : "Управление и просмотр обнаруженных объектов в файлах и данных", + "Manage document templates and themes" : "Управление шаблонами и темами документов", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Управление рабочими процессами n8n для автоматизации OpenRegister. Рабочие процессы будут сохранены в настроенном проекте.", + "Manage webhooks for event-driven integrations" : "Управление веб-хуками для интеграций на основе событий", + "Manage your applications and modules" : "Управляйте своими приложениями и модулями", + "Manage your data registers and their configurations" : "Управляйте реестрами данных и их конфигурациями", + "Manage your data schemas and their properties" : "Управляйте схемами данных и их свойствами", + "Manage your data sources and their configurations" : "Управляйте источниками данных и их конфигурациями", + "Manage your saved search configurations" : "Управляйте сохранёнными конфигурациями поиска", + "Manage your system configurations and settings" : "Управляйте системными конфигурациями и настройками", + "Managed" : "Управляемый", + "Max Files (0 = all)" : "Максимальное количество файлов (0 = все)", + "Max Length" : "Максимальная длина", + "Max Length:" : "Максимальная длина:", + "Max Objects (0 = all)" : "Максимальное количество объектов (0 = все)", + "Max Retries" : "Максимальное число повторных попыток", + "Max Shards/Node" : "Максимальное количество шардов/узел", + "Max execution time (ms)" : "Максимальное время выполнения (мс)", + "Max ms" : "Макс. мс", + "Max result count" : "Максимальное число результатов", + "Max results" : "Максимальное число результатов", + "Maximum" : "Максимум", + "Maximum Nesting Depth" : "Максимальная глубина вложенности", + "Maximum length constraint is missing" : "Ограничение максимальной длины отсутствует", + "Maximum length is too restrictive" : "Максимальная длина слишком ограничительна", + "Maximum number of files to process. Set to 0 to process all files." : "Максимальное количество файлов для обработки. Установите 0 для обработки всех файлов.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Максимальное количество объектов для обработки. Установите 0 для обработки всех объектов.", + "Maximum number of retry attempts for failed deliveries" : "Максимальное количество попыток повтора при неудачной доставке", + "Maximum time to wait before committing changes" : "Максимальное время ожидания перед фиксацией изменений", + "Maximum tokens to generate" : "Максимальное количество генерируемых токенов", + "Maximum value constraint is missing" : "Ограничение максимального значения отсутствует", + "Maximum value is too restrictive" : "Максимальное значение слишком ограничительно", + "Medium" : "Средний", + "Medium Confidence" : "Средняя достоверность", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Средние запросы: поиск с некоторой фильтрацией или несколькими параметрами (например, диапазоны дат, конкретные реестры/схемы)", + "Members of selected groups can access this view" : "Члены выбранных групп могут получить доступ к этому представлению", + "Members:" : "Участники:", + "Memory prediction calculated successfully" : "Прогноз памяти успешно рассчитан", + "Message content is required" : "Содержимое сообщения обязательно", + "Message does not belong to this conversation" : "Сообщение не принадлежит этой переписке", + "Metadata Filters" : "Фильтры метаданных", + "Method" : "Метод", + "Min Length" : "Минимальная длина", + "Min execution time (ms)" : "Минимальное время выполнения (мс)", + "Min ms" : "Мин. мс", + "Min result count" : "Минимальное число результатов", + "Min results" : "Минимальное число результатов", + "Minimum" : "Минимум", + "Minimum value constraint is missing" : "Ограничение минимального значения отсутствует", + "Minimum value is too restrictive" : "Минимальное значение слишком ограничительно", + "Missing object fields created successfully" : "Отсутствующие поля объекта успешно созданы", + "Mode:" : "Режим:", + "Model" : "Модель", + "Model Name" : "Название модели", + "Modified" : "Изменено", + "Monthly" : "Ежемесячно", + "More sources (10-20):" : "Больше источников (10-20):", + "Most Active Objects" : "Наиболее активные объекты", + "Multiple Of" : "Кратно", + "Name" : "Название", + "Names cache warmup completed" : "Прогрев кэша имён завершён", + "New Collection Name" : "Новое название коллекции", + "New Conversation" : "Новая переписка", + "New Properties" : "Новые свойства", + "New Property" : "Новое свойство", + "New Value" : "Новое значение", + "New users without specific organisation membership will be automatically added to this organisation" : "Новые пользователи без конкретного членства в организации будут автоматически добавлены в эту организацию", + "Next" : "Далее", + "No" : "Нет", + "No ConfigSets found" : "Наборы конфигурации не найдены", + "No GitHub token configured" : "Токен GitHub не настроен", + "No GitHub token provided" : "Токен GitHub не предоставлен", + "No GitLab token provided" : "Токен GitLab не предоставлен", + "No Organisation" : "Без организации", + "No active filters" : "Активных фильтров нет", + "No activity data available" : "Данные об активности недоступны", + "No agents available" : "Агенты недоступны", + "No agents found" : "Агенты не найдены", + "No app store cache exists yet" : "Кэш магазина приложений ещё не существует", + "No applications are available." : "Приложения недоступны.", + "No applications found" : "Приложения не найдены", + "No archived conversations" : "Архивированных переписок нет", + "No audit trail entries found" : "Записи журнала аудита не найдены", + "No changes recorded" : "Изменения не зафиксированы", + "No chunks to vectorize" : "Фрагментов для векторизации нет", + "No collections found" : "Коллекции не найдены", + "No configuration data" : "Данных конфигурации нет", + "No configurations are available." : "Конфигурации недоступны.", + "No configurations found" : "Конфигурации не найдены", + "No configurations found for this application." : "Конфигурации для этого приложения не найдены.", + "No conversations yet. Create a new one to get started!" : "Переписок ещё нет. Создайте новую, чтобы начать!", + "No data available" : "Данные недоступны", + "No data available for chart" : "Данные для графика недоступны", + "No deleted items found" : "Удалённые элементы не найдены", + "No deletion data available" : "Данные об удалении недоступны", + "No description found" : "Описание не найдено", + "No description provided" : "Описание не указано", + "No entities detected in this file. Run text extraction first." : "Объекты в этом файле не обнаружены. Сначала выполните извлечение текста.", + "No entities found" : "Объекты не найдены", + "No entities have been detected yet" : "Объекты ещё не обнаружены", + "No events found" : "События не найдены", + "No expired audit trails found to clear" : "Истёкшие записи журнала аудита для очистки не найдены", + "No facetable fields available. Select a register and schema to see available filters." : "Поля для фасетирования недоступны. Выберите реестр и схему для просмотра доступных фильтров.", + "No files found" : "Файлы не найдены", + "No files have been extracted yet" : "Файлы ещё не извлекались", + "No files to process" : "Файлов для обработки нет", + "No files to reindex" : "Файлов для переиндексации нет", + "No filters are currently active. This will delete ALL audit trail entries!" : "Активных фильтров нет. Это удалит ВСЕ записи журнала аудита!", + "No log entries found" : "Записи журнала не найдены", + "No logs are available for this configuration." : "Журналы для этой конфигурации недоступны.", + "No logs are available for this source." : "Журналы для этого источника недоступны.", + "No logs found" : "Журналы не найдены", + "No mismatched fields found - SOLR schema is properly configured" : "Несоответствующие поля не найдены — схема SOLR настроена правильно", + "No objects found" : "Объекты не найдены", + "No objects found to vectorize" : "Объекты для векторизации не найдены", + "No objects selected for deletion" : "Объекты для удаления не выбраны", + "No objects to delete" : "Объектов для удаления нет", + "No objects were permanently deleted" : "Объекты не были безвозвратно удалены", + "No properties found" : "Свойства не найдены", + "No properties match your filters." : "Свойства не соответствуют вашим фильтрам.", + "No purge date set" : "Дата очистки не задана", + "No register data available" : "Данные реестра недоступны", + "No registers found" : "Реестры не найдены", + "No registers found for this application." : "Реестры для этого приложения не найдены.", + "No relations found" : "Связи не найдены", + "No request body available" : "Тело запроса недоступно", + "No response body available" : "Тело ответа недоступно", + "No saved views yet. Create one in the Search tab!" : "Сохранённых представлений ещё нет. Создайте одно на вкладке Поиск!", + "No schema data available" : "Данные схемы недоступны", + "No schema selected for exploration" : "Схема для исследования не выбрана", + "No schemas found" : "Схемы не найдены", + "No schemas found for this application." : "Схемы для этого приложения не найдены.", + "No search terms data available" : "Данные о поисковых запросах недоступны", + "No search trail entries found" : "Записи журнала поиска не найдены", + "No sources are available." : "Источники недоступны.", + "No sources found" : "Источники не найдены", + "No templates found" : "Шаблоны не найдены", + "No templates have been created yet" : "Шаблоны ещё не созданы", + "No valid magic-mapped register+schema combinations found" : "Допустимые комбинации реестр+схема с magic mapping не найдены", + "No views found. Create views first before configuring vectorization." : "Представления не найдены. Сначала создайте представления перед настройкой векторизации.", + "No views match your search" : "Представления не соответствуют вашему поиску", + "No webhooks found" : "Веб-хуки не найдены", + "No webhooks have been configured yet" : "Веб-хуки ещё не настроены", + "No workflows found in this project. Create workflows in the n8n editor." : "В этом проекте рабочие процессы не найдены. Создайте рабочие процессы в редакторе n8n.", + "None" : "Нет", + "Not helpful" : "Бесполезно", + "Not in use" : "Не используется", + "Number Constraints" : "Числовые ограничения", + "Number of chunks to vectorize in one API call (1-100)." : "Количество фрагментов для векторизации в одном API-вызове (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Количество фрагментов для векторизации в одном API-вызове. Больше = быстрее, но больше памяти. Рекомендуется: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Количество объектов для обработки в каждом пакете (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Количество объектов для векторизации в одном API-вызове. Больше = быстрее, но больше памяти. Рекомендуется: 10-50.", + "Numeric Range:" : "Числовой диапазон:", + "Object" : "Объект", + "Object Collection" : "Коллекция объектов", + "Object Count Prediction" : "Прогноз количества объектов", + "Object ID" : "ID объекта", + "Object Management" : "Управление объектами", + "Object Vectorization" : "Векторизация объектов", + "Object Warmup" : "Прогрев объектов", + "Object collection not configured" : "Коллекция объектов не настроена", + "Object not found" : "Объект не найден", + "Object permanently deleted" : "Объект безвозвратно удалён", + "Object restored successfully" : "Объект успешно восстановлен", + "Object settings updated successfully" : "Настройки объекта успешно обновлены", + "Object sources" : "Источники объектов", + "Object unlocked successfully" : "Объект успешно разблокирован", + "Object vectorization configuration saved successfully" : "Конфигурация векторизации объектов успешно сохранена", + "Object vectorized successfully" : "Объект успешно векторизован", + "Object:" : "Объект:", + "Objects" : "Объекты", + "Objects Analyzed" : "Проанализированных объектов", + "Objects Distribution" : "Распределение объектов", + "Objects being analyzed" : "Анализируемые объекты", + "Objects by Register" : "Объекты по реестрам", + "Objects by Schema" : "Объекты по схемам", + "Objects deletion completed successfully" : "Удаление объектов успешно завершено", + "Objects in schema" : "Объекты в схеме", + "Objects to Process:" : "Объектов для обработки:", + "Objects to be analyzed" : "Объекты для анализа", + "Objects to be deleted" : "Объекты для удаления", + "Objects to be validated" : "Объекты для проверки", + "Objects will be serialized as JSON text before vectorization" : "Объекты будут сериализованы в формат JSON перед векторизацией", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Объекты будут программно удалены (помечены как удалённые, но сохранены в базе данных). При необходимости их можно восстановить позже.", + "Old Value" : "Старое значение", + "Oldest Item (days)" : "Старейший элемент (дней)", + "Ollama Chat Settings" : "Настройки чата Ollama", + "Ollama Embedding Configuration" : "Конфигурация векторных представлений Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Только с изменениями", + "Open Register Objects" : "Открыть объекты реестра", + "Open n8n Editor" : "Открыть редактор n8n", + "OpenAI Chat Settings" : "Настройки чата OpenAI", + "OpenAI Embedding Configuration" : "Конфигурация векторных представлений OpenAI", + "OpenRegister Settings" : "Настройки OpenRegister", + "Optional webhook secret for signature verification" : "Необязательный секрет веб-хука для проверки подписи", + "Order" : "Порядок", + "Organisation" : "Организация", + "Organisation ID" : "ID организации", + "Organisation Members" : "Участники организации", + "Organisation Statistics" : "Статистика организации", + "Organisation created successfully" : "Организация успешно создана", + "Organisation settings saved successfully" : "Настройки организации успешно сохранены", + "Organisations" : "Организации", + "Organization" : "Организация", + "Organization ID (Optional)" : "ID организации (необязательно)", + "Orphaned Items" : "Осиротевшие элементы", + "Owner" : "Владелец", + "Owner:" : "Владелец:", + "Parallel Mode (Faster, more resource intensive)" : "Параллельный режим (быстрее, но более ресурсоёмкий)", + "Parallel:" : "Параллельно:", + "Password" : "Пароль", + "Password for SOLR authentication (optional)" : "Пароль для аутентификации SOLR (необязательно)", + "Path" : "Путь", + "Path in repository" : "Путь в репозитории", + "Path where the configuration file will be saved in the repository" : "Путь сохранения файла конфигурации в репозитории", + "Path where the register OAS file will be saved in the repository" : "Путь сохранения файла OAS реестра в репозитории", + "Pattern (regex)" : "Шаблон (regex)", + "Pattern Issue" : "Проблема шаблона", + "Pattern constraint is missing" : "Ограничение шаблона отсутствует", + "Patterns:" : "Шаблоны:", + "Pending" : "Ожидает", + "Permanent Delete Mode" : "Режим безвозвратного удаления", + "Permanently Delete" : "Удалить навсегда", + "Permanently delete already soft-deleted objects" : "Безвозвратно удалить уже программно удалённые объекты", + "Permanently delete this conversation?" : "Безвозвратно удалить эту переписку?", + "Person" : "Персона", + "Personal Data" : "Персональные данные", + "Phone" : "Телефон", + "Photos" : "Фотографии", + "Please create an agent in the" : "Пожалуйста, создайте агента в", + "Please select an agent to continue" : "Пожалуйста, выберите агента для продолжения", + "Please select which register and schema to use for the new object" : "Пожалуйста, выберите реестр и схему для нового объекта", + "Please try again later." : "Пожалуйста, повторите попытку позже.", + "Please wait while we fetch your agents." : "Пожалуйста, подождите, пока загружаются ваши агенты.", + "Please wait while we fetch your applications." : "Пожалуйста, подождите, пока загружаются ваши приложения.", + "Please wait while we fetch your configurations." : "Пожалуйста, подождите, пока загружаются ваши конфигурации.", + "Please wait while we fetch your deleted items." : "Пожалуйста, подождите, пока загружаются ваши удалённые элементы.", + "Please wait while we fetch your sources." : "Пожалуйста, подождите, пока загружаются ваши источники.", + "Polls" : "Опросы", + "Popular Search Terms" : "Популярные поисковые запросы", + "Port" : "Порт", + "Prerequisites" : "Предварительные условия", + "Press Enter to send, Shift+Enter for new line" : "Нажмите Enter для отправки, Shift+Enter для новой строки", + "Previous" : "Предыдущий", + "Private" : "Частный", + "Processes chunks in batches with simulated parallelism." : "Обрабатывает фрагменты пакетами с симулированным параллелизмом.", + "Processes file chunks sequentially (safest)." : "Обрабатывает фрагменты файлов последовательно (самый безопасный режим).", + "Processes objects in chunks with simulated parallelism." : "Обрабатывает объекты фрагментами с симулированным параллелизмом.", + "Processes objects sequentially (safest)." : "Обрабатывает объекты последовательно (самый безопасный режим).", + "Processing" : "Обработка", + "Processing Limits" : "Ограничения обработки", + "Processing..." : "Обработка...", + "Project initialized successfully" : "Проект успешно инициализирован", + "Project not found. Please initialize first." : "Проект не найден. Пожалуйста, сначала инициализируйте.", + "Properties" : "Свойства", + "Property Behaviors" : "Поведение свойств", + "Property Configuration:" : "Конфигурация свойства:", + "Property Filters" : "Фильтры свойств", + "Property Title" : "Заголовок свойства", + "Property Type" : "Тип свойства", + "Property can be improved" : "Свойство можно улучшить", + "Protected" : "Защищённый", + "Provider" : "Провайдер", + "Provider is required for testing" : "Для тестирования необходим провайдер", + "Public" : "Публичный", + "Public View" : "Публичное представление", + "Public views can be accessed by anyone in the system" : "Публичные представления доступны всем пользователям системы", + "Publish" : "Опубликовать", + "Publish Objects" : "Опубликовать объекты", + "Publish all objects in this schema" : "Опубликовать все объекты в этой схеме", + "Published" : "Опубликовано", + "Published:" : "Опубликовано:", + "Publishing..." : "Публикация...", + "Purge" : "Очистить", + "Purge Date" : "Дата очистки", + "Query Complexity Distribution" : "Распределение сложности запросов", + "Query parameter is required" : "Параметр запроса обязателен", + "RAG" : "RAG", + "RAG Configuration" : "Конфигурация RAG", + "Rate limits cleared successfully" : "Ограничения частоты запросов успешно очищены", + "Raw Changes Data" : "Необработанные данные изменений", + "Re-vectorize on object update" : "Повторно векторизовать при обновлении объекта", + "Read" : "Чтение", + "Recommendations:" : "Рекомендации:", + "Recommended Type:" : "Рекомендуемый тип:", + "Recommended: 5 sources" : "Рекомендуется: 5 источников", + "Refresh" : "Обновить", + "Refresh Data" : "Обновить данные", + "Refresh Stats" : "Обновить статистику", + "Refresh Workflows" : "Обновить рабочие процессы", + "Refresh dashboard" : "Обновить дашборд", + "Refresh database info" : "Обновить информацию о базе данных", + "Register" : "Реестр", + "Register ID" : "ID реестра", + "Register Statistics" : "Статистика реестра", + "Register Totals" : "Итоги реестра", + "Register depublished successfully" : "Реестр успешно снят с публикации", + "Register not found" : "Реестр не найден", + "Register objects deletion completed successfully" : "Удаление объектов реестра успешно завершено", + "Register published successfully" : "Реестр успешно опубликован", + "Register/Schema Usage" : "Использование реестра/схемы", + "Registers" : "Реестры", + "Reindex" : "Переиндексировать", + "Reindex completed" : "Переиндексация завершена", + "Reindex completed successfully" : "Переиндексация успешно завершена", + "Reindex failed" : "Переиндексация не удалась", + "Relations" : "Связи", + "Remote Version" : "Удалённая версия", + "Remove" : "Удалить", + "Remove filter" : "Удалить фильтр", + "Remove from favorites" : "Удалить из избранного", + "Remove group" : "Удалить группу", + "Removed" : "Удалено", + "Removed from favorites" : "Удалено из избранного", + "Rename Conversation" : "Переименовать переписку", + "Rename conversation" : "Переименовать переписку", + "Replicas" : "Реплики", + "Repository" : "Репозиторий", + "Request Body" : "Тело запроса", + "Request Data" : "Данные запроса", + "Request Details" : "Детали запроса", + "Request body must be JSON with a \"query\" field" : "Тело запроса должно быть JSON с полем \"query\"", + "Request timeout in seconds" : "Тайм-аут запроса в секундах", + "Required field" : "Обязательное поле", + "Required status is inconsistent" : "Статус обязательности несогласован", + "Rerun Search" : "Повторить поиск", + "Reset Changes" : "Сбросить изменения", + "Reset Filters" : "Сбросить фильтры", + "Response Body" : "Тело ответа", + "Response Details" : "Детали ответа", + "Response Time:" : "Время ответа:", + "Restore" : "Восстановить", + "Restore conversation" : "Восстановить переписку", + "Restore or permanently delete items" : "Восстановить или безвозвратно удалить элементы", + "Result Count Range" : "Диапазон количества результатов", + "Results" : "Результаты", + "Retrieve all objects for this schema" : "Получить все объекты для этой схемы", + "Retry" : "Повторить", + "Retry Failed Extractions" : "Повторить неудачные извлечения", + "Retry Policy" : "Политика повтора", + "Retry completed" : "Повтор завершён", + "Retrying..." : "Повтор...", + "Risk Level" : "Уровень риска", + "Roles updated successfully" : "Роли успешно обновлены", + "SOLR Connection Settings" : "Настройки подключения SOLR", + "SOLR Version:" : "Версия SOLR:", + "SOLR actions menu" : "Меню действий SOLR", + "SOLR base path (usually /solr)" : "Базовый путь SOLR (обычно /solr)", + "SOLR is disabled" : "SOLR отключён", + "SOLR is not available or not configured" : "SOLR недоступен или не настроен", + "SOLR logging disabled" : "Логирование SOLR отключено", + "SOLR logging enabled" : "Логирование SOLR включено", + "SOLR search disabled" : "Поиск SOLR отключён", + "SOLR search enabled" : "Поиск SOLR включён", + "SOLR server hostname or IP address" : "Имя хоста или IP-адрес сервера SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Номер порта сервера SOLR (необязательно, по умолчанию 8983)", + "SOLR setup completed successfully" : "Настройка SOLR успешно завершена", + "SOLR setup failed" : "Настройка SOLR не удалась", + "SOLR setup failed - check logs" : "Настройка SOLR не удалась — проверьте журналы", + "Sample Values:" : "Примеры значений:", + "Save" : "Сохранить", + "Save Configuration" : "Сохранить конфигурацию", + "Save Connection Settings" : "Сохранить настройки подключения", + "Save Roles" : "Сохранить роли", + "Save Settings" : "Сохранить настройки", + "Save current search as view" : "Сохранить текущий поиск как представление", + "Saved Views" : "Сохранённые представления", + "Saving..." : "Сохранение...", + "Schema" : "Схема", + "Schema ID" : "ID схемы", + "Schema Name" : "Название схемы", + "Schema Statistics" : "Статистика схемы", + "Schema depublished successfully" : "Схема успешно снята с публикации", + "Schema objects deletion completed successfully" : "Удаление объектов схемы успешно завершено", + "Schema published successfully" : "Схема успешно опубликована", + "Schema successfully created" : "Схема успешно создана", + "Schema successfully updated" : "Схема успешно обновлена", + "Schemas" : "Схемы", + "Scheme" : "Схема", + "Search / Views" : "Поиск / Представления", + "Search Activity" : "Поисковая активность", + "Search Analytics" : "Аналитика поиска", + "Search Entities" : "Поиск объектов", + "Search Files" : "Поиск файлов", + "Search GitHub" : "Поиск на GitHub", + "Search GitLab" : "Поиск на GitLab", + "Search Method" : "Метод поиска", + "Search Term" : "Поисковый запрос", + "Search Trail Details" : "Детали журнала поиска", + "Search Trail Management" : "Управление журналом поиска", + "Search Trail Statistics" : "Статистика журнала поиска", + "Search Trails" : "Журналы поиска", + "Search Views" : "Поиск представлений", + "Search Webhooks" : "Поиск веб-хуков", + "Search by file name or path" : "Поиск по имени файла или пути", + "Search by name or URL" : "Поиск по имени или URL", + "Search by value" : "Поиск по значению", + "Search in Files" : "Поиск в файлах", + "Search in Objects" : "Поиск в объектах", + "Search property names..." : "Поиск имён свойств...", + "Search trail deletion not implemented yet" : "Удаление журнала поиска ещё не реализовано", + "Search views..." : "Поиск представлений...", + "Secret" : "Секрет", + "Secret key for HMAC signature generation (optional)" : "Секретный ключ для генерации подписи HMAC (необязательно)", + "Select AI Agent" : "Выбрать ИИ-агента", + "Select All" : "Выбрать всё", + "Select ConfigSet" : "Выбрать набор конфигурации", + "Select File Types to Vectorize:" : "Выбрать типы файлов для векторизации:", + "Select HTTP method" : "Выбрать метод HTTP", + "Select Register and Schema" : "Выбрать реестр и схему", + "Select Views to Vectorize:" : "Выбрать представления для векторизации:", + "Select a Nextcloud group to add" : "Выбрать группу Nextcloud для добавления", + "Select a branch" : "Выбрать ветку", + "Select a model or type a custom model name" : "Выберите модель или введите пользовательское имя модели", + "Select a repository" : "Выбрать репозиторий", + "Select a repository you have write access to" : "Выберите репозиторий, к которому у вас есть доступ на запись", + "Select an AI Agent" : "Выбрать ИИ-агента", + "Select an AI agent to begin chatting with your data." : "Выберите ИИ-агента для начала общения с вашими данными.", + "Select backend" : "Выбрать backend", + "Select chat model" : "Выбрать модель чата", + "Select collection for files" : "Выбрать коллекцию для файлов", + "Select collection for objects" : "Выбрать коллекцию для объектов", + "Select default organisation" : "Выбрать организацию по умолчанию", + "Select event to listen to..." : "Выбрать событие для прослушивания...", + "Select model" : "Выбрать модель", + "Select options..." : "Выбрать параметры...", + "Select period" : "Выбрать период", + "Select property to send as payload" : "Выбрать свойство для отправки в качестве полезной нагрузки", + "Select provider" : "Выбрать провайдера", + "Select registers and schemas to save a view" : "Выберите реестры и схемы для сохранения представления", + "Select retry policy" : "Выбрать политику повтора", + "Select the branch to publish to" : "Выбрать ветку для публикации", + "Select the event this webhook should listen to" : "Выбрать событие, которое должен прослушивать этот веб-хук", + "Select views to vectorize:" : "Выбрать представления для векторизации:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Выберите группы Nextcloud, доступные для этой организации. Пользователи в этих группах получат доступ к ресурсам организации.", + "Select which data views the AI can search" : "Выбрать представления данных, которые ИИ может искать", + "Select which property from the event should be used as the webhook payload data" : "Выбрать свойство события для использования в качестве данных полезной нагрузки веб-хука", + "Select which tools the AI can use to perform actions" : "Выбрать инструменты, которые ИИ может использовать для выполнения действий", + "Selected" : "Выбрано", + "Selected Groups" : "Выбранные группы", + "Selected audit trails deleted successfully" : "Выбранные записи журнала аудита успешно удалены", + "Selected search trails deleted successfully" : "Выбранные записи журнала поиска успешно удалены", + "Selected users can access this view" : "Выбранные пользователи могут получить доступ к этому представлению", + "Send additional feedback" : "Отправить дополнительный отзыв", + "Send as CloudEvent" : "Отправить как CloudEvent", + "Sensitive PII" : "Конфиденциальные персональные данные", + "Serial Mode (Safer, slower)" : "Последовательный режим (безопаснее, медленнее)", + "Serial:" : "Последовательно:", + "Server Configuration" : "Конфигурация сервера", + "Server Information" : "Информация о сервере", + "Settings" : "Настройки", + "Shards" : "Шарды", + "Share" : "Поделиться", + "Share with Groups" : "Поделиться с группами", + "Share with Users" : "Поделиться с пользователями", + "Show Filters" : "Показать фильтры", + "Show less" : "Показать меньше", + "Show only entries with changes" : "Показывать только записи с изменениями", + "Simple" : "Простой", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Простые запросы: базовый текстовый поиск с минимальными параметрами (например, один поисковый запрос без расширенных фильтров)", + "Size" : "Размер", + "Slug" : "Slug", + "Soft Delete Mode" : "Режим программного удаления", + "Soft Deleted Items" : "Программно удалённые элементы", + "SolrCloud mode enabled" : "Режим SolrCloud включён", + "Source" : "Источник", + "Sources" : "Источники", + "Sources:" : "Источники:", + "Standalone SOLR mode" : "Автономный режим SOLR", + "Start Conversation" : "Начать переписку", + "Start Vectorization" : "Начать векторизацию", + "Start a conversation" : "Начать переписку", + "Starting names cache warmup..." : "Начало прогрева кэша имён...", + "Starting..." : "Запуск...", + "Statistics" : "Статистика", + "Stats calculation not yet implemented" : "Расчёт статистики ещё не реализован", + "Status" : "Статус", + "Status Code" : "Код статуса", + "String Constraints" : "Строковые ограничения", + "Success" : "Успех", + "Success Rate" : "Процент успеха", + "Success Status" : "Статус успеха", + "Successful" : "Успешно", + "Successfully joined organisation" : "Вы успешно присоединились к организации", + "Sync Table" : "Синхронизировать таблицу", + "Synchronization Settings" : "Настройки синхронизации", + "System Default" : "Системное значение по умолчанию", + "Technical Description" : "Техническое описание", + "Technical description for developers and administrators" : "Техническое описание для разработчиков и администраторов", + "Temperature" : "Температура", + "Templates" : "Шаблоны", + "Test" : "Тест", + "Test Chat" : "Тест чата", + "Test Connection" : "Проверить подключение", + "Test Embedding" : "Тест векторных представлений", + "Test endpoint executed successfully" : "Тест конечной точки выполнен успешно", + "Test webhook delivered successfully" : "Тестовый веб-хук успешно доставлен", + "Test webhook delivery failed" : "Доставка тестового веб-хука не удалась", + "Test webhook sent successfully" : "Тестовый веб-хук успешно отправлен", + "Testing..." : "Тестирование...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Текстовые фрагменты создаются в процессе извлечения файлов и сохраняются в базе данных. Векторизация читает эти предварительно разбитые файлы и преобразует их в векторные представления.", + "Text extracted successfully" : "Текст успешно извлечён", + "Text extraction disabled" : "Извлечение текста отключено", + "The URL where webhook events will be sent" : "URL, на который будут отправляться события веб-хука", + "The requested conversation does not exist" : "Запрошенная переписка не существует", + "The saved view and all its search configuration" : "Сохранённое представление и вся его конфигурация поиска", + "There are no audit trail entries matching your current filters." : "Нет записей журнала аудита, соответствующих вашим текущим фильтрам.", + "There are no deleted items matching your current filters." : "Нет удалённых элементов, соответствующих вашим текущим фильтрам.", + "There are no search trail entries matching your current filters." : "Нет записей журнала поиска, соответствующих вашим текущим фильтрам.", + "There are no webhook log entries matching your filters." : "Нет записей журнала веб-хука, соответствующих вашим фильтрам.", + "This action cannot be undone." : "Это действие нельзя отменить.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Это действие нельзя отменить. Убедитесь, что ни одна коллекция не использует этот набор конфигурации.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "У этого агента нет настраиваемых представлений или инструментов, но вы можете изменить настройки RAG ниже.", + "This analysis may take some time" : "Этот анализ может занять некоторое время", + "This audit trail entry does not contain any change information." : "Эта запись журнала аудита не содержит информации об изменениях.", + "This configuration has no data defined." : "Для этой конфигурации не определены данные.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Эта конечная точка устарела. Вместо неё используйте конечные точки на основе фрагментов.", + "This entity has no relations to objects or files" : "Этот объект не имеет связей с объектами или файлами", + "This is a test webhook from OpenRegister" : "Это тестовый веб-хук от OpenRegister", + "This schema must use Magic Table configuration to sync" : "Эта схема должна использовать конфигурацию Magic Table для синхронизации", + "This source has no associated registers." : "Этот источник не имеет связанных реестров.", + "This will copy the _default ConfigSet with the new name" : "Это скопирует набор конфигурации _default с новым именем", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Это безвозвратно удалит ВСЕ векторные представления из базы данных. Вам потребуется повторно векторизовать все объекты и файлы. Это действие нельзя отменить.\\n\\nВы уверены, что хотите продолжить?", + "This will permanently delete:" : "Это безвозвратно удалит:", + "Timeout (seconds)" : "Тайм-аут (секунды)", + "Timestamp" : "Временная метка", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Совет: включайте только те представления, которым нужен семантический поиск, чтобы снизить затраты на векторные представления. Простые таблицы поиска редко нуждаются в векторизации.", + "Title" : "Заголовок", + "To Date" : "Дата окончания", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Для изменения размера фрагмента или стратегии перейдите в Конфигурация файлов → Ограничения обработки.", + "To change the embedding provider or model, go to LLM Configuration." : "Для изменения провайдера или модели векторных представлений перейдите в Настройка LLM.", + "To date" : "По дату", + "Toggle search sidebar" : "Переключить боковую панель поиска", + "Tools" : "Инструменты", + "Top Deleters" : "Наибольшее число удалений", + "Total" : "Итого", + "Total Audit Trails" : "Всего журналов аудита", + "Total Chunks Available:" : "Доступно фрагментов:", + "Total Deleted Items" : "Всего удалённых элементов", + "Total Members" : "Всего участников", + "Total Objects" : "Всего объектов", + "Total Objects in Database:" : "Всего объектов в базе данных:", + "Total Organisations" : "Всего организаций", + "Total Results" : "Всего результатов", + "Total Searches" : "Всего поисков", + "Total Size" : "Общий размер", + "Total Storage" : "Общее хранилище", + "Total:" : "Итого:", + "Totals" : "Итоги", + "Type" : "Тип", + "Type Issue" : "Проблема типа", + "Type Variations:" : "Варианты типа:", + "Type must be \"positive\" or \"negative\"" : "Тип должен быть «positive» или «negative»", + "Type to search for groups" : "Введите для поиска групп", + "Type to search for users" : "Введите для поиска пользователей", + "Type to search groups..." : "Введите для поиска групп...", + "Type to search users..." : "Введите для поиска пользователей...", + "URL" : "URL", + "URL where Ollama is running" : "URL, по которому работает Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Без изменений", + "Unique Search Terms" : "Уникальные поисковые запросы", + "Unique Terms" : "Уникальные термины", + "Unknown" : "Неизвестно", + "Unknown Register" : "Неизвестный реестр", + "Unknown Schema" : "Неизвестная схема", + "Untitled View" : "Представление без названия", + "Update" : "Обновить", + "Update Operations" : "Операции обновления", + "Update configuration: ..." : "Обновить конфигурацию: ...", + "Update register OAS: ..." : "Обновить OAS реестра: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Обновлять векторы при изменении данных объекта (рекомендуется для точного поиска)", + "Updated" : "Обновлено", + "Updated At" : "Обновлено в", + "Updated:" : "Обновлено:", + "Uptime:" : "Время работы:", + "Usage Count" : "Количество использований", + "Use SolrCloud with Zookeeper for distributed search" : "Использовать SolrCloud с Zookeeper для распределённого поиска", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Используйте фильтры для сужения записей журнала аудита по реестру, схеме, типу действия, пользователю, диапазону дат или ID объекта.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Используйте фильтры для сужения удалённых элементов по реестру, схеме, дате удаления или пользователю, который их удалил.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Используйте фильтры для сужения записей журнала поиска по реестру, схеме, статусу успеха, пользователю, диапазону дат, поисковым запросам или метрикам производительности.", + "Used By Collections" : "Используется коллекциями", + "User" : "Пользователь", + "User Agent Statistics" : "Статистика агентов пользователей", + "User Description" : "Описание пользователя", + "User rate limits cleared successfully" : "Ограничения частоты запросов пользователя успешно очищены", + "User removed successfully" : "Пользователь успешно удалён", + "User-friendly description shown in forms and help text" : "Понятное описание, отображаемое в формах и текстах справки", + "Username" : "Имя пользователя", + "Username for SOLR authentication (optional)" : "Имя пользователя для аутентификации SOLR (необязательно)", + "Users" : "Пользователи", + "Using Pre-Generated Chunks" : "Использование предварительно сгенерированных фрагментов", + "Validate" : "Проверить", + "Validate Objects" : "Проверить объекты", + "Validation completed successfully" : "Проверка успешно завершена", + "Validation failed" : "Проверка не удалась", + "Value" : "Значение", + "Vector Dimensions" : "Размерность вектора", + "Vector Search Backend" : "Backend векторного поиска", + "Vector field: _embedding_" : "Векторное поле: _embedding_", + "Vectorization Triggers" : "Триггеры векторизации", + "Vectorize All Files" : "Векторизовать все файлы", + "Vectorize All Objects" : "Векторизовать все объекты", + "Vectorize all file types" : "Векторизовать все типы файлов", + "Vectorize all views" : "Векторизовать все представления", + "Vectorize on object creation" : "Векторизовать при создании объекта", + "Vectors will be stored in your existing object and file collections" : "Векторы будут сохранены в существующих коллекциях объектов и файлов", + "Version" : "Версия", + "Very High" : "Очень высокий", + "View API Docs" : "Просмотр документации API", + "View Changes" : "Просмотр изменений", + "View Details" : "Детали представления", + "View Error" : "Ошибка представления", + "View File" : "Просмотр файла", + "View Full Details" : "Просмотреть полные детали", + "View Logs" : "Просмотр журналов", + "View Name" : "Название представления", + "View Object" : "Просмотр объекта", + "View Parameters" : "Параметры представления", + "View Selection" : "Выбор представления", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Просматривать и анализировать журналы поиска с расширенными возможностями фильтрации и аналитики", + "View and analyze system audit trails with advanced filtering capabilities" : "Просматривать и анализировать системные журналы аудита с расширенными возможностями фильтрации", + "View deleted successfully" : "Представление успешно удалено", + "View entity details and manage relations" : "Просматривать детали объекта и управлять связями", + "View name" : "Название представления", + "View name is required" : "Название представления обязательно", + "View saved successfully!" : "Представление успешно сохранено!", + "View search analytics and manage search logs" : "Просматривать аналитику поиска и управлять журналами поиска", + "View successfully deleted" : "Представление успешно удалено", + "View successfully updated" : "Представление успешно обновлено", + "View updated successfully!" : "Представление успешно обновлено!", + "View webhook delivery logs and filter by webhook" : "Просматривать журналы доставки веб-хуков и фильтровать по веб-хуку", + "Views" : "Представления", + "Visible to users" : "Видно пользователям", + "Wait for Response" : "Ожидать ответа", + "Wait for webhook response before continuing (required for request/response flows)" : "Ожидать ответ веб-хука перед продолжением (обязательно для потоков запрос/ответ)", + "Warmup Names Cache" : "Прогреть кэш имён", + "Warning" : "Предупреждение", + "Warning:" : "Предупреждение:", + "Webhook" : "Веб-хук", + "Webhook Log Details" : "Детали журнала веб-хука", + "Webhook Logs" : "Журналы веб-хука", + "Webhook created successfully" : "Веб-хук успешно создан", + "Webhook deleted" : "Веб-хук удалён", + "Webhook retry delivered successfully" : "Повтор веб-хука успешно доставлен", + "Webhook retry delivery failed" : "Доставка повтора веб-хука не удалась", + "Webhook updated" : "Веб-хук обновлён", + "Webhook updated successfully" : "Веб-хук успешно обновлён", + "Webhooks" : "Веб-хуки", + "Weekly" : "Еженедельно", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Обернуть полезную нагрузку веб-хука в формат CloudEvents для лучшей совместимости", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Да", + "You" : "Вы", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Вы можете создавать новые наборы конфигурации на основе шаблона _default или загружать пользовательские непосредственно на сервер SOLR.", + "You do not have access to this conversation" : "У вас нет доступа к этой переписке", + "You do not have permission to delete this conversation" : "У вас нет разрешения на удаление этой переписки", + "You do not have permission to modify this conversation" : "У вас нет разрешения на изменение этой переписки", + "You do not have permission to restore this conversation" : "У вас нет разрешения на восстановление этой переписки", + "You must be logged in to favorite views" : "Для добавления представлений в избранное необходимо войти в систему", + "You need an AI agent to start a conversation." : "Для начала переписки вам нужен ИИ-агент.", + "Your Fireworks AI API key. Get one at" : "Ваш API-ключ Fireworks AI. Получить по адресу", + "Your OpenAI API key. Get one at" : "Ваш API-ключ OpenAI. Получить по адресу", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Ваш отзыв записан. При желании вы можете предоставить дополнительные сведения здесь...", + "Zookeeper Hosts" : "Хосты Zookeeper", + "Zookeeper Port" : "Порт Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Настройки Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Строка подключения Zookeeper для SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Номер порта Zookeeper (необязательно, по умолчанию 2181)", + "chunk" : "фрагмент", + "chunks" : "фрагменты", + "chunks processed" : "фрагментов обработано", + "configuration(s)" : "конфигурация(ии)", + "e.g., lib/Settings/config.json" : "например, lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "например, lib/Settings/register.json", + "failed" : "не удалось", + "file" : "файл", + "files" : "файлы", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "участники", + "menu or contact someone with permission to create agents." : "меню или обратитесь к кому-то с разрешением на создание агентов.", + "messages" : "сообщения", + "n8n URL and API key are required" : "URL n8n и API-ключ обязательны", + "n8n configuration saved successfully" : "Конфигурация n8n успешно сохранена", + "n8n connection not configured" : "Подключение n8n не настроено", + "n8n connection successful" : "Подключение n8n успешно", + "n8n connection test successful" : "Тест подключения n8n успешен", + "n8n integration disabled" : "Интеграция n8n отключена", + "n8n integration enabled" : "Интеграция n8n включена", + "n8n project initialized successfully" : "Проект n8n успешно инициализирован", + "n8n settings saved successfully" : "Настройки n8n успешно сохранены", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "объекты", + "objects processed" : "объектов обработано", + "or pick a range" : "или выберите диапазон", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "обеспечивает хороший баланс между скоростью и точностью.", + "register(s)" : "реестр(ов)", + "register(s) selected" : "реестр(ов) выбрано", + "required" : "обязательно", + "results" : "результаты", + "schema(s)" : "схема(ем)", + "schema(s) selected" : "схема(ем) выбрано", + "sk-..." : "sk-...", + "this application" : "это приложение", + "to" : "в", + "vectorized" : "векторизовано", + "views selected" : "представлений выбрано", + "ℹ️ Current Configuration" : "ℹ️ Текущая конфигурация", + "⚡ Batch Processing" : "⚡ Пакетная обработка", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Полный контекст ✓ Меньше шансов пропустить детали ✗ Медленные ответы ✗ Может включать менее релевантную информацию", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Быстрые ответы ✓ Более сфокусированные ответы ✗ Может упустить релевантную информацию", + "✨ AI Features" : "✨ Функции ИИ", + "All search trails cleared successfully" : "Все журналы поиска успешно очищены", + "Multiple search trail deletion not implemented yet" : "Множественное удаление журналов поиска ещё не реализовано", + "No expired search trails found to clear" : "Истёкшие записи журнала поиска для очистки не найдены", + "Missing message" : "Отсутствует сообщение", + "message content is required" : "содержимое сообщения обязательно", + "Missing conversation or agentUuid" : "Отсутствует переписка или agentUuid", + "Access denied" : "Доступ запрещён", + "Conversation not found" : "Переписка не найдена", + "AI service not configured" : "Сервис ИИ не настроен", + "Failed to process message" : "Не удалось обработать сообщение", + "Missing conversationId" : "Отсутствует conversationId", + "conversationId is required" : "conversationId обязателен", + "Failed to fetch conversation history" : "Не удалось получить историю переписки", + "Failed to clear conversation" : "Не удалось очистить переписку", + "Invalid feedback type" : "Недопустимый тип отзыва", + "type must be \"positive\" or \"negative\"" : "тип должен быть «positive» или «negative»", + "Message not found" : "Сообщение не найдено", + "Failed to save feedback" : "Не удалось сохранить отзыв", + "Failed to get chat statistics" : "Не удалось получить статистику чата", + "Not Found" : "Не найдено", + "Engine not found" : "Движок не найден", + "Consumer not found" : "Потребитель не найден", + "Backend parameter is required" : "Параметр backend обязателен", + "Failed to get database information: %s" : "Не удалось получить информацию о базе данных: %s", + "SOLR setup error: %s" : "Ошибка настройки SOLR: %s", + "Reindex failed: %s" : "Переиндексация не удалась: %s", + "Anonymized" : "Анонимизировано", + "Entities detected" : "Объекты обнаружены", + "Extract Now" : "Извлечь сейчас", + "Extracted at" : "Извлечено в", + "Extraction" : "Извлечение", + "Failed to load extraction data" : "Не удалось загрузить данные извлечения", + "Failed to load register data" : "Не удалось загрузить данные реестра", + "Failed to retrieve extraction status." : "Не удалось получить статус извлечения.", + "Failed to retrieve objects for file." : "Не удалось получить объекты для файла.", + "No extraction data available for this file" : "Данные извлечения для этого файла недоступны", + "No register objects reference this file" : "Ни один объект реестра не ссылается на этот файл", + "Not extracted" : "Не извлечено", + "Register Objects" : "Объекты реестра", + "Risk level" : "Уровень риска", + "Text chunks" : "Текстовые фрагменты", + "Unknown error" : "Неизвестная ошибка", + "Very high" : "Очень высокий", + "{title} in {register} / {schema}" : "{title} в {register} / {schema}", + "Push Notifications" : "Push-уведомления", + "Real-time push notification status via notify_push" : "Статус push-уведомлений в реальном времени через notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push в реальном времени недоступен — приложение notify_push не установлено", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Установите приложение notify_push из магазина приложений Nextcloud для включения обновлений в реальном времени.", + "Open Nextcloud App Store" : "Открыть магазин приложений Nextcloud", + "notify_push is installed but not yet active" : "notify_push установлен, но ещё не активен", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push установлен, но OpenRegister ещё не подтвердил успешный push. Активируйте сохранение объекта или проверьте конфигурацию notify_push.", + "notify_push configuration guide" : "Руководство по настройке notify_push", + "Realtime push active" : "Push в реальном времени активен", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push-уведомления в реальном времени активны. Подключённые клиенты получают мгновенные обновления при создании, обновлении или удалении объектов.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +); diff --git a/l10n/ru.json b/l10n/ru.json new file mode 100644 index 0000000000..481a0adf92 --- /dev/null +++ b/l10n/ru.json @@ -0,0 +1,2670 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(требуется Dolphin)", + "(no subject)": "(без темы)", + "(no title)": "(без названия)", + "(unnamed)": "(без имени)", + "(untitled card)": "(карточка без названия)", + "({count} selected)": "({count} выбрано)", + ", {failed} failed": ", {failed} неудачно", + "0 = unlimited": "0 = неограниченно", + "A deactivation request is pending.": "Запрос на деактивацию ожидает обработки.", + "AI Assistant": "ИИ-помощник", + "AI Chat": "ИИ-чат", + "AI service not configured": "Сервис ИИ не настроен", + "API Key": "API-ключ", + "API Request Quota (requests/day)": "Квота запросов API (запросов/день)", + "API Token Configuration": "Настройка API-токенов", + "API Tokens": "API-токены", + "API tokens saved successfully": "API-токены успешно сохранены", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Ст. 30 §4: в этом отчёте каждая операция обработки связывается с общим количеством записей журнала аудита. Используется аудиторами и Autoriteit Persoonsgegevens для надзорной проверки.", + "About ConfigSets": "О наборах конфигурации (ConfigSets)", + "Access denied": "Доступ запрещён", + "Access organisation dashboard and analytics": "Доступ к дашборду и аналитике организации", + "Account": "Аккаунт", + "Action": "Действие", + "Action Distribution": "Распределение действий", + "Action:": "Действие:", + "Actions": "Действия", + "Active": "Активный", + "Active Collections": "Активные коллекции", + "Active Filters": "Активные фильтры", + "Active Organisation:": "Активная организация:", + "Active Organisations": "Активные организации", + "Active filters:": "Активные фильтры:", + "Active organisation changed successfully": "Активная организация успешно изменена", + "Active organisation set successfully": "Активная организация успешно установлена", + "Activities": "Действия", + "Activity": "Активность", + "Activity Period": "Период активности", + "Add": "Добавить", + "Add Attachment": "Добавить вложение", + "Add Endpoint": "Добавить конечную точку", + "Add Groups": "Добавить группы", + "Add Property to '{title}'": "Добавить свойство к '{title}'", + "Add Register": "Добавить реестр", + "Add Schema": "Добавить схему", + "Add Source": "Добавить источник", + "Add User": "Добавить пользователя", + "Add User to Organisation": "Добавить пользователя в организацию", + "Add a contact from any of your address books to associate it with this object.": "Добавьте контакт из любой адресной книги для связи с этим объектом.", + "Add a file or files": "Добавить файл или файлы", + "Add card": "Добавить карточку", + "Add contact": "Добавить контакт", + "Add endpoint": "Добавить конечную точку", + "Add oneOf entry": "Добавить запись oneOf", + "Add range": "Добавить диапазон", + "Add schema titles, descriptions, and register information to provide richer context for search": "Добавьте заголовки, описания схем и информацию реестра для более богатого контекста поиска", + "Add to favorites": "Добавить в избранное", + "Added": "Добавлено", + "Added to favorites": "Добавлено в избранное", + "Additional Information": "Дополнительная информация", + "Additional feedback saved. Thank you!": "Дополнительный отзыв сохранён. Спасибо!", + "Addresses": "Адреса", + "Admin": "Администратор", + "Advanced": "Дополнительно", + "Advanced Filters": "Расширенные фильтры", + "Advanced Options": "Расширенные параметры", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Расширенные фильтры недоступны при использовании источника базы данных. Переключитесь на Auto или SOLR Index для использования фильтров.", + "After": "После", + "Agent Type": "Тип агента", + "Agent deleted successfully": "Агент успешно удалён", + "Agent successfully created": "Агент успешно создан", + "Agent successfully updated": "Агент успешно обновлён", + "Agents": "Агенты", + "Aggregated across schemas": "Агрегировано по схемам", + "All": "Все", + "All Categories": "Все категории", + "All Collections": "Все коллекции", + "All Confidence Levels": "Все уровни достоверности", + "All ConfigSets": "Все наборы конфигурации", + "All Files": "Все файлы", + "All Levels": "Все уровни", + "All Types": "Все типы", + "All Webhooks": "Все веб-хуки", + "All actions": "Все действия", + "All audit trails cleared successfully": "Все журналы аудита успешно очищены", + "All clear": "Всё в порядке", + "All file types": "Все типы файлов", + "All registers": "Все реестры", + "All schemas": "Все схемы", + "All search trails cleared successfully": "Все журналы поиска успешно очищены", + "All searches": "Все поисковые запросы", + "All users": "Все пользователи", + "All-day events": "События на весь день", + "Allowed MIME Types": "Разрешённые типы MIME", + "Already linked": "Уже связано", + "An error occurred while clearing audit trails": "Произошла ошибка при очистке журналов аудита", + "An error occurred while deleting the audit trail": "Произошла ошибка при удалении записи журнала аудита", + "An error occurred while deleting the object": "Произошла ошибка при удалении объекта", + "An error occurred while deleting the view": "Произошла ошибка при удалении представления", + "An error occurred while permanently deleting the objects": "Произошла ошибка при безвозвратном удалении объектов", + "An error occurred while restoring the objects": "Произошла ошибка при восстановлении объектов", + "An error occurred while saving the organisation": "Произошла ошибка при сохранении организации", + "Analysis completed:": "Анализ завершён:", + "Analysis steps:": "Шаги анализа:", + "Analytics": "Аналитика", + "Analyze Objects": "Анализировать объекты", + "Analyze Properties": "Анализировать свойства", + "Analyze existing properties for improvement opportunities": "Анализировать существующие свойства для поиска возможностей улучшения", + "Analyzing...": "Анализ...", + "Anonymized": "Анонимизировано", + "Anonymous Group": "Анонимная группа", + "Any favorites and sharing settings for this view": "Все настройки избранного и совместного доступа для этого представления", + "Any user": "Любой пользователь", + "App ID": "ID приложения", + "App store cache invalidated successfully": "Кэш магазина приложений успешно аннулирован", + "App store cache invalidated: {invalidated}": "Кэш магазина приложений аннулирован: {invalidated}", + "Application deleted successfully": "Приложение успешно удалено", + "Application identifier for this configuration (optional)": "Идентификатор приложения для этой конфигурации (необязательно)", + "Application settings": "Настройки приложения", + "Application successfully created": "Приложение успешно создано", + "Application successfully updated": "Приложение успешно обновлено", + "Applications": "Приложения", + "Apply": "Применить", + "Apply Changes": "Применить изменения", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Применить роль ко всем схемам в этом реестре, у которых нет явных переопределений авторизации.", + "Approval Progress": "Ход согласования", + "Approve": "Одобрить", + "Approved": "Одобрено", + "Archive": "Архив", + "Archive conversation": "Архивировать переписку", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Архивировать эту verwerkingsactiviteit? Строки журнала аудита будут по-прежнему ссылаться на неё.", + "Archived conversations are hidden from your active list": "Архивированные переписки скрыты из активного списка", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Вы уверены, что хотите УДАЛИТЬ коллекцию \"{name}\"?\n\nЭто действие:\n• Безвозвратно удалит коллекцию и все её данные\n• Удалит все проиндексированные документы\n• Нельзя отменить", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Вы уверены, что хотите очистить старые журналы поиска? Это удалит записи старше 30 дней.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Вы уверены, что хотите очистить все данные из коллекции \"{name}\"?\n\nЭто действие:\n• Удалит все проиндексированные документы\n• Сохранит структуру коллекции\n• Нельзя отменить", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Вы уверены, что хотите удалить приложение \"{name}\"? Это действие нельзя отменить.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Вы уверены, что хотите удалить выбранные записи журнала аудита? Это действие нельзя отменить.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Вы уверены, что хотите удалить выбранные записи журнала поиска? Это действие нельзя отменить.", + "Are you sure you want to delete this ConfigSet?": "Вы уверены, что хотите удалить этот набор конфигурации?", + "Are you sure you want to delete this configset?": "Вы уверены, что хотите удалить этот набор конфигурации?", + "Are you sure you want to leave '{name}'?": "Вы уверены, что хотите покинуть '{name}'?", + "Are you sure you want to permanently delete": "Вы уверены, что хотите безвозвратно удалить", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Вы уверены, что хотите переиндексировать коллекцию \"{name}\"?\n\nЭто действие:\n• Перестроит индекс со всеми объектами\n• Займёт несколько минут\n• Может повлиять на производительность поиска во время переиндексации", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Вы уверены, что хотите удалить схему \"{schema}\" из реестра \"{register}\"? Это действие нельзя отменить.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Вы уверены, что хотите программно удалить {count} активных объектов схемы \"{schema}\"?\n\nОни будут помечены как удалённые, но могут быть безвозвратно удалены позже.", + "Array Object Configuration:": "Конфигурация объекта массива:", + "Articles": "Статьи", + "Ask a question...": "Задайте вопрос...", + "Ask questions about your data using natural language": "Задавайте вопросы о ваших данных на естественном языке", + "Assigned collaborative tags": "Назначенные совместные теги", + "Assignment notifications": "Уведомления о назначениях", + "Attachment added to {name}": "Вложение добавлено к {name}", + "Attempt": "Попытка", + "Audit Trail Changes": "Изменения в журнале аудита", + "Audit Trail Details": "Детали записи журнала аудита", + "Audit Trail Management": "Управление журналом аудита", + "Audit Trail Statistics": "Статистика журнала аудита", + "Audit Trails": "Журналы аудита", + "Audit trail data copied to clipboard": "Данные журнала аудита скопированы в буфер обмена", + "Audit trail deleted successfully": "Запись журнала аудита успешно удалена", + "Audit trail successfully deleted": "Запись журнала аудита успешно удалена", + "Authentication": "Аутентификация", + "Auto (e.g. Y for year, F Y for month)": "Авто (например, Y для года, F Y для месяца)", + "Auto share": "Автоматический общий доступ", + "Auto-commit disabled": "Автоматическая фиксация отключена", + "Auto-commit enabled": "Автоматическая фиксация включена", + "Auto-create Default Organisation": "Автоматически создавать организацию по умолчанию", + "Auto-retry failed vectorizations": "Автоматически повторять неудачные векторизации", + "Automatically commit changes to SOLR index": "Автоматически фиксировать изменения в индексе SOLR", + "Automatically connect to the federation network.": "Автоматически подключаться к сети федерации.", + "Automatically create a default organisation if none exists when the app is initialized": "Автоматически создавать организацию по умолчанию, если она отсутствует при инициализации приложения", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Автоматически создавать векторные представления из текстовых фрагментов при загрузке и обработке файлов", + "Automatically generate vector embeddings when objects are created or updated": "Автоматически создавать векторные представления при создании или обновлении объектов", + "Automatically retry failed vectorization attempts (max 3 retries)": "Автоматически повторять неудачные попытки векторизации (максимум 3 попытки)", + "Automatically update catalogues.": "Автоматически обновлять каталоги.", + "Available Filters": "Доступные фильтры", + "Available Workflows": "Доступные рабочие процессы", + "Available placeholders:": "Доступные заполнители:", + "Avatar": "Аватар", + "Avatar changes are not supported by your authentication provider.": "Ваш провайдер аутентификации не поддерживает изменение аватара.", + "Avatar removed": "Аватар удалён", + "Avatar updated successfully": "Аватар успешно обновлён", + "Avg Execution Time": "Среднее время выполнения", + "Avg Members/Org": "Среднее число участников/организация", + "Avg Object Views/Session": "Среднее количество просмотров объектов/сессия", + "Avg Response Time": "Среднее время ответа", + "Avg Results/Search": "Среднее число результатов/поиск", + "Avg Searches/Session": "Среднее число поисков/сессия", + "Back": "Назад", + "Back to Entities": "Назад к объектам", + "Back to Registers": "Назад к реестрам", + "Back to Webhooks": "Назад к веб-хукам", + "Back to applications": "Назад к приложениям", + "Back to entities": "Назад к объектам", + "Backend parameter is required": "Параметр backend обязателен", + "Backend updated successfully. Please reload the application.": "Backend успешно обновлён. Пожалуйста, перезагрузите приложение.", + "Bandwidth Quota (MB/month)": "Квота пропускной способности (МБ/месяц)", + "Base URL (Optional)": "Базовый URL (необязательно)", + "Basic Information": "Основная информация", + "Batch Size": "Размер пакета", + "Batch extraction completed": "Пакетное извлечение завершено", + "Before": "До", + "Before object vectorization can work:": "Перед тем как начать векторизацию объектов:", + "Behavior": "Поведение", + "Behavior Issue": "Проблема поведения", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (длительность ISO-8601, например P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Хранилище Blob устарело. Все объекты теперь используют magic tables.", + "Bookmarks": "Закладки", + "Branch": "Ветка", + "Bulk Role Assignment: {title}": "Массовое назначение ролей: {title}", + "Bulk delete operation completed successfully": "Операция массового удаления успешно завершена", + "Bulk save operation completed successfully": "Операция массового сохранения успешно завершена", + "Business Data": "Бизнес-данные", + "CSV": "CSV", + "Cache cleared successfully": "Кэш успешно очищен", + "Cache grew from {old} to {new} entries.": "Кэш вырос с {old} до {new} записей.", + "Cache shrunk from {old} to {new} entries.": "Кэш сократился с {old} до {new} записей.", + "Cache stayed the same at {size} entries.": "Кэш остался без изменений: {size} записей.", + "Calculate Sizes": "Вычислить размеры", + "Calendar": "Календарь", + "Calendar Provider Configuration": "Настройка провайдера календаря", + "Calendar integration is not available": "Интеграция с календарём недоступна", + "Calendar name": "Название календаря", + "Cancel": "Отмена", + "Cancel deactivation request": "Отменить запрос на деактивацию", + "Cannot delete: objects are still attached": "Невозможно удалить: объекты всё ещё прикреплены", + "Cannot edit: This register is managed by external configuration {title}": "Невозможно редактировать: этот реестр управляется внешней конфигурацией {title}", + "Cannot remove schema with existing objects ({count} objects)": "Невозможно удалить схему с существующими объектами ({count} объектов)", + "Cannot remove schema {schema} because it contains {count} objects": "Невозможно удалить схему {schema}, так как она содержит {count} объектов", + "Cards": "Карточки", + "Cascade delete": "Каскадное удаление", + "Categories of data subjects (one per line)": "Категории субъектов данных (по одной на строку)", + "Categories of personal data (one per line)": "Категории персональных данных (по одной на строку)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Категория", + "Change Type": "Тип изменения", + "Change password": "Изменить пароль", + "Changes": "Изменения", + "Changes copied to clipboard": "Изменения скопированы в буфер обмена", + "Changes for Audit Trail #{id}": "Изменения для записи журнала аудита #{id}", + "Chat Model": "Модель чата", + "Chat Provider": "Провайдер чата", + "Chat Provider (RAG)": "Провайдер чата (RAG)", + "Chat Settings": "Настройки чата", + "Chat provider connection successful!": "Подключение к провайдеру чата успешно!", + "Chat settings": "Настройки чата", + "Chat test failed: {error}": "Тест чата не удался: {error}", + "Choose a register": "Выберите реестр", + "Choose a schema": "Выберите схему", + "Choose how vector similarity calculations are performed for semantic search": "Выберите метод вычисления векторного сходства для семантического поиска", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Выберите типы файлов для включения в процесс векторизации. Будут обработаны только файлы с извлечённым текстом и фрагментами.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Выберите представления для включения в процесс векторизации. Оставьте пустым для обработки всех представлений согласно вашей конфигурации.", + "Chunk": "Фрагмент", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Удаление фрагментов ещё не реализовано. Используйте конечные точки на основе фрагментов.", + "Chunks": "Фрагменты", + "Chunks to Vectorize:": "Фрагменты для векторизации:", + "Cleanup Old Trails": "Очистить старые журналы", + "Cleanup completed": "Очистка завершена", + "Cleanup completed successfully. Deleted {count} entries.": "Очистка успешно завершена. Удалено {count} записей.", + "Cleanup failed: {error}": "Ошибка очистки: {error}", + "Clear All Embeddings": "Очистить все векторные представления", + "Clear Entries": "Очистить записи", + "Clear Filtered Audit Trails": "Очистить отфильтрованные журналы аудита", + "Clear Filters": "Сбросить фильтры", + "Clear Index": "Очистить индекс", + "Clear Selection": "Снять выделение", + "Clear all": "Очистить всё", + "Clear all filters": "Сбросить все фильтры", + "Clear date range": "Очистить диапазон дат", + "Clear filters": "Сбросить фильтры", + "Clearing...": "Очистка...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Нажмите «Запустить проверку соответствия», чтобы найти схемы, в которых обнаружены персональные данные, но нет аннотации об операции обработки.", + "Click to subscribe to notifications": "Нажмите, чтобы подписаться на уведомления", + "Click to unsubscribe from notifications": "Нажмите, чтобы отписаться от уведомлений", + "Close": "Закрыть", + "Cluster name": "Имя кластера", + "Code": "Код", + "Code (short readable key, e.g. v-2026-001)": "Код (короткий читаемый ключ, например v-2026-001)", + "Collapse details": "Свернуть детали", + "Collection Management": "Управление коллекциями", + "Collection Name": "Название коллекции", + "Collection assignments updated successfully": "Назначения коллекции успешно обновлены", + "Collection cleared successfully": "Коллекция успешно очищена", + "Collection copied successfully": "Коллекция успешно скопирована", + "Collection created successfully": "Коллекция успешно создана", + "Collection deleted successfully": "Коллекция успешно удалена", + "Collection used to store and index file metadata and content": "Коллекция для хранения и индексации метаданных и содержимого файлов", + "Collection used to store and index object data": "Коллекция для хранения и индексации данных объектов", + "Collections:": "Коллекции:", + "Color": "Цвет", + "Comment...": "Комментарий...", + "Commit Message": "Сообщение коммита", + "Commit Within (ms)": "Фиксация в течение (мс)", + "Commit message": "Сообщение коммита", + "Compare current schema with real object data": "Сравнить текущую схему с реальными данными объектов", + "Completed": "Завершено", + "Complex": "Сложный", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Сложные запросы: расширенный поиск с несколькими фильтрами, операторами и сложными комбинациями параметров", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Сложные запросы: расширенный поиск с несколькими фильтрами, операторами и сложными комбинациями параметров", + "Compliance": "Соответствие требованиям", + "Confidence Level": "Уровень достоверности", + "Confidence Score": "Оценка достоверности", + "Confidence: {confidence}%": "Достоверность: {confidence}%", + "Config must be provided as an object": "Конфигурация должна быть предоставлена как объект", + "ConfigSet": "Набор конфигурации", + "ConfigSet Management": "Управление наборами конфигурации", + "ConfigSet Name": "Название набора конфигурации", + "ConfigSet created successfully": "Набор конфигурации успешно создан", + "ConfigSet deleted successfully": "Набор конфигурации успешно удалён", + "ConfigSet:": "Набор конфигурации:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Наборы конфигурации определяют схему и настройки для ваших коллекций SOLR. Они содержат определения полей, анализаторы и другие настройки поиска.", + "Configuration": "Конфигурация", + "Configuration Data": "Данные конфигурации", + "Configuration is up to date": "Конфигурация актуальна", + "Configuration saved": "Конфигурация сохранена", + "Configuration saved successfully.": "Конфигурация успешно сохранена.", + "Configuration type (default, application, etc.)": "Тип конфигурации (по умолчанию, приложение и т.д.)", + "Configurations": "Конфигурации", + "Configure API tokens for external service integrations": "Настроить API-токены для интеграций с внешними сервисами", + "Configure Apache SOLR search engine for advanced search capabilities": "Настроить поисковой движок Apache SOLR для расширенных возможностей поиска", + "Configure Facets": "Настроить фасеты", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Настроить провайдеров больших языковых моделей (LLM) для функций на основе ИИ, включая семантический поиск, векторные представления и чат.", + "Configure SOLR Facets": "Настроить фасеты SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Настроить основные параметры подключения к серверу SOLR, включая аутентификацию и сетевые параметры. Используйте отдельные диалоги управления наборами конфигурации и коллекциями для управления ядрами и коллекциями.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Настроить основные параметры подключения к серверу SOLR, включая аутентификацию и сетевые параметры. Используйте отдельные диалоги управления наборами конфигурации и коллекциями для управления ядрами и коллекциями.", + "Configure default organisation and organisation-related settings": "Настроить организацию по умолчанию и связанные настройки", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Настроить преобразование объектов базы данных в векторные представления для семантического поиска. Объекты векторизуются напрямую без необходимости извлечения текста.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Настроить преобразование объектов в текст перед векторизацией. Эти настройки влияют на качество поиска и контекст.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Настроить провайдеров больших языковых моделей (LLM) для функций на основе ИИ, включая семантический поиск, векторные представления и чат.", + "Configure multi-organization support and tenant isolation": "Настроить поддержку нескольких организаций и изоляцию арендаторов", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Настроить параметры векторизации файлов. Этот процесс создаст векторные представления для всех извлечённых фрагментов файлов.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Настроить параметры векторизации объектов. Этот процесс создаст векторные представления для всех объектов, соответствующих фильтрам представления.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Настроить эту схему для отображения объектов как событий в приложении Nextcloud Календарь.", + "Configure which types of data to search and how many sources to retrieve": "Настроить типы данных для поиска и количество извлекаемых источников", + "Configuring field mappings...": "Настройка сопоставления полей...", + "Confirm Account Deactivation": "Подтвердить деактивацию аккаунта", + "Confirm Object Deletion": "Подтвердить удаление объекта", + "Confirm Object Restoration": "Подтвердить восстановление объекта", + "Confirm Permanent Object Deletion": "Подтвердить безвозвратное удаление объекта", + "Confirm deactivation": "Подтвердить деактивацию", + "Connecting to SOLR cluster...": "Подключение к кластеру SOLR...", + "Connection Failed": "Подключение не удалось", + "Connection Settings": "Настройки подключения", + "Connection Successful!": "Подключение успешно!", + "Connection failed": "Подключение не удалось", + "Connection protocol": "Протокол подключения", + "Connection successful": "Подключение успешно", + "Connection timeout in seconds": "Тайм-аут подключения в секундах", + "Constant delay between retries (5 minutes)": "Постоянная задержка между повторными попытками (5 минут)", + "Constraint Issue": "Проблема ограничения", + "Consumer not found": "Потребитель не найден", + "Contacts": "Контакты", + "Contacts integration is not available": "Интеграция с контактами недоступна", + "Content Filters": "Фильтры содержимого", + "Continue to Properties": "Перейти к свойствам", + "Contracts": "Контракты", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Управляйте тем, какие представления объектов следует векторизовать, чтобы снизить расходы на API. Векторизуйте только те представления, которые выигрывают от семантического поиска.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Управляйте тем, какие представления и инструменты ИИ может использовать в этой переписке. По умолчанию все возможности агента включены.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Управляет случайностью (0 = детерминировано, 2 = очень творчески)", + "Conversation ID is required": "Требуется ID переписки", + "Conversation archived": "Переписка архивирована", + "Conversation archived successfully": "Переписка успешно архивирована", + "Conversation cleared successfully": "Переписка успешно очищена", + "Conversation deleted": "Переписка удалена", + "Conversation not found": "Переписка не найдена", + "Conversation permanently deleted": "Переписка безвозвратно удалена", + "Conversation renamed": "Переписка переименована", + "Conversation restored": "Переписка восстановлена", + "Conversation started with {agent}": "Переписка начата с {agent}", + "Conversation title": "Название переписки", + "Conversations": "Переписки", + "Converting {schema} to blob storage...": "Конвертация {schema} в blob-хранилище...", + "Converting {schema} to magic table...": "Конвертация {schema} в magic table...", + "Copied!": "Скопировано!", + "Copy": "Копировать", + "Copy Changes": "Копировать изменения", + "Copy Collection": "Копировать коллекцию", + "Copy Data": "Копировать данные", + "Copy Full Data": "Копировать все данные", + "Copy of {name}": "Копия {name}", + "Copy to clipboard": "Копировать в буфер обмена", + "Copying...": "Копирование...", + "Costs": "Затраты", + "Count": "Количество", + "Counting objects...": "Подсчёт объектов...", + "Create": "Создать", + "Create API Token": "Создать API-токен", + "Create Agent": "Создать агента", + "Create Application": "Создать приложение", + "Create Collection": "Создать коллекцию", + "Create ConfigSet": "Создать набор конфигурации", + "Create New Collection": "Создать новую коллекцию", + "Create New ConfigSet": "Создать новый набор конфигурации", + "Create Operations": "Операции создания", + "Create Organisation": "Создать организацию", + "Create Webhook": "Создать веб-хук", + "Create a copy of collection:": "Создать копию коллекции:", + "Create a new ConfigSet based on the _default template": "Создать новый набор конфигурации на основе шаблона _default", + "Create a new SOLR collection from an existing ConfigSet": "Создать новую коллекцию SOLR из существующего набора конфигурации", + "Create a new SOLR collection from an existing configset": "Создать новую коллекцию SOLR из существующего набора конфигурации", + "Create a new configset based on the _default template": "Создать новый набор конфигурации на основе шаблона _default", + "Create a new event or link an existing one from any of your calendars.": "Создайте новое событие или свяжите существующее из любого из ваших календарей.", + "Create another": "Создать ещё", + "Create copies of": "Создать копии", + "Create event": "Создать событие", + "Create new token": "Создать новый токен", + "Create or link a Deck card to track work on this object.": "Создайте или свяжите карточку Deck для отслеживания работы с этим объектом.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Создайте первую verwerkingsactiviteit, чтобы начать помечать строки журнала аудита их атрибуцией по AVG Ст. 30.", + "Create your first AI agent to get started.": "Создайте своего первого ИИ-агента для начала работы.", + "Created": "Создано", + "Created:": "Создано:", + "Creating search collections...": "Создание коллекций для поиска...", + "Creating...": "Создание...", + "Current": "Текущий", + "Current Filters": "Текущие фильтры", + "Current Type:": "Текущий тип:", + "Current password": "Текущий пароль", + "Custom API endpoint if using a different region": "Пользовательская конечная точка API для другого региона", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Пользовательские HTTP-заголовки (по одному на строку, формат: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Пользовательские HTTP-заголовки (по одному на строку, формат: header-name: value)", + "Custom Ranges:": "Пользовательские диапазоны:", + "Custom display title for this facet": "Пользовательский заголовок для этого фасета", + "Custom pattern:": "Пользовательский шаблон:", + "DELETE COLLECTION": "УДАЛИТЬ КОЛЛЕКЦИЮ", + "DSAR": "DSAR", + "Daily": "Ежедневно", + "Dashboard": "Дашборд", + "Dashboard not found": "Дашборд не найден", + "Data": "Данные", + "Data Source": "Источник данных", + "Data Sources": "Источники данных", + "Data Views": "Представления данных", + "Data sources": "Источники данных", + "Data type does not match observed values": "Тип данных не соответствует наблюдаемым значениям", + "Data-subject access request": "Запрос на доступ субъекта данных", + "Database URL": "URL базы данных", + "Database information refreshed": "Информация о базе данных обновлена", + "Date Faceting:": "Фасетирование по дате:", + "Date Range": "Диапазон дат", + "Dates": "Даты", + "Deactivation request cancelled": "Запрос на деактивацию отменён", + "Deactivation request submitted": "Запрос на деактивацию отправлен", + "Deck": "Deck", + "Deck integration is not available": "Интеграция с Deck недоступна", + "Default": "По умолчанию", + "Default Object Tenant": "Арендатор объекта по умолчанию", + "Default Organisation": "Организация по умолчанию", + "Default Owner": "Владелец по умолчанию", + "Default User (for cron/background jobs)": "Пользователь по умолчанию (для cron/фоновых задач)", + "Default User Tenant": "Арендатор пользователя по умолчанию", + "Default value": "Значение по умолчанию", + "Default value (Email)": "Значение по умолчанию (Email)", + "Default value (Password)": "Значение по умолчанию (Пароль)", + "Default value (Phone number)": "Значение по умолчанию (Номер телефона)", + "Default value (Regex)": "Значение по умолчанию (Regex)", + "Default: 5": "По умолчанию: 5", + "Defaults to current user. Select a different user if needed.": "По умолчанию — текущий пользователь. При необходимости выберите другого.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Задержка удваивается с каждой попыткой (2, 4, 8 минут...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Задержка увеличивается линейно (5, 10, 15 минут...)", + "Delete": "Удалить", + "Delete ({count})": "Удалить ({count})", + "Delete Application": "Удалить приложение", + "Delete Audit Trail": "Удалить запись журнала аудита", + "Delete Collection": "Удалить коллекцию", + "Delete ConfigSet": "Удалить набор конфигурации", + "Delete Objects": "Удалить объекты", + "Delete Operations": "Операции удаления", + "Delete SOLR Collection": "Удалить коллекцию SOLR", + "Delete View": "Удалить представление", + "Delete all objects in this schema": "Удалить все объекты в этой схеме", + "Delete permanently": "Удалить навсегда", + "Delete this conversation?": "Удалить эту переписку?", + "Delete view": "Удалить представление", + "Delete {count} object": "Удалить {count} объект", + "Delete {count} objects": "Удалить {count} объектов", + "Deleted": "Удалено", + "Deleted By": "Удалено пользователем", + "Deleted Date": "Дата удаления", + "Deleted Items Management": "Управление удалёнными элементами", + "Deleted This Week": "Удалено на этой неделе", + "Deleted Today": "Удалено сегодня", + "Deleted:": "Удалено:", + "Deleting SOLR Collection...": "Удаление коллекции SOLR...", + "Deleting...": "Удаление...", + "Deletion Date Range": "Диапазон дат удаления", + "Deletion Statistics": "Статистика удалений", + "Deprecated": "Устаревший", + "Depublish": "Снять с публикации", + "Description": "Описание", + "Description Template": "Шаблон описания", + "Description shown as tooltip": "Описание, отображаемое как подсказка", + "Description:": "Описание:", + "Detect data types and patterns": "Определить типы данных и шаблоны", + "Detected At": "Обнаружено в", + "Detected Format:": "Обнаруженный формат:", + "Detected Issues:": "Обнаруженные проблемы:", + "Detected: {date}": "Обнаружено: {date}", + "Direct permission": "Прямое разрешение", + "Disable": "Отключить", + "Disable this to make the agent": "Отключите это, чтобы агент", + "Disabled": "Отключено", + "Discover Files": "Обнаружить файлы", + "Discovered Properties": "Обнаруженные свойства", + "Display Format": "Формат отображения", + "Display Name": "Отображаемое имя", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Вы хотите безвозвратно удалить все отфильтрованные записи журнала аудита? Это действие нельзя отменить.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Вы хотите безвозвратно удалить эту запись журнала аудита? Это действие нельзя отменить.", + "Documents": "Документы", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR включён", + "Download API Spec": "Скачать спецификацию API", + "Download API Specification": "Скачать спецификацию API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Скачать копию всех ваших персональных данных, хранящихся в OpenRegister (GDPR Статья 20).", + "Drag a file or files here": "Перетащите файл или файлы сюда", + "Dry run completed: {count} fields would be created": "Тестовый запуск завершён: будет создано {count} полей", + "Dry run completed: {count} fields would be fixed": "Тестовый запуск завершён: будет исправлено {count} полей", + "Duration in seconds (optional)": "Длительность в секундах (необязательно)", + "Edit": "Редактировать", + "Edit Agent": "Редактировать агента", + "Edit Application": "Редактировать приложение", + "Edit Configuration": "Редактировать конфигурацию", + "Edit Endpoint": "Редактировать конечную точку", + "Edit Organisation": "Редактировать организацию", + "Edit Property '{key}' of '{title}'": "Редактировать свойство '{key}' объекта '{title}'", + "Edit Register": "Редактировать реестр", + "Edit Schema": "Редактировать схему", + "Edit Source": "Редактировать источник", + "Edit View": "Редактировать представление", + "Edit Webhook": "Редактировать веб-хук", + "Edit labels": "Редактировать метки", + "Edit processing activity": "Редактировать операцию обработки", + "Edit tags for {name}": "Редактировать теги для {name}", + "Edit verwerkingsactiviteit": "Редактировать verwerkingsactiviteit", + "Edit view": "Редактировать представление", + "Edit view details": "Редактировать детали представления", + "Effectiveness": "Эффективность", + "Email": "Электронная почта", + "Email Addresses": "Адреса электронной почты", + "Email digest frequency": "Частота дайджеста по почте", + "Emails": "Письма", + "Embedding Model": "Модель векторных представлений", + "Embedding Provider": "Провайдер векторных представлений", + "Embedding generated successfully": "Векторное представление успешно создано", + "Embedding provider connection successful!": "Подключение к провайдеру векторных представлений успешно!", + "Embedding test failed: {error}": "Тест векторных представлений не удался: {error}", + "Enable": "Включить", + "Enable RAG": "Включить RAG", + "Enable auto-creation": "Включить автосоздание", + "Enable automatic file vectorization": "Включить автоматическую векторизацию файлов", + "Enable automatic object vectorization": "Включить автоматическую векторизацию объектов", + "Enable automatic synchronization": "Включить автоматическую синхронизацию", + "Enable calendar provider": "Включить провайдер календаря", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Включить подробное логирование операций SOLR (рекомендуется для отладки)", + "Enable faceting": "Включить фасетирование", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Включить или отключить функции LLM. Настройте провайдеров и модели с помощью кнопки «Настройка LLM» выше.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Включить или отключить функции LLM. Настройте провайдеров и модели с помощью кнопки настройки LLM выше.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Включить или отключить интеграцию поиска SOLR. Настройте параметры подключения с помощью кнопки «Настройки подключения» выше.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Включить или отключить интеграцию поиска SOLR. Настройте параметры подключения с помощью кнопки настроек подключения выше.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Включить или отключить интеграцию с рабочими процессами n8n. Настройте параметры подключения ниже.", + "Enable or disable this webhook": "Включить или отключить этот веб-хук", + "Enable tools that allow the agent to interact with data through function calling.": "Включить инструменты, позволяющие агенту взаимодействовать с данными через вызов функций.", + "Enable vectorization for all existing and future views (may increase costs)": "Включить векторизацию для всех существующих и будущих представлений (может увеличить затраты)", + "Enable write-back to target objects": "Включить обратную запись в целевые объекты", + "Enabled": "Включено", + "End Date Field": "Поле даты окончания", + "Endpoint Path*": "Путь конечной точки*", + "Endpoints": "Конечные точки", + "Engine not found": "Движок не найден", + "Enter ConfigSet name": "Введите название набора конфигурации", + "Enter Nextcloud usernames to grant access to this private agent": "Введите имена пользователей Nextcloud для предоставления доступа к этому частному агенту", + "Enter agent description (optional)": "Введите описание агента (необязательно)", + "Enter agent name": "Введите имя агента", + "Enter application description (optional)": "Введите описание приложения (необязательно)", + "Enter application name": "Введите название приложения", + "Enter collection name": "Введите название коллекции", + "Enter configuration description (optional)": "Введите описание конфигурации (необязательно)", + "Enter configuration title": "Введите заголовок конфигурации", + "Enter description (optional)...": "Введите описание (необязательно)...", + "Enter new collection name": "Введите новое название коллекции", + "Enter object ID": "Введите ID объекта", + "Enter organisation description (optional)": "Введите описание организации (необязательно)", + "Enter organisation name": "Введите название организации", + "Enter search term": "Введите поисковый запрос", + "Enter search terms or leave empty to browse all": "Введите поисковые запросы или оставьте пустым для просмотра всех", + "Enter system prompt for the agent": "Введите системный промпт для агента", + "Enter username and press Enter": "Введите имя пользователя и нажмите Enter", + "Enter view name...": "Введите название представления...", + "Enter webhook name": "Введите название веб-хука", + "Enter your API key": "Введите ваш API-ключ", + "Enter your object here...": "Введите ваш объект здесь...", + "Enter your schema here...": "Введите вашу схему здесь...", + "Entities": "Объекты", + "Entities detected": "Объекты обнаружены", + "Entity ID: {id}": "ID объекта: {id}", + "Entity Information": "Информация об объекте", + "Entity Recognition": "Распознавание объектов", + "Entity Recognition Method": "Метод распознавания объектов", + "Entity deleted successfully": "Объект успешно удалён", + "Entity not found": "Объект не найден", + "Entries to be deleted:": "Записи для удаления:", + "Enum Issue": "Проблема с перечислением", + "Enum constraint is missing": "Ограничение перечисления отсутствует", + "Erase (Art 17)": "Стереть (Ст. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Стереть {count} объект(ов) для этого субъекта? Это действие будет зафиксировано в журнале аудита.", + "Erasure complete": "Стирание завершено", + "Erasure preview": "Предпросмотр стирания", + "Error": "Ошибка", + "Error Details": "Детали ошибки", + "Error Information": "Информация об ошибке", + "Error Message": "Сообщение об ошибке", + "Error deleting audit trails: {error}": "Ошибка при удалении журналов аудита: {error}", + "Error deleting search trails: {error}": "Ошибка при удалении журналов поиска: {error}", + "Error loading application": "Ошибка при загрузке приложения", + "Error loading audit trails": "Ошибка при загрузке журналов аудита", + "Error loading entity": "Ошибка при загрузке объекта", + "Error loading search trails": "Ошибка при загрузке журналов поиска", + "Estimated Batches:": "Расчётное количество пакетов:", + "Estimated Cost:": "Расчётная стоимость:", + "Estimated Duration:": "Расчётная длительность:", + "Event": "Событие", + "Event Property for Payload": "Свойство события для полезной нагрузки", + "Events": "События", + "Every schema with detected PII has a processing-activity annotation.": "Каждая схема с обнаруженными персональными данными имеет аннотацию об операции обработки.", + "Example": "Пример", + "Example Value": "Пример значения", + "Example value for this property": "Пример значения для этого свойства", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Строгий максимум", + "Exclusive Minimum": "Строгий минимум", + "Exclusive maximum": "Строгий максимум", + "Exclusive minimum": "Строгий минимум", + "Execution Mode": "Режим выполнения", + "Execution Time": "Время выполнения", + "Execution Time Range": "Диапазон времени выполнения", + "Existing Improvements": "Существующие улучшения", + "Expand details": "Развернуть детали", + "Expiration": "Срок действия", + "Expires": "Истекает", + "Expires in (e.g., 90d)": "Истекает через (например, 90d)", + "Exponential": "Экспоненциальный", + "Export": "Экспорт", + "Export \"{schema}\" objects from \"{register}\"": "Экспорт объектов \"{schema}\" из \"{register}\"", + "Export Configuration": "Экспортировать конфигурацию", + "Export Format:": "Формат экспорта:", + "Export Objects": "Экспортировать объекты", + "Export completed successfully": "Экспорт успешно завершён", + "Export downloaded successfully": "Экспорт успешно загружен", + "Export failed: {error}": "Ошибка экспорта: {error}", + "Export is rate limited. Please try again later.": "Экспорт ограничен по частоте запросов. Повторите попытку позже.", + "Export my data": "Экспортировать мои данные", + "Export, view, or delete audit trails": "Экспортировать, просматривать или удалять журналы аудита", + "Exporting...": "Экспорт...", + "Extend Schema": "Расширить схему", + "Extended": "Расширенный", + "Extensions": "Расширения", + "Extra Query Parameters": "Дополнительные параметры запроса", + "Extract Now": "Извлечь сейчас", + "Extract Pending Files": "Извлечь ожидающие файлы", + "Extract now": "Извлечь сейчас", + "Extract properties from each object": "Извлечь свойства из каждого объекта", + "Extracted At": "Извлечено в", + "Extracted at": "Извлечено в", + "Extraction": "Извлечение", + "Extraction Mode": "Режим извлечения", + "Extraction Scope": "Область извлечения", + "Extraction Status": "Статус извлечения", + "Extraction error for {file}: {error}": "Ошибка извлечения для {file}: {error}", + "Facet Description": "Описание фасета", + "Facet Order": "Порядок фасетов", + "Facet Title": "Заголовок фасета", + "Facet Type": "Тип фасета", + "Facet configuration updated successfully": "Конфигурация фасетов успешно обновлена", + "Facetable": "Поддерживает фасетирование", + "Faceting Configuration:": "Конфигурация фасетирования:", + "Facets discovered and configured successfully": "Фасеты успешно обнаружены и настроены", + "Facets discovered successfully": "Фасеты успешно обнаружены", + "Failed": "Не удалось", + "Failed to add attachment to object": "Не удалось добавить вложение к объекту", + "Failed to add user to organisation": "Не удалось добавить пользователя в организацию", + "Failed to analyze schema properties": "Не удалось проанализировать свойства схемы", + "Failed to archive conversation": "Не удалось архивировать переписку", + "Failed to calculate sizes": "Не удалось вычислить размеры", + "Failed to cancel deactivation": "Не удалось отменить деактивацию", + "Failed to change active organisation: {error}": "Не удалось изменить активную организацию: {error}", + "Failed to change password": "Не удалось изменить пароль", + "Failed to check version: {error}": "Не удалось проверить версию: {error}", + "Failed to clear audit trails: {error}": "Не удалось очистить журналы аудита: {error}", + "Failed to clear blob objects: {error}": "Не удалось очистить объекты blob: {error}", + "Failed to clear cache: {error}": "Не удалось очистить кэш: {error}", + "Failed to clear collection": "Не удалось очистить коллекцию", + "Failed to clear conversation": "Не удалось очистить переписку", + "Failed to clear embeddings: {error}": "Не удалось очистить векторные представления: {error}", + "Failed to clear search trails: {error}": "Не удалось очистить журналы поиска: {error}", + "Failed to clear {type} cache: {error}": "Не удалось очистить кэш {type}: {error}", + "Failed to copy changes": "Не удалось скопировать изменения", + "Failed to copy data": "Не удалось скопировать данные", + "Failed to copy data to clipboard": "Не удалось скопировать данные в буфер обмена", + "Failed to copy to clipboard": "Не удалось скопировать в буфер обмена", + "Failed to copy token": "Не удалось скопировать токен", + "Failed to create conversation": "Не удалось создать переписку", + "Failed to create missing SOLR fields: {error}": "Не удалось создать отсутствующие поля SOLR: {error}", + "Failed to create or find project": "Не удалось создать или найти проект", + "Failed to create token": "Не удалось создать токен", + "Failed to delete SOLR collection: {error}": "Не удалось удалить коллекцию SOLR: {error}", + "Failed to delete collection": "Не удалось удалить коллекцию", + "Failed to delete conversation": "Не удалось удалить переписку", + "Failed to delete objects for {schema}: {error}": "Не удалось удалить объекты для {schema}: {error}", + "Failed to delete webhook": "Не удалось удалить веб-хук", + "Failed to depublish register: {error}": "Не удалось снять реестр с публикации: {error}", + "Failed to depublish schema: {error}": "Не удалось снять схему с публикации: {error}", + "Failed to depublish: {error}": "Не удалось снять с публикации: {error}", + "Failed to discover files: {error}": "Не удалось обнаружить файлы: {error}", + "Failed to download API specification": "Не удалось скачать спецификацию API", + "Failed to export data": "Не удалось экспортировать данные", + "Failed to export objects": "Не удалось экспортировать объекты", + "Failed to fetch conversation history": "Не удалось получить историю переписки", + "Failed to fix mismatched SOLR fields: {error}": "Не удалось исправить несоответствующие поля SOLR: {error}", + "Failed to get SOLR field configuration": "Не удалось получить конфигурацию полей SOLR", + "Failed to get chat statistics": "Не удалось получить статистику чата", + "Failed to get database information: %s": "Не удалось получить информацию о базе данных: %s", + "Failed to get entity categories": "Не удалось получить категории объектов", + "Failed to get entity statistics": "Не удалось получить статистику объектов", + "Failed to get entity types": "Не удалось получить типы объектов", + "Failed to get response: {error}": "Не удалось получить ответ: {error}", + "Failed to invalidate app store cache: {error}": "Не удалось аннулировать кэш магазина приложений: {error}", + "Failed to leave organisation: {error}": "Не удалось покинуть организацию: {error}", + "Failed to link object": "Не удалось связать объект", + "Failed to load Deck cards": "Не удалось загрузить карточки Deck", + "Failed to load LLM configuration": "Не удалось загрузить конфигурацию LLM", + "Failed to load Nextcloud groups": "Не удалось загрузить группы Nextcloud", + "Failed to load cache statistics: {error}": "Не удалось загрузить статистику кэша: {error}", + "Failed to load conversation": "Не удалось загрузить переписку", + "Failed to load entities": "Не удалось загрузить объекты", + "Failed to load entity": "Не удалось загрузить объект", + "Failed to load extraction data": "Не удалось загрузить данные извлечения", + "Failed to load files": "Не удалось загрузить файлы", + "Failed to load linked contacts": "Не удалось загрузить связанные контакты", + "Failed to load linked emails": "Не удалось загрузить связанные письма", + "Failed to load linked events": "Не удалось загрузить связанные события", + "Failed to load organisations": "Не удалось загрузить организации", + "Failed to load register data": "Не удалось загрузить данные реестра", + "Failed to load related entities": "Не удалось загрузить связанные объекты", + "Failed to load settings: {error}": "Не удалось загрузить настройки: {error}", + "Failed to load statistics: {error}": "Не удалось загрузить статистику: {error}", + "Failed to load templates": "Не удалось загрузить шаблоны", + "Failed to load view: {error}": "Не удалось загрузить представление: {error}", + "Failed to load webhooks": "Не удалось загрузить веб-хуки", + "Failed to load workflows": "Не удалось загрузить рабочие процессы", + "Failed to process message": "Не удалось обработать сообщение", + "Failed to publish register: {error}": "Не удалось опубликовать реестр: {error}", + "Failed to publish schema: {error}": "Не удалось опубликовать схему: {error}", + "Failed to publish: {error}": "Не удалось опубликовать: {error}", + "Failed to refresh database information": "Не удалось обновить информацию о базе данных", + "Failed to reindex collection": "Не удалось переиндексировать коллекцию", + "Failed to remove avatar": "Не удалось удалить аватар", + "Failed to remove link": "Не удалось удалить ссылку", + "Failed to remove member: {error}": "Не удалось удалить участника: {error}", + "Failed to remove schema {schema}: {error}": "Не удалось удалить схему {schema}: {error}", + "Failed to remove user from organisation": "Не удалось удалить пользователя из организации", + "Failed to rename conversation": "Не удалось переименовать переписку", + "Failed to request deactivation": "Не удалось запросить деактивацию", + "Failed to restore conversation": "Не удалось восстановить переписку", + "Failed to retrieve extraction status.": "Не удалось получить статус извлечения.", + "Failed to retrieve objects for file.": "Не удалось получить объекты для файла.", + "Failed to retry extraction": "Не удалось повторить извлечение", + "Failed to retry webhook": "Не удалось повторить веб-хук", + "Failed to revoke token": "Не удалось отозвать токен", + "Failed to save GitHub token": "Не удалось сохранить токен GitHub", + "Failed to save GitLab URL": "Не удалось сохранить URL GitLab", + "Failed to save GitLab token": "Не удалось сохранить токен GitLab", + "Failed to save LLM settings: {error}": "Не удалось сохранить настройки LLM: {error}", + "Failed to save additional feedback": "Не удалось сохранить дополнительный отзыв", + "Failed to save configuration.": "Не удалось сохранить конфигурацию.", + "Failed to save configuration: {error}": "Не удалось сохранить конфигурацию: {error}", + "Failed to save facet configuration": "Не удалось сохранить конфигурацию фасетов", + "Failed to save feedback": "Не удалось сохранить отзыв", + "Failed to save file settings: {error}": "Не удалось сохранить настройки файлов: {error}", + "Failed to save n8n configuration": "Не удалось сохранить конфигурацию n8n", + "Failed to save preferences": "Не удалось сохранить предпочтения", + "Failed to save roles": "Не удалось сохранить роли", + "Failed to save settings": "Не удалось сохранить настройки", + "Failed to save settings: {error}": "Не удалось сохранить настройки: {error}", + "Failed to save view: {error}": "Не удалось сохранить представление: {error}", + "Failed to save warmup interval: {error}": "Не удалось сохранить интервал прогрева: {error}", + "Failed to save webhook": "Не удалось сохранить веб-хук", + "Failed to send feedback": "Не удалось отправить отзыв", + "Failed to set active organisation: {error}": "Не удалось установить активную организацию: {error}", + "Failed to setup SOLR: {error}": "Не удалось настроить SOLR: {error}", + "Failed to start file warmup: {error}": "Не удалось запустить прогрев файлов: {error}", + "Failed to start processing {type} files: {error}": "Не удалось начать обработку файлов {type}: {error}", + "Failed to start vectorization: {error}": "Не удалось запустить векторизацию: {error}", + "Failed to switch organisation: {error}": "Не удалось переключить организацию: {error}", + "Failed to sync magic table for {schema}: {error}": "Не удалось синхронизировать magic table для {schema}: {error}", + "Failed to test SOLR connection: {error}": "Не удалось проверить подключение SOLR: {error}", + "Failed to test webhook": "Не удалось проверить веб-хук", + "Failed to update LLM settings: {error}": "Не удалось обновить настройки LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Не удалось обновить настройки мультиарендности: {error}", + "Failed to update RBAC settings: {error}": "Не удалось обновить настройки RBAC: {error}", + "Failed to update Retention settings: {error}": "Не удалось обновить настройки хранения: {error}", + "Failed to update SOLR settings: {error}": "Не удалось обновить настройки SOLR: {error}", + "Failed to update favorite status": "Не удалось обновить статус избранного", + "Failed to update schema configuration for {schema}: {error}": "Не удалось обновить конфигурацию схемы {schema}: {error}", + "Failed to update schema properties": "Не удалось обновить свойства схемы", + "Failed to update view": "Не удалось обновить представление", + "Failed to update view: {error}": "Не удалось обновить представление: {error}", + "Failed to update webhook": "Не удалось обновить веб-хук", + "Failed to upload avatar": "Не удалось загрузить аватар", + "Failed to validate {schema}: {error}": "Не удалось проверить {schema}: {error}", + "Failed to vectorize files: {error}": "Не удалось векторизовать файлы: {error}", + "Failed to warmup names cache: {error}": "Не удалось прогреть кэш имён: {error}", + "Federation": "Федерация", + "Feedback recorded": "Отзыв записан", + "Fewer sources (1-3):": "Меньше источников (1-3):", + "Field": "Поле", + "Fields to Display": "Поля для отображения", + "File": "Файл", + "File #{id}": "Файл #{id}", + "File Chunk Prediction": "Прогноз фрагментов файла", + "File Collection": "Коллекция файлов", + "File Handling": "Обработка файлов", + "File Location": "Расположение файла", + "File Management": "Управление файлами", + "File Name": "Имя файла", + "File Path": "Путь к файлу", + "File Type Selection": "Выбор типа файла", + "File Types to Process": "Типы файлов для обработки", + "File Vectorization": "Векторизация файлов", + "File Warmup": "Прогрев файлов", + "File actions menu": "Меню действий с файлом", + "File anonymized successfully": "Файл успешно анонимизирован", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Векторизация фрагментов файлов ещё не реализована. Фрагменты готовы и сохранены, но сервис векторизации находится в разработке.", + "File collection not configured": "Коллекция файлов не настроена", + "File discovery completed": "Обнаружение файлов завершено", + "File extraction completed": "Извлечение файлов завершено", + "File extraction queued": "Извлечение файлов поставлено в очередь", + "File indexed successfully": "Файл успешно проиндексирован", + "File is already anonymized": "Файл уже анонимизирован", + "File name": "Имя файла", + "File not found": "Файл не найден", + "File settings saved successfully": "Настройки файлов успешно сохранены", + "File settings updated successfully": "Настройки файлов успешно обновлены", + "File sources": "Источники файлов", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Векторизация файлов завершена! Векторизовано {vectorized} фрагментов из {files} файлов. {failed} не удалось.", + "File vectorization configuration saved successfully": "Конфигурация векторизации файлов успешно сохранена", + "File vectorization started. Check the statistics section for progress.": "Векторизация файлов запущена. Прогресс см. в разделе статистики.", + "File warmup completed": "Прогрев файлов завершён", + "Files": "Файлы", + "Files added successfully": "Файлы успешно добавлены", + "Files with Completed Extraction:": "Файлы с завершённым извлечением:", + "Files → fileCollection, Objects → objectCollection": "Файлы → fileCollection, Объекты → objectCollection", + "Files → filecollection, objects → objectcollection": "Файлы → filecollection, объекты → objectcollection", + "Filter Audit Trails": "Фильтровать журналы аудита", + "Filter Deleted Items": "Фильтровать удалённые элементы", + "Filter Objects": "Фильтровать объекты", + "Filter Properties": "Фильтровать свойства", + "Filter Search Trails": "Фильтровать журналы поиска", + "Filter Statistics": "Статистика фильтров", + "Filter and analyze search trail entries": "Фильтровать и анализировать записи журнала поиска", + "Filter and manage audit trail entries": "Фильтровать и управлять записями журнала аудита", + "Filter and manage soft deleted items": "Фильтровать и управлять программно удалёнными элементами", + "Filter and search entities": "Фильтровать и искать объекты", + "Filter by object ID": "Фильтровать по ID объекта", + "Filter by search term": "Фильтровать по поисковому запросу", + "Filter by type": "Фильтровать по типу", + "Filter by webhook": "Фильтровать по веб-хуку", + "Filter data loaded automatically. Use the filters below to refine your search.": "Данные фильтров загружены автоматически. Используйте фильтры ниже для уточнения поиска.", + "Filter fields...": "Фильтровать поля...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Фильтровать триггеры веб-хука по свойствам полезной нагрузки (по одному на строку, формат: key: value)", + "Filtered": "Отфильтровано", + "Filters": "Фильтры", + "Finalizing setup...": "Завершение настройки...", + "Fireworks AI Chat Settings": "Настройки чата Fireworks AI", + "Fireworks AI Embedding Configuration": "Конфигурация векторных представлений Fireworks AI", + "First": "Первый", + "Fixed": "Исправлено", + "For chat and retrieval-augmented generation": "Для чата и генерации с расширенным поиском", + "For vector embeddings and semantic search": "Для векторных представлений и семантического поиска", + "Format": "Формат", + "Format Issue": "Проблема формата", + "Format constraint is missing": "Ограничение формата отсутствует", + "From (e.g. -7 days or 2025-01-01)": "От (например, -7 days или 2025-01-01)", + "From Date": "Дата начала", + "From date": "Дата начала", + "Full data copied to clipboard": "Полные данные скопированы в буфер обмена", + "Function Tools": "Функциональные инструменты", + "General Issue": "Общая проблема", + "Generate recommendations and confidence scores": "Сгенерировать рекомендации и оценки достоверности", + "Generate report": "Сгенерировать отчёт", + "Generate the verantwoordingsdocument": "Сгенерировать verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Генерировать векторы сразу при создании новых объектов", + "Generated: {time}": "Сгенерировано: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Получайте данные в реальном времени о состоянии данных вашей организации, уделяя внимание реестрам, определениям схем, хранению и использованию объектов.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Получайте данные в реальном времени о состоянии данных вашей организации, тенденциях роста и показателях качества.", + "GitHub Personal Access Token": "Персональный токен доступа GitHub", + "GitHub token is valid": "Токен GitHub действителен", + "GitHub token is valid! Username: {username}": "Токен GitHub действителен! Имя пользователя: {username}", + "GitHub token removed successfully": "Токен GitHub успешно удалён", + "GitHub token saved successfully": "Токен GitHub успешно сохранён", + "GitHub token test failed: {message}": "Тест токена GitHub не удался: {message}", + "GitLab Personal Access Token": "Персональный токен доступа GitLab", + "GitLab URL saved successfully": "URL GitLab успешно сохранён", + "GitLab token is valid": "Токен GitLab действителен", + "GitLab token is valid! Username: {username}": "Токен GitLab действителен! Имя пользователя: {username}", + "GitLab token saved successfully": "Токен GitLab успешно сохранён", + "GitLab token test failed: {message}": "Тест токена GitLab не удался: {message}", + "Go to linked Register": "Перейти к связанному реестру", + "Go to linked Schema": "Перейти к связанной схеме", + "Go to organisation": "Перейти к организации", + "Grant additional permissions beyond standard CRUD operations": "Предоставить дополнительные разрешения помимо стандартных CRUD-операций", + "Group Quota": "Квота группы", + "Groups": "Группы", + "HTML preview": "Предпросмотр HTML", + "HTTP Method": "Метод HTTP", + "HTTP method used to send webhook requests": "Метод HTTP для отправки запросов веб-хука", + "Headers": "Заголовки", + "Health": "Состояние", + "Heartbeat successful - connection kept alive": "Сигнал жизнеспособности успешен — соединение поддерживается", + "Helpful": "Полезно", + "Here you can configure the details for various connections.": "Здесь вы можете настроить параметры различных подключений.", + "Here you can configure the details for your organisation.": "Здесь вы можете настроить параметры вашей организации.", + "Hide": "Скрыть", + "Hide Filters": "Скрыть фильтры", + "Hide in collection view": "Скрыть в представлении коллекции", + "Hide in forms": "Скрыть в формах", + "Hide in list view": "Скрыть в списковом представлении", + "High": "Высокий", + "High Confidence": "Высокая достоверность", + "Host": "Хост", + "Hourly": "Каждый час", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Глубина обхода вложенных свойств объекта (1-20). Большие значения захватывают больше деталей, но увеличивают размер вектора.", + "How often to check for updates (1-168 hours)": "Как часто проверять обновления (1-168 часов)", + "How to handle retries for failed webhook deliveries": "Как обрабатывать повторные попытки при неудачной доставке веб-хука", + "IBANs": "Номера IBAN", + "ID": "ID", + "ID of input mapping (optional)": "ID входного маппинга (необязательно)", + "ID of output mapping (optional)": "ID выходного маппинга (необязательно)", + "ID of the target resource": "ID целевого ресурса", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ограничения частоты запросов IP успешно очищены", + "Identify properties not in the schema": "Определить свойства, отсутствующие в схеме", + "Immutable": "Неизменяемый", + "Import": "Импорт", + "Import Configuration": "Импортировать конфигурацию", + "Import Data into Register": "Импортировать данные в реестр", + "Import successful": "Импорт успешно выполнен", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Импортируйте шаблон report-bundle.json, чтобы получить реестр `reports`, затем создайте первый дашборд через стандартный интерфейс объектов. Дашборды объявляют свои виджеты в JSON, а рендерер передаёт каждому виджету живые агрегированные данные.", + "Improved Property": "Улучшенное свойство", + "In use": "Используется", + "Inactive": "Неактивный", + "Inactive organisations cannot be used": "Неактивные организации нельзя использовать", + "Include IDs and names of related objects for better contextual search": "Включить идентификаторы и имена связанных объектов для улучшения контекстного поиска", + "Include ids and names of related objects for better contextual search": "Включить идентификаторы и имена связанных объектов для улучшения контекстного поиска", + "Include related object references": "Включить ссылки на связанные объекты", + "Include schema and register metadata": "Включить метаданные схемы и реестра", + "Index": "Индекс", + "Information about the current OpenRegister installation": "Информация о текущей установке OpenRegister", + "Inherited from register": "Унаследовано от реестра", + "Inherits permissions from register": "Наследует разрешения от реестра", + "Initialization failed": "Инициализация не удалась", + "Initialize Project": "Инициализировать проект", + "Initializing SOLR setup...": "Инициализация настройки SOLR...", + "Input Mapping": "Входной маппинг", + "Inspect Fields": "Проверить поля", + "Inspect Index": "Проверить индекс", + "Inspect SOLR Index": "Проверить индекс SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Установите приложение notify_push из магазина приложений Nextcloud для включения обновлений в реальном времени.", + "Integrations": "Интеграции", + "Internet location (URL) of this installation": "Интернет-адрес (URL) этой установки", + "Interval": "Интервал", + "Invalid": "Недействительный", + "Invalid batch size. Must be between 1 and 5000": "Неверный размер пакета. Должен быть от 1 до 5000", + "Invalid feedback type": "Недопустимый тип отзыва", + "Invalid field name provided": "Указано недействительное имя поля", + "Invalid maxObjects. Must be 0 (all) or positive number": "Неверное значение maxObjects. Должно быть 0 (все) или положительное число", + "Invalid:": "Недействительный:", + "Invite Users": "Пригласить пользователей", + "Invited Users": "Приглашённые пользователи", + "Inzage (Art 15)": "Inzage (Ст. 15)", + "Inzage results": "Результаты Inzage", + "Issue": "Проблема", + "Items per page:": "Элементов на странице:", + "Just now": "Только что", + "Key": "Ключ", + "LLM Configuration": "Настройка LLM", + "LLM actions menu": "Меню действий LLM", + "LLM configuration saved successfully": "Конфигурация LLM успешно сохранена", + "LLM features disabled": "Функции LLM отключены", + "LLM features enabled": "Функции LLM включены", + "LLM must be enabled with an embedding provider configured": "LLM должен быть включён с настроенным провайдером векторных представлений", + "LLM settings saved successfully": "Настройки LLM успешно сохранены", + "LLM settings updated successfully": "Настройки LLM успешно обновлены", + "Label": "Метка", + "Labels": "Метки", + "Languages": "Языки", + "Last": "Последний", + "Last Triggered": "Последнее срабатывание", + "Leave empty to allow all users access": "Оставьте пустым для доступа всех пользователей", + "Leave off for auto-detection from property format.": "Оставьте выключенным для автоопределения из формата свойства.", + "Left organisation successfully": "Организация успешно покинута", + "Legal basis *": "Правовое основание *", + "Length Range:": "Диапазон длины:", + "Linear": "Линейный", + "Link": "Ссылка", + "Link an email from the Mail app sidebar to associate it with this object.": "Свяжите письмо из боковой панели приложения Mail с этим объектом.", + "Link an object to see it here.": "Свяжите объект, чтобы увидеть его здесь.", + "Link another object": "Связать другой объект", + "Link existing event": "Связать существующее событие", + "Link removed": "Ссылка удалена", + "Link to Object": "Ссылка на объект", + "Link to {name}": "Ссылка на {name}", + "Linked Objects": "Связанные объекты", + "Linked by {user}": "Связано пользователем {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Здесь будут отображаться связанные письма, события календаря, контакты и карточки Deck.", + "Linked to {name}": "Связано с {name}", + "Load Advanced Filters": "Загрузить расширенные фильтры", + "Load advanced filters with live data from your search index": "Загрузить расширенные фильтры с живыми данными из вашего поискового индекса", + "Load more": "Загрузить ещё", + "Load view": "Загрузить представление", + "Loading API tokens...": "Загрузка API-токенов...", + "Loading ConfigSets...": "Загрузка наборов конфигурации...", + "Loading LLM configuration...": "Загрузка конфигурации LLM...", + "Loading RBAC settings...": "Загрузка настроек RBAC...", + "Loading activity...": "Загрузка активности...", + "Loading advanced filters...": "Загрузка расширенных фильтров...", + "Loading agents...": "Загрузка агентов...", + "Loading application details...": "Загрузка сведений о приложении...", + "Loading applications...": "Загрузка приложений...", + "Loading archived conversations...": "Загрузка архивированных переписок...", + "Loading audit trails...": "Загрузка журналов аудита...", + "Loading available tools...": "Загрузка доступных инструментов...", + "Loading cache statistics...": "Загрузка статистики кэша...", + "Loading collections...": "Загрузка коллекций...", + "Loading configurations...": "Загрузка конфигураций...", + "Loading conversation...": "Загрузка переписки...", + "Loading conversations...": "Загрузка переписок...", + "Loading deleted items...": "Загрузка удалённых элементов...", + "Loading endpoints": "Загрузка конечных точек", + "Loading entities...": "Загрузка объектов...", + "Loading events...": "Загрузка событий...", + "Loading filter data...": "Загрузка данных фильтров...", + "Loading groups...": "Загрузка групп...", + "Loading linked objects...": "Загрузка связанных объектов...", + "Loading log details...": "Загрузка деталей журнала...", + "Loading multitenancy settings...": "Загрузка настроек мультиарендности...", + "Loading n8n configuration...": "Загрузка конфигурации n8n...", + "Loading permission matrix...": "Загрузка матрицы разрешений...", + "Loading preferences...": "Загрузка предпочтений...", + "Loading register data...": "Загрузка данных реестра...", + "Loading registers...": "Загрузка реестров...", + "Loading retention settings...": "Загрузка настроек хранения...", + "Loading schemas...": "Загрузка схем...", + "Loading search configuration...": "Загрузка конфигурации поиска...", + "Loading search trails...": "Загрузка журналов поиска...", + "Loading sources...": "Загрузка источников...", + "Loading statistics...": "Загрузка статистики...", + "Loading text extraction settings...": "Загрузка настроек извлечения текста...", + "Loading tokens...": "Загрузка токенов...", + "Loading user groups...": "Загрузка групп пользователей...", + "Loading users...": "Загрузка пользователей...", + "Loading views...": "Загрузка представлений...", + "Loading...": "Загрузка...", + "Local": "Локальный", + "Local Version": "Локальная версия", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Найдите каждый объект, ссылающийся на субъекта данных (Ст. 15 inzage), предпросмотрите стирание (Ст. 17 vergetelheid) или экспортируйте их данные (Ст. 20 portabiliteit).", + "Location": "Расположение", + "Location Field": "Поле расположения", + "Locations": "Местоположения", + "Locked": "Заблокировано", + "Locked:": "Заблокировано:", + "Login successful": "Вход выполнен успешно", + "Logs": "Журналы", + "Low": "Низкий", + "Low Confidence": "Низкая достоверность", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Меньшие числа отображаются первыми в боковой панели фильтров. Оставьте пустым для автоматической сортировки.", + "Lucene Version:": "Версия Lucene:", + "Magic table sync completed for {schema}": "Синхронизация magic table для {schema} завершена", + "Magic table synchronized successfully": "Magic table успешно синхронизирована", + "Mail Integration": "Интеграция с почтой", + "Mail integration is not available": "Интеграция с почтой недоступна", + "Make this installation discoverable within the federation network.": "Сделать эту установку доступной для обнаружения в сети федерации.", + "Manage Organisation Roles": "Управление ролями организации", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Управление коллекциями SOLR (хранилищами данных) и их назначение для объектов и файлов.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Управление наборами конфигурации SOLR (шаблонами конфигурации) для ваших коллекций.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Управление коллекциями SOLR (хранилищами данных) и их назначение для объектов и файлов.", + "Manage SOLR configsets (configuration templates) for your collections.": "Управление наборами конфигурации SOLR (шаблонами конфигурации) для ваших коллекций.", + "Manage and configure agents for automated tasks": "Управление и настройка агентов для автоматизированных задач", + "Manage and monitor file text extraction status": "Управление и мониторинг статуса извлечения текста из файлов", + "Manage and restore soft deleted items from your registers": "Управление и восстановление программно удалённых элементов из ваших реестров", + "Manage and view detected entities from files and objects": "Управление и просмотр обнаруженных объектов в файлах и данных", + "Manage document templates and themes": "Управление шаблонами и темами документов", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Управление рабочими процессами n8n для автоматизации OpenRegister. Рабочие процессы будут сохранены в настроенном проекте.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Управление операциями обработки, выполнение запросов на доступ субъектов данных и аудит соответствия требованиям EU GDPR / нидерландского AVG.", + "Manage webhooks for event-driven integrations": "Управление веб-хуками для интеграций на основе событий", + "Manage your account settings, security, and personal data.": "Управляйте настройками аккаунта, безопасностью и персональными данными.", + "Manage your applications and modules": "Управляйте своими приложениями и модулями", + "Manage your chat conversations": "Управляйте своими переписками", + "Manage your data registers and their configurations": "Управляйте реестрами данных и их конфигурациями", + "Manage your data schemas and their properties": "Управляйте схемами данных и их свойствами", + "Manage your data sources and their configurations": "Управляйте источниками данных и их конфигурациями", + "Manage your organisations and switch between them": "Управляйте своими организациями и переключайтесь между ними", + "Manage your saved search configurations": "Управляйте сохранёнными конфигурациями поиска", + "Manage your system configurations and settings": "Управляйте системными конфигурациями и настройками", + "Managed": "Управляемый", + "Mappings": "Маппинги", + "Mass validation completed successfully": "Массовая проверка успешно завершена", + "Mass validation failed: {error}": "Массовая проверка не удалась: {error}", + "Max Files (0 = all)": "Максимальное количество файлов (0 = все)", + "Max Length": "Максимальная длина", + "Max Length:": "Максимальная длина:", + "Max Objects (0 = all)": "Максимальное количество объектов (0 = все)", + "Max Retries": "Максимальное число повторных попыток", + "Max Shards/Node": "Максимальное количество шардов/узел", + "Max Tokens": "Максимальное количество токенов", + "Max execution time (ms)": "Максимальное время выполнения (мс)", + "Max ms": "Макс. мс", + "Max result count": "Максимальное число результатов", + "Max results": "Максимальное число результатов", + "Maximum": "Максимум", + "Maximum File Size (MB)": "Максимальный размер файла (МБ)", + "Maximum Nesting Depth": "Максимальная глубина вложенности", + "Maximum length": "Максимальная длина", + "Maximum length constraint is missing": "Ограничение максимальной длины отсутствует", + "Maximum length is too restrictive": "Максимальная длина слишком ограничительна", + "Maximum number of files to process. Set to 0 to process all files.": "Максимальное количество файлов для обработки. Установите 0 для обработки всех файлов.", + "Maximum number of items": "Максимальное количество элементов", + "Maximum number of objects to process. Set to 0 to process all objects.": "Максимальное количество объектов для обработки. Установите 0 для обработки всех объектов.", + "Maximum number of retry attempts for failed deliveries": "Максимальное количество попыток повтора при неудачной доставке", + "Maximum time to wait before committing changes": "Максимальное время ожидания перед фиксацией изменений", + "Maximum tokens to generate": "Максимальное количество генерируемых токенов", + "Maximum value": "Максимальное значение", + "Maximum value constraint is missing": "Ограничение максимального значения отсутствует", + "Maximum value is too restrictive": "Максимальное значение слишком ограничительно", + "Medium": "Средний", + "Medium Confidence": "Средняя достоверность", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средние запросы: поиск с некоторой фильтрацией или несколькими параметрами (например, диапазоны дат, конкретные реестры/схемы)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средние запросы: поиск с некоторой фильтрацией или несколькими параметрами (например, диапазоны дат, конкретные реестры/схемы)", + "Member removed successfully": "Участник успешно удалён", + "Members": "Участники", + "Members ({count})": "Участники ({count})", + "Members of selected groups can access this view": "Члены выбранных групп могут получить доступ к этому представлению", + "Members:": "Участники:", + "Memory prediction calculated successfully": "Прогноз памяти успешно рассчитан", + "Message content is required": "Содержимое сообщения обязательно", + "Message does not belong to this conversation": "Сообщение не принадлежит этой переписке", + "Message not found": "Сообщение не найдено", + "Metadata": "Метаданные", + "Metadata Filters": "Фильтры метаданных", + "Method": "Метод", + "Method*": "Метод*", + "Method: {method}": "Метод: {method}", + "Min Length": "Минимальная длина", + "Min execution time (ms)": "Минимальное время выполнения (мс)", + "Min ms": "Мин. мс", + "Min result count": "Минимальное число результатов", + "Min results": "Минимальное число результатов", + "Minimum": "Минимум", + "Minimum length": "Минимальная длина", + "Minimum number of items": "Минимальное количество элементов", + "Minimum value": "Минимальное значение", + "Minimum value constraint is missing": "Ограничение минимального значения отсутствует", + "Minimum value is too restrictive": "Минимальное значение слишком ограничительно", + "Missing conversation or agentUuid": "Отсутствует переписка или agentUuid", + "Missing conversationId": "Отсутствует conversationId", + "Missing message": "Отсутствует сообщение", + "Missing object fields created successfully": "Отсутствующие поля объекта успешно созданы", + "Mode:": "Режим:", + "Model": "Модель", + "Model Name": "Название модели", + "Modified": "Изменено", + "Monthly": "Ежемесячно", + "More sources (10-20):": "Больше источников (10-20):", + "Most Active Objects": "Наиболее активные объекты", + "Multiple Of": "Кратно", + "Multiple of": "Кратно", + "Multiple search trail deletion not implemented yet": "Множественное удаление журналов поиска ещё не реализовано", + "Multitenancy": "Мультиарендность", + "Multitenancy settings updated successfully": "Настройки мультиарендности успешно обновлены", + "My Account": "Мой аккаунт", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Название", + "Name *": "Название *", + "Name*": "Название*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Кэш имён успешно прогрет: загружено {count} имён за {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Кэш имён успешно прогрет: загружено {count} имён за {time}. {message}", + "Names cache warmup completed": "Прогрев кэша имён завершён", + "Naming pattern for copies:": "Шаблон именования копий:", + "New Collection Name": "Новое название коллекции", + "New Configuration": "Новая конфигурация", + "New Conversation": "Новая переписка", + "New Properties": "Новые свойства", + "New Property": "Новое свойство", + "New User Group": "Новая группа пользователей", + "New Value": "Новое значение", + "New activity": "Новая активность", + "New password": "Новый пароль", + "New processing activity": "Новая операция обработки", + "New users without specific organisation membership will be automatically added to this organisation": "Новые пользователи без конкретного членства в организации будут автоматически добавлены в эту организацию", + "New verwerkingsactiviteit": "Новая verwerkingsactiviteit", + "Next": "Далее", + "Nextcloud Groups": "Группы Nextcloud", + "No": "Нет", + "No API tokens.": "API-токенов нет.", + "No ConfigSets found": "Наборы конфигурации не найдены", + "No Deck cards linked to this object": "К этому объекту не привязаны карточки Deck", + "No GitHub token configured": "Токен GitHub не настроен", + "No GitHub token provided": "Токен GitHub не предоставлен", + "No GitLab token provided": "Токен GitLab не предоставлен", + "No Organisation": "Без организации", + "No active filters": "Активных фильтров нет", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Нет активных объектов для программного удаления в схеме {schema}. Используйте «Удалить навсегда» для удаления программно удалённых объектов.", + "No activity data available": "Данные об активности недоступны", + "No activity found.": "Активность не найдена.", + "No agents available": "Агенты недоступны", + "No agents found": "Агенты не найдены", + "No app store cache exists yet": "Кэш магазина приложений ещё не существует", + "No applications are available.": "Приложения недоступны.", + "No applications found": "Приложения не найдены", + "No approval steps for this object.": "Шаги согласования для этого объекта отсутствуют.", + "No archived conversations": "Архивированных переписок нет", + "No audit trail entries found": "Записи журнала аудита не найдены", + "No changes recorded": "Изменения не зафиксированы", + "No chunks to vectorize": "Фрагментов для векторизации нет", + "No collections found": "Коллекции не найдены", + "No configuration data": "Данных конфигурации нет", + "No configuration selected": "Конфигурация не выбрана", + "No configurations are available.": "Конфигурации недоступны.", + "No configurations found": "Конфигурации не найдены", + "No configurations found for this application.": "Конфигурации для этого приложения не найдены.", + "No contacts linked to this object": "К этому объекту не привязаны контакты", + "No conversations yet. Create a new one to get started!": "Переписок ещё нет. Создайте новую, чтобы начать!", + "No dashboards yet": "Дашбордов ещё нет", + "No data available": "Данные недоступны", + "No data available for chart": "Данные для графика недоступны", + "No deleted items found": "Удалённые элементы не найдены", + "No deletion data available": "Данные об удалении недоступны", + "No description found": "Описание не найдено", + "No description provided": "Описание не указано", + "No emails linked to this object": "К этому объекту не привязаны письма", + "No endpoints defined": "Конечные точки не определены", + "No entities detected for this email.": "Объекты для этого письма не обнаружены.", + "No entities detected in this file. Run text extraction first.": "Объекты в этом файле не обнаружены. Сначала выполните извлечение текста.", + "No entities found": "Объекты не найдены", + "No entities have been detected yet": "Объекты ещё не обнаружены", + "No events found": "События не найдены", + "No events linked to this object": "К этому объекту не привязаны события", + "No expired audit trails found to clear": "Истёкшие записи журнала аудита для очистки не найдены", + "No expired search trails found to clear": "Истёкшие записи журнала поиска для очистки не найдены", + "No extraction data available for this file": "Данные извлечения для этого файла недоступны", + "No facetable fields available. Select a register and schema to see available filters.": "Поля для фасетирования недоступны. Выберите реестр и схему для просмотра доступных фильтров.", + "No files found": "Файлы не найдены", + "No files have been extracted yet": "Файлы ещё не извлекались", + "No files selected": "Файлы не выбраны", + "No files to process": "Файлов для обработки нет", + "No files to reindex": "Файлов для переиндексации нет", + "No filters are currently active. This will delete ALL audit trail entries!": "Активных фильтров нет. Это удалит ВСЕ записи журнала аудита!", + "No groups found. Try a different search.": "Группы не найдены. Попробуйте другой поисковый запрос.", + "No label": "Без метки", + "No labels": "Без меток", + "No linked objects": "Связанных объектов нет", + "No log entries found": "Записи журнала не найдены", + "No logs are available for this configuration.": "Журналы для этой конфигурации недоступны.", + "No logs are available for this source.": "Журналы для этого источника недоступны.", + "No logs found": "Журналы не найдены", + "No matches": "Совпадений нет", + "No mismatched fields found - SOLR schema is properly configured": "Несоответствующие поля не найдены — схема SOLR настроена правильно", + "No objects are currently selected for deletion.": "Объекты для удаления не выбраны.", + "No objects are currently selected for permanent deletion.": "Объекты для безвозвратного удаления не выбраны.", + "No objects are currently selected for restoration.": "Объекты для восстановления не выбраны.", + "No objects found": "Объекты не найдены", + "No objects found to vectorize": "Объекты для векторизации не найдены", + "No objects linked to this email": "К этому письму не привязаны объекты", + "No objects selected": "Объекты не выбраны", + "No objects selected for deletion": "Объекты для удаления не выбраны", + "No objects selected for restoration": "Объекты для восстановления не выбраны", + "No objects to delete": "Объектов для удаления нет", + "No objects to delete for schema {schema}": "Объектов для удаления в схеме {schema} нет", + "No objects were permanently deleted": "Объекты не были безвозвратно удалены", + "No organisations found": "Организации не найдены", + "No personal data was found for this subject identifier.": "Персональные данные для этого идентификатора субъекта не найдены.", + "No processing activities yet": "Операций обработки ещё нет", + "No properties found": "Свойства не найдены", + "No properties match your filters.": "Свойства не соответствуют вашим фильтрам.", + "No purge date set": "Дата очистки не задана", + "No register data available": "Данные реестра недоступны", + "No register objects reference this file": "Ни один объект реестра не ссылается на этот файл", + "No registers found": "Реестры не найдены", + "No registers found for this application.": "Реестры для этого приложения не найдены.", + "No registers found. Create a register to configure permissions.": "Реестры не найдены. Создайте реестр для настройки разрешений.", + "No related cases found for this sender": "Связанных дел для этого отправителя не найдено", + "No related entities": "Связанных объектов нет", + "No relations found": "Связи не найдены", + "No request body available": "Тело запроса недоступно", + "No response body available": "Тело ответа недоступно", + "No saved views yet. Create one in the Search tab!": "Сохранённых представлений ещё нет. Создайте одно на вкладке Поиск!", + "No saved views yet. create one in the search tab!": "Сохранённых представлений ещё нет. Создайте одно на вкладке поиска!", + "No scan run yet": "Проверка ещё не запускалась", + "No schema data available": "Данные схемы недоступны", + "No schema selected for exploration": "Схема для исследования не выбрана", + "No schemas configured for mail linking.": "Схемы для привязки почты не настроены.", + "No schemas found": "Схемы не найдены", + "No schemas found for this application.": "Схемы для этого приложения не найдены.", + "No search terms data available": "Данные о поисковых запросах недоступны", + "No search trail entries found": "Записи журнала поиска не найдены", + "No sources are available.": "Источники недоступны.", + "No sources found": "Источники не найдены", + "No synchronizations found": "Синхронизации не найдены", + "No templates found": "Шаблоны не найдены", + "No templates have been created yet": "Шаблоны ещё не созданы", + "No tools available. Tools can be registered by installed apps.": "Инструменты недоступны. Инструменты могут быть зарегистрированы установленными приложениями.", + "No users in this organisation.": "В этой организации нет пользователей.", + "No valid magic-mapped register+schema combinations found": "Допустимые комбинации реестр+схема с magic mapping не найдены", + "No views found. Create views first before configuring vectorization.": "Представления не найдены. Сначала создайте представления перед настройкой векторизации.", + "No views match your search": "Представления не соответствуют вашему поиску", + "No webhooks found": "Веб-хуки не найдены", + "No webhooks have been configured yet": "Веб-хуки ещё не настроены", + "No workflows found in this project. Create workflows in the n8n editor.": "В этом проекте рабочие процессы не найдены. Создайте рабочие процессы в редакторе n8n.", + "None": "Нет", + "Not Found": "Не найдено", + "Not extracted": "Не извлечено", + "Not helpful": "Бесполезно", + "Not in use": "Не используется", + "Note:": "Примечание:", + "Notifications": "Уведомления", + "Number Constraints": "Числовые ограничения", + "Number of Sources": "Количество источников", + "Number of chunks to vectorize in one API call (1-100).": "Количество фрагментов для векторизации в одном API-вызове (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Количество фрагментов для векторизации в одном API-вызове. Больше = быстрее, но больше памяти. Рекомендуется: 10-50.", + "Number of objects to process in each batch (1-5000).": "Количество объектов для обработки в каждом пакете (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Количество объектов для векторизации в одном API-вызове. Больше = быстрее, но больше памяти. Рекомендуется: 10-50.", + "Numeric Range:": "Числовой диапазон:", + "Object": "Объект", + "Object #{id}": "Объект #{id}", + "Object Collection": "Коллекция объектов", + "Object Configuration:": "Конфигурация объекта:", + "Object Count Prediction": "Прогноз количества объектов", + "Object Extraction Mode": "Режим извлечения объектов", + "Object ID": "ID объекта", + "Object Management": "Управление объектами", + "Object Text Extraction": "Извлечение текста из объектов", + "Object Vectorization": "Векторизация объектов", + "Object Warmup": "Прогрев объектов", + "Object changes in owned objects": "Изменения объектов в принадлежащих объектах", + "Object collection not configured": "Коллекция объектов не настроена", + "Object metadata incomplete for file upload": "Метаданные объекта неполны для загрузки файла", + "Object not found": "Объект не найден", + "Object permanently deleted": "Объект безвозвратно удалён", + "Object restored successfully": "Объект успешно восстановлен", + "Object settings updated successfully": "Настройки объекта успешно обновлены", + "Object sources": "Источники объектов", + "Object successfully deleted": "Объект успешно удалён", + "Object successfully modified": "Объект успешно изменён", + "Object unlocked successfully": "Объект успешно разблокирован", + "Object vectorization configuration saved successfully": "Конфигурация векторизации объектов успешно сохранена", + "Object vectorized successfully": "Объект успешно векторизован", + "Object:": "Объект:", + "Objects": "Объекты", + "Objects Analyzed": "Проанализированных объектов", + "Objects Distribution": "Распределение объектов", + "Objects being analyzed": "Анализируемые объекты", + "Objects by Register": "Объекты по реестрам", + "Objects by Schema": "Объекты по схемам", + "Objects deletion completed for {schema}": "Удаление объектов для {schema} завершено", + "Objects deletion completed successfully": "Удаление объектов успешно завершено", + "Objects in schema": "Объекты в схеме", + "Objects successfully deleted": "Объекты успешно удалены", + "Objects to Process:": "Объектов для обработки:", + "Objects to be analyzed": "Объекты для анализа", + "Objects to be deleted": "Объекты для удаления", + "Objects to be validated": "Объекты для проверки", + "Objects will be serialized as JSON text before vectorization": "Объекты будут сериализованы в формат JSON перед векторизацией", + "Objects will be soft deleted and moved to the": "Объекты будут программно удалены и перемещены в", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Объекты будут программно удалены (помечены как удалённые, но сохранены в базе данных). При необходимости их можно восстановить позже.", + "Old Value": "Старое значение", + "Oldest Item (days)": "Старейший элемент (дней)", + "Ollama Chat Settings": "Настройки чата Ollama", + "Ollama Embedding Configuration": "Конфигурация векторных представлений Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Только с изменениями", + "Only members of selected groups can access this organisation": "Только члены выбранных групп могут получить доступ к этой организации", + "Open Nextcloud App Store": "Открыть магазин приложений Nextcloud", + "Open OpenRegister sidebar": "Открыть боковую панель OpenRegister", + "Open Register Objects": "Открыть объекты реестра", + "Open folder": "Открыть папку", + "Open in OpenRegister": "Открыть в OpenRegister", + "Open n8n Editor": "Открыть редактор n8n", + "OpenAI Chat Settings": "Настройки чата OpenAI", + "OpenAI Embedding Configuration": "Конфигурация векторных представлений OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Настройки OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Операторские дашборды и запланированные отчёты. Каждый дашборд является первоклассным объектом в реестре `reports`; виджеты объявляются в массиве `widgets` дашборда и отображаются в реальном времени из агрегаций / GraphQL.", + "Optimizing search performance...": "Оптимизация производительности поиска...", + "Optional URL-friendly identifier": "Необязательный удобный для URL идентификатор", + "Optional description for this facet": "Необязательное описание для этого фасета", + "Optional end date property": "Необязательное свойство даты окончания", + "Optional event description template": "Необязательный шаблон описания события", + "Optional location property": "Необязательное свойство расположения", + "Optional webhook secret for signature verification": "Необязательный секрет веб-хука для проверки подписи", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Необязательно: добавьте параметры запроса для фильтрации ссылочной схемы (например, status=active&type=public)", + "Or": "Или", + "Order": "Порядок", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Упорядоченные теги языка BCP 47. Первый язык является языком реестра по умолчанию и управляет резервным вариантом Accept-Language для переводимых свойств.", + "Organisation": "Организация", + "Organisation Configuration": "Конфигурация организации", + "Organisation ID": "ID организации", + "Organisation Members": "Участники организации", + "Organisation Statistics": "Статистика организации", + "Organisation created successfully": "Организация успешно создана", + "Organisation membership changes": "Изменения членства в организации", + "Organisation name is required": "Название организации обязательно", + "Organisation settings saved successfully": "Настройки организации успешно сохранены", + "Organisation successfully created": "Организация успешно создана", + "Organisation successfully updated": "Организация успешно обновлена", + "Organisational measures": "Организационные меры", + "Organisations": "Организации", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Организация", + "Organization ID (Optional)": "ID организации (необязательно)", + "Organizations": "Организации", + "Orphaned Items": "Осиротевшие элементы", + "Other": "Другое", + "Output Mapping": "Выходной маппинг", + "Overview": "Обзор", + "Owner": "Владелец", + "Owner:": "Владелец:", + "PDF": "PDF", + "PII hits": "Попадания PII", + "PII rows": "Строки PII", + "Page {current} of {total}": "Страница {current} из {total}", + "Parallel Mode (Faster, more resource intensive)": "Параллельный режим (быстрее, но более ресурсоёмкий)", + "Parallel:": "Параллельно:", + "Password": "Пароль", + "Password changes are not supported by your authentication provider.": "Ваш провайдер аутентификации не поддерживает изменение пароля.", + "Password for SOLR authentication (optional)": "Пароль для аутентификации SOLR (необязательно)", + "Password updated successfully": "Пароль успешно обновлён", + "Path": "Путь", + "Path in repository": "Путь в репозитории", + "Path where the configuration file will be saved in the repository": "Путь сохранения файла конфигурации в репозитории", + "Path where the register OAS file will be saved in the repository": "Путь сохранения файла OAS реестра в репозитории", + "Pattern (regex)": "Шаблон (regex)", + "Pattern Issue": "Проблема шаблона", + "Pattern constraint is missing": "Ограничение шаблона отсутствует", + "Patterns:": "Шаблоны:", + "Pending": "Ожидает", + "Permanent Delete Mode": "Режим безвозвратного удаления", + "Permanently Delete": "Удалить навсегда", + "Permanently Delete ({count})": "Удалить навсегда ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Безвозвратно удалить все {count} программно удалённых объекта. Это действие нельзя отменить!", + "Permanently delete already soft-deleted objects": "Безвозвратно удалить уже программно удалённые объекты", + "Permanently delete this conversation?": "Безвозвратно удалить эту переписку?", + "Permission Matrix": "Матрица разрешений", + "Person": "Персона", + "Personal Data": "Персональные данные", + "Personal Data Export": "Экспорт персональных данных", + "Persons": "Персоны", + "Phone": "Телефон", + "Phone Numbers": "Номера телефонов", + "Photos": "Фотографии", + "Please create an agent in the": "Пожалуйста, создайте агента в", + "Please select a user": "Пожалуйста, выберите пользователя", + "Please select an agent to continue": "Пожалуйста, выберите агента для продолжения", + "Please select an organisation": "Пожалуйста, выберите организацию", + "Please select at least one item to import": "Пожалуйста, выберите хотя бы один элемент для импорта", + "Please select registers or schemas first": "Пожалуйста, сначала выберите реестры или схемы", + "Please select which register and schema to use for the new object": "Пожалуйста, выберите реестр и схему для нового объекта", + "Please try again later.": "Пожалуйста, повторите попытку позже.", + "Please wait while we fetch your agents.": "Пожалуйста, подождите, пока загружаются ваши агенты.", + "Please wait while we fetch your applications.": "Пожалуйста, подождите, пока загружаются ваши приложения.", + "Please wait while we fetch your configurations.": "Пожалуйста, подождите, пока загружаются ваши конфигурации.", + "Please wait while we fetch your deleted items.": "Пожалуйста, подождите, пока загружаются ваши удалённые элементы.", + "Please wait while we fetch your sources.": "Пожалуйста, подождите, пока загружаются ваши источники.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Пожалуйста, подождите, пока коллекция SOLR безвозвратно удаляется. Это может занять некоторое время.", + "Polls": "Опросы", + "Popular Search Terms": "Популярные поисковые запросы", + "Port": "Порт", + "Portabiliteit (Art 20)": "Portabiliteit (Ст. 20)", + "Preferences saved": "Предпочтения сохранены", + "Prerequisites": "Предварительные условия", + "Press Enter to send, Shift+Enter for new line": "Нажмите Enter для отправки, Shift+Enter для новой строки", + "Press enter to send, shift+enter for new line": "Нажмите Enter для отправки, Shift+Enter для новой строки", + "Prettify": "Форматировать", + "Preview erasure": "Предпросмотр стирания", + "Preview:": "Предпросмотр:", + "Previous": "Предыдущий", + "Private": "Частный", + "Private Agent (Default)": "Частный агент (по умолчанию)", + "Private agents": "Частные агенты", + "Process Name (optional)": "Название процесса (необязательно)", + "Processed {indexed} files, {failed} failed": "Обработано {indexed} файлов, {failed} не удалось", + "Processes chunks in batches with simulated parallelism.": "Обрабатывает фрагменты пакетами с симулированным параллелизмом.", + "Processes file chunks sequentially (safest).": "Обрабатывает фрагменты файлов последовательно (самый безопасный режим).", + "Processes objects in chunks with simulated parallelism.": "Обрабатывает объекты фрагментами с симулированным параллелизмом.", + "Processes objects sequentially (safest).": "Обрабатывает объекты последовательно (самый безопасный режим).", + "Processing": "Обработка", + "Processing Limits": "Ограничения обработки", + "Processing...": "Обработка...", + "Project initialized successfully": "Проект успешно инициализирован", + "Project not found. Please initialize first.": "Проект не найден. Пожалуйста, сначала инициализируйте.", + "Properties": "Свойства", + "Property Behaviors": "Поведение свойств", + "Property Configuration:": "Конфигурация свойства:", + "Property Filters": "Фильтры свойств", + "Property Title": "Заголовок свойства", + "Property Type": "Тип свойства", + "Property can be improved": "Свойство можно улучшить", + "Property could not be added": "Свойство не удалось добавить", + "Property could not be updated": "Свойство не удалось обновить", + "Property name of inversed relation": "Имя свойства обратной связи", + "Property successfully added": "Свойство успешно добавлено", + "Property successfully updated": "Свойство успешно обновлено", + "Protected": "Защищённый", + "Provider": "Провайдер", + "Provider is required for testing": "Для тестирования необходим провайдер", + "Public": "Публичный", + "Public View": "Публичное представление", + "Public views can be accessed by anyone in the system": "Публичные представления доступны всем пользователям системы", + "Publish": "Опубликовать", + "Publish Configuration to GitHub": "Опубликовать конфигурацию на GitHub", + "Publish OAS": "Опубликовать OAS", + "Publish Objects": "Опубликовать объекты", + "Publish Register OAS to GitHub": "Опубликовать OAS реестра на GitHub", + "Publish all objects in this schema": "Опубликовать все объекты в этой схеме", + "Publish objects to make them publicly available": "Опубликовать объекты для открытого доступа", + "Published": "Опубликовано", + "Published:": "Опубликовано:", + "Publishing...": "Публикация...", + "Purge": "Очистить", + "Purge Date": "Дата очистки", + "Purge {count} object from database": "Очистить {count} объект из базы данных", + "Purge {count} objects from database": "Очистить {count} объектов из базы данных", + "Purpose limitation *": "Ограничение цели *", + "Push Notifications": "Push-уведомления", + "Query Complexity Distribution": "Распределение сложности запросов", + "Query Help": "Справка по запросам", + "Query parameter is required": "Параметр запроса обязателен", + "Quota": "Квота", + "RAG": "RAG", + "RAG Configuration": "Конфигурация RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Настройки RBAC успешно обновлены", + "Rate limits cleared successfully": "Ограничения частоты запросов успешно очищены", + "Raw Changes Data": "Необработанные данные изменений", + "Re-vectorize on object update": "Повторно векторизовать при обновлении объекта", + "Read": "Чтение", + "Real-time push notification status via notify_push": "Статус push-уведомлений в реальном времени через notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push-уведомления в реальном времени активны. Подключённые клиенты получают мгновенные обновления при создании, обновлении или удалении объектов.", + "Realtime push active": "Push в реальном времени активен", + "Realtime push not available — the notify_push app is not installed": "Push в реальном времени недоступен — приложение notify_push не установлено", + "Reason": "Причина", + "Reason (optional)": "Причина (необязательно)", + "Rebase operation completed successfully": "Операция перебазирования успешно завершена", + "Rebase operation failed: {error}": "Операция перебазирования не удалась: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Рекомендации:", + "Recommended Type:": "Рекомендуемый тип:", + "Recommended: 5 sources": "Рекомендуется: 5 источников", + "Refresh": "Обновить", + "Refresh Data": "Обновить данные", + "Refresh Stats": "Обновить статистику", + "Refresh Workflows": "Обновить рабочие процессы", + "Refresh dashboard": "Обновить дашборд", + "Refresh database info": "Обновить информацию о базе данных", + "Register": "Реестр", + "Register #{id}": "Реестр #{id}", + "Register / Schema": "Реестр / Схема", + "Register ID": "ID реестра", + "Register Objects": "Объекты реестра", + "Register Overview": "Обзор реестра", + "Register Statistics": "Статистика реестра", + "Register Totals": "Итоги реестра", + "Register and schema are required": "Реестр и схема обязательны", + "Register annotation": "Аннотация реестра", + "Register depublished successfully": "Реестр успешно снят с публикации", + "Register not found": "Реестр не найден", + "Register objects": "Объекты реестра", + "Register objects deletion completed successfully": "Удаление объектов реестра успешно завершено", + "Register published successfully": "Реестр успешно опубликован", + "Register/Schema Usage": "Использование реестра/схемы", + "Register:": "Реестр:", + "Registers": "Реестры", + "Reindex": "Переиндексировать", + "Reindex completed": "Переиндексация завершена", + "Reindex completed successfully": "Переиндексация успешно завершена", + "Reindex completed! Processed {count} objects in {duration}s": "Переиндексация завершена! Обработано {count} объектов за {duration}с", + "Reindex failed": "Переиндексация не удалась", + "Reindex failed: %s": "Переиндексация не удалась: %s", + "Reject": "Отклонить", + "Rejected": "Отклонено", + "Related Cases": "Связанные дела", + "Relations": "Связи", + "Remote Version": "Удалённая версия", + "Remove": "Удалить", + "Remove Deck card": "Удалить карточку Deck", + "Remove avatar": "Удалить аватар", + "Remove contact": "Удалить контакт", + "Remove filter": "Удалить фильтр", + "Remove from favorites": "Удалить из избранного", + "Remove from list": "Удалить из списка", + "Remove group": "Удалить группу", + "Remove link": "Удалить ссылку", + "Remove link to {name}": "Удалить ссылку на {name}", + "Remove link to {name}?": "Удалить ссылку на {name}?", + "Remove link to {title}": "Удалить ссылку на {title}", + "Remove oneOf entry": "Удалить запись oneOf", + "Remove property after write-back": "Удалить свойство после обратной записи", + "Remove {title}": "Удалить {title}", + "Remove {userId} from this organisation?": "Удалить {userId} из этой организации?", + "Removed": "Удалено", + "Removed from favorites": "Удалено из избранного", + "Removing schema {schema}...": "Удаление схемы {schema}...", + "Rename Conversation": "Переименовать переписку", + "Rename conversation": "Переименовать переписку", + "Replicas": "Реплики", + "Reports": "Отчёты", + "Repository": "Репозиторий", + "Request Body": "Тело запроса", + "Request Data": "Данные запроса", + "Request Details": "Детали запроса", + "Request Quota (per day)": "Квота запросов (в день)", + "Request account deactivation": "Запросить деактивацию аккаунта", + "Request account deactivation. This will notify administrators for review.": "Запросить деактивацию аккаунта. Это уведомит администраторов для проверки.", + "Request body must be JSON with a \"query\" field": "Тело запроса должно быть JSON с полем \"query\"", + "Request timeout in seconds": "Тайм-аут запроса в секундах", + "Requested at": "Запрошено в", + "Required": "Обязательно", + "Required Token Scopes": "Обязательные области токена", + "Required field": "Обязательное поле", + "Required status is inconsistent": "Статус обязательности несогласован", + "Requires Dolphin AI for OCR text extraction": "Требуется Dolphin AI для извлечения текста OCR", + "Rerun Search": "Повторить поиск", + "Rerunning search: {searchTerm}": "Повтор поиска: {searchTerm}", + "Reset Changes": "Сбросить изменения", + "Reset Filters": "Сбросить фильтры", + "Resource Quotas": "Квоты ресурсов", + "Response Body": "Тело ответа", + "Response Details": "Детали ответа", + "Response Time:": "Время ответа:", + "Restore": "Восстановить", + "Restore conversation": "Восстановить переписку", + "Restore or permanently delete items": "Восстановить или безвозвратно удалить элементы", + "Restore {count} object": "Восстановить {count} объект", + "Restore {count} objects": "Восстановить {count} объектов", + "Result Count Range": "Диапазон количества результатов", + "Results": "Результаты", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Период хранения (длительность ISO-8601, например P10Y, P30D)", + "Retention settings updated successfully": "Настройки хранения успешно обновлены", + "Retrieve all objects for this schema": "Получить все объекты для этой схемы", + "Retry": "Повторить", + "Retry Failed Extractions": "Повторить неудачные извлечения", + "Retry Policy": "Политика повтора", + "Retry completed": "Повтор завершён", + "Retry upload": "Повторить загрузку", + "Retrying...": "Повтор...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Просмотрите выбранные объекты ниже. Вы можете убрать любые объекты, которые не хотите удалять, нажав кнопку удаления.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Просмотрите выбранные объекты ниже. Вы можете убрать любые объекты, которые не хотите включать.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Просмотрите выбранные объекты ниже. Вы можете убрать любые объекты, которые не хотите безвозвратно удалять, нажав кнопку удаления. Это действие нельзя отменить.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Просмотрите выбранные объекты ниже. Вы можете убрать любые объекты, которые не хотите восстанавливать, нажав кнопку удаления. Объекты будут восстановлены в исходное расположение.", + "Revoke": "Отозвать", + "Right": "Право", + "Risk Level": "Уровень риска", + "Risk level": "Уровень риска", + "Roles and Permissions": "Роли и разрешения", + "Roles updated successfully": "Роли успешно обновлены", + "Run compliance scan": "Запустить проверку соответствия", + "SOLR Connection Settings": "Настройки подключения SOLR", + "SOLR Index Warmup": "Прогрев индекса SOLR", + "SOLR Version:": "Версия SOLR:", + "SOLR actions menu": "Меню действий SOLR", + "SOLR base path (usually /solr)": "Базовый путь SOLR (обычно /solr)", + "SOLR collection deleted successfully": "Коллекция SOLR успешно удалена", + "SOLR connection test failed: {error}": "Тест подключения SOLR не удался: {error}", + "SOLR connection test successful!": "Тест подключения SOLR успешен!", + "SOLR index warmup completed successfully": "Прогрев индекса SOLR успешно завершён", + "SOLR is disabled": "SOLR отключён", + "SOLR is not available or not configured": "SOLR недоступен или не настроен", + "SOLR logging disabled": "Логирование SOLR отключено", + "SOLR logging enabled": "Логирование SOLR включено", + "SOLR search disabled": "Поиск SOLR отключён", + "SOLR search enabled": "Поиск SOLR включён", + "SOLR server hostname or IP address": "Имя хоста или IP-адрес сервера SOLR", + "SOLR server port number (optional, defaults to 8983)": "Номер порта сервера SOLR (необязательно, по умолчанию 8983)", + "SOLR settings updated successfully": "Настройки SOLR успешно обновлены", + "SOLR setup completed successfully": "Настройка SOLR успешно завершена", + "SOLR setup completed successfully!": "Настройка SOLR успешно завершена!", + "SOLR setup error: %s": "Ошибка настройки SOLR: %s", + "SOLR setup failed": "Настройка SOLR не удалась", + "SOLR setup failed - check logs": "Настройка SOLR не удалась — проверьте журналы", + "SOLR setup failed. Please check the configuration and try again.": "Настройка SOLR не удалась. Проверьте конфигурацию и повторите попытку.", + "SOLR setup failed: {error}": "Настройка SOLR не удалась: {error}", + "SOLR warmup failed: {error}": "Прогрев SOLR не удался: {error}", + "Sample Values:": "Примеры значений:", + "Save": "Сохранить", + "Save Configuration": "Сохранить конфигурацию", + "Save Connection Settings": "Сохранить настройки подключения", + "Save Roles": "Сохранить роли", + "Save Settings": "Сохранить настройки", + "Save changes": "Сохранить изменения", + "Save current search as view": "Сохранить текущий поиск как представление", + "Save labels": "Сохранить метки", + "Save tags for {name}": "Сохранить теги для {name}", + "Save the organisation first to manage users.": "Сначала сохраните организацию для управления пользователями.", + "Saved Views": "Сохранённые представления", + "Saving...": "Сохранение...", + "Scheduled": "Запланировано", + "Schema": "Схема", + "Schema ID": "ID схемы", + "Schema Name": "Название схемы", + "Schema Statistics": "Статистика схемы", + "Schema annotation": "Аннотация схемы", + "Schema configuration updated successfully for {schema}": "Конфигурация схемы для {schema} успешно обновлена", + "Schema depublished successfully": "Схема успешно снята с публикации", + "Schema objects deletion completed successfully": "Удаление объектов схемы успешно завершено", + "Schema published successfully": "Схема успешно опубликована", + "Schema reference ($ref)": "Ссылка на схему ($ref)", + "Schema successfully created": "Схема успешно создана", + "Schema successfully updated": "Схема успешно обновлена", + "Schema {schema} removed successfully": "Схема {schema} успешно удалена", + "Schema:": "Схема:", + "Schemas": "Схемы", + "Scheme": "Схема", + "Search": "Поиск", + "Search / Views": "Поиск / Представления", + "Search / views": "Поиск / представления", + "Search Activity": "Поисковая активность", + "Search Analytics": "Аналитика поиска", + "Search Configuration": "Конфигурация поиска", + "Search Entities": "Поиск объектов", + "Search Files": "Поиск файлов", + "Search GitHub": "Поиск на GitHub", + "Search GitLab": "Поиск на GitLab", + "Search Method": "Метод поиска", + "Search Mode": "Режим поиска", + "Search Query": "Поисковый запрос", + "Search Results": "Результаты поиска", + "Search Term": "Поисковый запрос", + "Search Trail Details": "Детали журнала поиска", + "Search Trail Management": "Управление журналом поиска", + "Search Trail Statistics": "Статистика журнала поиска", + "Search Trails": "Журналы поиска", + "Search Views": "Поиск представлений", + "Search Webhooks": "Поиск веб-хуков", + "Search agents...": "Поиск агентов...", + "Search applications...": "Поиск приложений...", + "Search by file name or path": "Поиск по имени файла или пути", + "Search by name or URL": "Поиск по имени или URL", + "Search by title or UUID...": "Поиск по заголовку или UUID...", + "Search by value": "Поиск по значению", + "Search configurations": "Поиск конфигураций", + "Search data sources...": "Поиск источников данных...", + "Search groups...": "Поиск групп...", + "Search in Database Objects": "Поиск в объектах базы данных", + "Search in Files": "Поиск в файлах", + "Search in Objects": "Поиск в объектах", + "Search objects": "Поиск объектов", + "Search objects...": "Поиск объектов...", + "Search property names...": "Поиск имён свойств...", + "Search registers...": "Поиск реестров...", + "Search schemas...": "Поиск схем...", + "Search trail deletion not implemented yet": "Удаление журнала поиска ещё не реализовано", + "Search views...": "Поиск представлений...", + "Search {name}...": "Поиск {name}...", + "Searching...": "Поиск...", + "Secret": "Секрет", + "Secret key for HMAC signature generation (optional)": "Секретный ключ для генерации подписи HMAC (необязательно)", + "Security": "Безопасность", + "See organisations as a table": "Просматривать организации в виде таблицы", + "See organisations as cards": "Просматривать организации в виде карточек", + "Select AI Agent": "Выбрать ИИ-агента", + "Select Active Organisation": "Выбрать активную организацию", + "Select All": "Выбрать всё", + "Select ConfigSet": "Выбрать набор конфигурации", + "Select File Types to Vectorize:": "Выбрать типы файлов для векторизации:", + "Select HTTP method": "Выбрать метод HTTP", + "Select Register and Schema": "Выбрать реестр и схему", + "Select Views to Vectorize:": "Выбрать представления для векторизации:", + "Select a Nextcloud group to add": "Выбрать группу Nextcloud для добавления", + "Select a branch": "Выбрать ветку", + "Select a date property": "Выбрать свойство даты", + "Select a model or type a custom model name": "Выберите модель или введите пользовательское имя модели", + "Select a register": "Выбрать реестр", + "Select a register...": "Выбрать реестр...", + "Select a repository": "Выбрать репозиторий", + "Select a repository you have write access to": "Выберите репозиторий, к которому у вас есть доступ на запись", + "Select a schema": "Выбрать схему", + "Select a schema...": "Выбрать схему...", + "Select a user": "Выбрать пользователя", + "Select agent type": "Выбрать тип агента", + "Select all entities": "Выбрать все объекты", + "Select an AI Agent": "Выбрать ИИ-агента", + "Select an AI agent to begin chatting with your data.": "Выберите ИИ-агента для начала общения с вашими данными.", + "Select an email to see linked objects": "Выберите письмо для просмотра связанных объектов", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Выберите организацию и пользователя для добавления в качестве участника. Поиск организаций по имени.", + "Select backend": "Выбрать backend", + "Select chat model": "Выбрать модель чата", + "Select collection for files": "Выбрать коллекцию для файлов", + "Select collection for objects": "Выбрать коллекцию для объектов", + "Select configuration type...": "Выбрать тип конфигурации...", + "Select default organisation": "Выбрать организацию по умолчанию", + "Select entity {value}": "Выбрать объект {value}", + "Select event to listen to...": "Выбрать событие для прослушивания...", + "Select fields to display": "Выбрать поля для отображения", + "Select files with the correct extension": "Выбрать файлы с правильным расширением", + "Select group": "Выбрать группу", + "Select groups (optional)": "Выбрать группы (необязательно)", + "Select groups to notify...": "Выбрать группы для уведомления...", + "Select groups with access to this agent": "Выбрать группы с доступом к этому агенту", + "Select groups...": "Выбрать группы...", + "Select model": "Выбрать модель", + "Select one or more registers": "Выбрать один или несколько реестров", + "Select one or more schemas": "Выбрать одну или несколько схем", + "Select options...": "Выбрать параметры...", + "Select or create labels, or select \"No label\" to add files": "Выберите или создайте метки, или выберите «Без метки», чтобы добавить файлы", + "Select owner application (optional)...": "Выбрать приложение-владелец (необязательно)...", + "Select period": "Выбрать период", + "Select property to send as payload": "Выбрать свойство для отправки в качестве полезной нагрузки", + "Select provider": "Выбрать провайдера", + "Select registers and schemas to save a view": "Выберите реестры и схемы для сохранения представления", + "Select retry policy": "Выбрать политику повтора", + "Select role": "Выбрать роль", + "Select schemas (empty = all schemas)": "Выбрать схемы (пусто = все схемы)", + "Select schemas to warm up": "Выбрать схемы для прогрева", + "Select search mode": "Выбрать режим поиска", + "Select source type...": "Выбрать тип источника...", + "Select target type": "Выбрать тип цели", + "Select the branch to publish to": "Выбрать ветку для публикации", + "Select the event this webhook should listen to": "Выбрать событие, которое должен прослушивать этот веб-хук", + "Select views to filter data (optional)": "Выбрать представления для фильтрации данных (необязательно)", + "Select views to limit which data the agent can access": "Выбрать представления для ограничения доступных агенту данных", + "Select views to vectorize:": "Выбрать представления для векторизации:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Выберите группы Nextcloud, доступные для этой организации. Пользователи в этих группах получат доступ к ресурсам организации.", + "Select which data views the AI can search": "Выбрать представления данных, которые ИИ может искать", + "Select which property from the event should be used as the webhook payload data": "Выбрать свойство события для использования в качестве данных полезной нагрузки веб-хука", + "Select which tools the AI can use to perform actions": "Выбрать инструменты, которые ИИ может использовать для выполнения действий", + "Select {fieldName} values": "Выбрать значения {fieldName}", + "Selected": "Выбрано", + "Selected Groups": "Выбранные группы", + "Selected Objects ({count})": "Выбранные объекты ({count})", + "Selected audit trails deleted successfully": "Выбранные записи журнала аудита успешно удалены", + "Selected search trails deleted successfully": "Выбранные записи журнала поиска успешно удалены", + "Selected users can access this view": "Выбранные пользователи могут получить доступ к этому представлению", + "Send additional feedback": "Отправить дополнительный отзыв", + "Send as CloudEvent": "Отправить как CloudEvent", + "Send as cloudevent": "Отправить как CloudEvent", + "Sensitive PII": "Конфиденциальные персональные данные", + "Serial Mode (Safer, slower)": "Последовательный режим (безопаснее, медленнее)", + "Serial:": "Последовательно:", + "Server Configuration": "Конфигурация сервера", + "Server Information": "Информация о сервере", + "Set as Active": "Установить как активный", + "Set as active organisation": "Установить как активную организацию", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Установите ограничения для использования API и потребления токенов. Используйте 0 для неограниченных ресурсов.", + "Settings": "Настройки", + "Settings updated successfully": "Настройки успешно обновлены", + "Shards": "Шарды", + "Share": "Поделиться", + "Share with Groups": "Поделиться с группами", + "Share with Users": "Поделиться с пользователями", + "Show": "Показать", + "Show Filters": "Показать фильтры", + "Show less": "Показать меньше", + "Show only entries with changes": "Показывать только записи с изменениями", + "Showing {count} dashboard(s)": "Показано {count} дашборд(ов)", + "Showing {count} processing activities": "Показано {count} операций обработки", + "Showing {showing} of {total} agents": "Показано {showing} из {total} агентов", + "Showing {showing} of {total} applications": "Показано {showing} из {total} приложений", + "Showing {showing} of {total} audit trail entries": "Показано {showing} из {total} записей журнала аудита", + "Showing {showing} of {total} configurations": "Показано {showing} из {total} конфигураций", + "Showing {showing} of {total} deleted items": "Показано {showing} из {total} удалённых элементов", + "Showing {showing} of {total} entities": "Показано {showing} из {total} объектов", + "Showing {showing} of {total} files": "Показано {showing} из {total} файлов", + "Showing {showing} of {total} log entries": "Показано {showing} из {total} записей журнала", + "Showing {showing} of {total} search trail entries": "Показано {showing} из {total} записей журнала поиска", + "Showing {showing} of {total} sources": "Показано {showing} из {total} источников", + "Showing {showing} of {total} templates": "Показано {showing} из {total} шаблонов", + "Showing {showing} of {total} webhooks": "Показано {showing} из {total} веб-хуков", + "Showing {shown} of {total} organisations": "Показано {shown} из {total} организаций", + "Simple": "Простой", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Простые запросы: базовый текстовый поиск с минимальными параметрами (например, один поисковый запрос без расширенных фильтров)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Простые запросы: базовый текстовый поиск с минимальными параметрами (например, один поисковый запрос без расширенных фильтров)", + "Size": "Размер", + "Slug": "Slug", + "Soft Delete Mode": "Режим программного удаления", + "Soft Deleted Items": "Программно удалённые элементы", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Программно удалить все объекты этой схемы ({active} активных, {deleted} уже удалённых)", + "SolrCloud mode enabled": "Режим SolrCloud включён", + "Something went wrong while adding files": "При добавлении файлов что-то пошло не так", + "Source": "Источник", + "Source successfully updated": "Источник успешно обновлён", + "Sources": "Источники", + "Sources:": "Источники:", + "Special Rights": "Особые права", + "Standalone SOLR mode": "Автономный режим SOLR", + "Start Conversation": "Начать переписку", + "Start Date Field": "Поле даты начала", + "Start Vectorization": "Начать векторизацию", + "Start a conversation": "Начать переписку", + "Starting names cache warmup...": "Начало прогрева кэша имён...", + "Starting validation for {schema}...": "Начало проверки {schema}...", + "Starting {action} for {schema}...": "Начало {action} для {schema}...", + "Starting...": "Запуск...", + "Statistics": "Статистика", + "Statistics and Metrics": "Статистика и метрики", + "Statistics refreshed": "Статистика обновлена", + "Stats calculation not yet implemented": "Расчёт статистики ещё не реализован", + "Status": "Статус", + "Status Code": "Код статуса", + "Step": "Шаг", + "Storage": "Хранилище", + "Storage Quota (MB)": "Квота хранилища (МБ)", + "String Constraints": "Строковые ограничения", + "Subject identifier (email, BSN, name, etc.)": "Идентификатор субъекта (email, BSN, имя и т.д.)", + "Subscribe": "Подписаться", + "Subscribed": "Подписан", + "Success": "Успех", + "Success Rate": "Процент успеха", + "Success Status": "Статус успеха", + "Successful": "Успешно", + "Successfully added user to organisation: {name}": "Пользователь успешно добавлен в организацию: {name}", + "Successfully cleared {count} audit trails": "Успешно очищено {count} записей журнала аудита", + "Successfully cleared {count} blob storage objects": "Успешно очищено {count} объектов blob-хранилища", + "Successfully cleared {count} search trails": "Успешно очищено {count} записей журнала поиска", + "Successfully created {count} SOLR fields": "Успешно создано {count} полей SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Успешно удалено {count} векторных представлений. Пожалуйста, повторно векторизуйте ваши данные.", + "Successfully deleted {count} objects for {schema}": "Успешно удалено {count} объектов для {schema}", + "Successfully fixed {count} SOLR field configurations": "Успешно исправлено {count} конфигураций полей SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Успешно импортировано: {registers} реестров, {schemas} схем, {objects} объектов", + "Successfully joined organisation": "Вы успешно присоединились к организации", + "Successfully permanently deleted {count} objects": "Успешно безвозвратно удалено {count} объектов", + "Successfully processed {count} files!": "Успешно обработано {count} файлов!", + "Successfully restored {count} object": "Успешно восстановлен {count} объект", + "Successfully restored {count} objects": "Успешно восстановлено {count} объектов", + "Successfully saved configuration for {count} facets!": "Конфигурация для {count} фасетов успешно сохранена!", + "Supported File Types": "Поддерживаемые типы файлов", + "Switch Active Organisation": "Переключить активную организацию", + "Switch Organisation": "Переключить организацию", + "Sync Table": "Синхронизировать таблицу", + "Synchronization Settings": "Настройки синхронизации", + "Syncing magic table for {schema}...": "Синхронизация magic table для {schema}...", + "Syncs": "Синхронизации", + "System Default": "Системное значение по умолчанию", + "System Prompt": "Системный промпт", + "System announcements": "Системные объявления", + "Table": "Таблица", + "Target ID": "ID цели", + "Target Type*": "Тип цели*", + "Technical Description": "Техническое описание", + "Technical Details": "Технические детали", + "Technical description for developers and administrators": "Техническое описание для разработчиков и администраторов", + "Technical measures": "Технические меры", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Температура", + "Templates": "Шаблоны", + "Test": "Тест", + "Test Chat": "Тест чата", + "Test Connection": "Проверить подключение", + "Test Embedding": "Тест векторных представлений", + "Test endpoint executed successfully": "Тест конечной точки выполнен успешно", + "Test webhook delivered successfully": "Тестовый веб-хук успешно доставлен", + "Test webhook delivery failed": "Доставка тестового веб-хука не удалась", + "Test webhook sent successfully": "Тестовый веб-хук успешно отправлен", + "Testing...": "Тестирование...", + "Text Chunk #{id}": "Текстовый фрагмент #{id}", + "Text Extraction": "Извлечение текста", + "Text Extraction Engine": "Движок извлечения текста", + "Text chunks": "Текстовые фрагменты", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Текстовые фрагменты создаются в процессе извлечения файлов и сохраняются в базе данных. Векторизация читает эти предварительно разбитые файлы и преобразует их в векторные представления.", + "Text extracted successfully": "Текст успешно извлечён", + "Text extraction disabled": "Извлечение текста отключено", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Приложение Nextcloud Календарь не установлено или не включено на этом сервере.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Приложение Nextcloud Контакты не установлено или не включено на этом сервере.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Приложение Nextcloud Deck не установлено или не включено на этом сервере.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Приложение Nextcloud Почта не установлено или не включено на этом сервере.", + "The URL where webhook events will be sent": "URL, на который будут отправляться события веб-хука", + "The requested conversation does not exist": "Запрошенная переписка не существует", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Запрошенный дашборд не удалось загрузить. Проверьте URL или выберите другой дашборд из списка.", + "The saved view and all its search configuration": "Сохранённое представление и вся его конфигурация поиска", + "There are no audit trail entries matching your current filters.": "Нет записей журнала аудита, соответствующих вашим текущим фильтрам.", + "There are no deleted items matching your current filters.": "Нет удалённых элементов, соответствующих вашим текущим фильтрам.", + "There are no search trail entries matching your current filters.": "Нет записей журнала поиска, соответствующих вашим текущим фильтрам.", + "There are no webhook log entries matching your filters.": "Нет записей журнала веб-хука, соответствующих вашим фильтрам.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Они будут храниться в соответствии с настроенным периодом хранения схемы и автоматически безвозвратно удалены по истечении периода хранения. Период хранения настраивается для каждой схемы и находится в настройках схемы.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Они будут храниться в соответствии с настроенным периодом хранения схемы, а затем безвозвратно удалены.", + "This action cannot be undone.": "Это действие нельзя отменить.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Это действие нельзя отменить. Убедитесь, что ни одна коллекция не использует этот набор конфигурации.", + "This action cannot be undone. Make sure no collections are using this configset.": "Это действие нельзя отменить. Убедитесь, что ни одна коллекция не использует этот набор конфигурации.", + "This action will submit a deactivation request to your administrators.": "Это действие отправит запрос на деактивацию вашим администраторам.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "У этого агента нет настраиваемых представлений или инструментов, но вы можете изменить настройки RAG ниже.", + "This analysis may take some time": "Этот анализ может занять некоторое время", + "This audit trail entry does not contain any change information.": "Эта запись журнала аудита не содержит информации об изменениях.", + "This configuration has no data defined.": "Для этой конфигурации не определены данные.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Эта конечная точка устарела. Вместо неё используйте конечные точки на основе фрагментов.", + "This entity has no relations to objects or files": "Этот объект не имеет связей с объектами или файлами", + "This is a test webhook from OpenRegister": "Это тестовый веб-хук от OpenRegister", + "This is not valid JSON": "Это не является допустимым JSON", + "This key already exists on this schema": "Этот ключ уже существует в этой схеме", + "This schema must use Magic Table configuration to sync": "Эта схема должна использовать конфигурацию Magic Table для синхронизации", + "This schema must use magic table configuration to sync": "Эта схема должна использовать конфигурацию magic table для синхронизации", + "This source has no associated registers.": "Этот источник не имеет связанных реестров.", + "This token will only be shown once. Copy it now.": "Этот токен будет показан только один раз. Скопируйте его сейчас.", + "This will copy the _default ConfigSet with the new name": "Это скопирует набор конфигурации _default с новым именем", + "This will copy the _default configset with the new name": "Это скопирует набор конфигурации _default с новым именем", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Это безвозвратно удалит ВСЕ векторные представления из базы данных. Вам потребуется повторно векторизовать все объекты и файлы. Это действие нельзя отменить.\n\nВы уверены, что хотите продолжить?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Это безвозвратно удалит ВСЕ векторные представления из базы данных. Вам потребуется повторно векторизовать все объекты и файлы. Это действие нельзя отменить.\\n\\nВы уверены, что хотите продолжить?", + "This will permanently delete:": "Это безвозвратно удалит:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Это безвозвратно удалит {count} уже программно удалённых объектов из базы данных. Это действие нельзя отменить.", + "Timeout (seconds)": "Тайм-аут (секунды)", + "Timestamp": "Временная метка", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Совет: включайте только те представления, которым нужен семантический поиск, чтобы снизить затраты на векторные представления. Простые таблицы поиска редко нуждаются в векторизации.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Совет: включайте только те представления, которым нужен семантический поиск, чтобы снизить затраты на векторные представления. Простые таблицы поиска редко нуждаются в векторизации.", + "Title": "Заголовок", + "Title *": "Заголовок *", + "Title Template": "Шаблон заголовка", + "Title*": "Заголовок*", + "To (e.g. -1 year or 2025-12-31)": "По (например, -1 year или 2025-12-31)", + "To Date": "Дата окончания", + "To add files larger than or equal to 512MB, go to the": "Для добавления файлов размером 512 МБ и более перейдите в", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Для изменения размера фрагмента или стратегии перейдите в Конфигурация файлов → Ограничения обработки.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Для изменения размера фрагмента или стратегии перейдите в конфигурация файлов → ограничения обработки.", + "To change the embedding provider or model, go to LLM Configuration.": "Для изменения провайдера или модели векторных представлений перейдите в Настройка LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Для изменения провайдера или модели векторных представлений перейдите в настройку LLM.", + "To date": "По дату", + "Toggle search sidebar": "Переключить боковую панель поиска", + "Token Created": "Токен создан", + "Token Quota (per request)": "Квота токенов (на запрос)", + "Token copied to clipboard": "Токен скопирован в буфер обмена", + "Token name": "Имя токена", + "Token revoked": "Токен отозван", + "Tools": "Инструменты", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Инструменты выполняются с правами пользователя агента по умолчанию, когда пользовательский сеанс неактивен (например, задания cron). Настройте пользователя по умолчанию на вкладке Настройки.", + "Tools respect the agent's own role and organization boundaries.": "Инструменты соблюдают собственную роль агента и границы организации.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Инструменты соблюдают представления, разрешения и границы организации агента.", + "Top Deleters": "Наибольшее число удалений", + "Total": "Итого", + "Total Audit Trails": "Всего журналов аудита", + "Total Chunks Available:": "Доступно фрагментов:", + "Total Deleted Items": "Всего удалённых элементов", + "Total Members": "Всего участников", + "Total Objects": "Всего объектов", + "Total Objects in Database:": "Всего объектов в базе данных:", + "Total Organisations": "Всего организаций", + "Total Results": "Всего результатов", + "Total Searches": "Всего поисков", + "Total Size": "Общий размер", + "Total Storage": "Общее хранилище", + "Total events": "Всего событий", + "Total:": "Итого:", + "Totals": "Итоги", + "Try searching by UUID or with different keywords": "Попробуйте поиск по UUID или с другими ключевыми словами", + "Type": "Тип", + "Type \"DELETE COLLECTION\" to confirm:": "Введите «DELETE COLLECTION» для подтверждения:", + "Type Issue": "Проблема типа", + "Type Variations:": "Варианты типа:", + "Type filter (optional, e.g. email)": "Фильтр типа (необязательно, например email)", + "Type must be \"positive\" or \"negative\"": "Тип должен быть «positive» или «negative»", + "Type to search for groups": "Введите для поиска групп", + "Type to search for objects...": "Введите для поиска объектов...", + "Type to search for organisations": "Введите для поиска организаций", + "Type to search for users": "Введите для поиска пользователей", + "Type to search groups...": "Введите для поиска групп...", + "Type to search users...": "Введите для поиска пользователей...", + "Type to search...": "Введите для поиска...", + "Type your username to confirm": "Введите имя пользователя для подтверждения", + "URL": "URL", + "URL where Ollama is running": "URL, по которому работает Ollama", + "UUID copied to clipboard": "UUID скопирован в буфер обмена", + "UUID:": "UUID:", + "Unchanged": "Без изменений", + "Unique Search Terms": "Уникальные поисковые запросы", + "Unique Terms": "Уникальные термины", + "Unknown": "Неизвестно", + "Unknown Object": "Неизвестный объект", + "Unknown Register": "Неизвестный реестр", + "Unknown Schema": "Неизвестная схема", + "Unknown error": "Неизвестная ошибка", + "Unknown widget type: {type}": "Неизвестный тип виджета: {type}", + "Unlink email": "Отвязать письмо", + "Unlink event": "Отвязать событие", + "Unnamed Object": "Объект без имени", + "Untitled": "Без названия", + "Untitled View": "Представление без названия", + "Update": "Обновить", + "Update Operations": "Операции обновления", + "Update available: {local} → {remote}": "Доступно обновление: {local} → {remote}", + "Update configuration: ...": "Обновить конфигурацию: ...", + "Update register OAS: ...": "Обновить OAS реестра: ...", + "Update vectors when object data changes (recommended for accurate search)": "Обновлять векторы при изменении данных объекта (рекомендуется для точного поиска)", + "Updated": "Обновлено", + "Updated At": "Обновлено в", + "Updated:": "Обновлено:", + "Upload new avatar": "Загрузить новый аватар", + "Uploading configuration sets...": "Загрузка наборов конфигурации...", + "Uptime:": "Время работы:", + "Url": "Url", + "Usage Count": "Количество использований", + "Use AI agents for processing and analysis": "Использовать ИИ-агентов для обработки и анализа", + "Use Large Language Model features": "Использовать функции больших языковых моделей", + "Use SolrCloud with Zookeeper for distributed search": "Использовать SolrCloud с Zookeeper для распределённого поиска", + "Use SolrCloud with zookeeper for distributed search": "Использовать SolrCloud с Zookeeper для распределённого поиска", + "Use VNG APIs instead of MongoDB.": "Использовать VNG API вместо MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Использовать диапазоны по умолчанию (последние 7/30/90 дней, последний год, старее)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Использовать внешнее хранилище (например, MongoDB) вместо внутреннего хранилища Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Используйте фильтры для сужения записей журнала аудита по реестру, схеме, типу действия, пользователю, диапазону дат или ID объекта.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Используйте фильтры для сужения удалённых элементов по реестру, схеме, дате удаления или пользователю, который их удалил.", + "Use filters to narrow down entities by type or category.": "Используйте фильтры для сужения объектов по типу или категории.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Используйте фильтры для сужения записей журнала поиска по реестру, схеме, статусу успеха, пользователю, диапазону дат, поисковым запросам или метрикам производительности.", + "Used By Collections": "Используется коллекциями", + "Used by": "Используется", + "User": "Пользователь", + "User Agent Statistics": "Статистика агентов пользователей", + "User Description": "Описание пользователя", + "User Quota": "Квота пользователя", + "User rate limits cleared successfully": "Ограничения частоты запросов пользователя успешно очищены", + "User removed successfully": "Пользователь успешно удалён", + "User-friendly description shown in forms and help text": "Понятное описание, отображаемое в формах и текстах справки", + "Username": "Имя пользователя", + "Username for SOLR authentication (optional)": "Имя пользователя для аутентификации SOLR (необязательно)", + "Users": "Пользователи", + "Uses": "Использует", + "Using Pre-Generated Chunks": "Использование предварительно сгенерированных фрагментов", + "Validate": "Проверить", + "Validate Objects": "Проверить объекты", + "Validation completed for {schema}": "Проверка {schema} завершена", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Проверка {schema} завершена: обработано {processed}, обновлено {updated}, не удалось {failed}", + "Validation completed successfully": "Проверка успешно завершена", + "Validation failed": "Проверка не удалась", + "Value": "Значение", + "Value list (split on ,)": "Список значений (разделять запятой)", + "Vector Dimensions": "Размерность вектора", + "Vector Search Backend": "Backend векторного поиска", + "Vector field: _embedding_": "Векторное поле: _embedding_", + "Vectorization Triggers": "Триггеры векторизации", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Векторизация завершена: векторизовано {vectorized} объектов, {failed} не удалось", + "Vectorization failed: {error}": "Векторизация не удалась: {error}", + "Vectorize All Files": "Векторизовать все файлы", + "Vectorize All Objects": "Векторизовать все объекты", + "Vectorize all file types": "Векторизовать все типы файлов", + "Vectorize all views": "Векторизовать все представления", + "Vectorize on object creation": "Векторизовать при создании объекта", + "Vectors will be stored in your existing object and file collections": "Векторы будут сохранены в существующих коллекциях объектов и файлов", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Проверка подключения к серверу...", + "Version": "Версия", + "Version Information": "Информация о версии", + "Very High": "Очень высокий", + "Very high": "Очень высокий", + "View": "Представление", + "View API Docs": "Просмотр документации API", + "View API Documentation": "Просмотр документации API", + "View APIs": "Просмотр API", + "View Changes": "Просмотр изменений", + "View Details": "Детали представления", + "View Error": "Ошибка представления", + "View File": "Просмотр файла", + "View Full Details": "Просмотреть полные детали", + "View Logs": "Просмотр журналов", + "View Name": "Название представления", + "View Object": "Просмотр объекта", + "View Parameters": "Параметры представления", + "View Selection": "Выбор представления", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Просматривать и анализировать журналы поиска с расширенными возможностями фильтрации и аналитики", + "View and analyze system audit trails with advanced filtering capabilities": "Просматривать и анализировать системные журналы аудита с расширенными возможностями фильтрации", + "View and manage authorization across registers and schemas": "Просматривать и управлять авторизацией в реестрах и схемах", + "View deleted successfully": "Представление успешно удалено", + "View entity details and manage relations": "Просматривать детали объекта и управлять связями", + "View name": "Название представления", + "View name is required": "Название представления обязательно", + "View object": "Просмотр объекта", + "View saved successfully!": "Представление успешно сохранено!", + "View search analytics and manage search logs": "Просматривать аналитику поиска и управлять журналами поиска", + "View successfully deleted": "Представление успешно удалено", + "View successfully updated": "Представление успешно обновлено", + "View updated successfully!": "Представление успешно обновлено!", + "View webhook delivery logs and filter by webhook": "Просматривать журналы доставки веб-хуков и фильтровать по веб-хуку", + "View {count} more": "Просмотреть ещё {count}", + "Views": "Представления", + "Visible to end users": "Видно конечным пользователям", + "Visible to users": "Видно пользователям", + "Wait for Response": "Ожидать ответа", + "Wait for webhook response before continuing (required for request/response flows)": "Ожидать ответ веб-хука перед продолжением (обязательно для потоков запрос/ответ)", + "Waiting for cluster synchronization...": "Ожидание синхронизации кластера...", + "Warmup Names Cache": "Прогреть кэш имён", + "Warmup failed": "Прогрев не удался", + "Warning": "Предупреждение", + "Warning:": "Предупреждение:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Предупреждение: это свойство имеет несогласованные типы: {types}. Рассмотрите, должны ли все объекты иметь одинаковый тип.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Мы просканируем все объекты, принадлежащие этой схеме, чтобы обнаружить новые свойства и проанализировать существующие свойства для потенциальных улучшений. Процесс включает изучение структуры данных каждого объекта, выявление свойств, не определённых в текущей схеме, и поиск возможностей улучшения существующих определений свойств с лучшими ограничениями, форматами и правилами проверки.", + "Webhook": "Веб-хук", + "Webhook Log Details": "Детали журнала веб-хука", + "Webhook Logs": "Журналы веб-хука", + "Webhook created successfully": "Веб-хук успешно создан", + "Webhook deleted": "Веб-хук удалён", + "Webhook retry delivered successfully": "Повтор веб-хука успешно доставлен", + "Webhook retry delivery failed": "Доставка повтора веб-хука не удалась", + "Webhook updated": "Веб-хук обновлён", + "Webhook updated successfully": "Веб-хук успешно обновлён", + "Webhooks": "Веб-хуки", + "Weekly": "Еженедельно", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Когда агент работает без пользовательского сеанса (например, запланированные задачи), будет использоваться контекст этого пользователя", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Когда агент работает без пользовательского сеанса (например, запланированные задачи), будет использоваться контекст этого пользователя.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "При отключении этот фасет будет показывать только значения из этой схемы и включать фильтр схемы при выборе.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "При включении сохранение этого объекта также обновит целевые объекты для включения обратной ссылки на этот объект.", + "When enabled, this property will be removed from the source object after updating the target objects.": "При включении это свойство будет удалено из исходного объекта после обновления целевых объектов.", + "Workflow Management": "Управление рабочими процессами", + "Workflows": "Рабочие процессы", + "Wrap webhook payload in CloudEvents format for better interoperability": "Обернуть полезную нагрузку веб-хука в формат CloudEvents для лучшей совместимости", + "Wrap webhook payload in cloudevents format for better interoperability": "Обернуть полезную нагрузку веб-хука в формат CloudEvents для лучшей совместимости", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Да", + "You": "Вы", + "You are not yet a member of any organisations.": "Вы ещё не являетесь участником ни одной организации.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Вы можете создавать новые наборы конфигурации на основе шаблона _default или загружать пользовательские непосредственно на сервер SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Вы можете создавать новые наборы конфигурации на основе шаблона _default или загружать пользовательские непосредственно на сервер SOLR.", + "You do not have access to this conversation": "У вас нет доступа к этой переписке", + "You do not have permission to delete this conversation": "У вас нет разрешения на удаление этой переписки", + "You do not have permission to modify this conversation": "У вас нет разрешения на изменение этой переписки", + "You do not have permission to restore this conversation": "У вас нет разрешения на восстановление этой переписки", + "You do not have permission to view the permission matrix. Admin access is required.": "У вас нет разрешения для просмотра матрицы разрешений. Требуется доступ администратора.", + "You have selected {count} properties to add to the schema.": "Вы выбрали {count} свойств для добавления в схему.", + "You must be logged in to favorite views": "Для добавления представлений в избранное необходимо войти в систему", + "You need an AI agent to start a conversation.": "Для начала переписки вам нужен ИИ-агент.", + "Your Fireworks AI API key. Get one at": "Ваш API-ключ Fireworks AI. Получить по адресу", + "Your OpenAI API key. Get one at": "Ваш API-ключ OpenAI. Получить по адресу", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Ваш отзыв записан. При желании вы можете предоставить дополнительные сведения здесь...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Ваш отзыв записан. При желании вы можете предоставить дополнительные сведения здесь...", + "Zookeeper Hosts": "Хосты Zookeeper", + "Zookeeper Port": "Порт Zookeeper", + "Zookeeper Settings (SolrCloud)": "Настройки Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Строка подключения Zookeeper для SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Номер порта Zookeeper (необязательно, по умолчанию 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ПРЕДУПРЕЖДЕНИЕ О БЕЗВОЗВРАТНОМ УДАЛЕНИИ \\u26A0\\uFE0F\n\nВы собираетесь БЕЗВОЗВРАТНО удалить ВСЕ объекты схемы \"{schema}\":\n\n\\u2022 Активных объектов: {active}\n\\u2022 Программно удалённых объектов: {deleted}\n\\u2022 Итого: {total}\n\nЭти объекты будут полностью удалены из базы данных и НЕ МОГУТ быть восстановлены.\n\nВы абсолютно уверены?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ПРЕДУПРЕЖДЕНИЕ О БЕЗВОЗВРАТНОМ УДАЛЕНИИ \\u26A0\\uFE0F\n\nВы собираетесь БЕЗВОЗВРАТНО удалить {count} программно удалённых объектов схемы \"{schema}\".\n\nЭти объекты будут полностью удалены из базы данных и НЕ МОГУТ быть восстановлены.\n\nВы абсолютно уверены?", + "and accessible to all users in selected groups (or all users if no groups selected).": "и доступно всем пользователям в выбранных группах (или всем пользователям, если группы не выбраны).", + "and add the files there.": "и добавьте файлы туда.", + "approved": "одобрено", + "are only accessible to invited users.": "доступны только приглашённым пользователям.", + "by": "от", + "chunk": "фрагмент", + "chunks": "фрагменты", + "chunks processed": "фрагментов обработано", + "completed": "завершено", + "configuration(s)": "конфигурация(ии)", + "conversationId is required": "conversationId обязателен", + "create": "создать", + "delete": "удалить", + "deleted objects section": "раздел удалённых объектов", + "e.g., lib/Settings/config.json": "например, lib/Settings/config.json", + "e.g., lib/Settings/register.json": "например, lib/Settings/register.json", + "e.g., lib/settings/config.json": "например, lib/settings/config.json", + "e.g., lib/settings/register.json": "например, lib/settings/register.json", + "execute": "выполнить", + "failed": "не удалось", + "file": "файл", + "files": "файлы", + "file{plural}": "файл(ов)", + "folder": "папка", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "в {register}", + "in_progress": "в процессе", + "inherited": "унаследовано", + "list": "список", + "log{plural}": "журнал(ов)", + "manage": "управлять", + "members": "участники", + "menu or contact someone with permission to create agents.": "меню или обратитесь к кому-то с разрешением на создание агентов.", + "message content is required": "содержимое сообщения обязательно", + "messages": "сообщения", + "missing": "отсутствует", + "myapp": "myapp", + "n8n URL and API key are required": "URL n8n и API-ключ обязательны", + "n8n configuration saved successfully": "Конфигурация n8n успешно сохранена", + "n8n connection not configured": "Подключение n8n не настроено", + "n8n connection successful": "Подключение n8n успешно", + "n8n connection test failed: {message}": "Тест подключения n8n не удался: {message}", + "n8n connection test successful": "Тест подключения n8n успешен", + "n8n initialization failed: {message}": "Инициализация n8n не удалась: {message}", + "n8n integration disabled": "Интеграция n8n отключена", + "n8n integration enabled": "Интеграция n8n включена", + "n8n project initialized successfully": "Проект n8n успешно инициализирован", + "n8n settings saved successfully": "Настройки n8n успешно сохранены", + "notify_push configuration guide": "Руководство по настройке notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push установлен, но OpenRegister ещё не подтвердил успешный push. Активируйте сохранение объекта или проверьте конфигурацию notify_push.", + "notify_push is installed but not yet active": "notify_push установлен, но ещё не активен", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "объекты", + "objects processed": "объектов обработано", + "object{plural}": "объект(ов)", + "or pick a range": "или выберите диапазон", + "org-...": "org-...", + "pending": "ожидает", + "provides a good balance between speed and accuracy.": "обеспечивает хороший баланс между скоростью и точностью.", + "public": "публичный", + "read": "чтение", + "register(s)": "реестр(ов)", + "register(s) selected": "реестр(ов) выбрано", + "register{plural}": "реестр(ов)", + "rejected": "отклонено", + "required": "обязательно", + "results": "результаты", + "schema(s)": "схема(ем)", + "schema(s) selected": "схема(ем) выбрано", + "schema{plural}": "схема(ем)", + "selected objects": "выбранные объекты", + "set": "установить", + "sk-...": "sk-...", + "this application": "это приложение", + "to": "в", + "type must be \"positive\" or \"negative\"": "тип должен быть «positive» или «negative»", + "update": "обновить", + "vectorized": "векторизовано", + "view": "представление", + "views selected": "представлений выбрано", + "waiting": "ожидание", + "{active} active, {deleted} deleted": "{active} активных, {deleted} удалённых", + "{count} audit trails cleared successfully": "{count} записей журнала аудита успешно очищено", + "{count} compliance issue(s) detected": "Обнаружено {count} проблема(ем) соответствия", + "{count} deletions": "{count} удалений", + "{count} documents found": "Найдено {count} документов", + "{count} email": "{count} письмо", + "{count} emails": "{count} писем", + "{count} entries": "{count} записей", + "{count} issues": "{count} проблем", + "{count} members": "{count} участников", + "{count} searches": "{count} поисков", + "{count} selected": "{count} выбрано", + "{count} widget(s)": "{count} виджет(ов)", + "{matched} object(s) matched. {erased} erased.": "{matched} объект(ов) найдено. {erased} удалено.", + "{minutes} minutes ago": "{minutes} минут назад", + "{percentage}% of objects": "{percentage}% объектов", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} в {register} / {schema}", + "{type} cache cleared successfully": "Кэш {type} успешно очищен", + "ℹ️ Current Configuration": "ℹ️ Текущая конфигурация", + "ℹ️ current configuration": "ℹ️ текущая конфигурация", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ ОПАСНОСТЬ: Все {total} объектов будут БЕЗВОЗВРАТНО УДАЛЕНЫ из базы данных. Это действие НЕОБРАТИМО и не может быть отменено!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕЖДЕНИЕ О БЕЗВОЗВРАТНОМ УДАЛЕНИИ ⚠️\n\nВы собираетесь БЕЗВОЗВРАТНО удалить ВСЕ объекты схемы \"{schema}\":\n\n• Активных объектов: {active}\n• Программно удалённых объектов: {deleted}\n• Итого: {total}\n\nЭти объекты будут полностью удалены из базы данных и НЕ МОГУТ быть восстановлены.\n\nВы абсолютно уверены?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕЖДЕНИЕ О БЕЗВОЗВРАТНОМ УДАЛЕНИИ ⚠️\n\nВы собираетесь БЕЗВОЗВРАТНО удалить {count} программно удалённых объектов схемы \"{schema}\".\n\nЭти объекты будут полностью удалены из базы данных и НЕ МОГУТ быть восстановлены.\n\nВы абсолютно уверены?", + "⚡ Batch Processing": "⚡ Пакетная обработка", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR может обрабатывать миллионы документов со временем ответа поиска менее секунды...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Полный контекст ✓ Меньше шансов пропустить детали ✗ Медленные ответы ✗ Может включать менее релевантную информацию", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Быстрые ответы ✓ Более сфокусированные ответы ✗ Может упустить релевантную информацию", + "✨ AI Features": "✨ Функции ИИ", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 В распределённом режиме SOLR использует ZooKeeper для координации кластера...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Фасетный поиск позволяет пользователям углубляться в результаты по категориям...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Знали ли вы? SOLR обеспечивает поиск для Netflix, Apple и многих других крупных сайтов!", + "💰 View Selection (Cost Optimization)": "💰 Выбор представлений (оптимизация затрат)", + "📄 Object Serialization": "📄 Сериализация объектов", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR предоставляет богатую аналитику и статистику производительности поиска...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Наборы конфигурации содержат схему и файлы конфигурации для вашего поискового индекса...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Репликация гарантирует доступность поискового индекса даже при отказе узлов...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR — мощная корпоративная поисковая платформа, построенная на Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Настройки векторизации", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Автомасштабирование может динамически добавлять или удалять узлы в зависимости от нагрузки...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Функции безопасности включают аутентификацию, авторизацию и шифрование SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} письмо", + "{count} письма", + "{count} писем" + ], + "Purge {count} object from database": [ + "Очистить {count} объект из базы данных", + "Очистить {count} объекта из базы данных", + "Очистить {count} объектов из базы данных" + ], + "Restore {count} object": [ + "Восстановить {count} объект", + "Восстановить {count} объекта", + "Восстановить {count} объектов" + ], + "Successfully restored {count} object": [ + "Успешно восстановлен {count} объект", + "Успешно восстановлено {count} объекта", + "Успешно восстановлено {count} объектов" + ], + "Delete {count} object": [ + "Удалить {count} объект", + "Удалить {count} объекта", + "Удалить {count} объектов" + ], + "Object successfully deleted": [ + "Объект успешно удалён", + "Объекта успешно удалены", + "Объектов успешно удалено" + ] + } +} diff --git a/l10n/sk.js b/l10n/sk.js new file mode 100644 index 0000000000..48ecd402fa --- /dev/null +++ b/l10n/sk.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(bez predmetu)", + "(no title)" : "(bez názvu)", + "(unnamed)" : "(bez názvu)", + "(untitled card)" : "(karta bez názvu)", + "Activities" : "Aktivity", + "Add Endpoint" : "Pridať koncový bod", + "Add Source" : "Pridať zdroj", + "Add card" : "Pridať kartu", + "Add contact" : "Pridať kontakt", + "All clear" : "Všetko v poriadku", + "Articles" : "Články", + "Code" : "Kód", + "Compliance" : "Súlad s predpismi", + "Contacts" : "Kontakty", + "Create Agent" : "Vytvoriť agenta", + "Create Application" : "Vytvoriť aplikáciu", + "Create event" : "Vytvoriť udalosť", + "Erasure complete" : "Vymazanie dokončené", + "Erasure preview" : "Náhľad vymazania", + "Expand details" : "Rozbaliť podrobnosti", + "Save changes" : "Uložiť zmeny", + "📄 Object Serialization" : "📄 Serializácia objektu", + "🔢 Vectorization Settings" : "🔢 Nastavenia vektorizácie", + "💰 View Selection (Cost Optimization)" : "💰 Výber zobrazení (optimalizácia nákladov)", + "AI Assistant" : "AI asistent", + "AI Chat" : "AI chat", + "API Key" : "API kľúč", + "API tokens saved successfully" : "API tokeny boli úspešne uložené", + "About ConfigSets" : "O ConfigSetoch", + "Action" : "Akcia", + "Action Distribution" : "Rozloženie akcií", + "Action:" : "Akcia:", + "Actions" : "Akcie", + "Active" : "Aktívne", + "Active Collections" : "Aktívne kolekcie", + "Active Filters" : "Aktívne filtre", + "Active Organisations" : "Aktívne organizácie", + "Active filters:" : "Aktívne filtre:", + "Active organisation set successfully" : "Aktívna organizácia bola úspešne nastavená", + "Activity Period" : "Obdobie aktivity", + "Add Groups" : "Pridať skupiny", + "Add Schema" : "Pridať schému", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Pridajte názvy schém, popisy a informácie o registri pre bohatší kontext vyhľadávania", + "Add to favorites" : "Pridať medzi obľúbené", + "Added" : "Pridané", + "Added to favorites" : "Pridané medzi obľúbené", + "Additional Information" : "Ďalšie informácie", + "Additional feedback saved. Thank you!" : "Ďalšia spätná väzba bola uložená. Ďakujeme!", + "Admin" : "Správca", + "Advanced" : "Pokročilé", + "Advanced Filters" : "Pokročilé filtre", + "Advanced Options" : "Pokročilé možnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Pokročilé filtre nie sú dostupné pri použití databázového zdroja. Prepnite na Auto alebo SOLR Index pre možnosti filtrovania.", + "After" : "Po", + "Agent deleted successfully" : "Agent bol úspešne odstránený", + "Agents" : "Agenti", + "All" : "Všetko", + "All Categories" : "Všetky kategórie", + "All Collections" : "Všetky kolekcie", + "All Confidence Levels" : "Všetky úrovne spoľahlivosti", + "All ConfigSets" : "Všetky ConfigSety", + "All Files" : "Všetky súbory", + "All Levels" : "Všetky úrovne", + "All Types" : "Všetky typy", + "All Webhooks" : "Všetky webhooky", + "All actions" : "Všetky akcie", + "All audit trails cleared successfully" : "Všetky audítne záznamy boli úspešne vymazané", + "All registers" : "Všetky registre", + "All schemas" : "Všetky schémy", + "All searches" : "Všetky vyhľadávania", + "All users" : "Všetci používatelia", + "An error occurred while clearing audit trails" : "Pri vymazávaní audítnych záznamov sa vyskytla chyba", + "An error occurred while deleting the audit trail" : "Pri odstraňovaní audítneho záznamu sa vyskytla chyba", + "An error occurred while deleting the view" : "Pri odstraňovaní zobrazenia sa vyskytla chyba", + "An error occurred while permanently deleting the objects" : "Pri trvalom odstraňovaní objektov sa vyskytla chyba", + "Analysis completed:" : "Analýza dokončená:", + "Analysis steps:" : "Kroky analýzy:", + "Analytics" : "Analytika", + "Analyze Objects" : "Analyzovať objekty", + "Analyze Properties" : "Analyzovať vlastnosti", + "Analyze existing properties for improvement opportunities" : "Analyzujte existujúce vlastnosti pre možnosti zlepšenia", + "Analyzing..." : "Analyzuje sa...", + "Any favorites and sharing settings for this view" : "Akékoľvek obľúbené položky a nastavenia zdieľania pre toto zobrazenie", + "Any user" : "Akýkoľvek používateľ", + "App store cache invalidated successfully" : "Vyrovnávacia pamäť obchodu s aplikáciami bola úspešne zneplatnená", + "Application deleted successfully" : "Aplikácia bola úspešne odstránená", + "Applications" : "Aplikácie", + "Apply Changes" : "Použiť zmeny", + "Archive" : "Archivovať", + "Archive conversation" : "Archivovať konverzáciu", + "Archived conversations are hidden from your active list" : "Archivované konverzácie sú skryté z vášho aktívneho zoznamu", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Naozaj chcete vyčistiť staré záznamy vyhľadávania? Týmto sa odstránia záznamy staršie ako 30 dní.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Naozaj chcete odstrániť vybrané audítne záznamy? Túto akciu nie je možné vrátiť späť.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Naozaj chcete odstrániť vybrané záznamy vyhľadávania? Túto akciu nie je možné vrátiť späť.", + "Are you sure you want to delete this ConfigSet?" : "Naozaj chcete odstrániť tento ConfigSet?", + "Are you sure you want to permanently delete" : "Naozaj chcete trvalo odstrániť", + "Ask a question..." : "Položte otázku...", + "Ask questions about your data using natural language" : "Pýtajte sa na svoje údaje pomocou prirodzeného jazyka", + "Attempt" : "Pokus", + "Audit Trail Changes" : "Zmeny audítneho záznamu", + "Audit Trail Details" : "Podrobnosti audítneho záznamu", + "Audit Trail Management" : "Správa audítnych záznamov", + "Audit Trail Statistics" : "Štatistiky audítnych záznamov", + "Audit Trails" : "Audítne záznamy", + "Audit trail data copied to clipboard" : "Údaje audítneho záznamu skopírované do schránky", + "Audit trail deleted successfully" : "Audítny záznam bol úspešne odstránený", + "Audit trail successfully deleted" : "Audítny záznam bol úspešne odstránený", + "Authentication" : "Overenie totožnosti", + "Auto-commit disabled" : "Automatické potvrdenie zakázané", + "Auto-commit enabled" : "Automatické potvrdenie povolené", + "Auto-create Default Organisation" : "Automaticky vytvoriť predvolenú organizáciu", + "Auto-retry failed vectorizations" : "Automaticky opakovať neúspešné vektorizácie", + "Automatically commit changes to SOLR index" : "Automaticky potvrdiť zmeny do SOLR indexu", + "Automatically create a default organisation if none exists when the app is initialized" : "Automaticky vytvoriť predvolenú organizáciu, ak žiadna neexistuje pri inicializácii aplikácie", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Automaticky generovať vektorové vloženia z textových úsekov pri nahraní a spracovaní súborov", + "Automatically generate vector embeddings when objects are created or updated" : "Automaticky generovať vektorové vloženia pri vytvorení alebo aktualizácii objektov", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Automaticky opakovať neúspešné pokusy o vektorizáciu (max. 3 opakovania)", + "Available Filters" : "Dostupné filtre", + "Available Workflows" : "Dostupné pracovné postupy", + "Avg Execution Time" : "Priemerný čas vykonania", + "Avg Members/Org" : "Priemer členov/org.", + "Avg Object Views/Session" : "Priemer zobrazení objektov/reláciu", + "Avg Response Time" : "Priemerný čas odozvy", + "Avg Results/Search" : "Priemer výsledkov/vyhľadávanie", + "Avg Searches/Session" : "Priemer vyhľadávaní/reláciu", + "Back" : "Späť", + "Back to Entities" : "Späť na entity", + "Back to Registers" : "Späť na registre", + "Back to Webhooks" : "Späť na webhooky", + "Back to applications" : "Späť na aplikácie", + "Back to entities" : "Späť na entity", + "Backend updated successfully. Please reload the application." : "Backend bol úspešne aktualizovaný. Znovu načítajte aplikáciu.", + "Base URL (Optional)" : "Základná URL (voliteľné)", + "Basic Information" : "Základné informácie", + "Batch Size" : "Veľkosť dávky", + "Batch extraction completed" : "Dávková extrakcia dokončená", + "Before" : "Pred", + "Before object vectorization can work:" : "Predtým, než môže vektorizácia objektov fungovať:", + "Behavior Issue" : "Problém správania", + "Bookmarks" : "Záložky", + "Blob storage has been retired. All objects now use magic tables." : "Úložisko blob bolo vyradené. Všetky objekty teraz používajú magické tabuľky.", + "Branch" : "Vetva", + "Bulk delete operation completed successfully" : "Hromadná operácia odstránenia bola úspešne dokončená", + "Bulk save operation completed successfully" : "Hromadná operácia uloženia bola úspešne dokončená", + "Business Data" : "Obchodné údaje", + "Cache cleared successfully" : "Vyrovnávacia pamäť bola úspešne vymazaná", + "Calculate Sizes" : "Vypočítať veľkosti", + "Cancel" : "Zrušiť", + "Cannot delete: objects are still attached" : "Nie je možné odstrániť: objekty sú stále pripojené", + "Category" : "Kategória", + "Change Type" : "Typ zmeny", + "Changes" : "Zmeny", + "Changes copied to clipboard" : "Zmeny skopírované do schránky", + "Chat Model" : "Model chatu", + "Chat Provider" : "Poskytovateľ chatu", + "Chat Provider (RAG)" : "Poskytovateľ chatu (RAG)", + "Chat Settings" : "Nastavenia chatu", + "Chat provider connection successful!" : "Pripojenie k poskytovateľovi chatu úspešné!", + "Chat settings" : "Nastavenia chatu", + "Choose a register" : "Vyberte register", + "Choose a schema" : "Vyberte schému", + "Choose how vector similarity calculations are performed for semantic search" : "Vyberte, ako sa vykonávajú výpočty vektorovej podobnosti pre sémantické vyhľadávanie", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Vyberte, ktoré typy súborov zahrnúť do procesu vektorizácie. Spracujú sa len súbory s extrahovaným textom a úsekmi.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Vyberte, ktoré zobrazenia zahrnúť do procesu vektorizácie. Ponechajte prázdne pre spracovanie všetkých zobrazení podľa vašej konfigurácie.", + "Chunk" : "Úsek", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Odstránenie úseku ešte nie je implementované. Použite koncové body založené na úsekoch.", + "Chunks" : "Úseky", + "Chunks to Vectorize:" : "Úseky na vektorizáciu:", + "Cleanup Old Trails" : "Vyčistiť staré záznamy", + "Cleanup completed" : "Čistenie dokončené", + "Clear All Embeddings" : "Vymazať všetky vloženia", + "Clear Entries" : "Vymazať záznamy", + "Clear Filtered Audit Trails" : "Vymazať filtrované audítne záznamy", + "Clear Filters" : "Vymazať filtre", + "Clear Index" : "Vymazať index", + "Clear Selection" : "Vymazať výber", + "Clear all" : "Vymazať všetko", + "Clear all filters" : "Vymazať všetky filtre", + "Clear date range" : "Vymazať rozsah dátumov", + "Clearing..." : "Vymazáva sa...", + "Close" : "Zatvoriť", + "Collection Management" : "Správa kolekcií", + "Collection Name" : "Názov kolekcie", + "Collection assignments updated successfully" : "Priradenia kolekcií boli úspešne aktualizované", + "Collection cleared successfully" : "Kolekcia bola úspešne vymazaná", + "Collection copied successfully" : "Kolekcia bola úspešne skopírovaná", + "Collection created successfully" : "Kolekcia bola úspešne vytvorená", + "Collection deleted successfully" : "Kolekcia bola úspešne odstránená", + "Collection used to store and index file metadata and content" : "Kolekcia použitá na uloženie a indexovanie metaúdajov a obsahu súborov", + "Collection used to store and index object data" : "Kolekcia použitá na uloženie a indexovanie údajov objektov", + "Collections:" : "Kolekcie:", + "Commit Message" : "Správa potvrdenia", + "Commit Within (ms)" : "Potvrdiť do (ms)", + "Commit message" : "Správa potvrdenia", + "Compare current schema with real object data" : "Porovnať aktuálnu schému so skutočnými údajmi objektu", + "Completed" : "Dokončené", + "Complex" : "Komplexné", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Komplexné dopyty: Pokročilé vyhľadávania s viacerými filtrami, operátormi a komplexnými kombináciami parametrov", + "Confidence Level" : "Úroveň spoľahlivosti", + "Confidence Score" : "Skóre spoľahlivosti", + "Config must be provided as an object" : "Konfigurácia musí byť poskytnutá ako objekt", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Správa ConfigSetov", + "ConfigSet Name" : "Názov ConfigSetu", + "ConfigSet created successfully" : "ConfigSet bol úspešne vytvorený", + "ConfigSet deleted successfully" : "ConfigSet bol úspešne odstránený", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSety definujú schému a konfiguráciu vašich SOLR kolekcií. Obsahujú definície polí, analyzátory a ďalšie nastavenia vyhľadávania.", + "Configuration" : "Konfigurácia", + "Configuration Data" : "Konfiguračné údaje", + "Configuration saved" : "Konfigurácia uložená", + "Configurations" : "Konfigurácie", + "Configure Facets" : "Konfigurovať fasety", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurujte poskytovateľov veľkých jazykových modelov (LLM) pre funkcie poháňané AI vrátane sémantického vyhľadávania, vložení a chatu.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurujte základné nastavenia pripojenia pre váš SOLR server vrátane overenia totožnosti a sieťových možností. Na správu jadier a kolekcií použite samostatné dialógové okná správy ConfigSetov a kolekcií.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurujte, ako sa databázové objekty konvertujú na vektorové vloženia pre sémantické vyhľadávanie. Objekty sa vektorizujú priamo bez potreby extrakcie textu.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurujte, ako sa objekty konvertujú na text pred vektorizáciou. Tieto nastavenia ovplyvňujú kvalitu vyhľadávania a kontext.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurujte parametre pre vektorizáciu súborov. Tento proces vygeneruje vektorové vloženia pre všetky extrahované úseky súborov.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurujte parametre pre vektorizáciu objektov. Tento proces vygeneruje vektorové vloženia pre všetky objekty zodpovedajúce vašim filtrom zobrazenia.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurujte, ktoré typy údajov vyhľadávať a koľko zdrojov získať", + "Connection Failed" : "Pripojenie zlyhalo", + "Connection Settings" : "Nastavenia pripojenia", + "Connection Successful!" : "Pripojenie úspešné!", + "Connection failed" : "Pripojenie zlyhalo", + "Connection protocol" : "Protokol pripojenia", + "Connection successful" : "Pripojenie úspešné", + "Connection timeout in seconds" : "Časový limit pripojenia v sekundách", + "Constant delay between retries (5 minutes)" : "Konštantné oneskorenie medzi opakovaniami (5 minút)", + "Constraint Issue" : "Problém obmedzenia", + "Content Filters" : "Filtre obsahu", + "Continue to Properties" : "Pokračovať na vlastnosti", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Ovládajte, ktoré zobrazenia objektov sa majú vektorizovať na zníženie nákladov na API. Vektorizujte len zobrazenia, ktoré profitujú zo sémantického vyhľadávania.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Ovládajte, ktoré zobrazenia a nástroje môže AI v tejto konverzácii používať. Predvolene sú povolené všetky schopnosti agenta.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Ovláda náhodnosť (0 = deterministické, 2 = veľmi kreatívne)", + "Conversation ID is required" : "ID konverzácie je povinné", + "Conversation archived" : "Konverzácia archivovaná", + "Conversation archived successfully" : "Konverzácia bola úspešne archivovaná", + "Conversation cleared successfully" : "Konverzácia bola úspešne vymazaná", + "Conversation deleted" : "Konverzácia odstránená", + "Conversation permanently deleted" : "Konverzácia trvalo odstránená", + "Conversation renamed" : "Konverzácia premenovaná", + "Conversation restored" : "Konverzácia obnovená", + "Conversation title" : "Názov konverzácie", + "Copied!" : "Skopírované!", + "Copy" : "Kopírovať", + "Copy Changes" : "Kopírovať zmeny", + "Copy Collection" : "Kopírovať kolekciu", + "Copy Data" : "Kopírovať údaje", + "Copy Full Data" : "Kopírovať všetky údaje", + "Copying..." : "Kopíruje sa...", + "Count" : "Počet", + "Counting objects..." : "Počítajú sa objekty...", + "Costs" : "Náklady", + "Create" : "Vytvoriť", + "Create Collection" : "Vytvoriť kolekciu", + "Create ConfigSet" : "Vytvoriť ConfigSet", + "Create New Collection" : "Vytvoriť novú kolekciu", + "Create New ConfigSet" : "Vytvoriť nový ConfigSet", + "Create Operations" : "Operácie vytvorenia", + "Create Webhook" : "Vytvoriť webhook", + "Create a copy of collection:" : "Vytvoriť kópiu kolekcie:", + "Create a new ConfigSet based on the _default template" : "Vytvoriť nový ConfigSet založený na šablóne _default", + "Create a new SOLR collection from an existing ConfigSet" : "Vytvoriť novú SOLR kolekciu z existujúceho ConfigSetu", + "Create your first AI agent to get started." : "Vytvorte svojho prvého AI agenta a začnite.", + "Created" : "Vytvorené", + "Created:" : "Vytvorené:", + "Creating..." : "Vytvára sa...", + "Current Filters" : "Aktuálne filtre", + "Current Type:" : "Aktuálny typ:", + "Custom API endpoint if using a different region" : "Vlastný API koncový bod pri použití inej oblasti", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Vlastné HTTP hlavičky (jedna na riadok, formát: Header-Name: value)", + "Daily" : "Denne", + "Dashboard" : "Nástenka", + "Data Source" : "Zdroj údajov", + "Data sources" : "Zdroje údajov", + "Data type does not match observed values" : "Typ údajov nezodpovedá pozorovaným hodnotám", + "Database URL" : "URL databázy", + "Database information refreshed" : "Informácie o databáze obnovené", + "Date Range" : "Rozsah dátumov", + "Default" : "Predvolené", + "Default Organisation" : "Predvolená organizácia", + "Default: 5" : "Predvolené: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Oneskorenia sa s každým pokusom zdvojnásobujú (2, 4, 8 minút...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Oneskorenia sa zvyšujú lineárne (5, 10, 15 minút...)", + "Delete" : "Odstrániť", + "Delete Application" : "Odstrániť aplikáciu", + "Delete Audit Trail" : "Odstrániť audítny záznam", + "Delete Collection" : "Odstrániť kolekciu", + "Delete ConfigSet" : "Odstrániť ConfigSet", + "Delete Objects" : "Odstrániť objekty", + "Delete Operations" : "Operácie odstránenia", + "Delete View" : "Odstrániť zobrazenie", + "Delete all objects in this schema" : "Odstrániť všetky objekty v tejto schéme", + "Delete permanently" : "Odstrániť trvalo", + "Delete this conversation?" : "Odstrániť túto konverzáciu?", + "Delete view" : "Odstrániť zobrazenie", + "Deleted" : "Odstránené", + "Deleted By" : "Odstránil", + "Deleted Date" : "Dátum odstránenia", + "Deleted Items Management" : "Správa odstránených položiek", + "Deleted This Week" : "Odstránené tento týždeň", + "Deleted Today" : "Odstránené dnes", + "Deleted:" : "Odstránené:", + "Deleting..." : "Odstraňuje sa...", + "Deletion Date Range" : "Rozsah dátumov odstránenia", + "Deletion Statistics" : "Štatistiky odstránení", + "Deprecated" : "Zastarané", + "Description" : "Popis", + "Description:" : "Popis:", + "Detect data types and patterns" : "Zistiť typy údajov a vzory", + "Detected At" : "Zistené o", + "Detected Format:" : "Zistený formát:", + "Detected Issues:" : "Zistené problémy:", + "Disable" : "Zakázať", + "Disabled" : "Zakázané", + "Discover Files" : "Objaviť súbory", + "Discovered Properties" : "Objavené vlastnosti", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Chcete trvalo odstrániť všetky filtrované záznamy audítneho záznamu? Túto akciu nie je možné vrátiť späť.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Chcete trvalo odstrániť tento záznam audítneho záznamu? Túto akciu nie je možné vrátiť späť.", + "Documents" : "Dokumenty", + "Download API Spec" : "Stiahnuť API špecifikáciu", + "Edit" : "Upraviť", + "Edit Register" : "Upraviť register", + "Edit Schema" : "Upraviť schému", + "Edit View" : "Upraviť zobrazenie", + "Edit Webhook" : "Upraviť webhook", + "Edit view" : "Upraviť zobrazenie", + "Edit view details" : "Upraviť podrobnosti zobrazenia", + "Effectiveness" : "Účinnosť", + "Email" : "E-mail", + "Emails" : "E-maily", + "Embedding Model" : "Model vloženia", + "Embedding Provider" : "Poskytovateľ vloženia", + "Embedding generated successfully" : "Vloženie bolo úspešne vygenerované", + "Embedding provider connection successful!" : "Pripojenie k poskytovateľovi vloženia úspešné!", + "Enable" : "Povoliť", + "Enable auto-creation" : "Povoliť automatické vytváranie", + "Enable automatic file vectorization" : "Povoliť automatickú vektorizáciu súborov", + "Enable automatic object vectorization" : "Povoliť automatickú vektorizáciu objektov", + "Enable automatic synchronization" : "Povoliť automatickú synchronizáciu", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Povoliť podrobné protokolovanie pre SOLR operácie (odporúčané pri ladení)", + "Enable faceting" : "Povoliť fasetovanie", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Povoliť alebo zakázať funkcie LLM. Konfigurujte poskytovateľov a modely pomocou tlačidla Konfigurácia LLM vyššie.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Povoliť alebo zakázať integráciu SOLR vyhľadávania. Konfigurujte nastavenia pripojenia pomocou tlačidla Nastavenia pripojenia vyššie.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Povoliť alebo zakázať integráciu pracovných postupov n8n. Konfigurujte nastavenia pripojenia nižšie.", + "Enable or disable this webhook" : "Povoliť alebo zakázať tento webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Povoliť vektorizáciu pre všetky existujúce a budúce zobrazenia (môže zvýšiť náklady)", + "Enabled" : "Povolené", + "Endpoints" : "Koncové body", + "Enter ConfigSet name" : "Zadajte názov ConfigSetu", + "Enter collection name" : "Zadajte názov kolekcie", + "Enter description (optional)..." : "Zadajte popis (voliteľné)...", + "Enter new collection name" : "Zadajte nový názov kolekcie", + "Enter object ID" : "Zadajte ID objektu", + "Enter search term" : "Zadajte hľadaný výraz", + "Enter view name..." : "Zadajte názov zobrazenia...", + "Enter webhook name" : "Zadajte názov webhooku", + "Entities" : "Entity", + "Entity Information" : "Informácie o entite", + "Entity deleted successfully" : "Entita bola úspešne odstránená", + "Entity not found" : "Entita sa nenašla", + "Entries to be deleted:" : "Záznamy na odstránenie:", + "Enum Issue" : "Problém enum", + "Enum constraint is missing" : "Chýba obmedzenie enum", + "Error" : "Chyba", + "Error Details" : "Podrobnosti chyby", + "Error Information" : "Informácie o chybe", + "Error Message" : "Chybové hlásenie", + "Error loading application" : "Chyba pri načítavaní aplikácie", + "Error loading audit trails" : "Chyba pri načítavaní audítnych záznamov", + "Error loading entity" : "Chyba pri načítavaní entity", + "Error loading search trails" : "Chyba pri načítavaní záznamov vyhľadávania", + "Estimated Batches:" : "Odhadované dávky:", + "Estimated Cost:" : "Odhadované náklady:", + "Estimated Duration:" : "Odhadované trvanie:", + "Event" : "Udalosť", + "Event Property for Payload" : "Vlastnosť udalosti pre payload", + "Events" : "Udalosti", + "Example Value" : "Príkladová hodnota", + "Exclusive Maximum" : "Exkluzívne maximum", + "Exclusive Minimum" : "Exkluzívne minimum", + "Execution Mode" : "Režim vykonania", + "Execution Time" : "Čas vykonania", + "Execution Time Range" : "Rozsah času vykonania", + "Existing Improvements" : "Existujúce zlepšenia", + "Exponential" : "Exponenciálne", + "Export" : "Exportovať", + "Export completed successfully" : "Export bol úspešne dokončený", + "Export, view, or delete audit trails" : "Exportujte, zobrazte alebo odstráňte audítne záznamy", + "Extend Schema" : "Rozšíriť schému", + "Extended" : "Rozšírené", + "Extensions" : "Rozšírenia", + "Extract Pending Files" : "Extrahovať čakajúce súbory", + "Extract properties from each object" : "Extrahovať vlastnosti z každého objektu", + "Extracted At" : "Extrahované o", + "Extraction Status" : "Stav extrakcie", + "Facet configuration updated successfully" : "Konfigurácia fasiet bola úspešne aktualizovaná", + "Facets discovered and configured successfully" : "Fasety boli úspešne objavené a nakonfigurované", + "Facets discovered successfully" : "Fasety boli úspešne objavené", + "Failed" : "Zlyhalo", + "Failed to analyze schema properties" : "Nepodarilo sa analyzovať vlastnosti schémy", + "Failed to archive conversation" : "Nepodarilo sa archivovať konverzáciu", + "Failed to calculate sizes" : "Nepodarilo sa vypočítať veľkosti", + "Failed to clear collection" : "Nepodarilo sa vymazať kolekciu", + "Failed to copy changes" : "Nepodarilo sa skopírovať zmeny", + "Failed to copy data" : "Nepodarilo sa skopírovať údaje", + "Failed to copy data to clipboard" : "Nepodarilo sa skopírovať údaje do schránky", + "Failed to create conversation" : "Nepodarilo sa vytvoriť konverzáciu", + "Failed to create or find project" : "Nepodarilo sa vytvoriť alebo nájsť projekt", + "Failed to delete collection" : "Nepodarilo sa odstrániť kolekciu", + "Failed to delete conversation" : "Nepodarilo sa odstrániť konverzáciu", + "Failed to delete webhook" : "Nepodarilo sa odstrániť webhook", + "Failed to download API specification" : "Nepodarilo sa stiahnuť API špecifikáciu", + "Failed to get SOLR field configuration" : "Nepodarilo sa získať konfiguráciu polí SOLR", + "Failed to get entity categories" : "Nepodarilo sa získať kategórie entít", + "Failed to get entity statistics" : "Nepodarilo sa získať štatistiky entít", + "Failed to get entity types" : "Nepodarilo sa získať typy entít", + "Failed to load LLM configuration" : "Nepodarilo sa načítať konfiguráciu LLM", + "Failed to load Nextcloud groups" : "Nepodarilo sa načítať skupiny Nextcloud", + "Failed to load conversation" : "Nepodarilo sa načítať konverzáciu", + "Failed to load entities" : "Nepodarilo sa načítať entity", + "Failed to load entity" : "Nepodarilo sa načítať entitu", + "Failed to load files" : "Nepodarilo sa načítať súbory", + "Failed to load organisations" : "Nepodarilo sa načítať organizácie", + "Failed to load templates" : "Nepodarilo sa načítať šablóny", + "Failed to load webhooks" : "Nepodarilo sa načítať webhooky", + "Failed to load workflows" : "Nepodarilo sa načítať pracovné postupy", + "Failed to refresh database information" : "Nepodarilo sa obnoviť informácie o databáze", + "Failed to reindex collection" : "Nepodarilo sa preindexovať kolekciu", + "Failed to rename conversation" : "Nepodarilo sa premenovať konverzáciu", + "Failed to restore conversation" : "Nepodarilo sa obnoviť konverzáciu", + "Failed to retry extraction" : "Nepodarilo sa zopakovať extrakciu", + "Failed to retry webhook" : "Nepodarilo sa zopakovať webhook", + "Failed to save GitHub token" : "Nepodarilo sa uložiť GitHub token", + "Failed to save GitLab URL" : "Nepodarilo sa uložiť GitLab URL", + "Failed to save GitLab token" : "Nepodarilo sa uložiť GitLab token", + "Failed to save additional feedback" : "Nepodarilo sa uložiť ďalšiu spätnú väzbu", + "Failed to save n8n configuration" : "Nepodarilo sa uložiť konfiguráciu n8n", + "Failed to save roles" : "Nepodarilo sa uložiť roly", + "Failed to save settings" : "Nepodarilo sa uložiť nastavenia", + "Failed to save webhook" : "Nepodarilo sa uložiť webhook", + "Failed to send feedback" : "Nepodarilo sa odoslať spätnú väzbu", + "Failed to test webhook" : "Nepodarilo sa otestovať webhook", + "Failed to update favorite status" : "Nepodarilo sa aktualizovať stav obľúbenosti", + "Failed to update schema properties" : "Nepodarilo sa aktualizovať vlastnosti schémy", + "Failed to update view" : "Nepodarilo sa aktualizovať zobrazenie", + "Failed to update webhook" : "Nepodarilo sa aktualizovať webhook", + "Feedback recorded" : "Spätná väzba zaznamenaná", + "Fewer sources (1-3):" : "Menej zdrojov (1 – 3):", + "Field" : "Pole", + "File" : "Súbor", + "File Chunk Prediction" : "Predikcia úsekov súboru", + "File Collection" : "Kolekcia súborov", + "File Management" : "Správa súborov", + "File Name" : "Názov súboru", + "File Path" : "Cesta k súboru", + "File Type Selection" : "Výber typu súboru", + "File Vectorization" : "Vektorizácia súborov", + "File Warmup" : "Zahriatie súborov", + "File actions menu" : "Ponuka akcií súboru", + "File anonymized successfully" : "Súbor bol úspešne anonymizovaný", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vektorizácia úsekov súborov ešte nie je implementovaná. Úseky sú pripravené a uložené, ale služba vektorizácie sa vyvíja.", + "File collection not configured" : "Kolekcia súborov nie je nakonfigurovaná", + "File discovery completed" : "Objavovanie súborov dokončené", + "File extraction completed" : "Extrakcia súboru dokončená", + "File extraction queued" : "Extrakcia súboru zaradená do frontu", + "File indexed successfully" : "Súbor bol úspešne indexovaný", + "File is already anonymized" : "Súbor je už anonymizovaný", + "File not found" : "Súbor sa nenašiel", + "File settings updated successfully" : "Nastavenia súboru boli úspešne aktualizované", + "File sources" : "Zdroje súborov", + "File vectorization configuration saved successfully" : "Konfigurácia vektorizácie súborov bola úspešne uložená", + "File vectorization started. Check the statistics section for progress." : "Vektorizácia súborov spustená. Priebeh skontrolujte v sekcii štatistík.", + "File warmup completed" : "Zahriatie súborov dokončené", + "Files" : "Súbory", + "Files with Completed Extraction:" : "Súbory s dokončenou extrakciou:", + "Files → fileCollection, Objects → objectCollection" : "Súbory → fileCollection, Objekty → objectCollection", + "Filter Audit Trails" : "Filtrovať audítne záznamy", + "Filter Deleted Items" : "Filtrovať odstránené položky", + "Filter Objects" : "Filtrovať objekty", + "Filter Properties" : "Filtrovať vlastnosti", + "Filter Search Trails" : "Filtrovať záznamy vyhľadávania", + "Filter Statistics" : "Filtrovať štatistiky", + "Filter and analyze search trail entries" : "Filtrujte a analyzujte záznamy vyhľadávania", + "Filter and manage audit trail entries" : "Filtrujte a spravujte záznamy audítneho záznamu", + "Filter and manage soft deleted items" : "Filtrujte a spravujte mäkko odstránené položky", + "Filter by object ID" : "Filtrovať podľa ID objektu", + "Filter by search term" : "Filtrovať podľa hľadaného výrazu", + "Filter by webhook" : "Filtrovať podľa webhooku", + "Filter data loaded automatically. Use the filters below to refine your search." : "Údaje filtra boli načítané automaticky. Na spresnenie vyhľadávania použite filtre nižšie.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrujte spúšťače webhooku podľa vlastností payloadu (jedna na riadok, formát: key: value)", + "Filtered" : "Filtrované", + "Filters" : "Filtre", + "Fireworks AI Chat Settings" : "Nastavenia chatu Fireworks AI", + "Fireworks AI Embedding Configuration" : "Konfigurácia vloženia Fireworks AI", + "First" : "Prvé", + "Fixed" : "Opravené", + "For chat and retrieval-augmented generation" : "Pre chat a generovanie rozšírené o získavanie", + "For vector embeddings and semantic search" : "Pre vektorové vloženia a sémantické vyhľadávanie", + "Format" : "Formát", + "Format Issue" : "Problém formátu", + "Format constraint is missing" : "Chýba obmedzenie formátu", + "From Date" : "Od dátumu", + "From date" : "Od dátumu", + "Full data copied to clipboard" : "Všetky údaje skopírované do schránky", + "General Issue" : "Všeobecný problém", + "Generate recommendations and confidence scores" : "Generovať odporúčania a skóre spoľahlivosti", + "Generate vectors immediately when new objects are created" : "Generovať vektory okamžite pri vytvorení nových objektov", + "GitHub token is valid" : "GitHub token je platný", + "GitHub token removed successfully" : "GitHub token bol úspešne odstránený", + "GitHub token saved successfully" : "GitHub token bol úspešne uložený", + "GitLab URL saved successfully" : "GitLab URL bola úspešne uložená", + "GitLab token is valid" : "GitLab token je platný", + "GitLab token saved successfully" : "GitLab token bol úspešne uložený", + "HTTP Method" : "HTTP metóda", + "HTTP method used to send webhook requests" : "HTTP metóda použitá na odosielanie požiadaviek webhooku", + "Headers" : "Hlavičky", + "Health" : "Stav", + "Heartbeat successful - connection kept alive" : "Heartbeat úspešný – pripojenie udržané pri živote", + "Helpful" : "Užitočné", + "Hide Filters" : "Skryť filtre", + "Hide in forms" : "Skryť vo formulároch", + "Hide in list view" : "Skryť v zozname", + "High" : "Vysoká", + "High Confidence" : "Vysoká spoľahlivosť", + "Host" : "Hostiteľ", + "Hourly" : "Hodinovo", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Ako hlboko prechádzať vnorené vlastnosti objektu (1 – 20). Vyššie hodnoty zachytávajú viac podrobností, ale zväčšujú veľkosť vektora.", + "How often to check for updates (1-168 hours)" : "Ako často kontrolovať aktualizácie (1 – 168 hodín)", + "How to handle retries for failed webhook deliveries" : "Ako spracovať opakovania pre neúspešné doručenia webhooku", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Limity rýchlosti IP boli úspešne vymazané", + "Identify properties not in the schema" : "Identifikovať vlastnosti, ktoré nie sú v schéme", + "Immutable" : "Nemenné", + "Import" : "Importovať", + "Import successful" : "Import úspešný", + "Improved Property" : "Zlepšená vlastnosť", + "In use" : "Používa sa", + "Inactive" : "Neaktívne", + "Include IDs and names of related objects for better contextual search" : "Zahrnúť ID a názvy súvisiacich objektov pre lepšie kontextové vyhľadávanie", + "Include related object references" : "Zahrnúť odkazy na súvisiace objekty", + "Include schema and register metadata" : "Zahrnúť metaúdaje schémy a registra", + "Initialization failed" : "Inicializácia zlyhala", + "Initialize Project" : "Inicializovať projekt", + "Inspect Fields" : "Skontrolovať polia", + "Inspect Index" : "Skontrolovať index", + "Invalid" : "Neplatné", + "Invalid batch size. Must be between 1 and 5000" : "Neplatná veľkosť dávky. Musí byť medzi 1 a 5000", + "Invalid field name provided" : "Poskytnutý neplatný názov poľa", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Neplatný maxObjects. Musí byť 0 (všetko) alebo kladné číslo", + "Invalid:" : "Neplatné:", + "Issue" : "Problém", + "Items per page:" : "Položiek na stránku:", + "Just now" : "Práve teraz", + "LLM Configuration" : "Konfigurácia LLM", + "LLM actions menu" : "Ponuka akcií LLM", + "LLM configuration saved successfully" : "Konfigurácia LLM bola úspešne uložená", + "LLM features disabled" : "Funkcie LLM zakázané", + "LLM features enabled" : "Funkcie LLM povolené", + "LLM must be enabled with an embedding provider configured" : "LLM musí byť povolené s nakonfigurovaným poskytovateľom vloženia", + "LLM settings updated successfully" : "Nastavenia LLM boli úspešne aktualizované", + "Last" : "Posledné", + "Last Triggered" : "Naposledy spustené", + "Length Range:" : "Rozsah dĺžky:", + "Linear" : "Lineárne", + "Load Advanced Filters" : "Načítať pokročilé filtre", + "Load advanced filters with live data from your search index" : "Načítať pokročilé filtre so živými údajmi z vášho indexu vyhľadávania", + "Load view" : "Načítať zobrazenie", + "Loading ConfigSets..." : "Načítavajú sa ConfigSety...", + "Loading advanced filters..." : "Načítavajú sa pokročilé filtre...", + "Loading agents..." : "Načítavajú sa agenti...", + "Loading application details..." : "Načítavajú sa podrobnosti aplikácie...", + "Loading applications..." : "Načítavajú sa aplikácie...", + "Loading archived conversations..." : "Načítavajú sa archivované konverzácie...", + "Loading audit trails..." : "Načítavajú sa audítne záznamy...", + "Loading collections..." : "Načítavajú sa kolekcie...", + "Loading configurations..." : "Načítavajú sa konfigurácie...", + "Loading conversation..." : "Načítava sa konverzácia...", + "Loading conversations..." : "Načítavajú sa konverzácie...", + "Loading deleted items..." : "Načítavajú sa odstránené položky...", + "Loading events..." : "Načítavajú sa udalosti...", + "Loading filter data..." : "Načítavajú sa údaje filtra...", + "Loading groups..." : "Načítavajú sa skupiny...", + "Loading log details..." : "Načítavajú sa podrobnosti protokolu...", + "Loading registers..." : "Načítavajú sa registre...", + "Loading schemas..." : "Načítavajú sa schémy...", + "Loading search trails..." : "Načítavajú sa záznamy vyhľadávania...", + "Loading sources..." : "Načítavajú sa zdroje...", + "Loading statistics..." : "Načítavajú sa štatistiky...", + "Loading users..." : "Načítavajú sa používatelia...", + "Loading views..." : "Načítavajú sa zobrazenia...", + "Loading..." : "Načítava sa...", + "Local" : "Lokálne", + "Local Version" : "Lokálna verzia", + "Locked" : "Uzamknuté", + "Locked:" : "Uzamknuté:", + "Login successful" : "Prihlásenie úspešné", + "Logs" : "Protokoly", + "Low" : "Nízka", + "Low Confidence" : "Nízka spoľahlivosť", + "Lucene Version:" : "Verzia Lucene:", + "Magic table synchronized successfully" : "Magická tabuľka bola úspešne synchronizovaná", + "Manage Organisation Roles" : "Spravovať roly organizácie", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Spravujte SOLR kolekcie (úložiská údajov) a priraďte ich pre objekty a súbory.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Spravujte SOLR ConfigSety (konfiguračné šablóny) pre vaše kolekcie.", + "Manage and configure agents for automated tasks" : "Spravujte a konfigurujte agentov pre automatizované úlohy", + "Manage and monitor file text extraction status" : "Spravujte a monitorujte stav extrakcie textu zo súborov", + "Manage and restore soft deleted items from your registers" : "Spravujte a obnovujte mäkko odstránené položky z vašich registrov", + "Manage and view detected entities from files and objects" : "Spravujte a zobrazte zistené entity zo súborov a objektov", + "Manage document templates and themes" : "Spravujte šablóny dokumentov a témy", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Spravujte pracovné postupy n8n pre automatizáciu OpenRegister. Pracovné postupy sa uložia v nakonfigurovanom projekte.", + "Manage webhooks for event-driven integrations" : "Spravujte webhooky pre integrácie riadené udalosťami", + "Manage your applications and modules" : "Spravujte svoje aplikácie a moduly", + "Manage your data registers and their configurations" : "Spravujte svoje dátové registre a ich konfigurácie", + "Manage your data schemas and their properties" : "Spravujte svoje dátové schémy a ich vlastnosti", + "Manage your data sources and their configurations" : "Spravujte svoje zdroje údajov a ich konfigurácie", + "Manage your saved search configurations" : "Spravujte svoje uložené konfigurácie vyhľadávania", + "Manage your system configurations and settings" : "Spravujte svoje systémové konfigurácie a nastavenia", + "Managed" : "Spravované", + "Max Files (0 = all)" : "Max. súborov (0 = všetky)", + "Max Length" : "Max. dĺžka", + "Max Length:" : "Max. dĺžka:", + "Max Objects (0 = all)" : "Max. objektov (0 = všetky)", + "Max Retries" : "Max. opakovaní", + "Max Shards/Node" : "Max. shardov/uzol", + "Max execution time (ms)" : "Max. čas vykonania (ms)", + "Max ms" : "Max. ms", + "Max result count" : "Max. počet výsledkov", + "Max results" : "Max. výsledkov", + "Maximum" : "Maximum", + "Maximum Nesting Depth" : "Maximálna hĺbka vnorenia", + "Maximum length constraint is missing" : "Chýba obmedzenie maximálnej dĺžky", + "Maximum length is too restrictive" : "Maximálna dĺžka je príliš obmedzujúca", + "Maximum number of files to process. Set to 0 to process all files." : "Maximálny počet súborov na spracovanie. Nastavte na 0 na spracovanie všetkých súborov.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maximálny počet objektov na spracovanie. Nastavte na 0 na spracovanie všetkých objektov.", + "Maximum number of retry attempts for failed deliveries" : "Maximálny počet pokusov o opakovanie pre neúspešné doručenia", + "Maximum time to wait before committing changes" : "Maximálny čas čakania pred potvrdením zmien", + "Maximum tokens to generate" : "Maximálny počet tokenov na generovanie", + "Maximum value constraint is missing" : "Chýba obmedzenie maximálnej hodnoty", + "Maximum value is too restrictive" : "Maximálna hodnota je príliš obmedzujúca", + "Medium" : "Stredná", + "Medium Confidence" : "Stredná spoľahlivosť", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Stredné dopyty: Vyhľadávania s určitým filtrovaním alebo viacerými parametrami (napr. rozsahy dátumov, konkrétne registre/schémy)", + "Members of selected groups can access this view" : "Členovia vybraných skupín majú prístup k tomuto zobrazeniu", + "Members:" : "Členovia:", + "Memory prediction calculated successfully" : "Predikcia pamäte bola úspešne vypočítaná", + "Message content is required" : "Obsah správy je povinný", + "Message does not belong to this conversation" : "Správa nepatrí do tejto konverzácie", + "Metadata Filters" : "Filtre metaúdajov", + "Method" : "Metóda", + "Min Length" : "Min. dĺžka", + "Min execution time (ms)" : "Min. čas vykonania (ms)", + "Min ms" : "Min. ms", + "Min result count" : "Min. počet výsledkov", + "Min results" : "Min. výsledkov", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Chýba obmedzenie minimálnej hodnoty", + "Minimum value is too restrictive" : "Minimálna hodnota je príliš obmedzujúca", + "Missing object fields created successfully" : "Chýbajúce polia objektu boli úspešne vytvorené", + "Mode:" : "Režim:", + "Model" : "Model", + "Model Name" : "Názov modelu", + "Modified" : "Upravené", + "Monthly" : "Mesačne", + "More sources (10-20):" : "Viac zdrojov (10 – 20):", + "Most Active Objects" : "Najaktívnejšie objekty", + "Multiple Of" : "Násobok", + "Name" : "Názov", + "Names cache warmup completed" : "Zahriatie vyrovnávacej pamäte názvov dokončené", + "New Collection Name" : "Názov novej kolekcie", + "New Conversation" : "Nová konverzácia", + "New Properties" : "Nové vlastnosti", + "New Property" : "Nová vlastnosť", + "New Value" : "Nová hodnota", + "New users without specific organisation membership will be automatically added to this organisation" : "Noví používatelia bez konkrétneho členstva v organizácii budú automaticky pridaní do tejto organizácie", + "Next" : "Ďalej", + "No" : "Nie", + "No ConfigSets found" : "Nenašli sa žiadne ConfigSety", + "No GitHub token configured" : "Nie je nakonfigurovaný žiadny GitHub token", + "No GitHub token provided" : "Nebol poskytnutý žiadny GitHub token", + "No GitLab token provided" : "Nebol poskytnutý žiadny GitLab token", + "No Organisation" : "Žiadna organizácia", + "No active filters" : "Žiadne aktívne filtre", + "No activity data available" : "Nie sú dostupné žiadne údaje o aktivite", + "No agents available" : "Nie sú dostupní žiadni agenti", + "No agents found" : "Nenašli sa žiadni agenti", + "No app store cache exists yet" : "Zatiaľ neexistuje žiadna vyrovnávacia pamäť obchodu s aplikáciami", + "No applications are available." : "Nie sú dostupné žiadne aplikácie.", + "No applications found" : "Nenašli sa žiadne aplikácie", + "No archived conversations" : "Žiadne archivované konverzácie", + "No audit trail entries found" : "Nenašli sa žiadne záznamy audítneho záznamu", + "No changes recorded" : "Nezaznamenané žiadne zmeny", + "No chunks to vectorize" : "Žiadne úseky na vektorizáciu", + "No collections found" : "Nenašli sa žiadne kolekcie", + "No configuration data" : "Žiadne konfiguračné údaje", + "No configurations are available." : "Nie sú dostupné žiadne konfigurácie.", + "No configurations found" : "Nenašli sa žiadne konfigurácie", + "No configurations found for this application." : "Pre túto aplikáciu sa nenašli žiadne konfigurácie.", + "No conversations yet. Create a new one to get started!" : "Zatiaľ žiadne konverzácie. Vytvorte novú a začnite!", + "No data available" : "Nie sú dostupné žiadne údaje", + "No data available for chart" : "Nie sú dostupné žiadne údaje pre graf", + "No deleted items found" : "Nenašli sa žiadne odstránené položky", + "No deletion data available" : "Nie sú dostupné žiadne údaje o odstránení", + "No description found" : "Nenašiel sa žiadny popis", + "No description provided" : "Nebol poskytnutý žiadny popis", + "No entities detected in this file. Run text extraction first." : "V tomto súbore neboli zistené žiadne entity. Najprv spustite extrakciu textu.", + "No entities found" : "Nenašli sa žiadne entity", + "No entities have been detected yet" : "Zatiaľ neboli zistené žiadne entity", + "No events found" : "Nenašli sa žiadne udalosti", + "No expired audit trails found to clear" : "Nenašli sa žiadne uplynuté audítne záznamy na vymazanie", + "No facetable fields available. Select a register and schema to see available filters." : "Nie sú dostupné žiadne fasetovateľné polia. Vyberte register a schému, aby ste videli dostupné filtre.", + "No files found" : "Nenašli sa žiadne súbory", + "No files have been extracted yet" : "Zatiaľ neboli extrahované žiadne súbory", + "No files to process" : "Žiadne súbory na spracovanie", + "No files to reindex" : "Žiadne súbory na preindexovanie", + "No filters are currently active. This will delete ALL audit trail entries!" : "Momentálne nie sú aktívne žiadne filtre. Týmto sa odstránia VŠETKY záznamy audítneho záznamu!", + "No log entries found" : "Nenašli sa žiadne záznamy protokolu", + "No logs are available for this configuration." : "Pre túto konfiguráciu nie sú dostupné žiadne protokoly.", + "No logs are available for this source." : "Pre tento zdroj nie sú dostupné žiadne protokoly.", + "No logs found" : "Nenašli sa žiadne protokoly", + "No mismatched fields found - SOLR schema is properly configured" : "Nenašli sa žiadne nezhodné polia – SOLR schéma je správne nakonfigurovaná", + "No objects found" : "Nenašli sa žiadne objekty", + "No objects found to vectorize" : "Nenašli sa žiadne objekty na vektorizáciu", + "No objects selected for deletion" : "Na odstránenie neboli vybrané žiadne objekty", + "No objects to delete" : "Žiadne objekty na odstránenie", + "No objects were permanently deleted" : "Žiadne objekty neboli trvalo odstránené", + "No properties found" : "Nenašli sa žiadne vlastnosti", + "No properties match your filters." : "Žiadne vlastnosti nezodpovedajú vašim filtrom.", + "No purge date set" : "Nie je nastavený žiadny dátum vyčistenia", + "No register data available" : "Nie sú dostupné žiadne údaje registra", + "No registers found" : "Nenašli sa žiadne registre", + "No registers found for this application." : "Pre túto aplikáciu sa nenašli žiadne registre.", + "No relations found" : "Nenašli sa žiadne vzťahy", + "No request body available" : "Nie je dostupné žiadne telo požiadavky", + "No response body available" : "Nie je dostupné žiadne telo odpovede", + "No saved views yet. Create one in the Search tab!" : "Zatiaľ žiadne uložené zobrazenia. Vytvorte jedno na karte Vyhľadávanie!", + "No schema data available" : "Nie sú dostupné žiadne údaje schémy", + "No schema selected for exploration" : "Na preskúmanie nebola vybraná žiadna schéma", + "No schemas found" : "Nenašli sa žiadne schémy", + "No schemas found for this application." : "Pre túto aplikáciu sa nenašli žiadne schémy.", + "No search terms data available" : "Nie sú dostupné žiadne údaje o hľadaných výrazoch", + "No search trail entries found" : "Nenašli sa žiadne záznamy vyhľadávania", + "No sources are available." : "Nie sú dostupné žiadne zdroje.", + "No sources found" : "Nenašli sa žiadne zdroje", + "No templates found" : "Nenašli sa žiadne šablóny", + "No templates have been created yet" : "Zatiaľ neboli vytvorené žiadne šablóny", + "No valid magic-mapped register+schema combinations found" : "Nenašli sa žiadne platné magicky mapované kombinácie register+schéma", + "No views found. Create views first before configuring vectorization." : "Nenašli sa žiadne zobrazenia. Pred konfiguráciou vektorizácie najprv vytvorte zobrazenia.", + "No views match your search" : "Žiadne zobrazenia nezodpovedajú vášmu vyhľadávaniu", + "No webhooks found" : "Nenašli sa žiadne webhooky", + "No webhooks have been configured yet" : "Zatiaľ neboli nakonfigurované žiadne webhooky", + "No workflows found in this project. Create workflows in the n8n editor." : "V tomto projekte sa nenašli žiadne pracovné postupy. Vytvorte pracovné postupy v editore n8n.", + "None" : "Žiadne", + "Not helpful" : "Neužitočné", + "Not in use" : "Nepoužíva sa", + "Number Constraints" : "Číselné obmedzenia", + "Number of chunks to vectorize in one API call (1-100)." : "Počet úsekov na vektorizáciu v jednom API volaní (1 – 100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Počet úsekov na vektorizáciu v jednom API volaní. Vyššie = rýchlejšie, ale viac pamäte. Odporúčané: 10 – 50.", + "Number of objects to process in each batch (1-5000)." : "Počet objektov na spracovanie v každej dávke (1 – 5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Počet objektov na vektorizáciu v jednom API volaní. Vyššie = rýchlejšie, ale viac pamäte. Odporúčané: 10 – 50.", + "Numeric Range:" : "Číselný rozsah:", + "Object" : "Objekt", + "Object Collection" : "Kolekcia objektov", + "Object Count Prediction" : "Predikcia počtu objektov", + "Object ID" : "ID objektu", + "Object Management" : "Správa objektov", + "Object Vectorization" : "Vektorizácia objektov", + "Object Warmup" : "Zahriatie objektov", + "Object collection not configured" : "Kolekcia objektov nie je nakonfigurovaná", + "Object not found" : "Objekt sa nenašiel", + "Object permanently deleted" : "Objekt trvalo odstránený", + "Object restored successfully" : "Objekt bol úspešne obnovený", + "Object settings updated successfully" : "Nastavenia objektu boli úspešne aktualizované", + "Object sources" : "Zdroje objektov", + "Object unlocked successfully" : "Objekt bol úspešne odomknutý", + "Object vectorization configuration saved successfully" : "Konfigurácia vektorizácie objektov bola úspešne uložená", + "Object vectorized successfully" : "Objekt bol úspešne vektorizovaný", + "Object:" : "Objekt:", + "Objects" : "Objekty", + "Objects Analyzed" : "Analyzované objekty", + "Objects Distribution" : "Rozloženie objektov", + "Objects being analyzed" : "Analyzujú sa objekty", + "Objects by Register" : "Objekty podľa registra", + "Objects by Schema" : "Objekty podľa schémy", + "Objects deletion completed successfully" : "Odstránenie objektov bolo úspešne dokončené", + "Objects in schema" : "Objekty v schéme", + "Objects to Process:" : "Objekty na spracovanie:", + "Objects to be analyzed" : "Objekty na analýzu", + "Objects to be deleted" : "Objekty na odstránenie", + "Objects to be validated" : "Objekty na overenie", + "Objects will be serialized as JSON text before vectorization" : "Objekty budú pred vektorizáciou serializované ako JSON text", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekty budú mäkko odstránené (označené ako odstránené, ale ponechané v databáze). V prípade potreby ich možno neskôr obnoviť.", + "Old Value" : "Stará hodnota", + "Oldest Item (days)" : "Najstaršia položka (dni)", + "Ollama Chat Settings" : "Nastavenia chatu Ollama", + "Ollama Embedding Configuration" : "Konfigurácia vloženia Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Len so zmenami", + "Open Register Objects" : "Objekty Open Register", + "Open n8n Editor" : "Otvoriť editor n8n", + "OpenAI Chat Settings" : "Nastavenia chatu OpenAI", + "OpenAI Embedding Configuration" : "Konfigurácia vloženia OpenAI", + "OpenRegister Settings" : "Nastavenia OpenRegister", + "Optional webhook secret for signature verification" : "Voliteľný tajný kľúč webhooku na overenie podpisu", + "Order" : "Poradie", + "Organisation" : "Organizácia", + "Organisation ID" : "ID organizácie", + "Organisation Members" : "Členovia organizácie", + "Organisation Statistics" : "Štatistiky organizácie", + "Organisation created successfully" : "Organizácia bola úspešne vytvorená", + "Organisation settings saved successfully" : "Nastavenia organizácie boli úspešne uložené", + "Organisations" : "Organizácie", + "Organization" : "Organizácia", + "Organization ID (Optional)" : "ID organizácie (voliteľné)", + "Orphaned Items" : "Osirelé položky", + "Owner" : "Vlastník", + "Owner:" : "Vlastník:", + "Parallel Mode (Faster, more resource intensive)" : "Paralelný režim (rýchlejší, náročnejší na zdroje)", + "Parallel:" : "Paralelne:", + "Password" : "Heslo", + "Password for SOLR authentication (optional)" : "Heslo pre overenie totožnosti SOLR (voliteľné)", + "Path" : "Cesta", + "Path in repository" : "Cesta v repozitári", + "Path where the configuration file will be saved in the repository" : "Cesta, kam sa uloží konfiguračný súbor v repozitári", + "Path where the register OAS file will be saved in the repository" : "Cesta, kam sa uloží súbor OAS registra v repozitári", + "Pattern (regex)" : "Vzor (regex)", + "Pattern Issue" : "Problém vzoru", + "Pattern constraint is missing" : "Chýba obmedzenie vzoru", + "Patterns:" : "Vzory:", + "Pending" : "Čakajúce", + "Permanent Delete Mode" : "Režim trvalého odstránenia", + "Permanently Delete" : "Trvalo odstrániť", + "Permanently delete already soft-deleted objects" : "Trvalo odstrániť už mäkko odstránené objekty", + "Permanently delete this conversation?" : "Trvalo odstrániť túto konverzáciu?", + "Person" : "Osoba", + "Personal Data" : "Osobné údaje", + "Phone" : "Telefón", + "Photos" : "Fotografie", + "Please create an agent in the" : "Vytvorte agenta v", + "Please select an agent to continue" : "Vyberte agenta na pokračovanie", + "Please select which register and schema to use for the new object" : "Vyberte, ktorý register a schému použiť pre nový objekt", + "Please try again later." : "Skúste to znova neskôr.", + "Please wait while we fetch your agents." : "Počkajte, kým načítame vašich agentov.", + "Please wait while we fetch your applications." : "Počkajte, kým načítame vaše aplikácie.", + "Please wait while we fetch your configurations." : "Počkajte, kým načítame vaše konfigurácie.", + "Please wait while we fetch your deleted items." : "Počkajte, kým načítame vaše odstránené položky.", + "Please wait while we fetch your sources." : "Počkajte, kým načítame vaše zdroje.", + "Polls" : "Ankety", + "Popular Search Terms" : "Obľúbené hľadané výrazy", + "Port" : "Port", + "Prerequisites" : "Predpoklady", + "Press Enter to send, Shift+Enter for new line" : "Stlačte Enter na odoslanie, Shift+Enter pre nový riadok", + "Previous" : "Predchádzajúce", + "Private" : "Súkromné", + "Processes chunks in batches with simulated parallelism." : "Spracúva úseky v dávkach so simulovanou paralelizáciou.", + "Processes file chunks sequentially (safest)." : "Spracúva úseky súborov sekvenčne (najbezpečnejšie).", + "Processes objects in chunks with simulated parallelism." : "Spracúva objekty v úsekoch so simulovanou paralelizáciou.", + "Processes objects sequentially (safest)." : "Spracúva objekty sekvenčne (najbezpečnejšie).", + "Processing" : "Spracúva sa", + "Processing Limits" : "Limity spracovania", + "Processing..." : "Spracúva sa...", + "Project initialized successfully" : "Projekt bol úspešne inicializovaný", + "Project not found. Please initialize first." : "Projekt sa nenašiel. Najprv ho inicializujte.", + "Properties" : "Vlastnosti", + "Property Behaviors" : "Správanie vlastností", + "Property Configuration:" : "Konfigurácia vlastnosti:", + "Property Filters" : "Filtre vlastností", + "Property Title" : "Názov vlastnosti", + "Property Type" : "Typ vlastnosti", + "Property can be improved" : "Vlastnosť možno zlepšiť", + "Protected" : "Chránené", + "Provider" : "Poskytovateľ", + "Provider is required for testing" : "Poskytovateľ je povinný na testovanie", + "Public" : "Verejné", + "Public View" : "Verejné zobrazenie", + "Public views can be accessed by anyone in the system" : "Verejné zobrazenia sú prístupné komukoľvek v systéme", + "Publish" : "Publikovať", + "Publish Objects" : "Publikovať objekty", + "Publish all objects in this schema" : "Publikovať všetky objekty v tejto schéme", + "Published" : "Publikované", + "Published:" : "Publikované:", + "Publishing..." : "Publikuje sa...", + "Purge" : "Vyčistiť", + "Purge Date" : "Dátum vyčistenia", + "Query Complexity Distribution" : "Rozloženie zložitosti dopytov", + "Query parameter is required" : "Parameter dopytu je povinný", + "RAG" : "RAG", + "RAG Configuration" : "Konfigurácia RAG", + "Rate limits cleared successfully" : "Limity rýchlosti boli úspešne vymazané", + "Raw Changes Data" : "Surové údaje o zmenách", + "Re-vectorize on object update" : "Znovu vektorizovať pri aktualizácii objektu", + "Read" : "Čítať", + "Recommendations:" : "Odporúčania:", + "Recommended Type:" : "Odporúčaný typ:", + "Recommended: 5 sources" : "Odporúčané: 5 zdrojov", + "Refresh" : "Obnoviť", + "Refresh Data" : "Obnoviť údaje", + "Refresh Stats" : "Obnoviť štatistiky", + "Refresh Workflows" : "Obnoviť pracovné postupy", + "Refresh dashboard" : "Obnoviť nástenku", + "Refresh database info" : "Obnoviť informácie o databáze", + "Register" : "Register", + "Register ID" : "ID registra", + "Register Statistics" : "Štatistiky registra", + "Register Totals" : "Súčty registra", + "Register depublished successfully" : "Register bol úspešne odpublikovaný", + "Register not found" : "Register sa nenašiel", + "Register objects deletion completed successfully" : "Odstránenie objektov registra bolo úspešne dokončené", + "Register published successfully" : "Register bol úspešne publikovaný", + "Register/Schema Usage" : "Využitie registra/schémy", + "Registers" : "Registre", + "Reindex" : "Preindexovať", + "Reindex completed" : "Preindexovanie dokončené", + "Reindex completed successfully" : "Preindexovanie bolo úspešne dokončené", + "Reindex failed" : "Preindexovanie zlyhalo", + "Relations" : "Vzťahy", + "Remote Version" : "Vzdialená verzia", + "Remove" : "Odstrániť", + "Remove filter" : "Odstrániť filter", + "Remove from favorites" : "Odstrániť z obľúbených", + "Remove group" : "Odstrániť skupinu", + "Removed" : "Odstránené", + "Removed from favorites" : "Odstránené z obľúbených", + "Rename Conversation" : "Premenovať konverzáciu", + "Rename conversation" : "Premenovať konverzáciu", + "Replicas" : "Repliky", + "Repository" : "Repozitár", + "Request Body" : "Telo požiadavky", + "Request Data" : "Údaje požiadavky", + "Request Details" : "Podrobnosti požiadavky", + "Request body must be JSON with a \"query\" field" : "Telo požiadavky musí byť JSON s poľom „query“", + "Request timeout in seconds" : "Časový limit požiadavky v sekundách", + "Required field" : "Povinné pole", + "Required status is inconsistent" : "Stav povinnosti je nekonzistentný", + "Rerun Search" : "Znovu spustiť vyhľadávanie", + "Reset Changes" : "Obnoviť zmeny", + "Reset Filters" : "Obnoviť filtre", + "Response Body" : "Telo odpovede", + "Response Details" : "Podrobnosti odpovede", + "Response Time:" : "Čas odozvy:", + "Restore" : "Obnoviť", + "Restore conversation" : "Obnoviť konverzáciu", + "Restore or permanently delete items" : "Obnoviť alebo trvalo odstrániť položky", + "Result Count Range" : "Rozsah počtu výsledkov", + "Results" : "Výsledky", + "Retrieve all objects for this schema" : "Získať všetky objekty pre túto schému", + "Retry" : "Opakovať", + "Retry Failed Extractions" : "Opakovať neúspešné extrakcie", + "Retry Policy" : "Politika opakovania", + "Retry completed" : "Opakovanie dokončené", + "Retrying..." : "Opakuje sa...", + "Risk Level" : "Úroveň rizika", + "Roles updated successfully" : "Roly boli úspešne aktualizované", + "SOLR Connection Settings" : "Nastavenia pripojenia SOLR", + "SOLR Version:" : "Verzia SOLR:", + "SOLR actions menu" : "Ponuka akcií SOLR", + "SOLR base path (usually /solr)" : "Základná cesta SOLR (zvyčajne /solr)", + "SOLR is disabled" : "SOLR je zakázaný", + "SOLR is not available or not configured" : "SOLR nie je dostupný alebo nie je nakonfigurovaný", + "SOLR logging disabled" : "Protokolovanie SOLR zakázané", + "SOLR logging enabled" : "Protokolovanie SOLR povolené", + "SOLR search disabled" : "Vyhľadávanie SOLR zakázané", + "SOLR search enabled" : "Vyhľadávanie SOLR povolené", + "SOLR server hostname or IP address" : "Názov hostiteľa alebo IP adresa servera SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Číslo portu servera SOLR (voliteľné, predvolene 8983)", + "SOLR setup completed successfully" : "Nastavenie SOLR bolo úspešne dokončené", + "SOLR setup failed" : "Nastavenie SOLR zlyhalo", + "SOLR setup failed - check logs" : "Nastavenie SOLR zlyhalo – skontrolujte protokoly", + "Sample Values:" : "Vzorové hodnoty:", + "Save" : "Uložiť", + "Save Configuration" : "Uložiť konfiguráciu", + "Save Connection Settings" : "Uložiť nastavenia pripojenia", + "Save Roles" : "Uložiť roly", + "Save Settings" : "Uložiť nastavenia", + "Save current search as view" : "Uložiť aktuálne vyhľadávanie ako zobrazenie", + "Saved Views" : "Uložené zobrazenia", + "Saving..." : "Ukladá sa...", + "Schema" : "Schéma", + "Schema ID" : "ID schémy", + "Schema Name" : "Názov schémy", + "Schema Statistics" : "Štatistiky schémy", + "Schema depublished successfully" : "Schéma bola úspešne odpublikovaná", + "Schema objects deletion completed successfully" : "Odstránenie objektov schémy bolo úspešne dokončené", + "Schema published successfully" : "Schéma bola úspešne publikovaná", + "Schema successfully created" : "Schéma bola úspešne vytvorená", + "Schema successfully updated" : "Schéma bola úspešne aktualizovaná", + "Schemas" : "Schémy", + "Scheme" : "Schéma", + "Search / Views" : "Vyhľadávanie / Zobrazenia", + "Search Activity" : "Aktivita vyhľadávania", + "Search Analytics" : "Analytika vyhľadávania", + "Search Entities" : "Vyhľadať entity", + "Search Files" : "Vyhľadať súbory", + "Search GitHub" : "Vyhľadať na GitHube", + "Search GitLab" : "Vyhľadať na GitLabe", + "Search Method" : "Metóda vyhľadávania", + "Search Term" : "Hľadaný výraz", + "Search Trail Details" : "Podrobnosti záznamu vyhľadávania", + "Search Trail Management" : "Správa záznamov vyhľadávania", + "Search Trail Statistics" : "Štatistiky záznamov vyhľadávania", + "Search Trails" : "Záznamy vyhľadávania", + "Search Views" : "Vyhľadať zobrazenia", + "Search Webhooks" : "Vyhľadať webhooky", + "Search by file name or path" : "Vyhľadať podľa názvu súboru alebo cesty", + "Search by name or URL" : "Vyhľadať podľa názvu alebo URL", + "Search by value" : "Vyhľadať podľa hodnoty", + "Search in Files" : "Vyhľadať v súboroch", + "Search in Objects" : "Vyhľadať v objektoch", + "Search property names..." : "Vyhľadať názvy vlastností...", + "Search trail deletion not implemented yet" : "Odstránenie záznamu vyhľadávania ešte nie je implementované", + "Search views..." : "Vyhľadať zobrazenia...", + "Secret" : "Tajný kľúč", + "Secret key for HMAC signature generation (optional)" : "Tajný kľúč pre generovanie podpisu HMAC (voliteľné)", + "Select AI Agent" : "Vybrať AI agenta", + "Select All" : "Vybrať všetko", + "Select ConfigSet" : "Vybrať ConfigSet", + "Select File Types to Vectorize:" : "Vyberte typy súborov na vektorizáciu:", + "Select HTTP method" : "Vybrať HTTP metódu", + "Select Register and Schema" : "Vybrať register a schému", + "Select Views to Vectorize:" : "Vyberte zobrazenia na vektorizáciu:", + "Select a Nextcloud group to add" : "Vyberte skupinu Nextcloud na pridanie", + "Select a branch" : "Vyberte vetvu", + "Select a model or type a custom model name" : "Vyberte model alebo zadajte vlastný názov modelu", + "Select a repository" : "Vyberte repozitár", + "Select a repository you have write access to" : "Vyberte repozitár, ku ktorému máte prístup na zápis", + "Select an AI Agent" : "Vyberte AI agenta", + "Select an AI agent to begin chatting with your data." : "Vyberte AI agenta a začnite chatovať so svojimi údajmi.", + "Select backend" : "Vybrať backend", + "Select chat model" : "Vybrať model chatu", + "Select collection for files" : "Vybrať kolekciu pre súbory", + "Select collection for objects" : "Vybrať kolekciu pre objekty", + "Select default organisation" : "Vybrať predvolenú organizáciu", + "Select event to listen to..." : "Vyberte udalosť, ktorú počúvať...", + "Select model" : "Vybrať model", + "Select options..." : "Vyberte možnosti...", + "Select period" : "Vybrať obdobie", + "Select property to send as payload" : "Vyberte vlastnosť na odoslanie ako payload", + "Select provider" : "Vybrať poskytovateľa", + "Select registers and schemas to save a view" : "Vyberte registre a schémy na uloženie zobrazenia", + "Select retry policy" : "Vybrať politiku opakovania", + "Select the branch to publish to" : "Vyberte vetvu, do ktorej publikovať", + "Select the event this webhook should listen to" : "Vyberte udalosť, ktorú má tento webhook počúvať", + "Select views to vectorize:" : "Vyberte zobrazenia na vektorizáciu:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Vyberte, ktoré skupiny Nextcloud sú dostupné pre túto organizáciu. Používatelia v týchto skupinách budú mať prístup k zdrojom organizácie.", + "Select which data views the AI can search" : "Vyberte, ktoré dátové zobrazenia môže AI vyhľadávať", + "Select which property from the event should be used as the webhook payload data" : "Vyberte, ktorá vlastnosť z udalosti sa má použiť ako údaje payloadu webhooku", + "Select which tools the AI can use to perform actions" : "Vyberte, ktoré nástroje môže AI použiť na vykonávanie akcií", + "Selected" : "Vybrané", + "Selected Groups" : "Vybrané skupiny", + "Selected audit trails deleted successfully" : "Vybrané audítne záznamy boli úspešne odstránené", + "Selected search trails deleted successfully" : "Vybrané záznamy vyhľadávania boli úspešne odstránené", + "Selected users can access this view" : "Vybraní používatelia majú prístup k tomuto zobrazeniu", + "Send additional feedback" : "Odoslať ďalšiu spätnú väzbu", + "Send as CloudEvent" : "Odoslať ako CloudEvent", + "Sensitive PII" : "Citlivé osobné údaje", + "Serial Mode (Safer, slower)" : "Sériový režim (bezpečnejší, pomalší)", + "Serial:" : "Sériovo:", + "Server Configuration" : "Konfigurácia servera", + "Server Information" : "Informácie o serveri", + "Settings" : "Nastavenia", + "Shards" : "Shardy", + "Share" : "Zdieľať", + "Share with Groups" : "Zdieľať so skupinami", + "Share with Users" : "Zdieľať s používateľmi", + "Show Filters" : "Zobraziť filtre", + "Show less" : "Zobraziť menej", + "Show only entries with changes" : "Zobraziť len záznamy so zmenami", + "Simple" : "Jednoduché", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Jednoduché dopyty: Základné textové vyhľadávania s minimálnymi parametrami (napr. jeden hľadaný výraz, žiadne pokročilé filtre)", + "Size" : "Veľkosť", + "Slug" : "Slug", + "Soft Delete Mode" : "Režim mäkkého odstránenia", + "Soft Deleted Items" : "Mäkko odstránené položky", + "SolrCloud mode enabled" : "Režim SolrCloud povolený", + "Source" : "Zdroj", + "Sources" : "Zdroje", + "Sources:" : "Zdroje:", + "Standalone SOLR mode" : "Samostatný režim SOLR", + "Start Conversation" : "Začať konverzáciu", + "Start Vectorization" : "Spustiť vektorizáciu", + "Start a conversation" : "Začať konverzáciu", + "Starting names cache warmup..." : "Spúšťa sa zahriatie vyrovnávacej pamäte názvov...", + "Starting..." : "Spúšťa sa...", + "Statistics" : "Štatistiky", + "Stats calculation not yet implemented" : "Výpočet štatistík ešte nie je implementovaný", + "Status" : "Stav", + "Status Code" : "Stavový kód", + "String Constraints" : "Reťazcové obmedzenia", + "Success" : "Úspech", + "Success Rate" : "Miera úspešnosti", + "Success Status" : "Stav úspešnosti", + "Successful" : "Úspešné", + "Successfully joined organisation" : "Úspešné pripojenie k organizácii", + "Sync Table" : "Synchronizovať tabuľku", + "Synchronization Settings" : "Nastavenia synchronizácie", + "System Default" : "Systémové predvolené", + "Technical Description" : "Technický popis", + "Technical description for developers and administrators" : "Technický popis pre vývojárov a správcov", + "Temperature" : "Teplota", + "Templates" : "Šablóny", + "Test" : "Testovať", + "Test Chat" : "Testovať chat", + "Test Connection" : "Testovať pripojenie", + "Test Embedding" : "Testovať vloženie", + "Test endpoint executed successfully" : "Testovací koncový bod bol úspešne vykonaný", + "Test webhook delivered successfully" : "Testovací webhook bol úspešne doručený", + "Test webhook delivery failed" : "Doručenie testovacieho webhooku zlyhalo", + "Test webhook sent successfully" : "Testovací webhook bol úspešne odoslaný", + "Testing..." : "Testuje sa...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Textové úseky sa generujú počas extrakcie súborov a ukladajú sa do databázy. Vektorizácia číta tieto vopred rozdelené súbory a konvertuje ich na vloženia.", + "Text extracted successfully" : "Text bol úspešne extrahovaný", + "Text extraction disabled" : "Extrakcia textu zakázaná", + "The URL where webhook events will be sent" : "URL, kam sa budú odosielať udalosti webhooku", + "The requested conversation does not exist" : "Požadovaná konverzácia neexistuje", + "The saved view and all its search configuration" : "Uložené zobrazenie a celá jeho konfigurácia vyhľadávania", + "There are no audit trail entries matching your current filters." : "Neexistujú žiadne záznamy audítneho záznamu zodpovedajúce vašim aktuálnym filtrom.", + "There are no deleted items matching your current filters." : "Neexistujú žiadne odstránené položky zodpovedajúce vašim aktuálnym filtrom.", + "There are no search trail entries matching your current filters." : "Neexistujú žiadne záznamy vyhľadávania zodpovedajúce vašim aktuálnym filtrom.", + "There are no webhook log entries matching your filters." : "Neexistujú žiadne záznamy protokolu webhooku zodpovedajúce vašim filtrom.", + "This action cannot be undone." : "Túto akciu nie je možné vrátiť späť.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Túto akciu nie je možné vrátiť späť. Uistite sa, že tento ConfigSet nepoužívajú žiadne kolekcie.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Tento agent nemá žiadne konfigurovateľné zobrazenia ani nástroje, ale nastavenia RAG nižšie môžete stále upraviť.", + "This analysis may take some time" : "Táto analýza môže chvíľu trvať", + "This audit trail entry does not contain any change information." : "Tento záznam audítneho záznamu neobsahuje žiadne informácie o zmene.", + "This configuration has no data defined." : "Táto konfigurácia nemá definované žiadne údaje.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Tento koncový bod je zastaraný. Namiesto neho použite koncové body založené na úsekoch.", + "This entity has no relations to objects or files" : "Táto entita nemá žiadne vzťahy k objektom ani súborom", + "This is a test webhook from OpenRegister" : "Toto je testovací webhook z OpenRegister", + "This schema must use Magic Table configuration to sync" : "Táto schéma musí na synchronizáciu používať konfiguráciu magickej tabuľky", + "This source has no associated registers." : "Tento zdroj nemá žiadne priradené registre.", + "This will copy the _default ConfigSet with the new name" : "Týmto sa skopíruje ConfigSet _default s novým názvom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Týmto sa trvalo odstránia VŠETKY vloženia (vektory) z databázy. Budete musieť znovu vektorizovať všetky objekty a súbory. Túto akciu nie je možné vrátiť späť.\\n\\nNaozaj chcete pokračovať?", + "This will permanently delete:" : "Týmto sa trvalo odstráni:", + "Timeout (seconds)" : "Časový limit (sekundy)", + "Timestamp" : "Časová pečiatka", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tip: Povoľte len zobrazenia, ktoré potrebujú sémantické vyhľadávanie, aby ste minimalizovali náklady na vloženia. Jednoduché vyhľadávacie tabuľky zriedka potrebujú vektorizáciu.", + "Title" : "Názov", + "To Date" : "Do dátumu", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Na úpravu veľkosti úseku alebo stratégie prejdite na Konfigurácia súborov → Limity spracovania.", + "To change the embedding provider or model, go to LLM Configuration." : "Na zmenu poskytovateľa vloženia alebo modelu prejdite na Konfigurácia LLM.", + "To date" : "Do dátumu", + "Toggle search sidebar" : "Prepnúť bočný panel vyhľadávania", + "Tools" : "Nástroje", + "Top Deleters" : "Najčastejší odstraňovatelia", + "Total" : "Celkom", + "Total Audit Trails" : "Celkový počet audítnych záznamov", + "Total Chunks Available:" : "Celkový počet dostupných úsekov:", + "Total Deleted Items" : "Celkový počet odstránených položiek", + "Total Members" : "Celkový počet členov", + "Total Objects" : "Celkový počet objektov", + "Total Objects in Database:" : "Celkový počet objektov v databáze:", + "Total Organisations" : "Celkový počet organizácií", + "Total Results" : "Celkový počet výsledkov", + "Total Searches" : "Celkový počet vyhľadávaní", + "Total Size" : "Celková veľkosť", + "Total Storage" : "Celkové úložisko", + "Total:" : "Celkom:", + "Totals" : "Súčty", + "Type" : "Typ", + "Type Issue" : "Problém typu", + "Type Variations:" : "Variácie typu:", + "Type must be \"positive\" or \"negative\"" : "Typ musí byť „positive“ alebo „negative“", + "Type to search for groups" : "Píšte na vyhľadávanie skupín", + "Type to search for users" : "Píšte na vyhľadávanie používateľov", + "Type to search groups..." : "Píšte na vyhľadávanie skupín...", + "Type to search users..." : "Píšte na vyhľadávanie používateľov...", + "URL" : "URL", + "URL where Ollama is running" : "URL, kde beží Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Nezmenené", + "Unique Search Terms" : "Jedinečné hľadané výrazy", + "Unique Terms" : "Jedinečné výrazy", + "Unknown" : "Neznáme", + "Unknown Register" : "Neznámy register", + "Unknown Schema" : "Neznáma schéma", + "Untitled View" : "Zobrazenie bez názvu", + "Update" : "Aktualizovať", + "Update Operations" : "Operácie aktualizácie", + "Update configuration: ..." : "Aktualizovať konfiguráciu: ...", + "Update register OAS: ..." : "Aktualizovať OAS registra: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Aktualizovať vektory pri zmene údajov objektu (odporúčané pre presné vyhľadávanie)", + "Updated" : "Aktualizované", + "Updated At" : "Aktualizované o", + "Updated:" : "Aktualizované:", + "Uptime:" : "Doba prevádzky:", + "Usage Count" : "Počet použití", + "Use SolrCloud with Zookeeper for distributed search" : "Použiť SolrCloud so Zookeeperom pre distribuované vyhľadávanie", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Použite filtre na zúženie záznamov audítneho záznamu podľa registra, schémy, typu akcie, používateľa, rozsahu dátumov alebo ID objektu.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Použite filtre na zúženie odstránených položiek podľa registra, schémy, dátumu odstránenia alebo používateľa, ktorý ich odstránil.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Použite filtre na zúženie záznamov vyhľadávania podľa registra, schémy, stavu úspešnosti, používateľa, rozsahu dátumov, hľadaných výrazov alebo výkonnostných metrík.", + "Used By Collections" : "Používané kolekciami", + "User" : "Používateľ", + "User Agent Statistics" : "Štatistiky agenta používateľa", + "User Description" : "Popis používateľa", + "User rate limits cleared successfully" : "Limity rýchlosti používateľa boli úspešne vymazané", + "User removed successfully" : "Používateľ bol úspešne odstránený", + "User-friendly description shown in forms and help text" : "Používateľsky prívetivý popis zobrazený vo formulároch a texte pomocníka", + "Username" : "Používateľské meno", + "Username for SOLR authentication (optional)" : "Používateľské meno pre overenie totožnosti SOLR (voliteľné)", + "Users" : "Používatelia", + "Using Pre-Generated Chunks" : "Použitie vopred vygenerovaných úsekov", + "Validate" : "Overiť", + "Validate Objects" : "Overiť objekty", + "Validation completed successfully" : "Overenie bolo úspešne dokončené", + "Validation failed" : "Overenie zlyhalo", + "Value" : "Hodnota", + "Vector Dimensions" : "Vektorové rozmery", + "Vector Search Backend" : "Backend vektorového vyhľadávania", + "Vector field: _embedding_" : "Vektorové pole: _embedding_", + "Vectorization Triggers" : "Spúšťače vektorizácie", + "Vectorize All Files" : "Vektorizovať všetky súbory", + "Vectorize All Objects" : "Vektorizovať všetky objekty", + "Vectorize all file types" : "Vektorizovať všetky typy súborov", + "Vectorize all views" : "Vektorizovať všetky zobrazenia", + "Vectorize on object creation" : "Vektorizovať pri vytvorení objektu", + "Vectors will be stored in your existing object and file collections" : "Vektory sa uložia vo vašich existujúcich kolekciách objektov a súborov", + "Version" : "Verzia", + "Very High" : "Veľmi vysoká", + "View API Docs" : "Zobraziť dokumentáciu API", + "View Changes" : "Zobraziť zmeny", + "View Details" : "Zobraziť podrobnosti", + "View Error" : "Zobraziť chybu", + "View File" : "Zobraziť súbor", + "View Full Details" : "Zobraziť všetky podrobnosti", + "View Logs" : "Zobraziť protokoly", + "View Name" : "Názov zobrazenia", + "View Object" : "Zobraziť objekt", + "View Parameters" : "Zobraziť parametre", + "View Selection" : "Výber zobrazenia", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Zobrazte a analyzujte protokoly záznamov vyhľadávania s pokročilými možnosťami filtrovania a analytiky", + "View and analyze system audit trails with advanced filtering capabilities" : "Zobrazte a analyzujte systémové audítne záznamy s pokročilými možnosťami filtrovania", + "View deleted successfully" : "Zobrazenie bolo úspešne odstránené", + "View entity details and manage relations" : "Zobrazte podrobnosti entity a spravujte vzťahy", + "View name" : "Názov zobrazenia", + "View name is required" : "Názov zobrazenia je povinný", + "View saved successfully!" : "Zobrazenie bolo úspešne uložené!", + "View search analytics and manage search logs" : "Zobrazte analytiku vyhľadávania a spravujte protokoly vyhľadávania", + "View successfully deleted" : "Zobrazenie bolo úspešne odstránené", + "View successfully updated" : "Zobrazenie bolo úspešne aktualizované", + "View updated successfully!" : "Zobrazenie bolo úspešne aktualizované!", + "View webhook delivery logs and filter by webhook" : "Zobrazte protokoly doručenia webhooku a filtrujte podľa webhooku", + "Views" : "Zobrazenia", + "Visible to users" : "Viditeľné pre používateľov", + "Wait for Response" : "Čakať na odpoveď", + "Wait for webhook response before continuing (required for request/response flows)" : "Pred pokračovaním čakať na odpoveď webhooku (vyžadované pre toky požiadavka/odpoveď)", + "Warmup Names Cache" : "Zahriať vyrovnávaciu pamäť názvov", + "Warning" : "Upozornenie", + "Warning:" : "Upozornenie:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Podrobnosti protokolu webhooku", + "Webhook Logs" : "Protokoly webhooku", + "Webhook created successfully" : "Webhook bol úspešne vytvorený", + "Webhook deleted" : "Webhook odstránený", + "Webhook retry delivered successfully" : "Opakovanie webhooku bolo úspešne doručené", + "Webhook retry delivery failed" : "Doručenie opakovania webhooku zlyhalo", + "Webhook updated" : "Webhook aktualizovaný", + "Webhook updated successfully" : "Webhook bol úspešne aktualizovaný", + "Webhooks" : "Webhooky", + "Weekly" : "Týždenne", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Zabaliť payload webhooku do formátu CloudEvents pre lepšiu interoperabilitu", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Áno", + "You" : "Vy", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Môžete vytvárať nové ConfigSety založené na šablóne _default alebo nahrať vlastné priamo na váš SOLR server.", + "You do not have access to this conversation" : "Nemáte prístup k tejto konverzácii", + "You do not have permission to delete this conversation" : "Nemáte oprávnenie na odstránenie tejto konverzácie", + "You do not have permission to modify this conversation" : "Nemáte oprávnenie na úpravu tejto konverzácie", + "You do not have permission to restore this conversation" : "Nemáte oprávnenie na obnovenie tejto konverzácie", + "You must be logged in to favorite views" : "Na pridanie zobrazení medzi obľúbené musíte byť prihlásení", + "You need an AI agent to start a conversation." : "Na začatie konverzácie potrebujete AI agenta.", + "Your Fireworks AI API key. Get one at" : "Váš API kľúč Fireworks AI. Získajte ho na", + "Your OpenAI API key. Get one at" : "Váš API kľúč OpenAI. Získajte ho na", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Vaša spätná väzba bola zaznamenaná. Voliteľne tu môžete poskytnúť ďalšie podrobnosti...", + "Zookeeper Hosts" : "Hostitelia Zookeeper", + "Zookeeper Port" : "Port Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Nastavenia Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Reťazec pripojenia Zookeeper pre SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Číslo portu Zookeeper (voliteľné, predvolene 2181)", + "chunk" : "úsek", + "chunks" : "úseky", + "chunks processed" : "spracovaných úsekov", + "configuration(s)" : "konfigurácia(-ie)", + "e.g., lib/Settings/config.json" : "napr. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "napr. lib/Settings/register.json", + "failed" : "zlyhalo", + "file" : "súbor", + "files" : "súbory", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "členovia", + "menu or contact someone with permission to create agents." : "ponuke alebo kontaktujte niekoho s oprávnením vytvárať agentov.", + "messages" : "správy", + "n8n URL and API key are required" : "URL a API kľúč n8n sú povinné", + "n8n configuration saved successfully" : "Konfigurácia n8n bola úspešne uložená", + "n8n connection not configured" : "Pripojenie n8n nie je nakonfigurované", + "n8n connection successful" : "Pripojenie n8n úspešné", + "n8n connection test successful" : "Test pripojenia n8n úspešný", + "n8n integration disabled" : "Integrácia n8n zakázaná", + "n8n integration enabled" : "Integrácia n8n povolená", + "n8n project initialized successfully" : "Projekt n8n bol úspešne inicializovaný", + "n8n settings saved successfully" : "Nastavenia n8n boli úspešne uložené", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekty", + "objects processed" : "spracovaných objektov", + "or pick a range" : "alebo vyberte rozsah", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "poskytuje dobrú rovnováhu medzi rýchlosťou a presnosťou.", + "register(s)" : "register(-tre)", + "register(s) selected" : "register(-tre) vybraté", + "required" : "povinné", + "results" : "výsledky", + "schema(s)" : "schéma(-y)", + "schema(s) selected" : "schéma(-y) vybraté", + "sk-..." : "sk-...", + "this application" : "táto aplikácia", + "to" : "do", + "vectorized" : "vektorizované", + "views selected" : "zobrazení vybratých", + "ℹ️ Current Configuration" : "ℹ️ Aktuálna konfigurácia", + "⚡ Batch Processing" : "⚡ Dávkové spracovanie", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Komplexný kontext ✓ Menšia pravdepodobnosť vynechania podrobností ✗ Pomalšie odpovede ✗ Môže zahŕňať menej relevantné informácie", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Rýchlejšie odpovede ✓ Cielenejšie odpovede ✗ Môže vynechať relevantné informácie", + "✨ AI Features" : "✨ Funkcie AI", + "All search trails cleared successfully" : "Všetky záznamy vyhľadávania boli úspešne vymazané", + "Multiple search trail deletion not implemented yet" : "Odstránenie viacerých záznamov vyhľadávania ešte nie je implementované", + "No expired search trails found to clear" : "Nenašli sa žiadne uplynuté záznamy vyhľadávania na vymazanie", + "Missing message" : "Chýba správa", + "message content is required" : "obsah správy je povinný", + "Missing conversation or agentUuid" : "Chýba konverzácia alebo agentUuid", + "Access denied" : "Prístup zamietnutý", + "Conversation not found" : "Konverzácia sa nenašla", + "AI service not configured" : "Služba AI nie je nakonfigurovaná", + "Failed to process message" : "Nepodarilo sa spracovať správu", + "Missing conversationId" : "Chýba conversationId", + "conversationId is required" : "conversationId je povinné", + "Failed to fetch conversation history" : "Nepodarilo sa načítať históriu konverzácie", + "Failed to clear conversation" : "Nepodarilo sa vymazať konverzáciu", + "Invalid feedback type" : "Neplatný typ spätnej väzby", + "type must be \"positive\" or \"negative\"" : "typ musí byť „positive“ alebo „negative“", + "Message not found" : "Správa sa nenašla", + "Failed to save feedback" : "Nepodarilo sa uložiť spätnú väzbu", + "Failed to get chat statistics" : "Nepodarilo sa získať štatistiky chatu", + "Not Found" : "Nenájdené", + "Engine not found" : "Engine sa nenašiel", + "Consumer not found" : "Konzument sa nenašiel", + "Backend parameter is required" : "Parameter backendu je povinný", + "Failed to get database information: %s" : "Nepodarilo sa získať informácie o databáze: %s", + "SOLR setup error: %s" : "Chyba nastavenia SOLR: %s", + "Reindex failed: %s" : "Preindexovanie zlyhalo: %s", + "Anonymized" : "Anonymizované", + "Entities detected" : "Zistené entity", + "Extract Now" : "Extrahovať teraz", + "Extracted at" : "Extrahované o", + "Extraction" : "Extrakcia", + "Failed to load extraction data" : "Nepodarilo sa načítať údaje extrakcie", + "Failed to load register data" : "Nepodarilo sa načítať údaje registra", + "Failed to retrieve extraction status." : "Nepodarilo sa získať stav extrakcie.", + "Failed to retrieve objects for file." : "Nepodarilo sa získať objekty pre súbor.", + "No extraction data available for this file" : "Pre tento súbor nie sú dostupné žiadne údaje extrakcie", + "No register objects reference this file" : "Tento súbor neodkazuje žiadne objekty registra", + "Not extracted" : "Neextrahované", + "Register Objects" : "Objekty registra", + "Risk level" : "Úroveň rizika", + "Text chunks" : "Textové úseky", + "Unknown error" : "Neznáma chyba", + "Very high" : "Veľmi vysoká", + "{title} in {register} / {schema}" : "{title} v {register} / {schema}", + "Push Notifications" : "Push notifikácie", + "Real-time push notification status via notify_push" : "Stav push notifikácií v reálnom čase cez notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push v reálnom čase nie je dostupný — aplikácia notify_push nie je nainštalovaná", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Nainštalujte aplikáciu notify_push z obchodu Nextcloud App Store na povolenie aktualizácií v reálnom čase.", + "Open Nextcloud App Store" : "Otvoriť Nextcloud App Store", + "notify_push is installed but not yet active" : "notify_push je nainštalovaný, ale ešte nie je aktívny", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push je nainštalovaný, ale OpenRegister ešte nepotvrdil úspešný push. Aktivujte ho uložením objektu alebo skontrolujte svoju konfiguráciu notify_push.", + "notify_push configuration guide" : "sprievodca konfiguráciou notify_push", + "Realtime push active" : "Push v reálnom čase aktívny", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push notifikácie v reálnom čase sú aktívne. Pripojení klienti dostávajú okamžité aktualizácie pri vytvorení, aktualizácii alebo odstránení objektov.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" +); diff --git a/l10n/sk.json b/l10n/sk.json new file mode 100644 index 0000000000..fd1542d00f --- /dev/null +++ b/l10n/sk.json @@ -0,0 +1,2638 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(vyžaduje sa Dolphin)", + "(no subject)": "(bez predmetu)", + "(no title)": "(bez názvu)", + "(unnamed)": "(bez názvu)", + "(untitled card)": "(karta bez názvu)", + "({count} selected)": "(vybrané: {count})", + ", {failed} failed": ", {failed} zlyhalo", + "0 = unlimited": "0 = neobmedzené", + "A deactivation request is pending.": "Žiadosť o deaktiváciu čaká na vybavenie.", + "AI Assistant": "AI asistent", + "AI Chat": "AI chat", + "AI service not configured": "Služba AI nie je nakonfigurovaná", + "API Key": "API kľúč", + "API Request Quota (requests/day)": "Kvóta API požiadaviek (požiadaviek/deň)", + "API Token Configuration": "Konfigurácia API tokenov", + "API Tokens": "API tokeny", + "API tokens saved successfully": "API tokeny boli úspešne uložené", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: táto správa spája každú verwerkingsactiviteit s celkovými počtami záznamov auditnej stopy, ktoré sú jej priradené. Audítori a Autoriteit Persoonsgegevens ju používajú na dozorné preskúmanie.", + "About ConfigSets": "O ConfigSetoch", + "Access denied": "Prístup zamietnutý", + "Access organisation dashboard and analytics": "Prístup k nástenke a analytike organizácie", + "Account": "Účet", + "Action": "Akcia", + "Action Distribution": "Rozloženie akcií", + "Action:": "Akcia:", + "Actions": "Akcie", + "Active": "Aktívne", + "Active Collections": "Aktívne kolekcie", + "Active Filters": "Aktívne filtre", + "Active Organisation:": "Aktívna organizácia:", + "Active Organisations": "Aktívne organizácie", + "Active filters:": "Aktívne filtre:", + "Active organisation changed successfully": "Aktívna organizácia bola úspešne zmenená", + "Active organisation set successfully": "Aktívna organizácia bola úspešne nastavená", + "Activities": "Aktivity", + "Activity": "Aktivita", + "Activity Period": "Obdobie aktivity", + "Add": "Pridať", + "Add Attachment": "Pridať prílohu", + "Add Endpoint": "Pridať koncový bod", + "Add Groups": "Pridať skupiny", + "Add Property to '{title}'": "Pridať vlastnosť do '{title}'", + "Add Register": "Pridať register", + "Add Schema": "Pridať schému", + "Add Source": "Pridať zdroj", + "Add User": "Pridať používateľa", + "Add User to Organisation": "Pridať používateľa do organizácie", + "Add a contact from any of your address books to associate it with this object.": "Pridajte kontakt z ktoréhokoľvek z vašich adresárov a priraďte ho k tomuto objektu.", + "Add a file or files": "Pridať súbor alebo súbory", + "Add card": "Pridať kartu", + "Add contact": "Pridať kontakt", + "Add endpoint": "Pridať koncový bod", + "Add oneOf entry": "Pridať položku oneOf", + "Add range": "Pridať rozsah", + "Add schema titles, descriptions, and register information to provide richer context for search": "Pridajte názvy schém, popisy a informácie o registri pre bohatší kontext vyhľadávania", + "Add to favorites": "Pridať medzi obľúbené", + "Added": "Pridané", + "Added to favorites": "Pridané medzi obľúbené", + "Additional Information": "Ďalšie informácie", + "Additional feedback saved. Thank you!": "Ďalšia spätná väzba bola uložená. Ďakujeme!", + "Addresses": "Adresy", + "Admin": "Správca", + "Advanced": "Pokročilé", + "Advanced Filters": "Pokročilé filtre", + "Advanced Options": "Pokročilé možnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Pokročilé filtre nie sú dostupné pri použití databázového zdroja. Prepnite na Auto alebo SOLR Index pre možnosti filtrovania.", + "After": "Po", + "Agent Type": "Typ agenta", + "Agent deleted successfully": "Agent bol úspešne odstránený", + "Agent successfully created": "Agent bol úspešne vytvorený", + "Agent successfully updated": "Agent bol úspešne aktualizovaný", + "Agents": "Agenti", + "Aggregated across schemas": "Agregované naprieč schémami", + "All": "Všetko", + "All Categories": "Všetky kategórie", + "All Collections": "Všetky kolekcie", + "All Confidence Levels": "Všetky úrovne spoľahlivosti", + "All ConfigSets": "Všetky ConfigSety", + "All Files": "Všetky súbory", + "All Levels": "Všetky úrovne", + "All Types": "Všetky typy", + "All Webhooks": "Všetky webhooky", + "All actions": "Všetky akcie", + "All audit trails cleared successfully": "Všetky audítne záznamy boli úspešne vymazané", + "All clear": "Všetko v poriadku", + "All file types": "Všetky typy súborov", + "All registers": "Všetky registre", + "All schemas": "Všetky schémy", + "All search trails cleared successfully": "Všetky záznamy vyhľadávania boli úspešne vymazané", + "All searches": "Všetky vyhľadávania", + "All users": "Všetci používatelia", + "All-day events": "Celodenné udalosti", + "Allowed MIME Types": "Povolené typy MIME", + "Already linked": "Už prepojené", + "An error occurred while clearing audit trails": "Pri vymazávaní audítnych záznamov sa vyskytla chyba", + "An error occurred while deleting the audit trail": "Pri odstraňovaní audítneho záznamu sa vyskytla chyba", + "An error occurred while deleting the object": "Pri odstraňovaní objektu sa vyskytla chyba", + "An error occurred while deleting the view": "Pri odstraňovaní zobrazenia sa vyskytla chyba", + "An error occurred while permanently deleting the objects": "Pri trvalom odstraňovaní objektov sa vyskytla chyba", + "An error occurred while restoring the objects": "Pri obnovovaní objektov sa vyskytla chyba", + "An error occurred while saving the organisation": "Pri ukladaní organizácie sa vyskytla chyba", + "Analysis completed:": "Analýza dokončená:", + "Analysis steps:": "Kroky analýzy:", + "Analytics": "Analytika", + "Analyze Objects": "Analyzovať objekty", + "Analyze Properties": "Analyzovať vlastnosti", + "Analyze existing properties for improvement opportunities": "Analyzujte existujúce vlastnosti pre možnosti zlepšenia", + "Analyzing...": "Analyzuje sa...", + "Anonymized": "Anonymizované", + "Anonymous Group": "Anonymná skupina", + "Any favorites and sharing settings for this view": "Akékoľvek obľúbené položky a nastavenia zdieľania pre toto zobrazenie", + "Any user": "Akýkoľvek používateľ", + "App ID": "ID aplikácie", + "App store cache invalidated successfully": "Vyrovnávacia pamäť obchodu s aplikáciami bola úspešne zneplatnená", + "App store cache invalidated: {invalidated}": "Vyrovnávacia pamäť obchodu s aplikáciami bola zneplatnená: {invalidated}", + "Application deleted successfully": "Aplikácia bola úspešne odstránená", + "Application identifier for this configuration (optional)": "Identifikátor aplikácie pre túto konfiguráciu (voliteľné)", + "Application settings": "Nastavenia aplikácie", + "Application successfully created": "Aplikácia bola úspešne vytvorená", + "Application successfully updated": "Aplikácia bola úspešne aktualizovaná", + "Applications": "Aplikácie", + "Apply": "Použiť", + "Apply Changes": "Použiť zmeny", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Použiť rolu na všetky schémy v tomto registri, ktoré nemajú explicitné prepísania autorizácie.", + "Approval Progress": "Priebeh schvaľovania", + "Approve": "Schváliť", + "Approved": "Schválené", + "Archive": "Archivovať", + "Archive conversation": "Archivovať konverzáciu", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Archivovať túto verwerkingsactiviteit? Riadky auditnej stopy na ňu budú naďalej odkazovať.", + "Archived conversations are hidden from your active list": "Archivované konverzácie sú skryté z vášho aktívneho zoznamu", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Naozaj chcete ODSTRÁNIŤ kolekciu \"{name}\"?\n\nToto:\n• Natrvalo odstráni kolekciu a všetky jej údaje\n• Odstráni všetky indexované dokumenty\n• Túto akciu nie je možné vrátiť späť", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Naozaj chcete vyčistiť staré záznamy vyhľadávania? Týmto sa odstránia záznamy staršie ako 30 dní.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Naozaj chcete vymazať všetky údaje z kolekcie \"{name}\"?\n\nToto:\n• Odstráni všetky indexované dokumenty\n• Zachová štruktúru kolekcie nedotknutú\n• Túto akciu nie je možné vrátiť späť", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Naozaj chcete odstrániť aplikáciu \"{name}\"? Túto akciu nie je možné vrátiť späť.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Naozaj chcete odstrániť vybrané audítne záznamy? Túto akciu nie je možné vrátiť späť.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Naozaj chcete odstrániť vybrané záznamy vyhľadávania? Túto akciu nie je možné vrátiť späť.", + "Are you sure you want to delete this ConfigSet?": "Naozaj chcete odstrániť tento ConfigSet?", + "Are you sure you want to delete this configset?": "Naozaj chcete odstrániť tento ConfigSet?", + "Are you sure you want to leave '{name}'?": "Naozaj chcete opustiť '{name}'?", + "Are you sure you want to permanently delete": "Naozaj chcete trvalo odstrániť", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Naozaj chcete znovu indexovať kolekciu \"{name}\"?\n\nToto:\n• Znovu zostaví index so všetkými objektmi\n• Dokončenie potrvá niekoľko minút\n• Môže ovplyvniť výkon vyhľadávania počas reindexácie", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Naozaj chcete odstrániť schému \"{schema}\" z registra \"{register}\"? Túto akciu nie je možné vrátiť späť.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Naozaj chcete vykonať mäkké odstránenie {count} aktívnych objektov pre schému \"{schema}\"?\n\nBudú označené ako odstránené, ale neskôr ich možno natrvalo odstrániť.", + "Array Object Configuration:": "Konfigurácia objektu poľa:", + "Articles": "Články", + "Ask a question...": "Položte otázku...", + "Ask questions about your data using natural language": "Pýtajte sa na svoje údaje pomocou prirodzeného jazyka", + "Assigned collaborative tags": "Priradené spoločné značky", + "Assignment notifications": "Upozornenia na priradenia", + "Attachment added to {name}": "Príloha pridaná do {name}", + "Attempt": "Pokus", + "Audit Trail Changes": "Zmeny audítneho záznamu", + "Audit Trail Details": "Podrobnosti audítneho záznamu", + "Audit Trail Management": "Správa audítnych záznamov", + "Audit Trail Statistics": "Štatistiky audítnych záznamov", + "Audit Trails": "Audítne záznamy", + "Audit trail data copied to clipboard": "Údaje audítneho záznamu skopírované do schránky", + "Audit trail deleted successfully": "Audítny záznam bol úspešne odstránený", + "Audit trail successfully deleted": "Audítny záznam bol úspešne odstránený", + "Authentication": "Overenie totožnosti", + "Auto (e.g. Y for year, F Y for month)": "Automaticky (napr. Y pre rok, F Y pre mesiac)", + "Auto share": "Automatické zdieľanie", + "Auto-commit disabled": "Automatické potvrdenie zakázané", + "Auto-commit enabled": "Automatické potvrdenie povolené", + "Auto-create Default Organisation": "Automaticky vytvoriť predvolenú organizáciu", + "Auto-retry failed vectorizations": "Automaticky opakovať neúspešné vektorizácie", + "Automatically commit changes to SOLR index": "Automaticky potvrdiť zmeny do SOLR indexu", + "Automatically connect to the federation network.": "Automaticky sa pripojiť k federačnej sieti.", + "Automatically create a default organisation if none exists when the app is initialized": "Automaticky vytvoriť predvolenú organizáciu, ak žiadna neexistuje pri inicializácii aplikácie", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automaticky generovať vektorové vloženia z textových úsekov pri nahraní a spracovaní súborov", + "Automatically generate vector embeddings when objects are created or updated": "Automaticky generovať vektorové vloženia pri vytvorení alebo aktualizácii objektov", + "Automatically retry failed vectorization attempts (max 3 retries)": "Automaticky opakovať neúspešné pokusy o vektorizáciu (max. 3 opakovania)", + "Automatically update catalogues.": "Automaticky aktualizovať katalógy.", + "Available Filters": "Dostupné filtre", + "Available Workflows": "Dostupné pracovné postupy", + "Available placeholders:": "Dostupné zástupné symboly:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Zmeny avatara nie sú podporované vaším poskytovateľom overovania.", + "Avatar removed": "Avatar bol odstránený", + "Avatar updated successfully": "Avatar bol úspešne aktualizovaný", + "Avg Execution Time": "Priemerný čas vykonania", + "Avg Members/Org": "Priemer členov/org.", + "Avg Object Views/Session": "Priemer zobrazení objektov/reláciu", + "Avg Response Time": "Priemerný čas odozvy", + "Avg Results/Search": "Priemer výsledkov/vyhľadávanie", + "Avg Searches/Session": "Priemer vyhľadávaní/reláciu", + "Back": "Späť", + "Back to Entities": "Späť na entity", + "Back to Registers": "Späť na registre", + "Back to Webhooks": "Späť na webhooky", + "Back to applications": "Späť na aplikácie", + "Back to entities": "Späť na entity", + "Backend parameter is required": "Parameter backendu je povinný", + "Backend updated successfully. Please reload the application.": "Backend bol úspešne aktualizovaný. Znovu načítajte aplikáciu.", + "Bandwidth Quota (MB/month)": "Kvóta šírky pásma (MB/mesiac)", + "Base URL (Optional)": "Základná URL (voliteľné)", + "Basic Information": "Základné informácie", + "Batch Size": "Veľkosť dávky", + "Batch extraction completed": "Dávková extrakcia dokončená", + "Before": "Pred", + "Before object vectorization can work:": "Predtým, než môže vektorizácia objektov fungovať:", + "Behavior": "Správanie", + "Behavior Issue": "Problém správania", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (trvanie ISO-8601, napr. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Úložisko blob bolo vyradené. Všetky objekty teraz používajú magické tabuľky.", + "Bookmarks": "Záložky", + "Branch": "Vetva", + "Bulk Role Assignment: {title}": "Hromadné priradenie rolí: {title}", + "Bulk delete operation completed successfully": "Hromadná operácia odstránenia bola úspešne dokončená", + "Bulk save operation completed successfully": "Hromadná operácia uloženia bola úspešne dokončená", + "Business Data": "Obchodné údaje", + "CSV": "CSV", + "Cache cleared successfully": "Vyrovnávacia pamäť bola úspešne vymazaná", + "Cache grew from {old} to {new} entries.": "Vyrovnávacia pamäť vzrástla z {old} na {new} položiek.", + "Cache shrunk from {old} to {new} entries.": "Vyrovnávacia pamäť sa zmenšila z {old} na {new} položiek.", + "Cache stayed the same at {size} entries.": "Vyrovnávacia pamäť zostala rovnaká na {size} položkách.", + "Calculate Sizes": "Vypočítať veľkosti", + "Calendar": "Kalendár", + "Calendar Provider Configuration": "Konfigurácia poskytovateľa kalendára", + "Calendar integration is not available": "Integrácia kalendára nie je dostupná", + "Calendar name": "Názov kalendára", + "Cancel": "Zrušiť", + "Cancel deactivation request": "Zrušiť žiadosť o deaktiváciu", + "Cannot delete: objects are still attached": "Nie je možné odstrániť: objekty sú stále pripojené", + "Cannot edit: This register is managed by external configuration {title}": "Nie je možné upraviť: Tento register je spravovaný externou konfiguráciou {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nie je možné odstrániť schému s existujúcimi objektmi ({count} objektov)", + "Cannot remove schema {schema} because it contains {count} objects": "Nie je možné odstrániť schému {schema}, pretože obsahuje {count} objektov", + "Cards": "Karty", + "Cascade delete": "Kaskádové odstránenie", + "Categories of data subjects (one per line)": "Kategórie dotknutých osôb (jedna na riadok)", + "Categories of personal data (one per line)": "Kategórie osobných údajov (jedna na riadok)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategória", + "Change Type": "Typ zmeny", + "Change password": "Zmeniť heslo", + "Changes": "Zmeny", + "Changes copied to clipboard": "Zmeny skopírované do schránky", + "Changes for Audit Trail #{id}": "Zmeny pre auditnú stopu #{id}", + "Chat Model": "Model chatu", + "Chat Provider": "Poskytovateľ chatu", + "Chat Provider (RAG)": "Poskytovateľ chatu (RAG)", + "Chat Settings": "Nastavenia chatu", + "Chat provider connection successful!": "Pripojenie k poskytovateľovi chatu úspešné!", + "Chat settings": "Nastavenia chatu", + "Chat test failed: {error}": "Test chatu zlyhal: {error}", + "Choose a register": "Vyberte register", + "Choose a schema": "Vyberte schému", + "Choose how vector similarity calculations are performed for semantic search": "Vyberte, ako sa vykonávajú výpočty vektorovej podobnosti pre sémantické vyhľadávanie", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Vyberte, ktoré typy súborov zahrnúť do procesu vektorizácie. Spracujú sa len súbory s extrahovaným textom a úsekmi.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Vyberte, ktoré zobrazenia zahrnúť do procesu vektorizácie. Ponechajte prázdne pre spracovanie všetkých zobrazení podľa vašej konfigurácie.", + "Chunk": "Úsek", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Odstránenie úseku ešte nie je implementované. Použite koncové body založené na úsekoch.", + "Chunks": "Úseky", + "Chunks to Vectorize:": "Úseky na vektorizáciu:", + "Cleanup Old Trails": "Vyčistiť staré záznamy", + "Cleanup completed": "Čistenie dokončené", + "Cleanup completed successfully. Deleted {count} entries.": "Vyčistenie bolo úspešne dokončené. Odstránených {count} položiek.", + "Cleanup failed: {error}": "Vyčistenie zlyhalo: {error}", + "Clear All Embeddings": "Vymazať všetky vloženia", + "Clear Entries": "Vymazať záznamy", + "Clear Filtered Audit Trails": "Vymazať filtrované audítne záznamy", + "Clear Filters": "Vymazať filtre", + "Clear Index": "Vymazať index", + "Clear Selection": "Vymazať výber", + "Clear all": "Vymazať všetko", + "Clear all filters": "Vymazať všetky filtre", + "Clear date range": "Vymazať rozsah dátumov", + "Clear filters": "Vymazať filtre", + "Clearing...": "Vymazáva sa...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Kliknutím na Spustiť kontrolu súladu nájdete schémy, kde boli zistené osobné údaje, ale neexistuje žiadna anotácia spracovateľskej činnosti.", + "Click to subscribe to notifications": "Kliknutím sa prihlásite na odber upozornení", + "Click to unsubscribe from notifications": "Kliknutím sa odhlásite z odberu upozornení", + "Close": "Zatvoriť", + "Cluster name": "Názov klastra", + "Code": "Kód", + "Code (short readable key, e.g. v-2026-001)": "Kód (krátky čitateľný kľúč, napr. v-2026-001)", + "Collapse details": "Zbaliť podrobnosti", + "Collection Management": "Správa kolekcií", + "Collection Name": "Názov kolekcie", + "Collection assignments updated successfully": "Priradenia kolekcií boli úspešne aktualizované", + "Collection cleared successfully": "Kolekcia bola úspešne vymazaná", + "Collection copied successfully": "Kolekcia bola úspešne skopírovaná", + "Collection created successfully": "Kolekcia bola úspešne vytvorená", + "Collection deleted successfully": "Kolekcia bola úspešne odstránená", + "Collection used to store and index file metadata and content": "Kolekcia použitá na uloženie a indexovanie metaúdajov a obsahu súborov", + "Collection used to store and index object data": "Kolekcia použitá na uloženie a indexovanie údajov objektov", + "Collections:": "Kolekcie:", + "Color": "Farba", + "Comment...": "Komentár...", + "Commit Message": "Správa potvrdenia", + "Commit Within (ms)": "Potvrdiť do (ms)", + "Commit message": "Správa potvrdenia", + "Compare current schema with real object data": "Porovnať aktuálnu schému so skutočnými údajmi objektu", + "Completed": "Dokončené", + "Complex": "Komplexné", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Komplexné dopyty: Pokročilé vyhľadávania s viacerými filtrami, operátormi a komplexnými kombináciami parametrov", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Zložité dotazy: pokročilé vyhľadávania s viacerými filtrami, operátormi a zložitými kombináciami parametrov", + "Compliance": "Súlad s predpismi", + "Confidence Level": "Úroveň spoľahlivosti", + "Confidence Score": "Skóre spoľahlivosti", + "Confidence: {confidence}%": "Spoľahlivosť: {confidence}%", + "Config must be provided as an object": "Konfigurácia musí byť poskytnutá ako objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Správa ConfigSetov", + "ConfigSet Name": "Názov ConfigSetu", + "ConfigSet created successfully": "ConfigSet bol úspešne vytvorený", + "ConfigSet deleted successfully": "ConfigSet bol úspešne odstránený", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSety definujú schému a konfiguráciu vašich SOLR kolekcií. Obsahujú definície polí, analyzátory a ďalšie nastavenia vyhľadávania.", + "Configuration": "Konfigurácia", + "Configuration Data": "Konfiguračné údaje", + "Configuration is up to date": "Konfigurácia je aktuálna", + "Configuration saved": "Konfigurácia uložená", + "Configuration saved successfully.": "Konfigurácia bola úspešne uložená.", + "Configuration type (default, application, etc.)": "Typ konfigurácie (predvolená, aplikačná atď.)", + "Configurations": "Konfigurácie", + "Configure API tokens for external service integrations": "Konfigurovať API tokeny pre integrácie externých služieb", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurovať vyhľadávací nástroj Apache SOLR pre pokročilé možnosti vyhľadávania", + "Configure Facets": "Konfigurovať fasety", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurujte poskytovateľov veľkých jazykových modelov (LLM) pre funkcie poháňané AI vrátane sémantického vyhľadávania, vložení a chatu.", + "Configure SOLR Facets": "Konfigurovať fazety SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurujte základné nastavenia pripojenia pre váš SOLR server vrátane overenia totožnosti a sieťových možností. Na správu jadier a kolekcií použite samostatné dialógové okná správy ConfigSetov a kolekcií.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurujte základné nastavenia pripojenia pre váš server SOLR vrátane overovania a sieťových možností. Na správu jadier a kolekcií použite samostatné dialógy správy ConfigSetov a kolekcií.", + "Configure default organisation and organisation-related settings": "Konfigurovať predvolenú organizáciu a nastavenia súvisiace s organizáciou", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurujte, ako sa databázové objekty konvertujú na vektorové vloženia pre sémantické vyhľadávanie. Objekty sa vektorizujú priamo bez potreby extrakcie textu.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurujte, ako sa objekty konvertujú na text pred vektorizáciou. Tieto nastavenia ovplyvňujú kvalitu vyhľadávania a kontext.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurujte poskytovateľov veľkých jazykových modelov (LLM) pre funkcie s podporou AI vrátane sémantického vyhľadávania, vnárania a chatu.", + "Configure multi-organization support and tenant isolation": "Konfigurovať podporu viacerých organizácií a izoláciu nájomníkov", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurujte parametre pre vektorizáciu súborov. Tento proces vygeneruje vektorové vloženia pre všetky extrahované úseky súborov.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurujte parametre pre vektorizáciu objektov. Tento proces vygeneruje vektorové vloženia pre všetky objekty zodpovedajúce vašim filtrom zobrazenia.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Nakonfigurujte túto schému tak, aby zobrazovala objekty ako udalosti v aplikácii Nextcloud Kalendár.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurujte, ktoré typy údajov vyhľadávať a koľko zdrojov získať", + "Configuring field mappings...": "Konfigurácia mapovaní polí...", + "Confirm Account Deactivation": "Potvrdiť deaktiváciu účtu", + "Confirm Object Deletion": "Potvrdiť odstránenie objektu", + "Confirm Object Restoration": "Potvrdiť obnovenie objektu", + "Confirm Permanent Object Deletion": "Potvrdiť trvalé odstránenie objektu", + "Confirm deactivation": "Potvrdiť deaktiváciu", + "Connecting to SOLR cluster...": "Pripájanie ku klastru SOLR...", + "Connection Failed": "Pripojenie zlyhalo", + "Connection Settings": "Nastavenia pripojenia", + "Connection Successful!": "Pripojenie úspešné!", + "Connection failed": "Pripojenie zlyhalo", + "Connection protocol": "Protokol pripojenia", + "Connection successful": "Pripojenie úspešné", + "Connection timeout in seconds": "Časový limit pripojenia v sekundách", + "Constant delay between retries (5 minutes)": "Konštantné oneskorenie medzi opakovaniami (5 minút)", + "Constraint Issue": "Problém obmedzenia", + "Consumer not found": "Konzument sa nenašiel", + "Contacts": "Kontakty", + "Contacts integration is not available": "Integrácia kontaktov nie je dostupná", + "Content Filters": "Filtre obsahu", + "Continue to Properties": "Pokračovať na vlastnosti", + "Contracts": "Zmluvy", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Ovládajte, ktoré zobrazenia objektov sa majú vektorizovať na zníženie nákladov na API. Vektorizujte len zobrazenia, ktoré profitujú zo sémantického vyhľadávania.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Ovládajte, ktoré zobrazenia a nástroje môže AI v tejto konverzácii používať. Predvolene sú povolené všetky schopnosti agenta.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Ovláda náhodnosť (0 = deterministické, 2 = veľmi kreatívne)", + "Conversation ID is required": "ID konverzácie je povinné", + "Conversation archived": "Konverzácia archivovaná", + "Conversation archived successfully": "Konverzácia bola úspešne archivovaná", + "Conversation cleared successfully": "Konverzácia bola úspešne vymazaná", + "Conversation deleted": "Konverzácia odstránená", + "Conversation not found": "Konverzácia sa nenašla", + "Conversation permanently deleted": "Konverzácia trvalo odstránená", + "Conversation renamed": "Konverzácia premenovaná", + "Conversation restored": "Konverzácia obnovená", + "Conversation started with {agent}": "Konverzácia začatá s {agent}", + "Conversation title": "Názov konverzácie", + "Conversations": "Konverzácie", + "Converting {schema} to blob storage...": "Konverzia {schema} na úložisko blob...", + "Converting {schema} to magic table...": "Konverzia {schema} na magickú tabuľku...", + "Copied!": "Skopírované!", + "Copy": "Kopírovať", + "Copy Changes": "Kopírovať zmeny", + "Copy Collection": "Kopírovať kolekciu", + "Copy Data": "Kopírovať údaje", + "Copy Full Data": "Kopírovať všetky údaje", + "Copy of {name}": "Kópia {name}", + "Copy to clipboard": "Kopírovať do schránky", + "Copying...": "Kopíruje sa...", + "Costs": "Náklady", + "Count": "Počet", + "Counting objects...": "Počítajú sa objekty...", + "Create": "Vytvoriť", + "Create API Token": "Vytvoriť API token", + "Create Agent": "Vytvoriť agenta", + "Create Application": "Vytvoriť aplikáciu", + "Create Collection": "Vytvoriť kolekciu", + "Create ConfigSet": "Vytvoriť ConfigSet", + "Create New Collection": "Vytvoriť novú kolekciu", + "Create New ConfigSet": "Vytvoriť nový ConfigSet", + "Create Operations": "Operácie vytvorenia", + "Create Organisation": "Vytvoriť organizáciu", + "Create Webhook": "Vytvoriť webhook", + "Create a copy of collection:": "Vytvoriť kópiu kolekcie:", + "Create a new ConfigSet based on the _default template": "Vytvoriť nový ConfigSet založený na šablóne _default", + "Create a new SOLR collection from an existing ConfigSet": "Vytvoriť novú SOLR kolekciu z existujúceho ConfigSetu", + "Create a new SOLR collection from an existing configset": "Vytvoriť novú kolekciu SOLR z existujúceho ConfigSetu", + "Create a new configset based on the _default template": "Vytvoriť nový ConfigSet na základe šablóny _default", + "Create a new event or link an existing one from any of your calendars.": "Vytvorte novú udalosť alebo prepojte existujúcu z ktoréhokoľvek z vašich kalendárov.", + "Create another": "Vytvoriť ďalší", + "Create copies of": "Vytvoriť kópie", + "Create event": "Vytvoriť udalosť", + "Create new token": "Vytvoriť nový token", + "Create or link a Deck card to track work on this object.": "Vytvorte alebo prepojte kartu Deck na sledovanie práce na tomto objekte.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Vytvorte prvú verwerkingsactiviteit a začnite označovať riadky auditnej stopy ich priradením podľa AVG Art 30.", + "Create your first AI agent to get started.": "Vytvorte svojho prvého AI agenta a začnite.", + "Created": "Vytvorené", + "Created:": "Vytvorené:", + "Creating search collections...": "Vytváranie vyhľadávacích kolekcií...", + "Creating...": "Vytvára sa...", + "Current": "Aktuálne", + "Current Filters": "Aktuálne filtre", + "Current Type:": "Aktuálny typ:", + "Current password": "Aktuálne heslo", + "Custom API endpoint if using a different region": "Vlastný API koncový bod pri použití inej oblasti", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Vlastné HTTP hlavičky (jedna na riadok, formát: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Vlastné hlavičky HTTP (jedna na riadok, formát: header-name: value)", + "Custom Ranges:": "Vlastné rozsahy:", + "Custom display title for this facet": "Vlastný zobrazovaný názov pre túto fazetu", + "Custom pattern:": "Vlastný vzor:", + "DELETE COLLECTION": "ODSTRÁNIŤ KOLEKCIU", + "DSAR": "DSAR", + "Daily": "Denne", + "Dashboard": "Nástenka", + "Dashboard not found": "Nástenka sa nenašla", + "Data": "Údaje", + "Data Source": "Zdroj údajov", + "Data Sources": "Zdroje údajov", + "Data Views": "Zobrazenia údajov", + "Data sources": "Zdroje údajov", + "Data type does not match observed values": "Typ údajov nezodpovedá pozorovaným hodnotám", + "Data-subject access request": "Žiadosť dotknutej osoby o prístup", + "Database URL": "URL databázy", + "Database information refreshed": "Informácie o databáze obnovené", + "Date Faceting:": "Fazetovanie podľa dátumu:", + "Date Range": "Rozsah dátumov", + "Dates": "Dátumy", + "Deactivation request cancelled": "Žiadosť o deaktiváciu bola zrušená", + "Deactivation request submitted": "Žiadosť o deaktiváciu bola odoslaná", + "Deck": "Deck", + "Deck integration is not available": "Integrácia Deck nie je dostupná", + "Default": "Predvolené", + "Default Object Tenant": "Predvolený nájomník objektu", + "Default Organisation": "Predvolená organizácia", + "Default Owner": "Predvolený vlastník", + "Default User (for cron/background jobs)": "Predvolený používateľ (pre cron/úlohy na pozadí)", + "Default User Tenant": "Predvolený nájomník používateľa", + "Default value": "Predvolená hodnota", + "Default value (Email)": "Predvolená hodnota (e-mail)", + "Default value (Password)": "Predvolená hodnota (heslo)", + "Default value (Phone number)": "Predvolená hodnota (telefónne číslo)", + "Default value (Regex)": "Predvolená hodnota (regulárny výraz)", + "Default: 5": "Predvolené: 5", + "Defaults to current user. Select a different user if needed.": "Predvolene aktuálny používateľ. V prípade potreby vyberte iného používateľa.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Oneskorenia sa s každým pokusom zdvojnásobujú (2, 4, 8 minút...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Oneskorenia sa zvyšujú lineárne (5, 10, 15 minút...)", + "Delete": "Odstrániť", + "Delete ({count})": "Odstrániť ({count})", + "Delete Application": "Odstrániť aplikáciu", + "Delete Audit Trail": "Odstrániť audítny záznam", + "Delete Collection": "Odstrániť kolekciu", + "Delete ConfigSet": "Odstrániť ConfigSet", + "Delete Objects": "Odstrániť objekty", + "Delete Operations": "Operácie odstránenia", + "Delete SOLR Collection": "Odstrániť kolekciu SOLR", + "Delete View": "Odstrániť zobrazenie", + "Delete all objects in this schema": "Odstrániť všetky objekty v tejto schéme", + "Delete permanently": "Odstrániť trvalo", + "Delete this conversation?": "Odstrániť túto konverzáciu?", + "Delete view": "Odstrániť zobrazenie", + "Delete {count} object": "Odstrániť {count} objekt", + "Delete {count} objects": "Odstrániť {count} objektov", + "Deleted": "Odstránené", + "Deleted By": "Odstránil", + "Deleted Date": "Dátum odstránenia", + "Deleted Items Management": "Správa odstránených položiek", + "Deleted This Week": "Odstránené tento týždeň", + "Deleted Today": "Odstránené dnes", + "Deleted:": "Odstránené:", + "Deleting SOLR Collection...": "Odstraňovanie kolekcie SOLR...", + "Deleting...": "Odstraňuje sa...", + "Deletion Date Range": "Rozsah dátumov odstránenia", + "Deletion Statistics": "Štatistiky odstránení", + "Deprecated": "Zastarané", + "Depublish": "Zrušiť publikovanie", + "Description": "Popis", + "Description Template": "Šablóna popisu", + "Description shown as tooltip": "Popis zobrazený ako popis nástroja", + "Description:": "Popis:", + "Detect data types and patterns": "Zistiť typy údajov a vzory", + "Detected At": "Zistené o", + "Detected Format:": "Zistený formát:", + "Detected Issues:": "Zistené problémy:", + "Detected: {date}": "Zistené: {date}", + "Direct permission": "Priame oprávnenie", + "Disable": "Zakázať", + "Disable this to make the agent": "Vypnutím tejto možnosti urobíte agenta", + "Disabled": "Zakázané", + "Discover Files": "Objaviť súbory", + "Discovered Properties": "Objavené vlastnosti", + "Display Format": "Formát zobrazenia", + "Display Name": "Zobrazované meno", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Chcete trvalo odstrániť všetky filtrované záznamy audítneho záznamu? Túto akciu nie je možné vrátiť späť.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Chcete trvalo odstrániť tento záznam audítneho záznamu? Túto akciu nie je možné vrátiť späť.", + "Documents": "Dokumenty", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR povolené", + "Download API Spec": "Stiahnuť API špecifikáciu", + "Download API Specification": "Stiahnuť špecifikáciu API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Stiahnite si kópiu všetkých vašich osobných údajov uložených v OpenRegister (GDPR článok 20).", + "Drag a file or files here": "Sem presuňte súbor alebo súbory", + "Dry run completed: {count} fields would be created": "Skúšobný beh dokončený: bolo by vytvorených {count} polí", + "Dry run completed: {count} fields would be fixed": "Skúšobný beh dokončený: bolo by opravených {count} polí", + "Duration in seconds (optional)": "Trvanie v sekundách (voliteľné)", + "Edit": "Upraviť", + "Edit Agent": "Upraviť agenta", + "Edit Application": "Upraviť aplikáciu", + "Edit Configuration": "Upraviť konfiguráciu", + "Edit Endpoint": "Upraviť koncový bod", + "Edit Organisation": "Upraviť organizáciu", + "Edit Property '{key}' of '{title}'": "Upraviť vlastnosť '{key}' v '{title}'", + "Edit Register": "Upraviť register", + "Edit Schema": "Upraviť schému", + "Edit Source": "Upraviť zdroj", + "Edit View": "Upraviť zobrazenie", + "Edit Webhook": "Upraviť webhook", + "Edit labels": "Upraviť štítky", + "Edit processing activity": "Upraviť spracovateľskú činnosť", + "Edit tags for {name}": "Upraviť značky pre {name}", + "Edit verwerkingsactiviteit": "Upraviť verwerkingsactiviteit", + "Edit view": "Upraviť zobrazenie", + "Edit view details": "Upraviť podrobnosti zobrazenia", + "Effectiveness": "Účinnosť", + "Email": "E-mail", + "Email Addresses": "E-mailové adresy", + "Email digest frequency": "Frekvencia e-mailového súhrnu", + "Emails": "E-maily", + "Embedding Model": "Model vloženia", + "Embedding Provider": "Poskytovateľ vloženia", + "Embedding generated successfully": "Vloženie bolo úspešne vygenerované", + "Embedding provider connection successful!": "Pripojenie k poskytovateľovi vloženia úspešné!", + "Embedding test failed: {error}": "Test vnárania zlyhal: {error}", + "Enable": "Povoliť", + "Enable RAG": "Povoliť RAG", + "Enable auto-creation": "Povoliť automatické vytváranie", + "Enable automatic file vectorization": "Povoliť automatickú vektorizáciu súborov", + "Enable automatic object vectorization": "Povoliť automatickú vektorizáciu objektov", + "Enable automatic synchronization": "Povoliť automatickú synchronizáciu", + "Enable calendar provider": "Povoliť poskytovateľa kalendára", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Povoliť podrobné protokolovanie pre SOLR operácie (odporúčané pri ladení)", + "Enable faceting": "Povoliť fasetovanie", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Povoliť alebo zakázať funkcie LLM. Konfigurujte poskytovateľov a modely pomocou tlačidla Konfigurácia LLM vyššie.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Povoliť alebo zakázať funkcie LLM. Poskytovateľov a modely nakonfigurujte pomocou tlačidla konfigurácie LLM vyššie.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Povoliť alebo zakázať integráciu SOLR vyhľadávania. Konfigurujte nastavenia pripojenia pomocou tlačidla Nastavenia pripojenia vyššie.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Povoliť alebo zakázať integráciu vyhľadávania SOLR. Nastavenia pripojenia nakonfigurujte pomocou tlačidla nastavení pripojenia vyššie.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Povoliť alebo zakázať integráciu pracovných postupov n8n. Konfigurujte nastavenia pripojenia nižšie.", + "Enable or disable this webhook": "Povoliť alebo zakázať tento webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Povoľte nástroje, ktoré agentovi umožňujú interagovať s údajmi prostredníctvom volania funkcií.", + "Enable vectorization for all existing and future views (may increase costs)": "Povoliť vektorizáciu pre všetky existujúce a budúce zobrazenia (môže zvýšiť náklady)", + "Enable write-back to target objects": "Povoliť spätný zápis do cieľových objektov", + "Enabled": "Povolené", + "End Date Field": "Pole dátumu konca", + "Endpoint Path*": "Cesta koncového bodu*", + "Endpoints": "Koncové body", + "Engine not found": "Engine sa nenašiel", + "Enter ConfigSet name": "Zadajte názov ConfigSetu", + "Enter Nextcloud usernames to grant access to this private agent": "Zadajte používateľské mená Nextcloud na udelenie prístupu k tomuto súkromnému agentovi", + "Enter agent description (optional)": "Zadajte popis agenta (voliteľné)", + "Enter agent name": "Zadajte názov agenta", + "Enter application description (optional)": "Zadajte popis aplikácie (voliteľné)", + "Enter application name": "Zadajte názov aplikácie", + "Enter collection name": "Zadajte názov kolekcie", + "Enter configuration description (optional)": "Zadajte popis konfigurácie (voliteľné)", + "Enter configuration title": "Zadajte názov konfigurácie", + "Enter description (optional)...": "Zadajte popis (voliteľné)...", + "Enter new collection name": "Zadajte nový názov kolekcie", + "Enter object ID": "Zadajte ID objektu", + "Enter organisation description (optional)": "Zadajte popis organizácie (voliteľné)", + "Enter organisation name": "Zadajte názov organizácie", + "Enter search term": "Zadajte hľadaný výraz", + "Enter search terms or leave empty to browse all": "Zadajte hľadané výrazy alebo nechajte prázdne na prehliadanie všetkého", + "Enter system prompt for the agent": "Zadajte systémovú výzvu pre agenta", + "Enter username and press Enter": "Zadajte používateľské meno a stlačte Enter", + "Enter view name...": "Zadajte názov zobrazenia...", + "Enter webhook name": "Zadajte názov webhooku", + "Enter your API key": "Zadajte svoj API kľúč", + "Enter your object here...": "Sem zadajte svoj objekt...", + "Enter your schema here...": "Sem zadajte svoju schému...", + "Entities": "Entity", + "Entities detected": "Zistené entity", + "Entity ID: {id}": "ID entity: {id}", + "Entity Information": "Informácie o entite", + "Entity Recognition": "Rozpoznávanie entít", + "Entity Recognition Method": "Metóda rozpoznávania entít", + "Entity deleted successfully": "Entita bola úspešne odstránená", + "Entity not found": "Entita sa nenašla", + "Entries to be deleted:": "Záznamy na odstránenie:", + "Enum Issue": "Problém enum", + "Enum constraint is missing": "Chýba obmedzenie enum", + "Erase (Art 17)": "Vymazať (čl. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Vymazať {count} objekt(ov) pre túto osobu? Táto akcia sa zaznamenáva do auditnej stopy.", + "Erasure complete": "Vymazanie dokončené", + "Erasure preview": "Náhľad vymazania", + "Error": "Chyba", + "Error Details": "Podrobnosti chyby", + "Error Information": "Informácie o chybe", + "Error Message": "Chybové hlásenie", + "Error deleting audit trails: {error}": "Chyba pri odstraňovaní auditných stôp: {error}", + "Error deleting search trails: {error}": "Chyba pri odstraňovaní stôp vyhľadávania: {error}", + "Error loading application": "Chyba pri načítavaní aplikácie", + "Error loading audit trails": "Chyba pri načítavaní audítnych záznamov", + "Error loading entity": "Chyba pri načítavaní entity", + "Error loading search trails": "Chyba pri načítavaní záznamov vyhľadávania", + "Estimated Batches:": "Odhadované dávky:", + "Estimated Cost:": "Odhadované náklady:", + "Estimated Duration:": "Odhadované trvanie:", + "Event": "Udalosť", + "Event Property for Payload": "Vlastnosť udalosti pre payload", + "Events": "Udalosti", + "Every schema with detected PII has a processing-activity annotation.": "Každá schéma so zistenými osobnými údajmi má anotáciu spracovateľskej činnosti.", + "Example": "Príklad", + "Example Value": "Príkladová hodnota", + "Example value for this property": "Príklad hodnoty pre túto vlastnosť", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Exkluzívne maximum", + "Exclusive Minimum": "Exkluzívne minimum", + "Exclusive maximum": "Exkluzívne maximum", + "Exclusive minimum": "Exkluzívne minimum", + "Execution Mode": "Režim vykonania", + "Execution Time": "Čas vykonania", + "Execution Time Range": "Rozsah času vykonania", + "Existing Improvements": "Existujúce zlepšenia", + "Expand details": "Rozbaliť podrobnosti", + "Expiration": "Vypršanie platnosti", + "Expires": "Vyprší", + "Expires in (e.g., 90d)": "Vyprší o (napr. 90d)", + "Exponential": "Exponenciálne", + "Export": "Exportovať", + "Export \"{schema}\" objects from \"{register}\"": "Exportovať objekty \"{schema}\" z \"{register}\"", + "Export Configuration": "Exportovať konfiguráciu", + "Export Format:": "Formát exportu:", + "Export Objects": "Exportovať objekty", + "Export completed successfully": "Export bol úspešne dokončený", + "Export downloaded successfully": "Export bol úspešne stiahnutý", + "Export failed: {error}": "Export zlyhal: {error}", + "Export is rate limited. Please try again later.": "Export je obmedzený rýchlosťou. Skúste to prosím neskôr.", + "Export my data": "Exportovať moje údaje", + "Export, view, or delete audit trails": "Exportujte, zobrazte alebo odstráňte audítne záznamy", + "Exporting...": "Exportovanie...", + "Extend Schema": "Rozšíriť schému", + "Extended": "Rozšírené", + "Extensions": "Rozšírenia", + "Extra Query Parameters": "Ďalšie parametre dotazu", + "Extract Now": "Extrahovať teraz", + "Extract Pending Files": "Extrahovať čakajúce súbory", + "Extract now": "Extrahovať teraz", + "Extract properties from each object": "Extrahovať vlastnosti z každého objektu", + "Extracted At": "Extrahované o", + "Extracted at": "Extrahované o", + "Extraction": "Extrakcia", + "Extraction Mode": "Režim extrakcie", + "Extraction Scope": "Rozsah extrakcie", + "Extraction Status": "Stav extrakcie", + "Extraction error for {file}: {error}": "Chyba extrakcie pre {file}: {error}", + "Facet Description": "Popis fazety", + "Facet Order": "Poradie faziet", + "Facet Title": "Názov fazety", + "Facet Type": "Typ fazety", + "Facet configuration updated successfully": "Konfigurácia fasiet bola úspešne aktualizovaná", + "Facetable": "Fazetovateľné", + "Faceting Configuration:": "Konfigurácia fazetovania:", + "Facets discovered and configured successfully": "Fasety boli úspešne objavené a nakonfigurované", + "Facets discovered successfully": "Fasety boli úspešne objavené", + "Failed": "Zlyhalo", + "Failed to add attachment to object": "Nepodarilo sa pridať prílohu k objektu", + "Failed to add user to organisation": "Nepodarilo sa pridať používateľa do organizácie", + "Failed to analyze schema properties": "Nepodarilo sa analyzovať vlastnosti schémy", + "Failed to archive conversation": "Nepodarilo sa archivovať konverzáciu", + "Failed to calculate sizes": "Nepodarilo sa vypočítať veľkosti", + "Failed to cancel deactivation": "Nepodarilo sa zrušiť deaktiváciu", + "Failed to change active organisation: {error}": "Nepodarilo sa zmeniť aktívnu organizáciu: {error}", + "Failed to change password": "Nepodarilo sa zmeniť heslo", + "Failed to check version: {error}": "Nepodarilo sa skontrolovať verziu: {error}", + "Failed to clear audit trails: {error}": "Nepodarilo sa vymazať auditné stopy: {error}", + "Failed to clear blob objects: {error}": "Nepodarilo sa vymazať objekty blob: {error}", + "Failed to clear cache: {error}": "Nepodarilo sa vymazať vyrovnávaciu pamäť: {error}", + "Failed to clear collection": "Nepodarilo sa vymazať kolekciu", + "Failed to clear conversation": "Nepodarilo sa vymazať konverzáciu", + "Failed to clear embeddings: {error}": "Nepodarilo sa vymazať vnárania: {error}", + "Failed to clear search trails: {error}": "Nepodarilo sa vymazať stopy vyhľadávania: {error}", + "Failed to clear {type} cache: {error}": "Nepodarilo sa vymazať vyrovnávaciu pamäť {type}: {error}", + "Failed to copy changes": "Nepodarilo sa skopírovať zmeny", + "Failed to copy data": "Nepodarilo sa skopírovať údaje", + "Failed to copy data to clipboard": "Nepodarilo sa skopírovať údaje do schránky", + "Failed to copy to clipboard": "Nepodarilo sa skopírovať do schránky", + "Failed to copy token": "Nepodarilo sa skopírovať token", + "Failed to create conversation": "Nepodarilo sa vytvoriť konverzáciu", + "Failed to create missing SOLR fields: {error}": "Nepodarilo sa vytvoriť chýbajúce polia SOLR: {error}", + "Failed to create or find project": "Nepodarilo sa vytvoriť alebo nájsť projekt", + "Failed to create token": "Nepodarilo sa vytvoriť token", + "Failed to delete SOLR collection: {error}": "Nepodarilo sa odstrániť kolekciu SOLR: {error}", + "Failed to delete collection": "Nepodarilo sa odstrániť kolekciu", + "Failed to delete conversation": "Nepodarilo sa odstrániť konverzáciu", + "Failed to delete objects for {schema}: {error}": "Nepodarilo sa odstrániť objekty pre {schema}: {error}", + "Failed to delete webhook": "Nepodarilo sa odstrániť webhook", + "Failed to depublish register: {error}": "Nepodarilo sa zrušiť publikovanie registra: {error}", + "Failed to depublish schema: {error}": "Nepodarilo sa zrušiť publikovanie schémy: {error}", + "Failed to depublish: {error}": "Nepodarilo sa zrušiť publikovanie: {error}", + "Failed to discover files: {error}": "Nepodarilo sa objaviť súbory: {error}", + "Failed to download API specification": "Nepodarilo sa stiahnuť API špecifikáciu", + "Failed to export data": "Nepodarilo sa exportovať údaje", + "Failed to export objects": "Nepodarilo sa exportovať objekty", + "Failed to fetch conversation history": "Nepodarilo sa načítať históriu konverzácie", + "Failed to fix mismatched SOLR fields: {error}": "Nepodarilo sa opraviť nezhodujúce sa polia SOLR: {error}", + "Failed to get SOLR field configuration": "Nepodarilo sa získať konfiguráciu polí SOLR", + "Failed to get chat statistics": "Nepodarilo sa získať štatistiky chatu", + "Failed to get database information: %s": "Nepodarilo sa získať informácie o databáze: %s", + "Failed to get entity categories": "Nepodarilo sa získať kategórie entít", + "Failed to get entity statistics": "Nepodarilo sa získať štatistiky entít", + "Failed to get entity types": "Nepodarilo sa získať typy entít", + "Failed to get response: {error}": "Nepodarilo sa získať odpoveď: {error}", + "Failed to invalidate app store cache: {error}": "Nepodarilo sa zneplatniť vyrovnávaciu pamäť obchodu s aplikáciami: {error}", + "Failed to leave organisation: {error}": "Nepodarilo sa opustiť organizáciu: {error}", + "Failed to link object": "Nepodarilo sa prepojiť objekt", + "Failed to load Deck cards": "Nepodarilo sa načítať karty Deck", + "Failed to load LLM configuration": "Nepodarilo sa načítať konfiguráciu LLM", + "Failed to load Nextcloud groups": "Nepodarilo sa načítať skupiny Nextcloud", + "Failed to load cache statistics: {error}": "Nepodarilo sa načítať štatistiky vyrovnávacej pamäte: {error}", + "Failed to load conversation": "Nepodarilo sa načítať konverzáciu", + "Failed to load entities": "Nepodarilo sa načítať entity", + "Failed to load entity": "Nepodarilo sa načítať entitu", + "Failed to load extraction data": "Nepodarilo sa načítať údaje extrakcie", + "Failed to load files": "Nepodarilo sa načítať súbory", + "Failed to load linked contacts": "Nepodarilo sa načítať prepojené kontakty", + "Failed to load linked emails": "Nepodarilo sa načítať prepojené e-maily", + "Failed to load linked events": "Nepodarilo sa načítať prepojené udalosti", + "Failed to load organisations": "Nepodarilo sa načítať organizácie", + "Failed to load register data": "Nepodarilo sa načítať údaje registra", + "Failed to load related entities": "Nepodarilo sa načítať súvisiace entity", + "Failed to load settings: {error}": "Nepodarilo sa načítať nastavenia: {error}", + "Failed to load statistics: {error}": "Nepodarilo sa načítať štatistiky: {error}", + "Failed to load templates": "Nepodarilo sa načítať šablóny", + "Failed to load view: {error}": "Nepodarilo sa načítať zobrazenie: {error}", + "Failed to load webhooks": "Nepodarilo sa načítať webhooky", + "Failed to load workflows": "Nepodarilo sa načítať pracovné postupy", + "Failed to process message": "Nepodarilo sa spracovať správu", + "Failed to publish register: {error}": "Nepodarilo sa publikovať register: {error}", + "Failed to publish schema: {error}": "Nepodarilo sa publikovať schému: {error}", + "Failed to publish: {error}": "Nepodarilo sa publikovať: {error}", + "Failed to refresh database information": "Nepodarilo sa obnoviť informácie o databáze", + "Failed to reindex collection": "Nepodarilo sa preindexovať kolekciu", + "Failed to remove avatar": "Nepodarilo sa odstrániť avatar", + "Failed to remove link": "Nepodarilo sa odstrániť prepojenie", + "Failed to remove member: {error}": "Nepodarilo sa odstrániť člena: {error}", + "Failed to remove schema {schema}: {error}": "Nepodarilo sa odstrániť schému {schema}: {error}", + "Failed to remove user from organisation": "Nepodarilo sa odstrániť používateľa z organizácie", + "Failed to rename conversation": "Nepodarilo sa premenovať konverzáciu", + "Failed to request deactivation": "Nepodarilo sa požiadať o deaktiváciu", + "Failed to restore conversation": "Nepodarilo sa obnoviť konverzáciu", + "Failed to retrieve extraction status.": "Nepodarilo sa získať stav extrakcie.", + "Failed to retrieve objects for file.": "Nepodarilo sa získať objekty pre súbor.", + "Failed to retry extraction": "Nepodarilo sa zopakovať extrakciu", + "Failed to retry webhook": "Nepodarilo sa zopakovať webhook", + "Failed to revoke token": "Nepodarilo sa zrušiť token", + "Failed to save GitHub token": "Nepodarilo sa uložiť GitHub token", + "Failed to save GitLab URL": "Nepodarilo sa uložiť GitLab URL", + "Failed to save GitLab token": "Nepodarilo sa uložiť GitLab token", + "Failed to save LLM settings: {error}": "Nepodarilo sa uložiť nastavenia LLM: {error}", + "Failed to save additional feedback": "Nepodarilo sa uložiť ďalšiu spätnú väzbu", + "Failed to save configuration.": "Nepodarilo sa uložiť konfiguráciu.", + "Failed to save configuration: {error}": "Nepodarilo sa uložiť konfiguráciu: {error}", + "Failed to save facet configuration": "Nepodarilo sa uložiť konfiguráciu fazety", + "Failed to save feedback": "Nepodarilo sa uložiť spätnú väzbu", + "Failed to save file settings: {error}": "Nepodarilo sa uložiť nastavenia súborov: {error}", + "Failed to save n8n configuration": "Nepodarilo sa uložiť konfiguráciu n8n", + "Failed to save preferences": "Nepodarilo sa uložiť predvoľby", + "Failed to save roles": "Nepodarilo sa uložiť roly", + "Failed to save settings": "Nepodarilo sa uložiť nastavenia", + "Failed to save settings: {error}": "Nepodarilo sa uložiť nastavenia: {error}", + "Failed to save view: {error}": "Nepodarilo sa uložiť zobrazenie: {error}", + "Failed to save warmup interval: {error}": "Nepodarilo sa uložiť interval zahrievania: {error}", + "Failed to save webhook": "Nepodarilo sa uložiť webhook", + "Failed to send feedback": "Nepodarilo sa odoslať spätnú väzbu", + "Failed to set active organisation: {error}": "Nepodarilo sa nastaviť aktívnu organizáciu: {error}", + "Failed to setup SOLR: {error}": "Nepodarilo sa nastaviť SOLR: {error}", + "Failed to start file warmup: {error}": "Nepodarilo sa spustiť zahriatie súborov: {error}", + "Failed to start processing {type} files: {error}": "Nepodarilo sa spustiť spracovanie súborov {type}: {error}", + "Failed to start vectorization: {error}": "Nepodarilo sa spustiť vektorizáciu: {error}", + "Failed to switch organisation: {error}": "Nepodarilo sa prepnúť organizáciu: {error}", + "Failed to sync magic table for {schema}: {error}": "Nepodarilo sa synchronizovať magickú tabuľku pre {schema}: {error}", + "Failed to test SOLR connection: {error}": "Nepodarilo sa otestovať pripojenie k SOLR: {error}", + "Failed to test webhook": "Nepodarilo sa otestovať webhook", + "Failed to update LLM settings: {error}": "Nepodarilo sa aktualizovať nastavenia LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Nepodarilo sa aktualizovať nastavenia viacnásobného nájmu: {error}", + "Failed to update RBAC settings: {error}": "Nepodarilo sa aktualizovať nastavenia RBAC: {error}", + "Failed to update Retention settings: {error}": "Nepodarilo sa aktualizovať nastavenia uchovávania: {error}", + "Failed to update SOLR settings: {error}": "Nepodarilo sa aktualizovať nastavenia SOLR: {error}", + "Failed to update favorite status": "Nepodarilo sa aktualizovať stav obľúbenosti", + "Failed to update schema configuration for {schema}: {error}": "Nepodarilo sa aktualizovať konfiguráciu schémy pre {schema}: {error}", + "Failed to update schema properties": "Nepodarilo sa aktualizovať vlastnosti schémy", + "Failed to update view": "Nepodarilo sa aktualizovať zobrazenie", + "Failed to update view: {error}": "Nepodarilo sa aktualizovať zobrazenie: {error}", + "Failed to update webhook": "Nepodarilo sa aktualizovať webhook", + "Failed to upload avatar": "Nepodarilo sa nahrať avatar", + "Failed to validate {schema}: {error}": "Nepodarilo sa overiť {schema}: {error}", + "Failed to vectorize files: {error}": "Nepodarilo sa vektorizovať súbory: {error}", + "Failed to warmup names cache: {error}": "Nepodarilo sa zahriať vyrovnávaciu pamäť názvov: {error}", + "Federation": "Federácia", + "Feedback recorded": "Spätná väzba zaznamenaná", + "Fewer sources (1-3):": "Menej zdrojov (1 – 3):", + "Field": "Pole", + "Fields to Display": "Polia na zobrazenie", + "File": "Súbor", + "File #{id}": "Súbor #{id}", + "File Chunk Prediction": "Predikcia úsekov súboru", + "File Collection": "Kolekcia súborov", + "File Handling": "Spracovanie súborov", + "File Location": "Umiestnenie súboru", + "File Management": "Správa súborov", + "File Name": "Názov súboru", + "File Path": "Cesta k súboru", + "File Type Selection": "Výber typu súboru", + "File Types to Process": "Typy súborov na spracovanie", + "File Vectorization": "Vektorizácia súborov", + "File Warmup": "Zahriatie súborov", + "File actions menu": "Ponuka akcií súboru", + "File anonymized successfully": "Súbor bol úspešne anonymizovaný", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vektorizácia úsekov súborov ešte nie je implementovaná. Úseky sú pripravené a uložené, ale služba vektorizácie sa vyvíja.", + "File collection not configured": "Kolekcia súborov nie je nakonfigurovaná", + "File discovery completed": "Objavovanie súborov dokončené", + "File extraction completed": "Extrakcia súboru dokončená", + "File extraction queued": "Extrakcia súboru zaradená do frontu", + "File indexed successfully": "Súbor bol úspešne indexovaný", + "File is already anonymized": "Súbor je už anonymizovaný", + "File name": "Názov súboru", + "File not found": "Súbor sa nenašiel", + "File settings saved successfully": "Nastavenia súboru boli úspešne uložené", + "File settings updated successfully": "Nastavenia súboru boli úspešne aktualizované", + "File sources": "Zdroje súborov", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vektorizácia súborov dokončená! {vectorized} úsekov vektorizovaných z {files} súborov. {failed} zlyhalo.", + "File vectorization configuration saved successfully": "Konfigurácia vektorizácie súborov bola úspešne uložená", + "File vectorization started. Check the statistics section for progress.": "Vektorizácia súborov spustená. Priebeh skontrolujte v sekcii štatistík.", + "File warmup completed": "Zahriatie súborov dokončené", + "Files": "Súbory", + "Files added successfully": "Súbory boli úspešne pridané", + "Files with Completed Extraction:": "Súbory s dokončenou extrakciou:", + "Files → fileCollection, Objects → objectCollection": "Súbory → fileCollection, Objekty → objectCollection", + "Files → filecollection, objects → objectcollection": "Súbory → filecollection, objekty → objectcollection", + "Filter Audit Trails": "Filtrovať audítne záznamy", + "Filter Deleted Items": "Filtrovať odstránené položky", + "Filter Objects": "Filtrovať objekty", + "Filter Properties": "Filtrovať vlastnosti", + "Filter Search Trails": "Filtrovať záznamy vyhľadávania", + "Filter Statistics": "Filtrovať štatistiky", + "Filter and analyze search trail entries": "Filtrujte a analyzujte záznamy vyhľadávania", + "Filter and manage audit trail entries": "Filtrujte a spravujte záznamy audítneho záznamu", + "Filter and manage soft deleted items": "Filtrujte a spravujte mäkko odstránené položky", + "Filter and search entities": "Filtrujte a vyhľadávajte entity", + "Filter by object ID": "Filtrovať podľa ID objektu", + "Filter by search term": "Filtrovať podľa hľadaného výrazu", + "Filter by type": "Filtrovať podľa typu", + "Filter by webhook": "Filtrovať podľa webhooku", + "Filter data loaded automatically. Use the filters below to refine your search.": "Údaje filtra boli načítané automaticky. Na spresnenie vyhľadávania použite filtre nižšie.", + "Filter fields...": "Filtrovať polia...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrujte spúšťače webhooku podľa vlastností payloadu (jedna na riadok, formát: key: value)", + "Filtered": "Filtrované", + "Filters": "Filtre", + "Finalizing setup...": "Dokončuje sa nastavenie...", + "Fireworks AI Chat Settings": "Nastavenia chatu Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfigurácia vloženia Fireworks AI", + "First": "Prvé", + "Fixed": "Opravené", + "For chat and retrieval-augmented generation": "Pre chat a generovanie rozšírené o získavanie", + "For vector embeddings and semantic search": "Pre vektorové vloženia a sémantické vyhľadávanie", + "Format": "Formát", + "Format Issue": "Problém formátu", + "Format constraint is missing": "Chýba obmedzenie formátu", + "From (e.g. -7 days or 2025-01-01)": "Od (napr. -7 days alebo 2025-01-01)", + "From Date": "Od dátumu", + "From date": "Od dátumu", + "Full data copied to clipboard": "Všetky údaje skopírované do schránky", + "Function Tools": "Funkčné nástroje", + "General Issue": "Všeobecný problém", + "Generate recommendations and confidence scores": "Generovať odporúčania a skóre spoľahlivosti", + "Generate report": "Generovať report", + "Generate the verantwoordingsdocument": "Generovať verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generovať vektory okamžite pri vytvorení nových objektov", + "Generated: {time}": "Vygenerované: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Získajte prehľad o stave údajov vašej organizácie v reálnom čase so zameraním na registre, definície schém a ukladanie a využitie objektov.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Získajte prehľad o stave údajov vašej organizácie, trendoch rastu a metrikách kvality v reálnom čase.", + "GitHub Personal Access Token": "Osobný prístupový token GitHub", + "GitHub token is valid": "GitHub token je platný", + "GitHub token is valid! Username: {username}": "GitHub token je platný! Používateľské meno: {username}", + "GitHub token removed successfully": "GitHub token bol úspešne odstránený", + "GitHub token saved successfully": "GitHub token bol úspešne uložený", + "GitHub token test failed: {message}": "Test GitHub tokenu zlyhal: {message}", + "GitLab Personal Access Token": "Osobný prístupový token GitLab", + "GitLab URL saved successfully": "GitLab URL bola úspešne uložená", + "GitLab token is valid": "GitLab token je platný", + "GitLab token is valid! Username: {username}": "GitLab token je platný! Používateľské meno: {username}", + "GitLab token saved successfully": "GitLab token bol úspešne uložený", + "GitLab token test failed: {message}": "Test GitLab tokenu zlyhal: {message}", + "Go to linked Register": "Prejsť na prepojený Register", + "Go to linked Schema": "Prejsť na prepojenú Schému", + "Go to organisation": "Prejsť na organizáciu", + "Grant additional permissions beyond standard CRUD operations": "Udeliť ďalšie oprávnenia nad rámec štandardných operácií CRUD", + "Group Quota": "Kvóta skupiny", + "Groups": "Skupiny", + "HTML preview": "Náhľad HTML", + "HTTP Method": "HTTP metóda", + "HTTP method used to send webhook requests": "HTTP metóda použitá na odosielanie požiadaviek webhooku", + "Headers": "Hlavičky", + "Health": "Stav", + "Heartbeat successful - connection kept alive": "Heartbeat úspešný – pripojenie udržané pri živote", + "Helpful": "Užitočné", + "Here you can configure the details for various connections.": "Tu môžete nakonfigurovať podrobnosti pre rôzne pripojenia.", + "Here you can configure the details for your organisation.": "Tu môžete nakonfigurovať podrobnosti pre vašu organizáciu.", + "Hide": "Skryť", + "Hide Filters": "Skryť filtre", + "Hide in collection view": "Skryť v zobrazení kolekcie", + "Hide in forms": "Skryť vo formulároch", + "Hide in list view": "Skryť v zozname", + "High": "Vysoká", + "High Confidence": "Vysoká spoľahlivosť", + "Host": "Hostiteľ", + "Hourly": "Hodinovo", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Ako hlboko prechádzať vnorené vlastnosti objektu (1 – 20). Vyššie hodnoty zachytávajú viac podrobností, ale zväčšujú veľkosť vektora.", + "How often to check for updates (1-168 hours)": "Ako často kontrolovať aktualizácie (1 – 168 hodín)", + "How to handle retries for failed webhook deliveries": "Ako spracovať opakovania pre neúspešné doručenia webhooku", + "IBANs": "IBANy", + "ID": "ID", + "ID of input mapping (optional)": "ID vstupného mapovania (voliteľné)", + "ID of output mapping (optional)": "ID výstupného mapovania (voliteľné)", + "ID of the target resource": "ID cieľového zdroja", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Limity rýchlosti IP boli úspešne vymazané", + "Identify properties not in the schema": "Identifikovať vlastnosti, ktoré nie sú v schéme", + "Immutable": "Nemenné", + "Import": "Importovať", + "Import Configuration": "Importovať konfiguráciu", + "Import Data into Register": "Importovať údaje do registra", + "Import successful": "Import úspešný", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importujte šablónu report-bundle.json, aby ste získali register `reports`, potom vytvorte svoju prvú nástenku cez štandardné používateľské rozhranie objektov. Nástenky deklarujú svoje widgety v JSON a renderer každému widgetu dodáva živé agregačné údaje.", + "Improved Property": "Zlepšená vlastnosť", + "In use": "Používa sa", + "Inactive": "Neaktívne", + "Inactive organisations cannot be used": "Neaktívne organizácie nie je možné použiť", + "Include IDs and names of related objects for better contextual search": "Zahrnúť ID a názvy súvisiacich objektov pre lepšie kontextové vyhľadávanie", + "Include ids and names of related objects for better contextual search": "Zahrnúť ID a názvy súvisiacich objektov pre lepšie kontextové vyhľadávanie", + "Include related object references": "Zahrnúť odkazy na súvisiace objekty", + "Include schema and register metadata": "Zahrnúť metaúdaje schémy a registra", + "Index": "Index", + "Information about the current OpenRegister installation": "Informácie o aktuálnej inštalácii OpenRegister", + "Inherited from register": "Zdedené z registra", + "Inherits permissions from register": "Dedí oprávnenia z registra", + "Initialization failed": "Inicializácia zlyhala", + "Initialize Project": "Inicializovať projekt", + "Initializing SOLR setup...": "Inicializuje sa nastavenie SOLR...", + "Input Mapping": "Vstupné mapovanie", + "Inspect Fields": "Skontrolovať polia", + "Inspect Index": "Skontrolovať index", + "Inspect SOLR Index": "Skontrolovať SOLR index", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Nainštalujte aplikáciu notify_push z obchodu Nextcloud App Store na povolenie aktualizácií v reálnom čase.", + "Integrations": "Integrácie", + "Internet location (URL) of this installation": "Internetové umiestnenie (URL) tejto inštalácie", + "Interval": "Interval", + "Invalid": "Neplatné", + "Invalid batch size. Must be between 1 and 5000": "Neplatná veľkosť dávky. Musí byť medzi 1 a 5000", + "Invalid feedback type": "Neplatný typ spätnej väzby", + "Invalid field name provided": "Poskytnutý neplatný názov poľa", + "Invalid maxObjects. Must be 0 (all) or positive number": "Neplatný maxObjects. Musí byť 0 (všetko) alebo kladné číslo", + "Invalid:": "Neplatné:", + "Invite Users": "Pozvať používateľov", + "Invited Users": "Pozvaní používatelia", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Výsledky inzage", + "Issue": "Problém", + "Items per page:": "Položiek na stránku:", + "Just now": "Práve teraz", + "Key": "Kľúč", + "LLM Configuration": "Konfigurácia LLM", + "LLM actions menu": "Ponuka akcií LLM", + "LLM configuration saved successfully": "Konfigurácia LLM bola úspešne uložená", + "LLM features disabled": "Funkcie LLM zakázané", + "LLM features enabled": "Funkcie LLM povolené", + "LLM must be enabled with an embedding provider configured": "LLM musí byť povolené s nakonfigurovaným poskytovateľom vloženia", + "LLM settings saved successfully": "Nastavenia LLM boli úspešne uložené", + "LLM settings updated successfully": "Nastavenia LLM boli úspešne aktualizované", + "Label": "Štítok", + "Labels": "Štítky", + "Languages": "Jazyky", + "Last": "Posledné", + "Last Triggered": "Naposledy spustené", + "Leave empty to allow all users access": "Ponechajte prázdne na povolenie prístupu všetkým používateľom", + "Leave off for auto-detection from property format.": "Ponechajte vypnuté na automatické zistenie z formátu vlastnosti.", + "Left organisation successfully": "Organizácia bola úspešne opustená", + "Legal basis *": "Právny základ *", + "Length Range:": "Rozsah dĺžky:", + "Linear": "Lineárne", + "Link": "Odkaz", + "Link an email from the Mail app sidebar to associate it with this object.": "Prepojte e-mail z bočného panela aplikácie Mail, aby ste ho priradili k tomuto objektu.", + "Link an object to see it here.": "Prepojte objekt, aby ste ho tu videli.", + "Link another object": "Prepojiť ďalší objekt", + "Link existing event": "Prepojiť existujúcu udalosť", + "Link removed": "Prepojenie odstránené", + "Link to Object": "Prepojiť s objektom", + "Link to {name}": "Prepojiť s {name}", + "Linked Objects": "Prepojené objekty", + "Linked by {user}": "Prepojené používateľom {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Prepojené e-maily, udalosti kalendára, kontakty a karty Deck sa zobrazia tu.", + "Linked to {name}": "Prepojené s {name}", + "Load Advanced Filters": "Načítať pokročilé filtre", + "Load advanced filters with live data from your search index": "Načítať pokročilé filtre so živými údajmi z vášho indexu vyhľadávania", + "Load more": "Načítať viac", + "Load view": "Načítať zobrazenie", + "Loading API tokens...": "Načítavajú sa API tokeny...", + "Loading ConfigSets...": "Načítavajú sa ConfigSety...", + "Loading LLM configuration...": "Načítava sa konfigurácia LLM...", + "Loading RBAC settings...": "Načítavajú sa nastavenia RBAC...", + "Loading activity...": "Načítava sa aktivita...", + "Loading advanced filters...": "Načítavajú sa pokročilé filtre...", + "Loading agents...": "Načítavajú sa agenti...", + "Loading application details...": "Načítavajú sa podrobnosti aplikácie...", + "Loading applications...": "Načítavajú sa aplikácie...", + "Loading archived conversations...": "Načítavajú sa archivované konverzácie...", + "Loading audit trails...": "Načítavajú sa audítne záznamy...", + "Loading available tools...": "Načítavajú sa dostupné nástroje...", + "Loading cache statistics...": "Načítavajú sa štatistiky vyrovnávacej pamäte...", + "Loading collections...": "Načítavajú sa kolekcie...", + "Loading configurations...": "Načítavajú sa konfigurácie...", + "Loading conversation...": "Načítava sa konverzácia...", + "Loading conversations...": "Načítavajú sa konverzácie...", + "Loading deleted items...": "Načítavajú sa odstránené položky...", + "Loading endpoints": "Načítavajú sa koncové body", + "Loading entities...": "Načítavajú sa entity...", + "Loading events...": "Načítavajú sa udalosti...", + "Loading filter data...": "Načítavajú sa údaje filtra...", + "Loading groups...": "Načítavajú sa skupiny...", + "Loading linked objects...": "Načítavajú sa prepojené objekty...", + "Loading log details...": "Načítavajú sa podrobnosti protokolu...", + "Loading multitenancy settings...": "Načítavajú sa nastavenia viacnásobného nájmu...", + "Loading n8n configuration...": "Načítava sa konfigurácia n8n...", + "Loading permission matrix...": "Načítava sa matica oprávnení...", + "Loading preferences...": "Načítavajú sa predvoľby...", + "Loading register data...": "Načítavajú sa údaje registra...", + "Loading registers...": "Načítavajú sa registre...", + "Loading retention settings...": "Načítavajú sa nastavenia uchovávania...", + "Loading schemas...": "Načítavajú sa schémy...", + "Loading search configuration...": "Načítava sa konfigurácia vyhľadávania...", + "Loading search trails...": "Načítavajú sa záznamy vyhľadávania...", + "Loading sources...": "Načítavajú sa zdroje...", + "Loading statistics...": "Načítavajú sa štatistiky...", + "Loading text extraction settings...": "Načítavajú sa nastavenia extrakcie textu...", + "Loading tokens...": "Načítavajú sa tokeny...", + "Loading user groups...": "Načítavajú sa skupiny používateľov...", + "Loading users...": "Načítavajú sa používatelia...", + "Loading views...": "Načítavajú sa zobrazenia...", + "Loading...": "Načítava sa...", + "Local": "Lokálne", + "Local Version": "Lokálna verzia", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Vyhľadajte každý objekt odkazujúci na dotknutú osobu (Art 15 inzage), zobrazte náhľad vymazania (Art 17 vergetelheid) alebo exportujte jej údaje (Art 20 portabiliteit).", + "Location": "Umiestnenie", + "Location Field": "Pole umiestnenia", + "Locations": "Umiestnenia", + "Locked": "Uzamknuté", + "Locked:": "Uzamknuté:", + "Login successful": "Prihlásenie úspešné", + "Logs": "Protokoly", + "Low": "Nízka", + "Low Confidence": "Nízka spoľahlivosť", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Nižšie čísla sa zobrazujú ako prvé v bočnom paneli filtrov. Ponechajte prázdne pre automatické zoradenie.", + "Lucene Version:": "Verzia Lucene:", + "Magic table sync completed for {schema}": "Synchronizácia magickej tabuľky dokončená pre {schema}", + "Magic table synchronized successfully": "Magická tabuľka bola úspešne synchronizovaná", + "Mail Integration": "Integrácia Mail", + "Mail integration is not available": "Integrácia Mail nie je dostupná", + "Make this installation discoverable within the federation network.": "Sprístupniť túto inštaláciu na objavenie v rámci federačnej siete.", + "Manage Organisation Roles": "Spravovať roly organizácie", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Spravujte SOLR kolekcie (úložiská údajov) a priraďte ich pre objekty a súbory.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Spravujte SOLR ConfigSety (konfiguračné šablóny) pre vaše kolekcie.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Spravujte SOLR kolekcie (úložiská údajov) a priraďte ich pre objekty a súbory.", + "Manage SOLR configsets (configuration templates) for your collections.": "Spravujte SOLR ConfigSety (konfiguračné šablóny) pre vaše kolekcie.", + "Manage and configure agents for automated tasks": "Spravujte a konfigurujte agentov pre automatizované úlohy", + "Manage and monitor file text extraction status": "Spravujte a monitorujte stav extrakcie textu zo súborov", + "Manage and restore soft deleted items from your registers": "Spravujte a obnovujte mäkko odstránené položky z vašich registrov", + "Manage and view detected entities from files and objects": "Spravujte a zobrazte zistené entity zo súborov a objektov", + "Manage document templates and themes": "Spravujte šablóny dokumentov a témy", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Spravujte pracovné postupy n8n pre automatizáciu OpenRegister. Pracovné postupy sa uložia v nakonfigurovanom projekte.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Spravujte verwerkingsactiviteit, spúšťajte žiadosti o prístup dotknutých osôb a auditujte súlad podľa EU GDPR / holandského AVG.", + "Manage webhooks for event-driven integrations": "Spravujte webhooky pre integrácie riadené udalosťami", + "Manage your account settings, security, and personal data.": "Spravujte nastavenia svojho účtu, zabezpečenie a osobné údaje.", + "Manage your applications and modules": "Spravujte svoje aplikácie a moduly", + "Manage your chat conversations": "Spravujte svoje chatové konverzácie", + "Manage your data registers and their configurations": "Spravujte svoje dátové registre a ich konfigurácie", + "Manage your data schemas and their properties": "Spravujte svoje dátové schémy a ich vlastnosti", + "Manage your data sources and their configurations": "Spravujte svoje zdroje údajov a ich konfigurácie", + "Manage your organisations and switch between them": "Spravujte svoje organizácie a prepínajte medzi nimi", + "Manage your saved search configurations": "Spravujte svoje uložené konfigurácie vyhľadávania", + "Manage your system configurations and settings": "Spravujte svoje systémové konfigurácie a nastavenia", + "Managed": "Spravované", + "Mappings": "Mapovania", + "Mass validation completed successfully": "Hromadné overenie bolo úspešne dokončené", + "Mass validation failed: {error}": "Hromadné overenie zlyhalo: {error}", + "Max Files (0 = all)": "Max. súborov (0 = všetky)", + "Max Length": "Max. dĺžka", + "Max Length:": "Max. dĺžka:", + "Max Objects (0 = all)": "Max. objektov (0 = všetky)", + "Max Retries": "Max. opakovaní", + "Max Shards/Node": "Max. shardov/uzol", + "Max Tokens": "Max. tokenov", + "Max execution time (ms)": "Max. čas vykonania (ms)", + "Max ms": "Max. ms", + "Max result count": "Max. počet výsledkov", + "Max results": "Max. výsledkov", + "Maximum": "Maximum", + "Maximum File Size (MB)": "Maximálna veľkosť súboru (MB)", + "Maximum Nesting Depth": "Maximálna hĺbka vnorenia", + "Maximum length": "Maximálna dĺžka", + "Maximum length constraint is missing": "Chýba obmedzenie maximálnej dĺžky", + "Maximum length is too restrictive": "Maximálna dĺžka je príliš obmedzujúca", + "Maximum number of files to process. Set to 0 to process all files.": "Maximálny počet súborov na spracovanie. Nastavte na 0 na spracovanie všetkých súborov.", + "Maximum number of items": "Maximálny počet položiek", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maximálny počet objektov na spracovanie. Nastavte na 0 na spracovanie všetkých objektov.", + "Maximum number of retry attempts for failed deliveries": "Maximálny počet pokusov o opakovanie pre neúspešné doručenia", + "Maximum time to wait before committing changes": "Maximálny čas čakania pred potvrdením zmien", + "Maximum tokens to generate": "Maximálny počet tokenov na generovanie", + "Maximum value": "Maximálna hodnota", + "Maximum value constraint is missing": "Chýba obmedzenie maximálnej hodnoty", + "Maximum value is too restrictive": "Maximálna hodnota je príliš obmedzujúca", + "Medium": "Stredná", + "Medium Confidence": "Stredná spoľahlivosť", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Stredné dopyty: Vyhľadávania s určitým filtrovaním alebo viacerými parametrami (napr. rozsahy dátumov, konkrétne registre/schémy)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Stredné dopyty: vyhľadávania s určitým filtrovaním alebo viacerými parametrami (napr. rozsahy dátumov, konkrétne registre/schémy)", + "Member removed successfully": "Člen bol úspešne odstránený", + "Members": "Členovia", + "Members ({count})": "Členovia ({count})", + "Members of selected groups can access this view": "Členovia vybraných skupín majú prístup k tomuto zobrazeniu", + "Members:": "Členovia:", + "Memory prediction calculated successfully": "Predikcia pamäte bola úspešne vypočítaná", + "Message content is required": "Obsah správy je povinný", + "Message does not belong to this conversation": "Správa nepatrí do tejto konverzácie", + "Message not found": "Správa sa nenašla", + "Metadata": "Metaúdaje", + "Metadata Filters": "Filtre metaúdajov", + "Method": "Metóda", + "Method*": "Metóda*", + "Method: {method}": "Metóda: {method}", + "Min Length": "Min. dĺžka", + "Min execution time (ms)": "Min. čas vykonania (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. počet výsledkov", + "Min results": "Min. výsledkov", + "Minimum": "Minimum", + "Minimum length": "Minimálna dĺžka", + "Minimum number of items": "Minimálny počet položiek", + "Minimum value": "Minimálna hodnota", + "Minimum value constraint is missing": "Chýba obmedzenie minimálnej hodnoty", + "Minimum value is too restrictive": "Minimálna hodnota je príliš obmedzujúca", + "Missing conversation or agentUuid": "Chýba konverzácia alebo agentUuid", + "Missing conversationId": "Chýba conversationId", + "Missing message": "Chýba správa", + "Missing object fields created successfully": "Chýbajúce polia objektu boli úspešne vytvorené", + "Mode:": "Režim:", + "Model": "Model", + "Model Name": "Názov modelu", + "Modified": "Upravené", + "Monthly": "Mesačne", + "More sources (10-20):": "Viac zdrojov (10 – 20):", + "Most Active Objects": "Najaktívnejšie objekty", + "Multiple Of": "Násobok", + "Multiple of": "Násobok", + "Multiple search trail deletion not implemented yet": "Odstránenie viacerých záznamov vyhľadávania ešte nie je implementované", + "Multitenancy": "Viacnásobný nájom", + "Multitenancy settings updated successfully": "Nastavenia viacnásobného nájmu boli úspešne aktualizované", + "My Account": "Môj účet", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Názov", + "Name *": "Názov *", + "Name*": "Názov*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Vyrovnávacia pamäť názvov bola úspešne zahriata: {count} názvov načítaných za {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Vyrovnávacia pamäť názvov bola úspešne zahriata: {count} názvov načítaných za {time}. {message}", + "Names cache warmup completed": "Zahriatie vyrovnávacej pamäte názvov dokončené", + "Naming pattern for copies:": "Vzor pomenovania pre kópie:", + "New Collection Name": "Názov novej kolekcie", + "New Configuration": "Nová konfigurácia", + "New Conversation": "Nová konverzácia", + "New Properties": "Nové vlastnosti", + "New Property": "Nová vlastnosť", + "New User Group": "Nová skupina používateľov", + "New Value": "Nová hodnota", + "New activity": "Nová aktivita", + "New password": "Nové heslo", + "New processing activity": "Nová verwerkingsactiviteit", + "New users without specific organisation membership will be automatically added to this organisation": "Noví používatelia bez konkrétneho členstva v organizácii budú automaticky pridaní do tejto organizácie", + "New verwerkingsactiviteit": "Nová verwerkingsactiviteit", + "Next": "Ďalej", + "Nextcloud Groups": "Skupiny Nextcloud", + "No": "Nie", + "No API tokens.": "Žiadne API tokeny.", + "No ConfigSets found": "Nenašli sa žiadne ConfigSety", + "No Deck cards linked to this object": "K tomuto objektu nie sú prepojené žiadne karty Deck", + "No GitHub token configured": "Nie je nakonfigurovaný žiadny GitHub token", + "No GitHub token provided": "Nebol poskytnutý žiadny GitHub token", + "No GitLab token provided": "Nebol poskytnutý žiadny GitLab token", + "No Organisation": "Žiadna organizácia", + "No active filters": "Žiadne aktívne filtre", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Žiadne aktívne objekty na mäkké odstránenie pre schému {schema}. Na odstránenie mäkko odstránených objektov použite „Trvalo odstrániť“.", + "No activity data available": "Nie sú dostupné žiadne údaje o aktivite", + "No activity found.": "Nenašla sa žiadna aktivita.", + "No agents available": "Nie sú dostupní žiadni agenti", + "No agents found": "Nenašli sa žiadni agenti", + "No app store cache exists yet": "Zatiaľ neexistuje žiadna vyrovnávacia pamäť obchodu s aplikáciami", + "No applications are available.": "Nie sú dostupné žiadne aplikácie.", + "No applications found": "Nenašli sa žiadne aplikácie", + "No approval steps for this object.": "Pre tento objekt nie sú žiadne kroky schvaľovania.", + "No archived conversations": "Žiadne archivované konverzácie", + "No audit trail entries found": "Nenašli sa žiadne záznamy audítneho záznamu", + "No changes recorded": "Nezaznamenané žiadne zmeny", + "No chunks to vectorize": "Žiadne úseky na vektorizáciu", + "No collections found": "Nenašli sa žiadne kolekcie", + "No configuration data": "Žiadne konfiguračné údaje", + "No configuration selected": "Nie je vybraná žiadna konfigurácia", + "No configurations are available.": "Nie sú dostupné žiadne konfigurácie.", + "No configurations found": "Nenašli sa žiadne konfigurácie", + "No configurations found for this application.": "Pre túto aplikáciu sa nenašli žiadne konfigurácie.", + "No contacts linked to this object": "K tomuto objektu nie sú prepojené žiadne kontakty", + "No conversations yet. Create a new one to get started!": "Zatiaľ žiadne konverzácie. Vytvorte novú a začnite!", + "No dashboards yet": "Zatiaľ žiadne nástenky", + "No data available": "Nie sú dostupné žiadne údaje", + "No data available for chart": "Nie sú dostupné žiadne údaje pre graf", + "No deleted items found": "Nenašli sa žiadne odstránené položky", + "No deletion data available": "Nie sú dostupné žiadne údaje o odstránení", + "No description found": "Nenašiel sa žiadny popis", + "No description provided": "Nebol poskytnutý žiadny popis", + "No emails linked to this object": "K tomuto objektu nie sú prepojené žiadne e-maily", + "No endpoints defined": "Nie sú definované žiadne koncové body", + "No entities detected for this email.": "Pre tento e-mail neboli zistené žiadne entity.", + "No entities detected in this file. Run text extraction first.": "V tomto súbore neboli zistené žiadne entity. Najprv spustite extrakciu textu.", + "No entities found": "Nenašli sa žiadne entity", + "No entities have been detected yet": "Zatiaľ neboli zistené žiadne entity", + "No events found": "Nenašli sa žiadne udalosti", + "No events linked to this object": "K tomuto objektu nie sú prepojené žiadne udalosti", + "No expired audit trails found to clear": "Nenašli sa žiadne uplynuté audítne záznamy na vymazanie", + "No expired search trails found to clear": "Nenašli sa žiadne uplynuté záznamy vyhľadávania na vymazanie", + "No extraction data available for this file": "Pre tento súbor nie sú dostupné žiadne údaje extrakcie", + "No facetable fields available. Select a register and schema to see available filters.": "Nie sú dostupné žiadne fasetovateľné polia. Vyberte register a schému, aby ste videli dostupné filtre.", + "No files found": "Nenašli sa žiadne súbory", + "No files have been extracted yet": "Zatiaľ neboli extrahované žiadne súbory", + "No files selected": "Nie sú vybrané žiadne súbory", + "No files to process": "Žiadne súbory na spracovanie", + "No files to reindex": "Žiadne súbory na preindexovanie", + "No filters are currently active. This will delete ALL audit trail entries!": "Momentálne nie sú aktívne žiadne filtre. Týmto sa odstránia VŠETKY záznamy audítneho záznamu!", + "No groups found. Try a different search.": "Nenašli sa žiadne skupiny. Skúste iné vyhľadávanie.", + "No label": "Žiadny štítok", + "No labels": "Žiadne štítky", + "No linked objects": "Žiadne prepojené objekty", + "No log entries found": "Nenašli sa žiadne záznamy protokolu", + "No logs are available for this configuration.": "Pre túto konfiguráciu nie sú dostupné žiadne protokoly.", + "No logs are available for this source.": "Pre tento zdroj nie sú dostupné žiadne protokoly.", + "No logs found": "Nenašli sa žiadne protokoly", + "No matches": "Žiadne zhody", + "No mismatched fields found - SOLR schema is properly configured": "Nenašli sa žiadne nezhodné polia – SOLR schéma je správne nakonfigurovaná", + "No objects are currently selected for deletion.": "Momentálne nie sú vybrané žiadne objekty na odstránenie.", + "No objects are currently selected for permanent deletion.": "Momentálne nie sú vybrané žiadne objekty na trvalé odstránenie.", + "No objects are currently selected for restoration.": "Momentálne nie sú vybrané žiadne objekty na obnovenie.", + "No objects found": "Nenašli sa žiadne objekty", + "No objects found to vectorize": "Nenašli sa žiadne objekty na vektorizáciu", + "No objects linked to this email": "K tomuto e-mailu nie sú prepojené žiadne objekty", + "No objects selected": "Nie sú vybrané žiadne objekty", + "No objects selected for deletion": "Na odstránenie neboli vybrané žiadne objekty", + "No objects selected for restoration": "Na obnovenie neboli vybrané žiadne objekty", + "No objects to delete": "Žiadne objekty na odstránenie", + "No objects to delete for schema {schema}": "Žiadne objekty na odstránenie pre schému {schema}", + "No objects were permanently deleted": "Žiadne objekty neboli trvalo odstránené", + "No organisations found": "Nenašli sa žiadne organizácie", + "No personal data was found for this subject identifier.": "Pre tento identifikátor subjektu sa nenašli žiadne osobné údaje.", + "No processing activities yet": "Zatiaľ žiadne verwerkingsactiviteit", + "No properties found": "Nenašli sa žiadne vlastnosti", + "No properties match your filters.": "Žiadne vlastnosti nezodpovedajú vašim filtrom.", + "No purge date set": "Nie je nastavený žiadny dátum vyčistenia", + "No register data available": "Nie sú dostupné žiadne údaje registra", + "No register objects reference this file": "Tento súbor neodkazuje žiadne objekty registra", + "No registers found": "Nenašli sa žiadne registre", + "No registers found for this application.": "Pre túto aplikáciu sa nenašli žiadne registre.", + "No registers found. Create a register to configure permissions.": "Nenašli sa žiadne registre. Vytvorte register na konfiguráciu oprávnení.", + "No related cases found for this sender": "Pre tohto odosielateľa sa nenašli žiadne súvisiace prípady", + "No related entities": "Žiadne súvisiace entity", + "No relations found": "Nenašli sa žiadne vzťahy", + "No request body available": "Nie je dostupné žiadne telo požiadavky", + "No response body available": "Nie je dostupné žiadne telo odpovede", + "No saved views yet. Create one in the Search tab!": "Zatiaľ žiadne uložené zobrazenia. Vytvorte jedno na karte Vyhľadávanie!", + "No saved views yet. create one in the search tab!": "Zatiaľ žiadne uložené zobrazenia. Vytvorte jedno na karte vyhľadávania!", + "No scan run yet": "Zatiaľ nebol spustený žiadny sken", + "No schema data available": "Nie sú dostupné žiadne údaje schémy", + "No schema selected for exploration": "Na preskúmanie nebola vybraná žiadna schéma", + "No schemas configured for mail linking.": "Pre prepojenie pošty nie sú nakonfigurované žiadne schémy.", + "No schemas found": "Nenašli sa žiadne schémy", + "No schemas found for this application.": "Pre túto aplikáciu sa nenašli žiadne schémy.", + "No search terms data available": "Nie sú dostupné žiadne údaje o hľadaných výrazoch", + "No search trail entries found": "Nenašli sa žiadne záznamy vyhľadávania", + "No sources are available.": "Nie sú dostupné žiadne zdroje.", + "No sources found": "Nenašli sa žiadne zdroje", + "No synchronizations found": "Nenašli sa žiadne synchronizácie", + "No templates found": "Nenašli sa žiadne šablóny", + "No templates have been created yet": "Zatiaľ neboli vytvorené žiadne šablóny", + "No tools available. Tools can be registered by installed apps.": "Nie sú dostupné žiadne nástroje. Nástroje môžu registrovať nainštalované aplikácie.", + "No users in this organisation.": "Žiadni používatelia v tejto organizácii.", + "No valid magic-mapped register+schema combinations found": "Nenašli sa žiadne platné magicky mapované kombinácie register+schéma", + "No views found. Create views first before configuring vectorization.": "Nenašli sa žiadne zobrazenia. Pred konfiguráciou vektorizácie najprv vytvorte zobrazenia.", + "No views match your search": "Žiadne zobrazenia nezodpovedajú vášmu vyhľadávaniu", + "No webhooks found": "Nenašli sa žiadne webhooky", + "No webhooks have been configured yet": "Zatiaľ neboli nakonfigurované žiadne webhooky", + "No workflows found in this project. Create workflows in the n8n editor.": "V tomto projekte sa nenašli žiadne pracovné postupy. Vytvorte pracovné postupy v editore n8n.", + "None": "Žiadne", + "Not Found": "Nenájdené", + "Not extracted": "Neextrahované", + "Not helpful": "Neužitočné", + "Not in use": "Nepoužíva sa", + "Note:": "Poznámka:", + "Notifications": "Notifikácie", + "Number Constraints": "Číselné obmedzenia", + "Number of Sources": "Počet zdrojov", + "Number of chunks to vectorize in one API call (1-100).": "Počet úsekov na vektorizáciu v jednom API volaní (1 – 100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Počet úsekov na vektorizáciu v jednom API volaní. Vyššie = rýchlejšie, ale viac pamäte. Odporúčané: 10 – 50.", + "Number of objects to process in each batch (1-5000).": "Počet objektov na spracovanie v každej dávke (1 – 5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Počet objektov na vektorizáciu v jednom API volaní. Vyššie = rýchlejšie, ale viac pamäte. Odporúčané: 10 – 50.", + "Numeric Range:": "Číselný rozsah:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Kolekcia objektov", + "Object Configuration:": "Konfigurácia objektu:", + "Object Count Prediction": "Predikcia počtu objektov", + "Object Extraction Mode": "Režim extrakcie objektov", + "Object ID": "ID objektu", + "Object Management": "Správa objektov", + "Object Text Extraction": "Extrakcia textu z objektov", + "Object Vectorization": "Vektorizácia objektov", + "Object Warmup": "Zahriatie objektov", + "Object changes in owned objects": "Zmeny objektov vo vlastnených objektoch", + "Object collection not configured": "Kolekcia objektov nie je nakonfigurovaná", + "Object metadata incomplete for file upload": "Metaúdaje objektu sú neúplné pre nahranie súboru", + "Object not found": "Objekt sa nenašiel", + "Object permanently deleted": "Objekt trvalo odstránený", + "Object restored successfully": "Objekt bol úspešne obnovený", + "Object settings updated successfully": "Nastavenia objektu boli úspešne aktualizované", + "Object sources": "Zdroje objektov", + "Object successfully deleted": "Objekt bol úspešne odstránený", + "Object successfully modified": "Objekt bol úspešne upravený", + "Object unlocked successfully": "Objekt bol úspešne odomknutý", + "Object vectorization configuration saved successfully": "Konfigurácia vektorizácie objektov bola úspešne uložená", + "Object vectorized successfully": "Objekt bol úspešne vektorizovaný", + "Object:": "Objekt:", + "Objects": "Objekty", + "Objects Analyzed": "Analyzované objekty", + "Objects Distribution": "Rozloženie objektov", + "Objects being analyzed": "Analyzujú sa objekty", + "Objects by Register": "Objekty podľa registra", + "Objects by Schema": "Objekty podľa schémy", + "Objects deletion completed for {schema}": "Odstránenie objektov dokončené pre {schema}", + "Objects deletion completed successfully": "Odstránenie objektov bolo úspešne dokončené", + "Objects in schema": "Objekty v schéme", + "Objects successfully deleted": "Objekty boli úspešne odstránené", + "Objects to Process:": "Objekty na spracovanie:", + "Objects to be analyzed": "Objekty na analýzu", + "Objects to be deleted": "Objekty na odstránenie", + "Objects to be validated": "Objekty na overenie", + "Objects will be serialized as JSON text before vectorization": "Objekty budú pred vektorizáciou serializované ako JSON text", + "Objects will be soft deleted and moved to the": "Objekty budú mäkko odstránené a presunuté do", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekty budú mäkko odstránené (označené ako odstránené, ale ponechané v databáze). V prípade potreby ich možno neskôr obnoviť.", + "Old Value": "Stará hodnota", + "Oldest Item (days)": "Najstaršia položka (dni)", + "Ollama Chat Settings": "Nastavenia chatu Ollama", + "Ollama Embedding Configuration": "Konfigurácia vloženia Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Len so zmenami", + "Only members of selected groups can access this organisation": "Len členovia vybraných skupín majú prístup k tejto organizácii", + "Open Nextcloud App Store": "Otvoriť Nextcloud App Store", + "Open OpenRegister sidebar": "Otvoriť bočný panel OpenRegister", + "Open Register Objects": "Objekty Open Register", + "Open folder": "Otvoriť priečinok", + "Open in OpenRegister": "Otvoriť v OpenRegister", + "Open n8n Editor": "Otvoriť editor n8n", + "OpenAI Chat Settings": "Nastavenia chatu OpenAI", + "OpenAI Embedding Configuration": "Konfigurácia vloženia OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Nastavenia OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Nástenky a naplánované reporty definované operátorom. Každá nástenka je plnohodnotný objekt v registri `reports`; widgety sú deklarované v poli `widgets` nástenky a vykresľované naživo z agregácií / GraphQL.", + "Optimizing search performance...": "Optimalizuje sa výkon vyhľadávania...", + "Optional URL-friendly identifier": "Voliteľný identifikátor vhodný pre URL", + "Optional description for this facet": "Voliteľný popis pre túto fasetu", + "Optional end date property": "Voliteľná vlastnosť dátumu ukončenia", + "Optional event description template": "Voliteľná šablóna popisu udalosti", + "Optional location property": "Voliteľná vlastnosť umiestnenia", + "Optional webhook secret for signature verification": "Voliteľný tajný kľúč webhooku na overenie podpisu", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Voliteľné: Pridajte parametre dopytu na filtrovanie odkazovanej schémy (napr. status=active&type=public)", + "Or": "Alebo", + "Order": "Poradie", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Zoradené jazykové značky BCP 47. Prvý jazyk je predvolený pre register a riadi záložné riešenie Accept-Language pre preložiteľné vlastnosti.", + "Organisation": "Organizácia", + "Organisation Configuration": "Konfigurácia organizácie", + "Organisation ID": "ID organizácie", + "Organisation Members": "Členovia organizácie", + "Organisation Statistics": "Štatistiky organizácie", + "Organisation created successfully": "Organizácia bola úspešne vytvorená", + "Organisation membership changes": "Zmeny členstva v organizácii", + "Organisation name is required": "Názov organizácie je povinný", + "Organisation settings saved successfully": "Nastavenia organizácie boli úspešne uložené", + "Organisation successfully created": "Organizácia bola úspešne vytvorená", + "Organisation successfully updated": "Organizácia bola úspešne aktualizovaná", + "Organisational measures": "Organizačné opatrenia", + "Organisations": "Organizácie", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organizácia", + "Organization ID (Optional)": "ID organizácie (voliteľné)", + "Organizations": "Organizácie", + "Orphaned Items": "Osirelé položky", + "Other": "Iné", + "Output Mapping": "Výstupné mapovanie", + "Overview": "Prehľad", + "Owner": "Vlastník", + "Owner:": "Vlastník:", + "PDF": "PDF", + "PII hits": "Zhody PII", + "PII rows": "Riadky PII", + "Page {current} of {total}": "Strana {current} z {total}", + "Parallel Mode (Faster, more resource intensive)": "Paralelný režim (rýchlejší, náročnejší na zdroje)", + "Parallel:": "Paralelne:", + "Password": "Heslo", + "Password changes are not supported by your authentication provider.": "Zmeny hesla nie sú podporované vaším poskytovateľom overenia totožnosti.", + "Password for SOLR authentication (optional)": "Heslo pre overenie totožnosti SOLR (voliteľné)", + "Password updated successfully": "Heslo bolo úspešne aktualizované", + "Path": "Cesta", + "Path in repository": "Cesta v repozitári", + "Path where the configuration file will be saved in the repository": "Cesta, kam sa uloží konfiguračný súbor v repozitári", + "Path where the register OAS file will be saved in the repository": "Cesta, kam sa uloží súbor OAS registra v repozitári", + "Pattern (regex)": "Vzor (regex)", + "Pattern Issue": "Problém vzoru", + "Pattern constraint is missing": "Chýba obmedzenie vzoru", + "Patterns:": "Vzory:", + "Pending": "Čakajúce", + "Permanent Delete Mode": "Režim trvalého odstránenia", + "Permanently Delete": "Trvalo odstrániť", + "Permanently Delete ({count})": "Trvalo odstrániť ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Trvalo odstrániť všetkých {count} mäkko odstránených objektov. Toto nie je možné vrátiť späť!", + "Permanently delete already soft-deleted objects": "Trvalo odstrániť už mäkko odstránené objekty", + "Permanently delete this conversation?": "Trvalo odstrániť túto konverzáciu?", + "Permission Matrix": "Matica oprávnení", + "Person": "Osoba", + "Personal Data": "Osobné údaje", + "Personal Data Export": "Export osobných údajov", + "Persons": "Osoby", + "Phone": "Telefón", + "Phone Numbers": "Telefónne čísla", + "Photos": "Fotografie", + "Please create an agent in the": "Vytvorte agenta v", + "Please select a user": "Vyberte používateľa", + "Please select an agent to continue": "Vyberte agenta na pokračovanie", + "Please select an organisation": "Vyberte organizáciu", + "Please select at least one item to import": "Vyberte aspoň jednu položku na import", + "Please select registers or schemas first": "Najprv vyberte registre alebo schémy", + "Please select which register and schema to use for the new object": "Vyberte, ktorý register a schému použiť pre nový objekt", + "Please try again later.": "Skúste to znova neskôr.", + "Please wait while we fetch your agents.": "Počkajte, kým načítame vašich agentov.", + "Please wait while we fetch your applications.": "Počkajte, kým načítame vaše aplikácie.", + "Please wait while we fetch your configurations.": "Počkajte, kým načítame vaše konfigurácie.", + "Please wait while we fetch your deleted items.": "Počkajte, kým načítame vaše odstránené položky.", + "Please wait while we fetch your sources.": "Počkajte, kým načítame vaše zdroje.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Počkajte, kým trvalo odstránime SOLR kolekciu. Môže to chvíľu trvať.", + "Polls": "Ankety", + "Popular Search Terms": "Obľúbené hľadané výrazy", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Predvoľby uložené", + "Prerequisites": "Predpoklady", + "Press Enter to send, Shift+Enter for new line": "Stlačte Enter na odoslanie, Shift+Enter pre nový riadok", + "Press enter to send, shift+enter for new line": "Stlačte enter na odoslanie, shift+enter pre nový riadok", + "Prettify": "Skrášliť", + "Preview erasure": "Náhľad vymazania", + "Preview:": "Náhľad:", + "Previous": "Predchádzajúce", + "Private": "Súkromné", + "Private Agent (Default)": "Súkromný agent (predvolené)", + "Private agents": "Súkromní agenti", + "Process Name (optional)": "Názov procesu (voliteľné)", + "Processed {indexed} files, {failed} failed": "Spracovaných {indexed} súborov, {failed} zlyhalo", + "Processes chunks in batches with simulated parallelism.": "Spracúva úseky v dávkach so simulovanou paralelizáciou.", + "Processes file chunks sequentially (safest).": "Spracúva úseky súborov sekvenčne (najbezpečnejšie).", + "Processes objects in chunks with simulated parallelism.": "Spracúva objekty v úsekoch so simulovanou paralelizáciou.", + "Processes objects sequentially (safest).": "Spracúva objekty sekvenčne (najbezpečnejšie).", + "Processing": "Spracúva sa", + "Processing Limits": "Limity spracovania", + "Processing...": "Spracúva sa...", + "Project initialized successfully": "Projekt bol úspešne inicializovaný", + "Project not found. Please initialize first.": "Projekt sa nenašiel. Najprv ho inicializujte.", + "Properties": "Vlastnosti", + "Property Behaviors": "Správanie vlastností", + "Property Configuration:": "Konfigurácia vlastnosti:", + "Property Filters": "Filtre vlastností", + "Property Title": "Názov vlastnosti", + "Property Type": "Typ vlastnosti", + "Property can be improved": "Vlastnosť možno zlepšiť", + "Property could not be added": "Vlastnosť nebolo možné pridať", + "Property could not be updated": "Vlastnosť nebolo možné aktualizovať", + "Property name of inversed relation": "Názov vlastnosti inverzného vzťahu", + "Property successfully added": "Vlastnosť bola úspešne pridaná", + "Property successfully updated": "Vlastnosť bola úspešne aktualizovaná", + "Protected": "Chránené", + "Provider": "Poskytovateľ", + "Provider is required for testing": "Poskytovateľ je povinný na testovanie", + "Public": "Verejné", + "Public View": "Verejné zobrazenie", + "Public views can be accessed by anyone in the system": "Verejné zobrazenia sú prístupné komukoľvek v systéme", + "Publish": "Publikovať", + "Publish Configuration to GitHub": "Publikovať konfiguráciu na GitHub", + "Publish OAS": "Publikovať OAS", + "Publish Objects": "Publikovať objekty", + "Publish Register OAS to GitHub": "Publikovať OAS registra na GitHub", + "Publish all objects in this schema": "Publikovať všetky objekty v tejto schéme", + "Publish objects to make them publicly available": "Publikujte objekty, aby boli verejne dostupné", + "Published": "Publikované", + "Published:": "Publikované:", + "Publishing...": "Publikuje sa...", + "Purge": "Vyčistiť", + "Purge Date": "Dátum vyčistenia", + "Purge {count} object from database": "Vyčistiť {count} objekt z databázy", + "Purge {count} objects from database": "Vyčistiť {count} objektov z databázy", + "Purpose limitation *": "Doelbinding *", + "Push Notifications": "Push notifikácie", + "Query Complexity Distribution": "Rozloženie zložitosti dopytov", + "Query Help": "Pomocník k dopytom", + "Query parameter is required": "Parameter dopytu je povinný", + "Quota": "Kvóta", + "RAG": "RAG", + "RAG Configuration": "Konfigurácia RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Nastavenia RBAC boli úspešne aktualizované", + "Rate limits cleared successfully": "Limity rýchlosti boli úspešne vymazané", + "Raw Changes Data": "Surové údaje o zmenách", + "Re-vectorize on object update": "Znovu vektorizovať pri aktualizácii objektu", + "Read": "Čítať", + "Real-time push notification status via notify_push": "Stav push notifikácií v reálnom čase cez notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push notifikácie v reálnom čase sú aktívne. Pripojení klienti dostávajú okamžité aktualizácie pri vytvorení, aktualizácii alebo odstránení objektov.", + "Realtime push active": "Push v reálnom čase aktívny", + "Realtime push not available — the notify_push app is not installed": "Push v reálnom čase nie je dostupný — aplikácia notify_push nie je nainštalovaná", + "Reason": "Dôvod", + "Reason (optional)": "Dôvod (voliteľné)", + "Rebase operation completed successfully": "Operácia rebase bola úspešne dokončená", + "Rebase operation failed: {error}": "Operácia rebase zlyhala: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Odporúčania:", + "Recommended Type:": "Odporúčaný typ:", + "Recommended: 5 sources": "Odporúčané: 5 zdrojov", + "Refresh": "Obnoviť", + "Refresh Data": "Obnoviť údaje", + "Refresh Stats": "Obnoviť štatistiky", + "Refresh Workflows": "Obnoviť pracovné postupy", + "Refresh dashboard": "Obnoviť nástenku", + "Refresh database info": "Obnoviť informácie o databáze", + "Register": "Register", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Schéma", + "Register ID": "ID registra", + "Register Objects": "Objekty registra", + "Register Overview": "Prehľad registra", + "Register Statistics": "Štatistiky registra", + "Register Totals": "Súčty registra", + "Register and schema are required": "Register a schéma sú povinné", + "Register annotation": "Anotácia registra", + "Register depublished successfully": "Register bol úspešne odpublikovaný", + "Register not found": "Register sa nenašiel", + "Register objects": "Objekty registra", + "Register objects deletion completed successfully": "Odstránenie objektov registra bolo úspešne dokončené", + "Register published successfully": "Register bol úspešne publikovaný", + "Register/Schema Usage": "Využitie registra/schémy", + "Register:": "Register:", + "Registers": "Registre", + "Reindex": "Preindexovať", + "Reindex completed": "Preindexovanie dokončené", + "Reindex completed successfully": "Preindexovanie bolo úspešne dokončené", + "Reindex completed! Processed {count} objects in {duration}s": "Reindexácia dokončená! Spracovaných {count} objektov za {duration} s", + "Reindex failed": "Preindexovanie zlyhalo", + "Reindex failed: %s": "Preindexovanie zlyhalo: %s", + "Reject": "Zamietnuť", + "Rejected": "Zamietnuté", + "Related Cases": "Súvisiace prípady", + "Relations": "Vzťahy", + "Remote Version": "Vzdialená verzia", + "Remove": "Odstrániť", + "Remove Deck card": "Odstrániť kartu Deck", + "Remove avatar": "Odstrániť avatara", + "Remove contact": "Odstrániť kontakt", + "Remove filter": "Odstrániť filter", + "Remove from favorites": "Odstrániť z obľúbených", + "Remove from list": "Odstrániť zo zoznamu", + "Remove group": "Odstrániť skupinu", + "Remove link": "Odstrániť odkaz", + "Remove link to {name}": "Odstrániť odkaz na {name}", + "Remove link to {name}?": "Odstrániť odkaz na {name}?", + "Remove link to {title}": "Odstrániť odkaz na {title}", + "Remove oneOf entry": "Odstrániť položku oneOf", + "Remove property after write-back": "Odstrániť vlastnosť po spätnom zápise", + "Remove {title}": "Odstrániť {title}", + "Remove {userId} from this organisation?": "Odstrániť {userId} z tejto organizácie?", + "Removed": "Odstránené", + "Removed from favorites": "Odstránené z obľúbených", + "Removing schema {schema}...": "Odstraňuje sa schéma {schema}...", + "Rename Conversation": "Premenovať konverzáciu", + "Rename conversation": "Premenovať konverzáciu", + "Replicas": "Repliky", + "Reports": "Správy", + "Repository": "Repozitár", + "Request Body": "Telo požiadavky", + "Request Data": "Údaje požiadavky", + "Request Details": "Podrobnosti požiadavky", + "Request Quota (per day)": "Kvóta požiadaviek (za deň)", + "Request account deactivation": "Požiadať o deaktiváciu účtu", + "Request account deactivation. This will notify administrators for review.": "Požiadať o deaktiváciu účtu. Týmto sa upozornia administrátori na posúdenie.", + "Request body must be JSON with a \"query\" field": "Telo požiadavky musí byť JSON s poľom \"query\"", + "Request timeout in seconds": "Časový limit požiadavky v sekundách", + "Requested at": "Požiadané dňa", + "Required": "Povinné", + "Required Token Scopes": "Požadované rozsahy tokenu", + "Required field": "Povinné pole", + "Required status is inconsistent": "Stav povinnosti je nekonzistentný", + "Requires Dolphin AI for OCR text extraction": "Vyžaduje Dolphin AI na extrakciu textu pomocou OCR", + "Rerun Search": "Znovu spustiť vyhľadávanie", + "Rerunning search: {searchTerm}": "Opätovné spustenie vyhľadávania: {searchTerm}", + "Reset Changes": "Obnoviť zmeny", + "Reset Filters": "Obnoviť filtre", + "Resource Quotas": "Kvóty zdrojov", + "Response Body": "Telo odpovede", + "Response Details": "Podrobnosti odpovede", + "Response Time:": "Čas odozvy:", + "Restore": "Obnoviť", + "Restore conversation": "Obnoviť konverzáciu", + "Restore or permanently delete items": "Obnoviť alebo trvalo odstrániť položky", + "Restore {count} object": "Obnoviť {count} objekt", + "Restore {count} objects": "Obnoviť {count} objektov", + "Result Count Range": "Rozsah počtu výsledkov", + "Results": "Výsledky", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Doba uchovávania (trvanie ISO-8601, napr. P10Y, P30D)", + "Retention settings updated successfully": "Nastavenia uchovávania boli úspešne aktualizované", + "Retrieve all objects for this schema": "Získať všetky objekty pre túto schému", + "Retry": "Opakovať", + "Retry Failed Extractions": "Opakovať neúspešné extrakcie", + "Retry Policy": "Politika opakovania", + "Retry completed": "Opakovanie dokončené", + "Retry upload": "Skúsiť nahrávanie znova", + "Retrying...": "Opakuje sa...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Skontrolujte vybrané objekty nižšie. Akékoľvek objekty, ktoré nechcete odstrániť, môžete odobrať kliknutím na tlačidlo odstránenia.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Skontrolujte vybrané objekty nižšie. Akékoľvek objekty, ktoré nechcete zahrnúť, môžete odobrať.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Skontrolujte vybrané objekty nižšie. Akékoľvek objekty, ktoré nechcete natrvalo odstrániť, môžete odobrať kliknutím na tlačidlo odstránenia. Túto akciu nie je možné vrátiť späť.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Skontrolujte vybrané objekty nižšie. Akékoľvek objekty, ktoré nechcete obnoviť, môžete odobrať kliknutím na tlačidlo odstránenia. Objekty budú obnovené do ich pôvodného umiestnenia.", + "Revoke": "Zrušiť", + "Right": "Vpravo", + "Risk Level": "Úroveň rizika", + "Risk level": "Úroveň rizika", + "Roles and Permissions": "Roly a oprávnenia", + "Roles updated successfully": "Roly boli úspešne aktualizované", + "Run compliance scan": "Spustiť kontrolu súladu", + "SOLR Connection Settings": "Nastavenia pripojenia SOLR", + "SOLR Index Warmup": "Zahriatie indexu SOLR", + "SOLR Version:": "Verzia SOLR:", + "SOLR actions menu": "Ponuka akcií SOLR", + "SOLR base path (usually /solr)": "Základná cesta SOLR (zvyčajne /solr)", + "SOLR collection deleted successfully": "Kolekcia SOLR bola úspešne odstránená", + "SOLR connection test failed: {error}": "Test pripojenia SOLR zlyhal: {error}", + "SOLR connection test successful!": "Test pripojenia SOLR bol úspešný!", + "SOLR index warmup completed successfully": "Zahriatie indexu SOLR bolo úspešne dokončené", + "SOLR is disabled": "SOLR je zakázaný", + "SOLR is not available or not configured": "SOLR nie je dostupný alebo nie je nakonfigurovaný", + "SOLR logging disabled": "Protokolovanie SOLR zakázané", + "SOLR logging enabled": "Protokolovanie SOLR povolené", + "SOLR search disabled": "Vyhľadávanie SOLR zakázané", + "SOLR search enabled": "Vyhľadávanie SOLR povolené", + "SOLR server hostname or IP address": "Názov hostiteľa alebo IP adresa servera SOLR", + "SOLR server port number (optional, defaults to 8983)": "Číslo portu servera SOLR (voliteľné, predvolene 8983)", + "SOLR settings updated successfully": "Nastavenia SOLR boli úspešne aktualizované", + "SOLR setup completed successfully": "Nastavenie SOLR bolo úspešne dokončené", + "SOLR setup completed successfully!": "Nastavenie SOLR bolo úspešne dokončené!", + "SOLR setup error: %s": "Chyba nastavenia SOLR: %s", + "SOLR setup failed": "Nastavenie SOLR zlyhalo", + "SOLR setup failed - check logs": "Nastavenie SOLR zlyhalo – skontrolujte protokoly", + "SOLR setup failed. Please check the configuration and try again.": "Nastavenie SOLR zlyhalo. Skontrolujte konfiguráciu a skúste to znova.", + "SOLR setup failed: {error}": "Nastavenie SOLR zlyhalo: {error}", + "SOLR warmup failed: {error}": "Zahriatie SOLR zlyhalo: {error}", + "Sample Values:": "Vzorové hodnoty:", + "Save": "Uložiť", + "Save Configuration": "Uložiť konfiguráciu", + "Save Connection Settings": "Uložiť nastavenia pripojenia", + "Save Roles": "Uložiť roly", + "Save Settings": "Uložiť nastavenia", + "Save changes": "Uložiť zmeny", + "Save current search as view": "Uložiť aktuálne vyhľadávanie ako zobrazenie", + "Save labels": "Uložiť štítky", + "Save tags for {name}": "Uložiť značky pre {name}", + "Save the organisation first to manage users.": "Najprv uložte organizáciu, aby ste mohli spravovať používateľov.", + "Saved Views": "Uložené zobrazenia", + "Saving...": "Ukladá sa...", + "Scheduled": "Naplánované", + "Schema": "Schéma", + "Schema ID": "ID schémy", + "Schema Name": "Názov schémy", + "Schema Statistics": "Štatistiky schémy", + "Schema annotation": "Anotácia schémy", + "Schema configuration updated successfully for {schema}": "Konfigurácia schémy bola úspešne aktualizovaná pre {schema}", + "Schema depublished successfully": "Schéma bola úspešne odpublikovaná", + "Schema objects deletion completed successfully": "Odstránenie objektov schémy bolo úspešne dokončené", + "Schema published successfully": "Schéma bola úspešne publikovaná", + "Schema reference ($ref)": "Odkaz na schému ($ref)", + "Schema successfully created": "Schéma bola úspešne vytvorená", + "Schema successfully updated": "Schéma bola úspešne aktualizovaná", + "Schema {schema} removed successfully": "Schéma {schema} bola úspešne odstránená", + "Schema:": "Schéma:", + "Schemas": "Schémy", + "Scheme": "Schéma", + "Search": "Hľadať", + "Search / Views": "Vyhľadávanie / Zobrazenia", + "Search / views": "Hľadanie / zobrazenia", + "Search Activity": "Aktivita vyhľadávania", + "Search Analytics": "Analytika vyhľadávania", + "Search Configuration": "Konfigurácia vyhľadávania", + "Search Entities": "Vyhľadať entity", + "Search Files": "Vyhľadať súbory", + "Search GitHub": "Vyhľadať na GitHube", + "Search GitLab": "Vyhľadať na GitLabe", + "Search Method": "Metóda vyhľadávania", + "Search Mode": "Režim vyhľadávania", + "Search Query": "Vyhľadávací dotaz", + "Search Results": "Výsledky vyhľadávania", + "Search Term": "Hľadaný výraz", + "Search Trail Details": "Podrobnosti záznamu vyhľadávania", + "Search Trail Management": "Správa záznamov vyhľadávania", + "Search Trail Statistics": "Štatistiky záznamov vyhľadávania", + "Search Trails": "Záznamy vyhľadávania", + "Search Views": "Vyhľadať zobrazenia", + "Search Webhooks": "Vyhľadať webhooky", + "Search agents...": "Hľadať agentov...", + "Search applications...": "Hľadať aplikácie...", + "Search by file name or path": "Vyhľadať podľa názvu súboru alebo cesty", + "Search by name or URL": "Vyhľadať podľa názvu alebo URL", + "Search by title or UUID...": "Hľadať podľa názvu alebo UUID...", + "Search by value": "Vyhľadať podľa hodnoty", + "Search configurations": "Hľadať konfigurácie", + "Search data sources...": "Hľadať zdroje údajov...", + "Search groups...": "Hľadať skupiny...", + "Search in Database Objects": "Hľadať v objektoch databázy", + "Search in Files": "Vyhľadať v súboroch", + "Search in Objects": "Vyhľadať v objektoch", + "Search objects": "Hľadať objekty", + "Search objects...": "Hľadať objekty...", + "Search property names...": "Vyhľadať názvy vlastností...", + "Search registers...": "Hľadať registre...", + "Search schemas...": "Hľadať schémy...", + "Search trail deletion not implemented yet": "Odstránenie záznamu vyhľadávania ešte nie je implementované", + "Search views...": "Vyhľadať zobrazenia...", + "Search {name}...": "Hľadať {name}...", + "Searching...": "Hľadá sa...", + "Secret": "Tajný kľúč", + "Secret key for HMAC signature generation (optional)": "Tajný kľúč pre generovanie podpisu HMAC (voliteľné)", + "Security": "Zabezpečenie", + "See organisations as a table": "Zobraziť organizácie ako tabuľku", + "See organisations as cards": "Zobraziť organizácie ako karty", + "Select AI Agent": "Vybrať AI agenta", + "Select Active Organisation": "Vybrať aktívnu organizáciu", + "Select All": "Vybrať všetko", + "Select ConfigSet": "Vybrať ConfigSet", + "Select File Types to Vectorize:": "Vyberte typy súborov na vektorizáciu:", + "Select HTTP method": "Vybrať HTTP metódu", + "Select Register and Schema": "Vybrať register a schému", + "Select Views to Vectorize:": "Vyberte zobrazenia na vektorizáciu:", + "Select a Nextcloud group to add": "Vyberte skupinu Nextcloud na pridanie", + "Select a branch": "Vyberte vetvu", + "Select a date property": "Vyberte vlastnosť dátumu", + "Select a model or type a custom model name": "Vyberte model alebo zadajte vlastný názov modelu", + "Select a register": "Vyberte register", + "Select a register...": "Vyberte register...", + "Select a repository": "Vyberte repozitár", + "Select a repository you have write access to": "Vyberte repozitár, ku ktorému máte prístup na zápis", + "Select a schema": "Vyberte schému", + "Select a schema...": "Vyberte schému...", + "Select a user": "Vyberte používateľa", + "Select agent type": "Vyberte typ agenta", + "Select all entities": "Vybrať všetky entity", + "Select an AI Agent": "Vyberte AI agenta", + "Select an AI agent to begin chatting with your data.": "Vyberte AI agenta a začnite chatovať so svojimi údajmi.", + "Select an email to see linked objects": "Vyberte e-mail, aby ste zobrazili prepojené objekty", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Vyberte organizáciu a používateľa, aby ste ho pridali ako člena. Organizácie vyhľadajte podľa názvu.", + "Select backend": "Vybrať backend", + "Select chat model": "Vybrať model chatu", + "Select collection for files": "Vybrať kolekciu pre súbory", + "Select collection for objects": "Vybrať kolekciu pre objekty", + "Select configuration type...": "Vyberte typ konfigurácie...", + "Select default organisation": "Vybrať predvolenú organizáciu", + "Select entity {value}": "Vybrať entitu {value}", + "Select event to listen to...": "Vyberte udalosť, ktorú počúvať...", + "Select fields to display": "Vyberte polia na zobrazenie", + "Select files with the correct extension": "Vyberte súbory so správnou príponou", + "Select group": "Vyberte skupinu", + "Select groups (optional)": "Vyberte skupiny (voliteľné)", + "Select groups to notify...": "Vyberte skupiny, ktoré sa majú upozorniť...", + "Select groups with access to this agent": "Vyberte skupiny s prístupom k tomuto agentovi", + "Select groups...": "Vyberte skupiny...", + "Select model": "Vybrať model", + "Select one or more registers": "Vyberte jeden alebo viac registrov", + "Select one or more schemas": "Vyberte jednu alebo viac schém", + "Select options...": "Vyberte možnosti...", + "Select or create labels, or select \"No label\" to add files": "Vyberte alebo vytvorte štítky, alebo vyberte \"Bez štítka\" na pridanie súborov", + "Select owner application (optional)...": "Vyberte vlastniacu aplikáciu (voliteľné)...", + "Select period": "Vybrať obdobie", + "Select property to send as payload": "Vyberte vlastnosť na odoslanie ako payload", + "Select provider": "Vybrať poskytovateľa", + "Select registers and schemas to save a view": "Vyberte registre a schémy na uloženie zobrazenia", + "Select retry policy": "Vybrať politiku opakovania", + "Select role": "Vyberte rolu", + "Select schemas (empty = all schemas)": "Vyberte schémy (prázdne = všetky schémy)", + "Select schemas to warm up": "Vyberte schémy na zahriatie", + "Select search mode": "Vyberte režim vyhľadávania", + "Select source type...": "Vyberte typ zdroja...", + "Select target type": "Vyberte cieľový typ", + "Select the branch to publish to": "Vyberte vetvu, do ktorej publikovať", + "Select the event this webhook should listen to": "Vyberte udalosť, ktorú má tento webhook počúvať", + "Select views to filter data (optional)": "Vyberte zobrazenia na filtrovanie údajov (voliteľné)", + "Select views to limit which data the agent can access": "Vyberte zobrazenia na obmedzenie údajov, ku ktorým má agent prístup", + "Select views to vectorize:": "Vyberte zobrazenia na vektorizáciu:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Vyberte, ktoré skupiny Nextcloud sú dostupné pre túto organizáciu. Používatelia v týchto skupinách budú mať prístup k zdrojom organizácie.", + "Select which data views the AI can search": "Vyberte, ktoré dátové zobrazenia môže AI vyhľadávať", + "Select which property from the event should be used as the webhook payload data": "Vyberte, ktorá vlastnosť z udalosti sa má použiť ako údaje payloadu webhooku", + "Select which tools the AI can use to perform actions": "Vyberte, ktoré nástroje môže AI použiť na vykonávanie akcií", + "Select {fieldName} values": "Vyberte hodnoty {fieldName}", + "Selected": "Vybrané", + "Selected Groups": "Vybrané skupiny", + "Selected Objects ({count})": "Vybrané objekty ({count})", + "Selected audit trails deleted successfully": "Vybrané audítne záznamy boli úspešne odstránené", + "Selected search trails deleted successfully": "Vybrané záznamy vyhľadávania boli úspešne odstránené", + "Selected users can access this view": "Vybraní používatelia majú prístup k tomuto zobrazeniu", + "Send additional feedback": "Odoslať ďalšiu spätnú väzbu", + "Send as CloudEvent": "Odoslať ako CloudEvent", + "Send as cloudevent": "Odoslať ako cloudevent", + "Sensitive PII": "Citlivé osobné údaje", + "Serial Mode (Safer, slower)": "Sériový režim (bezpečnejší, pomalší)", + "Serial:": "Sériovo:", + "Server Configuration": "Konfigurácia servera", + "Server Information": "Informácie o serveri", + "Set as Active": "Nastaviť ako aktívne", + "Set as active organisation": "Nastaviť ako aktívnu organizáciu", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Nastavte limity pre používanie API a spotrebu tokenov. Použite 0 pre neobmedzené zdroje.", + "Settings": "Nastavenia", + "Settings updated successfully": "Nastavenia boli úspešne aktualizované", + "Shards": "Shardy", + "Share": "Zdieľať", + "Share with Groups": "Zdieľať so skupinami", + "Share with Users": "Zdieľať s používateľmi", + "Show": "Zobraziť", + "Show Filters": "Zobraziť filtre", + "Show less": "Zobraziť menej", + "Show only entries with changes": "Zobraziť len záznamy so zmenami", + "Showing {count} dashboard(s)": "Zobrazuje sa {count} nástenok", + "Showing {count} processing activities": "Zobrazuje sa {count} spracovateľských činností", + "Showing {showing} of {total} agents": "Zobrazuje sa {showing} z {total} agentov", + "Showing {showing} of {total} applications": "Zobrazuje sa {showing} z {total} aplikácií", + "Showing {showing} of {total} audit trail entries": "Zobrazuje sa {showing} z {total} záznamov auditného záznamu", + "Showing {showing} of {total} configurations": "Zobrazuje sa {showing} z {total} konfigurácií", + "Showing {showing} of {total} deleted items": "Zobrazuje sa {showing} z {total} odstránených položiek", + "Showing {showing} of {total} entities": "Zobrazuje sa {showing} z {total} entít", + "Showing {showing} of {total} files": "Zobrazuje sa {showing} z {total} súborov", + "Showing {showing} of {total} log entries": "Zobrazuje sa {showing} z {total} záznamov denníka", + "Showing {showing} of {total} search trail entries": "Zobrazuje sa {showing} z {total} záznamov vyhľadávacieho záznamu", + "Showing {showing} of {total} sources": "Zobrazuje sa {showing} z {total} zdrojov", + "Showing {showing} of {total} templates": "Zobrazuje sa {showing} z {total} šablón", + "Showing {showing} of {total} webhooks": "Zobrazuje sa {showing} z {total} webhookov", + "Showing {shown} of {total} organisations": "Zobrazuje sa {shown} z {total} organizácií", + "Simple": "Jednoduché", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednoduché dopyty: Základné textové vyhľadávania s minimálnymi parametrami (napr. jeden hľadaný výraz, žiadne pokročilé filtre)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednoduché dotazy: základné textové vyhľadávania s minimálnymi parametrami (napr. jeden hľadaný výraz, bez pokročilých filtrov)", + "Size": "Veľkosť", + "Slug": "Slug", + "Soft Delete Mode": "Režim mäkkého odstránenia", + "Soft Deleted Items": "Mäkko odstránené položky", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Mäkko odstrániť všetky objekty pre túto schému ({active} aktívnych, {deleted} už odstránených)", + "SolrCloud mode enabled": "Režim SolrCloud povolený", + "Something went wrong while adding files": "Pri pridávaní súborov sa niečo pokazilo", + "Source": "Zdroj", + "Source successfully updated": "Zdroj bol úspešne aktualizovaný", + "Sources": "Zdroje", + "Sources:": "Zdroje:", + "Special Rights": "Osobitné práva", + "Standalone SOLR mode": "Samostatný režim SOLR", + "Start Conversation": "Začať konverzáciu", + "Start Date Field": "Pole dátumu začiatku", + "Start Vectorization": "Spustiť vektorizáciu", + "Start a conversation": "Začať konverzáciu", + "Starting names cache warmup...": "Spúšťa sa zahriatie vyrovnávacej pamäte názvov...", + "Starting validation for {schema}...": "Spúšťa sa validácia pre {schema}...", + "Starting {action} for {schema}...": "Spúšťa sa {action} pre {schema}...", + "Starting...": "Spúšťa sa...", + "Statistics": "Štatistiky", + "Statistics and Metrics": "Štatistiky a metriky", + "Statistics refreshed": "Štatistiky boli obnovené", + "Stats calculation not yet implemented": "Výpočet štatistík ešte nie je implementovaný", + "Status": "Stav", + "Status Code": "Stavový kód", + "Step": "Krok", + "Storage": "Úložisko", + "Storage Quota (MB)": "Kvóta úložiska (MB)", + "String Constraints": "Reťazcové obmedzenia", + "Subject identifier (email, BSN, name, etc.)": "Identifikátor subjektu (e-mail, BSN, meno atď.)", + "Subscribe": "Prihlásiť na odber", + "Subscribed": "Prihlásené na odber", + "Success": "Úspech", + "Success Rate": "Miera úspešnosti", + "Success Status": "Stav úspešnosti", + "Successful": "Úspešné", + "Successfully added user to organisation: {name}": "Používateľ bol úspešne pridaný do organizácie: {name}", + "Successfully cleared {count} audit trails": "Úspešne vymazaných {count} auditných záznamov", + "Successfully cleared {count} blob storage objects": "Úspešne vymazaných {count} objektov blob úložiska", + "Successfully cleared {count} search trails": "Úspešne vymazaných {count} vyhľadávacích záznamov", + "Successfully created {count} SOLR fields": "Úspešne vytvorených {count} polí SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Úspešne odstránených {count} embeddingov. Prosím, znovu vektorizujte svoje údaje.", + "Successfully deleted {count} objects for {schema}": "Úspešne odstránených {count} objektov pre {schema}", + "Successfully fixed {count} SOLR field configurations": "Úspešne opravených {count} konfigurácií polí SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Úspešne importované: {registers} registrov, {schemas} schém, {objects} objektov", + "Successfully joined organisation": "Úspešné pripojenie k organizácii", + "Successfully permanently deleted {count} objects": "Úspešne natrvalo odstránených {count} objektov", + "Successfully processed {count} files!": "Úspešne spracovaných {count} súborov!", + "Successfully restored {count} object": "Úspešne obnovený {count} objekt", + "Successfully restored {count} objects": "Úspešne obnovených {count} objektov", + "Successfully saved configuration for {count} facets!": "Úspešne uložená konfigurácia pre {count} faziet!", + "Supported File Types": "Podporované typy súborov", + "Switch Active Organisation": "Prepnúť aktívnu organizáciu", + "Switch Organisation": "Prepnúť organizáciu", + "Sync Table": "Synchronizovať tabuľku", + "Synchronization Settings": "Nastavenia synchronizácie", + "Syncing magic table for {schema}...": "Synchronizuje sa magická tabuľka pre {schema}...", + "Syncs": "Synchronizácie", + "System Default": "Systémové predvolené", + "System Prompt": "Systémový prompt", + "System announcements": "Systémové oznámenia", + "Table": "Tabuľka", + "Target ID": "ID cieľa", + "Target Type*": "Typ cieľa*", + "Technical Description": "Technický popis", + "Technical Details": "Technické podrobnosti", + "Technical description for developers and administrators": "Technický popis pre vývojárov a správcov", + "Technical measures": "Technické opatrenia", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Teplota", + "Templates": "Šablóny", + "Test": "Testovať", + "Test Chat": "Testovať chat", + "Test Connection": "Testovať pripojenie", + "Test Embedding": "Testovať vloženie", + "Test endpoint executed successfully": "Testovací koncový bod bol úspešne vykonaný", + "Test webhook delivered successfully": "Testovací webhook bol úspešne doručený", + "Test webhook delivery failed": "Doručenie testovacieho webhooku zlyhalo", + "Test webhook sent successfully": "Testovací webhook bol úspešne odoslaný", + "Testing...": "Testuje sa...", + "Text Chunk #{id}": "Textový blok č. {id}", + "Text Extraction": "Extrakcia textu", + "Text Extraction Engine": "Nástroj na extrakciu textu", + "Text chunks": "Textové úseky", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Textové úseky sa generujú počas extrakcie súborov a ukladajú sa do databázy. Vektorizácia číta tieto vopred rozdelené súbory a konvertuje ich na vloženia.", + "Text extracted successfully": "Text bol úspešne extrahovaný", + "Text extraction disabled": "Extrakcia textu zakázaná", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplikácia Nextcloud Calendar nie je na tomto serveri nainštalovaná ani povolená.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplikácia Nextcloud Contacts nie je na tomto serveri nainštalovaná ani povolená.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplikácia Nextcloud Deck nie je na tomto serveri nainštalovaná ani povolená.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplikácia Nextcloud Mail nie je na tomto serveri nainštalovaná ani povolená.", + "The URL where webhook events will be sent": "URL, kam sa budú odosielať udalosti webhooku", + "The requested conversation does not exist": "Požadovaná konverzácia neexistuje", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Požadovanú nástenku sa nepodarilo načítať. Overte URL alebo vyberte inú nástenku zo zoznamu.", + "The saved view and all its search configuration": "Uložené zobrazenie a celá jeho konfigurácia vyhľadávania", + "There are no audit trail entries matching your current filters.": "Neexistujú žiadne záznamy audítneho záznamu zodpovedajúce vašim aktuálnym filtrom.", + "There are no deleted items matching your current filters.": "Neexistujú žiadne odstránené položky zodpovedajúce vašim aktuálnym filtrom.", + "There are no search trail entries matching your current filters.": "Neexistujú žiadne záznamy vyhľadávania zodpovedajúce vašim aktuálnym filtrom.", + "There are no webhook log entries matching your filters.": "Neexistujú žiadne záznamy protokolu webhooku zodpovedajúce vašim filtrom.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Budú uchované v súlade s dobou uchovávania nakonfigurovanou v ich schéme a po jej uplynutí automaticky natrvalo odstránené. Dobu uchovávania je možné nastaviť pre každú schému a nájdete ju v nastaveniach schémy.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Budú uchované v súlade s dobou uchovávania nakonfigurovanou v ich schéme a potom natrvalo odstránené.", + "This action cannot be undone.": "Túto akciu nie je možné vrátiť späť.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Túto akciu nie je možné vrátiť späť. Uistite sa, že tento ConfigSet nepoužívajú žiadne kolekcie.", + "This action cannot be undone. Make sure no collections are using this configset.": "Túto akciu nie je možné vrátiť späť. Uistite sa, že tento ConfigSet nepoužívajú žiadne kolekcie.", + "This action will submit a deactivation request to your administrators.": "Táto akcia odošle žiadosť o deaktiváciu vašim administrátorom.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Tento agent nemá žiadne konfigurovateľné zobrazenia ani nástroje, ale nastavenia RAG nižšie môžete stále upraviť.", + "This analysis may take some time": "Táto analýza môže chvíľu trvať", + "This audit trail entry does not contain any change information.": "Tento záznam audítneho záznamu neobsahuje žiadne informácie o zmene.", + "This configuration has no data defined.": "Táto konfigurácia nemá definované žiadne údaje.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Tento koncový bod je zastaraný. Namiesto neho použite koncové body založené na úsekoch.", + "This entity has no relations to objects or files": "Táto entita nemá žiadne vzťahy k objektom ani súborom", + "This is a test webhook from OpenRegister": "Toto je testovací webhook z OpenRegister", + "This is not valid JSON": "Toto nie je platný JSON", + "This key already exists on this schema": "Tento kľúč už v tejto schéme existuje", + "This schema must use Magic Table configuration to sync": "Táto schéma musí na synchronizáciu používať konfiguráciu magickej tabuľky", + "This schema must use magic table configuration to sync": "Táto schéma musí na synchronizáciu používať konfiguráciu magickej tabuľky", + "This source has no associated registers.": "Tento zdroj nemá žiadne priradené registre.", + "This token will only be shown once. Copy it now.": "Tento token sa zobrazí iba raz. Skopírujte ho teraz.", + "This will copy the _default ConfigSet with the new name": "Týmto sa skopíruje ConfigSet _default s novým názvom", + "This will copy the _default configset with the new name": "Týmto sa skopíruje ConfigSet _default s novým názvom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Týmto sa natrvalo odstránia VŠETKY embeddingy (vektory) z databázy. Budete musieť znovu vektorizovať všetky objekty a súbory. Túto akciu nie je možné vrátiť späť.\n\nNaozaj chcete pokračovať?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Týmto sa natrvalo odstránia VŠETKY embeddingy (vektory) z databázy. Budete musieť znovu vektorizovať všetky objekty a súbory. Túto akciu nie je možné vrátiť späť.\\n\\nNaozaj chcete pokračovať?", + "This will permanently delete:": "Týmto sa trvalo odstráni:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Týmto sa natrvalo odstráni {count} už mäkko odstránených objektov z databázy. Túto akciu nie je možné vrátiť späť.", + "Timeout (seconds)": "Časový limit (sekundy)", + "Timestamp": "Časová pečiatka", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: Povoľte len zobrazenia, ktoré potrebujú sémantické vyhľadávanie, aby ste minimalizovali náklady na vloženia. Jednoduché vyhľadávacie tabuľky zriedka potrebujú vektorizáciu.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tip: povoľte iba zobrazenia, ktoré potrebujú sémantické vyhľadávanie, aby ste minimalizovali náklady na embeddingy. Jednoduché vyhľadávacie tabuľky vektorizáciu zriedka potrebujú.", + "Title": "Názov", + "Title *": "Názov *", + "Title Template": "Šablóna názvu", + "Title*": "Názov*", + "To (e.g. -1 year or 2025-12-31)": "Do (napr. -1 rok alebo 2025-12-31)", + "To Date": "Do dátumu", + "To add files larger than or equal to 512MB, go to the": "Na pridanie súborov väčších alebo rovných 512 MB prejdite do", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Na úpravu veľkosti úseku alebo stratégie prejdite na Konfigurácia súborov → Limity spracovania.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Na úpravu veľkosti bloku alebo stratégie prejdite do konfigurácie súborov → limity spracovania.", + "To change the embedding provider or model, go to LLM Configuration.": "Na zmenu poskytovateľa vloženia alebo modelu prejdite na Konfigurácia LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Na zmenu poskytovateľa alebo modelu embeddingov prejdite do konfigurácie LLM.", + "To date": "Do dátumu", + "Toggle search sidebar": "Prepnúť bočný panel vyhľadávania", + "Token Created": "Token vytvorený", + "Token Quota (per request)": "Kvóta tokenov (na požiadavku)", + "Token copied to clipboard": "Token bol skopírovaný do schránky", + "Token name": "Názov tokenu", + "Token revoked": "Token bol zrušený", + "Tools": "Nástroje", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Nástroje sa vykonávajú s predvolenými používateľskými oprávneniami agenta, keď nie je aktívna žiadna používateľská relácia (napr. úlohy cron). Predvoleného používateľa nakonfigurujte na karte Nastavenia.", + "Tools respect the agent's own role and organization boundaries.": "Nástroje rešpektujú vlastnú rolu agenta a hranice organizácie.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Nástroje rešpektujú zobrazenia, oprávnenia a hranice organizácie agenta.", + "Top Deleters": "Najčastejší odstraňovatelia", + "Total": "Celkom", + "Total Audit Trails": "Celkový počet audítnych záznamov", + "Total Chunks Available:": "Celkový počet dostupných úsekov:", + "Total Deleted Items": "Celkový počet odstránených položiek", + "Total Members": "Celkový počet členov", + "Total Objects": "Celkový počet objektov", + "Total Objects in Database:": "Celkový počet objektov v databáze:", + "Total Organisations": "Celkový počet organizácií", + "Total Results": "Celkový počet výsledkov", + "Total Searches": "Celkový počet vyhľadávaní", + "Total Size": "Celková veľkosť", + "Total Storage": "Celkové úložisko", + "Total events": "Celkový počet udalostí", + "Total:": "Celkom:", + "Totals": "Súčty", + "Try searching by UUID or with different keywords": "Skúste vyhľadávať podľa UUID alebo s inými kľúčovými slovami", + "Type": "Typ", + "Type \"DELETE COLLECTION\" to confirm:": "Na potvrdenie napíšte \"DELETE COLLECTION\":", + "Type Issue": "Problém typu", + "Type Variations:": "Variácie typu:", + "Type filter (optional, e.g. email)": "Filter typu (voliteľné, napr. e-mail)", + "Type must be \"positive\" or \"negative\"": "Typ musí byť \"positive\" alebo \"negative\"", + "Type to search for groups": "Píšte na vyhľadávanie skupín", + "Type to search for objects...": "Píšte na vyhľadávanie objektov...", + "Type to search for organisations": "Píšte na vyhľadávanie organizácií", + "Type to search for users": "Píšte na vyhľadávanie používateľov", + "Type to search groups...": "Píšte na vyhľadávanie skupín...", + "Type to search users...": "Píšte na vyhľadávanie používateľov...", + "Type to search...": "Píšte na vyhľadávanie...", + "Type your username to confirm": "Na potvrdenie napíšte svoje používateľské meno", + "URL": "URL", + "URL where Ollama is running": "URL, kde beží Ollama", + "UUID copied to clipboard": "UUID bolo skopírované do schránky", + "UUID:": "UUID:", + "Unchanged": "Nezmenené", + "Unique Search Terms": "Jedinečné hľadané výrazy", + "Unique Terms": "Jedinečné výrazy", + "Unknown": "Neznáme", + "Unknown Object": "Neznámy objekt", + "Unknown Register": "Neznámy register", + "Unknown Schema": "Neznáma schéma", + "Unknown error": "Neznáma chyba", + "Unknown widget type: {type}": "Neznámy typ widgetu: {type}", + "Unlink email": "Zrušiť prepojenie e-mailu", + "Unlink event": "Zrušiť prepojenie udalosti", + "Unnamed Object": "Nepomenovaný objekt", + "Untitled": "Bez názvu", + "Untitled View": "Zobrazenie bez názvu", + "Update": "Aktualizovať", + "Update Operations": "Operácie aktualizácie", + "Update available: {local} → {remote}": "Dostupná aktualizácia: {local} → {remote}", + "Update configuration: ...": "Aktualizovať konfiguráciu: ...", + "Update register OAS: ...": "Aktualizovať OAS registra: ...", + "Update vectors when object data changes (recommended for accurate search)": "Aktualizovať vektory pri zmene údajov objektu (odporúčané pre presné vyhľadávanie)", + "Updated": "Aktualizované", + "Updated At": "Aktualizované o", + "Updated:": "Aktualizované:", + "Upload new avatar": "Nahrať nového avatara", + "Uploading configuration sets...": "Nahrávajú sa konfiguračné sady...", + "Uptime:": "Doba prevádzky:", + "Url": "URL", + "Usage Count": "Počet použití", + "Use AI agents for processing and analysis": "Použiť AI agentov na spracovanie a analýzu", + "Use Large Language Model features": "Použiť funkcie veľkého jazykového modelu", + "Use SolrCloud with Zookeeper for distributed search": "Použiť SolrCloud so Zookeeperom pre distribuované vyhľadávanie", + "Use SolrCloud with zookeeper for distributed search": "Použiť SolrCloud so Zookeeperom na distribuované vyhľadávanie", + "Use VNG APIs instead of MongoDB.": "Použiť VNG API namiesto MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Použiť predvolené rozsahy (Posledných 7/30/90 dní, Minulý rok, Staršie)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Použiť externé úložisko (napr. MongoDB) namiesto interného úložiska Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Použite filtre na zúženie záznamov audítneho záznamu podľa registra, schémy, typu akcie, používateľa, rozsahu dátumov alebo ID objektu.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Použite filtre na zúženie odstránených položiek podľa registra, schémy, dátumu odstránenia alebo používateľa, ktorý ich odstránil.", + "Use filters to narrow down entities by type or category.": "Použite filtre na zúženie entít podľa typu alebo kategórie.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Použite filtre na zúženie záznamov vyhľadávania podľa registra, schémy, stavu úspešnosti, používateľa, rozsahu dátumov, hľadaných výrazov alebo výkonnostných metrík.", + "Used By Collections": "Používané kolekciami", + "Used by": "Používa", + "User": "Používateľ", + "User Agent Statistics": "Štatistiky agenta používateľa", + "User Description": "Popis používateľa", + "User Quota": "Kvóta používateľa", + "User rate limits cleared successfully": "Limity rýchlosti používateľa boli úspešne vymazané", + "User removed successfully": "Používateľ bol úspešne odstránený", + "User-friendly description shown in forms and help text": "Používateľsky prívetivý popis zobrazený vo formulároch a texte pomocníka", + "Username": "Používateľské meno", + "Username for SOLR authentication (optional)": "Používateľské meno pre overenie totožnosti SOLR (voliteľné)", + "Users": "Používatelia", + "Uses": "Použitia", + "Using Pre-Generated Chunks": "Použitie vopred vygenerovaných úsekov", + "Validate": "Overiť", + "Validate Objects": "Overiť objekty", + "Validation completed for {schema}": "Validácia dokončená pre {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validácia dokončená pre {schema}: {processed} spracovaných, {updated} aktualizovaných, {failed} zlyhaných", + "Validation completed successfully": "Overenie bolo úspešne dokončené", + "Validation failed": "Overenie zlyhalo", + "Value": "Hodnota", + "Value list (split on ,)": "Zoznam hodnôt (oddelené ,)", + "Vector Dimensions": "Vektorové rozmery", + "Vector Search Backend": "Backend vektorového vyhľadávania", + "Vector field: _embedding_": "Vektorové pole: _embedding_", + "Vectorization Triggers": "Spúšťače vektorizácie", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorizácia dokončená: {vectorized} objektov vektorizovaných, {failed} zlyhaných", + "Vectorization failed: {error}": "Vektorizácia zlyhala: {error}", + "Vectorize All Files": "Vektorizovať všetky súbory", + "Vectorize All Objects": "Vektorizovať všetky objekty", + "Vectorize all file types": "Vektorizovať všetky typy súborov", + "Vectorize all views": "Vektorizovať všetky zobrazenia", + "Vectorize on object creation": "Vektorizovať pri vytvorení objektu", + "Vectors will be stored in your existing object and file collections": "Vektory sa uložia vo vašich existujúcich kolekciách objektov a súborov", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Overuje sa pripojenie k serveru...", + "Version": "Verzia", + "Version Information": "Informácie o verzii", + "Very High": "Veľmi vysoká", + "Very high": "Veľmi vysoká", + "View": "Zobrazenie", + "View API Docs": "Zobraziť dokumentáciu API", + "View API Documentation": "Zobraziť dokumentáciu API", + "View APIs": "Zobraziť API", + "View Changes": "Zobraziť zmeny", + "View Details": "Zobraziť podrobnosti", + "View Error": "Zobraziť chybu", + "View File": "Zobraziť súbor", + "View Full Details": "Zobraziť všetky podrobnosti", + "View Logs": "Zobraziť protokoly", + "View Name": "Názov zobrazenia", + "View Object": "Zobraziť objekt", + "View Parameters": "Zobraziť parametre", + "View Selection": "Výber zobrazenia", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Zobrazte a analyzujte protokoly záznamov vyhľadávania s pokročilými možnosťami filtrovania a analytiky", + "View and analyze system audit trails with advanced filtering capabilities": "Zobrazte a analyzujte systémové audítne záznamy s pokročilými možnosťami filtrovania", + "View and manage authorization across registers and schemas": "Zobraziť a spravovať autorizáciu naprieč registrami a schémami", + "View deleted successfully": "Zobrazenie bolo úspešne odstránené", + "View entity details and manage relations": "Zobrazte podrobnosti entity a spravujte vzťahy", + "View name": "Názov zobrazenia", + "View name is required": "Názov zobrazenia je povinný", + "View object": "Zobraziť objekt", + "View saved successfully!": "Zobrazenie bolo úspešne uložené!", + "View search analytics and manage search logs": "Zobrazte analytiku vyhľadávania a spravujte protokoly vyhľadávania", + "View successfully deleted": "Zobrazenie bolo úspešne odstránené", + "View successfully updated": "Zobrazenie bolo úspešne aktualizované", + "View updated successfully!": "Zobrazenie bolo úspešne aktualizované!", + "View webhook delivery logs and filter by webhook": "Zobrazte protokoly doručenia webhooku a filtrujte podľa webhooku", + "View {count} more": "Zobraziť ďalších {count}", + "Views": "Zobrazenia", + "Visible to end users": "Viditeľné pre koncových používateľov", + "Visible to users": "Viditeľné pre používateľov", + "Wait for Response": "Čakať na odpoveď", + "Wait for webhook response before continuing (required for request/response flows)": "Pred pokračovaním čakať na odpoveď webhooku (vyžadované pre toky požiadavka/odpoveď)", + "Waiting for cluster synchronization...": "Čaká sa na synchronizáciu klastra...", + "Warmup Names Cache": "Zahriať vyrovnávaciu pamäť názvov", + "Warmup failed": "Zahriatie zlyhalo", + "Warning": "Upozornenie", + "Warning:": "Upozornenie:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Upozornenie: Táto vlastnosť má nekonzistentné typy: {types}. Zvážte, či by všetky objekty mali mať rovnaký typ.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Naskenujeme všetky objekty patriace k tejto schéme, aby sme objavili nové vlastnosti a analyzovali existujúce vlastnosti z hľadiska možných vylepšení. Tento proces zahŕňa skúmanie dátovej štruktúry každého objektu, identifikáciu vlastností, ktoré nie sú definované v aktuálnej schéme, a hľadanie príležitostí na zlepšenie existujúcich definícií vlastností pomocou lepších obmedzení, formátov a validačných pravidiel.", + "Webhook": "Webhook", + "Webhook Log Details": "Podrobnosti protokolu webhooku", + "Webhook Logs": "Protokoly webhooku", + "Webhook created successfully": "Webhook bol úspešne vytvorený", + "Webhook deleted": "Webhook odstránený", + "Webhook retry delivered successfully": "Opakovanie webhooku bolo úspešne doručené", + "Webhook retry delivery failed": "Doručenie opakovania webhooku zlyhalo", + "Webhook updated": "Webhook aktualizovaný", + "Webhook updated successfully": "Webhook bol úspešne aktualizovaný", + "Webhooks": "Webhooky", + "Weekly": "Týždenne", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Keď agent beží bez používateľskej relácie (napr. naplánované úlohy), použije sa kontext tohto používateľa", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Keď agent beží bez používateľskej relácie (napr. naplánované úlohy), použije sa kontext tohto používateľa.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Keď je táto možnosť zakázaná, táto fazeta zobrazí iba hodnoty z tejto schémy a po výbere zahrnie filter schémy.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Keď je táto možnosť povolená, uloženie tohto objektu aktualizuje aj cieľové objekty tak, aby zahŕňali spätný odkaz na tento objekt.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Keď je táto možnosť povolená, táto vlastnosť bude po aktualizácii cieľových objektov odstránená zo zdrojového objektu.", + "Workflow Management": "Správa pracovných postupov", + "Workflows": "Pracovné postupy", + "Wrap webhook payload in CloudEvents format for better interoperability": "Zabaliť payload webhooku do formátu CloudEvents pre lepšiu interoperabilitu", + "Wrap webhook payload in cloudevents format for better interoperability": "Zabaliť obsah webhooku do formátu cloudevents pre lepšiu interoperabilitu", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Áno", + "You": "Vy", + "You are not yet a member of any organisations.": "Zatiaľ nie ste členom žiadnej organizácie.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Môžete vytvárať nové ConfigSety založené na šablóne _default alebo nahrať vlastné priamo na váš SOLR server.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Môžete vytvárať nové ConfigSety na základe šablóny _default alebo nahrávať vlastné priamo na svoj server SOLR.", + "You do not have access to this conversation": "Nemáte prístup k tejto konverzácii", + "You do not have permission to delete this conversation": "Nemáte oprávnenie na odstránenie tejto konverzácie", + "You do not have permission to modify this conversation": "Nemáte oprávnenie na úpravu tejto konverzácie", + "You do not have permission to restore this conversation": "Nemáte oprávnenie na obnovenie tejto konverzácie", + "You do not have permission to view the permission matrix. Admin access is required.": "Nemáte oprávnenie zobraziť maticu oprávnení. Vyžaduje sa administrátorský prístup.", + "You have selected {count} properties to add to the schema.": "Vybrali ste {count} vlastností na pridanie do schémy.", + "You must be logged in to favorite views": "Na pridanie zobrazení medzi obľúbené musíte byť prihlásení", + "You need an AI agent to start a conversation.": "Na začatie konverzácie potrebujete AI agenta.", + "Your Fireworks AI API key. Get one at": "Váš API kľúč Fireworks AI. Získajte ho na", + "Your OpenAI API key. Get one at": "Váš API kľúč OpenAI. Získajte ho na", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Vaša spätná väzba bola zaznamenaná. Voliteľne tu môžete poskytnúť ďalšie podrobnosti...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Vaša spätná väzba bola zaznamenaná. Voliteľne tu môžete uviesť ďalšie podrobnosti...", + "Zookeeper Hosts": "Hostitelia Zookeeper", + "Zookeeper Port": "Port Zookeeper", + "Zookeeper Settings (SolrCloud)": "Nastavenia Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Reťazec pripojenia Zookeeper pre SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Číslo portu Zookeeper (voliteľné, predvolene 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORNENIE NA TRVALÉ ODSTRÁNENIE \\u26A0\\uFE0F\n\nChystáte sa NATRVALO odstrániť VŠETKY objekty pre schému \"{schema}\":\n\n\\u2022 Aktívne objekty: {active}\n\\u2022 Mäkko odstránené objekty: {deleted}\n\\u2022 Celkom: {total}\n\nTieto objekty budú úplne odstránené z databázy a NEBUDE ich možné obnoviť.\n\nNaozaj ste si úplne istí?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORNENIE NA TRVALÉ ODSTRÁNENIE \\u26A0\\uFE0F\n\nChystáte sa NATRVALO odstrániť {count} mäkko odstránených objektov pre schému \"{schema}\".\n\nTieto objekty budú úplne odstránené z databázy a NEBUDE ich možné obnoviť.\n\nNaozaj ste si úplne istí?", + "and accessible to all users in selected groups (or all users if no groups selected).": "a prístupné všetkým používateľom vo vybraných skupinách (alebo všetkým používateľom, ak nie sú vybrané žiadne skupiny).", + "and add the files there.": "a pridajte tam súbory.", + "approved": "schválené", + "are only accessible to invited users.": "sú prístupné iba pozvaným používateľom.", + "by": "od", + "chunk": "úsek", + "chunks": "úseky", + "chunks processed": "spracovaných úsekov", + "completed": "dokončené", + "configuration(s)": "konfigurácia(-ie)", + "conversationId is required": "conversationId je povinné", + "create": "vytvoriť", + "delete": "odstrániť", + "deleted objects section": "sekcia odstránených objektov", + "e.g., lib/Settings/config.json": "napr. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "napr. lib/Settings/register.json", + "e.g., lib/settings/config.json": "napr. lib/settings/config.json", + "e.g., lib/settings/register.json": "napr. lib/settings/register.json", + "execute": "vykonať", + "failed": "zlyhalo", + "file": "súbor", + "files": "súbory", + "file{plural}": "súbor{plural}", + "folder": "priečinok", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "v {register}", + "in_progress": "prebieha", + "inherited": "zdedené", + "list": "zoznam", + "log{plural}": "denník{plural}", + "manage": "spravovať", + "members": "členovia", + "menu or contact someone with permission to create agents.": "ponuke alebo kontaktujte niekoho s oprávnením vytvárať agentov.", + "message content is required": "obsah správy je povinný", + "messages": "správy", + "missing": "chýba", + "myapp": "myapp", + "n8n URL and API key are required": "URL a API kľúč n8n sú povinné", + "n8n configuration saved successfully": "Konfigurácia n8n bola úspešne uložená", + "n8n connection not configured": "Pripojenie n8n nie je nakonfigurované", + "n8n connection successful": "Pripojenie n8n úspešné", + "n8n connection test failed: {message}": "Test pripojenia n8n zlyhal: {message}", + "n8n connection test successful": "Test pripojenia n8n úspešný", + "n8n initialization failed: {message}": "Inicializácia n8n zlyhala: {message}", + "n8n integration disabled": "Integrácia n8n zakázaná", + "n8n integration enabled": "Integrácia n8n povolená", + "n8n project initialized successfully": "Projekt n8n bol úspešne inicializovaný", + "n8n settings saved successfully": "Nastavenia n8n boli úspešne uložené", + "notify_push configuration guide": "sprievodca konfiguráciou notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push je nainštalovaný, ale OpenRegister ešte nepotvrdil úspešný push. Aktivujte ho uložením objektu alebo skontrolujte svoju konfiguráciu notify_push.", + "notify_push is installed but not yet active": "notify_push je nainštalovaný, ale ešte nie je aktívny", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekty", + "objects processed": "spracovaných objektov", + "object{plural}": "objekt{plural}", + "or pick a range": "alebo vyberte rozsah", + "org-...": "org-...", + "pending": "čaká", + "provides a good balance between speed and accuracy.": "poskytuje dobrú rovnováhu medzi rýchlosťou a presnosťou.", + "public": "verejné", + "read": "čítať", + "register(s)": "register(-tre)", + "register(s) selected": "register(-tre) vybraté", + "register{plural}": "register{plural}", + "rejected": "zamietnuté", + "required": "povinné", + "results": "výsledky", + "schema(s)": "schéma(-y)", + "schema(s) selected": "schéma(-y) vybraté", + "schema{plural}": "schéma{plural}", + "selected objects": "vybrané objekty", + "set": "nastaviť", + "sk-...": "sk-...", + "this application": "táto aplikácia", + "to": "do", + "type must be \"positive\" or \"negative\"": "typ musí byť \"positive\" alebo \"negative\"", + "update": "aktualizovať", + "vectorized": "vektorizované", + "view": "zobraziť", + "views selected": "zobrazení vybratých", + "waiting": "čaká", + "{active} active, {deleted} deleted": "{active} aktívnych, {deleted} odstránených", + "{count} audit trails cleared successfully": "{count} auditných záznamov bolo úspešne vymazaných", + "{count} compliance issue(s) detected": "Zistených {count} problémov so súladom", + "{count} deletions": "{count} odstránení", + "{count} documents found": "Nájdených {count} dokumentov", + "{count} email": "{count} e-mail", + "{count} emails": "{count} e-mailov", + "{count} entries": "{count} záznamov", + "{count} issues": "{count} problémov", + "{count} members": "{count} členov", + "{count} searches": "{count} vyhľadávaní", + "{count} selected": "{count} vybraných", + "{count} widget(s)": "{count} widgetov", + "{matched} object(s) matched. {erased} erased.": "{matched} objektov zhodných. {erased} vymazaných.", + "{minutes} minutes ago": "pred {minutes} minútami", + "{percentage}% of objects": "{percentage} % objektov", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} v {register} / {schema}", + "{type} cache cleared successfully": "Vyrovnávacia pamäť {type} bola úspešne vymazaná", + "ℹ️ Current Configuration": "ℹ️ Aktuálna konfigurácia", + "ℹ️ current configuration": "ℹ️ aktuálna konfigurácia", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ NEBEZPEČENSTVO: Všetkých {total} objektov bude NATRVALO ODSTRÁNENÝCH z databázy. Túto akciu NIE JE MOŽNÉ OBNOVIŤ ani vrátiť späť!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORNENIE NA TRVALÉ ODSTRÁNENIE ⚠️\n\nChystáte sa NATRVALO odstrániť VŠETKY objekty pre schému \"{schema}\":\n\n• Aktívne objekty: {active}\n• Mäkko odstránené objekty: {deleted}\n• Celkom: {total}\n\nTieto objekty budú úplne odstránené z databázy a NEBUDE ich možné obnoviť.\n\nNaozaj ste si úplne istí?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORNENIE NA TRVALÉ ODSTRÁNENIE ⚠️\n\nChystáte sa NATRVALO odstrániť {count} mäkko odstránených objektov pre schému \"{schema}\".\n\nTieto objekty budú úplne odstránené z databázy a NEBUDE ich možné obnoviť.\n\nNaozaj ste si úplne istí?", + "⚡ Batch Processing": "⚡ Dávkové spracovanie", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR dokáže spracovať milióny dokumentov s časom odozvy vyhľadávania pod jednu sekundu...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Komplexný kontext ✓ Menšia pravdepodobnosť vynechania podrobností ✗ Pomalšie odpovede ✗ Môže zahŕňať menej relevantné informácie", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Rýchlejšie odpovede ✓ Cielenejšie odpovede ✗ Môže vynechať relevantné informácie", + "✨ AI Features": "✨ Funkcie AI", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 V distribuovanom režime SOLR používa ZooKeeper na koordináciu klastra...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fazetové vyhľadávanie umožňuje používateľom prechádzať výsledky podľa kategórií...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Vedeli ste? SOLR poháňa vyhľadávanie pre Netflix, Apple a mnoho ďalších významných stránok!", + "💰 View Selection (Cost Optimization)": "💰 Výber zobrazení (optimalizácia nákladov)", + "📄 Object Serialization": "📄 Serializácia objektu", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR poskytuje bohaté analýzy a štatistiky o výkone vyhľadávania...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSety obsahujú schému a konfiguračné súbory pre váš vyhľadávací index...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikácia zaisťuje dostupnosť vášho vyhľadávacieho indexu aj pri zlyhaní uzlov...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR je výkonná podniková vyhľadávacia platforma postavená na Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Nastavenia vektorizácie", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automatické škálovanie dokáže dynamicky pridávať alebo odoberať uzly podľa zaťaženia...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Bezpečnostné funkcie zahŕňajú autentifikáciu, autorizáciu a SSL šifrovanie...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + } +} diff --git a/l10n/sl.js b/l10n/sl.js new file mode 100644 index 0000000000..b0d9830d2f --- /dev/null +++ b/l10n/sl.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(brez zadeve)", + "(no title)" : "(brez naslova)", + "(unnamed)" : "(neimenovano)", + "(untitled card)" : "(kartica brez naslova)", + "Activities" : "Dejavnosti", + "Add Endpoint" : "Dodaj končno točko", + "Add Source" : "Dodaj vir", + "Add card" : "Dodaj kartico", + "Add contact" : "Dodaj stik", + "All clear" : "Vse je v redu", + "Articles" : "Članki", + "Code" : "Koda", + "Compliance" : "Skladnost", + "Contacts" : "Stiki", + "Create Agent" : "Ustvari agenta", + "Create Application" : "Ustvari aplikacijo", + "Create event" : "Ustvari dogodek", + "Erasure complete" : "Brisanje končano", + "Erasure preview" : "Predogled brisanja", + "Expand details" : "Razširi podrobnosti", + "Save changes" : "Shrani spremembe", + "📄 Object Serialization" : "📄 Serializacija objektov", + "🔢 Vectorization Settings" : "🔢 Nastavitve vektorizacije", + "💰 View Selection (Cost Optimization)" : "💰 Izbira pogledov (optimizacija stroškov)", + "AI Assistant" : "Pomočnik UI", + "AI Chat" : "Klepet UI", + "API Key" : "Ključ API", + "API tokens saved successfully" : "Žetoni API so bili uspešno shranjeni", + "About ConfigSets" : "O nizih konfiguracij", + "Action" : "Dejanje", + "Action Distribution" : "Porazdelitev dejanj", + "Action:" : "Dejanje:", + "Actions" : "Dejanja", + "Active" : "Aktivno", + "Active Collections" : "Aktivne zbirke", + "Active Filters" : "Aktivni filtri", + "Active Organisations" : "Aktivne organizacije", + "Active filters:" : "Aktivni filtri:", + "Active organisation set successfully" : "Aktivna organizacija je bila uspešno nastavljena", + "Activity Period" : "Obdobje dejavnosti", + "Add Groups" : "Dodaj skupine", + "Add Schema" : "Dodaj shemo", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Dodajte naslove shem, opise in informacije o registrih za bogatejši kontekst pri iskanju", + "Add to favorites" : "Dodaj med priljubljene", + "Added" : "Dodano", + "Added to favorites" : "Dodano med priljubljene", + "Additional Information" : "Dodatne informacije", + "Additional feedback saved. Thank you!" : "Dodatne povratne informacije so bile shranjene. Hvala!", + "Admin" : "Skrbnik", + "Advanced" : "Napredno", + "Advanced Filters" : "Napredni filtri", + "Advanced Options" : "Napredne možnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Napredni filtri niso na voljo pri uporabi vira podatkovne baze. Za možnosti filtriranja preklopite na samodejni način ali kazalo SOLR.", + "After" : "Po", + "Agent deleted successfully" : "Agent je bil uspešno izbrisan", + "Agents" : "Agenti", + "All" : "Vse", + "All Categories" : "Vse kategorije", + "All Collections" : "Vse zbirke", + "All Confidence Levels" : "Vse ravni zaupanja", + "All ConfigSets" : "Vsi nizi konfiguracij", + "All Files" : "Vse datoteke", + "All Levels" : "Vse ravni", + "All Types" : "Vse vrste", + "All Webhooks" : "Vsi spletni kljuki", + "All actions" : "Vsa dejanja", + "All audit trails cleared successfully" : "Vse revizijske sledi so bile uspešno počiščene", + "All registers" : "Vsi registri", + "All schemas" : "Vse sheme", + "All searches" : "Vsa iskanja", + "All users" : "Vsi uporabniki", + "An error occurred while clearing audit trails" : "Med čiščenjem revizijskih sledi je prišlo do napake", + "An error occurred while deleting the audit trail" : "Med brisanjem revizijske sledi je prišlo do napake", + "An error occurred while deleting the view" : "Med brisanjem pogleda je prišlo do napake", + "An error occurred while permanently deleting the objects" : "Med trajnim brisanjem objektov je prišlo do napake", + "Analysis completed:" : "Analiza je končana:", + "Analysis steps:" : "Koraki analize:", + "Analytics" : "Analitika", + "Analyze Objects" : "Analiziraj objekte", + "Analyze Properties" : "Analiziraj lastnosti", + "Analyze existing properties for improvement opportunities" : "Analizirajte obstoječe lastnosti za priložnosti izboljšav", + "Analyzing..." : "Poteka analiza ...", + "Any favorites and sharing settings for this view" : "Vse priljubljene nastavitve in nastavitve skupne rabe za ta pogled", + "Any user" : "Kateri koli uporabnik", + "App store cache invalidated successfully" : "Predpomnilnik trgovine z aplikacijami je bil uspešno razveljavljen", + "Application deleted successfully" : "Aplikacija je bila uspešno izbrisana", + "Applications" : "Aplikacije", + "Apply Changes" : "Uveljavi spremembe", + "Archive" : "Arhiviraj", + "Archive conversation" : "Arhiviraj pogovor", + "Archived conversations are hidden from your active list" : "Arhivirani pogovori so skriti z vašega seznama aktivnih", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Ali ste prepričani, da želite počistiti stare iskalne sledi? S tem boste izbrisali vnose, starejše od 30 dni.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Ali ste prepričani, da želite izbrisati izbrane revizijske sledi? Tega dejanja ni mogoče razveljaviti.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Ali ste prepričani, da želite izbrisati izbrane iskalne sledi? Tega dejanja ni mogoče razveljaviti.", + "Are you sure you want to delete this ConfigSet?" : "Ali ste prepričani, da želite izbrisati ta niz konfiguracij?", + "Are you sure you want to permanently delete" : "Ali ste prepričani, da želite trajno izbrisati", + "Ask a question..." : "Postavite vprašanje ...", + "Ask questions about your data using natural language" : "Postavljajte vprašanja o svojih podatkih z uporabo naravnega jezika", + "Attempt" : "Poskus", + "Audit Trail Changes" : "Spremembe revizijske sledi", + "Audit Trail Details" : "Podrobnosti revizijske sledi", + "Audit Trail Management" : "Upravljanje revizijskih sledi", + "Audit Trail Statistics" : "Statistika revizijskih sledi", + "Audit Trails" : "Revizijske sledi", + "Audit trail data copied to clipboard" : "Podatki revizijske sledi so bili kopirani v odložišče", + "Audit trail deleted successfully" : "Revizijska sled je bila uspešno izbrisana", + "Audit trail successfully deleted" : "Revizijska sled je bila uspešno izbrisana", + "Authentication" : "Overjanje", + "Auto-commit disabled" : "Samodejna potrditev je onemogočena", + "Auto-commit enabled" : "Samodejna potrditev je omogočena", + "Auto-create Default Organisation" : "Samodejno ustvari privzeto organizacijo", + "Auto-retry failed vectorizations" : "Samodejno ponovi neuspele vektorizacije", + "Automatically commit changes to SOLR index" : "Samodejno potrdi spremembe v kazalu SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Samodejno ustvari privzeto organizacijo, če ob inicializaciji aplikacije nobena ne obstaja", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Samodejno ustvari vektorske vložitve iz besedilnih delov, ko so datoteke naložene in obdelane", + "Automatically generate vector embeddings when objects are created or updated" : "Samodejno ustvari vektorske vložitve, ko so objekti ustvarjeni ali posodobljeni", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Samodejno ponovi neuspele poskuse vektorizacije (največ 3 ponovitve)", + "Available Filters" : "Razpoložljivi filtri", + "Available Workflows" : "Razpoložljivi poteki dela", + "Avg Execution Time" : "Povprečni čas izvajanja", + "Avg Members/Org" : "Povprečno članov/org.", + "Avg Object Views/Session" : "Povprečno ogledov objektov/seja", + "Avg Response Time" : "Povprečni odzivni čas", + "Avg Results/Search" : "Povprečno rezultatov/iskanje", + "Avg Searches/Session" : "Povprečno iskanj/seja", + "Back" : "Nazaj", + "Back to Entities" : "Nazaj na entitete", + "Back to Registers" : "Nazaj na registre", + "Back to Webhooks" : "Nazaj na spletne kljuke", + "Back to applications" : "Nazaj na aplikacije", + "Back to entities" : "Nazaj na entitete", + "Backend updated successfully. Please reload the application." : "Zaledje je bilo uspešno posodobljeno. Ponovno naložite aplikacijo.", + "Base URL (Optional)" : "Osnovni URL (neobvezno)", + "Basic Information" : "Osnovne informacije", + "Batch Size" : "Velikost paketa", + "Batch extraction completed" : "Paketno pridobivanje je končano", + "Before" : "Pred", + "Before object vectorization can work:" : "Preden lahko vektorizacija objektov deluje:", + "Behavior Issue" : "Težava z vedenjem", + "Bookmarks" : "Zaznamki", + "Blob storage has been retired. All objects now use magic tables." : "Shramba blob je bila ukinjena. Vsi objekti zdaj uporabljajo čarobne tabele.", + "Branch" : "Veja", + "Bulk delete operation completed successfully" : "Operacija množičnega brisanja je bila uspešno končana", + "Bulk save operation completed successfully" : "Operacija množičnega shranjevanja je bila uspešno končana", + "Business Data" : "Poslovni podatki", + "Cache cleared successfully" : "Predpomnilnik je bil uspešno počiščen", + "Calculate Sizes" : "Izračunaj velikosti", + "Cancel" : "Prekliči", + "Cannot delete: objects are still attached" : "Brisanje ni mogoče: objekti so še vedno pripeti", + "Category" : "Kategorija", + "Change Type" : "Vrsta spremembe", + "Changes" : "Spremembe", + "Changes copied to clipboard" : "Spremembe so bile kopirane v odložišče", + "Chat Model" : "Model klepeta", + "Chat Provider" : "Ponudnik klepeta", + "Chat Provider (RAG)" : "Ponudnik klepeta (RAG)", + "Chat Settings" : "Nastavitve klepeta", + "Chat provider connection successful!" : "Povezava s ponudnikom klepeta je uspela!", + "Chat settings" : "Nastavitve klepeta", + "Choose a register" : "Izberite register", + "Choose a schema" : "Izberite shemo", + "Choose how vector similarity calculations are performed for semantic search" : "Izberite, kako se izvajajo izračuni vektorske podobnosti za pomensko iskanje", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Izberite, katere vrste datotek vključiti v postopek vektorizacije. Obdelane bodo le datoteke s pridobljenim besedilom in deli.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Izberite, katere poglede vključiti v postopek vektorizacije. Pustite prazno, da obdelate vse poglede glede na svojo konfiguracijo.", + "Chunk" : "Del", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Brisanje delov še ni implementirano. Uporabite končne točke, ki temeljijo na delih.", + "Chunks" : "Deli", + "Chunks to Vectorize:" : "Deli za vektorizacijo:", + "Cleanup Old Trails" : "Počisti stare sledi", + "Cleanup completed" : "Čiščenje je končano", + "Clear All Embeddings" : "Počisti vse vložitve", + "Clear Entries" : "Počisti vnose", + "Clear Filtered Audit Trails" : "Počisti filtrirane revizijske sledi", + "Clear Filters" : "Počisti filtre", + "Clear Index" : "Počisti kazalo", + "Clear Selection" : "Počisti izbor", + "Clear all" : "Počisti vse", + "Clear all filters" : "Počisti vse filtre", + "Clear date range" : "Počisti datumski obseg", + "Clearing..." : "Poteka čiščenje ...", + "Close" : "Zapri", + "Collection Management" : "Upravljanje zbirk", + "Collection Name" : "Ime zbirke", + "Collection assignments updated successfully" : "Dodelitve zbirk so bile uspešno posodobljene", + "Collection cleared successfully" : "Zbirka je bila uspešno počiščena", + "Collection copied successfully" : "Zbirka je bila uspešno kopirana", + "Collection created successfully" : "Zbirka je bila uspešno ustvarjena", + "Collection deleted successfully" : "Zbirka je bila uspešno izbrisana", + "Collection used to store and index file metadata and content" : "Zbirka, ki se uporablja za shranjevanje in indeksiranje metapodatkov in vsebine datotek", + "Collection used to store and index object data" : "Zbirka, ki se uporablja za shranjevanje in indeksiranje podatkov objektov", + "Collections:" : "Zbirke:", + "Commit Message" : "Sporočilo potrditve", + "Commit Within (ms)" : "Potrdi v roku (ms)", + "Commit message" : "Sporočilo potrditve", + "Compare current schema with real object data" : "Primerjaj trenutno shemo z dejanskimi podatki objektov", + "Completed" : "Končano", + "Complex" : "Zapleteno", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Zapletene poizvedbe: napredna iskanja z več filtri, operatorji in zapletenimi kombinacijami parametrov", + "Confidence Level" : "Raven zaupanja", + "Confidence Score" : "Ocena zaupanja", + "Config must be provided as an object" : "Konfiguracija mora biti podana kot objekt", + "ConfigSet" : "Niz konfiguracij", + "ConfigSet Management" : "Upravljanje nizov konfiguracij", + "ConfigSet Name" : "Ime niza konfiguracij", + "ConfigSet created successfully" : "Niz konfiguracij je bil uspešno ustvarjen", + "ConfigSet deleted successfully" : "Niz konfiguracij je bil uspešno izbrisan", + "ConfigSet:" : "Niz konfiguracij:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "Nizi konfiguracij določajo shemo in konfiguracijo za vaše zbirke SOLR. Vsebujejo definicije polj, analizatorje in druge nastavitve iskanja.", + "Configuration" : "Konfiguracija", + "Configuration Data" : "Podatki konfiguracije", + "Configuration saved" : "Konfiguracija je bila shranjena", + "Configurations" : "Konfiguracije", + "Configure Facets" : "Konfiguriraj fasete", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurirajte ponudnike velikih jezikovnih modelov (LLM) za funkcije, podprte z UI, vključno s pomenskim iskanjem, vložitvami in klepetom.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurirajte osnovne nastavitve povezave za vaš strežnik SOLR, vključno z overjanjem in omrežnimi možnostmi. Za upravljanje jeder in zbirk uporabite ločena pogovorna okna za upravljanje nizov konfiguracij in zbirk.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurirajte, kako se objekti podatkovne baze pretvorijo v vektorske vložitve za pomensko iskanje. Objekti se vektorizirajo neposredno brez potrebe po pridobivanju besedila.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurirajte, kako se objekti pretvorijo v besedilo pred vektorizacijo. Te nastavitve vplivajo na kakovost in kontekst iskanja.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurirajte parametre za vektorizacijo datotek. Ta postopek bo ustvaril vektorske vložitve za vse pridobljene dele datotek.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurirajte parametre za vektorizacijo objektov. Ta postopek bo ustvaril vektorske vložitve za vse objekte, ki ustrezajo filtrom vašega pogleda.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurirajte, katere vrste podatkov iskati in koliko virov pridobiti", + "Connection Failed" : "Povezava ni uspela", + "Connection Settings" : "Nastavitve povezave", + "Connection Successful!" : "Povezava je uspela!", + "Connection failed" : "Povezava ni uspela", + "Connection protocol" : "Protokol povezave", + "Connection successful" : "Povezava je uspela", + "Connection timeout in seconds" : "Časovna omejitev povezave v sekundah", + "Constant delay between retries (5 minutes)" : "Stalna zakasnitev med ponovitvami (5 minut)", + "Constraint Issue" : "Težava z omejitvijo", + "Content Filters" : "Filtri vsebine", + "Continue to Properties" : "Nadaljuj na lastnosti", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Nadzorujte, kateri pogledi objektov naj bodo vektorizirani za zmanjšanje stroškov API. Vektorizirajte le poglede, ki imajo korist od pomenskega iskanja.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Nadzorujte, katere poglede in orodja lahko UI uporablja v tem pogovoru. Privzeto so omogočene vse zmožnosti agenta.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Nadzoruje naključnost (0 = determinističen, 2 = zelo ustvarjalen)", + "Conversation ID is required" : "ID pogovora je obvezen", + "Conversation archived" : "Pogovor je bil arhiviran", + "Conversation archived successfully" : "Pogovor je bil uspešno arhiviran", + "Conversation cleared successfully" : "Pogovor je bil uspešno počiščen", + "Conversation deleted" : "Pogovor je bil izbrisan", + "Conversation permanently deleted" : "Pogovor je bil trajno izbrisan", + "Conversation renamed" : "Pogovor je bil preimenovan", + "Conversation restored" : "Pogovor je bil obnovljen", + "Conversation title" : "Naslov pogovora", + "Copied!" : "Kopirano!", + "Copy" : "Kopiraj", + "Copy Changes" : "Kopiraj spremembe", + "Copy Collection" : "Kopiraj zbirko", + "Copy Data" : "Kopiraj podatke", + "Copy Full Data" : "Kopiraj vse podatke", + "Copying..." : "Poteka kopiranje ...", + "Count" : "Število", + "Counting objects..." : "Poteka štetje objektov ...", + "Costs" : "Stroški", + "Create" : "Ustvari", + "Create Collection" : "Ustvari zbirko", + "Create ConfigSet" : "Ustvari niz konfiguracij", + "Create New Collection" : "Ustvari novo zbirko", + "Create New ConfigSet" : "Ustvari nov niz konfiguracij", + "Create Operations" : "Operacije ustvarjanja", + "Create Webhook" : "Ustvari spletni kljuk", + "Create a copy of collection:" : "Ustvari kopijo zbirke:", + "Create a new ConfigSet based on the _default template" : "Ustvari nov niz konfiguracij na podlagi predloge _default", + "Create a new SOLR collection from an existing ConfigSet" : "Ustvari novo zbirko SOLR iz obstoječega niza konfiguracij", + "Create your first AI agent to get started." : "Ustvarite svojega prvega agenta UI za začetek.", + "Created" : "Ustvarjeno", + "Created:" : "Ustvarjeno:", + "Creating..." : "Poteka ustvarjanje ...", + "Current Filters" : "Trenutni filtri", + "Current Type:" : "Trenutna vrsta:", + "Custom API endpoint if using a different region" : "Konfiguracija končne točke API po meri, če uporabljate drugo regijo", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Glave HTTP po meri (ena na vrstico, oblika: Ime-glave: vrednost)", + "Daily" : "Dnevno", + "Dashboard" : "Nadzorna plošča", + "Data Source" : "Vir podatkov", + "Data sources" : "Viri podatkov", + "Data type does not match observed values" : "Vrsta podatkov se ne ujema z opaženimi vrednostmi", + "Database URL" : "URL podatkovne baze", + "Database information refreshed" : "Informacije o podatkovni bazi so bile osvežene", + "Date Range" : "Datumski obseg", + "Default" : "Privzeto", + "Default Organisation" : "Privzeta organizacija", + "Default: 5" : "Privzeto: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Zakasnitve se z vsakim poskusom podvojijo (2, 4, 8 minut ...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Zakasnitve naraščajo linearno (5, 10, 15 minut ...)", + "Delete" : "Izbriši", + "Delete Application" : "Izbriši aplikacijo", + "Delete Audit Trail" : "Izbriši revizijsko sled", + "Delete Collection" : "Izbriši zbirko", + "Delete ConfigSet" : "Izbriši niz konfiguracij", + "Delete Objects" : "Izbriši objekte", + "Delete Operations" : "Operacije brisanja", + "Delete View" : "Izbriši pogled", + "Delete all objects in this schema" : "Izbriši vse objekte v tej shemi", + "Delete permanently" : "Izbriši trajno", + "Delete this conversation?" : "Izbrišem ta pogovor?", + "Delete view" : "Izbriši pogled", + "Deleted" : "Izbrisano", + "Deleted By" : "Izbrisal", + "Deleted Date" : "Datum izbrisa", + "Deleted Items Management" : "Upravljanje izbrisanih elementov", + "Deleted This Week" : "Izbrisano ta teden", + "Deleted Today" : "Izbrisano danes", + "Deleted:" : "Izbrisano:", + "Deleting..." : "Poteka brisanje ...", + "Deletion Date Range" : "Datumski obseg brisanja", + "Deletion Statistics" : "Statistika brisanja", + "Deprecated" : "Opuščeno", + "Description" : "Opis", + "Description:" : "Opis:", + "Detect data types and patterns" : "Zaznaj vrste podatkov in vzorce", + "Detected At" : "Zaznano ob", + "Detected Format:" : "Zaznana oblika:", + "Detected Issues:" : "Zaznane težave:", + "Disable" : "Onemogoči", + "Disabled" : "Onemogočeno", + "Discover Files" : "Odkrij datoteke", + "Discovered Properties" : "Odkrite lastnosti", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Ali želite trajno izbrisati vse filtrirane vnose revizijske sledi? Tega dejanja ni mogoče razveljaviti.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Ali želite trajno izbrisati ta vnos revizijske sledi? Tega dejanja ni mogoče razveljaviti.", + "Documents" : "Dokumenti", + "Download API Spec" : "Prenesi specifikacijo API", + "Edit" : "Uredi", + "Edit Register" : "Uredi register", + "Edit Schema" : "Uredi shemo", + "Edit View" : "Uredi pogled", + "Edit Webhook" : "Uredi spletni kljuk", + "Edit view" : "Uredi pogled", + "Edit view details" : "Uredi podrobnosti pogleda", + "Effectiveness" : "Učinkovitost", + "Email" : "E-pošta", + "Emails" : "E-pošta", + "Embedding Model" : "Model vložitev", + "Embedding Provider" : "Ponudnik vložitev", + "Embedding generated successfully" : "Vložitev je bila uspešno ustvarjena", + "Embedding provider connection successful!" : "Povezava s ponudnikom vložitev je uspela!", + "Enable" : "Omogoči", + "Enable auto-creation" : "Omogoči samodejno ustvarjanje", + "Enable automatic file vectorization" : "Omogoči samodejno vektorizacijo datotek", + "Enable automatic object vectorization" : "Omogoči samodejno vektorizacijo objektov", + "Enable automatic synchronization" : "Omogoči samodejno sinhronizacijo", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Omogoči podrobno beleženje za operacije SOLR (priporočeno za razhroščevanje)", + "Enable faceting" : "Omogoči fasetiranje", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Omogočite ali onemogočite funkcije LLM. Ponudnike in modele konfigurirajte z gumbom Konfiguracija LLM zgoraj.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Omogočite ali onemogočite integracijo iskanja SOLR. Nastavitve povezave konfigurirajte z gumbom Nastavitve povezave zgoraj.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Omogočite ali onemogočite integracijo poteka dela n8n. Nastavitve povezave konfigurirajte spodaj.", + "Enable or disable this webhook" : "Omogoči ali onemogoči ta spletni kljuk", + "Enable vectorization for all existing and future views (may increase costs)" : "Omogoči vektorizacijo za vse obstoječe in prihodnje poglede (lahko poveča stroške)", + "Enabled" : "Omogočeno", + "Endpoints" : "Končne točke", + "Enter ConfigSet name" : "Vnesite ime niza konfiguracij", + "Enter collection name" : "Vnesite ime zbirke", + "Enter description (optional)..." : "Vnesite opis (neobvezno) ...", + "Enter new collection name" : "Vnesite novo ime zbirke", + "Enter object ID" : "Vnesite ID objekta", + "Enter search term" : "Vnesite iskalni izraz", + "Enter view name..." : "Vnesite ime pogleda ...", + "Enter webhook name" : "Vnesite ime spletnega kljuka", + "Entities" : "Entitete", + "Entity Information" : "Informacije o entiteti", + "Entity deleted successfully" : "Entiteta je bila uspešno izbrisana", + "Entity not found" : "Entiteta ni najdena", + "Entries to be deleted:" : "Vnosi za izbris:", + "Enum Issue" : "Težava z naštevanjem", + "Enum constraint is missing" : "Manjka omejitev naštevanja", + "Error" : "Napaka", + "Error Details" : "Podrobnosti napake", + "Error Information" : "Informacije o napaki", + "Error Message" : "Sporočilo o napaki", + "Error loading application" : "Napaka pri nalaganju aplikacije", + "Error loading audit trails" : "Napaka pri nalaganju revizijskih sledi", + "Error loading entity" : "Napaka pri nalaganju entitete", + "Error loading search trails" : "Napaka pri nalaganju iskalnih sledi", + "Estimated Batches:" : "Predvideni paketi:", + "Estimated Cost:" : "Predvideni stroški:", + "Estimated Duration:" : "Predvideno trajanje:", + "Event" : "Dogodek", + "Event Property for Payload" : "Lastnost dogodka za koristni tovor", + "Events" : "Dogodki", + "Example Value" : "Primer vrednosti", + "Exclusive Maximum" : "Izključni maksimum", + "Exclusive Minimum" : "Izključni minimum", + "Execution Mode" : "Način izvajanja", + "Execution Time" : "Čas izvajanja", + "Execution Time Range" : "Obseg časa izvajanja", + "Existing Improvements" : "Obstoječe izboljšave", + "Exponential" : "Eksponentno", + "Export" : "Izvozi", + "Export completed successfully" : "Izvoz je bil uspešno končan", + "Export, view, or delete audit trails" : "Izvozite, prikažite ali izbrišite revizijske sledi", + "Extend Schema" : "Razširi shemo", + "Extended" : "Razširjeno", + "Extensions" : "Razširitve", + "Extract Pending Files" : "Pridobi datoteke v čakanju", + "Extract properties from each object" : "Pridobi lastnosti iz vsakega objekta", + "Extracted At" : "Pridobljeno ob", + "Extraction Status" : "Stanje pridobivanja", + "Facet configuration updated successfully" : "Konfiguracija faset je bila uspešno posodobljena", + "Facets discovered and configured successfully" : "Fasete so bile uspešno odkrite in konfigurirane", + "Facets discovered successfully" : "Fasete so bile uspešno odkrite", + "Failed" : "Neuspešno", + "Failed to analyze schema properties" : "Analiza lastnosti sheme ni uspela", + "Failed to archive conversation" : "Arhiviranje pogovora ni uspelo", + "Failed to calculate sizes" : "Izračun velikosti ni uspel", + "Failed to clear collection" : "Čiščenje zbirke ni uspelo", + "Failed to copy changes" : "Kopiranje sprememb ni uspelo", + "Failed to copy data" : "Kopiranje podatkov ni uspelo", + "Failed to copy data to clipboard" : "Kopiranje podatkov v odložišče ni uspelo", + "Failed to create conversation" : "Ustvarjanje pogovora ni uspelo", + "Failed to create or find project" : "Ustvarjanje ali iskanje projekta ni uspelo", + "Failed to delete collection" : "Brisanje zbirke ni uspelo", + "Failed to delete conversation" : "Brisanje pogovora ni uspelo", + "Failed to delete webhook" : "Brisanje spletnega kljuka ni uspelo", + "Failed to download API specification" : "Prenos specifikacije API ni uspel", + "Failed to get SOLR field configuration" : "Pridobivanje konfiguracije polja SOLR ni uspelo", + "Failed to get entity categories" : "Pridobivanje kategorij entitet ni uspelo", + "Failed to get entity statistics" : "Pridobivanje statistike entitet ni uspelo", + "Failed to get entity types" : "Pridobivanje vrst entitet ni uspelo", + "Failed to load LLM configuration" : "Nalaganje konfiguracije LLM ni uspelo", + "Failed to load Nextcloud groups" : "Nalaganje skupin Nextcloud ni uspelo", + "Failed to load conversation" : "Nalaganje pogovora ni uspelo", + "Failed to load entities" : "Nalaganje entitet ni uspelo", + "Failed to load entity" : "Nalaganje entitete ni uspelo", + "Failed to load files" : "Nalaganje datotek ni uspelo", + "Failed to load organisations" : "Nalaganje organizacij ni uspelo", + "Failed to load templates" : "Nalaganje predlog ni uspelo", + "Failed to load webhooks" : "Nalaganje spletnih kljukov ni uspelo", + "Failed to load workflows" : "Nalaganje potekov dela ni uspelo", + "Failed to refresh database information" : "Osveževanje informacij o podatkovni bazi ni uspelo", + "Failed to reindex collection" : "Ponovno indeksiranje zbirke ni uspelo", + "Failed to rename conversation" : "Preimenovanje pogovora ni uspelo", + "Failed to restore conversation" : "Obnovitev pogovora ni uspela", + "Failed to retry extraction" : "Ponovni poskus pridobivanja ni uspel", + "Failed to retry webhook" : "Ponovni poskus spletnega kljuka ni uspel", + "Failed to save GitHub token" : "Shranjevanje žetona GitHub ni uspelo", + "Failed to save GitLab URL" : "Shranjevanje URL GitLab ni uspelo", + "Failed to save GitLab token" : "Shranjevanje žetona GitLab ni uspelo", + "Failed to save additional feedback" : "Shranjevanje dodatnih povratnih informacij ni uspelo", + "Failed to save n8n configuration" : "Shranjevanje konfiguracije n8n ni uspelo", + "Failed to save roles" : "Shranjevanje vlog ni uspelo", + "Failed to save settings" : "Shranjevanje nastavitev ni uspelo", + "Failed to save webhook" : "Shranjevanje spletnega kljuka ni uspelo", + "Failed to send feedback" : "Pošiljanje povratnih informacij ni uspelo", + "Failed to test webhook" : "Preizkus spletnega kljuka ni uspel", + "Failed to update favorite status" : "Posodobitev stanja priljubljenosti ni uspela", + "Failed to update schema properties" : "Posodobitev lastnosti sheme ni uspela", + "Failed to update view" : "Posodobitev pogleda ni uspela", + "Failed to update webhook" : "Posodobitev spletnega kljuka ni uspela", + "Feedback recorded" : "Povratne informacije so bile zabeležene", + "Fewer sources (1-3):" : "Manj virov (1–3):", + "Field" : "Polje", + "File" : "Datoteka", + "File Chunk Prediction" : "Napoved delov datoteke", + "File Collection" : "Zbirka datotek", + "File Management" : "Upravljanje datotek", + "File Name" : "Ime datoteke", + "File Path" : "Pot datoteke", + "File Type Selection" : "Izbira vrste datoteke", + "File Vectorization" : "Vektorizacija datotek", + "File Warmup" : "Ogrevanje datotek", + "File actions menu" : "Meni dejanj datoteke", + "File anonymized successfully" : "Datoteka je bila uspešno anonimizirana", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vektorizacija delov datotek še ni implementirana. Deli so pripravljeni in shranjeni, vendar je storitev vektorizacije v razvoju.", + "File collection not configured" : "Zbirka datotek ni konfigurirana", + "File discovery completed" : "Odkrivanje datotek je končano", + "File extraction completed" : "Pridobivanje datotek je končano", + "File extraction queued" : "Pridobivanje datotek je v čakalni vrsti", + "File indexed successfully" : "Datoteka je bila uspešno indeksirana", + "File is already anonymized" : "Datoteka je že anonimizirana", + "File not found" : "Datoteka ni najdena", + "File settings updated successfully" : "Nastavitve datotek so bile uspešno posodobljene", + "File sources" : "Viri datotek", + "File vectorization configuration saved successfully" : "Konfiguracija vektorizacije datotek je bila uspešno shranjena", + "File vectorization started. Check the statistics section for progress." : "Vektorizacija datotek se je začela. Za napredek preverite razdelek s statistiko.", + "File warmup completed" : "Ogrevanje datotek je končano", + "Files" : "Datoteke", + "Files with Completed Extraction:" : "Datoteke s končanim pridobivanjem:", + "Files → fileCollection, Objects → objectCollection" : "Datoteke → fileCollection, Objekti → objectCollection", + "Filter Audit Trails" : "Filtriraj revizijske sledi", + "Filter Deleted Items" : "Filtriraj izbrisane elemente", + "Filter Objects" : "Filtriraj objekte", + "Filter Properties" : "Filtriraj lastnosti", + "Filter Search Trails" : "Filtriraj iskalne sledi", + "Filter Statistics" : "Filtriraj statistiko", + "Filter and analyze search trail entries" : "Filtrirajte in analizirajte vnose iskalnih sledi", + "Filter and manage audit trail entries" : "Filtrirajte in upravljajte vnose revizijskih sledi", + "Filter and manage soft deleted items" : "Filtrirajte in upravljajte mehko izbrisane elemente", + "Filter by object ID" : "Filtriraj po ID objekta", + "Filter by search term" : "Filtriraj po iskalnem izrazu", + "Filter by webhook" : "Filtriraj po spletnem kljuku", + "Filter data loaded automatically. Use the filters below to refine your search." : "Podatki filtra so bili samodejno naloženi. Za izboljšanje iskanja uporabite filtre spodaj.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtriraj sprožilce spletnih kljukov po lastnostih koristnega tovora (ena na vrstico, oblika: ključ: vrednost)", + "Filtered" : "Filtrirano", + "Filters" : "Filtri", + "Fireworks AI Chat Settings" : "Nastavitve klepeta Fireworks AI", + "Fireworks AI Embedding Configuration" : "Konfiguracija vložitev Fireworks AI", + "First" : "Prvo", + "Fixed" : "Popravljeno", + "For chat and retrieval-augmented generation" : "Za klepet in generiranje, obogateno s pridobivanjem", + "For vector embeddings and semantic search" : "Za vektorske vložitve in pomensko iskanje", + "Format" : "Oblika", + "Format Issue" : "Težava z obliko", + "Format constraint is missing" : "Manjka omejitev oblike", + "From Date" : "Od datuma", + "From date" : "Od datuma", + "Full data copied to clipboard" : "Vsi podatki so bili kopirani v odložišče", + "General Issue" : "Splošna težava", + "Generate recommendations and confidence scores" : "Ustvari priporočila in ocene zaupanja", + "Generate vectors immediately when new objects are created" : "Ustvari vektorje takoj, ko so ustvarjeni novi objekti", + "GitHub token is valid" : "Žeton GitHub je veljaven", + "GitHub token removed successfully" : "Žeton GitHub je bil uspešno odstranjen", + "GitHub token saved successfully" : "Žeton GitHub je bil uspešno shranjen", + "GitLab URL saved successfully" : "URL GitLab je bil uspešno shranjen", + "GitLab token is valid" : "Žeton GitLab je veljaven", + "GitLab token saved successfully" : "Žeton GitLab je bil uspešno shranjen", + "HTTP Method" : "Metoda HTTP", + "HTTP method used to send webhook requests" : "Metoda HTTP, uporabljena za pošiljanje zahtev spletnih kljukov", + "Headers" : "Glave", + "Health" : "Zdravje", + "Heartbeat successful - connection kept alive" : "Srčni utrip je uspel – povezava ohranjena", + "Helpful" : "Koristno", + "Hide Filters" : "Skrij filtre", + "Hide in forms" : "Skrij v obrazcih", + "Hide in list view" : "Skrij v pogledu seznama", + "High" : "Visoko", + "High Confidence" : "Visoko zaupanje", + "Host" : "Gostitelj", + "Hourly" : "Urno", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Kako globoko prečkati gnezdene lastnosti objektov (1–20). Višje vrednosti zajamejo več podrobnosti, vendar povečajo velikost vektorja.", + "How often to check for updates (1-168 hours)" : "Kako pogosto preverjati za posodobitve (1–168 ur)", + "How to handle retries for failed webhook deliveries" : "Kako obravnavati ponovne poskuse za neuspele dostave spletnih kljukov", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Omejitve hitrosti IP so bile uspešno počiščene", + "Identify properties not in the schema" : "Identificiraj lastnosti, ki niso v shemi", + "Immutable" : "Nespremenljivo", + "Import" : "Uvozi", + "Import successful" : "Uvoz je uspel", + "Improved Property" : "Izboljšana lastnost", + "In use" : "V uporabi", + "Inactive" : "Neaktivno", + "Include IDs and names of related objects for better contextual search" : "Vključi ID-je in imena povezanih objektov za boljše kontekstualno iskanje", + "Include related object references" : "Vključi sklice na povezane objekte", + "Include schema and register metadata" : "Vključi metapodatke sheme in registra", + "Initialization failed" : "Inicializacija ni uspela", + "Initialize Project" : "Inicializiraj projekt", + "Inspect Fields" : "Preglej polja", + "Inspect Index" : "Preglej kazalo", + "Invalid" : "Neveljavno", + "Invalid batch size. Must be between 1 and 5000" : "Neveljavna velikost paketa. Mora biti med 1 in 5000", + "Invalid field name provided" : "Podano je neveljavno ime polja", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Neveljaven maxObjects. Mora biti 0 (vse) ali pozitivno število", + "Invalid:" : "Neveljavno:", + "Issue" : "Težava", + "Items per page:" : "Elementov na stran:", + "Just now" : "Pravkar", + "LLM Configuration" : "Konfiguracija LLM", + "LLM actions menu" : "Meni dejanj LLM", + "LLM configuration saved successfully" : "Konfiguracija LLM je bila uspešno shranjena", + "LLM features disabled" : "Funkcije LLM so onemogočene", + "LLM features enabled" : "Funkcije LLM so omogočene", + "LLM must be enabled with an embedding provider configured" : "LLM mora biti omogočen s konfiguriranim ponudnikom vložitev", + "LLM settings updated successfully" : "Nastavitve LLM so bile uspešno posodobljene", + "Last" : "Zadnje", + "Last Triggered" : "Zadnjič sproženo", + "Length Range:" : "Obseg dolžine:", + "Linear" : "Linearno", + "Load Advanced Filters" : "Naloži napredne filtre", + "Load advanced filters with live data from your search index" : "Naloži napredne filtre z živimi podatki iz vašega iskalnega kazala", + "Load view" : "Naloži pogled", + "Loading ConfigSets..." : "Poteka nalaganje nizov konfiguracij ...", + "Loading advanced filters..." : "Poteka nalaganje naprednih filtrov ...", + "Loading agents..." : "Poteka nalaganje agentov ...", + "Loading application details..." : "Poteka nalaganje podrobnosti aplikacije ...", + "Loading applications..." : "Poteka nalaganje aplikacij ...", + "Loading archived conversations..." : "Poteka nalaganje arhiviranih pogovorov ...", + "Loading audit trails..." : "Poteka nalaganje revizijskih sledi ...", + "Loading collections..." : "Poteka nalaganje zbirk ...", + "Loading configurations..." : "Poteka nalaganje konfiguracij ...", + "Loading conversation..." : "Poteka nalaganje pogovora ...", + "Loading conversations..." : "Poteka nalaganje pogovorov ...", + "Loading deleted items..." : "Poteka nalaganje izbrisanih elementov ...", + "Loading events..." : "Poteka nalaganje dogodkov ...", + "Loading filter data..." : "Poteka nalaganje podatkov filtra ...", + "Loading groups..." : "Poteka nalaganje skupin ...", + "Loading log details..." : "Poteka nalaganje podrobnosti dnevnika ...", + "Loading registers..." : "Poteka nalaganje registrov ...", + "Loading schemas..." : "Poteka nalaganje shem ...", + "Loading search trails..." : "Poteka nalaganje iskalnih sledi ...", + "Loading sources..." : "Poteka nalaganje virov ...", + "Loading statistics..." : "Poteka nalaganje statistike ...", + "Loading users..." : "Poteka nalaganje uporabnikov ...", + "Loading views..." : "Poteka nalaganje pogledov ...", + "Loading..." : "Poteka nalaganje ...", + "Local" : "Lokalno", + "Local Version" : "Lokalna različica", + "Locked" : "Zaklenjeno", + "Locked:" : "Zaklenjeno:", + "Login successful" : "Prijava je uspela", + "Logs" : "Dnevniki", + "Low" : "Nizko", + "Low Confidence" : "Nizko zaupanje", + "Lucene Version:" : "Različica Lucene:", + "Magic table synchronized successfully" : "Čarobna tabela je bila uspešno sinhronizirana", + "Manage Organisation Roles" : "Upravljaj vloge organizacije", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Upravljajte zbirke SOLR (shrambe podatkov) in jih dodelite objektom in datotekam.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Upravljajte nize konfiguracij SOLR (konfiguracijske predloge) za vaše zbirke.", + "Manage and configure agents for automated tasks" : "Upravljajte in konfigurirajte agente za samodejna opravila", + "Manage and monitor file text extraction status" : "Upravljajte in spremljajte stanje pridobivanja besedila datotek", + "Manage and restore soft deleted items from your registers" : "Upravljajte in obnovite mehko izbrisane elemente iz svojih registrov", + "Manage and view detected entities from files and objects" : "Upravljajte in prikažite zaznane entitete iz datotek in objektov", + "Manage document templates and themes" : "Upravljajte predloge in teme dokumentov", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Upravljajte poteke dela n8n za avtomatizacijo OpenRegister. Poteki dela bodo shranjeni v konfiguriranem projektu.", + "Manage webhooks for event-driven integrations" : "Upravljajte spletne kljuke za integracije, ki temeljijo na dogodkih", + "Manage your applications and modules" : "Upravljajte svoje aplikacije in module", + "Manage your data registers and their configurations" : "Upravljajte svoje podatkovne registre in njihove konfiguracije", + "Manage your data schemas and their properties" : "Upravljajte svoje podatkovne sheme in njihove lastnosti", + "Manage your data sources and their configurations" : "Upravljajte svoje vire podatkov in njihove konfiguracije", + "Manage your saved search configurations" : "Upravljajte svoje shranjene konfiguracije iskanja", + "Manage your system configurations and settings" : "Upravljajte svoje sistemske konfiguracije in nastavitve", + "Managed" : "Upravljano", + "Max Files (0 = all)" : "Največ datotek (0 = vse)", + "Max Length" : "Največja dolžina", + "Max Length:" : "Največja dolžina:", + "Max Objects (0 = all)" : "Največ objektov (0 = vse)", + "Max Retries" : "Največ ponovitev", + "Max Shards/Node" : "Največ drobcev/vozlišče", + "Max execution time (ms)" : "Največji čas izvajanja (ms)", + "Max ms" : "Največ ms", + "Max result count" : "Največje število rezultatov", + "Max results" : "Največ rezultatov", + "Maximum" : "Maksimum", + "Maximum Nesting Depth" : "Največja globina gnezdenja", + "Maximum length constraint is missing" : "Manjka omejitev največje dolžine", + "Maximum length is too restrictive" : "Največja dolžina je preveč omejujoča", + "Maximum number of files to process. Set to 0 to process all files." : "Največje število datotek za obdelavo. Nastavite na 0 za obdelavo vseh datotek.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Največje število objektov za obdelavo. Nastavite na 0 za obdelavo vseh objektov.", + "Maximum number of retry attempts for failed deliveries" : "Največje število poskusov ponovitve za neuspele dostave", + "Maximum time to wait before committing changes" : "Največji čas čakanja pred potrditvijo sprememb", + "Maximum tokens to generate" : "Največje število žetonov za ustvarjanje", + "Maximum value constraint is missing" : "Manjka omejitev največje vrednosti", + "Maximum value is too restrictive" : "Največja vrednost je preveč omejujoča", + "Medium" : "Srednje", + "Medium Confidence" : "Srednje zaupanje", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Srednje poizvedbe: iskanja z nekaj filtriranja ali več parametri (npr. datumski obsegi, določeni registri/sheme)", + "Members of selected groups can access this view" : "Člani izbranih skupin lahko dostopajo do tega pogleda", + "Members:" : "Člani:", + "Memory prediction calculated successfully" : "Napoved pomnilnika je bila uspešno izračunana", + "Message content is required" : "Vsebina sporočila je obvezna", + "Message does not belong to this conversation" : "Sporočilo ne pripada temu pogovoru", + "Metadata Filters" : "Filtri metapodatkov", + "Method" : "Metoda", + "Min Length" : "Najmanjša dolžina", + "Min execution time (ms)" : "Najmanjši čas izvajanja (ms)", + "Min ms" : "Najmanj ms", + "Min result count" : "Najmanjše število rezultatov", + "Min results" : "Najmanj rezultatov", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Manjka omejitev najmanjše vrednosti", + "Minimum value is too restrictive" : "Najmanjša vrednost je preveč omejujoča", + "Missing object fields created successfully" : "Manjkajoča polja objekta so bila uspešno ustvarjena", + "Mode:" : "Način:", + "Model" : "Model", + "Model Name" : "Ime modela", + "Modified" : "Spremenjeno", + "Monthly" : "Mesečno", + "More sources (10-20):" : "Več virov (10–20):", + "Most Active Objects" : "Najbolj aktivni objekti", + "Multiple Of" : "Večkratnik", + "Name" : "Ime", + "Names cache warmup completed" : "Ogrevanje predpomnilnika imen je končano", + "New Collection Name" : "Ime nove zbirke", + "New Conversation" : "Nov pogovor", + "New Properties" : "Nove lastnosti", + "New Property" : "Nova lastnost", + "New Value" : "Nova vrednost", + "New users without specific organisation membership will be automatically added to this organisation" : "Novi uporabniki brez določenega članstva v organizaciji bodo samodejno dodani tej organizaciji", + "Next" : "Naprej", + "No" : "Ne", + "No ConfigSets found" : "Najden ni bil noben niz konfiguracij", + "No GitHub token configured" : "Konfiguriran ni noben žeton GitHub", + "No GitHub token provided" : "Podan ni noben žeton GitHub", + "No GitLab token provided" : "Podan ni noben žeton GitLab", + "No Organisation" : "Brez organizacije", + "No active filters" : "Ni aktivnih filtrov", + "No activity data available" : "Na voljo ni nobenih podatkov o dejavnosti", + "No agents available" : "Na voljo ni nobenih agentov", + "No agents found" : "Najden ni bil noben agent", + "No app store cache exists yet" : "Predpomnilnik trgovine z aplikacijami še ne obstaja", + "No applications are available." : "Na voljo ni nobenih aplikacij.", + "No applications found" : "Najdena ni bila nobena aplikacija", + "No archived conversations" : "Ni arhiviranih pogovorov", + "No audit trail entries found" : "Najden ni bil noben vnos revizijske sledi", + "No changes recorded" : "Zabeležena ni bila nobena sprememba", + "No chunks to vectorize" : "Ni delov za vektorizacijo", + "No collections found" : "Najdena ni bila nobena zbirka", + "No configuration data" : "Ni podatkov konfiguracije", + "No configurations are available." : "Na voljo ni nobenih konfiguracij.", + "No configurations found" : "Najdena ni bila nobena konfiguracija", + "No configurations found for this application." : "Za to aplikacijo ni bila najdena nobena konfiguracija.", + "No conversations yet. Create a new one to get started!" : "Še ni pogovorov. Ustvarite novega za začetek!", + "No data available" : "Na voljo ni nobenih podatkov", + "No data available for chart" : "Za grafikon ni na voljo nobenih podatkov", + "No deleted items found" : "Najden ni bil noben izbrisan element", + "No deletion data available" : "Na voljo ni nobenih podatkov o brisanju", + "No description found" : "Najden ni bil noben opis", + "No description provided" : "Podan ni noben opis", + "No entities detected in this file. Run text extraction first." : "V tej datoteki ni zaznanih entitet. Najprej zaženite pridobivanje besedila.", + "No entities found" : "Najdena ni bila nobena entiteta", + "No entities have been detected yet" : "Zaznana še ni bila nobena entiteta", + "No events found" : "Najden ni bil noben dogodek", + "No expired audit trails found to clear" : "Najdena ni bila nobena potekla revizijska sled za čiščenje", + "No facetable fields available. Select a register and schema to see available filters." : "Na voljo ni nobenih fasetiranih polj. Izberite register in shemo za prikaz razpoložljivih filtrov.", + "No files found" : "Najdena ni bila nobena datoteka", + "No files have been extracted yet" : "Pridobljena še ni bila nobena datoteka", + "No files to process" : "Ni datotek za obdelavo", + "No files to reindex" : "Ni datotek za ponovno indeksiranje", + "No filters are currently active. This will delete ALL audit trail entries!" : "Trenutno ni aktiven noben filter. S tem bodo izbrisani VSI vnosi revizijske sledi!", + "No log entries found" : "Najden ni bil noben vnos dnevnika", + "No logs are available for this configuration." : "Za to konfiguracijo ni na voljo nobenih dnevnikov.", + "No logs are available for this source." : "Za ta vir ni na voljo nobenih dnevnikov.", + "No logs found" : "Najden ni bil noben dnevnik", + "No mismatched fields found - SOLR schema is properly configured" : "Najdena ni bila nobena neujemajoča se polja – shema SOLR je pravilno konfigurirana", + "No objects found" : "Najden ni bil noben objekt", + "No objects found to vectorize" : "Najden ni bil noben objekt za vektorizacijo", + "No objects selected for deletion" : "Za brisanje ni izbran noben objekt", + "No objects to delete" : "Ni objektov za brisanje", + "No objects were permanently deleted" : "Noben objekt ni bil trajno izbrisan", + "No properties found" : "Najdena ni bila nobena lastnost", + "No properties match your filters." : "Vašim filtrom ne ustreza nobena lastnost.", + "No purge date set" : "Datum čiščenja ni nastavljen", + "No register data available" : "Na voljo ni nobenih podatkov o registru", + "No registers found" : "Najden ni bil noben register", + "No registers found for this application." : "Za to aplikacijo ni bil najden noben register.", + "No relations found" : "Najdena ni bila nobena relacija", + "No request body available" : "Telo zahteve ni na voljo", + "No response body available" : "Telo odgovora ni na voljo", + "No saved views yet. Create one in the Search tab!" : "Še ni shranjenih pogledov. Ustvarite enega v zavihku Iskanje!", + "No schema data available" : "Na voljo ni nobenih podatkov o shemi", + "No schema selected for exploration" : "Za raziskovanje ni izbrana nobena shema", + "No schemas found" : "Najdena ni bila nobena shema", + "No schemas found for this application." : "Za to aplikacijo ni bila najdena nobena shema.", + "No search terms data available" : "Na voljo ni nobenih podatkov o iskalnih izrazih", + "No search trail entries found" : "Najden ni bil noben vnos iskalne sledi", + "No sources are available." : "Na voljo ni nobenih virov.", + "No sources found" : "Najden ni bil noben vir", + "No templates found" : "Najdena ni bila nobena predloga", + "No templates have been created yet" : "Ustvarjena še ni bila nobena predloga", + "No valid magic-mapped register+schema combinations found" : "Najdena ni bila nobena veljavna kombinacija registra in sheme s čarobnim preslikovanjem", + "No views found. Create views first before configuring vectorization." : "Najden ni bil noben pogled. Pred konfiguracijo vektorizacije najprej ustvarite poglede.", + "No views match your search" : "Vašemu iskanju ne ustreza noben pogled", + "No webhooks found" : "Najden ni bil noben spletni kljuk", + "No webhooks have been configured yet" : "Konfiguriran še ni bil noben spletni kljuk", + "No workflows found in this project. Create workflows in the n8n editor." : "V tem projektu ni bil najden noben potek dela. Poteke dela ustvarite v urejevalniku n8n.", + "None" : "Brez", + "Not helpful" : "Ni koristno", + "Not in use" : "Ni v uporabi", + "Number Constraints" : "Omejitve števila", + "Number of chunks to vectorize in one API call (1-100)." : "Število delov za vektorizacijo v enem klicu API (1–100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Število delov za vektorizacijo v enem klicu API. Višje = hitrejše, vendar več pomnilnika. Priporočeno: 10–50.", + "Number of objects to process in each batch (1-5000)." : "Število objektov za obdelavo v vsakem paketu (1–5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Število objektov za vektorizacijo v enem klicu API. Višje = hitrejše, vendar več pomnilnika. Priporočeno: 10–50.", + "Numeric Range:" : "Numerični obseg:", + "Object" : "Objekt", + "Object Collection" : "Zbirka objektov", + "Object Count Prediction" : "Napoved števila objektov", + "Object ID" : "ID objekta", + "Object Management" : "Upravljanje objektov", + "Object Vectorization" : "Vektorizacija objektov", + "Object Warmup" : "Ogrevanje objektov", + "Object collection not configured" : "Zbirka objektov ni konfigurirana", + "Object not found" : "Objekt ni najden", + "Object permanently deleted" : "Objekt je bil trajno izbrisan", + "Object restored successfully" : "Objekt je bil uspešno obnovljen", + "Object settings updated successfully" : "Nastavitve objekta so bile uspešno posodobljene", + "Object sources" : "Viri objektov", + "Object unlocked successfully" : "Objekt je bil uspešno odklenjen", + "Object vectorization configuration saved successfully" : "Konfiguracija vektorizacije objektov je bila uspešno shranjena", + "Object vectorized successfully" : "Objekt je bil uspešno vektoriziran", + "Object:" : "Objekt:", + "Objects" : "Objekti", + "Objects Analyzed" : "Analizirani objekti", + "Objects Distribution" : "Porazdelitev objektov", + "Objects being analyzed" : "Objekti, ki se analizirajo", + "Objects by Register" : "Objekti po registru", + "Objects by Schema" : "Objekti po shemi", + "Objects deletion completed successfully" : "Brisanje objektov je bilo uspešno končano", + "Objects in schema" : "Objekti v shemi", + "Objects to Process:" : "Objekti za obdelavo:", + "Objects to be analyzed" : "Objekti za analizo", + "Objects to be deleted" : "Objekti za izbris", + "Objects to be validated" : "Objekti za validacijo", + "Objects will be serialized as JSON text before vectorization" : "Objekti bodo pred vektorizacijo serializirani kot besedilo JSON", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekti bodo mehko izbrisani (označeni kot izbrisani, vendar ohranjeni v podatkovni bazi). Po potrebi jih je mogoče pozneje obnoviti.", + "Old Value" : "Stara vrednost", + "Oldest Item (days)" : "Najstarejši element (dni)", + "Ollama Chat Settings" : "Nastavitve klepeta Ollama", + "Ollama Embedding Configuration" : "Konfiguracija vložitev Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Samo s spremembami", + "Open Register Objects" : "Objekti Open Register", + "Open n8n Editor" : "Odpri urejevalnik n8n", + "OpenAI Chat Settings" : "Nastavitve klepeta OpenAI", + "OpenAI Embedding Configuration" : "Konfiguracija vložitev OpenAI", + "OpenRegister Settings" : "Nastavitve OpenRegister", + "Optional webhook secret for signature verification" : "Neobvezna skrivnost spletnega kljuka za preverjanje podpisa", + "Order" : "Vrstni red", + "Organisation" : "Organizacija", + "Organisation ID" : "ID organizacije", + "Organisation Members" : "Člani organizacije", + "Organisation Statistics" : "Statistika organizacije", + "Organisation created successfully" : "Organizacija je bila uspešno ustvarjena", + "Organisation settings saved successfully" : "Nastavitve organizacije so bile uspešno shranjene", + "Organisations" : "Organizacije", + "Organization" : "Organizacija", + "Organization ID (Optional)" : "ID organizacije (neobvezno)", + "Orphaned Items" : "Osiroteli elementi", + "Owner" : "Lastnik", + "Owner:" : "Lastnik:", + "Parallel Mode (Faster, more resource intensive)" : "Vzporedni način (hitrejši, bolj potraten z viri)", + "Parallel:" : "Vzporedno:", + "Password" : "Geslo", + "Password for SOLR authentication (optional)" : "Geslo za overjanje SOLR (neobvezno)", + "Path" : "Pot", + "Path in repository" : "Pot v repozitoriju", + "Path where the configuration file will be saved in the repository" : "Pot, kjer bo konfiguracijska datoteka shranjena v repozitoriju", + "Path where the register OAS file will be saved in the repository" : "Pot, kjer bo datoteka OAS registra shranjena v repozitoriju", + "Pattern (regex)" : "Vzorec (regex)", + "Pattern Issue" : "Težava z vzorcem", + "Pattern constraint is missing" : "Manjka omejitev vzorca", + "Patterns:" : "Vzorci:", + "Pending" : "V čakanju", + "Permanent Delete Mode" : "Način trajnega brisanja", + "Permanently Delete" : "Trajno izbriši", + "Permanently delete already soft-deleted objects" : "Trajno izbriši že mehko izbrisane objekte", + "Permanently delete this conversation?" : "Trajno izbrišem ta pogovor?", + "Person" : "Oseba", + "Personal Data" : "Osebni podatki", + "Phone" : "Telefon", + "Photos" : "Fotografije", + "Please create an agent in the" : "Ustvarite agenta v", + "Please select an agent to continue" : "Za nadaljevanje izberite agenta", + "Please select which register and schema to use for the new object" : "Izberite, kateri register in shemo uporabiti za nov objekt", + "Please try again later." : "Poskusite znova pozneje.", + "Please wait while we fetch your agents." : "Počakajte, medtem ko pridobivamo vaše agente.", + "Please wait while we fetch your applications." : "Počakajte, medtem ko pridobivamo vaše aplikacije.", + "Please wait while we fetch your configurations." : "Počakajte, medtem ko pridobivamo vaše konfiguracije.", + "Please wait while we fetch your deleted items." : "Počakajte, medtem ko pridobivamo vaše izbrisane elemente.", + "Please wait while we fetch your sources." : "Počakajte, medtem ko pridobivamo vaše vire.", + "Polls" : "Ankete", + "Popular Search Terms" : "Priljubljeni iskalni izrazi", + "Port" : "Vrata", + "Prerequisites" : "Predpogoji", + "Press Enter to send, Shift+Enter for new line" : "Pritisnite Enter za pošiljanje, Shift+Enter za novo vrstico", + "Previous" : "Prejšnje", + "Private" : "Zasebno", + "Processes chunks in batches with simulated parallelism." : "Obdela dele v paketih s simulirano vzporednostjo.", + "Processes file chunks sequentially (safest)." : "Obdela dele datotek zaporedno (najvarneje).", + "Processes objects in chunks with simulated parallelism." : "Obdela objekte v delih s simulirano vzporednostjo.", + "Processes objects sequentially (safest)." : "Obdela objekte zaporedno (najvarneje).", + "Processing" : "Obdelava", + "Processing Limits" : "Omejitve obdelave", + "Processing..." : "Poteka obdelava ...", + "Project initialized successfully" : "Projekt je bil uspešno inicializiran", + "Project not found. Please initialize first." : "Projekt ni najden. Najprej ga inicializirajte.", + "Properties" : "Lastnosti", + "Property Behaviors" : "Vedenja lastnosti", + "Property Configuration:" : "Konfiguracija lastnosti:", + "Property Filters" : "Filtri lastnosti", + "Property Title" : "Naslov lastnosti", + "Property Type" : "Vrsta lastnosti", + "Property can be improved" : "Lastnost je mogoče izboljšati", + "Protected" : "Zaščiteno", + "Provider" : "Ponudnik", + "Provider is required for testing" : "Ponudnik je obvezen za preizkušanje", + "Public" : "Javno", + "Public View" : "Javni pogled", + "Public views can be accessed by anyone in the system" : "Do javnih pogledov lahko dostopa kdor koli v sistemu", + "Publish" : "Objavi", + "Publish Objects" : "Objavi objekte", + "Publish all objects in this schema" : "Objavi vse objekte v tej shemi", + "Published" : "Objavljeno", + "Published:" : "Objavljeno:", + "Publishing..." : "Poteka objavljanje ...", + "Purge" : "Počisti", + "Purge Date" : "Datum čiščenja", + "Query Complexity Distribution" : "Porazdelitev zapletenosti poizvedb", + "Query parameter is required" : "Parameter poizvedbe je obvezen", + "RAG" : "RAG", + "RAG Configuration" : "Konfiguracija RAG", + "Rate limits cleared successfully" : "Omejitve hitrosti so bile uspešno počiščene", + "Raw Changes Data" : "Surovi podatki sprememb", + "Re-vectorize on object update" : "Ponovno vektoriziraj ob posodobitvi objekta", + "Read" : "Branje", + "Recommendations:" : "Priporočila:", + "Recommended Type:" : "Priporočena vrsta:", + "Recommended: 5 sources" : "Priporočeno: 5 virov", + "Refresh" : "Osveži", + "Refresh Data" : "Osveži podatke", + "Refresh Stats" : "Osveži statistiko", + "Refresh Workflows" : "Osveži poteke dela", + "Refresh dashboard" : "Osveži nadzorno ploščo", + "Refresh database info" : "Osveži informacije o podatkovni bazi", + "Register" : "Register", + "Register ID" : "ID registra", + "Register Statistics" : "Statistika registra", + "Register Totals" : "Skupne vrednosti registra", + "Register depublished successfully" : "Objava registra je bila uspešno preklicana", + "Register not found" : "Register ni najden", + "Register objects deletion completed successfully" : "Brisanje objektov registra je bilo uspešno končano", + "Register published successfully" : "Register je bil uspešno objavljen", + "Register/Schema Usage" : "Uporaba registra/sheme", + "Registers" : "Registri", + "Reindex" : "Ponovno indeksiraj", + "Reindex completed" : "Ponovno indeksiranje je končano", + "Reindex completed successfully" : "Ponovno indeksiranje je bilo uspešno končano", + "Reindex failed" : "Ponovno indeksiranje ni uspelo", + "Relations" : "Relacije", + "Remote Version" : "Oddaljena različica", + "Remove" : "Odstrani", + "Remove filter" : "Odstrani filter", + "Remove from favorites" : "Odstrani iz priljubljenih", + "Remove group" : "Odstrani skupino", + "Removed" : "Odstranjeno", + "Removed from favorites" : "Odstranjeno iz priljubljenih", + "Rename Conversation" : "Preimenuj pogovor", + "Rename conversation" : "Preimenuj pogovor", + "Replicas" : "Replike", + "Repository" : "Repozitorij", + "Request Body" : "Telo zahteve", + "Request Data" : "Podatki zahteve", + "Request Details" : "Podrobnosti zahteve", + "Request body must be JSON with a \"query\" field" : "Telo zahteve mora biti JSON s poljem \"query\"", + "Request timeout in seconds" : "Časovna omejitev zahteve v sekundah", + "Required field" : "Obvezno polje", + "Required status is inconsistent" : "Stanje obveznosti je neusklajeno", + "Rerun Search" : "Ponovno zaženi iskanje", + "Reset Changes" : "Ponastavi spremembe", + "Reset Filters" : "Ponastavi filtre", + "Response Body" : "Telo odgovora", + "Response Details" : "Podrobnosti odgovora", + "Response Time:" : "Odzivni čas:", + "Restore" : "Obnovi", + "Restore conversation" : "Obnovi pogovor", + "Restore or permanently delete items" : "Obnovi ali trajno izbriši elemente", + "Result Count Range" : "Obseg števila rezultatov", + "Results" : "Rezultati", + "Retrieve all objects for this schema" : "Pridobi vse objekte za to shemo", + "Retry" : "Poskusi znova", + "Retry Failed Extractions" : "Ponovi neuspela pridobivanja", + "Retry Policy" : "Pravilnik ponovitev", + "Retry completed" : "Ponovni poskus je končan", + "Retrying..." : "Poteka ponovni poskus ...", + "Risk Level" : "Raven tveganja", + "Roles updated successfully" : "Vloge so bile uspešno posodobljene", + "SOLR Connection Settings" : "Nastavitve povezave SOLR", + "SOLR Version:" : "Različica SOLR:", + "SOLR actions menu" : "Meni dejanj SOLR", + "SOLR base path (usually /solr)" : "Osnovna pot SOLR (običajno /solr)", + "SOLR is disabled" : "SOLR je onemogočen", + "SOLR is not available or not configured" : "SOLR ni na voljo ali ni konfiguriran", + "SOLR logging disabled" : "Beleženje SOLR je onemogočeno", + "SOLR logging enabled" : "Beleženje SOLR je omogočeno", + "SOLR search disabled" : "Iskanje SOLR je onemogočeno", + "SOLR search enabled" : "Iskanje SOLR je omogočeno", + "SOLR server hostname or IP address" : "Ime gostitelja ali naslov IP strežnika SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Številka vrat strežnika SOLR (neobvezno, privzeto 8983)", + "SOLR setup completed successfully" : "Nastavitev SOLR je bila uspešno končana", + "SOLR setup failed" : "Nastavitev SOLR ni uspela", + "SOLR setup failed - check logs" : "Nastavitev SOLR ni uspela – preverite dnevnike", + "Sample Values:" : "Vzorčne vrednosti:", + "Save" : "Shrani", + "Save Configuration" : "Shrani konfiguracijo", + "Save Connection Settings" : "Shrani nastavitve povezave", + "Save Roles" : "Shrani vloge", + "Save Settings" : "Shrani nastavitve", + "Save current search as view" : "Shrani trenutno iskanje kot pogled", + "Saved Views" : "Shranjeni pogledi", + "Saving..." : "Poteka shranjevanje ...", + "Schema" : "Shema", + "Schema ID" : "ID sheme", + "Schema Name" : "Ime sheme", + "Schema Statistics" : "Statistika sheme", + "Schema depublished successfully" : "Objava sheme je bila uspešno preklicana", + "Schema objects deletion completed successfully" : "Brisanje objektov sheme je bilo uspešno končano", + "Schema published successfully" : "Shema je bila uspešno objavljena", + "Schema successfully created" : "Shema je bila uspešno ustvarjena", + "Schema successfully updated" : "Shema je bila uspešno posodobljena", + "Schemas" : "Sheme", + "Scheme" : "Shema", + "Search / Views" : "Iskanje / Pogledi", + "Search Activity" : "Dejavnost iskanja", + "Search Analytics" : "Analitika iskanja", + "Search Entities" : "Išči entitete", + "Search Files" : "Išči datoteke", + "Search GitHub" : "Išči GitHub", + "Search GitLab" : "Išči GitLab", + "Search Method" : "Metoda iskanja", + "Search Term" : "Iskalni izraz", + "Search Trail Details" : "Podrobnosti iskalne sledi", + "Search Trail Management" : "Upravljanje iskalnih sledi", + "Search Trail Statistics" : "Statistika iskalnih sledi", + "Search Trails" : "Iskalne sledi", + "Search Views" : "Išči poglede", + "Search Webhooks" : "Išči spletne kljuke", + "Search by file name or path" : "Išči po imenu ali poti datoteke", + "Search by name or URL" : "Išči po imenu ali URL", + "Search by value" : "Išči po vrednosti", + "Search in Files" : "Išči v datotekah", + "Search in Objects" : "Išči v objektih", + "Search property names..." : "Išči imena lastnosti ...", + "Search trail deletion not implemented yet" : "Brisanje iskalne sledi še ni implementirano", + "Search views..." : "Išči poglede ...", + "Secret" : "Skrivnost", + "Secret key for HMAC signature generation (optional)" : "Skrivni ključ za generiranje podpisa HMAC (neobvezno)", + "Select AI Agent" : "Izberi agenta UI", + "Select All" : "Izberi vse", + "Select ConfigSet" : "Izberi niz konfiguracij", + "Select File Types to Vectorize:" : "Izberite vrste datotek za vektorizacijo:", + "Select HTTP method" : "Izberi metodo HTTP", + "Select Register and Schema" : "Izberi register in shemo", + "Select Views to Vectorize:" : "Izberite poglede za vektorizacijo:", + "Select a Nextcloud group to add" : "Izberi skupino Nextcloud za dodajanje", + "Select a branch" : "Izberi vejo", + "Select a model or type a custom model name" : "Izberite model ali vnesite ime modela po meri", + "Select a repository" : "Izberi repozitorij", + "Select a repository you have write access to" : "Izberite repozitorij, do katerega imate dostop za pisanje", + "Select an AI Agent" : "Izberi agenta UI", + "Select an AI agent to begin chatting with your data." : "Izberite agenta UI za začetek klepeta z vašimi podatki.", + "Select backend" : "Izberi zaledje", + "Select chat model" : "Izberi model klepeta", + "Select collection for files" : "Izberi zbirko za datoteke", + "Select collection for objects" : "Izberi zbirko za objekte", + "Select default organisation" : "Izberi privzeto organizacijo", + "Select event to listen to..." : "Izberi dogodek za poslušanje ...", + "Select model" : "Izberi model", + "Select options..." : "Izberi možnosti ...", + "Select period" : "Izberi obdobje", + "Select property to send as payload" : "Izberi lastnost za pošiljanje kot koristni tovor", + "Select provider" : "Izberi ponudnika", + "Select registers and schemas to save a view" : "Izberite registre in sheme za shranjevanje pogleda", + "Select retry policy" : "Izberi pravilnik ponovitev", + "Select the branch to publish to" : "Izberi vejo za objavo", + "Select the event this webhook should listen to" : "Izberi dogodek, ki naj ga ta spletni kljuk posluša", + "Select views to vectorize:" : "Izberite poglede za vektorizacijo:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Izberite, katere skupine Nextcloud so na voljo za to organizacijo. Uporabniki v teh skupinah bodo imeli dostop do virov organizacije.", + "Select which data views the AI can search" : "Izberite, katere podatkovne poglede lahko UI išče", + "Select which property from the event should be used as the webhook payload data" : "Izberite, katero lastnost dogodka uporabiti kot podatke koristnega tovora spletnega kljuka", + "Select which tools the AI can use to perform actions" : "Izberite, katera orodja lahko UI uporablja za izvajanje dejanj", + "Selected" : "Izbrano", + "Selected Groups" : "Izbrane skupine", + "Selected audit trails deleted successfully" : "Izbrane revizijske sledi so bile uspešno izbrisane", + "Selected search trails deleted successfully" : "Izbrane iskalne sledi so bile uspešno izbrisane", + "Selected users can access this view" : "Izbrani uporabniki lahko dostopajo do tega pogleda", + "Send additional feedback" : "Pošlji dodatne povratne informacije", + "Send as CloudEvent" : "Pošlji kot CloudEvent", + "Sensitive PII" : "Občutljivi PII", + "Serial Mode (Safer, slower)" : "Zaporedni način (varnejši, počasnejši)", + "Serial:" : "Zaporedno:", + "Server Configuration" : "Konfiguracija strežnika", + "Server Information" : "Informacije o strežniku", + "Settings" : "Nastavitve", + "Shards" : "Drobci", + "Share" : "Skupna raba", + "Share with Groups" : "Skupna raba s skupinami", + "Share with Users" : "Skupna raba z uporabniki", + "Show Filters" : "Prikaži filtre", + "Show less" : "Prikaži manj", + "Show only entries with changes" : "Prikaži samo vnose s spremembami", + "Simple" : "Preprosto", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Preproste poizvedbe: osnovna besedilna iskanja z minimalnimi parametri (npr. en sam iskalni izraz, brez naprednih filtrov)", + "Size" : "Velikost", + "Slug" : "Oznaka", + "Soft Delete Mode" : "Način mehkega brisanja", + "Soft Deleted Items" : "Mehko izbrisani elementi", + "SolrCloud mode enabled" : "Način SolrCloud je omogočen", + "Source" : "Vir", + "Sources" : "Viri", + "Sources:" : "Viri:", + "Standalone SOLR mode" : "Samostojni način SOLR", + "Start Conversation" : "Začni pogovor", + "Start Vectorization" : "Začni vektorizacijo", + "Start a conversation" : "Začni pogovor", + "Starting names cache warmup..." : "Poteka začetek ogrevanja predpomnilnika imen ...", + "Starting..." : "Poteka zagon ...", + "Statistics" : "Statistika", + "Stats calculation not yet implemented" : "Izračun statistike še ni implementiran", + "Status" : "Stanje", + "Status Code" : "Koda stanja", + "String Constraints" : "Omejitve nizov", + "Success" : "Uspeh", + "Success Rate" : "Stopnja uspeha", + "Success Status" : "Stanje uspeha", + "Successful" : "Uspešno", + "Successfully joined organisation" : "Uspešno pridruženo organizaciji", + "Sync Table" : "Sinhroniziraj tabelo", + "Synchronization Settings" : "Nastavitve sinhronizacije", + "System Default" : "Sistemsko privzeto", + "Technical Description" : "Tehnični opis", + "Technical description for developers and administrators" : "Tehnični opis za razvijalce in skrbnike", + "Temperature" : "Temperatura", + "Templates" : "Predloge", + "Test" : "Preizkus", + "Test Chat" : "Preizkusi klepet", + "Test Connection" : "Preizkusi povezavo", + "Test Embedding" : "Preizkusi vložitev", + "Test endpoint executed successfully" : "Preizkusna končna točka je bila uspešno izvedena", + "Test webhook delivered successfully" : "Preizkusni spletni kljuk je bil uspešno dostavljen", + "Test webhook delivery failed" : "Dostava preizkusnega spletnega kljuka ni uspela", + "Test webhook sent successfully" : "Preizkusni spletni kljuk je bil uspešno poslan", + "Testing..." : "Poteka preizkušanje ...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Besedilni deli se ustvarijo med pridobivanjem datotek in se shranijo v podatkovno bazo. Vektorizacija prebere te vnaprej razdeljene datoteke in jih pretvori v vložitve.", + "Text extracted successfully" : "Besedilo je bilo uspešno pridobljeno", + "Text extraction disabled" : "Pridobivanje besedila je onemogočeno", + "The URL where webhook events will be sent" : "URL, kamor bodo poslani dogodki spletnega kljuka", + "The requested conversation does not exist" : "Zahtevani pogovor ne obstaja", + "The saved view and all its search configuration" : "Shranjeni pogled in vsa njegova konfiguracija iskanja", + "There are no audit trail entries matching your current filters." : "Vašim trenutnim filtrom ne ustreza noben vnos revizijske sledi.", + "There are no deleted items matching your current filters." : "Vašim trenutnim filtrom ne ustreza noben izbrisan element.", + "There are no search trail entries matching your current filters." : "Vašim trenutnim filtrom ne ustreza noben vnos iskalne sledi.", + "There are no webhook log entries matching your filters." : "Vašim filtrom ne ustreza noben vnos dnevnika spletnih kljukov.", + "This action cannot be undone." : "Tega dejanja ni mogoče razveljaviti.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Tega dejanja ni mogoče razveljaviti. Prepričajte se, da nobena zbirka ne uporablja tega niza konfiguracij.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Ta agent nima konfigurabilnih pogledov ali orodij, vendar lahko spodaj še vedno prilagodite nastavitve RAG.", + "This analysis may take some time" : "Ta analiza lahko traja nekaj časa", + "This audit trail entry does not contain any change information." : "Ta vnos revizijske sledi ne vsebuje nobenih informacij o spremembah.", + "This configuration has no data defined." : "Ta konfiguracija nima določenih podatkov.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ta končna točka je opuščena. Namesto tega uporabite končne točke, ki temeljijo na delih.", + "This entity has no relations to objects or files" : "Ta entiteta nima relacij do objektov ali datotek", + "This is a test webhook from OpenRegister" : "To je preizkusni spletni kljuk iz OpenRegister", + "This schema must use Magic Table configuration to sync" : "Ta shema mora za sinhronizacijo uporabljati konfiguracijo čarobne tabele", + "This source has no associated registers." : "Ta vir nima povezanih registrov.", + "This will copy the _default ConfigSet with the new name" : "S tem boste kopirali niz konfiguracij _default z novim imenom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "S tem boste trajno izbrisali VSE vložitve (vektorje) iz podatkovne baze. Ponovno boste morali vektorizirati vse objekte in datoteke. Tega dejanja ni mogoče razveljaviti.\\n\\nAli ste prepričani, da želite nadaljevati?", + "This will permanently delete:" : "S tem boste trajno izbrisali:", + "Timeout (seconds)" : "Časovna omejitev (sekunde)", + "Timestamp" : "Časovni žig", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Namig: omogočite samo poglede, ki potrebujejo pomensko iskanje, da zmanjšate stroške vložitev. Preproste iskalne tabele redko potrebujejo vektorizacijo.", + "Title" : "Naslov", + "To Date" : "Do datuma", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Za prilagoditev velikosti delov ali strategije pojdite na Konfiguracija datotek → Omejitve obdelave.", + "To change the embedding provider or model, go to LLM Configuration." : "Za spremembo ponudnika vložitev ali modela pojdite na Konfiguracija LLM.", + "To date" : "Do datuma", + "Toggle search sidebar" : "Preklopi stransko vrstico iskanja", + "Tools" : "Orodja", + "Top Deleters" : "Največ brisalcev", + "Total" : "Skupaj", + "Total Audit Trails" : "Skupaj revizijskih sledi", + "Total Chunks Available:" : "Skupaj razpoložljivih delov:", + "Total Deleted Items" : "Skupaj izbrisanih elementov", + "Total Members" : "Skupaj članov", + "Total Objects" : "Skupaj objektov", + "Total Objects in Database:" : "Skupaj objektov v podatkovni bazi:", + "Total Organisations" : "Skupaj organizacij", + "Total Results" : "Skupaj rezultatov", + "Total Searches" : "Skupaj iskanj", + "Total Size" : "Skupna velikost", + "Total Storage" : "Skupna shramba", + "Total:" : "Skupaj:", + "Totals" : "Skupne vrednosti", + "Type" : "Vrsta", + "Type Issue" : "Težava z vrsto", + "Type Variations:" : "Variacije vrst:", + "Type must be \"positive\" or \"negative\"" : "Vrsta mora biti \"positive\" ali \"negative\"", + "Type to search for groups" : "Tipkajte za iskanje skupin", + "Type to search for users" : "Tipkajte za iskanje uporabnikov", + "Type to search groups..." : "Tipkajte za iskanje skupin ...", + "Type to search users..." : "Tipkajte za iskanje uporabnikov ...", + "URL" : "URL", + "URL where Ollama is running" : "URL, kjer teče Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Nespremenjeno", + "Unique Search Terms" : "Edinstveni iskalni izrazi", + "Unique Terms" : "Edinstveni izrazi", + "Unknown" : "Neznano", + "Unknown Register" : "Neznan register", + "Unknown Schema" : "Neznana shema", + "Untitled View" : "Pogled brez naslova", + "Update" : "Posodobi", + "Update Operations" : "Operacije posodabljanja", + "Update configuration: ..." : "Posodobi konfiguracijo: ...", + "Update register OAS: ..." : "Posodobi OAS registra: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Posodobi vektorje, ko se podatki objekta spremenijo (priporočeno za natančno iskanje)", + "Updated" : "Posodobljeno", + "Updated At" : "Posodobljeno ob", + "Updated:" : "Posodobljeno:", + "Uptime:" : "Čas delovanja:", + "Usage Count" : "Število uporab", + "Use SolrCloud with Zookeeper for distributed search" : "Uporabi SolrCloud z Zookeeper za porazdeljeno iskanje", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Uporabite filtre za zožitev vnosov revizijske sledi po registru, shemi, vrsti dejanja, uporabniku, datumskem obsegu ali ID objekta.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Uporabite filtre za zožitev izbrisanih elementov po registru, shemi, datumu brisanja ali uporabniku, ki jih je izbrisal.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Uporabite filtre za zožitev vnosov iskalne sledi po registru, shemi, stanju uspeha, uporabniku, datumskem obsegu, iskalnih izrazih ali meritvah zmogljivosti.", + "Used By Collections" : "Uporabljeno v zbirkah", + "User" : "Uporabnik", + "User Agent Statistics" : "Statistika uporabniškega agenta", + "User Description" : "Opis uporabnika", + "User rate limits cleared successfully" : "Omejitve hitrosti uporabnika so bile uspešno počiščene", + "User removed successfully" : "Uporabnik je bil uspešno odstranjen", + "User-friendly description shown in forms and help text" : "Uporabniku prijazen opis, prikazan v obrazcih in besedilu pomoči", + "Username" : "Uporabniško ime", + "Username for SOLR authentication (optional)" : "Uporabniško ime za overjanje SOLR (neobvezno)", + "Users" : "Uporabniki", + "Using Pre-Generated Chunks" : "Uporaba vnaprej ustvarjenih delov", + "Validate" : "Validiraj", + "Validate Objects" : "Validiraj objekte", + "Validation completed successfully" : "Validacija je bila uspešno končana", + "Validation failed" : "Validacija ni uspela", + "Value" : "Vrednost", + "Vector Dimensions" : "Dimenzije vektorja", + "Vector Search Backend" : "Zaledje vektorskega iskanja", + "Vector field: _embedding_" : "Vektorsko polje: _embedding_", + "Vectorization Triggers" : "Sprožilci vektorizacije", + "Vectorize All Files" : "Vektoriziraj vse datoteke", + "Vectorize All Objects" : "Vektoriziraj vse objekte", + "Vectorize all file types" : "Vektoriziraj vse vrste datotek", + "Vectorize all views" : "Vektoriziraj vse poglede", + "Vectorize on object creation" : "Vektoriziraj ob ustvarjanju objekta", + "Vectors will be stored in your existing object and file collections" : "Vektorji bodo shranjeni v vaših obstoječih zbirkah objektov in datotek", + "Version" : "Različica", + "Very High" : "Zelo visoko", + "View API Docs" : "Prikaži dokumentacijo API", + "View Changes" : "Prikaži spremembe", + "View Details" : "Prikaži podrobnosti", + "View Error" : "Prikaži napako", + "View File" : "Prikaži datoteko", + "View Full Details" : "Prikaži vse podrobnosti", + "View Logs" : "Prikaži dnevnike", + "View Name" : "Ime pogleda", + "View Object" : "Prikaži objekt", + "View Parameters" : "Prikaži parametre", + "View Selection" : "Izbira pogledov", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Prikažite in analizirajte dnevnike iskalnih sledi z naprednimi zmožnostmi filtriranja in analitike", + "View and analyze system audit trails with advanced filtering capabilities" : "Prikažite in analizirajte sistemske revizijske sledi z naprednimi zmožnostmi filtriranja", + "View deleted successfully" : "Pogled je bil uspešno izbrisan", + "View entity details and manage relations" : "Prikažite podrobnosti entitete in upravljajte relacije", + "View name" : "Ime pogleda", + "View name is required" : "Ime pogleda je obvezno", + "View saved successfully!" : "Pogled je bil uspešno shranjen!", + "View search analytics and manage search logs" : "Prikažite analitiko iskanja in upravljajte dnevnike iskanja", + "View successfully deleted" : "Pogled je bil uspešno izbrisan", + "View successfully updated" : "Pogled je bil uspešno posodobljen", + "View updated successfully!" : "Pogled je bil uspešno posodobljen!", + "View webhook delivery logs and filter by webhook" : "Prikažite dnevnike dostave spletnih kljukov in filtrirajte po spletnem kljuku", + "Views" : "Pogledi", + "Visible to users" : "Vidno uporabnikom", + "Wait for Response" : "Počakaj na odgovor", + "Wait for webhook response before continuing (required for request/response flows)" : "Počakaj na odgovor spletnega kljuka pred nadaljevanjem (obvezno za pretoke zahteva/odgovor)", + "Warmup Names Cache" : "Ogrej predpomnilnik imen", + "Warning" : "Opozorilo", + "Warning:" : "Opozorilo:", + "Webhook" : "Spletni kljuk", + "Webhook Log Details" : "Podrobnosti dnevnika spletnega kljuka", + "Webhook Logs" : "Dnevniki spletnih kljukov", + "Webhook created successfully" : "Spletni kljuk je bil uspešno ustvarjen", + "Webhook deleted" : "Spletni kljuk je bil izbrisan", + "Webhook retry delivered successfully" : "Ponovni poskus spletnega kljuka je bil uspešno dostavljen", + "Webhook retry delivery failed" : "Dostava ponovnega poskusa spletnega kljuka ni uspela", + "Webhook updated" : "Spletni kljuk je bil posodobljen", + "Webhook updated successfully" : "Spletni kljuk je bil uspešno posodobljen", + "Webhooks" : "Spletni kljuki", + "Weekly" : "Tedensko", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Ovij koristni tovor spletnega kljuka v obliko CloudEvents za boljšo interoperabilnost", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Da", + "You" : "Vi", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Ustvarite lahko nove nize konfiguracij na podlagi predloge _default ali naložite tiste po meri neposredno na vaš strežnik SOLR.", + "You do not have access to this conversation" : "Do tega pogovora nimate dostopa", + "You do not have permission to delete this conversation" : "Nimate dovoljenja za brisanje tega pogovora", + "You do not have permission to modify this conversation" : "Nimate dovoljenja za spreminjanje tega pogovora", + "You do not have permission to restore this conversation" : "Nimate dovoljenja za obnovitev tega pogovora", + "You must be logged in to favorite views" : "Za dodajanje pogledov med priljubljene morate biti prijavljeni", + "You need an AI agent to start a conversation." : "Za začetek pogovora potrebujete agenta UI.", + "Your Fireworks AI API key. Get one at" : "Vaš ključ API Fireworks AI. Pridobite ga na", + "Your OpenAI API key. Get one at" : "Vaš ključ API OpenAI. Pridobite ga na", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Vaše povratne informacije so bile zabeležene. Po želji lahko tukaj navedete dodatne podrobnosti ...", + "Zookeeper Hosts" : "Gostitelji Zookeeper", + "Zookeeper Port" : "Vrata Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Nastavitve Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Niz povezave Zookeeper za SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Številka vrat Zookeeper (neobvezno, privzeto 2181)", + "chunk" : "del", + "chunks" : "deli", + "chunks processed" : "obdelanih delov", + "configuration(s)" : "konfiguracija(-e)", + "e.g., lib/Settings/config.json" : "npr. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "npr. lib/Settings/register.json", + "failed" : "neuspešno", + "file" : "datoteka", + "files" : "datoteke", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "člani", + "menu or contact someone with permission to create agents." : "meniju ali se obrnite na nekoga z dovoljenjem za ustvarjanje agentov.", + "messages" : "sporočila", + "n8n URL and API key are required" : "URL n8n in ključ API sta obvezna", + "n8n configuration saved successfully" : "Konfiguracija n8n je bila uspešno shranjena", + "n8n connection not configured" : "Povezava n8n ni konfigurirana", + "n8n connection successful" : "Povezava n8n je uspela", + "n8n connection test successful" : "Preizkus povezave n8n je uspel", + "n8n integration disabled" : "Integracija n8n je onemogočena", + "n8n integration enabled" : "Integracija n8n je omogočena", + "n8n project initialized successfully" : "Projekt n8n je bil uspešno inicializiran", + "n8n settings saved successfully" : "Nastavitve n8n so bile uspešno shranjene", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekti", + "objects processed" : "obdelanih objektov", + "or pick a range" : "ali izberite obseg", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "zagotavlja dobro ravnovesje med hitrostjo in natančnostjo.", + "register(s)" : "register(-i)", + "register(s) selected" : "register(-i) izbran(-i)", + "required" : "obvezno", + "results" : "rezultati", + "schema(s)" : "shema(-e)", + "schema(s) selected" : "shema(-e) izbrana(-e)", + "sk-..." : "sk-...", + "this application" : "to aplikacijo", + "to" : "do", + "vectorized" : "vektorizirano", + "views selected" : "izbranih pogledov", + "ℹ️ Current Configuration" : "ℹ️ Trenutna konfiguracija", + "⚡ Batch Processing" : "⚡ Paketna obdelava", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Celovit kontekst ✓ Manjša verjetnost, da spregledate podrobnosti ✗ Počasnejši odgovori ✗ Lahko vključuje manj relevantne informacije", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Hitrejši odgovori ✓ Bolj osredotočeni odgovori ✗ Lahko spregleda relevantne informacije", + "✨ AI Features" : "✨ Funkcije UI", + "All search trails cleared successfully" : "Vse iskalne sledi so bile uspešno počiščene", + "Multiple search trail deletion not implemented yet" : "Brisanje več iskalnih sledi še ni implementirano", + "No expired search trails found to clear" : "Najdena ni bila nobena potekla iskalna sled za čiščenje", + "Missing message" : "Manjkajoče sporočilo", + "message content is required" : "vsebina sporočila je obvezna", + "Missing conversation or agentUuid" : "Manjka pogovor ali agentUuid", + "Access denied" : "Dostop zavrnjen", + "Conversation not found" : "Pogovor ni najden", + "AI service not configured" : "Storitev UI ni konfigurirana", + "Failed to process message" : "Obdelava sporočila ni uspela", + "Missing conversationId" : "Manjka conversationId", + "conversationId is required" : "conversationId je obvezen", + "Failed to fetch conversation history" : "Pridobivanje zgodovine pogovora ni uspelo", + "Failed to clear conversation" : "Čiščenje pogovora ni uspelo", + "Invalid feedback type" : "Neveljavna vrsta povratnih informacij", + "type must be \"positive\" or \"negative\"" : "vrsta mora biti \"positive\" ali \"negative\"", + "Message not found" : "Sporočilo ni najdeno", + "Failed to save feedback" : "Shranjevanje povratnih informacij ni uspelo", + "Failed to get chat statistics" : "Pridobivanje statistike klepeta ni uspelo", + "Not Found" : "Ni najdeno", + "Engine not found" : "Pogon ni najden", + "Consumer not found" : "Potrošnik ni najden", + "Backend parameter is required" : "Parameter zaledja je obvezen", + "Failed to get database information: %s" : "Pridobivanje informacij o podatkovni bazi ni uspelo: %s", + "SOLR setup error: %s" : "Napaka pri nastavitvi SOLR: %s", + "Reindex failed: %s" : "Ponovno indeksiranje ni uspelo: %s", + "Anonymized" : "Anonimizirano", + "Entities detected" : "Zaznane entitete", + "Extract Now" : "Pridobi zdaj", + "Extracted at" : "Pridobljeno ob", + "Extraction" : "Pridobivanje", + "Failed to load extraction data" : "Nalaganje podatkov pridobivanja ni uspelo", + "Failed to load register data" : "Nalaganje podatkov registra ni uspelo", + "Failed to retrieve extraction status." : "Pridobivanje stanja pridobivanja ni uspelo.", + "Failed to retrieve objects for file." : "Pridobivanje objektov za datoteko ni uspelo.", + "No extraction data available for this file" : "Za to datoteko ni na voljo nobenih podatkov pridobivanja", + "No register objects reference this file" : "Te datoteke ne sklicuje noben objekt registra", + "Not extracted" : "Ni pridobljeno", + "Register Objects" : "Objekti registra", + "Risk level" : "Raven tveganja", + "Text chunks" : "Besedilni deli", + "Unknown error" : "Neznana napaka", + "Very high" : "Zelo visoko", + "{title} in {register} / {schema}" : "{title} v {register} / {schema}", + "Push Notifications" : "Potisna obvestila", + "Real-time push notification status via notify_push" : "Stanje potisnih obvestil v realnem času prek notify_push", + "Realtime push not available — the notify_push app is not installed" : "Potiskanje v realnem času ni na voljo – aplikacija notify_push ni nameščena", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Namestite aplikacijo notify_push iz trgovine z aplikacijami Nextcloud, da omogočite posodobitve v realnem času.", + "Open Nextcloud App Store" : "Odpri trgovino z aplikacijami Nextcloud", + "notify_push is installed but not yet active" : "notify_push je nameščen, vendar še ni aktiven", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push je nameščen, vendar OpenRegister še ni potrdil uspešnega potiskanja. Za aktivacijo sprožite shranjevanje objekta ali preverite svojo konfiguracijo notify_push.", + "notify_push configuration guide" : "vodnik za konfiguracijo notify_push", + "Realtime push active" : "Potiskanje v realnem času je aktivno", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Potisna obvestila v realnem času so aktivna. Povezani odjemalci prejmejo takojšnje posodobitve, ko so objekti ustvarjeni, posodobljeni ali izbrisani.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" +); diff --git a/l10n/sl.json b/l10n/sl.json new file mode 100644 index 0000000000..fa0f8f1b30 --- /dev/null +++ b/l10n/sl.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Potreben Dolphin)", + "(no subject)": "(brez zadeve)", + "(no title)": "(brez naslova)", + "(unnamed)": "(brez imena)", + "(untitled card)": "(kartica brez naslova)", + "({count} selected)": "({count} izbrano)", + ", {failed} failed": ", {failed} neuspešno", + "0 = unlimited": "0 = neomejeno", + "A deactivation request is pending.": "Zahteva za deaktivacijo je v čakanju.", + "AI Assistant": "AI asistent", + "AI Chat": "AI klepet", + "AI service not configured": "AI storitev ni konfigurirana", + "API Key": "API ključ", + "API Request Quota (requests/day)": "Kvota API zahteva (zahteva/dan)", + "API Token Configuration": "Konfiguracija API tokena", + "API Tokens": "API tokeni", + "API tokens saved successfully": "API tokeni uspešno shranjeni", + "AVG / Verwerkingsregister": "AVG / Register obdelave", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG čl. 30 §4: to poročilo povezuje vsako aktivnost obdelave s skupnim številom zapisa revizijskega traga ki joj se pripisuju. Revizori in Autoriteit Persoonsgegevens to se uporabljajo za nadzorni pregled.", + "About ConfigSets": "O ConfigSet stavkama", + "Access denied": "Dostop zavrnjen", + "Access organisation dashboard and analytics": "Dostop do nadzorne plošči in analitike organizacije", + "Account": "Račun", + "Action": "Dejanje", + "Action Distribution": "Porazdelitev dejanj", + "Action:": "Dejanje:", + "Actions": "Dejanja", + "Active": "Aktivno", + "Active Collections": "Aktivne zbirke", + "Active Filters": "Aktivni filtri", + "Active Organisation:": "Aktivna organizacija:", + "Active Organisations": "Aktivne organizacije", + "Active filters:": "Aktivni filtri:", + "Active organisation changed successfully": "Aktivna organizacija uspešno spremenjena", + "Active organisation set successfully": "Aktivna organizacija uspešno nastavljena", + "Activities": "Aktivnosti", + "Activity": "Aktivnost", + "Activity Period": "Obdobje aktivnosti", + "Add": "Dodaj", + "Add Attachment": "Dodaj priponko", + "Add Endpoint": "Dodaj končno točko", + "Add Groups": "Dodaj skupine", + "Add Property to '{title}'": "Dodaj lastnost v '{title}'", + "Add Register": "Dodaj register", + "Add Schema": "Dodaj shemu", + "Add Source": "Dodaj izvor", + "Add User": "Dodaj uporabnika", + "Add User to Organisation": "Dodaj uporabnika v organizacijo", + "Add a contact from any of your address books to associate it with this object.": "Dodajte kontakt iz bilo ki svojeg imenika da bi ga povezali s temi objektom.", + "Add a file or files": "Dodajte datoteko ali datoteke", + "Add card": "Dodaj karticu", + "Add contact": "Dodaj kontakt", + "Add endpoint": "Dodaj končno točko", + "Add oneOf entry": "Dodaj oneOf vnos", + "Add range": "Dodaj raspon", + "Add schema titles, descriptions, and register information to provide richer context for search": "Dodajte naslove shem, opise in informacije o registru da bi zagotovili bogatejši kontekst za iskanje", + "Add to favorites": "Dodaj v priljubljene", + "Added": "Dodano", + "Added to favorites": "Dodano v priljubljene", + "Additional Information": "Dodatne informacije", + "Additional feedback saved. Thank you!": "Dodatne povratne informacije spremljene. Hvala vam!", + "Addresses": "Adrese", + "Admin": "Administrator", + "Advanced": "Napredno", + "Advanced Filters": "Napredni filtri", + "Advanced Options": "Napredne zmožnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Napredni filtri niso dosegljivi z uporabo izvora baze podatkov. Preklopite na Auto ali SOLR indeks za zmožnosti filtriranja.", + "After": "Nakon", + "Agent Type": "Vrsta agenta", + "Agent deleted successfully": "Agent uspešno izbrisan", + "Agent successfully created": "Agent uspešno ustvarjen", + "Agent successfully updated": "Agent uspešno posodobljen", + "Agents": "Agenti", + "Aggregated across schemas": "Agregirano kroz sheme", + "All": "Vse", + "All Categories": "Vse kategorije", + "All Collections": "Vse zbirke", + "All Confidence Levels": "Vse ravni pouzdanosti", + "All ConfigSets": "Vsi ConfigSet-ovi", + "All Files": "Vse datoteke", + "All Levels": "Vse ravni", + "All Types": "Vse vrste", + "All Webhooks": "Vsi webhookovi", + "All actions": "Vse dejanja", + "All audit trails cleared successfully": "Vsi revizijski tragti uspešno izbrisani", + "All clear": "Vse čisto", + "All file types": "Vse vrste datoteka", + "All registers": "Vsi registri", + "All schemas": "Vse sheme", + "All search trails cleared successfully": "Vsi tragti iskanja uspešno izbrisani", + "All searches": "Vsa iskanja", + "All users": "Vsi uporabniki", + "All-day events": "Celodnevni dogadjaji", + "Allowed MIME Types": "Dovoljene MIME vrste", + "Already linked": "Več povezano", + "An error occurred while clearing audit trails": "Došlo je do napake prilikom brisanja revizijskih tragova", + "An error occurred while deleting the audit trail": "Došlo je do napake prilikom brisanja revizijskega traga", + "An error occurred while deleting the object": "Došlo je do napake prilikom brisanja objekta", + "An error occurred while deleting the view": "Došlo je do napake prilikom brisanja prikaza", + "An error occurred while permanently deleting the objects": "Došlo je do napake prilikom trajnog brisanja objekata", + "An error occurred while restoring the objects": "Došlo je do napake prilikom vračanja objekata", + "An error occurred while saving the organisation": "Došlo je do napake prilikom shranjevanja organizacije", + "Analysis completed:": "Analiza dokončana:", + "Analysis steps:": "Koraki analize:", + "Analytics": "Analitika", + "Analyze Objects": "Analiziraj objekte", + "Analyze Properties": "Analiziraj svojstva", + "Analyze existing properties for improvement opportunities": "Analizirajte postoječa lastnosti za zmožnosti poboljšanja", + "Analyzing...": "Analiziranje...", + "Anonymized": "Anonimizirano", + "Anonymous Group": "Anonimna skupina", + "Any favorites and sharing settings for this view": "Vse nastavitve favorita in deljenja za ta prikaz", + "Any user": "Kateri koli uporabnik", + "App ID": "ID aplikacije", + "App store cache invalidated successfully": "Predpomnilnik trgovine aplikacija uspešno poništena", + "App store cache invalidated: {invalidated}": "Predpomnilnik trgovine aplikacija poništena: {invalidated}", + "Application deleted successfully": "Aplikacija uspešno izbrisana", + "Application identifier for this configuration (optional)": "Identifikator aplikacije za to konfiguraciju (neobvezno)", + "Application settings": "Nastavitve aplikacije", + "Application successfully created": "Aplikacija uspešno ustvarjena", + "Application successfully updated": "Aplikacija uspešno posodobljena", + "Applications": "Aplikacije", + "Apply": "Uporabi", + "Apply Changes": "Uporabi spremembe", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Uporabite ulogu na vse sheme v tem registru ki nemaju eksplicitna preglasovanj avtorizacije.", + "Approval Progress": "Napredek odobravanja", + "Approve": "Odobri", + "Approved": "Odobreno", + "Archive": "Arhiviraj", + "Archive conversation": "Arhiviraj pogovor", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arhivirati to aktivnost obdelave? Reci revizijskega traga nastavit če je referencirati.", + "Archived conversations are hidden from your active list": "Arhivirani pogovori so skriti na vašem aktivnem seznamu", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Ste prepričani, da želite IZBRISATI zbirku \"{name}\"?\n\nTo bo:\n• Trajno izbrisati zbirku in vse njene podatke\n• Odstraniti vse indeksirane dokumente\n• Ta se dejanje ne lahko poništiti", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Ste prepričani, da želite očistiti stare tragte iskanja? To bo izbrisati vnose starije od 30 dana.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Ste prepričani, da želite izbrisati vse podatke iz zbirke \"{name}\"?\n\nTo bo:\n• Izbrisati vse indeksirane dokumente\n• Zadržati strukturu zbirke netaknutom\n• Ta se dejanje ne lahko poništiti", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Ste prepričani, da želite izbrisati aplikaciju \"{name}\"? Ta se dejanje ne lahko poništiti.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Ste prepričani, da želite izbrisati izbrane revizijske tragove? Ta se dejanje ne lahko poništiti.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Ste prepričani, da želite izbrisati izbrane tragte iskanja? Ta se dejanje ne lahko poništiti.", + "Are you sure you want to delete this ConfigSet?": "Ste prepričani, da želite izbrisati ta ConfigSet?", + "Are you sure you want to delete this configset?": "Ste prepričani, da želite izbrisati ta configset?", + "Are you sure you want to leave '{name}'?": "Ste prepričani, da želite napustiti '{name}'?", + "Are you sure you want to permanently delete": "Ste prepričani, da želite trajno izbrisati", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Ste prepričani, da želite ponovno indeksirati zbirku \"{name}\"?\n\nTo bo:\n• Ponovno izgraditi indeks sa vsim objektima\n• Potrajati nekoliko minuta do dovršetka\n• Možda utjecati na performanse iskanja tijekom ponovnog indeksiranja", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Ste prepričani, da želite odstraniti shemu \"{schema}\" iz registra \"{register}\"? Ta se dejanje ne lahko poništiti.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Ste prepričani, da želite mehko izbrisati {count} aktivnih objekata za shemu \"{schema}\"?\n\nBo označeni kot izbrisani, ali se lahko trajno odstraniti kasni.", + "Array Object Configuration:": "Konfiguracija objekta polja:", + "Articles": "Članki", + "Ask a question...": "Postavite vprašanje...", + "Ask questions about your data using natural language": "Zastavljajte vprašanja o svojim podatkov z uporabo naravni jezik", + "Assigned collaborative tags": "Dodeljene sodelovalne oznake", + "Assignment notifications": "Obvestiloi o dodjeli", + "Attachment added to {name}": "Priponka dodan v {name}", + "Attempt": "Pokušaj", + "Audit Trail Changes": "Spremembe revizijskega traga", + "Audit Trail Details": "Podrobnosti revizijskega traga", + "Audit Trail Management": "Upravljanje revizijskim tragom", + "Audit Trail Statistics": "Statistika revizijskega traga", + "Audit Trails": "Revizijski tragovi", + "Audit trail data copied to clipboard": "Podatki revizijskega traga kopirani u medjuspremnik", + "Audit trail deleted successfully": "Revizijski trag uspešno izbrisan", + "Audit trail successfully deleted": "Revizijski trag uspešno izbrisan", + "Authentication": "Avtentikacija", + "Auto (e.g. Y for year, F Y for month)": "Auto (npr. Y za godinu, F Y za mjesec)", + "Auto share": "Samodejno deljenje", + "Auto-commit disabled": "Samodejna potrditev onemogočena", + "Auto-commit enabled": "Samodejna potrditev omogočena", + "Auto-create Default Organisation": "Samodejno ustvari privzeto organizacijo", + "Auto-retry failed vectorizations": "Samodejno ponovno pokušaj neuspjele vektorizacije", + "Automatically commit changes to SOLR index": "Samodejno potrdi spremembe v SOLR indeks", + "Automatically connect to the federation network.": "Samodejno se poveži na federacijsko omrežje.", + "Automatically create a default organisation if none exists when the app is initialized": "Samodejno ustvari privzeto organizacijo ako nobena ne obstaja ko se aplikacija inicijalizira", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Samodejno generiraj vektorske vdelave iz besedilnih kosov, ko so datoteke naložene in obdelane", + "Automatically generate vector embeddings when objects are created or updated": "Samodejno generiraj vektorske vdelave ko se objekti stvore ali posodobiu", + "Automatically retry failed vectorization attempts (max 3 retries)": "Samodejno ponovno pokušaj neuspjele pokušaje vektorizacije (največ 3 ponovna pokušaja)", + "Automatically update catalogues.": "Samodejno posodobi kataloge.", + "Available Filters": "Dosegljivi filtri", + "Available Workflows": "Dosegljivi poteki dela", + "Available placeholders:": "Dosegljivi rezervirani znakovi:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Vaš ponudnik avtentikacije ne podpira spremembe avatara.", + "Avatar removed": "Avatar uklonjen", + "Avatar updated successfully": "Avatar uspešno posodobljen", + "Avg Execution Time": "Prosj. vrijeme izvršavanja", + "Avg Members/Org": "Prosj. članova/org.", + "Avg Object Views/Session": "Prosj. pregleda objekata/sesiji", + "Avg Response Time": "Prosj. vrijeme odgovora", + "Avg Results/Search": "Prosj. rezultata/iskanju", + "Avg Searches/Session": "Prosj. iskanja/sesiji", + "Back": "Nazaj", + "Back to Entities": "Nazaj na entitete", + "Back to Registers": "Nazaj na registre", + "Back to Webhooks": "Nazaj na webhookov", + "Back to applications": "Nazaj na aplikacije", + "Back to entities": "Nazaj na entitete", + "Backend parameter is required": "Parametar zalednega sistema je obvezen", + "Backend updated successfully. Please reload the application.": "Zaledni sistem uspešno posodobljen. Prosimo ponovno naložite aplikaciju.", + "Bandwidth Quota (MB/month)": "Kvota propusnosti (MB/mjesec)", + "Base URL (Optional)": "Osnovni URL (neobvezno)", + "Basic Information": "Osnovne informacije", + "Batch Size": "Velikost serije", + "Batch extraction completed": "Serijska ekstrakcija dokončana", + "Before": "Prije", + "Before object vectorization can work:": "Preden vektorizacija objektov lahko deluje:", + "Behavior": "Vedenje", + "Behavior Issue": "Problem s vedenjem", + "Beschrijving": "Opis", + "Bewaartermijn": "Rok hramba", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Rok hramba (ISO-8601 trajanje, npr. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob shramba je bila umaknjena. Vsi objekti zdaj uporabljajo magične tabele.", + "Bookmarks": "Knjižne oznake", + "Branch": "Veja", + "Bulk Role Assignment: {title}": "Skupna dodelitev vloga: {title}", + "Bulk delete operation completed successfully": "Operacija skupnog brisanja uspešno dokončana", + "Bulk save operation completed successfully": "Operacija skupnog shranjevanja uspešno dokončana", + "Business Data": "Poslovni podatki", + "CSV": "CSV", + "Cache cleared successfully": "Predpomnilnik uspešno izbrisana", + "Cache grew from {old} to {new} entries.": "Predpomnilnik je narasla s {old} na {new} vnosa.", + "Cache shrunk from {old} to {new} entries.": "Predpomnilnik se smanjšila s {old} na {new} vnosa.", + "Cache stayed the same at {size} entries.": "Predpomnilnik je ostala ista na {size} vnosa.", + "Calculate Sizes": "Izračunaj velikosti", + "Calendar": "Kalendar", + "Calendar Provider Configuration": "Konfiguracija ponudnika kalendara", + "Calendar integration is not available": "Integracija kalendara ni dosegljiva", + "Calendar name": "Naziv kalendara", + "Cancel": "Prekliči", + "Cancel deactivation request": "Prekliči zahteva za deaktivaciju", + "Cannot delete: objects are still attached": "Ni moguče izbrisati: objekti so še vedno priloženi", + "Cannot edit: This register is managed by external configuration {title}": "Ni moguče urejati: temi registrom upravlja zunanja konfiguracija {title}", + "Cannot remove schema with existing objects ({count} objects)": "Ni moguče odstraniti shemu s postoječim objektima ({count} objekata)", + "Cannot remove schema {schema} because it contains {count} objects": "Ni moguče odstraniti shemu {schema} jer sadrži {count} objekata", + "Cards": "Kartice", + "Cascade delete": "Kaskadno brisanje", + "Categories of data subjects (one per line)": "Kategorije posameznikov (ena na vrstico)", + "Categories of personal data (one per line)": "Kategorije osobnih podatkov (ena na vrstico)", + "Categorieën betrokkenen (één per regel)": "Kategorije posameznikov (ena na vrstico)", + "Categorieën persoonsgegevens (één per regel)": "Kategorije osobnih podatkov (ena na vrstico)", + "Category": "Kategorija", + "Change Type": "Vrsta spremembe", + "Change password": "Promijeni lozinku", + "Changes": "Spremembe", + "Changes copied to clipboard": "Spremembe kopirane u medjuspremnik", + "Changes for Audit Trail #{id}": "Spremembe za revizijski trag #{id}", + "Chat Model": "Model klepeta", + "Chat Provider": "Ponudnik klepeta", + "Chat Provider (RAG)": "Ponudnik klepeta (RAG)", + "Chat Settings": "Nastavitve klepeta", + "Chat provider connection successful!": "Veza s ponudnikom klepeta uspešna!", + "Chat settings": "Nastavitve klepeta", + "Chat test failed: {error}": "Test klepeta neuspešen: {error}", + "Choose a register": "Izberite register", + "Choose a schema": "Izberite shemu", + "Choose how vector similarity calculations are performed for semantic search": "Izberite kako se izračuni vektorske podobnosti izvode za semantično iskanje", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Izberite katere vrste datotek vključiti v proces vektorizacije. Obdelane bodo le datoteke z določenimi tekstom in kosovima.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Izberite katere prikaze vključiti v proces vektorizacije. Ostavite prazno za obradu vsih prikaza na temelju vaše konfiguracije.", + "Chunk": "Dio", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Brisanje kosov še ni implementirano. Uporabite končne točke temeljene na kosovima.", + "Chunks": "Dijelovi", + "Chunks to Vectorize:": "Kosi za vektorizacijo:", + "Cleanup Old Trails": "Očisti stare tragove", + "Cleanup completed": "Čiščenje dokončano", + "Cleanup completed successfully. Deleted {count} entries.": "Čiščenje uspešno dokončano. Izbrisano {count} vnosa.", + "Cleanup failed: {error}": "Čiščenje neuspešno: {error}", + "Clear All Embeddings": "Izbriši vsa vdelava", + "Clear Entries": "Izbriši vnose", + "Clear Filtered Audit Trails": "Izbriši filtrirane revizijske tragove", + "Clear Filters": "Izbriši filtre", + "Clear Index": "Izbriši indeks", + "Clear Selection": "Izbriši izbor", + "Clear all": "Izbriši vse", + "Clear all filters": "Izbriši vse filtre", + "Clear date range": "Izbriši raspon datuma", + "Clear filters": "Izbriši filtre", + "Clearing...": "Brisanje...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Kliknite Zaženi preverjanje skladnosti da bi našli sheme u ki je otkriven PII, ali ne obstaja bilješka o aktivnosti obdelave.", + "Click to subscribe to notifications": "Kliknite za pretplatu na obvestiloi", + "Click to unsubscribe from notifications": "Kliknite za odjavu od obvestiloi", + "Close": "Zatvori", + "Cluster name": "Naziv gruče", + "Code": "Kod", + "Code (short readable key, e.g. v-2026-001)": "Kod (kratki čitljivi ključ, npr. v-2026-001)", + "Collapse details": "Strni podrobnosti", + "Collection Management": "Upravljanje zbirkam", + "Collection Name": "Naziv zbirke", + "Collection assignments updated successfully": "Dodelitve zbirki uspešno posodobljene", + "Collection cleared successfully": "Zbirka uspešno izbrisana", + "Collection copied successfully": "Zbirka uspešno kopirana", + "Collection created successfully": "Zbirka uspešno ustvarjena", + "Collection deleted successfully": "Zbirka uspešno izbrisana", + "Collection used to store and index file metadata and content": "Zbirka ki se se uporablja za shranjevanje in indeksiranje metapodatkov in vsebine datoteka", + "Collection used to store and index object data": "Zbirka ki se se uporablja za shranjevanje in indeksiranje podatkov objekata", + "Collections:": "Zbirke:", + "Color": "Boja", + "Comment...": "Komentar...", + "Commit Message": "Počasenočilo potrditve", + "Commit Within (ms)": "Potrdi znotraj (ms)", + "Commit message": "Počasenočilo potrditve", + "Compare current schema with real object data": "Upočasenedi trenutnv shemo sa stvarnim podatkov objekata", + "Completed": "Dokončano", + "Complex": "Složeno", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Složeni poizvedbai: napredna iskanja s več filtera, operatora in složenih kombinacija parametrov", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Složeni poizvedbai: napredna iskanja s več filtera, operatora in složenih kombinacija parametrov", + "Compliance": "Uskladjenost", + "Confidence Level": "Raven pouzdanosti", + "Confidence Score": "Rezultat pouzdanosti", + "Confidence: {confidence}%": "Pouzdanost: {confidence}%", + "Config must be provided as an object": "Konfiguracija mora biti navedena kot objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Upravljanje ConfigSet stavkama", + "ConfigSet Name": "Naziv ConfigSet stavke", + "ConfigSet created successfully": "ConfigSet uspešno ustvarjen", + "ConfigSet deleted successfully": "ConfigSet uspešno izbrisan", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSet stavke določajo shemu in konfiguraciju za vaše SOLR zbirke. Sadrže definicije polja, analizatore in druge nastavitve iskanja.", + "Configuration": "Konfiguracija", + "Configuration Data": "Podatki konfiguracije", + "Configuration is up to date": "Konfiguracija je ažurna", + "Configuration saved": "Konfiguracija spremljena", + "Configuration saved successfully.": "Konfiguracija uspešno spremljena.", + "Configuration type (default, application, etc.)": "Vrsta konfiguracije (privzeta, aplikacija itd.)", + "Configurations": "Konfiguracije", + "Configure API tokens for external service integrations": "Konfigurirajte API tokene za integracije zunanjih usluga", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurirajte Apache SOLR iskalnik za napredne zmožnosti iskanja", + "Configure Facets": "Konfiguriraj fasete", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurirajte ponudnike velikih jezikovnih modelov (LLM) za funkcije z umetno inteligenco, vključno s semantičnim iskanjem, vdelavami in klepetom.", + "Configure SOLR Facets": "Konfiguriraj SOLR fasete", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurirajte osnovne nastavitve veze za vaš SOLR strežnik vključno z autentifikaciju in mrežne zmožnosti. Uporabite zasebne dijaloge za upravljanje ConfigSet stavkama in zbirkam za upravljanje jezgrama in zbirkam.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurirajte osnovne nastavitve veze za vaš SOLR strežnik vključno z autentifikaciju in mrežne zmožnosti. Uporabite zasebne dijaloge za upravljanje configset stavkama in zbirkam za upravljanje jezgrama in zbirkam.", + "Configure default organisation and organisation-related settings": "Konfigurirajte privzeto organizacijo in nastavitve vezane uz organizacijo", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurirajte kako se objekti baze podatkov pretvarjajo u vektorske vdelave za semantično iskanje. Objekti se izravno vektoriziraju brez potrebe za ekstrakcijom teksta.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurirajte kako se objekti pretvarjajo u tekst pred vektorizacije. Te nastavitve utječu na kvalitetu in kontekst iskanja.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurirajte ponudnike velikih jezikovnih modelov (LLM) za funkcije z umetno inteligenco, vključno s semantičnim iskanjem, vdelavami in klepetom.", + "Configure multi-organization support and tenant isolation": "Konfigurirajte podršku za več organizacija in izolaciju zakupaca", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurirajte parametre za vektorizacijo datoteka. Ta če proces generirati vektorske vdelave za vse izvučene dijelte datoteka.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurirajte parametre za vektorizacijo objekata. Ta če proces generirati vektorske vdelave za vse objekte ki odgovaraju filtrima vašeg prikaza.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurirajte to shemu da prikazuje objekte kot dogadjaje v aplikaciji Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurirajte ki vrste podatkov iskati in koliko izvora pridobiti", + "Configuring field mappings...": "Konfiguriranje mapiranja polja...", + "Confirm Account Deactivation": "Potrdi deaktivaciju računa", + "Confirm Object Deletion": "Potrdi brisanje objekta", + "Confirm Object Restoration": "Potrdi vračanje objekta", + "Confirm Permanent Object Deletion": "Potrdi trajno brisanje objekta", + "Confirm deactivation": "Potrdi deaktivaciju", + "Connecting to SOLR cluster...": "Povezovanje s SOLR gručo...", + "Connection Failed": "Veza neuspešna", + "Connection Settings": "Nastavitve veze", + "Connection Successful!": "Veza uspešna!", + "Connection failed": "Veza neuspešna", + "Connection protocol": "Protokol veze", + "Connection successful": "Veza uspešna", + "Connection timeout in seconds": "Potek veze u sekundama", + "Constant delay between retries (5 minutes)": "Stalni odgoda izmedju ponovnih pokušaja (5 minuta)", + "Constraint Issue": "Problem s ovejičenjem", + "Consumer not found": "Potrošač ni najden", + "Contacts": "Kontakti", + "Contacts integration is not available": "Integracija kontakata ni dosegljiva", + "Content Filters": "Filtri vsebine", + "Continue to Properties": "Nastavi na svojstva", + "Contracts": "Ugovori", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Nadzirajte, kateri prikazi objektov je treba vektorizirati zaradi zmanjšanja stroška API-ja. Vektorizirajte le prikaze ki imajo se uporablja od semantičnega iskanja.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Nadzirajte, katere poglede in orodja lahko umetna inteligenca uporablja v tem pogovoru. Privzeto so vse zmožnosti agenta omogočene.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontrolira nasumičnost (0 = determinističko, 2 = vrlo kreativno)", + "Conversation ID is required": "ID klepeta je obvezen", + "Conversation archived": "Klepet arhiviran", + "Conversation archived successfully": "Klepet uspešno arhiviran", + "Conversation cleared successfully": "Klepet uspešno obrisan", + "Conversation deleted": "Klepet izbrisan", + "Conversation not found": "Klepet ni najden", + "Conversation permanently deleted": "Pogovor trajno izbrisan", + "Conversation renamed": "Klepet preimenovan", + "Conversation restored": "Klepet vračen", + "Conversation started with {agent}": "Klepet započet s {agent}", + "Conversation title": "Naslov klepeta", + "Conversations": "Klepeti", + "Converting {schema} to blob storage...": "Pretvarjanje {schema} u blob shranjevanje...", + "Converting {schema} to magic table...": "Pretvarjanje {schema} u magičnu tablicu...", + "Copied!": "Kopirano!", + "Copy": "Kopiraj", + "Copy Changes": "Kopiraj spremembe", + "Copy Collection": "Kopiraj zbirku", + "Copy Data": "Kopiraj podatke", + "Copy Full Data": "Kopiraj vse podatke", + "Copy of {name}": "Kopija od {name}", + "Copy to clipboard": "Kopiraj u medjuspremnik", + "Copying...": "Kopiranje...", + "Costs": "Stroški", + "Count": "Število", + "Counting objects...": "Številoanje objekata...", + "Create": "Ustvari", + "Create API Token": "Ustvari API token", + "Create Agent": "Ustvari agenta", + "Create Application": "Ustvari aplikaciju", + "Create Collection": "Ustvari zbirku", + "Create ConfigSet": "Ustvari ConfigSet", + "Create New Collection": "Ustvari novo zbirku", + "Create New ConfigSet": "Ustvari nti ConfigSet", + "Create Operations": "Ustvari operacije", + "Create Organisation": "Ustvari organizacijo", + "Create Webhook": "Ustvari webhook", + "Create a copy of collection:": "Ustvari kopiju zbirke:", + "Create a new ConfigSet based on the _default template": "Ustvari nti ConfigSet na temelju predloge _default", + "Create a new SOLR collection from an existing ConfigSet": "Ustvari novo SOLR zbirku iz postoječeg ConfigSet-a", + "Create a new SOLR collection from an existing configset": "Ustvari novo SOLR zbirku iz postoječeg configset-a", + "Create a new configset based on the _default template": "Ustvari nti configset na temelju predloge _default", + "Create a new event or link an existing one from any of your calendars.": "Ustvarite nti dogadjaj ali povežite postoječi iz bilo ki svojeg kalendara.", + "Create another": "Ustvari še jedan", + "Create copies of": "Ustvari kopije od", + "Create event": "Ustvari dogadjaj", + "Create new token": "Ustvari nti token", + "Create or link a Deck card to track work on this object.": "Ustvarite ali povežite Deck karticu za pračenje rada na tem objektu.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Ustvarite prvu aktivnost obdelave da bi počeli označavati vrstice revizijskega traga njihtem AVG čl. 30 atribucijom.", + "Create your first AI agent to get started.": "Ustvarite svojeg prvog AI agenta za početak.", + "Created": "Ustvarjeno", + "Created:": "Ustvarjeno:", + "Creating search collections...": "Ustvarjanje zbirki iskanja...", + "Creating...": "Ustvarjanje...", + "Current": "Trenutno", + "Current Filters": "Trenutni filtri", + "Current Type:": "Trenutna vrsta:", + "Current password": "Trenutna lozinka", + "Custom API endpoint if using a different region": "Prilagojena API končna točka ako uporabite drugu regiju", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Prilagojena HTTP glave (eno na vrstico, format: Header-Name: vrednost)", + "Custom HTTP headers (one per line, format: header-name: value)": "Prilagojena HTTP glave (eno na vrstico, format: header-name: vrednost)", + "Custom Ranges:": "Prilagojeni rasponi:", + "Custom display title for this facet": "Prilagojeni prikazni naslov za to fasetu", + "Custom pattern:": "Prilagojeni uzorak:", + "DELETE COLLECTION": "IZBRIŠI ZBIRKU", + "DSAR": "DSAR", + "Daily": "Dnevno", + "Dashboard": "Nadzorna plošča", + "Dashboard not found": "Nadzorna plošča ni najdena", + "Data": "Podatki", + "Data Source": "Izvor podatkov", + "Data Sources": "Izvori podatkov", + "Data Views": "Prikazi podatkov", + "Data sources": "Izvori podatkov", + "Data type does not match observed values": "Vrsta podatkov se ne ujema opazevanim vrednostima", + "Data-subject access request": "Zahteva posameznikov za dostop", + "Database URL": "URL baze podatkov", + "Database information refreshed": "Informacije o bazi podatkov osvježene", + "Date Faceting:": "Fasetiranje datuma:", + "Date Range": "Raspon datuma", + "Dates": "Datumi", + "Deactivation request cancelled": "Zahteva za deaktivacijo otkazan", + "Deactivation request submitted": "Zahteva za deaktivacijo podnesen", + "Deck": "Deck", + "Deck integration is not available": "Deck integracija ni dosegljiva", + "Default": "Privzeto", + "Default Object Tenant": "Privzeti zakupac objekta", + "Default Organisation": "Privzeta organizacija", + "Default Owner": "Privzeti vlasnik", + "Default User (for cron/background jobs)": "Privzeti uporabnik (za cron/pozadinske poslove)", + "Default User Tenant": "Privzeti zakupac uporabnika", + "Default value": "Privzeta vrednost", + "Default value (Email)": "Privzeta vrednost (e-pošta)", + "Default value (Password)": "Privzeta vrednost (lozinka)", + "Default value (Phone number)": "Privzeta vrednost (telefonski število)", + "Default value (Regex)": "Privzeta vrednost (regularni izraz)", + "Default: 5": "Privzeto: 5", + "Defaults to current user. Select a different user if needed.": "Privzeto na trenutnog uporabnika. Izberite drugog uporabnika ako je potrebno.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Odgode se udvostručuju sa vsakim pokušajem (2, 4, 8 minuta...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Odgode se povečavaju linearno (5, 10, 15 minuta...)", + "Delete": "Izbriši", + "Delete ({count})": "Izbriši ({count})", + "Delete Application": "Izbriši aplikaciju", + "Delete Audit Trail": "Izbriši revizijski trag", + "Delete Collection": "Izbriši zbirku", + "Delete ConfigSet": "Izbriši ConfigSet", + "Delete Objects": "Izbriši objekte", + "Delete Operations": "Izbriši operacije", + "Delete SOLR Collection": "Izbriši SOLR zbirku", + "Delete View": "Izbriši prikaz", + "Delete all objects in this schema": "Izbriši vse objekte u ovoj shemi", + "Delete permanently": "Izbriši trajno", + "Delete this conversation?": "Izbrisati ta klepet?", + "Delete view": "Izbriši prikaz", + "Delete {count} object": "Izbriši {count} objekt", + "Delete {count} objects": "Izbriši {count} objekata", + "Deleted": "Izbrisano", + "Deleted By": "Izbrisao", + "Deleted Date": "Datum brisanja", + "Deleted Items Management": "Upravljanje izbrisanim stavkama", + "Deleted This Week": "Izbrisano ta tjedan", + "Deleted Today": "Izbrisano danas", + "Deleted:": "Izbrisano:", + "Deleting SOLR Collection...": "Brisanje SOLR zbirke...", + "Deleting...": "Brisanje...", + "Deletion Date Range": "Raspon datuma brisanja", + "Deletion Statistics": "Statistika brisanja", + "Deprecated": "Zastarelo", + "Depublish": "Poništi objavu", + "Description": "Opis", + "Description Template": "Predloga opisa", + "Description shown as tooltip": "Opis prikazan kot opis alata", + "Description:": "Opis:", + "Detect data types and patterns": "Otkrij vrste in uzorke podatkov", + "Detected At": "Odkrito u", + "Detected Format:": "Odkriti format:", + "Detected Issues:": "Odkriti problemi:", + "Detected: {date}": "Odkrito: {date}", + "Direct permission": "Izravno dopuštenje", + "Disable": "Onemoguči", + "Disable this to make the agent": "Onemogočite to kako bi agent", + "Disabled": "Onemogočeno", + "Discover Files": "Otkrij datoteke", + "Discovered Properties": "Otkrivena svojstva", + "Display Format": "Format prikaza", + "Display Name": "Prikazni naziv", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Želite li trajno izbrisati vse filtrirane vnose revizijskega traga? Ta se dejanje ne lahko poništiti.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Želite li trajno izbrisati ta vnos revizijskega traga? Ta se dejanje ne lahko poništiti.", + "Documents": "Dokumenti", + "Doelbinding *": "Vezanost svrhe *", + "Dolphin OCR enabled": "Dolphin OCR omogučen", + "Download API Spec": "Prenesi API specifikaciju", + "Download API Specification": "Prenesi API specifikaciju", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Prenesite kopiju vsih svojih osobnih podatkov pohranjenih u OpenRegister (GDPR članek 20.).", + "Drag a file or files here": "Povucite datoteku ali datoteke ovdje", + "Dry run completed: {count} fields would be created": "Probno izvodjenje dokončano: {count} polja bilo bi ustvarjeno", + "Dry run completed: {count} fields would be fixed": "Probno izvodjenje dokončano: {count} polja bilo bi popravljeno", + "Duration in seconds (optional)": "Trajanje u sekundama (neobvezno)", + "Edit": "Uredi", + "Edit Agent": "Uredi agenta", + "Edit Application": "Uredi aplikaciju", + "Edit Configuration": "Uredi konfiguraciju", + "Edit Endpoint": "Uredi končno točko", + "Edit Organisation": "Uredi organizacijo", + "Edit Property '{key}' of '{title}'": "Uredi lastnost '{key}' od '{title}'", + "Edit Register": "Uredi register", + "Edit Schema": "Uredi shemu", + "Edit Source": "Uredi izvor", + "Edit View": "Uredi prikaz", + "Edit Webhook": "Uredi webhook", + "Edit labels": "Uredi oznake", + "Edit processing activity": "Uredi aktivnost obdelave", + "Edit tags for {name}": "Uredi oznake za {name}", + "Edit verwerkingsactiviteit": "Uredi aktivnost obdelave", + "Edit view": "Uredi prikaz", + "Edit view details": "Uredi podrobnosti prikaza", + "Effectiveness": "Učinkovitost", + "Email": "E-pošta", + "Email Addresses": "Adrese e-pošte", + "Email digest frequency": "Učestalost sažetka e-pošte", + "Emails": "E-pošte", + "Embedding Model": "Model vdelava", + "Embedding Provider": "Ponudnik vdelava", + "Embedding generated successfully": "Vdelava uspešno generirano", + "Embedding provider connection successful!": "Veza s ponudnikom vdelava uspešna!", + "Embedding test failed: {error}": "Test vdelava neuspešen: {error}", + "Enable": "Omoguči", + "Enable RAG": "Omogoči RAG", + "Enable auto-creation": "Omogoči samodejno ustvarjanje", + "Enable automatic file vectorization": "Omogoči automatsku vektorizaciju datoteka", + "Enable automatic object vectorization": "Omogoči automatsku vektorizaciju objekata", + "Enable automatic synchronization": "Omogoči automatsku sinkronizaciju", + "Enable calendar provider": "Omogoči ponudnika kalendara", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Omogoči detaljno bilježenje za SOLR operacije (preporučeno za otklanjanje pogrešaka)", + "Enable faceting": "Omogoči fasetiranje", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Omogočite ali onemogočite LLM funkcije. Konfigurirajte ponudnike in modele z uporabo gumb LLM konfiguracija iznad.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Omogočite ali onemogočite LLM funkcije. Konfigurirajte ponudnike in modele z uporabo gumb LLM konfiguracija iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Omogočite ali onemogočite integracijv SOLR iskanja. Konfigurirajte nastavitve veze z uporabo gumb Nastavitve veze iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Omogočite ali onemogočite integracijv SOLR iskanja. Konfigurirajte nastavitve veze z uporabo gumb nastavitve veze iznad.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Omogočite ali onemogočite integraciju n8n poteka dela. Konfigurirajte nastavitve veze ispod.", + "Enable or disable this webhook": "Omogoči ali onemoguči ta webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Omogočite alate ki agentu omogučuju interakciju s podatkov prek klicanja funkcija.", + "Enable vectorization for all existing and future views (may increase costs)": "Omogoči vektorizaciju za vse postoječe in buduče prikaze (lahko povečati troškove)", + "Enable write-back to target objects": "Omogoči upisivanje nazaj u ciljne objekte", + "Enabled": "Omogočeno", + "End Date Field": "Polje datuma završetka", + "Endpoint Path*": "Putanja končne točke*", + "Endpoints": "Končne točke", + "Engine not found": "Mehanizam ni najden", + "Enter ConfigSet name": "Vnesite naziv ConfigSet stavke", + "Enter Nextcloud usernames to grant access to this private agent": "Vnesite Nextcloud korisnička imena za dodelitev dostopa tem privatnom agentu", + "Enter agent description (optional)": "Vnesite opis agenta (neobvezno)", + "Enter agent name": "Vnesite naziv agenta", + "Enter application description (optional)": "Vnesite opis aplikacije (neobvezno)", + "Enter application name": "Vnesite naziv aplikacije", + "Enter collection name": "Vnesite naziv zbirke", + "Enter configuration description (optional)": "Vnesite opis konfiguracije (neobvezno)", + "Enter configuration title": "Vnesite naslov konfiguracije", + "Enter description (optional)...": "Vnesite opis (neobvezno)...", + "Enter new collection name": "Vnesite nti naziv zbirke", + "Enter object ID": "Vnesite ID objekta", + "Enter organisation description (optional)": "Vnesite opis organizacije (neobvezno)", + "Enter organisation name": "Vnesite naziv organizacije", + "Enter search term": "Vnesite pojam za iskanje", + "Enter search terms or leave empty to browse all": "Vnesite pojmte za iskanje ali ostavite prazno za pregledavanje vsega", + "Enter system prompt for the agent": "Vnesite sistemski poizvedba za agenta", + "Enter username and press Enter": "Vnesite uporabniško ime in pritisnite Enter", + "Enter view name...": "Vnesite naziv prikaza...", + "Enter webhook name": "Vnesite naziv webhooka", + "Enter your API key": "Vnesite svoj API ključ", + "Enter your object here...": "Vnesite svoj objekt tukaj...", + "Enter your schema here...": "Vnesite svojv shemo tukaj...", + "Entities": "Entitetei", + "Entities detected": "Odkriti entitetei", + "Entity ID: {id}": "ID entitetea: {id}", + "Entity Information": "Informacije o entiteteu", + "Entity Recognition": "Prepoznavanje entitetea", + "Entity Recognition Method": "Metoda prepoznavanja entitetea", + "Entity deleted successfully": "Entitete je uspešno izbrisan", + "Entity not found": "Entitete ni najden", + "Entries to be deleted:": "Vnosi ki če biti izbrisani:", + "Enum Issue": "Problem s enumeracijom", + "Enum constraint is missing": "Nedostaje ovejičenje enumeracije", + "Erase (Art 17)": "Brisanje (čl. 17.)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Izbrisati {count} objekt(a) za ta subjekt? Ta dejanje bilježi se u revizijskom tragu.", + "Erasure complete": "Brisanje dokončano", + "Erasure preview": "Pregled brisanja", + "Error": "Napaka", + "Error Details": "Podrobnosti o napaki", + "Error Information": "Informacije o napaki", + "Error Message": "Počasenočilo o napaki", + "Error deleting audit trails: {error}": "Napaka pri brisanju revizijskih tragova: {error}", + "Error deleting search trails: {error}": "Napaka pri brisanju tragta iskanja: {error}", + "Error loading application": "Napaka pri nalaganju aplikacije", + "Error loading audit trails": "Napaka pri nalaganju revizijskih tragova", + "Error loading entity": "Napaka pri nalaganju entitetea", + "Error loading search trails": "Napaka pri nalaganju tragta iskanja", + "Estimated Batches:": "Procijenjeni paketi:", + "Estimated Cost:": "Procijenjeni strošek:", + "Estimated Duration:": "Procijenjeno trajanje:", + "Event": "Dogadjaj", + "Event Property for Payload": "Lastnost dogadjaja za korisni teret", + "Events": "Dogadjaji", + "Every schema with detected PII has a processing-activity annotation.": "Vsaka shema s odkritim PII ima napomenu o aktivnosti obdelave.", + "Example": "Primjer", + "Example Value": "Primjer vrednosti", + "Example value for this property": "Primjer vrednosti za to svojstvo", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Isključivi maksimum", + "Exclusive Minimum": "Isključivi minimum", + "Exclusive maximum": "Isključivi maksimum", + "Exclusive minimum": "Isključivi minimum", + "Execution Mode": "Način izvodjenja", + "Execution Time": "Vrijeme izvodjenja", + "Execution Time Range": "Raspon vremena izvodjenja", + "Existing Improvements": "Postoječa poboljšanja", + "Expand details": "Razširi podrobnosti", + "Expiration": "Istek", + "Expires": "Istječe", + "Expires in (e.g., 90d)": "Istječe za (npr. 90d)", + "Exponential": "Eksponencijalno", + "Export": "Izvoz", + "Export \"{schema}\" objects from \"{register}\"": "Izvezi objekte \"{schema}\" iz \"{register}\"", + "Export Configuration": "Konfiguracija izvoza", + "Export Format:": "Format izvoza:", + "Export Objects": "Izvoz objekata", + "Export completed successfully": "Izvoz je uspešno dovršen", + "Export downloaded successfully": "Izvoz je uspešno preuzet", + "Export failed: {error}": "Izvoz ni uspio: {error}", + "Export is rate limited. Please try again later.": "Izvoz je ovejičen po stopi. Pokušajte ponovno kasni.", + "Export my data": "Izvezi moje podatke", + "Export, view, or delete audit trails": "Izvezite, pregledajte ali izbrišite revizijske tragove", + "Exporting...": "Izvoz...", + "Extend Schema": "Razširi shemu", + "Extended": "Razširjeno", + "Extensions": "Proširenja", + "Extra Query Parameters": "Dodatni parametri poizvedbaa", + "Extract Now": "Izdvoji sada", + "Extract Pending Files": "Izdvoji datoteke v čakanju", + "Extract now": "Izdvoji sada", + "Extract properties from each object": "Izdvoji lastnosti iz vsakog objekta", + "Extracted At": "Izdvojeno", + "Extracted at": "Izdvojeno", + "Extraction": "Izdvajanje", + "Extraction Mode": "Način izdvajanja", + "Extraction Scope": "Opseg izdvajanja", + "Extraction Status": "Status izdvajanja", + "Extraction error for {file}: {error}": "Napaka izdvajanja za {file}: {error}", + "Facet Description": "Opis fasete", + "Facet Order": "Redoslijed fasete", + "Facet Title": "Naslov fasete", + "Facet Type": "Vrsta fasete", + "Facet configuration updated successfully": "Konfiguracija fasete uspešno posodobljena", + "Facetable": "Fasetabilno", + "Faceting Configuration:": "Konfiguracija faseta:", + "Facets discovered and configured successfully": "Fasete so uspešno otkrivene in konfigurirane", + "Facets discovered successfully": "Fasete so uspešno otkrivene", + "Failed": "Ni uspelo", + "Failed to add attachment to object": "Dodavanje priponke objektu ni uspjelo", + "Failed to add user to organisation": "Dodavanje uporabnika v organizacijo ni uspjelo", + "Failed to analyze schema properties": "Analiza lastnosti sheme ni uspjela", + "Failed to archive conversation": "Arhiviranje klepeta ni uspjelo", + "Failed to calculate sizes": "Izračun velikost ni uspio", + "Failed to cancel deactivation": "Preklicanje deaktivacije ni uspjelo", + "Failed to change active organisation: {error}": "Sprememba aktivne organizacije ni uspjela: {error}", + "Failed to change password": "Sprememba lozinke ni uspjela", + "Failed to check version: {error}": "Preverjanje verzije ni uspjela: {error}", + "Failed to clear audit trails: {error}": "Brisanje revizijskih tragta ni uspjelo: {error}", + "Failed to clear blob objects: {error}": "Brisanje blob objekata ni uspjelo: {error}", + "Failed to clear cache: {error}": "Brisanje predpomnilnika ni uspjelo: {error}", + "Failed to clear collection": "Brisanje kolekcije ni uspjelo", + "Failed to clear conversation": "Brisanje klepeta ni uspjelo", + "Failed to clear embeddings: {error}": "Brisanje ugradbi ni uspjelo: {error}", + "Failed to clear search trails: {error}": "Brisanje tragta iskanja ni uspjelo: {error}", + "Failed to clear {type} cache: {error}": "Brisanje predpomnilnika {type} ni uspjelo: {error}", + "Failed to copy changes": "Kopiranje sprememba ni uspjelo", + "Failed to copy data": "Kopiranje podatkov ni uspjelo", + "Failed to copy data to clipboard": "Kopiranje podatkov u medjuspremnik ni uspjelo", + "Failed to copy to clipboard": "Kopiranje u medjuspremnik ni uspjelo", + "Failed to copy token": "Kopiranje tokena ni uspjelo", + "Failed to create conversation": "Ustvarjanje klepeta ni uspjelo", + "Failed to create missing SOLR fields: {error}": "Ustvarjanje nedostajučih SOLR polja ni uspjelo: {error}", + "Failed to create or find project": "Ustvarjanje ali iskanje projekta ni uspjelo", + "Failed to create token": "Ustvarjanje tokena ni uspjelo", + "Failed to delete SOLR collection: {error}": "Brisanje SOLR kolekcije ni uspjelo: {error}", + "Failed to delete collection": "Brisanje kolekcije ni uspjelo", + "Failed to delete conversation": "Brisanje pogovora ni uspelo", + "Failed to delete objects for {schema}: {error}": "Brisanje objekata za {schema} ni uspjelo: {error}", + "Failed to delete webhook": "Brisanje webhooka ni uspjelo", + "Failed to depublish register: {error}": "Depubliciranje registra ni uspjelo: {error}", + "Failed to depublish schema: {error}": "Depubliciranje sheme ni uspjelo: {error}", + "Failed to depublish: {error}": "Depubliciranje ni uspjelo: {error}", + "Failed to discover files: {error}": "Otkrivanje datoteka ni uspjelo: {error}", + "Failed to download API specification": "Prenos API specifikacije ni uspjelo", + "Failed to export data": "Izvoz podatkov ni uspio", + "Failed to export objects": "Izvoz objekata ni uspio", + "Failed to fetch conversation history": "Pridobivanje zgodovine pogovora ni uspelo", + "Failed to fix mismatched SOLR fields: {error}": "Ispravljanje neuskladjenih SOLR polja ni uspjelo: {error}", + "Failed to get SOLR field configuration": "Dohvačanje konfiguracije SOLR polja ni uspjelo", + "Failed to get chat statistics": "Dohvačanje statistike klepeta ni uspjelo", + "Failed to get database information: %s": "Dohvačanje informacija o bazi podatkov ni uspjelo: %s", + "Failed to get entity categories": "Dohvačanje kategorija entitetea ni uspjelo", + "Failed to get entity statistics": "Dohvačanje statistike entitetea ni uspjelo", + "Failed to get entity types": "Dohvačanje vrsta entitetea ni uspjelo", + "Failed to get response: {error}": "Dohvačanje odgovora ni uspjelo: {error}", + "Failed to invalidate app store cache: {error}": "Poništavanje predpomnilnika trgovine aplikacija ni uspjelo: {error}", + "Failed to leave organisation: {error}": "Napuštanje organizacije ni uspjelo: {error}", + "Failed to link object": "Povezovanje objekta ni uspjelo", + "Failed to load Deck cards": "Nalaganje Deck kartica ni uspjelo", + "Failed to load LLM configuration": "Nalaganje LLM konfiguracije ni uspjelo", + "Failed to load Nextcloud groups": "Nalaganje Nextcloud skupina ni uspjelo", + "Failed to load cache statistics: {error}": "Nalaganje statistike predpomnilnika ni uspjelo: {error}", + "Failed to load conversation": "Nalaganje klepeta ni uspjelo", + "Failed to load entities": "Nalaganje entitetea ni uspjelo", + "Failed to load entity": "Nalaganje entitetea ni uspjelo", + "Failed to load extraction data": "Nalaganje podatkov o izdvajanju ni uspjelo", + "Failed to load files": "Nalaganje datoteka ni uspjelo", + "Failed to load linked contacts": "Nalaganje povezanih kontakata ni uspjelo", + "Failed to load linked emails": "Nalaganje povezanih e-pošta ni uspjelo", + "Failed to load linked events": "Nalaganje povezanih dogadjaja ni uspjelo", + "Failed to load organisations": "Nalaganje organizacija ni uspjelo", + "Failed to load register data": "Nalaganje podatkov registra ni uspjelo", + "Failed to load related entities": "Nalaganje povezanih entitetea ni uspjelo", + "Failed to load settings: {error}": "Nalaganje nastavitev ni uspjelo: {error}", + "Failed to load statistics: {error}": "Nalaganje statistike ni uspjelo: {error}", + "Failed to load templates": "Nalaganje predlogaa ni uspjelo", + "Failed to load view: {error}": "Nalaganje prikaza ni uspjelo: {error}", + "Failed to load webhooks": "Nalaganje webhookta ni uspjelo", + "Failed to load workflows": "Nalaganje tijekta rada ni uspjelo", + "Failed to process message": "Obdelava počasenočila ni uspjela", + "Failed to publish register: {error}": "Objavljivanje registra ni uspjelo: {error}", + "Failed to publish schema: {error}": "Objavljivanje sheme ni uspjelo: {error}", + "Failed to publish: {error}": "Objavljivanje ni uspjelo: {error}", + "Failed to refresh database information": "Osvježavanje informacija o bazi podatkov ni uspjelo", + "Failed to reindex collection": "Ponovno indeksiranje kolekcije ni uspjelo", + "Failed to remove avatar": "Odstranitev avatara ni uspjelo", + "Failed to remove link": "Odstranitev veze ni uspjelo", + "Failed to remove member: {error}": "Odstranitev člana ni uspjelo: {error}", + "Failed to remove schema {schema}: {error}": "Odstranitev sheme {schema} ni uspjelo: {error}", + "Failed to remove user from organisation": "Odstranitev uporabnika iz organizacije ni uspjelo", + "Failed to rename conversation": "Preimenovanje klepeta ni uspjelo", + "Failed to request deactivation": "Zahteva za deaktivacijo ni uspio", + "Failed to restore conversation": "Vračanje klepeta ni uspjelo", + "Failed to retrieve extraction status.": "Dohvačanje statusa izdvajanja ni uspjelo.", + "Failed to retrieve objects for file.": "Dohvačanje objekata za datoteku ni uspjelo.", + "Failed to retry extraction": "Ponovni pokušaj izdvajanja ni uspio", + "Failed to retry webhook": "Ponovni pokušaj webhooka ni uspio", + "Failed to revoke token": "Opoziv tokena ni uspio", + "Failed to save GitHub token": "Shranjujenje GitHub tokena ni uspjelo", + "Failed to save GitLab URL": "Shranjujenje GitLab URL-a ni uspjelo", + "Failed to save GitLab token": "Shranjujenje GitLab tokena ni uspjelo", + "Failed to save LLM settings: {error}": "Shranjujenje LLM nastavitev ni uspjelo: {error}", + "Failed to save additional feedback": "Shranjujenje dodatnih povratnih informacija ni uspjelo", + "Failed to save configuration.": "Shranjujenje konfiguracije ni uspjelo.", + "Failed to save configuration: {error}": "Shranjujenje konfiguracije ni uspjelo: {error}", + "Failed to save facet configuration": "Shranjujenje konfiguracije fasete ni uspjelo", + "Failed to save feedback": "Shranjujenje povratnih informacija ni uspjelo", + "Failed to save file settings: {error}": "Shranjujenje nastavitev datoteke ni uspjelo: {error}", + "Failed to save n8n configuration": "Shranjujenje n8n konfiguracije ni uspjelo", + "Failed to save preferences": "Shranjujenje preferencija ni uspjelo", + "Failed to save roles": "Shranjujenje vloga ni uspjelo", + "Failed to save settings": "Shranjujenje nastavitev ni uspjelo", + "Failed to save settings: {error}": "Shranjujenje nastavitev ni uspjelo: {error}", + "Failed to save view: {error}": "Shranjujenje prikaza ni uspjelo: {error}", + "Failed to save warmup interval: {error}": "Shranjujenje intervala zagrijavanja ni uspjelo: {error}", + "Failed to save webhook": "Shranjujenje webhooka ni uspjelo", + "Failed to send feedback": "Pošiljanje povratnih informacija ni uspjelo", + "Failed to set active organisation: {error}": "Postavljanje aktivne organizacije ni uspjelo: {error}", + "Failed to setup SOLR: {error}": "Postavljanje SOLR-a ni uspjelo: {error}", + "Failed to start file warmup: {error}": "Zagon zagrijavanja datoteka ni uspjelo: {error}", + "Failed to start processing {type} files: {error}": "Zagon obdelave {type} datoteka ni uspjelo: {error}", + "Failed to start vectorization: {error}": "Zagon vektorizacije ni uspjelo: {error}", + "Failed to switch organisation: {error}": "Preklapljanje organizacije ni uspjelo: {error}", + "Failed to sync magic table for {schema}: {error}": "Sinkronizacija čarobne tabele za {schema} ni uspjela: {error}", + "Failed to test SOLR connection: {error}": "Testiranje SOLR veze ni uspjelo: {error}", + "Failed to test webhook": "Testiranje webhooka ni uspjelo", + "Failed to update LLM settings: {error}": "Posodobljenje LLM nastavitev ni uspjelo: {error}", + "Failed to update Multitenancy settings: {error}": "Posodobljenje nastavitev večstruke zakupništva ni uspjelo: {error}", + "Failed to update RBAC settings: {error}": "Posodobljenje RBAC nastavitev ni uspjelo: {error}", + "Failed to update Retention settings: {error}": "Posodobljenje nastavitev hramba ni uspjelo: {error}", + "Failed to update SOLR settings: {error}": "Posodobljenje SOLR nastavitev ni uspjelo: {error}", + "Failed to update favorite status": "Posodobljenje statusa favorita ni uspjelo", + "Failed to update schema configuration for {schema}: {error}": "Posodobljenje konfiguracije sheme za {schema} ni uspjelo: {error}", + "Failed to update schema properties": "Posodobljenje lastnosti sheme ni uspjelo", + "Failed to update view": "Posodobljenje prikaza ni uspjelo", + "Failed to update view: {error}": "Posodobljenje prikaza ni uspjelo: {error}", + "Failed to update webhook": "Posodobljenje webhooka ni uspjelo", + "Failed to upload avatar": "Nalaganje avatara ni uspjelo", + "Failed to validate {schema}: {error}": "Preverjanje veljavnosti {schema} ni uspjela: {error}", + "Failed to vectorize files: {error}": "Vektorizacija datoteka ni uspjela: {error}", + "Failed to warmup names cache: {error}": "Zagrijavanje predpomnilnika naziva ni uspjelo: {error}", + "Federation": "Federacija", + "Feedback recorded": "Povratne informacije zabilježene", + "Fewer sources (1-3):": "Manj izvora (1-3):", + "Field": "Polje", + "Fields to Display": "Polja za prikaz", + "File": "Datoteka", + "File #{id}": "Datoteka #{id}", + "File Chunk Prediction": "Predvidjanje fragmenta datoteke", + "File Collection": "Kolekcija datoteka", + "File Handling": "Rukovanje datotekama", + "File Location": "Lokacija datoteke", + "File Management": "Upravljanje datotekama", + "File Name": "Naziv datoteke", + "File Path": "Putanja datoteke", + "File Type Selection": "Izbor vrste datoteke", + "File Types to Process": "Vrste datoteka za obradu", + "File Vectorization": "Vektorizacija datoteka", + "File Warmup": "Zagrijavanje datoteka", + "File actions menu": "Izbornik dejanj datoteke", + "File anonymized successfully": "Datoteka je uspešno anonimizirana", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vektorizacija fragmenata datoteke še ni implementirana. Fragmenti so spremni in pohranjeni, ali je storitev vektorizacije u razvoju.", + "File collection not configured": "Kolekcija datoteka ni konfigurirana", + "File discovery completed": "Otkrivanje datoteka dokončano", + "File extraction completed": "Izdvajanje datoteke dokončano", + "File extraction queued": "Izdvajanje datoteke stavljeno u red čekanja", + "File indexed successfully": "Datoteka je uspešno indeksirana", + "File is already anonymized": "Datoteka je več anonimizirana", + "File name": "Naziv datoteke", + "File not found": "Datoteka ni najdena", + "File settings saved successfully": "Nastavitve datoteke uspešno spremljene", + "File settings updated successfully": "Nastavitve datoteke uspešno posodobljene", + "File sources": "Izvori datoteka", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vektorizacija datoteka dokončana! {vectorized} fragmenata vektorizirano iz {files} datoteka. {failed} ni uspelo.", + "File vectorization configuration saved successfully": "Konfiguracija vektorizacije datoteka uspešno spremljena", + "File vectorization started. Check the statistics section for progress.": "Vektorizacija datoteka pokrenuta. Preverite odjeljak statistike za napredek.", + "File warmup completed": "Zagrijavanje datoteka dokončano", + "Files": "Datoteke", + "Files added successfully": "Datoteke uspešno dodane", + "Files with Completed Extraction:": "Datoteke s dokončanim izdvajanjem:", + "Files → fileCollection, Objects → objectCollection": "Datoteke → fileCollection, Objekti → objectCollection", + "Files → filecollection, objects → objectcollection": "Datoteke → filecollection, objekti → objectcollection", + "Filter Audit Trails": "Filtriraj revizijske tragove", + "Filter Deleted Items": "Filtriraj izbrisane stavke", + "Filter Objects": "Filtriraj objekte", + "Filter Properties": "Filtriraj svojstva", + "Filter Search Trails": "Filtriraj tragte iskanja", + "Filter Statistics": "Filtriraj statistiku", + "Filter and analyze search trail entries": "Filtrirajte in analizirajte vnose tragta iskanja", + "Filter and manage audit trail entries": "Filtrirajte in upravljajte vnosima revizijskega traga", + "Filter and manage soft deleted items": "Filtrirajte in upravljajte meko izbrisanim stavkama", + "Filter and search entities": "Filtrirajte in pretražujte entitetee", + "Filter by object ID": "Filtriraj po ID-u objekta", + "Filter by search term": "Filtriraj po pojmu za iskanje", + "Filter by type": "Filtriraj po vrsti", + "Filter by webhook": "Filtriraj po webhooku", + "Filter data loaded automatically. Use the filters below to refine your search.": "Podatki filtera učitani samodejno. Upotrijebite filtere u nastavku za pročiščavanje iskanja.", + "Filter fields...": "Filtriraj polja...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrirajte okidače webhooka po svojstvima payload (eno na vrstico, format: key: value)", + "Filtered": "Filtrirano", + "Filters": "Filteri", + "Finalizing setup...": "Dokončevanje postavljanja...", + "Fireworks AI Chat Settings": "Nastavitve klepeta Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfiguracija ugradbi Fireworks AI", + "First": "Prvo", + "Fixed": "Fiksno", + "For chat and retrieval-augmented generation": "Za klepet in generiranje uz potporu dohvačanja", + "For vector embeddings and semantic search": "Za vektorske ugradbe in semantično iskanje", + "Format": "Format", + "Format Issue": "Problem s formatom", + "Format constraint is missing": "Nedostaje ovejičenje formata", + "From (e.g. -7 days or 2025-01-01)": "Od (npr. -7 dana ali 2025-01-01)", + "From Date": "Od datuma", + "From date": "Od datuma", + "Full data copied to clipboard": "Potpuni podatki kopirani u medjuspremnik", + "Function Tools": "Funkcijski alati", + "General Issue": "Opčeniti problem", + "Generate recommendations and confidence scores": "Generiraj prepočasenočila in ocjene pouzdanosti", + "Generate report": "Generiraj poročilo", + "Generate the verantwoordingsdocument": "Generiraj dokument o odgovornosti", + "Generate vectors immediately when new objects are created": "Generiraj vektore odmah ko se stvore nti objekti", + "Generated: {time}": "Generirano: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Dobijte vpoglede u stanje podatkov vaše organizacije u stvarnom vremenu fokusiranjem na registre, definicije shema te shranjevanje in upotrebu objekata.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Dobijte vpoglede u stanje podatkov vaše organizacije, trendte rasta in metrike kvalitete u stvarnom vremenu.", + "GitHub Personal Access Token": "GitHub osobni dostopni token", + "GitHub token is valid": "GitHub token je valjan", + "GitHub token is valid! Username: {username}": "GitHub token je valjan! Uporabniško ime: {username}", + "GitHub token removed successfully": "GitHub token uspešno uklonjen", + "GitHub token saved successfully": "GitHub token uspešno spremljen", + "GitHub token test failed: {message}": "Testiranje GitHub tokena ni uspjelo: {message}", + "GitLab Personal Access Token": "GitLab osobni dostopni token", + "GitLab URL saved successfully": "GitLab URL uspešno spremljen", + "GitLab token is valid": "GitLab token je valjan", + "GitLab token is valid! Username: {username}": "GitLab token je valjan! Uporabniško ime: {username}", + "GitLab token saved successfully": "GitLab token uspešno spremljen", + "GitLab token test failed: {message}": "Testiranje GitLab tokena ni uspjelo: {message}", + "Go to linked Register": "Idi na povezani register", + "Go to linked Schema": "Idi na povezanv shemo", + "Go to organisation": "Idi na organizacijo", + "Grant additional permissions beyond standard CRUD operations": "Dodijeli dodatna dopuštenja zunaj standardnih CRUD operacija", + "Group Quota": "Kvota skupine", + "Groups": "Skupine", + "HTML preview": "HTML pretpregled", + "HTTP Method": "HTTP metoda", + "HTTP method used to send webhook requests": "HTTP metoda ki se se uporablja za pošiljanje zahteva webhooka", + "Headers": "Glave", + "Health": "Stanje", + "Heartbeat successful - connection kept alive": "Otkucaj uspešen - veza održana aktivnom", + "Helpful": "Korisno", + "Here you can configure the details for various connections.": "Tukaj lahko konfigurirate podrobnosti za različne povezave.", + "Here you can configure the details for your organisation.": "Tukaj lahko konfigurirate podrobnosti za svojo organizacijo.", + "Hide": "Sakrij", + "Hide Filters": "Sakrij filtere", + "Hide in collection view": "Sakrij v prikazu kolekcije", + "Hide in forms": "Sakrij u obrascima", + "Hide in list view": "Sakrij v prikazu seznama", + "High": "Visoko", + "High Confidence": "Visoka pouzdanost", + "Host": "Poslužitelj", + "Hourly": "Vsaki sat", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Koliko duboko prolaziti kroz ugniždjena lastnosti objekta (1-20). Več vrednosti obuhvačaju več podrobnosti, ali povečavaju velikost vektora.", + "How often to check for updates (1-168 hours)": "Koliko često preverjanjevati posodobljenja (1-168 sati)", + "How to handle retries for failed webhook deliveries": "Kako rukovati ponovnim pokušajima za neuspjele ispočasenočila webhooka", + "IBANs": "IBAN-ovi", + "ID": "ID", + "ID of input mapping (optional)": "ID ulaznog mapiranja (neobvezno)", + "ID of output mapping (optional)": "ID izlaznog mapiranja (neobvezno)", + "ID of the target resource": "ID ciljnog resursa", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ovejičenja IP stope uspešno izbrisana", + "Identify properties not in the schema": "Prepoznaj lastnosti ki niso v shemi", + "Immutable": "Nepromjenjivo", + "Import": "Uvoz", + "Import Configuration": "Konfiguracija uvoza", + "Import Data into Register": "Uvezi podatke v register", + "Import successful": "Uvoz uspešen", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Uvozite predloga report-bundle.json da bi dobili register `reports`, nato ustvarite svojo prvu nadzornu ploču prek standardnog uporabniškog sučelja za objekte. Nadzorne plošče deklariraju svoje widgete u JSON-u, a prikazivač vsakom widgetu dostavlja podatke agregacije uživo.", + "Improved Property": "Poboljšano svojstvo", + "In use": "U upotrebi", + "Inactive": "Neaktivno", + "Inactive organisations cannot be used": "Neaktivne organizacije ne morejo se uporabiti", + "Include IDs and names of related objects for better contextual search": "Vključi ID-te in nazive povezanih objekata za bolje konbesedilno iskanje", + "Include ids and names of related objects for better contextual search": "Vključi id-te in nazive povezanih objekata za bolje konbesedilno iskanje", + "Include related object references": "Vključi reference povezanih objekata", + "Include schema and register metadata": "Vključi metapodatke sheme in registra", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Informacije o trenutnoj OpenRegister instalaciji", + "Inherited from register": "Naslijedjeno od registra", + "Inherits permissions from register": "Nasljedjuje dopuštenja od registra", + "Initialization failed": "Inicijalizacija ni uspjela", + "Initialize Project": "Inicijaliziraj projekt", + "Initializing SOLR setup...": "Inicijalizacija SOLR postavljanja...", + "Input Mapping": "Ulazno mapiranje", + "Inspect Fields": "Pregledaj polja", + "Inspect Index": "Pregledaj indeks", + "Inspect SOLR Index": "Pregledaj SOLR indeks", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instalirajte aplikaciju notify_push iz trgovine aplikacija Nextcloud da bi omogočili posodobljenja u stvarnom vremenu.", + "Integrations": "Integracije", + "Internet location (URL) of this installation": "Internetska lokacija (URL) te namestitve", + "Interval": "Interval", + "Invalid": "Neveljavno", + "Invalid batch size. Must be between 1 and 5000": "Neveljavna velikost paketa. Mora biti izmedju 1 in 5000", + "Invalid feedback type": "Neveljavna vrsta povratne informacije", + "Invalid field name provided": "Naveden je nevaljan naziv polja", + "Invalid maxObjects. Must be 0 (all) or positive number": "Nevaljan maxObjects. Mora biti 0 (vse) ali pozitivan število", + "Invalid:": "Neveljavno:", + "Invite Users": "Pozti uporabnike", + "Invited Users": "Pozvani uporabniki", + "Inzage (Art 15)": "Vpogled (čl. 15.)", + "Inzage results": "Rezultati vpogleda", + "Issue": "Problem", + "Items per page:": "Stavki po stranici:", + "Just now": "Upravo sada", + "Key": "Ključ", + "LLM Configuration": "LLM konfiguracija", + "LLM actions menu": "Izbornik LLM dejanj", + "LLM configuration saved successfully": "LLM konfiguracija uspešno spremljena", + "LLM features disabled": "LLM funkcije onemogočene", + "LLM features enabled": "LLM funkcije omogočene", + "LLM must be enabled with an embedding provider configured": "LLM mora biti omogučen uz konfiguriranog ponudnika ugradbi", + "LLM settings saved successfully": "LLM nastavitve uspešno spremljene", + "LLM settings updated successfully": "LLM nastavitve uspešno posodobljene", + "Label": "Oznaka", + "Labels": "Oznake", + "Languages": "Jezici", + "Last": "Zadnje", + "Last Triggered": "Zadnje aktivirano", + "Leave empty to allow all users access": "Ostavite prazno da bi dopustili dostop vsim uporabnikima", + "Leave off for auto-detection from property format.": "Ostavite izključeno za samodejno otkrivanje iz formata svojstva.", + "Left organisation successfully": "Uspešno napuštena organizacija", + "Legal basis *": "Pravna osnta *", + "Length Range:": "Raspon duljine:", + "Linear": "Linearno", + "Link": "Veza", + "Link an email from the Mail app sidebar to associate it with this object.": "Povežite e-poštu iz bočne trake aplikacije Mail da bi je pridružili tem objektu.", + "Link an object to see it here.": "Povežite objekt da bi ga tukaj vidjeli.", + "Link another object": "Poveži drugi objekt", + "Link existing event": "Poveži postoječi dogadjaj", + "Link removed": "Veza uklonjena", + "Link to Object": "Veza na objekt", + "Link to {name}": "Veza na {name}", + "Linked Objects": "Povezani objekti", + "Linked by {user}": "Povezao {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Povezane e-pošte, kalendarski dogadjaji, kontakti in Deck kartice pojavit če se tukaj.", + "Linked to {name}": "Povezano s {name}", + "Load Advanced Filters": "Naloži napredne filtere", + "Load advanced filters with live data from your search index": "Naloži napredne filtere s podatkov uživo iz vašeg indeksa iskanja", + "Load more": "Naloži več", + "Load view": "Naloži prikaz", + "Loading API tokens...": "Nalaganje API tokena...", + "Loading ConfigSets...": "Nalaganje ConfigSetova...", + "Loading LLM configuration...": "Nalaganje LLM konfiguracije...", + "Loading RBAC settings...": "Nalaganje RBAC nastavitev...", + "Loading activity...": "Nalaganje aktivnosti...", + "Loading advanced filters...": "Nalaganje naprednih filtera...", + "Loading agents...": "Nalaganje agenata...", + "Loading application details...": "Nalaganje podrobnosti aplikacije...", + "Loading applications...": "Nalaganje aplikacija...", + "Loading archived conversations...": "Nalaganje arhiviranih klepeta...", + "Loading audit trails...": "Nalaganje revizijskih tragova...", + "Loading available tools...": "Nalaganje dosegljivih alata...", + "Loading cache statistics...": "Nalaganje statistike predpomnilnika...", + "Loading collections...": "Nalaganje kolekcija...", + "Loading configurations...": "Nalaganje konfiguracija...", + "Loading conversation...": "Nalaganje klepeta...", + "Loading conversations...": "Nalaganje pogovorov...", + "Loading deleted items...": "Nalaganje izbrisanih stavki...", + "Loading endpoints": "Nalaganje končnih točk", + "Loading entities...": "Nalaganje entitetea...", + "Loading events...": "Nalaganje dogadjaja...", + "Loading filter data...": "Nalaganje podatkov filtera...", + "Loading groups...": "Nalaganje skupina...", + "Loading linked objects...": "Nalaganje povezanih objekata...", + "Loading log details...": "Nalaganje podrobnosti zapisnika...", + "Loading multitenancy settings...": "Nalaganje nastavitev večstruke zakupništva...", + "Loading n8n configuration...": "Nalaganje n8n konfiguracije...", + "Loading permission matrix...": "Nalaganje matrice dopuštenja...", + "Loading preferences...": "Nalaganje preferencija...", + "Loading register data...": "Nalaganje podatkov registra...", + "Loading registers...": "Nalaganje registera...", + "Loading retention settings...": "Nalaganje nastavitev hramba...", + "Loading schemas...": "Nalaganje shema...", + "Loading search configuration...": "Nalaganje konfiguracije iskanja...", + "Loading search trails...": "Nalaganje tragta iskanja...", + "Loading sources...": "Nalaganje izvora...", + "Loading statistics...": "Nalaganje statistike...", + "Loading text extraction settings...": "Nalaganje nastavitev izdvajanja teksta...", + "Loading tokens...": "Nalaganje tokena...", + "Loading user groups...": "Nalaganje uporabniških skupina...", + "Loading users...": "Nalaganje uporabnika...", + "Loading views...": "Nalaganje prikaza...", + "Loading...": "Nalaganje...", + "Local": "Lokalno", + "Local Version": "Lokalna verzija", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Pronadjite vsaki objekt ki referencira posameznikov (čl. 15. vpogled), pregledajte brisanje (čl. 17. pravo na zaborav) ali izvezite njihove podatke (čl. 20. prenosivost).", + "Location": "Lokacija", + "Location Field": "Polje lokacije", + "Locations": "Lokacije", + "Locked": "Zaključano", + "Locked:": "Zaključano:", + "Login successful": "Prijava uspešna", + "Logs": "Zapisnici", + "Low": "Nisko", + "Low Confidence": "Niska pouzdanost", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Niži številoevi pojavljuju se prvi u bočnoj traci filtera. Ostavite prazno za samodejno redoslijed.", + "Lucene Version:": "Lucene verzija:", + "Magic table sync completed for {schema}": "Sinkronizacija čarobne tabele dokončana za {schema}", + "Magic table synchronized successfully": "Čarobna tabela uspešno sinkronizirana", + "Mail Integration": "Integracija e-pošte", + "Mail integration is not available": "Integracija e-pošte ni dosegljiva", + "Make this installation discoverable within the federation network.": "Naredite to namestitev vidno znotraj omrežja federacije.", + "Manage Organisation Roles": "Upravljaj vlogama organizacije", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Upravljajte SOLR kolekcijama (shraništa podatkov) in dodijelite ih za objekte in datoteke.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Upravljajte SOLR ConfigSetovima (predlošci konfiguracije) za svoje kolekcije.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Upravljajte SOLR kolekcijama (shraništa podatkov) in dodijelite ih za objekte in datoteke.", + "Manage SOLR configsets (configuration templates) for your collections.": "Upravljajte SOLR configsetovima (predlošci konfiguracije) za svoje kolekcije.", + "Manage and configure agents for automated tasks": "Upravljajte in konfigurirajte agente za automatizirane zadatke", + "Manage and monitor file text extraction status": "Upravljajte in nadzirite status izdvajanja teksta iz datoteka", + "Manage and restore soft deleted items from your registers": "Upravljajte in vračajte meko izbrisane stavke iz svojih registera", + "Manage and view detected entities from files and objects": "Upravljajte in pregledajte otkrivene entitetee iz datoteka in objekata", + "Manage document templates and themes": "Upravljajte predlošcima in temama dokumenata", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Upravljajte n8n potekom dela za OpenRegister automatizaciju. Poteki dela bo pohranjeni u konfiguriranom projektu.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Upravljajte aktivnostima obdelave, izvajajte zahteve za dostop posameznikov in revidirajte uskladjenost glede na EU GDPR / nizozemskom AVG.", + "Manage webhooks for event-driven integrations": "Upravljajte webhookovima za integracije vodjene dogadjajima", + "Manage your account settings, security, and personal data.": "Upravljajte nastavitvami računa, sigurnošču in osobnim podatkov.", + "Manage your applications and modules": "Upravljajte svojim aplikacijama in modulima", + "Manage your chat conversations": "Upravljajte svojim klepetima", + "Manage your data registers and their configurations": "Upravljajte svojim registrom podatkov in njihovim konfiguracijama", + "Manage your data schemas and their properties": "Upravljajte svojim shemama podatkov in njihovim svojstvima", + "Manage your data sources and their configurations": "Upravljajte svojim izvorima podatkov in njihovim konfiguracijama", + "Manage your organisations and switch between them": "Upravljajte svojim organizacijama in prebacujte se izmedju njih", + "Manage your saved search configurations": "Upravljajte svojim shranjenim konfiguracijama iskanja", + "Manage your system configurations and settings": "Upravljajte svojim konfiguracijama in nastavitvami sistema", + "Managed": "Upravljano", + "Mappings": "Mapiranja", + "Mass validation completed successfully": "Masovna preverjanje veljavnosti uspešno dokončana", + "Mass validation failed: {error}": "Masovna preverjanje veljavnosti ni uspjela: {error}", + "Max Files (0 = all)": "Maks. datoteka (0 = vse)", + "Max Length": "Maks. duljina", + "Max Length:": "Maks. duljina:", + "Max Objects (0 = all)": "Maks. objekata (0 = vse)", + "Max Retries": "Maks. ponovnih pokušaja", + "Max Shards/Node": "Maks. shardova/čvor", + "Max Tokens": "Maks. tokena", + "Max execution time (ms)": "Maks. vrijeme izvodjenja (ms)", + "Max ms": "Maks. ms", + "Max result count": "Maks. število rezultata", + "Max results": "Maks. rezultata", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksimalna velikost datoteke (MB)", + "Maximum Nesting Depth": "Maksimalna dubina ugniždjivanja", + "Maximum length": "Maksimalna duljina", + "Maximum length constraint is missing": "Nedostaje ovejičenje maksimalne duljine", + "Maximum length is too restrictive": "Maksimalna duljina preveč je restriktivna", + "Maximum number of files to process. Set to 0 to process all files.": "Maksimalen število datoteka za obradu. Postavite na 0 za obradu vsih datoteka.", + "Maximum number of items": "Maksimalen število stavki", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksimalen število objekata za obradu. Postavite na 0 za obradu vsih objekata.", + "Maximum number of retry attempts for failed deliveries": "Maksimalen število ponovnih pokušaja za neuspjele ispočasenočila", + "Maximum time to wait before committing changes": "Maksimalno vrijeme čekanja pred potrjevanja sprememba", + "Maximum tokens to generate": "Maksimalen število tokena za generiranje", + "Maximum value": "Maksimalna vrednost", + "Maximum value constraint is missing": "Nedostaje ovejičenje maksimalne vrednosti", + "Maximum value is too restrictive": "Maksimalna vrednost preveč je restriktivna", + "Medium": "Srednje", + "Medium Confidence": "Srednja pouzdanost", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Srednji poizvedbai: Iskanja s nekim filtriranjem ali več parametrov (npr. rasponi datuma, odredjeni registri/sheme)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Srednji poizvedbai: iskanja s nekim filtriranjem ali več parametrov (npr. rasponi datuma, odredjeni registri/sheme)", + "Member removed successfully": "Član uspešno uklonjen", + "Members": "Članovi", + "Members ({count})": "Članti ({count})", + "Members of selected groups can access this view": "Članti izbranih skupina lahko dostopiti tem prikazu", + "Members:": "Članovi:", + "Memory prediction calculated successfully": "Predvidjanje memorije uspešno izračunato", + "Message content is required": "Vsebina počasenočila je obvezen", + "Message does not belong to this conversation": "Počasenočilo ne pripada tem klepetu", + "Message not found": "Počasenočilo ni najdena", + "Metadata": "Metapodatki", + "Metadata Filters": "Filteri metapodatkov", + "Method": "Metoda", + "Method*": "Metoda*", + "Method: {method}": "Metoda: {method}", + "Min Length": "Min. duljina", + "Min execution time (ms)": "Min. vrijeme izvodjenja (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. število rezultata", + "Min results": "Min. rezultata", + "Minimum": "Minimum", + "Minimum length": "Minimalna duljina", + "Minimum number of items": "Minimalen število stavki", + "Minimum value": "Minimalna vrednost", + "Minimum value constraint is missing": "Nedostaje ovejičenje minimalne vrednosti", + "Minimum value is too restrictive": "Minimalna vrednost preveč je restriktivna", + "Missing conversation or agentUuid": "Nedostaje klepet ali agentUuid", + "Missing conversationId": "Nedostaje conversationId", + "Missing message": "Nedostaje počasenočilo", + "Missing object fields created successfully": "Nedostajuča polja objekta uspešno ustvarjena", + "Mode:": "Način:", + "Model": "Model", + "Model Name": "Naziv modela", + "Modified": "Izmijenjeno", + "Monthly": "Mjesečno", + "More sources (10-20):": "Več izvora (10-20):", + "Most Active Objects": "Najaktivniji objekti", + "Multiple Of": "Večkratnik od", + "Multiple of": "Večkratnik od", + "Multiple search trail deletion not implemented yet": "Brisanje več tragta iskanja še ni implementirano", + "Multitenancy": "Večstruka zakupništvo", + "Multitenancy settings updated successfully": "Nastavitve večstruke zakupništva uspešno posodobljene", + "My Account": "Moj račun", + "Naam": "Naziv", + "Naam *": "Naziv *", + "Name": "Naziv", + "Name *": "Naziv *", + "Name*": "Naziv*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Predpomnilnik naziva uspešno je zagrijana: {count} naziva naloženo za {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Predpomnilnik naziva uspešno je zagrijana: {count} naziva naloženo za {time}. {message}", + "Names cache warmup completed": "Zagrijavanje predpomnilnika naziva dokončano", + "Naming pattern for copies:": "Obrazac imenovanja za kopije:", + "New Collection Name": "Naziv nte zbirke", + "New Configuration": "Nta konfiguracija", + "New Conversation": "Nov pogovor", + "New Properties": "Nta svojstva", + "New Property": "Nto svojstvo", + "New User Group": "Nta korisnička skupina", + "New Value": "Nta vrednost", + "New activity": "Nta aktivnost", + "New password": "Nta lozinka", + "New processing activity": "Nta aktivnost obdelave", + "New users without specific organisation membership will be automatically added to this organisation": "Novi uporabniki brez določenega članstva v organizaciji bodo samodejno dodani v to organizacijo", + "New verwerkingsactiviteit": "Nta aktivnost obdelave", + "Next": "Sljedeče", + "Nextcloud Groups": "Nextcloud skupine", + "No": "Ne", + "No API tokens.": "Nema API tokena.", + "No ConfigSets found": "Niso najdeni ConfigSet-ovi", + "No Deck cards linked to this object": "Nobena Deck kartica ni povezana s temi objektom", + "No GitHub token configured": "GitHub token ni konfiguriran", + "No GitHub token provided": "GitHub token ni naveden", + "No GitLab token provided": "GitLab token ni naveden", + "No Organisation": "Nema organizacije", + "No active filters": "Nema aktivnih filtera", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Nema aktivnih objekata za meko brisanje za shemu {schema}. Upotrijebite \"Trajno izbriši\" za odstranitev meko izbrisanih objekata.", + "No activity data available": "Nema dosegljivih podatkov o aktivnosti", + "No activity found.": "Ni najdena aktivnost.", + "No agents available": "Nema dosegljivih agenata", + "No agents found": "Niso najdeni agenti", + "No app store cache exists yet": "Predpomnilnik trgovine aplikacijama še ne obstaja", + "No applications are available.": "Nema dosegljivih aplikacija.", + "No applications found": "Niso najdene aplikacije", + "No approval steps for this object.": "Nema korakov odobravanja za ta objekt.", + "No archived conversations": "Nema arhiviranih klepeta", + "No audit trail entries found": "Niso najdeni vnosi revizijskega traga", + "No changes recorded": "Niso zabilježene spremembe", + "No chunks to vectorize": "Nema kosov za vektorizacijo", + "No collections found": "Niso najdene zbirke", + "No configuration data": "Nema podatkov o konfiguraciji", + "No configuration selected": "Ni izbrana konfiguracija", + "No configurations are available.": "Nema dosegljivih konfiguracija.", + "No configurations found": "Niso najdene konfiguracije", + "No configurations found for this application.": "Niso najdene konfiguracije za to aplikaciju.", + "No contacts linked to this object": "Nidan kontakt ni povezan s temi objektom", + "No conversations yet. Create a new one to get started!": "Još nema klepeta. Ustvarite nti za početak!", + "No dashboards yet": "Još nema nadzornih ploča", + "No data available": "Nema dosegljivih podatkov", + "No data available for chart": "Nema dosegljivih podatkov za grafikon", + "No deleted items found": "Niso najdene izbrisane stavke", + "No deletion data available": "Nema dosegljivih podatkov o brisanju", + "No description found": "Ni najden opis", + "No description provided": "Ni naveden opis", + "No emails linked to this object": "Nobena e-pošta ni povezana s temi objektom", + "No endpoints defined": "Niso določene končne točke", + "No entities detected for this email.": "Niso odkriti entitetei za to e-poštu.", + "No entities detected in this file. Run text extraction first.": "Niso odkriti entitetei u ovoj datoteci. Prvo zaženite izvlačenje teksta.", + "No entities found": "Niso najdeni entitetei", + "No entities have been detected yet": "Još niso odkriti entitetei", + "No events found": "Niso najdeni dogadjaji", + "No events linked to this object": "Nidan dogadjaj ni povezan s temi objektom", + "No expired audit trails found to clear": "Niso najdeni istekli revizijski tragti za brisanje", + "No expired search trails found to clear": "Niso najdeni istekli tragti iskanja za brisanje", + "No extraction data available for this file": "Nema dosegljivih podatkov o izvlačenju za to datoteku", + "No facetable fields available. Select a register and schema to see available filters.": "Nema dosegljivih polja za fasete. Izberite register in shemu da bi vidjeli dosegljive filtre.", + "No files found": "Niso najdene datoteke", + "No files have been extracted yet": "Još niso izvučene datoteke", + "No files selected": "Niso izbrane datoteke", + "No files to process": "Nema datoteka za obradu", + "No files to reindex": "Nema datoteka za ponovno indeksiranje", + "No filters are currently active. This will delete ALL audit trail entries!": "Trenutno ni aktiven nidan filter. Time če se izbrisati SVI vnosi revizijskega traga!", + "No groups found. Try a different search.": "Niso najdene skupine. Pokušajte s drugačijim iskanjem.", + "No label": "Nema oznake", + "No labels": "Nema oznaka", + "No linked objects": "Nema povezanih objekata", + "No log entries found": "Niso najdeni vnosi zapisnika", + "No logs are available for this configuration.": "Nema dosegljivih zapisnika za to konfiguraciju.", + "No logs are available for this source.": "Nema dosegljivih zapisnika za ta izvor.", + "No logs found": "Niso najdeni zapisnici", + "No matches": "Nema podudaranja", + "No mismatched fields found - SOLR schema is properly configured": "Niso najdena nepodudarna polja - SOLR shema je pravilno konfigurirana", + "No objects are currently selected for deletion.": "Trenutno ni izbran nidan objekt za brisanje.", + "No objects are currently selected for permanent deletion.": "Trenutno ni izbran nidan objekt za trajno brisanje.", + "No objects are currently selected for restoration.": "Trenutno ni izbran nidan objekt za vračanje.", + "No objects found": "Niso najdeni objekti", + "No objects found to vectorize": "Niso najdeni objekti za vektorizacijo", + "No objects linked to this email": "Nidan objekt ni povezan s tem e-poštom", + "No objects selected": "Ni izbran nidan objekt", + "No objects selected for deletion": "Ni izbran nidan objekt za brisanje", + "No objects selected for restoration": "Ni izbran nidan objekt za vračanje", + "No objects to delete": "Nema objekata za brisanje", + "No objects to delete for schema {schema}": "Nema objekata za brisanje za shemu {schema}", + "No objects were permanently deleted": "Nidan objekt ni trajno izbrisan", + "No organisations found": "Niso najdene organizacije", + "No personal data was found for this subject identifier.": "Niso najdeni osobni podatki za ta identifikator subjekta.", + "No processing activities yet": "Još nema aktivnosti obdelave", + "No properties found": "Niso najdena svojstva", + "No properties match your filters.": "Nidno lastnost se ne ujema vašim filtrima.", + "No purge date set": "Datum brisanja ni nastavljen", + "No register data available": "Nema dosegljivih podatkov o registru", + "No register objects reference this file": "Nidan objekt registra ne referencira to datoteku", + "No registers found": "Niso najdeni registri", + "No registers found for this application.": "Niso najdeni registri za to aplikaciju.", + "No registers found. Create a register to configure permissions.": "Niso najdeni registri. Ustvarite register za konfiguriranje dozvola.", + "No related cases found for this sender": "Niso najdeni povezani zadeve za tega pošiljatelja", + "No related entities": "Nema povezanih entitetea", + "No relations found": "Niso najdene veze", + "No request body available": "Nema na voljog tijela zahteva", + "No response body available": "Nema na voljog tijela odgovora", + "No saved views yet. Create one in the Search tab!": "Još nema shranjenih prikaza. Ustvarite en na kartici Iskanje!", + "No saved views yet. create one in the search tab!": "Još nema shranjenih prikaza. ustvarite en na kartici iskanja!", + "No scan run yet": "Skeniranje še ni pokrenuto", + "No schema data available": "Nema dosegljivih podatkov o shemi", + "No schema selected for exploration": "Ni izbrana shema za istraživanje", + "No schemas configured for mail linking.": "Nobena shema ni konfigurirana za povezovanje pošte.", + "No schemas found": "Niso najdene sheme", + "No schemas found for this application.": "Niso najdene sheme za to aplikaciju.", + "No search terms data available": "Nema dosegljivih podatkov o pojmovima iskanja", + "No search trail entries found": "Niso najdeni vnosi traga iskanja", + "No sources are available.": "Nema dosegljivih izvora.", + "No sources found": "Niso najdeni izvori", + "No synchronizations found": "Niso najdene sinkronizacije", + "No templates found": "Niso najdeni predlošci", + "No templates have been created yet": "Još niso ustvarjeni predlošci", + "No tools available. Tools can be registered by installed apps.": "Nema dosegljivih alata. Alate lahko registrirati instalirane aplikacije.", + "No users in this organisation.": "Nema uporabnika u ovoj organizaciji.", + "No valid magic-mapped register+schema combinations found": "Niso najdene veljavne magic-mapped kombinacije registra+sheme", + "No views found. Create views first before configuring vectorization.": "Niso najdeni prikazi. Prvo ustvarite prikaze pred konfiguriranja vektorizacije.", + "No views match your search": "Nidan prikaz se ne ujema vašem iskanju", + "No webhooks found": "Niso najdeni webhookovi", + "No webhooks have been configured yet": "Još niso konfigurirani webhookovi", + "No workflows found in this project. Create workflows in the n8n editor.": "U tem projektu niso najdeni poteki dela. Ustvarite tijekte rada u n8n uredjivaču.", + "None": "Nidan", + "Not Found": "Ni najdeno", + "Not extracted": "Ni izvučeno", + "Not helpful": "Ni korisno", + "Not in use": "Ni u upotrebi", + "Note:": "Opomba:", + "Notifications": "Obvestiloi", + "Number Constraints": "Ovejičenja številoa", + "Number of Sources": "Število izvora", + "Number of chunks to vectorize in one API call (1-100).": "Število kosov za vektorizacijo u enkrat API klicu (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Število kosov za vektorizacijo u enkrat API klicu. Več = hitreje, ali več memorije. Preporučeno: 10-50.", + "Number of objects to process in each batch (1-5000).": "Število objekata za obradu u vsakoj seriji (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Število objekata za vektorizacijo u enkrat API klicu. Več = hitreje, ali več memorije. Preporučeno: 10-50.", + "Numeric Range:": "Številočani raspon:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Zbirka objekata", + "Object Configuration:": "Konfiguracija objekta:", + "Object Count Prediction": "Predvidjanje številoa objekata", + "Object Extraction Mode": "Način izvlačenja objekata", + "Object ID": "ID objekta", + "Object Management": "Upravljanje objektima", + "Object Text Extraction": "Izvlačenje teksta objekta", + "Object Vectorization": "Vektorizacija objekata", + "Object Warmup": "Zagrijavanje objekata", + "Object changes in owned objects": "Spremembe objekta u vlastitim objektima", + "Object collection not configured": "Zbirka objekata ni konfigurirana", + "Object metadata incomplete for file upload": "Metapodatki objekta nepotpuni za prijenos datoteke", + "Object not found": "Objekt ni najden", + "Object permanently deleted": "Objekt trajno izbrisan", + "Object restored successfully": "Objekt uspešno vračen", + "Object settings updated successfully": "Nastavitve objekta uspešno posodobljene", + "Object sources": "Izvori objekata", + "Object successfully deleted": "Objekt uspešno izbrisan", + "Object successfully modified": "Objekt uspešno izmijenjen", + "Object unlocked successfully": "Objekt uspešno otključan", + "Object vectorization configuration saved successfully": "Konfiguracija vektorizacije objekata uspešno spremljena", + "Object vectorized successfully": "Objekt uspešno vektoriziran", + "Object:": "Objekt:", + "Objects": "Objekti", + "Objects Analyzed": "Analizirani objekti", + "Objects Distribution": "Porazdelitev objekata", + "Objects being analyzed": "Objekti ki se analiziraju", + "Objects by Register": "Objekti po registru", + "Objects by Schema": "Objekti po shemi", + "Objects deletion completed for {schema}": "Brisanje objekata dokončano za {schema}", + "Objects deletion completed successfully": "Brisanje objekata uspešno dokončano", + "Objects in schema": "Objekti v shemi", + "Objects successfully deleted": "Objekti uspešno izbrisani", + "Objects to Process:": "Objekti za obradu:", + "Objects to be analyzed": "Objekti ki če biti analizirani", + "Objects to be deleted": "Objekti ki če biti izbrisani", + "Objects to be validated": "Objekti ki če biti preverjanjani", + "Objects will be serialized as JSON text before vectorization": "Objekti če biti serijalizirani kot JSON tekst pred vektorizacije", + "Objects will be soft deleted and moved to the": "Objekti če biti meko izbrisani in premješteni u", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekti če biti meko izbrisani (označeni kot izbrisani, ali zadržani v bazi podatkov). Lahko se kasni obnoviti ako je potrebno.", + "Old Value": "Stara vrednost", + "Oldest Item (days)": "Najstarija stavka (dani)", + "Ollama Chat Settings": "Nastavitve Ollama klepeta", + "Ollama Embedding Configuration": "Konfiguracija Ollama vdelava", + "Ollama URL": "Ollama URL", + "Only With Changes": "Le s spremembama", + "Only members of selected groups can access this organisation": "Le članti izbranih skupina lahko dostopiti ovoj organizaciji", + "Open Nextcloud App Store": "Otvori Nextcloud trgovinu aplikacija", + "Open OpenRegister sidebar": "Otvori OpenRegister bočnu traku", + "Open Register Objects": "Otvori objekte registra", + "Open folder": "Otvori mapu", + "Open in OpenRegister": "Otvori u OpenRegister", + "Open n8n Editor": "Otvori n8n uredjivač", + "OpenAI Chat Settings": "Nastavitve OpenAI klepeta", + "OpenAI Embedding Configuration": "Konfiguracija OpenAI vdelava", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister nastavitve", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Nadzorne plošče in načrtovane izvještaji ki definira operater. Vsaka nadzorna plošča je prvorazredni objekt u `reports` registru; widgeti se deklariraju u `widgets` polju nadzorne plošče in prikazuju uživo iz agregacija / GraphQL.", + "Optimizing search performance...": "Optimiziranje izvedbe iskanja...", + "Optional URL-friendly identifier": "Neobvezni identifikator prilagojen URL-u", + "Optional description for this facet": "Neobvezni opis za to fasetu", + "Optional end date property": "Neobvezno lastnost datuma završetka", + "Optional event description template": "Neobvezni predloga opisa dogadjaja", + "Optional location property": "Neobvezno lastnost lokacije", + "Optional webhook secret for signature verification": "Neobvezna webhook tajna za preverjanje potpisa", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Neobvezno: Dodajte parametre poizvedbaa za filtriranje referencirane sheme (npr. status=active&type=public)", + "Or": "Ili", + "Order": "Redoslijed", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Poredane BCP 47 jezične oznake. Prvi jezik je privzeti za register in pokreče Accept-Language zamjenu za prevodiva svojstva.", + "Organisation": "Organizacija", + "Organisation Configuration": "Konfiguracija organizacije", + "Organisation ID": "ID organizacije", + "Organisation Members": "Članti organizacije", + "Organisation Statistics": "Statistika organizacije", + "Organisation created successfully": "Organizacija uspešno ustvarjena", + "Organisation membership changes": "Spremembe članstva v organizaciji", + "Organisation name is required": "Naziv organizacije je obvezen", + "Organisation settings saved successfully": "Nastavitve organizacije uspešno spremljene", + "Organisation successfully created": "Organizacija uspešno ustvarjena", + "Organisation successfully updated": "Organizacija uspešno posodobljena", + "Organisational measures": "Organizacijske mjere", + "Organisations": "Organizacije", + "Organisatorische maatregelen": "Organizacijske mjere", + "Organization": "Organizacija", + "Organization ID (Optional)": "ID organizacije (neobvezno)", + "Organizations": "Organizacije", + "Orphaned Items": "Stavke brez roditelja", + "Other": "Ostalo", + "Output Mapping": "Mapiranje izlaza", + "Overview": "Pregled", + "Owner": "Vlasnik", + "Owner:": "Vlasnik:", + "PDF": "PDF", + "PII hits": "PII pogodci", + "PII rows": "PII vrstice", + "Page {current} of {total}": "Stranica {current} od {total}", + "Parallel Mode (Faster, more resource intensive)": "Paralelni način (hitreje, zahtevani za resurse)", + "Parallel:": "Paralelno:", + "Password": "Lozinka", + "Password changes are not supported by your authentication provider.": "Vaš davatelj avtentikacije ne podpira spremembe lozinke.", + "Password for SOLR authentication (optional)": "Lozinka za SOLR autentifikaciju (neobvezno)", + "Password updated successfully": "Lozinka uspešno posodobljena", + "Path": "Putanja", + "Path in repository": "Putanja u repozitoriju", + "Path where the configuration file will be saved in the repository": "Putanja na kojoj če konfiguracijska datoteka biti spremljena u repozitoriju", + "Path where the register OAS file will be saved in the repository": "Putanja na kojoj če OAS datoteka registra biti spremljena u repozitoriju", + "Pattern (regex)": "Obrazac (regex)", + "Pattern Issue": "Problem s obrascem", + "Pattern constraint is missing": "Nedostaje ovejičenje obrasca", + "Patterns:": "Obrasci:", + "Pending": "V čakanju", + "Permanent Delete Mode": "Način trajnog brisanja", + "Permanently Delete": "Trajno izbriši", + "Permanently Delete ({count})": "Trajno izbriši ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Trajno izbriši vsih {count} meko izbrisanih objekata. To se ne lahko poništiti!", + "Permanently delete already soft-deleted objects": "Trajno izbriši več meko izbrisane objekte", + "Permanently delete this conversation?": "Trajno izbrisati ta klepet?", + "Permission Matrix": "Matrica dozvola", + "Person": "Osoba", + "Personal Data": "Osobni podatki", + "Personal Data Export": "Izvoz osobnih podatkov", + "Persons": "Osobe", + "Phone": "Telefon", + "Phone Numbers": "Telefonski številoevi", + "Photos": "Fotografije", + "Please create an agent in the": "Ustvarite agenta u", + "Please select a user": "Izberite uporabnika", + "Please select an agent to continue": "Izberite agenta za nastavak", + "Please select an organisation": "Izberite organizacijo", + "Please select at least one item to import": "Izberite barem jednu stavku za uvoz", + "Please select registers or schemas first": "Prvo izberite registre ali sheme", + "Please select which register and schema to use for the new object": "Izberite katere register in shemu uporabiti za nti objekt", + "Please try again later.": "Pokušajte ponovno kasni.", + "Please wait while we fetch your agents.": "Počakajte medtem ko pridobivamo vaše agente.", + "Please wait while we fetch your applications.": "Počakajte medtem ko pridobivamo vaše aplikacije.", + "Please wait while we fetch your configurations.": "Počakajte medtem ko pridobivamo vaše konfiguracije.", + "Please wait while we fetch your deleted items.": "Počakajte medtem ko pridobivamo vaše izbrisane stavke.", + "Please wait while we fetch your sources.": "Počakajte medtem ko pridobivamo vaše izvore.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Počakajte medtem ko trajno brishemo SOLR zbirku. To lahko potrajati nekoliko trenutaka.", + "Polls": "Ankete", + "Popular Search Terms": "Popularni pojmti iskanja", + "Port": "Priključak", + "Portabiliteit (Art 20)": "Prenosivost (čl. 20)", + "Preferences saved": "Preferencije spremljene", + "Prerequisites": "Predpogoji", + "Press Enter to send, Shift+Enter for new line": "Pritisnite Enter za pošiljanje, Shift+Enter za nti vrstica", + "Press enter to send, shift+enter for new line": "Pritisnite enter za pošiljanje, shift+enter za nti vrstica", + "Prettify": "Uljepšaj", + "Preview erasure": "Pretpregled brisanja", + "Preview:": "Pretpregled:", + "Previous": "Prethodno", + "Private": "Privatno", + "Private Agent (Default)": "Privatni agent (privzeto)", + "Private agents": "Privatni agenti", + "Process Name (optional)": "Naziv procesa (neobvezno)", + "Processed {indexed} files, {failed} failed": "Obradjeno {indexed} datoteka, {failed} neuspešno", + "Processes chunks in batches with simulated parallelism.": "Obradjuje dijelte u serijama sa simuliranim paralelizmom.", + "Processes file chunks sequentially (safest).": "Obradjuje dijelte datoteke sekvencijalno (najsigurni).", + "Processes objects in chunks with simulated parallelism.": "Obradjuje objekte u kosovima sa simuliranim paralelizmom.", + "Processes objects sequentially (safest).": "Obradjuje objekte sekvencijalno (najsigurni).", + "Processing": "Obdelava", + "Processing Limits": "Ovejičenja obdelave", + "Processing...": "Obdelava...", + "Project initialized successfully": "Projekt uspešno inicijaliziran", + "Project not found. Please initialize first.": "Projekt ni najden. Prvo inicijalizirajte.", + "Properties": "Svojstva", + "Property Behaviors": "Vedenja svojstva", + "Property Configuration:": "Konfiguracija svojstva:", + "Property Filters": "Filtri svojstva", + "Property Title": "Naslov svojstva", + "Property Type": "Vrsta svojstva", + "Property can be improved": "Lastnost se lahko poboljšati", + "Property could not be added": "Lastnost se ni moglo dodati", + "Property could not be updated": "Lastnost se ni moglo ažurirati", + "Property name of inversed relation": "Naziv lastnosti obrnute veze", + "Property successfully added": "Lastnost uspešno dodano", + "Property successfully updated": "Lastnost uspešno posodobljeno", + "Protected": "Zaštičeno", + "Provider": "Davatelj", + "Provider is required for testing": "Davatelj je obvezen za testiranje", + "Public": "Javno", + "Public View": "Javni prikaz", + "Public views can be accessed by anyone in the system": "Javnim prikazima lahko dostopiti bilo tko u sistemu", + "Publish": "Objavi", + "Publish Configuration to GitHub": "Objavi konfiguraciju na GitHub", + "Publish OAS": "Objavi OAS", + "Publish Objects": "Objavi objekte", + "Publish Register OAS to GitHub": "Objavi OAS registra na GitHub", + "Publish all objects in this schema": "Objavi vse objekte u ovoj shemi", + "Publish objects to make them publicly available": "Objavi objekte kako bi bili javno dosegljivi", + "Published": "Objavljeno", + "Published:": "Objavljeno:", + "Publishing...": "Objavljivanje...", + "Purge": "Očisti", + "Purge Date": "Datum čiščenja", + "Purge {count} object from database": "Očisti {count} objekt iz baze podatkov", + "Purge {count} objects from database": "Očisti {count} objekata iz baze podatkov", + "Purpose limitation *": "Ovejičenje svrhe *", + "Push Notifications": "Push obvestiloi", + "Query Complexity Distribution": "Porazdelitev složenosti poizvedbaa", + "Query Help": "Pomoč za poizvedbae", + "Query parameter is required": "Parametar poizvedbaa je obvezen", + "Quota": "Kvota", + "RAG": "RAG", + "RAG Configuration": "RAG konfiguracija", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC nastavitve uspešno posodobljene", + "Rate limits cleared successfully": "Ovejičenja hitrine uspešno izbrisana", + "Raw Changes Data": "Neobradjeni podatki o spremembama", + "Re-vectorize on object update": "Ponovno vektoriziraj pri posodobljenju objekta", + "Read": "Čitanje", + "Real-time push notification status via notify_push": "Status push obvestiloi u stvarnom vremenu prek notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push obvestiloi u stvarnom vremenu so aktivne. Povezani klijenti prejmeju trenutna posodobljenja ko se objekti stvore, posodobiu ali izbrišu.", + "Realtime push active": "Push u stvarnom vremenu aktiven", + "Realtime push not available — the notify_push app is not installed": "Push u stvarnom vremenu ni dosegljiv — aplikacija notify_push ni instalirana", + "Reason": "Razlog", + "Reason (optional)": "Razlog (neobvezno)", + "Rebase operation completed successfully": "Operacija rebase uspešno dokončana", + "Rebase operation failed: {error}": "Operacija rebase ni uspjela: {error}", + "Rechtsgrond": "Pravna osnova", + "Recommendations:": "Prepočasenočila:", + "Recommended Type:": "Preporučena vrsta:", + "Recommended: 5 sources": "Preporučeno: 5 izvora", + "Refresh": "Osvježi", + "Refresh Data": "Osvježi podatke", + "Refresh Stats": "Osvježi statistiku", + "Refresh Workflows": "Osvježi tijekte rada", + "Refresh dashboard": "Osvježi nadzornu ploču", + "Refresh database info": "Osvježi informacije o bazi podatkov", + "Register": "Register", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Shema", + "Register ID": "ID registra", + "Register Objects": "Objekti registra", + "Register Overview": "Pregled registra", + "Register Statistics": "Statistika registra", + "Register Totals": "Skupni iznosi registra", + "Register and schema are required": "Register in shema so obvezni", + "Register annotation": "Anotacija registra", + "Register depublished successfully": "Register uspešno povučen iz objave", + "Register not found": "Register ni najden", + "Register objects": "Objekti registra", + "Register objects deletion completed successfully": "Brisanje objekata registra uspešno dokončano", + "Register published successfully": "Register uspešno objavljen", + "Register/Schema Usage": "Upomora registra/sheme", + "Register:": "Register:", + "Registers": "Registri", + "Reindex": "Ponovno indeksiraj", + "Reindex completed": "Ponovno indeksiranje dokončano", + "Reindex completed successfully": "Ponovno indeksiranje uspešno dokončano", + "Reindex completed! Processed {count} objects in {duration}s": "Ponovno indeksiranje dokončano! Obradjeno {count} objekata za {duration}s", + "Reindex failed": "Ponovno indeksiranje ni uspjelo", + "Reindex failed: %s": "Ponovno indeksiranje ni uspjelo: %s", + "Reject": "Odbij", + "Rejected": "Zavrnjeno", + "Related Cases": "Povezani predmeti", + "Relations": "Veze", + "Remote Version": "Oddaljena verzija", + "Remove": "Odstrani", + "Remove Deck card": "Odstrani Deck karticu", + "Remove avatar": "Odstrani avatar", + "Remove contact": "Odstrani kontakt", + "Remove filter": "Odstrani filter", + "Remove from favorites": "Odstrani iz favorita", + "Remove from list": "Odstrani s seznama", + "Remove group": "Odstrani skupino", + "Remove link": "Odstrani vezu", + "Remove link to {name}": "Odstrani vezu na {name}", + "Remove link to {name}?": "Odstraniti vezu na {name}?", + "Remove link to {title}": "Odstrani vezu na {title}", + "Remove oneOf entry": "Odstrani oneOf vnos", + "Remove property after write-back": "Odstrani lastnost po povratnega upisa", + "Remove {title}": "Odstrani {title}", + "Remove {userId} from this organisation?": "Odstraniti {userId} iz te organizacije?", + "Removed": "Odstranjeno", + "Removed from favorites": "Odstranjeno iz favorita", + "Removing schema {schema}...": "Odstranitev sheme {schema}...", + "Rename Conversation": "Preimenuj klepet", + "Rename conversation": "Preimenuj klepet", + "Replicas": "Replike", + "Reports": "Izvještaji", + "Repository": "Repozitorij", + "Request Body": "Tijelo zahteva", + "Request Data": "Podatki zahteva", + "Request Details": "Podrobnosti zahteva", + "Request Quota (per day)": "Kvota zahteva (po danu)", + "Request account deactivation": "Zatraži deaktivaciju računa", + "Request account deactivation. This will notify administrators for review.": "Zatraži deaktivaciju računa. Time če administratori biti obveščeni zaradi pregleda.", + "Request body must be JSON with a \"query\" field": "Tijelo zahteva mora biti JSON s poljem \"query\"", + "Request timeout in seconds": "Potek zahteva u sekundama", + "Requested at": "Zaiskano u", + "Required": "Obvezno", + "Required Token Scopes": "Obvezni opsezi tokena", + "Required field": "Obvezno polje", + "Required status is inconsistent": "Status obveznosti je nedosleden", + "Requires Dolphin AI for OCR text extraction": "Zahtijeva Dolphin AI za OCR izvlačenje teksta", + "Rerun Search": "Ponovno zaženi iskanje", + "Rerunning search: {searchTerm}": "Ponovno zagon iskanja: {searchTerm}", + "Reset Changes": "Poništi spremembe", + "Reset Filters": "Poništi filtre", + "Resource Quotas": "Kvote resursa", + "Response Body": "Tijelo odgovora", + "Response Details": "Podrobnosti odgovora", + "Response Time:": "Vrijeme odgovora:", + "Restore": "Obnovi", + "Restore conversation": "Novi klepet", + "Restore or permanently delete items": "Novi ali trajno izbriši stavke", + "Restore {count} object": "Novi {count} objekt", + "Restore {count} objects": "Novi {count} objekata", + "Result Count Range": "Raspon številoa rezultata", + "Results": "Rezultati", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Rok hramba (ISO-8601 trajanje, npr. P10Y, P30D)", + "Retention settings updated successfully": "Nastavitve hramba uspešno posodobljene", + "Retrieve all objects for this schema": "Dohvati vse objekte za to shemu", + "Retry": "Pokušaj ponovno", + "Retry Failed Extractions": "Ponti ni uspela izvlačenja", + "Retry Policy": "Pravila ponovnog pokušaja", + "Retry completed": "Ponovni pokušaj dovršen", + "Retry upload": "Ponti prijenos", + "Retrying...": "Ponovni pokušaj...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Pregledajte izbrane objekte u nastavku. Lahko odstraniti vse objekte ki ne želite izbrisati klikom na gumb za odstranitev.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Pregledajte izbrane objekte u nastavku. Lahko odstraniti vse objekte ki ne želite uključiti.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Pregledajte izbrane objekte u nastavku. Lahko odstraniti vse objekte ki ne želite trajno izbrisati klikom na gumb za odstranitev. Ta se dejanje ne lahko poništiti.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Pregledajte izbrane objekte u nastavku. Lahko odstraniti vse objekte ki ne želite obnoviti klikom na gumb za odstranitev. Objekti če biti vračeni na svojo izvornu lokaciju.", + "Revoke": "Opozovi", + "Right": "Desno", + "Risk Level": "Raven rizika", + "Risk level": "Raven rizika", + "Roles and Permissions": "Vloge in dozvole", + "Roles updated successfully": "Vloge uspešno posodobljene", + "Run compliance scan": "Zaženi skeniranje skladnosti", + "SOLR Connection Settings": "Nastavitve SOLR veze", + "SOLR Index Warmup": "Zagrijavanje SOLR indeksa", + "SOLR Version:": "SOLR verzija:", + "SOLR actions menu": "Izbornik SOLR dejanj", + "SOLR base path (usually /solr)": "SOLR osnovna putanja (obično /solr)", + "SOLR collection deleted successfully": "SOLR zbirka uspešno izbrisana", + "SOLR connection test failed: {error}": "Test SOLR veze ni uspio: {error}", + "SOLR connection test successful!": "Test SOLR veze uspešen!", + "SOLR index warmup completed successfully": "Zagrijavanje SOLR indeksa uspešno dokončano", + "SOLR is disabled": "SOLR je onemogučen", + "SOLR is not available or not configured": "SOLR ni dosegljiv ali ni konfiguriran", + "SOLR logging disabled": "SOLR zapisivanje onemogočeno", + "SOLR logging enabled": "SOLR zapisivanje omogočeno", + "SOLR search disabled": "SOLR iskanje onemogočeno", + "SOLR search enabled": "SOLR iskanje omogočeno", + "SOLR server hostname or IP address": "SOLR naziv strežnika ali IP adresa", + "SOLR server port number (optional, defaults to 8983)": "Število priključka SOLR strežnika (neobvezno, privzeto 8983)", + "SOLR settings updated successfully": "SOLR nastavitve uspešno posodobljene", + "SOLR setup completed successfully": "SOLR postavljanje uspešno dokončano", + "SOLR setup completed successfully!": "SOLR postavljanje uspešno dokončano!", + "SOLR setup error: %s": "Napaka SOLR postavljanja: %s", + "SOLR setup failed": "SOLR postavljanje ni uspjelo", + "SOLR setup failed - check logs": "SOLR postavljanje ni uspjelo - preverite zapisnike", + "SOLR setup failed. Please check the configuration and try again.": "SOLR postavljanje ni uspjelo. Preverite konfiguraciju in pokušajte ponovno.", + "SOLR setup failed: {error}": "SOLR postavljanje ni uspjelo: {error}", + "SOLR warmup failed: {error}": "Zagrijavanje SOLR ni uspjelo: {error}", + "Sample Values:": "Uzorci vrednosti:", + "Save": "Shrani", + "Save Configuration": "Shrani konfiguraciju", + "Save Connection Settings": "Shrani nastavitve veze", + "Save Roles": "Shrani vloge", + "Save Settings": "Shrani nastavitve", + "Save changes": "Shrani spremembe", + "Save current search as view": "Shrani trenutno iskanje kot prikaz", + "Save labels": "Shrani oznake", + "Save tags for {name}": "Shrani oznake za {name}", + "Save the organisation first to manage users.": "Prvo shranite organizacijo za upravljanje uporabnikima.", + "Saved Views": "Shranljeni prikazi", + "Saving...": "Shranjujenje...", + "Scheduled": "Zakazano", + "Schema": "Shema", + "Schema ID": "ID sheme", + "Schema Name": "Naziv sheme", + "Schema Statistics": "Statistika sheme", + "Schema annotation": "Anotacija sheme", + "Schema configuration updated successfully for {schema}": "Konfiguracija sheme uspešno posodobljena za {schema}", + "Schema depublished successfully": "Shema uspešno umaknjena iz objave", + "Schema objects deletion completed successfully": "Brisanje objekata sheme uspešno dokončano", + "Schema published successfully": "Shema uspešno objavljena", + "Schema reference ($ref)": "Referenca sheme ($ref)", + "Schema successfully created": "Shema uspešno ustvarjena", + "Schema successfully updated": "Shema uspešno posodobljena", + "Schema {schema} removed successfully": "Shema {schema} uspešno uklonjena", + "Schema:": "Shema:", + "Schemas": "Sheme", + "Scheme": "Shema", + "Search": "Išči", + "Search / Views": "Iskanje / Prikazi", + "Search / views": "Iskanje / prikazi", + "Search Activity": "Aktivnost iskanja", + "Search Analytics": "Analitika iskanja", + "Search Configuration": "Konfiguracija iskanja", + "Search Entities": "Išči entitetee", + "Search Files": "Išči datoteke", + "Search GitHub": "Išči GitHub", + "Search GitLab": "Išči GitLab", + "Search Method": "Metoda iskanja", + "Search Mode": "Način iskanja", + "Search Query": "Poizvedba iskanja", + "Search Results": "Rezultati iskanja", + "Search Term": "Pojam iskanja", + "Search Trail Details": "Podrobnosti traga iskanja", + "Search Trail Management": "Upravljanje tragom iskanja", + "Search Trail Statistics": "Statistika traga iskanja", + "Search Trails": "Tragti iskanja", + "Search Views": "Prikazi iskanja", + "Search Webhooks": "Išči webhookove", + "Search agents...": "Išči agente...", + "Search applications...": "Išči aplikacije...", + "Search by file name or path": "Išči po nazivu datoteke ali putanji", + "Search by name or URL": "Išči po nazivu ali URL-u", + "Search by title or UUID...": "Išči po naslovu ali UUID...", + "Search by value": "Išči po vrednosti", + "Search configurations": "Išči konfiguracije", + "Search data sources...": "Išči izvore podatkov...", + "Search groups...": "Išči skupine...", + "Search in Database Objects": "Išči objekte v bazi podatkov", + "Search in Files": "Išči datoteke", + "Search in Objects": "Išči objekte", + "Search objects": "Išči objekte", + "Search objects...": "Išči objekte...", + "Search property names...": "Išči nazive lastnosti...", + "Search registers...": "Išči registre...", + "Search schemas...": "Išči sheme...", + "Search trail deletion not implemented yet": "Brisanje traga iskanja še ni implementirano", + "Search views...": "Išči prikaze...", + "Search {name}...": "Išči {name}...", + "Searching...": "Iskanje...", + "Secret": "Tajna", + "Secret key for HMAC signature generation (optional)": "Tajni ključ za generiranje HMAC potpisa (neobvezno)", + "Security": "Sigurnost", + "See organisations as a table": "Prikaži organizacije kot tablicu", + "See organisations as cards": "Prikaži organizacije kot kartice", + "Select AI Agent": "Izberi AI agenta", + "Select Active Organisation": "Izberi aktivno organizacijo", + "Select All": "Izberi vse", + "Select ConfigSet": "Izberite ConfigSet", + "Select File Types to Vectorize:": "Izberite vrste datoteka za vektorizacijo:", + "Select HTTP method": "Izberite HTTP metodu", + "Select Register and Schema": "Izberite Register in Shemu", + "Select Views to Vectorize:": "Izberite prikaze za vektorizacijo:", + "Select a Nextcloud group to add": "Izberite Nextcloud skupino za dodavanje", + "Select a branch": "Izberite granu", + "Select a date property": "Izberite lastnost datuma", + "Select a model or type a custom model name": "Izberite model ali upišite prilagojeni naziv modela", + "Select a register": "Izberite register", + "Select a register...": "Izberite register...", + "Select a repository": "Izberite repozitorij", + "Select a repository you have write access to": "Izberite repozitorij na ki imate pravo pisanja", + "Select a schema": "Izberite shemu", + "Select a schema...": "Izberite shemu...", + "Select a user": "Izberite uporabnika", + "Select agent type": "Izberite vrstu agenta", + "Select all entities": "Izberite vse entitetee", + "Select an AI Agent": "Izberite AI agenta", + "Select an AI agent to begin chatting with your data.": "Izberite AI agenta za početak klepeta s vašim podatkov.", + "Select an email to see linked objects": "Izberite e-poštu za prikaz povezanih objekata", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Izberite organizacijo in uporabnika da bi ih dodali kot člana. Iščite organizacije po nazivu.", + "Select backend": "Izberite zaledni sistem", + "Select chat model": "Izberite model klepeta", + "Select collection for files": "Izberite kolekciju za datoteke", + "Select collection for objects": "Izberite kolekciju za objekte", + "Select configuration type...": "Izberite vrstu konfiguracije...", + "Select default organisation": "Izberite privzeto organizacijo", + "Select entity {value}": "Izberite entitete {value}", + "Select event to listen to...": "Izberite dogadjaj ki če se osluškivati...", + "Select fields to display": "Izberite polja za prikaz", + "Select files with the correct extension": "Izberite datoteke s pravilnom ekstenzijom", + "Select group": "Izberite skupino", + "Select groups (optional)": "Izberite skupine (neobvezno)", + "Select groups to notify...": "Izberite skupine za obavještavanje...", + "Select groups with access to this agent": "Izberite skupine s dostopom tem agentu", + "Select groups...": "Izberite skupine...", + "Select model": "Izberite model", + "Select one or more registers": "Izberite en ali več registera", + "Select one or more schemas": "Izberite jednu ali več shema", + "Select options...": "Izberite opcije...", + "Select or create labels, or select \"No label\" to add files": "Izberite ali ustvarite oznake ali izberite \"Brez oznake\" za dodavanje datoteka", + "Select owner application (optional)...": "Izberite vlasničku aplikaciju (neobvezno)...", + "Select period": "Izberite obdobje", + "Select property to send as payload": "Izberite lastnost za pošiljanje kot korisni teret", + "Select provider": "Izberite ponudnika usluge", + "Select registers and schemas to save a view": "Izberite registre in sheme za shranjujenje prikaza", + "Select retry policy": "Izberite pravilo ponovnog pokušaja", + "Select role": "Izberite ulogu", + "Select schemas (empty = all schemas)": "Izberite sheme (prazno = vse sheme)", + "Select schemas to warm up": "Izberite sheme za zagrijavanje", + "Select search mode": "Izberite način iskanja", + "Select source type...": "Izberite vrstu izvora...", + "Select target type": "Izberite ciljnu vrstu", + "Select the branch to publish to": "Izberite granu za objavljivanje", + "Select the event this webhook should listen to": "Izberite dogadjaj ki bi ta webhook trebao osluškivati", + "Select views to filter data (optional)": "Izberite prikaze za filtriranje podatkov (neobvezno)", + "Select views to limit which data the agent can access": "Izberite prikaze za ovejičavanje podatkov ki agent lahko dostopiti", + "Select views to vectorize:": "Izberite prikaze za vektorizacijo:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Izberite ki so Nextcloud skupine dosegljive za to organizacijo. Uporabniki u tem skupinama imeti če dostop resursima organizacije.", + "Select which data views the AI can search": "Izberite katere prikaze podatkov AI lahko iskati", + "Select which property from the event should be used as the webhook payload data": "Izberite katere lastnost iz dogadjaja mora uporabiti kot podatke payload webhooka", + "Select which tools the AI can use to perform actions": "Izberite katere alate UI lahko uporablja za izvodjenje dejanj", + "Select {fieldName} values": "Izberite vrednosti {fieldName}", + "Selected": "Izbrano", + "Selected Groups": "Izbrane skupine", + "Selected Objects ({count})": "Izbrani objekti ({count})", + "Selected audit trails deleted successfully": "Izbrani revizijski tragti uspešno so izbrisani", + "Selected search trails deleted successfully": "Izbrani tragti iskanja uspešno so izbrisani", + "Selected users can access this view": "Izbrani uporabniki lahko dostopiti tem prikazu", + "Send additional feedback": "Pošljite dodatne povratne informacije", + "Send as CloudEvent": "Pošlji kot CloudEvent", + "Send as cloudevent": "Pošlji kot cloudevent", + "Sensitive PII": "Osjetljivi PII", + "Serial Mode (Safer, slower)": "Serijski način rada (sigurniji, počaseniji)", + "Serial:": "Serijski:", + "Server Configuration": "Konfiguracija strežnika", + "Server Information": "Informacije o strežniku", + "Set as Active": "Postavi kot aktivno", + "Set as active organisation": "Postavi kot aktivno organizacijo", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Postavite ovejičenja za uporaba API-ja in potrošnju tokena. Uporabite 0 za neomejene resurse.", + "Settings": "Nastavitve", + "Settings updated successfully": "Nastavitve so uspešno posodobljene", + "Shards": "Krhotine", + "Share": "Dijeli", + "Share with Groups": "Dijeli s skupinama", + "Share with Users": "Dijeli s uporabnikima", + "Show": "Prikaži", + "Show Filters": "Prikaži filtre", + "Show less": "Prikaži manj", + "Show only entries with changes": "Prikaži le vnose s spremembama", + "Showing {count} dashboard(s)": "Prikazuje se {count} nadzornih ploča", + "Showing {count} processing activities": "Prikazuje se {count} aktivnosti obdelave", + "Showing {showing} of {total} agents": "Prikazuje se {showing} od {total} agenata", + "Showing {showing} of {total} applications": "Prikazuje se {showing} od {total} aplikacija", + "Showing {showing} of {total} audit trail entries": "Prikazuje se {showing} od {total} vnosa revizijskega traga", + "Showing {showing} of {total} configurations": "Prikazuje se {showing} od {total} konfiguracija", + "Showing {showing} of {total} deleted items": "Prikazuje se {showing} od {total} izbrisanih stavki", + "Showing {showing} of {total} entities": "Prikazuje se {showing} od {total} entitetea", + "Showing {showing} of {total} files": "Prikazuje se {showing} od {total} datoteka", + "Showing {showing} of {total} log entries": "Prikazuje se {showing} od {total} vnosa zapisnika", + "Showing {showing} of {total} search trail entries": "Prikazuje se {showing} od {total} vnosa traga iskanja", + "Showing {showing} of {total} sources": "Prikazuje se {showing} od {total} izvora", + "Showing {showing} of {total} templates": "Prikazuje se {showing} od {total} predlogaa", + "Showing {showing} of {total} webhooks": "Prikazuje se {showing} od {total} webhookova", + "Showing {shown} of {total} organisations": "Prikazuje se {shown} od {total} organizacija", + "Simple": "Jednostavno", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednostavni poizvedbai: osnovna iskanja teksta s minimalnim parametri (npr. en pojam za iskanje, brez naprednih filtera)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednostavni poizvedbai: osnovna iskanja teksta s minimalnim parametri (npr. en pojam za iskanje, brez naprednih filtera)", + "Size": "Velikost", + "Slug": "Slug", + "Soft Delete Mode": "Način meke brisanja", + "Soft Deleted Items": "Meko izbrisane stavke", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Meko izbriši vse objekte za to shemu ({active} aktivnih, {deleted} več izbrisanih)", + "SolrCloud mode enabled": "SolrCloud način rada omogučen", + "Something went wrong while adding files": "Prišlo je do napake prilikom dodavanja datoteka", + "Source": "Izvor", + "Source successfully updated": "Izvor je uspešno posodobljen", + "Sources": "Izvori", + "Sources:": "Izvori:", + "Special Rights": "Posebna prava", + "Standalone SOLR mode": "Samostalni SOLR način rada", + "Start Conversation": "Započni klepet", + "Start Date Field": "Polje datuma početka", + "Start Vectorization": "Zaženi vektorizaciju", + "Start a conversation": "Započni klepet", + "Starting names cache warmup...": "Zagon zagrijavanja predpomnilnika naziva...", + "Starting validation for {schema}...": "Zagon preverjanja veljavnosti za {schema}...", + "Starting {action} for {schema}...": "Zagon {action} za {schema}...", + "Starting...": "Zagon...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika in mjerni podatki", + "Statistics refreshed": "Statistika je osvježena", + "Stats calculation not yet implemented": "Izračun statistike še ni implementiran", + "Status": "Status", + "Status Code": "Statusni kod", + "Step": "Korak", + "Storage": "Shramba", + "Storage Quota (MB)": "Kvota shrambe (MB)", + "String Constraints": "Ovejičenja niza", + "Subject identifier (email, BSN, name, etc.)": "Identifikator subjekta (e-pošta, BSN, ime itd.)", + "Subscribe": "Pretplati se", + "Subscribed": "Pretplačeno", + "Success": "Uspjeh", + "Success Rate": "Stopa uspjeha", + "Success Status": "Status uspjeha", + "Successful": "Uspešno", + "Successfully added user to organisation: {name}": "Uporabnik je uspešno dodan v organizacijo: {name}", + "Successfully cleared {count} audit trails": "Uspešno je izbrisano {count} revizijskih tragova", + "Successfully cleared {count} blob storage objects": "Uspešno je izbrisano {count} objekata blob shrambe", + "Successfully cleared {count} search trails": "Uspešno je izbrisano {count} tragta iskanja", + "Successfully created {count} SOLR fields": "Uspešno je ustvarjeno {count} SOLR polja", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Uspešno je izbrisano {count} vdelava. Ponovno vektorizirajte svoje podatke.", + "Successfully deleted {count} objects for {schema}": "Uspešno je izbrisano {count} objekata za {schema}", + "Successfully fixed {count} SOLR field configurations": "Uspešno je popravljeno {count} konfiguracija SOLR polja", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Uspešno uvezeno: {registers} registera, {schemas} shema, {objects} objekata", + "Successfully joined organisation": "Uspešno ste se pridružili organizaciji", + "Successfully permanently deleted {count} objects": "Uspešno je trajno izbrisano {count} objekata", + "Successfully processed {count} files!": "Uspešno je obradjeno {count} datoteka!", + "Successfully restored {count} object": "Uspešno je obnovljen {count} objekt", + "Successfully restored {count} objects": "Uspešno je obnovljeno {count} objekata", + "Successfully saved configuration for {count} facets!": "Uspešno je spremljena konfiguracija za {count} faseta!", + "Supported File Types": "Podprte vrste datoteka", + "Switch Active Organisation": "Promijeni aktivno organizacijo", + "Switch Organisation": "Promijeni organizacijo", + "Sync Table": "Sinkroniziraj tablicu", + "Synchronization Settings": "Nastavitve sinkronizacije", + "Syncing magic table for {schema}...": "Sinkronizacija magične tabele za {schema}...", + "Syncs": "Sinkronizacije", + "System Default": "Privzeto sistemom", + "System Prompt": "Sistemski poizvedba", + "System announcements": "Obvestiloi sistema", + "Table": "Tabela", + "Target ID": "Ciljni ID", + "Target Type*": "Ciljna vrsta*", + "Technical Description": "Tehnički opis", + "Technical Details": "Tehnički detalji", + "Technical description for developers and administrators": "Tehnički opis za razvojne programere in administratore", + "Technical measures": "Tehničke mjere", + "Technische maatregelen": "Tehničke mjere", + "Temperature": "Temperatura", + "Templates": "Predlošci", + "Test": "Test", + "Test Chat": "Testni klepet", + "Test Connection": "Testiraj vezu", + "Test Embedding": "Testiraj vdelava", + "Test endpoint executed successfully": "Testna končna točka uspešno je izvršena", + "Test webhook delivered successfully": "Testni webhook uspešno je ipočasenučen", + "Test webhook delivery failed": "Ispočasenočilo testnog webhooka ni uspjela", + "Test webhook sent successfully": "Testni webhook uspešno je poslan", + "Testing...": "Testiranje...", + "Text Chunk #{id}": "Besedilni odlomek #{id}", + "Text Extraction": "Izvlačenje teksta", + "Text Extraction Engine": "Mehanizam za izvlačenje teksta", + "Text chunks": "Besedilni odlomki", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Besedilni odlomki generiraju se tijekom izvlačenja datoteka in pohranjuju v bazo podatkov. Vektorizacija čita te unaprijed isječene datoteke in pretvara ih u vdelava.", + "Text extracted successfully": "Tekst je uspešno izvučen", + "Text extraction disabled": "Izvlačenje teksta onemogočeno", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplikacija Nextcloud Calendar ni instalirana ali omogočena na tem strežniku.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplikacija Nextcloud Contacts ni instalirana ali omogočena na tem strežniku.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplikacija Nextcloud Deck ni instalirana ali omogočena na tem strežniku.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplikacija Nextcloud Mail ni instalirana ali omogočena na tem strežniku.", + "The URL where webhook events will be sent": "URL na ki če se slati webhook dogadjaji", + "The requested conversation does not exist": "Traženi klepet ne obstaja", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Tražena nadzorna plošča ni se mogla učitati. Preverite URL ali izberite drugu nadzornu ploču s seznama.", + "The saved view and all its search configuration": "Shranljeni prikaz in vsa njegta konfiguracija iskanja", + "There are no audit trail entries matching your current filters.": "Nema vnosa revizijskega traga ki odgovaraju vašim trenutnim filtrima.", + "There are no deleted items matching your current filters.": "Nema izbrisanih stavki ki odgovaraju vašim trenutnim filtrima.", + "There are no search trail entries matching your current filters.": "Nema vnosa traga iskanja ki odgovaraju vašim trenutnim filtrima.", + "There are no webhook log entries matching your filters.": "Nema vnosa zapisnika webhooka ki odgovaraju vašim filtrima.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Bo zadržani v skladu s konfiguriranim rokom hramba njihove sheme in samodejno trajno izbrisani ko rok hramba istekne. Rok hramba moguče je konfigurirati po shemi in nalazi se u nastavitvami sheme.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Bo zadržani v skladu s konfiguriranim rokom hramba njihove sheme, a nato trajno odstranjeni.", + "This action cannot be undone.": "Ta se dejanje ne lahko poništiti.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Ta se dejanje ne lahko poništiti. Preverite da nobena kolekcija ne se uporablja ta ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Ta se dejanje ne lahko poništiti. Preverite da nobena kolekcija ne se uporablja ta configset.", + "This action will submit a deactivation request to your administrators.": "Ta če dejanje poslati zahteva za deaktivaciju vašim administratorima.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Ta agent nema konfigurabilne prikaze alin orodja, ali še vedno lahko prilagoditi RAG nastavitve u nastavku.", + "This analysis may take some time": "Ta analiza lahko potrajati", + "This audit trail entry does not contain any change information.": "Ta vnos revizijskega traga ne sadrži nikakve informacije o spremembama.", + "This configuration has no data defined.": "Ta konfiguracija nema določene podatke.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ta je končna točka zastarela. Namesto tega uporabite končne točke temeljene na odlomkima.", + "This entity has no relations to objects or files": "Ta entitete nema veza s objektima ali datotekama", + "This is a test webhook from OpenRegister": "To je testni webhook iz OpenRegister", + "This is not valid JSON": "To ni veljavni JSON", + "This key already exists on this schema": "Ta ključ več obstaja u ovoj shemi", + "This schema must use Magic Table configuration to sync": "Ta shema mora uporabiti konfiguraciju Magične tabele za sinkronizaciju", + "This schema must use magic table configuration to sync": "Ta shema mora uporabiti konfiguraciju magične tabele za sinkronizaciju", + "This source has no associated registers.": "Ta izvor nema povezanih registera.", + "This token will only be shown once. Copy it now.": "Ta če se token prikazati le jednom. Kopirajte ga zdaj.", + "This will copy the _default ConfigSet with the new name": "To bo kopirati _default ConfigSet s novim nazivom", + "This will copy the _default configset with the new name": "To bo kopirati _default configset s novim nazivom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "To bo trajno izbrisati VSE vdelava (vektore) iz baze podatkov. Morat boste ponovno vektorizirati vse objekte in datoteke. Ta se dejanje ne lahko poništiti.\n\nSte prepričani, da želite nastaviti?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "To bo trajno izbrisati VSE vdelava (vektore) iz baze podatkov. Morat boste ponovno vektorizirati vse objekte in datoteke. Ta se dejanje ne lahko poništiti.\\n\\nSte prepričani, da želite nastaviti?", + "This will permanently delete:": "To bo trajno izbrisati:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "To bo trajno odstraniti {count} več meko izbrisanih objekata iz baze podatkov. Ta se dejanje ne lahko poništiti.", + "Timeout (seconds)": "Vremensko ovejičenje (sekunde)", + "Timestamp": "Vremenska oznaka", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Nasvet: Omogočite le prikaze ki je potrebno semantično iskanje da bi minimizirali troškte vdelava. Jednostavne tabele za iskanje rijetko morajo vektorizaciju.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Nasvet: omogočite le prikaze ki je potrebno semantično iskanje da bi minimizirali troškte vdelava. Jednostavne tabele za iskanje rijetko morajo vektorizaciju.", + "Title": "Naslov", + "Title *": "Naslov *", + "Title Template": "Predloga naslova", + "Title*": "Naslov*", + "To (e.g. -1 year or 2025-12-31)": "Do (npr. -1 godina ali 2025-12-31)", + "To Date": "Do datuma", + "To add files larger than or equal to 512MB, go to the": "Za dodavanje datoteka večih ali jednakih 512 MB, idite na", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Za prilagodbu velikosti odlomka ali strategije, idite na Konfiguracija datoteke → Ovejičenja obdelave.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Za prilagodbu velikosti odlomka ali strategije, idite na konfiguracija datoteke → ovejičenja obdelave.", + "To change the embedding provider or model, go to LLM Configuration.": "Za spremembo ponudnika storitve vdelava ali modela, idite na LLM konfiguraciju.", + "To change the embedding provider or model, go to LLM configuration.": "Za spremembo ponudnika storitve vdelava ali modela, idite na LLM konfiguraciju.", + "To date": "Do datuma", + "Toggle search sidebar": "Uključi/izključi bočnu traku iskanja", + "Token Created": "Token ustvarjen", + "Token Quota (per request)": "Kvota tokena (po zahtevau)", + "Token copied to clipboard": "Token kopiran u medjuspremnik", + "Token name": "Naziv tokena", + "Token revoked": "Token opozvan", + "Tools": "Alati", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Alati se izvršavaju sa privzetim uporabniškim ovlastima agenta ko nobena korisnička sesija ni aktivna (npr. cron poslovi). Konfigurirajte privzetog uporabnika na kartici Nastavitve.", + "Tools respect the agent's own role and organization boundaries.": "Alati poštuju vlastitu ulogu agenta in vejice organizacije.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Alati poštuju prikaze, ovlasti in vejice organizacije agenta.", + "Top Deleters": "Najčešči brisači", + "Total": "Skupaj", + "Total Audit Trails": "Skupaj revizijskih tragova", + "Total Chunks Available:": "Skupaj dosegljivih odlomeka:", + "Total Deleted Items": "Skupaj izbrisanih stavki", + "Total Members": "Skupaj članova", + "Total Objects": "Skupaj objekata", + "Total Objects in Database:": "Skupaj objekata v bazi podatkov:", + "Total Organisations": "Skupaj organizacija", + "Total Results": "Skupaj rezultata", + "Total Searches": "Skupaj iskanja", + "Total Size": "Skupna velikost", + "Total Storage": "Skupna shramba", + "Total events": "Skupaj dogadjaja", + "Total:": "Skupaj:", + "Totals": "Skupni iznosi", + "Try searching by UUID or with different keywords": "Pokušajte iskati po UUID-u ali s drugačijim ključnim riječima", + "Type": "Vrsta", + "Type \"DELETE COLLECTION\" to confirm:": "Upišite \"DELETE COLLECTION\" za potvrdu:", + "Type Issue": "Vrsta problema", + "Type Variations:": "Varijacije vrste:", + "Type filter (optional, e.g. email)": "Filter vrste (neobvezno, npr. e-pošta)", + "Type must be \"positive\" or \"negative\"": "Vrsta mora biti \"positive\" ali \"negative\"", + "Type to search for groups": "Upišite za iskanje skupina", + "Type to search for objects...": "Upišite za iskanje objekata...", + "Type to search for organisations": "Upišite za iskanje organizacija", + "Type to search for users": "Upišite za iskanje uporabnika", + "Type to search groups...": "Upišite za iskanje skupina...", + "Type to search users...": "Upišite za iskanje uporabnika...", + "Type to search...": "Upišite za iskanje...", + "Type your username to confirm": "Upišite svoje uporabniško ime za potvrdu", + "URL": "URL", + "URL where Ollama is running": "URL na kojem se izvaja Ollama", + "UUID copied to clipboard": "UUID kopiran u medjuspremnik", + "UUID:": "UUID:", + "Unchanged": "Nespremenjeno", + "Unique Search Terms": "Jedinstveni pojmti za iskanje", + "Unique Terms": "Jedinstveni pojmovi", + "Unknown": "Neznano", + "Unknown Object": "Neznan objekt", + "Unknown Register": "Neznan register", + "Unknown Schema": "Neznana shema", + "Unknown error": "Neznana napaka", + "Unknown widget type: {type}": "Neznana vrsta widgeta: {type}", + "Unlink email": "Odveži e-poštu", + "Unlink event": "Odveži dogadjaj", + "Unnamed Object": "Neimenovani objekt", + "Untitled": "Brez naslova", + "Untitled View": "Prikaz brez naslova", + "Update": "Posodobi", + "Update Operations": "Operacije posodobljenja", + "Update available: {local} → {remote}": "Posodobljenje na voljo: {local} → {remote}", + "Update configuration: ...": "Posodobi konfiguraciju: ...", + "Update register OAS: ...": "Posodobi OAS registra: ...", + "Update vectors when object data changes (recommended for accurate search)": "Posodobi vektore ko se podatki objekta promijene (preporučeno za točno iskanje)", + "Updated": "Posodobljeno", + "Updated At": "Posodobljeno u", + "Updated:": "Posodobljeno:", + "Upload new avatar": "Prenesi nti avatar", + "Uploading configuration sets...": "Prenošenje skupta konfiguracije...", + "Uptime:": "Vrijeme rada:", + "Url": "Url", + "Usage Count": "Število korištenja", + "Use AI agents for processing and analysis": "Uporabite AI agente za obradu in analizu", + "Use Large Language Model features": "Uporabite funkcije velikog jezikovnega modela", + "Use SolrCloud with Zookeeper for distributed search": "Uporabite SolrCloud sa Zookeeperom za distribuirano iskanje", + "Use SolrCloud with zookeeper for distributed search": "Uporabite SolrCloud sa zookeeperom za distribuirano iskanje", + "Use VNG APIs instead of MongoDB.": "Uporabite VNG API-je namesto MongoDB-a.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Uporabite privzete raspone (zadnjih 7/30/90 dana, prošla godina, starije)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Uporabite vanjsku shranjevanje (npr. MongoDB) namesto interne Nextcloud shrambe.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Uporabite filtre za zožanje vnosa revizijskega traga glede na registru, shemi, vrsti dejanja, uporabniku, rasponu datuma ali ID-u objekta.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Uporabite filtre za zožanje izbrisanih stavki glede na registru, shemi, datumu brisanja ali uporabniku ki ih je izbrisao.", + "Use filters to narrow down entities by type or category.": "Uporabite filtre za zožanje entitetea glede na vrsti ali kategoriji.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Uporabite filtre za zožanje vnosov sledi iskanja glede na register, shemo, status uspešnosti, uporabnika, datumski razpon, iskalne pojme ali meritve zmogljivosti.", + "Used By Collections": "Korišteno od strane kolekcija", + "Used by": "Korišteno od strane", + "User": "Uporabnik", + "User Agent Statistics": "Statistika uporabniškog agenta", + "User Description": "Uporabniški opis", + "User Quota": "Korisnička kvota", + "User rate limits cleared successfully": "Ovejičenja hitrine uporabnika uspešno so izbrisana", + "User removed successfully": "Uporabnik je uspešno uklonjen", + "User-friendly description shown in forms and help text": "Uporabniku prilagojen opis prikazan u obrascima in tekstu pomoči", + "Username": "Uporabniško ime", + "Username for SOLR authentication (optional)": "Uporabniško ime za SOLR autentifikaciju (neobvezno)", + "Users": "Uporabniki", + "Uses": "Korištenja", + "Using Pre-Generated Chunks": "Uporaba unaprijed generiranih odlomeka", + "Validate": "Provjeri veljavnost", + "Validate Objects": "Provjeri veljavnost objekata", + "Validation completed for {schema}": "Preverjanje veljavnosti dokončana za {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Preverjanje veljavnosti dokončana za {schema}: {processed} obradjeno, {updated} posodobljeno, {failed} neuspešno", + "Validation completed successfully": "Preverjanje veljavnosti uspešno je dokončana", + "Validation failed": "Preverjanje veljavnosti ni uspjela", + "Value": "Vrednost", + "Value list (split on ,)": "Seznam vrednosti (razdvojeno s ,)", + "Vector Dimensions": "Dimenzije vektora", + "Vector Search Backend": "Zaledni sistem za vektorsko iskanje", + "Vector field: _embedding_": "Vektorsko polje: _embedding_", + "Vectorization Triggers": "Okidači vektorizacije", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorizacija dokončana: {vectorized} objekata vektorizirano, {failed} neuspešno", + "Vectorization failed: {error}": "Vektorizacija ni uspjela: {error}", + "Vectorize All Files": "Vektoriziraj vse datoteke", + "Vectorize All Objects": "Vektoriziraj vse objekte", + "Vectorize all file types": "Vektoriziraj vse vrste datoteka", + "Vectorize all views": "Vektoriziraj vse prikaze", + "Vectorize on object creation": "Vektoriziraj pri ustvarjanju objekta", + "Vectors will be stored in your existing object and file collections": "Vektori če biti pohranjeni v vaših postoječim kolekcijama objekata in datoteka", + "Verantwoording": "Obrazloženje", + "Verifying server connectivity...": "Preverjanje povezivosti strežnika...", + "Version": "Verzija", + "Version Information": "Informacije o verziji", + "Very High": "Vrlo visoko", + "Very high": "Vrlo visoko", + "View": "Prikaz", + "View API Docs": "Pregledaj API dokumentaciju", + "View API Documentation": "Pregledaj API dokumentaciju", + "View APIs": "Pregledaj API-je", + "View Changes": "Pregledaj spremembe", + "View Details": "Oglej si podrobnosti", + "View Error": "Pregledaj napako", + "View File": "Pregledaj datoteku", + "View Full Details": "Oglej si vse podrobnosti", + "View Logs": "Pregledaj zapisnike", + "View Name": "Naziv prikaza", + "View Object": "Pregledaj objekt", + "View Parameters": "Pregledaj parametre", + "View Selection": "Izbor prikaza", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Pregledajte in analizirajte zapisnike traga iskanja s naprednim zmožnostima filtriranja in analitike", + "View and analyze system audit trails with advanced filtering capabilities": "Pregledajte in analizirajte revizijske tragte sistema s naprednim zmožnostima filtriranja", + "View and manage authorization across registers and schemas": "Pregledajte in upravljajte ovlaštenjima u registrom in shemama", + "View deleted successfully": "Prikaz je uspešno izbrisan", + "View entity details and manage relations": "Pregledajte podrobnosti entitetea in upravljajte vezama", + "View name": "Naziv prikaza", + "View name is required": "Naziv prikaza je obvezen", + "View object": "Pregledaj objekt", + "View saved successfully!": "Prikaz je uspešno spremljen!", + "View search analytics and manage search logs": "Pregledajte analitiku iskanja in upravljajte zapisnicima iskanja", + "View successfully deleted": "Prikaz je uspešno izbrisan", + "View successfully updated": "Prikaz je uspešno posodobljen", + "View updated successfully!": "Prikaz je uspešno posodobljen!", + "View webhook delivery logs and filter by webhook": "Pregledajte zapisnike ispočasenočila webhooka in filtrirajte po webhooku", + "View {count} more": "Pregledaj še {count}", + "Views": "Prikazi", + "Visible to end users": "Vidljivo krajnjim uporabnikima", + "Visible to users": "Vidljivo uporabnikima", + "Wait for Response": "Čekaj odgovor", + "Wait for webhook response before continuing (required for request/response flows)": "Čekaj odgovor webhooka pred nastavka (obvezno za tokte zahteva/odgovor)", + "Waiting for cluster synchronization...": "Čakanje sinkronizacije gruče...", + "Warmup Names Cache": "Zagrij predpomnilnik naziva", + "Warmup failed": "Zagrijavanje ni uspjelo", + "Warning": "Opozorilo", + "Warning:": "Opozorilo:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Opozorilo: To lastnost ima nedosledne vrste: {types}. Razmislite morajo li vsi objekti imeti istu vrstu.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Pregledali bomo vse objekte ki pripadaju ovoj shemi kako bismo otkrili nta lastnosti in analizirali postoječa lastnosti za potencijalna poboljšanja. Proces vključuje ispitivanje strukture podatkov vsakog objekta, prepoznavanje lastnosti ki niso določena u trenutnoj shemi in iskanje prilika za poboljšanje postoječih definicija lastnosti boljim ovejičenjima, formatima in pravilima preverjanja veljavnosti.", + "Webhook": "Webhook", + "Webhook Log Details": "Podrobnosti zapisnika webhooka", + "Webhook Logs": "Zapisnici webhooka", + "Webhook created successfully": "Webhook je uspešno ustvarjen", + "Webhook deleted": "Webhook izbrisan", + "Webhook retry delivered successfully": "Ponovni pokušaj webhooka uspešno je ipočasenučen", + "Webhook retry delivery failed": "Ispočasenočilo ponovnog pokušaja webhooka ni uspjela", + "Webhook updated": "Webhook posodobljen", + "Webhook updated successfully": "Webhook je uspešno posodobljen", + "Webhooks": "Webhookovi", + "Weekly": "Tjedno", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kada se agent izvaja brez uporabniške sesije (npr. načrtovane naloge), koristit če se kontekst tega uporabnika", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kada se agent izvaja brez uporabniške sesije (npr. načrtovane naloge), koristit če se kontekst tega uporabnika.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Ko je onemogočeno, ta če faseta prikazovati le vrednosti iz te sheme in uključivat če filter sheme ko je izbrana.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Ko je omogočeno, shranjujenje tega objekta takodjer če ažurirati ciljne objekte kako bi uključili referencu nazaj na ta objekt.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Ko je omogočeno, to če se lastnost odstraniti iz izvornega objekta po posodobljenja ciljnih objekata.", + "Workflow Management": "Upravljanje tijekom rada", + "Workflows": "Poteki dela", + "Wrap webhook payload in CloudEvents format for better interoperability": "Ovij payload webhooka u CloudEvents format za boljšo interoperabilnost", + "Wrap webhook payload in cloudevents format for better interoperability": "Ovij payload webhooka u cloudevents format za boljšo interoperabilnost", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Da", + "You": "Vi", + "You are not yet a member of any organisations.": "Još niste član nidne organizacije.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Lahko ustvariti nte ConfigSette na temelju _default predloge ali izravno preniti prilagojene na svoj SOLR poslužitelj.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Lahko ustvariti nte configsette na temelju _default predloge ali izravno preniti prilagojene na svoj SOLR poslužitelj.", + "You do not have access to this conversation": "Nemate dostop tem klepetu", + "You do not have permission to delete this conversation": "Nemate ovlasti za brisanje tega klepeta", + "You do not have permission to modify this conversation": "Nemate ovlasti za izmjenu tega klepeta", + "You do not have permission to restore this conversation": "Nemate ovlasti za obnovo tega klepeta", + "You do not have permission to view the permission matrix. Admin access is required.": "Nemate ovlasti za pregled matrice ovlasti. Potreben je administratorski dostop.", + "You have selected {count} properties to add to the schema.": "Odabrali ste {count} lastnosti za dodavanje v shemo.", + "You must be logged in to favorite views": "Morate biti prijavljeni da bi prikaze označili kot omiljene", + "You need an AI agent to start a conversation.": "Potreben vam je AI agent za početak klepeta.", + "Your Fireworks AI API key. Get one at": "Vaš Fireworks AI API ključ. Nabavite ga na", + "Your OpenAI API key. Get one at": "Vaš OpenAI API ključ. Nabavite ga na", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Vaše povratne informacije so zabilježene. Po želji tukaj lahko navesti dodatne podrobnosti...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Vaše povratne informacije so zabilježene. po želji tukaj lahko navesti dodatne podrobnosti...", + "Zookeeper Hosts": "Zookeeper strežniki", + "Zookeeper Port": "Zookeeper priključak", + "Zookeeper Settings (SolrCloud)": "Zookeeper nastavitve (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper niz za povezovanje za SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Število Zookeeper priključka (neobvezno, privzeto 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORENJE O TRAJNOM BRISANJU \\u26A0\\uFE0F\n\nUpravo čete TRAJNO izbrisati VSE objekte za shemu \"{schema}\":\n\n\\u2022 Aktivni objekti: {active}\n\\u2022 Meko izbrisani objekti: {deleted}\n\\u2022 Skupaj: {total}\n\nTi če objekti biti potpuno odstranjeni iz baze podatkov in NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORENJE O TRAJNOM BRISANJU \\u26A0\\uFE0F\n\nUpravo čete TRAJNO izbrisati {count} meko izbrisanih objekata za shemu \"{schema}\".\n\nTi če objekti biti potpuno odstranjeni iz baze podatkov in NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "and accessible to all users in selected groups (or all users if no groups selected).": "i na voljo vsim uporabnikima u izbranim skupinama (ili vsim uporabnikima ako ni izbrana nobena skupina).", + "and add the files there.": "i ondje dodajte datoteke.", + "approved": "odobreno", + "are only accessible to invited users.": "dosegljivi so le pozvanim uporabnikima.", + "by": "od strane", + "chunk": "odlomek", + "chunks": "odlomki", + "chunks processed": "odlomeka obradjeno", + "completed": "dokončano", + "configuration(s)": "konfiguracija", + "conversationId is required": "conversationId je obvezen", + "create": "ustvari", + "delete": "izbriši", + "deleted objects section": "odjeljak izbrisanih objekata", + "e.g., lib/Settings/config.json": "npr. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "npr. lib/Settings/register.json", + "e.g., lib/settings/config.json": "npr. lib/settings/config.json", + "e.g., lib/settings/register.json": "npr. lib/settings/register.json", + "execute": "izvrši", + "failed": "neuspešno", + "file": "datoteka", + "files": "datoteke", + "file{plural}": "datotek{plural}", + "folder": "mapa", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-primerka.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "u {register}", + "in_progress": "u tijeku", + "inherited": "naslijedjeno", + "list": "seznam", + "log{plural}": "zapisni{plural}", + "manage": "upravljaj", + "members": "članovi", + "menu or contact someone with permission to create agents.": "izborniku ali se obratite nekome tko ima ovlasti za ustvarjanje agenata.", + "message content is required": "vsebina počasenočila je obvezen", + "messages": "počasenočila", + "missing": "nedostaje", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL in API ključ so obvezni", + "n8n configuration saved successfully": "n8n konfiguracija uspešno je spremljena", + "n8n connection not configured": "n8n veza ni konfigurirana", + "n8n connection successful": "n8n veza uspešna", + "n8n connection test failed: {message}": "n8n test veze ni uspio: {message}", + "n8n connection test successful": "n8n test veze uspešen", + "n8n initialization failed: {message}": "n8n inicijalizacija ni uspjela: {message}", + "n8n integration disabled": "n8n integracija onemogočena", + "n8n integration enabled": "n8n integracija omogočena", + "n8n project initialized successfully": "n8n projekt uspešno je inicijaliziran", + "n8n settings saved successfully": "n8n nastavitve uspešno so spremljene", + "notify_push configuration guide": "vodič za konfiguraciju notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push je instaliran, ali OpenRegister še ni potrdio uspešno guranje. Zaženite shranjujenje objekta za aktivaciju ali preverite svojo notify_push konfiguraciju.", + "notify_push is installed but not yet active": "notify_push je instaliran, ali še ni aktiven", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekti", + "objects processed": "objekata obradjeno", + "object{plural}": "objek{plural}", + "or pick a range": "ili izberite raspon", + "org-...": "org-...", + "pending": "v čakanju", + "provides a good balance between speed and accuracy.": "zagotavlja dobru ravnotežu izmedju hitrine in točnosti.", + "public": "javno", + "read": "čitaj", + "register(s)": "register/registri", + "register(s) selected": "register/registri izbran", + "register{plural}": "register{plural}", + "rejected": "zavrnjeno", + "required": "obvezno", + "results": "rezultati", + "schema(s)": "shema/sheme", + "schema(s) selected": "shema/sheme izbrana", + "schema{plural}": "shem{plural}", + "selected objects": "izbrani objekti", + "set": "postavi", + "sk-...": "sk-...", + "this application": "ta aplikacija", + "to": "do", + "type must be \"positive\" or \"negative\"": "vrsta mora biti \"positive\" ali \"negative\"", + "update": "posodobi", + "vectorized": "vektorizirano", + "view": "prikaz", + "views selected": "prikaza izbrano", + "waiting": "čeka se", + "{active} active, {deleted} deleted": "{active} aktivnih, {deleted} izbrisanih", + "{count} audit trails cleared successfully": "{count} revizijskih tragta uspešno izbrisano", + "{count} compliance issue(s) detected": "Odkrito {count} problema skladnosti", + "{count} deletions": "{count} brisanja", + "{count} documents found": "Najdeno {count} dokumenata", + "{count} email": "{count} e-pošta", + "{count} emails": "{count} e-pošta", + "{count} entries": "{count} vnosa", + "{count} issues": "{count} problema", + "{count} members": "{count} članova", + "{count} searches": "{count} iskanja", + "{count} selected": "{count} izbrano", + "{count} widget(s)": "{count} widgeta", + "{matched} object(s) matched. {erased} erased.": "{matched} objekata podudara se. {erased} izbrisano.", + "{minutes} minutes ago": "prije {minutes} minuta", + "{percentage}% of objects": "{percentage}% objekata", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} v {register} / {schema}", + "{type} cache cleared successfully": "{type} predpomnilnik uspešno izbrisana", + "ℹ️ Current Configuration": "ℹ️ Trenutna konfiguracija", + "ℹ️ current configuration": "ℹ️ trenutna konfiguracija", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ OPASNOST: Vsih {total} objekata bo TRAJNO IZBRISANO iz baze podatkov. Ta je dejanje NEPOVRATNA in ne lahko se poništiti!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORENJE O TRAJNOM BRISANJU ⚠️\n\nUpravo čete TRAJNO izbrisati VSE objekte za shemu \"{schema}\":\n\n• Aktivni objekti: {active}\n• Meko izbrisani objekti: {deleted}\n• Skupaj: {total}\n\nTi če objekti biti potpuno odstranjeni iz baze podatkov in NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORENJE O TRAJNOM BRISANJU ⚠️\n\nUpravo čete TRAJNO izbrisati {count} meko izbrisanih objekata za shemu \"{schema}\".\n\nTi če objekti biti potpuno odstranjeni iz baze podatkov in NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "⚡ Batch Processing": "⚡ Skupna obdelava", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR lahko obdelaviti milijune dokumenata s vremenom odziva iskanja kračim od sekunde...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Vseobuhvatan kontekst ✓ Manja vjerojatnost izostavljanja podrobnosti ✗ Počaseniji odgovori ✗ Lahko uključivati manj relevantne informacije", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Hitrejši odgovori ✓ Usredotočeniji odgovori ✗ Lahko izostaviti relevantne informacije", + "✨ AI Features": "✨ AI funkcije", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 U distribuiranom načinu rada, SOLR se uporablja ZooKeeper za koordinaciju gruče...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasetno iskanje omogučuje uporabnikima dublje istraživanje rezultata po kategorijama...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Jeste li znali? SOLR pokreče iskanje za Netflix, Apple in mnoge druge velike stranice!", + "💰 View Selection (Cost Optimization)": "💰 Izbor prikaza (optimizacija stroškov)", + "📄 Object Serialization": "📄 Serijalizacija objekta", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR zagotavlja bogato analitiku in statistiku o izvedbi iskanja...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSetti sadrže shemu in konfiguracijske datoteke za vaš indeks iskanja...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikacija osigurava na voljost vašeg indeksa iskanja celo in ako vozlišča zakažu...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR je močna platforma za iskanje na ravni poduzeča izgradjena na Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Nastavitve vektorizacije", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Samodejno skaliranje lahko dinamički dodavati ali odstranjevati čvorte na temelju opterečenja...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Varnostne funkcije vključujejo autentifikaciju, autorizaciju in SSL enkripciju...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-pošta", + "{count} e-pošti" + ], + "Purge {count} object from database": [ + "Trajno odstrani {count} objekt iz baze podatkov", + "Trajno odstrani {count} objektov iz baze podatkov" + ], + "Restore {count} object": [ + "Obnovi {count} objekt", + "Obnovi {count} objektov" + ], + "Successfully restored {count} object": [ + "Uspešno obnovljen {count} objekt", + "Uspešno obnovljeno {count} objektov" + ], + "Delete {count} object": [ + "Izbriši {count} objekt", + "Izbriši {count} objektov" + ], + "Object successfully deleted": [ + "Objekt je uspešno izbrisan", + "Objekti so uspešno izbrisani" + ] + } +} diff --git a/l10n/sq.js b/l10n/sq.js new file mode 100644 index 0000000000..1c79eda2a8 --- /dev/null +++ b/l10n/sq.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(pa temë)", + "(no title)" : "(pa titull)", + "(unnamed)" : "(pa emër)", + "(untitled card)" : "(kartë pa titull)", + "Activities" : "Aktivitete", + "Add Endpoint" : "Shto Pikëfund", + "Add Source" : "Shto Burim", + "Add card" : "Shto kartë", + "Add contact" : "Shto kontakt", + "All clear" : "Gjithçka e qartë", + "Articles" : "Artikuj", + "Code" : "Kod", + "Compliance" : "Pajtueshmëri", + "Contacts" : "Kontakte", + "Create Agent" : "Krijo Agjent", + "Create Application" : "Krijo Aplikacion", + "Create event" : "Krijo ngjarje", + "Erasure complete" : "Fshirja u krye", + "Erasure preview" : "Parapamja e fshirjes", + "Expand details" : "Zgjeroni detajet", + "Save changes" : "Ruaj ndryshimet", + "📄 Object Serialization" : "📄 Serializimi i Objektit", + "🔢 Vectorization Settings" : "🔢 Cilësimet e Vectorizimit", + "💰 View Selection (Cost Optimization)" : "💰 Zgjedhja e Pamjes (Optimizimi i Kostove)", + "AI Assistant" : "Asistent AI", + "AI Chat" : "Chat AI", + "API Key" : "Çelës API", + "API tokens saved successfully" : "Token API u ruajt me sukses", + "About ConfigSets" : "Rreth ConfigSets", + "Action" : "Veprim", + "Action Distribution" : "Shpërndarja e Veprimeve", + "Action:" : "Veprim:", + "Actions" : "Veprime", + "Active" : "Aktiv", + "Active Collections" : "Koleksionet Aktive", + "Active Filters" : "Filtrat Aktiv", + "Active Organisations" : "Organizatat Aktive", + "Active filters:" : "Filtrat aktiv:", + "Active organisation set successfully" : "Organizata aktive u vendos me sukses", + "Activity Period" : "Periudha e Aktivitetit", + "Add Groups" : "Shto Grupe", + "Add Schema" : "Shto Skemë", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Shtoni tituj skemash, përshkrime dhe informacion regjistri për të ofruar kontekst më të pasur për kërkim", + "Add to favorites" : "Shto te të preferuarat", + "Added" : "Shtuar", + "Added to favorites" : "Shtuar te të preferuarat", + "Additional Information" : "Informacion Shtesë", + "Additional feedback saved. Thank you!" : "Reagimet shtesë u ruajtën. Faleminderit!", + "Admin" : "Admin", + "Advanced" : "Avancuar", + "Advanced Filters" : "Filtra të Avancuar", + "Advanced Options" : "Opsione të Avancuara", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Filtrat e avancuar nuk janë të disponueshëm kur përdorni burimin e bazës së të dhënave. Kaloni te Auto ose SOLR Index për opsionet e filtrimit.", + "After" : "Pas", + "Agent deleted successfully" : "Agjenti u fshi me sukses", + "Agents" : "Agjentë", + "All" : "Të gjitha", + "All Categories" : "Të gjitha Kategoritë", + "All Collections" : "Të gjitha Koleksionet", + "All Confidence Levels" : "Të gjitha Nivelet e Besimit", + "All ConfigSets" : "Të gjitha ConfigSets", + "All Files" : "Të gjitha Skedarët", + "All Levels" : "Të gjitha Nivelet", + "All Types" : "Të gjitha Llojet", + "All Webhooks" : "Të gjitha Webhooks", + "All actions" : "Të gjitha veprimet", + "All audit trails cleared successfully" : "Të gjitha gjurmët e auditimit u pastruan me sukses", + "All registers" : "Të gjitha regjistrat", + "All schemas" : "Të gjitha skemat", + "All searches" : "Të gjitha kërkimet", + "All users" : "Të gjithë përdoruesit", + "An error occurred while clearing audit trails" : "Ndodhi një gabim gjatë pastrimit të gjurmëve të auditimit", + "An error occurred while deleting the audit trail" : "Ndodhi një gabim gjatë fshirjes së gjurmës së auditimit", + "An error occurred while deleting the view" : "Ndodhi një gabim gjatë fshirjes së pamjes", + "An error occurred while permanently deleting the objects" : "Ndodhi një gabim gjatë fshirjes së përhershme të objekteve", + "Analysis completed:" : "Analiza përfundoi:", + "Analysis steps:" : "Hapat e analizës:", + "Analytics" : "Analitikë", + "Analyze Objects" : "Analizoni Objektet", + "Analyze Properties" : "Analizoni Pronat", + "Analyze existing properties for improvement opportunities" : "Analizoni pronat ekzistuese për mundësi përmirësimi", + "Analyzing..." : "Duke analizuar...", + "Any favorites and sharing settings for this view" : "Çdo preferencë dhe cilësimet e ndarjes për këtë pamje", + "Any user" : "Çdo përdorues", + "App store cache invalidated successfully" : "Cache e dyqanit të aplikacioneve u invalidua me sukses", + "Application deleted successfully" : "Aplikacioni u fshi me sukses", + "Applications" : "Aplikacione", + "Apply Changes" : "Apliko Ndryshimet", + "Archive" : "Arkiv", + "Archive conversation" : "Arkivoni bisedën", + "Archived conversations are hidden from your active list" : "Bisedat e arkivuara janë fshehura nga lista juaj aktive", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Jeni i sigurt se doni të pastroni gjurmët e vjetra të kërkimit? Kjo do të fshijë hyrjet më të vjetra se 30 ditë.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Jeni i sigurt se doni të fshini gjurmët e zgjedhura të auditimit? Ky veprim nuk mund të kthehet.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Jeni i sigurt se doni të fshini gjurmët e zgjedhura të kërkimit? Ky veprim nuk mund të kthehet.", + "Are you sure you want to delete this ConfigSet?" : "Jeni i sigurt se doni të fshini këtë ConfigSet?", + "Are you sure you want to permanently delete" : "Jeni i sigurt se doni të fshini përgjithmonë", + "Ask a question..." : "Bëni një pyetje...", + "Ask questions about your data using natural language" : "Bëni pyetje rreth të dhënave tuaja duke përdorur gjuhën natyrore", + "Attempt" : "Tentativë", + "Audit Trail Changes" : "Ndryshimet e Gjurmës së Auditimit", + "Audit Trail Details" : "Detajet e Gjurmës së Auditimit", + "Audit Trail Management" : "Menaxhimi i Gjurmës së Auditimit", + "Audit Trail Statistics" : "Statistikat e Gjurmës së Auditimit", + "Audit Trails" : "Gjurmët e Auditimit", + "Audit trail data copied to clipboard" : "Të dhënat e gjurmës së auditimit u kopjuan në clipboard", + "Audit trail deleted successfully" : "Gjurma e auditimit u fshi me sukses", + "Audit trail successfully deleted" : "Gjurma e auditimit u fshi me sukses", + "Authentication" : "Autentifikim", + "Auto-commit disabled" : "Auto-commit çaktivizuar", + "Auto-commit enabled" : "Auto-commit aktivizuar", + "Auto-create Default Organisation" : "Krijo Automatikisht Organizatën Default", + "Auto-retry failed vectorizations" : "Riprovo automatikisht vectorizimet e dështuara", + "Automatically commit changes to SOLR index" : "Angazho automatikisht ndryshimet në indeksin SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Krijoni automatikisht një organizatë default nëse nuk ekziston asnjë kur aplikacioni inicializohet", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Gjeneroni automatikisht embeddings vektoriale nga copa teksti kur skedarët ngarkohen dhe përpunohen", + "Automatically generate vector embeddings when objects are created or updated" : "Gjeneroni automatikisht embeddings vektoriale kur objektet krijohen ose përditësohen", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Riprovo automatikisht tentativat e dështuara të vectorizimit (maks 3 riprovime)", + "Available Filters" : "Filtrat e Disponueshëm", + "Available Workflows" : "Flukset e Punës të Disponueshme", + "Avg Execution Time" : "Koha Mesatare e Ekzekutimit", + "Avg Members/Org" : "Anëtarë Mesatarë/Org", + "Avg Object Views/Session" : "Pamje Mesatare Objektesh/Sesion", + "Avg Response Time" : "Koha Mesatare e Përgjigjes", + "Avg Results/Search" : "Rezultate Mesatare/Kërkim", + "Avg Searches/Session" : "Kërkime Mesatare/Sesion", + "Back" : "Mbrapa", + "Back to Entities" : "Kthehu te Entitetet", + "Back to Registers" : "Kthehu te Regjistrat", + "Back to Webhooks" : "Kthehu te Webhooks", + "Back to applications" : "Kthehu te aplikacionet", + "Back to entities" : "Kthehu te entitetet", + "Backend updated successfully. Please reload the application." : "Backend u përditësua me sukses. Ju lutemi ringarkoni aplikacionin.", + "Base URL (Optional)" : "URL Bazë (Opsional)", + "Basic Information" : "Informacion Bazë", + "Batch Size" : "Madhësia e Grupit", + "Batch extraction completed" : "Nxjerrja në grupe përfundoi", + "Before" : "Para", + "Before object vectorization can work:" : "Para se vectorizimi i objektit të funksionojë:", + "Behavior Issue" : "Problem Sjelljeje", + "Bookmarks" : "Faqerojtës", + "Blob storage has been retired. All objects now use magic tables." : "Ruajtja blob është tërhequr. Të gjitha objektet tani përdorin tabela magjike.", + "Branch" : "Degë", + "Bulk delete operation completed successfully" : "Operacioni i fshirjes në grup u krye me sukses", + "Bulk save operation completed successfully" : "Operacioni i ruajtjes në grup u krye me sukses", + "Business Data" : "Të dhëna Biznesi", + "Cache cleared successfully" : "Cache u pastrua me sukses", + "Calculate Sizes" : "Llogaritni Madhësitë", + "Cancel" : "Anulo", + "Cannot delete: objects are still attached" : "Nuk mund të fshihet: objektet janë ende të lidhura", + "Category" : "Kategori", + "Change Type" : "Lloji i Ndryshimit", + "Changes" : "Ndryshime", + "Changes copied to clipboard" : "Ndryshimet u kopjuan në clipboard", + "Chat Model" : "Modeli i Chat", + "Chat Provider" : "Ofruesi i Chat", + "Chat Provider (RAG)" : "Ofruesi i Chat (RAG)", + "Chat Settings" : "Cilësimet e Chat", + "Chat provider connection successful!" : "Lidhja me ofruesin e chat u krye me sukses!", + "Chat settings" : "Cilësimet e chat", + "Choose a register" : "Zgjidhni një regjistër", + "Choose a schema" : "Zgjidhni një skemë", + "Choose how vector similarity calculations are performed for semantic search" : "Zgjidhni si kryhen llogaritjet e ngjashmërisë vektoriale për kërkim semantik", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Zgjidhni cilat lloje skedarësh të përfshihen në procesin e vectorizimit. Do të përpunohen vetëm skedarët me tekst të nxjerrë dhe copa.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Zgjidhni cilat pamje të përfshihen në procesin e vectorizimit. Lëreni bosh për të përpunuar të gjitha pamjet bazuar në konfigurimin tuaj.", + "Chunk" : "Copë", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Fshirja e copave nuk është zbatuar ende. Përdorni pikëfundt të bazuara në copa.", + "Chunks" : "Copa", + "Chunks to Vectorize:" : "Copa për Vectorizim:", + "Cleanup Old Trails" : "Pastro Gjurmët e Vjetra", + "Cleanup completed" : "Pastrimi përfundoi", + "Clear All Embeddings" : "Pastro Të gjitha Embeddings", + "Clear Entries" : "Pastro Hyrjet", + "Clear Filtered Audit Trails" : "Pastro Gjurmët e Filtruara të Auditimit", + "Clear Filters" : "Pastro Filtrat", + "Clear Index" : "Pastro Indeksin", + "Clear Selection" : "Pastro Zgjedhjen", + "Clear all" : "Pastro të gjitha", + "Clear all filters" : "Pastro të gjithë filtrat", + "Clear date range" : "Pastro intervalin e datave", + "Clearing..." : "Duke pastruar...", + "Close" : "Mbyll", + "Collection Management" : "Menaxhimi i Koleksionit", + "Collection Name" : "Emri i Koleksionit", + "Collection assignments updated successfully" : "Caktimet e koleksionit u përditësuan me sukses", + "Collection cleared successfully" : "Koleksioni u pastrua me sukses", + "Collection copied successfully" : "Koleksioni u kopjua me sukses", + "Collection created successfully" : "Koleksioni u krijua me sukses", + "Collection deleted successfully" : "Koleksioni u fshi me sukses", + "Collection used to store and index file metadata and content" : "Koleksioni i përdorur për të ruajtur dhe indeksuar meta të dhënat dhe përmbajtjen e skedarëve", + "Collection used to store and index object data" : "Koleksioni i përdorur për të ruajtur dhe indeksuar të dhënat e objekteve", + "Collections:" : "Koleksionet:", + "Commit Message" : "Mesazhi i Commit", + "Commit Within (ms)" : "Commit Brenda (ms)", + "Commit message" : "Mesazhi i commit", + "Compare current schema with real object data" : "Krahasoni skemën aktuale me të dhënat reale të objektit", + "Completed" : "Përfunduar", + "Complex" : "Kompleks", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Pyetje komplekse: Kërkime të avancuara me filtra të shumtë, operatorë dhe kombinime parametrash komplekse", + "Confidence Level" : "Niveli i Besimit", + "Confidence Score" : "Rezultati i Besimit", + "Config must be provided as an object" : "Konfigurimi duhet të jepet si objekt", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Menaxhimi i ConfigSet", + "ConfigSet Name" : "Emri i ConfigSet", + "ConfigSet created successfully" : "ConfigSet u krijua me sukses", + "ConfigSet deleted successfully" : "ConfigSet u fshi me sukses", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets përcaktojnë skemën dhe konfigurimin për koleksionet tuaja SOLR. Ato përmbajnë përkufizime fushash, analizues dhe cilësime të tjera kërkimi.", + "Configuration" : "Konfigurim", + "Configuration Data" : "Të dhënat e Konfigurimit", + "Configuration saved" : "Konfigurimi u ruajt", + "Configurations" : "Konfigurime", + "Configure Facets" : "Konfiguroni Facets", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfiguroni ofruesit e Modelit të Gjuhës së Madhe (LLM) për veçoritë e drejtuar nga AI duke përfshirë kërkim semantik, embeddings dhe chat.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfiguroni cilësimet bazë të lidhjes për serverin tuaj SOLR duke përfshirë autentifikimin dhe opsionet e rrjetit. Përdorni dialogët e veçantë të Menaxhimit të ConfigSet dhe Koleksionit për të menaxhuar bërthamat dhe koleksionet.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfiguroni si konvertohen objektet e bazës së të dhënave në embeddings vektoriale për kërkim semantik. Objektet vectorizohen drejtpërdrejt pa nevojën e nxjerrjes së tekstit.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfiguroni si konvertohen objektet në tekst para vectorizimit. Këto cilësime ndikojnë në cilësinë e kërkimit dhe kontekstin.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfiguroni parametrat për vectorizimin e skedarëve. Ky proces do të gjenerojë embeddings vektoriale për të gjitha copat e nxjerra të skedarëve.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfiguroni parametrat për vectorizimin e objekteve. Ky proces do të gjenerojë embeddings vektoriale për të gjitha objektet që përputhen me filtrat e pamjes tuaj.", + "Configure which types of data to search and how many sources to retrieve" : "Konfiguroni cilat lloje të dhënash të kërkohen dhe sa burime të merren", + "Connection Failed" : "Lidhja Dështoi", + "Connection Settings" : "Cilësimet e Lidhjes", + "Connection Successful!" : "Lidhja U Krye Me Sukses!", + "Connection failed" : "Lidhja dështoi", + "Connection protocol" : "Protokolli i lidhjes", + "Connection successful" : "Lidhja u krye me sukses", + "Connection timeout in seconds" : "Koha e skadimit të lidhjes në sekonda", + "Constant delay between retries (5 minutes)" : "Vonesë konstante midis riprovimeve (5 minuta)", + "Constraint Issue" : "Problem Kufizimi", + "Content Filters" : "Filtrat e Përmbajtjes", + "Continue to Properties" : "Vazhdoni te Pronat", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontrolloni cilat pamje objektesh duhet vectorizuar për të ulur kostot API. Vectorizoni vetëm pamjet që përfitojnë nga kërkimi semantik.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontrolloni cilat pamje dhe mjete mund të përdorë AI në këtë bisedë. Si parazgjedhje, të gjitha aftësitë e agjentit janë aktivizuara.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Kontrollon rastësinë (0 = deterministe, 2 = shumë krijuese)", + "Conversation ID is required" : "ID e bisedës është e nevojshme", + "Conversation archived" : "Biseda u arkivua", + "Conversation archived successfully" : "Biseda u arkivua me sukses", + "Conversation cleared successfully" : "Biseda u pastrua me sukses", + "Conversation deleted" : "Biseda u fshi", + "Conversation permanently deleted" : "Biseda u fshi përgjithmonë", + "Conversation renamed" : "Biseda u riemërtua", + "Conversation restored" : "Biseda u rivendos", + "Conversation title" : "Titulli i bisedës", + "Copied!" : "Kopjuar!", + "Copy" : "Kopjo", + "Copy Changes" : "Kopjo Ndryshimet", + "Copy Collection" : "Kopjo Koleksionin", + "Copy Data" : "Kopjo Të dhënat", + "Copy Full Data" : "Kopjo Të dhënat e Plota", + "Copying..." : "Duke kopjuar...", + "Count" : "Numërim", + "Counting objects..." : "Duke numëruar objektet...", + "Costs" : "Kosto", + "Create" : "Krijo", + "Create Collection" : "Krijo Koleksion", + "Create ConfigSet" : "Krijo ConfigSet", + "Create New Collection" : "Krijo Koleksion të Ri", + "Create New ConfigSet" : "Krijo ConfigSet të Ri", + "Create Operations" : "Operacionet e Krijimit", + "Create Webhook" : "Krijo Webhook", + "Create a copy of collection:" : "Krijo një kopje të koleksionit:", + "Create a new ConfigSet based on the _default template" : "Krijo një ConfigSet të ri bazuar në shablonin _default", + "Create a new SOLR collection from an existing ConfigSet" : "Krijo një koleksion SOLR të ri nga një ConfigSet ekzistues", + "Create your first AI agent to get started." : "Krijoni agjentin tuaj të parë AI për të filluar.", + "Created" : "Krijuar", + "Created:" : "Krijuar:", + "Creating..." : "Duke krijuar...", + "Current Filters" : "Filtrat Aktualë", + "Current Type:" : "Lloji Aktual:", + "Custom API endpoint if using a different region" : "Pikëfund API i personalizuar nëse përdorni një rajon tjetër", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Titujt HTTP të personalizuar (një për rresht, format: Emri-Titullit: vlerë)", + "Daily" : "Çdo ditë", + "Dashboard" : "Panel", + "Data Source" : "Burimi i Të dhënave", + "Data sources" : "Burimet e të dhënave", + "Data type does not match observed values" : "Lloji i të dhënave nuk përputhet me vlerat e vëzhguara", + "Database URL" : "URL e Bazës së Të dhënave", + "Database information refreshed" : "Informacioni i bazës së të dhënave u rifreskua", + "Date Range" : "Intervali i Datave", + "Default" : "Parazgjedhje", + "Default Organisation" : "Organizata Default", + "Default: 5" : "Parazgjedhje: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Vonesat dyfishohen me çdo tentativë (2, 4, 8 minuta...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Vonesat rriten linearkisht (5, 10, 15 minuta...)", + "Delete" : "Fshi", + "Delete Application" : "Fshi Aplikacionin", + "Delete Audit Trail" : "Fshi Gjurmën e Auditimit", + "Delete Collection" : "Fshi Koleksionin", + "Delete ConfigSet" : "Fshi ConfigSet", + "Delete Objects" : "Fshi Objektet", + "Delete Operations" : "Operacionet e Fshirjes", + "Delete View" : "Fshi Pamjen", + "Delete all objects in this schema" : "Fshi të gjitha objektet në këtë skemë", + "Delete permanently" : "Fshi përgjithmonë", + "Delete this conversation?" : "Fshini këtë bisedë?", + "Delete view" : "Fshi pamjen", + "Deleted" : "Fshirë", + "Deleted By" : "Fshirë Nga", + "Deleted Date" : "Data e Fshirjes", + "Deleted Items Management" : "Menaxhimi i Artikujve të Fshirë", + "Deleted This Week" : "Fshirë Këtë Javë", + "Deleted Today" : "Fshirë Sot", + "Deleted:" : "Fshirë:", + "Deleting..." : "Duke fshirë...", + "Deletion Date Range" : "Intervali i Datave të Fshirjes", + "Deletion Statistics" : "Statistikat e Fshirjes", + "Deprecated" : "I vjetruar", + "Description" : "Përshkrim", + "Description:" : "Përshkrim:", + "Detect data types and patterns" : "Zbuloni llojet e të dhënave dhe modelet", + "Detected At" : "Zbuluar Në", + "Detected Format:" : "Formati i Zbuluar:", + "Detected Issues:" : "Problemet e Zbuluara:", + "Disable" : "Çaktivizo", + "Disabled" : "Çaktivizuar", + "Discover Files" : "Zbuloni Skedarët", + "Discovered Properties" : "Pronat e Zbuluara", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Doni të fshini përgjithmonë të gjitha hyrjet e filtruara të gjurmës së auditimit? Ky veprim nuk mund të kthehet.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Doni të fshini përgjithmonë këtë hyrje të gjurmës së auditimit? Ky veprim nuk mund të kthehet.", + "Documents" : "Dokumente", + "Download API Spec" : "Shkarko Spec API", + "Edit" : "Ndrysho", + "Edit Register" : "Ndrysho Regjistrin", + "Edit Schema" : "Ndrysho Skemën", + "Edit View" : "Ndrysho Pamjen", + "Edit Webhook" : "Ndrysho Webhook", + "Edit view" : "Ndrysho pamjen", + "Edit view details" : "Ndrysho detajet e pamjes", + "Effectiveness" : "Efektivitet", + "Email" : "Email", + "Emails" : "Email", + "Embedding Model" : "Modeli i Embedding", + "Embedding Provider" : "Ofruesi i Embedding", + "Embedding generated successfully" : "Embedding u gjenerua me sukses", + "Embedding provider connection successful!" : "Lidhja me ofruesin e embedding u krye me sukses!", + "Enable" : "Aktivizo", + "Enable auto-creation" : "Aktivizo krijimin automatik", + "Enable automatic file vectorization" : "Aktivizo vectorizimin automatik të skedarëve", + "Enable automatic object vectorization" : "Aktivizo vectorizimin automatik të objekteve", + "Enable automatic synchronization" : "Aktivizo sinkronizimin automatik", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Aktivizo regjistrimin e detajuar për operacionet SOLR (rekomandohet për debugging)", + "Enable faceting" : "Aktivizo faceting", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Aktivizoni ose çaktivizoni veçoritë LLM. Konfiguroni ofruesit dhe modelet duke përdorur butonin e Konfigurimit LLM më sipër.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Aktivizoni ose çaktivizoni integrimin e kërkimit SOLR. Konfiguroni cilësimet e lidhjes duke përdorur butonin e Cilësimeve të Lidhjes më sipër.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Aktivizoni ose çaktivizoni integrimin e fluksit të punës n8n. Konfiguroni cilësimet e lidhjes më poshtë.", + "Enable or disable this webhook" : "Aktivizoni ose çaktivizoni këtë webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Aktivizo vectorizimin për të gjitha pamjet ekzistuese dhe të ardhshme (mund të rrisë kostot)", + "Enabled" : "Aktivizuar", + "Endpoints" : "Pikëfundte", + "Enter ConfigSet name" : "Futni emrin e ConfigSet", + "Enter collection name" : "Futni emrin e koleksionit", + "Enter description (optional)..." : "Futni përshkrimin (opsional)...", + "Enter new collection name" : "Futni emrin e ri të koleksionit", + "Enter object ID" : "Futni ID e objektit", + "Enter search term" : "Futni termin e kërkimit", + "Enter view name..." : "Futni emrin e pamjes...", + "Enter webhook name" : "Futni emrin e webhook", + "Entities" : "Entitete", + "Entity Information" : "Informacion Entiteti", + "Entity deleted successfully" : "Entiteti u fshi me sukses", + "Entity not found" : "Entiteti nuk u gjet", + "Entries to be deleted:" : "Hyrjet që do të fshihen:", + "Enum Issue" : "Problem Enum", + "Enum constraint is missing" : "Kufizimi enum mungon", + "Error" : "Gabim", + "Error Details" : "Detajet e Gabimit", + "Error Information" : "Informacioni i Gabimit", + "Error Message" : "Mesazhi i Gabimit", + "Error loading application" : "Gabim gjatë ngarkimit të aplikacionit", + "Error loading audit trails" : "Gabim gjatë ngarkimit të gjurmëve të auditimit", + "Error loading entity" : "Gabim gjatë ngarkimit të entitetit", + "Error loading search trails" : "Gabim gjatë ngarkimit të gjurmëve të kërkimit", + "Estimated Batches:" : "Grupet e Vlerësuara:", + "Estimated Cost:" : "Kostoja e Vlerësuar:", + "Estimated Duration:" : "Kohëzgjatja e Vlerësuar:", + "Event" : "Ngjarje", + "Event Property for Payload" : "Prona e Ngjarjes për Payload", + "Events" : "Ngjarje", + "Example Value" : "Vlera Shembull", + "Exclusive Maximum" : "Maksimumi Eksklusiv", + "Exclusive Minimum" : "Minimumi Eksklusiv", + "Execution Mode" : "Modaliteti i Ekzekutimit", + "Execution Time" : "Koha e Ekzekutimit", + "Execution Time Range" : "Intervali i Kohës së Ekzekutimit", + "Existing Improvements" : "Përmirësimet Ekzistuese", + "Exponential" : "Eksponencial", + "Export" : "Eksporto", + "Export completed successfully" : "Eksporti u krye me sukses", + "Export, view, or delete audit trails" : "Eksportoni, shikoni ose fshini gjurmët e auditimit", + "Extend Schema" : "Zgjeroni Skemën", + "Extended" : "Zgjeruar", + "Extensions" : "Zgjerime", + "Extract Pending Files" : "Nxirr Skedarët në Pritje", + "Extract properties from each object" : "Nxirrni pronat nga çdo objekt", + "Extracted At" : "Nxjerrë Në", + "Extraction Status" : "Statusi i Nxjerrjes", + "Facet configuration updated successfully" : "Konfigurimi i facet u përditësua me sukses", + "Facets discovered and configured successfully" : "Facets u zbuluan dhe konfiguruan me sukses", + "Facets discovered successfully" : "Facets u zbuluan me sukses", + "Failed" : "Dështoi", + "Failed to analyze schema properties" : "Dështoi analiza e pronave të skemës", + "Failed to archive conversation" : "Dështoi arkivimi i bisedës", + "Failed to calculate sizes" : "Dështoi llogaritja e madhësive", + "Failed to clear collection" : "Dështoi pastrimi i koleksionit", + "Failed to copy changes" : "Dështoi kopjimi i ndryshimeve", + "Failed to copy data" : "Dështoi kopjimi i të dhënave", + "Failed to copy data to clipboard" : "Dështoi kopjimi i të dhënave në clipboard", + "Failed to create conversation" : "Dështoi krijimi i bisedës", + "Failed to create or find project" : "Dështoi krijimi ose gjetja e projektit", + "Failed to delete collection" : "Dështoi fshirja e koleksionit", + "Failed to delete conversation" : "Dështoi fshirja e bisedës", + "Failed to delete webhook" : "Dështoi fshirja e webhook", + "Failed to download API specification" : "Dështoi shkarkimi i specifikimit API", + "Failed to get SOLR field configuration" : "Dështoi marrja e konfigurimit të fushave SOLR", + "Failed to get entity categories" : "Dështoi marrja e kategorive të entitetit", + "Failed to get entity statistics" : "Dështoi marrja e statistikave të entitetit", + "Failed to get entity types" : "Dështoi marrja e llojeve të entitetit", + "Failed to load LLM configuration" : "Dështoi ngarkimi i konfigurimit LLM", + "Failed to load Nextcloud groups" : "Dështoi ngarkimi i grupeve Nextcloud", + "Failed to load conversation" : "Dështoi ngarkimi i bisedës", + "Failed to load entities" : "Dështoi ngarkimi i entiteteve", + "Failed to load entity" : "Dështoi ngarkimi i entitetit", + "Failed to load files" : "Dështoi ngarkimi i skedarëve", + "Failed to load organisations" : "Dështoi ngarkimi i organizatave", + "Failed to load templates" : "Dështoi ngarkimi i shablloneve", + "Failed to load webhooks" : "Dështoi ngarkimi i webhooks", + "Failed to load workflows" : "Dështoi ngarkimi i flukseve të punës", + "Failed to refresh database information" : "Dështoi rifreskimi i informacionit të bazës së të dhënave", + "Failed to reindex collection" : "Dështoi riindeksimi i koleksionit", + "Failed to rename conversation" : "Dështoi riemërtimi i bisedës", + "Failed to restore conversation" : "Dështoi rivendosja e bisedës", + "Failed to retry extraction" : "Dështoi riprova e nxjerrjes", + "Failed to retry webhook" : "Dështoi riprova e webhook", + "Failed to save GitHub token" : "Dështoi ruajtja e token GitHub", + "Failed to save GitLab URL" : "Dështoi ruajtja e URL GitLab", + "Failed to save GitLab token" : "Dështoi ruajtja e token GitLab", + "Failed to save additional feedback" : "Dështoi ruajtja e reagimeve shtesë", + "Failed to save n8n configuration" : "Dështoi ruajtja e konfigurimit n8n", + "Failed to save roles" : "Dështoi ruajtja e roleve", + "Failed to save settings" : "Dështoi ruajtja e cilësimeve", + "Failed to save webhook" : "Dështoi ruajtja e webhook", + "Failed to send feedback" : "Dështoi dërgimi i reagimeve", + "Failed to test webhook" : "Dështoi testimi i webhook", + "Failed to update favorite status" : "Dështoi përditësimi i statusit të preferatës", + "Failed to update schema properties" : "Dështoi përditësimi i pronave të skemës", + "Failed to update view" : "Dështoi përditësimi i pamjes", + "Failed to update webhook" : "Dështoi përditësimi i webhook", + "Feedback recorded" : "Reagimet u regjistruan", + "Fewer sources (1-3):" : "Burime më pak (1-3):", + "Field" : "Fushë", + "File" : "Skedar", + "File Chunk Prediction" : "Parashikimi i Copës së Skedarit", + "File Collection" : "Koleksioni i Skedarëve", + "File Management" : "Menaxhimi i Skedarëve", + "File Name" : "Emri i Skedarit", + "File Path" : "Shtegu i Skedarit", + "File Type Selection" : "Zgjedhja e Llojit të Skedarit", + "File Vectorization" : "Vectorizimi i Skedarëve", + "File Warmup" : "Ngrohja e Skedarëve", + "File actions menu" : "Menuja e veprimeve të skedarit", + "File anonymized successfully" : "Skedari u anonimizua me sukses", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Vectorizimi i copave të skedarit nuk është zbatuar ende. Copat janë gati dhe të ruajtura, por shërbimi i vectorizimit është në zhvillim.", + "File collection not configured" : "Koleksioni i skedarëve nuk është konfiguruar", + "File discovery completed" : "Zbulimi i skedarëve përfundoi", + "File extraction completed" : "Nxjerrja e skedarëve përfundoi", + "File extraction queued" : "Nxjerrja e skedarëve është në radhë", + "File indexed successfully" : "Skedari u indeksua me sukses", + "File is already anonymized" : "Skedari është tashmë i anonimizuar", + "File not found" : "Skedari nuk u gjet", + "File settings updated successfully" : "Cilësimet e skedarit u përditësuan me sukses", + "File sources" : "Burimet e skedarëve", + "File vectorization configuration saved successfully" : "Konfigurimi i vectorizimit të skedarëve u ruajt me sukses", + "File vectorization started. Check the statistics section for progress." : "Vectorizimi i skedarëve filloi. Kontrolloni seksionin e statistikave për progresin.", + "File warmup completed" : "Ngrohja e skedarëve përfundoi", + "Files" : "Skedarë", + "Files with Completed Extraction:" : "Skedarë me Nxjerrje të Përfunduar:", + "Files → fileCollection, Objects → objectCollection" : "Skedarë → fileCollection, Objekte → objectCollection", + "Filter Audit Trails" : "Filtroni Gjurmët e Auditimit", + "Filter Deleted Items" : "Filtroni Artikujt e Fshirë", + "Filter Objects" : "Filtroni Objektet", + "Filter Properties" : "Filtroni Pronat", + "Filter Search Trails" : "Filtroni Gjurmët e Kërkimit", + "Filter Statistics" : "Filtroni Statistikat", + "Filter and analyze search trail entries" : "Filtroni dhe analizoni hyrjet e gjurmëve të kërkimit", + "Filter and manage audit trail entries" : "Filtroni dhe menaxhoni hyrjet e gjurmëve të auditimit", + "Filter and manage soft deleted items" : "Filtroni dhe menaxhoni artikujt e fshirë butë", + "Filter by object ID" : "Filtro sipas ID objektit", + "Filter by search term" : "Filtro sipas termit të kërkimit", + "Filter by webhook" : "Filtro sipas webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Të dhënat e filtrit u ngarkuan automatikisht. Përdorni filtrat më poshtë për të rafinuar kërkimin tuaj.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtroni aktivizuesit e webhook sipas pronave të payload (një për rresht, format: çelës: vlerë)", + "Filtered" : "Filtruar", + "Filters" : "Filtra", + "Fireworks AI Chat Settings" : "Cilësimet e Chat Fireworks AI", + "Fireworks AI Embedding Configuration" : "Konfigurimi i Embedding Fireworks AI", + "First" : "I pari", + "Fixed" : "Korrigjuar", + "For chat and retrieval-augmented generation" : "Për chat dhe gjenerim të rritur me rikthim", + "For vector embeddings and semantic search" : "Për embeddings vektoriale dhe kërkim semantik", + "Format" : "Format", + "Format Issue" : "Problem Formati", + "Format constraint is missing" : "Kufizimi i formatit mungon", + "From Date" : "Nga Data", + "From date" : "Nga data", + "Full data copied to clipboard" : "Të dhënat e plota u kopjuan në clipboard", + "General Issue" : "Problem i Përgjithshëm", + "Generate recommendations and confidence scores" : "Gjeneroni rekomandime dhe rezultate besimi", + "Generate vectors immediately when new objects are created" : "Gjeneroni vektorë menjëherë kur krijohen objekte të reja", + "GitHub token is valid" : "Token GitHub është i vlefshëm", + "GitHub token removed successfully" : "Token GitHub u hoq me sukses", + "GitHub token saved successfully" : "Token GitHub u ruajt me sukses", + "GitLab URL saved successfully" : "URL GitLab u ruajt me sukses", + "GitLab token is valid" : "Token GitLab është i vlefshëm", + "GitLab token saved successfully" : "Token GitLab u ruajt me sukses", + "HTTP Method" : "Metoda HTTP", + "HTTP method used to send webhook requests" : "Metoda HTTP e përdorur për të dërguar kërkesa webhook", + "Headers" : "Tituj", + "Health" : "Shëndet", + "Heartbeat successful - connection kept alive" : "Heartbeat i suksesshëm - lidhja mbajtur gjallë", + "Helpful" : "Ndihmës", + "Hide Filters" : "Fshih Filtrat", + "Hide in forms" : "Fshih në formularë", + "Hide in list view" : "Fshih në pamjen listë", + "High" : "Lartë", + "High Confidence" : "Besim i Lartë", + "Host" : "Pritës", + "Hourly" : "Çdo orë", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Sa thellë të kaluara pronat e objekteve të mbivendosura (1-20). Vlerat më të larta kapin më shumë detaje por rrisin madhësinë e vektorit.", + "How often to check for updates (1-168 hours)" : "Sa shpesh të kontrolloni për përditësime (1-168 orë)", + "How to handle retries for failed webhook deliveries" : "Si të trajtoni riprovimet për dërgesat e dështuara të webhook", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Kufizimet e shkallës IP u pastruan me sukses", + "Identify properties not in the schema" : "Identifikoni pronat që nuk janë në skemë", + "Immutable" : "I pandryshueshëm", + "Import" : "Importo", + "Import successful" : "Importimi u krye me sukses", + "Improved Property" : "Prona e Përmirësuar", + "In use" : "Në përdorim", + "Inactive" : "Joaktiv", + "Include IDs and names of related objects for better contextual search" : "Përfshini ID dhe emrat e objekteve të lidhura për kërkim kontekstual më të mirë", + "Include related object references" : "Përfshini referencat e objekteve të lidhura", + "Include schema and register metadata" : "Përfshini meta të dhënat e skemës dhe regjistrit", + "Initialization failed" : "Inicializimi dështoi", + "Initialize Project" : "Inicializoni Projektin", + "Inspect Fields" : "Inspektoni Fushat", + "Inspect Index" : "Inspektoni Indeksin", + "Invalid" : "I pavlefshëm", + "Invalid batch size. Must be between 1 and 5000" : "Madhësia e grupit e pavlefshme. Duhet të jetë midis 1 dhe 5000", + "Invalid field name provided" : "Emri i fushës i pavlefshëm i ofruar", + "Invalid maxObjects. Must be 0 (all) or positive number" : "maxObjects i pavlefshëm. Duhet të jetë 0 (të gjitha) ose numër pozitiv", + "Invalid:" : "I pavlefshëm:", + "Issue" : "Problem", + "Items per page:" : "Artikuj për faqe:", + "Just now" : "Tani", + "LLM Configuration" : "Konfigurimi LLM", + "LLM actions menu" : "Menuja e veprimeve LLM", + "LLM configuration saved successfully" : "Konfigurimi LLM u ruajt me sukses", + "LLM features disabled" : "Veçoritë LLM çaktivizuara", + "LLM features enabled" : "Veçoritë LLM aktivizuara", + "LLM must be enabled with an embedding provider configured" : "LLM duhet të aktivizohet me një ofrues embedding të konfiguruar", + "LLM settings updated successfully" : "Cilësimet LLM u përditësuan me sukses", + "Last" : "I fundit", + "Last Triggered" : "Aktivizuar Herën e Fundit", + "Length Range:" : "Intervali i Gjatësisë:", + "Linear" : "Linear", + "Load Advanced Filters" : "Ngarko Filtrat e Avancuar", + "Load advanced filters with live data from your search index" : "Ngarko filtrat e avancuar me të dhëna live nga indeksi juaj i kërkimit", + "Load view" : "Ngarko pamjen", + "Loading ConfigSets..." : "Duke ngarkuar ConfigSets...", + "Loading advanced filters..." : "Duke ngarkuar filtrat e avancuar...", + "Loading agents..." : "Duke ngarkuar agjentët...", + "Loading application details..." : "Duke ngarkuar detajet e aplikacionit...", + "Loading applications..." : "Duke ngarkuar aplikacionet...", + "Loading archived conversations..." : "Duke ngarkuar bisedat e arkivuara...", + "Loading audit trails..." : "Duke ngarkuar gjurmët e auditimit...", + "Loading collections..." : "Duke ngarkuar koleksionet...", + "Loading configurations..." : "Duke ngarkuar konfigurimet...", + "Loading conversation..." : "Duke ngarkuar bisedën...", + "Loading conversations..." : "Duke ngarkuar bisedat...", + "Loading deleted items..." : "Duke ngarkuar artikujt e fshirë...", + "Loading events..." : "Duke ngarkuar ngjarjet...", + "Loading filter data..." : "Duke ngarkuar të dhënat e filtrit...", + "Loading groups..." : "Duke ngarkuar grupet...", + "Loading log details..." : "Duke ngarkuar detajet e regjistrit...", + "Loading registers..." : "Duke ngarkuar regjistrat...", + "Loading schemas..." : "Duke ngarkuar skemat...", + "Loading search trails..." : "Duke ngarkuar gjurmët e kërkimit...", + "Loading sources..." : "Duke ngarkuar burimet...", + "Loading statistics..." : "Duke ngarkuar statistikat...", + "Loading users..." : "Duke ngarkuar përdoruesit...", + "Loading views..." : "Duke ngarkuar pamjet...", + "Loading..." : "Duke ngarkuar...", + "Local" : "Lokal", + "Local Version" : "Versioni Lokal", + "Locked" : "I kyçur", + "Locked:" : "I kyçur:", + "Login successful" : "Identifikimi u krye me sukses", + "Logs" : "Regjistra", + "Low" : "Ulët", + "Low Confidence" : "Besim i Ulët", + "Lucene Version:" : "Versioni Lucene:", + "Magic table synchronized successfully" : "Tabela magjike u sinkronizua me sukses", + "Manage Organisation Roles" : "Menaxhoni Rolet e Organizatës", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Menaxhoni Koleksionet SOLR (dyqanet e të dhënave) dhe caktojini ato për objekte dhe skedarë.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Menaxhoni ConfigSets SOLR (shablone konfigurimi) për koleksionet tuaja.", + "Manage and configure agents for automated tasks" : "Menaxhoni dhe konfiguroni agjentët për detyra të automatizuara", + "Manage and monitor file text extraction status" : "Menaxhoni dhe monitoroni statusin e nxjerrjes së tekstit të skedarit", + "Manage and restore soft deleted items from your registers" : "Menaxhoni dhe rivendosni artikujt e fshirë butë nga regjistrat tuaj", + "Manage and view detected entities from files and objects" : "Menaxhoni dhe shikoni entitetet e zbuluara nga skedarët dhe objektet", + "Manage document templates and themes" : "Menaxhoni shabllonet dhe temat e dokumenteve", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Menaxhoni flukset e punës n8n për automatizimin e OpenRegister. Flukset e punës do të ruhen në projektin e konfiguruar.", + "Manage webhooks for event-driven integrations" : "Menaxhoni webhooks për integrime të drejtuar nga ngjarje", + "Manage your applications and modules" : "Menaxhoni aplikacionet dhe modulet tuaja", + "Manage your data registers and their configurations" : "Menaxhoni regjistrat e të dhënave tuaja dhe konfigurimet e tyre", + "Manage your data schemas and their properties" : "Menaxhoni skemat e të dhënave tuaja dhe pronat e tyre", + "Manage your data sources and their configurations" : "Menaxhoni burimet e të dhënave tuaja dhe konfigurimet e tyre", + "Manage your saved search configurations" : "Menaxhoni konfigurimet e ruajtura të kërkimit", + "Manage your system configurations and settings" : "Menaxhoni konfigurimet dhe cilësimet e sistemit tuaj", + "Managed" : "Menaxhuar", + "Max Files (0 = all)" : "Skedarë Maks (0 = të gjithë)", + "Max Length" : "Gjatësi Maks", + "Max Length:" : "Gjatësi Maks:", + "Max Objects (0 = all)" : "Objekte Maks (0 = të gjitha)", + "Max Retries" : "Riprovime Maks", + "Max Shards/Node" : "Shards Maks/Nyje", + "Max execution time (ms)" : "Koha maks e ekzekutimit (ms)", + "Max ms" : "ms maks", + "Max result count" : "Numri maks i rezultateve", + "Max results" : "Rezultate maks", + "Maximum" : "Maksimum", + "Maximum Nesting Depth" : "Thellësia Maksimale e Mbivendosjes", + "Maximum length constraint is missing" : "Kufizimi i gjatësisë maksimale mungon", + "Maximum length is too restrictive" : "Gjatësia maksimale është shumë kufizuese", + "Maximum number of files to process. Set to 0 to process all files." : "Numri maksimal i skedarëve për përpunim. Vendosni 0 për të përpunuar të gjithë skedarët.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Numri maksimal i objekteve për përpunim. Vendosni 0 për të përpunuar të gjitha objektet.", + "Maximum number of retry attempts for failed deliveries" : "Numri maksimal i tentativave të riprovimit për dërgesat e dështuara", + "Maximum time to wait before committing changes" : "Koha maksimale për të pritur para angazhimit të ndryshimeve", + "Maximum tokens to generate" : "Token maksimalë për gjenerim", + "Maximum value constraint is missing" : "Kufizimi i vlerës maksimale mungon", + "Maximum value is too restrictive" : "Vlera maksimale është shumë kufizuese", + "Medium" : "Mesatar", + "Medium Confidence" : "Besim Mesatar", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Pyetje mesatare: Kërkime me filtrim të disa ose parametra të shumtë (p.sh., intervalet e datave, regjistrat/skemat specifike)", + "Members of selected groups can access this view" : "Anëtarët e grupeve të zgjedhura mund të qasen në këtë pamje", + "Members:" : "Anëtarë:", + "Memory prediction calculated successfully" : "Parashikimi i memories u llogarit me sukses", + "Message content is required" : "Përmbajtja e mesazhit është e nevojshme", + "Message does not belong to this conversation" : "Mesazhi nuk i përket kësaj bisede", + "Metadata Filters" : "Filtrat e Meta të dhënave", + "Method" : "Metodë", + "Min Length" : "Gjatësi Min", + "Min execution time (ms)" : "Koha min e ekzekutimit (ms)", + "Min ms" : "ms min", + "Min result count" : "Numri min i rezultateve", + "Min results" : "Rezultate min", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Kufizimi i vlerës minimale mungon", + "Minimum value is too restrictive" : "Vlera minimale është shumë kufizuese", + "Missing object fields created successfully" : "Fushat e objektit që mungonin u krijuan me sukses", + "Mode:" : "Modalitet:", + "Model" : "Model", + "Model Name" : "Emri i Modelit", + "Modified" : "Modifikuar", + "Monthly" : "Çdo muaj", + "More sources (10-20):" : "Burime më shumë (10-20):", + "Most Active Objects" : "Objektet Më Aktive", + "Multiple Of" : "Shumëfish i", + "Name" : "Emër", + "Names cache warmup completed" : "Ngrohja e cache të emrave përfundoi", + "New Collection Name" : "Emri i Ri i Koleksionit", + "New Conversation" : "Bisedë e Re", + "New Properties" : "Prona të Reja", + "New Property" : "Pronë e Re", + "New Value" : "Vlerë e Re", + "New users without specific organisation membership will be automatically added to this organisation" : "Përdoruesit e rinj pa anëtarësi specifike organizate do të shtohen automatikisht në këtë organizatë", + "Next" : "Tjetër", + "No" : "Jo", + "No ConfigSets found" : "Nuk u gjetën ConfigSets", + "No GitHub token configured" : "Nuk ka token GitHub të konfiguruar", + "No GitHub token provided" : "Nuk ka token GitHub të ofruar", + "No GitLab token provided" : "Nuk ka token GitLab të ofruar", + "No Organisation" : "Nuk ka Organizatë", + "No active filters" : "Nuk ka filtra aktiv", + "No activity data available" : "Nuk ka të dhëna aktiviteti të disponueshme", + "No agents available" : "Nuk ka agjentë të disponueshëm", + "No agents found" : "Nuk u gjetën agjentë", + "No app store cache exists yet" : "Nuk ekziston ende cache i dyqanit të aplikacioneve", + "No applications are available." : "Nuk ka aplikacione të disponueshme.", + "No applications found" : "Nuk u gjetën aplikacione", + "No archived conversations" : "Nuk ka biseda të arkivuara", + "No audit trail entries found" : "Nuk u gjetën hyrje gjurmësh auditimi", + "No changes recorded" : "Nuk u regjistruan ndryshime", + "No chunks to vectorize" : "Nuk ka copa për vectorizim", + "No collections found" : "Nuk u gjetën koleksione", + "No configuration data" : "Nuk ka të dhëna konfigurimi", + "No configurations are available." : "Nuk ka konfigurime të disponueshme.", + "No configurations found" : "Nuk u gjetën konfigurime", + "No configurations found for this application." : "Nuk u gjetën konfigurime për këtë aplikacion.", + "No conversations yet. Create a new one to get started!" : "Ende nuk ka biseda. Krijoni një të re për të filluar!", + "No data available" : "Nuk ka të dhëna të disponueshme", + "No data available for chart" : "Nuk ka të dhëna të disponueshme për grafikun", + "No deleted items found" : "Nuk u gjetën artikuj të fshirë", + "No deletion data available" : "Nuk ka të dhëna fshirjeje të disponueshme", + "No description found" : "Nuk u gjet përshkrim", + "No description provided" : "Nuk u ofrua përshkrim", + "No entities detected in this file. Run text extraction first." : "Nuk u zbuluan entitete në këtë skedar. Ekzekutoni nxjerrjen e tekstit fillimisht.", + "No entities found" : "Nuk u gjetën entitete", + "No entities have been detected yet" : "Ende nuk janë zbuluar entitete", + "No events found" : "Nuk u gjetën ngjarje", + "No expired audit trails found to clear" : "Nuk u gjetën gjurmë auditimi të skaduara për pastroi", + "No facetable fields available. Select a register and schema to see available filters." : "Nuk ka fusha me facet të disponueshme. Zgjidhni një regjistër dhe skemë për të parë filtrat e disponueshëm.", + "No files found" : "Nuk u gjetën skedarë", + "No files have been extracted yet" : "Ende nuk janë nxjerrë skedarë", + "No files to process" : "Nuk ka skedarë për përpunim", + "No files to reindex" : "Nuk ka skedarë për riindeksim", + "No filters are currently active. This will delete ALL audit trail entries!" : "Nuk ka filtra aktualë aktiv. Kjo do të fshijë TË GJITHA hyrjet e gjurmëve të auditimit!", + "No log entries found" : "Nuk u gjetën hyrje regjistri", + "No logs are available for this configuration." : "Nuk ka regjistra të disponueshëm për këtë konfigurim.", + "No logs are available for this source." : "Nuk ka regjistra të disponueshëm për këtë burim.", + "No logs found" : "Nuk u gjetën regjistra", + "No mismatched fields found - SOLR schema is properly configured" : "Nuk u gjetën fusha të papërputhshme - skema SOLR është konfiguruar siç duhet", + "No objects found" : "Nuk u gjetën objekte", + "No objects found to vectorize" : "Nuk u gjetën objekte për vectorizim", + "No objects selected for deletion" : "Nuk janë zgjedhur objekte për fshirje", + "No objects to delete" : "Nuk ka objekte për fshirje", + "No objects were permanently deleted" : "Nuk u fshi përgjithmonë asnjë objekt", + "No properties found" : "Nuk u gjetën prona", + "No properties match your filters." : "Asnjë pronë nuk përputhet me filtrat tuaj.", + "No purge date set" : "Nuk është vendosur datë pastrimi", + "No register data available" : "Nuk ka të dhëna regjistri të disponueshme", + "No registers found" : "Nuk u gjetën regjistër", + "No registers found for this application." : "Nuk u gjetën regjistër për këtë aplikacion.", + "No relations found" : "Nuk u gjetën lidhje", + "No request body available" : "Nuk ka trup kërkese të disponueshëm", + "No response body available" : "Nuk ka trup përgjigje të disponueshëm", + "No saved views yet. Create one in the Search tab!" : "Ende nuk ka pamje të ruajtura. Krijoni një në skedën Kërkim!", + "No schema data available" : "Nuk ka të dhëna skeme të disponueshme", + "No schema selected for exploration" : "Nuk është zgjedhur skemë për eksplorim", + "No schemas found" : "Nuk u gjetën skema", + "No schemas found for this application." : "Nuk u gjetën skema për këtë aplikacion.", + "No search terms data available" : "Nuk ka të dhëna termash kërkimi të disponueshme", + "No search trail entries found" : "Nuk u gjetën hyrje gjurmësh kërkimi", + "No sources are available." : "Nuk ka burime të disponueshme.", + "No sources found" : "Nuk u gjetën burime", + "No templates found" : "Nuk u gjetën shabllone", + "No templates have been created yet" : "Ende nuk janë krijuar shabllone", + "No valid magic-mapped register+schema combinations found" : "Nuk u gjetën kombinime të vlefshme regjistër+skemë të hartëzuara magjikisht", + "No views found. Create views first before configuring vectorization." : "Nuk u gjetën pamje. Krijoni pamje fillimisht para se të konfiguroni vectorizimin.", + "No views match your search" : "Asnjë pamje nuk përputhet me kërkimin tuaj", + "No webhooks found" : "Nuk u gjetën webhooks", + "No webhooks have been configured yet" : "Ende nuk janë konfiguruar webhooks", + "No workflows found in this project. Create workflows in the n8n editor." : "Nuk u gjetën flukse pune në këtë projekt. Krijoni flukse pune në redaktuesin n8n.", + "None" : "Asnjë", + "Not helpful" : "Nuk ndihmon", + "Not in use" : "Nuk është në përdorim", + "Number Constraints" : "Kufizimet e Numrit", + "Number of chunks to vectorize in one API call (1-100)." : "Numri i copave për vectorizim në një thirrje API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Numri i copave për vectorizim në një thirrje API. Më i lartë = më shpejtë por më shumë memory. Rekomandohet: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Numri i objekteve për përpunim në çdo grup (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Numri i objekteve për vectorizim në një thirrje API. Më i lartë = më shpejtë por më shumë memory. Rekomandohet: 10-50.", + "Numeric Range:" : "Intervali Numerik:", + "Object" : "Objekt", + "Object Collection" : "Koleksioni i Objekteve", + "Object Count Prediction" : "Parashikimi i Numrit të Objekteve", + "Object ID" : "ID e Objektit", + "Object Management" : "Menaxhimi i Objekteve", + "Object Vectorization" : "Vectorizimi i Objekteve", + "Object Warmup" : "Ngrohja e Objekteve", + "Object collection not configured" : "Koleksioni i objekteve nuk është konfiguruar", + "Object not found" : "Objekti nuk u gjet", + "Object permanently deleted" : "Objekti u fshi përgjithmonë", + "Object restored successfully" : "Objekti u rivendos me sukses", + "Object settings updated successfully" : "Cilësimet e objektit u përditësuan me sukses", + "Object sources" : "Burimet e objekteve", + "Object unlocked successfully" : "Objekti u çkyç me sukses", + "Object vectorization configuration saved successfully" : "Konfigurimi i vectorizimit të objektit u ruajt me sukses", + "Object vectorized successfully" : "Objekti u vectorizua me sukses", + "Object:" : "Objekt:", + "Objects" : "Objekte", + "Objects Analyzed" : "Objekte të Analizuara", + "Objects Distribution" : "Shpërndarja e Objekteve", + "Objects being analyzed" : "Objekte duke u analizuar", + "Objects by Register" : "Objekte sipas Regjistrit", + "Objects by Schema" : "Objekte sipas Skemës", + "Objects deletion completed successfully" : "Fshirja e objekteve u krye me sukses", + "Objects in schema" : "Objekte në skemë", + "Objects to Process:" : "Objekte për Përpunim:", + "Objects to be analyzed" : "Objekte për t'u analizuar", + "Objects to be deleted" : "Objekte për t'u fshirë", + "Objects to be validated" : "Objekte për t'u validuar", + "Objects will be serialized as JSON text before vectorization" : "Objektet do të serializohen si tekst JSON para vectorizimit", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objektet do të fshihen butë (shënuar si të fshirë por mbajtur në bazën e të dhënave). Ato mund të rikuperohen më vonë nëse nevojitet.", + "Old Value" : "Vlera e Vjetër", + "Oldest Item (days)" : "Artikulli Më i Vjetër (ditë)", + "Ollama Chat Settings" : "Cilësimet e Chat Ollama", + "Ollama Embedding Configuration" : "Konfigurimi i Embedding Ollama", + "Ollama URL" : "URL Ollama", + "Only With Changes" : "Vetëm Me Ndryshime", + "Open Register Objects" : "Hapni Objektet e Regjistrit", + "Open n8n Editor" : "Hapni Redaktuesin n8n", + "OpenAI Chat Settings" : "Cilësimet e Chat OpenAI", + "OpenAI Embedding Configuration" : "Konfigurimi i Embedding OpenAI", + "OpenRegister Settings" : "Cilësimet e OpenRegister", + "Optional webhook secret for signature verification" : "Sekret opsional webhook për verifikimin e nënshkrimit", + "Order" : "Rend", + "Organisation" : "Organizatë", + "Organisation ID" : "ID e Organizatës", + "Organisation Members" : "Anëtarët e Organizatës", + "Organisation Statistics" : "Statistikat e Organizatës", + "Organisation created successfully" : "Organizata u krijua me sukses", + "Organisation settings saved successfully" : "Cilësimet e organizatës u ruajtën me sukses", + "Organisations" : "Organizata", + "Organization" : "Organizatë", + "Organization ID (Optional)" : "ID e Organizatës (Opsional)", + "Orphaned Items" : "Artikuj të Braktisur", + "Owner" : "Pronar", + "Owner:" : "Pronar:", + "Parallel Mode (Faster, more resource intensive)" : "Modaliteti Paralel (Më i shpejtë, me më shumë burime)", + "Parallel:" : "Paralel:", + "Password" : "Fjalëkalim", + "Password for SOLR authentication (optional)" : "Fjalëkalim për autentifikimin SOLR (opsional)", + "Path" : "Shteg", + "Path in repository" : "Shtegu në depo", + "Path where the configuration file will be saved in the repository" : "Shtegu ku do të ruhet skedari i konfigurimit në depo", + "Path where the register OAS file will be saved in the repository" : "Shtegu ku do të ruhet skedari OAS i regjistrit në depo", + "Pattern (regex)" : "Model (regex)", + "Pattern Issue" : "Problem Modeli", + "Pattern constraint is missing" : "Kufizimi i modelit mungon", + "Patterns:" : "Modele:", + "Pending" : "Në pritje", + "Permanent Delete Mode" : "Modaliteti i Fshirjes së Përhershme", + "Permanently Delete" : "Fshi Përgjithmonë", + "Permanently delete already soft-deleted objects" : "Fshini përgjithmonë objektet tashmë të fshira butë", + "Permanently delete this conversation?" : "Fshini përgjithmonë këtë bisedë?", + "Person" : "Person", + "Personal Data" : "Të Dhëna Personale", + "Phone" : "Telefon", + "Photos" : "Foto", + "Please create an agent in the" : "Ju lutemi krijoni një agjent në", + "Please select an agent to continue" : "Ju lutemi zgjidhni një agjent për të vazhduar", + "Please select which register and schema to use for the new object" : "Ju lutemi zgjidhni cilin regjistër dhe skemë të përdorni për objektin e ri", + "Please try again later." : "Ju lutemi provoni përsëri më vonë.", + "Please wait while we fetch your agents." : "Ju lutemi prisni ndërkohë që marrim agjentët tuaj.", + "Please wait while we fetch your applications." : "Ju lutemi prisni ndërkohë që marrim aplikacionet tuaja.", + "Please wait while we fetch your configurations." : "Ju lutemi prisni ndërkohë që marrim konfigurimet tuaja.", + "Please wait while we fetch your deleted items." : "Ju lutemi prisni ndërkohë që marrim artikujt tuaj të fshirë.", + "Please wait while we fetch your sources." : "Ju lutemi prisni ndërkohë që marrim burimet tuaja.", + "Polls" : "Anketa", + "Popular Search Terms" : "Termat e Popullarizuara të Kërkimit", + "Port" : "Port", + "Prerequisites" : "Parakushte", + "Press Enter to send, Shift+Enter for new line" : "Shtypni Enter për të dërguar, Shift+Enter për rresht të ri", + "Previous" : "I mëparshëm", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Përpunon copa në grupe me paralelizëm të simuluar.", + "Processes file chunks sequentially (safest)." : "Përpunon copa skedarësh sekuencialisht (më i sigurt).", + "Processes objects in chunks with simulated parallelism." : "Përpunon objekte në copa me paralelizëm të simuluar.", + "Processes objects sequentially (safest)." : "Përpunon objekte sekuencialisht (më i sigurt).", + "Processing" : "Përpunim", + "Processing Limits" : "Kufijtë e Përpunimit", + "Processing..." : "Duke përpunuar...", + "Project initialized successfully" : "Projekti u inicializua me sukses", + "Project not found. Please initialize first." : "Projekti nuk u gjet. Ju lutemi inicializoni fillimisht.", + "Properties" : "Prona", + "Property Behaviors" : "Sjelljet e Pronave", + "Property Configuration:" : "Konfigurimi i Pronës:", + "Property Filters" : "Filtrat e Pronave", + "Property Title" : "Titulli i Pronës", + "Property Type" : "Lloji i Pronës", + "Property can be improved" : "Prona mund të përmirësohet", + "Protected" : "I mbrojtur", + "Provider" : "Ofrues", + "Provider is required for testing" : "Ofruesi është i nevojshëm për testim", + "Public" : "Publik", + "Public View" : "Pamje Publike", + "Public views can be accessed by anyone in the system" : "Pamjet publike mund të aksesohen nga çdokush në sistem", + "Publish" : "Publiko", + "Publish Objects" : "Publiko Objektet", + "Publish all objects in this schema" : "Publiko të gjitha objektet në këtë skemë", + "Published" : "Publikuar", + "Published:" : "Publikuar:", + "Publishing..." : "Duke publikuar...", + "Purge" : "Pastro", + "Purge Date" : "Data e Pastrimit", + "Query Complexity Distribution" : "Shpërndarja e Kompleksitetit të Pyetjeve", + "Query parameter is required" : "Parametri i pyetjes është i nevojshëm", + "RAG" : "RAG", + "RAG Configuration" : "Konfigurimi RAG", + "Rate limits cleared successfully" : "Kufizimet e shkallës u pastruan me sukses", + "Raw Changes Data" : "Të Dhënat Bruto të Ndryshimeve", + "Re-vectorize on object update" : "Rivektorizoni gjatë përditësimit të objektit", + "Read" : "Lexo", + "Recommendations:" : "Rekomandimet:", + "Recommended Type:" : "Lloji i Rekomanduar:", + "Recommended: 5 sources" : "Rekomandohet: 5 burime", + "Refresh" : "Rifresko", + "Refresh Data" : "Rifresko Të dhënat", + "Refresh Stats" : "Rifresko Statistikat", + "Refresh Workflows" : "Rifresko Flukset e Punës", + "Refresh dashboard" : "Rifresko panelin", + "Refresh database info" : "Rifresko informacionin e bazës së të dhënave", + "Register" : "Regjistër", + "Register ID" : "ID e Regjistrit", + "Register Statistics" : "Statistikat e Regjistrit", + "Register Totals" : "Totalet e Regjistrit", + "Register depublished successfully" : "Regjistri u çpublikua me sukses", + "Register not found" : "Regjistri nuk u gjet", + "Register objects deletion completed successfully" : "Fshirja e objekteve të regjistrit u krye me sukses", + "Register published successfully" : "Regjistri u publikua me sukses", + "Register/Schema Usage" : "Përdorimi i Regjistrit/Skemës", + "Registers" : "Regjistër", + "Reindex" : "Riindekso", + "Reindex completed" : "Riindeksimi përfundoi", + "Reindex completed successfully" : "Riindeksimi u krye me sukses", + "Reindex failed" : "Riindeksimi dështoi", + "Relations" : "Lidhje", + "Remote Version" : "Versioni i Largët", + "Remove" : "Hiq", + "Remove filter" : "Hiq filtrin", + "Remove from favorites" : "Hiq nga të preferuarat", + "Remove group" : "Hiq grupin", + "Removed" : "Hequr", + "Removed from favorites" : "Hequr nga të preferuarat", + "Rename Conversation" : "Riemërtoni Bisedën", + "Rename conversation" : "Riemërtoni bisedën", + "Replicas" : "Replika", + "Repository" : "Depo", + "Request Body" : "Trupi i Kërkesës", + "Request Data" : "Të Dhënat e Kërkesës", + "Request Details" : "Detajet e Kërkesës", + "Request body must be JSON with a \"query\" field" : "Trupi i kërkesës duhet të jetë JSON me një fushë \"query\"", + "Request timeout in seconds" : "Koha e skadimit të kërkesës në sekonda", + "Required field" : "Fushë e nevojshme", + "Required status is inconsistent" : "Statusi i nevojshëm është i pasqendrueshëm", + "Rerun Search" : "Rilëxoni Kërkimin", + "Reset Changes" : "Rivendosni Ndryshimet", + "Reset Filters" : "Rivendosni Filtrat", + "Response Body" : "Trupi i Përgjigjes", + "Response Details" : "Detajet e Përgjigjes", + "Response Time:" : "Koha e Përgjigjes:", + "Restore" : "Rivendos", + "Restore conversation" : "Rivendos bisedën", + "Restore or permanently delete items" : "Rivendosni ose fshini përgjithmonë artikujt", + "Result Count Range" : "Intervali i Numrit të Rezultateve", + "Results" : "Rezultate", + "Retrieve all objects for this schema" : "Merrni të gjitha objektet për këtë skemë", + "Retry" : "Riprovo", + "Retry Failed Extractions" : "Riprovo Nxjerrjet e Dështuara", + "Retry Policy" : "Politika e Riprovimit", + "Retry completed" : "Riprova u krye", + "Retrying..." : "Duke riprovuar...", + "Risk Level" : "Niveli i Rrezikut", + "Roles updated successfully" : "Rolet u përditësuan me sukses", + "SOLR Connection Settings" : "Cilësimet e Lidhjes SOLR", + "SOLR Version:" : "Versioni SOLR:", + "SOLR actions menu" : "Menuja e veprimeve SOLR", + "SOLR base path (usually /solr)" : "Shtegu bazë SOLR (zakonisht /solr)", + "SOLR is disabled" : "SOLR është çaktivizuar", + "SOLR is not available or not configured" : "SOLR nuk është i disponueshëm ose nuk është konfiguruar", + "SOLR logging disabled" : "Regjistrimi SOLR çaktivizuar", + "SOLR logging enabled" : "Regjistrimi SOLR aktivizuar", + "SOLR search disabled" : "Kërkimi SOLR çaktivizuar", + "SOLR search enabled" : "Kërkimi SOLR aktivizuar", + "SOLR server hostname or IP address" : "Emri i serverit SOLR ose adresa IP", + "SOLR server port number (optional, defaults to 8983)" : "Numri i portit të serverit SOLR (opsional, parazgjedhur 8983)", + "SOLR setup completed successfully" : "Konfigurimi SOLR u krye me sukses", + "SOLR setup failed" : "Konfigurimi SOLR dështoi", + "SOLR setup failed - check logs" : "Konfigurimi SOLR dështoi - kontrolloni regjistrat", + "Sample Values:" : "Vlera Shembull:", + "Save" : "Ruaj", + "Save Configuration" : "Ruaj Konfigurimin", + "Save Connection Settings" : "Ruaj Cilësimet e Lidhjes", + "Save Roles" : "Ruaj Rolet", + "Save Settings" : "Ruaj Cilësimet", + "Save current search as view" : "Ruaj kërkimin aktual si pamje", + "Saved Views" : "Pamjet e Ruajtura", + "Saving..." : "Duke ruajtur...", + "Schema" : "Skemë", + "Schema ID" : "ID e Skemës", + "Schema Name" : "Emri i Skemës", + "Schema Statistics" : "Statistikat e Skemës", + "Schema depublished successfully" : "Skema u çpublikua me sukses", + "Schema objects deletion completed successfully" : "Fshirja e objekteve të skemës u krye me sukses", + "Schema published successfully" : "Skema u publikua me sukses", + "Schema successfully created" : "Skema u krijua me sukses", + "Schema successfully updated" : "Skema u përditësua me sukses", + "Schemas" : "Skema", + "Scheme" : "Skemë", + "Search / Views" : "Kërkim / Pamje", + "Search Activity" : "Aktiviteti i Kërkimit", + "Search Analytics" : "Analitika e Kërkimit", + "Search Entities" : "Kërko Entitete", + "Search Files" : "Kërko Skedarë", + "Search GitHub" : "Kërko në GitHub", + "Search GitLab" : "Kërko në GitLab", + "Search Method" : "Metoda e Kërkimit", + "Search Term" : "Termi i Kërkimit", + "Search Trail Details" : "Detajet e Gjurmës së Kërkimit", + "Search Trail Management" : "Menaxhimi i Gjurmëve të Kërkimit", + "Search Trail Statistics" : "Statistikat e Gjurmëve të Kërkimit", + "Search Trails" : "Gjurmët e Kërkimit", + "Search Views" : "Kërko Pamje", + "Search Webhooks" : "Kërko Webhooks", + "Search by file name or path" : "Kërko sipas emrit të skedarit ose shtegjit", + "Search by name or URL" : "Kërko sipas emrit ose URL", + "Search by value" : "Kërko sipas vlerës", + "Search in Files" : "Kërko në Skedarë", + "Search in Objects" : "Kërko në Objekte", + "Search property names..." : "Kërko emra pronash...", + "Search trail deletion not implemented yet" : "Fshirja e gjurmëve të kërkimit nuk është zbatuar ende", + "Search views..." : "Kërko pamje...", + "Secret" : "Sekret", + "Secret key for HMAC signature generation (optional)" : "Çelësi sekret për gjenerimin e nënshkrimit HMAC (opsional)", + "Select AI Agent" : "Zgjidhni Agjentin AI", + "Select All" : "Zgjidhni Të gjitha", + "Select ConfigSet" : "Zgjidhni ConfigSet", + "Select File Types to Vectorize:" : "Zgjidhni Llojet e Skedarëve për Vectorizim:", + "Select HTTP method" : "Zgjidhni metodën HTTP", + "Select Register and Schema" : "Zgjidhni Regjistrin dhe Skemën", + "Select Views to Vectorize:" : "Zgjidhni Pamjet për Vectorizim:", + "Select a Nextcloud group to add" : "Zgjidhni një grup Nextcloud për të shtuar", + "Select a branch" : "Zgjidhni një degë", + "Select a model or type a custom model name" : "Zgjidhni një model ose shkruani një emër modeli të personalizuar", + "Select a repository" : "Zgjidhni një depo", + "Select a repository you have write access to" : "Zgjidhni një depo ku keni qasje shkrimi", + "Select an AI Agent" : "Zgjidhni një Agjent AI", + "Select an AI agent to begin chatting with your data." : "Zgjidhni një agjent AI për të filluar bisedën me të dhënat tuaja.", + "Select backend" : "Zgjidhni backend", + "Select chat model" : "Zgjidhni modelin e chat", + "Select collection for files" : "Zgjidhni koleksionin për skedarë", + "Select collection for objects" : "Zgjidhni koleksionin për objekte", + "Select default organisation" : "Zgjidhni organizatën default", + "Select event to listen to..." : "Zgjidhni ngjarjen për të dëgjuar...", + "Select model" : "Zgjidhni modelin", + "Select options..." : "Zgjidhni opsionet...", + "Select period" : "Zgjidhni periudhën", + "Select property to send as payload" : "Zgjidhni pronën për ta dërguar si payload", + "Select provider" : "Zgjidhni ofruesin", + "Select registers and schemas to save a view" : "Zgjidhni regjistrar dhe skema për të ruajtur një pamje", + "Select retry policy" : "Zgjidhni politikën e riprovimit", + "Select the branch to publish to" : "Zgjidhni degën për publikim", + "Select the event this webhook should listen to" : "Zgjidhni ngjarjen që duhet të dëgjojë ky webhook", + "Select views to vectorize:" : "Zgjidhni pamjet për vectorizim:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Zgjidhni cilat grupe Nextcloud janë të disponueshme për këtë organizatë. Përdoruesit në këto grupe do të kenë qasje në burimet e organizatës.", + "Select which data views the AI can search" : "Zgjidhni cilat pamje të dhënash mund të kërkojë AI", + "Select which property from the event should be used as the webhook payload data" : "Zgjidhni cilën pronë nga ngjarja duhet të përdoret si të dhëna payload të webhook", + "Select which tools the AI can use to perform actions" : "Zgjidhni cilat mjete mund të përdorë AI për të kryer veprime", + "Selected" : "Zgjedhur", + "Selected Groups" : "Grupet e Zgjedhura", + "Selected audit trails deleted successfully" : "Gjurmët e zgjedhura të auditimit u fshinë me sukses", + "Selected search trails deleted successfully" : "Gjurmët e zgjedhura të kërkimit u fshinë me sukses", + "Selected users can access this view" : "Përdoruesit e zgjedhur mund të qasen në këtë pamje", + "Send additional feedback" : "Dërgoni reagime shtesë", + "Send as CloudEvent" : "Dërgo si CloudEvent", + "Sensitive PII" : "PII i Ndjeshëm", + "Serial Mode (Safer, slower)" : "Modaliteti Serial (Më i sigurt, më i ngadaltë)", + "Serial:" : "Serial:", + "Server Configuration" : "Konfigurimi i Serverit", + "Server Information" : "Informacioni i Serverit", + "Settings" : "Cilësime", + "Shards" : "Shards", + "Share" : "Ndaj", + "Share with Groups" : "Ndaj me Grupet", + "Share with Users" : "Ndaj me Përdoruesit", + "Show Filters" : "Shfaq Filtrat", + "Show less" : "Shfaq më pak", + "Show only entries with changes" : "Shfaq vetëm hyrjet me ndryshime", + "Simple" : "I thjeshtë", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Pyetje të thjeshta: Kërkime bazë teksti me parametra minimal (p.sh., term i vetëm kërkimi, pa filtra të avancuar)", + "Size" : "Madhësi", + "Slug" : "Slug", + "Soft Delete Mode" : "Modaliteti i Fshirjes Butë", + "Soft Deleted Items" : "Artikujt e Fshirë Butë", + "SolrCloud mode enabled" : "Modaliteti SolrCloud aktivizuar", + "Source" : "Burim", + "Sources" : "Burime", + "Sources:" : "Burime:", + "Standalone SOLR mode" : "Modaliteti SOLR i pavarur", + "Start Conversation" : "Filloni Bisedën", + "Start Vectorization" : "Filloni Vectorizimin", + "Start a conversation" : "Filloni një bisedë", + "Starting names cache warmup..." : "Duke filluar ngrohjen e cache të emrave...", + "Starting..." : "Duke filluar...", + "Statistics" : "Statistika", + "Stats calculation not yet implemented" : "Llogaritja e statistikave nuk është zbatuar ende", + "Status" : "Status", + "Status Code" : "Kodi i Statusit", + "String Constraints" : "Kufizimet e Vargut", + "Success" : "Sukses", + "Success Rate" : "Shkalla e Suksesit", + "Success Status" : "Statusi i Suksesit", + "Successful" : "I suksesshëm", + "Successfully joined organisation" : "Organizata u bashkua me sukses", + "Sync Table" : "Sinkronizo Tabelën", + "Synchronization Settings" : "Cilësimet e Sinkronizimit", + "System Default" : "Parazgjedhja e Sistemit", + "Technical Description" : "Përshkrim Teknik", + "Technical description for developers and administrators" : "Përshkrim teknik për zhvilluesit dhe administratorët", + "Temperature" : "Temperaturë", + "Templates" : "Shabllone", + "Test" : "Test", + "Test Chat" : "Testo Chat", + "Test Connection" : "Testo Lidhjen", + "Test Embedding" : "Testo Embedding", + "Test endpoint executed successfully" : "Pikëfundi i testimit u ekzekutua me sukses", + "Test webhook delivered successfully" : "Webhook-i i testimit u dërgua me sukses", + "Test webhook delivery failed" : "Dërgimi i webhook-it të testimit dështoi", + "Test webhook sent successfully" : "Webhook-i i testimit u dërgua me sukses", + "Testing..." : "Duke testuar...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Copat e tekstit gjenerohen gjatë nxjerrjes së skedarëve dhe ruhen në bazën e të dhënave. Vectorizimi i lexon këto skedarë të para-copëzuar dhe i konverton në embeddings.", + "Text extracted successfully" : "Teksti u nxor me sukses", + "Text extraction disabled" : "Nxjerrja e tekstit çaktivizuar", + "The URL where webhook events will be sent" : "URL ku do të dërgohen ngjarjet e webhook", + "The requested conversation does not exist" : "Biseda e kërkuar nuk ekziston", + "The saved view and all its search configuration" : "Pamja e ruajtur dhe e gjithë konfigurimi i kërkimit të saj", + "There are no audit trail entries matching your current filters." : "Nuk ka hyrje gjurmësh auditimi që përputhen me filtrat tuaj aktualë.", + "There are no deleted items matching your current filters." : "Nuk ka artikuj të fshirë që përputhen me filtrat tuaj aktualë.", + "There are no search trail entries matching your current filters." : "Nuk ka hyrje gjurmësh kërkimi që përputhen me filtrat tuaj aktualë.", + "There are no webhook log entries matching your filters." : "Nuk ka hyrje regjistri webhook që përputhen me filtrat tuaj.", + "This action cannot be undone." : "Ky veprim nuk mund të kthehet.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Ky veprim nuk mund të kthehet. Sigurohuni që asnjë koleksion nuk po përdor këtë ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Ky agjent nuk ka pamje ose mjete të konfigurueshme, por mund të rregulloni ende cilësimet RAG më poshtë.", + "This analysis may take some time" : "Kjo analizë mund të marrë pak kohë", + "This audit trail entry does not contain any change information." : "Kjo hyrje gjurmës auditimi nuk përmban asnjë informacion ndryshimi.", + "This configuration has no data defined." : "Ky konfigurim nuk ka të dhëna të përcaktuara.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ky pikëfund është i vjetruar. Përdorni pikëfundte të bazuara në copa.", + "This entity has no relations to objects or files" : "Ky entitet nuk ka lidhje me objekte ose skedarë", + "This is a test webhook from OpenRegister" : "Ky është një webhook testimi nga OpenRegister", + "This schema must use Magic Table configuration to sync" : "Kjo skemë duhet të përdorë konfigurimin e Tabelës Magjike për sinkronizim", + "This source has no associated registers." : "Ky burim nuk ka regjistrar të lidhura.", + "This will copy the _default ConfigSet with the new name" : "Kjo do të kopjojë ConfigSet-in _default me emrin e ri", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Kjo do të fshijë përgjithmonë TË GJITHA embeddings (vektorët) nga baza e të dhënave. Do t'ju duhet të rivektorizoni të gjitha objektet dhe skedarët. Ky veprim nuk mund të kthehet.\\n\\nJeni i sigurt se doni të vazhdoni?", + "This will permanently delete:" : "Kjo do të fshijë përgjithmonë:", + "Timeout (seconds)" : "Koha e skadimit (sekonda)", + "Timestamp" : "Vulë kohore", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Këshillë: Aktivizoni vetëm pamjet që kanë nevojë për kërkim semantik për të minimizuar kostot e embedding. Tabelat e thjeshta të shfletimit rrallë kanë nevojë për vectorizim.", + "Title" : "Titull", + "To Date" : "Deri në Datë", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Për të rregulluar madhësinë e copës ose strategjinë, shkoni te Konfigurimi i Skedarit → Kufijtë e Përpunimit.", + "To change the embedding provider or model, go to LLM Configuration." : "Për të ndryshuar ofruesin e embedding ose modelin, shkoni te Konfigurimi LLM.", + "To date" : "Deri në datë", + "Toggle search sidebar" : "Ndërroni shiritin anësor të kërkimit", + "Tools" : "Mjete", + "Top Deleters" : "Fshirësit Kryesorë", + "Total" : "Total", + "Total Audit Trails" : "Gjurmë Auditimi Totale", + "Total Chunks Available:" : "Copa Totale të Disponueshme:", + "Total Deleted Items" : "Artikuj Totale të Fshirë", + "Total Members" : "Anëtarë Totalë", + "Total Objects" : "Objekte Totale", + "Total Objects in Database:" : "Objekte Totale në Bazën e Të Dhënave:", + "Total Organisations" : "Organizata Totale", + "Total Results" : "Rezultate Totale", + "Total Searches" : "Kërkime Totale", + "Total Size" : "Madhësia Totale", + "Total Storage" : "Ruajtje Totale", + "Total:" : "Total:", + "Totals" : "Totale", + "Type" : "Lloj", + "Type Issue" : "Problem Lloji", + "Type Variations:" : "Variacionet e Llojit:", + "Type must be \"positive\" or \"negative\"" : "Lloji duhet të jetë \"pozitiv\" ose \"negativ\"", + "Type to search for groups" : "Shkruani për të kërkuar grupe", + "Type to search for users" : "Shkruani për të kërkuar përdorues", + "Type to search groups..." : "Shkruani për të kërkuar grupe...", + "Type to search users..." : "Shkruani për të kërkuar përdorues...", + "URL" : "URL", + "URL where Ollama is running" : "URL ku po ekzekutohet Ollama", + "UUID:" : "UUID:", + "Unchanged" : "I pandryshuar", + "Unique Search Terms" : "Terma Unikë Kërkimi", + "Unique Terms" : "Terma Unikë", + "Unknown" : "I panjohur", + "Unknown Register" : "Regjistër i Panjohur", + "Unknown Schema" : "Skemë e Panjohur", + "Untitled View" : "Pamje pa Titull", + "Update" : "Përditëso", + "Update Operations" : "Operacionet e Përditësimit", + "Update configuration: ..." : "Përditëso konfigurimin: ...", + "Update register OAS: ..." : "Përditëso OAS të regjistrit: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Përditëso vektorët kur të dhënat e objektit ndryshojnë (rekomandohet për kërkim të saktë)", + "Updated" : "Përditësuar", + "Updated At" : "Përditësuar Në", + "Updated:" : "Përditësuar:", + "Uptime:" : "Kohëzgjatja aktive:", + "Usage Count" : "Numri i Përdorimit", + "Use SolrCloud with Zookeeper for distributed search" : "Përdorni SolrCloud me Zookeeper për kërkim të shpërndarë", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Përdorni filtra për të ngushtuar hyrjet e gjurmëve të auditimit sipas regjistrit, skemës, llojit të veprimit, përdoruesit, intervalit të datave ose ID objektit.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Përdorni filtra për të ngushtuar artikujt e fshirë sipas regjistrit, skemës, datës së fshirjes ose përdoruesit që i fshiu.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Përdorni filtra për të ngushtuar hyrjet e gjurmëve të kërkimit sipas regjistrit, skemës, statusit të suksesit, përdoruesit, intervalit të datave, termave të kërkimit ose metrikave të performancës.", + "Used By Collections" : "Përdorur Nga Koleksionet", + "User" : "Përdorues", + "User Agent Statistics" : "Statistikat e Agjentit Përdorues", + "User Description" : "Përshkrimi i Përdoruesit", + "User rate limits cleared successfully" : "Kufizimet e shkallës së përdoruesit u pastruan me sukses", + "User removed successfully" : "Përdoruesi u hoq me sukses", + "User-friendly description shown in forms and help text" : "Përshkrim miqësor për përdoruesin i shfaqur në formularë dhe tekst ndihmës", + "Username" : "Emri i Përdoruesit", + "Username for SOLR authentication (optional)" : "Emri i përdoruesit për autentifikimin SOLR (opsional)", + "Users" : "Përdorues", + "Using Pre-Generated Chunks" : "Duke Përdorur Copa të Para-Gjeneruara", + "Validate" : "Valido", + "Validate Objects" : "Valido Objektet", + "Validation completed successfully" : "Validimi u krye me sukses", + "Validation failed" : "Validimi dështoi", + "Value" : "Vlerë", + "Vector Dimensions" : "Dimensionet Vektoriale", + "Vector Search Backend" : "Backend i Kërkimit Vektorial", + "Vector field: _embedding_" : "Fusha vektoriale: _embedding_", + "Vectorization Triggers" : "Aktivizuesit e Vectorizimit", + "Vectorize All Files" : "Vectorizoni Të Gjithë Skedarët", + "Vectorize All Objects" : "Vectorizoni Të Gjitha Objektet", + "Vectorize all file types" : "Vectorizoni të gjitha llojet e skedarëve", + "Vectorize all views" : "Vectorizoni të gjitha pamjet", + "Vectorize on object creation" : "Vectorizoni gjatë krijimit të objektit", + "Vectors will be stored in your existing object and file collections" : "Vektorët do të ruhen në koleksionet tuaja ekzistuese të objekteve dhe skedarëve", + "Version" : "Version", + "Very High" : "Shumë i Lartë", + "View API Docs" : "Shikoni Dokument API", + "View Changes" : "Shikoni Ndryshimet", + "View Details" : "Shikoni Detajet", + "View Error" : "Gabim Pamje", + "View File" : "Shikoni Skedarin", + "View Full Details" : "Shikoni Detajet e Plota", + "View Logs" : "Shikoni Regjistrat", + "View Name" : "Emri i Pamjes", + "View Object" : "Shikoni Objektin", + "View Parameters" : "Parametrat e Pamjes", + "View Selection" : "Zgjedhja e Pamjes", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Shikoni dhe analizoni regjistrat e gjurmëve të kërkimit me aftësi filtrimi dhe analitike të avancuara", + "View and analyze system audit trails with advanced filtering capabilities" : "Shikoni dhe analizoni gjurmët e auditimit të sistemit me aftësi filtrimi të avancuara", + "View deleted successfully" : "Pamja u fshi me sukses", + "View entity details and manage relations" : "Shikoni detajet e entitetit dhe menaxhoni lidhjet", + "View name" : "Emri i pamjes", + "View name is required" : "Emri i pamjes është i nevojshëm", + "View saved successfully!" : "Pamja u ruajt me sukses!", + "View search analytics and manage search logs" : "Shikoni analitikën e kërkimit dhe menaxhoni regjistrat e kërkimit", + "View successfully deleted" : "Pamja u fshi me sukses", + "View successfully updated" : "Pamja u përditësua me sukses", + "View updated successfully!" : "Pamja u përditësua me sukses!", + "View webhook delivery logs and filter by webhook" : "Shikoni regjistrat e dërgimit të webhook dhe filtroni sipas webhook", + "Views" : "Pamje", + "Visible to users" : "Dukshëm për përdoruesit", + "Wait for Response" : "Prisni Përgjigjen", + "Wait for webhook response before continuing (required for request/response flows)" : "Prisni përgjigjen e webhook para se të vazhdoni (e nevojshme për flukset e kërkesës/përgjigjes)", + "Warmup Names Cache" : "Ngrohni Cache të Emrave", + "Warning" : "Paralajmërim", + "Warning:" : "Paralajmërim:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Detajet e Regjistrit të Webhook", + "Webhook Logs" : "Regjistrat e Webhook", + "Webhook created successfully" : "Webhook u krijua me sukses", + "Webhook deleted" : "Webhook u fshi", + "Webhook retry delivered successfully" : "Riprova e webhook u dërgua me sukses", + "Webhook retry delivery failed" : "Dërgimi i riprovës së webhook dështoi", + "Webhook updated" : "Webhook u përditësua", + "Webhook updated successfully" : "Webhook u përditësua me sukses", + "Webhooks" : "Webhooks", + "Weekly" : "Çdo javë", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Mbëshillni payload-in e webhook në formatin CloudEvents për ndërveprim më të mirë", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: vlerë\\nAuthorization: Bearer token", + "Yes" : "Po", + "You" : "Ju", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Mund të krijoni ConfigSets të rinj bazuar në shablonin _default, ose ngarkoni ato të personalizuara drejtpërdrejt në serverin tuaj SOLR.", + "You do not have access to this conversation" : "Nuk keni qasje në këtë bisedë", + "You do not have permission to delete this conversation" : "Nuk keni leje të fshini këtë bisedë", + "You do not have permission to modify this conversation" : "Nuk keni leje të modifikoni këtë bisedë", + "You do not have permission to restore this conversation" : "Nuk keni leje të rivendosni këtë bisedë", + "You must be logged in to favorite views" : "Duhet të jeni i identifikuar për të preferuar pamjet", + "You need an AI agent to start a conversation." : "Ju nevojitet një agjent AI për të filluar një bisedë.", + "Your Fireworks AI API key. Get one at" : "Çelësi juaj API Fireworks AI. Merreni në", + "Your OpenAI API key. Get one at" : "Çelësi juaj API OpenAI. Merreni në", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Reagimet tuaja janë regjistruar. Opsionalisht, mund të ofroni detaje shtesë këtu...", + "Zookeeper Hosts" : "Pritësit Zookeeper", + "Zookeeper Port" : "Porti Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Cilësimet Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Vargu i lidhjes Zookeeper për SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Numri i portit Zookeeper (opsional, parazgjedhur 2181)", + "chunk" : "copë", + "chunks" : "copa", + "chunks processed" : "copa të përpunuara", + "configuration(s)" : "konfigurim(e)", + "e.g., lib/Settings/config.json" : "p.sh., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "p.sh., lib/Settings/register.json", + "failed" : "dështoi", + "file" : "skedar", + "files" : "skedarë", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "anëtarë", + "menu or contact someone with permission to create agents." : "menusë ose kontaktoni dikë me leje për të krijuar agjentë.", + "messages" : "mesazhe", + "n8n URL and API key are required" : "URL dhe çelësi API i n8n janë të nevojshëm", + "n8n configuration saved successfully" : "Konfigurimi n8n u ruajt me sukses", + "n8n connection not configured" : "Lidhja n8n nuk është konfiguruar", + "n8n connection successful" : "Lidhja n8n u krye me sukses", + "n8n connection test successful" : "Testimi i lidhjes n8n u krye me sukses", + "n8n integration disabled" : "Integrimi n8n çaktivizuar", + "n8n integration enabled" : "Integrimi n8n aktivizuar", + "n8n project initialized successfully" : "Projekti n8n u inicializua me sukses", + "n8n settings saved successfully" : "Cilësimet n8n u ruajtën me sukses", + "objectType: object\\naction: created" : "objectType: objekt\\naction: krijuar", + "objects" : "objekte", + "objects processed" : "objekte të përpunuara", + "or pick a range" : "ose zgjidhni një varg", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "ofron një ekuilibër të mirë midis shpejtësisë dhe saktësisë.", + "register(s)" : "regjistër(e)", + "register(s) selected" : "regjistër(e) të zgjedhur", + "required" : "i nevojshëm", + "results" : "rezultate", + "schema(s)" : "skemë(a)", + "schema(s) selected" : "skemë(a) të zgjedhura", + "sk-..." : "sk-...", + "this application" : "ky aplikacion", + "to" : "te", + "vectorized" : "vectorizuar", + "views selected" : "pamje të zgjedhura", + "ℹ️ Current Configuration" : "ℹ️ Konfigurimi Aktual", + "⚡ Batch Processing" : "⚡ Përpunim në Grupe", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Kontekst gjithëpërfshirës ✓ Më pak të ngjarë të humbasë detaje ✗ Përgjigje më të ngadalta ✗ Mund të përfshijë informacion më pak relevant", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Përgjigje më të shpejta ✓ Përgjigje më të fokusuara ✗ Mund të humbasë informacion relevant", + "✨ AI Features" : "✨ Veçoritë AI", + "All search trails cleared successfully" : "Të gjitha gjurmët e kërkimit u pastruan me sukses", + "Multiple search trail deletion not implemented yet" : "Fshirja e shumë gjurmëve kërkimi nuk është zbatuar ende", + "No expired search trails found to clear" : "Nuk u gjetën gjurmë kërkimi të skaduara për pastroi", + "Missing message" : "Mesazhi mungon", + "message content is required" : "përmbajtja e mesazhit është e nevojshme", + "Missing conversation or agentUuid" : "Biseda ose agentUuid mungon", + "Access denied" : "Qasja refuzuar", + "Conversation not found" : "Biseda nuk u gjet", + "AI service not configured" : "Shërbimi AI nuk është konfiguruar", + "Failed to process message" : "Dështoi përpunimi i mesazhit", + "Missing conversationId" : "conversationId mungon", + "conversationId is required" : "conversationId është i nevojshëm", + "Failed to fetch conversation history" : "Dështoi marrja e historikut të bisedës", + "Failed to clear conversation" : "Dështoi pastrimi i bisedës", + "Invalid feedback type" : "Lloji i reagimit i pavlefshëm", + "type must be \"positive\" or \"negative\"" : "lloji duhet të jetë \"pozitiv\" ose \"negativ\"", + "Message not found" : "Mesazhi nuk u gjet", + "Failed to save feedback" : "Dështoi ruajtja e reagimeve", + "Failed to get chat statistics" : "Dështoi marrja e statistikave të chat", + "Not Found" : "Nuk u Gjet", + "Engine not found" : "Motori nuk u gjet", + "Consumer not found" : "Konsumatori nuk u gjet", + "Backend parameter is required" : "Parametri backend është i nevojshëm", + "Failed to get database information: %s" : "Dështoi marrja e informacionit të bazës së të dhënave: %s", + "SOLR setup error: %s" : "Gabim konfigurimi SOLR: %s", + "Reindex failed: %s" : "Riindeksimi dështoi: %s", + "Anonymized" : "Anonimizuar", + "Entities detected" : "Entitete të zbuluara", + "Extract Now" : "Nxirr Tani", + "Extracted at" : "Nxjerrë në", + "Extraction" : "Nxjerrje", + "Failed to load extraction data" : "Dështoi ngarkimi i të dhënave të nxjerrjes", + "Failed to load register data" : "Dështoi ngarkimi i të dhënave të regjistrit", + "Failed to retrieve extraction status." : "Dështoi marrja e statusit të nxjerrjes.", + "Failed to retrieve objects for file." : "Dështoi marrja e objekteve për skedarin.", + "No extraction data available for this file" : "Nuk ka të dhëna nxjerrjeje të disponueshme për këtë skedar", + "No register objects reference this file" : "Asnjë objekt regjistri nuk i referohet këtij skedari", + "Not extracted" : "Nuk është nxjerrë", + "Register Objects" : "Objekte Regjistri", + "Risk level" : "Niveli i rrezikut", + "Text chunks" : "Copa teksti", + "Unknown error" : "Gabim i panjohur", + "Very high" : "Shumë i lartë", + "{title} in {register} / {schema}" : "{title} në {register} / {schema}", + "Push Notifications" : "Njoftime Push", + "Real-time push notification status via notify_push" : "Statusi i njoftimeve push në kohë reale nëpërmjet notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push në kohë reale nuk është i disponueshëm — aplikacioni notify_push nuk është instaluar", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Instaloni aplikacionin notify_push nga Dyqani i Aplikacioneve Nextcloud për të aktivizuar përditësimet në kohë reale.", + "Open Nextcloud App Store" : "Hapni Dyqanin e Aplikacioneve Nextcloud", + "notify_push is installed but not yet active" : "notify_push është instaluar por ende nuk është aktiv", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push është instaluar por OpenRegister nuk ka konfirmuar ende një push të suksesshëm. Aktivizoni ruajtjen e objektit për ta aktivizuar, ose kontrolloni konfigurimin tuaj notify_push.", + "notify_push configuration guide" : "udhëzuesi i konfigurimit notify_push", + "Realtime push active" : "Push në kohë reale aktiv", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Njoftimet push në kohë reale janë aktive. Klientët e lidhur marrin përditësime të menjëhershme kur objektet krijohen, përditësohen ose fshihen.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/sq.json b/l10n/sq.json new file mode 100644 index 0000000000..90651c0449 --- /dev/null +++ b/l10n/sq.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Kërkohet Dolphin)", + "(no subject)": "(pa temë)", + "(no title)": "(pa titull)", + "(unnamed)": "(pa emër)", + "(untitled card)": "(kartë pa titull)", + "({count} selected)": "({count} të zgjedhura)", + ", {failed} failed": ", {failed} dështoi", + "0 = unlimited": "0 = pa limit", + "A deactivation request is pending.": "Një kërkesë çaktivizimi është në pritje.", + "AI Assistant": "Asistent AI", + "AI Chat": "Chat AI", + "AI service not configured": "Shërbimi AI nuk është konfiguruar", + "API Key": "Çelës API", + "API Request Quota (requests/day)": "Kuota e Kërkesave API (kërkesa/ditë)", + "API Token Configuration": "Konfigurimi i Token API", + "API Tokens": "Token API", + "API tokens saved successfully": "Token API u ruajt me sukses", + "AVG / Verwerkingsregister": "AVG / Regjistri i Përpunimit", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: ky raport bashkon çdo aktivitet përpunimi me numërimet e gjurmuara gjatë gjithë jetës. Audituesit dhe Autoriteit Persoonsgegevens e përdorin këtë për shqyrtim mbikëqyrës.", + "About ConfigSets": "Rreth ConfigSets", + "Access denied": "Qasja refuzuar", + "Access organisation dashboard and analytics": "Qasuni në panelin e organizatës dhe analitikën", + "Account": "Llogari", + "Action": "Veprim", + "Action Distribution": "Shpërndarja e Veprimeve", + "Action:": "Veprim:", + "Actions": "Veprime", + "Active": "Aktiv", + "Active Collections": "Koleksionet Aktive", + "Active Filters": "Filtrat Aktiv", + "Active Organisation:": "Organizata Aktive:", + "Active Organisations": "Organizatat Aktive", + "Active filters:": "Filtrat aktiv:", + "Active organisation changed successfully": "Organizata aktive u ndryshua me sukses", + "Active organisation set successfully": "Organizata aktive u vendos me sukses", + "Activities": "Aktivitete", + "Activity": "Aktivitet", + "Activity Period": "Periudha e Aktivitetit", + "Add": "Shto", + "Add Attachment": "Shto Bashkëngjitje", + "Add Endpoint": "Shto Pikëfund", + "Add Groups": "Shto Grupe", + "Add Property to '{title}'": "Shto Pronë te '{title}'", + "Add Register": "Shto Regjistër", + "Add Schema": "Shto Skemë", + "Add Source": "Shto Burim", + "Add User": "Shto Përdorues", + "Add User to Organisation": "Shto Përdorues në Organizatë", + "Add a contact from any of your address books to associate it with this object.": "Shtoni një kontakt nga librat tuaj të adresave për ta lidhur me këtë objekt.", + "Add a file or files": "Shtoni një skedar ose skedarë", + "Add card": "Shto kartë", + "Add contact": "Shto kontakt", + "Add endpoint": "Shto pikëfund", + "Add oneOf entry": "Shto hyrje oneOf", + "Add range": "Shto varg", + "Add schema titles, descriptions, and register information to provide richer context for search": "Shtoni tituj skemash, përshkrime dhe informacion regjistri për të ofruar kontekst më të pasur për kërkim", + "Add to favorites": "Shto te të preferuarat", + "Added": "Shtuar", + "Added to favorites": "Shtuar te të preferuarat", + "Additional Information": "Informacion Shtesë", + "Additional feedback saved. Thank you!": "Reagimet shtesë u ruajtën. Faleminderit!", + "Addresses": "Adresa", + "Admin": "Admin", + "Advanced": "Avancuar", + "Advanced Filters": "Filtra të Avancuar", + "Advanced Options": "Opsione të Avancuara", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Filtrat e avancuar nuk janë të disponueshëm kur përdorni burimin e bazës së të dhënave. Kaloni te Auto ose SOLR Index për opsionet e filtrimit.", + "After": "Pas", + "Agent Type": "Lloji i Agjentit", + "Agent deleted successfully": "Agjenti u fshi me sukses", + "Agent successfully created": "Agjenti u krijua me sukses", + "Agent successfully updated": "Agjenti u përditësua me sukses", + "Agents": "Agjentë", + "Aggregated across schemas": "Agreguar nëpër skema", + "All": "Të gjitha", + "All Categories": "Të gjitha Kategoritë", + "All Collections": "Të gjitha Koleksionet", + "All Confidence Levels": "Të gjitha Nivelet e Besimit", + "All ConfigSets": "Të gjitha ConfigSets", + "All Files": "Të gjitha Skedarët", + "All Levels": "Të gjitha Nivelet", + "All Types": "Të gjitha Llojet", + "All Webhooks": "Të gjitha Webhooks", + "All actions": "Të gjitha veprimet", + "All audit trails cleared successfully": "Të gjitha gjurmët e auditimit u pastruan me sukses", + "All clear": "Gjithçka e qartë", + "All file types": "Të gjitha llojet e skedarëve", + "All registers": "Të gjitha regjistrat", + "All schemas": "Të gjitha skemat", + "All search trails cleared successfully": "Të gjitha gjurmët e kërkimit u pastruan me sukses", + "All searches": "Të gjitha kërkimet", + "All users": "Të gjithë përdoruesit", + "All-day events": "Ngjarjet gjatë gjithë ditës", + "Allowed MIME Types": "Llojet MIME të Lejuara", + "Already linked": "Tashmë i lidhur", + "An error occurred while clearing audit trails": "Ndodhi një gabim gjatë pastrimit të gjurmëve të auditimit", + "An error occurred while deleting the audit trail": "Ndodhi një gabim gjatë fshirjes së gjurmës së auditimit", + "An error occurred while deleting the object": "Ndodhi një gabim gjatë fshirjes së objektit", + "An error occurred while deleting the view": "Ndodhi një gabim gjatë fshirjes së pamjes", + "An error occurred while permanently deleting the objects": "Ndodhi një gabim gjatë fshirjes së përhershme të objekteve", + "An error occurred while restoring the objects": "Ndodhi një gabim gjatë rivendosjes së objekteve", + "An error occurred while saving the organisation": "Ndodhi një gabim gjatë ruajtjes së organizatës", + "Analysis completed:": "Analiza përfundoi:", + "Analysis steps:": "Hapat e analizës:", + "Analytics": "Analitikë", + "Analyze Objects": "Analizoni Objektet", + "Analyze Properties": "Analizoni Pronat", + "Analyze existing properties for improvement opportunities": "Analizoni pronat ekzistuese për mundësi përmirësimi", + "Analyzing...": "Duke analizuar...", + "Anonymized": "Anonimizuar", + "Anonymous Group": "Grup Anonim", + "Any favorites and sharing settings for this view": "Çdo preferencë dhe cilësimet e ndarjes për këtë pamje", + "Any user": "Çdo përdorues", + "App ID": "ID e Aplikacionit", + "App store cache invalidated successfully": "Cache e dyqanit të aplikacioneve u invalidua me sukses", + "App store cache invalidated: {invalidated}": "Cache e dyqanit të aplikacioneve u invalidua: {invalidated}", + "Application deleted successfully": "Aplikacioni u fshi me sukses", + "Application identifier for this configuration (optional)": "Identifikuesi i aplikacionit për këtë konfigurim (opsional)", + "Application settings": "Cilësimet e aplikacionit", + "Application successfully created": "Aplikacioni u krijua me sukses", + "Application successfully updated": "Aplikacioni u përditësua me sukses", + "Applications": "Aplikacione", + "Apply": "Apliko", + "Apply Changes": "Apliko Ndryshimet", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Aplikoni një rol të gjitha skemave në këtë regjistër që nuk kanë anashkalime të autorizimit të qarta.", + "Approval Progress": "Progresi i Miratimit", + "Approve": "Mirato", + "Approved": "Miratuar", + "Archive": "Arkiv", + "Archive conversation": "Arkivoni bisedën", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arkivoni këtë verwerkingsactiviteit? Rreshtat e gjurmës së auditimit do të vazhdojnë t'i referohen.", + "Archived conversations are hidden from your active list": "Bisedat e arkivuara janë fshehura nga lista juaj aktive", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Jeni i sigurt se doni të FSHINI koleksionin \"{name}\"?\n\nKjo do:\n• Fshijë përgjithmonë koleksionin dhe të gjitha të dhënat e tij\n• Heqë të gjitha dokumentet e indeksuara\n• Ky veprim nuk mund të kthehet", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Jeni i sigurt se doni të pastroni gjurmët e vjetra të kërkimit? Kjo do të fshijë hyrjet më të vjetra se 30 ditë.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Jeni i sigurt se doni të pastroni të gjitha të dhënat nga koleksioni \"{name}\"?\n\nKjo do:\n• Fshijë të gjitha dokumentet e indeksuara\n• Mbajë strukturën e koleksionit intakte\n• Ky veprim nuk mund të kthehet", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Jeni i sigurt se doni të fshini aplikacionin \"{name}\"? Ky veprim nuk mund të kthehet.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Jeni i sigurt se doni të fshini gjurmët e zgjedhura të auditimit? Ky veprim nuk mund të kthehet.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Jeni i sigurt se doni të fshini gjurmët e zgjedhura të kërkimit? Ky veprim nuk mund të kthehet.", + "Are you sure you want to delete this ConfigSet?": "Jeni i sigurt se doni të fshini këtë ConfigSet?", + "Are you sure you want to delete this configset?": "Jeni i sigurt se doni të fshini këtë configset?", + "Are you sure you want to leave '{name}'?": "Jeni i sigurt se doni të largoheni nga '{name}'?", + "Are you sure you want to permanently delete": "Jeni i sigurt se doni të fshini përgjithmonë", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Jeni i sigurt se doni të riindeksoni koleksionin \"{name}\"?\n\nKjo do:\n• Rindërtojë indeksin me të gjitha objektet\n• Marrë disa minuta për t'u kryer\n• Mund të ndikojë në performancën e kërkimit gjatë riindeksimit", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Jeni i sigurt se doni të hiqni skemën \"{schema}\" nga regjistri \"{register}\"? Ky veprim nuk mund të kthehet.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Jeni i sigurt se doni të fshini butë {count} objekte aktive për skemën \"{schema}\"?\n\nAto do të shënohen si të fshira por mund të hiqen përgjithmonë më vonë.", + "Array Object Configuration:": "Konfigurimi i Objektit Array:", + "Articles": "Artikuj", + "Ask a question...": "Bëni një pyetje...", + "Ask questions about your data using natural language": "Bëni pyetje rreth të dhënave tuaja duke përdorur gjuhën natyrore", + "Assigned collaborative tags": "Etiketa bashkëpunuese të caktuara", + "Assignment notifications": "Njoftimet e caktimit", + "Attachment added to {name}": "Bashkëngjitja u shtua te {name}", + "Attempt": "Tentativë", + "Audit Trail Changes": "Ndryshimet e Gjurmës së Auditimit", + "Audit Trail Details": "Detajet e Gjurmës së Auditimit", + "Audit Trail Management": "Menaxhimi i Gjurmës së Auditimit", + "Audit Trail Statistics": "Statistikat e Gjurmës së Auditimit", + "Audit Trails": "Gjurmët e Auditimit", + "Audit trail data copied to clipboard": "Të dhënat e gjurmës së auditimit u kopjuan në clipboard", + "Audit trail deleted successfully": "Gjurma e auditimit u fshi me sukses", + "Audit trail successfully deleted": "Gjurma e auditimit u fshi me sukses", + "Authentication": "Autentifikim", + "Auto (e.g. Y for year, F Y for month)": "Auto (p.sh. Y për vit, F Y për muaj)", + "Auto share": "Ndarje automatike", + "Auto-commit disabled": "Auto-commit çaktivizuar", + "Auto-commit enabled": "Auto-commit aktivizuar", + "Auto-create Default Organisation": "Krijo Automatikisht Organizatën Default", + "Auto-retry failed vectorizations": "Riprovo automatikisht vectorizimet e dështuara", + "Automatically commit changes to SOLR index": "Angazho automatikisht ndryshimet në indeksin SOLR", + "Automatically connect to the federation network.": "Lidhuni automatikisht me rrjetin e federatës.", + "Automatically create a default organisation if none exists when the app is initialized": "Krijoni automatikisht një organizatë default nëse nuk ekziston asnjë kur aplikacioni inicializohet", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Gjeneroni automatikisht embeddings vektoriale nga copa teksti kur skedarët ngarkohen dhe përpunohen", + "Automatically generate vector embeddings when objects are created or updated": "Gjeneroni automatikisht embeddings vektoriale kur objektet krijohen ose përditësohen", + "Automatically retry failed vectorization attempts (max 3 retries)": "Riprovo automatikisht tentativat e dështuara të vectorizimit (maks 3 riprovime)", + "Automatically update catalogues.": "Përditësoni automatikisht kataloget.", + "Available Filters": "Filtrat e Disponueshëm", + "Available Workflows": "Flukset e Punës të Disponueshme", + "Available placeholders:": "Vendëmbajtësit e disponueshëm:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Ndryshimet e avatarit nuk mbështeten nga ofruesi juaj i autentifikimit.", + "Avatar removed": "Avatari u hoq", + "Avatar updated successfully": "Avatari u përditësua me sukses", + "Avg Execution Time": "Koha Mesatare e Ekzekutimit", + "Avg Members/Org": "Anëtarë Mesatarë/Org", + "Avg Object Views/Session": "Pamje Mesatare Objektesh/Sesion", + "Avg Response Time": "Koha Mesatare e Përgjigjes", + "Avg Results/Search": "Rezultate Mesatare/Kërkim", + "Avg Searches/Session": "Kërkime Mesatare/Sesion", + "Back": "Mbrapa", + "Back to Entities": "Kthehu te Entitetet", + "Back to Registers": "Kthehu te Regjistrat", + "Back to Webhooks": "Kthehu te Webhooks", + "Back to applications": "Kthehu te aplikacionet", + "Back to entities": "Kthehu te entitetet", + "Backend parameter is required": "Parametri backend është i nevojshëm", + "Backend updated successfully. Please reload the application.": "Backend u përditësua me sukses. Ju lutemi ringarkoni aplikacionin.", + "Bandwidth Quota (MB/month)": "Kuota e Gjerësisë së Brezit (MB/muaj)", + "Base URL (Optional)": "URL Bazë (Opsional)", + "Basic Information": "Informacion Bazë", + "Batch Size": "Madhësia e Grupit", + "Batch extraction completed": "Nxjerrja në grupe përfundoi", + "Before": "Para", + "Before object vectorization can work:": "Para se vectorizimi i objektit të funksionojë:", + "Behavior": "Sjellje", + "Behavior Issue": "Problem Sjelljeje", + "Beschrijving": "Përshkrim", + "Bewaartermijn": "Periudha e ruajtjes", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Periudha e ruajtjes (kohëzgjatje ISO-8601, p.sh. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Ruajtja blob është tërhequr. Të gjitha objektet tani përdorin tabela magjike.", + "Bookmarks": "Faqerojtës", + "Branch": "Degë", + "Bulk Role Assignment: {title}": "Caktimi i Roleve në Grup: {title}", + "Bulk delete operation completed successfully": "Operacioni i fshirjes në grup u krye me sukses", + "Bulk save operation completed successfully": "Operacioni i ruajtjes në grup u krye me sukses", + "Business Data": "Të dhëna Biznesi", + "CSV": "CSV", + "Cache cleared successfully": "Cache u pastrua me sukses", + "Cache grew from {old} to {new} entries.": "Cache u rrit nga {old} në {new} hyrje.", + "Cache shrunk from {old} to {new} entries.": "Cache u zvogëlua nga {old} në {new} hyrje.", + "Cache stayed the same at {size} entries.": "Cache mbeti i njëjtë me {size} hyrje.", + "Calculate Sizes": "Llogaritni Madhësitë", + "Calendar": "Kalendar", + "Calendar Provider Configuration": "Konfigurimi i Ofruesit të Kalendarit", + "Calendar integration is not available": "Integrimi i kalendarit nuk është i disponueshëm", + "Calendar name": "Emri i kalendarit", + "Cancel": "Anulo", + "Cancel deactivation request": "Anuloni kërkesën e çaktivizimit", + "Cannot delete: objects are still attached": "Nuk mund të fshihet: objektet janë ende të lidhura", + "Cannot edit: This register is managed by external configuration {title}": "Nuk mund të redaktohet: Ky regjistër menaxhohet nga konfigurimi i jashtëm {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nuk mund të hiqet skema me objekte ekzistuese ({count} objekte)", + "Cannot remove schema {schema} because it contains {count} objects": "Nuk mund të hiqet skema {schema} sepse përmban {count} objekte", + "Cards": "Karta", + "Cascade delete": "Fshirje kaskadë", + "Categories of data subjects (one per line)": "Kategoritë e subjekteve të të dhënave (një për rresht)", + "Categories of personal data (one per line)": "Kategoritë e të dhënave personale (një për rresht)", + "Categorieën betrokkenen (één per regel)": "Kategoritë e subjekteve (një për rresht)", + "Categorieën persoonsgegevens (één per regel)": "Kategoritë e të dhënave personale (një për rresht)", + "Category": "Kategori", + "Change Type": "Lloji i Ndryshimit", + "Change password": "Ndrysho fjalëkalimin", + "Changes": "Ndryshime", + "Changes copied to clipboard": "Ndryshimet u kopjuan në clipboard", + "Changes for Audit Trail #{id}": "Ndryshimet për Gjurmën e Auditimit #{id}", + "Chat Model": "Modeli i Chat", + "Chat Provider": "Ofruesi i Chat", + "Chat Provider (RAG)": "Ofruesi i Chat (RAG)", + "Chat Settings": "Cilësimet e Chat", + "Chat provider connection successful!": "Lidhja me ofruesin e chat u krye me sukses!", + "Chat settings": "Cilësimet e chat", + "Chat test failed: {error}": "Testi i chat dështoi: {error}", + "Choose a register": "Zgjidhni një regjistër", + "Choose a schema": "Zgjidhni një skemë", + "Choose how vector similarity calculations are performed for semantic search": "Zgjidhni si kryhen llogaritjet e ngjashmërisë vektoriale për kërkim semantik", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Zgjidhni cilat lloje skedarësh të përfshihen në procesin e vectorizimit. Do të përpunohen vetëm skedarët me tekst të nxjerrë dhe copa.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Zgjidhni cilat pamje të përfshihen në procesin e vectorizimit. Lëreni bosh për të përpunuar të gjitha pamjet bazuar në konfigurimin tuaj.", + "Chunk": "Copë", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Fshirja e copave nuk është zbatuar ende. Përdorni pikëfundt të bazuara në copa.", + "Chunks": "Copa", + "Chunks to Vectorize:": "Copa për Vectorizim:", + "Cleanup Old Trails": "Pastro Gjurmët e Vjetra", + "Cleanup completed": "Pastrimi përfundoi", + "Cleanup completed successfully. Deleted {count} entries.": "Pastrimi u krye me sukses. U fshinë {count} hyrje.", + "Cleanup failed: {error}": "Pastrimi dështoi: {error}", + "Clear All Embeddings": "Pastro Të gjitha Embeddings", + "Clear Entries": "Pastro Hyrjet", + "Clear Filtered Audit Trails": "Pastro Gjurmët e Filtruara të Auditimit", + "Clear Filters": "Pastro Filtrat", + "Clear Index": "Pastro Indeksin", + "Clear Selection": "Pastro Zgjedhjen", + "Clear all": "Pastro të gjitha", + "Clear all filters": "Pastro të gjithë filtrat", + "Clear date range": "Pastro intervalin e datave", + "Clear filters": "Pastro filtrat", + "Clearing...": "Duke pastruar...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Klikoni Run compliance scan për të gjetur skemat ku PII është zbuluar por nuk ekziston asnjë shënim i aktivitetit të përpunimit.", + "Click to subscribe to notifications": "Klikoni për t'u pajtuar në njoftimet", + "Click to unsubscribe from notifications": "Klikoni për t'u çpajtuar nga njoftimet", + "Close": "Mbyll", + "Cluster name": "Emri i klusterit", + "Code": "Kod", + "Code (short readable key, e.g. v-2026-001)": "Kod (çelës i shkurtër i lexueshëm, p.sh. v-2026-001)", + "Collapse details": "Mbyllni detajet", + "Collection Management": "Menaxhimi i Koleksionit", + "Collection Name": "Emri i Koleksionit", + "Collection assignments updated successfully": "Caktimet e koleksionit u përditësuan me sukses", + "Collection cleared successfully": "Koleksioni u pastrua me sukses", + "Collection copied successfully": "Koleksioni u kopjua me sukses", + "Collection created successfully": "Koleksioni u krijua me sukses", + "Collection deleted successfully": "Koleksioni u fshi me sukses", + "Collection used to store and index file metadata and content": "Koleksioni i përdorur për të ruajtur dhe indeksuar meta të dhënat dhe përmbajtjen e skedarëve", + "Collection used to store and index object data": "Koleksioni i përdorur për të ruajtur dhe indeksuar të dhënat e objekteve", + "Collections:": "Koleksionet:", + "Color": "Ngjyrë", + "Comment...": "Koment...", + "Commit Message": "Mesazhi i Commit", + "Commit Within (ms)": "Commit Brenda (ms)", + "Commit message": "Mesazhi i commit", + "Compare current schema with real object data": "Krahasoni skemën aktuale me të dhënat reale të objektit", + "Completed": "Përfunduar", + "Complex": "Kompleks", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Pyetje komplekse: Kërkime të avancuara me filtra të shumtë, operatorë dhe kombinime parametrash komplekse", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Pyetje komplekse: kërkime të avancuara me filtra të shumtë, operatorë dhe kombinime parametrash komplekse", + "Compliance": "Pajtueshmëri", + "Confidence Level": "Niveli i Besimit", + "Confidence Score": "Rezultati i Besimit", + "Confidence: {confidence}%": "Besimi: {confidence}%", + "Config must be provided as an object": "Konfigurimi duhet të jepet si objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Menaxhimi i ConfigSet", + "ConfigSet Name": "Emri i ConfigSet", + "ConfigSet created successfully": "ConfigSet u krijua me sukses", + "ConfigSet deleted successfully": "ConfigSet u fshi me sukses", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets përcaktojnë skemën dhe konfigurimin për koleksionet tuaja SOLR. Ato përmbajnë përkufizime fushash, analizues dhe cilësime të tjera kërkimi.", + "Configuration": "Konfigurim", + "Configuration Data": "Të dhënat e Konfigurimit", + "Configuration is up to date": "Konfigurimi është i përditësuar", + "Configuration saved": "Konfigurimi u ruajt", + "Configuration saved successfully.": "Konfigurimi u ruajt me sukses.", + "Configuration type (default, application, etc.)": "Lloji i konfigurimit (default, aplikacion, etj.)", + "Configurations": "Konfigurime", + "Configure API tokens for external service integrations": "Konfiguroni token API për integrimet e shërbimeve të jashtme", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfiguroni motorin e kërkimit Apache SOLR për aftësi kërkimi të avancuara", + "Configure Facets": "Konfiguroni Facets", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfiguroni ofruesit e Modelit të Gjuhës së Madhe (LLM) për veçoritë e drejtuar nga AI duke përfshirë kërkim semantik, embeddings dhe chat.", + "Configure SOLR Facets": "Konfiguroni SOLR Facets", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfiguroni cilësimet bazë të lidhjes për serverin tuaj SOLR duke përfshirë autentifikimin dhe opsionet e rrjetit. Përdorni dialogët e veçantë të Menaxhimit të ConfigSet dhe Koleksionit për të menaxhuar bërthamat dhe koleksionet.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfiguroni cilësimet bazë të lidhjes për serverin tuaj SOLR duke përfshirë autentifikimin dhe opsionet e rrjetit. Përdorni dialogët e veçantë të menaxhimit të configset dhe koleksionit për të menaxhuar bërthamat dhe koleksionet.", + "Configure default organisation and organisation-related settings": "Konfiguroni organizatën default dhe cilësimet e lidhura me organizatën", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfiguroni si konvertohen objektet e bazës së të dhënave në embeddings vektoriale për kërkim semantik. Objektet vectorizohen drejtpërdrejt pa nevojën e nxjerrjes së tekstit.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfiguroni si konvertohen objektet në tekst para vectorizimit. Këto cilësime ndikojnë në cilësinë e kërkimit dhe kontekstin.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfiguroni ofruesit e modelit të gjuhës së madhe (LLM) për veçoritë e drejtuar nga AI duke përfshirë kërkim semantik, embeddings dhe chat.", + "Configure multi-organization support and tenant isolation": "Konfiguroni mbështetjen multi-organizatë dhe izolimin e qiramarrësit", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfiguroni parametrat për vectorizimin e skedarëve. Ky proces do të gjenerojë embeddings vektoriale për të gjitha copat e nxjerra të skedarëve.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfiguroni parametrat për vectorizimin e objekteve. Ky proces do të gjenerojë embeddings vektoriale për të gjitha objektet që përputhen me filtrat e pamjes tuaj.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfiguroni këtë skemë për të shfaqur objektet si ngjarje në aplikacionin Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Konfiguroni cilat lloje të dhënash të kërkohen dhe sa burime të merren", + "Configuring field mappings...": "Duke konfiguruar hartat e fushave...", + "Confirm Account Deactivation": "Konfirmoni Çaktivizimin e Llogarisë", + "Confirm Object Deletion": "Konfirmoni Fshirjen e Objektit", + "Confirm Object Restoration": "Konfirmoni Rivendosjen e Objektit", + "Confirm Permanent Object Deletion": "Konfirmoni Fshirjen e Përhershme të Objektit", + "Confirm deactivation": "Konfirmoni çaktivizimin", + "Connecting to SOLR cluster...": "Duke u lidhur me klusterin SOLR...", + "Connection Failed": "Lidhja Dështoi", + "Connection Settings": "Cilësimet e Lidhjes", + "Connection Successful!": "Lidhja U Krye Me Sukses!", + "Connection failed": "Lidhja dështoi", + "Connection protocol": "Protokolli i lidhjes", + "Connection successful": "Lidhja u krye me sukses", + "Connection timeout in seconds": "Koha e skadimit të lidhjes në sekonda", + "Constant delay between retries (5 minutes)": "Vonesë konstante midis riprovimeve (5 minuta)", + "Constraint Issue": "Problem Kufizimi", + "Consumer not found": "Konsumatori nuk u gjet", + "Contacts": "Kontakte", + "Contacts integration is not available": "Integrimi i kontakteve nuk është i disponueshëm", + "Content Filters": "Filtrat e Përmbajtjes", + "Continue to Properties": "Vazhdoni te Pronat", + "Contracts": "Kontrata", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontrolloni cilat pamje objektesh duhet vectorizuar për të ulur kostot API. Vectorizoni vetëm pamjet që përfitojnë nga kërkimi semantik.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontrolloni cilat pamje dhe mjete mund të përdorë AI në këtë bisedë. Si parazgjedhje, të gjitha aftësitë e agjentit janë aktivizuara.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontrollon rastësinë (0 = deterministe, 2 = shumë krijuese)", + "Conversation ID is required": "ID e bisedës është e nevojshme", + "Conversation archived": "Biseda u arkivua", + "Conversation archived successfully": "Biseda u arkivua me sukses", + "Conversation cleared successfully": "Biseda u pastrua me sukses", + "Conversation deleted": "Biseda u fshi", + "Conversation not found": "Biseda nuk u gjet", + "Conversation permanently deleted": "Biseda u fshi përgjithmonë", + "Conversation renamed": "Biseda u riemërtua", + "Conversation restored": "Biseda u rivendos", + "Conversation started with {agent}": "Biseda filloi me {agent}", + "Conversation title": "Titulli i bisedës", + "Conversations": "Biseda", + "Converting {schema} to blob storage...": "Duke konvertuar {schema} në ruajtje blob...", + "Converting {schema} to magic table...": "Duke konvertuar {schema} në tabelë magjike...", + "Copied!": "Kopjuar!", + "Copy": "Kopjo", + "Copy Changes": "Kopjo Ndryshimet", + "Copy Collection": "Kopjo Koleksionin", + "Copy Data": "Kopjo Të dhënat", + "Copy Full Data": "Kopjo Të dhënat e Plota", + "Copy of {name}": "Kopje e {name}", + "Copy to clipboard": "Kopjo në clipboard", + "Copying...": "Duke kopjuar...", + "Costs": "Kosto", + "Count": "Numërim", + "Counting objects...": "Duke numëruar objektet...", + "Create": "Krijo", + "Create API Token": "Krijo Token API", + "Create Agent": "Krijo Agjent", + "Create Application": "Krijo Aplikacion", + "Create Collection": "Krijo Koleksion", + "Create ConfigSet": "Krijo ConfigSet", + "Create New Collection": "Krijo Koleksion të Ri", + "Create New ConfigSet": "Krijo ConfigSet të Ri", + "Create Operations": "Operacionet e Krijimit", + "Create Organisation": "Krijo Organizatë", + "Create Webhook": "Krijo Webhook", + "Create a copy of collection:": "Krijo një kopje të koleksionit:", + "Create a new ConfigSet based on the _default template": "Krijo një ConfigSet të ri bazuar në shablonin _default", + "Create a new SOLR collection from an existing ConfigSet": "Krijo një koleksion SOLR të ri nga një ConfigSet ekzistues", + "Create a new SOLR collection from an existing configset": "Krijo një koleksion SOLR të ri nga një configset ekzistues", + "Create a new configset based on the _default template": "Krijo një configset të ri bazuar në shablonin _default", + "Create a new event or link an existing one from any of your calendars.": "Krijoni një ngjarje të re ose lidhni një ekzistuese nga çdonjëri nga kalendarët tuaj.", + "Create another": "Krijo tjetër", + "Create copies of": "Krijo kopje të", + "Create event": "Krijo ngjarje", + "Create new token": "Krijo token të ri", + "Create or link a Deck card to track work on this object.": "Krijoni ose lidhni një kartë Deck për të gjurmuar punën në këtë objekt.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Krijoni verwerkingsactiviteit të parë për të filluar etiketimin e rreshtave të gjurmës së auditimit me atribuimin e tyre AVG Art 30.", + "Create your first AI agent to get started.": "Krijoni agjentin tuaj të parë AI për të filluar.", + "Created": "Krijuar", + "Created:": "Krijuar:", + "Creating search collections...": "Duke krijuar koleksionet e kërkimit...", + "Creating...": "Duke krijuar...", + "Current": "Aktual", + "Current Filters": "Filtrat Aktualë", + "Current Type:": "Lloji Aktual:", + "Current password": "Fjalëkalimi aktual", + "Custom API endpoint if using a different region": "Pikëfund API i personalizuar nëse përdorni një rajon tjetër", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Titujt HTTP të personalizuar (një për rresht, format: Emri-Titullit: vlerë)", + "Custom HTTP headers (one per line, format: header-name: value)": "Titujt HTTP të personalizuar (një për rresht, format: emri-titullit: vlerë)", + "Custom Ranges:": "Vargjet e Personalizuara:", + "Custom display title for this facet": "Titulli i shfaqjes i personalizuar për këtë facet", + "Custom pattern:": "Model i personalizuar:", + "DELETE COLLECTION": "FSHI KOLEKSIONIN", + "DSAR": "DSAR", + "Daily": "Çdo ditë", + "Dashboard": "Panel", + "Dashboard not found": "Paneli nuk u gjet", + "Data": "Të dhëna", + "Data Source": "Burimi i Të dhënave", + "Data Sources": "Burimet e Të dhënave", + "Data Views": "Pamjet e Të dhënave", + "Data sources": "Burimet e të dhënave", + "Data type does not match observed values": "Lloji i të dhënave nuk përputhet me vlerat e vëzhguara", + "Data-subject access request": "Kërkesë qasje e subjektit të të dhënave", + "Database URL": "URL e Bazës së Të dhënave", + "Database information refreshed": "Informacioni i bazës së të dhënave u rifreskua", + "Date Faceting:": "Faceting i Datës:", + "Date Range": "Intervali i Datave", + "Dates": "Data", + "Deactivation request cancelled": "Kërkesa e çaktivizimit u anulua", + "Deactivation request submitted": "Kërkesa e çaktivizimit u dorëzua", + "Deck": "Deck", + "Deck integration is not available": "Integrimi Deck nuk është i disponueshëm", + "Default": "Parazgjedhje", + "Default Object Tenant": "Qiramarrësi Default i Objektit", + "Default Organisation": "Organizata Default", + "Default Owner": "Pronari Default", + "Default User (for cron/background jobs)": "Përdoruesi Default (për punët cron/sfondo)", + "Default User Tenant": "Qiramarrësi Default i Përdoruesit", + "Default value": "Vlera default", + "Default value (Email)": "Vlera default (Email)", + "Default value (Password)": "Vlera default (Fjalëkalim)", + "Default value (Phone number)": "Vlera default (Numër telefoni)", + "Default value (Regex)": "Vlera default (Regex)", + "Default: 5": "Parazgjedhje: 5", + "Defaults to current user. Select a different user if needed.": "Parazgjedhur te përdoruesi aktual. Zgjidhni një përdorues tjetër nëse nevojitet.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Vonesat dyfishohen me çdo tentativë (2, 4, 8 minuta...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Vonesat rriten linearkisht (5, 10, 15 minuta...)", + "Delete": "Fshi", + "Delete ({count})": "Fshi ({count})", + "Delete Application": "Fshi Aplikacionin", + "Delete Audit Trail": "Fshi Gjurmën e Auditimit", + "Delete Collection": "Fshi Koleksionin", + "Delete ConfigSet": "Fshi ConfigSet", + "Delete Objects": "Fshi Objektet", + "Delete Operations": "Operacionet e Fshirjes", + "Delete SOLR Collection": "Fshi Koleksionin SOLR", + "Delete View": "Fshi Pamjen", + "Delete all objects in this schema": "Fshi të gjitha objektet në këtë skemë", + "Delete permanently": "Fshi përgjithmonë", + "Delete this conversation?": "Fshini këtë bisedë?", + "Delete view": "Fshi pamjen", + "Delete {count} object": "Fshi {count} objekt", + "Delete {count} objects": "Fshi {count} objekte", + "Deleted": "Fshirë", + "Deleted By": "Fshirë Nga", + "Deleted Date": "Data e Fshirjes", + "Deleted Items Management": "Menaxhimi i Artikujve të Fshirë", + "Deleted This Week": "Fshirë Këtë Javë", + "Deleted Today": "Fshirë Sot", + "Deleted:": "Fshirë:", + "Deleting SOLR Collection...": "Duke fshirë Koleksionin SOLR...", + "Deleting...": "Duke fshirë...", + "Deletion Date Range": "Intervali i Datave të Fshirjes", + "Deletion Statistics": "Statistikat e Fshirjes", + "Deprecated": "I vjetruar", + "Depublish": "Çpubliko", + "Description": "Përshkrim", + "Description Template": "Shablloni i Përshkrimit", + "Description shown as tooltip": "Përshkrimi i shfaqur si tooltip", + "Description:": "Përshkrim:", + "Detect data types and patterns": "Zbuloni llojet e të dhënave dhe modelet", + "Detected At": "Zbuluar Në", + "Detected Format:": "Formati i Zbuluar:", + "Detected Issues:": "Problemet e Zbuluara:", + "Detected: {date}": "Zbuluar: {date}", + "Direct permission": "Leje direkte", + "Disable": "Çaktivizo", + "Disable this to make the agent": "Çaktivizoni këtë për ta bërë agjentin", + "Disabled": "Çaktivizuar", + "Discover Files": "Zbuloni Skedarët", + "Discovered Properties": "Pronat e Zbuluara", + "Display Format": "Formati i Shfaqjes", + "Display Name": "Emri i Shfaqjes", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Doni të fshini përgjithmonë të gjitha hyrjet e filtruara të gjurmës së auditimit? Ky veprim nuk mund të kthehet.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Doni të fshini përgjithmonë këtë hyrje të gjurmës së auditimit? Ky veprim nuk mund të kthehet.", + "Documents": "Dokumente", + "Doelbinding *": "Qëllimi i lidhjes *", + "Dolphin OCR enabled": "Dolphin OCR aktivizuar", + "Download API Spec": "Shkarko Spec API", + "Download API Specification": "Shkarko Specifikimin API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Shkarkoni një kopje të të gjitha të dhënave tuaja personale të ruajtura në OpenRegister (GDPR Neni 20).", + "Drag a file or files here": "Tërhiqni një skedar ose skedarë këtu", + "Dry run completed: {count} fields would be created": "Simulimi u krye: {count} fusha do të krijoheshin", + "Dry run completed: {count} fields would be fixed": "Simulimi u krye: {count} fusha do të korrigjoheshin", + "Duration in seconds (optional)": "Kohëzgjatja në sekonda (opsional)", + "Edit": "Ndrysho", + "Edit Agent": "Ndrysho Agjentin", + "Edit Application": "Ndrysho Aplikacionin", + "Edit Configuration": "Ndrysho Konfigurimin", + "Edit Endpoint": "Ndrysho Pikëfundin", + "Edit Organisation": "Ndrysho Organizatën", + "Edit Property '{key}' of '{title}'": "Ndrysho Pronën '{key}' të '{title}'", + "Edit Register": "Ndrysho Regjistrin", + "Edit Schema": "Ndrysho Skemën", + "Edit Source": "Ndrysho Burimin", + "Edit View": "Ndrysho Pamjen", + "Edit Webhook": "Ndrysho Webhook", + "Edit labels": "Ndrysho etiketat", + "Edit processing activity": "Ndrysho aktivitetin e përpunimit", + "Edit tags for {name}": "Ndrysho etiketat për {name}", + "Edit verwerkingsactiviteit": "Ndrysho verwerkingsactiviteit", + "Edit view": "Ndrysho pamjen", + "Edit view details": "Ndrysho detajet e pamjes", + "Effectiveness": "Efektivitet", + "Email": "Email", + "Email Addresses": "Adresat Email", + "Email digest frequency": "Frekuenca e përmblodhjes email", + "Emails": "Email", + "Embedding Model": "Modeli i Embedding", + "Embedding Provider": "Ofruesi i Embedding", + "Embedding generated successfully": "Embedding u gjenerua me sukses", + "Embedding provider connection successful!": "Lidhja me ofruesin e embedding u krye me sukses!", + "Embedding test failed: {error}": "Testi i embedding dështoi: {error}", + "Enable": "Aktivizo", + "Enable RAG": "Aktivizo RAG", + "Enable auto-creation": "Aktivizo krijimin automatik", + "Enable automatic file vectorization": "Aktivizo vectorizimin automatik të skedarëve", + "Enable automatic object vectorization": "Aktivizo vectorizimin automatik të objekteve", + "Enable automatic synchronization": "Aktivizo sinkronizimin automatik", + "Enable calendar provider": "Aktivizo ofruesin e kalendarit", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Aktivizo regjistrimin e detajuar për operacionet SOLR (rekomandohet për debugging)", + "Enable faceting": "Aktivizo faceting", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Aktivizoni ose çaktivizoni veçoritë LLM. Konfiguroni ofruesit dhe modelet duke përdorur butonin e Konfigurimit LLM më sipër.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Aktivizoni ose çaktivizoni veçoritë LLM. Konfiguroni ofruesit dhe modelet duke përdorur butonin e konfigurimit LLM më sipër.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Aktivizoni ose çaktivizoni integrimin e kërkimit SOLR. Konfiguroni cilësimet e lidhjes duke përdorur butonin e Cilësimeve të Lidhjes më sipër.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Aktivizoni ose çaktivizoni integrimin e kërkimit SOLR. Konfiguroni cilësimet e lidhjes duke përdorur butonin e cilësimeve të lidhjes më sipër.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Aktivizoni ose çaktivizoni integrimin e fluksit të punës n8n. Konfiguroni cilësimet e lidhjes më poshtë.", + "Enable or disable this webhook": "Aktivizoni ose çaktivizoni këtë webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Aktivizoni mjetet që lejojnë agjentin të ndërveprojë me të dhënat përmes thirrjes së funksioneve.", + "Enable vectorization for all existing and future views (may increase costs)": "Aktivizo vectorizimin për të gjitha pamjet ekzistuese dhe të ardhshme (mund të rrisë kostot)", + "Enable write-back to target objects": "Aktivizo shkrimit kthyes te objektet e synuara", + "Enabled": "Aktivizuar", + "End Date Field": "Fusha e Datës së Mbarimit", + "Endpoint Path*": "Shtegu i Pikëfundit*", + "Endpoints": "Pikëfundte", + "Engine not found": "Motori nuk u gjet", + "Enter ConfigSet name": "Futni emrin e ConfigSet", + "Enter Nextcloud usernames to grant access to this private agent": "Futni emrat e përdoruesve Nextcloud për të dhënë qasje në këtë agjent privat", + "Enter agent description (optional)": "Futni përshkrimin e agjentit (opsional)", + "Enter agent name": "Futni emrin e agjentit", + "Enter application description (optional)": "Futni përshkrimin e aplikacionit (opsional)", + "Enter application name": "Futni emrin e aplikacionit", + "Enter collection name": "Futni emrin e koleksionit", + "Enter configuration description (optional)": "Futni përshkrimin e konfigurimit (opsional)", + "Enter configuration title": "Futni titullin e konfigurimit", + "Enter description (optional)...": "Futni përshkrimin (opsional)...", + "Enter new collection name": "Futni emrin e ri të koleksionit", + "Enter object ID": "Futni ID e objektit", + "Enter organisation description (optional)": "Futni përshkrimin e organizatës (opsional)", + "Enter organisation name": "Futni emrin e organizatës", + "Enter search term": "Futni termin e kërkimit", + "Enter search terms or leave empty to browse all": "Futni termat e kërkimit ose lëreni bosh për të shfletuar të gjitha", + "Enter system prompt for the agent": "Futni urdhërin e sistemit për agjentin", + "Enter username and press Enter": "Futni emrin e përdoruesit dhe shtypni Enter", + "Enter view name...": "Futni emrin e pamjes...", + "Enter webhook name": "Futni emrin e webhook", + "Enter your API key": "Futni çelësin tuaj API", + "Enter your object here...": "Futni objektin tuaj këtu...", + "Enter your schema here...": "Futni skemën tuaj këtu...", + "Entities": "Entitete", + "Entities detected": "Entitete të zbuluara", + "Entity ID: {id}": "ID e Entitetit: {id}", + "Entity Information": "Informacion Entiteti", + "Entity Recognition": "Njohja e Entitetit", + "Entity Recognition Method": "Metoda e Njohjes së Entitetit", + "Entity deleted successfully": "Entiteti u fshi me sukses", + "Entity not found": "Entiteti nuk u gjet", + "Entries to be deleted:": "Hyrjet që do të fshihen:", + "Enum Issue": "Problem Enum", + "Enum constraint is missing": "Kufizimi enum mungon", + "Erase (Art 17)": "Fshini (Neni 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Fshini {count} objekt(e) për këtë subjekt? Ky veprim regjistrohet në gjurmën e auditimit.", + "Erasure complete": "Fshirja u krye", + "Erasure preview": "Parapamja e fshirjes", + "Error": "Gabim", + "Error Details": "Detajet e Gabimit", + "Error Information": "Informacioni i Gabimit", + "Error Message": "Mesazhi i Gabimit", + "Error deleting audit trails: {error}": "Gabim gjatë fshirjes së gjurmëve të auditimit: {error}", + "Error deleting search trails: {error}": "Gabim gjatë fshirjes së gjurmëve të kërkimit: {error}", + "Error loading application": "Gabim gjatë ngarkimit të aplikacionit", + "Error loading audit trails": "Gabim gjatë ngarkimit të gjurmëve të auditimit", + "Error loading entity": "Gabim gjatë ngarkimit të entitetit", + "Error loading search trails": "Gabim gjatë ngarkimit të gjurmëve të kërkimit", + "Estimated Batches:": "Grupet e Vlerësuara:", + "Estimated Cost:": "Kostoja e Vlerësuar:", + "Estimated Duration:": "Kohëzgjatja e Vlerësuar:", + "Event": "Ngjarje", + "Event Property for Payload": "Prona e Ngjarjes për Payload", + "Events": "Ngjarje", + "Every schema with detected PII has a processing-activity annotation.": "Çdo skemë me PII të zbuluar ka një shënim të aktivitetit të përpunimit.", + "Example": "Shembull", + "Example Value": "Vlera Shembull", + "Example value for this property": "Vlera shembull për këtë pronë", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Maksimumi Eksklusiv", + "Exclusive Minimum": "Minimumi Eksklusiv", + "Exclusive maximum": "Maksimumi eksklusiv", + "Exclusive minimum": "Minimumi eksklusiv", + "Execution Mode": "Modaliteti i Ekzekutimit", + "Execution Time": "Koha e Ekzekutimit", + "Execution Time Range": "Intervali i Kohës së Ekzekutimit", + "Existing Improvements": "Përmirësimet Ekzistuese", + "Expand details": "Zgjeroni detajet", + "Expiration": "Skadim", + "Expires": "Skadon", + "Expires in (e.g., 90d)": "Skadon pas (p.sh., 90d)", + "Exponential": "Eksponencial", + "Export": "Eksporto", + "Export \"{schema}\" objects from \"{register}\"": "Eksporto objektet \"{schema}\" nga \"{register}\"", + "Export Configuration": "Eksporto Konfigurimin", + "Export Format:": "Formati i Eksportit:", + "Export Objects": "Eksporto Objektet", + "Export completed successfully": "Eksporti u krye me sukses", + "Export downloaded successfully": "Eksporti u shkarkua me sukses", + "Export failed: {error}": "Eksporti dështoi: {error}", + "Export is rate limited. Please try again later.": "Eksporti ka kufizim shkalle. Ju lutemi provoni përsëri më vonë.", + "Export my data": "Eksporto të dhënat e mia", + "Export, view, or delete audit trails": "Eksportoni, shikoni ose fshini gjurmët e auditimit", + "Exporting...": "Duke eksportuar...", + "Extend Schema": "Zgjeroni Skemën", + "Extended": "Zgjeruar", + "Extensions": "Zgjerime", + "Extra Query Parameters": "Parametra Shtesë Pyetjesh", + "Extract Now": "Nxirr Tani", + "Extract Pending Files": "Nxirr Skedarët në Pritje", + "Extract now": "Nxirr tani", + "Extract properties from each object": "Nxirrni pronat nga çdo objekt", + "Extracted At": "Nxjerrë Në", + "Extracted at": "Nxjerrë në", + "Extraction": "Nxjerrje", + "Extraction Mode": "Modaliteti i Nxjerrjes", + "Extraction Scope": "Shtrirja e Nxjerrjes", + "Extraction Status": "Statusi i Nxjerrjes", + "Extraction error for {file}: {error}": "Gabim nxjerrjeje për {file}: {error}", + "Facet Description": "Përshkrimi i Facet", + "Facet Order": "Rendi i Facet", + "Facet Title": "Titulli i Facet", + "Facet Type": "Lloji i Facet", + "Facet configuration updated successfully": "Konfigurimi i facet u përditësua me sukses", + "Facetable": "Me facet", + "Faceting Configuration:": "Konfigurimi i Faceting:", + "Facets discovered and configured successfully": "Facets u zbuluan dhe konfiguruan me sukses", + "Facets discovered successfully": "Facets u zbuluan me sukses", + "Failed": "Dështoi", + "Failed to add attachment to object": "Dështoi shtimi i bashkëngjitjes te objekti", + "Failed to add user to organisation": "Dështoi shtimi i përdoruesit në organizatë", + "Failed to analyze schema properties": "Dështoi analiza e pronave të skemës", + "Failed to archive conversation": "Dështoi arkivimi i bisedës", + "Failed to calculate sizes": "Dështoi llogaritja e madhësive", + "Failed to cancel deactivation": "Dështoi anulimi i çaktivizimit", + "Failed to change active organisation: {error}": "Dështoi ndryshimi i organizatës aktive: {error}", + "Failed to change password": "Dështoi ndryshimi i fjalëkalimit", + "Failed to check version: {error}": "Dështoi kontrolli i versionit: {error}", + "Failed to clear audit trails: {error}": "Dështoi pastrimi i gjurmëve të auditimit: {error}", + "Failed to clear blob objects: {error}": "Dështoi pastrimi i objekteve blob: {error}", + "Failed to clear cache: {error}": "Dështoi pastrimi i cache: {error}", + "Failed to clear collection": "Dështoi pastrimi i koleksionit", + "Failed to clear conversation": "Dështoi pastrimi i bisedës", + "Failed to clear embeddings: {error}": "Dështoi pastrimi i embeddings: {error}", + "Failed to clear search trails: {error}": "Dështoi pastrimi i gjurmëve të kërkimit: {error}", + "Failed to clear {type} cache: {error}": "Dështoi pastrimi i cache {type}: {error}", + "Failed to copy changes": "Dështoi kopjimi i ndryshimeve", + "Failed to copy data": "Dështoi kopjimi i të dhënave", + "Failed to copy data to clipboard": "Dështoi kopjimi i të dhënave në clipboard", + "Failed to copy to clipboard": "Dështoi kopjimi në clipboard", + "Failed to copy token": "Dështoi kopjimi i token", + "Failed to create conversation": "Dështoi krijimi i bisedës", + "Failed to create missing SOLR fields: {error}": "Dështoi krijimi i fushave SOLR që mungonin: {error}", + "Failed to create or find project": "Dështoi krijimi ose gjetja e projektit", + "Failed to create token": "Dështoi krijimi i token", + "Failed to delete SOLR collection: {error}": "Dështoi fshirja e koleksionit SOLR: {error}", + "Failed to delete collection": "Dështoi fshirja e koleksionit", + "Failed to delete conversation": "Dështoi fshirja e bisedës", + "Failed to delete objects for {schema}: {error}": "Dështoi fshirja e objekteve për {schema}: {error}", + "Failed to delete webhook": "Dështoi fshirja e webhook", + "Failed to depublish register: {error}": "Dështoi çpublikimi i regjistrit: {error}", + "Failed to depublish schema: {error}": "Dështoi çpublikimi i skemës: {error}", + "Failed to depublish: {error}": "Dështoi çpublikimi: {error}", + "Failed to discover files: {error}": "Dështoi zbulimi i skedarëve: {error}", + "Failed to download API specification": "Dështoi shkarkimi i specifikimit API", + "Failed to export data": "Dështoi eksporti i të dhënave", + "Failed to export objects": "Dështoi eksporti i objekteve", + "Failed to fetch conversation history": "Dështoi marrja e historikut të bisedës", + "Failed to fix mismatched SOLR fields: {error}": "Dështoi korrigjimi i fushave SOLR të papërputhshme: {error}", + "Failed to get SOLR field configuration": "Dështoi marrja e konfigurimit të fushave SOLR", + "Failed to get chat statistics": "Dështoi marrja e statistikave të chat", + "Failed to get database information: %s": "Dështoi marrja e informacionit të bazës së të dhënave: %s", + "Failed to get entity categories": "Dështoi marrja e kategorive të entitetit", + "Failed to get entity statistics": "Dështoi marrja e statistikave të entitetit", + "Failed to get entity types": "Dështoi marrja e llojeve të entitetit", + "Failed to get response: {error}": "Dështoi marrja e përgjigjes: {error}", + "Failed to invalidate app store cache: {error}": "Dështoi invalidimi i cache të dyqanit të aplikacioneve: {error}", + "Failed to leave organisation: {error}": "Dështoi largimi nga organizata: {error}", + "Failed to link object": "Dështoi lidhja e objektit", + "Failed to load Deck cards": "Dështoi ngarkimi i kartave Deck", + "Failed to load LLM configuration": "Dështoi ngarkimi i konfigurimit LLM", + "Failed to load Nextcloud groups": "Dështoi ngarkimi i grupeve Nextcloud", + "Failed to load cache statistics: {error}": "Dështoi ngarkimi i statistikave të cache: {error}", + "Failed to load conversation": "Dështoi ngarkimi i bisedës", + "Failed to load entities": "Dështoi ngarkimi i entiteteve", + "Failed to load entity": "Dështoi ngarkimi i entitetit", + "Failed to load extraction data": "Dështoi ngarkimi i të dhënave të nxjerrjes", + "Failed to load files": "Dështoi ngarkimi i skedarëve", + "Failed to load linked contacts": "Dështoi ngarkimi i kontakteve të lidhura", + "Failed to load linked emails": "Dështoi ngarkimi i emaileve të lidhura", + "Failed to load linked events": "Dështoi ngarkimi i ngjarjeve të lidhura", + "Failed to load organisations": "Dështoi ngarkimi i organizatave", + "Failed to load register data": "Dështoi ngarkimi i të dhënave të regjistrit", + "Failed to load related entities": "Dështoi ngarkimi i entiteteve të lidhura", + "Failed to load settings: {error}": "Dështoi ngarkimi i cilësimeve: {error}", + "Failed to load statistics: {error}": "Dështoi ngarkimi i statistikave: {error}", + "Failed to load templates": "Dështoi ngarkimi i shablloneve", + "Failed to load view: {error}": "Dështoi ngarkimi i pamjes: {error}", + "Failed to load webhooks": "Dështoi ngarkimi i webhooks", + "Failed to load workflows": "Dështoi ngarkimi i flukseve të punës", + "Failed to process message": "Dështoi përpunimi i mesazhit", + "Failed to publish register: {error}": "Dështoi publikimi i regjistrit: {error}", + "Failed to publish schema: {error}": "Dështoi publikimi i skemës: {error}", + "Failed to publish: {error}": "Dështoi publikimi: {error}", + "Failed to refresh database information": "Dështoi rifreskimi i informacionit të bazës së të dhënave", + "Failed to reindex collection": "Dështoi riindeksimi i koleksionit", + "Failed to remove avatar": "Dështoi heqja e avatarit", + "Failed to remove link": "Dështoi heqja e lidhjes", + "Failed to remove member: {error}": "Dështoi heqja e anëtarit: {error}", + "Failed to remove schema {schema}: {error}": "Dështoi heqja e skemës {schema}: {error}", + "Failed to remove user from organisation": "Dështoi heqja e përdoruesit nga organizata", + "Failed to rename conversation": "Dështoi riemërtimi i bisedës", + "Failed to request deactivation": "Dështoi kërkesa e çaktivizimit", + "Failed to restore conversation": "Dështoi rivendosja e bisedës", + "Failed to retrieve extraction status.": "Dështoi marrja e statusit të nxjerrjes.", + "Failed to retrieve objects for file.": "Dështoi marrja e objekteve për skedarin.", + "Failed to retry extraction": "Dështoi riprova e nxjerrjes", + "Failed to retry webhook": "Dështoi riprova e webhook", + "Failed to revoke token": "Dështoi revokimi i token", + "Failed to save GitHub token": "Dështoi ruajtja e token GitHub", + "Failed to save GitLab URL": "Dështoi ruajtja e URL GitLab", + "Failed to save GitLab token": "Dështoi ruajtja e token GitLab", + "Failed to save LLM settings: {error}": "Dështoi ruajtja e cilësimeve LLM: {error}", + "Failed to save additional feedback": "Dështoi ruajtja e reagimeve shtesë", + "Failed to save configuration.": "Dështoi ruajtja e konfigurimit.", + "Failed to save configuration: {error}": "Dështoi ruajtja e konfigurimit: {error}", + "Failed to save facet configuration": "Dështoi ruajtja e konfigurimit të facet", + "Failed to save feedback": "Dështoi ruajtja e reagimeve", + "Failed to save file settings: {error}": "Dështoi ruajtja e cilësimeve të skedarit: {error}", + "Failed to save n8n configuration": "Dështoi ruajtja e konfigurimit n8n", + "Failed to save preferences": "Dështoi ruajtja e preferencave", + "Failed to save roles": "Dështoi ruajtja e roleve", + "Failed to save settings": "Dështoi ruajtja e cilësimeve", + "Failed to save settings: {error}": "Dështoi ruajtja e cilësimeve: {error}", + "Failed to save view: {error}": "Dështoi ruajtja e pamjes: {error}", + "Failed to save warmup interval: {error}": "Dështoi ruajtja e intervalit të ngrohjes: {error}", + "Failed to save webhook": "Dështoi ruajtja e webhook", + "Failed to send feedback": "Dështoi dërgimi i reagimeve", + "Failed to set active organisation: {error}": "Dështoi vendosja e organizatës aktive: {error}", + "Failed to setup SOLR: {error}": "Dështoi konfigurimi i SOLR: {error}", + "Failed to start file warmup: {error}": "Dështoi nisja e ngrohjes së skedarit: {error}", + "Failed to start processing {type} files: {error}": "Dështoi nisja e përpunimit të skedarëve {type}: {error}", + "Failed to start vectorization: {error}": "Dështoi nisja e vectorizimit: {error}", + "Failed to switch organisation: {error}": "Dështoi kalimi i organizatës: {error}", + "Failed to sync magic table for {schema}: {error}": "Dështoi sinkronizimi i tabelës magjike për {schema}: {error}", + "Failed to test SOLR connection: {error}": "Dështoi testimi i lidhjes SOLR: {error}", + "Failed to test webhook": "Dështoi testimi i webhook", + "Failed to update LLM settings: {error}": "Dështoi përditësimi i cilësimeve LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Dështoi përditësimi i cilësimeve Multitenancy: {error}", + "Failed to update RBAC settings: {error}": "Dështoi përditësimi i cilësimeve RBAC: {error}", + "Failed to update Retention settings: {error}": "Dështoi përditësimi i cilësimeve të Ruajtjes: {error}", + "Failed to update SOLR settings: {error}": "Dështoi përditësimi i cilësimeve SOLR: {error}", + "Failed to update favorite status": "Dështoi përditësimi i statusit të preferatës", + "Failed to update schema configuration for {schema}: {error}": "Dështoi përditësimi i konfigurimit të skemës për {schema}: {error}", + "Failed to update schema properties": "Dështoi përditësimi i pronave të skemës", + "Failed to update view": "Dështoi përditësimi i pamjes", + "Failed to update view: {error}": "Dështoi përditësimi i pamjes: {error}", + "Failed to update webhook": "Dështoi përditësimi i webhook", + "Failed to upload avatar": "Dështoi ngarkimi i avatarit", + "Failed to validate {schema}: {error}": "Dështoi validimi i {schema}: {error}", + "Failed to vectorize files: {error}": "Dështoi vectorizimi i skedarëve: {error}", + "Failed to warmup names cache: {error}": "Dështoi ngrohja e cache të emrave: {error}", + "Federation": "Federatë", + "Feedback recorded": "Reagimet u regjistruan", + "Fewer sources (1-3):": "Burime më pak (1-3):", + "Field": "Fushë", + "Fields to Display": "Fushat për Shfaqje", + "File": "Skedar", + "File #{id}": "Skedar #{id}", + "File Chunk Prediction": "Parashikimi i Copës së Skedarit", + "File Collection": "Koleksioni i Skedarëve", + "File Handling": "Trajtimi i Skedarëve", + "File Location": "Vendndodhja e Skedarit", + "File Management": "Menaxhimi i Skedarëve", + "File Name": "Emri i Skedarit", + "File Path": "Shtegu i Skedarit", + "File Type Selection": "Zgjedhja e Llojit të Skedarit", + "File Types to Process": "Llojet e Skedarëve për Përpunim", + "File Vectorization": "Vectorizimi i Skedarëve", + "File Warmup": "Ngrohja e Skedarëve", + "File actions menu": "Menuja e veprimeve të skedarit", + "File anonymized successfully": "Skedari u anonimizua me sukses", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vectorizimi i copave të skedarit nuk është zbatuar ende. Copat janë gati dhe të ruajtura, por shërbimi i vectorizimit është në zhvillim.", + "File collection not configured": "Koleksioni i skedarëve nuk është konfiguruar", + "File discovery completed": "Zbulimi i skedarëve përfundoi", + "File extraction completed": "Nxjerrja e skedarëve përfundoi", + "File extraction queued": "Nxjerrja e skedarëve është në radhë", + "File indexed successfully": "Skedari u indeksua me sukses", + "File is already anonymized": "Skedari është tashmë i anonimizuar", + "File name": "Emri i skedarit", + "File not found": "Skedari nuk u gjet", + "File settings saved successfully": "Cilësimet e skedarit u ruajtën me sukses", + "File settings updated successfully": "Cilësimet e skedarit u përditësuan me sukses", + "File sources": "Burimet e skedarëve", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vectorizimi i skedarëve përfundoi! {vectorized} copa vectorizuara nga {files} skedarë. {failed} dështuan.", + "File vectorization configuration saved successfully": "Konfigurimi i vectorizimit të skedarëve u ruajt me sukses", + "File vectorization started. Check the statistics section for progress.": "Vectorizimi i skedarëve filloi. Kontrolloni seksionin e statistikave për progresin.", + "File warmup completed": "Ngrohja e skedarëve përfundoi", + "Files": "Skedarë", + "Files added successfully": "Skedarët u shtuan me sukses", + "Files with Completed Extraction:": "Skedarë me Nxjerrje të Përfunduar:", + "Files → fileCollection, Objects → objectCollection": "Skedarë → fileCollection, Objekte → objectCollection", + "Files → filecollection, objects → objectcollection": "Skedarë → filecollection, objekte → objectcollection", + "Filter Audit Trails": "Filtroni Gjurmët e Auditimit", + "Filter Deleted Items": "Filtroni Artikujt e Fshirë", + "Filter Objects": "Filtroni Objektet", + "Filter Properties": "Filtroni Pronat", + "Filter Search Trails": "Filtroni Gjurmët e Kërkimit", + "Filter Statistics": "Filtroni Statistikat", + "Filter and analyze search trail entries": "Filtroni dhe analizoni hyrjet e gjurmëve të kërkimit", + "Filter and manage audit trail entries": "Filtroni dhe menaxhoni hyrjet e gjurmëve të auditimit", + "Filter and manage soft deleted items": "Filtroni dhe menaxhoni artikujt e fshirë butë", + "Filter and search entities": "Filtroni dhe kërkoni entitete", + "Filter by object ID": "Filtro sipas ID objektit", + "Filter by search term": "Filtro sipas termit të kërkimit", + "Filter by type": "Filtro sipas llojit", + "Filter by webhook": "Filtro sipas webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Të dhënat e filtrit u ngarkuan automatikisht. Përdorni filtrat më poshtë për të rafinuar kërkimin tuaj.", + "Filter fields...": "Filtroni fushat...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtroni aktivizuesit e webhook sipas pronave të payload (një për rresht, format: çelës: vlerë)", + "Filtered": "Filtruar", + "Filters": "Filtra", + "Finalizing setup...": "Duke finalizuar konfigurimin...", + "Fireworks AI Chat Settings": "Cilësimet e Chat Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfigurimi i Embedding Fireworks AI", + "First": "I pari", + "Fixed": "Korrigjuar", + "For chat and retrieval-augmented generation": "Për chat dhe gjenerim të rritur me rikthim", + "For vector embeddings and semantic search": "Për embeddings vektoriale dhe kërkim semantik", + "Format": "Format", + "Format Issue": "Problem Formati", + "Format constraint is missing": "Kufizimi i formatit mungon", + "From (e.g. -7 days or 2025-01-01)": "Nga (p.sh. -7 ditë ose 2025-01-01)", + "From Date": "Nga Data", + "From date": "Nga data", + "Full data copied to clipboard": "Të dhënat e plota u kopjuan në clipboard", + "Function Tools": "Mjetet e Funksionit", + "General Issue": "Problem i Përgjithshëm", + "Generate recommendations and confidence scores": "Gjeneroni rekomandime dhe rezultate besimi", + "Generate report": "Gjenero raport", + "Generate the verantwoordingsdocument": "Gjenero verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Gjeneroni vektorë menjëherë kur krijohen objekte të reja", + "Generated: {time}": "Gjeneruar: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Merrni njohuri në kohë reale rreth shëndetit të të dhënave të organizatës suaj duke u fokusuar në regjistrat, përkufizimet e skemës dhe ruajtjen dhe përdorimin e objekteve.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Merrni njohuri në kohë reale rreth shëndetit të të dhënave të organizatës suaj, trendet e rritjes dhe metrikat e cilësisë.", + "GitHub Personal Access Token": "Token i Qasjes Personale GitHub", + "GitHub token is valid": "Token GitHub është i vlefshëm", + "GitHub token is valid! Username: {username}": "Token GitHub është i vlefshëm! Emri i përdoruesit: {username}", + "GitHub token removed successfully": "Token GitHub u hoq me sukses", + "GitHub token saved successfully": "Token GitHub u ruajt me sukses", + "GitHub token test failed: {message}": "Testimi i token GitHub dështoi: {message}", + "GitLab Personal Access Token": "Token i Qasjes Personale GitLab", + "GitLab URL saved successfully": "URL GitLab u ruajt me sukses", + "GitLab token is valid": "Token GitLab është i vlefshëm", + "GitLab token is valid! Username: {username}": "Token GitLab është i vlefshëm! Emri i përdoruesit: {username}", + "GitLab token saved successfully": "Token GitLab u ruajt me sukses", + "GitLab token test failed: {message}": "Testimi i token GitLab dështoi: {message}", + "Go to linked Register": "Shkoni te Regjistri i Lidhur", + "Go to linked Schema": "Shkoni te Skema e Lidhur", + "Go to organisation": "Shkoni te organizata", + "Grant additional permissions beyond standard CRUD operations": "Jepni leje shtesë përtej operacioneve standarde CRUD", + "Group Quota": "Kuota e Grupit", + "Groups": "Grupe", + "HTML preview": "Parapamja HTML", + "HTTP Method": "Metoda HTTP", + "HTTP method used to send webhook requests": "Metoda HTTP e përdorur për të dërguar kërkesa webhook", + "Headers": "Tituj", + "Health": "Shëndet", + "Heartbeat successful - connection kept alive": "Heartbeat i suksesshëm - lidhja mbajtur gjallë", + "Helpful": "Ndihmës", + "Here you can configure the details for various connections.": "Këtu mund të konfiguroni detajet për lidhje të ndryshme.", + "Here you can configure the details for your organisation.": "Këtu mund të konfiguroni detajet për organizatën tuaj.", + "Hide": "Fsheh", + "Hide Filters": "Fshih Filtrat", + "Hide in collection view": "Fshih në pamjen e koleksionit", + "Hide in forms": "Fshih në formularë", + "Hide in list view": "Fshih në pamjen listë", + "High": "Lartë", + "High Confidence": "Besim i Lartë", + "Host": "Pritës", + "Hourly": "Çdo orë", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Sa thellë të kaluara pronat e objekteve të mbivendosura (1-20). Vlerat më të larta kapin më shumë detaje por rrisin madhësinë e vektorit.", + "How often to check for updates (1-168 hours)": "Sa shpesh të kontrolloni për përditësime (1-168 orë)", + "How to handle retries for failed webhook deliveries": "Si të trajtoni riprovimet për dërgesat e dështuara të webhook", + "IBANs": "IBAN", + "ID": "ID", + "ID of input mapping (optional)": "ID e hartës hyrëse (opsional)", + "ID of output mapping (optional)": "ID e hartës dalëse (opsional)", + "ID of the target resource": "ID e burimit të synuar", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Kufizimet e shkallës IP u pastruan me sukses", + "Identify properties not in the schema": "Identifikoni pronat që nuk janë në skemë", + "Immutable": "I pandryshueshëm", + "Import": "Importo", + "Import Configuration": "Importo Konfigurimin", + "Import Data into Register": "Importo Të dhënat në Regjistër", + "Import successful": "Importimi u krye me sukses", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importoni shablonin report-bundle.json për të marrë regjistrin `reports`, pastaj krijoni panelin tuaj të parë nëpërmjet UI standarde të objektit. Panelet deklarojnë widget-et e tyre në JSON dhe renderizuesi furnizon çdo widget me të dhëna agregimi live.", + "Improved Property": "Prona e Përmirësuar", + "In use": "Në përdorim", + "Inactive": "Joaktiv", + "Inactive organisations cannot be used": "Organizatat joaktive nuk mund të përdoren", + "Include IDs and names of related objects for better contextual search": "Përfshini ID dhe emrat e objekteve të lidhura për kërkim kontekstual më të mirë", + "Include ids and names of related objects for better contextual search": "Përfshini id dhe emrat e objekteve të lidhura për kërkim kontekstual më të mirë", + "Include related object references": "Përfshini referencat e objekteve të lidhura", + "Include schema and register metadata": "Përfshini meta të dhënat e skemës dhe regjistrit", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Informacion rreth instalimit aktual të OpenRegister", + "Inherited from register": "Trashëguar nga regjistri", + "Inherits permissions from register": "Trashëgon lejet nga regjistri", + "Initialization failed": "Inicializimi dështoi", + "Initialize Project": "Inicializoni Projektin", + "Initializing SOLR setup...": "Duke inicializuar konfigurimin SOLR...", + "Input Mapping": "Harta Hyrëse", + "Inspect Fields": "Inspektoni Fushat", + "Inspect Index": "Inspektoni Indeksin", + "Inspect SOLR Index": "Inspektoni Indeksin SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instaloni aplikacionin notify_push nga Dyqani i Aplikacioneve Nextcloud për të aktivizuar përditësimet në kohë reale.", + "Integrations": "Integrime", + "Internet location (URL) of this installation": "Vendndodhja internet (URL) e këtij instalimi", + "Interval": "Interval", + "Invalid": "I pavlefshëm", + "Invalid batch size. Must be between 1 and 5000": "Madhësia e grupit e pavlefshme. Duhet të jetë midis 1 dhe 5000", + "Invalid feedback type": "Lloji i reagimit i pavlefshëm", + "Invalid field name provided": "Emri i fushës i pavlefshëm i ofruar", + "Invalid maxObjects. Must be 0 (all) or positive number": "maxObjects i pavlefshëm. Duhet të jetë 0 (të gjitha) ose numër pozitiv", + "Invalid:": "I pavlefshëm:", + "Invite Users": "Ftoni Përdoruesit", + "Invited Users": "Përdoruesit e Ftuar", + "Inzage (Art 15)": "Inspektim (Neni 15)", + "Inzage results": "Rezultatet e inspektimit", + "Issue": "Problem", + "Items per page:": "Artikuj për faqe:", + "Just now": "Tani", + "Key": "Çelës", + "LLM Configuration": "Konfigurimi LLM", + "LLM actions menu": "Menuja e veprimeve LLM", + "LLM configuration saved successfully": "Konfigurimi LLM u ruajt me sukses", + "LLM features disabled": "Veçoritë LLM çaktivizuara", + "LLM features enabled": "Veçoritë LLM aktivizuara", + "LLM must be enabled with an embedding provider configured": "LLM duhet të aktivizohet me një ofrues embedding të konfiguruar", + "LLM settings saved successfully": "Cilësimet LLM u ruajtën me sukses", + "LLM settings updated successfully": "Cilësimet LLM u përditësuan me sukses", + "Label": "Etiketë", + "Labels": "Etiketa", + "Languages": "Gjuhë", + "Last": "I fundit", + "Last Triggered": "Aktivizuar Herën e Fundit", + "Leave empty to allow all users access": "Lëreni bosh për të lejuar qasjen e të gjithë përdoruesve", + "Leave off for auto-detection from property format.": "Lëreni jashtë për zbulim automatik nga formati i pronës.", + "Left organisation successfully": "Organizata u la me sukses", + "Legal basis *": "Baza ligjore *", + "Length Range:": "Intervali i Gjatësisë:", + "Linear": "Linear", + "Link": "Lidhje", + "Link an email from the Mail app sidebar to associate it with this object.": "Lidhni një email nga shiriti anësor i aplikacionit Mail për ta lidhur me këtë objekt.", + "Link an object to see it here.": "Lidhni një objekt për ta parë këtu.", + "Link another object": "Lidhni objekt tjetër", + "Link existing event": "Lidhni ngjarje ekzistuese", + "Link removed": "Lidhja u hoq", + "Link to Object": "Lidhje te Objekti", + "Link to {name}": "Lidhje te {name}", + "Linked Objects": "Objektet e Lidhura", + "Linked by {user}": "Lidhur nga {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Emailet e lidhura, ngjarjet e kalendarit, kontaktet dhe kartat Deck do të shfaqen këtu.", + "Linked to {name}": "Lidhur te {name}", + "Load Advanced Filters": "Ngarko Filtrat e Avancuar", + "Load advanced filters with live data from your search index": "Ngarko filtrat e avancuar me të dhëna live nga indeksi juaj i kërkimit", + "Load more": "Ngarko më shumë", + "Load view": "Ngarko pamjen", + "Loading API tokens...": "Duke ngarkuar token API...", + "Loading ConfigSets...": "Duke ngarkuar ConfigSets...", + "Loading LLM configuration...": "Duke ngarkuar konfigurimin LLM...", + "Loading RBAC settings...": "Duke ngarkuar cilësimet RBAC...", + "Loading activity...": "Duke ngarkuar aktivitetin...", + "Loading advanced filters...": "Duke ngarkuar filtrat e avancuar...", + "Loading agents...": "Duke ngarkuar agjentët...", + "Loading application details...": "Duke ngarkuar detajet e aplikacionit...", + "Loading applications...": "Duke ngarkuar aplikacionet...", + "Loading archived conversations...": "Duke ngarkuar bisedat e arkivuara...", + "Loading audit trails...": "Duke ngarkuar gjurmët e auditimit...", + "Loading available tools...": "Duke ngarkuar mjetet e disponueshme...", + "Loading cache statistics...": "Duke ngarkuar statistikat e cache...", + "Loading collections...": "Duke ngarkuar koleksionet...", + "Loading configurations...": "Duke ngarkuar konfigurimet...", + "Loading conversation...": "Duke ngarkuar bisedën...", + "Loading conversations...": "Duke ngarkuar bisedat...", + "Loading deleted items...": "Duke ngarkuar artikujt e fshirë...", + "Loading endpoints": "Duke ngarkuar pikëfundte", + "Loading entities...": "Duke ngarkuar entitetet...", + "Loading events...": "Duke ngarkuar ngjarjet...", + "Loading filter data...": "Duke ngarkuar të dhënat e filtrit...", + "Loading groups...": "Duke ngarkuar grupet...", + "Loading linked objects...": "Duke ngarkuar objektet e lidhura...", + "Loading log details...": "Duke ngarkuar detajet e regjistrit...", + "Loading multitenancy settings...": "Duke ngarkuar cilësimet multitenancy...", + "Loading n8n configuration...": "Duke ngarkuar konfigurimin n8n...", + "Loading permission matrix...": "Duke ngarkuar matricën e lejeve...", + "Loading preferences...": "Duke ngarkuar preferencat...", + "Loading register data...": "Duke ngarkuar të dhënat e regjistrit...", + "Loading registers...": "Duke ngarkuar regjistrat...", + "Loading retention settings...": "Duke ngarkuar cilësimet e ruajtjes...", + "Loading schemas...": "Duke ngarkuar skemat...", + "Loading search configuration...": "Duke ngarkuar konfigurimin e kërkimit...", + "Loading search trails...": "Duke ngarkuar gjurmët e kërkimit...", + "Loading sources...": "Duke ngarkuar burimet...", + "Loading statistics...": "Duke ngarkuar statistikat...", + "Loading text extraction settings...": "Duke ngarkuar cilësimet e nxjerrjes së tekstit...", + "Loading tokens...": "Duke ngarkuar token...", + "Loading user groups...": "Duke ngarkuar grupet e përdoruesve...", + "Loading users...": "Duke ngarkuar përdoruesit...", + "Loading views...": "Duke ngarkuar pamjet...", + "Loading...": "Duke ngarkuar...", + "Local": "Lokal", + "Local Version": "Versioni Lokal", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Gjeni çdo objekt që i referohet një subjekti të dhënash (Neni 15 inzage), parapamje të fshirjes (Neni 17 vergetelheid), ose eksportoni të dhënat e tyre (Neni 20 portabiliteit).", + "Location": "Vendndodhje", + "Location Field": "Fusha e Vendndodhjes", + "Locations": "Vendndodhje", + "Locked": "I kyçur", + "Locked:": "I kyçur:", + "Login successful": "Identifikimi u krye me sukses", + "Logs": "Regjistra", + "Low": "Ulët", + "Low Confidence": "Besim i Ulët", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Numrat më të ulët shfaqen të parët në shiritanin e filtrave. Lëreni bosh për renditje automatike.", + "Lucene Version:": "Versioni Lucene:", + "Magic table sync completed for {schema}": "Sinkronizimi i tabelës magjike u krye për {schema}", + "Magic table synchronized successfully": "Tabela magjike u sinkronizua me sukses", + "Mail Integration": "Integrimi i Postës", + "Mail integration is not available": "Integrimi i postës nuk është i disponueshëm", + "Make this installation discoverable within the federation network.": "Bëjeni këtë instalim të zbulueshëm brenda rrjetit të federatës.", + "Manage Organisation Roles": "Menaxhoni Rolet e Organizatës", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Menaxhoni Koleksionet SOLR (dyqanet e të dhënave) dhe caktojini ato për objekte dhe skedarë.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Menaxhoni ConfigSets SOLR (shablone konfigurimi) për koleksionet tuaja.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Menaxhoni koleksionet SOLR (dyqanet e të dhënave) dhe caktojini ato për objekte dhe skedarë.", + "Manage SOLR configsets (configuration templates) for your collections.": "Menaxhoni configsets SOLR (shablone konfigurimi) për koleksionet tuaja.", + "Manage and configure agents for automated tasks": "Menaxhoni dhe konfiguroni agjentët për detyra të automatizuara", + "Manage and monitor file text extraction status": "Menaxhoni dhe monitoroni statusin e nxjerrjes së tekstit të skedarit", + "Manage and restore soft deleted items from your registers": "Menaxhoni dhe rivendosni artikujt e fshirë butë nga regjistrat tuaj", + "Manage and view detected entities from files and objects": "Menaxhoni dhe shikoni entitetet e zbuluara nga skedarët dhe objektet", + "Manage document templates and themes": "Menaxhoni shabllonet dhe temat e dokumenteve", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Menaxhoni flukset e punës n8n për automatizimin e OpenRegister. Flukset e punës do të ruhen në projektin e konfiguruar.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Menaxhoni aktivitetet e përpunimit, ekzekutoni kërkesat e qasjes së subjektit të të dhënave dhe auditoni pajtueshmërinë nën GDPR-in e BE / AVG-in holandez.", + "Manage webhooks for event-driven integrations": "Menaxhoni webhooks për integrime të drejtuar nga ngjarje", + "Manage your account settings, security, and personal data.": "Menaxhoni cilësimet e llogarisë, sigurinë dhe të dhënat personale.", + "Manage your applications and modules": "Menaxhoni aplikacionet dhe modulet tuaja", + "Manage your chat conversations": "Menaxhoni bisedat tuaja chat", + "Manage your data registers and their configurations": "Menaxhoni regjistrat e të dhënave tuaja dhe konfigurimet e tyre", + "Manage your data schemas and their properties": "Menaxhoni skemat e të dhënave tuaja dhe pronat e tyre", + "Manage your data sources and their configurations": "Menaxhoni burimet e të dhënave tuaja dhe konfigurimet e tyre", + "Manage your organisations and switch between them": "Menaxhoni organizatat tuaja dhe kaloni ndërmjet tyre", + "Manage your saved search configurations": "Menaxhoni konfigurimet e ruajtura të kërkimit", + "Manage your system configurations and settings": "Menaxhoni konfigurimet dhe cilësimet e sistemit tuaj", + "Managed": "Menaxhuar", + "Mappings": "Harta", + "Mass validation completed successfully": "Validimi masiv u krye me sukses", + "Mass validation failed: {error}": "Validimi masiv dështoi: {error}", + "Max Files (0 = all)": "Skedarë Maks (0 = të gjithë)", + "Max Length": "Gjatësi Maks", + "Max Length:": "Gjatësi Maks:", + "Max Objects (0 = all)": "Objekte Maks (0 = të gjitha)", + "Max Retries": "Riprovime Maks", + "Max Shards/Node": "Shards Maks/Nyje", + "Max Tokens": "Token Maks", + "Max execution time (ms)": "Koha maks e ekzekutimit (ms)", + "Max ms": "ms maks", + "Max result count": "Numri maks i rezultateve", + "Max results": "Rezultate maks", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Madhësia Maksimale e Skedarit (MB)", + "Maximum Nesting Depth": "Thellësia Maksimale e Mbivendosjes", + "Maximum length": "Gjatësi maksimale", + "Maximum length constraint is missing": "Kufizimi i gjatësisë maksimale mungon", + "Maximum length is too restrictive": "Gjatësia maksimale është shumë kufizuese", + "Maximum number of files to process. Set to 0 to process all files.": "Numri maksimal i skedarëve për përpunim. Vendosni 0 për të përpunuar të gjithë skedarët.", + "Maximum number of items": "Numri maksimal i artikujve", + "Maximum number of objects to process. Set to 0 to process all objects.": "Numri maksimal i objekteve për përpunim. Vendosni 0 për të përpunuar të gjitha objektet.", + "Maximum number of retry attempts for failed deliveries": "Numri maksimal i tentativave të riprovimit për dërgesat e dështuara", + "Maximum time to wait before committing changes": "Koha maksimale për të pritur para angazhimit të ndryshimeve", + "Maximum tokens to generate": "Token maksimalë për gjenerim", + "Maximum value": "Vlerë maksimale", + "Maximum value constraint is missing": "Kufizimi i vlerës maksimale mungon", + "Maximum value is too restrictive": "Vlera maksimale është shumë kufizuese", + "Medium": "Mesatar", + "Medium Confidence": "Besim Mesatar", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Pyetje mesatare: Kërkime me filtrim të disa ose parametra të shumtë (p.sh., intervalet e datave, regjistrat/skemat specifike)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Pyetje mesatare: kërkime me filtrim të disa ose parametra të shumtë (p.sh., intervalet e datave, regjistrat/skemat specifike)", + "Member removed successfully": "Anëtari u hoq me sukses", + "Members": "Anëtarë", + "Members ({count})": "Anëtarë ({count})", + "Members of selected groups can access this view": "Anëtarët e grupeve të zgjedhura mund të qasen në këtë pamje", + "Members:": "Anëtarë:", + "Memory prediction calculated successfully": "Parashikimi i memories u llogarit me sukses", + "Message content is required": "Përmbajtja e mesazhit është e nevojshme", + "Message does not belong to this conversation": "Mesazhi nuk i përket kësaj bisede", + "Message not found": "Mesazhi nuk u gjet", + "Metadata": "Meta të dhëna", + "Metadata Filters": "Filtrat e Meta të dhënave", + "Method": "Metodë", + "Method*": "Metodë*", + "Method: {method}": "Metodë: {method}", + "Min Length": "Gjatësi Min", + "Min execution time (ms)": "Koha min e ekzekutimit (ms)", + "Min ms": "ms min", + "Min result count": "Numri min i rezultateve", + "Min results": "Rezultate min", + "Minimum": "Minimum", + "Minimum length": "Gjatësi minimale", + "Minimum number of items": "Numri minimal i artikujve", + "Minimum value": "Vlerë minimale", + "Minimum value constraint is missing": "Kufizimi i vlerës minimale mungon", + "Minimum value is too restrictive": "Vlera minimale është shumë kufizuese", + "Missing conversation or agentUuid": "Biseda ose agentUuid mungon", + "Missing conversationId": "conversationId mungon", + "Missing message": "Mesazhi mungon", + "Missing object fields created successfully": "Fushat e objektit që mungonin u krijuan me sukses", + "Mode:": "Modalitet:", + "Model": "Model", + "Model Name": "Emri i Modelit", + "Modified": "Modifikuar", + "Monthly": "Çdo muaj", + "More sources (10-20):": "Burime më shumë (10-20):", + "Most Active Objects": "Objektet Më Aktive", + "Multiple Of": "Shumëfish i", + "Multiple of": "Shumëfish i", + "Multiple search trail deletion not implemented yet": "Fshirja e shumë gjurmëve kërkimi nuk është zbatuar ende", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Cilësimet multitenancy u përditësuan me sukses", + "My Account": "Llogaria Ime", + "Naam": "Emër", + "Naam *": "Emër *", + "Name": "Emër", + "Name *": "Emër *", + "Name*": "Emër*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Cache e emrave u ngroh me sukses: {count} emra ngarkuar në {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Cache e emrave u ngroh me sukses: {count} emra ngarkuar në {time}. {message}", + "Names cache warmup completed": "Ngrohja e cache të emrave përfundoi", + "Naming pattern for copies:": "Modeli i emërtimit për kopjet:", + "New Collection Name": "Emri i Ri i Koleksionit", + "New Configuration": "Konfigurim i Ri", + "New Conversation": "Bisedë e Re", + "New Properties": "Prona të Reja", + "New Property": "Pronë e Re", + "New User Group": "Grup i Ri Përdoruesish", + "New Value": "Vlerë e Re", + "New activity": "Aktivitet i ri", + "New password": "Fjalëkalim i ri", + "New processing activity": "Aktivitet i ri përpunimi", + "New users without specific organisation membership will be automatically added to this organisation": "Përdoruesit e rinj pa anëtarësi specifike organizate do të shtohen automatikisht në këtë organizatë", + "New verwerkingsactiviteit": "verwerkingsactiviteit e re", + "Next": "Tjetër", + "Nextcloud Groups": "Grupet Nextcloud", + "No": "Jo", + "No API tokens.": "Nuk ka token API.", + "No ConfigSets found": "Nuk u gjetën ConfigSets", + "No Deck cards linked to this object": "Nuk ka karta Deck të lidhura me këtë objekt", + "No GitHub token configured": "Nuk ka token GitHub të konfiguruar", + "No GitHub token provided": "Nuk ka token GitHub të ofruar", + "No GitLab token provided": "Nuk ka token GitLab të ofruar", + "No Organisation": "Nuk ka Organizatë", + "No active filters": "Nuk ka filtra aktiv", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Nuk ka objekte aktive për fshirje butë për skemën {schema}. Përdorni \"Fshi Përgjithmonë\" për të hequr objektet e fshira butë.", + "No activity data available": "Nuk ka të dhëna aktiviteti të disponueshme", + "No activity found.": "Nuk u gjet asnjë aktivitet.", + "No agents available": "Nuk ka agjentë të disponueshëm", + "No agents found": "Nuk u gjetën agjentë", + "No app store cache exists yet": "Nuk ekziston ende cache i dyqanit të aplikacioneve", + "No applications are available.": "Nuk ka aplikacione të disponueshme.", + "No applications found": "Nuk u gjetën aplikacione", + "No approval steps for this object.": "Nuk ka hapa miratimi për këtë objekt.", + "No archived conversations": "Nuk ka biseda të arkivuara", + "No audit trail entries found": "Nuk u gjetën hyrje gjurmësh auditimi", + "No changes recorded": "Nuk u regjistruan ndryshime", + "No chunks to vectorize": "Nuk ka copa për vectorizim", + "No collections found": "Nuk u gjetën koleksione", + "No configuration data": "Nuk ka të dhëna konfigurimi", + "No configuration selected": "Nuk është zgjedhur konfigurim", + "No configurations are available.": "Nuk ka konfigurime të disponueshme.", + "No configurations found": "Nuk u gjetën konfigurime", + "No configurations found for this application.": "Nuk u gjetën konfigurime për këtë aplikacion.", + "No contacts linked to this object": "Nuk ka kontakte të lidhura me këtë objekt", + "No conversations yet. Create a new one to get started!": "Ende nuk ka biseda. Krijoni një të re për të filluar!", + "No dashboards yet": "Ende nuk ka panele", + "No data available": "Nuk ka të dhëna të disponueshme", + "No data available for chart": "Nuk ka të dhëna të disponueshme për grafikun", + "No deleted items found": "Nuk u gjetën artikuj të fshirë", + "No deletion data available": "Nuk ka të dhëna fshirjeje të disponueshme", + "No description found": "Nuk u gjet përshkrim", + "No description provided": "Nuk u ofrua përshkrim", + "No emails linked to this object": "Nuk ka email të lidhura me këtë objekt", + "No endpoints defined": "Nuk ka pikëfundte të përkufizuara", + "No entities detected for this email.": "Nuk u zbuluan entitete për këtë email.", + "No entities detected in this file. Run text extraction first.": "Nuk u zbuluan entitete në këtë skedar. Ekzekutoni nxjerrjen e tekstit fillimisht.", + "No entities found": "Nuk u gjetën entitete", + "No entities have been detected yet": "Ende nuk janë zbuluar entitete", + "No events found": "Nuk u gjetën ngjarje", + "No events linked to this object": "Nuk ka ngjarje të lidhura me këtë objekt", + "No expired audit trails found to clear": "Nuk u gjetën gjurmë auditimi të skaduara për pastroi", + "No expired search trails found to clear": "Nuk u gjetën gjurmë kërkimi të skaduara për pastroi", + "No extraction data available for this file": "Nuk ka të dhëna nxjerrjeje të disponueshme për këtë skedar", + "No facetable fields available. Select a register and schema to see available filters.": "Nuk ka fusha me facet të disponueshme. Zgjidhni një regjistër dhe skemë për të parë filtrat e disponueshëm.", + "No files found": "Nuk u gjetën skedarë", + "No files have been extracted yet": "Ende nuk janë nxjerrë skedarë", + "No files selected": "Nuk janë zgjedhur skedarë", + "No files to process": "Nuk ka skedarë për përpunim", + "No files to reindex": "Nuk ka skedarë për riindeksim", + "No filters are currently active. This will delete ALL audit trail entries!": "Nuk ka filtra aktualë aktiv. Kjo do të fshijë TË GJITHA hyrjet e gjurmëve të auditimit!", + "No groups found. Try a different search.": "Nuk u gjetën grupe. Provoni kërkim tjetër.", + "No label": "Pa etiketë", + "No labels": "Pa etiketa", + "No linked objects": "Nuk ka objekte të lidhura", + "No log entries found": "Nuk u gjetën hyrje regjistri", + "No logs are available for this configuration.": "Nuk ka regjistra të disponueshëm për këtë konfigurim.", + "No logs are available for this source.": "Nuk ka regjistra të disponueshëm për këtë burim.", + "No logs found": "Nuk u gjetën regjistra", + "No matches": "Pa rezultate", + "No mismatched fields found - SOLR schema is properly configured": "Nuk u gjetën fusha të papërputhshme - skema SOLR është konfiguruar siç duhet", + "No objects are currently selected for deletion.": "Aktualisht nuk janë zgjedhur objekte për fshirje.", + "No objects are currently selected for permanent deletion.": "Aktualisht nuk janë zgjedhur objekte për fshirje të përhershme.", + "No objects are currently selected for restoration.": "Aktualisht nuk janë zgjedhur objekte për rivendosje.", + "No objects found": "Nuk u gjetën objekte", + "No objects found to vectorize": "Nuk u gjetën objekte për vectorizim", + "No objects linked to this email": "Nuk ka objekte të lidhura me këtë email", + "No objects selected": "Nuk janë zgjedhur objekte", + "No objects selected for deletion": "Nuk janë zgjedhur objekte për fshirje", + "No objects selected for restoration": "Nuk janë zgjedhur objekte për rivendosje", + "No objects to delete": "Nuk ka objekte për fshirje", + "No objects to delete for schema {schema}": "Nuk ka objekte për fshirje për skemën {schema}", + "No objects were permanently deleted": "Nuk u fshi përgjithmonë asnjë objekt", + "No organisations found": "Nuk u gjetën organizata", + "No personal data was found for this subject identifier.": "Nuk u gjetën të dhëna personale për këtë identifikues subjekti.", + "No processing activities yet": "Ende nuk ka aktivitete përpunimi", + "No properties found": "Nuk u gjetën prona", + "No properties match your filters.": "Asnjë pronë nuk përputhet me filtrat tuaj.", + "No purge date set": "Nuk është vendosur datë pastrimi", + "No register data available": "Nuk ka të dhëna regjistri të disponueshme", + "No register objects reference this file": "Asnjë objekt regjistri nuk i referohet këtij skedari", + "No registers found": "Nuk u gjetën regjistër", + "No registers found for this application.": "Nuk u gjetën regjistër për këtë aplikacion.", + "No registers found. Create a register to configure permissions.": "Nuk u gjetën regjistër. Krijoni një regjistër për të konfiguruar lejet.", + "No related cases found for this sender": "Nuk u gjetën raste të lidhura për këtë dërgues", + "No related entities": "Nuk ka entitete të lidhura", + "No relations found": "Nuk u gjetën lidhje", + "No request body available": "Nuk ka trup kërkese të disponueshëm", + "No response body available": "Nuk ka trup përgjigje të disponueshëm", + "No saved views yet. Create one in the Search tab!": "Ende nuk ka pamje të ruajtura. Krijoni një në skedën Kërkim!", + "No saved views yet. create one in the search tab!": "Ende nuk ka pamje të ruajtura. krijoni një në skedën kërkim!", + "No scan run yet": "Ende nuk është ekzekutuar skanim", + "No schema data available": "Nuk ka të dhëna skeme të disponueshme", + "No schema selected for exploration": "Nuk është zgjedhur skemë për eksplorim", + "No schemas configured for mail linking.": "Nuk ka skema të konfiguruara për lidhje poste.", + "No schemas found": "Nuk u gjetën skema", + "No schemas found for this application.": "Nuk u gjetën skema për këtë aplikacion.", + "No search terms data available": "Nuk ka të dhëna termash kërkimi të disponueshme", + "No search trail entries found": "Nuk u gjetën hyrje gjurmësh kërkimi", + "No sources are available.": "Nuk ka burime të disponueshme.", + "No sources found": "Nuk u gjetën burime", + "No synchronizations found": "Nuk u gjetën sinkronizime", + "No templates found": "Nuk u gjetën shabllone", + "No templates have been created yet": "Ende nuk janë krijuar shabllone", + "No tools available. Tools can be registered by installed apps.": "Nuk ka mjete të disponueshme. Mjetet mund të regjistrohen nga aplikacionet e instaluara.", + "No users in this organisation.": "Nuk ka përdorues në këtë organizatë.", + "No valid magic-mapped register+schema combinations found": "Nuk u gjetën kombinime të vlefshme regjistër+skemë të hartëzuara magjikisht", + "No views found. Create views first before configuring vectorization.": "Nuk u gjetën pamje. Krijoni pamje fillimisht para se të konfiguroni vectorizimin.", + "No views match your search": "Asnjë pamje nuk përputhet me kërkimin tuaj", + "No webhooks found": "Nuk u gjetën webhooks", + "No webhooks have been configured yet": "Ende nuk janë konfiguruar webhooks", + "No workflows found in this project. Create workflows in the n8n editor.": "Nuk u gjetën flukse pune në këtë projekt. Krijoni flukse pune në redaktuesin n8n.", + "None": "Asnjë", + "Not Found": "Nuk u Gjet", + "Not extracted": "Nuk është nxjerrë", + "Not helpful": "Nuk ndihmon", + "Not in use": "Nuk është në përdorim", + "Note:": "Shënim:", + "Notifications": "Njoftime", + "Number Constraints": "Kufizimet e Numrit", + "Number of Sources": "Numri i Burimeve", + "Number of chunks to vectorize in one API call (1-100).": "Numri i copave për vectorizim në një thirrje API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Numri i copave për vectorizim në një thirrje API. Më i lartë = më shpejtë por më shumë memory. Rekomandohet: 10-50.", + "Number of objects to process in each batch (1-5000).": "Numri i objekteve për përpunim në çdo grup (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Numri i objekteve për vectorizim në një thirrje API. Më i lartë = më shpejtë por më shumë memory. Rekomandohet: 10-50.", + "Numeric Range:": "Intervali Numerik:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Koleksioni i Objekteve", + "Object Configuration:": "Konfigurimi i Objektit:", + "Object Count Prediction": "Parashikimi i Numrit të Objekteve", + "Object Extraction Mode": "Modaliteti i Nxjerrjes së Objektit", + "Object ID": "ID e Objektit", + "Object Management": "Menaxhimi i Objekteve", + "Object Text Extraction": "Nxjerrja e Tekstit të Objektit", + "Object Vectorization": "Vectorizimi i Objekteve", + "Object Warmup": "Ngrohja e Objekteve", + "Object changes in owned objects": "Ndryshimet e objekteve në objektet e zotëruara", + "Object collection not configured": "Koleksioni i objekteve nuk është konfiguruar", + "Object metadata incomplete for file upload": "Meta të dhënat e objektit janë jo të plota për ngarkim skedari", + "Object not found": "Objekti nuk u gjet", + "Object permanently deleted": "Objekti u fshi përgjithmonë", + "Object restored successfully": "Objekti u rivendos me sukses", + "Object settings updated successfully": "Cilësimet e objektit u përditësuan me sukses", + "Object sources": "Burimet e objekteve", + "Object successfully deleted": "Objekti u fshi me sukses", + "Object successfully modified": "Objekti u modifikua me sukses", + "Object unlocked successfully": "Objekti u çkyç me sukses", + "Object vectorization configuration saved successfully": "Konfigurimi i vectorizimit të objektit u ruajt me sukses", + "Object vectorized successfully": "Objekti u vectorizua me sukses", + "Object:": "Objekt:", + "Objects": "Objekte", + "Objects Analyzed": "Objekte të Analizuara", + "Objects Distribution": "Shpërndarja e Objekteve", + "Objects being analyzed": "Objekte duke u analizuar", + "Objects by Register": "Objekte sipas Regjistrit", + "Objects by Schema": "Objekte sipas Skemës", + "Objects deletion completed for {schema}": "Fshirja e objekteve u krye për {schema}", + "Objects deletion completed successfully": "Fshirja e objekteve u krye me sukses", + "Objects in schema": "Objekte në skemë", + "Objects successfully deleted": "Objektet u fshinë me sukses", + "Objects to Process:": "Objekte për Përpunim:", + "Objects to be analyzed": "Objekte për t'u analizuar", + "Objects to be deleted": "Objekte për t'u fshirë", + "Objects to be validated": "Objekte për t'u validuar", + "Objects will be serialized as JSON text before vectorization": "Objektet do të serializohen si tekst JSON para vectorizimit", + "Objects will be soft deleted and moved to the": "Objektet do të fshihen butë dhe zhvendosen te", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objektet do të fshihen butë (shënuar si të fshirë por mbajtur në bazën e të dhënave). Ato mund të rikuperohen më vonë nëse nevojitet.", + "Old Value": "Vlera e Vjetër", + "Oldest Item (days)": "Artikulli Më i Vjetër (ditë)", + "Ollama Chat Settings": "Cilësimet e Chat Ollama", + "Ollama Embedding Configuration": "Konfigurimi i Embedding Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Vetëm Me Ndryshime", + "Only members of selected groups can access this organisation": "Vetëm anëtarët e grupeve të zgjedhura mund të qasen në këtë organizatë", + "Open Nextcloud App Store": "Hapni Dyqanin e Aplikacioneve Nextcloud", + "Open OpenRegister sidebar": "Hapni shiritin anësor OpenRegister", + "Open Register Objects": "Hapni Objektet e Regjistrit", + "Open folder": "Hapni skedarin", + "Open in OpenRegister": "Hapni në OpenRegister", + "Open n8n Editor": "Hapni Redaktuesin n8n", + "OpenAI Chat Settings": "Cilësimet e Chat OpenAI", + "OpenAI Embedding Configuration": "Konfigurimi i Embedding OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Cilësimet e OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Panele të përcaktuara nga operatori dhe raporte të planifikuara. Çdo panel është objekt i klasës së parë në regjistrin `reports`; widget-et deklarohen në grupin `widgets` të panelit dhe renderizen live nga agregime / GraphQL.", + "Optimizing search performance...": "Duke optimizuar performancën e kërkimit...", + "Optional URL-friendly identifier": "Identifikues opsional i përshtatshëm për URL", + "Optional description for this facet": "Përshkrim opsional për këtë facet", + "Optional end date property": "Prona opsionale e datës së mbarimit", + "Optional event description template": "Shablloni opsional i përshkrimit të ngjarjes", + "Optional location property": "Prona opsionale e vendndodhjes", + "Optional webhook secret for signature verification": "Sekret opsional webhook për verifikimin e nënshkrimit", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Opsional: Shtoni parametra pyetjeje për të filtruar skemën e referuara (p.sh., status=active&type=public)", + "Or": "Ose", + "Order": "Rend", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Etiketat e gjuhëve BCP 47 të renditura. Gjuha e parë është parazgjedhja e regjistrit dhe drejton rezervën Accept-Language për pronat e përkthyeshme.", + "Organisation": "Organizatë", + "Organisation Configuration": "Konfigurimi i Organizatës", + "Organisation ID": "ID e Organizatës", + "Organisation Members": "Anëtarët e Organizatës", + "Organisation Statistics": "Statistikat e Organizatës", + "Organisation created successfully": "Organizata u krijua me sukses", + "Organisation membership changes": "Ndryshimet e anëtarësisë së organizatës", + "Organisation name is required": "Emri i organizatës është i nevojshëm", + "Organisation settings saved successfully": "Cilësimet e organizatës u ruajtën me sukses", + "Organisation successfully created": "Organizata u krijua me sukses", + "Organisation successfully updated": "Organizata u përditësua me sukses", + "Organisational measures": "Masat organizative", + "Organisations": "Organizata", + "Organisatorische maatregelen": "Masa organizative", + "Organization": "Organizatë", + "Organization ID (Optional)": "ID e Organizatës (Opsional)", + "Organizations": "Organizata", + "Orphaned Items": "Artikuj të Braktisur", + "Other": "Tjetër", + "Output Mapping": "Harta Dalëse", + "Overview": "Pasqyrë", + "Owner": "Pronar", + "Owner:": "Pronar:", + "PDF": "PDF", + "PII hits": "Goditje PII", + "PII rows": "Rreshta PII", + "Page {current} of {total}": "Faqe {current} e {total}", + "Parallel Mode (Faster, more resource intensive)": "Modaliteti Paralel (Më i shpejtë, me më shumë burime)", + "Parallel:": "Paralel:", + "Password": "Fjalëkalim", + "Password changes are not supported by your authentication provider.": "Ndryshimet e fjalëkalimit nuk mbështeten nga ofruesi juaj i autentifikimit.", + "Password for SOLR authentication (optional)": "Fjalëkalim për autentifikimin SOLR (opsional)", + "Password updated successfully": "Fjalëkalimi u përditësua me sukses", + "Path": "Shteg", + "Path in repository": "Shtegu në depo", + "Path where the configuration file will be saved in the repository": "Shtegu ku do të ruhet skedari i konfigurimit në depo", + "Path where the register OAS file will be saved in the repository": "Shtegu ku do të ruhet skedari OAS i regjistrit në depo", + "Pattern (regex)": "Model (regex)", + "Pattern Issue": "Problem Modeli", + "Pattern constraint is missing": "Kufizimi i modelit mungon", + "Patterns:": "Modele:", + "Pending": "Në pritje", + "Permanent Delete Mode": "Modaliteti i Fshirjes së Përhershme", + "Permanently Delete": "Fshi Përgjithmonë", + "Permanently Delete ({count})": "Fshi Përgjithmonë ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Fshini përgjithmonë të gjitha {count} objektet e fshira butë. Kjo nuk mund të kthehet!", + "Permanently delete already soft-deleted objects": "Fshini përgjithmonë objektet tashmë të fshira butë", + "Permanently delete this conversation?": "Fshini përgjithmonë këtë bisedë?", + "Permission Matrix": "Matrica e Lejeve", + "Person": "Person", + "Personal Data": "Të Dhëna Personale", + "Personal Data Export": "Eksporti i Të Dhënave Personale", + "Persons": "Persona", + "Phone": "Telefon", + "Phone Numbers": "Numra Telefoni", + "Photos": "Foto", + "Please create an agent in the": "Ju lutemi krijoni një agjent në", + "Please select a user": "Ju lutemi zgjidhni një përdorues", + "Please select an agent to continue": "Ju lutemi zgjidhni një agjent për të vazhduar", + "Please select an organisation": "Ju lutemi zgjidhni një organizatë", + "Please select at least one item to import": "Ju lutemi zgjidhni të paktën një artikull për importim", + "Please select registers or schemas first": "Ju lutemi zgjidhni fillimisht regjistrat ose skemat", + "Please select which register and schema to use for the new object": "Ju lutemi zgjidhni cilin regjistër dhe skemë të përdorni për objektin e ri", + "Please try again later.": "Ju lutemi provoni përsëri më vonë.", + "Please wait while we fetch your agents.": "Ju lutemi prisni ndërkohë që marrim agjentët tuaj.", + "Please wait while we fetch your applications.": "Ju lutemi prisni ndërkohë që marrim aplikacionet tuaja.", + "Please wait while we fetch your configurations.": "Ju lutemi prisni ndërkohë që marrim konfigurimet tuaja.", + "Please wait while we fetch your deleted items.": "Ju lutemi prisni ndërkohë që marrim artikujt tuaj të fshirë.", + "Please wait while we fetch your sources.": "Ju lutemi prisni ndërkohë që marrim burimet tuaja.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Ju lutemi prisni ndërkohë që fshimë përgjithmonë koleksionin SOLR. Kjo mund të marrë disa çaste.", + "Polls": "Anketa", + "Popular Search Terms": "Termat e Popullarizuara të Kërkimit", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabilitet (Neni 20)", + "Preferences saved": "Preferencat u ruajtën", + "Prerequisites": "Parakushte", + "Press Enter to send, Shift+Enter for new line": "Shtypni Enter për të dërguar, Shift+Enter për rresht të ri", + "Press enter to send, shift+enter for new line": "Shtypni enter për të dërguar, shift+enter për rresht të ri", + "Prettify": "Zbukuro", + "Preview erasure": "Parapamje e fshirjes", + "Preview:": "Parapamje:", + "Previous": "I mëparshëm", + "Private": "Privat", + "Private Agent (Default)": "Agjent Privat (Parazgjedhje)", + "Private agents": "Agjentë privat", + "Process Name (optional)": "Emri i Procesit (opsional)", + "Processed {indexed} files, {failed} failed": "U përpunuan {indexed} skedarë, {failed} dështuan", + "Processes chunks in batches with simulated parallelism.": "Përpunon copa në grupe me paralelizëm të simuluar.", + "Processes file chunks sequentially (safest).": "Përpunon copa skedarësh sekuencialisht (më i sigurt).", + "Processes objects in chunks with simulated parallelism.": "Përpunon objekte në copa me paralelizëm të simuluar.", + "Processes objects sequentially (safest).": "Përpunon objekte sekuencialisht (më i sigurt).", + "Processing": "Përpunim", + "Processing Limits": "Kufijtë e Përpunimit", + "Processing...": "Duke përpunuar...", + "Project initialized successfully": "Projekti u inicializua me sukses", + "Project not found. Please initialize first.": "Projekti nuk u gjet. Ju lutemi inicializoni fillimisht.", + "Properties": "Prona", + "Property Behaviors": "Sjelljet e Pronave", + "Property Configuration:": "Konfigurimi i Pronës:", + "Property Filters": "Filtrat e Pronave", + "Property Title": "Titulli i Pronës", + "Property Type": "Lloji i Pronës", + "Property can be improved": "Prona mund të përmirësohet", + "Property could not be added": "Prona nuk mund të shtohej", + "Property could not be updated": "Prona nuk mund të përditësohej", + "Property name of inversed relation": "Emri i pronës i lidhjes të përmbysur", + "Property successfully added": "Prona u shtua me sukses", + "Property successfully updated": "Prona u përditësua me sukses", + "Protected": "I mbrojtur", + "Provider": "Ofrues", + "Provider is required for testing": "Ofruesi është i nevojshëm për testim", + "Public": "Publik", + "Public View": "Pamje Publike", + "Public views can be accessed by anyone in the system": "Pamjet publike mund të aksesohen nga çdokush në sistem", + "Publish": "Publiko", + "Publish Configuration to GitHub": "Publiko Konfigurimin te GitHub", + "Publish OAS": "Publiko OAS", + "Publish Objects": "Publiko Objektet", + "Publish Register OAS to GitHub": "Publiko OAS të Regjistrit te GitHub", + "Publish all objects in this schema": "Publiko të gjitha objektet në këtë skemë", + "Publish objects to make them publicly available": "Publiko objektet për t'i bërë të disponueshme publikisht", + "Published": "Publikuar", + "Published:": "Publikuar:", + "Publishing...": "Duke publikuar...", + "Purge": "Pastro", + "Purge Date": "Data e Pastrimit", + "Purge {count} object from database": "Pastro {count} objekt nga baza e të dhënave", + "Purge {count} objects from database": "Pastro {count} objekte nga baza e të dhënave", + "Purpose limitation *": "Kufizimi i qëllimit *", + "Push Notifications": "Njoftime Push", + "Query Complexity Distribution": "Shpërndarja e Kompleksitetit të Pyetjeve", + "Query Help": "Ndihmë Pyetjesh", + "Query parameter is required": "Parametri i pyetjes është i nevojshëm", + "Quota": "Kuotë", + "RAG": "RAG", + "RAG Configuration": "Konfigurimi RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Cilësimet RBAC u përditësuan me sukses", + "Rate limits cleared successfully": "Kufizimet e shkallës u pastruan me sukses", + "Raw Changes Data": "Të Dhënat Bruto të Ndryshimeve", + "Re-vectorize on object update": "Rivektorizoni gjatë përditësimit të objektit", + "Read": "Lexo", + "Real-time push notification status via notify_push": "Statusi i njoftimeve push në kohë reale nëpërmjet notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Njoftimet push në kohë reale janë aktive. Klientët e lidhur marrin përditësime të menjëhershme kur objektet krijohen, përditësohen ose fshihen.", + "Realtime push active": "Push në kohë reale aktiv", + "Realtime push not available — the notify_push app is not installed": "Push në kohë reale nuk është i disponueshëm — aplikacioni notify_push nuk është instaluar", + "Reason": "Arsye", + "Reason (optional)": "Arsye (opsional)", + "Rebase operation completed successfully": "Operacioni rebase u krye me sukses", + "Rebase operation failed: {error}": "Operacioni rebase dështoi: {error}", + "Rechtsgrond": "Baza ligjore", + "Recommendations:": "Rekomandimet:", + "Recommended Type:": "Lloji i Rekomanduar:", + "Recommended: 5 sources": "Rekomandohet: 5 burime", + "Refresh": "Rifresko", + "Refresh Data": "Rifresko Të dhënat", + "Refresh Stats": "Rifresko Statistikat", + "Refresh Workflows": "Rifresko Flukset e Punës", + "Refresh dashboard": "Rifresko panelin", + "Refresh database info": "Rifresko informacionin e bazës së të dhënave", + "Register": "Regjistër", + "Register #{id}": "Regjistër #{id}", + "Register / Schema": "Regjistër / Skemë", + "Register ID": "ID e Regjistrit", + "Register Objects": "Objekte Regjistri", + "Register Overview": "Pasqyra e Regjistrit", + "Register Statistics": "Statistikat e Regjistrit", + "Register Totals": "Totalet e Regjistrit", + "Register and schema are required": "Regjistri dhe skema janë të nevojshme", + "Register annotation": "Shënim regjistri", + "Register depublished successfully": "Regjistri u çpublikua me sukses", + "Register not found": "Regjistri nuk u gjet", + "Register objects": "Objekte regjistri", + "Register objects deletion completed successfully": "Fshirja e objekteve të regjistrit u krye me sukses", + "Register published successfully": "Regjistri u publikua me sukses", + "Register/Schema Usage": "Përdorimi i Regjistrit/Skemës", + "Register:": "Regjistër:", + "Registers": "Regjistër", + "Reindex": "Riindekso", + "Reindex completed": "Riindeksimi përfundoi", + "Reindex completed successfully": "Riindeksimi u krye me sukses", + "Reindex completed! Processed {count} objects in {duration}s": "Riindeksimi u krye! U përpunuan {count} objekte në {duration}s", + "Reindex failed": "Riindeksimi dështoi", + "Reindex failed: %s": "Riindeksimi dështoi: %s", + "Reject": "Refuzo", + "Rejected": "Refuzuar", + "Related Cases": "Rastet e Lidhura", + "Relations": "Lidhje", + "Remote Version": "Versioni i Largët", + "Remove": "Hiq", + "Remove Deck card": "Hiq kartën Deck", + "Remove avatar": "Hiq avatarin", + "Remove contact": "Hiq kontaktin", + "Remove filter": "Hiq filtrin", + "Remove from favorites": "Hiq nga të preferuarat", + "Remove from list": "Hiq nga lista", + "Remove group": "Hiq grupin", + "Remove link": "Hiq lidhjen", + "Remove link to {name}": "Hiq lidhjen te {name}", + "Remove link to {name}?": "Hiqni lidhjen te {name}?", + "Remove link to {title}": "Hiq lidhjen te {title}", + "Remove oneOf entry": "Hiq hyrjen oneOf", + "Remove property after write-back": "Hiq pronën pas shkrimit kthyes", + "Remove {title}": "Hiq {title}", + "Remove {userId} from this organisation?": "Hiqni {userId} nga kjo organizatë?", + "Removed": "Hequr", + "Removed from favorites": "Hequr nga të preferuarat", + "Removing schema {schema}...": "Duke hequr skemën {schema}...", + "Rename Conversation": "Riemërtoni Bisedën", + "Rename conversation": "Riemërtoni bisedën", + "Replicas": "Replika", + "Reports": "Raporte", + "Repository": "Depo", + "Request Body": "Trupi i Kërkesës", + "Request Data": "Të Dhënat e Kërkesës", + "Request Details": "Detajet e Kërkesës", + "Request Quota (per day)": "Kuota e Kërkesave (në ditë)", + "Request account deactivation": "Kërkoni çaktivizimin e llogarisë", + "Request account deactivation. This will notify administrators for review.": "Kërkoni çaktivizimin e llogarisë. Kjo do të njoftojë administratorët për shqyrtim.", + "Request body must be JSON with a \"query\" field": "Trupi i kërkesës duhet të jetë JSON me një fushë \"query\"", + "Request timeout in seconds": "Koha e skadimit të kërkesës në sekonda", + "Requested at": "Kërkuar në", + "Required": "I nevojshëm", + "Required Token Scopes": "Shtrirjet e Token të Nevojshme", + "Required field": "Fushë e nevojshme", + "Required status is inconsistent": "Statusi i nevojshëm është i pasqendrueshëm", + "Requires Dolphin AI for OCR text extraction": "Kërkon Dolphin AI për nxjerrjen e tekstit OCR", + "Rerun Search": "Rilëxoni Kërkimin", + "Rerunning search: {searchTerm}": "Duke rilëxuar kërkimin: {searchTerm}", + "Reset Changes": "Rivendosni Ndryshimet", + "Reset Filters": "Rivendosni Filtrat", + "Resource Quotas": "Kuotat e Burimeve", + "Response Body": "Trupi i Përgjigjes", + "Response Details": "Detajet e Përgjigjes", + "Response Time:": "Koha e Përgjigjes:", + "Restore": "Rivendos", + "Restore conversation": "Rivendos bisedën", + "Restore or permanently delete items": "Rivendosni ose fshini përgjithmonë artikujt", + "Restore {count} object": "Rivendos {count} objekt", + "Restore {count} objects": "Rivendos {count} objekte", + "Result Count Range": "Intervali i Numrit të Rezultateve", + "Results": "Rezultate", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Periudha e ruajtjes (kohëzgjatje ISO-8601, p.sh. P10Y, P30D)", + "Retention settings updated successfully": "Cilësimet e ruajtjes u përditësuan me sukses", + "Retrieve all objects for this schema": "Merrni të gjitha objektet për këtë skemë", + "Retry": "Riprovo", + "Retry Failed Extractions": "Riprovo Nxjerrjet e Dështuara", + "Retry Policy": "Politika e Riprovimit", + "Retry completed": "Riprova u krye", + "Retry upload": "Riprovo ngarkimin", + "Retrying...": "Duke riprovuar...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Shqyrtoni objektet e zgjedhura më poshtë. Mund të hiqni çdo objekt që nuk doni të fshini duke klikuar butonin e heqjes.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Shqyrtoni objektet e zgjedhura më poshtë. Mund të hiqni çdo objekt që nuk doni të përfshini.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Shqyrtoni objektet e zgjedhura më poshtë. Mund të hiqni çdo objekt që nuk doni të fshini përgjithmonë duke klikuar butonin e heqjes. Ky veprim nuk mund të kthehet.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Shqyrtoni objektet e zgjedhura më poshtë. Mund të hiqni çdo objekt që nuk doni të rivendosni duke klikuar butonin e heqjes. Objektet do të rivendosen në vendndodhjen e tyre origjinale.", + "Revoke": "Revoko", + "Right": "Djathtas", + "Risk Level": "Niveli i Rrezikut", + "Risk level": "Niveli i rrezikut", + "Roles and Permissions": "Rolet dhe Lejet", + "Roles updated successfully": "Rolet u përditësuan me sukses", + "Run compliance scan": "Ekzekutoni skanimin e pajtueshmërisë", + "SOLR Connection Settings": "Cilësimet e Lidhjes SOLR", + "SOLR Index Warmup": "Ngrohja e Indeksit SOLR", + "SOLR Version:": "Versioni SOLR:", + "SOLR actions menu": "Menuja e veprimeve SOLR", + "SOLR base path (usually /solr)": "Shtegu bazë SOLR (zakonisht /solr)", + "SOLR collection deleted successfully": "Koleksioni SOLR u fshi me sukses", + "SOLR connection test failed: {error}": "Testimi i lidhjes SOLR dështoi: {error}", + "SOLR connection test successful!": "Testimi i lidhjes SOLR u krye me sukses!", + "SOLR index warmup completed successfully": "Ngrohja e indeksit SOLR u krye me sukses", + "SOLR is disabled": "SOLR është çaktivizuar", + "SOLR is not available or not configured": "SOLR nuk është i disponueshëm ose nuk është konfiguruar", + "SOLR logging disabled": "Regjistrimi SOLR çaktivizuar", + "SOLR logging enabled": "Regjistrimi SOLR aktivizuar", + "SOLR search disabled": "Kërkimi SOLR çaktivizuar", + "SOLR search enabled": "Kërkimi SOLR aktivizuar", + "SOLR server hostname or IP address": "Emri i serverit SOLR ose adresa IP", + "SOLR server port number (optional, defaults to 8983)": "Numri i portit të serverit SOLR (opsional, parazgjedhur 8983)", + "SOLR settings updated successfully": "Cilësimet SOLR u përditësuan me sukses", + "SOLR setup completed successfully": "Konfigurimi SOLR u krye me sukses", + "SOLR setup completed successfully!": "Konfigurimi SOLR u krye me sukses!", + "SOLR setup error: %s": "Gabim konfigurimi SOLR: %s", + "SOLR setup failed": "Konfigurimi SOLR dështoi", + "SOLR setup failed - check logs": "Konfigurimi SOLR dështoi - kontrolloni regjistrat", + "SOLR setup failed. Please check the configuration and try again.": "Konfigurimi SOLR dështoi. Ju lutemi kontrolloni konfigurimin dhe provoni përsëri.", + "SOLR setup failed: {error}": "Konfigurimi SOLR dështoi: {error}", + "SOLR warmup failed: {error}": "Ngrohja SOLR dështoi: {error}", + "Sample Values:": "Vlera Shembull:", + "Save": "Ruaj", + "Save Configuration": "Ruaj Konfigurimin", + "Save Connection Settings": "Ruaj Cilësimet e Lidhjes", + "Save Roles": "Ruaj Rolet", + "Save Settings": "Ruaj Cilësimet", + "Save changes": "Ruaj ndryshimet", + "Save current search as view": "Ruaj kërkimin aktual si pamje", + "Save labels": "Ruaj etiketat", + "Save tags for {name}": "Ruaj etiketat për {name}", + "Save the organisation first to manage users.": "Ruani organizatën fillimisht për të menaxhuar përdoruesit.", + "Saved Views": "Pamjet e Ruajtura", + "Saving...": "Duke ruajtur...", + "Scheduled": "Planifikuar", + "Schema": "Skemë", + "Schema ID": "ID e Skemës", + "Schema Name": "Emri i Skemës", + "Schema Statistics": "Statistikat e Skemës", + "Schema annotation": "Shënim skeme", + "Schema configuration updated successfully for {schema}": "Konfigurimi i skemës u përditësua me sukses për {schema}", + "Schema depublished successfully": "Skema u çpublikua me sukses", + "Schema objects deletion completed successfully": "Fshirja e objekteve të skemës u krye me sukses", + "Schema published successfully": "Skema u publikua me sukses", + "Schema reference ($ref)": "Referenca e skemës ($ref)", + "Schema successfully created": "Skema u krijua me sukses", + "Schema successfully updated": "Skema u përditësua me sukses", + "Schema {schema} removed successfully": "Skema {schema} u hoq me sukses", + "Schema:": "Skemë:", + "Schemas": "Skema", + "Scheme": "Skemë", + "Search": "Kërko", + "Search / Views": "Kërkim / Pamje", + "Search / views": "Kërkim / pamje", + "Search Activity": "Aktiviteti i Kërkimit", + "Search Analytics": "Analitika e Kërkimit", + "Search Configuration": "Konfigurimi i Kërkimit", + "Search Entities": "Kërko Entitete", + "Search Files": "Kërko Skedarë", + "Search GitHub": "Kërko në GitHub", + "Search GitLab": "Kërko në GitLab", + "Search Method": "Metoda e Kërkimit", + "Search Mode": "Modaliteti i Kërkimit", + "Search Query": "Pyetja e Kërkimit", + "Search Results": "Rezultatet e Kërkimit", + "Search Term": "Termi i Kërkimit", + "Search Trail Details": "Detajet e Gjurmës së Kërkimit", + "Search Trail Management": "Menaxhimi i Gjurmëve të Kërkimit", + "Search Trail Statistics": "Statistikat e Gjurmëve të Kërkimit", + "Search Trails": "Gjurmët e Kërkimit", + "Search Views": "Kërko Pamje", + "Search Webhooks": "Kërko Webhooks", + "Search agents...": "Kërko agjentë...", + "Search applications...": "Kërko aplikacione...", + "Search by file name or path": "Kërko sipas emrit të skedarit ose shtegjit", + "Search by name or URL": "Kërko sipas emrit ose URL", + "Search by title or UUID...": "Kërko sipas titullit ose UUID...", + "Search by value": "Kërko sipas vlerës", + "Search configurations": "Kërko konfigurime", + "Search data sources...": "Kërko burime të dhënash...", + "Search groups...": "Kërko grupe...", + "Search in Database Objects": "Kërko në Objektet e Bazës së Të dhënave", + "Search in Files": "Kërko në Skedarë", + "Search in Objects": "Kërko në Objekte", + "Search objects": "Kërko objekte", + "Search objects...": "Kërko objekte...", + "Search property names...": "Kërko emra pronash...", + "Search registers...": "Kërko regjistrar...", + "Search schemas...": "Kërko skema...", + "Search trail deletion not implemented yet": "Fshirja e gjurmëve të kërkimit nuk është zbatuar ende", + "Search views...": "Kërko pamje...", + "Search {name}...": "Kërko {name}...", + "Searching...": "Duke kërkuar...", + "Secret": "Sekret", + "Secret key for HMAC signature generation (optional)": "Çelësi sekret për gjenerimin e nënshkrimit HMAC (opsional)", + "Security": "Siguri", + "See organisations as a table": "Shikoni organizatat si tabelë", + "See organisations as cards": "Shikoni organizatat si karta", + "Select AI Agent": "Zgjidhni Agjentin AI", + "Select Active Organisation": "Zgjidhni Organizatën Aktive", + "Select All": "Zgjidhni Të gjitha", + "Select ConfigSet": "Zgjidhni ConfigSet", + "Select File Types to Vectorize:": "Zgjidhni Llojet e Skedarëve për Vectorizim:", + "Select HTTP method": "Zgjidhni metodën HTTP", + "Select Register and Schema": "Zgjidhni Regjistrin dhe Skemën", + "Select Views to Vectorize:": "Zgjidhni Pamjet për Vectorizim:", + "Select a Nextcloud group to add": "Zgjidhni një grup Nextcloud për të shtuar", + "Select a branch": "Zgjidhni një degë", + "Select a date property": "Zgjidhni një pronë date", + "Select a model or type a custom model name": "Zgjidhni një model ose shkruani një emër modeli të personalizuar", + "Select a register": "Zgjidhni një regjistër", + "Select a register...": "Zgjidhni një regjistër...", + "Select a repository": "Zgjidhni një depo", + "Select a repository you have write access to": "Zgjidhni një depo ku keni qasje shkrimi", + "Select a schema": "Zgjidhni një skemë", + "Select a schema...": "Zgjidhni një skemë...", + "Select a user": "Zgjidhni një përdorues", + "Select agent type": "Zgjidhni llojin e agjentit", + "Select all entities": "Zgjidhni të gjitha entitetet", + "Select an AI Agent": "Zgjidhni një Agjent AI", + "Select an AI agent to begin chatting with your data.": "Zgjidhni një agjent AI për të filluar bisedën me të dhënat tuaja.", + "Select an email to see linked objects": "Zgjidhni një email për të parë objektet e lidhura", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Zgjidhni një organizatë dhe përdorues për t'i shtuar si anëtar. Kërkoni organizata sipas emrit.", + "Select backend": "Zgjidhni backend", + "Select chat model": "Zgjidhni modelin e chat", + "Select collection for files": "Zgjidhni koleksionin për skedarë", + "Select collection for objects": "Zgjidhni koleksionin për objekte", + "Select configuration type...": "Zgjidhni llojin e konfigurimit...", + "Select default organisation": "Zgjidhni organizatën default", + "Select entity {value}": "Zgjidhni entitetin {value}", + "Select event to listen to...": "Zgjidhni ngjarjen për të dëgjuar...", + "Select fields to display": "Zgjidhni fushat për shfaqje", + "Select files with the correct extension": "Zgjidhni skedarë me zgjerimin e saktë", + "Select group": "Zgjidhni grupin", + "Select groups (optional)": "Zgjidhni grupet (opsional)", + "Select groups to notify...": "Zgjidhni grupet për njoftim...", + "Select groups with access to this agent": "Zgjidhni grupet me qasje te ky agjent", + "Select groups...": "Zgjidhni grupet...", + "Select model": "Zgjidhni modelin", + "Select one or more registers": "Zgjidhni një ose më shumë regjistrar", + "Select one or more schemas": "Zgjidhni një ose më shumë skema", + "Select options...": "Zgjidhni opsionet...", + "Select or create labels, or select \"No label\" to add files": "Zgjidhni ose krijoni etiketa, ose zgjidhni \"Pa etiketë\" për të shtuar skedarë", + "Select owner application (optional)...": "Zgjidhni aplikacionin pronar (opsional)...", + "Select period": "Zgjidhni periudhën", + "Select property to send as payload": "Zgjidhni pronën për ta dërguar si payload", + "Select provider": "Zgjidhni ofruesin", + "Select registers and schemas to save a view": "Zgjidhni regjistrar dhe skema për të ruajtur një pamje", + "Select retry policy": "Zgjidhni politikën e riprovimit", + "Select role": "Zgjidhni rolin", + "Select schemas (empty = all schemas)": "Zgjidhni skemat (bosh = të gjitha skemat)", + "Select schemas to warm up": "Zgjidhni skemat për ngrohje", + "Select search mode": "Zgjidhni modalitetin e kërkimit", + "Select source type...": "Zgjidhni llojin e burimit...", + "Select target type": "Zgjidhni llojin e synuar", + "Select the branch to publish to": "Zgjidhni degën për publikim", + "Select the event this webhook should listen to": "Zgjidhni ngjarjen që duhet të dëgjojë ky webhook", + "Select views to filter data (optional)": "Zgjidhni pamjet për filtrim të dhënash (opsional)", + "Select views to limit which data the agent can access": "Zgjidhni pamjet për të kufizuar cilat të dhëna mund të qaset agjenti", + "Select views to vectorize:": "Zgjidhni pamjet për vectorizim:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Zgjidhni cilat grupe Nextcloud janë të disponueshme për këtë organizatë. Përdoruesit në këto grupe do të kenë qasje në burimet e organizatës.", + "Select which data views the AI can search": "Zgjidhni cilat pamje të dhënash mund të kërkojë AI", + "Select which property from the event should be used as the webhook payload data": "Zgjidhni cilën pronë nga ngjarja duhet të përdoret si të dhëna payload të webhook", + "Select which tools the AI can use to perform actions": "Zgjidhni cilat mjete mund të përdorë AI për të kryer veprime", + "Select {fieldName} values": "Zgjidhni vlerat {fieldName}", + "Selected": "Zgjedhur", + "Selected Groups": "Grupet e Zgjedhura", + "Selected Objects ({count})": "Objektet e Zgjedhura ({count})", + "Selected audit trails deleted successfully": "Gjurmët e zgjedhura të auditimit u fshinë me sukses", + "Selected search trails deleted successfully": "Gjurmët e zgjedhura të kërkimit u fshinë me sukses", + "Selected users can access this view": "Përdoruesit e zgjedhur mund të qasen në këtë pamje", + "Send additional feedback": "Dërgoni reagime shtesë", + "Send as CloudEvent": "Dërgo si CloudEvent", + "Send as cloudevent": "Dërgo si cloudevent", + "Sensitive PII": "PII i Ndjeshëm", + "Serial Mode (Safer, slower)": "Modaliteti Serial (Më i sigurt, më i ngadaltë)", + "Serial:": "Serial:", + "Server Configuration": "Konfigurimi i Serverit", + "Server Information": "Informacioni i Serverit", + "Set as Active": "Vendosni si Aktiv", + "Set as active organisation": "Vendosni si organizatë aktive", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Vendosni kufij për përdorimin e API dhe konsumimin e token. Përdorni 0 për burime pa limit.", + "Settings": "Cilësime", + "Settings updated successfully": "Cilësimet u përditësuan me sukses", + "Shards": "Shards", + "Share": "Ndaj", + "Share with Groups": "Ndaj me Grupet", + "Share with Users": "Ndaj me Përdoruesit", + "Show": "Shfaq", + "Show Filters": "Shfaq Filtrat", + "Show less": "Shfaq më pak", + "Show only entries with changes": "Shfaq vetëm hyrjet me ndryshime", + "Showing {count} dashboard(s)": "Duke shfaqur {count} panel(e)", + "Showing {count} processing activities": "Duke shfaqur {count} aktivitete përpunimi", + "Showing {showing} of {total} agents": "Duke shfaqur {showing} nga {total} agjentë", + "Showing {showing} of {total} applications": "Duke shfaqur {showing} nga {total} aplikacione", + "Showing {showing} of {total} audit trail entries": "Duke shfaqur {showing} nga {total} hyrje gjurmësh auditimi", + "Showing {showing} of {total} configurations": "Duke shfaqur {showing} nga {total} konfigurime", + "Showing {showing} of {total} deleted items": "Duke shfaqur {showing} nga {total} artikuj të fshirë", + "Showing {showing} of {total} entities": "Duke shfaqur {showing} nga {total} entitete", + "Showing {showing} of {total} files": "Duke shfaqur {showing} nga {total} skedarë", + "Showing {showing} of {total} log entries": "Duke shfaqur {showing} nga {total} hyrje regjistri", + "Showing {showing} of {total} search trail entries": "Duke shfaqur {showing} nga {total} hyrje gjurmësh kërkimi", + "Showing {showing} of {total} sources": "Duke shfaqur {showing} nga {total} burime", + "Showing {showing} of {total} templates": "Duke shfaqur {showing} nga {total} shabllone", + "Showing {showing} of {total} webhooks": "Duke shfaqur {showing} nga {total} webhooks", + "Showing {shown} of {total} organisations": "Duke shfaqur {shown} nga {total} organizata", + "Simple": "I thjeshtë", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Pyetje të thjeshta: Kërkime bazë teksti me parametra minimal (p.sh., term i vetëm kërkimi, pa filtra të avancuar)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Pyetje të thjeshta: kërkime bazë teksti me parametra minimal (p.sh., term i vetëm kërkimi, pa filtra të avancuar)", + "Size": "Madhësi", + "Slug": "Slug", + "Soft Delete Mode": "Modaliteti i Fshirjes Butë", + "Soft Deleted Items": "Artikujt e Fshirë Butë", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Fshijini butë të gjitha objektet për këtë skemë ({active} aktive, {deleted} tashmë të fshirë)", + "SolrCloud mode enabled": "Modaliteti SolrCloud aktivizuar", + "Something went wrong while adding files": "Diçka shkoi keq gjatë shtimit të skedarëve", + "Source": "Burim", + "Source successfully updated": "Burimi u përditësua me sukses", + "Sources": "Burime", + "Sources:": "Burime:", + "Special Rights": "Të drejta Speciale", + "Standalone SOLR mode": "Modaliteti SOLR i pavarur", + "Start Conversation": "Filloni Bisedën", + "Start Date Field": "Fusha e Datës së Fillimit", + "Start Vectorization": "Filloni Vectorizimin", + "Start a conversation": "Filloni një bisedë", + "Starting names cache warmup...": "Duke filluar ngrohjen e cache të emrave...", + "Starting validation for {schema}...": "Duke filluar validimin për {schema}...", + "Starting {action} for {schema}...": "Duke filluar {action} për {schema}...", + "Starting...": "Duke filluar...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika dhe Metrika", + "Statistics refreshed": "Statistikat u rifreskuan", + "Stats calculation not yet implemented": "Llogaritja e statistikave nuk është zbatuar ende", + "Status": "Status", + "Status Code": "Kodi i Statusit", + "Step": "Hap", + "Storage": "Ruajtje", + "Storage Quota (MB)": "Kuota e Ruajtjes (MB)", + "String Constraints": "Kufizimet e Vargut", + "Subject identifier (email, BSN, name, etc.)": "Identifikuesi i subjektit (email, BSN, emër, etj.)", + "Subscribe": "Pajtohuni", + "Subscribed": "Pajtuar", + "Success": "Sukses", + "Success Rate": "Shkalla e Suksesit", + "Success Status": "Statusi i Suksesit", + "Successful": "I suksesshëm", + "Successfully added user to organisation: {name}": "Përdoruesi u shtua me sukses në organizatën: {name}", + "Successfully cleared {count} audit trails": "U pastruan me sukses {count} gjurmë auditimi", + "Successfully cleared {count} blob storage objects": "U pastruan me sukses {count} objekte ruajtje blob", + "Successfully cleared {count} search trails": "U pastruan me sukses {count} gjurmë kërkimi", + "Successfully created {count} SOLR fields": "U krijuan me sukses {count} fusha SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "U fshinë me sukses {count} embeddings. Ju lutemi rivektorizoni të dhënat tuaja.", + "Successfully deleted {count} objects for {schema}": "U fshinë me sukses {count} objekte për {schema}", + "Successfully fixed {count} SOLR field configurations": "U korrigjuan me sukses {count} konfigurime fushash SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "U importuan me sukses: {registers} regjistrar, {schemas} skema, {objects} objekte", + "Successfully joined organisation": "Organizata u bashkua me sukses", + "Successfully permanently deleted {count} objects": "U fshinë përgjithmonë me sukses {count} objekte", + "Successfully processed {count} files!": "U përpunuan me sukses {count} skedarë!", + "Successfully restored {count} object": "U rivendos me sukses {count} objekt", + "Successfully restored {count} objects": "U rivendosën me sukses {count} objekte", + "Successfully saved configuration for {count} facets!": "U ruajt me sukses konfigurimi për {count} facets!", + "Supported File Types": "Llojet e Skedarëve të Mbështetura", + "Switch Active Organisation": "Ndërroni Organizatën Aktive", + "Switch Organisation": "Ndërroni Organizatën", + "Sync Table": "Sinkronizo Tabelën", + "Synchronization Settings": "Cilësimet e Sinkronizimit", + "Syncing magic table for {schema}...": "Duke sinkronizuar tabelën magjike për {schema}...", + "Syncs": "Sinkronizime", + "System Default": "Parazgjedhja e Sistemit", + "System Prompt": "Urdhëri i Sistemit", + "System announcements": "Njoftime sistemi", + "Table": "Tabelë", + "Target ID": "ID e Synuar", + "Target Type*": "Lloji i Synuar*", + "Technical Description": "Përshkrim Teknik", + "Technical Details": "Detaje Teknike", + "Technical description for developers and administrators": "Përshkrim teknik për zhvilluesit dhe administratorët", + "Technical measures": "Masa teknike", + "Technische maatregelen": "Masa teknike", + "Temperature": "Temperaturë", + "Templates": "Shabllone", + "Test": "Test", + "Test Chat": "Testo Chat", + "Test Connection": "Testo Lidhjen", + "Test Embedding": "Testo Embedding", + "Test endpoint executed successfully": "Pikëfundi i testimit u ekzekutua me sukses", + "Test webhook delivered successfully": "Webhook-i i testimit u dërgua me sukses", + "Test webhook delivery failed": "Dërgimi i webhook-it të testimit dështoi", + "Test webhook sent successfully": "Webhook-i i testimit u dërgua me sukses", + "Testing...": "Duke testuar...", + "Text Chunk #{id}": "Copa Teksti #{id}", + "Text Extraction": "Nxjerrja e Tekstit", + "Text Extraction Engine": "Motori i Nxjerrjes së Tekstit", + "Text chunks": "Copa teksti", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Copat e tekstit gjenerohen gjatë nxjerrjes së skedarëve dhe ruhen në bazën e të dhënave. Vectorizimi i lexon këto skedarë të para-copëzuar dhe i konverton në embeddings.", + "Text extracted successfully": "Teksti u nxor me sukses", + "Text extraction disabled": "Nxjerrja e tekstit çaktivizuar", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplikacioni Nextcloud Calendar nuk është instaluar ose aktivizuar në këtë server.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplikacioni Nextcloud Contacts nuk është instaluar ose aktivizuar në këtë server.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplikacioni Nextcloud Deck nuk është instaluar ose aktivizuar në këtë server.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplikacioni Nextcloud Mail nuk është instaluar ose aktivizuar në këtë server.", + "The URL where webhook events will be sent": "URL ku do të dërgohen ngjarjet e webhook", + "The requested conversation does not exist": "Biseda e kërkuar nuk ekziston", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Paneli i kërkuar nuk mund të ngarkohej. Verifikoni URL ose zgjidhni një panel tjetër nga lista.", + "The saved view and all its search configuration": "Pamja e ruajtur dhe e gjithë konfigurimi i kërkimit të saj", + "There are no audit trail entries matching your current filters.": "Nuk ka hyrje gjurmësh auditimi që përputhen me filtrat tuaj aktualë.", + "There are no deleted items matching your current filters.": "Nuk ka artikuj të fshirë që përputhen me filtrat tuaj aktualë.", + "There are no search trail entries matching your current filters.": "Nuk ka hyrje gjurmësh kërkimi që përputhen me filtrat tuaj aktualë.", + "There are no webhook log entries matching your filters.": "Nuk ka hyrje regjistri webhook që përputhen me filtrat tuaj.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Ato do të mbahen sipas periudhës së konfiguruar të ruajtjes së skemës dhe fshihen automatikisht përgjithmonë kur periudha e ruajtjes skadon. Periudha e ruajtjes është e konfigurueshme për çdo skemë dhe mund të gjendet në cilësimet e skemës.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Ato do të mbahen sipas periudhës së konfiguruar të ruajtjes së skemës dhe më pas hiqen përgjithmonë.", + "This action cannot be undone.": "Ky veprim nuk mund të kthehet.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Ky veprim nuk mund të kthehet. Sigurohuni që asnjë koleksion nuk po përdor këtë ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Ky veprim nuk mund të kthehet. Sigurohuni që asnjë koleksion nuk po përdor këtë configset.", + "This action will submit a deactivation request to your administrators.": "Ky veprim do të dërgojë një kërkesë çaktivizimi tek administratorët tuaj.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Ky agjent nuk ka pamje ose mjete të konfigurueshme, por mund të rregulloni ende cilësimet RAG më poshtë.", + "This analysis may take some time": "Kjo analizë mund të marrë pak kohë", + "This audit trail entry does not contain any change information.": "Kjo hyrje gjurmës auditimi nuk përmban asnjë informacion ndryshimi.", + "This configuration has no data defined.": "Ky konfigurim nuk ka të dhëna të përcaktuara.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ky pikëfund është i vjetruar. Përdorni pikëfundte të bazuara në copa.", + "This entity has no relations to objects or files": "Ky entitet nuk ka lidhje me objekte ose skedarë", + "This is a test webhook from OpenRegister": "Ky është një webhook testimi nga OpenRegister", + "This is not valid JSON": "Ky nuk është JSON i vlefshëm", + "This key already exists on this schema": "Ky çelës ekziston tashmë në këtë skemë", + "This schema must use Magic Table configuration to sync": "Kjo skemë duhet të përdorë konfigurimin e Tabelës Magjike për sinkronizim", + "This schema must use magic table configuration to sync": "Kjo skemë duhet të përdorë konfigurimin e tabelës magjike për sinkronizim", + "This source has no associated registers.": "Ky burim nuk ka regjistrar të lidhura.", + "This token will only be shown once. Copy it now.": "Ky token do të tregohet vetëm një herë. Kopjojeni tani.", + "This will copy the _default ConfigSet with the new name": "Kjo do të kopjojë ConfigSet-in _default me emrin e ri", + "This will copy the _default configset with the new name": "Kjo do të kopjojë configset-in _default me emrin e ri", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Kjo do të fshijë përgjithmonë TË GJITHA embeddings (vektorët) nga baza e të dhënave. Do t'ju duhet të rivektorizoni të gjitha objektet dhe skedarët. Ky veprim nuk mund të kthehet.\n\nJeni i sigurt se doni të vazhdoni?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Kjo do të fshijë përgjithmonë TË GJITHA embeddings (vektorët) nga baza e të dhënave. Do t'ju duhet të rivektorizoni të gjitha objektet dhe skedarët. Ky veprim nuk mund të kthehet.\\n\\nJeni i sigurt se doni të vazhdoni?", + "This will permanently delete:": "Kjo do të fshijë përgjithmonë:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Kjo do të heqë përgjithmonë {count} objekte tashmë të fshira butë nga baza e të dhënave. Ky veprim nuk mund të kthehet.", + "Timeout (seconds)": "Koha e skadimit (sekonda)", + "Timestamp": "Vulë kohore", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Këshillë: Aktivizoni vetëm pamjet që kanë nevojë për kërkim semantik për të minimizuar kostot e embedding. Tabelat e thjeshta të shfletimit rrallë kanë nevojë për vectorizim.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Këshillë: aktivizoni vetëm pamjet që kanë nevojë për kërkim semantik për të minimizuar kostot e embedding. Tabelat e thjeshta të shfletimit rrallë kanë nevojë për vectorizim.", + "Title": "Titull", + "Title *": "Titull *", + "Title Template": "Shablloni i Titullit", + "Title*": "Titull*", + "To (e.g. -1 year or 2025-12-31)": "Deri (p.sh. -1 vit ose 2025-12-31)", + "To Date": "Deri në Datë", + "To add files larger than or equal to 512MB, go to the": "Për të shtuar skedarë me madhësi 512MB ose më shumë, shkoni te", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Për të rregulluar madhësinë e copës ose strategjinë, shkoni te Konfigurimi i Skedarit → Kufijtë e Përpunimit.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Për të rregulluar madhësinë e copës ose strategjinë, shkoni te konfigurimi i skedarit → kufijtë e përpunimit.", + "To change the embedding provider or model, go to LLM Configuration.": "Për të ndryshuar ofruesin e embedding ose modelin, shkoni te Konfigurimi LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Për të ndryshuar ofruesin e embedding ose modelin, shkoni te konfigurimi LLM.", + "To date": "Deri në datë", + "Toggle search sidebar": "Ndërroni shiritin anësor të kërkimit", + "Token Created": "Token i Krijuar", + "Token Quota (per request)": "Kuota e Token (për kërkesë)", + "Token copied to clipboard": "Token u kopjua në clipboard", + "Token name": "Emri i token", + "Token revoked": "Token u revokua", + "Tools": "Mjete", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Mjetet ekzekutohen me lejet e parazgjedhura të përdoruesit të agjentit kur nuk është aktiv asnjë sesion përdoruesi (p.sh., punët cron). Konfiguroni përdoruesin default në skedën Cilësime.", + "Tools respect the agent's own role and organization boundaries.": "Mjetet respektojnë rolin e vetë agjentit dhe kufijtë e organizatës.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Mjetet respektojnë pamjet, lejet dhe kufijtë e organizatës të agjentit.", + "Top Deleters": "Fshirësit Kryesorë", + "Total": "Total", + "Total Audit Trails": "Gjurmë Auditimi Totale", + "Total Chunks Available:": "Copa Totale të Disponueshme:", + "Total Deleted Items": "Artikuj Totale të Fshirë", + "Total Members": "Anëtarë Totalë", + "Total Objects": "Objekte Totale", + "Total Objects in Database:": "Objekte Totale në Bazën e Të Dhënave:", + "Total Organisations": "Organizata Totale", + "Total Results": "Rezultate Totale", + "Total Searches": "Kërkime Totale", + "Total Size": "Madhësia Totale", + "Total Storage": "Ruajtje Totale", + "Total events": "Ngjarje totale", + "Total:": "Total:", + "Totals": "Totale", + "Try searching by UUID or with different keywords": "Provoni të kërkoni sipas UUID ose me fjalë kyçe të ndryshme", + "Type": "Lloj", + "Type \"DELETE COLLECTION\" to confirm:": "Shkruani \"DELETE COLLECTION\" për të konfirmuar:", + "Type Issue": "Problem Lloji", + "Type Variations:": "Variacionet e Llojit:", + "Type filter (optional, e.g. email)": "Filtri i llojit (opsional, p.sh. email)", + "Type must be \"positive\" or \"negative\"": "Lloji duhet të jetë \"pozitiv\" ose \"negativ\"", + "Type to search for groups": "Shkruani për të kërkuar grupe", + "Type to search for objects...": "Shkruani për të kërkuar objekte...", + "Type to search for organisations": "Shkruani për të kërkuar organizata", + "Type to search for users": "Shkruani për të kërkuar përdorues", + "Type to search groups...": "Shkruani për të kërkuar grupe...", + "Type to search users...": "Shkruani për të kërkuar përdorues...", + "Type to search...": "Shkruani për të kërkuar...", + "Type your username to confirm": "Shkruani emrin e përdoruesit tuaj për të konfirmuar", + "URL": "URL", + "URL where Ollama is running": "URL ku po ekzekutohet Ollama", + "UUID copied to clipboard": "UUID u kopjua në clipboard", + "UUID:": "UUID:", + "Unchanged": "I pandryshuar", + "Unique Search Terms": "Terma Unikë Kërkimi", + "Unique Terms": "Terma Unikë", + "Unknown": "I panjohur", + "Unknown Object": "Objekt i Panjohur", + "Unknown Register": "Regjistër i Panjohur", + "Unknown Schema": "Skemë e Panjohur", + "Unknown error": "Gabim i panjohur", + "Unknown widget type: {type}": "Lloj widget i panjohur: {type}", + "Unlink email": "Shkëputni emailin", + "Unlink event": "Shkëputni ngjarjen", + "Unnamed Object": "Objekt pa Emër", + "Untitled": "Pa Titull", + "Untitled View": "Pamje pa Titull", + "Update": "Përditëso", + "Update Operations": "Operacionet e Përditësimit", + "Update available: {local} → {remote}": "Përditësim i disponueshëm: {local} → {remote}", + "Update configuration: ...": "Përditëso konfigurimin: ...", + "Update register OAS: ...": "Përditëso OAS të regjistrit: ...", + "Update vectors when object data changes (recommended for accurate search)": "Përditëso vektorët kur të dhënat e objektit ndryshojnë (rekomandohet për kërkim të saktë)", + "Updated": "Përditësuar", + "Updated At": "Përditësuar Në", + "Updated:": "Përditësuar:", + "Upload new avatar": "Ngarko avatar të ri", + "Uploading configuration sets...": "Duke ngarkuar grupet e konfigurimit...", + "Uptime:": "Kohëzgjatja aktive:", + "Url": "Url", + "Usage Count": "Numri i Përdorimit", + "Use AI agents for processing and analysis": "Përdorni agjentë AI për përpunim dhe analizë", + "Use Large Language Model features": "Përdorni veçoritë e Modelit të Gjuhës së Madhe", + "Use SolrCloud with Zookeeper for distributed search": "Përdorni SolrCloud me Zookeeper për kërkim të shpërndarë", + "Use SolrCloud with zookeeper for distributed search": "Përdorni SolrCloud me zookeeper për kërkim të shpërndarë", + "Use VNG APIs instead of MongoDB.": "Përdorni API-të VNG në vend të MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Përdorni vargjet default (7/30/90 ditët e fundit, Vitin e fundit, Më të vjetër)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Përdorni ruajtje të jashtme (p.sh. MongoDB) në vend të ruajtjes së brendshme Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Përdorni filtra për të ngushtuar hyrjet e gjurmëve të auditimit sipas regjistrit, skemës, llojit të veprimit, përdoruesit, intervalit të datave ose ID objektit.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Përdorni filtra për të ngushtuar artikujt e fshirë sipas regjistrit, skemës, datës së fshirjes ose përdoruesit që i fshiu.", + "Use filters to narrow down entities by type or category.": "Përdorni filtra për të ngushtuar entitetet sipas llojit ose kategorisë.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Përdorni filtra për të ngushtuar hyrjet e gjurmëve të kërkimit sipas regjistrit, skemës, statusit të suksesit, përdoruesit, intervalit të datave, termave të kërkimit ose metrikave të performancës.", + "Used By Collections": "Përdorur Nga Koleksionet", + "Used by": "Përdorur nga", + "User": "Përdorues", + "User Agent Statistics": "Statistikat e Agjentit Përdorues", + "User Description": "Përshkrimi i Përdoruesit", + "User Quota": "Kuota e Përdoruesit", + "User rate limits cleared successfully": "Kufizimet e shkallës së përdoruesit u pastruan me sukses", + "User removed successfully": "Përdoruesi u hoq me sukses", + "User-friendly description shown in forms and help text": "Përshkrim miqësor për përdoruesin i shfaqur në formularë dhe tekst ndihmës", + "Username": "Emri i Përdoruesit", + "Username for SOLR authentication (optional)": "Emri i përdoruesit për autentifikimin SOLR (opsional)", + "Users": "Përdorues", + "Uses": "Përdorimi", + "Using Pre-Generated Chunks": "Duke Përdorur Copa të Para-Gjeneruara", + "Validate": "Valido", + "Validate Objects": "Valido Objektet", + "Validation completed for {schema}": "Validimi u krye për {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validimi u krye për {schema}: {processed} të përpunuara, {updated} të përditësuara, {failed} dështuan", + "Validation completed successfully": "Validimi u krye me sukses", + "Validation failed": "Validimi dështoi", + "Value": "Vlerë", + "Value list (split on ,)": "Lista e vlerave (ndahet me ,)", + "Vector Dimensions": "Dimensionet Vektoriale", + "Vector Search Backend": "Backend i Kërkimit Vektorial", + "Vector field: _embedding_": "Fusha vektoriale: _embedding_", + "Vectorization Triggers": "Aktivizuesit e Vectorizimit", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vectorizimi u krye: {vectorized} objekte vectorizuara, {failed} dështuan", + "Vectorization failed: {error}": "Vectorizimi dështoi: {error}", + "Vectorize All Files": "Vectorizoni Të Gjithë Skedarët", + "Vectorize All Objects": "Vectorizoni Të Gjitha Objektet", + "Vectorize all file types": "Vectorizoni të gjitha llojet e skedarëve", + "Vectorize all views": "Vectorizoni të gjitha pamjet", + "Vectorize on object creation": "Vectorizoni gjatë krijimit të objektit", + "Vectors will be stored in your existing object and file collections": "Vektorët do të ruhen në koleksionet tuaja ekzistuese të objekteve dhe skedarëve", + "Verantwoording": "Llogaridhënie", + "Verifying server connectivity...": "Duke verifikuar lidhshmërinë e serverit...", + "Version": "Version", + "Version Information": "Informacioni i Versionit", + "Very High": "Shumë i Lartë", + "Very high": "Shumë i lartë", + "View": "Pamje", + "View API Docs": "Shikoni Dokument API", + "View API Documentation": "Shikoni Dokumentacionin API", + "View APIs": "Shikoni API-të", + "View Changes": "Shikoni Ndryshimet", + "View Details": "Shikoni Detajet", + "View Error": "Gabim Pamje", + "View File": "Shikoni Skedarin", + "View Full Details": "Shikoni Detajet e Plota", + "View Logs": "Shikoni Regjistrat", + "View Name": "Emri i Pamjes", + "View Object": "Shikoni Objektin", + "View Parameters": "Parametrat e Pamjes", + "View Selection": "Zgjedhja e Pamjes", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Shikoni dhe analizoni regjistrat e gjurmëve të kërkimit me aftësi filtrimi dhe analitike të avancuara", + "View and analyze system audit trails with advanced filtering capabilities": "Shikoni dhe analizoni gjurmët e auditimit të sistemit me aftësi filtrimi të avancuara", + "View and manage authorization across registers and schemas": "Shikoni dhe menaxhoni autorizimin nëpër regjistrar dhe skema", + "View deleted successfully": "Pamja u fshi me sukses", + "View entity details and manage relations": "Shikoni detajet e entitetit dhe menaxhoni lidhjet", + "View name": "Emri i pamjes", + "View name is required": "Emri i pamjes është i nevojshëm", + "View object": "Shikoni objektin", + "View saved successfully!": "Pamja u ruajt me sukses!", + "View search analytics and manage search logs": "Shikoni analitikën e kërkimit dhe menaxhoni regjistrat e kërkimit", + "View successfully deleted": "Pamja u fshi me sukses", + "View successfully updated": "Pamja u përditësua me sukses", + "View updated successfully!": "Pamja u përditësua me sukses!", + "View webhook delivery logs and filter by webhook": "Shikoni regjistrat e dërgimit të webhook dhe filtroni sipas webhook", + "View {count} more": "Shikoni {count} më shumë", + "Views": "Pamje", + "Visible to end users": "Dukshëm për përdoruesit fundorë", + "Visible to users": "Dukshëm për përdoruesit", + "Wait for Response": "Prisni Përgjigjen", + "Wait for webhook response before continuing (required for request/response flows)": "Prisni përgjigjen e webhook para se të vazhdoni (e nevojshme për flukset e kërkesës/përgjigjes)", + "Waiting for cluster synchronization...": "Duke pritur sinkronizimin e klusterit...", + "Warmup Names Cache": "Ngrohni Cache të Emrave", + "Warmup failed": "Ngrohja dështoi", + "Warning": "Paralajmërim", + "Warning:": "Paralajmërim:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Paralajmërim: Kjo pronë ka lloje të pasqendrueshme: {types}. Merrni parasysh nëse të gjitha objektet duhet të kenë të njëjtin lloj.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Do të skanojmë të gjitha objektet që i përkasin kësaj skeme për të zbuluar prona të reja dhe analizuar pronat ekzistuese për përmirësime të mundshme. Procesi përfshin shqyrtimin e strukturës së të dhënave të çdo objekti, identifikimin e pronave të papërcaktuara në skemën aktuale dhe gjetjen e mundësive për të përmirësuar përkufizimet ekzistuese të pronave me kufizime, formate dhe rregulla validimi më të mira.", + "Webhook": "Webhook", + "Webhook Log Details": "Detajet e Regjistrit të Webhook", + "Webhook Logs": "Regjistrat e Webhook", + "Webhook created successfully": "Webhook u krijua me sukses", + "Webhook deleted": "Webhook u fshi", + "Webhook retry delivered successfully": "Riprova e webhook u dërgua me sukses", + "Webhook retry delivery failed": "Dërgimi i riprovës së webhook dështoi", + "Webhook updated": "Webhook u përditësua", + "Webhook updated successfully": "Webhook u përditësua me sukses", + "Webhooks": "Webhooks", + "Weekly": "Çdo javë", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kur agjenti ekzekutohet pa sesion përdoruesi (p.sh., detyra të planifikuara), do të përdoret konteksti i këtij përdoruesi", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kur agjenti ekzekutohet pa sesion përdoruesi (p.sh., detyra të planifikuara), do të përdoret konteksti i këtij përdoruesi.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Kur çaktivizuar, ky facet do të tregojë vetëm vlera nga kjo skemë dhe do të përfshijë një filtër skeme kur zgjidhet.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Kur aktivizuar, ruajtja e këtij objekti do të përditësojë gjithashtu objektet e synuara për të përfshirë një referencë kthyese te ky objekt.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Kur aktivizuar, kjo pronë do të hiqet nga objekti burim pasi të përditësohen objektet e synuara.", + "Workflow Management": "Menaxhimi i Flukseve të Punës", + "Workflows": "Flukse Pune", + "Wrap webhook payload in CloudEvents format for better interoperability": "Mbëshillni payload-in e webhook në formatin CloudEvents për ndërveprim më të mirë", + "Wrap webhook payload in cloudevents format for better interoperability": "Mbëshillni payload-in e webhook në formatin cloudevents për ndërveprim më të mirë", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: vlerë\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: vlerë\\nAuthorization: Bearer token", + "Yes": "Po", + "You": "Ju", + "You are not yet a member of any organisations.": "Ende nuk jeni anëtar i asnjë organizate.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Mund të krijoni ConfigSets të rinj bazuar në shablonin _default, ose ngarkoni ato të personalizuara drejtpërdrejt në serverin tuaj SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Mund të krijoni configsets të rinj bazuar në shablonin _default, ose ngarkoni ato të personalizuara drejtpërdrejt në serverin tuaj SOLR.", + "You do not have access to this conversation": "Nuk keni qasje në këtë bisedë", + "You do not have permission to delete this conversation": "Nuk keni leje të fshini këtë bisedë", + "You do not have permission to modify this conversation": "Nuk keni leje të modifikoni këtë bisedë", + "You do not have permission to restore this conversation": "Nuk keni leje të rivendosni këtë bisedë", + "You do not have permission to view the permission matrix. Admin access is required.": "Nuk keni leje të shikoni matricën e lejeve. Kërkohet qasje admin.", + "You have selected {count} properties to add to the schema.": "Keni zgjedhur {count} prona për të shtuar në skemë.", + "You must be logged in to favorite views": "Duhet të jeni i identifikuar për të preferuar pamjet", + "You need an AI agent to start a conversation.": "Ju nevojitet një agjent AI për të filluar një bisedë.", + "Your Fireworks AI API key. Get one at": "Çelësi juaj API Fireworks AI. Merreni në", + "Your OpenAI API key. Get one at": "Çelësi juaj API OpenAI. Merreni në", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Reagimet tuaja janë regjistruar. Opsionalisht, mund të ofroni detaje shtesë këtu...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Reagimet tuaja janë regjistruar. opsionalisht, mund të ofroni detaje shtesë këtu...", + "Zookeeper Hosts": "Pritësit Zookeeper", + "Zookeeper Port": "Porti Zookeeper", + "Zookeeper Settings (SolrCloud)": "Cilësimet Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Vargu i lidhjes Zookeeper për SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Numri i portit Zookeeper (opsional, parazgjedhur 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F PARALAJMËRIM FSHIRJEJE TË PËRHERSHME \\u26A0\\uFE0F\n\nJeni gati të FSHINI PËRGJITHMONË TË GJITHA objektet për skemën \"{schema}\":\n\n\\u2022 Objekte aktive: {active}\n\\u2022 Objekte të fshira butë: {deleted}\n\\u2022 Total: {total}\n\nKëto objekte do të hiqen plotësisht nga baza e të dhënave dhe NUK MUND TË RIKUPEROHEN.\n\nJeni absolutisht i sigurt?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F PARALAJMËRIM FSHIRJEJE TË PËRHERSHME \\u26A0\\uFE0F\n\nJeni gati të FSHINI PËRGJITHMONË {count} objekte të fshira butë për skemën \"{schema}\".\n\nKëto objekte do të hiqen plotësisht nga baza e të dhënave dhe NUK MUND TË RIKUPEROHEN.\n\nJeni absolutisht i sigurt?", + "and accessible to all users in selected groups (or all users if no groups selected).": "dhe të arritshme për të gjithë përdoruesit në grupet e zgjedhura (ose të gjithë përdoruesit nëse nuk janë zgjedhur grupe).", + "and add the files there.": "dhe shtoni skedarët atje.", + "approved": "miratuar", + "are only accessible to invited users.": "janë të arritshme vetëm për përdoruesit e ftuar.", + "by": "nga", + "chunk": "copë", + "chunks": "copa", + "chunks processed": "copa të përpunuara", + "completed": "të përfunduara", + "configuration(s)": "konfigurim(e)", + "conversationId is required": "conversationId është i nevojshëm", + "create": "krijo", + "delete": "fshi", + "deleted objects section": "seksioni i objekteve të fshirë", + "e.g., lib/Settings/config.json": "p.sh., lib/Settings/config.json", + "e.g., lib/Settings/register.json": "p.sh., lib/Settings/register.json", + "e.g., lib/settings/config.json": "p.sh., lib/settings/config.json", + "e.g., lib/settings/register.json": "p.sh., lib/settings/register.json", + "execute": "ekzekuto", + "failed": "dështoi", + "file": "skedar", + "files": "skedarë", + "file{plural}": "skedar{plural}", + "folder": "dosje", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "në {register}", + "in_progress": "në progres", + "inherited": "trashëguar", + "list": "listë", + "log{plural}": "regjistër{plural}", + "manage": "menaxho", + "members": "anëtarë", + "menu or contact someone with permission to create agents.": "menusë ose kontaktoni dikë me leje për të krijuar agjentë.", + "message content is required": "përmbajtja e mesazhit është e nevojshme", + "messages": "mesazhe", + "missing": "mungon", + "myapp": "myapp", + "n8n URL and API key are required": "URL dhe çelësi API i n8n janë të nevojshëm", + "n8n configuration saved successfully": "Konfigurimi n8n u ruajt me sukses", + "n8n connection not configured": "Lidhja n8n nuk është konfiguruar", + "n8n connection successful": "Lidhja n8n u krye me sukses", + "n8n connection test failed: {message}": "Testimi i lidhjes n8n dështoi: {message}", + "n8n connection test successful": "Testimi i lidhjes n8n u krye me sukses", + "n8n initialization failed: {message}": "Inicializimi n8n dështoi: {message}", + "n8n integration disabled": "Integrimi n8n çaktivizuar", + "n8n integration enabled": "Integrimi n8n aktivizuar", + "n8n project initialized successfully": "Projekti n8n u inicializua me sukses", + "n8n settings saved successfully": "Cilësimet n8n u ruajtën me sukses", + "notify_push configuration guide": "udhëzuesi i konfigurimit notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push është instaluar por OpenRegister nuk ka konfirmuar ende një push të suksesshëm. Aktivizoni ruajtjen e objektit për ta aktivizuar, ose kontrolloni konfigurimin tuaj notify_push.", + "notify_push is installed but not yet active": "notify_push është instaluar por ende nuk është aktiv", + "objectType: object\naction: created": "objectType: objekt\naction: krijuar", + "objectType: object\\naction: created": "objectType: objekt\\naction: krijuar", + "objects": "objekte", + "objects processed": "objekte të përpunuara", + "object{plural}": "objekt{plural}", + "or pick a range": "ose zgjidhni një varg", + "org-...": "org-...", + "pending": "në pritje", + "provides a good balance between speed and accuracy.": "ofron një ekuilibër të mirë midis shpejtësisë dhe saktësisë.", + "public": "publik", + "read": "lexo", + "register(s)": "regjistër(e)", + "register(s) selected": "regjistër(e) të zgjedhur", + "register{plural}": "regjistër{plural}", + "rejected": "refuzuar", + "required": "i nevojshëm", + "results": "rezultate", + "schema(s)": "skemë(a)", + "schema(s) selected": "skemë(a) të zgjedhura", + "schema{plural}": "skemë{plural}", + "selected objects": "objekte të zgjedhura", + "set": "vendosni", + "sk-...": "sk-...", + "this application": "ky aplikacion", + "to": "te", + "type must be \"positive\" or \"negative\"": "lloji duhet të jetë \"pozitiv\" ose \"negativ\"", + "update": "përditëso", + "vectorized": "vectorizuar", + "view": "pamje", + "views selected": "pamje të zgjedhura", + "waiting": "duke pritur", + "{active} active, {deleted} deleted": "{active} aktive, {deleted} të fshirë", + "{count} audit trails cleared successfully": "{count} gjurmë auditimi u pastruan me sukses", + "{count} compliance issue(s) detected": "{count} problem(e) pajtueshmërie të zbuluar", + "{count} deletions": "{count} fshirje", + "{count} documents found": "{count} dokumente të gjetura", + "{count} email": "{count} email", + "{count} emails": "{count} email", + "{count} entries": "{count} hyrje", + "{count} issues": "{count} probleme", + "{count} members": "{count} anëtarë", + "{count} searches": "{count} kërkime", + "{count} selected": "{count} të zgjedhura", + "{count} widget(s)": "{count} widget(e)", + "{matched} object(s) matched. {erased} erased.": "{matched} objekt(e) të përputhura. {erased} të fshirë.", + "{minutes} minutes ago": "{minutes} minuta më parë", + "{percentage}% of objects": "{percentage}% e objekteve", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} në {register} / {schema}", + "{type} cache cleared successfully": "Cache e {type} u pastrua me sukses", + "ℹ️ Current Configuration": "ℹ️ Konfigurimi Aktual", + "ℹ️ current configuration": "ℹ️ konfigurimi aktual", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ RREZIK: Të gjitha {total} objektet do të FSHIHEN PËRGJITHMONË nga baza e të dhënave. Ky veprim është I PARIKUPERUESHËM dhe nuk mund të kthehet!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ PARALAJMËRIM FSHIRJEJE TË PËRHERSHME ⚠️\n\nJeni gati të FSHINI PËRGJITHMONË TË GJITHA objektet për skemën \"{schema}\":\n\n• Objekte aktive: {active}\n• Objekte të fshira butë: {deleted}\n• Total: {total}\n\nKëto objekte do të hiqen plotësisht nga baza e të dhënave dhe NUK MUND TË RIKUPEROHEN.\n\nJeni absolutisht i sigurt?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ PARALAJMËRIM FSHIRJEJE TË PËRHERSHME ⚠️\n\nJeni gati të FSHINI PËRGJITHMONË {count} objekte të fshira butë për skemën \"{schema}\".\n\nKëto objekte do të hiqen plotësisht nga baza e të dhënave dhe NUK MUND TË RIKUPEROHEN.\n\nJeni absolutisht i sigurt?", + "⚡ Batch Processing": "⚡ Përpunim në Grupe", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR mund të trajtojë miliona dokumente me kohë reagimi kërkimi nën një sekondë...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Kontekst gjithëpërfshirës ✓ Më pak të ngjarë të humbasë detaje ✗ Përgjigje më të ngadalta ✗ Mund të përfshijë informacion më pak relevant", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Përgjigje më të shpejta ✓ Përgjigje më të fokusuara ✗ Mund të humbasë informacion relevant", + "✨ AI Features": "✨ Veçoritë AI", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Në modalitetin e shpërndarë, SOLR përdor ZooKeeper për koordinimin e klusterit...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Kërkimi me facet lejon përdoruesit të gërmojnë rezultate sipas kategorive...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 A dini? SOLR fuqizon kërkimin për Netflix, Apple dhe shumë sajte të tjera të mëdha!", + "💰 View Selection (Cost Optimization)": "💰 Zgjedhja e Pamjes (Optimizimi i Kostove)", + "📄 Object Serialization": "📄 Serializimi i Objektit", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR ofron analitikë dhe statistika të pasura rreth performancës së kërkimit...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets përmbajnë skemat dhe skedarët e konfigurimit për indeksin tuaj të kërkimit...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikimi siguron që indeksi juaj i kërkimit të jetë i disponueshëm edhe nëse nyjet dështojnë...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR është një platformë e fuqishme kërkimi enterprise e ndërtuar mbi Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Cilësimet e Vectorizimit", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Shkallëzimi automatik mund të shtojë ose heqë nyjet dinamikisht bazuar në ngarkesë...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Veçoritë e sigurisë përfshijnë autentifikimin, autorizimin dhe enkriptimin SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} email", + "{count} email" + ], + "Purge {count} object from database": [ + "Pastro {count} objekt nga baza e të dhënave", + "Pastro {count} objekte nga baza e të dhënave" + ], + "Restore {count} object": [ + "Ripristino {count} objekti", + "Ripristino {count} objekte" + ], + "Successfully restored {count} object": [ + "{count} objekt u ripristinua me sukses", + "{count} objekte u ripristinuan me sukses" + ], + "Delete {count} object": [ + "Fshi {count} objekt", + "Fshi {count} objekte" + ], + "Object successfully deleted": [ + "Objekti u fshi me sukses", + "Objektet u fshinë me sukses" + ] + } +} diff --git a/l10n/sr.js b/l10n/sr.js new file mode 100644 index 0000000000..6431447e3d --- /dev/null +++ b/l10n/sr.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(без наслова)", + "(no title)" : "(без наслова)", + "(unnamed)" : "(неименовано)", + "(untitled card)" : "(картица без наслова)", + "Activities" : "Активности", + "Add Endpoint" : "Додај крајњу тачку", + "Add Source" : "Додај извор", + "Add card" : "Додај картицу", + "Add contact" : "Додај контакт", + "All clear" : "Све је у реду", + "Articles" : "Чланци", + "Code" : "Код", + "Compliance" : "Усаглашеност", + "Contacts" : "Контакти", + "Create Agent" : "Креирај агента", + "Create Application" : "Креирај апликацију", + "Create event" : "Креирај догађај", + "Erasure complete" : "Брисање завршено", + "Erasure preview" : "Преглед брисања", + "Expand details" : "Прошири детаље", + "Save changes" : "Сачувај измене", + "📄 Object Serialization" : "📄 Серијализација Објекта", + "🔢 Vectorization Settings" : "🔢 Подешавања векторизације", + "💰 View Selection (Cost Optimization)" : "💰 Избор приказа (оптимизација трошкова)", + "AI Assistant" : "AI асистент", + "AI Chat" : "AI ћаскање", + "API Key" : "API кључ", + "API tokens saved successfully" : "API токени су успешно сачувани", + "About ConfigSets" : "О ConfigSet-овима", + "Action" : "Радња", + "Action Distribution" : "Расподела радњи", + "Action:" : "Радња:", + "Actions" : "Радње", + "Active" : "Активно", + "Active Collections" : "Активне колекције", + "Active Filters" : "Активни филтери", + "Active Organisations" : "Активне организације", + "Active filters:" : "Активни филтери:", + "Active organisation set successfully" : "Активна организација је успешно постављена", + "Activity Period" : "Период активности", + "Add Groups" : "Додај групе", + "Add Schema" : "Додај Шему", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Додајте наслове Шема, описе и информације о Регистру да бисте обезбедили богатији контекст за претрагу", + "Add to favorites" : "Додај у омиљено", + "Added" : "Додато", + "Added to favorites" : "Додато у омиљено", + "Additional Information" : "Додатне информације", + "Additional feedback saved. Thank you!" : "Додатне повратне информације су сачуване. Хвала вам!", + "Admin" : "Администратор", + "Advanced" : "Напредно", + "Advanced Filters" : "Напредни филтери", + "Advanced Options" : "Напредне опције", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Напредни филтери нису доступни када се користи извор базе података. Пређите на Аутоматски или SOLR Индекс за опције филтрирања.", + "After" : "После", + "Agent deleted successfully" : "Агент је успешно обрисан", + "Agents" : "Агенти", + "All" : "Све", + "All Categories" : "Све категорије", + "All Collections" : "Све колекције", + "All Confidence Levels" : "Сви нивои поузданости", + "All ConfigSets" : "Сви ConfigSet-ови", + "All Files" : "Све Датотеке", + "All Levels" : "Сви нивои", + "All Types" : "Сви типови", + "All Webhooks" : "Сви веб-кукице", + "All actions" : "Све радње", + "All audit trails cleared successfully" : "Сви ревизорски трагови су успешно обрисани", + "All registers" : "Сви Регистри", + "All schemas" : "Све Шеме", + "All searches" : "Све претраге", + "All users" : "Сви корисници", + "An error occurred while clearing audit trails" : "Дошло је до грешке приликом брисања ревизорских трагова", + "An error occurred while deleting the audit trail" : "Дошло је до грешке приликом брисања ревизорског трага", + "An error occurred while deleting the view" : "Дошло је до грешке приликом брисања приказа", + "An error occurred while permanently deleting the objects" : "Дошло је до грешке приликом трајног брисања Објеката", + "Analysis completed:" : "Анализа завршена:", + "Analysis steps:" : "Кораци анализе:", + "Analytics" : "Аналитика", + "Analyze Objects" : "Анализирај Објекте", + "Analyze Properties" : "Анализирај Својства", + "Analyze existing properties for improvement opportunities" : "Анализирајте постојећа Својства за могућности побољшања", + "Analyzing..." : "Анализирање...", + "Any favorites and sharing settings for this view" : "Сва омиљена и подешавања дељења за овај приказ", + "Any user" : "Било који корисник", + "App store cache invalidated successfully" : "Кеш продавнице апликација је успешно поништен", + "Application deleted successfully" : "Апликација је успешно обрисана", + "Applications" : "Апликације", + "Apply Changes" : "Примени измене", + "Archive" : "Архивирај", + "Archive conversation" : "Архивирај разговор", + "Archived conversations are hidden from your active list" : "Архивирани разговори су сакривени са ваше активне листе", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Да ли сте сигурни да желите да очистите старе трагове претраге? Ово ће обрисати уносе старије од 30 дана.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Да ли сте сигурни да желите да обришете изабране ревизорске трагове? Ова радња се не може опозвати.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Да ли сте сигурни да желите да обришете изабране трагове претраге? Ова радња се не може опозвати.", + "Are you sure you want to delete this ConfigSet?" : "Да ли сте сигурни да желите да обришете овај ConfigSet?", + "Are you sure you want to permanently delete" : "Да ли сте сигурни да желите да трајно обришете", + "Ask a question..." : "Поставите питање...", + "Ask questions about your data using natural language" : "Постављајте питања о вашим подацима користећи природни језик", + "Attempt" : "Покушај", + "Audit Trail Changes" : "Измене ревизорског трага", + "Audit Trail Details" : "Детаљи ревизорског трага", + "Audit Trail Management" : "Управљање ревизорским траговима", + "Audit Trail Statistics" : "Статистика ревизорских трагова", + "Audit Trails" : "Ревизорски трагови", + "Audit trail data copied to clipboard" : "Подаци ревизорског трага су копирани у привремену меморију", + "Audit trail deleted successfully" : "Ревизорски траг је успешно обрисан", + "Audit trail successfully deleted" : "Ревизорски траг је успешно обрисан", + "Authentication" : "Потврда идентитета", + "Auto-commit disabled" : "Аутоматско потврђивање је онемогућено", + "Auto-commit enabled" : "Аутоматско потврђивање је омогућено", + "Auto-create Default Organisation" : "Аутоматски креирај подразумевану организацију", + "Auto-retry failed vectorizations" : "Аутоматски поново покушај неуспеле векторизације", + "Automatically commit changes to SOLR index" : "Аутоматски потврди измене у SOLR индексу", + "Automatically create a default organisation if none exists when the app is initialized" : "Аутоматски креирај подразумевану организацију ако ниједна не постоји када се апликација иницијализује", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Аутоматски генериши векторска уграђивања из текстуалних делова када се Датотеке отпреме и обраде", + "Automatically generate vector embeddings when objects are created or updated" : "Аутоматски генериши векторска уграђивања када се Објекти креирају или ажурирају", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Аутоматски поново покушај неуспеле покушаје векторизације (највише 3 покушаја)", + "Available Filters" : "Доступни филтери", + "Available Workflows" : "Доступни радни токови", + "Avg Execution Time" : "Просечно време извршавања", + "Avg Members/Org" : "Просечно чланова/орг.", + "Avg Object Views/Session" : "Просечно прегледа Објеката/сесији", + "Avg Response Time" : "Просечно време одговора", + "Avg Results/Search" : "Просечно резултата/претрази", + "Avg Searches/Session" : "Просечно претрага/сесији", + "Back" : "Назад", + "Back to Entities" : "Назад на ентитете", + "Back to Registers" : "Назад на Регистре", + "Back to Webhooks" : "Назад на веб-кукице", + "Back to applications" : "Назад на апликације", + "Back to entities" : "Назад на ентитете", + "Backend updated successfully. Please reload the application." : "Позадински систем је успешно ажуриран. Поново учитајте апликацију.", + "Base URL (Optional)" : "Основни URL (опционо)", + "Basic Information" : "Основне информације", + "Batch Size" : "Величина групе", + "Batch extraction completed" : "Групно издвајање је завршено", + "Before" : "Пре", + "Before object vectorization can work:" : "Пре него што векторизација Објеката може да ради:", + "Behavior Issue" : "Проблем понашања", + "Bookmarks" : "Обележивачи", + "Blob storage has been retired. All objects now use magic tables." : "Blob складиште је повучено. Сви Објекти сада користе магичне табеле.", + "Branch" : "Грана", + "Bulk delete operation completed successfully" : "Операција групног брисања је успешно завршена", + "Bulk save operation completed successfully" : "Операција групног чувања је успешно завршена", + "Business Data" : "Пословни подаци", + "Cache cleared successfully" : "Кеш је успешно обрисан", + "Calculate Sizes" : "Израчунај величине", + "Cancel" : "Откажи", + "Cannot delete: objects are still attached" : "Није могуће обрисати: Објекти су још увек приложени", + "Category" : "Категорија", + "Change Type" : "Тип измене", + "Changes" : "Измене", + "Changes copied to clipboard" : "Измене су копиране у привремену меморију", + "Chat Model" : "Модел ћаскања", + "Chat Provider" : "Провајдер ћаскања", + "Chat Provider (RAG)" : "Провајдер ћаскања (RAG)", + "Chat Settings" : "Подешавања ћаскања", + "Chat provider connection successful!" : "Веза са провајдером ћаскања је успешна!", + "Chat settings" : "Подешавања ћаскања", + "Choose a register" : "Изаберите Регистар", + "Choose a schema" : "Изаберите Шему", + "Choose how vector similarity calculations are performed for semantic search" : "Изаберите како се обављају прорачуни векторске сличности за семантичку претрагу", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Изаберите које типове Датотека укључити у процес векторизације. Биће обрађене само Датотеке са издвојеним текстом и деловима.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Изаберите које приказе укључити у процес векторизације. Оставите празно да бисте обрадили све приказе на основу ваше конфигурације.", + "Chunk" : "Део", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Брисање делова још увек није имплементирано. Користите крајње тачке засноване на деловима.", + "Chunks" : "Делови", + "Chunks to Vectorize:" : "Делови за векторизацију:", + "Cleanup Old Trails" : "Очисти старе трагове", + "Cleanup completed" : "Чишћење је завршено", + "Clear All Embeddings" : "Обриши сва уграђивања", + "Clear Entries" : "Обриши уносе", + "Clear Filtered Audit Trails" : "Обриши филтриране ревизорске трагове", + "Clear Filters" : "Обриши филтере", + "Clear Index" : "Обриши индекс", + "Clear Selection" : "Обриши избор", + "Clear all" : "Обриши све", + "Clear all filters" : "Обриши све филтере", + "Clear date range" : "Обриши опсег датума", + "Clearing..." : "Брисање...", + "Close" : "Затвори", + "Collection Management" : "Управљање колекцијама", + "Collection Name" : "Назив колекције", + "Collection assignments updated successfully" : "Додели колекција су успешно ажуриране", + "Collection cleared successfully" : "Колекција је успешно обрисана", + "Collection copied successfully" : "Колекција је успешно копирана", + "Collection created successfully" : "Колекција је успешно креирана", + "Collection deleted successfully" : "Колекција је успешно обрисана", + "Collection used to store and index file metadata and content" : "Колекција која се користи за чување и индексирање метаподатака и садржаја Датотека", + "Collection used to store and index object data" : "Колекција која се користи за чување и индексирање података Објеката", + "Collections:" : "Колекције:", + "Commit Message" : "Порука потврде", + "Commit Within (ms)" : "Потврди у року од (ms)", + "Commit message" : "Порука потврде", + "Compare current schema with real object data" : "Упоредите тренутну Шему са стварним подацима Објеката", + "Completed" : "Завршено", + "Complex" : "Сложено", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Сложени упити: Напредне претраге са више филтера, оператора и сложених комбинација параметара", + "Confidence Level" : "Ниво поузданости", + "Confidence Score" : "Оцена поузданости", + "Config must be provided as an object" : "Конфигурација мора бити обезбеђена као Објекат", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Управљање ConfigSet-овима", + "ConfigSet Name" : "Назив ConfigSet-а", + "ConfigSet created successfully" : "ConfigSet је успешно креиран", + "ConfigSet deleted successfully" : "ConfigSet је успешно обрисан", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSet-ови дефинишу Шему и конфигурацију за ваше SOLR колекције. Они садрже дефиниције поља, анализаторе и друга подешавања претраге.", + "Configuration" : "Конфигурација", + "Configuration Data" : "Подаци конфигурације", + "Configuration saved" : "Конфигурација је сачувана", + "Configurations" : "Конфигурације", + "Configure Facets" : "Конфигуриши фасете", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Конфигуришите провајдере великих језичких модела (LLM) за функције вођене вештачком интелигенцијом, укључујући семантичку претрагу, уграђивања и ћаскање.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Конфигуришите основна подешавања везе за ваш SOLR сервер, укључујући потврду идентитета и мрежне опције. Користите засебне дијалоге за управљање ConfigSet-овима и колекцијама да бисте управљали језгрима и колекцијама.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Конфигуришите како се Објекти базе података претварају у векторска уграђивања за семантичку претрагу. Објекти се директно векторизују без потребе за издвајањем текста.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Конфигуришите како се Објекти претварају у текст пре векторизације. Ова подешавања утичу на квалитет претраге и контекст.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Конфигуришите параметре за векторизацију Датотека. Овај процес ће генерисати векторска уграђивања за све издвојене делове Датотека.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Конфигуришите параметре за векторизацију Објеката. Овај процес ће генерисати векторска уграђивања за све Објекте који одговарају вашим филтерима приказа.", + "Configure which types of data to search and how many sources to retrieve" : "Конфигуришите које типове података да претражујете и колико Извора да преузмете", + "Connection Failed" : "Веза није успела", + "Connection Settings" : "Подешавања везе", + "Connection Successful!" : "Веза успешна!", + "Connection failed" : "Веза није успела", + "Connection protocol" : "Протокол везе", + "Connection successful" : "Веза успешна", + "Connection timeout in seconds" : "Истек везе у секундама", + "Constant delay between retries (5 minutes)" : "Константно кашњење између покушаја (5 минута)", + "Constraint Issue" : "Проблем ограничења", + "Content Filters" : "Филтери садржаја", + "Continue to Properties" : "Настави на Својства", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Контролишите који прикази Објеката треба да буду векторизовани да бисте смањили API трошкове. Векторизујте само приказе који имају користи од семантичке претраге.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Контролишите које приказе и алате AI може да користи у овом разговору. Подразумевано су омогућене све могућности агента.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Контролише насумичност (0 = детерминистички, 2 = веома креативно)", + "Conversation ID is required" : "ИД разговора је обавезан", + "Conversation archived" : "Разговор је архивиран", + "Conversation archived successfully" : "Разговор је успешно архивиран", + "Conversation cleared successfully" : "Разговор је успешно обрисан", + "Conversation deleted" : "Разговор је обрисан", + "Conversation permanently deleted" : "Разговор је трајно обрисан", + "Conversation renamed" : "Разговор је преименован", + "Conversation restored" : "Разговор је враћен", + "Conversation title" : "Наслов разговора", + "Copied!" : "Копирано!", + "Copy" : "Копирај", + "Copy Changes" : "Копирај измене", + "Copy Collection" : "Копирај колекцију", + "Copy Data" : "Копирај податке", + "Copy Full Data" : "Копирај све податке", + "Copying..." : "Копирање...", + "Count" : "Број", + "Counting objects..." : "Бројање Објеката...", + "Costs" : "Трошкови", + "Create" : "Креирај", + "Create Collection" : "Креирај колекцију", + "Create ConfigSet" : "Креирај ConfigSet", + "Create New Collection" : "Креирај нову колекцију", + "Create New ConfigSet" : "Креирај нови ConfigSet", + "Create Operations" : "Операције креирања", + "Create Webhook" : "Креирај веб-кукицу", + "Create a copy of collection:" : "Креирај копију колекције:", + "Create a new ConfigSet based on the _default template" : "Креирај нови ConfigSet на основу _default шаблона", + "Create a new SOLR collection from an existing ConfigSet" : "Креирај нову SOLR колекцију из постојећег ConfigSet-а", + "Create your first AI agent to get started." : "Креирајте свог првог AI агента да бисте започели.", + "Created" : "Креирано", + "Created:" : "Креирано:", + "Creating..." : "Креирање...", + "Current Filters" : "Тренутни филтери", + "Current Type:" : "Тренутни тип:", + "Custom API endpoint if using a different region" : "Прилагођена API крајња тачка ако користите другачији регион", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Прилагођена HTTP заглавља (једно по реду, формат: Header-Name: value)", + "Daily" : "Дневно", + "Dashboard" : "Контролна табла", + "Data Source" : "Извор података", + "Data sources" : "Извори података", + "Data type does not match observed values" : "Тип података не одговара посматраним вредностима", + "Database URL" : "URL базе података", + "Database information refreshed" : "Информације о бази података су освежене", + "Date Range" : "Опсег датума", + "Default" : "Подразумевано", + "Default Organisation" : "Подразумевана организација", + "Default: 5" : "Подразумевано: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Кашњења се удвостручују са сваким покушајем (2, 4, 8 минута...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Кашњења се повећавају линеарно (5, 10, 15 минута...)", + "Delete" : "Обриши", + "Delete Application" : "Обриши апликацију", + "Delete Audit Trail" : "Обриши ревизорски траг", + "Delete Collection" : "Обриши колекцију", + "Delete ConfigSet" : "Обриши ConfigSet", + "Delete Objects" : "Обриши Објекте", + "Delete Operations" : "Операције брисања", + "Delete View" : "Обриши приказ", + "Delete all objects in this schema" : "Обриши све Објекте у овој Шеми", + "Delete permanently" : "Обриши трајно", + "Delete this conversation?" : "Обрисати овај разговор?", + "Delete view" : "Обриши приказ", + "Deleted" : "Обрисано", + "Deleted By" : "Обрисао", + "Deleted Date" : "Датум брисања", + "Deleted Items Management" : "Управљање обрисаним ставкама", + "Deleted This Week" : "Обрисано ове недеље", + "Deleted Today" : "Обрисано данас", + "Deleted:" : "Обрисано:", + "Deleting..." : "Брисање...", + "Deletion Date Range" : "Опсег датума брисања", + "Deletion Statistics" : "Статистика брисања", + "Deprecated" : "Застарело", + "Description" : "Опис", + "Description:" : "Опис:", + "Detect data types and patterns" : "Откриј типове података и обрасце", + "Detected At" : "Откривено", + "Detected Format:" : "Откривени формат:", + "Detected Issues:" : "Откривени проблеми:", + "Disable" : "Онемогући", + "Disabled" : "Онемогућено", + "Discover Files" : "Откриј Датотеке", + "Discovered Properties" : "Откривена Својства", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Да ли желите да трајно обришете све филтриране уносе ревизорског трага? Ова радња се не може опозвати.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Да ли желите да трајно обришете овај унос ревизорског трага? Ова радња се не може опозвати.", + "Documents" : "Документи", + "Download API Spec" : "Преузми API спецификацију", + "Edit" : "Уреди", + "Edit Register" : "Уреди Регистар", + "Edit Schema" : "Уреди Шему", + "Edit View" : "Уреди приказ", + "Edit Webhook" : "Уреди веб-кукицу", + "Edit view" : "Уреди приказ", + "Edit view details" : "Уреди детаље приказа", + "Effectiveness" : "Ефикасност", + "Email" : "Е-пошта", + "Emails" : "Е-поруке", + "Embedding Model" : "Модел уграђивања", + "Embedding Provider" : "Провајдер уграђивања", + "Embedding generated successfully" : "Уграђивање је успешно генерисано", + "Embedding provider connection successful!" : "Веза са провајдером уграђивања је успешна!", + "Enable" : "Омогући", + "Enable auto-creation" : "Омогући аутоматско креирање", + "Enable automatic file vectorization" : "Омогући аутоматску векторизацију Датотека", + "Enable automatic object vectorization" : "Омогући аутоматску векторизацију Објеката", + "Enable automatic synchronization" : "Омогући аутоматску синхронизацију", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Омогући детаљно бележење за SOLR операције (препоручено за отклањање грешака)", + "Enable faceting" : "Омогући фасете", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Омогући или онемогући LLM функције. Конфигуришите провајдере и моделе помоћу дугмета LLM конфигурација изнад.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Омогући или онемогући интеграцију SOLR претраге. Конфигуришите подешавања везе помоћу дугмета Подешавања везе изнад.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Омогући или онемогући интеграцију n8n радног тока. Конфигуришите подешавања везе испод.", + "Enable or disable this webhook" : "Омогући или онемогући ову веб-кукицу", + "Enable vectorization for all existing and future views (may increase costs)" : "Омогући векторизацију за све постојеће и будуће приказе (може повећати трошкове)", + "Enabled" : "Омогућено", + "Endpoints" : "Крајње тачке", + "Enter ConfigSet name" : "Унесите назив ConfigSet-а", + "Enter collection name" : "Унесите назив колекције", + "Enter description (optional)..." : "Унесите опис (опционо)...", + "Enter new collection name" : "Унесите нови назив колекције", + "Enter object ID" : "Унесите ИД Објекта", + "Enter search term" : "Унесите појам за претрагу", + "Enter view name..." : "Унесите назив приказа...", + "Enter webhook name" : "Унесите назив веб-кукице", + "Entities" : "Ентитети", + "Entity Information" : "Информације о ентитету", + "Entity deleted successfully" : "Ентитет је успешно обрисан", + "Entity not found" : "Ентитет није пронађен", + "Entries to be deleted:" : "Уноси за брисање:", + "Enum Issue" : "Проблем са набрајањем", + "Enum constraint is missing" : "Недостаје ограничење набрајања", + "Error" : "Грешка", + "Error Details" : "Детаљи грешке", + "Error Information" : "Информације о грешци", + "Error Message" : "Порука о грешци", + "Error loading application" : "Грешка при учитавању апликације", + "Error loading audit trails" : "Грешка при учитавању ревизорских трагова", + "Error loading entity" : "Грешка при учитавању ентитета", + "Error loading search trails" : "Грешка при учитавању трагова претраге", + "Estimated Batches:" : "Процењене групе:", + "Estimated Cost:" : "Процењени трошак:", + "Estimated Duration:" : "Процењено трајање:", + "Event" : "Догађај", + "Event Property for Payload" : "Својство догађаја за корисни терет", + "Events" : "Догађаји", + "Example Value" : "Пример вредности", + "Exclusive Maximum" : "Ексклузивни максимум", + "Exclusive Minimum" : "Ексклузивни минимум", + "Execution Mode" : "Режим извршавања", + "Execution Time" : "Време извршавања", + "Execution Time Range" : "Опсег времена извршавања", + "Existing Improvements" : "Постојећа побољшања", + "Exponential" : "Експоненцијално", + "Export" : "Извези", + "Export completed successfully" : "Извоз је успешно завршен", + "Export, view, or delete audit trails" : "Извезите, прегледајте или обришите ревизорске трагове", + "Extend Schema" : "Прошири Шему", + "Extended" : "Прошириво", + "Extensions" : "Екстензије", + "Extract Pending Files" : "Издвоји Датотеке на чекању", + "Extract properties from each object" : "Издвоји Својства из сваког Објекта", + "Extracted At" : "Издвојено", + "Extraction Status" : "Статус издвајања", + "Facet configuration updated successfully" : "Конфигурација фасета је успешно ажурирана", + "Facets discovered and configured successfully" : "Фасете су успешно откривене и конфигурисане", + "Facets discovered successfully" : "Фасете су успешно откривене", + "Failed" : "Неуспешно", + "Failed to analyze schema properties" : "Анализа Својстава Шеме није успела", + "Failed to archive conversation" : "Архивирање разговора није успело", + "Failed to calculate sizes" : "Израчунавање величина није успело", + "Failed to clear collection" : "Брисање колекције није успело", + "Failed to copy changes" : "Копирање измена није успело", + "Failed to copy data" : "Копирање података није успело", + "Failed to copy data to clipboard" : "Копирање података у привремену меморију није успело", + "Failed to create conversation" : "Креирање разговора није успело", + "Failed to create or find project" : "Креирање или проналажење пројекта није успело", + "Failed to delete collection" : "Брисање колекције није успело", + "Failed to delete conversation" : "Брисање разговора није успело", + "Failed to delete webhook" : "Брисање веб-кукице није успело", + "Failed to download API specification" : "Преузимање API спецификације није успело", + "Failed to get SOLR field configuration" : "Добављање конфигурације SOLR поља није успело", + "Failed to get entity categories" : "Добављање категорија ентитета није успело", + "Failed to get entity statistics" : "Добављање статистике ентитета није успело", + "Failed to get entity types" : "Добављање типова ентитета није успело", + "Failed to load LLM configuration" : "Учитавање LLM конфигурације није успело", + "Failed to load Nextcloud groups" : "Учитавање Nextcloud група није успело", + "Failed to load conversation" : "Учитавање разговора није успело", + "Failed to load entities" : "Учитавање ентитета није успело", + "Failed to load entity" : "Учитавање ентитета није успело", + "Failed to load files" : "Учитавање Датотека није успело", + "Failed to load organisations" : "Учитавање организација није успело", + "Failed to load templates" : "Учитавање шаблона није успело", + "Failed to load webhooks" : "Учитавање веб-кукица није успело", + "Failed to load workflows" : "Учитавање радних токова није успело", + "Failed to refresh database information" : "Освежавање информација о бази података није успело", + "Failed to reindex collection" : "Поновно индексирање колекције није успело", + "Failed to rename conversation" : "Преименовање разговора није успело", + "Failed to restore conversation" : "Враћање разговора није успело", + "Failed to retry extraction" : "Поновни покушај издвајања није успео", + "Failed to retry webhook" : "Поновни покушај веб-кукице није успео", + "Failed to save GitHub token" : "Чување GitHub токена није успело", + "Failed to save GitLab URL" : "Чување GitLab URL-а није успело", + "Failed to save GitLab token" : "Чување GitLab токена није успело", + "Failed to save additional feedback" : "Чување додатних повратних информација није успело", + "Failed to save n8n configuration" : "Чување n8n конфигурације није успело", + "Failed to save roles" : "Чување улога није успело", + "Failed to save settings" : "Чување подешавања није успело", + "Failed to save webhook" : "Чување веб-кукице није успело", + "Failed to send feedback" : "Слање повратних информација није успело", + "Failed to test webhook" : "Тестирање веб-кукице није успело", + "Failed to update favorite status" : "Ажурирање статуса омиљеног није успело", + "Failed to update schema properties" : "Ажурирање Својстава Шеме није успело", + "Failed to update view" : "Ажурирање приказа није успело", + "Failed to update webhook" : "Ажурирање веб-кукице није успело", + "Feedback recorded" : "Повратне информације су забележене", + "Fewer sources (1-3):" : "Мање Извора (1-3):", + "Field" : "Поље", + "File" : "Датотека", + "File Chunk Prediction" : "Предвиђање делова Датотеке", + "File Collection" : "Колекција Датотека", + "File Management" : "Управљање Датотекама", + "File Name" : "Назив Датотеке", + "File Path" : "Путања Датотеке", + "File Type Selection" : "Избор типа Датотеке", + "File Vectorization" : "Векторизација Датотека", + "File Warmup" : "Загревање Датотека", + "File actions menu" : "Мени радњи Датотеке", + "File anonymized successfully" : "Датотека је успешно анонимизована", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Векторизација делова Датотеке још увек није имплементирана. Делови су спремни и сачувани, али је услуга векторизације у развоју.", + "File collection not configured" : "Колекција Датотека није конфигурисана", + "File discovery completed" : "Откривање Датотека је завршено", + "File extraction completed" : "Издвајање Датотека је завршено", + "File extraction queued" : "Издвајање Датотека је стављено у ред", + "File indexed successfully" : "Датотека је успешно индексирана", + "File is already anonymized" : "Датотека је већ анонимизована", + "File not found" : "Датотека није пронађена", + "File settings updated successfully" : "Подешавања Датотеке су успешно ажурирана", + "File sources" : "Извори Датотека", + "File vectorization configuration saved successfully" : "Конфигурација векторизације Датотека је успешно сачувана", + "File vectorization started. Check the statistics section for progress." : "Векторизација Датотека је покренута. Проверите одељак статистике за напредак.", + "File warmup completed" : "Загревање Датотека је завршено", + "Files" : "Датотеке", + "Files with Completed Extraction:" : "Датотеке са завршеним издвајањем:", + "Files → fileCollection, Objects → objectCollection" : "Датотеке → fileCollection, Објекти → objectCollection", + "Filter Audit Trails" : "Филтрирај ревизорске трагове", + "Filter Deleted Items" : "Филтрирај обрисане ставке", + "Filter Objects" : "Филтрирај Објекте", + "Filter Properties" : "Филтрирај Својства", + "Filter Search Trails" : "Филтрирај трагове претраге", + "Filter Statistics" : "Статистика филтера", + "Filter and analyze search trail entries" : "Филтрирајте и анализирајте уносе трагова претраге", + "Filter and manage audit trail entries" : "Филтрирајте и управљајте уносима ревизорског трага", + "Filter and manage soft deleted items" : "Филтрирајте и управљајте меко обрисаним ставкама", + "Filter by object ID" : "Филтрирај по ИД-у Објекта", + "Filter by search term" : "Филтрирај по појму за претрагу", + "Filter by webhook" : "Филтрирај по веб-кукици", + "Filter data loaded automatically. Use the filters below to refine your search." : "Подаци филтера су аутоматски учитани. Користите филтере испод да бисте прецизирали претрагу.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Филтрирајте окидаче веб-кукице по Својствима корисног терета (једно по реду, формат: key: value)", + "Filtered" : "Филтрирано", + "Filters" : "Филтери", + "Fireworks AI Chat Settings" : "Подешавања Fireworks AI ћаскања", + "Fireworks AI Embedding Configuration" : "Конфигурација Fireworks AI уграђивања", + "First" : "Прво", + "Fixed" : "Фиксно", + "For chat and retrieval-augmented generation" : "За ћаскање и генерисање потпомогнуто претрагом", + "For vector embeddings and semantic search" : "За векторска уграђивања и семантичку претрагу", + "Format" : "Формат", + "Format Issue" : "Проблем формата", + "Format constraint is missing" : "Недостаје ограничење формата", + "From Date" : "Од датума", + "From date" : "Од датума", + "Full data copied to clipboard" : "Сви подаци су копирани у привремену меморију", + "General Issue" : "Општи проблем", + "Generate recommendations and confidence scores" : "Генериши препоруке и оцене поузданости", + "Generate vectors immediately when new objects are created" : "Генериши векторе одмах када се креирају нови Објекти", + "GitHub token is valid" : "GitHub токен је важећи", + "GitHub token removed successfully" : "GitHub токен је успешно уклоњен", + "GitHub token saved successfully" : "GitHub токен је успешно сачуван", + "GitLab URL saved successfully" : "GitLab URL је успешно сачуван", + "GitLab token is valid" : "GitLab токен је важећи", + "GitLab token saved successfully" : "GitLab токен је успешно сачуван", + "HTTP Method" : "HTTP метода", + "HTTP method used to send webhook requests" : "HTTP метода која се користи за слање захтева веб-кукице", + "Headers" : "Заглавља", + "Health" : "Здравље", + "Heartbeat successful - connection kept alive" : "Откуцај срца успешан - веза је одржана", + "Helpful" : "Корисно", + "Hide Filters" : "Сакриј филтере", + "Hide in forms" : "Сакриј у обрасцима", + "Hide in list view" : "Сакриј у приказу листе", + "High" : "Висок", + "High Confidence" : "Висока поузданост", + "Host" : "Домаћин", + "Hourly" : "На сат", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Колико дубоко прелазити кроз угнежђена Својства Објекта (1-20). Веће вредности хватају више детаља, али повећавају величину вектора.", + "How often to check for updates (1-168 hours)" : "Колико често проверавати ажурирања (1-168 сати)", + "How to handle retries for failed webhook deliveries" : "Како поступати са поновним покушајима неуспелих испорука веб-кукице", + "ID" : "ИД", + "ID:" : "ИД:", + "IP rate limits cleared successfully" : "Ограничења учесталости IP адреса су успешно обрисана", + "Identify properties not in the schema" : "Идентификуј Својства која нису у Шеми", + "Immutable" : "Непроменљиво", + "Import" : "Увези", + "Import successful" : "Увоз успешан", + "Improved Property" : "Побољшано Својство", + "In use" : "У употреби", + "Inactive" : "Неактивно", + "Include IDs and names of related objects for better contextual search" : "Укључи ИД-ове и називе сродних Објеката за бољу контекстуалну претрагу", + "Include related object references" : "Укључи референце сродних Објеката", + "Include schema and register metadata" : "Укључи метаподатке Шеме и Регистра", + "Initialization failed" : "Иницијализација није успела", + "Initialize Project" : "Иницијализуј пројекат", + "Inspect Fields" : "Прегледај поља", + "Inspect Index" : "Прегледај индекс", + "Invalid" : "Неважеће", + "Invalid batch size. Must be between 1 and 5000" : "Неважећа величина групе. Мора бити између 1 и 5000", + "Invalid field name provided" : "Дат је неважећи назив поља", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Неважећи maxObjects. Мора бити 0 (све) или позитиван број", + "Invalid:" : "Неважеће:", + "Issue" : "Проблем", + "Items per page:" : "Ставки по страници:", + "Just now" : "Управо сада", + "LLM Configuration" : "LLM конфигурација", + "LLM actions menu" : "Мени LLM радњи", + "LLM configuration saved successfully" : "LLM конфигурација је успешно сачувана", + "LLM features disabled" : "LLM функције су онемогућене", + "LLM features enabled" : "LLM функције су омогућене", + "LLM must be enabled with an embedding provider configured" : "LLM мора бити омогућен са конфигурисаним провајдером уграђивања", + "LLM settings updated successfully" : "LLM подешавања су успешно ажурирана", + "Last" : "Последње", + "Last Triggered" : "Последње покренуто", + "Length Range:" : "Опсег дужине:", + "Linear" : "Линеарно", + "Load Advanced Filters" : "Учитај напредне филтере", + "Load advanced filters with live data from your search index" : "Учитајте напредне филтере са подацима уживо из вашег индекса претраге", + "Load view" : "Учитај приказ", + "Loading ConfigSets..." : "Учитавање ConfigSet-ова...", + "Loading advanced filters..." : "Учитавање напредних филтера...", + "Loading agents..." : "Учитавање агената...", + "Loading application details..." : "Учитавање детаља апликације...", + "Loading applications..." : "Учитавање апликација...", + "Loading archived conversations..." : "Учитавање архивираних разговора...", + "Loading audit trails..." : "Учитавање ревизорских трагова...", + "Loading collections..." : "Учитавање колекција...", + "Loading configurations..." : "Учитавање конфигурација...", + "Loading conversation..." : "Учитавање разговора...", + "Loading conversations..." : "Учитавање разговора...", + "Loading deleted items..." : "Учитавање обрисаних ставки...", + "Loading events..." : "Учитавање догађаја...", + "Loading filter data..." : "Учитавање података филтера...", + "Loading groups..." : "Учитавање група...", + "Loading log details..." : "Учитавање детаља дневника...", + "Loading registers..." : "Учитавање Регистара...", + "Loading schemas..." : "Учитавање Шема...", + "Loading search trails..." : "Учитавање трагова претраге...", + "Loading sources..." : "Учитавање Извора...", + "Loading statistics..." : "Учитавање статистике...", + "Loading users..." : "Учитавање корисника...", + "Loading views..." : "Учитавање приказа...", + "Loading..." : "Учитавање...", + "Local" : "Локално", + "Local Version" : "Локална верзија", + "Locked" : "Закључано", + "Locked:" : "Закључано:", + "Login successful" : "Пријава успешна", + "Logs" : "Дневници", + "Low" : "Низак", + "Low Confidence" : "Ниска поузданост", + "Lucene Version:" : "Lucene верзија:", + "Magic table synchronized successfully" : "Магична табела је успешно синхронизована", + "Manage Organisation Roles" : "Управљај улогама организације", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Управљајте SOLR колекцијама (складиштима података) и доделите их за Објекте и Датотеке.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Управљајте SOLR ConfigSet-овима (шаблони конфигурације) за ваше колекције.", + "Manage and configure agents for automated tasks" : "Управљајте и конфигуришите агенте за аутоматизоване задатке", + "Manage and monitor file text extraction status" : "Управљајте и пратите статус издвајања текста из Датотека", + "Manage and restore soft deleted items from your registers" : "Управљајте и враћајте меко обрисане ставке из ваших Регистара", + "Manage and view detected entities from files and objects" : "Управљајте и прегледајте откривене ентитете из Датотека и Објеката", + "Manage document templates and themes" : "Управљајте шаблонима докумената и темама", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Управљајте n8n радним токовима за аутоматизацију OpenRegister-а. Радни токови ће бити сачувани у конфигурисаном пројекту.", + "Manage webhooks for event-driven integrations" : "Управљајте веб-кукицама за интеграције вођене догађајима", + "Manage your applications and modules" : "Управљајте вашим апликацијама и модулима", + "Manage your data registers and their configurations" : "Управљајте вашим Регистрима података и њиховим конфигурацијама", + "Manage your data schemas and their properties" : "Управљајте вашим Шемама података и њиховим Својствима", + "Manage your data sources and their configurations" : "Управљајте вашим Изворима података и њиховим конфигурацијама", + "Manage your saved search configurations" : "Управљајте вашим сачуваним конфигурацијама претраге", + "Manage your system configurations and settings" : "Управљајте вашим системским конфигурацијама и подешавањима", + "Managed" : "Управљано", + "Max Files (0 = all)" : "Највише Датотека (0 = све)", + "Max Length" : "Највећа дужина", + "Max Length:" : "Највећа дужина:", + "Max Objects (0 = all)" : "Највише Објеката (0 = све)", + "Max Retries" : "Највише покушаја", + "Max Shards/Node" : "Највише делова/чвору", + "Max execution time (ms)" : "Највеће време извршавања (ms)", + "Max ms" : "Највише ms", + "Max result count" : "Највећи број резултата", + "Max results" : "Највише резултата", + "Maximum" : "Максимум", + "Maximum Nesting Depth" : "Максимална дубина угнежђивања", + "Maximum length constraint is missing" : "Недостаје ограничење максималне дужине", + "Maximum length is too restrictive" : "Максимална дужина је превише рестриктивна", + "Maximum number of files to process. Set to 0 to process all files." : "Максималан број Датотека за обраду. Поставите на 0 да бисте обрадили све Датотеке.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Максималан број Објеката за обраду. Поставите на 0 да бисте обрадили све Објекте.", + "Maximum number of retry attempts for failed deliveries" : "Максималан број поновних покушаја за неуспеле испоруке", + "Maximum time to wait before committing changes" : "Максимално време чекања пре потврђивања измена", + "Maximum tokens to generate" : "Максималан број токена за генерисање", + "Maximum value constraint is missing" : "Недостаје ограничење максималне вредности", + "Maximum value is too restrictive" : "Максимална вредност је превише рестриктивна", + "Medium" : "Средње", + "Medium Confidence" : "Средња поузданост", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Средњи упити: Претраге са одређеним филтрирањем или више параметара (нпр. опсези датума, одређени Регистри/Шеме)", + "Members of selected groups can access this view" : "Чланови изабраних група могу да приступе овом приказу", + "Members:" : "Чланови:", + "Memory prediction calculated successfully" : "Предвиђање меморије је успешно израчунато", + "Message content is required" : "Садржај поруке је обавезан", + "Message does not belong to this conversation" : "Порука не припада овом разговору", + "Metadata Filters" : "Филтери метаподатака", + "Method" : "Метода", + "Min Length" : "Најмања дужина", + "Min execution time (ms)" : "Најмање време извршавања (ms)", + "Min ms" : "Најмање ms", + "Min result count" : "Најмањи број резултата", + "Min results" : "Најмање резултата", + "Minimum" : "Минимум", + "Minimum value constraint is missing" : "Недостаје ограничење минималне вредности", + "Minimum value is too restrictive" : "Минимална вредност је превише рестриктивна", + "Missing object fields created successfully" : "Недостајућа поља Објекта су успешно креирана", + "Mode:" : "Режим:", + "Model" : "Модел", + "Model Name" : "Назив модела", + "Modified" : "Измењено", + "Monthly" : "Месечно", + "More sources (10-20):" : "Више Извора (10-20):", + "Most Active Objects" : "Најактивнији Објекти", + "Multiple Of" : "Вишекратник од", + "Name" : "Назив", + "Names cache warmup completed" : "Загревање кеша назива је завршено", + "New Collection Name" : "Назив нове колекције", + "New Conversation" : "Нови разговор", + "New Properties" : "Нова Својства", + "New Property" : "Ново Својство", + "New Value" : "Нова вредност", + "New users without specific organisation membership will be automatically added to this organisation" : "Нови корисници без чланства у одређеној организацији ће аутоматски бити додати у ову организацију", + "Next" : "Следеће", + "No" : "Не", + "No ConfigSets found" : "Није пронађен ниједан ConfigSet", + "No GitHub token configured" : "Није конфигурисан ниједан GitHub токен", + "No GitHub token provided" : "Није дат ниједан GitHub токен", + "No GitLab token provided" : "Није дат ниједан GitLab токен", + "No Organisation" : "Нема организације", + "No active filters" : "Нема активних филтера", + "No activity data available" : "Нема доступних података о активности", + "No agents available" : "Нема доступних агената", + "No agents found" : "Није пронађен ниједан агент", + "No app store cache exists yet" : "Кеш продавнице апликација још увек не постоји", + "No applications are available." : "Нема доступних апликација.", + "No applications found" : "Није пронађена ниједна апликација", + "No archived conversations" : "Нема архивираних разговора", + "No audit trail entries found" : "Није пронађен ниједан унос ревизорског трага", + "No changes recorded" : "Нису забележене измене", + "No chunks to vectorize" : "Нема делова за векторизацију", + "No collections found" : "Није пронађена ниједна колекција", + "No configuration data" : "Нема података конфигурације", + "No configurations are available." : "Нема доступних конфигурација.", + "No configurations found" : "Није пронађена ниједна конфигурација", + "No configurations found for this application." : "Није пронађена ниједна конфигурација за ову апликацију.", + "No conversations yet. Create a new one to get started!" : "Још нема разговора. Креирајте нови да бисте започели!", + "No data available" : "Нема доступних података", + "No data available for chart" : "Нема доступних података за графикон", + "No deleted items found" : "Није пронађена ниједна обрисана ставка", + "No deletion data available" : "Нема доступних података о брисању", + "No description found" : "Није пронађен опис", + "No description provided" : "Није дат опис", + "No entities detected in this file. Run text extraction first." : "У овој Датотеци нису откривени ентитети. Прво покрените издвајање текста.", + "No entities found" : "Није пронађен ниједан ентитет", + "No entities have been detected yet" : "Још увек нису откривени ентитети", + "No events found" : "Није пронађен ниједан догађај", + "No expired audit trails found to clear" : "Није пронађен ниједан истекли ревизорски траг за брисање", + "No facetable fields available. Select a register and schema to see available filters." : "Нема доступних поља за фасете. Изаберите Регистар и Шему да бисте видели доступне филтере.", + "No files found" : "Није пронађена ниједна Датотека", + "No files have been extracted yet" : "Још увек нису издвојене Датотеке", + "No files to process" : "Нема Датотека за обраду", + "No files to reindex" : "Нема Датотека за поновно индексирање", + "No filters are currently active. This will delete ALL audit trail entries!" : "Тренутно ниједан филтер није активан. Ово ће обрисати СВЕ уносе ревизорског трага!", + "No log entries found" : "Није пронађен ниједан унос дневника", + "No logs are available for this configuration." : "Нема доступних дневника за ову конфигурацију.", + "No logs are available for this source." : "Нема доступних дневника за овај Извор.", + "No logs found" : "Није пронађен ниједан дневник", + "No mismatched fields found - SOLR schema is properly configured" : "Нису пронађена неусклађена поља - SOLR Шема је исправно конфигурисана", + "No objects found" : "Није пронађен ниједан Објекат", + "No objects found to vectorize" : "Није пронађен ниједан Објекат за векторизацију", + "No objects selected for deletion" : "Није изабран ниједан Објекат за брисање", + "No objects to delete" : "Нема Објеката за брисање", + "No objects were permanently deleted" : "Ниједан Објекат није трајно обрисан", + "No properties found" : "Није пронађено ниједно Својство", + "No properties match your filters." : "Ниједно Својство не одговара вашим филтерима.", + "No purge date set" : "Датум чишћења није постављен", + "No register data available" : "Нема доступних података Регистра", + "No registers found" : "Није пронађен ниједан Регистар", + "No registers found for this application." : "Није пронађен ниједан Регистар за ову апликацију.", + "No relations found" : "Није пронађена ниједна релација", + "No request body available" : "Нема доступног тела захтева", + "No response body available" : "Нема доступног тела одговора", + "No saved views yet. Create one in the Search tab!" : "Још нема сачуваних приказа. Креирајте један на картици Претрага!", + "No schema data available" : "Нема доступних података Шеме", + "No schema selected for exploration" : "Није изабрана Шема за истраживање", + "No schemas found" : "Није пронађена ниједна Шема", + "No schemas found for this application." : "Није пронађена ниједна Шема за ову апликацију.", + "No search terms data available" : "Нема доступних података о појмовима за претрагу", + "No search trail entries found" : "Није пронађен ниједан унос трага претраге", + "No sources are available." : "Нема доступних Извора.", + "No sources found" : "Није пронађен ниједан Извор", + "No templates found" : "Није пронађен ниједан шаблон", + "No templates have been created yet" : "Још увек нису креирани шаблони", + "No valid magic-mapped register+schema combinations found" : "Није пронађена ниједна важећа magic-mapped комбинација Регистар+Шема", + "No views found. Create views first before configuring vectorization." : "Није пронађен ниједан приказ. Прво креирајте приказе пре конфигурисања векторизације.", + "No views match your search" : "Ниједан приказ не одговара вашој претрази", + "No webhooks found" : "Није пронађена ниједна веб-кукица", + "No webhooks have been configured yet" : "Још увек није конфигурисана ниједна веб-кукица", + "No workflows found in this project. Create workflows in the n8n editor." : "Није пронађен ниједан радни ток у овом пројекту. Креирајте радне токове у n8n уређивачу.", + "None" : "Ниједно", + "Not helpful" : "Није корисно", + "Not in use" : "Није у употреби", + "Number Constraints" : "Ограничења бројева", + "Number of chunks to vectorize in one API call (1-100)." : "Број делова за векторизацију у једном API позиву (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Број делова за векторизацију у једном API позиву. Више = брже, али више меморије. Препоручено: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Број Објеката за обраду у свакој групи (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Број Објеката за векторизацију у једном API позиву. Више = брже, али више меморије. Препоручено: 10-50.", + "Numeric Range:" : "Нумерички опсег:", + "Object" : "Објекат", + "Object Collection" : "Колекција Објеката", + "Object Count Prediction" : "Предвиђање броја Објеката", + "Object ID" : "ИД Објекта", + "Object Management" : "Управљање Објектима", + "Object Vectorization" : "Векторизација Објеката", + "Object Warmup" : "Загревање Објеката", + "Object collection not configured" : "Колекција Објеката није конфигурисана", + "Object not found" : "Објекат није пронађен", + "Object permanently deleted" : "Објекат је трајно обрисан", + "Object restored successfully" : "Објекат је успешно враћен", + "Object settings updated successfully" : "Подешавања Објекта су успешно ажурирана", + "Object sources" : "Извори Објеката", + "Object unlocked successfully" : "Објекат је успешно откључан", + "Object vectorization configuration saved successfully" : "Конфигурација векторизације Објеката је успешно сачувана", + "Object vectorized successfully" : "Објекат је успешно векторизован", + "Object:" : "Објекат:", + "Objects" : "Објекти", + "Objects Analyzed" : "Анализирани Објекти", + "Objects Distribution" : "Расподела Објеката", + "Objects being analyzed" : "Објекти који се анализирају", + "Objects by Register" : "Објекти по Регистру", + "Objects by Schema" : "Објекти по Шеми", + "Objects deletion completed successfully" : "Брисање Објеката је успешно завршено", + "Objects in schema" : "Објекти у Шеми", + "Objects to Process:" : "Објекти за обраду:", + "Objects to be analyzed" : "Објекти за анализу", + "Objects to be deleted" : "Објекти за брисање", + "Objects to be validated" : "Објекти за валидацију", + "Objects will be serialized as JSON text before vectorization" : "Објекти ће бити серијализовани као JSON текст пре векторизације", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Објекти ће бити меко обрисани (означени као обрисани, али задржани у бази података). Могу се касније опоравити ако је потребно.", + "Old Value" : "Стара вредност", + "Oldest Item (days)" : "Најстарија ставка (дана)", + "Ollama Chat Settings" : "Подешавања Ollama ћаскања", + "Ollama Embedding Configuration" : "Конфигурација Ollama уграђивања", + "Ollama URL" : "Ollama URL", + "Only With Changes" : "Само са изменама", + "Open Register Objects" : "Објекти Open Register-а", + "Open n8n Editor" : "Отвори n8n уређивач", + "OpenAI Chat Settings" : "Подешавања OpenAI ћаскања", + "OpenAI Embedding Configuration" : "Конфигурација OpenAI уграђивања", + "OpenRegister Settings" : "OpenRegister подешавања", + "Optional webhook secret for signature verification" : "Опциона тајна веб-кукице за верификацију потписа", + "Order" : "Редослед", + "Organisation" : "Организација", + "Organisation ID" : "ИД организације", + "Organisation Members" : "Чланови организације", + "Organisation Statistics" : "Статистика организације", + "Organisation created successfully" : "Организација је успешно креирана", + "Organisation settings saved successfully" : "Подешавања организације су успешно сачувана", + "Organisations" : "Организације", + "Organization" : "Организација", + "Organization ID (Optional)" : "ИД организације (опционо)", + "Orphaned Items" : "Ставке без везе", + "Owner" : "Власник", + "Owner:" : "Власник:", + "Parallel Mode (Faster, more resource intensive)" : "Паралелни режим (бржи, захтева више ресурса)", + "Parallel:" : "Паралелно:", + "Password" : "Лозинка", + "Password for SOLR authentication (optional)" : "Лозинка за SOLR потврду идентитета (опционо)", + "Path" : "Путања", + "Path in repository" : "Путања у репозиторијуму", + "Path where the configuration file will be saved in the repository" : "Путања где ће конфигурациона Датотека бити сачувана у репозиторијуму", + "Path where the register OAS file will be saved in the repository" : "Путања где ће OAS Датотека Регистра бити сачувана у репозиторијуму", + "Pattern (regex)" : "Образац (регуларни израз)", + "Pattern Issue" : "Проблем обрасца", + "Pattern constraint is missing" : "Недостаје ограничење обрасца", + "Patterns:" : "Обрасци:", + "Pending" : "На чекању", + "Permanent Delete Mode" : "Режим трајног брисања", + "Permanently Delete" : "Трајно обриши", + "Permanently delete already soft-deleted objects" : "Трајно обриши већ меко обрисане Објекте", + "Permanently delete this conversation?" : "Трајно обрисати овај разговор?", + "Person" : "Особа", + "Personal Data" : "Лични подаци", + "Phone" : "Телефон", + "Photos" : "Фотографије", + "Please create an agent in the" : "Креирајте агента у", + "Please select an agent to continue" : "Изаберите агента да бисте наставили", + "Please select which register and schema to use for the new object" : "Изаберите који Регистар и Шему да користите за нови Објекат", + "Please try again later." : "Покушајте поново касније.", + "Please wait while we fetch your agents." : "Сачекајте док преузимамо ваше агенте.", + "Please wait while we fetch your applications." : "Сачекајте док преузимамо ваше апликације.", + "Please wait while we fetch your configurations." : "Сачекајте док преузимамо ваше конфигурације.", + "Please wait while we fetch your deleted items." : "Сачекајте док преузимамо ваше обрисане ставке.", + "Please wait while we fetch your sources." : "Сачекајте док преузимамо ваше Изворе.", + "Polls" : "Анкете", + "Popular Search Terms" : "Популарни појмови за претрагу", + "Port" : "Порт", + "Prerequisites" : "Предуслови", + "Press Enter to send, Shift+Enter for new line" : "Притисните Enter за слање, Shift+Enter за нови ред", + "Previous" : "Претходно", + "Private" : "Приватно", + "Processes chunks in batches with simulated parallelism." : "Обрађује делове у групама са симулираним паралелизмом.", + "Processes file chunks sequentially (safest)." : "Обрађује делове Датотеке секвенцијално (најбезбедније).", + "Processes objects in chunks with simulated parallelism." : "Обрађује Објекте у деловима са симулираним паралелизмом.", + "Processes objects sequentially (safest)." : "Обрађује Објекте секвенцијално (најбезбедније).", + "Processing" : "Обрада", + "Processing Limits" : "Ограничења обраде", + "Processing..." : "Обрада...", + "Project initialized successfully" : "Пројекат је успешно иницијализован", + "Project not found. Please initialize first." : "Пројекат није пронађен. Прво иницијализујте.", + "Properties" : "Својства", + "Property Behaviors" : "Понашања Својства", + "Property Configuration:" : "Конфигурација Својства:", + "Property Filters" : "Филтери Својства", + "Property Title" : "Наслов Својства", + "Property Type" : "Тип Својства", + "Property can be improved" : "Својство се може побољшати", + "Protected" : "Заштићено", + "Provider" : "Провајдер", + "Provider is required for testing" : "Провајдер је обавезан за тестирање", + "Public" : "Јавно", + "Public View" : "Јавни приказ", + "Public views can be accessed by anyone in the system" : "Јавним приказима може приступити свако у систему", + "Publish" : "Објави", + "Publish Objects" : "Објави Објекте", + "Publish all objects in this schema" : "Објави све Објекте у овој Шеми", + "Published" : "Објављено", + "Published:" : "Објављено:", + "Publishing..." : "Објављивање...", + "Purge" : "Очисти", + "Purge Date" : "Датум чишћења", + "Query Complexity Distribution" : "Расподела сложености упита", + "Query parameter is required" : "Параметар упита је обавезан", + "RAG" : "RAG", + "RAG Configuration" : "RAG конфигурација", + "Rate limits cleared successfully" : "Ограничења учесталости су успешно обрисана", + "Raw Changes Data" : "Сирови подаци о изменама", + "Re-vectorize on object update" : "Поново векторизуј при ажурирању Објекта", + "Read" : "Читање", + "Recommendations:" : "Препоруке:", + "Recommended Type:" : "Препоручени тип:", + "Recommended: 5 sources" : "Препоручено: 5 Извора", + "Refresh" : "Освежи", + "Refresh Data" : "Освежи податке", + "Refresh Stats" : "Освежи статистику", + "Refresh Workflows" : "Освежи радне токове", + "Refresh dashboard" : "Освежи контролну таблу", + "Refresh database info" : "Освежи информације о бази података", + "Register" : "Регистар", + "Register ID" : "ИД Регистра", + "Register Statistics" : "Статистика Регистра", + "Register Totals" : "Укупно по Регистру", + "Register depublished successfully" : "Регистар је успешно повучен из објаве", + "Register not found" : "Регистар није пронађен", + "Register objects deletion completed successfully" : "Брисање Објеката Регистра је успешно завршено", + "Register published successfully" : "Регистар је успешно објављен", + "Register/Schema Usage" : "Употреба Регистра/Шеме", + "Registers" : "Регистри", + "Reindex" : "Поново индексирај", + "Reindex completed" : "Поновно индексирање је завршено", + "Reindex completed successfully" : "Поновно индексирање је успешно завршено", + "Reindex failed" : "Поновно индексирање није успело", + "Relations" : "Релације", + "Remote Version" : "Удаљена верзија", + "Remove" : "Уклони", + "Remove filter" : "Уклони филтер", + "Remove from favorites" : "Уклони из омиљеног", + "Remove group" : "Уклони групу", + "Removed" : "Уклоњено", + "Removed from favorites" : "Уклоњено из омиљеног", + "Rename Conversation" : "Преименуј разговор", + "Rename conversation" : "Преименуј разговор", + "Replicas" : "Реплике", + "Repository" : "Репозиторијум", + "Request Body" : "Тело захтева", + "Request Data" : "Подаци захтева", + "Request Details" : "Детаљи захтева", + "Request body must be JSON with a \"query\" field" : "Тело захтева мора бити JSON са пољем „query“", + "Request timeout in seconds" : "Истек захтева у секундама", + "Required field" : "Обавезно поље", + "Required status is inconsistent" : "Статус обавезности је недоследан", + "Rerun Search" : "Поново покрени претрагу", + "Reset Changes" : "Поништи измене", + "Reset Filters" : "Поништи филтере", + "Response Body" : "Тело одговора", + "Response Details" : "Детаљи одговора", + "Response Time:" : "Време одговора:", + "Restore" : "Врати", + "Restore conversation" : "Врати разговор", + "Restore or permanently delete items" : "Врати или трајно обриши ставке", + "Result Count Range" : "Опсег броја резултата", + "Results" : "Резултати", + "Retrieve all objects for this schema" : "Преузми све Објекте за ову Шему", + "Retry" : "Покушај поново", + "Retry Failed Extractions" : "Поново покушај неуспела издвајања", + "Retry Policy" : "Политика поновних покушаја", + "Retry completed" : "Поновни покушај је завршен", + "Retrying..." : "Поновни покушај...", + "Risk Level" : "Ниво ризика", + "Roles updated successfully" : "Улоге су успешно ажуриране", + "SOLR Connection Settings" : "Подешавања SOLR везе", + "SOLR Version:" : "SOLR верзија:", + "SOLR actions menu" : "Мени SOLR радњи", + "SOLR base path (usually /solr)" : "SOLR основна путања (обично /solr)", + "SOLR is disabled" : "SOLR је онемогућен", + "SOLR is not available or not configured" : "SOLR није доступан или није конфигурисан", + "SOLR logging disabled" : "SOLR бележење је онемогућено", + "SOLR logging enabled" : "SOLR бележење је омогућено", + "SOLR search disabled" : "SOLR претрага је онемогућена", + "SOLR search enabled" : "SOLR претрага је омогућена", + "SOLR server hostname or IP address" : "Име домаћина или IP адреса SOLR сервера", + "SOLR server port number (optional, defaults to 8983)" : "Број порта SOLR сервера (опционо, подразумевано 8983)", + "SOLR setup completed successfully" : "Подешавање SOLR-а је успешно завршено", + "SOLR setup failed" : "Подешавање SOLR-а није успело", + "SOLR setup failed - check logs" : "Подешавање SOLR-а није успело - проверите дневнике", + "Sample Values:" : "Узорци вредности:", + "Save" : "Сачувај", + "Save Configuration" : "Сачувај конфигурацију", + "Save Connection Settings" : "Сачувај подешавања везе", + "Save Roles" : "Сачувај улоге", + "Save Settings" : "Сачувај подешавања", + "Save current search as view" : "Сачувај тренутну претрагу као приказ", + "Saved Views" : "Сачувани прикази", + "Saving..." : "Чување...", + "Schema" : "Шема", + "Schema ID" : "ИД Шеме", + "Schema Name" : "Назив Шеме", + "Schema Statistics" : "Статистика Шеме", + "Schema depublished successfully" : "Шема је успешно повучена из објаве", + "Schema objects deletion completed successfully" : "Брисање Објеката Шеме је успешно завршено", + "Schema published successfully" : "Шема је успешно објављена", + "Schema successfully created" : "Шема је успешно креирана", + "Schema successfully updated" : "Шема је успешно ажурирана", + "Schemas" : "Шеме", + "Scheme" : "Шема", + "Search / Views" : "Претрага / Прикази", + "Search Activity" : "Активност претраге", + "Search Analytics" : "Аналитика претраге", + "Search Entities" : "Претражи ентитете", + "Search Files" : "Претражи Датотеке", + "Search GitHub" : "Претражи GitHub", + "Search GitLab" : "Претражи GitLab", + "Search Method" : "Метода претраге", + "Search Term" : "Појам за претрагу", + "Search Trail Details" : "Детаљи трага претраге", + "Search Trail Management" : "Управљање траговима претраге", + "Search Trail Statistics" : "Статистика трагова претраге", + "Search Trails" : "Трагови претраге", + "Search Views" : "Прикази претраге", + "Search Webhooks" : "Претражи веб-кукице", + "Search by file name or path" : "Претражи по називу или путањи Датотеке", + "Search by name or URL" : "Претражи по називу или URL-у", + "Search by value" : "Претражи по вредности", + "Search in Files" : "Претражи у Датотекама", + "Search in Objects" : "Претражи у Објектима", + "Search property names..." : "Претражи називе Својстава...", + "Search trail deletion not implemented yet" : "Брисање трага претраге још увек није имплементирано", + "Search views..." : "Претражи приказе...", + "Secret" : "Тајна", + "Secret key for HMAC signature generation (optional)" : "Тајни кључ за генерисање HMAC потписа (опционо)", + "Select AI Agent" : "Изабери AI агента", + "Select All" : "Изабери све", + "Select ConfigSet" : "Изабери ConfigSet", + "Select File Types to Vectorize:" : "Изабери типове Датотека за векторизацију:", + "Select HTTP method" : "Изабери HTTP методу", + "Select Register and Schema" : "Изабери Регистар и Шему", + "Select Views to Vectorize:" : "Изабери приказе за векторизацију:", + "Select a Nextcloud group to add" : "Изабери Nextcloud групу за додавање", + "Select a branch" : "Изабери грану", + "Select a model or type a custom model name" : "Изабери модел или унеси прилагођени назив модела", + "Select a repository" : "Изабери репозиторијум", + "Select a repository you have write access to" : "Изабери репозиторијум за који имаш приступ за писање", + "Select an AI Agent" : "Изабери AI агента", + "Select an AI agent to begin chatting with your data." : "Изабери AI агента да бисте започели ћаскање са вашим подацима.", + "Select backend" : "Изабери позадински систем", + "Select chat model" : "Изабери модел ћаскања", + "Select collection for files" : "Изабери колекцију за Датотеке", + "Select collection for objects" : "Изабери колекцију за Објекте", + "Select default organisation" : "Изабери подразумевану организацију", + "Select event to listen to..." : "Изабери догађај за слушање...", + "Select model" : "Изабери модел", + "Select options..." : "Изабери опције...", + "Select period" : "Изабери период", + "Select property to send as payload" : "Изабери Својство за слање као корисни терет", + "Select provider" : "Изабери провајдера", + "Select registers and schemas to save a view" : "Изабери Регистре и Шеме да сачуваш приказ", + "Select retry policy" : "Изабери политику поновних покушаја", + "Select the branch to publish to" : "Изабери грану за објављивање", + "Select the event this webhook should listen to" : "Изабери догађај који ова веб-кукица треба да слуша", + "Select views to vectorize:" : "Изабери приказе за векторизацију:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Изаберите које Nextcloud групе су доступне за ову организацију. Корисници у овим групама ће имати приступ ресурсима организације.", + "Select which data views the AI can search" : "Изабери које приказе података AI може да претражује", + "Select which property from the event should be used as the webhook payload data" : "Изаберите које Својство из догађаја треба да се користи као подаци корисног терета веб-кукице", + "Select which tools the AI can use to perform actions" : "Изабери које алате AI може да користи за извршавање радњи", + "Selected" : "Изабрано", + "Selected Groups" : "Изабране групе", + "Selected audit trails deleted successfully" : "Изабрани ревизорски трагови су успешно обрисани", + "Selected search trails deleted successfully" : "Изабрани трагови претраге су успешно обрисани", + "Selected users can access this view" : "Изабрани корисници могу да приступе овом приказу", + "Send additional feedback" : "Пошаљи додатне повратне информације", + "Send as CloudEvent" : "Пошаљи као CloudEvent", + "Sensitive PII" : "Осетљиви лични подаци", + "Serial Mode (Safer, slower)" : "Серијски режим (безбеднији, спорији)", + "Serial:" : "Серијски:", + "Server Configuration" : "Конфигурација сервера", + "Server Information" : "Информације о серверу", + "Settings" : "Подешавања", + "Shards" : "Делови", + "Share" : "Дели", + "Share with Groups" : "Дели са групама", + "Share with Users" : "Дели са корисницима", + "Show Filters" : "Прикажи филтере", + "Show less" : "Прикажи мање", + "Show only entries with changes" : "Прикажи само уносе са изменама", + "Simple" : "Једноставно", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Једноставни упити: Основне текстуалне претраге са минималним параметрима (нпр. један појам за претрагу, без напредних филтера)", + "Size" : "Величина", + "Slug" : "Слаг", + "Soft Delete Mode" : "Режим меког брисања", + "Soft Deleted Items" : "Меко обрисане ставке", + "SolrCloud mode enabled" : "SolrCloud режим је омогућен", + "Source" : "Извор", + "Sources" : "Извори", + "Sources:" : "Извори:", + "Standalone SOLR mode" : "Самостални SOLR режим", + "Start Conversation" : "Започни разговор", + "Start Vectorization" : "Започни векторизацију", + "Start a conversation" : "Започни разговор", + "Starting names cache warmup..." : "Започињање загревања кеша назива...", + "Starting..." : "Започињање...", + "Statistics" : "Статистика", + "Stats calculation not yet implemented" : "Израчунавање статистике још увек није имплементирано", + "Status" : "Статус", + "Status Code" : "Статусни код", + "String Constraints" : "Ограничења ниски", + "Success" : "Успех", + "Success Rate" : "Стопа успеха", + "Success Status" : "Статус успеха", + "Successful" : "Успешно", + "Successfully joined organisation" : "Успешно сте се придружили организацији", + "Sync Table" : "Синхронизуј табелу", + "Synchronization Settings" : "Подешавања синхронизације", + "System Default" : "Системско подразумевано", + "Technical Description" : "Технички опис", + "Technical description for developers and administrators" : "Технички опис за програмере и администраторе", + "Temperature" : "Температура", + "Templates" : "Шаблони", + "Test" : "Тестирај", + "Test Chat" : "Тестирај ћаскање", + "Test Connection" : "Тестирај везу", + "Test Embedding" : "Тестирај уграђивање", + "Test endpoint executed successfully" : "Тест крајња тачка је успешно извршена", + "Test webhook delivered successfully" : "Тест веб-кукица је успешно испоручена", + "Test webhook delivery failed" : "Испорука тест веб-кукице није успела", + "Test webhook sent successfully" : "Тест веб-кукица је успешно послата", + "Testing..." : "Тестирање...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Текстуални делови се генеришу током издвајања Датотека и чувају у бази података. Векторизација чита ове унапред подељене Датотеке и претвара их у уграђивања.", + "Text extracted successfully" : "Текст је успешно издвојен", + "Text extraction disabled" : "Издвајање текста је онемогућено", + "The URL where webhook events will be sent" : "URL на који ће се слати догађаји веб-кукице", + "The requested conversation does not exist" : "Тражени разговор не постоји", + "The saved view and all its search configuration" : "Сачувани приказ и сва његова конфигурација претраге", + "There are no audit trail entries matching your current filters." : "Нема уноса ревизорског трага који одговарају вашим тренутним филтерима.", + "There are no deleted items matching your current filters." : "Нема обрисаних ставки које одговарају вашим тренутним филтерима.", + "There are no search trail entries matching your current filters." : "Нема уноса трага претраге који одговарају вашим тренутним филтерима.", + "There are no webhook log entries matching your filters." : "Нема уноса дневника веб-кукице који одговарају вашим филтерима.", + "This action cannot be undone." : "Ова радња се не може опозвати.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Ова радња се не може опозвати. Уверите се да ниједна колекција не користи овај ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Овај агент нема приказе или алате који се могу конфигурисати, али и даље можете подесити RAG подешавања испод.", + "This analysis may take some time" : "Ова анализа може потрајати", + "This audit trail entry does not contain any change information." : "Овај унос ревизорског трага не садржи никакве информације о изменама.", + "This configuration has no data defined." : "Ова конфигурација нема дефинисане податке.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ова крајња тачка је застарела. Уместо тога користите крајње тачке засноване на деловима.", + "This entity has no relations to objects or files" : "Овај ентитет нема релације са Објектима или Датотекама", + "This is a test webhook from OpenRegister" : "Ово је тест веб-кукица из OpenRegister-а", + "This schema must use Magic Table configuration to sync" : "Ова Шема мора да користи Magic Table конфигурацију за синхронизацију", + "This source has no associated registers." : "Овај Извор нема придружене Регистре.", + "This will copy the _default ConfigSet with the new name" : "Ово ће копирати _default ConfigSet са новим називом", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Ово ће трајно обрисати СВА уграђивања (векторе) из базе података. Мораћете поново да векторизујете све Објекте и Датотеке. Ова радња се не може опозвати.\\n\\nДа ли сте сигурни да желите да наставите?", + "This will permanently delete:" : "Ово ће трајно обрисати:", + "Timeout (seconds)" : "Истек (секунде)", + "Timestamp" : "Временска ознака", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Савет: Омогућите само приказе којима је потребна семантичка претрага да бисте минимизирали трошкове уграђивања. Једноставним табелама за претраживање ретко је потребна векторизација.", + "Title" : "Наслов", + "To Date" : "До датума", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Да бисте подесили величину делова или стратегију, идите на Конфигурација Датотека → Ограничења обраде.", + "To change the embedding provider or model, go to LLM Configuration." : "Да бисте променили провајдера уграђивања или модел, идите на LLM конфигурацију.", + "To date" : "До датума", + "Toggle search sidebar" : "Прикажи/сакриј бочну траку претраге", + "Tools" : "Алати", + "Top Deleters" : "Највећи брисачи", + "Total" : "Укупно", + "Total Audit Trails" : "Укупно ревизорских трагова", + "Total Chunks Available:" : "Укупно доступних делова:", + "Total Deleted Items" : "Укупно обрисаних ставки", + "Total Members" : "Укупно чланова", + "Total Objects" : "Укупно Објеката", + "Total Objects in Database:" : "Укупно Објеката у бази података:", + "Total Organisations" : "Укупно организација", + "Total Results" : "Укупно резултата", + "Total Searches" : "Укупно претрага", + "Total Size" : "Укупна величина", + "Total Storage" : "Укупно складиште", + "Total:" : "Укупно:", + "Totals" : "Укупно", + "Type" : "Тип", + "Type Issue" : "Проблем типа", + "Type Variations:" : "Варијације типа:", + "Type must be \"positive\" or \"negative\"" : "Тип мора бити „positive“ или „negative“", + "Type to search for groups" : "Куцајте да претражите групе", + "Type to search for users" : "Куцајте да претражите кориснике", + "Type to search groups..." : "Куцајте да претражите групе...", + "Type to search users..." : "Куцајте да претражите кориснике...", + "URL" : "URL", + "URL where Ollama is running" : "URL на коме се покреће Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Непромењено", + "Unique Search Terms" : "Јединствени појмови за претрагу", + "Unique Terms" : "Јединствени појмови", + "Unknown" : "Непознато", + "Unknown Register" : "Непознат Регистар", + "Unknown Schema" : "Непозната Шема", + "Untitled View" : "Приказ без наслова", + "Update" : "Ажурирај", + "Update Operations" : "Операције ажурирања", + "Update configuration: ..." : "Ажурирај конфигурацију: ...", + "Update register OAS: ..." : "Ажурирај OAS Регистра: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Ажурирај векторе када се подаци Објекта промене (препоручено за тачну претрагу)", + "Updated" : "Ажурирано", + "Updated At" : "Ажурирано", + "Updated:" : "Ажурирано:", + "Uptime:" : "Време рада:", + "Usage Count" : "Број употреба", + "Use SolrCloud with Zookeeper for distributed search" : "Користи SolrCloud са Zookeeper-ом за дистрибуирану претрагу", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Користите филтере да сузите уносе ревизорског трага по Регистру, Шеми, типу радње, кориснику, опсегу датума или ИД-у Објекта.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Користите филтере да сузите обрисане ставке по Регистру, Шеми, датуму брисања или кориснику који их је обрисао.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Користите филтере да сузите уносе трага претраге по Регистру, Шеми, статусу успеха, кориснику, опсегу датума, појмовима за претрагу или метрикама перформанси.", + "Used By Collections" : "Користе колекције", + "User" : "Корисник", + "User Agent Statistics" : "Статистика корисничких агената", + "User Description" : "Кориснички опис", + "User rate limits cleared successfully" : "Ограничења учесталости корисника су успешно обрисана", + "User removed successfully" : "Корисник је успешно уклоњен", + "User-friendly description shown in forms and help text" : "Кориснику разумљив опис приказан у обрасцима и тексту помоћи", + "Username" : "Корисничко име", + "Username for SOLR authentication (optional)" : "Корисничко име за SOLR потврду идентитета (опционо)", + "Users" : "Корисници", + "Using Pre-Generated Chunks" : "Коришћење унапред генерисаних делова", + "Validate" : "Валидирај", + "Validate Objects" : "Валидирај Објекте", + "Validation completed successfully" : "Валидација је успешно завршена", + "Validation failed" : "Валидација није успела", + "Value" : "Вредност", + "Vector Dimensions" : "Векторске димензије", + "Vector Search Backend" : "Позадински систем векторске претраге", + "Vector field: _embedding_" : "Векторско поље: _embedding_", + "Vectorization Triggers" : "Окидачи векторизације", + "Vectorize All Files" : "Векторизуј све Датотеке", + "Vectorize All Objects" : "Векторизуј све Објекте", + "Vectorize all file types" : "Векторизуј све типове Датотека", + "Vectorize all views" : "Векторизуј све приказе", + "Vectorize on object creation" : "Векторизуј при креирању Објекта", + "Vectors will be stored in your existing object and file collections" : "Вектори ће бити сачувани у вашим постојећим колекцијама Објеката и Датотека", + "Version" : "Верзија", + "Very High" : "Веома висок", + "View API Docs" : "Прикажи API документацију", + "View Changes" : "Прикажи измене", + "View Details" : "Прикажи детаље", + "View Error" : "Прикажи грешку", + "View File" : "Прикажи Датотеку", + "View Full Details" : "Прикажи све детаље", + "View Logs" : "Прикажи дневнике", + "View Name" : "Назив приказа", + "View Object" : "Прикажи Објекат", + "View Parameters" : "Параметри приказа", + "View Selection" : "Избор приказа", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Прегледајте и анализирајте дневнике трагова претраге са напредним могућностима филтрирања и аналитике", + "View and analyze system audit trails with advanced filtering capabilities" : "Прегледајте и анализирајте системске ревизорске трагове са напредним могућностима филтрирања", + "View deleted successfully" : "Приказ је успешно обрисан", + "View entity details and manage relations" : "Прегледајте детаље ентитета и управљајте релацијама", + "View name" : "Назив приказа", + "View name is required" : "Назив приказа је обавезан", + "View saved successfully!" : "Приказ је успешно сачуван!", + "View search analytics and manage search logs" : "Прегледајте аналитику претраге и управљајте дневницима претраге", + "View successfully deleted" : "Приказ је успешно обрисан", + "View successfully updated" : "Приказ је успешно ажуриран", + "View updated successfully!" : "Приказ је успешно ажуриран!", + "View webhook delivery logs and filter by webhook" : "Прегледајте дневнике испоруке веб-кукице и филтрирајте по веб-кукици", + "Views" : "Прикази", + "Visible to users" : "Видљиво корисницима", + "Wait for Response" : "Чекај на одговор", + "Wait for webhook response before continuing (required for request/response flows)" : "Чекај на одговор веб-кукице пре наставка (обавезно за токове захтев/одговор)", + "Warmup Names Cache" : "Загреј кеш назива", + "Warning" : "Упозорење", + "Warning:" : "Упозорење:", + "Webhook" : "Веб-кукица", + "Webhook Log Details" : "Детаљи дневника веб-кукице", + "Webhook Logs" : "Дневници веб-кукице", + "Webhook created successfully" : "Веб-кукица је успешно креирана", + "Webhook deleted" : "Веб-кукица је обрисана", + "Webhook retry delivered successfully" : "Поновни покушај веб-кукице је успешно испоручен", + "Webhook retry delivery failed" : "Испорука поновног покушаја веб-кукице није успела", + "Webhook updated" : "Веб-кукица је ажурирана", + "Webhook updated successfully" : "Веб-кукица је успешно ажурирана", + "Webhooks" : "Веб-кукице", + "Weekly" : "Недељно", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Умотај корисни терет веб-кукице у CloudEvents формат за бољу интероперабилност", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Да", + "You" : "Ви", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Можете креирати нове ConfigSet-ове на основу _default шаблона или отпремити прилагођене директно на ваш SOLR сервер.", + "You do not have access to this conversation" : "Немате приступ овом разговору", + "You do not have permission to delete this conversation" : "Немате дозволу да обришете овај разговор", + "You do not have permission to modify this conversation" : "Немате дозволу да измените овај разговор", + "You do not have permission to restore this conversation" : "Немате дозволу да вратите овај разговор", + "You must be logged in to favorite views" : "Морате бити пријављени да бисте означили приказе као омиљене", + "You need an AI agent to start a conversation." : "Потребан вам је AI агент да започнете разговор.", + "Your Fireworks AI API key. Get one at" : "Ваш Fireworks AI API кључ. Набавите га на", + "Your OpenAI API key. Get one at" : "Ваш OpenAI API кључ. Набавите га на", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Ваше повратне информације су забележене. Опционо, овде можете дати додатне детаље...", + "Zookeeper Hosts" : "Zookeeper домаћини", + "Zookeeper Port" : "Zookeeper порт", + "Zookeeper Settings (SolrCloud)" : "Zookeeper подешавања (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper ниска везе за SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Број Zookeeper порта (опционо, подразумевано 2181)", + "chunk" : "део", + "chunks" : "делови", + "chunks processed" : "делова обрађено", + "configuration(s)" : "конфигурација(е)", + "e.g., lib/Settings/config.json" : "нпр. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "нпр. lib/Settings/register.json", + "failed" : "неуспешно", + "file" : "датотека", + "files" : "датотеке", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "чланови", + "menu or contact someone with permission to create agents." : "менију или контактирајте некога са дозволом за креирање агената.", + "messages" : "поруке", + "n8n URL and API key are required" : "n8n URL и API кључ су обавезни", + "n8n configuration saved successfully" : "n8n конфигурација је успешно сачувана", + "n8n connection not configured" : "n8n веза није конфигурисана", + "n8n connection successful" : "n8n веза успешна", + "n8n connection test successful" : "n8n тест везе успешан", + "n8n integration disabled" : "n8n интеграција је онемогућена", + "n8n integration enabled" : "n8n интеграција је омогућена", + "n8n project initialized successfully" : "n8n пројекат је успешно иницијализован", + "n8n settings saved successfully" : "n8n подешавања су успешно сачувана", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "објекти", + "objects processed" : "објеката обрађено", + "or pick a range" : "или изаберите опсег", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "пружа добру равнотежу између брзине и тачности.", + "register(s)" : "Регистар(и)", + "register(s) selected" : "Регистар(и) изабрано", + "required" : "обавезно", + "results" : "резултати", + "schema(s)" : "Шема(е)", + "schema(s) selected" : "Шема(е) изабрано", + "sk-..." : "sk-...", + "this application" : "ова апликација", + "to" : "до", + "vectorized" : "векторизовано", + "views selected" : "приказа изабрано", + "ℹ️ Current Configuration" : "ℹ️ Тренутна конфигурација", + "⚡ Batch Processing" : "⚡ Групна обрада", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Свеобухватан контекст ✓ Мања вероватноћа да се пропусте детаљи ✗ Спорији одговори ✗ Може укључити мање релевантне информације", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Бржи одговори ✓ Фокусиранији одговори ✗ Може пропустити релевантне информације", + "✨ AI Features" : "✨ AI функције", + "All search trails cleared successfully" : "Сви трагови претраге су успешно обрисани", + "Multiple search trail deletion not implemented yet" : "Брисање више трагова претраге још увек није имплементирано", + "No expired search trails found to clear" : "Није пронађен ниједан истекли траг претраге за брисање", + "Missing message" : "Недостаје порука", + "message content is required" : "садржај поруке је обавезан", + "Missing conversation or agentUuid" : "Недостаје разговор или agentUuid", + "Access denied" : "Приступ одбијен", + "Conversation not found" : "Разговор није пронађен", + "AI service not configured" : "AI услуга није конфигурисана", + "Failed to process message" : "Обрада поруке није успела", + "Missing conversationId" : "Недостаје conversationId", + "conversationId is required" : "conversationId је обавезан", + "Failed to fetch conversation history" : "Преузимање историје разговора није успело", + "Failed to clear conversation" : "Брисање разговора није успело", + "Invalid feedback type" : "Неважећи тип повратних информација", + "type must be \"positive\" or \"negative\"" : "тип мора бити „positive“ или „negative“", + "Message not found" : "Порука није пронађена", + "Failed to save feedback" : "Чување повратних информација није успело", + "Failed to get chat statistics" : "Добављање статистике ћаскања није успело", + "Not Found" : "Није пронађено", + "Engine not found" : "Машина није пронађена", + "Consumer not found" : "Потрошач није пронађен", + "Backend parameter is required" : "Параметар позадинског система је обавезан", + "Failed to get database information: %s" : "Добављање информација о бази података није успело: %s", + "SOLR setup error: %s" : "Грешка при подешавању SOLR-а: %s", + "Reindex failed: %s" : "Поновно индексирање није успело: %s", + "Anonymized" : "Анонимизовано", + "Entities detected" : "Откривени ентитети", + "Extract Now" : "Издвоји одмах", + "Extracted at" : "Издвојено", + "Extraction" : "Издвајање", + "Failed to load extraction data" : "Учитавање података издвајања није успело", + "Failed to load register data" : "Учитавање података Регистра није успело", + "Failed to retrieve extraction status." : "Преузимање статуса издвајања није успело.", + "Failed to retrieve objects for file." : "Преузимање Објеката за Датотеку није успело.", + "No extraction data available for this file" : "Нема доступних података издвајања за ову Датотеку", + "No register objects reference this file" : "Ниједан Објекат Регистра не упућује на ову Датотеку", + "Not extracted" : "Није издвојено", + "Register Objects" : "Објекти Регистра", + "Risk level" : "Ниво ризика", + "Text chunks" : "Текстуални делови", + "Unknown error" : "Непозната грешка", + "Very high" : "Веома висок", + "{title} in {register} / {schema}" : "{title} у {register} / {schema}", + "Push Notifications" : "Push обавештења", + "Real-time push notification status via notify_push" : "Статус push обавештења у реалном времену преко notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push у реалном времену није доступан — апликација notify_push није инсталирана", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Инсталирајте апликацију notify_push из Nextcloud продавнице апликација да бисте омогућили ажурирања у реалном времену.", + "Open Nextcloud App Store" : "Отвори Nextcloud продавницу апликација", + "notify_push is installed but not yet active" : "notify_push је инсталиран, али још увек није активан", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push је инсталиран, али OpenRegister још увек није потврдио успешан push. Покрените чување Објекта да активирате или проверите вашу notify_push конфигурацију.", + "notify_push configuration guide" : "водич за конфигурацију notify_push", + "Realtime push active" : "Push у реалном времену активан", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push обавештења у реалном времену су активна. Повезани клијенти примају тренутна ажурирања када се Објекти креирају, ажурирају или бришу.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +); diff --git a/l10n/sr.json b/l10n/sr.json new file mode 100644 index 0000000000..3986bff2b2 --- /dev/null +++ b/l10n/sr.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Potreban Dolphin)", + "(no subject)": "(bez predmeta)", + "(no title)": "(bez naslova)", + "(unnamed)": "(bez naziva)", + "(untitled card)": "(kartica bez naslova)", + "({count} selected)": "({count} odabrano)", + ", {failed} failed": ", {failed} neuspješno", + "0 = unlimited": "0 = neograničeno", + "A deactivation request is pending.": "Zahtjev za deaktivaciju je na čekanju.", + "AI Assistant": "AI asistent", + "AI Chat": "AI razgovor", + "AI service not configured": "AI usluga nije konfigurirana", + "API Key": "API ključ", + "API Request Quota (requests/day)": "Kvota API zahtjeva (zahtjeva/dan)", + "API Token Configuration": "Konfiguracija API tokena", + "API Tokens": "API tokeni", + "API tokens saved successfully": "API tokeni uspješno spremljeni", + "AVG / Verwerkingsregister": "AVG / Registar obrade", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG čl. 30 §4: ovo izvješće povezuje svaku aktivnost obrade s ukupnim brojem zapisa revizijskog traga koji joj se pripisuju. Revizori i Autoriteit Persoonsgegevens to koriste za nadzorni pregled.", + "About ConfigSets": "O ConfigSet stavkama", + "Access denied": "Pristup odbijen", + "Access organisation dashboard and analytics": "Pristup nadzornoj ploči i analitici organizacije", + "Account": "Račun", + "Action": "Radnja", + "Action Distribution": "Raspodjela radnji", + "Action:": "Radnja:", + "Actions": "Radnje", + "Active": "Aktivno", + "Active Collections": "Aktivne zbirke", + "Active Filters": "Aktivni filtri", + "Active Organisation:": "Aktivna organizacija:", + "Active Organisations": "Aktivne organizacije", + "Active filters:": "Aktivni filtri:", + "Active organisation changed successfully": "Aktivna organizacija uspješno promijenjena", + "Active organisation set successfully": "Aktivna organizacija uspješno postavljena", + "Activities": "Aktivnosti", + "Activity": "Aktivnost", + "Activity Period": "Razdoblje aktivnosti", + "Add": "Dodaj", + "Add Attachment": "Dodaj privitak", + "Add Endpoint": "Dodaj krajnju točku", + "Add Groups": "Dodaj grupe", + "Add Property to '{title}'": "Dodaj svojstvo u '{title}'", + "Add Register": "Dodaj registar", + "Add Schema": "Dodaj shemu", + "Add Source": "Dodaj izvor", + "Add User": "Dodaj korisnika", + "Add User to Organisation": "Dodaj korisnika u organizaciju", + "Add a contact from any of your address books to associate it with this object.": "Dodajte kontakt iz bilo kojeg svojeg adresara kako biste ga povezali s ovim objektom.", + "Add a file or files": "Dodajte datoteku ili datoteke", + "Add card": "Dodaj karticu", + "Add contact": "Dodaj kontakt", + "Add endpoint": "Dodaj krajnju točku", + "Add oneOf entry": "Dodaj oneOf unos", + "Add range": "Dodaj raspon", + "Add schema titles, descriptions, and register information to provide richer context for search": "Dodajte naslove shema, opise i informacije o registru kako biste pružili bogatiji kontekst za pretraživanje", + "Add to favorites": "Dodaj u favorite", + "Added": "Dodano", + "Added to favorites": "Dodano u favorite", + "Additional Information": "Dodatne informacije", + "Additional feedback saved. Thank you!": "Dodatne povratne informacije spremljene. Hvala vam!", + "Addresses": "Adrese", + "Admin": "Administrator", + "Advanced": "Napredno", + "Advanced Filters": "Napredni filtri", + "Advanced Options": "Napredne mogućnosti", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Napredni filtri nisu dostupni pri korištenju izvora baze podataka. Prebacite na Auto ili SOLR indeks za mogućnosti filtriranja.", + "After": "Nakon", + "Agent Type": "Vrsta agenta", + "Agent deleted successfully": "Agent uspješno izbrisan", + "Agent successfully created": "Agent uspješno stvoren", + "Agent successfully updated": "Agent uspješno ažuriran", + "Agents": "Agenti", + "Aggregated across schemas": "Agregirano kroz sheme", + "All": "Sve", + "All Categories": "Sve kategorije", + "All Collections": "Sve zbirke", + "All Confidence Levels": "Sve razine pouzdanosti", + "All ConfigSets": "Svi ConfigSet-ovi", + "All Files": "Sve datoteke", + "All Levels": "Sve razine", + "All Types": "Sve vrste", + "All Webhooks": "Svi webhookovi", + "All actions": "Sve radnje", + "All audit trails cleared successfully": "Svi revizijski tragovi uspješno obrisani", + "All clear": "Sve čisto", + "All file types": "Sve vrste datoteka", + "All registers": "Svi registri", + "All schemas": "Sve sheme", + "All search trails cleared successfully": "Svi tragovi pretraživanja uspješno obrisani", + "All searches": "Sva pretraživanja", + "All users": "Svi korisnici", + "All-day events": "Cjelodnevni događaji", + "Allowed MIME Types": "Dopuštene MIME vrste", + "Already linked": "Već povezano", + "An error occurred while clearing audit trails": "Došlo je do pogreške prilikom brisanja revizijskih tragova", + "An error occurred while deleting the audit trail": "Došlo je do pogreške prilikom brisanja revizijskog traga", + "An error occurred while deleting the object": "Došlo je do pogreške prilikom brisanja objekta", + "An error occurred while deleting the view": "Došlo je do pogreške prilikom brisanja prikaza", + "An error occurred while permanently deleting the objects": "Došlo je do pogreške prilikom trajnog brisanja objekata", + "An error occurred while restoring the objects": "Došlo je do pogreške prilikom vraćanja objekata", + "An error occurred while saving the organisation": "Došlo je do pogreške prilikom spremanja organizacije", + "Analysis completed:": "Analiza dovršena:", + "Analysis steps:": "Koraci analize:", + "Analytics": "Analitika", + "Analyze Objects": "Analiziraj objekte", + "Analyze Properties": "Analiziraj svojstva", + "Analyze existing properties for improvement opportunities": "Analizirajte postojeća svojstva za mogućnosti poboljšanja", + "Analyzing...": "Analiziranje...", + "Anonymized": "Anonimizirano", + "Anonymous Group": "Anonimna grupa", + "Any favorites and sharing settings for this view": "Sve postavke favorita i dijeljenja za ovaj prikaz", + "Any user": "Bilo koji korisnik", + "App ID": "ID aplikacije", + "App store cache invalidated successfully": "Predmemorija trgovine aplikacija uspješno poništena", + "App store cache invalidated: {invalidated}": "Predmemorija trgovine aplikacija poništena: {invalidated}", + "Application deleted successfully": "Aplikacija uspješno izbrisana", + "Application identifier for this configuration (optional)": "Identifikator aplikacije za ovu konfiguraciju (neobavezno)", + "Application settings": "Postavke aplikacije", + "Application successfully created": "Aplikacija uspješno stvorena", + "Application successfully updated": "Aplikacija uspješno ažurirana", + "Applications": "Aplikacije", + "Apply": "Primijeni", + "Apply Changes": "Primijeni promjene", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Primijenite ulogu na sve sheme u ovom registru koje nemaju eksplicitna nadjačavanja autorizacije.", + "Approval Progress": "Napredak odobravanja", + "Approve": "Odobri", + "Approved": "Odobreno", + "Archive": "Arhiviraj", + "Archive conversation": "Arhiviraj razgovor", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arhivirati ovu aktivnost obrade? Reci revizijskog traga nastavit će je referencirati.", + "Archived conversations are hidden from your active list": "Arhivirani razgovori skriveni su s vašeg aktivnog popisa", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Jeste li sigurni da želite IZBRISATI zbirku \"{name}\"?\n\nOvo će:\n• Trajno izbrisati zbirku i sve njezine podatke\n• Ukloniti sve indeksirane dokumente\n• Ova se radnja ne može poništiti", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Jeste li sigurni da želite očistiti stare tragove pretraživanja? Ovo će izbrisati unose starije od 30 dana.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Jeste li sigurni da želite obrisati sve podatke iz zbirke \"{name}\"?\n\nOvo će:\n• Izbrisati sve indeksirane dokumente\n• Zadržati strukturu zbirke netaknutom\n• Ova se radnja ne može poništiti", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Jeste li sigurni da želite izbrisati aplikaciju \"{name}\"? Ova se radnja ne može poništiti.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Jeste li sigurni da želite izbrisati odabrane revizijske tragove? Ova se radnja ne može poništiti.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Jeste li sigurni da želite izbrisati odabrane tragove pretraživanja? Ova se radnja ne može poništiti.", + "Are you sure you want to delete this ConfigSet?": "Jeste li sigurni da želite izbrisati ovaj ConfigSet?", + "Are you sure you want to delete this configset?": "Jeste li sigurni da želite izbrisati ovaj configset?", + "Are you sure you want to leave '{name}'?": "Jeste li sigurni da želite napustiti '{name}'?", + "Are you sure you want to permanently delete": "Jeste li sigurni da želite trajno izbrisati", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Jeste li sigurni da želite ponovno indeksirati zbirku \"{name}\"?\n\nOvo će:\n• Ponovno izgraditi indeks sa svim objektima\n• Potrajati nekoliko minuta do dovršetka\n• Možda utjecati na performanse pretraživanja tijekom ponovnog indeksiranja", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Jeste li sigurni da želite ukloniti shemu \"{schema}\" iz registra \"{register}\"? Ova se radnja ne može poništiti.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Jeste li sigurni da želite meko izbrisati {count} aktivnih objekata za shemu \"{schema}\"?\n\nBit će označeni kao izbrisani, ali se mogu trajno ukloniti kasnije.", + "Array Object Configuration:": "Konfiguracija objekta polja:", + "Articles": "Članci", + "Ask a question...": "Postavite pitanje...", + "Ask questions about your data using natural language": "Postavljajte pitanja o svojim podacima koristeći prirodni jezik", + "Assigned collaborative tags": "Dodijeljene suradničke oznake", + "Assignment notifications": "Obavijesti o dodjeli", + "Attachment added to {name}": "Privitak dodan u {name}", + "Attempt": "Pokušaj", + "Audit Trail Changes": "Promjene revizijskog traga", + "Audit Trail Details": "Pojedinosti revizijskog traga", + "Audit Trail Management": "Upravljanje revizijskim tragom", + "Audit Trail Statistics": "Statistika revizijskog traga", + "Audit Trails": "Revizijski tragovi", + "Audit trail data copied to clipboard": "Podaci revizijskog traga kopirani u međuspremnik", + "Audit trail deleted successfully": "Revizijski trag uspješno izbrisan", + "Audit trail successfully deleted": "Revizijski trag uspješno izbrisan", + "Authentication": "Autentifikacija", + "Auto (e.g. Y for year, F Y for month)": "Auto (npr. Y za godinu, F Y za mjesec)", + "Auto share": "Automatsko dijeljenje", + "Auto-commit disabled": "Automatska potvrda onemogućena", + "Auto-commit enabled": "Automatska potvrda omogućena", + "Auto-create Default Organisation": "Automatski stvori zadanu organizaciju", + "Auto-retry failed vectorizations": "Automatski ponovno pokušaj neuspjele vektorizacije", + "Automatically commit changes to SOLR index": "Automatski potvrdi promjene u SOLR indeks", + "Automatically connect to the federation network.": "Automatski se poveži na federacijsku mrežu.", + "Automatically create a default organisation if none exists when the app is initialized": "Automatski stvori zadanu organizaciju ako nijedna ne postoji kada se aplikacija inicijalizira", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Automatski generiraj vektorska ugrađivanja iz tekstualnih dijelova kada se datoteke prenesu i obrade", + "Automatically generate vector embeddings when objects are created or updated": "Automatski generiraj vektorska ugrađivanja kada se objekti stvore ili ažuriraju", + "Automatically retry failed vectorization attempts (max 3 retries)": "Automatski ponovno pokušaj neuspjele pokušaje vektorizacije (najviše 3 ponovna pokušaja)", + "Automatically update catalogues.": "Automatski ažuriraj kataloge.", + "Available Filters": "Dostupni filtri", + "Available Workflows": "Dostupni tijekovi rada", + "Available placeholders:": "Dostupni rezervirani znakovi:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Vaš pružatelj autentifikacije ne podržava promjene avatara.", + "Avatar removed": "Avatar uklonjen", + "Avatar updated successfully": "Avatar uspješno ažuriran", + "Avg Execution Time": "Prosj. vrijeme izvršavanja", + "Avg Members/Org": "Prosj. članova/org.", + "Avg Object Views/Session": "Prosj. pregleda objekata/sesiji", + "Avg Response Time": "Prosj. vrijeme odgovora", + "Avg Results/Search": "Prosj. rezultata/pretraživanju", + "Avg Searches/Session": "Prosj. pretraživanja/sesiji", + "Back": "Natrag", + "Back to Entities": "Natrag na entitete", + "Back to Registers": "Natrag na registre", + "Back to Webhooks": "Natrag na webhookove", + "Back to applications": "Natrag na aplikacije", + "Back to entities": "Natrag na entitete", + "Backend parameter is required": "Parametar pozadinskog sustava je obavezan", + "Backend updated successfully. Please reload the application.": "Pozadinski sustav uspješno ažuriran. Molimo ponovno učitajte aplikaciju.", + "Bandwidth Quota (MB/month)": "Kvota propusnosti (MB/mjesec)", + "Base URL (Optional)": "Osnovni URL (neobavezno)", + "Basic Information": "Osnovne informacije", + "Batch Size": "Veličina serije", + "Batch extraction completed": "Serijska ekstrakcija dovršena", + "Before": "Prije", + "Before object vectorization can work:": "Prije nego što vektorizacija objekata može raditi:", + "Behavior": "Ponašanje", + "Behavior Issue": "Problem s ponašanjem", + "Beschrijving": "Opis", + "Bewaartermijn": "Rok čuvanja", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Rok čuvanja (ISO-8601 trajanje, npr. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob pohrana je povučena. Svi objekti sada koriste magične tablice.", + "Bookmarks": "Knjižne oznake", + "Branch": "Grana", + "Bulk Role Assignment: {title}": "Skupna dodjela uloga: {title}", + "Bulk delete operation completed successfully": "Operacija skupnog brisanja uspješno dovršena", + "Bulk save operation completed successfully": "Operacija skupnog spremanja uspješno dovršena", + "Business Data": "Poslovni podaci", + "CSV": "CSV", + "Cache cleared successfully": "Predmemorija uspješno obrisana", + "Cache grew from {old} to {new} entries.": "Predmemorija je narasla s {old} na {new} unosa.", + "Cache shrunk from {old} to {new} entries.": "Predmemorija se smanjila s {old} na {new} unosa.", + "Cache stayed the same at {size} entries.": "Predmemorija je ostala ista na {size} unosa.", + "Calculate Sizes": "Izračunaj veličine", + "Calendar": "Kalendar", + "Calendar Provider Configuration": "Konfiguracija pružatelja kalendara", + "Calendar integration is not available": "Integracija kalendara nije dostupna", + "Calendar name": "Naziv kalendara", + "Cancel": "Odustani", + "Cancel deactivation request": "Otkaži zahtjev za deaktivaciju", + "Cannot delete: objects are still attached": "Nije moguće izbrisati: objekti su još uvijek priloženi", + "Cannot edit: This register is managed by external configuration {title}": "Nije moguće uređivati: ovim registrom upravlja vanjska konfiguracija {title}", + "Cannot remove schema with existing objects ({count} objects)": "Nije moguće ukloniti shemu s postojećim objektima ({count} objekata)", + "Cannot remove schema {schema} because it contains {count} objects": "Nije moguće ukloniti shemu {schema} jer sadrži {count} objekata", + "Cards": "Kartice", + "Cascade delete": "Kaskadno brisanje", + "Categories of data subjects (one per line)": "Kategorije ispitanika (jedna po retku)", + "Categories of personal data (one per line)": "Kategorije osobnih podataka (jedna po retku)", + "Categorieën betrokkenen (één per regel)": "Kategorije ispitanika (jedna po retku)", + "Categorieën persoonsgegevens (één per regel)": "Kategorije osobnih podataka (jedna po retku)", + "Category": "Kategorija", + "Change Type": "Vrsta promjene", + "Change password": "Promijeni lozinku", + "Changes": "Promjene", + "Changes copied to clipboard": "Promjene kopirane u međuspremnik", + "Changes for Audit Trail #{id}": "Promjene za revizijski trag #{id}", + "Chat Model": "Model razgovora", + "Chat Provider": "Pružatelj razgovora", + "Chat Provider (RAG)": "Pružatelj razgovora (RAG)", + "Chat Settings": "Postavke razgovora", + "Chat provider connection successful!": "Veza s pružateljem razgovora uspješna!", + "Chat settings": "Postavke razgovora", + "Chat test failed: {error}": "Test razgovora neuspješan: {error}", + "Choose a register": "Odaberite registar", + "Choose a schema": "Odaberite shemu", + "Choose how vector similarity calculations are performed for semantic search": "Odaberite kako se izračuni vektorske sličnosti izvode za semantičko pretraživanje", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Odaberite koje vrste datoteka uključiti u proces vektorizacije. Obradit će se samo datoteke s izvučenim tekstom i dijelovima.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Odaberite koje prikaze uključiti u proces vektorizacije. Ostavite prazno za obradu svih prikaza na temelju vaše konfiguracije.", + "Chunk": "Dio", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Brisanje dijelova još nije implementirano. Koristite krajnje točke temeljene na dijelovima.", + "Chunks": "Dijelovi", + "Chunks to Vectorize:": "Dijelovi za vektorizaciju:", + "Cleanup Old Trails": "Očisti stare tragove", + "Cleanup completed": "Čišćenje dovršeno", + "Cleanup completed successfully. Deleted {count} entries.": "Čišćenje uspješno dovršeno. Izbrisano {count} unosa.", + "Cleanup failed: {error}": "Čišćenje neuspješno: {error}", + "Clear All Embeddings": "Obriši sva ugrađivanja", + "Clear Entries": "Obriši unose", + "Clear Filtered Audit Trails": "Obriši filtrirane revizijske tragove", + "Clear Filters": "Obriši filtre", + "Clear Index": "Obriši indeks", + "Clear Selection": "Obriši odabir", + "Clear all": "Obriši sve", + "Clear all filters": "Obriši sve filtre", + "Clear date range": "Obriši raspon datuma", + "Clear filters": "Obriši filtre", + "Clearing...": "Brisanje...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Kliknite Pokreni provjeru usklađenosti kako biste pronašli sheme u kojima je otkriven PII, ali ne postoji bilješka o aktivnosti obrade.", + "Click to subscribe to notifications": "Kliknite za pretplatu na obavijesti", + "Click to unsubscribe from notifications": "Kliknite za odjavu od obavijesti", + "Close": "Zatvori", + "Cluster name": "Naziv klastera", + "Code": "Kod", + "Code (short readable key, e.g. v-2026-001)": "Kod (kratki čitljivi ključ, npr. v-2026-001)", + "Collapse details": "Sažmi pojedinosti", + "Collection Management": "Upravljanje zbirkama", + "Collection Name": "Naziv zbirke", + "Collection assignments updated successfully": "Dodjele zbirki uspješno ažurirane", + "Collection cleared successfully": "Zbirka uspješno obrisana", + "Collection copied successfully": "Zbirka uspješno kopirana", + "Collection created successfully": "Zbirka uspješno stvorena", + "Collection deleted successfully": "Zbirka uspješno izbrisana", + "Collection used to store and index file metadata and content": "Zbirka koja se koristi za pohranu i indeksiranje metapodataka i sadržaja datoteka", + "Collection used to store and index object data": "Zbirka koja se koristi za pohranu i indeksiranje podataka objekata", + "Collections:": "Zbirke:", + "Color": "Boja", + "Comment...": "Komentar...", + "Commit Message": "Poruka potvrde", + "Commit Within (ms)": "Potvrdi unutar (ms)", + "Commit message": "Poruka potvrde", + "Compare current schema with real object data": "Usporedi trenutnu shemu sa stvarnim podacima objekata", + "Completed": "Dovršeno", + "Complex": "Složeno", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Složeni upiti: napredna pretraživanja s više filtara, operatora i složenih kombinacija parametara", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Složeni upiti: napredna pretraživanja s više filtara, operatora i složenih kombinacija parametara", + "Compliance": "Usklađenost", + "Confidence Level": "Razina pouzdanosti", + "Confidence Score": "Rezultat pouzdanosti", + "Confidence: {confidence}%": "Pouzdanost: {confidence}%", + "Config must be provided as an object": "Konfiguracija mora biti navedena kao objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "Upravljanje ConfigSet stavkama", + "ConfigSet Name": "Naziv ConfigSet stavke", + "ConfigSet created successfully": "ConfigSet uspješno stvoren", + "ConfigSet deleted successfully": "ConfigSet uspješno izbrisan", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSet stavke definiraju shemu i konfiguraciju za vaše SOLR zbirke. Sadrže definicije polja, analizatore i druge postavke pretraživanja.", + "Configuration": "Konfiguracija", + "Configuration Data": "Podaci konfiguracije", + "Configuration is up to date": "Konfiguracija je ažurna", + "Configuration saved": "Konfiguracija spremljena", + "Configuration saved successfully.": "Konfiguracija uspješno spremljena.", + "Configuration type (default, application, etc.)": "Vrsta konfiguracije (zadana, aplikacija itd.)", + "Configurations": "Konfiguracije", + "Configure API tokens for external service integrations": "Konfigurirajte API tokene za integracije vanjskih usluga", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurirajte Apache SOLR tražilicu za napredne mogućnosti pretraživanja", + "Configure Facets": "Konfiguriraj fasete", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurirajte pružatelje velikih jezičnih modela (LLM) za značajke pokretane AI-jem uključujući semantičko pretraživanje, ugrađivanja i razgovor.", + "Configure SOLR Facets": "Konfiguriraj SOLR fasete", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurirajte osnovne postavke veze za vaš SOLR poslužitelj uključujući autentifikaciju i mrežne mogućnosti. Koristite zasebne dijaloge za upravljanje ConfigSet stavkama i zbirkama za upravljanje jezgrama i zbirkama.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurirajte osnovne postavke veze za vaš SOLR poslužitelj uključujući autentifikaciju i mrežne mogućnosti. Koristite zasebne dijaloge za upravljanje configset stavkama i zbirkama za upravljanje jezgrama i zbirkama.", + "Configure default organisation and organisation-related settings": "Konfigurirajte zadanu organizaciju i postavke vezane uz organizaciju", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurirajte kako se objekti baze podataka pretvaraju u vektorska ugrađivanja za semantičko pretraživanje. Objekti se izravno vektoriziraju bez potrebe za ekstrakcijom teksta.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurirajte kako se objekti pretvaraju u tekst prije vektorizacije. Ove postavke utječu na kvalitetu i kontekst pretraživanja.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurirajte pružatelje velikih jezičnih modela (LLM) za značajke pokretane AI-jem uključujući semantičko pretraživanje, ugrađivanja i razgovor.", + "Configure multi-organization support and tenant isolation": "Konfigurirajte podršku za više organizacija i izolaciju zakupaca", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurirajte parametre za vektorizaciju datoteka. Ovaj će proces generirati vektorska ugrađivanja za sve izvučene dijelove datoteka.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurirajte parametre za vektorizaciju objekata. Ovaj će proces generirati vektorska ugrađivanja za sve objekte koji odgovaraju filtrima vašeg prikaza.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurirajte ovu shemu da prikazuje objekte kao događaje u aplikaciji Nextcloud Calendar.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurirajte koje vrste podataka pretraživati i koliko izvora dohvatiti", + "Configuring field mappings...": "Konfiguriranje mapiranja polja...", + "Confirm Account Deactivation": "Potvrdi deaktivaciju računa", + "Confirm Object Deletion": "Potvrdi brisanje objekta", + "Confirm Object Restoration": "Potvrdi vraćanje objekta", + "Confirm Permanent Object Deletion": "Potvrdi trajno brisanje objekta", + "Confirm deactivation": "Potvrdi deaktivaciju", + "Connecting to SOLR cluster...": "Povezivanje s SOLR klasterom...", + "Connection Failed": "Veza neuspješna", + "Connection Settings": "Postavke veze", + "Connection Successful!": "Veza uspješna!", + "Connection failed": "Veza neuspješna", + "Connection protocol": "Protokol veze", + "Connection successful": "Veza uspješna", + "Connection timeout in seconds": "Istek veze u sekundama", + "Constant delay between retries (5 minutes)": "Stalni odgoda između ponovnih pokušaja (5 minuta)", + "Constraint Issue": "Problem s ograničenjem", + "Consumer not found": "Potrošač nije pronađen", + "Contacts": "Kontakti", + "Contacts integration is not available": "Integracija kontakata nije dostupna", + "Content Filters": "Filtri sadržaja", + "Continue to Properties": "Nastavi na svojstva", + "Contracts": "Ugovori", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Kontrolirajte koji se prikazi objekata trebaju vektorizirati radi smanjenja troškova API-ja. Vektorizirajte samo prikaze koji imaju koristi od semantičkog pretraživanja.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Kontrolirajte koje prikaze i alate AI može koristiti u ovom razgovoru. Prema zadanim postavkama, sve su mogućnosti agenta omogućene.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Kontrolira nasumičnost (0 = determinističko, 2 = vrlo kreativno)", + "Conversation ID is required": "ID razgovora je obavezan", + "Conversation archived": "Razgovor arhiviran", + "Conversation archived successfully": "Razgovor uspješno arhiviran", + "Conversation cleared successfully": "Razgovor uspješno obrisan", + "Conversation deleted": "Razgovor izbrisan", + "Conversation not found": "Razgovor nije pronađen", + "Conversation permanently deleted": "Razgovor trajno izbrisan", + "Conversation renamed": "Razgovor preimenovan", + "Conversation restored": "Razgovor vraćen", + "Conversation started with {agent}": "Razgovor započet s {agent}", + "Conversation title": "Naslov razgovora", + "Conversations": "Razgovori", + "Converting {schema} to blob storage...": "Pretvaranje {schema} u blob pohranu...", + "Converting {schema} to magic table...": "Pretvaranje {schema} u magičnu tablicu...", + "Copied!": "Kopirano!", + "Copy": "Kopiraj", + "Copy Changes": "Kopiraj promjene", + "Copy Collection": "Kopiraj zbirku", + "Copy Data": "Kopiraj podatke", + "Copy Full Data": "Kopiraj sve podatke", + "Copy of {name}": "Kopija od {name}", + "Copy to clipboard": "Kopiraj u međuspremnik", + "Copying...": "Kopiranje...", + "Costs": "Troškovi", + "Count": "Broj", + "Counting objects...": "Brojanje objekata...", + "Create": "Stvori", + "Create API Token": "Stvori API token", + "Create Agent": "Stvori agenta", + "Create Application": "Stvori aplikaciju", + "Create Collection": "Stvori zbirku", + "Create ConfigSet": "Stvori ConfigSet", + "Create New Collection": "Stvori novu zbirku", + "Create New ConfigSet": "Stvori novi ConfigSet", + "Create Operations": "Stvori operacije", + "Create Organisation": "Stvori organizaciju", + "Create Webhook": "Stvori webhook", + "Create a copy of collection:": "Stvori kopiju zbirke:", + "Create a new ConfigSet based on the _default template": "Stvori novi ConfigSet na temelju predloška _default", + "Create a new SOLR collection from an existing ConfigSet": "Stvori novu SOLR zbirku iz postojećeg ConfigSet-a", + "Create a new SOLR collection from an existing configset": "Stvori novu SOLR zbirku iz postojećeg configset-a", + "Create a new configset based on the _default template": "Stvori novi configset na temelju predloška _default", + "Create a new event or link an existing one from any of your calendars.": "Stvorite novi događaj ili povežite postojeći iz bilo kojeg svojeg kalendara.", + "Create another": "Stvori još jedan", + "Create copies of": "Stvori kopije od", + "Create event": "Stvori događaj", + "Create new token": "Stvori novi token", + "Create or link a Deck card to track work on this object.": "Stvorite ili povežite Deck karticu za praćenje rada na ovom objektu.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Stvorite prvu aktivnost obrade kako biste počeli označavati retke revizijskog traga njihovom AVG čl. 30 atribucijom.", + "Create your first AI agent to get started.": "Stvorite svojeg prvog AI agenta za početak.", + "Created": "Stvoreno", + "Created:": "Stvoreno:", + "Creating search collections...": "Stvaranje zbirki pretraživanja...", + "Creating...": "Stvaranje...", + "Current": "Trenutno", + "Current Filters": "Trenutni filtri", + "Current Type:": "Trenutna vrsta:", + "Current password": "Trenutna lozinka", + "Custom API endpoint if using a different region": "Prilagođena API krajnja točka ako koristite drugu regiju", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Prilagođena HTTP zaglavlja (jedno po retku, format: Header-Name: vrijednost)", + "Custom HTTP headers (one per line, format: header-name: value)": "Prilagođena HTTP zaglavlja (jedno po retku, format: header-name: vrijednost)", + "Custom Ranges:": "Prilagođeni rasponi:", + "Custom display title for this facet": "Prilagođeni prikazni naslov za ovu fasetu", + "Custom pattern:": "Prilagođeni uzorak:", + "DELETE COLLECTION": "IZBRIŠI ZBIRKU", + "DSAR": "DSAR", + "Daily": "Dnevno", + "Dashboard": "Nadzorna ploča", + "Dashboard not found": "Nadzorna ploča nije pronađena", + "Data": "Podaci", + "Data Source": "Izvor podataka", + "Data Sources": "Izvori podataka", + "Data Views": "Prikazi podataka", + "Data sources": "Izvori podataka", + "Data type does not match observed values": "Vrsta podataka ne odgovara promatranim vrijednostima", + "Data-subject access request": "Zahtjev ispitanika za pristup", + "Database URL": "URL baze podataka", + "Database information refreshed": "Informacije o bazi podataka osvježene", + "Date Faceting:": "Fasetiranje datuma:", + "Date Range": "Raspon datuma", + "Dates": "Datumi", + "Deactivation request cancelled": "Zahtjev za deaktivaciju otkazan", + "Deactivation request submitted": "Zahtjev za deaktivaciju podnesen", + "Deck": "Deck", + "Deck integration is not available": "Deck integracija nije dostupna", + "Default": "Zadano", + "Default Object Tenant": "Zadani zakupac objekta", + "Default Organisation": "Zadana organizacija", + "Default Owner": "Zadani vlasnik", + "Default User (for cron/background jobs)": "Zadani korisnik (za cron/pozadinske poslove)", + "Default User Tenant": "Zadani zakupac korisnika", + "Default value": "Zadana vrijednost", + "Default value (Email)": "Zadana vrijednost (e-pošta)", + "Default value (Password)": "Zadana vrijednost (lozinka)", + "Default value (Phone number)": "Zadana vrijednost (telefonski broj)", + "Default value (Regex)": "Zadana vrijednost (regularni izraz)", + "Default: 5": "Zadano: 5", + "Defaults to current user. Select a different user if needed.": "Zadano na trenutnog korisnika. Odaberite drugog korisnika ako je potrebno.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Odgode se udvostručuju sa svakim pokušajem (2, 4, 8 minuta...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Odgode se povećavaju linearno (5, 10, 15 minuta...)", + "Delete": "Izbriši", + "Delete ({count})": "Izbriši ({count})", + "Delete Application": "Izbriši aplikaciju", + "Delete Audit Trail": "Izbriši revizijski trag", + "Delete Collection": "Izbriši zbirku", + "Delete ConfigSet": "Izbriši ConfigSet", + "Delete Objects": "Izbriši objekte", + "Delete Operations": "Izbriši operacije", + "Delete SOLR Collection": "Izbriši SOLR zbirku", + "Delete View": "Izbriši prikaz", + "Delete all objects in this schema": "Izbriši sve objekte u ovoj shemi", + "Delete permanently": "Izbriši trajno", + "Delete this conversation?": "Izbrisati ovaj razgovor?", + "Delete view": "Izbriši prikaz", + "Delete {count} object": "Izbriši {count} objekt", + "Delete {count} objects": "Izbriši {count} objekata", + "Deleted": "Izbrisano", + "Deleted By": "Izbrisao", + "Deleted Date": "Datum brisanja", + "Deleted Items Management": "Upravljanje izbrisanim stavkama", + "Deleted This Week": "Izbrisano ovaj tjedan", + "Deleted Today": "Izbrisano danas", + "Deleted:": "Izbrisano:", + "Deleting SOLR Collection...": "Brisanje SOLR zbirke...", + "Deleting...": "Brisanje...", + "Deletion Date Range": "Raspon datuma brisanja", + "Deletion Statistics": "Statistika brisanja", + "Deprecated": "Zastarjelo", + "Depublish": "Poništi objavu", + "Description": "Opis", + "Description Template": "Predložak opisa", + "Description shown as tooltip": "Opis prikazan kao opis alata", + "Description:": "Opis:", + "Detect data types and patterns": "Otkrij vrste i uzorke podataka", + "Detected At": "Otkriveno u", + "Detected Format:": "Otkriveni format:", + "Detected Issues:": "Otkriveni problemi:", + "Detected: {date}": "Otkriveno: {date}", + "Direct permission": "Izravno dopuštenje", + "Disable": "Onemogući", + "Disable this to make the agent": "Onemogućite ovo kako bi agent", + "Disabled": "Onemogućeno", + "Discover Files": "Otkrij datoteke", + "Discovered Properties": "Otkrivena svojstva", + "Display Format": "Format prikaza", + "Display Name": "Prikazni naziv", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Želite li trajno izbrisati sve filtrirane unose revizijskog traga? Ova se radnja ne može poništiti.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Želite li trajno izbrisati ovaj unos revizijskog traga? Ova se radnja ne može poništiti.", + "Documents": "Dokumenti", + "Doelbinding *": "Vezanost svrhe *", + "Dolphin OCR enabled": "Dolphin OCR omogućen", + "Download API Spec": "Preuzmi API specifikaciju", + "Download API Specification": "Preuzmi API specifikaciju", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Preuzmite kopiju svih svojih osobnih podataka pohranjenih u OpenRegister (GDPR članak 20.).", + "Drag a file or files here": "Povucite datoteku ili datoteke ovdje", + "Dry run completed: {count} fields would be created": "Probno izvođenje dovršeno: {count} polja bilo bi stvoreno", + "Dry run completed: {count} fields would be fixed": "Probno izvođenje dovršeno: {count} polja bilo bi ispravljeno", + "Duration in seconds (optional)": "Trajanje u sekundama (neobavezno)", + "Edit": "Uredi", + "Edit Agent": "Uredi agenta", + "Edit Application": "Uredi aplikaciju", + "Edit Configuration": "Uredi konfiguraciju", + "Edit Endpoint": "Uredi krajnju točku", + "Edit Organisation": "Uredi organizaciju", + "Edit Property '{key}' of '{title}'": "Uredi svojstvo '{key}' od '{title}'", + "Edit Register": "Uredi registar", + "Edit Schema": "Uredi shemu", + "Edit Source": "Uredi izvor", + "Edit View": "Uredi prikaz", + "Edit Webhook": "Uredi webhook", + "Edit labels": "Uredi oznake", + "Edit processing activity": "Uredi aktivnost obrade", + "Edit tags for {name}": "Uredi oznake za {name}", + "Edit verwerkingsactiviteit": "Uredi aktivnost obrade", + "Edit view": "Uredi prikaz", + "Edit view details": "Uredi pojedinosti prikaza", + "Effectiveness": "Učinkovitost", + "Email": "E-pošta", + "Email Addresses": "Adrese e-pošte", + "Email digest frequency": "Učestalost sažetka e-pošte", + "Emails": "E-pošte", + "Embedding Model": "Model ugrađivanja", + "Embedding Provider": "Pružatelj ugrađivanja", + "Embedding generated successfully": "Ugrađivanje uspješno generirano", + "Embedding provider connection successful!": "Veza s pružateljem ugrađivanja uspješna!", + "Embedding test failed: {error}": "Test ugrađivanja neuspješan: {error}", + "Enable": "Omogući", + "Enable RAG": "Omogući RAG", + "Enable auto-creation": "Omogući automatsko stvaranje", + "Enable automatic file vectorization": "Omogući automatsku vektorizaciju datoteka", + "Enable automatic object vectorization": "Omogući automatsku vektorizaciju objekata", + "Enable automatic synchronization": "Omogući automatsku sinkronizaciju", + "Enable calendar provider": "Omogući pružatelja kalendara", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Omogući detaljno bilježenje za SOLR operacije (preporučeno za otklanjanje pogrešaka)", + "Enable faceting": "Omogući fasetiranje", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Omogućite ili onemogućite LLM značajke. Konfigurirajte pružatelje i modele koristeći gumb LLM konfiguracija iznad.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Omogućite ili onemogućite LLM značajke. Konfigurirajte pružatelje i modele koristeći gumb LLM konfiguracija iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Omogućite ili onemogućite integraciju SOLR pretraživanja. Konfigurirajte postavke veze koristeći gumb Postavke veze iznad.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Omogućite ili onemogućite integraciju SOLR pretraživanja. Konfigurirajte postavke veze koristeći gumb postavke veze iznad.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Omogućite ili onemogućite integraciju n8n tijeka rada. Konfigurirajte postavke veze ispod.", + "Enable or disable this webhook": "Omogući ili onemogući ovaj webhook", + "Enable tools that allow the agent to interact with data through function calling.": "Omogućite alate koji agentu omogućuju interakciju s podacima putem pozivanja funkcija.", + "Enable vectorization for all existing and future views (may increase costs)": "Omogući vektorizaciju za sve postojeće i buduće prikaze (može povećati troškove)", + "Enable write-back to target objects": "Omogući upisivanje natrag u ciljne objekte", + "Enabled": "Omogućeno", + "End Date Field": "Polje datuma završetka", + "Endpoint Path*": "Putanja krajnje točke*", + "Endpoints": "Krajnje točke", + "Engine not found": "Mehanizam nije pronađen", + "Enter ConfigSet name": "Unesite naziv ConfigSet stavke", + "Enter Nextcloud usernames to grant access to this private agent": "Unesite Nextcloud korisnička imena za dodjelu pristupa ovom privatnom agentu", + "Enter agent description (optional)": "Unesite opis agenta (neobavezno)", + "Enter agent name": "Unesite naziv agenta", + "Enter application description (optional)": "Unesite opis aplikacije (neobavezno)", + "Enter application name": "Unesite naziv aplikacije", + "Enter collection name": "Unesite naziv zbirke", + "Enter configuration description (optional)": "Unesite opis konfiguracije (neobavezno)", + "Enter configuration title": "Unesite naslov konfiguracije", + "Enter description (optional)...": "Unesite opis (neobavezno)...", + "Enter new collection name": "Unesite novi naziv zbirke", + "Enter object ID": "Unesite ID objekta", + "Enter organisation description (optional)": "Unesite opis organizacije (neobavezno)", + "Enter organisation name": "Unesite naziv organizacije", + "Enter search term": "Unesite pojam za pretraživanje", + "Enter search terms or leave empty to browse all": "Unesite pojmove za pretraživanje ili ostavite prazno za pregledavanje svega", + "Enter system prompt for the agent": "Unesite sistemski upit za agenta", + "Enter username and press Enter": "Unesite korisničko ime i pritisnite Enter", + "Enter view name...": "Unesite naziv prikaza...", + "Enter webhook name": "Unesite naziv webhooka", + "Enter your API key": "Unesite svoj API ključ", + "Enter your object here...": "Unesite svoj objekt ovdje...", + "Enter your schema here...": "Unesite svoju shemu ovdje...", + "Entities": "Entiteti", + "Entities detected": "Otkriveni entiteti", + "Entity ID: {id}": "ID entiteta: {id}", + "Entity Information": "Informacije o entitetu", + "Entity Recognition": "Prepoznavanje entiteta", + "Entity Recognition Method": "Metoda prepoznavanja entiteta", + "Entity deleted successfully": "Entitet je uspješno izbrisan", + "Entity not found": "Entitet nije pronađen", + "Entries to be deleted:": "Unosi koji će biti izbrisani:", + "Enum Issue": "Problem s enumeracijom", + "Enum constraint is missing": "Nedostaje ograničenje enumeracije", + "Erase (Art 17)": "Brisanje (čl. 17.)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Izbrisati {count} objekt(a) za ovaj subjekt? Ova radnja bilježi se u revizijskom tragu.", + "Erasure complete": "Brisanje dovršeno", + "Erasure preview": "Pregled brisanja", + "Error": "Pogreška", + "Error Details": "Pojedinosti o pogrešci", + "Error Information": "Informacije o pogrešci", + "Error Message": "Poruka o pogrešci", + "Error deleting audit trails: {error}": "Pogreška pri brisanju revizijskih tragova: {error}", + "Error deleting search trails: {error}": "Pogreška pri brisanju tragova pretraživanja: {error}", + "Error loading application": "Pogreška pri učitavanju aplikacije", + "Error loading audit trails": "Pogreška pri učitavanju revizijskih tragova", + "Error loading entity": "Pogreška pri učitavanju entiteta", + "Error loading search trails": "Pogreška pri učitavanju tragova pretraživanja", + "Estimated Batches:": "Procijenjeni paketi:", + "Estimated Cost:": "Procijenjeni trošak:", + "Estimated Duration:": "Procijenjeno trajanje:", + "Event": "Događaj", + "Event Property for Payload": "Svojstvo događaja za korisni teret", + "Events": "Događaji", + "Every schema with detected PII has a processing-activity annotation.": "Svaka shema s otkrivenim PII ima napomenu o aktivnosti obrade.", + "Example": "Primjer", + "Example Value": "Primjer vrijednosti", + "Example value for this property": "Primjer vrijednosti za ovo svojstvo", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Isključivi maksimum", + "Exclusive Minimum": "Isključivi minimum", + "Exclusive maximum": "Isključivi maksimum", + "Exclusive minimum": "Isključivi minimum", + "Execution Mode": "Način izvođenja", + "Execution Time": "Vrijeme izvođenja", + "Execution Time Range": "Raspon vremena izvođenja", + "Existing Improvements": "Postojeća poboljšanja", + "Expand details": "Proširi pojedinosti", + "Expiration": "Istek", + "Expires": "Istječe", + "Expires in (e.g., 90d)": "Istječe za (npr. 90d)", + "Exponential": "Eksponencijalno", + "Export": "Izvoz", + "Export \"{schema}\" objects from \"{register}\"": "Izvezi objekte \"{schema}\" iz \"{register}\"", + "Export Configuration": "Konfiguracija izvoza", + "Export Format:": "Format izvoza:", + "Export Objects": "Izvoz objekata", + "Export completed successfully": "Izvoz je uspješno dovršen", + "Export downloaded successfully": "Izvoz je uspješno preuzet", + "Export failed: {error}": "Izvoz nije uspio: {error}", + "Export is rate limited. Please try again later.": "Izvoz je ograničen po stopi. Pokušajte ponovno kasnije.", + "Export my data": "Izvezi moje podatke", + "Export, view, or delete audit trails": "Izvezite, pregledajte ili izbrišite revizijske tragove", + "Exporting...": "Izvoz...", + "Extend Schema": "Proširi shemu", + "Extended": "Prošireno", + "Extensions": "Proširenja", + "Extra Query Parameters": "Dodatni parametri upita", + "Extract Now": "Izdvoji sada", + "Extract Pending Files": "Izdvoji datoteke na čekanju", + "Extract now": "Izdvoji sada", + "Extract properties from each object": "Izdvoji svojstva iz svakog objekta", + "Extracted At": "Izdvojeno", + "Extracted at": "Izdvojeno", + "Extraction": "Izdvajanje", + "Extraction Mode": "Način izdvajanja", + "Extraction Scope": "Opseg izdvajanja", + "Extraction Status": "Status izdvajanja", + "Extraction error for {file}: {error}": "Pogreška izdvajanja za {file}: {error}", + "Facet Description": "Opis fasete", + "Facet Order": "Redoslijed fasete", + "Facet Title": "Naslov fasete", + "Facet Type": "Vrsta fasete", + "Facet configuration updated successfully": "Konfiguracija fasete uspješno ažurirana", + "Facetable": "Fasetabilno", + "Faceting Configuration:": "Konfiguracija faseta:", + "Facets discovered and configured successfully": "Fasete su uspješno otkrivene i konfigurirane", + "Facets discovered successfully": "Fasete su uspješno otkrivene", + "Failed": "Neuspjelo", + "Failed to add attachment to object": "Dodavanje privitka objektu nije uspjelo", + "Failed to add user to organisation": "Dodavanje korisnika u organizaciju nije uspjelo", + "Failed to analyze schema properties": "Analiza svojstava sheme nije uspjela", + "Failed to archive conversation": "Arhiviranje razgovora nije uspjelo", + "Failed to calculate sizes": "Izračun veličina nije uspio", + "Failed to cancel deactivation": "Otkazivanje deaktivacije nije uspjelo", + "Failed to change active organisation: {error}": "Promjena aktivne organizacije nije uspjela: {error}", + "Failed to change password": "Promjena lozinke nije uspjela", + "Failed to check version: {error}": "Provjera verzije nije uspjela: {error}", + "Failed to clear audit trails: {error}": "Brisanje revizijskih tragova nije uspjelo: {error}", + "Failed to clear blob objects: {error}": "Brisanje blob objekata nije uspjelo: {error}", + "Failed to clear cache: {error}": "Brisanje predmemorije nije uspjelo: {error}", + "Failed to clear collection": "Brisanje kolekcije nije uspjelo", + "Failed to clear conversation": "Brisanje razgovora nije uspjelo", + "Failed to clear embeddings: {error}": "Brisanje ugradbi nije uspjelo: {error}", + "Failed to clear search trails: {error}": "Brisanje tragova pretraživanja nije uspjelo: {error}", + "Failed to clear {type} cache: {error}": "Brisanje predmemorije {type} nije uspjelo: {error}", + "Failed to copy changes": "Kopiranje promjena nije uspjelo", + "Failed to copy data": "Kopiranje podataka nije uspjelo", + "Failed to copy data to clipboard": "Kopiranje podataka u međuspremnik nije uspjelo", + "Failed to copy to clipboard": "Kopiranje u međuspremnik nije uspjelo", + "Failed to copy token": "Kopiranje tokena nije uspjelo", + "Failed to create conversation": "Stvaranje razgovora nije uspjelo", + "Failed to create missing SOLR fields: {error}": "Stvaranje nedostajućih SOLR polja nije uspjelo: {error}", + "Failed to create or find project": "Stvaranje ili pronalaženje projekta nije uspjelo", + "Failed to create token": "Stvaranje tokena nije uspjelo", + "Failed to delete SOLR collection: {error}": "Brisanje SOLR kolekcije nije uspjelo: {error}", + "Failed to delete collection": "Brisanje kolekcije nije uspjelo", + "Failed to delete conversation": "Brisanje razgovora nije uspjelo", + "Failed to delete objects for {schema}: {error}": "Brisanje objekata za {schema} nije uspjelo: {error}", + "Failed to delete webhook": "Brisanje webhooka nije uspjelo", + "Failed to depublish register: {error}": "Depubliciranje registra nije uspjelo: {error}", + "Failed to depublish schema: {error}": "Depubliciranje sheme nije uspjelo: {error}", + "Failed to depublish: {error}": "Depubliciranje nije uspjelo: {error}", + "Failed to discover files: {error}": "Otkrivanje datoteka nije uspjelo: {error}", + "Failed to download API specification": "Preuzimanje API specifikacije nije uspjelo", + "Failed to export data": "Izvoz podataka nije uspio", + "Failed to export objects": "Izvoz objekata nije uspio", + "Failed to fetch conversation history": "Dohvaćanje povijesti razgovora nije uspjelo", + "Failed to fix mismatched SOLR fields: {error}": "Ispravljanje neusklađenih SOLR polja nije uspjelo: {error}", + "Failed to get SOLR field configuration": "Dohvaćanje konfiguracije SOLR polja nije uspjelo", + "Failed to get chat statistics": "Dohvaćanje statistike razgovora nije uspjelo", + "Failed to get database information: %s": "Dohvaćanje informacija o bazi podataka nije uspjelo: %s", + "Failed to get entity categories": "Dohvaćanje kategorija entiteta nije uspjelo", + "Failed to get entity statistics": "Dohvaćanje statistike entiteta nije uspjelo", + "Failed to get entity types": "Dohvaćanje vrsta entiteta nije uspjelo", + "Failed to get response: {error}": "Dohvaćanje odgovora nije uspjelo: {error}", + "Failed to invalidate app store cache: {error}": "Poništavanje predmemorije trgovine aplikacija nije uspjelo: {error}", + "Failed to leave organisation: {error}": "Napuštanje organizacije nije uspjelo: {error}", + "Failed to link object": "Povezivanje objekta nije uspjelo", + "Failed to load Deck cards": "Učitavanje Deck kartica nije uspjelo", + "Failed to load LLM configuration": "Učitavanje LLM konfiguracije nije uspjelo", + "Failed to load Nextcloud groups": "Učitavanje Nextcloud grupa nije uspjelo", + "Failed to load cache statistics: {error}": "Učitavanje statistike predmemorije nije uspjelo: {error}", + "Failed to load conversation": "Učitavanje razgovora nije uspjelo", + "Failed to load entities": "Učitavanje entiteta nije uspjelo", + "Failed to load entity": "Učitavanje entiteta nije uspjelo", + "Failed to load extraction data": "Učitavanje podataka o izdvajanju nije uspjelo", + "Failed to load files": "Učitavanje datoteka nije uspjelo", + "Failed to load linked contacts": "Učitavanje povezanih kontakata nije uspjelo", + "Failed to load linked emails": "Učitavanje povezanih e-pošta nije uspjelo", + "Failed to load linked events": "Učitavanje povezanih događaja nije uspjelo", + "Failed to load organisations": "Učitavanje organizacija nije uspjelo", + "Failed to load register data": "Učitavanje podataka registra nije uspjelo", + "Failed to load related entities": "Učitavanje povezanih entiteta nije uspjelo", + "Failed to load settings: {error}": "Učitavanje postavki nije uspjelo: {error}", + "Failed to load statistics: {error}": "Učitavanje statistike nije uspjelo: {error}", + "Failed to load templates": "Učitavanje predložaka nije uspjelo", + "Failed to load view: {error}": "Učitavanje prikaza nije uspjelo: {error}", + "Failed to load webhooks": "Učitavanje webhookova nije uspjelo", + "Failed to load workflows": "Učitavanje tijekova rada nije uspjelo", + "Failed to process message": "Obrada poruke nije uspjela", + "Failed to publish register: {error}": "Objavljivanje registra nije uspjelo: {error}", + "Failed to publish schema: {error}": "Objavljivanje sheme nije uspjelo: {error}", + "Failed to publish: {error}": "Objavljivanje nije uspjelo: {error}", + "Failed to refresh database information": "Osvježavanje informacija o bazi podataka nije uspjelo", + "Failed to reindex collection": "Ponovno indeksiranje kolekcije nije uspjelo", + "Failed to remove avatar": "Uklanjanje avatara nije uspjelo", + "Failed to remove link": "Uklanjanje veze nije uspjelo", + "Failed to remove member: {error}": "Uklanjanje člana nije uspjelo: {error}", + "Failed to remove schema {schema}: {error}": "Uklanjanje sheme {schema} nije uspjelo: {error}", + "Failed to remove user from organisation": "Uklanjanje korisnika iz organizacije nije uspjelo", + "Failed to rename conversation": "Preimenovanje razgovora nije uspjelo", + "Failed to request deactivation": "Zahtjev za deaktivaciju nije uspio", + "Failed to restore conversation": "Vraćanje razgovora nije uspjelo", + "Failed to retrieve extraction status.": "Dohvaćanje statusa izdvajanja nije uspjelo.", + "Failed to retrieve objects for file.": "Dohvaćanje objekata za datoteku nije uspjelo.", + "Failed to retry extraction": "Ponovni pokušaj izdvajanja nije uspio", + "Failed to retry webhook": "Ponovni pokušaj webhooka nije uspio", + "Failed to revoke token": "Opoziv tokena nije uspio", + "Failed to save GitHub token": "Spremanje GitHub tokena nije uspjelo", + "Failed to save GitLab URL": "Spremanje GitLab URL-a nije uspjelo", + "Failed to save GitLab token": "Spremanje GitLab tokena nije uspjelo", + "Failed to save LLM settings: {error}": "Spremanje LLM postavki nije uspjelo: {error}", + "Failed to save additional feedback": "Spremanje dodatnih povratnih informacija nije uspjelo", + "Failed to save configuration.": "Spremanje konfiguracije nije uspjelo.", + "Failed to save configuration: {error}": "Spremanje konfiguracije nije uspjelo: {error}", + "Failed to save facet configuration": "Spremanje konfiguracije fasete nije uspjelo", + "Failed to save feedback": "Spremanje povratnih informacija nije uspjelo", + "Failed to save file settings: {error}": "Spremanje postavki datoteke nije uspjelo: {error}", + "Failed to save n8n configuration": "Spremanje n8n konfiguracije nije uspjelo", + "Failed to save preferences": "Spremanje preferencija nije uspjelo", + "Failed to save roles": "Spremanje uloga nije uspjelo", + "Failed to save settings": "Spremanje postavki nije uspjelo", + "Failed to save settings: {error}": "Spremanje postavki nije uspjelo: {error}", + "Failed to save view: {error}": "Spremanje prikaza nije uspjelo: {error}", + "Failed to save warmup interval: {error}": "Spremanje intervala zagrijavanja nije uspjelo: {error}", + "Failed to save webhook": "Spremanje webhooka nije uspjelo", + "Failed to send feedback": "Slanje povratnih informacija nije uspjelo", + "Failed to set active organisation: {error}": "Postavljanje aktivne organizacije nije uspjelo: {error}", + "Failed to setup SOLR: {error}": "Postavljanje SOLR-a nije uspjelo: {error}", + "Failed to start file warmup: {error}": "Pokretanje zagrijavanja datoteka nije uspjelo: {error}", + "Failed to start processing {type} files: {error}": "Pokretanje obrade {type} datoteka nije uspjelo: {error}", + "Failed to start vectorization: {error}": "Pokretanje vektorizacije nije uspjelo: {error}", + "Failed to switch organisation: {error}": "Prebacivanje organizacije nije uspjelo: {error}", + "Failed to sync magic table for {schema}: {error}": "Sinkronizacija čarobne tablice za {schema} nije uspjela: {error}", + "Failed to test SOLR connection: {error}": "Testiranje SOLR veze nije uspjelo: {error}", + "Failed to test webhook": "Testiranje webhooka nije uspjelo", + "Failed to update LLM settings: {error}": "Ažuriranje LLM postavki nije uspjelo: {error}", + "Failed to update Multitenancy settings: {error}": "Ažuriranje postavki višestruke zakupništva nije uspjelo: {error}", + "Failed to update RBAC settings: {error}": "Ažuriranje RBAC postavki nije uspjelo: {error}", + "Failed to update Retention settings: {error}": "Ažuriranje postavki čuvanja nije uspjelo: {error}", + "Failed to update SOLR settings: {error}": "Ažuriranje SOLR postavki nije uspjelo: {error}", + "Failed to update favorite status": "Ažuriranje statusa favorita nije uspjelo", + "Failed to update schema configuration for {schema}: {error}": "Ažuriranje konfiguracije sheme za {schema} nije uspjelo: {error}", + "Failed to update schema properties": "Ažuriranje svojstava sheme nije uspjelo", + "Failed to update view": "Ažuriranje prikaza nije uspjelo", + "Failed to update view: {error}": "Ažuriranje prikaza nije uspjelo: {error}", + "Failed to update webhook": "Ažuriranje webhooka nije uspjelo", + "Failed to upload avatar": "Učitavanje avatara nije uspjelo", + "Failed to validate {schema}: {error}": "Provjera valjanosti {schema} nije uspjela: {error}", + "Failed to vectorize files: {error}": "Vektorizacija datoteka nije uspjela: {error}", + "Failed to warmup names cache: {error}": "Zagrijavanje predmemorije naziva nije uspjelo: {error}", + "Federation": "Federacija", + "Feedback recorded": "Povratne informacije zabilježene", + "Fewer sources (1-3):": "Manje izvora (1-3):", + "Field": "Polje", + "Fields to Display": "Polja za prikaz", + "File": "Datoteka", + "File #{id}": "Datoteka #{id}", + "File Chunk Prediction": "Predviđanje fragmenta datoteke", + "File Collection": "Kolekcija datoteka", + "File Handling": "Rukovanje datotekama", + "File Location": "Lokacija datoteke", + "File Management": "Upravljanje datotekama", + "File Name": "Naziv datoteke", + "File Path": "Putanja datoteke", + "File Type Selection": "Odabir vrste datoteke", + "File Types to Process": "Vrste datoteka za obradu", + "File Vectorization": "Vektorizacija datoteka", + "File Warmup": "Zagrijavanje datoteka", + "File actions menu": "Izbornik radnji datoteke", + "File anonymized successfully": "Datoteka je uspješno anonimizirana", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vektorizacija fragmenata datoteke još nije implementirana. Fragmenti su spremni i pohranjeni, ali je usluga vektorizacije u razvoju.", + "File collection not configured": "Kolekcija datoteka nije konfigurirana", + "File discovery completed": "Otkrivanje datoteka dovršeno", + "File extraction completed": "Izdvajanje datoteke dovršeno", + "File extraction queued": "Izdvajanje datoteke stavljeno u red čekanja", + "File indexed successfully": "Datoteka je uspješno indeksirana", + "File is already anonymized": "Datoteka je već anonimizirana", + "File name": "Naziv datoteke", + "File not found": "Datoteka nije pronađena", + "File settings saved successfully": "Postavke datoteke uspješno spremljene", + "File settings updated successfully": "Postavke datoteke uspješno ažurirane", + "File sources": "Izvori datoteka", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Vektorizacija datoteka dovršena! {vectorized} fragmenata vektorizirano iz {files} datoteka. {failed} neuspjelo.", + "File vectorization configuration saved successfully": "Konfiguracija vektorizacije datoteka uspješno spremljena", + "File vectorization started. Check the statistics section for progress.": "Vektorizacija datoteka pokrenuta. Provjerite odjeljak statistike za napredak.", + "File warmup completed": "Zagrijavanje datoteka dovršeno", + "Files": "Datoteke", + "Files added successfully": "Datoteke uspješno dodane", + "Files with Completed Extraction:": "Datoteke s dovršenim izdvajanjem:", + "Files → fileCollection, Objects → objectCollection": "Datoteke → fileCollection, Objekti → objectCollection", + "Files → filecollection, objects → objectcollection": "Datoteke → filecollection, objekti → objectcollection", + "Filter Audit Trails": "Filtriraj revizijske tragove", + "Filter Deleted Items": "Filtriraj izbrisane stavke", + "Filter Objects": "Filtriraj objekte", + "Filter Properties": "Filtriraj svojstva", + "Filter Search Trails": "Filtriraj tragove pretraživanja", + "Filter Statistics": "Filtriraj statistiku", + "Filter and analyze search trail entries": "Filtrirajte i analizirajte unose tragova pretraživanja", + "Filter and manage audit trail entries": "Filtrirajte i upravljajte unosima revizijskog traga", + "Filter and manage soft deleted items": "Filtrirajte i upravljajte meko izbrisanim stavkama", + "Filter and search entities": "Filtrirajte i pretražujte entitete", + "Filter by object ID": "Filtriraj po ID-u objekta", + "Filter by search term": "Filtriraj po pojmu za pretraživanje", + "Filter by type": "Filtriraj po vrsti", + "Filter by webhook": "Filtriraj po webhooku", + "Filter data loaded automatically. Use the filters below to refine your search.": "Podaci filtera učitani automatski. Upotrijebite filtere u nastavku za pročišćavanje pretraživanja.", + "Filter fields...": "Filtriraj polja...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrirajte okidače webhooka po svojstvima korisnog tereta (jedno po retku, format: key: value)", + "Filtered": "Filtrirano", + "Filters": "Filteri", + "Finalizing setup...": "Dovršavanje postavljanja...", + "Fireworks AI Chat Settings": "Postavke razgovora Fireworks AI", + "Fireworks AI Embedding Configuration": "Konfiguracija ugradbi Fireworks AI", + "First": "Prvo", + "Fixed": "Fiksno", + "For chat and retrieval-augmented generation": "Za razgovor i generiranje uz potporu dohvaćanja", + "For vector embeddings and semantic search": "Za vektorske ugradbe i semantičko pretraživanje", + "Format": "Format", + "Format Issue": "Problem s formatom", + "Format constraint is missing": "Nedostaje ograničenje formata", + "From (e.g. -7 days or 2025-01-01)": "Od (npr. -7 dana ili 2025-01-01)", + "From Date": "Od datuma", + "From date": "Od datuma", + "Full data copied to clipboard": "Potpuni podaci kopirani u međuspremnik", + "Function Tools": "Funkcijski alati", + "General Issue": "Općeniti problem", + "Generate recommendations and confidence scores": "Generiraj preporuke i ocjene pouzdanosti", + "Generate report": "Generiraj izvješće", + "Generate the verantwoordingsdocument": "Generiraj dokument o odgovornosti", + "Generate vectors immediately when new objects are created": "Generiraj vektore odmah kada se stvore novi objekti", + "Generated: {time}": "Generirano: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Dobijte uvide u stanje podataka vaše organizacije u stvarnom vremenu fokusiranjem na registre, definicije shema te pohranu i upotrebu objekata.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Dobijte uvide u stanje podataka vaše organizacije, trendove rasta i metrike kvalitete u stvarnom vremenu.", + "GitHub Personal Access Token": "GitHub osobni pristupni token", + "GitHub token is valid": "GitHub token je valjan", + "GitHub token is valid! Username: {username}": "GitHub token je valjan! Korisničko ime: {username}", + "GitHub token removed successfully": "GitHub token uspješno uklonjen", + "GitHub token saved successfully": "GitHub token uspješno spremljen", + "GitHub token test failed: {message}": "Testiranje GitHub tokena nije uspjelo: {message}", + "GitLab Personal Access Token": "GitLab osobni pristupni token", + "GitLab URL saved successfully": "GitLab URL uspješno spremljen", + "GitLab token is valid": "GitLab token je valjan", + "GitLab token is valid! Username: {username}": "GitLab token je valjan! Korisničko ime: {username}", + "GitLab token saved successfully": "GitLab token uspješno spremljen", + "GitLab token test failed: {message}": "Testiranje GitLab tokena nije uspjelo: {message}", + "Go to linked Register": "Idi na povezani registar", + "Go to linked Schema": "Idi na povezanu shemu", + "Go to organisation": "Idi na organizaciju", + "Grant additional permissions beyond standard CRUD operations": "Dodijeli dodatna dopuštenja izvan standardnih CRUD operacija", + "Group Quota": "Kvota grupe", + "Groups": "Grupe", + "HTML preview": "HTML pretpregled", + "HTTP Method": "HTTP metoda", + "HTTP method used to send webhook requests": "HTTP metoda koja se koristi za slanje zahtjeva webhooka", + "Headers": "Zaglavlja", + "Health": "Stanje", + "Heartbeat successful - connection kept alive": "Otkucaj uspješan - veza održana aktivnom", + "Helpful": "Korisno", + "Here you can configure the details for various connections.": "Ovdje možete konfigurirati pojedinosti za razne veze.", + "Here you can configure the details for your organisation.": "Ovdje možete konfigurirati pojedinosti za svoju organizaciju.", + "Hide": "Sakrij", + "Hide Filters": "Sakrij filtere", + "Hide in collection view": "Sakrij u prikazu kolekcije", + "Hide in forms": "Sakrij u obrascima", + "Hide in list view": "Sakrij u prikazu popisa", + "High": "Visoko", + "High Confidence": "Visoka pouzdanost", + "Host": "Poslužitelj", + "Hourly": "Svaki sat", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Koliko duboko prolaziti kroz ugniježđena svojstva objekta (1-20). Više vrijednosti obuhvaćaju više pojedinosti, ali povećavaju veličinu vektora.", + "How often to check for updates (1-168 hours)": "Koliko često provjeravati ažuriranja (1-168 sati)", + "How to handle retries for failed webhook deliveries": "Kako rukovati ponovnim pokušajima za neuspjele isporuke webhooka", + "IBANs": "IBAN-ovi", + "ID": "ID", + "ID of input mapping (optional)": "ID ulaznog mapiranja (neobavezno)", + "ID of output mapping (optional)": "ID izlaznog mapiranja (neobavezno)", + "ID of the target resource": "ID ciljnog resursa", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ograničenja IP stope uspješno izbrisana", + "Identify properties not in the schema": "Identificiraj svojstva koja nisu u shemi", + "Immutable": "Nepromjenjivo", + "Import": "Uvoz", + "Import Configuration": "Konfiguracija uvoza", + "Import Data into Register": "Uvezi podatke u registar", + "Import successful": "Uvoz uspješan", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Uvezite predložak report-bundle.json da biste dobili registar `reports`, zatim stvorite svoju prvu nadzornu ploču putem standardnog korisničkog sučelja za objekte. Nadzorne ploče deklariraju svoje widgete u JSON-u, a prikazivač svakom widgetu dostavlja podatke agregacije uživo.", + "Improved Property": "Poboljšano svojstvo", + "In use": "U upotrebi", + "Inactive": "Neaktivno", + "Inactive organisations cannot be used": "Neaktivne organizacije ne mogu se koristiti", + "Include IDs and names of related objects for better contextual search": "Uključi ID-ove i nazive povezanih objekata za bolje kontekstualno pretraživanje", + "Include ids and names of related objects for better contextual search": "Uključi id-ove i nazive povezanih objekata za bolje kontekstualno pretraživanje", + "Include related object references": "Uključi reference povezanih objekata", + "Include schema and register metadata": "Uključi metapodatke sheme i registra", + "Index": "Indeks", + "Information about the current OpenRegister installation": "Informacije o trenutnoj OpenRegister instalaciji", + "Inherited from register": "Naslijeđeno od registra", + "Inherits permissions from register": "Nasljeđuje dopuštenja od registra", + "Initialization failed": "Inicijalizacija nije uspjela", + "Initialize Project": "Inicijaliziraj projekt", + "Initializing SOLR setup...": "Inicijalizacija SOLR postavljanja...", + "Input Mapping": "Ulazno mapiranje", + "Inspect Fields": "Pregledaj polja", + "Inspect Index": "Pregledaj indeks", + "Inspect SOLR Index": "Pregledaj SOLR indeks", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Instalirajte aplikaciju notify_push iz trgovine aplikacija Nextcloud kako biste omogućili ažuriranja u stvarnom vremenu.", + "Integrations": "Integracije", + "Internet location (URL) of this installation": "Internetska lokacija (URL) ove instalacije", + "Interval": "Interval", + "Invalid": "Nevaljano", + "Invalid batch size. Must be between 1 and 5000": "Nevaljana veličina paketa. Mora biti između 1 i 5000", + "Invalid feedback type": "Nevaljana vrsta povratne informacije", + "Invalid field name provided": "Naveden je nevaljan naziv polja", + "Invalid maxObjects. Must be 0 (all) or positive number": "Nevaljan maxObjects. Mora biti 0 (sve) ili pozitivan broj", + "Invalid:": "Nevaljano:", + "Invite Users": "Pozovi korisnike", + "Invited Users": "Pozvani korisnici", + "Inzage (Art 15)": "Uvid (čl. 15.)", + "Inzage results": "Rezultati uvida", + "Issue": "Problem", + "Items per page:": "Stavki po stranici:", + "Just now": "Upravo sada", + "Key": "Ključ", + "LLM Configuration": "LLM konfiguracija", + "LLM actions menu": "Izbornik LLM radnji", + "LLM configuration saved successfully": "LLM konfiguracija uspješno spremljena", + "LLM features disabled": "LLM značajke onemogućene", + "LLM features enabled": "LLM značajke omogućene", + "LLM must be enabled with an embedding provider configured": "LLM mora biti omogućen uz konfiguriranog pružatelja ugradbi", + "LLM settings saved successfully": "LLM postavke uspješno spremljene", + "LLM settings updated successfully": "LLM postavke uspješno ažurirane", + "Label": "Oznaka", + "Labels": "Oznake", + "Languages": "Jezici", + "Last": "Zadnje", + "Last Triggered": "Zadnje aktivirano", + "Leave empty to allow all users access": "Ostavite prazno da biste dopustili pristup svim korisnicima", + "Leave off for auto-detection from property format.": "Ostavite isključeno za automatsko otkrivanje iz formata svojstva.", + "Left organisation successfully": "Uspješno napuštena organizacija", + "Legal basis *": "Pravna osnova *", + "Length Range:": "Raspon duljine:", + "Linear": "Linearno", + "Link": "Veza", + "Link an email from the Mail app sidebar to associate it with this object.": "Povežite e-poštu iz bočne trake aplikacije Mail kako biste je pridružili ovom objektu.", + "Link an object to see it here.": "Povežite objekt da biste ga ovdje vidjeli.", + "Link another object": "Poveži drugi objekt", + "Link existing event": "Poveži postojeći događaj", + "Link removed": "Veza uklonjena", + "Link to Object": "Veza na objekt", + "Link to {name}": "Veza na {name}", + "Linked Objects": "Povezani objekti", + "Linked by {user}": "Povezao {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Povezane e-pošte, kalendarski događaji, kontakti i Deck kartice pojavit će se ovdje.", + "Linked to {name}": "Povezano s {name}", + "Load Advanced Filters": "Učitaj napredne filtere", + "Load advanced filters with live data from your search index": "Učitaj napredne filtere s podacima uživo iz vašeg indeksa pretraživanja", + "Load more": "Učitaj više", + "Load view": "Učitaj prikaz", + "Loading API tokens...": "Učitavanje API tokena...", + "Loading ConfigSets...": "Učitavanje ConfigSetova...", + "Loading LLM configuration...": "Učitavanje LLM konfiguracije...", + "Loading RBAC settings...": "Učitavanje RBAC postavki...", + "Loading activity...": "Učitavanje aktivnosti...", + "Loading advanced filters...": "Učitavanje naprednih filtera...", + "Loading agents...": "Učitavanje agenata...", + "Loading application details...": "Učitavanje pojedinosti aplikacije...", + "Loading applications...": "Učitavanje aplikacija...", + "Loading archived conversations...": "Učitavanje arhiviranih razgovora...", + "Loading audit trails...": "Učitavanje revizijskih tragova...", + "Loading available tools...": "Učitavanje dostupnih alata...", + "Loading cache statistics...": "Učitavanje statistike predmemorije...", + "Loading collections...": "Učitavanje kolekcija...", + "Loading configurations...": "Učitavanje konfiguracija...", + "Loading conversation...": "Učitavanje razgovora...", + "Loading conversations...": "Učitavanje razgovora...", + "Loading deleted items...": "Učitavanje izbrisanih stavki...", + "Loading endpoints": "Učitavanje krajnjih točaka", + "Loading entities...": "Učitavanje entiteta...", + "Loading events...": "Učitavanje događaja...", + "Loading filter data...": "Učitavanje podataka filtera...", + "Loading groups...": "Učitavanje grupa...", + "Loading linked objects...": "Učitavanje povezanih objekata...", + "Loading log details...": "Učitavanje pojedinosti zapisnika...", + "Loading multitenancy settings...": "Učitavanje postavki višestruke zakupništva...", + "Loading n8n configuration...": "Učitavanje n8n konfiguracije...", + "Loading permission matrix...": "Učitavanje matrice dopuštenja...", + "Loading preferences...": "Učitavanje preferencija...", + "Loading register data...": "Učitavanje podataka registra...", + "Loading registers...": "Učitavanje registara...", + "Loading retention settings...": "Učitavanje postavki čuvanja...", + "Loading schemas...": "Učitavanje shema...", + "Loading search configuration...": "Učitavanje konfiguracije pretraživanja...", + "Loading search trails...": "Učitavanje tragova pretraživanja...", + "Loading sources...": "Učitavanje izvora...", + "Loading statistics...": "Učitavanje statistike...", + "Loading text extraction settings...": "Učitavanje postavki izdvajanja teksta...", + "Loading tokens...": "Učitavanje tokena...", + "Loading user groups...": "Učitavanje korisničkih grupa...", + "Loading users...": "Učitavanje korisnika...", + "Loading views...": "Učitavanje prikaza...", + "Loading...": "Učitavanje...", + "Local": "Lokalno", + "Local Version": "Lokalna verzija", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Pronađite svaki objekt koji referencira ispitanika (čl. 15. uvid), pregledajte brisanje (čl. 17. pravo na zaborav) ili izvezite njihove podatke (čl. 20. prenosivost).", + "Location": "Lokacija", + "Location Field": "Polje lokacije", + "Locations": "Lokacije", + "Locked": "Zaključano", + "Locked:": "Zaključano:", + "Login successful": "Prijava uspješna", + "Logs": "Zapisnici", + "Low": "Nisko", + "Low Confidence": "Niska pouzdanost", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Niži brojevi pojavljuju se prvi u bočnoj traci filtera. Ostavite prazno za automatski redoslijed.", + "Lucene Version:": "Lucene verzija:", + "Magic table sync completed for {schema}": "Sinkronizacija čarobne tablice dovršena za {schema}", + "Magic table synchronized successfully": "Čarobna tablica uspješno sinkronizirana", + "Mail Integration": "Integracija e-pošte", + "Mail integration is not available": "Integracija e-pošte nije dostupna", + "Make this installation discoverable within the federation network.": "Učinite ovu instalaciju pronalažljivom unutar mreže federacije.", + "Manage Organisation Roles": "Upravljaj ulogama organizacije", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Upravljajte SOLR kolekcijama (spremišta podataka) i dodijelite ih za objekte i datoteke.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Upravljajte SOLR ConfigSetovima (predlošci konfiguracije) za svoje kolekcije.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Upravljajte SOLR kolekcijama (spremišta podataka) i dodijelite ih za objekte i datoteke.", + "Manage SOLR configsets (configuration templates) for your collections.": "Upravljajte SOLR configsetovima (predlošci konfiguracije) za svoje kolekcije.", + "Manage and configure agents for automated tasks": "Upravljajte i konfigurirajte agente za automatizirane zadatke", + "Manage and monitor file text extraction status": "Upravljajte i nadzirite status izdvajanja teksta iz datoteka", + "Manage and restore soft deleted items from your registers": "Upravljajte i vraćajte meko izbrisane stavke iz svojih registara", + "Manage and view detected entities from files and objects": "Upravljajte i pregledajte otkrivene entitete iz datoteka i objekata", + "Manage document templates and themes": "Upravljajte predlošcima i temama dokumenata", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Upravljajte n8n tijekovima rada za OpenRegister automatizaciju. Tijekovi rada bit će pohranjeni u konfiguriranom projektu.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Upravljajte aktivnostima obrade, izvršavajte zahtjeve za pristup ispitanika i revidirajte usklađenost prema EU GDPR / nizozemskom AVG.", + "Manage webhooks for event-driven integrations": "Upravljajte webhookovima za integracije vođene događajima", + "Manage your account settings, security, and personal data.": "Upravljajte postavkama računa, sigurnošću i osobnim podacima.", + "Manage your applications and modules": "Upravljajte svojim aplikacijama i modulima", + "Manage your chat conversations": "Upravljajte svojim razgovorima", + "Manage your data registers and their configurations": "Upravljajte svojim registrima podataka i njihovim konfiguracijama", + "Manage your data schemas and their properties": "Upravljajte svojim shemama podataka i njihovim svojstvima", + "Manage your data sources and their configurations": "Upravljajte svojim izvorima podataka i njihovim konfiguracijama", + "Manage your organisations and switch between them": "Upravljajte svojim organizacijama i prebacujte se između njih", + "Manage your saved search configurations": "Upravljajte svojim spremljenim konfiguracijama pretraživanja", + "Manage your system configurations and settings": "Upravljajte svojim konfiguracijama i postavkama sustava", + "Managed": "Upravljano", + "Mappings": "Mapiranja", + "Mass validation completed successfully": "Masovna provjera valjanosti uspješno dovršena", + "Mass validation failed: {error}": "Masovna provjera valjanosti nije uspjela: {error}", + "Max Files (0 = all)": "Maks. datoteka (0 = sve)", + "Max Length": "Maks. duljina", + "Max Length:": "Maks. duljina:", + "Max Objects (0 = all)": "Maks. objekata (0 = sve)", + "Max Retries": "Maks. ponovnih pokušaja", + "Max Shards/Node": "Maks. shardova/čvor", + "Max Tokens": "Maks. tokena", + "Max execution time (ms)": "Maks. vrijeme izvođenja (ms)", + "Max ms": "Maks. ms", + "Max result count": "Maks. broj rezultata", + "Max results": "Maks. rezultata", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksimalna veličina datoteke (MB)", + "Maximum Nesting Depth": "Maksimalna dubina ugniježđivanja", + "Maximum length": "Maksimalna duljina", + "Maximum length constraint is missing": "Nedostaje ograničenje maksimalne duljine", + "Maximum length is too restrictive": "Maksimalna duljina previše je restriktivna", + "Maximum number of files to process. Set to 0 to process all files.": "Maksimalan broj datoteka za obradu. Postavite na 0 za obradu svih datoteka.", + "Maximum number of items": "Maksimalan broj stavki", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maksimalan broj objekata za obradu. Postavite na 0 za obradu svih objekata.", + "Maximum number of retry attempts for failed deliveries": "Maksimalan broj ponovnih pokušaja za neuspjele isporuke", + "Maximum time to wait before committing changes": "Maksimalno vrijeme čekanja prije potvrđivanja promjena", + "Maximum tokens to generate": "Maksimalan broj tokena za generiranje", + "Maximum value": "Maksimalna vrijednost", + "Maximum value constraint is missing": "Nedostaje ograničenje maksimalne vrijednosti", + "Maximum value is too restrictive": "Maksimalna vrijednost previše je restriktivna", + "Medium": "Srednje", + "Medium Confidence": "Srednja pouzdanost", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Srednji upiti: Pretraživanja s nekim filtriranjem ili više parametara (npr. rasponi datuma, određeni registri/sheme)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Srednji upiti: pretraživanja s nekim filtriranjem ili više parametara (npr. rasponi datuma, određeni registri/sheme)", + "Member removed successfully": "Član uspješno uklonjen", + "Members": "Članovi", + "Members ({count})": "Članovi ({count})", + "Members of selected groups can access this view": "Članovi odabranih grupa mogu pristupiti ovom prikazu", + "Members:": "Članovi:", + "Memory prediction calculated successfully": "Predviđanje memorije uspješno izračunato", + "Message content is required": "Sadržaj poruke je obavezan", + "Message does not belong to this conversation": "Poruka ne pripada ovom razgovoru", + "Message not found": "Poruka nije pronađena", + "Metadata": "Metapodaci", + "Metadata Filters": "Filteri metapodataka", + "Method": "Metoda", + "Method*": "Metoda*", + "Method: {method}": "Metoda: {method}", + "Min Length": "Min. duljina", + "Min execution time (ms)": "Min. vrijeme izvođenja (ms)", + "Min ms": "Min. ms", + "Min result count": "Min. broj rezultata", + "Min results": "Min. rezultata", + "Minimum": "Minimum", + "Minimum length": "Minimalna duljina", + "Minimum number of items": "Minimalan broj stavki", + "Minimum value": "Minimalna vrijednost", + "Minimum value constraint is missing": "Nedostaje ograničenje minimalne vrijednosti", + "Minimum value is too restrictive": "Minimalna vrijednost previše je restriktivna", + "Missing conversation or agentUuid": "Nedostaje razgovor ili agentUuid", + "Missing conversationId": "Nedostaje conversationId", + "Missing message": "Nedostaje poruka", + "Missing object fields created successfully": "Nedostajuća polja objekta uspješno stvorena", + "Mode:": "Način:", + "Model": "Model", + "Model Name": "Naziv modela", + "Modified": "Izmijenjeno", + "Monthly": "Mjesečno", + "More sources (10-20):": "Više izvora (10-20):", + "Most Active Objects": "Najaktivniji objekti", + "Multiple Of": "Višekratnik od", + "Multiple of": "Višekratnik od", + "Multiple search trail deletion not implemented yet": "Brisanje više tragova pretraživanja još nije implementirano", + "Multitenancy": "Višestruka zakupništvo", + "Multitenancy settings updated successfully": "Postavke višestruke zakupništva uspješno ažurirane", + "My Account": "Moj račun", + "Naam": "Naziv", + "Naam *": "Naziv *", + "Name": "Naziv", + "Name *": "Naziv *", + "Name*": "Naziv*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Predmemorija naziva uspješno je zagrijana: {count} naziva učitano za {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Predmemorija naziva uspješno je zagrijana: {count} naziva učitano za {time}. {message}", + "Names cache warmup completed": "Zagrijavanje predmemorije naziva dovršeno", + "Naming pattern for copies:": "Obrazac imenovanja za kopije:", + "New Collection Name": "Naziv nove zbirke", + "New Configuration": "Nova konfiguracija", + "New Conversation": "Novi razgovor", + "New Properties": "Nova svojstva", + "New Property": "Novo svojstvo", + "New User Group": "Nova korisnička grupa", + "New Value": "Nova vrijednost", + "New activity": "Nova aktivnost", + "New password": "Nova lozinka", + "New processing activity": "Nova aktivnost obrade", + "New users without specific organisation membership will be automatically added to this organisation": "Novi korisnici bez određenog članstva u organizaciji bit će automatski dodani u ovu organizaciju", + "New verwerkingsactiviteit": "Nova aktivnost obrade", + "Next": "Sljedeće", + "Nextcloud Groups": "Nextcloud grupe", + "No": "Ne", + "No API tokens.": "Nema API tokena.", + "No ConfigSets found": "Nisu pronađeni ConfigSet-ovi", + "No Deck cards linked to this object": "Nijedna Deck kartica nije povezana s ovim objektom", + "No GitHub token configured": "GitHub token nije konfiguriran", + "No GitHub token provided": "GitHub token nije naveden", + "No GitLab token provided": "GitLab token nije naveden", + "No Organisation": "Nema organizacije", + "No active filters": "Nema aktivnih filtara", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Nema aktivnih objekata za meko brisanje za shemu {schema}. Upotrijebite \"Trajno izbriši\" za uklanjanje meko izbrisanih objekata.", + "No activity data available": "Nema dostupnih podataka o aktivnosti", + "No activity found.": "Nije pronađena aktivnost.", + "No agents available": "Nema dostupnih agenata", + "No agents found": "Nisu pronađeni agenti", + "No app store cache exists yet": "Predmemorija trgovine aplikacijama još ne postoji", + "No applications are available.": "Nema dostupnih aplikacija.", + "No applications found": "Nisu pronađene aplikacije", + "No approval steps for this object.": "Nema koraka odobravanja za ovaj objekt.", + "No archived conversations": "Nema arhiviranih razgovora", + "No audit trail entries found": "Nisu pronađeni unosi revizijskog traga", + "No changes recorded": "Nisu zabilježene promjene", + "No chunks to vectorize": "Nema dijelova za vektorizaciju", + "No collections found": "Nisu pronađene zbirke", + "No configuration data": "Nema podataka o konfiguraciji", + "No configuration selected": "Nije odabrana konfiguracija", + "No configurations are available.": "Nema dostupnih konfiguracija.", + "No configurations found": "Nisu pronađene konfiguracije", + "No configurations found for this application.": "Nisu pronađene konfiguracije za ovu aplikaciju.", + "No contacts linked to this object": "Nijedan kontakt nije povezan s ovim objektom", + "No conversations yet. Create a new one to get started!": "Još nema razgovora. Stvorite novi za početak!", + "No dashboards yet": "Još nema nadzornih ploča", + "No data available": "Nema dostupnih podataka", + "No data available for chart": "Nema dostupnih podataka za grafikon", + "No deleted items found": "Nisu pronađene izbrisane stavke", + "No deletion data available": "Nema dostupnih podataka o brisanju", + "No description found": "Nije pronađen opis", + "No description provided": "Nije naveden opis", + "No emails linked to this object": "Nijedna e-pošta nije povezana s ovim objektom", + "No endpoints defined": "Nisu definirane krajnje točke", + "No entities detected for this email.": "Nisu otkriveni entiteti za ovu e-poštu.", + "No entities detected in this file. Run text extraction first.": "Nisu otkriveni entiteti u ovoj datoteci. Prvo pokrenite izvlačenje teksta.", + "No entities found": "Nisu pronađeni entiteti", + "No entities have been detected yet": "Još nisu otkriveni entiteti", + "No events found": "Nisu pronađeni događaji", + "No events linked to this object": "Nijedan događaj nije povezan s ovim objektom", + "No expired audit trails found to clear": "Nisu pronađeni istekli revizijski tragovi za brisanje", + "No expired search trails found to clear": "Nisu pronađeni istekli tragovi pretraživanja za brisanje", + "No extraction data available for this file": "Nema dostupnih podataka o izvlačenju za ovu datoteku", + "No facetable fields available. Select a register and schema to see available filters.": "Nema dostupnih polja za fasete. Odaberite registar i shemu da biste vidjeli dostupne filtre.", + "No files found": "Nisu pronađene datoteke", + "No files have been extracted yet": "Još nisu izvučene datoteke", + "No files selected": "Nisu odabrane datoteke", + "No files to process": "Nema datoteka za obradu", + "No files to reindex": "Nema datoteka za ponovno indeksiranje", + "No filters are currently active. This will delete ALL audit trail entries!": "Trenutno nije aktivan nijedan filtar. Time će se izbrisati SVI unosi revizijskog traga!", + "No groups found. Try a different search.": "Nisu pronađene grupe. Pokušajte s drugačijim pretraživanjem.", + "No label": "Nema oznake", + "No labels": "Nema oznaka", + "No linked objects": "Nema povezanih objekata", + "No log entries found": "Nisu pronađeni unosi zapisnika", + "No logs are available for this configuration.": "Nema dostupnih zapisnika za ovu konfiguraciju.", + "No logs are available for this source.": "Nema dostupnih zapisnika za ovaj izvor.", + "No logs found": "Nisu pronađeni zapisnici", + "No matches": "Nema podudaranja", + "No mismatched fields found - SOLR schema is properly configured": "Nisu pronađena nepodudarna polja - SOLR shema je ispravno konfigurirana", + "No objects are currently selected for deletion.": "Trenutno nije odabran nijedan objekt za brisanje.", + "No objects are currently selected for permanent deletion.": "Trenutno nije odabran nijedan objekt za trajno brisanje.", + "No objects are currently selected for restoration.": "Trenutno nije odabran nijedan objekt za vraćanje.", + "No objects found": "Nisu pronađeni objekti", + "No objects found to vectorize": "Nisu pronađeni objekti za vektorizaciju", + "No objects linked to this email": "Nijedan objekt nije povezan s ovom e-poštom", + "No objects selected": "Nije odabran nijedan objekt", + "No objects selected for deletion": "Nije odabran nijedan objekt za brisanje", + "No objects selected for restoration": "Nije odabran nijedan objekt za vraćanje", + "No objects to delete": "Nema objekata za brisanje", + "No objects to delete for schema {schema}": "Nema objekata za brisanje za shemu {schema}", + "No objects were permanently deleted": "Nijedan objekt nije trajno izbrisan", + "No organisations found": "Nisu pronađene organizacije", + "No personal data was found for this subject identifier.": "Nisu pronađeni osobni podaci za ovaj identifikator subjekta.", + "No processing activities yet": "Još nema aktivnosti obrade", + "No properties found": "Nisu pronađena svojstva", + "No properties match your filters.": "Nijedno svojstvo ne odgovara vašim filtrima.", + "No purge date set": "Datum čišćenja nije postavljen", + "No register data available": "Nema dostupnih podataka o registru", + "No register objects reference this file": "Nijedan objekt registra ne referencira ovu datoteku", + "No registers found": "Nisu pronađeni registri", + "No registers found for this application.": "Nisu pronađeni registri za ovu aplikaciju.", + "No registers found. Create a register to configure permissions.": "Nisu pronađeni registri. Stvorite registar za konfiguriranje dozvola.", + "No related cases found for this sender": "Nisu pronađeni povezani predmeti za ovog pošiljatelja", + "No related entities": "Nema povezanih entiteta", + "No relations found": "Nisu pronađene veze", + "No request body available": "Nema dostupnog tijela zahtjeva", + "No response body available": "Nema dostupnog tijela odgovora", + "No saved views yet. Create one in the Search tab!": "Još nema spremljenih prikaza. Stvorite jedan na kartici Pretraživanje!", + "No saved views yet. create one in the search tab!": "Još nema spremljenih prikaza. stvorite jedan na kartici pretraživanja!", + "No scan run yet": "Skeniranje još nije pokrenuto", + "No schema data available": "Nema dostupnih podataka o shemi", + "No schema selected for exploration": "Nije odabrana shema za istraživanje", + "No schemas configured for mail linking.": "Nijedna shema nije konfigurirana za povezivanje pošte.", + "No schemas found": "Nisu pronađene sheme", + "No schemas found for this application.": "Nisu pronađene sheme za ovu aplikaciju.", + "No search terms data available": "Nema dostupnih podataka o pojmovima pretraživanja", + "No search trail entries found": "Nisu pronađeni unosi traga pretraživanja", + "No sources are available.": "Nema dostupnih izvora.", + "No sources found": "Nisu pronađeni izvori", + "No synchronizations found": "Nisu pronađene sinkronizacije", + "No templates found": "Nisu pronađeni predlošci", + "No templates have been created yet": "Još nisu stvoreni predlošci", + "No tools available. Tools can be registered by installed apps.": "Nema dostupnih alata. Alate mogu registrirati instalirane aplikacije.", + "No users in this organisation.": "Nema korisnika u ovoj organizaciji.", + "No valid magic-mapped register+schema combinations found": "Nisu pronađene valjane magic-mapped kombinacije registra+sheme", + "No views found. Create views first before configuring vectorization.": "Nisu pronađeni prikazi. Prvo stvorite prikaze prije konfiguriranja vektorizacije.", + "No views match your search": "Nijedan prikaz ne odgovara vašem pretraživanju", + "No webhooks found": "Nisu pronađeni webhookovi", + "No webhooks have been configured yet": "Još nisu konfigurirani webhookovi", + "No workflows found in this project. Create workflows in the n8n editor.": "U ovom projektu nisu pronađeni tijekovi rada. Stvorite tijekove rada u n8n uređivaču.", + "None": "Nijedan", + "Not Found": "Nije pronađeno", + "Not extracted": "Nije izvučeno", + "Not helpful": "Nije korisno", + "Not in use": "Nije u upotrebi", + "Note:": "Napomena:", + "Notifications": "Obavijesti", + "Number Constraints": "Ograničenja broja", + "Number of Sources": "Broj izvora", + "Number of chunks to vectorize in one API call (1-100).": "Broj dijelova za vektorizaciju u jednom API pozivu (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Broj dijelova za vektorizaciju u jednom API pozivu. Više = brže, ali više memorije. Preporučeno: 10-50.", + "Number of objects to process in each batch (1-5000).": "Broj objekata za obradu u svakoj seriji (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Broj objekata za vektorizaciju u jednom API pozivu. Više = brže, ali više memorije. Preporučeno: 10-50.", + "Numeric Range:": "Brojčani raspon:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Zbirka objekata", + "Object Configuration:": "Konfiguracija objekta:", + "Object Count Prediction": "Predviđanje broja objekata", + "Object Extraction Mode": "Način izvlačenja objekata", + "Object ID": "ID objekta", + "Object Management": "Upravljanje objektima", + "Object Text Extraction": "Izvlačenje teksta objekta", + "Object Vectorization": "Vektorizacija objekata", + "Object Warmup": "Zagrijavanje objekata", + "Object changes in owned objects": "Promjene objekta u vlastitim objektima", + "Object collection not configured": "Zbirka objekata nije konfigurirana", + "Object metadata incomplete for file upload": "Metapodaci objekta nepotpuni za prijenos datoteke", + "Object not found": "Objekt nije pronađen", + "Object permanently deleted": "Objekt trajno izbrisan", + "Object restored successfully": "Objekt uspješno vraćen", + "Object settings updated successfully": "Postavke objekta uspješno ažurirane", + "Object sources": "Izvori objekata", + "Object successfully deleted": "Objekt uspješno izbrisan", + "Object successfully modified": "Objekt uspješno izmijenjen", + "Object unlocked successfully": "Objekt uspješno otključan", + "Object vectorization configuration saved successfully": "Konfiguracija vektorizacije objekata uspješno spremljena", + "Object vectorized successfully": "Objekt uspješno vektoriziran", + "Object:": "Objekt:", + "Objects": "Objekti", + "Objects Analyzed": "Analizirani objekti", + "Objects Distribution": "Raspodjela objekata", + "Objects being analyzed": "Objekti koji se analiziraju", + "Objects by Register": "Objekti po registru", + "Objects by Schema": "Objekti po shemi", + "Objects deletion completed for {schema}": "Brisanje objekata dovršeno za {schema}", + "Objects deletion completed successfully": "Brisanje objekata uspješno dovršeno", + "Objects in schema": "Objekti u shemi", + "Objects successfully deleted": "Objekti uspješno izbrisani", + "Objects to Process:": "Objekti za obradu:", + "Objects to be analyzed": "Objekti koji će biti analizirani", + "Objects to be deleted": "Objekti koji će biti izbrisani", + "Objects to be validated": "Objekti koji će biti provjereni", + "Objects will be serialized as JSON text before vectorization": "Objekti će biti serijalizirani kao JSON tekst prije vektorizacije", + "Objects will be soft deleted and moved to the": "Objekti će biti meko izbrisani i premješteni u", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekti će biti meko izbrisani (označeni kao izbrisani, ali zadržani u bazi podataka). Mogu se kasnije vratiti ako je potrebno.", + "Old Value": "Stara vrijednost", + "Oldest Item (days)": "Najstarija stavka (dani)", + "Ollama Chat Settings": "Postavke Ollama razgovora", + "Ollama Embedding Configuration": "Konfiguracija Ollama ugrađivanja", + "Ollama URL": "Ollama URL", + "Only With Changes": "Samo s promjenama", + "Only members of selected groups can access this organisation": "Samo članovi odabranih grupa mogu pristupiti ovoj organizaciji", + "Open Nextcloud App Store": "Otvori Nextcloud trgovinu aplikacija", + "Open OpenRegister sidebar": "Otvori OpenRegister bočnu traku", + "Open Register Objects": "Otvori objekte registra", + "Open folder": "Otvori mapu", + "Open in OpenRegister": "Otvori u OpenRegister", + "Open n8n Editor": "Otvori n8n uređivač", + "OpenAI Chat Settings": "Postavke OpenAI razgovora", + "OpenAI Embedding Configuration": "Konfiguracija OpenAI ugrađivanja", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister postavke", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Nadzorne ploče i zakazani izvještaji koje definira operater. Svaka nadzorna ploča je prvorazredni objekt u `reports` registru; widgeti se deklariraju u `widgets` polju nadzorne ploče i prikazuju uživo iz agregacija / GraphQL.", + "Optimizing search performance...": "Optimiziranje izvedbe pretraživanja...", + "Optional URL-friendly identifier": "Neobavezni identifikator prilagođen URL-u", + "Optional description for this facet": "Neobavezni opis za ovu fasetu", + "Optional end date property": "Neobavezno svojstvo datuma završetka", + "Optional event description template": "Neobavezni predložak opisa događaja", + "Optional location property": "Neobavezno svojstvo lokacije", + "Optional webhook secret for signature verification": "Neobavezna webhook tajna za provjeru potpisa", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Neobavezno: Dodajte parametre upita za filtriranje referencirane sheme (npr. status=active&type=public)", + "Or": "Ili", + "Order": "Redoslijed", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Poredane BCP 47 jezične oznake. Prvi jezik je zadani za registar i pokreće Accept-Language zamjenu za prevodiva svojstva.", + "Organisation": "Organizacija", + "Organisation Configuration": "Konfiguracija organizacije", + "Organisation ID": "ID organizacije", + "Organisation Members": "Članovi organizacije", + "Organisation Statistics": "Statistika organizacije", + "Organisation created successfully": "Organizacija uspješno stvorena", + "Organisation membership changes": "Promjene članstva u organizaciji", + "Organisation name is required": "Naziv organizacije je obavezan", + "Organisation settings saved successfully": "Postavke organizacije uspješno spremljene", + "Organisation successfully created": "Organizacija uspješno stvorena", + "Organisation successfully updated": "Organizacija uspješno ažurirana", + "Organisational measures": "Organizacijske mjere", + "Organisations": "Organizacije", + "Organisatorische maatregelen": "Organizacijske mjere", + "Organization": "Organizacija", + "Organization ID (Optional)": "ID organizacije (neobavezno)", + "Organizations": "Organizacije", + "Orphaned Items": "Stavke bez roditelja", + "Other": "Ostalo", + "Output Mapping": "Mapiranje izlaza", + "Overview": "Pregled", + "Owner": "Vlasnik", + "Owner:": "Vlasnik:", + "PDF": "PDF", + "PII hits": "PII pogodci", + "PII rows": "PII redci", + "Page {current} of {total}": "Stranica {current} od {total}", + "Parallel Mode (Faster, more resource intensive)": "Paralelni način (brže, zahtjevnije za resurse)", + "Parallel:": "Paralelno:", + "Password": "Lozinka", + "Password changes are not supported by your authentication provider.": "Vaš davatelj autentifikacije ne podržava promjene lozinke.", + "Password for SOLR authentication (optional)": "Lozinka za SOLR autentifikaciju (neobavezno)", + "Password updated successfully": "Lozinka uspješno ažurirana", + "Path": "Putanja", + "Path in repository": "Putanja u repozitoriju", + "Path where the configuration file will be saved in the repository": "Putanja na kojoj će konfiguracijska datoteka biti spremljena u repozitoriju", + "Path where the register OAS file will be saved in the repository": "Putanja na kojoj će OAS datoteka registra biti spremljena u repozitoriju", + "Pattern (regex)": "Obrazac (regex)", + "Pattern Issue": "Problem s obrascem", + "Pattern constraint is missing": "Nedostaje ograničenje obrasca", + "Patterns:": "Obrasci:", + "Pending": "Na čekanju", + "Permanent Delete Mode": "Način trajnog brisanja", + "Permanently Delete": "Trajno izbriši", + "Permanently Delete ({count})": "Trajno izbriši ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Trajno izbriši svih {count} meko izbrisanih objekata. To se ne može poništiti!", + "Permanently delete already soft-deleted objects": "Trajno izbriši već meko izbrisane objekte", + "Permanently delete this conversation?": "Trajno izbrisati ovaj razgovor?", + "Permission Matrix": "Matrica dozvola", + "Person": "Osoba", + "Personal Data": "Osobni podaci", + "Personal Data Export": "Izvoz osobnih podataka", + "Persons": "Osobe", + "Phone": "Telefon", + "Phone Numbers": "Telefonski brojevi", + "Photos": "Fotografije", + "Please create an agent in the": "Stvorite agenta u", + "Please select a user": "Odaberite korisnika", + "Please select an agent to continue": "Odaberite agenta za nastavak", + "Please select an organisation": "Odaberite organizaciju", + "Please select at least one item to import": "Odaberite barem jednu stavku za uvoz", + "Please select registers or schemas first": "Prvo odaberite registre ili sheme", + "Please select which register and schema to use for the new object": "Odaberite koji registar i shemu koristiti za novi objekt", + "Please try again later.": "Pokušajte ponovno kasnije.", + "Please wait while we fetch your agents.": "Pričekajte dok dohvaćamo vaše agente.", + "Please wait while we fetch your applications.": "Pričekajte dok dohvaćamo vaše aplikacije.", + "Please wait while we fetch your configurations.": "Pričekajte dok dohvaćamo vaše konfiguracije.", + "Please wait while we fetch your deleted items.": "Pričekajte dok dohvaćamo vaše izbrisane stavke.", + "Please wait while we fetch your sources.": "Pričekajte dok dohvaćamo vaše izvore.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Pričekajte dok trajno brišemo SOLR zbirku. To može potrajati nekoliko trenutaka.", + "Polls": "Ankete", + "Popular Search Terms": "Popularni pojmovi pretraživanja", + "Port": "Priključak", + "Portabiliteit (Art 20)": "Prenosivost (čl. 20)", + "Preferences saved": "Preferencije spremljene", + "Prerequisites": "Preduvjeti", + "Press Enter to send, Shift+Enter for new line": "Pritisnite Enter za slanje, Shift+Enter za novi redak", + "Press enter to send, shift+enter for new line": "Pritisnite enter za slanje, shift+enter za novi redak", + "Prettify": "Uljepšaj", + "Preview erasure": "Pretpregled brisanja", + "Preview:": "Pretpregled:", + "Previous": "Prethodno", + "Private": "Privatno", + "Private Agent (Default)": "Privatni agent (zadano)", + "Private agents": "Privatni agenti", + "Process Name (optional)": "Naziv procesa (neobavezno)", + "Processed {indexed} files, {failed} failed": "Obrađeno {indexed} datoteka, {failed} neuspješno", + "Processes chunks in batches with simulated parallelism.": "Obrađuje dijelove u serijama sa simuliranim paralelizmom.", + "Processes file chunks sequentially (safest).": "Obrađuje dijelove datoteke sekvencijalno (najsigurnije).", + "Processes objects in chunks with simulated parallelism.": "Obrađuje objekte u dijelovima sa simuliranim paralelizmom.", + "Processes objects sequentially (safest).": "Obrađuje objekte sekvencijalno (najsigurnije).", + "Processing": "Obrada", + "Processing Limits": "Ograničenja obrade", + "Processing...": "Obrada...", + "Project initialized successfully": "Projekt uspješno inicijaliziran", + "Project not found. Please initialize first.": "Projekt nije pronađen. Prvo inicijalizirajte.", + "Properties": "Svojstva", + "Property Behaviors": "Ponašanja svojstva", + "Property Configuration:": "Konfiguracija svojstva:", + "Property Filters": "Filtri svojstva", + "Property Title": "Naslov svojstva", + "Property Type": "Vrsta svojstva", + "Property can be improved": "Svojstvo se može poboljšati", + "Property could not be added": "Svojstvo se nije moglo dodati", + "Property could not be updated": "Svojstvo se nije moglo ažurirati", + "Property name of inversed relation": "Naziv svojstva obrnute veze", + "Property successfully added": "Svojstvo uspješno dodano", + "Property successfully updated": "Svojstvo uspješno ažurirano", + "Protected": "Zaštićeno", + "Provider": "Davatelj", + "Provider is required for testing": "Davatelj je obavezan za testiranje", + "Public": "Javno", + "Public View": "Javni prikaz", + "Public views can be accessed by anyone in the system": "Javnim prikazima može pristupiti bilo tko u sustavu", + "Publish": "Objavi", + "Publish Configuration to GitHub": "Objavi konfiguraciju na GitHub", + "Publish OAS": "Objavi OAS", + "Publish Objects": "Objavi objekte", + "Publish Register OAS to GitHub": "Objavi OAS registra na GitHub", + "Publish all objects in this schema": "Objavi sve objekte u ovoj shemi", + "Publish objects to make them publicly available": "Objavi objekte kako bi bili javno dostupni", + "Published": "Objavljeno", + "Published:": "Objavljeno:", + "Publishing...": "Objavljivanje...", + "Purge": "Očisti", + "Purge Date": "Datum čišćenja", + "Purge {count} object from database": "Očisti {count} objekt iz baze podataka", + "Purge {count} objects from database": "Očisti {count} objekata iz baze podataka", + "Purpose limitation *": "Ograničenje svrhe *", + "Push Notifications": "Push obavijesti", + "Query Complexity Distribution": "Raspodjela složenosti upita", + "Query Help": "Pomoć za upite", + "Query parameter is required": "Parametar upita je obavezan", + "Quota": "Kvota", + "RAG": "RAG", + "RAG Configuration": "RAG konfiguracija", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC postavke uspješno ažurirane", + "Rate limits cleared successfully": "Ograničenja brzine uspješno obrisana", + "Raw Changes Data": "Neobrađeni podaci o promjenama", + "Re-vectorize on object update": "Ponovno vektoriziraj pri ažuriranju objekta", + "Read": "Čitanje", + "Real-time push notification status via notify_push": "Status push obavijesti u stvarnom vremenu putem notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push obavijesti u stvarnom vremenu su aktivne. Povezani klijenti primaju trenutna ažuriranja kada se objekti stvore, ažuriraju ili izbrišu.", + "Realtime push active": "Push u stvarnom vremenu aktivan", + "Realtime push not available — the notify_push app is not installed": "Push u stvarnom vremenu nije dostupan — aplikacija notify_push nije instalirana", + "Reason": "Razlog", + "Reason (optional)": "Razlog (neobavezno)", + "Rebase operation completed successfully": "Operacija rebase uspješno dovršena", + "Rebase operation failed: {error}": "Operacija rebase nije uspjela: {error}", + "Rechtsgrond": "Pravna osnova", + "Recommendations:": "Preporuke:", + "Recommended Type:": "Preporučena vrsta:", + "Recommended: 5 sources": "Preporučeno: 5 izvora", + "Refresh": "Osvježi", + "Refresh Data": "Osvježi podatke", + "Refresh Stats": "Osvježi statistiku", + "Refresh Workflows": "Osvježi tijekove rada", + "Refresh dashboard": "Osvježi nadzornu ploču", + "Refresh database info": "Osvježi informacije o bazi podataka", + "Register": "Registar", + "Register #{id}": "Registar #{id}", + "Register / Schema": "Registar / Shema", + "Register ID": "ID registra", + "Register Objects": "Objekti registra", + "Register Overview": "Pregled registra", + "Register Statistics": "Statistika registra", + "Register Totals": "Ukupni iznosi registra", + "Register and schema are required": "Registar i shema su obavezni", + "Register annotation": "Anotacija registra", + "Register depublished successfully": "Registar uspješno povučen iz objave", + "Register not found": "Registar nije pronađen", + "Register objects": "Objekti registra", + "Register objects deletion completed successfully": "Brisanje objekata registra uspješno dovršeno", + "Register published successfully": "Registar uspješno objavljen", + "Register/Schema Usage": "Upotreba registra/sheme", + "Register:": "Registar:", + "Registers": "Registri", + "Reindex": "Ponovno indeksiraj", + "Reindex completed": "Ponovno indeksiranje dovršeno", + "Reindex completed successfully": "Ponovno indeksiranje uspješno dovršeno", + "Reindex completed! Processed {count} objects in {duration}s": "Ponovno indeksiranje dovršeno! Obrađeno {count} objekata za {duration}s", + "Reindex failed": "Ponovno indeksiranje nije uspjelo", + "Reindex failed: %s": "Ponovno indeksiranje nije uspjelo: %s", + "Reject": "Odbij", + "Rejected": "Odbijeno", + "Related Cases": "Povezani predmeti", + "Relations": "Veze", + "Remote Version": "Udaljena verzija", + "Remove": "Ukloni", + "Remove Deck card": "Ukloni Deck karticu", + "Remove avatar": "Ukloni avatar", + "Remove contact": "Ukloni kontakt", + "Remove filter": "Ukloni filtar", + "Remove from favorites": "Ukloni iz favorita", + "Remove from list": "Ukloni s popisa", + "Remove group": "Ukloni grupu", + "Remove link": "Ukloni vezu", + "Remove link to {name}": "Ukloni vezu na {name}", + "Remove link to {name}?": "Ukloniti vezu na {name}?", + "Remove link to {title}": "Ukloni vezu na {title}", + "Remove oneOf entry": "Ukloni oneOf unos", + "Remove property after write-back": "Ukloni svojstvo nakon povratnog upisa", + "Remove {title}": "Ukloni {title}", + "Remove {userId} from this organisation?": "Ukloniti {userId} iz ove organizacije?", + "Removed": "Uklonjeno", + "Removed from favorites": "Uklonjeno iz favorita", + "Removing schema {schema}...": "Uklanjanje sheme {schema}...", + "Rename Conversation": "Preimenuj razgovor", + "Rename conversation": "Preimenuj razgovor", + "Replicas": "Replike", + "Reports": "Izvještaji", + "Repository": "Repozitorij", + "Request Body": "Tijelo zahtjeva", + "Request Data": "Podaci zahtjeva", + "Request Details": "Pojedinosti zahtjeva", + "Request Quota (per day)": "Kvota zahtjeva (po danu)", + "Request account deactivation": "Zatraži deaktivaciju računa", + "Request account deactivation. This will notify administrators for review.": "Zatraži deaktivaciju računa. Time će administratori biti obaviješteni radi pregleda.", + "Request body must be JSON with a \"query\" field": "Tijelo zahtjeva mora biti JSON s poljem \"query\"", + "Request timeout in seconds": "Istek zahtjeva u sekundama", + "Requested at": "Zatraženo u", + "Required": "Obavezno", + "Required Token Scopes": "Obavezni opsezi tokena", + "Required field": "Obavezno polje", + "Required status is inconsistent": "Status obaveznosti je nedosljedan", + "Requires Dolphin AI for OCR text extraction": "Zahtijeva Dolphin AI za OCR izvlačenje teksta", + "Rerun Search": "Ponovno pokreni pretraživanje", + "Rerunning search: {searchTerm}": "Ponovno pokretanje pretraživanja: {searchTerm}", + "Reset Changes": "Poništi promjene", + "Reset Filters": "Poništi filtre", + "Resource Quotas": "Kvote resursa", + "Response Body": "Tijelo odgovora", + "Response Details": "Pojedinosti odgovora", + "Response Time:": "Vrijeme odgovora:", + "Restore": "Vrati", + "Restore conversation": "Vrati razgovor", + "Restore or permanently delete items": "Vrati ili trajno izbriši stavke", + "Restore {count} object": "Vrati {count} objekt", + "Restore {count} objects": "Vrati {count} objekata", + "Result Count Range": "Raspon broja rezultata", + "Results": "Rezultati", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Rok čuvanja (ISO-8601 trajanje, npr. P10Y, P30D)", + "Retention settings updated successfully": "Postavke čuvanja uspješno ažurirane", + "Retrieve all objects for this schema": "Dohvati sve objekte za ovu shemu", + "Retry": "Pokušaj ponovno", + "Retry Failed Extractions": "Ponovi neuspjela izvlačenja", + "Retry Policy": "Pravila ponovnog pokušaja", + "Retry completed": "Ponovni pokušaj dovršen", + "Retry upload": "Ponovi prijenos", + "Retrying...": "Ponovni pokušaj...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite izbrisati klikom na gumb za uklanjanje.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite uključiti.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite trajno izbrisati klikom na gumb za uklanjanje. Ova se radnja ne može poništiti.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Pregledajte odabrane objekte u nastavku. Možete ukloniti sve objekte koje ne želite vratiti klikom na gumb za uklanjanje. Objekti će biti vraćeni na svoju izvornu lokaciju.", + "Revoke": "Opozovi", + "Right": "Desno", + "Risk Level": "Razina rizika", + "Risk level": "Razina rizika", + "Roles and Permissions": "Uloge i dozvole", + "Roles updated successfully": "Uloge uspješno ažurirane", + "Run compliance scan": "Pokreni skeniranje usklađenosti", + "SOLR Connection Settings": "Postavke SOLR veze", + "SOLR Index Warmup": "Zagrijavanje SOLR indeksa", + "SOLR Version:": "SOLR verzija:", + "SOLR actions menu": "Izbornik SOLR radnji", + "SOLR base path (usually /solr)": "SOLR osnovna putanja (obično /solr)", + "SOLR collection deleted successfully": "SOLR zbirka uspješno izbrisana", + "SOLR connection test failed: {error}": "Test SOLR veze nije uspio: {error}", + "SOLR connection test successful!": "Test SOLR veze uspješan!", + "SOLR index warmup completed successfully": "Zagrijavanje SOLR indeksa uspješno dovršeno", + "SOLR is disabled": "SOLR je onemogućen", + "SOLR is not available or not configured": "SOLR nije dostupan ili nije konfiguriran", + "SOLR logging disabled": "SOLR zapisivanje onemogućeno", + "SOLR logging enabled": "SOLR zapisivanje omogućeno", + "SOLR search disabled": "SOLR pretraživanje onemogućeno", + "SOLR search enabled": "SOLR pretraživanje omogućeno", + "SOLR server hostname or IP address": "SOLR naziv poslužitelja ili IP adresa", + "SOLR server port number (optional, defaults to 8983)": "Broj priključka SOLR poslužitelja (neobavezno, zadano 8983)", + "SOLR settings updated successfully": "SOLR postavke uspješno ažurirane", + "SOLR setup completed successfully": "SOLR postavljanje uspješno dovršeno", + "SOLR setup completed successfully!": "SOLR postavljanje uspješno dovršeno!", + "SOLR setup error: %s": "Pogreška SOLR postavljanja: %s", + "SOLR setup failed": "SOLR postavljanje nije uspjelo", + "SOLR setup failed - check logs": "SOLR postavljanje nije uspjelo - provjerite zapisnike", + "SOLR setup failed. Please check the configuration and try again.": "SOLR postavljanje nije uspjelo. Provjerite konfiguraciju i pokušajte ponovno.", + "SOLR setup failed: {error}": "SOLR postavljanje nije uspjelo: {error}", + "SOLR warmup failed: {error}": "Zagrijavanje SOLR nije uspjelo: {error}", + "Sample Values:": "Uzorci vrijednosti:", + "Save": "Spremi", + "Save Configuration": "Spremi konfiguraciju", + "Save Connection Settings": "Spremi postavke veze", + "Save Roles": "Spremi uloge", + "Save Settings": "Spremi postavke", + "Save changes": "Spremi promjene", + "Save current search as view": "Spremi trenutno pretraživanje kao prikaz", + "Save labels": "Spremi oznake", + "Save tags for {name}": "Spremi oznake za {name}", + "Save the organisation first to manage users.": "Prvo spremite organizaciju za upravljanje korisnicima.", + "Saved Views": "Spremljeni prikazi", + "Saving...": "Spremanje...", + "Scheduled": "Zakazano", + "Schema": "Shema", + "Schema ID": "ID sheme", + "Schema Name": "Naziv sheme", + "Schema Statistics": "Statistika sheme", + "Schema annotation": "Anotacija sheme", + "Schema configuration updated successfully for {schema}": "Konfiguracija sheme uspješno ažurirana za {schema}", + "Schema depublished successfully": "Shema uspješno povučena iz objave", + "Schema objects deletion completed successfully": "Brisanje objekata sheme uspješno dovršeno", + "Schema published successfully": "Shema uspješno objavljena", + "Schema reference ($ref)": "Referenca sheme ($ref)", + "Schema successfully created": "Shema uspješno stvorena", + "Schema successfully updated": "Shema uspješno ažurirana", + "Schema {schema} removed successfully": "Shema {schema} uspješno uklonjena", + "Schema:": "Shema:", + "Schemas": "Sheme", + "Scheme": "Shema", + "Search": "Pretraži", + "Search / Views": "Pretraživanje / Prikazi", + "Search / views": "Pretraživanje / prikazi", + "Search Activity": "Aktivnost pretraživanja", + "Search Analytics": "Analitika pretraživanja", + "Search Configuration": "Konfiguracija pretraživanja", + "Search Entities": "Pretraži entitete", + "Search Files": "Pretraži datoteke", + "Search GitHub": "Pretraži GitHub", + "Search GitLab": "Pretraži GitLab", + "Search Method": "Metoda pretraživanja", + "Search Mode": "Način pretraživanja", + "Search Query": "Upit pretraživanja", + "Search Results": "Rezultati pretraživanja", + "Search Term": "Pojam pretraživanja", + "Search Trail Details": "Pojedinosti traga pretraživanja", + "Search Trail Management": "Upravljanje tragom pretraživanja", + "Search Trail Statistics": "Statistika traga pretraživanja", + "Search Trails": "Tragovi pretraživanja", + "Search Views": "Prikazi pretraživanja", + "Search Webhooks": "Pretraži webhookove", + "Search agents...": "Pretraži agente...", + "Search applications...": "Pretraži aplikacije...", + "Search by file name or path": "Pretraži po nazivu datoteke ili putanji", + "Search by name or URL": "Pretraži po nazivu ili URL-u", + "Search by title or UUID...": "Pretraži po naslovu ili UUID...", + "Search by value": "Pretraži po vrijednosti", + "Search configurations": "Pretraži konfiguracije", + "Search data sources...": "Pretraži izvore podataka...", + "Search groups...": "Pretraži grupe...", + "Search in Database Objects": "Pretraži objekte u bazi podataka", + "Search in Files": "Pretraži datoteke", + "Search in Objects": "Pretraži objekte", + "Search objects": "Pretraži objekte", + "Search objects...": "Pretraži objekte...", + "Search property names...": "Pretraži nazive svojstava...", + "Search registers...": "Pretraži registre...", + "Search schemas...": "Pretraži sheme...", + "Search trail deletion not implemented yet": "Brisanje traga pretraživanja još nije implementirano", + "Search views...": "Pretraži prikaze...", + "Search {name}...": "Pretraži {name}...", + "Searching...": "Pretraživanje...", + "Secret": "Tajna", + "Secret key for HMAC signature generation (optional)": "Tajni ključ za generiranje HMAC potpisa (neobavezno)", + "Security": "Sigurnost", + "See organisations as a table": "Prikaži organizacije kao tablicu", + "See organisations as cards": "Prikaži organizacije kao kartice", + "Select AI Agent": "Odaberi AI agenta", + "Select Active Organisation": "Odaberi aktivnu organizaciju", + "Select All": "Odaberi sve", + "Select ConfigSet": "Odaberite ConfigSet", + "Select File Types to Vectorize:": "Odaberite vrste datoteka za vektorizaciju:", + "Select HTTP method": "Odaberite HTTP metodu", + "Select Register and Schema": "Odaberite Registar i Shemu", + "Select Views to Vectorize:": "Odaberite prikaze za vektorizaciju:", + "Select a Nextcloud group to add": "Odaberite Nextcloud grupu za dodavanje", + "Select a branch": "Odaberite granu", + "Select a date property": "Odaberite svojstvo datuma", + "Select a model or type a custom model name": "Odaberite model ili upišite prilagođeni naziv modela", + "Select a register": "Odaberite registar", + "Select a register...": "Odaberite registar...", + "Select a repository": "Odaberite repozitorij", + "Select a repository you have write access to": "Odaberite repozitorij na koji imate pravo pisanja", + "Select a schema": "Odaberite shemu", + "Select a schema...": "Odaberite shemu...", + "Select a user": "Odaberite korisnika", + "Select agent type": "Odaberite vrstu agenta", + "Select all entities": "Odaberite sve entitete", + "Select an AI Agent": "Odaberite AI agenta", + "Select an AI agent to begin chatting with your data.": "Odaberite AI agenta za početak razgovora s vašim podacima.", + "Select an email to see linked objects": "Odaberite e-poštu za prikaz povezanih objekata", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Odaberite organizaciju i korisnika da biste ih dodali kao člana. Pretražite organizacije po nazivu.", + "Select backend": "Odaberite pozadinski sustav", + "Select chat model": "Odaberite model razgovora", + "Select collection for files": "Odaberite kolekciju za datoteke", + "Select collection for objects": "Odaberite kolekciju za objekte", + "Select configuration type...": "Odaberite vrstu konfiguracije...", + "Select default organisation": "Odaberite zadanu organizaciju", + "Select entity {value}": "Odaberite entitet {value}", + "Select event to listen to...": "Odaberite događaj koji će se osluškivati...", + "Select fields to display": "Odaberite polja za prikaz", + "Select files with the correct extension": "Odaberite datoteke s ispravnom ekstenzijom", + "Select group": "Odaberite grupu", + "Select groups (optional)": "Odaberite grupe (neobavezno)", + "Select groups to notify...": "Odaberite grupe za obavještavanje...", + "Select groups with access to this agent": "Odaberite grupe s pristupom ovom agentu", + "Select groups...": "Odaberite grupe...", + "Select model": "Odaberite model", + "Select one or more registers": "Odaberite jedan ili više registara", + "Select one or more schemas": "Odaberite jednu ili više shema", + "Select options...": "Odaberite opcije...", + "Select or create labels, or select \"No label\" to add files": "Odaberite ili stvorite oznake ili odaberite \"Bez oznake\" za dodavanje datoteka", + "Select owner application (optional)...": "Odaberite vlasničku aplikaciju (neobavezno)...", + "Select period": "Odaberite razdoblje", + "Select property to send as payload": "Odaberite svojstvo za slanje kao korisni teret", + "Select provider": "Odaberite pružatelja usluge", + "Select registers and schemas to save a view": "Odaberite registre i sheme za spremanje prikaza", + "Select retry policy": "Odaberite pravilo ponovnog pokušaja", + "Select role": "Odaberite ulogu", + "Select schemas (empty = all schemas)": "Odaberite sheme (prazno = sve sheme)", + "Select schemas to warm up": "Odaberite sheme za zagrijavanje", + "Select search mode": "Odaberite način pretraživanja", + "Select source type...": "Odaberite vrstu izvora...", + "Select target type": "Odaberite ciljnu vrstu", + "Select the branch to publish to": "Odaberite granu za objavljivanje", + "Select the event this webhook should listen to": "Odaberite događaj koji bi ovaj webhook trebao osluškivati", + "Select views to filter data (optional)": "Odaberite prikaze za filtriranje podataka (neobavezno)", + "Select views to limit which data the agent can access": "Odaberite prikaze za ograničavanje podataka kojima agent može pristupiti", + "Select views to vectorize:": "Odaberite prikaze za vektorizaciju:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Odaberite koje su Nextcloud grupe dostupne za ovu organizaciju. Korisnici u tim grupama imat će pristup resursima organizacije.", + "Select which data views the AI can search": "Odaberite koje prikaze podataka AI može pretraživati", + "Select which property from the event should be used as the webhook payload data": "Odaberite koje svojstvo iz događaja treba koristiti kao podatke korisnog tereta webhooka", + "Select which tools the AI can use to perform actions": "Odaberite koje alate AI može koristiti za izvođenje radnji", + "Select {fieldName} values": "Odaberite vrijednosti {fieldName}", + "Selected": "Odabrano", + "Selected Groups": "Odabrane grupe", + "Selected Objects ({count})": "Odabrani objekti ({count})", + "Selected audit trails deleted successfully": "Odabrani revizijski tragovi uspješno su izbrisani", + "Selected search trails deleted successfully": "Odabrani tragovi pretraživanja uspješno su izbrisani", + "Selected users can access this view": "Odabrani korisnici mogu pristupiti ovom prikazu", + "Send additional feedback": "Pošaljite dodatne povratne informacije", + "Send as CloudEvent": "Pošalji kao CloudEvent", + "Send as cloudevent": "Pošalji kao cloudevent", + "Sensitive PII": "Osjetljivi PII", + "Serial Mode (Safer, slower)": "Serijski način rada (sigurniji, sporiji)", + "Serial:": "Serijski:", + "Server Configuration": "Konfiguracija poslužitelja", + "Server Information": "Informacije o poslužitelju", + "Set as Active": "Postavi kao aktivno", + "Set as active organisation": "Postavi kao aktivnu organizaciju", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Postavite ograničenja za korištenje API-ja i potrošnju tokena. Koristite 0 za neograničene resurse.", + "Settings": "Postavke", + "Settings updated successfully": "Postavke su uspješno ažurirane", + "Shards": "Krhotine", + "Share": "Dijeli", + "Share with Groups": "Dijeli s grupama", + "Share with Users": "Dijeli s korisnicima", + "Show": "Prikaži", + "Show Filters": "Prikaži filtre", + "Show less": "Prikaži manje", + "Show only entries with changes": "Prikaži samo unose s promjenama", + "Showing {count} dashboard(s)": "Prikazuje se {count} nadzornih ploča", + "Showing {count} processing activities": "Prikazuje se {count} aktivnosti obrade", + "Showing {showing} of {total} agents": "Prikazuje se {showing} od {total} agenata", + "Showing {showing} of {total} applications": "Prikazuje se {showing} od {total} aplikacija", + "Showing {showing} of {total} audit trail entries": "Prikazuje se {showing} od {total} unosa revizijskog traga", + "Showing {showing} of {total} configurations": "Prikazuje se {showing} od {total} konfiguracija", + "Showing {showing} of {total} deleted items": "Prikazuje se {showing} od {total} izbrisanih stavki", + "Showing {showing} of {total} entities": "Prikazuje se {showing} od {total} entiteta", + "Showing {showing} of {total} files": "Prikazuje se {showing} od {total} datoteka", + "Showing {showing} of {total} log entries": "Prikazuje se {showing} od {total} unosa zapisnika", + "Showing {showing} of {total} search trail entries": "Prikazuje se {showing} od {total} unosa traga pretraživanja", + "Showing {showing} of {total} sources": "Prikazuje se {showing} od {total} izvora", + "Showing {showing} of {total} templates": "Prikazuje se {showing} od {total} predložaka", + "Showing {showing} of {total} webhooks": "Prikazuje se {showing} od {total} webhookova", + "Showing {shown} of {total} organisations": "Prikazuje se {shown} od {total} organizacija", + "Simple": "Jednostavno", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednostavni upiti: osnovna pretraživanja teksta s minimalnim parametrima (npr. jedan pojam za pretraživanje, bez naprednih filtara)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Jednostavni upiti: osnovna pretraživanja teksta s minimalnim parametrima (npr. jedan pojam za pretraživanje, bez naprednih filtara)", + "Size": "Veličina", + "Slug": "Slug", + "Soft Delete Mode": "Način meke brisanja", + "Soft Deleted Items": "Meko izbrisane stavke", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Meko izbriši sve objekte za ovu shemu ({active} aktivnih, {deleted} već izbrisanih)", + "SolrCloud mode enabled": "SolrCloud način rada omogućen", + "Something went wrong while adding files": "Nešto je pošlo po zlu prilikom dodavanja datoteka", + "Source": "Izvor", + "Source successfully updated": "Izvor je uspješno ažuriran", + "Sources": "Izvori", + "Sources:": "Izvori:", + "Special Rights": "Posebna prava", + "Standalone SOLR mode": "Samostalni SOLR način rada", + "Start Conversation": "Započni razgovor", + "Start Date Field": "Polje datuma početka", + "Start Vectorization": "Pokreni vektorizaciju", + "Start a conversation": "Započni razgovor", + "Starting names cache warmup...": "Pokretanje zagrijavanja predmemorije naziva...", + "Starting validation for {schema}...": "Pokretanje provjere valjanosti za {schema}...", + "Starting {action} for {schema}...": "Pokretanje {action} za {schema}...", + "Starting...": "Pokretanje...", + "Statistics": "Statistika", + "Statistics and Metrics": "Statistika i mjerni podaci", + "Statistics refreshed": "Statistika je osvježena", + "Stats calculation not yet implemented": "Izračun statistike još nije implementiran", + "Status": "Status", + "Status Code": "Statusni kod", + "Step": "Korak", + "Storage": "Pohrana", + "Storage Quota (MB)": "Kvota pohrane (MB)", + "String Constraints": "Ograničenja niza", + "Subject identifier (email, BSN, name, etc.)": "Identifikator subjekta (e-pošta, BSN, ime itd.)", + "Subscribe": "Pretplati se", + "Subscribed": "Pretplaćeno", + "Success": "Uspjeh", + "Success Rate": "Stopa uspjeha", + "Success Status": "Status uspjeha", + "Successful": "Uspješno", + "Successfully added user to organisation: {name}": "Korisnik je uspješno dodan u organizaciju: {name}", + "Successfully cleared {count} audit trails": "Uspješno je obrisano {count} revizijskih tragova", + "Successfully cleared {count} blob storage objects": "Uspješno je obrisano {count} objekata blob pohrane", + "Successfully cleared {count} search trails": "Uspješno je obrisano {count} tragova pretraživanja", + "Successfully created {count} SOLR fields": "Uspješno je stvoreno {count} SOLR polja", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Uspješno je izbrisano {count} ugrađivanja. Ponovno vektorizirajte svoje podatke.", + "Successfully deleted {count} objects for {schema}": "Uspješno je izbrisano {count} objekata za {schema}", + "Successfully fixed {count} SOLR field configurations": "Uspješno je popravljeno {count} konfiguracija SOLR polja", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Uspješno uvezeno: {registers} registara, {schemas} shema, {objects} objekata", + "Successfully joined organisation": "Uspješno ste se pridružili organizaciji", + "Successfully permanently deleted {count} objects": "Uspješno je trajno izbrisano {count} objekata", + "Successfully processed {count} files!": "Uspješno je obrađeno {count} datoteka!", + "Successfully restored {count} object": "Uspješno je obnovljen {count} objekt", + "Successfully restored {count} objects": "Uspješno je obnovljeno {count} objekata", + "Successfully saved configuration for {count} facets!": "Uspješno je spremljena konfiguracija za {count} faseta!", + "Supported File Types": "Podržane vrste datoteka", + "Switch Active Organisation": "Promijeni aktivnu organizaciju", + "Switch Organisation": "Promijeni organizaciju", + "Sync Table": "Sinkroniziraj tablicu", + "Synchronization Settings": "Postavke sinkronizacije", + "Syncing magic table for {schema}...": "Sinkronizacija magične tablice za {schema}...", + "Syncs": "Sinkronizacije", + "System Default": "Zadano sustavom", + "System Prompt": "Sistemski upit", + "System announcements": "Obavijesti sustava", + "Table": "Tablica", + "Target ID": "Ciljni ID", + "Target Type*": "Ciljna vrsta*", + "Technical Description": "Tehnički opis", + "Technical Details": "Tehnički detalji", + "Technical description for developers and administrators": "Tehnički opis za razvojne programere i administratore", + "Technical measures": "Tehničke mjere", + "Technische maatregelen": "Tehničke mjere", + "Temperature": "Temperatura", + "Templates": "Predlošci", + "Test": "Test", + "Test Chat": "Testni razgovor", + "Test Connection": "Testiraj vezu", + "Test Embedding": "Testiraj ugrađivanje", + "Test endpoint executed successfully": "Testna krajnja točka uspješno je izvršena", + "Test webhook delivered successfully": "Testni webhook uspješno je isporučen", + "Test webhook delivery failed": "Isporuka testnog webhooka nije uspjela", + "Test webhook sent successfully": "Testni webhook uspješno je poslan", + "Testing...": "Testiranje...", + "Text Chunk #{id}": "Tekstualni isječak #{id}", + "Text Extraction": "Izvlačenje teksta", + "Text Extraction Engine": "Mehanizam za izvlačenje teksta", + "Text chunks": "Tekstualni isječci", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Tekstualni isječci generiraju se tijekom izvlačenja datoteka i pohranjuju u bazu podataka. Vektorizacija čita te unaprijed isječene datoteke i pretvara ih u ugrađivanja.", + "Text extracted successfully": "Tekst je uspješno izvučen", + "Text extraction disabled": "Izvlačenje teksta onemogućeno", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Aplikacija Nextcloud Calendar nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Aplikacija Nextcloud Contacts nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Aplikacija Nextcloud Deck nije instalirana ili omogućena na ovom poslužitelju.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Aplikacija Nextcloud Mail nije instalirana ili omogućena na ovom poslužitelju.", + "The URL where webhook events will be sent": "URL na koji će se slati webhook događaji", + "The requested conversation does not exist": "Traženi razgovor ne postoji", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Tražena nadzorna ploča nije se mogla učitati. Provjerite URL ili odaberite drugu nadzornu ploču s popisa.", + "The saved view and all its search configuration": "Spremljeni prikaz i sva njegova konfiguracija pretraživanja", + "There are no audit trail entries matching your current filters.": "Nema unosa revizijskog traga koji odgovaraju vašim trenutnim filtrima.", + "There are no deleted items matching your current filters.": "Nema izbrisanih stavki koje odgovaraju vašim trenutnim filtrima.", + "There are no search trail entries matching your current filters.": "Nema unosa traga pretraživanja koji odgovaraju vašim trenutnim filtrima.", + "There are no webhook log entries matching your filters.": "Nema unosa zapisnika webhooka koji odgovaraju vašim filtrima.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Bit će zadržani u skladu s konfiguriranim rokom čuvanja njihove sheme i automatski trajno izbrisani kada rok čuvanja istekne. Rok čuvanja moguće je konfigurirati po shemi i nalazi se u postavkama sheme.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Bit će zadržani u skladu s konfiguriranim rokom čuvanja njihove sheme, a zatim trajno uklonjeni.", + "This action cannot be undone.": "Ova se radnja ne može poništiti.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Ova se radnja ne može poništiti. Provjerite da nijedna kolekcija ne koristi ovaj ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Ova se radnja ne može poništiti. Provjerite da nijedna kolekcija ne koristi ovaj configset.", + "This action will submit a deactivation request to your administrators.": "Ova će radnja poslati zahtjev za deaktivaciju vašim administratorima.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Ovaj agent nema konfigurabilne prikaze ili alate, ali još uvijek možete prilagoditi RAG postavke u nastavku.", + "This analysis may take some time": "Ova analiza može potrajati", + "This audit trail entry does not contain any change information.": "Ovaj unos revizijskog traga ne sadrži nikakve informacije o promjenama.", + "This configuration has no data defined.": "Ova konfiguracija nema definirane podatke.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ova je krajnja točka zastarjela. Umjesto toga koristite krajnje točke temeljene na isječcima.", + "This entity has no relations to objects or files": "Ovaj entitet nema veza s objektima ili datotekama", + "This is a test webhook from OpenRegister": "Ovo je testni webhook iz OpenRegister", + "This is not valid JSON": "Ovo nije valjani JSON", + "This key already exists on this schema": "Ovaj ključ već postoji u ovoj shemi", + "This schema must use Magic Table configuration to sync": "Ova shema mora koristiti konfiguraciju Magične tablice za sinkronizaciju", + "This schema must use magic table configuration to sync": "Ova shema mora koristiti konfiguraciju magične tablice za sinkronizaciju", + "This source has no associated registers.": "Ovaj izvor nema povezanih registara.", + "This token will only be shown once. Copy it now.": "Ovaj će se token prikazati samo jednom. Kopirajte ga sada.", + "This will copy the _default ConfigSet with the new name": "Ovo će kopirati _default ConfigSet s novim nazivom", + "This will copy the _default configset with the new name": "Ovo će kopirati _default configset s novim nazivom", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Ovo će trajno izbrisati SVA ugrađivanja (vektore) iz baze podataka. Morat ćete ponovno vektorizirati sve objekte i datoteke. Ova se radnja ne može poništiti.\n\nJeste li sigurni da želite nastaviti?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Ovo će trajno izbrisati SVA ugrađivanja (vektore) iz baze podataka. Morat ćete ponovno vektorizirati sve objekte i datoteke. Ova se radnja ne može poništiti.\\n\\nJeste li sigurni da želite nastaviti?", + "This will permanently delete:": "Ovo će trajno izbrisati:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Ovo će trajno ukloniti {count} već meko izbrisanih objekata iz baze podataka. Ova se radnja ne može poništiti.", + "Timeout (seconds)": "Vremensko ograničenje (sekunde)", + "Timestamp": "Vremenska oznaka", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Savjet: Omogućite samo prikaze kojima je potrebno semantičko pretraživanje kako biste minimizirali troškove ugrađivanja. Jednostavne tablice za pretraživanje rijetko trebaju vektorizaciju.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Savjet: omogućite samo prikaze kojima je potrebno semantičko pretraživanje kako biste minimizirali troškove ugrađivanja. Jednostavne tablice za pretraživanje rijetko trebaju vektorizaciju.", + "Title": "Naslov", + "Title *": "Naslov *", + "Title Template": "Predložak naslova", + "Title*": "Naslov*", + "To (e.g. -1 year or 2025-12-31)": "Do (npr. -1 godina ili 2025-12-31)", + "To Date": "Do datuma", + "To add files larger than or equal to 512MB, go to the": "Za dodavanje datoteka većih ili jednakih 512 MB, idite na", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Za prilagodbu veličine isječka ili strategije, idite na Konfiguracija datoteke → Ograničenja obrade.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Za prilagodbu veličine isječka ili strategije, idite na konfiguracija datoteke → ograničenja obrade.", + "To change the embedding provider or model, go to LLM Configuration.": "Za promjenu pružatelja usluge ugrađivanja ili modela, idite na LLM konfiguraciju.", + "To change the embedding provider or model, go to LLM configuration.": "Za promjenu pružatelja usluge ugrađivanja ili modela, idite na LLM konfiguraciju.", + "To date": "Do datuma", + "Toggle search sidebar": "Uključi/isključi bočnu traku pretraživanja", + "Token Created": "Token stvoren", + "Token Quota (per request)": "Kvota tokena (po zahtjevu)", + "Token copied to clipboard": "Token kopiran u međuspremnik", + "Token name": "Naziv tokena", + "Token revoked": "Token opozvan", + "Tools": "Alati", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Alati se izvršavaju sa zadanim korisničkim ovlastima agenta kada nijedna korisnička sesija nije aktivna (npr. cron poslovi). Konfigurirajte zadanog korisnika na kartici Postavke.", + "Tools respect the agent's own role and organization boundaries.": "Alati poštuju vlastitu ulogu agenta i granice organizacije.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Alati poštuju prikaze, ovlasti i granice organizacije agenta.", + "Top Deleters": "Najčešći brisači", + "Total": "Ukupno", + "Total Audit Trails": "Ukupno revizijskih tragova", + "Total Chunks Available:": "Ukupno dostupnih isječaka:", + "Total Deleted Items": "Ukupno izbrisanih stavki", + "Total Members": "Ukupno članova", + "Total Objects": "Ukupno objekata", + "Total Objects in Database:": "Ukupno objekata u bazi podataka:", + "Total Organisations": "Ukupno organizacija", + "Total Results": "Ukupno rezultata", + "Total Searches": "Ukupno pretraživanja", + "Total Size": "Ukupna veličina", + "Total Storage": "Ukupna pohrana", + "Total events": "Ukupno događaja", + "Total:": "Ukupno:", + "Totals": "Ukupni iznosi", + "Try searching by UUID or with different keywords": "Pokušajte pretraživati po UUID-u ili s drugačijim ključnim riječima", + "Type": "Vrsta", + "Type \"DELETE COLLECTION\" to confirm:": "Upišite \"DELETE COLLECTION\" za potvrdu:", + "Type Issue": "Vrsta problema", + "Type Variations:": "Varijacije vrste:", + "Type filter (optional, e.g. email)": "Filtar vrste (neobavezno, npr. e-pošta)", + "Type must be \"positive\" or \"negative\"": "Vrsta mora biti \"positive\" ili \"negative\"", + "Type to search for groups": "Upišite za pretraživanje grupa", + "Type to search for objects...": "Upišite za pretraživanje objekata...", + "Type to search for organisations": "Upišite za pretraživanje organizacija", + "Type to search for users": "Upišite za pretraživanje korisnika", + "Type to search groups...": "Upišite za pretraživanje grupa...", + "Type to search users...": "Upišite za pretraživanje korisnika...", + "Type to search...": "Upišite za pretraživanje...", + "Type your username to confirm": "Upišite svoje korisničko ime za potvrdu", + "URL": "URL", + "URL where Ollama is running": "URL na kojem se izvodi Ollama", + "UUID copied to clipboard": "UUID kopiran u međuspremnik", + "UUID:": "UUID:", + "Unchanged": "Nepromijenjeno", + "Unique Search Terms": "Jedinstveni pojmovi za pretraživanje", + "Unique Terms": "Jedinstveni pojmovi", + "Unknown": "Nepoznato", + "Unknown Object": "Nepoznati objekt", + "Unknown Register": "Nepoznati registar", + "Unknown Schema": "Nepoznata shema", + "Unknown error": "Nepoznata pogreška", + "Unknown widget type: {type}": "Nepoznata vrsta widgeta: {type}", + "Unlink email": "Odveži e-poštu", + "Unlink event": "Odveži događaj", + "Unnamed Object": "Neimenovani objekt", + "Untitled": "Bez naslova", + "Untitled View": "Prikaz bez naslova", + "Update": "Ažuriraj", + "Update Operations": "Operacije ažuriranja", + "Update available: {local} → {remote}": "Ažuriranje dostupno: {local} → {remote}", + "Update configuration: ...": "Ažuriraj konfiguraciju: ...", + "Update register OAS: ...": "Ažuriraj OAS registra: ...", + "Update vectors when object data changes (recommended for accurate search)": "Ažuriraj vektore kada se podaci objekta promijene (preporučeno za točno pretraživanje)", + "Updated": "Ažurirano", + "Updated At": "Ažurirano u", + "Updated:": "Ažurirano:", + "Upload new avatar": "Prenesi novi avatar", + "Uploading configuration sets...": "Prenošenje skupova konfiguracije...", + "Uptime:": "Vrijeme rada:", + "Url": "Url", + "Usage Count": "Broj korištenja", + "Use AI agents for processing and analysis": "Koristite AI agente za obradu i analizu", + "Use Large Language Model features": "Koristite značajke velikog jezičnog modela", + "Use SolrCloud with Zookeeper for distributed search": "Koristite SolrCloud sa Zookeeperom za distribuirano pretraživanje", + "Use SolrCloud with zookeeper for distributed search": "Koristite SolrCloud sa zookeeperom za distribuirano pretraživanje", + "Use VNG APIs instead of MongoDB.": "Koristite VNG API-je umjesto MongoDB-a.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Koristite zadane raspone (posljednjih 7/30/90 dana, prošla godina, starije)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Koristite vanjsku pohranu (npr. MongoDB) umjesto interne Nextcloud pohrane.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Koristite filtre za sužavanje unosa revizijskog traga prema registru, shemi, vrsti radnje, korisniku, rasponu datuma ili ID-u objekta.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Koristite filtre za sužavanje izbrisanih stavki prema registru, shemi, datumu brisanja ili korisniku koji ih je izbrisao.", + "Use filters to narrow down entities by type or category.": "Koristite filtre za sužavanje entiteta prema vrsti ili kategoriji.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Koristite filtre za sužavanje unosa traga pretraživanja prema registru, shemi, statusu uspjeha, korisniku, rasponu datuma, pojmovima za pretraživanje ili mjernim podacima o izvedbi.", + "Used By Collections": "Korišteno od strane kolekcija", + "Used by": "Korišteno od strane", + "User": "Korisnik", + "User Agent Statistics": "Statistika korisničkog agenta", + "User Description": "Korisnički opis", + "User Quota": "Korisnička kvota", + "User rate limits cleared successfully": "Ograničenja brzine korisnika uspješno su obrisana", + "User removed successfully": "Korisnik je uspješno uklonjen", + "User-friendly description shown in forms and help text": "Korisniku prilagođen opis prikazan u obrascima i tekstu pomoći", + "Username": "Korisničko ime", + "Username for SOLR authentication (optional)": "Korisničko ime za SOLR autentifikaciju (neobavezno)", + "Users": "Korisnici", + "Uses": "Korištenja", + "Using Pre-Generated Chunks": "Korištenje unaprijed generiranih isječaka", + "Validate": "Provjeri valjanost", + "Validate Objects": "Provjeri valjanost objekata", + "Validation completed for {schema}": "Provjera valjanosti dovršena za {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Provjera valjanosti dovršena za {schema}: {processed} obrađeno, {updated} ažurirano, {failed} neuspješno", + "Validation completed successfully": "Provjera valjanosti uspješno je dovršena", + "Validation failed": "Provjera valjanosti nije uspjela", + "Value": "Vrijednost", + "Value list (split on ,)": "Popis vrijednosti (razdvojeno s ,)", + "Vector Dimensions": "Dimenzije vektora", + "Vector Search Backend": "Pozadinski sustav za vektorsko pretraživanje", + "Vector field: _embedding_": "Vektorsko polje: _embedding_", + "Vectorization Triggers": "Okidači vektorizacije", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorizacija dovršena: {vectorized} objekata vektorizirano, {failed} neuspješno", + "Vectorization failed: {error}": "Vektorizacija nije uspjela: {error}", + "Vectorize All Files": "Vektoriziraj sve datoteke", + "Vectorize All Objects": "Vektoriziraj sve objekte", + "Vectorize all file types": "Vektoriziraj sve vrste datoteka", + "Vectorize all views": "Vektoriziraj sve prikaze", + "Vectorize on object creation": "Vektoriziraj pri stvaranju objekta", + "Vectors will be stored in your existing object and file collections": "Vektori će biti pohranjeni u vašim postojećim kolekcijama objekata i datoteka", + "Verantwoording": "Obrazloženje", + "Verifying server connectivity...": "Provjera povezivosti poslužitelja...", + "Version": "Verzija", + "Version Information": "Informacije o verziji", + "Very High": "Vrlo visoko", + "Very high": "Vrlo visoko", + "View": "Prikaz", + "View API Docs": "Pregledaj API dokumentaciju", + "View API Documentation": "Pregledaj API dokumentaciju", + "View APIs": "Pregledaj API-je", + "View Changes": "Pregledaj promjene", + "View Details": "Pregledaj pojedinosti", + "View Error": "Pregledaj pogrešku", + "View File": "Pregledaj datoteku", + "View Full Details": "Pregledaj sve pojedinosti", + "View Logs": "Pregledaj zapisnike", + "View Name": "Naziv prikaza", + "View Object": "Pregledaj objekt", + "View Parameters": "Pregledaj parametre", + "View Selection": "Odabir prikaza", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Pregledajte i analizirajte zapisnike traga pretraživanja s naprednim mogućnostima filtriranja i analitike", + "View and analyze system audit trails with advanced filtering capabilities": "Pregledajte i analizirajte revizijske tragove sustava s naprednim mogućnostima filtriranja", + "View and manage authorization across registers and schemas": "Pregledajte i upravljajte ovlaštenjima u registrima i shemama", + "View deleted successfully": "Prikaz je uspješno izbrisan", + "View entity details and manage relations": "Pregledajte pojedinosti entiteta i upravljajte vezama", + "View name": "Naziv prikaza", + "View name is required": "Naziv prikaza je obavezan", + "View object": "Pregledaj objekt", + "View saved successfully!": "Prikaz je uspješno spremljen!", + "View search analytics and manage search logs": "Pregledajte analitiku pretraživanja i upravljajte zapisnicima pretraživanja", + "View successfully deleted": "Prikaz je uspješno izbrisan", + "View successfully updated": "Prikaz je uspješno ažuriran", + "View updated successfully!": "Prikaz je uspješno ažuriran!", + "View webhook delivery logs and filter by webhook": "Pregledajte zapisnike isporuke webhooka i filtrirajte po webhooku", + "View {count} more": "Pregledaj još {count}", + "Views": "Prikazi", + "Visible to end users": "Vidljivo krajnjim korisnicima", + "Visible to users": "Vidljivo korisnicima", + "Wait for Response": "Čekaj odgovor", + "Wait for webhook response before continuing (required for request/response flows)": "Čekaj odgovor webhooka prije nastavka (obavezno za tokove zahtjev/odgovor)", + "Waiting for cluster synchronization...": "Čekanje sinkronizacije klastera...", + "Warmup Names Cache": "Zagrij predmemoriju naziva", + "Warmup failed": "Zagrijavanje nije uspjelo", + "Warning": "Upozorenje", + "Warning:": "Upozorenje:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Upozorenje: Ovo svojstvo ima nedosljedne vrste: {types}. Razmislite trebaju li svi objekti imati istu vrstu.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Pregledat ćemo sve objekte koji pripadaju ovoj shemi kako bismo otkrili nova svojstva i analizirali postojeća svojstva za potencijalna poboljšanja. Proces uključuje ispitivanje strukture podataka svakog objekta, prepoznavanje svojstava koja nisu definirana u trenutnoj shemi i pronalaženje prilika za poboljšanje postojećih definicija svojstava boljim ograničenjima, formatima i pravilima provjere valjanosti.", + "Webhook": "Webhook", + "Webhook Log Details": "Pojedinosti zapisnika webhooka", + "Webhook Logs": "Zapisnici webhooka", + "Webhook created successfully": "Webhook je uspješno stvoren", + "Webhook deleted": "Webhook izbrisan", + "Webhook retry delivered successfully": "Ponovni pokušaj webhooka uspješno je isporučen", + "Webhook retry delivery failed": "Isporuka ponovnog pokušaja webhooka nije uspjela", + "Webhook updated": "Webhook ažuriran", + "Webhook updated successfully": "Webhook je uspješno ažuriran", + "Webhooks": "Webhookovi", + "Weekly": "Tjedno", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Kada se agent izvodi bez korisničke sesije (npr. zakazani zadaci), koristit će se kontekst ovog korisnika", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Kada se agent izvodi bez korisničke sesije (npr. zakazani zadaci), koristit će se kontekst ovog korisnika.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Kada je onemogućeno, ova će faseta prikazivati samo vrijednosti iz ove sheme i uključivat će filtar sheme kada je odabrana.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Kada je omogućeno, spremanje ovog objekta također će ažurirati ciljne objekte kako bi uključili referencu natrag na ovaj objekt.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Kada je omogućeno, ovo će se svojstvo ukloniti iz izvornog objekta nakon ažuriranja ciljnih objekata.", + "Workflow Management": "Upravljanje tijekom rada", + "Workflows": "Tijekovi rada", + "Wrap webhook payload in CloudEvents format for better interoperability": "Omotaj korisni teret webhooka u CloudEvents format za bolju interoperabilnost", + "Wrap webhook payload in cloudevents format for better interoperability": "Omotaj korisni teret webhooka u cloudevents format za bolju interoperabilnost", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Da", + "You": "Vi", + "You are not yet a member of any organisations.": "Još niste član nijedne organizacije.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Možete stvoriti nove ConfigSetove na temelju _default predloška ili izravno prenijeti prilagođene na svoj SOLR poslužitelj.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Možete stvoriti nove configsetove na temelju _default predloška ili izravno prenijeti prilagođene na svoj SOLR poslužitelj.", + "You do not have access to this conversation": "Nemate pristup ovom razgovoru", + "You do not have permission to delete this conversation": "Nemate ovlasti za brisanje ovog razgovora", + "You do not have permission to modify this conversation": "Nemate ovlasti za izmjenu ovog razgovora", + "You do not have permission to restore this conversation": "Nemate ovlasti za obnovu ovog razgovora", + "You do not have permission to view the permission matrix. Admin access is required.": "Nemate ovlasti za pregled matrice ovlasti. Potreban je administratorski pristup.", + "You have selected {count} properties to add to the schema.": "Odabrali ste {count} svojstava za dodavanje u shemu.", + "You must be logged in to favorite views": "Morate biti prijavljeni da biste prikaze označili kao omiljene", + "You need an AI agent to start a conversation.": "Potreban vam je AI agent za početak razgovora.", + "Your Fireworks AI API key. Get one at": "Vaš Fireworks AI API ključ. Nabavite ga na", + "Your OpenAI API key. Get one at": "Vaš OpenAI API ključ. Nabavite ga na", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Vaše povratne informacije su zabilježene. Po želji ovdje možete navesti dodatne pojedinosti...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Vaše povratne informacije su zabilježene. po želji ovdje možete navesti dodatne pojedinosti...", + "Zookeeper Hosts": "Zookeeper poslužitelji", + "Zookeeper Port": "Zookeeper priključak", + "Zookeeper Settings (SolrCloud)": "Zookeeper postavke (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper niz za povezivanje za SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Broj Zookeeper priključka (neobavezno, zadano 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORENJE O TRAJNOM BRISANJU \\u26A0\\uFE0F\n\nUpravo ćete TRAJNO izbrisati SVE objekte za shemu \"{schema}\":\n\n\\u2022 Aktivni objekti: {active}\n\\u2022 Meko izbrisani objekti: {deleted}\n\\u2022 Ukupno: {total}\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F UPOZORENJE O TRAJNOM BRISANJU \\u26A0\\uFE0F\n\nUpravo ćete TRAJNO izbrisati {count} meko izbrisanih objekata za shemu \"{schema}\".\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "and accessible to all users in selected groups (or all users if no groups selected).": "i dostupno svim korisnicima u odabranim grupama (ili svim korisnicima ako nije odabrana nijedna grupa).", + "and add the files there.": "i ondje dodajte datoteke.", + "approved": "odobreno", + "are only accessible to invited users.": "dostupni su samo pozvanim korisnicima.", + "by": "od strane", + "chunk": "isječak", + "chunks": "isječci", + "chunks processed": "isječaka obrađeno", + "completed": "dovršeno", + "configuration(s)": "konfiguracija", + "conversationId is required": "conversationId je obavezan", + "create": "stvori", + "delete": "izbriši", + "deleted objects section": "odjeljak izbrisanih objekata", + "e.g., lib/Settings/config.json": "npr. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "npr. lib/Settings/register.json", + "e.g., lib/settings/config.json": "npr. lib/settings/config.json", + "e.g., lib/settings/register.json": "npr. lib/settings/register.json", + "execute": "izvrši", + "failed": "neuspješno", + "file": "datoteka", + "files": "datoteke", + "file{plural}": "datotek{plural}", + "folder": "mapa", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "u {register}", + "in_progress": "u tijeku", + "inherited": "naslijeđeno", + "list": "popis", + "log{plural}": "zapisni{plural}", + "manage": "upravljaj", + "members": "članovi", + "menu or contact someone with permission to create agents.": "izborniku ili se obratite nekome tko ima ovlasti za stvaranje agenata.", + "message content is required": "sadržaj poruke je obavezan", + "messages": "poruke", + "missing": "nedostaje", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL i API ključ su obavezni", + "n8n configuration saved successfully": "n8n konfiguracija uspješno je spremljena", + "n8n connection not configured": "n8n veza nije konfigurirana", + "n8n connection successful": "n8n veza uspješna", + "n8n connection test failed: {message}": "n8n test veze nije uspio: {message}", + "n8n connection test successful": "n8n test veze uspješan", + "n8n initialization failed: {message}": "n8n inicijalizacija nije uspjela: {message}", + "n8n integration disabled": "n8n integracija onemogućena", + "n8n integration enabled": "n8n integracija omogućena", + "n8n project initialized successfully": "n8n projekt uspješno je inicijaliziran", + "n8n settings saved successfully": "n8n postavke uspješno su spremljene", + "notify_push configuration guide": "vodič za konfiguraciju notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push je instaliran, ali OpenRegister još nije potvrdio uspješno guranje. Pokrenite spremanje objekta za aktivaciju ili provjerite svoju notify_push konfiguraciju.", + "notify_push is installed but not yet active": "notify_push je instaliran, ali još nije aktivan", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekti", + "objects processed": "objekata obrađeno", + "object{plural}": "objek{plural}", + "or pick a range": "ili odaberite raspon", + "org-...": "org-...", + "pending": "na čekanju", + "provides a good balance between speed and accuracy.": "pruža dobru ravnotežu između brzine i točnosti.", + "public": "javno", + "read": "čitaj", + "register(s)": "registar/registri", + "register(s) selected": "registar/registri odabran", + "register{plural}": "registar{plural}", + "rejected": "odbijeno", + "required": "obavezno", + "results": "rezultati", + "schema(s)": "shema/sheme", + "schema(s) selected": "shema/sheme odabrana", + "schema{plural}": "shem{plural}", + "selected objects": "odabrani objekti", + "set": "postavi", + "sk-...": "sk-...", + "this application": "ova aplikacija", + "to": "do", + "type must be \"positive\" or \"negative\"": "vrsta mora biti \"positive\" ili \"negative\"", + "update": "ažuriraj", + "vectorized": "vektorizirano", + "view": "prikaz", + "views selected": "prikaza odabrano", + "waiting": "čeka se", + "{active} active, {deleted} deleted": "{active} aktivnih, {deleted} izbrisanih", + "{count} audit trails cleared successfully": "{count} revizijskih tragova uspješno obrisano", + "{count} compliance issue(s) detected": "Otkriveno {count} problema usklađenosti", + "{count} deletions": "{count} brisanja", + "{count} documents found": "Pronađeno {count} dokumenata", + "{count} email": "{count} e-pošta", + "{count} emails": "{count} e-pošta", + "{count} entries": "{count} unosa", + "{count} issues": "{count} problema", + "{count} members": "{count} članova", + "{count} searches": "{count} pretraživanja", + "{count} selected": "{count} odabrano", + "{count} widget(s)": "{count} widgeta", + "{matched} object(s) matched. {erased} erased.": "{matched} objekata podudara se. {erased} izbrisano.", + "{minutes} minutes ago": "prije {minutes} minuta", + "{percentage}% of objects": "{percentage}% objekata", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} u {register} / {schema}", + "{type} cache cleared successfully": "{type} predmemorija uspješno obrisana", + "ℹ️ Current Configuration": "ℹ️ Trenutna konfiguracija", + "ℹ️ current configuration": "ℹ️ trenutna konfiguracija", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ OPASNOST: Svih {total} objekata bit će TRAJNO IZBRISANO iz baze podataka. Ova je radnja NEPOVRATNA i ne može se poništiti!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORENJE O TRAJNOM BRISANJU ⚠️\n\nUpravo ćete TRAJNO izbrisati SVE objekte za shemu \"{schema}\":\n\n• Aktivni objekti: {active}\n• Meko izbrisani objekti: {deleted}\n• Ukupno: {total}\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ UPOZORENJE O TRAJNOM BRISANJU ⚠️\n\nUpravo ćete TRAJNO izbrisati {count} meko izbrisanih objekata za shemu \"{schema}\".\n\nTi će objekti biti potpuno uklonjeni iz baze podataka i NE MOGU se obnoviti.\n\nJeste li potpuno sigurni?", + "⚡ Batch Processing": "⚡ Skupna obrada", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR može obraditi milijune dokumenata s vremenom odziva pretraživanja kraćim od sekunde...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Sveobuhvatan kontekst ✓ Manja vjerojatnost izostavljanja pojedinosti ✗ Sporiji odgovori ✗ Može uključivati manje relevantne informacije", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Brži odgovori ✓ Usredotočeniji odgovori ✗ Može izostaviti relevantne informacije", + "✨ AI Features": "✨ AI značajke", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 U distribuiranom načinu rada, SOLR koristi ZooKeeper za koordinaciju klastera...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasetno pretraživanje omogućuje korisnicima dublje istraživanje rezultata po kategorijama...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Jeste li znali? SOLR pokreće pretraživanje za Netflix, Apple i mnoge druge velike stranice!", + "💰 View Selection (Cost Optimization)": "💰 Odabir prikaza (optimizacija troškova)", + "📄 Object Serialization": "📄 Serijalizacija objekta", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR pruža bogatu analitiku i statistiku o izvedbi pretraživanja...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSetovi sadrže shemu i konfiguracijske datoteke za vaš indeks pretraživanja...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikacija osigurava dostupnost vašeg indeksa pretraživanja čak i ako čvorovi zakažu...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR je moćna platforma za pretraživanje na razini poduzeća izgrađena na Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Postavke vektorizacije", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Automatsko skaliranje može dinamički dodavati ili uklanjati čvorove na temelju opterećenja...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Sigurnosne značajke uključuju autentifikaciju, autorizaciju i SSL enkripciju...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-pošta", + "{count} e-pošte" + ], + "Purge {count} object from database": [ + "Trajno ukloni {count} objekt iz baze podataka", + "Trajno ukloni {count} objekata iz baze podataka" + ], + "Restore {count} object": [ + "Vrati {count} objekt", + "Vrati {count} objekata" + ], + "Successfully restored {count} object": [ + "Uspješno vraćen {count} objekt", + "Uspješno vraćeno {count} objekata" + ], + "Delete {count} object": [ + "Izbriši {count} objekt", + "Izbriši {count} objekata" + ], + "Object successfully deleted": [ + "Objekt je uspješno izbrisan", + "Objekti su uspješno izbrisani" + ] + } +} diff --git a/l10n/sv.js b/l10n/sv.js new file mode 100644 index 0000000000..da8cafc341 --- /dev/null +++ b/l10n/sv.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(inget ämne)", + "(no title)" : "(ingen titel)", + "(unnamed)" : "(namnlös)", + "(untitled card)" : "(namnlöst kort)", + "Activities" : "Aktiviteter", + "Add Endpoint" : "Lägg till slutpunkt", + "Add Source" : "Lägg till källa", + "Add card" : "Lägg till kort", + "Add contact" : "Lägg till kontakt", + "All clear" : "Allt klart", + "Articles" : "Artiklar", + "Code" : "Kod", + "Compliance" : "Efterlevnad", + "Contacts" : "Kontakter", + "Create Agent" : "Skapa agent", + "Create Application" : "Skapa applikation", + "Create event" : "Skapa händelse", + "Erasure complete" : "Radering slutförd", + "Erasure preview" : "Förhandsvisning av radering", + "Expand details" : "Expandera detaljer", + "Save changes" : "Spara ändringar", + "📄 Object Serialization" : "📄 Objektserialisering", + "🔢 Vectorization Settings" : "🔢 Vektoriseringsinställningar", + "💰 View Selection (Cost Optimization)" : "💰 Vyval (kostnadsoptimering)", + "AI Assistant" : "AI-assistent", + "AI Chat" : "AI-chatt", + "API Key" : "API-nyckel", + "API tokens saved successfully" : "API-token sparades", + "About ConfigSets" : "Om ConfigSets", + "Action" : "Åtgärd", + "Action Distribution" : "Åtgärdsfördelning", + "Action:" : "Åtgärd:", + "Actions" : "Åtgärder", + "Active" : "Aktiv", + "Active Collections" : "Aktiva samlingar", + "Active Filters" : "Aktiva filter", + "Active Organisations" : "Aktiva organisationer", + "Active filters:" : "Aktiva filter:", + "Active organisation set successfully" : "Aktiv organisation angavs", + "Activity Period" : "Aktivitetsperiod", + "Add Groups" : "Lägg till grupper", + "Add Schema" : "Lägg till Schema", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Lägg till Schema-titlar, beskrivningar och Register-information för att ge rikare sammanhang för sökning", + "Add to favorites" : "Lägg till i favoriter", + "Added" : "Tillagd", + "Added to favorites" : "Tillagd i favoriter", + "Additional Information" : "Ytterligare information", + "Additional feedback saved. Thank you!" : "Ytterligare återkoppling sparades. Tack!", + "Admin" : "Administratör", + "Advanced" : "Avancerat", + "Advanced Filters" : "Avancerade filter", + "Advanced Options" : "Avancerade alternativ", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Avancerade filter är inte tillgängliga när databaskälla används. Växla till Auto eller SOLR-index för filtreringsalternativ.", + "After" : "Efter", + "Agent deleted successfully" : "Agenten togs bort", + "Agents" : "Agenter", + "All" : "Alla", + "All Categories" : "Alla kategorier", + "All Collections" : "Alla samlingar", + "All Confidence Levels" : "Alla tillförlitlighetsnivåer", + "All ConfigSets" : "Alla ConfigSets", + "All Files" : "Alla Filer", + "All Levels" : "Alla nivåer", + "All Types" : "Alla typer", + "All Webhooks" : "Alla webhooks", + "All actions" : "Alla åtgärder", + "All audit trails cleared successfully" : "Alla granskningsloggar rensades", + "All registers" : "Alla register", + "All schemas" : "Alla scheman", + "All searches" : "Alla sökningar", + "All users" : "Alla användare", + "An error occurred while clearing audit trails" : "Ett fel uppstod vid rensning av granskningsloggar", + "An error occurred while deleting the audit trail" : "Ett fel uppstod vid borttagning av granskningsloggen", + "An error occurred while deleting the view" : "Ett fel uppstod vid borttagning av vyn", + "An error occurred while permanently deleting the objects" : "Ett fel uppstod vid permanent borttagning av objekten", + "Analysis completed:" : "Analys slutförd:", + "Analysis steps:" : "Analyssteg:", + "Analytics" : "Analys", + "Analyze Objects" : "Analysera objekt", + "Analyze Properties" : "Analysera egenskaper", + "Analyze existing properties for improvement opportunities" : "Analysera befintliga egenskaper för förbättringsmöjligheter", + "Analyzing..." : "Analyserar...", + "Any favorites and sharing settings for this view" : "Eventuella favoriter och delningsinställningar för denna vy", + "Any user" : "Valfri användare", + "App store cache invalidated successfully" : "App store-cache ogiltigförklarades", + "Application deleted successfully" : "Applikationen togs bort", + "Applications" : "Applikationer", + "Apply Changes" : "Tillämpa ändringar", + "Archive" : "Arkivera", + "Archive conversation" : "Arkivera konversation", + "Archived conversations are hidden from your active list" : "Arkiverade konversationer döljs från din aktiva lista", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Är du säker på att du vill rensa gamla sökloggar? Detta tar bort poster äldre än 30 dagar.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Är du säker på att du vill ta bort de valda granskningsloggarna? Denna åtgärd kan inte ångras.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Är du säker på att du vill ta bort de valda sökloggarna? Denna åtgärd kan inte ångras.", + "Are you sure you want to delete this ConfigSet?" : "Är du säker på att du vill ta bort denna ConfigSet?", + "Are you sure you want to permanently delete" : "Är du säker på att du vill permanent ta bort", + "Ask a question..." : "Ställ en fråga...", + "Ask questions about your data using natural language" : "Ställ frågor om dina data med naturligt språk", + "Attempt" : "Försök", + "Audit Trail Changes" : "Granskningsloggändringar", + "Audit Trail Details" : "Granskningsloggdetaljer", + "Audit Trail Management" : "Hantering av granskningsloggar", + "Audit Trail Statistics" : "Granskningsloggstatistik", + "Audit Trails" : "Granskningsloggar", + "Audit trail data copied to clipboard" : "Granskningsloggdata kopierades till urklipp", + "Audit trail deleted successfully" : "Granskningsloggen togs bort", + "Audit trail successfully deleted" : "Granskningsloggen togs bort", + "Authentication" : "Autentisering", + "Auto-commit disabled" : "Automatisk commit inaktiverad", + "Auto-commit enabled" : "Automatisk commit aktiverad", + "Auto-create Default Organisation" : "Skapa standardorganisation automatiskt", + "Auto-retry failed vectorizations" : "Försök automatiskt igen med misslyckade vektoriseringar", + "Automatically commit changes to SOLR index" : "Genomför ändringar automatiskt till SOLR-index", + "Automatically create a default organisation if none exists when the app is initialized" : "Skapa automatiskt en standardorganisation om ingen finns när appen initieras", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Generera automatiskt vektorinbäddningar från textsegment när Filer laddas upp och bearbetas", + "Automatically generate vector embeddings when objects are created or updated" : "Generera automatiskt vektorinbäddningar när objekt skapas eller uppdateras", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Försök automatiskt igen med misslyckade vektoriseringsförsök (max 3 försök)", + "Available Filters" : "Tillgängliga filter", + "Available Workflows" : "Tillgängliga arbetsflöden", + "Avg Execution Time" : "Genomsnittlig exekveringstid", + "Avg Members/Org" : "Genomsnittligt antal medlemmar/org", + "Avg Object Views/Session" : "Genomsnittligt antal objektvyer/session", + "Avg Response Time" : "Genomsnittlig svarstid", + "Avg Results/Search" : "Genomsnittligt antal resultat/sökning", + "Avg Searches/Session" : "Genomsnittligt antal sökningar/session", + "Back" : "Tillbaka", + "Back to Entities" : "Tillbaka till entiteter", + "Back to Registers" : "Tillbaka till Register", + "Back to Webhooks" : "Tillbaka till webhooks", + "Back to applications" : "Tillbaka till applikationer", + "Back to entities" : "Tillbaka till entiteter", + "Backend updated successfully. Please reload the application." : "Backend uppdaterades. Ladda om applikationen.", + "Base URL (Optional)" : "Bas-URL (valfritt)", + "Basic Information" : "Grundläggande information", + "Batch Size" : "Batchstorlek", + "Batch extraction completed" : "Batchextrahering slutförd", + "Before" : "Före", + "Before object vectorization can work:" : "Innan objektvektorisering kan fungera:", + "Behavior Issue" : "Beteendeproblem", + "Bookmarks" : "Bokmärken", + "Blob storage has been retired. All objects now use magic tables." : "Blob-lagring har tagits ur bruk. Alla objekt använder nu magiska tabeller.", + "Branch" : "Gren", + "Bulk delete operation completed successfully" : "Massborttagningsåtgärden slutfördes", + "Bulk save operation completed successfully" : "Masssparåtgärden slutfördes", + "Business Data" : "Affärsdata", + "Cache cleared successfully" : "Cachen rensades", + "Calculate Sizes" : "Beräkna storlekar", + "Cancel" : "Avbryt", + "Cannot delete: objects are still attached" : "Kan inte ta bort: objekt är fortfarande bifogade", + "Category" : "Kategori", + "Change Type" : "Ändringstyp", + "Changes" : "Ändringar", + "Changes copied to clipboard" : "Ändringar kopierades till urklipp", + "Chat Model" : "Chattmodell", + "Chat Provider" : "Chattleverantör", + "Chat Provider (RAG)" : "Chattleverantör (RAG)", + "Chat Settings" : "Chattinställningar", + "Chat provider connection successful!" : "Anslutning till chattleverantör lyckades!", + "Chat settings" : "Chattinställningar", + "Choose a register" : "Välj ett Register", + "Choose a schema" : "Välj ett Schema", + "Choose how vector similarity calculations are performed for semantic search" : "Välj hur vektorlikhetsberäkningar utförs för semantisk sökning", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Välj vilka filtyper som ska inkluderas i vektoriseringsprocessen. Endast Filer med extraherad text och segment bearbetas.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Välj vilka vyer som ska inkluderas i vektoriseringsprocessen. Lämna tomt för att bearbeta alla vyer baserat på din konfiguration.", + "Chunk" : "Segment", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Segmentborttagning är ännu inte implementerad. Använd segmentbaserade slutpunkter.", + "Chunks" : "Segment", + "Chunks to Vectorize:" : "Segment att vektorisera:", + "Cleanup Old Trails" : "Rensa gamla loggar", + "Cleanup completed" : "Rensning slutförd", + "Clear All Embeddings" : "Rensa alla inbäddningar", + "Clear Entries" : "Rensa poster", + "Clear Filtered Audit Trails" : "Rensa filtrerade granskningsloggar", + "Clear Filters" : "Rensa filter", + "Clear Index" : "Rensa index", + "Clear Selection" : "Rensa urval", + "Clear all" : "Rensa alla", + "Clear all filters" : "Rensa alla filter", + "Clear date range" : "Rensa datumintervall", + "Clearing..." : "Rensar...", + "Close" : "Stäng", + "Collection Management" : "Samlingshantering", + "Collection Name" : "Samlingsnamn", + "Collection assignments updated successfully" : "Samlingstilldelningar uppdaterades", + "Collection cleared successfully" : "Samlingen rensades", + "Collection copied successfully" : "Samlingen kopierades", + "Collection created successfully" : "Samlingen skapades", + "Collection deleted successfully" : "Samlingen togs bort", + "Collection used to store and index file metadata and content" : "Samling som används för att lagra och indexera filmetadata och innehåll", + "Collection used to store and index object data" : "Samling som används för att lagra och indexera objektdata", + "Collections:" : "Samlingar:", + "Commit Message" : "Commit-meddelande", + "Commit Within (ms)" : "Genomför inom (ms)", + "Commit message" : "Commit-meddelande", + "Compare current schema with real object data" : "Jämför aktuellt Schema med verklig objektdata", + "Completed" : "Slutförd", + "Complex" : "Komplex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Komplexa frågor: Avancerade sökningar med flera filter, operatorer och komplexa parameterkombinationer", + "Confidence Level" : "Tillförlitlighetsnivå", + "Confidence Score" : "Tillförlitlighetspoäng", + "Config must be provided as an object" : "Konfiguration måste anges som ett objekt", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Hantering av ConfigSet", + "ConfigSet Name" : "Namn på ConfigSet", + "ConfigSet created successfully" : "ConfigSet skapades", + "ConfigSet deleted successfully" : "ConfigSet togs bort", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets definierar Schemat och konfigurationen för dina SOLR-samlingar. De innehåller fältdefinitioner, analysverktyg och andra sökinställningar.", + "Configuration" : "Konfiguration", + "Configuration Data" : "Konfigurationsdata", + "Configuration saved" : "Konfiguration sparad", + "Configurations" : "Konfigurationer", + "Configure Facets" : "Konfigurera facetter", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Konfigurera leverantörer av stora språkmodeller (LLM) för AI-drivna funktioner inklusive semantisk sökning, inbäddningar och chatt.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Konfigurera grundläggande anslutningsinställningar för din SOLR-server inklusive autentisering och nätverksalternativ. Använd de separata dialogrutorna för ConfigSet och samlingshantering för att hantera kärnor och samlingar.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Konfigurera hur databasobjekt konverteras till vektorinbäddningar för semantisk sökning. Objekt vektoriseras direkt utan att textextrahering behövs.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Konfigurera hur objekt konverteras till text före vektorisering. Dessa inställningar påverkar sökkvalitet och sammanhang.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Konfigurera parametrar för filvektorisering. Denna process genererar vektorinbäddningar för alla extraherade filsegment.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Konfigurera parametrar för objektvektorisering. Denna process genererar vektorinbäddningar för alla objekt som matchar dina vyfilter.", + "Configure which types of data to search and how many sources to retrieve" : "Konfigurera vilka typer av data som ska sökas och hur många källor som ska hämtas", + "Connection Failed" : "Anslutning misslyckades", + "Connection Settings" : "Anslutningsinställningar", + "Connection Successful!" : "Anslutning lyckades!", + "Connection failed" : "Anslutning misslyckades", + "Connection protocol" : "Anslutningsprotokoll", + "Connection successful" : "Anslutning lyckades", + "Connection timeout in seconds" : "Anslutningstimeout i sekunder", + "Constant delay between retries (5 minutes)" : "Konstant fördröjning mellan försök (5 minuter)", + "Constraint Issue" : "Begränsningsproblem", + "Content Filters" : "Innehållsfilter", + "Continue to Properties" : "Fortsätt till egenskaper", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Kontrollera vilka objektvyer som ska vektoriseras för att minska API-kostnader. Vektorisera endast vyer som drar nytta av semantisk sökning.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Kontrollera vilka vyer och verktyg AI:n kan använda i denna konversation. Som standard är alla agentfunktioner aktiverade.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Styr slumpmässighet (0 = deterministisk, 2 = mycket kreativ)", + "Conversation ID is required" : "Konversations-ID krävs", + "Conversation archived" : "Konversation arkiverad", + "Conversation archived successfully" : "Konversationen arkiverades", + "Conversation cleared successfully" : "Konversationen rensades", + "Conversation deleted" : "Konversation borttagen", + "Conversation permanently deleted" : "Konversation permanent borttagen", + "Conversation renamed" : "Konversation omdöpt", + "Conversation restored" : "Konversation återställd", + "Conversation title" : "Konversationstitel", + "Copied!" : "Kopierat!", + "Copy" : "Kopiera", + "Copy Changes" : "Kopiera ändringar", + "Copy Collection" : "Kopiera samling", + "Copy Data" : "Kopiera data", + "Copy Full Data" : "Kopiera fullständig data", + "Copying..." : "Kopierar...", + "Count" : "Antal", + "Counting objects..." : "Räknar objekt...", + "Costs" : "Kostnader", + "Create" : "Skapa", + "Create Collection" : "Skapa samling", + "Create ConfigSet" : "Skapa ConfigSet", + "Create New Collection" : "Skapa ny samling", + "Create New ConfigSet" : "Skapa ny ConfigSet", + "Create Operations" : "Skapa-åtgärder", + "Create Webhook" : "Skapa webhook", + "Create a copy of collection:" : "Skapa en kopia av samling:", + "Create a new ConfigSet based on the _default template" : "Skapa en ny ConfigSet baserad på mallen _default", + "Create a new SOLR collection from an existing ConfigSet" : "Skapa en ny SOLR-samling från en befintlig ConfigSet", + "Create your first AI agent to get started." : "Skapa din första AI-agent för att komma igång.", + "Created" : "Skapad", + "Created:" : "Skapad:", + "Creating..." : "Skapar...", + "Current Filters" : "Aktuella filter", + "Current Type:" : "Aktuell typ:", + "Custom API endpoint if using a different region" : "Anpassad API-slutpunkt om en annan region används", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Anpassade HTTP-rubriker (en per rad, format: Header-Name: value)", + "Daily" : "Dagligen", + "Dashboard" : "Instrumentpanel", + "Data Source" : "Datakälla", + "Data sources" : "Datakällor", + "Data type does not match observed values" : "Datatypen matchar inte observerade värden", + "Database URL" : "Databas-URL", + "Database information refreshed" : "Databasinformation uppdaterad", + "Date Range" : "Datumintervall", + "Default" : "Standard", + "Default Organisation" : "Standardorganisation", + "Default: 5" : "Standard: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Fördröjningar fördubblas vid varje försök (2, 4, 8 minuter...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Fördröjningar ökar linjärt (5, 10, 15 minuter...)", + "Delete" : "Ta bort", + "Delete Application" : "Ta bort applikation", + "Delete Audit Trail" : "Ta bort granskningslogg", + "Delete Collection" : "Ta bort samling", + "Delete ConfigSet" : "Ta bort ConfigSet", + "Delete Objects" : "Ta bort objekt", + "Delete Operations" : "Ta bort-åtgärder", + "Delete View" : "Ta bort vy", + "Delete all objects in this schema" : "Ta bort alla objekt i detta Schema", + "Delete permanently" : "Ta bort permanent", + "Delete this conversation?" : "Ta bort denna konversation?", + "Delete view" : "Ta bort vy", + "Deleted" : "Borttagen", + "Deleted By" : "Borttagen av", + "Deleted Date" : "Borttagningsdatum", + "Deleted Items Management" : "Hantering av borttagna objekt", + "Deleted This Week" : "Borttaget denna vecka", + "Deleted Today" : "Borttaget idag", + "Deleted:" : "Borttagen:", + "Deleting..." : "Tar bort...", + "Deletion Date Range" : "Datumintervall för borttagning", + "Deletion Statistics" : "Borttagningsstatistik", + "Deprecated" : "Föråldrad", + "Description" : "Beskrivning", + "Description:" : "Beskrivning:", + "Detect data types and patterns" : "Identifiera datatyper och mönster", + "Detected At" : "Identifierad vid", + "Detected Format:" : "Identifierat format:", + "Detected Issues:" : "Identifierade problem:", + "Disable" : "Inaktivera", + "Disabled" : "Inaktiverad", + "Discover Files" : "Upptäck Filer", + "Discovered Properties" : "Upptäckta egenskaper", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Vill du permanent ta bort alla filtrerade granskningsloggposter? Denna åtgärd kan inte ångras.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Vill du permanent ta bort denna granskningsloggpost? Denna åtgärd kan inte ångras.", + "Documents" : "Dokument", + "Download API Spec" : "Ladda ner API-specifikation", + "Edit" : "Redigera", + "Edit Register" : "Redigera Register", + "Edit Schema" : "Redigera Schema", + "Edit View" : "Redigera vy", + "Edit Webhook" : "Redigera webhook", + "Edit view" : "Redigera vy", + "Edit view details" : "Redigera vydetaljer", + "Effectiveness" : "Effektivitet", + "Email" : "E-post", + "Emails" : "E-postmeddelanden", + "Embedding Model" : "Inbäddningsmodell", + "Embedding Provider" : "Inbäddningsleverantör", + "Embedding generated successfully" : "Inbäddning genererades", + "Embedding provider connection successful!" : "Anslutning till inbäddningsleverantör lyckades!", + "Enable" : "Aktivera", + "Enable auto-creation" : "Aktivera automatiskt skapande", + "Enable automatic file vectorization" : "Aktivera automatisk filvektorisering", + "Enable automatic object vectorization" : "Aktivera automatisk objektvektorisering", + "Enable automatic synchronization" : "Aktivera automatisk synkronisering", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Aktivera detaljerad loggning för SOLR-åtgärder (rekommenderas för felsökning)", + "Enable faceting" : "Aktivera facettering", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Aktivera eller inaktivera LLM-funktioner. Konfigurera leverantörer och modeller med knappen LLM-konfiguration ovan.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Aktivera eller inaktivera SOLR-sökintegration. Konfigurera anslutningsinställningar med knappen Anslutningsinställningar ovan.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Aktivera eller inaktivera n8n-arbetsflödesintegration. Konfigurera anslutningsinställningar nedan.", + "Enable or disable this webhook" : "Aktivera eller inaktivera denna webhook", + "Enable vectorization for all existing and future views (may increase costs)" : "Aktivera vektorisering för alla befintliga och framtida vyer (kan öka kostnaderna)", + "Enabled" : "Aktiverad", + "Endpoints" : "Slutpunkter", + "Enter ConfigSet name" : "Ange namn på ConfigSet", + "Enter collection name" : "Ange samlingsnamn", + "Enter description (optional)..." : "Ange beskrivning (valfritt)...", + "Enter new collection name" : "Ange nytt samlingsnamn", + "Enter object ID" : "Ange objekt-ID", + "Enter search term" : "Ange sökterm", + "Enter view name..." : "Ange vynamn...", + "Enter webhook name" : "Ange webhook-namn", + "Entities" : "Entiteter", + "Entity Information" : "Entitetsinformation", + "Entity deleted successfully" : "Entiteten togs bort", + "Entity not found" : "Entiteten hittades inte", + "Entries to be deleted:" : "Poster som ska tas bort:", + "Enum Issue" : "Enum-problem", + "Enum constraint is missing" : "Enum-begränsning saknas", + "Error" : "Fel", + "Error Details" : "Feldetaljer", + "Error Information" : "Felinformation", + "Error Message" : "Felmeddelande", + "Error loading application" : "Fel vid inläsning av applikation", + "Error loading audit trails" : "Fel vid inläsning av granskningsloggar", + "Error loading entity" : "Fel vid inläsning av entitet", + "Error loading search trails" : "Fel vid inläsning av sökloggar", + "Estimated Batches:" : "Uppskattade batchar:", + "Estimated Cost:" : "Uppskattad kostnad:", + "Estimated Duration:" : "Uppskattad varaktighet:", + "Event" : "Händelse", + "Event Property for Payload" : "Händelseegenskap för nyttolast", + "Events" : "Händelser", + "Example Value" : "Exempelvärde", + "Exclusive Maximum" : "Exklusivt maximum", + "Exclusive Minimum" : "Exklusivt minimum", + "Execution Mode" : "Exekveringsläge", + "Execution Time" : "Exekveringstid", + "Execution Time Range" : "Exekveringstidsintervall", + "Existing Improvements" : "Befintliga förbättringar", + "Exponential" : "Exponentiell", + "Export" : "Exportera", + "Export completed successfully" : "Exporten slutfördes", + "Export, view, or delete audit trails" : "Exportera, visa eller ta bort granskningsloggar", + "Extend Schema" : "Utöka Schema", + "Extended" : "Utökad", + "Extensions" : "Tillägg", + "Extract Pending Files" : "Extrahera väntande Filer", + "Extract properties from each object" : "Extrahera egenskaper från varje objekt", + "Extracted At" : "Extraherad vid", + "Extraction Status" : "Extraheringsstatus", + "Facet configuration updated successfully" : "Facettkonfigurationen uppdaterades", + "Facets discovered and configured successfully" : "Facetter upptäcktes och konfigurerades", + "Facets discovered successfully" : "Facetter upptäcktes", + "Failed" : "Misslyckades", + "Failed to analyze schema properties" : "Det gick inte att analysera Schema-egenskaper", + "Failed to archive conversation" : "Det gick inte att arkivera konversationen", + "Failed to calculate sizes" : "Det gick inte att beräkna storlekar", + "Failed to clear collection" : "Det gick inte att rensa samlingen", + "Failed to copy changes" : "Det gick inte att kopiera ändringar", + "Failed to copy data" : "Det gick inte att kopiera data", + "Failed to copy data to clipboard" : "Det gick inte att kopiera data till urklipp", + "Failed to create conversation" : "Det gick inte att skapa konversationen", + "Failed to create or find project" : "Det gick inte att skapa eller hitta projektet", + "Failed to delete collection" : "Det gick inte att ta bort samlingen", + "Failed to delete conversation" : "Det gick inte att ta bort konversationen", + "Failed to delete webhook" : "Det gick inte att ta bort webhooken", + "Failed to download API specification" : "Det gick inte att ladda ner API-specifikationen", + "Failed to get SOLR field configuration" : "Det gick inte att hämta SOLR-fältkonfiguration", + "Failed to get entity categories" : "Det gick inte att hämta entitetskategorier", + "Failed to get entity statistics" : "Det gick inte att hämta entitetsstatistik", + "Failed to get entity types" : "Det gick inte att hämta entitetstyper", + "Failed to load LLM configuration" : "Det gick inte att läsa in LLM-konfiguration", + "Failed to load Nextcloud groups" : "Det gick inte att läsa in Nextcloud-grupper", + "Failed to load conversation" : "Det gick inte att läsa in konversationen", + "Failed to load entities" : "Det gick inte att läsa in entiteter", + "Failed to load entity" : "Det gick inte att läsa in entiteten", + "Failed to load files" : "Det gick inte att läsa in Filer", + "Failed to load organisations" : "Det gick inte att läsa in organisationer", + "Failed to load templates" : "Det gick inte att läsa in mallar", + "Failed to load webhooks" : "Det gick inte att läsa in webhooks", + "Failed to load workflows" : "Det gick inte att läsa in arbetsflöden", + "Failed to refresh database information" : "Det gick inte att uppdatera databasinformation", + "Failed to reindex collection" : "Det gick inte att indexera om samlingen", + "Failed to rename conversation" : "Det gick inte att döpa om konversationen", + "Failed to restore conversation" : "Det gick inte att återställa konversationen", + "Failed to retry extraction" : "Det gick inte att försöka extrahera igen", + "Failed to retry webhook" : "Det gick inte att försöka webhooken igen", + "Failed to save GitHub token" : "Det gick inte att spara GitHub-token", + "Failed to save GitLab URL" : "Det gick inte att spara GitLab-URL", + "Failed to save GitLab token" : "Det gick inte att spara GitLab-token", + "Failed to save additional feedback" : "Det gick inte att spara ytterligare återkoppling", + "Failed to save n8n configuration" : "Det gick inte att spara n8n-konfiguration", + "Failed to save roles" : "Det gick inte att spara roller", + "Failed to save settings" : "Det gick inte att spara inställningar", + "Failed to save webhook" : "Det gick inte att spara webhooken", + "Failed to send feedback" : "Det gick inte att skicka återkoppling", + "Failed to test webhook" : "Det gick inte att testa webhooken", + "Failed to update favorite status" : "Det gick inte att uppdatera favoritstatus", + "Failed to update schema properties" : "Det gick inte att uppdatera Schema-egenskaper", + "Failed to update view" : "Det gick inte att uppdatera vyn", + "Failed to update webhook" : "Det gick inte att uppdatera webhooken", + "Feedback recorded" : "Återkoppling registrerad", + "Fewer sources (1-3):" : "Färre källor (1-3):", + "Field" : "Fält", + "File" : "Fil", + "File Chunk Prediction" : "Förutsägelse av filsegment", + "File Collection" : "Filsamling", + "File Management" : "Filhantering", + "File Name" : "Filnamn", + "File Path" : "Filsökväg", + "File Type Selection" : "Val av filtyp", + "File Vectorization" : "Filvektorisering", + "File Warmup" : "Filuppvärmning", + "File actions menu" : "Filåtgärdsmeny", + "File anonymized successfully" : "Filen anonymiserades", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Filsegmentvektorisering är ännu inte implementerad. Segmenten är klara och lagrade, men vektoriseringstjänsten är under utveckling.", + "File collection not configured" : "Filsamling är inte konfigurerad", + "File discovery completed" : "Filupptäckt slutförd", + "File extraction completed" : "Filextrahering slutförd", + "File extraction queued" : "Filextrahering köad", + "File indexed successfully" : "Filen indexerades", + "File is already anonymized" : "Filen är redan anonymiserad", + "File not found" : "Filen hittades inte", + "File settings updated successfully" : "Filinställningar uppdaterades", + "File sources" : "Filkällor", + "File vectorization configuration saved successfully" : "Filvektoriseringskonfigurationen sparades", + "File vectorization started. Check the statistics section for progress." : "Filvektorisering startad. Kontrollera statistikavsnittet för förloppet.", + "File warmup completed" : "Filuppvärmning slutförd", + "Files" : "Filer", + "Files with Completed Extraction:" : "Filer med slutförd extrahering:", + "Files → fileCollection, Objects → objectCollection" : "Filer → fileCollection, objekt → objectCollection", + "Filter Audit Trails" : "Filtrera granskningsloggar", + "Filter Deleted Items" : "Filtrera borttagna objekt", + "Filter Objects" : "Filtrera objekt", + "Filter Properties" : "Filtrera egenskaper", + "Filter Search Trails" : "Filtrera sökloggar", + "Filter Statistics" : "Filtreringsstatistik", + "Filter and analyze search trail entries" : "Filtrera och analysera sökloggposter", + "Filter and manage audit trail entries" : "Filtrera och hantera granskningsloggposter", + "Filter and manage soft deleted items" : "Filtrera och hantera mjukt borttagna objekt", + "Filter by object ID" : "Filtrera efter objekt-ID", + "Filter by search term" : "Filtrera efter sökterm", + "Filter by webhook" : "Filtrera efter webhook", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filterdata laddades automatiskt. Använd filtren nedan för att förfina din sökning.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Filtrera webhook-utlösare efter nyttolastegenskaper (en per rad, format: key: value)", + "Filtered" : "Filtrerad", + "Filters" : "Filter", + "Fireworks AI Chat Settings" : "Fireworks AI-chattinställningar", + "Fireworks AI Embedding Configuration" : "Fireworks AI-inbäddningskonfiguration", + "First" : "Första", + "Fixed" : "Fast", + "For chat and retrieval-augmented generation" : "För chatt och hämtningsförstärkt generering", + "For vector embeddings and semantic search" : "För vektorinbäddningar och semantisk sökning", + "Format" : "Format", + "Format Issue" : "Formatproblem", + "Format constraint is missing" : "Formatbegränsning saknas", + "From Date" : "Från datum", + "From date" : "Från datum", + "Full data copied to clipboard" : "Fullständig data kopierades till urklipp", + "General Issue" : "Allmänt problem", + "Generate recommendations and confidence scores" : "Generera rekommendationer och tillförlitlighetspoäng", + "Generate vectors immediately when new objects are created" : "Generera vektorer omedelbart när nya objekt skapas", + "GitHub token is valid" : "GitHub-token är giltig", + "GitHub token removed successfully" : "GitHub-token togs bort", + "GitHub token saved successfully" : "GitHub-token sparades", + "GitLab URL saved successfully" : "GitLab-URL sparades", + "GitLab token is valid" : "GitLab-token är giltig", + "GitLab token saved successfully" : "GitLab-token sparades", + "HTTP Method" : "HTTP-metod", + "HTTP method used to send webhook requests" : "HTTP-metod som används för att skicka webhook-förfrågningar", + "Headers" : "Rubriker", + "Health" : "Hälsa", + "Heartbeat successful - connection kept alive" : "Heartbeat lyckades - anslutningen hölls vid liv", + "Helpful" : "Användbart", + "Hide Filters" : "Dölj filter", + "Hide in forms" : "Dölj i formulär", + "Hide in list view" : "Dölj i listvy", + "High" : "Hög", + "High Confidence" : "Hög tillförlitlighet", + "Host" : "Värd", + "Hourly" : "Varje timme", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Hur djupt nästlade objektegenskaper ska genomsökas (1-20). Högre värden fångar mer detaljer men ökar vektorstorleken.", + "How often to check for updates (1-168 hours)" : "Hur ofta uppdateringar ska kontrolleras (1-168 timmar)", + "How to handle retries for failed webhook deliveries" : "Hur försök för misslyckade webhook-leveranser ska hanteras", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "IP-hastighetsgränser rensades", + "Identify properties not in the schema" : "Identifiera egenskaper som inte finns i Schemat", + "Immutable" : "Oföränderlig", + "Import" : "Importera", + "Import successful" : "Import lyckades", + "Improved Property" : "Förbättrad egenskap", + "In use" : "Används", + "Inactive" : "Inaktiv", + "Include IDs and names of related objects for better contextual search" : "Inkludera ID:n och namn på relaterade objekt för bättre kontextuell sökning", + "Include related object references" : "Inkludera relaterade objektreferenser", + "Include schema and register metadata" : "Inkludera Schema- och Register-metadata", + "Initialization failed" : "Initiering misslyckades", + "Initialize Project" : "Initiera projekt", + "Inspect Fields" : "Inspektera fält", + "Inspect Index" : "Inspektera index", + "Invalid" : "Ogiltig", + "Invalid batch size. Must be between 1 and 5000" : "Ogiltig batchstorlek. Måste vara mellan 1 och 5000", + "Invalid field name provided" : "Ogiltigt fältnamn angivet", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Ogiltigt maxObjects. Måste vara 0 (alla) eller ett positivt tal", + "Invalid:" : "Ogiltig:", + "Issue" : "Problem", + "Items per page:" : "Objekt per sida:", + "Just now" : "Just nu", + "LLM Configuration" : "LLM-konfiguration", + "LLM actions menu" : "LLM-åtgärdsmeny", + "LLM configuration saved successfully" : "LLM-konfigurationen sparades", + "LLM features disabled" : "LLM-funktioner inaktiverade", + "LLM features enabled" : "LLM-funktioner aktiverade", + "LLM must be enabled with an embedding provider configured" : "LLM måste vara aktiverat med en inbäddningsleverantör konfigurerad", + "LLM settings updated successfully" : "LLM-inställningarna uppdaterades", + "Last" : "Sista", + "Last Triggered" : "Senast utlöst", + "Length Range:" : "Längdintervall:", + "Linear" : "Linjär", + "Load Advanced Filters" : "Läs in avancerade filter", + "Load advanced filters with live data from your search index" : "Läs in avancerade filter med realtidsdata från ditt sökindex", + "Load view" : "Läs in vy", + "Loading ConfigSets..." : "Läser in ConfigSets...", + "Loading advanced filters..." : "Läser in avancerade filter...", + "Loading agents..." : "Läser in agenter...", + "Loading application details..." : "Läser in applikationsdetaljer...", + "Loading applications..." : "Läser in applikationer...", + "Loading archived conversations..." : "Läser in arkiverade konversationer...", + "Loading audit trails..." : "Läser in granskningsloggar...", + "Loading collections..." : "Läser in samlingar...", + "Loading configurations..." : "Läser in konfigurationer...", + "Loading conversation..." : "Läser in konversation...", + "Loading conversations..." : "Läser in konversationer...", + "Loading deleted items..." : "Läser in borttagna objekt...", + "Loading events..." : "Läser in händelser...", + "Loading filter data..." : "Läser in filterdata...", + "Loading groups..." : "Läser in grupper...", + "Loading log details..." : "Läser in loggdetaljer...", + "Loading registers..." : "Läser in Register...", + "Loading schemas..." : "Läser in scheman...", + "Loading search trails..." : "Läser in sökloggar...", + "Loading sources..." : "Läser in källor...", + "Loading statistics..." : "Läser in statistik...", + "Loading users..." : "Läser in användare...", + "Loading views..." : "Läser in vyer...", + "Loading..." : "Läser in...", + "Local" : "Lokal", + "Local Version" : "Lokal version", + "Locked" : "Låst", + "Locked:" : "Låst:", + "Login successful" : "Inloggning lyckades", + "Logs" : "Loggar", + "Low" : "Låg", + "Low Confidence" : "Låg tillförlitlighet", + "Lucene Version:" : "Lucene-version:", + "Magic table synchronized successfully" : "Den magiska tabellen synkroniserades", + "Manage Organisation Roles" : "Hantera organisationsroller", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Hantera SOLR-samlingar (datalager) och tilldela dem för objekt och Filer.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Hantera SOLR ConfigSets (konfigurationsmallar) för dina samlingar.", + "Manage and configure agents for automated tasks" : "Hantera och konfigurera agenter för automatiserade uppgifter", + "Manage and monitor file text extraction status" : "Hantera och övervaka status för filtextextrahering", + "Manage and restore soft deleted items from your registers" : "Hantera och återställ mjukt borttagna objekt från dina Register", + "Manage and view detected entities from files and objects" : "Hantera och visa identifierade entiteter från Filer och objekt", + "Manage document templates and themes" : "Hantera dokumentmallar och teman", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Hantera n8n-arbetsflöden för OpenRegister-automatisering. Arbetsflöden lagras i det konfigurerade projektet.", + "Manage webhooks for event-driven integrations" : "Hantera webhooks för händelsestyrda integrationer", + "Manage your applications and modules" : "Hantera dina applikationer och moduler", + "Manage your data registers and their configurations" : "Hantera dina dataregister och deras konfigurationer", + "Manage your data schemas and their properties" : "Hantera dina datascheman och deras egenskaper", + "Manage your data sources and their configurations" : "Hantera dina datakällor och deras konfigurationer", + "Manage your saved search configurations" : "Hantera dina sparade sökkonfigurationer", + "Manage your system configurations and settings" : "Hantera dina systemkonfigurationer och inställningar", + "Managed" : "Hanterad", + "Max Files (0 = all)" : "Max antal Filer (0 = alla)", + "Max Length" : "Maxlängd", + "Max Length:" : "Maxlängd:", + "Max Objects (0 = all)" : "Max antal objekt (0 = alla)", + "Max Retries" : "Max antal försök", + "Max Shards/Node" : "Max antal shards/nod", + "Max execution time (ms)" : "Max exekveringstid (ms)", + "Max ms" : "Max ms", + "Max result count" : "Max antal resultat", + "Max results" : "Max antal resultat", + "Maximum" : "Maximum", + "Maximum Nesting Depth" : "Maximalt nästlingsdjup", + "Maximum length constraint is missing" : "Begränsning för maxlängd saknas", + "Maximum length is too restrictive" : "Maxlängd är för restriktiv", + "Maximum number of files to process. Set to 0 to process all files." : "Maximalt antal Filer att bearbeta. Ange 0 för att bearbeta alla Filer.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Maximalt antal objekt att bearbeta. Ange 0 för att bearbeta alla objekt.", + "Maximum number of retry attempts for failed deliveries" : "Maximalt antal omförsök för misslyckade leveranser", + "Maximum time to wait before committing changes" : "Maximal tid att vänta innan ändringar genomförs", + "Maximum tokens to generate" : "Maximalt antal token att generera", + "Maximum value constraint is missing" : "Begränsning för maxvärde saknas", + "Maximum value is too restrictive" : "Maxvärde är för restriktivt", + "Medium" : "Medel", + "Medium Confidence" : "Medeltillförlitlighet", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Medelfrågor: Sökningar med viss filtrering eller flera parametrar (t.ex. datumintervall, specifika register/scheman)", + "Members of selected groups can access this view" : "Medlemmar i valda grupper kan komma åt denna vy", + "Members:" : "Medlemmar:", + "Memory prediction calculated successfully" : "Minnesförutsägelse beräknades", + "Message content is required" : "Meddelandeinnehåll krävs", + "Message does not belong to this conversation" : "Meddelandet tillhör inte denna konversation", + "Metadata Filters" : "Metadatafilter", + "Method" : "Metod", + "Min Length" : "Minlängd", + "Min execution time (ms)" : "Min exekveringstid (ms)", + "Min ms" : "Min ms", + "Min result count" : "Min antal resultat", + "Min results" : "Min antal resultat", + "Minimum" : "Minimum", + "Minimum value constraint is missing" : "Begränsning för minvärde saknas", + "Minimum value is too restrictive" : "Minvärde är för restriktivt", + "Missing object fields created successfully" : "Saknade objektfält skapades", + "Mode:" : "Läge:", + "Model" : "Modell", + "Model Name" : "Modellnamn", + "Modified" : "Ändrad", + "Monthly" : "Månadsvis", + "More sources (10-20):" : "Fler källor (10-20):", + "Most Active Objects" : "Mest aktiva objekt", + "Multiple Of" : "Multipel av", + "Name" : "Namn", + "Names cache warmup completed" : "Uppvärmning av namncache slutförd", + "New Collection Name" : "Nytt samlingsnamn", + "New Conversation" : "Ny konversation", + "New Properties" : "Nya egenskaper", + "New Property" : "Ny egenskap", + "New Value" : "Nytt värde", + "New users without specific organisation membership will be automatically added to this organisation" : "Nya användare utan specifikt organisationsmedlemskap läggs automatiskt till i denna organisation", + "Next" : "Nästa", + "No" : "Nej", + "No ConfigSets found" : "Inga ConfigSets hittades", + "No GitHub token configured" : "Ingen GitHub-token konfigurerad", + "No GitHub token provided" : "Ingen GitHub-token angiven", + "No GitLab token provided" : "Ingen GitLab-token angiven", + "No Organisation" : "Ingen organisation", + "No active filters" : "Inga aktiva filter", + "No activity data available" : "Ingen aktivitetsdata tillgänglig", + "No agents available" : "Inga agenter tillgängliga", + "No agents found" : "Inga agenter hittades", + "No app store cache exists yet" : "Ingen app store-cache finns ännu", + "No applications are available." : "Inga applikationer är tillgängliga.", + "No applications found" : "Inga applikationer hittades", + "No archived conversations" : "Inga arkiverade konversationer", + "No audit trail entries found" : "Inga granskningsloggposter hittades", + "No changes recorded" : "Inga ändringar registrerade", + "No chunks to vectorize" : "Inga segment att vektorisera", + "No collections found" : "Inga samlingar hittades", + "No configuration data" : "Ingen konfigurationsdata", + "No configurations are available." : "Inga konfigurationer är tillgängliga.", + "No configurations found" : "Inga konfigurationer hittades", + "No configurations found for this application." : "Inga konfigurationer hittades för denna applikation.", + "No conversations yet. Create a new one to get started!" : "Inga konversationer ännu. Skapa en ny för att komma igång!", + "No data available" : "Ingen data tillgänglig", + "No data available for chart" : "Ingen data tillgänglig för diagram", + "No deleted items found" : "Inga borttagna objekt hittades", + "No deletion data available" : "Ingen borttagningsdata tillgänglig", + "No description found" : "Ingen beskrivning hittades", + "No description provided" : "Ingen beskrivning angiven", + "No entities detected in this file. Run text extraction first." : "Inga entiteter identifierades i denna Fil. Kör textextrahering först.", + "No entities found" : "Inga entiteter hittades", + "No entities have been detected yet" : "Inga entiteter har identifierats ännu", + "No events found" : "Inga händelser hittades", + "No expired audit trails found to clear" : "Inga utgångna granskningsloggar hittades att rensa", + "No facetable fields available. Select a register and schema to see available filters." : "Inga facetterbara fält tillgängliga. Välj ett Register och Schema för att se tillgängliga filter.", + "No files found" : "Inga Filer hittades", + "No files have been extracted yet" : "Inga Filer har extraherats ännu", + "No files to process" : "Inga Filer att bearbeta", + "No files to reindex" : "Inga Filer att indexera om", + "No filters are currently active. This will delete ALL audit trail entries!" : "Inga filter är aktiva för närvarande. Detta tar bort ALLA granskningsloggposter!", + "No log entries found" : "Inga loggposter hittades", + "No logs are available for this configuration." : "Inga loggar är tillgängliga för denna konfiguration.", + "No logs are available for this source." : "Inga loggar är tillgängliga för denna Källa.", + "No logs found" : "Inga loggar hittades", + "No mismatched fields found - SOLR schema is properly configured" : "Inga felmatchade fält hittades - SOLR-Schemat är korrekt konfigurerat", + "No objects found" : "Inga objekt hittades", + "No objects found to vectorize" : "Inga objekt hittades att vektorisera", + "No objects selected for deletion" : "Inga objekt valda för borttagning", + "No objects to delete" : "Inga objekt att ta bort", + "No objects were permanently deleted" : "Inga objekt togs bort permanent", + "No properties found" : "Inga egenskaper hittades", + "No properties match your filters." : "Inga egenskaper matchar dina filter.", + "No purge date set" : "Inget rensningsdatum angivet", + "No register data available" : "Ingen Register-data tillgänglig", + "No registers found" : "Inga Register hittades", + "No registers found for this application." : "Inga Register hittades för denna applikation.", + "No relations found" : "Inga relationer hittades", + "No request body available" : "Ingen förfrågningskropp tillgänglig", + "No response body available" : "Ingen svarskropp tillgänglig", + "No saved views yet. Create one in the Search tab!" : "Inga sparade vyer ännu. Skapa en i fliken Sök!", + "No schema data available" : "Ingen Schema-data tillgänglig", + "No schema selected for exploration" : "Inget Schema valt för utforskning", + "No schemas found" : "Inga scheman hittades", + "No schemas found for this application." : "Inga scheman hittades för denna applikation.", + "No search terms data available" : "Ingen söktermdata tillgänglig", + "No search trail entries found" : "Inga sökloggposter hittades", + "No sources are available." : "Inga källor är tillgängliga.", + "No sources found" : "Inga källor hittades", + "No templates found" : "Inga mallar hittades", + "No templates have been created yet" : "Inga mallar har skapats ännu", + "No valid magic-mapped register+schema combinations found" : "Inga giltiga magiskt mappade Register+Schema-kombinationer hittades", + "No views found. Create views first before configuring vectorization." : "Inga vyer hittades. Skapa vyer först innan vektorisering konfigureras.", + "No views match your search" : "Inga vyer matchar din sökning", + "No webhooks found" : "Inga webhooks hittades", + "No webhooks have been configured yet" : "Inga webhooks har konfigurerats ännu", + "No workflows found in this project. Create workflows in the n8n editor." : "Inga arbetsflöden hittades i detta projekt. Skapa arbetsflöden i n8n-redigeraren.", + "None" : "Ingen", + "Not helpful" : "Inte användbart", + "Not in use" : "Används inte", + "Number Constraints" : "Talbegränsningar", + "Number of chunks to vectorize in one API call (1-100)." : "Antal segment att vektorisera i ett API-anrop (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Antal segment att vektorisera i ett API-anrop. Högre = snabbare men mer minne. Rekommenderas: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Antal objekt att bearbeta i varje batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Antal objekt att vektorisera i ett API-anrop. Högre = snabbare men mer minne. Rekommenderas: 10-50.", + "Numeric Range:" : "Numeriskt intervall:", + "Object" : "Objekt", + "Object Collection" : "Objektsamling", + "Object Count Prediction" : "Förutsägelse av objektantal", + "Object ID" : "Objekt-ID", + "Object Management" : "Objekthantering", + "Object Vectorization" : "Objektvektorisering", + "Object Warmup" : "Objektuppvärmning", + "Object collection not configured" : "Objektsamling är inte konfigurerad", + "Object not found" : "Objektet hittades inte", + "Object permanently deleted" : "Objektet togs bort permanent", + "Object restored successfully" : "Objektet återställdes", + "Object settings updated successfully" : "Objektinställningar uppdaterades", + "Object sources" : "Objektkällor", + "Object unlocked successfully" : "Objektet låstes upp", + "Object vectorization configuration saved successfully" : "Objektvektoriseringskonfigurationen sparades", + "Object vectorized successfully" : "Objektet vektoriserades", + "Object:" : "Objekt:", + "Objects" : "Objekt", + "Objects Analyzed" : "Analyserade objekt", + "Objects Distribution" : "Objektfördelning", + "Objects being analyzed" : "Objekt som analyseras", + "Objects by Register" : "Objekt per Register", + "Objects by Schema" : "Objekt per Schema", + "Objects deletion completed successfully" : "Objektborttagningen slutfördes", + "Objects in schema" : "Objekt i Schema", + "Objects to Process:" : "Objekt att bearbeta:", + "Objects to be analyzed" : "Objekt som ska analyseras", + "Objects to be deleted" : "Objekt som ska tas bort", + "Objects to be validated" : "Objekt som ska valideras", + "Objects will be serialized as JSON text before vectorization" : "Objekt serialiseras som JSON-text före vektorisering", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Objekt mjukraderas (markeras som borttagna men behålls i databasen). De kan återställas senare vid behov.", + "Old Value" : "Gammalt värde", + "Oldest Item (days)" : "Äldsta objekt (dagar)", + "Ollama Chat Settings" : "Ollama-chattinställningar", + "Ollama Embedding Configuration" : "Ollama-inbäddningskonfiguration", + "Ollama URL" : "Ollama-URL", + "Only With Changes" : "Endast med ändringar", + "Open Register Objects" : "Open Register-objekt", + "Open n8n Editor" : "Öppna n8n-redigeraren", + "OpenAI Chat Settings" : "OpenAI-chattinställningar", + "OpenAI Embedding Configuration" : "OpenAI-inbäddningskonfiguration", + "OpenRegister Settings" : "OpenRegister-inställningar", + "Optional webhook secret for signature verification" : "Valfri webhook-hemlighet för signaturverifiering", + "Order" : "Ordning", + "Organisation" : "Organisation", + "Organisation ID" : "Organisations-ID", + "Organisation Members" : "Organisationsmedlemmar", + "Organisation Statistics" : "Organisationsstatistik", + "Organisation created successfully" : "Organisationen skapades", + "Organisation settings saved successfully" : "Organisationsinställningar sparades", + "Organisations" : "Organisationer", + "Organization" : "Organisation", + "Organization ID (Optional)" : "Organisations-ID (valfritt)", + "Orphaned Items" : "Föräldralösa objekt", + "Owner" : "Ägare", + "Owner:" : "Ägare:", + "Parallel Mode (Faster, more resource intensive)" : "Parallellt läge (snabbare, mer resurskrävande)", + "Parallel:" : "Parallellt:", + "Password" : "Lösenord", + "Password for SOLR authentication (optional)" : "Lösenord för SOLR-autentisering (valfritt)", + "Path" : "Sökväg", + "Path in repository" : "Sökväg i arkivet", + "Path where the configuration file will be saved in the repository" : "Sökväg där konfigurationsFilen sparas i arkivet", + "Path where the register OAS file will be saved in the repository" : "Sökväg där Register-OAS-Filen sparas i arkivet", + "Pattern (regex)" : "Mönster (regex)", + "Pattern Issue" : "Mönsterproblem", + "Pattern constraint is missing" : "Mönsterbegränsning saknas", + "Patterns:" : "Mönster:", + "Pending" : "Väntar", + "Permanent Delete Mode" : "Permanent borttagningsläge", + "Permanently Delete" : "Ta bort permanent", + "Permanently delete already soft-deleted objects" : "Ta bort redan mjukraderade objekt permanent", + "Permanently delete this conversation?" : "Ta bort denna konversation permanent?", + "Person" : "Person", + "Personal Data" : "Personuppgifter", + "Phone" : "Telefon", + "Photos" : "Foton", + "Please create an agent in the" : "Skapa en agent i", + "Please select an agent to continue" : "Välj en agent för att fortsätta", + "Please select which register and schema to use for the new object" : "Välj vilket Register och Schema som ska användas för det nya objektet", + "Please try again later." : "Försök igen senare.", + "Please wait while we fetch your agents." : "Vänta medan vi hämtar dina agenter.", + "Please wait while we fetch your applications." : "Vänta medan vi hämtar dina applikationer.", + "Please wait while we fetch your configurations." : "Vänta medan vi hämtar dina konfigurationer.", + "Please wait while we fetch your deleted items." : "Vänta medan vi hämtar dina borttagna objekt.", + "Please wait while we fetch your sources." : "Vänta medan vi hämtar dina källor.", + "Polls" : "Omröstningar", + "Popular Search Terms" : "Populära söktermer", + "Port" : "Port", + "Prerequisites" : "Förutsättningar", + "Press Enter to send, Shift+Enter for new line" : "Tryck på Enter för att skicka, Shift+Enter för ny rad", + "Previous" : "Föregående", + "Private" : "Privat", + "Processes chunks in batches with simulated parallelism." : "Bearbetar segment i batchar med simulerad parallellism.", + "Processes file chunks sequentially (safest)." : "Bearbetar filsegment sekventiellt (säkrast).", + "Processes objects in chunks with simulated parallelism." : "Bearbetar objekt i segment med simulerad parallellism.", + "Processes objects sequentially (safest)." : "Bearbetar objekt sekventiellt (säkrast).", + "Processing" : "Bearbetar", + "Processing Limits" : "Bearbetningsgränser", + "Processing..." : "Bearbetar...", + "Project initialized successfully" : "Projektet initierades", + "Project not found. Please initialize first." : "Projektet hittades inte. Initiera först.", + "Properties" : "Egenskaper", + "Property Behaviors" : "Egenskapsbeteenden", + "Property Configuration:" : "Egenskapskonfiguration:", + "Property Filters" : "Egenskapsfilter", + "Property Title" : "Egenskapstitel", + "Property Type" : "Egenskapstyp", + "Property can be improved" : "Egenskapen kan förbättras", + "Protected" : "Skyddad", + "Provider" : "Leverantör", + "Provider is required for testing" : "Leverantör krävs för testning", + "Public" : "Offentlig", + "Public View" : "Offentlig vy", + "Public views can be accessed by anyone in the system" : "Offentliga vyer kan kommas åt av vem som helst i systemet", + "Publish" : "Publicera", + "Publish Objects" : "Publicera objekt", + "Publish all objects in this schema" : "Publicera alla objekt i detta Schema", + "Published" : "Publicerad", + "Published:" : "Publicerad:", + "Publishing..." : "Publicerar...", + "Purge" : "Rensa", + "Purge Date" : "Rensningsdatum", + "Query Complexity Distribution" : "Fördelning av frågekomplexitet", + "Query parameter is required" : "Frågeparameter krävs", + "RAG" : "RAG", + "RAG Configuration" : "RAG-konfiguration", + "Rate limits cleared successfully" : "Hastighetsgränser rensades", + "Raw Changes Data" : "Rådata för ändringar", + "Re-vectorize on object update" : "Vektorisera om vid objektuppdatering", + "Read" : "Läs", + "Recommendations:" : "Rekommendationer:", + "Recommended Type:" : "Rekommenderad typ:", + "Recommended: 5 sources" : "Rekommenderas: 5 källor", + "Refresh" : "Uppdatera", + "Refresh Data" : "Uppdatera data", + "Refresh Stats" : "Uppdatera statistik", + "Refresh Workflows" : "Uppdatera arbetsflöden", + "Refresh dashboard" : "Uppdatera instrumentpanel", + "Refresh database info" : "Uppdatera databasinformation", + "Register" : "Register", + "Register ID" : "Register-ID", + "Register Statistics" : "Register-statistik", + "Register Totals" : "Register-totaler", + "Register depublished successfully" : "Register avpublicerades", + "Register not found" : "Register hittades inte", + "Register objects deletion completed successfully" : "Borttagning av Register-objekt slutfördes", + "Register published successfully" : "Register publicerades", + "Register/Schema Usage" : "Register/Schema-användning", + "Registers" : "Register", + "Reindex" : "Indexera om", + "Reindex completed" : "Omindexering slutförd", + "Reindex completed successfully" : "Omindexeringen slutfördes", + "Reindex failed" : "Omindexering misslyckades", + "Relations" : "Relationer", + "Remote Version" : "Fjärrversion", + "Remove" : "Ta bort", + "Remove filter" : "Ta bort filter", + "Remove from favorites" : "Ta bort från favoriter", + "Remove group" : "Ta bort grupp", + "Removed" : "Borttagen", + "Removed from favorites" : "Borttagen från favoriter", + "Rename Conversation" : "Döp om konversation", + "Rename conversation" : "Döp om konversation", + "Replicas" : "Repliker", + "Repository" : "Arkiv", + "Request Body" : "Förfrågningskropp", + "Request Data" : "Förfrågningsdata", + "Request Details" : "Förfrågningsdetaljer", + "Request body must be JSON with a \"query\" field" : "Förfrågningskroppen måste vara JSON med ett \"query\"-fält", + "Request timeout in seconds" : "Förfrågningstimeout i sekunder", + "Required field" : "Obligatoriskt fält", + "Required status is inconsistent" : "Obligatorisk status är inkonsekvent", + "Rerun Search" : "Kör om sökning", + "Reset Changes" : "Återställ ändringar", + "Reset Filters" : "Återställ filter", + "Response Body" : "Svarskropp", + "Response Details" : "Svarsdetaljer", + "Response Time:" : "Svarstid:", + "Restore" : "Återställ", + "Restore conversation" : "Återställ konversation", + "Restore or permanently delete items" : "Återställ eller ta bort objekt permanent", + "Result Count Range" : "Intervall för resultatantal", + "Results" : "Resultat", + "Retrieve all objects for this schema" : "Hämta alla objekt för detta Schema", + "Retry" : "Försök igen", + "Retry Failed Extractions" : "Försök misslyckade extraheringar igen", + "Retry Policy" : "Omförsöksprincip", + "Retry completed" : "Omförsök slutfört", + "Retrying..." : "Försöker igen...", + "Risk Level" : "Risknivå", + "Roles updated successfully" : "Roller uppdaterades", + "SOLR Connection Settings" : "SOLR-anslutningsinställningar", + "SOLR Version:" : "SOLR-version:", + "SOLR actions menu" : "SOLR-åtgärdsmeny", + "SOLR base path (usually /solr)" : "SOLR-bassökväg (vanligtvis /solr)", + "SOLR is disabled" : "SOLR är inaktiverat", + "SOLR is not available or not configured" : "SOLR är inte tillgängligt eller inte konfigurerat", + "SOLR logging disabled" : "SOLR-loggning inaktiverad", + "SOLR logging enabled" : "SOLR-loggning aktiverad", + "SOLR search disabled" : "SOLR-sökning inaktiverad", + "SOLR search enabled" : "SOLR-sökning aktiverad", + "SOLR server hostname or IP address" : "SOLR-serverns värdnamn eller IP-adress", + "SOLR server port number (optional, defaults to 8983)" : "SOLR-serverns portnummer (valfritt, standard är 8983)", + "SOLR setup completed successfully" : "SOLR-konfigurationen slutfördes", + "SOLR setup failed" : "SOLR-konfiguration misslyckades", + "SOLR setup failed - check logs" : "SOLR-konfiguration misslyckades - kontrollera loggar", + "Sample Values:" : "Exempelvärden:", + "Save" : "Spara", + "Save Configuration" : "Spara konfiguration", + "Save Connection Settings" : "Spara anslutningsinställningar", + "Save Roles" : "Spara roller", + "Save Settings" : "Spara inställningar", + "Save current search as view" : "Spara aktuell sökning som vy", + "Saved Views" : "Sparade vyer", + "Saving..." : "Sparar...", + "Schema" : "Schema", + "Schema ID" : "Schema-ID", + "Schema Name" : "Schema-namn", + "Schema Statistics" : "Schema-statistik", + "Schema depublished successfully" : "Schema avpublicerades", + "Schema objects deletion completed successfully" : "Borttagning av Schema-objekt slutfördes", + "Schema published successfully" : "Schema publicerades", + "Schema successfully created" : "Schema skapades", + "Schema successfully updated" : "Schema uppdaterades", + "Schemas" : "Scheman", + "Scheme" : "Schema", + "Search / Views" : "Sök / Vyer", + "Search Activity" : "Sökaktivitet", + "Search Analytics" : "Sökanalys", + "Search Entities" : "Sök entiteter", + "Search Files" : "Sök Filer", + "Search GitHub" : "Sök GitHub", + "Search GitLab" : "Sök GitLab", + "Search Method" : "Sökmetod", + "Search Term" : "Sökterm", + "Search Trail Details" : "Sökloggdetaljer", + "Search Trail Management" : "Hantering av sökloggar", + "Search Trail Statistics" : "Sökloggstatistik", + "Search Trails" : "Sökloggar", + "Search Views" : "Sök vyer", + "Search Webhooks" : "Sök webhooks", + "Search by file name or path" : "Sök efter filnamn eller sökväg", + "Search by name or URL" : "Sök efter namn eller URL", + "Search by value" : "Sök efter värde", + "Search in Files" : "Sök i Filer", + "Search in Objects" : "Sök i objekt", + "Search property names..." : "Sök egenskapsnamn...", + "Search trail deletion not implemented yet" : "Borttagning av sökloggar är inte implementerad ännu", + "Search views..." : "Sök vyer...", + "Secret" : "Hemlighet", + "Secret key for HMAC signature generation (optional)" : "Hemlig nyckel för HMAC-signaturgenerering (valfritt)", + "Select AI Agent" : "Välj AI-agent", + "Select All" : "Välj alla", + "Select ConfigSet" : "Välj ConfigSet", + "Select File Types to Vectorize:" : "Välj filtyper att vektorisera:", + "Select HTTP method" : "Välj HTTP-metod", + "Select Register and Schema" : "Välj Register och Schema", + "Select Views to Vectorize:" : "Välj vyer att vektorisera:", + "Select a Nextcloud group to add" : "Välj en Nextcloud-grupp att lägga till", + "Select a branch" : "Välj en gren", + "Select a model or type a custom model name" : "Välj en modell eller skriv ett anpassat modellnamn", + "Select a repository" : "Välj ett arkiv", + "Select a repository you have write access to" : "Välj ett arkiv du har skrivåtkomst till", + "Select an AI Agent" : "Välj en AI-agent", + "Select an AI agent to begin chatting with your data." : "Välj en AI-agent för att börja chatta med dina data.", + "Select backend" : "Välj backend", + "Select chat model" : "Välj chattmodell", + "Select collection for files" : "Välj samling för Filer", + "Select collection for objects" : "Välj samling för objekt", + "Select default organisation" : "Välj standardorganisation", + "Select event to listen to..." : "Välj händelse att lyssna på...", + "Select model" : "Välj modell", + "Select options..." : "Välj alternativ...", + "Select period" : "Välj period", + "Select property to send as payload" : "Välj egenskap att skicka som nyttolast", + "Select provider" : "Välj leverantör", + "Select registers and schemas to save a view" : "Välj Register och scheman för att spara en vy", + "Select retry policy" : "Välj omförsöksprincip", + "Select the branch to publish to" : "Välj gren att publicera till", + "Select the event this webhook should listen to" : "Välj händelsen denna webhook ska lyssna på", + "Select views to vectorize:" : "Välj vyer att vektorisera:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Välj vilka Nextcloud-grupper som är tillgängliga för denna organisation. Användare i dessa grupper får åtkomst till organisationsresurser.", + "Select which data views the AI can search" : "Välj vilka datavyer AI:n kan söka i", + "Select which property from the event should be used as the webhook payload data" : "Välj vilken egenskap från händelsen som ska användas som webhook-nyttolastdata", + "Select which tools the AI can use to perform actions" : "Välj vilka verktyg AI:n kan använda för att utföra åtgärder", + "Selected" : "Vald", + "Selected Groups" : "Valda grupper", + "Selected audit trails deleted successfully" : "Valda granskningsloggar togs bort", + "Selected search trails deleted successfully" : "Valda sökloggar togs bort", + "Selected users can access this view" : "Valda användare kan komma åt denna vy", + "Send additional feedback" : "Skicka ytterligare återkoppling", + "Send as CloudEvent" : "Skicka som CloudEvent", + "Sensitive PII" : "Känsliga personuppgifter", + "Serial Mode (Safer, slower)" : "Sekventiellt läge (säkrare, långsammare)", + "Serial:" : "Sekventiellt:", + "Server Configuration" : "Serverkonfiguration", + "Server Information" : "Serverinformation", + "Settings" : "Inställningar", + "Shards" : "Shards", + "Share" : "Dela", + "Share with Groups" : "Dela med grupper", + "Share with Users" : "Dela med användare", + "Show Filters" : "Visa filter", + "Show less" : "Visa mindre", + "Show only entries with changes" : "Visa endast poster med ändringar", + "Simple" : "Enkel", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Enkla frågor: Grundläggande textsökningar med minimala parametrar (t.ex. en enda sökterm, inga avancerade filter)", + "Size" : "Storlek", + "Slug" : "Slug", + "Soft Delete Mode" : "Mjukraderingsläge", + "Soft Deleted Items" : "Mjukraderade objekt", + "SolrCloud mode enabled" : "SolrCloud-läge aktiverat", + "Source" : "Källa", + "Sources" : "Källor", + "Sources:" : "Källor:", + "Standalone SOLR mode" : "Fristående SOLR-läge", + "Start Conversation" : "Starta konversation", + "Start Vectorization" : "Starta vektorisering", + "Start a conversation" : "Starta en konversation", + "Starting names cache warmup..." : "Startar uppvärmning av namncache...", + "Starting..." : "Startar...", + "Statistics" : "Statistik", + "Stats calculation not yet implemented" : "Statistikberäkning är inte implementerad ännu", + "Status" : "Status", + "Status Code" : "Statuskod", + "String Constraints" : "Strängbegränsningar", + "Success" : "Lyckades", + "Success Rate" : "Andel lyckade", + "Success Status" : "Lyckandestatus", + "Successful" : "Lyckad", + "Successfully joined organisation" : "Anslöt till organisationen", + "Sync Table" : "Synkronisera tabell", + "Synchronization Settings" : "Synkroniseringsinställningar", + "System Default" : "Systemstandard", + "Technical Description" : "Teknisk beskrivning", + "Technical description for developers and administrators" : "Teknisk beskrivning för utvecklare och administratörer", + "Temperature" : "Temperatur", + "Templates" : "Mallar", + "Test" : "Testa", + "Test Chat" : "Testa chatt", + "Test Connection" : "Testa anslutning", + "Test Embedding" : "Testa inbäddning", + "Test endpoint executed successfully" : "Testslutpunkten exekverades", + "Test webhook delivered successfully" : "Test-webhooken levererades", + "Test webhook delivery failed" : "Leverans av test-webhook misslyckades", + "Test webhook sent successfully" : "Test-webhooken skickades", + "Testing..." : "Testar...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Textsegment genereras under filextrahering och lagras i databasen. Vektorisering läser dessa förseg menterade Filer och konverterar dem till inbäddningar.", + "Text extracted successfully" : "Text extraherades", + "Text extraction disabled" : "Textextrahering inaktiverad", + "The URL where webhook events will be sent" : "URL:en dit webhook-händelser skickas", + "The requested conversation does not exist" : "Den begärda konversationen finns inte", + "The saved view and all its search configuration" : "Den sparade vyn och hela dess sökkonfiguration", + "There are no audit trail entries matching your current filters." : "Det finns inga granskningsloggposter som matchar dina aktuella filter.", + "There are no deleted items matching your current filters." : "Det finns inga borttagna objekt som matchar dina aktuella filter.", + "There are no search trail entries matching your current filters." : "Det finns inga sökloggposter som matchar dina aktuella filter.", + "There are no webhook log entries matching your filters." : "Det finns inga webhook-loggposter som matchar dina filter.", + "This action cannot be undone." : "Denna åtgärd kan inte ångras.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Denna åtgärd kan inte ångras. Se till att inga samlingar använder denna ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Denna agent har inga konfigurerbara vyer eller verktyg, men du kan fortfarande justera RAG-inställningar nedan.", + "This analysis may take some time" : "Denna analys kan ta en stund", + "This audit trail entry does not contain any change information." : "Denna granskningsloggpost innehåller ingen ändringsinformation.", + "This configuration has no data defined." : "Denna konfiguration har ingen data definierad.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Denna slutpunkt är föråldrad. Använd segmentbaserade slutpunkter i stället.", + "This entity has no relations to objects or files" : "Denna entitet har inga relationer till objekt eller Filer", + "This is a test webhook from OpenRegister" : "Detta är en test-webhook från OpenRegister", + "This schema must use Magic Table configuration to sync" : "Detta Schema måste använda Magic Table-konfiguration för att synkronisera", + "This source has no associated registers." : "Denna Källa har inga associerade Register.", + "This will copy the _default ConfigSet with the new name" : "Detta kopierar ConfigSet _default med det nya namnet", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Detta tar permanent bort ALLA inbäddningar (vektorer) från databasen. Du måste vektorisera om alla objekt och Filer. Denna åtgärd kan inte ångras.\\n\\nÄr du säker på att du vill fortsätta?", + "This will permanently delete:" : "Detta tar permanent bort:", + "Timeout (seconds)" : "Timeout (sekunder)", + "Timestamp" : "Tidsstämpel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Tips: Aktivera endast vyer som behöver semantisk sökning för att minimera inbäddningskostnader. Enkla uppslagstabeller behöver sällan vektorisering.", + "Title" : "Titel", + "To Date" : "Till datum", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "För att justera segmentstorlek eller strategi, gå till Filkonfiguration → Bearbetningsgränser.", + "To change the embedding provider or model, go to LLM Configuration." : "För att ändra inbäddningsleverantör eller modell, gå till LLM-konfiguration.", + "To date" : "Till datum", + "Toggle search sidebar" : "Växla söksidofält", + "Tools" : "Verktyg", + "Top Deleters" : "Toppborttagare", + "Total" : "Totalt", + "Total Audit Trails" : "Totalt antal granskningsloggar", + "Total Chunks Available:" : "Totalt antal tillgängliga segment:", + "Total Deleted Items" : "Totalt antal borttagna objekt", + "Total Members" : "Totalt antal medlemmar", + "Total Objects" : "Totalt antal objekt", + "Total Objects in Database:" : "Totalt antal objekt i databasen:", + "Total Organisations" : "Totalt antal organisationer", + "Total Results" : "Totalt antal resultat", + "Total Searches" : "Totalt antal sökningar", + "Total Size" : "Total storlek", + "Total Storage" : "Total lagring", + "Total:" : "Totalt:", + "Totals" : "Totaler", + "Type" : "Typ", + "Type Issue" : "Typproblem", + "Type Variations:" : "Typvariationer:", + "Type must be \"positive\" or \"negative\"" : "Typ måste vara \"positive\" eller \"negative\"", + "Type to search for groups" : "Skriv för att söka efter grupper", + "Type to search for users" : "Skriv för att söka efter användare", + "Type to search groups..." : "Skriv för att söka grupper...", + "Type to search users..." : "Skriv för att söka användare...", + "URL" : "URL", + "URL where Ollama is running" : "URL där Ollama körs", + "UUID:" : "UUID:", + "Unchanged" : "Oförändrad", + "Unique Search Terms" : "Unika söktermer", + "Unique Terms" : "Unika termer", + "Unknown" : "Okänd", + "Unknown Register" : "Okänt Register", + "Unknown Schema" : "Okänt Schema", + "Untitled View" : "Namnlös vy", + "Update" : "Uppdatera", + "Update Operations" : "Uppdateringsåtgärder", + "Update configuration: ..." : "Uppdatera konfiguration: ...", + "Update register OAS: ..." : "Uppdatera Register-OAS: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Uppdatera vektorer när objektdata ändras (rekommenderas för korrekt sökning)", + "Updated" : "Uppdaterad", + "Updated At" : "Uppdaterad vid", + "Updated:" : "Uppdaterad:", + "Uptime:" : "Drifttid:", + "Usage Count" : "Användningsantal", + "Use SolrCloud with Zookeeper for distributed search" : "Använd SolrCloud med Zookeeper för distribuerad sökning", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Använd filter för att begränsa granskningsloggposter efter Register, Schema, åtgärdstyp, användare, datumintervall eller objekt-ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Använd filter för att begränsa borttagna objekt efter Register, Schema, borttagningsdatum eller användaren som tog bort dem.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Använd filter för att begränsa sökloggposter efter Register, Schema, lyckandestatus, användare, datumintervall, söktermer eller prestandamått.", + "Used By Collections" : "Används av samlingar", + "User" : "Användare", + "User Agent Statistics" : "Användaragentstatistik", + "User Description" : "Användarbeskrivning", + "User rate limits cleared successfully" : "Användarhastighetsgränser rensades", + "User removed successfully" : "Användaren togs bort", + "User-friendly description shown in forms and help text" : "Användarvänlig beskrivning som visas i formulär och hjälptext", + "Username" : "Användarnamn", + "Username for SOLR authentication (optional)" : "Användarnamn för SOLR-autentisering (valfritt)", + "Users" : "Användare", + "Using Pre-Generated Chunks" : "Använder förgenererade segment", + "Validate" : "Validera", + "Validate Objects" : "Validera objekt", + "Validation completed successfully" : "Valideringen slutfördes", + "Validation failed" : "Validering misslyckades", + "Value" : "Värde", + "Vector Dimensions" : "Vektordimensioner", + "Vector Search Backend" : "Vektorsökningsbackend", + "Vector field: _embedding_" : "Vektorfält: _embedding_", + "Vectorization Triggers" : "Vektoriseringsutlösare", + "Vectorize All Files" : "Vektorisera alla Filer", + "Vectorize All Objects" : "Vektorisera alla objekt", + "Vectorize all file types" : "Vektorisera alla filtyper", + "Vectorize all views" : "Vektorisera alla vyer", + "Vectorize on object creation" : "Vektorisera vid objektskapande", + "Vectors will be stored in your existing object and file collections" : "Vektorer lagras i dina befintliga objekt- och filsamlingar", + "Version" : "Version", + "Very High" : "Mycket hög", + "View API Docs" : "Visa API-dokumentation", + "View Changes" : "Visa ändringar", + "View Details" : "Visa detaljer", + "View Error" : "Visa fel", + "View File" : "Visa Fil", + "View Full Details" : "Visa fullständiga detaljer", + "View Logs" : "Visa loggar", + "View Name" : "Vynamn", + "View Object" : "Visa objekt", + "View Parameters" : "Visa parametrar", + "View Selection" : "Vyval", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Visa och analysera sökloggar med avancerade filtrerings- och analysfunktioner", + "View and analyze system audit trails with advanced filtering capabilities" : "Visa och analysera systemgranskningsloggar med avancerade filtreringsfunktioner", + "View deleted successfully" : "Vyn togs bort", + "View entity details and manage relations" : "Visa entitetsdetaljer och hantera relationer", + "View name" : "Vynamn", + "View name is required" : "Vynamn krävs", + "View saved successfully!" : "Vyn sparades!", + "View search analytics and manage search logs" : "Visa sökanalys och hantera sökloggar", + "View successfully deleted" : "Vyn togs bort", + "View successfully updated" : "Vyn uppdaterades", + "View updated successfully!" : "Vyn uppdaterades!", + "View webhook delivery logs and filter by webhook" : "Visa webhook-leveransloggar och filtrera efter webhook", + "Views" : "Vyer", + "Visible to users" : "Synlig för användare", + "Wait for Response" : "Vänta på svar", + "Wait for webhook response before continuing (required for request/response flows)" : "Vänta på webhook-svar innan fortsättning (krävs för förfrågnings-/svarsflöden)", + "Warmup Names Cache" : "Värm upp namncache", + "Warning" : "Varning", + "Warning:" : "Varning:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Webhook-loggdetaljer", + "Webhook Logs" : "Webhook-loggar", + "Webhook created successfully" : "Webhooken skapades", + "Webhook deleted" : "Webhook borttagen", + "Webhook retry delivered successfully" : "Webhook-omförsöket levererades", + "Webhook retry delivery failed" : "Leverans av webhook-omförsök misslyckades", + "Webhook updated" : "Webhook uppdaterad", + "Webhook updated successfully" : "Webhooken uppdaterades", + "Webhooks" : "Webhooks", + "Weekly" : "Veckovis", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Omslut webhook-nyttolasten i CloudEvents-format för bättre interoperabilitet", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Ja", + "You" : "Du", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Du kan skapa nya ConfigSets baserade på mallen _default, eller ladda upp anpassade direkt till din SOLR-server.", + "You do not have access to this conversation" : "Du har inte åtkomst till denna konversation", + "You do not have permission to delete this conversation" : "Du har inte behörighet att ta bort denna konversation", + "You do not have permission to modify this conversation" : "Du har inte behörighet att ändra denna konversation", + "You do not have permission to restore this conversation" : "Du har inte behörighet att återställa denna konversation", + "You must be logged in to favorite views" : "Du måste vara inloggad för att markera vyer som favoriter", + "You need an AI agent to start a conversation." : "Du behöver en AI-agent för att starta en konversation.", + "Your Fireworks AI API key. Get one at" : "Din Fireworks AI API-nyckel. Skaffa en på", + "Your OpenAI API key. Get one at" : "Din OpenAI API-nyckel. Skaffa en på", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Din återkoppling har registrerats. Du kan valfritt ange ytterligare detaljer här...", + "Zookeeper Hosts" : "Zookeeper-värdar", + "Zookeeper Port" : "Zookeeper-port", + "Zookeeper Settings (SolrCloud)" : "Zookeeper-inställningar (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Zookeeper-anslutningssträng för SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper-portnummer (valfritt, standard är 2181)", + "chunk" : "segment", + "chunks" : "segment", + "chunks processed" : "segment bearbetade", + "configuration(s)" : "konfiguration(er)", + "e.g., lib/Settings/config.json" : "t.ex. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "t.ex. lib/Settings/register.json", + "failed" : "misslyckades", + "file" : "fil", + "files" : "filer", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "medlemmar", + "menu or contact someone with permission to create agents." : "menyn eller kontakta någon med behörighet att skapa agenter.", + "messages" : "meddelanden", + "n8n URL and API key are required" : "n8n-URL och API-nyckel krävs", + "n8n configuration saved successfully" : "n8n-konfigurationen sparades", + "n8n connection not configured" : "n8n-anslutning är inte konfigurerad", + "n8n connection successful" : "n8n-anslutning lyckades", + "n8n connection test successful" : "n8n-anslutningstest lyckades", + "n8n integration disabled" : "n8n-integration inaktiverad", + "n8n integration enabled" : "n8n-integration aktiverad", + "n8n project initialized successfully" : "n8n-projektet initierades", + "n8n settings saved successfully" : "n8n-inställningarna sparades", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "objekt", + "objects processed" : "objekt bearbetade", + "or pick a range" : "eller välj ett intervall", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "ger en bra balans mellan hastighet och noggrannhet.", + "register(s)" : "register", + "register(s) selected" : "register valda", + "required" : "obligatorisk", + "results" : "resultat", + "schema(s)" : "schema(n)", + "schema(s) selected" : "schema(n) valda", + "sk-..." : "sk-...", + "this application" : "denna applikation", + "to" : "till", + "vectorized" : "vektoriserad", + "views selected" : "vyer valda", + "ℹ️ Current Configuration" : "ℹ️ Aktuell konfiguration", + "⚡ Batch Processing" : "⚡ Batchbearbetning", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Omfattande sammanhang ✓ Mindre risk att missa detaljer ✗ Långsammare svar ✗ Kan inkludera mindre relevant information", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Snabbare svar ✓ Mer fokuserade svar ✗ Kan missa relevant information", + "✨ AI Features" : "✨ AI-funktioner", + "All search trails cleared successfully" : "Alla sökloggar rensades", + "Multiple search trail deletion not implemented yet" : "Borttagning av flera sökloggar är inte implementerad ännu", + "No expired search trails found to clear" : "Inga utgångna sökloggar hittades att rensa", + "Missing message" : "Meddelande saknas", + "message content is required" : "meddelandeinnehåll krävs", + "Missing conversation or agentUuid" : "Konversation eller agentUuid saknas", + "Access denied" : "Åtkomst nekad", + "Conversation not found" : "Konversationen hittades inte", + "AI service not configured" : "AI-tjänsten är inte konfigurerad", + "Failed to process message" : "Det gick inte att bearbeta meddelandet", + "Missing conversationId" : "conversationId saknas", + "conversationId is required" : "conversationId krävs", + "Failed to fetch conversation history" : "Det gick inte att hämta konversationshistorik", + "Failed to clear conversation" : "Det gick inte att rensa konversationen", + "Invalid feedback type" : "Ogiltig återkopplingstyp", + "type must be \"positive\" or \"negative\"" : "typ måste vara \"positive\" eller \"negative\"", + "Message not found" : "Meddelandet hittades inte", + "Failed to save feedback" : "Det gick inte att spara återkoppling", + "Failed to get chat statistics" : "Det gick inte att hämta chattstatistik", + "Not Found" : "Hittades inte", + "Engine not found" : "Motorn hittades inte", + "Consumer not found" : "Konsumenten hittades inte", + "Backend parameter is required" : "Backend-parameter krävs", + "Failed to get database information: %s" : "Det gick inte att hämta databasinformation: %s", + "SOLR setup error: %s" : "SOLR-konfigurationsfel: %s", + "Reindex failed: %s" : "Omindexering misslyckades: %s", + "Anonymized" : "Anonymiserad", + "Entities detected" : "Entiteter identifierade", + "Extract Now" : "Extrahera nu", + "Extracted at" : "Extraherad vid", + "Extraction" : "Extrahering", + "Failed to load extraction data" : "Det gick inte att läsa in extraheringsdata", + "Failed to load register data" : "Det gick inte att läsa in Register-data", + "Failed to retrieve extraction status." : "Det gick inte att hämta extraheringsstatus.", + "Failed to retrieve objects for file." : "Det gick inte att hämta objekt för Filen.", + "No extraction data available for this file" : "Ingen extraheringsdata tillgänglig för denna Fil", + "No register objects reference this file" : "Inga Register-objekt refererar till denna Fil", + "Not extracted" : "Inte extraherad", + "Register Objects" : "Register-objekt", + "Risk level" : "Risknivå", + "Text chunks" : "Textsegment", + "Unknown error" : "Okänt fel", + "Very high" : "Mycket hög", + "{title} in {register} / {schema}" : "{title} i {register} / {schema}", + "Push Notifications" : "Push-notiser", + "Real-time push notification status via notify_push" : "Status för push-notiser i realtid via notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push i realtid är inte tillgänglig — appen notify_push är inte installerad", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Installera appen notify_push från Nextcloud App Store för att aktivera uppdateringar i realtid.", + "Open Nextcloud App Store" : "Öppna Nextcloud App Store", + "notify_push is installed but not yet active" : "notify_push är installerad men ännu inte aktiv", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push är installerad men OpenRegister har ännu inte bekräftat en lyckad push. Utlös en objektsparning för att aktivera, eller kontrollera din notify_push-konfiguration.", + "notify_push configuration guide" : "konfigurationsguide för notify_push", + "Realtime push active" : "Push i realtid aktiv", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push-notiser i realtid är aktiva. Anslutna klienter får omedelbara uppdateringar när objekt skapas, uppdateras eller tas bort.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/sv.json b/l10n/sv.json new file mode 100644 index 0000000000..b4390c925b --- /dev/null +++ b/l10n/sv.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin krävs)", + "(no subject)": "(inget ämne)", + "(no title)": "(ingen titel)", + "(unnamed)": "(namnlös)", + "(untitled card)": "(namnlöst kort)", + "({count} selected)": "({count} valda)", + ", {failed} failed": ", {failed} misslyckades", + "0 = unlimited": "0 = obegränsat", + "A deactivation request is pending.": "En avaktiveringsförfrågan väntar.", + "AI Assistant": "AI-assistent", + "AI Chat": "AI-chatt", + "AI service not configured": "AI-tjänsten är inte konfigurerad", + "API Key": "API-nyckel", + "API Request Quota (requests/day)": "API-förfrågningskvot (förfrågningar/dag)", + "API Token Configuration": "Konfiguration av API-token", + "API Tokens": "API-token", + "API tokens saved successfully": "API-token sparades", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Art 30 §4: denna rapport kopplar varje behandlingsaktivitet med de livstidsrevisionsloggar som tillskrivs den. Revisorer och Autoriteit Persoonsgegevens använder detta för tillsynsgranskning.", + "About ConfigSets": "Om ConfigSets", + "Access denied": "Åtkomst nekad", + "Access organisation dashboard and analytics": "Öppna organisationens instrumentpanel och analys", + "Account": "Konto", + "Action": "Åtgärd", + "Action Distribution": "Åtgärdsfördelning", + "Action:": "Åtgärd:", + "Actions": "Åtgärder", + "Active": "Aktiv", + "Active Collections": "Aktiva samlingar", + "Active Filters": "Aktiva filter", + "Active Organisation:": "Aktiv organisation:", + "Active Organisations": "Aktiva organisationer", + "Active filters:": "Aktiva filter:", + "Active organisation changed successfully": "Aktiv organisation har ändrats", + "Active organisation set successfully": "Aktiv organisation har angetts", + "Activities": "Aktiviteter", + "Activity": "Aktivitet", + "Activity Period": "Aktivitetsperiod", + "Add": "Lägg till", + "Add Attachment": "Lägg till bilaga", + "Add Endpoint": "Lägg till slutpunkt", + "Add Groups": "Lägg till grupper", + "Add Property to '{title}'": "Lägg till egenskap i '{title}'", + "Add Register": "Lägg till register", + "Add Schema": "Lägg till schema", + "Add Source": "Lägg till källa", + "Add User": "Lägg till användare", + "Add User to Organisation": "Lägg till användare i organisation", + "Add a contact from any of your address books to associate it with this object.": "Lägg till en kontakt från någon av dina adressböcker för att koppla den till det här objektet.", + "Add a file or files": "Lägg till en eller flera filer", + "Add card": "Lägg till kort", + "Add contact": "Lägg till kontakt", + "Add endpoint": "Lägg till slutpunkt", + "Add oneOf entry": "Lägg till oneOf-post", + "Add range": "Lägg till intervall", + "Add schema titles, descriptions, and register information to provide richer context for search": "Lägg till schematitlar, beskrivningar och registerinformation för att ge ett rikare sammanhang för sökning", + "Add to favorites": "Lägg till i favoriter", + "Added": "Tillagd", + "Added to favorites": "Tillagd i favoriter", + "Additional Information": "Ytterligare information", + "Additional feedback saved. Thank you!": "Ytterligare feedback sparad. Tack!", + "Addresses": "Adresser", + "Admin": "Administratör", + "Advanced": "Avancerat", + "Advanced Filters": "Avancerade filter", + "Advanced Options": "Avancerade alternativ", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Avancerade filter är inte tillgängliga när databaskälla används. Byt till Auto eller SOLR-index för filtreringsalternativ.", + "After": "Efter", + "Agent Type": "Agenttyp", + "Agent deleted successfully": "Agenten har tagits bort", + "Agent successfully created": "Agenten skapades", + "Agent successfully updated": "Agenten uppdaterades", + "Agents": "Agenter", + "Aggregated across schemas": "Aggregerat över scheman", + "All": "Alla", + "All Categories": "Alla kategorier", + "All Collections": "Alla samlingar", + "All Confidence Levels": "Alla konfidensnivåer", + "All ConfigSets": "Alla ConfigSets", + "All Files": "Alla filer", + "All Levels": "Alla nivåer", + "All Types": "Alla typer", + "All Webhooks": "Alla webhooks", + "All actions": "Alla åtgärder", + "All audit trails cleared successfully": "Alla revisionsloggar har rensats", + "All clear": "Allt klart", + "All file types": "Alla filtyper", + "All registers": "Alla register", + "All schemas": "Alla scheman", + "All search trails cleared successfully": "Alla sökloggar har rensats", + "All searches": "Alla sökningar", + "All users": "Alla användare", + "All-day events": "Heldagshändelser", + "Allowed MIME Types": "Tillåtna MIME-typer", + "Already linked": "Redan länkad", + "An error occurred while clearing audit trails": "Ett fel uppstod vid rensning av revisionsloggar", + "An error occurred while deleting the audit trail": "Ett fel uppstod vid borttagning av revisionsloggen", + "An error occurred while deleting the object": "Ett fel uppstod vid borttagning av objektet", + "An error occurred while deleting the view": "Ett fel uppstod vid borttagning av vyn", + "An error occurred while permanently deleting the objects": "Ett fel uppstod vid permanent borttagning av objekten", + "An error occurred while restoring the objects": "Ett fel uppstod vid återställning av objekten", + "An error occurred while saving the organisation": "Ett fel uppstod vid sparande av organisationen", + "Analysis completed:": "Analys slutförd:", + "Analysis steps:": "Analyssteg:", + "Analytics": "Analys", + "Analyze Objects": "Analysera objekt", + "Analyze Properties": "Analysera egenskaper", + "Analyze existing properties for improvement opportunities": "Analysera befintliga egenskaper för förbättringsmöjligheter", + "Analyzing...": "Analyserar...", + "Anonymized": "Anonymiserad", + "Anonymous Group": "Anonym grupp", + "Any favorites and sharing settings for this view": "Alla favoriter och delningsinställningar för den här vyn", + "Any user": "Valfri användare", + "App ID": "App-ID", + "App store cache invalidated successfully": "App Store-cachen har ogiltigförklarats", + "App store cache invalidated: {invalidated}": "App Store-cachen ogiltigförklarad: {invalidated}", + "Application deleted successfully": "Applikationen har tagits bort", + "Application identifier for this configuration (optional)": "Applikationsidentifierare för denna konfiguration (valfri)", + "Application settings": "Applikationsinställningar", + "Application successfully created": "Applikationen skapades", + "Application successfully updated": "Applikationen uppdaterades", + "Applications": "Applikationer", + "Apply": "Tillämpa", + "Apply Changes": "Tillämpa ändringar", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Tillämpa en roll på alla scheman i det här registret som saknar explicita auktoriseringsoverstyningar.", + "Approval Progress": "Godkännandeförlopp", + "Approve": "Godkänn", + "Approved": "Godkänd", + "Archive": "Arkivera", + "Archive conversation": "Arkivera konversation", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Arkivera denna verwerkingsactiviteit? Revisionsloggens rader fortsätter att referera till den.", + "Archived conversations are hidden from your active list": "Arkiverade konversationer döljs från din aktiva lista", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Är du säker på att du vill TA BORT samlingen \"{name}\"?\n\nDetta kommer att:\n• Permanent ta bort samlingen och all dess data\n• Ta bort alla indexerade dokument\n• Denna åtgärd kan inte ångras", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Är du säker på att du vill rensa gamla sökloggar? Detta tar bort poster äldre än 30 dagar.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Är du säker på att du vill rensa all data från samlingen \"{name}\"?\n\nDetta kommer att:\n• Ta bort alla indexerade dokument\n• Behålla samlingsstrukturen intakt\n• Denna åtgärd kan inte ångras", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Är du säker på att du vill ta bort applikationen \"{name}\"? Denna åtgärd kan inte ångras.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Är du säker på att du vill ta bort de valda revisionsloggarna? Denna åtgärd kan inte ångras.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Är du säker på att du vill ta bort de valda sökloggarna? Denna åtgärd kan inte ångras.", + "Are you sure you want to delete this ConfigSet?": "Är du säker på att du vill ta bort denna ConfigSet?", + "Are you sure you want to delete this configset?": "Är du säker på att du vill ta bort denna configset?", + "Are you sure you want to leave '{name}'?": "Är du säker på att du vill lämna '{name}'?", + "Are you sure you want to permanently delete": "Är du säker på att du vill ta bort permanent", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Är du säker på att du vill omindexera samlingen \"{name}\"?\n\nDetta kommer att:\n• Återskapa indexet med alla objekt\n• Ta flera minuter att slutföra\n• Kan påverka sökprestandan under omindexering", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Är du säker på att du vill ta bort schemat \"{schema}\" från registret \"{register}\"? Denna åtgärd kan inte ångras.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Är du säker på att du vill mjukradera {count} aktiva objekt för schemat \"{schema}\"?\n\nDe markeras som borttagna men kan tas bort permanent senare.", + "Array Object Configuration:": "Konfiguration av arrayobjekt:", + "Articles": "Artiklar", + "Ask a question...": "Ställ en fråga...", + "Ask questions about your data using natural language": "Ställ frågor om dina data med naturligt språk", + "Assigned collaborative tags": "Tilldelade samarbetstaggar", + "Assignment notifications": "Tilldelningsaviseringar", + "Attachment added to {name}": "Bilaga tillagd i {name}", + "Attempt": "Försök", + "Audit Trail Changes": "Ändringar i revisionsloggen", + "Audit Trail Details": "Detaljer om revisionsloggen", + "Audit Trail Management": "Hantering av revisionsloggar", + "Audit Trail Statistics": "Statistik för revisionsloggar", + "Audit Trails": "Revisionsloggar", + "Audit trail data copied to clipboard": "Data från revisionsloggen kopierades till urklipp", + "Audit trail deleted successfully": "Revisionsloggen har tagits bort", + "Audit trail successfully deleted": "Revisionsloggen togs bort", + "Authentication": "Autentisering", + "Auto (e.g. Y for year, F Y for month)": "Auto (t.ex. Y för år, F Y för månad)", + "Auto share": "Automatisk delning", + "Auto-commit disabled": "Auto-commit inaktiverad", + "Auto-commit enabled": "Auto-commit aktiverad", + "Auto-create Default Organisation": "Skapa standardorganisation automatiskt", + "Auto-retry failed vectorizations": "Försök automatiskt igen vid misslyckad vektorisering", + "Automatically commit changes to SOLR index": "Bekräfta ändringar i SOLR-index automatiskt", + "Automatically connect to the federation network.": "Anslut automatiskt till federationsnätverket.", + "Automatically create a default organisation if none exists when the app is initialized": "Skapa automatiskt en standardorganisation om ingen finns när appen initieras", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Generera vektorinbäddningar automatiskt från textblock när filer laddas upp och bearbetas", + "Automatically generate vector embeddings when objects are created or updated": "Generera vektorinbäddningar automatiskt när objekt skapas eller uppdateras", + "Automatically retry failed vectorization attempts (max 3 retries)": "Försök automatiskt igen vid misslyckad vektorisering (max 3 försök)", + "Automatically update catalogues.": "Uppdatera kataloger automatiskt.", + "Available Filters": "Tillgängliga filter", + "Available Workflows": "Tillgängliga arbetsflöden", + "Available placeholders:": "Tillgängliga platshållare:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Din autentiseringsleverantör stöder inte avatarbyten.", + "Avatar removed": "Avataren har tagits bort", + "Avatar updated successfully": "Avataren uppdaterades", + "Avg Execution Time": "Genomsnittlig körningstid", + "Avg Members/Org": "Genomsnittliga medlemmar/org", + "Avg Object Views/Session": "Genomsnittliga objektvisningar/session", + "Avg Response Time": "Genomsnittlig svarstid", + "Avg Results/Search": "Genomsnittliga resultat/sökning", + "Avg Searches/Session": "Genomsnittliga sökningar/session", + "Back": "Tillbaka", + "Back to Entities": "Tillbaka till entiteter", + "Back to Registers": "Tillbaka till register", + "Back to Webhooks": "Tillbaka till webhooks", + "Back to applications": "Tillbaka till applikationer", + "Back to entities": "Tillbaka till entiteter", + "Backend parameter is required": "Backendparameter krävs", + "Backend updated successfully. Please reload the application.": "Backendet uppdaterades. Ladda om applikationen.", + "Bandwidth Quota (MB/month)": "Bandbreddskvot (MB/månad)", + "Base URL (Optional)": "Bas-URL (valfri)", + "Basic Information": "Grundläggande information", + "Batch Size": "Batchstorlek", + "Batch extraction completed": "Batchextraktion slutförd", + "Before": "Innan", + "Before object vectorization can work:": "Innan objektvektorisering kan fungera:", + "Behavior": "Beteende", + "Behavior Issue": "Beteendeproblem", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 varaktighet, t.ex. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob-lagring har avvecklats. Alla objekt använder nu magiska tabeller.", + "Bookmarks": "Bokmärken", + "Branch": "Gren", + "Bulk Role Assignment: {title}": "Masktilldelning av roller: {title}", + "Bulk delete operation completed successfully": "Massradering slutförd", + "Bulk save operation completed successfully": "Masssparning slutförd", + "Business Data": "Affärsdata", + "CSV": "CSV", + "Cache cleared successfully": "Cachen har rensats", + "Cache grew from {old} to {new} entries.": "Cachen växte från {old} till {new} poster.", + "Cache shrunk from {old} to {new} entries.": "Cachen krympte från {old} till {new} poster.", + "Cache stayed the same at {size} entries.": "Cachen förblev oförändrad med {size} poster.", + "Calculate Sizes": "Beräkna storlekar", + "Calendar": "Kalender", + "Calendar Provider Configuration": "Konfiguration av kalenderleverantör", + "Calendar integration is not available": "Kalenderintegration är inte tillgänglig", + "Calendar name": "Kalendernamn", + "Cancel": "Avbryt", + "Cancel deactivation request": "Avbryt avaktiveringsförfrågan", + "Cannot delete: objects are still attached": "Kan inte tas bort: objekt är fortfarande kopplade", + "Cannot edit: This register is managed by external configuration {title}": "Kan inte redigeras: Det här registret hanteras av extern konfiguration {title}", + "Cannot remove schema with existing objects ({count} objects)": "Kan inte ta bort schema med befintliga objekt ({count} objekt)", + "Cannot remove schema {schema} because it contains {count} objects": "Kan inte ta bort schemat {schema} eftersom det innehåller {count} objekt", + "Cards": "Kort", + "Cascade delete": "Kaskadradering", + "Categories of data subjects (one per line)": "Kategorier av registrerade (en per rad)", + "Categories of personal data (one per line)": "Kategorier av personuppgifter (en per rad)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategori", + "Change Type": "Ändringstyp", + "Change password": "Ändra lösenord", + "Changes": "Ändringar", + "Changes copied to clipboard": "Ändringar kopierades till urklipp", + "Changes for Audit Trail #{id}": "Ändringar för revisionslogg #{id}", + "Chat Model": "Chattmodell", + "Chat Provider": "Chattleverantör", + "Chat Provider (RAG)": "Chattleverantör (RAG)", + "Chat Settings": "Chattinställningar", + "Chat provider connection successful!": "Anslutning till chattleverantören lyckades!", + "Chat settings": "Chattinställningar", + "Chat test failed: {error}": "Chatttest misslyckades: {error}", + "Choose a register": "Välj ett register", + "Choose a schema": "Välj ett schema", + "Choose how vector similarity calculations are performed for semantic search": "Välj hur vektorsimilaritetsberäkningar utförs för semantisk sökning", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Välj vilka filtyper som ska inkluderas i vektoriseringsprocessen. Endast filer med extraherad text och block bearbetas.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Välj vilka vyer som ska inkluderas i vektoriseringsprocessen. Lämna tomt för att bearbeta alla vyer baserat på din konfiguration.", + "Chunk": "Block", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Blockborttagning är ännu inte implementerat. Använd blockbaserade slutpunkter.", + "Chunks": "Block", + "Chunks to Vectorize:": "Block att vektorisera:", + "Cleanup Old Trails": "Rensa gamla loggar", + "Cleanup completed": "Rensning slutförd", + "Cleanup completed successfully. Deleted {count} entries.": "Rensning slutförd. {count} poster togs bort.", + "Cleanup failed: {error}": "Rensning misslyckades: {error}", + "Clear All Embeddings": "Rensa alla inbäddningar", + "Clear Entries": "Rensa poster", + "Clear Filtered Audit Trails": "Rensa filtrerade revisionsloggar", + "Clear Filters": "Rensa filter", + "Clear Index": "Rensa index", + "Clear Selection": "Rensa urval", + "Clear all": "Rensa alla", + "Clear all filters": "Rensa alla filter", + "Clear date range": "Rensa datumintervall", + "Clear filters": "Rensa filter", + "Clearing...": "Rensar...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Klicka på Kör efterlevnadssökning för att hitta scheman där PII har identifierats men ingen anteckning om behandlingsaktivitet finns.", + "Click to subscribe to notifications": "Klicka för att prenumerera på aviseringar", + "Click to unsubscribe from notifications": "Klicka för att avsluta prenumeration på aviseringar", + "Close": "Stäng", + "Cluster name": "Klusternamn", + "Code": "Kod", + "Code (short readable key, e.g. v-2026-001)": "Kod (kort läsbar nyckel, t.ex. v-2026-001)", + "Collapse details": "Dölj detaljer", + "Collection Management": "Samlingshantering", + "Collection Name": "Samlingsnamn", + "Collection assignments updated successfully": "Samlingstilldelningar uppdaterades", + "Collection cleared successfully": "Samlingen rensades", + "Collection copied successfully": "Samlingen kopierades", + "Collection created successfully": "Samlingen skapades", + "Collection deleted successfully": "Samlingen togs bort", + "Collection used to store and index file metadata and content": "Samling som används för att lagra och indexera filmetadata och innehåll", + "Collection used to store and index object data": "Samling som används för att lagra och indexera objektdata", + "Collections:": "Samlingar:", + "Color": "Färg", + "Comment...": "Kommentar...", + "Commit Message": "Incheckningsmeddelande", + "Commit Within (ms)": "Bekräfta inom (ms)", + "Commit message": "Incheckningsmeddelande", + "Compare current schema with real object data": "Jämför aktuellt schema med verklig objektdata", + "Completed": "Slutförd", + "Complex": "Komplex", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Komplexa frågor: Avancerade sökningar med flera filter, operatorer och komplexa parameterkombinationer", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Komplexa frågor: avancerade sökningar med flera filter, operatorer och komplexa parameterkombinationer", + "Compliance": "Efterlevnad", + "Confidence Level": "Konfidensnivå", + "Confidence Score": "Konfidenspoäng", + "Confidence: {confidence}%": "Konfidensgrad: {confidence}%", + "Config must be provided as an object": "Konfigurationen måste anges som ett objekt", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet-hantering", + "ConfigSet Name": "ConfigSet-namn", + "ConfigSet created successfully": "ConfigSet skapades", + "ConfigSet deleted successfully": "ConfigSet togs bort", + "ConfigSet:": "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSets definierar schemat och konfigurationen för dina SOLR-samlingar. De innehåller fältdefinitioner, analysatorer och andra sökinställningar.", + "Configuration": "Konfiguration", + "Configuration Data": "Konfigurationsdata", + "Configuration is up to date": "Konfigurationen är aktuell", + "Configuration saved": "Konfigurationen sparad", + "Configuration saved successfully.": "Konfigurationen sparades.", + "Configuration type (default, application, etc.)": "Konfigurationstyp (standard, applikation, osv.)", + "Configurations": "Konfigurationer", + "Configure API tokens for external service integrations": "Konfigurera API-token för externa tjänstintegrationer", + "Configure Apache SOLR search engine for advanced search capabilities": "Konfigurera Apache SOLR-sökmotorn för avancerade sökfunktioner", + "Configure Facets": "Konfigurera fasetter", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Konfigurera LLM-leverantörer för AI-drivna funktioner inklusive semantisk sökning, inbäddningar och chatt.", + "Configure SOLR Facets": "Konfigurera SOLR-fasetter", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Konfigurera grundläggande anslutningsinställningar för din SOLR-server inklusive autentisering och nätverksalternativ. Använd de separata dialogerna för ConfigSet och samlingshantering för att hantera kärnar och samlingar.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Konfigurera grundläggande anslutningsinställningar för din SOLR-server inklusive autentisering och nätverksalternativ. Använd de separata dialogerna för configset och samlingshantering för att hantera kärnar och samlingar.", + "Configure default organisation and organisation-related settings": "Konfigurera standardorganisation och organisationsrelaterade inställningar", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Konfigurera hur databasobjekt konverteras till vektorinbäddningar för semantisk sökning. Objekt vektoriseras direkt utan att textextraktion behövs.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Konfigurera hur objekt konverteras till text innan vektorisering. Dessa inställningar påverkar sökkvalitet och kontext.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Konfigurera LLM-leverantörer för AI-drivna funktioner inklusive semantisk sökning, inbäddningar och chatt.", + "Configure multi-organization support and tenant isolation": "Konfigurera stöd för flera organisationer och klientisolering", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Konfigurera parametrar för filvektorisering. Den här processen genererar vektorinbäddningar för alla extraherade filblock.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Konfigurera parametrar för objektvektorisering. Den här processen genererar vektorinbäddningar för alla objekt som matchar dina vyfilter.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Konfigurera det här schemat för att visa objekt som händelser i Nextcloud Kalender-appen.", + "Configure which types of data to search and how many sources to retrieve": "Konfigurera vilka typer av data som ska sökas och hur många källor som ska hämtas", + "Configuring field mappings...": "Konfigurerar fältmappningar...", + "Confirm Account Deactivation": "Bekräfta kontoinaktivering", + "Confirm Object Deletion": "Bekräfta objektborttagning", + "Confirm Object Restoration": "Bekräfta objektåterställning", + "Confirm Permanent Object Deletion": "Bekräfta permanent objektborttagning", + "Confirm deactivation": "Bekräfta inaktivering", + "Connecting to SOLR cluster...": "Ansluter till SOLR-kluster...", + "Connection Failed": "Anslutning misslyckades", + "Connection Settings": "Anslutningsinställningar", + "Connection Successful!": "Anslutning lyckades!", + "Connection failed": "Anslutning misslyckades", + "Connection protocol": "Anslutningsprotokoll", + "Connection successful": "Anslutning lyckades", + "Connection timeout in seconds": "Anslutningstimeout i sekunder", + "Constant delay between retries (5 minutes)": "Konstant fördröjning mellan försök (5 minuter)", + "Constraint Issue": "Begränsningsproblem", + "Consumer not found": "Konsument hittades inte", + "Contacts": "Kontakter", + "Contacts integration is not available": "Kontaktintegration är inte tillgänglig", + "Content Filters": "Innehållsfilter", + "Continue to Properties": "Fortsätt till egenskaper", + "Contracts": "Kontrakt", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Styr vilka objektvyer som ska vektoriseras för att minska API-kostnader. Vektorisera bara vyer som drar nytta av semantisk sökning.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Styr vilka vyer och verktyg AI:n kan använda i den här konversationen. Som standard är alla agentfunktioner aktiverade.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Styr slumpmässighet (0 = deterministisk, 2 = mycket kreativ)", + "Conversation ID is required": "Konversations-ID krävs", + "Conversation archived": "Konversation arkiverad", + "Conversation archived successfully": "Konversationen arkiverades", + "Conversation cleared successfully": "Konversationen rensades", + "Conversation deleted": "Konversation borttagen", + "Conversation not found": "Konversation hittades inte", + "Conversation permanently deleted": "Konversationen har tagits bort permanent", + "Conversation renamed": "Konversation omdöpt", + "Conversation restored": "Konversation återställd", + "Conversation started with {agent}": "Konversation startad med {agent}", + "Conversation title": "Konversationstitel", + "Conversations": "Konversationer", + "Converting {schema} to blob storage...": "Konverterar {schema} till blob-lagring...", + "Converting {schema} to magic table...": "Konverterar {schema} till magisk tabell...", + "Copied!": "Kopierat!", + "Copy": "Kopiera", + "Copy Changes": "Kopiera ändringar", + "Copy Collection": "Kopiera samling", + "Copy Data": "Kopiera data", + "Copy Full Data": "Kopiera fullständig data", + "Copy of {name}": "Kopia av {name}", + "Copy to clipboard": "Kopiera till urklipp", + "Copying...": "Kopierar...", + "Costs": "Kostnader", + "Count": "Antal", + "Counting objects...": "Räknar objekt...", + "Create": "Skapa", + "Create API Token": "Skapa API-token", + "Create Agent": "Skapa agent", + "Create Application": "Skapa applikation", + "Create Collection": "Skapa samling", + "Create ConfigSet": "Skapa ConfigSet", + "Create New Collection": "Skapa ny samling", + "Create New ConfigSet": "Skapa ny ConfigSet", + "Create Operations": "Skapa operationer", + "Create Organisation": "Skapa organisation", + "Create Webhook": "Skapa webhook", + "Create a copy of collection:": "Skapa en kopia av samlingen:", + "Create a new ConfigSet based on the _default template": "Skapa en ny ConfigSet baserat på _default-mallen", + "Create a new SOLR collection from an existing ConfigSet": "Skapa en ny SOLR-samling från en befintlig ConfigSet", + "Create a new SOLR collection from an existing configset": "Skapa en ny SOLR-samling från en befintlig configset", + "Create a new configset based on the _default template": "Skapa en ny configset baserat på _default-mallen", + "Create a new event or link an existing one from any of your calendars.": "Skapa en ny händelse eller länka en befintlig från någon av dina kalendrar.", + "Create another": "Skapa ytterligare", + "Create copies of": "Skapa kopior av", + "Create event": "Skapa händelse", + "Create new token": "Skapa ny token", + "Create or link a Deck card to track work on this object.": "Skapa eller länka ett Deck-kort för att spåra arbete på det här objektet.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Skapa den första verwerkingsactiviteit för att börja tagga revisionsloggrader med deras AVG Art 30-tillskrivning.", + "Create your first AI agent to get started.": "Skapa din första AI-agent för att komma igång.", + "Created": "Skapad", + "Created:": "Skapad:", + "Creating search collections...": "Skapar söksamlingar...", + "Creating...": "Skapar...", + "Current": "Nuvarande", + "Current Filters": "Aktuella filter", + "Current Type:": "Aktuell typ:", + "Current password": "Nuvarande lösenord", + "Custom API endpoint if using a different region": "Anpassad API-slutpunkt om du använder en annan region", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Anpassade HTTP-huvuden (ett per rad, format: Header-Name: värde)", + "Custom HTTP headers (one per line, format: header-name: value)": "Anpassade HTTP-huvuden (ett per rad, format: header-name: värde)", + "Custom Ranges:": "Anpassade intervall:", + "Custom display title for this facet": "Anpassad visningstitel för den här fasetten", + "Custom pattern:": "Anpassat mönster:", + "DELETE COLLECTION": "TA BORT SAMLING", + "DSAR": "DSAR", + "Daily": "Dagligen", + "Dashboard": "Instrumentpanel", + "Dashboard not found": "Instrumentpanel hittades inte", + "Data": "Data", + "Data Source": "Datakälla", + "Data Sources": "Datakällor", + "Data Views": "Datavyer", + "Data sources": "Datakällor", + "Data type does not match observed values": "Datatypen matchar inte observerade värden", + "Data-subject access request": "Begäran om registrerades åtkomst", + "Database URL": "Databas-URL", + "Database information refreshed": "Databasinformation uppdaterad", + "Date Faceting:": "Datumfasettning:", + "Date Range": "Datumintervall", + "Dates": "Datum", + "Deactivation request cancelled": "Avaktiveringsförfrågan avbruten", + "Deactivation request submitted": "Avaktiveringsförfrågan skickad", + "Deck": "Deck", + "Deck integration is not available": "Deck-integration är inte tillgänglig", + "Default": "Standard", + "Default Object Tenant": "Standardklient för objekt", + "Default Organisation": "Standardorganisation", + "Default Owner": "Standardägare", + "Default User (for cron/background jobs)": "Standardanvändare (för cron/bakgrundsjobb)", + "Default User Tenant": "Standardanvändarklient", + "Default value": "Standardvärde", + "Default value (Email)": "Standardvärde (e-post)", + "Default value (Password)": "Standardvärde (lösenord)", + "Default value (Phone number)": "Standardvärde (telefonnummer)", + "Default value (Regex)": "Standardvärde (Regex)", + "Default: 5": "Standard: 5", + "Defaults to current user. Select a different user if needed.": "Standardinställningen är den aktuella användaren. Välj en annan användare vid behov.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Fördröjningar fördubblas vid varje försök (2, 4, 8 minuter...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Fördröjningar ökar linjärt (5, 10, 15 minuter...)", + "Delete": "Ta bort", + "Delete ({count})": "Ta bort ({count})", + "Delete Application": "Ta bort applikation", + "Delete Audit Trail": "Ta bort revisionslogg", + "Delete Collection": "Ta bort samling", + "Delete ConfigSet": "Ta bort ConfigSet", + "Delete Objects": "Ta bort objekt", + "Delete Operations": "Ta bort operationer", + "Delete SOLR Collection": "Ta bort SOLR-samling", + "Delete View": "Ta bort vy", + "Delete all objects in this schema": "Ta bort alla objekt i det här schemat", + "Delete permanently": "Ta bort permanent", + "Delete this conversation?": "Ta bort den här konversationen?", + "Delete view": "Ta bort vy", + "Delete {count} object": "Ta bort {count} objekt", + "Delete {count} objects": "Ta bort {count} objekt", + "Deleted": "Borttagen", + "Deleted By": "Borttagen av", + "Deleted Date": "Borttagningsdatum", + "Deleted Items Management": "Hantering av borttagna objekt", + "Deleted This Week": "Borttagen den här veckan", + "Deleted Today": "Borttagen idag", + "Deleted:": "Borttagen:", + "Deleting SOLR Collection...": "Tar bort SOLR-samling...", + "Deleting...": "Tar bort...", + "Deletion Date Range": "Datumintervall för borttagning", + "Deletion Statistics": "Statistik för borttagning", + "Deprecated": "Föråldrad", + "Depublish": "Avpublicera", + "Description": "Beskrivning", + "Description Template": "Beskrivningsmall", + "Description shown as tooltip": "Beskrivning visas som verktygstips", + "Description:": "Beskrivning:", + "Detect data types and patterns": "Identifiera datatyper och mönster", + "Detected At": "Identifierad", + "Detected Format:": "Identifierat format:", + "Detected Issues:": "Identifierade problem:", + "Detected: {date}": "Identifierad: {date}", + "Direct permission": "Direkt behörighet", + "Disable": "Inaktivera", + "Disable this to make the agent": "Inaktivera detta för att göra agenten", + "Disabled": "Inaktiverad", + "Discover Files": "Identifiera filer", + "Discovered Properties": "Identifierade egenskaper", + "Display Format": "Visningsformat", + "Display Name": "Visningsnamn", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Vill du permanent ta bort alla filtrerade revisionsloggsposter? Denna åtgärd kan inte ångras.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Vill du permanent ta bort den här revisionsloggsposten? Denna åtgärd kan inte ångras.", + "Documents": "Dokument", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR aktiverad", + "Download API Spec": "Hämta API-specifikation", + "Download API Specification": "Hämta API-specifikation", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Ladda ned en kopia av alla dina personuppgifter lagrade i OpenRegister (GDPR Artikel 20).", + "Drag a file or files here": "Dra en eller flera filer hit", + "Dry run completed: {count} fields would be created": "Testkörning slutförd: {count} fält skulle skapas", + "Dry run completed: {count} fields would be fixed": "Testkörning slutförd: {count} fält skulle åtgärdas", + "Duration in seconds (optional)": "Varaktighet i sekunder (valfri)", + "Edit": "Redigera", + "Edit Agent": "Redigera agent", + "Edit Application": "Redigera applikation", + "Edit Configuration": "Redigera konfiguration", + "Edit Endpoint": "Redigera slutpunkt", + "Edit Organisation": "Redigera organisation", + "Edit Property '{key}' of '{title}'": "Redigera egenskap '{key}' i '{title}'", + "Edit Register": "Redigera register", + "Edit Schema": "Redigera schema", + "Edit Source": "Redigera källa", + "Edit View": "Redigera vy", + "Edit Webhook": "Redigera webhook", + "Edit labels": "Redigera etiketter", + "Edit processing activity": "Redigera behandlingsaktivitet", + "Edit tags for {name}": "Redigera taggar för {name}", + "Edit verwerkingsactiviteit": "Redigera verwerkingsactiviteit", + "Edit view": "Redigera vy", + "Edit view details": "Redigera vydetaljer", + "Effectiveness": "Effektivitet", + "Email": "E-post", + "Email Addresses": "E-postadresser", + "Email digest frequency": "Frekvens för e-postsammanfattning", + "Emails": "E-postmeddelanden", + "Embedding Model": "Inbäddningsmodell", + "Embedding Provider": "Inbäddningsleverantör", + "Embedding generated successfully": "Inbäddning genererades", + "Embedding provider connection successful!": "Anslutning till inbäddningsleverantören lyckades!", + "Embedding test failed: {error}": "Inbäddningstest misslyckades: {error}", + "Enable": "Aktivera", + "Enable RAG": "Aktivera RAG", + "Enable auto-creation": "Aktivera automatisk skapande", + "Enable automatic file vectorization": "Aktivera automatisk filvektorisering", + "Enable automatic object vectorization": "Aktivera automatisk objektvektorisering", + "Enable automatic synchronization": "Aktivera automatisk synkronisering", + "Enable calendar provider": "Aktivera kalenderleverantör", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Aktivera detaljerad loggning för SOLR-operationer (rekommenderas för felsökning)", + "Enable faceting": "Aktivera fasettning", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Aktivera eller inaktivera LLM-funktioner. Konfigurera leverantörer och modeller med knappen LLM-konfiguration ovan.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Aktivera eller inaktivera LLM-funktioner. Konfigurera leverantörer och modeller med knappen LLM-konfiguration ovan.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Aktivera eller inaktivera SOLR-sökintegration. Konfigurera anslutningsinställningar med knappen Anslutningsinställningar ovan.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Aktivera eller inaktivera SOLR-sökintegration. Konfigurera anslutningsinställningar med knappen anslutningsinställningar ovan.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Aktivera eller inaktivera n8n-arbetsflödesintegration. Konfigurera anslutningsinställningar nedan.", + "Enable or disable this webhook": "Aktivera eller inaktivera den här webhooken", + "Enable tools that allow the agent to interact with data through function calling.": "Aktivera verktyg som låter agenten interagera med data via funktionsanrop.", + "Enable vectorization for all existing and future views (may increase costs)": "Aktivera vektorisering för alla befintliga och framtida vyer (kan öka kostnader)", + "Enable write-back to target objects": "Aktivera återskrivning till målobjekt", + "Enabled": "Aktiverad", + "End Date Field": "Fält för slutdatum", + "Endpoint Path*": "Slutpunktssökväg*", + "Endpoints": "Slutpunkter", + "Engine not found": "Motor hittades inte", + "Enter ConfigSet name": "Ange ConfigSet-namn", + "Enter Nextcloud usernames to grant access to this private agent": "Ange Nextcloud-användarnamn för att ge åtkomst till den här privata agenten", + "Enter agent description (optional)": "Ange agentbeskrivning (valfri)", + "Enter agent name": "Ange agentnamn", + "Enter application description (optional)": "Ange applikationsbeskrivning (valfri)", + "Enter application name": "Ange applikationsnamn", + "Enter collection name": "Ange samlingsnamn", + "Enter configuration description (optional)": "Ange konfigurationsbeskrivning (valfri)", + "Enter configuration title": "Ange konfigurationstitel", + "Enter description (optional)...": "Ange beskrivning (valfri)...", + "Enter new collection name": "Ange nytt samlingsnamn", + "Enter object ID": "Ange objekt-ID", + "Enter organisation description (optional)": "Ange organisationsbeskrivning (valfri)", + "Enter organisation name": "Ange organisationsnamn", + "Enter search term": "Ange sökterm", + "Enter search terms or leave empty to browse all": "Ange söktermer eller lämna tomt för att bläddra bland allt", + "Enter system prompt for the agent": "Ange systemuppmaning för agenten", + "Enter username and press Enter": "Ange användarnamn och tryck Enter", + "Enter view name...": "Ange vynamn...", + "Enter webhook name": "Ange webhook-namn", + "Enter your API key": "Ange din API-nyckel", + "Enter your object here...": "Ange ditt objekt här...", + "Enter your schema here...": "Ange ditt schema här...", + "Entities": "Entiteter", + "Entities detected": "Entiteter identifierade", + "Entity ID: {id}": "Entitets-ID: {id}", + "Entity Information": "Entitetsinformation", + "Entity Recognition": "Entitetsigenkänning", + "Entity Recognition Method": "Metod för entitetsigenkänning", + "Entity deleted successfully": "Entiteten togs bort", + "Entity not found": "Entitet hittades inte", + "Entries to be deleted:": "Poster att ta bort:", + "Enum Issue": "Enum-problem", + "Enum constraint is missing": "Enum-begränsning saknas", + "Erase (Art 17)": "Radera (Art 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Radera {count} objekt för det här subjektet? Denna åtgärd loggas i revisionsloggen.", + "Erasure complete": "Radering slutförd", + "Erasure preview": "Förhandsvisning av radering", + "Error": "Fel", + "Error Details": "Felinformation", + "Error Information": "Felinformation", + "Error Message": "Felmeddelande", + "Error deleting audit trails: {error}": "Fel vid borttagning av revisionsloggar: {error}", + "Error deleting search trails: {error}": "Fel vid borttagning av sökloggar: {error}", + "Error loading application": "Fel vid inläsning av applikation", + "Error loading audit trails": "Fel vid inläsning av revisionsloggar", + "Error loading entity": "Fel vid inläsning av entitet", + "Error loading search trails": "Fel vid inläsning av sökloggar", + "Estimated Batches:": "Uppskattade batchar:", + "Estimated Cost:": "Uppskattad kostnad:", + "Estimated Duration:": "Uppskattad varaktighet:", + "Event": "Händelse", + "Event Property for Payload": "Händelseegenskap för nyttolast", + "Events": "Händelser", + "Every schema with detected PII has a processing-activity annotation.": "Varje schema med identifierat PII har en anteckning om behandlingsaktivitet.", + "Example": "Exempel", + "Example Value": "Exempelvärde", + "Example value for this property": "Exempelvärde för den här egenskapen", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Exklusivt maximum", + "Exclusive Minimum": "Exklusivt minimum", + "Exclusive maximum": "Exklusivt maximum", + "Exclusive minimum": "Exklusivt minimum", + "Execution Mode": "Körningsläge", + "Execution Time": "Körningstid", + "Execution Time Range": "Körningstidsintervall", + "Existing Improvements": "Befintliga förbättringar", + "Expand details": "Expandera detaljer", + "Expiration": "Utgång", + "Expires": "Upphör", + "Expires in (e.g., 90d)": "Upphör om (t.ex. 90d)", + "Exponential": "Exponentiell", + "Export": "Exportera", + "Export \"{schema}\" objects from \"{register}\"": "Exportera \"{schema}\"-objekt från \"{register}\"", + "Export Configuration": "Exportkonfiguration", + "Export Format:": "Exportformat:", + "Export Objects": "Exportera objekt", + "Export completed successfully": "Export slutförd", + "Export downloaded successfully": "Export hämtad", + "Export failed: {error}": "Export misslyckades: {error}", + "Export is rate limited. Please try again later.": "Exporten är hastighetsbegränsad. Försök igen senare.", + "Export my data": "Exportera mina data", + "Export, view, or delete audit trails": "Exportera, visa eller ta bort revisionsloggar", + "Exporting...": "Exporterar...", + "Extend Schema": "Utöka schema", + "Extended": "Utökad", + "Extensions": "Tillägg", + "Extra Query Parameters": "Extra frågeparametrar", + "Extract Now": "Extrahera nu", + "Extract Pending Files": "Extrahera väntande filer", + "Extract now": "Extrahera nu", + "Extract properties from each object": "Extrahera egenskaper från varje objekt", + "Extracted At": "Extraherad", + "Extracted at": "Extraherad", + "Extraction": "Extraktion", + "Extraction Mode": "Extraktionsläge", + "Extraction Scope": "Extraktionsomfång", + "Extraction Status": "Extraktionsstatus", + "Extraction error for {file}: {error}": "Extraktionsfel för {file}: {error}", + "Facet Description": "Fasettbeskrivning", + "Facet Order": "Fasettordning", + "Facet Title": "Fasettitel", + "Facet Type": "Fasetttyp", + "Facet configuration updated successfully": "Fasettkonfiguration uppdaterades", + "Facetable": "Fasettbar", + "Faceting Configuration:": "Fasettkonfiguration:", + "Facets discovered and configured successfully": "Fasetter identifierades och konfigurerades", + "Facets discovered successfully": "Fasetter identifierades", + "Failed": "Misslyckades", + "Failed to add attachment to object": "Det gick inte att lägga till bilaga till objekt", + "Failed to add user to organisation": "Det gick inte att lägga till användare i organisation", + "Failed to analyze schema properties": "Det gick inte att analysera schemaegenskaper", + "Failed to archive conversation": "Det gick inte att arkivera konversation", + "Failed to calculate sizes": "Det gick inte att beräkna storlekar", + "Failed to cancel deactivation": "Det gick inte att avbryta inaktivering", + "Failed to change active organisation: {error}": "Det gick inte att ändra aktiv organisation: {error}", + "Failed to change password": "Det gick inte att ändra lösenord", + "Failed to check version: {error}": "Det gick inte att kontrollera version: {error}", + "Failed to clear audit trails: {error}": "Det gick inte att rensa revisionsloggar: {error}", + "Failed to clear blob objects: {error}": "Det gick inte att rensa blob-objekt: {error}", + "Failed to clear cache: {error}": "Det gick inte att rensa cache: {error}", + "Failed to clear collection": "Det gick inte att rensa samling", + "Failed to clear conversation": "Det gick inte att rensa konversation", + "Failed to clear embeddings: {error}": "Det gick inte att rensa inbäddningar: {error}", + "Failed to clear search trails: {error}": "Det gick inte att rensa sökloggar: {error}", + "Failed to clear {type} cache: {error}": "Det gick inte att rensa {type}-cache: {error}", + "Failed to copy changes": "Det gick inte att kopiera ändringar", + "Failed to copy data": "Det gick inte att kopiera data", + "Failed to copy data to clipboard": "Det gick inte att kopiera data till urklipp", + "Failed to copy to clipboard": "Det gick inte att kopiera till urklipp", + "Failed to copy token": "Det gick inte att kopiera token", + "Failed to create conversation": "Det gick inte att skapa konversation", + "Failed to create missing SOLR fields: {error}": "Det gick inte att skapa saknade SOLR-fält: {error}", + "Failed to create or find project": "Det gick inte att skapa eller hitta projekt", + "Failed to create token": "Det gick inte att skapa token", + "Failed to delete SOLR collection: {error}": "Det gick inte att ta bort SOLR-samling: {error}", + "Failed to delete collection": "Det gick inte att ta bort samling", + "Failed to delete conversation": "Det gick inte att ta bort konversation", + "Failed to delete objects for {schema}: {error}": "Det gick inte att ta bort objekt för {schema}: {error}", + "Failed to delete webhook": "Det gick inte att ta bort webhook", + "Failed to depublish register: {error}": "Det gick inte att avpublicera register: {error}", + "Failed to depublish schema: {error}": "Det gick inte att avpublicera schema: {error}", + "Failed to depublish: {error}": "Det gick inte att avpublicera: {error}", + "Failed to discover files: {error}": "Det gick inte att identifiera filer: {error}", + "Failed to download API specification": "Det gick inte att hämta API-specifikation", + "Failed to export data": "Det gick inte att exportera data", + "Failed to export objects": "Det gick inte att exportera objekt", + "Failed to fetch conversation history": "Det gick inte att hämta konversationshistorik", + "Failed to fix mismatched SOLR fields: {error}": "Det gick inte att åtgärda felmatchade SOLR-fält: {error}", + "Failed to get SOLR field configuration": "Det gick inte att hämta SOLR-fältskonfiguration", + "Failed to get chat statistics": "Det gick inte att hämta chattstatistik", + "Failed to get database information: %s": "Det gick inte att hämta databasinformation: %s", + "Failed to get entity categories": "Det gick inte att hämta entitetskategorier", + "Failed to get entity statistics": "Det gick inte att hämta entitetsstatistik", + "Failed to get entity types": "Det gick inte att hämta entitetstyper", + "Failed to get response: {error}": "Det gick inte att hämta svar: {error}", + "Failed to invalidate app store cache: {error}": "Det gick inte att ogiltigförklara App Store-cache: {error}", + "Failed to leave organisation: {error}": "Det gick inte att lämna organisation: {error}", + "Failed to link object": "Det gick inte att länka objekt", + "Failed to load Deck cards": "Det gick inte att läsa in Deck-kort", + "Failed to load LLM configuration": "Det gick inte att läsa in LLM-konfiguration", + "Failed to load Nextcloud groups": "Det gick inte att läsa in Nextcloud-grupper", + "Failed to load cache statistics: {error}": "Det gick inte att läsa in cache-statistik: {error}", + "Failed to load conversation": "Det gick inte att läsa in konversation", + "Failed to load entities": "Det gick inte att läsa in entiteter", + "Failed to load entity": "Det gick inte att läsa in entitet", + "Failed to load extraction data": "Det gick inte att läsa in extraktionsdata", + "Failed to load files": "Det gick inte att läsa in filer", + "Failed to load linked contacts": "Det gick inte att läsa in länkade kontakter", + "Failed to load linked emails": "Det gick inte att läsa in länkade e-postmeddelanden", + "Failed to load linked events": "Det gick inte att läsa in länkade händelser", + "Failed to load organisations": "Det gick inte att läsa in organisationer", + "Failed to load register data": "Det gick inte att läsa in registerdata", + "Failed to load related entities": "Det gick inte att läsa in relaterade entiteter", + "Failed to load settings: {error}": "Det gick inte att läsa in inställningar: {error}", + "Failed to load statistics: {error}": "Det gick inte att läsa in statistik: {error}", + "Failed to load templates": "Det gick inte att läsa in mallar", + "Failed to load view: {error}": "Det gick inte att läsa in vy: {error}", + "Failed to load webhooks": "Det gick inte att läsa in webhooks", + "Failed to load workflows": "Det gick inte att läsa in arbetsflöden", + "Failed to process message": "Det gick inte att bearbeta meddelande", + "Failed to publish register: {error}": "Det gick inte att publicera register: {error}", + "Failed to publish schema: {error}": "Det gick inte att publicera schema: {error}", + "Failed to publish: {error}": "Det gick inte att publicera: {error}", + "Failed to refresh database information": "Det gick inte att uppdatera databasinformation", + "Failed to reindex collection": "Det gick inte att omindexera samling", + "Failed to remove avatar": "Det gick inte att ta bort avatar", + "Failed to remove link": "Det gick inte att ta bort länk", + "Failed to remove member: {error}": "Det gick inte att ta bort medlem: {error}", + "Failed to remove schema {schema}: {error}": "Det gick inte att ta bort schemat {schema}: {error}", + "Failed to remove user from organisation": "Det gick inte att ta bort användare från organisation", + "Failed to rename conversation": "Det gick inte att byta namn på konversation", + "Failed to request deactivation": "Det gick inte att begära inaktivering", + "Failed to restore conversation": "Det gick inte att återställa konversation", + "Failed to retrieve extraction status.": "Det gick inte att hämta extraktionsstatus.", + "Failed to retrieve objects for file.": "Det gick inte att hämta objekt för fil.", + "Failed to retry extraction": "Det gick inte att försöka extrahera igen", + "Failed to retry webhook": "Det gick inte att försöka webhooket igen", + "Failed to revoke token": "Det gick inte att återkalla token", + "Failed to save GitHub token": "Det gick inte att spara GitHub-token", + "Failed to save GitLab URL": "Det gick inte att spara GitLab-URL", + "Failed to save GitLab token": "Det gick inte att spara GitLab-token", + "Failed to save LLM settings: {error}": "Det gick inte att spara LLM-inställningar: {error}", + "Failed to save additional feedback": "Det gick inte att spara ytterligare feedback", + "Failed to save configuration.": "Det gick inte att spara konfiguration.", + "Failed to save configuration: {error}": "Det gick inte att spara konfiguration: {error}", + "Failed to save facet configuration": "Det gick inte att spara fasettkonfiguration", + "Failed to save feedback": "Det gick inte att spara feedback", + "Failed to save file settings: {error}": "Det gick inte att spara filinställningar: {error}", + "Failed to save n8n configuration": "Det gick inte att spara n8n-konfiguration", + "Failed to save preferences": "Det gick inte att spara inställningar", + "Failed to save roles": "Det gick inte att spara roller", + "Failed to save settings": "Det gick inte att spara inställningar", + "Failed to save settings: {error}": "Det gick inte att spara inställningar: {error}", + "Failed to save view: {error}": "Det gick inte att spara vy: {error}", + "Failed to save warmup interval: {error}": "Det gick inte att spara uppvärmningsintervall: {error}", + "Failed to save webhook": "Det gick inte att spara webhook", + "Failed to send feedback": "Det gick inte att skicka feedback", + "Failed to set active organisation: {error}": "Det gick inte att ange aktiv organisation: {error}", + "Failed to setup SOLR: {error}": "Det gick inte att konfigurera SOLR: {error}", + "Failed to start file warmup: {error}": "Det gick inte att starta filuppvärmning: {error}", + "Failed to start processing {type} files: {error}": "Det gick inte att starta bearbetning av {type}-filer: {error}", + "Failed to start vectorization: {error}": "Det gick inte att starta vektorisering: {error}", + "Failed to switch organisation: {error}": "Det gick inte att byta organisation: {error}", + "Failed to sync magic table for {schema}: {error}": "Det gick inte att synkronisera magisk tabell för {schema}: {error}", + "Failed to test SOLR connection: {error}": "Det gick inte att testa SOLR-anslutning: {error}", + "Failed to test webhook": "Det gick inte att testa webhook", + "Failed to update LLM settings: {error}": "Det gick inte att uppdatera LLM-inställningar: {error}", + "Failed to update Multitenancy settings: {error}": "Det gick inte att uppdatera inställningar för multitenancy: {error}", + "Failed to update RBAC settings: {error}": "Det gick inte att uppdatera RBAC-inställningar: {error}", + "Failed to update Retention settings: {error}": "Det gick inte att uppdatera lagringsinställningar: {error}", + "Failed to update SOLR settings: {error}": "Det gick inte att uppdatera SOLR-inställningar: {error}", + "Failed to update favorite status": "Det gick inte att uppdatera favoritstatus", + "Failed to update schema configuration for {schema}: {error}": "Det gick inte att uppdatera schemakonfiguration för {schema}: {error}", + "Failed to update schema properties": "Det gick inte att uppdatera schemaegenskaper", + "Failed to update view": "Det gick inte att uppdatera vy", + "Failed to update view: {error}": "Det gick inte att uppdatera vy: {error}", + "Failed to update webhook": "Det gick inte att uppdatera webhook", + "Failed to upload avatar": "Det gick inte att ladda upp avatar", + "Failed to validate {schema}: {error}": "Det gick inte att validera {schema}: {error}", + "Failed to vectorize files: {error}": "Det gick inte att vektorisera filer: {error}", + "Failed to warmup names cache: {error}": "Det gick inte att värma upp namnscache: {error}", + "Federation": "Federation", + "Feedback recorded": "Feedback registrerad", + "Fewer sources (1-3):": "Färre källor (1-3):", + "Field": "Fält", + "Fields to Display": "Fält att visa", + "File": "Fil", + "File #{id}": "Fil #{id}", + "File Chunk Prediction": "Prediktion av filblock", + "File Collection": "Filsamling", + "File Handling": "Filhantering", + "File Location": "Filplats", + "File Management": "Filhantering", + "File Name": "Filnamn", + "File Path": "Filsökväg", + "File Type Selection": "Val av filtyp", + "File Types to Process": "Filtyper att bearbeta", + "File Vectorization": "Filvektorisering", + "File Warmup": "Filuppvärmning", + "File actions menu": "Meny för filåtgärder", + "File anonymized successfully": "Filen anonymiserades", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Vektorisering av filblock är ännu inte implementerad. Blocken är klara och lagrade, men vektoriseringstjänsten är under utveckling.", + "File collection not configured": "Filsamling är inte konfigurerad", + "File discovery completed": "Filidentifiering slutförd", + "File extraction completed": "Filextraktion slutförd", + "File extraction queued": "Filextraktion köad", + "File indexed successfully": "Filen indexerades", + "File is already anonymized": "Filen är redan anonymiserad", + "File name": "Filnamn", + "File not found": "Fil hittades inte", + "File settings saved successfully": "Filinställningar sparades", + "File settings updated successfully": "Filinställningar uppdaterades", + "File sources": "Filkällor", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Filvektorisering slutförd! {vectorized} block vektoriserades från {files} filer. {failed} misslyckades.", + "File vectorization configuration saved successfully": "Konfiguration för filvektorisering sparades", + "File vectorization started. Check the statistics section for progress.": "Filvektorisering startad. Kontrollera statistikavsnittet för förlopp.", + "File warmup completed": "Filuppvärmning slutförd", + "Files": "Filer", + "Files added successfully": "Filer tillagda", + "Files with Completed Extraction:": "Filer med slutförd extraktion:", + "Files → fileCollection, Objects → objectCollection": "Filer → fileCollection, Objekt → objectCollection", + "Files → filecollection, objects → objectcollection": "Filer → filecollection, objekt → objectcollection", + "Filter Audit Trails": "Filtrera revisionsloggar", + "Filter Deleted Items": "Filtrera borttagna objekt", + "Filter Objects": "Filtrera objekt", + "Filter Properties": "Filtrera egenskaper", + "Filter Search Trails": "Filtrera sökloggar", + "Filter Statistics": "Filtrera statistik", + "Filter and analyze search trail entries": "Filtrera och analysera sökloggsposter", + "Filter and manage audit trail entries": "Filtrera och hantera revisionsloggsposter", + "Filter and manage soft deleted items": "Filtrera och hantera mjukraderade objekt", + "Filter and search entities": "Filtrera och sök entiteter", + "Filter by object ID": "Filtrera efter objekt-ID", + "Filter by search term": "Filtrera efter sökterm", + "Filter by type": "Filtrera efter typ", + "Filter by webhook": "Filtrera efter webhook", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filterdata laddades automatiskt. Använd filtren nedan för att förfina sökningen.", + "Filter fields...": "Filtrera fält...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Filtrera webhook-utlösare efter nyttolastegenskaper (en per rad, format: nyckel: värde)", + "Filtered": "Filtrerad", + "Filters": "Filter", + "Finalizing setup...": "Slutför installation...", + "Fireworks AI Chat Settings": "Fireworks AI chattinställningar", + "Fireworks AI Embedding Configuration": "Fireworks AI inbäddningskonfiguration", + "First": "Första", + "Fixed": "Fast", + "For chat and retrieval-augmented generation": "För chatt och hämtningsförstärkt generering", + "For vector embeddings and semantic search": "För vektorinbäddningar och semantisk sökning", + "Format": "Format", + "Format Issue": "Formatproblem", + "Format constraint is missing": "Formatbegränsning saknas", + "From (e.g. -7 days or 2025-01-01)": "Från (t.ex. -7 dagar eller 2025-01-01)", + "From Date": "Från datum", + "From date": "Från datum", + "Full data copied to clipboard": "Fullständig data kopierades till urklipp", + "Function Tools": "Funktionsverktyg", + "General Issue": "Allmänt problem", + "Generate recommendations and confidence scores": "Generera rekommendationer och konfidenspoäng", + "Generate report": "Generera rapport", + "Generate the verantwoordingsdocument": "Generera verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Generera vektorer omedelbart när nya objekt skapas", + "Generated: {time}": "Genererad: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Få realtidsinsikter om din organisations datahälsa med fokus på register, schemadefinitioner och objektlagring och -användning.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Få realtidsinsikter om din organisations datahälsa, tillväxttrender och kvalitetsmätvärden.", + "GitHub Personal Access Token": "GitHub personlig åtkomsttoken", + "GitHub token is valid": "GitHub-token är giltig", + "GitHub token is valid! Username: {username}": "GitHub-token är giltig! Användarnamn: {username}", + "GitHub token removed successfully": "GitHub-token har tagits bort", + "GitHub token saved successfully": "GitHub-token sparades", + "GitHub token test failed: {message}": "GitHub-tokentest misslyckades: {message}", + "GitLab Personal Access Token": "GitLab personlig åtkomsttoken", + "GitLab URL saved successfully": "GitLab-URL sparades", + "GitLab token is valid": "GitLab-token är giltig", + "GitLab token is valid! Username: {username}": "GitLab-token är giltig! Användarnamn: {username}", + "GitLab token saved successfully": "GitLab-token sparades", + "GitLab token test failed: {message}": "GitLab-tokentest misslyckades: {message}", + "Go to linked Register": "Gå till länkat register", + "Go to linked Schema": "Gå till länkat schema", + "Go to organisation": "Gå till organisation", + "Grant additional permissions beyond standard CRUD operations": "Bevilja ytterligare behörigheter utöver standard CRUD-operationer", + "Group Quota": "Gruppkvot", + "Groups": "Grupper", + "HTML preview": "HTML-förhandsgranskning", + "HTTP Method": "HTTP-metod", + "HTTP method used to send webhook requests": "HTTP-metod som används för att skicka webhook-förfrågningar", + "Headers": "Huvuden", + "Health": "Hälsa", + "Heartbeat successful - connection kept alive": "Heartbeat lyckades – anslutning hållen aktiv", + "Helpful": "Hjälpsam", + "Here you can configure the details for various connections.": "Här kan du konfigurera detaljer för olika anslutningar.", + "Here you can configure the details for your organisation.": "Här kan du konfigurera detaljer för din organisation.", + "Hide": "Dölj", + "Hide Filters": "Dölj filter", + "Hide in collection view": "Dölj i samlingsvy", + "Hide in forms": "Dölj i formulär", + "Hide in list view": "Dölj i listvy", + "High": "Hög", + "High Confidence": "Hög konfidensgrad", + "Host": "Värd", + "Hourly": "Varje timme", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Hur djupt kapslade objektegenskaper ska traverseras (1-20). Högre värden fångar mer detaljer men ökar vektorstorleken.", + "How often to check for updates (1-168 hours)": "Hur ofta uppdateringar ska kontrolleras (1-168 timmar)", + "How to handle retries for failed webhook deliveries": "Hur nya försök för misslyckade webhook-leveranser ska hanteras", + "IBANs": "IBAN-nummer", + "ID": "ID", + "ID of input mapping (optional)": "ID för ingångsmappning (valfri)", + "ID of output mapping (optional)": "ID för utgångsmappning (valfri)", + "ID of the target resource": "ID för målresursen", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "IP-hastighetsbegränsningar rensades", + "Identify properties not in the schema": "Identifiera egenskaper som inte finns i schemat", + "Immutable": "Oföränderlig", + "Import": "Importera", + "Import Configuration": "Importkonfiguration", + "Import Data into Register": "Importera data till register", + "Import successful": "Import lyckades", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Importera mallen report-bundle.json för att få `reports`-registret, skapa sedan din första instrumentpanel via standardobjektgränssnittet. Instrumentpaneler deklarerar sina widgetar i JSON och renderaren förser varje widget med liveaggregationsdata.", + "Improved Property": "Förbättrad egenskap", + "In use": "Används", + "Inactive": "Inaktiv", + "Inactive organisations cannot be used": "Inaktiva organisationer kan inte användas", + "Include IDs and names of related objects for better contextual search": "Inkludera ID:n och namn på relaterade objekt för bättre kontextuell sökning", + "Include ids and names of related objects for better contextual search": "Inkludera id:n och namn på relaterade objekt för bättre kontextuell sökning", + "Include related object references": "Inkludera relaterade objektreferenser", + "Include schema and register metadata": "Inkludera schema- och registermetadata", + "Index": "Index", + "Information about the current OpenRegister installation": "Information om den aktuella OpenRegister-installationen", + "Inherited from register": "Ärvd från register", + "Inherits permissions from register": "Ärver behörigheter från register", + "Initialization failed": "Initiering misslyckades", + "Initialize Project": "Initiera projekt", + "Initializing SOLR setup...": "Initierar SOLR-konfiguration...", + "Input Mapping": "Ingångsmappning", + "Inspect Fields": "Granska fält", + "Inspect Index": "Granska index", + "Inspect SOLR Index": "Granska SOLR-index", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Installera notify_push-appen från Nextcloud App Store för att aktivera realtidsuppdateringar.", + "Integrations": "Integrationer", + "Internet location (URL) of this installation": "Internetplats (URL) för den här installationen", + "Interval": "Intervall", + "Invalid": "Ogiltig", + "Invalid batch size. Must be between 1 and 5000": "Ogiltig batchstorlek. Måste vara mellan 1 och 5000", + "Invalid feedback type": "Ogiltig feedbacktyp", + "Invalid field name provided": "Ogiltigt fältnamn angivet", + "Invalid maxObjects. Must be 0 (all) or positive number": "Ogiltigt maxObjects. Måste vara 0 (alla) eller positivt tal", + "Invalid:": "Ogiltig:", + "Invite Users": "Bjud in användare", + "Invited Users": "Inbjudna användare", + "Inzage (Art 15)": "Inzage (Art 15)", + "Inzage results": "Inzage-resultat", + "Issue": "Problem", + "Items per page:": "Objekt per sida:", + "Just now": "Just nu", + "Key": "Nyckel", + "LLM Configuration": "LLM-konfiguration", + "LLM actions menu": "LLM-åtgärdsmeny", + "LLM configuration saved successfully": "LLM-konfiguration sparades", + "LLM features disabled": "LLM-funktioner inaktiverade", + "LLM features enabled": "LLM-funktioner aktiverade", + "LLM must be enabled with an embedding provider configured": "LLM måste aktiveras med en inbäddningsleverantör konfigurerad", + "LLM settings saved successfully": "LLM-inställningar sparades", + "LLM settings updated successfully": "LLM-inställningar uppdaterades", + "Label": "Etikett", + "Labels": "Etiketter", + "Languages": "Språk", + "Last": "Sista", + "Last Triggered": "Senast utlöst", + "Leave empty to allow all users access": "Lämna tomt för att ge alla användare åtkomst", + "Leave off for auto-detection from property format.": "Lämna av för automatisk identifiering från egenskapsformat.", + "Left organisation successfully": "Lämnade organisation", + "Legal basis *": "Rättslig grund *", + "Length Range:": "Längdintervall:", + "Linear": "Linjär", + "Link": "Länk", + "Link an email from the Mail app sidebar to associate it with this object.": "Länka ett e-postmeddelande från sidopanelen i e-postappen för att koppla det till det här objektet.", + "Link an object to see it here.": "Länka ett objekt för att se det här.", + "Link another object": "Länka ett annat objekt", + "Link existing event": "Länka befintlig händelse", + "Link removed": "Länk borttagen", + "Link to Object": "Länk till objekt", + "Link to {name}": "Länk till {name}", + "Linked Objects": "Länkade objekt", + "Linked by {user}": "Länkad av {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Länkade e-postmeddelanden, kalenderhändelser, kontakter och Deck-kort visas här.", + "Linked to {name}": "Länkad till {name}", + "Load Advanced Filters": "Läs in avancerade filter", + "Load advanced filters with live data from your search index": "Läs in avancerade filter med livedata från ditt sökindex", + "Load more": "Läs in mer", + "Load view": "Läs in vy", + "Loading API tokens...": "Läser in API-token...", + "Loading ConfigSets...": "Läser in ConfigSets...", + "Loading LLM configuration...": "Läser in LLM-konfiguration...", + "Loading RBAC settings...": "Läser in RBAC-inställningar...", + "Loading activity...": "Läser in aktivitet...", + "Loading advanced filters...": "Läser in avancerade filter...", + "Loading agents...": "Läser in agenter...", + "Loading application details...": "Läser in applikationsdetaljer...", + "Loading applications...": "Läser in applikationer...", + "Loading archived conversations...": "Läser in arkiverade konversationer...", + "Loading audit trails...": "Läser in revisionsloggar...", + "Loading available tools...": "Läser in tillgängliga verktyg...", + "Loading cache statistics...": "Läser in cache-statistik...", + "Loading collections...": "Läser in samlingar...", + "Loading configurations...": "Läser in konfigurationer...", + "Loading conversation...": "Läser in konversation...", + "Loading conversations...": "Läser in konversationer...", + "Loading deleted items...": "Läser in borttagna objekt...", + "Loading endpoints": "Läser in slutpunkter", + "Loading entities...": "Läser in entiteter...", + "Loading events...": "Läser in händelser...", + "Loading filter data...": "Läser in filterdata...", + "Loading groups...": "Läser in grupper...", + "Loading linked objects...": "Läser in länkade objekt...", + "Loading log details...": "Läser in loggdetaljer...", + "Loading multitenancy settings...": "Läser in multitenancy-inställningar...", + "Loading n8n configuration...": "Läser in n8n-konfiguration...", + "Loading permission matrix...": "Läser in behörighetsmatris...", + "Loading preferences...": "Läser in inställningar...", + "Loading register data...": "Läser in registerdata...", + "Loading registers...": "Läser in register...", + "Loading retention settings...": "Läser in lagringsinställningar...", + "Loading schemas...": "Läser in scheman...", + "Loading search configuration...": "Läser in sökkonfiguration...", + "Loading search trails...": "Läser in sökloggar...", + "Loading sources...": "Läser in källor...", + "Loading statistics...": "Läser in statistik...", + "Loading text extraction settings...": "Läser in inställningar för textextraktion...", + "Loading tokens...": "Läser in token...", + "Loading user groups...": "Läser in användargrupper...", + "Loading users...": "Läser in användare...", + "Loading views...": "Läser in vyer...", + "Loading...": "Läser in...", + "Local": "Lokal", + "Local Version": "Lokal version", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Hitta alla objekt som refererar till en registrerad (Art 15 inzage), förhandsgranska en radering (Art 17 vergetelheid), eller exportera deras data (Art 20 portabiliteit).", + "Location": "Plats", + "Location Field": "Platsfält", + "Locations": "Platser", + "Locked": "Låst", + "Locked:": "Låst:", + "Login successful": "Inloggning lyckades", + "Logs": "Loggar", + "Low": "Låg", + "Low Confidence": "Låg konfidensgrad", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Lägre nummer visas först i filterpanelen. Lämna tomt för automatisk ordning.", + "Lucene Version:": "Lucene-version:", + "Magic table sync completed for {schema}": "Synkronisering av magisk tabell slutförd för {schema}", + "Magic table synchronized successfully": "Magisk tabell synkroniserades", + "Mail Integration": "E-postintegration", + "Mail integration is not available": "E-postintegration är inte tillgänglig", + "Make this installation discoverable within the federation network.": "Gör den här installationen synlig i federationsnätverket.", + "Manage Organisation Roles": "Hantera organisationsroller", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Hantera SOLR-samlingar (datalager) och tilldela dem för objekt och filer.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Hantera SOLR ConfigSets (konfigurationsmallar) för dina samlingar.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Hantera SOLR-samlingar (datalager) och tilldela dem för objekt och filer.", + "Manage SOLR configsets (configuration templates) for your collections.": "Hantera SOLR configsets (konfigurationsmallar) för dina samlingar.", + "Manage and configure agents for automated tasks": "Hantera och konfigurera agenter för automatiserade uppgifter", + "Manage and monitor file text extraction status": "Hantera och övervaka status för filtextextraktion", + "Manage and restore soft deleted items from your registers": "Hantera och återställ mjukraderade objekt från dina register", + "Manage and view detected entities from files and objects": "Hantera och visa identifierade entiteter från filer och objekt", + "Manage document templates and themes": "Hantera dokumentmallar och teman", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Hantera n8n-arbetsflöden för OpenRegister-automatisering. Arbetsflöden lagras i det konfigurerade projektet.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Hantera behandlingsaktiviteter, kör begäranden om registrerades åtkomst och granska efterlevnad enligt EU GDPR / nederländska AVG.", + "Manage webhooks for event-driven integrations": "Hantera webhooks för händelsedrivna integrationer", + "Manage your account settings, security, and personal data.": "Hantera dina kontoinställningar, säkerhet och personuppgifter.", + "Manage your applications and modules": "Hantera dina applikationer och moduler", + "Manage your chat conversations": "Hantera dina chattkonversationer", + "Manage your data registers and their configurations": "Hantera dina dataregister och deras konfigurationer", + "Manage your data schemas and their properties": "Hantera dina datascheman och deras egenskaper", + "Manage your data sources and their configurations": "Hantera dina datakällor och deras konfigurationer", + "Manage your organisations and switch between them": "Hantera dina organisationer och växla mellan dem", + "Manage your saved search configurations": "Hantera dina sparade sökkonfigurationer", + "Manage your system configurations and settings": "Hantera dina systemkonfigurationer och inställningar", + "Managed": "Hanterad", + "Mappings": "Mappningar", + "Mass validation completed successfully": "Massvalidering slutförd", + "Mass validation failed: {error}": "Massvalidering misslyckades: {error}", + "Max Files (0 = all)": "Max filer (0 = alla)", + "Max Length": "Maxlängd", + "Max Length:": "Maxlängd:", + "Max Objects (0 = all)": "Max objekt (0 = alla)", + "Max Retries": "Max försök", + "Max Shards/Node": "Max shards/nod", + "Max Tokens": "Max tokens", + "Max execution time (ms)": "Max körningstid (ms)", + "Max ms": "Max ms", + "Max result count": "Max antal resultat", + "Max results": "Max resultat", + "Maximum": "Maximum", + "Maximum File Size (MB)": "Maximal filstorlek (MB)", + "Maximum Nesting Depth": "Maximal kapslingsnivå", + "Maximum length": "Maxlängd", + "Maximum length constraint is missing": "Maxlängdsbegränsning saknas", + "Maximum length is too restrictive": "Maxlängden är för restriktiv", + "Maximum number of files to process. Set to 0 to process all files.": "Maximalt antal filer att bearbeta. Ange 0 för att bearbeta alla filer.", + "Maximum number of items": "Maximalt antal objekt", + "Maximum number of objects to process. Set to 0 to process all objects.": "Maximalt antal objekt att bearbeta. Ange 0 för att bearbeta alla objekt.", + "Maximum number of retry attempts for failed deliveries": "Maximalt antal återförsök för misslyckade leveranser", + "Maximum time to wait before committing changes": "Maximal väntetid innan ändringar bekräftas", + "Maximum tokens to generate": "Maximalt antal tokens att generera", + "Maximum value": "Maxvärde", + "Maximum value constraint is missing": "Maxvärdebegränsning saknas", + "Maximum value is too restrictive": "Maxvärdet är för restriktivt", + "Medium": "Medel", + "Medium Confidence": "Medel konfidensgrad", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Medelkomplexa frågor: Sökningar med viss filtrering eller flera parametrar (t.ex. datumintervall, specifika register/scheman)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Medelkomplexa frågor: sökningar med viss filtrering eller flera parametrar (t.ex. datumintervall, specifika register/scheman)", + "Member removed successfully": "Medlem borttagen", + "Members": "Medlemmar", + "Members ({count})": "Medlemmar ({count})", + "Members of selected groups can access this view": "Medlemmar i valda grupper kan komma åt den här vyn", + "Members:": "Medlemmar:", + "Memory prediction calculated successfully": "Minnesprediktion beräknad", + "Message content is required": "Meddelandeinnehåll krävs", + "Message does not belong to this conversation": "Meddelandet tillhör inte den här konversationen", + "Message not found": "Meddelande hittades inte", + "Metadata": "Metadata", + "Metadata Filters": "Metadatafilter", + "Method": "Metod", + "Method*": "Metod*", + "Method: {method}": "Metod: {method}", + "Min Length": "Minlängd", + "Min execution time (ms)": "Min körningstid (ms)", + "Min ms": "Min ms", + "Min result count": "Min antal resultat", + "Min results": "Min resultat", + "Minimum": "Minimum", + "Minimum length": "Minlängd", + "Minimum number of items": "Minimalt antal objekt", + "Minimum value": "Minvärde", + "Minimum value constraint is missing": "Minvärdebegränsning saknas", + "Minimum value is too restrictive": "Minvärdet är för restriktivt", + "Missing conversation or agentUuid": "Konversation eller agentUuid saknas", + "Missing conversationId": "ConversationId saknas", + "Missing message": "Meddelande saknas", + "Missing object fields created successfully": "Saknade objektfält skapades", + "Mode:": "Läge:", + "Model": "Modell", + "Model Name": "Modellnamn", + "Modified": "Ändrad", + "Monthly": "Månadsvis", + "More sources (10-20):": "Fler källor (10-20):", + "Most Active Objects": "Mest aktiva objekt", + "Multiple Of": "Multipel av", + "Multiple of": "Multipel av", + "Multiple search trail deletion not implemented yet": "Borttagning av flera sökloggar är ännu inte implementerat", + "Multitenancy": "Multitenancy", + "Multitenancy settings updated successfully": "Multitenancy-inställningar uppdaterades", + "My Account": "Mitt konto", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Namn", + "Name *": "Namn *", + "Name*": "Namn*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Namnscache uppvärmdes: {count} namn laddades in på {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Namnscache uppvärmdes: {count} namn laddades in på {time}. {message}", + "Names cache warmup completed": "Uppvärmning av namnscache slutförd", + "Naming pattern for copies:": "Namnmönster för kopior:", + "New Collection Name": "Nytt samlingsnamn", + "New Configuration": "Ny konfiguration", + "New Conversation": "Ny konversation", + "New Properties": "Nya egenskaper", + "New Property": "Ny egenskap", + "New User Group": "Ny användargrupp", + "New Value": "Nytt värde", + "New activity": "Ny aktivitet", + "New password": "Nytt lösenord", + "New processing activity": "Ny behandlingsaktivitet", + "New users without specific organisation membership will be automatically added to this organisation": "Nya användare utan specifikt organisationsmedlemskap läggs automatiskt till i den här organisationen", + "New verwerkingsactiviteit": "Ny verwerkingsactiviteit", + "Next": "Nästa", + "Nextcloud Groups": "Nextcloud-grupper", + "No": "Nej", + "No API tokens.": "Inga API-token.", + "No ConfigSets found": "Inga ConfigSets hittades", + "No Deck cards linked to this object": "Inga Deck-kort länkade till det här objektet", + "No GitHub token configured": "Ingen GitHub-token konfigurerad", + "No GitHub token provided": "Ingen GitHub-token angiven", + "No GitLab token provided": "Ingen GitLab-token angiven", + "No Organisation": "Ingen organisation", + "No active filters": "Inga aktiva filter", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Inga aktiva objekt att mjukradera för schemat {schema}. Använd \"Ta bort permanent\" för att ta bort mjukraderade objekt.", + "No activity data available": "Ingen aktivitetsdata tillgänglig", + "No activity found.": "Ingen aktivitet hittades.", + "No agents available": "Inga agenter tillgängliga", + "No agents found": "Inga agenter hittades", + "No app store cache exists yet": "Ingen App Store-cache finns ännu", + "No applications are available.": "Inga applikationer är tillgängliga.", + "No applications found": "Inga applikationer hittades", + "No approval steps for this object.": "Inga godkännandesteg för det här objektet.", + "No archived conversations": "Inga arkiverade konversationer", + "No audit trail entries found": "Inga revisionsloggsposter hittades", + "No changes recorded": "Inga ändringar registrerade", + "No chunks to vectorize": "Inga block att vektorisera", + "No collections found": "Inga samlingar hittades", + "No configuration data": "Ingen konfigurationsdata", + "No configuration selected": "Ingen konfiguration vald", + "No configurations are available.": "Inga konfigurationer är tillgängliga.", + "No configurations found": "Inga konfigurationer hittades", + "No configurations found for this application.": "Inga konfigurationer hittades för den här applikationen.", + "No contacts linked to this object": "Inga kontakter länkade till det här objektet", + "No conversations yet. Create a new one to get started!": "Inga konversationer ännu. Skapa en ny för att komma igång!", + "No dashboards yet": "Inga instrumentpaneler ännu", + "No data available": "Ingen data tillgänglig", + "No data available for chart": "Ingen data tillgänglig för diagram", + "No deleted items found": "Inga borttagna objekt hittades", + "No deletion data available": "Ingen borttagningsdata tillgänglig", + "No description found": "Ingen beskrivning hittades", + "No description provided": "Ingen beskrivning angiven", + "No emails linked to this object": "Inga e-postmeddelanden länkade till det här objektet", + "No endpoints defined": "Inga slutpunkter definierade", + "No entities detected for this email.": "Inga entiteter identifierade för det här e-postmeddelandet.", + "No entities detected in this file. Run text extraction first.": "Inga entiteter identifierade i den här filen. Kör textextraktion först.", + "No entities found": "Inga entiteter hittades", + "No entities have been detected yet": "Inga entiteter har identifierats ännu", + "No events found": "Inga händelser hittades", + "No events linked to this object": "Inga händelser länkade till det här objektet", + "No expired audit trails found to clear": "Inga utgångna revisionsloggar hittades att rensa", + "No expired search trails found to clear": "Inga utgångna sökloggar hittades att rensa", + "No extraction data available for this file": "Ingen extraktionsdata tillgänglig för den här filen", + "No facetable fields available. Select a register and schema to see available filters.": "Inga fasettbara fält tillgängliga. Välj ett register och schema för att se tillgängliga filter.", + "No files found": "Inga filer hittades", + "No files have been extracted yet": "Inga filer har extraherats ännu", + "No files selected": "Inga filer valda", + "No files to process": "Inga filer att bearbeta", + "No files to reindex": "Inga filer att omindexera", + "No filters are currently active. This will delete ALL audit trail entries!": "Inga filter är aktiva för tillfället. Detta tar bort ALLA revisionsloggsposter!", + "No groups found. Try a different search.": "Inga grupper hittades. Försök med en annan sökning.", + "No label": "Ingen etikett", + "No labels": "Inga etiketter", + "No linked objects": "Inga länkade objekt", + "No log entries found": "Inga loggposter hittades", + "No logs are available for this configuration.": "Inga loggar är tillgängliga för den här konfigurationen.", + "No logs are available for this source.": "Inga loggar är tillgängliga för den här källan.", + "No logs found": "Inga loggar hittades", + "No matches": "Inga träffar", + "No mismatched fields found - SOLR schema is properly configured": "Inga felmatchade fält hittades – SOLR-schemat är korrekt konfigurerat", + "No objects are currently selected for deletion.": "Inga objekt är valda för borttagning för tillfället.", + "No objects are currently selected for permanent deletion.": "Inga objekt är valda för permanent borttagning för tillfället.", + "No objects are currently selected for restoration.": "Inga objekt är valda för återställning för tillfället.", + "No objects found": "Inga objekt hittades", + "No objects found to vectorize": "Inga objekt hittades att vektorisera", + "No objects linked to this email": "Inga objekt länkade till det här e-postmeddelandet", + "No objects selected": "Inga objekt valda", + "No objects selected for deletion": "Inga objekt valda för borttagning", + "No objects selected for restoration": "Inga objekt valda för återställning", + "No objects to delete": "Inga objekt att ta bort", + "No objects to delete for schema {schema}": "Inga objekt att ta bort för schemat {schema}", + "No objects were permanently deleted": "Inga objekt togs bort permanent", + "No organisations found": "Inga organisationer hittades", + "No personal data was found for this subject identifier.": "Inga personuppgifter hittades för det här subjektidentifieraren.", + "No processing activities yet": "Inga behandlingsaktiviteter ännu", + "No properties found": "Inga egenskaper hittades", + "No properties match your filters.": "Inga egenskaper matchar dina filter.", + "No purge date set": "Inget raderngsdatum angett", + "No register data available": "Ingen registerdata tillgänglig", + "No register objects reference this file": "Inga registerobjekt refererar till den här filen", + "No registers found": "Inga register hittades", + "No registers found for this application.": "Inga register hittades för den här applikationen.", + "No registers found. Create a register to configure permissions.": "Inga register hittades. Skapa ett register för att konfigurera behörigheter.", + "No related cases found for this sender": "Inga relaterade ärenden hittades för den här avsändaren", + "No related entities": "Inga relaterade entiteter", + "No relations found": "Inga relationer hittades", + "No request body available": "Ingen förfrågningskropp tillgänglig", + "No response body available": "Ingen svarskropp tillgänglig", + "No saved views yet. Create one in the Search tab!": "Inga sparade vyer ännu. Skapa en i fliken Sök!", + "No saved views yet. create one in the search tab!": "Inga sparade vyer ännu. Skapa en i fliken sök!", + "No scan run yet": "Ingen genomsökning har körts ännu", + "No schema data available": "Ingen schemadata tillgänglig", + "No schema selected for exploration": "Inget schema valt för utforskning", + "No schemas configured for mail linking.": "Inga scheman konfigurerade för e-postlänkning.", + "No schemas found": "Inga scheman hittades", + "No schemas found for this application.": "Inga scheman hittades för den här applikationen.", + "No search terms data available": "Ingen data om söktermer tillgänglig", + "No search trail entries found": "Inga sökloggsposter hittades", + "No sources are available.": "Inga källor är tillgängliga.", + "No sources found": "Inga källor hittades", + "No synchronizations found": "Inga synkroniseringar hittades", + "No templates found": "Inga mallar hittades", + "No templates have been created yet": "Inga mallar har skapats ännu", + "No tools available. Tools can be registered by installed apps.": "Inga verktyg tillgängliga. Verktyg kan registreras av installerade appar.", + "No users in this organisation.": "Inga användare i den här organisationen.", + "No valid magic-mapped register+schema combinations found": "Inga giltiga magiskt mappade register+schema-kombinationer hittades", + "No views found. Create views first before configuring vectorization.": "Inga vyer hittades. Skapa vyer först innan du konfigurerar vektorisering.", + "No views match your search": "Inga vyer matchar din sökning", + "No webhooks found": "Inga webhooks hittades", + "No webhooks have been configured yet": "Inga webhooks har konfigurerats ännu", + "No workflows found in this project. Create workflows in the n8n editor.": "Inga arbetsflöden hittades i det här projektet. Skapa arbetsflöden i n8n-redigeraren.", + "None": "Inga", + "Not Found": "Hittades inte", + "Not extracted": "Ej extraherad", + "Not helpful": "Inte hjälpsam", + "Not in use": "Används inte", + "Note:": "Notering:", + "Notifications": "Aviseringar", + "Number Constraints": "Nummerbegränsningar", + "Number of Sources": "Antal källor", + "Number of chunks to vectorize in one API call (1-100).": "Antal block att vektorisera i ett API-anrop (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Antal block att vektorisera i ett API-anrop. Högre = snabbare men mer minne. Rekommenderat: 10-50.", + "Number of objects to process in each batch (1-5000).": "Antal objekt att bearbeta i varje batch (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Antal objekt att vektorisera i ett API-anrop. Högre = snabbare men mer minne. Rekommenderat: 10-50.", + "Numeric Range:": "Numeriskt intervall:", + "Object": "Objekt", + "Object #{id}": "Objekt #{id}", + "Object Collection": "Objektsamling", + "Object Configuration:": "Objektkonfiguration:", + "Object Count Prediction": "Prediktion av objektantal", + "Object Extraction Mode": "Objektextraktionsläge", + "Object ID": "Objekt-ID", + "Object Management": "Objekthantering", + "Object Text Extraction": "Objekttextextraktion", + "Object Vectorization": "Objektvektorisering", + "Object Warmup": "Objektuppvärmning", + "Object changes in owned objects": "Objektändringar i ägda objekt", + "Object collection not configured": "Objektsamling är inte konfigurerad", + "Object metadata incomplete for file upload": "Objektmetadata ofullständig för filuppladdning", + "Object not found": "Objekt hittades inte", + "Object permanently deleted": "Objekt permanent borttaget", + "Object restored successfully": "Objekt återställt", + "Object settings updated successfully": "Objektinställningar uppdaterades", + "Object sources": "Objektkällor", + "Object successfully deleted": "Objekt borttaget", + "Object successfully modified": "Objekt ändrat", + "Object unlocked successfully": "Objekt upplåst", + "Object vectorization configuration saved successfully": "Konfiguration för objektvektorisering sparades", + "Object vectorized successfully": "Objekt vektoriserat", + "Object:": "Objekt:", + "Objects": "Objekt", + "Objects Analyzed": "Objekt analyserade", + "Objects Distribution": "Objektfördelning", + "Objects being analyzed": "Objekt analyseras", + "Objects by Register": "Objekt per register", + "Objects by Schema": "Objekt per schema", + "Objects deletion completed for {schema}": "Objektborttagning slutförd för {schema}", + "Objects deletion completed successfully": "Objektborttagning slutförd", + "Objects in schema": "Objekt i schema", + "Objects successfully deleted": "Objekt borttagna", + "Objects to Process:": "Objekt att bearbeta:", + "Objects to be analyzed": "Objekt att analysera", + "Objects to be deleted": "Objekt att ta bort", + "Objects to be validated": "Objekt att validera", + "Objects will be serialized as JSON text before vectorization": "Objekt serialiseras som JSON-text innan vektorisering", + "Objects will be soft deleted and moved to the": "Objekt kommer att mjukraderas och flyttas till", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Objekt mjukraderas (markeras som borttagna men behålls i databasen). De kan återställas senare vid behov.", + "Old Value": "Gammalt värde", + "Oldest Item (days)": "Äldsta objekt (dagar)", + "Ollama Chat Settings": "Ollama chattinställningar", + "Ollama Embedding Configuration": "Ollama inbäddningskonfiguration", + "Ollama URL": "Ollama URL", + "Only With Changes": "Endast med ändringar", + "Only members of selected groups can access this organisation": "Endast medlemmar i valda grupper kan komma åt den här organisationen", + "Open Nextcloud App Store": "Öppna Nextcloud App Store", + "Open OpenRegister sidebar": "Öppna OpenRegister-sidopanelen", + "Open Register Objects": "Öppna registerobjekt", + "Open folder": "Öppna mapp", + "Open in OpenRegister": "Öppna i OpenRegister", + "Open n8n Editor": "Öppna n8n-redigeraren", + "OpenAI Chat Settings": "OpenAI chattinställningar", + "OpenAI Embedding Configuration": "OpenAI inbäddningskonfiguration", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister-inställningar", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operatördefinierade instrumentpaneler och schemalagda rapporter. Varje instrumentpanel är ett förstklassigt objekt i `reports`-registret; widgetar deklareras i instrumentpanelens `widgets`-array och renderas live från aggregationer / GraphQL.", + "Optimizing search performance...": "Optimerar sökprestanda...", + "Optional URL-friendly identifier": "Valfri URL-vänlig identifierare", + "Optional description for this facet": "Valfri beskrivning för den här fasetten", + "Optional end date property": "Valfri slutdatumsegenskap", + "Optional event description template": "Valfri mall för händelsebeskrivning", + "Optional location property": "Valfri platsegenskap", + "Optional webhook secret for signature verification": "Valfri webhook-hemlighet för signaturverifiering", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Valfritt: Lägg till frågeparametrar för att filtrera det refererade schemat (t.ex. status=active&type=public)", + "Or": "Eller", + "Order": "Ordning", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Ordnade BCP 47-språktaggar. Det första språket är registerstandarden och styr Accept-Language-reserv för översättningsbara egenskaper.", + "Organisation": "Organisation", + "Organisation Configuration": "Organisationskonfiguration", + "Organisation ID": "Organisations-ID", + "Organisation Members": "Organisationsmedlemmar", + "Organisation Statistics": "Organisationsstatistik", + "Organisation created successfully": "Organisation skapad", + "Organisation membership changes": "Ändringar i organisationsmedlemskap", + "Organisation name is required": "Organisationsnamn krävs", + "Organisation settings saved successfully": "Organisationsinställningar sparades", + "Organisation successfully created": "Organisation skapades", + "Organisation successfully updated": "Organisation uppdaterades", + "Organisational measures": "Organisatoriska åtgärder", + "Organisations": "Organisationer", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Organisation", + "Organization ID (Optional)": "Organisations-ID (valfri)", + "Organizations": "Organisationer", + "Orphaned Items": "Föräldralösa objekt", + "Other": "Övrigt", + "Output Mapping": "Utgångsmappning", + "Overview": "Översikt", + "Owner": "Ägare", + "Owner:": "Ägare:", + "PDF": "PDF", + "PII hits": "PII-träffar", + "PII rows": "PII-rader", + "Page {current} of {total}": "Sida {current} av {total}", + "Parallel Mode (Faster, more resource intensive)": "Parallellt läge (snabbare, mer resurskrävande)", + "Parallel:": "Parallellt:", + "Password": "Lösenord", + "Password changes are not supported by your authentication provider.": "Din autentiseringsleverantör stöder inte lösenordsbyten.", + "Password for SOLR authentication (optional)": "Lösenord för SOLR-autentisering (valfritt)", + "Password updated successfully": "Lösenord uppdaterat", + "Path": "Sökväg", + "Path in repository": "Sökväg i arkiv", + "Path where the configuration file will be saved in the repository": "Sökväg där konfigurationsfilen sparas i arkivet", + "Path where the register OAS file will be saved in the repository": "Sökväg där register OAS-filen sparas i arkivet", + "Pattern (regex)": "Mönster (regex)", + "Pattern Issue": "Mönsterproblem", + "Pattern constraint is missing": "Mönsterbegränsning saknas", + "Patterns:": "Mönster:", + "Pending": "Väntar", + "Permanent Delete Mode": "Permanent borttagningsläge", + "Permanently Delete": "Ta bort permanent", + "Permanently Delete ({count})": "Ta bort permanent ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Ta bort permanent alla {count} mjukraderade objekt. Detta kan inte ångras!", + "Permanently delete already soft-deleted objects": "Ta bort permanent redan mjukraderade objekt", + "Permanently delete this conversation?": "Ta bort den här konversationen permanent?", + "Permission Matrix": "Behörighetsmatris", + "Person": "Person", + "Personal Data": "Personuppgifter", + "Personal Data Export": "Export av personuppgifter", + "Persons": "Personer", + "Phone": "Telefon", + "Phone Numbers": "Telefonnummer", + "Photos": "Foton", + "Please create an agent in the": "Skapa en agent i", + "Please select a user": "Välj en användare", + "Please select an agent to continue": "Välj en agent för att fortsätta", + "Please select an organisation": "Välj en organisation", + "Please select at least one item to import": "Välj minst ett objekt att importera", + "Please select registers or schemas first": "Välj register eller scheman först", + "Please select which register and schema to use for the new object": "Välj vilket register och schema som ska användas för det nya objektet", + "Please try again later.": "Försök igen senare.", + "Please wait while we fetch your agents.": "Vänta medan vi hämtar dina agenter.", + "Please wait while we fetch your applications.": "Vänta medan vi hämtar dina applikationer.", + "Please wait while we fetch your configurations.": "Vänta medan vi hämtar dina konfigurationer.", + "Please wait while we fetch your deleted items.": "Vänta medan vi hämtar dina borttagna objekt.", + "Please wait while we fetch your sources.": "Vänta medan vi hämtar dina källor.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Vänta medan vi permanent tar bort SOLR-samlingen. Det kan ta en stund.", + "Polls": "Omröstningar", + "Popular Search Terms": "Populära söktermer", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Art 20)", + "Preferences saved": "Inställningar sparade", + "Prerequisites": "Förutsättningar", + "Press Enter to send, Shift+Enter for new line": "Tryck Enter för att skicka, Skift+Enter för ny rad", + "Press enter to send, shift+enter for new line": "Tryck enter för att skicka, skift+enter för ny rad", + "Prettify": "Formattera", + "Preview erasure": "Förhandsgranska radering", + "Preview:": "Förhandsgranskning:", + "Previous": "Föregående", + "Private": "Privat", + "Private Agent (Default)": "Privat agent (standard)", + "Private agents": "Privata agenter", + "Process Name (optional)": "Processnamn (valfritt)", + "Processed {indexed} files, {failed} failed": "Bearbetade {indexed} filer, {failed} misslyckades", + "Processes chunks in batches with simulated parallelism.": "Bearbetar block i batchar med simulerad parallellism.", + "Processes file chunks sequentially (safest).": "Bearbetar filblock sekventiellt (säkrast).", + "Processes objects in chunks with simulated parallelism.": "Bearbetar objekt i block med simulerad parallellism.", + "Processes objects sequentially (safest).": "Bearbetar objekt sekventiellt (säkrast).", + "Processing": "Bearbetar", + "Processing Limits": "Bearbetningsgränser", + "Processing...": "Bearbetar...", + "Project initialized successfully": "Projekt initierat", + "Project not found. Please initialize first.": "Projekt hittades inte. Initiera först.", + "Properties": "Egenskaper", + "Property Behaviors": "Egenskapsbeteenden", + "Property Configuration:": "Egenskapskonfiguration:", + "Property Filters": "Egenskapsfilter", + "Property Title": "Egenskapstitel", + "Property Type": "Egenskapstyp", + "Property can be improved": "Egenskapen kan förbättras", + "Property could not be added": "Egenskapen kunde inte läggas till", + "Property could not be updated": "Egenskapen kunde inte uppdateras", + "Property name of inversed relation": "Egenskapsnamn för inverterad relation", + "Property successfully added": "Egenskap tillagd", + "Property successfully updated": "Egenskap uppdaterad", + "Protected": "Skyddad", + "Provider": "Leverantör", + "Provider is required for testing": "Leverantör krävs för testning", + "Public": "Offentlig", + "Public View": "Offentlig vy", + "Public views can be accessed by anyone in the system": "Offentliga vyer kan nås av alla i systemet", + "Publish": "Publicera", + "Publish Configuration to GitHub": "Publicera konfiguration till GitHub", + "Publish OAS": "Publicera OAS", + "Publish Objects": "Publicera objekt", + "Publish Register OAS to GitHub": "Publicera register-OAS till GitHub", + "Publish all objects in this schema": "Publicera alla objekt i det här schemat", + "Publish objects to make them publicly available": "Publicera objekt för att göra dem offentligt tillgängliga", + "Published": "Publicerad", + "Published:": "Publicerad:", + "Publishing...": "Publicerar...", + "Purge": "Rensa", + "Purge Date": "Rensningsdatum", + "Purge {count} object from database": "Rensa {count} objekt från databasen", + "Purge {count} objects from database": "Rensa {count} objekt från databasen", + "Purpose limitation *": "Ändamålsbegränsning *", + "Push Notifications": "Push-aviseringar", + "Query Complexity Distribution": "Fördelning av frågekomplexitet", + "Query Help": "Frågehjälp", + "Query parameter is required": "Frågeparameter krävs", + "Quota": "Kvot", + "RAG": "RAG", + "RAG Configuration": "RAG-konfiguration", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC-inställningar uppdaterades", + "Rate limits cleared successfully": "Hastighetsbegränsningar rensades", + "Raw Changes Data": "Rådata för ändringar", + "Re-vectorize on object update": "Revektorisera vid objektuppdatering", + "Read": "Läs", + "Real-time push notification status via notify_push": "Status för realtids-push-aviseringar via notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Realtids-push-aviseringar är aktiva. Anslutna klienter tar emot omedelbara uppdateringar när objekt skapas, uppdateras eller tas bort.", + "Realtime push active": "Realtids-push aktiv", + "Realtime push not available — the notify_push app is not installed": "Realtids-push inte tillgänglig — notify_push-appen är inte installerad", + "Reason": "Anledning", + "Reason (optional)": "Anledning (valfri)", + "Rebase operation completed successfully": "Rebase-operation slutförd", + "Rebase operation failed: {error}": "Rebase-operation misslyckades: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Rekommendationer:", + "Recommended Type:": "Rekommenderad typ:", + "Recommended: 5 sources": "Rekommenderat: 5 källor", + "Refresh": "Uppdatera", + "Refresh Data": "Uppdatera data", + "Refresh Stats": "Uppdatera statistik", + "Refresh Workflows": "Uppdatera arbetsflöden", + "Refresh dashboard": "Uppdatera instrumentpanel", + "Refresh database info": "Uppdatera databasinformation", + "Register": "Register", + "Register #{id}": "Register #{id}", + "Register / Schema": "Register / Schema", + "Register ID": "Register-ID", + "Register Objects": "Registerobjekt", + "Register Overview": "Registeröversikt", + "Register Statistics": "Registerstatistik", + "Register Totals": "Registertotaler", + "Register and schema are required": "Register och schema krävs", + "Register annotation": "Registeranteckning", + "Register depublished successfully": "Register avpublicerat", + "Register not found": "Register hittades inte", + "Register objects": "Registerobjekt", + "Register objects deletion completed successfully": "Borttagning av registerobjekt slutförd", + "Register published successfully": "Register publicerat", + "Register/Schema Usage": "Register/schema-användning", + "Register:": "Register:", + "Registers": "Register", + "Reindex": "Omindexera", + "Reindex completed": "Omindexering slutförd", + "Reindex completed successfully": "Omindexering slutförd", + "Reindex completed! Processed {count} objects in {duration}s": "Omindexering slutförd! Bearbetade {count} objekt på {duration}s", + "Reindex failed": "Omindexering misslyckades", + "Reindex failed: %s": "Omindexering misslyckades: %s", + "Reject": "Avvisa", + "Rejected": "Avvisad", + "Related Cases": "Relaterade ärenden", + "Relations": "Relationer", + "Remote Version": "Fjärrversion", + "Remove": "Ta bort", + "Remove Deck card": "Ta bort Deck-kort", + "Remove avatar": "Ta bort avatar", + "Remove contact": "Ta bort kontakt", + "Remove filter": "Ta bort filter", + "Remove from favorites": "Ta bort från favoriter", + "Remove from list": "Ta bort från lista", + "Remove group": "Ta bort grupp", + "Remove link": "Ta bort länk", + "Remove link to {name}": "Ta bort länk till {name}", + "Remove link to {name}?": "Ta bort länk till {name}?", + "Remove link to {title}": "Ta bort länk till {title}", + "Remove oneOf entry": "Ta bort oneOf-post", + "Remove property after write-back": "Ta bort egenskap efter återskrivning", + "Remove {title}": "Ta bort {title}", + "Remove {userId} from this organisation?": "Ta bort {userId} från den här organisationen?", + "Removed": "Borttagen", + "Removed from favorites": "Borttagen från favoriter", + "Removing schema {schema}...": "Tar bort schema {schema}...", + "Rename Conversation": "Byt namn på konversation", + "Rename conversation": "Byt namn på konversation", + "Replicas": "Repliker", + "Reports": "Rapporter", + "Repository": "Arkiv", + "Request Body": "Förfrågningskropp", + "Request Data": "Förfrågningsdata", + "Request Details": "Förfrågningsdetaljer", + "Request Quota (per day)": "Förfrågningskvot (per dag)", + "Request account deactivation": "Begär kontoinaktivering", + "Request account deactivation. This will notify administrators for review.": "Begär kontoinaktivering. Administratörer meddelas för granskning.", + "Request body must be JSON with a \"query\" field": "Förfrågningskroppen måste vara JSON med ett \"query\"-fält", + "Request timeout in seconds": "Förfrågningstimeout i sekunder", + "Requested at": "Begärd", + "Required": "Obligatorisk", + "Required Token Scopes": "Obligatoriska token-omfång", + "Required field": "Obligatoriskt fält", + "Required status is inconsistent": "Obligatorisk status är inkonsekvent", + "Requires Dolphin AI for OCR text extraction": "Kräver Dolphin AI för OCR-textextraktion", + "Rerun Search": "Kör sökning igen", + "Rerunning search: {searchTerm}": "Kör sökning igen: {searchTerm}", + "Reset Changes": "Återställ ändringar", + "Reset Filters": "Återställ filter", + "Resource Quotas": "Resurskvoter", + "Response Body": "Svarskropp", + "Response Details": "Svardetaljer", + "Response Time:": "Svarstid:", + "Restore": "Återställ", + "Restore conversation": "Återställ konversation", + "Restore or permanently delete items": "Återställ eller ta bort permanent", + "Restore {count} object": "Återställ {count} objekt", + "Restore {count} objects": "Återställ {count} objekt", + "Result Count Range": "Intervall för antal resultat", + "Results": "Resultat", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Lagringsperiod (ISO-8601 varaktighet, t.ex. P10Y, P30D)", + "Retention settings updated successfully": "Lagringsinställningar uppdaterades", + "Retrieve all objects for this schema": "Hämta alla objekt för det här schemat", + "Retry": "Försök igen", + "Retry Failed Extractions": "Försök extraktion igen", + "Retry Policy": "Policy för återförsök", + "Retry completed": "Återförsök slutfört", + "Retry upload": "Försök uppladdning igen", + "Retrying...": "Försöker igen...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Granska de valda objekten nedan. Du kan ta bort objekt som du inte vill radera genom att klicka på ta bort-knappen.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Granska de valda objekten nedan. Du kan ta bort objekt som du inte vill inkludera.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Granska de valda objekten nedan. Du kan ta bort objekt som du inte vill ta bort permanent genom att klicka på ta bort-knappen. Denna åtgärd kan inte ångras.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Granska de valda objekten nedan. Du kan ta bort objekt som du inte vill återställa genom att klicka på ta bort-knappen. Objekt återställs till sin ursprungliga plats.", + "Revoke": "Återkalla", + "Right": "Höger", + "Risk Level": "Risknivå", + "Risk level": "Risknivå", + "Roles and Permissions": "Roller och behörigheter", + "Roles updated successfully": "Roller uppdaterades", + "Run compliance scan": "Kör efterlevnadssökning", + "SOLR Connection Settings": "SOLR-anslutningsinställningar", + "SOLR Index Warmup": "SOLR-indexuppvärmning", + "SOLR Version:": "SOLR-version:", + "SOLR actions menu": "SOLR-åtgärdsmeny", + "SOLR base path (usually /solr)": "SOLR-bassökväg (vanligtvis /solr)", + "SOLR collection deleted successfully": "SOLR-samling borttagen", + "SOLR connection test failed: {error}": "SOLR-anslutningstest misslyckades: {error}", + "SOLR connection test successful!": "SOLR-anslutningstest lyckades!", + "SOLR index warmup completed successfully": "SOLR-indexuppvärmning slutförd", + "SOLR is disabled": "SOLR är inaktiverad", + "SOLR is not available or not configured": "SOLR är inte tillgänglig eller konfigurerad", + "SOLR logging disabled": "SOLR-loggning inaktiverad", + "SOLR logging enabled": "SOLR-loggning aktiverad", + "SOLR search disabled": "SOLR-sökning inaktiverad", + "SOLR search enabled": "SOLR-sökning aktiverad", + "SOLR server hostname or IP address": "SOLR-serverns värdnamn eller IP-adress", + "SOLR server port number (optional, defaults to 8983)": "SOLR-serverns portnummer (valfritt, standard 8983)", + "SOLR settings updated successfully": "SOLR-inställningar uppdaterades", + "SOLR setup completed successfully": "SOLR-konfiguration slutförd", + "SOLR setup completed successfully!": "SOLR-konfiguration slutförd!", + "SOLR setup error: %s": "SOLR-konfigurationsfel: %s", + "SOLR setup failed": "SOLR-konfiguration misslyckades", + "SOLR setup failed - check logs": "SOLR-konfiguration misslyckades – kontrollera loggar", + "SOLR setup failed. Please check the configuration and try again.": "SOLR-konfiguration misslyckades. Kontrollera konfigurationen och försök igen.", + "SOLR setup failed: {error}": "SOLR-konfiguration misslyckades: {error}", + "SOLR warmup failed: {error}": "SOLR-uppvärmning misslyckades: {error}", + "Sample Values:": "Exempelvärden:", + "Save": "Spara", + "Save Configuration": "Spara konfiguration", + "Save Connection Settings": "Spara anslutningsinställningar", + "Save Roles": "Spara roller", + "Save Settings": "Spara inställningar", + "Save changes": "Spara ändringar", + "Save current search as view": "Spara aktuell sökning som vy", + "Save labels": "Spara etiketter", + "Save tags for {name}": "Spara taggar för {name}", + "Save the organisation first to manage users.": "Spara organisationen först för att hantera användare.", + "Saved Views": "Sparade vyer", + "Saving...": "Sparar...", + "Scheduled": "Schemalagd", + "Schema": "Schema", + "Schema ID": "Schema-ID", + "Schema Name": "Schemanamn", + "Schema Statistics": "Schemastatistik", + "Schema annotation": "Schemaanteckning", + "Schema configuration updated successfully for {schema}": "Schemakonfiguration uppdaterades för {schema}", + "Schema depublished successfully": "Schema avpublicerat", + "Schema objects deletion completed successfully": "Borttagning av schemaobjekt slutförd", + "Schema published successfully": "Schema publicerat", + "Schema reference ($ref)": "Schemareferens ($ref)", + "Schema successfully created": "Schema skapat", + "Schema successfully updated": "Schema uppdaterat", + "Schema {schema} removed successfully": "Schema {schema} borttaget", + "Schema:": "Schema:", + "Schemas": "Scheman", + "Scheme": "Schema", + "Search": "Sök", + "Search / Views": "Sök / Vyer", + "Search / views": "Sök / vyer", + "Search Activity": "Sökaktivitet", + "Search Analytics": "Sökanalys", + "Search Configuration": "Sökkonfiguration", + "Search Entities": "Sök entiteter", + "Search Files": "Sök filer", + "Search GitHub": "Sök GitHub", + "Search GitLab": "Sök GitLab", + "Search Method": "Sökmetod", + "Search Mode": "Sökläge", + "Search Query": "Sökfråga", + "Search Results": "Sökresultat", + "Search Term": "Sökterm", + "Search Trail Details": "Detaljer om söklogs", + "Search Trail Management": "Hantering av sökloggar", + "Search Trail Statistics": "Statistik för sökloggar", + "Search Trails": "Sökloggar", + "Search Views": "Sökvyer", + "Search Webhooks": "Sök webhooks", + "Search agents...": "Sök agenter...", + "Search applications...": "Sök applikationer...", + "Search by file name or path": "Sök efter filnamn eller sökväg", + "Search by name or URL": "Sök efter namn eller URL", + "Search by title or UUID...": "Sök efter titel eller UUID...", + "Search by value": "Sök efter värde", + "Search configurations": "Sökkonfigurationer", + "Search data sources...": "Sök datakällor...", + "Search groups...": "Sök grupper...", + "Search in Database Objects": "Sök i databasobjekt", + "Search in Files": "Sök i filer", + "Search in Objects": "Sök i objekt", + "Search objects": "Sök objekt", + "Search objects...": "Sök objekt...", + "Search property names...": "Sök egenskapsnamn...", + "Search registers...": "Sök register...", + "Search schemas...": "Sök scheman...", + "Search trail deletion not implemented yet": "Borttagning av sökloggar är ännu inte implementerat", + "Search views...": "Sök vyer...", + "Search {name}...": "Sök {name}...", + "Searching...": "Söker...", + "Secret": "Hemlighet", + "Secret key for HMAC signature generation (optional)": "Hemlig nyckel för HMAC-signaturgenerering (valfri)", + "Security": "Säkerhet", + "See organisations as a table": "Se organisationer som tabell", + "See organisations as cards": "Se organisationer som kort", + "Select AI Agent": "Välj AI-agent", + "Select Active Organisation": "Välj aktiv organisation", + "Select All": "Markera alla", + "Select ConfigSet": "Välj ConfigSet", + "Select File Types to Vectorize:": "Välj filtyper att vektorisera:", + "Select HTTP method": "Välj HTTP-metod", + "Select Register and Schema": "Välj register och schema", + "Select Views to Vectorize:": "Välj vyer att vektorisera:", + "Select a Nextcloud group to add": "Välj en Nextcloud-grupp att lägga till", + "Select a branch": "Välj en gren", + "Select a date property": "Välj en datumegenskap", + "Select a model or type a custom model name": "Välj en modell eller ange ett anpassat modellnamn", + "Select a register": "Välj ett register", + "Select a register...": "Välj ett register...", + "Select a repository": "Välj ett arkiv", + "Select a repository you have write access to": "Välj ett arkiv som du har skrivåtkomst till", + "Select a schema": "Välj ett schema", + "Select a schema...": "Välj ett schema...", + "Select a user": "Välj en användare", + "Select agent type": "Välj agenttyp", + "Select all entities": "Välj alla entiteter", + "Select an AI Agent": "Välj en AI-agent", + "Select an AI agent to begin chatting with your data.": "Välj en AI-agent för att börja chatta med dina data.", + "Select an email to see linked objects": "Välj ett e-postmeddelande för att se länkade objekt", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Välj en organisation och användare för att lägga till dem som medlemmar. Sök efter organisationer efter namn.", + "Select backend": "Välj backend", + "Select chat model": "Välj chattmodell", + "Select collection for files": "Välj samling för filer", + "Select collection for objects": "Välj samling för objekt", + "Select configuration type...": "Välj konfigurationstyp...", + "Select default organisation": "Välj standardorganisation", + "Select entity {value}": "Välj entitet {value}", + "Select event to listen to...": "Välj händelse att lyssna på...", + "Select fields to display": "Välj fält att visa", + "Select files with the correct extension": "Välj filer med rätt filändelse", + "Select group": "Välj grupp", + "Select groups (optional)": "Välj grupper (valfritt)", + "Select groups to notify...": "Välj grupper att meddela...", + "Select groups with access to this agent": "Välj grupper med åtkomst till den här agenten", + "Select groups...": "Välj grupper...", + "Select model": "Välj modell", + "Select one or more registers": "Välj ett eller flera register", + "Select one or more schemas": "Välj ett eller flera scheman", + "Select options...": "Välj alternativ...", + "Select or create labels, or select \"No label\" to add files": "Välj eller skapa etiketter, eller välj \"Ingen etikett\" för att lägga till filer", + "Select owner application (optional)...": "Välj ägarapplikation (valfri)...", + "Select period": "Välj period", + "Select property to send as payload": "Välj egenskap att skicka som nyttolast", + "Select provider": "Välj leverantör", + "Select registers and schemas to save a view": "Välj register och scheman för att spara en vy", + "Select retry policy": "Välj policy för återförsök", + "Select role": "Välj roll", + "Select schemas (empty = all schemas)": "Välj scheman (tomt = alla scheman)", + "Select schemas to warm up": "Välj scheman att värma upp", + "Select search mode": "Välj sökläge", + "Select source type...": "Välj källtyp...", + "Select target type": "Välj måltyp", + "Select the branch to publish to": "Välj grenen att publicera till", + "Select the event this webhook should listen to": "Välj den händelse den här webhooken ska lyssna på", + "Select views to filter data (optional)": "Välj vyer för att filtrera data (valfritt)", + "Select views to limit which data the agent can access": "Välj vyer för att begränsa vilken data agenten kan komma åt", + "Select views to vectorize:": "Välj vyer att vektorisera:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Välj vilka Nextcloud-grupper som är tillgängliga för den här organisationen. Användare i dessa grupper får åtkomst till organisationens resurser.", + "Select which data views the AI can search": "Välj vilka datavyer AI:n kan söka i", + "Select which property from the event should be used as the webhook payload data": "Välj vilken egenskap från händelsen som ska användas som webhook-nyttolastdata", + "Select which tools the AI can use to perform actions": "Välj vilka verktyg AI:n kan använda för att utföra åtgärder", + "Select {fieldName} values": "Välj {fieldName}-värden", + "Selected": "Vald", + "Selected Groups": "Valda grupper", + "Selected Objects ({count})": "Valda objekt ({count})", + "Selected audit trails deleted successfully": "Valda revisionsloggar borttagna", + "Selected search trails deleted successfully": "Valda sökloggar borttagna", + "Selected users can access this view": "Valda användare kan komma åt den här vyn", + "Send additional feedback": "Skicka ytterligare feedback", + "Send as CloudEvent": "Skicka som CloudEvent", + "Send as cloudevent": "Skicka som cloudevent", + "Sensitive PII": "Känsliga PII", + "Serial Mode (Safer, slower)": "Seriellt läge (säkrare, långsammare)", + "Serial:": "Seriellt:", + "Server Configuration": "Serverkonfiguration", + "Server Information": "Serverinformation", + "Set as Active": "Ange som aktiv", + "Set as active organisation": "Ange som aktiv organisation", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Ange gränser för API-användning och tokenförbrukning. Använd 0 för obegränsade resurser.", + "Settings": "Inställningar", + "Settings updated successfully": "Inställningar uppdaterades", + "Shards": "Shards", + "Share": "Dela", + "Share with Groups": "Dela med grupper", + "Share with Users": "Dela med användare", + "Show": "Visa", + "Show Filters": "Visa filter", + "Show less": "Visa mindre", + "Show only entries with changes": "Visa endast poster med ändringar", + "Showing {count} dashboard(s)": "Visar {count} instrumentpanel(er)", + "Showing {count} processing activities": "Visar {count} behandlingsaktiviteter", + "Showing {showing} of {total} agents": "Visar {showing} av {total} agenter", + "Showing {showing} of {total} applications": "Visar {showing} av {total} applikationer", + "Showing {showing} of {total} audit trail entries": "Visar {showing} av {total} revisionsloggsposter", + "Showing {showing} of {total} configurations": "Visar {showing} av {total} konfigurationer", + "Showing {showing} of {total} deleted items": "Visar {showing} av {total} borttagna objekt", + "Showing {showing} of {total} entities": "Visar {showing} av {total} entiteter", + "Showing {showing} of {total} files": "Visar {showing} av {total} filer", + "Showing {showing} of {total} log entries": "Visar {showing} av {total} loggposter", + "Showing {showing} of {total} search trail entries": "Visar {showing} av {total} sökloggsposter", + "Showing {showing} of {total} sources": "Visar {showing} av {total} källor", + "Showing {showing} of {total} templates": "Visar {showing} av {total} mallar", + "Showing {showing} of {total} webhooks": "Visar {showing} av {total} webhooks", + "Showing {shown} of {total} organisations": "Visar {shown} av {total} organisationer", + "Simple": "Enkel", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Enkla frågor: Grundläggande textsökningar med minimala parametrar (t.ex. en sökterm, inga avancerade filter)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Enkla frågor: grundläggande textsökningar med minimala parametrar (t.ex. en sökterm, inga avancerade filter)", + "Size": "Storlek", + "Slug": "Slug", + "Soft Delete Mode": "Mjukraderingsläge", + "Soft Deleted Items": "Mjukraderade objekt", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Mjukradera alla objekt för det här schemat ({active} aktiva, {deleted} redan raderade)", + "SolrCloud mode enabled": "SolrCloud-läge aktiverat", + "Something went wrong while adding files": "Något gick fel när filer lades till", + "Source": "Källa", + "Source successfully updated": "Källa uppdaterad", + "Sources": "Källor", + "Sources:": "Källor:", + "Special Rights": "Särskilda rättigheter", + "Standalone SOLR mode": "Fristående SOLR-läge", + "Start Conversation": "Starta konversation", + "Start Date Field": "Fält för startdatum", + "Start Vectorization": "Starta vektorisering", + "Start a conversation": "Starta en konversation", + "Starting names cache warmup...": "Startar uppvärmning av namnscache...", + "Starting validation for {schema}...": "Startar validering för {schema}...", + "Starting {action} for {schema}...": "Startar {action} för {schema}...", + "Starting...": "Startar...", + "Statistics": "Statistik", + "Statistics and Metrics": "Statistik och mätvärden", + "Statistics refreshed": "Statistik uppdaterad", + "Stats calculation not yet implemented": "Statistikberäkning är ännu inte implementerad", + "Status": "Status", + "Status Code": "Statuskod", + "Step": "Steg", + "Storage": "Lagring", + "Storage Quota (MB)": "Lagringskvot (MB)", + "String Constraints": "Strängbegränsningar", + "Subject identifier (email, BSN, name, etc.)": "Subjektidentifierare (e-post, BSN, namn, osv.)", + "Subscribe": "Prenumerera", + "Subscribed": "Prenumererad", + "Success": "Lyckades", + "Success Rate": "Framgångsfrekvens", + "Success Status": "Framgångsstatus", + "Successful": "Lyckad", + "Successfully added user to organisation: {name}": "Användare tillagd i organisation: {name}", + "Successfully cleared {count} audit trails": "{count} revisionsloggar rensades", + "Successfully cleared {count} blob storage objects": "{count} blob-lagringsobjekt rensades", + "Successfully cleared {count} search trails": "{count} sökloggar rensades", + "Successfully created {count} SOLR fields": "{count} SOLR-fält skapades", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "{count} inbäddningar togs bort. Revektorisera dina data.", + "Successfully deleted {count} objects for {schema}": "{count} objekt togs bort för {schema}", + "Successfully fixed {count} SOLR field configurations": "{count} SOLR-fältskonfigurationer åtgärdades", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Importerades: {registers} register, {schemas} scheman, {objects} objekt", + "Successfully joined organisation": "Gick med i organisation", + "Successfully permanently deleted {count} objects": "{count} objekt togs bort permanent", + "Successfully processed {count} files!": "{count} filer bearbetades!", + "Successfully restored {count} object": "{count} objekt återställdes", + "Successfully restored {count} objects": "{count} objekt återställdes", + "Successfully saved configuration for {count} facets!": "Konfiguration sparades för {count} fasetter!", + "Supported File Types": "Filtyper som stöds", + "Switch Active Organisation": "Byt aktiv organisation", + "Switch Organisation": "Byt organisation", + "Sync Table": "Synkronisera tabell", + "Synchronization Settings": "Synkroniseringsinställningar", + "Syncing magic table for {schema}...": "Synkroniserar magisk tabell för {schema}...", + "Syncs": "Synkroniseringar", + "System Default": "Systemstandard", + "System Prompt": "Systemuppmaning", + "System announcements": "Systemmeddelanden", + "Table": "Tabell", + "Target ID": "Mål-ID", + "Target Type*": "Måltyp*", + "Technical Description": "Teknisk beskrivning", + "Technical Details": "Tekniska detaljer", + "Technical description for developers and administrators": "Teknisk beskrivning för utvecklare och administratörer", + "Technical measures": "Tekniska åtgärder", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Temperatur", + "Templates": "Mallar", + "Test": "Testa", + "Test Chat": "Testa chatt", + "Test Connection": "Testa anslutning", + "Test Embedding": "Testa inbäddning", + "Test endpoint executed successfully": "Testslutpunkt kördes", + "Test webhook delivered successfully": "Test-webhook levererades", + "Test webhook delivery failed": "Test-webhook-leverans misslyckades", + "Test webhook sent successfully": "Test-webhook skickades", + "Testing...": "Testar...", + "Text Chunk #{id}": "Textblock #{id}", + "Text Extraction": "Textextraktion", + "Text Extraction Engine": "Motor för textextraktion", + "Text chunks": "Textblock", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Textblock genereras under filextraktion och lagras i databasen. Vektorisering läser dessa förchunkade filer och konverterar dem till inbäddningar.", + "Text extracted successfully": "Text extraherades", + "Text extraction disabled": "Textextraktion inaktiverad", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloud Kalender-appen är inte installerad eller aktiverad på den här servern.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloud Kontakter-appen är inte installerad eller aktiverad på den här servern.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloud Deck-appen är inte installerad eller aktiverad på den här servern.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloud E-post-appen är inte installerad eller aktiverad på den här servern.", + "The URL where webhook events will be sent": "URL:en dit webhook-händelser skickas", + "The requested conversation does not exist": "Den begärda konversationen finns inte", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Den begärda instrumentpanelen kunde inte laddas. Verifiera URL:en eller välj en annan instrumentpanel från listan.", + "The saved view and all its search configuration": "Den sparade vyn och all dess sökkonfiguration", + "There are no audit trail entries matching your current filters.": "Det finns inga revisionsloggsposter som matchar dina aktuella filter.", + "There are no deleted items matching your current filters.": "Det finns inga borttagna objekt som matchar dina aktuella filter.", + "There are no search trail entries matching your current filters.": "Det finns inga sökloggsposter som matchar dina aktuella filter.", + "There are no webhook log entries matching your filters.": "Det finns inga webhook-loggposter som matchar dina filter.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "De behålls enligt schemats konfigurerade lagringsperiod och tas automatiskt bort permanent när lagringsperioden löper ut. Lagringsperioden kan konfigureras per schema och finns i schemainställningarna.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "De behålls enligt schemats konfigurerade lagringsperiod och tas sedan bort permanent.", + "This action cannot be undone.": "Denna åtgärd kan inte ångras.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Denna åtgärd kan inte ångras. Se till att inga samlingar använder denna ConfigSet.", + "This action cannot be undone. Make sure no collections are using this configset.": "Denna åtgärd kan inte ångras. Se till att inga samlingar använder denna configset.", + "This action will submit a deactivation request to your administrators.": "Den här åtgärden skickar en avaktiveringsförfrågan till dina administratörer.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Den här agenten har inga konfigurerbara vyer eller verktyg, men du kan fortfarande justera RAG-inställningar nedan.", + "This analysis may take some time": "Den här analysen kan ta lite tid", + "This audit trail entry does not contain any change information.": "Den här revisionsloggsposten innehåller ingen ändringsinformation.", + "This configuration has no data defined.": "Den här konfigurationen har ingen definierad data.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Den här slutpunkten är föråldrad. Använd blockbaserade slutpunkter istället.", + "This entity has no relations to objects or files": "Den här entiteten har inga relationer till objekt eller filer", + "This is a test webhook from OpenRegister": "Det här är en test-webhook från OpenRegister", + "This is not valid JSON": "Det här är inte giltig JSON", + "This key already exists on this schema": "Den här nyckeln finns redan i det här schemat", + "This schema must use Magic Table configuration to sync": "Det här schemat måste använda Magic Table-konfiguration för att synkronisera", + "This schema must use magic table configuration to sync": "Det här schemat måste använda magic table-konfiguration för att synkronisera", + "This source has no associated registers.": "Den här källan har inga associerade register.", + "This token will only be shown once. Copy it now.": "Den här token visas bara en gång. Kopiera den nu.", + "This will copy the _default ConfigSet with the new name": "Detta kopierar _default ConfigSet med det nya namnet", + "This will copy the _default configset with the new name": "Detta kopierar _default configset med det nya namnet", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Detta tar permanent bort ALLA inbäddningar (vektorer) från databasen. Du behöver revektorisera alla objekt och filer. Denna åtgärd kan inte ångras.\n\nÄr du säker på att du vill fortsätta?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Detta tar permanent bort ALLA inbäddningar (vektorer) från databasen. Du behöver revektorisera alla objekt och filer. Denna åtgärd kan inte ångras.\\n\\nÄr du säker på att du vill fortsätta?", + "This will permanently delete:": "Detta tar permanent bort:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Detta tar permanent bort {count} redan mjukraderade objekt från databasen. Denna åtgärd kan inte ångras.", + "Timeout (seconds)": "Timeout (sekunder)", + "Timestamp": "Tidsstämpel", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tips: Aktivera bara vyer som behöver semantisk sökning för att minimera inbäddningskostnader. Enkla uppslagsregister behöver sällan vektorisering.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Tips: aktivera bara vyer som behöver semantisk sökning för att minimera inbäddningskostnader. Enkla uppslagsregister behöver sällan vektorisering.", + "Title": "Titel", + "Title *": "Titel *", + "Title Template": "Titelmall", + "Title*": "Titel*", + "To (e.g. -1 year or 2025-12-31)": "Till (t.ex. -1 år eller 2025-12-31)", + "To Date": "Till datum", + "To add files larger than or equal to 512MB, go to the": "För att lägga till filer som är 512 MB eller större, gå till", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "För att justera blockstorlek eller strategi, gå till Filkonfiguration → Bearbetningsgränser.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "För att justera blockstorlek eller strategi, gå till filkonfiguration → bearbetningsgränser.", + "To change the embedding provider or model, go to LLM Configuration.": "För att ändra inbäddningsleverantör eller modell, gå till LLM-konfiguration.", + "To change the embedding provider or model, go to LLM configuration.": "För att ändra inbäddningsleverantör eller modell, gå till LLM-konfiguration.", + "To date": "Till datum", + "Toggle search sidebar": "Växla sökpanel", + "Token Created": "Token skapad", + "Token Quota (per request)": "Tokenkvot (per förfrågan)", + "Token copied to clipboard": "Token kopierades till urklipp", + "Token name": "Tokennamn", + "Token revoked": "Token återkallad", + "Tools": "Verktyg", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Verktyg körs med agentens standardanvändarbehörigheter när ingen användarsession är aktiv (t.ex. cron-jobb). Konfigurera standardanvändaren under fliken Inställningar.", + "Tools respect the agent's own role and organization boundaries.": "Verktyg respekterar agentens egna roll- och organisationsgränser.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Verktyg respekterar agentens vyer, behörigheter och organisationsgränser.", + "Top Deleters": "Topp-raderare", + "Total": "Totalt", + "Total Audit Trails": "Totala revisionsloggar", + "Total Chunks Available:": "Totala tillgängliga block:", + "Total Deleted Items": "Totala borttagna objekt", + "Total Members": "Totala medlemmar", + "Total Objects": "Totala objekt", + "Total Objects in Database:": "Totala objekt i databasen:", + "Total Organisations": "Totala organisationer", + "Total Results": "Totala resultat", + "Total Searches": "Totala sökningar", + "Total Size": "Total storlek", + "Total Storage": "Total lagring", + "Total events": "Totala händelser", + "Total:": "Totalt:", + "Totals": "Totaler", + "Try searching by UUID or with different keywords": "Försök söka med UUID eller med andra sökord", + "Type": "Typ", + "Type \"DELETE COLLECTION\" to confirm:": "Ange \"DELETE COLLECTION\" för att bekräfta:", + "Type Issue": "Typtproblem", + "Type Variations:": "Typvariationer:", + "Type filter (optional, e.g. email)": "Typfilter (valfritt, t.ex. e-post)", + "Type must be \"positive\" or \"negative\"": "Typen måste vara \"positive\" eller \"negative\"", + "Type to search for groups": "Ange text för att söka efter grupper", + "Type to search for objects...": "Ange text för att söka efter objekt...", + "Type to search for organisations": "Ange text för att söka efter organisationer", + "Type to search for users": "Ange text för att söka efter användare", + "Type to search groups...": "Ange text för att söka grupper...", + "Type to search users...": "Ange text för att söka användare...", + "Type to search...": "Ange text för att söka...", + "Type your username to confirm": "Ange ditt användarnamn för att bekräfta", + "URL": "URL", + "URL where Ollama is running": "URL där Ollama körs", + "UUID copied to clipboard": "UUID kopierades till urklipp", + "UUID:": "UUID:", + "Unchanged": "Oförändrad", + "Unique Search Terms": "Unika söktermer", + "Unique Terms": "Unika termer", + "Unknown": "Okänd", + "Unknown Object": "Okänt objekt", + "Unknown Register": "Okänt register", + "Unknown Schema": "Okänt schema", + "Unknown error": "Okänt fel", + "Unknown widget type: {type}": "Okänd widgettyp: {type}", + "Unlink email": "Ta bort länk för e-post", + "Unlink event": "Ta bort länk för händelse", + "Unnamed Object": "Namnlöst objekt", + "Untitled": "Namnlös", + "Untitled View": "Namnlös vy", + "Update": "Uppdatera", + "Update Operations": "Uppdateringsoperationer", + "Update available: {local} → {remote}": "Uppdatering tillgänglig: {local} → {remote}", + "Update configuration: ...": "Uppdatera konfiguration: ...", + "Update register OAS: ...": "Uppdatera register-OAS: ...", + "Update vectors when object data changes (recommended for accurate search)": "Uppdatera vektorer när objektdata ändras (rekommenderas för exakt sökning)", + "Updated": "Uppdaterad", + "Updated At": "Uppdaterad", + "Updated:": "Uppdaterad:", + "Upload new avatar": "Ladda upp ny avatar", + "Uploading configuration sets...": "Laddar upp konfigurationsuppsättningar...", + "Uptime:": "Drifttid:", + "Url": "Url", + "Usage Count": "Användningsantal", + "Use AI agents for processing and analysis": "Använd AI-agenter för bearbetning och analys", + "Use Large Language Model features": "Använd LLM-funktioner", + "Use SolrCloud with Zookeeper for distributed search": "Använd SolrCloud med Zookeeper för distribuerad sökning", + "Use SolrCloud with zookeeper for distributed search": "Använd SolrCloud med zookeeper för distribuerad sökning", + "Use VNG APIs instead of MongoDB.": "Använd VNG API:er istället för MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Använd standardintervall (Senaste 7/30/90 dagarna, Senaste år, Äldre)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Använd extern lagring (t.ex. MongoDB) istället för intern Nextcloud-lagring.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Använd filter för att begränsa revisionsloggsposter efter register, schema, åtgärdstyp, användare, datumintervall eller objekt-ID.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Använd filter för att begränsa borttagna objekt efter register, schema, borttagningsdatum eller användare som tog bort dem.", + "Use filters to narrow down entities by type or category.": "Använd filter för att begränsa entiteter efter typ eller kategori.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Använd filter för att begränsa sökloggsposter efter register, schema, framgångsstatus, användare, datumintervall, söktermer eller prestandamätvärden.", + "Used By Collections": "Används av samlingar", + "Used by": "Används av", + "User": "Användare", + "User Agent Statistics": "Statistik för användaragent", + "User Description": "Användarbeskrivning", + "User Quota": "Användarkvot", + "User rate limits cleared successfully": "Användarhastighetsbegränsningar rensades", + "User removed successfully": "Användare borttagen", + "User-friendly description shown in forms and help text": "Användarvänlig beskrivning som visas i formulär och hjälptext", + "Username": "Användarnamn", + "Username for SOLR authentication (optional)": "Användarnamn för SOLR-autentisering (valfritt)", + "Users": "Användare", + "Uses": "Använder", + "Using Pre-Generated Chunks": "Använder förgenerade block", + "Validate": "Validera", + "Validate Objects": "Validera objekt", + "Validation completed for {schema}": "Validering slutförd för {schema}", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Validering slutförd för {schema}: {processed} bearbetade, {updated} uppdaterade, {failed} misslyckades", + "Validation completed successfully": "Validering slutförd", + "Validation failed": "Validering misslyckades", + "Value": "Värde", + "Value list (split on ,)": "Värdelista (dela på ,)", + "Vector Dimensions": "Vektordimensioner", + "Vector Search Backend": "Vektorsök-backend", + "Vector field: _embedding_": "Vektorfält: _embedding_", + "Vectorization Triggers": "Vektoriseringsutlösare", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektorisering slutförd: {vectorized} objekt vektoriserade, {failed} misslyckades", + "Vectorization failed: {error}": "Vektorisering misslyckades: {error}", + "Vectorize All Files": "Vektorisera alla filer", + "Vectorize All Objects": "Vektorisera alla objekt", + "Vectorize all file types": "Vektorisera alla filtyper", + "Vectorize all views": "Vektorisera alla vyer", + "Vectorize on object creation": "Vektorisera vid objektskapande", + "Vectors will be stored in your existing object and file collections": "Vektorer lagras i dina befintliga objekt- och filsamlingar", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Verifierar serveranslutning...", + "Version": "Version", + "Version Information": "Versionsinformation", + "Very High": "Mycket hög", + "Very high": "Mycket hög", + "View": "Vy", + "View API Docs": "Visa API-dokumentation", + "View API Documentation": "Visa API-dokumentation", + "View APIs": "Visa API:er", + "View Changes": "Visa ändringar", + "View Details": "Visa detaljer", + "View Error": "Vyfel", + "View File": "Visa fil", + "View Full Details": "Visa fullständiga detaljer", + "View Logs": "Visa loggar", + "View Name": "Vynam", + "View Object": "Visa objekt", + "View Parameters": "Vyparametrar", + "View Selection": "Vyval", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Visa och analysera sökloggsposter med avancerade filter- och analysfunktioner", + "View and analyze system audit trails with advanced filtering capabilities": "Visa och analysera systemrevisionsloggar med avancerade filterfunktioner", + "View and manage authorization across registers and schemas": "Visa och hantera auktorisering över register och scheman", + "View deleted successfully": "Vy borttagen", + "View entity details and manage relations": "Visa entitetsdetaljer och hantera relationer", + "View name": "Vynamn", + "View name is required": "Vynamn krävs", + "View object": "Visa objekt", + "View saved successfully!": "Vy sparad!", + "View search analytics and manage search logs": "Visa sökanalys och hantera sökloggar", + "View successfully deleted": "Vy borttagen", + "View successfully updated": "Vy uppdaterad", + "View updated successfully!": "Vy uppdaterad!", + "View webhook delivery logs and filter by webhook": "Visa webhook-leveransloggar och filtrera efter webhook", + "View {count} more": "Visa {count} till", + "Views": "Vyer", + "Visible to end users": "Synlig för slutanvändare", + "Visible to users": "Synlig för användare", + "Wait for Response": "Vänta på svar", + "Wait for webhook response before continuing (required for request/response flows)": "Vänta på webhook-svar innan du fortsätter (krävs för begäran/svarsflöden)", + "Waiting for cluster synchronization...": "Väntar på klustersynkronisering...", + "Warmup Names Cache": "Värm upp namnscache", + "Warmup failed": "Uppvärmning misslyckades", + "Warning": "Varning", + "Warning:": "Varning:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Varning: Den här egenskapen har inkonsekventa typer: {types}. Fundera på om alla objekt bör ha samma typ.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Vi genomsöker alla objekt som tillhör det här schemat för att hitta nya egenskaper och analysera befintliga egenskaper för potentiella förbättringar. Processen innebär att undersöka varje objekts datastruktur, identifiera egenskaper som inte är definierade i det aktuella schemat och hitta möjligheter att förbättra befintliga egenskapsdefinitioner med bättre begränsningar, format och valideringsregler.", + "Webhook": "Webhook", + "Webhook Log Details": "Detaljer om webhook-logg", + "Webhook Logs": "Webhook-loggar", + "Webhook created successfully": "Webhook skapad", + "Webhook deleted": "Webhook borttagen", + "Webhook retry delivered successfully": "Webhook-återförsök levererades", + "Webhook retry delivery failed": "Webhook-återförsöks leverans misslyckades", + "Webhook updated": "Webhook uppdaterad", + "Webhook updated successfully": "Webhook uppdaterades", + "Webhooks": "Webhooks", + "Weekly": "Veckovis", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "När agenten körs utan användarsession (t.ex. schemalagda uppgifter) används den här användarens kontext", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "När agenten körs utan användarsession (t.ex. schemalagda uppgifter) används den här användarens kontext.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "När den är inaktiverad visar den här fasetten bara värden från det här schemat och inkluderar ett schemafilter när det väljs.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "När den är aktiverad uppdateras målobjekten vid sparande av det här objektet för att inkludera en referens tillbaka till det här objektet.", + "When enabled, this property will be removed from the source object after updating the target objects.": "När den är aktiverad tas den här egenskapen bort från källobjektet efter att målobjekten uppdaterats.", + "Workflow Management": "Arbetsflödeshantering", + "Workflows": "Arbetsflöden", + "Wrap webhook payload in CloudEvents format for better interoperability": "Omslut webhook-nyttolast i CloudEvents-format för bättre interoperabilitet", + "Wrap webhook payload in cloudevents format for better interoperability": "Omslut webhook-nyttolast i cloudevents-format för bättre interoperabilitet", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Ja", + "You": "Du", + "You are not yet a member of any organisations.": "Du är ännu inte medlem i någon organisation.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Du kan skapa nya ConfigSets baserat på _default-mallen eller ladda upp anpassade direkt till din SOLR-server.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Du kan skapa nya configsets baserat på _default-mallen eller ladda upp anpassade direkt till din SOLR-server.", + "You do not have access to this conversation": "Du har inte åtkomst till den här konversationen", + "You do not have permission to delete this conversation": "Du har inte behörighet att ta bort den här konversationen", + "You do not have permission to modify this conversation": "Du har inte behörighet att ändra den här konversationen", + "You do not have permission to restore this conversation": "Du har inte behörighet att återställa den här konversationen", + "You do not have permission to view the permission matrix. Admin access is required.": "Du har inte behörighet att visa behörighetsmatrisen. Administratörsåtkomst krävs.", + "You have selected {count} properties to add to the schema.": "Du har valt {count} egenskaper att lägga till i schemat.", + "You must be logged in to favorite views": "Du måste vara inloggad för att lägga till vyer i favoriter", + "You need an AI agent to start a conversation.": "Du behöver en AI-agent för att starta en konversation.", + "Your Fireworks AI API key. Get one at": "Din Fireworks AI API-nyckel. Skaffa en på", + "Your OpenAI API key. Get one at": "Din OpenAI API-nyckel. Skaffa en på", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Din feedback har registrerats. Valfritt kan du ange ytterligare detaljer här...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Din feedback har registrerats. Valfritt kan du ange ytterligare detaljer här...", + "Zookeeper Hosts": "Zookeeper-värdar", + "Zookeeper Port": "Zookeeper-port", + "Zookeeper Settings (SolrCloud)": "Zookeeper-inställningar (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Zookeeper-anslutningssträng för SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper-portnummer (valfritt, standard 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F VARNING FÖR PERMANENT RADERING \\u26A0\\uFE0F\n\nDu håller på att PERMANENT ta bort ALLA objekt för schemat \"{schema}\":\n\n\\u2022 Aktiva objekt: {active}\n\\u2022 Mjukraderade objekt: {deleted}\n\\u2022 Totalt: {total}\n\nDessa objekt tas helt bort från databasen och KAN INTE återställas.\n\nÄr du absolut säker?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F VARNING FÖR PERMANENT RADERING \\u26A0\\uFE0F\n\nDu håller på att PERMANENT ta bort {count} mjukraderade objekt för schemat \"{schema}\".\n\nDessa objekt tas helt bort från databasen och KAN INTE återställas.\n\nÄr du absolut säker?", + "and accessible to all users in selected groups (or all users if no groups selected).": "och tillgänglig för alla användare i valda grupper (eller alla användare om inga grupper valts).", + "and add the files there.": "och lägg till filerna där.", + "approved": "godkänd", + "are only accessible to invited users.": "är endast tillgängliga för inbjudna användare.", + "by": "av", + "chunk": "block", + "chunks": "block", + "chunks processed": "block bearbetade", + "completed": "slutförd", + "configuration(s)": "konfiguration(er)", + "conversationId is required": "conversationId krävs", + "create": "skapa", + "delete": "ta bort", + "deleted objects section": "sektion för borttagna objekt", + "e.g., lib/Settings/config.json": "t.ex. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "t.ex. lib/Settings/register.json", + "e.g., lib/settings/config.json": "t.ex. lib/settings/config.json", + "e.g., lib/settings/register.json": "t.ex. lib/settings/register.json", + "execute": "kör", + "failed": "misslyckades", + "file": "fil", + "files": "filer", + "file{plural}": "fil{plural}", + "folder": "mapp", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "i {register}", + "in_progress": "pågår", + "inherited": "ärvd", + "list": "lista", + "log{plural}": "log{plural}", + "manage": "hantera", + "members": "medlemmar", + "menu or contact someone with permission to create agents.": "menyn eller kontakta någon med behörighet att skapa agenter.", + "message content is required": "meddelandeinnehåll krävs", + "messages": "meddelanden", + "missing": "saknas", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL och API-nyckel krävs", + "n8n configuration saved successfully": "n8n-konfiguration sparades", + "n8n connection not configured": "n8n-anslutning inte konfigurerad", + "n8n connection successful": "n8n-anslutning lyckades", + "n8n connection test failed: {message}": "n8n-anslutningstest misslyckades: {message}", + "n8n connection test successful": "n8n-anslutningstest lyckades", + "n8n initialization failed: {message}": "n8n-initiering misslyckades: {message}", + "n8n integration disabled": "n8n-integration inaktiverad", + "n8n integration enabled": "n8n-integration aktiverad", + "n8n project initialized successfully": "n8n-projekt initierat", + "n8n settings saved successfully": "n8n-inställningar sparades", + "notify_push configuration guide": "konfigurationsguide för notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push är installerad men OpenRegister har ännu inte bekräftat en lyckad push. Utlös en objektsparning för att aktivera, eller kontrollera din notify_push-konfiguration.", + "notify_push is installed but not yet active": "notify_push är installerad men ännu inte aktiv", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "objekt", + "objects processed": "objekt bearbetade", + "object{plural}": "objekt{plural}", + "or pick a range": "eller välj ett intervall", + "org-...": "org-...", + "pending": "väntar", + "provides a good balance between speed and accuracy.": "ger en god balans mellan hastighet och noggrannhet.", + "public": "offentlig", + "read": "läs", + "register(s)": "register", + "register(s) selected": "register valda", + "register{plural}": "register{plural}", + "rejected": "avvisad", + "required": "obligatorisk", + "results": "resultat", + "schema(s)": "schema(n)", + "schema(s) selected": "schema(n) valda", + "schema{plural}": "schema{plural}", + "selected objects": "valda objekt", + "set": "ange", + "sk-...": "sk-...", + "this application": "den här applikationen", + "to": "till", + "type must be \"positive\" or \"negative\"": "typen måste vara \"positive\" eller \"negative\"", + "update": "uppdatera", + "vectorized": "vektoriserad", + "view": "vy", + "views selected": "vyer valda", + "waiting": "väntar", + "{active} active, {deleted} deleted": "{active} aktiva, {deleted} borttagna", + "{count} audit trails cleared successfully": "{count} revisionsloggar rensades", + "{count} compliance issue(s) detected": "{count} efterlevnadsproblem identifierade", + "{count} deletions": "{count} borttagningar", + "{count} documents found": "{count} dokument hittades", + "{count} email": "{count} e-post", + "{count} emails": "{count} e-postmeddelanden", + "{count} entries": "{count} poster", + "{count} issues": "{count} problem", + "{count} members": "{count} medlemmar", + "{count} searches": "{count} sökningar", + "{count} selected": "{count} valda", + "{count} widget(s)": "{count} widget(ar)", + "{matched} object(s) matched. {erased} erased.": "{matched} objekt matchade. {erased} raderades.", + "{minutes} minutes ago": "för {minutes} minuter sedan", + "{percentage}% of objects": "{percentage}% av objekt", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} i {register} / {schema}", + "{type} cache cleared successfully": "{type}-cache rensades", + "ℹ️ Current Configuration": "ℹ️ Aktuell konfiguration", + "ℹ️ current configuration": "ℹ️ aktuell konfiguration", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ FARA: Alla {total} objekt kommer att PERMANENT RADERAS från databasen. Denna åtgärd är OÅTERKALLERLIG och kan inte ångras!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ VARNING FÖR PERMANENT RADERING ⚠️\n\nDu håller på att PERMANENT ta bort ALLA objekt för schemat \"{schema}\":\n\n• Aktiva objekt: {active}\n• Mjukraderade objekt: {deleted}\n• Totalt: {total}\n\nDessa objekt tas helt bort från databasen och KAN INTE återställas.\n\nÄr du absolut säker?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ VARNING FÖR PERMANENT RADERING ⚠️\n\nDu håller på att PERMANENT ta bort {count} mjukraderade objekt för schemat \"{schema}\".\n\nDessa objekt tas helt bort från databasen och KAN INTE återställas.\n\nÄr du absolut säker?", + "⚡ Batch Processing": "⚡ Batchbearbetning", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR kan hantera miljontals dokument med söksvarstider under en sekund...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Heltäckande kontext ✓ Mindre risk att missa detaljer ✗ Långsammare svar ✗ Kan inkludera mindre relevant information", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Snabbare svar ✓ Mer fokuserade svar ✗ Kan missa relevant information", + "✨ AI Features": "✨ AI-funktioner", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 I distribuerat läge använder SOLR ZooKeeper för klusterkoordinering...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasetterad sökning låter användare borra ner i resultat efter kategorier...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Visste du det? SOLR driver sökning för Netflix, Apple och många andra stora sajter!", + "💰 View Selection (Cost Optimization)": "💰 Vyval (kostnadsoptimering)", + "📄 Object Serialization": "📄 Objektserialisering", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR tillhandahåller rik analys och statistik om sökprestanda...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSets innehåller schema- och konfigurationsfiler för ditt sökindex...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikering säkerställer att ditt sökindex är tillgängligt även om noder misslyckas...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR är en kraftfull sökmotorsplattform för företag byggd på Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Vektoriseringsinställningar", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Autoskalning kan dynamiskt lägga till eller ta bort noder baserat på belastning...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Säkerhetsfunktioner inkluderar autentisering, auktorisering och SSL-kryptering...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-post", + "{count} e-postmeddelanden" + ], + "Purge {count} object from database": [ + "Rensa {count} objekt från databasen", + "Rensa {count} objekt från databasen" + ], + "Restore {count} object": [ + "Återställ {count} objekt", + "Återställ {count} objekt" + ], + "Successfully restored {count} object": [ + "{count} objekt återställdes", + "{count} objekt återställdes" + ], + "Delete {count} object": [ + "Ta bort {count} objekt", + "Ta bort {count} objekt" + ], + "Object successfully deleted": [ + "Objekt togs bort", + "Objekt togs bort" + ] + } +} diff --git a/l10n/tr.js b/l10n/tr.js new file mode 100644 index 0000000000..a086dc5c92 --- /dev/null +++ b/l10n/tr.js @@ -0,0 +1,2410 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(Dolphin required)" : "(Dolphin gerekli)", + "(no subject)" : "(konu yok)", + "(no title)" : "(başlık yok)", + "(unnamed)" : "(adsız)", + "(untitled card)" : "(başlıksız kart)", + "({count} selected)" : "({count} seçildi)", + ", {failed} failed" : ", {failed} başarısız", + "0 = unlimited" : "0 = sınırsız", + "A deactivation request is pending." : "Bir devre dışı bırakma isteği beklemede.", + "AI Assistant" : "Yapay Zeka Asistanı", + "AI Chat" : "Yapay Zeka Sohbeti", + "AI service not configured" : "Yapay zeka servisi yapılandırılmamış", + "API Key" : "API Anahtarı", + "API Request Quota (requests/day)" : "API İstek Kotası (istek/gün)", + "API Token Configuration" : "API Token Yapılandırması", + "API Tokens" : "API Token'ları", + "API tokens saved successfully" : "API token'ları başarıyla kaydedildi", + "AVG / Verwerkingsregister" : "AVG / Verwerkingsregister", + "About ConfigSets" : "ConfigSet'ler Hakkında", + "Access denied" : "Erişim reddedildi", + "Access organisation dashboard and analytics" : "Kuruluş panosuna ve analizlerine erişin", + "Account" : "Hesap", + "Action" : "Eylem", + "Action Distribution" : "Eylem Dağılımı", + "Action:" : "Eylem:", + "Actions" : "Eylemler", + "Active" : "Etkin", + "Active Collections" : "Etkin Koleksiyonlar", + "Active Filters" : "Etkin Filtreler", + "Active Organisation:" : "Etkin Kuruluş:", + "Active Organisations" : "Etkin Kuruluşlar", + "Active filters:" : "Etkin filtreler:", + "Active organisation changed successfully" : "Etkin kuruluş başarıyla değiştirildi", + "Active organisation set successfully" : "Etkin kuruluş başarıyla ayarlandı", + "Activities" : "Etkinlikler", + "Activity" : "Etkinlik", + "Activity Period" : "Etkinlik Dönemi", + "Add" : "Ekle", + "Add Attachment" : "Ek Ekle", + "Add Endpoint" : "Uç Nokta Ekle", + "Add Groups" : "Grup Ekle", + "Add Property to '{title}'" : "'{title}' Öğesine Özellik Ekle", + "Add Register" : "Kayıt Ekle", + "Add Schema" : "Şema Ekle", + "Add Source" : "Kaynak Ekle", + "Add User" : "Kullanıcı Ekle", + "Add User to Organisation" : "Kuruluşa Kullanıcı Ekle", + "Add a contact from any of your address books to associate it with this object." : "Bu nesneyle ilişkilendirmek için adres defterlerinizden herhangi birinden kişi ekleyin.", + "Add a file or files" : "Dosya veya dosyalar ekle", + "Add card" : "Kart ekle", + "Add contact" : "Kişi ekle", + "Add endpoint" : "Uç nokta ekle", + "Add oneOf entry" : "oneOf girişi ekle", + "Add range" : "Aralık ekle", + "Add to favorites" : "Favorilere ekle", + "Added" : "Eklendi", + "Added to favorites" : "Favorilere eklendi", + "Additional Information" : "Ek Bilgi", + "Additional feedback saved. Thank you!" : "Ek geri bildirim kaydedildi. Teşekkürler!", + "Addresses" : "Adresler", + "Admin" : "Yönetici", + "Advanced" : "Gelişmiş", + "Advanced Filters" : "Gelişmiş Filtreler", + "Advanced Options" : "Gelişmiş Seçenekler", + "After" : "Sonra", + "Agent Type" : "Ajan Türü", + "Agent deleted successfully" : "Ajan başarıyla silindi", + "Agent successfully created" : "Ajan başarıyla oluşturuldu", + "Agent successfully updated" : "Ajan başarıyla güncellendi", + "Agents" : "Ajanlar", + "Aggregated across schemas" : "Şemalar genelinde toplanmış", + "All" : "Tümü", + "All Categories" : "Tüm Kategoriler", + "All Collections" : "Tüm Koleksiyonlar", + "All Confidence Levels" : "Tüm Güven Seviyeleri", + "All ConfigSets" : "Tüm ConfigSet'ler", + "All Files" : "Tüm Dosyalar", + "All Levels" : "Tüm Seviyeler", + "All Types" : "Tüm Türler", + "All Webhooks" : "Tüm Webhook'lar", + "All actions" : "Tüm eylemler", + "All audit trails cleared successfully" : "Tüm denetim izleri başarıyla temizlendi", + "All clear" : "Her şey tamam", + "All file types" : "Tüm dosya türleri", + "All registers" : "Tüm kayıtlar", + "All schemas" : "Tüm şemalar", + "All search trails cleared successfully" : "Tüm arama izleri başarıyla temizlendi", + "All searches" : "Tüm aramalar", + "All users" : "Tüm kullanıcılar", + "All-day events" : "Tüm gün etkinlikleri", + "Allowed MIME Types" : "İzin Verilen MIME Türleri", + "Already linked" : "Zaten bağlantılı", + "An error occurred while clearing audit trails" : "Denetim izleri temizlenirken bir hata oluştu", + "An error occurred while deleting the audit trail" : "Denetim izi silinirken bir hata oluştu", + "An error occurred while deleting the object" : "Nesne silinirken bir hata oluştu", + "An error occurred while deleting the view" : "Görünüm silinirken bir hata oluştu", + "An error occurred while permanently deleting the objects" : "Nesneler kalıcı olarak silinirken bir hata oluştu", + "An error occurred while restoring the objects" : "Nesneler geri yüklenirken bir hata oluştu", + "An error occurred while saving the organisation" : "Kuruluş kaydedilirken bir hata oluştu", + "Analysis completed:" : "Analiz tamamlandı:", + "Analysis steps:" : "Analiz adımları:", + "Analytics" : "Analitik", + "Analyze Objects" : "Nesneleri Analiz Et", + "Analyze Properties" : "Özellikleri Analiz Et", + "Analyze existing properties for improvement opportunities" : "İyileştirme fırsatları için mevcut özellikleri analiz et", + "Analyzing..." : "Analiz ediliyor...", + "Anonymized" : "Anonimleştirilmiş", + "Anonymous Group" : "Anonim Grup", + "Any favorites and sharing settings for this view" : "Bu görünüm için herhangi bir favori ve paylaşım ayarı", + "Any user" : "Herhangi bir kullanıcı", + "App ID" : "Uygulama Kimliği", + "App store cache invalidated successfully" : "Uygulama mağazası önbelleği başarıyla geçersiz kılındı", + "App store cache invalidated: {invalidated}" : "Uygulama mağazası önbelleği geçersiz kılındı: {invalidated}", + "Application deleted successfully" : "Uygulama başarıyla silindi", + "Application identifier for this configuration (optional)" : "Bu yapılandırma için uygulama tanımlayıcısı (isteğe bağlı)", + "Application settings" : "Uygulama ayarları", + "Application successfully created" : "Uygulama başarıyla oluşturuldu", + "Application successfully updated" : "Uygulama başarıyla güncellendi", + "Applications" : "Uygulamalar", + "Apply" : "Uygula", + "Apply Changes" : "Değişiklikleri Uygula", + "Approval Progress" : "Onay İlerlemesi", + "Approve" : "Onayla", + "Approved" : "Onaylandı", + "Archive" : "Arşiv", + "Archive conversation" : "Konuşmayı arşivle", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it." : "Bu verwerkingsactiviteit arşivlensin mi? Denetim izi satırları buna atıfta bulunmaya devam edecek.", + "Archived conversations are hidden from your active list" : "Arşivlenen konuşmalar etkin listenizden gizlenir", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone." : "\"{name}\" uygulamasını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Seçilen denetim izlerini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Seçilen arama izlerini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to delete this ConfigSet?" : "Bu ConfigSet'i silmek istediğinizden emin misiniz?", + "Are you sure you want to delete this configset?" : "Bu configset'i silmek istediğinizden emin misiniz?", + "Are you sure you want to leave '{name}'?" : "'{name}' grubundan ayrılmak istediğinizden emin misiniz?", + "Are you sure you want to permanently delete" : "Kalıcı olarak silmek istediğinizden emin misiniz", + "Array Object Configuration:" : "Dizi Nesne Yapılandırması:", + "Articles" : "Makaleler", + "Ask a question..." : "Soru sor...", + "Ask questions about your data using natural language" : "Doğal dil kullanarak verileriniz hakkında soru sorun", + "Assigned collaborative tags" : "Atanan işbirliği etiketleri", + "Assignment notifications" : "Atama bildirimleri", + "Attachment added to {name}" : "{name} öğesine ek eklendi", + "Attempt" : "Deneme", + "Audit Trail Changes" : "Denetim İzi Değişiklikleri", + "Audit Trail Details" : "Denetim İzi Ayrıntıları", + "Audit Trail Management" : "Denetim İzi Yönetimi", + "Audit Trail Statistics" : "Denetim İzi İstatistikleri", + "Audit Trails" : "Denetim İzleri", + "Audit trail data copied to clipboard" : "Denetim izi verileri panoya kopyalandı", + "Audit trail deleted successfully" : "Denetim izi başarıyla silindi", + "Audit trail successfully deleted" : "Denetim izi başarıyla silindi", + "Authentication" : "Kimlik Doğrulama", + "Auto (e.g. Y for year, F Y for month)" : "Otomatik (örn. Y yıl için, F Y ay için)", + "Auto share" : "Otomatik paylaş", + "Auto-commit disabled" : "Otomatik kayıt devre dışı", + "Auto-commit enabled" : "Otomatik kayıt etkin", + "Auto-create Default Organisation" : "Varsayılan Kuruluşu Otomatik Oluştur", + "Auto-retry failed vectorizations" : "Başarısız vektörleştirmeleri otomatik yeniden dene", + "Automatically commit changes to SOLR index" : "Değişiklikleri SOLR dizinine otomatik olarak kaydet", + "Automatically connect to the federation network." : "Federasyon ağına otomatik olarak bağlan.", + "Automatically generate vector embeddings when objects are created or updated" : "Nesneler oluşturulduğunda veya güncellendiğinde vektör gömülümleri otomatik olarak oluştur", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Başarısız vektörleştirme girişimlerini otomatik olarak yeniden dene (en fazla 3 deneme)", + "Automatically update catalogues." : "Katalogları otomatik olarak güncelle.", + "Available Filters" : "Mevcut Filtreler", + "Available Workflows" : "Mevcut İş Akışları", + "Available placeholders:" : "Mevcut yer tutucular:", + "Avatar" : "Avatar", + "Avatar changes are not supported by your authentication provider." : "Kimlik doğrulama sağlayıcınız avatar değişikliklerini desteklemiyor.", + "Avatar removed" : "Avatar kaldırıldı", + "Avatar updated successfully" : "Avatar başarıyla güncellendi", + "Avg Execution Time" : "Ort. Yürütme Süresi", + "Avg Members/Org" : "Ort. Üyeler/Kuruluş", + "Avg Object Views/Session" : "Ort. Nesne Görüntüleme/Oturum", + "Avg Response Time" : "Ort. Yanıt Süresi", + "Avg Results/Search" : "Ort. Sonuç/Arama", + "Avg Searches/Session" : "Ort. Arama/Oturum", + "Back" : "Geri", + "Back to Entities" : "Varlıklara Geri Dön", + "Back to Registers" : "Kayıtlara Geri Dön", + "Back to Webhooks" : "Webhook'lara Geri Dön", + "Back to applications" : "Uygulamalara geri dön", + "Back to entities" : "Varlıklara geri dön", + "Backend parameter is required" : "Backend parametresi gereklidir", + "Backend updated successfully. Please reload the application." : "Backend başarıyla güncellendi. Lütfen uygulamayı yeniden yükleyin.", + "Bandwidth Quota (MB/month)" : "Bant Genişliği Kotası (MB/ay)", + "Base URL (Optional)" : "Temel URL (İsteğe Bağlı)", + "Basic Information" : "Temel Bilgi", + "Batch Size" : "Toplu İş Boyutu", + "Batch extraction completed" : "Toplu çıkarma tamamlandı", + "Before" : "Önce", + "Before object vectorization can work:" : "Nesne vektörleştirmesi çalışabilmeden önce:", + "Behavior" : "Davranış", + "Behavior Issue" : "Davranış Sorunu", + "Beschrijving" : "Beschrijving", + "Bewaartermijn" : "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)" : "Bewaartermijn (ISO-8601 süresi, örn. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables." : "Blob depolama kullanımdan kaldırıldı. Tüm nesneler artık sihirli tabloları kullanıyor.", + "Bookmarks" : "Yer İşaretleri", + "Branch" : "Dal", + "Bulk Role Assignment: {title}" : "Toplu Rol Atama: {title}", + "Bulk delete operation completed successfully" : "Toplu silme işlemi başarıyla tamamlandı", + "Bulk save operation completed successfully" : "Toplu kaydetme işlemi başarıyla tamamlandı", + "Business Data" : "İş Verileri", + "CSV" : "CSV", + "Cache cleared successfully" : "Önbellek başarıyla temizlendi", + "Cache grew from {old} to {new} entries." : "Önbellek {old} girişten {new} girişe yükseldi.", + "Cache shrunk from {old} to {new} entries." : "Önbellek {old} girişten {new} girişe düştü.", + "Cache stayed the same at {size} entries." : "Önbellek {size} girişte aynı kaldı.", + "Calculate Sizes" : "Boyutları Hesapla", + "Calendar" : "Takvim", + "Calendar Provider Configuration" : "Takvim Sağlayıcı Yapılandırması", + "Calendar integration is not available" : "Takvim entegrasyonu mevcut değil", + "Calendar name" : "Takvim adı", + "Cancel" : "İptal", + "Cancel deactivation request" : "Devre dışı bırakma isteğini iptal et", + "Cannot delete: objects are still attached" : "Silinemiyor: nesneler hâlâ ekli", + "Cannot edit: This register is managed by external configuration {title}" : "Düzenlenemiyor: Bu kayıt {title} harici yapılandırmasıyla yönetiliyor", + "Cannot remove schema with existing objects ({count} objects)" : "Mevcut nesnelerle ({count} nesne) şema kaldırılamıyor", + "Cannot remove schema {schema} because it contains {count} objects" : "{schema} şeması {count} nesne içerdiğinden kaldırılamıyor", + "Cards" : "Kartlar", + "Cascade delete" : "Ardışık silme", + "Categories of data subjects (one per line)" : "Veri sahipleri kategorileri (her satırda bir)", + "Categories of personal data (one per line)" : "Kişisel veri kategorileri (her satırda bir)", + "Categorieën betrokkenen (één per regel)" : "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)" : "Categorieën persoonsgegevens (één per regel)", + "Category" : "Kategori", + "Change Type" : "Değişiklik Türü", + "Change password" : "Şifre değiştir", + "Changes" : "Değişiklikler", + "Changes copied to clipboard" : "Değişiklikler panoya kopyalandı", + "Changes for Audit Trail #{id}" : "#{id} Denetim İzi için Değişiklikler", + "Chat Model" : "Sohbet Modeli", + "Chat Provider" : "Sohbet Sağlayıcı", + "Chat Provider (RAG)" : "Sohbet Sağlayıcı (RAG)", + "Chat Settings" : "Sohbet Ayarları", + "Chat provider connection successful!" : "Sohbet sağlayıcı bağlantısı başarılı!", + "Chat settings" : "Sohbet ayarları", + "Chat test failed: {error}" : "Sohbet testi başarısız: {error}", + "Choose a register" : "Bir kayıt seçin", + "Choose a schema" : "Bir şema seçin", + "Choose how vector similarity calculations are performed for semantic search" : "Anlamsal arama için vektör benzerlik hesaplamalarının nasıl yapılacağını seçin", + "Chunk" : "Parça", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Parça silme henüz uygulanmadı. Parça tabanlı uç noktaları kullanın.", + "Chunks" : "Parçalar", + "Chunks to Vectorize:" : "Vektörleştirilecek Parçalar:", + "Cleanup Old Trails" : "Eski İzleri Temizle", + "Cleanup completed" : "Temizleme tamamlandı", + "Cleanup completed successfully. Deleted {count} entries." : "Temizleme başarıyla tamamlandı. {count} giriş silindi.", + "Cleanup failed: {error}" : "Temizleme başarısız: {error}", + "Clear All Embeddings" : "Tüm Gömülümleri Temizle", + "Clear Entries" : "Girişleri Temizle", + "Clear Filtered Audit Trails" : "Filtrelenmiş Denetim İzlerini Temizle", + "Clear Filters" : "Filtreleri Temizle", + "Clear Index" : "Dizini Temizle", + "Clear Selection" : "Seçimi Temizle", + "Clear all" : "Tümünü temizle", + "Clear all filters" : "Tüm filtreleri temizle", + "Clear date range" : "Tarih aralığını temizle", + "Clear filters" : "Filtreleri temizle", + "Clearing..." : "Temizleniyor...", + "Click to subscribe to notifications" : "Bildirimlere abone olmak için tıklayın", + "Click to unsubscribe from notifications" : "Bildirimlerden çıkmak için tıklayın", + "Close" : "Kapat", + "Cluster name" : "Küme adı", + "Code" : "Kod", + "Code (short readable key, e.g. v-2026-001)" : "Kod (kısa okunabilir anahtar, örn. v-2026-001)", + "Collapse details" : "Ayrıntıları daralt", + "Collection Management" : "Koleksiyon Yönetimi", + "Collection Name" : "Koleksiyon Adı", + "Collection assignments updated successfully" : "Koleksiyon atamaları başarıyla güncellendi", + "Collection cleared successfully" : "Koleksiyon başarıyla temizlendi", + "Collection copied successfully" : "Koleksiyon başarıyla kopyalandı", + "Collection created successfully" : "Koleksiyon başarıyla oluşturuldu", + "Collection deleted successfully" : "Koleksiyon başarıyla silindi", + "Collection used to store and index file metadata and content" : "Dosya meta verilerini ve içeriğini depolamak ve dizinlemek için kullanılan koleksiyon", + "Collection used to store and index object data" : "Nesne verilerini depolamak ve dizinlemek için kullanılan koleksiyon", + "Collections:" : "Koleksiyonlar:", + "Color" : "Renk", + "Comment..." : "Yorum...", + "Commit Message" : "Kayıt Mesajı", + "Commit Within (ms)" : "Süre İçinde Kaydet (ms)", + "Commit message" : "Kayıt mesajı", + "Compare current schema with real object data" : "Mevcut şemayı gerçek nesne verileriyle karşılaştır", + "Completed" : "Tamamlandı", + "Complex" : "Karmaşık", + "Compliance" : "Uyumluluk", + "Confidence Level" : "Güven Seviyesi", + "Confidence Score" : "Güven Puanı", + "Confidence: {confidence}%" : "Güven: {confidence}%", + "Config must be provided as an object" : "Yapılandırma bir nesne olarak sağlanmalıdır", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "ConfigSet Yönetimi", + "ConfigSet Name" : "ConfigSet Adı", + "ConfigSet created successfully" : "ConfigSet başarıyla oluşturuldu", + "ConfigSet deleted successfully" : "ConfigSet başarıyla silindi", + "ConfigSet:" : "ConfigSet:", + "Configuration" : "Yapılandırma", + "Configuration Data" : "Yapılandırma Verisi", + "Configuration is up to date" : "Yapılandırma güncel", + "Configuration saved" : "Yapılandırma kaydedildi", + "Configuration saved successfully." : "Yapılandırma başarıyla kaydedildi.", + "Configuration type (default, application, etc.)" : "Yapılandırma türü (varsayılan, uygulama vb.)", + "Configurations" : "Yapılandırmalar", + "Configure API tokens for external service integrations" : "Harici servis entegrasyonları için API token'larını yapılandırın", + "Configure Apache SOLR search engine for advanced search capabilities" : "Gelişmiş arama özellikleri için Apache SOLR arama motorunu yapılandırın", + "Configure Facets" : "Fasetleri Yapılandır", + "Configure SOLR Facets" : "SOLR Fasetlerini Yapılandır", + "Configure default organisation and organisation-related settings" : "Varsayılan kuruluşu ve kuruluşla ilgili ayarları yapılandırın", + "Configure multi-organization support and tenant isolation" : "Çoklu kuruluş desteği ve kiracı izolasyonunu yapılandırın", + "Configure this schema to surface objects as events in the Nextcloud Calendar app." : "Bu şemayı Nextcloud Takvim uygulamasında nesneleri etkinlik olarak gösterecek şekilde yapılandırın.", + "Configure which types of data to search and how many sources to retrieve" : "Hangi tür verilerin aranacağını ve kaç kaynağın alınacağını yapılandırın", + "Configuring field mappings..." : "Alan eşlemeleri yapılandırılıyor...", + "Confirm Account Deactivation" : "Hesap Devre Dışı Bırakmayı Onayla", + "Confirm Object Deletion" : "Nesne Silmeyi Onayla", + "Confirm Object Restoration" : "Nesne Geri Yüklemeyi Onayla", + "Confirm Permanent Object Deletion" : "Nesnenin Kalıcı Silmesini Onayla", + "Confirm deactivation" : "Devre dışı bırakmayı onayla", + "Connecting to SOLR cluster..." : "SOLR kümesine bağlanılıyor...", + "Connection Failed" : "Bağlantı Başarısız", + "Connection Settings" : "Bağlantı Ayarları", + "Connection Successful!" : "Bağlantı Başarılı!", + "Connection failed" : "Bağlantı başarısız", + "Connection protocol" : "Bağlantı protokolü", + "Connection successful" : "Bağlantı başarılı", + "Connection timeout in seconds" : "Saniye cinsinden bağlantı zaman aşımı", + "Constant delay between retries (5 minutes)" : "Yeniden denemeler arasında sabit gecikme (5 dakika)", + "Constraint Issue" : "Kısıtlama Sorunu", + "Consumer not found" : "Tüketici bulunamadı", + "Contacts" : "Kişiler", + "Contacts integration is not available" : "Kişiler entegrasyonu mevcut değil", + "Content Filters" : "İçerik Filtreleri", + "Continue to Properties" : "Özelliklere Devam Et", + "Contracts" : "Sözleşmeler", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Rastgeleliği kontrol eder (0 = belirleyici, 2 = çok yaratıcı)", + "Conversation ID is required" : "Konuşma kimliği gereklidir", + "Conversation archived" : "Konuşma arşivlendi", + "Conversation archived successfully" : "Konuşma başarıyla arşivlendi", + "Conversation cleared successfully" : "Konuşma başarıyla temizlendi", + "Conversation deleted" : "Konuşma silindi", + "Conversation not found" : "Konuşma bulunamadı", + "Conversation permanently deleted" : "Konuşma kalıcı olarak silindi", + "Conversation renamed" : "Konuşma yeniden adlandırıldı", + "Conversation restored" : "Konuşma geri yüklendi", + "Conversation started with {agent}" : "{agent} ile konuşma başlatıldı", + "Conversation title" : "Konuşma başlığı", + "Conversations" : "Konuşmalar", + "Converting {schema} to blob storage..." : "{schema} blob depolamaya dönüştürülüyor...", + "Converting {schema} to magic table..." : "{schema} sihirli tabloya dönüştürülüyor...", + "Copied!" : "Kopyalandı!", + "Copy" : "Kopyala", + "Copy Changes" : "Değişiklikleri Kopyala", + "Copy Collection" : "Koleksiyonu Kopyala", + "Copy Data" : "Veriyi Kopyala", + "Copy Full Data" : "Tüm Veriyi Kopyala", + "Copy of {name}" : "{name} kopyası", + "Copy to clipboard" : "Panoya kopyala", + "Copying..." : "Kopyalanıyor...", + "Costs" : "Maliyetler", + "Count" : "Sayı", + "Counting objects..." : "Nesneler sayılıyor...", + "Create" : "Oluştur", + "Create API Token" : "API Token Oluştur", + "Create Agent" : "Ajan Oluştur", + "Create Application" : "Uygulama Oluştur", + "Create Collection" : "Koleksiyon Oluştur", + "Create ConfigSet" : "ConfigSet Oluştur", + "Create New Collection" : "Yeni Koleksiyon Oluştur", + "Create New ConfigSet" : "Yeni ConfigSet Oluştur", + "Create Operations" : "Oluşturma İşlemleri", + "Create Organisation" : "Kuruluş Oluştur", + "Create Webhook" : "Webhook Oluştur", + "Create a copy of collection:" : "Koleksiyonun kopyasını oluştur:", + "Create a new ConfigSet based on the _default template" : "_default şablonuna dayalı yeni bir ConfigSet oluştur", + "Create a new SOLR collection from an existing ConfigSet" : "Mevcut bir ConfigSet'ten yeni bir SOLR koleksiyonu oluştur", + "Create a new SOLR collection from an existing configset" : "Mevcut bir configset'ten yeni bir SOLR koleksiyonu oluştur", + "Create a new configset based on the _default template" : "_default şablonuna dayalı yeni bir configset oluştur", + "Create a new event or link an existing one from any of your calendars." : "Yeni bir etkinlik oluşturun veya takvimlerinizden herhangi birinden mevcut bir etkinliği bağlayın.", + "Create another" : "Başka bir tane oluştur", + "Create copies of" : "Kopyaları oluştur", + "Create event" : "Etkinlik oluştur", + "Create new token" : "Yeni token oluştur", + "Create or link a Deck card to track work on this object." : "Bu nesnedeki çalışmayı takip etmek için Deck kartı oluşturun veya bağlayın.", + "Create your first AI agent to get started." : "Başlamak için ilk yapay zeka ajanınızı oluşturun.", + "Created" : "Oluşturuldu", + "Created:" : "Oluşturuldu:", + "Creating search collections..." : "Arama koleksiyonları oluşturuluyor...", + "Creating..." : "Oluşturuluyor...", + "Current" : "Mevcut", + "Current Filters" : "Mevcut Filtreler", + "Current Type:" : "Mevcut Tür:", + "Current password" : "Mevcut şifre", + "Custom API endpoint if using a different region" : "Farklı bir bölge kullanılıyorsa özel API uç noktası", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Özel HTTP başlıkları (her satırda bir, biçim: Başlık-Adı: değer)", + "Custom HTTP headers (one per line, format: header-name: value)" : "Özel HTTP başlıkları (her satırda bir, biçim: başlık-adı: değer)", + "Custom Ranges:" : "Özel Aralıklar:", + "Custom display title for this facet" : "Bu faset için özel görüntüleme başlığı", + "Custom pattern:" : "Özel desen:", + "DELETE COLLECTION" : "KOLEKSİYONU SİL", + "DSAR" : "DSAR", + "Daily" : "Günlük", + "Dashboard" : "Gösterge Paneli", + "Dashboard not found" : "Gösterge paneli bulunamadı", + "Data" : "Veri", + "Data Source" : "Veri Kaynağı", + "Data Sources" : "Veri Kaynakları", + "Data Views" : "Veri Görünümleri", + "Data sources" : "Veri kaynakları", + "Data type does not match observed values" : "Veri türü gözlemlenen değerlerle eşleşmiyor", + "Data-subject access request" : "Veri sahibi erişim talebi", + "Database URL" : "Veritabanı URL'si", + "Database information refreshed" : "Veritabanı bilgisi yenilendi", + "Date Faceting:" : "Tarih Fasetleme:", + "Date Range" : "Tarih Aralığı", + "Dates" : "Tarihler", + "Deactivation request cancelled" : "Devre dışı bırakma isteği iptal edildi", + "Deactivation request submitted" : "Devre dışı bırakma isteği gönderildi", + "Deck" : "Deck", + "Deck integration is not available" : "Deck entegrasyonu mevcut değil", + "Default" : "Varsayılan", + "Default Object Tenant" : "Varsayılan Nesne Kiracısı", + "Default Organisation" : "Varsayılan Kuruluş", + "Default Owner" : "Varsayılan Sahip", + "Default User (for cron/background jobs)" : "Varsayılan Kullanıcı (cron/arka plan işleri için)", + "Default User Tenant" : "Varsayılan Kullanıcı Kiracısı", + "Default value" : "Varsayılan değer", + "Default value (Email)" : "Varsayılan değer (E-posta)", + "Default value (Password)" : "Varsayılan değer (Şifre)", + "Default value (Phone number)" : "Varsayılan değer (Telefon numarası)", + "Default value (Regex)" : "Varsayılan değer (Regex)", + "Default: 5" : "Varsayılan: 5", + "Defaults to current user. Select a different user if needed." : "Varsayılan olarak mevcut kullanıcıdır. Gerekirse farklı bir kullanıcı seçin.", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Gecikmeler her denemede iki katına çıkar (2, 4, 8 dakika...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Gecikmeler doğrusal olarak artar (5, 10, 15 dakika...)", + "Delete" : "Sil", + "Delete ({count})" : "Sil ({count})", + "Delete Application" : "Uygulamayı Sil", + "Delete Audit Trail" : "Denetim İzini Sil", + "Delete Collection" : "Koleksiyonu Sil", + "Delete ConfigSet" : "ConfigSet'i Sil", + "Delete Objects" : "Nesneleri Sil", + "Delete Operations" : "Silme İşlemleri", + "Delete SOLR Collection" : "SOLR Koleksiyonunu Sil", + "Delete View" : "Görünümü Sil", + "Delete all objects in this schema" : "Bu şemadaki tüm nesneleri sil", + "Delete permanently" : "Kalıcı olarak sil", + "Delete this conversation?" : "Bu konuşma silinsin mi?", + "Delete view" : "Görünümü sil", + "Delete {count} object" : [ + "{count} nesneyi sil", + "{count} nesneyi sil"], + "Delete {count} objects" : "{count} nesneyi sil", + "Deleted" : "Silindi", + "Deleted By" : "Silen", + "Deleted Date" : "Silinme Tarihi", + "Deleted Items Management" : "Silinen Öğelerin Yönetimi", + "Deleted This Week" : "Bu Hafta Silindi", + "Deleted Today" : "Bugün Silindi", + "Deleted:" : "Silindi:", + "Deleting SOLR Collection..." : "SOLR Koleksiyonu siliniyor...", + "Deleting..." : "Siliniyor...", + "Deletion Date Range" : "Silme Tarihi Aralığı", + "Deletion Statistics" : "Silme İstatistikleri", + "Deprecated" : "Kullanım Dışı", + "Depublish" : "Yayından Kaldır", + "Description" : "Açıklama", + "Description Template" : "Açıklama Şablonu", + "Description shown as tooltip" : "Araç ipucu olarak gösterilen açıklama", + "Description:" : "Açıklama:", + "Detect data types and patterns" : "Veri türlerini ve desenleri algıla", + "Detected At" : "Algılandığı Zaman", + "Detected Format:" : "Algılanan Biçim:", + "Detected Issues:" : "Algılanan Sorunlar:", + "Detected: {date}" : "Algılandı: {date}", + "Direct permission" : "Doğrudan izin", + "Disable" : "Devre Dışı Bırak", + "Disable this to make the agent" : "Ajanı yapmak için bunu devre dışı bırakın", + "Disabled" : "Devre Dışı", + "Discover Files" : "Dosyaları Keşfet", + "Discovered Properties" : "Keşfedilen Özellikler", + "Display Format" : "Görüntüleme Biçimi", + "Display Name" : "Görünen Ad", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Tüm filtrelenmiş denetim izi girişlerini kalıcı olarak silmek istiyor musunuz? Bu işlem geri alınamaz.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Bu denetim izi girişini kalıcı olarak silmek istiyor musunuz? Bu işlem geri alınamaz.", + "Documents" : "Belgeler", + "Doelbinding *" : "Doelbinding *", + "Dolphin OCR enabled" : "Dolphin OCR etkin", + "Download API Spec" : "API Spec'i İndir", + "Download API Specification" : "API Spesifikasyonunu İndir", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20)." : "OpenRegister'da depolanan tüm kişisel verilerinizin bir kopyasını indirin (GDPR Madde 20).", + "Drag a file or files here" : "Bir veya birden fazla dosyayı buraya sürükleyin", + "Dry run completed: {count} fields would be created" : "Test çalıştırması tamamlandı: {count} alan oluşturulacak", + "Dry run completed: {count} fields would be fixed" : "Test çalıştırması tamamlandı: {count} alan düzeltilecek", + "Duration in seconds (optional)" : "Saniye cinsinden süre (isteğe bağlı)", + "Edit" : "Düzenle", + "Edit Agent" : "Ajanı Düzenle", + "Edit Application" : "Uygulamayı Düzenle", + "Edit Configuration" : "Yapılandırmayı Düzenle", + "Edit Endpoint" : "Uç Noktayı Düzenle", + "Edit Organisation" : "Kuruluşu Düzenle", + "Edit Property '{key}' of '{title}'" : "'{title}' Öğesinin '{key}' Özelliğini Düzenle", + "Edit Register" : "Kaydı Düzenle", + "Edit Schema" : "Şemayı Düzenle", + "Edit Source" : "Kaynağı Düzenle", + "Edit View" : "Görünümü Düzenle", + "Edit Webhook" : "Webhook'u Düzenle", + "Edit labels" : "Etiketleri düzenle", + "Edit processing activity" : "İşleme faaliyetini düzenle", + "Edit tags for {name}" : "{name} için etiketleri düzenle", + "Edit verwerkingsactiviteit" : "Verwerkingsactiviteit'i düzenle", + "Edit view" : "Görünümü düzenle", + "Edit view details" : "Görünüm ayrıntılarını düzenle", + "Effectiveness" : "Etkinlik", + "Email" : "E-posta", + "Email Addresses" : "E-posta Adresleri", + "Email digest frequency" : "E-posta özeti sıklığı", + "Emails" : "E-postalar", + "Embedding Model" : "Gömülüm Modeli", + "Embedding Provider" : "Gömülüm Sağlayıcı", + "Embedding generated successfully" : "Gömülüm başarıyla oluşturuldu", + "Embedding provider connection successful!" : "Gömülüm sağlayıcı bağlantısı başarılı!", + "Embedding test failed: {error}" : "Gömülüm testi başarısız: {error}", + "Enable" : "Etkinleştir", + "Enable RAG" : "RAG'ı Etkinleştir", + "Enable auto-creation" : "Otomatik oluşturmayı etkinleştir", + "Enable automatic file vectorization" : "Otomatik dosya vektörleştirmeyi etkinleştir", + "Enable automatic object vectorization" : "Otomatik nesne vektörleştirmeyi etkinleştir", + "Enable automatic synchronization" : "Otomatik senkronizasyonu etkinleştir", + "Enable calendar provider" : "Takvim sağlayıcısını etkinleştir", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "SOLR işlemleri için ayrıntılı günlük kaydını etkinleştir (hata ayıklama için önerilir)", + "Enable faceting" : "Fasetlemeyi etkinleştir", + "Enable or disable n8n workflow integration. Configure connection settings below." : "n8n iş akışı entegrasyonunu etkinleştirin veya devre dışı bırakın. Aşağıda bağlantı ayarlarını yapılandırın.", + "Enable or disable this webhook" : "Bu webhook'u etkinleştirin veya devre dışı bırakın", + "Enable vectorization for all existing and future views (may increase costs)" : "Mevcut ve gelecekteki tüm görünümler için vektörleştirmeyi etkinleştir (maliyetleri artırabilir)", + "Enable write-back to target objects" : "Hedef nesnelere geri yazma özelliğini etkinleştir", + "Enabled" : "Etkin", + "End Date Field" : "Bitiş Tarihi Alanı", + "Endpoint Path*" : "Uç Nokta Yolu*", + "Endpoints" : "Uç Noktalar", + "Engine not found" : "Motor bulunamadı", + "Enter ConfigSet name" : "ConfigSet adını girin", + "Enter Nextcloud usernames to grant access to this private agent" : "Bu özel ajana erişim vermek için Nextcloud kullanıcı adlarını girin", + "Enter agent description (optional)" : "Ajan açıklamasını girin (isteğe bağlı)", + "Enter agent name" : "Ajan adını girin", + "Enter application description (optional)" : "Uygulama açıklamasını girin (isteğe bağlı)", + "Enter application name" : "Uygulama adını girin", + "Enter collection name" : "Koleksiyon adını girin", + "Enter configuration description (optional)" : "Yapılandırma açıklamasını girin (isteğe bağlı)", + "Enter configuration title" : "Yapılandırma başlığını girin", + "Enter description (optional)..." : "Açıklama girin (isteğe bağlı)...", + "Enter new collection name" : "Yeni koleksiyon adını girin", + "Enter object ID" : "Nesne kimliğini girin", + "Enter organisation description (optional)" : "Kuruluş açıklamasını girin (isteğe bağlı)", + "Enter organisation name" : "Kuruluş adını girin", + "Enter search term" : "Arama terimi girin", + "Enter search terms or leave empty to browse all" : "Arama terimleri girin veya tümüne göz atmak için boş bırakın", + "Enter system prompt for the agent" : "Ajan için sistem istemi girin", + "Enter username and press Enter" : "Kullanıcı adını girin ve Enter'a basın", + "Enter view name..." : "Görünüm adını girin...", + "Enter webhook name" : "Webhook adını girin", + "Enter your API key" : "API anahtarınızı girin", + "Enter your object here..." : "Nesnenizi buraya girin...", + "Enter your schema here..." : "Şemanızı buraya girin...", + "Entities" : "Varlıklar", + "Entities detected" : "Varlıklar algılandı", + "Entity ID: {id}" : "Varlık Kimliği: {id}", + "Entity Information" : "Varlık Bilgisi", + "Entity Recognition" : "Varlık Tanıma", + "Entity Recognition Method" : "Varlık Tanıma Yöntemi", + "Entity deleted successfully" : "Varlık başarıyla silindi", + "Entity not found" : "Varlık bulunamadı", + "Entries to be deleted:" : "Silinecek girişler:", + "Enum Issue" : "Enum Sorunu", + "Enum constraint is missing" : "Enum kısıtlaması eksik", + "Erase (Art 17)" : "Sil (Madde 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail." : "Bu özne için {count} nesne silinsin mi? Bu işlem denetim izine kaydedilir.", + "Erasure complete" : "Silme tamamlandı", + "Erasure preview" : "Silme önizlemesi", + "Error" : "Hata", + "Error Details" : "Hata Ayrıntıları", + "Error Information" : "Hata Bilgisi", + "Error Message" : "Hata Mesajı", + "Error deleting audit trails: {error}" : "Denetim izleri silinirken hata: {error}", + "Error deleting search trails: {error}" : "Arama izleri silinirken hata: {error}", + "Error loading application" : "Uygulama yüklenirken hata", + "Error loading audit trails" : "Denetim izleri yüklenirken hata", + "Error loading entity" : "Varlık yüklenirken hata", + "Error loading search trails" : "Arama izleri yüklenirken hata", + "Estimated Batches:" : "Tahmini Toplu İşler:", + "Estimated Cost:" : "Tahmini Maliyet:", + "Estimated Duration:" : "Tahmini Süre:", + "Event" : "Etkinlik", + "Event Property for Payload" : "Yük için Etkinlik Özelliği", + "Events" : "Etkinlikler", + "Every schema with detected PII has a processing-activity annotation." : "Tespit edilen KBB'ye sahip her şemanın bir işleme faaliyeti ek açıklaması vardır.", + "Example" : "Örnek", + "Example Value" : "Örnek Değer", + "Example value for this property" : "Bu özellik için örnek değer", + "Excel (.xlsx)" : "Excel (.xlsx)", + "Exclusive Maximum" : "Dışlayıcı Maksimum", + "Exclusive Minimum" : "Dışlayıcı Minimum", + "Exclusive maximum" : "Dışlayıcı maksimum", + "Exclusive minimum" : "Dışlayıcı minimum", + "Execution Mode" : "Yürütme Modu", + "Execution Time" : "Yürütme Süresi", + "Execution Time Range" : "Yürütme Süresi Aralığı", + "Existing Improvements" : "Mevcut İyileştirmeler", + "Expand details" : "Ayrıntıları genişlet", + "Expiration" : "Son Kullanma", + "Expires" : "Sona Eriyor", + "Expires in (e.g., 90d)" : "Bitiş süresi (örn. 90g)", + "Exponential" : "Üstel", + "Export" : "Dışa Aktar", + "Export \"{schema}\" objects from \"{register}\"" : "\"{register}\" kaydından \"{schema}\" nesnelerini dışa aktar", + "Export Configuration" : "Yapılandırmayı Dışa Aktar", + "Export Format:" : "Dışa Aktarma Biçimi:", + "Export Objects" : "Nesneleri Dışa Aktar", + "Export completed successfully" : "Dışa aktarma başarıyla tamamlandı", + "Export downloaded successfully" : "Dışa aktarma başarıyla indirildi", + "Export failed: {error}" : "Dışa aktarma başarısız: {error}", + "Export is rate limited. Please try again later." : "Dışa aktarma hız sınırlamalı. Lütfen daha sonra tekrar deneyin.", + "Export my data" : "Verilerimi dışa aktar", + "Export, view, or delete audit trails" : "Denetim izlerini dışa aktarın, görüntüleyin veya silin", + "Exporting..." : "Dışa aktarılıyor...", + "Extend Schema" : "Şemayı Genişlet", + "Extended" : "Genişletilmiş", + "Extensions" : "Uzantılar", + "Extra Query Parameters" : "Ekstra Sorgu Parametreleri", + "Extract Now" : "Şimdi Çıkar", + "Extract Pending Files" : "Bekleyen Dosyaları Çıkar", + "Extract now" : "Şimdi çıkar", + "Extract properties from each object" : "Her nesneden özellikleri çıkar", + "Extracted At" : "Çıkarıldığı Zaman", + "Extracted at" : "Çıkarıldığı zaman", + "Extraction" : "Çıkarma", + "Extraction Mode" : "Çıkarma Modu", + "Extraction Scope" : "Çıkarma Kapsamı", + "Extraction Status" : "Çıkarma Durumu", + "Extraction error for {file}: {error}" : "{file} için çıkarma hatası: {error}", + "Facet Description" : "Faset Açıklaması", + "Facet Order" : "Faset Sırası", + "Facet Title" : "Faset Başlığı", + "Facet Type" : "Faset Türü", + "Facet configuration updated successfully" : "Faset yapılandırması başarıyla güncellendi", + "Facetable" : "Fasetlenebilir", + "Faceting Configuration:" : "Fasetleme Yapılandırması:", + "Facets discovered and configured successfully" : "Fasetler başarıyla keşfedildi ve yapılandırıldı", + "Facets discovered successfully" : "Fasetler başarıyla keşfedildi", + "Failed" : "Başarısız", + "Failed to add attachment to object" : "Nesneye ek eklenemedi", + "Failed to add user to organisation" : "Kuruluşa kullanıcı eklenemedi", + "Failed to analyze schema properties" : "Şema özellikleri analiz edilemedi", + "Failed to archive conversation" : "Konuşma arşivlenemedi", + "Failed to calculate sizes" : "Boyutlar hesaplanamadı", + "Failed to cancel deactivation" : "Devre dışı bırakma iptal edilemedi", + "Failed to change active organisation: {error}" : "Etkin kuruluş değiştirilemedi: {error}", + "Failed to change password" : "Şifre değiştirilemedi", + "Failed to check version: {error}" : "Sürüm kontrol edilemedi: {error}", + "Failed to clear audit trails: {error}" : "Denetim izleri temizlenemedi: {error}", + "Failed to clear blob objects: {error}" : "Blob nesneleri temizlenemedi: {error}", + "Failed to clear cache: {error}" : "Önbellek temizlenemedi: {error}", + "Failed to clear collection" : "Koleksiyon temizlenemedi", + "Failed to clear conversation" : "Konuşma temizlenemedi", + "Failed to clear embeddings: {error}" : "Gömülümler temizlenemedi: {error}", + "Failed to clear search trails: {error}" : "Arama izleri temizlenemedi: {error}", + "Failed to clear {type} cache: {error}" : "{type} önbelleği temizlenemedi: {error}", + "Failed to copy changes" : "Değişiklikler kopyalanamadı", + "Failed to copy data" : "Veri kopyalanamadı", + "Failed to copy data to clipboard" : "Veri panoya kopyalanamadı", + "Failed to copy to clipboard" : "Panoya kopyalanamadı", + "Failed to copy token" : "Token kopyalanamadı", + "Failed to create conversation" : "Konuşma oluşturulamadı", + "Failed to create missing SOLR fields: {error}" : "Eksik SOLR alanları oluşturulamadı: {error}", + "Failed to create or find project" : "Proje oluşturulamadı veya bulunamadı", + "Failed to create token" : "Token oluşturulamadı", + "Failed to delete SOLR collection: {error}" : "SOLR koleksiyonu silinemedi: {error}", + "Failed to delete collection" : "Koleksiyon silinemedi", + "Failed to delete conversation" : "Konuşma silinemedi", + "Failed to delete objects for {schema}: {error}" : "{schema} için nesneler silinemedi: {error}", + "Failed to delete webhook" : "Webhook silinemedi", + "Failed to depublish register: {error}" : "Kayıt yayından kaldırılamadı: {error}", + "Failed to depublish schema: {error}" : "Şema yayından kaldırılamadı: {error}", + "Failed to depublish: {error}" : "Yayından kaldırılamadı: {error}", + "Failed to discover files: {error}" : "Dosyalar keşfedilemedi: {error}", + "Failed to download API specification" : "API spesifikasyonu indirilemedi", + "Failed to export data" : "Veri dışa aktarılamadı", + "Failed to export objects" : "Nesneler dışa aktarılamadı", + "Failed to fetch conversation history" : "Konuşma geçmişi alınamadı", + "Failed to fix mismatched SOLR fields: {error}" : "Eşleşmeyen SOLR alanları düzeltilemedi: {error}", + "Failed to get SOLR field configuration" : "SOLR alan yapılandırması alınamadı", + "Failed to get chat statistics" : "Sohbet istatistikleri alınamadı", + "Failed to get database information: %s" : "Veritabanı bilgisi alınamadı: %s", + "Failed to get entity categories" : "Varlık kategorileri alınamadı", + "Failed to get entity statistics" : "Varlık istatistikleri alınamadı", + "Failed to get entity types" : "Varlık türleri alınamadı", + "Failed to get response: {error}" : "Yanıt alınamadı: {error}", + "Failed to invalidate app store cache: {error}" : "Uygulama mağazası önbelleği geçersiz kılınamadı: {error}", + "Failed to leave organisation: {error}" : "Kuruluştan ayrılınamadı: {error}", + "Failed to link object" : "Nesne bağlanamadı", + "Failed to load Deck cards" : "Deck kartları yüklenemedi", + "Failed to load LLM configuration" : "LLM yapılandırması yüklenemedi", + "Failed to load Nextcloud groups" : "Nextcloud grupları yüklenemedi", + "Failed to load cache statistics: {error}" : "Önbellek istatistikleri yüklenemedi: {error}", + "Failed to load conversation" : "Konuşma yüklenemedi", + "Failed to load entities" : "Varlıklar yüklenemedi", + "Failed to load entity" : "Varlık yüklenemedi", + "Failed to load extraction data" : "Çıkarma verisi yüklenemedi", + "Failed to load files" : "Dosyalar yüklenemedi", + "Failed to load linked contacts" : "Bağlantılı kişiler yüklenemedi", + "Failed to load linked emails" : "Bağlantılı e-postalar yüklenemedi", + "Failed to load linked events" : "Bağlantılı etkinlikler yüklenemedi", + "Failed to load organisations" : "Kuruluşlar yüklenemedi", + "Failed to load register data" : "Kayıt verisi yüklenemedi", + "Failed to load related entities" : "İlgili varlıklar yüklenemedi", + "Failed to load settings: {error}" : "Ayarlar yüklenemedi: {error}", + "Failed to load statistics: {error}" : "İstatistikler yüklenemedi: {error}", + "Failed to load templates" : "Şablonlar yüklenemedi", + "Failed to load view: {error}" : "Görünüm yüklenemedi: {error}", + "Failed to load webhooks" : "Webhook'lar yüklenemedi", + "Failed to load workflows" : "İş akışları yüklenemedi", + "Failed to process message" : "Mesaj işlenemedi", + "Failed to publish register: {error}" : "Kayıt yayımlanamadı: {error}", + "Failed to publish schema: {error}" : "Şema yayımlanamadı: {error}", + "Failed to publish: {error}" : "Yayımlanamadı: {error}", + "Failed to refresh database information" : "Veritabanı bilgisi yenilenemedi", + "Failed to reindex collection" : "Koleksiyon yeniden dizinlenemedi", + "Failed to remove avatar" : "Avatar kaldırılamadı", + "Failed to remove link" : "Bağlantı kaldırılamadı", + "Failed to remove member: {error}" : "Üye kaldırılamadı: {error}", + "Failed to remove schema {schema}: {error}" : "{schema} şeması kaldırılamadı: {error}", + "Failed to remove user from organisation" : "Kullanıcı kuruluştan kaldırılamadı", + "Failed to rename conversation" : "Konuşma yeniden adlandırılamadı", + "Failed to request deactivation" : "Devre dışı bırakma isteği gönderilemedi", + "Failed to restore conversation" : "Konuşma geri yüklenemedi", + "Failed to retrieve extraction status." : "Çıkarma durumu alınamadı.", + "Failed to retrieve objects for file." : "Dosya için nesneler alınamadı.", + "Failed to retry extraction" : "Çıkarma yeniden denenemedi", + "Failed to retry webhook" : "Webhook yeniden denenemedi", + "Failed to revoke token" : "Token iptal edilemedi", + "Failed to save GitHub token" : "GitHub token'ı kaydedilemedi", + "Failed to save GitLab URL" : "GitLab URL'si kaydedilemedi", + "Failed to save GitLab token" : "GitLab token'ı kaydedilemedi", + "Failed to save LLM settings: {error}" : "LLM ayarları kaydedilemedi: {error}", + "Failed to save additional feedback" : "Ek geri bildirim kaydedilemedi", + "Failed to save configuration." : "Yapılandırma kaydedilemedi.", + "Failed to save configuration: {error}" : "Yapılandırma kaydedilemedi: {error}", + "Failed to save facet configuration" : "Faset yapılandırması kaydedilemedi", + "Failed to save feedback" : "Geri bildirim kaydedilemedi", + "Failed to save file settings: {error}" : "Dosya ayarları kaydedilemedi: {error}", + "Failed to save n8n configuration" : "n8n yapılandırması kaydedilemedi", + "Failed to save preferences" : "Tercihler kaydedilemedi", + "Failed to save roles" : "Roller kaydedilemedi", + "Failed to save settings" : "Ayarlar kaydedilemedi", + "Failed to save settings: {error}" : "Ayarlar kaydedilemedi: {error}", + "Failed to save view: {error}" : "Görünüm kaydedilemedi: {error}", + "Failed to save warmup interval: {error}" : "Isınma aralığı kaydedilemedi: {error}", + "Failed to save webhook" : "Webhook kaydedilemedi", + "Failed to send feedback" : "Geri bildirim gönderilemedi", + "Failed to set active organisation: {error}" : "Etkin kuruluş ayarlanamadı: {error}", + "Failed to setup SOLR: {error}" : "SOLR kurulamadı: {error}", + "Failed to start file warmup: {error}" : "Dosya ısınması başlatılamadı: {error}", + "Failed to start processing {type} files: {error}" : "{type} dosyalarının işlenmesi başlatılamadı: {error}", + "Failed to start vectorization: {error}" : "Vektörleştirme başlatılamadı: {error}", + "Failed to switch organisation: {error}" : "Kuruluş değiştirilemedi: {error}", + "Failed to sync magic table for {schema}: {error}" : "{schema} için sihirli tablo senkronize edilemedi: {error}", + "Failed to test SOLR connection: {error}" : "SOLR bağlantısı test edilemedi: {error}", + "Failed to test webhook" : "Webhook test edilemedi", + "Failed to update LLM settings: {error}" : "LLM ayarları güncellenemedi: {error}", + "Failed to update Multitenancy settings: {error}" : "Çok kiracılık ayarları güncellenemedi: {error}", + "Failed to update RBAC settings: {error}" : "RBAC ayarları güncellenemedi: {error}", + "Failed to update Retention settings: {error}" : "Saklama ayarları güncellenemedi: {error}", + "Failed to update SOLR settings: {error}" : "SOLR ayarları güncellenemedi: {error}", + "Failed to update favorite status" : "Favori durumu güncellenemedi", + "Failed to update schema configuration for {schema}: {error}" : "{schema} için şema yapılandırması güncellenemedi: {error}", + "Failed to update schema properties" : "Şema özellikleri güncellenemedi", + "Failed to update view" : "Görünüm güncellenemedi", + "Failed to update view: {error}" : "Görünüm güncellenemedi: {error}", + "Failed to update webhook" : "Webhook güncellenemedi", + "Failed to upload avatar" : "Avatar yüklenemedi", + "Failed to validate {schema}: {error}" : "{schema} doğrulanamadı: {error}", + "Failed to vectorize files: {error}" : "Dosyalar vektörleştirilemedi: {error}", + "Failed to warmup names cache: {error}" : "İsimler önbelleği ısıtılamadı: {error}", + "Federation" : "Federasyon", + "Feedback recorded" : "Geri bildirim kaydedildi", + "Fewer sources (1-3):" : "Daha az kaynak (1-3):", + "Field" : "Alan", + "Fields to Display" : "Görüntülenecek Alanlar", + "File" : "Dosya", + "File #{id}" : "Dosya #{id}", + "File Chunk Prediction" : "Dosya Parça Tahmini", + "File Collection" : "Dosya Koleksiyonu", + "File Handling" : "Dosya İşleme", + "File Location" : "Dosya Konumu", + "File Management" : "Dosya Yönetimi", + "File Name" : "Dosya Adı", + "File Path" : "Dosya Yolu", + "File Type Selection" : "Dosya Türü Seçimi", + "File Types to Process" : "İşlenecek Dosya Türleri", + "File Vectorization" : "Dosya Vektörleştirme", + "File Warmup" : "Dosya Isınması", + "File actions menu" : "Dosya işlemleri menüsü", + "File anonymized successfully" : "Dosya başarıyla anonimleştirildi", + "File collection not configured" : "Dosya koleksiyonu yapılandırılmamış", + "File discovery completed" : "Dosya keşfi tamamlandı", + "File extraction completed" : "Dosya çıkarma tamamlandı", + "File extraction queued" : "Dosya çıkarma sıraya alındı", + "File indexed successfully" : "Dosya başarıyla dizinlendi", + "File is already anonymized" : "Dosya zaten anonimleştirilmiş", + "File name" : "Dosya adı", + "File not found" : "Dosya bulunamadı", + "File settings saved successfully" : "Dosya ayarları başarıyla kaydedildi", + "File settings updated successfully" : "Dosya ayarları başarıyla güncellendi", + "File sources" : "Dosya kaynakları", + "File vectorization configuration saved successfully" : "Dosya vektörleştirme yapılandırması başarıyla kaydedildi", + "File vectorization started. Check the statistics section for progress." : "Dosya vektörleştirme başladı. İlerleme için istatistikler bölümünü kontrol edin.", + "File warmup completed" : "Dosya ısınması tamamlandı", + "Files" : "Dosyalar", + "Files added successfully" : "Dosyalar başarıyla eklendi", + "Files with Completed Extraction:" : "Çıkarma Tamamlanan Dosyalar:", + "Files → fileCollection, Objects → objectCollection" : "Dosyalar → fileCollection, Nesneler → objectCollection", + "Files → filecollection, objects → objectcollection" : "Dosyalar → filecollection, nesneler → objectcollection", + "Filter Audit Trails" : "Denetim İzlerini Filtrele", + "Filter Deleted Items" : "Silinen Öğeleri Filtrele", + "Filter Objects" : "Nesneleri Filtrele", + "Filter Properties" : "Özellikleri Filtrele", + "Filter Search Trails" : "Arama İzlerini Filtrele", + "Filter Statistics" : "Filtre İstatistikleri", + "Filter and analyze search trail entries" : "Arama izi girişlerini filtrele ve analiz et", + "Filter and manage audit trail entries" : "Denetim izi girişlerini filtrele ve yönet", + "Filter and manage soft deleted items" : "Geçici silinen öğeleri filtrele ve yönet", + "Filter and search entities" : "Varlıkları filtrele ve ara", + "Filter by object ID" : "Nesne kimliğine göre filtrele", + "Filter by search term" : "Arama terimine göre filtrele", + "Filter by type" : "Türe göre filtrele", + "Filter by webhook" : "Webhook'a göre filtrele", + "Filter data loaded automatically. Use the filters below to refine your search." : "Filtre verileri otomatik olarak yüklendi. Aramanızı daraltmak için aşağıdaki filtreleri kullanın.", + "Filter fields..." : "Alanları filtrele...", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Webhook tetikleyicilerini yük özellikleriyle filtrele (her satırda bir, biçim: anahtar: değer)", + "Filtered" : "Filtrelendi", + "Filters" : "Filtreler", + "Finalizing setup..." : "Kurulum tamamlanıyor...", + "Fireworks AI Chat Settings" : "Fireworks AI Sohbet Ayarları", + "Fireworks AI Embedding Configuration" : "Fireworks AI Gömülüm Yapılandırması", + "First" : "İlk", + "Fixed" : "Düzeltildi", + "For chat and retrieval-augmented generation" : "Sohbet ve geri alma artırımlı oluşturma için", + "For vector embeddings and semantic search" : "Vektör gömülümleri ve anlamsal arama için", + "Format" : "Biçim", + "Format Issue" : "Biçim Sorunu", + "Format constraint is missing" : "Biçim kısıtlaması eksik", + "From (e.g. -7 days or 2025-01-01)" : "Başlangıç (örn. -7 gün veya 2025-01-01)", + "From Date" : "Başlangıç Tarihi", + "From date" : "Başlangıç tarihi", + "Full data copied to clipboard" : "Tüm veri panoya kopyalandı", + "Function Tools" : "İşlev Araçları", + "General Issue" : "Genel Sorun", + "Generate recommendations and confidence scores" : "Öneriler ve güven puanları oluştur", + "Generate report" : "Rapor oluştur", + "Generate the verantwoordingsdocument" : "Verantwoordingsdocument oluştur", + "Generate vectors immediately when new objects are created" : "Yeni nesneler oluşturulduğunda vektörleri hemen oluştur", + "Generated: {time}" : "Oluşturuldu: {time}", + "GitHub Personal Access Token" : "GitHub Kişisel Erişim Token'ı", + "GitHub token is valid" : "GitHub token'ı geçerli", + "GitHub token is valid! Username: {username}" : "GitHub token'ı geçerli! Kullanıcı adı: {username}", + "GitHub token removed successfully" : "GitHub token'ı başarıyla kaldırıldı", + "GitHub token saved successfully" : "GitHub token'ı başarıyla kaydedildi", + "GitHub token test failed: {message}" : "GitHub token testi başarısız: {message}", + "GitLab Personal Access Token" : "GitLab Kişisel Erişim Token'ı", + "GitLab URL saved successfully" : "GitLab URL'si başarıyla kaydedildi", + "GitLab token is valid" : "GitLab token'ı geçerli", + "GitLab token is valid! Username: {username}" : "GitLab token'ı geçerli! Kullanıcı adı: {username}", + "GitLab token saved successfully" : "GitLab token'ı başarıyla kaydedildi", + "GitLab token test failed: {message}" : "GitLab token testi başarısız: {message}", + "Go to linked Register" : "Bağlantılı Kayda Git", + "Go to linked Schema" : "Bağlantılı Şemaya Git", + "Go to organisation" : "Kuruluşa git", + "Grant additional permissions beyond standard CRUD operations" : "Standart CRUD işlemlerinin ötesinde ek izinler ver", + "Group Quota" : "Grup Kotası", + "Groups" : "Gruplar", + "HTML preview" : "HTML önizlemesi", + "HTTP Method" : "HTTP Yöntemi", + "HTTP method used to send webhook requests" : "Webhook isteklerini göndermek için kullanılan HTTP yöntemi", + "Headers" : "Başlıklar", + "Health" : "Sağlık", + "Heartbeat successful - connection kept alive" : "Sinyal başarılı - bağlantı canlı tutuldu", + "Helpful" : "Yararlı", + "Here you can configure the details for various connections." : "Burada çeşitli bağlantılar için ayrıntıları yapılandırabilirsiniz.", + "Here you can configure the details for your organisation." : "Burada kuruluşunuzun ayrıntılarını yapılandırabilirsiniz.", + "Hide" : "Gizle", + "Hide Filters" : "Filtreleri Gizle", + "Hide in collection view" : "Koleksiyon görünümünde gizle", + "Hide in forms" : "Formlarda gizle", + "Hide in list view" : "Liste görünümünde gizle", + "High" : "Yüksek", + "High Confidence" : "Yüksek Güven", + "Host" : "Ana Bilgisayar", + "Hourly" : "Saatlik", + "How often to check for updates (1-168 hours)" : "Güncellemelerin ne sıklıkta kontrol edileceği (1-168 saat)", + "How to handle retries for failed webhook deliveries" : "Başarısız webhook teslimatları için yeniden deneme nasıl yapılır", + "IBANs" : "IBAN'lar", + "ID" : "Kimlik", + "ID of input mapping (optional)" : "Giriş eşlemesinin kimliği (isteğe bağlı)", + "ID of output mapping (optional)" : "Çıkış eşlemesinin kimliği (isteğe bağlı)", + "ID of the target resource" : "Hedef kaynağın kimliği", + "ID:" : "Kimlik:", + "ID: {id}" : "Kimlik: {id}", + "IP rate limits cleared successfully" : "IP hız sınırları başarıyla temizlendi", + "Identify properties not in the schema" : "Şemada bulunmayan özellikleri tanımla", + "Immutable" : "Değiştirilemez", + "Import" : "İçe Aktar", + "Import Configuration" : "Yapılandırmayı İçe Aktar", + "Import Data into Register" : "Kayda Veri İçe Aktar", + "Import successful" : "İçe aktarma başarılı", + "Improved Property" : "İyileştirilmiş Özellik", + "In use" : "Kullanımda", + "Inactive" : "Etkin Değil", + "Inactive organisations cannot be used" : "Etkin olmayan kuruluşlar kullanılamaz", + "Include IDs and names of related objects for better contextual search" : "Daha iyi bağlamsal arama için ilgili nesnelerin kimliklerini ve adlarını dahil et", + "Include ids and names of related objects for better contextual search" : "Daha iyi bağlamsal arama için ilgili nesnelerin kimliklerini ve adlarını dahil et", + "Include related object references" : "İlgili nesne referanslarını dahil et", + "Include schema and register metadata" : "Şema ve kayıt meta verilerini dahil et", + "Index" : "Dizin", + "Information about the current OpenRegister installation" : "Mevcut OpenRegister kurulumu hakkında bilgi", + "Inherited from register" : "Kayıttan devralındı", + "Inherits permissions from register" : "İzinleri kayıttan devralır", + "Initialization failed" : "Başlatma başarısız", + "Initialize Project" : "Projeyi Başlat", + "Initializing SOLR setup..." : "SOLR kurulumu başlatılıyor...", + "Input Mapping" : "Giriş Eşlemesi", + "Inspect Fields" : "Alanları İncele", + "Inspect Index" : "Dizini İncele", + "Inspect SOLR Index" : "SOLR Dizinini İncele", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Gerçek zamanlı güncellemeleri etkinleştirmek için Nextcloud Uygulama Mağazası'ndan notify_push uygulamasını yükleyin.", + "Integrations" : "Entegrasyonlar", + "Internet location (URL) of this installation" : "Bu kurulumun internet konumu (URL)", + "Interval" : "Aralık", + "Invalid" : "Geçersiz", + "Invalid batch size. Must be between 1 and 5000" : "Geçersiz toplu iş boyutu. 1 ile 5000 arasında olmalıdır", + "Invalid feedback type" : "Geçersiz geri bildirim türü", + "Invalid field name provided" : "Geçersiz alan adı sağlandı", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Geçersiz maxObjects. 0 (tümü) veya pozitif sayı olmalıdır", + "Invalid:" : "Geçersiz:", + "Invite Users" : "Kullanıcıları Davet Et", + "Invited Users" : "Davet Edilen Kullanıcılar", + "Inzage (Art 15)" : "Inzage (Madde 15)", + "Inzage results" : "Inzage sonuçları", + "Issue" : "Sorun", + "Items per page:" : "Sayfa başına öğe:", + "Just now" : "Az önce", + "Key" : "Anahtar", + "LLM Configuration" : "LLM Yapılandırması", + "LLM actions menu" : "LLM işlemleri menüsü", + "LLM configuration saved successfully" : "LLM yapılandırması başarıyla kaydedildi", + "LLM features disabled" : "LLM özellikleri devre dışı", + "LLM features enabled" : "LLM özellikleri etkin", + "LLM must be enabled with an embedding provider configured" : "LLM, yapılandırılmış bir gömülüm sağlayıcısıyla etkinleştirilmelidir", + "LLM settings saved successfully" : "LLM ayarları başarıyla kaydedildi", + "LLM settings updated successfully" : "LLM ayarları başarıyla güncellendi", + "Label" : "Etiket", + "Labels" : "Etiketler", + "Languages" : "Diller", + "Last" : "Son", + "Last Triggered" : "Son Tetiklenme", + "Leave empty to allow all users access" : "Tüm kullanıcılara erişim izni vermek için boş bırakın", + "Leave off for auto-detection from property format." : "Özellik biçiminden otomatik algılama için kapalı bırakın.", + "Left organisation successfully" : "Kuruluştan başarıyla ayrıldı", + "Legal basis *" : "Hukuki dayanak *", + "Length Range:" : "Uzunluk Aralığı:", + "Linear" : "Doğrusal", + "Link" : "Bağlantı", + "Link an email from the Mail app sidebar to associate it with this object." : "Bu nesneyle ilişkilendirmek için Mail uygulaması kenar çubuğundan bir e-posta bağlayın.", + "Link an object to see it here." : "Burada görmek için bir nesne bağlayın.", + "Link another object" : "Başka bir nesne bağla", + "Link existing event" : "Mevcut etkinliği bağla", + "Link removed" : "Bağlantı kaldırıldı", + "Link to Object" : "Nesneye Bağlantı", + "Link to {name}" : "{name} öğesine bağlantı", + "Linked Objects" : "Bağlantılı Nesneler", + "Linked by {user}" : "{user} tarafından bağlandı", + "Linked emails, calendar events, contacts and Deck cards will appear here." : "Bağlantılı e-postalar, takvim etkinlikleri, kişiler ve Deck kartları burada görünecek.", + "Linked to {name}" : "{name} öğesine bağlantılı", + "Load Advanced Filters" : "Gelişmiş Filtreleri Yükle", + "Load advanced filters with live data from your search index" : "Arama dizininizdeki canlı verilerle gelişmiş filtreleri yükleyin", + "Load more" : "Daha fazla yükle", + "Load view" : "Görünümü yükle", + "Loading API tokens..." : "API token'ları yükleniyor...", + "Loading ConfigSets..." : "ConfigSet'ler yükleniyor...", + "Loading LLM configuration..." : "LLM yapılandırması yükleniyor...", + "Loading RBAC settings..." : "RBAC ayarları yükleniyor...", + "Loading activity..." : "Etkinlik yükleniyor...", + "Loading advanced filters..." : "Gelişmiş filtreler yükleniyor...", + "Loading agents..." : "Ajanlar yükleniyor...", + "Loading application details..." : "Uygulama ayrıntıları yükleniyor...", + "Loading applications..." : "Uygulamalar yükleniyor...", + "Loading archived conversations..." : "Arşivlenmiş konuşmalar yükleniyor...", + "Loading audit trails..." : "Denetim izleri yükleniyor...", + "Loading available tools..." : "Mevcut araçlar yükleniyor...", + "Loading cache statistics..." : "Önbellek istatistikleri yükleniyor...", + "Loading collections..." : "Koleksiyonlar yükleniyor...", + "Loading configurations..." : "Yapılandırmalar yükleniyor...", + "Loading conversation..." : "Konuşma yükleniyor...", + "Loading conversations..." : "Konuşmalar yükleniyor...", + "Loading deleted items..." : "Silinen öğeler yükleniyor...", + "Loading endpoints" : "Uç noktalar yükleniyor", + "Loading entities..." : "Varlıklar yükleniyor...", + "Loading events..." : "Etkinlikler yükleniyor...", + "Loading filter data..." : "Filtre verileri yükleniyor...", + "Loading groups..." : "Gruplar yükleniyor...", + "Loading linked objects..." : "Bağlantılı nesneler yükleniyor...", + "Loading log details..." : "Günlük ayrıntıları yükleniyor...", + "Loading multitenancy settings..." : "Çok kiracılık ayarları yükleniyor...", + "Loading n8n configuration..." : "n8n yapılandırması yükleniyor...", + "Loading permission matrix..." : "İzin matrisi yükleniyor...", + "Loading preferences..." : "Tercihler yükleniyor...", + "Loading register data..." : "Kayıt verileri yükleniyor...", + "Loading registers..." : "Kayıtlar yükleniyor...", + "Loading retention settings..." : "Saklama ayarları yükleniyor...", + "Loading schemas..." : "Şemalar yükleniyor...", + "Loading search configuration..." : "Arama yapılandırması yükleniyor...", + "Loading search trails..." : "Arama izleri yükleniyor...", + "Loading sources..." : "Kaynaklar yükleniyor...", + "Loading statistics..." : "İstatistikler yükleniyor...", + "Loading text extraction settings..." : "Metin çıkarma ayarları yükleniyor...", + "Loading tokens..." : "Token'lar yükleniyor...", + "Loading user groups..." : "Kullanıcı grupları yükleniyor...", + "Loading users..." : "Kullanıcılar yükleniyor...", + "Loading views..." : "Görünümler yükleniyor...", + "Loading..." : "Yükleniyor...", + "Local" : "Yerel", + "Local Version" : "Yerel Sürüm", + "Location" : "Konum", + "Location Field" : "Konum Alanı", + "Locations" : "Konumlar", + "Locked" : "Kilitli", + "Locked:" : "Kilitli:", + "Login successful" : "Giriş başarılı", + "Logs" : "Günlükler", + "Low" : "Düşük", + "Low Confidence" : "Düşük Güven", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering." : "Daha düşük sayılar filtre kenar çubuğunda önce görünür. Otomatik sıralama için boş bırakın.", + "Lucene Version:" : "Lucene Sürümü:", + "Magic table sync completed for {schema}" : "{schema} için sihirli tablo senkronizasyonu tamamlandı", + "Magic table synchronized successfully" : "Sihirli tablo başarıyla senkronize edildi", + "Mail Integration" : "Posta Entegrasyonu", + "Mail integration is not available" : "Posta entegrasyonu mevcut değil", + "Make this installation discoverable within the federation network." : "Bu kurulumu federasyon ağı içinde keşfedilebilir yapın.", + "Manage Organisation Roles" : "Kuruluş Rollerini Yönet", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "SOLR Koleksiyonlarını (veri depoları) yönetin ve nesneler ve dosyalar için atayın.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Koleksiyonlarınız için SOLR ConfigSet'lerini (yapılandırma şablonları) yönetin.", + "Manage SOLR collections (data stores) and assign them for objects and files." : "SOLR koleksiyonlarını (veri depoları) yönetin ve nesneler ve dosyalar için atayın.", + "Manage SOLR configsets (configuration templates) for your collections." : "Koleksiyonlarınız için SOLR configset'lerini (yapılandırma şablonları) yönetin.", + "Manage and configure agents for automated tasks" : "Otomatikleştirilmiş görevler için ajanları yönetin ve yapılandırın", + "Manage and monitor file text extraction status" : "Dosya metin çıkarma durumunu yönetin ve izleyin", + "Manage and restore soft deleted items from your registers" : "Kayıtlarınızdan geçici silinen öğeleri yönetin ve geri yükleyin", + "Manage and view detected entities from files and objects" : "Dosyalar ve nesnelerden tespit edilen varlıkları yönetin ve görüntüleyin", + "Manage document templates and themes" : "Belge şablonlarını ve temalarını yönetin", + "Manage webhooks for event-driven integrations" : "Etkinlik odaklı entegrasyonlar için webhook'ları yönetin", + "Manage your account settings, security, and personal data." : "Hesap ayarlarınızı, güvenliğinizi ve kişisel verilerinizi yönetin.", + "Manage your applications and modules" : "Uygulamalarınızı ve modüllerinizi yönetin", + "Manage your chat conversations" : "Sohbet konuşmalarınızı yönetin", + "Manage your data registers and their configurations" : "Veri kayıtlarınızı ve yapılandırmalarını yönetin", + "Manage your data schemas and their properties" : "Veri şemalarınızı ve özelliklerini yönetin", + "Manage your data sources and their configurations" : "Veri kaynaklarınızı ve yapılandırmalarını yönetin", + "Manage your organisations and switch between them" : "Kuruluşlarınızı yönetin ve aralarında geçiş yapın", + "Manage your saved search configurations" : "Kaydedilmiş arama yapılandırmalarınızı yönetin", + "Manage your system configurations and settings" : "Sistem yapılandırmalarınızı ve ayarlarınızı yönetin", + "Managed" : "Yönetilen", + "Mappings" : "Eşlemeler", + "Mass validation completed successfully" : "Toplu doğrulama başarıyla tamamlandı", + "Mass validation failed: {error}" : "Toplu doğrulama başarısız: {error}", + "Max Files (0 = all)" : "Maks Dosyalar (0 = tümü)", + "Max Length" : "Maks Uzunluk", + "Max Length:" : "Maks Uzunluk:", + "Max Objects (0 = all)" : "Maks Nesneler (0 = tümü)", + "Max Retries" : "Maks Yeniden Deneme", + "Max Shards/Node" : "Maks Parça/Düğüm", + "Max Tokens" : "Maks Token", + "Max execution time (ms)" : "Maks yürütme süresi (ms)", + "Max ms" : "Maks ms", + "Max result count" : "Maks sonuç sayısı", + "Max results" : "Maks sonuçlar", + "Maximum" : "Maksimum", + "Maximum File Size (MB)" : "Maksimum Dosya Boyutu (MB)", + "Maximum Nesting Depth" : "Maksimum İç İçe Geçme Derinliği", + "Maximum length" : "Maksimum uzunluk", + "Maximum length constraint is missing" : "Maksimum uzunluk kısıtlaması eksik", + "Maximum length is too restrictive" : "Maksimum uzunluk çok kısıtlayıcı", + "Maximum number of files to process. Set to 0 to process all files." : "İşlenecek maksimum dosya sayısı. Tüm dosyaları işlemek için 0 olarak ayarlayın.", + "Maximum number of items" : "Maksimum öğe sayısı", + "Maximum number of objects to process. Set to 0 to process all objects." : "İşlenecek maksimum nesne sayısı. Tüm nesneleri işlemek için 0 olarak ayarlayın.", + "Maximum number of retry attempts for failed deliveries" : "Başarısız teslimatlar için maksimum yeniden deneme girişimi sayısı", + "Maximum time to wait before committing changes" : "Değişiklikleri kaydetmeden önce beklenecek maksimum süre", + "Maximum tokens to generate" : "Oluşturulacak maksimum token sayısı", + "Maximum value" : "Maksimum değer", + "Maximum value constraint is missing" : "Maksimum değer kısıtlaması eksik", + "Maximum value is too restrictive" : "Maksimum değer çok kısıtlayıcı", + "Medium" : "Orta", + "Medium Confidence" : "Orta Güven", + "Member removed successfully" : "Üye başarıyla kaldırıldı", + "Members" : "Üyeler", + "Members ({count})" : "Üyeler ({count})", + "Members of selected groups can access this view" : "Seçilen grupların üyeleri bu görünüme erişebilir", + "Members:" : "Üyeler:", + "Memory prediction calculated successfully" : "Bellek tahmini başarıyla hesaplandı", + "Message content is required" : "Mesaj içeriği gereklidir", + "Message does not belong to this conversation" : "Mesaj bu konuşmaya ait değil", + "Message not found" : "Mesaj bulunamadı", + "Metadata" : "Meta Veri", + "Metadata Filters" : "Meta Veri Filtreleri", + "Method" : "Yöntem", + "Method*" : "Yöntem*", + "Method: {method}" : "Yöntem: {method}", + "Min Length" : "Min Uzunluk", + "Min execution time (ms)" : "Min yürütme süresi (ms)", + "Min ms" : "Min ms", + "Min result count" : "Min sonuç sayısı", + "Min results" : "Min sonuçlar", + "Minimum" : "Minimum", + "Minimum length" : "Minimum uzunluk", + "Minimum number of items" : "Minimum öğe sayısı", + "Minimum value" : "Minimum değer", + "Minimum value constraint is missing" : "Minimum değer kısıtlaması eksik", + "Minimum value is too restrictive" : "Minimum değer çok kısıtlayıcı", + "Missing conversation or agentUuid" : "Konuşma veya agentUuid eksik", + "Missing conversationId" : "conversationId eksik", + "Missing message" : "Mesaj eksik", + "Missing object fields created successfully" : "Eksik nesne alanları başarıyla oluşturuldu", + "Mode:" : "Mod:", + "Model" : "Model", + "Model Name" : "Model Adı", + "Modified" : "Değiştirildi", + "Monthly" : "Aylık", + "More sources (10-20):" : "Daha fazla kaynak (10-20):", + "Most Active Objects" : "En Aktif Nesneler", + "Multiple Of" : "Katı", + "Multiple of" : "Katı", + "Multiple search trail deletion not implemented yet" : "Çoklu arama izi silme henüz uygulanmadı", + "Multitenancy" : "Çok Kiracılık", + "Multitenancy settings updated successfully" : "Çok kiracılık ayarları başarıyla güncellendi", + "My Account" : "Hesabım", + "Naam" : "Naam", + "Naam *" : "Naam *", + "Name" : "Ad", + "Name *" : "Ad *", + "Name*" : "Ad*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}" : "İsimler önbelleği başarıyla ısıtıldı: {time} sürede {count} isim yüklendi. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}" : "İsimler önbelleği başarıyla ısıtıldı: {time} sürede {count} isim yüklendi. {message}", + "Names cache warmup completed" : "İsimler önbelleği ısıtması tamamlandı", + "Naming pattern for copies:" : "Kopyalar için adlandırma deseni:", + "New Collection Name" : "Yeni Koleksiyon Adı", + "New Configuration" : "Yeni Yapılandırma", + "New Conversation" : "Yeni Konuşma", + "New Properties" : "Yeni Özellikler", + "New Property" : "Yeni Özellik", + "New User Group" : "Yeni Kullanıcı Grubu", + "New Value" : "Yeni Değer", + "New activity" : "Yeni etkinlik", + "New password" : "Yeni şifre", + "New processing activity" : "Yeni işleme faaliyeti", + "New verwerkingsactiviteit" : "Yeni verwerkingsactiviteit", + "Next" : "İleri", + "Nextcloud Groups" : "Nextcloud Grupları", + "No" : "Hayır", + "No API tokens." : "API token'ı yok.", + "No ConfigSets found" : "ConfigSet bulunamadı", + "No Deck cards linked to this object" : "Bu nesneye bağlantılı Deck kartı yok", + "No GitHub token configured" : "GitHub token'ı yapılandırılmamış", + "No GitHub token provided" : "GitHub token'ı sağlanmamış", + "No GitLab token provided" : "GitLab token'ı sağlanmamış", + "No Organisation" : "Kuruluş Yok", + "No active filters" : "Etkin filtre yok", + "No activity data available" : "Etkinlik verisi mevcut değil", + "No activity found." : "Etkinlik bulunamadı.", + "No agents available" : "Mevcut ajan yok", + "No agents found" : "Ajan bulunamadı", + "No app store cache exists yet" : "Henüz uygulama mağazası önbelleği yok", + "No applications are available." : "Mevcut uygulama yok.", + "No applications found" : "Uygulama bulunamadı", + "No approval steps for this object." : "Bu nesne için onay adımı yok.", + "No archived conversations" : "Arşivlenmiş konuşma yok", + "No audit trail entries found" : "Denetim izi girişi bulunamadı", + "No changes recorded" : "Değişiklik kaydedilmedi", + "No chunks to vectorize" : "Vektörleştirilecek parça yok", + "No collections found" : "Koleksiyon bulunamadı", + "No configuration data" : "Yapılandırma verisi yok", + "No configuration selected" : "Yapılandırma seçilmedi", + "No configurations are available." : "Mevcut yapılandırma yok.", + "No configurations found" : "Yapılandırma bulunamadı", + "No configurations found for this application." : "Bu uygulama için yapılandırma bulunamadı.", + "No contacts linked to this object" : "Bu nesneye bağlantılı kişi yok", + "No conversations yet. Create a new one to get started!" : "Henüz konuşma yok. Başlamak için yeni bir tane oluşturun!", + "No dashboards yet" : "Henüz gösterge paneli yok", + "No data available" : "Veri mevcut değil", + "No data available for chart" : "Grafik için veri mevcut değil", + "No deleted items found" : "Silinmiş öğe bulunamadı", + "No deletion data available" : "Silme verisi mevcut değil", + "No description found" : "Açıklama bulunamadı", + "No description provided" : "Açıklama sağlanmadı", + "No emails linked to this object" : "Bu nesneye bağlantılı e-posta yok", + "No endpoints defined" : "Uç nokta tanımlanmamış", + "No entities detected for this email." : "Bu e-posta için varlık algılanmadı.", + "No entities detected in this file. Run text extraction first." : "Bu dosyada varlık algılanmadı. Önce metin çıkarmayı çalıştırın.", + "No entities found" : "Varlık bulunamadı", + "No entities have been detected yet" : "Henüz varlık algılanmadı", + "No events found" : "Etkinlik bulunamadı", + "No events linked to this object" : "Bu nesneye bağlantılı etkinlik yok", + "No expired audit trails found to clear" : "Temizlenecek süresi dolmuş denetim izi bulunamadı", + "No expired search trails found to clear" : "Temizlenecek süresi dolmuş arama izi bulunamadı", + "No extraction data available for this file" : "Bu dosya için çıkarma verisi mevcut değil", + "No files found" : "Dosya bulunamadı", + "No files have been extracted yet" : "Henüz dosya çıkarılmadı", + "No files selected" : "Dosya seçilmedi", + "No files to process" : "İşlenecek dosya yok", + "No files to reindex" : "Yeniden dizinlenecek dosya yok", + "No filters are currently active. This will delete ALL audit trail entries!" : "Şu anda etkin filtre yok. Bu, TÜM denetim izi girişlerini siler!", + "No groups found. Try a different search." : "Grup bulunamadı. Farklı bir arama deneyin.", + "No label" : "Etiket yok", + "No labels" : "Etiket yok", + "No linked objects" : "Bağlantılı nesne yok", + "No log entries found" : "Günlük girişi bulunamadı", + "No logs are available for this configuration." : "Bu yapılandırma için günlük mevcut değil.", + "No logs are available for this source." : "Bu kaynak için günlük mevcut değil.", + "No logs found" : "Günlük bulunamadı", + "No matches" : "Eşleşme yok", + "No mismatched fields found - SOLR schema is properly configured" : "Eşleşmeyen alan bulunamadı - SOLR şeması düzgün şekilde yapılandırılmış", + "No objects are currently selected for deletion." : "Şu anda silme için seçili nesne yok.", + "No objects are currently selected for permanent deletion." : "Şu anda kalıcı silme için seçili nesne yok.", + "No objects are currently selected for restoration." : "Şu anda geri yükleme için seçili nesne yok.", + "No objects found" : "Nesne bulunamadı", + "No objects found to vectorize" : "Vektörleştirilecek nesne bulunamadı", + "No objects linked to this email" : "Bu e-postaya bağlantılı nesne yok", + "No objects selected" : "Nesne seçilmedi", + "No objects selected for deletion" : "Silme için nesne seçilmedi", + "No objects selected for restoration" : "Geri yükleme için nesne seçilmedi", + "No objects to delete" : "Silinecek nesne yok", + "No objects to delete for schema {schema}" : "{schema} şeması için silinecek nesne yok", + "No objects were permanently deleted" : "Hiçbir nesne kalıcı olarak silinmedi", + "No organisations found" : "Kuruluş bulunamadı", + "No personal data was found for this subject identifier." : "Bu özne tanımlayıcısı için kişisel veri bulunamadı.", + "No processing activities yet" : "Henüz işleme faaliyeti yok", + "No properties found" : "Özellik bulunamadı", + "No properties match your filters." : "Filtrelerinizle eşleşen özellik yok.", + "No purge date set" : "Temizleme tarihi ayarlanmadı", + "No register data available" : "Kayıt verisi mevcut değil", + "No register objects reference this file" : "Bu dosyaya atıfta bulunan kayıt nesnesi yok", + "No registers found" : "Kayıt bulunamadı", + "No registers found for this application." : "Bu uygulama için kayıt bulunamadı.", + "No registers found. Create a register to configure permissions." : "Kayıt bulunamadı. İzinleri yapılandırmak için kayıt oluşturun.", + "No related cases found for this sender" : "Bu gönderici için ilgili durum bulunamadı", + "No related entities" : "İlgili varlık yok", + "No relations found" : "İlişki bulunamadı", + "No request body available" : "Mevcut istek gövdesi yok", + "No response body available" : "Mevcut yanıt gövdesi yok", + "No saved views yet. Create one in the Search tab!" : "Henüz kaydedilmiş görünüm yok. Arama sekmesinde bir tane oluşturun!", + "No saved views yet. create one in the search tab!" : "Henüz kaydedilmiş görünüm yok. Arama sekmesinde bir tane oluşturun!", + "No scan run yet" : "Henüz tarama çalıştırılmadı", + "No schema data available" : "Şema verisi mevcut değil", + "No schema selected for exploration" : "Keşif için şema seçilmedi", + "No schemas configured for mail linking." : "Posta bağlantısı için yapılandırılmış şema yok.", + "No schemas found" : "Şema bulunamadı", + "No schemas found for this application." : "Bu uygulama için şema bulunamadı.", + "No search terms data available" : "Arama terimi verisi mevcut değil", + "No search trail entries found" : "Arama izi girişi bulunamadı", + "No sources are available." : "Mevcut kaynak yok.", + "No sources found" : "Kaynak bulunamadı", + "No synchronizations found" : "Senkronizasyon bulunamadı", + "No templates found" : "Şablon bulunamadı", + "No templates have been created yet" : "Henüz şablon oluşturulmadı", + "No tools available. Tools can be registered by installed apps." : "Mevcut araç yok. Araçlar yüklü uygulamalar tarafından kaydedilebilir.", + "No users in this organisation." : "Bu kuruluşta kullanıcı yok.", + "No valid magic-mapped register+schema combinations found" : "Geçerli sihirli eşlenmiş kayıt+şema kombinasyonu bulunamadı", + "No views found. Create views first before configuring vectorization." : "Görünüm bulunamadı. Vektörleştirmeyi yapılandırmadan önce önce görünümler oluşturun.", + "No views match your search" : "Aramanızla eşleşen görünüm yok", + "No webhooks found" : "Webhook bulunamadı", + "No webhooks have been configured yet" : "Henüz webhook yapılandırılmadı", + "No workflows found in this project. Create workflows in the n8n editor." : "Bu projede iş akışı bulunamadı. n8n düzenleyicisinde iş akışları oluşturun.", + "None" : "Hiçbiri", + "Not Found" : "Bulunamadı", + "Not extracted" : "Çıkarılmadı", + "Not helpful" : "Yararlı değil", + "Not in use" : "Kullanımda değil", + "Note:" : "Not:", + "Notifications" : "Bildirimler", + "Number Constraints" : "Sayı Kısıtlamaları", + "Number of Sources" : "Kaynak Sayısı", + "Number of chunks to vectorize in one API call (1-100)." : "Bir API çağrısında vektörleştirilecek parça sayısı (1-100).", + "Number of objects to process in each batch (1-5000)." : "Her toplu işte işlenecek nesne sayısı (1-5000).", + "Numeric Range:" : "Sayısal Aralık:", + "Object" : "Nesne", + "Object #{id}" : "Nesne #{id}", + "Object Collection" : "Nesne Koleksiyonu", + "Object Configuration:" : "Nesne Yapılandırması:", + "Object Count Prediction" : "Nesne Sayısı Tahmini", + "Object Extraction Mode" : "Nesne Çıkarma Modu", + "Object ID" : "Nesne Kimliği", + "Object Management" : "Nesne Yönetimi", + "Object Text Extraction" : "Nesne Metin Çıkarma", + "Object Vectorization" : "Nesne Vektörleştirme", + "Object Warmup" : "Nesne Isınması", + "Object changes in owned objects" : "Sahip olunan nesnelerdeki değişiklikler", + "Object collection not configured" : "Nesne koleksiyonu yapılandırılmamış", + "Object metadata incomplete for file upload" : "Dosya yükleme için nesne meta verisi eksik", + "Object not found" : "Nesne bulunamadı", + "Object permanently deleted" : "Nesne kalıcı olarak silindi", + "Object restored successfully" : "Nesne başarıyla geri yüklendi", + "Object settings updated successfully" : "Nesne ayarları başarıyla güncellendi", + "Object sources" : "Nesne kaynakları", + "Object successfully deleted" : [ + "Nesne başarıyla silindi", + "Nesneler başarıyla silindi"], + "Object successfully modified" : "Nesne başarıyla değiştirildi", + "Object unlocked successfully" : "Nesnenin kilidi başarıyla açıldı", + "Object vectorization configuration saved successfully" : "Nesne vektörleştirme yapılandırması başarıyla kaydedildi", + "Object vectorized successfully" : "Nesne başarıyla vektörleştirildi", + "Object:" : "Nesne:", + "Objects" : "Nesneler", + "Objects Analyzed" : "Analiz Edilen Nesneler", + "Objects Distribution" : "Nesne Dağılımı", + "Objects being analyzed" : "Analiz edilen nesneler", + "Objects by Register" : "Kayda Göre Nesneler", + "Objects by Schema" : "Şemaya Göre Nesneler", + "Objects deletion completed for {schema}" : "{schema} için nesne silme tamamlandı", + "Objects deletion completed successfully" : "Nesne silme başarıyla tamamlandı", + "Objects in schema" : "Şemadaki nesneler", + "Objects successfully deleted" : "Nesneler başarıyla silindi", + "Objects to Process:" : "İşlenecek Nesneler:", + "Objects to be analyzed" : "Analiz edilecek nesneler", + "Objects to be deleted" : "Silinecek nesneler", + "Objects to be validated" : "Doğrulanacak nesneler", + "Objects will be serialized as JSON text before vectorization" : "Nesneler vektörleştirmeden önce JSON metni olarak serileştirilecek", + "Objects will be soft deleted and moved to the" : "Nesneler geçici silinecek ve taşınacak", + "Old Value" : "Eski Değer", + "Oldest Item (days)" : "En Eski Öğe (gün)", + "Ollama Chat Settings" : "Ollama Sohbet Ayarları", + "Ollama Embedding Configuration" : "Ollama Gömülüm Yapılandırması", + "Ollama URL" : "Ollama URL", + "Only With Changes" : "Yalnızca Değişikliklerle", + "Only members of selected groups can access this organisation" : "Yalnızca seçilen grupların üyeleri bu kuruluşa erişebilir", + "Open Nextcloud App Store" : "Nextcloud Uygulama Mağazasını Aç", + "Open OpenRegister sidebar" : "OpenRegister kenar çubuğunu aç", + "Open Register Objects" : "Kayıt Nesnelerini Aç", + "Open folder" : "Klasörü aç", + "Open in OpenRegister" : "OpenRegister'da Aç", + "Open n8n Editor" : "n8n Düzenleyiciyi Aç", + "OpenAI Chat Settings" : "OpenAI Sohbet Ayarları", + "OpenAI Embedding Configuration" : "OpenAI Gömülüm Yapılandırması", + "OpenDocument (.ods)" : "OpenDocument (.ods)", + "OpenRegister" : "OpenRegister", + "OpenRegister Settings" : "OpenRegister Ayarları", + "Optimizing search performance..." : "Arama performansı optimize ediliyor...", + "Optional URL-friendly identifier" : "İsteğe bağlı URL dostu tanımlayıcı", + "Optional description for this facet" : "Bu faset için isteğe bağlı açıklama", + "Optional end date property" : "İsteğe bağlı bitiş tarihi özelliği", + "Optional event description template" : "İsteğe bağlı etkinlik açıklama şablonu", + "Optional location property" : "İsteğe bağlı konum özelliği", + "Optional webhook secret for signature verification" : "İmza doğrulama için isteğe bağlı webhook gizliliği", + "Or" : "Veya", + "Order" : "Sıra", + "Organisation" : "Kuruluş", + "Organisation Configuration" : "Kuruluş Yapılandırması", + "Organisation ID" : "Kuruluş Kimliği", + "Organisation Members" : "Kuruluş Üyeleri", + "Organisation Statistics" : "Kuruluş İstatistikleri", + "Organisation created successfully" : "Kuruluş başarıyla oluşturuldu", + "Organisation membership changes" : "Kuruluş üyeliği değişiklikleri", + "Organisation name is required" : "Kuruluş adı gereklidir", + "Organisation settings saved successfully" : "Kuruluş ayarları başarıyla kaydedildi", + "Organisation successfully created" : "Kuruluş başarıyla oluşturuldu", + "Organisation successfully updated" : "Kuruluş başarıyla güncellendi", + "Organisational measures" : "Organizasyonel önlemler", + "Organisations" : "Kuruluşlar", + "Organisatorische maatregelen" : "Organisatorische maatregelen", + "Organization" : "Kuruluş", + "Organization ID (Optional)" : "Kuruluş Kimliği (İsteğe Bağlı)", + "Organizations" : "Kuruluşlar", + "Orphaned Items" : "Sahipsiz Öğeler", + "Other" : "Diğer", + "Output Mapping" : "Çıkış Eşlemesi", + "Overview" : "Genel Bakış", + "Owner" : "Sahip", + "Owner:" : "Sahip:", + "PDF" : "PDF", + "PII hits" : "KBB eşleşmeleri", + "PII rows" : "KBB satırları", + "Page {current} of {total}" : "{total} sayfanın {current}. sayfası", + "Parallel Mode (Faster, more resource intensive)" : "Paralel Mod (Daha hızlı, daha fazla kaynak tüketen)", + "Parallel:" : "Paralel:", + "Password" : "Şifre", + "Password changes are not supported by your authentication provider." : "Kimlik doğrulama sağlayıcınız şifre değişikliklerini desteklemiyor.", + "Password for SOLR authentication (optional)" : "SOLR kimlik doğrulaması için şifre (isteğe bağlı)", + "Password updated successfully" : "Şifre başarıyla güncellendi", + "Path" : "Yol", + "Path in repository" : "Depodaki yol", + "Path where the configuration file will be saved in the repository" : "Yapılandırma dosyasının depoya kaydedileceği yol", + "Path where the register OAS file will be saved in the repository" : "Kayıt OAS dosyasının depoya kaydedileceği yol", + "Pattern (regex)" : "Desen (regex)", + "Pattern Issue" : "Desen Sorunu", + "Pattern constraint is missing" : "Desen kısıtlaması eksik", + "Patterns:" : "Desenler:", + "Pending" : "Beklemede", + "Permanent Delete Mode" : "Kalıcı Silme Modu", + "Permanently Delete" : "Kalıcı Olarak Sil", + "Permanently Delete ({count})" : "Kalıcı Olarak Sil ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!" : "Tüm {count} geçici silinmiş nesneyi kalıcı olarak silin. Bu işlem geri alınamaz!", + "Permanently delete already soft-deleted objects" : "Zaten geçici silinmiş nesneleri kalıcı olarak silin", + "Permanently delete this conversation?" : "Bu konuşma kalıcı olarak silinsin mi?", + "Permission Matrix" : "İzin Matrisi", + "Person" : "Kişi", + "Personal Data" : "Kişisel Veri", + "Personal Data Export" : "Kişisel Veri Dışa Aktarımı", + "Persons" : "Kişiler", + "Phone" : "Telefon", + "Phone Numbers" : "Telefon Numaraları", + "Photos" : "Fotoğraflar", + "Please create an agent in the" : "Lütfen bir ajan oluşturun", + "Please select a user" : "Lütfen bir kullanıcı seçin", + "Please select an agent to continue" : "Devam etmek için lütfen bir ajan seçin", + "Please select an organisation" : "Lütfen bir kuruluş seçin", + "Please select at least one item to import" : "İçe aktarmak için lütfen en az bir öğe seçin", + "Please select registers or schemas first" : "Lütfen önce kayıtları veya şemaları seçin", + "Please select which register and schema to use for the new object" : "Yeni nesne için hangi kayıt ve şemanın kullanılacağını seçin", + "Please try again later." : "Lütfen daha sonra tekrar deneyin.", + "Please wait while we fetch your agents." : "Ajanlarınız getirilirken lütfen bekleyin.", + "Please wait while we fetch your applications." : "Uygulamalarınız getirilirken lütfen bekleyin.", + "Please wait while we fetch your configurations." : "Yapılandırmalarınız getirilirken lütfen bekleyin.", + "Please wait while we fetch your deleted items." : "Silinen öğeleriniz getirilirken lütfen bekleyin.", + "Please wait while we fetch your sources." : "Kaynaklarınız getirilirken lütfen bekleyin.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments." : "SOLR koleksiyonu kalıcı olarak silinirken lütfen bekleyin. Bu birkaç dakika sürebilir.", + "Polls" : "Anketler", + "Popular Search Terms" : "Popüler Arama Terimleri", + "Port" : "Port", + "Portabiliteit (Art 20)" : "Portabiliteit (Madde 20)", + "Preferences saved" : "Tercihler kaydedildi", + "Prerequisites" : "Ön Koşullar", + "Press Enter to send, Shift+Enter for new line" : "Göndermek için Enter'a, yeni satır için Shift+Enter'a basın", + "Press enter to send, shift+enter for new line" : "Göndermek için enter'a, yeni satır için shift+enter'a basın", + "Prettify" : "Güzelleştir", + "Preview erasure" : "Silme önizlemesi", + "Preview:" : "Önizleme:", + "Previous" : "Önceki", + "Private" : "Özel", + "Private Agent (Default)" : "Özel Ajan (Varsayılan)", + "Private agents" : "Özel ajanlar", + "Process Name (optional)" : "Süreç Adı (isteğe bağlı)", + "Processed {indexed} files, {failed} failed" : "{indexed} dosya işlendi, {failed} başarısız", + "Processes chunks in batches with simulated parallelism." : "Parçaları simüle edilmiş paralellikle toplu olarak işler.", + "Processes file chunks sequentially (safest)." : "Dosya parçalarını sırayla işler (en güvenli).", + "Processes objects in chunks with simulated parallelism." : "Nesneleri simüle edilmiş paralellikle parçalar halinde işler.", + "Processes objects sequentially (safest)." : "Nesneleri sırayla işler (en güvenli).", + "Processing" : "İşleniyor", + "Processing Limits" : "İşleme Sınırları", + "Processing..." : "İşleniyor...", + "Project initialized successfully" : "Proje başarıyla başlatıldı", + "Project not found. Please initialize first." : "Proje bulunamadı. Lütfen önce başlatın.", + "Properties" : "Özellikler", + "Property Behaviors" : "Özellik Davranışları", + "Property Configuration:" : "Özellik Yapılandırması:", + "Property Filters" : "Özellik Filtreleri", + "Property Title" : "Özellik Başlığı", + "Property Type" : "Özellik Türü", + "Property can be improved" : "Özellik geliştirilebilir", + "Property could not be added" : "Özellik eklenemedi", + "Property could not be updated" : "Özellik güncellenemedi", + "Property name of inversed relation" : "Ters ilişkinin özellik adı", + "Property successfully added" : "Özellik başarıyla eklendi", + "Property successfully updated" : "Özellik başarıyla güncellendi", + "Protected" : "Korumalı", + "Provider" : "Sağlayıcı", + "Provider is required for testing" : "Test için sağlayıcı gereklidir", + "Public" : "Genel", + "Public View" : "Genel Görünüm", + "Public views can be accessed by anyone in the system" : "Genel görünümlere sistemdeki herkes erişebilir", + "Publish" : "Yayımla", + "Publish Configuration to GitHub" : "Yapılandırmayı GitHub'a Yayımla", + "Publish OAS" : "OAS'yi Yayımla", + "Publish Objects" : "Nesneleri Yayımla", + "Publish Register OAS to GitHub" : "Kayıt OAS'sini GitHub'a Yayımla", + "Publish all objects in this schema" : "Bu şemadaki tüm nesneleri yayımla", + "Publish objects to make them publicly available" : "Nesneleri herkese açık yapmak için yayımla", + "Published" : "Yayımlandı", + "Published:" : "Yayımlandı:", + "Publishing..." : "Yayımlanıyor...", + "Purge" : "Temizle", + "Purge Date" : "Temizleme Tarihi", + "Purge {count} object from database" : [ + "Veritabanından {count} nesneyi temizle", + "Veritabanından {count} nesneyi temizle"], + "Purge {count} objects from database" : "Veritabanından {count} nesneyi temizle", + "Purpose limitation *" : "Amaç sınırlaması *", + "Push Notifications" : "Anlık Bildirimler", + "Query Complexity Distribution" : "Sorgu Karmaşıklığı Dağılımı", + "Query Help" : "Sorgu Yardımı", + "Query parameter is required" : "Sorgu parametresi gereklidir", + "Quota" : "Kota", + "RAG" : "RAG", + "RAG Configuration" : "RAG Yapılandırması", + "RBAC" : "RBAC", + "RBAC settings updated successfully" : "RBAC ayarları başarıyla güncellendi", + "Rate limits cleared successfully" : "Hız sınırları başarıyla temizlendi", + "Raw Changes Data" : "Ham Değişiklik Verisi", + "Re-vectorize on object update" : "Nesne güncellemesinde yeniden vektörleştir", + "Read" : "Oku", + "Real-time push notification status via notify_push" : "notify_push aracılığıyla gerçek zamanlı anlık bildirim durumu", + "Realtime push active" : "Gerçek zamanlı anlık iletim etkin", + "Realtime push not available — the notify_push app is not installed" : "Gerçek zamanlı anlık iletim mevcut değil — notify_push uygulaması yüklü değil", + "Reason" : "Neden", + "Reason (optional)" : "Neden (isteğe bağlı)", + "Rebase operation completed successfully" : "Yeniden taban alma işlemi başarıyla tamamlandı", + "Rebase operation failed: {error}" : "Yeniden taban alma işlemi başarısız: {error}", + "Rechtsgrond" : "Rechtsgrond", + "Recommendations:" : "Öneriler:", + "Recommended Type:" : "Önerilen Tür:", + "Recommended: 5 sources" : "Önerilen: 5 kaynak", + "Refresh" : "Yenile", + "Refresh Data" : "Veriyi Yenile", + "Refresh Stats" : "İstatistikleri Yenile", + "Refresh Workflows" : "İş Akışlarını Yenile", + "Refresh dashboard" : "Gösterge panelini yenile", + "Refresh database info" : "Veritabanı bilgisini yenile", + "Register" : "Kayıt", + "Register #{id}" : "Kayıt #{id}", + "Register / Schema" : "Kayıt / Şema", + "Register ID" : "Kayıt Kimliği", + "Register Objects" : "Kayıt Nesneleri", + "Register Overview" : "Kayıt Genel Bakışı", + "Register Statistics" : "Kayıt İstatistikleri", + "Register Totals" : "Kayıt Toplamları", + "Register and schema are required" : "Kayıt ve şema gereklidir", + "Register annotation" : "Kayıt ek açıklaması", + "Register depublished successfully" : "Kayıt başarıyla yayından kaldırıldı", + "Register not found" : "Kayıt bulunamadı", + "Register objects" : "Kayıt nesneleri", + "Register objects deletion completed successfully" : "Kayıt nesneleri silme başarıyla tamamlandı", + "Register published successfully" : "Kayıt başarıyla yayımlandı", + "Register/Schema Usage" : "Kayıt/Şema Kullanımı", + "Register:" : "Kayıt:", + "Registers" : "Kayıtlar", + "Reindex" : "Yeniden Dizinle", + "Reindex completed" : "Yeniden dizinleme tamamlandı", + "Reindex completed successfully" : "Yeniden dizinleme başarıyla tamamlandı", + "Reindex completed! Processed {count} objects in {duration}s" : "Yeniden dizinleme tamamlandı! {duration} saniyede {count} nesne işlendi", + "Reindex failed" : "Yeniden dizinleme başarısız", + "Reindex failed: %s" : "Yeniden dizinleme başarısız: %s", + "Reject" : "Reddet", + "Rejected" : "Reddedildi", + "Related Cases" : "İlgili Durumlar", + "Relations" : "İlişkiler", + "Remote Version" : "Uzak Sürüm", + "Remove" : "Kaldır", + "Remove Deck card" : "Deck kartını kaldır", + "Remove avatar" : "Avatar kaldır", + "Remove contact" : "Kişi kaldır", + "Remove filter" : "Filtreyi kaldır", + "Remove from favorites" : "Favorilerden kaldır", + "Remove from list" : "Listeden kaldır", + "Remove group" : "Grubu kaldır", + "Remove link" : "Bağlantıyı kaldır", + "Remove link to {name}" : "{name} öğesine bağlantıyı kaldır", + "Remove link to {name}?" : "{name} öğesine bağlantı kaldırılsın mı?", + "Remove link to {title}" : "{title} öğesine bağlantıyı kaldır", + "Remove oneOf entry" : "oneOf girişini kaldır", + "Remove property after write-back" : "Geri yazmadan sonra özelliği kaldır", + "Remove {title}" : "{title} öğesini kaldır", + "Remove {userId} from this organisation?" : "{userId} bu kuruluştan kaldırılsın mı?", + "Removed" : "Kaldırıldı", + "Removed from favorites" : "Favorilerden kaldırıldı", + "Removing schema {schema}..." : "{schema} şeması kaldırılıyor...", + "Rename Conversation" : "Konuşmayı Yeniden Adlandır", + "Rename conversation" : "Konuşmayı yeniden adlandır", + "Replicas" : "Replikalar", + "Reports" : "Raporlar", + "Repository" : "Depo", + "Request Body" : "İstek Gövdesi", + "Request Data" : "İstek Verisi", + "Request Details" : "İstek Ayrıntıları", + "Request Quota (per day)" : "İstek Kotası (günlük)", + "Request account deactivation" : "Hesap devre dışı bırakma talebinde bulun", + "Request account deactivation. This will notify administrators for review." : "Hesap devre dışı bırakma talebinde bulunun. Bu, yöneticilere inceleme için bildirim gönderecek.", + "Request body must be JSON with a \"query\" field" : "İstek gövdesi \"query\" alanına sahip JSON olmalıdır", + "Request timeout in seconds" : "Saniye cinsinden istek zaman aşımı", + "Requested at" : "Talep edildi:", + "Required" : "Gerekli", + "Required Token Scopes" : "Gerekli Token Kapsamları", + "Required field" : "Zorunlu alan", + "Required status is inconsistent" : "Gerekli durum tutarsız", + "Requires Dolphin AI for OCR text extraction" : "OCR metin çıkarma için Dolphin AI gerektirir", + "Rerun Search" : "Aramayı Yeniden Çalıştır", + "Rerunning search: {searchTerm}" : "Arama yeniden çalıştırılıyor: {searchTerm}", + "Reset Changes" : "Değişiklikleri Sıfırla", + "Reset Filters" : "Filtreleri Sıfırla", + "Resource Quotas" : "Kaynak Kotaları", + "Response Body" : "Yanıt Gövdesi", + "Response Details" : "Yanıt Ayrıntıları", + "Response Time:" : "Yanıt Süresi:", + "Restore" : "Geri Yükle", + "Restore conversation" : "Konuşmayı geri yükle", + "Restore or permanently delete items" : "Öğeleri geri yükleyin veya kalıcı olarak silin", + "Restore {count} object" : [ + "{count} nesneyi geri yükle", + "{count} nesneyi geri yükle"], + "Restore {count} objects" : "{count} nesneyi geri yükle", + "Result Count Range" : "Sonuç Sayısı Aralığı", + "Results" : "Sonuçlar", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)" : "Saklama süresi (ISO-8601 süresi, örn. P10Y, P30D)", + "Retention settings updated successfully" : "Saklama ayarları başarıyla güncellendi", + "Retrieve all objects for this schema" : "Bu şema için tüm nesneleri al", + "Retry" : "Yeniden Dene", + "Retry Failed Extractions" : "Başarısız Çıkarmaları Yeniden Dene", + "Retry Policy" : "Yeniden Deneme Politikası", + "Retry completed" : "Yeniden deneme tamamlandı", + "Retry upload" : "Yüklemeyi yeniden dene", + "Retrying..." : "Yeniden deneniyor...", + "Revoke" : "İptal Et", + "Right" : "Sağ", + "Risk Level" : "Risk Seviyesi", + "Risk level" : "Risk seviyesi", + "Roles and Permissions" : "Roller ve İzinler", + "Roles updated successfully" : "Roller başarıyla güncellendi", + "Run compliance scan" : "Uyumluluk taraması çalıştır", + "SOLR Connection Settings" : "SOLR Bağlantı Ayarları", + "SOLR Index Warmup" : "SOLR Dizin Isınması", + "SOLR Version:" : "SOLR Sürümü:", + "SOLR actions menu" : "SOLR işlemleri menüsü", + "SOLR base path (usually /solr)" : "SOLR temel yolu (genellikle /solr)", + "SOLR collection deleted successfully" : "SOLR koleksiyonu başarıyla silindi", + "SOLR connection test failed: {error}" : "SOLR bağlantı testi başarısız: {error}", + "SOLR connection test successful!" : "SOLR bağlantı testi başarılı!", + "SOLR index warmup completed successfully" : "SOLR dizin ısınması başarıyla tamamlandı", + "SOLR is disabled" : "SOLR devre dışı", + "SOLR is not available or not configured" : "SOLR mevcut değil veya yapılandırılmamış", + "SOLR logging disabled" : "SOLR günlüğü devre dışı", + "SOLR logging enabled" : "SOLR günlüğü etkin", + "SOLR search disabled" : "SOLR araması devre dışı", + "SOLR search enabled" : "SOLR araması etkin", + "SOLR server hostname or IP address" : "SOLR sunucusu ana bilgisayar adı veya IP adresi", + "SOLR server port number (optional, defaults to 8983)" : "SOLR sunucusu port numarası (isteğe bağlı, varsayılan 8983)", + "SOLR settings updated successfully" : "SOLR ayarları başarıyla güncellendi", + "SOLR setup completed successfully" : "SOLR kurulumu başarıyla tamamlandı", + "SOLR setup completed successfully!" : "SOLR kurulumu başarıyla tamamlandı!", + "SOLR setup error: %s" : "SOLR kurulum hatası: %s", + "SOLR setup failed" : "SOLR kurulumu başarısız", + "SOLR setup failed - check logs" : "SOLR kurulumu başarısız - günlükleri kontrol edin", + "SOLR setup failed. Please check the configuration and try again." : "SOLR kurulumu başarısız. Lütfen yapılandırmayı kontrol edip tekrar deneyin.", + "SOLR setup failed: {error}" : "SOLR kurulumu başarısız: {error}", + "SOLR warmup failed: {error}" : "SOLR ısınması başarısız: {error}", + "Sample Values:" : "Örnek Değerler:", + "Save" : "Kaydet", + "Save Configuration" : "Yapılandırmayı Kaydet", + "Save Connection Settings" : "Bağlantı Ayarlarını Kaydet", + "Save Roles" : "Rolleri Kaydet", + "Save Settings" : "Ayarları Kaydet", + "Save changes" : "Değişiklikleri kaydet", + "Save current search as view" : "Mevcut aramayı görünüm olarak kaydet", + "Save labels" : "Etiketleri kaydet", + "Save tags for {name}" : "{name} için etiketleri kaydet", + "Save the organisation first to manage users." : "Kullanıcıları yönetmek için önce kuruluşu kaydedin.", + "Saved Views" : "Kaydedilmiş Görünümler", + "Saving..." : "Kaydediliyor...", + "Scheduled" : "Zamanlandı", + "Schema" : "Şema", + "Schema ID" : "Şema Kimliği", + "Schema Name" : "Şema Adı", + "Schema Statistics" : "Şema İstatistikleri", + "Schema annotation" : "Şema ek açıklaması", + "Schema configuration updated successfully for {schema}" : "{schema} için şema yapılandırması başarıyla güncellendi", + "Schema depublished successfully" : "Şema başarıyla yayından kaldırıldı", + "Schema objects deletion completed successfully" : "Şema nesneleri silme başarıyla tamamlandı", + "Schema published successfully" : "Şema başarıyla yayımlandı", + "Schema reference ($ref)" : "Şema referansı ($ref)", + "Schema successfully created" : "Şema başarıyla oluşturuldu", + "Schema successfully updated" : "Şema başarıyla güncellendi", + "Schema {schema} removed successfully" : "{schema} şeması başarıyla kaldırıldı", + "Schema:" : "Şema:", + "Schemas" : "Şemalar", + "Scheme" : "Şema", + "Search" : "Ara", + "Search / Views" : "Arama / Görünümler", + "Search / views" : "Arama / görünümler", + "Search Activity" : "Arama Etkinliği", + "Search Analytics" : "Arama Analitiği", + "Search Configuration" : "Arama Yapılandırması", + "Search Entities" : "Varlıkları Ara", + "Search Files" : "Dosyaları Ara", + "Search GitHub" : "GitHub'da Ara", + "Search GitLab" : "GitLab'da Ara", + "Search Method" : "Arama Yöntemi", + "Search Mode" : "Arama Modu", + "Search Query" : "Arama Sorgusu", + "Search Results" : "Arama Sonuçları", + "Search Term" : "Arama Terimi", + "Search Trail Details" : "Arama İzi Ayrıntıları", + "Search Trail Management" : "Arama İzi Yönetimi", + "Search Trail Statistics" : "Arama İzi İstatistikleri", + "Search Trails" : "Arama İzleri", + "Search Views" : "Görünümlerde Ara", + "Search Webhooks" : "Webhook'larda Ara", + "Search agents..." : "Ajanlarda ara...", + "Search applications..." : "Uygulamalarda ara...", + "Search by file name or path" : "Dosya adı veya yola göre ara", + "Search by name or URL" : "Ada veya URL'ye göre ara", + "Search by title or UUID..." : "Başlık veya UUID'ye göre ara...", + "Search by value" : "Değere göre ara", + "Search configurations" : "Yapılandırmalarda ara", + "Search data sources..." : "Veri kaynaklarında ara...", + "Search groups..." : "Gruplarda ara...", + "Search in Database Objects" : "Veritabanı Nesnelerinde Ara", + "Search in Files" : "Dosyalarda Ara", + "Search in Objects" : "Nesnelerde Ara", + "Search objects" : "Nesnelerde ara", + "Search objects..." : "Nesnelerde ara...", + "Search property names..." : "Özellik adlarında ara...", + "Search registers..." : "Kayıtlarda ara...", + "Search schemas..." : "Şemalarda ara...", + "Search trail deletion not implemented yet" : "Arama izi silme henüz uygulanmadı", + "Search views..." : "Görünümlerde ara...", + "Search {name}..." : "{name} arama...", + "Searching..." : "Aranıyor...", + "Secret" : "Gizli", + "Secret key for HMAC signature generation (optional)" : "HMAC imza oluşturma için gizli anahtar (isteğe bağlı)", + "Security" : "Güvenlik", + "See organisations as a table" : "Kuruluşları tablo olarak görüntüle", + "See organisations as cards" : "Kuruluşları kartlar olarak görüntüle", + "Select AI Agent" : "Yapay Zeka Ajanı Seç", + "Select Active Organisation" : "Etkin Kuruluş Seç", + "Select All" : "Tümünü Seç", + "Select ConfigSet" : "ConfigSet Seç", + "Select File Types to Vectorize:" : "Vektörleştirilecek Dosya Türlerini Seçin:", + "Select HTTP method" : "HTTP yöntemi seç", + "Select Register and Schema" : "Kayıt ve Şema Seç", + "Select Views to Vectorize:" : "Vektörleştirilecek Görünümleri Seçin:", + "Select a Nextcloud group to add" : "Eklenecek Nextcloud grubunu seçin", + "Select a branch" : "Dal seçin", + "Select a date property" : "Tarih özelliği seçin", + "Select a model or type a custom model name" : "Model seçin veya özel model adı girin", + "Select a register" : "Kayıt seçin", + "Select a register..." : "Kayıt seçin...", + "Select a repository" : "Depo seçin", + "Select a repository you have write access to" : "Yazma erişiminiz olan bir depo seçin", + "Select a schema" : "Şema seçin", + "Select a schema..." : "Şema seçin...", + "Select a user" : "Kullanıcı seçin", + "Select agent type" : "Ajan türü seçin", + "Select all entities" : "Tüm varlıkları seç", + "Select an AI Agent" : "Bir Yapay Zeka Ajanı Seçin", + "Select an AI agent to begin chatting with your data." : "Verilerinizle sohbet etmeye başlamak için bir yapay zeka ajanı seçin.", + "Select an email to see linked objects" : "Bağlantılı nesneleri görmek için bir e-posta seçin", + "Select backend" : "Backend seçin", + "Select chat model" : "Sohbet modeli seçin", + "Select collection for files" : "Dosyalar için koleksiyon seçin", + "Select collection for objects" : "Nesneler için koleksiyon seçin", + "Select configuration type..." : "Yapılandırma türü seçin...", + "Select default organisation" : "Varsayılan kuruluş seçin", + "Select entity {value}" : "{value} varlığını seçin", + "Select event to listen to..." : "Dinlenecek etkinlik seçin...", + "Select fields to display" : "Görüntülenecek alanları seçin", + "Select files with the correct extension" : "Doğru uzantılı dosyaları seçin", + "Select group" : "Grup seçin", + "Select groups (optional)" : "Grupları seçin (isteğe bağlı)", + "Select groups to notify..." : "Bildirim gönderilecek grupları seçin...", + "Select groups with access to this agent" : "Bu ajana erişimi olan grupları seçin", + "Select groups..." : "Grupları seçin...", + "Select model" : "Model seçin", + "Select one or more registers" : "Bir veya daha fazla kayıt seçin", + "Select one or more schemas" : "Bir veya daha fazla şema seçin", + "Select options..." : "Seçenekleri seçin...", + "Select or create labels, or select \"No label\" to add files" : "Etiketleri seçin veya oluşturun ya da dosya eklemek için \"Etiket yok\" seçeneğini seçin", + "Select owner application (optional)..." : "Sahip uygulamasını seçin (isteğe bağlı)...", + "Select period" : "Dönem seçin", + "Select property to send as payload" : "Yük olarak gönderilecek özelliği seçin", + "Select provider" : "Sağlayıcı seçin", + "Select registers and schemas to save a view" : "Görünüm kaydetmek için kayıtları ve şemaları seçin", + "Select retry policy" : "Yeniden deneme politikası seçin", + "Select role" : "Rol seçin", + "Select schemas (empty = all schemas)" : "Şemaları seçin (boş = tüm şemalar)", + "Select schemas to warm up" : "Isıtılacak şemaları seçin", + "Select search mode" : "Arama modu seçin", + "Select source type..." : "Kaynak türü seçin...", + "Select target type" : "Hedef tür seçin", + "Select the branch to publish to" : "Yayımlanacak dalı seçin", + "Select the event this webhook should listen to" : "Bu webhook'un dinlemesi gereken etkinliği seçin", + "Select views to filter data (optional)" : "Verileri filtrelemek için görünümleri seçin (isteğe bağlı)", + "Select views to limit which data the agent can access" : "Ajanın hangi verilere erişebileceğini sınırlamak için görünümleri seçin", + "Select views to vectorize:" : "Vektörleştirilecek görünümleri seçin:", + "Select which data views the AI can search" : "Yapay zekanın arayabileceği veri görünümlerini seçin", + "Select which property from the event should be used as the webhook payload data" : "Etkinlikten hangi özelliğin webhook yük verisi olarak kullanılacağını seçin", + "Select which tools the AI can use to perform actions" : "Yapay zekanın işlem gerçekleştirmek için kullanabileceği araçları seçin", + "Select {fieldName} values" : "{fieldName} değerlerini seçin", + "Selected" : "Seçildi", + "Selected Groups" : "Seçilen Gruplar", + "Selected Objects ({count})" : "Seçilen Nesneler ({count})", + "Selected audit trails deleted successfully" : "Seçilen denetim izleri başarıyla silindi", + "Selected search trails deleted successfully" : "Seçilen arama izleri başarıyla silindi", + "Selected users can access this view" : "Seçilen kullanıcılar bu görünüme erişebilir", + "Send additional feedback" : "Ek geri bildirim gönder", + "Send as CloudEvent" : "CloudEvent olarak gönder", + "Send as cloudevent" : "Cloudevent olarak gönder", + "Sensitive PII" : "Hassas KBK", + "Serial Mode (Safer, slower)" : "Seri Mod (Daha güvenli, daha yavaş)", + "Serial:" : "Seri:", + "Server Configuration" : "Sunucu Yapılandırması", + "Server Information" : "Sunucu Bilgisi", + "Set as Active" : "Etkin Olarak Ayarla", + "Set as active organisation" : "Etkin kuruluş olarak ayarla", + "Set limits for API usage and token consumption. Use 0 for unlimited resources." : "API kullanımı ve token tüketimi için sınırlar belirleyin. Sınırsız kaynaklar için 0 kullanın.", + "Settings" : "Ayarlar", + "Settings updated successfully" : "Ayarlar başarıyla güncellendi", + "Shards" : "Parçalar", + "Share" : "Paylaş", + "Share with Groups" : "Gruplarla Paylaş", + "Share with Users" : "Kullanıcılarla Paylaş", + "Show" : "Göster", + "Show Filters" : "Filtreleri Göster", + "Show less" : "Daha az göster", + "Show only entries with changes" : "Yalnızca değişiklikli girişleri göster", + "Showing {count} dashboard(s)" : "{count} gösterge paneli gösteriliyor", + "Showing {count} processing activities" : "{count} işleme faaliyeti gösteriliyor", + "Showing {showing} of {total} agents" : "{total} ajandan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} applications" : "{total} uygulamadan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} audit trail entries" : "{total} denetim izi girişinden {showing} tanesi gösteriliyor", + "Showing {showing} of {total} configurations" : "{total} yapılandırmadan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} deleted items" : "{total} silinen öğeden {showing} tanesi gösteriliyor", + "Showing {showing} of {total} entities" : "{total} varlıktan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} files" : "{total} dosyadan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} log entries" : "{total} günlük girişinden {showing} tanesi gösteriliyor", + "Showing {showing} of {total} search trail entries" : "{total} arama izi girişinden {showing} tanesi gösteriliyor", + "Showing {showing} of {total} sources" : "{total} kaynaktan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} templates" : "{total} şablondan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} webhooks" : "{total} webhook'tan {showing} tanesi gösteriliyor", + "Showing {shown} of {total} organisations" : "{total} kuruluştan {shown} tanesi gösteriliyor", + "Simple" : "Basit", + "Size" : "Boyut", + "Slug" : "Slug", + "Soft Delete Mode" : "Geçici Silme Modu", + "Soft Deleted Items" : "Geçici Silinen Öğeler", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)" : "Bu şemadaki tüm nesneleri geçici sil ({active} etkin, {deleted} zaten silindi)", + "SolrCloud mode enabled" : "SolrCloud modu etkin", + "Something went wrong while adding files" : "Dosyalar eklenirken bir şeyler ters gitti", + "Source" : "Kaynak", + "Source successfully updated" : "Kaynak başarıyla güncellendi", + "Sources" : "Kaynaklar", + "Sources:" : "Kaynaklar:", + "Special Rights" : "Özel Haklar", + "Standalone SOLR mode" : "Bağımsız SOLR modu", + "Start Conversation" : "Konuşmayı Başlat", + "Start Date Field" : "Başlangıç Tarihi Alanı", + "Start Vectorization" : "Vektörleştirmeyi Başlat", + "Start a conversation" : "Konuşma başlat", + "Starting names cache warmup..." : "İsimler önbelleği ısıtması başlatılıyor...", + "Starting validation for {schema}..." : "{schema} için doğrulama başlatılıyor...", + "Starting {action} for {schema}..." : "{schema} için {action} başlatılıyor...", + "Starting..." : "Başlatılıyor...", + "Statistics" : "İstatistikler", + "Statistics and Metrics" : "İstatistikler ve Metrikler", + "Statistics refreshed" : "İstatistikler yenilendi", + "Stats calculation not yet implemented" : "İstatistik hesaplama henüz uygulanmadı", + "Status" : "Durum", + "Status Code" : "Durum Kodu", + "Step" : "Adım", + "Storage" : "Depolama", + "Storage Quota (MB)" : "Depolama Kotası (MB)", + "String Constraints" : "Dize Kısıtlamaları", + "Subject identifier (email, BSN, name, etc.)" : "Özne tanımlayıcısı (e-posta, BSN, isim vb.)", + "Subscribe" : "Abone Ol", + "Subscribed" : "Abone Olundu", + "Success" : "Başarı", + "Success Rate" : "Başarı Oranı", + "Success Status" : "Başarı Durumu", + "Successful" : "Başarılı", + "Successfully added user to organisation: {name}" : "Kullanıcı başarıyla kuruluşa eklendi: {name}", + "Successfully cleared {count} audit trails" : "{count} denetim izi başarıyla temizlendi", + "Successfully cleared {count} blob storage objects" : "{count} blob depolama nesnesi başarıyla temizlendi", + "Successfully cleared {count} search trails" : "{count} arama izi başarıyla temizlendi", + "Successfully created {count} SOLR fields" : "{count} SOLR alanı başarıyla oluşturuldu", + "Successfully deleted {count} embeddings. Please re-vectorize your data." : "{count} gömülüm başarıyla silindi. Lütfen verilerinizi yeniden vektörleştirin.", + "Successfully deleted {count} objects for {schema}" : "{schema} için {count} nesne başarıyla silindi", + "Successfully fixed {count} SOLR field configurations" : "{count} SOLR alan yapılandırması başarıyla düzeltildi", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects" : "Başarıyla içe aktarıldı: {registers} kayıt, {schemas} şema, {objects} nesne", + "Successfully joined organisation" : "Kuruluşa başarıyla katıldı", + "Successfully permanently deleted {count} objects" : "{count} nesne başarıyla kalıcı olarak silindi", + "Successfully processed {count} files!" : "{count} dosya başarıyla işlendi!", + "Successfully restored {count} object" : [ + "{count} nesne başarıyla geri yüklendi", + "{count} nesne başarıyla geri yüklendi"], + "Successfully restored {count} objects" : "{count} nesne başarıyla geri yüklendi", + "Successfully saved configuration for {count} facets!" : "{count} faset için yapılandırma başarıyla kaydedildi!", + "Supported File Types" : "Desteklenen Dosya Türleri", + "Switch Active Organisation" : "Etkin Kuruluşu Değiştir", + "Switch Organisation" : "Kuruluş Değiştir", + "Sync Table" : "Tabloyu Senkronize Et", + "Synchronization Settings" : "Senkronizasyon Ayarları", + "Syncing magic table for {schema}..." : "{schema} için sihirli tablo senkronize ediliyor...", + "Syncs" : "Senkronizasyonlar", + "System Default" : "Sistem Varsayılanı", + "System Prompt" : "Sistem İstemi", + "System announcements" : "Sistem duyuruları", + "Table" : "Tablo", + "Target ID" : "Hedef Kimliği", + "Target Type*" : "Hedef Tür*", + "Technical Description" : "Teknik Açıklama", + "Technical Details" : "Teknik Ayrıntılar", + "Technical description for developers and administrators" : "Geliştiriciler ve yöneticiler için teknik açıklama", + "Technical measures" : "Teknik önlemler", + "Technische maatregelen" : "Technische maatregelen", + "Temperature" : "Sıcaklık", + "Templates" : "Şablonlar", + "Test" : "Test", + "Test Chat" : "Sohbeti Test Et", + "Test Connection" : "Bağlantıyı Test Et", + "Test Embedding" : "Gömülümü Test Et", + "Test endpoint executed successfully" : "Test uç noktası başarıyla yürütüldü", + "Test webhook delivered successfully" : "Test webhook'u başarıyla teslim edildi", + "Test webhook delivery failed" : "Test webhook teslimi başarısız", + "Test webhook sent successfully" : "Test webhook'u başarıyla gönderildi", + "Testing..." : "Test ediliyor...", + "Text Chunk #{id}" : "Metin Parçası #{id}", + "Text Extraction" : "Metin Çıkarma", + "Text Extraction Engine" : "Metin Çıkarma Motoru", + "Text chunks" : "Metin parçaları", + "Text extracted successfully" : "Metin başarıyla çıkarıldı", + "Text extraction disabled" : "Metin çıkarma devre dışı", + "The Nextcloud Calendar app is not installed or enabled on this server." : "Nextcloud Takvim uygulaması bu sunucuda yüklü değil veya etkin değil.", + "The Nextcloud Contacts app is not installed or enabled on this server." : "Nextcloud Kişiler uygulaması bu sunucuda yüklü değil veya etkin değil.", + "The Nextcloud Deck app is not installed or enabled on this server." : "Nextcloud Deck uygulaması bu sunucuda yüklü değil veya etkin değil.", + "The Nextcloud Mail app is not installed or enabled on this server." : "Nextcloud Mail uygulaması bu sunucuda yüklü değil veya etkin değil.", + "The URL where webhook events will be sent" : "Webhook etkinliklerinin gönderileceği URL", + "The requested conversation does not exist" : "İstenen konuşma mevcut değil", + "The saved view and all its search configuration" : "Kaydedilmiş görünüm ve tüm arama yapılandırması", + "There are no audit trail entries matching your current filters." : "Mevcut filtrelerinizle eşleşen denetim izi girişi yok.", + "There are no deleted items matching your current filters." : "Mevcut filtrelerinizle eşleşen silinmiş öğe yok.", + "There are no search trail entries matching your current filters." : "Mevcut filtrelerinizle eşleşen arama izi girişi yok.", + "There are no webhook log entries matching your filters." : "Filtrelerinizle eşleşen webhook günlük girişi yok.", + "This action cannot be undone." : "Bu işlem geri alınamaz.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Bu işlem geri alınamaz. Hiçbir koleksiyonun bu ConfigSet'i kullanmadığından emin olun.", + "This action cannot be undone. Make sure no collections are using this configset." : "Bu işlem geri alınamaz. Hiçbir koleksiyonun bu configset'i kullanmadığından emin olun.", + "This action will submit a deactivation request to your administrators." : "Bu işlem, yöneticilerinize bir devre dışı bırakma isteği gönderecek.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Bu ajanın yapılandırılabilir görünümü veya aracı yok, ancak aşağıdaki RAG ayarlarını yine de düzenleyebilirsiniz.", + "This analysis may take some time" : "Bu analiz biraz zaman alabilir", + "This audit trail entry does not contain any change information." : "Bu denetim izi girişi herhangi bir değişiklik bilgisi içermiyor.", + "This configuration has no data defined." : "Bu yapılandırmada tanımlanmış veri yok.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Bu uç nokta kullanım dışı. Bunun yerine parça tabanlı uç noktaları kullanın.", + "This entity has no relations to objects or files" : "Bu varlığın nesnelerle veya dosyalarla ilişkisi yok", + "This is a test webhook from OpenRegister" : "Bu OpenRegister'dan bir test webhook'udur", + "This is not valid JSON" : "Bu geçerli JSON değil", + "This key already exists on this schema" : "Bu anahtar bu şemada zaten mevcut", + "This schema must use Magic Table configuration to sync" : "Bu şemanın senkronize olabilmesi için Sihirli Tablo yapılandırması kullanması gerekir", + "This schema must use magic table configuration to sync" : "Bu şemanın senkronize olabilmesi için sihirli tablo yapılandırması kullanması gerekir", + "This source has no associated registers." : "Bu kaynağın ilişkili kaydı yok.", + "This token will only be shown once. Copy it now." : "Bu token yalnızca bir kez gösterilecek. Şimdi kopyalayın.", + "This will copy the _default ConfigSet with the new name" : "Bu, _default ConfigSet'ini yeni adla kopyalayacak", + "This will copy the _default configset with the new name" : "Bu, _default configset'ini yeni adla kopyalayacak", + "This will permanently delete:" : "Bu kalıcı olarak silecek:", + "Timeout (seconds)" : "Zaman Aşımı (saniye)", + "Timestamp" : "Zaman Damgası", + "Title" : "Başlık", + "Title *" : "Başlık *", + "Title Template" : "Başlık Şablonu", + "Title*" : "Başlık*", + "To (e.g. -1 year or 2025-12-31)" : "Bitiş (örn. -1 yıl veya 2025-12-31)", + "To Date" : "Bitiş Tarihi", + "To add files larger than or equal to 512MB, go to the" : "512 MB veya daha büyük dosyalar eklemek için gidin", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Parça boyutunu veya stratejisini ayarlamak için Dosya Yapılandırması → İşleme Sınırları bölümüne gidin.", + "To adjust chunk size or strategy, go to file configuration → processing limits." : "Parça boyutunu veya stratejisini ayarlamak için dosya yapılandırması → işleme sınırları bölümüne gidin.", + "To change the embedding provider or model, go to LLM Configuration." : "Gömülüm sağlayıcısını veya modelini değiştirmek için LLM Yapılandırması bölümüne gidin.", + "To change the embedding provider or model, go to LLM configuration." : "Gömülüm sağlayıcısını veya modelini değiştirmek için LLM yapılandırması bölümüne gidin.", + "To date" : "Bitiş tarihi", + "Toggle search sidebar" : "Arama kenar çubuğunu aç/kapat", + "Token Created" : "Token Oluşturuldu", + "Token Quota (per request)" : "Token Kotası (istek başına)", + "Token copied to clipboard" : "Token panoya kopyalandı", + "Token name" : "Token adı", + "Token revoked" : "Token iptal edildi", + "Tools" : "Araçlar", + "Tools respect the agent's own role and organization boundaries." : "Araçlar, ajanın kendi rolüne ve kuruluş sınırlarına saygı gösterir.", + "Tools respect the agent's views, permissions, and organization boundaries." : "Araçlar, ajanın görünümlerine, izinlerine ve kuruluş sınırlarına saygı gösterir.", + "Top Deleters" : "En Çok Silen Kullanıcılar", + "Total" : "Toplam", + "Total Audit Trails" : "Toplam Denetim İzleri", + "Total Chunks Available:" : "Toplam Mevcut Parçalar:", + "Total Deleted Items" : "Toplam Silinen Öğeler", + "Total Members" : "Toplam Üyeler", + "Total Objects" : "Toplam Nesneler", + "Total Objects in Database:" : "Veritabanındaki Toplam Nesneler:", + "Total Organisations" : "Toplam Kuruluşlar", + "Total Results" : "Toplam Sonuçlar", + "Total Searches" : "Toplam Aramalar", + "Total Size" : "Toplam Boyut", + "Total Storage" : "Toplam Depolama", + "Total events" : "Toplam etkinlikler", + "Total:" : "Toplam:", + "Totals" : "Toplamlar", + "Try searching by UUID or with different keywords" : "UUID ile veya farklı anahtar kelimelerle aramayı deneyin", + "Type" : "Tür", + "Type \"DELETE COLLECTION\" to confirm:" : "Onaylamak için \"DELETE COLLECTION\" yazın:", + "Type Issue" : "Tür Sorunu", + "Type Variations:" : "Tür Varyasyonları:", + "Type filter (optional, e.g. email)" : "Tür filtresi (isteğe bağlı, örn. e-posta)", + "Type must be \"positive\" or \"negative\"" : "Tür \"positive\" veya \"negative\" olmalıdır", + "Type to search for groups" : "Grupları aramak için yazın", + "Type to search for objects..." : "Nesneleri aramak için yazın...", + "Type to search for organisations" : "Kuruluşları aramak için yazın", + "Type to search for users" : "Kullanıcıları aramak için yazın", + "Type to search groups..." : "Grupları aramak için yazın...", + "Type to search users..." : "Kullanıcıları aramak için yazın...", + "Type to search..." : "Aramak için yazın...", + "Type your username to confirm" : "Onaylamak için kullanıcı adınızı yazın", + "URL" : "URL", + "URL where Ollama is running" : "Ollama'nın çalıştığı URL", + "UUID copied to clipboard" : "UUID panoya kopyalandı", + "UUID:" : "UUID:", + "Unchanged" : "Değişmedi", + "Unique Search Terms" : "Benzersiz Arama Terimleri", + "Unique Terms" : "Benzersiz Terimler", + "Unknown" : "Bilinmiyor", + "Unknown Object" : "Bilinmeyen Nesne", + "Unknown Register" : "Bilinmeyen Kayıt", + "Unknown Schema" : "Bilinmeyen Şema", + "Unknown error" : "Bilinmeyen hata", + "Unknown widget type: {type}" : "Bilinmeyen bileşen türü: {type}", + "Unlink email" : "E-posta bağlantısını kaldır", + "Unlink event" : "Etkinlik bağlantısını kaldır", + "Unnamed Object" : "Adsız Nesne", + "Untitled" : "Başlıksız", + "Untitled View" : "Başlıksız Görünüm", + "Update" : "Güncelle", + "Update Operations" : "Güncelleme İşlemleri", + "Update available: {local} → {remote}" : "Güncelleme mevcut: {local} → {remote}", + "Update configuration: ..." : "Yapılandırmayı güncelle: ...", + "Update register OAS: ..." : "Kayıt OAS'sini güncelle: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Nesne verileri değiştiğinde vektörleri güncelle (doğru arama için önerilir)", + "Updated" : "Güncellendi", + "Updated At" : "Güncellenme Zamanı", + "Updated:" : "Güncellendi:", + "Upload new avatar" : "Yeni avatar yükle", + "Uploading configuration sets..." : "Yapılandırma setleri yükleniyor...", + "Uptime:" : "Çalışma Süresi:", + "Url" : "Url", + "Usage Count" : "Kullanım Sayısı", + "Use AI agents for processing and analysis" : "İşleme ve analiz için yapay zeka ajanlarını kullanın", + "Use Large Language Model features" : "Büyük Dil Modeli özelliklerini kullanın", + "Use SolrCloud with Zookeeper for distributed search" : "Dağıtılmış arama için Zookeeper ile SolrCloud kullanın", + "Use SolrCloud with zookeeper for distributed search" : "Dağıtılmış arama için zookeeper ile SolrCloud kullanın", + "Use VNG APIs instead of MongoDB." : "MongoDB yerine VNG API'larını kullanın.", + "Use default ranges (Last 7/30/90 days, Last year, Older)" : "Varsayılan aralıkları kullanın (Son 7/30/90 gün, Geçen yıl, Daha eski)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage." : "Dahili Nextcloud depolaması yerine harici depolama (örn. MongoDB) kullanın.", + "Use filters to narrow down entities by type or category." : "Türe veya kategoriye göre varlıkları daraltmak için filtreler kullanın.", + "Used By Collections" : "Koleksiyonlar Tarafından Kullanılan", + "Used by" : "Kullanan", + "User" : "Kullanıcı", + "User Agent Statistics" : "Kullanıcı Ajanı İstatistikleri", + "User Description" : "Kullanıcı Açıklaması", + "User Quota" : "Kullanıcı Kotası", + "User rate limits cleared successfully" : "Kullanıcı hız sınırları başarıyla temizlendi", + "User removed successfully" : "Kullanıcı başarıyla kaldırıldı", + "User-friendly description shown in forms and help text" : "Formlarda ve yardım metninde gösterilen kullanıcı dostu açıklama", + "Username" : "Kullanıcı Adı", + "Username for SOLR authentication (optional)" : "SOLR kimlik doğrulaması için kullanıcı adı (isteğe bağlı)", + "Users" : "Kullanıcılar", + "Uses" : "Kullanımlar", + "Using Pre-Generated Chunks" : "Önceden Oluşturulmuş Parçaları Kullanma", + "Validate" : "Doğrula", + "Validate Objects" : "Nesneleri Doğrula", + "Validation completed for {schema}" : "{schema} için doğrulama tamamlandı", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed" : "{schema} için doğrulama tamamlandı: {processed} işlendi, {updated} güncellendi, {failed} başarısız", + "Validation completed successfully" : "Doğrulama başarıyla tamamlandı", + "Validation failed" : "Doğrulama başarısız", + "Value" : "Değer", + "Value list (split on ,)" : "Değer listesi (, ile bölünmüş)", + "Vector Dimensions" : "Vektör Boyutları", + "Vector Search Backend" : "Vektör Arama Backend'i", + "Vector field: _embedding_" : "Vektör alanı: _embedding_", + "Vectorization Triggers" : "Vektörleştirme Tetikleyicileri", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed" : "Vektörleştirme tamamlandı: {vectorized} nesne vektörleştirildi, {failed} başarısız", + "Vectorization failed: {error}" : "Vektörleştirme başarısız: {error}", + "Vectorize All Files" : "Tüm Dosyaları Vektörleştir", + "Vectorize All Objects" : "Tüm Nesneleri Vektörleştir", + "Vectorize all file types" : "Tüm dosya türlerini vektörleştir", + "Vectorize all views" : "Tüm görünümleri vektörleştir", + "Vectorize on object creation" : "Nesne oluşturulduğunda vektörleştir", + "Vectors will be stored in your existing object and file collections" : "Vektörler mevcut nesne ve dosya koleksiyonlarınızda depolanacak", + "Verantwoording" : "Verantwoording", + "Verifying server connectivity..." : "Sunucu bağlantısı doğrulanıyor...", + "Version" : "Sürüm", + "Version Information" : "Sürüm Bilgisi", + "Very High" : "Çok Yüksek", + "Very high" : "Çok yüksek", + "View" : "Görünüm", + "View API Docs" : "API Belgelerine Bak", + "View API Documentation" : "API Belgelerine Göz At", + "View APIs" : "API'lere Göz At", + "View Changes" : "Değişiklikleri Görüntüle", + "View Details" : "Ayrıntıları Görüntüle", + "View Error" : "Görünüm Hatası", + "View File" : "Dosyayı Görüntüle", + "View Full Details" : "Tüm Ayrıntıları Görüntüle", + "View Logs" : "Günlükleri Görüntüle", + "View Name" : "Görünüm Adı", + "View Object" : "Nesneyi Görüntüle", + "View Parameters" : "Görünüm Parametreleri", + "View Selection" : "Görünüm Seçimi", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Gelişmiş filtreleme ve analitik özellikleriyle arama izi günlüklerini görüntüleyin ve analiz edin", + "View and analyze system audit trails with advanced filtering capabilities" : "Gelişmiş filtreleme özellikleriyle sistem denetim izlerini görüntüleyin ve analiz edin", + "View and manage authorization across registers and schemas" : "Kayıtlar ve şemalar genelinde yetkilendirmeyi görüntüleyin ve yönetin", + "View deleted successfully" : "Görünüm başarıyla silindi", + "View entity details and manage relations" : "Varlık ayrıntılarını görüntüleyin ve ilişkileri yönetin", + "View name" : "Görünüm adı", + "View name is required" : "Görünüm adı gereklidir", + "View object" : "Nesneyi görüntüle", + "View saved successfully!" : "Görünüm başarıyla kaydedildi!", + "View search analytics and manage search logs" : "Arama analitiğini görüntüleyin ve arama günlüklerini yönetin", + "View successfully deleted" : "Görünüm başarıyla silindi", + "View successfully updated" : "Görünüm başarıyla güncellendi", + "View updated successfully!" : "Görünüm başarıyla güncellendi!", + "View webhook delivery logs and filter by webhook" : "Webhook teslim günlüklerini görüntüleyin ve webhook'a göre filtreleyin", + "View {count} more" : "{count} tanesini daha görüntüle", + "Views" : "Görünümler", + "Visible to end users" : "Son kullanıcılara görünür", + "Visible to users" : "Kullanıcılara görünür", + "Wait for Response" : "Yanıt Bekle", + "Wait for webhook response before continuing (required for request/response flows)" : "Devam etmeden önce webhook yanıtını bekleyin (istek/yanıt akışları için gerekli)", + "Waiting for cluster synchronization..." : "Küme senkronizasyonu bekleniyor...", + "Warmup Names Cache" : "İsimler Önbelleğini Isıt", + "Warmup failed" : "Isınma başarısız", + "Warning" : "Uyarı", + "Warning:" : "Uyarı:", + "Webhook" : "Webhook", + "Webhook Log Details" : "Webhook Günlük Ayrıntıları", + "Webhook Logs" : "Webhook Günlükleri", + "Webhook created successfully" : "Webhook başarıyla oluşturuldu", + "Webhook deleted" : "Webhook silindi", + "Webhook retry delivered successfully" : "Webhook yeniden denemesi başarıyla teslim edildi", + "Webhook retry delivery failed" : "Webhook yeniden deneme teslimi başarısız", + "Webhook updated" : "Webhook güncellendi", + "Webhook updated successfully" : "Webhook başarıyla güncellendi", + "Webhooks" : "Webhook'lar", + "Weekly" : "Haftalık", + "When enabled, saving this object will also update the target objects to include a reference back to this object." : "Etkinleştirildiğinde, bu nesneyi kaydetmek hedef nesneleri de bu nesneye geri atıfta bulunacak şekilde güncelleyecek.", + "When enabled, this property will be removed from the source object after updating the target objects." : "Etkinleştirildiğinde, bu özellik hedef nesneleri güncelledikten sonra kaynak nesneden kaldırılacak.", + "Workflow Management" : "İş Akışı Yönetimi", + "Workflows" : "İş Akışları", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Daha iyi birlikte çalışabilirlik için webhook yükünü CloudEvents biçiminde sarın", + "Wrap webhook payload in cloudevents format for better interoperability" : "Daha iyi birlikte çalışabilirlik için webhook yükünü cloudevents biçiminde sarın", + "X-Custom-Header: value\nAuthorization: Bearer token" : "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Evet", + "You" : "Siz", + "You are not yet a member of any organisations." : "Henüz hiçbir kuruluşun üyesi değilsiniz.", + "You do not have access to this conversation" : "Bu konuşmaya erişiminiz yok", + "You do not have permission to delete this conversation" : "Bu konuşmayı silme izniniz yok", + "You do not have permission to modify this conversation" : "Bu konuşmayı değiştirme izniniz yok", + "You do not have permission to restore this conversation" : "Bu konuşmayı geri yükleme izniniz yok", + "You do not have permission to view the permission matrix. Admin access is required." : "İzin matrisini görüntüleme izniniz yok. Yönetici erişimi gereklidir.", + "You have selected {count} properties to add to the schema." : "Şemaya eklemek için {count} özellik seçtiniz.", + "You must be logged in to favorite views" : "Görünümleri favorilere eklemek için giriş yapmış olmanız gerekir", + "You need an AI agent to start a conversation." : "Konuşma başlatmak için bir yapay zeka ajanına ihtiyacınız var.", + "Your Fireworks AI API key. Get one at" : "Fireworks AI API anahtarınız. Bir tane alın:", + "Your OpenAI API key. Get one at" : "OpenAI API anahtarınız. Bir tane alın:", + "Zookeeper Hosts" : "Zookeeper Ana Bilgisayarları", + "Zookeeper Port" : "Zookeeper Port", + "Zookeeper Settings (SolrCloud)" : "Zookeeper Ayarları (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "SolrCloud için Zookeeper bağlantı dizesi", + "Zookeeper port number (optional, defaults to 2181)" : "Zookeeper port numarası (isteğe bağlı, varsayılan 2181)", + "and add the files there." : "ve dosyaları oraya ekleyin.", + "approved" : "onaylandı", + "are only accessible to invited users." : "yalnızca davet edilen kullanıcılara erişilebilir.", + "by" : "tarafından", + "chunk" : "parça", + "chunks" : "parçalar", + "chunks processed" : "parça işlendi", + "completed" : "tamamlandı", + "configuration(s)" : "yapılandırma(lar)", + "conversationId is required" : "conversationId gereklidir", + "create" : "oluştur", + "delete" : "sil", + "deleted objects section" : "silinen nesneler bölümü", + "e.g., lib/Settings/config.json" : "örn. lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "örn. lib/Settings/register.json", + "e.g., lib/settings/config.json" : "örn. lib/settings/config.json", + "e.g., lib/settings/register.json" : "örn. lib/settings/register.json", + "execute" : "yürüt", + "failed" : "başarısız", + "file" : "dosya", + "files" : "dosyalar", + "file{plural}" : "dosya{plural}", + "folder" : "klasör", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com" : "https://api.your-dolphin-instance.com", + "https://example.com/webhook" : "https://example.com/webhook", + "in {register}" : "{register} içinde", + "in_progress" : "devam ediyor", + "inherited" : "devralındı", + "list" : "listele", + "log{plural}" : "günlük{plural}", + "manage" : "yönet", + "members" : "üyeler", + "menu or contact someone with permission to create agents." : "menüsü veya ajan oluşturma iznine sahip biriyle iletişime geçin.", + "message content is required" : "mesaj içeriği gereklidir", + "messages" : "mesajlar", + "missing" : "eksik", + "myapp" : "myapp", + "n8n URL and API key are required" : "n8n URL ve API anahtarı gereklidir", + "n8n configuration saved successfully" : "n8n yapılandırması başarıyla kaydedildi", + "n8n connection not configured" : "n8n bağlantısı yapılandırılmamış", + "n8n connection successful" : "n8n bağlantısı başarılı", + "n8n connection test failed: {message}" : "n8n bağlantı testi başarısız: {message}", + "n8n connection test successful" : "n8n bağlantı testi başarılı", + "n8n initialization failed: {message}" : "n8n başlatma başarısız: {message}", + "n8n integration disabled" : "n8n entegrasyonu devre dışı", + "n8n integration enabled" : "n8n entegrasyonu etkin", + "n8n project initialized successfully" : "n8n projesi başarıyla başlatıldı", + "n8n settings saved successfully" : "n8n ayarları başarıyla kaydedildi", + "notify_push configuration guide" : "notify_push yapılandırma kılavuzu", + "notify_push is installed but not yet active" : "notify_push yüklü ancak henüz etkin değil", + "objectType: object\naction: created" : "objectType: object\naction: created", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "nesneler", + "objects processed" : "nesne işlendi", + "object{plural}" : "nesne{plural}", + "or pick a range" : "veya bir aralık seçin", + "org-..." : "org-...", + "pending" : "beklemede", + "provides a good balance between speed and accuracy." : "hız ve doğruluk arasında iyi bir denge sağlar.", + "public" : "genel", + "read" : "oku", + "register(s)" : "kayıt(lar)", + "register(s) selected" : "kayıt(lar) seçildi", + "register{plural}" : "kayıt{plural}", + "rejected" : "reddedildi", + "required" : "gerekli", + "results" : "sonuçlar", + "schema(s)" : "şema(lar)", + "schema(s) selected" : "şema(lar) seçildi", + "schema{plural}" : "şema{plural}", + "selected objects" : "seçilen nesneler", + "set" : "ayarla", + "sk-..." : "sk-...", + "this application" : "bu uygulama", + "to" : "için", + "type must be \"positive\" or \"negative\"" : "tür \"positive\" veya \"negative\" olmalıdır", + "update" : "güncelle", + "vectorized" : "vektörleştirildi", + "view" : "görüntüle", + "views selected" : "görünümler seçildi", + "waiting" : "bekliyor", + "{active} active, {deleted} deleted" : "{active} etkin, {deleted} silindi", + "{count} audit trails cleared successfully" : "{count} denetim izi başarıyla temizlendi", + "{count} compliance issue(s) detected" : "{count} uyumluluk sorunu tespit edildi", + "{count} deletions" : "{count} silme", + "{count} documents found" : "{count} belge bulundu", + "{count} email" : [ + "{count} e-posta", + "{count} e-posta"], + "{count} emails" : "{count} e-posta", + "{count} entries" : "{count} giriş", + "{count} issues" : "{count} sorun", + "{count} members" : "{count} üye", + "{count} searches" : "{count} arama", + "{count} selected" : "{count} seçildi", + "{count} widget(s)" : "{count} bileşen", + "{matched} object(s) matched. {erased} erased." : "{matched} nesne eşleşti. {erased} silindi.", + "{minutes} minutes ago" : "{minutes} dakika önce", + "{percentage}% of objects" : "Nesnelerin %{percentage}", + "{property} - {other}" : "{property} - {other}", + "{title} in {register} / {schema}" : "{register} / {schema} içinde {title}", + "{type} cache cleared successfully" : "{type} önbelleği başarıyla temizlendi", + "ℹ️ Current Configuration" : "ℹ️ Mevcut Yapılandırma", + "ℹ️ current configuration" : "ℹ️ mevcut yapılandırma", + "⚡ Batch Processing" : "⚡ Toplu İşleme", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Daha hızlı yanıtlar ✓ Daha odaklı cevaplar ✗ İlgili bilgileri kaçırabilir", + "✨ AI Features" : "✨ Yapay Zeka Özellikleri", + "💰 View Selection (Cost Optimization)" : "💰 Görünüm Seçimi (Maliyet Optimizasyonu)", + "📄 Object Serialization" : "📄 Nesne Serileştirme", + "🔢 Vectorization Settings" : "🔢 Vektörleştirme Ayarları", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review." : "AVG Madde 30 §4: Bu rapor, her işleme faaliyetini kendisine atfedilen ömür boyu denetim izi sayılarıyla birleştirir. Denetçiler ve Autoriteit Persoonsgegevens bunu denetim incelemesi için kullanır.", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Arama için daha zengin bağlam sağlamak amacıyla şema başlıkları, açıklamalar ve kayıt bilgileri ekleyin", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Veritabanı kaynağı kullanılırken gelişmiş filtreler mevcut değil. Filtreleme seçenekleri için Otomatik veya SOLR Dizini'ne geçin.", + "Apply a role to all schemas in this register that do not have explicit authorization overrides." : "Bu kayıttaki açık yetkilendirme geçersiz kılmaları olmayan tüm şemalara rol atayın.", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone" : "\"{name}\" koleksiyonunu SİLMEK istediğinizden emin misiniz?\n\nBu işlem:\n• Koleksiyonu ve tüm verilerini kalıcı olarak siler\n• Tüm dizinlenmiş belgeleri kaldırır\n• Bu işlem geri alınamaz", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Eski arama izlerini temizlemek istediğinizden emin misiniz? Bu, 30 günden eski girişleri siler.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone" : "\"{name}\" koleksiyonundan tüm verileri temizlemek istediğinizden emin misiniz?\n\nBu işlem:\n• Tüm dizinlenmiş belgeleri siler\n• Koleksiyon yapısını bozulmadan tutar\n• Bu işlem geri alınamaz", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing" : "\"{name}\" koleksiyonunu yeniden dizinlemek istediğinizden emin misiniz?\n\nBu işlem:\n• Tüm nesnelerle dizini yeniden oluşturur\n• Tamamlanması birkaç dakika sürebilir\n• Yeniden dizinleme sırasında arama performansını etkileyebilir", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone." : "\"{schema}\" şemasını \"{register}\" kaydından kaldırmak istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later." : "\"{schema}\" şeması için {count} etkin nesneyi geçici silmek istediğinizden emin misiniz?\n\nBunlar silinmiş olarak işaretlenecek ancak daha sonra kalıcı olarak kaldırılabilecek.", + "Automatically create a default organisation if none exists when the app is initialized" : "Uygulama başlatıldığında hiç kuruluş yoksa varsayılan bir kuruluş otomatik olarak oluştur", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Dosyalar yüklendiğinde ve işlendiğinde metin parçalarından vektör gömülümleri otomatik olarak oluştur", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists." : "KBB tespit edilmiş ancak işleme faaliyeti ek açıklaması bulunmayan şemaları bulmak için uyumluluk taramasını çalıştırın.", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Karmaşık sorgular: Birden fazla filtre, operatör ve karmaşık parametre kombinasyonlu gelişmiş aramalar", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations" : "Karmaşık sorgular: birden fazla filtre, operatör ve karmaşık parametre kombinasyonlu gelişmiş aramalar", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "API maliyetlerini azaltmak için hangi nesne görünümlerinin vektörleştirilmesi gerektiğini kontrol edin. Yalnızca anlamsal aramadan yararlanan görünümleri vektörleştirin.", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "LLM özelliklerini etkinleştirin veya devre dışı bırakın. Yukarıdaki LLM Yapılandırma düğmesini kullanarak sağlayıcıları ve modelleri yapılandırın.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above." : "LLM özelliklerini etkinleştirin veya devre dışı bırakın. Yukarıdaki LLM yapılandırma düğmesini kullanarak sağlayıcıları ve modelleri yapılandırın.", + "Enable tools that allow the agent to interact with data through function calling." : "Ajanın işlev çağrısı aracılığıyla verilerle etkileşime girmesine izin veren araçları etkinleştirin.", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed." : "Dosya vektörleştirme tamamlandı! {files} dosyasından {vectorized} parça vektörleştirildi. {failed} başarısız.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics." : "Kuruluşunuzun veri sağlığı, büyüme eğilimleri ve kalite metrikleri hakkında gerçek zamanlı içgörüler edinin.", + "New users without specific organisation membership will be automatically added to this organisation" : "Belirli bir kuruluş üyeliği olmayan yeni kullanıcılar otomatik olarak bu kuruluşa eklenecek", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects." : "{schema} şeması için geçici silinecek etkin nesne yok. Geçici silinmiş nesneleri kaldırmak için \"Kalıcı Sil\" kullanın.", + "No facetable fields available. Select a register and schema to see available filters." : "Fasetlenebilir alan mevcut değil. Mevcut filtreleri görmek için bir kayıt ve şema seçin.", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Bir API çağrısında vektörleştirilecek parça sayısı. Yüksek = daha hızlı ama daha fazla bellek. Önerilen: 10-50.", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Bir API çağrısında vektörleştirilecek nesne sayısı. Yüksek = daha hızlı ama daha fazla bellek. Önerilen: 10-50.", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)" : "İsteğe bağlı: Başvurulan şemayı filtrelemek için sorgu parametreleri ekleyin (örn. status=active&type=public)", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Gerçek zamanlı anlık bildirimler etkin. Nesneler oluşturulduğunda, güncellendiğinde veya silindiğinde bağlı istemciler anlık güncellemeler alır.", + "Review the selected objects below. You can remove any objects you don't want to include." : "Aşağıda seçilen nesneleri inceleyin. Dahil etmek istemediğiniz nesneleri kaldırabilirsiniz.", + "Select an organisation and user to add them as a member. Search for organisations by name." : "Üye olarak eklemek için bir kuruluş ve kullanıcı seçin. Kuruluşları ada göre arayın.", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Basit sorgular: Minimum parametre ile temel metin aramaları (örn. tek arama terimi, gelişmiş filtre yok)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Basit sorgular: minimum parametre ile temel metin aramaları (örn. tek arama terimi, gelişmiş filtre yok)", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list." : "İstenen gösterge paneli yüklenemedi. URL'yi doğrulayın veya listeden başka bir gösterge paneli seçin.", + "They will be retained according to their schema's configured retention period and then permanently removed." : "Bunlar, şemalarının yapılandırılmış saklama süresine göre tutulacak ve ardından kalıcı olarak kaldırılacak.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Kayıt, şema, silme tarihi veya silme yapan kullanıcıya göre silinmiş öğeleri daraltmak için filtreler kullanın.", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type." : "Uyarı: Bu özelliğin tutarsız türleri var: {types}. Tüm nesnelerin aynı türe sahip olması gerekip gerekmediğini değerlendirin.", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used" : "Ajan bir kullanıcı oturumu olmadan çalıştığında (örn. zamanlanmış görevler), bu kullanıcının bağlamı kullanılacak", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used." : "Ajan bir kullanıcı oturumu olmadan çalıştığında (örn. zamanlanmış görevler), bu kullanıcının bağlamı kullanılacak.", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Geri bildiriminiz kaydedildi. İsteğe bağlı olarak burada ek ayrıntılar sağlayabilirsiniz...", + "Your feedback has been recorded. optionally, you can provide additional details here..." : "Geri bildiriminiz kaydedildi. İsteğe bağlı olarak burada ek ayrıntılar sağlayabilirsiniz...", + "and accessible to all users in selected groups (or all users if no groups selected)." : "ve seçilen gruplardaki tüm kullanıcılara erişilebilir (grup seçilmezse tüm kullanıcılara).", + "⚡ SOLR can handle millions of documents with sub-second search response times..." : "⚡ SOLR, saniyenin altında arama yanıt süreleriyle milyonlarca belgeyi işleyebilir...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Kapsamlı bağlam ✓ Ayrıntıları kaçırma olasılığı daha az ✗ Daha yavaş yanıtlar ✗ Daha az alakalı bilgi içerebilir", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination..." : "🌐 Dağıtılmış modda SOLR, küme koordinasyonu için ZooKeeper kullanır...", + "🎯 Faceted search allows users to drill down into results by categories..." : "🎯 Fasetli arama, kullanıcıların sonuçları kategorilere göre derinlemesine incelemesine olanak tanır...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!" : "💡 Biliyor muydunuz? SOLR, Netflix, Apple ve diğer büyük siteler için aramayı güçlendiriyor!", + "📊 SOLR provides rich analytics and statistics about search performance..." : "📊 SOLR, arama performansı hakkında zengin analitik ve istatistikler sağlar...", + "📦 ConfigSets contain the schema and configuration files for your search index..." : "📦 ConfigSet'ler, arama dizininizin şemasını ve yapılandırma dosyalarını içerir...", + "🔄 Replication ensures your search index is available even if nodes fail..." : "🔄 Replikasyon, düğümler başarısız olsa bile arama dizininizin kullanılabilir olmasını sağlar...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene..." : "🔍 SOLR, Apache Lucene üzerine inşa edilmiş güçlü bir kurumsal arama platformudur...", + "🚀 Auto-scaling can dynamically add or remove nodes based on load..." : "🚀 Otomatik ölçeklendirme, yüke göre dinamik olarak düğüm ekleyebilir veya kaldırabilir...", + "🛡️ Security features include authentication, authorization, and SSL encryption..." : "🛡️ Güvenlik özellikleri kimlik doğrulama, yetkilendirme ve SSL şifrelemeyi içerir...", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Vektörleştirme sürecine hangi dosya türlerinin dahil edileceğini seçin. Yalnızca çıkarılmış metin ve parçalara sahip dosyalar işlenecek.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Vektörleştirme sürecine hangi görünümlerin dahil edileceğini seçin. Yapılandırmanıza göre tüm görünümleri işlemek için boş bırakın.", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSet'ler, SOLR koleksiyonlarınızın şemasını ve yapılandırmasını tanımlar. Alan tanımları, analizörler ve diğer arama ayarlarını içerirler.", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Anlamsal arama, gömülümler ve sohbet dahil yapay zeka destekli özellikler için Büyük Dil Modeli (LLM) sağlayıcılarını yapılandırın.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Kimlik doğrulama ve ağ seçenekleri dahil SOLR sunucunuz için temel bağlantı ayarlarını yapılandırın. Çekirdekleri ve koleksiyonları yönetmek için ayrı ConfigSet ve Koleksiyon Yönetimi iletişim kutularını kullanın.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections." : "Kimlik doğrulama ve ağ seçenekleri dahil SOLR sunucunuz için temel bağlantı ayarlarını yapılandırın. Çekirdekleri ve koleksiyonları yönetmek için ayrı configset ve koleksiyon yönetimi iletişim kutularını kullanın.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Anlamsal arama için veritabanı nesnelerinin vektör gömülümlerine nasıl dönüştürüleceğini yapılandırın. Nesneler, metin çıkarmaya gerek kalmadan doğrudan vektörleştirilir.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Vektörleştirmeden önce nesnelerin metne nasıl dönüştürüleceğini yapılandırın. Bu ayarlar arama kalitesini ve bağlamı etkiler.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat." : "Anlamsal arama, gömülümler ve sohbet dahil yapay zeka destekli özellikler için büyük dil modeli (LLM) sağlayıcılarını yapılandırın.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Dosya vektörleştirmesi için parametreleri yapılandırın. Bu işlem, tüm çıkarılan dosya parçaları için vektör gömülümleri oluşturur.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Nesne vektörleştirmesi için parametreleri yapılandırın. Bu işlem, görünüm filtrelerinizle eşleşen tüm nesneler için vektör gömülümleri oluşturur.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Yapay zekanın bu konuşmada hangi görünümleri ve araçları kullanabileceğini kontrol edin. Varsayılan olarak tüm ajan özellikleri etkindir.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution." : "Denetim izi satırlarını AVG Madde 30 atıflarıyla etiketlemeye başlamak için ilk verwerkingsactiviteit'i oluşturun.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "SOLR arama entegrasyonunu etkinleştirin veya devre dışı bırakın. Yukarıdaki Bağlantı Ayarları düğmesini kullanarak bağlantı ayarlarını yapılandırın.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above." : "SOLR arama entegrasyonunu etkinleştirin veya devre dışı bırakın. Yukarıdaki bağlantı ayarları düğmesini kullanarak bağlantı ayarlarını yapılandırın.", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Dosya parça vektörleştirme henüz uygulanmadı. Parçalar hazır ve depolandı, ancak vektörleştirme servisi geliştirme aşamasındadır.", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage." : "Kayıtlara, şema tanımlarına ve nesne depolaması ile kullanımına odaklanarak kuruluşunuzun veri sağlığı hakkında gerçek zamanlı içgörüler edinin.", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "İç içe geçmiş nesne özelliklerini ne kadar derinliğe kadar gezileceği (1-20). Daha yüksek değerler daha fazla ayrıntı yakalar ancak vektör boyutunu artırır.", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data." : "`reports` kaydını almak için report-bundle.json şablonunu içe aktarın, ardından standart nesne arayüzü aracılığıyla ilk gösterge panelinizi oluşturun. Gösterge panelleri widget'larını JSON'da bildirir ve işleyici her widget'a canlı toplama verisi besler.", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit)." : "Bir veri öznesiyle başvuran her nesneyi bulun (Madde 15 inzage), silme işlemini önizleyin (Madde 17 vergetelheid) veya verilerini dışa aktarın (Madde 20 portabiliteit).", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "OpenRegister otomasyonu için n8n iş akışlarını yönetin. İş akışları yapılandırılmış projede depolanacak.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG." : "İşleme faaliyetlerini yönetin, veri sahibi erişim isteklerini çalıştırın ve AB GDPR / Hollanda AVG kapsamında uyumluluğu denetleyin.", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Orta sorgular: Bazı filtreleme veya birden fazla parametre içeren aramalar (örn. tarih aralıkları, belirli kayıtlar/şemalar)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Orta sorgular: bazı filtreleme veya birden fazla parametre içeren aramalar (örn. tarih aralıkları, belirli kayıtlar/şemalar)", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Nesneler geçici silinecek (silinmiş olarak işaretlenecek ancak veritabanında tutulacak). Gerekirse daha sonra kurtarılabilirler.", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL." : "Operatör tanımlı gösterge panelleri ve zamanlanmış raporlar. Her gösterge paneli `reports` kaydında birinci sınıf bir nesnedir; widget'lar gösterge panelinin `widgets` dizisinde bildirilir ve toplama/GraphQL'den canlı olarak işlenir.", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties." : "Sıralı BCP 47 dil etiketleri. İlk dil, kayıt varsayılanı olup çevrilebilir özellikler için Accept-Language geri dönüşünü yönlendirir.", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button." : "Aşağıda seçilen nesneleri inceleyin. Kaldır düğmesine tıklayarak silmek istemediğiniz nesneleri kaldırabilirsiniz.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone." : "Aşağıda seçilen nesneleri inceleyin. Kaldır düğmesine tıklayarak kalıcı olarak silmek istemediğiniz nesneleri kaldırabilirsiniz. Bu işlem geri alınamaz.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location." : "Aşağıda seçilen nesneleri inceleyin. Kaldır düğmesine tıklayarak geri yüklemek istemediğiniz nesneleri kaldırabilirsiniz. Nesneler orijinal konumlarına geri yüklenecek.", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Bu kuruluş için hangi Nextcloud gruplarının mevcut olduğunu seçin. Bu gruplardaki kullanıcılar kuruluş kaynaklarına erişime sahip olacak.", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Metin parçaları dosya çıkarma sırasında oluşturulur ve veritabanında depolanır. Vektörleştirme, önceden parçalanmış bu dosyaları okur ve gömülümlere dönüştürür.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings." : "Bunlar, şemalarının yapılandırılmış saklama süresine göre tutulacak ve saklama süresi dolduğunda otomatik olarak kalıcı biçimde silinecek. Saklama süresi şema bazında yapılandırılabilir ve şemanın ayarlarında bulunabilir.", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?" : "Bu, veritabanından TÜM gömülümleri (vektörleri) kalıcı olarak siler. Tüm nesneleri ve dosyaları yeniden vektörleştirmeniz gerekecek. Bu işlem geri alınamaz.\n\nDevam etmek istediğinizden emin misiniz?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Bu, veritabanından TÜM gömülümleri (vektörleri) kalıcı olarak siler. Tüm nesneleri ve dosyaları yeniden vektörleştirmeniz gerekecek. Bu işlem geri alınamaz.\\n\\nDevam etmek istediğinizden emin misiniz?", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone." : "Bu, veritabanından {count} zaten geçici silinmiş nesneyi kalıcı olarak kaldıracak. Bu işlem geri alınamaz.", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "İpucu: Gömülüm maliyetlerini en aza indirmek için yalnızca anlamsal arama gerektiren görünümleri etkinleştirin. Basit arama tabloları nadiren vektörleştirmeye ihtiyaç duyar.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "İpucu: gömülüm maliyetlerini en aza indirmek için yalnızca anlamsal arama gerektiren görünümleri etkinleştirin. Basit arama tabloları nadiren vektörleştirmeye ihtiyaç duyar.", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab." : "Hiçbir kullanıcı oturumu etkin olmadığında araçlar ajanın varsayılan kullanıcı izinleriyle yürütülür (örn. cron işleri). Varsayılan kullanıcıyı Ayarlar sekmesinde yapılandırın.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Kayıt, şema, eylem türü, kullanıcı, tarih aralığı veya nesne kimliğine göre denetim izi girişlerini daraltmak için filtreler kullanın.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Kayıt, şema, başarı durumu, kullanıcı, tarih aralığı, arama terimleri veya performans metriklerine göre arama izi girişlerini daraltmak için filtreler kullanın.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules." : "Yeni özellikleri keşfetmek ve mevcut özellikleri potansiyel iyileştirmeler için analiz etmek amacıyla bu şemaya ait tüm nesneleri tarayacağız. Süreç, her nesnenin veri yapısını incelemeyi, mevcut şemada tanımlanmamış özellikleri belirlemeyi ve daha iyi kısıtlamalar, biçimler ve doğrulama kurallarıyla mevcut özellik tanımlarını iyileştirme fırsatları bulmayı içerir.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected." : "Devre dışı bırakıldığında, bu faset yalnızca bu şemadan değerleri gösterecek ve seçildiğinde bir şema filtresi içerecek.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "_default şablonuna dayalı yeni ConfigSet'ler oluşturabilir veya doğrudan SOLR sunucunuza özel olanları yükleyebilirsiniz.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server." : "_default şablonuna dayalı yeni configset'ler oluşturabilir veya doğrudan SOLR sunucunuza özel olanları yükleyebilirsiniz.", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?" : "\\u26A0\\uFE0F KALICI SİLME UYARISI \\u26A0\\uFE0F\n\n\"{schema}\" şeması için TÜM nesneleri KALICI OLARAK silmek üzeresiniz:\n\n\\u2022 Etkin nesneler: {active}\n\\u2022 Geçici silinmiş nesneler: {deleted}\n\\u2022 Toplam: {total}\n\nBu nesneler veritabanından tamamen kaldırılacak ve KURTARILAMAZ.\n\nKesinlikle emin misiniz?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?" : "\\u26A0\\uFE0F KALICI SİLME UYARISI \\u26A0\\uFE0F\n\n\"{schema}\" şeması için {count} geçici silinmiş nesneyi KALICI OLARAK silmek üzeresiniz.\n\nBu nesneler veritabanından tamamen kaldırılacak ve KURTARILAMAZ.\n\nKesinlikle emin misiniz?", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push yüklü, ancak OpenRegister henüz başarılı bir anlık bildirim onaylamadı. Etkinleştirmek için nesne kaydı tetikleyin veya notify_push yapılandırmanızı kontrol edin.", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!" : "⚠️ TEHLİKE: Tüm {total} nesne veritabanından KALICI OLARAK SİLİNECEK. Bu işlem GERİ DÖNDÜRÜLEMEZ ve geri alınamaz!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?" : "⚠️ KALICI SİLME UYARISI ⚠️\n\n\"{schema}\" şeması için TÜM nesneleri KALICI OLARAK silmek üzeresiniz:\n\n• Etkin nesneler: {active}\n• Geçici silinmiş nesneler: {deleted}\n• Toplam: {total}\n\nBu nesneler veritabanından tamamen kaldırılacak ve KURTARILAMAZ.\n\nKesinlikle emin misiniz?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?" : "⚠️ KALICI SİLME UYARISI ⚠️\n\n\"{schema}\" şeması için {count} geçici silinmiş nesneyi KALICI OLARAK silmek üzeresiniz.\n\nBu nesneler veritabanından tamamen kaldırılacak ve KURTARILAMAZ.\n\nKesinlikle emin misiniz?", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=2; plural=(n != 1);" +); diff --git a/l10n/tr.json b/l10n/tr.json new file mode 100644 index 0000000000..11e4c4751d --- /dev/null +++ b/l10n/tr.json @@ -0,0 +1,2664 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(Dolphin gerekli)", + "(no subject)": "(konu yok)", + "(no title)": "(başlık yok)", + "(unnamed)": "(adsız)", + "(untitled card)": "(başlıksız kart)", + "({count} selected)": "({count} seçildi)", + ", {failed} failed": ", {failed} başarısız", + "0 = unlimited": "0 = sınırsız", + "A deactivation request is pending.": "Bir devre dışı bırakma isteği beklemede.", + "AI Assistant": "Yapay Zeka Asistanı", + "AI Chat": "Yapay Zeka Sohbeti", + "AI service not configured": "Yapay zeka servisi yapılandırılmamış", + "API Key": "API Anahtarı", + "API Request Quota (requests/day)": "API İstek Kotası (istek/gün)", + "API Token Configuration": "API Token Yapılandırması", + "API Tokens": "API Token'ları", + "API tokens saved successfully": "API token'ları başarıyla kaydedildi", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "About ConfigSets": "ConfigSet'ler Hakkında", + "Access denied": "Erişim reddedildi", + "Access organisation dashboard and analytics": "Kuruluş panosuna ve analizlerine erişin", + "Account": "Hesap", + "Action": "Eylem", + "Action Distribution": "Eylem Dağılımı", + "Action:": "Eylem:", + "Actions": "Eylemler", + "Active": "Etkin", + "Active Collections": "Etkin Koleksiyonlar", + "Active Filters": "Etkin Filtreler", + "Active Organisation:": "Etkin Kuruluş:", + "Active Organisations": "Etkin Kuruluşlar", + "Active filters:": "Etkin filtreler:", + "Active organisation changed successfully": "Etkin kuruluş başarıyla değiştirildi", + "Active organisation set successfully": "Etkin kuruluş başarıyla ayarlandı", + "Activities": "Etkinlikler", + "Activity": "Etkinlik", + "Activity Period": "Etkinlik Dönemi", + "Add": "Ekle", + "Add Attachment": "Ek Ekle", + "Add Endpoint": "Uç Nokta Ekle", + "Add Groups": "Grup Ekle", + "Add Property to '{title}'": "'{title}' Öğesine Özellik Ekle", + "Add Register": "Kayıt Ekle", + "Add Schema": "Şema Ekle", + "Add Source": "Kaynak Ekle", + "Add User": "Kullanıcı Ekle", + "Add User to Organisation": "Kuruluşa Kullanıcı Ekle", + "Add a contact from any of your address books to associate it with this object.": "Bu nesneyle ilişkilendirmek için adres defterlerinizden herhangi birinden kişi ekleyin.", + "Add a file or files": "Dosya veya dosyalar ekle", + "Add card": "Kart ekle", + "Add contact": "Kişi ekle", + "Add endpoint": "Uç nokta ekle", + "Add oneOf entry": "oneOf girişi ekle", + "Add range": "Aralık ekle", + "Add to favorites": "Favorilere ekle", + "Added": "Eklendi", + "Added to favorites": "Favorilere eklendi", + "Additional Information": "Ek Bilgi", + "Additional feedback saved. Thank you!": "Ek geri bildirim kaydedildi. Teşekkürler!", + "Addresses": "Adresler", + "Admin": "Yönetici", + "Advanced": "Gelişmiş", + "Advanced Filters": "Gelişmiş Filtreler", + "Advanced Options": "Gelişmiş Seçenekler", + "After": "Sonra", + "Agent Type": "Ajan Türü", + "Agent deleted successfully": "Ajan başarıyla silindi", + "Agent successfully created": "Ajan başarıyla oluşturuldu", + "Agent successfully updated": "Ajan başarıyla güncellendi", + "Agents": "Ajanlar", + "Aggregated across schemas": "Şemalar genelinde toplanmış", + "All": "Tümü", + "All Categories": "Tüm Kategoriler", + "All Collections": "Tüm Koleksiyonlar", + "All Confidence Levels": "Tüm Güven Seviyeleri", + "All ConfigSets": "Tüm ConfigSet'ler", + "All Files": "Tüm Dosyalar", + "All Levels": "Tüm Seviyeler", + "All Types": "Tüm Türler", + "All Webhooks": "Tüm Webhook'lar", + "All actions": "Tüm eylemler", + "All audit trails cleared successfully": "Tüm denetim izleri başarıyla temizlendi", + "All clear": "Her şey tamam", + "All file types": "Tüm dosya türleri", + "All registers": "Tüm kayıtlar", + "All schemas": "Tüm şemalar", + "All search trails cleared successfully": "Tüm arama izleri başarıyla temizlendi", + "All searches": "Tüm aramalar", + "All users": "Tüm kullanıcılar", + "All-day events": "Tüm gün etkinlikleri", + "Allowed MIME Types": "İzin Verilen MIME Türleri", + "Already linked": "Zaten bağlantılı", + "An error occurred while clearing audit trails": "Denetim izleri temizlenirken bir hata oluştu", + "An error occurred while deleting the audit trail": "Denetim izi silinirken bir hata oluştu", + "An error occurred while deleting the object": "Nesne silinirken bir hata oluştu", + "An error occurred while deleting the view": "Görünüm silinirken bir hata oluştu", + "An error occurred while permanently deleting the objects": "Nesneler kalıcı olarak silinirken bir hata oluştu", + "An error occurred while restoring the objects": "Nesneler geri yüklenirken bir hata oluştu", + "An error occurred while saving the organisation": "Kuruluş kaydedilirken bir hata oluştu", + "Analysis completed:": "Analiz tamamlandı:", + "Analysis steps:": "Analiz adımları:", + "Analytics": "Analitik", + "Analyze Objects": "Nesneleri Analiz Et", + "Analyze Properties": "Özellikleri Analiz Et", + "Analyze existing properties for improvement opportunities": "İyileştirme fırsatları için mevcut özellikleri analiz et", + "Analyzing...": "Analiz ediliyor...", + "Anonymized": "Anonimleştirilmiş", + "Anonymous Group": "Anonim Grup", + "Any favorites and sharing settings for this view": "Bu görünüm için herhangi bir favori ve paylaşım ayarı", + "Any user": "Herhangi bir kullanıcı", + "App ID": "Uygulama Kimliği", + "App store cache invalidated successfully": "Uygulama mağazası önbelleği başarıyla geçersiz kılındı", + "App store cache invalidated: {invalidated}": "Uygulama mağazası önbelleği geçersiz kılındı: {invalidated}", + "Application deleted successfully": "Uygulama başarıyla silindi", + "Application identifier for this configuration (optional)": "Bu yapılandırma için uygulama tanımlayıcısı (isteğe bağlı)", + "Application settings": "Uygulama ayarları", + "Application successfully created": "Uygulama başarıyla oluşturuldu", + "Application successfully updated": "Uygulama başarıyla güncellendi", + "Applications": "Uygulamalar", + "Apply": "Uygula", + "Apply Changes": "Değişiklikleri Uygula", + "Approval Progress": "Onay İlerlemesi", + "Approve": "Onayla", + "Approved": "Onaylandı", + "Archive": "Arşiv", + "Archive conversation": "Konuşmayı arşivle", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Bu verwerkingsactiviteit arşivlensin mi? Denetim izi satırları buna atıfta bulunmaya devam edecek.", + "Archived conversations are hidden from your active list": "Arşivlenen konuşmalar etkin listenizden gizlenir", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "\"{name}\" uygulamasını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Seçilen denetim izlerini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Seçilen arama izlerini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to delete this ConfigSet?": "Bu ConfigSet'i silmek istediğinizden emin misiniz?", + "Are you sure you want to delete this configset?": "Bu configset'i silmek istediğinizden emin misiniz?", + "Are you sure you want to leave '{name}'?": "'{name}' grubundan ayrılmak istediğinizden emin misiniz?", + "Are you sure you want to permanently delete": "Kalıcı olarak silmek istediğinizden emin misiniz", + "Array Object Configuration:": "Dizi Nesne Yapılandırması:", + "Articles": "Makaleler", + "Ask a question...": "Soru sor...", + "Ask questions about your data using natural language": "Doğal dil kullanarak verileriniz hakkında soru sorun", + "Assigned collaborative tags": "Atanan işbirliği etiketleri", + "Assignment notifications": "Atama bildirimleri", + "Attachment added to {name}": "{name} öğesine ek eklendi", + "Attempt": "Deneme", + "Audit Trail Changes": "Denetim İzi Değişiklikleri", + "Audit Trail Details": "Denetim İzi Ayrıntıları", + "Audit Trail Management": "Denetim İzi Yönetimi", + "Audit Trail Statistics": "Denetim İzi İstatistikleri", + "Audit Trails": "Denetim İzleri", + "Audit trail data copied to clipboard": "Denetim izi verileri panoya kopyalandı", + "Audit trail deleted successfully": "Denetim izi başarıyla silindi", + "Audit trail successfully deleted": "Denetim izi başarıyla silindi", + "Authentication": "Kimlik Doğrulama", + "Auto (e.g. Y for year, F Y for month)": "Otomatik (örn. Y yıl için, F Y ay için)", + "Auto share": "Otomatik paylaş", + "Auto-commit disabled": "Otomatik kayıt devre dışı", + "Auto-commit enabled": "Otomatik kayıt etkin", + "Auto-create Default Organisation": "Varsayılan Kuruluşu Otomatik Oluştur", + "Auto-retry failed vectorizations": "Başarısız vektörleştirmeleri otomatik yeniden dene", + "Automatically commit changes to SOLR index": "Değişiklikleri SOLR dizinine otomatik olarak kaydet", + "Automatically connect to the federation network.": "Federasyon ağına otomatik olarak bağlan.", + "Automatically generate vector embeddings when objects are created or updated": "Nesneler oluşturulduğunda veya güncellendiğinde vektör gömülümleri otomatik olarak oluştur", + "Automatically retry failed vectorization attempts (max 3 retries)": "Başarısız vektörleştirme girişimlerini otomatik olarak yeniden dene (en fazla 3 deneme)", + "Automatically update catalogues.": "Katalogları otomatik olarak güncelle.", + "Available Filters": "Mevcut Filtreler", + "Available Workflows": "Mevcut İş Akışları", + "Available placeholders:": "Mevcut yer tutucular:", + "Avatar": "Avatar", + "Avatar changes are not supported by your authentication provider.": "Kimlik doğrulama sağlayıcınız avatar değişikliklerini desteklemiyor.", + "Avatar removed": "Avatar kaldırıldı", + "Avatar updated successfully": "Avatar başarıyla güncellendi", + "Avg Execution Time": "Ort. Yürütme Süresi", + "Avg Members/Org": "Ort. Üyeler/Kuruluş", + "Avg Object Views/Session": "Ort. Nesne Görüntüleme/Oturum", + "Avg Response Time": "Ort. Yanıt Süresi", + "Avg Results/Search": "Ort. Sonuç/Arama", + "Avg Searches/Session": "Ort. Arama/Oturum", + "Back": "Geri", + "Back to Entities": "Varlıklara Geri Dön", + "Back to Registers": "Kayıtlara Geri Dön", + "Back to Webhooks": "Webhook'lara Geri Dön", + "Back to applications": "Uygulamalara geri dön", + "Back to entities": "Varlıklara geri dön", + "Backend parameter is required": "Backend parametresi gereklidir", + "Backend updated successfully. Please reload the application.": "Backend başarıyla güncellendi. Lütfen uygulamayı yeniden yükleyin.", + "Bandwidth Quota (MB/month)": "Bant Genişliği Kotası (MB/ay)", + "Base URL (Optional)": "Temel URL (İsteğe Bağlı)", + "Basic Information": "Temel Bilgi", + "Batch Size": "Toplu İş Boyutu", + "Batch extraction completed": "Toplu çıkarma tamamlandı", + "Before": "Önce", + "Before object vectorization can work:": "Nesne vektörleştirmesi çalışabilmeden önce:", + "Behavior": "Davranış", + "Behavior Issue": "Davranış Sorunu", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (ISO-8601 süresi, örn. P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Blob depolama kullanımdan kaldırıldı. Tüm nesneler artık sihirli tabloları kullanıyor.", + "Bookmarks": "Yer İşaretleri", + "Branch": "Dal", + "Bulk Role Assignment: {title}": "Toplu Rol Atama: {title}", + "Bulk delete operation completed successfully": "Toplu silme işlemi başarıyla tamamlandı", + "Bulk save operation completed successfully": "Toplu kaydetme işlemi başarıyla tamamlandı", + "Business Data": "İş Verileri", + "CSV": "CSV", + "Cache cleared successfully": "Önbellek başarıyla temizlendi", + "Cache grew from {old} to {new} entries.": "Önbellek {old} girişten {new} girişe yükseldi.", + "Cache shrunk from {old} to {new} entries.": "Önbellek {old} girişten {new} girişe düştü.", + "Cache stayed the same at {size} entries.": "Önbellek {size} girişte aynı kaldı.", + "Calculate Sizes": "Boyutları Hesapla", + "Calendar": "Takvim", + "Calendar Provider Configuration": "Takvim Sağlayıcı Yapılandırması", + "Calendar integration is not available": "Takvim entegrasyonu mevcut değil", + "Calendar name": "Takvim adı", + "Cancel": "İptal", + "Cancel deactivation request": "Devre dışı bırakma isteğini iptal et", + "Cannot delete: objects are still attached": "Silinemiyor: nesneler hâlâ ekli", + "Cannot edit: This register is managed by external configuration {title}": "Düzenlenemiyor: Bu kayıt {title} harici yapılandırmasıyla yönetiliyor", + "Cannot remove schema with existing objects ({count} objects)": "Mevcut nesnelerle ({count} nesne) şema kaldırılamıyor", + "Cannot remove schema {schema} because it contains {count} objects": "{schema} şeması {count} nesne içerdiğinden kaldırılamıyor", + "Cards": "Kartlar", + "Cascade delete": "Ardışık silme", + "Categories of data subjects (one per line)": "Veri sahipleri kategorileri (her satırda bir)", + "Categories of personal data (one per line)": "Kişisel veri kategorileri (her satırda bir)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Kategori", + "Change Type": "Değişiklik Türü", + "Change password": "Şifre değiştir", + "Changes": "Değişiklikler", + "Changes copied to clipboard": "Değişiklikler panoya kopyalandı", + "Changes for Audit Trail #{id}": "#{id} Denetim İzi için Değişiklikler", + "Chat Model": "Sohbet Modeli", + "Chat Provider": "Sohbet Sağlayıcı", + "Chat Provider (RAG)": "Sohbet Sağlayıcı (RAG)", + "Chat Settings": "Sohbet Ayarları", + "Chat provider connection successful!": "Sohbet sağlayıcı bağlantısı başarılı!", + "Chat settings": "Sohbet ayarları", + "Chat test failed: {error}": "Sohbet testi başarısız: {error}", + "Choose a register": "Bir kayıt seçin", + "Choose a schema": "Bir şema seçin", + "Choose how vector similarity calculations are performed for semantic search": "Anlamsal arama için vektör benzerlik hesaplamalarının nasıl yapılacağını seçin", + "Chunk": "Parça", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Parça silme henüz uygulanmadı. Parça tabanlı uç noktaları kullanın.", + "Chunks": "Parçalar", + "Chunks to Vectorize:": "Vektörleştirilecek Parçalar:", + "Cleanup Old Trails": "Eski İzleri Temizle", + "Cleanup completed": "Temizleme tamamlandı", + "Cleanup completed successfully. Deleted {count} entries.": "Temizleme başarıyla tamamlandı. {count} giriş silindi.", + "Cleanup failed: {error}": "Temizleme başarısız: {error}", + "Clear All Embeddings": "Tüm Gömülümleri Temizle", + "Clear Entries": "Girişleri Temizle", + "Clear Filtered Audit Trails": "Filtrelenmiş Denetim İzlerini Temizle", + "Clear Filters": "Filtreleri Temizle", + "Clear Index": "Dizini Temizle", + "Clear Selection": "Seçimi Temizle", + "Clear all": "Tümünü temizle", + "Clear all filters": "Tüm filtreleri temizle", + "Clear date range": "Tarih aralığını temizle", + "Clear filters": "Filtreleri temizle", + "Clearing...": "Temizleniyor...", + "Click to subscribe to notifications": "Bildirimlere abone olmak için tıklayın", + "Click to unsubscribe from notifications": "Bildirimlerden çıkmak için tıklayın", + "Close": "Kapat", + "Cluster name": "Küme adı", + "Code": "Kod", + "Code (short readable key, e.g. v-2026-001)": "Kod (kısa okunabilir anahtar, örn. v-2026-001)", + "Collapse details": "Ayrıntıları daralt", + "Collection Management": "Koleksiyon Yönetimi", + "Collection Name": "Koleksiyon Adı", + "Collection assignments updated successfully": "Koleksiyon atamaları başarıyla güncellendi", + "Collection cleared successfully": "Koleksiyon başarıyla temizlendi", + "Collection copied successfully": "Koleksiyon başarıyla kopyalandı", + "Collection created successfully": "Koleksiyon başarıyla oluşturuldu", + "Collection deleted successfully": "Koleksiyon başarıyla silindi", + "Collection used to store and index file metadata and content": "Dosya meta verilerini ve içeriğini depolamak ve dizinlemek için kullanılan koleksiyon", + "Collection used to store and index object data": "Nesne verilerini depolamak ve dizinlemek için kullanılan koleksiyon", + "Collections:": "Koleksiyonlar:", + "Color": "Renk", + "Comment...": "Yorum...", + "Commit Message": "Kayıt Mesajı", + "Commit Within (ms)": "Süre İçinde Kaydet (ms)", + "Commit message": "Kayıt mesajı", + "Compare current schema with real object data": "Mevcut şemayı gerçek nesne verileriyle karşılaştır", + "Completed": "Tamamlandı", + "Complex": "Karmaşık", + "Compliance": "Uyumluluk", + "Confidence Level": "Güven Seviyesi", + "Confidence Score": "Güven Puanı", + "Confidence: {confidence}%": "Güven: {confidence}%", + "Config must be provided as an object": "Yapılandırma bir nesne olarak sağlanmalıdır", + "ConfigSet": "ConfigSet", + "ConfigSet Management": "ConfigSet Yönetimi", + "ConfigSet Name": "ConfigSet Adı", + "ConfigSet created successfully": "ConfigSet başarıyla oluşturuldu", + "ConfigSet deleted successfully": "ConfigSet başarıyla silindi", + "ConfigSet:": "ConfigSet:", + "Configuration": "Yapılandırma", + "Configuration Data": "Yapılandırma Verisi", + "Configuration is up to date": "Yapılandırma güncel", + "Configuration saved": "Yapılandırma kaydedildi", + "Configuration saved successfully.": "Yapılandırma başarıyla kaydedildi.", + "Configuration type (default, application, etc.)": "Yapılandırma türü (varsayılan, uygulama vb.)", + "Configurations": "Yapılandırmalar", + "Configure API tokens for external service integrations": "Harici servis entegrasyonları için API token'larını yapılandırın", + "Configure Apache SOLR search engine for advanced search capabilities": "Gelişmiş arama özellikleri için Apache SOLR arama motorunu yapılandırın", + "Configure Facets": "Fasetleri Yapılandır", + "Configure SOLR Facets": "SOLR Fasetlerini Yapılandır", + "Configure default organisation and organisation-related settings": "Varsayılan kuruluşu ve kuruluşla ilgili ayarları yapılandırın", + "Configure multi-organization support and tenant isolation": "Çoklu kuruluş desteği ve kiracı izolasyonunu yapılandırın", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Bu şemayı Nextcloud Takvim uygulamasında nesneleri etkinlik olarak gösterecek şekilde yapılandırın.", + "Configure which types of data to search and how many sources to retrieve": "Hangi tür verilerin aranacağını ve kaç kaynağın alınacağını yapılandırın", + "Configuring field mappings...": "Alan eşlemeleri yapılandırılıyor...", + "Confirm Account Deactivation": "Hesap Devre Dışı Bırakmayı Onayla", + "Confirm Object Deletion": "Nesne Silmeyi Onayla", + "Confirm Object Restoration": "Nesne Geri Yüklemeyi Onayla", + "Confirm Permanent Object Deletion": "Nesnenin Kalıcı Silmesini Onayla", + "Confirm deactivation": "Devre dışı bırakmayı onayla", + "Connecting to SOLR cluster...": "SOLR kümesine bağlanılıyor...", + "Connection Failed": "Bağlantı Başarısız", + "Connection Settings": "Bağlantı Ayarları", + "Connection Successful!": "Bağlantı Başarılı!", + "Connection failed": "Bağlantı başarısız", + "Connection protocol": "Bağlantı protokolü", + "Connection successful": "Bağlantı başarılı", + "Connection timeout in seconds": "Saniye cinsinden bağlantı zaman aşımı", + "Constant delay between retries (5 minutes)": "Yeniden denemeler arasında sabit gecikme (5 dakika)", + "Constraint Issue": "Kısıtlama Sorunu", + "Consumer not found": "Tüketici bulunamadı", + "Contacts": "Kişiler", + "Contacts integration is not available": "Kişiler entegrasyonu mevcut değil", + "Content Filters": "İçerik Filtreleri", + "Continue to Properties": "Özelliklere Devam Et", + "Contracts": "Sözleşmeler", + "Controls randomness (0 = deterministic, 2 = very creative)": "Rastgeleliği kontrol eder (0 = belirleyici, 2 = çok yaratıcı)", + "Conversation ID is required": "Konuşma kimliği gereklidir", + "Conversation archived": "Konuşma arşivlendi", + "Conversation archived successfully": "Konuşma başarıyla arşivlendi", + "Conversation cleared successfully": "Konuşma başarıyla temizlendi", + "Conversation deleted": "Konuşma silindi", + "Conversation not found": "Konuşma bulunamadı", + "Conversation permanently deleted": "Konuşma kalıcı olarak silindi", + "Conversation renamed": "Konuşma yeniden adlandırıldı", + "Conversation restored": "Konuşma geri yüklendi", + "Conversation started with {agent}": "{agent} ile konuşma başlatıldı", + "Conversation title": "Konuşma başlığı", + "Conversations": "Konuşmalar", + "Converting {schema} to blob storage...": "{schema} blob depolamaya dönüştürülüyor...", + "Converting {schema} to magic table...": "{schema} sihirli tabloya dönüştürülüyor...", + "Copied!": "Kopyalandı!", + "Copy": "Kopyala", + "Copy Changes": "Değişiklikleri Kopyala", + "Copy Collection": "Koleksiyonu Kopyala", + "Copy Data": "Veriyi Kopyala", + "Copy Full Data": "Tüm Veriyi Kopyala", + "Copy of {name}": "{name} kopyası", + "Copy to clipboard": "Panoya kopyala", + "Copying...": "Kopyalanıyor...", + "Costs": "Maliyetler", + "Count": "Sayı", + "Counting objects...": "Nesneler sayılıyor...", + "Create": "Oluştur", + "Create API Token": "API Token Oluştur", + "Create Agent": "Ajan Oluştur", + "Create Application": "Uygulama Oluştur", + "Create Collection": "Koleksiyon Oluştur", + "Create ConfigSet": "ConfigSet Oluştur", + "Create New Collection": "Yeni Koleksiyon Oluştur", + "Create New ConfigSet": "Yeni ConfigSet Oluştur", + "Create Operations": "Oluşturma İşlemleri", + "Create Organisation": "Kuruluş Oluştur", + "Create Webhook": "Webhook Oluştur", + "Create a copy of collection:": "Koleksiyonun kopyasını oluştur:", + "Create a new ConfigSet based on the _default template": "_default şablonuna dayalı yeni bir ConfigSet oluştur", + "Create a new SOLR collection from an existing ConfigSet": "Mevcut bir ConfigSet'ten yeni bir SOLR koleksiyonu oluştur", + "Create a new SOLR collection from an existing configset": "Mevcut bir configset'ten yeni bir SOLR koleksiyonu oluştur", + "Create a new configset based on the _default template": "_default şablonuna dayalı yeni bir configset oluştur", + "Create a new event or link an existing one from any of your calendars.": "Yeni bir etkinlik oluşturun veya takvimlerinizden herhangi birinden mevcut bir etkinliği bağlayın.", + "Create another": "Başka bir tane oluştur", + "Create copies of": "Kopyaları oluştur", + "Create event": "Etkinlik oluştur", + "Create new token": "Yeni token oluştur", + "Create or link a Deck card to track work on this object.": "Bu nesnedeki çalışmayı takip etmek için Deck kartı oluşturun veya bağlayın.", + "Create your first AI agent to get started.": "Başlamak için ilk yapay zeka ajanınızı oluşturun.", + "Created": "Oluşturuldu", + "Created:": "Oluşturuldu:", + "Creating search collections...": "Arama koleksiyonları oluşturuluyor...", + "Creating...": "Oluşturuluyor...", + "Current": "Mevcut", + "Current Filters": "Mevcut Filtreler", + "Current Type:": "Mevcut Tür:", + "Current password": "Mevcut şifre", + "Custom API endpoint if using a different region": "Farklı bir bölge kullanılıyorsa özel API uç noktası", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Özel HTTP başlıkları (her satırda bir, biçim: Başlık-Adı: değer)", + "Custom HTTP headers (one per line, format: header-name: value)": "Özel HTTP başlıkları (her satırda bir, biçim: başlık-adı: değer)", + "Custom Ranges:": "Özel Aralıklar:", + "Custom display title for this facet": "Bu faset için özel görüntüleme başlığı", + "Custom pattern:": "Özel desen:", + "DELETE COLLECTION": "KOLEKSİYONU SİL", + "DSAR": "DSAR", + "Daily": "Günlük", + "Dashboard": "Gösterge Paneli", + "Dashboard not found": "Gösterge paneli bulunamadı", + "Data": "Veri", + "Data Source": "Veri Kaynağı", + "Data Sources": "Veri Kaynakları", + "Data Views": "Veri Görünümleri", + "Data sources": "Veri kaynakları", + "Data type does not match observed values": "Veri türü gözlemlenen değerlerle eşleşmiyor", + "Data-subject access request": "Veri sahibi erişim talebi", + "Database URL": "Veritabanı URL'si", + "Database information refreshed": "Veritabanı bilgisi yenilendi", + "Date Faceting:": "Tarih Fasetleme:", + "Date Range": "Tarih Aralığı", + "Dates": "Tarihler", + "Deactivation request cancelled": "Devre dışı bırakma isteği iptal edildi", + "Deactivation request submitted": "Devre dışı bırakma isteği gönderildi", + "Deck": "Deck", + "Deck integration is not available": "Deck entegrasyonu mevcut değil", + "Default": "Varsayılan", + "Default Object Tenant": "Varsayılan Nesne Kiracısı", + "Default Organisation": "Varsayılan Kuruluş", + "Default Owner": "Varsayılan Sahip", + "Default User (for cron/background jobs)": "Varsayılan Kullanıcı (cron/arka plan işleri için)", + "Default User Tenant": "Varsayılan Kullanıcı Kiracısı", + "Default value": "Varsayılan değer", + "Default value (Email)": "Varsayılan değer (E-posta)", + "Default value (Password)": "Varsayılan değer (Şifre)", + "Default value (Phone number)": "Varsayılan değer (Telefon numarası)", + "Default value (Regex)": "Varsayılan değer (Regex)", + "Default: 5": "Varsayılan: 5", + "Defaults to current user. Select a different user if needed.": "Varsayılan olarak mevcut kullanıcıdır. Gerekirse farklı bir kullanıcı seçin.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Gecikmeler her denemede iki katına çıkar (2, 4, 8 dakika...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Gecikmeler doğrusal olarak artar (5, 10, 15 dakika...)", + "Delete": "Sil", + "Delete ({count})": "Sil ({count})", + "Delete Application": "Uygulamayı Sil", + "Delete Audit Trail": "Denetim İzini Sil", + "Delete Collection": "Koleksiyonu Sil", + "Delete ConfigSet": "ConfigSet'i Sil", + "Delete Objects": "Nesneleri Sil", + "Delete Operations": "Silme İşlemleri", + "Delete SOLR Collection": "SOLR Koleksiyonunu Sil", + "Delete View": "Görünümü Sil", + "Delete all objects in this schema": "Bu şemadaki tüm nesneleri sil", + "Delete permanently": "Kalıcı olarak sil", + "Delete this conversation?": "Bu konuşma silinsin mi?", + "Delete view": "Görünümü sil", + "Delete {count} object": "{count} nesneyi sil", + "Delete {count} objects": "{count} nesneyi sil", + "Deleted": "Silindi", + "Deleted By": "Silen", + "Deleted Date": "Silinme Tarihi", + "Deleted Items Management": "Silinen Öğelerin Yönetimi", + "Deleted This Week": "Bu Hafta Silindi", + "Deleted Today": "Bugün Silindi", + "Deleted:": "Silindi:", + "Deleting SOLR Collection...": "SOLR Koleksiyonu siliniyor...", + "Deleting...": "Siliniyor...", + "Deletion Date Range": "Silme Tarihi Aralığı", + "Deletion Statistics": "Silme İstatistikleri", + "Deprecated": "Kullanım Dışı", + "Depublish": "Yayından Kaldır", + "Description": "Açıklama", + "Description Template": "Açıklama Şablonu", + "Description shown as tooltip": "Araç ipucu olarak gösterilen açıklama", + "Description:": "Açıklama:", + "Detect data types and patterns": "Veri türlerini ve desenleri algıla", + "Detected At": "Algılandığı Zaman", + "Detected Format:": "Algılanan Biçim:", + "Detected Issues:": "Algılanan Sorunlar:", + "Detected: {date}": "Algılandı: {date}", + "Direct permission": "Doğrudan izin", + "Disable": "Devre Dışı Bırak", + "Disable this to make the agent": "Ajanı yapmak için bunu devre dışı bırakın", + "Disabled": "Devre Dışı", + "Discover Files": "Dosyaları Keşfet", + "Discovered Properties": "Keşfedilen Özellikler", + "Display Format": "Görüntüleme Biçimi", + "Display Name": "Görünen Ad", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Tüm filtrelenmiş denetim izi girişlerini kalıcı olarak silmek istiyor musunuz? Bu işlem geri alınamaz.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Bu denetim izi girişini kalıcı olarak silmek istiyor musunuz? Bu işlem geri alınamaz.", + "Documents": "Belgeler", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR etkin", + "Download API Spec": "API Spec'i İndir", + "Download API Specification": "API Spesifikasyonunu İndir", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "OpenRegister'da depolanan tüm kişisel verilerinizin bir kopyasını indirin (GDPR Madde 20).", + "Drag a file or files here": "Bir veya birden fazla dosyayı buraya sürükleyin", + "Dry run completed: {count} fields would be created": "Test çalıştırması tamamlandı: {count} alan oluşturulacak", + "Dry run completed: {count} fields would be fixed": "Test çalıştırması tamamlandı: {count} alan düzeltilecek", + "Duration in seconds (optional)": "Saniye cinsinden süre (isteğe bağlı)", + "Edit": "Düzenle", + "Edit Agent": "Ajanı Düzenle", + "Edit Application": "Uygulamayı Düzenle", + "Edit Configuration": "Yapılandırmayı Düzenle", + "Edit Endpoint": "Uç Noktayı Düzenle", + "Edit Organisation": "Kuruluşu Düzenle", + "Edit Property '{key}' of '{title}'": "'{title}' Öğesinin '{key}' Özelliğini Düzenle", + "Edit Register": "Kaydı Düzenle", + "Edit Schema": "Şemayı Düzenle", + "Edit Source": "Kaynağı Düzenle", + "Edit View": "Görünümü Düzenle", + "Edit Webhook": "Webhook'u Düzenle", + "Edit labels": "Etiketleri düzenle", + "Edit processing activity": "İşleme faaliyetini düzenle", + "Edit tags for {name}": "{name} için etiketleri düzenle", + "Edit verwerkingsactiviteit": "Verwerkingsactiviteit'i düzenle", + "Edit view": "Görünümü düzenle", + "Edit view details": "Görünüm ayrıntılarını düzenle", + "Effectiveness": "Etkinlik", + "Email": "E-posta", + "Email Addresses": "E-posta Adresleri", + "Email digest frequency": "E-posta özeti sıklığı", + "Emails": "E-postalar", + "Embedding Model": "Gömülüm Modeli", + "Embedding Provider": "Gömülüm Sağlayıcı", + "Embedding generated successfully": "Gömülüm başarıyla oluşturuldu", + "Embedding provider connection successful!": "Gömülüm sağlayıcı bağlantısı başarılı!", + "Embedding test failed: {error}": "Gömülüm testi başarısız: {error}", + "Enable": "Etkinleştir", + "Enable RAG": "RAG'ı Etkinleştir", + "Enable auto-creation": "Otomatik oluşturmayı etkinleştir", + "Enable automatic file vectorization": "Otomatik dosya vektörleştirmeyi etkinleştir", + "Enable automatic object vectorization": "Otomatik nesne vektörleştirmeyi etkinleştir", + "Enable automatic synchronization": "Otomatik senkronizasyonu etkinleştir", + "Enable calendar provider": "Takvim sağlayıcısını etkinleştir", + "Enable detailed logging for SOLR operations (recommended for debugging)": "SOLR işlemleri için ayrıntılı günlük kaydını etkinleştir (hata ayıklama için önerilir)", + "Enable faceting": "Fasetlemeyi etkinleştir", + "Enable or disable n8n workflow integration. Configure connection settings below.": "n8n iş akışı entegrasyonunu etkinleştirin veya devre dışı bırakın. Aşağıda bağlantı ayarlarını yapılandırın.", + "Enable or disable this webhook": "Bu webhook'u etkinleştirin veya devre dışı bırakın", + "Enable vectorization for all existing and future views (may increase costs)": "Mevcut ve gelecekteki tüm görünümler için vektörleştirmeyi etkinleştir (maliyetleri artırabilir)", + "Enable write-back to target objects": "Hedef nesnelere geri yazma özelliğini etkinleştir", + "Enabled": "Etkin", + "End Date Field": "Bitiş Tarihi Alanı", + "Endpoint Path*": "Uç Nokta Yolu*", + "Endpoints": "Uç Noktalar", + "Engine not found": "Motor bulunamadı", + "Enter ConfigSet name": "ConfigSet adını girin", + "Enter Nextcloud usernames to grant access to this private agent": "Bu özel ajana erişim vermek için Nextcloud kullanıcı adlarını girin", + "Enter agent description (optional)": "Ajan açıklamasını girin (isteğe bağlı)", + "Enter agent name": "Ajan adını girin", + "Enter application description (optional)": "Uygulama açıklamasını girin (isteğe bağlı)", + "Enter application name": "Uygulama adını girin", + "Enter collection name": "Koleksiyon adını girin", + "Enter configuration description (optional)": "Yapılandırma açıklamasını girin (isteğe bağlı)", + "Enter configuration title": "Yapılandırma başlığını girin", + "Enter description (optional)...": "Açıklama girin (isteğe bağlı)...", + "Enter new collection name": "Yeni koleksiyon adını girin", + "Enter object ID": "Nesne kimliğini girin", + "Enter organisation description (optional)": "Kuruluş açıklamasını girin (isteğe bağlı)", + "Enter organisation name": "Kuruluş adını girin", + "Enter search term": "Arama terimi girin", + "Enter search terms or leave empty to browse all": "Arama terimleri girin veya tümüne göz atmak için boş bırakın", + "Enter system prompt for the agent": "Ajan için sistem istemi girin", + "Enter username and press Enter": "Kullanıcı adını girin ve Enter'a basın", + "Enter view name...": "Görünüm adını girin...", + "Enter webhook name": "Webhook adını girin", + "Enter your API key": "API anahtarınızı girin", + "Enter your object here...": "Nesnenizi buraya girin...", + "Enter your schema here...": "Şemanızı buraya girin...", + "Entities": "Varlıklar", + "Entities detected": "Varlıklar algılandı", + "Entity ID: {id}": "Varlık Kimliği: {id}", + "Entity Information": "Varlık Bilgisi", + "Entity Recognition": "Varlık Tanıma", + "Entity Recognition Method": "Varlık Tanıma Yöntemi", + "Entity deleted successfully": "Varlık başarıyla silindi", + "Entity not found": "Varlık bulunamadı", + "Entries to be deleted:": "Silinecek girişler:", + "Enum Issue": "Enum Sorunu", + "Enum constraint is missing": "Enum kısıtlaması eksik", + "Erase (Art 17)": "Sil (Madde 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Bu özne için {count} nesne silinsin mi? Bu işlem denetim izine kaydedilir.", + "Erasure complete": "Silme tamamlandı", + "Erasure preview": "Silme önizlemesi", + "Error": "Hata", + "Error Details": "Hata Ayrıntıları", + "Error Information": "Hata Bilgisi", + "Error Message": "Hata Mesajı", + "Error deleting audit trails: {error}": "Denetim izleri silinirken hata: {error}", + "Error deleting search trails: {error}": "Arama izleri silinirken hata: {error}", + "Error loading application": "Uygulama yüklenirken hata", + "Error loading audit trails": "Denetim izleri yüklenirken hata", + "Error loading entity": "Varlık yüklenirken hata", + "Error loading search trails": "Arama izleri yüklenirken hata", + "Estimated Batches:": "Tahmini Toplu İşler:", + "Estimated Cost:": "Tahmini Maliyet:", + "Estimated Duration:": "Tahmini Süre:", + "Event": "Etkinlik", + "Event Property for Payload": "Yük için Etkinlik Özelliği", + "Events": "Etkinlikler", + "Every schema with detected PII has a processing-activity annotation.": "Tespit edilen KBB'ye sahip her şemanın bir işleme faaliyeti ek açıklaması vardır.", + "Example": "Örnek", + "Example Value": "Örnek Değer", + "Example value for this property": "Bu özellik için örnek değer", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Dışlayıcı Maksimum", + "Exclusive Minimum": "Dışlayıcı Minimum", + "Exclusive maximum": "Dışlayıcı maksimum", + "Exclusive minimum": "Dışlayıcı minimum", + "Execution Mode": "Yürütme Modu", + "Execution Time": "Yürütme Süresi", + "Execution Time Range": "Yürütme Süresi Aralığı", + "Existing Improvements": "Mevcut İyileştirmeler", + "Expand details": "Ayrıntıları genişlet", + "Expiration": "Son Kullanma", + "Expires": "Sona Eriyor", + "Expires in (e.g., 90d)": "Bitiş süresi (örn. 90g)", + "Exponential": "Üstel", + "Export": "Dışa Aktar", + "Export \"{schema}\" objects from \"{register}\"": "\"{register}\" kaydından \"{schema}\" nesnelerini dışa aktar", + "Export Configuration": "Yapılandırmayı Dışa Aktar", + "Export Format:": "Dışa Aktarma Biçimi:", + "Export Objects": "Nesneleri Dışa Aktar", + "Export completed successfully": "Dışa aktarma başarıyla tamamlandı", + "Export downloaded successfully": "Dışa aktarma başarıyla indirildi", + "Export failed: {error}": "Dışa aktarma başarısız: {error}", + "Export is rate limited. Please try again later.": "Dışa aktarma hız sınırlamalı. Lütfen daha sonra tekrar deneyin.", + "Export my data": "Verilerimi dışa aktar", + "Export, view, or delete audit trails": "Denetim izlerini dışa aktarın, görüntüleyin veya silin", + "Exporting...": "Dışa aktarılıyor...", + "Extend Schema": "Şemayı Genişlet", + "Extended": "Genişletilmiş", + "Extensions": "Uzantılar", + "Extra Query Parameters": "Ekstra Sorgu Parametreleri", + "Extract Now": "Şimdi Çıkar", + "Extract Pending Files": "Bekleyen Dosyaları Çıkar", + "Extract now": "Şimdi çıkar", + "Extract properties from each object": "Her nesneden özellikleri çıkar", + "Extracted At": "Çıkarıldığı Zaman", + "Extracted at": "Çıkarıldığı zaman", + "Extraction": "Çıkarma", + "Extraction Mode": "Çıkarma Modu", + "Extraction Scope": "Çıkarma Kapsamı", + "Extraction Status": "Çıkarma Durumu", + "Extraction error for {file}: {error}": "{file} için çıkarma hatası: {error}", + "Facet Description": "Faset Açıklaması", + "Facet Order": "Faset Sırası", + "Facet Title": "Faset Başlığı", + "Facet Type": "Faset Türü", + "Facet configuration updated successfully": "Faset yapılandırması başarıyla güncellendi", + "Facetable": "Fasetlenebilir", + "Faceting Configuration:": "Fasetleme Yapılandırması:", + "Facets discovered and configured successfully": "Fasetler başarıyla keşfedildi ve yapılandırıldı", + "Facets discovered successfully": "Fasetler başarıyla keşfedildi", + "Failed": "Başarısız", + "Failed to add attachment to object": "Nesneye ek eklenemedi", + "Failed to add user to organisation": "Kuruluşa kullanıcı eklenemedi", + "Failed to analyze schema properties": "Şema özellikleri analiz edilemedi", + "Failed to archive conversation": "Konuşma arşivlenemedi", + "Failed to calculate sizes": "Boyutlar hesaplanamadı", + "Failed to cancel deactivation": "Devre dışı bırakma iptal edilemedi", + "Failed to change active organisation: {error}": "Etkin kuruluş değiştirilemedi: {error}", + "Failed to change password": "Şifre değiştirilemedi", + "Failed to check version: {error}": "Sürüm kontrol edilemedi: {error}", + "Failed to clear audit trails: {error}": "Denetim izleri temizlenemedi: {error}", + "Failed to clear blob objects: {error}": "Blob nesneleri temizlenemedi: {error}", + "Failed to clear cache: {error}": "Önbellek temizlenemedi: {error}", + "Failed to clear collection": "Koleksiyon temizlenemedi", + "Failed to clear conversation": "Konuşma temizlenemedi", + "Failed to clear embeddings: {error}": "Gömülümler temizlenemedi: {error}", + "Failed to clear search trails: {error}": "Arama izleri temizlenemedi: {error}", + "Failed to clear {type} cache: {error}": "{type} önbelleği temizlenemedi: {error}", + "Failed to copy changes": "Değişiklikler kopyalanamadı", + "Failed to copy data": "Veri kopyalanamadı", + "Failed to copy data to clipboard": "Veri panoya kopyalanamadı", + "Failed to copy to clipboard": "Panoya kopyalanamadı", + "Failed to copy token": "Token kopyalanamadı", + "Failed to create conversation": "Konuşma oluşturulamadı", + "Failed to create missing SOLR fields: {error}": "Eksik SOLR alanları oluşturulamadı: {error}", + "Failed to create or find project": "Proje oluşturulamadı veya bulunamadı", + "Failed to create token": "Token oluşturulamadı", + "Failed to delete SOLR collection: {error}": "SOLR koleksiyonu silinemedi: {error}", + "Failed to delete collection": "Koleksiyon silinemedi", + "Failed to delete conversation": "Konuşma silinemedi", + "Failed to delete objects for {schema}: {error}": "{schema} için nesneler silinemedi: {error}", + "Failed to delete webhook": "Webhook silinemedi", + "Failed to depublish register: {error}": "Kayıt yayından kaldırılamadı: {error}", + "Failed to depublish schema: {error}": "Şema yayından kaldırılamadı: {error}", + "Failed to depublish: {error}": "Yayından kaldırılamadı: {error}", + "Failed to discover files: {error}": "Dosyalar keşfedilemedi: {error}", + "Failed to download API specification": "API spesifikasyonu indirilemedi", + "Failed to export data": "Veri dışa aktarılamadı", + "Failed to export objects": "Nesneler dışa aktarılamadı", + "Failed to fetch conversation history": "Konuşma geçmişi alınamadı", + "Failed to fix mismatched SOLR fields: {error}": "Eşleşmeyen SOLR alanları düzeltilemedi: {error}", + "Failed to get SOLR field configuration": "SOLR alan yapılandırması alınamadı", + "Failed to get chat statistics": "Sohbet istatistikleri alınamadı", + "Failed to get database information: %s": "Veritabanı bilgisi alınamadı: %s", + "Failed to get entity categories": "Varlık kategorileri alınamadı", + "Failed to get entity statistics": "Varlık istatistikleri alınamadı", + "Failed to get entity types": "Varlık türleri alınamadı", + "Failed to get response: {error}": "Yanıt alınamadı: {error}", + "Failed to invalidate app store cache: {error}": "Uygulama mağazası önbelleği geçersiz kılınamadı: {error}", + "Failed to leave organisation: {error}": "Kuruluştan ayrılınamadı: {error}", + "Failed to link object": "Nesne bağlanamadı", + "Failed to load Deck cards": "Deck kartları yüklenemedi", + "Failed to load LLM configuration": "LLM yapılandırması yüklenemedi", + "Failed to load Nextcloud groups": "Nextcloud grupları yüklenemedi", + "Failed to load cache statistics: {error}": "Önbellek istatistikleri yüklenemedi: {error}", + "Failed to load conversation": "Konuşma yüklenemedi", + "Failed to load entities": "Varlıklar yüklenemedi", + "Failed to load entity": "Varlık yüklenemedi", + "Failed to load extraction data": "Çıkarma verisi yüklenemedi", + "Failed to load files": "Dosyalar yüklenemedi", + "Failed to load linked contacts": "Bağlantılı kişiler yüklenemedi", + "Failed to load linked emails": "Bağlantılı e-postalar yüklenemedi", + "Failed to load linked events": "Bağlantılı etkinlikler yüklenemedi", + "Failed to load organisations": "Kuruluşlar yüklenemedi", + "Failed to load register data": "Kayıt verisi yüklenemedi", + "Failed to load related entities": "İlgili varlıklar yüklenemedi", + "Failed to load settings: {error}": "Ayarlar yüklenemedi: {error}", + "Failed to load statistics: {error}": "İstatistikler yüklenemedi: {error}", + "Failed to load templates": "Şablonlar yüklenemedi", + "Failed to load view: {error}": "Görünüm yüklenemedi: {error}", + "Failed to load webhooks": "Webhook'lar yüklenemedi", + "Failed to load workflows": "İş akışları yüklenemedi", + "Failed to process message": "Mesaj işlenemedi", + "Failed to publish register: {error}": "Kayıt yayımlanamadı: {error}", + "Failed to publish schema: {error}": "Şema yayımlanamadı: {error}", + "Failed to publish: {error}": "Yayımlanamadı: {error}", + "Failed to refresh database information": "Veritabanı bilgisi yenilenemedi", + "Failed to reindex collection": "Koleksiyon yeniden dizinlenemedi", + "Failed to remove avatar": "Avatar kaldırılamadı", + "Failed to remove link": "Bağlantı kaldırılamadı", + "Failed to remove member: {error}": "Üye kaldırılamadı: {error}", + "Failed to remove schema {schema}: {error}": "{schema} şeması kaldırılamadı: {error}", + "Failed to remove user from organisation": "Kullanıcı kuruluştan kaldırılamadı", + "Failed to rename conversation": "Konuşma yeniden adlandırılamadı", + "Failed to request deactivation": "Devre dışı bırakma isteği gönderilemedi", + "Failed to restore conversation": "Konuşma geri yüklenemedi", + "Failed to retrieve extraction status.": "Çıkarma durumu alınamadı.", + "Failed to retrieve objects for file.": "Dosya için nesneler alınamadı.", + "Failed to retry extraction": "Çıkarma yeniden denenemedi", + "Failed to retry webhook": "Webhook yeniden denenemedi", + "Failed to revoke token": "Token iptal edilemedi", + "Failed to save GitHub token": "GitHub token'ı kaydedilemedi", + "Failed to save GitLab URL": "GitLab URL'si kaydedilemedi", + "Failed to save GitLab token": "GitLab token'ı kaydedilemedi", + "Failed to save LLM settings: {error}": "LLM ayarları kaydedilemedi: {error}", + "Failed to save additional feedback": "Ek geri bildirim kaydedilemedi", + "Failed to save configuration.": "Yapılandırma kaydedilemedi.", + "Failed to save configuration: {error}": "Yapılandırma kaydedilemedi: {error}", + "Failed to save facet configuration": "Faset yapılandırması kaydedilemedi", + "Failed to save feedback": "Geri bildirim kaydedilemedi", + "Failed to save file settings: {error}": "Dosya ayarları kaydedilemedi: {error}", + "Failed to save n8n configuration": "n8n yapılandırması kaydedilemedi", + "Failed to save preferences": "Tercihler kaydedilemedi", + "Failed to save roles": "Roller kaydedilemedi", + "Failed to save settings": "Ayarlar kaydedilemedi", + "Failed to save settings: {error}": "Ayarlar kaydedilemedi: {error}", + "Failed to save view: {error}": "Görünüm kaydedilemedi: {error}", + "Failed to save warmup interval: {error}": "Isınma aralığı kaydedilemedi: {error}", + "Failed to save webhook": "Webhook kaydedilemedi", + "Failed to send feedback": "Geri bildirim gönderilemedi", + "Failed to set active organisation: {error}": "Etkin kuruluş ayarlanamadı: {error}", + "Failed to setup SOLR: {error}": "SOLR kurulamadı: {error}", + "Failed to start file warmup: {error}": "Dosya ısınması başlatılamadı: {error}", + "Failed to start processing {type} files: {error}": "{type} dosyalarının işlenmesi başlatılamadı: {error}", + "Failed to start vectorization: {error}": "Vektörleştirme başlatılamadı: {error}", + "Failed to switch organisation: {error}": "Kuruluş değiştirilemedi: {error}", + "Failed to sync magic table for {schema}: {error}": "{schema} için sihirli tablo senkronize edilemedi: {error}", + "Failed to test SOLR connection: {error}": "SOLR bağlantısı test edilemedi: {error}", + "Failed to test webhook": "Webhook test edilemedi", + "Failed to update LLM settings: {error}": "LLM ayarları güncellenemedi: {error}", + "Failed to update Multitenancy settings: {error}": "Çok kiracılık ayarları güncellenemedi: {error}", + "Failed to update RBAC settings: {error}": "RBAC ayarları güncellenemedi: {error}", + "Failed to update Retention settings: {error}": "Saklama ayarları güncellenemedi: {error}", + "Failed to update SOLR settings: {error}": "SOLR ayarları güncellenemedi: {error}", + "Failed to update favorite status": "Favori durumu güncellenemedi", + "Failed to update schema configuration for {schema}: {error}": "{schema} için şema yapılandırması güncellenemedi: {error}", + "Failed to update schema properties": "Şema özellikleri güncellenemedi", + "Failed to update view": "Görünüm güncellenemedi", + "Failed to update view: {error}": "Görünüm güncellenemedi: {error}", + "Failed to update webhook": "Webhook güncellenemedi", + "Failed to upload avatar": "Avatar yüklenemedi", + "Failed to validate {schema}: {error}": "{schema} doğrulanamadı: {error}", + "Failed to vectorize files: {error}": "Dosyalar vektörleştirilemedi: {error}", + "Failed to warmup names cache: {error}": "İsimler önbelleği ısıtılamadı: {error}", + "Federation": "Federasyon", + "Feedback recorded": "Geri bildirim kaydedildi", + "Fewer sources (1-3):": "Daha az kaynak (1-3):", + "Field": "Alan", + "Fields to Display": "Görüntülenecek Alanlar", + "File": "Dosya", + "File #{id}": "Dosya #{id}", + "File Chunk Prediction": "Dosya Parça Tahmini", + "File Collection": "Dosya Koleksiyonu", + "File Handling": "Dosya İşleme", + "File Location": "Dosya Konumu", + "File Management": "Dosya Yönetimi", + "File Name": "Dosya Adı", + "File Path": "Dosya Yolu", + "File Type Selection": "Dosya Türü Seçimi", + "File Types to Process": "İşlenecek Dosya Türleri", + "File Vectorization": "Dosya Vektörleştirme", + "File Warmup": "Dosya Isınması", + "File actions menu": "Dosya işlemleri menüsü", + "File anonymized successfully": "Dosya başarıyla anonimleştirildi", + "File collection not configured": "Dosya koleksiyonu yapılandırılmamış", + "File discovery completed": "Dosya keşfi tamamlandı", + "File extraction completed": "Dosya çıkarma tamamlandı", + "File extraction queued": "Dosya çıkarma sıraya alındı", + "File indexed successfully": "Dosya başarıyla dizinlendi", + "File is already anonymized": "Dosya zaten anonimleştirilmiş", + "File name": "Dosya adı", + "File not found": "Dosya bulunamadı", + "File settings saved successfully": "Dosya ayarları başarıyla kaydedildi", + "File settings updated successfully": "Dosya ayarları başarıyla güncellendi", + "File sources": "Dosya kaynakları", + "File vectorization configuration saved successfully": "Dosya vektörleştirme yapılandırması başarıyla kaydedildi", + "File vectorization started. Check the statistics section for progress.": "Dosya vektörleştirme başladı. İlerleme için istatistikler bölümünü kontrol edin.", + "File warmup completed": "Dosya ısınması tamamlandı", + "Files": "Dosyalar", + "Files added successfully": "Dosyalar başarıyla eklendi", + "Files with Completed Extraction:": "Çıkarma Tamamlanan Dosyalar:", + "Files → fileCollection, Objects → objectCollection": "Dosyalar → fileCollection, Nesneler → objectCollection", + "Files → filecollection, objects → objectcollection": "Dosyalar → filecollection, nesneler → objectcollection", + "Filter Audit Trails": "Denetim İzlerini Filtrele", + "Filter Deleted Items": "Silinen Öğeleri Filtrele", + "Filter Objects": "Nesneleri Filtrele", + "Filter Properties": "Özellikleri Filtrele", + "Filter Search Trails": "Arama İzlerini Filtrele", + "Filter Statistics": "Filtre İstatistikleri", + "Filter and analyze search trail entries": "Arama izi girişlerini filtrele ve analiz et", + "Filter and manage audit trail entries": "Denetim izi girişlerini filtrele ve yönet", + "Filter and manage soft deleted items": "Geçici silinen öğeleri filtrele ve yönet", + "Filter and search entities": "Varlıkları filtrele ve ara", + "Filter by object ID": "Nesne kimliğine göre filtrele", + "Filter by search term": "Arama terimine göre filtrele", + "Filter by type": "Türe göre filtrele", + "Filter by webhook": "Webhook'a göre filtrele", + "Filter data loaded automatically. Use the filters below to refine your search.": "Filtre verileri otomatik olarak yüklendi. Aramanızı daraltmak için aşağıdaki filtreleri kullanın.", + "Filter fields...": "Alanları filtrele...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Webhook tetikleyicilerini yük özellikleriyle filtrele (her satırda bir, biçim: anahtar: değer)", + "Filtered": "Filtrelendi", + "Filters": "Filtreler", + "Finalizing setup...": "Kurulum tamamlanıyor...", + "Fireworks AI Chat Settings": "Fireworks AI Sohbet Ayarları", + "Fireworks AI Embedding Configuration": "Fireworks AI Gömülüm Yapılandırması", + "First": "İlk", + "Fixed": "Düzeltildi", + "For chat and retrieval-augmented generation": "Sohbet ve geri alma artırımlı oluşturma için", + "For vector embeddings and semantic search": "Vektör gömülümleri ve anlamsal arama için", + "Format": "Biçim", + "Format Issue": "Biçim Sorunu", + "Format constraint is missing": "Biçim kısıtlaması eksik", + "From (e.g. -7 days or 2025-01-01)": "Başlangıç (örn. -7 gün veya 2025-01-01)", + "From Date": "Başlangıç Tarihi", + "From date": "Başlangıç tarihi", + "Full data copied to clipboard": "Tüm veri panoya kopyalandı", + "Function Tools": "İşlev Araçları", + "General Issue": "Genel Sorun", + "Generate recommendations and confidence scores": "Öneriler ve güven puanları oluştur", + "Generate report": "Rapor oluştur", + "Generate the verantwoordingsdocument": "Verantwoordingsdocument oluştur", + "Generate vectors immediately when new objects are created": "Yeni nesneler oluşturulduğunda vektörleri hemen oluştur", + "Generated: {time}": "Oluşturuldu: {time}", + "GitHub Personal Access Token": "GitHub Kişisel Erişim Token'ı", + "GitHub token is valid": "GitHub token'ı geçerli", + "GitHub token is valid! Username: {username}": "GitHub token'ı geçerli! Kullanıcı adı: {username}", + "GitHub token removed successfully": "GitHub token'ı başarıyla kaldırıldı", + "GitHub token saved successfully": "GitHub token'ı başarıyla kaydedildi", + "GitHub token test failed: {message}": "GitHub token testi başarısız: {message}", + "GitLab Personal Access Token": "GitLab Kişisel Erişim Token'ı", + "GitLab URL saved successfully": "GitLab URL'si başarıyla kaydedildi", + "GitLab token is valid": "GitLab token'ı geçerli", + "GitLab token is valid! Username: {username}": "GitLab token'ı geçerli! Kullanıcı adı: {username}", + "GitLab token saved successfully": "GitLab token'ı başarıyla kaydedildi", + "GitLab token test failed: {message}": "GitLab token testi başarısız: {message}", + "Go to linked Register": "Bağlantılı Kayda Git", + "Go to linked Schema": "Bağlantılı Şemaya Git", + "Go to organisation": "Kuruluşa git", + "Grant additional permissions beyond standard CRUD operations": "Standart CRUD işlemlerinin ötesinde ek izinler ver", + "Group Quota": "Grup Kotası", + "Groups": "Gruplar", + "HTML preview": "HTML önizlemesi", + "HTTP Method": "HTTP Yöntemi", + "HTTP method used to send webhook requests": "Webhook isteklerini göndermek için kullanılan HTTP yöntemi", + "Headers": "Başlıklar", + "Health": "Sağlık", + "Heartbeat successful - connection kept alive": "Sinyal başarılı - bağlantı canlı tutuldu", + "Helpful": "Yararlı", + "Here you can configure the details for various connections.": "Burada çeşitli bağlantılar için ayrıntıları yapılandırabilirsiniz.", + "Here you can configure the details for your organisation.": "Burada kuruluşunuzun ayrıntılarını yapılandırabilirsiniz.", + "Hide": "Gizle", + "Hide Filters": "Filtreleri Gizle", + "Hide in collection view": "Koleksiyon görünümünde gizle", + "Hide in forms": "Formlarda gizle", + "Hide in list view": "Liste görünümünde gizle", + "High": "Yüksek", + "High Confidence": "Yüksek Güven", + "Host": "Ana Bilgisayar", + "Hourly": "Saatlik", + "How often to check for updates (1-168 hours)": "Güncellemelerin ne sıklıkta kontrol edileceği (1-168 saat)", + "How to handle retries for failed webhook deliveries": "Başarısız webhook teslimatları için yeniden deneme nasıl yapılır", + "IBANs": "IBAN'lar", + "ID": "Kimlik", + "ID of input mapping (optional)": "Giriş eşlemesinin kimliği (isteğe bağlı)", + "ID of output mapping (optional)": "Çıkış eşlemesinin kimliği (isteğe bağlı)", + "ID of the target resource": "Hedef kaynağın kimliği", + "ID:": "Kimlik:", + "ID: {id}": "Kimlik: {id}", + "IP rate limits cleared successfully": "IP hız sınırları başarıyla temizlendi", + "Identify properties not in the schema": "Şemada bulunmayan özellikleri tanımla", + "Immutable": "Değiştirilemez", + "Import": "İçe Aktar", + "Import Configuration": "Yapılandırmayı İçe Aktar", + "Import Data into Register": "Kayda Veri İçe Aktar", + "Import successful": "İçe aktarma başarılı", + "Improved Property": "İyileştirilmiş Özellik", + "In use": "Kullanımda", + "Inactive": "Etkin Değil", + "Inactive organisations cannot be used": "Etkin olmayan kuruluşlar kullanılamaz", + "Include IDs and names of related objects for better contextual search": "Daha iyi bağlamsal arama için ilgili nesnelerin kimliklerini ve adlarını dahil et", + "Include ids and names of related objects for better contextual search": "Daha iyi bağlamsal arama için ilgili nesnelerin kimliklerini ve adlarını dahil et", + "Include related object references": "İlgili nesne referanslarını dahil et", + "Include schema and register metadata": "Şema ve kayıt meta verilerini dahil et", + "Index": "Dizin", + "Information about the current OpenRegister installation": "Mevcut OpenRegister kurulumu hakkında bilgi", + "Inherited from register": "Kayıttan devralındı", + "Inherits permissions from register": "İzinleri kayıttan devralır", + "Initialization failed": "Başlatma başarısız", + "Initialize Project": "Projeyi Başlat", + "Initializing SOLR setup...": "SOLR kurulumu başlatılıyor...", + "Input Mapping": "Giriş Eşlemesi", + "Inspect Fields": "Alanları İncele", + "Inspect Index": "Dizini İncele", + "Inspect SOLR Index": "SOLR Dizinini İncele", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Gerçek zamanlı güncellemeleri etkinleştirmek için Nextcloud Uygulama Mağazası'ndan notify_push uygulamasını yükleyin.", + "Integrations": "Entegrasyonlar", + "Internet location (URL) of this installation": "Bu kurulumun internet konumu (URL)", + "Interval": "Aralık", + "Invalid": "Geçersiz", + "Invalid batch size. Must be between 1 and 5000": "Geçersiz toplu iş boyutu. 1 ile 5000 arasında olmalıdır", + "Invalid feedback type": "Geçersiz geri bildirim türü", + "Invalid field name provided": "Geçersiz alan adı sağlandı", + "Invalid maxObjects. Must be 0 (all) or positive number": "Geçersiz maxObjects. 0 (tümü) veya pozitif sayı olmalıdır", + "Invalid:": "Geçersiz:", + "Invite Users": "Kullanıcıları Davet Et", + "Invited Users": "Davet Edilen Kullanıcılar", + "Inzage (Art 15)": "Inzage (Madde 15)", + "Inzage results": "Inzage sonuçları", + "Issue": "Sorun", + "Items per page:": "Sayfa başına öğe:", + "Just now": "Az önce", + "Key": "Anahtar", + "LLM Configuration": "LLM Yapılandırması", + "LLM actions menu": "LLM işlemleri menüsü", + "LLM configuration saved successfully": "LLM yapılandırması başarıyla kaydedildi", + "LLM features disabled": "LLM özellikleri devre dışı", + "LLM features enabled": "LLM özellikleri etkin", + "LLM must be enabled with an embedding provider configured": "LLM, yapılandırılmış bir gömülüm sağlayıcısıyla etkinleştirilmelidir", + "LLM settings saved successfully": "LLM ayarları başarıyla kaydedildi", + "LLM settings updated successfully": "LLM ayarları başarıyla güncellendi", + "Label": "Etiket", + "Labels": "Etiketler", + "Languages": "Diller", + "Last": "Son", + "Last Triggered": "Son Tetiklenme", + "Leave empty to allow all users access": "Tüm kullanıcılara erişim izni vermek için boş bırakın", + "Leave off for auto-detection from property format.": "Özellik biçiminden otomatik algılama için kapalı bırakın.", + "Left organisation successfully": "Kuruluştan başarıyla ayrıldı", + "Legal basis *": "Hukuki dayanak *", + "Length Range:": "Uzunluk Aralığı:", + "Linear": "Doğrusal", + "Link": "Bağlantı", + "Link an email from the Mail app sidebar to associate it with this object.": "Bu nesneyle ilişkilendirmek için Mail uygulaması kenar çubuğundan bir e-posta bağlayın.", + "Link an object to see it here.": "Burada görmek için bir nesne bağlayın.", + "Link another object": "Başka bir nesne bağla", + "Link existing event": "Mevcut etkinliği bağla", + "Link removed": "Bağlantı kaldırıldı", + "Link to Object": "Nesneye Bağlantı", + "Link to {name}": "{name} öğesine bağlantı", + "Linked Objects": "Bağlantılı Nesneler", + "Linked by {user}": "{user} tarafından bağlandı", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Bağlantılı e-postalar, takvim etkinlikleri, kişiler ve Deck kartları burada görünecek.", + "Linked to {name}": "{name} öğesine bağlantılı", + "Load Advanced Filters": "Gelişmiş Filtreleri Yükle", + "Load advanced filters with live data from your search index": "Arama dizininizdeki canlı verilerle gelişmiş filtreleri yükleyin", + "Load more": "Daha fazla yükle", + "Load view": "Görünümü yükle", + "Loading API tokens...": "API token'ları yükleniyor...", + "Loading ConfigSets...": "ConfigSet'ler yükleniyor...", + "Loading LLM configuration...": "LLM yapılandırması yükleniyor...", + "Loading RBAC settings...": "RBAC ayarları yükleniyor...", + "Loading activity...": "Etkinlik yükleniyor...", + "Loading advanced filters...": "Gelişmiş filtreler yükleniyor...", + "Loading agents...": "Ajanlar yükleniyor...", + "Loading application details...": "Uygulama ayrıntıları yükleniyor...", + "Loading applications...": "Uygulamalar yükleniyor...", + "Loading archived conversations...": "Arşivlenmiş konuşmalar yükleniyor...", + "Loading audit trails...": "Denetim izleri yükleniyor...", + "Loading available tools...": "Mevcut araçlar yükleniyor...", + "Loading cache statistics...": "Önbellek istatistikleri yükleniyor...", + "Loading collections...": "Koleksiyonlar yükleniyor...", + "Loading configurations...": "Yapılandırmalar yükleniyor...", + "Loading conversation...": "Konuşma yükleniyor...", + "Loading conversations...": "Konuşmalar yükleniyor...", + "Loading deleted items...": "Silinen öğeler yükleniyor...", + "Loading endpoints": "Uç noktalar yükleniyor", + "Loading entities...": "Varlıklar yükleniyor...", + "Loading events...": "Etkinlikler yükleniyor...", + "Loading filter data...": "Filtre verileri yükleniyor...", + "Loading groups...": "Gruplar yükleniyor...", + "Loading linked objects...": "Bağlantılı nesneler yükleniyor...", + "Loading log details...": "Günlük ayrıntıları yükleniyor...", + "Loading multitenancy settings...": "Çok kiracılık ayarları yükleniyor...", + "Loading n8n configuration...": "n8n yapılandırması yükleniyor...", + "Loading permission matrix...": "İzin matrisi yükleniyor...", + "Loading preferences...": "Tercihler yükleniyor...", + "Loading register data...": "Kayıt verileri yükleniyor...", + "Loading registers...": "Kayıtlar yükleniyor...", + "Loading retention settings...": "Saklama ayarları yükleniyor...", + "Loading schemas...": "Şemalar yükleniyor...", + "Loading search configuration...": "Arama yapılandırması yükleniyor...", + "Loading search trails...": "Arama izleri yükleniyor...", + "Loading sources...": "Kaynaklar yükleniyor...", + "Loading statistics...": "İstatistikler yükleniyor...", + "Loading text extraction settings...": "Metin çıkarma ayarları yükleniyor...", + "Loading tokens...": "Token'lar yükleniyor...", + "Loading user groups...": "Kullanıcı grupları yükleniyor...", + "Loading users...": "Kullanıcılar yükleniyor...", + "Loading views...": "Görünümler yükleniyor...", + "Loading...": "Yükleniyor...", + "Local": "Yerel", + "Local Version": "Yerel Sürüm", + "Location": "Konum", + "Location Field": "Konum Alanı", + "Locations": "Konumlar", + "Locked": "Kilitli", + "Locked:": "Kilitli:", + "Login successful": "Giriş başarılı", + "Logs": "Günlükler", + "Low": "Düşük", + "Low Confidence": "Düşük Güven", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Daha düşük sayılar filtre kenar çubuğunda önce görünür. Otomatik sıralama için boş bırakın.", + "Lucene Version:": "Lucene Sürümü:", + "Magic table sync completed for {schema}": "{schema} için sihirli tablo senkronizasyonu tamamlandı", + "Magic table synchronized successfully": "Sihirli tablo başarıyla senkronize edildi", + "Mail Integration": "Posta Entegrasyonu", + "Mail integration is not available": "Posta entegrasyonu mevcut değil", + "Make this installation discoverable within the federation network.": "Bu kurulumu federasyon ağı içinde keşfedilebilir yapın.", + "Manage Organisation Roles": "Kuruluş Rollerini Yönet", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "SOLR Koleksiyonlarını (veri depoları) yönetin ve nesneler ve dosyalar için atayın.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Koleksiyonlarınız için SOLR ConfigSet'lerini (yapılandırma şablonları) yönetin.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "SOLR koleksiyonlarını (veri depoları) yönetin ve nesneler ve dosyalar için atayın.", + "Manage SOLR configsets (configuration templates) for your collections.": "Koleksiyonlarınız için SOLR configset'lerini (yapılandırma şablonları) yönetin.", + "Manage and configure agents for automated tasks": "Otomatikleştirilmiş görevler için ajanları yönetin ve yapılandırın", + "Manage and monitor file text extraction status": "Dosya metin çıkarma durumunu yönetin ve izleyin", + "Manage and restore soft deleted items from your registers": "Kayıtlarınızdan geçici silinen öğeleri yönetin ve geri yükleyin", + "Manage and view detected entities from files and objects": "Dosyalar ve nesnelerden tespit edilen varlıkları yönetin ve görüntüleyin", + "Manage document templates and themes": "Belge şablonlarını ve temalarını yönetin", + "Manage webhooks for event-driven integrations": "Etkinlik odaklı entegrasyonlar için webhook'ları yönetin", + "Manage your account settings, security, and personal data.": "Hesap ayarlarınızı, güvenliğinizi ve kişisel verilerinizi yönetin.", + "Manage your applications and modules": "Uygulamalarınızı ve modüllerinizi yönetin", + "Manage your chat conversations": "Sohbet konuşmalarınızı yönetin", + "Manage your data registers and their configurations": "Veri kayıtlarınızı ve yapılandırmalarını yönetin", + "Manage your data schemas and their properties": "Veri şemalarınızı ve özelliklerini yönetin", + "Manage your data sources and their configurations": "Veri kaynaklarınızı ve yapılandırmalarını yönetin", + "Manage your organisations and switch between them": "Kuruluşlarınızı yönetin ve aralarında geçiş yapın", + "Manage your saved search configurations": "Kaydedilmiş arama yapılandırmalarınızı yönetin", + "Manage your system configurations and settings": "Sistem yapılandırmalarınızı ve ayarlarınızı yönetin", + "Managed": "Yönetilen", + "Mappings": "Eşlemeler", + "Mass validation completed successfully": "Toplu doğrulama başarıyla tamamlandı", + "Mass validation failed: {error}": "Toplu doğrulama başarısız: {error}", + "Max Files (0 = all)": "Maks Dosyalar (0 = tümü)", + "Max Length": "Maks Uzunluk", + "Max Length:": "Maks Uzunluk:", + "Max Objects (0 = all)": "Maks Nesneler (0 = tümü)", + "Max Retries": "Maks Yeniden Deneme", + "Max Shards/Node": "Maks Parça/Düğüm", + "Max Tokens": "Maks Token", + "Max execution time (ms)": "Maks yürütme süresi (ms)", + "Max ms": "Maks ms", + "Max result count": "Maks sonuç sayısı", + "Max results": "Maks sonuçlar", + "Maximum": "Maksimum", + "Maximum File Size (MB)": "Maksimum Dosya Boyutu (MB)", + "Maximum Nesting Depth": "Maksimum İç İçe Geçme Derinliği", + "Maximum length": "Maksimum uzunluk", + "Maximum length constraint is missing": "Maksimum uzunluk kısıtlaması eksik", + "Maximum length is too restrictive": "Maksimum uzunluk çok kısıtlayıcı", + "Maximum number of files to process. Set to 0 to process all files.": "İşlenecek maksimum dosya sayısı. Tüm dosyaları işlemek için 0 olarak ayarlayın.", + "Maximum number of items": "Maksimum öğe sayısı", + "Maximum number of objects to process. Set to 0 to process all objects.": "İşlenecek maksimum nesne sayısı. Tüm nesneleri işlemek için 0 olarak ayarlayın.", + "Maximum number of retry attempts for failed deliveries": "Başarısız teslimatlar için maksimum yeniden deneme girişimi sayısı", + "Maximum time to wait before committing changes": "Değişiklikleri kaydetmeden önce beklenecek maksimum süre", + "Maximum tokens to generate": "Oluşturulacak maksimum token sayısı", + "Maximum value": "Maksimum değer", + "Maximum value constraint is missing": "Maksimum değer kısıtlaması eksik", + "Maximum value is too restrictive": "Maksimum değer çok kısıtlayıcı", + "Medium": "Orta", + "Medium Confidence": "Orta Güven", + "Member removed successfully": "Üye başarıyla kaldırıldı", + "Members": "Üyeler", + "Members ({count})": "Üyeler ({count})", + "Members of selected groups can access this view": "Seçilen grupların üyeleri bu görünüme erişebilir", + "Members:": "Üyeler:", + "Memory prediction calculated successfully": "Bellek tahmini başarıyla hesaplandı", + "Message content is required": "Mesaj içeriği gereklidir", + "Message does not belong to this conversation": "Mesaj bu konuşmaya ait değil", + "Message not found": "Mesaj bulunamadı", + "Metadata": "Meta Veri", + "Metadata Filters": "Meta Veri Filtreleri", + "Method": "Yöntem", + "Method*": "Yöntem*", + "Method: {method}": "Yöntem: {method}", + "Min Length": "Min Uzunluk", + "Min execution time (ms)": "Min yürütme süresi (ms)", + "Min ms": "Min ms", + "Min result count": "Min sonuç sayısı", + "Min results": "Min sonuçlar", + "Minimum": "Minimum", + "Minimum length": "Minimum uzunluk", + "Minimum number of items": "Minimum öğe sayısı", + "Minimum value": "Minimum değer", + "Minimum value constraint is missing": "Minimum değer kısıtlaması eksik", + "Minimum value is too restrictive": "Minimum değer çok kısıtlayıcı", + "Missing conversation or agentUuid": "Konuşma veya agentUuid eksik", + "Missing conversationId": "conversationId eksik", + "Missing message": "Mesaj eksik", + "Missing object fields created successfully": "Eksik nesne alanları başarıyla oluşturuldu", + "Mode:": "Mod:", + "Model": "Model", + "Model Name": "Model Adı", + "Modified": "Değiştirildi", + "Monthly": "Aylık", + "More sources (10-20):": "Daha fazla kaynak (10-20):", + "Most Active Objects": "En Aktif Nesneler", + "Multiple Of": "Katı", + "Multiple of": "Katı", + "Multiple search trail deletion not implemented yet": "Çoklu arama izi silme henüz uygulanmadı", + "Multitenancy": "Çok Kiracılık", + "Multitenancy settings updated successfully": "Çok kiracılık ayarları başarıyla güncellendi", + "My Account": "Hesabım", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Ad", + "Name *": "Ad *", + "Name*": "Ad*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "İsimler önbelleği başarıyla ısıtıldı: {time} sürede {count} isim yüklendi. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "İsimler önbelleği başarıyla ısıtıldı: {time} sürede {count} isim yüklendi. {message}", + "Names cache warmup completed": "İsimler önbelleği ısıtması tamamlandı", + "Naming pattern for copies:": "Kopyalar için adlandırma deseni:", + "New Collection Name": "Yeni Koleksiyon Adı", + "New Configuration": "Yeni Yapılandırma", + "New Conversation": "Yeni Konuşma", + "New Properties": "Yeni Özellikler", + "New Property": "Yeni Özellik", + "New User Group": "Yeni Kullanıcı Grubu", + "New Value": "Yeni Değer", + "New activity": "Yeni etkinlik", + "New password": "Yeni şifre", + "New processing activity": "Yeni işleme faaliyeti", + "New verwerkingsactiviteit": "Yeni verwerkingsactiviteit", + "Next": "İleri", + "Nextcloud Groups": "Nextcloud Grupları", + "No": "Hayır", + "No API tokens.": "API token'ı yok.", + "No ConfigSets found": "ConfigSet bulunamadı", + "No Deck cards linked to this object": "Bu nesneye bağlantılı Deck kartı yok", + "No GitHub token configured": "GitHub token'ı yapılandırılmamış", + "No GitHub token provided": "GitHub token'ı sağlanmamış", + "No GitLab token provided": "GitLab token'ı sağlanmamış", + "No Organisation": "Kuruluş Yok", + "No active filters": "Etkin filtre yok", + "No activity data available": "Etkinlik verisi mevcut değil", + "No activity found.": "Etkinlik bulunamadı.", + "No agents available": "Mevcut ajan yok", + "No agents found": "Ajan bulunamadı", + "No app store cache exists yet": "Henüz uygulama mağazası önbelleği yok", + "No applications are available.": "Mevcut uygulama yok.", + "No applications found": "Uygulama bulunamadı", + "No approval steps for this object.": "Bu nesne için onay adımı yok.", + "No archived conversations": "Arşivlenmiş konuşma yok", + "No audit trail entries found": "Denetim izi girişi bulunamadı", + "No changes recorded": "Değişiklik kaydedilmedi", + "No chunks to vectorize": "Vektörleştirilecek parça yok", + "No collections found": "Koleksiyon bulunamadı", + "No configuration data": "Yapılandırma verisi yok", + "No configuration selected": "Yapılandırma seçilmedi", + "No configurations are available.": "Mevcut yapılandırma yok.", + "No configurations found": "Yapılandırma bulunamadı", + "No configurations found for this application.": "Bu uygulama için yapılandırma bulunamadı.", + "No contacts linked to this object": "Bu nesneye bağlantılı kişi yok", + "No conversations yet. Create a new one to get started!": "Henüz konuşma yok. Başlamak için yeni bir tane oluşturun!", + "No dashboards yet": "Henüz gösterge paneli yok", + "No data available": "Veri mevcut değil", + "No data available for chart": "Grafik için veri mevcut değil", + "No deleted items found": "Silinmiş öğe bulunamadı", + "No deletion data available": "Silme verisi mevcut değil", + "No description found": "Açıklama bulunamadı", + "No description provided": "Açıklama sağlanmadı", + "No emails linked to this object": "Bu nesneye bağlantılı e-posta yok", + "No endpoints defined": "Uç nokta tanımlanmamış", + "No entities detected for this email.": "Bu e-posta için varlık algılanmadı.", + "No entities detected in this file. Run text extraction first.": "Bu dosyada varlık algılanmadı. Önce metin çıkarmayı çalıştırın.", + "No entities found": "Varlık bulunamadı", + "No entities have been detected yet": "Henüz varlık algılanmadı", + "No events found": "Etkinlik bulunamadı", + "No events linked to this object": "Bu nesneye bağlantılı etkinlik yok", + "No expired audit trails found to clear": "Temizlenecek süresi dolmuş denetim izi bulunamadı", + "No expired search trails found to clear": "Temizlenecek süresi dolmuş arama izi bulunamadı", + "No extraction data available for this file": "Bu dosya için çıkarma verisi mevcut değil", + "No files found": "Dosya bulunamadı", + "No files have been extracted yet": "Henüz dosya çıkarılmadı", + "No files selected": "Dosya seçilmedi", + "No files to process": "İşlenecek dosya yok", + "No files to reindex": "Yeniden dizinlenecek dosya yok", + "No filters are currently active. This will delete ALL audit trail entries!": "Şu anda etkin filtre yok. Bu, TÜM denetim izi girişlerini siler!", + "No groups found. Try a different search.": "Grup bulunamadı. Farklı bir arama deneyin.", + "No label": "Etiket yok", + "No labels": "Etiket yok", + "No linked objects": "Bağlantılı nesne yok", + "No log entries found": "Günlük girişi bulunamadı", + "No logs are available for this configuration.": "Bu yapılandırma için günlük mevcut değil.", + "No logs are available for this source.": "Bu kaynak için günlük mevcut değil.", + "No logs found": "Günlük bulunamadı", + "No matches": "Eşleşme yok", + "No mismatched fields found - SOLR schema is properly configured": "Eşleşmeyen alan bulunamadı - SOLR şeması düzgün şekilde yapılandırılmış", + "No objects are currently selected for deletion.": "Şu anda silme için seçili nesne yok.", + "No objects are currently selected for permanent deletion.": "Şu anda kalıcı silme için seçili nesne yok.", + "No objects are currently selected for restoration.": "Şu anda geri yükleme için seçili nesne yok.", + "No objects found": "Nesne bulunamadı", + "No objects found to vectorize": "Vektörleştirilecek nesne bulunamadı", + "No objects linked to this email": "Bu e-postaya bağlantılı nesne yok", + "No objects selected": "Nesne seçilmedi", + "No objects selected for deletion": "Silme için nesne seçilmedi", + "No objects selected for restoration": "Geri yükleme için nesne seçilmedi", + "No objects to delete": "Silinecek nesne yok", + "No objects to delete for schema {schema}": "{schema} şeması için silinecek nesne yok", + "No objects were permanently deleted": "Hiçbir nesne kalıcı olarak silinmedi", + "No organisations found": "Kuruluş bulunamadı", + "No personal data was found for this subject identifier.": "Bu özne tanımlayıcısı için kişisel veri bulunamadı.", + "No processing activities yet": "Henüz işleme faaliyeti yok", + "No properties found": "Özellik bulunamadı", + "No properties match your filters.": "Filtrelerinizle eşleşen özellik yok.", + "No purge date set": "Temizleme tarihi ayarlanmadı", + "No register data available": "Kayıt verisi mevcut değil", + "No register objects reference this file": "Bu dosyaya atıfta bulunan kayıt nesnesi yok", + "No registers found": "Kayıt bulunamadı", + "No registers found for this application.": "Bu uygulama için kayıt bulunamadı.", + "No registers found. Create a register to configure permissions.": "Kayıt bulunamadı. İzinleri yapılandırmak için kayıt oluşturun.", + "No related cases found for this sender": "Bu gönderici için ilgili durum bulunamadı", + "No related entities": "İlgili varlık yok", + "No relations found": "İlişki bulunamadı", + "No request body available": "Mevcut istek gövdesi yok", + "No response body available": "Mevcut yanıt gövdesi yok", + "No saved views yet. Create one in the Search tab!": "Henüz kaydedilmiş görünüm yok. Arama sekmesinde bir tane oluşturun!", + "No saved views yet. create one in the search tab!": "Henüz kaydedilmiş görünüm yok. Arama sekmesinde bir tane oluşturun!", + "No scan run yet": "Henüz tarama çalıştırılmadı", + "No schema data available": "Şema verisi mevcut değil", + "No schema selected for exploration": "Keşif için şema seçilmedi", + "No schemas configured for mail linking.": "Posta bağlantısı için yapılandırılmış şema yok.", + "No schemas found": "Şema bulunamadı", + "No schemas found for this application.": "Bu uygulama için şema bulunamadı.", + "No search terms data available": "Arama terimi verisi mevcut değil", + "No search trail entries found": "Arama izi girişi bulunamadı", + "No sources are available.": "Mevcut kaynak yok.", + "No sources found": "Kaynak bulunamadı", + "No synchronizations found": "Senkronizasyon bulunamadı", + "No templates found": "Şablon bulunamadı", + "No templates have been created yet": "Henüz şablon oluşturulmadı", + "No tools available. Tools can be registered by installed apps.": "Mevcut araç yok. Araçlar yüklü uygulamalar tarafından kaydedilebilir.", + "No users in this organisation.": "Bu kuruluşta kullanıcı yok.", + "No valid magic-mapped register+schema combinations found": "Geçerli sihirli eşlenmiş kayıt+şema kombinasyonu bulunamadı", + "No views found. Create views first before configuring vectorization.": "Görünüm bulunamadı. Vektörleştirmeyi yapılandırmadan önce önce görünümler oluşturun.", + "No views match your search": "Aramanızla eşleşen görünüm yok", + "No webhooks found": "Webhook bulunamadı", + "No webhooks have been configured yet": "Henüz webhook yapılandırılmadı", + "No workflows found in this project. Create workflows in the n8n editor.": "Bu projede iş akışı bulunamadı. n8n düzenleyicisinde iş akışları oluşturun.", + "None": "Hiçbiri", + "Not Found": "Bulunamadı", + "Not extracted": "Çıkarılmadı", + "Not helpful": "Yararlı değil", + "Not in use": "Kullanımda değil", + "Note:": "Not:", + "Notifications": "Bildirimler", + "Number Constraints": "Sayı Kısıtlamaları", + "Number of Sources": "Kaynak Sayısı", + "Number of chunks to vectorize in one API call (1-100).": "Bir API çağrısında vektörleştirilecek parça sayısı (1-100).", + "Number of objects to process in each batch (1-5000).": "Her toplu işte işlenecek nesne sayısı (1-5000).", + "Numeric Range:": "Sayısal Aralık:", + "Object": "Nesne", + "Object #{id}": "Nesne #{id}", + "Object Collection": "Nesne Koleksiyonu", + "Object Configuration:": "Nesne Yapılandırması:", + "Object Count Prediction": "Nesne Sayısı Tahmini", + "Object Extraction Mode": "Nesne Çıkarma Modu", + "Object ID": "Nesne Kimliği", + "Object Management": "Nesne Yönetimi", + "Object Text Extraction": "Nesne Metin Çıkarma", + "Object Vectorization": "Nesne Vektörleştirme", + "Object Warmup": "Nesne Isınması", + "Object changes in owned objects": "Sahip olunan nesnelerdeki değişiklikler", + "Object collection not configured": "Nesne koleksiyonu yapılandırılmamış", + "Object metadata incomplete for file upload": "Dosya yükleme için nesne meta verisi eksik", + "Object not found": "Nesne bulunamadı", + "Object permanently deleted": "Nesne kalıcı olarak silindi", + "Object restored successfully": "Nesne başarıyla geri yüklendi", + "Object settings updated successfully": "Nesne ayarları başarıyla güncellendi", + "Object sources": "Nesne kaynakları", + "Object successfully deleted": "Nesne başarıyla silindi", + "Object successfully modified": "Nesne başarıyla değiştirildi", + "Object unlocked successfully": "Nesnenin kilidi başarıyla açıldı", + "Object vectorization configuration saved successfully": "Nesne vektörleştirme yapılandırması başarıyla kaydedildi", + "Object vectorized successfully": "Nesne başarıyla vektörleştirildi", + "Object:": "Nesne:", + "Objects": "Nesneler", + "Objects Analyzed": "Analiz Edilen Nesneler", + "Objects Distribution": "Nesne Dağılımı", + "Objects being analyzed": "Analiz edilen nesneler", + "Objects by Register": "Kayda Göre Nesneler", + "Objects by Schema": "Şemaya Göre Nesneler", + "Objects deletion completed for {schema}": "{schema} için nesne silme tamamlandı", + "Objects deletion completed successfully": "Nesne silme başarıyla tamamlandı", + "Objects in schema": "Şemadaki nesneler", + "Objects successfully deleted": "Nesneler başarıyla silindi", + "Objects to Process:": "İşlenecek Nesneler:", + "Objects to be analyzed": "Analiz edilecek nesneler", + "Objects to be deleted": "Silinecek nesneler", + "Objects to be validated": "Doğrulanacak nesneler", + "Objects will be serialized as JSON text before vectorization": "Nesneler vektörleştirmeden önce JSON metni olarak serileştirilecek", + "Objects will be soft deleted and moved to the": "Nesneler geçici silinecek ve taşınacak", + "Old Value": "Eski Değer", + "Oldest Item (days)": "En Eski Öğe (gün)", + "Ollama Chat Settings": "Ollama Sohbet Ayarları", + "Ollama Embedding Configuration": "Ollama Gömülüm Yapılandırması", + "Ollama URL": "Ollama URL", + "Only With Changes": "Yalnızca Değişikliklerle", + "Only members of selected groups can access this organisation": "Yalnızca seçilen grupların üyeleri bu kuruluşa erişebilir", + "Open Nextcloud App Store": "Nextcloud Uygulama Mağazasını Aç", + "Open OpenRegister sidebar": "OpenRegister kenar çubuğunu aç", + "Open Register Objects": "Kayıt Nesnelerini Aç", + "Open folder": "Klasörü aç", + "Open in OpenRegister": "OpenRegister'da Aç", + "Open n8n Editor": "n8n Düzenleyiciyi Aç", + "OpenAI Chat Settings": "OpenAI Sohbet Ayarları", + "OpenAI Embedding Configuration": "OpenAI Gömülüm Yapılandırması", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "OpenRegister Ayarları", + "Optimizing search performance...": "Arama performansı optimize ediliyor...", + "Optional URL-friendly identifier": "İsteğe bağlı URL dostu tanımlayıcı", + "Optional description for this facet": "Bu faset için isteğe bağlı açıklama", + "Optional end date property": "İsteğe bağlı bitiş tarihi özelliği", + "Optional event description template": "İsteğe bağlı etkinlik açıklama şablonu", + "Optional location property": "İsteğe bağlı konum özelliği", + "Optional webhook secret for signature verification": "İmza doğrulama için isteğe bağlı webhook gizliliği", + "Or": "Veya", + "Order": "Sıra", + "Organisation": "Kuruluş", + "Organisation Configuration": "Kuruluş Yapılandırması", + "Organisation ID": "Kuruluş Kimliği", + "Organisation Members": "Kuruluş Üyeleri", + "Organisation Statistics": "Kuruluş İstatistikleri", + "Organisation created successfully": "Kuruluş başarıyla oluşturuldu", + "Organisation membership changes": "Kuruluş üyeliği değişiklikleri", + "Organisation name is required": "Kuruluş adı gereklidir", + "Organisation settings saved successfully": "Kuruluş ayarları başarıyla kaydedildi", + "Organisation successfully created": "Kuruluş başarıyla oluşturuldu", + "Organisation successfully updated": "Kuruluş başarıyla güncellendi", + "Organisational measures": "Organizasyonel önlemler", + "Organisations": "Kuruluşlar", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Kuruluş", + "Organization ID (Optional)": "Kuruluş Kimliği (İsteğe Bağlı)", + "Organizations": "Kuruluşlar", + "Orphaned Items": "Sahipsiz Öğeler", + "Other": "Diğer", + "Output Mapping": "Çıkış Eşlemesi", + "Overview": "Genel Bakış", + "Owner": "Sahip", + "Owner:": "Sahip:", + "PDF": "PDF", + "PII hits": "KBB eşleşmeleri", + "PII rows": "KBB satırları", + "Page {current} of {total}": "{total} sayfanın {current}. sayfası", + "Parallel Mode (Faster, more resource intensive)": "Paralel Mod (Daha hızlı, daha fazla kaynak tüketen)", + "Parallel:": "Paralel:", + "Password": "Şifre", + "Password changes are not supported by your authentication provider.": "Kimlik doğrulama sağlayıcınız şifre değişikliklerini desteklemiyor.", + "Password for SOLR authentication (optional)": "SOLR kimlik doğrulaması için şifre (isteğe bağlı)", + "Password updated successfully": "Şifre başarıyla güncellendi", + "Path": "Yol", + "Path in repository": "Depodaki yol", + "Path where the configuration file will be saved in the repository": "Yapılandırma dosyasının depoya kaydedileceği yol", + "Path where the register OAS file will be saved in the repository": "Kayıt OAS dosyasının depoya kaydedileceği yol", + "Pattern (regex)": "Desen (regex)", + "Pattern Issue": "Desen Sorunu", + "Pattern constraint is missing": "Desen kısıtlaması eksik", + "Patterns:": "Desenler:", + "Pending": "Beklemede", + "Permanent Delete Mode": "Kalıcı Silme Modu", + "Permanently Delete": "Kalıcı Olarak Sil", + "Permanently Delete ({count})": "Kalıcı Olarak Sil ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Tüm {count} geçici silinmiş nesneyi kalıcı olarak silin. Bu işlem geri alınamaz!", + "Permanently delete already soft-deleted objects": "Zaten geçici silinmiş nesneleri kalıcı olarak silin", + "Permanently delete this conversation?": "Bu konuşma kalıcı olarak silinsin mi?", + "Permission Matrix": "İzin Matrisi", + "Person": "Kişi", + "Personal Data": "Kişisel Veri", + "Personal Data Export": "Kişisel Veri Dışa Aktarımı", + "Persons": "Kişiler", + "Phone": "Telefon", + "Phone Numbers": "Telefon Numaraları", + "Photos": "Fotoğraflar", + "Please create an agent in the": "Lütfen bir ajan oluşturun", + "Please select a user": "Lütfen bir kullanıcı seçin", + "Please select an agent to continue": "Devam etmek için lütfen bir ajan seçin", + "Please select an organisation": "Lütfen bir kuruluş seçin", + "Please select at least one item to import": "İçe aktarmak için lütfen en az bir öğe seçin", + "Please select registers or schemas first": "Lütfen önce kayıtları veya şemaları seçin", + "Please select which register and schema to use for the new object": "Yeni nesne için hangi kayıt ve şemanın kullanılacağını seçin", + "Please try again later.": "Lütfen daha sonra tekrar deneyin.", + "Please wait while we fetch your agents.": "Ajanlarınız getirilirken lütfen bekleyin.", + "Please wait while we fetch your applications.": "Uygulamalarınız getirilirken lütfen bekleyin.", + "Please wait while we fetch your configurations.": "Yapılandırmalarınız getirilirken lütfen bekleyin.", + "Please wait while we fetch your deleted items.": "Silinen öğeleriniz getirilirken lütfen bekleyin.", + "Please wait while we fetch your sources.": "Kaynaklarınız getirilirken lütfen bekleyin.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "SOLR koleksiyonu kalıcı olarak silinirken lütfen bekleyin. Bu birkaç dakika sürebilir.", + "Polls": "Anketler", + "Popular Search Terms": "Popüler Arama Terimleri", + "Port": "Port", + "Portabiliteit (Art 20)": "Portabiliteit (Madde 20)", + "Preferences saved": "Tercihler kaydedildi", + "Prerequisites": "Ön Koşullar", + "Press Enter to send, Shift+Enter for new line": "Göndermek için Enter'a, yeni satır için Shift+Enter'a basın", + "Press enter to send, shift+enter for new line": "Göndermek için enter'a, yeni satır için shift+enter'a basın", + "Prettify": "Güzelleştir", + "Preview erasure": "Silme önizlemesi", + "Preview:": "Önizleme:", + "Previous": "Önceki", + "Private": "Özel", + "Private Agent (Default)": "Özel Ajan (Varsayılan)", + "Private agents": "Özel ajanlar", + "Process Name (optional)": "Süreç Adı (isteğe bağlı)", + "Processed {indexed} files, {failed} failed": "{indexed} dosya işlendi, {failed} başarısız", + "Processes chunks in batches with simulated parallelism.": "Parçaları simüle edilmiş paralellikle toplu olarak işler.", + "Processes file chunks sequentially (safest).": "Dosya parçalarını sırayla işler (en güvenli).", + "Processes objects in chunks with simulated parallelism.": "Nesneleri simüle edilmiş paralellikle parçalar halinde işler.", + "Processes objects sequentially (safest).": "Nesneleri sırayla işler (en güvenli).", + "Processing": "İşleniyor", + "Processing Limits": "İşleme Sınırları", + "Processing...": "İşleniyor...", + "Project initialized successfully": "Proje başarıyla başlatıldı", + "Project not found. Please initialize first.": "Proje bulunamadı. Lütfen önce başlatın.", + "Properties": "Özellikler", + "Property Behaviors": "Özellik Davranışları", + "Property Configuration:": "Özellik Yapılandırması:", + "Property Filters": "Özellik Filtreleri", + "Property Title": "Özellik Başlığı", + "Property Type": "Özellik Türü", + "Property can be improved": "Özellik geliştirilebilir", + "Property could not be added": "Özellik eklenemedi", + "Property could not be updated": "Özellik güncellenemedi", + "Property name of inversed relation": "Ters ilişkinin özellik adı", + "Property successfully added": "Özellik başarıyla eklendi", + "Property successfully updated": "Özellik başarıyla güncellendi", + "Protected": "Korumalı", + "Provider": "Sağlayıcı", + "Provider is required for testing": "Test için sağlayıcı gereklidir", + "Public": "Genel", + "Public View": "Genel Görünüm", + "Public views can be accessed by anyone in the system": "Genel görünümlere sistemdeki herkes erişebilir", + "Publish": "Yayımla", + "Publish Configuration to GitHub": "Yapılandırmayı GitHub'a Yayımla", + "Publish OAS": "OAS'yi Yayımla", + "Publish Objects": "Nesneleri Yayımla", + "Publish Register OAS to GitHub": "Kayıt OAS'sini GitHub'a Yayımla", + "Publish all objects in this schema": "Bu şemadaki tüm nesneleri yayımla", + "Publish objects to make them publicly available": "Nesneleri herkese açık yapmak için yayımla", + "Published": "Yayımlandı", + "Published:": "Yayımlandı:", + "Publishing...": "Yayımlanıyor...", + "Purge": "Temizle", + "Purge Date": "Temizleme Tarihi", + "Purge {count} object from database": "Veritabanından {count} nesneyi temizle", + "Purge {count} objects from database": "Veritabanından {count} nesneyi temizle", + "Purpose limitation *": "Amaç sınırlaması *", + "Push Notifications": "Anlık Bildirimler", + "Query Complexity Distribution": "Sorgu Karmaşıklığı Dağılımı", + "Query Help": "Sorgu Yardımı", + "Query parameter is required": "Sorgu parametresi gereklidir", + "Quota": "Kota", + "RAG": "RAG", + "RAG Configuration": "RAG Yapılandırması", + "RBAC": "RBAC", + "RBAC settings updated successfully": "RBAC ayarları başarıyla güncellendi", + "Rate limits cleared successfully": "Hız sınırları başarıyla temizlendi", + "Raw Changes Data": "Ham Değişiklik Verisi", + "Re-vectorize on object update": "Nesne güncellemesinde yeniden vektörleştir", + "Read": "Oku", + "Real-time push notification status via notify_push": "notify_push aracılığıyla gerçek zamanlı anlık bildirim durumu", + "Realtime push active": "Gerçek zamanlı anlık iletim etkin", + "Realtime push not available — the notify_push app is not installed": "Gerçek zamanlı anlık iletim mevcut değil — notify_push uygulaması yüklü değil", + "Reason": "Neden", + "Reason (optional)": "Neden (isteğe bağlı)", + "Rebase operation completed successfully": "Yeniden taban alma işlemi başarıyla tamamlandı", + "Rebase operation failed: {error}": "Yeniden taban alma işlemi başarısız: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Öneriler:", + "Recommended Type:": "Önerilen Tür:", + "Recommended: 5 sources": "Önerilen: 5 kaynak", + "Refresh": "Yenile", + "Refresh Data": "Veriyi Yenile", + "Refresh Stats": "İstatistikleri Yenile", + "Refresh Workflows": "İş Akışlarını Yenile", + "Refresh dashboard": "Gösterge panelini yenile", + "Refresh database info": "Veritabanı bilgisini yenile", + "Register": "Kayıt", + "Register #{id}": "Kayıt #{id}", + "Register / Schema": "Kayıt / Şema", + "Register ID": "Kayıt Kimliği", + "Register Objects": "Kayıt Nesneleri", + "Register Overview": "Kayıt Genel Bakışı", + "Register Statistics": "Kayıt İstatistikleri", + "Register Totals": "Kayıt Toplamları", + "Register and schema are required": "Kayıt ve şema gereklidir", + "Register annotation": "Kayıt ek açıklaması", + "Register depublished successfully": "Kayıt başarıyla yayından kaldırıldı", + "Register not found": "Kayıt bulunamadı", + "Register objects": "Kayıt nesneleri", + "Register objects deletion completed successfully": "Kayıt nesneleri silme başarıyla tamamlandı", + "Register published successfully": "Kayıt başarıyla yayımlandı", + "Register/Schema Usage": "Kayıt/Şema Kullanımı", + "Register:": "Kayıt:", + "Registers": "Kayıtlar", + "Reindex": "Yeniden Dizinle", + "Reindex completed": "Yeniden dizinleme tamamlandı", + "Reindex completed successfully": "Yeniden dizinleme başarıyla tamamlandı", + "Reindex completed! Processed {count} objects in {duration}s": "Yeniden dizinleme tamamlandı! {duration} saniyede {count} nesne işlendi", + "Reindex failed": "Yeniden dizinleme başarısız", + "Reindex failed: %s": "Yeniden dizinleme başarısız: %s", + "Reject": "Reddet", + "Rejected": "Reddedildi", + "Related Cases": "İlgili Durumlar", + "Relations": "İlişkiler", + "Remote Version": "Uzak Sürüm", + "Remove": "Kaldır", + "Remove Deck card": "Deck kartını kaldır", + "Remove avatar": "Avatar kaldır", + "Remove contact": "Kişi kaldır", + "Remove filter": "Filtreyi kaldır", + "Remove from favorites": "Favorilerden kaldır", + "Remove from list": "Listeden kaldır", + "Remove group": "Grubu kaldır", + "Remove link": "Bağlantıyı kaldır", + "Remove link to {name}": "{name} öğesine bağlantıyı kaldır", + "Remove link to {name}?": "{name} öğesine bağlantı kaldırılsın mı?", + "Remove link to {title}": "{title} öğesine bağlantıyı kaldır", + "Remove oneOf entry": "oneOf girişini kaldır", + "Remove property after write-back": "Geri yazmadan sonra özelliği kaldır", + "Remove {title}": "{title} öğesini kaldır", + "Remove {userId} from this organisation?": "{userId} bu kuruluştan kaldırılsın mı?", + "Removed": "Kaldırıldı", + "Removed from favorites": "Favorilerden kaldırıldı", + "Removing schema {schema}...": "{schema} şeması kaldırılıyor...", + "Rename Conversation": "Konuşmayı Yeniden Adlandır", + "Rename conversation": "Konuşmayı yeniden adlandır", + "Replicas": "Replikalar", + "Reports": "Raporlar", + "Repository": "Depo", + "Request Body": "İstek Gövdesi", + "Request Data": "İstek Verisi", + "Request Details": "İstek Ayrıntıları", + "Request Quota (per day)": "İstek Kotası (günlük)", + "Request account deactivation": "Hesap devre dışı bırakma talebinde bulun", + "Request account deactivation. This will notify administrators for review.": "Hesap devre dışı bırakma talebinde bulunun. Bu, yöneticilere inceleme için bildirim gönderecek.", + "Request body must be JSON with a \"query\" field": "İstek gövdesi \"query\" alanına sahip JSON olmalıdır", + "Request timeout in seconds": "Saniye cinsinden istek zaman aşımı", + "Requested at": "Talep edildi:", + "Required": "Gerekli", + "Required Token Scopes": "Gerekli Token Kapsamları", + "Required field": "Zorunlu alan", + "Required status is inconsistent": "Gerekli durum tutarsız", + "Requires Dolphin AI for OCR text extraction": "OCR metin çıkarma için Dolphin AI gerektirir", + "Rerun Search": "Aramayı Yeniden Çalıştır", + "Rerunning search: {searchTerm}": "Arama yeniden çalıştırılıyor: {searchTerm}", + "Reset Changes": "Değişiklikleri Sıfırla", + "Reset Filters": "Filtreleri Sıfırla", + "Resource Quotas": "Kaynak Kotaları", + "Response Body": "Yanıt Gövdesi", + "Response Details": "Yanıt Ayrıntıları", + "Response Time:": "Yanıt Süresi:", + "Restore": "Geri Yükle", + "Restore conversation": "Konuşmayı geri yükle", + "Restore or permanently delete items": "Öğeleri geri yükleyin veya kalıcı olarak silin", + "Restore {count} object": "{count} nesneyi geri yükle", + "Restore {count} objects": "{count} nesneyi geri yükle", + "Result Count Range": "Sonuç Sayısı Aralığı", + "Results": "Sonuçlar", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Saklama süresi (ISO-8601 süresi, örn. P10Y, P30D)", + "Retention settings updated successfully": "Saklama ayarları başarıyla güncellendi", + "Retrieve all objects for this schema": "Bu şema için tüm nesneleri al", + "Retry": "Yeniden Dene", + "Retry Failed Extractions": "Başarısız Çıkarmaları Yeniden Dene", + "Retry Policy": "Yeniden Deneme Politikası", + "Retry completed": "Yeniden deneme tamamlandı", + "Retry upload": "Yüklemeyi yeniden dene", + "Retrying...": "Yeniden deneniyor...", + "Revoke": "İptal Et", + "Right": "Sağ", + "Risk Level": "Risk Seviyesi", + "Risk level": "Risk seviyesi", + "Roles and Permissions": "Roller ve İzinler", + "Roles updated successfully": "Roller başarıyla güncellendi", + "Run compliance scan": "Uyumluluk taraması çalıştır", + "SOLR Connection Settings": "SOLR Bağlantı Ayarları", + "SOLR Index Warmup": "SOLR Dizin Isınması", + "SOLR Version:": "SOLR Sürümü:", + "SOLR actions menu": "SOLR işlemleri menüsü", + "SOLR base path (usually /solr)": "SOLR temel yolu (genellikle /solr)", + "SOLR collection deleted successfully": "SOLR koleksiyonu başarıyla silindi", + "SOLR connection test failed: {error}": "SOLR bağlantı testi başarısız: {error}", + "SOLR connection test successful!": "SOLR bağlantı testi başarılı!", + "SOLR index warmup completed successfully": "SOLR dizin ısınması başarıyla tamamlandı", + "SOLR is disabled": "SOLR devre dışı", + "SOLR is not available or not configured": "SOLR mevcut değil veya yapılandırılmamış", + "SOLR logging disabled": "SOLR günlüğü devre dışı", + "SOLR logging enabled": "SOLR günlüğü etkin", + "SOLR search disabled": "SOLR araması devre dışı", + "SOLR search enabled": "SOLR araması etkin", + "SOLR server hostname or IP address": "SOLR sunucusu ana bilgisayar adı veya IP adresi", + "SOLR server port number (optional, defaults to 8983)": "SOLR sunucusu port numarası (isteğe bağlı, varsayılan 8983)", + "SOLR settings updated successfully": "SOLR ayarları başarıyla güncellendi", + "SOLR setup completed successfully": "SOLR kurulumu başarıyla tamamlandı", + "SOLR setup completed successfully!": "SOLR kurulumu başarıyla tamamlandı!", + "SOLR setup error: %s": "SOLR kurulum hatası: %s", + "SOLR setup failed": "SOLR kurulumu başarısız", + "SOLR setup failed - check logs": "SOLR kurulumu başarısız - günlükleri kontrol edin", + "SOLR setup failed. Please check the configuration and try again.": "SOLR kurulumu başarısız. Lütfen yapılandırmayı kontrol edip tekrar deneyin.", + "SOLR setup failed: {error}": "SOLR kurulumu başarısız: {error}", + "SOLR warmup failed: {error}": "SOLR ısınması başarısız: {error}", + "Sample Values:": "Örnek Değerler:", + "Save": "Kaydet", + "Save Configuration": "Yapılandırmayı Kaydet", + "Save Connection Settings": "Bağlantı Ayarlarını Kaydet", + "Save Roles": "Rolleri Kaydet", + "Save Settings": "Ayarları Kaydet", + "Save changes": "Değişiklikleri kaydet", + "Save current search as view": "Mevcut aramayı görünüm olarak kaydet", + "Save labels": "Etiketleri kaydet", + "Save tags for {name}": "{name} için etiketleri kaydet", + "Save the organisation first to manage users.": "Kullanıcıları yönetmek için önce kuruluşu kaydedin.", + "Saved Views": "Kaydedilmiş Görünümler", + "Saving...": "Kaydediliyor...", + "Scheduled": "Zamanlandı", + "Schema": "Şema", + "Schema ID": "Şema Kimliği", + "Schema Name": "Şema Adı", + "Schema Statistics": "Şema İstatistikleri", + "Schema annotation": "Şema ek açıklaması", + "Schema configuration updated successfully for {schema}": "{schema} için şema yapılandırması başarıyla güncellendi", + "Schema depublished successfully": "Şema başarıyla yayından kaldırıldı", + "Schema objects deletion completed successfully": "Şema nesneleri silme başarıyla tamamlandı", + "Schema published successfully": "Şema başarıyla yayımlandı", + "Schema reference ($ref)": "Şema referansı ($ref)", + "Schema successfully created": "Şema başarıyla oluşturuldu", + "Schema successfully updated": "Şema başarıyla güncellendi", + "Schema {schema} removed successfully": "{schema} şeması başarıyla kaldırıldı", + "Schema:": "Şema:", + "Schemas": "Şemalar", + "Scheme": "Şema", + "Search": "Ara", + "Search / Views": "Arama / Görünümler", + "Search / views": "Arama / görünümler", + "Search Activity": "Arama Etkinliği", + "Search Analytics": "Arama Analitiği", + "Search Configuration": "Arama Yapılandırması", + "Search Entities": "Varlıkları Ara", + "Search Files": "Dosyaları Ara", + "Search GitHub": "GitHub'da Ara", + "Search GitLab": "GitLab'da Ara", + "Search Method": "Arama Yöntemi", + "Search Mode": "Arama Modu", + "Search Query": "Arama Sorgusu", + "Search Results": "Arama Sonuçları", + "Search Term": "Arama Terimi", + "Search Trail Details": "Arama İzi Ayrıntıları", + "Search Trail Management": "Arama İzi Yönetimi", + "Search Trail Statistics": "Arama İzi İstatistikleri", + "Search Trails": "Arama İzleri", + "Search Views": "Görünümlerde Ara", + "Search Webhooks": "Webhook'larda Ara", + "Search agents...": "Ajanlarda ara...", + "Search applications...": "Uygulamalarda ara...", + "Search by file name or path": "Dosya adı veya yola göre ara", + "Search by name or URL": "Ada veya URL'ye göre ara", + "Search by title or UUID...": "Başlık veya UUID'ye göre ara...", + "Search by value": "Değere göre ara", + "Search configurations": "Yapılandırmalarda ara", + "Search data sources...": "Veri kaynaklarında ara...", + "Search groups...": "Gruplarda ara...", + "Search in Database Objects": "Veritabanı Nesnelerinde Ara", + "Search in Files": "Dosyalarda Ara", + "Search in Objects": "Nesnelerde Ara", + "Search objects": "Nesnelerde ara", + "Search objects...": "Nesnelerde ara...", + "Search property names...": "Özellik adlarında ara...", + "Search registers...": "Kayıtlarda ara...", + "Search schemas...": "Şemalarda ara...", + "Search trail deletion not implemented yet": "Arama izi silme henüz uygulanmadı", + "Search views...": "Görünümlerde ara...", + "Search {name}...": "{name} arama...", + "Searching...": "Aranıyor...", + "Secret": "Gizli", + "Secret key for HMAC signature generation (optional)": "HMAC imza oluşturma için gizli anahtar (isteğe bağlı)", + "Security": "Güvenlik", + "See organisations as a table": "Kuruluşları tablo olarak görüntüle", + "See organisations as cards": "Kuruluşları kartlar olarak görüntüle", + "Select AI Agent": "Yapay Zeka Ajanı Seç", + "Select Active Organisation": "Etkin Kuruluş Seç", + "Select All": "Tümünü Seç", + "Select ConfigSet": "ConfigSet Seç", + "Select File Types to Vectorize:": "Vektörleştirilecek Dosya Türlerini Seçin:", + "Select HTTP method": "HTTP yöntemi seç", + "Select Register and Schema": "Kayıt ve Şema Seç", + "Select Views to Vectorize:": "Vektörleştirilecek Görünümleri Seçin:", + "Select a Nextcloud group to add": "Eklenecek Nextcloud grubunu seçin", + "Select a branch": "Dal seçin", + "Select a date property": "Tarih özelliği seçin", + "Select a model or type a custom model name": "Model seçin veya özel model adı girin", + "Select a register": "Kayıt seçin", + "Select a register...": "Kayıt seçin...", + "Select a repository": "Depo seçin", + "Select a repository you have write access to": "Yazma erişiminiz olan bir depo seçin", + "Select a schema": "Şema seçin", + "Select a schema...": "Şema seçin...", + "Select a user": "Kullanıcı seçin", + "Select agent type": "Ajan türü seçin", + "Select all entities": "Tüm varlıkları seç", + "Select an AI Agent": "Bir Yapay Zeka Ajanı Seçin", + "Select an AI agent to begin chatting with your data.": "Verilerinizle sohbet etmeye başlamak için bir yapay zeka ajanı seçin.", + "Select an email to see linked objects": "Bağlantılı nesneleri görmek için bir e-posta seçin", + "Select backend": "Backend seçin", + "Select chat model": "Sohbet modeli seçin", + "Select collection for files": "Dosyalar için koleksiyon seçin", + "Select collection for objects": "Nesneler için koleksiyon seçin", + "Select configuration type...": "Yapılandırma türü seçin...", + "Select default organisation": "Varsayılan kuruluş seçin", + "Select entity {value}": "{value} varlığını seçin", + "Select event to listen to...": "Dinlenecek etkinlik seçin...", + "Select fields to display": "Görüntülenecek alanları seçin", + "Select files with the correct extension": "Doğru uzantılı dosyaları seçin", + "Select group": "Grup seçin", + "Select groups (optional)": "Grupları seçin (isteğe bağlı)", + "Select groups to notify...": "Bildirim gönderilecek grupları seçin...", + "Select groups with access to this agent": "Bu ajana erişimi olan grupları seçin", + "Select groups...": "Grupları seçin...", + "Select model": "Model seçin", + "Select one or more registers": "Bir veya daha fazla kayıt seçin", + "Select one or more schemas": "Bir veya daha fazla şema seçin", + "Select options...": "Seçenekleri seçin...", + "Select or create labels, or select \"No label\" to add files": "Etiketleri seçin veya oluşturun ya da dosya eklemek için \"Etiket yok\" seçeneğini seçin", + "Select owner application (optional)...": "Sahip uygulamasını seçin (isteğe bağlı)...", + "Select period": "Dönem seçin", + "Select property to send as payload": "Yük olarak gönderilecek özelliği seçin", + "Select provider": "Sağlayıcı seçin", + "Select registers and schemas to save a view": "Görünüm kaydetmek için kayıtları ve şemaları seçin", + "Select retry policy": "Yeniden deneme politikası seçin", + "Select role": "Rol seçin", + "Select schemas (empty = all schemas)": "Şemaları seçin (boş = tüm şemalar)", + "Select schemas to warm up": "Isıtılacak şemaları seçin", + "Select search mode": "Arama modu seçin", + "Select source type...": "Kaynak türü seçin...", + "Select target type": "Hedef tür seçin", + "Select the branch to publish to": "Yayımlanacak dalı seçin", + "Select the event this webhook should listen to": "Bu webhook'un dinlemesi gereken etkinliği seçin", + "Select views to filter data (optional)": "Verileri filtrelemek için görünümleri seçin (isteğe bağlı)", + "Select views to limit which data the agent can access": "Ajanın hangi verilere erişebileceğini sınırlamak için görünümleri seçin", + "Select views to vectorize:": "Vektörleştirilecek görünümleri seçin:", + "Select which data views the AI can search": "Yapay zekanın arayabileceği veri görünümlerini seçin", + "Select which property from the event should be used as the webhook payload data": "Etkinlikten hangi özelliğin webhook yük verisi olarak kullanılacağını seçin", + "Select which tools the AI can use to perform actions": "Yapay zekanın işlem gerçekleştirmek için kullanabileceği araçları seçin", + "Select {fieldName} values": "{fieldName} değerlerini seçin", + "Selected": "Seçildi", + "Selected Groups": "Seçilen Gruplar", + "Selected Objects ({count})": "Seçilen Nesneler ({count})", + "Selected audit trails deleted successfully": "Seçilen denetim izleri başarıyla silindi", + "Selected search trails deleted successfully": "Seçilen arama izleri başarıyla silindi", + "Selected users can access this view": "Seçilen kullanıcılar bu görünüme erişebilir", + "Send additional feedback": "Ek geri bildirim gönder", + "Send as CloudEvent": "CloudEvent olarak gönder", + "Send as cloudevent": "Cloudevent olarak gönder", + "Sensitive PII": "Hassas KBK", + "Serial Mode (Safer, slower)": "Seri Mod (Daha güvenli, daha yavaş)", + "Serial:": "Seri:", + "Server Configuration": "Sunucu Yapılandırması", + "Server Information": "Sunucu Bilgisi", + "Set as Active": "Etkin Olarak Ayarla", + "Set as active organisation": "Etkin kuruluş olarak ayarla", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "API kullanımı ve token tüketimi için sınırlar belirleyin. Sınırsız kaynaklar için 0 kullanın.", + "Settings": "Ayarlar", + "Settings updated successfully": "Ayarlar başarıyla güncellendi", + "Shards": "Parçalar", + "Share": "Paylaş", + "Share with Groups": "Gruplarla Paylaş", + "Share with Users": "Kullanıcılarla Paylaş", + "Show": "Göster", + "Show Filters": "Filtreleri Göster", + "Show less": "Daha az göster", + "Show only entries with changes": "Yalnızca değişiklikli girişleri göster", + "Showing {count} dashboard(s)": "{count} gösterge paneli gösteriliyor", + "Showing {count} processing activities": "{count} işleme faaliyeti gösteriliyor", + "Showing {showing} of {total} agents": "{total} ajandan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} applications": "{total} uygulamadan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} audit trail entries": "{total} denetim izi girişinden {showing} tanesi gösteriliyor", + "Showing {showing} of {total} configurations": "{total} yapılandırmadan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} deleted items": "{total} silinen öğeden {showing} tanesi gösteriliyor", + "Showing {showing} of {total} entities": "{total} varlıktan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} files": "{total} dosyadan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} log entries": "{total} günlük girişinden {showing} tanesi gösteriliyor", + "Showing {showing} of {total} search trail entries": "{total} arama izi girişinden {showing} tanesi gösteriliyor", + "Showing {showing} of {total} sources": "{total} kaynaktan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} templates": "{total} şablondan {showing} tanesi gösteriliyor", + "Showing {showing} of {total} webhooks": "{total} webhook'tan {showing} tanesi gösteriliyor", + "Showing {shown} of {total} organisations": "{total} kuruluştan {shown} tanesi gösteriliyor", + "Simple": "Basit", + "Size": "Boyut", + "Slug": "Slug", + "Soft Delete Mode": "Geçici Silme Modu", + "Soft Deleted Items": "Geçici Silinen Öğeler", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Bu şemadaki tüm nesneleri geçici sil ({active} etkin, {deleted} zaten silindi)", + "SolrCloud mode enabled": "SolrCloud modu etkin", + "Something went wrong while adding files": "Dosyalar eklenirken bir şeyler ters gitti", + "Source": "Kaynak", + "Source successfully updated": "Kaynak başarıyla güncellendi", + "Sources": "Kaynaklar", + "Sources:": "Kaynaklar:", + "Special Rights": "Özel Haklar", + "Standalone SOLR mode": "Bağımsız SOLR modu", + "Start Conversation": "Konuşmayı Başlat", + "Start Date Field": "Başlangıç Tarihi Alanı", + "Start Vectorization": "Vektörleştirmeyi Başlat", + "Start a conversation": "Konuşma başlat", + "Starting names cache warmup...": "İsimler önbelleği ısıtması başlatılıyor...", + "Starting validation for {schema}...": "{schema} için doğrulama başlatılıyor...", + "Starting {action} for {schema}...": "{schema} için {action} başlatılıyor...", + "Starting...": "Başlatılıyor...", + "Statistics": "İstatistikler", + "Statistics and Metrics": "İstatistikler ve Metrikler", + "Statistics refreshed": "İstatistikler yenilendi", + "Stats calculation not yet implemented": "İstatistik hesaplama henüz uygulanmadı", + "Status": "Durum", + "Status Code": "Durum Kodu", + "Step": "Adım", + "Storage": "Depolama", + "Storage Quota (MB)": "Depolama Kotası (MB)", + "String Constraints": "Dize Kısıtlamaları", + "Subject identifier (email, BSN, name, etc.)": "Özne tanımlayıcısı (e-posta, BSN, isim vb.)", + "Subscribe": "Abone Ol", + "Subscribed": "Abone Olundu", + "Success": "Başarı", + "Success Rate": "Başarı Oranı", + "Success Status": "Başarı Durumu", + "Successful": "Başarılı", + "Successfully added user to organisation: {name}": "Kullanıcı başarıyla kuruluşa eklendi: {name}", + "Successfully cleared {count} audit trails": "{count} denetim izi başarıyla temizlendi", + "Successfully cleared {count} blob storage objects": "{count} blob depolama nesnesi başarıyla temizlendi", + "Successfully cleared {count} search trails": "{count} arama izi başarıyla temizlendi", + "Successfully created {count} SOLR fields": "{count} SOLR alanı başarıyla oluşturuldu", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "{count} gömülüm başarıyla silindi. Lütfen verilerinizi yeniden vektörleştirin.", + "Successfully deleted {count} objects for {schema}": "{schema} için {count} nesne başarıyla silindi", + "Successfully fixed {count} SOLR field configurations": "{count} SOLR alan yapılandırması başarıyla düzeltildi", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Başarıyla içe aktarıldı: {registers} kayıt, {schemas} şema, {objects} nesne", + "Successfully joined organisation": "Kuruluşa başarıyla katıldı", + "Successfully permanently deleted {count} objects": "{count} nesne başarıyla kalıcı olarak silindi", + "Successfully processed {count} files!": "{count} dosya başarıyla işlendi!", + "Successfully restored {count} object": "{count} nesne başarıyla geri yüklendi", + "Successfully restored {count} objects": "{count} nesne başarıyla geri yüklendi", + "Successfully saved configuration for {count} facets!": "{count} faset için yapılandırma başarıyla kaydedildi!", + "Supported File Types": "Desteklenen Dosya Türleri", + "Switch Active Organisation": "Etkin Kuruluşu Değiştir", + "Switch Organisation": "Kuruluş Değiştir", + "Sync Table": "Tabloyu Senkronize Et", + "Synchronization Settings": "Senkronizasyon Ayarları", + "Syncing magic table for {schema}...": "{schema} için sihirli tablo senkronize ediliyor...", + "Syncs": "Senkronizasyonlar", + "System Default": "Sistem Varsayılanı", + "System Prompt": "Sistem İstemi", + "System announcements": "Sistem duyuruları", + "Table": "Tablo", + "Target ID": "Hedef Kimliği", + "Target Type*": "Hedef Tür*", + "Technical Description": "Teknik Açıklama", + "Technical Details": "Teknik Ayrıntılar", + "Technical description for developers and administrators": "Geliştiriciler ve yöneticiler için teknik açıklama", + "Technical measures": "Teknik önlemler", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Sıcaklık", + "Templates": "Şablonlar", + "Test": "Test", + "Test Chat": "Sohbeti Test Et", + "Test Connection": "Bağlantıyı Test Et", + "Test Embedding": "Gömülümü Test Et", + "Test endpoint executed successfully": "Test uç noktası başarıyla yürütüldü", + "Test webhook delivered successfully": "Test webhook'u başarıyla teslim edildi", + "Test webhook delivery failed": "Test webhook teslimi başarısız", + "Test webhook sent successfully": "Test webhook'u başarıyla gönderildi", + "Testing...": "Test ediliyor...", + "Text Chunk #{id}": "Metin Parçası #{id}", + "Text Extraction": "Metin Çıkarma", + "Text Extraction Engine": "Metin Çıkarma Motoru", + "Text chunks": "Metin parçaları", + "Text extracted successfully": "Metin başarıyla çıkarıldı", + "Text extraction disabled": "Metin çıkarma devre dışı", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Nextcloud Takvim uygulaması bu sunucuda yüklü değil veya etkin değil.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Nextcloud Kişiler uygulaması bu sunucuda yüklü değil veya etkin değil.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Nextcloud Deck uygulaması bu sunucuda yüklü değil veya etkin değil.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Nextcloud Mail uygulaması bu sunucuda yüklü değil veya etkin değil.", + "The URL where webhook events will be sent": "Webhook etkinliklerinin gönderileceği URL", + "The requested conversation does not exist": "İstenen konuşma mevcut değil", + "The saved view and all its search configuration": "Kaydedilmiş görünüm ve tüm arama yapılandırması", + "There are no audit trail entries matching your current filters.": "Mevcut filtrelerinizle eşleşen denetim izi girişi yok.", + "There are no deleted items matching your current filters.": "Mevcut filtrelerinizle eşleşen silinmiş öğe yok.", + "There are no search trail entries matching your current filters.": "Mevcut filtrelerinizle eşleşen arama izi girişi yok.", + "There are no webhook log entries matching your filters.": "Filtrelerinizle eşleşen webhook günlük girişi yok.", + "This action cannot be undone.": "Bu işlem geri alınamaz.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Bu işlem geri alınamaz. Hiçbir koleksiyonun bu ConfigSet'i kullanmadığından emin olun.", + "This action cannot be undone. Make sure no collections are using this configset.": "Bu işlem geri alınamaz. Hiçbir koleksiyonun bu configset'i kullanmadığından emin olun.", + "This action will submit a deactivation request to your administrators.": "Bu işlem, yöneticilerinize bir devre dışı bırakma isteği gönderecek.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "Bu ajanın yapılandırılabilir görünümü veya aracı yok, ancak aşağıdaki RAG ayarlarını yine de düzenleyebilirsiniz.", + "This analysis may take some time": "Bu analiz biraz zaman alabilir", + "This audit trail entry does not contain any change information.": "Bu denetim izi girişi herhangi bir değişiklik bilgisi içermiyor.", + "This configuration has no data defined.": "Bu yapılandırmada tanımlanmış veri yok.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Bu uç nokta kullanım dışı. Bunun yerine parça tabanlı uç noktaları kullanın.", + "This entity has no relations to objects or files": "Bu varlığın nesnelerle veya dosyalarla ilişkisi yok", + "This is a test webhook from OpenRegister": "Bu OpenRegister'dan bir test webhook'udur", + "This is not valid JSON": "Bu geçerli JSON değil", + "This key already exists on this schema": "Bu anahtar bu şemada zaten mevcut", + "This schema must use Magic Table configuration to sync": "Bu şemanın senkronize olabilmesi için Sihirli Tablo yapılandırması kullanması gerekir", + "This schema must use magic table configuration to sync": "Bu şemanın senkronize olabilmesi için sihirli tablo yapılandırması kullanması gerekir", + "This source has no associated registers.": "Bu kaynağın ilişkili kaydı yok.", + "This token will only be shown once. Copy it now.": "Bu token yalnızca bir kez gösterilecek. Şimdi kopyalayın.", + "This will copy the _default ConfigSet with the new name": "Bu, _default ConfigSet'ini yeni adla kopyalayacak", + "This will copy the _default configset with the new name": "Bu, _default configset'ini yeni adla kopyalayacak", + "This will permanently delete:": "Bu kalıcı olarak silecek:", + "Timeout (seconds)": "Zaman Aşımı (saniye)", + "Timestamp": "Zaman Damgası", + "Title": "Başlık", + "Title *": "Başlık *", + "Title Template": "Başlık Şablonu", + "Title*": "Başlık*", + "To (e.g. -1 year or 2025-12-31)": "Bitiş (örn. -1 yıl veya 2025-12-31)", + "To Date": "Bitiş Tarihi", + "To add files larger than or equal to 512MB, go to the": "512 MB veya daha büyük dosyalar eklemek için gidin", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Parça boyutunu veya stratejisini ayarlamak için Dosya Yapılandırması → İşleme Sınırları bölümüne gidin.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Parça boyutunu veya stratejisini ayarlamak için dosya yapılandırması → işleme sınırları bölümüne gidin.", + "To change the embedding provider or model, go to LLM Configuration.": "Gömülüm sağlayıcısını veya modelini değiştirmek için LLM Yapılandırması bölümüne gidin.", + "To change the embedding provider or model, go to LLM configuration.": "Gömülüm sağlayıcısını veya modelini değiştirmek için LLM yapılandırması bölümüne gidin.", + "To date": "Bitiş tarihi", + "Toggle search sidebar": "Arama kenar çubuğunu aç/kapat", + "Token Created": "Token Oluşturuldu", + "Token Quota (per request)": "Token Kotası (istek başına)", + "Token copied to clipboard": "Token panoya kopyalandı", + "Token name": "Token adı", + "Token revoked": "Token iptal edildi", + "Tools": "Araçlar", + "Tools respect the agent's own role and organization boundaries.": "Araçlar, ajanın kendi rolüne ve kuruluş sınırlarına saygı gösterir.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Araçlar, ajanın görünümlerine, izinlerine ve kuruluş sınırlarına saygı gösterir.", + "Top Deleters": "En Çok Silen Kullanıcılar", + "Total": "Toplam", + "Total Audit Trails": "Toplam Denetim İzleri", + "Total Chunks Available:": "Toplam Mevcut Parçalar:", + "Total Deleted Items": "Toplam Silinen Öğeler", + "Total Members": "Toplam Üyeler", + "Total Objects": "Toplam Nesneler", + "Total Objects in Database:": "Veritabanındaki Toplam Nesneler:", + "Total Organisations": "Toplam Kuruluşlar", + "Total Results": "Toplam Sonuçlar", + "Total Searches": "Toplam Aramalar", + "Total Size": "Toplam Boyut", + "Total Storage": "Toplam Depolama", + "Total events": "Toplam etkinlikler", + "Total:": "Toplam:", + "Totals": "Toplamlar", + "Try searching by UUID or with different keywords": "UUID ile veya farklı anahtar kelimelerle aramayı deneyin", + "Type": "Tür", + "Type \"DELETE COLLECTION\" to confirm:": "Onaylamak için \"DELETE COLLECTION\" yazın:", + "Type Issue": "Tür Sorunu", + "Type Variations:": "Tür Varyasyonları:", + "Type filter (optional, e.g. email)": "Tür filtresi (isteğe bağlı, örn. e-posta)", + "Type must be \"positive\" or \"negative\"": "Tür \"positive\" veya \"negative\" olmalıdır", + "Type to search for groups": "Grupları aramak için yazın", + "Type to search for objects...": "Nesneleri aramak için yazın...", + "Type to search for organisations": "Kuruluşları aramak için yazın", + "Type to search for users": "Kullanıcıları aramak için yazın", + "Type to search groups...": "Grupları aramak için yazın...", + "Type to search users...": "Kullanıcıları aramak için yazın...", + "Type to search...": "Aramak için yazın...", + "Type your username to confirm": "Onaylamak için kullanıcı adınızı yazın", + "URL": "URL", + "URL where Ollama is running": "Ollama'nın çalıştığı URL", + "UUID copied to clipboard": "UUID panoya kopyalandı", + "UUID:": "UUID:", + "Unchanged": "Değişmedi", + "Unique Search Terms": "Benzersiz Arama Terimleri", + "Unique Terms": "Benzersiz Terimler", + "Unknown": "Bilinmiyor", + "Unknown Object": "Bilinmeyen Nesne", + "Unknown Register": "Bilinmeyen Kayıt", + "Unknown Schema": "Bilinmeyen Şema", + "Unknown error": "Bilinmeyen hata", + "Unknown widget type: {type}": "Bilinmeyen bileşen türü: {type}", + "Unlink email": "E-posta bağlantısını kaldır", + "Unlink event": "Etkinlik bağlantısını kaldır", + "Unnamed Object": "Adsız Nesne", + "Untitled": "Başlıksız", + "Untitled View": "Başlıksız Görünüm", + "Update": "Güncelle", + "Update Operations": "Güncelleme İşlemleri", + "Update available: {local} → {remote}": "Güncelleme mevcut: {local} → {remote}", + "Update configuration: ...": "Yapılandırmayı güncelle: ...", + "Update register OAS: ...": "Kayıt OAS'sini güncelle: ...", + "Update vectors when object data changes (recommended for accurate search)": "Nesne verileri değiştiğinde vektörleri güncelle (doğru arama için önerilir)", + "Updated": "Güncellendi", + "Updated At": "Güncellenme Zamanı", + "Updated:": "Güncellendi:", + "Upload new avatar": "Yeni avatar yükle", + "Uploading configuration sets...": "Yapılandırma setleri yükleniyor...", + "Uptime:": "Çalışma Süresi:", + "Url": "Url", + "Usage Count": "Kullanım Sayısı", + "Use AI agents for processing and analysis": "İşleme ve analiz için yapay zeka ajanlarını kullanın", + "Use Large Language Model features": "Büyük Dil Modeli özelliklerini kullanın", + "Use SolrCloud with Zookeeper for distributed search": "Dağıtılmış arama için Zookeeper ile SolrCloud kullanın", + "Use SolrCloud with zookeeper for distributed search": "Dağıtılmış arama için zookeeper ile SolrCloud kullanın", + "Use VNG APIs instead of MongoDB.": "MongoDB yerine VNG API'larını kullanın.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Varsayılan aralıkları kullanın (Son 7/30/90 gün, Geçen yıl, Daha eski)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Dahili Nextcloud depolaması yerine harici depolama (örn. MongoDB) kullanın.", + "Use filters to narrow down entities by type or category.": "Türe veya kategoriye göre varlıkları daraltmak için filtreler kullanın.", + "Used By Collections": "Koleksiyonlar Tarafından Kullanılan", + "Used by": "Kullanan", + "User": "Kullanıcı", + "User Agent Statistics": "Kullanıcı Ajanı İstatistikleri", + "User Description": "Kullanıcı Açıklaması", + "User Quota": "Kullanıcı Kotası", + "User rate limits cleared successfully": "Kullanıcı hız sınırları başarıyla temizlendi", + "User removed successfully": "Kullanıcı başarıyla kaldırıldı", + "User-friendly description shown in forms and help text": "Formlarda ve yardım metninde gösterilen kullanıcı dostu açıklama", + "Username": "Kullanıcı Adı", + "Username for SOLR authentication (optional)": "SOLR kimlik doğrulaması için kullanıcı adı (isteğe bağlı)", + "Users": "Kullanıcılar", + "Uses": "Kullanımlar", + "Using Pre-Generated Chunks": "Önceden Oluşturulmuş Parçaları Kullanma", + "Validate": "Doğrula", + "Validate Objects": "Nesneleri Doğrula", + "Validation completed for {schema}": "{schema} için doğrulama tamamlandı", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "{schema} için doğrulama tamamlandı: {processed} işlendi, {updated} güncellendi, {failed} başarısız", + "Validation completed successfully": "Doğrulama başarıyla tamamlandı", + "Validation failed": "Doğrulama başarısız", + "Value": "Değer", + "Value list (split on ,)": "Değer listesi (, ile bölünmüş)", + "Vector Dimensions": "Vektör Boyutları", + "Vector Search Backend": "Vektör Arama Backend'i", + "Vector field: _embedding_": "Vektör alanı: _embedding_", + "Vectorization Triggers": "Vektörleştirme Tetikleyicileri", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Vektörleştirme tamamlandı: {vectorized} nesne vektörleştirildi, {failed} başarısız", + "Vectorization failed: {error}": "Vektörleştirme başarısız: {error}", + "Vectorize All Files": "Tüm Dosyaları Vektörleştir", + "Vectorize All Objects": "Tüm Nesneleri Vektörleştir", + "Vectorize all file types": "Tüm dosya türlerini vektörleştir", + "Vectorize all views": "Tüm görünümleri vektörleştir", + "Vectorize on object creation": "Nesne oluşturulduğunda vektörleştir", + "Vectors will be stored in your existing object and file collections": "Vektörler mevcut nesne ve dosya koleksiyonlarınızda depolanacak", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Sunucu bağlantısı doğrulanıyor...", + "Version": "Sürüm", + "Version Information": "Sürüm Bilgisi", + "Very High": "Çok Yüksek", + "Very high": "Çok yüksek", + "View": "Görünüm", + "View API Docs": "API Belgelerine Bak", + "View API Documentation": "API Belgelerine Göz At", + "View APIs": "API'lere Göz At", + "View Changes": "Değişiklikleri Görüntüle", + "View Details": "Ayrıntıları Görüntüle", + "View Error": "Görünüm Hatası", + "View File": "Dosyayı Görüntüle", + "View Full Details": "Tüm Ayrıntıları Görüntüle", + "View Logs": "Günlükleri Görüntüle", + "View Name": "Görünüm Adı", + "View Object": "Nesneyi Görüntüle", + "View Parameters": "Görünüm Parametreleri", + "View Selection": "Görünüm Seçimi", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Gelişmiş filtreleme ve analitik özellikleriyle arama izi günlüklerini görüntüleyin ve analiz edin", + "View and analyze system audit trails with advanced filtering capabilities": "Gelişmiş filtreleme özellikleriyle sistem denetim izlerini görüntüleyin ve analiz edin", + "View and manage authorization across registers and schemas": "Kayıtlar ve şemalar genelinde yetkilendirmeyi görüntüleyin ve yönetin", + "View deleted successfully": "Görünüm başarıyla silindi", + "View entity details and manage relations": "Varlık ayrıntılarını görüntüleyin ve ilişkileri yönetin", + "View name": "Görünüm adı", + "View name is required": "Görünüm adı gereklidir", + "View object": "Nesneyi görüntüle", + "View saved successfully!": "Görünüm başarıyla kaydedildi!", + "View search analytics and manage search logs": "Arama analitiğini görüntüleyin ve arama günlüklerini yönetin", + "View successfully deleted": "Görünüm başarıyla silindi", + "View successfully updated": "Görünüm başarıyla güncellendi", + "View updated successfully!": "Görünüm başarıyla güncellendi!", + "View webhook delivery logs and filter by webhook": "Webhook teslim günlüklerini görüntüleyin ve webhook'a göre filtreleyin", + "View {count} more": "{count} tanesini daha görüntüle", + "Views": "Görünümler", + "Visible to end users": "Son kullanıcılara görünür", + "Visible to users": "Kullanıcılara görünür", + "Wait for Response": "Yanıt Bekle", + "Wait for webhook response before continuing (required for request/response flows)": "Devam etmeden önce webhook yanıtını bekleyin (istek/yanıt akışları için gerekli)", + "Waiting for cluster synchronization...": "Küme senkronizasyonu bekleniyor...", + "Warmup Names Cache": "İsimler Önbelleğini Isıt", + "Warmup failed": "Isınma başarısız", + "Warning": "Uyarı", + "Warning:": "Uyarı:", + "Webhook": "Webhook", + "Webhook Log Details": "Webhook Günlük Ayrıntıları", + "Webhook Logs": "Webhook Günlükleri", + "Webhook created successfully": "Webhook başarıyla oluşturuldu", + "Webhook deleted": "Webhook silindi", + "Webhook retry delivered successfully": "Webhook yeniden denemesi başarıyla teslim edildi", + "Webhook retry delivery failed": "Webhook yeniden deneme teslimi başarısız", + "Webhook updated": "Webhook güncellendi", + "Webhook updated successfully": "Webhook başarıyla güncellendi", + "Webhooks": "Webhook'lar", + "Weekly": "Haftalık", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "Etkinleştirildiğinde, bu nesneyi kaydetmek hedef nesneleri de bu nesneye geri atıfta bulunacak şekilde güncelleyecek.", + "When enabled, this property will be removed from the source object after updating the target objects.": "Etkinleştirildiğinde, bu özellik hedef nesneleri güncelledikten sonra kaynak nesneden kaldırılacak.", + "Workflow Management": "İş Akışı Yönetimi", + "Workflows": "İş Akışları", + "Wrap webhook payload in CloudEvents format for better interoperability": "Daha iyi birlikte çalışabilirlik için webhook yükünü CloudEvents biçiminde sarın", + "Wrap webhook payload in cloudevents format for better interoperability": "Daha iyi birlikte çalışabilirlik için webhook yükünü cloudevents biçiminde sarın", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Evet", + "You": "Siz", + "You are not yet a member of any organisations.": "Henüz hiçbir kuruluşun üyesi değilsiniz.", + "You do not have access to this conversation": "Bu konuşmaya erişiminiz yok", + "You do not have permission to delete this conversation": "Bu konuşmayı silme izniniz yok", + "You do not have permission to modify this conversation": "Bu konuşmayı değiştirme izniniz yok", + "You do not have permission to restore this conversation": "Bu konuşmayı geri yükleme izniniz yok", + "You do not have permission to view the permission matrix. Admin access is required.": "İzin matrisini görüntüleme izniniz yok. Yönetici erişimi gereklidir.", + "You have selected {count} properties to add to the schema.": "Şemaya eklemek için {count} özellik seçtiniz.", + "You must be logged in to favorite views": "Görünümleri favorilere eklemek için giriş yapmış olmanız gerekir", + "You need an AI agent to start a conversation.": "Konuşma başlatmak için bir yapay zeka ajanına ihtiyacınız var.", + "Your Fireworks AI API key. Get one at": "Fireworks AI API anahtarınız. Bir tane alın:", + "Your OpenAI API key. Get one at": "OpenAI API anahtarınız. Bir tane alın:", + "Zookeeper Hosts": "Zookeeper Ana Bilgisayarları", + "Zookeeper Port": "Zookeeper Port", + "Zookeeper Settings (SolrCloud)": "Zookeeper Ayarları (SolrCloud)", + "Zookeeper connection string for SolrCloud": "SolrCloud için Zookeeper bağlantı dizesi", + "Zookeeper port number (optional, defaults to 2181)": "Zookeeper port numarası (isteğe bağlı, varsayılan 2181)", + "and add the files there.": "ve dosyaları oraya ekleyin.", + "approved": "onaylandı", + "are only accessible to invited users.": "yalnızca davet edilen kullanıcılara erişilebilir.", + "by": "tarafından", + "chunk": "parça", + "chunks": "parçalar", + "chunks processed": "parça işlendi", + "completed": "tamamlandı", + "configuration(s)": "yapılandırma(lar)", + "conversationId is required": "conversationId gereklidir", + "create": "oluştur", + "delete": "sil", + "deleted objects section": "silinen nesneler bölümü", + "e.g., lib/Settings/config.json": "örn. lib/Settings/config.json", + "e.g., lib/Settings/register.json": "örn. lib/Settings/register.json", + "e.g., lib/settings/config.json": "örn. lib/settings/config.json", + "e.g., lib/settings/register.json": "örn. lib/settings/register.json", + "execute": "yürüt", + "failed": "başarısız", + "file": "dosya", + "files": "dosyalar", + "file{plural}": "dosya{plural}", + "folder": "klasör", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "{register} içinde", + "in_progress": "devam ediyor", + "inherited": "devralındı", + "list": "listele", + "log{plural}": "günlük{plural}", + "manage": "yönet", + "members": "üyeler", + "menu or contact someone with permission to create agents.": "menüsü veya ajan oluşturma iznine sahip biriyle iletişime geçin.", + "message content is required": "mesaj içeriği gereklidir", + "messages": "mesajlar", + "missing": "eksik", + "myapp": "myapp", + "n8n URL and API key are required": "n8n URL ve API anahtarı gereklidir", + "n8n configuration saved successfully": "n8n yapılandırması başarıyla kaydedildi", + "n8n connection not configured": "n8n bağlantısı yapılandırılmamış", + "n8n connection successful": "n8n bağlantısı başarılı", + "n8n connection test failed: {message}": "n8n bağlantı testi başarısız: {message}", + "n8n connection test successful": "n8n bağlantı testi başarılı", + "n8n initialization failed: {message}": "n8n başlatma başarısız: {message}", + "n8n integration disabled": "n8n entegrasyonu devre dışı", + "n8n integration enabled": "n8n entegrasyonu etkin", + "n8n project initialized successfully": "n8n projesi başarıyla başlatıldı", + "n8n settings saved successfully": "n8n ayarları başarıyla kaydedildi", + "notify_push configuration guide": "notify_push yapılandırma kılavuzu", + "notify_push is installed but not yet active": "notify_push yüklü ancak henüz etkin değil", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "nesneler", + "objects processed": "nesne işlendi", + "object{plural}": "nesne{plural}", + "or pick a range": "veya bir aralık seçin", + "org-...": "org-...", + "pending": "beklemede", + "provides a good balance between speed and accuracy.": "hız ve doğruluk arasında iyi bir denge sağlar.", + "public": "genel", + "read": "oku", + "register(s)": "kayıt(lar)", + "register(s) selected": "kayıt(lar) seçildi", + "register{plural}": "kayıt{plural}", + "rejected": "reddedildi", + "required": "gerekli", + "results": "sonuçlar", + "schema(s)": "şema(lar)", + "schema(s) selected": "şema(lar) seçildi", + "schema{plural}": "şema{plural}", + "selected objects": "seçilen nesneler", + "set": "ayarla", + "sk-...": "sk-...", + "this application": "bu uygulama", + "to": "için", + "type must be \"positive\" or \"negative\"": "tür \"positive\" veya \"negative\" olmalıdır", + "update": "güncelle", + "vectorized": "vektörleştirildi", + "view": "görüntüle", + "views selected": "görünümler seçildi", + "waiting": "bekliyor", + "{active} active, {deleted} deleted": "{active} etkin, {deleted} silindi", + "{count} audit trails cleared successfully": "{count} denetim izi başarıyla temizlendi", + "{count} compliance issue(s) detected": "{count} uyumluluk sorunu tespit edildi", + "{count} deletions": "{count} silme", + "{count} documents found": "{count} belge bulundu", + "{count} email": "{count} e-posta", + "{count} emails": "{count} e-posta", + "{count} entries": "{count} giriş", + "{count} issues": "{count} sorun", + "{count} members": "{count} üye", + "{count} searches": "{count} arama", + "{count} selected": "{count} seçildi", + "{count} widget(s)": "{count} bileşen", + "{matched} object(s) matched. {erased} erased.": "{matched} nesne eşleşti. {erased} silindi.", + "{minutes} minutes ago": "{minutes} dakika önce", + "{percentage}% of objects": "Nesnelerin %{percentage}", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{register} / {schema} içinde {title}", + "{type} cache cleared successfully": "{type} önbelleği başarıyla temizlendi", + "ℹ️ Current Configuration": "ℹ️ Mevcut Yapılandırma", + "ℹ️ current configuration": "ℹ️ mevcut yapılandırma", + "⚡ Batch Processing": "⚡ Toplu İşleme", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Daha hızlı yanıtlar ✓ Daha odaklı cevaplar ✗ İlgili bilgileri kaçırabilir", + "✨ AI Features": "✨ Yapay Zeka Özellikleri", + "💰 View Selection (Cost Optimization)": "💰 Görünüm Seçimi (Maliyet Optimizasyonu)", + "📄 Object Serialization": "📄 Nesne Serileştirme", + "🔢 Vectorization Settings": "🔢 Vektörleştirme Ayarları", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Madde 30 §4: Bu rapor, her işleme faaliyetini kendisine atfedilen ömür boyu denetim izi sayılarıyla birleştirir. Denetçiler ve Autoriteit Persoonsgegevens bunu denetim incelemesi için kullanır.", + "Add schema titles, descriptions, and register information to provide richer context for search": "Arama için daha zengin bağlam sağlamak amacıyla şema başlıkları, açıklamalar ve kayıt bilgileri ekleyin", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Veritabanı kaynağı kullanılırken gelişmiş filtreler mevcut değil. Filtreleme seçenekleri için Otomatik veya SOLR Dizini'ne geçin.", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Bu kayıttaki açık yetkilendirme geçersiz kılmaları olmayan tüm şemalara rol atayın.", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "\"{name}\" koleksiyonunu SİLMEK istediğinizden emin misiniz?\n\nBu işlem:\n• Koleksiyonu ve tüm verilerini kalıcı olarak siler\n• Tüm dizinlenmiş belgeleri kaldırır\n• Bu işlem geri alınamaz", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Eski arama izlerini temizlemek istediğinizden emin misiniz? Bu, 30 günden eski girişleri siler.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "\"{name}\" koleksiyonundan tüm verileri temizlemek istediğinizden emin misiniz?\n\nBu işlem:\n• Tüm dizinlenmiş belgeleri siler\n• Koleksiyon yapısını bozulmadan tutar\n• Bu işlem geri alınamaz", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "\"{name}\" koleksiyonunu yeniden dizinlemek istediğinizden emin misiniz?\n\nBu işlem:\n• Tüm nesnelerle dizini yeniden oluşturur\n• Tamamlanması birkaç dakika sürebilir\n• Yeniden dizinleme sırasında arama performansını etkileyebilir", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "\"{schema}\" şemasını \"{register}\" kaydından kaldırmak istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "\"{schema}\" şeması için {count} etkin nesneyi geçici silmek istediğinizden emin misiniz?\n\nBunlar silinmiş olarak işaretlenecek ancak daha sonra kalıcı olarak kaldırılabilecek.", + "Automatically create a default organisation if none exists when the app is initialized": "Uygulama başlatıldığında hiç kuruluş yoksa varsayılan bir kuruluş otomatik olarak oluştur", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Dosyalar yüklendiğinde ve işlendiğinde metin parçalarından vektör gömülümleri otomatik olarak oluştur", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "KBB tespit edilmiş ancak işleme faaliyeti ek açıklaması bulunmayan şemaları bulmak için uyumluluk taramasını çalıştırın.", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Karmaşık sorgular: Birden fazla filtre, operatör ve karmaşık parametre kombinasyonlu gelişmiş aramalar", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Karmaşık sorgular: birden fazla filtre, operatör ve karmaşık parametre kombinasyonlu gelişmiş aramalar", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "API maliyetlerini azaltmak için hangi nesne görünümlerinin vektörleştirilmesi gerektiğini kontrol edin. Yalnızca anlamsal aramadan yararlanan görünümleri vektörleştirin.", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "LLM özelliklerini etkinleştirin veya devre dışı bırakın. Yukarıdaki LLM Yapılandırma düğmesini kullanarak sağlayıcıları ve modelleri yapılandırın.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "LLM özelliklerini etkinleştirin veya devre dışı bırakın. Yukarıdaki LLM yapılandırma düğmesini kullanarak sağlayıcıları ve modelleri yapılandırın.", + "Enable tools that allow the agent to interact with data through function calling.": "Ajanın işlev çağrısı aracılığıyla verilerle etkileşime girmesine izin veren araçları etkinleştirin.", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Dosya vektörleştirme tamamlandı! {files} dosyasından {vectorized} parça vektörleştirildi. {failed} başarısız.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Kuruluşunuzun veri sağlığı, büyüme eğilimleri ve kalite metrikleri hakkında gerçek zamanlı içgörüler edinin.", + "New users without specific organisation membership will be automatically added to this organisation": "Belirli bir kuruluş üyeliği olmayan yeni kullanıcılar otomatik olarak bu kuruluşa eklenecek", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "{schema} şeması için geçici silinecek etkin nesne yok. Geçici silinmiş nesneleri kaldırmak için \"Kalıcı Sil\" kullanın.", + "No facetable fields available. Select a register and schema to see available filters.": "Fasetlenebilir alan mevcut değil. Mevcut filtreleri görmek için bir kayıt ve şema seçin.", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Bir API çağrısında vektörleştirilecek parça sayısı. Yüksek = daha hızlı ama daha fazla bellek. Önerilen: 10-50.", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Bir API çağrısında vektörleştirilecek nesne sayısı. Yüksek = daha hızlı ama daha fazla bellek. Önerilen: 10-50.", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "İsteğe bağlı: Başvurulan şemayı filtrelemek için sorgu parametreleri ekleyin (örn. status=active&type=public)", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Gerçek zamanlı anlık bildirimler etkin. Nesneler oluşturulduğunda, güncellendiğinde veya silindiğinde bağlı istemciler anlık güncellemeler alır.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Aşağıda seçilen nesneleri inceleyin. Dahil etmek istemediğiniz nesneleri kaldırabilirsiniz.", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Üye olarak eklemek için bir kuruluş ve kullanıcı seçin. Kuruluşları ada göre arayın.", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Basit sorgular: Minimum parametre ile temel metin aramaları (örn. tek arama terimi, gelişmiş filtre yok)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Basit sorgular: minimum parametre ile temel metin aramaları (örn. tek arama terimi, gelişmiş filtre yok)", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "İstenen gösterge paneli yüklenemedi. URL'yi doğrulayın veya listeden başka bir gösterge paneli seçin.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Bunlar, şemalarının yapılandırılmış saklama süresine göre tutulacak ve ardından kalıcı olarak kaldırılacak.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Kayıt, şema, silme tarihi veya silme yapan kullanıcıya göre silinmiş öğeleri daraltmak için filtreler kullanın.", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Uyarı: Bu özelliğin tutarsız türleri var: {types}. Tüm nesnelerin aynı türe sahip olması gerekip gerekmediğini değerlendirin.", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Ajan bir kullanıcı oturumu olmadan çalıştığında (örn. zamanlanmış görevler), bu kullanıcının bağlamı kullanılacak", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Ajan bir kullanıcı oturumu olmadan çalıştığında (örn. zamanlanmış görevler), bu kullanıcının bağlamı kullanılacak.", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Geri bildiriminiz kaydedildi. İsteğe bağlı olarak burada ek ayrıntılar sağlayabilirsiniz...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Geri bildiriminiz kaydedildi. İsteğe bağlı olarak burada ek ayrıntılar sağlayabilirsiniz...", + "and accessible to all users in selected groups (or all users if no groups selected).": "ve seçilen gruplardaki tüm kullanıcılara erişilebilir (grup seçilmezse tüm kullanıcılara).", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR, saniyenin altında arama yanıt süreleriyle milyonlarca belgeyi işleyebilir...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Kapsamlı bağlam ✓ Ayrıntıları kaçırma olasılığı daha az ✗ Daha yavaş yanıtlar ✗ Daha az alakalı bilgi içerebilir", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 Dağıtılmış modda SOLR, küme koordinasyonu için ZooKeeper kullanır...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Fasetli arama, kullanıcıların sonuçları kategorilere göre derinlemesine incelemesine olanak tanır...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Biliyor muydunuz? SOLR, Netflix, Apple ve diğer büyük siteler için aramayı güçlendiriyor!", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR, arama performansı hakkında zengin analitik ve istatistikler sağlar...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 ConfigSet'ler, arama dizininizin şemasını ve yapılandırma dosyalarını içerir...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Replikasyon, düğümler başarısız olsa bile arama dizininizin kullanılabilir olmasını sağlar...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR, Apache Lucene üzerine inşa edilmiş güçlü bir kurumsal arama platformudur...", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Otomatik ölçeklendirme, yüke göre dinamik olarak düğüm ekleyebilir veya kaldırabilir...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Güvenlik özellikleri kimlik doğrulama, yetkilendirme ve SSL şifrelemeyi içerir...", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Vektörleştirme sürecine hangi dosya türlerinin dahil edileceğini seçin. Yalnızca çıkarılmış metin ve parçalara sahip dosyalar işlenecek.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Vektörleştirme sürecine hangi görünümlerin dahil edileceğini seçin. Yapılandırmanıza göre tüm görünümleri işlemek için boş bırakın.", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "ConfigSet'ler, SOLR koleksiyonlarınızın şemasını ve yapılandırmasını tanımlar. Alan tanımları, analizörler ve diğer arama ayarlarını içerirler.", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Anlamsal arama, gömülümler ve sohbet dahil yapay zeka destekli özellikler için Büyük Dil Modeli (LLM) sağlayıcılarını yapılandırın.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Kimlik doğrulama ve ağ seçenekleri dahil SOLR sunucunuz için temel bağlantı ayarlarını yapılandırın. Çekirdekleri ve koleksiyonları yönetmek için ayrı ConfigSet ve Koleksiyon Yönetimi iletişim kutularını kullanın.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Kimlik doğrulama ve ağ seçenekleri dahil SOLR sunucunuz için temel bağlantı ayarlarını yapılandırın. Çekirdekleri ve koleksiyonları yönetmek için ayrı configset ve koleksiyon yönetimi iletişim kutularını kullanın.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Anlamsal arama için veritabanı nesnelerinin vektör gömülümlerine nasıl dönüştürüleceğini yapılandırın. Nesneler, metin çıkarmaya gerek kalmadan doğrudan vektörleştirilir.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Vektörleştirmeden önce nesnelerin metne nasıl dönüştürüleceğini yapılandırın. Bu ayarlar arama kalitesini ve bağlamı etkiler.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Anlamsal arama, gömülümler ve sohbet dahil yapay zeka destekli özellikler için büyük dil modeli (LLM) sağlayıcılarını yapılandırın.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Dosya vektörleştirmesi için parametreleri yapılandırın. Bu işlem, tüm çıkarılan dosya parçaları için vektör gömülümleri oluşturur.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Nesne vektörleştirmesi için parametreleri yapılandırın. Bu işlem, görünüm filtrelerinizle eşleşen tüm nesneler için vektör gömülümleri oluşturur.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Yapay zekanın bu konuşmada hangi görünümleri ve araçları kullanabileceğini kontrol edin. Varsayılan olarak tüm ajan özellikleri etkindir.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Denetim izi satırlarını AVG Madde 30 atıflarıyla etiketlemeye başlamak için ilk verwerkingsactiviteit'i oluşturun.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "SOLR arama entegrasyonunu etkinleştirin veya devre dışı bırakın. Yukarıdaki Bağlantı Ayarları düğmesini kullanarak bağlantı ayarlarını yapılandırın.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "SOLR arama entegrasyonunu etkinleştirin veya devre dışı bırakın. Yukarıdaki bağlantı ayarları düğmesini kullanarak bağlantı ayarlarını yapılandırın.", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Dosya parça vektörleştirme henüz uygulanmadı. Parçalar hazır ve depolandı, ancak vektörleştirme servisi geliştirme aşamasındadır.", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Kayıtlara, şema tanımlarına ve nesne depolaması ile kullanımına odaklanarak kuruluşunuzun veri sağlığı hakkında gerçek zamanlı içgörüler edinin.", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "İç içe geçmiş nesne özelliklerini ne kadar derinliğe kadar gezileceği (1-20). Daha yüksek değerler daha fazla ayrıntı yakalar ancak vektör boyutunu artırır.", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "`reports` kaydını almak için report-bundle.json şablonunu içe aktarın, ardından standart nesne arayüzü aracılığıyla ilk gösterge panelinizi oluşturun. Gösterge panelleri widget'larını JSON'da bildirir ve işleyici her widget'a canlı toplama verisi besler.", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Bir veri öznesiyle başvuran her nesneyi bulun (Madde 15 inzage), silme işlemini önizleyin (Madde 17 vergetelheid) veya verilerini dışa aktarın (Madde 20 portabiliteit).", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "OpenRegister otomasyonu için n8n iş akışlarını yönetin. İş akışları yapılandırılmış projede depolanacak.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "İşleme faaliyetlerini yönetin, veri sahibi erişim isteklerini çalıştırın ve AB GDPR / Hollanda AVG kapsamında uyumluluğu denetleyin.", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Orta sorgular: Bazı filtreleme veya birden fazla parametre içeren aramalar (örn. tarih aralıkları, belirli kayıtlar/şemalar)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Orta sorgular: bazı filtreleme veya birden fazla parametre içeren aramalar (örn. tarih aralıkları, belirli kayıtlar/şemalar)", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Nesneler geçici silinecek (silinmiş olarak işaretlenecek ancak veritabanında tutulacak). Gerekirse daha sonra kurtarılabilirler.", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Operatör tanımlı gösterge panelleri ve zamanlanmış raporlar. Her gösterge paneli `reports` kaydında birinci sınıf bir nesnedir; widget'lar gösterge panelinin `widgets` dizisinde bildirilir ve toplama/GraphQL'den canlı olarak işlenir.", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Sıralı BCP 47 dil etiketleri. İlk dil, kayıt varsayılanı olup çevrilebilir özellikler için Accept-Language geri dönüşünü yönlendirir.", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Aşağıda seçilen nesneleri inceleyin. Kaldır düğmesine tıklayarak silmek istemediğiniz nesneleri kaldırabilirsiniz.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Aşağıda seçilen nesneleri inceleyin. Kaldır düğmesine tıklayarak kalıcı olarak silmek istemediğiniz nesneleri kaldırabilirsiniz. Bu işlem geri alınamaz.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Aşağıda seçilen nesneleri inceleyin. Kaldır düğmesine tıklayarak geri yüklemek istemediğiniz nesneleri kaldırabilirsiniz. Nesneler orijinal konumlarına geri yüklenecek.", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Bu kuruluş için hangi Nextcloud gruplarının mevcut olduğunu seçin. Bu gruplardaki kullanıcılar kuruluş kaynaklarına erişime sahip olacak.", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Metin parçaları dosya çıkarma sırasında oluşturulur ve veritabanında depolanır. Vektörleştirme, önceden parçalanmış bu dosyaları okur ve gömülümlere dönüştürür.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Bunlar, şemalarının yapılandırılmış saklama süresine göre tutulacak ve saklama süresi dolduğunda otomatik olarak kalıcı biçimde silinecek. Saklama süresi şema bazında yapılandırılabilir ve şemanın ayarlarında bulunabilir.", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Bu, veritabanından TÜM gömülümleri (vektörleri) kalıcı olarak siler. Tüm nesneleri ve dosyaları yeniden vektörleştirmeniz gerekecek. Bu işlem geri alınamaz.\n\nDevam etmek istediğinizden emin misiniz?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Bu, veritabanından TÜM gömülümleri (vektörleri) kalıcı olarak siler. Tüm nesneleri ve dosyaları yeniden vektörleştirmeniz gerekecek. Bu işlem geri alınamaz.\\n\\nDevam etmek istediğinizden emin misiniz?", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Bu, veritabanından {count} zaten geçici silinmiş nesneyi kalıcı olarak kaldıracak. Bu işlem geri alınamaz.", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "İpucu: Gömülüm maliyetlerini en aza indirmek için yalnızca anlamsal arama gerektiren görünümleri etkinleştirin. Basit arama tabloları nadiren vektörleştirmeye ihtiyaç duyar.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "İpucu: gömülüm maliyetlerini en aza indirmek için yalnızca anlamsal arama gerektiren görünümleri etkinleştirin. Basit arama tabloları nadiren vektörleştirmeye ihtiyaç duyar.", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Hiçbir kullanıcı oturumu etkin olmadığında araçlar ajanın varsayılan kullanıcı izinleriyle yürütülür (örn. cron işleri). Varsayılan kullanıcıyı Ayarlar sekmesinde yapılandırın.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Kayıt, şema, eylem türü, kullanıcı, tarih aralığı veya nesne kimliğine göre denetim izi girişlerini daraltmak için filtreler kullanın.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Kayıt, şema, başarı durumu, kullanıcı, tarih aralığı, arama terimleri veya performans metriklerine göre arama izi girişlerini daraltmak için filtreler kullanın.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Yeni özellikleri keşfetmek ve mevcut özellikleri potansiyel iyileştirmeler için analiz etmek amacıyla bu şemaya ait tüm nesneleri tarayacağız. Süreç, her nesnenin veri yapısını incelemeyi, mevcut şemada tanımlanmamış özellikleri belirlemeyi ve daha iyi kısıtlamalar, biçimler ve doğrulama kurallarıyla mevcut özellik tanımlarını iyileştirme fırsatları bulmayı içerir.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "Devre dışı bırakıldığında, bu faset yalnızca bu şemadan değerleri gösterecek ve seçildiğinde bir şema filtresi içerecek.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "_default şablonuna dayalı yeni ConfigSet'ler oluşturabilir veya doğrudan SOLR sunucunuza özel olanları yükleyebilirsiniz.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "_default şablonuna dayalı yeni configset'ler oluşturabilir veya doğrudan SOLR sunucunuza özel olanları yükleyebilirsiniz.", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F KALICI SİLME UYARISI \\u26A0\\uFE0F\n\n\"{schema}\" şeması için TÜM nesneleri KALICI OLARAK silmek üzeresiniz:\n\n\\u2022 Etkin nesneler: {active}\n\\u2022 Geçici silinmiş nesneler: {deleted}\n\\u2022 Toplam: {total}\n\nBu nesneler veritabanından tamamen kaldırılacak ve KURTARILAMAZ.\n\nKesinlikle emin misiniz?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F KALICI SİLME UYARISI \\u26A0\\uFE0F\n\n\"{schema}\" şeması için {count} geçici silinmiş nesneyi KALICI OLARAK silmek üzeresiniz.\n\nBu nesneler veritabanından tamamen kaldırılacak ve KURTARILAMAZ.\n\nKesinlikle emin misiniz?", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push yüklü, ancak OpenRegister henüz başarılı bir anlık bildirim onaylamadı. Etkinleştirmek için nesne kaydı tetikleyin veya notify_push yapılandırmanızı kontrol edin.", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ TEHLİKE: Tüm {total} nesne veritabanından KALICI OLARAK SİLİNECEK. Bu işlem GERİ DÖNDÜRÜLEMEZ ve geri alınamaz!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ KALICI SİLME UYARISI ⚠️\n\n\"{schema}\" şeması için TÜM nesneleri KALICI OLARAK silmek üzeresiniz:\n\n• Etkin nesneler: {active}\n• Geçici silinmiş nesneler: {deleted}\n• Toplam: {total}\n\nBu nesneler veritabanından tamamen kaldırılacak ve KURTARILAMAZ.\n\nKesinlikle emin misiniz?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ KALICI SİLME UYARISI ⚠️\n\n\"{schema}\" şeması için {count} geçici silinmiş nesneyi KALICI OLARAK silmek üzeresiniz.\n\nBu nesneler veritabanından tamamen kaldırılacak ve KURTARILAMAZ.\n\nKesinlikle emin misiniz?", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} e-posta", + "{count} e-posta" + ], + "Purge {count} object from database": [ + "Veritabanından {count} nesneyi temizle", + "Veritabanından {count} nesneyi temizle" + ], + "Restore {count} object": [ + "{count} nesneyi geri yükle", + "{count} nesneyi geri yükle" + ], + "Successfully restored {count} object": [ + "{count} nesne başarıyla geri yüklendi", + "{count} nesne başarıyla geri yüklendi" + ], + "Delete {count} object": [ + "{count} nesneyi sil", + "{count} nesneyi sil" + ], + "Object successfully deleted": [ + "Nesne başarıyla silindi", + "Nesneler başarıyla silindi" + ] + } +} diff --git a/l10n/uk.js b/l10n/uk.js new file mode 100644 index 0000000000..4b79bcded3 --- /dev/null +++ b/l10n/uk.js @@ -0,0 +1,1414 @@ +OC.L10N.register( + "openregister", + { + "3" : "3", + "30" : "30", + "(no subject)" : "(без теми)", + "(no title)" : "(без назви)", + "(unnamed)" : "(без імені)", + "(untitled card)" : "(картка без назви)", + "Activities" : "Дії", + "Add Endpoint" : "Додати кінцеву точку", + "Add Source" : "Додати джерело", + "Add card" : "Додати картку", + "Add contact" : "Додати контакт", + "All clear" : "Усе чисто", + "Articles" : "Статті", + "Code" : "Код", + "Compliance" : "Відповідність", + "Contacts" : "Контакти", + "Create Agent" : "Створити агента", + "Create Application" : "Створити застосунок", + "Create event" : "Створити подію", + "Erasure complete" : "Видалення завершено", + "Erasure preview" : "Попередній перегляд видалення", + "Expand details" : "Розгорнути подробиці", + "Save changes" : "Зберегти зміни", + "📄 Object Serialization" : "📄 Серіалізація об'єкта", + "🔢 Vectorization Settings" : "🔢 Налаштування векторизації", + "💰 View Selection (Cost Optimization)" : "💰 Вибір подання (оптимізація витрат)", + "AI Assistant" : "Помічник ШІ", + "AI Chat" : "Чат ШІ", + "API Key" : "Ключ API", + "API tokens saved successfully" : "Токени API успішно збережено", + "About ConfigSets" : "Про ConfigSets", + "Action" : "Дія", + "Action Distribution" : "Розподіл дій", + "Action:" : "Дія:", + "Actions" : "Дії", + "Active" : "Активний", + "Active Collections" : "Активні колекції", + "Active Filters" : "Активні фільтри", + "Active Organisations" : "Активні організації", + "Active filters:" : "Активні фільтри:", + "Active organisation set successfully" : "Активну організацію успішно встановлено", + "Activity Period" : "Період активності", + "Add Groups" : "Додати групи", + "Add Schema" : "Додати Схему", + "Add schema titles, descriptions, and register information to provide richer context for search" : "Додайте назви Схем, описи та інформацію про Реєстр, щоб забезпечити повніший контекст для пошуку", + "Add to favorites" : "Додати до обраного", + "Added" : "Додано", + "Added to favorites" : "Додано до обраного", + "Additional Information" : "Додаткова інформація", + "Additional feedback saved. Thank you!" : "Додатковий відгук збережено. Дякуємо!", + "Admin" : "Адміністратор", + "Advanced" : "Розширені", + "Advanced Filters" : "Розширені фільтри", + "Advanced Options" : "Розширені параметри", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options." : "Розширені фільтри недоступні під час використання Джерела бази даних. Перемкніться на Авто або індекс SOLR для параметрів фільтрування.", + "After" : "Після", + "Agent deleted successfully" : "Агента успішно видалено", + "Agents" : "Агенти", + "All" : "Усі", + "All Categories" : "Усі категорії", + "All Collections" : "Усі колекції", + "All Confidence Levels" : "Усі рівні впевненості", + "All ConfigSets" : "Усі ConfigSets", + "All Files" : "Усі Файли", + "All Levels" : "Усі рівні", + "All Types" : "Усі типи", + "All Webhooks" : "Усі вебхуки", + "All actions" : "Усі дії", + "All audit trails cleared successfully" : "Усі журнали аудиту успішно очищено", + "All registers" : "Усі Реєстри", + "All schemas" : "Усі Схеми", + "All searches" : "Усі пошуки", + "All users" : "Усі користувачі", + "An error occurred while clearing audit trails" : "Під час очищення журналів аудиту сталася помилка", + "An error occurred while deleting the audit trail" : "Під час видалення журналу аудиту сталася помилка", + "An error occurred while deleting the view" : "Під час видалення подання сталася помилка", + "An error occurred while permanently deleting the objects" : "Під час остаточного видалення об'єктів сталася помилка", + "Analysis completed:" : "Аналіз завершено:", + "Analysis steps:" : "Кроки аналізу:", + "Analytics" : "Аналітика", + "Analyze Objects" : "Аналізувати об'єкти", + "Analyze Properties" : "Аналізувати Властивості", + "Analyze existing properties for improvement opportunities" : "Аналізувати наявні Властивості на предмет можливостей покращення", + "Analyzing..." : "Аналіз...", + "Any favorites and sharing settings for this view" : "Будь-які налаштування обраного та спільного доступу для цього подання", + "Any user" : "Будь-який користувач", + "App store cache invalidated successfully" : "Кеш магазину застосунків успішно скинуто", + "Application deleted successfully" : "Застосунок успішно видалено", + "Applications" : "Застосунки", + "Apply Changes" : "Застосувати зміни", + "Archive" : "Архівувати", + "Archive conversation" : "Архівувати розмову", + "Archived conversations are hidden from your active list" : "Архівовані розмови приховано з вашого активного списку", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days." : "Ви впевнені, що хочете очистити старі журнали пошуку? Це видалить записи, старші за 30 днів.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone." : "Ви впевнені, що хочете видалити вибрані журнали аудиту? Цю дію неможливо скасувати.", + "Are you sure you want to delete the selected search trails? This action cannot be undone." : "Ви впевнені, що хочете видалити вибрані журнали пошуку? Цю дію неможливо скасувати.", + "Are you sure you want to delete this ConfigSet?" : "Ви впевнені, що хочете видалити цей ConfigSet?", + "Are you sure you want to permanently delete" : "Ви впевнені, що хочете остаточно видалити", + "Ask a question..." : "Поставте запитання...", + "Ask questions about your data using natural language" : "Ставте запитання про ваші дані природною мовою", + "Attempt" : "Спроба", + "Audit Trail Changes" : "Зміни журналу аудиту", + "Audit Trail Details" : "Подробиці журналу аудиту", + "Audit Trail Management" : "Керування журналом аудиту", + "Audit Trail Statistics" : "Статистика журналу аудиту", + "Audit Trails" : "Журнали аудиту", + "Audit trail data copied to clipboard" : "Дані журналу аудиту скопійовано до буфера обміну", + "Audit trail deleted successfully" : "Журнал аудиту успішно видалено", + "Audit trail successfully deleted" : "Журнал аудиту успішно видалено", + "Authentication" : "Автентифікація", + "Auto-commit disabled" : "Автоматичне фіксування вимкнено", + "Auto-commit enabled" : "Автоматичне фіксування увімкнено", + "Auto-create Default Organisation" : "Автоматично створювати організацію за замовчуванням", + "Auto-retry failed vectorizations" : "Автоматично повторювати невдалі векторизації", + "Automatically commit changes to SOLR index" : "Автоматично фіксувати зміни в індексі SOLR", + "Automatically create a default organisation if none exists when the app is initialized" : "Автоматично створювати організацію за замовчуванням, якщо жодної не існує під час ініціалізації застосунку", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed" : "Автоматично генерувати векторні вкладення з текстових фрагментів під час завантаження та оброблення Файлів", + "Automatically generate vector embeddings when objects are created or updated" : "Автоматично генерувати векторні вкладення під час створення або оновлення об'єктів", + "Automatically retry failed vectorization attempts (max 3 retries)" : "Автоматично повторювати невдалі спроби векторизації (максимум 3 повторення)", + "Available Filters" : "Доступні фільтри", + "Available Workflows" : "Доступні робочі процеси", + "Avg Execution Time" : "Середній час виконання", + "Avg Members/Org" : "Середня кількість учасників/орг.", + "Avg Object Views/Session" : "Середня кількість переглядів об'єктів/сеанс", + "Avg Response Time" : "Середній час відповіді", + "Avg Results/Search" : "Середня кількість результатів/пошук", + "Avg Searches/Session" : "Середня кількість пошуків/сеанс", + "Back" : "Назад", + "Back to Entities" : "Назад до сутностей", + "Back to Registers" : "Назад до Реєстрів", + "Back to Webhooks" : "Назад до вебхуків", + "Back to applications" : "Назад до застосунків", + "Back to entities" : "Назад до сутностей", + "Backend updated successfully. Please reload the application." : "Бекенд успішно оновлено. Будь ласка, перезавантажте застосунок.", + "Base URL (Optional)" : "Базова URL-адреса (необов'язково)", + "Basic Information" : "Основна інформація", + "Batch Size" : "Розмір пакету", + "Batch extraction completed" : "Пакетне видобування завершено", + "Before" : "До", + "Before object vectorization can work:" : "Перш ніж векторизація об'єктів зможе працювати:", + "Behavior Issue" : "Проблема поведінки", + "Bookmarks" : "Закладки", + "Blob storage has been retired. All objects now use magic tables." : "Сховище Blob виведено з експлуатації. Усі об'єкти тепер використовують магічні таблиці.", + "Branch" : "Гілка", + "Bulk delete operation completed successfully" : "Операцію масового видалення успішно завершено", + "Bulk save operation completed successfully" : "Операцію масового збереження успішно завершено", + "Business Data" : "Бізнес-дані", + "Cache cleared successfully" : "Кеш успішно очищено", + "Calculate Sizes" : "Обчислити розміри", + "Cancel" : "Скасувати", + "Cannot delete: objects are still attached" : "Неможливо видалити: об'єкти все ще прикріплені", + "Category" : "Категорія", + "Change Type" : "Тип зміни", + "Changes" : "Зміни", + "Changes copied to clipboard" : "Зміни скопійовано до буфера обміну", + "Chat Model" : "Модель чату", + "Chat Provider" : "Постачальник чату", + "Chat Provider (RAG)" : "Постачальник чату (RAG)", + "Chat Settings" : "Налаштування чату", + "Chat provider connection successful!" : "З'єднання з постачальником чату успішне!", + "Chat settings" : "Налаштування чату", + "Choose a register" : "Виберіть Реєстр", + "Choose a schema" : "Виберіть Схему", + "Choose how vector similarity calculations are performed for semantic search" : "Виберіть, як виконуються обчислення векторної подібності для семантичного пошуку", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed." : "Виберіть, які типи Файлів включити до процесу векторизації. Оброблятимуться лише Файли з видобутим текстом і фрагментами.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration." : "Виберіть, які подання включити до процесу векторизації. Залиште порожнім, щоб обробити всі подання на основі вашої конфігурації.", + "Chunk" : "Фрагмент", + "Chunk deletion not yet implemented. Use chunk-based endpoints." : "Видалення фрагментів ще не реалізовано. Використовуйте кінцеві точки на основі фрагментів.", + "Chunks" : "Фрагменти", + "Chunks to Vectorize:" : "Фрагменти для векторизації:", + "Cleanup Old Trails" : "Очистити старі журнали", + "Cleanup completed" : "Очищення завершено", + "Clear All Embeddings" : "Очистити всі вкладення", + "Clear Entries" : "Очистити записи", + "Clear Filtered Audit Trails" : "Очистити відфільтровані журнали аудиту", + "Clear Filters" : "Очистити фільтри", + "Clear Index" : "Очистити індекс", + "Clear Selection" : "Очистити вибір", + "Clear all" : "Очистити все", + "Clear all filters" : "Очистити всі фільтри", + "Clear date range" : "Очистити діапазон дат", + "Clearing..." : "Очищення...", + "Close" : "Закрити", + "Collection Management" : "Керування колекціями", + "Collection Name" : "Назва колекції", + "Collection assignments updated successfully" : "Призначення колекцій успішно оновлено", + "Collection cleared successfully" : "Колекцію успішно очищено", + "Collection copied successfully" : "Колекцію успішно скопійовано", + "Collection created successfully" : "Колекцію успішно створено", + "Collection deleted successfully" : "Колекцію успішно видалено", + "Collection used to store and index file metadata and content" : "Колекція, що використовується для зберігання та індексування метаданих і вмісту Файлів", + "Collection used to store and index object data" : "Колекція, що використовується для зберігання та індексування даних об'єктів", + "Collections:" : "Колекції:", + "Commit Message" : "Повідомлення фіксації", + "Commit Within (ms)" : "Фіксувати протягом (мс)", + "Commit message" : "Повідомлення фіксації", + "Compare current schema with real object data" : "Порівняти поточну Схему з реальними даними об'єкта", + "Completed" : "Завершено", + "Complex" : "Складний", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations" : "Складні запити: розширені пошуки з декількома фільтрами, операторами та складними комбінаціями параметрів", + "Confidence Level" : "Рівень впевненості", + "Confidence Score" : "Оцінка впевненості", + "Config must be provided as an object" : "Конфігурацію потрібно надати як об'єкт", + "ConfigSet" : "ConfigSet", + "ConfigSet Management" : "Керування ConfigSet", + "ConfigSet Name" : "Назва ConfigSet", + "ConfigSet created successfully" : "ConfigSet успішно створено", + "ConfigSet deleted successfully" : "ConfigSet успішно видалено", + "ConfigSet:" : "ConfigSet:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings." : "ConfigSets визначають Схему та конфігурацію для ваших колекцій SOLR. Вони містять визначення полів, аналізатори та інші налаштування пошуку.", + "Configuration" : "Конфігурація", + "Configuration Data" : "Дані конфігурації", + "Configuration saved" : "Конфігурацію збережено", + "Configurations" : "Конфігурації", + "Configure Facets" : "Налаштувати фасети", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat." : "Налаштуйте постачальників великих мовних моделей (LLM) для функцій на основі ШІ, зокрема семантичного пошуку, вкладень і чату.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections." : "Налаштуйте основні параметри з'єднання для вашого сервера SOLR, зокрема автентифікацію та мережеві параметри. Використовуйте окремі діалоги керування ConfigSet і колекціями для керування ядрами та колекціями.", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction." : "Налаштуйте, як об'єкти бази даних перетворюються на векторні вкладення для семантичного пошуку. Об'єкти векторизуються безпосередньо без потреби видобування тексту.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context." : "Налаштуйте, як об'єкти перетворюються на текст перед векторизацією. Ці налаштування впливають на якість і контекст пошуку.", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks." : "Налаштуйте параметри для векторизації Файлів. Цей процес згенерує векторні вкладення для всіх видобутих фрагментів Файлів.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters." : "Налаштуйте параметри для векторизації об'єктів. Цей процес згенерує векторні вкладення для всіх об'єктів, що відповідають фільтрам вашого подання.", + "Configure which types of data to search and how many sources to retrieve" : "Налаштуйте, які типи даних шукати та скільки Джерел отримувати", + "Connection Failed" : "З'єднання не вдалося", + "Connection Settings" : "Налаштування з'єднання", + "Connection Successful!" : "З'єднання успішне!", + "Connection failed" : "З'єднання не вдалося", + "Connection protocol" : "Протокол з'єднання", + "Connection successful" : "З'єднання успішне", + "Connection timeout in seconds" : "Час очікування з'єднання в секундах", + "Constant delay between retries (5 minutes)" : "Постійна затримка між повтореннями (5 хвилин)", + "Constraint Issue" : "Проблема обмеження", + "Content Filters" : "Фільтри вмісту", + "Continue to Properties" : "Продовжити до Властивостей", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search." : "Керуйте тим, які подання об'єктів слід векторизувати, щоб зменшити витрати на API. Векторизуйте лише ті подання, які виграють від семантичного пошуку.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled." : "Керуйте тим, які подання та інструменти ШІ може використовувати в цій розмові. За замовчуванням усі можливості агента увімкнено.", + "Controls randomness (0 = deterministic, 2 = very creative)" : "Керує випадковістю (0 = детермінований, 2 = дуже креативний)", + "Conversation ID is required" : "Потрібен ідентифікатор розмови", + "Conversation archived" : "Розмову архівовано", + "Conversation archived successfully" : "Розмову успішно архівовано", + "Conversation cleared successfully" : "Розмову успішно очищено", + "Conversation deleted" : "Розмову видалено", + "Conversation permanently deleted" : "Розмову остаточно видалено", + "Conversation renamed" : "Розмову перейменовано", + "Conversation restored" : "Розмову відновлено", + "Conversation title" : "Назва розмови", + "Copied!" : "Скопійовано!", + "Copy" : "Копіювати", + "Copy Changes" : "Копіювати зміни", + "Copy Collection" : "Копіювати колекцію", + "Copy Data" : "Копіювати дані", + "Copy Full Data" : "Копіювати повні дані", + "Copying..." : "Копіювання...", + "Count" : "Кількість", + "Counting objects..." : "Підрахунок об'єктів...", + "Costs" : "Витрати", + "Create" : "Створити", + "Create Collection" : "Створити колекцію", + "Create ConfigSet" : "Створити ConfigSet", + "Create New Collection" : "Створити нову колекцію", + "Create New ConfigSet" : "Створити новий ConfigSet", + "Create Operations" : "Операції створення", + "Create Webhook" : "Створити вебхук", + "Create a copy of collection:" : "Створити копію колекції:", + "Create a new ConfigSet based on the _default template" : "Створити новий ConfigSet на основі шаблону _default", + "Create a new SOLR collection from an existing ConfigSet" : "Створити нову колекцію SOLR з наявного ConfigSet", + "Create your first AI agent to get started." : "Створіть свого першого агента ШІ, щоб почати.", + "Created" : "Створено", + "Created:" : "Створено:", + "Creating..." : "Створення...", + "Current Filters" : "Поточні фільтри", + "Current Type:" : "Поточний тип:", + "Custom API endpoint if using a different region" : "Власна кінцева точка API, якщо використовується інший регіон", + "Custom HTTP headers (one per line, format: Header-Name: value)" : "Власні заголовки HTTP (по одному на рядок, формат: Header-Name: value)", + "Daily" : "Щодня", + "Dashboard" : "Інформаційна панель", + "Data Source" : "Джерело даних", + "Data sources" : "Джерела даних", + "Data type does not match observed values" : "Тип даних не відповідає спостережуваним значенням", + "Database URL" : "URL-адреса бази даних", + "Database information refreshed" : "Інформацію про базу даних оновлено", + "Date Range" : "Діапазон дат", + "Default" : "За замовчуванням", + "Default Organisation" : "Організація за замовчуванням", + "Default: 5" : "За замовчуванням: 5", + "Delays double with each attempt (2, 4, 8 minutes...)" : "Затримки подвоюються з кожною спробою (2, 4, 8 хвилин...)", + "Delays increase linearly (5, 10, 15 minutes...)" : "Затримки зростають лінійно (5, 10, 15 хвилин...)", + "Delete" : "Видалити", + "Delete Application" : "Видалити застосунок", + "Delete Audit Trail" : "Видалити журнал аудиту", + "Delete Collection" : "Видалити колекцію", + "Delete ConfigSet" : "Видалити ConfigSet", + "Delete Objects" : "Видалити об'єкти", + "Delete Operations" : "Операції видалення", + "Delete View" : "Видалити подання", + "Delete all objects in this schema" : "Видалити всі об'єкти в цій Схемі", + "Delete permanently" : "Видалити остаточно", + "Delete this conversation?" : "Видалити цю розмову?", + "Delete view" : "Видалити подання", + "Deleted" : "Видалено", + "Deleted By" : "Видалено користувачем", + "Deleted Date" : "Дата видалення", + "Deleted Items Management" : "Керування видаленими елементами", + "Deleted This Week" : "Видалено цього тижня", + "Deleted Today" : "Видалено сьогодні", + "Deleted:" : "Видалено:", + "Deleting..." : "Видалення...", + "Deletion Date Range" : "Діапазон дат видалення", + "Deletion Statistics" : "Статистика видалення", + "Deprecated" : "Застаріле", + "Description" : "Опис", + "Description:" : "Опис:", + "Detect data types and patterns" : "Виявляти типи даних і шаблони", + "Detected At" : "Виявлено о", + "Detected Format:" : "Виявлений формат:", + "Detected Issues:" : "Виявлені проблеми:", + "Disable" : "Вимкнути", + "Disabled" : "Вимкнено", + "Discover Files" : "Виявити Файли", + "Discovered Properties" : "Виявлені Властивості", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone." : "Бажаєте остаточно видалити всі відфільтровані записи журналу аудиту? Цю дію неможливо скасувати.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone." : "Бажаєте остаточно видалити цей запис журналу аудиту? Цю дію неможливо скасувати.", + "Documents" : "Документи", + "Download API Spec" : "Завантажити специфікацію API", + "Edit" : "Редагувати", + "Edit Register" : "Редагувати Реєстр", + "Edit Schema" : "Редагувати Схему", + "Edit View" : "Редагувати подання", + "Edit Webhook" : "Редагувати вебхук", + "Edit view" : "Редагувати подання", + "Edit view details" : "Редагувати подробиці подання", + "Effectiveness" : "Ефективність", + "Email" : "Електронна пошта", + "Emails" : "Електронні листи", + "Embedding Model" : "Модель вкладень", + "Embedding Provider" : "Постачальник вкладень", + "Embedding generated successfully" : "Вкладення успішно згенеровано", + "Embedding provider connection successful!" : "З'єднання з постачальником вкладень успішне!", + "Enable" : "Увімкнути", + "Enable auto-creation" : "Увімкнути автоматичне створення", + "Enable automatic file vectorization" : "Увімкнути автоматичну векторизацію Файлів", + "Enable automatic object vectorization" : "Увімкнути автоматичну векторизацію об'єктів", + "Enable automatic synchronization" : "Увімкнути автоматичну синхронізацію", + "Enable detailed logging for SOLR operations (recommended for debugging)" : "Увімкнути детальне журналювання для операцій SOLR (рекомендовано для налагодження)", + "Enable faceting" : "Увімкнути фасетування", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above." : "Увімкніть або вимкніть функції LLM. Налаштуйте постачальників і моделі за допомогою кнопки «Конфігурація LLM» вище.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above." : "Увімкніть або вимкніть інтеграцію пошуку SOLR. Налаштуйте параметри з'єднання за допомогою кнопки «Налаштування з'єднання» вище.", + "Enable or disable n8n workflow integration. Configure connection settings below." : "Увімкніть або вимкніть інтеграцію робочих процесів n8n. Налаштуйте параметри з'єднання нижче.", + "Enable or disable this webhook" : "Увімкнути або вимкнути цей вебхук", + "Enable vectorization for all existing and future views (may increase costs)" : "Увімкнути векторизацію для всіх наявних і майбутніх подань (може збільшити витрати)", + "Enabled" : "Увімкнено", + "Endpoints" : "Кінцеві точки", + "Enter ConfigSet name" : "Введіть назву ConfigSet", + "Enter collection name" : "Введіть назву колекції", + "Enter description (optional)..." : "Введіть опис (необов'язково)...", + "Enter new collection name" : "Введіть нову назву колекції", + "Enter object ID" : "Введіть ідентифікатор об'єкта", + "Enter search term" : "Введіть пошуковий запит", + "Enter view name..." : "Введіть назву подання...", + "Enter webhook name" : "Введіть назву вебхука", + "Entities" : "Сутності", + "Entity Information" : "Інформація про сутність", + "Entity deleted successfully" : "Сутність успішно видалено", + "Entity not found" : "Сутність не знайдено", + "Entries to be deleted:" : "Записи, які буде видалено:", + "Enum Issue" : "Проблема переліку", + "Enum constraint is missing" : "Відсутнє обмеження переліку", + "Error" : "Помилка", + "Error Details" : "Подробиці помилки", + "Error Information" : "Інформація про помилку", + "Error Message" : "Повідомлення про помилку", + "Error loading application" : "Помилка завантаження застосунку", + "Error loading audit trails" : "Помилка завантаження журналів аудиту", + "Error loading entity" : "Помилка завантаження сутності", + "Error loading search trails" : "Помилка завантаження журналів пошуку", + "Estimated Batches:" : "Орієнтовна кількість пакетів:", + "Estimated Cost:" : "Орієнтовна вартість:", + "Estimated Duration:" : "Орієнтовна тривалість:", + "Event" : "Подія", + "Event Property for Payload" : "Властивість події для корисного навантаження", + "Events" : "Події", + "Example Value" : "Приклад значення", + "Exclusive Maximum" : "Виключний максимум", + "Exclusive Minimum" : "Виключний мінімум", + "Execution Mode" : "Режим виконання", + "Execution Time" : "Час виконання", + "Execution Time Range" : "Діапазон часу виконання", + "Existing Improvements" : "Наявні покращення", + "Exponential" : "Експоненційний", + "Export" : "Експортувати", + "Export completed successfully" : "Експортування успішно завершено", + "Export, view, or delete audit trails" : "Експортуйте, переглядайте або видаляйте журнали аудиту", + "Extend Schema" : "Розширити Схему", + "Extended" : "Розширено", + "Extensions" : "Розширення", + "Extract Pending Files" : "Видобути Файли, що очікують", + "Extract properties from each object" : "Видобувати Властивості з кожного об'єкта", + "Extracted At" : "Видобуто о", + "Extraction Status" : "Статус видобування", + "Facet configuration updated successfully" : "Конфігурацію фасетів успішно оновлено", + "Facets discovered and configured successfully" : "Фасети успішно виявлено та налаштовано", + "Facets discovered successfully" : "Фасети успішно виявлено", + "Failed" : "Не вдалося", + "Failed to analyze schema properties" : "Не вдалося проаналізувати Властивості Схеми", + "Failed to archive conversation" : "Не вдалося архівувати розмову", + "Failed to calculate sizes" : "Не вдалося обчислити розміри", + "Failed to clear collection" : "Не вдалося очистити колекцію", + "Failed to copy changes" : "Не вдалося скопіювати зміни", + "Failed to copy data" : "Не вдалося скопіювати дані", + "Failed to copy data to clipboard" : "Не вдалося скопіювати дані до буфера обміну", + "Failed to create conversation" : "Не вдалося створити розмову", + "Failed to create or find project" : "Не вдалося створити або знайти проект", + "Failed to delete collection" : "Не вдалося видалити колекцію", + "Failed to delete conversation" : "Не вдалося видалити розмову", + "Failed to delete webhook" : "Не вдалося видалити вебхук", + "Failed to download API specification" : "Не вдалося завантажити специфікацію API", + "Failed to get SOLR field configuration" : "Не вдалося отримати конфігурацію поля SOLR", + "Failed to get entity categories" : "Не вдалося отримати категорії сутностей", + "Failed to get entity statistics" : "Не вдалося отримати статистику сутностей", + "Failed to get entity types" : "Не вдалося отримати типи сутностей", + "Failed to load LLM configuration" : "Не вдалося завантажити конфігурацію LLM", + "Failed to load Nextcloud groups" : "Не вдалося завантажити групи Nextcloud", + "Failed to load conversation" : "Не вдалося завантажити розмову", + "Failed to load entities" : "Не вдалося завантажити сутності", + "Failed to load entity" : "Не вдалося завантажити сутність", + "Failed to load files" : "Не вдалося завантажити Файли", + "Failed to load organisations" : "Не вдалося завантажити організації", + "Failed to load templates" : "Не вдалося завантажити шаблони", + "Failed to load webhooks" : "Не вдалося завантажити вебхуки", + "Failed to load workflows" : "Не вдалося завантажити робочі процеси", + "Failed to refresh database information" : "Не вдалося оновити інформацію про базу даних", + "Failed to reindex collection" : "Не вдалося переіндексувати колекцію", + "Failed to rename conversation" : "Не вдалося перейменувати розмову", + "Failed to restore conversation" : "Не вдалося відновити розмову", + "Failed to retry extraction" : "Не вдалося повторити видобування", + "Failed to retry webhook" : "Не вдалося повторити вебхук", + "Failed to save GitHub token" : "Не вдалося зберегти токен GitHub", + "Failed to save GitLab URL" : "Не вдалося зберегти URL-адресу GitLab", + "Failed to save GitLab token" : "Не вдалося зберегти токен GitLab", + "Failed to save additional feedback" : "Не вдалося зберегти додатковий відгук", + "Failed to save n8n configuration" : "Не вдалося зберегти конфігурацію n8n", + "Failed to save roles" : "Не вдалося зберегти ролі", + "Failed to save settings" : "Не вдалося зберегти налаштування", + "Failed to save webhook" : "Не вдалося зберегти вебхук", + "Failed to send feedback" : "Не вдалося надіслати відгук", + "Failed to test webhook" : "Не вдалося протестувати вебхук", + "Failed to update favorite status" : "Не вдалося оновити статус обраного", + "Failed to update schema properties" : "Не вдалося оновити Властивості Схеми", + "Failed to update view" : "Не вдалося оновити подання", + "Failed to update webhook" : "Не вдалося оновити вебхук", + "Feedback recorded" : "Відгук записано", + "Fewer sources (1-3):" : "Менше Джерел (1-3):", + "Field" : "Поле", + "File" : "Файл", + "File Chunk Prediction" : "Прогноз фрагментів Файлу", + "File Collection" : "Колекція Файлів", + "File Management" : "Керування Файлами", + "File Name" : "Назва Файлу", + "File Path" : "Шлях до Файлу", + "File Type Selection" : "Вибір типу Файлу", + "File Vectorization" : "Векторизація Файлів", + "File Warmup" : "Прогрівання Файлів", + "File actions menu" : "Меню дій із Файлом", + "File anonymized successfully" : "Файл успішно анонімізовано", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development." : "Векторизацію фрагментів Файлів ще не реалізовано. Фрагменти готові та збережені, але служба векторизації перебуває в розробці.", + "File collection not configured" : "Колекцію Файлів не налаштовано", + "File discovery completed" : "Виявлення Файлів завершено", + "File extraction completed" : "Видобування Файлів завершено", + "File extraction queued" : "Видобування Файлу поставлено в чергу", + "File indexed successfully" : "Файл успішно проіндексовано", + "File is already anonymized" : "Файл вже анонімізовано", + "File not found" : "Файл не знайдено", + "File settings updated successfully" : "Налаштування Файлу успішно оновлено", + "File sources" : "Джерела Файлів", + "File vectorization configuration saved successfully" : "Конфігурацію векторизації Файлів успішно збережено", + "File vectorization started. Check the statistics section for progress." : "Векторизацію Файлів розпочато. Перевірте розділ статистики для відстеження прогресу.", + "File warmup completed" : "Прогрівання Файлів завершено", + "Files" : "Файли", + "Files with Completed Extraction:" : "Файли із завершеним видобуванням:", + "Files → fileCollection, Objects → objectCollection" : "Файли → fileCollection, Об'єкти → objectCollection", + "Filter Audit Trails" : "Фільтрувати журнали аудиту", + "Filter Deleted Items" : "Фільтрувати видалені елементи", + "Filter Objects" : "Фільтрувати об'єкти", + "Filter Properties" : "Фільтрувати Властивості", + "Filter Search Trails" : "Фільтрувати журнали пошуку", + "Filter Statistics" : "Статистика фільтрування", + "Filter and analyze search trail entries" : "Фільтруйте та аналізуйте записи журналу пошуку", + "Filter and manage audit trail entries" : "Фільтруйте та керуйте записами журналу аудиту", + "Filter and manage soft deleted items" : "Фільтруйте та керуйте м'яко видаленими елементами", + "Filter by object ID" : "Фільтрувати за ідентифікатором об'єкта", + "Filter by search term" : "Фільтрувати за пошуковим запитом", + "Filter by webhook" : "Фільтрувати за вебхуком", + "Filter data loaded automatically. Use the filters below to refine your search." : "Дані фільтра завантажено автоматично. Використовуйте фільтри нижче, щоб уточнити пошук.", + "Filter webhook triggers by payload properties (one per line, format: key: value)" : "Фільтрувати тригери вебхуків за Властивостями корисного навантаження (по одному на рядок, формат: ключ: значення)", + "Filtered" : "Відфільтровано", + "Filters" : "Фільтри", + "Fireworks AI Chat Settings" : "Налаштування чату Fireworks AI", + "Fireworks AI Embedding Configuration" : "Конфігурація вкладень Fireworks AI", + "First" : "Перший", + "Fixed" : "Виправлено", + "For chat and retrieval-augmented generation" : "Для чату та генерації з доповненим пошуком", + "For vector embeddings and semantic search" : "Для векторних вкладень і семантичного пошуку", + "Format" : "Формат", + "Format Issue" : "Проблема формату", + "Format constraint is missing" : "Відсутнє обмеження формату", + "From Date" : "Від дати", + "From date" : "Від дати", + "Full data copied to clipboard" : "Повні дані скопійовано до буфера обміну", + "General Issue" : "Загальна проблема", + "Generate recommendations and confidence scores" : "Генерувати рекомендації та оцінки впевненості", + "Generate vectors immediately when new objects are created" : "Генерувати вектори негайно під час створення нових об'єктів", + "GitHub token is valid" : "Токен GitHub дійсний", + "GitHub token removed successfully" : "Токен GitHub успішно видалено", + "GitHub token saved successfully" : "Токен GitHub успішно збережено", + "GitLab URL saved successfully" : "URL-адресу GitLab успішно збережено", + "GitLab token is valid" : "Токен GitLab дійсний", + "GitLab token saved successfully" : "Токен GitLab успішно збережено", + "HTTP Method" : "Метод HTTP", + "HTTP method used to send webhook requests" : "Метод HTTP, що використовується для надсилання запитів вебхуків", + "Headers" : "Заголовки", + "Health" : "Стан", + "Heartbeat successful - connection kept alive" : "Контрольний сигнал успішний — з'єднання підтримується", + "Helpful" : "Корисно", + "Hide Filters" : "Приховати фільтри", + "Hide in forms" : "Приховати у формах", + "Hide in list view" : "Приховати у поданні списком", + "High" : "Високий", + "High Confidence" : "Висока впевненість", + "Host" : "Хост", + "Hourly" : "Щогодини", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size." : "Наскільки глибоко обходити вкладені Властивості об'єкта (1-20). Вищі значення фіксують більше деталей, але збільшують розмір вектора.", + "How often to check for updates (1-168 hours)" : "Як часто перевіряти наявність оновлень (1-168 годин)", + "How to handle retries for failed webhook deliveries" : "Як обробляти повторення для невдалих доставок вебхуків", + "ID" : "ID", + "ID:" : "ID:", + "IP rate limits cleared successfully" : "Обмеження швидкості за IP успішно очищено", + "Identify properties not in the schema" : "Визначати Властивості, відсутні у Схемі", + "Immutable" : "Незмінний", + "Import" : "Імпортувати", + "Import successful" : "Імпортування успішне", + "Improved Property" : "Покращена Властивість", + "In use" : "Використовується", + "Inactive" : "Неактивний", + "Include IDs and names of related objects for better contextual search" : "Включати ідентифікатори та назви пов'язаних об'єктів для кращого контекстного пошуку", + "Include related object references" : "Включати посилання на пов'язані об'єкти", + "Include schema and register metadata" : "Включати метадані Схеми та Реєстру", + "Initialization failed" : "Ініціалізація не вдалася", + "Initialize Project" : "Ініціалізувати проект", + "Inspect Fields" : "Перевірити поля", + "Inspect Index" : "Перевірити індекс", + "Invalid" : "Недійсний", + "Invalid batch size. Must be between 1 and 5000" : "Недійсний розмір пакету. Має бути між 1 і 5000", + "Invalid field name provided" : "Надано недійсну назву поля", + "Invalid maxObjects. Must be 0 (all) or positive number" : "Недійсне maxObjects. Має бути 0 (усі) або додатне число", + "Invalid:" : "Недійсний:", + "Issue" : "Проблема", + "Items per page:" : "Елементів на сторінці:", + "Just now" : "Щойно", + "LLM Configuration" : "Конфігурація LLM", + "LLM actions menu" : "Меню дій LLM", + "LLM configuration saved successfully" : "Конфігурацію LLM успішно збережено", + "LLM features disabled" : "Функції LLM вимкнено", + "LLM features enabled" : "Функції LLM увімкнено", + "LLM must be enabled with an embedding provider configured" : "LLM має бути увімкнено з налаштованим постачальником вкладень", + "LLM settings updated successfully" : "Налаштування LLM успішно оновлено", + "Last" : "Останній", + "Last Triggered" : "Останнє спрацювання", + "Length Range:" : "Діапазон довжини:", + "Linear" : "Лінійний", + "Load Advanced Filters" : "Завантажити розширені фільтри", + "Load advanced filters with live data from your search index" : "Завантажити розширені фільтри з реальними даними з вашого пошукового індексу", + "Load view" : "Завантажити подання", + "Loading ConfigSets..." : "Завантаження ConfigSets...", + "Loading advanced filters..." : "Завантаження розширених фільтрів...", + "Loading agents..." : "Завантаження агентів...", + "Loading application details..." : "Завантаження подробиць застосунку...", + "Loading applications..." : "Завантаження застосунків...", + "Loading archived conversations..." : "Завантаження архівованих розмов...", + "Loading audit trails..." : "Завантаження журналів аудиту...", + "Loading collections..." : "Завантаження колекцій...", + "Loading configurations..." : "Завантаження конфігурацій...", + "Loading conversation..." : "Завантаження розмови...", + "Loading conversations..." : "Завантаження розмов...", + "Loading deleted items..." : "Завантаження видалених елементів...", + "Loading events..." : "Завантаження подій...", + "Loading filter data..." : "Завантаження даних фільтра...", + "Loading groups..." : "Завантаження груп...", + "Loading log details..." : "Завантаження подробиць журналу...", + "Loading registers..." : "Завантаження Реєстрів...", + "Loading schemas..." : "Завантаження Схем...", + "Loading search trails..." : "Завантаження журналів пошуку...", + "Loading sources..." : "Завантаження Джерел...", + "Loading statistics..." : "Завантаження статистики...", + "Loading users..." : "Завантаження користувачів...", + "Loading views..." : "Завантаження подань...", + "Loading..." : "Завантаження...", + "Local" : "Локальний", + "Local Version" : "Локальна версія", + "Locked" : "Заблоковано", + "Locked:" : "Заблоковано:", + "Login successful" : "Вхід успішний", + "Logs" : "Журнали", + "Low" : "Низький", + "Low Confidence" : "Низька впевненість", + "Lucene Version:" : "Версія Lucene:", + "Magic table synchronized successfully" : "Магічну таблицю успішно синхронізовано", + "Manage Organisation Roles" : "Керувати ролями організації", + "Manage SOLR Collections (data stores) and assign them for objects and files." : "Керуйте колекціями SOLR (сховищами даних) і призначайте їх для об'єктів і Файлів.", + "Manage SOLR ConfigSets (configuration templates) for your collections." : "Керуйте ConfigSets SOLR (шаблонами конфігурації) для ваших колекцій.", + "Manage and configure agents for automated tasks" : "Керуйте та налаштовуйте агентів для автоматизованих завдань", + "Manage and monitor file text extraction status" : "Керуйте та відстежуйте статус видобування тексту з Файлів", + "Manage and restore soft deleted items from your registers" : "Керуйте та відновлюйте м'яко видалені елементи з ваших Реєстрів", + "Manage and view detected entities from files and objects" : "Керуйте та переглядайте виявлені сутності з Файлів і об'єктів", + "Manage document templates and themes" : "Керуйте шаблонами та темами документів", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project." : "Керуйте робочими процесами n8n для автоматизації OpenRegister. Робочі процеси зберігатимуться в налаштованому проекті.", + "Manage webhooks for event-driven integrations" : "Керуйте вебхуками для подієво-керованих інтеграцій", + "Manage your applications and modules" : "Керуйте вашими застосунками та модулями", + "Manage your data registers and their configurations" : "Керуйте вашими Реєстрами даних та їхніми конфігураціями", + "Manage your data schemas and their properties" : "Керуйте вашими Схемами даних та їхніми Властивостями", + "Manage your data sources and their configurations" : "Керуйте вашими Джерелами даних та їхніми конфігураціями", + "Manage your saved search configurations" : "Керуйте вашими збереженими конфігураціями пошуку", + "Manage your system configurations and settings" : "Керуйте вашими системними конфігураціями та налаштуваннями", + "Managed" : "Кероване", + "Max Files (0 = all)" : "Макс. Файлів (0 = усі)", + "Max Length" : "Макс. довжина", + "Max Length:" : "Макс. довжина:", + "Max Objects (0 = all)" : "Макс. об'єктів (0 = усі)", + "Max Retries" : "Макс. повторень", + "Max Shards/Node" : "Макс. шардів/вузол", + "Max execution time (ms)" : "Макс. час виконання (мс)", + "Max ms" : "Макс. мс", + "Max result count" : "Макс. кількість результатів", + "Max results" : "Макс. результатів", + "Maximum" : "Максимум", + "Maximum Nesting Depth" : "Максимальна глибина вкладеності", + "Maximum length constraint is missing" : "Відсутнє обмеження максимальної довжини", + "Maximum length is too restrictive" : "Максимальна довжина надто обмежувальна", + "Maximum number of files to process. Set to 0 to process all files." : "Максимальна кількість Файлів для оброблення. Установіть 0, щоб обробити всі Файли.", + "Maximum number of objects to process. Set to 0 to process all objects." : "Максимальна кількість об'єктів для оброблення. Установіть 0, щоб обробити всі об'єкти.", + "Maximum number of retry attempts for failed deliveries" : "Максимальна кількість спроб повторення для невдалих доставок", + "Maximum time to wait before committing changes" : "Максимальний час очікування перед фіксацією змін", + "Maximum tokens to generate" : "Максимальна кількість токенів для генерації", + "Maximum value constraint is missing" : "Відсутнє обмеження максимального значення", + "Maximum value is too restrictive" : "Максимальне значення надто обмежувальне", + "Medium" : "Середній", + "Medium Confidence" : "Середня впевненість", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)" : "Середні запити: пошуки з певним фільтруванням або декількома параметрами (наприклад, діапазони дат, конкретні Реєстри/Схеми)", + "Members of selected groups can access this view" : "Учасники вибраних груп можуть отримати доступ до цього подання", + "Members:" : "Учасники:", + "Memory prediction calculated successfully" : "Прогноз пам'яті успішно обчислено", + "Message content is required" : "Потрібен вміст повідомлення", + "Message does not belong to this conversation" : "Повідомлення не належить до цієї розмови", + "Metadata Filters" : "Фільтри метаданих", + "Method" : "Метод", + "Min Length" : "Мін. довжина", + "Min execution time (ms)" : "Мін. час виконання (мс)", + "Min ms" : "Мін. мс", + "Min result count" : "Мін. кількість результатів", + "Min results" : "Мін. результатів", + "Minimum" : "Мінімум", + "Minimum value constraint is missing" : "Відсутнє обмеження мінімального значення", + "Minimum value is too restrictive" : "Мінімальне значення надто обмежувальне", + "Missing object fields created successfully" : "Відсутні поля об'єкта успішно створено", + "Mode:" : "Режим:", + "Model" : "Модель", + "Model Name" : "Назва моделі", + "Modified" : "Змінено", + "Monthly" : "Щомісяця", + "More sources (10-20):" : "Більше Джерел (10-20):", + "Most Active Objects" : "Найактивніші об'єкти", + "Multiple Of" : "Кратне", + "Name" : "Назва", + "Names cache warmup completed" : "Прогрівання кешу імен завершено", + "New Collection Name" : "Назва нової колекції", + "New Conversation" : "Нова розмова", + "New Properties" : "Нові Властивості", + "New Property" : "Нова Властивість", + "New Value" : "Нове значення", + "New users without specific organisation membership will be automatically added to this organisation" : "Нових користувачів без конкретного членства в організації буде автоматично додано до цієї організації", + "Next" : "Далі", + "No" : "Ні", + "No ConfigSets found" : "ConfigSets не знайдено", + "No GitHub token configured" : "Токен GitHub не налаштовано", + "No GitHub token provided" : "Токен GitHub не надано", + "No GitLab token provided" : "Токен GitLab не надано", + "No Organisation" : "Немає організації", + "No active filters" : "Немає активних фільтрів", + "No activity data available" : "Дані про активність недоступні", + "No agents available" : "Немає доступних агентів", + "No agents found" : "Агентів не знайдено", + "No app store cache exists yet" : "Кеш магазину застосунків ще не існує", + "No applications are available." : "Немає доступних застосунків.", + "No applications found" : "Застосунків не знайдено", + "No archived conversations" : "Немає архівованих розмов", + "No audit trail entries found" : "Записів журналу аудиту не знайдено", + "No changes recorded" : "Зміни не записано", + "No chunks to vectorize" : "Немає фрагментів для векторизації", + "No collections found" : "Колекцій не знайдено", + "No configuration data" : "Немає даних конфігурації", + "No configurations are available." : "Немає доступних конфігурацій.", + "No configurations found" : "Конфігурацій не знайдено", + "No configurations found for this application." : "Для цього застосунку конфігурацій не знайдено.", + "No conversations yet. Create a new one to get started!" : "Поки що немає розмов. Створіть нову, щоб почати!", + "No data available" : "Немає доступних даних", + "No data available for chart" : "Немає доступних даних для діаграми", + "No deleted items found" : "Видалених елементів не знайдено", + "No deletion data available" : "Немає доступних даних про видалення", + "No description found" : "Опису не знайдено", + "No description provided" : "Опис не надано", + "No entities detected in this file. Run text extraction first." : "У цьому Файлі не виявлено сутностей. Спочатку запустіть видобування тексту.", + "No entities found" : "Сутностей не знайдено", + "No entities have been detected yet" : "Сутностей ще не виявлено", + "No events found" : "Подій не знайдено", + "No expired audit trails found to clear" : "Прострочених журналів аудиту для очищення не знайдено", + "No facetable fields available. Select a register and schema to see available filters." : "Немає доступних полів для фасетування. Виберіть Реєстр і Схему, щоб побачити доступні фільтри.", + "No files found" : "Файлів не знайдено", + "No files have been extracted yet" : "Файли ще не видобуто", + "No files to process" : "Немає Файлів для оброблення", + "No files to reindex" : "Немає Файлів для переіндексування", + "No filters are currently active. This will delete ALL audit trail entries!" : "Наразі немає активних фільтрів. Це видалить УСІ записи журналу аудиту!", + "No log entries found" : "Записів журналу не знайдено", + "No logs are available for this configuration." : "Для цієї конфігурації журнали недоступні.", + "No logs are available for this source." : "Для цього Джерела журнали недоступні.", + "No logs found" : "Журналів не знайдено", + "No mismatched fields found - SOLR schema is properly configured" : "Невідповідних полів не знайдено — Схему SOLR налаштовано правильно", + "No objects found" : "Об'єктів не знайдено", + "No objects found to vectorize" : "Об'єктів для векторизації не знайдено", + "No objects selected for deletion" : "Не вибрано об'єктів для видалення", + "No objects to delete" : "Немає об'єктів для видалення", + "No objects were permanently deleted" : "Жоден об'єкт не було остаточно видалено", + "No properties found" : "Властивостей не знайдено", + "No properties match your filters." : "Жодна Властивість не відповідає вашим фільтрам.", + "No purge date set" : "Дату очищення не встановлено", + "No register data available" : "Немає доступних даних Реєстру", + "No registers found" : "Реєстрів не знайдено", + "No registers found for this application." : "Для цього застосунку Реєстрів не знайдено.", + "No relations found" : "Зв'язків не знайдено", + "No request body available" : "Тіло запиту недоступне", + "No response body available" : "Тіло відповіді недоступне", + "No saved views yet. Create one in the Search tab!" : "Поки що немає збережених подань. Створіть одне на вкладці «Пошук»!", + "No schema data available" : "Немає доступних даних Схеми", + "No schema selected for exploration" : "Не вибрано Схему для дослідження", + "No schemas found" : "Схем не знайдено", + "No schemas found for this application." : "Для цього застосунку Схем не знайдено.", + "No search terms data available" : "Немає доступних даних про пошукові запити", + "No search trail entries found" : "Записів журналу пошуку не знайдено", + "No sources are available." : "Немає доступних Джерел.", + "No sources found" : "Джерел не знайдено", + "No templates found" : "Шаблонів не знайдено", + "No templates have been created yet" : "Шаблони ще не створено", + "No valid magic-mapped register+schema combinations found" : "Не знайдено дійсних комбінацій Реєстр+Схема з магічним зіставленням", + "No views found. Create views first before configuring vectorization." : "Подань не знайдено. Спочатку створіть подання, перш ніж налаштовувати векторизацію.", + "No views match your search" : "Жодне подання не відповідає вашому пошуку", + "No webhooks found" : "Вебхуків не знайдено", + "No webhooks have been configured yet" : "Вебхуки ще не налаштовано", + "No workflows found in this project. Create workflows in the n8n editor." : "У цьому проекті робочих процесів не знайдено. Створіть робочі процеси в редакторі n8n.", + "None" : "Немає", + "Not helpful" : "Некорисно", + "Not in use" : "Не використовується", + "Number Constraints" : "Числові обмеження", + "Number of chunks to vectorize in one API call (1-100)." : "Кількість фрагментів для векторизації в одному виклику API (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Кількість фрагментів для векторизації в одному виклику API. Вище = швидше, але більше пам'яті. Рекомендовано: 10-50.", + "Number of objects to process in each batch (1-5000)." : "Кількість об'єктів для оброблення в кожному пакеті (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50." : "Кількість об'єктів для векторизації в одному виклику API. Вище = швидше, але більше пам'яті. Рекомендовано: 10-50.", + "Numeric Range:" : "Числовий діапазон:", + "Object" : "Об'єкт", + "Object Collection" : "Колекція об'єктів", + "Object Count Prediction" : "Прогноз кількості об'єктів", + "Object ID" : "Ідентифікатор об'єкта", + "Object Management" : "Керування об'єктами", + "Object Vectorization" : "Векторизація об'єктів", + "Object Warmup" : "Прогрівання об'єктів", + "Object collection not configured" : "Колекцію об'єктів не налаштовано", + "Object not found" : "Об'єкт не знайдено", + "Object permanently deleted" : "Об'єкт остаточно видалено", + "Object restored successfully" : "Об'єкт успішно відновлено", + "Object settings updated successfully" : "Налаштування об'єкта успішно оновлено", + "Object sources" : "Джерела об'єктів", + "Object unlocked successfully" : "Об'єкт успішно розблоковано", + "Object vectorization configuration saved successfully" : "Конфігурацію векторизації об'єктів успішно збережено", + "Object vectorized successfully" : "Об'єкт успішно векторизовано", + "Object:" : "Об'єкт:", + "Objects" : "Об'єкти", + "Objects Analyzed" : "Проаналізовані об'єкти", + "Objects Distribution" : "Розподіл об'єктів", + "Objects being analyzed" : "Об'єкти, що аналізуються", + "Objects by Register" : "Об'єкти за Реєстром", + "Objects by Schema" : "Об'єкти за Схемою", + "Objects deletion completed successfully" : "Видалення об'єктів успішно завершено", + "Objects in schema" : "Об'єкти у Схемі", + "Objects to Process:" : "Об'єкти для оброблення:", + "Objects to be analyzed" : "Об'єкти, які буде проаналізовано", + "Objects to be deleted" : "Об'єкти, які буде видалено", + "Objects to be validated" : "Об'єкти, які буде перевірено", + "Objects will be serialized as JSON text before vectorization" : "Об'єкти буде серіалізовано як текст JSON перед векторизацією", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed." : "Об'єкти буде м'яко видалено (позначено як видалені, але збережено в базі даних). За потреби їх можна відновити пізніше.", + "Old Value" : "Старе значення", + "Oldest Item (days)" : "Найстаріший елемент (днів)", + "Ollama Chat Settings" : "Налаштування чату Ollama", + "Ollama Embedding Configuration" : "Конфігурація вкладень Ollama", + "Ollama URL" : "URL-адреса Ollama", + "Only With Changes" : "Лише зі змінами", + "Open Register Objects" : "Об'єкти Open Register", + "Open n8n Editor" : "Відкрити редактор n8n", + "OpenAI Chat Settings" : "Налаштування чату OpenAI", + "OpenAI Embedding Configuration" : "Конфігурація вкладень OpenAI", + "OpenRegister Settings" : "Налаштування OpenRegister", + "Optional webhook secret for signature verification" : "Необов'язковий секрет вебхука для перевірки підпису", + "Order" : "Порядок", + "Organisation" : "Організація", + "Organisation ID" : "Ідентифікатор організації", + "Organisation Members" : "Учасники організації", + "Organisation Statistics" : "Статистика організації", + "Organisation created successfully" : "Організацію успішно створено", + "Organisation settings saved successfully" : "Налаштування організації успішно збережено", + "Organisations" : "Організації", + "Organization" : "Організація", + "Organization ID (Optional)" : "Ідентифікатор організації (необов'язково)", + "Orphaned Items" : "Осиротілі елементи", + "Owner" : "Власник", + "Owner:" : "Власник:", + "Parallel Mode (Faster, more resource intensive)" : "Паралельний режим (швидше, більше ресурсів)", + "Parallel:" : "Паралельно:", + "Password" : "Пароль", + "Password for SOLR authentication (optional)" : "Пароль для автентифікації SOLR (необов'язково)", + "Path" : "Шлях", + "Path in repository" : "Шлях у репозиторії", + "Path where the configuration file will be saved in the repository" : "Шлях, за яким файл конфігурації буде збережено в репозиторії", + "Path where the register OAS file will be saved in the repository" : "Шлях, за яким файл OAS Реєстру буде збережено в репозиторії", + "Pattern (regex)" : "Шаблон (регулярний вираз)", + "Pattern Issue" : "Проблема шаблону", + "Pattern constraint is missing" : "Відсутнє обмеження шаблону", + "Patterns:" : "Шаблони:", + "Pending" : "Очікує", + "Permanent Delete Mode" : "Режим остаточного видалення", + "Permanently Delete" : "Видалити остаточно", + "Permanently delete already soft-deleted objects" : "Остаточно видалити вже м'яко видалені об'єкти", + "Permanently delete this conversation?" : "Остаточно видалити цю розмову?", + "Person" : "Особа", + "Personal Data" : "Персональні дані", + "Phone" : "Телефон", + "Photos" : "Фото", + "Please create an agent in the" : "Будь ласка, створіть агента в", + "Please select an agent to continue" : "Будь ласка, виберіть агента, щоб продовжити", + "Please select which register and schema to use for the new object" : "Будь ласка, виберіть, який Реєстр і Схему використати для нового об'єкта", + "Please try again later." : "Будь ласка, спробуйте пізніше.", + "Please wait while we fetch your agents." : "Будь ласка, зачекайте, поки ми отримуємо ваших агентів.", + "Please wait while we fetch your applications." : "Будь ласка, зачекайте, поки ми отримуємо ваші застосунки.", + "Please wait while we fetch your configurations." : "Будь ласка, зачекайте, поки ми отримуємо ваші конфігурації.", + "Please wait while we fetch your deleted items." : "Будь ласка, зачекайте, поки ми отримуємо ваші видалені елементи.", + "Please wait while we fetch your sources." : "Будь ласка, зачекайте, поки ми отримуємо ваші Джерела.", + "Polls" : "Опитування", + "Popular Search Terms" : "Популярні пошукові запити", + "Port" : "Порт", + "Prerequisites" : "Передумови", + "Press Enter to send, Shift+Enter for new line" : "Натисніть Enter, щоб надіслати, Shift+Enter для нового рядка", + "Previous" : "Попередній", + "Private" : "Приватний", + "Processes chunks in batches with simulated parallelism." : "Обробляє фрагменти пакетами з імітованим паралелізмом.", + "Processes file chunks sequentially (safest)." : "Обробляє фрагменти Файлів послідовно (найбезпечніше).", + "Processes objects in chunks with simulated parallelism." : "Обробляє об'єкти фрагментами з імітованим паралелізмом.", + "Processes objects sequentially (safest)." : "Обробляє об'єкти послідовно (найбезпечніше).", + "Processing" : "Оброблення", + "Processing Limits" : "Обмеження оброблення", + "Processing..." : "Оброблення...", + "Project initialized successfully" : "Проект успішно ініціалізовано", + "Project not found. Please initialize first." : "Проект не знайдено. Будь ласка, спочатку ініціалізуйте.", + "Properties" : "Властивості", + "Property Behaviors" : "Поведінка Властивостей", + "Property Configuration:" : "Конфігурація Властивості:", + "Property Filters" : "Фільтри Властивостей", + "Property Title" : "Назва Властивості", + "Property Type" : "Тип Властивості", + "Property can be improved" : "Властивість можна покращити", + "Protected" : "Захищений", + "Provider" : "Постачальник", + "Provider is required for testing" : "Для тестування потрібен постачальник", + "Public" : "Публічний", + "Public View" : "Публічне подання", + "Public views can be accessed by anyone in the system" : "До публічних подань може отримати доступ будь-хто в системі", + "Publish" : "Опублікувати", + "Publish Objects" : "Опублікувати об'єкти", + "Publish all objects in this schema" : "Опублікувати всі об'єкти в цій Схемі", + "Published" : "Опубліковано", + "Published:" : "Опубліковано:", + "Publishing..." : "Публікування...", + "Purge" : "Очистити", + "Purge Date" : "Дата очищення", + "Query Complexity Distribution" : "Розподіл складності запитів", + "Query parameter is required" : "Потрібен параметр запиту", + "RAG" : "RAG", + "RAG Configuration" : "Конфігурація RAG", + "Rate limits cleared successfully" : "Обмеження швидкості успішно очищено", + "Raw Changes Data" : "Необроблені дані змін", + "Re-vectorize on object update" : "Повторно векторизувати під час оновлення об'єкта", + "Read" : "Читання", + "Recommendations:" : "Рекомендації:", + "Recommended Type:" : "Рекомендований тип:", + "Recommended: 5 sources" : "Рекомендовано: 5 Джерел", + "Refresh" : "Оновити", + "Refresh Data" : "Оновити дані", + "Refresh Stats" : "Оновити статистику", + "Refresh Workflows" : "Оновити робочі процеси", + "Refresh dashboard" : "Оновити інформаційну панель", + "Refresh database info" : "Оновити інформацію про базу даних", + "Register" : "Реєстр", + "Register ID" : "Ідентифікатор Реєстру", + "Register Statistics" : "Статистика Реєстру", + "Register Totals" : "Підсумки Реєстру", + "Register depublished successfully" : "Реєстр успішно знято з публікації", + "Register not found" : "Реєстр не знайдено", + "Register objects deletion completed successfully" : "Видалення об'єктів Реєстру успішно завершено", + "Register published successfully" : "Реєстр успішно опубліковано", + "Register/Schema Usage" : "Використання Реєстру/Схеми", + "Registers" : "Реєстри", + "Reindex" : "Переіндексувати", + "Reindex completed" : "Переіндексування завершено", + "Reindex completed successfully" : "Переіндексування успішно завершено", + "Reindex failed" : "Переіндексування не вдалося", + "Relations" : "Зв'язки", + "Remote Version" : "Віддалена версія", + "Remove" : "Вилучити", + "Remove filter" : "Вилучити фільтр", + "Remove from favorites" : "Вилучити з обраного", + "Remove group" : "Вилучити групу", + "Removed" : "Вилучено", + "Removed from favorites" : "Вилучено з обраного", + "Rename Conversation" : "Перейменувати розмову", + "Rename conversation" : "Перейменувати розмову", + "Replicas" : "Репліки", + "Repository" : "Репозиторій", + "Request Body" : "Тіло запиту", + "Request Data" : "Дані запиту", + "Request Details" : "Подробиці запиту", + "Request body must be JSON with a \"query\" field" : "Тіло запиту має бути JSON з полем \"query\"", + "Request timeout in seconds" : "Час очікування запиту в секундах", + "Required field" : "Обов'язкове поле", + "Required status is inconsistent" : "Обов'язковий статус суперечливий", + "Rerun Search" : "Повторити пошук", + "Reset Changes" : "Скинути зміни", + "Reset Filters" : "Скинути фільтри", + "Response Body" : "Тіло відповіді", + "Response Details" : "Подробиці відповіді", + "Response Time:" : "Час відповіді:", + "Restore" : "Відновити", + "Restore conversation" : "Відновити розмову", + "Restore or permanently delete items" : "Відновити або остаточно видалити елементи", + "Result Count Range" : "Діапазон кількості результатів", + "Results" : "Результати", + "Retrieve all objects for this schema" : "Отримати всі об'єкти для цієї Схеми", + "Retry" : "Повторити", + "Retry Failed Extractions" : "Повторити невдалі видобування", + "Retry Policy" : "Політика повторення", + "Retry completed" : "Повторення завершено", + "Retrying..." : "Повторення...", + "Risk Level" : "Рівень ризику", + "Roles updated successfully" : "Ролі успішно оновлено", + "SOLR Connection Settings" : "Налаштування з'єднання SOLR", + "SOLR Version:" : "Версія SOLR:", + "SOLR actions menu" : "Меню дій SOLR", + "SOLR base path (usually /solr)" : "Базовий шлях SOLR (зазвичай /solr)", + "SOLR is disabled" : "SOLR вимкнено", + "SOLR is not available or not configured" : "SOLR недоступний або не налаштований", + "SOLR logging disabled" : "Журналювання SOLR вимкнено", + "SOLR logging enabled" : "Журналювання SOLR увімкнено", + "SOLR search disabled" : "Пошук SOLR вимкнено", + "SOLR search enabled" : "Пошук SOLR увімкнено", + "SOLR server hostname or IP address" : "Ім'я хоста або IP-адреса сервера SOLR", + "SOLR server port number (optional, defaults to 8983)" : "Номер порту сервера SOLR (необов'язково, за замовчуванням 8983)", + "SOLR setup completed successfully" : "Налаштування SOLR успішно завершено", + "SOLR setup failed" : "Налаштування SOLR не вдалося", + "SOLR setup failed - check logs" : "Налаштування SOLR не вдалося — перевірте журнали", + "Sample Values:" : "Зразки значень:", + "Save" : "Зберегти", + "Save Configuration" : "Зберегти конфігурацію", + "Save Connection Settings" : "Зберегти налаштування з'єднання", + "Save Roles" : "Зберегти ролі", + "Save Settings" : "Зберегти налаштування", + "Save current search as view" : "Зберегти поточний пошук як подання", + "Saved Views" : "Збережені подання", + "Saving..." : "Збереження...", + "Schema" : "Схема", + "Schema ID" : "Ідентифікатор Схеми", + "Schema Name" : "Назва Схеми", + "Schema Statistics" : "Статистика Схеми", + "Schema depublished successfully" : "Схему успішно знято з публікації", + "Schema objects deletion completed successfully" : "Видалення об'єктів Схеми успішно завершено", + "Schema published successfully" : "Схему успішно опубліковано", + "Schema successfully created" : "Схему успішно створено", + "Schema successfully updated" : "Схему успішно оновлено", + "Schemas" : "Схеми", + "Scheme" : "Схема", + "Search / Views" : "Пошук / Подання", + "Search Activity" : "Активність пошуку", + "Search Analytics" : "Аналітика пошуку", + "Search Entities" : "Пошук сутностей", + "Search Files" : "Пошук Файлів", + "Search GitHub" : "Пошук у GitHub", + "Search GitLab" : "Пошук у GitLab", + "Search Method" : "Метод пошуку", + "Search Term" : "Пошуковий запит", + "Search Trail Details" : "Подробиці журналу пошуку", + "Search Trail Management" : "Керування журналом пошуку", + "Search Trail Statistics" : "Статистика журналу пошуку", + "Search Trails" : "Журнали пошуку", + "Search Views" : "Подання пошуку", + "Search Webhooks" : "Пошук вебхуків", + "Search by file name or path" : "Пошук за назвою або шляхом Файлу", + "Search by name or URL" : "Пошук за назвою або URL-адресою", + "Search by value" : "Пошук за значенням", + "Search in Files" : "Пошук у Файлах", + "Search in Objects" : "Пошук в об'єктах", + "Search property names..." : "Пошук назв Властивостей...", + "Search trail deletion not implemented yet" : "Видалення журналу пошуку ще не реалізовано", + "Search views..." : "Пошук подань...", + "Secret" : "Секрет", + "Secret key for HMAC signature generation (optional)" : "Секретний ключ для генерації підпису HMAC (необов'язково)", + "Select AI Agent" : "Виберіть агента ШІ", + "Select All" : "Вибрати все", + "Select ConfigSet" : "Виберіть ConfigSet", + "Select File Types to Vectorize:" : "Виберіть типи Файлів для векторизації:", + "Select HTTP method" : "Виберіть метод HTTP", + "Select Register and Schema" : "Виберіть Реєстр і Схему", + "Select Views to Vectorize:" : "Виберіть подання для векторизації:", + "Select a Nextcloud group to add" : "Виберіть групу Nextcloud для додавання", + "Select a branch" : "Виберіть гілку", + "Select a model or type a custom model name" : "Виберіть модель або введіть власну назву моделі", + "Select a repository" : "Виберіть репозиторій", + "Select a repository you have write access to" : "Виберіть репозиторій, до якого ви маєте доступ на запис", + "Select an AI Agent" : "Виберіть агента ШІ", + "Select an AI agent to begin chatting with your data." : "Виберіть агента ШІ, щоб почати спілкуватися з вашими даними.", + "Select backend" : "Виберіть бекенд", + "Select chat model" : "Виберіть модель чату", + "Select collection for files" : "Виберіть колекцію для Файлів", + "Select collection for objects" : "Виберіть колекцію для об'єктів", + "Select default organisation" : "Виберіть організацію за замовчуванням", + "Select event to listen to..." : "Виберіть подію для прослуховування...", + "Select model" : "Виберіть модель", + "Select options..." : "Виберіть параметри...", + "Select period" : "Виберіть період", + "Select property to send as payload" : "Виберіть Властивість для надсилання як корисне навантаження", + "Select provider" : "Виберіть постачальника", + "Select registers and schemas to save a view" : "Виберіть Реєстри та Схеми, щоб зберегти подання", + "Select retry policy" : "Виберіть політику повторення", + "Select the branch to publish to" : "Виберіть гілку для публікування", + "Select the event this webhook should listen to" : "Виберіть подію, яку має прослуховувати цей вебхук", + "Select views to vectorize:" : "Виберіть подання для векторизації:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources." : "Виберіть, які групи Nextcloud доступні для цієї організації. Користувачі цих груп матимуть доступ до ресурсів організації.", + "Select which data views the AI can search" : "Виберіть, які подання даних ШІ може шукати", + "Select which property from the event should be used as the webhook payload data" : "Виберіть, яку Властивість з події слід використовувати як дані корисного навантаження вебхука", + "Select which tools the AI can use to perform actions" : "Виберіть, які інструменти ШІ може використовувати для виконання дій", + "Selected" : "Вибрано", + "Selected Groups" : "Вибрані групи", + "Selected audit trails deleted successfully" : "Вибрані журнали аудиту успішно видалено", + "Selected search trails deleted successfully" : "Вибрані журнали пошуку успішно видалено", + "Selected users can access this view" : "Вибрані користувачі можуть отримати доступ до цього подання", + "Send additional feedback" : "Надіслати додатковий відгук", + "Send as CloudEvent" : "Надіслати як CloudEvent", + "Sensitive PII" : "Конфіденційні PII", + "Serial Mode (Safer, slower)" : "Послідовний режим (безпечніше, повільніше)", + "Serial:" : "Послідовно:", + "Server Configuration" : "Конфігурація сервера", + "Server Information" : "Інформація про сервер", + "Settings" : "Налаштування", + "Shards" : "Шарди", + "Share" : "Поділитися", + "Share with Groups" : "Поділитися з групами", + "Share with Users" : "Поділитися з користувачами", + "Show Filters" : "Показати фільтри", + "Show less" : "Показати менше", + "Show only entries with changes" : "Показувати лише записи зі змінами", + "Simple" : "Простий", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)" : "Прості запити: базові текстові пошуки з мінімальними параметрами (наприклад, один пошуковий запит, без розширених фільтрів)", + "Size" : "Розмір", + "Slug" : "Слаг", + "Soft Delete Mode" : "Режим м'якого видалення", + "Soft Deleted Items" : "М'яко видалені елементи", + "SolrCloud mode enabled" : "Режим SolrCloud увімкнено", + "Source" : "Джерело", + "Sources" : "Джерела", + "Sources:" : "Джерела:", + "Standalone SOLR mode" : "Автономний режим SOLR", + "Start Conversation" : "Почати розмову", + "Start Vectorization" : "Почати векторизацію", + "Start a conversation" : "Почати розмову", + "Starting names cache warmup..." : "Початок прогрівання кешу імен...", + "Starting..." : "Початок...", + "Statistics" : "Статистика", + "Stats calculation not yet implemented" : "Обчислення статистики ще не реалізовано", + "Status" : "Статус", + "Status Code" : "Код статусу", + "String Constraints" : "Обмеження рядка", + "Success" : "Успіх", + "Success Rate" : "Частота успіху", + "Success Status" : "Статус успіху", + "Successful" : "Успішно", + "Successfully joined organisation" : "Успішно приєднано до організації", + "Sync Table" : "Синхронізувати таблицю", + "Synchronization Settings" : "Налаштування синхронізації", + "System Default" : "Системне за замовчуванням", + "Technical Description" : "Технічний опис", + "Technical description for developers and administrators" : "Технічний опис для розробників і адміністраторів", + "Temperature" : "Температура", + "Templates" : "Шаблони", + "Test" : "Тест", + "Test Chat" : "Тестувати чат", + "Test Connection" : "Тестувати з'єднання", + "Test Embedding" : "Тестувати вкладення", + "Test endpoint executed successfully" : "Тестову кінцеву точку успішно виконано", + "Test webhook delivered successfully" : "Тестовий вебхук успішно доставлено", + "Test webhook delivery failed" : "Доставка тестового вебхука не вдалася", + "Test webhook sent successfully" : "Тестовий вебхук успішно надіслано", + "Testing..." : "Тестування...", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings." : "Текстові фрагменти генеруються під час видобування Файлів і зберігаються в базі даних. Векторизація читає ці попередньо фрагментовані Файли та перетворює їх на вкладення.", + "Text extracted successfully" : "Текст успішно видобуто", + "Text extraction disabled" : "Видобування тексту вимкнено", + "The URL where webhook events will be sent" : "URL-адреса, на яку надсилатимуться події вебхука", + "The requested conversation does not exist" : "Запитувана розмова не існує", + "The saved view and all its search configuration" : "Збережене подання та вся його конфігурація пошуку", + "There are no audit trail entries matching your current filters." : "Немає записів журналу аудиту, що відповідають вашим поточним фільтрам.", + "There are no deleted items matching your current filters." : "Немає видалених елементів, що відповідають вашим поточним фільтрам.", + "There are no search trail entries matching your current filters." : "Немає записів журналу пошуку, що відповідають вашим поточним фільтрам.", + "There are no webhook log entries matching your filters." : "Немає записів журналу вебхуків, що відповідають вашим фільтрам.", + "This action cannot be undone." : "Цю дію неможливо скасувати.", + "This action cannot be undone. Make sure no collections are using this ConfigSet." : "Цю дію неможливо скасувати. Переконайтеся, що жодна колекція не використовує цей ConfigSet.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below." : "Цей агент не має налаштовуваних подань або інструментів, але ви все одно можете налаштувати параметри RAG нижче.", + "This analysis may take some time" : "Цей аналіз може зайняти деякий час", + "This audit trail entry does not contain any change information." : "Цей запис журналу аудиту не містить жодної інформації про зміни.", + "This configuration has no data defined." : "Ця конфігурація не має визначених даних.", + "This endpoint is deprecated. Use chunk-based endpoints instead." : "Ця кінцева точка застаріла. Натомість використовуйте кінцеві точки на основі фрагментів.", + "This entity has no relations to objects or files" : "Ця сутність не має зв'язків з об'єктами або Файлами", + "This is a test webhook from OpenRegister" : "Це тестовий вебхук від OpenRegister", + "This schema must use Magic Table configuration to sync" : "Ця Схема має використовувати конфігурацію Magic Table для синхронізації", + "This source has no associated registers." : "Це Джерело не має пов'язаних Реєстрів.", + "This will copy the _default ConfigSet with the new name" : "Це скопіює ConfigSet _default з новою назвою", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?" : "Це остаточно видалить УСІ вкладення (вектори) з бази даних. Вам потрібно буде повторно векторизувати всі об'єкти та Файли. Цю дію неможливо скасувати.\\n\\nВи впевнені, що хочете продовжити?", + "This will permanently delete:" : "Це остаточно видалить:", + "Timeout (seconds)" : "Час очікування (секунди)", + "Timestamp" : "Часова мітка", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization." : "Порада: вмикайте лише ті подання, які потребують семантичного пошуку, щоб мінімізувати витрати на вкладення. Прості таблиці пошуку рідко потребують векторизації.", + "Title" : "Назва", + "To Date" : "До дати", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits." : "Щоб налаштувати розмір фрагмента або стратегію, перейдіть до «Конфігурація Файлів → Обмеження оброблення».", + "To change the embedding provider or model, go to LLM Configuration." : "Щоб змінити постачальника вкладень або модель, перейдіть до «Конфігурація LLM».", + "To date" : "До дати", + "Toggle search sidebar" : "Перемкнути бічну панель пошуку", + "Tools" : "Інструменти", + "Top Deleters" : "Найактивніші видаляючі", + "Total" : "Усього", + "Total Audit Trails" : "Усього журналів аудиту", + "Total Chunks Available:" : "Усього доступних фрагментів:", + "Total Deleted Items" : "Усього видалених елементів", + "Total Members" : "Усього учасників", + "Total Objects" : "Усього об'єктів", + "Total Objects in Database:" : "Усього об'єктів у базі даних:", + "Total Organisations" : "Усього організацій", + "Total Results" : "Усього результатів", + "Total Searches" : "Усього пошуків", + "Total Size" : "Загальний розмір", + "Total Storage" : "Загальне сховище", + "Total:" : "Усього:", + "Totals" : "Підсумки", + "Type" : "Тип", + "Type Issue" : "Проблема типу", + "Type Variations:" : "Варіації типу:", + "Type must be \"positive\" or \"negative\"" : "Тип має бути \"positive\" або \"negative\"", + "Type to search for groups" : "Введіть для пошуку груп", + "Type to search for users" : "Введіть для пошуку користувачів", + "Type to search groups..." : "Введіть для пошуку груп...", + "Type to search users..." : "Введіть для пошуку користувачів...", + "URL" : "URL", + "URL where Ollama is running" : "URL-адреса, на якій працює Ollama", + "UUID:" : "UUID:", + "Unchanged" : "Без змін", + "Unique Search Terms" : "Унікальні пошукові запити", + "Unique Terms" : "Унікальні запити", + "Unknown" : "Невідомо", + "Unknown Register" : "Невідомий Реєстр", + "Unknown Schema" : "Невідома Схема", + "Untitled View" : "Подання без назви", + "Update" : "Оновити", + "Update Operations" : "Операції оновлення", + "Update configuration: ..." : "Оновити конфігурацію: ...", + "Update register OAS: ..." : "Оновити OAS Реєстру: ...", + "Update vectors when object data changes (recommended for accurate search)" : "Оновлювати вектори, коли дані об'єкта змінюються (рекомендовано для точного пошуку)", + "Updated" : "Оновлено", + "Updated At" : "Оновлено о", + "Updated:" : "Оновлено:", + "Uptime:" : "Час роботи:", + "Usage Count" : "Кількість використань", + "Use SolrCloud with Zookeeper for distributed search" : "Використовуйте SolrCloud із Zookeeper для розподіленого пошуку", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID." : "Використовуйте фільтри, щоб звузити записи журналу аудиту за Реєстром, Схемою, типом дії, користувачем, діапазоном дат або ідентифікатором об'єкта.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them." : "Використовуйте фільтри, щоб звузити видалені елементи за Реєстром, Схемою, датою видалення або користувачем, який їх видалив.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics." : "Використовуйте фільтри, щоб звузити записи журналу пошуку за Реєстром, Схемою, статусом успіху, користувачем, діапазоном дат, пошуковими запитами або показниками продуктивності.", + "Used By Collections" : "Використовується колекціями", + "User" : "Користувач", + "User Agent Statistics" : "Статистика User Agent", + "User Description" : "Опис користувача", + "User rate limits cleared successfully" : "Обмеження швидкості за користувачем успішно очищено", + "User removed successfully" : "Користувача успішно вилучено", + "User-friendly description shown in forms and help text" : "Зручний для користувача опис, що відображається у формах і тексті довідки", + "Username" : "Ім'я користувача", + "Username for SOLR authentication (optional)" : "Ім'я користувача для автентифікації SOLR (необов'язково)", + "Users" : "Користувачі", + "Using Pre-Generated Chunks" : "Використання попередньо згенерованих фрагментів", + "Validate" : "Перевірити", + "Validate Objects" : "Перевірити об'єкти", + "Validation completed successfully" : "Перевірку успішно завершено", + "Validation failed" : "Перевірка не вдалася", + "Value" : "Значення", + "Vector Dimensions" : "Розмірності вектора", + "Vector Search Backend" : "Бекенд векторного пошуку", + "Vector field: _embedding_" : "Векторне поле: _embedding_", + "Vectorization Triggers" : "Тригери векторизації", + "Vectorize All Files" : "Векторизувати всі Файли", + "Vectorize All Objects" : "Векторизувати всі об'єкти", + "Vectorize all file types" : "Векторизувати всі типи Файлів", + "Vectorize all views" : "Векторизувати всі подання", + "Vectorize on object creation" : "Векторизувати під час створення об'єкта", + "Vectors will be stored in your existing object and file collections" : "Вектори зберігатимуться у ваших наявних колекціях об'єктів і Файлів", + "Version" : "Версія", + "Very High" : "Дуже високий", + "View API Docs" : "Переглянути документацію API", + "View Changes" : "Переглянути зміни", + "View Details" : "Переглянути подробиці", + "View Error" : "Переглянути помилку", + "View File" : "Переглянути Файл", + "View Full Details" : "Переглянути повні подробиці", + "View Logs" : "Переглянути журнали", + "View Name" : "Назва подання", + "View Object" : "Переглянути об'єкт", + "View Parameters" : "Переглянути параметри", + "View Selection" : "Вибір подання", + "View and analyze search trail logs with advanced filtering and analytics capabilities" : "Переглядайте та аналізуйте журнали пошуку з розширеними можливостями фільтрування та аналітики", + "View and analyze system audit trails with advanced filtering capabilities" : "Переглядайте та аналізуйте системні журнали аудиту з розширеними можливостями фільтрування", + "View deleted successfully" : "Подання успішно видалено", + "View entity details and manage relations" : "Переглядайте подробиці сутності та керуйте зв'язками", + "View name" : "Назва подання", + "View name is required" : "Потрібна назва подання", + "View saved successfully!" : "Подання успішно збережено!", + "View search analytics and manage search logs" : "Переглядайте аналітику пошуку та керуйте журналами пошуку", + "View successfully deleted" : "Подання успішно видалено", + "View successfully updated" : "Подання успішно оновлено", + "View updated successfully!" : "Подання успішно оновлено!", + "View webhook delivery logs and filter by webhook" : "Переглядайте журнали доставки вебхуків і фільтруйте за вебхуком", + "Views" : "Подання", + "Visible to users" : "Видимо для користувачів", + "Wait for Response" : "Очікувати на відповідь", + "Wait for webhook response before continuing (required for request/response flows)" : "Очікувати на відповідь вебхука перед продовженням (потрібно для потоків запит/відповідь)", + "Warmup Names Cache" : "Прогріти кеш імен", + "Warning" : "Попередження", + "Warning:" : "Попередження:", + "Webhook" : "Вебхук", + "Webhook Log Details" : "Подробиці журналу вебхука", + "Webhook Logs" : "Журнали вебхуків", + "Webhook created successfully" : "Вебхук успішно створено", + "Webhook deleted" : "Вебхук видалено", + "Webhook retry delivered successfully" : "Повторну спробу вебхука успішно доставлено", + "Webhook retry delivery failed" : "Доставка повторної спроби вебхука не вдалася", + "Webhook updated" : "Вебхук оновлено", + "Webhook updated successfully" : "Вебхук успішно оновлено", + "Webhooks" : "Вебхуки", + "Weekly" : "Щотижня", + "Wrap webhook payload in CloudEvents format for better interoperability" : "Обгортати корисне навантаження вебхука у формат CloudEvents для кращої взаємодії", + "X-Custom-Header: value\\nAuthorization: Bearer token" : "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes" : "Так", + "You" : "Ви", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server." : "Ви можете створювати нові ConfigSets на основі шаблону _default або завантажувати власні безпосередньо на ваш сервер SOLR.", + "You do not have access to this conversation" : "Ви не маєте доступу до цієї розмови", + "You do not have permission to delete this conversation" : "Ви не маєте дозволу видаляти цю розмову", + "You do not have permission to modify this conversation" : "Ви не маєте дозволу змінювати цю розмову", + "You do not have permission to restore this conversation" : "Ви не маєте дозволу відновлювати цю розмову", + "You must be logged in to favorite views" : "Ви повинні увійти в систему, щоб додавати подання до обраного", + "You need an AI agent to start a conversation." : "Вам потрібен агент ШІ, щоб почати розмову.", + "Your Fireworks AI API key. Get one at" : "Ваш ключ API Fireworks AI. Отримайте його на", + "Your OpenAI API key. Get one at" : "Ваш ключ API OpenAI. Отримайте його на", + "Your feedback has been recorded. Optionally, you can provide additional details here..." : "Ваш відгук записано. За бажанням ви можете надати додаткові подробиці тут...", + "Zookeeper Hosts" : "Хости Zookeeper", + "Zookeeper Port" : "Порт Zookeeper", + "Zookeeper Settings (SolrCloud)" : "Налаштування Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud" : "Рядок з'єднання Zookeeper для SolrCloud", + "Zookeeper port number (optional, defaults to 2181)" : "Номер порту Zookeeper (необов'язково, за замовчуванням 2181)", + "chunk" : "фрагмент", + "chunks" : "фрагменти", + "chunks processed" : "фрагментів оброблено", + "configuration(s)" : "конфігурація(ї)", + "e.g., lib/Settings/config.json" : "напр., lib/Settings/config.json", + "e.g., lib/Settings/register.json" : "напр., lib/Settings/register.json", + "failed" : "не вдалося", + "file" : "файл", + "files" : "файли", + "fw_..." : "fw_...", + "http://localhost:11434" : "http://localhost:11434", + "https://api.fireworks.ai/inference/v1" : "https://api.fireworks.ai/inference/v1", + "https://example.com/webhook" : "https://example.com/webhook", + "members" : "учасники", + "menu or contact someone with permission to create agents." : "меню або зверніться до когось із дозволом створювати агентів.", + "messages" : "повідомлення", + "n8n URL and API key are required" : "Потрібні URL-адреса та ключ API n8n", + "n8n configuration saved successfully" : "Конфігурацію n8n успішно збережено", + "n8n connection not configured" : "З'єднання n8n не налаштовано", + "n8n connection successful" : "З'єднання n8n успішне", + "n8n connection test successful" : "Тест з'єднання n8n успішний", + "n8n integration disabled" : "Інтеграцію n8n вимкнено", + "n8n integration enabled" : "Інтеграцію n8n увімкнено", + "n8n project initialized successfully" : "Проект n8n успішно ініціалізовано", + "n8n settings saved successfully" : "Налаштування n8n успішно збережено", + "objectType: object\\naction: created" : "objectType: object\\naction: created", + "objects" : "об'єкти", + "objects processed" : "об'єктів оброблено", + "or pick a range" : "або виберіть діапазон", + "org-..." : "org-...", + "provides a good balance between speed and accuracy." : "забезпечує хороший баланс між швидкістю та точністю.", + "register(s)" : "Реєстр(и)", + "register(s) selected" : "Реєстр(ів) вибрано", + "required" : "обов'язково", + "results" : "результати", + "schema(s)" : "Схема(и)", + "schema(s) selected" : "Схем(и) вибрано", + "sk-..." : "sk-...", + "this application" : "цей застосунок", + "to" : "до", + "vectorized" : "векторизовано", + "views selected" : "подань вибрано", + "ℹ️ Current Configuration" : "ℹ️ Поточна конфігурація", + "⚡ Batch Processing" : "⚡ Пакетне оброблення", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information" : "✓ Вичерпний контекст ✓ Менша ймовірність пропустити деталі ✗ Повільніші відповіді ✗ Може включати менш релевантну інформацію", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information" : "✓ Швидші відповіді ✓ Точніші відповіді ✗ Може пропустити релевантну інформацію", + "✨ AI Features" : "✨ Функції ШІ", + "All search trails cleared successfully" : "Усі журнали пошуку успішно очищено", + "Multiple search trail deletion not implemented yet" : "Видалення кількох журналів пошуку ще не реалізовано", + "No expired search trails found to clear" : "Прострочених журналів пошуку для очищення не знайдено", + "Missing message" : "Відсутнє повідомлення", + "message content is required" : "потрібен вміст повідомлення", + "Missing conversation or agentUuid" : "Відсутня розмова або agentUuid", + "Access denied" : "Доступ заборонено", + "Conversation not found" : "Розмову не знайдено", + "AI service not configured" : "Службу ШІ не налаштовано", + "Failed to process message" : "Не вдалося обробити повідомлення", + "Missing conversationId" : "Відсутній conversationId", + "conversationId is required" : "Потрібен conversationId", + "Failed to fetch conversation history" : "Не вдалося отримати історію розмови", + "Failed to clear conversation" : "Не вдалося очистити розмову", + "Invalid feedback type" : "Недійсний тип відгуку", + "type must be \"positive\" or \"negative\"" : "тип має бути \"positive\" або \"negative\"", + "Message not found" : "Повідомлення не знайдено", + "Failed to save feedback" : "Не вдалося зберегти відгук", + "Failed to get chat statistics" : "Не вдалося отримати статистику чату", + "Not Found" : "Не знайдено", + "Engine not found" : "Рушій не знайдено", + "Consumer not found" : "Споживача не знайдено", + "Backend parameter is required" : "Потрібен параметр бекенду", + "Failed to get database information: %s" : "Не вдалося отримати інформацію про базу даних: %s", + "SOLR setup error: %s" : "Помилка налаштування SOLR: %s", + "Reindex failed: %s" : "Переіндексування не вдалося: %s", + "Anonymized" : "Анонімізовано", + "Entities detected" : "Сутності виявлено", + "Extract Now" : "Видобути зараз", + "Extracted at" : "Видобуто о", + "Extraction" : "Видобування", + "Failed to load extraction data" : "Не вдалося завантажити дані видобування", + "Failed to load register data" : "Не вдалося завантажити дані Реєстру", + "Failed to retrieve extraction status." : "Не вдалося отримати статус видобування.", + "Failed to retrieve objects for file." : "Не вдалося отримати об'єкти для Файлу.", + "No extraction data available for this file" : "Для цього Файлу немає доступних даних видобування", + "No register objects reference this file" : "Жоден об'єкт Реєстру не посилається на цей Файл", + "Not extracted" : "Не видобуто", + "Register Objects" : "Об'єкти Реєстру", + "Risk level" : "Рівень ризику", + "Text chunks" : "Текстові фрагменти", + "Unknown error" : "Невідома помилка", + "Very high" : "Дуже високий", + "{title} in {register} / {schema}" : "{title} у {register} / {schema}", + "Push Notifications" : "Push-сповіщення", + "Real-time push notification status via notify_push" : "Статус push-сповіщень у реальному часі через notify_push", + "Realtime push not available — the notify_push app is not installed" : "Push у реальному часі недоступний — застосунок notify_push не встановлено", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates." : "Установіть застосунок notify_push з магазину застосунків Nextcloud, щоб увімкнути оновлення в реальному часі.", + "Open Nextcloud App Store" : "Відкрити магазин застосунків Nextcloud", + "notify_push is installed but not yet active" : "notify_push встановлено, але ще не активовано", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration." : "notify_push встановлено, але OpenRegister ще не підтвердив успішний push. Виконайте збереження об'єкта для активації або перевірте вашу конфігурацію notify_push.", + "notify_push configuration guide" : "посібник з конфігурації notify_push", + "Realtime push active" : "Push у реальному часі активний", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted." : "Push-сповіщення в реальному часі активні. Підключені клієнти отримують миттєві оновлення під час створення, оновлення або видалення об'єктів.", + "File content is required" : "File content is required", + "File name is required" : "File name is required", + "File name is required (use \"name\" or \"filename\")" : "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access" : "File not available for anonymous access", + "Files folder not found" : "Files folder not found", + "Forbidden" : "Forbidden", + "Preview not available for unpublished files" : "Preview not available for unpublished files", + "Source object not found" : "Source object not found", + "Target object ID is required" : "Target object ID is required", + "Target object not found" : "Target object not found", + "You do not have access to this object" : "You do not have access to this object", + "Browser Web Push (VAPID)" : "Browser Web Push (VAPID)", + "Browser web push is configured" : "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed." : "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key" : "VAPID public key", + "Browser web push is not configured" : "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:" : "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …" : "Checking browser web push configuration …", + "Browser notifications" : "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed." : "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications." : "This browser does not support web push notifications.", + "Enable browser notifications" : "Enable browser notifications", + "Notification permission: granted" : "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings." : "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission." : "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings." : "Could not change browser notification settings." +}, +"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" +); diff --git a/l10n/uk.json b/l10n/uk.json new file mode 100644 index 0000000000..ee8f6dfc83 --- /dev/null +++ b/l10n/uk.json @@ -0,0 +1,2670 @@ +{ + "translations": { + "3": "3", + "30": "30", + "(Dolphin required)": "(требуется Dolphin)", + "(no subject)": "(без теми)", + "(no title)": "(без назви)", + "(unnamed)": "(без імені)", + "(untitled card)": "(картка без назви)", + "({count} selected)": "({count} вибрано)", + ", {failed} failed": ", {failed} неудачно", + "0 = unlimited": "0 = неограниченно", + "A deactivation request is pending.": "Запит на деактивацію ожидает обробки.", + "AI Assistant": "ИИ-помощник", + "AI Chat": "ИИ-чат", + "AI service not configured": "Сервис ИИ не налаштовано", + "API Key": "API-ключ", + "API Request Quota (requests/day)": "Квота запитів API (запитів/день)", + "API Token Configuration": "Налаштування API-токенов", + "API Tokens": "API-токени", + "API tokens saved successfully": "API-токени успішно збережено", + "AVG / Verwerkingsregister": "AVG / Verwerkingsregister", + "AVG Art 30 §4: this report joins each processing activity with the lifetime audit-trail counts attributed to it. Auditors and the Autoriteit Persoonsgegevens use this for supervisory review.": "AVG Ст. 30 §4: у цьому звіті кожна операція обробки зв'язується з загальним кількістю записів журналу аудиту. Використовується аудиторами і Autoriteit Persoonsgegevens для надзорной перевірки.", + "About ConfigSets": "Про наборах конфігурації (ConfigSets)", + "Access denied": "Доступ заборонено", + "Access organisation dashboard and analytics": "Доступ до дашборду і аналитике організації", + "Account": "Аккаунт", + "Action": "Дія", + "Action Distribution": "Распределение дій", + "Action:": "Дія:", + "Actions": "Дії", + "Active": "Активний", + "Active Collections": "Активні колекції", + "Active Filters": "Активні фільтри", + "Active Organisation:": "Активна організація:", + "Active Organisations": "Активні організації", + "Active filters:": "Активні фільтри:", + "Active organisation changed successfully": "Активна організація успішно изменена", + "Active organisation set successfully": "Активна організація успішно установлена", + "Activities": "Дії", + "Activity": "Активність", + "Activity Period": "Период активності", + "Add": "Додати", + "Add Attachment": "Додати вложение", + "Add Endpoint": "Додати кінцеву точку", + "Add Groups": "Додати групи", + "Add Property to '{title}'": "Додати властивість до '{title}'", + "Add Register": "Додати реєстр", + "Add Schema": "Додати схему", + "Add Source": "Додати джерело", + "Add User": "Додати користувача", + "Add User to Organisation": "Додати користувача у організацію", + "Add a contact from any of your address books to associate it with this object.": "Додайте контакт з будь-якої адресної книги для зв'язки з цим об'єктом.", + "Add a file or files": "Додати файл або файли", + "Add card": "Додати картку", + "Add contact": "Додати контакт", + "Add endpoint": "Додати кінцеву точку", + "Add oneOf entry": "Додати запис oneOf", + "Add range": "Додати діапазон", + "Add schema titles, descriptions, and register information to provide richer context for search": "Додайте заголовки, опису схем і інформацію реєстра для более богатого контекста пошуку", + "Add to favorites": "Додати у избранное", + "Added": "Добавлено", + "Added to favorites": "Добавлено у избранное", + "Additional Information": "Додаткова інформація", + "Additional feedback saved. Thank you!": "Додатковий відгук збережено. Дякуємо!", + "Addresses": "Адреса", + "Admin": "Администратор", + "Advanced": "Додатково", + "Advanced Filters": "Розширені фільтри", + "Advanced Options": "Розширені параметри", + "Advanced filters are not available when using database source. Switch to Auto or SOLR Index for filtering options.": "Розширені фільтри недоступні при використанні джерела бази даних. Переключіться на Auto або SOLR Index для використання фільтріу.", + "After": "После", + "Agent Type": "Тип агента", + "Agent deleted successfully": "Агент успішно видалено", + "Agent successfully created": "Агент успішно створено", + "Agent successfully updated": "Агент успішно оновлено", + "Agents": "Агенти", + "Aggregated across schemas": "Агрегировано по схемам", + "All": "Всі", + "All Categories": "Всі категорії", + "All Collections": "Всі колекції", + "All Confidence Levels": "Всі рівні достоверности", + "All ConfigSets": "Всі набори конфігурації", + "All Files": "Всі файли", + "All Levels": "Всі рівні", + "All Types": "Всі типи", + "All Webhooks": "Всі вебхуки", + "All actions": "Всі дії", + "All audit trails cleared successfully": "Всі журнали аудиту успішно очищено", + "All clear": "Все у порядке", + "All file types": "Всі типи файлів", + "All registers": "Всі реєстри", + "All schemas": "Всі схеми", + "All search trails cleared successfully": "Всі журнали пошуку успішно очищено", + "All searches": "Всі пошукові запити", + "All users": "Всі користувачі", + "All-day events": "Події на весь день", + "Allowed MIME Types": "Дозволені типи MIME", + "Already linked": "Вже связано", + "An error occurred while clearing audit trails": "Произошла помилка при очистке журналов аудиту", + "An error occurred while deleting the audit trail": "Произошла помилка при удалении записи журналу аудиту", + "An error occurred while deleting the object": "Произошла помилка при удалении об'єкта", + "An error occurred while deleting the view": "Произошла помилка при удалении представлення", + "An error occurred while permanently deleting the objects": "Произошла помилка при безвозвратном удалении об'єктів", + "An error occurred while restoring the objects": "Произошла помилка при восстановлении об'єктів", + "An error occurred while saving the organisation": "Произошла помилка при сохранении організації", + "Analysis completed:": "Анализ завершено:", + "Analysis steps:": "Шаги анализа:", + "Analytics": "Аналитика", + "Analyze Objects": "Анализировать об'єкти", + "Analyze Properties": "Анализировать властивості", + "Analyze existing properties for improvement opportunities": "Анализировать существующие властивості для пошуку возможностей улучшения", + "Analyzing...": "Анализ...", + "Anonymized": "Анонимизировано", + "Anonymous Group": "Анонимная група", + "Any favorites and sharing settings for this view": "Всі налаштування избранного і совместного доступу для цього представлення", + "Any user": "Будь-якої користувач", + "App ID": "ID застосунки", + "App store cache invalidated successfully": "Кеш магазина застосунків успішно аннулирован", + "App store cache invalidated: {invalidated}": "Кеш магазина застосунків аннулирован: {invalidated}", + "Application deleted successfully": "Застосунок успішно видалено", + "Application identifier for this configuration (optional)": "Ідентифікатор застосунки для цієї конфігурації (необязательно)", + "Application settings": "Налаштування застосунки", + "Application successfully created": "Застосунок успішно створено", + "Application successfully updated": "Застосунок успішно оновлено", + "Applications": "Застосунки", + "Apply": "Застосувати", + "Apply Changes": "Застосувати зміни", + "Apply a role to all schemas in this register that do not have explicit authorization overrides.": "Застосувати роль до всіх схемам у цьому реєстрі, у яких немає явних перевизначень авторизації.", + "Approval Progress": "Ход согласования", + "Approve": "Одобрить", + "Approved": "Одобрено", + "Archive": "Архіу", + "Archive conversation": "Архівувати переписку", + "Archive this verwerkingsactiviteit? Audit-trail rows will keep referring to it.": "Архівувати цю verwerkingsactiviteit? Рядки журналу аудиту будуть як і раніше посилатися на неї.", + "Archived conversations are hidden from your active list": "Архівовані переписки приховані з активного списку", + "Are you sure you want to DELETE collection \"{name}\"?\n\nThis will:\n• Permanently delete the collection and all its data\n• Remove all indexed documents\n• This action cannot be undone": "Ви впевнені, що хочете ВИДАЛИТИ колекцію \"{name}\"?\n\nЦе дія:\n• Безповоротно видалить колекцію і всі її дані\n• Видалить всі проіндексовані документи\n• Нельзя скасувати", + "Are you sure you want to cleanup old search trails? This will delete entries older than 30 days.": "Ви впевнені, що хочете очистити старі журнали пошуку? Це видалить записи старше 30 днів.", + "Are you sure you want to clear all data from collection \"{name}\"?\n\nThis will:\n• Delete all indexed documents\n• Keep the collection structure intact\n• This action cannot be undone": "Ви впевнені, що хочете очистити всі дані з колекції \"{name}\"?\n\nЦе дія:\n• Видалить всі проіндексовані документи\n• Сохранит структуру колекції\n• Нельзя скасувати", + "Are you sure you want to delete the application \"{name}\"? This action cannot be undone.": "Ви впевнені, що хочете видалити застосунок \"{name}\"? Це дія нельзя скасувати.", + "Are you sure you want to delete the selected audit trails? This action cannot be undone.": "Ви впевнені, що хочете видалити вибрані записи журналу аудиту? Це дія нельзя скасувати.", + "Are you sure you want to delete the selected search trails? This action cannot be undone.": "Ви впевнені, що хочете видалити вибрані записи журналу пошуку? Це дія нельзя скасувати.", + "Are you sure you want to delete this ConfigSet?": "Ви впевнені, що хочете видалити цей набір конфігурації?", + "Are you sure you want to delete this configset?": "Ви впевнені, що хочете видалити цей набір конфігурації?", + "Are you sure you want to leave '{name}'?": "Ви впевнені, що хочете покинуть '{name}'?", + "Are you sure you want to permanently delete": "Ви впевнені, що хочете безповоротно видалити", + "Are you sure you want to reindex collection \"{name}\"?\n\nThis will:\n• Rebuild the index with all objects\n• Take several minutes to complete\n• May impact search performance during reindexing": "Ви впевнені, що хочете переіндексувати колекцію \"{name}\"?\n\nЦе дія:\n• Перебудує індекс зі всеми об'єктами\n• Займе кілька хвилин\n• Може повлиять на продуктивність пошуку во година переиндексации", + "Are you sure you want to remove the schema \"{schema}\" from register \"{register}\"? This action cannot be undone.": "Ви впевнені, що хочете видалити схему \"{schema}\" з реєстра \"{register}\"? Це дія нельзя скасувати.", + "Are you sure you want to soft-delete {count} active objects for schema \"{schema}\"?\n\nThey will be marked as deleted but can be permanently removed later.": "Ви впевнені, що хочете програмно видалити {count} активних об'єктів схеми \"{schema}\"?\n\nВони будуть позначено як видалені, але можуть бути безповоротно видалено позже.", + "Array Object Configuration:": "Конфігурація об'єкта массива:", + "Articles": "Статьи", + "Ask a question...": "Задайте питання...", + "Ask questions about your data using natural language": "Задавайте питання про ваші даних на природній мові", + "Assigned collaborative tags": "Призначені спільні теги", + "Assignment notifications": "Сповіщення про назначениях", + "Attachment added to {name}": "Вложение добавлено до {name}", + "Attempt": "Спроба", + "Audit Trail Changes": "Зміни у журнале аудиту", + "Audit Trail Details": "Детали записи журналу аудиту", + "Audit Trail Management": "Управління журналом аудиту", + "Audit Trail Statistics": "Статистика журналу аудиту", + "Audit Trails": "Журнали аудиту", + "Audit trail data copied to clipboard": "Дані журналу аудиту скопійовано у буфер обміну", + "Audit trail deleted successfully": "Запис журналу аудиту успішно видалена", + "Audit trail successfully deleted": "Запис журналу аудиту успішно видалена", + "Authentication": "Автентифікація", + "Auto (e.g. Y for year, F Y for month)": "Авто (наприклад, Y для року, F Y для місяця)", + "Auto share": "Автоматический загальний доступ", + "Auto-commit disabled": "Автоматическая фиксация отключена", + "Auto-commit enabled": "Автоматическая фиксация включена", + "Auto-create Default Organisation": "Автоматично створювати організацію за замовчуванням", + "Auto-retry failed vectorizations": "Автоматично повторювати невдалі векторизації", + "Automatically commit changes to SOLR index": "Автоматично фиксировать зміни у индексе SOLR", + "Automatically connect to the federation network.": "Автоматично подключаться до сети федерации.", + "Automatically create a default organisation if none exists when the app is initialized": "Автоматично створювати організацію за замовчуванням, якщо вона отсутствует при инициализации застосунки", + "Automatically generate vector embeddings from text chunks when files are uploaded and processed": "Автоматично створювати векторні представлення з текстових фрагментів при завантаженні і обработке файлів", + "Automatically generate vector embeddings when objects are created or updated": "Автоматично створювати векторні представлення при создании або оновленні об'єктів", + "Automatically retry failed vectorization attempts (max 3 retries)": "Автоматично повторювати невдалі спроби векторизації (максимум 3 спроби)", + "Automatically update catalogues.": "Автоматично обновлять каталоги.", + "Available Filters": "Доступні фільтри", + "Available Workflows": "Доступні робочі процеси", + "Available placeholders:": "Доступні заповнювачі:", + "Avatar": "Аватар", + "Avatar changes are not supported by your authentication provider.": "Ваш провайдер автентифікації не поддерживает зміна аватара.", + "Avatar removed": "Аватар видалено", + "Avatar updated successfully": "Аватар успішно оновлено", + "Avg Execution Time": "Середній година виконання", + "Avg Members/Org": "Середній число участников/організація", + "Avg Object Views/Session": "Середній кількість просмотров об'єктів/сессия", + "Avg Response Time": "Середній година відповіді", + "Avg Results/Search": "Середній число результатіу/пошук", + "Avg Searches/Session": "Середній число поисков/сессия", + "Back": "Назад", + "Back to Entities": "Назад до об'єктам", + "Back to Registers": "Назад до реестрам", + "Back to Webhooks": "Назад до веб-хукам", + "Back to applications": "Назад до приложениям", + "Back to entities": "Назад до об'єктам", + "Backend parameter is required": "Параметр backend обязателен", + "Backend updated successfully. Please reload the application.": "Backend успішно оновлено. Будь ласка, перезагрузите застосунок.", + "Bandwidth Quota (MB/month)": "Квота пропускной способности (МБ/місяць)", + "Base URL (Optional)": "Базовий URL (необязательно)", + "Basic Information": "Основна інформація", + "Batch Size": "Розмір пакету", + "Batch extraction completed": "Пакетное видобуток завершено", + "Before": "До", + "Before object vectorization can work:": "Перед тем як начать векторизацію об'єктів:", + "Behavior": "Поведение", + "Behavior Issue": "Проблема поведения", + "Beschrijving": "Beschrijving", + "Bewaartermijn": "Bewaartermijn", + "Bewaartermijn (ISO-8601 duration, e.g. P10Y, P30D)": "Bewaartermijn (длительность ISO-8601, наприклад P10Y, P30D)", + "Blob storage has been retired. All objects now use magic tables.": "Сховище Blob устарело. Всі об'єкти теперь використовують magic tables.", + "Bookmarks": "Закладки", + "Branch": "Ветка", + "Bulk Role Assignment: {title}": "Массовое призначення ролей: {title}", + "Bulk delete operation completed successfully": "Операція массового удаления успішно завершена", + "Bulk save operation completed successfully": "Операція массового сохранения успішно завершена", + "Business Data": "Бизнес-дані", + "CSV": "CSV", + "Cache cleared successfully": "Кеш успішно очищено", + "Cache grew from {old} to {new} entries.": "Кеш виріс з {old} до {new} записів.", + "Cache shrunk from {old} to {new} entries.": "Кеш сократился з {old} до {new} записів.", + "Cache stayed the same at {size} entries.": "Кеш остался без изменений: {size} записів.", + "Calculate Sizes": "Обчислити розміри", + "Calendar": "Календар", + "Calendar Provider Configuration": "Налаштування провайдера календаря", + "Calendar integration is not available": "Інтеграція з календарем недоступна", + "Calendar name": "Назва календаря", + "Cancel": "Скасування", + "Cancel deactivation request": "Скасувати запит на деактивацію", + "Cannot delete: objects are still attached": "Неможливо видалити: об'єкти все ще прикріплені", + "Cannot edit: This register is managed by external configuration {title}": "Неможливо редагувати: цей реєстр управляется внешней конфигурацией {title}", + "Cannot remove schema with existing objects ({count} objects)": "Неможливо видалити схему з існуючими об'єктами ({count} об'єктів)", + "Cannot remove schema {schema} because it contains {count} objects": "Неможливо видалити схему {schema}, так як вона містить {count} об'єктів", + "Cards": "Картки", + "Cascade delete": "Каскадное видалення", + "Categories of data subjects (one per line)": "Категорії суб'єктів даних (по одному на рядок)", + "Categories of personal data (one per line)": "Категорії персональних даних (по одному на рядок)", + "Categorieën betrokkenen (één per regel)": "Categorieën betrokkenen (één per regel)", + "Categorieën persoonsgegevens (één per regel)": "Categorieën persoonsgegevens (één per regel)", + "Category": "Категорія", + "Change Type": "Тип зміни", + "Change password": "Змінити пароль", + "Changes": "Зміни", + "Changes copied to clipboard": "Зміни скопійовано у буфер обміну", + "Changes for Audit Trail #{id}": "Зміни для записи журналу аудиту #{id}", + "Chat Model": "Модель чату", + "Chat Provider": "Провайдер чату", + "Chat Provider (RAG)": "Провайдер чату (RAG)", + "Chat Settings": "Налаштування чату", + "Chat provider connection successful!": "Підключення до провайдеру чату успішно!", + "Chat settings": "Налаштування чату", + "Chat test failed: {error}": "Тест чату не удался: {error}", + "Choose a register": "Виберіть реєстр", + "Choose a schema": "Виберіть схему", + "Choose how vector similarity calculations are performed for semantic search": "Виберіть метод обчислення векторного схожості для семантичного пошуку", + "Choose which file types to include in the vectorization process. Only files with extracted text and chunks will be processed.": "Виберіть типи файлів для включення у процес векторизації. Будуть оброблені тільки файли з вилученим текстом і фрагментами.", + "Choose which views to include in the vectorization process. Leave empty to process all views based on your configuration.": "Виберіть представлення для включення у процес векторизації. Залиште порожнім для обробки всіх представлень согласно вашей конфігурації.", + "Chunk": "Фрагмент", + "Chunk deletion not yet implemented. Use chunk-based endpoints.": "Видалення фрагментів ще не реалізовано. Використовуйте кінцеві точки на основе фрагментів.", + "Chunks": "Фрагменти", + "Chunks to Vectorize:": "Фрагменти для векторизації:", + "Cleanup Old Trails": "Очистити старі журнали", + "Cleanup completed": "Очищення завершена", + "Cleanup completed successfully. Deleted {count} entries.": "Очищення успішно завершена. Видалено {count} записів.", + "Cleanup failed: {error}": "Помилка очистки: {error}", + "Clear All Embeddings": "Очистити всі векторні представлення", + "Clear Entries": "Очистити записи", + "Clear Filtered Audit Trails": "Очистити відфільтровані журнали аудиту", + "Clear Filters": "Сбросить фільтри", + "Clear Index": "Очистити індекс", + "Clear Selection": "Зняти виділення", + "Clear all": "Очистити все", + "Clear all filters": "Сбросить всі фільтри", + "Clear date range": "Очистити діапазон дат", + "Clear filters": "Сбросить фільтри", + "Clearing...": "Очищення...", + "Click Run compliance scan to find schemas where PII has been detected but no processing-activity annotation exists.": "Натисніть «Запустити перевірку відповідності», щоб знайти схеми, у яких виявлено персональні дані, але немає аннотации про операції обробки.", + "Click to subscribe to notifications": "Натисніть, щоб підписатися на сповіщення", + "Click to unsubscribe from notifications": "Натисніть, щоб відписатися уід сповіщень", + "Close": "Закрити", + "Cluster name": "Ім'я кластера", + "Code": "Код", + "Code (short readable key, e.g. v-2026-001)": "Код (короткий читабельний ключ, наприклад v-2026-001)", + "Collapse details": "Свернуть детали", + "Collection Management": "Управління коллекциями", + "Collection Name": "Назва колекції", + "Collection assignments updated successfully": "Призначення колекції успішно оновлено", + "Collection cleared successfully": "Колекція успішно очищена", + "Collection copied successfully": "Колекція успішно скопирована", + "Collection created successfully": "Колекція успішно створена", + "Collection deleted successfully": "Колекція успішно видалена", + "Collection used to store and index file metadata and content": "Колекція для зберігання і индексации метаданих і содержимого файлів", + "Collection used to store and index object data": "Колекція для зберігання і индексации даних об'єктів", + "Collections:": "Колекції:", + "Color": "Цвет", + "Comment...": "Коментар...", + "Commit Message": "Сообщение коммита", + "Commit Within (ms)": "Фиксация у течение (мс)", + "Commit message": "Сообщение коммита", + "Compare current schema with real object data": "Порівняти поточну схему з реальними даними об'єктів", + "Completed": "Завершено", + "Complex": "Складний", + "Complex queries: Advanced searches with multiple filters, operators, and complex parameter combinations": "Складні запити: розширений пошук з кількома фільтрами, операторами і складними комбінаціями параметрів", + "Complex queries: advanced searches with multiple filters, operators, and complex parameter combinations": "Складні запити: розширений пошук з кількома фільтрами, операторами і складними комбінаціями параметрів", + "Compliance": "Відповідність требованиям", + "Confidence Level": "Рівень достоверности", + "Confidence Score": "Оценка достоверности", + "Confidence: {confidence}%": "Достоверность: {confidence}%", + "Config must be provided as an object": "Конфігурація повинна бути надана як об'єкт", + "ConfigSet": "Набір конфігурації", + "ConfigSet Management": "Управління наборами конфігурації", + "ConfigSet Name": "Назва набору конфігурації", + "ConfigSet created successfully": "Набір конфігурації успішно створено", + "ConfigSet deleted successfully": "Набір конфігурації успішно видалено", + "ConfigSet:": "Набір конфігурації:", + "ConfigSets define the schema and configuration for your SOLR collections. They contain field definitions, analyzers, and other search settings.": "Набори конфігурації визначають схему і налаштування для ваших колекцій SOLR. Вони містять определения поліу, аналізатори і другие налаштування пошуку.", + "Configuration": "Конфігурація", + "Configuration Data": "Дані конфігурації", + "Configuration is up to date": "Конфігурація актуальна", + "Configuration saved": "Конфігурація збережена", + "Configuration saved successfully.": "Конфігурація успішно збережена.", + "Configuration type (default, application, etc.)": "Тип конфігурації (за замовчуванням, застосунок і т.д.)", + "Configurations": "Конфігурації", + "Configure API tokens for external service integrations": "Налаштувати API-токени для інтеграцій з внешними сервисами", + "Configure Apache SOLR search engine for advanced search capabilities": "Налаштувати поисковой движок Apache SOLR для розширених возможностей пошуку", + "Configure Facets": "Налаштувати фасети", + "Configure Large Language Model (LLM) providers for AI-powered features including semantic search, embeddings, and chat.": "Налаштувати провайдерів великих мовних моделей (LLM) для функцій на основе ИИ, включаючи семантичний пошук, векторні представлення і чат.", + "Configure SOLR Facets": "Налаштувати фасети SOLR", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate ConfigSet and Collection Management dialogs to manage cores and collections.": "Налаштувати основні параметри підключення до серверу SOLR, включаючи автентифікацію і мережеві параметри. Використовуйте окремі диалоги управления наборами конфігурації і коллекциями для управления ядрами і коллекциями.", + "Configure basic connection settings for your SOLR server including authentication and network options. Use the separate configset and collection management dialogs to manage cores and collections.": "Налаштувати основні параметри підключення до серверу SOLR, включаючи автентифікацію і мережеві параметри. Використовуйте окремі диалоги управления наборами конфігурації і коллекциями для управления ядрами і коллекциями.", + "Configure default organisation and organisation-related settings": "Налаштувати організацію за замовчуванням і пов'язані налаштування", + "Configure how database objects are converted into vector embeddings for semantic search. Objects are directly vectorized without needing text extraction.": "Налаштувати перетворення об'єктів бази даних у векторні представлення для семантичного пошуку. Про'єкти векторизуются напрямую без необходимости извлечения текста.", + "Configure how objects are converted to text before vectorization. These settings affect search quality and context.": "Налаштувати перетворення об'єктів у текст перед векторизацией. Ці налаштування впливають на качество пошуку і контекст.", + "Configure large language model (LLM) providers for ai-powered features including semantic search, embeddings, and chat.": "Налаштувати провайдерів великих мовних моделей (LLM) для функцій на основе ИИ, включаючи семантичний пошук, векторні представлення і чат.", + "Configure multi-organization support and tenant isolation": "Налаштувати поддержку кількох організацій і изоляцию арендаторов", + "Configure parameters for file vectorization. This process will generate vector embeddings for all extracted file chunks.": "Налаштувати параметри векторизації файлів. Цей процес створить векторні представлення для всіх вилучених фрагментів файлів.", + "Configure parameters for object vectorization. This process will generate vector embeddings for all objects matching your view filters.": "Налаштувати параметри векторизації об'єктів. Цей процес створить векторні представлення для всіх об'єктів, відповідних фильтрам представлення.", + "Configure this schema to surface objects as events in the Nextcloud Calendar app.": "Налаштувати цю схему для отображения об'єктів як подій у приложении Nextcloud Календар.", + "Configure which types of data to search and how many sources to retrieve": "Налаштувати типи даних для пошуку і кількість видобуваних источников", + "Configuring field mappings...": "Налаштування сопоставления поліу...", + "Confirm Account Deactivation": "Підтвердити деактивацію аккаунта", + "Confirm Object Deletion": "Підтвердити видалення об'єкта", + "Confirm Object Restoration": "Підтвердити відновлення об'єкта", + "Confirm Permanent Object Deletion": "Підтвердити безвозвратное видалення об'єкта", + "Confirm deactivation": "Підтвердити деактивацію", + "Connecting to SOLR cluster...": "Підключення до кластеру SOLR...", + "Connection Failed": "Підключення не вдалося", + "Connection Settings": "Налаштування підключення", + "Connection Successful!": "Підключення успішно!", + "Connection failed": "Підключення не вдалося", + "Connection protocol": "Протокол підключення", + "Connection successful": "Підключення успішно", + "Connection timeout in seconds": "Тайм-аут підключення у секундах", + "Constant delay between retries (5 minutes)": "Постійна затримка між повторними спробами (5 хвилин)", + "Constraint Issue": "Проблема ограничения", + "Consumer not found": "Потребитель не знайдено", + "Contacts": "Контакти", + "Contacts integration is not available": "Інтеграція з контактами недоступна", + "Content Filters": "Фільтри содержимого", + "Continue to Properties": "Перейти до свойствам", + "Contracts": "Контракти", + "Control which object views should be vectorized to reduce API costs. Only vectorize views that benefit from semantic search.": "Керуйте тим, які представлення об'єктів слід векторизовать, щоб знизити витрати на API. Векторизуйте тільки те представлення, які виграють уід семантичного пошуку.", + "Control which views and tools the AI can use in this conversation. By default, all agent capabilities are enabled.": "Керуйте тим, які представлення і інструменти ИИ може використовувати у цієї переписке. За замовчуванням всі возможности агента включеноі.", + "Controls randomness (0 = deterministic, 2 = very creative)": "Управляє случайностью (0 = детерминировано, 2 = очень творчески)", + "Conversation ID is required": "Требуется ID переписки", + "Conversation archived": "Переписка архивирована", + "Conversation archived successfully": "Переписка успішно архивирована", + "Conversation cleared successfully": "Переписка успішно очищена", + "Conversation deleted": "Переписка видалена", + "Conversation not found": "Переписка не знайдена", + "Conversation permanently deleted": "Переписка безповоротно видалена", + "Conversation renamed": "Переписка переименована", + "Conversation restored": "Переписка відновлена", + "Conversation started with {agent}": "Переписка начата з {agent}", + "Conversation title": "Назва переписки", + "Conversations": "Переписки", + "Converting {schema} to blob storage...": "Конвертация {schema} у blob-сховище...", + "Converting {schema} to magic table...": "Конвертация {schema} у magic table...", + "Copied!": "Скопійовано!", + "Copy": "Копіювати", + "Copy Changes": "Копіювати зміни", + "Copy Collection": "Копіювати колекцію", + "Copy Data": "Копіювати дані", + "Copy Full Data": "Копіювати всі дані", + "Copy of {name}": "Копія {name}", + "Copy to clipboard": "Копіювати у буфер обміну", + "Copying...": "Копирование...", + "Costs": "Витрати", + "Count": "Кількість", + "Counting objects...": "Підрахунок об'єктів...", + "Create": "Створити", + "Create API Token": "Створити API-токен", + "Create Agent": "Створити агента", + "Create Application": "Створити застосунок", + "Create Collection": "Створити колекцію", + "Create ConfigSet": "Створити набір конфігурації", + "Create New Collection": "Створити новую колекцію", + "Create New ConfigSet": "Створити новий набір конфігурації", + "Create Operations": "Операції создания", + "Create Organisation": "Створити організацію", + "Create Webhook": "Створити вебхук", + "Create a copy of collection:": "Створити копию колекції:", + "Create a new ConfigSet based on the _default template": "Створити новий набір конфігурації на основе шаблону _default", + "Create a new SOLR collection from an existing ConfigSet": "Створити новую колекцію SOLR з существующего набору конфігурації", + "Create a new SOLR collection from an existing configset": "Створити новую колекцію SOLR з существующего набору конфігурації", + "Create a new configset based on the _default template": "Створити новий набір конфігурації на основе шаблону _default", + "Create a new event or link an existing one from any of your calendars.": "Створіть нове подія або зв'яжіть существующее з будь-якого з ваших календарей.", + "Create another": "Створити ще", + "Create copies of": "Створити копії", + "Create event": "Створити подія", + "Create new token": "Створити новий токен", + "Create or link a Deck card to track work on this object.": "Створіть або зв'яжіть картку Deck для відстеження роботи з цим об'єктом.", + "Create the first verwerkingsactiviteit to start tagging audit-trail rows with their AVG Art 30 attribution.": "Створіть первую verwerkingsactiviteit, щоб начать помечать рядки журналу аудиту їх атрибуцией по AVG Ст. 30.", + "Create your first AI agent to get started.": "Створіть свого першого ИИ-агента для початку роботи.", + "Created": "Створено", + "Created:": "Створено:", + "Creating search collections...": "Створення колекцій для пошуку...", + "Creating...": "Створення...", + "Current": "Поточний", + "Current Filters": "Текущие фільтри", + "Current Type:": "Поточний тип:", + "Current password": "Поточний пароль", + "Custom API endpoint if using a different region": "Користувацька конечная точка API для другого региона", + "Custom HTTP headers (one per line, format: Header-Name: value)": "Користувацькі HTTP-заголовки (по одному на строку, формат: Header-Name: value)", + "Custom HTTP headers (one per line, format: header-name: value)": "Користувацькі HTTP-заголовки (по одному на строку, формат: header-name: value)", + "Custom Ranges:": "Користувацькі діапазони:", + "Custom display title for this facet": "Користувацький заголовок для цього фасету", + "Custom pattern:": "Користувацький шаблон:", + "DELETE COLLECTION": "ВИДАЛИТИ КОЛЕКЦІЮ", + "DSAR": "DSAR", + "Daily": "Ежедневно", + "Dashboard": "Дашборд", + "Dashboard not found": "Дашборд не знайдено", + "Data": "Дані", + "Data Source": "Джерело даних", + "Data Sources": "Джерела даних", + "Data Views": "Представлення даних", + "Data sources": "Джерела даних", + "Data type does not match observed values": "Тип даних не відповідає спостережуваним значениям", + "Data-subject access request": "Запит на доступ суб'єкта даних", + "Database URL": "URL бази даних", + "Database information refreshed": "Інформація про базе даних оновлена", + "Date Faceting:": "Фасетирование по даті:", + "Date Range": "Діапазон дат", + "Dates": "Дати", + "Deactivation request cancelled": "Запит на деактивацію скасовано", + "Deactivation request submitted": "Запит на деактивацію надіслано", + "Deck": "Deck", + "Deck integration is not available": "Інтеграція з Deck недоступна", + "Default": "За замовчуванням", + "Default Object Tenant": "Арендатор об'єкта за замовчуванням", + "Default Organisation": "Організація за замовчуванням", + "Default Owner": "Владелец за замовчуванням", + "Default User (for cron/background jobs)": "Користувач за замовчуванням (для cron/фонових завдань)", + "Default User Tenant": "Арендатор користувача за замовчуванням", + "Default value": "Значення за замовчуванням", + "Default value (Email)": "Значення за замовчуванням (Email)", + "Default value (Password)": "Значення за замовчуванням (Пароль)", + "Default value (Phone number)": "Значення за замовчуванням (Номер телефона)", + "Default value (Regex)": "Значення за замовчуванням (Regex)", + "Default: 5": "За замовчуванням: 5", + "Defaults to current user. Select a different user if needed.": "За замовчуванням — поточний користувач. При необходимости виберіть другого.", + "Delays double with each attempt (2, 4, 8 minutes...)": "Затримка подвоюється з кожної спробою (2, 4, 8 хвилин...)", + "Delays increase linearly (5, 10, 15 minutes...)": "Затримка увеличивается линейно (5, 10, 15 хвилин...)", + "Delete": "Видалити", + "Delete ({count})": "Видалити ({count})", + "Delete Application": "Видалити застосунок", + "Delete Audit Trail": "Видалити запис журналу аудиту", + "Delete Collection": "Видалити колекцію", + "Delete ConfigSet": "Видалити набір конфігурації", + "Delete Objects": "Видалити об'єкти", + "Delete Operations": "Операції удаления", + "Delete SOLR Collection": "Видалити колекцію SOLR", + "Delete View": "Видалити представлення", + "Delete all objects in this schema": "Видалити всі об'єкти у цієї схеме", + "Delete permanently": "Видалити навсегда", + "Delete this conversation?": "Видалити цю переписку?", + "Delete view": "Видалити представлення", + "Delete {count} object": "Видалити {count} об'єкт", + "Delete {count} objects": "Видалити {count} об'єктів", + "Deleted": "Видалено", + "Deleted By": "Видалено пользователем", + "Deleted Date": "Дата удаления", + "Deleted Items Management": "Управління видаленими елементами", + "Deleted This Week": "Видалено на цієї неделе", + "Deleted Today": "Видалено сегодня", + "Deleted:": "Видалено:", + "Deleting SOLR Collection...": "Видалення колекції SOLR...", + "Deleting...": "Видалення...", + "Deletion Date Range": "Діапазон дат удаления", + "Deletion Statistics": "Статистика удалений", + "Deprecated": "Устаревший", + "Depublish": "Зняти з публікації", + "Description": "Опис", + "Description Template": "Шаблон опису", + "Description shown as tooltip": "Опис, отображаемое як подсказка", + "Description:": "Опис:", + "Detect data types and patterns": "Визначити типи даних і шаблони", + "Detected At": "Обнаружено у", + "Detected Format:": "Виявлений формат:", + "Detected Issues:": "Виявлені проблеми:", + "Detected: {date}": "Обнаружено: {date}", + "Direct permission": "Прямое разрешение", + "Disable": "Отключить", + "Disable this to make the agent": "Отключите це, щоб агент", + "Disabled": "Отключено", + "Discover Files": "Обнаружить файли", + "Discovered Properties": "Виявлені властивості", + "Display Format": "Формат отображения", + "Display Name": "Отображаемое ім'я", + "Do you want to permanently delete all filtered audit trail entries? This action cannot be undone.": "Ви хочете безповоротно видалити всі відфільтровані записи журналу аудиту? Це дія нельзя скасувати.", + "Do you want to permanently delete this audit trail entry? This action cannot be undone.": "Ви хочете безповоротно видалити цю запис журналу аудиту? Це дія нельзя скасувати.", + "Documents": "Документи", + "Doelbinding *": "Doelbinding *", + "Dolphin OCR enabled": "Dolphin OCR увімкнено", + "Download API Spec": "Завантажити спецификацию API", + "Download API Specification": "Завантажити спецификацию API", + "Download a copy of all your personal data stored in OpenRegister (GDPR Article 20).": "Завантажити копию всіх ваших персональних даних, хранящихся у OpenRegister (GDPR Статья 20).", + "Drag a file or files here": "Перетащите файл або файли сюда", + "Dry run completed: {count} fields would be created": "Тестовий запуск завершено: буде створено {count} поліу", + "Dry run completed: {count} fields would be fixed": "Тестовий запуск завершено: буде исправлено {count} поліу", + "Duration in seconds (optional)": "Длительность у секундах (необязательно)", + "Edit": "Редагувати", + "Edit Agent": "Редагувати агента", + "Edit Application": "Редагувати застосунок", + "Edit Configuration": "Редагувати конфігурацію", + "Edit Endpoint": "Редагувати кінцеву точку", + "Edit Organisation": "Редагувати організацію", + "Edit Property '{key}' of '{title}'": "Редагувати властивість '{key}' об'єкта '{title}'", + "Edit Register": "Редагувати реєстр", + "Edit Schema": "Редагувати схему", + "Edit Source": "Редагувати джерело", + "Edit View": "Редагувати представлення", + "Edit Webhook": "Редагувати вебхук", + "Edit labels": "Редагувати метки", + "Edit processing activity": "Редагувати операцію обробки", + "Edit tags for {name}": "Редагувати теги для {name}", + "Edit verwerkingsactiviteit": "Редагувати verwerkingsactiviteit", + "Edit view": "Редагувати представлення", + "Edit view details": "Редагувати детали представлення", + "Effectiveness": "Ефективність", + "Email": "Електронна пошта", + "Email Addresses": "Адреса електронної пошти", + "Email digest frequency": "Частота дайджеста по почте", + "Emails": "Письма", + "Embedding Model": "Модель векторних представлень", + "Embedding Provider": "Провайдер векторних представлень", + "Embedding generated successfully": "Векторное представлення успішно створено", + "Embedding provider connection successful!": "Підключення до провайдеру векторних представлень успішно!", + "Embedding test failed: {error}": "Тест векторних представлень не удался: {error}", + "Enable": "Увімкнути", + "Enable RAG": "Увімкнути RAG", + "Enable auto-creation": "Увімкнути автосоздание", + "Enable automatic file vectorization": "Увімкнути автоматическую векторизацію файлів", + "Enable automatic object vectorization": "Увімкнути автоматическую векторизацію об'єктів", + "Enable automatic synchronization": "Увімкнути автоматическую синхронизацию", + "Enable calendar provider": "Увімкнути провайдер календаря", + "Enable detailed logging for SOLR operations (recommended for debugging)": "Увімкнути подробное логирование операцій SOLR (рекомендуется для отладки)", + "Enable faceting": "Увімкнути фасетирование", + "Enable or disable LLM features. Configure providers and models using the LLM Configuration button above.": "Увімкнути або отключить функції LLM. Настройте провайдерів і моделі з помощью кнопки «Налаштування LLM» вище.", + "Enable or disable LLM features. Configure providers and models using the LLM configuration button above.": "Увімкнути або отключить функції LLM. Настройте провайдерів і моделі з помощью кнопки налаштування LLM вище.", + "Enable or disable SOLR search integration. Configure connection settings using the Connection Settings button above.": "Увімкнути або отключить інтеграцію пошуку SOLR. Настройте параметри підключення з помощью кнопки «Налаштування підключення» вище.", + "Enable or disable SOLR search integration. Configure connection settings using the connection settings button above.": "Увімкнути або отключить інтеграцію пошуку SOLR. Настройте параметри підключення з помощью кнопки налаштувань підключення вище.", + "Enable or disable n8n workflow integration. Configure connection settings below.": "Увімкнути або отключить інтеграцію з рабочими процессами n8n. Настройте параметри підключення ниже.", + "Enable or disable this webhook": "Увімкнути або отключить цей вебхук", + "Enable tools that allow the agent to interact with data through function calling.": "Увімкнути інструменти, позволяющие агенту взаимодействовать з даними через виклик функцій.", + "Enable vectorization for all existing and future views (may increase costs)": "Увімкнути векторизацію для всіх існуючих і будущих представлень (може увеличить витрати)", + "Enable write-back to target objects": "Увімкнути обратную запис у цільові об'єкти", + "Enabled": "Включено", + "End Date Field": "Поле дати окончания", + "Endpoint Path*": "Путь конечной точки*", + "Endpoints": "Кінцеві точки", + "Engine not found": "Движок не знайдено", + "Enter ConfigSet name": "Введите назва набору конфігурації", + "Enter Nextcloud usernames to grant access to this private agent": "Введите імена користувачіу Nextcloud для предоставления доступу до цьому частному агенту", + "Enter agent description (optional)": "Введите опис агента (необязательно)", + "Enter agent name": "Введите ім'я агента", + "Enter application description (optional)": "Введите опис застосунки (необязательно)", + "Enter application name": "Введите назва застосунки", + "Enter collection name": "Введите назва колекції", + "Enter configuration description (optional)": "Введите опис конфігурації (необязательно)", + "Enter configuration title": "Введите заголовок конфігурації", + "Enter description (optional)...": "Введите опис (необязательно)...", + "Enter new collection name": "Введите нове назва колекції", + "Enter object ID": "Введите ID об'єкта", + "Enter organisation description (optional)": "Введите опис організації (необязательно)", + "Enter organisation name": "Введите назва організації", + "Enter search term": "Введите пошуковий запит", + "Enter search terms or leave empty to browse all": "Введите пошукові запити або залиште порожнім для перегляду всіх", + "Enter system prompt for the agent": "Введите системний промпт для агента", + "Enter username and press Enter": "Введите ім'я користувача і натисніть Enter", + "Enter view name...": "Введите назва представлення...", + "Enter webhook name": "Введите назва вебхука", + "Enter your API key": "Введите ваш API-ключ", + "Enter your object here...": "Введите ваш об'єкт тут...", + "Enter your schema here...": "Введите вашу схему тут...", + "Entities": "Про'єкти", + "Entities detected": "Про'єкти виявлено", + "Entity ID: {id}": "ID об'єкта: {id}", + "Entity Information": "Інформація про об'єкті", + "Entity Recognition": "Розпізнавання об'єктів", + "Entity Recognition Method": "Метод распознавания об'єктів", + "Entity deleted successfully": "Про'єкт успішно видалено", + "Entity not found": "Про'єкт не знайдено", + "Entries to be deleted:": "Записи для удаления:", + "Enum Issue": "Проблема з перечислением", + "Enum constraint is missing": "Ограничение перечисления отсутствует", + "Erase (Art 17)": "Стереть (Ст. 17)", + "Erase {count} object(s) for this subject? This action is logged in the audit trail.": "Стереть {count} об'єкт(ов) для цього суб'єкта? Це дія буде зафиксировано у журнале аудиту.", + "Erasure complete": "Стирание завершено", + "Erasure preview": "Предпросмотр стирания", + "Error": "Помилка", + "Error Details": "Детали помилки", + "Error Information": "Інформація про ошибке", + "Error Message": "Сообщение про ошибке", + "Error deleting audit trails: {error}": "Помилка при удалении журналов аудиту: {error}", + "Error deleting search trails: {error}": "Помилка при удалении журналов пошуку: {error}", + "Error loading application": "Помилка при завантаженні застосунки", + "Error loading audit trails": "Помилка при завантаженні журналов аудиту", + "Error loading entity": "Помилка при завантаженні об'єкта", + "Error loading search trails": "Помилка при завантаженні журналов пошуку", + "Estimated Batches:": "Розрахункове кількість пакетов:", + "Estimated Cost:": "Розрахункова стоимость:", + "Estimated Duration:": "Розрахункова длительность:", + "Event": "Подія", + "Event Property for Payload": "Властивість події для полезной навантаження", + "Events": "Події", + "Every schema with detected PII has a processing-activity annotation.": "Кожна схема з виявленими персональними даними имеет аннотацию про операції обробки.", + "Example": "Приклад", + "Example Value": "Приклад значення", + "Example value for this property": "Приклад значення для цього властивості", + "Excel (.xlsx)": "Excel (.xlsx)", + "Exclusive Maximum": "Строгий максимум", + "Exclusive Minimum": "Строгий минимум", + "Exclusive maximum": "Строгий максимум", + "Exclusive minimum": "Строгий минимум", + "Execution Mode": "Режим виконання", + "Execution Time": "Година виконання", + "Execution Time Range": "Діапазон часу виконання", + "Existing Improvements": "Существующие улучшения", + "Expand details": "Развернуть детали", + "Expiration": "Срок дії", + "Expires": "Истекает", + "Expires in (e.g., 90d)": "Истекает через (наприклад, 90d)", + "Exponential": "Експоненціальний", + "Export": "Експорт", + "Export \"{schema}\" objects from \"{register}\"": "Експорт об'єктів \"{schema}\" з \"{register}\"", + "Export Configuration": "Експортувати конфігурацію", + "Export Format:": "Формат експорту:", + "Export Objects": "Експортувати об'єкти", + "Export completed successfully": "Експорт успішно завершено", + "Export downloaded successfully": "Експорт успішно завантажено", + "Export failed: {error}": "Помилка експорту: {error}", + "Export is rate limited. Please try again later.": "Експорт ограничен по частоте запитів. Повторите спробу позже.", + "Export my data": "Експортувати мои дані", + "Export, view, or delete audit trails": "Експортувати, просматривать або удалять журнали аудиту", + "Exporting...": "Експорт...", + "Extend Schema": "Расширить схему", + "Extended": "Розширений", + "Extensions": "Розширення", + "Extra Query Parameters": "Додаткові параметри запиту", + "Extract Now": "Извлечь сейчас", + "Extract Pending Files": "Извлечь ожидающие файли", + "Extract now": "Извлечь сейчас", + "Extract properties from each object": "Извлечь властивості з кожного об'єкта", + "Extracted At": "Извлечено у", + "Extracted at": "Извлечено у", + "Extraction": "Видобуток", + "Extraction Mode": "Режим извлечения", + "Extraction Scope": "Область извлечения", + "Extraction Status": "Статус извлечения", + "Extraction error for {file}: {error}": "Помилка извлечения для {file}: {error}", + "Facet Description": "Опис фасету", + "Facet Order": "Порядок фасетов", + "Facet Title": "Заголовок фасету", + "Facet Type": "Тип фасету", + "Facet configuration updated successfully": "Конфігурація фасетов успішно оновлена", + "Facetable": "Поддерживает фасетирование", + "Faceting Configuration:": "Конфігурація фасетирования:", + "Facets discovered and configured successfully": "Фасети успішно виявлено і налаштовано", + "Facets discovered successfully": "Фасети успішно виявлено", + "Failed": "Не вдалося", + "Failed to add attachment to object": "Не вдалося додати вложение до об'єкту", + "Failed to add user to organisation": "Не вдалося додати користувача у організацію", + "Failed to analyze schema properties": "Не вдалося проанализировать властивості схеми", + "Failed to archive conversation": "Не вдалося архівувати переписку", + "Failed to calculate sizes": "Не вдалося обчислити розміри", + "Failed to cancel deactivation": "Не вдалося скасувати деактивацію", + "Failed to change active organisation: {error}": "Не вдалося змінити активную організацію: {error}", + "Failed to change password": "Не вдалося змінити пароль", + "Failed to check version: {error}": "Не вдалося перевірити версію: {error}", + "Failed to clear audit trails: {error}": "Не вдалося очистити журнали аудиту: {error}", + "Failed to clear blob objects: {error}": "Не вдалося очистити об'єкти blob: {error}", + "Failed to clear cache: {error}": "Не вдалося очистити кеш: {error}", + "Failed to clear collection": "Не вдалося очистити колекцію", + "Failed to clear conversation": "Не вдалося очистити переписку", + "Failed to clear embeddings: {error}": "Не вдалося очистити векторні представлення: {error}", + "Failed to clear search trails: {error}": "Не вдалося очистити журнали пошуку: {error}", + "Failed to clear {type} cache: {error}": "Не вдалося очистити кеш {type}: {error}", + "Failed to copy changes": "Не вдалося скопировать зміни", + "Failed to copy data": "Не вдалося скопировать дані", + "Failed to copy data to clipboard": "Не вдалося скопировать дані у буфер обміну", + "Failed to copy to clipboard": "Не вдалося скопировать у буфер обміну", + "Failed to copy token": "Не вдалося скопировать токен", + "Failed to create conversation": "Не вдалося створити переписку", + "Failed to create missing SOLR fields: {error}": "Не вдалося створити отсутствующие поля SOLR: {error}", + "Failed to create or find project": "Не вдалося створити або знайти проект", + "Failed to create token": "Не вдалося створити токен", + "Failed to delete SOLR collection: {error}": "Не вдалося видалити колекцію SOLR: {error}", + "Failed to delete collection": "Не вдалося видалити колекцію", + "Failed to delete conversation": "Не вдалося видалити переписку", + "Failed to delete objects for {schema}: {error}": "Не вдалося видалити об'єкти для {schema}: {error}", + "Failed to delete webhook": "Не вдалося видалити вебхук", + "Failed to depublish register: {error}": "Не вдалося зняти реєстр з публікації: {error}", + "Failed to depublish schema: {error}": "Не вдалося зняти схему з публікації: {error}", + "Failed to depublish: {error}": "Не вдалося зняти з публікації: {error}", + "Failed to discover files: {error}": "Не вдалося обнаружить файли: {error}", + "Failed to download API specification": "Не вдалося завантажити спецификацию API", + "Failed to export data": "Не вдалося експортувати дані", + "Failed to export objects": "Не вдалося експортувати об'єкти", + "Failed to fetch conversation history": "Не вдалося отримати историю переписки", + "Failed to fix mismatched SOLR fields: {error}": "Не вдалося исправить несоответствующие поля SOLR: {error}", + "Failed to get SOLR field configuration": "Не вдалося отримати конфігурацію поліу SOLR", + "Failed to get chat statistics": "Не вдалося отримати статистику чату", + "Failed to get database information: %s": "Не вдалося отримати інформацію про базе даних: %s", + "Failed to get entity categories": "Не вдалося отримати категорії об'єктів", + "Failed to get entity statistics": "Не вдалося отримати статистику об'єктів", + "Failed to get entity types": "Не вдалося отримати типи об'єктів", + "Failed to get response: {error}": "Не вдалося отримати відповідь: {error}", + "Failed to invalidate app store cache: {error}": "Не вдалося аннулировать кеш магазина застосунків: {error}", + "Failed to leave organisation: {error}": "Не вдалося покинуть організацію: {error}", + "Failed to link object": "Не вдалося связать об'єкт", + "Failed to load Deck cards": "Не вдалося завантажити картки Deck", + "Failed to load LLM configuration": "Не вдалося завантажити конфігурацію LLM", + "Failed to load Nextcloud groups": "Не вдалося завантажити групи Nextcloud", + "Failed to load cache statistics: {error}": "Не вдалося завантажити статистику кешу: {error}", + "Failed to load conversation": "Не вдалося завантажити переписку", + "Failed to load entities": "Не вдалося завантажити об'єкти", + "Failed to load entity": "Не вдалося завантажити об'єкт", + "Failed to load extraction data": "Не вдалося завантажити дані извлечения", + "Failed to load files": "Не вдалося завантажити файли", + "Failed to load linked contacts": "Не вдалося завантажити пов'язані контакти", + "Failed to load linked emails": "Не вдалося завантажити пов'язані письма", + "Failed to load linked events": "Не вдалося завантажити пов'язані події", + "Failed to load organisations": "Не вдалося завантажити організації", + "Failed to load register data": "Не вдалося завантажити дані реєстра", + "Failed to load related entities": "Не вдалося завантажити пов'язані об'єкти", + "Failed to load settings: {error}": "Не вдалося завантажити налаштування: {error}", + "Failed to load statistics: {error}": "Не вдалося завантажити статистику: {error}", + "Failed to load templates": "Не вдалося завантажити шаблони", + "Failed to load view: {error}": "Не вдалося завантажити представлення: {error}", + "Failed to load webhooks": "Не вдалося завантажити вебхуки", + "Failed to load workflows": "Не вдалося завантажити робочі процеси", + "Failed to process message": "Не вдалося обробити сообщение", + "Failed to publish register: {error}": "Не вдалося опублікувати реєстр: {error}", + "Failed to publish schema: {error}": "Не вдалося опублікувати схему: {error}", + "Failed to publish: {error}": "Не вдалося опублікувати: {error}", + "Failed to refresh database information": "Не вдалося оновити інформацію про базе даних", + "Failed to reindex collection": "Не вдалося переіндексувати колекцію", + "Failed to remove avatar": "Не вдалося видалити аватар", + "Failed to remove link": "Не вдалося видалити посилання", + "Failed to remove member: {error}": "Не вдалося видалити участника: {error}", + "Failed to remove schema {schema}: {error}": "Не вдалося видалити схему {schema}: {error}", + "Failed to remove user from organisation": "Не вдалося видалити користувача з організації", + "Failed to rename conversation": "Не вдалося переименовать переписку", + "Failed to request deactivation": "Не вдалося запросить деактивацію", + "Failed to restore conversation": "Не вдалося відновити переписку", + "Failed to retrieve extraction status.": "Не вдалося отримати статус извлечения.", + "Failed to retrieve objects for file.": "Не вдалося отримати об'єкти для файлу.", + "Failed to retry extraction": "Не вдалося повторить видобуток", + "Failed to retry webhook": "Не вдалося повторить вебхук", + "Failed to revoke token": "Не вдалося отозвать токен", + "Failed to save GitHub token": "Не вдалося зберегти токен GitHub", + "Failed to save GitLab URL": "Не вдалося зберегти URL GitLab", + "Failed to save GitLab token": "Не вдалося зберегти токен GitLab", + "Failed to save LLM settings: {error}": "Не вдалося зберегти налаштування LLM: {error}", + "Failed to save additional feedback": "Не вдалося зберегти додатковий відгук", + "Failed to save configuration.": "Не вдалося зберегти конфігурацію.", + "Failed to save configuration: {error}": "Не вдалося зберегти конфігурацію: {error}", + "Failed to save facet configuration": "Не вдалося зберегти конфігурацію фасетов", + "Failed to save feedback": "Не вдалося зберегти відгук", + "Failed to save file settings: {error}": "Не вдалося зберегти налаштування файлів: {error}", + "Failed to save n8n configuration": "Не вдалося зберегти конфігурацію n8n", + "Failed to save preferences": "Не вдалося зберегти предпочтения", + "Failed to save roles": "Не вдалося зберегти ролі", + "Failed to save settings": "Не вдалося зберегти налаштування", + "Failed to save settings: {error}": "Не вдалося зберегти налаштування: {error}", + "Failed to save view: {error}": "Не вдалося зберегти представлення: {error}", + "Failed to save warmup interval: {error}": "Не вдалося зберегти интервал прогрева: {error}", + "Failed to save webhook": "Не вдалося зберегти вебхук", + "Failed to send feedback": "Не вдалося надіслати відгук", + "Failed to set active organisation: {error}": "Не вдалося встановити активную організацію: {error}", + "Failed to setup SOLR: {error}": "Не вдалося налаштувати SOLR: {error}", + "Failed to start file warmup: {error}": "Не вдалося запустити прогрев файлів: {error}", + "Failed to start processing {type} files: {error}": "Не вдалося начать обробку файлів {type}: {error}", + "Failed to start vectorization: {error}": "Не вдалося запустити векторизацію: {error}", + "Failed to switch organisation: {error}": "Не вдалося переключить організацію: {error}", + "Failed to sync magic table for {schema}: {error}": "Не вдалося синхронізувати magic table для {schema}: {error}", + "Failed to test SOLR connection: {error}": "Не вдалося перевірити підключення SOLR: {error}", + "Failed to test webhook": "Не вдалося перевірити вебхук", + "Failed to update LLM settings: {error}": "Не вдалося оновити налаштування LLM: {error}", + "Failed to update Multitenancy settings: {error}": "Не вдалося оновити налаштування мультиарендности: {error}", + "Failed to update RBAC settings: {error}": "Не вдалося оновити налаштування RBAC: {error}", + "Failed to update Retention settings: {error}": "Не вдалося оновити налаштування зберігання: {error}", + "Failed to update SOLR settings: {error}": "Не вдалося оновити налаштування SOLR: {error}", + "Failed to update favorite status": "Не вдалося оновити статус избранного", + "Failed to update schema configuration for {schema}: {error}": "Не вдалося оновити конфігурацію схеми {schema}: {error}", + "Failed to update schema properties": "Не вдалося оновити властивості схеми", + "Failed to update view": "Не вдалося оновити представлення", + "Failed to update view: {error}": "Не вдалося оновити представлення: {error}", + "Failed to update webhook": "Не вдалося оновити вебхук", + "Failed to upload avatar": "Не вдалося завантажити аватар", + "Failed to validate {schema}: {error}": "Не вдалося перевірити {schema}: {error}", + "Failed to vectorize files: {error}": "Не вдалося векторизовать файли: {error}", + "Failed to warmup names cache: {error}": "Не вдалося прогреть кеш імен: {error}", + "Federation": "Федерация", + "Feedback recorded": "Відгук записан", + "Fewer sources (1-3):": "Меньше источников (1-3):", + "Field": "Поле", + "Fields to Display": "Поля для отображения", + "File": "Файл", + "File #{id}": "Файл #{id}", + "File Chunk Prediction": "Прогноз фрагментів файлу", + "File Collection": "Колекція файлів", + "File Handling": "Обробка файлів", + "File Location": "Расположение файлу", + "File Management": "Управління файлами", + "File Name": "Ім'я файлу", + "File Path": "Путь до файлу", + "File Type Selection": "Вибір типу файлу", + "File Types to Process": "Типи файлів для обробки", + "File Vectorization": "Векторизація файлів", + "File Warmup": "Прогрев файлів", + "File actions menu": "Меню дій з файлом", + "File anonymized successfully": "Файл успішно анонимизирован", + "File chunk vectorization is not yet implemented. The chunks are ready and stored, but the vectorization service is under development.": "Векторизація фрагментів файлів ще не реалізована. Фрагменти готовийі і збережено, але сервис векторизації находится у разработке.", + "File collection not configured": "Колекція файлів не налаштована", + "File discovery completed": "Обнаружение файлів завершено", + "File extraction completed": "Видобуток файлів завершено", + "File extraction queued": "Видобуток файлів поставлено у очередь", + "File indexed successfully": "Файл успішно проіндексовано", + "File is already anonymized": "Файл вже анонимизирован", + "File name": "Ім'я файлу", + "File not found": "Файл не знайдено", + "File settings saved successfully": "Налаштування файлів успішно збережено", + "File settings updated successfully": "Налаштування файлів успішно оновлено", + "File sources": "Джерела файлів", + "File vectorization completed! {vectorized} chunks vectorized from {files} files. {failed} failed.": "Векторизація файлів завершена! Векторизовано {vectorized} фрагментів з {files} файлів. {failed} не вдалося.", + "File vectorization configuration saved successfully": "Конфігурація векторизації файлів успішно збережена", + "File vectorization started. Check the statistics section for progress.": "Векторизація файлів запущена. Прогресс см. у разделе статистики.", + "File warmup completed": "Прогрев файлів завершено", + "Files": "Файли", + "Files added successfully": "Файли успішно додано", + "Files with Completed Extraction:": "Файли з завершеним извлечением:", + "Files → fileCollection, Objects → objectCollection": "Файли → fileCollection, Про'єкти → objectCollection", + "Files → filecollection, objects → objectcollection": "Файли → filecollection, об'єкти → objectcollection", + "Filter Audit Trails": "Фильтровать журнали аудиту", + "Filter Deleted Items": "Фильтровать видалені елементи", + "Filter Objects": "Фильтровать об'єкти", + "Filter Properties": "Фильтровать властивості", + "Filter Search Trails": "Фильтровать журнали пошуку", + "Filter Statistics": "Статистика фільтріу", + "Filter and analyze search trail entries": "Фильтровать і анализировать записи журналу пошуку", + "Filter and manage audit trail entries": "Фильтровать і управляти записями журналу аудиту", + "Filter and manage soft deleted items": "Фильтровать і управляти програмно видаленими елементами", + "Filter and search entities": "Фильтровать і искать об'єкти", + "Filter by object ID": "Фильтровать по ID об'єкта", + "Filter by search term": "Фильтровать по поисковому запросу", + "Filter by type": "Фильтровать по типу", + "Filter by webhook": "Фильтровать по веб-хуку", + "Filter data loaded automatically. Use the filters below to refine your search.": "Дані фільтріу завантажено автоматично. Використовуйте фільтри ниже для уточнения пошуку.", + "Filter fields...": "Фильтровать поля...", + "Filter webhook triggers by payload properties (one per line, format: key: value)": "Фильтровать тригери вебхука по свойствам полезной навантаження (по одному на строку, формат: key: value)", + "Filtered": "Отфильтровано", + "Filters": "Фільтри", + "Finalizing setup...": "Завершение налаштування...", + "Fireworks AI Chat Settings": "Налаштування чату Fireworks AI", + "Fireworks AI Embedding Configuration": "Конфігурація векторних представлень Fireworks AI", + "First": "Перший", + "Fixed": "Исправлено", + "For chat and retrieval-augmented generation": "Для чату і генерации з розширеним поиском", + "For vector embeddings and semantic search": "Для векторних представлень і семантичного пошуку", + "Format": "Формат", + "Format Issue": "Проблема формату", + "Format constraint is missing": "Ограничение формату отсутствует", + "From (e.g. -7 days or 2025-01-01)": "Уід (наприклад, -7 days або 2025-01-01)", + "From Date": "Дата початку", + "From date": "Дата початку", + "Full data copied to clipboard": "Повні дані скопійовано у буфер обміну", + "Function Tools": "Функціональні інструменти", + "General Issue": "Загальна проблема", + "Generate recommendations and confidence scores": "Сгенерировать рекомендации і оценки достоверности", + "Generate report": "Сгенерировать звіт", + "Generate the verantwoordingsdocument": "Сгенерировать verantwoordingsdocument", + "Generate vectors immediately when new objects are created": "Генерировать вектори сразу при создании нових об'єктів", + "Generated: {time}": "Сгенерировано: {time}", + "Get real-time insights into your organization's data health by focusing on registers, schema definitions, and object storage and usage.": "Получайте дані у реальном часу про состоянии даних вашей організації, уделяя внимание реестрам, определениям схем, хранению і использованию об'єктів.", + "Get real-time insights into your organization's data health, growth trends, and quality metrics.": "Получайте дані у реальном часу про состоянии даних вашей організації, тенденциях роста і показателях качества.", + "GitHub Personal Access Token": "Персональний токен доступу GitHub", + "GitHub token is valid": "Токен GitHub действителен", + "GitHub token is valid! Username: {username}": "Токен GitHub действителен! Ім'я користувача: {username}", + "GitHub token removed successfully": "Токен GitHub успішно видалено", + "GitHub token saved successfully": "Токен GitHub успішно збережено", + "GitHub token test failed: {message}": "Тест токена GitHub не удался: {message}", + "GitLab Personal Access Token": "Персональний токен доступу GitLab", + "GitLab URL saved successfully": "URL GitLab успішно збережено", + "GitLab token is valid": "Токен GitLab действителен", + "GitLab token is valid! Username: {username}": "Токен GitLab действителен! Ім'я користувача: {username}", + "GitLab token saved successfully": "Токен GitLab успішно збережено", + "GitLab token test failed: {message}": "Тест токена GitLab не удался: {message}", + "Go to linked Register": "Перейти до связанному реєстру", + "Go to linked Schema": "Перейти до пов'язаної схеме", + "Go to organisation": "Перейти до організації", + "Grant additional permissions beyond standard CRUD operations": "Надати додаткові разрешения помимо стандартних CRUD-операцій", + "Group Quota": "Квота групи", + "Groups": "Групи", + "HTML preview": "Предпросмотр HTML", + "HTTP Method": "Метод HTTP", + "HTTP method used to send webhook requests": "Метод HTTP для отправки запитів вебхука", + "Headers": "Заголовки", + "Health": "Состояние", + "Heartbeat successful - connection kept alive": "Сигнал жизнеспособности успешен — з'єднання поддерживается", + "Helpful": "Полезно", + "Here you can configure the details for various connections.": "Тут ви можете налаштувати параметри різних подключений.", + "Here you can configure the details for your organisation.": "Тут ви можете налаштувати параметри вашей організації.", + "Hide": "Приховати", + "Hide Filters": "Приховати фільтри", + "Hide in collection view": "Приховати у представлении колекції", + "Hide in forms": "Приховати у формах", + "Hide in list view": "Приховати у списковом представлении", + "High": "Високий", + "High Confidence": "Висока достоверность", + "Host": "Хост", + "Hourly": "Кожний година", + "How deep to traverse nested object properties (1-20). Higher values capture more detail but increase vector size.": "Глубина обхода вкладених властивостей об'єкта (1-20). Большие значення захоплюють больше деталей, але увеличивают розмір вектора.", + "How often to check for updates (1-168 hours)": "Як часто проверять обновления (1-168 годин)", + "How to handle retries for failed webhook deliveries": "Як обробляти повторні спроби при неудачной доставке вебхука", + "IBANs": "Номера IBAN", + "ID": "ID", + "ID of input mapping (optional)": "ID входного маппінгу (необязательно)", + "ID of output mapping (optional)": "ID вихідного маппінгу (необязательно)", + "ID of the target resource": "ID целевого ресурса", + "ID:": "ID:", + "ID: {id}": "ID: {id}", + "IP rate limits cleared successfully": "Ограничения частоти запитів IP успішно очищено", + "Identify properties not in the schema": "Визначити властивості, отсутствующие у схеме", + "Immutable": "Незмінний", + "Import": "Імпорт", + "Import Configuration": "Імпортувати конфігурацію", + "Import Data into Register": "Імпортувати дані у реєстр", + "Import successful": "Імпорт успішно виконано", + "Import the report-bundle.json template to get the `reports` register, then create your first dashboard via the standard object UI. Dashboards declare their widgets in JSON and the renderer feeds each widget live aggregation data.": "Импортируйте шаблон report-bundle.json, щоб отримати реєстр `reports`, затем створіть перший дашборд через стандартний интерфейс об'єктів. Дашборди оголошують свои віджети у JSON, а рендерер передає каждому виджету живі агреговані дані.", + "Improved Property": "Улучшенное властивість", + "In use": "Використовується", + "Inactive": "Неактивний", + "Inactive organisations cannot be used": "Неактивні організації нельзя використовувати", + "Include IDs and names of related objects for better contextual search": "Увімкнути ідентифікатори і імена пов'язаних об'єктів для улучшения контекстного пошуку", + "Include ids and names of related objects for better contextual search": "Увімкнути ідентифікатори і імена пов'язаних об'єктів для улучшения контекстного пошуку", + "Include related object references": "Увімкнути посилання на пов'язані об'єкти", + "Include schema and register metadata": "Увімкнути метадані схеми і реєстра", + "Index": "Індекс", + "Information about the current OpenRegister installation": "Інформація про текущей установке OpenRegister", + "Inherited from register": "Унаследовано уід реєстра", + "Inherits permissions from register": "Наследует разрешения уід реєстра", + "Initialization failed": "Инициализация не удалась", + "Initialize Project": "Инициализировать проект", + "Initializing SOLR setup...": "Инициализация налаштування SOLR...", + "Input Mapping": "Входной маппінг", + "Inspect Fields": "Перевірити поля", + "Inspect Index": "Перевірити індекс", + "Inspect SOLR Index": "Перевірити індекс SOLR", + "Install the notify_push app from the Nextcloud App Store to enable real-time updates.": "Установите застосунок notify_push з магазина застосунків Nextcloud для включення обновлений у реальном часу.", + "Integrations": "Інтеграції", + "Internet location (URL) of this installation": "Интернет-адрес (URL) цієї установки", + "Interval": "Интервал", + "Invalid": "Недійсний", + "Invalid batch size. Must be between 1 and 5000": "Невірний розмір пакету. Повинен бути уід 1 до 5000", + "Invalid feedback type": "Недопустимий тип відгуку", + "Invalid field name provided": "Указано недійсне ім'я поля", + "Invalid maxObjects. Must be 0 (all) or positive number": "Невірне значення maxObjects. Повинно бути 0 (всі) або положительное число", + "Invalid:": "Недійсний:", + "Invite Users": "Пригласить користувачіу", + "Invited Users": "Запрошені користувачі", + "Inzage (Art 15)": "Inzage (Ст. 15)", + "Inzage results": "Результати Inzage", + "Issue": "Проблема", + "Items per page:": "Елементів на странице:", + "Just now": "Тільки що", + "Key": "Ключ", + "LLM Configuration": "Налаштування LLM", + "LLM actions menu": "Меню дій LLM", + "LLM configuration saved successfully": "Конфігурація LLM успішно збережена", + "LLM features disabled": "Функції LLM відключені", + "LLM features enabled": "Функції LLM включеноі", + "LLM must be enabled with an embedding provider configured": "LLM повинен бути увімкнено з налаштованим провайдером векторних представлень", + "LLM settings saved successfully": "Налаштування LLM успішно збережено", + "LLM settings updated successfully": "Налаштування LLM успішно оновлено", + "Label": "Метка", + "Labels": "Метки", + "Languages": "Мови", + "Last": "Останній", + "Last Triggered": "Останнє спрацювання", + "Leave empty to allow all users access": "Залиште порожнім для доступу всіх користувачіу", + "Leave off for auto-detection from property format.": "Залиште вимкненим для автоопределения з формату властивості.", + "Left organisation successfully": "Організація успішно покинута", + "Legal basis *": "Правовое основание *", + "Length Range:": "Діапазон довжини:", + "Linear": "Лінійний", + "Link": "Посилання", + "Link an email from the Mail app sidebar to associate it with this object.": "Зв'яжіть письмо з боковой панели застосунки Mail з цим об'єктом.", + "Link an object to see it here.": "Зв'яжіть об'єкт, щоб увидеть його тут.", + "Link another object": "Связать другой об'єкт", + "Link existing event": "Связать существующее подія", + "Link removed": "Посилання видалена", + "Link to Object": "Посилання на об'єкт", + "Link to {name}": "Посилання на {name}", + "Linked Objects": "Пов'язані об'єкти", + "Linked by {user}": "Связано пользователем {user}", + "Linked emails, calendar events, contacts and Deck cards will appear here.": "Тут будуть отображаться пов'язані письма, події календаря, контакти і картки Deck.", + "Linked to {name}": "Связано з {name}", + "Load Advanced Filters": "Завантажити розширені фільтри", + "Load advanced filters with live data from your search index": "Завантажити розширені фільтри з живими даними з вашего поискового індексу", + "Load more": "Завантажити ще", + "Load view": "Завантажити представлення", + "Loading API tokens...": "Завантаження API-токенов...", + "Loading ConfigSets...": "Завантаження наборів конфігурації...", + "Loading LLM configuration...": "Завантаження конфігурації LLM...", + "Loading RBAC settings...": "Завантаження налаштувань RBAC...", + "Loading activity...": "Завантаження активності...", + "Loading advanced filters...": "Завантаження розширених фільтріу...", + "Loading agents...": "Завантаження агентів...", + "Loading application details...": "Завантаження сведений про приложении...", + "Loading applications...": "Завантаження застосунків...", + "Loading archived conversations...": "Завантаження архівованих переписок...", + "Loading audit trails...": "Завантаження журналов аудиту...", + "Loading available tools...": "Завантаження доступних інструментів...", + "Loading cache statistics...": "Завантаження статистики кешу...", + "Loading collections...": "Завантаження колекцій...", + "Loading configurations...": "Завантаження конфигураций...", + "Loading conversation...": "Завантаження переписки...", + "Loading conversations...": "Завантаження переписок...", + "Loading deleted items...": "Завантаження видалених елементів...", + "Loading endpoints": "Завантаження кінцевих точок", + "Loading entities...": "Завантаження об'єктів...", + "Loading events...": "Завантаження подій...", + "Loading filter data...": "Завантаження даних фільтріу...", + "Loading groups...": "Завантаження груп...", + "Loading linked objects...": "Завантаження пов'язаних об'єктів...", + "Loading log details...": "Завантаження деталей журналу...", + "Loading multitenancy settings...": "Завантаження налаштувань мультиарендности...", + "Loading n8n configuration...": "Завантаження конфігурації n8n...", + "Loading permission matrix...": "Завантаження матрицьі разрешений...", + "Loading preferences...": "Завантаження предпочтений...", + "Loading register data...": "Завантаження даних реєстра...", + "Loading registers...": "Завантаження реєстріу...", + "Loading retention settings...": "Завантаження налаштувань зберігання...", + "Loading schemas...": "Завантаження схем...", + "Loading search configuration...": "Завантаження конфігурації пошуку...", + "Loading search trails...": "Завантаження журналов пошуку...", + "Loading sources...": "Завантаження источников...", + "Loading statistics...": "Завантаження статистики...", + "Loading text extraction settings...": "Завантаження налаштувань извлечения текста...", + "Loading tokens...": "Завантаження токенов...", + "Loading user groups...": "Завантаження груп користувачіу...", + "Loading users...": "Завантаження користувачіу...", + "Loading views...": "Завантаження представлень...", + "Loading...": "Завантаження...", + "Local": "Локальний", + "Local Version": "Локальна версія", + "Locate every object referencing a data subject (Art 15 inzage), preview an erasure (Art 17 vergetelheid), or export their data (Art 20 portabiliteit).": "Найдите кожний об'єкт, що посилається на суб'єкта даних (Ст. 15 inzage), предпросмотрите стирание (Ст. 17 vergetelheid) або експортуйте їх дані (Ст. 20 portabiliteit).", + "Location": "Расположение", + "Location Field": "Поле расположения", + "Locations": "Местоположения", + "Locked": "Заблокировано", + "Locked:": "Заблокировано:", + "Login successful": "Вход виконано успішно", + "Logs": "Журнали", + "Low": "Низкий", + "Low Confidence": "Низкая достоверность", + "Lower numbers appear first in the filter sidebar. Leave empty for automatic ordering.": "Меньшие числа отображаются першими у боковой панели фільтріу. Залиште порожнім для автоматической сортування.", + "Lucene Version:": "Версія Lucene:", + "Magic table sync completed for {schema}": "Синхронізація magic table для {schema} завершена", + "Magic table synchronized successfully": "Magic table успішно синхронизирована", + "Mail Integration": "Інтеграція з почтой", + "Mail integration is not available": "Інтеграція з почтой недоступна", + "Make this installation discoverable within the federation network.": "Сделать цю установку доступной для обнаружения у сети федерации.", + "Manage Organisation Roles": "Управління ролями організації", + "Manage SOLR Collections (data stores) and assign them for objects and files.": "Управління коллекциями SOLR (хранилищами даних) і їх призначення для об'єктів і файлів.", + "Manage SOLR ConfigSets (configuration templates) for your collections.": "Управління наборами конфігурації SOLR (шаблонами конфігурації) для ваших колекцій.", + "Manage SOLR collections (data stores) and assign them for objects and files.": "Управління коллекциями SOLR (хранилищами даних) і їх призначення для об'єктів і файлів.", + "Manage SOLR configsets (configuration templates) for your collections.": "Управління наборами конфігурації SOLR (шаблонами конфігурації) для ваших колекцій.", + "Manage and configure agents for automated tasks": "Управління і налаштування агентів для автоматизованих завдань", + "Manage and monitor file text extraction status": "Управління і мониторинг статусу извлечения текста з файлів", + "Manage and restore soft deleted items from your registers": "Управління і відновлення програмно видалених елементів з ваших реєстріу", + "Manage and view detected entities from files and objects": "Управління і перегляд виявлених об'єктів у файлах і даних", + "Manage document templates and themes": "Управління шаблонами і темами документів", + "Manage n8n workflows for OpenRegister automation. Workflows will be stored in the configured project.": "Управління рабочими процессами n8n для автоматизации OpenRegister. Робочі процеси будуть збережено у настроенном проекте.", + "Manage processing activities, run data-subject access requests, and audit compliance under the EU GDPR / Dutch AVG.": "Управління операциями обробки, виконання запитів на доступ суб'єктів даних і аудит відповідності требованиям EU GDPR / нидерландского AVG.", + "Manage webhooks for event-driven integrations": "Управління веб-хуками для інтеграцій на основе подій", + "Manage your account settings, security, and personal data.": "Керуйте настройками аккаунта, безопасностью і персональними даними.", + "Manage your applications and modules": "Керуйте своїми приложениями і модулями", + "Manage your chat conversations": "Керуйте своїми переписками", + "Manage your data registers and their configurations": "Керуйте реестрами даних і їх конфигурациями", + "Manage your data schemas and their properties": "Керуйте схемами даних і їх свойствами", + "Manage your data sources and their configurations": "Керуйте источниками даних і їх конфигурациями", + "Manage your organisations and switch between them": "Керуйте своїми организациями і переключайтесь між ними", + "Manage your saved search configurations": "Керуйте збереженими конфигурациями пошуку", + "Manage your system configurations and settings": "Керуйте системними конфигурациями і настройками", + "Managed": "Керований", + "Mappings": "Маппинги", + "Mass validation completed successfully": "Массовая перевірка успішно завершена", + "Mass validation failed: {error}": "Массовая перевірка не удалась: {error}", + "Max Files (0 = all)": "Максимальне кількість файлів (0 = всі)", + "Max Length": "Максимальна довжина", + "Max Length:": "Максимальна довжина:", + "Max Objects (0 = all)": "Максимальне кількість об'єктів (0 = всі)", + "Max Retries": "Максимальне число повторних спроб", + "Max Shards/Node": "Максимальне кількість шардов/вузол", + "Max Tokens": "Максимальне кількість токенов", + "Max execution time (ms)": "Максимальне година виконання (мс)", + "Max ms": "Макс. мс", + "Max result count": "Максимальне число результатіу", + "Max results": "Максимальне число результатіу", + "Maximum": "Максимум", + "Maximum File Size (MB)": "Максимальний розмір файлу (МБ)", + "Maximum Nesting Depth": "Максимальна глубина вложенности", + "Maximum length": "Максимальна довжина", + "Maximum length constraint is missing": "Ограничение максимальной довжини отсутствует", + "Maximum length is too restrictive": "Максимальна довжина слишком ограничительна", + "Maximum number of files to process. Set to 0 to process all files.": "Максимальне кількість файлів для обробки. Установите 0 для обробки всіх файлів.", + "Maximum number of items": "Максимальне кількість елементів", + "Maximum number of objects to process. Set to 0 to process all objects.": "Максимальне кількість об'єктів для обробки. Установите 0 для обробки всіх об'єктів.", + "Maximum number of retry attempts for failed deliveries": "Максимальне кількість спроб повтора при неудачной доставке", + "Maximum time to wait before committing changes": "Максимальне година ожидания перед фиксацией изменений", + "Maximum tokens to generate": "Максимальне кількість генерованих токенов", + "Maximum value": "Максимальне значення", + "Maximum value constraint is missing": "Ограничение максимального значення отсутствует", + "Maximum value is too restrictive": "Максимальне значення слишком ограничительно", + "Medium": "Средний", + "Medium Confidence": "Средняя достоверность", + "Medium queries: Searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средние запити: пошук з некоторой фильтрацией або кількома параметрами (наприклад, діапазони дат, конкретні реєстри/схеми)", + "Medium queries: searches with some filtering or multiple parameters (e.g., date ranges, specific registers/schemas)": "Средние запити: пошук з некоторой фильтрацией або кількома параметрами (наприклад, діапазони дат, конкретні реєстри/схеми)", + "Member removed successfully": "Участник успішно видалено", + "Members": "Участники", + "Members ({count})": "Участники ({count})", + "Members of selected groups can access this view": "Члени вибраних груп можуть отримати доступ до цьому представлению", + "Members:": "Участники:", + "Memory prediction calculated successfully": "Прогноз памяти успішно рассчитан", + "Message content is required": "Вміст сообщения обязательно", + "Message does not belong to this conversation": "Сообщение не принадлежит цієї переписке", + "Message not found": "Сообщение не найдено", + "Metadata": "Метадані", + "Metadata Filters": "Фільтри метаданих", + "Method": "Метод", + "Method*": "Метод*", + "Method: {method}": "Метод: {method}", + "Min Length": "Мінімальна довжина", + "Min execution time (ms)": "Мінімальне година виконання (мс)", + "Min ms": "Мин. мс", + "Min result count": "Мінімальне число результатіу", + "Min results": "Мінімальне число результатіу", + "Minimum": "Минимум", + "Minimum length": "Мінімальна довжина", + "Minimum number of items": "Мінімальне кількість елементів", + "Minimum value": "Мінімальне значення", + "Minimum value constraint is missing": "Ограничение минимального значення отсутствует", + "Minimum value is too restrictive": "Мінімальне значення слишком ограничительно", + "Missing conversation or agentUuid": "Отсутствует переписка або agentUuid", + "Missing conversationId": "Отсутствует conversationId", + "Missing message": "Отсутствует сообщение", + "Missing object fields created successfully": "Отсутствующие поля об'єкта успішно створено", + "Mode:": "Режим:", + "Model": "Модель", + "Model Name": "Назва моделі", + "Modified": "Изменено", + "Monthly": "Ежемесячно", + "More sources (10-20):": "Больше источников (10-20):", + "Most Active Objects": "Наиболее активні об'єкти", + "Multiple Of": "Кратно", + "Multiple of": "Кратно", + "Multiple search trail deletion not implemented yet": "Множественное видалення журналов пошуку ще не реалізовано", + "Multitenancy": "Мультиарендность", + "Multitenancy settings updated successfully": "Налаштування мультиарендности успішно оновлено", + "My Account": "Мой аккаунт", + "Naam": "Naam", + "Naam *": "Naam *", + "Name": "Назва", + "Name *": "Назва *", + "Name*": "Назва*", + "Names cache warmed up successfully: {count} names loaded in {time}. {cache}": "Кеш імен успішно прогрет: завантажено {count} імен за {time}. {cache}", + "Names cache warmed up successfully: {count} names loaded in {time}. {message}": "Кеш імен успішно прогрет: завантажено {count} імен за {time}. {message}", + "Names cache warmup completed": "Прогрев кешу імен завершено", + "Naming pattern for copies:": "Шаблон именования копий:", + "New Collection Name": "Нове назва колекції", + "New Configuration": "Нова конфігурація", + "New Conversation": "Нова переписка", + "New Properties": "Нові властивості", + "New Property": "Нове властивість", + "New User Group": "Нова група користувачіу", + "New Value": "Нове значення", + "New activity": "Нова активність", + "New password": "Новий пароль", + "New processing activity": "Нова операція обробки", + "New users without specific organisation membership will be automatically added to this organisation": "Нові користувачі без конкретного членства у організації будуть автоматично додано у цю організацію", + "New verwerkingsactiviteit": "Нова verwerkingsactiviteit", + "Next": "Далее", + "Nextcloud Groups": "Групи Nextcloud", + "No": "Немає", + "No API tokens.": "API-токенов немає.", + "No ConfigSets found": "Набори конфігурації не знайдено", + "No Deck cards linked to this object": "До цьому об'єкту не прив'язані картки Deck", + "No GitHub token configured": "Токен GitHub не налаштовано", + "No GitHub token provided": "Токен GitHub не надано", + "No GitLab token provided": "Токен GitLab не надано", + "No Organisation": "Без організації", + "No active filters": "Активних фільтріу немає", + "No active objects to soft-delete for schema {schema}. Use \"Permanently Delete\" to remove soft-deleted objects.": "Немає активних об'єктів для программного удаления у схеме {schema}. Використовуйте «Видалити навсегда» для удаления програмно видалених об'єктів.", + "No activity data available": "Дані про активності недоступні", + "No activity found.": "Активність не знайдена.", + "No agents available": "Агенти недоступні", + "No agents found": "Агенти не знайдено", + "No app store cache exists yet": "Кеш магазина застосунків ще не существует", + "No applications are available.": "Застосунки недоступні.", + "No applications found": "Застосунки не знайдено", + "No approval steps for this object.": "Шаги согласования для цього об'єкта отсутствуют.", + "No archived conversations": "Архівованих переписок немає", + "No audit trail entries found": "Записи журналу аудиту не знайдено", + "No changes recorded": "Зміни не зафіксовано", + "No chunks to vectorize": "Фрагментів для векторизації немає", + "No collections found": "Колекції не знайдено", + "No configuration data": "Даних конфігурації немає", + "No configuration selected": "Конфігурація не вибрана", + "No configurations are available.": "Конфігурації недоступні.", + "No configurations found": "Конфігурації не знайдено", + "No configurations found for this application.": "Конфігурації для цього застосунки не знайдено.", + "No contacts linked to this object": "До цьому об'єкту не прив'язані контакти", + "No conversations yet. Create a new one to get started!": "Переписок ще немає. Створіть новую, щоб начать!", + "No dashboards yet": "Дашбордов ще немає", + "No data available": "Дані недоступні", + "No data available for chart": "Дані для графика недоступні", + "No deleted items found": "Видалені елементи не знайдено", + "No deletion data available": "Дані про удалении недоступні", + "No description found": "Опис не найдено", + "No description provided": "Опис не указано", + "No emails linked to this object": "До цьому об'єкту не прив'язані письма", + "No endpoints defined": "Кінцеві точки не визначено", + "No entities detected for this email.": "Про'єкти для цього письма не виявлено.", + "No entities detected in this file. Run text extraction first.": "Про'єкти у цьому файле не виявлено. Сначала виконайте видобуток текста.", + "No entities found": "Про'єкти не знайдено", + "No entities have been detected yet": "Про'єкти ще не виявлено", + "No events found": "Події не знайдено", + "No events linked to this object": "До цьому об'єкту не прив'язані події", + "No expired audit trails found to clear": "Прострочені записи журналу аудиту для очистки не знайдено", + "No expired search trails found to clear": "Прострочені записи журналу пошуку для очистки не знайдено", + "No extraction data available for this file": "Дані извлечения для цього файлу недоступні", + "No facetable fields available. Select a register and schema to see available filters.": "Поля для фасетирования недоступні. Виберіть реєстр і схему для перегляду доступних фільтріу.", + "No files found": "Файли не знайдено", + "No files have been extracted yet": "Файли ще не извлекались", + "No files selected": "Файли не вибрано", + "No files to process": "Файлів для обробки немає", + "No files to reindex": "Файлів для переиндексации немає", + "No filters are currently active. This will delete ALL audit trail entries!": "Активних фільтріу немає. Це видалить ВСІ записи журналу аудиту!", + "No groups found. Try a different search.": "Групи не знайдено. Попробуйте другой пошуковий запит.", + "No label": "Без метки", + "No labels": "Без меток", + "No linked objects": "Пов'язаних об'єктів немає", + "No log entries found": "Записи журналу не знайдено", + "No logs are available for this configuration.": "Журнали для цієї конфігурації недоступні.", + "No logs are available for this source.": "Журнали для цього джерела недоступні.", + "No logs found": "Журнали не знайдено", + "No matches": "Совпадений немає", + "No mismatched fields found - SOLR schema is properly configured": "Несоответствующие поля не знайдено — схема SOLR налаштована правильно", + "No objects are currently selected for deletion.": "Про'єкти для удаления не вибрано.", + "No objects are currently selected for permanent deletion.": "Про'єкти для безвозвратного удаления не вибрано.", + "No objects are currently selected for restoration.": "Про'єкти для восстановления не вибрано.", + "No objects found": "Про'єкти не знайдено", + "No objects found to vectorize": "Про'єкти для векторизації не знайдено", + "No objects linked to this email": "До цьому письму не прив'язані об'єкти", + "No objects selected": "Про'єкти не вибрано", + "No objects selected for deletion": "Про'єкти для удаления не вибрано", + "No objects selected for restoration": "Про'єкти для восстановления не вибрано", + "No objects to delete": "Об'єктів для удаления немає", + "No objects to delete for schema {schema}": "Об'єктів для удаления у схеме {schema} немає", + "No objects were permanently deleted": "Про'єкти не були безповоротно видалено", + "No organisations found": "Організації не знайдено", + "No personal data was found for this subject identifier.": "Персональні дані для цього ідентифікатора суб'єкта не знайдено.", + "No processing activities yet": "Операцій обробки ще немає", + "No properties found": "Властивості не знайдено", + "No properties match your filters.": "Властивості не соответствуют вашим фильтрам.", + "No purge date set": "Дата очистки не задана", + "No register data available": "Дані реєстра недоступні", + "No register objects reference this file": "Ни один об'єкт реєстра не посилається на цей файл", + "No registers found": "Реєстри не знайдено", + "No registers found for this application.": "Реєстри для цього застосунки не знайдено.", + "No registers found. Create a register to configure permissions.": "Реєстри не знайдено. Створіть реєстр для налаштування разрешений.", + "No related cases found for this sender": "Пов'язаних дел для цього отправителя не найдено", + "No related entities": "Пов'язаних об'єктів немає", + "No relations found": "Зв'язки не знайдено", + "No request body available": "Тело запиту недоступно", + "No response body available": "Тело відповіді недоступно", + "No saved views yet. Create one in the Search tab!": "Збережених представлень ще немає. Створіть одно на вкладке Пошук!", + "No saved views yet. create one in the search tab!": "Збережених представлень ще немає. Створіть одно на вкладке пошуку!", + "No scan run yet": "Перевірка ще не запускалась", + "No schema data available": "Дані схеми недоступні", + "No schema selected for exploration": "Схема для исследования не вибрана", + "No schemas configured for mail linking.": "Схеми для привязки пошти не налаштовано.", + "No schemas found": "Схеми не знайдено", + "No schemas found for this application.": "Схеми для цього застосунки не знайдено.", + "No search terms data available": "Дані про пошукових запросах недоступні", + "No search trail entries found": "Записи журналу пошуку не знайдено", + "No sources are available.": "Джерела недоступні.", + "No sources found": "Джерела не знайдено", + "No synchronizations found": "Синхронізації не знайдено", + "No templates found": "Шаблони не знайдено", + "No templates have been created yet": "Шаблони ще не створено", + "No tools available. Tools can be registered by installed apps.": "Інструменти недоступні. Інструменти можуть бути зареєстровано встановленими приложениями.", + "No users in this organisation.": "У цієї організації немає користувачіу.", + "No valid magic-mapped register+schema combinations found": "Допустимі комбінації реєстр+схема з magic mapping не знайдено", + "No views found. Create views first before configuring vectorization.": "Представлення не знайдено. Сначала створіть представлення перед настройкой векторизації.", + "No views match your search": "Представлення не соответствуют вашему поиску", + "No webhooks found": "Вебхуки не знайдено", + "No webhooks have been configured yet": "Вебхуки ще не налаштовано", + "No workflows found in this project. Create workflows in the n8n editor.": "У цьому проекте робочі процеси не знайдено. Створіть робочі процеси у редакторе n8n.", + "None": "Немає", + "Not Found": "Не найдено", + "Not extracted": "Не извлечено", + "Not helpful": "Бесполезно", + "Not in use": "Не використовується", + "Note:": "Примечание:", + "Notifications": "Сповіщення", + "Number Constraints": "Числові ограничения", + "Number of Sources": "Кількість источников", + "Number of chunks to vectorize in one API call (1-100).": "Кількість фрагментів для векторизації у одном API-виклику (1-100).", + "Number of chunks to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Кількість фрагментів для векторизації у одном API-виклику. Больше = швидше, але больше памяти. Рекомендуется: 10-50.", + "Number of objects to process in each batch (1-5000).": "Кількість об'єктів для обробки у каждом пакете (1-5000).", + "Number of objects to vectorize in one API call. Higher = faster but more memory. Recommended: 10-50.": "Кількість об'єктів для векторизації у одном API-виклику. Больше = швидше, але больше памяти. Рекомендуется: 10-50.", + "Numeric Range:": "Числовий діапазон:", + "Object": "Про'єкт", + "Object #{id}": "Про'єкт #{id}", + "Object Collection": "Колекція об'єктів", + "Object Configuration:": "Конфігурація об'єкта:", + "Object Count Prediction": "Прогноз кількості об'єктів", + "Object Extraction Mode": "Режим извлечения об'єктів", + "Object ID": "ID об'єкта", + "Object Management": "Управління об'єктами", + "Object Text Extraction": "Видобуток текста з об'єктів", + "Object Vectorization": "Векторизація об'єктів", + "Object Warmup": "Прогрев об'єктів", + "Object changes in owned objects": "Зміни об'єктів у принадлежащих об'єктах", + "Object collection not configured": "Колекція об'єктів не налаштована", + "Object metadata incomplete for file upload": "Метадані об'єкта неповні для завантаження файлу", + "Object not found": "Про'єкт не знайдено", + "Object permanently deleted": "Про'єкт безповоротно видалено", + "Object restored successfully": "Про'єкт успішно відновлено", + "Object settings updated successfully": "Налаштування об'єкта успішно оновлено", + "Object sources": "Джерела об'єктів", + "Object successfully deleted": "Про'єкт успішно видалено", + "Object successfully modified": "Про'єкт успішно змінено", + "Object unlocked successfully": "Про'єкт успішно разблокирован", + "Object vectorization configuration saved successfully": "Конфігурація векторизації об'єктів успішно збережена", + "Object vectorized successfully": "Про'єкт успішно векторизован", + "Object:": "Про'єкт:", + "Objects": "Про'єкти", + "Objects Analyzed": "Проаналізованих об'єктів", + "Objects Distribution": "Распределение об'єктів", + "Objects being analyzed": "Аналізовані об'єкти", + "Objects by Register": "Про'єкти по реестрам", + "Objects by Schema": "Про'єкти по схемам", + "Objects deletion completed for {schema}": "Видалення об'єктів для {schema} завершено", + "Objects deletion completed successfully": "Видалення об'єктів успішно завершено", + "Objects in schema": "Про'єкти у схеме", + "Objects successfully deleted": "Про'єкти успішно видалено", + "Objects to Process:": "Об'єктів для обробки:", + "Objects to be analyzed": "Про'єкти для анализа", + "Objects to be deleted": "Про'єкти для удаления", + "Objects to be validated": "Про'єкти для перевірки", + "Objects will be serialized as JSON text before vectorization": "Про'єкти будуть серіалізовано у формат JSON перед векторизацией", + "Objects will be soft deleted and moved to the": "Про'єкти будуть програмно видалено і переміщено у", + "Objects will be soft-deleted (marked as deleted but kept in database). They can be recovered later if needed.": "Про'єкти будуть програмно видалено (позначено як видалені, але збережено у базе даних). При необходимости їх можна відновити позже.", + "Old Value": "Старое значення", + "Oldest Item (days)": "Старейший елемент (днів)", + "Ollama Chat Settings": "Налаштування чату Ollama", + "Ollama Embedding Configuration": "Конфігурація векторних представлень Ollama", + "Ollama URL": "URL Ollama", + "Only With Changes": "Тільки з изменениями", + "Only members of selected groups can access this organisation": "Тільки члени вибраних груп можуть отримати доступ до цієї організації", + "Open Nextcloud App Store": "Уідкрити магазин застосунків Nextcloud", + "Open OpenRegister sidebar": "Уідкрити боковую панель OpenRegister", + "Open Register Objects": "Уідкрити об'єкти реєстра", + "Open folder": "Уідкрити папку", + "Open in OpenRegister": "Уідкрити у OpenRegister", + "Open n8n Editor": "Уідкрити редактор n8n", + "OpenAI Chat Settings": "Налаштування чату OpenAI", + "OpenAI Embedding Configuration": "Конфігурація векторних представлень OpenAI", + "OpenDocument (.ods)": "OpenDocument (.ods)", + "OpenRegister": "OpenRegister", + "OpenRegister Settings": "Налаштування OpenRegister", + "Operator-defined dashboards and scheduled reports. Each dashboard is a first-class object in the `reports` register; widgets are declared in the dashboard's `widgets` array and rendered live from aggregations / GraphQL.": "Операторские дашборди і заплановані звіти. Кожний дашборд є першокласним об'єктом у реєстрі `reports`; віджети оголошуються у массиве `widgets` дашборда і отображаются у реальном часу з агрегаций / GraphQL.", + "Optimizing search performance...": "Оптимизация продуктивності пошуку...", + "Optional URL-friendly identifier": "Необов'язковий зручний для URL ідентифікатор", + "Optional description for this facet": "Необов'язковий опис для цього фасету", + "Optional end date property": "Необов'язковий властивість дати окончания", + "Optional event description template": "Необов'язковий шаблон опису події", + "Optional location property": "Необов'язковий властивість расположения", + "Optional webhook secret for signature verification": "Необов'язковий секрет вебхука для перевірки подписи", + "Optional: Add query parameters to filter the referenced schema (e.g., status=active&type=public)": "Необязательно: додайте параметри запиту для фильтрации посилальної схеми (наприклад, status=active&type=public)", + "Or": "Або", + "Order": "Порядок", + "Ordered BCP 47 language tags. The first language is the register default and drives Accept-Language fallback for translatable properties.": "Впорядковані теги мови BCP 47. Перший мова є мовою реєстра за замовчуванням і управляє резервним вариантом Accept-Language для перекладних властивостей.", + "Organisation": "Організація", + "Organisation Configuration": "Конфігурація організації", + "Organisation ID": "ID організації", + "Organisation Members": "Участники організації", + "Organisation Statistics": "Статистика організації", + "Organisation created successfully": "Організація успішно створена", + "Organisation membership changes": "Зміни членства у організації", + "Organisation name is required": "Назва організації обязательно", + "Organisation settings saved successfully": "Налаштування організації успішно збережено", + "Organisation successfully created": "Організація успішно створена", + "Organisation successfully updated": "Організація успішно оновлена", + "Organisational measures": "Організаційні заходи", + "Organisations": "Організації", + "Organisatorische maatregelen": "Organisatorische maatregelen", + "Organization": "Організація", + "Organization ID (Optional)": "ID організації (необязательно)", + "Organizations": "Організації", + "Orphaned Items": "Осиротевшие елементи", + "Other": "Другое", + "Output Mapping": "Вихідний маппінг", + "Overview": "Обзор", + "Owner": "Владелец", + "Owner:": "Владелец:", + "PDF": "PDF", + "PII hits": "Попадания PII", + "PII rows": "Рядки PII", + "Page {current} of {total}": "Страница {current} з {total}", + "Parallel Mode (Faster, more resource intensive)": "Паралельний режим (швидше, але более ресурсомісткий)", + "Parallel:": "Параллельно:", + "Password": "Пароль", + "Password changes are not supported by your authentication provider.": "Ваш провайдер автентифікації не поддерживает зміна пароля.", + "Password for SOLR authentication (optional)": "Пароль для автентифікації SOLR (необязательно)", + "Password updated successfully": "Пароль успішно оновлено", + "Path": "Путь", + "Path in repository": "Путь у репозитории", + "Path where the configuration file will be saved in the repository": "Путь сохранения файлу конфігурації у репозитории", + "Path where the register OAS file will be saved in the repository": "Путь сохранения файлу OAS реєстра у репозитории", + "Pattern (regex)": "Шаблон (regex)", + "Pattern Issue": "Проблема шаблону", + "Pattern constraint is missing": "Ограничение шаблону отсутствует", + "Patterns:": "Шаблони:", + "Pending": "Ожидает", + "Permanent Delete Mode": "Режим безвозвратного удаления", + "Permanently Delete": "Видалити навсегда", + "Permanently Delete ({count})": "Видалити навсегда ({count})", + "Permanently delete all {count} soft-deleted objects. This cannot be undone!": "Безповоротно видалити всі {count} програмно видалених об'єкта. Це дія нельзя скасувати!", + "Permanently delete already soft-deleted objects": "Безповоротно видалити вже програмно видалені об'єкти", + "Permanently delete this conversation?": "Безповоротно видалити цю переписку?", + "Permission Matrix": "Матриця разрешений", + "Person": "Персона", + "Personal Data": "Персональні дані", + "Personal Data Export": "Експорт персональних даних", + "Persons": "Персони", + "Phone": "Телефон", + "Phone Numbers": "Номера телефонов", + "Photos": "Фотографии", + "Please create an agent in the": "Будь ласка, створіть агента у", + "Please select a user": "Будь ласка, виберіть користувача", + "Please select an agent to continue": "Будь ласка, виберіть агента для продолжения", + "Please select an organisation": "Будь ласка, виберіть організацію", + "Please select at least one item to import": "Будь ласка, виберіть хотя б один елемент для імпорту", + "Please select registers or schemas first": "Будь ласка, сначала виберіть реєстри або схеми", + "Please select which register and schema to use for the new object": "Будь ласка, виберіть реєстр і схему для нового об'єкта", + "Please try again later.": "Будь ласка, повторите спробу позже.", + "Please wait while we fetch your agents.": "Будь ласка, подождите, поки загружаются ваши агенти.", + "Please wait while we fetch your applications.": "Будь ласка, подождите, поки загружаются ваши застосунки.", + "Please wait while we fetch your configurations.": "Будь ласка, подождите, поки загружаются ваши конфігурації.", + "Please wait while we fetch your deleted items.": "Будь ласка, подождите, поки загружаются ваши видалені елементи.", + "Please wait while we fetch your sources.": "Будь ласка, подождите, поки загружаются ваши джерела.", + "Please wait while we permanently delete the SOLR collection. This may take a few moments.": "Будь ласка, подождите, поки колекція SOLR безповоротно видаляється. Це може занять некоторое година.", + "Polls": "Опитування", + "Popular Search Terms": "Популярні пошукові запити", + "Port": "Порт", + "Portabiliteit (Art 20)": "Portabiliteit (Ст. 20)", + "Preferences saved": "Предпочтения збережено", + "Prerequisites": "Попередні условия", + "Press Enter to send, Shift+Enter for new line": "Натисніть Enter для отправки, Shift+Enter для новой рядки", + "Press enter to send, shift+enter for new line": "Натисніть Enter для отправки, Shift+Enter для новой рядки", + "Prettify": "Форматировать", + "Preview erasure": "Предпросмотр стирания", + "Preview:": "Предпросмотр:", + "Previous": "Попередній", + "Private": "Приватний", + "Private Agent (Default)": "Приватний агент (за замовчуванням)", + "Private agents": "Приватні агенти", + "Process Name (optional)": "Назва процесу (необязательно)", + "Processed {indexed} files, {failed} failed": "Обработано {indexed} файлів, {failed} не вдалося", + "Processes chunks in batches with simulated parallelism.": "Обробляє фрагменти пакетами з симульованим параллелизмом.", + "Processes file chunks sequentially (safest).": "Обробляє фрагменти файлів последовательно (найбільш безпечний режим).", + "Processes objects in chunks with simulated parallelism.": "Обробляє об'єкти фрагментами з симульованим параллелизмом.", + "Processes objects sequentially (safest).": "Обробляє об'єкти последовательно (найбільш безпечний режим).", + "Processing": "Обробка", + "Processing Limits": "Ограничения обробки", + "Processing...": "Обробка...", + "Project initialized successfully": "Проект успішно инициализирован", + "Project not found. Please initialize first.": "Проект не знайдено. Будь ласка, сначала инициализируйте.", + "Properties": "Властивості", + "Property Behaviors": "Поведение властивостей", + "Property Configuration:": "Конфігурація властивості:", + "Property Filters": "Фільтри властивостей", + "Property Title": "Заголовок властивості", + "Property Type": "Тип властивості", + "Property can be improved": "Властивість можна улучшить", + "Property could not be added": "Властивість не вдалося додати", + "Property could not be updated": "Властивість не вдалося оновити", + "Property name of inversed relation": "Ім'я властивості обратной зв'язки", + "Property successfully added": "Властивість успішно добавлено", + "Property successfully updated": "Властивість успішно оновлено", + "Protected": "Захищений", + "Provider": "Провайдер", + "Provider is required for testing": "Для тестирования необходим провайдер", + "Public": "Публічний", + "Public View": "Публічне представлення", + "Public views can be accessed by anyone in the system": "Публічні представлення доступні всім пользователям системи", + "Publish": "Опублікувати", + "Publish Configuration to GitHub": "Опублікувати конфігурацію на GitHub", + "Publish OAS": "Опублікувати OAS", + "Publish Objects": "Опублікувати об'єкти", + "Publish Register OAS to GitHub": "Опублікувати OAS реєстра на GitHub", + "Publish all objects in this schema": "Опублікувати всі об'єкти у цієї схеме", + "Publish objects to make them publicly available": "Опублікувати об'єкти для відкритого доступу", + "Published": "Опубликовано", + "Published:": "Опубликовано:", + "Publishing...": "Публікація...", + "Purge": "Очистити", + "Purge Date": "Дата очистки", + "Purge {count} object from database": "Очистити {count} об'єкт з бази даних", + "Purge {count} objects from database": "Очистити {count} об'єктів з бази даних", + "Purpose limitation *": "Ограничение цели *", + "Push Notifications": "Push-сповіщення", + "Query Complexity Distribution": "Распределение сложности запитів", + "Query Help": "Справка по запросам", + "Query parameter is required": "Параметр запиту обязателен", + "Quota": "Квота", + "RAG": "RAG", + "RAG Configuration": "Конфігурація RAG", + "RBAC": "RBAC", + "RBAC settings updated successfully": "Налаштування RBAC успішно оновлено", + "Rate limits cleared successfully": "Ограничения частоти запитів успішно очищено", + "Raw Changes Data": "Необроблені дані изменений", + "Re-vectorize on object update": "Повторно векторизовать при оновленні об'єкта", + "Read": "Чтение", + "Real-time push notification status via notify_push": "Статус push-сповіщень у реальном часу через notify_push", + "Real-time push notifications are active. Connected clients receive instant updates when objects are created, updated, or deleted.": "Push-сповіщення у реальном часу активні. Підключені клієнти получают миттєві обновления при создании, оновленні або удалении об'єктів.", + "Realtime push active": "Push у реальном часу активний", + "Realtime push not available — the notify_push app is not installed": "Push у реальном часу недоступний — застосунок notify_push не установлено", + "Reason": "Причина", + "Reason (optional)": "Причина (необязательно)", + "Rebase operation completed successfully": "Операція перебазирования успішно завершена", + "Rebase operation failed: {error}": "Операція перебазирования не удалась: {error}", + "Rechtsgrond": "Rechtsgrond", + "Recommendations:": "Рекомендации:", + "Recommended Type:": "Рекомендований тип:", + "Recommended: 5 sources": "Рекомендуется: 5 источников", + "Refresh": "Оновити", + "Refresh Data": "Оновити дані", + "Refresh Stats": "Оновити статистику", + "Refresh Workflows": "Оновити робочі процеси", + "Refresh dashboard": "Оновити дашборд", + "Refresh database info": "Оновити інформацію про базе даних", + "Register": "Реєстр", + "Register #{id}": "Реєстр #{id}", + "Register / Schema": "Реєстр / Схема", + "Register ID": "ID реєстра", + "Register Objects": "Про'єкти реєстра", + "Register Overview": "Обзор реєстра", + "Register Statistics": "Статистика реєстра", + "Register Totals": "Итоги реєстра", + "Register and schema are required": "Реєстр і схема обов'язкові", + "Register annotation": "Аннотация реєстра", + "Register depublished successfully": "Реєстр успішно снят з публікації", + "Register not found": "Реєстр не знайдено", + "Register objects": "Про'єкти реєстра", + "Register objects deletion completed successfully": "Видалення об'єктів реєстра успішно завершено", + "Register published successfully": "Реєстр успішно опубликован", + "Register/Schema Usage": "Використання реєстра/схеми", + "Register:": "Реєстр:", + "Registers": "Реєстри", + "Reindex": "Переіндексувати", + "Reindex completed": "Переіндексація завершена", + "Reindex completed successfully": "Переіндексація успішно завершена", + "Reindex completed! Processed {count} objects in {duration}s": "Переіндексація завершена! Обработано {count} об'єктів за {duration}з", + "Reindex failed": "Переіндексація не удалась", + "Reindex failed: %s": "Переіндексація не удалась: %s", + "Reject": "Відхилити", + "Rejected": "Отклонено", + "Related Cases": "Пов'язані дела", + "Relations": "Зв'язки", + "Remote Version": "Видалена версія", + "Remove": "Видалити", + "Remove Deck card": "Видалити картку Deck", + "Remove avatar": "Видалити аватар", + "Remove contact": "Видалити контакт", + "Remove filter": "Видалити фільтр", + "Remove from favorites": "Видалити з избранного", + "Remove from list": "Видалити з списку", + "Remove group": "Видалити группу", + "Remove link": "Видалити посилання", + "Remove link to {name}": "Видалити посилання на {name}", + "Remove link to {name}?": "Видалити посилання на {name}?", + "Remove link to {title}": "Видалити посилання на {title}", + "Remove oneOf entry": "Видалити запис oneOf", + "Remove property after write-back": "Видалити властивість после обратной записи", + "Remove {title}": "Видалити {title}", + "Remove {userId} from this organisation?": "Видалити {userId} з цієї організації?", + "Removed": "Видалено", + "Removed from favorites": "Видалено з избранного", + "Removing schema {schema}...": "Видалення схеми {schema}...", + "Rename Conversation": "Переименовать переписку", + "Rename conversation": "Переименовать переписку", + "Replicas": "Реплики", + "Reports": "Звіти", + "Repository": "Репозиторий", + "Request Body": "Тело запиту", + "Request Data": "Дані запиту", + "Request Details": "Детали запиту", + "Request Quota (per day)": "Квота запитів (у день)", + "Request account deactivation": "Запросить деактивацію аккаунта", + "Request account deactivation. This will notify administrators for review.": "Запросить деактивацію аккаунта. Це уведомит администраторов для перевірки.", + "Request body must be JSON with a \"query\" field": "Тело запиту повинно бути JSON з полем \"query\"", + "Request timeout in seconds": "Тайм-аут запиту у секундах", + "Requested at": "Запрошено у", + "Required": "Обязательно", + "Required Token Scopes": "Обов'язкові области токена", + "Required field": "Обов'язкове поле", + "Required status is inconsistent": "Статус обязательности несогласован", + "Requires Dolphin AI for OCR text extraction": "Требуется Dolphin AI для извлечения текста OCR", + "Rerun Search": "Повторить пошук", + "Rerunning search: {searchTerm}": "Повтор пошуку: {searchTerm}", + "Reset Changes": "Сбросить зміни", + "Reset Filters": "Сбросить фільтри", + "Resource Quotas": "Квоти ресурсов", + "Response Body": "Тело відповіді", + "Response Details": "Детали відповіді", + "Response Time:": "Година відповіді:", + "Restore": "Відновити", + "Restore conversation": "Відновити переписку", + "Restore or permanently delete items": "Відновити або безповоротно видалити елементи", + "Restore {count} object": "Відновити {count} об'єкт", + "Restore {count} objects": "Відновити {count} об'єктів", + "Result Count Range": "Діапазон кількості результатіу", + "Results": "Результати", + "Retention period (ISO-8601 duration, e.g. P10Y, P30D)": "Период зберігання (длительность ISO-8601, наприклад P10Y, P30D)", + "Retention settings updated successfully": "Налаштування зберігання успішно оновлено", + "Retrieve all objects for this schema": "Отримати всі об'єкти для цієї схеми", + "Retry": "Повторить", + "Retry Failed Extractions": "Повторить невдалі извлечения", + "Retry Policy": "Политика повтора", + "Retry completed": "Повтор завершено", + "Retry upload": "Повторить загрузку", + "Retrying...": "Повтор...", + "Review the selected objects below. You can remove any objects you don't want to delete by clicking the remove button.": "Просмотрите вибрані об'єкти ниже. Ви можете убрать будь-які об'єкти, які не хочете удалять, нажав кнопку удаления.", + "Review the selected objects below. You can remove any objects you don't want to include.": "Просмотрите вибрані об'єкти ниже. Ви можете убрать будь-які об'єкти, які не хочете включать.", + "Review the selected objects below. You can remove any objects you don't want to permanently delete by clicking the remove button. This action cannot be undone.": "Просмотрите вибрані об'єкти ниже. Ви можете убрать будь-які об'єкти, які не хочете безповоротно удалять, нажав кнопку удаления. Це дія нельзя скасувати.", + "Review the selected objects below. You can remove any objects you don't want to restore by clicking the remove button. Objects will be restored to their original location.": "Просмотрите вибрані об'єкти ниже. Ви можете убрать будь-які об'єкти, які не хочете восстанавливать, нажав кнопку удаления. Про'єкти будуть відновлено у исходное расположение.", + "Revoke": "Отозвать", + "Right": "Право", + "Risk Level": "Рівень риска", + "Risk level": "Рівень риска", + "Roles and Permissions": "Ролі і разрешения", + "Roles updated successfully": "Ролі успішно оновлено", + "Run compliance scan": "Запустити перевірку відповідності", + "SOLR Connection Settings": "Налаштування підключення SOLR", + "SOLR Index Warmup": "Прогрев індексу SOLR", + "SOLR Version:": "Версія SOLR:", + "SOLR actions menu": "Меню дій SOLR", + "SOLR base path (usually /solr)": "Базовий путь SOLR (зазвичай /solr)", + "SOLR collection deleted successfully": "Колекція SOLR успішно видалена", + "SOLR connection test failed: {error}": "Тест підключення SOLR не удался: {error}", + "SOLR connection test successful!": "Тест підключення SOLR успешен!", + "SOLR index warmup completed successfully": "Прогрев індексу SOLR успішно завершено", + "SOLR is disabled": "SOLR відключено", + "SOLR is not available or not configured": "SOLR недоступний або не налаштовано", + "SOLR logging disabled": "Логирование SOLR отключено", + "SOLR logging enabled": "Логирование SOLR включено", + "SOLR search disabled": "Пошук SOLR відключено", + "SOLR search enabled": "Пошук SOLR увімкнено", + "SOLR server hostname or IP address": "Ім'я хоста або IP-адрес сервера SOLR", + "SOLR server port number (optional, defaults to 8983)": "Номер порта сервера SOLR (необязательно, за замовчуванням 8983)", + "SOLR settings updated successfully": "Налаштування SOLR успішно оновлено", + "SOLR setup completed successfully": "Налаштування SOLR успішно завершена", + "SOLR setup completed successfully!": "Налаштування SOLR успішно завершена!", + "SOLR setup error: %s": "Помилка налаштування SOLR: %s", + "SOLR setup failed": "Налаштування SOLR не удалась", + "SOLR setup failed - check logs": "Налаштування SOLR не удалась — проверьте журнали", + "SOLR setup failed. Please check the configuration and try again.": "Налаштування SOLR не удалась. Проверьте конфігурацію і повторите спробу.", + "SOLR setup failed: {error}": "Налаштування SOLR не удалась: {error}", + "SOLR warmup failed: {error}": "Прогрев SOLR не удался: {error}", + "Sample Values:": "Приклади значень:", + "Save": "Зберегти", + "Save Configuration": "Зберегти конфігурацію", + "Save Connection Settings": "Зберегти налаштування підключення", + "Save Roles": "Зберегти ролі", + "Save Settings": "Зберегти налаштування", + "Save changes": "Зберегти зміни", + "Save current search as view": "Зберегти поточний пошук як представлення", + "Save labels": "Зберегти метки", + "Save tags for {name}": "Зберегти теги для {name}", + "Save the organisation first to manage users.": "Сначала сохраните організацію для управления пользователями.", + "Saved Views": "Збережені представлення", + "Saving...": "Збереження...", + "Scheduled": "Запланировано", + "Schema": "Схема", + "Schema ID": "ID схеми", + "Schema Name": "Назва схеми", + "Schema Statistics": "Статистика схеми", + "Schema annotation": "Аннотация схеми", + "Schema configuration updated successfully for {schema}": "Конфігурація схеми для {schema} успішно оновлена", + "Schema depublished successfully": "Схема успішно снята з публікації", + "Schema objects deletion completed successfully": "Видалення об'єктів схеми успішно завершено", + "Schema published successfully": "Схема успішно опубликована", + "Schema reference ($ref)": "Посилання на схему ($ref)", + "Schema successfully created": "Схема успішно створена", + "Schema successfully updated": "Схема успішно оновлена", + "Schema {schema} removed successfully": "Схема {schema} успішно видалена", + "Schema:": "Схема:", + "Schemas": "Схеми", + "Scheme": "Схема", + "Search": "Пошук", + "Search / Views": "Пошук / Представлення", + "Search / views": "Пошук / представлення", + "Search Activity": "Пошукова активність", + "Search Analytics": "Аналитика пошуку", + "Search Configuration": "Конфігурація пошуку", + "Search Entities": "Пошук об'єктів", + "Search Files": "Пошук файлів", + "Search GitHub": "Пошук на GitHub", + "Search GitLab": "Пошук на GitLab", + "Search Method": "Метод пошуку", + "Search Mode": "Режим пошуку", + "Search Query": "Пошуковий запит", + "Search Results": "Результати пошуку", + "Search Term": "Пошуковий запит", + "Search Trail Details": "Детали журналу пошуку", + "Search Trail Management": "Управління журналом пошуку", + "Search Trail Statistics": "Статистика журналу пошуку", + "Search Trails": "Журнали пошуку", + "Search Views": "Пошук представлень", + "Search Webhooks": "Пошук веб-хуков", + "Search agents...": "Пошук агентів...", + "Search applications...": "Пошук застосунків...", + "Search by file name or path": "Пошук по імені файлу або пути", + "Search by name or URL": "Пошук по імені або URL", + "Search by title or UUID...": "Пошук по заголовку або UUID...", + "Search by value": "Пошук по значению", + "Search configurations": "Пошук конфигураций", + "Search data sources...": "Пошук источников даних...", + "Search groups...": "Пошук груп...", + "Search in Database Objects": "Пошук у об'єктах бази даних", + "Search in Files": "Пошук у файлах", + "Search in Objects": "Пошук у об'єктах", + "Search objects": "Пошук об'єктів", + "Search objects...": "Пошук об'єктів...", + "Search property names...": "Пошук імен властивостей...", + "Search registers...": "Пошук реєстріу...", + "Search schemas...": "Пошук схем...", + "Search trail deletion not implemented yet": "Видалення журналу пошуку ще не реалізовано", + "Search views...": "Пошук представлень...", + "Search {name}...": "Пошук {name}...", + "Searching...": "Пошук...", + "Secret": "Секрет", + "Secret key for HMAC signature generation (optional)": "Секретний ключ для генерации подписи HMAC (необязательно)", + "Security": "Безопасность", + "See organisations as a table": "Просматривать організації у виде таблицьі", + "See organisations as cards": "Просматривать організації у виде карток", + "Select AI Agent": "Вибрати ИИ-агента", + "Select Active Organisation": "Вибрати активную організацію", + "Select All": "Вибрати все", + "Select ConfigSet": "Вибрати набір конфігурації", + "Select File Types to Vectorize:": "Вибрати типи файлів для векторизації:", + "Select HTTP method": "Вибрати метод HTTP", + "Select Register and Schema": "Вибрати реєстр і схему", + "Select Views to Vectorize:": "Вибрати представлення для векторизації:", + "Select a Nextcloud group to add": "Вибрати группу Nextcloud для добавления", + "Select a branch": "Вибрати ветку", + "Select a date property": "Вибрати властивість дати", + "Select a model or type a custom model name": "Виберіть модель або введите користувацьке ім'я моделі", + "Select a register": "Вибрати реєстр", + "Select a register...": "Вибрати реєстр...", + "Select a repository": "Вибрати репозиторий", + "Select a repository you have write access to": "Виберіть репозиторий, до которому у вас є доступ на запис", + "Select a schema": "Вибрати схему", + "Select a schema...": "Вибрати схему...", + "Select a user": "Вибрати користувача", + "Select agent type": "Вибрати тип агента", + "Select all entities": "Вибрати всі об'єкти", + "Select an AI Agent": "Вибрати ИИ-агента", + "Select an AI agent to begin chatting with your data.": "Виберіть ИИ-агента для початку общения з вашими даними.", + "Select an email to see linked objects": "Виберіть письмо для перегляду пов'язаних об'єктів", + "Select an organisation and user to add them as a member. Search for organisations by name.": "Виберіть організацію і користувача для добавления у качестве участника. Пошук організацій по імені.", + "Select backend": "Вибрати backend", + "Select chat model": "Вибрати модель чату", + "Select collection for files": "Вибрати колекцію для файлів", + "Select collection for objects": "Вибрати колекцію для об'єктів", + "Select configuration type...": "Вибрати тип конфігурації...", + "Select default organisation": "Вибрати організацію за замовчуванням", + "Select entity {value}": "Вибрати об'єкт {value}", + "Select event to listen to...": "Вибрати подія для прослушивания...", + "Select fields to display": "Вибрати поля для отображения", + "Select files with the correct extension": "Вибрати файли з правильним расширением", + "Select group": "Вибрати группу", + "Select groups (optional)": "Вибрати групи (необязательно)", + "Select groups to notify...": "Вибрати групи для сповіщення...", + "Select groups with access to this agent": "Вибрати групи з доступом до цьому агенту", + "Select groups...": "Вибрати групи...", + "Select model": "Вибрати модель", + "Select one or more registers": "Вибрати один або кілька реєстріу", + "Select one or more schemas": "Вибрати одну або кілька схем", + "Select options...": "Вибрати параметри...", + "Select or create labels, or select \"No label\" to add files": "Виберіть або створіть метки, або виберіть «Без метки», щоб додати файли", + "Select owner application (optional)...": "Вибрати застосунок-владелец (необязательно)...", + "Select period": "Вибрати период", + "Select property to send as payload": "Вибрати властивість для отправки у качестве полезной навантаження", + "Select provider": "Вибрати провайдера", + "Select registers and schemas to save a view": "Виберіть реєстри і схеми для сохранения представлення", + "Select retry policy": "Вибрати политику повтора", + "Select role": "Вибрати роль", + "Select schemas (empty = all schemas)": "Вибрати схеми (пусто = всі схеми)", + "Select schemas to warm up": "Вибрати схеми для прогрева", + "Select search mode": "Вибрати режим пошуку", + "Select source type...": "Вибрати тип джерела...", + "Select target type": "Вибрати тип цели", + "Select the branch to publish to": "Вибрати ветку для публікації", + "Select the event this webhook should listen to": "Вибрати подія, яке повинен прослушивать цей вебхук", + "Select views to filter data (optional)": "Вибрати представлення для фильтрации даних (необязательно)", + "Select views to limit which data the agent can access": "Вибрати представлення для ограничения доступних агенту даних", + "Select views to vectorize:": "Вибрати представлення для векторизації:", + "Select which Nextcloud groups are available for this organisation. Users in these groups will have access to organisation resources.": "Виберіть групи Nextcloud, доступні для цієї організації. Користувачі у цих группах получат доступ до ресурсам організації.", + "Select which data views the AI can search": "Вибрати представлення даних, які ИИ може искать", + "Select which property from the event should be used as the webhook payload data": "Вибрати властивість події для використання у качестве даних полезной навантаження вебхука", + "Select which tools the AI can use to perform actions": "Вибрати інструменти, які ИИ може використовувати для виконання дій", + "Select {fieldName} values": "Вибрати значення {fieldName}", + "Selected": "Вибрано", + "Selected Groups": "Вибрані групи", + "Selected Objects ({count})": "Вибрані об'єкти ({count})", + "Selected audit trails deleted successfully": "Вибрані записи журналу аудиту успішно видалено", + "Selected search trails deleted successfully": "Вибрані записи журналу пошуку успішно видалено", + "Selected users can access this view": "Вибрані користувачі можуть отримати доступ до цьому представлению", + "Send additional feedback": "Надіслати додатковий відгук", + "Send as CloudEvent": "Надіслати як CloudEvent", + "Send as cloudevent": "Надіслати як CloudEvent", + "Sensitive PII": "Конфіденційні персональні дані", + "Serial Mode (Safer, slower)": "Послідовний режим (безопаснее, медленнее)", + "Serial:": "Последовательно:", + "Server Configuration": "Конфігурація сервера", + "Server Information": "Інформація про сервере", + "Set as Active": "Встановити як активний", + "Set as active organisation": "Встановити як активную організацію", + "Set limits for API usage and token consumption. Use 0 for unlimited resources.": "Установите ограничения для використання API і потребления токенов. Використовуйте 0 для необмежених ресурсов.", + "Settings": "Налаштування", + "Settings updated successfully": "Налаштування успішно оновлено", + "Shards": "Шарди", + "Share": "Поделиться", + "Share with Groups": "Поделиться з группами", + "Share with Users": "Поделиться з пользователями", + "Show": "Показать", + "Show Filters": "Показать фільтри", + "Show less": "Показать меньше", + "Show only entries with changes": "Показувати тільки записи з изменениями", + "Showing {count} dashboard(s)": "Показано {count} дашборд(ов)", + "Showing {count} processing activities": "Показано {count} операцій обробки", + "Showing {showing} of {total} agents": "Показано {showing} з {total} агентів", + "Showing {showing} of {total} applications": "Показано {showing} з {total} застосунків", + "Showing {showing} of {total} audit trail entries": "Показано {showing} з {total} записів журналу аудиту", + "Showing {showing} of {total} configurations": "Показано {showing} з {total} конфигураций", + "Showing {showing} of {total} deleted items": "Показано {showing} з {total} видалених елементів", + "Showing {showing} of {total} entities": "Показано {showing} з {total} об'єктів", + "Showing {showing} of {total} files": "Показано {showing} з {total} файлів", + "Showing {showing} of {total} log entries": "Показано {showing} з {total} записів журналу", + "Showing {showing} of {total} search trail entries": "Показано {showing} з {total} записів журналу пошуку", + "Showing {showing} of {total} sources": "Показано {showing} з {total} источников", + "Showing {showing} of {total} templates": "Показано {showing} з {total} шаблонов", + "Showing {showing} of {total} webhooks": "Показано {showing} з {total} веб-хуков", + "Showing {shown} of {total} organisations": "Показано {shown} з {total} організацій", + "Simple": "Простий", + "Simple queries: Basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Прості запити: базовий текстовий пошук з мінімальними параметрами (наприклад, один пошуковий запит без розширених фільтріу)", + "Simple queries: basic text searches with minimal parameters (e.g., single search term, no advanced filters)": "Прості запити: базовий текстовий пошук з мінімальними параметрами (наприклад, один пошуковий запит без розширених фільтріу)", + "Size": "Розмір", + "Slug": "Slug", + "Soft Delete Mode": "Режим программного удаления", + "Soft Deleted Items": "Програмно видалені елементи", + "Soft delete all objects for this schema ({active} active, {deleted} already deleted)": "Програмно видалити всі об'єкти цієї схеми ({active} активних, {deleted} вже видалених)", + "SolrCloud mode enabled": "Режим SolrCloud увімкнено", + "Something went wrong while adding files": "При добавлении файлів що-то пошло не так", + "Source": "Джерело", + "Source successfully updated": "Джерело успішно оновлено", + "Sources": "Джерела", + "Sources:": "Джерела:", + "Special Rights": "Особливі права", + "Standalone SOLR mode": "Автономний режим SOLR", + "Start Conversation": "Начать переписку", + "Start Date Field": "Поле дати початку", + "Start Vectorization": "Начать векторизацію", + "Start a conversation": "Начать переписку", + "Starting names cache warmup...": "Початок прогрева кешу імен...", + "Starting validation for {schema}...": "Початок перевірки {schema}...", + "Starting {action} for {schema}...": "Початок {action} для {schema}...", + "Starting...": "Запуск...", + "Statistics": "Статистика", + "Statistics and Metrics": "Статистика і метрики", + "Statistics refreshed": "Статистика оновлена", + "Stats calculation not yet implemented": "Розрахунок статистики ще не реалізовано", + "Status": "Статус", + "Status Code": "Код статусу", + "Step": "Шаг", + "Storage": "Сховище", + "Storage Quota (MB)": "Квота сховища (МБ)", + "String Constraints": "Рядкові ограничения", + "Subject identifier (email, BSN, name, etc.)": "Ідентифікатор суб'єкта (email, BSN, ім'я і т.д.)", + "Subscribe": "Підписатися", + "Subscribed": "Підписано", + "Success": "Успех", + "Success Rate": "Процент успеха", + "Success Status": "Статус успеха", + "Successful": "Успішно", + "Successfully added user to organisation: {name}": "Користувач успішно додано у організацію: {name}", + "Successfully cleared {count} audit trails": "Успішно очищено {count} записів журналу аудиту", + "Successfully cleared {count} blob storage objects": "Успішно очищено {count} об'єктів blob-сховища", + "Successfully cleared {count} search trails": "Успішно очищено {count} записів журналу пошуку", + "Successfully created {count} SOLR fields": "Успішно створено {count} поліу SOLR", + "Successfully deleted {count} embeddings. Please re-vectorize your data.": "Успішно видалено {count} векторних представлень. Будь ласка, повторно векторизуйте ваши дані.", + "Successfully deleted {count} objects for {schema}": "Успішно видалено {count} об'єктів для {schema}", + "Successfully fixed {count} SOLR field configurations": "Успішно исправлено {count} конфигураций поліу SOLR", + "Successfully imported: {registers} registers, {schemas} schemas, {objects} objects": "Успішно импортировано: {registers} реєстріу, {schemas} схем, {objects} об'єктів", + "Successfully joined organisation": "Ви успішно присоединились до організації", + "Successfully permanently deleted {count} objects": "Успішно безповоротно видалено {count} об'єктів", + "Successfully processed {count} files!": "Успішно обработано {count} файлів!", + "Successfully restored {count} object": "Успішно відновлено {count} об'єкт", + "Successfully restored {count} objects": "Успішно восстановлено {count} об'єктів", + "Successfully saved configuration for {count} facets!": "Конфігурація для {count} фасетов успішно збережена!", + "Supported File Types": "Підтримувані типи файлів", + "Switch Active Organisation": "Переключить активную організацію", + "Switch Organisation": "Переключить організацію", + "Sync Table": "Синхронізувати таблицю", + "Synchronization Settings": "Налаштування синхронізації", + "Syncing magic table for {schema}...": "Синхронізація magic table для {schema}...", + "Syncs": "Синхронізації", + "System Default": "Системне значення за замовчуванням", + "System Prompt": "Системний промпт", + "System announcements": "Системні оголошення", + "Table": "Таблиця", + "Target ID": "ID цели", + "Target Type*": "Тип цели*", + "Technical Description": "Технічне опис", + "Technical Details": "Технические детали", + "Technical description for developers and administrators": "Технічне опис для разработчиков і администраторов", + "Technical measures": "Технические заходи", + "Technische maatregelen": "Technische maatregelen", + "Temperature": "Температура", + "Templates": "Шаблони", + "Test": "Тест", + "Test Chat": "Тест чату", + "Test Connection": "Перевірити підключення", + "Test Embedding": "Тест векторних представлень", + "Test endpoint executed successfully": "Тест конечной точки виконано успішно", + "Test webhook delivered successfully": "Тестовий вебхук успішно доставлен", + "Test webhook delivery failed": "Доставка тестового вебхука не удалась", + "Test webhook sent successfully": "Тестовий вебхук успішно надіслано", + "Testing...": "Тестирование...", + "Text Chunk #{id}": "Текстовий фрагмент #{id}", + "Text Extraction": "Видобуток текста", + "Text Extraction Engine": "Движок извлечения текста", + "Text chunks": "Текстові фрагменти", + "Text chunks are generated during file extraction and stored in the database. Vectorization reads these pre-chunked files and converts them to embeddings.": "Текстові фрагменти создаются у процессе извлечения файлів і сохраняются у базе даних. Векторизація читает ці предварительно розбиті файли і преобразует їх у векторні представлення.", + "Text extracted successfully": "Текст успішно вилучено", + "Text extraction disabled": "Видобуток текста отключено", + "The Nextcloud Calendar app is not installed or enabled on this server.": "Застосунок Nextcloud Календар не установлено або не включено на цьому сервере.", + "The Nextcloud Contacts app is not installed or enabled on this server.": "Застосунок Nextcloud Контакти не установлено або не включено на цьому сервере.", + "The Nextcloud Deck app is not installed or enabled on this server.": "Застосунок Nextcloud Deck не установлено або не включено на цьому сервере.", + "The Nextcloud Mail app is not installed or enabled on this server.": "Застосунок Nextcloud Пошта не установлено або не включено на цьому сервере.", + "The URL where webhook events will be sent": "URL, на який будуть отправляться події вебхука", + "The requested conversation does not exist": "Запитана переписка не существует", + "The requested dashboard could not be loaded. Verify the URL or pick another dashboard from the list.": "Запитаний дашборд не вдалося завантажити. Проверьте URL або виберіть другой дашборд з списку.", + "The saved view and all its search configuration": "Збережене представлення і вся його конфігурація пошуку", + "There are no audit trail entries matching your current filters.": "Немає записів журналу аудиту, відповідних вашим текущим фильтрам.", + "There are no deleted items matching your current filters.": "Немає видалених елементів, відповідних вашим текущим фильтрам.", + "There are no search trail entries matching your current filters.": "Немає записів журналу пошуку, відповідних вашим текущим фильтрам.", + "There are no webhook log entries matching your filters.": "Немає записів журналу вебхука, відповідних вашим фильтрам.", + "They will be retained according to their schema's configured retention period and automatically permanently deleted when the retention period expires. The retention period is configurable per schema and can be found in the schema's settings.": "Вони будуть храниться у соответствии з налаштованим периодом зберігання схеми і автоматично безповоротно видалено по истечении периода зберігання. Период зберігання настраивается для кожної схеми і находится у настройках схеми.", + "They will be retained according to their schema's configured retention period and then permanently removed.": "Вони будуть храниться у соответствии з налаштованим периодом зберігання схеми, а затем безповоротно видалено.", + "This action cannot be undone.": "Це дія нельзя скасувати.", + "This action cannot be undone. Make sure no collections are using this ConfigSet.": "Це дія нельзя скасувати. Убедитесь, що ни одна колекція не використовує цей набір конфігурації.", + "This action cannot be undone. Make sure no collections are using this configset.": "Це дія нельзя скасувати. Убедитесь, що ни одна колекція не використовує цей набір конфігурації.", + "This action will submit a deactivation request to your administrators.": "Це дія отправит запит на деактивацію вашим администраторам.", + "This agent has no configurable views or tools, but you can still adjust RAG settings below.": "У цього агента немає налаштовуваних представлень або інструментів, але ви можете змінити налаштування RAG ниже.", + "This analysis may take some time": "Цей анализ може занять некоторое година", + "This audit trail entry does not contain any change information.": "Ця запис журналу аудиту не містить інформації про изменениях.", + "This configuration has no data defined.": "Для цієї конфігурації не визначено дані.", + "This endpoint is deprecated. Use chunk-based endpoints instead.": "Ця конечная точка устарела. Вместо неї використовуйте кінцеві точки на основе фрагментів.", + "This entity has no relations to objects or files": "Цей об'єкт не имеет зв'язків з об'єктами або файлами", + "This is a test webhook from OpenRegister": "Це тестовий вебхук уід OpenRegister", + "This is not valid JSON": "Це не є допустимим JSON", + "This key already exists on this schema": "Цей ключ вже существует у цієї схеме", + "This schema must use Magic Table configuration to sync": "Ця схема повинна використовувати конфігурацію Magic Table для синхронізації", + "This schema must use magic table configuration to sync": "Ця схема повинна використовувати конфігурацію magic table для синхронізації", + "This source has no associated registers.": "Цей джерело не имеет пов'язаних реєстріу.", + "This token will only be shown once. Copy it now.": "Цей токен буде показан тільки один раз. Скопируйте його сейчас.", + "This will copy the _default ConfigSet with the new name": "Це скопирует набір конфігурації _default з новим именем", + "This will copy the _default configset with the new name": "Це скопирует набір конфігурації _default з новим именем", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\n\nAre you sure you want to continue?": "Це безповоротно видалить ВСІ векторні представлення з бази даних. Вам потребуется повторно векторизовать всі об'єкти і файли. Це дія нельзя скасувати.\n\nВи впевнені, що хочете продолжить?", + "This will permanently delete ALL embeddings (vectors) from the database. You will need to re-vectorize all objects and files. This action cannot be undone.\\n\\nAre you sure you want to continue?": "Це безповоротно видалить ВСІ векторні представлення з бази даних. Вам потрібно буде повторно векторизувати всі об'єкти і файли. Цю дію не можна скасувати.\\n\\nВи впевнені, що хочете продовжити?", + "This will permanently delete:": "Це безповоротно видалить:", + "This will permanently remove {count} already soft-deleted objects from the database. This action cannot be undone.": "Це безповоротно видалить {count} вже програмно видалених об'єктів з бази даних. Це дія нельзя скасувати.", + "Timeout (seconds)": "Тайм-аут (секунди)", + "Timestamp": "Временная метка", + "Tip: Only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Совет: включайте тільки те представлення, яким нужен семантичний пошук, щоб знизити витрати на векторні представлення. Прості таблицьі пошуку редко нуждаются у векторизації.", + "Tip: only enable views that need semantic search to minimize embedding costs. Simple lookup tables rarely need vectorization.": "Совет: включайте тільки те представлення, яким нужен семантичний пошук, щоб знизити витрати на векторні представлення. Прості таблицьі пошуку редко нуждаются у векторизації.", + "Title": "Заголовок", + "Title *": "Заголовок *", + "Title Template": "Шаблон заголовка", + "Title*": "Заголовок*", + "To (e.g. -1 year or 2025-12-31)": "По (наприклад, -1 year або 2025-12-31)", + "To Date": "Дата окончания", + "To add files larger than or equal to 512MB, go to the": "Для добавления файлів размером 512 МБ і более перейдите у", + "To adjust chunk size or strategy, go to File Configuration → Processing Limits.": "Для зміни розміру фрагмента або стратегии перейдите у Конфігурація файлів → Ограничения обробки.", + "To adjust chunk size or strategy, go to file configuration → processing limits.": "Для зміни розміру фрагмента або стратегии перейдите у конфігурація файлів → ограничения обробки.", + "To change the embedding provider or model, go to LLM Configuration.": "Для зміни провайдера або моделі векторних представлень перейдите у Налаштування LLM.", + "To change the embedding provider or model, go to LLM configuration.": "Для зміни провайдера або моделі векторних представлень перейдите у настройку LLM.", + "To date": "По дату", + "Toggle search sidebar": "Переключить боковую панель пошуку", + "Token Created": "Токен створено", + "Token Quota (per request)": "Квота токенов (на запит)", + "Token copied to clipboard": "Токен скопійовано у буфер обміну", + "Token name": "Ім'я токена", + "Token revoked": "Токен отозван", + "Tools": "Інструменти", + "Tools execute with the agent's default user permissions when no user session is active (e.g., cron jobs). Configure the default user in the Settings tab.": "Інструменти виконуються з правами користувача агента за замовчуванням, коли користувацький сеанс неактивний (наприклад, задания cron). Настройте користувача за замовчуванням на вкладке Налаштування.", + "Tools respect the agent's own role and organization boundaries.": "Інструменти соблюдают собственную роль агента і межі організації.", + "Tools respect the agent's views, permissions, and organization boundaries.": "Інструменти соблюдают представлення, разрешения і межі організації агента.", + "Top Deleters": "Наибольшее число удалений", + "Total": "Итого", + "Total Audit Trails": "Всего журналов аудиту", + "Total Chunks Available:": "Доступно фрагментів:", + "Total Deleted Items": "Всего видалених елементів", + "Total Members": "Всего участников", + "Total Objects": "Всего об'єктів", + "Total Objects in Database:": "Всего об'єктів у базе даних:", + "Total Organisations": "Всего організацій", + "Total Results": "Всего результатіу", + "Total Searches": "Всего поисков", + "Total Size": "Загальний розмір", + "Total Storage": "Загальне сховище", + "Total events": "Всего подій", + "Total:": "Итого:", + "Totals": "Итоги", + "Try searching by UUID or with different keywords": "Попробуйте пошук по UUID або з другими ключовими словами", + "Type": "Тип", + "Type \"DELETE COLLECTION\" to confirm:": "Введите «DELETE COLLECTION» для подтверждения:", + "Type Issue": "Проблема типу", + "Type Variations:": "Варіанти типу:", + "Type filter (optional, e.g. email)": "Фільтр типу (необязательно, наприклад email)", + "Type must be \"positive\" or \"negative\"": "Тип повинен бути «positive» або «negative»", + "Type to search for groups": "Введите для пошуку груп", + "Type to search for objects...": "Введите для пошуку об'єктів...", + "Type to search for organisations": "Введите для пошуку організацій", + "Type to search for users": "Введите для пошуку користувачіу", + "Type to search groups...": "Введите для пошуку груп...", + "Type to search users...": "Введите для пошуку користувачіу...", + "Type to search...": "Введите для пошуку...", + "Type your username to confirm": "Введите ім'я користувача для подтверждения", + "URL": "URL", + "URL where Ollama is running": "URL, по которому работает Ollama", + "UUID copied to clipboard": "UUID скопійовано у буфер обміну", + "UUID:": "UUID:", + "Unchanged": "Без изменений", + "Unique Search Terms": "Унікальні пошукові запити", + "Unique Terms": "Унікальні терміни", + "Unknown": "Неизвестно", + "Unknown Object": "Невідомий об'єкт", + "Unknown Register": "Невідомий реєстр", + "Unknown Schema": "Невідома схема", + "Unknown error": "Невідома помилка", + "Unknown widget type: {type}": "Невідомий тип віджета: {type}", + "Unlink email": "Отвязать письмо", + "Unlink event": "Отвязать подія", + "Unnamed Object": "Про'єкт без імені", + "Untitled": "Без назви", + "Untitled View": "Представлення без назви", + "Update": "Оновити", + "Update Operations": "Операції обновления", + "Update available: {local} → {remote}": "Доступно оновлення: {local} → {remote}", + "Update configuration: ...": "Оновити конфігурацію: ...", + "Update register OAS: ...": "Оновити OAS реєстра: ...", + "Update vectors when object data changes (recommended for accurate search)": "Обновлять вектори при изменении даних об'єкта (рекомендуется для точного пошуку)", + "Updated": "Оновлено", + "Updated At": "Оновлено у", + "Updated:": "Оновлено:", + "Upload new avatar": "Завантажити новий аватар", + "Uploading configuration sets...": "Завантаження наборів конфігурації...", + "Uptime:": "Година роботи:", + "Url": "Url", + "Usage Count": "Кількість использований", + "Use AI agents for processing and analysis": "Використовувати ИИ-агентів для обробки і анализа", + "Use Large Language Model features": "Використовувати функції великих мовних моделей", + "Use SolrCloud with Zookeeper for distributed search": "Використовувати SolrCloud з Zookeeper для розподіленого пошуку", + "Use SolrCloud with zookeeper for distributed search": "Використовувати SolrCloud з Zookeeper для розподіленого пошуку", + "Use VNG APIs instead of MongoDB.": "Використовувати VNG API вместо MongoDB.", + "Use default ranges (Last 7/30/90 days, Last year, Older)": "Використовувати діапазони за замовчуванням (последние 7/30/90 днів, останній рік, старее)", + "Use external storage (e.g. MongoDB) instead of the internal Nextcloud storage.": "Використовувати зовнішнє сховище (наприклад, MongoDB) вместо внутреннего сховища Nextcloud.", + "Use filters to narrow down audit trail entries by register, schema, action type, user, date range, or object ID.": "Використовуйте фільтри для сужения записів журналу аудиту по реєстру, схеме, типу дії, пользователю, диапазону дат або ID об'єкта.", + "Use filters to narrow down deleted items by register, schema, deletion date, or user who deleted them.": "Використовуйте фільтри для сужения видалених елементів по реєстру, схеме, даті удаления або пользователю, який їх удалил.", + "Use filters to narrow down entities by type or category.": "Використовуйте фільтри для сужения об'єктів по типу або категорії.", + "Use filters to narrow down search trail entries by register, schema, success status, user, date range, search terms, or performance metrics.": "Використовуйте фільтри для сужения записів журналу пошуку по реєстру, схеме, статусу успеха, пользователю, диапазону дат, пошуковим запросам або метрикам продуктивності.", + "Used By Collections": "Використовується коллекциями", + "Used by": "Використовується", + "User": "Користувач", + "User Agent Statistics": "Статистика агентів користувачіу", + "User Description": "Опис користувача", + "User Quota": "Квота користувача", + "User rate limits cleared successfully": "Ограничения частоти запитів користувача успішно очищено", + "User removed successfully": "Користувач успішно видалено", + "User-friendly description shown in forms and help text": "Понятное опис, отображаемое у формах і текстах справки", + "Username": "Ім'я користувача", + "Username for SOLR authentication (optional)": "Ім'я користувача для автентифікації SOLR (необязательно)", + "Users": "Користувачі", + "Uses": "Використовує", + "Using Pre-Generated Chunks": "Використання предварительно згенерованих фрагментів", + "Validate": "Перевірити", + "Validate Objects": "Перевірити об'єкти", + "Validation completed for {schema}": "Перевірка {schema} завершена", + "Validation completed for {schema}: {processed} processed, {updated} updated, {failed} failed": "Перевірка {schema} завершена: обработано {processed}, оновлено {updated}, не вдалося {failed}", + "Validation completed successfully": "Перевірка успішно завершена", + "Validation failed": "Перевірка не удалась", + "Value": "Значення", + "Value list (split on ,)": "Список значень (разделять запятой)", + "Vector Dimensions": "Размерность вектора", + "Vector Search Backend": "Backend векторного пошуку", + "Vector field: _embedding_": "Векторное поле: _embedding_", + "Vectorization Triggers": "Тригери векторизації", + "Vectorization completed: {vectorized} objects vectorized, {failed} failed": "Векторизація завершена: векторизовано {vectorized} об'єктів, {failed} не вдалося", + "Vectorization failed: {error}": "Векторизація не удалась: {error}", + "Vectorize All Files": "Векторизовать всі файли", + "Vectorize All Objects": "Векторизовать всі об'єкти", + "Vectorize all file types": "Векторизовать всі типи файлів", + "Vectorize all views": "Векторизовать всі представлення", + "Vectorize on object creation": "Векторизовать при создании об'єкта", + "Vectors will be stored in your existing object and file collections": "Вектори будуть збережено у існуючих коллекциях об'єктів і файлів", + "Verantwoording": "Verantwoording", + "Verifying server connectivity...": "Перевірка підключення до серверу...", + "Version": "Версія", + "Version Information": "Інформація про версії", + "Very High": "Очень високий", + "Very high": "Очень високий", + "View": "Представлення", + "View API Docs": "Перегляд документації API", + "View API Documentation": "Перегляд документації API", + "View APIs": "Перегляд API", + "View Changes": "Перегляд изменений", + "View Details": "Детали представлення", + "View Error": "Помилка представлення", + "View File": "Перегляд файлу", + "View Full Details": "Переглянути повні детали", + "View Logs": "Перегляд журналов", + "View Name": "Назва представлення", + "View Object": "Перегляд об'єкта", + "View Parameters": "Параметри представлення", + "View Selection": "Вибір представлення", + "View and analyze search trail logs with advanced filtering and analytics capabilities": "Просматривать і анализировать журнали пошуку з розширеними возможностями фильтрации і аналитики", + "View and analyze system audit trails with advanced filtering capabilities": "Просматривать і анализировать системні журнали аудиту з розширеними возможностями фильтрации", + "View and manage authorization across registers and schemas": "Просматривать і управляти авторизацией у реестрах і схемах", + "View deleted successfully": "Представлення успішно видалено", + "View entity details and manage relations": "Просматривать детали об'єкта і управляти связями", + "View name": "Назва представлення", + "View name is required": "Назва представлення обязательно", + "View object": "Перегляд об'єкта", + "View saved successfully!": "Представлення успішно збережено!", + "View search analytics and manage search logs": "Просматривать аналитику пошуку і управляти журналами пошуку", + "View successfully deleted": "Представлення успішно видалено", + "View successfully updated": "Представлення успішно оновлено", + "View updated successfully!": "Представлення успішно оновлено!", + "View webhook delivery logs and filter by webhook": "Просматривать журнали доставки веб-хуков і фильтровать по веб-хуку", + "View {count} more": "Переглянути ще {count}", + "Views": "Представлення", + "Visible to end users": "Видно кінцевим пользователям", + "Visible to users": "Видно пользователям", + "Wait for Response": "Ожидать відповіді", + "Wait for webhook response before continuing (required for request/response flows)": "Ожидать відповідь вебхука перед продолжением (обязательно для потоков запит/відповідь)", + "Waiting for cluster synchronization...": "Очікування синхронізації кластера...", + "Warmup Names Cache": "Прогреть кеш імен", + "Warmup failed": "Прогрев не удался", + "Warning": "Предупреждение", + "Warning:": "Предупреждение:", + "Warning: This property has inconsistent types: {types}. Consider if all objects should have the same type.": "Предупреждение: це властивість имеет неузгоджені типи: {types}. Рассмотрите, повинні ли всі об'єкти иметь однаковий тип.", + "We'll scan all objects belonging to this schema to discover new properties and analyze existing properties for potential enhancements. The process involves examining each object's data structure, identifying properties not defined in the current schema, and finding opportunities to improve existing property definitions with better constraints, formats, and validation rules.": "Ми просканируем всі об'єкти, принадлежащие цієї схеме, щоб обнаружить нові властивості і проанализировать существующие властивості для потенційних улучшений. Процес включает изучение структури даних кожного об'єкта, виявлення властивостей, не визначених у текущей схеме, і пошук возможностей улучшения існуючих определений властивостей з лучшими ограничениями, форматами і правилами перевірки.", + "Webhook": "Вебхук", + "Webhook Log Details": "Детали журналу вебхука", + "Webhook Logs": "Журнали вебхука", + "Webhook created successfully": "Вебхук успішно створено", + "Webhook deleted": "Вебхук видалено", + "Webhook retry delivered successfully": "Повтор вебхука успішно доставлен", + "Webhook retry delivery failed": "Доставка повтора вебхука не удалась", + "Webhook updated": "Вебхук оновлено", + "Webhook updated successfully": "Вебхук успішно оновлено", + "Webhooks": "Вебхуки", + "Weekly": "Еженедельно", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used": "Коли агент работает без пользовательского сеанса (наприклад, заплановані завдання), буде использоваться контекст цього користувача", + "When agent runs without a user session (e.g., scheduled tasks), this user's context will be used.": "Коли агент работает без пользовательского сеанса (наприклад, заплановані завдання), буде использоваться контекст цього користувача.", + "When disabled, this facet will only show values from this schema and will include a schema filter when selected.": "При отключении цей фасет буде показувати тільки значення з цієї схеми і включать фільтр схеми при виборі.", + "When enabled, saving this object will also update the target objects to include a reference back to this object.": "При включении збереження цього об'єкта також обновит цільові об'єкти для включення обратной посилання на цей об'єкт.", + "When enabled, this property will be removed from the source object after updating the target objects.": "При включении це властивість буде видалено з исходного об'єкта после обновления цільових об'єктів.", + "Workflow Management": "Управління рабочими процессами", + "Workflows": "Робочі процеси", + "Wrap webhook payload in CloudEvents format for better interoperability": "Обернуть полезную навантаження вебхука у формат CloudEvents для лучшей совместимости", + "Wrap webhook payload in cloudevents format for better interoperability": "Обернуть полезную навантаження вебхука у формат CloudEvents для лучшей совместимости", + "X-Custom-Header: value\nAuthorization: Bearer token": "X-Custom-Header: value\nAuthorization: Bearer token", + "X-Custom-Header: value\\nAuthorization: Bearer token": "X-Custom-Header: value\\nAuthorization: Bearer token", + "Yes": "Так", + "You": "Ви", + "You are not yet a member of any organisations.": "Ви ще не являетесь участником ни одній організації.", + "You can create new ConfigSets based on the _default template, or upload custom ones directly to your SOLR server.": "Ви можете створювати нові набори конфігурації на основе шаблону _default або загружать користувацькі непосредственно на сервер SOLR.", + "You can create new configsets based on the _default template, or upload custom ones directly to your SOLR server.": "Ви можете створювати нові набори конфігурації на основе шаблону _default або загружать користувацькі непосредственно на сервер SOLR.", + "You do not have access to this conversation": "У вас немає доступу до цієї переписке", + "You do not have permission to delete this conversation": "У вас немає разрешения на видалення цієї переписки", + "You do not have permission to modify this conversation": "У вас немає разрешения на зміна цієї переписки", + "You do not have permission to restore this conversation": "У вас немає разрешения на відновлення цієї переписки", + "You do not have permission to view the permission matrix. Admin access is required.": "У вас немає разрешения для перегляду матрицьі разрешений. Требуется доступ администратора.", + "You have selected {count} properties to add to the schema.": "Ви вибрали {count} властивостей для добавления у схему.", + "You must be logged in to favorite views": "Для добавления представлень у избранное необходимо войти у систему", + "You need an AI agent to start a conversation.": "Для початку переписки вам нужен ИИ-агент.", + "Your Fireworks AI API key. Get one at": "Ваш API-ключ Fireworks AI. Отримати по адресу", + "Your OpenAI API key. Get one at": "Ваш API-ключ OpenAI. Отримати по адресу", + "Your feedback has been recorded. Optionally, you can provide additional details here...": "Ваш відгук записан. При желании ви можете надати додаткові сведения тут...", + "Your feedback has been recorded. optionally, you can provide additional details here...": "Ваш відгук записан. При желании ви можете надати додаткові сведения тут...", + "Zookeeper Hosts": "Хости Zookeeper", + "Zookeeper Port": "Порт Zookeeper", + "Zookeeper Settings (SolrCloud)": "Налаштування Zookeeper (SolrCloud)", + "Zookeeper connection string for SolrCloud": "Рядок підключення Zookeeper для SolrCloud", + "Zookeeper port number (optional, defaults to 2181)": "Номер порта Zookeeper (необязательно, за замовчуванням 2181)", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n\\u2022 Active objects: {active}\n\\u2022 Soft-deleted objects: {deleted}\n\\u2022 Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ПРЕДУПРЕЖДЕНИЕ Про БЕЗВОЗВРАТНОМ УДАЛЕНИИ \\u26A0\\uFE0F\n\nВи собираетесь БЕЗПОВОРОТНО видалити ВСІ об'єкти схеми \"{schema}\":\n\n\\u2022 Активних об'єктів: {active}\n\\u2022 Програмно видалених об'єктів: {deleted}\n\\u2022 Итого: {total}\n\nЦі об'єкти будуть полностью видалено з бази даних і НЕ МОЖУТЬ бути відновлено.\n\nВи абсолютно впевнені?", + "\\u26A0\\uFE0F PERMANENT DELETION WARNING \\u26A0\\uFE0F\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "\\u26A0\\uFE0F ПРЕДУПРЕЖДЕНИЕ Про БЕЗВОЗВРАТНОМ УДАЛЕНИИ \\u26A0\\uFE0F\n\nВи собираетесь БЕЗПОВОРОТНО видалити {count} програмно видалених об'єктів схеми \"{schema}\".\n\nЦі об'єкти будуть полностью видалено з бази даних і НЕ МОЖУТЬ бути відновлено.\n\nВи абсолютно впевнені?", + "and accessible to all users in selected groups (or all users if no groups selected).": "і доступно всім пользователям у вибраних группах (або всім пользователям, якщо групи не вибрано).", + "and add the files there.": "і додайте файли туда.", + "approved": "одобрено", + "are only accessible to invited users.": "доступні тільки запрошеним пользователям.", + "by": "уід", + "chunk": "фрагмент", + "chunks": "фрагменти", + "chunks processed": "фрагментів обработано", + "completed": "завершено", + "configuration(s)": "конфігурація(ии)", + "conversationId is required": "conversationId обязателен", + "create": "створити", + "delete": "видалити", + "deleted objects section": "раздел видалених об'єктів", + "e.g., lib/Settings/config.json": "наприклад, lib/Settings/config.json", + "e.g., lib/Settings/register.json": "наприклад, lib/Settings/register.json", + "e.g., lib/settings/config.json": "наприклад, lib/settings/config.json", + "e.g., lib/settings/register.json": "наприклад, lib/settings/register.json", + "execute": "виконати", + "failed": "не вдалося", + "file": "файл", + "files": "файли", + "file{plural}": "файл(ов)", + "folder": "папка", + "fw_...": "fw_...", + "http://localhost:11434": "http://localhost:11434", + "https://api.fireworks.ai/inference/v1": "https://api.fireworks.ai/inference/v1", + "https://api.your-dolphin-instance.com": "https://api.your-dolphin-instance.com", + "https://example.com/webhook": "https://example.com/webhook", + "in {register}": "у {register}", + "in_progress": "у процессе", + "inherited": "унаследовано", + "list": "список", + "log{plural}": "журнал(ов)", + "manage": "управляти", + "members": "участники", + "menu or contact someone with permission to create agents.": "меню або обратитесь до кому-то з разрешением на створення агентів.", + "message content is required": "вміст сообщения обязательно", + "messages": "сообщения", + "missing": "отсутствует", + "myapp": "myapp", + "n8n URL and API key are required": "URL n8n і API-ключ обов'язкові", + "n8n configuration saved successfully": "Конфігурація n8n успішно збережена", + "n8n connection not configured": "Підключення n8n не настроено", + "n8n connection successful": "Підключення n8n успішно", + "n8n connection test failed: {message}": "Тест підключення n8n не удался: {message}", + "n8n connection test successful": "Тест підключення n8n успешен", + "n8n initialization failed: {message}": "Инициализация n8n не удалась: {message}", + "n8n integration disabled": "Інтеграція n8n отключена", + "n8n integration enabled": "Інтеграція n8n включена", + "n8n project initialized successfully": "Проект n8n успішно инициализирован", + "n8n settings saved successfully": "Налаштування n8n успішно збережено", + "notify_push configuration guide": "Руководство по настройке notify_push", + "notify_push is installed but OpenRegister has not yet confirmed a successful push. Trigger an object save to activate, or check your notify_push configuration.": "notify_push установлен, але OpenRegister ще не подтвердил успішний push. Активируйте збереження об'єкта або проверьте конфігурацію notify_push.", + "notify_push is installed but not yet active": "notify_push установлен, але ще не активний", + "objectType: object\naction: created": "objectType: object\naction: created", + "objectType: object\\naction: created": "objectType: object\\naction: created", + "objects": "об'єкти", + "objects processed": "об'єктів обработано", + "object{plural}": "об'єкт(ов)", + "or pick a range": "або виберіть діапазон", + "org-...": "org-...", + "pending": "ожидает", + "provides a good balance between speed and accuracy.": "обеспечивает хороший баланс між скоростью і точностью.", + "public": "публічний", + "read": "чтение", + "register(s)": "реєстр(ов)", + "register(s) selected": "реєстр(ов) вибрано", + "register{plural}": "реєстр(ов)", + "rejected": "отклонено", + "required": "обязательно", + "results": "результати", + "schema(s)": "схема(ем)", + "schema(s) selected": "схема(ем) вибрано", + "schema{plural}": "схема(ем)", + "selected objects": "вибрані об'єкти", + "set": "встановити", + "sk-...": "sk-...", + "this application": "це застосунок", + "to": "у", + "type must be \"positive\" or \"negative\"": "тип повинен бути «positive» або «negative»", + "update": "оновити", + "vectorized": "векторизовано", + "view": "представлення", + "views selected": "представлень вибрано", + "waiting": "очікування", + "{active} active, {deleted} deleted": "{active} активних, {deleted} видалених", + "{count} audit trails cleared successfully": "{count} записів журналу аудиту успішно очищено", + "{count} compliance issue(s) detected": "Обнаружено {count} проблема(ем) відповідності", + "{count} deletions": "{count} удалений", + "{count} documents found": "Найдено {count} документів", + "{count} email": "{count} письмо", + "{count} emails": "{count} писем", + "{count} entries": "{count} записів", + "{count} issues": "{count} проблем", + "{count} members": "{count} участников", + "{count} searches": "{count} поисков", + "{count} selected": "{count} вибрано", + "{count} widget(s)": "{count} віджет(ов)", + "{matched} object(s) matched. {erased} erased.": "{matched} об'єкт(ов) найдено. {erased} видалено.", + "{minutes} minutes ago": "{minutes} хвилин назад", + "{percentage}% of objects": "{percentage}% об'єктів", + "{property} - {other}": "{property} - {other}", + "{title} in {register} / {schema}": "{title} у {register} / {schema}", + "{type} cache cleared successfully": "Кеш {type} успішно очищено", + "ℹ️ Current Configuration": "ℹ️ Поточна конфігурація", + "ℹ️ current configuration": "ℹ️ поточна конфігурація", + "⚠️ DANGER: All {total} objects will be PERMANENTLY DELETED from the database. This action is UNRECOVERABLE and cannot be undone!": "⚠️ ОПАСНОСТЬ: Всі {total} об'єктів будуть БЕЗПОВОРОТНО ВИДАЛЕНО з бази даних. Це дія НЕОБРАТИМО і не може бути скасовано!", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete ALL objects for schema \"{schema}\":\n\n• Active objects: {active}\n• Soft-deleted objects: {deleted}\n• Total: {total}\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕЖДЕНИЕ Про БЕЗВОЗВРАТНОМ УДАЛЕНИИ ⚠️\n\nВи собираетесь БЕЗПОВОРОТНО видалити ВСІ об'єкти схеми \"{schema}\":\n\n• Активних об'єктів: {active}\n• Програмно видалених об'єктів: {deleted}\n• Итого: {total}\n\nЦі об'єкти будуть полностью видалено з бази даних і НЕ МОЖУТЬ бути відновлено.\n\nВи абсолютно впевнені?", + "⚠️ PERMANENT DELETION WARNING ⚠️\n\nYou are about to PERMANENTLY delete {count} soft-deleted objects for schema \"{schema}\".\n\nThese objects will be completely removed from the database and CANNOT be recovered.\n\nAre you absolutely sure?": "⚠️ ПРЕДУПРЕЖДЕНИЕ Про БЕЗВОЗВРАТНОМ УДАЛЕНИИ ⚠️\n\nВи собираетесь БЕЗПОВОРОТНО видалити {count} програмно видалених об'єктів схеми \"{schema}\".\n\nЦі об'єкти будуть полностью видалено з бази даних і НЕ МОЖУТЬ бути відновлено.\n\nВи абсолютно впевнені?", + "⚡ Batch Processing": "⚡ Пакетная обробка", + "⚡ SOLR can handle millions of documents with sub-second search response times...": "⚡ SOLR може обробляти мільйони документів зі временем відповіді пошуку менее секунди...", + "✓ Comprehensive context ✓ Less likely to miss details ✗ Slower responses ✗ May include less relevant information": "✓ Повний контекст ✓ Меньше шансов пропустить детали ✗ Повільні відповіді ✗ Може включать менее релевантную інформацію", + "✓ Faster responses ✓ More focused answers ✗ May miss relevant information": "✓ Швидкі відповіді ✓ Более зосереджені відповіді ✗ Може упустить релевантную інформацію", + "✨ AI Features": "✨ Функції ИИ", + "🌐 In distributed mode, SOLR uses ZooKeeper for cluster coordination...": "🌐 У розподіленому режиме SOLR використовує ZooKeeper для координации кластера...", + "🎯 Faceted search allows users to drill down into results by categories...": "🎯 Фасетний пошук позволяет пользователям углубляться у результати по категориям...", + "💡 Did you know? SOLR powers search for Netflix, Apple, and many other major sites!": "💡 Знали ли ви? SOLR обеспечивает пошук для Netflix, Apple і многих других великих сайтов!", + "💰 View Selection (Cost Optimization)": "💰 Вибір представлень (оптимизация витрат)", + "📄 Object Serialization": "📄 Сериализация об'єктів", + "📊 SOLR provides rich analytics and statistics about search performance...": "📊 SOLR предоставляет богатую аналитику і статистику продуктивності пошуку...", + "📦 ConfigSets contain the schema and configuration files for your search index...": "📦 Набори конфігурації містять схему і файли конфігурації для вашего поискового індексу...", + "🔄 Replication ensures your search index is available even if nodes fail...": "🔄 Репликация гарантирует доступность поискового індексу даже при отказе вузлів...", + "🔍 SOLR is a powerful enterprise search platform built on Apache Lucene...": "🔍 SOLR — мощная корпоративная пошукова платформа, построенная на Apache Lucene...", + "🔢 Vectorization Settings": "🔢 Налаштування векторизації", + "🚀 Auto-scaling can dynamically add or remove nodes based on load...": "🚀 Автомасштабирование може динамически добавлять або удалять вузли у залежності уід навантаження...", + "🛡️ Security features include authentication, authorization, and SSL encryption...": "🛡️ Функції безопасности включают автентифікацію, авторизацию і шифрование SSL...", + "File content is required": "File content is required", + "File name is required": "File name is required", + "File name is required (use \"name\" or \"filename\")": "File name is required (use \"name\" or \"filename\")", + "File not available for anonymous access": "File not available for anonymous access", + "Files folder not found": "Files folder not found", + "Forbidden": "Forbidden", + "Preview not available for unpublished files": "Preview not available for unpublished files", + "Browser Web Push (VAPID)": "Browser Web Push (VAPID)", + "Browser web push is configured": "Browser web push is configured", + "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.": "A VAPID keypair is configured. Users can opt in to browser notifications from their personal settings, which are delivered even when the browser tab is closed.", + "VAPID public key": "VAPID public key", + "Browser web push is not configured": "Browser web push is not configured", + "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:": "No VAPID keypair is configured yet. Generate one with the following occ command to enable browser notifications:", + "Checking browser web push configuration …": "Checking browser web push configuration …", + "Browser notifications": "Browser notifications", + "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.": "Receive OpenRegister notifications as native browser notifications, even when the tab is closed.", + "This browser does not support web push notifications.": "This browser does not support web push notifications.", + "Enable browser notifications": "Enable browser notifications", + "Notification permission: granted": "Notification permission: granted", + "Notification permission: denied. Re-enable notifications for this site in your browser settings.": "Notification permission: denied. Re-enable notifications for this site in your browser settings.", + "Notification permission: not yet requested. Enabling the toggle will ask for permission.": "Notification permission: not yet requested. Enabling the toggle will ask for permission.", + "Could not change browser notification settings.": "Could not change browser notification settings.", + "Source object not found": "Source object not found", + "Target object ID is required": "Target object ID is required", + "Target object not found": "Target object not found", + "You do not have access to this object": "You do not have access to this object", + "Add a connection": "Add a connection", + "Add another connection": "Add another connection", + "Connect": "Connect", + "Connect this email to a case, lead, invoice and more.": "Connect this email to a case, lead, invoice and more.", + "Connect to {name}": "Connect to {name}", + "Connected to {name}": "Connected to {name}", + "Connecting…": "Connecting…", + "Connection removed": "Connection removed", + "Connections": "Connections", + "Could not read this email": "Could not read this email", + "Create & connect": "Create & connect", + "Creating {name}…": "Creating {name}…", + "Failed to add attachment": "Failed to add attachment", + "Failed to connect": "Failed to connect", + "Failed to create {name} from email": "Failed to create {name} from email", + "Failed to remove connection": "Failed to remove connection", + "Links": "Links", + "Loading connections...": "Loading connections...", + "Loading…": "Loading…", + "New {name}": "New {name}", + "New {name} from this email": "New {name} from this email", + "No connections yet": "No connections yet", + "object": "object", + "Open connected item": "Open connected item", + "Open connections sidebar": "Open connections sidebar", + "People": "People", + "Remove connection": "Remove connection", + "Remove connection to {name}": "Remove connection to {name}", + "Remove the connection between this email and {name}?": "Remove the connection between this email and {name}?", + "Review the details below. The new {name} will be connected to this email.": "Review the details below. The new {name} will be connected to this email.", + "Select an email to see its connections": "Select an email to see its connections", + "There is nothing set up to connect this email to yet.": "There is nothing set up to connect this email to yet.", + "This cannot be undone.": "This cannot be undone.", + "{count} locations": "{count} locations", + "{pct}% success rate": "{pct}% success rate", + "across {count} registers": "across {count} registers", + "Active method: {active}": "Active method: {active}", + "Allow private/loopback targets": "Allow private/loopback targets", + "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.": "AppAPI is not installed. Install AppAPI and the OpenAnonymiser ExApp to enable Dutch-focused PII detection.", + "Available": "Available", + "Base map layer": "Base map layer", + "Browser notification client is still loading — please try again in a moment.": "Browser notification client is still loading — please try again in a moment.", + "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.": "Calls the installed OpenAnonymiser ExApp directly through AppAPI — no endpoint needed.", + "Change date range": "Change date range", + "Connection failed: {error}": "Connection failed: {error}", + "Connection test failed": "Connection test failed", + "Detection backends": "Detection backends", + "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.": "Disable the SSRF guard for this webhook so it can deliver to private, loopback or link-local addresses (e.g. http://localhost:8000). Only enable this for local testing.", + "Effective: {effective}": "Effective: {effective}", + "falling back": "falling back", + "Install OpenAnonymiser": "Install OpenAnonymiser", + "Map with {count} object locations": "Map with {count} object locations", + "No objects with a location to display": "No objects with a location to display", + "Not configured": "Not configured", + "OpenAnonymiser API endpoint": "OpenAnonymiser API endpoint", + "OpenAnonymiser source": "OpenAnonymiser source", + "Reachable": "Reachable", + "Recommended": "Recommended", + "Search recording mode": "Search recording mode", + "Searches": "Searches", + "Test connection": "Test connection", + "Test connection for {backend}": "Test connection for {backend}", + "Text searches only": "Text searches only", + "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.": "The OpenAnonymiser ExApp is installed but disabled. Enable it to use it for entity recognition.", + "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.": "The OpenAnonymiser ExApp is not installed. Install it to enable Dutch-focused PII detection.", + "Unavailable": "Unavailable", + "Unreachable": "Unreachable", + "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)": "URL to an external OpenAnonymiser instance (Dutch-focused PII detection)", + "Use an external OpenAnonymiser endpoint": "Use an external OpenAnonymiser endpoint", + "Use the built-in OpenAnonymiser (recommended)": "Use the built-in OpenAnonymiser (recommended)", + "View objects": "View objects", + "Attribute": "Attribute", + "Average score": "Average score", + "Bucket": "Bucket", + "Choose a register and schema above to see its duplicate candidates.": "Choose a register and schema above to see its duplicate candidates.", + "Choose a register and schema above to see its quality statistics.": "Choose a register and schema above to see its quality statistics.", + "Choose a survivorship-enabled register and schema above to list its master entities.": "Choose a survivorship-enabled register and schema above to list its master entities.", + "Confidence": "Confidence", + "Configure a webhook to see its delivery health here.": "Configure a webhook to see its delivery health here.", + "Data Quality": "Data Quality", + "Delivered": "Delivered", + "Duplicate Candidates": "Duplicate Candidates", + "Fair": "Fair", + "Golden record": "Golden record", + "Good": "Good", + "Id": "Id", + "Lowest-quality objects": "Lowest-quality objects", + "Master entities": "Master entities", + "Matched on": "Matched on", + "No candidate pairs were detected for this register and schema.": "No candidate pairs were detected for this register and schema.", + "No duplicate candidates found": "No duplicate candidates found", + "No golden-record provenance": "No golden-record provenance", + "No master entities found": "No master entities found", + "No master entity selected": "No master entity selected", + "No objects to show": "No objects to show", + "No recent failures": "No recent failures", + "No scored objects for this schema": "No scored objects for this schema", + "No webhooks configured": "No webhooks configured", + "Object A": "Object A", + "Object B": "Object B", + "Pending retries": "Pending retries", + "Per-webhook health": "Per-webhook health", + "Poor": "Poor", + "Quality score": "Quality score", + "Quality score (best first)": "Quality score (best first)", + "Quality score (worst first)": "Quality score (worst first)", + "Quality status": "Quality status", + "Queue / sync health": "Queue / sync health", + "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.": "Read-only view of candidate duplicate pairs detected for a register and schema. Merge execution is out of scope here.", + "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.": "Read-only webhook delivery telemetry: per-webhook delivered / failed / pending-retry counts and recent failures.", + "Recent failures": "Recent failures", + "Review score distribution and the lowest-quality objects for a register and schema.": "Review score distribution and the lowest-quality objects for a register and schema.", + "Score": "Score", + "Score histogram": "Score histogram", + "Select a register and schema": "Select a register and schema", + "Showing {from}-{to} of {total}": "Showing {from}-{to} of {total}", + "Sort by": "Sort by", + "Status code": "Status code", + "Survivorship-enabled objects for a register and schema, with their materialised quality columns.": "Survivorship-enabled objects for a register and schema, with their materialised quality columns.", + "This object has no materialised attribute-provenance map.": "This object has no materialised attribute-provenance map.", + "This schema has no objects with a materialised quality score yet.": "This schema has no objects with a materialised quality score yet.", + "This schema has no objects yet.": "This schema has no objects yet.", + "View golden record": "View golden record", + "Winning source": "Winning source", + "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.": "Audit trail of recent reversible merges. Reverse an operation while it is still within its reversal window.", + "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.": "Candidate duplicate pairs detected for a register and schema. Launch the merge wizard on a pair to review and execute a reversible merge.", + "Choose the authoritative value for each conflicting attribute, then save.": "Choose the authoritative value for each conflicting attribute, then save.", + "Confirm merge": "Confirm merge", + "Confirmed duplicate": "Confirmed duplicate", + "Data stewardship review": "Data stewardship review", + "Every attribute across the linked sources already agrees, or has only one source.": "Every attribute across the linked sources already agrees, or has only one source.", + "Manual bulk cleanup": "Manual bulk cleanup", + "Manual override ({actor})": "Manual override ({actor})", + "Merge": "Merge", + "Merge objects": "Merge objects", + "Merge Operations": "Merge Operations", + "Merge reason": "Merge reason", + "Merged at": "Merged at", + "Merged from": "Merged from", + "Merges executed from the Duplicate Candidates view will show up here.": "Merges executed from the Duplicate Candidates view will show up here.", + "Migration cleanup": "Migration cleanup", + "N/A": "N/A", + "No conflicts to resolve": "No conflicts to resolve", + "No merge operations found": "No merge operations found", + "One-off (this record only)": "One-off (this record only)", + "Persistent (trust rule)": "Persistent (trust rule)", + "Post-merge golden record": "Post-merge golden record", + "Rationale (optional)": "Rationale (optional)", + "Resolve conflicts": "Resolve conflicts", + "Reverse": "Reverse", + "Reversed / final": "Reversed / final", + "Reversible": "Reversible", + "Select a reason": "Select a reason", + "Survivor": "Survivor", + "The conflict resolution could not be saved.": "The conflict resolution could not be saved.", + "The merge could not be completed.": "The merge could not be completed.", + "The merge preview could not be generated.": "The merge preview could not be generated.", + "This is a preview only — no object or merge operation is written until you confirm.": "This is a preview only — no object or merge operation is written until you confirm.", + "This merge can be reversed until {date}.": "This merge can be reversed until {date}.", + "unknown": "unknown", + "Why is this the authoritative value?": "Why is this the authoritative value?", + "Winning source for {attribute}": "Winning source for {attribute}", + "Winning value": "Winning value", + "{days} day(s) left": "{days} day(s) left", + "{days} day(s) remaining": "{days} day(s) remaining", + "All handlers": "All handlers", + "All statuses": "All statuses", + "Apply redaction": "Apply redaction", + "Back to cases": "Back to cases", + "Cases": "Cases", + "Collect evidence": "Collect evidence", + "Compose denial": "Compose denial", + "Data-subject request": "Data-subject request", + "Days remaining": "Days remaining", + "Deadline": "Deadline", + "Deadline tracking": "Deadline tracking", + "Denial": "Denial", + "Denial ground": "Denial ground", + "Denial letter template": "Denial letter template", + "Draft denial": "Draft denial", + "Effective deadline": "Effective deadline", + "Escalate to regulator": "Escalate to regulator", + "Escalation": "Escalation", + "Escalation tier": "Escalation tier", + "Evidence": "Evidence", + "Export & escalation": "Export & escalation", + "Field to redact *": "Field to redact *", + "Filter by handler": "Filter by handler", + "Filter by status": "Filter by status", + "Finalising a denial is blocked until a regulator reference is recorded on the case.": "Finalising a denial is blocked until a regulator reference is recorded on the case.", + "Generate export bundle": "Generate export bundle", + "Ground": "Ground", + "Handler": "Handler", + "Identity verification: {status}": "Identity verification: {status}", + "Lifecycle": "Lifecycle", + "No cases": "No cases", + "No cases match the current filters. Clear the filters to see the full authorised set.": "No cases match the current filters. Clear the filters to see the full authorised set.", + "No data-subject-request cases are tracked for you yet.": "No data-subject-request cases are tracked for you yet.", + "No evidence collected yet.": "No evidence collected yet.", + "No redactions applied.": "No redactions applied.", + "No transitions are available from the current state.": "No transitions are available from the current state.", + "Notes": "Notes", + "Open": "Open", + "Overdue": "Overdue", + "Overdue by {days} day(s)": "Overdue by {days} day(s)", + "Overdue only": "Overdue only", + "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.": "Record a field-level redaction on this case. The redaction and its ground are logged in the case audit trail.", + "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.": "Record a proposed denial ground for this request. Drafting does not require a regulator reference; finalising the denial does.", + "Record a regulator reference": "Record a regulator reference", + "Recorded ground": "Recorded ground", + "Redaction ground": "Redaction ground", + "Redactions": "Redactions", + "Regulator escalation: {status}": "Regulator escalation: {status}", + "Regulator reference": "Regulator reference", + "Replacement value (leave empty to blank the field)": "Replacement value (leave empty to blank the field)", + "Save reference": "Save reference", + "Showing {shown} of {total} case(s)": "Showing {shown} of {total} case(s)", + "Statutory citation": "Statutory citation", + "Subject": "Subject", + "The active policy pack supplies no denial-letter template reference for this jurisdiction.": "The active policy pack supplies no denial-letter template reference for this jurisdiction.", + "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.": "The letter body is rendered from this template reference (a document leaf); it is not stored in this form.", + "Unassigned": "Unassigned", + "Verify identity": "Verify identity", + "Allow writes (create, update and delete push through to the external database)": "Allow writes (create, update and delete push through to the external database)", + "Chunks vectorized": "Chunks vectorized", + "Create virtual register": "Create virtual register", + "Credential (UUID from the credential store)": "Credential (UUID from the credential store)", + "Database file path *": "Database file path *", + "Database name *": "Database name *", + "Driver": "Driver", + "HNSW index": "HNSW index", + "Host *": "Host *", + "Hybrid search readiness": "Hybrid search readiness", + "Keyword GIN index": "Keyword GIN index", + "pgvector ANN sidecar": "pgvector ANN sidecar", + "Templates are coming soon": "Templates are coming soon", + "The action failed": "The action failed", + "The database password is never stored on the source; reference a credential from the credential store instead.": "The database password is never stored on the source; reference a credential from the credential store instead.", + "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.": "The templates feature is not available yet. This page is a placeholder and will list templates once the feature ships.", + "Vectors on pgvector fast path": "Vectors on pgvector fast path", + "Virtual register created: {created} new and {updated} updated schemas": "Virtual register created: {created} new and {updated} updated schemas", + "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas.": "Warning: writes are executed directly on the external database. Use a database user with least-privilege grants. Re-run \"Create virtual register\" to unlock existing schemas." + }, + "plurals": { + "{count} email": [ + "{count} лист", + "{count} листи", + "{count} листів" + ], + "Purge {count} object from database": [ + "Очистити {count} об'єкт з бази даних", + "Очистити {count} об'єкта з бази даних", + "Очистити {count} об'єктів з бази даних" + ], + "Restore {count} object": [ + "Відновити {count} об'єкт", + "Відновити {count} об'єкта", + "Відновити {count} об'єктів" + ], + "Successfully restored {count} object": [ + "Успішно відновлено {count} об'єкт", + "Успішно відновлено {count} об'єкта", + "Успішно відновлено {count} об'єктів" + ], + "Delete {count} object": [ + "Видалити {count} об'єкт", + "Видалити {count} об'єкта", + "Видалити {count} об'єктів" + ], + "Object successfully deleted": [ + "Об'єкт успішно видалено", + "Об'єкта успішно видалено", + "Об'єктів успішно видалено" + ] + } +} diff --git a/lib/Activity/Filter.php b/lib/Activity/Filter.php index 0ee8423c1f..69b14a7274 100644 --- a/lib/Activity/Filter.php +++ b/lib/Activity/Filter.php @@ -40,7 +40,7 @@ class Filter implements IFilter * @param IL10N $l The localization service. * @param IURLGenerator $urlGenerator The URL generator. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private IL10N $l, @@ -53,7 +53,7 @@ public function __construct( * * @return string The filter identifier. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getIdentifier(): string { @@ -65,7 +65,7 @@ public function getIdentifier(): string * * @return string The filter name. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getName(): string { @@ -77,7 +77,7 @@ public function getName(): string * * @return int The filter priority. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getPriority(): int { @@ -89,7 +89,7 @@ public function getPriority(): int * * @return string The icon URL. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getIcon(): string { @@ -107,7 +107,7 @@ public function getIcon(): string * * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $types required by IFilter interface * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function filterTypes(array $types): array { @@ -119,7 +119,7 @@ public function filterTypes(array $types): array * * @return array The allowed app IDs. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function allowedApps(): array { diff --git a/lib/Activity/Provider.php b/lib/Activity/Provider.php index d6c4942e18..0c2596322b 100644 --- a/lib/Activity/Provider.php +++ b/lib/Activity/Provider.php @@ -60,7 +60,7 @@ class Provider implements IProvider * @param IURLGenerator $urlGenerator The URL generator. * @param ProviderSubjectHandler $subjectHandler The subject handler. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private IFactory $l10nFactory, @@ -82,7 +82,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $previousEvent required by IProvider interface * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function parse($language, IEvent $event, ?IEvent $previousEvent=null): IEvent { diff --git a/lib/Activity/ProviderSubjectHandler.php b/lib/Activity/ProviderSubjectHandler.php index 3d92e15df7..df235154c0 100644 --- a/lib/Activity/ProviderSubjectHandler.php +++ b/lib/Activity/ProviderSubjectHandler.php @@ -57,7 +57,7 @@ class ProviderSubjectHandler * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function applySubjectText(IEvent $event, object $l, array $params): void { @@ -89,7 +89,7 @@ public function applySubjectText(IEvent $event, object $l, array $params): void * * @return array The rich parameters. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function buildRichParams(IEvent $event, string $title): array { @@ -114,7 +114,7 @@ private function buildRichParams(IEvent $event, string $title): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function applySimpleSubject( IEvent $event, diff --git a/lib/Activity/Setting/ObjectSetting.php b/lib/Activity/Setting/ObjectSetting.php index c03ad550ee..9a0df6a6a1 100644 --- a/lib/Activity/Setting/ObjectSetting.php +++ b/lib/Activity/Setting/ObjectSetting.php @@ -37,7 +37,7 @@ class ObjectSetting extends ActivitySettings * * @param IL10N $l The localization service. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private IL10N $l, @@ -49,7 +49,7 @@ public function __construct( * * @return string The setting identifier. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getIdentifier(): string { @@ -61,7 +61,7 @@ public function getIdentifier(): string * * @return string The setting name. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getName(): string { @@ -73,7 +73,7 @@ public function getName(): string * * @return string The group identifier. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getGroupIdentifier(): string { @@ -85,7 +85,7 @@ public function getGroupIdentifier(): string * * @return string The group name. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getGroupName(): string { @@ -97,7 +97,7 @@ public function getGroupName(): string * * @return int The priority. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getPriority(): int { @@ -109,7 +109,7 @@ public function getPriority(): int * * @return bool True if changeable. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function canChangeStream(): bool { @@ -121,7 +121,7 @@ public function canChangeStream(): bool * * @return bool True if enabled by default. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function isDefaultEnabledStream(): bool { @@ -133,7 +133,7 @@ public function isDefaultEnabledStream(): bool * * @return bool True if changeable. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function canChangeMail(): bool { @@ -145,7 +145,7 @@ public function canChangeMail(): bool * * @return bool True if enabled by default. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function isDefaultEnabledMail(): bool { diff --git a/lib/Activity/Setting/SchemaSetting.php b/lib/Activity/Setting/SchemaSetting.php index 80df26a64c..ec3a74dc17 100644 --- a/lib/Activity/Setting/SchemaSetting.php +++ b/lib/Activity/Setting/SchemaSetting.php @@ -37,7 +37,7 @@ class SchemaSetting extends ActivitySettings * * @param IL10N $l The localization service. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private IL10N $l, @@ -49,7 +49,7 @@ public function __construct( * * @return string The setting identifier. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getIdentifier(): string { @@ -61,7 +61,7 @@ public function getIdentifier(): string * * @return string The setting name. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getName(): string { @@ -73,7 +73,7 @@ public function getName(): string * * @return string The group identifier. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getGroupIdentifier(): string { @@ -85,7 +85,7 @@ public function getGroupIdentifier(): string * * @return string The group name. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getGroupName(): string { @@ -97,7 +97,7 @@ public function getGroupName(): string * * @return int The priority. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function getPriority(): int { @@ -109,7 +109,7 @@ public function getPriority(): int * * @return bool True if changeable. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function canChangeStream(): bool { @@ -121,7 +121,7 @@ public function canChangeStream(): bool * * @return bool True if enabled by default. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function isDefaultEnabledStream(): bool { @@ -133,7 +133,7 @@ public function isDefaultEnabledStream(): bool * * @return bool True if changeable. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function canChangeMail(): bool { @@ -145,7 +145,7 @@ public function canChangeMail(): bool * * @return bool True if enabled by default. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-2 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function isDefaultEnabledMail(): bool { diff --git a/lib/AppHost/Bootstrap.php b/lib/AppHost/Bootstrap.php new file mode 100644 index 0000000000..703815fa0e --- /dev/null +++ b/lib/AppHost/Bootstrap.php @@ -0,0 +1,437 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost; + +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use Psr\Container\ContainerInterface; + +/** + * Declarative one-call bootstrap for AppHost leaf apps. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-3.1 + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: One-Call Bootstrap + */ +class Bootstrap +{ + /** + * Fully-qualified generic class names, kept as plain strings so they are + * never autoloaded by referencing this map (only when a closure resolves + * one through the container). + */ + private const GENERIC_DASHBOARD_CONTROLLER = 'OCA\\OpenRegister\\AppHost\\Controller\\GenericDashboardController'; + private const GENERIC_PREFERENCES_CONTROLLER = 'OCA\\OpenRegister\\AppHost\\Controller\\GenericPreferencesController'; + private const GENERIC_SETTINGS_CONTROLLER = 'OCA\\OpenRegister\\AppHost\\Controller\\GenericSettingsController'; + private const GENERIC_HEALTH_CONTROLLER = 'OCA\\OpenRegister\\AppHost\\Controller\\GenericHealthController'; + private const GENERIC_METRICS_CONTROLLER = 'OCA\\OpenRegister\\AppHost\\Controller\\GenericMetricsController'; + private const GENERIC_SETTINGS_SERVICE = 'OCA\\OpenRegister\\AppHost\\Service\\AppHostSettingsService'; + private const GENERIC_ACTION_AUTH_SERVICE = 'OCA\\OpenRegister\\AppHost\\Service\\GenericActionAuthService'; + private const GENERIC_INITIALIZE_SETTINGS = 'OCA\\OpenRegister\\AppHost\\Repair\\GenericInitializeSettings'; + private const GENERIC_INITIALIZE_ACTIONS = 'OCA\\OpenRegister\\AppHost\\Repair\\GenericInitializeActions'; + private const GENERIC_ADMIN_SETTINGS = 'OCA\\OpenRegister\\AppHost\\Settings\\GenericAdminSettings'; + private const GENERIC_SETTINGS_SECTION = 'OCA\\OpenRegister\\AppHost\\Settings\\GenericSettingsSection'; + private const GENERIC_DEEPLINK_LISTENER = 'OCA\\OpenRegister\\AppHost\\Listener\\GenericDeepLinkRegistrationListener'; + + private const OBSERVABILITY_MANIFEST_LOADER = 'OCA\\OpenRegister\\AppHost\\Observability\\ManifestLoader'; + private const OBSERVABILITY_EXECUTOR = 'OCA\\OpenRegister\\AppHost\\Observability\\HealthCheckExecutor'; + private const OBSERVABILITY_METRICS_ENGINE = 'OCA\\OpenRegister\\AppHost\\Observability\\MetricsEngine'; + + private const DEEPLINK_EVENT = 'OCA\\OpenRegister\\Event\\DeepLinkRegistrationEvent'; + + /** + * Register all standard AppHost plumbing for a leaf app. + * + * @param IRegistrationContext $context The leaf app's registration context. + * @param string $appId The leaf app id (e.g. 'petstore'). + * @param array $options Optional overrides: + * - 'namespace' (string): the leaf app's `OCA\X` base namespace + * (e.g. `OCA\PetStore`). STRONGLY RECOMMENDED — the fleet's + * namespaces are not derivable from the app id (petstore → + * PetStore, opencatalogi → OpenCatalogi, decidesk → Decidesk), + * so pass it explicitly. When omitted, a StudlyCase guess from + * the app id is used as a last-resort fallback. + * - 'controllerNamespace' (string): override just the controller namespace. + * - 'repairNamespace' (string): override just the repair namespace. + * - 'settingsNamespace' (string): override just the settings namespace. + * - 'sectionsNamespace' (string): override just the sections namespace. + * - 'listenerNamespace' (string): override just the listener namespace. + * - 'serviceNamespace' (string): override just the service namespace. + * - 'sectionId' (string): admin section id, default `$appId`. + * - 'sectionName' (string): admin section display name, default StudlyAppId. + * - 'sectionIcon' (string): icon file, default 'app-dark.svg'. + * - 'sectionPriority' (int): default 75. + * - 'adminPriority' (int): default 10. + * - 'dashboardWidgets' (string[]): widget classes to register. + * - 'mcpProvider' (string): MCP tool provider class to alias. + * - 'deepLinks' (bool): register the deep-link listener, default true. + * - 'observability' (bool): alias health/metrics controllers, default true. + * + * @return void + * + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: One-Call Bootstrap + */ + public static function register(IRegistrationContext $context, string $appId, array $options=[]): void + { + $studly = self::studly(appId: $appId); + $base = rtrim(string: (string) ($options['namespace'] ?? ('OCA\\'.$studly)), characters: '\\'); + $controllerNs = (string) ($options['controllerNamespace'] ?? ($base.'\\Controller')); + $repairNs = (string) ($options['repairNamespace'] ?? ($base.'\\Repair')); + $settingsNs = (string) ($options['settingsNamespace'] ?? ($base.'\\Settings')); + $sectionsNs = (string) ($options['sectionsNamespace'] ?? ($base.'\\Sections')); + $listenerNs = (string) ($options['listenerNamespace'] ?? ($base.'\\Listener')); + $serviceNs = (string) ($options['serviceNamespace'] ?? ($base.'\\Service')); + + $sectionId = (string) ($options['sectionId'] ?? $appId); + $sectionName = (string) ($options['sectionName'] ?? $studly); + $sectionIcon = (string) ($options['sectionIcon'] ?? 'app-dark.svg'); + $sectionPriority = (int) ($options['sectionPriority'] ?? 75); + $adminPriority = (int) ($options['adminPriority'] ?? 10); + $registerDeepLinks = ($options['deepLinks'] ?? true) !== false; + $registerObserv = ($options['observability'] ?? true) !== false; + + self::registerControllers(context: $context, appId: $appId, controllerNs: $controllerNs, observability: $registerObserv); + self::registerServices(context: $context, appId: $appId, serviceNs: $serviceNs); + self::registerRepairSteps(context: $context, appId: $appId, repairNs: $repairNs); + self::registerAdminSettings( + context: $context, + appId: $appId, + settingsNs: $settingsNs, + sectionsNs: $sectionsNs, + sectionId: $sectionId, + sectionName: $sectionName, + sectionIcon: $sectionIcon, + sectionPriority: $sectionPriority, + adminPriority: $adminPriority + ); + + if ($registerDeepLinks === true) { + self::registerDeepLinkListener(context: $context, appId: $appId, listenerNs: $listenerNs); + } + + foreach ((array) ($options['dashboardWidgets'] ?? []) as $widgetClass) { + $context->registerDashboardWidget((string) $widgetClass); + } + + if (isset($options['mcpProvider']) === true) { + $context->registerServiceAlias( + 'OCA\\OpenRegister\\Mcp\\IMcpToolProvider::'.$appId, + (string) $options['mcpProvider'] + ); + } + }//end register() + + /** + * Alias the leaf controller class names to the generic controllers, with + * the leaf appId injected as the controllers' `$appName`. + * + * @param IRegistrationContext $context Leaf registration context. + * @param string $appId Leaf app id. + * @param string $controllerNs Leaf controller namespace. + * @param bool $observability Whether to alias health/metrics. + * + * @return void + */ + private static function registerControllers(IRegistrationContext $context, string $appId, string $controllerNs, bool $observability): void + { + $context->registerService( + $controllerNs.'\\DashboardController', + static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_DASHBOARD_CONTROLLER; + return new $class( + appName: $appId, + request: $c->get('OCP\\IRequest') + ); + } + ); + + $context->registerService( + $controllerNs.'\\PreferencesController', + static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_PREFERENCES_CONTROLLER; + return new $class( + appName: $appId, + request: $c->get('OCP\\IRequest'), + config: $c->get('OCP\\IConfig'), + userSession: $c->get('OCP\\IUserSession') + ); + } + ); + + $context->registerService( + $controllerNs.'\\SettingsController', + static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_SETTINGS_CONTROLLER; + return new $class( + appName: $appId, + request: $c->get('OCP\\IRequest'), + settingsService: $c->get(self::GENERIC_SETTINGS_SERVICE) + ); + } + ); + + if ($observability === false) { + return; + } + + $context->registerService( + $controllerNs.'\\HealthController', + static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_HEALTH_CONTROLLER; + return new $class( + appName: $appId, + request: $c->get('OCP\\IRequest'), + manifestLoader: $c->get(self::OBSERVABILITY_MANIFEST_LOADER), + executor: $c->get(self::OBSERVABILITY_EXECUTOR) + ); + } + ); + + $context->registerService( + $controllerNs.'\\MetricsController', + static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_METRICS_CONTROLLER; + return new $class( + appName: $appId, + request: $c->get('OCP\\IRequest'), + manifestLoader: $c->get(self::OBSERVABILITY_MANIFEST_LOADER), + engine: $c->get(self::OBSERVABILITY_METRICS_ENGINE) + ); + } + ); + }//end registerControllers() + + /** + * Register the app-scoped settings + action-auth services under both the + * generic class name (so the controllers resolve them) and the leaf's + * conventional service class names (so leaf code keeps working). + * + * @param IRegistrationContext $context Leaf registration context. + * @param string $appId Leaf app id. + * @param string $serviceNs Leaf service namespace. + * + * @return void + */ + private static function registerServices(IRegistrationContext $context, string $appId, string $serviceNs): void + { + $settingsFactory = static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_SETTINGS_SERVICE; + return new $class( + appId: $appId, + appConfig: $c->get('OCP\\IAppConfig'), + appManager: $c->get('OCP\\App\\IAppManager'), + container: $c, + groupManager: $c->get('OCP\\IGroupManager'), + userSession: $c->get('OCP\\IUserSession'), + logger: $c->get('Psr\\Log\\LoggerInterface') + ); + }; + $context->registerService(self::GENERIC_SETTINGS_SERVICE, $settingsFactory); + $context->registerService($serviceNs.'\\SettingsService', $settingsFactory); + + $actionAuthFactory = static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_ACTION_AUTH_SERVICE; + return new $class( + appId: $appId, + appConfig: $c->get('OCP\\IAppConfig'), + groupManager: $c->get('OCP\\IGroupManager') + ); + }; + $context->registerService(self::GENERIC_ACTION_AUTH_SERVICE, $actionAuthFactory); + $context->registerService($serviceNs.'\\ActionAuthService', $actionAuthFactory); + }//end registerServices() + + /** + * Bind the leaf repair-step class names (referenced by info.xml) to the + * generic repair steps with the appId + app-scoped services injected. + * + * @param IRegistrationContext $context Leaf registration context. + * @param string $appId Leaf app id. + * @param string $repairNs Leaf repair namespace. + * + * @return void + */ + private static function registerRepairSteps(IRegistrationContext $context, string $appId, string $repairNs): void + { + $context->registerService( + $repairNs.'\\InitializeSettings', + static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_INITIALIZE_SETTINGS; + return new $class( + appId: $appId, + settingsService: $c->get(self::GENERIC_SETTINGS_SERVICE), + logger: $c->get('Psr\\Log\\LoggerInterface'), + appManager: $c->get('OCP\\App\\IAppManager'), + tokenService: $c->get('OCA\\OpenRegister\\Service\\Credential\\CredentialAppTokenService'), + applicationRegistrar: $c->get('OCA\\OpenRegister\\Service\\Credential\\DoriathApplicationRegistrar') + ); + } + ); + + $context->registerService( + $repairNs.'\\InitializeActions', + static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_INITIALIZE_ACTIONS; + return new $class( + appId: $appId, + actionAuth: $c->get(self::GENERIC_ACTION_AUTH_SERVICE), + appManager: $c->get('OCP\\App\\IAppManager'), + logger: $c->get('Psr\\Log\\LoggerInterface') + ); + } + ); + }//end registerRepairSteps() + + /** + * Bind the leaf admin-settings + section class names (referenced by + * info.xml) to the generic admin settings/section with the leaf metadata. + * + * @param IRegistrationContext $context Leaf registration context. + * @param string $appId Leaf app id. + * @param string $settingsNs Leaf settings namespace. + * @param string $sectionsNs Leaf sections namespace. + * @param string $sectionId Section id. + * @param string $sectionName Section display name. + * @param string $sectionIcon Section icon file. + * @param int $sectionPriority Section priority. + * @param int $adminPriority Admin form priority. + * + * @return void + */ + private static function registerAdminSettings( + IRegistrationContext $context, + string $appId, + string $settingsNs, + string $sectionsNs, + string $sectionId, + string $sectionName, + string $sectionIcon, + int $sectionPriority, + int $adminPriority + ): void { + $context->registerService( + $settingsNs.'\\AdminSettings', + static function (ContainerInterface $c) use ($appId, $sectionId, $adminPriority) { + $class = self::GENERIC_ADMIN_SETTINGS; + return new $class( + appId: $appId, + sectionId: $sectionId, + priority: $adminPriority, + appManager: $c->get('OCP\\App\\IAppManager'), + initialState: $c->get('OCP\\AppFramework\\Services\\IInitialState'), + appConfig: $c->get('OCP\\IAppConfig') + ); + } + ); + + $context->registerService( + $sectionsNs.'\\SettingsSection', + static function (ContainerInterface $c) use ($appId, $sectionId, $sectionName, $sectionIcon, $sectionPriority) { + $class = self::GENERIC_SETTINGS_SECTION; + return new $class( + sectionId: $sectionId, + name: $sectionName, + appId: $appId, + iconFile: $sectionIcon, + priority: $sectionPriority, + urlGenerator: $c->get('OCP\\IURLGenerator') + ); + } + ); + }//end registerAdminSettings() + + /** + * Register the generic deep-link listener (manifest-driven) and bind the + * leaf listener class name to it. The event listener is registered against + * OpenRegister's DeepLinkRegistrationEvent by its string name, so a + * disabled OR simply never dispatches the event — no fatal. + * + * @param IRegistrationContext $context Leaf registration context. + * @param string $appId Leaf app id. + * @param string $listenerNs Leaf listener namespace. + * + * @return void + */ + private static function registerDeepLinkListener(IRegistrationContext $context, string $appId, string $listenerNs): void + { + $factory = static function (ContainerInterface $c) use ($appId) { + $class = self::GENERIC_DEEPLINK_LISTENER; + return new $class( + appId: $appId, + appManager: $c->get('OCP\\App\\IAppManager'), + logger: $c->get('Psr\\Log\\LoggerInterface') + ); + }; + $context->registerService(self::GENERIC_DEEPLINK_LISTENER, $factory); + $context->registerService($listenerNs.'\\DeepLinkRegistrationListener', $factory); + + $context->registerEventListener(self::DEEPLINK_EVENT, $listenerNs.'\\DeepLinkRegistrationListener'); + }//end registerDeepLinkListener() + + /** + * Convert a dash/underscore app id into a StudlyCase namespace segment + * (e.g. `pet_store` / `pet-store` → `PetStore`). + * + * @param string $appId The app id. + * + * @return string StudlyCase form. + */ + private static function studly(string $appId): string + { + $parts = preg_split(pattern: '/[_\-]+/', subject: $appId); + if ($parts === false || count($parts) === 0) { + $parts = [$appId]; + } + + $studly = ''; + foreach ($parts as $part) { + $studly .= ucfirst($part); + }//end foreach + + return $studly; + }//end studly() +}//end class diff --git a/lib/AppHost/Controller/GenericDashboardController.php b/lib/AppHost/Controller/GenericDashboardController.php new file mode 100644 index 0000000000..3d36f343c3 --- /dev/null +++ b/lib/AppHost/Controller/GenericDashboardController.php @@ -0,0 +1,112 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IRequest; + +/** + * Generic SPA page + catch-all controller for AppHost-adopting apps. + * + * Behavioural parity with the bespoke per-app `DashboardController`: + * - `page()` → `TemplateResponse({appId}, 'index')` + * - `catchAll()` → delegates to `page()` (Vue history-mode deep links). + * + * Auth posture (authenticated user, no CSRF token for the GET page) is owned + * here and matches every bespoke copy; leaf apps cannot drift it. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-1.1 + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: Canonical Route Table + */ +class GenericDashboardController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The calling (leaf) app id, supplied by the alias closure. + * @param IRequest $request HTTP request. + */ + public function __construct( + string $appName, + IRequest $request + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Render the main SPA page from the leaf app's `templates/index.php`. + * + * @return TemplateResponse The rendered template for the calling app. + * + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: Canonical Route Table + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function page(): TemplateResponse + { + return $this->renderIndex(); + }//end page() + + /** + * Serve the SPA for deep links (Vue history mode). Delegates to {@see page()}. + * + * @return TemplateResponse The rendered template for the calling app. + * + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: Canonical Route Table + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function catchAll(): TemplateResponse + { + return $this->page(); + }//end catchAll() + + /** + * Build the `index` TemplateResponse for the calling app. + * + * Overridable hook: a leaf app needing extra initial-state or a non-default + * template name aliases its DashboardController at a local subclass and + * overrides this single method — all routing stays generic. + * + * @return TemplateResponse + */ + protected function renderIndex(): TemplateResponse + { + return new TemplateResponse($this->appName, 'index'); + }//end renderIndex() +}//end class diff --git a/lib/AppHost/Controller/GenericHealthController.php b/lib/AppHost/Controller/GenericHealthController.php new file mode 100644 index 0000000000..e37ff45d20 --- /dev/null +++ b/lib/AppHost/Controller/GenericHealthController.php @@ -0,0 +1,102 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Controller; + +use OCA\OpenRegister\AppHost\Observability\HealthCheckExecutor; +use OCA\OpenRegister\AppHost\Observability\ManifestLoader; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Public, declarative health endpoint for AppHost-adopting apps. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-2.3 + */ +class GenericHealthController extends Controller +{ + /** + * Constructor. + * + * The controller's `$appName` is the calling (leaf) app id — set by the + * leaf app's alias registration in its own Application.php. For OpenRegister + * self-dogfooding it is `openregister`. + * + * @param string $appName Calling app id. + * @param IRequest $request HTTP request. + * @param ManifestLoader $manifestLoader Loads the app's observability config. + * @param HealthCheckExecutor $executor Runs the checks. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly ManifestLoader $manifestLoader, + private readonly HealthCheckExecutor $executor + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * GET /api/health — declarative health check (ADR-006). + * + * @return JSONResponse `{status, app, version, checks}` with HTTP code per statusCodePolicy. + * + * @spec openspec/specs/apphost-observability/spec.md — Requirement: Declarative Health Execution + */ + #[PublicPage] + #[NoCSRFRequired] + public function index(): JSONResponse + { + $appId = $this->appName; + $manifest = $this->manifestLoader->load(appId: $appId); + $result = $this->executor->execute(manifest: $manifest); + + $response = new JSONResponse( + [ + 'status' => $result->status, + 'app' => $appId, + 'version' => $this->manifestLoader->appVersion(appId: $appId), + 'checks' => $result->checks, + ], + $result->httpStatusCode + ); + + if ($manifest->cors === true) { + $response->addHeader('Access-Control-Allow-Origin', '*'); + $response->addHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + } + + return $response; + }//end index() +}//end class diff --git a/lib/AppHost/Controller/GenericMetricsController.php b/lib/AppHost/Controller/GenericMetricsController.php new file mode 100644 index 0000000000..a23de33271 --- /dev/null +++ b/lib/AppHost/Controller/GenericMetricsController.php @@ -0,0 +1,94 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Controller; + +use OCA\OpenRegister\AppHost\Observability\ManifestLoader; +use OCA\OpenRegister\AppHost\Observability\MetricsEngine; +use OCA\OpenRegister\AppHost\Observability\PrometheusRenderer; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\TextPlainResponse; +use OCP\IRequest; + +/** + * Admin-only declarative Prometheus metrics endpoint for AppHost adopters. + * + * No `#[NoAdminRequired]` — the absence of that attribute means NC requires an + * admin session, which is the intended ADR-006 posture for metrics. Anonymous + * callers get the NC login redirect / 401, never metric data. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.8 + */ +class GenericMetricsController extends Controller +{ + /** + * Constructor. + * + * The controller's `$appName` is the calling (leaf) app id, set by the + * leaf app's alias registration in its own Application.php. For OpenRegister + * self-dogfooding it is `openregister`. + * + * @param string $appName Calling app id. + * @param IRequest $request HTTP request. + * @param ManifestLoader $manifestLoader Loads the app's observability config. + * @param MetricsEngine $engine Renders the metrics. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly ManifestLoader $manifestLoader, + private readonly MetricsEngine $engine + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * GET /api/metrics — declarative Prometheus metrics (admin-only, ADR-006). + * + * @return TextPlainResponse Prometheus text exposition 0.0.4. + * + * @spec openspec/specs/apphost-observability/spec.md — Requirement: Declarative Metrics Execution + */ + #[NoCSRFRequired] + public function index(): TextPlainResponse + { + $appId = $this->appName; + $manifest = $this->manifestLoader->load(appId: $appId); + $body = $this->engine->render(manifest: $manifest); + + $response = new TextPlainResponse($body); + $response->addHeader('Content-Type', PrometheusRenderer::CONTENT_TYPE); + return $response; + }//end index() +}//end class diff --git a/lib/AppHost/Controller/GenericPreferencesController.php b/lib/AppHost/Controller/GenericPreferencesController.php new file mode 100644 index 0000000000..aa040aa4f5 --- /dev/null +++ b/lib/AppHost/Controller/GenericPreferencesController.php @@ -0,0 +1,178 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IUserSession; + +/** + * Generic per-user preferences controller for AppHost-adopting apps. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/changes/apphost-generic-preferences/tasks.md#task-1.1 + * @spec openspec/changes/apphost-generic-preferences/specs/apphost-boilerplate/spec.md — Requirement: Generic Preferences Controller + */ +class GenericPreferencesController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The calling (leaf) app id, supplied by the alias closure. + * @param IRequest $request HTTP request. + * @param IConfig $config The Nextcloud config (user values). + * @param IUserSession $userSession The user session. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly IConfig $config, + private readonly IUserSession $userSession + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Read a per-user preference value for the current user. + * + * @param string $key The preference key (kebab/alphanumeric). + * + * @return JSONResponse `{value: string|null}`, 401 when anonymous, 400 on an invalid key. + * + * @spec openspec/changes/apphost-generic-preferences/specs/apphost-boilerplate/spec.md — Requirement: Generic Preferences Controller + */ + #[NoAdminRequired] + public function getPreference(string $key): JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse(data: ['message' => 'Not logged in'], statusCode: Http::STATUS_UNAUTHORIZED); + } + + $safeKey = $this->sanitizeKey(key: $key); + if ($safeKey === '') { + return new JSONResponse(data: ['message' => 'Invalid key'], statusCode: Http::STATUS_BAD_REQUEST); + } + + $value = $this->config->getUserValue( + userId: $user->getUID(), + appName: $this->appName, + key: 'pref_'.$safeKey, + default: '' + ); + + $stored = null; + if ($value !== '') { + $stored = $value; + } + + return new JSONResponse(data: ['value' => $stored]); + }//end getPreference() + + /** + * Write a per-user preference value for the current user. An empty value clears it. + * + * @param string $key The preference key (kebab/alphanumeric). + * @param string $value The value to store (empty string clears it). + * + * @return JSONResponse `{value: string|null}`, 401 when anonymous, 400 on an invalid key. + * + * @spec openspec/changes/apphost-generic-preferences/specs/apphost-boilerplate/spec.md — Requirement: Generic Preferences Controller + */ + #[NoAdminRequired] + public function setPreference(string $key, string $value=''): JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse(data: ['message' => 'Not logged in'], statusCode: Http::STATUS_UNAUTHORIZED); + } + + $safeKey = $this->sanitizeKey(key: $key); + if ($safeKey === '') { + return new JSONResponse(data: ['message' => 'Invalid key'], statusCode: Http::STATUS_BAD_REQUEST); + } + + if ($value === '') { + $this->config->deleteUserValue( + userId: $user->getUID(), + appName: $this->appName, + key: 'pref_'.$safeKey + ); + + return new JSONResponse(data: ['value' => null]); + } + + $this->config->setUserValue( + userId: $user->getUID(), + appName: $this->appName, + key: 'pref_'.$safeKey, + value: $value + ); + + return new JSONResponse(data: ['value' => $value]); + }//end setPreference() + + /** + * Restrict keys to a safe charset so callers cannot reach arbitrary + * IConfig user values outside the `pref_` namespace. + * + * @param string $key The raw key. + * + * @return string The sanitised key, or '' when nothing safe remains. + */ + private function sanitizeKey(string $key): string + { + $safe = preg_replace(pattern: '/[^a-z0-9-]/', replacement: '', subject: strtolower($key)); + return substr((string) $safe, offset: 0, length: 64); + }//end sanitizeKey() +}//end class diff --git a/lib/AppHost/Controller/GenericSettingsController.php b/lib/AppHost/Controller/GenericSettingsController.php new file mode 100644 index 0000000000..f048852a97 --- /dev/null +++ b/lib/AppHost/Controller/GenericSettingsController.php @@ -0,0 +1,124 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Controller; + +use OCA\OpenRegister\AppHost\Service\AppHostSettingsService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Generic settings controller for AppHost-adopting apps. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-1.3 + */ +class GenericSettingsController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The calling (leaf) app id. + * @param IRequest $request HTTP request. + * @param AppHostSettingsService $settingsService Generic settings service bound to this app. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly AppHostSettingsService $settingsService + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Retrieve all current settings. + * + * Admin-sensitive fields (the register binding) are stripped for non-admin + * users so the register UUID is not exposed to regular authenticated users. + * + * @return JSONResponse + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-1.3 + */ + #[NoAdminRequired] + public function index(): JSONResponse + { + $settings = $this->settingsService->getSettings(); + $isAdmin = ($settings['isAdmin'] ?? false); + + if ($isAdmin === false) { + unset($settings['register']); + } + + return new JSONResponse($settings); + }//end index() + + /** + * Update settings with the provided data. Full admin required (no attribute). + * + * @return JSONResponse + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-1.3 + */ + public function create(): JSONResponse + { + $data = $this->request->getParams(); + $config = $this->settingsService->updateSettings($data); + + return new JSONResponse( + [ + 'success' => true, + 'config' => $config, + ] + ); + }//end create() + + /** + * Re-import the configuration from the app's register JSON. Full admin + * required (no attribute). Forces a fresh import regardless of version. + * + * @return JSONResponse + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-1.3 + */ + public function load(): JSONResponse + { + $result = $this->settingsService->loadConfiguration(force: true); + + return new JSONResponse($result); + }//end load() +}//end class diff --git a/lib/AppHost/IHealthCheckProvider.php b/lib/AppHost/IHealthCheckProvider.php new file mode 100644 index 0000000000..521ab3e397 --- /dev/null +++ b/lib/AppHost/IHealthCheckProvider.php @@ -0,0 +1,47 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost; + +/** + * Imperative health-check provider for the AppHost observability engine. + */ +interface IHealthCheckProvider +{ + /** + * Run the provider's checks. + * + * @return array + * Map of check id => result. `ok` drives status; optional `severity` + * (critical|degraded, default critical) drives the HTTP code; optional + * `message` is shown only on failure and MUST NOT leak internals. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-2.4 + */ + public function check(): array; +}//end interface diff --git a/lib/AppHost/IMetricsProvider.php b/lib/AppHost/IMetricsProvider.php new file mode 100644 index 0000000000..26b1199a04 --- /dev/null +++ b/lib/AppHost/IMetricsProvider.php @@ -0,0 +1,51 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost; + +use OCA\OpenRegister\AppHost\Observability\MetricSample; + +/** + * Imperative metrics provider for the AppHost observability engine. + */ +interface IMetricsProvider +{ + /** + * Produce the provider's metric samples. + * + * Returned samples are rendered by the engine's Prometheus renderer with + * the app's `{app}_` prefix and HELP/TYPE lines, exactly like declarative + * metrics, so providers never emit raw exposition text themselves. + * + * @return MetricSample[] The provider's samples. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.5 + */ + public function metrics(): array; +}//end interface diff --git a/lib/AppHost/Listener/GenericDeepLinkRegistrationListener.php b/lib/AppHost/Listener/GenericDeepLinkRegistrationListener.php new file mode 100644 index 0000000000..ad4bf9bc75 --- /dev/null +++ b/lib/AppHost/Listener/GenericDeepLinkRegistrationListener.php @@ -0,0 +1,161 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Listener; + +use OCA\OpenRegister\Event\DeepLinkRegistrationEvent; +use OCP\App\IAppManager; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Registers a leaf app's manifest-declared deep links with OpenRegister. + * + * @implements IEventListener + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.5 + */ +class GenericDeepLinkRegistrationListener implements IEventListener +{ + /** + * Constructor. + * + * @param string $appId The leaf app id whose manifest is read. + * @param IAppManager $appManager App path resolution. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + protected readonly string $appId, + protected readonly IAppManager $appManager, + protected readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Handle the deep-link registration event. + * + * @param Event $event The dispatched event. + * + * @return void + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.5 + */ + public function handle(Event $event): void + { + if ($event instanceof DeepLinkRegistrationEvent === false) { + return; + } + + foreach ($this->loadDeepLinks() as $link) { + $registerSlug = ($link['registerSlug'] ?? ''); + $schemaSlug = ($link['schemaSlug'] ?? ''); + $urlTemplate = ($link['urlTemplate'] ?? ''); + + if ($registerSlug === '' || $schemaSlug === '' || $urlTemplate === '') { + continue; + } + + $displayName = ($link['displayName'] ?? null); + if (is_string($displayName) === false) { + $displayName = null; + } + + $event->register( + appId: $this->appId, + registerSlug: (string) $registerSlug, + schemaSlug: (string) $schemaSlug, + urlTemplate: (string) $urlTemplate, + icon: (string) ($link['icon'] ?? ''), + displayName: $displayName + ); + }//end foreach + }//end handle() + + /** + * Load the `deepLinks` array from the leaf app's manifest. Overridable hook. + * + * Always returns an array; a missing/unreadable manifest or block yields + * an empty list (no deep links registered), never a fatal. + * + * @return array> + * + * @SuppressWarnings(PHPMD.StaticAccess) + */ + protected function loadDeepLinks(): array + { + try { + $appPath = $this->appManager->getAppPath(appId: $this->appId); + } catch (Throwable $e) { + return []; + } + + $manifestPath = $appPath.'/src/manifest.json'; + if (is_readable($manifestPath) === false) { + return []; + } + + $raw = file_get_contents($manifestPath); + if ($raw === false) { + return []; + } + + $decoded = json_decode($raw, associative: true); + if (json_last_error() !== JSON_ERROR_NONE || is_array($decoded) === false) { + $this->logger->warning(sprintf('[AppHost:%s] manifest.json is invalid JSON; no deep links registered', $this->appId)); + return []; + } + + $links = ($decoded['deepLinks'] ?? []); + if (is_array($links) === false) { + return []; + } + + // Keep only array entries. + $entries = []; + foreach ($links as $link) { + if (is_array($link) === true) { + $entries[] = $link; + } + }//end foreach + + return $entries; + }//end loadDeepLinks() +}//end class diff --git a/lib/AppHost/Observability/HealthCheckDescriptor.php b/lib/AppHost/Observability/HealthCheckDescriptor.php new file mode 100644 index 0000000000..60c4523c59 --- /dev/null +++ b/lib/AppHost/Observability/HealthCheckDescriptor.php @@ -0,0 +1,175 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +/** + * Validated descriptor for one declarative health check. + * + * The closed set of supported types is defined by ADR-040; any other value + * raises {@see ObservabilityValidationException} so it surfaces as a manifest + * diagnostic rather than a runtime 500. + * + * @psalm-immutable + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.1 + */ +final class HealthCheckDescriptor +{ + /** + * Closed set of supported check types (ADR-040). + * + * @var string[] + */ + public const TYPES = [ + 'database', + 'filesystem', + 'appEnabled', + 'appConfig', + 'orAvailable', + ]; + + /** + * Closed set of supported severities. + * + * @var string[] + */ + public const SEVERITIES = ['critical', 'degraded']; + + /** + * Closed set of supported appConfig assertions. + * + * @var string[] + */ + public const ASSERTIONS = ['present', 'nonEmpty']; + + /** + * Constructor. + * + * @param string $id Stable check id used as the `checks` map key. + * @param string $type One of self::TYPES. + * @param string $severity One of self::SEVERITIES. + * @param string|null $app Target app id (appEnabled only). + * @param string|null $key AppConfig key (appConfig only). + * @param string|null $assert AppConfig assertion (appConfig only). + */ + public function __construct( + public readonly string $id, + public readonly string $type, + public readonly string $severity, + public readonly ?string $app=null, + public readonly ?string $key=null, + public readonly ?string $assert=null + ) { + }//end __construct() + + /** + * Build a validated descriptor from a raw manifest array entry. + * + * @param array $raw Raw `checks[]` entry. + * @param int $index Position in the array (for default ids). + * + * @return self + * + * @throws ObservabilityValidationException When the entry is malformed. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.1 + */ + public static function fromArray(array $raw, int $index=0): self + { + $type = $raw['type'] ?? null; + if (is_string($type) === false || in_array($type, self::TYPES, true) === false) { + $allowed = implode(', ', self::TYPES); + throw new ObservabilityValidationException( + sprintf('Unknown health check type "%s" (allowed: %s).', self::describe(value: $type), $allowed) + ); + } + + $severity = $raw['severity'] ?? 'critical'; + if (is_string($severity) === false || in_array($severity, self::SEVERITIES, true) === false) { + $allowed = implode(', ', self::SEVERITIES); + throw new ObservabilityValidationException( + sprintf('Invalid severity "%s" for health check (allowed: %s).', self::describe(value: $severity), $allowed) + ); + } + + $id = $raw['id'] ?? $type.'_'.$index; + if (is_string($id) === false || $id === '') { + throw new ObservabilityValidationException('Health check id must be a non-empty string.'); + } + + $app = null; + $key = null; + $assert = null; + + if ($type === 'appEnabled') { + $app = $raw['app'] ?? null; + if (is_string($app) === false || preg_match('/^[a-z0-9_-]+$/i', $app) !== 1) { + throw new ObservabilityValidationException('appEnabled check requires a valid "app" id.'); + } + } + + if ($type === 'appConfig') { + $key = $raw['key'] ?? null; + if (is_string($key) === false || $key === '') { + throw new ObservabilityValidationException('appConfig check requires a non-empty "key".'); + } + + $assert = $raw['assert'] ?? 'present'; + if (is_string($assert) === false || in_array($assert, self::ASSERTIONS, true) === false) { + $allowed = implode(', ', self::ASSERTIONS); + throw new ObservabilityValidationException( + sprintf('Invalid appConfig assert "%s" (allowed: %s).', self::describe(value: $assert), $allowed) + ); + } + } + + return new self( + id: $id, + type: $type, + severity: $severity, + app: $app, + key: $key, + assert: $assert + ); + }//end fromArray() + + /** + * Render a raw (possibly non-scalar) value as a short string for an + * error message, without leaking object internals. + * + * @param mixed $value Raw value. + * + * @return string + */ + private static function describe(mixed $value): string + { + if (is_scalar($value) === true) { + return (string) $value; + } + + return gettype($value); + }//end describe() +}//end class diff --git a/lib/AppHost/Observability/HealthCheckExecutor.php b/lib/AppHost/Observability/HealthCheckExecutor.php new file mode 100644 index 0000000000..48b13ce6d6 --- /dev/null +++ b/lib/AppHost/Observability/HealthCheckExecutor.php @@ -0,0 +1,381 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +use OCA\OpenRegister\AppHost\IHealthCheckProvider; +use OCP\App\IAppManager; +use OCP\AppFramework\Http; +use OCP\IAppConfig; +use OCP\IDBConnection; +use OCP\ITempManager; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Runs declarative health checks and resolves the response status + code. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-2.1 + */ +class HealthCheckExecutor +{ + private const STATUS_OK = 'ok'; + + private const STATUS_DEGRADED = 'degraded'; + + private const STATUS_ERROR = 'error'; + + /** + * Constructor. + * + * @param IDBConnection $db Database connection (database check). + * @param ITempManager $tempManager Temp-file manager (filesystem check). + * @param IAppManager $appManager App manager (appEnabled check). + * @param IAppConfig $appConfig App config (appConfig check). + * @param ContainerInterface $container DI container (lazy orAvailable + provider lookup). + * @param LoggerInterface $logger PSR logger (exception messages logged, never returned). + */ + public function __construct( + private readonly IDBConnection $db, + private readonly ITempManager $tempManager, + private readonly IAppManager $appManager, + private readonly IAppConfig $appConfig, + private readonly ContainerInterface $container, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Execute all health checks for the given manifest and resolve the result. + * + * @param ObservabilityManifest $manifest The app's observability config. + * + * @return HealthResult + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-2.2 + */ + public function execute(ObservabilityManifest $manifest): HealthResult + { + $checks = []; + $worstStatus = self::STATUS_OK; + + foreach ($manifest->checks as $descriptor) { + [$ok, $message] = $this->runCheck(appId: $manifest->appId, descriptor: $descriptor); + $checks[$descriptor->id] = $this->formatCheckResult(ok: $ok, message: $message); + + if ($ok === false) { + $worstStatus = $this->escalate(current: $worstStatus, severity: $descriptor->severity); + } + } + + // Merge IHealthCheckProvider escape-hatch results. + foreach ($this->resolveProviderResults(appId: $manifest->appId) as $id => $result) { + $ok = ($result['ok'] ?? false) === true; + $severity = ($result['severity'] ?? 'critical'); + if (in_array($severity, HealthCheckDescriptor::SEVERITIES, true) === false) { + $severity = 'critical'; + } + + $message = (string) ($result['message'] ?? ''); + $checks[$id] = $this->formatCheckResult(ok: $ok, message: $message); + if ($ok === false) { + $worstStatus = $this->escalate(current: $worstStatus, severity: $severity); + } + } + + return new HealthResult( + status: $worstStatus, + checks: $checks, + httpStatusCode: $this->resolveHttpCode(status: $worstStatus, policy: $manifest->statusCodePolicy) + ); + }//end execute() + + /** + * Run a single check descriptor. + * + * @param string $appId Calling app id. + * @param HealthCheckDescriptor $descriptor The check. + * + * @return array{0: bool, 1: string} [ok, generic failure message] + */ + private function runCheck(string $appId, HealthCheckDescriptor $descriptor): array + { + try { + switch ($descriptor->type) { + case 'database': + return $this->checkDatabase(); + case 'filesystem': + return $this->checkFilesystem(); + case 'appEnabled': + return $this->checkAppEnabled(app: (string) $descriptor->app); + case 'appConfig': + return $this->checkAppConfig(appId: $appId, key: (string) $descriptor->key, assert: (string) $descriptor->assert); + case 'orAvailable': + return $this->checkOrAvailable(); + default: + return [false, 'unknown check']; + } + } catch (Throwable $e) { + // Log internals; return a generic message so health never leaks. + $logMessage = sprintf( + '[AppHost\\Health] Check "%s" (%s) for app "%s" threw: %s', + $descriptor->id, + $descriptor->type, + $appId, + $e->getMessage() + ); + $this->logger->warning( + message: $logMessage, + context: ['file' => __FILE__, 'line' => __LINE__, 'app' => $appId] + ); + return [false, 'check error']; + }//end try + }//end runCheck() + + /** + * Format a check outcome for the `checks` response map. + * + * @param bool $ok Whether the check passed. + * @param string $message Generic failure message (only used on failure). + * + * @return string "ok" or "failed[: message]". + */ + private function formatCheckResult(bool $ok, string $message): string + { + if ($ok === true) { + return self::STATUS_OK; + } + + if ($message === '') { + return 'failed'; + } + + return 'failed: '.$message; + }//end formatCheckResult() + + /** + * `SELECT 1` round-trip. + * + * @return array{0: bool, 1: string} + */ + private function checkDatabase(): array + { + $result = $this->db->executeQuery('SELECT 1'); + $result->fetch(); + $result->closeCursor(); + return [true, '']; + }//end checkDatabase() + + /** + * Write + unlink a temp file. + * + * @return array{0: bool, 1: string} + */ + private function checkFilesystem(): array + { + $path = $this->tempManager->getTemporaryFile('-apphost-health'); + if ($path === false || $path === '') { + return [false, 'temp file unavailable']; + } + + $bytes = file_put_contents($path, 'ok'); + $ok = ($bytes !== false); + // The ITempManager cleans its files on shutdown; unlink eagerly anyway. + @unlink($path); + if ($ok === true) { + return [true, '']; + } + + return [false, 'temp write failed']; + }//end checkFilesystem() + + /** + * App-enabled check. + * + * @param string $app Target app id. + * + * @return array{0: bool, 1: string} + */ + private function checkAppEnabled(string $app): array + { + $enabled = $this->appManager->isInstalled($app); + if ($enabled === true) { + return [true, '']; + } + + return [false, 'app not enabled']; + }//end checkAppEnabled() + + /** + * App-config key assertion (present | nonEmpty). + * + * @param string $appId Calling app id. + * @param string $key Config key. + * @param string $assert present|nonEmpty. + * + * @return array{0: bool, 1: string} + */ + private function checkAppConfig(string $appId, string $key, string $assert): array + { + $has = $this->appConfig->hasKey($appId, $key); + if ($has === false) { + return [false, 'config key missing']; + } + + if ($assert === 'nonEmpty') { + $value = $this->appConfig->getValueString($appId, $key, ''); + if ($value === '') { + return [false, 'config key empty']; + } + } + + return [true, '']; + }//end checkAppConfig() + + /** + * OpenRegister availability — resolve ObjectService, non-null. + * + * @return array{0: bool, 1: string} + */ + private function checkOrAvailable(): array + { + $service = $this->container->get(\OCA\OpenRegister\Service\ObjectService::class); + if ($service !== null) { + return [true, '']; + } + + return [false, 'openregister unavailable']; + }//end checkOrAvailable() + + /** + * Resolve and run the app's IHealthCheckProvider, if any. + * + * @param string $appId Calling app id. + * + * @return array> + */ + private function resolveProviderResults(string $appId): array + { + $alias = IHealthCheckProvider::class.'::'.$appId; + try { + // Resolve from the CALLING APP's own container — where the app + // registered its `IHealthCheckProvider::` alias — not from + // OpenRegister's container, which cannot see another app's DI + // registrations (same root cause as the MCP-provider fix, #390). + $container = $this->resolveAppContainer(appId: $appId); + + if ($container->has($alias) === false) { + return []; + } + + $provider = $container->get($alias); + if (($provider instanceof IHealthCheckProvider) === false) { + return []; + } + + return $provider->check(); + } catch (Throwable $e) { + $this->logger->warning( + message: sprintf('[AppHost\\Health] Provider for app "%s" threw: %s', $appId, $e->getMessage()), + context: ['file' => __FILE__, 'line' => __LINE__, 'app' => $appId] + ); + return []; + }//end try + }//end resolveProviderResults() + + /** + * Resolve the calling app's OWN DI container so its + * `IHealthCheckProvider::` alias resolves from where the leaf app + * actually registered it (#390). Fail-soft to OpenRegister's container. + * + * @param string $appId The calling app id. + * + * @return ContainerInterface The app's own container, or OpenRegister's as a fallback. + */ + private function resolveAppContainer(string $appId): ContainerInterface + { + try { + $appContainer = \OC::$server->getRegisteredAppContainer($appId); + if ($appContainer instanceof ContainerInterface) { + return $appContainer; + } + } catch (Throwable $e) { + $this->logger->debug( + message: sprintf('[AppHost\\Health] no registered app container for "%s": %s', $appId, $e->getMessage()), + context: ['file' => __FILE__, 'line' => __LINE__, 'app' => $appId] + ); + }//end try + + return $this->container; + }//end resolveAppContainer() + + /** + * Escalate the running status given a failed check's severity. + * + * @param string $current Current worst status. + * @param string $severity critical|degraded. + * + * @return string New worst status. + */ + private function escalate(string $current, string $severity): string + { + if ($severity === 'critical') { + return self::STATUS_ERROR; + } + + // Degraded failure cannot downgrade an already-error status. + if ($current === self::STATUS_ERROR) { + return self::STATUS_ERROR; + } + + return self::STATUS_DEGRADED; + }//end escalate() + + /** + * Resolve the HTTP status code under the active policy. + * + * @param string $status ok|degraded|error. + * @param string $policy adr006|always200. + * + * @return int HTTP code. + */ + private function resolveHttpCode(string $status, string $policy): int + { + if ($policy === ObservabilityManifest::POLICY_ALWAYS200) { + return Http::STATUS_OK; + } + + // The adr006 policy returns 503 only when a critical check failed + // (status === error); everything else stays 200. + if ($status === self::STATUS_ERROR) { + return Http::STATUS_SERVICE_UNAVAILABLE; + } + + return Http::STATUS_OK; + }//end resolveHttpCode() +}//end class diff --git a/lib/AppHost/Observability/HealthResult.php b/lib/AppHost/Observability/HealthResult.php new file mode 100644 index 0000000000..fb40b05e9b --- /dev/null +++ b/lib/AppHost/Observability/HealthResult.php @@ -0,0 +1,51 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +/** + * Health execution outcome. + * + * @psalm-immutable + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-2.2 + */ +final class HealthResult +{ + /** + * Constructor. + * + * @param string $status ok|degraded|error. + * @param array $checks Map of check id => "ok" | "failed: ...". + * @param int $httpStatusCode Resolved HTTP status code. + */ + public function __construct( + public readonly string $status, + public readonly array $checks, + public readonly int $httpStatusCode + ) { + }//end __construct() +}//end class diff --git a/lib/AppHost/Observability/ManifestLoader.php b/lib/AppHost/Observability/ManifestLoader.php new file mode 100644 index 0000000000..b0a5cf1122 --- /dev/null +++ b/lib/AppHost/Observability/ManifestLoader.php @@ -0,0 +1,144 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +use OCP\App\IAppManager; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Loads + decodes an app's manifest and parses its observability config. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.2 + */ +class ManifestLoader +{ + /** + * Constructor. + * + * @param IAppManager $appManager App-path resolution. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Load the observability config for an app id. + * + * Always returns a usable config: when the manifest is missing or + * unreadable, the app's defaults apply (database check, implicit metrics). + * + * @param string $appId The calling app's id. + * + * @return ObservabilityManifest + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.2 + */ + public function load(string $appId): ObservabilityManifest + { + $manifest = $this->loadBundledManifest(appId: $appId); + if ($manifest === null) { + return ObservabilityManifest::defaults(appId: $appId, manifest: []); + } + + return ObservabilityManifest::fromManifest(appId: $appId, manifest: $manifest); + }//end load() + + /** + * Resolve the installed version of an app (for the implicit `{app}_info`). + * + * @param string $appId The app id. + * + * @return string The version string, or 'unknown'. + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.6 + */ + public function appVersion(string $appId): string + { + try { + $version = $this->appManager->getAppVersion($appId); + if ($version === '') { + return 'unknown'; + } + + return $version; + } catch (Throwable $e) { + return 'unknown'; + } + }//end appVersion() + + /** + * Load + JSON-decode an app's bundled `src/manifest.json`. + * + * @param string $appId The Nextcloud app id. + * + * @return array|null Decoded manifest, or null if unreadable. + * + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function loadBundledManifest(string $appId): ?array + { + try { + $appPath = $this->appManager->getAppPath(appId: $appId); + } catch (Throwable $e) { + $this->logger->debug( + message: sprintf('[AppHost\\ManifestLoader] App "%s" path not found: %s', $appId, $e->getMessage()), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return null; + } + + $manifestPath = $appPath.'/src/manifest.json'; + if (is_readable($manifestPath) === false) { + return null; + } + + $raw = file_get_contents($manifestPath); + if ($raw === false) { + return null; + } + + $decoded = json_decode($raw, associative: true); + if (json_last_error() !== JSON_ERROR_NONE || is_array($decoded) === false) { + $this->logger->warning( + message: sprintf('[AppHost\\ManifestLoader] manifest.json for "%s" is invalid JSON', $appId), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return null; + } + + return $decoded; + }//end loadBundledManifest() +}//end class diff --git a/lib/AppHost/Observability/MetricDescriptor.php b/lib/AppHost/Observability/MetricDescriptor.php new file mode 100644 index 0000000000..9674201384 --- /dev/null +++ b/lib/AppHost/Observability/MetricDescriptor.php @@ -0,0 +1,338 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +/** + * Validated descriptor for one declarative metric. + * + * The closed set of source kinds, the Prometheus metric types, the filter + * operator allowlist and the `tableCount.table` regex are all defined by + * ADR-040 and enforced here at manifest-validation time. + * + * @psalm-immutable + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.1 + */ +final class MetricDescriptor +{ + /** + * Closed set of supported source kinds (ADR-040). + * + * @var string[] + */ + public const KINDS = [ + 'objectCount', + 'objectSum', + 'tableCount', + 'appConfig', + 'provider', + ]; + + /** + * Closed set of supported Prometheus metric types. + * + * @var string[] + */ + public const METRIC_TYPES = ['gauge', 'counter']; + + /** + * Closed set of supported filter operators (ADR-040). + * + * @var string[] + */ + public const FILTER_OPERATORS = ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'like']; + + /** + * Allowlist regex for a `tableCount.table` value. The security boundary: + * only `[a-z0-9_]+` table names reach the QueryBuilder table API. + */ + public const TABLE_REGEX = '/^[a-z0-9_]+$/'; + + /** + * Constructor. + * + * @param string $name Metric name (without the `{app}_` prefix). + * @param string $type Prometheus type (gauge|counter). + * @param string $kind Source kind (self::KINDS). + * @param array $source Raw validated source params. + * @param string|null $help HELP text. + * @param int $cacheTtl Per-metric cache TTL in seconds (0 = no cache). + */ + public function __construct( + public readonly string $name, + public readonly string $type, + public readonly string $kind, + public readonly array $source, + public readonly ?string $help=null, + public readonly int $cacheTtl=0 + ) { + }//end __construct() + + /** + * Build a validated descriptor from a raw manifest array entry. + * + * @param array $raw Raw `metrics[]` entry. + * + * @return self + * + * @throws ObservabilityValidationException When the entry is malformed. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.1 + */ + public static function fromArray(array $raw): self + { + $name = $raw['name'] ?? null; + if (is_string($name) === false || preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name) !== 1) { + throw new ObservabilityValidationException('Metric "name" must be a valid Prometheus metric name.'); + } + + $type = $raw['type'] ?? 'gauge'; + if (is_string($type) === false || in_array($type, self::METRIC_TYPES, true) === false) { + $allowed = implode(', ', self::METRIC_TYPES); + throw new ObservabilityValidationException( + sprintf('Invalid metric type "%s" (allowed: %s).', self::describe(value: $type), $allowed) + ); + } + + $source = $raw['source'] ?? null; + if (is_array($source) === false) { + throw new ObservabilityValidationException(sprintf('Metric "%s" requires a "source" object.', $name)); + } + + $kind = $source['kind'] ?? null; + if (is_string($kind) === false || in_array($kind, self::KINDS, true) === false) { + $allowed = implode(', ', self::KINDS); + throw new ObservabilityValidationException( + sprintf('Unknown metric source kind "%s" (allowed: %s).', self::describe(value: $kind), $allowed) + ); + } + + self::validateSource(name: $name, kind: $kind, source: $source); + + $cacheTtl = $raw['cacheTtl'] ?? 0; + if (is_int($cacheTtl) === false || $cacheTtl < 0) { + throw new ObservabilityValidationException(sprintf('Metric "%s" cacheTtl must be a non-negative integer.', $name)); + } + + $help = $raw['help'] ?? null; + if ($help !== null && is_string($help) === false) { + throw new ObservabilityValidationException(sprintf('Metric "%s" help must be a string.', $name)); + } + + return new self( + name: $name, + type: $type, + kind: $kind, + source: $source, + help: $help, + cacheTtl: $cacheTtl + ); + }//end fromArray() + + /** + * Validate a source block per its kind. + * + * @param string $name Metric name (for messages). + * @param string $kind Source kind. + * @param array $source Raw source block. + * + * @return void + * + * @throws ObservabilityValidationException When the source is malformed. + */ + private static function validateSource(string $name, string $kind, array $source): void + { + switch ($kind) { + case 'objectCount': + case 'objectSum': + if (isset($source['schema']) === false || is_string($source['schema']) === false || $source['schema'] === '') { + throw new ObservabilityValidationException(sprintf('Metric "%s" %s source requires a "schema" slug.', $name, $kind)); + } + + if (isset($source['register']) === true && is_string($source['register']) === false) { + throw new ObservabilityValidationException(sprintf('Metric "%s" "register" must be a slug string.', $name)); + } + + if ($kind === 'objectSum') { + $field = $source['field'] ?? null; + $fieldInvalid = (is_string($field) === false || $field === ''); + if ($fieldInvalid === true) { + throw new ObservabilityValidationException(sprintf('Metric "%s" objectSum source requires a numeric "field".', $name)); + } + } + + self::validateGroupBy(name: $name, source: $source); + self::validateFilter(name: $name, source: $source); + break; + + case 'tableCount': + $table = $source['table'] ?? null; + if (is_string($table) === false || preg_match(self::TABLE_REGEX, $table) !== 1) { + $shown = self::describe(value: $table); + throw new ObservabilityValidationException( + sprintf('Metric "%s" tableCount "table" must match %s (got "%s").', $name, self::TABLE_REGEX, $shown) + ); + } + + self::validateGroupBy(name: $name, source: $source); + self::validateFilter(name: $name, source: $source); + self::validateColumnMap(name: $name, key: 'labelMap', source: $source); + self::validateColumnMap(name: $name, key: 'labelDefaults', source: $source); + break; + + case 'appConfig': + if (isset($source['key']) === false || is_string($source['key']) === false || $source['key'] === '') { + throw new ObservabilityValidationException(sprintf('Metric "%s" appConfig source requires a "key".', $name)); + } + break; + + case 'provider': + // No parameters; resolution happens via the registered IMetricsProvider alias. + break; + }//end switch + }//end validateSource() + + /** + * Validate a `groupBy` list of string field/column names. + * + * @param string $name Metric name. + * @param array $source Source block. + * + * @return void + * + * @throws ObservabilityValidationException When malformed. + */ + private static function validateGroupBy(string $name, array $source): void + { + if (isset($source['groupBy']) === false) { + return; + } + + if (is_array($source['groupBy']) === false) { + throw new ObservabilityValidationException(sprintf('Metric "%s" groupBy must be an array.', $name)); + } + + foreach ($source['groupBy'] as $field) { + if (is_string($field) === false || preg_match('/^[a-zA-Z0-9_.]+$/', $field) !== 1) { + $shown = self::describe(value: $field); + throw new ObservabilityValidationException(sprintf('Metric "%s" groupBy field "%s" is invalid.', $name, $shown)); + } + } + }//end validateGroupBy() + + /** + * Validate a `filter` map of field => { operator: value } against the + * operator allowlist. + * + * @param string $name Metric name. + * @param array $source Source block. + * + * @return void + * + * @throws ObservabilityValidationException When an operator is not allowlisted. + */ + private static function validateFilter(string $name, array $source): void + { + if (isset($source['filter']) === false) { + return; + } + + if (is_array($source['filter']) === false) { + throw new ObservabilityValidationException(sprintf('Metric "%s" filter must be an object.', $name)); + } + + foreach ($source['filter'] as $field => $ops) { + if (is_string($field) === false || preg_match('/^[a-zA-Z0-9_.]+$/', $field) !== 1) { + $shown = self::describe(value: $field); + throw new ObservabilityValidationException(sprintf('Metric "%s" filter field "%s" is invalid.', $name, $shown)); + } + + if (is_array($ops) === false) { + throw new ObservabilityValidationException(sprintf('Metric "%s" filter "%s" must map operators to values.', $name, $field)); + } + + foreach (array_keys($ops) as $operator) { + if (in_array($operator, self::FILTER_OPERATORS, true) === false) { + $shown = self::describe(value: $operator); + $allowed = implode(', ', self::FILTER_OPERATORS); + throw new ObservabilityValidationException( + sprintf('Metric "%s" filter operator "%s" is not allowed (allowed: %s).', $name, $shown, $allowed) + ); + } + } + }//end foreach + }//end validateFilter() + + /** + * Validate a column => string map (labelMap / labelDefaults). + * + * @param string $name Metric name. + * @param string $key Source key. + * @param array $source Source block. + * + * @return void + * + * @throws ObservabilityValidationException When malformed. + */ + private static function validateColumnMap(string $name, string $key, array $source): void + { + if (isset($source[$key]) === false) { + return; + } + + if (is_array($source[$key]) === false) { + throw new ObservabilityValidationException(sprintf('Metric "%s" %s must be an object.', $name, $key)); + } + + foreach ($source[$key] as $column => $value) { + if (is_string($column) === false || preg_match('/^[a-zA-Z0-9_]+$/', $column) !== 1) { + $shown = self::describe(value: $column); + throw new ObservabilityValidationException(sprintf('Metric "%s" %s column "%s" is invalid.', $name, $key, $shown)); + } + + if (is_scalar($value) === false) { + throw new ObservabilityValidationException(sprintf('Metric "%s" %s value for "%s" must be scalar.', $name, $key, $column)); + } + } + }//end validateColumnMap() + + /** + * Render a raw (possibly non-scalar) value as a short string for an + * error message, without leaking object internals. + * + * @param mixed $value Raw value. + * + * @return string + */ + private static function describe(mixed $value): string + { + if (is_scalar($value) === true) { + return (string) $value; + } + + return gettype($value); + }//end describe() +}//end class diff --git a/lib/AppHost/Observability/MetricSample.php b/lib/AppHost/Observability/MetricSample.php new file mode 100644 index 0000000000..db2f4dcd76 --- /dev/null +++ b/lib/AppHost/Observability/MetricSample.php @@ -0,0 +1,77 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +/** + * One metric family: a name + type + HELP plus its labelled samples. + * + * @psalm-immutable + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.1 + */ +final class MetricSample +{ + /** + * Constructor. + * + * @param string $name Metric name (without `{app}_` prefix). + * @param string $type Prometheus type (gauge|counter). + * @param string $help HELP text. + * @param array, value: float|int}> $samples Labelled samples. + */ + public function __construct( + public readonly string $name, + public readonly string $type, + public readonly string $help, + public readonly array $samples + ) { + }//end __construct() + + /** + * Convenience factory for a single unlabelled sample. + * + * @param string $name Metric name. + * @param string $type Prometheus type. + * @param string $help HELP text. + * @param float|int $value Sample value. + * @param array $labels Optional labels. + * + * @return self + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.1 + */ + public static function single(string $name, string $type, string $help, float|int $value, array $labels=[]): self + { + return new self( + name: $name, + type: $type, + help: $help, + samples: [['labels' => $labels, 'value' => $value]] + ); + }//end single() +}//end class diff --git a/lib/AppHost/Observability/MetricSourceInterface.php b/lib/AppHost/Observability/MetricSourceInterface.php new file mode 100644 index 0000000000..b68a6e9db5 --- /dev/null +++ b/lib/AppHost/Observability/MetricSourceInterface.php @@ -0,0 +1,54 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +/** + * One metric-source executor. + */ +interface MetricSourceInterface +{ + /** + * The source kind this executor handles (MetricDescriptor::KINDS). + * + * @return string + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.2 + */ + public function kind(): string; + + /** + * Execute the descriptor for the given app and return its samples. + * + * @param string $appId Calling app id. + * @param MetricDescriptor $descriptor The metric descriptor. + * + * @return MetricSample[] One or more metric families (usually one). + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.2 + */ + public function collect(string $appId, MetricDescriptor $descriptor): array; +}//end interface diff --git a/lib/AppHost/Observability/MetricsEngine.php b/lib/AppHost/Observability/MetricsEngine.php new file mode 100644 index 0000000000..62ffebed76 --- /dev/null +++ b/lib/AppHost/Observability/MetricsEngine.php @@ -0,0 +1,255 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +use OCA\OpenRegister\AppHost\Observability\Source\AppConfigMetricSource; +use OCA\OpenRegister\AppHost\Observability\Source\ObjectMetricSource; +use OCA\OpenRegister\AppHost\Observability\Source\ProviderMetricSource; +use OCA\OpenRegister\AppHost\Observability\Source\TableMetricSource; +use OCP\ICacheFactory; +use OCP\IConfig; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Renders an app's declarative + implicit metrics to Prometheus text. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.1 + */ +class MetricsEngine +{ + /** + * Constructor. + * + * @param ObjectMetricSource $objectSource objectCount/objectSum. + * @param TableMetricSource $tableSource tableCount. + * @param AppConfigMetricSource $appConfigSource appConfig. + * @param ProviderMetricSource $providerSource provider escape hatch. + * @param PrometheusRenderer $renderer Text renderer. + * @param ManifestLoader $manifestLoader App version resolution. + * @param ICacheFactory $cacheFactory Distributed cache (cacheTtl). + * @param IConfig $config NC version (implicit info). + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly ObjectMetricSource $objectSource, + private readonly TableMetricSource $tableSource, + private readonly AppConfigMetricSource $appConfigSource, + private readonly ProviderMetricSource $providerSource, + private readonly PrometheusRenderer $renderer, + private readonly ManifestLoader $manifestLoader, + private readonly ICacheFactory $cacheFactory, + private readonly IConfig $config, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Render the full Prometheus exposition for an app's manifest. + * + * @param ObservabilityManifest $manifest The app's observability config. + * + * @return string Prometheus text 0.0.4. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.1 + */ + public function render(ObservabilityManifest $manifest): string + { + $samples = $this->implicitMetrics(appId: $manifest->appId); + + foreach ($manifest->metrics as $descriptor) { + foreach ($this->collectDescriptor(appId: $manifest->appId, descriptor: $descriptor) as $sample) { + $samples[] = $sample; + } + } + + return $this->renderer->render(appId: $manifest->appId, samples: $samples); + }//end render() + + /** + * Collect one descriptor, honouring cacheTtl. + * + * @param string $appId Calling app id. + * @param MetricDescriptor $descriptor Metric descriptor. + * + * @return MetricSample[] + */ + private function collectDescriptor(string $appId, MetricDescriptor $descriptor): array + { + if ($descriptor->cacheTtl <= 0 || $this->cacheFactory->isAvailable() === false) { + return $this->dispatch(appId: $appId, descriptor: $descriptor); + } + + $cache = $this->cacheFactory->createDistributed('openregister_apphost_metrics'); + $cacheKey = $appId.':'.$descriptor->name; + + $cached = $cache->get($cacheKey); + if (is_string($cached) === true) { + $restored = $this->decodeSamples(payload: $cached); + if ($restored !== null) { + return $restored; + } + } + + $samples = $this->dispatch(appId: $appId, descriptor: $descriptor); + $cache->set($cacheKey, $this->encodeSamples(samples: $samples), $descriptor->cacheTtl); + return $samples; + }//end collectDescriptor() + + /** + * Route a descriptor to its source. + * + * @param string $appId Calling app id. + * @param MetricDescriptor $descriptor Metric descriptor. + * + * @return MetricSample[] + */ + private function dispatch(string $appId, MetricDescriptor $descriptor): array + { + try { + switch ($descriptor->kind) { + case 'objectCount': + case 'objectSum': + return $this->objectSource->collect(appId: $appId, descriptor: $descriptor); + case 'tableCount': + return $this->tableSource->collect(appId: $appId, descriptor: $descriptor); + case 'appConfig': + return $this->appConfigSource->collect(appId: $appId, descriptor: $descriptor); + case 'provider': + return $this->providerSource->collect(appId: $appId, descriptor: $descriptor); + default: + return []; + } + } catch (Throwable $e) { + $logMessage = sprintf( + '[AppHost\\Metrics] metric "%s" (kind %s, app %s) failed: %s', + $descriptor->name, + $descriptor->kind, + $appId, + $e->getMessage() + ); + $this->logger->warning( + message: $logMessage, + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return []; + }//end try + }//end dispatch() + + /** + * Build the implicit `{app}_info` and `{app}_up` metrics. + * + * @param string $appId Calling app id. + * + * @return MetricSample[] + */ + private function implicitMetrics(string $appId): array + { + $version = $this->manifestLoader->appVersion(appId: $appId); + $ncVersion = $this->config->getSystemValueString('version', 'unknown'); + + $info = new MetricSample( + name: 'info', + type: 'gauge', + help: 'Application information', + samples: [ + [ + 'labels' => [ + 'version' => $version, + 'php_version' => PHP_VERSION, + 'nextcloud_version' => $ncVersion, + ], + 'value' => 1, + ], + ] + ); + + $up = MetricSample::single(name: 'up', type: 'gauge', help: 'Whether the application is up', value: 1); + + return [$info, $up]; + }//end implicitMetrics() + + /** + * Encode samples for the cache. + * + * @param MetricSample[] $samples Samples. + * + * @return string JSON payload. + */ + private function encodeSamples(array $samples): string + { + $plain = []; + foreach ($samples as $sample) { + $plain[] = [ + 'name' => $sample->name, + 'type' => $sample->type, + 'help' => $sample->help, + 'samples' => $sample->samples, + ]; + } + + return (string) json_encode($plain); + }//end encodeSamples() + + /** + * Decode cached samples. + * + * @param string $payload JSON payload. + * + * @return MetricSample[]|null Null when the payload is unusable. + */ + private function decodeSamples(string $payload): ?array + { + $decoded = json_decode($payload, associative: true); + if (is_array($decoded) === false) { + return null; + } + + $samples = []; + foreach ($decoded as $row) { + if (is_array($row) === false || isset($row['name'], $row['type'], $row['samples']) === false) { + return null; + } + + $rowSamples = []; + if (is_array($row['samples']) === true) { + $rowSamples = $row['samples']; + } + + $samples[] = new MetricSample( + name: (string) $row['name'], + type: (string) $row['type'], + help: (string) ($row['help'] ?? ''), + samples: $rowSamples + ); + } + + return $samples; + }//end decodeSamples() +}//end class diff --git a/lib/AppHost/Observability/ObservabilityManifest.php b/lib/AppHost/Observability/ObservabilityManifest.php new file mode 100644 index 0000000000..40a485c64f --- /dev/null +++ b/lib/AppHost/Observability/ObservabilityManifest.php @@ -0,0 +1,248 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +/** + * Validated observability configuration for a single host app. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.2 + */ +final class ObservabilityManifest +{ + /** + * Default status-code policy (ADR-006: 503 on critical failure). + */ + public const POLICY_ADR006 = 'adr006'; + + /** + * Always-200 policy (decidesk REQ-API-004 reverse-proxy contract). + */ + public const POLICY_ALWAYS200 = 'always200'; + + /** + * Closed set of supported status-code policies. + * + * @var string[] + */ + public const POLICIES = [self::POLICY_ADR006, self::POLICY_ALWAYS200]; + + /** + * Constructor. + * + * @param string $appId Calling app id. + * @param HealthCheckDescriptor[] $checks Parsed health checks. + * @param MetricDescriptor[] $metrics Parsed metrics. + * @param string $statusCodePolicy adr006|always200. + * @param bool $cors Emit CORS headers when true. + * @param string[] $diagnostics Validation diagnostics (block fell back to defaults). + */ + public function __construct( + public readonly string $appId, + public readonly array $checks, + public readonly array $metrics, + public readonly string $statusCodePolicy=self::POLICY_ADR006, + public readonly bool $cors=false, + public readonly array $diagnostics=[] + ) { + }//end __construct() + + /** + * Build an observability config from a decoded manifest. + * + * Invalid descriptors do not throw out of this method: they are collected + * into `$diagnostics` and the config falls back to safe defaults so the + * endpoint never 500s on a bad manifest (Descriptor Validation requirement). + * + * @param string $appId Calling app id. + * @param array $manifest Decoded manifest.json. + * + * @return self + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.2 + */ + public static function fromManifest(string $appId, array $manifest): self + { + $block = $manifest['observability'] ?? null; + if (is_array($block) === false) { + return self::defaults(appId: $appId, manifest: $manifest); + } + + $diagnostics = []; + + // Health section. + $statusCodePolicy = self::POLICY_ADR006; + $cors = false; + $checks = []; + $healthValid = true; + + $health = $block['health'] ?? null; + if (is_array($health) === true) { + $policy = $health['statusCodePolicy'] ?? self::POLICY_ADR006; + if (is_string($policy) === true && in_array($policy, self::POLICIES, true) === true) { + $statusCodePolicy = $policy; + } else { + $shownPolicy = gettype($policy); + if (is_scalar($policy) === true) { + $shownPolicy = (string) $policy; + } + + $diagnostics[] = sprintf('Invalid statusCodePolicy "%s"; defaulting to adr006.', $shownPolicy); + } + + $cors = ($health['cors'] ?? false) === true; + + $rawChecks = $health['checks'] ?? []; + if (is_array($rawChecks) === true) { + foreach ($rawChecks as $index => $rawCheck) { + if (is_array($rawCheck) === false) { + $diagnostics[] = sprintf('Health check #%d is not an object.', $index); + $healthValid = false; + continue; + } + + try { + $checks[] = HealthCheckDescriptor::fromArray(raw: $rawCheck, index: (int) $index); + } catch (ObservabilityValidationException $e) { + $diagnostics[] = $e->getMessage(); + $healthValid = false; + } + } + } else { + $diagnostics[] = 'observability.health.checks must be an array.'; + $healthValid = false; + } + }//end if + + // If the health block was malformed enough to lose checks, fall back to default health. + if ($healthValid === false || $checks === []) { + $checks = self::defaultChecks(manifest: $manifest); + } + + // Metrics section. + $metrics = []; + $rawMetrics = $block['metrics'] ?? []; + if (is_array($rawMetrics) === true) { + foreach ($rawMetrics as $index => $rawMetric) { + if (is_array($rawMetric) === false) { + $diagnostics[] = sprintf('Metric #%d is not an object.', $index); + continue; + } + + try { + $metrics[] = MetricDescriptor::fromArray(raw: $rawMetric); + } catch (ObservabilityValidationException $e) { + // Skip the single bad metric; implicit info/up always still serve. + $diagnostics[] = $e->getMessage(); + } + } + } else { + $diagnostics[] = 'observability.metrics must be an array.'; + } + + return new self( + appId: $appId, + checks: $checks, + metrics: $metrics, + statusCodePolicy: $statusCodePolicy, + cors: $cors, + diagnostics: $diagnostics + ); + }//end fromManifest() + + /** + * Build the default config for an app with no (or an unusable) + * observability block: a `database` check, plus `orAvailable` when the + * manifest declares OpenRegister registers; metrics = implicit only. + * + * @param string $appId Calling app id. + * @param array $manifest Decoded manifest.json. + * + * @return self + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.2 + */ + public static function defaults(string $appId, array $manifest): self + { + return new self( + appId: $appId, + checks: self::defaultChecks(manifest: $manifest), + metrics: [], + statusCodePolicy: self::POLICY_ADR006, + cors: false, + diagnostics: [] + ); + }//end defaults() + + /** + * Build the default health check list from a manifest. + * + * @param array $manifest Decoded manifest.json. + * + * @return HealthCheckDescriptor[] + */ + private static function defaultChecks(array $manifest): array + { + $checks = [ + new HealthCheckDescriptor(id: 'database', type: 'database', severity: 'critical'), + ]; + + if (self::declaresOpenRegisterRegisters(manifest: $manifest) === true) { + $checks[] = new HealthCheckDescriptor(id: 'or', type: 'orAvailable', severity: 'critical'); + } + + return $checks; + }//end defaultChecks() + + /** + * Heuristic: does the manifest declare OpenRegister registers? Looks at the + * common manifest shapes (`registers`, `openregister.registers`, + * `config.registers`) so the default `orAvailable` check is added for + * OR-backed apps. + * + * @param array $manifest Decoded manifest.json. + * + * @return bool + */ + private static function declaresOpenRegisterRegisters(array $manifest): bool + { + if (isset($manifest['registers']) === true && empty($manifest['registers']) === false) { + return true; + } + + $openregister = $manifest['openregister'] ?? null; + if (is_array($openregister) === true && empty($openregister['registers']) === false) { + return true; + } + + $config = $manifest['config'] ?? null; + if (is_array($config) === true && empty($config['registers']) === false) { + return true; + } + + return false; + }//end declaresOpenRegisterRegisters() +}//end class diff --git a/lib/AppHost/Observability/ObservabilityValidationException.php b/lib/AppHost/Observability/ObservabilityValidationException.php new file mode 100644 index 0000000000..0df262777f --- /dev/null +++ b/lib/AppHost/Observability/ObservabilityValidationException.php @@ -0,0 +1,37 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +/** + * Raised on a malformed observability descriptor (manifest-validation time). + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-1.1 + */ +class ObservabilityValidationException extends \InvalidArgumentException +{ +}//end class diff --git a/lib/AppHost/Observability/PrometheusRenderer.php b/lib/AppHost/Observability/PrometheusRenderer.php new file mode 100644 index 0000000000..c43c8716b8 --- /dev/null +++ b/lib/AppHost/Observability/PrometheusRenderer.php @@ -0,0 +1,175 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability; + +/** + * Renders metric samples to Prometheus text exposition format 0.0.4. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.1 + */ +class PrometheusRenderer +{ + /** + * The Content-Type for Prometheus text exposition 0.0.4. + */ + public const CONTENT_TYPE = 'text/plain; version=0.0.4; charset=utf-8'; + + /** + * Render a list of metric families for a given app prefix. + * + * @param string $appId Calling app id (used for the metric prefix). + * @param MetricSample[] $samples Metric families. + * + * @return string Prometheus text exposition. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.1 + */ + public function render(string $appId, array $samples): string + { + $prefix = $this->sanitizePrefix(appId: $appId); + $lines = []; + + foreach ($samples as $sample) { + $metricName = $prefix.'_'.$this->sanitizeMetricName(name: $sample->name); + $metricType = 'gauge'; + if ($sample->type === 'counter') { + $metricType = 'counter'; + } + + $lines[] = '# HELP '.$metricName.' '.$this->escapeHelp(text: $sample->help); + $lines[] = '# TYPE '.$metricName.' '.$metricType; + + foreach ($sample->samples as $point) { + $labels = $point['labels']; + $value = $point['value']; + $lines[] = $metricName.$this->renderLabels(labels: $labels).' '.$this->renderValue(value: $value); + } + + $lines[] = ''; + }//end foreach + + return implode("\n", $lines)."\n"; + }//end render() + + /** + * Sanitise the app id into a Prometheus-safe metric prefix `[a-z0-9_]`. + * + * @param string $appId Calling app id. + * + * @return string + */ + private function sanitizePrefix(string $appId): string + { + $prefix = strtolower($appId); + $prefix = preg_replace('/[^a-z0-9_]/', '_', $prefix); + $prefix = trim((string) $prefix, '_'); + if ($prefix === '' || preg_match('/^[a-z_]/', $prefix) !== 1) { + $prefix = 'app_'.$prefix; + } + + return $prefix; + }//end sanitizePrefix() + + /** + * Sanitise a metric name body to `[a-zA-Z0-9_]`. + * + * @param string $name Metric name. + * + * @return string + */ + private function sanitizeMetricName(string $name): string + { + return (string) preg_replace('/[^a-zA-Z0-9_]/', '_', $name); + }//end sanitizeMetricName() + + /** + * Render a label set into `{k="v",...}` (or empty string). + * + * @param array $labels Label map. + * + * @return string + */ + private function renderLabels(array $labels): string + { + if ($labels === []) { + return ''; + } + + $parts = []; + foreach ($labels as $key => $value) { + $safeKey = (string) preg_replace('/[^a-zA-Z0-9_]/', '_', (string) $key); + if (preg_match('/^[a-zA-Z_]/', $safeKey) !== 1) { + $safeKey = '_'.$safeKey; + } + + $parts[] = $safeKey.'="'.$this->escapeLabelValue(value: (string) $value).'"'; + } + + return '{'.implode(',', $parts).'}'; + }//end renderLabels() + + /** + * Escape a label value per the exposition format (backslash, quote, newline). + * + * @param string $value Raw value. + * + * @return string + */ + private function escapeLabelValue(string $value): string + { + return str_replace(['\\', '"', "\n"], ['\\\\', '\\"', '\\n'], $value); + }//end escapeLabelValue() + + /** + * Escape HELP text (backslash + newline). + * + * @param string $text Help text. + * + * @return string + */ + private function escapeHelp(string $text): string + { + return str_replace(['\\', "\n"], ['\\\\', '\\n'], $text); + }//end escapeHelp() + + /** + * Render a numeric value (ints stay integral, floats keep precision). + * + * @param float|int $value Value. + * + * @return string + */ + private function renderValue(float|int $value): string + { + if (is_int($value) === true || (float) (int) $value === (float) $value) { + return (string) (int) $value; + } + + return (string) $value; + }//end renderValue() +}//end class diff --git a/lib/AppHost/Observability/Source/AppConfigMetricSource.php b/lib/AppHost/Observability/Source/AppConfigMetricSource.php new file mode 100644 index 0000000000..10b51f2f13 --- /dev/null +++ b/lib/AppHost/Observability/Source/AppConfigMetricSource.php @@ -0,0 +1,88 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability\Source; + +use OCA\OpenRegister\AppHost\Observability\MetricDescriptor; +use OCA\OpenRegister\AppHost\Observability\MetricSample; +use OCA\OpenRegister\AppHost\Observability\MetricSourceInterface; +use OCP\IAppConfig; + +/** + * AppConfig integer-value source. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.4 + */ +class AppConfigMetricSource implements MetricSourceInterface +{ + /** + * Constructor. + * + * @param IAppConfig $appConfig App config reader. + */ + public function __construct( + private readonly IAppConfig $appConfig + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.4 + */ + public function kind(): string + { + return 'appConfig'; + }//end kind() + + /** + * {@inheritDoc} + * + * @param string $appId Calling app id. + * @param MetricDescriptor $descriptor The metric descriptor. + * + * @return MetricSample[] + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.4 + */ + public function collect(string $appId, MetricDescriptor $descriptor): array + { + $key = (string) $descriptor->source['key']; + $help = $descriptor->help ?? sprintf('App config value for %s', $key); + $value = $this->appConfig->getValueInt($appId, $key, 0); + + return [ + MetricSample::single( + name: $descriptor->name, + type: $descriptor->type, + help: $help, + value: $value + ), + ]; + }//end collect() +}//end class diff --git a/lib/AppHost/Observability/Source/ObjectMetricSource.php b/lib/AppHost/Observability/Source/ObjectMetricSource.php new file mode 100644 index 0000000000..09e8c19331 --- /dev/null +++ b/lib/AppHost/Observability/Source/ObjectMetricSource.php @@ -0,0 +1,342 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability\Source; + +use OCA\OpenRegister\AppHost\Observability\MetricDescriptor; +use OCA\OpenRegister\AppHost\Observability\MetricSample; +use OCA\OpenRegister\AppHost\Observability\MetricSourceInterface; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Object-count / object-sum source via OR's portable aggregation layer. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.2 + */ +class ObjectMetricSource implements MetricSourceInterface +{ + /** + * Constructor. + * + * @param ObjectService $objectService OR object service (aggregation). + * @param RegisterMapper $registerMapper Slug → register id. + * @param SchemaMapper $schemaMapper Slug → schema id. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.2 + */ + public function kind(): string + { + // This source handles two kinds; the engine routes both here. + return 'objectCount'; + }//end kind() + + /** + * {@inheritDoc} + * + * @param string $appId Calling app id. + * @param MetricDescriptor $descriptor The metric descriptor. + * + * @return MetricSample[] + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.2 + */ + public function collect(string $appId, MetricDescriptor $descriptor): array + { + $source = $descriptor->source; + $help = $descriptor->help ?? sprintf('%s for %s', $descriptor->kind, $source['schema'] ?? 'objects'); + + try { + $baseQuery = $this->buildBaseQuery(source: $source); + } catch (Throwable $e) { + $logMessage = sprintf( + '[AppHost\\Metrics] objectCount "%s" (app %s) could not resolve register/schema: %s', + $descriptor->name, + $appId, + $e->getMessage() + ); + $this->logger->warning( + message: $logMessage, + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return [new MetricSample(name: $descriptor->name, type: $descriptor->type, help: $help, samples: [])]; + } + + $groupBy = $source['groupBy'] ?? []; + $field = null; + if ($descriptor->kind === 'objectSum') { + $field = (string) $source['field']; + } + + if (is_array($groupBy) === true && $groupBy !== []) { + $samples = $this->collectGrouped(baseQuery: $baseQuery, groupBy: array_values($groupBy), field: $field); + } else { + $samples = [['labels' => [], 'value' => $this->aggregate(query: $baseQuery, field: $field)]]; + } + + return [new MetricSample(name: $descriptor->name, type: $descriptor->type, help: $help, samples: $samples)]; + }//end collect() + + /** + * Build the base search query (register/schema numeric ids + filters). + * + * @param array $source Validated source block. + * + * @return array + * + * @throws \OCP\AppFramework\Db\DoesNotExistException When the schema slug is unknown. + */ + private function buildBaseQuery(array $source): array + { + $schemaId = $this->schemaMapper->find($source['schema'], _rbac: false, _multitenancy: false)->getId(); + + $self = ['schema' => $schemaId]; + if (isset($source['register']) === true && $source['register'] !== '') { + $registerId = $this->registerMapper->find($source['register'], _rbac: false, _multitenancy: false)->getId(); + $self['register'] = $registerId; + } + + $query = ['@self' => $self]; + + foreach (($source['filter'] ?? []) as $fieldName => $ops) { + foreach ($ops as $operator => $value) { + $this->applyFilter(query: $query, field: (string) $fieldName, operator: (string) $operator, value: $value); + } + } + + return $query; + }//end buildBaseQuery() + + /** + * Apply one filter operator into the MagicMapper query shape, resolving + * `now`/`today` date tokens server-side. + * + * @param array $query Query (by reference). + * @param string $field Object field name. + * @param string $operator eq|neq|lt|lte|gt|gte|like. + * @param mixed $value Raw value (may be a date token). + * + * @return void + */ + private function applyFilter(array &$query, string $field, string $operator, mixed $value): void + { + $resolved = $this->resolveValue(value: $value); + + switch ($operator) { + case 'eq': + $query[$field] = $resolved; + break; + case 'neq': + $query[$field] = ['ne' => $resolved]; + break; + case 'lt': + case 'lte': + case 'gt': + case 'gte': + $query[$field] = ($query[$field] ?? []); + $query[$field][$operator] = $resolved; + break; + case 'like': + $query[$field] = ['like' => $resolved]; + break; + } + }//end applyFilter() + + /** + * Resolve a filter value, expanding the `now` / `today` date tokens. + * + * @param mixed $value Raw value. + * + * @return mixed Resolved value. + */ + private function resolveValue(mixed $value): mixed + { + if ($value === 'now') { + return (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s'); + } + + if ($value === 'today') { + return (new \DateTimeImmutable('today'))->format('Y-m-d'); + } + + return $value; + }//end resolveValue() + + /** + * Aggregate the query: COUNT, or SUM of a numeric field for objectSum. + * + * @param array $query Search query. + * @param string|null $field Numeric field for objectSum (null = count). + * + * @return float|int + */ + private function aggregate(array $query, ?string $field): float|int + { + if ($field === null) { + return $this->objectService->countSearchObjects(query: $query, _rbac: false, _multitenancy: false); + } + + // The objectSum kind sums a numeric JSON field portably via the search + // layer (no dialect-specific SUM SQL leaks into this engine). + $objects = $this->objectService->searchObjects(query: $query, _rbac: false, _multitenancy: false); + $sum = 0.0; + foreach ($objects as $object) { + $data = $this->extractObjectData(object: $object); + $value = $data[$field] ?? 0; + if (is_numeric($value) === true) { + $sum += (float) $value; + } + } + + return $sum; + }//end aggregate() + + /** + * Extract the data array from a search result row, which may be a plain + * array or an ObjectEntity exposing getObject(). + * + * @param mixed $object Search result row. + * + * @return array + */ + private function extractObjectData(mixed $object): array + { + if (is_array($object) === true) { + return $object; + } + + if (is_object($object) === true && method_exists($object, 'getObject') === true) { + $data = $object->getObject(); + if (is_array($data) === true) { + return $data; + } + } + + return []; + }//end extractObjectData() + + /** + * Collect grouped samples by faceting over the groupBy fields. + * + * @param array $baseQuery Base query. + * @param string[] $groupBy Group-by JSON fields. + * @param string|null $field Numeric field for objectSum. + * + * @return array, value: float|int}> + */ + private function collectGrouped(array $baseQuery, array $groupBy, ?string $field): array + { + return $this->expandGroups(query: $baseQuery, remaining: $groupBy, labels: [], field: $field); + }//end collectGrouped() + + /** + * Recursively expand group-by fields into labelled samples. For each field + * we facet to discover its distinct values, then descend with each value + * pinned as an equality filter. Pure OR aggregation — no JSON SQL here. + * + * @param array $query Current query. + * @param string[] $remaining Remaining group-by fields. + * @param array $labels Accumulated labels. + * @param string|null $field Numeric field for objectSum. + * + * @return array, value: float|int}> + */ + private function expandGroups(array $query, array $remaining, array $labels, ?string $field): array + { + if ($remaining === []) { + $value = $this->aggregate(query: $query, field: $field); + if ($value === 0 || $value === 0.0) { + return []; + } + + return [['labels' => $labels, 'value' => $value]]; + } + + $groupField = array_shift($remaining); + $buckets = $this->facetValues(query: $query, field: $groupField); + + $samples = []; + foreach ($buckets as $bucketValue) { + $scoped = $query; + $scoped[$groupField] = $bucketValue; + $childLabels = ($labels + [$groupField => (string) $bucketValue]); + foreach ($this->expandGroups(query: $scoped, remaining: $remaining, labels: $childLabels, field: $field) as $sample) { + $samples[] = $sample; + } + } + + return $samples; + }//end expandGroups() + + /** + * Discover the distinct values of a field via OR's facet layer. + * + * @param array $query Current query. + * @param string $field Field to facet. + * + * @return array Distinct values. + */ + private function facetValues(array $query, string $field): array + { + $facetQuery = $query; + $facetQuery['_facets'] = [$field => ['type' => 'terms']]; + + $facets = $this->objectService->getFacetsForObjects(query: $facetQuery); + + // External standardized format: { "_": { data: { buckets: [{value,count}] } } }. + $node = $facets['_'.$field] ?? $facets[$field] ?? null; + $buckets = $node['data']['buckets'] ?? $node['buckets'] ?? []; + + $values = []; + foreach ($buckets as $bucket) { + $value = $bucket['value'] ?? $bucket['key'] ?? null; + if ($value !== null && $value !== '') { + $values[] = (string) $value; + } + } + + return $values; + }//end facetValues() +}//end class diff --git a/lib/AppHost/Observability/Source/ProviderMetricSource.php b/lib/AppHost/Observability/Source/ProviderMetricSource.php new file mode 100644 index 0000000000..62e0aa47a5 --- /dev/null +++ b/lib/AppHost/Observability/Source/ProviderMetricSource.php @@ -0,0 +1,149 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability\Source; + +use OCA\OpenRegister\AppHost\IMetricsProvider; +use OCA\OpenRegister\AppHost\Observability\MetricDescriptor; +use OCA\OpenRegister\AppHost\Observability\MetricSample; +use OCA\OpenRegister\AppHost\Observability\MetricSourceInterface; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Provider escape-hatch source via container-alias discovery. + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.5 + */ +class ProviderMetricSource implements MetricSourceInterface +{ + /** + * Constructor. + * + * @param ContainerInterface $container DI container (alias lookup). + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly ContainerInterface $container, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.5 + */ + public function kind(): string + { + return 'provider'; + }//end kind() + + /** + * {@inheritDoc} + * + * @param string $appId Calling app id. + * @param MetricDescriptor $descriptor The metric descriptor (no params). + * + * @return MetricSample[] + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.5 + */ + public function collect(string $appId, MetricDescriptor $descriptor): array + { + $alias = IMetricsProvider::class.'::'.$appId; + + try { + // Resolve the provider from the CALLING APP's own container — where + // the app registered its `IMetricsProvider::` alias — not from + // OpenRegister's container, which never sees another app's DI + // registrations (Nextcloud app containers are isolated). Resolving + // against `$this->container` here silently returned [] for every + // consuming app (same root cause as the MCP-provider fix, #390). + $container = $this->resolveAppContainer(appId: $appId); + + if ($container->has($alias) === false) { + return []; + } + + $provider = $container->get($alias); + if (($provider instanceof IMetricsProvider) === false) { + return []; + } + + $samples = []; + foreach ($provider->metrics() as $sample) { + if ($sample instanceof MetricSample) { + $samples[] = $sample; + } + } + + return $samples; + } catch (Throwable $e) { + $this->logger->warning( + message: sprintf('[AppHost\\Metrics] provider for app "%s" threw: %s', $appId, $e->getMessage()), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return []; + }//end try + }//end collect() + + /** + * Resolve the calling app's OWN DI container so its + * `IMetricsProvider::` alias resolves from where the leaf app + * actually registered it (#390). + * + * Wraps `\OC::$server->getRegisteredAppContainer($appId)`, which throws + * when the app has no bootstrapped container. Fail-soft: on any error the + * OpenRegister container is returned so a single misbehaving app never + * fatals a metrics scrape (its alias simply will not resolve there). + * + * @param string $appId The calling app id. + * + * @return ContainerInterface The app's own container, or OpenRegister's as a fallback. + */ + private function resolveAppContainer(string $appId): ContainerInterface + { + try { + $appContainer = \OC::$server->getRegisteredAppContainer($appId); + if ($appContainer instanceof ContainerInterface) { + return $appContainer; + } + } catch (Throwable $e) { + $this->logger->debug( + message: sprintf('[AppHost\\Metrics] no registered app container for "%s": %s', $appId, $e->getMessage()), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + }//end try + + return $this->container; + }//end resolveAppContainer() +}//end class diff --git a/lib/AppHost/Observability/Source/TableMetricSource.php b/lib/AppHost/Observability/Source/TableMetricSource.php new file mode 100644 index 0000000000..8642c0b2fb --- /dev/null +++ b/lib/AppHost/Observability/Source/TableMetricSource.php @@ -0,0 +1,263 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Observability\Source; + +use OCA\OpenRegister\AppHost\Observability\MetricDescriptor; +use OCA\OpenRegister\AppHost\Observability\MetricSample; +use OCA\OpenRegister\AppHost\Observability\MetricSourceInterface; +use OCA\OpenRegister\AppHost\Observability\ObservabilityValidationException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Table-count source via QueryBuilder (aggregate-only, allowlisted table). + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.3 + */ +class TableMetricSource implements MetricSourceInterface +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly IDBConnection $db, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.3 + */ + public function kind(): string + { + return 'tableCount'; + }//end kind() + + /** + * {@inheritDoc} + * + * @param string $appId Calling app id. + * @param MetricDescriptor $descriptor The metric descriptor. + * + * @return MetricSample[] + * + * @spec openspec/changes/apphost-observability-engine/tasks.md#task-3.3 + */ + public function collect(string $appId, MetricDescriptor $descriptor): array + { + $source = $descriptor->source; + $table = (string) ($source['table'] ?? ''); + $help = $descriptor->help ?? sprintf('Row count of %s', $table); + + // SECURITY BOUNDARY (re-enforced): only [a-z0-9_] table names reach the + // QueryBuilder. The descriptor validator already rejects others; this is + // defence in depth so the source can never be misused in isolation. + if (preg_match(MetricDescriptor::TABLE_REGEX, $table) !== 1) { + $errorMessage = sprintf('tableCount table "%s" is not allowlisted.', $table); + throw new ObservabilityValidationException(message: $errorMessage); + } + + // Graceful zero-emission when the table does not exist (drained-table parity). + if ($this->db->tableExists($table) === false) { + return [new MetricSample(name: $descriptor->name, type: $descriptor->type, help: $help, samples: [])]; + } + + $groupBy = array_values($source['groupBy'] ?? []); + $labelMap = $source['labelMap'] ?? []; + $labelDefaults = $source['labelDefaults'] ?? []; + + try { + if ($groupBy === []) { + $value = $this->countAll(table: $table, filter: $source['filter'] ?? []); + $samples = [['labels' => [], 'value' => $value]]; + return [new MetricSample(name: $descriptor->name, type: $descriptor->type, help: $help, samples: $samples)]; + } + + $samples = $this->countGrouped( + table: $table, + groupBy: $groupBy, + filter: $source['filter'] ?? [], + labelMap: $labelMap, + labelDefaults: $labelDefaults + ); + + return [new MetricSample(name: $descriptor->name, type: $descriptor->type, help: $help, samples: $samples)]; + } catch (Throwable $e) { + $this->logger->warning( + message: sprintf('[AppHost\\Metrics] tableCount "%s" (app %s) failed: %s', $descriptor->name, $appId, $e->getMessage()), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return [new MetricSample(name: $descriptor->name, type: $descriptor->type, help: $help, samples: [])]; + }//end try + }//end collect() + + /** + * COUNT(*) of the table with optional filters. + * + * @param string $table Allowlisted table name. + * @param array $filter Column filters. + * + * @return int + * + * @throws \OCP\DB\Exception On DB error. + */ + private function countAll(string $table, array $filter): int + { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*', 'cnt'))->from($table); + $this->applyFilters(qb: $qb, filter: $filter); + + $result = $qb->executeQuery(); + $count = (int) $result->fetchOne(); + $result->closeCursor(); + return $count; + }//end countAll() + + /** + * COUNT(*) GROUP BY the given columns, with optional filters. + * + * @param string $table Allowlisted table name. + * @param string[] $groupBy Columns to group by. + * @param array $filter Column filters. + * @param array $labelMap Column → label rename. + * @param array $labelDefaults Default value for NULL/empty columns. + * + * @return array, value: int}> + * + * @throws \OCP\DB\Exception On DB error. + */ + private function countGrouped(string $table, array $groupBy, array $filter, array $labelMap, array $labelDefaults): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*', 'cnt')); + + foreach ($groupBy as $column) { + $this->assertColumn(column: (string) $column); + $qb->addSelect((string) $column)->addGroupBy((string) $column); + } + + $qb->from($table); + $this->applyFilters(qb: $qb, filter: $filter); + + $result = $qb->executeQuery(); + $samples = []; + foreach ($result->fetchAll() as $row) { + $labels = []; + foreach ($groupBy as $column) { + $column = (string) $column; + $raw = $row[$column] ?? null; + + $value = $raw; + if ($raw === null || $raw === '') { + $value = ($labelDefaults[$column] ?? ''); + } + + $labelKey = $column; + if (isset($labelMap[$column]) === true) { + $labelKey = (string) $labelMap[$column]; + } + + $labels[$labelKey] = (string) $value; + } + + $samples[] = ['labels' => $labels, 'value' => (int) ($row['cnt'] ?? 0)]; + }//end foreach + + $result->closeCursor(); + return $samples; + }//end countGrouped() + + /** + * Apply column filters (eq/neq/lt/lte/gt/gte/like) to the query builder. + * + * @param IQueryBuilder $qb Query builder (by reference via object). + * @param array $filter Column filters. + * + * @return void + */ + private function applyFilters(IQueryBuilder $qb, array $filter): void + { + foreach ($filter as $column => $ops) { + $this->assertColumn(column: (string) $column); + foreach ($ops as $operator => $value) { + $param = $qb->createNamedParameter($value); + switch ($operator) { + case 'eq': + $qb->andWhere($qb->expr()->eq((string) $column, $param)); + break; + case 'neq': + $qb->andWhere($qb->expr()->neq((string) $column, $param)); + break; + case 'lt': + $qb->andWhere($qb->expr()->lt((string) $column, $param)); + break; + case 'lte': + $qb->andWhere($qb->expr()->lte((string) $column, $param)); + break; + case 'gt': + $qb->andWhere($qb->expr()->gt((string) $column, $param)); + break; + case 'gte': + $qb->andWhere($qb->expr()->gte((string) $column, $param)); + break; + case 'like': + $qb->andWhere($qb->expr()->like((string) $column, $param)); + break; + }//end switch + }//end foreach + }//end foreach + }//end applyFilters() + + /** + * Assert a column name is a safe identifier before it touches the builder. + * + * @param string $column Column name. + * + * @return void + * + * @throws ObservabilityValidationException When the column name is unsafe. + */ + private function assertColumn(string $column): void + { + if (preg_match('/^[a-zA-Z0-9_]+$/', $column) !== 1) { + $errorMessage = sprintf('Unsafe column identifier "%s".', $column); + throw new ObservabilityValidationException(message: $errorMessage); + } + }//end assertColumn() +}//end class diff --git a/lib/AppHost/Repair/GenericInitializeActions.php b/lib/AppHost/Repair/GenericInitializeActions.php new file mode 100644 index 0000000000..f4d6570bc1 --- /dev/null +++ b/lib/AppHost/Repair/GenericInitializeActions.php @@ -0,0 +1,148 @@ +`. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Repair + * @package OCA\OpenRegister\AppHost\Repair + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Repair; + +use OCA\OpenRegister\AppHost\Service\GenericActionAuthService; +use OCP\App\IAppManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Generic repair step that seeds a leaf app's ADR-023 action matrix. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.2 + */ +class GenericInitializeActions implements IRepairStep +{ + /** + * Constructor. + * + * @param string $appId The leaf app id. + * @param GenericActionAuthService $actionAuth App-scoped action-auth service. + * @param IAppManager $appManager App path resolution for the seed file. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + protected readonly string $appId, + protected readonly GenericActionAuthService $actionAuth, + protected readonly IAppManager $appManager, + protected readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Repair-step name. + * + * @return string + */ + public function getName(): string + { + return sprintf('Initialize %s action-authorization matrix (ADR-023)', $this->appId); + }//end getName() + + /** + * Seed the matrix if empty; preserve any existing admin-customised matrix. + * + * @param IOutput $output Repair output channel. + * + * @return void + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.2 + */ + public function run(IOutput $output): void + { + $existing = $this->actionAuth->getMatrix(); + if (count($existing) > 0) { + $output->info(sprintf('Action matrix already has %d entries — preserving.', count($existing))); + return; + } + + $seedPath = $this->resolveSeedPath(); + if ($seedPath === null || file_exists($seedPath) === false) { + $output->warning('actions.seed.json not found — matrix left empty (default-deny).'); + $this->logger->warning(sprintf('[AppHost:%s] ADR-023 seed file missing', $this->appId)); + return; + } + + $raw = file_get_contents($seedPath); + if ($raw === false) { + $output->warning('Could not read actions.seed.json — matrix left empty (default-deny).'); + return; + } + + try { + $parsed = json_decode($raw, associative: true, depth: 512, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + $output->warning('actions.seed.json invalid JSON: '.$e->getMessage()); + $this->logger->error(sprintf('[AppHost:%s] ADR-023 seed malformed: %s', $this->appId, $e->getMessage())); + return; + } + + $actions = ($parsed['actions'] ?? null); + if (is_array($actions) === false) { + $output->warning('actions.seed.json missing `actions` object — matrix left empty.'); + return; + } + + try { + $this->actionAuth->setMatrix($actions); + } catch (\JsonException $e) { + $output->warning('Failed to write matrix: '.$e->getMessage()); + return; + } + + $output->info(sprintf('Seeded action matrix with %d actions (default: admin-only).', count($actions))); + }//end run() + + /** + * Resolve the leaf app's `lib/actions.seed.json` path. Overridable hook. + * + * @return string|null Absolute path, or null when the app path is unresolvable. + * + * @SuppressWarnings(PHPMD.StaticAccess) + */ + protected function resolveSeedPath(): ?string + { + try { + $appPath = $this->appManager->getAppPath(appId: $this->appId); + } catch (Throwable $e) { + return null; + } + + return $appPath.'/lib/actions.seed.json'; + }//end resolveSeedPath() +}//end class diff --git a/lib/AppHost/Repair/GenericInitializeSettings.php b/lib/AppHost/Repair/GenericInitializeSettings.php new file mode 100644 index 0000000000..2fcdddc19d --- /dev/null +++ b/lib/AppHost/Repair/GenericInitializeSettings.php @@ -0,0 +1,325 @@ +`, which must live in the leaf namespace. The leaf + * therefore keeps a one-line subclass — `class InitializeSettings extends + * GenericInitializeSettings {}` — and Bootstrap registers a factory binding it + * to the app-scoped {@see AppHostSettingsService}. The subclass carries no + * logic; this base owns all behaviour. + * + * Credential-broker onboarding (credential-doriath-leaf design D-G): when the + * leaf's bundled `src/manifest.json` declares a non-empty `credentials[]`, the + * step auto-registers the app with the credential broker + * (`CredentialAppTokenService::registerApp`) — guarded by `isRegistered()` so + * an auto-run NEVER rotates an existing signing secret (rotation stays an + * explicit admin action). This initialisation path serves classic AppHost + * leaves and generated virtual apps alike: both route through + * {@see \OCA\OpenRegister\AppHost\Bootstrap::registerRepairSteps()}, so it IS + * the manifest-registration point for credential consumers. + * + * Per-app Doriath identity (per-app-doriath-application design D-5): alongside + * the OR app-key onboarding, the same hook also registers the consuming app as + * its OWN identity-only Doriath `Application` via + * {@see \OCA\OpenRegister\Service\Credential\DoriathApplicationRegistrar} — + * manifest-driven, idempotent, `pending`, no CSR. This adds identity only; + * brokered secret custody stays under OR's single self-registered application + * vault (credential-doriath-leaf D-C/D-F), untouched. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Repair + * @package OCA\OpenRegister\AppHost\Repair + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Repair; + +use OCA\OpenRegister\AppHost\Service\AppHostSettingsService; +use OCA\OpenRegister\Service\Credential\CredentialAppTokenService; +use OCA\OpenRegister\Service\Credential\DoriathApplicationRegistrar; +use OCP\App\IAppManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use OCP\Server; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Generic install/upgrade repair step that imports a leaf app's register JSON. + * + * @SuppressWarnings(PHPMD.StaticAccess) The credential-broker onboarding hook + * lazily resolves its collaborators via `OCP\Server::get` when the Bootstrap + * factory did not inject them, preserving the degrade-don't-throw posture. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.3 + */ +class GenericInitializeSettings implements IRepairStep +{ + /** + * Constructor. + * + * @param string $appId The leaf app id (display only). + * @param AppHostSettingsService $settingsService App-scoped settings service. + * @param LoggerInterface $logger PSR logger. + * @param IAppManager|null $appManager Locates the leaf's bundled manifest (lazily resolved when null). + * @param CredentialAppTokenService|null $tokenService Credential-broker app registry (lazily resolved when null). + * @param DoriathApplicationRegistrar|null $applicationRegistrar Per-app Doriath application registrar (lazily resolved when null). + */ + public function __construct( + protected readonly string $appId, + protected readonly AppHostSettingsService $settingsService, + protected readonly LoggerInterface $logger, + protected readonly ?IAppManager $appManager=null, + protected readonly ?CredentialAppTokenService $tokenService=null, + protected readonly ?DoriathApplicationRegistrar $applicationRegistrar=null, + ) { + }//end __construct() + + /** + * Repair-step name. + * + * @return string + */ + public function getName(): string + { + return sprintf('Initialize %s register and schemas via ConfigurationService', $this->appId); + }//end getName() + + /** + * Import the app's register configuration. Degrades (does not throw) when + * OpenRegister is unavailable, preserving the fleet's disabled-OR posture. + * + * After the import attempt the credential-broker onboarding hook runs + * (design D-G): a leaf manifest declaring `credentials[]` is registered + * with the broker exactly once — never rotating an existing secret. + * + * @param IOutput $output Repair output channel. + * + * @return void + * + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: Install Plumbing via Repair Steps + * @spec openspec/specs/credential-broker/spec.md + */ + public function run(IOutput $output): void + { + $output->info(sprintf('Initializing %s configuration...', $this->appId)); + + if ($this->settingsService->isOpenRegisterAvailable() === false) { + $output->warning('OpenRegister is not installed or enabled. Skipping auto-configuration.'); + $this->logger->warning(sprintf('[AppHost:%s] OpenRegister not available, skipping register initialization', $this->appId)); + return; + } + + $this->importConfiguration(output: $output); + $this->registerCredentialConsumer(output: $output); + }//end run() + + /** + * Run the register-JSON import, reporting (never throwing) any issue. + * + * @param IOutput $output Repair output channel. + * + * @return void + * + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: Install Plumbing via Repair Steps + */ + private function importConfiguration(IOutput $output): void + { + try { + $result = $this->settingsService->loadConfiguration(force: false); + + if (($result['success'] ?? false) === true) { + $version = ($result['version'] ?? 'unknown'); + $output->info(sprintf('%s configuration imported successfully (version: %s)', $this->appId, $version)); + return; + } + + $message = ($result['message'] ?? 'unknown error'); + $output->warning(sprintf('%s configuration import issue: %s', $this->appId, $message)); + } catch (Throwable $e) { + $output->warning(sprintf('Could not auto-configure %s: %s', $this->appId, $e->getMessage())); + $this->logger->error(sprintf('[AppHost:%s] initialization failed', $this->appId), ['exception' => $e->getMessage()]); + }//end try + }//end importConfiguration() + + /** + * Onboarding hook for a `credentials[]`-declaring leaf. + * + * When the leaf's bundled `src/manifest.json` declares a non-empty + * `credentials` array it runs two INDEPENDENT, idempotent onboarding paths: + * the OR credential-broker app-key registration (design D-G, + * {@see registerBrokerAppKey()}) and the per-app Doriath IDENTITY + * registration (per-app-doriath-application D-5, + * {@see registerDoriathApplication()}). Both degrade (warn, never throw) on + * their own; the Doriath-identity path runs even when the app already holds + * a broker signing secret. + * + * @param IOutput $output Repair output channel. + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + private function registerCredentialConsumer(IOutput $output): void + { + if ($this->manifestDeclaresCredentials() === false) { + return; + } + + // OR app-key onboarding (credential-doriath-leaf D-G) and per-app Doriath + // IDENTITY registration (per-app-doriath-application D-5) are INDEPENDENT + // and both idempotent: the Doriath-identity path runs even when the app + // already holds a signing secret (its own live-row probe collapses re-runs + // to a no-op). Both degrade (warn, never throw) on their own. + $this->registerBrokerAppKey(output: $output); + $this->registerDoriathApplication(); + }//end registerCredentialConsumer() + + /** + * Register the leaf's OR credential-broker app-key once (never rotating). + * + * The `isRegistered()` guard is essential: `registerApp()` ROTATES the + * signing secret on every call, and an unguarded auto-run on each upgrade + * would invalidate the app's held copy — auto-onboarding only ever registers + * ABSENT apps (design D-G). The freshly generated secret is deliberately + * discarded here: in-process consumers call the broker without an HMAC token + * (same-instance PHP is trusted), and cross-runtime consumers obtain a + * secret through the explicit admin registration endpoint instead. Never + * throws — any failure warns and leaves the instance healthy. + * + * @param IOutput $output Repair output channel. + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function registerBrokerAppKey(IOutput $output): void + { + try { + $tokenService = ($this->tokenService ?? Server::get(CredentialAppTokenService::class)); + + if ($tokenService->isRegistered($this->appId) === true) { + // Never rotate an existing signing secret from an auto-run. + $this->logger->debug( + sprintf('[AppHost:%s] already registered with the credential broker — not rotating', $this->appId) + ); + return; + } + + // The returned secret is intentionally unused (see docblock). + $tokenService->registerApp($this->appId); + $output->info(sprintf('%s registered with the credential broker (manifest declares credentials).', $this->appId)); + } catch (Throwable $e) { + $output->warning(sprintf('Credential-broker registration for %s skipped: %s', $this->appId, $e->getMessage())); + $this->logger->warning( + sprintf('[AppHost:%s] credential-broker auto-registration failed', $this->appId), + ['exception' => $e->getMessage()] + ); + }//end try + }//end registerBrokerAppKey() + + /** + * Register the consuming app as its own identity-only Doriath `Application`. + * + * Delegates to {@see DoriathApplicationRegistrar}: name = appId, description + * = the manifest description (fallback appId), type `internal`, no CSR, + * `pending`. Idempotent and never-throw are owned by the registrar; this + * method only supplies the description read from the leaf manifest. Custody + * (OR's single `openregister` vault, credential-doriath-leaf D-C/D-F) is NOT + * touched — this adds identity, not custody. + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function registerDoriathApplication(): void + { + $registrar = ($this->applicationRegistrar ?? Server::get(DoriathApplicationRegistrar::class)); + $registrar->registerApplication($this->appId, $this->manifestDescription()); + }//end registerDoriathApplication() + + /** + * The leaf's bundled `src/manifest.json` `description`, or null when absent. + * + * @return string|null The manifest description, or null. + * + * @spec openspec/specs/credential-broker/spec.md + */ + protected function manifestDescription(): ?string + { + try { + $appManager = ($this->appManager ?? Server::get(IAppManager::class)); + $manifestPath = $appManager->getAppPath($this->appId).'/src/manifest.json'; + } catch (Throwable $e) { + return null; + } + + if (is_readable($manifestPath) === false) { + return null; + } + + $manifest = json_decode((string) file_get_contents($manifestPath), true); + if (is_array($manifest) === false) { + return null; + } + + $description = ($manifest['description'] ?? null); + if (is_string($description) === true && $description !== '') { + return $description; + } + + return null; + }//end manifestDescription() + + /** + * Whether the leaf's bundled `src/manifest.json` declares `credentials[]`. + * + * @return bool True when the manifest declares a non-empty credentials array. + * + * @spec openspec/specs/credential-broker/spec.md + */ + protected function manifestDeclaresCredentials(): bool + { + try { + $appManager = ($this->appManager ?? Server::get(IAppManager::class)); + $manifestPath = $appManager->getAppPath($this->appId).'/src/manifest.json'; + } catch (Throwable $e) { + return false; + } + + if (is_readable($manifestPath) === false) { + return false; + } + + $manifest = json_decode((string) file_get_contents($manifestPath), true); + if (is_array($manifest) === false) { + return false; + } + + $credentials = ($manifest['credentials'] ?? null); + + return (is_array($credentials) === true && $credentials !== []); + }//end manifestDeclaresCredentials() +}//end class diff --git a/lib/AppHost/Routes.php b/lib/AppHost/Routes.php new file mode 100644 index 0000000000..d375438ca6 --- /dev/null +++ b/lib/AppHost/Routes.php @@ -0,0 +1,169 @@ + 'pets#index', 'url' => '/api/pets', 'verb' => 'GET'], + * ]); + * + * The returned set is bit-compatible with the petstore reference skeleton: + * the route names (`dashboard#page`, `dashboard#catchAll`, `settings#index`, + * `settings#create`, `settings#load`, `preferences#getPreference`, + * `preferences#setPreference`, `metrics#index`, `health#index`), URLs and + * verbs are unchanged, so info.xml navigation entries keep resolving. The SPA + * catch-all is ordered LAST and carries a distinct name so it never shadows the + * dashboard index route. + * + * This file references no `OCA\OpenRegister\…` symbol — it is a pure array + * builder, so requiring it from a leaf `routes.php` is safe even when + * OpenRegister is disabled. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category AppHost + * @package OCA\OpenRegister\AppHost + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost; + +/** + * Canonical AppHost route table builder. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-3.2 + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: Canonical Route Table + */ +class Routes +{ + /** + * Return the canonical route array, merging app-specific `$extra` routes. + * + * App-specific routes are inserted BEFORE the SPA catch-all so they keep + * priority over the `/{path}` fallback. A duplicate route name in `$extra` + * overrides the canonical entry of the same name (intentional: lets an app + * re-point one route at a local controller) — but a duplicate name WITHIN + * `$extra` itself throws, since Symfony silently replaces same-named routes + * and that is always a mistake. + * + * @param array> $extra App-specific routes. + * + * @return array{routes: array>} + * + * @throws \InvalidArgumentException When `$extra` contains duplicate route names. + * + * @spec openspec/specs/apphost-boilerplate/spec.md — Requirement: Canonical Route Table + */ + public static function standard(array $extra=[]): array + { + self::assertNoDuplicateNames(extra: $extra); + + $extraNames = []; + foreach ($extra as $route) { + if (isset($route['name']) === true) { + $extraNames[(string) $route['name']] = true; + } + } + + // Canonical routes, minus the SPA catch-all (appended last). + $canonical = []; + foreach (self::canonicalRoutes() as $route) { + // An $extra route with the same name overrides the canonical one. + if (isset($extraNames[$route['name']]) === true) { + continue; + } + + $canonical[] = $route; + } + + $merged = array_merge($canonical, $extra); + $merged[] = self::catchAllRoute(); + + return ['routes' => $merged]; + }//end standard() + + /** + * The canonical AppHost routes (everything except the SPA catch-all). + * + * @return array> + */ + private static function canonicalRoutes(): array + { + return [ + // Dashboard page. + ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'], + + // Settings API. + ['name' => 'settings#index', 'url' => '/api/settings', 'verb' => 'GET'], + ['name' => 'settings#create', 'url' => '/api/settings', 'verb' => 'POST'], + ['name' => 'settings#load', 'url' => '/api/settings/load', 'verb' => 'POST'], + + // Generic per-user preferences (shared nextcloud-vue widgets). + ['name' => 'preferences#getPreference', 'url' => '/api/preferences/{key}', 'verb' => 'GET'], + ['name' => 'preferences#setPreference', 'url' => '/api/preferences/{key}', 'verb' => 'PUT'], + + // Observability (ADR-006 / ADR-040). + ['name' => 'metrics#index', 'url' => '/api/metrics', 'verb' => 'GET'], + ['name' => 'health#index', 'url' => '/api/health', 'verb' => 'GET'], + ]; + }//end canonicalRoutes() + + /** + * The SPA catch-all route — same controller as `dashboard#page`, distinct + * name so it does not replace the index route in Symfony's router. + * + * @return array + */ + private static function catchAllRoute(): array + { + return [ + 'name' => 'dashboard#catchAll', + 'url' => '/{path}', + 'verb' => 'GET', + 'requirements' => ['path' => '.+'], + 'defaults' => ['path' => ''], + ]; + }//end catchAllRoute() + + /** + * Guard against duplicate route names within the caller's `$extra` set. + * + * @param array> $extra App-specific routes. + * + * @return void + * + * @throws \InvalidArgumentException When two `$extra` routes share a name. + */ + private static function assertNoDuplicateNames(array $extra): void + { + $seen = []; + foreach ($extra as $route) { + if (isset($route['name']) === false) { + continue; + } + + $name = (string) $route['name']; + if (isset($seen[$name]) === true) { + throw new \InvalidArgumentException(sprintf('Duplicate route name "%s" in AppHost Routes::standard($extra)', $name)); + } + + $seen[$name] = true; + } + }//end assertNoDuplicateNames() +}//end class diff --git a/lib/AppHost/Scheduling/CronScheduleEvaluator.php b/lib/AppHost/Scheduling/CronScheduleEvaluator.php new file mode 100644 index 0000000000..35df465138 --- /dev/null +++ b/lib/AppHost/Scheduling/CronScheduleEvaluator.php @@ -0,0 +1,84 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Scheduling; + +use Cron\CronExpression; +use DateTime; +use DateTimeInterface; +use Throwable; + +/** + * Evaluates cron expressions to their next fire time via a vendored library. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ +class CronScheduleEvaluator +{ + /** + * Whether a cron expression is parseable by the vendored library. + * + * @param string $expression The cron expression. + * + * @return bool True when the expression parses; false otherwise. + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function isValid(string $expression): bool + { + return CronExpression::isValidExpression($expression); + }//end isValid() + + /** + * Compute the next fire time strictly after `$from` (default now). + * + * @param string $expression The cron expression. + * @param DateTimeInterface|null $from Reference time; defaults to now. + * + * @return DateTime|null The next fire time, or null when the expression is unparseable. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function nextRun(string $expression, ?DateTimeInterface $from=null): ?DateTime + { + if ($this->isValid(expression: $expression) === false) { + return null; + } + + try { + $cron = new CronExpression($expression); + $current = ($from ?? new DateTime()); + return $cron->getNextRunDate($current); + } catch (Throwable $e) { + return null; + } + }//end nextRun() +}//end class diff --git a/lib/AppHost/Scheduling/ScheduleActionAllowList.php b/lib/AppHost/Scheduling/ScheduleActionAllowList.php new file mode 100644 index 0000000000..d56297c9fd --- /dev/null +++ b/lib/AppHost/Scheduling/ScheduleActionAllowList.php @@ -0,0 +1,79 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Scheduling; + +/** + * Closed allow-list mapping action types to vetted job classes. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ +class ScheduleActionAllowList +{ + /** + * The closed action-type → vetted `jobClass` map. + * + * Kept as plain strings so referencing this map never autoloads the + * cross-app job class (it is resolved lazily by OpenConnector's JobService + * container at execution time, not by OpenRegister). + * + * @var array + */ + private const MAP = [ + 'openconnector:synchronization' => 'OCA\\OpenConnector\\Action\\SynchronizationAction', + ]; + + /** + * Resolve an action type to its vetted `jobClass`, or null when not allow-listed. + * + * @param string $action The manifest-declared action type. + * + * @return string|null The server-vetted job class, or null when the action is not allow-listed. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function resolve(string $action): ?string + { + return self::MAP[$action] ?? null; + }//end resolve() + + /** + * Whether an action type is on the allow-list. + * + * @param string $action The manifest-declared action type. + * + * @return bool + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function isAllowed(string $action): bool + { + return isset(self::MAP[$action]); + }//end isAllowed() +}//end class diff --git a/lib/AppHost/Scheduling/ScheduleDescriptor.php b/lib/AppHost/Scheduling/ScheduleDescriptor.php new file mode 100644 index 0000000000..eaa361452b --- /dev/null +++ b/lib/AppHost/Scheduling/ScheduleDescriptor.php @@ -0,0 +1,175 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Scheduling; + +/** + * A single validated schedule declaration. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ +final class ScheduleDescriptor +{ + /** + * Constructor. + * + * @param string $id Stable schedule id (unique per application). + * @param int|null $intervalSeconds Positive interval in seconds, or null for cron schedules. + * @param string|null $cron Cron expression, or null for interval schedules. + * @param string $action Allow-listed action type (e.g. `openconnector:synchronization`). + * @param array $arguments Arguments passed to the resolved action. + * @param bool $enabled Whether the schedule is active (default true). + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function __construct( + public readonly string $id, + public readonly ?int $intervalSeconds, + public readonly ?string $cron, + public readonly string $action, + public readonly array $arguments=[], + public readonly bool $enabled=true + ) { + }//end __construct() + + /** + * Whether this is an interval-cadence schedule. + * + * @return bool + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function isInterval(): bool + { + return $this->intervalSeconds !== null; + }//end isInterval() + + /** + * Whether this is a cron-cadence schedule. + * + * @return bool + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function isCron(): bool + { + return $this->cron !== null; + }//end isCron() + + /** + * Build a descriptor from a raw manifest entry, validating its structure. + * + * Rejects (throws) an entry that lacks a non-empty `id` or `action`, or that + * declares neither or both of `interval`/`cron`, or a non-positive + * `interval`. Cron *parseability* is validated separately by + * {@see ScheduleManifest} via the cron evaluator, so this method stays free + * of the cron library. + * + * @param array $raw The raw schedule entry. + * @param int $index Index of the entry within `schedules[]` (for diagnostics). + * + * @return self + * + * @throws ScheduleValidationException When the entry is structurally invalid. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public static function fromArray(array $raw, int $index): self + { + $id = $raw['id'] ?? null; + if (is_string($id) === false || trim($id) === '') { + throw new ScheduleValidationException(sprintf('Schedule #%d is missing a non-empty "id".', $index)); + } + + $action = $raw['action'] ?? null; + if (is_string($action) === false || trim($action) === '') { + throw new ScheduleValidationException(sprintf('Schedule "%s" is missing a non-empty "action".', $id)); + } + + [$intervalSeconds, $cron] = self::parseCadence(raw: $raw, id: $id); + + $arguments = $raw['arguments'] ?? []; + if (is_array($arguments) === false) { + $arguments = []; + } + + return new self( + id: $id, + intervalSeconds: $intervalSeconds, + cron: $cron, + action: $action, + arguments: $arguments, + enabled: (($raw['enabled'] ?? true) !== false) + ); + }//end fromArray() + + /** + * Parse and validate the cadence: exactly one of interval/cron. + * + * @param array $raw The raw schedule entry. + * @param string $id The schedule id (for messages). + * + * @return array{0: int|null, 1: string|null} Tuple of [intervalSeconds, cron]. + * + * @throws ScheduleValidationException When neither/both are present or a value is invalid. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + private static function parseCadence(array $raw, string $id): array + { + $hasInterval = array_key_exists('interval', $raw) === true && $raw['interval'] !== null; + $hasCron = array_key_exists('cron', $raw) === true && $raw['cron'] !== null; + + if ($hasInterval === $hasCron) { + throw new ScheduleValidationException( + sprintf('Schedule "%s" must declare exactly one of "interval" or "cron".', $id) + ); + } + + if ($hasInterval === true) { + $interval = $raw['interval']; + if (is_int($interval) === false || $interval <= 0) { + throw new ScheduleValidationException( + sprintf('Schedule "%s" has an invalid "interval" (must be a positive integer of seconds).', $id) + ); + } + + return [$interval, null]; + } + + $cronRaw = $raw['cron']; + if (is_string($cronRaw) === false || trim($cronRaw) === '') { + throw new ScheduleValidationException( + sprintf('Schedule "%s" has an empty "cron" expression.', $id) + ); + } + + return [null, $cronRaw]; + }//end parseCadence() +}//end class diff --git a/lib/AppHost/Scheduling/ScheduleManifest.php b/lib/AppHost/Scheduling/ScheduleManifest.php new file mode 100644 index 0000000000..5a3093370c --- /dev/null +++ b/lib/AppHost/Scheduling/ScheduleManifest.php @@ -0,0 +1,103 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Scheduling; + +/** + * Validated set of schedules declared by a single application's manifest. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ +final class ScheduleManifest +{ + /** + * Constructor. + * + * @param string $applicationId The owning application identifier (app id or OR object uuid). + * @param ScheduleDescriptor[] $schedules Parsed, structurally-valid schedules. + * @param string[] $diagnostics Rejected-entry diagnostics (never thrown). + */ + public function __construct( + public readonly string $applicationId, + public readonly array $schedules, + public readonly array $diagnostics=[] + ) { + }//end __construct() + + /** + * Build a schedule manifest from a decoded application manifest. + * + * Each `schedules[]` entry is validated structurally by + * {@see ScheduleDescriptor::fromArray}; a `cron` entry is additionally + * checked for parseability via the evaluator when one is supplied. Rejected + * entries are collected into diagnostics and omitted from `$schedules`. + * + * @param string $applicationId The owning application identifier. + * @param array $manifest The decoded manifest. + * @param CronScheduleEvaluator|null $cron Cron evaluator (rejects unparseable cron when supplied). + * + * @return self + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public static function fromManifest(string $applicationId, array $manifest, ?CronScheduleEvaluator $cron=null): self + { + $raw = $manifest['schedules'] ?? null; + if (is_array($raw) === false) { + return new self(applicationId: $applicationId, schedules: [], diagnostics: []); + } + + $schedules = []; + $diagnostics = []; + + foreach ($raw as $index => $entry) { + if (is_array($entry) === false) { + $diagnostics[] = sprintf('Schedule #%d is not an object.', (int) $index); + continue; + } + + try { + $descriptor = ScheduleDescriptor::fromArray(raw: $entry, index: (int) $index); + } catch (ScheduleValidationException $e) { + $diagnostics[] = $e->getMessage(); + continue; + } + + if ($descriptor->isCron() === true && $cron !== null && $cron->isValid((string) $descriptor->cron) === false) { + $diagnostics[] = sprintf('Schedule "%s" has an unparseable cron expression.', $descriptor->id); + continue; + } + + $schedules[] = $descriptor; + }//end foreach + + return new self(applicationId: $applicationId, schedules: $schedules, diagnostics: $diagnostics); + }//end fromManifest() +}//end class diff --git a/lib/AppHost/Scheduling/ScheduleManifestLoader.php b/lib/AppHost/Scheduling/ScheduleManifestLoader.php new file mode 100644 index 0000000000..6cb9eee1aa --- /dev/null +++ b/lib/AppHost/Scheduling/ScheduleManifestLoader.php @@ -0,0 +1,150 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Scheduling; + +use OCP\App\IAppManager; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Loads on-disk app manifests and parses their `schedules[]` declarations. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ +class ScheduleManifestLoader +{ + /** + * Constructor. + * + * @param IAppManager $appManager App-path + installed-app enumeration. + * @param CronScheduleEvaluator $cron Cron parseability validator. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly CronScheduleEvaluator $cron, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Load the schedule manifest for a single on-disk app id. + * + * @param string $appId The Nextcloud app id. + * + * @return ScheduleManifest|null The parsed schedules, or null when the app has no readable manifest. + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function loadForApp(string $appId): ?ScheduleManifest + { + $manifest = $this->loadBundledManifest(appId: $appId); + if ($manifest === null) { + return null; + } + + return ScheduleManifest::fromManifest(applicationId: $appId, manifest: $manifest, cron: $this->cron); + }//end loadForApp() + + /** + * Enumerate all installed on-disk apps that declare a `schedules[]` block. + * + * @return ScheduleManifest[] One entry per installed app with schedules declared. + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function loadAllOnDisk(): array + { + $manifests = []; + + try { + $appIds = $this->appManager->getInstalledApps(); + } catch (Throwable $e) { + $this->logger->warning( + message: '[AppHost\\Scheduling] Failed to enumerate installed apps: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return []; + } + + foreach ($appIds as $appId) { + $manifest = $this->loadForApp(appId: (string) $appId); + if ($manifest === null || $manifest->schedules === []) { + continue; + } + + $manifests[] = $manifest; + } + + return $manifests; + }//end loadAllOnDisk() + + /** + * Load + JSON-decode an app's bundled `src/manifest.json`. + * + * @param string $appId The Nextcloud app id. + * + * @return array|null Decoded manifest, or null if unreadable. + * + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function loadBundledManifest(string $appId): ?array + { + try { + $appPath = $this->appManager->getAppPath(appId: $appId); + } catch (Throwable $e) { + return null; + } + + $manifestPath = $appPath.'/src/manifest.json'; + if (is_readable($manifestPath) === false) { + return null; + } + + $raw = file_get_contents($manifestPath); + if ($raw === false) { + return null; + } + + $decoded = json_decode($raw, associative: true); + if (json_last_error() !== JSON_ERROR_NONE || is_array($decoded) === false) { + $this->logger->warning( + message: sprintf('[AppHost\\Scheduling] manifest.json for "%s" is invalid JSON', $appId), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return null; + } + + return $decoded; + }//end loadBundledManifest() +}//end class diff --git a/lib/AppHost/Scheduling/ScheduleReconciler.php b/lib/AppHost/Scheduling/ScheduleReconciler.php new file mode 100644 index 0000000000..fca4dff5a2 --- /dev/null +++ b/lib/AppHost/Scheduling/ScheduleReconciler.php @@ -0,0 +1,783 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Scheduling; + +use DateTime; +use DateTimeInterface; +use OCA\OpenRegister\Service\ObjectService; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Enumerates manifest schedules and reconciles them into OpenConnector jobs. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ScheduleReconciler +{ + /** + * OpenConnector register slug that owns the `job` schema. + */ + private const OC_REGISTER_SLUG = 'openconnector'; + + /** + * OpenConnector `job` schema slug. + */ + private const OC_JOB_SCHEMA_SLUG = 'job'; + + /** + * Page size for the reconciler's sweep reads. ObjectService::findAll + * defaults to ~25 rows; a system sweep must see every application and + * every managed job, so an explicit high ceiling is passed on each read. + */ + private const SWEEP_LIMIT = 1000; + + /** + * OpenBuild register slug that owns virtual-app `application` objects. + */ + private const OB_REGISTER_SLUG = 'openbuild'; + + /** + * OpenBuild `application` schema slug. + */ + private const OB_APPLICATION_SCHEMA_SLUG = 'application'; + + /** + * OpenBuild `applicationVersion` schema slug — a virtual app's manifest + * lives on its production ApplicationVersion, not on the application object. + */ + private const OB_APPLICATION_VERSION_SCHEMA_SLUG = 'applicationVersion'; + + /** + * Reference prefix marking a `job` object as AppHost-schedule-managed. + */ + public const REFERENCE_PREFIX = 'apphost-schedule'; + + /** + * Constructor. + * + * @param ObjectService $objectService OR CRUD facade for reading applications and upserting jobs. + * @param ScheduleManifestLoader $manifestLoader On-disk manifest enumeration. + * @param CronScheduleEvaluator $cron Cron → next-fire + * computation. + * @param ScheduleActionAllowList $allowList Closed action → vetted jobClass + * map. + * @param IUserManager $userManager Resolves an owner UID to a live NC user. + * @param LoggerInterface $logger Secret-free diagnostics. + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly ScheduleManifestLoader $manifestLoader, + private readonly CronScheduleEvaluator $cron, + private readonly ScheduleActionAllowList $allowList, + private readonly IUserManager $userManager, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Reconcile every declared schedule into an OpenConnector job (never throws). + * + * @return void + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function reconcile(): void + { + try { + $existing = $this->loadManagedJobs(); + if ($existing === null) { + // OpenConnector register/schema absent — degrade quietly. + return; + } + + $now = new DateTime(); + $desiredKeys = []; + + foreach ($this->collectSources() as $source) { + $applicationId = $source['applicationId']; + $ownerUid = $source['ownerUid']; + + foreach ($source['schedules'] as $descriptor) { + $reference = $this->computeReference(applicationId: $applicationId, scheduleId: $descriptor->id); + + $jobClass = $this->allowList->resolve($descriptor->action); + if ($jobClass === null) { + $this->logger->warning( + message: '[AppHost\\Scheduling] Rejected non-allow-listed action', + context: [ + 'applicationId' => $applicationId, + 'scheduleId' => $descriptor->id, + 'action' => $descriptor->action, + ] + ); + continue; + } + + if ($ownerUid === null) { + $this->logger->warning( + message: '[AppHost\\Scheduling] Skipping schedule: no resolvable owner (fail-closed)', + context: ['applicationId' => $applicationId, 'scheduleId' => $descriptor->id] + ); + continue; + } + + $desiredKeys[$reference] = true; + $this->upsert( + descriptor: $descriptor, + reference: $reference, + jobClass: $jobClass, + ownerUid: $ownerUid, + existing: ($existing[$reference] ?? null), + now: $now + ); + }//end foreach + }//end foreach + + $this->garbageCollect(existing: $existing, desiredKeys: $desiredKeys); + } catch (Throwable $e) { + // Top-level guard: a scheduling hiccup must never break cron.php. + $this->logger->error( + message: '[AppHost\\Scheduling] Reconcile aborted: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + }//end try + }//end reconcile() + + /** + * Collect all schedule sources (on-disk + virtual) with their resolved owners. + * + * @return array + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function collectSources(): array + { + $sources = []; + + // Virtual apps: openbuild `application` objects carry both the manifest + // and the owner directly on the OR object. + foreach ($this->loadVirtualApplications() as $application) { + $applicationId = $this->extractApplicationId(application: $application); + if ($applicationId === null) { + continue; + } + + $manifest = $this->extractManifest(application: $application); + $parsed = ScheduleManifest::fromManifest( + applicationId: $applicationId, + manifest: $manifest, + cron: $this->cron + ); + $this->logDiagnostics(applicationId: $applicationId, diagnostics: $parsed->diagnostics); + if ($parsed->schedules === []) { + continue; + } + + $sources[] = [ + 'applicationId' => $applicationId, + 'ownerUid' => $this->resolveOwner(ownerUid: ($application['@self']['owner'] ?? null)), + 'schedules' => $parsed->schedules, + ]; + }//end foreach + + // On-disk apps: the owner is resolved from a matching openbuild + // `application` object (slug == appId); absent that, no owner (skip). + foreach ($this->manifestLoader->loadAllOnDisk() as $parsed) { + $this->logDiagnostics(applicationId: $parsed->applicationId, diagnostics: $parsed->diagnostics); + $sources[] = [ + 'applicationId' => $parsed->applicationId, + 'ownerUid' => $this->resolveOnDiskOwner(appId: $parsed->applicationId), + 'schedules' => $parsed->schedules, + ]; + } + + return $sources; + }//end collectSources() + + /** + * Build the deterministic matching reference for a schedule. + * + * @param string $applicationId The owning application identifier. + * @param string $scheduleId The schedule id. + * + * @return string The `apphost-schedule:{applicationId}:{scheduleId}` key. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function computeReference(string $applicationId, string $scheduleId): string + { + return sprintf('%s:%s:%s', self::REFERENCE_PREFIX, $applicationId, $scheduleId); + }//end computeReference() + + /** + * Build the desired `job` data for a schedule, merged over any existing job. + * + * Preserves fields owned by `JobService` (`lastRun`, and — for interval + * schedules — `nextRun`). For a cron schedule, sets `nextRun` to the next + * fire time, but only rolls it FORWARD: an existing future `nextRun` is left + * untouched so a job is never rewound mid-flight (design D-2/D-5). + * + * @param ScheduleDescriptor $descriptor The schedule declaration. + * @param string $reference The matching reference key. + * @param string $jobClass The server-vetted job class. + * @param string $ownerUid The resolved application owner UID. + * @param array|null $existing Existing job data props (null on create). + * @param DateTimeInterface $now Reference "now". + * + * @return array The desired job data properties. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function buildJobData( + ScheduleDescriptor $descriptor, + string $reference, + string $jobClass, + string $ownerUid, + ?array $existing, + DateTimeInterface $now + ): array { + // Start from existing data props (minus metadata) so JobService-owned + // bookkeeping is preserved on update; empty on create. + $data = []; + if ($existing !== null) { + $data = $existing; + unset($data['@self']); + } + + $data['reference'] = $reference; + $data['jobClass'] = $jobClass; + $data['arguments'] = $descriptor->arguments; + $data['isEnabled'] = $descriptor->enabled; + $data['userId'] = $ownerUid; + $data['name'] = sprintf('AppHost schedule %s', $descriptor->id); + $data['description'] = sprintf('Reconciled from manifest schedule "%s".', $descriptor->id); + + if ($descriptor->isInterval() === true) { + $data['interval'] = $descriptor->intervalSeconds; + // Interval jobs: never touch lastRun/nextRun (owned by JobService). + return $data; + } + + // Cron schedule: drive execution via nextRun, not interval. + $data['interval'] = null; + + $existingNextRun = $this->parseNextRun(value: ($existing['nextRun'] ?? null)); + if ($existingNextRun !== null && $existingNextRun > $now) { + // A future nextRun is left as-is (never rewind mid-flight). + return $data; + } + + $next = $this->cron->nextRun((string) $descriptor->cron, $now); + if ($next !== null) { + $data['nextRun'] = $next->format(DateTimeInterface::ATOM); + } + + return $data; + }//end buildJobData() + + /** + * Whether the owned fields of the desired job differ from the existing job. + * + * Compares only reconciler-owned fields; JobService-owned bookkeeping + * (`lastRun`, interval jobs' `nextRun`) is ignored so an unchanged schedule + * is a genuine no-op. + * + * @param array|null $existing Existing job data props (null on create → always differs). + * @param array $desired Desired job data props. + * @param bool $isCron Whether this is a cron schedule (compare nextRun too). + * + * @return bool True when a write is required. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + public function jobDiffers(?array $existing, array $desired, bool $isCron): bool + { + if ($existing === null) { + return true; + } + + $keys = ['reference', 'jobClass', 'isEnabled', 'userId', 'name', 'description']; + foreach ($keys as $key) { + if (($existing[$key] ?? null) !== ($desired[$key] ?? null)) { + return true; + } + } + + if (($existing['arguments'] ?? []) !== ($desired['arguments'] ?? [])) { + return true; + } + + if ($isCron === true) { + if (($existing['nextRun'] ?? null) !== ($desired['nextRun'] ?? null)) { + return true; + } + + return (($existing['interval'] ?? null) !== ($desired['interval'] ?? null)); + } + + return ((int) ($existing['interval'] ?? 0) !== (int) ($desired['interval'] ?? 0)); + }//end jobDiffers() + + /** + * Resolve an owner UID to a live NC user UID, or null when unresolvable. + * + * @param mixed $ownerUid The candidate owner UID from the application object. + * + * @return string|null The verified owner UID, or null (fail-closed). + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function resolveOwner(mixed $ownerUid): ?string + { + if (is_string($ownerUid) === false || trim($ownerUid) === '') { + return null; + } + + if ($this->userManager->get($ownerUid) === null) { + return null; + } + + return $ownerUid; + }//end resolveOwner() + + /** + * Upsert a single schedule into its OpenConnector job. + * + * @param ScheduleDescriptor $descriptor The schedule declaration. + * @param string $reference The matching reference key. + * @param string $jobClass The server-vetted job class. + * @param string $ownerUid The resolved application owner UID. + * @param array|null $existing Existing job data props (null on create). + * @param DateTimeInterface $now Reference "now". + * + * @return void + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function upsert( + ScheduleDescriptor $descriptor, + string $reference, + string $jobClass, + string $ownerUid, + ?array $existing, + DateTimeInterface $now + ): void { + $desired = $this->buildJobData( + descriptor: $descriptor, + reference: $reference, + jobClass: $jobClass, + ownerUid: $ownerUid, + existing: $existing, + now: $now + ); + + if ($this->jobDiffers(existing: $existing, desired: $desired, isCron: $descriptor->isCron()) === false) { + // No-op: unchanged schedule, no write (idempotent). + return; + } + + $uuid = null; + if ($existing !== null) { + $uuid = ($existing['@self']['uuid'] ?? ($existing['@self']['id'] ?? null)); + if ($uuid !== null) { + $uuid = (string) $uuid; + } + } + + $this->saveJob(data: $desired, uuid: $uuid); + }//end upsert() + + /** + * Garbage-collect managed jobs whose schedule is gone from every manifest. + * + * @param array> $existing Managed jobs keyed by reference. + * @param array $desiredKeys References that are still declared. + * + * @return void + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function garbageCollect(array $existing, array $desiredKeys): void + { + foreach ($existing as $reference => $job) { + if (isset($desiredKeys[$reference]) === true) { + continue; + } + + // Orphaned: disable (preserve run history) rather than delete. + if (($job['isEnabled'] ?? false) === false) { + continue; + } + + $data = $job; + unset($data['@self']); + $data['isEnabled'] = false; + + $uuid = ($job['@self']['uuid'] ?? ($job['@self']['id'] ?? null)); + if ($uuid !== null) { + $uuid = (string) $uuid; + } + + $this->saveJob(data: $data, uuid: $uuid); + }//end foreach + }//end garbageCollect() + + /** + * Load all AppHost-managed jobs, keyed by their reference. + * + * @return array>|null Keyed jobs, or null when the OC register/schema is absent. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function loadManagedJobs(): ?array + { + try { + $rows = $this->objectService->findAll( + config: [ + 'filters' => [ + 'register' => self::OC_REGISTER_SLUG, + 'schema' => self::OC_JOB_SCHEMA_SLUG, + ], + 'limit' => self::SWEEP_LIMIT, + ], + _rbac: false + ); + } catch (Throwable $e) { + $this->logger->info( + message: '[AppHost\\Scheduling] OpenConnector job register/schema unavailable; scheduling inert', + context: ['reason' => $e->getMessage()] + ); + return null; + } + + $managed = []; + foreach ($this->normaliseRows(rows: $rows) as $row) { + $reference = ($row['reference'] ?? null); + if (is_string($reference) === false || str_starts_with($reference, self::REFERENCE_PREFIX.':') === false) { + continue; + } + + $managed[$reference] = $row; + } + + return $managed; + }//end loadManagedJobs() + + /** + * Load the openbuild `application` objects (virtual apps). + * + * @return array> Rendered application objects (empty on any failure). + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function loadVirtualApplications(): array + { + try { + // NOTE: pass ONLY `_rbac: false` (a system sweep must see apps + // regardless of owner). Do NOT also pass `_multitenancy: false`: + // ObjectService::findAll returns ZERO rows when BOTH flags are + // false, whereas either flag alone (or the defaults) returns the + // full set. `_rbac: false` alone is the correct, working bypass + // for this background sweep. Applied to every read/write below. + $rows = $this->objectService->findAll( + config: [ + 'filters' => [ + 'register' => self::OB_REGISTER_SLUG, + 'schema' => self::OB_APPLICATION_SCHEMA_SLUG, + ], + 'limit' => self::SWEEP_LIMIT, + ], + _rbac: false + ); + } catch (Throwable $e) { + $this->logger->info( + message: '[AppHost\\Scheduling] openbuild application register/schema unavailable; virtual apps skipped', + context: ['reason' => $e->getMessage()] + ); + return []; + }//end try + + return $this->normaliseRows(rows: $rows); + }//end loadVirtualApplications() + + /** + * Normalise a findAll result set to plain arrays. + * + * ObjectService::findAll returns rendered rows that may be `ObjectEntity` + * (JsonSerializable) objects rather than plain arrays; serialize those so + * downstream array access works uniformly. Non-serialisable rows are dropped. + * + * @param array $rows The raw findAll result. + * + * @return array> The rows as plain arrays. + */ + private function normaliseRows(array $rows): array + { + $normalised = []; + foreach ($rows as $row) { + if (is_array($row) === true) { + $normalised[] = $row; + continue; + } + + if (is_object($row) === true && method_exists($row, 'jsonSerialize') === true) { + $serialized = $row->jsonSerialize(); + if (is_array($serialized) === true) { + $normalised[] = $serialized; + } + } + } + + return $normalised; + }//end normaliseRows() + + /** + * Resolve an on-disk app's owner from a matching openbuild `application` object. + * + * @param string $appId The Nextcloud app id (matched against the application slug). + * + * @return string|null The resolved owner UID, or null when no matching application/owner exists. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function resolveOnDiskOwner(string $appId): ?string + { + foreach ($this->loadVirtualApplications() as $application) { + $slug = ($application['slug'] ?? ($application['@self']['slug'] ?? null)); + if (is_string($slug) === true && $slug === $appId) { + return $this->resolveOwner(ownerUid: ($application['@self']['owner'] ?? null)); + } + } + + return null; + }//end resolveOnDiskOwner() + + /** + * Persist a `job` object into the OpenConnector register (create or update). + * + * @param array $data The job data props. + * @param string|null $uuid The existing job uuid (null → create). + * + * @return void + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function saveJob(array $data, ?string $uuid): void + { + try { + $this->objectService->saveObject( + object: $data, + register: self::OC_REGISTER_SLUG, + schema: self::OC_JOB_SCHEMA_SLUG, + uuid: $uuid, + _rbac: false + ); + } catch (Throwable $e) { + $this->logger->error( + message: '[AppHost\\Scheduling] Failed to persist reconciled job', + context: ['reference' => ($data['reference'] ?? ''), 'reason' => $e->getMessage()] + ); + } + }//end saveJob() + + /** + * Extract the stable application identifier from an application object. + * + * @param array $application The rendered application object. + * + * @return string|null The uuid (preferred) or id, or null when absent. + */ + private function extractApplicationId(array $application): ?string + { + $id = ($application['@self']['uuid'] ?? ($application['@self']['id'] ?? null)); + if ($id === null || $id === '') { + return null; + } + + return (string) $id; + }//end extractApplicationId() + + /** + * Extract the effective manifest for a virtual application. + * + * Prefers an inline `manifest` on the application object (legacy / on-disk + * shape); when absent — the normal OpenBuild case, where the manifest is + * stored on the production ApplicationVersion rather than the application + * object — resolves the production version and reads its manifest. + * + * @param array $application The rendered application object. + * + * @return array The decoded manifest (empty when absent/invalid). + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + private function extractManifest(array $application): array + { + $inline = $this->decodeManifest(value: ($application['manifest'] ?? null)); + if ($inline !== []) { + return $inline; + } + + return $this->resolveProductionManifest(application: $application); + }//end extractManifest() + + /** + * Resolve a virtual app's manifest from its production ApplicationVersion. + * + * @param array $application The rendered application object. + * + * @return array The decoded production manifest, or empty. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function resolveProductionManifest(array $application): array + { + $versionId = ($application['productionVersion'] ?? ($application['@self']['relations']['productionVersion'] ?? null)); + if (is_string($versionId) === false || $versionId === '') { + return []; + } + + try { + $version = $this->objectService->find( + id: $versionId, + register: self::OB_REGISTER_SLUG, + schema: self::OB_APPLICATION_VERSION_SCHEMA_SLUG, + _rbac: false + ); + } catch (Throwable $e) { + $this->logger->info( + message: '[AppHost\\Scheduling] Could not resolve production version manifest', + context: ['versionId' => $versionId, 'reason' => $e->getMessage()] + ); + return []; + } + + if ($version === null) { + return []; + } + + // JsonSerialize() renders the version's data props (incl. `manifest`) + // at the top level; read the dynamic offset via a generic-typed helper. + $serialized = $version->jsonSerialize(); + return $this->decodeManifest(value: $this->readOffset(data: $serialized, key: 'manifest')); + }//end resolveProductionManifest() + + /** + * Read a dynamic offset from an entity's serialized data map. + * + * @param array $data The serialized object data. + * @param string $key The offset to read. + * + * @return mixed The value at the offset, or null when absent. + */ + private function readOffset(array $data, string $key): mixed + { + return ($data[$key] ?? null); + }//end readOffset() + + /** + * Decode a manifest value that may be a JSON string or an already-decoded array. + * + * @param mixed $value The raw manifest value. + * + * @return array The decoded manifest (empty when absent/invalid). + */ + private function decodeManifest(mixed $value): array + { + if (is_string($value) === true) { + $decoded = json_decode($value, associative: true); + $value = null; + if (is_array($decoded) === true) { + $value = $decoded; + } + } + + if (is_array($value) === true) { + return $value; + } + + return []; + }//end decodeManifest() + + /** + * Parse a stored `nextRun` value into a comparable DateTime. + * + * @param mixed $value The stored nextRun (ISO-8601 string) or null. + * + * @return DateTime|null The parsed DateTime, or null when unparseable/absent. + */ + private function parseNextRun(mixed $value): ?DateTime + { + if (is_string($value) === false || trim($value) === '') { + return null; + } + + try { + return new DateTime($value); + } catch (Throwable $e) { + return null; + } + }//end parseNextRun() + + /** + * Log a manifest's rejected-schedule diagnostics (secret-free). + * + * @param string $applicationId The owning application identifier. + * @param string[] $diagnostics The diagnostics to log. + * + * @return void + */ + private function logDiagnostics(string $applicationId, array $diagnostics): void + { + foreach ($diagnostics as $diagnostic) { + $this->logger->warning( + message: '[AppHost\\Scheduling] Rejected schedule entry: '.$diagnostic, + context: ['applicationId' => $applicationId] + ); + } + }//end logDiagnostics() +}//end class diff --git a/lib/AppHost/Scheduling/ScheduleValidationException.php b/lib/AppHost/Scheduling/ScheduleValidationException.php new file mode 100644 index 0000000000..50a294424a --- /dev/null +++ b/lib/AppHost/Scheduling/ScheduleValidationException.php @@ -0,0 +1,39 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Scheduling; + +use Exception; + +/** + * Raised for a structurally-invalid single schedule entry. + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ +class ScheduleValidationException extends Exception +{ +}//end class diff --git a/lib/AppHost/Service/AppHostSettingsService.php b/lib/AppHost/Service/AppHostSettingsService.php new file mode 100644 index 0000000000..0092b9f525 --- /dev/null +++ b/lib/AppHost/Service/AppHostSettingsService.php @@ -0,0 +1,347 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Service; + +use OCP\App\IAppManager; +use OCP\IAppConfig; +use OCP\IGroupManager; +use OCP\IUserSession; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Generic settings service for AppHost-adopting apps. + * + * Behavioural parity with the bespoke per-app `SettingsService`: + * - `getSettings()` → flat config map + `openregisters` + `isAdmin`. + * - `updateSettings()`→ writes configured keys, returns refreshed settings. + * - `loadConfiguration()` → imports register JSON via ConfigurationService. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.1 + */ +class AppHostSettingsService +{ + /** + * Default configuration keys managed for an AppHost leaf app. + * + * @var array + */ + protected const DEFAULT_CONFIG_KEYS = ['register']; + + /** + * Constructor. + * + * @param string $appId The calling (leaf) app id. + * @param IAppConfig $appConfig App config store. + * @param IAppManager $appManager App manager (OR availability). + * @param ContainerInterface $container DI container (lazy OR service resolution). + * @param IGroupManager $groupManager Group manager (admin check). + * @param IUserSession $userSession Current user session. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly string $appId, + private readonly IAppConfig $appConfig, + private readonly IAppManager $appManager, + private readonly ContainerInterface $container, + private readonly IGroupManager $groupManager, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Configuration keys this service manages. + * + * Overridable hook: an app with extra config keys subclasses this service + * and widens the list. Defaults to `['register']`, matching the fleet. + * + * @return array + */ + protected function configKeys(): array + { + return static::DEFAULT_CONFIG_KEYS; + }//end configKeys() + + /** + * Whether OpenRegister is installed and enabled. + * + * @return bool + */ + public function isOpenRegisterAvailable(): bool + { + return $this->appManager->isInstalled('openregister'); + }//end isOpenRegisterAvailable() + + /** + * Retrieve all current settings. + * + * Returns the configured keys plus `openregisters` (availability) and + * `isAdmin`. The controller strips admin-sensitive keys for non-admins; + * this method does not — it is the controller's gating responsibility. + * + * @return array + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.1 + */ + public function getSettings(): array + { + $settings = []; + foreach ($this->configKeys() as $key) { + $settings[$key] = $this->appConfig->getValueString($this->appId, $key, ''); + } + + $user = $this->userSession->getUser(); + $isAdmin = ($user !== null && $this->groupManager->isAdmin($user->getUID())); + + return array_merge( + $settings, + [ + 'openregisters' => $this->isOpenRegisterAvailable(), + 'isAdmin' => $isAdmin, + ] + ); + }//end getSettings() + + /** + * Update the managed settings with the provided data. + * + * Only keys in {@see configKeys()} are written; unknown keys are ignored. + * + * @param array $data Submitted data. + * + * @return array Refreshed settings. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.1 + */ + public function updateSettings(array $data): array + { + foreach ($this->configKeys() as $key) { + if (isset($data[$key]) === true) { + $this->appConfig->setValueString($this->appId, $key, (string) $data[$key]); + } + } + + return $this->getSettings(); + }//end updateSettings() + + /** + * Import the app's register JSON via OpenRegister's ConfigurationService. + * + * Repair-step-safe: when OR is unavailable this returns a failure result + * rather than throwing, so a disabled OR degrades instead of fataling. + * + * @param bool $force Force re-import even when already configured. + * + * @return array Result with `success`, `message`, and `version`. + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.1 + */ + public function loadConfiguration(bool $force=false): array + { + if ($this->isOpenRegisterAvailable() === false) { + $this->logger->warning(sprintf('[AppHost:%s] OpenRegister not available, skipping register initialization', $this->appId)); + return [ + 'success' => false, + 'message' => 'OpenRegister is not installed or enabled.', + ]; + } + + try { + [$data, $version] = $this->resolveRegisterConfiguration(); + if ($data === null) { + $message = sprintf( + 'Configuration file %s_register.json not found under the %s app path.', + $this->appId, + $this->appId + ); + $this->logger->warning(sprintf('[AppHost:%s] %s', $this->appId, $message)); + return [ + 'success' => false, + 'message' => $message, + ]; + } + + $configurationService = $this->container->get('OCA\OpenRegister\Service\ConfigurationService'); + $result = $configurationService->importFromApp(appId: $this->appId, data: $data, version: $version, force: $force); + + if (empty($result) === false) { + $this->logger->info(sprintf('[AppHost:%s] register configuration imported successfully', $this->appId)); + // Stamp the app version the config was imported for, so the admin + // page can show a REAL up-to-date check (config_version === app version). + $this->appConfig->setValueString($this->appId, 'config_version', $this->appManager->getAppVersion($this->appId)); + return [ + 'success' => true, + 'message' => 'Configuration imported successfully.', + 'version' => ($result['version'] ?? 'unknown'), + ]; + } + + return [ + 'success' => false, + 'message' => 'Import returned an empty result.', + ]; + } catch (Throwable $e) { + $this->logger->error( + sprintf('[AppHost:%s] configuration import failed', $this->appId), + ['exception' => $e->getMessage()] + ); + return [ + 'success' => false, + 'message' => $e->getMessage(), + ]; + }//end try + }//end loadConfiguration() + + /** + * Resolve the leaf app's register JSON + `register.d/` fragments so they can be + * passed to {@see \OCA\OpenRegister\Service\ConfigurationService::importFromApp()}, + * which requires both a `$data` array and a `$version` string. + * + * Mirrors the fleet convention hand-rolled by every bespoke per-app + * `SettingsService::doLoadConfiguration()` (e.g. openbuild, procest, scholiq, + * pipelinq): `lib/Settings/{appId}_register.json` as the base document, with + * `lib/Settings/register.d/*.json` fragments deep-merged on top in sorted + * filename order. The fragment signature (filename + content hash of every + * fragment) is folded into the returned version string so OpenRegister's + * version-gated import re-imports whenever a fragment changes, even when the + * base document's own `info.version` did not change. + * + * Uses {@see IAppManager::getAppPath()} to locate the leaf app's install + * directory, since - unlike each app's own bespoke SettingsService - this + * generic service lives inside OpenRegister itself and has no `__DIR__` + * relative to the calling (leaf) app. + * + * @return array{0: array|null, 1: string} `[$data, $version]`; + * `$data` is `null` when + * no register JSON was found. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.1 + */ + private function resolveRegisterConfiguration(): array + { + try { + $appPath = $this->appManager->getAppPath($this->appId); + } catch (Throwable $e) { + return [null, '']; + } + + $configPath = $appPath.'/lib/Settings/'.$this->appId.'_register.json'; + if (file_exists($configPath) === false) { + return [null, '']; + } + + $configContent = file_get_contents($configPath); + if ($configContent === false) { + return [null, '']; + } + + $configData = json_decode($configContent, true); + if (json_last_error() !== JSON_ERROR_NONE || is_array($configData) === false) { + return [null, '']; + } + + // ADR-037: merge modular register fragments from Settings/register.d/*.json, + // same as every bespoke per-app SettingsService. + $fragmentDir = $appPath.'/lib/Settings/register.d'; + $fragmentSig = ''; + if (is_dir($fragmentDir) === true) { + $fragmentFiles = glob($fragmentDir.'/*.json'); + sort($fragmentFiles); + foreach ($fragmentFiles as $fragmentFile) { + $fragmentContent = file_get_contents($fragmentFile); + if ($fragmentContent === false) { + continue; + } + + $fragmentData = json_decode($fragmentContent, true); + if (json_last_error() !== JSON_ERROR_NONE || is_array($fragmentData) === false) { + continue; + } + + $configData = self::deepMergeConfig(base: $configData, overlay: $fragmentData); + $fragmentSig .= basename($fragmentFile).':'.md5($fragmentContent).';'; + } + } + + $version = (string) ($configData['info']['version'] ?? '0.0.0'); + if ($fragmentSig !== '') { + $version .= '+frag.'.substr(md5($fragmentSig), 0, 8); + } + + return [$configData, $version]; + }//end resolveRegisterConfiguration() + + /** + * Recursively deep-merges an overlay config onto a base config. + * + * Keyed (associative) arrays are merged key-by-key (recursing into nested + * arrays); list arrays (sequential integer keys) are concatenated. Scalars + * in the overlay win. Identical semantics to every bespoke per-app + * `SettingsService::deepMergeConfig()` (e.g. openbuild), duplicated here so + * the generic AppHost path merges `register.d/` fragments the same way. + * + * @param array $base The base configuration array. + * @param array $overlay The overlay to merge onto the base. + * + * @return array The merged configuration. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.1 + */ + private static function deepMergeConfig(array $base, array $overlay): array + { + foreach ($overlay as $key => $value) { + $bothArrays = (is_array($value) === true + && isset($base[$key]) === true + && is_array($base[$key]) === true); + if ($bothArrays === false) { + $base[$key] = $value; + continue; + } + + $baseIsList = ($base[$key] === [] || array_keys($base[$key]) === range(0, (count($base[$key]) - 1))); + $overlayIsList = ($value === [] || array_keys($value) === range(0, (count($value) - 1))); + if ($baseIsList === true && $overlayIsList === true) { + $base[$key] = array_merge($base[$key], $value); + continue; + } + + $base[$key] = self::deepMergeConfig(base: $base[$key], overlay: $value); + } + + return $base; + }//end deepMergeConfig() +}//end class diff --git a/lib/AppHost/Service/GenericActionAuthService.php b/lib/AppHost/Service/GenericActionAuthService.php new file mode 100644 index 0000000000..a08a36d967 --- /dev/null +++ b/lib/AppHost/Service/GenericActionAuthService.php @@ -0,0 +1,221 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Service; + +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\IAppConfig; +use OCP\IGroupManager; +use OCP\IUser; + +/** + * Generic action-level authorization service for AppHost-adopting apps. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.2 + */ +class GenericActionAuthService +{ + /** + * IAppConfig key holding the JSON-encoded matrix. + */ + protected const CONFIG_KEY = 'actions'; + + /** + * Constructor. + * + * @param string $appId The calling (leaf) app id. + * @param IAppConfig $appConfig App config store for the matrix. + * @param IGroupManager $groupManager Group manager for resolving user groups. + */ + public function __construct( + private readonly string $appId, + private readonly IAppConfig $appConfig, + private readonly IGroupManager $groupManager + ) { + }//end __construct() + + /** + * Require that the user may perform the named action. + * + * Admin users always pass. Non-admins pass only when any of their groups + * intersects the matrix entry. Fail-closed: an unknown action or an + * admin-only entry denies non-admins. + * + * @param IUser $user The authenticated user. + * @param string $action Dot-separated action name (e.g. "item.publish"). + * + * @return void + * + * @throws OCSForbiddenException When the user's groups don't match. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.2 + */ + public function requireAction(IUser $user, string $action): void + { + if ($this->groupManager->isAdmin($user->getUID()) === true) { + return; + } + + $allowedGroups = $this->getAllowedGroups(action: $action); + + if (count($allowedGroups) === 0 || $allowedGroups === ['admin']) { + throw new OCSForbiddenException("Action '{$action}' requires admin rights"); + } + + $userGroups = $this->groupManager->getUserGroupIds($user); + $nonAdminAllowed = array_values(array_diff($allowedGroups, ['admin'])); + + if (count(array_intersect($userGroups, $nonAdminAllowed)) === 0) { + throw new OCSForbiddenException("Action '{$action}' not allowed for your groups"); + } + }//end requireAction() + + /** + * Check whether the user may perform the named action (non-throwing). + * + * @param IUser $user The authenticated user. + * @param string $action Dot-separated action name. + * + * @return bool True if the user may perform the action. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.2 + */ + public function can(IUser $user, string $action): bool + { + try { + $this->requireAction(user: $user, action: $action); + return true; + } catch (OCSForbiddenException $e) { + return false; + } + }//end can() + + /** + * Groups allowed to perform the action, or ["admin"] when undeclared. + * + * @param string $action Dot-separated action name. + * + * @return array + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.2 + */ + public function getAllowedGroups(string $action): array + { + $matrix = $this->getMatrix(); + return $matrix[$action] ?? ['admin']; + }//end getAllowedGroups() + + /** + * The full action-to-groups matrix. Missing/malformed config → empty + * (default-deny: getAllowedGroups falls back to ["admin"]). + * + * @return array> + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.2 + */ + public function getMatrix(): array + { + $json = $this->appConfig->getValueString($this->appId, static::CONFIG_KEY, '{}'); + + try { + $decoded = json_decode($json, associative: true, depth: 512, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + return []; + } + + if (is_array($decoded) === false) { + return []; + } + + $matrix = []; + foreach ($decoded as $action => $groups) { + if (is_string($action) === false || is_array($groups) === false) { + continue; + } + + $clean = []; + foreach ($groups as $group) { + if (is_string($group) === true && $group !== '') { + $clean[] = $group; + } + } + + $matrix[$action] = array_values(array_unique($clean)); + } + + return $matrix; + }//end getMatrix() + + /** + * Write the full matrix. Caller MUST enforce admin-only before invoking; + * this method does not gate writes (called from an admin-only endpoint). + * + * @param array> $matrix The new matrix. + * + * @return void + * + * @throws \JsonException When the matrix cannot be encoded. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.2 + */ + public function setMatrix(array $matrix): void + { + $normalized = []; + foreach ($matrix as $action => $groups) { + $clean = []; + foreach ($groups as $group) { + if ($group !== '') { + $clean[] = $group; + } + } + + $normalized[$action] = array_values(array_unique($clean)); + } + + $json = json_encode($normalized, flags: JSON_THROW_ON_ERROR); + $this->appConfig->setValueString($this->appId, static::CONFIG_KEY, $json); + }//end setMatrix() + + /** + * List all action keys currently in the matrix. + * + * @return array + */ + public function getActions(): array + { + return array_keys($this->getMatrix()); + }//end getActions() +}//end class diff --git a/lib/AppHost/Settings/GenericAdminSettings.php b/lib/AppHost/Settings/GenericAdminSettings.php new file mode 100644 index 0000000000..a3343cf50b --- /dev/null +++ b/lib/AppHost/Settings/GenericAdminSettings.php @@ -0,0 +1,141 @@ +` and by that + * attribute. `getAuthorizedAppConfig()` defaults to empty (no delegatable + * sub-keys), which scopes the endpoint to full admins — fail-closed, NOT + * fail-open: the bespoke admin gating is preserved exactly. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Settings + * @package OCA\OpenRegister\AppHost\Settings + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Settings; + +use OCP\App\IAppManager; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IAppConfig; +use OCP\Settings\IDelegatedSettings; + +/** + * Generic admin settings form for AppHost-adopting apps. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.4 + */ +class GenericAdminSettings implements IDelegatedSettings +{ + /** + * Constructor. + * + * @param string $appId The leaf app id (template + version). + * @param string $sectionId The settings section id this form sits in. + * @param int $priority Ordering priority within the section. + * @param IAppManager $appManager App manager (version lookup). + * @param IInitialState $initialState Initial-state service. + * @param IAppConfig|null $appConfig App config — when provided, enables a + * real up-to-date check by comparing the + * running app version against the version + * stored when the configuration was last + * (re-)imported. Nullable so the older + * registration factories keep working. + */ + public function __construct( + protected readonly string $appId, + protected readonly string $sectionId, + protected readonly int $priority, + protected readonly IAppManager $appManager, + protected readonly IInitialState $initialState, + protected readonly ?IAppConfig $appConfig=null + ) { + }//end __construct() + + /** + * Get the settings form template (the leaf app's `settings/admin`). + * + * Provides `version` (running app version) and, when an IAppConfig is + * available, `configuredVersion` + `isUpToDate` — a REAL up-to-date signal + * (the config version is stamped by AppHostSettingsService on each import). + * The shared CnAdminSettingsShell reads these via loadState so the badge is + * truthful instead of hardcoded. + * + * @return TemplateResponse + */ + public function getForm(): TemplateResponse + { + $version = $this->appManager->getAppVersion(appId: $this->appId); + $this->initialState->provideInitialState('version', $version); + + if ($this->appConfig !== null) { + $configuredVersion = $this->appConfig->getValueString($this->appId, 'config_version', ''); + $this->initialState->provideInitialState('configuredVersion', $configuredVersion); + // Up to date when the config was imported for the current app version. + // Empty configuredVersion (never imported) → not up to date. + $this->initialState->provideInitialState('isUpToDate', ($configuredVersion !== '' && $configuredVersion === $version)); + } + + return new TemplateResponse($this->appId, 'settings/admin', []); + }//end getForm() + + /** + * Section id this settings page belongs to. + * + * @return string + */ + public function getSection(): string + { + return $this->sectionId; + }//end getSection() + + /** + * Ordering priority within the section. + * + * @return int + */ + public function getPriority(): int + { + return $this->priority; + }//end getPriority() + + /** + * Human-readable name of the delegated settings section. + * + * @return string|null Null to use the section default. + */ + public function getName(): ?string + { + return null; + }//end getName() + + /** + * App-config keys a delegated admin may manage. Empty = full-admin only. + * + * @return array Map of appId to allowed config keys. + */ + public function getAuthorizedAppConfig(): array + { + return []; + }//end getAuthorizedAppConfig() +}//end class diff --git a/lib/AppHost/Settings/GenericSettingsSection.php b/lib/AppHost/Settings/GenericSettingsSection.php new file mode 100644 index 0000000000..f9bd0dbf35 --- /dev/null +++ b/lib/AppHost/Settings/GenericSettingsSection.php @@ -0,0 +1,101 @@ +`, so the leaf keeps a one-line subclass. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Settings + * @package OCA\OpenRegister\AppHost\Settings + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\AppHost\Settings; + +use OCP\IURLGenerator; +use OCP\Settings\IIconSection; + +/** + * Generic admin settings section for AppHost-adopting apps. + * + * @spec openspec/changes/apphost-boilerplate-controllers/tasks.md#task-2.4 + */ +class GenericSettingsSection implements IIconSection +{ + /** + * Constructor. + * + * @param string $sectionId The section identifier. + * @param string $name Human-readable section display name. + * @param string $appId Owning app id (for the icon image path). + * @param string $iconFile Icon file name within the app's img/ dir. + * @param int $priority Section ordering priority. + * @param IURLGenerator $urlGenerator URL generator (icon path). + */ + public function __construct( + protected readonly string $sectionId, + protected readonly string $name, + protected readonly string $appId, + protected readonly string $iconFile, + protected readonly int $priority, + protected readonly IURLGenerator $urlGenerator + ) { + }//end __construct() + + /** + * The section identifier. + * + * @return string + */ + public function getID(): string + { + return $this->sectionId; + }//end getID() + + /** + * The display name of this section. + * + * @return string + */ + public function getName(): string + { + return $this->name; + }//end getName() + + /** + * The ordering priority for this section. + * + * @return int + */ + public function getPriority(): int + { + return $this->priority; + }//end getPriority() + + /** + * The icon path for this section. + * + * @return string + */ + public function getIcon(): string + { + return $this->urlGenerator->imagePath(appName: $this->appId, file: $this->iconFile); + }//end getIcon() +}//end class diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 4423dc07a0..1039d63525 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -30,7 +30,9 @@ use OCA\OpenRegister\Service\Translation\IdentityTranslationProvider; use OCA\OpenRegister\Service\Translation\TranslationProviderInterface; use OCA\OpenRegister\Db\SearchTrailMapper; +use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Db\ViewMapper; use OCA\OpenRegister\Db\MappingMapper; @@ -89,15 +91,7 @@ use OCA\OpenRegister\Service\File\FolderManagementHandler; use OCA\OpenRegister\Service\Object\CacheHandler; use OCA\OpenRegister\Service\ImportService; -use OCA\OpenRegister\Service\Index\Backends\SolrBackend; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrHttpClient; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrCollectionManager; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrDocumentIndexer; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrQueryExecutor; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrFacetProcessor; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrSchemaManager; use OCA\OpenRegister\Service\ExportService; -use OCA\OpenRegister\Service\IndexService; use OCA\OpenRegister\Service\Vectorization\VectorEmbeddings; use OCA\OpenRegister\Service\VectorizationService; use OCA\OpenRegister\Service\Vectorization\Strategies\FileVectorizationStrategy; @@ -118,18 +112,12 @@ use OCA\OpenRegister\Service\Settings\FileSettingsHandler; use OCA\OpenRegister\Service\Settings\ObjectRetentionHandler; use OCA\OpenRegister\Service\Settings\CacheSettingsHandler; -use OCA\OpenRegister\Service\Settings\SolrSettingsHandler; use OCA\OpenRegister\Service\Settings\ConfigurationSettingsHandler; -use OCA\OpenRegister\Service\Index\SetupHandler; use OCA\OpenRegister\Service\Schemas\SchemaCacheHandler; -use OCA\OpenRegister\Command\SolrDebugCommand; -use OCA\OpenRegister\Command\SolrManagementCommand; use OCA\OpenRegister\Service\Schemas\FacetCacheHandler; use OCA\OpenRegister\Search\ObjectsProvider; use OCA\OpenRegister\Service\DeepLinkRegistryService; use OCA\OpenRegister\Event\DeepLinkRegistrationEvent; -use OCA\OpenRegister\BackgroundJob\SolrWarmupJob; -use OCA\OpenRegister\BackgroundJob\SolrNightlyWarmupJob; use OCA\OpenRegister\BackgroundJob\NameCacheWarmupJob; use OCA\OpenRegister\BackgroundJob\BlobMigrationJob; use OCA\OpenRegister\BackgroundJob\CronFileTextExtractionJob; @@ -141,7 +129,6 @@ use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\EventDispatcher\IEventDispatcher; use OCP\Security\IContentSecurityPolicyManager; -use OCA\OpenRegister\EventListener\SolrEventListener; use OCA\OpenRegister\Listener\CommentsEntityListener; use OCA\OpenRegister\Listener\FileChangeListener; use OCA\OpenRegister\Listener\ObjectChangeListener; @@ -150,21 +137,58 @@ use OCA\OpenRegister\Listener\GraphQLSubscriptionListener; use OCA\OpenRegister\Listener\NotifyPushListener; use OCA\OpenRegister\Listener\WebhookEventListener; +use OCA\OpenRegister\Listener\ActionListener; use OCA\OpenRegister\Listener\FilesSidebarListener; use OCA\OpenRegister\Listener\AggregationCacheInvalidationListener; use OCA\OpenRegister\Listener\AggregationThresholdListener; -use OCA\OpenRegister\Listener\RealtimeEventListener; use OCA\OpenRegister\Listener\TranslationProjectionListener; use OCA\OpenRegister\Listener\AnnotationNotificationListener; +use OCA\OpenRegister\Listener\FlowActionListener; +use OCA\OpenRegister\Listener\SystemEntityNotificationListener; +use OCA\OpenRegister\Listener\NotificationDedupeAnnotationSyncListener; +use OCA\OpenRegister\Listener\NotificationDedupePruneListener; use OCA\OpenRegister\Service\Notification\NotificationsAnnotationInstaller; use OCA\OpenRegister\Notification\AnnotationNotifier; use OCA\OpenRegister\Listener\CalculationOnSaveListener; +use OCA\OpenRegister\Listener\QualityScoreOnSaveListener; +use OCA\OpenRegister\Listener\ObjectMetricsListener; +use OCA\OpenRegister\Listener\SourceRecordChangeListener; +use OCA\OpenRegister\Listener\SurvivorshipRecomputeListener; use OCA\OpenRegister\Listener\MailAppScriptListener; use OCA\OpenRegister\Listener\HookListener; +use OCA\OpenRegister\Listener\HandoffLifecycleListener; +use OCA\OpenRegister\Listener\HandoffQueueDrainListener; +use OCA\OpenRegister\Listener\LifecycleActionListener; use OCA\OpenRegister\Listener\LifecycleInitialStateListener; use OCA\OpenRegister\Listener\LifecycleValidationListener; +use OCA\OpenRegister\Listener\ApprovalChainGateListener; +use OCA\OpenRegister\Listener\ApprovalChainAdvanceListener; +use OCA\OpenRegister\Service\ApprovalChainAnnotationInstaller; +use OCA\OpenRegister\Event\ApprovalStepCompletedEvent; use OCA\OpenRegister\Service\NoteService; use OCA\OpenRegister\Service\TaskService; +use OCA\OpenRegister\Service\ObjectSource\ObjectSourceRegistry; +use OCA\OpenRegister\Service\ObjectSource\CalDavVtodoObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\UserDirectoryObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\GroupObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\ContactsObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\CalendarEventObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\FilesObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\DeckObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\TalkObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\FederatedObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\TablesObjectSourceProvider; +use OCA\OpenRegister\Service\ObjectSource\TablesTableReader; +use OCA\OpenRegister\Service\ObjectSource\TablesColumnMapper; +use OCA\OpenRegister\Service\ObjectSource\TablesUuidDeriver; +use OCA\OpenRegister\Service\ObjectSource\TablesSchemaSyncService; +use OCA\OpenRegister\Listener\TablesTableDeletedListener; +use OCA\OpenRegister\Service\ObjectSource\DbalObjectSourceProvider; +use OCA\OpenRegister\Federation\OpenRegisterCloudFederationProvider; +use OCP\Federation\ICloudFederationProviderManager; +use OCP\IAppConfig; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\Comments\CommentsEntityEvent; use OCP\Files\Events\Node\NodeCreatedEvent; use OCP\Files\Events\Node\NodeWrittenEvent; @@ -223,10 +247,14 @@ use OCA\OpenRegister\Controller\IntegrationsController; use OCA\OpenRegister\Controller\ObjectIntegrationsController; use OCA\OpenRegister\Mcp\IMcpToolProvider; +use OCA\OpenRegister\Mcp\IMcpScannableServices; +use OCA\OpenRegister\Mcp\AttributeToolScanner; use OCA\OpenRegister\Mcp\BuiltIn\RegistersToolProvider; use OCA\OpenRegister\Mcp\BuiltIn\SchemasToolProvider; use OCA\OpenRegister\Mcp\BuiltIn\ObjectsToolProvider; use OCA\OpenRegister\Mcp\BuiltIn\IntegrationsToolProvider; +use OCA\OpenRegister\Mcp\BuiltIn\SchemaDerivedToolProvider; +use OCA\OpenRegister\Mcp\BuiltIn\AttributeToolProvider; use OCA\OpenRegister\Repair\LogDanglingLinkedTypes; use OCA\OpenRegister\Service\Integration\BuiltinProviders\AuditTrailProvider; use OCA\OpenRegister\Service\Integration\BuiltinProviders\FilesProvider; @@ -236,6 +264,7 @@ use OCA\OpenRegister\Service\Integration\ExternalIntegrationRouter; use OCA\OpenRegister\Service\Integration\IntegrationRegistry; use OCA\OpenRegister\Service\Integration\PropertyReferenceTypeValidator; +use OCA\OpenRegister\Service\Integration\PropertySemanticReferenceValidator; use OCA\OpenRegister\Service\Integration\Providers\BookmarksProvider; use OCA\OpenRegister\Service\Integration\Providers\CalendarProvider; use OCA\OpenRegister\Service\Integration\Providers\ContactsProvider; @@ -243,6 +272,9 @@ use OCA\OpenRegister\Service\Integration\Providers\EmailProvider; use OCA\OpenRegister\Service\Integration\Providers\OpenProjectProvider; use OCA\OpenRegister\Service\Integration\Providers\PollsProvider; +use OCA\OpenRegister\Service\Integration\Providers\KvkProvider; +use OCA\OpenRegister\Service\Integration\Providers\MessageDispatchProvider; +use OCA\OpenRegister\Service\Integration\Providers\OpenCorporatesProvider; use OCA\OpenRegister\Service\Integration\Providers\SharesProvider; use OCA\OpenRegister\Service\Integration\Providers\TalkProvider; use OCA\OpenRegister\Service\Integration\Providers\XwikiProvider; @@ -281,17 +313,30 @@ * * @link https://github.com/nextcloud/server/blob/master/apps-extra/openregister * + * Public services exposed for consumer apps via Nextcloud DI: + * - {@see \OCA\OpenRegister\Service\RegisterService} CRUD + helpers on Register entities. + * - {@see \OCA\OpenRegister\Service\SchemaService} CRUD + helpers on Schema entities. + * - {@see \OCA\OpenRegister\Service\ObjectService} CRUD + search on registered objects. + * - {@see \OCA\OpenRegister\Service\RegisterResolverService} Resolve `_register` / `_schema` + * IAppConfig keys to slug/UUID strings or hydrated entities; request-scoped cache + tenant aware. + * - {@see \OCA\OpenRegister\Service\Serializer\RegisterSerializer} Serialize Register entities with `_extend` + * expansion (schemas, @self.stats) — single source of truth shared between HTTP and DI callers. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Bootstrap class must reference every * service, mapper, event listener, and middleware registered for the entire app; the * coupling is structural and cannot be reduced without breaking the NC bootstrap contract. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Service registration methods enumerate + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Service registration methods enumerate * all DI bindings in one place per NC bootstrap pattern; splitting them would spread * the wire-up across unrelated files and obscure the dependency graph. - * @SuppressWarnings(PHPMD.ExcessiveClassLength) Single IBootstrap implementation per NC + * @SuppressWarnings(PHPMD.ExcessiveClassLength) Single IBootstrap implementation per NC * app-info convention; all register/boot logic must live in this one class. - * @SuppressWarnings(PHPMD.UnusedFormalParameter) IBootstrap::boot(IBootContext) signature + * @SuppressWarnings(PHPMD.UnusedFormalParameter) IBootstrap::boot(IBootContext) signature * is fixed by the NC framework contract; the $context parameter may not be used in * every version of the boot method. + * @SuppressWarnings(PHPMD.TooManyMethods) Bootstrap class groups its register/boot + * wire-up into per-concern helper methods (services, listeners, middleware, capabilities); + * inlining them to satisfy the method-count threshold would recreate one unreadable + * mega-method that the ExcessiveMethodLength suppression already guards against. */ class Application extends App implements IBootstrap { @@ -302,12 +347,47 @@ class Application extends App implements IBootstrap */ public const APP_ID = 'openregister'; + /** + * Distributed-cache prefix for the per-app MCP provider discovery map. + * + * @var string + */ + private const MCP_PROBE_CACHE_PREFIX = 'openregister_mcp_provider_probe'; + + /** + * IAppConfig key holding the MCP provider discovery-cache TTL in seconds. + * + * @var string + */ + private const MCP_PROBE_TTL_CONFIG_KEY = 'mcp.provider_probe_cache_ttl'; + + /** + * Default MCP provider discovery-cache TTL in seconds. + * + * @var integer + */ + private const MCP_PROBE_TTL_DEFAULT = 60; + + /** + * Minimum allowed MCP provider discovery-cache TTL in seconds. + * + * @var integer + */ + private const MCP_PROBE_TTL_MIN = 10; + + /** + * Maximum allowed MCP provider discovery-cache TTL in seconds. + * + * @var integer + */ + private const MCP_PROBE_TTL_MAX = 600; + /** * Constructor for the Application class * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md */ public function __construct() { @@ -321,12 +401,27 @@ public function __construct() * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md + * @spec openspec/changes/credential-doriath-leaf/specs/credential-broker/spec.md#requirement-credential-store-backend-resolution */ public function register(IRegistrationContext $context): void { include_once __DIR__.'/../../vendor/autoload.php'; + // Credential broker (credential-broker-service + credential-doriath-leaf): + // bind the CredentialStore abstraction through the CredentialStoreResolver + // (design D-A) — the Doriath application-vault leaf when the doriath app is + // eligible (enabled + service classes + application-scoped seam methods + + // OR self-registration), else the NC encrypted per-user vault leaf. The + // broker and controller keep depending only on the CredentialStore + // interface; this factory is the ONLY place the selection happens. + $context->registerService( + \OCA\OpenRegister\Service\Credential\CredentialStore::class, + static function ($c) { + return $c->get(\OCA\OpenRegister\Service\Credential\CredentialStoreResolver::class)->resolve(); + } + ); + // Register request-scoped LanguageService as a singleton (shared per request). $context->registerService( LanguageService::class, @@ -363,17 +458,164 @@ function () { // Fail-OPEN: any limiter error allows the request through. $context->registerMiddleware(\OCA\OpenRegister\Middleware\RateLimitMiddleware::class); + // Register the PublicApiCorsMiddleware: reflects the request Origin onto + // `@PublicPage` responses so browsers on other origins (e.g. a marketing + // site posting a lead into a public-create schema) can read the result. + // Scoped to public endpoints only and never sets allow-credentials. + $context->registerMiddleware(\OCA\OpenRegister\Middleware\PublicApiCorsMiddleware::class); + + // Register the ChatCompatMiddleware (or-chat-proxy-deprecation): adds + // Deprecation/Sunset/Link response headers to every chat/agents/ + // conversations controller response and forwards those requests + // server-side to hermiq. ON by default since + // or-chat-engine-decommission; operators opt out via the + // `openregister.chat.proxyTo` appconfig value (any value != 'hermiq'). + // Any failure falls back to serving the request locally unchanged. + $context->registerMiddleware(\OCA\OpenRegister\Middleware\ChatCompatMiddleware::class); + + // Register the ObjectSourceErrorMiddleware (dbal-virtual-registers D8): + // maps DbalObjectSourceException thrown during reads against an + // external-database-backed schema onto its declared 502/503 status — + // an unreachable or failing external database must never surface as a + // bare 500. All other exceptions are rethrown untouched. + $context->registerMiddleware(\OCA\OpenRegister\Middleware\ObjectSourceErrorMiddleware::class); + + // Register the EncryptedFieldFilterMiddleware (field-level-object-encryption): + // maps EncryptedFieldFilterException — thrown when a caller filters/ + // searches/facets on a field flagged `x-openregister-encrypted` — onto a + // clear HTTP 400 instead of a silent zero-row result or a bare 500. + $context->registerMiddleware(\OCA\OpenRegister\Middleware\EncryptedFieldFilterMiddleware::class); + + // Bind the dormant Path B PDF anonymisation fallback bridge to its + // null implementation. Tenants enabling Path B replace this binding + // with a concrete NcOfficeConverterInterface implementation that + // talks to Collabora Online / Code (per the + // `pdf-anonymisation-odt-fallback` scaffold). + $context->registerService( + \OCA\OpenRegister\Service\File\Pdf\Fallback\NcOfficeConverterInterface::class, + function () { + return new \OCA\OpenRegister\Service\File\Pdf\Fallback\NullNcOfficeConverter(); + } + ); + + // DSAR case-engine (dsar-case-engine): bind the swappable PAdES signing + // seam to its default SHA-256-only stub. The real PAdES-LTV signer drops + // in here later by replacing this binding — the ExportBundleService + // depends only on the PadesSigner interface, so nothing else changes. + // TODO(ADR-047 Phase-1b, DEFERRED): PAdES-LTV signing deferred — the + // 2026-07-04 tc-lib-pdf spike was No-Go (8.65 stubs the B-T timestamp). + // Interim = SHA-256 hash-only. When resumed, bind a real PadesSigner + // against pyHanko (MIT sidecar) or a matured tc-lib-pdf; TSA URL config. + $context->registerService( + \OCA\OpenRegister\Service\Gdpr\Export\PadesSigner::class, + function () { + return new \OCA\OpenRegister\Service\Gdpr\Export\UnsignedPadesSigner(); + } + ); + + // DSAR evidence-source registry (shared, so leaf-app addProvider() + // registrations during boot() persist for the whole request). The + // harvest service auto-wires it; OR core enumerates only registered + // providers (ADR-019 — an unregistered source contributes nothing). + $context->registerService( + \OCA\OpenRegister\Service\Gdpr\Evidence\EvidenceSourceRegistry::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Gdpr\Evidence\EvidenceSourceRegistry( + logger: $container->get('Psr\Log\LoggerInterface') + ); + } + ); + + // DSAR integration seams (dsar-integration-seams): two pluggable seams — + // identity-verify and regulator-escalate — as shared per-request + // registries (so a leaf app's addProvider() during boot() persists for + // the whole request, ADR-019) each pre-seeded with the OR fail-closed + // default in its constructor. Resolution is pack-selector-driven and + // fail-closed: an unset/unknown selector resolves the default that + // refuses (never null, never "verified"/"escalated" — ADR-005/CWE-863). + $context->registerService( + \OCA\OpenRegister\Service\Gdpr\Identity\NullIdentityVerifyProvider::class, + function () { + return new \OCA\OpenRegister\Service\Gdpr\Identity\NullIdentityVerifyProvider(); + } + ); + $context->registerService( + \OCA\OpenRegister\Service\Gdpr\Identity\IdentityVerifyRegistry::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Gdpr\Identity\IdentityVerifyRegistry( + logger: $container->get('Psr\Log\LoggerInterface'), + default: $container->get(\OCA\OpenRegister\Service\Gdpr\Identity\NullIdentityVerifyProvider::class) + ); + } + ); + $context->registerService( + \OCA\OpenRegister\Service\Gdpr\Regulator\NullRegulatorEscalateProvider::class, + function () { + return new \OCA\OpenRegister\Service\Gdpr\Regulator\NullRegulatorEscalateProvider(); + } + ); + $context->registerService( + \OCA\OpenRegister\Service\Gdpr\Regulator\RegulatorEscalateRegistry::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Gdpr\Regulator\RegulatorEscalateRegistry( + logger: $container->get('Psr\Log\LoggerInterface'), + default: $container->get(\OCA\OpenRegister\Service\Gdpr\Regulator\NullRegulatorEscalateProvider::class) + ); + } + ); + + // Web Push hex-icon service (openregister-web-push-engine): needs an + // app-scoped IAppData for its rendered-PNG cache, which is not + // auto-wirable, so resolve it via IAppDataFactory here. + $context->registerService( + \OCA\OpenRegister\Service\WebPush\HexIconService::class, + function (\Psr\Container\ContainerInterface $c): \OCA\OpenRegister\Service\WebPush\HexIconService { + return new \OCA\OpenRegister\Service\WebPush\HexIconService( + $c->get(\OCP\App\IAppManager::class), + $c->get(\OCP\Files\AppData\IAppDataFactory::class)->get('openregister'), + $c->get(\Psr\Log\LoggerInterface::class) + ); + } + ); + + // Register the data-sync source-fetcher registry pre-loaded with the + // built-in REST/OpenRegister fetcher. Apps can register additional + // fetchers (OData, SOAP, CSV) by resolving and extending this service. + $context->registerService( + \OCA\OpenRegister\Service\Sync\SourceFetcherRegistry::class, + function ($c) { + $registry = new \OCA\OpenRegister\Service\Sync\SourceFetcherRegistry(); + $registry->register($c->get(\OCA\OpenRegister\Service\Sync\RestApiSourceFetcher::class)); + return $registry; + } + ); + + // Register the standards schema-import service with the bundled, + // versioned snapshot resources (schema-import-standards). Built + // explicitly so the resource root is deterministic; the dialect + // registry inside it makes DCAT/SKOS/ZGW importers follow-ups. + $context->registerService( + \OCA\OpenRegister\Service\SchemaImport\SchemaImportService::class, + function () { + return new \OCA\OpenRegister\Service\SchemaImport\SchemaImportService( + new \OCA\OpenRegister\Service\SchemaImport\DialectDetector(), + new \OCA\OpenRegister\Service\SchemaImport\ThreeWayMerge() + ); + } + ); + // Register all services in phases to resolve circular dependencies. $this->registerMappersWithCircularDependencies(context: $context); $this->registerCacheAndFileHandlers(context: $context); $this->registerConfigurationServices(context: $context); $this->registerSettingsServices(context: $context); - $this->registerSearchBackend(context: $context); $this->registerVectorizationService(context: $context); $this->registerObjectInteractionServices(context: $context); $this->registerIntegrationRegistry(context: $context); + $this->registerObjectSourceProviders(context: $context); $this->registerEventListeners(context: $context); $this->registerMcpToolProviders(context: $context); + $this->registerAppHostObservability(context: $context); // Register the annotation-driven INotifier so notifications fired by // AnnotationNotificationDispatcher get a parsed subject — without @@ -418,7 +660,7 @@ function () { * the length is structural to the NC DI pattern and cannot be shortened without * obscuring the circular-dependency resolution order. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md */ private function registerMappersWithCircularDependencies(IRegistrationContext $context): void { @@ -527,6 +769,25 @@ function (ContainerInterface $container) { } ); + // EntityRelationMapper is registered explicitly because it constructor-injects + // `IEventDispatcher` to dispatch `EntityRelationDecisionUpdatedEvent`. Every + // other event-dispatcher-dependent mapper in this method (SchemaMapper, + // RegisterMapper, MagicMapper, WebhookMapper) is wired explicitly to avoid + // depending on NC's autowirer resolution of framework-interface keys, which + // has historically been version-sensitive. + $context->registerService( + EntityRelationMapper::class, + function (ContainerInterface $container) { + return new EntityRelationMapper( + db: $container->get('OCP\IDBConnection'), + auditTrailMapper: $container->get(\OCA\OpenRegister\Db\AuditTrailMapper::class), + userSession: $container->get('OCP\IUserSession'), + eventDispatcher: $container->get('OCP\EventDispatcher\IEventDispatcher'), + logger: $container->get('Psr\Log\LoggerInterface') + ); + } + ); + }//end registerMappersWithCircularDependencies() /** @@ -536,11 +797,10 @@ function (ContainerInterface $container) { * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md */ private function registerCacheAndFileHandlers(IRegistrationContext $context): void { - // CacheHandler uses lazy loading of IndexService to break circular dependency. $context->registerService( CacheHandler::class, function (ContainerInterface $container) { @@ -568,6 +828,7 @@ function (ContainerInterface $container) { userSession: $container->get('OCP\IUserSession'), groupManager: $container->get('OCP\IGroupManager'), logger: $container->get('Psr\Log\LoggerInterface'), + auditTrailMapper: $container->get(\OCA\OpenRegister\Db\AuditTrailMapper::class), fileService: null ); } @@ -581,7 +842,7 @@ function (ContainerInterface $container) { * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md */ private function registerConfigurationServices(IRegistrationContext $context): void { @@ -670,6 +931,22 @@ function (ContainerInterface $container) { $logger->debug('[Application] IUserSession unavailable for ImportHandler: '.$e->getMessage()); } + // Optional: group/user managers used to resolve a fallback admin + // acting user when import runs without a logged-in session + // (occ/installer/cron). Wrapped so a missing dependency never + // breaks import. + try { + $importHandler->setGroupManager($container->get('OCP\IGroupManager')); + } catch (\Throwable $e) { + $logger->debug('[Application] IGroupManager unavailable for ImportHandler: '.$e->getMessage()); + } + + try { + $importHandler->setUserManager($container->get('OCP\IUserManager')); + } catch (\Throwable $e) { + $logger->debug('[Application] IUserManager unavailable for ImportHandler: '.$e->getMessage()); + } + return $importHandler; }; @@ -742,7 +1019,7 @@ function (ContainerInterface $container) { * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md */ private function registerSettingsServices(IRegistrationContext $context): void { @@ -783,7 +1060,6 @@ function (ContainerInterface $container) { fileSettingsHandler: $container->get(FileSettingsHandler::class), objRetentionHandler: $container->get(ObjectRetentionHandler::class), cacheSettingsHandler: $container->get(CacheSettingsHandler::class), - solrSettingsHandler: $container->get(SolrSettingsHandler::class), cfgSettingsHandler: $container->get(ConfigurationSettingsHandler::class) ); } @@ -802,36 +1078,6 @@ function (ContainerInterface $container) { ); }//end registerSettingsServices() - /** - * Register search backend interface with dynamic backend selection. - * - * @param IRegistrationContext $context The registration context - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 - */ - private function registerSearchBackend(IRegistrationContext $context): void - { - $context->registerService( - \OCA\OpenRegister\Service\Index\SearchBackendInterface::class, - function (ContainerInterface $container): \OCA\OpenRegister\Service\Index\SearchBackendInterface { - $settingsService = $container->get(SettingsService::class); - $backendConfig = $settingsService->getSearchBackendConfig(); - $activeBackend = $backendConfig['active'] ?? 'solr'; - - switch ($activeBackend) { - case 'elasticsearch': - return $container->get(\OCA\OpenRegister\Service\Index\Backends\ElasticsearchBackend::class); - - case 'solr': - default: - return $container->get(SolrBackend::class); - } - } - ); - }//end registerSearchBackend() - /** * Register vectorization service with strategies. * @@ -839,7 +1085,7 @@ function (ContainerInterface $container): \OCA\OpenRegister\Service\Index\Search * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md */ private function registerVectorizationService(IRegistrationContext $context): void { @@ -870,7 +1116,7 @@ function (ContainerInterface $container) { * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md */ private function registerObjectInteractionServices(IRegistrationContext $context): void { @@ -880,7 +1126,8 @@ function (ContainerInterface $container) { return new TaskService( calDavBackend: $container->get('OCA\DAV\CalDAV\CalDavBackend'), userSession: $container->get('OCP\IUserSession'), - logger: $container->get('Psr\Log\LoggerInterface') + logger: $container->get('Psr\Log\LoggerInterface'), + urlGenerator: $container->get('OCP\IURLGenerator') ); } ); @@ -896,6 +1143,15 @@ function (ContainerInterface $container) { ); } ); + + $context->registerService( + \OCA\OpenRegister\Service\Integration\TimeProvider::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Integration\TimeProvider( + appConfig: $container->get('OCP\IAppConfig') + ); + } + ); }//end registerObjectInteractionServices() /** @@ -912,7 +1168,7 @@ function (ContainerInterface $container) { * * @return void * - * @spec openspec/changes/pluggable-integration-registry/tasks.md#task-5 + * @spec openspec/specs/integration-registry/spec.md */ private function registerIntegrationRegistry(IRegistrationContext $context): void { @@ -950,6 +1206,20 @@ function (ContainerInterface $container) { } ); + // PropertySemanticReferenceValidator — validates the opt-in + // `referenceSemanticType` property marker (ADR-048, cross-app + // semantic references) as a well-formed absolute IRI. Standalone, + // mirroring PropertyReferenceTypeValidator; wire into schema-save + // when write-time enforcement is desired. + $context->registerService( + PropertySemanticReferenceValidator::class, + function (ContainerInterface $container) { + return new PropertySemanticReferenceValidator( + jsonLd: $container->get(\OCA\OpenRegister\Service\JsonLd\JsonLdContextService::class), + ); + } + ); + // Repair step LogDanglingLinkedTypes — runs on install + // post-migration to surface schemas whose linkedTypes contain // ids that the registry can no longer resolve. Strictly @@ -991,7 +1261,8 @@ function (ContainerInterface $container) { appName: 'openregister', request: $container->get('OCP\IRequest'), registry: $container->get(IntegrationRegistry::class), - logger: $container->get('Psr\Log\LoggerInterface') + logger: $container->get('Psr\Log\LoggerInterface'), + objectService: $container->get(\OCA\OpenRegister\Service\ObjectService::class) ); } ); @@ -1019,822 +1290,1937 @@ function (ContainerInterface $container) { }//end registerIntegrationRegistry() /** - * Register the 5 BuiltinProviders/* services so they can be - * resolved lazily from the container. + * Register the ObjectSourceRegistry (shared) and the built-in + * CalDAV-VTODO object-source provider service. * - * Each provider wraps an existing OR service and exposes it - * through the IntegrationProvider contract. They self-register - * with the IntegrationRegistry during `boot()` — - * `bootBuiltinIntegrationProviders()` walks this same list. + * The registry is shared so `addProvider()` registrations made during + * `boot()` persist for the whole request; GetObject auto-wires it. * * @param IRegistrationContext $context The registration context. * * @return void * - * @spec openspec/changes/pluggable-integration-registry/tasks.md#task-12 + * @spec openspec/changes/object-source-providers/tasks.md#1-provider-interface-registry */ - private function registerBuiltinIntegrationProviders(IRegistrationContext $context): void + private function registerObjectSourceProviders(IRegistrationContext $context): void { $context->registerService( - FilesProvider::class, + ObjectSourceRegistry::class, function (ContainerInterface $container) { - return new FilesProvider( - fileService: $container->get(\OCA\OpenRegister\Service\FileService::class), - container: $container, - l10n: $container->get('OCP\IL10N'), + return new ObjectSourceRegistry( + logger: $container->get('Psr\Log\LoggerInterface') ); } ); $context->registerService( - NotesProvider::class, + FederatedObjectSourceProvider::class, function (ContainerInterface $container) { - return new NotesProvider( - noteService: $container->get(\OCA\OpenRegister\Service\NoteService::class), - l10n: $container->get('OCP\IL10N'), + return new FederatedObjectSourceProvider( + clientService: $container->get('OCP\Http\Client\IClientService'), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); $context->registerService( - TasksProvider::class, + CalDavVtodoObjectSourceProvider::class, function (ContainerInterface $container) { - return new TasksProvider( - taskService: $container->get(\OCA\OpenRegister\Service\TaskService::class), - registerMapper: $container->get(\OCA\OpenRegister\Db\RegisterMapper::class), - schemaMapper: $container->get(\OCA\OpenRegister\Db\SchemaMapper::class), - objectService: $container->get(\OCA\OpenRegister\Service\ObjectService::class), - l10n: $container->get('OCP\IL10N'), + return new CalDavVtodoObjectSourceProvider( + taskService: $container->get(TaskService::class), + appManager: $container->get('OCP\App\IAppManager'), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); $context->registerService( - TagsProvider::class, + UserDirectoryObjectSourceProvider::class, function (ContainerInterface $container) { - return new TagsProvider( - tagManager: $container->get('OCP\SystemTag\ISystemTagManager'), - objectMapper: $container->get('OCP\SystemTag\ISystemTagObjectMapper'), - l10n: $container->get('OCP\IL10N'), + return new UserDirectoryObjectSourceProvider( + userManager: $container->get('OCP\IUserManager'), + userSession: $container->get('OCP\IUserSession'), + groupManager: $container->get('OCP\IGroupManager'), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); $context->registerService( - AuditTrailProvider::class, + GroupObjectSourceProvider::class, function (ContainerInterface $container) { - return new AuditTrailProvider( - mapper: $container->get(\OCA\OpenRegister\Db\AuditTrailMapper::class), - l10n: $container->get('OCP\IL10N'), + return new GroupObjectSourceProvider( + groupManager: $container->get('OCP\IGroupManager'), + userSession: $container->get('OCP\IUserSession'), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // Leaf provider: XWiki (external, OpenConnector-backed). Ships - // in-repo as the worked external-storage example; routed - // through ExternalIntegrationRouter, credentials on the - // OpenConnector `xwiki` source. - // @spec openspec/changes/integration-xwiki/tasks.md. $context->registerService( - XwikiProvider::class, + ContactsObjectSourceProvider::class, function (ContainerInterface $container) { - return new XwikiProvider( - router: $container->get(ExternalIntegrationRouter::class), + return new ContactsObjectSourceProvider( + contactsManager: $container->get('OCP\Contacts\IManager'), appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), - xwikiLinkMapper: $container->get(\OCA\OpenRegister\Db\XwikiLinkMapper::class), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // XwikiLinkService — Tier-2 link/create/unlink/picker service - // backing the XwikiLinksController. xWiki is external: the provider - // + router are resolved lazily so the service loads even when - // OpenConnector is absent, mirroring the unconfigured-source - // 503-with-cause pattern. - // @spec openspec/changes/integration-xwiki/tasks.md. $context->registerService( - \OCA\OpenRegister\Service\XwikiLinkService::class, + CalendarEventObjectSourceProvider::class, function (ContainerInterface $container) { - return new XwikiLinkService( - xwikiLinkMapper: $container->get(\OCA\OpenRegister\Db\XwikiLinkMapper::class), - container: $container, + return new CalendarEventObjectSourceProvider( + calendarEventService: $container->get(\OCA\OpenRegister\Service\CalendarEventService::class), appManager: $container->get('OCP\App\IAppManager'), - userSession: $container->get('OCP\IUserSession'), - logger: $container->get('Psr\Log\LoggerInterface'), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // Leaf provider: OpenProject (external, OpenConnector-backed) — - // mirrors the XwikiProvider pattern (AD-4 / AD-22). Credentials - // on the OpenConnector `openproject` source. Tier-2: the - // OpenProjectLinkMapper backs the linked list so it renders from - // the cached link rows even when the source is temporarily down. - // @spec openspec/changes/integration-openproject/tasks.md. $context->registerService( - OpenProjectProvider::class, + FilesObjectSourceProvider::class, function (ContainerInterface $container) { - return new OpenProjectProvider( - router: $container->get(ExternalIntegrationRouter::class), - appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), - linkMapper: $container->get(\OCA\OpenRegister\Db\OpenProjectLinkMapper::class), + return new FilesObjectSourceProvider( + rootFolder: $container->get('OCP\Files\IRootFolder'), + userSession: $container->get('OCP\IUserSession'), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // OpenProjectLinkService — Tier-2 link/create/unlink/picker - // service backing the OpenProjectLinksController. OpenProject is - // external; the service composes the link mapper with the - // OpenProjectProvider + ExternalIntegrationRouter so picker / - // create / refresh flows go through the OpenConnector - // `openproject` source, degrading to a 503-with-cause when the - // source is missing or the upstream is unreachable (wave-5.2). - // @spec openspec/changes/integration-openproject/tasks.md. $context->registerService( - \OCA\OpenRegister\Service\OpenProjectLinkService::class, + DeckObjectSourceProvider::class, function (ContainerInterface $container) { - return new OpenProjectLinkService( - openProjectLinkMapper: $container->get(\OCA\OpenRegister\Db\OpenProjectLinkMapper::class), - provider: $container->get(OpenProjectProvider::class), - router: $container->get(ExternalIntegrationRouter::class), + return new DeckObjectSourceProvider( appManager: $container->get('OCP\App\IAppManager'), userSession: $container->get('OCP\IUserSession'), - logger: $container->get('Psr\Log\LoggerInterface'), + container: $container, + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // Leaf providers — NC-native, "backend already shipped": - // wrap the existing OR services (Calendar, Contacts, Deck, Email) - // through the registry contract so they surface in the sidebar / - // widgets / admin UI / OCS caps without per-app glue. Each - // provider gates on its required NC app via IAppManager. - // @spec openspec/changes/integration-calendar/tasks.md. $context->registerService( - CalendarProvider::class, + TalkObjectSourceProvider::class, function (ContainerInterface $container) { - return new CalendarProvider( - calendarEventService: $container->get(\OCA\OpenRegister\Service\CalendarEventService::class), - calendarLinkService: $container->get(\OCA\OpenRegister\Service\CalendarLinkService::class), + return new TalkObjectSourceProvider( appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), - logger: $container->get(\Psr\Log\LoggerInterface::class), + userSession: $container->get('OCP\IUserSession'), + container: $container, + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // @spec openspec/changes/integration-contacts/tasks.md. + // DBAL virtual-register services + provider (dbal-virtual-registers). + // The connection factory depends only on the CredentialStore interface, + // which is already bound to the active leaf via CredentialStoreResolver. $context->registerService( - ContactsProvider::class, + \OCA\OpenRegister\Service\Dbal\DbalConnectionFactory::class, function (ContainerInterface $container) { - return new ContactsProvider( - contactService: $container->get(\OCA\OpenRegister\Service\ContactService::class), - appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), + return new \OCA\OpenRegister\Service\Dbal\DbalConnectionFactory( + credentialStore: $container->get(\OCA\OpenRegister\Service\Credential\CredentialStore::class), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // @spec openspec/changes/integration-deck/tasks.md. $context->registerService( - DeckProvider::class, + \OCA\OpenRegister\Service\Dbal\SqlTypeMapper::class, function (ContainerInterface $container) { - return new DeckProvider( - deckLinkService: $container->get(\OCA\OpenRegister\Service\DeckLinkService::class), - appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), + return new \OCA\OpenRegister\Service\Dbal\SqlTypeMapper( + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // @spec openspec/changes/integration-email/tasks.md. $context->registerService( - EmailProvider::class, + \OCA\OpenRegister\Service\Dbal\DatabaseIntrospectionService::class, function (ContainerInterface $container) { - return new EmailProvider( - emailService: $container->get(\OCA\OpenRegister\Service\EmailService::class), - emailLinkService: $container->get(\OCA\OpenRegister\Service\EmailLinkService::class), - appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), + return new \OCA\OpenRegister\Service\Dbal\DatabaseIntrospectionService( + connectionFactory: $container->get(\OCA\OpenRegister\Service\Dbal\DbalConnectionFactory::class), + typeMapper: $container->get(\OCA\OpenRegister\Service\Dbal\SqlTypeMapper::class), + registerMapper: $container->get(\OCA\OpenRegister\Db\RegisterMapper::class), + schemaMapper: $container->get(\OCA\OpenRegister\Db\SchemaMapper::class), + diffService: $container->get(\OCA\OpenRegister\Service\Schema\SchemaDiffService::class), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // Leaf providers — NC-app-backed greenfield. Each registers the - // registry surface (sidebar slot + admin UI presence + OCS caps - // entry) gated on the named NC app. The wrapped read service + - // link table land in per-leaf follow-up PRs; until then `list()` - // returns an empty array so the slot renders an empty state - // rather than a 500. - $greenfieldProviders = [ - // @spec openspec/changes/integration-activity/tasks.md. - \OCA\OpenRegister\Service\Integration\Providers\ActivityProvider::class, - // NB: AnalyticsProvider + CollectivesProvider + CospendProvider - // were Tier-1 greenfield until Tier-2; each now takes its own - // link mapper (AnalyticsLinkMapper / CollectiveLinkMapper / - // CospendLinkMapper) and is registered separately below. - // NB: FormsProvider is NO LONGER greenfield — it has a Tier-2 - // link-table dep (FormLinkMapper) that doesn't fit the (db, - // appManager, l10n) signature shared by the rest of this list. - // Registered separately below. - // NB: FlowProvider was Tier-1 greenfield until Tier-2; it now - // takes a FlowLinkMapper. Registered separately below. - // NB: MapsProvider was Tier-1 greenfield until Tier-2; it now - // takes a MapLinkMapper. Registered separately below. - // NB: PhotosProvider was Tier-1 greenfield until Tier-2; it now - // takes a PhotoLinkMapper. Registered separately below. - // NB: TimeProvider was Tier-1 greenfield until Tier-2; it now - // takes a TimeTrackerLinkMapper. Registered separately below. - ]; - // Each greenfield provider now uses MarkerLookupTrait to query - // its upstream app's main table directly via IDBConnection. All - // share the same constructor signature (db, appManager, l10n). - foreach ($greenfieldProviders as $providerClass) { - $context->registerService( - $providerClass, - function (ContainerInterface $container) use ($providerClass) { - return new $providerClass( - db: $container->get('OCP\IDBConnection'), - appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), - ); - } - ); - } - - // FormsProvider — Tier-2 link-table backed. Promoted from the - // marker-only greenfield wave to its own factory so it gets the - // FormLinkMapper injected. The provider still gracefully degrades - // to the legacy marker scan when the link table is empty (e.g. - // forms that pre-date the Tier-2 link table). - // @spec openspec/changes/integration-forms/tasks.md. $context->registerService( - \OCA\OpenRegister\Service\Integration\Providers\FormsProvider::class, + DbalObjectSourceProvider::class, function (ContainerInterface $container) { - return new FormsProvider( - db: $container->get('OCP\IDBConnection'), - appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), - formLinkMapper: $container->get(\OCA\OpenRegister\Db\FormLinkMapper::class), + return new DbalObjectSourceProvider( + sourceMapper: $container->get(\OCA\OpenRegister\Db\SourceMapper::class), + connectionFactory: $container->get(\OCA\OpenRegister\Service\Dbal\DbalConnectionFactory::class), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // SharesProvider — NC core (no app gate); wraps OCP\Share\IManager - // (query-time, no link table — IShare is first-class NC state). - // @spec openspec/changes/integration-shares/tasks.md. + // Tables provider stack — a guarded reader (the sole Tables-boundary), + // pure mappers, a schema-sync reconciler, and the provider itself. $context->registerService( - SharesProvider::class, + TablesUuidDeriver::class, + function () { + return new TablesUuidDeriver(); + } + ); + + $context->registerService( + TablesTableReader::class, function (ContainerInterface $container) { - return new SharesProvider( - db: $container->get('OCP\IDBConnection'), + return new TablesTableReader( appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), + container: $container, + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // ShareLinkService — Tier-2 share create/revoke/list service - // backing the ShareLinksController. NO link table / NO cache: - // OCP\Share\IManager is the single source of truth, resolved - // lazily through the container (same pattern as SharesProvider). - // @spec openspec/changes/integration-shares/tasks.md. $context->registerService( - \OCA\OpenRegister\Service\ShareLinkService::class, + TablesColumnMapper::class, function (ContainerInterface $container) { - return new ShareLinkService( - l10n: $container->get('OCP\IL10N'), - logger: $container->get('Psr\Log\LoggerInterface'), + return new TablesColumnMapper( + uuidDeriver: $container->get(TablesUuidDeriver::class), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // BookmarksProvider — Tier-2: backed by the BookmarkLinkMapper. - // Replaces the original `or:{uuid}` tag-marker convention with a - // proper persistence layer; the wrapping BookmarkLinkService owns - // the late-bound NC Bookmarks reads/writes. Needs the container - // (NC Bookmarks classes are only on the classpath when that app is - // installed) plus IUserSession to scope the query to the user. - // @spec openspec/changes/integration-bookmarks/tasks.md. $context->registerService( - BookmarksProvider::class, + TablesSchemaSyncService::class, function (ContainerInterface $container) { - return new BookmarksProvider( - bookmarkLinkMapper: $container->get(\OCA\OpenRegister\Db\BookmarkLinkMapper::class), - appManager: $container->get('OCP\App\IAppManager'), - l10n: $container->get('OCP\IL10N'), + return new TablesSchemaSyncService( + registerMapper: $container->get(\OCA\OpenRegister\Db\RegisterMapper::class), + schemaMapper: $container->get(\OCA\OpenRegister\Db\SchemaMapper::class), + columnMapper: $container->get(TablesColumnMapper::class), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // TalkProvider — needs container for late-bound `OCA\Talk\Manager` - // (only on the classpath with `spreed` installed) plus - // IUserSession to scope `getRoomsForUser`. Tier-2: the - // TalkLinkMapper is injected so the provider can short-circuit - // the legacy marker scan when the link table is populated. - // @spec openspec/changes/integration-talk/tasks.md. $context->registerService( - TalkProvider::class, + TablesObjectSourceProvider::class, function (ContainerInterface $container) { - return new TalkProvider( - container: $container, - appManager: $container->get('OCP\App\IAppManager'), + return new TablesObjectSourceProvider( + reader: $container->get(TablesTableReader::class), + columnMapper: $container->get(TablesColumnMapper::class), + uuidDeriver: $container->get(TablesUuidDeriver::class), + syncService: $container->get(TablesSchemaSyncService::class), userSession: $container->get('OCP\IUserSession'), - l10n: $container->get('OCP\IL10N'), - talkLinkMapper: $container->get(\OCA\OpenRegister\Db\TalkLinkMapper::class), + logger: $container->get('Psr\Log\LoggerInterface') ); } ); - // TalkLinkService — Tier-2 link/create/unlink service backing - // the TalkLinksController. Same late-bound Talk pattern as - // the provider (container for OCA\Talk\* lookups). - // @spec openspec/changes/integration-talk/tasks.md. + }//end registerObjectSourceProviders() + + /** + * Register the 5 BuiltinProviders/* services so they can be + * resolved lazily from the container. + * + * Each provider wraps an existing OR service and exposes it + * through the IntegrationProvider contract. They self-register + * with the IntegrationRegistry during `boot()` — + * `bootBuiltinIntegrationProviders()` walks this same list. + * + * @param IRegistrationContext $context The registration context. + * + * @return void + * + * @spec openspec/specs/integration-registry/spec.md + */ + private function registerBuiltinIntegrationProviders(IRegistrationContext $context): void + { $context->registerService( - \OCA\OpenRegister\Service\TalkLinkService::class, + FilesProvider::class, function (ContainerInterface $container) { - return new TalkLinkService( - talkLinkMapper: $container->get(\OCA\OpenRegister\Db\TalkLinkMapper::class), + return new FilesProvider( + fileService: $container->get(\OCA\OpenRegister\Service\FileService::class), container: $container, - appManager: $container->get('OCP\App\IAppManager'), - userSession: $container->get('OCP\IUserSession'), l10n: $container->get('OCP\IL10N'), - logger: $container->get('Psr\Log\LoggerInterface'), ); } ); - // PollsProvider — Tier-2: backed by the PollLinkMapper + - // direct queries against `oc_polls_*` tables. Replaces the - // original title-marker convention with a proper persistence - // layer. - // @spec openspec/changes/integration-polls/tasks.md. $context->registerService( - PollsProvider::class, + NotesProvider::class, function (ContainerInterface $container) { - return new PollsProvider( - pollLinkMapper: $container->get(\OCA\OpenRegister\Db\PollLinkMapper::class), - db: $container->get('OCP\IDBConnection'), - appManager: $container->get('OCP\App\IAppManager'), - userSession: $container->get('OCP\IUserSession'), + return new NotesProvider( + noteService: $container->get(\OCA\OpenRegister\Service\NoteService::class), l10n: $container->get('OCP\IL10N'), ); } ); - // FlowProvider — Tier-2: backed by FlowLinkMapper + direct - // queries against `oc_flow_operations`. Replaces the Tier-1 - // `[or:{uuid}]` name-marker convention with a proper - // persistence layer. - // @spec openspec/changes/integration-flow/tasks.md. $context->registerService( - \OCA\OpenRegister\Service\Integration\Providers\FlowProvider::class, + TasksProvider::class, function (ContainerInterface $container) { - return new FlowProvider( - flowLinkMapper: $container->get(\OCA\OpenRegister\Db\FlowLinkMapper::class), - db: $container->get('OCP\IDBConnection'), - appManager: $container->get('OCP\App\IAppManager'), + return new TasksProvider( + taskService: $container->get(\OCA\OpenRegister\Service\TaskService::class), + registerMapper: $container->get(\OCA\OpenRegister\Db\RegisterMapper::class), + schemaMapper: $container->get(\OCA\OpenRegister\Db\SchemaMapper::class), + objectService: $container->get(\OCA\OpenRegister\Service\ObjectService::class), l10n: $container->get('OCP\IL10N'), ); } ); - // ActivityFilterService — Tier-2 read-only filter/paginate - // service backing the ActivityLinksController. NC Activity - // entries are core-generated; this only wraps the wave-5.3 - // MarkerLookupTrait carve-out query with type/actor/date - // filters + cursor pagination. - // @spec openspec/changes/integration-activity/tasks.md. $context->registerService( - \OCA\OpenRegister\Service\ActivityFilterService::class, + TagsProvider::class, function (ContainerInterface $container) { - return new ActivityFilterService( - db: $container->get('OCP\IDBConnection'), - appManager: $container->get('OCP\App\IAppManager'), + return new TagsProvider( + tagManager: $container->get('OCP\SystemTag\ISystemTagManager'), + objectMapper: $container->get('OCP\SystemTag\ISystemTagObjectMapper'), + l10n: $container->get('OCP\IL10N'), ); } ); - // FlowLinkService — Tier-2 admin-gated link/unlink/picker - // service backing the FlowLinksController. NC Flow operations - // are configured by admins only; the service enforces that - // gate via IGroupManager. - // @spec openspec/changes/integration-flow/tasks.md. $context->registerService( - \OCA\OpenRegister\Service\FlowLinkService::class, + AuditTrailProvider::class, function (ContainerInterface $container) { - return new FlowLinkService( - flowLinkMapper: $container->get(\OCA\OpenRegister\Db\FlowLinkMapper::class), - db: $container->get('OCP\IDBConnection'), - container: $container, - appManager: $container->get('OCP\App\IAppManager'), - userSession: $container->get('OCP\IUserSession'), - groupManager: $container->get('OCP\IGroupManager'), - logger: $container->get('Psr\Log\LoggerInterface'), + return new AuditTrailProvider( + mapper: $container->get(\OCA\OpenRegister\Db\AuditTrailMapper::class), + l10n: $container->get('OCP\IL10N'), ); } ); - // MapsProvider — Tier-2: backed by the MapLinkMapper. The - // provider still gracefully degrades to the legacy `[or:{uuid}]` - // favorite-name marker scan when the link table is empty (POIs - // that pre-date the Tier-2 link table). - // @spec openspec/changes/integration-maps/tasks.md. + // Leaf provider: XWiki (external, OpenConnector-backed). Ships + // in-repo as the worked external-storage example; routed + // through ExternalIntegrationRouter, credentials on the + // OpenConnector `xwiki` source. + // @spec openspec/specs/integration-xwiki/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\Integration\Providers\MapsProvider::class, + XwikiProvider::class, function (ContainerInterface $container) { - return new MapsProvider( - db: $container->get('OCP\IDBConnection'), + return new XwikiProvider( + router: $container->get(ExternalIntegrationRouter::class), appManager: $container->get('OCP\App\IAppManager'), l10n: $container->get('OCP\IL10N'), - mapLinkMapper: $container->get(\OCA\OpenRegister\Db\MapLinkMapper::class), + xwikiLinkMapper: $container->get(\OCA\OpenRegister\Db\XwikiLinkMapper::class), ); } ); - // MapLinkService — Tier-2 link/create/unlink/picker service - // backing the MapLinksController. NC Maps favorites are - // user-scoped; the FavoritesService is resolved lazily via the - // container so the service loads even when Maps is absent. - // @spec openspec/changes/integration-maps/tasks.md. + // XwikiLinkService — Tier-2 link/create/unlink/picker service + // backing the XwikiLinksController. xWiki is external: the provider + // + router are resolved lazily so the service loads even when + // OpenConnector is absent, mirroring the unconfigured-source + // 503-with-cause pattern. + // @spec openspec/specs/integration-xwiki/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\MapLinkService::class, + \OCA\OpenRegister\Service\XwikiLinkService::class, function (ContainerInterface $container) { - return new MapLinkService( - mapLinkMapper: $container->get(\OCA\OpenRegister\Db\MapLinkMapper::class), + return new XwikiLinkService( + xwikiLinkMapper: $container->get(\OCA\OpenRegister\Db\XwikiLinkMapper::class), container: $container, appManager: $container->get('OCP\App\IAppManager'), userSession: $container->get('OCP\IUserSession'), - urlGenerator: $container->get('OCP\IURLGenerator'), logger: $container->get('Psr\Log\LoggerInterface'), ); } ); - // PhotosProvider — Tier-2: backed by the PhotoLinkMapper. The - // provider still gracefully degrades to the legacy `[or:{uuid}]` - // album-name marker scan when the link table is empty (albums - // that pre-date the Tier-2 link table). - // @spec openspec/changes/integration-photos/tasks.md. + // Leaf provider: KvK company lookup (external, OpenConnector-backed). + // Stateless, read-only — no link table; routed through + // ExternalIntegrationRouter, credentials (the `apikey` header) on the + // OpenConnector `kvk` source. Centralises pipelinq's KvkApiClient + // onto the canonical OR/OpenConnector path (ADR-022). + // @spec openspec/specs/integration-registry/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\Integration\Providers\PhotosProvider::class, + KvkProvider::class, function (ContainerInterface $container) { - return new PhotosProvider( - db: $container->get('OCP\IDBConnection'), + return new KvkProvider( + router: $container->get(ExternalIntegrationRouter::class), appManager: $container->get('OCP\App\IAppManager'), l10n: $container->get('OCP\IL10N'), - photoLinkMapper: $container->get(\OCA\OpenRegister\Db\PhotoLinkMapper::class), + logger: $container->get('Psr\Log\LoggerInterface'), ); } ); - // PhotoLinkService — Tier-2 link/create/unlink/picker service - // backing the PhotoLinksController. NC Photos albums are - // user-scoped; the AlbumMapper is resolved lazily via the - // container so the service loads even when Photos is absent. - // @spec openspec/changes/integration-photos/tasks.md. + // Leaf provider: OpenCorporates company search (external, + // OpenConnector-backed). Stateless, read-only — no link table; + // routed through ExternalIntegrationRouter, credentials (the + // `api_token` query param) on the OpenConnector `opencorporates` + // source. Centralises pipelinq's OpenCorporatesApiClient onto the + // canonical OR/OpenConnector path (ADR-022). + // @spec openspec/specs/integration-registry/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\PhotoLinkService::class, + OpenCorporatesProvider::class, function (ContainerInterface $container) { - return new PhotoLinkService( - photoLinkMapper: $container->get(\OCA\OpenRegister\Db\PhotoLinkMapper::class), - container: $container, + return new OpenCorporatesProvider( + router: $container->get(ExternalIntegrationRouter::class), appManager: $container->get('OCP\App\IAppManager'), - userSession: $container->get('OCP\IUserSession'), - urlGenerator: $container->get('OCP\IURLGenerator'), + l10n: $container->get('OCP\IL10N'), logger: $container->get('Psr\Log\LoggerInterface'), ); } ); - // CollectivesProvider — Tier-2: backed by the CollectiveLinkMapper. - // The provider still gracefully degrades to the legacy - // `[or:{uuid}]` slug-marker scan when the link table is empty - // (pages that pre-date the Tier-2 link table). - // @spec openspec/changes/integration-collectives/tasks.md. + // Leaf provider: outbound-messaging dispatch (external, + // OpenConnector-backed) — side-effecting SMS / WhatsApp send. Routed + // through ExternalIntegrationRouter, credentials on the per-call + // OpenConnector source (cmcom-sms / messagebird-sms / twilio-sms / + // whatsapp-cloud-api / whatsapp-bsp). NOT added to the IntegrationRegistry + // boot loop: it is a send-only leaf with no listable surface, reached + // via MessageDispatchController, not as an object-sidebar tab. + // Centralises only pipelinq's per-provider DISPATCH + credentials onto + // the canonical OR/OpenConnector path (ADR-022); all orchestration + // (provider selection, STOP opt-out, template-approval, 24h session, + // dedupe, delivery-status) stays in pipelinq. + // @spec openspec/specs/integration-registry/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\Integration\Providers\CollectivesProvider::class, + MessageDispatchProvider::class, function (ContainerInterface $container) { - return new CollectivesProvider( - db: $container->get('OCP\IDBConnection'), + return new MessageDispatchProvider( + router: $container->get(ExternalIntegrationRouter::class), appManager: $container->get('OCP\App\IAppManager'), l10n: $container->get('OCP\IL10N'), - collectiveLinkMapper: $container->get(\OCA\OpenRegister\Db\CollectiveLinkMapper::class), + logger: $container->get('Psr\Log\LoggerInterface'), ); } ); - // AnalyticsProvider — Tier-2: backed by the AnalyticsLinkMapper. - // The provider still gracefully degrades to the legacy - // `[or:{uuid}]` report-name marker scan when the link table is - // empty (reports that pre-date the Tier-2 link table; wave-2.2 - // marker-on-name convention). - // @spec openspec/changes/integration-analytics/tasks.md. + // Leaf provider: OpenProject (external, OpenConnector-backed) — + // mirrors the XwikiProvider pattern (AD-4 / AD-22). Credentials + // on the OpenConnector `openproject` source. Tier-2: the + // OpenProjectLinkMapper backs the linked list so it renders from + // the cached link rows even when the source is temporarily down. + // @spec openspec/specs/integration-openproject/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\Integration\Providers\AnalyticsProvider::class, + OpenProjectProvider::class, function (ContainerInterface $container) { - return new AnalyticsProvider( - db: $container->get('OCP\IDBConnection'), + return new OpenProjectProvider( + router: $container->get(ExternalIntegrationRouter::class), appManager: $container->get('OCP\App\IAppManager'), l10n: $container->get('OCP\IL10N'), - analyticsLinkMapper: $container->get(\OCA\OpenRegister\Db\AnalyticsLinkMapper::class), + linkMapper: $container->get(\OCA\OpenRegister\Db\OpenProjectLinkMapper::class), ); } ); - // CospendProvider — Tier-2: backed by the CospendLinkMapper. The - // provider still gracefully degrades to the legacy `[or:{uuid}]` - // name-marker scan (projects + bills) when the link table is empty - // (entities that pre-date the Tier-2 link table; wave-2.3 - // marker-on-name convention). - // @spec openspec/changes/integration-cospend/tasks.md. + // OpenProjectLinkService — Tier-2 link/create/unlink/picker + // service backing the OpenProjectLinksController. OpenProject is + // external; the service composes the link mapper with the + // OpenProjectProvider + ExternalIntegrationRouter so picker / + // create / refresh flows go through the OpenConnector + // `openproject` source, degrading to a 503-with-cause when the + // source is missing or the upstream is unreachable (wave-5.2). + // @spec openspec/specs/integration-openproject/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\Integration\Providers\CospendProvider::class, + \OCA\OpenRegister\Service\OpenProjectLinkService::class, function (ContainerInterface $container) { - return new CospendProvider( - db: $container->get('OCP\IDBConnection'), + return new OpenProjectLinkService( + openProjectLinkMapper: $container->get(\OCA\OpenRegister\Db\OpenProjectLinkMapper::class), + provider: $container->get(OpenProjectProvider::class), + router: $container->get(ExternalIntegrationRouter::class), + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // Leaf providers — NC-native, "backend already shipped": + // wrap the existing OR services (Calendar, Contacts, Deck, Email) + // through the registry contract so they surface in the sidebar / + // widgets / admin UI / OCS caps without per-app glue. Each + // provider gates on its required NC app via IAppManager. + // @spec openspec/specs/integration-calendar/spec.md (canonical spec). + $context->registerService( + CalendarProvider::class, + function (ContainerInterface $container) { + return new CalendarProvider( + calendarEventService: $container->get(\OCA\OpenRegister\Service\CalendarEventService::class), + calendarLinkService: $container->get(\OCA\OpenRegister\Service\CalendarLinkService::class), appManager: $container->get('OCP\App\IAppManager'), l10n: $container->get('OCP\IL10N'), - cospendLinkMapper: $container->get(\OCA\OpenRegister\Db\CospendLinkMapper::class), + logger: $container->get(\Psr\Log\LoggerInterface::class), ); } ); - // CospendLinkService — Tier-2 link/create/unlink/picker service - // backing the CospendLinksController. NC Cospend projects are - // user-scoped; the ProjectService is resolved lazily via the - // container (behind a class_exists guard) and bills/projects are - // read directly from the cospend_* tables, so the service loads - // even when Cospend is absent. - // @spec openspec/changes/integration-cospend/tasks.md. + // @spec openspec/specs/integration-contacts/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\CospendLinkService::class, + ContactsProvider::class, function (ContainerInterface $container) { - return new CospendLinkService( - cospendLinkMapper: $container->get(\OCA\OpenRegister\Db\CospendLinkMapper::class), - container: $container, + return new ContactsProvider( + contactService: $container->get(\OCA\OpenRegister\Service\ContactService::class), appManager: $container->get('OCP\App\IAppManager'), - userSession: $container->get('OCP\IUserSession'), - db: $container->get('OCP\IDBConnection'), - logger: $container->get('Psr\Log\LoggerInterface'), + l10n: $container->get('OCP\IL10N'), ); } ); - // CollectiveLinkService — Tier-2 link/create/unlink/picker service - // backing the CollectiveLinksController. NC Collectives pages are - // user-scoped; CollectiveService + PageService are resolved lazily - // via the container so the service loads even when Collectives is - // absent. - // @spec openspec/changes/integration-collectives/tasks.md. + // @spec openspec/specs/integration-deck/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\CollectiveLinkService::class, + DeckProvider::class, function (ContainerInterface $container) { - return new CollectiveLinkService( - collectiveLinkMapper: $container->get(\OCA\OpenRegister\Db\CollectiveLinkMapper::class), - container: $container, + return new DeckProvider( + deckLinkService: $container->get(\OCA\OpenRegister\Service\DeckLinkService::class), appManager: $container->get('OCP\App\IAppManager'), - userSession: $container->get('OCP\IUserSession'), - urlGenerator: $container->get('OCP\IURLGenerator'), - logger: $container->get('Psr\Log\LoggerInterface'), + l10n: $container->get('OCP\IL10N'), ); } ); - // AnalyticsLinkService — Tier-2 link/create/unlink/picker service - // backing the AnalyticsLinksController. NC Analytics reports are - // user-scoped; the ReportService is resolved lazily via - // `\OCP\Server::get()` behind a class_exists guard so the service - // loads even when Analytics is absent. - // @spec openspec/changes/integration-analytics/tasks.md. + // @spec openspec/specs/integration-email/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\AnalyticsLinkService::class, + EmailProvider::class, function (ContainerInterface $container) { - return new AnalyticsLinkService( - analyticsLinkMapper: $container->get(\OCA\OpenRegister\Db\AnalyticsLinkMapper::class), + return new EmailProvider( + emailService: $container->get(\OCA\OpenRegister\Service\EmailService::class), + emailLinkService: $container->get(\OCA\OpenRegister\Service\EmailLinkService::class), appManager: $container->get('OCP\App\IAppManager'), - userSession: $container->get('OCP\IUserSession'), - logger: $container->get('Psr\Log\LoggerInterface'), + l10n: $container->get('OCP\IL10N'), ); } ); - // TimeProvider — Tier-2: backed by the TimeTrackerLinkMapper. The - // provider still gracefully degrades to the legacy - // `[or:{uuid}]` note/name marker scan in `timemanager_client` / - // `timemanager_task` when the link table is empty (entries that - // pre-date the Tier-2 link table; wave-2.4 marker convention). The - // leaf slug is `time-tracker`; the NC app id is `timemanager`. - // @spec openspec/changes/integration-time-tracker/tasks.md. + // Leaf providers — NC-app-backed greenfield. Each registers the + // registry surface (sidebar slot + admin UI presence + OCS caps + // entry) gated on the named NC app. The wrapped read service + + // link table land in per-leaf follow-up PRs; until then `list()` + // returns an empty array so the slot renders an empty state + // rather than a 500. + $greenfieldProviders = [ + // @spec openspec/specs/integration-activity/spec.md (canonical spec). + \OCA\OpenRegister\Service\Integration\Providers\ActivityProvider::class, + // NB: AnalyticsProvider + CollectivesProvider + CospendProvider + // were Tier-1 greenfield until Tier-2; each now takes its own + // link mapper (AnalyticsLinkMapper / CollectiveLinkMapper / + // CospendLinkMapper) and is registered separately below. + // NB: FormsProvider is NO LONGER greenfield — it has a Tier-2 + // link-table dep (FormLinkMapper) that doesn't fit the (db, + // appManager, l10n) signature shared by the rest of this list. + // Registered separately below. + // NB: FlowProvider was Tier-1 greenfield until Tier-2; it now + // takes a FlowLinkMapper. Registered separately below. + // NB: MapsProvider was Tier-1 greenfield until Tier-2; it now + // takes a MapLinkMapper. Registered separately below. + // NB: PhotosProvider was Tier-1 greenfield until Tier-2; it now + // takes a PhotoLinkMapper. Registered separately below. + // NB: TimeProvider was Tier-1 greenfield until Tier-2; it now + // takes a TimeTrackerLinkMapper. Registered separately below. + ]; + // Each greenfield provider now uses MarkerLookupTrait to query + // its upstream app's main table directly via IDBConnection. All + // share the same constructor signature (db, appManager, l10n). + foreach ($greenfieldProviders as $providerClass) { + $context->registerService( + $providerClass, + function (ContainerInterface $container) use ($providerClass) { + return new $providerClass( + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + ); + } + ); + } + + // FormsProvider — Tier-2 link-table backed. Promoted from the + // marker-only greenfield wave to its own factory so it gets the + // FormLinkMapper injected. The provider still gracefully degrades + // to the legacy marker scan when the link table is empty (e.g. + // forms that pre-date the Tier-2 link table). + // @spec openspec/specs/integration-forms/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\Integration\Providers\TimeProvider::class, + \OCA\OpenRegister\Service\Integration\Providers\FormsProvider::class, function (ContainerInterface $container) { - return new TimeProvider( + return new FormsProvider( db: $container->get('OCP\IDBConnection'), appManager: $container->get('OCP\App\IAppManager'), l10n: $container->get('OCP\IL10N'), - linkMapper: $container->get(\OCA\OpenRegister\Db\TimeTrackerLinkMapper::class), + formLinkMapper: $container->get(\OCA\OpenRegister\Db\FormLinkMapper::class), ); } ); - // TimeTrackerLinkService — Tier-2 link/create/unlink/picker service - // backing the TimeTrackerLinksController. NC TimeManager entries are - // user-scoped; ClientMapper + TaskMapper are resolved lazily via the - // container so the service loads even when TimeManager is absent. - // @spec openspec/changes/integration-time-tracker/tasks.md. + // SharesProvider — NC core (no app gate); wraps OCP\Share\IManager + // (query-time, no link table — IShare is first-class NC state). + // @spec openspec/specs/integration-shares/spec.md (canonical spec). $context->registerService( - \OCA\OpenRegister\Service\TimeTrackerLinkService::class, + SharesProvider::class, function (ContainerInterface $container) { - return new TimeTrackerLinkService( - timeTrackerLinkMapper: $container->get(\OCA\OpenRegister\Db\TimeTrackerLinkMapper::class), + return new SharesProvider( db: $container->get('OCP\IDBConnection'), - container: $container, appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + ); + } + ); + + // ShareLinkService — Tier-2 share create/revoke/list service + // backing the ShareLinksController. NO link table / NO cache: + // OCP\Share\IManager is the single source of truth, resolved + // lazily through the container (same pattern as SharesProvider). + // @spec openspec/specs/integration-shares/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\ShareLinkService::class, + function (ContainerInterface $container) { + return new ShareLinkService( + l10n: $container->get('OCP\IL10N'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // CaseTokenService — mint/resolve/revoke public "track your case" + // token links (the SharesProvider create()/delete() public-token + // surface). ObjectService is resolved lazily through the container + // (same pattern as ShareLinkService) so the resolve path runs the + // canonical RBAC-respecting read; the token never bypasses RBAC. + // @spec openspec/specs/integration-leaf-foundation/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\CaseTokenService::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\CaseTokenService( + mapper: $container->get(\OCA\OpenRegister\Db\CaseTokenMapper::class), + secureRandom: $container->get('OCP\Security\ISecureRandom'), userSession: $container->get('OCP\IUserSession'), + urlGenerator: $container->get('OCP\IURLGenerator'), logger: $container->get('Psr\Log\LoggerInterface'), ); } ); - }//end registerBuiltinIntegrationProviders() - /** - * Register all event listeners for the application. - * - * @param IRegistrationContext $context The registration context - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 - */ - private function registerEventListeners(IRegistrationContext $context): void - { - // Solr event listeners for automatic indexing. - $context->registerEventListener(ObjectCreatedEvent::class, SolrEventListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, SolrEventListener::class); - $context->registerEventListener(ObjectDeletedEvent::class, SolrEventListener::class); + // CaseTokenController — anonymous public resolve endpoint. + $context->registerService( + \OCA\OpenRegister\Controller\CaseTokenController::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Controller\CaseTokenController( + appName: 'openregister', + request: $container->get('OCP\IRequest'), + tokenService: $container->get(\OCA\OpenRegister\Service\CaseTokenService::class), + ); + } + ); - // Solr event listeners for schema lifecycle management. - $context->registerEventListener(SchemaCreatedEvent::class, SolrEventListener::class); - $context->registerEventListener(SchemaUpdatedEvent::class, SolrEventListener::class); - $context->registerEventListener(SchemaDeletedEvent::class, SolrEventListener::class); + // AnalyticsSeriesService — register/fetch page-level pre-computed + // chart series (the leaf-foundation SLA-dashboard render surface). + // RBAC-scoped; (re)declares its page widget on the registry. + // @spec openspec/specs/integration-leaf-foundation/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\AnalyticsSeriesService::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\AnalyticsSeriesService( + mapper: $container->get(\OCA\OpenRegister\Db\AnalyticsSeriesMapper::class), + registry: $container->get(IntegrationRegistry::class), + userSession: $container->get('OCP\IUserSession'), + groupManager: $container->get('OCP\IGroupManager'), + ); + } + ); - // FileChangeListener for automatic file text extraction. - $context->registerEventListener(NodeCreatedEvent::class, FileChangeListener::class); - $context->registerEventListener(NodeWrittenEvent::class, FileChangeListener::class); + // AnalyticsSeriesController — register/fetch HTTP surface a leaf + // calls to feed a dashboard chart widget. + $context->registerService( + \OCA\OpenRegister\Controller\AnalyticsSeriesController::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Controller\AnalyticsSeriesController( + appName: 'openregister', + request: $container->get('OCP\IRequest'), + seriesService: $container->get(\OCA\OpenRegister\Service\AnalyticsSeriesService::class), + ); + } + ); - // ObjectChangeListener for automatic object text extraction. - $context->registerEventListener(ObjectCreatedEvent::class, ObjectChangeListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, ObjectChangeListener::class); + // BookmarksProvider — Tier-2: backed by the BookmarkLinkMapper. + // Replaces the original `or:{uuid}` tag-marker convention with a + // proper persistence layer; the wrapping BookmarkLinkService owns + // the late-bound NC Bookmarks reads/writes. Needs the container + // (NC Bookmarks classes are only on the classpath when that app is + // installed) plus IUserSession to scope the query to the user. + // @spec openspec/specs/integration-bookmarks/spec.md (canonical spec). + $context->registerService( + BookmarksProvider::class, + function (ContainerInterface $container) { + return new BookmarksProvider( + bookmarkLinkMapper: $container->get(\OCA\OpenRegister\Db\BookmarkLinkMapper::class), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + ); + } + ); + + // TalkProvider — needs container for late-bound `OCA\Talk\Manager` + // (only on the classpath with `spreed` installed) plus + // IUserSession to scope `getRoomsForUser`. Tier-2: the + // TalkLinkMapper is injected so the provider can short-circuit + // the legacy marker scan when the link table is populated. + // @spec openspec/specs/integration-talk/spec.md (canonical spec). + $context->registerService( + TalkProvider::class, + function (ContainerInterface $container) { + return new TalkProvider( + container: $container, + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + l10n: $container->get('OCP\IL10N'), + talkLinkMapper: $container->get(\OCA\OpenRegister\Db\TalkLinkMapper::class), + ); + } + ); + + // TalkLinkService — Tier-2 link/create/unlink service backing + // the TalkLinksController. Same late-bound Talk pattern as + // the provider (container for OCA\Talk\* lookups). + // @spec openspec/specs/integration-talk/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\TalkLinkService::class, + function (ContainerInterface $container) { + return new TalkLinkService( + talkLinkMapper: $container->get(\OCA\OpenRegister\Db\TalkLinkMapper::class), + container: $container, + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + l10n: $container->get('OCP\IL10N'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // PollsProvider — Tier-2: backed by the PollLinkMapper + + // direct queries against `oc_polls_*` tables. Replaces the + // original title-marker convention with a proper persistence + // layer. + // @spec openspec/specs/integration-polls/spec.md (canonical spec). + $context->registerService( + PollsProvider::class, + function (ContainerInterface $container) { + return new PollsProvider( + pollLinkMapper: $container->get(\OCA\OpenRegister\Db\PollLinkMapper::class), + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + l10n: $container->get('OCP\IL10N'), + ); + } + ); + + // FlowProvider — Tier-2: backed by FlowLinkMapper + direct + // queries against `oc_flow_operations`. Replaces the Tier-1 + // `[or:{uuid}]` name-marker convention with a proper + // persistence layer. + // @spec openspec/specs/integration-flow/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\Integration\Providers\FlowProvider::class, + function (ContainerInterface $container) { + return new FlowProvider( + flowLinkMapper: $container->get(\OCA\OpenRegister\Db\FlowLinkMapper::class), + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + ); + } + ); + + // ActivityFilterService — Tier-2 read-only filter/paginate + // service backing the ActivityLinksController. NC Activity + // entries are core-generated; this only wraps the wave-5.3 + // MarkerLookupTrait carve-out query with type/actor/date + // filters + cursor pagination. + // @spec openspec/specs/integration-activity/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\ActivityFilterService::class, + function (ContainerInterface $container) { + return new ActivityFilterService( + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + ); + } + ); + + // FlowLinkService — Tier-2 admin-gated link/unlink/picker + // service backing the FlowLinksController. NC Flow operations + // are configured by admins only; the service enforces that + // gate via IGroupManager. + // @spec openspec/specs/integration-flow/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\FlowLinkService::class, + function (ContainerInterface $container) { + return new FlowLinkService( + flowLinkMapper: $container->get(\OCA\OpenRegister\Db\FlowLinkMapper::class), + db: $container->get('OCP\IDBConnection'), + container: $container, + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + groupManager: $container->get('OCP\IGroupManager'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // MapsProvider — Tier-2: backed by the MapLinkMapper. The + // provider still gracefully degrades to the legacy `[or:{uuid}]` + // favorite-name marker scan when the link table is empty (POIs + // that pre-date the Tier-2 link table). + // @spec openspec/specs/integration-maps/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\Integration\Providers\MapsProvider::class, + function (ContainerInterface $container) { + return new MapsProvider( + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + mapLinkMapper: $container->get(\OCA\OpenRegister\Db\MapLinkMapper::class), + ); + } + ); + + // MapLinkService — Tier-2 link/create/unlink/picker service + // backing the MapLinksController. NC Maps favorites are + // user-scoped; the FavoritesService is resolved lazily via the + // container so the service loads even when Maps is absent. + // @spec openspec/specs/integration-maps/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\MapLinkService::class, + function (ContainerInterface $container) { + return new MapLinkService( + mapLinkMapper: $container->get(\OCA\OpenRegister\Db\MapLinkMapper::class), + container: $container, + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + urlGenerator: $container->get('OCP\IURLGenerator'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // PhotosProvider — Tier-2: backed by the PhotoLinkMapper. The + // provider still gracefully degrades to the legacy `[or:{uuid}]` + // album-name marker scan when the link table is empty (albums + // that pre-date the Tier-2 link table). + // @spec openspec/specs/integration-photos/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\Integration\Providers\PhotosProvider::class, + function (ContainerInterface $container) { + return new PhotosProvider( + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + photoLinkMapper: $container->get(\OCA\OpenRegister\Db\PhotoLinkMapper::class), + ); + } + ); + + // PhotoLinkService — Tier-2 link/create/unlink/picker service + // backing the PhotoLinksController. NC Photos albums are + // user-scoped; the AlbumMapper is resolved lazily via the + // container so the service loads even when Photos is absent. + // @spec openspec/specs/integration-photos/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\PhotoLinkService::class, + function (ContainerInterface $container) { + return new PhotoLinkService( + photoLinkMapper: $container->get(\OCA\OpenRegister\Db\PhotoLinkMapper::class), + container: $container, + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + urlGenerator: $container->get('OCP\IURLGenerator'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // CollectivesProvider — Tier-2: backed by the CollectiveLinkMapper. + // The provider still gracefully degrades to the legacy + // `[or:{uuid}]` slug-marker scan when the link table is empty + // (pages that pre-date the Tier-2 link table). + // @spec openspec/specs/integration-collectives/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\Integration\Providers\CollectivesProvider::class, + function (ContainerInterface $container) { + return new CollectivesProvider( + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + collectiveLinkMapper: $container->get(\OCA\OpenRegister\Db\CollectiveLinkMapper::class), + ); + } + ); + + // AnalyticsProvider — Tier-2: backed by the AnalyticsLinkMapper. + // The provider still gracefully degrades to the legacy + // `[or:{uuid}]` report-name marker scan when the link table is + // empty (reports that pre-date the Tier-2 link table; wave-2.2 + // marker-on-name convention). + // @spec openspec/specs/integration-analytics/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\Integration\Providers\AnalyticsProvider::class, + function (ContainerInterface $container) { + return new AnalyticsProvider( + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + analyticsLinkMapper: $container->get(\OCA\OpenRegister\Db\AnalyticsLinkMapper::class), + ); + } + ); + + // CospendProvider — Tier-2: backed by the CospendLinkMapper. The + // provider still gracefully degrades to the legacy `[or:{uuid}]` + // name-marker scan (projects + bills) when the link table is empty + // (entities that pre-date the Tier-2 link table; wave-2.3 + // marker-on-name convention). + // @spec openspec/specs/integration-cospend/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\Integration\Providers\CospendProvider::class, + function (ContainerInterface $container) { + return new CospendProvider( + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + cospendLinkMapper: $container->get(\OCA\OpenRegister\Db\CospendLinkMapper::class), + ); + } + ); + + // CospendLinkService — Tier-2 link/create/unlink/picker service + // backing the CospendLinksController. NC Cospend projects are + // user-scoped; the ProjectService is resolved lazily via the + // container (behind a class_exists guard) and bills/projects are + // read directly from the cospend_* tables, so the service loads + // even when Cospend is absent. + // @spec openspec/specs/integration-cospend/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\CospendLinkService::class, + function (ContainerInterface $container) { + return new CospendLinkService( + cospendLinkMapper: $container->get(\OCA\OpenRegister\Db\CospendLinkMapper::class), + container: $container, + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + db: $container->get('OCP\IDBConnection'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // CollectiveLinkService — Tier-2 link/create/unlink/picker service + // backing the CollectiveLinksController. NC Collectives pages are + // user-scoped; CollectiveService + PageService are resolved lazily + // via the container so the service loads even when Collectives is + // absent. + // @spec openspec/specs/integration-collectives/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\CollectiveLinkService::class, + function (ContainerInterface $container) { + return new CollectiveLinkService( + collectiveLinkMapper: $container->get(\OCA\OpenRegister\Db\CollectiveLinkMapper::class), + container: $container, + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + urlGenerator: $container->get('OCP\IURLGenerator'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // AnalyticsLinkService — Tier-2 link/create/unlink/picker service + // backing the AnalyticsLinksController. NC Analytics reports are + // user-scoped; the ReportService is resolved lazily via + // `\OCP\Server::get()` behind a class_exists guard so the service + // loads even when Analytics is absent. + // @spec openspec/specs/integration-analytics/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\AnalyticsLinkService::class, + function (ContainerInterface $container) { + return new AnalyticsLinkService( + analyticsLinkMapper: $container->get(\OCA\OpenRegister\Db\AnalyticsLinkMapper::class), + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // TimeProvider — Tier-2: backed by the TimeTrackerLinkMapper. The + // provider still gracefully degrades to the legacy + // `[or:{uuid}]` note/name marker scan in `timemanager_client` / + // `timemanager_task` when the link table is empty (entries that + // pre-date the Tier-2 link table; wave-2.4 marker convention). The + // leaf slug is `time-tracker`; the NC app id is `timemanager`. + // @spec openspec/specs/integration-time-tracker/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\Integration\Providers\TimeProvider::class, + function (ContainerInterface $container) { + return new TimeProvider( + db: $container->get('OCP\IDBConnection'), + appManager: $container->get('OCP\App\IAppManager'), + l10n: $container->get('OCP\IL10N'), + linkMapper: $container->get(\OCA\OpenRegister\Db\TimeTrackerLinkMapper::class), + config: $container->get('OCP\IConfig'), + ); + } + ); + + // TimeTrackerLinkService — Tier-2 link/create/unlink/picker service + // backing the TimeTrackerLinksController. NC TimeManager entries are + // user-scoped; ClientMapper + TaskMapper are resolved lazily via the + // container so the service loads even when TimeManager is absent. + // @spec openspec/specs/integration-time-tracker/spec.md (canonical spec). + $context->registerService( + \OCA\OpenRegister\Service\TimeTrackerLinkService::class, + function (ContainerInterface $container) { + return new TimeTrackerLinkService( + timeTrackerLinkMapper: $container->get(\OCA\OpenRegister\Db\TimeTrackerLinkMapper::class), + db: $container->get('OCP\IDBConnection'), + container: $container, + appManager: $container->get('OCP\App\IAppManager'), + userSession: $container->get('OCP\IUserSession'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + + // Schema versioning & object migration services (schema-versioning-and-object-migration). + $context->registerService( + \OCA\OpenRegister\Service\Schema\SchemaDiffService::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Schema\SchemaDiffService(); + } + ); + $context->registerService( + \OCA\OpenRegister\Service\Schema\SchemaMigrationPlanner::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Schema\SchemaMigrationPlanner(); + } + ); + $context->registerService( + \OCA\OpenRegister\Service\Schema\SchemaVersioningService::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Schema\SchemaVersioningService( + diffService: $container->get(\OCA\OpenRegister\Service\Schema\SchemaDiffService::class), + changelogMapper: $container->get(\OCA\OpenRegister\Db\SchemaChangelogMapper::class), + runMapper: $container->get(\OCA\OpenRegister\Db\SchemaRunMapper::class), + runEntryMapper: $container->get(\OCA\OpenRegister\Db\SchemaRunEntryMapper::class), + userSession: $container->get('OCP\IUserSession'), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + $context->registerService( + \OCA\OpenRegister\Service\Schema\SchemaRevalidationService::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Schema\SchemaRevalidationService( + runMapper: $container->get(\OCA\OpenRegister\Db\SchemaRunMapper::class), + runEntryMapper: $container->get(\OCA\OpenRegister\Db\SchemaRunEntryMapper::class), + schemaMapper: $container->get(SchemaMapper::class), + objectService: $container->get(\OCA\OpenRegister\Service\ObjectService::class), + validateObject: $container->get(\OCA\OpenRegister\Service\Object\ValidateObject::class), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + $context->registerService( + \OCA\OpenRegister\Service\Schema\SchemaMigrationService::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\Service\Schema\SchemaMigrationService( + planner: $container->get(\OCA\OpenRegister\Service\Schema\SchemaMigrationPlanner::class), + runMapper: $container->get(\OCA\OpenRegister\Db\SchemaRunMapper::class), + runEntryMapper: $container->get(\OCA\OpenRegister\Db\SchemaRunEntryMapper::class), + schemaMapper: $container->get(SchemaMapper::class), + objectService: $container->get(\OCA\OpenRegister\Service\ObjectService::class), + logger: $container->get('Psr\Log\LoggerInterface'), + ); + } + ); + }//end registerBuiltinIntegrationProviders() + + /** + * Register all event listeners for the application. + * + * @param IRegistrationContext $context The registration context + * + * @return void + * + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md + */ + private function registerEventListeners(IRegistrationContext $context): void + { + // FileChangeListener for automatic file text extraction. + $context->registerEventListener(NodeCreatedEvent::class, FileChangeListener::class); + $context->registerEventListener(NodeWrittenEvent::class, FileChangeListener::class); + + // Advertise the `openregister` OCM resource type in /ocm-provider discovery. + $context->registerEventListener( + \OCP\OCM\Events\ResourceTypeRegisterEvent::class, + \OCA\OpenRegister\Listener\OcmResourceTypeListener::class + ); + + // ObjectChangeListener for automatic object text extraction. + $context->registerEventListener(ObjectCreatedEvent::class, ObjectChangeListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, ObjectChangeListener::class); // ToolRegistrationListener for agent function tools. $context->registerEventListener(ToolRegistrationEvent::class, ToolRegistrationListener::class); - // Lifecycle annotation listeners — see x-openregister-lifecycle. - // Order matters: initial state runs on creating; validation runs on updating. - $context->registerEventListener(ObjectCreatingEvent::class, LifecycleInitialStateListener::class); - $context->registerEventListener(ObjectUpdatingEvent::class, LifecycleValidationListener::class); + // Tables schema-lifecycle listener — retire the managed virtual schema of + // a deleted Tables table. Guarded by class_exists so boot never fatals on + // an instance without the (soft-dependency) Tables app installed. + if (class_exists('OCA\\Tables\\Event\\TableDeletedEvent') === true) { + $context->registerEventListener('OCA\\Tables\\Event\\TableDeletedEvent', TablesTableDeletedListener::class); + } + + // Lifecycle annotation listeners — see x-openregister-lifecycle. + // Order matters: initial state runs on creating; validation runs on updating. + $context->registerEventListener(ObjectCreatingEvent::class, LifecycleInitialStateListener::class); + $context->registerEventListener(ObjectUpdatingEvent::class, LifecycleValidationListener::class); + + // Approval-chains declarative wiring — see x-openregister-approval-chains. + // Provisions ApprovalChain rows from the annotation (schema save), then + // gates any lifecycle transition it names until the provisioned chain's + // steps are all approved. Registered immediately after + // LifecycleValidationListener: transition legality must be established + // before approval-chain gating runs against it. + $context->registerEventListener(SchemaCreatedEvent::class, ApprovalChainAnnotationInstaller::class); + $context->registerEventListener(SchemaUpdatedEvent::class, ApprovalChainAnnotationInstaller::class); + $context->registerEventListener(ObjectUpdatingEvent::class, ApprovalChainGateListener::class); + $context->registerEventListener(ApprovalStepCompletedEvent::class, ApprovalChainAdvanceListener::class); + + // Lifecycle action executor — see x-openregister-lifecycle.transitions[*].actions[]. + // Runs the declared actions of a matched transition on the save path, so + // they fire for EVERY transition form — a named TransitionEngine action + // AND a plain list-form edit of the lifecycle field (issue #427). Registered + // after the validation + approval-gate listeners: a transition's actions + // must only run once its legality is established and no gate has blocked it + // (the listener checks isPropagationStopped()). A declared action naming an + // unregistered handler fails loudly via LifecycleActionRegistry. + $context->registerEventListener(ObjectUpdatingEvent::class, LifecycleActionListener::class); + + // Calculations annotation listener — materialises declared calculations + // into the object payload before persistence (see x-openregister-calculations). + $context->registerEventListener(ObjectCreatingEvent::class, CalculationOnSaveListener::class); + $context->registerEventListener(ObjectUpdatingEvent::class, CalculationOnSaveListener::class); + + // Quality annotation listener — materialises a per-object data-quality + // score (0-1) into the object payload before persistence + // (see x-openregister-quality). MDM foundation capability. + $context->registerEventListener(ObjectCreatingEvent::class, QualityScoreOnSaveListener::class); + $context->registerEventListener(ObjectUpdatingEvent::class, QualityScoreOnSaveListener::class); + + // Survivorship annotation listener — materialises a declared golden + // record + attribute provenance into the object payload before + // persistence (see x-openregister-survivorship). MDM capability. + $context->registerEventListener(ObjectCreatingEvent::class, SurvivorshipRecomputeListener::class); + $context->registerEventListener(ObjectUpdatingEvent::class, SurvivorshipRecomputeListener::class); + + // Reverse-FK source-change listener — when a source object (declared via + // a master schema's x-openregister-survivorship sourceLink.reverseFk) + // is created/updated/deleted, recompute the referenced master's golden + // record so it stays current as its sources change. MDM capability. + $context->registerEventListener(ObjectCreatedEvent::class, SourceRecordChangeListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, SourceRecordChangeListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, SourceRecordChangeListener::class); + // Schema lifecycle events invalidate the listener's cross-request + // reverse-FK index cache (reverse-FK declarations live on schemas). + $context->registerEventListener(SchemaCreatedEvent::class, SourceRecordChangeListener::class); + $context->registerEventListener(SchemaUpdatedEvent::class, SourceRecordChangeListener::class); + $context->registerEventListener(SchemaDeletedEvent::class, SourceRecordChangeListener::class); + + // CRUD metric listener — persists an operational metric row per object + // create/update/delete into `openregister_metrics`, which the canonical + // production-observability spec requires for counters that survive PHP + // request boundaries. Fail-soft: never aborts the write it observes. + $context->registerEventListener(ObjectCreatedEvent::class, ObjectMetricsListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, ObjectMetricsListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, ObjectMetricsListener::class); + + // Notifications annotation listener — fires INotificationManager + // notifications declared on the schema's x-openregister-notifications. + $context->registerEventListener(ObjectCreatedEvent::class, AnnotationNotificationListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, AnnotationNotificationListener::class); + $context->registerEventListener(ObjectTransitionedEvent::class, AnnotationNotificationListener::class); + + // Declarative flow engine — runs x-openregister-flows actions (calendar + // agenda tasks, email, ...) declared on the schema when an object's + // create/update/delete lifecycle event fires. + $context->registerEventListener(ObjectCreatedEvent::class, FlowActionListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, FlowActionListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, FlowActionListener::class); + + // System-entity notification bridge — routes create/update signals from + // OpenRegister's own system entities through the same annotation-notification + // pipeline used for stored register objects (spec: openregister-system-notifications). + $context->registerEventListener(RegisterCreatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(RegisterUpdatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(SchemaCreatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(SchemaUpdatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(ConfigurationCreatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(ConfigurationUpdatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(SourceCreatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(SourceUpdatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(AgentCreatedEvent::class, SystemEntityNotificationListener::class); + $context->registerEventListener(AgentUpdatedEvent::class, SystemEntityNotificationListener::class); + + // Aggregation cache eviction on every object write. + $context->registerEventListener(ObjectCreatedEvent::class, AggregationCacheInvalidationListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, AggregationCacheInvalidationListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, AggregationCacheInvalidationListener::class); + + // Translation sidecar projection — keeps oc_openregister_translations in sync with JSONB property data. + $context->registerEventListener(ObjectCreatedEvent::class, TranslationProjectionListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, TranslationProjectionListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, TranslationProjectionListener::class); + $context->registerEventListener(ObjectTransitionedEvent::class, TranslationProjectionListener::class); + $context->registerEventListener(ObjectTransitionedEvent::class, AggregationCacheInvalidationListener::class); + + // Webhook auto-create installer for x-openregister-notifications with webhook.persistent: true. + $context->registerEventListener(SchemaCreatedEvent::class, NotificationsAnnotationInstaller::class); + $context->registerEventListener(SchemaUpdatedEvent::class, NotificationsAnnotationInstaller::class); + + // Semantic-object handoff engine (ADR-051): + // - lifecycle-triggered handoffs run off ObjectTransitionedEvent (real actors only); + // - queue-mode drain triggers: schema save + app enable (a provider may + // have appeared); the fallback HandoffQueueDrainJob catches the rest. + $context->registerEventListener(ObjectTransitionedEvent::class, HandoffLifecycleListener::class); + $context->registerEventListener(SchemaCreatedEvent::class, HandoffQueueDrainListener::class); + $context->registerEventListener(SchemaUpdatedEvent::class, HandoffQueueDrainListener::class); + $context->registerEventListener(\OCP\App\Events\AppEnableEvent::class, HandoffQueueDrainListener::class); + + // Scheduled-notification per-object dedup pruning (Phase 3.4): + // - drop dedup rows on object purge so a re-created UUID re-arms cleanly; + // - drop dedup rows for rule keys removed/renamed in the schema annotation + // so orphan state does not pile up after edits. + $context->registerEventListener(ObjectDeletedEvent::class, NotificationDedupePruneListener::class); + $context->registerEventListener(SchemaCreatedEvent::class, NotificationDedupeAnnotationSyncListener::class); + $context->registerEventListener(SchemaUpdatedEvent::class, NotificationDedupeAnnotationSyncListener::class); + + // Threshold trigger evaluator: re-runs aggregations on writes and dispatches when thresholds are crossed. + $context->registerEventListener(ObjectCreatedEvent::class, AggregationThresholdListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, AggregationThresholdListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, AggregationThresholdListener::class); + $context->registerEventListener(ObjectTransitionedEvent::class, AggregationThresholdListener::class); + + // HookListener for schema hook execution on lifecycle events. + $context->registerEventListener(ObjectCreatingEvent::class, HookListener::class); + $context->registerEventListener(ObjectUpdatingEvent::class, HookListener::class); + $context->registerEventListener(ObjectDeletingEvent::class, HookListener::class); + $context->registerEventListener(ObjectCreatedEvent::class, HookListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, HookListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, HookListener::class); + + // WebhookEventListener for webhook delivery. + // OPS-2: register for EVERY event the listener's extractPayload() handles, + // not just create — otherwise update/delete/lock/revert/register/schema + // webhooks silently never deliver. + $context->registerEventListener(ObjectCreatedEvent::class, WebhookEventListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, WebhookEventListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, WebhookEventListener::class); + $context->registerEventListener(ObjectLockedEvent::class, WebhookEventListener::class); + $context->registerEventListener(ObjectUnlockedEvent::class, WebhookEventListener::class); + $context->registerEventListener(ObjectRevertedEvent::class, WebhookEventListener::class); + $context->registerEventListener(RegisterCreatedEvent::class, WebhookEventListener::class); + $context->registerEventListener(RegisterUpdatedEvent::class, WebhookEventListener::class); + $context->registerEventListener(RegisterDeletedEvent::class, WebhookEventListener::class); + $context->registerEventListener(SchemaCreatedEvent::class, WebhookEventListener::class); + $context->registerEventListener(SchemaUpdatedEvent::class, WebhookEventListener::class); + $context->registerEventListener(SchemaDeletedEvent::class, WebhookEventListener::class); + + // OPS-1: ActionListener drives the event-driven Actions feature. It is + // event-agnostic (resolves the payload from the dispatched event), so it + // must be wired to the object lifecycle events or configured Actions + // never fire. + $context->registerEventListener(ObjectCreatedEvent::class, ActionListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, ActionListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, ActionListener::class); + $context->registerEventListener(ObjectTransitionedEvent::class, ActionListener::class); + + // GraphQL subscription event listeners. + $context->registerEventListener(ObjectCreatedEvent::class, GraphQLSubscriptionListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, GraphQLSubscriptionListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, GraphQLSubscriptionListener::class); + + // Notify_push real-time push listeners (soft-fail when notify_push not installed). + $context->registerEventListener(ObjectCreatedEvent::class, NotifyPushListener::class); + $context->registerEventListener(ObjectUpdatedEvent::class, NotifyPushListener::class); + $context->registerEventListener(ObjectDeletedEvent::class, NotifyPushListener::class); + + // FilesSidebarListener injects the sidebar tab script into the Files app. + $context->registerEventListener('OCA\Files\Event\LoadAdditionalScriptsEvent', FilesSidebarListener::class); + + // MailAppScriptListener injects the mail sidebar script into the Mail app + // (gated to the Mail app via TemplateResponse->getApp() === 'mail'). + $context->registerEventListener( + \OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent::class, + MailAppScriptListener::class + ); + + // IntegrationGlobalScriptListener loads the shared integration-registry + // bootstrap on EVERY full-page render so the registry is installed + + // populated universally — letting leaves render inside any consuming + // app's object detail page (e.g. an OpenCatalogi publication) without + // that app bootstrapping the registry itself (universal-shared-integration-registry). + $context->registerEventListener( + \OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent::class, + \OCA\OpenRegister\Listener\IntegrationGlobalScriptListener::class + ); + + // PushClientScriptListener loads the always-on, opt-in Web Push + // subscribe client on EVERY full-page render (openregister-web-push-engine). + // The client never prompts on load — it only subscribes on a user + // gesture / settings toggle. + $context->registerEventListener( + \OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent::class, + \OCA\OpenRegister\Listener\PushClientScriptListener::class + ); + + // CommentsEntityListener registers "openregister" objectType for Nextcloud Comments. + $context->registerEventListener(CommentsEntityEvent::class, CommentsEntityListener::class); + + // ObjectCleanupListener cleans up notes and tasks when an object is deleted. + $context->registerEventListener(ObjectDeletedEvent::class, ObjectCleanupListener::class); + + // ActivityEventListener publishes Nextcloud Activity events for entity lifecycle. + $activityListener = \OCA\OpenRegister\Listener\ActivityEventListener::class; + $context->registerEventListener(ObjectCreatedEvent::class, $activityListener); + $context->registerEventListener(ObjectUpdatedEvent::class, $activityListener); + $context->registerEventListener(ObjectDeletedEvent::class, $activityListener); + $context->registerEventListener(RegisterCreatedEvent::class, $activityListener); + $context->registerEventListener(RegisterUpdatedEvent::class, $activityListener); + $context->registerEventListener(RegisterDeletedEvent::class, $activityListener); + $context->registerEventListener(SchemaCreatedEvent::class, $activityListener); + $context->registerEventListener(SchemaUpdatedEvent::class, $activityListener); + $context->registerEventListener(SchemaDeletedEvent::class, $activityListener); + }//end registerEventListeners() + + /** + * Register the AppHost declarative observability engine (ADR-040). + * + * Wires the manifest loader, the health-check executor, the four metric + * sources, the Prometheus renderer and the metrics engine. The generic + * controllers are auto-resolved by Nextcloud's container (their `$appName` + * is supplied by the alias registration in each adopting leaf app's own + * Application.php). Services that need the DI container itself (lazy + * orAvailable + IMetricsProvider/IHealthCheckProvider alias discovery) get + * explicit factory closures; the rest auto-wire. + * + * @param IRegistrationContext $context The registration context. + * + * @return void + * + * @spec openspec/specs/apphost-observability/spec.md + */ + private function registerAppHostObservability(IRegistrationContext $context): void + { + $context->registerService( + \OCA\OpenRegister\AppHost\Observability\ManifestLoader::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\AppHost\Observability\ManifestLoader( + appManager: $container->get('OCP\App\IAppManager'), + logger: $container->get('Psr\Log\LoggerInterface') + ); + } + ); + + $context->registerService( + \OCA\OpenRegister\AppHost\Observability\HealthCheckExecutor::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\AppHost\Observability\HealthCheckExecutor( + db: $container->get('OCP\IDBConnection'), + tempManager: $container->get('OCP\ITempManager'), + appManager: $container->get('OCP\App\IAppManager'), + appConfig: $container->get('OCP\IAppConfig'), + container: $container, + logger: $container->get('Psr\Log\LoggerInterface') + ); + } + ); + + $context->registerService( + \OCA\OpenRegister\AppHost\Observability\Source\ObjectMetricSource::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\AppHost\Observability\Source\ObjectMetricSource( + objectService: $container->get(\OCA\OpenRegister\Service\ObjectService::class), + registerMapper: $container->get(RegisterMapper::class), + schemaMapper: $container->get(SchemaMapper::class), + logger: $container->get('Psr\Log\LoggerInterface') + ); + } + ); + + $context->registerService( + \OCA\OpenRegister\AppHost\Observability\Source\TableMetricSource::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\AppHost\Observability\Source\TableMetricSource( + db: $container->get('OCP\IDBConnection'), + logger: $container->get('Psr\Log\LoggerInterface') + ); + } + ); - // Calculations annotation listener — materialises declared calculations - // into the object payload before persistence (see x-openregister-calculations). - $context->registerEventListener(ObjectCreatingEvent::class, CalculationOnSaveListener::class); - $context->registerEventListener(ObjectUpdatingEvent::class, CalculationOnSaveListener::class); + $context->registerService( + \OCA\OpenRegister\AppHost\Observability\Source\AppConfigMetricSource::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\AppHost\Observability\Source\AppConfigMetricSource( + appConfig: $container->get('OCP\IAppConfig') + ); + } + ); - // Notifications annotation listener — fires INotificationManager - // notifications declared on the schema's x-openregister-notifications. - $context->registerEventListener(ObjectCreatedEvent::class, AnnotationNotificationListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, AnnotationNotificationListener::class); - $context->registerEventListener(ObjectTransitionedEvent::class, AnnotationNotificationListener::class); + $context->registerService( + \OCA\OpenRegister\AppHost\Observability\Source\ProviderMetricSource::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\AppHost\Observability\Source\ProviderMetricSource( + container: $container, + logger: $container->get('Psr\Log\LoggerInterface') + ); + } + ); - // Aggregation cache eviction on every object write. - $context->registerEventListener(ObjectCreatedEvent::class, AggregationCacheInvalidationListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, AggregationCacheInvalidationListener::class); - $context->registerEventListener(ObjectDeletedEvent::class, AggregationCacheInvalidationListener::class); + $context->registerService( + \OCA\OpenRegister\AppHost\Observability\PrometheusRenderer::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\AppHost\Observability\PrometheusRenderer(); + } + ); - // Realtime event log — append-only CloudEvent records for SSE/polling clients. - $context->registerEventListener(ObjectCreatedEvent::class, RealtimeEventListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, RealtimeEventListener::class); - $context->registerEventListener(ObjectDeletedEvent::class, RealtimeEventListener::class); - $context->registerEventListener(ObjectTransitionedEvent::class, RealtimeEventListener::class); + $context->registerService( + \OCA\OpenRegister\AppHost\Observability\MetricsEngine::class, + function (ContainerInterface $container) { + return new \OCA\OpenRegister\AppHost\Observability\MetricsEngine( + objectSource: $container->get(\OCA\OpenRegister\AppHost\Observability\Source\ObjectMetricSource::class), + tableSource: $container->get(\OCA\OpenRegister\AppHost\Observability\Source\TableMetricSource::class), + appConfigSource: $container->get(\OCA\OpenRegister\AppHost\Observability\Source\AppConfigMetricSource::class), + providerSource: $container->get(\OCA\OpenRegister\AppHost\Observability\Source\ProviderMetricSource::class), + renderer: $container->get(\OCA\OpenRegister\AppHost\Observability\PrometheusRenderer::class), + manifestLoader: $container->get(\OCA\OpenRegister\AppHost\Observability\ManifestLoader::class), + cacheFactory: $container->get('OCP\ICacheFactory'), + config: $container->get('OCP\IConfig'), + logger: $container->get('Psr\Log\LoggerInterface') + ); + } + ); + }//end registerAppHostObservability() - // Translation sidecar projection — keeps oc_openregister_translations in sync with JSONB property data. - $context->registerEventListener(ObjectCreatedEvent::class, TranslationProjectionListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, TranslationProjectionListener::class); - $context->registerEventListener(ObjectDeletedEvent::class, TranslationProjectionListener::class); - $context->registerEventListener(ObjectTransitionedEvent::class, TranslationProjectionListener::class); - $context->registerEventListener(ObjectTransitionedEvent::class, AggregationCacheInvalidationListener::class); + /** + * Register MCP tool providers (built-ins first). + * + * Wires the four built-in IMcpToolProvider implementations into + * McpToolsService. External apps may call addProvider() after boot + * or override the McpToolsService binding to prepend their own providers. + * + * @param IRegistrationContext $context The registration context + * + * @return void + * + * @spec openspec/specs/chat-ai/spec.md#requirement-mcptoolsservice-provider-discovery-refactor + */ + private function registerMcpToolProviders(IRegistrationContext $context): void + { + $context->registerService( + McpToolsService::class, + function (ContainerInterface $container) { + $logger = $container->get('Psr\Log\LoggerInterface'); + $providers = [ + $container->get(RegistersToolProvider::class), + $container->get(SchemasToolProvider::class), + $container->get(ObjectsToolProvider::class), + $container->get(IntegrationsToolProvider::class), + ]; - // Webhook auto-create installer for x-openregister-notifications with webhook.persistent: true. - $context->registerEventListener(SchemaCreatedEvent::class, NotificationsAnnotationInstaller::class); - $context->registerEventListener(SchemaUpdatedEvent::class, NotificationsAnnotationInstaller::class); + $this->collectPerAppMcpProviders( + container: $container, + logger: $logger, + providers: $providers + ); - // Threshold trigger evaluator: re-runs aggregations on writes and dispatches when thresholds are crossed. - $context->registerEventListener(ObjectCreatedEvent::class, AggregationThresholdListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, AggregationThresholdListener::class); - $context->registerEventListener(ObjectDeletedEvent::class, AggregationThresholdListener::class); - $context->registerEventListener(ObjectTransitionedEvent::class, AggregationThresholdListener::class); + // Schema-derived providers (ADR-063 chain 2/3) MUST be + // appended LAST — McpToolsService is first-wins on tool + // name, so ordering after every hand-written per-app + // provider makes hand-written tools win automatically on a + // collision (REQ-DERIVED-003). + $this->collectSchemaDerivedMcpProviders( + container: $container, + logger: $logger, + providers: $providers + ); - // HookListener for schema hook execution on lifecycle events. - $context->registerEventListener(ObjectCreatingEvent::class, HookListener::class); - $context->registerEventListener(ObjectUpdatingEvent::class, HookListener::class); - $context->registerEventListener(ObjectDeletingEvent::class, HookListener::class); - $context->registerEventListener(ObjectCreatedEvent::class, HookListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, HookListener::class); - $context->registerEventListener(ObjectDeletedEvent::class, HookListener::class); + // Attributed providers (ADR-063 chain 3/3) MUST be collected + // LAST, after both hand-written and schema-derived providers, + // so their collision checks see the FULL existing id set: + // an attributed id colliding with a hand-written id is + // self-suppressed (hand-written wins, same as derived); an + // attributed id colliding with a DERIVED id is a discovery-time + // error (REQ-ATTR-002) — see collectAttributeMcpProviders(). + $this->collectAttributeMcpProviders( + container: $container, + logger: $logger, + providers: $providers + ); - // WebhookEventListener for webhook delivery. - $context->registerEventListener(ObjectCreatedEvent::class, WebhookEventListener::class); + return new McpToolsService( + providers: $providers, + logger: $logger + ); + } + ); + }//end registerMcpToolProviders() - // GraphQL subscription event listeners. - $context->registerEventListener(ObjectCreatedEvent::class, GraphQLSubscriptionListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, GraphQLSubscriptionListener::class); - $context->registerEventListener(ObjectDeletedEvent::class, GraphQLSubscriptionListener::class); + /** + * Derive one SchemaDerivedToolProvider per owning app that has at least + * one schema with a validated `x-openregister-mcp.enabled:true` block, + * and append it to $providers AFTER every hand-written provider already + * collected (built-ins + collectPerAppMcpProviders()). + * + * Fail-soft: enumeration or resolution failures are logged and leave + * $providers untouched — a broken derivation must never break MCP tool + * discovery for the rest of the instance. + * + * @param ContainerInterface $container The DI container. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * @param array $providers Providers array (modified in place by reference). + * + * @return void + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-001 — SchemaDerivedToolProvider emits declarative CRUD tools) + */ + private function collectSchemaDerivedMcpProviders( + ContainerInterface $container, + \Psr\Log\LoggerInterface $logger, + array &$providers + ): void { + try { + $schemaMapper = $container->get(SchemaMapper::class); + $registerMapper = $container->get(RegisterMapper::class); + + // Catalog enumeration, not data access: every enabled schema + // MUST be considered regardless of the resolving request's org + // context, or the derived tool catalog would silently vary by + // caller — unlike SchemasToolProvider's user-facing `list` + // action, RBAC/multitenancy is deliberately bypassed here. + $schemas = $schemaMapper->findAll(_rbac: false, _multitenancy: false); + + $schemasByApp = $this->groupOptedInSchemasByApp( + schemas: $schemas, + registerMapper: $registerMapper, + logger: $logger + ); - // Notify_push real-time push listeners (soft-fail when notify_push not installed). - $context->registerEventListener(ObjectCreatedEvent::class, NotifyPushListener::class); - $context->registerEventListener(ObjectUpdatedEvent::class, NotifyPushListener::class); - $context->registerEventListener(ObjectDeletedEvent::class, NotifyPushListener::class); + foreach ($schemasByApp as $appId => $schemaEntries) { + $suppressedIds = $this->collectExistingToolIds(providers: $providers, appId: $appId); + + $providers[] = new SchemaDerivedToolProvider( + appId: $appId, + schemaEntries: $schemaEntries, + suppressedIds: $suppressedIds, + objectService: $container->get(ObjectService::class), + auditTrailMapper: $container->get(AuditTrailMapper::class), + logger: $logger + ); + } + } catch (\Throwable $e) { + $logger->warning( + '[Application] Schema-derived provider enumeration failed: '.$e->getMessage() + ); + }//end try + }//end collectSchemaDerivedMcpProviders() + + /** + * Group every schema with a validated `x-openregister-mcp.enabled:true` + * block by its owning app id, each paired with its owning register. + * + * Owning app id resolution mirrors {@see \OCA\OpenRegister\Service\SemanticTypeResolver::owningAppId()}: + * the schema's own `application` field, else the owning register's + * `application`. A schema that resolves to no owning app is skipped — + * the ABI requires a concrete `getAppId()` per provider instance. + * + * @param array $schemas Every schema on the instance. + * @param RegisterMapper $registerMapper Register mapper (owning-app + owning-register resolution). + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * + * @return array> Opted-in schemas grouped by owning app id. + */ + private function groupOptedInSchemasByApp( + array $schemas, + RegisterMapper $registerMapper, + \Psr\Log\LoggerInterface $logger + ): array { + $schemasByApp = []; + + foreach ($schemas as $schema) { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-mcp'] ?? null); + if (is_array($annotation) === false || ($annotation['enabled'] ?? false) !== true) { + continue; + } + + $register = $this->resolveSchemaRegister(schema: $schema, registerMapper: $registerMapper, logger: $logger); + $owningApp = $this->resolveOwningAppId(schema: $schema, register: $register); + if ($owningApp === null) { + $logger->debug( + '[Application] Opted-in schema has no resolvable owning app — skipped', + ['schemaId' => $schema->getId()] + ); + continue; + } + + $schemasByApp[$owningApp][] = ['schema' => $schema, 'register' => $register]; + }//end foreach + + return $schemasByApp; + }//end groupOptedInSchemasByApp() + + /** + * Resolve a schema's owning register (the first register whose + * `schemas` id-list contains it), or null when the schema is orphaned. + * + * @param Schema $schema The schema. + * @param RegisterMapper $registerMapper Register mapper. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * + * @return Register|null The owning register, or null when none is found. + */ + private function resolveSchemaRegister( + Schema $schema, + RegisterMapper $registerMapper, + \Psr\Log\LoggerInterface $logger + ): ?Register { + try { + $registerId = $registerMapper->getFirstRegisterWithSchema(schemaId: (int) $schema->getId()); + if ($registerId === null) { + return null; + } + + return $registerMapper->find(id: $registerId, _rbac: false, _multitenancy: false); + } catch (\Throwable $e) { + $logger->debug( + '[Application] Owning-register resolution failed for schema '.$schema->getId().': '.$e->getMessage() + ); + return null; + } + }//end resolveSchemaRegister() + + /** + * Resolve the owning app id: the schema's own `application` field + * (present and reliable on real fleet schemas), else the owning + * register's `application`. Null when neither names an app. + * + * @param Schema $schema The candidate schema. + * @param Register|null $register The schema's owning register, when resolvable. + * + * @return string|null The owning app id, or null when undeclared. + */ + private function resolveOwningAppId(Schema $schema, ?Register $register): ?string + { + $appId = $schema->getApplication(); + if (is_string($appId) === true && $appId !== '') { + return $appId; + } + + if ($register !== null) { + $registerApp = $register->getApplication(); + if (is_string($registerApp) === true && $registerApp !== '') { + return $registerApp; + } + } + + return null; + }//end resolveOwningAppId() + + /** + * Collect every tool id already exposed, on the providers list built so + * far, by a provider for the given app id — the derived provider's + * self-suppression set (REQ-DERIVED-003). + * + * @param array $providers Providers collected so far. + * @param string $appId The owning app id. + * + * @return list Tool ids already claimed for this app. + */ + private function collectExistingToolIds(array $providers, string $appId): array + { + $ids = []; + + foreach ($providers as $provider) { + if ($provider->getAppId() !== $appId) { + continue; + } + + foreach ($provider->getTools() as $descriptor) { + if (isset($descriptor['id']) === true) { + $ids[] = (string) $descriptor['id']; + } + } + } + + return $ids; + }//end collectExistingToolIds() + + /** + * Derive one AttributeToolProvider per app that declares at least one + * `#[McpTool]`-attributed method on its own scannable service classes + * (ADR-063 chain 3/3), and append it to $providers. + * + * MUST run LAST (after collectPerAppMcpProviders() and + * collectSchemaDerivedMcpProviders()) so its collision check sees every + * hand-written AND derived id already claimed for that app: + * - An attributed id colliding with a hand-written id is self-suppressed + * (silent — hand-written wins, same precedence as the derived provider). + * - An attributed id colliding with a schema-DERIVED id is a + * developer-facing discovery-time error: the ambiguous attributed tool + * is rejected (logged, skipped) rather than silently resolved + * (REQ-ATTR-002). + * + * Discovery scope (which classes get reflected) is the provisional + * resolution of design.md's DEFERRED_QUESTION: an app opts in by + * registering an {@see IMcpScannableServices} implementation under the + * alias key `OCA\OpenRegister\Mcp\IMcpScannableServices::` — + * mirrors the existing per-app `IMcpToolProvider::` convention + * used by collectPerAppMcpProviders(). Apps that register nothing under + * that key are simply not scanned (opt-in, not enumerate-everything). + * + * Fail-soft: enumeration, reflection, or resolution failures are logged + * and leave $providers untouched for the affected app — a broken scan + * must never break MCP tool discovery for the rest of the instance. + * + * @param ContainerInterface $container The DI container. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * @param array $providers Providers array (modified in place by reference). + * + * @return void + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-002 — Reflection scanner registers attributed tools in the same catalog) + */ + private function collectAttributeMcpProviders( + ContainerInterface $container, + \Psr\Log\LoggerInterface $logger, + array &$providers + ): void { + try { + $appManager = $container->get('OCP\App\IAppManager'); + $scanner = new AttributeToolScanner(); + + foreach ($appManager->getInstalledApps() as $appId) { + // The `IMcpScannableServices::` opt-in alias — and the + // scanned service class itself (ADR-041) — live in the opting-in + // app's OWN DI container, NOT OR's shared container: a leaf app + // registers them via $context->registerServiceAlias() in its own + // Application::register(), which binds into that app's + // \OC\AppFramework\DependencyInjection\DIContainer. Resolving them + // off the shared $container silently returns nothing (#390), so + // both alias and instance resolution must go through the app's + // own container. Fail-soft: an app with no registered container + // (or a container whose resolution throws) is simply skipped. + $appContainer = $this->getRegisteredAppContainer(appId: $appId, logger: $logger); + if ($appContainer === null) { + continue; + } + + $classNames = $this->resolveScannableServiceClasses( + appContainer: $appContainer, + logger: $logger, + appId: $appId + ); + + if ($classNames === []) { + continue; + } + + $descriptors = $scanner->scanClasses(appId: $appId, classNames: $classNames, logger: $logger); + if ($descriptors === []) { + continue; + } + + $entries = $this->resolveAttributeEntries( + appContainer: $appContainer, + logger: $logger, + appId: $appId, + descriptors: $descriptors, + providers: $providers + ); + + if ($entries === []) { + continue; + } + + $providers[] = new AttributeToolProvider( + appId: $appId, + entries: $entries, + auditTrailMapper: $container->get(AuditTrailMapper::class), + logger: $logger + ); + }//end foreach + } catch (\Throwable $e) { + $logger->warning( + '[Application] Attributed-tool provider enumeration failed: '.$e->getMessage() + ); + }//end try + }//end collectAttributeMcpProviders() + + /** + * Obtain the opting-in app's OWN DI container so its + * `IMcpScannableServices::` opt-in alias (and the attributed + * service instances) can be resolved from where the leaf app actually + * registered them (#390). + * + * Wraps `\OC::$server->getRegisteredAppContainer($appId)`, which throws + * when the app has no bootstrapped container. Fail-soft: any error (app + * not registered, server seam unavailable) returns null so a single + * misbehaving app never fatals the whole MCP catalog. Isolated behind + * its own method so unit tests can override the app-container topology. + * + * @param string $appId The candidate app id. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * + * @return ContainerInterface|null The app's own container, or null when unavailable. + */ + protected function getRegisteredAppContainer( + string $appId, + \Psr\Log\LoggerInterface $logger + ): ?ContainerInterface { + try { + $appContainer = \OC::$server->getRegisteredAppContainer($appId); + } catch (\Throwable $e) { + $logger->debug( + '[Application] No registered app container', + ['appId' => $appId, 'error' => $e->getMessage()] + ); + return null; + } - // FilesSidebarListener injects the sidebar tab script into the Files app. - $context->registerEventListener('OCA\Files\Event\LoadAdditionalScriptsEvent', FilesSidebarListener::class); + return ($appContainer instanceof ContainerInterface) ? $appContainer : null; - // MailAppScriptListener injects the mail sidebar script into the Mail app - // (gated to the Mail app via TemplateResponse->getApp() === 'mail'). - $context->registerEventListener( - \OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent::class, - MailAppScriptListener::class - ); + }//end getRegisteredAppContainer() - // IntegrationGlobalScriptListener loads the shared integration-registry - // bootstrap on EVERY full-page render so the registry is installed + - // populated universally — letting leaves render inside any consuming - // app's object detail page (e.g. an OpenCatalogi publication) without - // that app bootstrapping the registry itself (universal-shared-integration-registry). - $context->registerEventListener( - \OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent::class, - \OCA\OpenRegister\Listener\IntegrationGlobalScriptListener::class - ); + /** + * Resolve one app's declared scannable service classes via the + * `IMcpScannableServices::` alias registered in THAT APP'S OWN + * container, or an empty list when the app has not opted in / the alias + * fails to resolve. + * + * The alias is registered by the leaf app in its own + * Application::register() and therefore lives in the app's own + * \OC\AppFramework\DependencyInjection\DIContainer — never in OR's shared + * container. Resolving it off the shared container silently discovered + * nothing (#390); {@see collectAttributeMcpProviders()} passes the + * per-app container obtained from {@see getRegisteredAppContainer()}. + * + * @param ContainerInterface $appContainer The opting-in app's own DI container. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * @param string $appId The candidate app id. + * + * @return list Candidate scannable service FQCNs (possibly empty). + */ + private function resolveScannableServiceClasses( + ContainerInterface $appContainer, + \Psr\Log\LoggerInterface $logger, + string $appId + ): array { + $key = 'OCA\\OpenRegister\\Mcp\\IMcpScannableServices::'.$appId; - // CommentsEntityListener registers "openregister" objectType for Nextcloud Comments. - $context->registerEventListener(CommentsEntityEvent::class, CommentsEntityListener::class); + try { + if ($appContainer->has($key) === false) { + return []; + } - // ObjectCleanupListener cleans up notes and tasks when an object is deleted. - $context->registerEventListener(ObjectDeletedEvent::class, ObjectCleanupListener::class); + $declaration = $appContainer->get($key); + } catch (\Throwable $e) { + $logger->debug( + '[Application] Scannable-services alias resolve failed', + ['appId' => $appId, 'error' => $e->getMessage()] + ); + return []; + } - // ActivityEventListener publishes Nextcloud Activity events for entity lifecycle. - $activityListener = \OCA\OpenRegister\Listener\ActivityEventListener::class; - $context->registerEventListener(ObjectCreatedEvent::class, $activityListener); - $context->registerEventListener(ObjectUpdatedEvent::class, $activityListener); - $context->registerEventListener(ObjectDeletedEvent::class, $activityListener); - $context->registerEventListener(RegisterCreatedEvent::class, $activityListener); - $context->registerEventListener(RegisterUpdatedEvent::class, $activityListener); - $context->registerEventListener(RegisterDeletedEvent::class, $activityListener); - $context->registerEventListener(SchemaCreatedEvent::class, $activityListener); - $context->registerEventListener(SchemaUpdatedEvent::class, $activityListener); - $context->registerEventListener(SchemaDeletedEvent::class, $activityListener); - }//end registerEventListeners() + if (($declaration instanceof IMcpScannableServices) === false) { + return []; + } + + return $declaration->getScannableServiceClasses(); + }//end resolveScannableServiceClasses() /** - * Register MCP tool providers (built-ins first). - * - * Wires the four built-in IMcpToolProvider implementations into - * McpToolsService. External apps may call addProvider() after boot - * or override the McpToolsService binding to prepend their own providers. - * - * @param IRegistrationContext $context The registration context - * - * @return void - * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#mcptoolsservice-provider-discovery-refactor + * Resolve every scanned descriptor for one app to an invocable entry + * (attaching the owning app's own DI-resolved service instance — + * ADR-041), applying the collision policy documented on + * {@see collectAttributeMcpProviders()}. + * + * The service instance is resolved from the owning app's OWN container + * (#390) — the scanned class and its dependencies are only reliably + * wireable there, not in OR's shared container. + * + * @param ContainerInterface $appContainer The owning app's own DI container. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * @param string $appId The owning app id. + * @param array> $descriptors Scanned descriptors (see AttributeToolScanner). + * @param array $providers Providers collected so far (for collision checks). + * + * @return list> Invocable entries (descriptor + resolved `instance`). */ - private function registerMcpToolProviders(IRegistrationContext $context): void - { - $context->registerService( - McpToolsService::class, - function (ContainerInterface $container) { - $logger = $container->get('Psr\Log\LoggerInterface'); - $providers = [ - $container->get(RegistersToolProvider::class), - $container->get(SchemasToolProvider::class), - $container->get(ObjectsToolProvider::class), - $container->get(IntegrationsToolProvider::class), - ]; + private function resolveAttributeEntries( + ContainerInterface $appContainer, + \Psr\Log\LoggerInterface $logger, + string $appId, + array $descriptors, + array $providers + ): array { + $derivedIds = []; + $otherIds = []; - $this->collectPerAppMcpProviders( - container: $container, - logger: $logger, - providers: $providers + foreach ($providers as $provider) { + if ($provider->getAppId() !== $appId) { + continue; + } + + $ids = array_column($provider->getTools(), 'id'); + if ($provider instanceof SchemaDerivedToolProvider) { + $derivedIds = array_merge($derivedIds, $ids); + continue; + } + + $otherIds = array_merge($otherIds, $ids); + } + + $entries = []; + + foreach ($descriptors as $descriptor) { + $id = (string) $descriptor['id']; + + if (in_array($id, $derivedIds, true) === true) { + $logger->error( + '[Application] Attributed tool id collides with a schema-derived tool id — rejected', + ['appId' => $appId, 'toolId' => $id] ); + continue; + } - return new McpToolsService( - providers: $providers, - logger: $logger + if (in_array($id, $otherIds, true) === true) { + // Hand-written-wins precedence — silent self-suppression, + // same as SchemaDerivedToolProvider's REQ-DERIVED-003. + continue; + } + + try { + $descriptor['instance'] = $appContainer->get($descriptor['class']); + } catch (\Throwable $e) { + $logger->warning( + '[Application] Could not resolve attributed tool service instance', + [ + 'appId' => $appId, + 'class' => $descriptor['class'], + 'error' => $e->getMessage(), + ] ); + continue; } - ); - }//end registerMcpToolProviders() + + $entries[] = $descriptor; + }//end foreach + + return $entries; + }//end resolveAttributeEntries() /** * Iterate over all installed apps and append any discovered @@ -1843,11 +3229,43 @@ function (ContainerInterface $container) { * Tries up to three candidate keys per app — alias, ucfirst FQCN, * and namespace-from-info.xml FQCN — stopping at the first match. * - * @param ContainerInterface $container The DI container. - * @param \Psr\Log\LoggerInterface $logger PSR logger. - * @param array &$providers Providers array (modified in place). + * PROBE CACHE (#308) — the raw probe is expensive and, for a stock NC + * instance, almost entirely negative: every installed app costs an + * info.xml read + XML parse (buildMcpProviderCandidates()) plus a + * throwing container lookup and an autoloader miss + * (tryResolveMcpProviderCandidate()). This factory is registered + * `$shared` so it already runs only once per request — the waste is + * CROSS-request, once per MCP/chat turn. + * + * We therefore cache the discovery RESOLUTION MAP (appId => winning + * candidate key, or null for "this app has no provider" — the negative + * result is the whole point) in a distributed cache. Only the small + * string map is cached: IMcpToolProvider instances are container-resolved + * and request-scoped, so a warm request still calls $container->get() for + * the handful of winners, but skips the per-app info.xml + candidate + * probing entirely. + * + * Invalidation is twofold, because neither mechanism suffices alone: + * - the cache key embeds a hash of the installed-app list, so + * installing/removing an app rebuilds immediately; but + * - an app UPGRADE can add a provider class WITHOUT changing the app + * list, so a short clamped TTL bounds that staleness. + * + * Fail-open: with no distributed cache configured the map is never read + * or written and behaviour is byte-for-byte the pre-cache path. + * + * NOTE: only the DISCOVERY result is cached. McpToolsService::addProvider() + * remains a live runtime path for apps that self-register from their own + * boot() when discovery misses them — it mutates the live instance and is + * deliberately outside this cache. + * + * @param ContainerInterface $container The DI container. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * @param array $providers Providers array (modified in place by reference). * * @return void + * + * @spec openspec/specs/chat-ai/spec.md#requirement-mcptoolsservice-provider-discovery-refactor */ private function collectPerAppMcpProviders( ContainerInterface $container, @@ -1856,8 +3274,29 @@ private function collectPerAppMcpProviders( ): void { try { $appManager = $container->get('OCP\App\IAppManager'); - foreach ($appManager->getInstalledApps() as $appId) { - $candidates = $this->buildMcpProviderCandidates( + $appIds = $appManager->getInstalledApps(); + + $cache = $this->getMcpDiscoveryCache(container: $container, logger: $logger); + $cacheKey = $this->buildMcpDiscoveryCacheKey(appIds: $appIds); + $map = $this->readMcpDiscoveryMap(cache: $cache, cacheKey: $cacheKey); + + if ($map !== null) { + // WARM — resolve only the known winners; no info.xml reads, no + // class_exists() misses, no throwing container lookups. + $this->materialiseMcpProvidersFromMap( + container: $container, + logger: $logger, + map: $map, + providers: $providers + ); + return; + } + + // COLD — run the full probe once and record the outcome per app. + $map = []; + foreach ($appIds as $appId) { + $map[$appId] = null; + $candidates = $this->buildMcpProviderCandidates( appId: $appId, appManager: $appManager ); @@ -1871,17 +3310,221 @@ private function collectPerAppMcpProviders( ); if ($resolved !== null) { $providers[] = $resolved; + $map[$appId] = $key; break; } }//end foreach }//end foreach + + $this->writeMcpDiscoveryMap( + container: $container, + cache: $cache, + cacheKey: $cacheKey, + map: $map + ); + + // One summary line per REBUILD replaces the ~2-3 debug lines per + // missing app the raw probe used to emit on every single request. + $logger->debug( + '[Application] Per-app MCP provider discovery rebuilt', + [ + 'appsProbed' => count($map), + 'discovered' => array_keys(array_filter($map, static fn ($key) => $key !== null)), + 'cached' => ($cache !== null), + ] + ); } catch (\Throwable $e) { $logger->warning( - '[McpToolsService] Per-app provider enumeration failed: '.$e->getMessage() + '[Application] Per-app provider enumeration failed: '.$e->getMessage() ); }//end try }//end collectPerAppMcpProviders() + /** + * Append providers for a cached discovery map's winning candidate keys. + * + * A cached key can go stale within the TTL (e.g. an app disabled mid-window), + * so each key is still resolved through the normal fail-soft path and a + * failure simply yields no provider for that app. + * + * @param ContainerInterface $container The DI container. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * @param array $map Cached appId => winning key|null map. + * @param array $providers Providers array (modified in place by reference). + * + * @return void + */ + private function materialiseMcpProvidersFromMap( + ContainerInterface $container, + \Psr\Log\LoggerInterface $logger, + array $map, + array &$providers + ): void { + foreach ($map as $appId => $key) { + if ($key === null) { + // Cached negative result — the app was probed on a previous + // request and ships no MCP tool provider. Skip silently. + continue; + } + + $resolved = $this->tryResolveMcpProviderCandidate( + container: $container, + logger: $logger, + appId: (string) $appId, + key: $key + ); + if ($resolved !== null) { + $providers[] = $resolved; + } + }//end foreach + }//end materialiseMcpProvidersFromMap() + + /** + * Obtain the distributed discovery cache, or null when unavailable. + * + * Only armed when a genuinely DISTRIBUTED memory cache is configured + * (ICacheFactory::isAvailable()); createDistributed() otherwise silently + * degrades to a node-local backend. Returning null disables the cache and + * restores the exact pre-cache behaviour (constraint: fail open). + * + * @param ContainerInterface $container The DI container. + * @param \Psr\Log\LoggerInterface $logger PSR logger. + * + * @return ICache|null The cache, or null when no distributed backend exists. + */ + private function getMcpDiscoveryCache( + ContainerInterface $container, + \Psr\Log\LoggerInterface $logger + ): ?ICache { + try { + $cacheFactory = $container->get(ICacheFactory::class); + if ($cacheFactory instanceof ICacheFactory === false || $cacheFactory->isAvailable() === false) { + return null; + } + + return $cacheFactory->createDistributed(prefix: self::MCP_PROBE_CACHE_PREFIX); + } catch (\Throwable $e) { + $logger->debug( + '[Application] MCP discovery cache unavailable: '.$e->getMessage() + ); + return null; + }//end try + }//end getMcpDiscoveryCache() + + /** + * Build the discovery-map cache key for an installed-app list. + * + * The list is sorted before hashing so a pure ordering change from + * IAppManager does not needlessly invalidate the map. + * + * @param string[] $appIds The installed app ids. + * + * @return string The cache key. + */ + private function buildMcpDiscoveryCacheKey(array $appIds): string + { + $sorted = $appIds; + sort($sorted); + + return 'map:'.hash('sha256', implode(',', $sorted)); + }//end buildMcpDiscoveryCacheKey() + + /** + * Read the cached discovery map, or null on miss / disabled cache. + * + * @param ICache|null $cache The cache, or null when disabled. + * @param string $cacheKey The cache key. + * + * @return array|null The cached map, or null on miss. + */ + private function readMcpDiscoveryMap(?ICache $cache, string $cacheKey): ?array + { + if ($cache === null) { + return null; + } + + $blob = $cache->get(key: $cacheKey); + if (is_string($blob) === false) { + return null; + } + + $data = json_decode($blob, true); + if (is_array($data) === false) { + return null; + } + + $map = []; + foreach ($data as $appId => $key) { + if ($key === null) { + $map[(string) $appId] = null; + continue; + } + + if (is_string($key) === false || $key === '') { + // Corrupt entry — discard the whole blob rather than trust it. + return null; + } + + $map[(string) $appId] = $key; + }//end foreach + + return $map; + }//end readMcpDiscoveryMap() + + /** + * Write the discovery map to the cache with the clamped TTL. + * + * @param ContainerInterface $container The DI container (for the TTL config read). + * @param ICache|null $cache The cache, or null when disabled. + * @param string $cacheKey The cache key. + * @param array $map The appId => winning key|null map. + * + * @return void + */ + private function writeMcpDiscoveryMap( + ContainerInterface $container, + ?ICache $cache, + string $cacheKey, + array $map + ): void { + if ($cache === null) { + return; + } + + $blob = json_encode($map); + if (is_string($blob) === false) { + return; + } + + $cache->set(key: $cacheKey, value: $blob, ttl: $this->resolveMcpProbeCacheTtl(container: $container)); + }//end writeMcpDiscoveryMap() + + /** + * Resolve the discovery-cache TTL, clamped to the allowed range. + * + * The TTL is the ONLY thing that catches a provider class added by an app + * UPGRADE (which leaves the installed-app list, and therefore the cache + * key, unchanged) — so it is deliberately short and bounded. + * + * @param ContainerInterface $container The DI container. + * + * @return int The TTL in seconds. + */ + private function resolveMcpProbeCacheTtl(ContainerInterface $container): int + { + try { + $ttl = $container->get(IAppConfig::class)->getValueInt( + self::APP_ID, + self::MCP_PROBE_TTL_CONFIG_KEY, + self::MCP_PROBE_TTL_DEFAULT + ); + } catch (\Throwable $e) { + $ttl = self::MCP_PROBE_TTL_DEFAULT; + } + + return max(self::MCP_PROBE_TTL_MIN, min(self::MCP_PROBE_TTL_MAX, $ttl)); + }//end resolveMcpProbeCacheTtl() + /** * Build the list of candidate lookup keys for a given app's MCP tool provider. * @@ -1890,14 +3533,17 @@ private function collectPerAppMcpProviders( * 2. The canonical FQCN built from ucfirst($appId). * 3. (Optional) A FQCN derived from the `` declared in info.xml, * when the declared namespace differs from ucfirst($appId) (covers camel-cased - * app names like `openbuilt` → `OpenBuilt`). + * app names like `openbuild` → `OpenBuild`). + * + * Protected as a test seam (#308): the probe-cache tests override this to + * count how often the expensive info.xml read actually runs. * * @param string $appId The Nextcloud app id. * @param mixed $appManager The IAppManager instance. * * @return string[] Ordered candidate key list. */ - private function buildMcpProviderCandidates(string $appId, $appManager): array + protected function buildMcpProviderCandidates(string $appId, $appManager): array { $candidates = [ 'OCA\\OpenRegister\\Mcp\\IMcpToolProvider::'.$appId, @@ -1940,14 +3586,19 @@ private function buildMcpProviderCandidates(string $appId, $appManager): array * candidate does not exist, could not be resolved, or resolved to a * non-IMcpToolProvider object. * + * Protected as a test seam (#308): the probe-cache tests override this to + * count how many candidate resolutions a request actually performs. + * * @param ContainerInterface $container The DI container. * @param \Psr\Log\LoggerInterface $logger PSR logger. * @param string $appId The Nextcloud app id (for logging). * @param string $key The candidate lookup key. * * @return IMcpToolProvider|null The resolved provider or null. + * + * @spec openspec/specs/chat-ai/spec.md#requirement-mcptoolsservice-provider-discovery-refactor */ - private function tryResolveMcpProviderCandidate( + protected function tryResolveMcpProviderCandidate( ContainerInterface $container, \Psr\Log\LoggerInterface $logger, string $appId, @@ -1959,31 +3610,39 @@ private function tryResolveMcpProviderCandidate( // non-existent class would throw NotFoundExceptionInterface for every // installed app (noisy). if (class_exists($key) === false) { - $logger->warning( - '[McpToolsService] Class does not exist', - ['appId' => $appId, 'fqcn' => $key] - ); + // Expected for every installed app that ships no MCP tool + // provider — i.e. almost all of them. Deliberately silent: + // this fired ~2 lines per installed app per request (#308). + // The single summary line in collectPerAppMcpProviders() + // carries the same information at 1/143rd the volume. return null; } } $appProvider = $container->get($key); if ($appProvider instanceof IMcpToolProvider) { - $logger->warning( - '[McpToolsService] Discovered per-app tool provider', + $logger->info( + '[Application] Discovered per-app tool provider', ['appId' => $appId, 'via' => $key, 'class' => get_class($appProvider)] ); return $appProvider; } - // get_debug_type() returns the FQCN for objects and the type name for scalars. + // Get_debug_type() returns the FQCN for objects and the type name for scalars. $logger->warning( - '[McpToolsService] Resolved but not IMcpToolProvider', + '[Application] Resolved but not IMcpToolProvider', ['appId' => $appId, 'via' => $key, 'class' => get_debug_type($appProvider)] ); + } catch (\Psr\Container\NotFoundExceptionInterface $e) { + // Alias key not registered — the expected case for every app + // without an MCP provider, i.e. almost all of them. Deliberately + // silent; see the class_exists() branch above (#308). + return null; } catch (\Throwable $e) { + // A genuine misconfiguration (the alias IS registered but its + // factory blew up) — this one is worth hearing about. $logger->warning( - '[McpToolsService] Resolve failed', + '[Application] Resolve failed', ['appId' => $appId, 'key' => $key, 'error' => $e->getMessage()] ); }//end try @@ -1998,7 +3657,7 @@ private function tryResolveMcpProviderCandidate( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-24 + * @spec openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md */ public function boot(IBootContext $context): void { @@ -2019,6 +3678,8 @@ public function boot(IBootContext $context): void // registry never touches a provider's wrapped service unless a // caller actually invokes that provider's CRUD path. $this->bootBuiltinIntegrationProviders(server: $server); + $this->bootObjectSourceProviders(server: $server); + $this->bootFederation(server: $server); // Allow GitHub-hosted avatars in img-src so the CnRoadmapItem // component can render submitter faces alongside the issues @@ -2029,6 +3690,13 @@ public function boot(IBootContext $context): void // the host here lifts the block instance-wide for any app that // proxies GitHub issues through OpenRegister. $this->relaxCspForGithubAvatars(server: $server); + + // Allow registering the Web Push Service Worker (openregister-web-push-engine). + // Nextcloud's default CSP uses a nonce-based script-src, which a Service + // Worker script cannot carry, so navigator.serviceWorker.register() is + // rejected with "violates the Content Security Policy". Adding + // `worker-src 'self'` lets the same-origin SW register. + $this->relaxCspForWebPushWorker(server: $server); }//end boot() /** @@ -2041,7 +3709,7 @@ public function boot(IBootContext $context): void * * @return void * - * @spec openspec/changes/add-features-roadmap-menu/specs/features-roadmap-component/spec.md#requirement-roadmap-item-avatar + * @spec openspec/specs/features-roadmap-menu/spec.md */ private function relaxCspForGithubAvatars($server): void { @@ -2056,6 +3724,32 @@ private function relaxCspForGithubAvatars($server): void } }//end relaxCspForGithubAvatars() + /** + * Allow the same-origin Web Push Service Worker to register by adding + * `worker-src 'self'` to Nextcloud's default Content-Security-Policy via + * `IContentSecurityPolicyManager::addDefaultPolicy()`. NC's nonce-based + * script-src otherwise rejects `navigator.serviceWorker.register()` with a + * CSP violation (a SW script cannot carry a request nonce). Idempotent — + * NC merges policies additively, never narrowing. + * + * @param mixed $server Server container (passed in from boot()). + * + * @return void + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + private function relaxCspForWebPushWorker($server): void + { + try { + $cspManager = $server->get(IContentSecurityPolicyManager::class); + $policy = new ContentSecurityPolicy(); + $policy->addAllowedWorkerSrcDomain("'self'"); + $cspManager->addDefaultPolicy($policy); + } catch (\Throwable $e) { + // CSP manager unavailable (rare). Stay silent rather than fail boot. + } + }//end relaxCspForWebPushWorker() + /** * Resolve every BuiltinProviders/* class and register it with the * shared IntegrationRegistry. @@ -2070,7 +3764,7 @@ private function relaxCspForGithubAvatars($server): void * * @return void * - * @spec openspec/changes/pluggable-integration-registry/tasks.md#task-17 + * @spec openspec/specs/integration-registry/spec.md */ private function bootBuiltinIntegrationProviders($server): void { @@ -2091,46 +3785,49 @@ private function bootBuiltinIntegrationProviders($server): void TagsProvider::class, AuditTrailProvider::class, // Leaves: external (OpenConnector-backed). - // @spec openspec/changes/integration-xwiki/tasks.md. + // @spec openspec/specs/integration-xwiki/spec.md (canonical spec). XwikiProvider::class, - // @spec openspec/changes/integration-openproject/tasks.md. + // @spec openspec/specs/integration-openproject/spec.md (canonical spec). OpenProjectProvider::class, + // @spec openspec/specs/integration-registry/spec.md (canonical spec). + KvkProvider::class, + OpenCorporatesProvider::class, // Leaves: NC-native, backend-shipped (wrap existing OR services). - // @spec openspec/changes/integration-calendar/tasks.md. + // @spec openspec/specs/integration-calendar/spec.md (canonical spec). CalendarProvider::class, - // @spec openspec/changes/integration-contacts/tasks.md. + // @spec openspec/specs/integration-contacts/spec.md (canonical spec). ContactsProvider::class, - // @spec openspec/changes/integration-deck/tasks.md. + // @spec openspec/specs/integration-deck/spec.md (canonical spec). DeckProvider::class, - // @spec openspec/changes/integration-email/tasks.md. + // @spec openspec/specs/integration-email/spec.md (canonical spec). EmailProvider::class, // Leaves: NC-app-backed greenfield (registry surface only; // service + link table land in per-leaf follow-ups). - // @spec openspec/changes/integration-activity/tasks.md. + // @spec openspec/specs/integration-activity/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\ActivityProvider::class, - // @spec openspec/changes/integration-analytics/tasks.md. + // @spec openspec/specs/integration-analytics/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\AnalyticsProvider::class, - // @spec openspec/changes/integration-bookmarks/tasks.md. + // @spec openspec/specs/integration-bookmarks/spec.md (canonical spec). BookmarksProvider::class, - // @spec openspec/changes/integration-collectives/tasks.md. + // @spec openspec/specs/integration-collectives/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\CollectivesProvider::class, - // @spec openspec/changes/integration-cospend/tasks.md. + // @spec openspec/specs/integration-cospend/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\CospendProvider::class, - // @spec openspec/changes/integration-flow/tasks.md. + // @spec openspec/specs/integration-flow/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\FlowProvider::class, - // @spec openspec/changes/integration-forms/tasks.md. + // @spec openspec/specs/integration-forms/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\FormsProvider::class, - // @spec openspec/changes/integration-maps/tasks.md. + // @spec openspec/specs/integration-maps/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\MapsProvider::class, - // @spec openspec/changes/integration-photos/tasks.md. + // @spec openspec/specs/integration-photos/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\PhotosProvider::class, - // @spec openspec/changes/integration-polls/tasks.md. + // @spec openspec/specs/integration-polls/spec.md (canonical spec). PollsProvider::class, - // @spec openspec/changes/integration-shares/tasks.md. + // @spec openspec/specs/integration-shares/spec.md (canonical spec). SharesProvider::class, - // @spec openspec/changes/integration-talk/tasks.md. + // @spec openspec/specs/integration-talk/spec.md (canonical spec). TalkProvider::class, - // @spec openspec/changes/integration-time-tracker/tasks.md. + // @spec openspec/specs/integration-time-tracker/spec.md (canonical spec). \OCA\OpenRegister\Service\Integration\Providers\TimeProvider::class, ]; @@ -2168,4 +3865,90 @@ private function bootBuiltinIntegrationProviders($server): void }//end try }//end foreach }//end bootBuiltinIntegrationProviders() + + /** + * Register the built-in object-source providers with the shared + * ObjectSourceRegistry. + * + * Runs in boot() (post-registration) because addProvider() needs the + * registry instance. Guarded so one absent provider never takes the app + * down — a failing provider simply won't serve its bound schemas. + * + * @param mixed $server Server container (passed in from boot()). + * + * @return void + * + * @spec openspec/changes/object-source-providers/tasks.md#5-caldav-vtodo-provider + */ + private function bootObjectSourceProviders($server): void + { + try { + $registry = $server->get(ObjectSourceRegistry::class); + } catch (\Throwable $e) { + return; + } + + // Register each built-in provider independently so one absent provider + // never blocks the others — a failing provider simply won't serve its + // bound schemas. + $providerClasses = [ + CalDavVtodoObjectSourceProvider::class, + UserDirectoryObjectSourceProvider::class, + GroupObjectSourceProvider::class, + ContactsObjectSourceProvider::class, + CalendarEventObjectSourceProvider::class, + FilesObjectSourceProvider::class, + DeckObjectSourceProvider::class, + TalkObjectSourceProvider::class, + FederatedObjectSourceProvider::class, + TablesObjectSourceProvider::class, + // Virtual registers (dbal-virtual-registers): external SQL databases over Doctrine DBAL. + DbalObjectSourceProvider::class, + ]; + foreach ($providerClasses as $providerClass) { + try { + $registry->addProvider($server->get($providerClass)); + } catch (\Throwable $e) { + try { + $server->get(\Psr\Log\LoggerInterface::class)->warning( + '[ObjectSource] could not register '.$providerClass.': '.$e->getMessage() + ); + } catch (\Throwable $inner) { + // Logger unavailable on this build — nothing to do. + } + } + } + }//end bootObjectSourceProviders() + + /** + * Register OpenRegister's OCM cloud-federation provider so cross-instance + * shares of registers/schemas/objects ride Nextcloud's Open Cloud Mesh + * transport. Guarded: if the federation stack is unavailable, OpenRegister + * still runs (federation simply won't be offered). + * + * @param mixed $server Server container (passed from boot()). + * + * @return void + */ + private function bootFederation($server): void + { + try { + $manager = $server->get(ICloudFederationProviderManager::class); + $manager->addCloudFederationProvider( + 'openregister', + 'OpenRegister', + function () use ($server) { + return $server->get(OpenRegisterCloudFederationProvider::class); + } + ); + } catch (\Throwable $e) { + try { + $server->get(\Psr\Log\LoggerInterface::class)->warning( + '[Federation] could not register OCM provider: '.$e->getMessage() + ); + } catch (\Throwable $inner) { + // Logging unavailable — never take the app down over federation. + } + } + }//end bootFederation() }//end class diff --git a/lib/BackgroundJob/ActionRetryJob.php b/lib/BackgroundJob/ActionRetryJob.php index 58ec94c379..09ed115c5c 100644 --- a/lib/BackgroundJob/ActionRetryJob.php +++ b/lib/BackgroundJob/ActionRetryJob.php @@ -19,8 +19,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-32 - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md + * @spec openspec/specs/actions/spec.md */ declare(strict_types=1); @@ -80,7 +80,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-4 + * @spec openspec/specs/actions/spec.md */ protected function run($arguments): void { @@ -181,7 +181,7 @@ protected function run($arguments): void * * @return int Delay in seconds * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-4 + * @spec openspec/specs/actions/spec.md */ public static function calculateDelay(string $policy, int $attempt): int { diff --git a/lib/BackgroundJob/ActionScheduleJob.php b/lib/BackgroundJob/ActionScheduleJob.php index 54c6998f97..90067f41d6 100644 --- a/lib/BackgroundJob/ActionScheduleJob.php +++ b/lib/BackgroundJob/ActionScheduleJob.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-5 + * @spec openspec/specs/actions/spec.md */ declare(strict_types=1); @@ -54,7 +54,7 @@ class ActionScheduleJob extends TimedJob * @param ActionExecutor $actionExecutor Action executor * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-6 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function __construct( ITimeFactory $time, @@ -75,8 +75,8 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-6 - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md + * @spec openspec/specs/actions/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/ActorForwardedJob.php b/lib/BackgroundJob/ActorForwardedJob.php new file mode 100644 index 0000000000..6c7f9d8c67 --- /dev/null +++ b/lib/BackgroundJob/ActorForwardedJob.php @@ -0,0 +1,189 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Service\Deferral\DeferredListenerContext; +use OCA\OpenRegister\Service\OrganisationService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\IUserManager; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Base class for deferred listener jobs that must run as the original actor. + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-1.3 + */ +abstract class ActorForwardedJob extends QueuedJob +{ + /** + * Wire the identity plumbing shared by all actor-forwarded jobs. + * + * @param ITimeFactory $time Time factory for the parent job class. + * @param IUserSession $userSession Session to impersonate on / restore. + * @param IUserManager $userManager Resolver for the captured user id. + * @param OrganisationService $organisation Active-organisation resolver (drift detection). + * @param LoggerInterface $logger PSR logger (shared with subclasses). + * + * @return void + */ + public function __construct( + ITimeFactory $time, + private readonly IUserSession $userSession, + private readonly IUserManager $userManager, + private readonly OrganisationService $organisation, + protected readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + }//end __construct() + + /** + * Re-establish the captured actor, run the deferred work, restore. + * + * @param array $argument Serialized DeferredListenerContext. + * + * @return void + * + * @SuppressWarnings("PHPMD.StaticAccess") — fromJobArguments is the canonical named constructor of the context VO. + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + protected function run($argument): void + { + $context = DeferredListenerContext::fromJobArguments($argument); + if (count($context->getEntries()) === 0) { + $this->logger->info( + message: '[ActorForwardedJob] Job carried no entries — nothing to do', + context: ['file' => __FILE__, 'line' => __LINE__, 'job' => static::class] + ); + return; + } + + $userId = $context->getUserId(); + $user = null; + if ($userId !== null) { + $user = $this->userManager->get($userId); + if ($user === null) { + // The captured actor no longer exists. Running anyway would + // misattribute the work to whatever identity the worker has, + // so the job skips — matching "owner gone" semantics used by + // scheduled reports. + $this->logger->warning( + message: '[ActorForwardedJob] Captured user no longer resolves — skipping deferred work', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'job' => static::class, + 'userId' => $userId, + ] + ); + return; + } + } + + $previousUser = $this->userSession->getUser(); + if ($user !== null) { + $this->userSession->setUser($user); + } + + try { + $this->logOrganisationDrift(context: $context); + $this->runDeferred(context: $context); + } finally { + // ALWAYS restore — also on exceptions — so the cron process + // never carries this job's identity into the next job. + $this->userSession->setUser($previousUser); + } + }//end run() + + /** + * The deferred listener work, executed under the re-established actor. + * + * Implementations MUST be idempotent (at-least-once delivery) and MUST + * treat entries whose object is gone or soft-deleted as stale no-ops. + * (Named runDeferred because QueuedJob::execute() is a final OCP method.) + * + * @param DeferredListenerContext $context The captured dispatch-time context. + * + * @return void + */ + abstract protected function runDeferred(DeferredListenerContext $context): void; + + /** + * Log when the user's active organisation changed since capture. + * + * The job proceeds under the user's CURRENT authority — captured + * organisation context is observability metadata, never a privilege + * source. + * + * @param DeferredListenerContext $context The captured context. + * + * @return void + */ + private function logOrganisationDrift(DeferredListenerContext $context): void + { + $captured = $context->getOrganisationUuid(); + if ($captured === null || $context->getUserId() === null) { + return; + } + + try { + $current = $this->organisation->getActiveOrganisation()?->getUuid(); + } catch (\Throwable $e) { + return; + } + + if ($current !== $captured) { + $this->logger->info( + message: '[ActorForwardedJob] Active organisation drifted since capture — running under current authority', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'job' => static::class, + 'captured' => $captured, + 'current' => $current, + ] + ); + } + }//end logOrganisationDrift() +}//end class diff --git a/lib/BackgroundJob/AggregationThresholdJob.php b/lib/BackgroundJob/AggregationThresholdJob.php new file mode 100644 index 0000000000..c02b03e9fe --- /dev/null +++ b/lib/BackgroundJob/AggregationThresholdJob.php @@ -0,0 +1,126 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @psalm-suppress UnusedClass + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\Aggregation\ThresholdEvaluationService; +use OCA\OpenRegister\Service\Deferral\DeferredEntryObjectResolver; +use OCA\OpenRegister\Service\Deferral\DeferredListenerContext; +use OCA\OpenRegister\Service\OrganisationService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IUserManager; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Deferred threshold-notification evaluation under the forwarded actor. + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-2.3 + */ +class AggregationThresholdJob extends ActorForwardedJob +{ + /** + * Wire the evaluation collaborators on top of the actor plumbing. + * + * @param ITimeFactory $time Time factory for the parent job class. + * @param IUserSession $userSession Session to impersonate on / restore. + * @param IUserManager $userManager Resolver for the captured user id. + * @param OrganisationService $organisation Active-organisation resolver. + * @param LoggerInterface $logger PSR logger. + * @param DeferredEntryObjectResolver $resolver Stale-safe entry re-fetch. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param ThresholdEvaluationService $evaluator Shared threshold evaluation logic. + * + * @return void + */ + public function __construct( + ITimeFactory $time, + IUserSession $userSession, + IUserManager $userManager, + OrganisationService $organisation, + LoggerInterface $logger, + private readonly DeferredEntryObjectResolver $resolver, + private readonly SchemaMapper $schemaMapper, + private readonly ThresholdEvaluationService $evaluator + ) { + parent::__construct( + time: $time, + userSession: $userSession, + userManager: $userManager, + organisation: $organisation, + logger: $logger + ); + }//end __construct() + + /** + * Evaluate thresholds for every live entry's schema. + * + * Per-entry failures are logged and do not abort the chunk. + * + * @param DeferredListenerContext $context The captured dispatch-time context. + * + * @return void + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + protected function runDeferred(DeferredListenerContext $context): void + { + foreach ($context->getEntries() as $entry) { + $object = $this->resolver->resolve(entry: $entry); + if ($object === null) { + continue; + } + + try { + $schema = $this->schemaMapper->find((string) ($entry['schema'] ?? '')); + $this->evaluator->evaluateSchema(schema: $schema, object: $object); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[AggregationThresholdJob] Threshold evaluation failed for entry', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => ($entry['uuid'] ?? null), + 'schema' => ($entry['schema'] ?? null), + 'error' => $e->getMessage(), + ] + ); + } + }//end foreach + }//end runDeferred() +}//end class diff --git a/lib/BackgroundJob/AnnotationNotificationDispatchJob.php b/lib/BackgroundJob/AnnotationNotificationDispatchJob.php new file mode 100644 index 0000000000..e89e0e2312 --- /dev/null +++ b/lib/BackgroundJob/AnnotationNotificationDispatchJob.php @@ -0,0 +1,188 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @psalm-suppress UnusedClass + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Service\Deferral\DeferredEntryObjectResolver; +use OCA\OpenRegister\Service\Deferral\DeferredListenerContext; +use OCA\OpenRegister\Service\Notification\AnnotationNotificationDispatcher; +use OCA\OpenRegister\Service\OrganisationService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IUserManager; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Deferred x-openregister-notifications dispatch under the forwarded actor. + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-2.2 + */ +class AnnotationNotificationDispatchJob extends ActorForwardedJob +{ + /** + * Wire the dispatcher on top of the actor plumbing. + * + * @param ITimeFactory $time Time factory for the parent job class. + * @param IUserSession $userSession Session to impersonate on / restore. + * @param IUserManager $userManager Resolver for the captured user id. + * @param OrganisationService $organisation Active-organisation resolver. + * @param LoggerInterface $logger PSR logger. + * @param DeferredEntryObjectResolver $resolver Stale-safe entry re-fetch. + * @param AnnotationNotificationDispatcher $dispatcher Notification dispatcher doing the real work. + * + * @return void + */ + public function __construct( + ITimeFactory $time, + IUserSession $userSession, + IUserManager $userManager, + OrganisationService $organisation, + LoggerInterface $logger, + private readonly DeferredEntryObjectResolver $resolver, + private readonly AnnotationNotificationDispatcher $dispatcher + ) { + parent::__construct( + time: $time, + userSession: $userSession, + userManager: $userManager, + organisation: $organisation, + logger: $logger + ); + }//end __construct() + + /** + * Replay the inline listener's dispatch calls for every live entry. + * + * Per-entry failures are logged and do not abort the chunk. + * + * @param DeferredListenerContext $context The captured dispatch-time context. + * + * @return void + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + protected function runDeferred(DeferredListenerContext $context): void + { + foreach ($context->getEntries() as $entry) { + $object = $this->resolver->resolve(entry: $entry); + if ($object === null) { + continue; + } + + try { + $this->dispatchEntry(entry: $entry, object: $object); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[AnnotationNotificationDispatchJob] Dispatch failed for entry', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => ($entry['uuid'] ?? null), + 'trigger' => ($entry['trigger'] ?? null), + 'error' => $e->getMessage(), + ] + ); + } + }//end foreach + }//end runDeferred() + + /** + * Mirror the inline AnnotationNotificationListener dispatch semantics. + * + * - `created` → dispatch('created'). + * - `transition` → dispatch('transition', {action, from, to}). + * - `updated` → dispatch('updated', old/new context when a snapshot + * exists) followed by dispatch('calculatedChange', same context) — + * exactly the pair the inline listener fired. + * + * @param array $entry The job entry. + * @param ObjectEntity $object The re-fetched current object. + * + * @return void + */ + private function dispatchEntry(array $entry, ObjectEntity $object): void + { + $trigger = (string) ($entry['trigger'] ?? ''); + + if ($trigger === 'created') { + $this->dispatcher->dispatch(object: $object, trigger: 'created'); + return; + } + + if ($trigger === 'transition') { + $this->dispatcher->dispatch( + object: $object, + trigger: 'transition', + context: [ + 'action' => (string) ($entry['action'] ?? ''), + 'from' => (string) ($entry['from'] ?? ''), + 'to' => (string) ($entry['to'] ?? ''), + ] + ); + return; + } + + if ($trigger === 'updated') { + $oldData = ($entry['oldData'] ?? null); + + $updatedContext = []; + if (is_array($oldData) === true) { + $updatedContext = [ + '_newData' => ($object->getObject() ?? []), + '_oldData' => $oldData, + ]; + } + + $this->dispatcher->dispatch(object: $object, trigger: 'updated', context: $updatedContext); + + if (is_array($oldData) === true) { + $this->dispatcher->dispatch( + object: $object, + trigger: 'calculatedChange', + context: [ + '_newData' => ($object->getObject() ?? []), + '_oldData' => $oldData, + ] + ); + } + }//end if + }//end dispatchEntry() +}//end class diff --git a/lib/BackgroundJob/AvgRetentionJob.php b/lib/BackgroundJob/AvgRetentionJob.php index 559252f03e..a11c855678 100644 --- a/lib/BackgroundJob/AvgRetentionJob.php +++ b/lib/BackgroundJob/AvgRetentionJob.php @@ -22,9 +22,9 @@ * @category BackgroundJob * @package OCA\OpenRegister\BackgroundJob * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -81,7 +81,7 @@ class AvgRetentionJob extends TimedJob * @param AvgRetentionService $retentionService Domain service. * @param LoggerInterface $logger Logger. * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ public function __construct( ITimeFactory $time, @@ -103,7 +103,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/BackfillCalendarLinksJob.php b/lib/BackgroundJob/BackfillCalendarLinksJob.php index dc44ec7852..d8ace13146 100644 --- a/lib/BackgroundJob/BackfillCalendarLinksJob.php +++ b/lib/BackgroundJob/BackfillCalendarLinksJob.php @@ -195,9 +195,20 @@ private function backfillEvent( $link->setSchemaId((int) ($event['schemaId'] ?? 0)); $link->setCalendarUri(''); - $calendarId = isset($event['calendarId']) ? (int) $event['calendarId'] : null; - $summary = isset($event['summary']) ? (string) $event['summary'] : null; - $location = isset($event['location']) ? (string) $event['location'] : null; + $calendarId = null; + if (isset($event['calendarId']) === true) { + $calendarId = (int) $event['calendarId']; + } + + $summary = null; + if (isset($event['summary']) === true) { + $summary = (string) $event['summary']; + } + + $location = null; + if (isset($event['location']) === true) { + $location = (string) $event['location']; + } $link->setCalendarId($calendarId); $link->setEventUid($eventUid); diff --git a/lib/BackgroundJob/BatchNotificationJob.php b/lib/BackgroundJob/BatchNotificationJob.php deleted file mode 100644 index 3e63a38043..0000000000 --- a/lib/BackgroundJob/BatchNotificationJob.php +++ /dev/null @@ -1,162 +0,0 @@ -}` on the rule spec. - * - * Default interval: 5 minutes (300s). Configurable via app-config - * `notification_batch_interval`. Set to 0 to disable (the job runs - * with a 1-year interval as the standard "off" pattern in this app). - * - * The digest queue itself is the `NotificationDigest` primitive - * already in `lib/Service/Notification/`. Other code paths that want - * to enqueue a notification call `NotificationDigest::enqueue()` - * instead of dispatching immediately; this job is the consumer side. - * - * SPDX-License-Identifier: EUPL-1.2 - * SPDX-FileCopyrightText: 2026 Conduction B.V. - * - * @category BackgroundJob - * @package OCA\OpenRegister\BackgroundJob - * - * @author Conduction Development Team - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @link https://www.OpenRegister.app - * - * @spec openspec/changes/notificatie-engine/tasks.md "Notifications MUST support batching and digest delivery" - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\BackgroundJob; - -use OCA\OpenRegister\Service\Notification\NotificationDigest; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; -use OCP\IAppConfig; -use Psr\Log\LoggerInterface; - -/** - * Configurable recurring background job that flushes the digest queue. - */ -class BatchNotificationJob extends TimedJob -{ - - /** - * Default interval — 5 minutes is the sweet spot between batching - * latency (don't make a user wait too long for a digest) and - * per-recipient dispatch cost (one message per N events). - */ - private const DEFAULT_INTERVAL = 300; - - /** - * Constructor. - * - * @param ITimeFactory $time Time factory for parent. - * @param NotificationDigest $digest The shared digest queue. - * @param IAppConfig $appConfig App-config for the interval setting. - * @param LoggerInterface $logger Logger. - */ - public function __construct( - ITimeFactory $time, - private readonly NotificationDigest $digest, - private readonly IAppConfig $appConfig, - private readonly LoggerInterface $logger - ) { - parent::__construct(time: $time); - - $interval = (int) $this->appConfig->getValueString( - app: 'openregister', - key: 'notification_batch_interval', - default: (string) self::DEFAULT_INTERVAL - ); - - if ($interval === 0) { - // Standard "off" pattern: schedule far in the future so the - // cron worker effectively never picks it up. - $this->setInterval(seconds: (86400 * 365)); - return; - } - - $this->setInterval(seconds: $interval); - - }//end __construct() - - /** - * Run the digest flush. - * - * @param mixed $argument Job arguments (unused for recurring jobs). - * - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-1 - */ - protected function run(mixed $argument): void - { - $interval = (int) $this->appConfig->getValueString( - app: 'openregister', - key: 'notification_batch_interval', - default: (string) self::DEFAULT_INTERVAL - ); - if ($interval === 0) { - $this->logger->info( - '[BatchNotificationJob] notification batching is disabled (interval=0); skipping', - ['file' => __FILE__, 'line' => __LINE__] - ); - return; - } - - $totalPending = $this->digest->totalPending(); - if ($totalPending === 0) { - $this->logger->debug( - '[BatchNotificationJob] digest queue is empty; nothing to dispatch', - ['file' => __FILE__, 'line' => __LINE__] - ); - return; - } - - $buckets = $this->digest->flush(); - $this->logger->info( - sprintf( - '[BatchNotificationJob] flushing %d digests across %d recipients (total events: %d)', - count($buckets), - count($buckets), - $totalPending - ), - ['file' => __FILE__, 'line' => __LINE__] - ); - - // Per-recipient dispatch hook lives outside this job to avoid - // a circular dependency on AnnotationNotificationDispatcher. - // Until the dispatcher exposes a public `dispatchDigest()` - // entry, this job simply logs the flush so operators can - // verify the batching path is wired correctly. The dispatch - // wiring is a focused follow-up commit on the same branch. - foreach ($buckets as $recipientId => $events) { - $this->logger->info( - sprintf( - '[BatchNotificationJob] would dispatch digest to recipient=%s with %d events', - $recipientId, - count($events) - ), - ['file' => __FILE__, 'line' => __LINE__] - ); - } - - $this->appConfig->setValueString( - app: 'openregister', - key: 'notification_batch_last_run', - value: date('Y-m-d H:i:s') - ); - - }//end run() -}//end class diff --git a/lib/BackgroundJob/BlobMigrationJob.php b/lib/BackgroundJob/BlobMigrationJob.php index 597e2bbce1..ce3c0225c0 100644 --- a/lib/BackgroundJob/BlobMigrationJob.php +++ b/lib/BackgroundJob/BlobMigrationJob.php @@ -71,7 +71,7 @@ class BlobMigrationJob extends TimedJob * * @param ITimeFactory $time Time factory for parent class * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ public function __construct(ITimeFactory $time) { @@ -94,7 +94,7 @@ public function __construct(ITimeFactory $time) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ protected function run($argument): void { @@ -281,7 +281,7 @@ function (array $row) { * * @return bool True if the table exists * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ private function blobTableExists(IDBConnection $db): bool { @@ -313,7 +313,7 @@ private function blobTableExists(IDBConnection $db): bool * * @return array> Raw rows from the blob table * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ private function fetchBlobObjects(IDBConnection $db): array { @@ -336,7 +336,7 @@ private function fetchBlobObjects(IDBConnection $db): array * * @return int Number of remaining rows * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ private function countBlobRows(IDBConnection $db): int { @@ -361,7 +361,7 @@ private function countBlobRows(IDBConnection $db): int * * @return array>> Grouped rows keyed by "registerId_schemaId" * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ private function groupByRegisterSchema(array $objects, LoggerInterface $logger): array { @@ -401,7 +401,7 @@ private function groupByRegisterSchema(array $objects, LoggerInterface $logger): * * @return array Object array for saving to magic table * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ private function blobRowToObjectArray(array $row): array { @@ -458,7 +458,7 @@ private function blobRowToObjectArray(array $row): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ private function deleteBlobRows(IDBConnection $db, array $rows): void { diff --git a/lib/BackgroundJob/BulkLegalHoldJob.php b/lib/BackgroundJob/BulkLegalHoldJob.php index cdf9bbff14..96390deb04 100644 --- a/lib/BackgroundJob/BulkLegalHoldJob.php +++ b/lib/BackgroundJob/BulkLegalHoldJob.php @@ -43,7 +43,7 @@ class BulkLegalHoldJob extends QueuedJob * * @param ITimeFactory $time Time factory * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-7 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function __construct(ITimeFactory $time) { @@ -59,7 +59,7 @@ public function __construct(ITimeFactory $time) * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-7 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/CacheWarmupJob.php b/lib/BackgroundJob/CacheWarmupJob.php index c40e7a3fa4..29fa8a02bb 100644 --- a/lib/BackgroundJob/CacheWarmupJob.php +++ b/lib/BackgroundJob/CacheWarmupJob.php @@ -21,7 +21,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-30 + * @spec openspec/specs/object-lifecycle/spec.md */ declare(strict_types=1); @@ -106,7 +106,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-4 + * @spec openspec/specs/faceting-configuration/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/ChunkVectorizationJob.php b/lib/BackgroundJob/ChunkVectorizationJob.php new file mode 100644 index 0000000000..5a73e66708 --- /dev/null +++ b/lib/BackgroundJob/ChunkVectorizationJob.php @@ -0,0 +1,356 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use DateTime; +use Exception; +use OCA\OpenRegister\Db\Chunk; +use OCA\OpenRegister\Db\ChunkMapper; +use OCA\OpenRegister\Service\Vectorization\Handlers\VectorStorageHandler; +use OCA\OpenRegister\Service\Vectorization\VectorEmbeddings; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; + +/** + * Vectorizes unvectorized chunks and warms up the pgvector column in batches + * + * Runs every 5 minutes (mirroring BlobMigrationJob). Each run: + * 1. Fetches up to BATCH_SIZE chunks where `vectorized = false` + * (ChunkMapper::findUnvectorized(), FIFO), generates embeddings via the + * existing generateBatchEmbeddings() path, stores them via + * VectorStorageHandler::storeVector() (which dual-writes the pgvector ANN + * sidecar), and marks each successfully-processed chunk `vectorized = true`. + * A single chunk's embedding failure is logged and does NOT abort the batch; + * the failed chunk stays `vectorized = false` for retry on the next run. + * 2. Converts up to BACKFILL_BATCH_SIZE existing BLOB-only vector rows to + * pgvector ANN-sidecar rows (PostgreSQL only, dimension-matched, idempotent) + * via VectorStorageHandler::backfillEmbeddingVectors(), tracking a cursor in + * appconfig so persistently-failing rows can't stall the sweep. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ChunkVectorizationJob extends TimedJob +{ + /** + * Interval: 5 minutes (matches BlobMigrationJob::INTERVAL) + */ + private const INTERVAL = 5 * 60; + + /** + * Maximum chunks to embed per run (bounded provider calls) + */ + private const BATCH_SIZE = 50; + + /** + * Maximum BLOB rows to convert to pgvector per run (cheap, no provider calls) + */ + private const BACKFILL_BATCH_SIZE = 100; + + /** + * App config key prefix + */ + private const CONFIG_PREFIX = 'chunk_vectorization_'; + + /** + * Constructor + * + * @param ITimeFactory $time Time factory for parent class + * + * @spec openspec/changes/hybrid-document-search/tasks.md#5.2 + */ + public function __construct(ITimeFactory $time) + { + parent::__construct(time: $time); + $this->setInterval(seconds: self::INTERVAL); + }//end __construct() + + /** + * Execute the chunk vectorization job + * + * @param mixed $argument Job arguments (unused for recurring jobs) + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/hybrid-document-search/tasks.md#5.2 + */ + protected function run($argument): void + { + $startTime = microtime(true); + + $logger = \OC::$server->get(LoggerInterface::class); + $appConfig = \OC::$server->get(IAppConfig::class); + $chunkMapper = \OC::$server->get(ChunkMapper::class); + $embeddings = \OC::$server->get(VectorEmbeddings::class); + $storageHandler = \OC::$server->get(VectorStorageHandler::class); + + $logger->debug( + message: '[ChunkVectorizationJob] Starting chunk vectorization batch', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + + $vectorized = 0; + $failed = 0; + + try { + [$vectorized, $failed] = $this->vectorizeChunks( + chunkMapper: $chunkMapper, + embeddings: $embeddings, + storageHandler: $storageHandler, + logger: $logger + ); + } catch (\Throwable $e) { + $logger->warning( + message: '[ChunkVectorizationJob] Chunk vectorization batch failed ' + .'(embedding provider unavailable or misconfigured?)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + } + + // The pgvector warm-up backfill (job-only warm-up, DECIDED 2026-07-06). + $backfill = ['converted' => 0, 'failed' => 0, 'remaining' => 0]; + try { + $backfill = $this->runWarmupBackfill( + storageHandler: $storageHandler, + appConfig: $appConfig + ); + } catch (\Throwable $e) { + $logger->error( + message: '[ChunkVectorizationJob] pgvector warm-up backfill failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + } + + $appConfig->setValueString( + app: 'openregister', + key: self::CONFIG_PREFIX.'last_run', + value: date('c') + ); + $appConfig->setValueString( + app: 'openregister', + key: self::CONFIG_PREFIX.'backfill_remaining', + value: (string) $backfill['remaining'] + ); + + $executionTime = microtime(true) - $startTime; + + $logger->info( + message: '[ChunkVectorizationJob] Batch completed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'vectorized' => $vectorized, + 'failed' => $failed, + 'backfill_converted' => $backfill['converted'], + 'backfill_failed' => $backfill['failed'], + 'backfill_remaining' => $backfill['remaining'], + 'execution_time_seconds' => round($executionTime, 2), + ] + ); + }//end run() + + /** + * Vectorize a batch of unvectorized chunks. + * + * A single chunk's embedding or storage failure is logged and skipped — + * the rest of the batch continues, and the failed chunk stays + * `vectorized = false` for retry on the next scheduled run. + * + * @param ChunkMapper $chunkMapper Chunk mapper + * @param VectorEmbeddings $embeddings Embedding coordinator + * @param VectorStorageHandler $storageHandler Vector storage handler + * @param LoggerInterface $logger Logger + * + * @return array{0: int, 1: int} [vectorized count, failed count] + * + * @throws Exception When the batch-embedding call itself fails (e.g. no provider configured) + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @spec openspec/changes/hybrid-document-search/tasks.md#5.2 + */ + private function vectorizeChunks( + ChunkMapper $chunkMapper, + VectorEmbeddings $embeddings, + VectorStorageHandler $storageHandler, + LoggerInterface $logger + ): array { + $chunks = $chunkMapper->findUnvectorized(limit: self::BATCH_SIZE); + + if ($chunks === []) { + return [0, 0]; + } + + $texts = array_map( + static fn(Chunk $chunk): string => (string) $chunk->getTextContent(), + $chunks + ); + + // The batch-embedding call tolerates per-text failures (null embedding + // entries) but throws when the provider itself is unavailable. + $embeddingResults = $embeddings->generateBatchEmbeddings($texts); + + $vectorized = 0; + $failed = 0; + $totalChunksCache = []; + + foreach ($chunks as $index => $chunk) { + $embeddingData = $embeddingResults[$index] ?? null; + + if ($embeddingData === null || ($embeddingData['embedding'] ?? null) === null) { + $failed++; + $logger->warning( + message: '[ChunkVectorizationJob] Embedding failed for chunk, will retry next run', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'chunk_id' => $chunk->getId(), + 'error' => $embeddingData['error'] ?? 'No embedding returned', + ] + ); + continue; + } + + try { + $sourceKey = $chunk->getSourceType().'_'.$chunk->getSourceId(); + if (isset($totalChunksCache[$sourceKey]) === false) { + $totalChunksCache[$sourceKey] = count( + $chunkMapper->findBySource( + sourceType: (string) $chunk->getSourceType(), + sourceId: (int) $chunk->getSourceId() + ) + ); + } + + $storageHandler->storeVector( + entityType: (string) $chunk->getSourceType(), + entityId: (string) $chunk->getSourceId(), + embedding: $embeddingData['embedding'], + model: (string) $embeddingData['model'], + dimensions: (int) $embeddingData['dimensions'], + chunkIndex: (int) $chunk->getChunkIndex(), + totalChunks: max(1, $totalChunksCache[$sourceKey]), + chunkText: substr((string) $chunk->getTextContent(), 0, 500), + metadata: [ + 'source_id' => $chunk->getSourceId(), + 'start_offset' => $chunk->getStartOffset(), + 'end_offset' => $chunk->getEndOffset(), + ] + ); + + $chunk->setVectorized(true); + $chunk->setUpdatedAt(new DateTime()); + $chunkMapper->update($chunk); + $vectorized++; + } catch (Exception $e) { + $failed++; + $logger->warning( + message: '[ChunkVectorizationJob] Failed to store vector for chunk, will retry next run', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'chunk_id' => $chunk->getId(), + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end foreach + + return [$vectorized, $failed]; + }//end vectorizeChunks() + + /** + * Run one pgvector warm-up backfill batch with cursor tracking. + * + * The cursor (appconfig) lets consecutive runs make progress past rows that + * persistently fail to convert; when a sweep completes (no rows processed + * or nothing remaining) the cursor resets to 0 so future NULL rows — e.g. + * after an embedding-dimension change — are swept again. + * + * @param VectorStorageHandler $storageHandler Vector storage handler + * @param IAppConfig $appConfig App config (cursor storage) + * + * @return array{converted: int, failed: int, last_id: int, remaining: int} + * + * @spec openspec/changes/hybrid-document-search/tasks.md#2.2 + */ + private function runWarmupBackfill( + VectorStorageHandler $storageHandler, + IAppConfig $appConfig + ): array { + $cursor = (int) $appConfig->getValueString( + app: 'openregister', + key: self::CONFIG_PREFIX.'backfill_cursor', + default: '0' + ); + + $result = $storageHandler->backfillEmbeddingVectors( + batchSize: self::BACKFILL_BATCH_SIZE, + afterId: $cursor + ); + + $newCursor = (int) $result['last_id']; + if ($newCursor <= $cursor || (int) $result['remaining'] === 0) { + // Sweep finished (or made no progress): restart from the beginning + // next run so earlier rows and future NULL rows are re-checked. + $newCursor = 0; + } + + $appConfig->setValueString( + app: 'openregister', + key: self::CONFIG_PREFIX.'backfill_cursor', + value: (string) $newCursor + ); + + return $result; + }//end runWarmupBackfill() +}//end class diff --git a/lib/BackgroundJob/CronFileTextExtractionJob.php b/lib/BackgroundJob/CronFileTextExtractionJob.php index 4ed07f72f0..49ec90cb24 100644 --- a/lib/BackgroundJob/CronFileTextExtractionJob.php +++ b/lib/BackgroundJob/CronFileTextExtractionJob.php @@ -66,7 +66,7 @@ class CronFileTextExtractionJob extends TimedJob * * @param ITimeFactory $time Time factory for parent class * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct(ITimeFactory $time) { @@ -84,7 +84,7 @@ public function __construct(ITimeFactory $time) * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function run($argument): void { @@ -268,7 +268,7 @@ protected function run($argument): void * * @return array> List of pending files with metadata. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ private function getPendingFiles( FileMapper $fileMapper, diff --git a/lib/BackgroundJob/DestructionCheckJob.php b/lib/BackgroundJob/DestructionCheckJob.php index 089c79f815..9e78b3e654 100644 --- a/lib/BackgroundJob/DestructionCheckJob.php +++ b/lib/BackgroundJob/DestructionCheckJob.php @@ -20,9 +20,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-63 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-4 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/retention-management/spec.md#requirement-the-system-must-send-pre-destruction-notifications + * @spec openspec/specs/archival-destruction-workflow/spec.md */ declare(strict_types=1); @@ -68,7 +68,7 @@ class DestructionCheckJob extends TimedJob * @param ITimeFactory $time Time factory for parent class * @param IDBConnection $db Database connection * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function __construct( ITimeFactory $time, @@ -97,9 +97,9 @@ public function __construct( * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-24-archival-destruction-workflow/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-4 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ protected function run($argument): void { @@ -178,7 +178,7 @@ protected function run($argument): void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-63 + * @spec openspec/specs/retention-management/spec.md#requirement-the-system-must-send-pre-destruction-notifications */ private function sendPreDestructionNotifications( array $settings, @@ -190,69 +190,94 @@ private function sendPreDestructionNotifications( try { $objectMapper = \OC::$server->get(MagicMapper::class); - $qb = $this->db->getQueryBuilder(); - - $qb->select('id')->from('openregister_objects') - ->where($qb->expr()->isNotNull('retention')); - - $result = $qb->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); $appConfig = \OC::$server->get(\OCP\IAppConfig::class); $notifiedJson = $appConfig->getValueString('openregister', self::NOTIFIED_KEY, '[]'); $notified = json_decode($notifiedJson, true) ?? []; - $newCount = 0; - - foreach ($rows as $row) { - try { - $object = $objectMapper->find(intval($row['id']), null, null, false, false, false); - } catch (Exception $e) { - continue; - } - - $retention = $object->getRetention() ?? []; - $status = $retention['archiefstatus'] ?? null; - $actieDate = $retention['archiefactiedatum'] ?? null; - $nominatie = $retention['archiefnominatie'] ?? null; - - if ($actieDate === null || $status !== 'nog_te_archiveren') { - continue; - } - - if ($actieDate <= $today || $actieDate > $threshold) { - continue; - } - - $uuid = $object->getUuid(); - if (in_array($uuid, $notified, true) === true) { - continue; - } - - if (($retention['legalHold']['active'] ?? false) === true) { - continue; - } - - $subject = 'Object approaching destruction date'; - if ($nominatie === 'bewaren') { - $subject = 'Object requires e-Depot transfer'; - } - - $this->sendObjectNotification( - uuid: $uuid, - subject: $subject, - title: $object->getTitle() ?? $uuid, - actieDate: $actieDate, - classificatie: $retention['classificatie'] ?? null, - logger: $logger - ); - - $notified[] = $uuid; - $newCount++; - }//end foreach - - if ($newCount > 0) { - $appConfig->setValueString('openregister', self::NOTIFIED_KEY, json_encode($notified)); + // Track which already-notified UUIDs are still inside the pre-destruction window + // this run; anything not re-seen has passed its date (or moved out) and is pruned + // below so the appconfig blob stays bounded over time (OPS-7). + $stillRelevant = []; + $newNotified = []; + $newCount = 0; + + // Page the retention scan instead of fetchAll()-ing every retention row into + // memory at once (OPS-7). batchSize bounds peak memory on large registers. + $batchSize = 500; + $offset = 0; + + do { + $qb = $this->db->getQueryBuilder(); + $qb->select('id')->from('openregister_objects') + ->where($qb->expr()->isNotNull('retention')) + ->setFirstResult($offset) + ->setMaxResults($batchSize); + + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + foreach ($rows as $row) { + try { + $object = $objectMapper->find(intval($row['id']), null, null, false, false, false); + } catch (Exception $e) { + continue; + } + + $retention = $object->getRetention() ?? []; + $status = $retention['archiefstatus'] ?? null; + $actieDate = $retention['archiefactiedatum'] ?? null; + $nominatie = $retention['archiefnominatie'] ?? null; + + if ($actieDate === null || $status !== 'nog_te_archiveren') { + continue; + } + + if ($actieDate <= $today || $actieDate > $threshold) { + continue; + } + + $uuid = $object->getUuid(); + + // Object is still inside the notification window — keep it in the + // notified set even if we already alerted on it earlier. + if (in_array($uuid, $notified, true) === true) { + $stillRelevant[$uuid] = true; + continue; + } + + if (($retention['legalHold']['active'] ?? false) === true) { + continue; + } + + $subject = 'Object approaching destruction date'; + if ($nominatie === 'bewaren') { + $subject = 'Object requires e-Depot transfer'; + } + + $this->sendObjectNotification( + uuid: $uuid, + subject: $subject, + title: $object->getTitle() ?? $uuid, + actieDate: $actieDate, + classificatie: $retention['classificatie'] ?? null, + logger: $logger + ); + + $newNotified[$uuid] = true; + $newCount++; + }//end foreach + + $rowCount = count($rows); + $offset += $batchSize; + } while ($rowCount === $batchSize); + + // Rebuild the persisted set from only the still-in-window UUIDs plus the freshly + // notified ones, dropping any whose destruction date has passed or moved away. + $rebuilt = array_values(array_keys($stillRelevant + $newNotified)); + + if ($newCount > 0 || count($rebuilt) !== count($notified)) { + $appConfig->setValueString('openregister', self::NOTIFIED_KEY, json_encode($rebuilt)); $logger->info('[DestructionCheckJob] Sent '.$newCount.' pre-destruction notifications'); } } catch (Exception $e) { @@ -272,8 +297,8 @@ classificatie: $retention['classificatie'] ?? null, * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-24-archival-destruction-workflow/tasks.md#task-1 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function sendObjectNotification( string $uuid, @@ -323,7 +348,7 @@ private function sendObjectNotification( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function sendReviewNotification( string $listUuid, diff --git a/lib/BackgroundJob/DestructionExecutionJob.php b/lib/BackgroundJob/DestructionExecutionJob.php index 5c5370957c..83f7a2fbc4 100644 --- a/lib/BackgroundJob/DestructionExecutionJob.php +++ b/lib/BackgroundJob/DestructionExecutionJob.php @@ -21,9 +21,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-5 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ declare(strict_types=1); @@ -80,9 +80,9 @@ public function __construct(ITimeFactory $time) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-05-24-b3a-workflow-seed/tasks.md#task-2 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ protected function run($argument): void { @@ -121,6 +121,16 @@ protected function run($argument): void return; } + // Re-entrancy guard (harden-retention-destruction): claim the list by + // transitioning approved -> executing and persisting it BEFORE the + // batch loop. A concurrent or retried run then loads status + // 'executing', fails the approved-check above, and exits — preventing + // the same objects being deleted twice and duplicate audit rows. + $listData['status'] = 'executing'; + $listData['executingAt'] = (new DateTime())->format('c'); + $listObject->setObject($listData); + $objectMapper->update($listObject); + $objects = $listData['objects'] ?? []; $destroyedCount = 0; $skippedHolds = 0; @@ -255,7 +265,7 @@ protected function run($argument): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function notifySkippedHolds( string $listUuid, diff --git a/lib/BackgroundJob/DsarDpiaDetectionJob.php b/lib/BackgroundJob/DsarDpiaDetectionJob.php new file mode 100644 index 0000000000..cf9d99923f --- /dev/null +++ b/lib/BackgroundJob/DsarDpiaDetectionJob.php @@ -0,0 +1,346 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use DateTimeImmutable; +use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Service\Gdpr\Case\CaseObjectAccessor; +use OCA\OpenRegister\Service\Gdpr\DpiaPatternDetectionService; +use OCA\OpenRegister\Service\Gdpr\Policy\DsarPolicyPackResolver; +use OCA\OpenRegister\Service\ObjectService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; + +/** + * Daily DPIA pattern-detection sweep over DSAR cases. + * + * @psalm-suppress UnusedClass Instantiated by the NC background-job framework (appinfo/info.xml). + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) The job wires the case + * store, the pack resolver, the pure detection service, the audit mapper, + * and the object write path — each a distinct shipped collaborator. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Requirement: DPIA pattern detection over DSAR cases) + */ +class DsarDpiaDetectionJob extends TimedJob +{ + + /** + * Default interval — once per 24 hours (pipelinq's detection cadence). + * + * @var int + */ + private const RUN_INTERVAL_SECONDS = 86400; + + /** + * App-config key for the enable/disable toggle. + * + * @var string + */ + private const CONFIG_KEY_ENABLED = 'dsar_dpia_detection_enabled'; + + /** + * App identifier for app-config lookups. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * Audit action recorded on every detection flag. + * + * @var string + */ + public const AUDIT_ACTION = 'dpia.detected'; + + /** + * Constructor. + * + * @param ITimeFactory $time Time factory required by parent. + * @param IAppConfig $appConfig App-config reader. + * @param ObjectService $objectService Object read/write path. + * @param DsarPolicyPackResolver $packResolver Active-pack resolution per jurisdiction. + * @param DpiaPatternDetectionService $detectionService Pure grouping/threshold engine. + * @param AuditTrailMapper $auditTrailMapper Immutable detection audit rows. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + ITimeFactory $time, + private readonly IAppConfig $appConfig, + private readonly ObjectService $objectService, + private readonly DsarPolicyPackResolver $packResolver, + private readonly DpiaPatternDetectionService $detectionService, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + $this->setInterval(seconds: self::RUN_INTERVAL_SECONDS); + + }//end __construct() + + /** + * Drive the DPIA detection sweep. + * + * @param mixed $argument Job arguments (unused for recurring jobs). + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Scenario: Threshold crossing flags the group) + */ + protected function run($argument): void + { + $enabled = filter_var( + $this->appConfig->getValueString( + app: self::APP_ID, + key: self::CONFIG_KEY_ENABLED, + default: 'true' + ), + FILTER_VALIDATE_BOOLEAN + ); + + if ($enabled === false) { + $this->logger->info( + message: '[DsarDpiaDetectionJob] detection disabled (dsar_dpia_detection_enabled=false), skipping', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return; + } + + try { + $summary = $this->detect(); + if ($summary['flagged'] > 0 || $summary['errors'] > 0) { + $this->logger->info( + message: '[DsarDpiaDetectionJob] detection complete', + context: array_merge(['file' => __FILE__, 'line' => __LINE__], $summary) + ); + } + } catch (\Throwable $e) { + $this->logger->error( + message: '[DsarDpiaDetectionJob] detection failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + } + + }//end run() + + /** + * Run one detection pass: partition cases by jurisdiction, detect per + * active pack, flag unflagged members of triggering groups. + * + * @return array{cases: int, partitions: int, groupsTriggered: int, flagged: int, errors: int} Detection summary. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Scenario: Re-runs are idempotent) + */ + private function detect(): array + { + $summary = [ + 'cases' => 0, + 'partitions' => 0, + 'groupsTriggered' => 0, + 'flagged' => 0, + 'errors' => 0, + ]; + + $now = new DateTimeImmutable(); + $cases = $this->loadCases(); + + $summary['cases'] = count($cases); + + // Partition by jurisdiction: each partition resolves its own active + // pack (jurisdiction is data — grouping across packs with different + // thresholds would be ambiguous). + $partitions = []; + foreach ($cases as $case) { + $jurisdiction = (string) ($case['jurisdiction'] ?? ''); + $partitions[$jurisdiction][] = $case; + } + + foreach ($partitions as $partitionCases) { + $summary['partitions']++; + + $pack = $this->packResolver->activePackForCase(case: $partitionCases[0], systemContext: true); + $config = null; + if ($pack !== null) { + $config = ($pack['dpiaDetection'] ?? null); + } + + if (is_array($config) === false || $config === []) { + // Fail-safe: no pack / no dpiaDetection block ⇒ no flagging. + continue; + } + + $groups = $this->detectionService->detect(cases: $partitionCases, config: $config, now: $now); + foreach ($groups as $group) { + $summary['groupsTriggered']++; + $this->flagGroup(group: $group, config: $config, summary: $summary); + } + }//end foreach + + return $summary; + + }//end detect() + + /** + * Flag every unflagged case of a triggering group (audited write). + * + * @param array{key: string, count: int, caseUuids: list, unflaggedUuids: list} $group The triggering group. + * @param array $config The pack's `dpiaDetection` block. + * @param array $summary Running summary counters (by reference). + * + * @return void + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Scenario: Threshold crossing flags the group) + */ + private function flagGroup(array $group, array $config, array &$summary): void + { + foreach ($group['unflaggedUuids'] as $uuid) { + try { + $entity = $this->objectService->find( + id: $uuid, + register: CaseObjectAccessor::REGISTER_SLUG, + schema: CaseObjectAccessor::SCHEMA_SLUG, + _rbac: false, + _multitenancy: false + ); + if ($entity === null) { + continue; + } + + $data = ($entity->getObject() ?? []); + if (($data['dpiaRequired'] ?? false) === true) { + // Raced with a manual flag — idempotency: never re-write. + continue; + } + + $data['dpiaRequired'] = true; + + $saved = $this->objectService->saveObject( + object: $data, + register: CaseObjectAccessor::REGISTER_SLUG, + schema: CaseObjectAccessor::SCHEMA_SLUG, + uuid: $uuid, + _rbac: false, + _multitenancy: false + ); + + $this->auditTrailMapper->createAuditTrailEntry( + object: $saved, + action: self::AUDIT_ACTION, + context: [ + 'rule' => 'dpia-pattern-detection', + 'groupKey' => $group['key'], + 'windowDays' => (int) ($config['windowDays'] ?? 0), + 'threshold' => (int) ($config['threshold'] ?? 0), + 'count' => $group['count'], + ] + ); + + $summary['flagged']++; + } catch (\Throwable $e) { + $summary['errors']++; + $this->logger->warning( + message: '[DsarDpiaDetectionJob] flagging failed for case '.$uuid.': '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + }//end try + }//end foreach + + }//end flagGroup() + + /** + * Load every DSAR case payload (system scope), with the object uuid + * injected as `@uuid` for the pure detection service. + * + * @return array> The case payloads. + */ + private function loadCases(): array + { + try { + $rows = $this->objectService->findAll( + config: [ + 'filters' => [ + 'register' => CaseObjectAccessor::REGISTER_SLUG, + 'schema' => CaseObjectAccessor::SCHEMA_SLUG, + ], + ], + _rbac: false, + _multitenancy: false + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[DsarDpiaDetectionJob] case enumeration failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return []; + } + + $cases = []; + foreach ($rows as $row) { + if (is_array($row) === false) { + continue; + } + + $uuid = (string) ($row['@self']['uuid'] ?? ($row['@self']['id'] ?? ($row['id'] ?? ''))); + if ($uuid === '') { + continue; + } + + $row['@uuid'] = $uuid; + $cases[] = $row; + } + + return $cases; + + }//end loadCases() +}//end class diff --git a/lib/BackgroundJob/DsarRetentionSweepJob.php b/lib/BackgroundJob/DsarRetentionSweepJob.php new file mode 100644 index 0000000000..29c809a43a --- /dev/null +++ b/lib/BackgroundJob/DsarRetentionSweepJob.php @@ -0,0 +1,160 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Service\Gdpr\Retention\RetentionSweepService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; + +/** + * Daily DSAR-case retention sweep. + */ +class DsarRetentionSweepJob extends TimedJob +{ + + /** + * Default interval — once per 24 hours. + * + * @var int + */ + private const RUN_INTERVAL_SECONDS = 86400; + + /** + * App-config key for the enable/disable toggle. + * + * @var string + */ + private const CONFIG_KEY_ENABLED = 'dsar_retention_sweep_enabled'; + + /** + * App-config key for the dry-run toggle. + * + * @var string + */ + private const CONFIG_KEY_DRY_RUN = 'dsar_retention_sweep_dry_run'; + + /** + * App identifier for app-config lookups. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * Constructor. + * + * @param ITimeFactory $time Time factory required by parent. + * @param IAppConfig $appConfig App-config reader. + * @param RetentionSweepService $sweepService Domain sweep service. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + ITimeFactory $time, + private readonly IAppConfig $appConfig, + private readonly RetentionSweepService $sweepService, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + $this->setInterval(seconds: self::RUN_INTERVAL_SECONDS); + + }//end __construct() + + /** + * Drive the retention sweep. + * + * @param mixed $argument Job arguments (unused for recurring jobs). + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-retention-sweep/spec.md + */ + protected function run($argument): void + { + $enabled = filter_var( + $this->appConfig->getValueString( + app: self::APP_ID, + key: self::CONFIG_KEY_ENABLED, + default: 'true' + ), + FILTER_VALIDATE_BOOLEAN + ); + + if ($enabled === false) { + $this->logger->info( + message: '[DsarRetentionSweepJob] sweep disabled (dsar_retention_sweep_enabled=false), skipping', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return; + } + + $dryRun = filter_var( + $this->appConfig->getValueString( + app: self::APP_ID, + key: self::CONFIG_KEY_DRY_RUN, + default: 'false' + ), + FILTER_VALIDATE_BOOLEAN + ); + + try { + $summary = $this->sweepService->runSweep(dryRun: $dryRun); + $this->logger->info( + message: '[DsarRetentionSweepJob] sweep complete', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'dryRun' => $dryRun, + 'evaluated' => $summary['evaluated'], + 'purgedCount' => count($summary['purged']), + 'skippedHeld' => count($summary['skippedHeld']), + 'withinWindow' => $summary['withinWindow'], + ] + ); + } catch (\Throwable $e) { + $this->logger->error( + message: '[DsarRetentionSweepJob] sweep failed: '.$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + }//end try + + }//end run() +}//end class diff --git a/lib/BackgroundJob/ExecutionHistoryCleanupJob.php b/lib/BackgroundJob/ExecutionHistoryCleanupJob.php index d3135a5541..d4e38fe995 100644 --- a/lib/BackgroundJob/ExecutionHistoryCleanupJob.php +++ b/lib/BackgroundJob/ExecutionHistoryCleanupJob.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-32 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ declare(strict_types=1); @@ -76,7 +76,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-6 + * @spec openspec/specs/workflow-operations/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/FileTextExtractionJob.php b/lib/BackgroundJob/FileTextExtractionJob.php index 5eb84b1702..7cdc934165 100644 --- a/lib/BackgroundJob/FileTextExtractionJob.php +++ b/lib/BackgroundJob/FileTextExtractionJob.php @@ -108,7 +108,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-10 + * @spec openspec/specs/search-index/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/HookRetryJob.php b/lib/BackgroundJob/HookRetryJob.php index afdcc623bc..1462e2f095 100644 --- a/lib/BackgroundJob/HookRetryJob.php +++ b/lib/BackgroundJob/HookRetryJob.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-72 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-retry-via-background-job */ declare(strict_types=1); @@ -89,7 +89,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-72 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-retry-via-background-job */ protected function run($argument): void { diff --git a/lib/BackgroundJob/NameCacheWarmupJob.php b/lib/BackgroundJob/NameCacheWarmupJob.php index e6bf04ffa5..a3ee498ce7 100644 --- a/lib/BackgroundJob/NameCacheWarmupJob.php +++ b/lib/BackgroundJob/NameCacheWarmupJob.php @@ -21,7 +21,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-30 + * @spec openspec/specs/object-lifecycle/spec.md */ declare(strict_types=1); @@ -29,6 +29,7 @@ namespace OCA\OpenRegister\BackgroundJob; use OCA\OpenRegister\Service\Object\CacheHandler; +use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\TimedJob; use OCP\AppFramework\Utility\ITimeFactory; use Psr\Log\LoggerInterface; @@ -65,6 +66,8 @@ public function __construct(ITimeFactory $time) { parent::__construct(time: $time); $this->setInterval(seconds: self::DEFAULT_INTERVAL); + // Warmup is not time-critical: let NC defer it to a low-load window (OPS-13). + $this->setTimeSensitivity(sensitivity: IJob::TIME_INSENSITIVE); }//end __construct() /** @@ -76,7 +79,7 @@ public function __construct(ITimeFactory $time) * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-5 + * @spec openspec/specs/faceting-configuration/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/NotificationQueueFlushJob.php b/lib/BackgroundJob/NotificationQueueFlushJob.php new file mode 100644 index 0000000000..11111b281e --- /dev/null +++ b/lib/BackgroundJob/NotificationQueueFlushJob.php @@ -0,0 +1,239 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/notificatie-engine/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use DateTimeImmutable; +use OCA\OpenRegister\Db\QueuedNotification; +use OCA\OpenRegister\Db\QueuedNotificationMapper; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\Notification\AnnotationNotificationDispatcher; +use OCA\OpenRegister\Service\Notification\DigestScheduleEvaluator; +use OCA\OpenRegister\Service\Notification\NotificationDeliveryWindowService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; + +/** + * Periodic queue flush for the delivery-window / digest-schedule gate. + * + * @psalm-suppress UnusedClass + */ +final class NotificationQueueFlushJob extends TimedJob +{ + /** + * Wire collaborators and configure the timed-job interval. + * + * @param ITimeFactory $time Nextcloud time factory. + * @param QueuedNotificationMapper $queuedMapper Durable queue mapper. + * @param SchemaMapper $schemaMapper Schema lookup mapper (rule digest config). + * @param AnnotationNotificationDispatcher $dispatcher Notification dispatcher (flush entry point). + * @param NotificationDeliveryWindowService $windowService Delivery-window resolver + evaluator. + * @param DigestScheduleEvaluator $digestEvaluator Live digest-schedule evaluator. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + ITimeFactory $time, + private readonly QueuedNotificationMapper $queuedMapper, + private readonly SchemaMapper $schemaMapper, + private readonly AnnotationNotificationDispatcher $dispatcher, + private readonly NotificationDeliveryWindowService $windowService, + private readonly DigestScheduleEvaluator $digestEvaluator, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + $this->setInterval(seconds: 60); + + }//end __construct() + + /** + * Scan every queued row, group by (schema_id, rule_key, recipient), + * and flush the groups/rows whose holding condition has cleared. + * + * @param mixed $argument Background-job argument (unused). + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function run($argument): void + { + // `$this->time` (protected on the OCP `Job` base class) rather than + // `new DateTimeImmutable('now')` so tests can inject a controllable + // clock — required to exercise the DST-transition re-evaluation + // scenario deterministically. + $now = DateTimeImmutable::createFromInterface($this->time->getDateTime()); + + try { + $rows = $this->queuedMapper->findAll(); + } catch (\Throwable $e) { + $this->logger->warning('[NotificationQueueFlushJob] queue scan failed: '.$e->getMessage()); + return; + } + + if (count($rows) === 0) { + return; + } + + $groups = []; + foreach ($rows as $row) { + $key = $row->getSchemaId().'|'.$row->getRuleKey().'|'.$row->getRecipient(); + $groups[$key][] = $row; + } + + $flushed = 0; + foreach ($groups as $groupRows) { + $flushed += $this->processGroup(rows: $groupRows, now: $now); + } + + if ($flushed > 0) { + $this->logger->info(sprintf('[NotificationQueueFlushJob] flushed %d queued notification(s)', $flushed)); + } + + }//end run() + + /** + * Evaluate and flush one `(schema_id, rule_key, recipient)` group. + * + * @param array $rows Rows in the group. + * @param DateTimeImmutable $now Logical "now" for this scan pass. + * + * @return int Number of rows flushed. + */ + private function processGroup(array $rows, DateTimeImmutable $now): int + { + $first = $rows[0]; + $recipient = (string) $first->getRecipient(); + + // Window-active blocks the WHOLE group, regardless of digest state + // (window-overlap semantics: delivery waits for the later of + // quiet-hours-end and digest-due-time). + $window = $this->windowService->getForUser(userId: $recipient); + if ($window !== null && $this->windowService->isInsideWindow(window: $window, now: $now) === true) { + return 0; + } + + $digest = $this->resolveDigestSpec(schemaId: (int) $first->getSchemaId(), ruleKey: (string) $first->getRuleKey()); + + $dueRows = []; + foreach ($rows as $row) { + if ($digest === null) { + // No digest schedule declared (or it was removed from the + // schema since the row was queued) — the window has + // cleared, so the row is due immediately. + $dueRows[] = $row; + continue; + } + + $enqueuedAt = DateTimeImmutable::createFromInterface($row->getCreatedAt()); + if ($this->digestEvaluator->isDue(digest: $digest, enqueuedAt: $enqueuedAt, now: $now) === true) { + $dueRows[] = $row; + } + } + + if (count($dueRows) === 0) { + return 0; + } + + try { + $this->dispatcher->dispatchQueued(queuedRows: $dueRows); + } catch (\Throwable $e) { + $this->logger->warning( + sprintf( + '[NotificationQueueFlushJob] dispatchQueued failed for rule="%s" recipient="%s": %s', + $first->getRuleKey(), + $recipient, + $e->getMessage() + ) + ); + return 0; + } + + foreach ($dueRows as $row) { + $this->queuedMapper->deleteById(id: (int) $row->getId()); + } + + return count($dueRows); + + }//end processGroup() + + /** + * Resolve the rule's `digest` block from the owning schema, or null + * when the schema/rule/digest is missing or malformed. + * + * @param int $schemaId Owning schema id. + * @param string $ruleKey Notification annotation key. + * + * @return array|null + */ + private function resolveDigestSpec(int $schemaId, string $ruleKey): ?array + { + try { + $schema = $this->schemaMapper->find($schemaId, _multitenancy: false); + } catch (\Throwable $e) { + return null; + } + + $config = ($schema->getConfiguration() ?? []); + $notifications = ($config['x-openregister-notifications'] ?? null); + if (is_array($notifications) === false) { + return null; + } + + $spec = ($notifications[$ruleKey] ?? null); + if (is_array($spec) === false) { + return null; + } + + $digest = ($spec['digest'] ?? null); + if (is_array($digest) === false || $this->digestEvaluator->isValidDigestSpec($digest) === false) { + return null; + } + + return $digest; + + }//end resolveDigestSpec() +}//end class diff --git a/lib/BackgroundJob/ObjectTextExtractionJob.php b/lib/BackgroundJob/ObjectTextExtractionJob.php index 5fb6b43c9f..8a38d7e803 100644 --- a/lib/BackgroundJob/ObjectTextExtractionJob.php +++ b/lib/BackgroundJob/ObjectTextExtractionJob.php @@ -80,7 +80,7 @@ class ObjectTextExtractionJob extends QueuedJob * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( ITimeFactory $time, @@ -104,7 +104,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/RealtimeEventRetentionJob.php b/lib/BackgroundJob/RealtimeEventRetentionJob.php deleted file mode 100644 index be9186066d..0000000000 --- a/lib/BackgroundJob/RealtimeEventRetentionJob.php +++ /dev/null @@ -1,147 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\BackgroundJob; - -use OCA\OpenRegister\Db\RealtimeEventMapper; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; -use OCP\IAppConfig; -use Psr\Log\LoggerInterface; - -/** - * Daily prune of stale realtime events. - */ -class RealtimeEventRetentionJob extends TimedJob -{ - - /** - * Default retention window: 7 days, expressed in seconds. - * - * @var int - */ - private const DEFAULT_RETENTION_SECONDS = (7 * 86400); - - /** - * Job interval: 24 hours. The retention window is enforced per - * tick — running more often than daily would be noise. - * - * @var int - */ - private const RUN_INTERVAL_SECONDS = 86400; - - /** - * App-config key for the retention window override. - * - * @var string - */ - private const CONFIG_KEY_RETENTION = 'realtime_event_retention_seconds'; - - /** - * App identifier for app-config lookups. - * - * @var string - */ - private const APP_ID = 'openregister'; - - /** - * Constructor. - * - * @param ITimeFactory $time Time factory required by - * the parent `TimedJob`. - * @param IAppConfig $appConfig App-config reader for the - * retention-window override. - * @param RealtimeEventMapper $eventMapper Mapper exposing - * `deleteOlderThan()`. - * @param LoggerInterface $logger Logger for telemetry. - * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-2 - */ - public function __construct( - ITimeFactory $time, - private readonly IAppConfig $appConfig, - private readonly RealtimeEventMapper $eventMapper, - private readonly LoggerInterface $logger - ) { - parent::__construct(time: $time); - $this->setInterval(seconds: self::RUN_INTERVAL_SECONDS); - - }//end __construct() - - /** - * Execute the prune. - * - * @param mixed $argument Job arguments (unused for recurring jobs). - * - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-2 - */ - protected function run($argument): void - { - $retention = (int) $this->appConfig->getValueString( - app: self::APP_ID, - key: self::CONFIG_KEY_RETENTION, - default: (string) self::DEFAULT_RETENTION_SECONDS - ); - - if ($retention <= 0) { - $this->logger->info( - message: '[RealtimeEventRetentionJob] Retention disabled (value <= 0), skipping prune', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return; - } - - try { - $deleted = $this->eventMapper->deleteOlderThan(retentionSeconds: $retention); - $this->logger->info( - message: '[RealtimeEventRetentionJob] Pruned realtime event log', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'retentionSeconds' => $retention, - 'deletedRows' => $deleted, - ] - ); - } catch (\Throwable $e) { - $this->logger->error( - message: '[RealtimeEventRetentionJob] Prune failed: '.$e->getMessage(), - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - }//end try - - }//end run() -}//end class diff --git a/lib/BackgroundJob/ReportRenderJob.php b/lib/BackgroundJob/ReportRenderJob.php index d6022c838e..b2d5e6b763 100644 --- a/lib/BackgroundJob/ReportRenderJob.php +++ b/lib/BackgroundJob/ReportRenderJob.php @@ -21,9 +21,9 @@ * @category BackgroundJob * @package OCA\OpenRegister\BackgroundJob * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -113,7 +113,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-8 + * @spec openspec/specs/rapportage-bi-export/spec.md */ protected function run($argument): void { diff --git a/lib/BackgroundJob/ScheduleReconcilerJob.php b/lib/BackgroundJob/ScheduleReconcilerJob.php new file mode 100644 index 0000000000..84843eb326 --- /dev/null +++ b/lib/BackgroundJob/ScheduleReconcilerJob.php @@ -0,0 +1,87 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\AppHost\Scheduling\ScheduleReconciler; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * TimedJob that reconciles manifest schedules into OpenConnector jobs. + * + * @psalm-suppress UnusedClass + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ScheduleReconcilerJob extends TimedJob +{ + /** + * Constructor for ScheduleReconcilerJob. + * + * @param ITimeFactory $time Time factory. + * @param ScheduleReconciler $reconciler The AppHost scheduling engine. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + ITimeFactory $time, + private readonly ScheduleReconciler $reconciler, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + // Run every 60 seconds; the reconcile is a cheap set-diff (execution is + // OpenConnector's separate JobTask). + $this->setInterval(seconds: 60); + }//end __construct() + + /** + * Execute one reconciliation sweep. + * + * @param mixed $argument Job argument (unused for TimedJob). + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/apphost-manifest-schedules/specs/apphost-scheduling/spec.md + */ + protected function run($argument): void + { + try { + $this->reconciler->reconcile(); + } catch (Throwable $e) { + // Defence in depth — reconcile() already never throws. + $this->logger->error( + message: '[ScheduleReconcilerJob] Reconcile sweep failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + } + }//end run() +}//end class diff --git a/lib/BackgroundJob/ScheduledNotificationJob.php b/lib/BackgroundJob/ScheduledNotificationJob.php index 8a438fc847..5efb9319a9 100644 --- a/lib/BackgroundJob/ScheduledNotificationJob.php +++ b/lib/BackgroundJob/ScheduledNotificationJob.php @@ -33,12 +33,18 @@ namespace OCA\OpenRegister\BackgroundJob; use OCA\OpenRegister\Db\MagicMapper; +use OCA\OpenRegister\Db\NotificationDedupeStateMapper; use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; +use DateTime; +use DateTimeImmutable; +use DateTimeZone; use OCA\OpenRegister\Service\Notification\AnnotationNotificationDispatcher; +use OCA\OpenRegister\Service\Notification\ScheduledFilterEvaluator; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; +use OCP\IAppConfig; use OCP\ICache; use OCP\ICacheFactory; use Psr\Log\LoggerInterface; @@ -51,6 +57,18 @@ final class ScheduledNotificationJob extends TimedJob { + /** + * Hard upper bound on the number of objects scanned per (schema, notification) + * fire. Acts as a memory/time guard so a single huge schema cannot OOM or stall + * the cron run (OPS-6 / PERF-3). When the cap is hit a warning is logged. + * + * TODO(PERF-3): push the trigger `filter` into SQL (a paged findBySchema with + * _filter+_limit in lib/Db/MagicMapper) and add a per-schema watermark for + * delta scans, so we no longer load the whole table into PHP and filter + * in-memory. Until then this cap bounds the blast radius. + */ + private const MAX_OBJECTS_PER_FIRE = 5000; + /** * Distributed cache holding last-fire timestamps per (schema, notification). * @@ -61,12 +79,15 @@ final class ScheduledNotificationJob extends TimedJob /** * Wire collaborators and configure the timed-job interval. * - * @param ITimeFactory $time Nextcloud time factory. - * @param SchemaMapper $schemaMapper Schema lookup mapper. - * @param MagicMapper $objectMapper Magic object mapper. - * @param AnnotationNotificationDispatcher $dispatcher Notification dispatcher. - * @param LoggerInterface $logger PSR logger. - * @param ICacheFactory $cacheFactory Distributed cache factory. + * @param ITimeFactory $time Nextcloud time factory. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param MagicMapper $objectMapper Magic object mapper. + * @param AnnotationNotificationDispatcher $dispatcher Notification dispatcher. + * @param LoggerInterface $logger PSR logger. + * @param ICacheFactory $cacheFactory Distributed cache factory. + * @param ScheduledFilterEvaluator $filterEvaluator Operator-aware filter evaluator. + * @param NotificationDedupeStateMapper $dedupeMapper Per-object dedup state mapper. + * @param IAppConfig $appConfig App config for tunable retention window. * * @return void */ @@ -76,7 +97,10 @@ public function __construct( private readonly MagicMapper $objectMapper, private readonly AnnotationNotificationDispatcher $dispatcher, private readonly LoggerInterface $logger, - ICacheFactory $cacheFactory + ICacheFactory $cacheFactory, + private readonly ScheduledFilterEvaluator $filterEvaluator, + private readonly NotificationDedupeStateMapper $dedupeMapper, + private readonly IAppConfig $appConfig ) { parent::__construct(time: $time); $this->setInterval(seconds: 60); @@ -97,11 +121,14 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-2 + * @spec openspec/specs/notificatie-engine/spec.md */ protected function run($argument): void { $now = time(); + // One logical "now" per scan pass so every entry sees the same window + // (Phase 1 — filter operator evaluator). + $nowDt = (new DateTimeImmutable('@'.$now))->setTimezone(new DateTimeZone('UTC')); try { $schemas = $this->schemaMapper->findAll(); @@ -117,19 +144,55 @@ protected function run($argument): void continue; } - $this->processSchema(schema: $schema, now: $now); + $this->processSchema(schema: $schema, now: $now, nowDt: $nowDt); } + + // Retention sweep — once per scan pass, best-effort. Drop dedup rows + // last seen before the configured cutoff so that an object that no + // longer matches any rule (purged / archived / annotation removed) + // does not pile up state forever. + $this->runRetentionSweep(nowDt: $nowDt); }//end run() + /** + * Drop dedup rows whose `seen_at` is older than the configured retention. + * + * @param DateTimeImmutable $nowDt Logical "now" for this scan pass. + * + * @return void + */ + private function runRetentionSweep(DateTimeImmutable $nowDt): void + { + try { + $days = (int) $this->appConfig->getValueString( + 'openregister', + 'notification_dedupe_retention_days', + (string) NotificationDedupeStateMapper::DEFAULT_RETENTION_DAYS + ); + } catch (\Throwable $e) { + $days = NotificationDedupeStateMapper::DEFAULT_RETENTION_DAYS; + } + + if ($days <= 0) { + return; + } + + $cutoff = DateTime::createFromImmutable($nowDt); + $cutoff->modify(sprintf('-%d days', $days)); + + $this->dedupeMapper->deleteSeenBefore(cutoff: $cutoff); + }//end runRetentionSweep() + /** * Inspect one schema's notification specs and fire those that are due. * - * @param Schema $schema Schema being inspected. - * @param int $now Current epoch second. + * @param Schema $schema Schema being inspected. + * @param int $now Current epoch second. + * @param DateTimeImmutable $nowDt Logical "now" for relative-date filters in this scan pass. * * @return void */ - private function processSchema(Schema $schema, int $now): void + private function processSchema(Schema $schema, int $now, DateTimeImmutable $nowDt): void { $config = ($schema->getConfiguration() ?? []); $notifications = ($config['x-openregister-notifications'] ?? null); @@ -162,7 +225,7 @@ private function processSchema(Schema $schema, int $now): void continue; } - $this->fire(schema: $schema, notificationName: (string) $name, trigger: $trigger); + $this->fire(schema: $schema, notificationName: (string) $name, trigger: $trigger, nowDt: $nowDt); // Mark as fired regardless of per-object errors; the dispatcher // already swallows + logs its own failures. @@ -243,10 +306,11 @@ private function stateKey(int $schemaId, string $notificationName): string * @param Schema $schema Schema whose objects to scan. * @param string $notificationName Notification key in the schema config. * @param array $trigger Trigger configuration including filters. + * @param DateTimeImmutable $nowDt Logical "now" used for relative-date operators. * * @return void */ - private function fire(Schema $schema, string $notificationName, array $trigger): void + private function fire(Schema $schema, string $notificationName, array $trigger, DateTimeImmutable $nowDt): void { try { $filter = (array) ($trigger['filter'] ?? []); @@ -263,13 +327,113 @@ private function fire(Schema $schema, string $notificationName, array $trigger): return; } - $matched = 0; + // Bound the in-memory scan so a pathologically large schema cannot OOM or + // stall the cron run (OPS-6 / PERF-3). Excess objects are processed via a + // ROTATING window keyed by a persisted per-(schema, notification) offset + // cursor: each run handles the next MAX_OBJECTS_PER_FIRE slice and advances + // the cursor (wrapping at the end), so every object is eventually swept — + // fixing the previous behaviour where array_slice(0, MAX) always processed + // the same first N and objects beyond N never fired. + if (count($objects) > self::MAX_OBJECTS_PER_FIRE) { + $total = count($objects); + $offsetKey = 'sched_offset:'.((int) $schema->getId()).':'.$notificationName; + + $offset = 0; + try { + $offset = (int) $this->appConfig->getValueString('openregister', $offsetKey, '0'); + } catch (\Throwable $e) { + $offset = 0; + } + + if ($offset < 0 || $offset >= $total) { + $offset = 0; + } + + $this->logger->warning( + sprintf( + '[ScheduledNotificationJob] schema %d / "%s" returned %d objects; ' + .'processing rotating window [%d, %d) this run (PERF-3 SQL filter pushdown pending)', + $schema->getId(), + $notificationName, + $total, + $offset, + min($offset + self::MAX_OBJECTS_PER_FIRE, $total) + ) + ); + + $objects = array_slice($objects, $offset, self::MAX_OBJECTS_PER_FIRE); + + // Advance the cursor for the next run; wrap once the schema is swept. + $nextOffset = ($offset + self::MAX_OBJECTS_PER_FIRE); + if ($nextOffset >= $total) { + $nextOffset = 0; + } + + try { + $this->appConfig->setValueString('openregister', $offsetKey, (string) $nextOffset); + } catch (\Throwable $e) { + // Non-fatal: worst case the window does not advance this run. + $this->logger->warning( + sprintf( + '[ScheduledNotificationJob] failed to persist rotation offset for schema %d / "%s": %s', + $schema->getId(), + $notificationName, + $e->getMessage() + ) + ); + } + }//end if + + $watchedFields = $this->resolveWatchedFields(trigger: $trigger); + $schemaId = (int) $schema->getId(); + $now = DateTime::createFromImmutable($nowDt); + + $matched = 0; + $dispatched = 0; + $deduplicated = 0; foreach ($objects as $object) { if (($object instanceof ObjectEntity) === false) { continue; } - if ($this->matchesFilter(object: $object, filter: $filter) === false) { + $objectData = (array) ($object->getObject() ?? []); + if ($this->filterEvaluator->matches(objectData: $objectData, filter: $filter, now: $nowDt) === false) { + continue; + } + + $matched++; + + $objectUuid = (string) $object->getUuid(); + if ($objectUuid === '') { + continue; + } + + $fingerprint = $this->computeFingerprint(objectData: $objectData, watchedFields: $watchedFields); + $existing = $this->dedupeMapper->findOne( + schemaId: $schemaId, + ruleKey: $notificationName, + objectUuid: $objectUuid + ); + + $shouldDispatch = ($existing === null + || (string) $existing->getFingerprint() !== $fingerprint); + + if ($shouldDispatch === false) { + // Fingerprint unchanged: touch seen_at, skip dispatch. + try { + $this->dedupeMapper->upsert( + schemaId: $schemaId, + ruleKey: $notificationName, + objectUuid: $objectUuid, + fingerprint: $fingerprint, + now: $now, + dispatched: false + ); + } catch (\Throwable $e) { + // Non-fatal — sweep will reclaim eventually. + } + + $deduplicated++; continue; } @@ -279,53 +443,130 @@ private function fire(Schema $schema, string $notificationName, array $trigger): 'scheduled', ['notificationName' => $notificationName] ); - $matched++; + + $this->dedupeMapper->upsert( + schemaId: $schemaId, + ruleKey: $notificationName, + objectUuid: $objectUuid, + fingerprint: $fingerprint, + now: $now, + dispatched: true + ); + + $dispatched++; } catch (\Throwable $e) { $this->logger->warning( sprintf( '[ScheduledNotificationJob] dispatch failed for object %s: %s', - (string) $object->getUuid(), + $objectUuid, $e->getMessage() ) ); - } + }//end try }//end foreach $this->logger->info( sprintf( - '[ScheduledNotificationJob] fired "%s" on schema %d: %d/%d objects', + '[ScheduledNotificationJob] fired "%s" on schema %d: matched=%d dispatched=%d deduped=%d of %d', $notificationName, $schema->getId(), $matched, + $dispatched, + $deduplicated, count($objects) ) ); }//end fire() /** - * Simple equality match against object data fields. - * For v1 we only support flat `{ field: value }` filters; richer - * shapes (operators, nested paths) are a v1.1 extension. + * Resolve the ordered list of object fields to fingerprint for dedup. + * + * Precedence: + * 1. Explicit `trigger.dedupeFields` (array of strings) — used verbatim. + * 2. Otherwise the set of `field` keys in `trigger.filter` whose value is + * an operator object using a date operator (`withinNext`, `olderThan`) + * — i.e. the values whose change should re-arm the notification. + * 3. Otherwise empty — produces a constant fingerprint so a triggered + * rule fires exactly once per object until pruned (fire-once). * - * @param ObjectEntity $object Object instance whose data is being inspected. - * @param array $filter Flat key/value equality filter. + * @param array $trigger Trigger configuration block. * - * @return bool True when every filter entry matches, false otherwise. + * @return array Sorted, distinct field names. */ - private function matchesFilter(ObjectEntity $object, array $filter): bool + private function resolveWatchedFields(array $trigger): array { - if (count($filter) === 0) { - return true; + $explicit = ($trigger['dedupeFields'] ?? null); + if (is_array($explicit) === true) { + $fields = []; + foreach ($explicit as $field) { + if (is_string($field) === true && $field !== '') { + $fields[] = $field; + } + } + + if ($fields !== []) { + $fields = array_values(array_unique($fields)); + sort($fields); + return $fields; + } } - $data = (array) ($object->getObject() ?? []); - foreach ($filter as $key => $expected) { - $actual = ($data[$key] ?? null); - if ($actual !== $expected) { - return false; + $filter = (array) ($trigger['filter'] ?? []); + $fields = []; + foreach ($filter as $field => $spec) { + if (is_string($field) === false || $field === '') { + continue; } + + if (is_array($spec) === false) { + continue; + } + + $operator = (string) ($spec['operator'] ?? ''); + if (in_array($operator, ['withinNext', 'olderThan'], true) === true) { + $fields[] = $field; + } + } + + if ($fields === []) { + return []; + } + + $fields = array_values(array_unique($fields)); + sort($fields); + return $fields; + }//end resolveWatchedFields() + + /** + * SHA-1 fingerprint of the watched field values on this object. + * + * Empty watched-field list yields a stable constant fingerprint so a + * triggered rule fires exactly once per object until state is pruned. + * Missing fields are encoded as `null` so adding a value re-arms the + * rule. + * + * @param array $objectData Decoded object payload. + * @param array $watchedFields Sorted field names. + * + * @return string Hex SHA-1 string. + */ + private function computeFingerprint(array $objectData, array $watchedFields): string + { + if ($watchedFields === []) { + return sha1('constant'); + } + + $payload = []; + foreach ($watchedFields as $field) { + $payload[$field] = ($objectData[$field] ?? null); + } + + $encoded = json_encode($payload, (JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + if ($encoded === false) { + // JSON encode failure (resource etc.) — fall back to var_export. + $encoded = var_export($payload, true); } - return true; - }//end matchesFilter() + return sha1($encoded); + }//end computeFingerprint() }//end class diff --git a/lib/BackgroundJob/ScheduledReportJob.php b/lib/BackgroundJob/ScheduledReportJob.php new file mode 100644 index 0000000000..2d1c7b2d51 --- /dev/null +++ b/lib/BackgroundJob/ScheduledReportJob.php @@ -0,0 +1,146 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use DateTime; +use OCA\OpenRegister\Db\ScheduledReportMapper; +use OCA\OpenRegister\Service\ScheduledReportService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; + +/** + * Hourly scheduled-report deliverer. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ +class ScheduledReportJob extends TimedJob +{ + + /** + * Hourly cadence — runs once every hour, so due reports are caught + * within an hour of their period elapsing (and any downtime is caught + * up on the next tick — see ScheduledReportService::isDue()). + * + * @var int + */ + private const RUN_INTERVAL_SECONDS = 3600; + + /** + * Constructor. + * + * @param ITimeFactory $time Time factory. + * @param ScheduledReportMapper $mapper Scheduled report mapper. + * @param ScheduledReportService $service Due-check + execution logic. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + ITimeFactory $time, + private readonly ScheduledReportMapper $mapper, + private readonly ScheduledReportService $service, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + $this->setInterval(seconds: self::RUN_INTERVAL_SECONDS); + }//end __construct() + + /** + * Drive the scheduled report deliveries. + * + * @param mixed $argument Job arguments (unused). + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + protected function run($argument): void + { + $candidates = []; + try { + $candidates = $this->mapper->findEnabled(); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ScheduledReportJob] Failed to enumerate enabled scheduled reports', + context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] + ); + return; + } + + $now = new DateTime(); + $ran = 0; + $skip = 0; + foreach ($candidates as $report) { + if ($this->service->isDue(report: $report, now: $now) === false) { + $skip++; + continue; + } + + try { + // Per-report isolation: ScheduledReportService::runOne() already + // catches everything internally and never throws, but this + // try/catch is defence in depth so an unexpected error in a + // future refactor still can't abort the batch (mirrors + // ReportRenderJob::run()'s per-dashboard loop). + $this->service->runOne(report: $report); + $ran++; + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ScheduledReportJob] Run failed for scheduled report', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'reportId' => $report->getId(), + 'error' => $e->getMessage(), + ] + ); + } + }//end foreach + + $this->logger->info( + message: '[ScheduledReportJob] Scheduled-report pass complete', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'candidates' => count($candidates), + 'ran' => $ran, + 'skipped' => $skip, + ] + ); + }//end run() +}//end class diff --git a/lib/BackgroundJob/ScheduledReportRunNowJob.php b/lib/BackgroundJob/ScheduledReportRunNowJob.php new file mode 100644 index 0000000000..e4c070c67c --- /dev/null +++ b/lib/BackgroundJob/ScheduledReportRunNowJob.php @@ -0,0 +1,100 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Db\ScheduledReportMapper; +use OCA\OpenRegister\Service\ScheduledReportService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use Psr\Log\LoggerInterface; + +/** + * One-shot immediate execution of a single scheduled report. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ +class ScheduledReportRunNowJob extends QueuedJob +{ + /** + * Constructor. + * + * @param ITimeFactory $time Time factory. + * @param ScheduledReportMapper $mapper Scheduled report mapper. + * @param ScheduledReportService $service Execution logic. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + ITimeFactory $time, + private readonly ScheduledReportMapper $mapper, + private readonly ScheduledReportService $service, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + }//end __construct() + + /** + * Run the identified scheduled report immediately. + * + * @param mixed $argument Job argument: `['scheduledReportId' => int]`. + * + * @return void + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + protected function run($argument): void + { + $rawReportId = 0; + if (is_array($argument) === true) { + $rawReportId = ($argument['scheduledReportId'] ?? 0); + } + + $reportId = (int) $rawReportId; + if ($reportId <= 0) { + $this->logger->warning( + message: '[ScheduledReportRunNowJob] Missing/invalid scheduledReportId argument', + context: ['file' => __FILE__, 'line' => __LINE__, 'argument' => $argument] + ); + return; + } + + try { + $report = $this->mapper->find(id: $reportId); + } catch (DoesNotExistException $e) { + $this->logger->warning( + message: '[ScheduledReportRunNowJob] Scheduled report no longer exists', + context: ['file' => __FILE__, 'line' => __LINE__, 'reportId' => $reportId] + ); + return; + } + + $this->service->runOne(report: $report); + }//end run() +}//end class diff --git a/lib/BackgroundJob/ScheduledWorkflowJob.php b/lib/BackgroundJob/ScheduledWorkflowJob.php index e30d4a9836..b9554afa75 100644 --- a/lib/BackgroundJob/ScheduledWorkflowJob.php +++ b/lib/BackgroundJob/ScheduledWorkflowJob.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-83 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-workflow-execution-api-sync-and-async */ declare(strict_types=1); @@ -79,7 +79,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-7 + * @spec openspec/specs/workflow-operations/spec.md */ protected function run($argument): void { @@ -209,7 +209,7 @@ private function evaluateSchedule(ScheduledWorkflow $schedule): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-83 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-workflow-execution-api-sync-and-async */ private function handleError(ScheduledWorkflow $schedule, $startTime, string $error): void { diff --git a/lib/BackgroundJob/SchemaRunJob.php b/lib/BackgroundJob/SchemaRunJob.php new file mode 100644 index 0000000000..858ebe0468 --- /dev/null +++ b/lib/BackgroundJob/SchemaRunJob.php @@ -0,0 +1,138 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Db\SchemaRun; +use OCA\OpenRegister\Db\SchemaRunMapper; +use OCA\OpenRegister\Service\Schema\SchemaMigrationService; +use OCA\OpenRegister\Service\Schema\SchemaRevalidationService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\QueuedJob; +use Psr\Log\LoggerInterface; + +/** + * Background executor for schema runs. + */ +class SchemaRunJob extends QueuedJob +{ + /** + * Constructor. + * + * @param ITimeFactory $time Time factory. + * @param SchemaRunMapper $runMapper Run persistence. + * @param SchemaRevalidationService $revalidationService Revalidation engine. + * @param SchemaMigrationService $migrationService Migration engine. + * @param IJobList $jobList Job list for re-enqueue. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + ITimeFactory $time, + private readonly SchemaRunMapper $runMapper, + private readonly SchemaRevalidationService $revalidationService, + private readonly SchemaMigrationService $migrationService, + private readonly IJobList $jobList, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + + }//end __construct() + + /** + * Run one batch of a schema run. + * + * @param array $argument Job arguments: run_id (required), + * batch_size (optional). + * + * @return void + * + * @spec openspec/specs/schema-migration/spec.md + */ + protected function run($argument): void + { + $runId = ($argument['run_id'] ?? null); + if ($runId === null) { + $this->logger->error('[SchemaRunJob] Missing run_id argument', ['argument' => $argument]); + return; + } + + $batchSize = (int) ($argument['batch_size'] ?? 100); + + try { + $run = $this->runMapper->find((int) $runId); + } catch (\Throwable $e) { + $this->logger->error('[SchemaRunJob] Run not found', ['run_id' => $runId, 'error' => $e->getMessage()]); + return; + } + + if ($run->getState() !== SchemaRun::STATE_RUNNING) { + // Nothing to do for a terminal/non-running run. + return; + } + + try { + $more = $this->advance(run: $run, batchSize: $batchSize); + } catch (\Throwable $e) { + $this->logger->error( + '[SchemaRunJob] Batch processing failed', + ['run_id' => $runId, 'error' => $e->getMessage()] + ); + $run->setState(SchemaRun::STATE_FAILED); + $report = ($run->getReport() ?? []); + $report['fatal'] = $e->getMessage(); + $run->setReport($report); + $this->runMapper->save($run); + return; + } + + if ($more === true) { + $this->jobList->add(self::class, ['run_id' => $runId, 'batch_size' => $batchSize]); + } + + }//end run() + + /** + * Advance the run by one batch, dispatching by run type. + * + * @param SchemaRun $run The run. + * @param int $batchSize The batch size. + * + * @return bool True when more work remains. + */ + private function advance(SchemaRun $run, int $batchSize): bool + { + if ($run->getType() === SchemaRun::TYPE_MIGRATION) { + return $this->migrationService->processBatch($run, $batchSize); + } + + return $this->revalidationService->processBatch($run, $batchSize); + + }//end advance() +}//end class diff --git a/lib/BackgroundJob/SolrNightlyWarmupJob.php b/lib/BackgroundJob/SolrNightlyWarmupJob.php deleted file mode 100644 index d98099ae3a..0000000000 --- a/lib/BackgroundJob/SolrNightlyWarmupJob.php +++ /dev/null @@ -1,437 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.nl - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\BackgroundJob; - -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Db\SchemaMapper; -use OCP\BackgroundJob\TimedJob; -use OCP\ILogger; -use OCP\AppFramework\Utility\ITimeFactory; -use Psr\Log\LoggerInterface; - -/** - * Recurring nightly background job for SOLR index warmup - * - * This job runs automatically every night at 00:00 to ensure the SOLR index - * is warm and optimized for search performance. It performs comprehensive - * index warmup including schema mirroring and cache warming. - * - * Features: - * - Runs daily at 00:00 (configurable) - * - Comprehensive SOLR index warmup - * - Performance optimizations and cache warming - * - Detailed logging and monitoring - * - Configurable via OpenRegister settings - * - Automatic error handling and recovery - */ -class SolrNightlyWarmupJob extends TimedJob -{ - /** - * Default interval: 24 hours (daily) - */ - private const DEFAULT_INTERVAL = 24 * 60 * 60; - - /** - * Default maximum objects for nightly warmup - */ - private const DEFAULT_NIGHTLY_MAX_OBJECTS = 10000; - - /** - * Default warmup mode for nightly runs. - */ - private const DEFAULT_NIGHTLY_MODE = 'parallel'; - - /** - * Constructor - * - * Initializes the timed job with the time factory and sets the interval. - * - * @param ITimeFactory $time Time factory for parent class - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - public function __construct(ITimeFactory $time) - { - parent::__construct(time: $time); - $this->setInterval(seconds: self::DEFAULT_INTERVAL); - }//end __construct() - - /** - * Execute the nightly SOLR warmup job - * - * @param mixed $argument Job arguments (unused for recurring jobs) - * - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - protected function run($argument): void - { - $startTime = microtime(true); - - /* - * @var LoggerInterface $logger - */ - - $logger = \OC::$server->get(LoggerInterface::class); - - $logger->info( - message: '[SolrNightlyWarmupJob] 🌙 SOLR Nightly Warmup Job Started', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'scheduled_time' => date('Y-m-d H:i:s'), - 'timezone' => date_default_timezone_get(), - ] - ); - - try { - /* - * Get required services. - * - * @var IndexService $solrService - */ - - $solrService = \OC::$server->get(IndexService::class); - - /* - * @var SettingsService $settingsService - */ - - $settingsService = \OC::$server->get(SettingsService::class); - - /* - * @var SchemaMapper $schemaMapper - */ - - $schemaMapper = \OC::$server->get(SchemaMapper::class); - - // Check if SOLR is enabled and available. - $isSolrAvailable = $this->isSolrEnabledAndAvailable( - solrService: $solrService, - settingsService: $settingsService, - logger: $logger - ); - if ($isSolrAvailable === false) { - // phpcs:ignore Generic.Files.LineLength.MaxExceeded - $logger->info(message: '[SolrNightlyWarmupJob] SOLR Nightly Warmup Job skipped - SOLR not enabled or available', context: ['file' => __FILE__, 'line' => __LINE__]); - return; - } - - // Get warmup configuration from settings. - $config = $this->getWarmupConfiguration(_settingsService: $settingsService, _logger: $logger); - - // Get all schemas for comprehensive warmup. - $schemas = $schemaMapper->findAll(); - - $logger->info( - message: '[SolrNightlyWarmupJob] Starting nightly SOLR index warmup', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemas_found' => count($schemas), - 'max_objects' => $config['maxObjects'], - 'mode' => $config['mode'], - 'collect_errors' => $config['collectErrors'], - ] - ); - - // Execute the comprehensive nightly warmup. - $result = $solrService->warmupIndex( - schemas: $schemas, - maxObjects: $config['maxObjects'], - mode: $config['mode'], - collectErrors: $config['collectErrors'] - ); - - $executionTime = microtime(true) - $startTime; - - if ($result['success'] ?? false) { - $logger->info( - message: '[SolrNightlyWarmupJob] ✅ SOLR Nightly Warmup Job Completed Successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'execution_time_seconds' => round($executionTime, 2), - 'objects_indexed' => $result['operations']['objects_indexed'] ?? 0, - 'schemas_processed' => $result['operations']['schemas_processed'] ?? 0, - 'fields_created' => $result['operations']['fields_created'] ?? 0, - 'conflicts_resolved' => $result['operations']['conflicts_resolved'] ?? 0, - 'performance_metrics' => [ - 'total_time_ms' => $result['execution_time_ms'] ?? 0, - 'objects_per_second' => $this->calculateObjectsPerSecond( - result: $result, - executionTime: $executionTime - ), - 'next_run' => date('Y-m-d H:i:s', time() + self::DEFAULT_INTERVAL), - ], - 'operations_summary' => $this->summarizeOperations(operations: $result['operations'] ?? []), - ] - ); - - // Log performance statistics for monitoring. - $this->logPerformanceStats(result: $result, executionTime: $executionTime, logger: $logger); - }//end if - - if (($result['success'] ?? false) === false) { - $logger->error( - message: '[SolrNightlyWarmupJob] ❌ SOLR Nightly Warmup Job Failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'execution_time_seconds' => round($executionTime, 2), - 'error' => $result['error'] ?? 'Unknown error', - 'next_retry' => date('Y-m-d H:i:s', time() + self::DEFAULT_INTERVAL), - ] - ); - }//end if - } catch (\Exception $e) { - $executionTime = microtime(true) - $startTime; - - $logger->error( - message: '[SolrNightlyWarmupJob] 🚨 SOLR Nightly Warmup Job Exception', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'execution_time_seconds' => round($executionTime, 2), - 'exception' => $e->getMessage(), - 'exception_file' => $e->getFile(), - 'exception_line' => $e->getLine(), - 'next_retry' => date('Y-m-d H:i:s', time() + self::DEFAULT_INTERVAL), - 'trace' => $e->getTraceAsString(), - ] - ); - - // Don't re-throw for recurring jobs - let them retry next time. - }//end try - }//end run() - - /** - * Calculate objects per second performance metric - * - * @param array $result Warmup result - * @param float $executionTime Total execution time in seconds - * - * @return float Objects indexed per second - * - * @psalm-suppress UnusedMethod - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function calculateObjectsPerSecond(array $result, float $executionTime): float - { - $objectsIndexed = $result['operations']['objects_indexed'] ?? 0; - - if ($executionTime > 0 && $objectsIndexed > 0) { - return round($objectsIndexed / $executionTime, 2); - } - - return 0.0; - }//end calculateObjectsPerSecond() - - /** - * Count successful warmup queries - * - * @param array $operations Operations array - * - * @return int Number of successful warmup queries - * - * @psalm-suppress UnusedMethod - * - * @psalm-return int<0, max> - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function countSuccessfulWarmupQueries(array $operations): int - { - $count = 0; - - foreach ($operations as $key => $value) { - if (str_starts_with($key, 'warmup_query_') === true && $value === true) { - $count++; - } - } - - return $count; - }//end countSuccessfulWarmupQueries() - - /** - * Calculate warmup efficiency percentage - * - * @param array $result Warmup result - * - * @return float Efficiency percentage - * - * @psalm-suppress UnusedMethod - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function calculateWarmupEfficiency(array $result): float - { - $operations = $result['operations'] ?? []; - $totalOperations = count($operations); - - if ($totalOperations === 0) { - return 0.0; - } - - $successfulOperations = array_sum( - array_map( - function (bool $op): int { - if ($op === true) { - return 1; - } - - return 0; - }, - $operations - ) - ); - - return round(($successfulOperations / $totalOperations) * 100, 1); - }//end calculateWarmupEfficiency() - - /** - * Check if SOLR is enabled and available. - * - * @param IndexService $solrService SOLR service instance - * @param SettingsService $settingsService Settings service instance - * @param LoggerInterface $logger Logger instance - * - * @return bool True if SOLR is enabled and available, false otherwise - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function isSolrEnabledAndAvailable( - IndexService $solrService, - SettingsService $settingsService, - LoggerInterface $logger - ): bool { - // Check if SOLR is enabled in settings. - $solrSettings = $settingsService->getSolrSettings(); - if (($solrSettings['enabled'] ?? false) === false) { - // phpcs:ignore Generic.Files.LineLength.MaxExceeded - $logger->debug(message: '[SolrNightlyWarmupJob] SOLR Nightly Warmup Job skipped - SOLR not enabled in settings', context: ['file' => __FILE__, 'line' => __LINE__]); - return false; - } - - // Check if SOLR service is available. - if ($solrService->isAvailable() === false) { - // phpcs:ignore Generic.Files.LineLength.MaxExceeded - $logger->debug(message: '[SolrNightlyWarmupJob] SOLR Nightly Warmup Job skipped - SOLR service not available', context: ['file' => __FILE__, 'line' => __LINE__]); - return false; - } - - return true; - }//end isSolrEnabledAndAvailable() - - /** - * Get warmup configuration from settings. - * - * @param SettingsService $_settingsService Settings service instance (unused, kept for API compatibility) - * @param LoggerInterface $_logger Logger instance (unused, kept for API compatibility) - * - * @return array Warmup configuration array - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function getWarmupConfiguration( - SettingsService $_settingsService, - LoggerInterface $_logger - ): array { - /* - * @var \OCP\IConfig $config - */ - - $config = \OC::$server->get(\OCP\IConfig::class); - - $defaultMaxObjects = (string) self::DEFAULT_NIGHTLY_MAX_OBJECTS; - $maxObjects = $config->getAppValue('openregister', 'solr_nightly_max_objects', $defaultMaxObjects); - $mode = $config->getAppValue('openregister', 'solr_nightly_mode', self::DEFAULT_NIGHTLY_MODE); - $collectErrors = $config->getAppValue('openregister', 'solr_nightly_collect_errors', 'false') === 'true'; - - return [ - 'maxObjects' => (int) $maxObjects, - 'mode' => $mode, - 'collectErrors' => $collectErrors, - ]; - }//end getWarmupConfiguration() - - /** - * Summarize operations for logging. - * - * @param array $operations Operations array - * - * @return (float|int)[] - * - * @psalm-return array{total: int<0, max>, successful: int<0, max>, efficiency: float} - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function summarizeOperations(array $operations): array - { - return [ - 'total' => count($operations), - 'successful' => $this->countSuccessfulWarmupQueries(operations: $operations), - 'efficiency' => $this->calculateWarmupEfficiency(result: ['operations' => $operations]), - ]; - }//end summarizeOperations() - - /** - * Log performance statistics. - * - * @param array $result Warmup result - * @param float $executionTime Total execution time in seconds - * @param LoggerInterface $logger Logger instance - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function logPerformanceStats(array $result, float $executionTime, LoggerInterface $logger): void - { - $logger->info( - message: '[SolrNightlyWarmupJob] SOLR Nightly Warmup Performance Stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'execution_time_seconds' => round($executionTime, 2), - 'objects_per_second' => $this->calculateObjectsPerSecond(result: $result, executionTime: $executionTime), - 'efficiency_percentage' => $this->calculateWarmupEfficiency(result: $result), - ] - ); - }//end logPerformanceStats() -}//end class diff --git a/lib/BackgroundJob/SolrWarmupJob.php b/lib/BackgroundJob/SolrWarmupJob.php deleted file mode 100644 index 4734a72eb2..0000000000 --- a/lib/BackgroundJob/SolrWarmupJob.php +++ /dev/null @@ -1,294 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * - * @version GIT: - * - * @link https://www.OpenRegister.nl - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-31 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\BackgroundJob; - -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Db\SchemaMapper; -use OCP\BackgroundJob\QueuedJob; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\ILogger; -use Psr\Log\LoggerInterface; - -/** - * One-time background job for SOLR index warmup - * - * This job is automatically scheduled after import operations to warm up - * the SOLR index in the background, ensuring optimal search performance - * without impacting import speed. - * - * Features: - * - Runs once after being queued - * - Configurable warmup parameters via job arguments - * - Comprehensive logging and error handling - * - Performance metrics tracking - * - Automatic cleanup after execution - */ -class SolrWarmupJob extends QueuedJob -{ - /** - * Default maximum objects to index during warmup - * - * Limits the number of objects indexed per warmup to prevent - * excessive resource usage and long execution times. - * - * @var int Maximum objects to index (default: 5000) - */ - private const DEFAULT_MAX_OBJECTS = 5000; - - /** - * Default warmup mode - * - * Serial mode processes objects one at a time, which is safer - * but slower than parallel or hyper modes. - * - * @var string Warmup mode: 'serial', 'parallel', or 'hyper' (default: 'serial') - */ - private const DEFAULT_MODE = 'serial'; - - /** - * Constructor - * - * Initializes the queued job with the time factory. - * - * @param ITimeFactory $time Time factory for parent class - */ - public function __construct(ITimeFactory $time) - { - parent::__construct(time: $time); - }//end __construct() - - /** - * Execute the SOLR warmup job - * - * Runs SOLR index warmup in the background to optimize search performance. - * Processes schemas and indexes objects up to the specified maximum. - * Logs comprehensive metrics and handles errors gracefully. - * - * @param array $argument Job arguments containing warmup parameters: - * - maxObjects: Maximum number of objects to index (default: 5000) - * - mode: Warmup mode - 'serial', 'parallel', or 'hyper' (default: 'serial') - * - collectErrors: Whether to collect detailed errors (default: false) - * - triggeredBy: What triggered this warmup (default: 'unknown') - * - * @return void - * - * @throws \Exception If warmup fails critically (job will be marked as failed) - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-9 - */ - protected function run($argument): void - { - // Record start time for performance metrics. - $startTime = microtime(true); - - // Parse job arguments with defaults. - // These parameters control warmup behavior and resource usage. - $maxObjects = $argument['maxObjects'] ?? self::DEFAULT_MAX_OBJECTS; - $mode = $argument['mode'] ?? self::DEFAULT_MODE; - $collectErrors = $argument['collectErrors'] ?? false; - $triggeredBy = $argument['triggeredBy'] ?? 'unknown'; - - // @var LoggerInterface $logger - $logger = \OC::$server->get(LoggerInterface::class); - - $logger->info( - message: '[SolrWarmupJob] 🔥 SOLR Warmup Job Started', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'max_objects' => $maxObjects, - 'mode' => $mode, - 'triggered_by' => $triggeredBy, - 'collect_errors' => $collectErrors, - ] - ); - - try { - /* - * Get required services. - * - * @var IndexService $solrService - * @var SchemaMapper $schemaMapper - */ - - $solrService = \OC::$server->get(IndexService::class); - $schemaMapper = \OC::$server->get(SchemaMapper::class); - - // Check if SOLR is available before proceeding. - if ($this->isSolrAvailable(solrService: $solrService, logger: $logger) === false) { - $logger->warning( - message: '[SolrWarmupJob] SOLR Warmup Job skipped - SOLR not available', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'triggered_by' => $triggeredBy, - ] - ); - return; - } - - // Get all schemas for comprehensive warmup. - $schemas = $schemaMapper->findAll(); - - $logger->info( - message: '[SolrWarmupJob] Starting SOLR index warmup', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemas_found' => count($schemas), - 'max_objects' => $maxObjects, - 'mode' => $mode, - ] - ); - - // Execute the warmup. - $result = $solrService->warmupIndex( - schemas: $schemas, - maxObjects: $maxObjects, - mode: $mode, - collectErrors: $collectErrors - ); - - $executionTime = microtime(true) - $startTime; - - if (($result['success'] ?? false) === true) { - $logger->info( - message: '[SolrWarmupJob] ✅ SOLR Warmup Job Completed Successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'execution_time_seconds' => round($executionTime, 2), - 'objects_indexed' => $result['operations']['objects_indexed'] ?? 0, - 'schemas_processed' => $result['operations']['schemas_processed'] ?? 0, - 'fields_created' => $result['operations']['fields_created'] ?? 0, - 'triggered_by' => $triggeredBy, - 'performance_metrics' => [ - 'total_time_ms' => $result['execution_time_ms'] ?? 0, - 'objects_per_second' => $this->calculateObjectsPerSecond( - result: $result, - executionTime: $executionTime - ), - ], - ] - ); - }//end if - - if (($result['success'] ?? false) === false) { - $logger->error( - message: '[SolrWarmupJob] ❌ SOLR Warmup Job Failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'execution_time_seconds' => round($executionTime, 2), - 'error' => $result['error'] ?? 'Unknown error', - 'triggered_by' => $triggeredBy, - ] - ); - }//end if - } catch (\Exception $e) { - $executionTime = microtime(true) - $startTime; - - $logger->error( - message: '[SolrWarmupJob] 🚨 SOLR Warmup Job Exception', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'job_id' => $this->getId(), - 'execution_time_seconds' => round($executionTime, 2), - 'exception' => $e->getMessage(), - 'exception_file' => $e->getFile(), - 'exception_line' => $e->getLine(), - 'triggered_by' => $triggeredBy, - 'trace' => $e->getTraceAsString(), - ] - ); - - // Re-throw to mark job as failed. - throw $e; - }//end try - }//end run() - - /** - * Check if SOLR is available - * - * Verifies that SOLR service is configured and accessible before - * attempting warmup operations. Prevents errors from running warmup - * when SOLR is not configured or unavailable. - * - * @param IndexService $solrService SOLR service instance to check - * @param LoggerInterface $logger Logger instance for debug messages - * - * @return bool True if SOLR is available and ready, false otherwise - */ - private function isSolrAvailable(IndexService $solrService, LoggerInterface $logger): bool - { - // Check if SOLR service is available and configured. - // Returns false if SOLR is not configured or connection fails. - if ($solrService->isAvailable() === false) { - // phpcs:ignore Generic.Files.LineLength.MaxExceeded - $logger->debug(message: '[SolrWarmupJob] SOLR Warmup Job skipped - SOLR service not available', context: ['file' => __FILE__, 'line' => __LINE__]); - return false; - } - - // SOLR is available and ready for warmup operations. - return true; - }//end isSolrAvailable() - - /** - * Calculate objects indexed per second - * - * Calculates indexing throughput rate for performance metrics. - * Used to measure warmup efficiency and identify performance bottlenecks. - * - * @param array $result Warmup result containing operations data - * @param float $executionTime Total execution time in seconds - * - * @return float Objects indexed per second (rounded to 2 decimal places), or 0.0 if calculation not possible - */ - private function calculateObjectsPerSecond(array $result, float $executionTime): float - { - // Extract number of objects indexed from result. - $objectsIndexed = $result['operations']['objects_indexed'] ?? 0; - - // Calculate throughput rate: objects indexed / execution time. - // Only calculate if both values are positive to avoid division by zero. - if ($executionTime > 0 && $objectsIndexed > 0) { - return round($objectsIndexed / $executionTime, 2); - } - - // Return 0.0 if calculation not possible (no objects indexed or zero execution time). - return 0.0; - }//end calculateObjectsPerSecond() -}//end class diff --git a/lib/BackgroundJob/TemporalCalculationSweepJob.php b/lib/BackgroundJob/TemporalCalculationSweepJob.php new file mode 100644 index 0000000000..7be3339915 --- /dev/null +++ b/lib/BackgroundJob/TemporalCalculationSweepJob.php @@ -0,0 +1,167 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Service\Calculation\TemporalCalculationSweepService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; + +/** + * Hourly temporal re-evaluation sweep. + * + * @psalm-suppress UnusedClass Instantiated by the NC background-job framework (appinfo/info.xml). + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ +class TemporalCalculationSweepJob extends TimedJob +{ + + /** + * Default interval — hourly (the tier boundaries are day-granular, so + * hourly keeps the worst-case dispatch delay well inside a day). + * + * @var int + */ + private const DEFAULT_INTERVAL_SECONDS = 3600; + + /** + * Minimum permitted operator-configured interval. + * + * @var int + */ + private const MIN_INTERVAL_SECONDS = 300; + + /** + * App-config key for the enable/disable toggle. + * + * @var string + */ + private const CONFIG_KEY_ENABLED = 'temporal_calculation_sweep_enabled'; + + /** + * App-config key for the interval override. + * + * @var string + */ + private const CONFIG_KEY_INTERVAL = 'temporal_calculation_sweep_interval'; + + /** + * App identifier for app-config lookups. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * Constructor. + * + * @param ITimeFactory $time Time factory required by parent. + * @param IAppConfig $appConfig App-config reader. + * @param TemporalCalculationSweepService $sweepService Domain sweep service. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + ITimeFactory $time, + private readonly IAppConfig $appConfig, + private readonly TemporalCalculationSweepService $sweepService, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + + $interval = (int) $appConfig->getValueString( + app: self::APP_ID, + key: self::CONFIG_KEY_INTERVAL, + default: (string) self::DEFAULT_INTERVAL_SECONDS + ); + if ($interval < self::MIN_INTERVAL_SECONDS) { + $interval = self::DEFAULT_INTERVAL_SECONDS; + } + + $this->setInterval(seconds: $interval); + + }//end __construct() + + /** + * Drive the temporal re-evaluation sweep. + * + * @param mixed $argument Job arguments (unused for recurring jobs). + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Scenario: Untouched case crosses the reminder tier) + */ + protected function run($argument): void + { + $enabled = filter_var( + $this->appConfig->getValueString( + app: self::APP_ID, + key: self::CONFIG_KEY_ENABLED, + default: 'true' + ), + FILTER_VALIDATE_BOOLEAN + ); + + if ($enabled === false) { + $this->logger->info( + message: '[TemporalCalculationSweepJob] sweep disabled (temporal_calculation_sweep_enabled=false), skipping', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return; + } + + try { + $summary = $this->sweepService->runSweep(); + if ($summary['objectsRewritten'] > 0 || $summary['errors'] > 0) { + $this->logger->info( + message: '[TemporalCalculationSweepJob] sweep complete', + context: array_merge(['file' => __FILE__, 'line' => __LINE__], $summary) + ); + } + } catch (\Throwable $e) { + $this->logger->error( + message: '[TemporalCalculationSweepJob] sweep failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + } + + }//end run() +}//end class diff --git a/lib/BackgroundJob/TenantDeprovisionJob.php b/lib/BackgroundJob/TenantDeprovisionJob.php index 745346fc0d..42517a0b3a 100644 --- a/lib/BackgroundJob/TenantDeprovisionJob.php +++ b/lib/BackgroundJob/TenantDeprovisionJob.php @@ -18,7 +18,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 + * @spec openspec/specs/tenant-lifecycle/spec.md */ declare(strict_types=1); @@ -68,8 +68,8 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-tenant-lifecycle/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 + * @spec openspec/specs/tenant-lifecycle/spec.md#requirement-deprovisioned-organisations-must-transition-to-archived-with-data-retention + * @spec openspec/specs/tenant-lifecycle/spec.md */ protected function run(mixed $argument): void { diff --git a/lib/BackgroundJob/TenantPurgeJob.php b/lib/BackgroundJob/TenantPurgeJob.php index 48f3d360e6..4eeacf41de 100644 --- a/lib/BackgroundJob/TenantPurgeJob.php +++ b/lib/BackgroundJob/TenantPurgeJob.php @@ -18,7 +18,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 + * @spec openspec/specs/tenant-lifecycle/spec.md */ declare(strict_types=1); @@ -77,8 +77,8 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-tenant-isolation-audit/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 + * @spec openspec/specs/tenant-lifecycle/spec.md#requirement-deprovisioned-organisations-must-transition-to-archived-with-data-retention + * @spec openspec/specs/tenant-lifecycle/spec.md */ protected function run(mixed $argument): void { diff --git a/lib/BackgroundJob/TenantUsageSyncJob.php b/lib/BackgroundJob/TenantUsageSyncJob.php index e8862703e6..19bb5603c2 100644 --- a/lib/BackgroundJob/TenantUsageSyncJob.php +++ b/lib/BackgroundJob/TenantUsageSyncJob.php @@ -18,7 +18,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-80 + * @spec openspec/specs/tenant-quotas/spec.md */ declare(strict_types=1); @@ -72,8 +72,8 @@ public function __construct( * @SuppressWarnings(PHPMD.StaticAccess) * @SuppressWarnings(PHPMD.UndefinedVariable) * - * @spec openspec/changes/retrofit-2026-04-28-tenant-isolation-audit/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-80 + * @spec openspec/specs/tenant-quotas/spec.md#requirement-usage-counters-must-be-persisted-via-background-job + * @spec openspec/specs/tenant-quotas/spec.md */ protected function run(mixed $argument): void { @@ -114,10 +114,16 @@ protected function run(mixed $argument): void $bandwidthKey = "or_bw_{$orgUuid}_{$hourBucket}"; $rawRequest = apcu_fetch($requestKey, $reqSuccess); - $requestCount = ($reqSuccess === true) ? (int) $rawRequest : 0; + $requestCount = 0; + if ($reqSuccess === true) { + $requestCount = (int) $rawRequest; + } $rawBandwidth = apcu_fetch($bandwidthKey, $bwSuccess); - $bandwidthBytes = ($bwSuccess === true) ? (int) $rawBandwidth : 0; + $bandwidthBytes = 0; + if ($bwSuccess === true) { + $bandwidthBytes = (int) $rawBandwidth; + } if ($requestCount === 0 && $bandwidthBytes === 0) { continue; diff --git a/lib/BackgroundJob/TransferExecutionJob.php b/lib/BackgroundJob/TransferExecutionJob.php index f3fe17f25a..950f390953 100644 --- a/lib/BackgroundJob/TransferExecutionJob.php +++ b/lib/BackgroundJob/TransferExecutionJob.php @@ -3,7 +3,17 @@ /** * OpenRegister Transfer Execution Job * - * Queued background job that executes e-Depot transfers for approved transfer lists. + * Queued background job that executes one e-Depot transfer ATTEMPT for an + * approved transfer list and orchestrates long-horizon cross-request retry + * (archival-transfer-hardening, OR-AD-2): it loads the durable transfer + * record, runs a single transport attempt via `EdepotTransferService`, and + * on non-terminal failure re-enqueues itself with `attempt + 1` after an + * exponential backoff (1 m → 8 h cap, ±10 % jitter) using the background-job + * scheduler — never an in-process `sleep()`. After the attempt cap the + * transfer is marked failed and the archivists are notified. + * + * Mirrors OR's shipped `WebhookDeliveryJob` argument convention + * (`{..., attempt}`) and `WebhookRetryJob` durability semantics. * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. @@ -19,8 +29,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-33 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-37 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md */ declare(strict_types=1); @@ -28,113 +37,264 @@ namespace OCA\OpenRegister\BackgroundJob; use OCA\OpenRegister\Service\Edepot\EdepotTransferService; +use OCA\OpenRegister\Service\Edepot\TransferListService; +use OCA\OpenRegister\Service\Edepot\TransferRecordService; use OCA\OpenRegister\Service\Edepot\Transport\OpenConnectorTransport; use OCA\OpenRegister\Service\Edepot\Transport\RestApiTransport; use OCA\OpenRegister\Service\Edepot\Transport\SftpTransport; use OCA\OpenRegister\Service\Edepot\Transport\TransportInterface; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\QueuedJob; use OCP\IAppConfig; use Psr\Log\LoggerInterface; /** - * Queued job for executing e-Depot transfers. - * - * Picks up approved transfer lists and runs the full transfer pipeline - * (SIP build, transport, status update). + * Queued job executing one e-Depot transfer attempt with durable retry. * * @psalm-suppress UnusedClass * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) The job composes the transfer + * service, the durable record store, the three transports, the scheduler, + * and app-config — each a distinct collaborator in the attempt/retry flow. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Requirement: One job run performs one transport attempt per package) */ class TransferExecutionJob extends QueuedJob { + + /** + * Base backoff in seconds (attempt 1 → 60 s). + * + * @var int + */ + private const BASE_BACKOFF_SECONDS = 60; + + /** + * Backoff cap in seconds (8 hours). + * + * @var int + */ + private const MAX_BACKOFF_SECONDS = 28800; + + /** + * Default attempt cap (≈ 2 days of coverage) when unset in app-config. + * + * @var int + */ + private const DEFAULT_MAX_ATTEMPTS = 10; + + /** + * App-config key for the attempt cap. + * + * @var string + */ + private const CONFIG_KEY_MAX_ATTEMPTS = 'edepot_transfer_max_attempts'; + /** * Constructor. * - * @param ITimeFactory $time The time factory. - * @param EdepotTransferService $transferService The transfer service. - * @param SftpTransport $sftpTransport SFTP transport. - * @param RestApiTransport $restTransport REST API transport. - * @param OpenConnectorTransport $ocTransport OpenConnector transport. - * @param IAppConfig $appConfig The app configuration. - * @param LoggerInterface $logger Logger. + * @param ITimeFactory $time The time factory. + * @param EdepotTransferService $transferService The transfer service. + * @param TransferRecordService $transferRecordService Durable transfer-record persistence. + * @param TransferListService $transferListService Status machine + archivist escalation. + * @param SftpTransport $sftpTransport SFTP transport. + * @param RestApiTransport $restTransport REST API transport. + * @param OpenConnectorTransport $ocTransport OpenConnector transport. + * @param IJobList $jobList Background-job scheduler (re-enqueue). + * @param IAppConfig $appConfig The app configuration. + * @param LoggerInterface $logger Logger. */ public function __construct( ITimeFactory $time, private readonly EdepotTransferService $transferService, + private readonly TransferRecordService $transferRecordService, + private readonly TransferListService $transferListService, private readonly SftpTransport $sftpTransport, private readonly RestApiTransport $restTransport, private readonly OpenConnectorTransport $ocTransport, + private readonly IJobList $jobList, private readonly IAppConfig $appConfig, private readonly LoggerInterface $logger, ) { parent::__construct(time: $time); + }//end __construct() /** - * Execute the transfer job. + * Execute one transfer attempt and schedule the next on non-terminal + * failure (or escalate at the attempt cap). * - * @param mixed $argument Job arguments containing the transfer list data. + * @param mixed $argument Job argument: `{transferListId, attempt}`. * * @return void * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-37 + * @SuppressWarnings(PHPMD.CyclomaticComplexity) The attempt/terminal/retry/escalate + * branches are the durable-retry state machine; each is a required path. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Scenario: Failed attempt reschedules with backoff) */ protected function run(mixed $argument): void { - if (is_array($argument) === false || empty($argument['transferList']) === true) { + if (is_array($argument) === false || empty($argument['transferListId']) === true) { $this->logger->error( - message: '[TransferExecutionJob] Invalid job argument: missing transferList' + message: '[TransferExecutionJob] Invalid job argument: missing transferListId', + context: ['file' => __FILE__, 'line' => __LINE__] ); return; } - $transferList = $argument['transferList']; + $transferListId = (string) $argument['transferListId']; + $attempt = (int) ($argument['attempt'] ?? 1); - $this->logger->info( - message: '[TransferExecutionJob] Starting transfer execution', - context: ['transferUuid' => ($transferList['uuid'] ?? 'unknown')] - ); + $list = $this->transferRecordService->loadTransferList($transferListId); + if ($list === null) { + $this->logger->error( + message: '[TransferExecutionJob] Transfer list not found: '.$transferListId, + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return; + } try { $transport = $this->resolveTransport(); - $this->transferService->executeTransfer($transferList, $transport); + $list = $this->transferService->executeAttempt(transferList: $list, transport: $transport, attempt: $attempt); + } catch (\Throwable $e) { + $this->logger->error( + message: '[TransferExecutionJob] Attempt threw: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'transferListId' => $transferListId, 'attempt' => $attempt] + ); + // Treat an unexpected throw as a failed attempt for retry purposes. + $list['status'] = TransferListService::STATUS_PARTIALLY_FAILED; + } + + $status = (string) ($list['status'] ?? TransferListService::STATUS_FAILED); + if ($status === TransferListService::STATUS_COMPLETED) { + $this->transferRecordService->saveTransferList($list); $this->logger->info( - message: '[TransferExecutionJob] Transfer execution completed', - context: ['transferUuid' => ($transferList['uuid'] ?? 'unknown')] + message: '[TransferExecutionJob] Transfer completed', + context: ['file' => __FILE__, 'line' => __LINE__, 'transferListId' => $transferListId, 'attempt' => $attempt] ); - } catch (\Exception $e) { - $this->logger->error( - message: '[TransferExecutionJob] Transfer execution failed', + return; + } + + // Non-terminal failure: retry until the attempt cap, then escalate. + $maxAttempts = $this->maxAttempts(); + if ($attempt >= $maxAttempts) { + $list['status'] = TransferListService::STATUS_FAILED; + $this->transferRecordService->saveTransferList($list); + $this->transferListService->notifyArchivists($list); + $this->logger->warning( + message: '[TransferExecutionJob] Attempt cap reached — transfer failed, archivists notified', context: [ - 'transferUuid' => ($transferList['uuid'] ?? 'unknown'), - 'error' => $e->getMessage(), + 'file' => __FILE__, + 'line' => __LINE__, + 'transferListId' => $transferListId, + 'attempt' => $attempt, + 'maxAttempts' => $maxAttempts, ] ); + return; } + + // Keep the list non-terminal (in_progress) between attempts so a + // partially-failed run keeps its confirmed objects while retrying. + $list['status'] = TransferListService::STATUS_IN_PROGRESS; + $this->transferRecordService->saveTransferList($list); + + $delay = $this->backoffSeconds(attempt: $attempt); + $runAfter = ($this->time->getTime() + $delay); + $this->jobList->scheduleAfter( + TransferExecutionJob::class, + $runAfter, + [ + 'transferListId' => $transferListId, + 'attempt' => ($attempt + 1), + ] + ); + + $this->logger->info( + message: '[TransferExecutionJob] Attempt failed — rescheduled', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'transferListId' => $transferListId, + 'attempt' => $attempt, + 'nextAttempt' => ($attempt + 1), + 'delaySeconds' => $delay, + ] + ); + }//end run() + /** + * Exponential backoff with jitter: `min(60 * 2^(attempt-1), 28800)` ± 10 %. + * + * @param int $attempt The 1-based current attempt number. + * + * @return int The delay in seconds before the next attempt. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Scenario: Failed attempt reschedules with backoff) + */ + public function backoffSeconds(int $attempt): int + { + $exponent = max(0, ($attempt - 1)); + $base = (self::BASE_BACKOFF_SECONDS * (2 ** $exponent)); + $capped = min($base, self::MAX_BACKOFF_SECONDS); + + // ±10 % jitter to avoid synchronised retries (thundering herd). + $jitterRange = (int) floor($capped * 0.10); + $jitter = 0; + if ($jitterRange > 0) { + $jitter = random_int(($jitterRange * -1), $jitterRange); + } + + return max(1, ($capped + $jitter)); + + }//end backoffSeconds() + + /** + * The configured attempt cap (default 10). + * + * @return int The maximum number of attempts. + */ + private function maxAttempts(): int + { + $configured = (int) $this->appConfig->getValueString( + 'openregister', + self::CONFIG_KEY_MAX_ATTEMPTS, + (string) self::DEFAULT_MAX_ATTEMPTS + ); + + if ($configured < 1) { + return self::DEFAULT_MAX_ATTEMPTS; + } + + return $configured; + + }//end maxAttempts() + /** * Resolve the configured transport implementation. * * @return TransportInterface The transport to use. * - * @spec openspec/changes/retrofit-2026-05-24-edepot-transfer/tasks.md#task-1 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Requirement: One job run performs one transport attempt per package) */ private function resolveTransport(): TransportInterface { $transportType = $this->appConfig->getValueString('openregister', 'edepot_transport', 'rest_api'); - switch ($transportType) { - case 'sftp': - return $this->sftpTransport; - case 'openconnector': - return $this->ocTransport; - case 'rest_api': - default: - return $this->restTransport; - } + return match ($transportType) { + 'sftp' => $this->sftpTransport, + 'openconnector' => $this->ocTransport, + default => $this->restTransport, + }; + }//end resolveTransport() }//end class diff --git a/lib/BackgroundJob/TranslationProjectionJob.php b/lib/BackgroundJob/TranslationProjectionJob.php new file mode 100644 index 0000000000..88cef53c2f --- /dev/null +++ b/lib/BackgroundJob/TranslationProjectionJob.php @@ -0,0 +1,119 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @psalm-suppress UnusedClass + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Service\Deferral\DeferredEntryObjectResolver; +use OCA\OpenRegister\Service\Deferral\DeferredListenerContext; +use OCA\OpenRegister\Service\OrganisationService; +use OCA\OpenRegister\Service\TranslationProjectionService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IUserManager; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Deferred translation-sidecar projection under the forwarded actor. + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-2.1 + */ +class TranslationProjectionJob extends ActorForwardedJob +{ + /** + * Wire the projection collaborators on top of the actor plumbing. + * + * @param ITimeFactory $time Time factory for the parent job class. + * @param IUserSession $userSession Session to impersonate on / restore. + * @param IUserManager $userManager Resolver for the captured user id. + * @param OrganisationService $organisation Active-organisation resolver. + * @param LoggerInterface $logger PSR logger. + * @param DeferredEntryObjectResolver $resolver Stale-safe entry re-fetch. + * @param TranslationProjectionService $projection Projection service doing the real work. + * + * @return void + */ + public function __construct( + ITimeFactory $time, + IUserSession $userSession, + IUserManager $userManager, + OrganisationService $organisation, + LoggerInterface $logger, + private readonly DeferredEntryObjectResolver $resolver, + private readonly TranslationProjectionService $projection + ) { + parent::__construct( + time: $time, + userSession: $userSession, + userManager: $userManager, + organisation: $organisation, + logger: $logger + ); + }//end __construct() + + /** + * Project every still-live entry's current state into the sidecar. + * + * Per-entry failures are logged and do not abort the chunk. + * + * @param DeferredListenerContext $context The captured dispatch-time context. + * + * @return void + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + protected function runDeferred(DeferredListenerContext $context): void + { + foreach ($context->getEntries() as $entry) { + $object = $this->resolver->resolve(entry: $entry); + if ($object === null) { + continue; + } + + try { + $this->projection->project($object); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[TranslationProjectionJob] Projection failed for entry', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => ($entry['uuid'] ?? null), + 'error' => $e->getMessage(), + ] + ); + } + }//end foreach + }//end runDeferred() +}//end class diff --git a/lib/BackgroundJob/WebPushDispatchJob.php b/lib/BackgroundJob/WebPushDispatchJob.php new file mode 100644 index 0000000000..735dba0871 --- /dev/null +++ b/lib/BackgroundJob/WebPushDispatchJob.php @@ -0,0 +1,185 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\BackgroundJob; + +use OCA\OpenRegister\Service\WebPush\WebPushService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use Psr\Log\LoggerInterface; + +/** + * One-shot background job that delivers a Web Push notification to one user. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ +class WebPushDispatchJob extends QueuedJob +{ + /** + * Constructor. + * + * @param ITimeFactory $time Time factory for the base job. + * @param WebPushService $webPushService Encrypt/sign/POST/prune service. + * @param LoggerInterface $logger Logger for delivery diagnostics. + * + * @return void + */ + public function __construct( + ITimeFactory $time, + private readonly WebPushService $webPushService, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + }//end __construct() + + /** + * Run the job: build the payload and deliver it to the recipient. + * + * @param array $argument Job arguments: + * - uid: recipient uid (required) + * - ruleId: rule id (diagnostics) + * - originApp: origin app id (icon) + * - title: notification title + * - body: notification body + * - tag: dedup/collapse tag + * - actions: resolved action buttons + * + * @return void + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + protected function run($argument): void + { + $uid = ($argument['uid'] ?? null); + if (is_string($uid) === false || $uid === '') { + $this->logger->warning('[WebPushDispatchJob] Missing recipient uid; skipping.'); + return; + } + + $payload = $this->buildPayload(argument: $argument); + + try { + $delivered = $this->webPushService->deliver(uid: $uid, payload: $payload); + $this->logger->debug( + sprintf('[WebPushDispatchJob] Delivered web-push to %d endpoint(s) for "%s".', $delivered, $uid) + ); + } catch (\Throwable $e) { + // Web-push is best-effort and out of band — never escalate. + $this->logger->warning( + sprintf('[WebPushDispatchJob] Delivery to "%s" failed: %s', $uid, $e->getMessage()) + ); + } + }//end run() + + /** + * Build the Service-Worker-facing JSON payload from the job argument. + * + * Shape matches js/openregister-push-sw.js (title/body/icon/badge/tag/url/ + * actions[].action/.title/.url). + * + * @param array $argument The job argument. + * + * @return array The payload. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + private function buildPayload(array $argument): array + { + $originApp = (string) ($argument['originApp'] ?? 'openregister'); + $title = (string) ($argument['title'] ?? 'OpenRegister'); + $body = (string) ($argument['body'] ?? ''); + $tag = (string) ($argument['tag'] ?? ''); + + // Two parallel structures matching the Service Worker contract + // (js/openregister-push-sw.js): `actions` is [{action, title}] passed to + // showNotification() to render the buttons; `data.actions` is a + // { actionId: url } map used by notificationclick to route a clicked + // button, and `data.url` routes a click on the notification body. + $actions = []; + $actionUrls = []; + $topLevelUrl = ''; + $rawActions = ($argument['actions'] ?? []); + if (is_array($rawActions) === true) { + foreach ($rawActions as $idx => $action) { + if (is_array($action) === false) { + continue; + } + + $url = (string) ($action['url'] ?? ''); + if ($url === '') { + continue; + } + + if ($topLevelUrl === '') { + // First (or primary) action drives the body-click deeplink. + $topLevelUrl = $url; + } + + if (($action['primary'] ?? false) === true) { + $topLevelUrl = $url; + } + + $label = ''; + if (is_array($action['label'] ?? null) === true) { + $fallbackLabel = ''; + $firstLabel = reset($action['label']); + if ($firstLabel !== false) { + $fallbackLabel = $firstLabel; + } + + $label = (string) ($action['label']['en'] ?? $fallbackLabel); + } + + $actionKey = 'action-'.(string) $idx; + $actions[] = ['action' => $actionKey, 'title' => $label]; + $actionUrls[$actionKey] = $url; + }//end foreach + }//end if + + return [ + 'title' => $title, + 'body' => $body, + 'icon' => '/index.php/apps/openregister/webpush/icon/'.rawurlencode($originApp), + 'badge' => '/index.php/apps/openregister/webpush/badge/'.rawurlencode($originApp), + 'tag' => $tag, + 'actions' => $actions, + 'data' => [ + 'url' => $topLevelUrl, + 'actions' => $actionUrls, + ], + ]; + }//end buildPayload() +}//end class diff --git a/lib/BackgroundJob/WebhookDeliveryJob.php b/lib/BackgroundJob/WebhookDeliveryJob.php index 25bcd6dd91..8fd4310f05 100644 --- a/lib/BackgroundJob/WebhookDeliveryJob.php +++ b/lib/BackgroundJob/WebhookDeliveryJob.php @@ -19,8 +19,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-79 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-webhook-delivery-must-support-async-processing-via-background-jobs + * @spec openspec/specs/webhook-payload-mapping/spec.md */ declare(strict_types=1); @@ -125,8 +125,8 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-79 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-webhook-delivery-must-support-async-processing-via-background-jobs + * @spec openspec/specs/webhook-payload-mapping/spec.md */ protected function run($argument): void { diff --git a/lib/Calendar/CalendarEventTransformer.php b/lib/Calendar/CalendarEventTransformer.php index 2de2f4f360..12e8fad47a 100644 --- a/lib/Calendar/CalendarEventTransformer.php +++ b/lib/Calendar/CalendarEventTransformer.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/calendar-integration/spec.md */ declare(strict_types=1); @@ -64,8 +64,8 @@ class CalendarEventTransformer * * @return array|null The VEVENT array, or null if the object lacks required date data * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/calendar-integration/spec.md + * @spec openspec/specs/calendar-integration/spec.md */ public function transform( ObjectEntity $object, @@ -163,8 +163,8 @@ public function transform( * * @return bool True if events should be all-day * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/calendar-integration/spec.md + * @spec openspec/specs/calendar-integration/spec.md */ public function determineAllDay(array $calendarConfig, Schema $schema, string $dtstartField): bool { @@ -222,8 +222,8 @@ public function determineAllDay(array $calendarConfig, Schema $schema, string $d * * @return array The formatted [value, params] array * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/calendar-integration/spec.md + * @spec openspec/specs/calendar-integration/spec.md */ public function formatDateValue(string $value, bool $allDay): array { @@ -333,7 +333,7 @@ private function detectSourceTimezone(string $value): ?string * * @return array The formatted [value, params] array for DTEND * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ private function buildDtend( array $objectData, @@ -386,8 +386,8 @@ private function buildDtend( * * @return string The interpolated string * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/calendar-integration/spec.md + * @spec openspec/specs/calendar-integration/spec.md */ public function interpolateTemplate(string $template, array $objectData): string { @@ -421,7 +421,7 @@ function ($matches) use ($objectData) { * * @return string The VEVENT STATUS value (CONFIRMED, CANCELLED, TENTATIVE) * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-3 + * @spec openspec/specs/calendar-integration/spec.md */ private function resolveStatus(array $objectData, array $calendarConfig): string { diff --git a/lib/Calendar/RegisterCalendar.php b/lib/Calendar/RegisterCalendar.php index ad63458f5d..9bf00de118 100644 --- a/lib/Calendar/RegisterCalendar.php +++ b/lib/Calendar/RegisterCalendar.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-18 + * @spec openspec/specs/calendar-integration/spec.md */ declare(strict_types=1); @@ -138,7 +138,7 @@ public function __construct( * * @return string The calendar key * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function getKey(): string { @@ -150,7 +150,7 @@ public function getKey(): string * * @return string The calendar URI * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function getUri(): string { @@ -162,7 +162,7 @@ public function getUri(): string * * @return string|null The display name * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function getDisplayName(): ?string { @@ -174,7 +174,7 @@ public function getDisplayName(): ?string * * @return string|null The CSS hex color * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function getDisplayColor(): ?string { @@ -186,7 +186,7 @@ public function getDisplayColor(): ?string * * @return int The permission bitmask * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function getPermissions(): int { @@ -198,7 +198,7 @@ public function getPermissions(): int * * @return bool Always false for virtual calendars * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function isDeleted(): bool { @@ -222,8 +222,8 @@ public function isDeleted(): bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-18 + * @spec openspec/specs/calendar-integration/spec.md + * @spec openspec/specs/calendar-integration/spec.md */ public function search( string $pattern='', @@ -304,7 +304,7 @@ public function search( * * @return string|null The user ID or null if not a valid user principal * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ private function extractUserId(string $principalUri): ?string { @@ -322,7 +322,7 @@ private function extractUserId(string $principalUri): ?string * * @return array|null The filters array, or null if no timerange * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ private function buildTimerangeFilters(array $options): ?array { @@ -374,7 +374,7 @@ private function buildTimerangeFilters(array $options): ?array * * @return array Array of Register entities * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ private function findRegistersForSchema(Schema $schema): array { @@ -409,7 +409,7 @@ private function findRegistersForSchema(Schema $schema): array * * @return bool True if the event matches * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ private function matchesPattern(array $event, string $pattern): bool { diff --git a/lib/Calendar/RegisterCalendarProvider.php b/lib/Calendar/RegisterCalendarProvider.php index 84128e4bdb..d084188a68 100644 --- a/lib/Calendar/RegisterCalendarProvider.php +++ b/lib/Calendar/RegisterCalendarProvider.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-18 + * @spec openspec/specs/calendar-integration/spec.md */ declare(strict_types=1); @@ -134,8 +134,8 @@ public function __construct( * * @return array Array of ICalendar instances * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-18 + * @spec openspec/specs/calendar-integration/spec.md + * @spec openspec/specs/calendar-integration/spec.md */ public function getCalendars(string $principalUri, array $calendarUris=[]): array { @@ -191,7 +191,7 @@ public function getCalendars(string $principalUri, array $calendarUris=[]): arra * * @return array Array of ['schema' => Schema, 'config' => array] entries * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ private function getCalendarEnabledSchemas(): array { @@ -241,7 +241,7 @@ private function getCalendarEnabledSchemas(): array * * @return bool True if the principal is a valid user * - * @spec openspec/changes/retrofit-2026-04-28-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ private function isValidUserPrincipal(string $principalUri): bool { diff --git a/lib/Capabilities/UrnCapability.php b/lib/Capabilities/UrnCapability.php index 4e1af4b953..34cd3bc4a4 100644 --- a/lib/Capabilities/UrnCapability.php +++ b/lib/Capabilities/UrnCapability.php @@ -19,9 +19,9 @@ * @category Capabilities * @package OCA\OpenRegister\Capabilities * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Command/BackfillSystemOwnerCommand.php b/lib/Command/BackfillSystemOwnerCommand.php index 8dd6f7b81c..60a290dfe6 100644 --- a/lib/Command/BackfillSystemOwnerCommand.php +++ b/lib/Command/BackfillSystemOwnerCommand.php @@ -22,7 +22,7 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-1 + * @spec openspec/specs/auth-system/spec.md */ declare(strict_types=1); @@ -58,7 +58,7 @@ class BackfillSystemOwnerCommand extends Command * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-1 + * @spec openspec/specs/auth-system/spec.md */ public function __construct( private readonly RegisterMapper $registerMapper, @@ -74,7 +74,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-1 + * @spec openspec/specs/auth-system/spec.md */ protected function configure(): void { @@ -99,7 +99,7 @@ protected function configure(): void * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-1 + * @spec openspec/specs/auth-system/spec.md */ protected function execute(InputInterface $input, OutputInterface $output): int { @@ -208,7 +208,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int * * @return Register[] * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-1 + * @spec openspec/specs/auth-system/spec.md */ private function resolveRegisters(?string $registerRef): array { @@ -226,7 +226,7 @@ private function resolveRegisters(?string $registerRef): array * * @return Schema[] * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-1 + * @spec openspec/specs/auth-system/spec.md */ private function resolveSchemas(?string $schemaRef): array { @@ -245,7 +245,7 @@ private function resolveSchemas(?string $schemaRef): array * * @return array{0:int,1:int} Tuple of [scanned, updated]. * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-1 + * @spec openspec/specs/auth-system/spec.md */ private function backfillTable(string $tableName, bool $dryRun): array { diff --git a/lib/Command/BackfillTranslationSourceLanguageCommand.php b/lib/Command/BackfillTranslationSourceLanguageCommand.php new file mode 100644 index 0000000000..5933f42421 --- /dev/null +++ b/lib/Command/BackfillTranslationSourceLanguageCommand.php @@ -0,0 +1,159 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Command; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\TranslationMapper; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Back-fills `openregister_translations.source_language` from each row's + * parent register default language. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-1 + */ +class BackfillTranslationSourceLanguageCommand extends Command +{ + /** + * Wire the mappers used by the command. + * + * @param RegisterMapper $registerMapper Register lookup mapper. + * @param TranslationMapper $translationMapper Translation sidecar mapper. + */ + public function __construct( + private readonly RegisterMapper $registerMapper, + private readonly TranslationMapper $translationMapper + ) { + parent::__construct(); + }//end __construct() + + /** + * Configure command name + options. + * + * @return void + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-1 + */ + protected function configure(): void + { + $this->setName(name: 'openregister:translations:backfill-source-language') + ->setDescription( + 'Back-fill openregister_translations.source_language from each register default. Idempotent.' + ) + ->addOption( + 'batch-size', + null, + InputOption::VALUE_REQUIRED, + 'Maximum rows updated per register pass.', + '1000' + ) + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'Report rows that would be updated without writing.' + ); + }//end configure() + + /** + * Execute the back-fill. + * + * @param InputInterface $input Console input. + * @param OutputInterface $output Console output. + * + * @return int Symfony command exit code. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-1 + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $batchSize = (int) $input->getOption('batch-size'); + if ($batchSize < 1) { + $batchSize = 1000; + } + + $pending = $this->translationMapper->countMissingSourceLanguage(); + $output->writeln(sprintf('Translation rows pending back-fill: %d', $pending)); + + if ($pending === 0) { + $output->writeln('0 rows updated — column is fully back-filled.'); + return Command::SUCCESS; + } + + if ((bool) $input->getOption('dry-run') === true) { + $output->writeln( + sprintf( + 'Dry-run: would update up to %d rows (batch size %d).', + $pending, + $batchSize + ) + ); + return Command::SUCCESS; + } + + // Build register-id -> default-language map. + $registers = $this->registerMapper->findAll(_rbac: false, _multitenancy: false); + $defaults = []; + foreach ($registers as $register) { + if (($register instanceof Register) === false) { + continue; + } + + $id = $register->getId(); + if ($id === null) { + continue; + } + + $defaults[(string) $id] = $register->getDefaultLanguage(); + } + + $updated = $this->translationMapper->backfillSourceLanguage($defaults, $batchSize); + $output->writeln(sprintf('%d rows updated.', $updated)); + + $remaining = $this->translationMapper->countMissingSourceLanguage(); + if ($remaining > 0) { + $output->writeln( + sprintf( + '%d rows still pending — re-run the command to continue back-filling.', + $remaining + ) + ); + } + + return Command::SUCCESS; + }//end execute() +}//end class diff --git a/lib/Command/DedupeRegistersCommand.php b/lib/Command/DedupeRegistersCommand.php new file mode 100644 index 0000000000..79c028aff8 --- /dev/null +++ b/lib/Command/DedupeRegistersCommand.php @@ -0,0 +1,278 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Command; + +use OCA\OpenRegister\Db\MagicMapper; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\SchemaMapper; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Detect and merge duplicate registers sharing a slug. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DedupeRegistersCommand extends Command +{ + /** + * Wire the mappers used by the command. + * + * @param RegisterMapper $registerMapper Register lookup/mutation mapper. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param MagicMapper $magicMapper Magic table resolver for object counts. + * + * @return void + */ + public function __construct( + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly MagicMapper $magicMapper + ) { + parent::__construct(); + }//end __construct() + + /** + * Define command name, description, and options. + * + * @return void + */ + protected function configure(): void + { + $this->setName(name: 'openregister:registers:dedupe') + ->setDescription( + 'Detect and merge duplicate registers sharing a slug (keeps the lowest-id canonical register).' + ) + ->addOption( + 'apply', + null, + InputOption::VALUE_NONE, + 'Actually delete the duplicate registers. Without this flag the command runs in ' + .'dry-run mode and only reports what it WOULD delete.' + ) + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'Report duplicates without deleting anything (this is the default; kept for clarity)' + ) + ->addOption( + 'force', + null, + InputOption::VALUE_NONE, + 'Also delete duplicate registers that still own objects (DANGEROUS — drops those objects). ' + .'Has no effect without --apply.' + ); + }//end configure() + + /** + * Group registers by slug, report duplicates, and delete empty ones. + * + * @param InputInterface $input Console input. + * @param OutputInterface $output Console output stream. + * + * @return int Symfony command exit code. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + // Default to dry-run: a destructive sweep must be opted into with --apply (OPS-9). + // --dry-run is accepted as an explicit no-op alias for the default behaviour. + $apply = (bool) $input->getOption('apply'); + $dryRun = ($apply === false); + $force = (bool) $input->getOption('force'); + + if ($dryRun === true) { + $output->writeln( + 'Running in DRY-RUN mode — no registers will be deleted. ' + .'Re-run with --apply to perform deletions.' + ); + } + + $registers = $this->registerMapper->findAll(_rbac: false, _multitenancy: false); + + // Group by lowercased slug. + $bySlug = []; + foreach ($registers as $register) { + $slug = $register->getSlug(); + if ($slug === null || $slug === '') { + continue; + } + + $bySlug[strtolower($slug)][] = $register; + } + + $duplicateGroups = 0; + $deleted = 0; + $keptNonEmpty = 0; + + foreach ($bySlug as $slug => $group) { + if (count($group) < 2) { + continue; + } + + $duplicateGroups++; + + // Sort by id ASC: lowest-id is canonical (matches find() resolution). + usort($group, static fn(Register $a, Register $b) => ((int) $a->getId() <=> (int) $b->getId())); + $canonical = array_shift($group); + + $output->writeln( + sprintf( + 'Slug "%s": keeping canonical register id=%d, %d duplicate(s)', + $slug, + (int) $canonical->getId(), + count($group) + ) + ); + + foreach ($group as $duplicate) { + $objectCount = $this->countObjectsForRegister(register: $duplicate); + + if ($objectCount > 0 && $force === false) { + $keptNonEmpty++; + $output->writeln( + sprintf( + ' SKIP duplicate id=%d (owns %d object(s) — re-run with --force to drop, or merge manually)', + (int) $duplicate->getId(), + $objectCount + ) + ); + continue; + } + + if ($dryRun === true) { + $output->writeln( + sprintf( + ' WOULD DELETE duplicate id=%d (objects=%d)', + (int) $duplicate->getId(), + $objectCount + ) + ); + continue; + } + + try { + $this->registerMapper->delete($duplicate); + } catch (\Throwable $e) { + // RegisterMapper::delete() applies its own attached-object + // guard. If it refuses, keep the duplicate rather than + // aborting the whole sweep. + $keptNonEmpty++; + $output->writeln( + sprintf( + ' SKIP duplicate id=%d (delete refused: %s)', + (int) $duplicate->getId(), + $e->getMessage() + ) + ); + continue; + } + + $deleted++; + $output->writeln( + sprintf( + ' DELETED duplicate id=%d (objects=%d)', + (int) $duplicate->getId(), + $objectCount + ) + ); + }//end foreach + }//end foreach + + if ($duplicateGroups === 0) { + $output->writeln('No duplicate registers found.'); + return Command::SUCCESS; + } + + $suffix = ''; + if ($dryRun === true) { + $suffix = ' (dry run — no writes performed)'; + } + + $output->writeln( + sprintf( + 'Done. Duplicate slug groups=%d, deleted=%d, kept-non-empty=%d%s', + $duplicateGroups, + $deleted, + $keptNonEmpty, + $suffix + ) + ); + + return Command::SUCCESS; + }//end execute() + + /** + * Count objects owned by a register across all its magic tables. + * + * @param Register $register The register to count objects for. + * + * @return int Total object count across the register's magic tables. + */ + private function countObjectsForRegister(Register $register): int + { + $total = 0; + + foreach ($register->getSchemas() as $schemaRef) { + try { + $schema = $this->schemaMapper->find($schemaRef, _multitenancy: false); + } catch (\Throwable $e) { + continue; + } + + if ($this->magicMapper->tableExistsForRegisterSchema(register: $register, schema: $schema) === false) { + continue; + } + + try { + $total += $this->magicMapper->countObjectsInRegisterSchemaTable( + query: [], + register: $register, + schema: $schema + ); + } catch (\Throwable $e) { + continue; + } + }//end foreach + + return $total; + }//end countObjectsForRegister() +}//end class diff --git a/lib/Command/EncryptFieldCommand.php b/lib/Command/EncryptFieldCommand.php new file mode 100644 index 0000000000..0323b75dee --- /dev/null +++ b/lib/Command/EncryptFieldCommand.php @@ -0,0 +1,370 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @spec openspec/specs/field-level-encryption/spec.md#requirement-existing-plaintext-values-can-be-migrated-to-encrypted + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Command; + +use OCA\OpenRegister\Db\MagicMapper; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\FieldEncryptionHandler; +use OCP\DB\Exception as DbException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Encrypt existing plaintext values of a newly-flagged property. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.LongVariable) + */ +class EncryptFieldCommand extends Command +{ + /** + * Wire the mappers, encryption handler and database connection used by the command. + * + * @param RegisterMapper $registerMapper Register lookup mapper. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param MagicMapper $magicMapper Magic table resolver. + * @param FieldEncryptionHandler $fieldEncryptionHandler Envelope encrypt/decrypt logic. + * @param IDBConnection $db Database connection for native SELECT/UPDATE. + * + * @return void + */ + public function __construct( + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly MagicMapper $magicMapper, + private readonly FieldEncryptionHandler $fieldEncryptionHandler, + private readonly IDBConnection $db + ) { + parent::__construct(); + }//end __construct() + + /** + * Define command name, description, and options. + * + * @return void + */ + protected function configure(): void + { + $this->setName(name: 'openregister:encrypt-field') + ->setDescription( + 'Encrypt existing plaintext values of a property flagged x-openregister-encrypted. ' + .'Idempotent: values already encrypted are skipped.' + ) + ->addOption('property', null, InputOption::VALUE_REQUIRED, 'The property name to encrypt (required)') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Report counts without writing to the database') + ->addOption('register', null, InputOption::VALUE_REQUIRED, 'Limit to a single register (slug, uuid or id)') + ->addOption('schema', null, InputOption::VALUE_REQUIRED, 'Limit to a single schema (slug, uuid or id)'); + }//end configure() + + /** + * Iterate every magic table whose schema flags the given property encrypted, + * and encrypt any plaintext value found. + * + * @param InputInterface $input Console input. + * @param OutputInterface $output Console output stream. + * + * @return int Symfony command exit code. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $property = (string) ($input->getOption('property') ?? ''); + if ($property === '') { + $output->writeln('--property is required.'); + return Command::FAILURE; + } + + $dryRun = (bool) $input->getOption('dry-run'); + $registerRef = $input->getOption('register'); + $schemaRef = $input->getOption('schema'); + + try { + $registers = $this->resolveRegisters(registerRef: $registerRef); + $schemas = $this->resolveSchemas(schemaRef: $schemaRef); + } catch (\Throwable $e) { + $output->writeln(''.$e->getMessage().''); + return Command::FAILURE; + } + + if (count($registers) === 0 || count($schemas) === 0) { + $output->writeln('No registers or schemas resolved — nothing to do.'); + return Command::SUCCESS; + } + + $modeLabel = ''; + if ($dryRun === true) { + $modeLabel = ' (dry run)'; + } + + $output->writeln( + sprintf('Encrypting property \'%s\'%s', $property, $modeLabel) + ); + + $grandScanned = 0; + $grandEncrypted = 0; + $grandFailed = 0; + $tablesTouched = 0; + + $schemasById = []; + foreach ($schemas as $schema) { + $schemasById[(int) $schema->getId()] = $schema; + } + + foreach ($registers as $register) { + $allowedSchemaIds = $register->getSchemas(); + foreach ($allowedSchemaIds as $allowedSchemaId) { + $allowedId = (int) $allowedSchemaId; + if (isset($schemasById[$allowedId]) === false) { + continue; + } + + $schema = $schemasById[$allowedId]; + + if (in_array($property, $schema->getEncryptedProperties(), true) === false) { + // This schema does not flag the property as encrypted — skip it + // rather than encrypting a field nobody asked to protect. + continue; + } + + if ($this->magicMapper->tableExistsForRegisterSchema(register: $register, schema: $schema) === false) { + continue; + } + + $tableName = $this->magicMapper->getTableNameForRegisterSchema( + register: $register, + schema: $schema + ); + + [$scanned, $encrypted, $failed] = $this->encryptTable( + tableName: $tableName, + property: $property, + dryRun: $dryRun, + output: $output + ); + + $grandScanned += $scanned; + $grandEncrypted += $encrypted; + $grandFailed += $failed; + $tablesTouched += 1; + + $output->writeln( + sprintf( + ' %s/%s (%s): scanned=%d encrypted=%d failed=%d', + $register->getSlug() ?? $register->getId(), + $schema->getSlug() ?? $schema->getId(), + $tableName, + $scanned, + $encrypted, + $failed + ) + ); + }//end foreach + }//end foreach + + $summarySuffix = ''; + if ($dryRun === true) { + $summarySuffix = ' (dry run — no writes performed)'; + } + + $output->writeln( + sprintf( + 'Done. Tables=%d scanned=%d encrypted=%d failed=%d%s', + $tablesTouched, + $grandScanned, + $grandEncrypted, + $grandFailed, + $summarySuffix + ) + ); + + // Fail loud: a run with per-row failures must not exit 0 — an operator + // scripting this command needs to know some rows still hold plaintext. + if ($grandFailed > 0) { + $output->writeln('One or more rows failed to encrypt — see log for details.'); + return Command::FAILURE; + } + + return Command::SUCCESS; + }//end execute() + + /** + * Resolve the list of registers to operate on. + * + * @param string|null $registerRef Optional register slug, uuid or id. + * + * @return Register[] + */ + private function resolveRegisters(?string $registerRef): array + { + if ($registerRef !== null && $registerRef !== '') { + return [$this->registerMapper->find($registerRef, _multitenancy: false)]; + } + + return $this->registerMapper->findAll(_rbac: false, _multitenancy: false); + }//end resolveRegisters() + + /** + * Resolve the list of schemas to operate on. + * + * @param string|null $schemaRef Optional schema slug, uuid or id. + * + * @return Schema[] + */ + private function resolveSchemas(?string $schemaRef): array + { + if ($schemaRef !== null && $schemaRef !== '') { + return [$this->schemaMapper->find($schemaRef, _multitenancy: false)]; + } + + return $this->schemaMapper->findAll(_rbac: false, _multitenancy: false); + }//end resolveSchemas() + + /** + * Encrypt the plaintext values of one property across one magic table. + * + * Row-by-row (ICrypto::encrypt() is not batchable in SQL): SELECT id + the + * JSON `object` blob, decode, check the property value, encrypt if it is a + * non-empty string that is not already an envelope, re-encode, UPDATE. + * Also NULLs out any legacy dedicated column matching the property name + * (pre-existing from before the property was flagged encrypted) so no + * plaintext mirror survives outside the blob — best-effort: the column may + * not exist, which is expected and not an error. + * + * @param string $tableName Fully qualified magic table name. + * @param string $property The property name to encrypt. + * @param bool $dryRun When true, only count; never write. + * @param OutputInterface $output Console output for per-row failure logging. + * + * @return array{0:int,1:int,2:int} Tuple of [scanned, encrypted, failed]. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function encryptTable(string $tableName, string $property, bool $dryRun, OutputInterface $output): array + { + $columnName = preg_replace('/[^a-zA-Z0-9_]/', '', $property); + + $select = $this->db->getQueryBuilder(); + $select->select('id', 'object')->from($tableName); + $result = $select->executeQuery(); + + $scanned = 0; + $encrypted = 0; + $failed = 0; + + while (($row = $result->fetch()) !== false) { + $objectJson = $row['object'] ?? null; + if (is_string($objectJson) === false || $objectJson === '') { + continue; + } + + $objectData = json_decode($objectJson, true); + if (is_array($objectData) === false || array_key_exists($property, $objectData) === false) { + continue; + } + + $value = $objectData[$property]; + if ($value === null || $value === '' || is_string($value) === false) { + continue; + } + + $scanned++; + + if ($this->fieldEncryptionHandler->isEnvelope($value) === true) { + // Already encrypted — idempotent no-op. + continue; + } + + if ($dryRun === true) { + $encrypted++; + continue; + } + + try { + $objectData[$property] = $this->fieldEncryptionHandler->encryptValue($value); + } catch (\Throwable $e) { + $failed++; + $output->writeln( + sprintf( + ' row id=%s: failed to encrypt \'%s\': %s', + (string) ($row['id'] ?? '?'), + $property, + $e->getMessage() + ) + ); + continue; + } + + $update = $this->db->getQueryBuilder(); + $update->update($tableName) + ->set('object', $update->createNamedParameter(json_encode($objectData))) + ->where($update->expr()->eq('id', $update->createNamedParameter($row['id'], IQueryBuilder::PARAM_INT))); + $update->executeStatement(); + + $encrypted++; + }//end while + + $result->closeCursor(); + + // Best-effort: null out a legacy dedicated column for this property, if + // one still exists from before it was flagged encrypted. The column may + // not exist (the common case going forward — buildTableColumnsFromSchema() + // no longer creates one for an encrypted property) which is expected, not + // an error, so an "unknown column" failure here is swallowed deliberately + // (unlike decryption failures, which are never swallowed) — it is not + // itself a source of data loss, only a no-op when there was nothing to null. + if ($dryRun === false && $encrypted > 0) { + try { + $nullify = $this->db->getQueryBuilder(); + $nullify->update($tableName) + ->set($columnName, $nullify->createNamedParameter(null)) + ->where($nullify->expr()->isNotNull($columnName)); + $nullify->executeStatement(); + } catch (DbException $e) { + // Column does not exist (or is not nullable) — nothing to clean up. + unset($e); + } + } + + return [$scanned, $encrypted, $failed]; + }//end encryptTable() +}//end class diff --git a/lib/Command/GenerateVapidKeys.php b/lib/Command/GenerateVapidKeys.php new file mode 100644 index 0000000000..8248bc22a4 --- /dev/null +++ b/lib/Command/GenerateVapidKeys.php @@ -0,0 +1,127 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Command; + +use Minishlink\WebPush\VAPID; +use OCA\OpenRegister\Service\WebPush\WebPushService; +use OCP\IAppConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Generate + store the instance VAPID keypair for Web Push. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ +class GenerateVapidKeys extends Command +{ + + /** + * App id used for IAppConfig writes. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * Wire the command against app config. + * + * @param IAppConfig $appConfig App-config writer for the keypair. + */ + public function __construct( + private readonly IAppConfig $appConfig, + ) { + parent::__construct(); + + }//end __construct() + + /** + * Define the command name + options. + * + * @return void + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + protected function configure(): void + { + $this->setName(name: 'openregister:web-push:generate-vapid') + ->setDescription('Generate and store the VAPID keypair used to sign Web Push payloads.') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Rotate the keypair even when one already exists (existing subscriptions will be re-keyed on next subscribe).' + ); + + }//end configure() + + /** + * Generate the keypair and persist it to app config. + * + * @param InputInterface $input Console input. + * @param OutputInterface $output Console output stream. + * + * @return int Symfony command exit code. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $force = (bool) $input->getOption('force'); + $existing = $this->appConfig->getValueString(self::APP_ID, WebPushService::VAPID_PUBLIC_KEY, ''); + + if ($existing !== '' && $force === false) { + $output->writeln('A VAPID keypair already exists. Re-run with --force to rotate it.'); + $output->writeln(sprintf('Public key: %s', $existing)); + return Command::SUCCESS; + } + + try { + $keys = VAPID::createVapidKeys(); + } catch (\Throwable $e) { + $output->writeln(sprintf('Failed to generate VAPID keypair: %s', $e->getMessage())); + return Command::FAILURE; + } + + $this->appConfig->setValueString(self::APP_ID, WebPushService::VAPID_PUBLIC_KEY, (string) $keys['publicKey']); + $this->appConfig->setValueString(self::APP_ID, WebPushService::VAPID_PRIVATE_KEY, (string) $keys['privateKey'], false, true); + + $output->writeln('VAPID keypair generated and stored in app config.'); + $output->writeln(sprintf('Public key: %s', (string) $keys['publicKey'])); + $output->writeln('The private key is stored as a sensitive app value and is never printed.'); + + return Command::SUCCESS; + }//end execute() +}//end class diff --git a/lib/Command/MigrateStorageCommand.php b/lib/Command/MigrateStorageCommand.php index 93ca793dcd..54e711beb4 100644 --- a/lib/Command/MigrateStorageCommand.php +++ b/lib/Command/MigrateStorageCommand.php @@ -47,7 +47,7 @@ class MigrateStorageCommand extends Command * * @param MigrationService $migrationService Migration service instance. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-21 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly MigrationService $migrationService, @@ -60,7 +60,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-21 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function configure(): void { @@ -130,7 +130,7 @@ protected function configure(): void * * @return int Command exit code. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-21 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/lib/Command/RematerialiseCalculationsCommand.php b/lib/Command/RematerialiseCalculationsCommand.php index f4adf2ca78..014eeabe69 100644 --- a/lib/Command/RematerialiseCalculationsCommand.php +++ b/lib/Command/RematerialiseCalculationsCommand.php @@ -25,7 +25,7 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-2 + * @spec openspec/specs/computed-fields/spec.md */ declare(strict_types=1); @@ -37,7 +37,9 @@ use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\Calculation\AggregateReferenceResolver; use OCA\OpenRegister\Service\Calculation\CalculationEvaluator; +use OCA\OpenRegister\Service\Calculation\ReferenceResolver; use OCA\OpenRegister\Service\ObjectService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -55,22 +57,28 @@ class RematerialiseCalculationsCommand extends Command /** * Wire the mappers, evaluator, and object service used by the command. * - * @param RegisterMapper $registerMapper Register lookup mapper. - * @param SchemaMapper $schemaMapper Schema lookup mapper. - * @param MagicMapper $magicMapper Magic table mapper for objects. - * @param ObjectService $objectService Object persistence service. - * @param CalculationEvaluator $evaluator Expression evaluator. + * @param RegisterMapper $registerMapper Register lookup mapper. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param MagicMapper $magicMapper Magic table mapper for objects. + * @param ObjectService $objectService Object persistence service. + * @param CalculationEvaluator $evaluator Expression evaluator. + * @param ReferenceResolver $references Cross-object reference pre-resolver. + * @param AggregateReferenceResolver $aggregates Aggregate-reference pre-resolver. * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-2 + * @spec openspec/specs/computed-fields/spec.md + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-2 + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-2 */ public function __construct( private readonly RegisterMapper $registerMapper, private readonly SchemaMapper $schemaMapper, private readonly MagicMapper $magicMapper, private readonly ObjectService $objectService, - private readonly CalculationEvaluator $evaluator + private readonly CalculationEvaluator $evaluator, + private readonly ReferenceResolver $references, + private readonly AggregateReferenceResolver $aggregates ) { parent::__construct(); }//end __construct() @@ -80,7 +88,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-2 + * @spec openspec/specs/computed-fields/spec.md */ protected function configure(): void { @@ -105,7 +113,7 @@ protected function configure(): void * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-2 + * @spec openspec/specs/computed-fields/spec.md */ protected function execute(InputInterface $input, OutputInterface $output): int { @@ -160,6 +168,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int limit: 100000 ); + // Declared cross-object references are pre-resolved per object so the + // recompute path refreshes references the same way the save path does. + $referenceSpecs = ($schema->getConfiguration()['x-openregister-references'] ?? null); + if (is_array($referenceSpecs) === false || count($referenceSpecs) === 0) { + $referenceSpecs = null; + } + + // Declared aggregate-references are pre-resolved per object so the + // recompute path refreshes save-time aggregate snapshots the same way + // the save path does. + $aggregateSpecs = ($schema->getConfiguration()['x-openregister-aggregate-refs'] ?? null); + if (is_array($aggregateSpecs) === false || count($aggregateSpecs) === 0) { + $aggregateSpecs = null; + } + $touched = 0; $unchanged = 0; $failed = 0; @@ -168,6 +191,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int $data = $entity->getObject() ?? []; $payload = $this->withSelf(data: $data, entity: $entity); + if ($referenceSpecs !== null) { + $payload['@ref'] = $this->references->resolveAll( + payload: $payload, + references: $referenceSpecs, + register: $entity->getRegister() + ); + } + + if ($aggregateSpecs !== null) { + $payload['@aggregate'] = $this->aggregates->resolveAll( + payload: $payload, + aggregates: $aggregateSpecs, + registerRef: $entity->getRegister() + ); + } + $changed = false; foreach ($calcs as $name => $spec) { if (is_array($spec) === false || ($spec['materialise'] ?? false) !== true) { @@ -248,7 +287,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int * * @return array Payload with `@self` injected. * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-2 + * @spec openspec/specs/computed-fields/spec.md */ private function withSelf(array $data, \OCA\OpenRegister\Db\ObjectEntity $entity): array { @@ -283,7 +322,7 @@ private function withSelf(array $data, \OCA\OpenRegister\Db\ObjectEntity $entity * * @return array|null Calculations map, or null when absent. * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-2 + * @spec openspec/specs/computed-fields/spec.md */ private function getCalculations(Schema $schema): ?array { diff --git a/lib/Command/ResolverListCommand.php b/lib/Command/ResolverListCommand.php new file mode 100644 index 0000000000..a02bbe1f2e --- /dev/null +++ b/lib/Command/ResolverListCommand.php @@ -0,0 +1,111 @@ +_register` / `_schema` IAppConfig key + * configured for a given Conduction app, paired with its raw value. + * Drives admin diagnostics for the canonical resolver convention; the + * same enumeration also powers admin UIs via + * `RegisterResolverService::enumerateAppConfigs()`. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Command + * @package OCA\OpenRegister\Command + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/register-resolver-service/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Command; + +use OCA\OpenRegister\Service\RegisterResolverService; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * List every resolver-shaped app-config key for the given app. + * + * @spec openspec/specs/register-resolver-service/spec.md + */ +class ResolverListCommand extends Command +{ + /** + * Wire the command against the resolver service. + * + * @param RegisterResolverService $resolverService Resolver used for enumeration. + */ + public function __construct( + private readonly RegisterResolverService $resolverService, + ) { + parent::__construct(); + + }//end __construct() + + /** + * Define command name + the required `app-id` argument. + * + * @return void + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Phase 3 — Convention check + diagnostics: console command surface) + */ + protected function configure(): void + { + $this->setName(name: 'openregister:resolver:list') + ->setDescription( + 'List every `_register` / `_schema` IAppConfig key for the given Conduction app.' + ) + ->addArgument( + 'app-id', + InputArgument::REQUIRED, + 'The consumer app id (e.g. opencatalogi, pipelinq, docudesk).' + ); + + }//end configure() + + /** + * Print the resolver-key inventory. + * + * @param InputInterface $input Console input. + * @param OutputInterface $output Console output stream. + * + * @return int Symfony command exit code. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: enumerateAppConfigs — driven via this CLI for admin diagnostics) + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $appId = (string) $input->getArgument('app-id'); + $map = $this->resolverService->enumerateAppConfigs($appId); + + if ($map === []) { + $output->writeln( + sprintf('No resolver-shaped config keys set for app "%s".', $appId) + ); + return Command::SUCCESS; + } + + $output->writeln(sprintf('Resolver keys for app "%s":', $appId)); + foreach ($map as $key => $value) { + $output->writeln(sprintf(' %s = %s', $key, $value)); + } + + return Command::SUCCESS; + + }//end execute() +}//end class diff --git a/lib/Command/SolrDebugCommand.php b/lib/Command/SolrDebugCommand.php deleted file mode 100644 index 7d965a8ada..0000000000 --- a/lib/Command/SolrDebugCommand.php +++ /dev/null @@ -1,435 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Command; - -use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Service\Index\SetupHandler; -use OCP\IConfig; -use OCP\Http\Client\IClientService; -use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * SOLR Debug Command for testing SOLR functionality step by step - * - * @category Command - * @package OCA\OpenRegister\Command - * @author OpenRegister Team - * @copyright 2024 OpenRegister - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - */ -class SolrDebugCommand extends Command -{ - /** - * Constructor - * - * Initializes the SOLR debug command with required services. - * - * @param SettingsService $settingsService Settings service for SOLR configuration - * @param LoggerInterface $logger Logger for debugging output - * @param IConfig $config Nextcloud configuration - * @param IClientService $clientService HTTP client service (unused) - * @param IndexService $indexService SOLR/OpenSearch index service - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - public function __construct( - private readonly SettingsService $settingsService, - private readonly LoggerInterface $logger, - private readonly IConfig $config, - /** - * HTTP client service (unused but required by dependency injection). - * - * @psalm-suppress UnusedProperty - */ - private readonly IClientService $clientService, - private readonly IndexService $indexService - ) { - parent::__construct(); - }//end __construct() - - /** - * Configure the command - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - protected function configure(): void - { - $this - ->setName(name: 'openregister:solr:debug') - ->setDescription('Debug SOLR configuration and functionality step by step') - ->addOption( - 'setup', - 's', - InputOption::VALUE_NONE, - 'Run SOLR setup process' - ) - ->addOption( - 'test-connection', - 't', - InputOption::VALUE_NONE, - 'Test SOLR connection' - ) - ->addOption( - 'check-cores', - 'c', - InputOption::VALUE_NONE, - 'Check existing cores/collections' - ) - ->addOption( - 'tenant-info', - 'i', - InputOption::VALUE_NONE, - 'Show tenant information' - ) - ->addOption( - 'all', - 'a', - InputOption::VALUE_NONE, - 'Run all debug steps' - ); - }//end configure() - - /** - * Execute the command - * - * @param InputInterface $input Input interface - * @param OutputInterface $output Output interface - * - * @return int Command exit code - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $output->writeln('🔍 SOLR Debug Tool - OpenRegister Multi-Tenant'); - $output->writeln('================================================'); - - $runAll = $input->getOption('all'); - - if ($runAll === true || $input->getOption('tenant-info') === true) { - $this->showTenantInfo(output: $output); - } - - if ($runAll === true || $input->getOption('setup') === true) { - $this->testSetup(output: $output); - } - - if ($runAll === true || $input->getOption('test-connection') === true) { - $this->testConnection(output: $output); - } - - if ($runAll === true || $input->getOption('check-cores') === true) { - $this->checkCores(output: $output); - } - - $hasSetup = $input->getOption('setup') === true; - $hasTestConnection = $input->getOption('test-connection') === true; - $hasCheckCores = $input->getOption('check-cores') === true; - $hasTenantInfo = $input->getOption('tenant-info') === true; - - $noOptions = $hasSetup === false && $hasTestConnection === false; - $noOptions = $noOptions && $hasCheckCores === false && $hasTenantInfo === false; - if ($runAll === false && $noOptions === true) { - $msg = 'No options specified. Use --all or specific options like --setup, --test-connection, --check-cores'; - $output->writeln(''.$msg.''); - return Command::SUCCESS; - } - - return Command::SUCCESS; - }//end execute() - - /** - * Show tenant information - * - * @param OutputInterface $output Output interface - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function showTenantInfo(OutputInterface $output): void - { - $output->writeln('📋 Tenant Information'); - - // Generate tenant ID the same way as SolrService. - $instanceId = $this->config->getSystemValue(key: 'instanceid', default: 'default'); - $overwriteHost = $this->config->getSystemValue(key: 'overwrite.cli.url', default: ''); - - // Use overwrite host for tenant ID if set, otherwise use instance ID. - $tenantId = 'nc_'.substr($instanceId, 0, 8); - if (empty($overwriteHost) === false) { - $tenantId = 'nc_'.hash('crc32', $overwriteHost); - } - - // Display overwrite host value or 'not set'. - $overwriteHostDisplay = 'not set'; - if ($overwriteHost !== '' && $overwriteHost !== null) { - $overwriteHostDisplay = $overwriteHost; - } - - $output->writeln(" Instance ID: $instanceId"); - $output->writeln(" Overwrite Host: $overwriteHostDisplay"); - $output->writeln(" Generated Tenant ID: $tenantId"); - - // Get SOLR settings. - $solrSettings = $this->settingsService->getSolrSettings(); - $baseCoreName = $solrSettings['core'] ?? 'openregister'; - $tenantSpecificCore = $baseCoreName.'_'.$tenantId; - - $output->writeln(" Base Core Name: $baseCoreName"); - $output->writeln(" Tenant Specific Core: $tenantSpecificCore"); - $output->writeln(''); - }//end showTenantInfo() - - /** - * Test SOLR setup - * - * @param OutputInterface $output Output interface - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function testSetup(OutputInterface $output): void - { - $output->writeln('🔧 Testing SOLR Setup'); - - try { - $solrSettings = $this->settingsService->getSolrSettings(); - - if ($solrSettings['enabled'] === false) { - $output->writeln('❌ SOLR is disabled in settings'); - return; - } - - $output->writeln(' SOLR Configuration:'); - $output->writeln(" Host: {$solrSettings['host']}"); - $output->writeln(" Port: {$solrSettings['port']}"); - $output->writeln(" Path: {$solrSettings['path']}"); - $output->writeln(" Core: {$solrSettings['core']}"); - $output->writeln(" Scheme: {$solrSettings['scheme']}"); - - // Create IndexService from settings. - // NOTE: This requires proper dependency injection - IndexService needs - // FileHandler, ObjectHandler, SchemaHandler, SearchBackendInterface - // For now, this will fail at runtime and needs to be fixed with proper DI. - // TODO: Inject these dependencies via constructor. - // Command classes don't have getContainer() method - this needs to be fixed. - $output->writeln('IndexService creation requires dependency injection - not yet implemented'); - } catch (\Exception $e) { - $output->writeln("❌ Setup failed: {$e->getMessage()}"); - }//end try - - $output->writeln(''); - }//end testSetup() - - /** - * Test SOLR connection - * - * @param OutputInterface $output Output interface - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function testConnection(OutputInterface $output): void - { - $output->writeln('🔗 Testing SOLR Connection'); - - try { - $solrService = $this->indexService; - - if ($solrService->isAvailable() === false) { - $output->writeln('❌ SOLR service is not available'); - return; - } - - $connectionResult = $solrService->testConnection(); - - if ($connectionResult['success'] !== true) { - $output->writeln("❌ Connection failed: {$connectionResult['message']}"); - return; - } - - $output->writeln('✅ SOLR connection successful (Guzzle HTTP)'); - $output->writeln(" Response time: {$connectionResult['details']['response_time_ms']}ms"); - $output->writeln(" SOLR version: {$connectionResult['details']['solr_version']}"); - $output->writeln(" Tenant ID: {$connectionResult['details']['tenant_id']}"); - $output->writeln(" Mode: {$connectionResult['details']['mode']}"); - - // Test tenant collection creation (throws on failure). - $output->writeln(''); - $output->writeln('🏗️ Testing tenant collection creation...'); - $solrService->ensureTenantCollection(); - $output->writeln('✅ Tenant collection ready'); - $docCount = $solrService->getDocumentCount(); - $output->writeln(" Document count: $docCount"); - } catch (\Exception $e) { - $output->writeln("❌ Connection test failed: {$e->getMessage()}"); - }//end try - - $output->writeln(''); - }//end testConnection() - - /** - * Check existing cores/collections - * - * @param OutputInterface $output Output interface - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function checkCores(OutputInterface $output): void - { - $output->writeln('🗄️ Checking SOLR Cores/Collections'); - - try { - $solrSettings = $this->settingsService->getSolrSettings(); - - if ($solrSettings['enabled'] === false) { - $output->writeln('❌ SOLR is disabled'); - return; - } - - // Test direct SOLR admin API calls. - $this->testSolrAdminAPI(output: $output, solrSettings: $solrSettings); - } catch (\Exception $e) { - $output->writeln("❌ Core check failed: {$e->getMessage()}"); - } - - $output->writeln(''); - }//end checkCores() - - /** - * Test SOLR Admin API directly - * - * @param OutputInterface $output Output interface - * @param array $solrSettings SOLR configuration - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function testSolrAdminAPI(OutputInterface $output, array $solrSettings): void - { - // Test cores listing (standalone SOLR). - $coresUrl = sprintf( - '%s://%s:%d%s/admin/cores?action=STATUS&wt=json', - $solrSettings['scheme'], - $solrSettings['host'], - $solrSettings['port'], - $solrSettings['path'] - ); - - $output->writeln(" Testing cores API: $coresUrl"); - - $coresResponse = file_get_contents($coresUrl); - if ($coresResponse === false || $coresResponse === '') { - $output->writeln(' ❓ Cores API not available (might be SolrCloud)'); - } - - if ($coresResponse !== false && $coresResponse !== '') { - $coresData = json_decode($coresResponse, true); - if ($coresData !== null && ($coresData['status'] ?? null) !== null) { - $coreCount = count($coresData['status']); - $output->writeln(" ✅ Found $coreCount cores (standalone mode)"); - foreach ($coresData['status'] as $coreName => $coreInfo) { - $docCount = $coreInfo['index']['numDocs'] ?? 'unknown'; - $output->writeln(" - $coreName ($docCount documents)"); - } - } - } - - // Test collections listing (SolrCloud). - $collectionsUrl = sprintf( - '%s://%s:%d%s/admin/collections?action=CLUSTERSTATUS&wt=json', - $solrSettings['scheme'], - $solrSettings['host'], - $solrSettings['port'], - $solrSettings['path'] - ); - - $output->writeln(" Testing collections API: $collectionsUrl"); - - $collectionsResponse = file_get_contents($collectionsUrl); - if ($collectionsResponse === false || $collectionsResponse === '') { - $output->writeln(' ❓ Collections API not available (might be standalone)'); - } - - if ($collectionsResponse !== false && $collectionsResponse !== '') { - $collectionsData = json_decode($collectionsResponse, true); - if ($collectionsData !== null && ($collectionsData['cluster']['collections'] ?? null) !== null) { - $collectionCount = count($collectionsData['cluster']['collections']); - $output->writeln(" ✅ Found $collectionCount collections (SolrCloud mode)"); - foreach (array_keys($collectionsData['cluster']['collections']) as $collectionName) { - $output->writeln(" - ".$collectionName.""); - } - } - } - - // Test configSets listing. - $configSetsUrl = sprintf( - '%s://%s:%d%s/admin/configs?action=LIST&wt=json', - $solrSettings['scheme'], - $solrSettings['host'], - $solrSettings['port'], - $solrSettings['path'] - ); - - $output->writeln(" Testing configSets API: $configSetsUrl"); - - $configSetsResponse = file_get_contents($configSetsUrl); - if ($configSetsResponse === false || $configSetsResponse === '') { - $output->writeln(' ❓ ConfigSets API not available'); - } - - if ($configSetsResponse !== false && $configSetsResponse !== '') { - $configSetsData = json_decode($configSetsResponse, true); - if ($configSetsData !== null && ($configSetsData['configSets'] ?? null) !== null) { - $configSetCount = count($configSetsData['configSets']); - $output->writeln(" ✅ Found $configSetCount configSets"); - foreach ($configSetsData['configSets'] as $configSetName) { - $output->writeln(" - $configSetName"); - } - } - } - }//end testSolrAdminAPI() -}//end class diff --git a/lib/Command/SolrManagementCommand.php b/lib/Command/SolrManagementCommand.php deleted file mode 100644 index c4dcea2901..0000000000 --- a/lib/Command/SolrManagementCommand.php +++ /dev/null @@ -1,697 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Command; - -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\Index\SetupHandler; -use OCP\IConfig; -use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Input\InputArgument; - -/** - * SOLR Management Command for production operations - * - * Provides comprehensive SOLR management including: - * - Initial setup and schema deployment - * - Index optimization and warming - * - Collection management - * - Health checks and diagnostics - * - * @category Command - * @package OCA\OpenRegister\Command - * @author OpenRegister Team - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/OpenRegister/OpenRegister - * @version GIT: - * @copyright 2024 OpenRegister - * - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - */ -class SolrManagementCommand extends Command -{ - /** - * Constructor - * - * @param LoggerInterface $logger Logger for debugging and monitoring - * @param IndexService $solrService SOLR service for operations - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - public function __construct( - private readonly LoggerInterface $logger, - private readonly IndexService $solrService - ) { - parent::__construct(); - }//end __construct() - - /** - * Configure the command - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - protected function configure(): void - { - $this->setName(name: 'openregister:solr:manage') - ->setDescription('🔧 SOLR Management - Setup, optimize, and maintain SOLR infrastructure') - ->addArgument( - name: 'action', - mode: InputArgument::REQUIRED, - description: 'Action: setup, optimize, warm, health, schema-check, clear, stats, configure-vectors' - ) - ->addOption( - name: 'tenant-collection', - shortcut: 't', - mode: InputOption::VALUE_OPTIONAL, - description: 'Target specific tenant collection (default: current tenant)' - ) - ->addOption( - name: 'force', - shortcut: 'f', - mode: InputOption::VALUE_NONE, - description: 'Force operation (use with caution)' - ) - ->addOption( - name: 'commit', - shortcut: 'c', - mode: InputOption::VALUE_NONE, - description: 'Commit changes immediately' - ) - ->setHelp( - '🔧 SOLR Management Command - -Available Actions: - setup - Initialize SOLR: create configSets, base collections, tenant collections - optimize - Optimize SOLR index for better performance (production ready) - warm - Warm up SOLR caches with common queries - health - Comprehensive health check of SOLR infrastructure - schema-check - Validate SOLR schema matches ObjectEntity fields - clear - Clear tenant-specific index (with confirmation) - stats - Display detailed SOLR statistics and performance metrics - -Examples: - php occ openregister:solr:manage setup - Initialize complete SOLR infrastructure - - php occ openregister:solr:manage optimize --commit - Optimize index and commit changes - - php occ openregister:solr:manage warm - Warm up SOLR caches for better performance - - php occ openregister:solr:manage health - Run comprehensive health check - - php occ openregister:solr:manage schema-check - Validate schema compatibility with ObjectEntity - - php occ openregister:solr:manage clear --force - Clear index (requires --force for safety) - -Production Notes: - • Run setup once during deployment - • Use optimize during maintenance windows - • warm after cold starts or major updates - • health for monitoring and diagnostics -' - ); - }//end configure() - - /** - * Execute the command - * - * @param InputInterface $input Input interface - * @param OutputInterface $output Output interface - * - * @return int Exit code - * - * @psalm-return 0|1 - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $action = $input->getArgument('action'); - $force = $input->getOption('force'); - $commit = $input->getOption('commit'); - - $output->writeln(''); - $output->writeln('🔧 SOLR Management - OpenRegister Production Tool'); - $output->writeln('================================================'); - - // Check if SOLR is available. - if ($this->solrService->isAvailable() === false) { - $output->writeln('❌ SOLR is not available or not configured'); - $output->writeln(' Configure SOLR in admin settings first'); - return self::FAILURE; - } - - return match ($action) { - 'setup' => $this->handleSetup(output: $output), - 'optimize' => $this->handleOptimize(output: $output, commit: $commit), - 'warm' => $this->handleWarm(output: $output), - 'health' => $this->handleHealth(output: $output), - 'schema-check' => $this->handleSchemaCheck(output: $output), - 'clear' => $this->handleClear(output: $output, force: $force), - 'stats' => $this->handleStats(output: $output), - default => $this->handleInvalidAction(output: $output, action: $action), - }; - }//end execute() - - /** - * Handle SOLR setup - * - * @param OutputInterface $output Output interface - * - * @return int Exit code - * - * @psalm-return 0|1 - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function handleSetup(OutputInterface $output): int - { - $output->writeln('🏗️ Setting up SOLR infrastructure...'); - $output->writeln(''); - - try { - // Test connection first. - $connectionResult = $this->solrService->testConnection(); - if ($connectionResult['success'] === false) { - $output->writeln('❌ SOLR connection failed: '.$connectionResult['message'].''); - return self::FAILURE; - } - - $output->writeln('✅ SOLR connection successful'); - $version = $connectionResult['details']['solr_version'] ?? 'unknown'; - $output->writeln(' Version: '.$version.''); - $output->writeln(' Mode: '.($connectionResult['details']['mode'] ?? 'unknown').''); - $output->writeln(''); - - // Run comprehensive SOLR setup with corrected schema configuration. - $output->writeln('📋 Running comprehensive SOLR setup with corrected schema configuration...'); - $output->writeln(' • Using self_ prefixes for metadata fields'); - $output->writeln(' • Clean field names (no suffixes) with explicit types'); - $output->writeln(' • Single-valued tenant_id field'); - $output->writeln(''); - - // Use the injected solrService instead of creating a new one. - // Initialize SolrSetup with proper configuration. - $solrSetup = new SetupHandler(solrService: $this->solrService, logger: $this->logger); - - // Run complete setup including schema field configuration. - $setupResult = $solrSetup->setupSolr(); - if ($setupResult === true) { - $output->writeln('✅ Base SOLR infrastructure and schema configured'); - $output->writeln(' • ConfigSet: openregister'); - $output->writeln(' • Base collection: openregister'); - $output->writeln(' • Schema fields: 22 ObjectEntity metadata fields'); - $output->writeln(''); - - // Ensure tenant collection. - $output->writeln('🏠 Verifying tenant-specific collection...'); - try { - $this->solrService->ensureTenantCollection(); - $output->writeln('✅ Tenant collection ready with proper schema'); - } catch (\Exception $e) { - $output->writeln('❌ Failed to create tenant collection: '.$e->getMessage().''); - return self::FAILURE; - } - - $docCount = $this->solrService->getDocumentCount(); - $output->writeln(' Document count: '.$docCount.''); - }//end if - - if ($setupResult !== true) { - $output->writeln('❌ SOLR setup failed - check logs for details'); - return self::FAILURE; - }//end if - - $output->writeln(''); - $output->writeln('🎉 SOLR setup completed successfully!'); - $output->writeln(' Your SOLR infrastructure is ready for production use.'); - - return self::SUCCESS; - } catch (\Exception $e) { - $output->writeln('❌ Setup failed: '.$e->getMessage().''); - $this->logger->error( - message: '[SolrManagementCommand] SOLR setup failed', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - return self::FAILURE; - }//end try - }//end handleSetup() - - /** - * Handle index optimization - * - * @param OutputInterface $output Output interface - * @param bool $commit Whether to commit - * - * @return int Exit code - * - * @psalm-return 0|1 - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function handleOptimize(OutputInterface $output, bool $commit): int - { - $output->writeln('⚡ Optimizing SOLR index...'); - $output->writeln(' This may take several minutes for large indexes'); - $output->writeln(''); - - try { - $startTime = microtime(true); - - if ($this->solrService->optimize() === true) { - $executionTime = round((microtime(true) - $startTime) * 1000, 2); - $output->writeln('✅ Index optimization completed'); - $output->writeln(' Execution time: '.$executionTime.'ms'); - - if ($commit === true) { - $output->writeln('💾 Committing changes...'); - if ($this->solrService->commit() === true) { - $output->writeln('✅ Changes committed successfully'); - } - - if ($this->solrService->commit() === false) { - $output->writeln('⚠️ Commit failed, but optimization succeeded'); - } - } - - return self::SUCCESS; - }//end if - - // If optimize failed, return failure. - $output->writeln('❌ Index optimization failed'); - return self::FAILURE; - } catch (\Exception $e) { - $output->writeln('❌ Optimization failed: '.$e->getMessage().''); - return self::FAILURE; - }//end try - }//end handleOptimize() - - /** - * Handle cache warming - * - * @param OutputInterface $output Output interface - * - * @return int Exit code - * - * @psalm-return 0|1 - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function handleWarm(OutputInterface $output): int - { - $output->writeln('🔥 Warming SOLR caches...'); - $output->writeln(''); - - try { - // Common warming queries. - $warmQueries = [ - ['q' => '*:*', 'rows' => 10, 'description' => 'All documents sample'], - [ - 'q' => '*:*', - 'rows' => 0, - 'facet' => 'true', - 'facet.field' => ['register_id', 'schema_id'], - 'description' => 'Facet warming', - ], - ]; - - $successCount = 0; - foreach ($warmQueries as $query) { - $output->write(' 🔥 '.$query['description'].'... '); - - $result = $this->solrService->searchObjects(query: $query); - if ($result['success'] === true) { - $output->writeln(''); - $successCount++; - } - - if ($result['success'] === false) { - $output->writeln(''); - } - } - - $output->writeln(''); - if ($successCount === count($warmQueries)) { - $output->writeln('🔥 Cache warming completed successfully!'); - $output->writeln(' SOLR caches are now pre-loaded for optimal performance.'); - return self::SUCCESS; - } - - $total = count($warmQueries); - $output->writeln("Some warming queries failed ({$successCount}/{$total} successful)"); - return self::FAILURE; - } catch (\Exception $e) { - $output->writeln('❌ Cache warming failed: '.$e->getMessage().''); - return self::FAILURE; - }//end try - }//end handleWarm() - - /** - * Handle health check - * - * @param OutputInterface $output Output interface - * - * @return int Exit code - * - * @psalm-return 0|1 - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function handleHealth(OutputInterface $output): int - { - $output->writeln('🏥 SOLR Health Check'); - $output->writeln(''); - - $issues = 0; - - try { - // Connection test. - $output->writeln('🔗 Testing connection...'); - $connectionResult = $this->solrService->testConnection(); - if ($connectionResult['success'] === true) { - $output->writeln(' ✅ Connection successful ('.$connectionResult['details']['response_time_ms'].'ms)'); - $output->writeln(' 📊 SOLR version: '.$connectionResult['details']['solr_version'].''); - $output->writeln(' 🏗️ Mode: '.$connectionResult['details']['mode'].''); - } - - if ($connectionResult['success'] === false) { - $output->writeln(' ❌ Connection failed: '.$connectionResult['message'].''); - $issues++; - } - - // Collection test. - $output->writeln(''); - $output->writeln('🏠 Testing tenant collection...'); - try { - $this->solrService->ensureTenantCollection(); - $output->writeln(' ✅ Tenant collection accessible'); - - $docCount = $this->solrService->getDocumentCount(); - $output->writeln(' 📊 Document count: '.$docCount.''); - } catch (\Exception $e) { - $output->writeln(' ❌ Tenant collection not accessible: '.$e->getMessage().''); - $issues++; - } - - // Basic search test. - $output->writeln(''); - $output->writeln('🔍 Testing search functionality...'); - $searchResult = $this->solrService->searchObjects(query: ['q' => '*:*', 'rows' => 1]); - if ($searchResult['success'] === true) { - $output->writeln(' ✅ Search working ('.$searchResult['execution_time_ms'].'ms)'); - $output->writeln(' 📊 Total documents: '.$searchResult['total'].''); - } - - if ($searchResult['success'] === false) { - $output->writeln(' ❌ Search failed: '.($searchResult['error'] ?? 'Unknown error').''); - $issues++; - } - - // Service statistics. - $output->writeln(''); - $output->writeln('📊 Service Statistics'); - $stats = $this->solrService->getStats(); - $output->writeln(' 🔍 Searches: '.$stats['searches'].''); - $output->writeln(' 📝 Indexes: '.$stats['indexes'].''); - $output->writeln(' 🗑️ Deletes: '.$stats['deletes'].''); - $output->writeln(' ⚠️ Errors: '.$stats['errors'].''); - - $output->writeln(''); - if ($issues === 0) { - $output->writeln('🎉 All health checks passed! SOLR is healthy.'); - return self::SUCCESS; - } - - $output->writeln('⚠️ Health check found '.$issues.' issues'); - return self::FAILURE; - } catch (\Exception $e) { - $output->writeln('❌ Health check failed: '.$e->getMessage().''); - return self::FAILURE; - }//end try - }//end handleHealth() - - /** - * Handle schema validation - * - * @param OutputInterface $output Output interface - * - * @return int Exit code - * - * @psalm-return 0|1 - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function handleSchemaCheck(OutputInterface $output): int - { - $output->writeln('📋 Validating SOLR schema compatibility with ObjectEntity...'); - $output->writeln(''); - - // Expected fields based on ObjectEntity. - $expectedFields = [ - 'id', - 'uuid', - 'slug', - 'name', - 'description', - 'summary', - 'image', - 'uri', - 'version', - 'register_id', - 'schema_id', - 'organisation_id', - 'created', - 'updated', - 'tenant_id', - '_text_', - // Full-text search field. - ]; - - try { - // Get schema information (this is a simplified check). - $output->writeln('🔍 Checking field compatibility...'); - - // Test a document structure. - $testResult = $this->solrService->searchObjects(query: ['q' => '*:*', 'rows' => 1]); - if ($testResult['success'] === true && empty($testResult['data']) === false) { - $sampleDoc = $testResult['data'][0]; - $availableFields = array_keys($sampleDoc); - - $output->writeln('📊 Available fields in SOLR: '.count($availableFields).''); - $output->writeln('📋 Expected fields: '.count($expectedFields).''); - - $missingFields = array_diff($expectedFields, $availableFields); - $extraFields = array_diff($availableFields, $expectedFields); - - if (empty($missingFields) === true) { - $output->writeln('✅ All expected fields are available'); - } - - if (empty($missingFields) === false) { - $output->writeln('⚠️ Missing fields: '.implode(', ', $missingFields).''); - } - - if (empty($extraFields) === false) { - $extra = implode(', ', array_slice($extraFields, 0, 10)); - $output->writeln('Additional fields: '.$extra.''); - } - }//end if - - if ($testResult['success'] === false || empty($testResult['data']) === true) { - $output->writeln('⚠️ No documents available for schema analysis'); - $output->writeln(' Create some objects first to validate the schema'); - }//end if - - $output->writeln(''); - $output->writeln('✅ Schema compatibility check completed'); - return self::SUCCESS; - } catch (\Exception $e) { - $output->writeln('❌ Schema check failed: '.$e->getMessage().''); - return self::FAILURE; - }//end try - }//end handleSchemaCheck() - - /** - * Handle index clearing - * - * @param OutputInterface $output Output interface - * @param bool $force Force operation - * - * @return int Exit code - * - * @psalm-return 0|1 - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function handleClear(OutputInterface $output, bool $force): int - { - if ($force === false) { - $output->writeln('❌ Clear operation requires --force flag for safety'); - $output->writeln(' This will DELETE ALL indexed documents for current tenant!'); - $output->writeln(' Use: php occ openregister:solr:manage clear --force'); - return self::FAILURE; - } - - $output->writeln('🗑️ Clearing SOLR index...'); - $output->writeln(' ⚠️ This will delete all documents for current tenant!'); - $output->writeln(''); - - try { - $result = $this->solrService->clearIndex(); - if (($result['success'] ?? null) !== null && ($result['success'] === true) === true) { - $output->writeln('✅ Index cleared successfully'); - $output->writeln(' All documents have been removed from the index'); - return self::SUCCESS; - } - - $output->writeln('❌ Failed to clear index'); - return self::FAILURE; - } catch (\Exception $e) { - $output->writeln('❌ Clear operation failed: '.$e->getMessage().''); - return self::FAILURE; - } - }//end handleClear() - - /** - * Handle statistics display - * - * @param OutputInterface $output Output interface - * - * @return int Exit code - * - * @psalm-return 0|1 - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function handleStats(OutputInterface $output): int - { - $output->writeln('📊 SOLR Statistics & Performance Metrics'); - $output->writeln(''); - - try { - $dashboardStats = $this->solrService->getDashboardStats(); - - if ($dashboardStats['available'] === true) { - $output->writeln('🏠 Index Information'); - $output->writeln(' Status: Available'); - - // Display backend stats if available. - if (isset($dashboardStats['backend']) === true && is_array($dashboardStats['backend']) === true) { - $backendStats = $dashboardStats['backend']; - $output->writeln(''); - $output->writeln('⚡ Backend Statistics'); - $output->writeln(' Searches: '.($backendStats['searches'] ?? 0).''); - $output->writeln(' Indexes: '.($backendStats['indexes'] ?? 0).''); - $output->writeln(' Deletes: '.($backendStats['deletes'] ?? 0).''); - $output->writeln(' Errors: '.($backendStats['errors'] ?? 0).''); - if (isset($backendStats['search_time']) === true) { - $searchTime = round((float) $backendStats['search_time'] * 1000, 2); - $output->writeln(' Total search time: '.$searchTime.'ms'); - } - - if (isset($backendStats['index_time']) === true) { - $indexTime = round((float) $backendStats['index_time'] * 1000, 2); - $output->writeln(' Total index time: '.$indexTime.'ms'); - } - } - - // Display file stats if available. - if (isset($dashboardStats['files']) === true && is_array($dashboardStats['files']) === true) { - $output->writeln(''); - $output->writeln('📁 File Statistics'); - foreach ($dashboardStats['files'] as $key => $value) { - $output->writeln(' '.ucfirst((string) $key).': '.$value.''); - } - } - - // Display chunk stats if available. - if (isset($dashboardStats['chunks']) === true && is_array($dashboardStats['chunks']) === true) { - $output->writeln(''); - $output->writeln('📦 Chunk Statistics'); - foreach ($dashboardStats['chunks'] as $key => $value) { - $output->writeln(' '.ucfirst((string) $key).': '.$value.''); - } - } - }//end if - - if ($dashboardStats['available'] === false) { - $errMsg = $dashboardStats['error'] ?? 'Unknown error'; - $output->writeln('SOLR statistics unavailable: '.$errMsg.''); - return self::FAILURE; - }//end if - - return self::SUCCESS; - } catch (\Exception $e) { - $output->writeln('❌ Failed to retrieve statistics: '.$e->getMessage().''); - return self::FAILURE; - }//end try - }//end handleStats() - - /** - * Handle invalid action - * - * @param OutputInterface $output Output interface - * @param string $action Invalid action - * - * @return int Exit code - * - * @psalm-return 1 - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function handleInvalidAction(OutputInterface $output, string $action): int - { - $output->writeln('❌ Invalid action: '.$action.''); - $output->writeln(''); - $output->writeln('Available actions:'); - $output->writeln(' • setup - Initialize SOLR infrastructure'); - $output->writeln(' • optimize - Optimize index for performance'); - $output->writeln(' • warm - Warm up caches'); - $output->writeln(' • health - Run health check'); - $output->writeln(' • schema-check - Validate schema compatibility'); - $output->writeln(' • clear - Clear index (requires --force)'); - $output->writeln(' • stats - Display statistics'); - $output->writeln(''); - $output->writeln('Use --help for detailed information'); - - return self::FAILURE; - }//end handleInvalidAction() -}//end class diff --git a/lib/Command/TablesSyncCommand.php b/lib/Command/TablesSyncCommand.php new file mode 100644 index 0000000000..38932cda1c --- /dev/null +++ b/lib/Command/TablesSyncCommand.php @@ -0,0 +1,151 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Command; + +use OCA\OpenRegister\Service\ObjectSource\TablesSchemaSyncService; +use OCA\OpenRegister\Service\ObjectSource\TablesTableReader; +use OCP\IGroupManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Reconcile the `tables` virtual register with the Nextcloud Tables app. + */ +class TablesSyncCommand extends Command +{ + /** + * Constructor. + * + * @param TablesTableReader $reader Guarded gateway to Tables services. + * @param TablesSchemaSyncService $syncService Schema reconcile logic. + * @param IGroupManager $groupManager Admin-user enumeration. + * + * @return void + */ + public function __construct( + private readonly TablesTableReader $reader, + private readonly TablesSchemaSyncService $syncService, + private readonly IGroupManager $groupManager, + ) { + parent::__construct(); + }//end __construct() + + /** + * Define command name, description, and options. + * + * @return void + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + protected function configure(): void + { + $this->setName(name: 'openregister:tables:sync') + ->setDescription( + 'Reconcile the "tables" virtual register with the Nextcloud Tables app ' + .'(seed a schema per table, refresh columns, retire deleted tables).' + ) + ->addOption( + 'user', + null, + InputOption::VALUE_REQUIRED, + 'Enumerate the tables visible to this user id instead of the admin users.' + ); + }//end configure() + + /** + * Run the reconcile. + * + * @param InputInterface $input Console input. + * @param OutputInterface $output Console output stream. + * + * @return int Symfony command exit code. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + if ($this->reader->isAvailable() === false) { + $output->writeln('Nextcloud Tables app is not enabled — nothing to sync.'); + return Command::SUCCESS; + } + + $userOption = $input->getOption('user'); + $userIds = $this->adminUserIds(); + if ($userOption !== null && $userOption !== '') { + $userIds = [(string) $userOption]; + } + + if (empty($userIds) === true) { + $output->writeln('No user to enumerate Tables for (pass --user=UID).'); + return Command::SUCCESS; + } + + $tables = $this->reader->collectTableDescriptors(userIds: $userIds); + $stats = $this->syncService->reconcile(tables: $tables); + + $output->writeln( + sprintf( + 'Reconciled the "tables" register: seeded=%d, retired=%d, skipped=%d (from %d table(s)).', + $stats['seeded'], + $stats['retired'], + $stats['skipped'], + count($tables) + ) + ); + + return Command::SUCCESS; + }//end execute() + + /** + * Resolve the instance's admin user ids for table enumeration. + * + * @return array The admin user ids (may be empty). + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function adminUserIds(): array + { + $admins = $this->groupManager->get('admin'); + if ($admins === null) { + return []; + } + + $ids = []; + foreach ($admins->getUsers() as $user) { + $ids[] = $user->getUID(); + } + + return $ids; + }//end adminUserIds() +}//end class diff --git a/lib/Command/TimeReconcileCommand.php b/lib/Command/TimeReconcileCommand.php new file mode 100644 index 0000000000..2b84934cb6 --- /dev/null +++ b/lib/Command/TimeReconcileCommand.php @@ -0,0 +1,141 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/integration-time-tracker/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Command; + +use OCA\OpenRegister\Service\TimeTrackerLinkService; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Reconcile every persisted time-tracker link row against its upstream + * NC TimeManager entry — refreshes the denormalised name / duration / + * billable / started_at cache so dashboards can read per-object totals + * from the link table without aggregating across N entries at render + * time (per the spec's "Denormalized Object Total" requirement). + */ +class TimeReconcileCommand extends Command +{ + /** + * Wire the time-tracker link service used by the command. + * + * @param TimeTrackerLinkService $service Tier-2 link-service facade. + * + * @return void + */ + public function __construct( + private readonly TimeTrackerLinkService $service + ) { + parent::__construct(); + }//end __construct() + + /** + * Define command name, description, and options. + * + * @return void + * + * @spec openspec/specs/integration-time-tracker/spec.md + */ + protected function configure(): void + { + $this->setName(name: 'openregister:time:reconcile') + ->setDescription( + 'Reconcile denormalised time-tracker link metadata (name, duration, billable, started_at) ' + .'against the authoritative NC TimeManager source so per-object totals stay correct.' + ) + ->addOption( + 'object', + null, + InputOption::VALUE_REQUIRED, + 'Optionally restrict the scan to a single object uuid; default is every link row.' + ) + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'Report drift without writing the link table.' + ); + }//end configure() + + /** + * Run the reconcile walk and report the summary stats. + * + * @param InputInterface $input Console input. + * @param OutputInterface $output Console output stream. + * + * @return int Symfony command exit code. + * + * @spec openspec/specs/integration-time-tracker/spec.md + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $objectUuid = $input->getOption('object'); + if (is_string($objectUuid) === false || $objectUuid === '') { + $objectUuid = null; + } + + $dryRun = (bool) $input->getOption('dry-run'); + $dryRunLabel = ''; + if ($dryRun === true) { + $dryRunLabel = ' (dry run)'; + } + + $scope = 'all link rows'; + if ($objectUuid !== null) { + $scope = 'object='.$objectUuid; + } + + $output->writeln(sprintf('Reconciling %s%s', $scope, $dryRunLabel)); + + $stats = $this->service->reconcileAllLinks(objectUuid: $objectUuid, dryRun: $dryRun); + + $output->writeln( + sprintf( + ' walked=%d refreshed=%d missing=%d errors=%d', + $stats['walked'], + $stats['refreshed'], + $stats['missing'], + $stats['errors'] + ) + ); + + if ($stats['errors'] > 0) { + return Command::FAILURE; + } + + return Command::SUCCESS; + }//end execute() +}//end class diff --git a/lib/Contacts/ContactsMenuProvider.php b/lib/Contacts/ContactsMenuProvider.php index 42231f0bfe..c550462535 100644 --- a/lib/Contacts/ContactsMenuProvider.php +++ b/lib/Contacts/ContactsMenuProvider.php @@ -103,7 +103,7 @@ class ContactsMenuProvider implements IProvider * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( ContactMatchingService $matchingService, @@ -128,7 +128,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function process(IEntry $entry): void { @@ -152,7 +152,7 @@ public function process(IEntry $entry): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ private function doProcess(IEntry $entry): void { @@ -198,7 +198,7 @@ private function doProcess(IEntry $entry): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ private function injectCountBadge(IEntry $entry, array $matches, string $primaryEmail): void { @@ -238,7 +238,7 @@ private function injectCountBadge(IEntry $entry, array $matches, string $primary * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ private function injectEntityActions( IEntry $entry, diff --git a/lib/Controller/ActionsController.php b/lib/Controller/ActionsController.php index 04dcdf15e2..70750bdf44 100644 --- a/lib/Controller/ActionsController.php +++ b/lib/Controller/ActionsController.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ declare(strict_types=1); @@ -44,9 +44,14 @@ * * @psalm-suppress UnusedClass * - * @SuppressWarnings(PHPMD.TooManyPublicMethods) Actions CRUD exposes index/show/create/update/patch/destroy/test/logs/migrateFromHooks — each maps to a distinct REST verb or utility route required by the Actions feature; collapsing routes would break the REST contract. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Controller composes ActionMapper + ActionLogMapper + ActionService + IUserSession + IGroupManager + LoggerInterface; each dependency serves a distinct responsibility (persistence, logging, business logic, auth) and cannot be removed without losing functionality. - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complexity is spread across 9 thin public action methods; each method is independently simple. PHPMD accumulates per-method scores into the class total. + * @SuppressWarnings(PHPMD.TooManyPublicMethods) Actions CRUD exposes + * index/show/create/update/patch/destroy/test/logs/migrateFromHooks — each maps to a distinct REST + * verb or utility route required by the Actions feature; collapsing routes would break the REST contract. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Controller composes ActionMapper + ActionLogMapper + + * ActionService + IUserSession + IGroupManager + LoggerInterface; each dependency serves a distinct + * responsibility (persistence, logging, business logic, auth) and cannot be removed without losing functionality. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complexity is spread across 9 thin public action + * methods; each method is independently simple. PHPMD accumulates per-method scores into the class total. */ class ActionsController extends Controller { @@ -124,7 +129,7 @@ public function __construct( * * @return JSONResponse|null 403 response when not admin, null when allowed. * - * @spec openspec/changes/retrofit-2026-05-24-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ private function requireAdmin(): ?JSONResponse { @@ -156,11 +161,15 @@ private function requireAdmin(): ?JSONResponse * * @NoCSRFRequired * - * @SuppressWarnings(PHPMD.NPathComplexity) index() handles limit/offset/page/search/filters in one pass to avoid a second DB round-trip; extracting each branch into helpers would add indirection without reducing total paths. - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Branches cover five independent optional query parameters (limit, offset, page, search, field filters); each is a single isset/cast guard and cannot be split without duplicating the parameter-parsing logic. + * @SuppressWarnings(PHPMD.NPathComplexity) index() handles limit/offset/page/search/filters in one + * pass to avoid a second DB round-trip; extracting each branch into helpers would add indirection + * without reducing total paths. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Branches cover five independent optional query + * parameters (limit, offset, page, search, field filters); each is a single isset/cast guard and + * cannot be split without duplicating the parameter-parsing logic. * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-05-24-actions/tasks.md#task-5 + * @spec openspec/specs/actions/spec.md + * @spec openspec/specs/actions/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -267,7 +276,7 @@ function ($action) { * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -300,7 +309,7 @@ public function show(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ #[NoCSRFRequired] public function create(): JSONResponse @@ -354,7 +363,7 @@ public function create(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ #[NoCSRFRequired] public function update(int $id): JSONResponse @@ -403,7 +412,7 @@ public function update(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ #[NoCSRFRequired] public function patch(int $id): JSONResponse @@ -424,7 +433,7 @@ public function patch(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ #[NoCSRFRequired] public function destroy(int $id): JSONResponse @@ -463,7 +472,7 @@ public function destroy(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-actions/tasks.md#task-2 + * @spec openspec/specs/actions/spec.md */ #[NoCSRFRequired] public function test(int $id): JSONResponse @@ -509,7 +518,7 @@ public function test(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-actions/tasks.md#task-4 + * @spec openspec/specs/actions/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -569,7 +578,7 @@ function ($log) { * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-actions/tasks.md#task-3 + * @spec openspec/specs/actions/spec.md */ #[NoCSRFRequired] public function migrateFromHooks(int $schemaId): JSONResponse diff --git a/lib/Controller/ActivityLinksController.php b/lib/Controller/ActivityLinksController.php index 680ef57e07..86fb7173cc 100644 --- a/lib/Controller/ActivityLinksController.php +++ b/lib/Controller/ActivityLinksController.php @@ -80,7 +80,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -127,7 +127,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function types(): JSONResponse { @@ -144,7 +144,7 @@ public function types(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function actors(): JSONResponse { diff --git a/lib/Controller/AgentsController.php b/lib/Controller/AgentsController.php index dd551159bb..b88302607f 100644 --- a/lib/Controller/AgentsController.php +++ b/lib/Controller/AgentsController.php @@ -19,8 +19,8 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-4 - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-5 + * @spec openspec/specs/chat-ai/spec.md + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); @@ -59,7 +59,7 @@ * * @psalm-suppress UnusedClass * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ class AgentsController extends Controller { @@ -145,32 +145,6 @@ public function __construct( $this->userId = $userId; }//end __construct() - /** - * Render the Agents page - * - * Returns the template for the main agents page. - * All routing is handled client-side by the SPA. - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @return TemplateResponse Template response for agents SPA - * - * @psalm-return TemplateResponse<200, array> - * - * @spec exclude SPA-mount stub — returns the Vue `index` template; client-side router owns navigation. No HTTP contract beyond the shell. - */ - public function page(): TemplateResponse - { - // Return SPA template response (routing handled client-side). - return new TemplateResponse( - appName: 'openregister', - templateName: 'index', - params: [] - ); - }//end page() - /** * Get all agents accessible by current user * @@ -185,7 +159,7 @@ public function page(): TemplateResponse * @psalm-return JSONResponse<200|500, array{error?: 'Failed to retrieve agents', * results?: array}, array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-4 + * @spec openspec/specs/chat-ai/spec.md */ public function index(): JSONResponse { @@ -270,7 +244,7 @@ public function index(): JSONResponse * array{error: 'Access denied to this agent'|'Agent not found'}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-4 + * @spec openspec/specs/chat-ai/spec.md */ public function show(int $id): JSONResponse { @@ -320,7 +294,7 @@ public function show(int $id): JSONResponse * * @return JSONResponse JSON response with created agent * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-4 + * @spec openspec/specs/chat-ai/spec.md */ public function create(): JSONResponse { @@ -408,7 +382,7 @@ public function create(): JSONResponse * * @return JSONResponse JSON response with updated agent * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-4 + * @spec openspec/specs/chat-ai/spec.md */ public function update(int $id): JSONResponse { @@ -491,7 +465,7 @@ public function update(int $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-4 + * @spec openspec/specs/chat-ai/spec.md */ public function patch(int $id): JSONResponse { @@ -518,7 +492,7 @@ public function patch(int $id): JSONResponse * 'User not authenticated', message?: 'Agent deleted successfully'}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-4 + * @spec openspec/specs/chat-ai/spec.md */ public function destroy(int $id): JSONResponse { @@ -584,7 +558,7 @@ public function destroy(int $id): JSONResponse * array{error?: 'Failed to retrieve statistics', total?: int, * active?: int, inactive?: int}, array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-5 + * @spec openspec/specs/chat-ai/spec.md */ public function stats(): JSONResponse { @@ -626,12 +600,14 @@ public function stats(): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Capability discovery: returns the static tool-registry metadata (ToolRegistry::getAllTools); + * no caller-supplied object id. * * @return JSONResponse List of available tools with metadata * * @psalm-return JSONResponse<200|500, array{error?: 'Failed to retrieve tools', results?: array}, array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-4 + * @spec openspec/specs/chat-ai/spec.md */ public function tools(): JSONResponse { diff --git a/lib/Controller/AggregationController.php b/lib/Controller/AggregationController.php index b944c6fb25..c158fb995c 100644 --- a/lib/Controller/AggregationController.php +++ b/lib/Controller/AggregationController.php @@ -32,9 +32,9 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-3 + * @spec openspec/specs/aggregations-backend-native/spec.md + * @spec openspec/specs/aggregations-backend-native/spec.md + * @spec openspec/specs/aggregations-backend-native/spec.md * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 */ @@ -44,6 +44,7 @@ use InvalidArgumentException; use OCA\OpenRegister\Exception\NotAuthorizedException; +use OCA\OpenRegister\Service\Aggregation\AggregationQuery; use OCA\OpenRegister\Service\Aggregation\AggregationRunner; use OCA\OpenRegister\Service\Aggregation\TimeseriesRequestValidator; use OCP\AppFramework\Controller; @@ -112,6 +113,130 @@ public function aggregate(string $register, string $schema, string $name): JSONR return $response; }//end aggregate() + /** + * Ad-hoc single-value aggregation entry point. + * + * The non-bucketed sibling of {@see timeseries()} — returns one scalar + * `value` for manifest-configured KPI widgets (nextcloud-vue CnStatWidget). + * RBAC + multi-tenancy are enforced inside AggregationRunner::runAdhocByRef. + * + * Accepts: metric (default count), field (required for non-count), + * filter[...] (operator-aware). The literal `value` URL never collides with + * the named `{name}` aggregate route. + * + * @param string $register Register reference. + * @param string $schema Schema reference. + * + * @return JSONResponse JSON response with the scalar aggregation value. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 + */ + public function value(string $register, string $schema): JSONResponse + { + $metric = (string) $this->request->getParam('metric', 'count'); + $field = $this->request->getParam('field'); + $filter = (array) ($this->request->getParam('filter', [])); + + $resolvedField = $field; + if ($field === '') { + $resolvedField = null; + } + + try { + $query = AggregationQuery::create( + metric: $metric, + field: $resolvedField, + filter: $filter + ); + } catch (InvalidArgumentException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } + + try { + $result = $this->runner->runAdhocByRef(registerRef: $register, schemaRef: $schema, query: $query); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($result); + + }//end value() + + /** + * Ad-hoc categorical group-by aggregation entry point. + * + * The non-time sibling of {@see timeseries()} — groups by a NON-date field + * and returns `{ groups: [{ key, value }] }` for manifest-configured + * category charts (nextcloud-vue CnChartWidget group-by source). + * + * Accepts: groupBy (required field), metric (default count), field + * (required for non-count), sort (asc|desc), limit (top-N), filter[...]. + * + * @param string $register Register reference. + * @param string $schema Schema reference. + * + * @return JSONResponse JSON response with `{ groups, backend, cached }`. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 + */ + public function grouped(string $register, string $schema): JSONResponse + { + $metric = (string) $this->request->getParam('metric', 'count'); + $field = $this->request->getParam('field'); + $groupBy = (string) $this->request->getParam('groupBy', ''); + $filter = (array) ($this->request->getParam('filter', [])); + + if ($groupBy === '') { + return new JSONResponse(['error' => 'groupBy is required'], Http::STATUS_BAD_REQUEST); + } + + $groupSpec = ['field' => $groupBy]; + $sort = $this->request->getParam('sort'); + if ($sort === 'asc' || $sort === 'desc') { + $groupSpec['sort'] = $sort; + } + + $limit = $this->request->getParam('limit'); + if ($limit !== null && (string) $limit !== '' && (int) $limit > 0) { + $groupSpec['limit'] = (int) $limit; + } + + $resolvedField = $field; + if ($field === '') { + $resolvedField = null; + } + + try { + $query = AggregationQuery::create( + metric: $metric, + field: $resolvedField, + filter: $filter, + groupBy: $groupSpec + ); + } catch (InvalidArgumentException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } + + try { + $result = $this->runner->runAdhocByRef(registerRef: $register, schemaRef: $schema, query: $query); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($result); + + }//end grouped() + /** * Ad-hoc time-bucket aggregation entry point. * diff --git a/lib/Controller/AnalyticsLinksController.php b/lib/Controller/AnalyticsLinksController.php index a31a6bd45a..4e4142fc14 100644 --- a/lib/Controller/AnalyticsLinksController.php +++ b/lib/Controller/AnalyticsLinksController.php @@ -270,6 +270,8 @@ public function destroy(string $register, string $schema, string $id, string $re * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Capability/availability probe: reports whether the companion Analytics integration is available + * and lists report types; no caller-supplied per-object resource. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-1 */ diff --git a/lib/Controller/AnalyticsSeriesController.php b/lib/Controller/AnalyticsSeriesController.php new file mode 100644 index 0000000000..7602471252 --- /dev/null +++ b/lib/Controller/AnalyticsSeriesController.php @@ -0,0 +1,162 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\AnalyticsSeriesService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Page-level analytics series controller. + */ +class AnalyticsSeriesController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App name (injected by NC). + * @param IRequest $request Current request. + * @param AnalyticsSeriesService $seriesService Series register/fetch service. + * + * @return void + */ + public function __construct( + string $appName, + IRequest $request, + private AnalyticsSeriesService $seriesService, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * POST /api/integrations/analytics/series + * + * Register or refresh a page-level series. Body: + * - seriesKey (string, required) + * - labels (array) + * - datasets (array) + * - title (string, optional) + * - chartType (string, optional, default 'line') + * - visibility (string, optional, default 'private') + * - registerId (int, optional) + * - schemaId (int, optional) + * + * @NoAdminRequired + * + * @return JSONResponse The stored series render contract, or 400. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + #[NoAdminRequired] + public function register(): JSONResponse + { + $params = $this->request->getParams(); + + try { + // Authz guard (visible at the endpoint boundary): registering + // a series is a write — it requires an authenticated user. + $this->seriesService->ensureCanRegister(); + + $title = null; + if (isset($params['title']) === true) { + $title = (string) $params['title']; + } + + $stored = $this->seriesService->register( + seriesKey: (string) ($params['seriesKey'] ?? ''), + labels: (array) ($params['labels'] ?? []), + datasets: (array) ($params['datasets'] ?? []), + title: $title, + chartType: (string) ($params['chartType'] ?? 'line'), + visibility: (string) ($params['visibility'] ?? 'private'), + registerId: $this->intOrNull(value: ($params['registerId'] ?? null)), + schemaId: $this->intOrNull(value: ($params['schemaId'] ?? null)) + ); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + }//end try + + return new JSONResponse($stored, Http::STATUS_CREATED); + }//end register() + + /** + * GET /api/integrations/analytics/series/{seriesKey} + * + * Fetch a registered series, RBAC-scoped. Returns 404 when unknown + * or not readable by the current user. + * + * @param string $seriesKey The series key. + * + * @NoAdminRequired + * + * @return JSONResponse The series render contract, or 404. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + #[NoAdminRequired] + public function fetch(string $seriesKey): JSONResponse + { + // Authz guard (visible at the endpoint boundary): the read is + // RBAC-scoped inside the service; a disallowed/unknown read + // returns null → uniform 404 (no enumeration oracle). + $series = $this->seriesService->ensureReadableOrNull($seriesKey); + if ($series === null) { + return new JSONResponse(['message' => 'Not Found'], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($series); + }//end fetch() + + /** + * Coerce a request param to an int, or null when absent / non-numeric. + * + * @param mixed $value The raw param. + * + * @return int|null + */ + private function intOrNull(mixed $value): ?int + { + if ($value === null || is_numeric($value) === false) { + return null; + } + + return (int) $value; + }//end intOrNull() +}//end class diff --git a/lib/Controller/AnonymisationBackendController.php b/lib/Controller/AnonymisationBackendController.php new file mode 100644 index 0000000000..7794dca50c --- /dev/null +++ b/lib/Controller/AnonymisationBackendController.php @@ -0,0 +1,136 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\Anonymisation\AnonymisationBackendService; +use OCA\OpenRegister\Service\Anonymisation\BackendState; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IUserSession; + +/** + * Admin-only endpoints exposing anonymisation backend state and probes. + * + * @category Controller + * @package OCA\OpenRegister\Controller + */ +class AnonymisationBackendController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The app name. + * @param IRequest $request The request. + * @param IUserSession $userSession Current user session for admin gating. + * @param IGroupManager $groupManager Group manager for admin checks. + * @param AnonymisationBackendService $service Backend state service (single source of truth). + */ + public function __construct( + $appName, + IRequest $request, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, + private readonly AnonymisationBackendService $service, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Get the resolved anonymisation backend state. + * + * Admin-only: the absence of `@NoAdminRequired` makes Nextcloud's + * SecurityMiddleware reject non-admins before the method runs; the + * `requireAdmin()` body check remains as defense-in-depth (returns a + * JSON 403 rather than the framework's default response). + * + * @NoCSRFRequired + * + * @return JSONResponse The serialised BackendState, or 403 for non-admins. + * + * @psalm-return JSONResponse<200|403, array, array> + */ + public function getBackendState(): JSONResponse + { + $forbidden = $this->requireAdmin(); + if ($forbidden !== null) { + return $forbidden; + } + + return new JSONResponse(data: $this->service->getState()->jsonSerialize()); + }//end getBackendState() + + /** + * Probe a single backend, bypassing the cache. + * + * Admin-only: the absence of `@NoAdminRequired` makes Nextcloud's + * SecurityMiddleware reject non-admins before the method runs; the + * `requireAdmin()` body check remains as defense-in-depth (returns a + * JSON 403 rather than the framework's default response). + * + * @NoCSRFRequired + * + * @return JSONResponse The serialised ProbeResult, 400 for an invalid method, or 403 for non-admins. + * + * @psalm-return JSONResponse<200|400|403, array, array> + */ + public function testConnection(): JSONResponse + { + $forbidden = $this->requireAdmin(); + if ($forbidden !== null) { + return $forbidden; + } + + $method = (string) $this->request->getParam('method', ''); + if (in_array($method, BackendState::METHODS, true) === false) { + return new JSONResponse( + data: ['error' => 'Invalid method; expected one of: '.implode(', ', BackendState::METHODS)], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + return new JSONResponse(data: $this->service->testConnection(method: $method)->jsonSerialize()); + }//end testConnection() + + /** + * Return a 403 response when the current user is not an admin, else null. + * + * Unauthenticated requests are rejected with HTTP 401 by the framework + * before this method runs (the route is not public). + * + * @return JSONResponse|null A forbidden response, or null when the user is an admin. + * + * @psalm-return JSONResponse<403, array{error: string}, array>|null + */ + private function requireAdmin(): ?JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null || $this->groupManager->isAdmin($user->getUID()) === false) { + return new JSONResponse( + data: ['error' => 'Administrator privileges are required to access anonymisation backend settings'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + + return null; + }//end requireAdmin() +}//end class diff --git a/lib/Controller/ApplicationsController.php b/lib/Controller/ApplicationsController.php index bd650b68c3..33de98d14a 100644 --- a/lib/Controller/ApplicationsController.php +++ b/lib/Controller/ApplicationsController.php @@ -51,7 +51,7 @@ * * @psalm-suppress UnusedClass * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ class ApplicationsController extends Controller { @@ -157,7 +157,12 @@ public function page(): TemplateResponse * results?: array<\OCA\OpenRegister\Db\Application>}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md + * + * @no-admin-idor-exempt List endpoint for the application-registry resource (app/template + * metadata, not per-user objects); access follows OpenRegister's fleet-wide registry-controller + * convention where authorisation lives in the ObjectService/RBAC layer behind ApplicationService, + * not per-controller-method. Pre-existing; this PR only touches the docblock, not the handler. */ public function index(): JSONResponse { @@ -235,7 +240,13 @@ public function index(): JSONResponse * array>|JSONResponse<404, * array{error: 'Application not found'}, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec exclude Thin read endpoint for the applications resource route; delegates to + * ApplicationService (domain layer), not a raw ObjectService duplication. gate-17's + * receiver regex over-matches any $this->*Service call. + * + * @no-admin-idor-exempt Read of an application-registry entry (app/template metadata); + * authorisation follows OpenRegister's registry-controller convention (ObjectService/RBAC + * behind ApplicationService::find). Pre-existing; untouched by this PR's functional diff. */ public function show(int $id): JSONResponse { @@ -280,7 +291,11 @@ public function show(int $id): JSONResponse * * @return JSONResponse JSON response with created application * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md + * + * @no-admin-idor-exempt Write to the application-registry resource (app/template metadata); + * authorisation follows OpenRegister's registry-controller convention (ObjectService/RBAC + * behind ApplicationService::create). Pre-existing; untouched by this PR's functional diff. */ public function create(): JSONResponse { @@ -331,7 +346,12 @@ public function create(): JSONResponse * * @return JSONResponse JSON response with updated application * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md + * + * @no-admin-idor-exempt Update of an application-registry entry (app/template metadata); + * authorisation follows OpenRegister's registry-controller convention (ObjectService/RBAC + * behind ApplicationService::update). Immutable fields (id/organisation/owner/created) are + * stripped here. Pre-existing; untouched by this PR's functional diff. */ public function update(int $id): JSONResponse { @@ -391,7 +411,10 @@ public function update(int $id): JSONResponse * array>|JSONResponse<400, array{error: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md + * + * @no-admin-idor-exempt Pure delegation to update(), which carries the same registry-controller + * authorisation posture; this method has no body of its own. Pre-existing; untouched by this PR. */ public function patch(int $id): JSONResponse { @@ -417,7 +440,11 @@ public function patch(int $id): JSONResponse * array{error?: 'Failed to delete application', * message?: 'Application deleted successfully'}, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md + * + * @no-admin-idor-exempt Delete of an application-registry entry (app/template metadata); + * authorisation follows OpenRegister's registry-controller convention (ObjectService/RBAC + * behind ApplicationService::delete). Pre-existing; untouched by this PR's functional diff. */ public function destroy(int $id): JSONResponse { diff --git a/lib/Controller/ApprovalController.php b/lib/Controller/ApprovalController.php index 43aac56fc1..8ccf046cc4 100644 --- a/lib/Controller/ApprovalController.php +++ b/lib/Controller/ApprovalController.php @@ -17,10 +17,10 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-5 + * @spec openspec/specs/approval-workflow/spec.md + * @spec openspec/specs/approval-workflow/spec.md + * @spec openspec/specs/approval-workflow/spec.md + * @spec openspec/specs/approval-workflow/spec.md */ declare(strict_types=1); @@ -94,7 +94,7 @@ private function isCurrentUserAdmin(): bool * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-1 + * @spec openspec/specs/approval-workflow/spec.md */ public function index(): JSONResponse { @@ -114,7 +114,7 @@ public function index(): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-1 + * @spec openspec/specs/approval-workflow/spec.md */ public function show(int $id): JSONResponse { @@ -132,7 +132,7 @@ public function show(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-1 + * @spec openspec/specs/approval-workflow/spec.md * * @NoAdminRequired */ @@ -161,7 +161,7 @@ public function create(): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-1 + * @spec openspec/specs/approval-workflow/spec.md * * @NoAdminRequired */ @@ -191,7 +191,7 @@ public function update(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-1 + * @spec openspec/specs/approval-workflow/spec.md * * @NoAdminRequired */ @@ -221,7 +221,7 @@ public function destroy(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-2 + * @spec openspec/specs/approval-workflow/spec.md */ public function objects(int $id): JSONResponse { @@ -263,8 +263,8 @@ public function objects(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-05-24-b3a-workflow-seed/tasks.md#task-1 + * @spec openspec/specs/approval-workflow/spec.md + * @spec openspec/specs/approval-workflow/spec.md */ public function steps(): JSONResponse { @@ -306,7 +306,7 @@ public function steps(): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-5 + * @spec openspec/specs/approval-workflow/spec.md */ public function approve(int $id): JSONResponse { @@ -345,7 +345,7 @@ public function approve(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-5 + * @spec openspec/specs/approval-workflow/spec.md */ public function reject(int $id): JSONResponse { diff --git a/lib/Controller/ArchivalController.php b/lib/Controller/ArchivalController.php index 5d5ae87281..7e053cb201 100644 --- a/lib/Controller/ArchivalController.php +++ b/lib/Controller/ArchivalController.php @@ -20,13 +20,13 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-6 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-7 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ namespace OCA\OpenRegister\Controller; @@ -143,8 +143,8 @@ public function __construct( * * @NoAdminRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-6 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function listDestructionLists(): JSONResponse { @@ -176,8 +176,8 @@ public function listDestructionLists(): JSONResponse * * @NoAdminRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-7 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function getDestructionList(string $id): JSONResponse { @@ -209,8 +209,8 @@ public function getDestructionList(string $id): JSONResponse * * @NoAdminRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function approveDestructionList(string $id): JSONResponse { @@ -236,7 +236,8 @@ public function approveDestructionList(string $id): JSONResponse action: $action, excludedIds: $excludedIds, exclusionReasons: $exclusionReasons, - requiresDual: $requiresDual + requiresDual: $requiresDual, + listUuid: $id ); // Check if dual approval was rejected (same user). @@ -249,6 +250,14 @@ public function approveDestructionList(string $id): JSONResponse ); } + // Persist the approval. Without this the recorded approval and the + // `approved` status were returned to the caller but never written back, + // so the list stayed `in_review` in storage and the execution job — which + // reloads the list by uuid and requires status `approved` — refused to run + // (openregister#393). RetentionController has always persisted here. + $object->setObject($result); + $this->objectMapper->update($object); + return new JSONResponse( data: $result, statusCode: Http::STATUS_OK @@ -279,8 +288,8 @@ public function approveDestructionList(string $id): JSONResponse * * @NoAdminRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function rejectDestructionList(string $id): JSONResponse { @@ -333,8 +342,8 @@ public function rejectDestructionList(string $id): JSONResponse * * @NoAdminRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function createLegalHold(): JSONResponse { @@ -422,8 +431,8 @@ public function createLegalHold(): JSONResponse * * @NoAdminRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function releaseLegalHold(string $id): JSONResponse { @@ -469,8 +478,8 @@ public function releaseLegalHold(string $id): JSONResponse * * @NoAdminRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function listLegalHolds(): JSONResponse { @@ -496,8 +505,8 @@ public function listLegalHolds(): JSONResponse * * @NoAdminRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-3 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function listCertificates(): JSONResponse { @@ -522,7 +531,7 @@ public function listCertificates(): JSONResponse * * @return JSONResponse|null Returns a 403 response if unauthorized, null if authorized. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function checkArchivistRole(): ?JSONResponse { diff --git a/lib/Controller/AuditQueryController.php b/lib/Controller/AuditQueryController.php new file mode 100644 index 0000000000..8c7f2f9ca4 --- /dev/null +++ b/lib/Controller/AuditQueryController.php @@ -0,0 +1,237 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-2.1 + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-2.2 + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-3.1 + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-3.2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use DateTime; +use OCA\OpenRegister\Service\AuditQueryService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\DataDownloadResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IUserSession; + +/** + * Handles GET /api/v2/audit and GET /api/v2/audit/export. + * + * Admin-only: neither action carries `@NoAdminRequired`, so Nextcloud's + * framework-level default (admin required) applies; `requireAdmin()` is a + * defence-in-depth body-level check mirroring {@see AuditTrailController}, + * this app's existing admin-gated audit surface. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-2.1 + */ +class AuditQueryController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The app id. + * @param IRequest $request The request object. + * @param AuditQueryService $auditQueryService Cross-schema audit-entry query service. + * @param IUserSession $userSession Active user session for the admin gate. + * @param IGroupManager $groupManager Group manager for the admin gate. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly AuditQueryService $auditQueryService, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager + ) { + parent::__construct(appName: $appName, request: $request); + + }//end __construct() + + /** + * Gate every action on this controller on NC-admin membership. + * + * @return JSONResponse|null 401 when anonymous, 403 when non-admin, null when allowed. + */ + private function requireAdmin(): ?JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse( + data: ['error' => 'Authentication required'], + statusCode: 401 + ); + } + + if ($this->groupManager->isAdmin($user->getUID()) === false) { + return new JSONResponse( + data: ['error' => 'Forbidden: this endpoint is admin-only'], + statusCode: 403 + ); + } + + return null; + + }//end requireAdmin() + + /** + * Extract the audit-query filters from the request. + * + * @return array + */ + private function extractFilters(): array + { + $filters = []; + foreach (['registerId', 'schemaId', 'objectId', 'app', 'timestampStart', 'timestampEnd', 'sort'] as $key) { + $value = $this->request->getParam($key); + if ($value !== null && $value !== '') { + $filters[$key] = $value; + } + } + + return $filters; + + }//end extractFilters() + + /** + * `GET /api/v2/audit` — query audit-entry objects across all apps/schemas. + * + * @return JSONResponse Response shaped `{entries, total, limit, offset}`. + * + * @NoCSRFRequired + * + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-2.1 + */ + public function query(): JSONResponse + { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + + $filters = $this->extractFilters(); + $limit = (int) $this->request->getParam('limit', '50'); + $offset = (int) $this->request->getParam('offset', '0'); + + $result = $this->auditQueryService->query(filters: $filters, limit: $limit, offset: $offset); + + return new JSONResponse(data: $result); + + }//end query() + + /** + * `GET /api/v2/audit/export` — export the same query as CSV (default) or JSON. + * + * @return JSONResponse|DataDownloadResponse CSV download, or JSON body when `?format=json`. + * + * @NoCSRFRequired + * + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-2.1 + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-3.1 + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-3.2 + */ + public function export(): JSONResponse|DataDownloadResponse + { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + + $filters = $this->extractFilters(); + $format = (string) $this->request->getParam('format', 'csv'); + // Export defaults to the maximum page size (still clamped by the + // service to [1, 200]) so a single call carries as much of the + // filtered result set as the clamp allows. + $limit = (int) $this->request->getParam('limit', '200'); + $offset = (int) $this->request->getParam('offset', '0'); + + $result = $this->auditQueryService->query(filters: $filters, limit: $limit, offset: $offset); + $entries = $result['entries']; + + if (strtolower($format) === 'json') { + return new JSONResponse( + data: [ + 'entries' => $entries, + 'total' => $result['total'], + 'limit' => $result['limit'], + 'offset' => $result['offset'], + ] + ); + } + + $csv = $this->buildCsvFromAuditEntries(entries: $entries); + $filename = sprintf('audit-export_%s.csv', (new DateTime())->format('Y-m-d_His')); + + return new DataDownloadResponse($csv, $filename, 'text/csv'); + + }//end export() + + /** + * Flatten audit entries into a CSV string. + * + * Columns: id, registerId, schemaId, objectId, data (JSON), created, userId. + * + * @param array> $entries Entries as returned by AuditQueryService::query(). + * + * @return string The CSV document (including header row). + * + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-3.1 + */ + private function buildCsvFromAuditEntries(array $entries): string + { + $handle = fopen('php://temp', 'r+'); + + fputcsv($handle, ['id', 'registerId', 'schemaId', 'objectId', 'data', 'created', 'userId']); + + foreach ($entries as $entry) { + fputcsv( + $handle, + [ + ($entry['id'] ?? ''), + ($entry['registerId'] ?? ''), + ($entry['schemaId'] ?? ''), + ($entry['objectId'] ?? ''), + json_encode($entry['data'] ?? []), + ($entry['created'] ?? ''), + ($entry['userId'] ?? ''), + ] + ); + } + + rewind($handle); + $csv = stream_get_contents($handle); + fclose($handle); + + if ($csv === false) { + return ''; + } + + return $csv; + + }//end buildCsvFromAuditEntries() +}//end class diff --git a/lib/Controller/AuditTrailController.php b/lib/Controller/AuditTrailController.php index 226588e17d..52e97323c2 100644 --- a/lib/Controller/AuditTrailController.php +++ b/lib/Controller/AuditTrailController.php @@ -21,13 +21,13 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-12 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-15 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-17 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-81 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-82 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-83 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/audit-hash-chain/spec.md + * @spec openspec/specs/audit-trail-immutable/spec.md + * @spec openspec/specs/audit-trail-immutable/spec.md + * @spec openspec/specs/verwerkingsregister-api/spec.md + * @spec openspec/specs/verwerkingsregister-api/spec.md + * @spec openspec/specs/verwerkingsregister-api/spec.md */ namespace OCA\OpenRegister\Controller; @@ -130,7 +130,7 @@ private function requireAdmin(): ?JSONResponse * two alternative parameter names (with and without underscore prefix); the resulting * if/else-if pairs are required to preserve backward compatibility with both formats. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining */ private function extractRequestParameters(): array { @@ -277,8 +277,8 @@ function ($key) use ($systemKeys) { * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-17 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/audit-trail-immutable/spec.md */ public function index(): JSONResponse { @@ -326,8 +326,8 @@ public function index(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/audit-trail-immutable/spec.md */ public function show(int $id): JSONResponse { @@ -354,10 +354,13 @@ public function show(int $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * + * @no-admin-idor-exempt Immutability stub: returns HTTP 405 unconditionally and + * performs no object read or write, so there is no per-object resource to guard. + * * @SuppressWarnings(PHPMD.UnusedFormalParameter) $id is required by the OCP Controller * route contract; the method intentionally ignores it to enforce immutability. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining */ public function update(int $id): JSONResponse { @@ -370,12 +373,16 @@ public function update(int $id): JSONResponse /** * Get logs for an object * + * Admin-only at the framework level (no @NoAdminRequired): a per-object + * audit trail records actor UID, IP address and per-field diffs. Returning + * it for an arbitrary object id leaks cross-tenant PII (wave-3 C7), so it + * stays admin-only like index()/show()/export(). Body `requireAdmin()` is + * defence-in-depth. + * * @param string $register The register identifier * @param string $schema The schema identifier * @param string $id The object ID * - * @NoAdminRequired - * * @return JSONResponse JSON response containing audit trails for specific object * * @NoCSRFRequired @@ -386,10 +393,15 @@ public function update(int $id): JSONResponse * total?: int<0, max>, page?: int|null, pages?: float, limit?: int, * offset?: int|null}, array> * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining */ public function objects(string $register, string $schema, string $id): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + // Extract common parameters. $params = $this->extractRequestParameters(); @@ -433,8 +445,8 @@ public function objects(string $register, string $schema, string $id): JSONRespo * * @return JSONResponse JSON response with export data or error * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function export(): JSONResponse { @@ -508,10 +520,13 @@ public function export(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * + * @no-admin-idor-exempt Immutability stub: returns HTTP 405 unconditionally and + * performs no object read or write, so there is no per-object resource to guard. + * * @SuppressWarnings(PHPMD.UnusedFormalParameter) $id is required by the OCP Controller * route contract; the method intentionally ignores it to enforce immutability. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining */ public function destroy(int $id): JSONResponse { @@ -529,7 +544,10 @@ public function destroy(int $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 + * @no-admin-idor-exempt Immutability stub: returns HTTP 405 unconditionally and + * performs no object read or write, so there is no per-object resource to guard. + * + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining */ public function destroyMultiple(): JSONResponse { @@ -549,8 +567,8 @@ public function destroyMultiple(): JSONResponse * * @return JSONResponse JSON response confirming clear or error * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-05-24-audit-trail-immutable/tasks.md#task-2 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/audit-trail-immutable/spec.md */ public function clearAll(): JSONResponse { @@ -596,16 +614,25 @@ public function clearAll(): JSONResponse /** * Verify the integrity of the audit trail hash chain. * - * @NoAdminRequired + * Admin-only at the framework level (no @NoAdminRequired): validates the + * tenant-wide audit hash chain across all registers/schemas — a GDPR + * chain-of-trust management surface, like export()/clearAll(). Body + * `requireAdmin()` is defence-in-depth. + * * @NoCSRFRequired * * @return JSONResponse Verification result with valid/invalid status * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-12 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/audit-hash-chain/spec.md */ public function verify(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + $from = $this->request->getParam('from'); $to = $this->request->getParam('to'); @@ -645,8 +672,8 @@ public function verify(): JSONResponse * (@NoAdminRequired, @NoCSRFRequired, @psalm-return) inflates the reported line * count; the actual executable body is 12 lines. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-83 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function verwerkingsregister(): JSONResponse { @@ -674,8 +701,8 @@ public function verwerkingsregister(): JSONResponse * * @return JSONResponse Matching audit trail entries grouped by schema * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function inzageverzoek(): JSONResponse { diff --git a/lib/Controller/BookmarkLinksController.php b/lib/Controller/BookmarkLinksController.php index 76badcf57d..6310af0d48 100644 --- a/lib/Controller/BookmarkLinksController.php +++ b/lib/Controller/BookmarkLinksController.php @@ -73,7 +73,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -114,7 +114,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function link(string $register, string $schema, string $id): JSONResponse { @@ -167,7 +167,7 @@ public function link(string $register, string $schema, string $id): JSONResponse * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function createNew(string $register, string $schema, string $id): JSONResponse { @@ -237,7 +237,7 @@ public function createNew(string $register, string $schema, string $id): JSONRes * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroy(string $register, string $schema, string $id, string $bookmarkId): JSONResponse { @@ -274,8 +274,9 @@ public function destroy(string $register, string $schema, string $id, string $bo * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns bookmarks visible to the current user via the Bookmarks app; no caller-supplied object id. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function available(): JSONResponse { diff --git a/lib/Controller/BulkController.php b/lib/Controller/BulkController.php index 59e5742ec4..32bb928754 100644 --- a/lib/Controller/BulkController.php +++ b/lib/Controller/BulkController.php @@ -20,8 +20,8 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-10 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/object-lifecycle/spec.md */ namespace OCA\OpenRegister\Controller; @@ -39,6 +39,7 @@ use OCP\IGroupManager; use OCP\IRequest; use OCP\IUserSession; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Exception; /** @@ -249,7 +250,7 @@ private function resolveRegisterSchemaIds(string $register, string $schema, Obje * * @return JSONResponse JSON response with bulk delete result * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function delete(string $register, string $schema): JSONResponse { @@ -325,7 +326,7 @@ public function delete(string $register, string $schema): JSONResponse * saved_count?: mixed, saved_objects?: array, * requested_count?: int<0, max>}, array> * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function save(string $register, string $schema): JSONResponse { @@ -341,6 +342,33 @@ public function save(string $register, string $schema): JSONResponse return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: Http::STATUS_NOT_FOUND); } + // AUTHORIZATION (wave-11 WF1 / wave-3 C4 pattern): bulk-writing objects is a + // potentially high-impact write that any authenticated user could otherwise use + // to spray validation work or flood the audit trail. Gate on manage-permission + // for the target schema (default-SECURE: admin-only when no manage rule exists). + // Mixed-schema (schema=0) bulk operations skip this gate because there is no + // single schema entity to check against — the per-object RBAC flag (_rbac:true) + // still runs inside saveObjects for individual object writes. + $isMixedSchema = ($resolved['schema'] === 0); + + if ($isMixedSchema === false) { + try { + $schemaEntityForGate = $this->schemaMapper->find((int) $resolved['schema']); + } catch (\Throwable $e) { + return new JSONResponse( + data: ['error' => 'Schema not found'], + statusCode: Http::STATUS_NOT_FOUND + ); + } + + if ($this->checkSchemaManagePermission(schema: $schemaEntityForGate) === false) { + return new JSONResponse( + data: ['error' => 'User does not have permission to bulk-write objects on this schema'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + }//end if + // Get request data. $data = $this->request->getParams(); $objects = $data['objects'] ?? []; @@ -353,10 +381,6 @@ public function save(string $register, string $schema): JSONResponse ); } - // FLEXIBLE SCHEMA HANDLING: Support both single-schema and mixed-schema operations. - // Use schema=0 to indicate mixed-schema operations where objects specify their own schemas. - $isMixedSchema = ($resolved['schema'] === 0); - // Determine schema to use (null for mixed-schema, resolved for single-schema). $schemaToUse = $resolved['schema']; if ($isMixedSchema === true) { @@ -384,6 +408,18 @@ public function save(string $register, string $schema): JSONResponse 'requested_count' => count($objects), ] ); + } catch (UniqueConstraintViolationException $e) { + // WF3 (wave-11): a client-supplied UUID collided with an existing row at a + // non-upsert boundary (e.g. unique-slug constraint, or concurrent insert race). + // Surface a human-readable 422 instead of leaking a raw DBAL trace. + return new JSONResponse( + data: [ + 'error' => 'One or more objects contain a UUID or unique field that conflicts with an existing record.', + 'error_code' => 'uuid_conflict', + 'detail' => $e->getMessage(), + ], + statusCode: Http::STATUS_UNPROCESSABLE_ENTITY + ); } catch (Exception $e) { return new JSONResponse( data: ['error' => 'Bulk save operation failed: '.$e->getMessage()], @@ -395,6 +431,11 @@ public function save(string $register, string $schema): JSONResponse /** * Delete all objects belonging to a specific schema * + * Despite its name and its route (`/api/bulk/{register}/{schema}/delete-schema`), + * this endpoint does NOT delete the schema — it deletes the schema's OBJECTS. It + * is a near-duplicate of deleteSchemaObjects(), differing only in that it requires + * numeric ids instead of resolving slugs. + * * @param string $register The register identifier * @param string $schema The schema identifier * @@ -403,7 +444,12 @@ public function save(string $register, string $schema): JSONResponse * * @return JSONResponse JSON response with schema delete result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-15 + * @deprecated Use bulk#deleteSchemaObjects (`/api/bulk/{register}/{schema}/delete-objects`) + * to delete a schema's objects, or `DELETE /api/schemas/{id}?deleteObjects=true` + * to delete the schema AND its objects. Retained only for API back-compat; + * the misleading name is documented rather than changed. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md */ public function deleteSchema(string $register, string $schema): JSONResponse { @@ -436,9 +482,11 @@ public function deleteSchema(string $register, string $schema): JSONResponse ); } - // Get request data. + // Get request data. A JSON body delivers a real bool, but a form/query + // request delivers the string "true" — which would be a TypeError against + // the bool-typed service parameter, so normalize here. $data = $this->request->getParams(); - $hardDelete = $data['hardDelete'] ?? false; + $hardDelete = filter_var(($data['hardDelete'] ?? false), FILTER_VALIDATE_BOOLEAN); // Set register and schema context. $this->objectService->setRegister($register); @@ -521,9 +569,11 @@ public function deleteSchemaObjects(string $register, string $schema): JSONRespo ); } - // Get request data. + // Get request data. A JSON body delivers a real bool, but a form/query + // request delivers the string "true" — which would be a TypeError against + // the bool-typed service parameter, so normalize here. $data = $this->request->getParams(); - $hardDelete = $data['hardDelete'] ?? false; + $hardDelete = filter_var(($data['hardDelete'] ?? false), FILTER_VALIDATE_BOOLEAN); // Set register and schema context using resolved IDs. $this->objectService->setRegister((string) $resolved['register']); @@ -631,7 +681,7 @@ public function deleteRegister(string $register): JSONResponse * * @return JSONResponse JSON response with validation result * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-10 + * @spec openspec/specs/data-import-export/spec.md */ public function runSchemaValidation(string $schema): JSONResponse { diff --git a/lib/Controller/CalendarEventsController.php b/lib/Controller/CalendarEventsController.php index 7f4b364677..f8a108683a 100644 --- a/lib/Controller/CalendarEventsController.php +++ b/lib/Controller/CalendarEventsController.php @@ -105,7 +105,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -139,7 +139,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function create(string $register, string $schema, string $id): JSONResponse { @@ -189,7 +189,7 @@ public function create(string $register, string $schema, string $id): JSONRespon * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function link(string $register, string $schema, string $id): JSONResponse { @@ -243,7 +243,7 @@ public function link(string $register, string $schema, string $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function unlink(string $register, string $schema, string $id, string $eventUid): JSONResponse { @@ -285,7 +285,7 @@ public function unlink(string $register, string $schema, string $id, string $eve * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function destroy(string $register, string $schema, string $id, string $eventId): JSONResponse { @@ -337,8 +337,9 @@ public function destroy(string $register, string $schema, string $id, string $ev * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns only the current user's VEVENT-capable calendars; no caller-supplied object id. * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function listCalendars(): JSONResponse { @@ -359,8 +360,11 @@ public function listCalendars(): JSONResponse * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: CalendarLinkService resolves the caller-supplied calendarUri only against calendars + * scoped to the session user's principal (getCalendarsForUser); a non-owned URI resolves to null and throws, never leaking + * another user's calendar. * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function listCalendarEvents(string $calendarUri): JSONResponse { diff --git a/lib/Controller/CaseTokenController.php b/lib/Controller/CaseTokenController.php new file mode 100644 index 0000000000..2d7741cf41 --- /dev/null +++ b/lib/Controller/CaseTokenController.php @@ -0,0 +1,108 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\CaseTokenService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Public case-token resolve controller. + */ +class CaseTokenController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App name (injected by NC). + * @param IRequest $request Current request. + * @param CaseTokenService $tokenService Case-token mint/resolve/revoke service. + * + * @return void + */ + public function __construct( + string $appName, + IRequest $request, + private CaseTokenService $tokenService, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * GET /api/public/case-tokens/{token} + * + * Resolves a public case token to an RBAC-scoped, public-safe view of + * the referenced object. Fails closed (404) on any unresolved token. + * + * @param string $token The opaque public token. + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @return JSONResponse The public-safe object view, or 404. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + #[PublicPage] + #[NoCSRFRequired] + public function resolve(string $token): JSONResponse + { + $resolved = $this->tokenService->resolve($token); + if ($resolved === null) { + // Uniform 404 — never distinguish "unknown" from + // "revoked / expired / forbidden". No enumeration oracle. + return new JSONResponse( + ['message' => 'Not Found'], + Http::STATUS_NOT_FOUND + ); + } + + return new JSONResponse($resolved); + }//end resolve() +}//end class diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 5b0400f42b..f5e272fbeb 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -19,11 +19,11 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md + * @spec openspec/specs/chat-ai/spec.md + * @spec openspec/specs/chat-ai/spec.md + * @spec openspec/specs/chat-ai/spec.md + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Controller; @@ -39,6 +39,7 @@ use OCA\OpenRegister\Db\FeedbackMapper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IL10N; use OCP\IRequest; use Psr\Log\LoggerInterface; @@ -230,7 +231,7 @@ public function __construct( * ragSettings: array{includeObjects: bool|mixed, includeFiles: bool|mixed, * numSourcesFiles: int|mixed, numSourcesObjects: int|mixed}} * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function extractMessageRequestParams(): array { @@ -292,7 +293,7 @@ private function extractMessageRequestParams(): array * * @throws Exception If conversation not found * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function loadExistingConversation(string $uuid): Conversation { @@ -312,7 +313,7 @@ private function loadExistingConversation(string $uuid): Conversation * * @throws Exception If agent not found * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function createNewConversation(string $agentUuid): Conversation { @@ -367,7 +368,7 @@ private function createNewConversation(string $agentUuid): Conversation * * @throws Exception If both parameters are empty or if entities not found * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function resolveConversation(string $conversationUuid, string $agentUuid): Conversation { @@ -394,7 +395,7 @@ private function resolveConversation(string $conversationUuid, string $agentUuid * * @throws Exception If user does not have access (403) * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function verifyConversationAccess(Conversation $conversation): void { @@ -415,7 +416,7 @@ private function verifyConversationAccess(Conversation $conversation): void * * @return JSONResponse JSON response with AI response or error * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-1 + * @spec openspec/specs/chat-ai/spec.md */ public function sendMessage(): JSONResponse { @@ -527,7 +528,7 @@ public function sendMessage(): JSONResponse * uuid: null|string}>, total?: int, conversationId?: int}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-3 + * @spec openspec/specs/chat-ai/spec.md */ public function getHistory(): JSONResponse { @@ -618,7 +619,7 @@ function ($msg) { * 'Missing conversationId', message: string, conversationId?: int}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function clearHistory(): JSONResponse { @@ -707,7 +708,7 @@ public function clearHistory(): JSONResponse * * @suppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-5 + * @spec openspec/specs/chat-ai/spec.md */ public function sendFeedback(string $conversationUuid, int $messageId): JSONResponse { @@ -834,9 +835,23 @@ public function sendFeedback(string $conversationUuid, int $messageId): JSONResp /** * Get chat statistics * + * Statistics are scoped to the current session's active organisation so + * that one tenant never sees another tenant's agent/conversation/message + * counts. When there is no active organisation (e.g. a CLI/system + * context), no organisation filter is applied — the same "filter only + * if provided" convention used by ConversationMapper::countByUser() and + * ConversationMapper::findByUser() above. + * + * Messages have no organisation column of their own (see + * openregister_messages schema), so they are scoped indirectly via the + * conversation_id foreign key to the conversations already scoped to + * the active organisation. + * * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Guarded in-body: scopes every count to the caller's active organisation (OrganisationService::getActiveOrganisation) + * via an organisation = ? filter on agents and conversations; no caller-supplied object id. * * @return JSONResponse Chat statistics * @@ -845,28 +860,88 @@ public function sendFeedback(string $conversationUuid, int $messageId): JSONResp * total_agents?: int, total_conversations?: int, total_messages?: int}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-5 + * @spec openspec/specs/chat-ai/spec.md */ public function getChatStats(): JSONResponse { try { - // Get agent count. + // Resolve the active organisation once; every count below is + // scoped to it, mirroring the OrganisationService pattern used + // in createNewConversation() and sendFeedback() above. + $organisation = $this->organisationService->getActiveOrganisation(); + $organisationUuid = $organisation?->getUuid(); + + // No resolvable organisation means there is nothing this caller + // may count: return zeros rather than falling back to unscoped, + // instance-wide totals (multi-tenant leak, + // or-chat-engine-decommission REQ "Usage statistics are + // organisation-scoped, never instance-wide"). + if ($organisationUuid === null) { + return new JSONResponse( + data: [ + 'total_agents' => 0, + 'total_conversations' => 0, + 'total_messages' => 0, + ], + statusCode: 200 + ); + } + + // Get agent count, scoped to the active organisation. $qb = $this->db->getQueryBuilder(); $qb->select($qb->func()->count('id', 'total')) - ->from('openregister_agents'); + ->from('openregister_agents') + ->where( + $qb->expr()->eq('organisation', $qb->createNamedParameter($organisationUuid, IQueryBuilder::PARAM_STR)) + ); + $totalAgents = (int) $qb->executeQuery()->fetchOne(); - // Get conversation count. + // Get conversation count, scoped to the active organisation. $qb = $this->db->getQueryBuilder(); $qb->select($qb->func()->count('id', 'total')) - ->from('openregister_conversations'); + ->from('openregister_conversations') + ->where( + $qb->expr()->eq('organisation', $qb->createNamedParameter($organisationUuid, IQueryBuilder::PARAM_STR)) + ); + $totalConversations = (int) $qb->executeQuery()->fetchOne(); - // Get message count. + // Collect the ids of conversations scoped to the active + // organisation so messages (no organisation column of their + // own) can be scoped via their conversation_id foreign key. $qb = $this->db->getQueryBuilder(); - $qb->select($qb->func()->count('id', 'total')) - ->from('openregister_messages'); - $totalMessages = (int) $qb->executeQuery()->fetchOne(); + $qb->select('id') + ->from('openregister_conversations') + ->where( + $qb->expr()->eq('organisation', $qb->createNamedParameter($organisationUuid, IQueryBuilder::PARAM_STR)) + ); + + $result = $qb->executeQuery(); + $conversationIds = []; + while (($row = $result->fetch()) !== false) { + $conversationIds[] = (int) $row['id']; + } + + $result->closeCursor(); + + // Get message count, scoped to the conversation ids collected above. + // An empty id list means no conversations matched, so the message + // count is trivially 0 — skip the query rather than issuing an + // invalid empty IN (...) clause. + $totalMessages = 0; + if (empty($conversationIds) === false) { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('id', 'total')) + ->from('openregister_messages') + ->where( + $qb->expr()->in( + 'conversation_id', + $qb->createNamedParameter($conversationIds, IQueryBuilder::PARAM_INT_ARRAY) + ) + ); + $totalMessages = (int) $qb->executeQuery()->fetchOne(); + } return new JSONResponse( data: [ diff --git a/lib/Controller/ChatHealthController.php b/lib/Controller/ChatHealthController.php index eb2ef890c9..f59fd18935 100644 --- a/lib/Controller/ChatHealthController.php +++ b/lib/Controller/ChatHealthController.php @@ -22,7 +22,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#health-probe-endpoint-get-apichathealth + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); @@ -103,7 +103,7 @@ public function __construct( * * @return JSONResponse 200 or 503 JSON response * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#health-probe-endpoint-get-apichathealth + * @spec openspec/specs/chat-ai/spec.md */ #[PublicPage] #[NoCSRFRequired] diff --git a/lib/Controller/ChatStreamController.php b/lib/Controller/ChatStreamController.php index 59ccad9439..4794db3ab9 100644 --- a/lib/Controller/ChatStreamController.php +++ b/lib/Controller/ChatStreamController.php @@ -29,8 +29,8 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#sse-streaming-endpoint-post-apichatstream - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); @@ -65,8 +65,11 @@ * @category Controller * @package OCA\OpenRegister\Controller * - * @psalm-suppress UnusedClass - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) SSE controller composes ChatService + ConversationMapper + AgentMapper + IUserSession + IDBConnection + LoggerInterface; each handles a separate concern of the streaming pipeline (auth, DB commit safety, conversation routing, message processing) and cannot be removed without losing functionality. + * @psalm-suppress UnusedClass + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) SSE controller composes ChatService + + * ConversationMapper + AgentMapper + IUserSession + IDBConnection + LoggerInterface; each handles a + * separate concern of the streaming pipeline (auth, DB commit safety, conversation routing, message + * processing) and cannot be removed without losing functionality. */ class ChatStreamController extends Controller { @@ -199,11 +202,18 @@ public function __construct( * * @return Response Never returned — emitAndExit() always terminates. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) stream() is the single SSE dispatch path: auth check, body parse, agent/conversation resolution, heartbeat, channel wiring, and error handling are sequential guards that cannot be split into sub-methods without leaking the exit-based flow control across multiple layers. - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple independent early-exit paths (unauthenticated, missing message, missing agent, forbidden conversation, stream failure) must all terminate via emitAndExit(); collapsing them would hide the distinct SSE error codes. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) The method coordinates the full SSE lifecycle in one readable sequence; extracting sub-stages would split the exit-based control flow across multiple methods and make the error-path analysis harder. - * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#sse-streaming-endpoint-post-apichatstream + * @SuppressWarnings(PHPMD.CyclomaticComplexity) stream() is the single SSE dispatch path: auth + * check, body parse, agent/conversation resolution, heartbeat, channel wiring, and error handling + * are sequential guards that cannot be split into sub-methods without leaking the exit-based flow + * control across multiple layers. + * @SuppressWarnings(PHPMD.NPathComplexity) Multiple independent early-exit paths (unauthenticated, + * missing message, missing agent, forbidden conversation, stream failure) must all terminate via + * emitAndExit(); collapsing them would hide the distinct SSE error codes. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) The method coordinates the full SSE lifecycle in + * one readable sequence; extracting sub-stages would split the exit-based control flow across + * multiple methods and make the error-path analysis harder. + * + * @spec openspec/specs/chat-ai/spec.md */ #[NoAdminRequired] public function stream(): Response @@ -397,7 +407,9 @@ public function stream(): Response * * @return never * - * @SuppressWarnings(PHPMD.ExitExpression) SSE streaming contract requires process termination after the final frame; exit cannot be replaced by a return because the calling loop in stream() must stop unconditionally. + * @SuppressWarnings(PHPMD.ExitExpression) SSE streaming contract requires process termination after + * the final frame; exit cannot be replaced by a return because the calling loop in stream() must + * stop unconditionally. * * @spec exclude SSE-framing helper: emits one frame, finalises the DB transaction, and exits; * the streaming endpoint contract is owned by @@ -540,14 +552,26 @@ protected function clearOutputBuffers(): void }//end clearOutputBuffers() /** - * Emit the three SSE response headers. Extracted so tests can skip - * (header() emits a "headers already sent" warning under PHPUnit - * because the test runner has already written output). + * Emit the three SSE response headers, plus the or-chat-proxy-deprecation + * compat-window headers. Extracted so tests can skip (header() emits a + * "headers already sent" warning under PHPUnit because the test runner + * has already written output). + * + * The three deprecation headers are added here — rather than relying on + * ChatCompatMiddleware::afterController() the way every other + * chat-family controller does — because this method always terminates + * via `exit;` (see stream()'s docblock) and bypasses the AppFramework + * Response/middleware pipeline entirely on the local-serving path. + * ChatCompatMiddleware still covers the *proxied* path (its + * beforeController() redirect short-circuits before stream() ever runs, + * so afterController() applies there as normal); this is only the + * local-serving fallback. * * @return void * - * @spec exclude SSE-framing helper: emits the three text/event-stream response headers; - * the streaming endpoint contract is owned by + * @spec exclude SSE-framing helper: emits the three text/event-stream response headers + * plus the or-chat-proxy-deprecation compat headers; the streaming endpoint + * contract is owned by * ai-chat-companion-orchestrator/specs/chat-ai/spec.md#sse-streaming-endpoint-post-apichatstream. */ protected function emitSseHeaders(): void @@ -555,6 +579,10 @@ protected function emitSseHeaders(): void header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('X-Accel-Buffering: no'); + // Or-chat-proxy-deprecation compat window — see ChatCompatMiddleware. + header('Deprecation: Mon, 06 Jul 2026 00:00:00 GMT'); + header('Sunset: Wed, 06 Jan 2027 00:00:00 GMT'); + header('Link: ; rel="successor-version"'); }//end emitSseHeaders() /** @@ -613,7 +641,7 @@ private function pickFallbackAgentForUser(string $userId): string * stream() entry point translates this into the * `forbidden` SSE error. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function resolveConversation(string $conversationUuid, string $agentUuid, string $userId): Conversation { diff --git a/lib/Controller/CollectiveLinksController.php b/lib/Controller/CollectiveLinksController.php index 3e24a75ea9..65497df8f8 100644 --- a/lib/Controller/CollectiveLinksController.php +++ b/lib/Controller/CollectiveLinksController.php @@ -55,9 +55,9 @@ class CollectiveLinksController extends Controller /** * Constructor. * - * @param string $appName App id. - * @param IRequest $request HTTP request. - * @param CollectiveLinkService $linkService Backing service. + * @param string $appName App id. + * @param IRequest $request HTTP request. + * @param CollectiveLinkService $linkService Backing service. * @param ObjectService $objectService OR object resolver. */ public function __construct( @@ -270,6 +270,7 @@ public function destroy(string $register, string $schema, string $id, string $pa * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped capability probe: reports Collectives availability for the current user; no caller-supplied object id. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-1 */ @@ -298,6 +299,7 @@ public function available(): JSONResponse * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns the current user's own Collectives; no caller-supplied object id. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-1 */ diff --git a/lib/Controller/CompanyLookupController.php b/lib/Controller/CompanyLookupController.php new file mode 100644 index 0000000000..bd3c845b57 --- /dev/null +++ b/lib/Controller/CompanyLookupController.php @@ -0,0 +1,200 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\Integration\Providers\KvkProvider; +use OCA\OpenRegister\Service\Integration\Providers\OpenCorporatesProvider; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Read-only company-lookup controller (KvK + OpenCorporates leaves). + * + * @category Controller + * @package OCA\OpenRegister\Controller + */ +class CompanyLookupController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App id. + * @param IRequest $request HTTP request. + * @param KvkProvider $kvkProvider KvK lookup leaf. + * @param OpenCorporatesProvider $openCorporatesProvider OpenCorporates search leaf. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly KvkProvider $kvkProvider, + private readonly OpenCorporatesProvider $openCorporatesProvider, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Look up a single company by its KvK number. + * + * Query param: `kvkNumber` (8-digit KvK number, required). + * + * @return JSONResponse `{ results, total }` on success, or a 503 with + * `details.cause` when the `kvk` source is + * unconfigured / KvK is down (AD-23). + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt External-gateway proxy: forwards a KVK number to the admin-configured external KVK + * provider; takes no OpenRegister object id. + * + * @spec openspec/changes/integration-kvk-opencorporates/specs/integration-company-lookup/spec.md + */ + public function kvkCompany(): JSONResponse + { + $kvkNumber = trim((string) $this->request->getParam('kvkNumber', '')); + if ($kvkNumber === '') { + return new JSONResponse(['error' => 'kvkNumber is required'], 400); + } + + $result = $this->kvkProvider->lookupByKvkNumber($kvkNumber); + + return $this->relay(result: $result, sourceLabel: 'kvk'); + }//end kvkCompany() + + /** + * Free-text company search against the KvK Handelsregister. + * + * Query params: `q` (company-name query), `limit`, `page`, plus optional + * KvK criteria (`plaats`, `type`, `sbiHoofdActiviteit`) passed through. + * + * @return JSONResponse `{ results, total, limit, page }` on success, or + * a 503 with `details.cause` when unconfigured/down. + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt External-gateway proxy: free-text company search against the admin-configured KVK + * provider; takes no OpenRegister object id. + * + * @spec openspec/changes/integration-kvk-opencorporates/specs/integration-company-lookup/spec.md + */ + public function kvkSearch(): JSONResponse + { + $query = (string) $this->request->getParam('q', ''); + $limit = (int) $this->request->getParam('limit', 25); + $page = (int) $this->request->getParam('page', 1); + + $criteria = []; + foreach (['plaats', 'type', 'sbiHoofdActiviteit'] as $key) { + $value = $this->request->getParam($key); + if ($value !== null && (string) $value !== '') { + $criteria[$key] = (string) $value; + } + } + + $result = $this->kvkProvider->searchCompanies($query, $criteria, $limit, $page); + + return $this->relay(result: $result, sourceLabel: 'kvk'); + }//end kvkSearch() + + /** + * Free-text company search against the OpenCorporates register. + * + * Query params: `q` (company-name query), `jurisdiction` (ISO code, + * optional), `limit`, `page`. + * + * @return JSONResponse `{ results, total, limit, page }` on success, or + * a 503 with `details.cause` when unconfigured/down. + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt External-gateway proxy: free-text company search against the admin-configured + * OpenCorporates provider; takes no OpenRegister object id. + * + * @spec openspec/changes/integration-kvk-opencorporates/specs/integration-company-lookup/spec.md + */ + public function openCorporatesSearch(): JSONResponse + { + $query = (string) $this->request->getParam('q', ''); + + $jurisdiction = $this->request->getParam('jurisdiction'); + if ($jurisdiction !== null) { + $jurisdiction = (string) $jurisdiction; + } + + $limit = (int) $this->request->getParam('limit', 30); + $page = (int) $this->request->getParam('page', 1); + + $result = $this->openCorporatesProvider->searchCompanies($query, $jurisdiction, $limit, $page); + + return $this->relay(result: $result, sourceLabel: 'opencorporates'); + }//end openCorporatesSearch() + + /** + * Relay a provider lookup result as a JSONResponse. + * + * A degraded `{ unavailable: true, cause }` descriptor is surfaced as a + * 503 carrying `details.cause` so the consuming app's 4-state banner can + * render (AD-23); a successful envelope is returned as-is (200). + * + * @param array $result The provider lookup result. + * @param string $sourceLabel Leaf id (for the error message). + * + * @return JSONResponse + * + * @spec exclude Private helper: maps the provider's degraded descriptor to a + * 503-with-cause; the lookup REST contract is owned by + * integration-kvk-opencorporates/specs/integration-company-lookup/spec.md. + */ + private function relay(array $result, string $sourceLabel): JSONResponse + { + if (($result['unavailable'] ?? false) === true) { + return new JSONResponse( + [ + 'error' => sprintf('%s source is not available', $sourceLabel), + 'details' => ['cause' => $result['cause']], + ], + 503 + ); + } + + return new JSONResponse($result); + }//end relay() +}//end class diff --git a/lib/Controller/ConfigurationController.php b/lib/Controller/ConfigurationController.php index 2a98b02258..88c47d8799 100644 --- a/lib/Controller/ConfigurationController.php +++ b/lib/Controller/ConfigurationController.php @@ -30,6 +30,7 @@ use OCA\OpenRegister\Service\Configuration\GitHubHandler; use OCA\OpenRegister\Service\Configuration\GitLabHandler; use OCA\OpenRegister\Service\NotificationService; +use OCA\OpenRegister\Service\SecurityService; use OCP\App\IAppManager; use DateTime; use stdClass; @@ -59,6 +60,7 @@ */ class ConfigurationController extends Controller { + use \OCA\OpenRegister\Controller\Trait\HandlesExceptionsTrait; /** * Configuration mapper instance. @@ -174,7 +176,7 @@ private function isCurrentUserAdmin(): bool * * @psalm-return JSONResponse<200|500, array<'Failed to fetch configurations'|Configuration>, array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function index(): JSONResponse { @@ -211,10 +213,15 @@ public function index(): JSONResponse * array{error: 'Configuration not found'|'Failed to fetch configuration'}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function show(int $id): JSONResponse { + // SEC-CTRL-3: prevent IDOR — only admins may read a configuration by id. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $configuration = $this->configurationMapper->find($id); @@ -243,10 +250,17 @@ public function show(int $id): JSONResponse * * @return JSONResponse JSON response with enriched configuration details * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function enrichDetails(): JSONResponse { + // SEC-CTRL: admin-only — reaches external repo content via the + // instance-wide admin-configured GitHub/GitLab credential (matches the + // guard on this controller's create/update/import/publish siblings). + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $data = $this->request->getParams(); $source = strtolower($data['source'] ?? 'github'); @@ -313,7 +327,7 @@ public function enrichDetails(): JSONResponse ] ); - return new JSONResponse(data: ['error' => 'Failed to enrich configuration: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to enrich configuration'); }//end try }//end enrichDetails() @@ -326,7 +340,7 @@ public function enrichDetails(): JSONResponse * * @return JSONResponse JSON response with created configuration * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function create(): JSONResponse { @@ -382,7 +396,7 @@ public function create(): JSONResponse ] ); - return new JSONResponse(data: ['error' => 'Failed to create configuration: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to create configuration'); }//end try }//end create() @@ -399,7 +413,7 @@ public function create(): JSONResponse * * @SuppressWarnings(PHPMD.NPathComplexity) Already refactored — NPath from try/catch + field mapping * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function update(int $id): JSONResponse { @@ -439,10 +453,7 @@ public function update(int $id): JSONResponse ] ); - return new JSONResponse( - data: ['error' => 'Failed to update configuration: '.$e->getMessage()], - statusCode: 500 - ); + return $this->errorResponse(e: $e, context: 'Failed to update configuration'); }//end try }//end update() @@ -504,7 +515,7 @@ private function applyConfigurationUpdates(Configuration $configuration, array $ * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function destroy(int $id): JSONResponse { @@ -551,7 +562,7 @@ public function destroy(int $id): JSONResponse * * @return JSONResponse JSON response with version comparison * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function versionStatus(int $id): JSONResponse { @@ -580,7 +591,7 @@ public function versionStatus(int $id): JSONResponse ] ); - return new JSONResponse(data: ['error' => 'Failed to fetch remote version: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to fetch remote version'); } catch (Exception $e) { $this->logger->error( message: "[ConfigurationController] Failed to check version for configuration {$id}: ".$e->getMessage(), @@ -608,10 +619,15 @@ public function versionStatus(int $id): JSONResponse * * @return JSONResponse JSON response with configuration preview * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function preview(int $id): JSONResponse { + // SEC-CTRL-3: prevent IDOR — only admins may preview a configuration by id. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $configuration = $this->configurationMapper->find($id); @@ -696,7 +712,7 @@ public function import(int $id): JSONResponse context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => 'Failed to import configuration: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to import configuration'); }//end try }//end import() @@ -717,6 +733,12 @@ public function import(int $id): JSONResponse */ public function export(int $id): JSONResponse { + // SEC-CTRL-3: prevent IDOR — export (incl. ?includeObjects=true) serialises a + // configuration's objects; restrict to admins to block bulk exfiltration by id. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $configuration = $this->configurationMapper->find($id); $data = $this->request->getParams(); @@ -738,7 +760,7 @@ public function export(int $id): JSONResponse context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => 'Failed to export configuration: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to export configuration'); }//end try }//end export() @@ -768,6 +790,13 @@ public function export(int $id): JSONResponse */ public function discover(): JSONResponse { + // SEC-CTRL: admin-only — reaches external repo content via the + // instance-wide admin-configured GitHub/GitLab credential (matches the + // guard on this controller's create/update/import/publish siblings). + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $data = $this->request->getParams(); $source = strtolower($data['source'] ?? 'github'); @@ -833,10 +862,7 @@ public function discover(): JSONResponse ] ); - return new JSONResponse( - data: ['error' => 'Failed to discover configurations: '.$e->getMessage()], - statusCode: 500 - ); + return $this->errorResponse(e: $e, context: 'Failed to discover configurations'); }//end try }//end discover() @@ -855,6 +881,13 @@ public function discover(): JSONResponse */ public function getGitHubBranches(): JSONResponse { + // SEC-CTRL: admin-only — reaches external repo content via the + // instance-wide admin-configured GitHub/GitLab credential (matches the + // guard on this controller's create/update/import/publish siblings). + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $data = $this->request->getParams(); $owner = $data['owner'] ?? ''; @@ -883,7 +916,7 @@ public function getGitHubBranches(): JSONResponse context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => 'Failed to fetch branches: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to fetch branches'); }//end try }//end getGitHubBranches() @@ -896,10 +929,17 @@ public function getGitHubBranches(): JSONResponse * * @return JSONResponse JSON response with repositories list * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function getGitHubRepositories(): JSONResponse { + // SEC-CTRL: admin-only — enumerates repositories reachable by the + // instance-wide admin-configured GitHub credential (matches the guard + // on this controller's create/update/import/publish siblings). + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $data = $this->request->getParams(); $page = 1; @@ -934,7 +974,7 @@ public function getGitHubRepositories(): JSONResponse context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => 'Failed to fetch repositories: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to fetch repositories'); }//end try }//end getGitHubRepositories() @@ -949,10 +989,17 @@ public function getGitHubRepositories(): JSONResponse * * @return JSONResponse JSON response with configuration files * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function getGitHubConfigurations(): JSONResponse { + // SEC-CTRL: admin-only — reaches external repo content via the + // instance-wide admin-configured GitHub credential (matches the guard + // on this controller's create/update/import/publish siblings). + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $data = $this->request->getParams(); $owner = $data['owner'] ?? ''; @@ -983,7 +1030,7 @@ public function getGitHubConfigurations(): JSONResponse context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => 'Failed to fetch configurations: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to fetch configurations'); }//end try }//end getGitHubConfigurations() @@ -1002,6 +1049,13 @@ public function getGitHubConfigurations(): JSONResponse */ public function getGitLabBranches(): JSONResponse { + // SEC-CTRL: admin-only — reaches external repo content via the + // instance-wide admin-configured GitLab credential (matches the guard + // on this controller's create/update/import/publish siblings). + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $data = $this->request->getParams(); $namespace = $data['namespace'] ?? ''; @@ -1035,7 +1089,7 @@ public function getGitLabBranches(): JSONResponse context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => 'Failed to fetch branches: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to fetch branches'); }//end try }//end getGitLabBranches() @@ -1050,10 +1104,17 @@ public function getGitLabBranches(): JSONResponse * * @return JSONResponse JSON response with configuration files * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function getGitLabConfigurations(): JSONResponse { + // SEC-CTRL: admin-only — reaches external repo content via the + // instance-wide admin-configured GitLab credential (matches the guard + // on this controller's create/update/import/publish siblings). + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { $data = $this->request->getParams(); $namespace = $data['namespace'] ?? ''; @@ -1089,7 +1150,7 @@ public function getGitLabConfigurations(): JSONResponse context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => 'Failed to fetch configurations: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e, context: 'Failed to fetch configurations'); }//end try }//end getGitLabConfigurations() @@ -1200,50 +1261,19 @@ private function fetchConfigFromUrl(array $params): array throw new Exception('Invalid URL provided', 400); } - // SSRF guard: only allow HTTPS URLs and block RFC-1918 / link-local / - // loopback / cloud-metadata IP ranges that should never be reachable - // from a legitimate external configuration URL. - $parsedUrl = parse_url($url); - if (($parsedUrl['scheme'] ?? '') !== 'https') { - throw new Exception('Only HTTPS URLs are allowed for configuration import', 400); + // SEC-SVC-6: replace the ad-hoc (DNS-rebinding/IPv6-bypassable) guard with the + // shared anti-SSRF check that resolves every A and AAAA record and rejects any + // loopback/private/reserved/link-local target. + try { + SecurityService::assertSafeFetchUrl($url); + } catch (\InvalidArgumentException $e) { + throw new Exception($e->getMessage(), 400); } - $host = strtolower($parsedUrl['host'] ?? ''); - // Resolve the host to an IP for range checks (best-effort; DNS must succeed). - $resolvedIp = gethostbyname($host); - if ($resolvedIp !== $host) { - $longIp = ip2long($resolvedIp); - if ($longIp !== false) { - // Block loopback (127.0.0.0/8). - if (($longIp & 0xFF000000) === 0x7F000000) { - throw new Exception('URL resolves to a blocked IP range (loopback)', 400); - } - - // Block RFC-1918: 10.0.0.0/8. - if (($longIp & 0xFF000000) === 0x0A000000) { - throw new Exception('URL resolves to a blocked IP range (RFC-1918)', 400); - } - - // Block RFC-1918: 172.16.0.0/12. - if (($longIp & 0xFFF00000) === 0xAC100000) { - throw new Exception('URL resolves to a blocked IP range (RFC-1918)', 400); - } - - // Block RFC-1918: 192.168.0.0/16. - if (($longIp & 0xFFFF0000) === 0xC0A80000) { - throw new Exception('URL resolves to a blocked IP range (RFC-1918)', 400); - } - - // Block link-local (169.254.0.0/16) including AWS metadata endpoint. - if (($longIp & 0xFFFF0000) === 0xA9FE0000) { - throw new Exception('URL resolves to a blocked IP range (link-local/metadata)', 400); - } - }//end if - }//end if - - // Fetch content from URL. + // Fetch content from URL. Redirects are disabled so a public host cannot + // 302 us to an internal address after the point-in-time DNS validation. $client = new Client(); - $response = $client->request('GET', $url); + $response = $client->request('GET', $url, ['allow_redirects' => false]); $content = $response->getBody()->getContents(); $configData = json_decode($content, true); @@ -1306,10 +1336,44 @@ private function importFromSource(callable $fetchConfig, array $params, string $ // Step 2: Extract metadata from config. $info = $configData['info'] ?? []; $xOpenregister = $configData['x-openregister'] ?? []; - $appId = $xOpenregister['app'] ?? 'imported'; - $version = $info['version'] ?? $xOpenregister['version'] ?? '1.0.0'; - $title = $info['title'] ?? $xOpenregister['title'] ?? "Configuration from {$sourceType}"; - $description = $info['description'] ?? $xOpenregister['description'] ?? "Imported from {$sourceType}"; + + // Derive the configuration's app id. Prefer the explicit + // x-openregister.app; otherwise derive a stable id from the first + // register's slug so two independent URL imports (e.g. a Woo and a + // DCAT register) don't both collapse onto the generic 'imported' + // bucket and 409 the second import. Idempotent: re-importing the + // same register yields the same id, so it updates instead of + // colliding. + $appId = $xOpenregister['app'] ?? null; + if (empty($appId) === true) { + $registers = $configData['components']['registers'] ?? []; + $firstSlug = null; + if (is_array($registers) === true && empty($registers) === false) { + $first = reset($registers); + if (is_array($first) === true) { + $firstSlug = ($first['slug'] ?? null); + } else { + $firstSlug = null; + } + + if ($firstSlug === null) { + $key = array_key_first($registers); + if (is_string($key) === true) { + $firstSlug = $key; + } + } + } + + if (empty($firstSlug) === false) { + $appId = 'imported-'.preg_replace('/[^a-z0-9_-]+/', '', strtolower((string) $firstSlug)); + } else { + $appId = 'imported'; + } + }//end if + + $version = $info['version'] ?? $xOpenregister['version'] ?? '1.0.0'; + $title = $info['title'] ?? $xOpenregister['title'] ?? "Configuration from {$sourceType}"; + $description = $info['description'] ?? $xOpenregister['description'] ?? "Imported from {$sourceType}"; // Step 3: Check if configuration already exists for this app. $existingConfigs = $this->configurationMapper->findByApp($appId); @@ -1411,6 +1475,12 @@ private function importFromSource(callable $fetchConfig, array $params, string $ context: ['file' => __FILE__, 'line' => __LINE__] ); + // SEC-CTRL-7: only 4xx validation responses keep the specific message; + // 500s return a generic envelope (the real error is logged above). + if ($statusCode >= 500) { + return new JSONResponse(data: ['error' => 'Internal server error'], statusCode: $statusCode); + } + return new JSONResponse( data: ['error' => 'Failed to import configuration: '.$e->getMessage()], statusCode: $statusCode @@ -1928,8 +1998,9 @@ private function handlePublishingError(Exception $exception): JSONResponse context: ['file' => __FILE__, 'line' => __LINE__] ); + // SEC-CTRL-7: generic 500 envelope; the real error is logged above. return new JSONResponse( - data: ['error' => 'Failed to publish configuration: '.$exception->getMessage()], + data: ['error' => 'Internal server error'], statusCode: 500 ); }//end handlePublishingError() diff --git a/lib/Controller/ConfigurationsController.php b/lib/Controller/ConfigurationsController.php index bb5b161788..219a579ebf 100644 --- a/lib/Controller/ConfigurationsController.php +++ b/lib/Controller/ConfigurationsController.php @@ -111,7 +111,7 @@ private function isCurrentUserAdmin(): bool * * @psalm-return JSONResponse<200, array{results: array<\OCA\OpenRegister\Db\Configuration>}, array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function index(): JSONResponse { @@ -158,7 +158,7 @@ public function index(): JSONResponse * array>|JSONResponse<404, * array{error: 'Configuration not found'}, array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function show(int $id): JSONResponse { @@ -188,7 +188,7 @@ public function show(int $id): JSONResponse * array>|JSONResponse<400, array{error: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function create(): JSONResponse { @@ -253,7 +253,7 @@ public function create(): JSONResponse * array>|JSONResponse<400, array{error: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function update(int $id): JSONResponse { @@ -309,7 +309,7 @@ public function update(int $id): JSONResponse * array>|JSONResponse<400, array{error: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function patch(int $id): JSONResponse { @@ -331,7 +331,7 @@ public function patch(int $id): JSONResponse * array>|JSONResponse<400, array{error: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-2 + * @spec openspec/specs/data-import-export/spec.md */ public function destroy(int $id): JSONResponse { diff --git a/lib/Controller/ConsumersController.php b/lib/Controller/ConsumersController.php index 1073523f8d..de4cd66068 100644 --- a/lib/Controller/ConsumersController.php +++ b/lib/Controller/ConsumersController.php @@ -9,9 +9,9 @@ * @category Controller * @package OCA\OpenRegister\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app * @@ -35,7 +35,7 @@ * * @package OCA\OpenRegister\Controller * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ class ConsumersController extends Controller { @@ -64,7 +64,7 @@ public function __construct( * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ public function index(): JSONResponse { @@ -88,7 +88,7 @@ public function index(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ public function show(int $id): JSONResponse { @@ -108,7 +108,7 @@ public function show(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ public function create(): JSONResponse { @@ -135,7 +135,7 @@ public function create(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ public function update(int $id): JSONResponse { @@ -164,7 +164,7 @@ public function update(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ public function destroy(int $id): JSONResponse { @@ -188,7 +188,7 @@ public function destroy(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ public function patch(int $id): JSONResponse { diff --git a/lib/Controller/ContactsController.php b/lib/Controller/ContactsController.php index 93a5c29323..4c318a7c81 100644 --- a/lib/Controller/ContactsController.php +++ b/lib/Controller/ContactsController.php @@ -130,7 +130,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -172,7 +172,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function create(string $register, string $schema, string $id): JSONResponse { @@ -343,7 +343,7 @@ private function resolveSchemaId(\OCA\OpenRegister\Db\ObjectEntity $object): ?in * * @SuppressWarnings(PHPMD.UnusedFormalParameter) Route-bound; method 501 pending role updates. * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function update(string $register, string $schema, string $id, string $contactUid): JSONResponse { @@ -388,7 +388,7 @@ public function update(string $register, string $schema, string $id, string $con * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function destroy(string $register, string $schema, string $id, string $contactUid): JSONResponse { @@ -433,7 +433,12 @@ public function destroy(string $register, string $schema, string $id, string $co * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-2 + * @no-admin-idor-exempt Guarded downstream: ContactService::getObjectsForContact scopes + * the result to the caller's own addressbooks (cardDavBackend->getAddressBooksForUser + * for the session principal) and returns [] for an anonymous session, so a caller only + * ever sees links for contacts in addressbooks they own. + * + * @spec openspec/specs/contacts-actions/spec.md */ public function objects(string $contactUid): JSONResponse { @@ -475,7 +480,11 @@ private function validateObject( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @no-admin-idor-exempt No per-object resource: free-text matching over caller-supplied + * email/name/organization strings against schemas that opt into linkedTypes:["contact"]; + * takes no caller-supplied object id. + * + * @spec openspec/specs/contacts-actions/spec.md */ public function match(): JSONResponse { @@ -526,7 +535,7 @@ public function match(): JSONResponse * * @return array Enriched matches * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @spec openspec/specs/contacts-actions/spec.md */ private function enrichMatches(array $matches): array { diff --git a/lib/Controller/ContextsController.php b/lib/Controller/ContextsController.php new file mode 100644 index 0000000000..6f7e1d7e7f --- /dev/null +++ b/lib/Controller/ContextsController.php @@ -0,0 +1,221 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\JsonLd\JsonLdContextService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\IRequest; + +/** + * Serves JSON-LD context documents. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/specs/json-ld-output/spec.md + */ +class ContextsController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The application name. + * @param IRequest $request The current request. + * @param RegisterMapper $registerMapper The register mapper. + * @param SchemaMapper $schemaMapper The schema mapper. + * @param JsonLdContextService $contextService The JSON-LD context service. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly JsonLdContextService $contextService + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Return the register-wide JSON-LD context document. + * + * @param string $register The register slug or identifier. + * + * @return DataResponse The `{"@context": {...}}` document, or 404/304. + * + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @PublicPage + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function register(string $register): DataResponse + { + try { + $registerEntity = $this->registerMapper->find($register); + } catch (DoesNotExistException | \Exception $e) { + return new DataResponse(['error' => 'Register not found'], Http::STATUS_NOT_FOUND); + } + + $schemas = $this->loadSchemas(register: $registerEntity); + $contextMap = $this->contextService->buildRegisterContext(register: $registerEntity, schemas: $schemas); + $etag = $this->buildEtag(register: $registerEntity, schemas: $schemas); + + return $this->contextResponse(document: ['@context' => $contextMap], etag: $etag); + }//end register() + + /** + * Return a per-schema JSON-LD context document. + * + * @param string $register The register slug or identifier. + * @param string $schema The schema slug or identifier. + * + * @return DataResponse The `{"@context": {...}}` document, or 404/304. + * + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @PublicPage + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function schema(string $register, string $schema): DataResponse + { + try { + $registerEntity = $this->registerMapper->find($register); + } catch (DoesNotExistException | \Exception $e) { + return new DataResponse(['error' => 'Register not found'], Http::STATUS_NOT_FOUND); + } + + try { + $schemaEntity = $this->schemaMapper->find($schema); + } catch (DoesNotExistException | \Exception $e) { + return new DataResponse(['error' => 'Schema not found'], Http::STATUS_NOT_FOUND); + } + + $contextMap = $this->contextService->buildSchemaContext(register: $registerEntity, schema: $schemaEntity); + $etag = $this->buildEtag(register: $registerEntity, schemas: [$schemaEntity]); + + return $this->contextResponse(document: ['@context' => $contextMap], etag: $etag); + }//end schema() + + /** + * Load the schema entities belonging to a register, skipping any that fail + * to resolve (a stale id in register.schemas must not 500 the document). + * + * @param Register $register The register. + * + * @return array The resolvable schema entities. + */ + private function loadSchemas(Register $register): array + { + $schemas = []; + foreach ($register->getSchemas() as $schemaId) { + try { + $schemas[] = $this->schemaMapper->find($schemaId); + } catch (\Throwable $e) { + // Skip unresolvable schema references; the context document + // covers the schemas that actually exist. + continue; + } + } + + return $schemas; + }//end loadSchemas() + + /** + * Build a weak ETag from the register/schema `updated` timestamps so a + * conditional GET can short-circuit when nothing changed. + * + * @param Register $register The register. + * @param array $schemas The schemas covered by the document. + * + * @return string The ETag value (quoted). + */ + private function buildEtag(Register $register, array $schemas): string + { + $parts = []; + $updated = $register->getUpdated(); + if ($updated !== null) { + $parts[] = $updated->format('U'); + } else { + $parts[] = '0'; + } + + foreach ($schemas as $schema) { + if (($schema instanceof Schema) === false) { + continue; + } + + $schemaUpdated = $schema->getUpdated(); + if ($schemaUpdated !== null) { + $schemaStamp = $schemaUpdated->format('U'); + } else { + $schemaStamp = '0'; + } + + $parts[] = ((string) $schema->getId()).'-'.$schemaStamp; + } + + return '"'.md5(implode(':', $parts)).'"'; + }//end buildEtag() + + /** + * Build a JSON-LD context DataResponse with ETag/Cache-Control headers, + * honouring a matching `If-None-Match` with a 304. + * + * @param array $document The `{"@context": {...}}` document. + * @param string $etag The ETag value. + * + * @return DataResponse The response (200 or 304), with JSON-LD headers. + */ + private function contextResponse(array $document, string $etag): DataResponse + { + $ifNoneMatch = $this->request->getHeader('If-None-Match'); + if ($ifNoneMatch !== '' && trim($ifNoneMatch) === $etag) { + $notModified = new DataResponse([], Http::STATUS_NOT_MODIFIED); + $notModified->addHeader('ETag', $etag); + $notModified->addHeader('Cache-Control', 'public, max-age=3600'); + return $notModified; + } + + $response = new DataResponse($document); + $response->addHeader('Content-Type', 'application/ld+json'); + $response->addHeader('ETag', $etag); + $response->addHeader('Cache-Control', 'public, max-age=3600'); + return $response; + }//end contextResponse() +}//end class diff --git a/lib/Controller/ConversationController.php b/lib/Controller/ConversationController.php index 07487fa0ce..efc6ecddba 100644 --- a/lib/Controller/ConversationController.php +++ b/lib/Controller/ConversationController.php @@ -19,8 +19,8 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-3 + * @spec openspec/specs/chat-ai/spec.md + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Controller; @@ -33,6 +33,8 @@ use OCA\OpenRegister\Service\OrganisationService; use OCA\OpenRegister\Service\ChatService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Db\DoesNotExistException; @@ -174,7 +176,7 @@ public function __construct( * userId: null|string, uuid: null|string}>, * total?: int, limit?: int, offset?: int}, array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function index(): JSONResponse { @@ -279,7 +281,7 @@ public function index(): JSONResponse * deletedAt?: null|string, created?: null|string, updated?: null|string, * messageCount?: int}, array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function show(string $uuid): JSONResponse { @@ -364,7 +366,7 @@ public function show(string $uuid): JSONResponse * uuid: null|string}>, total?: int, limit?: int, offset?: int}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-3 + * @spec openspec/specs/chat-ai/spec.md */ public function messages(string $uuid): JSONResponse { @@ -474,10 +476,10 @@ public function messages(string $uuid): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 - * - * @NoAdminRequired + * @spec openspec/specs/chat-ai/spec.md */ + #[NoAdminRequired] + #[NoCSRFRequired] public function create(): JSONResponse { try { @@ -587,7 +589,7 @@ public function create(): JSONResponse * deletedAt?: null|string, created?: null|string, updated?: null|string}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function update(string $uuid): JSONResponse { @@ -688,7 +690,7 @@ public function update(string $uuid): JSONResponse * * @suppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function destroy(string $uuid): JSONResponse { @@ -821,7 +823,7 @@ public function destroy(string $uuid): JSONResponse * deletedAt?: null|string, created?: null|string, updated?: null|string}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function restore(string $uuid): JSONResponse { @@ -905,7 +907,7 @@ public function restore(string $uuid): JSONResponse * 'Failed to permanently delete conversation', message: string, * uuid?: string}, array> * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function destroyPermanent(string $uuid): JSONResponse { diff --git a/lib/Controller/CospendLinksController.php b/lib/Controller/CospendLinksController.php index 43f3982add..b94708388f 100644 --- a/lib/Controller/CospendLinksController.php +++ b/lib/Controller/CospendLinksController.php @@ -84,7 +84,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -134,7 +134,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function link(string $register, string $schema, string $id): JSONResponse { @@ -204,7 +204,7 @@ public function link(string $register, string $schema, string $id): JSONResponse * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function createAndLink(string $register, string $schema, string $id): JSONResponse { @@ -259,7 +259,7 @@ public function createAndLink(string $register, string $schema, string $id): JSO * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroy(string $register, string $schema, string $id, string $entryId): JSONResponse { @@ -295,8 +295,9 @@ public function destroy(string $register, string $schema, string $id, string $en * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns the current user's own Cospend projects; no caller-supplied object id. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function available(): JSONResponse { @@ -325,7 +326,8 @@ public function available(): JSONResponse * * @return ObjectEntity|null * - * @SuppressWarnings(PHPMD.ShortVariable) $id is the object identifier passed from the route parameter; renaming would break consistency with caller method signatures. + * @SuppressWarnings(PHPMD.ShortVariable) $id is the object identifier passed from the route + * parameter; renaming would break consistency with caller method signatures. */ private function validateObject(string $register, string $schema, string $id): ?ObjectEntity { diff --git a/lib/Controller/CredentialController.php b/lib/Controller/CredentialController.php new file mode 100644 index 0000000000..02b8b7f3b2 --- /dev/null +++ b/lib/Controller/CredentialController.php @@ -0,0 +1,776 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Service\Credential\CredentialAccessDeniedException; +use OCA\OpenRegister\Service\Credential\CredentialAppTokenService; +use OCA\OpenRegister\Service\Credential\CredentialBrokerService; +use OCA\OpenRegister\Service\Credential\CredentialStore; +use OCA\OpenRegister\Service\Credential\CredentialUpstreamException; +use OCA\OpenRegister\Service\Credential\ProviderCatalogue; +use OCA\OpenRegister\Service\ObjectService; +use OCA\OpenRegister\Service\OrganisationService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IUserSession; +use Throwable; + +/** + * HTTP controller for owner-scoped credentials and the constrained broker call. + */ +class CredentialController extends Controller +{ + /** + * The personal (owner-scoped) credential scope — the default when absent. + * + * @var string + */ + private const SCOPE_PERSONAL = 'personal'; + + /** + * The organisation (membership-scoped) credential scope. + * + * @var string + */ + private const SCOPE_ORGANISATION = 'organisation'; + + /** + * Constructor. + * + * @param string $appName App name (injected by NC). + * @param IRequest $request Current request. + * @param IUserSession $userSession Current session (owner identity). + * @param IGroupManager $groupManager Group manager (admin check for app registration). + * @param ObjectService $objectService OR object CRUD for credential metadata. + * @param CredentialStore $credentialStore Secret store leaf (vault). + * @param ProviderCatalogue $catalogue Read-only provider catalogue (validation). + * @param CredentialBrokerService $broker The guarded outbound broker. + * @param CredentialAppTokenService $tokenService Per-app signing-secret registry + token verify. + * @param OrganisationService $organisationService Organisation membership + admin authority resolution. + * + * @return void + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Composes the credential-broker collaborators. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, + private readonly ObjectService $objectService, + private readonly CredentialStore $credentialStore, + private readonly ProviderCatalogue $catalogue, + private readonly CredentialBrokerService $broker, + private readonly CredentialAppTokenService $tokenService, + private readonly OrganisationService $organisationService, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * GET /api/credentials — list credential metadata (never a secret). + * + * `?scope=organisation` lists the caller's active organisation's credentials + * (visible to any member — D4). No scope, or `?scope=personal`, lists the + * caller's OWN personal credentials, unchanged. + * + * @return JSONResponse The credential metadata (metadata only; never a secret). + * + * @spec openspec/specs/credential-broker/spec.md + */ + #[NoAdminRequired] + public function index(): JSONResponse + { + $uid = $this->currentUid(); + if ($uid === null) { + return new JSONResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED); + } + + if ($this->requestedScope() === self::SCOPE_ORGANISATION) { + return $this->indexOrganisation(); + } + + try { + $this->objectService->setRegister(CredentialBrokerService::REGISTER)->setSchema(CredentialBrokerService::SCHEMA); + $objects = $this->objectService->findAll(); + } catch (Throwable $e) { + return new JSONResponse(['message' => 'Unable to list credentials'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + $own = []; + foreach ($objects as $object) { + $data = $this->serialise(object: $object); + // Personal listing: the caller's own personal (scope-absent) credentials only. + if (($data['scope'] ?? self::SCOPE_PERSONAL) === self::SCOPE_ORGANISATION) { + continue; + } + + if (($data['@self']['owner'] ?? null) === $uid) { + $own[] = $data; + } + } + + return new JSONResponse(['results' => $own]); + }//end index() + + /** + * List the caller's active organisation's credential metadata (members may read). + * + * Returns the organisation-scoped credentials whose `organisation` equals the + * caller's active organisation UUID. The caller is a member of their own active + * organisation by construction, so any member may read this metadata; no secret + * is ever included (design D4). + * + * @return JSONResponse The active organisation's credential metadata. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function indexOrganisation(): JSONResponse + { + $activeOrg = $this->organisationService->getActiveOrganisation()?->getUuid(); + if ($activeOrg === null || $activeOrg === '') { + return new JSONResponse(['results' => []]); + } + + try { + $this->objectService->setRegister(CredentialBrokerService::REGISTER)->setSchema(CredentialBrokerService::SCHEMA); + $objects = $this->objectService->findAll(); + } catch (Throwable $e) { + return new JSONResponse(['message' => 'Unable to list credentials'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + $results = []; + foreach ($objects as $object) { + $data = $this->serialise(object: $object); + if (($data['scope'] ?? self::SCOPE_PERSONAL) !== self::SCOPE_ORGANISATION) { + continue; + } + + if ((string) ($data['organisation'] ?? '') === $activeOrg) { + $results[] = $data; + } + } + + return new JSONResponse(['results' => $results]); + }//end indexOrganisation() + + /** + * GET /api/credentials/providers — list the read-only provider catalogue (id + title only). + * + * Powers the settings-UI provider picker. Exposes ONLY the identifier and title — never a + * secret (there is none in the catalogue) and never the allow-rules (an internal guardrail). + * + * @return JSONResponse `{results: Array<{identifier, title}>}`. + * + * @spec openspec/specs/credential-broker/spec.md + */ + #[NoAdminRequired] + public function providers(): JSONResponse + { + if ($this->currentUid() === null) { + return new JSONResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED); + } + + $out = []; + foreach ($this->catalogue->all() as $entry) { + if (is_array($entry) === false) { + continue; + } + + $identifier = (string) ($entry['identifier'] ?? ''); + if ($identifier === '') { + continue; + } + + $out[] = [ + 'identifier' => $identifier, + 'title' => (string) ($entry['title'] ?? $identifier), + ]; + } + + return new JSONResponse(['results' => $out]); + }//end providers() + + /** + * POST /api/credentials — create a credential and store its secret to the vault. + * + * Body: `{name, provider, allowedApps?, secret?, scope?, organisation?}`. The + * secret (if given) is written straight to the vault under the new object UUID + * (its scope's vault owner) — never persisted to the OR object. + * + * This method owns only the REQUEST half — parsing, provider-catalogue validation, + * and the organisation-administrator gate. The mint itself (metadata object + vault + * secret, atomic) belongs to {@see CredentialBrokerService::mint()} so an in-process + * caller without an HTTP session mints through the exact same path (ADR-022). + * + * Personal (scope absent / `personal`): unchanged — any authenticated user may + * create their own. Organisation (`scope = organisation`): the `organisation` + * defaults to the caller's active organisation when omitted, and the caller MUST + * be an administrator of that organisation (or a Nextcloud admin) — design D4. + * + * @return JSONResponse The created credential metadata, or a static error. + * + * @spec openspec/specs/credential-broker/spec.md + */ + #[NoAdminRequired] + public function create(): JSONResponse + { + $uid = $this->currentUid(); + if ($uid === null) { + return new JSONResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED); + } + + $name = trim((string) $this->request->getParam('name', '')); + $provider = (string) $this->request->getParam('provider', ''); + $secret = $this->request->getParam('secret'); + $allowed = $this->normaliseAllowedApps(value: $this->request->getParam('allowedApps', [])); + $scope = $this->requestedScope(); + + if ($name === '' || $this->catalogue->get($provider) === null) { + return new JSONResponse(['message' => 'Invalid credential request'], Http::STATUS_BAD_REQUEST); + } + + // Organisation scope: resolve + admin-gate the owning organisation (D4). This is a + // request/authz concern and stays here; the broker is handed the resolved value. + $organisation = null; + if ($scope === self::SCOPE_ORGANISATION) { + $organisation = $this->gatedOrganisation(uid: $uid); + if ($organisation instanceof JSONResponse) { + return $organisation; + } + } + + if (is_string($secret) === false) { + $secret = null; + } + + try { + // The single mint path (metadata object + vault secret) lives in the broker so + // an in-process caller can mint identically without an HTTP session. + $saved = $this->broker->mint( + name: $name, + provider: $provider, + owner: $uid, + allowedApps: $allowed, + secret: $secret, + scope: $scope, + organisation: $organisation + ); + } catch (Throwable $e) { + return new JSONResponse(['message' => 'Unable to create credential'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + return new JSONResponse($this->serialise(object: $saved), Http::STATUS_CREATED); + }//end create() + + /** + * PUT /api/credentials/{id} — update metadata / allowedApps (owner only); optional rotation. + * + * @param string $id The credential UUID. + * + * @return JSONResponse The updated credential metadata, or a static error. + * + * @spec openspec/specs/credential-broker/spec.md + */ + #[NoAdminRequired] + public function update(string $id): JSONResponse + { + $uid = $this->currentUid(); + $existing = $this->ensureManageable(id: $id, uid: $uid); + if ($existing instanceof JSONResponse) { + return $existing; + } + + // Raw property bag (no `@self` envelope / `id`) — the update carries only + // metadata, never a secret (there is none in the object). + $data = $existing->getObject(); + $scope = $this->scopeOf(data: $data); + + $nameParam = $this->request->getParam('name'); + if (is_string($nameParam) === true && trim($nameParam) !== '') { + $data['name'] = trim($nameParam); + } + + if ($this->request->getParam('allowedApps') !== null) { + $data['allowedApps'] = $this->normaliseAllowedApps(value: $this->request->getParam('allowedApps', [])); + } + + try { + $saved = $this->objectService->saveObject( + object: $data, + register: CredentialBrokerService::REGISTER, + schema: CredentialBrokerService::SCHEMA, + uuid: $id + ); + } catch (Throwable $e) { + return new JSONResponse(['message' => 'Unable to update credential'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + $secret = $this->request->getParam('secret'); + if (is_string($secret) === true && $secret !== '') { + $this->credentialStore->put($id, $secret, $scope); + } + + return new JSONResponse($this->serialise(object: $saved)); + }//end update() + + /** + * DELETE /api/credentials/{id} — delete the object and its vault secret (owner only). + * + * @param string $id The credential UUID. + * + * @return JSONResponse An empty success payload, or a static error. + * + * @spec openspec/specs/credential-broker/spec.md + */ + #[NoAdminRequired] + public function destroy(string $id): JSONResponse + { + $uid = $this->currentUid(); + $existing = $this->ensureManageable(id: $id, uid: $uid); + if ($existing instanceof JSONResponse) { + return $existing; + } + + $scope = $this->scopeOf(data: $existing->getObject()); + + try { + $this->objectService->deleteObject( + uuid: $id, + register: CredentialBrokerService::REGISTER, + schema: CredentialBrokerService::SCHEMA + ); + } catch (Throwable $e) { + return new JSONResponse(['message' => 'Unable to delete credential'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + $this->credentialStore->delete($id, $scope); + + return new JSONResponse(['message' => 'Deleted']); + }//end destroy() + + /** + * POST /api/credentials/apps/{appId}/register — register/rotate an app signing secret. + * + * Admin-only (safest testable posture): the returned secret is shown ONCE and is + * never retrievable afterward. Consuming apps use it to sign their broker tokens. + * + * @param string $appId The consuming app's id. + * + * @return JSONResponse `{appId, secret}` once, or a static error. + * + * @spec openspec/specs/credential-broker/spec.md + */ + #[NoAdminRequired] + public function registerApp(string $appId): JSONResponse + { + $uid = $this->currentUid(); + if ($uid === null || $this->groupManager->isAdmin($uid) === false) { + return new JSONResponse(['message' => 'Forbidden'], Http::STATUS_FORBIDDEN); + } + + if (preg_match('/^[a-z0-9_-]+$/', $appId) !== 1) { + return new JSONResponse(['message' => 'Invalid app id'], Http::STATUS_BAD_REQUEST); + } + + $secret = $this->tokenService->registerApp(appId: $appId); + + return new JSONResponse(['appId' => $appId, 'secret' => $secret], Http::STATUS_CREATED); + }//end registerApp() + + /** + * POST /api/credentials/{id}/request — the guarded broker call. + * + * The calling app id is taken ONLY from the verified `X-Credential-Token` header + * (never a body field). Body: `{method, path, headers?, body?}`. + * + * NEVER forwards an acting user (credential-doriath-leaf design D-K): on the + * HTTP path identity is session-only — the broker's optional `actingUserId` + * parameter is deliberately NOT read from any request field here, so any + * caller-supplied acting-user value is ignored entirely and a broker call + * carrying one is by construction an in-process PHP caller. + * + * @param string $id The credential UUID. + * + * @return JSONResponse `{status, headers, body}` from the upstream, or a static error. + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function brokerRequest(string $id): JSONResponse + { + if ($this->currentUid() === null) { + return new JSONResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED); + } + + $token = $this->request->getHeader('X-Credential-Token'); + $method = (string) $this->request->getParam('method', 'GET'); + $path = (string) $this->request->getParam('path', ''); + + try { + // Pass method+path so a request-bound token (harden-credential-token-binding) + // is only accepted for the exact call it was minted for. Unbound tokens + // ignore these (backward-compatible). + $claims = $this->tokenService->verify(token: $token, method: $method, path: $path); + if ($claims['credentialId'] !== $id) { + throw new CredentialAccessDeniedException(message: 'token/credential mismatch'); + } + + $result = $this->broker->request( + credentialId: $id, + appId: $claims['appId'], + method: $method, + path: $path, + headers: $this->normaliseHeaders(value: $this->request->getParam('headers', [])), + body: $this->normaliseBody(value: $this->request->getParam('body')) + ); + } catch (CredentialAccessDeniedException $e) { + return new JSONResponse(['message' => 'Request not permitted'], Http::STATUS_FORBIDDEN); + } catch (CredentialUpstreamException $e) { + return new JSONResponse(['message' => 'Upstream request failed'], Http::STATUS_BAD_GATEWAY); + } catch (Throwable $e) { + return new JSONResponse(['message' => 'Request not permitted'], Http::STATUS_FORBIDDEN); + }//end try + + return new JSONResponse($result); + }//end brokerRequest() + + /** + * POST /api/credentials/{id}/session-request — the session-authenticated browser broker call. + * + * The browser-side counterpart to {@see brokerRequest()}: a manifest-driven app's + * frontend (nc-vue) makes a constrained brokered outbound call through OpenRegister + * WITHOUT holding the secret and WITHOUT minting a signed HMAC token. Identity is the + * logged-in Nextcloud SESSION user and the request MUST carry the CSRF `requesttoken` + * — this method is deliberately NOT `#[NoCSRFRequired]`, the key difference from the + * token endpoint which is CSRF-exempt for server / cross-runtime callers. + * + * The calling app id is taken from the `appId` body field (the manifest app id); the + * owner IDOR guard still fires because the broker resolves the owner identity from the + * SESSION user (design D-K: session identity wins unconditionally), so a user can never + * use a credential they do not own via this endpoint. No `actingUserId` is ever + * forwarded — the session path lets the broker evaluate the owner guard against the + * current session user naturally. + * + * Body: `{appId, method?, path, headers?, body?}`. + * + * @param string $id The credential UUID. + * + * @return JSONResponse `{status, headers, body}` from the upstream, or a static error. + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + #[NoAdminRequired] + public function sessionBrokerRequest(string $id): JSONResponse + { + if ($this->currentUid() === null) { + return new JSONResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED); + } + + $appId = trim((string) $this->request->getParam('appId', '')); + if ($appId === '') { + return new JSONResponse(['message' => 'Request not permitted'], Http::STATUS_FORBIDDEN); + } + + try { + $result = $this->broker->request( + credentialId: $id, + appId: $appId, + method: (string) $this->request->getParam('method', 'GET'), + path: (string) $this->request->getParam('path', ''), + headers: $this->normaliseHeaders(value: $this->request->getParam('headers', [])), + body: $this->normaliseBody(value: $this->request->getParam('body')) + ); + } catch (CredentialAccessDeniedException $e) { + return new JSONResponse(['message' => 'Request not permitted'], Http::STATUS_FORBIDDEN); + } catch (CredentialUpstreamException $e) { + return new JSONResponse(['message' => 'Upstream request failed'], Http::STATUS_BAD_GATEWAY); + } catch (Throwable $e) { + return new JSONResponse(['message' => 'Request not permitted'], Http::STATUS_FORBIDDEN); + }//end try + + return new JSONResponse($result); + }//end sessionBrokerRequest() + + /** + * Resolve the current user's id, or null when unauthenticated. + * + * @return string|null The current UID, or null. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function currentUid(): ?string + { + return $this->userSession->getUser()?->getUID(); + }//end currentUid() + + /** + * Load a credential and enforce the scope-aware management guard (D4). + * + * Personal (scope absent / `personal`): the per-object owner IDOR guard, + * UNCHANGED — the caller must be the credential `owner`. Organisation + * (`scope = organisation`): the caller must be an administrator of the + * credential's `organisation` (or a Nextcloud admin). + * + * @param string $id The credential UUID. + * @param string|null $uid The current user's UID (null when unauthenticated). + * + * @return ObjectEntity|JSONResponse The manageable entity, or a static error response. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function ensureManageable(string $id, ?string $uid): ObjectEntity | JSONResponse + { + if ($uid === null) { + return new JSONResponse(['message' => 'Unauthorized'], Http::STATUS_UNAUTHORIZED); + } + + try { + $object = $this->objectService->find( + id: $id, + register: CredentialBrokerService::REGISTER, + schema: CredentialBrokerService::SCHEMA, + _rbac: false + ); + } catch (Throwable $e) { + $object = null; + } + + if ($object === null) { + // Uniform 403 — never distinguish "missing" from "not yours". + return new JSONResponse(['message' => 'Forbidden'], Http::STATUS_FORBIDDEN); + } + + if ($this->scopeOf(data: $object->getObject()) === self::SCOPE_ORGANISATION) { + $organisation = (string) ($object->getObject()['organisation'] ?? ''); + if ($organisation === '' || $this->organisationService->isOrganisationAdmin($organisation, $uid) === false) { + return new JSONResponse(['message' => 'Forbidden'], Http::STATUS_FORBIDDEN); + } + + return $object; + } + + // Personal — the per-object owner IDOR guard, unchanged. + if ($object->getOwner() !== $uid) { + return new JSONResponse(['message' => 'Forbidden'], Http::STATUS_FORBIDDEN); + } + + return $object; + }//end ensureManageable() + + /** + * Resolve the requested scope from the request (body `scope` / query `scope`). + * + * Anything other than `organisation` (including absent) resolves to `personal` + * so the personal path is the safe default (design D1/D5). + * + * @return string The requested scope (`personal`|`organisation`). + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function requestedScope(): string + { + $scope = strtolower(trim((string) $this->request->getParam('scope', self::SCOPE_PERSONAL))); + if ($scope === self::SCOPE_ORGANISATION) { + return self::SCOPE_ORGANISATION; + } + + return self::SCOPE_PERSONAL; + }//end requestedScope() + + /** + * Resolve a stored credential's scope from its property bag (absent ⇒ personal). + * + * @param array $data The credential's property bag. + * + * @return string The scope (`personal`|`organisation`). + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function scopeOf(array $data): string + { + if ((string) ($data['scope'] ?? self::SCOPE_PERSONAL) === self::SCOPE_ORGANISATION) { + return self::SCOPE_ORGANISATION; + } + + return self::SCOPE_PERSONAL; + }//end scopeOf() + + /** + * Resolve the owning organisation for a create, defaulting to the active one. + * + * @param string $requested The `organisation` body param (may be empty). + * + * @return string The organisation UUID, or '' when none can be resolved. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function resolveOrganisation(string $requested): string + { + $organisation = trim($requested); + if ($organisation !== '') { + return $organisation; + } + + return (string) ($this->organisationService->getActiveOrganisation()?->getUuid() ?? ''); + }//end resolveOrganisation() + + /** + * Resolve + admin-gate the owning organisation for an organisation-scoped create. + * + * The `organisation` defaults to the caller's active organisation when omitted; + * the caller MUST be an administrator of it (or a Nextcloud admin) — design D4. + * This is the authz half of an organisation create and deliberately stays in the + * controller: the broker's mint takes the ALREADY-GATED organisation and trusts it. + * + * @param string $uid The current user's UID. + * + * @return string|JSONResponse The gated organisation UUID, or a static error response. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function gatedOrganisation(string $uid): string | JSONResponse + { + $organisation = $this->resolveOrganisation(requested: (string) $this->request->getParam('organisation', '')); + if ($organisation === '') { + return new JSONResponse(['message' => 'Invalid credential request'], Http::STATUS_BAD_REQUEST); + } + + if ($this->organisationService->isOrganisationAdmin($organisation, $uid) === false) { + return new JSONResponse(['message' => 'Forbidden'], Http::STATUS_FORBIDDEN); + } + + return $organisation; + }//end gatedOrganisation() + + /** + * Serialise a credential entity to a metadata-only array (never carries a secret). + * + * @param ObjectEntity $object The credential entity. + * + * @return array The serialised metadata. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function serialise(ObjectEntity $object): array + { + return $object->jsonSerialize(); + }//end serialise() + + /** + * Coerce a caller-supplied allowedApps value into a string array. + * + * @param mixed $value The raw param value. + * + * @return array The sanitised app-id list. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function normaliseAllowedApps(mixed $value): array + { + if (is_array($value) === false) { + return []; + } + + $apps = []; + foreach ($value as $entry) { + if (is_string($entry) === true && $entry !== '') { + $apps[] = $entry; + } + } + + return $apps; + }//end normaliseAllowedApps() + + /** + * Coerce a caller-supplied headers value into a string=>string map. + * + * @param mixed $value The raw param value. + * + * @return array The sanitised header map. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function normaliseHeaders(mixed $value): array + { + if (is_array($value) === false) { + return []; + } + + $headers = []; + foreach ($value as $key => $entry) { + if (is_string($key) === true && (is_string($entry) === true || is_numeric($entry) === true)) { + $headers[$key] = (string) $entry; + } + } + + return $headers; + }//end normaliseHeaders() + + /** + * Coerce a caller-supplied body value into a raw string or null. + * + * @param mixed $value The raw param value. + * + * @return string|null The request body, or null. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function normaliseBody(mixed $value): ?string + { + if (is_string($value) === true) { + return $value; + } + + if (is_array($value) === true) { + $encoded = json_encode($value); + if ($encoded !== false) { + return $encoded; + } + } + + return null; + }//end normaliseBody() +}//end class diff --git a/lib/Controller/DashboardController.php b/lib/Controller/DashboardController.php index 124d0c5830..595d5c333f 100644 --- a/lib/Controller/DashboardController.php +++ b/lib/Controller/DashboardController.php @@ -23,10 +23,14 @@ use DateTime; use OCA\OpenRegister\Service\DashboardService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\IGroupManager; use OCP\IRequest; +use OCP\IUserSession; use Psr\Log\LoggerInterface; /** @@ -85,6 +89,8 @@ class DashboardController extends Controller * @param IRequest $request The HTTP request object * @param DashboardService $dashboardService The dashboard service instance * @param LoggerInterface $logger Logger instance for error tracking + * @param IUserSession $userSession Active user session for caller identity + * @param IGroupManager $groupManager Group manager for admin checks * * @return void */ @@ -92,7 +98,9 @@ public function __construct( string $appName, IRequest $request, DashboardService $dashboardService, - LoggerInterface $logger + LoggerInterface $logger, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager ) { // Call parent constructor to initialize base controller. parent::__construct(appName: $appName, request: $request); @@ -102,6 +110,27 @@ public function __construct( $this->logger = $logger; }//end __construct() + /** + * Check whether the currently authenticated user is a Nextcloud administrator. + * + * The dashboard analytics aggregate object counts, storage sizes and audit + * activity across ALL registers/schemas (tenant-wide, no per-register + * scoping). Returning them to a non-admin leaks cross-tenant data, so these + * endpoints are admin-only — consistent with the SearchTrail/AuditTrail + * analytics posture. + * + * @return bool True if a user is signed in and belongs to the admin group. + */ + private function isCurrentUserAdmin(): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + return $this->groupManager->isAdmin($user->getUID()); + }//end isCurrentUserAdmin() + /** * Returns the template of the dashboard page * @@ -264,11 +293,15 @@ public function page(): TemplateResponse * > * * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-6 - * - * @NoAdminRequired */ + #[NoAdminRequired] + #[NoCSRFRequired] public function index(): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { // Get all request parameters. $params = $this->request->getParams(); @@ -321,6 +354,10 @@ public function index(): JSONResponse */ public function calculate(?int $registerId=null, ?int $schemaId=null): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { // Calculate sizes and statistics using dashboard service. // Service handles aggregation of object and log sizes. @@ -368,6 +405,10 @@ public function getAuditTrailActionChart( ?int $registerId=null, ?int $schemaId=null ): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $fromDate = null; if ($from !== null) { @@ -411,6 +452,10 @@ public function getAuditTrailActionChart( */ public function getObjectsByRegisterChart(?int $registerId=null, ?int $schemaId=null): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $data = $this->dashboardService->getObjectsByRegisterChartData(registerId: $registerId, schemaId: $schemaId); return new JSONResponse(data: $data); @@ -439,6 +484,10 @@ public function getObjectsByRegisterChart(?int $registerId=null, ?int $schemaId= */ public function getObjectsBySchemaChart(?int $registerId=null, ?int $schemaId=null): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $data = $this->dashboardService->getObjectsBySchemaChartData(registerId: $registerId, schemaId: $schemaId); return new JSONResponse(data: $data); @@ -469,6 +518,10 @@ public function getObjectsBySchemaChart(?int $registerId=null, ?int $schemaId=nu */ public function getObjectsBySizeChart(?int $registerId=null, ?int $schemaId=null): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $data = $this->dashboardService->getObjectsBySizeChartData(registerId: $registerId, schemaId: $schemaId); return new JSONResponse(data: $data); @@ -499,6 +552,10 @@ public function getObjectsBySizeChart(?int $registerId=null, ?int $schemaId=null */ public function getAuditTrailStatistics(?int $registerId=null, ?int $schemaId=null, ?int $hours=24): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $data = $this->dashboardService->getAuditTrailStatistics( registerId: $registerId, @@ -532,6 +589,10 @@ public function getAuditTrailStatistics(?int $registerId=null, ?int $schemaId=nu */ public function getAuditTrailActionDistribution(?int $registerId=null, ?int $schemaId=null, ?int $hours=24): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $data = $this->dashboardService->getAuditTrailActionDistribution( registerId: $registerId, @@ -570,6 +631,10 @@ public function getMostActiveObjects( ?int $limit=10, ?int $hours=24 ): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $data = $this->dashboardService->getMostActiveObjects( registerId: $registerId, diff --git a/lib/Controller/DataSubjectRequestController.php b/lib/Controller/DataSubjectRequestController.php new file mode 100644 index 0000000000..10b4168bd1 --- /dev/null +++ b/lib/Controller/DataSubjectRequestController.php @@ -0,0 +1,304 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\Gdpr\DataSubjectRequestService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * RBAC + tenant scoped data-subject-rights endpoints. + */ +class DataSubjectRequestController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The app name. + * @param IRequest $request The request. + * @param DataSubjectRequestService $service The consumable DSR service. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly DataSubjectRequestService $service + ) { + parent::__construct(appName: $appName, request: $request); + + }//end __construct() + + /** + * GET /api/gdpr/subject-data — discover a subject's objects (art-15/20). + * + * Query params: `subject` (required), `type` (optional), `mode` + * (`exact`|`ilike`). RBAC + tenant scoped. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: DataSubjectRequestService loads every matched object through + * MagicMapper::find with _rbac:true and _multitenancy:true (the cross-org IDOR access-control filter); + * unauthorised subject ids resolve to no rows. Admin DSAR case-management lives in the separate + * DsarController (ADR-047). + * + * @spec openspec/changes/gdpr-data-subject-rights/tasks.md#task-2.2 + */ + public function subjectData(): JSONResponse + { + $subject = (string) ($this->request->getParam(key: 'subject') ?? ''); + if ($subject === '') { + return $this->missingSubject(); + } + + $results = $this->service->findSubjectData( + subjectId: $subject, + type: $this->optionalType(), + mode: (string) ($this->request->getParam(key: 'mode') ?? 'exact') + ); + + return new JSONResponse( + data: [ + 'subject' => $subject, + 'count' => count($results), + 'results' => $results, + ] + ); + + }//end subjectData() + + /** + * GET /api/gdpr/access-export — portable export of a subject's data (art-15/20). + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: as subjectData; every object is loaded via + * MagicMapper::find(_rbac:true,_multitenancy:true), so only objects the caller may read are exported. + * + * @spec openspec/changes/gdpr-data-subject-rights/tasks.md#task-2.3 + */ + public function accessExport(): JSONResponse + { + $subject = (string) ($this->request->getParam(key: 'subject') ?? ''); + if ($subject === '') { + return $this->missingSubject(); + } + + $bundle = $this->service->assembleAccessExport( + subjectId: $subject, + type: $this->optionalType() + ); + + return new JSONResponse(data: $bundle); + + }//end accessExport() + + /** + * POST /api/gdpr/rectify — correct a single object (art-16). + * + * Body: `object` (id/uuid, required), `changes` (object, required). + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: DataSubjectRequestService::rectify loads the object via + * MagicMapper::find(_rbac:true,_multitenancy:true); an unauthorised or absent id yields 404 and the write + * is re-scoped with the same flags. + * + * @spec openspec/changes/gdpr-data-subject-rights/tasks.md#task-2.4 + */ + public function rectify(): JSONResponse + { + $objectId = (string) ($this->request->getParam(key: 'object') ?? ''); + $changes = $this->request->getParam(key: 'changes'); + if ($objectId === '' || is_array($changes) === false) { + return $this->badRequest(message: 'object and changes are required'); + } + + $result = $this->service->rectify(objectIdentifier: $objectId, changes: $changes); + if ($result === null) { + return new JSONResponse(data: ['error' => 'Object not found, not authorised, or immutable'], statusCode: Http::STATUS_NOT_FOUND); + } + + return new JSONResponse(data: $result); + + }//end rectify() + + /** + * POST /api/gdpr/erase — erase a subject's data (art-17). + * + * Body: `subject` (required), `type` (optional), `eraseMode` + * (`pseudonymise`|`whole-object`), `dryRun` (bool). Objects under legal + * hold / immutable status are reported as `held`, never erased. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: each discovered object is loaded via + * MagicMapper::find(_rbac:true,_multitenancy:true); unauthorised loads are excluded and saveObject + * re-enforces _rbac + _multitenancy. + * + * @spec openspec/changes/gdpr-data-subject-rights/tasks.md#task-2.5 + */ + public function erase(): JSONResponse + { + $subject = (string) ($this->request->getParam(key: 'subject') ?? ''); + if ($subject === '') { + return $this->missingSubject(); + } + + $summary = $this->service->erase( + subjectId: $subject, + type: $this->optionalType(), + eraseMode: (string) ($this->request->getParam(key: 'eraseMode') ?? DataSubjectRequestService::ERASE_MODE_PSEUDONYMISE), + dryRun: filter_var($this->request->getParam(key: 'dryRun'), FILTER_VALIDATE_BOOLEAN) + ); + + return new JSONResponse(data: $summary); + + }//end erase() + + /** + * POST /api/gdpr/restrict — set/clear processing restriction (art-18). + * + * Body: `object` (required), `restricted` (bool), `reason` (optional). + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: DataSubjectRequestService::setRestriction loads the object via + * MagicMapper::find(_rbac:true,_multitenancy:true); a null result yields 404. + * + * @spec openspec/changes/gdpr-data-subject-rights/tasks.md#task-2.6 + */ + public function restrict(): JSONResponse + { + $objectId = (string) ($this->request->getParam(key: 'object') ?? ''); + if ($objectId === '') { + return $this->badRequest(message: 'object is required'); + } + + $result = $this->service->setRestriction( + objectIdentifier: $objectId, + restricted: filter_var($this->request->getParam(key: 'restricted'), FILTER_VALIDATE_BOOLEAN), + reason: (string) ($this->request->getParam(key: 'reason') ?? '') + ); + if ($result === null) { + return new JSONResponse(data: ['error' => 'Object not found, not authorised, or immutable'], statusCode: Http::STATUS_NOT_FOUND); + } + + return new JSONResponse(data: $result); + + }//end restrict() + + /** + * POST /api/gdpr/object — set/clear objection (art-21). + * + * Body: `object` (required), `objected` (bool), `reason` (optional). + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: DataSubjectRequestService::setObjection loads the object via + * MagicMapper::find(_rbac:true,_multitenancy:true); a null result yields 404. + * + * @spec openspec/changes/gdpr-data-subject-rights/tasks.md#task-2.6 + */ + public function objection(): JSONResponse + { + $objectId = (string) ($this->request->getParam(key: 'object') ?? ''); + if ($objectId === '') { + return $this->badRequest(message: 'object is required'); + } + + $result = $this->service->setObjection( + objectIdentifier: $objectId, + objected: filter_var($this->request->getParam(key: 'objected'), FILTER_VALIDATE_BOOLEAN), + reason: (string) ($this->request->getParam(key: 'reason') ?? '') + ); + if ($result === null) { + return new JSONResponse(data: ['error' => 'Object not found, not authorised, or immutable'], statusCode: Http::STATUS_NOT_FOUND); + } + + return new JSONResponse(data: $result); + + }//end objection() + + /** + * Resolve the optional `type` query/body parameter to a string|null. + * + * @return string|null + */ + private function optionalType(): ?string + { + $type = $this->request->getParam(key: 'type'); + if ($type === null || $type === '') { + return null; + } + + return (string) $type; + + }//end optionalType() + + /** + * 422 — the required `subject` parameter is missing. + * + * @return JSONResponse + */ + private function missingSubject(): JSONResponse + { + return new JSONResponse( + data: ['error' => 'Missing required parameter: subject'], + statusCode: Http::STATUS_UNPROCESSABLE_ENTITY + ); + + }//end missingSubject() + + /** + * 400 — a required body parameter is missing/invalid. + * + * @param string $message Human-readable reason. + * + * @return JSONResponse + */ + private function badRequest(string $message): JSONResponse + { + return new JSONResponse(data: ['error' => $message], statusCode: Http::STATUS_BAD_REQUEST); + + }//end badRequest() +}//end class diff --git a/lib/Controller/DeckController.php b/lib/Controller/DeckController.php index b99a9ab478..f20bf6372a 100644 --- a/lib/Controller/DeckController.php +++ b/lib/Controller/DeckController.php @@ -177,6 +177,11 @@ public function create(string $register, string $schema, string $id): JSONRespon * @NoAdminRequired * @NoCSRFRequired * + * @no-admin-idor-exempt Guarded downstream: DeckCardService::getObjectsForBoard gates on + * Deck's own BoardService::find (board ACL, throws for non-members) and returns [] when + * the caller cannot access the board, so link metadata never leaks for boards the caller + * has no Deck access to. + * * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-misc/tasks.md#task-3 */ public function objects(string $boardId): JSONResponse diff --git a/lib/Controller/DeckLinksController.php b/lib/Controller/DeckLinksController.php index aa08065711..375a767d5b 100644 --- a/lib/Controller/DeckLinksController.php +++ b/lib/Controller/DeckLinksController.php @@ -14,6 +14,8 @@ * - DELETE /api/objects/{register}/{schema}/{id}/deck/{cardId} — unlink * - GET /api/integrations/deck/boards — list boards * - GET /api/integrations/deck/boards/{boardId}/stacks — list stacks + * - GET /api/integrations/deck/schemas/{schema}/default — get sticky default + * - PUT /api/integrations/deck/schemas/{schema}/default — set sticky default * * @category Controller * @package OCA\OpenRegister\Controller @@ -32,6 +34,7 @@ use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Service\DeckLinkService; use OCA\OpenRegister\Service\ObjectService; +use OCA\OpenRegister\Service\SettingsService; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http\JSONResponse; @@ -52,12 +55,14 @@ class DeckLinksController extends Controller * @param IRequest $request HTTP request. * @param DeckLinkService $deckLinkService Backing service. * @param ObjectService $objectService OR object resolver. + * @param SettingsService $settingsService Settings service for sticky default. */ public function __construct( string $appName, IRequest $request, private readonly DeckLinkService $deckLinkService, private readonly ObjectService $objectService, + private readonly SettingsService $settingsService, ) { parent::__construct(appName: $appName, request: $request); }//end __construct() @@ -74,7 +79,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -115,7 +120,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function link(string $register, string $schema, string $id): JSONResponse { @@ -172,7 +177,7 @@ public function link(string $register, string $schema, string $id): JSONResponse * Each branch carries a distinct surface contract — splitting would * scatter the request-handling intent across helper methods. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function createNew(string $register, string $schema, string $id): JSONResponse { @@ -224,6 +229,13 @@ public function createNew(string $register, string $schema, string $id): JSONRes $duedateStr ); + // AD-1: persist sticky schema-level default after each successful create. + $this->settingsService->setDeckDefault( + schemaSlug: $schema, + boardId: $boardId, + stackId: $stackId, + ); + return new JSONResponse($link->jsonSerialize(), 201); } catch (DoesNotExistException $e) { return new JSONResponse(['error' => 'Object not found'], 404); @@ -245,7 +257,7 @@ public function createNew(string $register, string $schema, string $id): JSONRes * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroy(string $register, string $schema, string $id, string $cardId): JSONResponse { @@ -279,8 +291,9 @@ public function destroy(string $register, string $schema, string $id, string $ca * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: Deck BoardService returns only boards the current user may access; no unguarded caller-supplied id. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function boards(): JSONResponse { @@ -304,8 +317,10 @@ public function boards(): JSONResponse * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: DeckLinkService::getStacksForBoard delegates to Deck StackService::findAll, + * which calls PermissionService::checkPermission(READ) on the board and throws NoPermissionException for non-members. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function stacks(string $boardId): JSONResponse { @@ -320,6 +335,72 @@ public function stacks(string $boardId): JSONResponse return new JSONResponse(['results' => $stacks, 'total' => count($stacks)]); }//end stacks() + /** + * Get the schema-level sticky default board+stack. + * + * Returns `{boardId: null, stackId: null}` when no default has been saved. + * + * @param string $schema Schema slug. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @no-admin-idor-exempt Operates on schema-level Deck configuration (the + * sticky default board+stack for a schema), not on a user-owned object. + * There is no per-object resource to scope; the value is shared config + * for everyone authoring objects of this schema. + * + * @spec openspec/changes/nextcloud-entity-relations/tasks.md#deck-card-relations + */ + public function getDefault(string $schema): JSONResponse + { + $default = $this->settingsService->getDeckDefault(schemaSlug: $schema); + if ($default === null) { + return new JSONResponse(['boardId' => null, 'stackId' => null]); + } + + return new JSONResponse($default); + }//end getDefault() + + /** + * Set (or update) the schema-level sticky default board+stack. + * + * Body: `{ boardId: int, stackId: int }`. + * + * @param string $schema Schema slug. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @no-admin-idor-exempt Operates on schema-level Deck configuration (the + * sticky default board+stack for a schema), not on a user-owned object. + * There is no per-object resource to scope; the value is shared config + * for everyone authoring objects of this schema. + * + * @spec openspec/changes/nextcloud-entity-relations/tasks.md#deck-card-relations + */ + public function setDefault(string $schema): JSONResponse + { + $boardId = (int) $this->request->getParam('boardId', 0); + $stackId = (int) $this->request->getParam('stackId', 0); + + if ($boardId === 0 || $stackId === 0) { + return new JSONResponse(['error' => 'boardId and stackId are required'], 400); + } + + $this->settingsService->setDeckDefault( + schemaSlug: $schema, + boardId: $boardId, + stackId: $stackId, + ); + + return new JSONResponse(['boardId' => $boardId, 'stackId' => $stackId]); + }//end setDefault() + /** * Resolve an OR object from register/schema/id. * diff --git a/lib/Controller/DeletedController.php b/lib/Controller/DeletedController.php index 574886b3cf..08e01e3183 100644 --- a/lib/Controller/DeletedController.php +++ b/lib/Controller/DeletedController.php @@ -20,9 +20,9 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-28 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-30 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/deletion-audit-trail/spec.md + * @spec openspec/specs/deletion-audit-trail/spec.md + * @spec openspec/specs/deletion-audit-trail/spec.md */ namespace OCA\OpenRegister\Controller; @@ -33,7 +33,6 @@ use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Service\Object\PermissionHandler; -use OCA\OpenRegister\Service\ObjectService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; @@ -59,7 +58,6 @@ class DeletedController extends Controller * @param MagicMapper $objectEntityMapper The object entity mapper * @param RegisterMapper $registerMapper The register mapper * @param SchemaMapper $schemaMapper The schema mapper - * @param ObjectService $objectService The object service * @param IUserSession $userSession The user session * @param IGroupManager $groupManager The group manager for admin checks * @param PermissionHandler $permissionHandler Handler for per-schema RBAC checks @@ -72,7 +70,6 @@ public function __construct( private readonly MagicMapper $objectEntityMapper, private readonly RegisterMapper $registerMapper, private readonly SchemaMapper $schemaMapper, - private readonly ObjectService $objectService, private readonly IUserSession $userSession, private readonly IGroupManager $groupManager, private readonly PermissionHandler $permissionHandler @@ -253,43 +250,22 @@ function ($key) { * page?: int, pages?: 1|float, limit?: int|null, offset?: int|null}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/deletion-audit-trail/spec.md */ public function index(): JSONResponse { $params = $this->extractRequestParameters(); try { - // Use searchObjectsPaginated with @self.deleted filter to find deleted objects. - // Build query array with filter for deleted objects. - $query = [ - '@self.deleted' => 'IS NOT NULL', - '_limit' => $params['limit'], - '_offset' => $params['offset'], - '_order' => $params['sort'], - ]; - - // Merge any additional filters from request. - foreach ($params['filters'] as $key => $value) { - if ($key !== '@self.deleted') { - $query[$key] = $value; - } - } - - // Determine if current user is admin and disable multitenancy if so. - $isAdmin = $this->isCurrentUserAdmin(); - - // Use ObjectService to search for deleted objects with deleted=true to include them. - $result = $this->objectService->searchObjectsPaginated( - query: $query, - deleted: true, - // This tells the service to include deleted objects in the search. - _multitenancy: !$isAdmin - // Disable multitenancy for admins so they can see all deleted objects. + // Objects live in per-register/schema magic tables, so there is no + // single table for searchObjectsPaginated() to query without a + // register/schema context — it always fell through to an empty + // result. Scan every magic table for soft-deleted rows directly. + $deletedObjects = $this->objectEntityMapper->findDeletedAcrossAllMagicTables( + limit: $params['limit'], + offset: $params['offset'] ); - - $deletedObjects = $result['results'] ?? []; - $total = $result['total'] ?? 0; + $total = $this->objectEntityMapper->countDeletedAcrossAllMagicTables(); // Calculate pagination. $pages = 1; @@ -331,10 +307,10 @@ public function index(): JSONResponse public function statistics(): JSONResponse { try { - // Get total deleted count. - $totalDeleted = $this->objectEntityMapper->countAll( - _filters: ['@self.deleted' => 'IS NOT NULL'], - ); + // Count soft-deleted rows across every magic table. countAll() with + // no register/schema context returns 0 (it cannot pick a table), so + // the dedicated cross-table count is required. + $totalDeleted = $this->objectEntityMapper->countDeletedAcrossAllMagicTables(); // Get deleted today count. $today = (new DateTime())->format('Y-m-d'); @@ -388,6 +364,12 @@ public function statistics(): JSONResponse */ public function topDeleters(): JSONResponse { + // SEC-CTRL: admin-only — cross-user deletion analytics (usernames are + // PII); a tenant-wide management surface, not per-user data. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => 'Admin privileges required'], statusCode: 403); + } + try { // TODO: Implement aggregation query to get top deleters from deleted objects. // For now, return mock data structure. @@ -419,7 +401,7 @@ public function topDeleters(): JSONResponse * * @return JSONResponse JSON response with restore result * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/deletion-audit-trail/spec.md */ public function restore(string $id): JSONResponse { @@ -435,13 +417,11 @@ public function restore(string $id): JSONResponse ); } - // Clear the deleted status using direct SQL update. - // Nextcloud Entity system has issues detecting array->null changes for JSON fields. - $qb = $this->objectEntityMapper->getQueryBuilder(); - $qb->update('openregister_objects') - ->set('deleted', $qb->createNamedParameter(null, \PDO::PARAM_NULL)) - ->where($qb->expr()->eq('uuid', $qb->createNamedParameter($id))) - ->executeStatement(); + // Restore via MagicMapper: objects live in per-register/schema magic + // tables, NOT the legacy generic `openregister_objects` table. The + // old direct `UPDATE openregister_objects` matched zero rows, so the + // call reported success but never un-deleted the object. + $this->objectEntityMapper->restoreObject(uuid: $id); return new JSONResponse( data: [ @@ -475,7 +455,7 @@ public function restore(string $id): JSONResponse * * @return JSONResponse JSON response with multiple restore result * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/deletion-audit-trail/spec.md */ public function restoreMultiple(): JSONResponse { @@ -498,17 +478,13 @@ public function restoreMultiple(): JSONResponse } try { - // Use findAll for better database performance - single query instead of multiple. - $objects = $this->objectEntityMapper->findAll( - limit: null, - offset: null, - filters: [], - searchConditions: [], - searchParams: [], - sort: [], - search: null, - ids: $ids, - uses: null, + // Resolve across ALL magic tables, including deleted rows. findAll() + // without register/schema context returns [] (it cannot know which + // magic table to query), so a UUID-keyed cross-table lookup is + // required to actually find the soft-deleted objects. + $objects = $this->objectEntityMapper->findMultipleAcrossAllMagicTables( + uuids: $ids, + includeDeleted: true ); // Track results. @@ -518,7 +494,7 @@ public function restoreMultiple(): JSONResponse // Process found objects. foreach ($objects as $object) { - $foundIds[] = $object->getId(); + $foundIds[] = $object->getUuid(); try { if ($object->getDeleted() === null) { @@ -536,8 +512,9 @@ public function restoreMultiple(): JSONResponse continue; } - $object->setDeleted(null); - $this->objectEntityMapper->update(entity: $object); + // Restore via the magic-table-aware path (writes _deleted=NULL + // on the correct per-register/schema table). + $this->objectEntityMapper->restoreObject(uuid: (string) $object->getUuid()); $restored++; } catch (\Exception $e) { $failed++; @@ -583,7 +560,7 @@ public function restoreMultiple(): JSONResponse * * @return JSONResponse JSON response with deletion result * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-28 + * @spec openspec/specs/deletion-audit-trail/spec.md */ public function destroy(string $id): JSONResponse { @@ -652,7 +629,7 @@ public function destroy(string $id): JSONResponse * * @return JSONResponse JSON response with multiple deletion result * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-28 + * @spec openspec/specs/deletion-audit-trail/spec.md */ public function destroyMultiple(): JSONResponse { @@ -675,17 +652,12 @@ public function destroyMultiple(): JSONResponse } try { - // Use findAll for better database performance - single query instead of multiple. - $objects = $this->objectEntityMapper->findAll( - limit: null, - offset: null, - filters: [], - searchConditions: [], - searchParams: [], - sort: [], - search: null, - ids: $ids, - uses: null, + // Resolve across ALL magic tables, including deleted rows. findAll() + // without register/schema context returns [], so a UUID-keyed + // cross-table lookup is required to find the soft-deleted objects. + $objects = $this->objectEntityMapper->findMultipleAcrossAllMagicTables( + uuids: $ids, + includeDeleted: true ); // Track results. @@ -695,7 +667,7 @@ public function destroyMultiple(): JSONResponse // Process found objects. foreach ($objects as $object) { - $foundIds[] = $object->getId(); + $foundIds[] = $object->getUuid(); try { if ($object->getDeleted() === null) { diff --git a/lib/Controller/DsarCaseController.php b/lib/Controller/DsarCaseController.php new file mode 100644 index 0000000000..8fab6f9a0b --- /dev/null +++ b/lib/Controller/DsarCaseController.php @@ -0,0 +1,549 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\Gdpr\Case\CaseAccessControl; +use OCA\OpenRegister\Service\Gdpr\Case\CaseObjectAccessor; +use OCA\OpenRegister\Service\Gdpr\Evidence\EvidenceHarvestService; +use OCA\OpenRegister\Service\Gdpr\Export\ExportBundleService; +use OCA\OpenRegister\Service\Gdpr\Identity\IdentityVerifyRegistry; +use OCA\OpenRegister\Service\Gdpr\Policy\DsarPolicyPackResolver; +use OCA\OpenRegister\Service\Gdpr\Redaction\RedactionWriteService; +use OCA\OpenRegister\Service\Gdpr\Regulator\RegulatorEscalateRegistry; +use OCA\OpenRegister\Service\Lifecycle\TransitionEngine; +use OCA\OpenRegister\Service\ObjectService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataDownloadResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\IUserSession; +use Throwable; + +/** + * RBAC + case-level access controlled DSAR case-management endpoints. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Transport controller wiring the case-engine services + object store. + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Constructor-injected case-engine collaborators + framework appName/request. + */ +class DsarCaseController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The app name. + * @param IRequest $request The request. + * @param ObjectService $objectService RBAC + tenant scoped object store (case creation). + * @param CaseObjectAccessor $accessor RBAC-scoped case load helper. + * @param CaseAccessControl $accessControl Case-level access-control check (fail closed). + * @param TransitionEngine $transitionEngine Lifecycle transition driver (runs the guard on save). + * @param EvidenceHarvestService $harvestService Evidence-collection service. + * @param RedactionWriteService $redactionService Field-level redaction write path. + * @param ExportBundleService $bundleService Export-bundle + dossier service. + * @param IUserSession $userSession Current caller (anonymous rejection). + * @param DsarPolicyPackResolver $packResolver Resolves the case's active pack seam selectors. + * @param IdentityVerifyRegistry $identityRegistry Identity-verify seam registry (pack-selector-driven, fail-closed). + * @param RegulatorEscalateRegistry $regulatorRegistry Regulator-escalate seam registry (pack-selector-driven, fail-closed). + */ + public function __construct( + string $appName, + IRequest $request, + private readonly ObjectService $objectService, + private readonly CaseObjectAccessor $accessor, + private readonly CaseAccessControl $accessControl, + private readonly TransitionEngine $transitionEngine, + private readonly EvidenceHarvestService $harvestService, + private readonly RedactionWriteService $redactionService, + private readonly ExportBundleService $bundleService, + private readonly IUserSession $userSession, + private readonly DsarPolicyPackResolver $packResolver, + private readonly IdentityVerifyRegistry $identityRegistry, + private readonly RegulatorEscalateRegistry $regulatorRegistry + ) { + parent::__construct(appName: $appName, request: $request); + + }//end __construct() + + /** + * POST /api/gdpr/cases — create a data-subject-request case. + * + * The body is persisted through ObjectService (RBAC + multitenancy) under + * the case register/schema. There is no pre-existing object to case-scope + * here; object RBAC on the write governs creation. + * + * @return JSONResponse + * + * @NoAdminRequired + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md + */ + public function create(): JSONResponse + { + $denied = $this->requireAuthenticated(); + if ($denied !== null) { + return $denied; + } + + $body = (array) $this->request->getParams(); + // Strip framework-injected keys so only the case payload is saved. + unset($body['_route']); + + try { + $saved = $this->objectService->saveObject( + object: $body, + register: CaseObjectAccessor::REGISTER_SLUG, + schema: CaseObjectAccessor::SCHEMA_SLUG, + _rbac: true, + _multitenancy: true + ); + } catch (Throwable $e) { + return new JSONResponse( + data: ['error' => $e->getMessage()], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + return new JSONResponse(data: $saved->jsonSerialize(), statusCode: Http::STATUS_CREATED); + }//end create() + + /** + * POST /api/gdpr/cases/{id}/transition — run a declared lifecycle transition. + * + * The `action` param names the transition (e.g. `assign`, `collectEvidence`, + * `draftDenial`, `finaliseDenial`). `finaliseDenial` passes through the + * denial-finalise guard on save. Case-scoped + object-RBAC gated. + * + * @param string $id The case uuid. + * + * @return JSONResponse + * + * @NoAdminRequired + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md + */ + public function transition(string $id): JSONResponse + { + $guard = $this->guardCase(caseUuid: $id); + if ($guard !== null) { + return $guard; + } + + $action = (string) ($this->request->getParam(key: 'action') ?? ''); + if ($action === '') { + return new JSONResponse( + data: ['error' => 'A transition "action" is required.'], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + try { + $saved = $this->transitionEngine->transition(objectId: $id, action: $action); + } catch (Throwable $e) { + // Guard denial / illegal transition surface as 403. + return new JSONResponse( + data: ['error' => $e->getMessage()], + statusCode: Http::STATUS_FORBIDDEN + ); + } + + return new JSONResponse(data: $saved->jsonSerialize()); + }//end transition() + + /** + * POST /api/gdpr/cases/{id}/evidence — trigger evidence harvest for a case. + * + * @param string $id The case uuid. + * + * @return JSONResponse + * + * @NoAdminRequired + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function evidence(string $id): JSONResponse + { + $guard = $this->guardCase(caseUuid: $id); + if ($guard !== null) { + return $guard; + } + + try { + $result = $this->harvestService->harvest(caseUuid: $id); + } catch (Throwable $e) { + return new JSONResponse( + data: ['error' => $e->getMessage()], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + return new JSONResponse(data: $result); + }//end evidence() + + /** + * POST /api/gdpr/cases/{id}/redactions — apply a field-level redaction. + * + * @param string $id The case uuid. + * + * @return JSONResponse + * + * @NoAdminRequired + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-redaction-write/spec.md + */ + public function redact(string $id): JSONResponse + { + $guard = $this->guardCase(caseUuid: $id); + if ($guard !== null) { + return $guard; + } + + $field = (string) ($this->request->getParam(key: 'field') ?? ''); + $after = (string) ($this->request->getParam(key: 'after') ?? ''); + $ground = (string) ($this->request->getParam(key: 'ground') ?? ''); + if ($field === '' || $ground === '') { + return new JSONResponse( + data: ['error' => 'Both "field" and "ground" are required.'], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + try { + $result = $this->redactionService->applyRedaction( + caseUuid: $id, + field: $field, + after: $after, + ground: $ground + ); + } catch (Throwable $e) { + return new JSONResponse( + data: ['error' => $e->getMessage()], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + return new JSONResponse(data: $result); + }//end redact() + + /** + * POST /api/gdpr/cases/{id}/bundle — generate the signed export bundle. + * + * Returns the bundle metadata + a one-time download token (not the bytes). + * + * @param string $id The case uuid. + * + * @return JSONResponse + * + * @NoAdminRequired + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function generateBundle(string $id): JSONResponse + { + $guard = $this->guardCase(caseUuid: $id); + if ($guard !== null) { + return $guard; + } + + try { + $result = $this->bundleService->generate(caseUuid: $id); + } catch (Throwable $e) { + return new JSONResponse( + data: ['error' => $e->getMessage()], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + return new JSONResponse(data: $result, statusCode: Http::STATUS_CREATED); + }//end generateBundle() + + /** + * GET /api/gdpr/cases/{id}/bundle/download — one-time secure download. + * + * Requires the one-time `token` param; the token is burned on first success + * so a replay is refused. Authenticated + case-scoped (never `@PublicPage`). + * `@NoCSRFRequired` because this is a browser navigation download that + * cannot carry a CSRF token — auth + case scope + one-time token remain. + * + * @param string $id The case uuid. + * + * @return DataDownloadResponse|JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function downloadBundle(string $id) + { + $denied = $this->requireAuthenticated(); + if ($denied !== null) { + return $denied; + } + + // Case-scope the download: the token is bound to this case AND the + // caller must pass the case-level access control. + $case = $this->accessor->load(caseUuid: $id); + if ($case === null || $this->accessControl->mayAct(case: $case->getObject()) === false) { + return new JSONResponse( + data: ['error' => 'Not found or not authorised.'], + statusCode: Http::STATUS_NOT_FOUND + ); + } + + $token = (string) ($this->request->getParam(key: 'token') ?? ''); + $bundle = $this->bundleService->download(caseUuid: $id, token: $token); + if ($bundle === null) { + return new JSONResponse( + data: ['error' => 'Invalid, expired, or already-used download token.'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + + return new DataDownloadResponse( + $bundle->getBytes(), + 'dsar-case-'.$id.'.pdf', + $bundle->getMimeType() + ); + }//end downloadBundle() + + /** + * GET /api/gdpr/cases/{id}/dossier — assemble the regulator dossier. + * + * @param string $id The case uuid. + * + * @return JSONResponse + * + * @NoAdminRequired + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function dossier(string $id): JSONResponse + { + $guard = $this->guardCase(caseUuid: $id); + if ($guard !== null) { + return $guard; + } + + try { + $result = $this->bundleService->assembleRegulatorDossier(caseUuid: $id); + } catch (Throwable $e) { + return new JSONResponse( + data: ['error' => $e->getMessage()], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + return new JSONResponse(data: $result); + }//end dossier() + + /** + * POST /api/gdpr/cases/{id}/verify-identity — verify the data-subject's + * identity via the pack-selected identity-verify seam provider. + * + * Resolves the provider named by the case's active `dsarPolicyPack` + * `identityVerifyProvider` selector through {@see IdentityVerifyRegistry} + * (never a hardcoded provider) and runs it. Resolution is FAIL-CLOSED: an + * unset/unknown selector resolves to the OR default that returns `needs-more` + * (never `verified`), so a missing provider can never pass identity + * verification (ADR-005 / CWE-863). The three-state result is recorded on the + * case and returned to the caller so the case flow can drive its transition. + * + * @param string $id The case uuid. + * + * @return JSONResponse + * + * @NoAdminRequired + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function identityVerify(string $id): JSONResponse + { + $guard = $this->guardCase(caseUuid: $id); + if ($guard !== null) { + return $guard; + } + + $case = $this->accessor->load(caseUuid: $id); + if ($case === null) { + return new JSONResponse( + data: ['error' => 'Not found or not authorised.'], + statusCode: Http::STATUS_NOT_FOUND + ); + } + + $data = $case->getObject(); + + // Resolve the provider from the active pack selector through the + // registry — never a hardcoded provider. resolve() ALWAYS returns a + // provider (the fail-closed default when unset/unknown), so there is no + // null branch to (mis)treat as "verified". + $selectorId = $this->packResolver->identityVerifyProviderId(case: $data); + $provider = $this->identityRegistry->resolve(selectorId: $selectorId); + $result = $provider->verify(caseUuid: $id, case: $data); + + // Record the three-state outcome on the case (audited via the accessor). + $data['identityVerification'] = $result->toArray(); + try { + $this->accessor->save(case: $case, data: $data); + } catch (Throwable $e) { + // Persisting the record failed, but the verification DECISION stands + // — surface it as a 400 rather than pretend the case verified. + return new JSONResponse( + data: ['error' => $e->getMessage(), 'status' => $result->getStatus()], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + return new JSONResponse(data: $result->toArray()); + }//end identityVerify() + + /** + * POST /api/gdpr/cases/{id}/escalate — escalate the case to a supervisory + * authority via the pack-selected regulator-escalate seam provider. + * + * Resolves the provider named by the case's active `dsarPolicyPack` + * `regulatorEscalateProvider` selector through {@see RegulatorEscalateRegistry} + * (never a hardcoded provider) and runs it. Resolution is FAIL-CLOSED: an + * unset/unknown selector resolves to the OR default that REFUSES (never a + * silent success), so a case is never recorded as escalated when the seam is + * unbound (ADR-005 / CWE-863). Only an `escalated` outcome writes the + * `regulatorReference` that the dossier/denial-finalise gate reads. + * + * @param string $id The case uuid. + * + * @return JSONResponse + * + * @NoAdminRequired + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function escalate(string $id): JSONResponse + { + $guard = $this->guardCase(caseUuid: $id); + if ($guard !== null) { + return $guard; + } + + $case = $this->accessor->load(caseUuid: $id); + if ($case === null) { + return new JSONResponse( + data: ['error' => 'Not found or not authorised.'], + statusCode: Http::STATUS_NOT_FOUND + ); + } + + $data = $case->getObject(); + + $selectorId = $this->packResolver->regulatorEscalateProviderId(case: $data); + $provider = $this->regulatorRegistry->resolve(selectorId: $selectorId); + $result = $provider->escalate(caseUuid: $id, case: $data); + + // Record the outcome. Only a positive escalation writes the + // regulatorReference the denial-finalise gate + dossier consume — a + // refusal MUST NOT mint or claim a reference. + $data['regulatorEscalation'] = $result->toArray(); + if ($result->isEscalated() === true && $result->getReference() !== '') { + $data['regulatorReference'] = $result->getReference(); + } + + try { + $this->accessor->save(case: $case, data: $data); + } catch (Throwable $e) { + return new JSONResponse( + data: ['error' => $e->getMessage(), 'status' => $result->getStatus()], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + return new JSONResponse(data: $result->toArray()); + }//end escalate() + + /** + * Reject anonymous callers before any case is read or written. + * + * @return JSONResponse|null A 401 response when unauthenticated, else null. + */ + private function requireAuthenticated(): ?JSONResponse + { + if ($this->userSession->getUser() === null) { + return new JSONResponse( + data: ['error' => 'Authentication required.'], + statusCode: Http::STATUS_UNAUTHORIZED + ); + } + + return null; + }//end requireAuthenticated() + + /** + * Per-object RBAC + case-level access guard shared by every acting method. + * + * Loads the case under the caller's object RBAC (null when absent OR + * unauthorised — no enumeration oracle) and then applies the case-level + * access-control check (handler-scopes-own + officer-override, fail closed). + * Returns a response to short-circuit on denial, or null to proceed. + * + * @param string $caseUuid The case uuid from the route. + * + * @return JSONResponse|null Denial response, or null when the caller may act. + */ + private function guardCase(string $caseUuid): ?JSONResponse + { + $denied = $this->requireAuthenticated(); + if ($denied !== null) { + return $denied; + } + + $case = $this->accessor->load(caseUuid: $caseUuid); + if ($case === null) { + // Absent OR object-RBAC-denied — indistinguishable (no oracle). + return new JSONResponse( + data: ['error' => 'Not found or not authorised.'], + statusCode: Http::STATUS_NOT_FOUND + ); + } + + if ($this->accessControl->mayAct(case: $case->getObject()) === false) { + return new JSONResponse( + data: ['error' => 'You are not authorised to act on this case.'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + + return null; + }//end guardCase() +}//end class diff --git a/lib/Controller/DsarController.php b/lib/Controller/DsarController.php index 27abafd9c1..6ac4b48284 100644 --- a/lib/Controller/DsarController.php +++ b/lib/Controller/DsarController.php @@ -23,9 +23,9 @@ * @category Controller * @package OCA\OpenRegister\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -87,7 +87,7 @@ public function __construct( * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-6 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function inzage(): JSONResponse { @@ -136,7 +136,7 @@ public function inzage(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-6 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function portabiliteit(): JSONResponse { @@ -194,7 +194,7 @@ public function portabiliteit(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-6 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function vergetelheid(): JSONResponse { @@ -242,7 +242,7 @@ public function vergetelheid(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-6 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function rectificatie(): JSONResponse { @@ -298,7 +298,7 @@ public function rectificatie(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-6 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function compliance(): JSONResponse { diff --git a/lib/Controller/DuplicateController.php b/lib/Controller/DuplicateController.php new file mode 100644 index 0000000000..e36ec700a4 --- /dev/null +++ b/lib/Controller/DuplicateController.php @@ -0,0 +1,120 @@ + + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Exception\NotAuthorizedException; +use OCA\OpenRegister\Service\Quality\DuplicateDetectionService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use RuntimeException; + +class DuplicateController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The application name. + * @param IRequest $request The current request. + * @param DuplicateDetectionService $duplicates Duplicate-candidate detection service. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly DuplicateDetectionService $duplicates + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Paginated duplicate-candidate pairs for a register/schema, descending + * by similarity score. Read-only: no merge, no write, no side effects. + * Accepts an optional `threshold` (falls back to the schema's + * `x-openregister-dedup` annotation, then the service default) and + * `limit`/`offset` pagination params. + * + * @param string $register Register reference. + * @param string $schema Schema reference. + * + * @return JSONResponse JSON response with the paginated candidate pairs. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-2 + */ + public function index(string $register, string $schema): JSONResponse + { + $thresholdParam = $this->request->getParam('threshold'); + $threshold = null; + if ($thresholdParam !== null && (string) $thresholdParam !== '' && is_numeric($thresholdParam) === true) { + $threshold = (float) $thresholdParam; + } + + $limit = (int) $this->request->getParam('limit', 20); + $offset = (int) $this->request->getParam('offset', 0); + + if ($limit <= 0) { + $limit = 20; + } + + if ($offset < 0) { + $offset = 0; + } + + try { + $pairs = $this->duplicates->findDuplicates( + register: $register, + schema: $schema, + matchRules: null, + threshold: $threshold + ); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + $total = count($pairs); + $page = array_slice($pairs, $offset, $limit); + + return new JSONResponse( + [ + 'items' => $page, + 'total' => $total, + 'limit' => $limit, + 'offset' => $offset, + ] + ); + }//end index() +}//end class diff --git a/lib/Controller/EmailLinksController.php b/lib/Controller/EmailLinksController.php index 177b6e2f20..dd3b4e47d1 100644 --- a/lib/Controller/EmailLinksController.php +++ b/lib/Controller/EmailLinksController.php @@ -214,6 +214,8 @@ public function destroy(string $register, string $schema, string $id, string $li * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: EmailLinkService::getAvailableAccounts filters oc_mail_accounts by user_id = current UID; + * no caller-supplied object id. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-1 */ diff --git a/lib/Controller/EmailsController.php b/lib/Controller/EmailsController.php index 6466f13bf8..ecf9826ca6 100644 --- a/lib/Controller/EmailsController.php +++ b/lib/Controller/EmailsController.php @@ -16,8 +16,8 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-51 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mail-sidebar/spec.md + * @spec openspec/specs/mail-sidebar/spec.md */ declare(strict_types=1); @@ -109,7 +109,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index( string $register, @@ -162,7 +162,7 @@ public function index( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function create( string $register, @@ -228,7 +228,7 @@ public function create( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroy( string $register, @@ -272,7 +272,12 @@ public function destroy( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-51 + * @no-admin-idor-exempt Guarded downstream: EmailService::searchBySender scopes the + * mailbox scan to the caller's own Mail accounts (oc_mail_accounts.user_id = session + * UID) and returns [] for an anonymous session, so a caller only ever sees links for + * mail their own accounts received. + * + * @spec openspec/specs/mail-sidebar/spec.md */ public function search(): JSONResponse { @@ -328,7 +333,11 @@ private function validateObject( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-53 + * @no-admin-idor-exempt Guarded downstream: EmailService::searchBySender scopes the + * mailbox scan to the caller's own Mail accounts (oc_mail_accounts.user_id = session + * UID) and returns [] for an anonymous session. + * + * @spec openspec/specs/mail-sidebar/spec.md */ public function bySender(): JSONResponse { diff --git a/lib/Controller/EndpointsController.php b/lib/Controller/EndpointsController.php index d46e84796f..ad62c07e14 100644 --- a/lib/Controller/EndpointsController.php +++ b/lib/Controller/EndpointsController.php @@ -59,7 +59,7 @@ * * @SuppressWarnings(PHPMD.TooManyPublicMethods) One public method per endpoint route. * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md * * The resource-CRUD verbs (index/show/create/update/patch/destroy) are governed by task-1. * The test/logs/logStats/allLogs methods are a production-observability cross-cut (endpoint @@ -597,7 +597,7 @@ public function destroy(int $id): JSONResponse * statusCode?: int, response?: mixed}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ #[NoAdminRequired] #[NoCSRFRequired] @@ -684,7 +684,7 @@ public function test(int $id): JSONResponse * results?: list<\OCA\OpenRegister\Db\EndpointLog>, total?: int<0, max>}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md#requirement-per-entity-statistics-and-endpoint-delivery-log-api */ #[NoAdminRequired] #[NoCSRFRequired] @@ -759,7 +759,7 @@ public function logs(int $id): JSONResponse * 'Failed to retrieve endpoint log statistics', total?: int, * success?: int, failed?: int}, array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md#requirement-per-entity-statistics-and-endpoint-delivery-log-api */ #[NoAdminRequired] #[NoCSRFRequired] @@ -824,7 +824,7 @@ public function logStats(int $id): JSONResponse * total?: int<0, max>}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md#requirement-per-entity-statistics-and-endpoint-delivery-log-api */ #[NoAdminRequired] #[NoCSRFRequired] diff --git a/lib/Controller/EntityRelationsController.php b/lib/Controller/EntityRelationsController.php new file mode 100644 index 0000000000..c5702ec6c3 --- /dev/null +++ b/lib/Controller/EntityRelationsController.php @@ -0,0 +1,469 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/entity-relation-grondslagen/spec.md + * "A PATCH /api/entity-relations/{id} endpoint MUST exist with a decision-only field whitelist" + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Db\EmailLinkMapper; +use OCA\OpenRegister\Db\EntityRelation; +use OCA\OpenRegister\Db\EntityRelationMapper; +use OCA\OpenRegister\Db\MagicMapper; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Exception\CustomValidationException; +use OCA\OpenRegister\Service\Object\PermissionHandler; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\Files\File; +use OCP\Files\IRootFolder; +use OCP\IRequest; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Handles the PATCH endpoint for entity-relation decision metadata. + */ +class EntityRelationsController extends Controller +{ + /** + * Constructor. + * + * @param string $appName Nextcloud app name. + * @param IRequest $request Current request. + * @param EntityRelationMapper $entityRelationMapper Relation mapper. + * @param IUserSession $userSession Session user accessor. + * @param IRootFolder $rootFolder Nextcloud root folder (for file lookups). + * @param PermissionHandler $permissionHandler RBAC verdict for object-bound relations. + * @param MagicMapper $magicMapper ObjectEntity loader (for object-bound authz). + * @param SchemaMapper $schemaMapper Schema loader (for hasPermission inputs). + * @param RegisterMapper $registerMapper Register loader (for object lookup disambiguation). + * @param EmailLinkMapper $emailLinkMapper EmailLink loader (for email-bound authz indirection). + * @param LoggerInterface $logger Structured log sink. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly EntityRelationMapper $entityRelationMapper, + private readonly IUserSession $userSession, + private readonly IRootFolder $rootFolder, + private readonly PermissionHandler $permissionHandler, + private readonly MagicMapper $magicMapper, + private readonly SchemaMapper $schemaMapper, + private readonly RegisterMapper $registerMapper, + private readonly EmailLinkMapper $emailLinkMapper, + private readonly LoggerInterface $logger, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * PATCH /api/entity-relations/{id}. + * + * Body: { bases?: ?array, skipAnonymization?: bool } — any other + * key triggers HTTP 400. + * + * @param int $id Relation row id. + * + * @return JSONResponse Updated relation (200) or a structured error. + * + * @NoAdminRequired + */ + public function update(int $id): JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse( + data: ['error' => 'unauthenticated'], + statusCode: Http::STATUS_UNAUTHORIZED + ); + } + + try { + $relation = $this->entityRelationMapper->find($id); + } catch (DoesNotExistException $e) { + return new JSONResponse( + data: ['error' => 'not_found', 'id' => $id], + statusCode: Http::STATUS_NOT_FOUND + ); + } + + if ($this->actorCanWriteRelationSubject(relation: $relation, userId: $user->getUID()) === false) { + $this->logger->info( + message: '[EntityRelationsController] PATCH denied — no write-access to relation subject', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'relation_id' => $id, + 'actor' => $user->getUID(), + ] + ); + return new JSONResponse( + data: ['error' => 'forbidden', 'reason' => 'no_write_access_to_relation_subject'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + + // Require explicit application/json — anything else and Nextcloud's + // body parser may silently drop unknown content-types or coerce + // form-encoded array values inconsistently. Decision-time + // mutations on legal grondslagen must not depend on content-type + // heuristics; clients send JSON or get 415. + $contentType = (string) $this->request->getHeader('Content-Type'); + // Strip any trailing parameters (charset, boundary, etc.). + $mediaType = strtolower(trim(explode(';', $contentType, 2)[0])); + if ($mediaType !== 'application/json' && $mediaType !== '') { + return new JSONResponse( + data: [ + 'error' => 'unsupported_media_type', + 'reason' => 'PATCH /api/entity-relations/{id} requires Content-Type: application/json', + ], + statusCode: Http::STATUS_UNSUPPORTED_MEDIA_TYPE + ); + } + + $body = $this->request->getParams(); + // Strip framework-injected keys; only the JSON body fields remain candidates. + unset($body['id'], $body['_route']); + + try { + $relation = $this->entityRelationMapper->updateDecisionMetadata( + relation: $relation, + fields: $body, + actingUser: $user + ); + } catch (CustomValidationException $e) { + return new JSONResponse( + data: ['error' => $e->getMessage(), 'details' => $e->getErrors()], + statusCode: Http::STATUS_BAD_REQUEST + ); + } catch (\Throwable $e) { + $this->logger->error( + message: '[EntityRelationsController] PATCH failed: '.$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'relation_id' => $id, + 'error' => $e->getMessage(), + ] + ); + return new JSONResponse( + data: ['error' => 'internal_error'], + statusCode: Http::STATUS_INTERNAL_SERVER_ERROR + ); + }//end try + + return new JSONResponse( + data: $relation->jsonSerialize(), + statusCode: Http::STATUS_OK + ); + }//end update() + + /** + * Check that the acting user can write the relation's parent file or object. + * + * Resolution order (per `entity-relation-grondslagen` design.md §authz): + * 1. If the relation has `fileId` — the file MUST be present in the + * acting user's user-folder and `isUpdateable()` MUST return true. + * 2. Else if the relation has `objectId` — the caller MUST hold + * `update` on the underlying ObjectEntity per the same + * PermissionHandler verdict the rest of OR uses for object writes. + * 3. Else if the relation has `emailId` — the email's parent + * ObjectEntity is looked up via EmailLink, then the same + * object-update verdict applies. Email links without a parent + * object cannot satisfy an authz check and are denied. + * 4. Otherwise — deny. + * + * Every denial path is logged at `info` (expected operator denials) + * or `warning` (lookup failures) so unexpected drift is visible. + * + * @param EntityRelation $relation Relation row. + * @param string $userId Acting user UID. + * + * @return bool True when the acting user may write. + */ + private function actorCanWriteRelationSubject(EntityRelation $relation, string $userId): bool + { + $fileId = $relation->getFileId(); + if ($fileId !== null && $fileId > 0) { + return $this->canWriteFile(fileId: $fileId, userId: $userId); + } + + $objectId = $relation->getObjectId(); + if ($objectId !== null && $objectId > 0) { + return $this->canUpdateObject( + objectId: $objectId, + registerId: $relation->getRegisterId(), + schemaId: $relation->getSchemaId(), + userId: $userId + ); + } + + $emailId = $relation->getEmailId(); + if ($emailId !== null && $emailId > 0) { + return $this->canUpdateEmailParentObject(emailId: $emailId, userId: $userId); + } + + return false; + }//end actorCanWriteRelationSubject() + + /** + * File-bound authz: the file MUST be reachable in the acting user's + * user-folder and writable (isUpdateable). Mirrors how the rest of + * OR's per-file authz reads. + * + * @param int $fileId Nextcloud file node id. + * @param string $userId Acting user UID. + * + * @return bool + */ + private function canWriteFile(int $fileId, string $userId): bool + { + try { + $userFolder = $this->rootFolder->getUserFolder($userId); + $nodes = $userFolder->getById($fileId); + if (empty($nodes) === true) { + return false; + } + + $node = $nodes[0]; + if (($node instanceof File) === false) { + return false; + } + + return $node->isUpdateable(); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[EntityRelationsController] File-bound auth check raised exception, denying', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'file_id' => $fileId, + 'error' => $e->getMessage(), + ] + ); + return false; + }//end try + }//end canWriteFile() + + /** + * Object-bound authz: caller MUST hold `update` permission on the + * specific ObjectEntity. Reuses `PermissionHandler::hasPermission` + * — the same verdict the rest of OR runs for object writes, so this + * PATCH cannot grant access the saveObject path would have denied. + * + * Lookup uses register + schema disambiguation when both are + * available (the multi-table magic schema means object_id is not + * globally unique). When either is missing on the relation, falls + * back to the magic-mapper's cross-table find, which yields + * `DoesNotExistException` if no match — denied. + * + * @param int $objectId Object row id on its magic table. + * @param string|null $registerId Register identifier (uuid or slug). + * @param string|null $schemaId Schema identifier (uuid or slug). + * @param string $userId Acting user UID. + * + * @return bool + */ + private function canUpdateObject( + int $objectId, + ?string $registerId, + ?string $schemaId, + string $userId + ): bool { + try { + $register = null; + if ($registerId !== null) { + $register = $this->registerMapper->find($registerId); + } + + $schema = null; + if ($schemaId !== null) { + $schema = $this->schemaMapper->find($schemaId); + } + + $object = $this->magicMapper->find( + identifier: $objectId, + register: $register, + schema: $schema + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[EntityRelationsController] Object-bound auth check could not resolve object, denying', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'object_id' => $objectId, + 'register_id' => $registerId, + 'schema_id' => $schemaId, + 'error' => $e->getMessage(), + ] + ); + return false; + }//end try + + $resolvedSchemaId = (string) $object->getSchema(); + if ($schemaId !== null) { + $resolvedSchemaId = $schemaId; + } + + try { + $resolvedSchema = $this->schemaMapper->find($resolvedSchemaId); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[EntityRelationsController] Object-bound auth check could not load schema, denying', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'object_id' => $objectId, + 'schema_id' => $resolvedSchemaId, + 'error' => $e->getMessage(), + ] + ); + return false; + } + + return $this->permissionHandler->hasPermission( + schema: $resolvedSchema, + action: 'update', + userId: $userId, + objectOwner: $object->getOwner(), + object: $object + ); + }//end canUpdateObject() + + /** + * Email-bound authz: emails are linked to an ObjectEntity via + * EmailLink, so the operator must hold `update` on the parent + * object. Email links without a parent object cannot satisfy + * authz and are denied (orphan/system-generated links). + * + * @param int $emailId EmailLink row id. + * @param string $userId Acting user UID. + * + * @return bool + */ + private function canUpdateEmailParentObject(int $emailId, string $userId): bool + { + try { + $emailLink = $this->emailLinkMapper->find($emailId); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[EntityRelationsController] Email-bound auth check could not load EmailLink, denying', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'email_id' => $emailId, + 'error' => $e->getMessage(), + ] + ); + return false; + } + + $parentObjectUuid = $emailLink->getObjectUuid(); + $parentRegisterId = $emailLink->getRegisterId(); + // EmailLink's `@method string getObjectUuid()` claims non-nullable, + // but the underlying column is `?string`. Guard via empty() so we + // catch both null and empty-string parents. + if (empty($parentObjectUuid) === true) { + $this->logger->info( + message: '[EntityRelationsController] Email-bound PATCH denied — EmailLink has no parent object', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'email_id' => $emailId, + 'actor' => $userId, + ] + ); + return false; + } + + try { + // EmailLink's `@method int getRegisterId()` is non-nullable in + // the docblock; defensively coerce via empty() in case the + // column is unexpectedly null at runtime. + $register = null; + if (empty($parentRegisterId) === false) { + $register = $this->registerMapper->find((string) $parentRegisterId); + } + + $object = $this->magicMapper->find( + identifier: $parentObjectUuid, + register: $register + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[EntityRelationsController] Email-bound auth check could not resolve parent object, denying', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'email_id' => $emailId, + 'object_uuid' => $parentObjectUuid, + 'error' => $e->getMessage(), + ] + ); + return false; + }//end try + + try { + $schema = $this->schemaMapper->find((string) $object->getSchema()); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[EntityRelationsController] Email-bound auth check could not load parent schema, denying', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'email_id' => $emailId, + 'schema_id' => $object->getSchema(), + 'error' => $e->getMessage(), + ] + ); + return false; + } + + return $this->permissionHandler->hasPermission( + schema: $schema, + action: 'update', + userId: $userId, + objectOwner: $object->getOwner(), + object: $object + ); + }//end canUpdateEmailParentObject() +}//end class diff --git a/lib/Controller/FederationController.php b/lib/Controller/FederationController.php new file mode 100644 index 0000000000..be6a981455 --- /dev/null +++ b/lib/Controller/FederationController.php @@ -0,0 +1,542 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Db\FederatedShare; +use OCA\OpenRegister\Db\FederatedShareMapper; +use OCA\OpenRegister\Service\FederationShareService; +use OCA\OpenRegister\Service\ObjectService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Token-scoped federation serving endpoints (read; write-through in a later phase). + */ +class FederationController extends Controller +{ + + /** + * Confidentiality values treated as public (servable through a schema share). + * + * @var string[] + */ + private const PUBLIC_CONFIDENTIALITY = ['', 'openbaar', 'public', 'open']; + + /** + * Constructor. + * + * @param string $appName App name. + * @param IRequest $request Current request. + * @param FederatedShareMapper $shareMapper Federated-share persistence. + * @param ObjectService $objectService Object read service. + * @param FederationShareService $shareService Share management (list/create/revoke). + * @param LoggerInterface $logger Logger. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly FederatedShareMapper $shareMapper, + private readonly ObjectService $objectService, + private readonly FederationShareService $shareService, + private readonly LoggerInterface $logger, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * List the active organisation's federated shares. + * + * @return JSONResponse `{ results, total }`. + */ + #[NoAdminRequired] + public function shares(): JSONResponse + { + $direction = $this->request->getParam('direction'); + if ($direction !== null) { + $direction = (string) $direction; + } + + $shares = $this->shareService->listShares(direction: $direction); + + return new JSONResponse(data: ['results' => $shares, 'total' => count($shares)]); + }//end shares() + + /** + * Create an outgoing federated share and return it (with its token). + * + * @return JSONResponse The created share, or a validation error. + */ + #[NoAdminRequired] + public function createShare(): JSONResponse + { + $params = [ + 'scope' => $this->request->getParam('scope', 'schema'), + 'register' => $this->request->getParam('register'), + 'schema' => $this->request->getParam('schema'), + 'objectUri' => $this->request->getParam('objectUri'), + 'queryFilter' => $this->request->getParam('queryFilter'), + 'permissions' => $this->request->getParam('permissions', 'read'), + 'sharedWith' => $this->request->getParam('sharedWith'), + 'remoteInstanceUrl' => $this->request->getParam('remoteInstanceUrl'), + ]; + + try { + $share = $this->shareService->createOutgoingShare(params: $params); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: Http::STATUS_BAD_REQUEST); + } catch (Throwable $e) { + $this->logger->error('[Federation] create share failed: '.$e->getMessage()); + return new JSONResponse(data: ['error' => 'Could not create share'], statusCode: Http::STATUS_INTERNAL_SERVER_ERROR); + } + + return new JSONResponse(data: $share, statusCode: Http::STATUS_CREATED); + }//end createShare() + + /** + * Revoke a federated share. + * + * @param int $id The share id. + * + * @return JSONResponse The revoked share, or an error. + */ + #[NoAdminRequired] + public function revokeShare(int $id): JSONResponse + { + try { + $share = $this->shareService->setStatus(id: $id, status: 'revoked'); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Not found'], statusCode: Http::STATUS_NOT_FOUND); + } catch (Throwable $e) { + $this->logger->error('[Federation] revoke share failed: '.$e->getMessage()); + return new JSONResponse(data: ['error' => 'Could not revoke share'], statusCode: Http::STATUS_INTERNAL_SERVER_ERROR); + } + + return new JSONResponse(data: $share); + }//end revokeShare() + + /** + * Serve the objects a share grants (paginated). + * + * @param string $shareToken The scoped bearer share token. + * + * @return JSONResponse `{ results, total }` or an error. + */ + #[PublicPage] + #[NoCSRFRequired] + public function objects(string $shareToken): JSONResponse + { + $share = $this->resolveAcceptedShare(shareToken: $shareToken); + if ($share === null) { + return new JSONResponse(data: ['error' => 'Invalid, unaccepted or revoked share'], statusCode: Http::STATUS_FORBIDDEN); + } + + $config = $this->buildScopeConfig(share: $share); + $config['limit'] = (int) $this->request->getParam('_limit', 50); + $config['offset'] = (int) $this->request->getParam('_offset', 0); + + $search = $this->request->getParam('_search'); + if ($search !== null && $search !== '') { + $config['search'] = (string) $search; + } + + try { + $this->setServeContext(share: $share); + $objects = $this->objectService->findAll(config: $config, _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + $this->logger->error('[Federation] serve objects failed: '.get_class($e).': '.$e->getMessage().' @ '.$e->getFile().':'.$e->getLine()); + return new JSONResponse(data: ['error' => 'Could not read shared objects'], statusCode: Http::STATUS_INTERNAL_SERVER_ERROR); + } + + $objects = $this->normalize(objects: $objects); + $objects = $this->applyShareVisibility(objects: $objects, share: $share); + + return new JSONResponse(data: ['results' => $objects, 'total' => count($objects)]); + }//end objects() + + /** + * Serve a single shared object by id/uuid. + * + * @param string $shareToken The scoped bearer share token. + * @param string $id The object id or uuid. + * + * @return JSONResponse The object or an error. + */ + #[PublicPage] + #[NoCSRFRequired] + public function object(string $shareToken, string $id): JSONResponse + { + $share = $this->resolveAcceptedShare(shareToken: $shareToken); + if ($share === null) { + return new JSONResponse(data: ['error' => 'Invalid, unaccepted or revoked share'], statusCode: Http::STATUS_FORBIDDEN); + } + + try { + $entity = $this->objectService->find( + id: $id, + register: $share->getRegister(), + schema: $share->getSchema(), + _rbac: false, + _multitenancy: false + ); + } catch (Throwable $e) { + $this->logger->error('[Federation] serve object failed: '.$e->getMessage()); + return new JSONResponse(data: ['error' => 'Could not read shared object'], statusCode: Http::STATUS_INTERNAL_SERVER_ERROR); + } + + if ($entity === null) { + return new JSONResponse(data: ['error' => 'Not found'], statusCode: Http::STATUS_NOT_FOUND); + } + + $visible = $this->applyShareVisibility(objects: [$entity->jsonSerialize()], share: $share); + if (count($visible) === 0) { + return new JSONResponse(data: ['error' => 'Not found'], statusCode: Http::STATUS_NOT_FOUND); + } + + return new JSONResponse(data: $visible[0]); + }//end object() + + /** + * Create an object in a shared register/schema (read-write shares only). + * + * @param string $shareToken The scoped bearer share token. + * + * @return JSONResponse The created object, or an error. + */ + #[PublicPage] + #[NoCSRFRequired] + public function createObject(string $shareToken): JSONResponse + { + $share = $this->resolveWritableShare(shareToken: $shareToken); + if ($share === null) { + return new JSONResponse(data: ['error' => 'Invalid share or read-only'], statusCode: Http::STATUS_FORBIDDEN); + } + + $data = (array) $this->request->getParams(); + unset($data['shareToken'], $data['_route']); + // Pin the object to the sharing organisation — a federated writer can + // never plant an object into another organisation. + $data['@self'] = (($data['@self'] ?? []) + ['organisation' => $share->getOrganisation()]); + + try { + $saved = $this->objectService->saveObject( + object: $data, + register: $share->getRegister(), + schema: $share->getSchema(), + _rbac: false, + _multitenancy: false + ); + } catch (Throwable $e) { + $this->logger->error('[Federation] create object failed: '.$e->getMessage()); + return new JSONResponse(data: ['error' => 'Could not create object'], statusCode: Http::STATUS_INTERNAL_SERVER_ERROR); + } + + return new JSONResponse(data: $saved->jsonSerialize(), statusCode: Http::STATUS_CREATED); + }//end createObject() + + /** + * Update a shared object (read-write shares only). + * + * @param string $shareToken The scoped bearer share token. + * @param string $id The object id/uuid. + * + * @return JSONResponse The updated object, or an error. + */ + #[PublicPage] + #[NoCSRFRequired] + public function updateObject(string $shareToken, string $id): JSONResponse + { + $share = $this->resolveWritableShare(shareToken: $shareToken); + if ($share === null) { + return new JSONResponse(data: ['error' => 'Invalid share or read-only'], statusCode: Http::STATUS_FORBIDDEN); + } + + $data = (array) $this->request->getParams(); + unset($data['shareToken'], $data['id'], $data['_route']); + $data['@self'] = (($data['@self'] ?? []) + ['organisation' => $share->getOrganisation()]); + + try { + $saved = $this->objectService->saveObject( + object: $data, + register: $share->getRegister(), + schema: $share->getSchema(), + uuid: $id, + _rbac: false, + _multitenancy: false + ); + } catch (Throwable $e) { + $this->logger->error('[Federation] update object failed: '.$e->getMessage()); + return new JSONResponse(data: ['error' => 'Could not update object'], statusCode: Http::STATUS_INTERNAL_SERVER_ERROR); + } + + return new JSONResponse(data: $saved->jsonSerialize()); + }//end updateObject() + + /** + * Delete a shared object (read-write shares only). + * + * @param string $shareToken The scoped bearer share token. + * @param string $id The object id/uuid. + * + * @return JSONResponse Success, or an error. + */ + #[PublicPage] + #[NoCSRFRequired] + public function deleteObject(string $shareToken, string $id): JSONResponse + { + $share = $this->resolveWritableShare(shareToken: $shareToken); + if ($share === null) { + return new JSONResponse(data: ['error' => 'Invalid share or read-only'], statusCode: Http::STATUS_FORBIDDEN); + } + + try { + $ok = $this->objectService->deleteObject( + uuid: $id, + register: $share->getRegister(), + schema: $share->getSchema(), + _rbac: false, + _multitenancy: false + ); + } catch (Throwable $e) { + $this->logger->error('[Federation] delete object failed: '.$e->getMessage()); + return new JSONResponse(data: ['error' => 'Could not delete object'], statusCode: Http::STATUS_INTERNAL_SERVER_ERROR); + } + + return new JSONResponse(data: ['deleted' => $ok]); + }//end deleteObject() + + /** + * Resolve a share that grants write access (accepted + read-write), or null. + * + * @param string $shareToken The scoped bearer share token. + * + * @return FederatedShare|null The writable share, else null. + */ + private function resolveWritableShare(string $shareToken): ?FederatedShare + { + $share = $this->resolveAcceptedShare(shareToken: $shareToken); + if ($share === null || $share->getPermissions() !== 'read-write') { + return null; + } + + return $share; + }//end resolveWritableShare() + + /** + * Describe a share (scope, register/schema, permissions) so the receiving + * instance can provision a shadow schema bound to the federated source. + * + * @param string $shareToken The scoped bearer share token. + * + * @return JSONResponse The share descriptor or an error. + */ + #[PublicPage] + #[NoCSRFRequired] + public function meta(string $shareToken): JSONResponse + { + $share = $this->resolveAcceptedShare(shareToken: $shareToken); + if ($share === null) { + return new JSONResponse(data: ['error' => 'Invalid, unaccepted or revoked share'], statusCode: Http::STATUS_FORBIDDEN); + } + + return new JSONResponse( + data: [ + 'scope' => $share->getScope(), + 'register' => $share->getRegister(), + 'schema' => $share->getSchema(), + 'permissions' => $share->getPermissions(), + ] + ); + }//end meta() + + /** + * Resolve an outgoing, accepted share by token, or null. + * + * @param string $shareToken The scoped bearer share token. + * + * @return FederatedShare|null The share when valid + accepted, else null. + */ + private function resolveAcceptedShare(string $shareToken): ?FederatedShare + { + if ($shareToken === '') { + return null; + } + + try { + $share = $this->shareMapper->findByToken(shareToken: $shareToken); + } catch (DoesNotExistException $e) { + return null; + } catch (Throwable $e) { + $this->logger->warning('[Federation] token lookup failed: '.$e->getMessage()); + return null; + } + + // Only outgoing shares are served here, and only once accepted. + if ($share->getDirection() !== 'outgoing' || $share->getStatus() === 'revoked' || $share->getStatus() === 'declined') { + return null; + } + + return $share; + }//end resolveAcceptedShare() + + /** + * Build the ObjectService findAll config for a share's scope. + * + * @param FederatedShare $share The share being served. + * + * @return array The findAll config (filters + scope narrowing). + */ + private function buildScopeConfig(FederatedShare $share): array + { + /* + * Register/schema are set as ObjectService CONTEXT (setServeContext), not + * as domain filters: a magic table's system columns are underscore-prefixed + * (_register/_schema/_organisation), so they are not valid domain filters. + * The organisation + confidentiality guards run in PHP (applyShareVisibility). + */ + + $filters = []; + + if ($share->getScope() === 'object' && $share->getObjectUri() !== null && $share->getObjectUri() !== '') { + // The objectUri stores the object uuid for an object-scope share. + $filters['uuid'] = $this->uuidFromUri(uri: $share->getObjectUri()); + } + + if ($share->getScope() === 'query' && is_array($share->getQueryFilter()) === true) { + $filters = array_merge($filters, $share->getQueryFilter()); + } + + return ['filters' => $filters]; + }//end buildScopeConfig() + + /** + * Set the register/schema context on ObjectService for a share. + * + * @param FederatedShare $share The share being served. + * + * @return void + */ + private function setServeContext(FederatedShare $share): void + { + if ($share->getRegister() !== null && $share->getRegister() !== '') { + $this->objectService->setRegister(register: $share->getRegister()); + } + + if ($share->getSchema() !== null && $share->getSchema() !== '') { + $this->objectService->setSchema(schema: $share->getSchema()); + } + }//end setServeContext() + + /** + * Normalise a findAll result to plain arrays (entities → jsonSerialize). + * + * @param array $objects The raw findAll result. + * + * @return array> The rendered arrays. + */ + private function normalize(array $objects): array + { + return array_map( + static function ($object) { + if (is_array($object) === true) { + return $object; + } + + if ($object instanceof \JsonSerializable) { + return (array) $object->jsonSerialize(); + } + + return (array) $object; + }, + $objects + ); + }//end normalize() + + /** + * Enforce confidentiality visibility for the share scope. + * + * Object-scope shares serve exactly the granted object (the sharer chose it). + * Register/schema/query shares never leak an object marked confidential. + * + * @param array> $objects The rendered objects. + * @param FederatedShare $share The share being served. + * + * @return array> The visible objects. + */ + private function applyShareVisibility(array $objects, FederatedShare $share): array + { + $org = $share->getOrganisation(); + + return array_values( + array_filter( + $objects, + function (array $object) use ($share, $org) { + // Organisation guard: never serve another org's objects. + if ($org !== null && $org !== '') { + $self = ($object['@self'] ?? []); + $objOrg = (string) ($self['organisation'] ?? $object['organisation'] ?? ''); + if ($objOrg !== $org) { + return false; + } + } + + // Confidentiality guard for non-object scopes (object shares + // serve exactly the one object the sharer chose). + if ($share->getScope() !== 'object') { + $confidentiality = strtolower((string) ($object['confidentiality'] ?? '')); + if (in_array($confidentiality, self::PUBLIC_CONFIDENTIALITY, true) === false) { + return false; + } + } + + return true; + } + ) + ); + }//end applyShareVisibility() + + /** + * Extract the trailing uuid from a canonical object uri (or return it as-is). + * + * @param string $uri The object uri or uuid. + * + * @return string The uuid. + */ + private function uuidFromUri(string $uri): string + { + $parts = explode('/', rtrim($uri, '/')); + return (string) end($parts); + }//end uuidFromUri() +}//end class diff --git a/lib/Controller/FileExtractionController.php b/lib/Controller/FileExtractionController.php index 91c943b527..b8e0e3073b 100644 --- a/lib/Controller/FileExtractionController.php +++ b/lib/Controller/FileExtractionController.php @@ -32,8 +32,11 @@ use OCA\OpenRegister\Db\EntityRelationMapper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; +use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; +use OCP\IGroupManager; use OCP\IRequest; +use OCP\IUserSession; /** * FileExtractionController @@ -65,8 +68,11 @@ class FileExtractionController extends Controller * @param ChunkMapper $chunkMapper Chunk mapper for text chunks * @param EntityRelationMapper $entityRelationMapper Entity relation mapper * @param RiskLevelService $riskLevelService Risk level computation service + * @param IRootFolder $rootFolder Root folder for per-user file access checks + * @param IUserSession $userSession Active user session for caller identity + * @param IGroupManager $groupManager Group manager for admin checks * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( string $appName, @@ -75,11 +81,56 @@ public function __construct( private readonly VectorizationService $vectorizationService, private readonly ChunkMapper $chunkMapper, private readonly EntityRelationMapper $entityRelationMapper, - private readonly RiskLevelService $riskLevelService + private readonly RiskLevelService $riskLevelService, + private readonly IRootFolder $rootFolder, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager ) { parent::__construct(appName: $appName, request: $request); }//end __construct() + /** + * Whether the current session user can access the given Nextcloud file. + * + * Resolves the file through the caller's own user folder so Nextcloud's + * share/permission ACLs apply. A file the user cannot access resolves to + * no node — preventing IDOR where a caller extracts/inspects arbitrary + * file IDs they do not own. + * + * @param int $fileId Nextcloud file ID. + * + * @return bool True when the file is reachable in the caller's user folder. + */ + private function hasFileAccess(int $fileId): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + return empty($userFolder->getById($fileId)) === false; + }//end hasFileAccess() + + /** + * Check whether the currently authenticated user is a Nextcloud administrator. + * + * The instance-wide extraction maintenance operations (discover/extractAll/ + * retryFailed/cleanup/vectorizeBatch) act on every file in the instance, so + * they are admin-only. + * + * @return bool True if a user is signed in and belongs to the admin group. + */ + private function isCurrentUserAdmin(): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + return $this->groupManager->isAdmin($user->getUID()); + }//end isCurrentUserAdmin() + /** * Get all files tracked in the extraction system. * @@ -92,7 +143,7 @@ public function __construct( * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function index(): JSONResponse { @@ -223,7 +274,7 @@ function ($a, $b) use ($sort, $order, $riskOrder) { * sourceType: null|string, startOffset: int, updatedAt: null|string, * uuid: null|string, vectorized: bool}>}, array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function show(int $id): JSONResponse { @@ -287,10 +338,23 @@ public function show(int $id): JSONResponse * * @suppressWarnings(PHPMD.BooleanArgumentFlag) Force flag allows re-extraction bypass * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function extract(int $id, bool $forceReExtract=false): JSONResponse { + // IDOR guard: only extract files the caller can actually access. 404 + // (not 403) so a non-owner cannot probe which file IDs exist. + if ($this->hasFileAccess(fileId: $id) === false) { + return new JSONResponse( + data: [ + 'success' => false, + 'error' => 'File not found in Nextcloud', + 'message' => 'File not found or access denied', + ], + statusCode: 404 + ); + } + try { // ExtractFile returns void, not an object. $this->textExtractor->extractFile(fileId: $id, forceReExtract: $forceReExtract); @@ -352,10 +416,14 @@ public function extract(int $id, bool $forceReExtract=false): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function discover(int $limit=100): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['success' => false, 'error' => 'Admin privileges required'], 403); + } + try { $stats = $this->textExtractor->discoverUntrackedFiles($limit); @@ -403,10 +471,14 @@ public function discover(int $limit=100): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function extractAll(int $limit=100): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['success' => false, 'error' => 'Admin privileges required'], 403); + } + try { $stats = $this->textExtractor->extractPendingFiles($limit); @@ -451,10 +523,14 @@ public function extractAll(int $limit=100): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function retryFailed(int $limit=50): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['success' => false, 'error' => 'Admin privileges required'], 403); + } + try { $stats = $this->textExtractor->retryFailedExtractions($limit); @@ -486,6 +562,9 @@ public function retryFailed(int $limit=50): JSONResponse * * @NoCSRFRequired * + * @no-admin-idor-exempt No per-object resource: returns aggregate extraction + * counters (TextExtractionService::getStats); takes no caller-supplied file id. + * * @psalm-return JSONResponse< * 200|500, * array{ @@ -503,7 +582,7 @@ public function retryFailed(int $limit=50): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function stats(): JSONResponse { @@ -551,10 +630,14 @@ public function stats(): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function cleanup(): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['success' => false, 'error' => 'Admin privileges required'], 403); + } + try { // Note: cleanupInvalidEntries not available in TextExtractionService. return new JSONResponse( @@ -591,6 +674,9 @@ public function cleanup(): JSONResponse * * @NoCSRFRequired * + * @no-admin-idor-exempt No per-object resource: returns aggregate file-type + * counts (currently an empty stub); takes no caller-supplied file id. + * * @psalm-return JSONResponse< * 200|500, * array{ @@ -602,7 +688,7 @@ public function cleanup(): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function fileTypes(): JSONResponse { @@ -640,10 +726,14 @@ public function fileTypes(): JSONResponse * * @return JSONResponse JSON response with vectorization result * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function vectorizeBatch(): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['success' => false, 'error' => 'Admin privileges required'], 403); + } + try { $data = $this->request->getParams(); $mode = $data['mode'] ?? 'serial'; diff --git a/lib/Controller/FileSearchController.php b/lib/Controller/FileSearchController.php index 7a1e4e281a..5b1bb8343f 100644 --- a/lib/Controller/FileSearchController.php +++ b/lib/Controller/FileSearchController.php @@ -16,16 +16,15 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-87 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-full-text-search-across-object-properties */ declare(strict_types=1); namespace OCA\OpenRegister\Controller; -use OCA\OpenRegister\Service\IndexService; +use OCA\OpenRegister\Db\ChunkMapper; use OCA\OpenRegister\Service\VectorizationService; -use OCA\OpenRegister\Service\SettingsService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; @@ -48,164 +47,30 @@ class FileSearchController extends Controller /** * Constructor * - * @param string $appName App name - * @param IRequest $request Request object - * @param IndexService $indexService Index service - * @param VectorizationService $vectorService Vectorization service - * @param SettingsService $settingsService Settings service - * @param LoggerInterface $logger Logger + * @param string $appName App name + * @param IRequest $request Request object + * @param VectorizationService $vectorService Vectorization service + * @param ChunkMapper $chunkMapper Chunk mapper (ranked keyword arm) + * @param LoggerInterface $logger Logger */ public function __construct( string $appName, IRequest $request, - private readonly IndexService $indexService, private readonly VectorizationService $vectorService, - private readonly SettingsService $settingsService, + private readonly ChunkMapper $chunkMapper, private readonly LoggerInterface $logger ) { parent::__construct(appName: $appName, request: $request); }//end __construct() - /** - * Keyword search in file contents (SOLR full-text search) - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @return JSONResponse Search results - * - * @suppressWarnings(PHPMD.ExcessiveMethodLength) - * @suppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-87 - */ - public function keywordSearch(): JSONResponse - { - try { - $query = $this->request->getParam('query', ''); - $limit = (int) $this->request->getParam('limit', 10); - $offset = (int) $this->request->getParam('offset', 0); - $fileTypes = $this->request->getParam('file_types', []); - - if (empty($query) === true) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Query parameter is required', - ], - statusCode: 400 - ); - } - - // Get file collection. - $settings = $this->settingsService->getSettings(); - $fileCollection = $settings['solr']['fileCollection'] ?? null; - if ($fileCollection === null || $fileCollection === '') { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'File collection not configured', - ], - statusCode: 422 - ); - } - - // Build SOLR query. - $solrQuery = [ - 'q' => "text_content:($query)", - 'rows' => $limit, - 'start' => $offset, - 'fl' => 'file_id,file_name,file_path,mime_type,chunk_index,chunk_text,score', - 'sort' => 'score desc', - ]; - - // Add file type filter if specified. - if (empty($fileTypes) === false) { - $typeFilter = implode(' OR ', array_map(fn(string $t): string => "mime_type:\"$t\"", $fileTypes)); - $solrQuery['fq'] = $typeFilter; - } - - // Execute search. - $queryUrl = $this->indexService->getEndpointUrl().'/'.$fileCollection.'/select'; - $solrConfig = $this->settingsService->getSettings()['solr'] ?? []; - - $requestOptions = [ - 'query' => $solrQuery, - 'timeout' => $solrConfig['timeout'] ?? 30, - ]; - - // Add authentication. - if (empty($solrConfig['username']) === false && empty($solrConfig['password']) === false) { - $requestOptions['auth'] = [$solrConfig['username'], $solrConfig['password']]; - } - - // - // @var \OCP\Http\Client\IClientService $clientService - $clientService = \OC::$server->get(\OCP\Http\Client\IClientService::class); - $httpClient = $clientService->newClient(); - $response = $httpClient->get(uri: $queryUrl, options: $requestOptions); - $result = json_decode($response->getBody()->getContents(), true); - - $results = $result['response']['docs'] ?? []; - $numFound = $result['response']['numFound'] ?? 0; - - // Group results by file_id. - $groupedResults = []; - foreach ($results as $doc) { - $fileId = $doc['file_id']; - if (isset($groupedResults[$fileId]) === false) { - $groupedResults[$fileId] = [ - 'file_id' => $fileId, - 'file_name' => $doc['file_name'] ?? '', - 'file_path' => $doc['file_path'] ?? '', - 'mime_type' => $doc['mime_type'] ?? '', - 'score' => $doc['score'] ?? 0, - 'chunks' => [], - ]; - } - - $groupedResults[$fileId]['chunks'][] = [ - 'chunk_index' => $doc['chunk_index'] ?? 0, - 'text' => $doc['chunk_text'] ?? '', - 'score' => $doc['score'] ?? 0, - ]; - } - - return new JSONResponse( - data: [ - 'success' => true, - 'query' => $query, - 'total' => $numFound, - 'results' => array_values($groupedResults), - 'search_type' => 'keyword', - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[FileSearchController] Keyword search failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Search failed: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end keywordSearch() - /** * Semantic search in file contents (vector similarity search) * * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt No caller-supplied id: free-text semantic search over the file vector index; + * takes a query string, not an object or file id. * * @return JSONResponse JSON response with search results or error * @@ -215,7 +80,7 @@ public function keywordSearch(): JSONResponse * search_type?: 'semantic'}, * array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 + * @spec openspec/changes/hybrid-document-search/tasks.md#4.1 */ public function semanticSearch(): JSONResponse { @@ -233,11 +98,13 @@ public function semanticSearch(): JSONResponse ); } - // Use existing semanticSearch method from VectorizationService. + // File-only scope: `entity_type` (snake_case) is the key + // VectorSearchHandler::fetchVectors() actually reads — the former + // `entityType` key was silently ignored (or#277). $results = $this->vectorService->semanticSearch( query: $query, limit: $limit, - filters: ['entityType' => 'file'] + filters: ['entity_type' => 'file'] ); return new JSONResponse( @@ -270,7 +137,13 @@ public function semanticSearch(): JSONResponse }//end semanticSearch() /** - * Hybrid search - Combines keyword (SOLR) and semantic (vector) search + * Hybrid search - fuses ranked keyword (tsvector/ts_rank) and semantic (vector) results + * + * The keyword arm is real (ChunkMapper::searchByKeyword() over file chunks, + * empty on non-PostgreSQL platforms) and is fused with the vector arm via + * Reciprocal Rank Fusion. The response is flat `{results, total, ...}`: + * `results` is the fused result list and `total` its count — not the nested + * service response with a wrong outer-key count (or#277). * * @NoAdminRequired * @@ -278,7 +151,7 @@ public function semanticSearch(): JSONResponse * * @return JSONResponse JSON response with hybrid search results or error * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 + * @spec openspec/changes/hybrid-document-search/tasks.md#4.2 */ public function hybridSearch(): JSONResponse { @@ -298,25 +171,34 @@ public function hybridSearch(): JSONResponse ); } - // Use existing hybridSearch method from VectorizationService. - $results = $this->vectorService->hybridSearch( + // Real keyword arm: ranked ts_rank results over file chunks, + // fetched with the same candidate-pool size as the vector leg. + $keywordResults = $this->chunkMapper->searchByKeyword( + query: $query, + limit: $limit * 2, + filters: ['source_type' => 'file'] + ); + + $serviceResponse = $this->vectorService->hybridSearch( query: $query, - solrFilters: ['entityType' => 'file'], + keywordResults: $keywordResults, limit: $limit, - weights: ['solr' => $keywordWeight, 'vector' => $semanticWeight] + weights: ['keyword' => $keywordWeight, 'vector' => $semanticWeight] ); return new JSONResponse( data: [ - 'success' => true, - 'query' => $query, - 'total' => count($results), - 'results' => $results, - 'search_type' => 'hybrid', - 'weights' => [ - 'keyword' => $keywordWeight, - 'semantic' => $semanticWeight, + 'success' => true, + 'query' => $query, + 'results' => $serviceResponse['results'] ?? [], + 'total' => $serviceResponse['total'] ?? count($serviceResponse['results'] ?? []), + 'search_time_ms' => $serviceResponse['search_time_ms'] ?? null, + 'source_breakdown' => $serviceResponse['source_breakdown'] ?? [], + 'weights' => $serviceResponse['weights'] ?? [ + 'keyword' => $keywordWeight, + 'vector' => $semanticWeight, ], + 'search_type' => 'hybrid', ] ); } catch (\Exception $e) { diff --git a/lib/Controller/FileSidebarController.php b/lib/Controller/FileSidebarController.php index 30a83ef7a8..de4d2e4595 100644 --- a/lib/Controller/FileSidebarController.php +++ b/lib/Controller/FileSidebarController.php @@ -29,7 +29,9 @@ use OCA\OpenRegister\Service\FileSidebarService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; +use OCP\Files\IRootFolder; use OCP\IRequest; +use OCP\IUserSession; use Psr\Log\LoggerInterface; /** @@ -52,16 +54,43 @@ class FileSidebarController extends Controller * @param IRequest $request HTTP request. * @param FileSidebarService $fileSidebarService File sidebar service. * @param LoggerInterface $logger Logger. + * @param IRootFolder $rootFolder Root folder for per-user file access checks. + * @param IUserSession $userSession Active user session for caller identity. */ public function __construct( string $appName, IRequest $request, private readonly FileSidebarService $fileSidebarService, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly IRootFolder $rootFolder, + private readonly IUserSession $userSession ) { parent::__construct(appName: $appName, request: $request); }//end __construct() + /** + * Whether the current session user can access the given Nextcloud file. + * + * Resolves the file through the caller's own user folder so Nextcloud's + * share/permission ACLs apply. A file the user cannot access resolves to + * no node — preventing IDOR where a caller inspects object links or + * extraction metadata for arbitrary file IDs they do not own. + * + * @param int $fileId Nextcloud file ID. + * + * @return bool True when the file is reachable in the caller's user folder. + */ + private function hasFileAccess(int $fileId): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + return empty($userFolder->getById($fileId)) === false; + }//end hasFileAccess() + /** * Get all OpenRegister objects that reference the given file. * @@ -76,6 +105,15 @@ public function __construct( */ public function getObjectsForFile(int $fileId): JSONResponse { + // IDOR guard: only reveal object links for files the caller can access. + // 404 (not 403) so a non-owner cannot probe which file IDs exist. + if ($this->hasFileAccess(fileId: $fileId) === false) { + return new JSONResponse( + data: ['success' => false, 'error' => 'File not found or access denied'], + statusCode: 404 + ); + } + try { $objects = $this->fileSidebarService->getObjectsForFile($fileId); @@ -114,6 +152,16 @@ public function getObjectsForFile(int $fileId): JSONResponse */ public function getExtractionStatus(int $fileId): JSONResponse { + // IDOR guard: extraction status reveals chunk counts, entity types and + // PII risk level. Only expose it for files the caller can access; 404 + // (not 403) so a non-owner cannot probe which file IDs exist. + if ($this->hasFileAccess(fileId: $fileId) === false) { + return new JSONResponse( + data: ['success' => false, 'error' => 'File not found or access denied'], + statusCode: 404 + ); + } + try { $status = $this->fileSidebarService->getExtractionStatus($fileId); diff --git a/lib/Controller/FileTextController.php b/lib/Controller/FileTextController.php index 144d69ecd2..a8f7f85ec1 100644 --- a/lib/Controller/FileTextController.php +++ b/lib/Controller/FileTextController.php @@ -26,14 +26,21 @@ use OCP\AppFramework\Http; use OCA\OpenRegister\Db\EntityRelationMapper; +use OCA\OpenRegister\Exception\ManualEntityException; +use OCA\OpenRegister\Exception\PdfAnonymisationException; +use OCA\OpenRegister\Service\File\ManualEntityResult; +use OCA\OpenRegister\Service\File\ManualEntityService; use OCA\OpenRegister\Service\FileService; use OCA\OpenRegister\Service\TextExtractionService; -use OCA\OpenRegister\Service\IndexService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; +use OCP\Files\IRootFolder; use OCP\IAppConfig; +use OCP\IGroupManager; use OCP\IRequest; +use OCP\IUserSession; use Psr\Log\LoggerInterface; +use Throwable; /** * FileTextController @@ -58,25 +65,72 @@ class FileTextController extends Controller * @param string $appName App name * @param IRequest $request Request object * @param TextExtractionService $textExtractor Text extraction service - * @param IndexService $indexService Index service for file operations * @param FileService $fileService File service for file operations * @param EntityRelationMapper $entityRelationMapper Entity relation mapper * @param LoggerInterface $logger Logger * @param IAppConfig $config Application configuration + * @param ManualEntityService $manualEntityService Orchestrator for the manual-entity write path + * @param IUserSession $userSession Session user accessor (for the manual-entity endpoint) + * @param IRootFolder $rootFolder Root folder for per-user file access checks + * @param IGroupManager $groupManager Group manager for admin checks */ public function __construct( string $appName, IRequest $request, private readonly TextExtractionService $textExtractor, - private readonly IndexService $indexService, private readonly FileService $fileService, private readonly EntityRelationMapper $entityRelationMapper, private readonly LoggerInterface $logger, - private readonly IAppConfig $config + private readonly IAppConfig $config, + private readonly ManualEntityService $manualEntityService, + private readonly IUserSession $userSession, + private readonly IRootFolder $rootFolder, + private readonly IGroupManager $groupManager ) { parent::__construct(appName: $appName, request: $request); }//end __construct() + /** + * Whether the current session user can access the given Nextcloud file. + * + * Resolves the file through the caller's own user folder so Nextcloud's + * share/permission ACLs apply. A file the user cannot access resolves to + * no node — preventing IDOR where a caller forces (re-)extraction of + * arbitrary file IDs they do not own. + * + * @param int $fileId Nextcloud file ID. + * + * @return bool True when the file is reachable in the caller's user folder. + */ + private function hasFileAccess(int $fileId): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + return empty($userFolder->getById($fileId)) === false; + }//end hasFileAccess() + + /** + * Check whether the currently authenticated user is a Nextcloud administrator. + * + * The bulk extraction endpoint processes pending files across the whole + * instance, so it is admin-only. + * + * @return bool True if a user is signed in and belongs to the admin group. + */ + private function isCurrentUserAdmin(): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + return $this->groupManager->isAdmin($user->getUID()); + }//end isCurrentUserAdmin() + /** * Get extracted text for a file * @@ -88,6 +142,9 @@ public function __construct( * * @return JSONResponse JSON response with file text or error * + * @no-admin-idor-exempt Deprecated no-op stub: returns HTTP 404 unconditionally + * and performs no file/object read; there is no per-object resource to guard. + * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 */ public function getFileText(int $fileId): JSONResponse @@ -140,6 +197,15 @@ public function getFileText(int $fileId): JSONResponse */ public function extractFileText(int $fileId): JSONResponse { + // IDOR guard: only (re-)extract files the caller can access. 404 (not + // 403) so a non-owner cannot probe which file IDs exist. + if ($this->hasFileAccess(fileId: $fileId) === false) { + return new JSONResponse( + data: ['success' => false, 'message' => 'File not found or access denied'], + statusCode: 404 + ); + } + $hasFileManagement = $this->config->hasKey(app: 'openregister', key: 'fileManagement'); $fileManagementConfig = json_decode( $this->config->getValueString(app: 'openregister', key: 'fileManagement'), @@ -199,6 +265,10 @@ public function extractFileText(int $fileId): JSONResponse */ public function bulkExtract(): JSONResponse { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['success' => false, 'message' => 'Admin privileges required'], 403); + } + try { $limit = (int) $this->request->getParam('limit', 100); $limit = min($limit, 500); @@ -242,6 +312,9 @@ public function bulkExtract(): JSONResponse * * @return JSONResponse JSON response with extraction stats * + * @no-admin-idor-exempt No per-object resource: returns aggregate extraction + * counters (TextExtractionService::getStats); takes no caller-supplied file id. + * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 */ public function getStats(): JSONResponse @@ -290,6 +363,15 @@ public function getStats(): JSONResponse */ public function deleteFileText(int $fileId): JSONResponse { + // IDOR guard: deletion targets a file by id — only permit it for files + // the caller can access (guards the stub before it is implemented). + if ($this->hasFileAccess(fileId: $fileId) === false) { + return new JSONResponse( + data: ['success' => false, 'message' => 'File not found or access denied'], + statusCode: 404 + ); + } + try { // TextExtractionService works with chunks. // TODO: Implement chunk deletion for file. @@ -322,149 +404,6 @@ public function deleteFileText(int $fileId): JSONResponse }//end try }//end deleteFileText() - /** - * Process extracted files and index their chunks to SOLR - * - * @param int|null $limit Maximum number of files to process - * @param int|null $chunkSize Chunk size in characters - * @param int|null $chunkOverlap Overlap between chunks in characters - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with indexing stats - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function processAndIndexExtracted(?int $limit=null, ?int $chunkSize=null, ?int $chunkOverlap=null): JSONResponse - { - try { - $options = []; - if ($chunkSize !== null) { - $options['chunk_size'] = $chunkSize; - } - - if ($chunkOverlap !== null) { - $options['chunk_overlap'] = $chunkOverlap; - } - - $result = $this->indexService->processUnindexedChunks(limit: $limit); - - return new JSONResponse(data: $result); - } catch (\Exception $e) { - $this->logger->error( - message: '[FileTextController] Failed to process extracted files', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to process extracted files: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end processAndIndexExtracted() - - /** - * Process and index a single extracted file - * - * @param int $fileId File ID - * @param int|null $chunkSize Chunk size in characters - * @param int|null $_chunkOverlap Overlap between chunks in characters (reserved for future use) - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @SuppressWarnings (PHPMD.UnusedFormalParameter) $_chunkOverlap reserved for future implementation - * - * @return JSONResponse JSON response with indexing result - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function processAndIndexFile(int $fileId, ?int $chunkSize=null, ?int $_chunkOverlap=null): JSONResponse - { - try { - $options = []; - if ($chunkSize !== null) { - $options['chunk_size'] = $chunkSize; - } - - // Process unindexed chunks for all files (fileId and options are not supported by current API). - // TODO: Implement file-specific chunk processing with chunk size/overlap options. - $result = $this->indexService->processUnindexedChunks(); - - return new JSONResponse(data: $result); - } catch (\Exception $e) { - $this->logger->error( - message: '[FileTextController] Failed to process file', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'file_id' => $fileId, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to process file: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end processAndIndexFile() - - /** - * Get chunking statistics - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with chunking stats - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function getChunkingStats(): JSONResponse - { - try { - $stats = $this->indexService->getChunkingStats(); - - return new JSONResponse( - data: [ - 'success' => true, - 'stats' => $stats, - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[FileTextController] Failed to get chunking stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to get chunking stats: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end getChunkingStats() - /** * Anonymize a file by replacing detected entities with placeholders * @@ -481,6 +420,7 @@ public function getChunkingStats(): JSONResponse * @return JSONResponse JSON response with anonymization result * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 + * @spec openspec/specs/pdf-anonymisation/spec.md */ public function anonymizeFile(int $fileId): JSONResponse { @@ -514,8 +454,11 @@ public function anonymizeFile(int $fileId): JSONResponse ); } - // Get detected entities for this file. - $entityData = $this->entityRelationMapper->findEntitiesForFile($fileId); + // Get detected entities for this file, excluding those the operator + // has flagged with skip_anonymization=true. Per the + // `entity-relation-grondslagen` change, the anonymise pass MUST NOT + // redact rows that carry an operator skip decision. + $entityData = $this->entityRelationMapper->findEntitiesForAnonymization($fileId); if (empty($entityData) === true) { return new JSONResponse( @@ -558,17 +501,63 @@ public function anonymizeFile(int $fileId): JSONResponse ] ); - // Perform anonymization. - $anonymizedFile = $this->fileService->anonymizeDocument($fileNode, $entities); + // Perform anonymization. `anonymizeDocument` is the canonical + // caller of `EntityRelationMapper::markAsAnonymized` per the + // `entity-relation-grondslagen` Requirement on canonical + // marking ownership: marking the source's relation rows is the + // responsibility of the redaction path itself (which exists on + // every entry point — HTTP, DI, event listeners), not the + // controller (which is bypassed by the non-HTTP paths). A + // controller-side mark would conflict with the DocumentProcessingHandler's + // mark on this same fileId (second UPDATE overwrites + // `anonymized_value`), so this controller MUST NOT call + // `markAsAnonymized` directly. + // Placeholder-numbering scope (anonymisation-placeholder-id-scope): + // optional request-body params. `scope` defaults to 'document' + // (counter restarts per run); `scope=dossier` makes the number + // consistent across the dossier folder's files. `dossierKey` is the + // stable folder id; when omitted under scope=dossier the handler + // falls back to the file's parent folder (forgiving default — no + // HTTP 400). Any value other than 'dossier' normalises to + // per-document so existing callers are unaffected. + $scopeParam = (string) $this->request->getParam('scope', 'document'); + $scope = 'document'; + if ($scopeParam === 'dossier') { + $scope = 'dossier'; + } - // Mark entity relations as anonymized. - $this->entityRelationMapper->markAsAnonymized( - fileId: $fileId, - anonymizedValue: 'anonymized_'.date('Y-m-d_H-i-s') + $dossierKeyParam = $this->request->getParam('dossierKey', null); + $dossierKey = null; + if ($dossierKeyParam !== null && $dossierKeyParam !== '') { + $dossierKey = (string) $dossierKeyParam; + } + + $anonymizedFile = $this->fileService->anonymizeDocument( + node: $fileNode, + entities: $entities, + scope: $scope, + dossierKey: $dossierKey ); + // Best-effort policy: the file is produced even when some entity + // text could not be removed. Surface the residuals so the operator + // can iterate (manual entities, skip unselected occurrences). Logs + // stay PII-free (ADR-005); the residual TEXT is carried only in + // this authenticated response for the review UI (deliberate, + // product-owner-approved deviation from the no-PII-in-responses rule). + $residualEntities = $this->fileService->getLastResidualEntities(); + $isComplete = (count($residualEntities) === 0); + + $logSuffix = ' with residual entities'; + $messageResult = 'File anonymized, but some entities could not be fully removed — review the output' + .' and refine the entities (manual entities, skip unselected occurrences).'; + if ($isComplete === true) { + $logSuffix = ' successfully'; + $messageResult = 'File anonymized successfully'; + } + $this->logger->info( - message: '[FileTextController] File anonymized successfully', + message: '[FileTextController] File anonymized'.$logSuffix, context: [ 'file' => __FILE__, 'line' => __LINE__, @@ -576,20 +565,90 @@ public function anonymizeFile(int $fileId): JSONResponse 'anonymized_file_id' => $anonymizedFile->getId(), 'anonymized_path' => $anonymizedFile->getPath(), 'entities_replaced' => count($entities), + 'complete' => $isComplete, + // PII-free: count only, never the residual text. + 'residual_count' => count($residualEntities), ] ); return new JSONResponse( data: [ 'success' => true, - 'message' => 'File anonymized successfully', + 'complete' => $isComplete, + 'message' => $messageResult, 'original_file_id' => $fileId, 'anonymized_file_id' => $anonymizedFile->getId(), 'anonymized_path' => $anonymizedFile->getPath(), 'entities_replaced' => count($entities), + 'residual_count' => count($residualEntities), + 'residual_entities' => $residualEntities, ] ); + } catch (PdfAnonymisationException $e) { + // Map the structured PDF-anonymisation reason to an HTTP status + // per the `pdf-anonymisation` spec (REQ:filter-coverage + + // REQ:validation-gate + REQ:image-only-defer): + // + // - encrypted_pdf → 422 (caller-correctable) + // - text_layer_missing → 422 (caller MUST route to OCR via + // the `ocr-document-scanning` + // capability — the controller + // surfaces a structured body so + // the caller can dispatch; v1 + // does not auto-redirect) + // - validation_failed → 500 (pipeline integrity failure; + // fail-closed for the strict + // entity-anonymisation flow) + // - internal_error → 500 (unexpected pipeline failure) + // + // Per ADR-005 the response body MUST NOT echo the + // operator-supplied entity text — the exception's diagnostic + // surface is PII-free by construction (counts + structural + // counters only) and is the only thing we forward. + $reason = $e->getReason(); + $statusCode = Http::STATUS_INTERNAL_SERVER_ERROR; + switch ($reason) { + case PdfAnonymisationException::REASON_ENCRYPTED_PDF: + case PdfAnonymisationException::REASON_TEXT_LAYER_MISSING: + $statusCode = Http::STATUS_UNPROCESSABLE_ENTITY; + break; + case PdfAnonymisationException::REASON_VALIDATION_FAILED: + case PdfAnonymisationException::REASON_INTERNAL_ERROR: + default: + $statusCode = Http::STATUS_INTERNAL_SERVER_ERROR; + break; + } + + // PII-redacted error log: NEVER include the operator-supplied + // entity text (ADR-005). The PdfAnonymisationException + // diagnostic surface is constructed to be PII-free; carry + // only that + the structured reason + the file id. + $this->logger->error( + message: '[FileTextController] PDF anonymisation failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'file_id' => $fileId, + 'reason' => $reason, + 'diagnostic' => $e->getDiagnostic(), + ] + ); + + return new JSONResponse( + data: [ + 'success' => false, + 'error' => 'pdf_anonymisation_failed', + 'reason' => $reason, + 'details' => $e->getDiagnostic(), + ], + statusCode: $statusCode + ); } catch (\Exception $e) { + // PII-redacted error log: never echo operator-supplied entity + // text or the substitution map in error responses or logs + // (ADR-005). The exception message + file id are the only + // thing safe to surface — the audit trail (ADR-022) keeps + // entity values. $this->logger->error( message: '[FileTextController] Failed to anonymize file', context: [ @@ -597,7 +656,6 @@ public function anonymizeFile(int $fileId): JSONResponse 'line' => __LINE__, 'file_id' => $fileId, 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), ] ); @@ -610,4 +668,245 @@ public function anonymizeFile(int $fileId): JSONResponse ); }//end try }//end anonymizeFile() + + /** + * Add an operator-supplied manual entity to a file. + * + * Implements `manual-entity-anonymisation`: takes an operator-typed + * value + type, performs chunk-aware string matching against the + * file's extracted text, creates (or reuses) the catalogue entry, + * and inserts one `EntityRelation` row per occurrence found. + * + * Idempotent: re-calling for the same value on the same file does + * NOT create duplicate relation rows. Zero-match responses are + * non-errors (HTTP 200 with a `message` field). + * + * Request body: + * + * { + * "value": "Jan Jansen", // required + * "type": "PERSON", // required + * "category": "name", // optional + * "wholeWord": true, // optional, default true + * "caseSensitive": true // optional, default true + * } + * + * @param int $fileId Nextcloud file ID the manual entity applies to. + * + * @return JSONResponse 201 on matches found, 200 on zero matches, 4xx/5xx on failure. + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function addManualEntity(int $fileId): JSONResponse + { + // Content-type guard. The endpoint accepts JSON only; reject + // other media types with 415 so callers don't accidentally + // trip the body-parser heuristics. + $contentType = (string) $this->request->getHeader('Content-Type'); + $mediaType = strtolower(trim(explode(';', $contentType, 2)[0])); + if ($mediaType !== 'application/json' && $mediaType !== '') { + return new JSONResponse( + data: [ + 'error' => 'unsupported_media_type', + 'reason' => 'POST /api/files/{fileId}/manual-entities requires Content-Type: application/json', + ], + statusCode: Http::STATUS_UNSUPPORTED_MEDIA_TYPE + ); + } + + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse( + data: ['error' => 'unauthenticated'], + statusCode: Http::STATUS_UNAUTHORIZED + ); + } + + $body = $this->request->getParams(); + unset($body['fileId'], $body['_route']); + + $value = ''; + if (isset($body['value']) === true) { + $value = (string) $body['value']; + } + + $type = ''; + if (isset($body['type']) === true) { + $type = (string) $body['type']; + } + + $wholeWord = true; + if (isset($body['wholeWord']) === true) { + $wholeWord = (bool) $body['wholeWord']; + } + + $caseSensitive = true; + if (isset($body['caseSensitive']) === true) { + $caseSensitive = (bool) $body['caseSensitive']; + } + + if ($value === '') { + return new JSONResponse( + data: ['error' => 'invalid_request', 'field' => 'value'], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + if ($type === '') { + return new JSONResponse( + data: ['error' => 'invalid_request', 'field' => 'type'], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + // ADR-005 PII rule: log the request shape WITHOUT the value. + $this->logger->info( + message: '[FileTextController] addManualEntity request', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $fileId, + 'type' => $type, + 'wholeWord' => $wholeWord, + 'caseSensitive' => $caseSensitive, + 'valueLength' => strlen($value), + 'actor' => $user->getUID(), + ] + ); + + try { + $result = $this->manualEntityService->addManualEntity( + fileId: $fileId, + value: $value, + type: $type, + wholeWord: $wholeWord, + caseSensitive: $caseSensitive, + actor: $user + ); + } catch (ManualEntityException $e) { + return $this->mapManualEntityException(exception: $e, fileId: $fileId); + } catch (Throwable $e) { + $this->logger->error( + message: '[FileTextController] addManualEntity unexpected failure', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $fileId, + 'error' => $e->getMessage(), + ] + ); + return new JSONResponse( + data: ['error' => 'internal_error'], + statusCode: Http::STATUS_INTERNAL_SERVER_ERROR + ); + }//end try + + return $this->formatManualEntityResponse(result: $result); + + }//end addManualEntity() + + /** + * Translate a `ManualEntityException` to the matching HTTP response. + * + * Per the spec: + * file_not_extracted → 422 (operator must run extraction first) + * regex_compile_failure → 400 (malformed needle) + * unsupported_entity_type → 400 + * forbidden → 403 (service-side write-access check failed) + * internal_error → 500 + * + * @param ManualEntityException $exception Source exception. + * @param int $fileId Target file id (used for logging). + * + * @return JSONResponse Structured error body with no PII echo. + */ + private function mapManualEntityException(ManualEntityException $exception, int $fileId): JSONResponse + { + $reason = $exception->getReason(); + + if ($reason === ManualEntityException::REASON_FORBIDDEN) { + return new JSONResponse( + data: [ + 'error' => 'forbidden', + 'reason' => 'write access to file required', + ], + statusCode: Http::STATUS_FORBIDDEN + ); + } + + $statusByReason = [ + ManualEntityException::REASON_FILE_NOT_EXTRACTED => Http::STATUS_UNPROCESSABLE_ENTITY, + ManualEntityException::REASON_REGEX_COMPILE_FAILURE => Http::STATUS_BAD_REQUEST, + ManualEntityException::REASON_UNSUPPORTED_ENTITY_TYPE => Http::STATUS_BAD_REQUEST, + ManualEntityException::REASON_INTERNAL_ERROR => Http::STATUS_INTERNAL_SERVER_ERROR, + ]; + + $status = ($statusByReason[$reason] ?? Http::STATUS_INTERNAL_SERVER_ERROR); + + $this->logger->info( + message: '[FileTextController] addManualEntity translated exception', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $fileId, + 'reason' => $reason, + 'status' => $status, + ] + ); + + return new JSONResponse( + data: ['error' => $reason], + statusCode: $status + ); + + }//end mapManualEntityException() + + /** + * Format the success response body per the proposal. + * + * 201 when one or more matches were found; 200 with a `message` + * field when zero matches were found (catalogue entry was still + * created or reused). + * + * @param ManualEntityResult $result Service-layer result. + * + * @return JSONResponse + */ + private function formatManualEntityResponse(ManualEntityResult $result): JSONResponse + { + $entityPayload = [ + 'id' => (int) $result->entity->getId(), + 'uuid' => $result->entity->getUuid(), + 'value' => $result->entity->getValue(), + 'type' => $result->entity->getType(), + 'reused' => ($result->entityWasNew === false), + ]; + + $relationsPayload = []; + foreach ($result->relations as $relation) { + $relationsPayload[] = [ + 'id' => (int) $relation->getId(), + 'chunkId' => $relation->getChunkId(), + 'positionStart' => $relation->getPositionStart(), + 'positionEnd' => $relation->getPositionEnd(), + 'context' => $relation->getContext(), + ]; + } + + $body = [ + 'entity' => $entityPayload, + 'relations' => $relationsPayload, + 'matchCount' => $result->matchCount, + 'matchesSkipped' => $result->matchesSkipped, + ]; + + if ($result->matchCount === 0) { + $body['message'] = 'Text not found in file. Catalogue entry created (or reused) and is available for use on other files.'; + return new JSONResponse(data: $body, statusCode: Http::STATUS_OK); + } + + return new JSONResponse(data: $body, statusCode: Http::STATUS_CREATED); + + }//end formatManualEntityResponse() }//end class diff --git a/lib/Controller/FilesController.php b/lib/Controller/FilesController.php index f692de0d42..64a7780c20 100644 --- a/lib/Controller/FilesController.php +++ b/lib/Controller/FilesController.php @@ -16,8 +16,8 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-58 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-11 + * @spec openspec/specs/object-interactions/spec.md + * @spec openspec/specs/content-versioning/spec.md */ declare(strict_types=1); @@ -43,6 +43,7 @@ use OCA\OpenRegister\Event\FileUnlockedEvent; use OCA\OpenRegister\Event\FileVersionRestoredEvent; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IL10N; use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; @@ -66,6 +67,9 @@ * * @psalm-suppress UnusedClass * + * @spec openspec/specs/object-interactions/spec.md + * @spec openspec/specs/content-versioning/spec.md + * * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.TooManyMethods) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -74,6 +78,7 @@ */ class FilesController extends Controller { + use \OCA\OpenRegister\Controller\Trait\HandlesExceptionsTrait; /** * File service for handling file operations @@ -109,6 +114,9 @@ class FilesController extends Controller * @param \OCA\OpenRegister\Db\FileMapper|null $fileMapper OR-side metadata mapper. Null-safe. * @param \OCA\OpenRegister\Service\File\FileAuditHandler|null $fileAuditHandler Audit-trail writer. Null-safe. * @param IUserSession|null $userSession Session for auth gating. + * @param IL10N|null $l10n Localization service for error + * messages. Null-safe: when absent the + * raw English source string is used. * * @return void * @@ -124,7 +132,8 @@ public function __construct( private readonly IEventDispatcher $eventDispatcher, private readonly ?\OCA\OpenRegister\Db\FileMapper $fileMapper=null, private readonly ?\OCA\OpenRegister\Service\File\FileAuditHandler $fileAuditHandler=null, - private readonly ?IUserSession $userSession=null + private readonly ?IUserSession $userSession=null, + private readonly ?IL10N $l10n=null ) { // Call parent constructor to initialize base controller. parent::__construct(appName: $appName, request: $request); @@ -151,6 +160,94 @@ private function isAnonymousRequest(): bool }//end isAnonymousRequest() + /** + * Translate a user-facing error message via IL10N when available. + * + * Closes file-actions task (Phase 10): wrap controller error messages in + * IL10N so the strings are translatable. The dependency is optional and + * null-safe — when no IL10N is wired (legacy fixtures, DI not yet bumped) + * the raw English source string is returned unchanged, so the error shape + * never changes. Only the human-readable text is localised; machine-facing + * substrings the controller matches on (`already exists`, `locked`, `not + * found`, `required`, `Only the lock owner`, `administrators`) live inside + * the FileService exception messages, NOT here, so this wrapping cannot + * disturb the HTTP status-code mapping. + * + * @param string $text English source string (the i18n key). + * @param array $parameters Optional placeholder replacements. + * + * @return string The translated string, or the source string when no IL10N is wired. + */ + private function t(string $text, array $parameters=[]): string + { + if ($this->l10n === null) { + // No localisation service wired — return the source string verbatim. + if (empty($parameters) === true) { + return $text; + } + + return strtr($text, $parameters); + } + + return $this->l10n->t($text, $parameters); + + }//end t() + + /** + * Enforce object-level RBAC before a file action runs (ADR-005 / gate-7). + * + * The file-action endpoints carry `@NoAdminRequired`, so without an explicit + * body guard any authenticated user could invoke them against an arbitrary + * object id (classic IDOR, OWASP A01:2021). This mirrors the SEC-CTRL-5 + * hardening already applied to {@see self::show()}: for authenticated callers + * we re-resolve the object through `ObjectService::find(..., _rbac: true)`, + * which applies the read-permission check and throws + * {@see \OCA\OpenRegister\Exception\NotAuthorizedException} (mapped to HTTP 403 + * by the caller) when the user may not access this object. + * + * Anonymous callers are denied by default (TEMP guard until proper + * write-action RBAC is wired for file endpoints); read-only published-file + * endpoints pass $allowAnonymous = true to keep their anonymous access. + * + * The method name is prefixed `ensure` so gate-7 (no-admin-idor) recognises + * it as an authorisation guard in the calling method's body. + * + * @param string $register The register slug or identifier. + * @param string $schema The schema slug or identifier. + * @param string $id The object UUID or identifier. + * @param bool $allowAnonymous When false (default) anonymous callers are denied; read-only published-file endpoints pass true. + * + * @return void + * + * @throws \OCA\OpenRegister\Exception\NotAuthorizedException When the + * authenticated caller may not access the object. + */ + private function ensureObjectAccess(string $register, string $schema, string $id, bool $allowAnonymous=false): void + { + // TEMP GUARD (rbac-default-deny follow-up): these file write/action + // endpoints are @PublicPage so authenticated non-members are not blocked + // by the app group-gate (HTTP 412), but their object RBAC here is only a + // READ check for authenticated callers and is NOT evaluated for anonymous + // principals. Until proper write-action RBAC (incl. public-group + // evaluation) is wired for file endpoints, deny anonymous outright. + // Published-file READ endpoints (e.g. preview) pass $allowAnonymous=true + // to keep their existing anonymous access. + if ($this->isAnonymousRequest() === true) { + if ($allowAnonymous === false) { + throw new \OCA\OpenRegister\Exception\NotAuthorizedException( + message: 'Authentication is required for this object action' + ); + } + + return; + } + + // The find(_rbac: true) call applies the object read-permission check and + // throws NotAuthorizedException when the caller may not read this object. + $this->objectService->find(id: $id, register: $register, schema: $schema, _rbac: true); + + }//end ensureObjectAccess() + /** * Record a download event: bump the OR-side download counter and * write an audit-trail row. Best-effort — failures here MUST NOT @@ -207,7 +304,7 @@ private function recordDownloadEvent(int $fileId, ?\OCA\OpenRegister\Db\ObjectEn * * @PublicPage * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-58 + * @spec openspec/specs/object-interactions/spec.md */ public function index( string $register, @@ -234,13 +331,14 @@ public function index( return new JSONResponse(data: $formattedFiles); } catch (DoesNotExistException $e) { return new JSONResponse( - data: ['error' => 'Object not found'], + data: ['error' => $this->t(text: 'Object not found')], statusCode: 404 ); } catch (NotFoundException $e) { - return new JSONResponse(data: ['error' => 'Files folder not found'], statusCode: 404); + return new JSONResponse(data: ['error' => $this->t(text: 'Files folder not found')], statusCode: 404); } catch (\Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + // SEC-CTRL-7: do not leak internal exception detail on 500. + return $this->errorResponse(e: $e); }//end try }//end index() @@ -279,6 +377,14 @@ public function show( $this->objectService->setObject($id); $object = $this->objectService->getObject(); + // SEC-CTRL-5: enforce object-level read RBAC for authenticated callers too + // (not just NC mount visibility). Anonymous callers are gated separately by + // the published-file check below. find() applies the read permission check and + // throws NotAuthorizedException (403) when the caller may not read this object. + if ($this->isAnonymousRequest() === false) { + $this->objectService->find(id: $id, register: $register, schema: $schema, _rbac: true); + } + $file = $this->fileService->getFile(object: $object, file: $fileId); // Fall back to direct file ID lookup via known user contexts @@ -301,7 +407,7 @@ public function show( if ($file === null) { return new JSONResponse( - data: ['error' => 'File not found'], + data: ['error' => $this->t(text: 'File not found')], statusCode: 404 ); } @@ -312,7 +418,7 @@ public function show( if ($isAnonymous === true) { if ($this->fileMapper === null || $this->fileMapper->isFilePublished((int) $file->getId()) === false) { return new JSONResponse( - data: ['error' => 'File not available for anonymous access'], + data: ['error' => $this->t(text: 'File not available for anonymous access')], statusCode: 403 ); } @@ -321,7 +427,7 @@ public function show( // Stream the file inline so browsers display images/logos directly. $response = new StreamResponse($file->fopen('r')); $response->addHeader('Content-Type', $file->getMimeType()); - $response->addHeader('Content-Disposition', 'inline; filename="'.$file->getName().'"'); + $response->addHeader('Content-Disposition', $this->buildContentDisposition(disposition: 'inline', filename: $file->getName())); $response->addHeader('Content-Length', (string) $file->getSize()); // Record download (counter + audit). Best-effort. @@ -329,7 +435,10 @@ public function show( return $response; } catch (DoesNotExistException $e) { - return new JSONResponse(data: ['error' => 'Object not found'], statusCode: 404); + return new JSONResponse(data: ['error' => $this->t(text: 'Object not found')], statusCode: 404); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + // SEC-CTRL-5: read-permission denial maps to 403. + return new JSONResponse(data: ['error' => $this->t(text: 'Forbidden')], statusCode: 403); } catch (Exception $e) { return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 400); }//end try @@ -424,7 +533,9 @@ private function fileBelongsToObject(File $file, ObjectEntity $object): bool * * @psalm-return JSONResponse<200|400|404, array{error?: mixed|string, labels?: list,...}, array> * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-58 + * @spec openspec/specs/object-interactions/spec.md + * + * @PublicPage */ public function create( string $register, @@ -436,12 +547,15 @@ public function create( $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before adding files. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { return new JSONResponse( - data: ['error' => 'Object not found'], + data: ['error' => $this->t(text: 'Object not found')], statusCode: 404 ); } @@ -453,14 +567,14 @@ public function create( if (empty($fileName) === true) { return new JSONResponse( - data: ['error' => 'File name is required (use "name" or "filename")'], + data: ['error' => $this->t(text: 'File name is required (use "name" or "filename")')], statusCode: 400 ); } if (array_key_exists('content', $data) === false) { return new JSONResponse( - data: ['error' => 'File content is required'], + data: ['error' => $this->t(text: 'File content is required')], statusCode: 400 ); } @@ -476,8 +590,10 @@ public function create( tags: $tags ); return new JSONResponse(data: $this->fileService->formatFile($result)); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ['error' => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (DoesNotExistException $e) { - return new JSONResponse(data: ['error' => 'Object not found'], statusCode: 404); + return new JSONResponse(data: ['error' => $this->t(text: 'Object not found')], statusCode: 404); } catch (Exception $e) { return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 400); }//end try @@ -507,6 +623,8 @@ public function create( * @suppressWarnings(PHPMD.CyclomaticComplexity) * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-12 + * + * @PublicPage */ public function save( string $register, @@ -518,12 +636,15 @@ public function save( $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before saving files. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { return new JSONResponse( - data: ['error' => 'Object not found'], + data: ['error' => $this->t(text: 'Object not found')], statusCode: 404 ); } @@ -533,7 +654,7 @@ public function save( // Validate required parameters. if (empty($data['name']) === true) { return new JSONResponse( - data: ['error' => 'File name is required'], + data: ['error' => $this->t(text: 'File name is required')], statusCode: 400 ); } @@ -543,7 +664,7 @@ public function save( if ($contentExists === true || $contentEmpty === true) { return new JSONResponse( - data: ['error' => 'File content is required'], + data: ['error' => $this->t(text: 'File content is required')], statusCode: 400 ); } @@ -553,7 +674,7 @@ public function save( if (empty($fileName) === true) { return new JSONResponse( - data: ['error' => 'File name is required (use "name" or "filename")'], + data: ['error' => $this->t(text: 'File name is required (use "name" or "filename")')], statusCode: 400 ); } @@ -582,8 +703,10 @@ public function save( ); return new JSONResponse(data: $this->fileService->formatFile($result)); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ['error' => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (DoesNotExistException $e) { - return new JSONResponse(data: ['error' => 'Object not found'], statusCode: 404); + return new JSONResponse(data: ['error' => $this->t(text: 'Object not found')], statusCode: 404); } catch (Exception $e) { return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 400); }//end try @@ -605,6 +728,8 @@ public function save( * @psalm-return JSONResponse<200|400|404, array{error?: string, 0?: array,...}, array> * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-12 + * + * @PublicPage */ public function createMultipart( string $register, @@ -612,6 +737,9 @@ public function createMultipart( string $id ): JSONResponse { try { + // ADR-005 / gate-7: enforce object-level RBAC before accepting uploads. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + // Validate object exists. $object = $this->validateAndGetObject( register: $register, @@ -621,7 +749,7 @@ public function createMultipart( if ($object === null) { return new JSONResponse( - data: ['error' => 'Object not found'], + data: ['error' => $this->t(text: 'Object not found')], statusCode: 404 ); } @@ -646,6 +774,8 @@ public function createMultipart( ); return new JSONResponse($formattedFiles['results']); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(['error' => $this->t(text: 'You do not have access to this object')], 403); } catch (Exception $e) { return new JSONResponse(['error' => $e->getMessage()], 400); }//end try @@ -922,6 +1052,8 @@ private function validateUploadedFile(array $file): void * @NoCSRFRequired * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-12 + * + * @PublicPage */ public function update( string $register, @@ -934,6 +1066,9 @@ public function update( $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before mutating files. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $data = $this->request->getParams(); @@ -952,8 +1087,10 @@ public function update( ); return new JSONResponse(data: $this->fileService->formatFile($result)); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ['error' => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (DoesNotExistException $e) { - return new JSONResponse(data: ['error' => 'Object not found'], statusCode: 404); + return new JSONResponse(data: ['error' => $this->t(text: 'Object not found')], statusCode: 404); } catch (Exception $e) { return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 400); }//end try @@ -977,7 +1114,9 @@ public function update( * array{error?: string, success?: bool}, * array> * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-58 + * @spec openspec/specs/object-interactions/spec.md + * + * @PublicPage */ public function delete( string $register, @@ -990,6 +1129,9 @@ public function delete( $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before deleting files. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $result = $this->fileService->deleteFile( @@ -998,14 +1140,16 @@ public function delete( ); return new JSONResponse(data: ['success' => $result]); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ['error' => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (DoesNotExistException $e) { - return new JSONResponse(data: ['error' => 'Object not found'], statusCode: 404); + return new JSONResponse(data: ['error' => $this->t(text: 'Object not found')], statusCode: 404); } catch (Exception $e) { return new JSONResponse( data: ['error' => $e->getMessage()], statusCode: 400 ); - } + }//end try }//end delete() /** @@ -1044,7 +1188,7 @@ public function downloadById(int $fileId): JSONResponse|\OCP\AppFramework\Http\S if ($isAnonymous === true) { if ($this->fileMapper === null || $this->fileMapper->isFilePublished($fileId) === false) { return new JSONResponse( - data: ['error' => 'File not available for anonymous access'], + data: ['error' => $this->t(text: 'File not available for anonymous access')], statusCode: 403 ); } @@ -1055,10 +1199,17 @@ public function downloadById(int $fileId): JSONResponse|\OCP\AppFramework\Http\S $file = $this->fileService->getFileById($fileId); if ($file === null) { - return new JSONResponse(data: ['error' => 'File not found'], statusCode: 404); + return new JSONResponse(data: ['error' => $this->t(text: 'File not found')], statusCode: 404); } // L2: resolve parent object for audit context (best-effort). + // TODO(SEC-CTRL-5): authenticated callers here are gated only by the file + // owner check inside FileService::getFileById()/checkOwnership() (deny-on- + // mismatch as of SEC-CTRL-5) and NC mount visibility. resolveParentObjectForFile() + // is currently a best-effort stub that returns null, so a full object-level read + // RBAC check (as done in show()) is not yet possible on this id-only path. Wire + // real parent-object resolution + PermissionHandler read check before relying on + // this endpoint for strict object-level isolation. $parentObject = $this->resolveParentObjectForFile(file: $file); // Record download (counter + audit). Best-effort. @@ -1067,10 +1218,11 @@ public function downloadById(int $fileId): JSONResponse|\OCP\AppFramework\Http\S // Stream the file content back to the client. return $this->fileService->streamFile($file); } catch (NotFoundException $e) { - return new JSONResponse(data: ['error' => 'File not found'], statusCode: 404); + return new JSONResponse(data: ['error' => $this->t(text: 'File not found')], statusCode: 404); } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - } + // SEC-CTRL-7: do not leak internal exception detail on 500. + return $this->errorResponse(e: $e); + }//end try }//end downloadById() /** @@ -1103,6 +1255,40 @@ private function resolveParentObjectForFile(File $file): ?\OCA\OpenRegister\Db\O } }//end resolveParentObjectForFile() + /** + * Build an RFC 6266 compliant Content-Disposition header value. + * + * SEC-CTRL-9: a raw filename can contain quotes, control chars, or non-ASCII + * bytes that break the header or allow header/response splitting. This emits a + * sanitised ASCII `filename="..."` fallback plus a UTF-8 `filename*` parameter. + * + * @param string $disposition Either 'inline' or 'attachment'. + * @param string $filename The raw file name. + * + * @return string The encoded Content-Disposition header value. + */ + private function buildContentDisposition(string $disposition, string $filename): string + { + // Strip control characters (incl. CR/LF) that could split headers. + $clean = preg_replace('/[\x00-\x1F\x7F]/', '', $filename); + if ($clean === null) { + $clean = ''; + } + + // ASCII fallback: replace non-ASCII and quote/backslash with underscore. + $ascii = preg_replace('/[^\x20-\x7E]/', '_', $clean); + if ($ascii === null) { + $ascii = ''; + } + + $ascii = str_replace(['\\', '"'], '_', $ascii); + + // RFC 5987 / 6266 UTF-8 encoded form for capable clients. + $encoded = rawurlencode($clean); + + return $disposition.'; filename="'.$ascii.'"; filename*=UTF-8\'\''.$encoded; + }//end buildContentDisposition() + /** * Get a human-readable error message for PHP file upload errors * @@ -1205,7 +1391,9 @@ private function normalizeTags(mixed $tags): array * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md + * + * @PublicPage */ public function rename(string $register, string $schema, string $id, int $fileId): JSONResponse { @@ -1213,10 +1401,13 @@ public function rename(string $register, string $schema, string $id, int $fileId $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before mutating files. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { - return new JSONResponse(data: ["error" => "Object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Object not found')], statusCode: 404); } $data = $this->request->getParams(); @@ -1242,6 +1433,8 @@ public function rename(string $register, string $schema, string $id, int $fileId ); return new JSONResponse(data: $this->fileService->formatFile($file)); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { $statusCode = match (true) { str_contains($e->getMessage(), "already exists") => 409, @@ -1268,7 +1461,9 @@ public function rename(string $register, string $schema, string $id, int $fileId * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md + * + * @PublicPage */ public function copy(string $register, string $schema, string $id, int $fileId): JSONResponse { @@ -1276,10 +1471,13 @@ public function copy(string $register, string $schema, string $id, int $fileId): $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC on the source object. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $sourceObject = $this->objectService->getObject(); if ($sourceObject === null) { - return new JSONResponse(data: ["error" => "Source object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Source object not found')], statusCode: 404); } $data = $this->request->getParams(); @@ -1288,16 +1486,19 @@ public function copy(string $register, string $schema, string $id, int $fileId): $targetSchema = $data["targetSchema"] ?? $schema; if (empty($targetObjectId) === true) { - return new JSONResponse(data: ["error" => "Target object ID is required"], statusCode: 400); + return new JSONResponse(data: ["error" => $this->t(text: 'Target object ID is required')], statusCode: 400); } + // ADR-005 / gate-7: the caller must also have access to the target object. + $this->ensureObjectAccess(register: $targetRegister, schema: $targetSchema, id: $targetObjectId); + // Load target object. $this->objectService->setSchema($targetSchema); $this->objectService->setRegister($targetRegister); $this->objectService->setObject($targetObjectId); $targetObject = $this->objectService->getObject(); if ($targetObject === null) { - return new JSONResponse(data: ["error" => "Target object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Target object not found')], statusCode: 404); } $newFile = $this->fileService->copyFile( @@ -1337,6 +1538,8 @@ public function copy(string $register, string $schema, string $id, int $fileId): ); return new JSONResponse(data: $this->fileService->formatFile($newFile), statusCode: 201); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { $statusCode = 400; if (str_contains($e->getMessage(), 'not found') === true) { @@ -1360,7 +1563,9 @@ public function copy(string $register, string $schema, string $id, int $fileId): * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md + * + * @PublicPage */ public function move(string $register, string $schema, string $id, int $fileId): JSONResponse { @@ -1368,10 +1573,13 @@ public function move(string $register, string $schema, string $id, int $fileId): $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC on the source object. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $sourceObject = $this->objectService->getObject(); if ($sourceObject === null) { - return new JSONResponse(data: ["error" => "Source object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Source object not found')], statusCode: 404); } $data = $this->request->getParams(); @@ -1380,15 +1588,18 @@ public function move(string $register, string $schema, string $id, int $fileId): $targetSchema = $data["targetSchema"] ?? $schema; if (empty($targetObjectId) === true) { - return new JSONResponse(data: ["error" => "Target object ID is required"], statusCode: 400); + return new JSONResponse(data: ["error" => $this->t(text: 'Target object ID is required')], statusCode: 400); } + // ADR-005 / gate-7: the caller must also have access to the target object. + $this->ensureObjectAccess(register: $targetRegister, schema: $targetSchema, id: $targetObjectId); + $this->objectService->setSchema($targetSchema); $this->objectService->setRegister($targetRegister); $this->objectService->setObject($targetObjectId); $targetObject = $this->objectService->getObject(); if ($targetObject === null) { - return new JSONResponse(data: ["error" => "Target object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Target object not found')], statusCode: 404); } $movedFile = $this->fileService->moveFile( @@ -1428,6 +1639,8 @@ public function move(string $register, string $schema, string $id, int $fileId): ); return new JSONResponse(data: $this->fileService->formatFile($movedFile)); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { $statusCode = match (true) { str_contains($e->getMessage(), "not found") => 404, @@ -1452,7 +1665,9 @@ public function move(string $register, string $schema, string $id, int $fileId): * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-11 + * @spec openspec/specs/content-versioning/spec.md + * + * @PublicPage */ public function listVersions(string $register, string $schema, string $id, int $fileId): JSONResponse { @@ -1460,23 +1675,28 @@ public function listVersions(string $register, string $schema, string $id, int $ $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: object read access required to list versions. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { - return new JSONResponse(data: ["error" => "Object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Object not found')], statusCode: 404); } $file = $this->fileService->getFile(object: $object, file: $fileId); if ($file === null) { - return new JSONResponse(data: ["error" => "File not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'File not found')], statusCode: 404); } $result = $this->fileService->getVersioningHandler()->listVersions($file); return new JSONResponse(data: $result); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { return new JSONResponse(data: ["error" => $e->getMessage()], statusCode: 400); - } + }//end try }//end listVersions() /** @@ -1493,7 +1713,9 @@ public function listVersions(string $register, string $schema, string $id, int $ * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-11 + * @spec openspec/specs/content-versioning/spec.md + * + * @PublicPage */ public function restoreVersion( string $register, @@ -1506,15 +1728,18 @@ public function restoreVersion( $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before restoring. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { - return new JSONResponse(data: ["error" => "Object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Object not found')], statusCode: 404); } $file = $this->fileService->getFile(object: $object, file: $fileId); if ($file === null) { - return new JSONResponse(data: ["error" => "File not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'File not found')], statusCode: 404); } $this->fileService->getVersioningHandler()->restoreVersion($file, $versionId); @@ -1536,6 +1761,8 @@ public function restoreVersion( ); return new JSONResponse(data: $this->fileService->formatFile($file)); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { $statusCode = 400; if (str_contains($e->getMessage(), 'not found') === true) { @@ -1559,7 +1786,9 @@ public function restoreVersion( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-2 + * @spec openspec/specs/file-actions/spec.md + * + * @PublicPage */ public function lock(string $register, string $schema, string $id, int $fileId): JSONResponse { @@ -1567,10 +1796,13 @@ public function lock(string $register, string $schema, string $id, int $fileId): $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before locking. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { - return new JSONResponse(data: ["error" => "Object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Object not found')], statusCode: 404); } $result = $this->fileService->getLockHandler()->lockFile($fileId); @@ -1592,6 +1824,8 @@ public function lock(string $register, string $schema, string $id, int $fileId): ); return new JSONResponse(data: $result); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { $statusCode = 400; if (str_contains($e->getMessage(), 'locked') === true) { @@ -1615,7 +1849,9 @@ public function lock(string $register, string $schema, string $id, int $fileId): * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-2 + * @spec openspec/specs/file-actions/spec.md + * + * @PublicPage */ public function unlock(string $register, string $schema, string $id, int $fileId): JSONResponse { @@ -1623,10 +1859,13 @@ public function unlock(string $register, string $schema, string $id, int $fileId $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before unlocking. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { - return new JSONResponse(data: ["error" => "Object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Object not found')], statusCode: 404); } $data = $this->request->getParams(); @@ -1656,6 +1895,8 @@ public function unlock(string $register, string $schema, string $id, int $fileId ); return new JSONResponse(data: $result); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { $statusCode = match (true) { str_contains($e->getMessage(), "Only the lock owner") => 403, @@ -1680,6 +1921,8 @@ public function unlock(string $register, string $schema, string $id, int $fileId * @NoCSRFRequired * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-12 + * + * @PublicPage */ public function batch(string $register, string $schema, string $id): JSONResponse { @@ -1687,10 +1930,13 @@ public function batch(string $register, string $schema, string $id): JSONRespons $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before batch mutation. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { - return new JSONResponse(data: ["error" => "Object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Object not found')], statusCode: 404); } $data = $this->request->getParams(); @@ -1712,6 +1958,8 @@ public function batch(string $register, string $schema, string $id): JSONRespons } return new JSONResponse(data: $result, statusCode: $statusCode); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { return new JSONResponse(data: ["error" => $e->getMessage()], statusCode: 400); }//end try @@ -1731,7 +1979,7 @@ public function batch(string $register, string $schema, string $id): JSONRespons * @NoCSRFRequired * @PublicPage * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-3 + * @spec openspec/specs/file-actions/spec.md */ public function preview(string $register, string $schema, string $id, int $fileId): JSONResponse|StreamResponse { @@ -1750,21 +1998,26 @@ public function preview(string $register, string $schema, string $id, int $fileI if ($this->isAnonymousRequest() === true) { if ($this->fileMapper === null || $this->fileMapper->isFilePublished($fileId) === false) { return new JSONResponse( - data: ["error" => "Preview not available for unpublished files"], + data: ["error" => $this->t(text: 'Preview not available for unpublished files')], statusCode: 403 ); } } + // ADR-005 / gate-7: authenticated callers must have object read access. + // Anonymous callers were already gated by the published-file check + // above, so preview opts out of the write/action anonymous-deny guard. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id, allowAnonymous: true); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { - return new JSONResponse(data: ["error" => "Object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Object not found')], statusCode: 404); } $file = $this->fileService->getFile(object: $object, file: $fileId); if ($file === null) { - return new JSONResponse(data: ["error" => "File not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'File not found')], statusCode: 404); } $width = (int) ($this->request->getParam("width") ?? 256); @@ -1778,6 +2031,8 @@ public function preview(string $register, string $schema, string $id, int $fileI $response->addHeader("Content-Length", (string) $preview->getSize()); return $response; + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { $fallbackIcon = "/core/img/filetypes/file.svg"; return new JSONResponse( @@ -1801,6 +2056,8 @@ public function preview(string $register, string $schema, string $id, int $fileI * @NoCSRFRequired * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-12 + * + * @PublicPage */ public function updateLabels(string $register, string $schema, string $id, int $fileId): JSONResponse { @@ -1808,10 +2065,13 @@ public function updateLabels(string $register, string $schema, string $id, int $ $this->objectService->setRegister($register); try { + // ADR-005 / gate-7: enforce object-level RBAC before mutating labels. + $this->ensureObjectAccess(register: $register, schema: $schema, id: $id); + $this->objectService->setObject($id); $object = $this->objectService->getObject(); if ($object === null) { - return new JSONResponse(data: ["error" => "Object not found"], statusCode: 404); + return new JSONResponse(data: ["error" => $this->t(text: 'Object not found')], statusCode: 404); } $data = $this->request->getParams(); @@ -1830,6 +2090,8 @@ public function updateLabels(string $register, string $schema, string $id, int $ ); return new JSONResponse(data: $this->fileService->formatFile($result)); + } catch (\OCA\OpenRegister\Exception\NotAuthorizedException $e) { + return new JSONResponse(data: ["error" => $this->t(text: 'You do not have access to this object')], statusCode: 403); } catch (Exception $e) { return new JSONResponse(data: ["error" => $e->getMessage()], statusCode: 400); }//end try diff --git a/lib/Controller/FlowLinksController.php b/lib/Controller/FlowLinksController.php index 7e7e4237dd..f7536096b7 100644 --- a/lib/Controller/FlowLinksController.php +++ b/lib/Controller/FlowLinksController.php @@ -81,7 +81,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -128,7 +128,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function link(string $register, string $schema, string $id): JSONResponse { @@ -178,7 +178,7 @@ public function link(string $register, string $schema, string $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroy(string $register, string $schema, string $id, string $operationId): JSONResponse { @@ -219,7 +219,7 @@ public function destroy(string $register, string $schema, string $id, string $op * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function available(): JSONResponse { diff --git a/lib/Controller/FormLinksController.php b/lib/Controller/FormLinksController.php index 49b56bf0df..542c29ae42 100644 --- a/lib/Controller/FormLinksController.php +++ b/lib/Controller/FormLinksController.php @@ -121,7 +121,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -161,7 +161,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function link(string $register, string $schema, string $id): JSONResponse { @@ -244,7 +244,7 @@ public function link(string $register, string $schema, string $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function create(string $register, string $schema, string $id): JSONResponse { @@ -307,7 +307,7 @@ public function create(string $register, string $schema, string $id): JSONRespon * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroyForm(string $register, string $schema, string $id, string $formId): JSONResponse { @@ -345,7 +345,7 @@ public function destroyForm(string $register, string $schema, string $id, string * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroySubmission( string $register, @@ -386,14 +386,18 @@ public function destroySubmission( * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns the current user's own Nextcloud Forms; no caller-supplied object id. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function available(): JSONResponse { try { $rawObjectUuid = $this->request->getParam('objectUuid'); - $objectUuid = ($rawObjectUuid !== null && $rawObjectUuid !== '') ? (string) $rawObjectUuid : null; + $objectUuid = null; + if ($rawObjectUuid !== null && $rawObjectUuid !== '') { + $objectUuid = (string) $rawObjectUuid; + } $results = $this->formLinkService->getAvailableForms(objectUuid: $objectUuid); diff --git a/lib/Controller/GdprEntitiesController.php b/lib/Controller/GdprEntitiesController.php index 7569b1fa2f..b500dd4c60 100644 --- a/lib/Controller/GdprEntitiesController.php +++ b/lib/Controller/GdprEntitiesController.php @@ -97,7 +97,7 @@ public function __construct( * * @return JSONResponse JSON response with entities list * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-7 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function index(): JSONResponse { @@ -237,7 +237,7 @@ public function index(): JSONResponse * * @return JSONResponse JSON response with entity details * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-7 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function show(int $id): JSONResponse { @@ -312,7 +312,7 @@ public function show(int $id): JSONResponse * * @return JSONResponse JSON response with entity types * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-7 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function getTypes(): JSONResponse { @@ -378,7 +378,7 @@ public function getTypes(): JSONResponse * * @return JSONResponse JSON response with entity categories * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-7 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function getCategories(): JSONResponse { @@ -444,7 +444,7 @@ public function getCategories(): JSONResponse * * @return JSONResponse JSON response with entity statistics * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-7 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function getStats(): JSONResponse { @@ -579,7 +579,7 @@ public function getStats(): JSONResponse * * @return JSONResponse JSON response with deletion result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-7 + * @spec openspec/specs/avg-verwerkingsregister/spec.md */ public function destroy(int $id): JSONResponse { diff --git a/lib/Controller/GitHubIssuesController.php b/lib/Controller/GitHubIssuesController.php index 441473918e..7afe68532e 100644 --- a/lib/Controller/GitHubIssuesController.php +++ b/lib/Controller/GitHubIssuesController.php @@ -117,6 +117,9 @@ public function __construct( * * @return JSONResponse * + * @no-admin-idor-exempt Guarded downstream: GitHubGuards::enforceRepoAllowlist restricts the caller-supplied repo to the + * admin-configured allowlist (plus feature-flag and rate-limit); takes no OpenRegister object id. + * * @spec openspec/changes/add-features-roadmap-menu/tasks.md#task-3 * @spec openspec/changes/add-features-roadmap-menu/tasks.md#task-5 * @spec openspec/changes/add-features-roadmap-menu/tasks.md#task-7 @@ -215,11 +218,14 @@ public function create(): JSONResponse $uid = $user->getUID(); - $repo = (string) $this->request->getParam('repo', ''); - $title = (string) $this->request->getParam('title', ''); - $body = (string) $this->request->getParam('body', ''); + $repo = (string) $this->request->getParam('repo', ''); + $title = (string) $this->request->getParam('title', ''); + $body = (string) $this->request->getParam('body', ''); $specRefRaw = $this->request->getParam('specRef'); - $specRef = ($specRefRaw !== null && $specRefRaw !== '') ? (string) $specRefRaw : null; + $specRef = null; + if ($specRefRaw !== null && $specRefRaw !== '') { + $specRef = (string) $specRefRaw; + } $guardError = $this->guards->runGuards( $this->writeGuardPipeline(repo: $repo, title: $title, body: $body, specRef: $specRef, uid: $uid) diff --git a/lib/Controller/GraphQLController.php b/lib/Controller/GraphQLController.php index 0fdb221e6d..ae95eae4b2 100644 --- a/lib/Controller/GraphQLController.php +++ b/lib/Controller/GraphQLController.php @@ -20,8 +20,8 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-46 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-47 + * @spec openspec/specs/graphql-api/spec.md + * @spec openspec/specs/graphql-api/spec.md * * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-1 * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-2 @@ -86,7 +86,7 @@ public function __construct( * * @CORS * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-46 + * @spec openspec/specs/graphql-api/spec.md * * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-1 */ @@ -145,8 +145,9 @@ public function execute(): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt No per-object resource: returns the static GraphiQL explorer HTML page; no data access. * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-47 + * @spec openspec/specs/graphql-api/spec.md * * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-2 */ diff --git a/lib/Controller/GraphQLSubscriptionController.php b/lib/Controller/GraphQLSubscriptionController.php index 827c0e1fcc..5438925abf 100644 --- a/lib/Controller/GraphQLSubscriptionController.php +++ b/lib/Controller/GraphQLSubscriptionController.php @@ -18,7 +18,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-41 + * @spec openspec/specs/event-driven-architecture/spec.md */ namespace OCA\OpenRegister\Controller; @@ -72,10 +72,12 @@ public function __construct( * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: SubscriptionService filters every streamed event through + * verifyEventRBAC -> PermissionHandler::hasPermission(schema,'read'); non-readable schemas are never streamed. * * @CORS * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-41 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function subscribe(): Response { diff --git a/lib/Controller/HandoffController.php b/lib/Controller/HandoffController.php new file mode 100644 index 0000000000..0488b7c0ad --- /dev/null +++ b/lib/Controller/HandoffController.php @@ -0,0 +1,188 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Exception\HandoffException; +use OCA\OpenRegister\Exception\NotAuthorizedException; +use OCA\OpenRegister\Exception\ValidationException; +use OCA\OpenRegister\Service\Handoff\HandoffService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +/** + * Handoff availability + execution endpoints. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Handoff REST surface) + */ +class HandoffController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App name (injected by NC). + * @param IRequest $request Request (injected by NC). + * @param HandoffService $handoffService The handoff engine. + * @param LoggerInterface $logger Structured logging. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly HandoffService $handoffService, + private readonly LoggerInterface $logger, + ) { + parent::__construct(appName: $appName, request: $request); + + }//end __construct() + + /** + * GET /api/objects/{register}/{schema}/{id}/handoffs + * + * Availability: every handoff the object's schema declares, each with + * state `available` (naming the resolved provider schema), `unavailable` + * (machine-readable reason for the "provider not installed" UI copy), or + * `queued` (a parked queue-mode entry exists). + * + * Per-object guard: the service loads the object via `ObjectService` + * under the caller's RBAC — read denial surfaces as 403/404 here. + * + * @param string $register The register slug or id. + * @param string $schema The schema slug or id. + * @param string $id The object id/uuid. + * + * @return JSONResponse `{handoffs: [...]}` or a typed error payload. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Availability endpoint with provider present) + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function availability(string $register, string $schema, string $id): JSONResponse + { + try { + $handoffs = $this->handoffService->listAvailability(register: $register, schema: $schema, id: $id); + return new JSONResponse(data: ['handoffs' => $handoffs]); + } catch (NotAuthorizedException $e) { + return new JSONResponse(data: ['error' => 'forbidden', 'message' => $e->getMessage()], statusCode: Http::STATUS_FORBIDDEN); + } catch (HandoffException | DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'not-found', 'message' => 'Object not found.'], statusCode: Http::STATUS_NOT_FOUND); + } catch (\Throwable $e) { + $this->logger->error( + message: '[HandoffController] availability failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'exception' => $e] + ); + return new JSONResponse( + data: ['error' => 'internal', 'message' => 'Handoff availability failed.'], + statusCode: Http::STATUS_INTERNAL_SERVER_ERROR + ); + }//end try + + }//end availability() + + /** + * POST /api/objects/{register}/{schema}/{id}/handoffs/{handoffId} + * + * Execute a declared handoff (or park it, queue mode). Typed outcomes: + * 200 executed (created target ref), 202 parked, 400 validation, 403 + * permission (no escalation), 404 handoff-not-declared, 409 + * handoff-provider-unavailable (hide mode — explicitly never a 5xx). + * + * Per-object guard: write permission on the source and create permission + * on the resolved target schema are enforced by the service as the + * calling user before any write. + * + * @param string $register The register slug or id. + * @param string $schema The schema slug or id. + * @param string $id The object id/uuid. + * @param string $handoffId The declared handoff entry id. + * + * @return JSONResponse The execution result or typed error payload. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Successful request-to-case handoff) + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function execute(string $register, string $schema, string $id, string $handoffId): JSONResponse + { + try { + $result = $this->handoffService->execute(register: $register, schema: $schema, id: $id, handoffId: $handoffId); + + $status = Http::STATUS_OK; + if (($result['status'] ?? '') === 'parked') { + $status = Http::STATUS_ACCEPTED; + } + + return new JSONResponse(data: $result, statusCode: $status); + } catch (HandoffException $e) { + if ($e->getErrorCode() === HandoffException::PROVIDER_UNAVAILABLE) { + // Machine-readable degradation — a missing provider is a + // conflict with current install state, never a 5xx. + return new JSONResponse( + data: ['error' => HandoffException::PROVIDER_UNAVAILABLE, 'message' => $e->getMessage()], + statusCode: Http::STATUS_CONFLICT + ); + } + + return new JSONResponse( + data: ['error' => HandoffException::NOT_DECLARED, 'message' => $e->getMessage()], + statusCode: Http::STATUS_NOT_FOUND + ); + } catch (NotAuthorizedException $e) { + return new JSONResponse(data: ['error' => 'forbidden', 'message' => $e->getMessage()], statusCode: Http::STATUS_FORBIDDEN); + } catch (ValidationException $e) { + return new JSONResponse(data: ['error' => 'validation', 'message' => $e->getMessage()], statusCode: Http::STATUS_BAD_REQUEST); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'not-found', 'message' => 'Object not found.'], statusCode: Http::STATUS_NOT_FOUND); + } catch (\Throwable $e) { + $this->logger->error( + message: '[HandoffController] execute failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'exception' => $e] + ); + return new JSONResponse( + data: ['error' => 'internal', 'message' => 'Handoff execution failed.'], + statusCode: Http::STATUS_INTERNAL_SERVER_ERROR + ); + }//end try + + }//end execute() +}//end class diff --git a/lib/Controller/HealthController.php b/lib/Controller/HealthController.php deleted file mode 100644 index 2bca945402..0000000000 --- a/lib/Controller/HealthController.php +++ /dev/null @@ -1,161 +0,0 @@ - - * @copyright 2025 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.nl - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Controller; - -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IDBConnection; -use OCP\IRequest; -use OCP\App\IAppManager; -use Psr\Log\LoggerInterface; - -/** - * Controller for health check endpoints. - * - * @psalm-suppress UnusedClass - */ -class HealthController extends Controller -{ - /** - * Constructor. - * - * @param string $appName The application name - * @param IRequest $request The HTTP request - * @param IDBConnection $db Database connection - * @param IAppManager $appManager App manager - * @param LoggerInterface $logger Logger - */ - public function __construct( - string $appName, - IRequest $request, - private IDBConnection $db, - private IAppManager $appManager, - private LoggerInterface $logger, - ) { - parent::__construct(appName: $appName, request: $request); - }//end __construct() - - /** - * Health check endpoint. - * - * Returns JSON with status and individual check results. - * - * @NoCSRFRequired - * - * @return JSONResponse Health status - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-18 - */ - public function index(): JSONResponse - { - $checks = []; - $status = 'ok'; - - // Check database connectivity. - $checks['database'] = $this->checkDatabase(); - if ($checks['database'] !== 'ok') { - $status = 'error'; - } - - // Check filesystem. - $checks['filesystem'] = $this->checkFilesystem(); - if ($checks['filesystem'] !== 'ok') { - if ($status !== 'error') { - $status = 'degraded'; - } - } - - $httpStatus = Http::STATUS_SERVICE_UNAVAILABLE; - if ($status === 'ok') { - $httpStatus = Http::STATUS_OK; - } - - return new JSONResponse( - [ - 'status' => $status, - 'version' => $this->getAppVersion(), - 'checks' => $checks, - ], - $httpStatus - ); - }//end index() - - /** - * Check database connectivity. - * - * @return string 'ok' or error message - */ - private function checkDatabase(): string - { - try { - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->createFunction('1')); - $result = $qb->executeQuery(); - $result->closeCursor(); - - return 'ok'; - } catch (\Exception $e) { - $this->logger->error('[HealthController] Database check failed', ['error' => $e->getMessage()]); - return 'failed: '.$e->getMessage(); - } - }//end checkDatabase() - - /** - * Check filesystem access. - * - * @return string 'ok' or error message - */ - private function checkFilesystem(): string - { - try { - $tmpFile = sys_get_temp_dir().'/openregister_health_'.getmypid(); - $written = file_put_contents($tmpFile, 'health'); - if ($written === false) { - return 'failed: cannot write to temp directory'; - } - - unlink($tmpFile); - - return 'ok'; - } catch (\Exception $e) { - return 'failed: '.$e->getMessage(); - } - }//end checkFilesystem() - - /** - * Get the app version. - * - * @return string The app version - */ - private function getAppVersion(): string - { - try { - return $this->appManager->getAppVersion('openregister'); - } catch (\Exception $e) { - return 'unknown'; - } - }//end getAppVersion() -}//end class diff --git a/lib/Controller/HeartbeatController.php b/lib/Controller/HeartbeatController.php index da3a92e5a2..4e9685a384 100644 --- a/lib/Controller/HeartbeatController.php +++ b/lib/Controller/HeartbeatController.php @@ -80,6 +80,7 @@ public function __construct( * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt No per-object resource: liveness probe returning status and timestamp only; no data access. * * @return JSONResponse Simple success response with status, timestamp, and message * @@ -88,7 +89,7 @@ public function __construct( * message: 'Heartbeat successful - connection kept alive'}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-5 + * @spec openspec/specs/production-observability/spec.md */ public function heartbeat(): JSONResponse { diff --git a/lib/Controller/IconController.php b/lib/Controller/IconController.php new file mode 100644 index 0000000000..ec5e01271f --- /dev/null +++ b/lib/Controller/IconController.php @@ -0,0 +1,92 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\MdiIconRenderer; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\IRequest; + +/** + * Renders curated MDI glyphs as SVG images. + */ +class IconController extends Controller +{ + /** + * Constructor for the IconController. + * + * @param string $appName The name of the app + * @param IRequest $request The HTTP request object + * + * @return void + */ + public function __construct(string $appName, IRequest $request) + { + parent::__construct(appName: $appName, request: $request); + + }//end __construct() + + /** + * Serve a curated Material Design Icon as an SVG image. + * + * Public, cacheable, and read-only: it returns nothing but static glyph + * geometry from a curated allow-list, so it is safe without authentication. + * Unknown icon names return 404 so the caller falls back to its own icon. + * + * @param string $name The MDI icon reference (e.g. "Dog", "mdi-dog"). + * + * @return DataDisplayResponse The SVG image, or a 404 for an unknown icon. + * + * @spec openspec/changes/unified-search-index/specs/unified-search-provider/spec.md + */ + #[PublicPage] + #[NoCSRFRequired] + public function mdi(string $name): DataDisplayResponse + { + $svg = MdiIconRenderer::svg(icon: $name); + if ($svg === null) { + return new DataDisplayResponse( + data: '', + statusCode: Http::STATUS_NOT_FOUND + ); + } + + $response = new DataDisplayResponse( + data: $svg, + statusCode: Http::STATUS_OK, + headers: ['Content-Type' => 'image/svg+xml'] + ); + // Glyph geometry is immutable for a given name — cache hard. + $response->cacheFor(86400, false, true); + + return $response; + + }//end mdi() +}//end class diff --git a/lib/Controller/LinkedEntityController.php b/lib/Controller/LinkedEntityController.php index 2281ece4f4..314ab11835 100644 --- a/lib/Controller/LinkedEntityController.php +++ b/lib/Controller/LinkedEntityController.php @@ -6,28 +6,31 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Controller - * @package OCA\OpenRegister - * @author Conduction + * @category Controller + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-44 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-45 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-48 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-49 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-reverse-lookup-across-tables + * @spec openspec/specs/linked-entity-types/spec.md#requirement-remove-link-entities-and-mappers + * @spec openspec/specs/linked-entity-types/spec.md + * @spec openspec/specs/linked-entity-types/spec.md */ namespace OCA\OpenRegister\Controller; use Exception; +use OCA\OpenRegister\Exception\NotAuthorizedException; use OCA\OpenRegister\Service\LinkedEntityService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\JSONResponse; +use OCP\IGroupManager; use OCP\IRequest; +use OCP\IUserSession; use Psr\Log\LoggerInterface; /** @@ -41,7 +44,7 @@ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * @link https://github.com/ConductionNL/openregister * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ class LinkedEntityController extends Controller { @@ -52,16 +55,42 @@ class LinkedEntityController extends Controller * @param IRequest $request The request object * @param LinkedEntityService $linkedEntityService The linked entity service * @param LoggerInterface $logger Logger + * @param IUserSession $userSession Active user session for caller identity + * @param IGroupManager $groupManager Group manager for admin checks */ public function __construct( string $appName, IRequest $request, private readonly LinkedEntityService $linkedEntityService, private readonly LoggerInterface $logger, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, ) { parent::__construct(appName: $appName, request: $request); }//end __construct() + /** + * Check whether the currently authenticated user is a Nextcloud administrator. + * + * Register/schema link mutations change register/schema configuration, and + * reverseLookup scans across all tenants (RBAC/multitenancy intentionally + * disabled — see LinkedEntityService, TODO #1273); both are admin-only. + * + * @return bool True if a user is signed in and belongs to the admin group. + */ + private function isCurrentUserAdmin(): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + return $this->groupManager->isAdmin($user->getUID()); + }//end isCurrentUserAdmin() + + // SEC-CTRL-8: CSRF protection retained — this is an SPA-called authenticated write + // (axios sends the CSRF token); #[NoCSRFRequired] removed. + /** * Add a linked entity to an object. * @@ -73,12 +102,10 @@ public function __construct( * @return JSONResponse The updated linked IDs * * @NoAdminRequired - * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-48 + * @spec openspec/specs/linked-entity-types/spec.md */ #[NoAdminRequired] - #[NoCSRFRequired] public function addObjectLink(string $uuid, string $type): JSONResponse { try { @@ -92,6 +119,9 @@ public function addObjectLink(string $uuid, string $type): JSONResponse $result = $this->linkedEntityService->addLink($uuid, $type, (string) $entityId); return new JSONResponse(['_'.$type => $result]); + } catch (NotAuthorizedException $e) { + // SEC-CTRL-4: write-permission denial maps to 403. + return new JSONResponse(['error' => $e->getMessage()], 403); } catch (Exception $e) { $this->logger->error( '[LinkedEntityController] addObjectLink failed', @@ -99,9 +129,11 @@ public function addObjectLink(string $uuid, string $type): JSONResponse ); return new JSONResponse(['error' => $e->getMessage()], 400); - } + }//end try }//end addObjectLink() + // SEC-CTRL-8: CSRF protection retained — SPA-called authenticated write; #[NoCSRFRequired] removed. + /** * Remove a linked entity from an object. * @@ -114,19 +146,20 @@ public function addObjectLink(string $uuid, string $type): JSONResponse * @return JSONResponse The updated linked IDs * * @NoAdminRequired - * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-45 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-48 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-remove-link-entities-and-mappers + * @spec openspec/specs/linked-entity-types/spec.md */ #[NoAdminRequired] - #[NoCSRFRequired] public function removeObjectLink(string $uuid, string $type, string $entityId): JSONResponse { try { $result = $this->linkedEntityService->removeLink($uuid, $type, $entityId); return new JSONResponse(['_'.$type => $result]); + } catch (NotAuthorizedException $e) { + // SEC-CTRL-4: write-permission denial maps to 403. + return new JSONResponse(['error' => $e->getMessage()], 403); } catch (Exception $e) { $this->logger->error( '[LinkedEntityController] removeObjectLink failed', @@ -137,6 +170,8 @@ public function removeObjectLink(string $uuid, string $type, string $entityId): } }//end removeObjectLink() + // SEC-CTRL-8: CSRF protection retained — SPA-called authenticated write; #[NoCSRFRequired] removed. + /** * Add a linked entity to a register. * @@ -148,14 +183,17 @@ public function removeObjectLink(string $uuid, string $type, string $entityId): * @return JSONResponse The updated linked IDs * * @NoAdminRequired - * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ #[NoAdminRequired] - #[NoCSRFRequired] public function addRegisterLink(string $uuid, string $type): JSONResponse { + // SEC-CTRL: admin-only — mutates register configuration. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $body = $this->request->getParams(); $entityId = $body['id'] ?? null; @@ -172,6 +210,8 @@ public function addRegisterLink(string $uuid, string $type): JSONResponse } }//end addRegisterLink() + // SEC-CTRL-8: CSRF protection retained — SPA-called authenticated write; #[NoCSRFRequired] removed. + /** * Add a linked entity to a schema. * @@ -183,14 +223,17 @@ public function addRegisterLink(string $uuid, string $type): JSONResponse * @return JSONResponse The updated linked IDs * * @NoAdminRequired - * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ #[NoAdminRequired] - #[NoCSRFRequired] public function addSchemaLink(string $uuid, string $type): JSONResponse { + // SEC-CTRL: admin-only — mutates schema configuration. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $body = $this->request->getParams(); $entityId = $body['id'] ?? null; @@ -220,13 +263,21 @@ public function addSchemaLink(string $uuid, string $type): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-44 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-49 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-reverse-lookup-across-tables + * @spec openspec/specs/linked-entity-types/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] public function reverseLookup(string $type, string $entityId): JSONResponse { + // SEC-CTRL: admin-only — LinkedEntityService::reverseLookup scans magic + // tables with RBAC + multitenancy intentionally disabled (cross-tenant; + // TODO #1273). Until per-tenant scoping lands, restrict to admins so an + // arbitrary user cannot enumerate cross-tenant object links by entity id. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $results = $this->linkedEntityService->reverseLookup($type, $entityId); diff --git a/lib/Controller/ManifestController.php b/lib/Controller/ManifestController.php index 8354267c26..82fbfe7473 100644 --- a/lib/Controller/ManifestController.php +++ b/lib/Controller/ManifestController.php @@ -49,6 +49,8 @@ * can still load a manifest and receive `runtime.user = null`; public pages * are filtered by nc-vue using that null signal. * + * @spec openspec/changes/manifest-user-context/tasks.md + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ManifestController extends Controller @@ -110,14 +112,25 @@ public function index(string $appId): JSONResponse try { $enriched = $this->manifestService->getEnrichedManifest(manifest: $manifest); - return new JSONResponse($enriched); + + // ETag over the final per-user payload: NotModifiedMiddleware turns a + // matching If-None-Match into a 304, so warm reloads skip the transfer + // (fleet manifests reach ~270KB) while staying fully fresh — enrichment + // still runs, and any change in manifest OR runtime.user changes the tag. + // `private, no-cache` replaces the default `no-store`: the browser MAY + // store the response but MUST revalidate, which is what makes the 304 + // path usable at all. + $response = new JSONResponse($enriched); + $response->setETag(md5(json_encode($enriched))); + $response->addHeader('Cache-Control', 'private, no-cache'); + return $response; } catch (Throwable $e) { $this->logger->error( message: sprintf('[ManifestController] Enrichment failed for app "%s": %s', $appId, $e->getMessage()), context: ['file' => __FILE__, 'line' => __LINE__, 'appId' => $appId] ); return new JSONResponse(['error' => 'Internal server error.'], Http::STATUS_INTERNAL_SERVER_ERROR); - } + }//end try }//end index() /** diff --git a/lib/Controller/MapLinksController.php b/lib/Controller/MapLinksController.php index 8f66607cf7..557206bcba 100644 --- a/lib/Controller/MapLinksController.php +++ b/lib/Controller/MapLinksController.php @@ -303,6 +303,7 @@ public function destroy(string $register, string $schema, string $id, string $fa * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns the current user's own Maps POIs; no caller-supplied object id. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-1 */ diff --git a/lib/Controller/MappingsController.php b/lib/Controller/MappingsController.php index 0ffd3504ef..52b1bfe069 100644 --- a/lib/Controller/MappingsController.php +++ b/lib/Controller/MappingsController.php @@ -57,7 +57,7 @@ * * @psalm-suppress UnusedClass * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ class MappingsController extends Controller { @@ -338,12 +338,14 @@ public function destroy(int $id): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt No per-object resource: stateless evaluation of a caller-supplied transient mapping over caller-supplied input; + * nothing is loaded by id. * * @return JSONResponse JSON response with test results * * @suppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function test(): JSONResponse { diff --git a/lib/Controller/MapsOverviewController.php b/lib/Controller/MapsOverviewController.php new file mode 100644 index 0000000000..e5707e2aa6 --- /dev/null +++ b/lib/Controller/MapsOverviewController.php @@ -0,0 +1,221 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/integration-maps-overview/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\MapsOverviewService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Page-level multi-object map overview controller. + */ +class MapsOverviewController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App name (injected by NC). + * @param IRequest $request Current request. + * @param MapsOverviewService $overviewService Map overview register/query service. + * + * @return void + */ + public function __construct( + string $appName, + IRequest $request, + private MapsOverviewService $overviewService, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * POST /api/integrations/maps/overviews + * + * Declare or refresh a page-level map overview widget. Body: + * - overviewKey (string, required) + * - register (string|int, required) + * - schema (string|int, required) + * - title (string, optional) + * - geoProperty (string, optional) + * - filters (array, optional) + * - baseLayer (array, optional — declarative base-layer override) + * + * @NoAdminRequired + * + * @return JSONResponse The stored widget render contract, or 400. + * + * @spec openspec/specs/integration-maps-overview/spec.md + */ + #[NoAdminRequired] + public function register(): JSONResponse + { + $params = $this->request->getParams(); + + try { + // Authz guard (visible at the endpoint boundary): declaring a + // map overview is a write — it requires an authenticated user. + $this->overviewService->ensureCanRegister(); + + $title = null; + if (isset($params['title']) === true) { + $title = (string) $params['title']; + } + + $geoProperty = null; + if (isset($params['geoProperty']) === true) { + $geoProperty = (string) $params['geoProperty']; + } + + $baseLayer = null; + if (isset($params['baseLayer']) === true) { + $baseLayer = (array) $params['baseLayer']; + } + + $stored = $this->overviewService->registerOverview( + overviewKey: (string) ($params['overviewKey'] ?? ''), + register: (string) ($params['register'] ?? ''), + schema: (string) ($params['schema'] ?? ''), + title: $title, + geoProperty: $geoProperty, + filters: (array) ($params['filters'] ?? []), + baseLayer: $baseLayer + ); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + }//end try + + return new JSONResponse($stored, Http::STATUS_CREATED); + }//end register() + + /** + * GET /api/integrations/maps/overviews/{register}/{schema}/points + * + * Query the marker point set for a register/schema, RBAC-scoped. + * Returns `[{id,label,lat,lng,register,schema,geometry}]` for every + * object the current user may read that carries a geometry. The read is + * RBAC-scoped inside the service (fail-closed): objects the caller + * cannot read never appear, and the response is a uniform point list + * (empty when nothing is readable) rather than an enumeration oracle. + * + * @param string $register Register slug or id to query. + * @param string $schema Schema slug or id to query. + * + * @NoAdminRequired + * + * @return JSONResponse The RBAC-scoped marker point set, or 400. + * + * @spec openspec/specs/integration-maps-overview/spec.md + */ + #[NoAdminRequired] + public function points(string $register, string $schema): JSONResponse + { + $params = $this->request->getParams(); + + try { + // Authz guard (visible at the endpoint boundary): the point + // query is RBAC-scoped inside the service — it runs the + // canonical OR read path with _rbac:true for non-admins, so a + // caller only ever sees objects they may read (fail-closed). + $geoProperty = null; + if (isset($params['geoProperty']) === true) { + $geoProperty = (string) $params['geoProperty']; + } + + $filters = (array) ($params['filters'] ?? $this->nonReservedParams(params: $params)); + $limit = $this->intOrNull(value: ($params['limit'] ?? null)); + + $points = $this->overviewService->ensureReadablePoints( + register: $register, + schema: $schema, + filters: $filters, + geoProperty: $geoProperty, + limit: $limit + ); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + }//end try + + return new JSONResponse(['points' => $points, 'count' => count($points)]); + }//end points() + + /** + * Strip framework / reserved params, leaving the object filter set. + * + * When no explicit `filters` object is posted, treat the remaining + * query params (minus route + control keys) as object filters. + * + * @param array $params The raw request params. + * + * @return array The object filter params. + */ + private function nonReservedParams(array $params): array + { + $reserved = ['register', 'schema', 'geoProperty', 'limit', 'filters', '_route']; + $filters = []; + foreach ($params as $key => $value) { + if (in_array($key, $reserved, true) === true) { + continue; + } + + $filters[$key] = $value; + } + + return $filters; + }//end nonReservedParams() + + /** + * Coerce a request param to an int, or null when absent / non-numeric. + * + * @param mixed $value The raw param. + * + * @return int|null + */ + private function intOrNull(mixed $value): ?int + { + if ($value === null || is_numeric($value) === false) { + return null; + } + + return (int) $value; + }//end intOrNull() +}//end class diff --git a/lib/Controller/McpController.php b/lib/Controller/McpController.php index bd14d055b3..3bbac45a2a 100644 --- a/lib/Controller/McpController.php +++ b/lib/Controller/McpController.php @@ -20,10 +20,10 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-52 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-55 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-56 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-1-discovery-catalog + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data + * @spec openspec/specs/mcp-discovery/spec.md + * @spec openspec/specs/mcp-discovery/spec.md */ namespace OCA\OpenRegister\Controller; @@ -84,8 +84,8 @@ public function __construct( * * @CORS * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-52 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-55 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-1-discovery-catalog + * @spec openspec/specs/mcp-discovery/spec.md */ public function discover(): JSONResponse { @@ -110,11 +110,13 @@ public function discover(): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Closed enum, no object: McpDiscoveryService::getCapabilityDetail maps the capability + * name against a fixed hardcoded allowlist and returns 404 otherwise; no OpenRegister object lookup. * * @CORS * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-56 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data + * @spec openspec/specs/mcp-discovery/spec.md */ public function discoverCapability(string $capability): JSONResponse { diff --git a/lib/Controller/McpServerController.php b/lib/Controller/McpServerController.php index c0a5fd7296..a636135f9d 100644 --- a/lib/Controller/McpServerController.php +++ b/lib/Controller/McpServerController.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-54 + * @spec openspec/specs/mcp-discovery/spec.md */ namespace OCA\OpenRegister\Controller; @@ -125,7 +125,7 @@ public function __construct( * * @return Response JSON-RPC response or HTTP 202 for notifications * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-54 + * @spec openspec/specs/mcp-discovery/spec.md */ public function handle(): Response { diff --git a/lib/Controller/MergeController.php b/lib/Controller/MergeController.php new file mode 100644 index 0000000000..815ff03e79 --- /dev/null +++ b/lib/Controller/MergeController.php @@ -0,0 +1,154 @@ + + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#5.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Exception\NotAuthorizedException; +use OCA\OpenRegister\Service\Merge\MergeService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\IUserSession; +use RuntimeException; + +class MergeController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The application name. + * @param IRequest $request The current request. + * @param MergeService $mergeService MDM merge engine. + * @param IUserSession $userSession Current user session, for actor attribution. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly MergeService $mergeService, + private readonly IUserSession $userSession + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Side-effect-free preview of a merge: projected survivor golden record, + * provenance and reversal deadline. No object, mergeOperation, or event + * is written. + * + * @return JSONResponse JSON response with the preview payload. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#5.1 + */ + public function preview(): JSONResponse + { + $from = (string) $this->request->getParam('from', ''); + $into = (string) $this->request->getParam('into', ''); + + try { + $result = $this->mergeService->previewMerge(from: $from, into: $into); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($result); + }//end preview() + + /** + * Execute a merge atomically: relink source records, recompute the + * survivor, persist a `mergeOperation` audit row, and dispatch + * `ObjectsMergedEvent`. + * + * @return JSONResponse JSON response with the persisted `mergeOperation`. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#5.1 + */ + public function execute(): JSONResponse + { + $from = (string) $this->request->getParam('from', ''); + $into = (string) $this->request->getParam('into', ''); + $reason = (string) $this->request->getParam('reason', ''); + + $mergedBy = ((string) ($this->userSession->getUser()?->getUID() ?? '')); + + try { + $result = $this->mergeService->executeMerge( + from: $from, + into: $into, + reason: $reason, + mergedBy: $mergedBy + ); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($result); + }//end execute() + + /** + * Reverse a merge within its reversal window, restoring both objects + * from the snapshot and flipping the operation to `reversible: false`. + * + * @param string $id The `mergeOperation` uuid. + * + * @return JSONResponse JSON response with the updated `mergeOperation`. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#5.1 + */ + public function reverse(string $id): JSONResponse + { + $reversedBy = ((string) ($this->userSession->getUser()?->getUID() ?? '')); + + try { + $result = $this->mergeService->reverseMerge(mergeOperationId: $id, reversedBy: $reversedBy); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($result); + }//end reverse() +}//end class diff --git a/lib/Controller/MessageDispatchController.php b/lib/Controller/MessageDispatchController.php new file mode 100644 index 0000000000..1eb3919554 --- /dev/null +++ b/lib/Controller/MessageDispatchController.php @@ -0,0 +1,218 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\Integration\Providers\MessageDispatchProvider; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Outbound-messaging send controller (SMS + WhatsApp dispatch leaf). + * + * @category Controller + * @package OCA\OpenRegister\Controller + */ +class MessageDispatchController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App id. + * @param IRequest $request HTTP request. + * @param MessageDispatchProvider $provider Outbound-messaging dispatch leaf. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly MessageDispatchProvider $provider, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Send one SMS via a named OpenConnector source. + * + * Body params: + * - `source` (required) — one of `cmcom-sms` / `messagebird-sms` / `twilio-sms`. + * - `body` (required) — the vendor-shaped request payload. + * - `path` (required) — the send path relative to the source base URL. + * - `headers` (optional) — extra request headers. + * + * @return JSONResponse `{ status: 'sent', source, response }` on success, + * a 400 when `source`/`path` are missing, or a 503 + * with `details.cause` when the source is + * unconfigured/down (AD-23). + * + * @no-admin-idor-exempt No per-object resource: dispatches via the admin-owned source (base URL admin-configured, + * source checked against a fixed per-channel allowlist); no OpenRegister object id. + * + * @spec openspec/changes/messaging-dispatch-leaf/specs/integration-message-dispatch/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function smsSend(): JSONResponse + { + return $this->relaySend(allowedKinds: ['cmcom-sms', 'messagebird-sms', 'twilio-sms']); + }//end smsSend() + + /** + * Send one WhatsApp message via a named OpenConnector source. + * + * Body params: + * - `source` (required) — one of `whatsapp-cloud-api` / `whatsapp-bsp`. + * - `body` (required) — the Meta-shaped request payload (template or free-form). + * - `path` (required) — the send path relative to the source base URL. + * - `headers` (optional) — extra request headers. + * + * @return JSONResponse `{ status: 'sent', source, response }` on success, + * a 400 when `source`/`path` are missing, or a 503 + * with `details.cause` when the source is + * unconfigured/down (AD-23). + * + * @no-admin-idor-exempt No per-object resource: dispatches via the admin-owned source (base URL admin-configured, + * source checked against a fixed per-channel allowlist); no OpenRegister object id. + * + * @spec openspec/changes/messaging-dispatch-leaf/specs/integration-message-dispatch/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function whatsappSend(): JSONResponse + { + return $this->relaySend(allowedKinds: ['whatsapp-cloud-api', 'whatsapp-bsp']); + }//end whatsappSend() + + /** + * Shared send relay — validates the request, gates the `source` to the + * channel's allowed kinds, dispatches through the provider, and maps the + * degraded descriptor to a 503-with-cause. + * + * @param array $allowedKinds The source slugs valid for this + * channel (SMS vs WhatsApp), so an + * SMS endpoint can't be used to send + * a WhatsApp message and vice versa. + * + * @return JSONResponse + * + * @spec exclude Private helper: validates + relays to MessageDispatchProvider::dispatch; + * the REST contract is owned by + * messaging-dispatch-leaf/specs/integration-message-dispatch/spec.md. + */ + private function relaySend(array $allowedKinds): JSONResponse + { + $source = trim((string) $this->request->getParam('source', '')); + $path = trim((string) $this->request->getParam('path', '')); + $body = $this->request->getParam('body', []); + $headers = $this->request->getParam('headers', []); + + if ($source === '' || $path === '') { + return new JSONResponse(['error' => 'source and path are required'], 400); + } + + if (in_array($source, $allowedKinds, true) === false) { + return new JSONResponse( + ['error' => sprintf('source "%s" is not valid for this channel', $source)], + 400 + ); + } + + if (is_array($body) === false) { + $body = []; + } + + if (is_array($headers) === false) { + $headers = []; + } + + $result = $this->provider->dispatch( + source: $source, + body: $body, + path: $path, + headers: $this->stringifyHeaders(headers: $headers), + ); + + if (($result['unavailable'] ?? false) === true) { + return new JSONResponse( + [ + 'error' => sprintf('messaging source "%s" is not available', $source), + 'details' => ['cause' => $result['cause']], + ], + 503 + ); + } + + return new JSONResponse($result); + }//end relaySend() + + /** + * Coerce a headers bag to `array` — the dispatch contract + * requires string header values. + * + * @param array $headers Raw headers from the request. + * + * @return array + * + * @spec exclude Private input coercion; the dispatch contract is owned by the spec. + */ + private function stringifyHeaders(array $headers): array + { + $out = []; + foreach ($headers as $key => $value) { + if (is_scalar($value) === true) { + $out[(string) $key] = (string) $value; + } + } + + return $out; + }//end stringifyHeaders() +}//end class diff --git a/lib/Controller/MetricsController.php b/lib/Controller/MetricsController.php deleted file mode 100644 index b4a5bc8010..0000000000 --- a/lib/Controller/MetricsController.php +++ /dev/null @@ -1,362 +0,0 @@ - - * @copyright 2025 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.nl - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Controller; - -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\TextPlainResponse; -use OCP\IDBConnection; -use OCP\IRequest; -use OCP\App\IAppManager; -use Psr\Log\LoggerInterface; - -/** - * Controller for exposing Prometheus metrics. - * - * @psalm-suppress UnusedClass - */ -class MetricsController extends Controller -{ - /** - * Constructor. - * - * @param string $appName The application name - * @param IRequest $request The HTTP request - * @param IDBConnection $db Database connection - * @param IAppManager $appManager App manager - * @param LoggerInterface $logger Logger - */ - public function __construct( - string $appName, - IRequest $request, - private IDBConnection $db, - private IAppManager $appManager, - private LoggerInterface $logger, - ) { - parent::__construct(appName: $appName, request: $request); - }//end __construct() - - /** - * Return Prometheus metrics in text exposition format. - * - * @NoCSRFRequired - * - * @return TextPlainResponse Prometheus-formatted metrics - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-18 - */ - public function index(): TextPlainResponse - { - $metrics = $this->collectMetrics(); - $response = new TextPlainResponse($metrics); - $response->addHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); - - return $response; - }//end index() - - /** - * Collect all metrics and format as Prometheus text. - * - * @return string Prometheus exposition format text - */ - private function collectMetrics(): string - { - $lines = []; - - // App info gauge. - $version = $this->getAppVersion(); - $phpVersion = PHP_VERSION; - - $lines[] = '# HELP openregister_info Application information'; - $lines[] = '# TYPE openregister_info gauge'; - $lines[] = 'openregister_info{version="'.$version.'",php_version="'.$phpVersion.'"} 1'; - $lines[] = ''; - - // App up gauge. - $lines[] = '# HELP openregister_up Whether the application is healthy'; - $lines[] = '# TYPE openregister_up gauge'; - $lines[] = 'openregister_up 1'; - $lines[] = ''; - - // Registers total. - $registersTotal = $this->countTable(table: 'openregister_registers'); - $lines[] = '# HELP openregister_registers_total Total number of registers'; - $lines[] = '# TYPE openregister_registers_total gauge'; - $lines[] = 'openregister_registers_total '.$registersTotal; - $lines[] = ''; - - // Schemas total. - $schemasTotal = $this->countTable(table: 'openregister_schemas'); - $lines[] = '# HELP openregister_schemas_total Total number of schemas'; - $lines[] = '# TYPE openregister_schemas_total gauge'; - $lines[] = 'openregister_schemas_total '.$schemasTotal; - $lines[] = ''; - - // Objects total (by register and schema). - $lines[] = '# HELP openregister_objects_total Total objects by register and schema'; - $lines[] = '# TYPE openregister_objects_total gauge'; - $objectCounts = $this->getObjectCountsByRegisterAndSchema(); - foreach ($objectCounts as $row) { - $register = $this->sanitizeLabel(value: $row['register_name']); - $schema = $this->sanitizeLabel(value: $row['schema_name']); - $count = (int) $row['object_count']; - $lines[] = 'openregister_objects_total{register="'.$register.'",schema="'.$schema.'"} '.$count; - } - - $lines[] = ''; - - // Search requests total (from metrics table if it exists). - $searchCount = $this->countMetricsByType(typePrefix: 'search_'); - $lines[] = '# HELP openregister_search_requests_total Total search requests'; - $lines[] = '# TYPE openregister_search_requests_total counter'; - $lines[] = 'openregister_search_requests_total '.$searchCount; - $lines[] = ''; - - // CRUD operation counters sourced from the audit-trail ledger. - // Lifetime counters reset only on audit-trail truncation, so they - // satisfy Prometheus counter monotonicity for typical operations. - $crudCounts = $this->getCrudCountsByAction(); - $lines[] = '# HELP openregister_objects_created_total Total object create operations recorded in the audit trail'; - $lines[] = '# TYPE openregister_objects_created_total counter'; - $lines[] = 'openregister_objects_created_total '.($crudCounts['create'] ?? 0); - $lines[] = ''; - $lines[] = '# HELP openregister_objects_updated_total Total object update operations recorded in the audit trail'; - $lines[] = '# TYPE openregister_objects_updated_total counter'; - $lines[] = 'openregister_objects_updated_total '.($crudCounts['update'] ?? 0); - $lines[] = ''; - $lines[] = '# HELP openregister_objects_deleted_total Total object delete operations recorded in the audit trail'; - $lines[] = '# TYPE openregister_objects_deleted_total counter'; - $lines[] = 'openregister_objects_deleted_total '.($crudCounts['delete'] ?? 0); - $lines[] = ''; - $lines[] = '# HELP openregister_objects_read_total Total object read operations recorded in the audit trail'; - $lines[] = '# TYPE openregister_objects_read_total counter'; - $lines[] = 'openregister_objects_read_total '.($crudCounts['read'] ?? 0); - $lines[] = ''; - - // Webhook delivery counters from the webhook log table. - $webhookCounts = $this->getWebhookCountsByStatus(); - $lines[] = '# HELP openregister_webhook_deliveries_total Total webhook delivery attempts grouped by status'; - $lines[] = '# TYPE openregister_webhook_deliveries_total counter'; - foreach ($webhookCounts as $status => $count) { - $statusLabel = $this->sanitizeLabel(value: (string) $status); - $lines[] = 'openregister_webhook_deliveries_total{status="'.$statusLabel.'"} '.$count; - } - - $lines[] = ''; - - return implode("\n", $lines)."\n"; - }//end collectMetrics() - - /** - * Aggregate audit-trail rows by action. - * - * Returns a map keyed by action ('create', 'update', 'delete', 'read', - * etc.) where each value is the lifetime count for that action. Used - * as the data source for `openregister_objects_{action}_total` - * counters. Failures (table missing, permission denied) collapse to - * an empty array — the counter then emits 0. - * - * @return array Map action => count - */ - private function getCrudCountsByAction(): array - { - try { - $qb = $this->db->getQueryBuilder(); - $qb->select('action') - ->selectAlias($qb->func()->count('*'), 'cnt') - ->from('openregister_audit_trails') - ->groupBy('action'); - - $result = $qb->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - $out = []; - foreach ($rows as $row) { - $action = (string) ($row['action'] ?? ''); - $out[$action] = (int) ($row['cnt'] ?? 0); - } - - return $out; - } catch (\Exception $e) { - $this->logger->warning( - '[MetricsController] Failed to aggregate audit trails by action', - ['error' => $e->getMessage()] - ); - return []; - }//end try - }//end getCrudCountsByAction() - - /** - * Aggregate webhook delivery log rows by status. - * - * The webhook log stores delivery outcome as a boolean `success` - * column; this method projects that into the conventional Prometheus - * status label vocabulary ('success' / 'failure'). Used as the data - * source for the labelled - * `openregister_webhook_deliveries_total{status}` counter so - * operators can alert on rising failure rates without label - * cardinality explosions. Failures (table missing, permission - * denied) collapse to an empty array. - * - * @return array Map status => count - */ - private function getWebhookCountsByStatus(): array - { - try { - $qb = $this->db->getQueryBuilder(); - $qb->select('success') - ->selectAlias($qb->func()->count('*'), 'cnt') - ->from('openregister_webhook_logs') - ->groupBy('success'); - - $result = $qb->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - $out = []; - foreach ($rows as $row) { - $isSuccess = (bool) ($row['success'] ?? false); - $status = 'failure'; - if ($isSuccess === true) { - $status = 'success'; - } - - $out[$status] = (int) ($row['cnt'] ?? 0); - } - - return $out; - } catch (\Exception $e) { - return []; - }//end try - }//end getWebhookCountsByStatus() - - /** - * Count rows in a database table. - * - * @param string $table The table name (without prefix) - * - * @return int Row count - */ - private function countTable(string $table): int - { - try { - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->func()->count('*', 'cnt')) - ->from($table); - $result = $qb->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - return (int) ($row['cnt'] ?? 0); - } catch (\Exception $e) { - $this->logger->warning('[MetricsController] Failed to count table '.$table, ['error' => $e->getMessage()]); - return 0; - } - }//end countTable() - - /** - * Get object counts grouped by register and schema. - * - * @return array Grouped counts - */ - private function getObjectCountsByRegisterAndSchema(): array - { - try { - $qb = $this->db->getQueryBuilder(); - $qb->select('r.title AS register_name', 's.title AS schema_name') - ->selectAlias($qb->func()->count('o.id'), 'object_count') - ->from('openregister_objects', 'o') - ->leftJoin('o', 'openregister_registers', 'r', $qb->expr()->eq('o.register', 'r.id')) - ->leftJoin('o', 'openregister_schemas', 's', $qb->expr()->eq('o.schema', 's.id')) - ->groupBy('o.register', 'o.schema', 'r.title', 's.title'); - - $result = $qb->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - return $rows; - } catch (\Exception $e) { - $this->logger->warning('[MetricsController] Failed to get object counts', ['error' => $e->getMessage()]); - return []; - } - }//end getObjectCountsByRegisterAndSchema() - - /** - * Count metrics entries by type prefix. - * - * @param string $typePrefix The metric type prefix to match - * - * @return int Count of matching metrics - */ - private function countMetricsByType(string $typePrefix): int - { - try { - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->func()->count('*', 'cnt')) - ->from('openregister_metrics') - ->where($qb->expr()->like('metric_type', $qb->createNamedParameter($typePrefix.'%'))); - - $result = $qb->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - return (int) ($row['cnt'] ?? 0); - } catch (\Exception $e) { - // Table may not exist yet. - return 0; - } - }//end countMetricsByType() - - /** - * Get the app version from the app manager. - * - * @return string The app version - */ - private function getAppVersion(): string - { - try { - return $this->appManager->getAppVersion('openregister'); - } catch (\Exception $e) { - return 'unknown'; - } - }//end getAppVersion() - - /** - * Sanitize a label value for Prometheus format. - * - * @param string $value The label value - * - * @return string Sanitized label value - */ - private function sanitizeLabel(string $value): string - { - // Escape backslashes, double quotes, and newlines. - return str_replace( - ['\\', '"', "\n"], - ['\\\\', '\\"', '\\n'], - $value - ); - }//end sanitizeLabel() -}//end class diff --git a/lib/Controller/MigrationPacksController.php b/lib/Controller/MigrationPacksController.php new file mode 100644 index 0000000000..f7d5ad15a0 --- /dev/null +++ b/lib/Controller/MigrationPacksController.php @@ -0,0 +1,404 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use InvalidArgumentException; +use OCA\OpenRegister\Service\MigrationPackService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\DataDownloadResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * MigrationPacksController handles CRUD + JSON import/export for migration packs. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ +class MigrationPacksController extends Controller +{ + /** + * Constructor. + * + * @param string $appName Application name. + * @param IRequest $request HTTP request. + * @param MigrationPackService $service Business logic. + * @param IUserSession $userSession Current-user session. + * @param IGroupManager $groupManager Group manager (admin check). + * @param LoggerInterface $logger Logger. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly MigrationPackService $service, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, + private readonly LoggerInterface $logger + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Resolve the current user's uid, or null when anonymous. + * + * @return ?string + */ + private function resolveUserId(): ?string + { + $user = $this->userSession->getUser(); + if ($user === null) { + return null; + } + + return $user->getUID(); + }//end resolveUserId() + + /** + * Whether the current user is a Nextcloud administrator. + * + * @return bool + */ + private function isCurrentUserAdmin(): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + return $this->groupManager->isAdmin($user->getUID()); + }//end isCurrentUserAdmin() + + /** + * Build a 401 response for anonymous callers. + * + * @return JSONResponse + */ + private function authRequiredResponse(): JSONResponse + { + return new JSONResponse(data: ['error' => 'Authentication required'], statusCode: 401); + }//end authRequiredResponse() + + /** + * Build a 403 response for non-admin callers on an admin-only action. + * + * @return JSONResponse + */ + private function adminRequiredResponse(): JSONResponse + { + return new JSONResponse(data: ['error' => 'Administrator privileges required'], statusCode: 403); + }//end adminRequiredResponse() + + /** + * List every migration pack. Any authenticated user may browse packs to + * pick one for an import request. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function index(): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + $packs = $this->service->findAll(); + $items = array_map(static fn($pack) => $pack->jsonSerialize(), $packs); + + return new JSONResponse(data: ['results' => $items, 'total' => count($items)]); + }//end index() + + /** + * Get a single migration pack. + * + * @param int $id The migration pack id. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function show(int $id): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + try { + $pack = $this->service->find(id: $id); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Migration pack not found'], statusCode: 404); + } + + return new JSONResponse(data: $pack->jsonSerialize()); + }//end show() + + /** + * Download a migration pack's definition as a standalone JSON file, so it + * can be shared with (and imported into) another instance. + * + * @param int $id The migration pack id. + * + * @return JSONResponse|DataDownloadResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function export(int $id): JSONResponse|DataDownloadResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + try { + $pack = $this->service->find(id: $id); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Migration pack not found'], statusCode: 404); + } + + $json = json_encode($pack->getDefinitionArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $filename = $pack->getPackSlug().'.json'; + + return new DataDownloadResponse($json, $filename, 'application/json'); + }//end export() + + /** + * Import a migration pack definition from an uploaded JSON file. Admin only. + * Upserts by the document's own `id` — re-importing an updated pack + * updates the existing row. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function import(): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + if ($this->isCurrentUserAdmin() === false) { + return $this->adminRequiredResponse(); + } + + $uploadedFile = $this->request->getUploadedFile('file'); + if (empty($uploadedFile['tmp_name']) === true) { + return new JSONResponse(data: ['error' => 'No file uploaded'], statusCode: 400); + } + + $raw = file_get_contents($uploadedFile['tmp_name']); + $decoded = null; + if ($raw !== false) { + $decoded = json_decode($raw, true); + } + + if (is_array($decoded) === false) { + return new JSONResponse(data: ['error' => 'Uploaded file is not a valid JSON object'], statusCode: 400); + } + + try { + $pack = $this->service->importDefinition(definition: $decoded, ownerUid: $userId); + } catch (InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 422); + } catch (\Throwable $e) { + $this->logger->error( + message: '[MigrationPacksController] Error importing migration pack: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'trace' => $e->getTraceAsString()] + ); + return new JSONResponse(data: ['error' => 'Failed to import migration pack'], statusCode: 500); + } + + return new JSONResponse(data: $pack->jsonSerialize(), statusCode: 201); + }//end import() + + /** + * Create a migration pack. Admin only. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function create(): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + if ($this->isCurrentUserAdmin() === false) { + return $this->adminRequiredResponse(); + } + + $definition = $this->request->getParams(); + foreach (array_keys($definition) as $key) { + if (str_starts_with($key, '_') === true) { + unset($definition[$key]); + } + } + + try { + $pack = $this->service->create(definition: $definition, ownerUid: $userId); + } catch (InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 422); + } catch (\Throwable $e) { + $this->logger->error( + message: '[MigrationPacksController] Error creating migration pack: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'trace' => $e->getTraceAsString()] + ); + return new JSONResponse(data: ['error' => 'Failed to create migration pack'], statusCode: 500); + } + + return new JSONResponse(data: $pack->jsonSerialize(), statusCode: 201); + }//end create() + + /** + * Update an existing migration pack. Admin only. + * + * @param int $id The migration pack id. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function update(int $id): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + if ($this->isCurrentUserAdmin() === false) { + return $this->adminRequiredResponse(); + } + + $definition = $this->request->getParams(); + foreach (array_keys($definition) as $key) { + if (str_starts_with($key, '_') === true) { + unset($definition[$key]); + } + } + + try { + $pack = $this->service->update(id: $id, definition: $definition); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Migration pack not found'], statusCode: 404); + } catch (InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 422); + } catch (\Throwable $e) { + $this->logger->error( + message: '[MigrationPacksController] Error updating migration pack: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $id, 'trace' => $e->getTraceAsString()] + ); + return new JSONResponse(data: ['error' => 'Failed to update migration pack'], statusCode: 500); + } + + return new JSONResponse(data: $pack->jsonSerialize()); + }//end update() + + /** + * Delete a migration pack. Admin only. + * + * @param int $id The migration pack id. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function destroy(int $id): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + if ($this->isCurrentUserAdmin() === false) { + return $this->adminRequiredResponse(); + } + + try { + $this->service->delete(id: $id); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Migration pack not found'], statusCode: 404); + } catch (\Throwable $e) { + $this->logger->error( + message: '[MigrationPacksController] Error deleting migration pack: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $id, 'trace' => $e->getTraceAsString()] + ); + return new JSONResponse(data: ['error' => 'Failed to delete migration pack'], statusCode: 500); + } + + return new JSONResponse(data: null, statusCode: 204); + }//end destroy() +}//end class diff --git a/lib/Controller/NamesController.php b/lib/Controller/NamesController.php index 69dcb818c6..21671a6df4 100644 --- a/lib/Controller/NamesController.php +++ b/lib/Controller/NamesController.php @@ -37,6 +37,7 @@ use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; +use OCP\IUserSession; use Psr\Log\LoggerInterface; /** @@ -66,12 +67,14 @@ class NamesController extends Controller * @param IRequest $request HTTP request object * @param CacheHandler $objectCacheService Object cache service for name operations * @param LoggerInterface $logger Logger for performance monitoring + * @param IUserSession $userSession User session for the current user */ public function __construct( string $appName, IRequest $request, private readonly CacheHandler $objectCacheService, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly IUserSession $userSession ) { parent::__construct(appName: $appName, request: $request); }//end __construct() @@ -103,19 +106,27 @@ public function __construct( * * @NoCSRFRequired * - * @PublicPage - * * @throws \Exception If name lookup fails * * @return JSONResponse JSON response with object names or error * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-22 + * @spec openspec/specs/schema-driven-read-coercion/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] - #[PublicPage] public function index(): JSONResponse { + // SEC-CTRL-2: require authentication — this endpoint must not leak object/ + // organisation names anonymously. Dropped @PublicPage. + if ($this->userSession->getUser() === null) { + return new JSONResponse(data: ['error' => 'Authentication required'], statusCode: 401); + } + + // TODO(SEC-CTRL-2): make name resolution RBAC/tenant-aware. getMultipleObjectNames() + // and getAllObjectNames() in lib/Service/Object/CacheHandler.php + // (warmupNameCache / findAllWithUserCount / getObjectMapper()->findAll()) currently + // return names across ALL organisations with no RBAC filtering. Filter by the + // caller's read permissions + active organisation there before widening exposure. $startTime = microtime(true); try { @@ -245,19 +256,24 @@ public function index(): JSONResponse * * @NoCSRFRequired * - * @PublicPage - * * @throws \Exception If name lookup fails * * @return JSONResponse JSON response with object names or error * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-22 + * @spec openspec/specs/schema-driven-read-coercion/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] - #[PublicPage] public function create(): JSONResponse { + // SEC-CTRL-2: require authentication — per-ids name resolution must not be + // reachable anonymously. Dropped @PublicPage. + if ($this->userSession->getUser() === null) { + return new JSONResponse(data: ['error' => 'Authentication required'], statusCode: 401); + } + + // TODO(SEC-CTRL-2): getMultipleObjectNames() in CacheHandler resolves names with + // no RBAC/tenant filtering; restrict resolved ids to those the caller may read. $startTime = microtime(true); try { @@ -356,7 +372,7 @@ public function create(): JSONResponse * * @return JSONResponse JSON response with object name or error * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-22 + * @spec openspec/specs/schema-driven-read-coercion/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -453,7 +469,7 @@ public function show(string $id): JSONResponse * distributed_cache_available: true, warmup_available: true}}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-22 + * @spec openspec/specs/schema-driven-read-coercion/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -501,7 +517,7 @@ public function stats(): JSONResponse * * @return JSONResponse JSON response with warmup result or error * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-22 + * @spec openspec/specs/schema-driven-read-coercion/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] diff --git a/lib/Controller/NotesController.php b/lib/Controller/NotesController.php index 87e4e9733a..c605a43163 100644 --- a/lib/Controller/NotesController.php +++ b/lib/Controller/NotesController.php @@ -17,7 +17,7 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-60 + * @spec openspec/specs/object-interactions/spec.md */ declare(strict_types=1); @@ -92,7 +92,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-60 + * @spec openspec/specs/object-interactions/spec.md */ public function index( string $register, @@ -134,7 +134,7 @@ public function index( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-60 + * @spec openspec/specs/object-interactions/spec.md */ public function create( string $register, @@ -232,7 +232,7 @@ public function update( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-60 + * @spec openspec/specs/object-interactions/spec.md */ public function destroy( string $register, diff --git a/lib/Controller/NotificationDeliveryWindowController.php b/lib/Controller/NotificationDeliveryWindowController.php new file mode 100644 index 0000000000..8c332af70b --- /dev/null +++ b/lib/Controller/NotificationDeliveryWindowController.php @@ -0,0 +1,146 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/notificatie-engine/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\Notification\NotificationDeliveryWindowService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\IUserSession; + +class NotificationDeliveryWindowController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App name. + * @param IRequest $request Request. + * @param NotificationDeliveryWindowService $windowService Override-only window store + evaluator. + * @param IUserSession $userSession Current-user session. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly NotificationDeliveryWindowService $windowService, + private readonly IUserSession $userSession + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Return the current user's stored delivery-window preference, or + * `{enabled: false}` when none is configured. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/notificatie-engine/spec.md + */ + public function index(): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return new JSONResponse(data: ['error' => 'Authentication required'], statusCode: 401); + } + + $window = $this->windowService->getForUser(userId: $userId); + if ($window === null) { + return new JSONResponse(data: ['enabled' => false]); + } + + return new JSONResponse(data: $window); + }//end index() + + /** + * Store (or, with `enabled: false`, clear) the current user's + * delivery-window preference. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/notificatie-engine/spec.md + */ + public function update(): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return new JSONResponse(data: ['error' => 'Authentication required'], statusCode: 401); + } + + $params = $this->request->getParams(); + $enabled = ($params['enabled'] ?? true); + + if ($enabled === false) { + $this->windowService->setForUser(userId: $userId, window: null); + return new JSONResponse(data: ['enabled' => false]); + } + + try { + $this->windowService->setForUser(userId: $userId, window: $params); + } catch (\InvalidArgumentException $e) { + return new JSONResponse( + data: ['error' => $e->getMessage(), 'code' => 'notification-delivery-window-invalid'], + statusCode: 422 + ); + } + + return new JSONResponse(data: ($this->windowService->getForUser(userId: $userId) ?? ['enabled' => false])); + }//end update() + + /** + * Resolve the current user's UID, or null when anonymous. + * + * @return string|null + */ + private function resolveUserId(): ?string + { + $user = $this->userSession->getUser(); + if ($user === null) { + return null; + } + + return $user->getUID(); + }//end resolveUserId() +}//end class diff --git a/lib/Controller/NotificationHistoryController.php b/lib/Controller/NotificationHistoryController.php index 1ebd7caeff..37a30c6d80 100644 --- a/lib/Controller/NotificationHistoryController.php +++ b/lib/Controller/NotificationHistoryController.php @@ -23,7 +23,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/notificatie-engine/tasks.md + * @spec openspec/specs/notificatie-engine/spec.md */ declare(strict_types=1); @@ -76,7 +76,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/notificatie-engine/tasks.md + * @spec openspec/specs/notificatie-engine/spec.md */ public function index(): JSONResponse { diff --git a/lib/Controller/NotificationPreferencesController.php b/lib/Controller/NotificationPreferencesController.php index 1af132f7a1..d1a7e20f97 100644 --- a/lib/Controller/NotificationPreferencesController.php +++ b/lib/Controller/NotificationPreferencesController.php @@ -11,7 +11,10 @@ * * GET /api/notification-preferences * → every notification the current user's accessible schemas declare, - * merged with that user's overrides, tagged by source. + * merged with that user's overrides, tagged by source. Each entry + * also carries `application` (the owning app id, e.g. "pipelinq", + * or null when the schema has no known owning app) so a consuming + * settings UI can scope the list to the currently open app. * * PUT /api/notification-preferences * body: { schema, notification, enabled?, channels?, reset? } diff --git a/lib/Controller/NotificationSubscriptionsController.php b/lib/Controller/NotificationSubscriptionsController.php index 115501b324..f0049d0da4 100644 --- a/lib/Controller/NotificationSubscriptionsController.php +++ b/lib/Controller/NotificationSubscriptionsController.php @@ -29,7 +29,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/notificatie-engine/tasks.md "Users MUST be able to manage their notification preferences" + * @spec openspec/specs/notificatie-engine/spec.md "Users MUST be able to manage their notification preferences" */ declare(strict_types=1); @@ -88,7 +88,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/notificatie-engine/tasks.md "Users MUST be able to manage their notification preferences" + * @spec openspec/specs/notificatie-engine/spec.md "Users MUST be able to manage their notification preferences" */ public function index(): JSONResponse { @@ -117,7 +117,7 @@ public function index(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/notificatie-engine/tasks.md "Users MUST be able to manage their notification preferences" + * @spec openspec/specs/notificatie-engine/spec.md "Users MUST be able to manage their notification preferences" */ public function create(): JSONResponse { @@ -180,7 +180,7 @@ public function create(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/notificatie-engine/tasks.md "Users MUST be able to manage their notification preferences" + * @spec openspec/specs/notificatie-engine/spec.md "Users MUST be able to manage their notification preferences" */ public function destroy(): JSONResponse { diff --git a/lib/Controller/OasController.php b/lib/Controller/OasController.php index 5320a7cbd9..2d2102565e 100644 --- a/lib/Controller/OasController.php +++ b/lib/Controller/OasController.php @@ -24,9 +24,9 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-01-oas-generation/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-05-01-oas-generation/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-22 + * @spec openspec/specs/oas-generation/spec.md + * @spec openspec/specs/oas-generation/spec.md + * @spec openspec/specs/oas-generation/spec.md */ namespace OCA\OpenRegister\Controller; @@ -87,7 +87,7 @@ public function __construct( * * @psalm-return JSONResponse<200|422|500, array, array> * - * @spec openspec/changes/retrofit-2026-05-01-oas-generation/tasks.md#task-1 + * @spec openspec/specs/oas-generation/spec.md */ public function generateAll(): JSONResponse { @@ -109,7 +109,7 @@ public function generateAll(): JSONResponse * * @psalm-return JSONResponse<200|422|500, array, array> * - * @spec openspec/changes/retrofit-2026-05-01-oas-generation/tasks.md#task-2 + * @spec openspec/specs/oas-generation/spec.md */ public function generate(string $id): JSONResponse { @@ -126,7 +126,7 @@ public function generate(string $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-22 + * @spec openspec/specs/oas-generation/spec.md */ private function generateInternal(?string $registerId): JSONResponse { diff --git a/lib/Controller/ObjectIntegrationsController.php b/lib/Controller/ObjectIntegrationsController.php index 8bd9a321ff..0f4e1dbc9e 100644 --- a/lib/Controller/ObjectIntegrationsController.php +++ b/lib/Controller/ObjectIntegrationsController.php @@ -49,6 +49,7 @@ use OCA\OpenRegister\Service\Integration\IntegrationRegistry; use OCA\OpenRegister\Service\Integration\PaginatedResult; use OCA\OpenRegister\Service\Integration\QueryTimeContract; +use OCA\OpenRegister\Service\ObjectService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\NoAdminRequired; @@ -64,10 +65,11 @@ class ObjectIntegrationsController extends Controller /** * Constructor. * - * @param string $appName App name (injected by NC). - * @param IRequest $request Current request. - * @param IntegrationRegistry $registry Integration registry. - * @param LoggerInterface $logger Logger. + * @param string $appName App name (injected by NC). + * @param IRequest $request Current request. + * @param IntegrationRegistry $registry Integration registry. + * @param LoggerInterface $logger Logger. + * @param ObjectService $objectService OpenRegister object service (RBAC boundary). * * @return void */ @@ -76,10 +78,48 @@ public function __construct( IRequest $request, private IntegrationRegistry $registry, private LoggerInterface $logger, + private ObjectService $objectService, ) { parent::__construct(appName: $appName, request: $request); }//end __construct() + /** + * Guard access to the target OpenRegister object before any integration + * dispatch. + * + * The integration endpoints receive an OR object id ($id) and forward it to + * a pluggable provider. Without an OR-side authorization check any + * authenticated user could manage the integrations of an arbitrary object + * by id (IDOR). Resolving the object through ObjectService applies OR's + * register RBAC + multitenancy (ADR-022); an object the caller cannot read + * resolves to null and the request is refused with 404 (existence is not + * leaked). + * + * @param string $register Register slug or numeric id. + * @param string $schema Schema slug or numeric id. + * @param string $id Object uuid. + * + * @return JSONResponse|null 404 response when access is denied, null when allowed. + */ + private function guardObjectAccess(string $register, string $schema, string $id): ?JSONResponse + { + $this->objectService->setRegister($register); + $this->objectService->setSchema($schema); + $this->objectService->setObject($id); + + try { + $object = $this->objectService->getObject(); + } catch (\Throwable $e) { + return new JSONResponse(['message' => 'Object not found'], Http::STATUS_NOT_FOUND); + } + + if ($object === null) { + return new JSONResponse(['message' => 'Object not found'], Http::STATUS_NOT_FOUND); + } + + return null; + }//end guardObjectAccess() + /** * GET /api/objects/{register}/{schema}/{id}/integrations/{integrationId} * @@ -106,6 +146,11 @@ public function __construct( #[NoAdminRequired] public function index(string $register, string $schema, string $id, string $integrationId): JSONResponse { + $denial = $this->guardObjectAccess(register: $register, schema: $schema, id: $id); + if ($denial !== null) { + return $denial; + } + return $this->dispatch( integrationId: $integrationId, callback: fn ($provider) => PaginatedResult::fromMixed( @@ -132,6 +177,11 @@ public function index(string $register, string $schema, string $id, string $inte #[NoAdminRequired] public function show(string $register, string $schema, string $id, string $integrationId, string $entityId): JSONResponse { + $denial = $this->guardObjectAccess(register: $register, schema: $schema, id: $id); + if ($denial !== null) { + return $denial; + } + return $this->dispatch( integrationId: $integrationId, callback: fn ($provider) => $provider->get($register, $schema, $id, $entityId) @@ -155,6 +205,11 @@ public function show(string $register, string $schema, string $id, string $integ #[NoAdminRequired] public function create(string $register, string $schema, string $id, string $integrationId): JSONResponse { + $denial = $this->guardObjectAccess(register: $register, schema: $schema, id: $id); + if ($denial !== null) { + return $denial; + } + $payload = $this->collectPayload(); return $this->dispatch( integrationId: $integrationId, @@ -181,6 +236,11 @@ public function create(string $register, string $schema, string $id, string $int #[NoAdminRequired] public function update(string $register, string $schema, string $id, string $integrationId, string $entityId): JSONResponse { + $denial = $this->guardObjectAccess(register: $register, schema: $schema, id: $id); + if ($denial !== null) { + return $denial; + } + $payload = $this->collectPayload(); return $this->dispatch( integrationId: $integrationId, @@ -206,6 +266,11 @@ public function update(string $register, string $schema, string $id, string $int #[NoAdminRequired] public function destroy(string $register, string $schema, string $id, string $integrationId, string $entityId): JSONResponse { + $denial = $this->guardObjectAccess(register: $register, schema: $schema, id: $id); + if ($denial !== null) { + return $denial; + } + try { $provider = $this->resolveProvider(integrationId: $integrationId); $provider->delete($register, $schema, $id, $entityId); diff --git a/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php index 7856cc7774..e4209fd030 100644 --- a/lib/Controller/ObjectsController.php +++ b/lib/Controller/ObjectsController.php @@ -20,12 +20,12 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-95 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-62 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-30 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-20 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ declare(strict_types=1); @@ -36,6 +36,8 @@ use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Exception\CustomValidationException; +use OCA\OpenRegister\Exception\ExportTooLargeException; +use OCA\OpenRegister\Exception\FolderAccessDeniedException; use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Exception\RegisterNotFoundException; use OCA\OpenRegister\Exception\SchemaNotFoundException; @@ -54,6 +56,7 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\DB\Exception; @@ -85,9 +88,12 @@ * @suppressWarnings(PHPMD.ExcessiveMethodLength) Complex file upload handling with multiple formats * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @spec openspec/specs/aggregation-api/spec.md */ class ObjectsController extends Controller { + use \OCA\OpenRegister\Controller\Trait\HandlesExceptionsTrait; /** * Export service for handling data exports @@ -106,23 +112,29 @@ class ObjectsController extends Controller /** * Constructor for the ObjectsController * - * @param string $appName The name of the app - * @param IRequest $request The request object - * @param IAppConfig $config The app configuration object - * @param IAppManager $appManager The app manager - * @param ContainerInterface $container The DI container - * @param RegisterMapper $registerMapper The register mapper - * @param SchemaMapper $schemaMapper The schema mapper - * @param AuditTrailMapper $auditTrailMapper The audit trail mapper - * @param ObjectService $objectService The object service - * @param IUserSession $userSession The user session - * @param IGroupManager $groupManager The group manager - * @param ExportService $exportService The export service - * @param ImportService $importService The import service - * @param WebhookService $webhookService The webhook service (optional) - * @param LoggerInterface $logger The logger (optional) - * @param ?\OCA\OpenRegister\Service\Geo\GeoFilterParser $geoFilterParser Optional geo wire-format adapter (null-safe) - * @param ?\OCA\OpenRegister\Service\Geo\GeoFilterApplier $geoFilterApplier Optional geo post-filter (null-safe) + * @param string $appName The name of the app + * @param IRequest $request The request object + * @param IAppConfig $config The app configuration object + * @param IAppManager $appManager The app manager + * @param ContainerInterface $container The DI container + * @param RegisterMapper $registerMapper The register mapper + * @param SchemaMapper $schemaMapper The schema mapper + * @param AuditTrailMapper $auditTrailMapper The audit trail mapper + * @param ObjectService $objectService The object service + * @param IUserSession $userSession The user session + * @param IGroupManager $groupManager The group manager + * @param ExportService $exportService The export service + * @param ImportService $importService The import service + * @param WebhookService $webhookService The webhook service (optional) + * @param LoggerInterface $logger The logger (optional) + * @param ?\OCA\OpenRegister\Service\Geo\GeoFilterParser $geoFilterParser Optional geo wire-format adapter (null-safe) + * @param ?\OCA\OpenRegister\Service\Geo\GeoFilterApplier $geoFilterApplier Optional geo post-filter (null-safe) + * @param ?\OCA\OpenRegister\Service\JsonLd\JsonLdSerializer $jsonLdSerializer Optional JSON-LD serializer (null-safe) + * @param ?\OCA\OpenRegister\Service\JsonLd\JsonLdContextService $jsonLdContextService Optional JSON-LD context service (null-safe) + * @param ?\OCA\OpenRegister\Service\Geo\GeoFeatureCollectionBuilder $geoFeatureBuilder Optional GeoJSON/WFS feature builder (null-safe) + * @param ?\OCA\OpenRegister\Service\Geo\PdokGeocoder $pdokGeocoder Optional PDOK geocoder (null-safe) + * @param ?\OCA\OpenRegister\Service\DeepLinkRegistryService $deepLinkRegistry Relation resourceUrl resolver (null-safe) + * @param ?\OCP\IURLGenerator $relationUrlGenerator Relation fallback URL generator (null-safe) * * @return void * @@ -145,7 +157,13 @@ public function __construct( private readonly ?WebhookService $webhookService=null, private readonly ?LoggerInterface $logger=null, private readonly ?\OCA\OpenRegister\Service\Geo\GeoFilterParser $geoFilterParser=null, - private readonly ?\OCA\OpenRegister\Service\Geo\GeoFilterApplier $geoFilterApplier=null + private readonly ?\OCA\OpenRegister\Service\Geo\GeoFilterApplier $geoFilterApplier=null, + private readonly ?\OCA\OpenRegister\Service\JsonLd\JsonLdSerializer $jsonLdSerializer=null, + private readonly ?\OCA\OpenRegister\Service\JsonLd\JsonLdContextService $jsonLdContextService=null, + private readonly ?\OCA\OpenRegister\Service\Geo\GeoFeatureCollectionBuilder $geoFeatureBuilder=null, + private readonly ?\OCA\OpenRegister\Service\Geo\PdokGeocoder $pdokGeocoder=null, + private readonly ?\OCA\OpenRegister\Service\DeepLinkRegistryService $deepLinkRegistry=null, + private readonly ?\OCP\IURLGenerator $relationUrlGenerator=null ) { parent::__construct(appName: $appName, request: $request); $this->exportService = $exportService; @@ -174,6 +192,89 @@ private function isCurrentUserAdmin(): bool return in_array('admin', $userGroups); }//end isCurrentUserAdmin() + /** + * Whether the current request asks for JSON-LD output via content negotiation. + * + * Null-safe: when the JSON-LD services are not wired (e.g. minimal DI in a + * test harness) this always returns false and the default JSON path is kept. + * + * @return bool True when JSON-LD output is requested and available. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + private function wantsJsonLd(): bool + { + if ($this->jsonLdSerializer === null) { + return false; + } + + return $this->jsonLdSerializer->wantsJsonLd(request: $this->request); + }//end wantsJsonLd() + + /** + * Decorate an already-rendered single-object array as a JSON-LD JSONResponse. + * + * The serializer wraps the rendered array only — it introduces no second + * data path, so RBAC / multitenancy / field-level security / the published + * predicate all remain applied exactly as for the plain-JSON response. + * + * @param array $renderedObject The rendered object array. + * @param \OCA\OpenRegister\Db\Register|null $register The resolved register entity. + * @param \OCA\OpenRegister\Db\Schema|null $schema The resolved schema entity. + * + * @return JSONResponse The JSON-LD response (Content-Type/Vary set). + * + * @spec openspec/specs/json-ld-output/spec.md + */ + private function jsonLdObjectResponse(array $renderedObject, $register, $schema): JSONResponse + { + $document = $this->jsonLdSerializer->serialize( + renderedObject: $renderedObject, + schema: $schema, + register: $register + ); + + return $this->withJsonLdHeaders(response: new JSONResponse(data: $document)); + }//end jsonLdObjectResponse() + + /** + * Decorate a paginated collection result as a JSON-LD `@graph` JSONResponse. + * + * @param array $result The paginated result array. + * @param \OCA\OpenRegister\Db\Register|null $register The resolved register entity. + * @param \OCA\OpenRegister\Db\Schema|null $schema The resolved schema entity. + * + * @return JSONResponse The JSON-LD response (Content-Type/Vary set). + * + * @spec openspec/specs/json-ld-output/spec.md + */ + private function jsonLdCollectionResponse(array $result, $register, $schema): JSONResponse + { + $document = $this->jsonLdSerializer->serializeCollection( + paginatedResult: $result, + schema: $schema, + register: $register + ); + + return $this->withJsonLdHeaders(response: new JSONResponse(data: $document)); + }//end jsonLdCollectionResponse() + + /** + * Add the JSON-LD content negotiation headers to a response. + * + * @param JSONResponse $response The response to decorate. + * + * @return JSONResponse The decorated response. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + private function withJsonLdHeaders(JSONResponse $response): JSONResponse + { + $response->addHeader('Content-Type', 'application/ld+json'); + $response->addHeader('Vary', 'Accept'); + return $response; + }//end withJsonLdHeaders() + /** * Normalize form data values by decoding JSON strings. * @@ -218,6 +319,52 @@ private function normalizeFormDataValues(array $data): array return $data; }//end normalizeFormDataValues() + /** + * Strip server-managed @self fields from client-supplied object data. + * + * The top-level filter in create/update/patch/postPatch already passes `@self` + * through unchanged because certain integrations legitimately set `@self.slug` + * or `@self.relations`. However, several `@self` sub-fields MUST NOT be accepted + * from client input because they are either server-authoritative (owner, organisation) + * or carry security-sensitive semantics (authorization, groups). + * + * The service layer (SaveObject::setSelfMetadata + applyOwnerAttribution) enforces + * the same rules; this controller-level strip is an additional defense-in-depth + * boundary that catches injections before they even reach the service (wave-11 WF2). + * + * Allowed @self keys for client input (non-exhaustive; extend as features are added): + * slug, name, description, summary, image, relations, tmlo (update path only) + * + * Rejected at this layer (server-managed or security-sensitive): + * owner, organisation, authorization, groups, application, folder + * + * @param array $data The raw request data (may contain a '@self' key) + * + * @return array The data with dangerous @self sub-keys stripped + */ + private function sanitiseSelfMetadata(array $data): array + { + if (isset($data['@self']) === false || is_array($data['@self']) === false) { + return $data; + } + + // Fields that clients must never supply — they are set server-side. + $serverManagedKeys = [ + 'owner', + 'organisation', + 'authorization', + 'groups', + 'application', + 'folder', + ]; + + foreach ($serverManagedKeys as $key) { + unset($data['@self'][$key]); + } + + return $data; + }//end sanitiseSelfMetadata() + /** * Extract all uploaded files from the current request. * @@ -701,46 +848,53 @@ private function parseMultiValue($param, string $defaultValue): array private function crossTableSearch(array $registers, array $schemas, ObjectService $objectService): JSONResponse { $magicMapper = \OC::$server->get(\OCA\OpenRegister\Db\MagicMapper::class); - $registerMapper = \OC::$server->get(\OCA\OpenRegister\Db\RegisterMapper::class); - $schemaMapper = \OC::$server->get(\OCA\OpenRegister\Db\SchemaMapper::class); + $registerMapper = $this->registerMapper; + $schemaMapper = $this->schemaMapper; + + // PERF-14: resolve each register and schema exactly once up front instead of + // re-running find() for every register×schema combination in the inner loop. + $registerEntities = []; + foreach (array_unique($registers) as $registerId) { + try { + $registerEntities[(string) $registerId] = $registerMapper->find(id: $registerId, _multitenancy: false, _rbac: false); + } catch (\Exception $e) { + $this->logger->warning( + message: '[ObjectsController] Invalid register in cross-table search', + context: ['file' => __FILE__, 'line' => __LINE__, 'register' => $registerId, 'error' => $e->getMessage()] + ); + } + } + + $schemaEntities = []; + foreach (array_unique($schemas) as $schemaId) { + try { + $schemaEntities[(string) $schemaId] = $schemaMapper->find(id: $schemaId, _multitenancy: false, _rbac: false); + } catch (\Exception $e) { + $this->logger->warning( + message: '[ObjectsController] Invalid schema in cross-table search', + context: ['file' => __FILE__, 'line' => __LINE__, 'schema' => $schemaId, 'error' => $e->getMessage()] + ); + } + } - // Build register+schema pairs. + // Build register+schema pairs from the pre-resolved entities. $pairs = []; - foreach ($registers as $registerId) { - foreach ($schemas as $schemaId) { - try { - // Resolve register and schema entities. - $registerEntity = $registerMapper->find(id: $registerId, _multitenancy: false, _rbac: false); - $schemaEntity = $schemaMapper->find(id: $schemaId, _multitenancy: false, _rbac: false); - - // Check if magic mapping is enabled for this combination. - // Uses Register::isMagicMappingEnabledForSchema() which supports both - // new format {"schemas": {"slug": {"magicMapping": true}}} and - // legacy format {"enableMagicMapping": true, "magicMappingSchemas": [...]}. - if ($registerEntity->isMagicMappingEnabledForSchema( - schemaId: $schemaEntity->getId(), - schemaSlug: $schemaEntity->getSlug() - ) === true - ) { - $pairs[] = [ - 'register' => $registerEntity, - 'schema' => $schemaEntity, - ]; - } - } catch (\Exception $e) { - // Skip invalid register/schema combinations. - $this->logger->warning( - message: '[ObjectsController] Invalid register/schema in cross-table search', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'register' => $registerId, - 'schema' => $schemaId, - 'error' => $e->getMessage(), - ] - ); - continue; - }//end try + foreach ($registerEntities as $registerEntity) { + foreach ($schemaEntities as $schemaEntity) { + // Check if magic mapping is enabled for this combination. + // Uses Register::isMagicMappingEnabledForSchema() which supports both + // new format {"schemas": {"slug": {"magicMapping": true}}} and + // legacy format {"enableMagicMapping": true, "magicMappingSchemas": [...]}. + if ($registerEntity->isMagicMappingEnabledForSchema( + schemaId: $schemaEntity->getId(), + schemaSlug: $schemaEntity->getSlug() + ) === true + ) { + $pairs[] = [ + 'register' => $registerEntity, + 'schema' => $schemaEntity, + ]; + } }//end foreach }//end foreach @@ -759,6 +913,18 @@ private function crossTableSearch(array $registers, array $schemas, ObjectServic // Cross-table search handles multiple register+schema pairs internally. $query = $objectService->buildSearchQuery(requestParams: $this->request->getParams()); + // SEC-CTRL-1: This path does NOT read rbac/multi from the request, so the + // request-controlled bypass does not apply here. Derive the posture from + // admin status for completeness and forward it on the query. + // TODO(SEC-CTRL-1): MagicMapper::searchAcrossMultipleTables() and its + // union/sequential builders currently apply NO RBAC or multitenancy filter + // (they ignore these query flags). Enforcing per-pair RBAC/tenant scoping + // lives in lib/Db/MagicMapper.php (out of this controller's scope) and must + // be wired there before cross-table search is exposed to non-admins. + $isAdmin = $this->isCurrentUserAdmin(); + $query['_rbac'] = ($isAdmin === false); + $query['_multitenancy'] = ($isAdmin === false); + // Remove all register/schema context from query to prevent filtering. unset( $query['_register'], @@ -773,6 +939,17 @@ private function crossTableSearch(array $registers, array $schemas, ObjectServic // Perform cross-table search. $results = $magicMapper->searchAcrossMultipleTables(query: $query, registerSchemaPairs: $pairs); + // Redact write-only secrets before serialising (openregister#380, ocon#147): this + // direct-magic-mapper path bypasses renderEntity, so without this the read returns + // write-only fields in cleartext. redactWriteOnlyFromRows resolves each row's schema + // individually, so the cross-schema result set is handled correctly. + // + // `_rbac` is forwarded only to gate the property `authorization.read` strip. It does + // NOT gate the writeOnly strip (#460): `$query['_rbac']` is false for an ADMIN here, + // and an admin is not exempt from the writeOnly render boundary (#389). + $renderHandler = \OC::$server->get(\OCA\OpenRegister\Service\Object\RenderObject::class); + $renderHandler->redactWriteOnlyFromRows(rows: $results, _rbac: $query['_rbac'] ?? true); + // Serialize results. $serializedResults = []; foreach ($results as $entity) { @@ -793,14 +970,38 @@ private function crossTableSearch(array $registers, array $schemas, ObjectServic } // Calculate pagination. - $limit = $query['_limit'] ?? 20; - $offset = $query['_offset'] ?? 0; - $total = count($serializedResults); - $pages = 1; - $page = 1; + $limit = (int) ($query['_limit'] ?? 20); + $offset = (int) ($query['_offset'] ?? 0); + + // PERF-6: the per-table merge can return up to limit×tableCount rows. Slice + // down to the requested page window so the response honours _limit/_offset + // instead of returning every fetched row. + // NOTE: an exact cross-table total still requires a per-table COUNT(*) in SQL + // (MagicMapper, out of this controller's scope); $fetchedCount is the best + // available bound here. TODO(PERF-6): sum per-table COUNT(*) in MagicMapper. + $fetchedCount = count($serializedResults); + if ($limit > 0) { + $serializedResults = array_slice($serializedResults, $offset, $limit); + } else if ($offset > 0) { + $serializedResults = array_slice($serializedResults, $offset); + } + + // PERF-10: allow callers to skip the (here, in-PHP) total when not needed. + $wantTotal = filter_var($params['_count'] ?? true, FILTER_VALIDATE_BOOLEAN); + if ($wantTotal === true) { + $total = $fetchedCount; + } else { + $total = null; + } + + $pages = 1; + $page = 1; if ($limit > 0) { - $pages = (int) ceil($total / $limit); - $page = (int) floor($offset / $limit) + 1; + if ($total !== null) { + $pages = (int) ceil($total / $limit); + } + + $page = (int) floor($offset / $limit) + 1; } return new JSONResponse( @@ -853,29 +1054,14 @@ private function resolveRegisterSchemaIds(string $register, string $schema, Obje $resolvedRegisterId = $objectService->getRegister(); $resolvedSchemaId = $objectService->getSchema(); - // STEP 3: Fetch entities for magic mapper support. - $registerEntity = null; - $schemaEntity = null; - - try { - $registerMapper = \OC::$server->get(\OCA\OpenRegister\Db\RegisterMapper::class); - $registerEntity = $registerMapper->find(id: $resolvedRegisterId, _multitenancy: false); - } catch (\Exception $e) { - // Log but don't fail - entities are optional. - } - - try { - $schemaMapper = \OC::$server->get(\OCA\OpenRegister\Db\SchemaMapper::class); - $schemaEntity = $schemaMapper->find(id: $resolvedSchemaId, _multitenancy: false); - } catch (\Exception $e) { - // Log but don't fail - entities are optional. - } - + // STEP 3: Reuse the entities already resolved by setRegister()/setSchema() + // above — re-fetching them via the mappers would resolve the same + // register and schema twice per request. return [ 'register' => $resolvedRegisterId, 'schema' => $resolvedSchemaId, - 'registerEntity' => $registerEntity, - 'schemaEntity' => $schemaEntity, + 'registerEntity' => $objectService->getCurrentRegisterEntity(), + 'schemaEntity' => $objectService->getCurrentSchemaEntity(), ]; }//end resolveRegisterSchemaIds() @@ -918,7 +1104,7 @@ private function resolveRegisterSchemaIds(string $register, string $schema, Obje * @suppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multi-schema search + pagination + filtering requires branching * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-95 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function index(string $register, string $schema, ObjectService $objectService): JSONResponse { @@ -971,11 +1157,16 @@ public function index(string $register, string $schema, ObjectService $objectSer // Extract filtering parameters from request. $params = $this->request->getParams(); - $rbac = filter_var($params['rbac'] ?? true, FILTER_VALIDATE_BOOLEAN); - // Check both _multi and multi params (URL uses _multi, but we also support multi). - $multiExplicitlySet = isset($params['_multi']) || isset($params['multi']); - $multi = filter_var($params['_multi'] ?? $params['multi'] ?? true, FILTER_VALIDATE_BOOLEAN); - $deleted = filter_var($params['deleted'] ?? false, FILTER_VALIDATE_BOOLEAN); + // SEC-CTRL-1: RBAC and multitenancy posture MUST be derived from the + // caller's admin status, never from request parameters. This endpoint is + // reachable anonymously (@PublicPage); honouring ?rbac=false&_multi=false + // would let any caller list every object across all organisations/ACLs. + $isAdmin = $this->isCurrentUserAdmin(); + $rbac = ($isAdmin === false); + $multi = ($isAdmin === false); + // No longer request-controlled: never treat _multi as explicitly set by the client. + $multiExplicitlySet = false; + $deleted = filter_var($params['deleted'] ?? false, FILTER_VALIDATE_BOOLEAN); // Check if magic mapping is enabled for this register+schema. $registerEntity = $resolved['registerEntity'] ?? null; @@ -990,7 +1181,10 @@ public function index(string $register, string $schema, ObjectService $objectSer schemaSlug: $schemaEntity->getSlug() ); - if ($isMagicMapped === true) { + // A schema served from an external object-source (x-openregister-object-source) + // must never read the magic table — fall through to searchObjectsPaginated, + // which delegates to the registered provider (object-source-providers). + if ($isMagicMapped === true && $schemaEntity->getObjectSource() === null) { // Use MagicMapper for magic-mapped schemas. $magicMapper = \OC::$server->get(\OCA\OpenRegister\Db\MagicMapper::class); @@ -1046,12 +1240,24 @@ function (string $item): bool { _multitenancy: $multi ); } else { - // Convert ObjectEntity array to JSON-serializable format (no complex rendering). + // Convert ObjectEntity array to JSON-serializable format (no complex + // rendering). This fast path bypasses renderEntity — where write-only + // secrets are stripped (openregister#380, ocon#147) — so without the + // redaction call below it returns every write-only field in cleartext. + // This is the exact path the OpenConnector Source leak used: a plain + // list read (no _extend) hit this branch. Redact the raw entities with + // the same read-strip renderEntity applies, then serialize. + // `$rbac` is `($isAdmin === false)` and gates ONLY the property + // `authorization.read` strip — writeOnly strips unconditionally, admin + // included (#389/#460). + $renderHandler = \OC::$server->get(\OCA\OpenRegister\Service\Object\RenderObject::class); + $renderHandler->redactWriteOnlyFromRows(rows: $results, _rbac: $rbac); + $serializedResults = []; foreach ($results as $entity) { $serializedResults[] = $entity->jsonSerialize(); } - } + }//end if // Calculate pagination - need a separate count query since search applies limit/offset. $limit = (int) ($query['_limit'] ?? 20); @@ -1065,20 +1271,40 @@ function (string $item): bool { $offset = (int) ($offset ?? 0); } - // Build count query (same filters, no pagination). - $countQuery = $query; - unset($countQuery['_limit'], $countQuery['_offset'], $countQuery['_page']); + // PERF-10: allow callers to skip the extra COUNT(*) query when they + // don't need the grand total (e.g. infinite scroll). _count=false / + // _noTotal=true returns total:null and skips the count round-trip. + if (($params['_noTotal'] ?? false) === true) { + $countDefault = false; + } else { + $countDefault = true; + } - // Get actual total count. - $total = $magicMapper->countObjectsInRegisterSchemaTable( - query: $countQuery, - register: $registerEntity, - schema: $schemaEntity + $wantTotal = filter_var( + $params['_count'] ?? $countDefault, + FILTER_VALIDATE_BOOLEAN ); + $total = null; + if ($wantTotal === true) { + // Build count query (same filters, no pagination). + $countQuery = $query; + unset($countQuery['_limit'], $countQuery['_offset'], $countQuery['_page']); + + // Get actual total count. + $total = $magicMapper->countObjectsInRegisterSchemaTable( + query: $countQuery, + register: $registerEntity, + schema: $schemaEntity + ); + } + $pages = 1; if ($limit > 0) { - $pages = (int) ceil($total / $limit); + if ($total !== null) { + $pages = (int) ceil($total / $limit); + } + // Calculate page from offset if not explicitly provided. if ($page === null) { $page = (int) floor($offset / $limit) + 1; @@ -1093,9 +1319,9 @@ function (string $item): bool { $activeOrganisation = null; try { $organisationService = \OC::$server->get(\OCA\OpenRegister\Service\OrganisationService::class); - $activeOrg = $organisationService->getActiveOrganisation(); + $activeOrg = $organisationService?->getActiveOrganisation(); $activeOrganisation = $activeOrg?->getUuid(); - } catch (\Exception $e) { + } catch (\Throwable $e) { // Silently ignore if organisation service is not available. } @@ -1199,16 +1425,19 @@ function (string $item): bool { // path so geo filtering works for both register layouts. $responseData = $this->applyGeoQueryFilters(params: $params, result: $responseData); - // Return in expected format. - $response = new JSONResponse(data: $responseData); - - // Enable gzip compression for large payloads. - if (count($responseData['results'] ?? []) > 10) { - $response->addHeader('Content-Encoding', 'gzip'); - $response->addHeader('Vary', 'Accept-Encoding'); + // Content negotiation: JSON-LD @graph for magic-mapped results + // (json-ld-output). + if ($this->wantsJsonLd() === true && $registerEntity !== null && $schemaEntity !== null) { + return $this->jsonLdCollectionResponse( + result: $responseData, + register: $registerEntity, + schema: $schemaEntity + ); } - return $response; + // Return in expected format. Response compression is negotiated + // at the webserver level; the controller never sets encoding headers. + return new JSONResponse(data: $responseData); }//end if }//end if @@ -1220,12 +1449,20 @@ function (string $item): bool { ); // **INTELLIGENT SOURCE SELECTION**: ObjectService automatically chooses optimal source. - $result = $objectService->searchObjectsPaginated( - query: $query, - _rbac: $rbac, - _multitenancy: $multi, - deleted: $deleted - ); + try { + $result = $objectService->searchObjectsPaginated( + query: $query, + _rbac: $rbac, + _multitenancy: $multi, + deleted: $deleted + ); + } catch (NotAuthorizedException $exception) { + // RBAC denied the schema-level list read (raised by the + // object-source parity check before the provider is consulted). + // Mirror show(): 404, not 403/500, so denial reveals nothing + // about the schema's contents. + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } // Strip empty values from results unless _empty=true is set. $includeEmpty = filter_var( @@ -1258,18 +1495,258 @@ function (string $item): bool { // PostGIS push-down is tracked in `geo-spatial-queries`. $result = $this->applyGeoQueryFilters(params: $params, result: $result); - // **SUB-SECOND OPTIMIZATION**: Enable response compression for large payloads. - $response = new JSONResponse(data: $result); - - // Enable gzip compression for responses > 1KB. - if (($result['results'] ?? null) !== null && (count($result['results']) > 10) === true) { - $response->addHeader('Content-Encoding', 'gzip'); - $response->addHeader('Vary', 'Accept-Encoding'); + // Content negotiation: emit a JSON-LD @graph when requested. Wraps the + // already-rendered/RBAC-filtered result — no second data path + // (json-ld-output). + if ($this->wantsJsonLd() === true + && ($resolved['registerEntity'] ?? null) !== null + && ($resolved['schemaEntity'] ?? null) !== null + ) { + return $this->jsonLdCollectionResponse( + result: $result, + register: $resolved['registerEntity'], + schema: $resolved['schemaEntity'] + ); } - return $response; + // Response compression is negotiated at the webserver level; the + // controller never sets encoding headers. + return new JSONResponse(data: $result); }//end index() + /** + * Batched object-count endpoint — POST /api/objects/counts. + * + * Accepts a JSON body of the shape + * `{ "counts": [ { "register": , "schema": , "filter": } ] }` + * and returns `{ "results": [ { "register": ..., "schema": ..., "count": } ] }` + * with exactly one result per input entry, in the same order as the + * request array. Identical `(register, schema, filter)` triples are + * deduped server-side so the aggregate runs once per distinct triple; + * every input entry still receives a result (duplicates share the count). + * An empty or missing `counts` array returns `{ results: [] }`. + * + * SECURITY — authorization parity with collection reads: each count is + * produced through the SAME RBAC + multitenancy scoping the collection + * list read (`index()` / `GET /api/objects/{register}/{schema}?_limit=1`) + * applies. The RBAC/multitenancy posture is derived from the caller's + * admin status (mirrors `index()` lines 1160-1162), never from request + * parameters, and threaded into the identical count paths `index()` uses + * (see `countPairScoped()`). A caller therefore cannot obtain a count for + * objects they are not permitted to list. A pair that cannot be resolved + * (unknown or withheld) yields `count: null` without disclosing whether it + * does not exist or is access-restricted. + * + * The route carries `@NoAdminRequired` and is deliberately NOT a public + * page: any authenticated user may call it; an unauthenticated request is + * rejected by the security middleware exactly like a non-public read. + * + * @param ObjectService $objectService The object service (DI). + * + * @return JSONResponse `{ results: [ { register, schema, count } ] }` + * + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @psalm-return JSONResponse<200|400, array, array> + * + * @spec openspec/specs/aggregation-api/spec.md + */ + public function counts(ObjectService $objectService): JSONResponse + { + $params = $this->request->getParams(); + $entries = ($params['counts'] ?? null); + + // Missing counts key → empty success (spec: empty or missing → { results: [] }). + if ($entries === null) { + return new JSONResponse(data: ['results' => []]); + } + + // A present-but-non-array counts value is a malformed request. + if (is_array($entries) === false) { + return new JSONResponse( + data: ['error' => 'counts must be an array of { register, schema, filter? } entries'], + statusCode: 400 + ); + } + + // Empty array → empty success. + if ($entries === []) { + return new JSONResponse(data: ['results' => []]); + } + + // Derive the RBAC + multitenancy posture from the caller's admin + // status — identical to the collection read (index() lines 1160-1162). + // Never honour request-supplied rbac/multi flags on a data endpoint. + $isAdmin = $this->isCurrentUserAdmin(); + $rbac = ($isAdmin === false); + $multi = ($isAdmin === false); + + // Validate every entry up-front so a malformed batch is rejected + // wholesale rather than silently skipping entries. + foreach ($entries as $index => $entry) { + if (is_array($entry) === false + || isset($entry['register']) === false + || isset($entry['schema']) === false + || is_scalar($entry['register']) === false + || is_scalar($entry['schema']) === false + || (isset($entry['filter']) === true && is_array($entry['filter']) === false) + ) { + return new JSONResponse( + data: [ + 'error' => 'Malformed counts entry at index '.$index + .': register and schema are required scalars and filter must be an object', + ], + statusCode: 400 + ); + } + } + + // Dedupe identical (register, schema, filter) triples: run one + // aggregate per distinct triple, but return one result per input + // entry in request order (duplicates share the deduped count). + $cache = []; + $results = []; + foreach ($entries as $entry) { + $register = (string) $entry['register']; + $schema = (string) $entry['schema']; + $filter = ($entry['filter'] ?? []); + + $cacheKey = $register.'|'.$schema.'|'.json_encode($filter); + if (array_key_exists($cacheKey, $cache) === false) { + $cache[$cacheKey] = $this->countPairScoped( + register: $register, + schema: $schema, + filter: $filter, + rbac: $rbac, + multi: $multi, + objectService: $objectService + ); + } + + $results[] = [ + 'register' => $entry['register'], + 'schema' => $entry['schema'], + 'count' => $cache[$cacheKey], + ]; + }//end foreach + + return new JSONResponse(data: ['results' => $results]); + }//end counts() + + /** + * Produce a single (register, schema, filter) object count with the exact + * RBAC + multitenancy scoping the collection read applies. + * + * This mirrors `index()`'s count logic so a batched count can never leak a + * total the equivalent list read would not surface. It resolves the pair, + * then routes through the same two count paths `index()` uses: + * - magic-mapped schema → `MagicMapper::countObjectsInRegisterSchemaTable()` + * with `_rbac` / `_multitenancy` threaded into the query + * (mirrors index() lines 1194-1198 + 1279-1283); + * - database-backed schema → `ObjectService::searchObjectsPaginated()` + * total, produced by the same RBAC/multitenancy-scoped query + * (mirrors index() lines 1443-1448), read at `?_limit=1` like the + * reference collection read. + * + * Returns null when the pair cannot be resolved, so a restricted or + * unknown pair is withheld without disclosing which. + * + * @param string $register Register id or slug. + * @param string $schema Schema id or slug. + * @param array $filter Object-property filters for this entry. + * @param bool $rbac Whether to apply RBAC (parity with the list read). + * @param bool $multi Whether to apply multitenancy (parity with the list read). + * @param ObjectService $objectService The object service (DI). + * + * @return int|null The scoped count, or null when the pair is withheld. + * + * @spec openspec/specs/aggregation-api/spec.md + */ + private function countPairScoped( + string $register, + string $schema, + array $filter, + bool $rbac, + bool $multi, + ObjectService $objectService + ): ?int { + try { + // Resolve slugs/ids to numeric ids + entities (same as index()). + $resolved = $this->resolveRegisterSchemaIds( + register: $register, + schema: $schema, + objectService: $objectService + ); + } catch (RegisterNotFoundException | SchemaNotFoundException $e) { + // Withhold: never disclose whether the pair is missing or restricted. + return null; + } + + $registerEntity = ($resolved['registerEntity'] ?? null); + $schemaEntity = ($resolved['schemaEntity'] ?? null); + + // Build the search query from the per-entry filter using the same + // builder index() uses, scoped to the resolved numeric ids. + $query = $objectService->buildSearchQuery( + requestParams: $filter, + register: $resolved['register'], + schema: $resolved['schema'] + ); + + // Magic-mapped parity: count via the magic table with RBAC / + // multitenancy threaded into the query (index() lines 1194-1198, 1279-1283). + if ($registerEntity !== null && $schemaEntity !== null) { + $isMagicMapped = $registerEntity->isMagicMappingEnabledForSchema( + schemaId: $schemaEntity->getId(), + schemaSlug: $schemaEntity->getSlug() + ); + + if ($isMagicMapped === true && $schemaEntity->getObjectSource() === null) { + $magicMapper = \OC::$server->get(\OCA\OpenRegister\Db\MagicMapper::class); + + $countQuery = $query; + unset($countQuery['_limit'], $countQuery['_offset'], $countQuery['_page']); + $countQuery['_rbac'] = $rbac; + $countQuery['_multitenancy'] = $multi; + $countQuery['_multitenancy_explicit'] = false; + + return (int) $magicMapper->countObjectsInRegisterSchemaTable( + query: $countQuery, + register: $registerEntity, + schema: $schemaEntity + ); + } + }//end if + + // Database-backed parity: read the paginated total at _limit=1, which + // is produced by the same RBAC/multitenancy-scoped query the + // collection read runs (index() lines 1443-1448). + $query['_limit'] = 1; + unset($query['_offset'], $query['_page']); + + try { + $result = $objectService->searchObjectsPaginated( + query: $query, + _rbac: $rbac, + _multitenancy: $multi, + deleted: false + ); + } catch (NotAuthorizedException $exception) { + // Schema-level read denied (object-source parity check): + // the count is simply unavailable to this caller. + return null; + } + + $total = ($result['total'] ?? null); + if ($total === null) { + return null; + } + + return (int) $total; + }//end countPairScoped() + /** * Geo-search endpoint — POST /api/objects/{register}/{schema}/geo-search. * @@ -1290,47 +1767,266 @@ function (string $item): bool { * @param string $schema Schema slug or id. * @param ObjectService $objectService Object service via DI. * - * @return JSONResponse + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + */ + public function geoSearch(string $register, string $schema, ObjectService $objectService): JSONResponse + { + if ($this->geoFilterParser === null || $this->geoFilterApplier === null) { + return new JSONResponse( + data: ['error' => 'Geo filtering primitives not configured'], + statusCode: 501 + ); + } + + $body = $this->request->getParams(); + try { + $filters = $this->geoFilterParser->fromGeoSearchBody(body: $body); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 422); + } + + // Reuse the standard listing path, then post-filter. + $listing = $this->index(register: $register, schema: $schema, objectService: $objectService); + $payload = (array) $listing->getData(); + $rows = ($payload['results'] ?? []); + if (is_array($rows) === false) { + return $listing; + } + + $filtered = $this->geoFilterApplier->applyAll(rows: $rows, filters: $filters); + $payload['results'] = $filtered; + if (isset($payload['total']) === true) { + $payload['total'] = count($filtered); + } + + return new JSONResponse(data: $payload); + + }//end geoSearch() + + /** + * Export a register/schema's objects as a GeoJSON FeatureCollection. + * + * Reuses the standard listing path (`index()`), so the result is + * already scoped to the objects the caller may read — this endpoint + * adds no new data access and therefore no IDOR surface (the listing + * applies per-object RBAC). Objects without a geometry are omitted. + * + * Supports `?_fields=title,status` to restrict Feature properties + * (geometry is always retained), `?geo.property=` to pick the geo + * property, and the standard geo filter params (REQ-GEO-008). + * + * @param string $register Register slug or id. + * @param string $schema Schema slug or id. + * @param ObjectService $objectService Object service via DI. + * + * @return JSONResponse A GeoJSON FeatureCollection (application/geo+json). + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @no-admin-idor-exempt Read access is enforced by the delegated index() + * listing (per-object RBAC via scopedGeoRows()); this method only + * reshapes the already-scoped rows and accesses no object by id. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + public function geoJson(string $register, string $schema, ObjectService $objectService): JSONResponse + { + if ($this->geoFeatureBuilder === null) { + return new JSONResponse(data: ['error' => 'Geo feature builder not configured'], statusCode: 501); + } + + $params = $this->request->getParams(); + $rows = $this->scopedGeoRows(register: $register, schema: $schema, objectService: $objectService); + $fields = $this->parseFieldsParam(params: $params); + $geoProp = ($params['geo.property'] ?? null); + + if ($geoProp !== null) { + $geoPropertyValue = (string) $geoProp; + } else { + $geoPropertyValue = null; + } + + $collection = $this->geoFeatureBuilder->buildFeatureCollection( + rows: $rows, + geoProperty: $geoPropertyValue, + fields: $fields, + includeArea: true + ); + + // Body is a valid GeoJSON FeatureCollection. The default + // application/json content type is kept (NC's Content-Type + // override requires the full container); the @self envelope is + // omitted so the body is consumable directly by GIS clients. + return new JSONResponse(data: $collection); + + }//end geoJson() + + /** + * WFS GetFeature-compatible endpoint (REQ-GEO-008). + * + * Returns a GeoJSON FeatureCollection compatible with WFS 2.0 + * `outputFormat=application/json`. Supports `count`/`maxFeatures` + * caps. Reuses the RBAC-scoped listing path (no new IDOR surface). + * + * @param string $register Register slug or id. + * @param string $schema Schema slug or id. + * @param ObjectService $objectService Object service via DI. + * + * @return JSONResponse A WFS-compatible GeoJSON FeatureCollection. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @no-admin-idor-exempt Read access is enforced by the delegated index() + * listing (per-object RBAC via scopedGeoRows()); this method only + * reshapes the already-scoped rows and accesses no object by id. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + public function wfs(string $register, string $schema, ObjectService $objectService): JSONResponse + { + if ($this->geoFeatureBuilder === null) { + return new JSONResponse(data: ['error' => 'Geo feature builder not configured'], statusCode: 501); + } + + $params = $this->request->getParams(); + $rows = $this->scopedGeoRows(register: $register, schema: $schema, objectService: $objectService); + + $maxFeatures = null; + $rawMax = ($params['count'] ?? ($params['maxFeatures'] ?? null)); + if ($rawMax !== null && is_numeric($rawMax) === true) { + $maxFeatures = (int) $rawMax; + } + + $geoProp = ($params['geo.property'] ?? null); + + if ($geoProp !== null) { + $geoPropertyValue = (string) $geoProp; + } else { + $geoPropertyValue = null; + } + + $response = $this->geoFeatureBuilder->buildWfsResponse( + rows: $rows, + geoProperty: $geoPropertyValue, + maxFeatures: $maxFeatures + ); + + return new JSONResponse(data: $response); + + }//end wfs() + + /** + * Forward / reverse geocoding via PDOK Locatieserver (REQ-GEO-005). + * + * `?q=
` performs forward geocoding; `?lon=&lat=` performs + * reverse geocoding. Degrades gracefully: when OpenConnector / PDOK + * is unavailable an empty suggestion list is returned with a flag, + * never an error (geocoding is non-blocking, REQ-GEO-005). + * + * @return JSONResponse Geocoding suggestions and availability flag. * * @NoAdminRequired * @NoCSRFRequired - * @PublicPage * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @no-admin-idor-exempt Stateless PDOK address-lookup proxy: accesses no + * OpenRegister object and takes no object id, so there is no IDOR + * surface. Authenticated-user access is enforced by NoAdminRequired. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 */ - public function geoSearch(string $register, string $schema, ObjectService $objectService): JSONResponse + public function geocode(): JSONResponse { - if ($this->geoFilterParser === null || $this->geoFilterApplier === null) { - return new JSONResponse( - data: ['error' => 'Geo filtering primitives not configured'], - statusCode: 501 + if ($this->pdokGeocoder === null) { + return new JSONResponse(data: ['available' => false, 'suggestions' => []]); + } + + $params = $this->request->getParams(); + $available = $this->pdokGeocoder->isAvailable(); + + $query = ($params['q'] ?? null); + if ($query !== null && trim((string) $query) !== '') { + $bagOnly = filter_var(($params['bagOnly'] ?? false), FILTER_VALIDATE_BOOLEAN); + $suggestions = $this->pdokGeocoder->geocodeFree( + query: (string) $query, + maxItems: 5, + bagOnly: $bagOnly ); + return new JSONResponse(data: ['available' => $available, 'suggestions' => $suggestions]); } - $body = $this->request->getParams(); - try { - $filters = $this->geoFilterParser->fromGeoSearchBody(body: $body); - } catch (\InvalidArgumentException $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 422); + $lon = ($params['lon'] ?? null); + $lat = ($params['lat'] ?? null); + if (is_numeric($lon) === true && is_numeric($lat) === true) { + $address = $this->pdokGeocoder->reverseGeocode(longitude: (float) $lon, latitude: (float) $lat); + return new JSONResponse(data: ['available' => $available, 'address' => $address]); } - // Reuse the standard listing path, then post-filter. + return new JSONResponse( + data: ['error' => 'geocode requires either ?q=
or ?lon=&lat='], + statusCode: 422 + ); + + }//end geocode() + + /** + * Fetch the RBAC-scoped listing rows for a register/schema. + * + * Delegates to the standard `index()` listing — which enforces + * per-object read access — then returns just the `results`. Geo + * endpoints build on this so they can never widen access. + * + * @param string $register Register slug or id. + * @param string $schema Schema slug or id. + * @param ObjectService $objectService Object service via DI. + * + * @return array The RBAC-scoped result rows. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + private function scopedGeoRows(string $register, string $schema, ObjectService $objectService): array + { $listing = $this->index(register: $register, schema: $schema, objectService: $objectService); $payload = (array) $listing->getData(); $rows = ($payload['results'] ?? []); - if (is_array($rows) === false) { - return $listing; + if (is_array($rows) === true) { + return $rows; } - $filtered = $this->geoFilterApplier->applyAll(rows: $rows, filters: $filters); - $payload['results'] = $filtered; - if (isset($payload['total']) === true) { - $payload['total'] = count($filtered); + return []; + + }//end scopedGeoRows() + + /** + * Parse a `?_fields=a,b,c` allow-list into an array (null = all). + * + * @param array $params The request params. + * + * @return string[]|null + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + private function parseFieldsParam(array $params): ?array + { + $raw = ($params['_fields'] ?? null); + if ($raw === null || $raw === '') { + return null; } - return new JSONResponse(data: $payload); + if (is_array($raw) === true) { + return array_values(array_map('strval', $raw)); + } - }//end geoSearch() + return array_values(array_filter(array_map('trim', explode(',', (string) $raw)), fn($v) => $v !== '')); + + }//end parseFieldsParam() /** * Apply geo query-param filters to a listing result. @@ -1349,7 +2045,7 @@ public function geoSearch(string $register, string $schema, ObjectService $objec * * @return array The result, possibly with `results` filtered down. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ private function applyGeoQueryFilters(array $params, array $result): array { @@ -1414,7 +2110,7 @@ private function applyGeoQueryFilters(array $params, array $result): array * * @return array The same params with `geo.*` keys hoisted from `geo`. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ private function flattenGeoParams(array $params): array { @@ -1474,7 +2170,7 @@ private function flattenGeoParams(array $params): array * @suppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) Cross-table search + multi-schema routing requires branching * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-1 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function objects(ObjectService $objectService): JSONResponse { @@ -1510,7 +2206,14 @@ public function objects(ObjectService $objectService): JSONResponse ); } - // Single register+schema: check if magic mapping is enabled. + // Single register+schema: resolve slugs/IDs to numeric IDs (same + // resolution semantics as the path-style index() route) and check + // whether magic mapping is enabled. The resolved numeric IDs are + // captured in $resolvedRegisterId/$resolvedSchemaId so the fallback + // buildSearchQuery() call below can filter correctly instead of + // silently matching zero rows against an unresolved slug string. + $resolvedRegisterId = null; + $resolvedSchemaId = null; if ($registerParam !== null && $schemaParam !== null) { try { $resolved = $this->resolveRegisterSchemaIds( @@ -1519,6 +2222,9 @@ public function objects(ObjectService $objectService): JSONResponse objectService: $objectService ); + $resolvedRegisterId = $resolved['register']; + $resolvedSchemaId = $resolved['schema']; + // Check if magic mapping is enabled for this register+schema. $registerEntity = $resolved['registerEntity'] ?? null; $schemaEntity = $resolved['schemaEntity'] ?? null; @@ -1553,6 +2259,13 @@ public function objects(ObjectService $objectService): JSONResponse schema: $schemaEntity ); + // Redact write-only secrets before serialising (openregister#380, + // ocon#147) — this direct-magic-mapper path bypasses renderEntity. + // `_rbac` (false for an admin) gates only the property + // `authorization.read` strip; writeOnly strips unconditionally (#460). + $renderHandler = \OC::$server->get(\OCA\OpenRegister\Service\Object\RenderObject::class); + $renderHandler->redactWriteOnlyFromRows(rows: $results, _rbac: $query['_rbac'] ?? true); + // Convert ObjectEntity array to JSON-serializable format. $serializedResults = []; foreach ($results as $entity) { @@ -1605,10 +2318,27 @@ public function objects(ObjectService $objectService): JSONResponse }//end if // Build search query and execute via normal route (magic tables or SOLR). - $query = $objectService->buildSearchQuery($this->request->getParams()); + // Pass the already-resolved numeric register/schema IDs (when a + // register/schema query parameter was supplied) so the query filters + // on '@self.register' / '@self.schema' the same way the path-style + // index() route does. Without this, buildSearchQuery() falls back to + // treating the raw 'register'/'schema' query-string values (slugs) + // as the metadata filter, which never matches the numeric register + // ID column and silently returns zero results. + $query = $objectService->buildSearchQuery( + requestParams: $this->request->getParams(), + register: $resolvedRegisterId, + schema: $resolvedSchemaId + ); // **INTELLIGENT SOURCE SELECTION**: ObjectService automatically chooses optimal source. - $result = $objectService->searchObjectsPaginated($query); + try { + $result = $objectService->searchObjectsPaginated($query); + } catch (NotAuthorizedException $exception) { + // Schema-level list read denied (object-source parity check). + // Mirror show(): 404 so denial reveals nothing. + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } // Strip empty values from results unless _empty=true is set. $includeEmpty = filter_var( @@ -1660,7 +2390,7 @@ public function objects(ObjectService $objectService): JSONResponse * @suppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) Object retrieval with slug resolution + access checks requires branching * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-2 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function show( string $id, @@ -1709,7 +2439,12 @@ public function show( // If admin, disable RBAC. $multi = $isAdmin === false; // If admin, disable multitenancy. - // Find and validate the object. + // Find and validate the object. Rendering is deferred to the single + // renderEntity() call below (`_render: false`): find() previously + // rendered the entity with the same $extend and the controller then + // rendered it AGAIN, repeating file hydration, writeOnly redaction and + // the expensive inverse-property resolution on every single read. + // Permission checks and read logging inside find() still run. try { $objectEntity = $this->objectService->find( id: $id, @@ -1718,7 +2453,8 @@ public function show( register: $register, schema: $schema, _rbac: $rbac, - _multitenancy: $multi + _multitenancy: $multi, + _render: false ); if ($objectEntity === null) { $errorMsg = "Object with id {$id} not found"; @@ -1726,6 +2462,9 @@ public function show( } // Render the object with requested extensions, filters, fields, and unset parameters. + // This is the ONLY render on the show() response path: writeOnly + // redaction and read-authorization stripping (openregister#385/#386) + // are applied here, exactly once, inside renderEntity(). $renderedObject = $this->objectService->renderEntity( entity: $objectEntity, _extend: $extend, @@ -1801,9 +2540,30 @@ public function show( $renderedData = $this->stripEmptyValues(data: $renderedData); } + // Content negotiation: emit JSON-LD when requested. The serializer + // wraps the already-rendered array — no second data path — so all + // access control above remains applied (json-ld-output). + if ($this->wantsJsonLd() === true + && $resolved['registerEntity'] !== null + && $resolved['schemaEntity'] !== null + ) { + return $this->jsonLdObjectResponse( + renderedObject: $renderedData, + register: $resolved['registerEntity'], + schema: $resolved['schemaEntity'] + ); + } + return new JSONResponse(data: $renderedData); } catch (DoesNotExistException $exception) { return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } catch (NotAuthorizedException $exception) { + // RBAC denied the read. Return 404 (not 500, and not 403) so an + // unauthorized caller cannot distinguish "exists but forbidden" + // from "does not exist" — avoids leaking object existence while + // still denying access. Previously this exception escaped the + // handler and surfaced as an HTTP 500. + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); }//end try }//end show() @@ -1825,36 +2585,21 @@ public function show( * * @PublicPage * - * @psalm-return JSONResponse<201|401|403|404, - * array{'@self'?: array{name: mixed|null|string,...}|mixed, - * message?: mixed|string, error?: mixed|string,...}, - * array>|JSONResponse<400, string, array> + * @psalm-return JSONResponse * * @psalm-suppress TypeDoesNotContainType * @psalm-suppress NoValue * * @suppressWarnings(PHPMD.NPathComplexity) Object creation requires many validation and processing steps * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-62 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ + #[AnonRateLimit(limit: 30, period: 60)] public function create( string $register, string $schema, ObjectService $objectService ): JSONResponse { - // Wave-3 C13: short-circuit anonymous writes BEFORE the webhook - // intercept fires. The endpoint is @PublicPage so unauthenticated - // reads can pass through the access guard, but writes from an - // anonymous caller must never trigger pre-event webhooks - // (which can cause side effects in receiving systems — - // file ingest, n8n flows, audit logs — before any RBAC check runs). - if ($this->userSession->getUser() === null) { - return new JSONResponse( - data: ['error' => 'Authentication required to create objects'], - statusCode: 401 - ); - } - try { // Resolve slugs to numeric IDs consistently. $resolved = $this->resolveRegisterSchemaIds(register: $register, schema: $schema, objectService: $objectService); @@ -1904,6 +2649,10 @@ public function create( // Normalize multipart/form-data: decode JSON-encoded strings back into arrays/objects. $object = $this->normalizeFormDataValues(data: $object); + // Defense-in-depth (wave-11 WF2): strip server-managed @self fields so they + // cannot be injected via the single-object create path. + $object = $this->sanitiseSelfMetadata(data: $object); + // Extract uploaded files from multipart/form-data using Request object. $uploadedFiles = $this->extractAllUploadedFiles(); @@ -1951,8 +2700,18 @@ public function create( ], statusCode: 422 ); + } catch (FolderAccessDeniedException $exception) { + // MUST be caught before generic \Exception to avoid being absorbed as a 403 with + // a non-structured body. See the `self-folder-access-control` capability spec. + return $this->folderAccessDeniedResponse(exception: $exception); } catch (\Exception $exception) { // Handle all other exceptions (including RBAC permission errors). + // Sanitized external-write failures carry their own 4xx status + // (dbal-virtual-registers-crud) — never flatten them to 403. + if ($exception instanceof \OCA\OpenRegister\Service\ObjectSource\DbalWriteException === true) { + return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: $exception->getStatusCode()); + } + return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: 403); }//end try @@ -1978,6 +2737,8 @@ public function create( * * @NoCSRFRequired * + * @PublicPage + * * @psalm-suppress TypeDoesNotContainType * @psalm-suppress NoValue * @@ -1985,7 +2746,7 @@ public function create( * @suppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CyclomaticComplexity) Object update requires many validation and processing steps * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-62 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function update( string $register, @@ -2018,6 +2779,9 @@ public function update( // Normalize multipart/form-data: decode JSON-encoded strings back into arrays/objects. $object = $this->normalizeFormDataValues(data: $object); + // Defense-in-depth (wave-11 WF2): strip server-managed @self fields. + $object = $this->sanitiseSelfMetadata(data: $object); + // Extract uploaded files from multipart/form-data using Request object. $uploadedFiles = $this->extractAllUploadedFiles(); @@ -2082,7 +2846,8 @@ public function update( 'trace' => $exception->getTraceAsString(), ] ); - return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: 500); + // SEC-CTRL-7: do not leak internal exception detail on 500. + return $this->errorResponse(e: $exception); } catch (NotFoundExceptionInterface | ContainerExceptionInterface $e) { // If there's an issue getting the user ID, continue without the lock check. }//end try @@ -2109,8 +2874,12 @@ public function update( // Unlock the object after saving. try { $this->objectService->unlockObject($objectEntity->getUuid()); - } catch (Exception $e) { + } catch (\Exception $e) { // Ignore unlock errors since the update was successful. + // NOTE: must be the global \Exception — the unqualified `Exception` + // resolves to OCP\DB\Exception here (see `use` block) and would NOT + // catch the \Exception thrown by LockHandler::unlock(), which then + // surfaced as a spurious 403. See openregister#195. } // Return the successfully saved object directly. @@ -2126,8 +2895,17 @@ public function update( data: ['error' => $exception->getMessage(), 'errors' => $exception->getErrors()], statusCode: 422 ); + } catch (FolderAccessDeniedException $exception) { + // MUST be caught before generic \Exception. See `self-folder-access-control` spec. + return $this->folderAccessDeniedResponse(exception: $exception); } catch (\Exception $exception) { // Handle all other exceptions (including RBAC permission errors). + // Sanitized external-write failures carry their own 4xx status + // (dbal-virtual-registers-crud) — never flatten them to 403. + if ($exception instanceof \OCA\OpenRegister\Service\ObjectSource\DbalWriteException === true) { + return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: $exception->getStatusCode()); + } + return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: 403); }//end try }//end update() @@ -2150,10 +2928,12 @@ public function update( * * @NoCSRFRequired * + * @PublicPage + * * @suppressWarnings(PHPMD.ExcessiveMethodLength) * @suppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-3 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function patch( string $register, @@ -2183,13 +2963,16 @@ public function patch( // Normalize multipart/form-data: decode JSON-encoded strings back into arrays/objects. $patchData = $this->normalizeFormDataValues(data: $patchData); + // Defense-in-depth (wave-11 WF2): strip server-managed @self fields. + $patchData = $this->sanitiseSelfMetadata(data: $patchData); + // Determine RBAC and multitenancy settings based on admin status. $isAdmin = $this->isCurrentUserAdmin(); $rbac = $isAdmin === false; $multi = $isAdmin === false; // Log RBAC/multitenancy settings for debugging. - $this->logger->info( + $this->logger->debug( message: '[ObjectsController] PATCH: RBAC/Multitenancy settings', context: [ 'file' => __FILE__, @@ -2239,6 +3022,29 @@ public function patch( return new JSONResponse(data: ['error' => 'Object not found'], statusCode: 404); }//end try + // Optimistic concurrency (fix-object-patch-lost-update): PATCH is a + // read-merge-write, so two concurrent PATCHes can silently clobber each + // other's untouched fields. A caller that read the object may pass the + // `updated` value it saw as `_expectedUpdated` (If-Match semantics); if + // the stored object changed since, the caller is working from stale data + // and the write is rejected with 409 instead of overwriting the newer + // version. Opt-in: callers that omit `_expectedUpdated` behave as before. + // Read from the raw request: the patchData filter strips `_`-prefixed keys. + $expectedUpdated = $this->request->getParam('_expectedUpdated'); + if ($expectedUpdated !== null) { + $currentUpdated = $existingObject->getUpdated()?->format(\DateTimeInterface::ATOM); + if ((string) $currentUpdated !== (string) $expectedUpdated) { + return new JSONResponse( + data: [ + 'error' => 'Conflict: the object was modified since it was read. Re-read and retry.', + 'expectedUpdated' => (string) $expectedUpdated, + 'currentUpdated' => (string) $currentUpdated, + ], + statusCode: 409 + ); + } + } + // Get the existing object data and merge with patch data. $existingData = $existingObject->getObject(); $mergedData = array_merge($existingData ?? [], $patchData); @@ -2252,7 +3058,7 @@ public function patch( uuid: $id ); - $this->logger->info( + $this->logger->debug( message: '[ObjectsController] PATCH: saveObject succeeded', context: [ 'file' => __FILE__, @@ -2277,7 +3083,7 @@ public function patch( ); } - $this->logger->info( + $this->logger->debug( message: '[ObjectsController] PATCH: Starting to prepare response', context: ['file' => __FILE__, 'line' => __LINE__] ); @@ -2304,6 +3110,9 @@ public function patch( data: ['error' => $exception->getMessage(), 'errors' => $exception->getErrors()], statusCode: 422 ); + } catch (FolderAccessDeniedException $exception) { + // MUST be caught before generic \Exception. See `self-folder-access-control` spec. + return $this->folderAccessDeniedResponse(exception: $exception); } catch (\Exception $exception) { // Handle all other exceptions (including RBAC permission errors). $this->logger->error( @@ -2315,7 +3124,8 @@ public function patch( 'trace' => $exception->getTraceAsString(), ] ); - return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: 500); + // SEC-CTRL-7: do not leak internal exception detail on 500. + return $this->errorResponse(e: $exception); }//end try }//end patch() @@ -2333,13 +3143,13 @@ public function patch( * * @return JSONResponse A JSON response containing the updated object * - * @PublicPage - * * @NoAdminRequired * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-13 + * @PublicPage + * + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function postPatch( string $register, @@ -2347,18 +3157,6 @@ public function postPatch( string $id, ObjectService $objectService ): JSONResponse { - // Wave-3 C13: short-circuit anonymous writes early. postPatch is - // @PublicPage to let multipart file-upload PATCH-semantics work for - // logged-in users, but it must never accept writes from an - // unauthenticated caller — consistent with create() above and with - // OR's read-public / write-authenticated split. - if ($this->userSession->getUser() === null) { - return new JSONResponse( - data: ['error' => 'Authentication required to update objects'], - statusCode: 401 - ); - } - try { $resolved = $this->resolveRegisterSchemaIds(register: $register, schema: $schema, objectService: $objectService); } catch (RegisterNotFoundException | SchemaNotFoundException $e) { @@ -2378,6 +3176,9 @@ public function postPatch( // Normalize multipart/form-data: decode JSON-encoded strings back into arrays/objects. $patchData = $this->normalizeFormDataValues(data: $patchData); + // Defense-in-depth (wave-11 WF2): strip server-managed @self fields. + $patchData = $this->sanitiseSelfMetadata(data: $patchData); + // Extract uploaded files — works because this is a POST request. $uploadedFiles = $this->extractAllUploadedFiles(); if (empty($uploadedFiles) === false) { @@ -2441,8 +3242,15 @@ public function postPatch( data: ['error' => $exception->getMessage(), 'errors' => $exception->getErrors()], statusCode: 422 ); + } catch (FolderAccessDeniedException $exception) { + // MUST be caught before generic \Exception so a @self.folder + // denial on the post-patch path returns 403 with the structured + // body (no folder-id oracle) — same contract as create/update/patch. + // See the `self-folder-access-control` spec. + return $this->folderAccessDeniedResponse(exception: $exception); } catch (\Exception $exception) { - return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: 500); + // SEC-CTRL-7: do not leak internal exception detail on 500. + return $this->errorResponse(e: $exception); }//end try }//end postPatch() @@ -2462,8 +3270,9 @@ public function postPatch( * * @NoAdminRequired * @NoCSRFRequired + * @PublicPage * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-30 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function destroy(string $id, string $register, string $schema, ObjectService $objectService): JSONResponse { @@ -2502,9 +3311,40 @@ public function destroy(string $id, string $register, string $schema, ObjectServ data: ['error' => $exception->getMessage(), 'errors' => $exception->getErrors()], statusCode: 422 ); + } catch (DoesNotExistException $exception) { + // Absent objects (native or external) are a uniform 404. + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); } catch (\Exception $exception) { // Handle all exceptions (including RBAC permission errors and object not found). + // Sanitized external-write failures carry their own 4xx status + // (dbal-virtual-registers-crud) — never flatten them to 403. + if ($exception instanceof \OCA\OpenRegister\Service\ObjectSource\DbalWriteException === true) { + return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: $exception->getStatusCode()); + } + return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: 403); + } catch (\Throwable $throwable) { + // Safety net for fatal errors (\Error/\TypeError) that do NOT extend + // \Exception and would otherwise escape as an HTML 500 fatal-error page + // to API clients. Log the full trace and return a clean JSON 500 so the + // contract is always machine-readable. \Throwable MUST be caught last so + // the specific catches above keep their dedicated status codes. + $this->logger?->error( + message: '[ObjectsController] Unexpected fatal error while deleting object', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'register' => $register, + 'schema' => $schema, + 'id' => $id, + 'exception' => $throwable->getMessage(), + 'trace' => $throwable->getTraceAsString(), + ] + ); + return new JSONResponse( + data: ['error' => 'An unexpected error occurred while deleting the object'], + statusCode: Http::STATUS_INTERNAL_SERVER_ERROR + ); }//end try }//end destroy() @@ -2523,7 +3363,7 @@ public function destroy(string $id, string $register, string $schema, ObjectServ * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-14 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function canDelete( string $id, @@ -2581,7 +3421,7 @@ public function canDelete( * offset: int<0, max>, next?: string, prev?: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-6 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function contracts(string $id, string $register, string $schema, ObjectService $objectService): JSONResponse { @@ -2625,6 +3465,9 @@ public function contracts(string $id, string $register, string $schema, ObjectSe // Use ObjectService delegation to handler. $result = $objectService->getObjectContracts(objectId: $id, filters: $filters); + // Stamp resourceUrls (deep-links) before paginating. + $result = $this->stampObjectUrls(result: $result); + // Return empty paginated response. return new JSONResponse( data: $this->paginate( @@ -2637,6 +3480,94 @@ public function contracts(string $id, string $register, string $schema, ObjectSe ); }//end contracts() + /** + * Stamp a canonical `url` (resourceUrl) onto each related-object record. + * + * This reuses the SAME resolver the unified-search provider uses + * ({@see \OCA\OpenRegister\Service\DeepLinkRegistryService::resolveUrl()}), + * so there is a single source of truth for "how is an object opened in the + * UI". Consuming apps register per-(register, schema) URL templates via the + * DeepLinkRegistrationEvent; when no registration exists we fall back to + * OpenRegister's own object route (mirroring `lib/Search/ObjectsProvider.php`). + * + * Resolution is defensive: a failure (or missing dependency) omits the `url` + * for that record without altering or dropping the record itself. + * + * @param array $result The relation envelope ({results, total, ...}). + * + * @return array The same envelope with `url` stamped on each resolvable record. + */ + private function stampObjectUrls(array $result): array + { + if ($this->deepLinkRegistry === null + || isset($result['results']) === false + || is_array($result['results']) === false + ) { + return $result; + } + + foreach ($result['results'] as $index => $record) { + // Normalise entities to their serialized array form. + if (is_object($record) === true && method_exists($record, 'jsonSerialize') === true) { + $record = $record->jsonSerialize(); + } + + if (is_array($record) === false) { + continue; + } + + $self = ($record['@self'] ?? []); + $uuid = ($self['id'] ?? ($record['id'] ?? null)); + $registerId = ($self['register'] ?? null); + $schemaId = ($self['schema'] ?? null); + + if ($uuid === null || is_numeric($registerId) === false || is_numeric($schemaId) === false) { + $result['results'][$index] = $record; + continue; + } + + try { + $flat = array_merge( + $record, + [ + 'uuid' => $uuid, + 'id' => $uuid, + 'register' => $registerId, + 'schema' => $schemaId, + ] + ); + + $url = $this->deepLinkRegistry->resolveUrl( + registerId: (int) $registerId, + schemaId: (int) $schemaId, + objectData: $flat + ); + + if ($url === null && $this->relationUrlGenerator !== null) { + $url = $this->relationUrlGenerator->linkToRoute( + 'openregister.objects.show', + [ + 'register' => $registerId, + 'schema' => $schemaId, + 'id' => $uuid, + ] + ); + } + + if ($url !== null) { + $record['url'] = $url; + } + } catch (\Throwable $e) { + // Defensive: never let URL resolution break the relation response. + $this->logger?->debug('Relation URL resolution failed: '.$e->getMessage()); + }//end try + + $result['results'][$index] = $record; + }//end foreach + + return $result; + }//end stampObjectUrls() + /** * Retrieves all objects that this object references * @@ -2659,7 +3590,7 @@ public function contracts(string $id, string $register, string $schema, ObjectSe * limit: 30|mixed, offset: 0|mixed}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-6 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function uses(string $id, string $register, string $schema, ObjectService $objectService): JSONResponse { @@ -2682,8 +3613,8 @@ public function uses(string $id, string $register, string $schema, ObjectService _multitenancy: true ); - // Return the result directly from ObjectService. - return new JSONResponse(data: $result); + // Stamp resourceUrls (deep-links) and return the result from ObjectService. + return new JSONResponse(data: $this->stampObjectUrls(result: $result)); }//end uses() /** @@ -2708,7 +3639,7 @@ public function uses(string $id, string $register, string $schema, ObjectService * offset: 0|mixed, message?: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-6 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function used(string $id, string $register, string $schema, ObjectService $objectService): JSONResponse { @@ -2731,8 +3662,8 @@ public function used(string $id, string $register, string $schema, ObjectService _multitenancy: true ); - // Return the result directly from ObjectService. - return new JSONResponse(data: $result); + // Stamp resourceUrls (deep-links) and return the result from ObjectService. + return new JSONResponse(data: $this->stampObjectUrls(result: $result)); }//end used() /** @@ -2761,7 +3692,7 @@ public function used(string $id, string $register, string $schema, ObjectService * @suppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) Audit log retrieval with pagination + access checks requires branching * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-7 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function logs(string $id, string $register, string $schema, ObjectService $objectService): JSONResponse { @@ -2866,7 +3797,7 @@ public function logs(string $id, string $register, string $schema, ObjectService * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-4 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function lock(string $register, string $schema, string $id): JSONResponse { @@ -2894,7 +3825,8 @@ public function lock(string $register, string $schema, string $id): JSONResponse } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { return new JSONResponse(data: ['error' => 'Object not found'], statusCode: 404); } catch (\Throwable $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + // SEC-CTRL-7: do not leak internal exception detail on 500. + return $this->errorResponse(e: $e); }//end try }//end lock() @@ -2911,7 +3843,7 @@ public function lock(string $register, string $schema, string $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-4 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function unlock(string $register, string $schema, string $id): JSONResponse { @@ -2959,22 +3891,24 @@ public function unlock(string $register, string $schema, string $id): JSONRespon * @param string $schema The schema slug or identifier * @param ObjectService $objectService The object service * - * @return DataDownloadResponse The exported file as a download response + * @return DataDownloadResponse|JSONResponse The exported file as a download response, or a + * 400 JSON error when a `format=pdf` request exceeds {@see \OCA\OpenRegister\Service\ExportService::MAX_PDF_EXPORT_ROWS}. * * @NoAdminRequired * * @NoCSRFRequired * * @psalm-return DataDownloadResponse<200, - * 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'|'text/csv', - * array> + * 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'|'text/csv'|'application/pdf', + * array>|JSONResponse * * @psalm-suppress NoValue * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-11 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/specs/export-pdf-format/spec.md */ - public function export(string $register, string $schema, ObjectService $objectService): DataDownloadResponse + public function export(string $register, string $schema, ObjectService $objectService): DataDownloadResponse|JSONResponse { // Set the register and schema context. $objectService->setRegister(register: $register); @@ -3015,6 +3949,47 @@ public function export(string $register, string $schema, ObjectService $objectSe ); } + if ($type === 'json') { + $content = $this->exportService->exportToJson( + register: $registerEntity, + schema: $schemaEntity, + filters: $filters + ); + + return new DataDownloadResponse( + data: $content, + filename: "{$filenameBase}.json", + contentType: 'application/json' + ); + } + + if ($type === 'pdf') { + try { + $content = $this->exportService->exportToPdf( + register: $registerEntity, + schema: $schemaEntity, + filters: $filters, + currentUser: $this->userSession->getUser() + ); + } catch (ExportTooLargeException $e) { + return new JSONResponse( + data: [ + 'error' => 'export_too_large', + 'message' => $e->getMessage(), + 'rowCount' => $e->getRowCount(), + 'maxRows' => $e->getMaxRows(), + ], + statusCode: ExportTooLargeException::HTTP_STATUS + ); + } + + return new DataDownloadResponse( + data: $content, + filename: "{$filenameBase}.pdf", + contentType: 'application/pdf' + ); + }//end if + // Default to Excel. $spreadsheet = $this->exportService->exportToExcel( register: $registerEntity, @@ -3036,100 +4011,6 @@ public function export(string $register, string $schema, ObjectService $objectSe ); }//end export() - /** - * Import objects into a register - * - * @param int $register The ID of the register to import into - * - * @return JSONResponse JSON response with import result or error. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @psalm-suppress NoValue - * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-20 - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-11 - */ - public function import(int $register): JSONResponse - { - try { - // Get the uploaded file. - $uploadedFile = $this->request->getUploadedFile('file'); - if ($uploadedFile === null) { - return new JSONResponse(data: ['error' => 'No file uploaded'], statusCode: 400); - } - - // Find the register. - $registerEntity = $this->registerMapper->find($register); - - // Get optional schema for CSV (can be null, Excel auto-resolves per sheet). - $schemaId = $this->request->getParam(key: 'schema'); - $schema = null; - if ($schemaId !== null && $schemaId !== '') { - $schema = $this->schemaMapper->find($schemaId); - } - - // Get optional parameters with sensible defaults. - $validation = filter_var($this->request->getParam(key: 'validation', default: false), FILTER_VALIDATE_BOOLEAN); - $events = filter_var($this->request->getParam(key: 'events', default: false), FILTER_VALIDATE_BOOLEAN); - $rbac = filter_var($this->request->getParam(key: 'rbac', default: true), FILTER_VALIDATE_BOOLEAN); - $multi = filter_var($this->request->getParam(key: 'multi', default: true), FILTER_VALIDATE_BOOLEAN); - $publish = filter_var($this->request->getParam(key: 'publish', default: false), FILTER_VALIDATE_BOOLEAN); - $enrich = filter_var($this->request->getParam(key: 'enrich', default: true), FILTER_VALIDATE_BOOLEAN); - - // Determine the import type from the uploaded file extension. - $filename = ($uploadedFile['name'] ?? ''); - $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - - // Route to the real ImportService. CSV requires a specific schema; - // Excel auto-resolves schemas per sheet. - if ($extension === 'csv') { - if ($schema === null) { - return new JSONResponse( - data: ['error' => 'Schema parameter is required for CSV imports.'], - statusCode: 400 - ); - } - - $result = $this->importService->importFromCsv( - filePath: $uploadedFile['tmp_name'], - register: $registerEntity, - schema: $schema, - validation: $validation, - events: $events, - _rbac: $rbac, - _multitenancy: $multi, - publish: $publish, - currentUser: $this->userSession->getUser(), - enrich: $enrich - ); - } else { - $result = $this->importService->importFromExcel( - filePath: $uploadedFile['tmp_name'], - register: $registerEntity, - schema: $schema, - validation: $validation, - events: $events, - _rbac: $rbac, - _multitenancy: $multi, - publish: $publish, - currentUser: $this->userSession->getUser(), - enrich: $enrich - ); - }//end if - - return new JSONResponse( - data: [ - 'message' => 'Import successful', - 'summary' => $result, - ] - ); - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - }//end try - }//end import() - /** * Merge two objects * @@ -3147,7 +4028,7 @@ public function import(int $register): JSONResponse * * @return JSONResponse JSON response with merge result or error * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-5 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function merge( string $id, @@ -3185,7 +4066,7 @@ public function merge( } catch (\Exception $exception) { return new JSONResponse( data: [ - 'error' => 'Failed to merge objects: '.$exception->getMessage(), + 'error' => 'Internal server error', ], statusCode: 500 ); @@ -3206,7 +4087,7 @@ public function merge( * * @return JSONResponse JSON response with migration result or error * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-12 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function migrate(ObjectService $objectService): JSONResponse { @@ -3255,7 +4136,7 @@ public function migrate(ObjectService $objectService): JSONResponse } catch (\Exception $exception) { return new JSONResponse( data: [ - 'error' => 'Failed to migrate objects: '.$exception->getMessage(), + 'error' => 'Internal server error', ], statusCode: 500 ); @@ -3282,7 +4163,7 @@ public function migrate(ObjectService $objectService): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-10 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function downloadFiles( string $id, @@ -3360,7 +4241,7 @@ public function downloadFiles( } catch (\Exception $exception) { return new JSONResponse( data: [ - 'error' => 'Failed to create ZIP file: '.$exception->getMessage(), + 'error' => 'Internal server error', ], statusCode: 500 ); @@ -3380,7 +4261,7 @@ public function downloadFiles( * * @psalm-return JSONResponse<200|500, array{success: bool, error?: string, data?: mixed}, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-9 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function vectorizeBatch(): JSONResponse { @@ -3405,7 +4286,7 @@ public function vectorizeBatch(): JSONResponse return new JSONResponse( data: [ 'success' => false, - 'error' => $e->getMessage(), + 'error' => 'Internal server error', ], statusCode: 500 ); @@ -3425,7 +4306,7 @@ public function vectorizeBatch(): JSONResponse * * @psalm-return JSONResponse<200|500, array{success: bool, error?: string, stats?: mixed}, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-9 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function getObjectVectorizationStats(): JSONResponse { @@ -3449,7 +4330,7 @@ public function getObjectVectorizationStats(): JSONResponse return new JSONResponse( data: [ 'success' => false, - 'error' => $e->getMessage(), + 'error' => 'Internal server error', ], statusCode: 500 ); @@ -3469,7 +4350,7 @@ public function getObjectVectorizationStats(): JSONResponse * * @psalm-return JSONResponse<200|500, array{success: bool, error?: string, count?: mixed}, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-9 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function getObjectVectorizationCount(): JSONResponse { @@ -3493,7 +4374,7 @@ public function getObjectVectorizationCount(): JSONResponse return new JSONResponse( data: [ 'success' => false, - 'error' => $e->getMessage(), + 'error' => 'Internal server error', ], statusCode: 500 ); @@ -3513,7 +4394,7 @@ public function getObjectVectorizationCount(): JSONResponse * * @psalm-return JSONResponse * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-8 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function validate(): JSONResponse { @@ -3718,34 +4599,6 @@ private function isUuid(string $value): bool return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value) === 1; }//end isUuid() - /** - * Clear all blob storage objects (deprecated) - * - * The blob objects table has been retired. All objects now live in per-schema - * magic tables. This endpoint is kept for backwards compatibility but is a no-op. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @return JSONResponse JSON response indicating the operation is no longer applicable - * - * @psalm-return JSONResponse - * - * @deprecated Blob storage has been retired; this endpoint is a no-op. - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-object-data/tasks.md#task-8 - */ - public function clearBlob(): JSONResponse - { - return new JSONResponse( - data: [ - 'success' => true, - 'deleted' => 0, - 'message' => 'Blob storage has been retired. All objects now use magic tables.', - ] - ); - }//end clearBlob() - /** * Recursively strips empty values (null, empty string, empty array) from an array. * @@ -3807,4 +4660,50 @@ private function stripEmptyValues(array $data): array return $result; }//end stripEmptyValues() + + /** + * Build the structured HTTP 403 response for a folder-access denial. + * + * Per the `self-folder-access-control` capability spec, every save + * endpoint that propagates `FolderAccessDeniedException` MUST return + * status 403 with body `{ "error": "folder_access_denied" }`. + * + * The body does NOT echo the attempted folder ID. Doing so would add + * an enumeration oracle: a caller probing `@self.folder` with sequential + * integers could distinguish "folder exists but I can't read it" (403) + * from "folder does not exist" (auto-create / no-op) just by observing + * the response shape. Returning a uniform 403 with no folder context + * forces the attacker to rely on the status code alone — which is already + * a documented privacy property of the spec — and removes the body-level + * confirmation. The caller already knows which folder ID they sent. + * + * The exception's `getAttemptedFolderId()` still carries the ID for + * server-side logging and the audit trail. + * + * Centralised here so the three save endpoints (create / update / postPatch) + * stay in sync without copy-pasting the response shape. + * + * @param FolderAccessDeniedException $exception The denial exception carrying the attempted folder ID. + * + * @return JSONResponse HTTP 403 with the structured body. + */ + private function folderAccessDeniedResponse(FolderAccessDeniedException $exception): JSONResponse + { + // Side-effect: ensure the attempted ID is recorded server-side + // (visible in the audit trail via logFolderAccessDenied + the + // exception message) even though we do NOT echo it back to the + // caller. `$exception` is referenced only to make the audit + // intent clear; the structured body is intentionally minimal. + $this->logger?->info( + '[ObjectsController] Folder access denied — returning 403', + [ + 'attemptedFolderId' => $exception->getAttemptedFolderId(), + ] + ); + + return new JSONResponse( + data: ['error' => 'folder_access_denied'], + statusCode: FolderAccessDeniedException::HTTP_STATUS + ); + }//end folderAccessDeniedResponse() }//end class diff --git a/lib/Controller/OpenProjectLinksController.php b/lib/Controller/OpenProjectLinksController.php index 9748efe17a..d55dc20861 100644 --- a/lib/Controller/OpenProjectLinksController.php +++ b/lib/Controller/OpenProjectLinksController.php @@ -56,10 +56,10 @@ class OpenProjectLinksController extends Controller /** * Constructor. * - * @param string $appName App id. - * @param IRequest $request HTTP request. - * @param OpenProjectLinkService $linkService Backing service. - * @param ObjectService $objectService OR object resolver. + * @param string $appName App id. + * @param IRequest $request HTTP request. + * @param OpenProjectLinkService $linkService Backing service. + * @param ObjectService $objectService OR object resolver. */ public function __construct( string $appName, diff --git a/lib/Controller/OrganisationController.php b/lib/Controller/OrganisationController.php index 9baa0913aa..61277a59d2 100644 --- a/lib/Controller/OrganisationController.php +++ b/lib/Controller/OrganisationController.php @@ -21,10 +21,10 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-76 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-73 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-72 + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-isolation-audit/spec.md + * @spec openspec/specs/tenant-isolation-audit/spec.md */ namespace OCA\OpenRegister\Controller; @@ -129,7 +129,7 @@ class OrganisationController extends Controller * @param IUserSession $userSession User session for authorization checks * @param IGroupManager $groupManager Group manager for admin checks * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function __construct( string $appName, @@ -215,10 +215,11 @@ private function canManageOrganisationMembers(string $organisationUuid): bool * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: OrganisationService::getUserOrganisationStats returns only the current user's own organisations. * * @return JSONResponse JSON response with organisations or error * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function index(): JSONResponse { @@ -256,6 +257,8 @@ public function index(): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: OrganisationService::setActiveOrganisation enforces membership + * (hasUser(userId) === false throws); a user cannot activate an organisation they do not belong to. * * @psalm-return JSONResponse<200|400, * array{error?: string, message?: 'Active organisation set successfully', @@ -269,7 +272,7 @@ public function index(): JSONResponse * users: int<0, max>, groups: int<0, max>}, authorization: array, * created: null|string, updated: null|string}|null}, array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function setActive(string $uuid): JSONResponse { @@ -324,6 +327,7 @@ public function setActive(string $uuid): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped: returns only the current user's active organisation. * * @return JSONResponse Active organisation data * @@ -339,7 +343,7 @@ public function setActive(string $uuid): JSONResponse * users: int<0, max>, groups: int<0, max>}, authorization: array, * created: null|string, updated: null|string}|null}, array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function getActive(): JSONResponse { @@ -387,6 +391,8 @@ public function getActive(): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Self-service, no object read: creates a new organisation with the current user added as owner; + * no caller-supplied object id is read. * * @psalm-return JSONResponse<201|400, * array{error?: string, message?: 'Organisation created successfully', @@ -400,7 +406,7 @@ public function getActive(): JSONResponse * users: int<0, max>, groups: int<0, max>}, authorization: array, * created: null|string, updated: null|string}}, array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function create(string $name, string $description=''): JSONResponse { @@ -464,7 +470,7 @@ public function create(string $name, string $description=''): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function join(string $uuid): JSONResponse { @@ -545,7 +551,7 @@ public function join(string $uuid): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function leave(string $uuid): JSONResponse { @@ -644,7 +650,7 @@ public function leave(string $uuid): JSONResponse * users: int<0, max>, groups: int<0, max>}, authorization: array, * created: null|string, updated: null|string}}, array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function show(string $uuid): JSONResponse { @@ -704,7 +710,7 @@ public function show(string $uuid): JSONResponse * * @SuppressWarnings(PHPMD.NPathComplexity) Already decomposed into helper methods * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function update(string $uuid): JSONResponse { @@ -755,7 +761,7 @@ public function update(string $uuid): JSONResponse * * @return JSONResponse JSON response with patched organisation or error * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function patch(string $uuid): JSONResponse { @@ -787,7 +793,7 @@ public function patch(string $uuid): JSONResponse * limit?: int<1, 100>, offset?: int<0, max>, count?: int<0, max>}, * array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function search(string $query=''): JSONResponse { @@ -859,6 +865,8 @@ function (Organisation $org): array { * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped: OrganisationService::clearCache clears only the current user's own caches keyed by UID; + * not an instance-wide admin operation despite the name. * * @return JSONResponse Success response * @@ -866,7 +874,7 @@ function (Organisation $org): array { * array{error?: 'Failed to clear cache', message?: 'Cache cleared successfully'}, * array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function clearCache(): JSONResponse { @@ -911,7 +919,7 @@ public function clearCache(): JSONResponse * array{error?: 'Failed to retrieve statistics', statistics?: array{total: int}}, * array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function stats(): JSONResponse { @@ -950,7 +958,7 @@ public function stats(): JSONResponse * * @return array Cleaned request data. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function extractRequestData(): array { @@ -970,7 +978,7 @@ private function extractRequestData(): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function handleNameAndSlugUpdate(object $organisation, array $data): void { @@ -995,7 +1003,7 @@ private function handleNameAndSlugUpdate(object $organisation, array $data): voi * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function handleDescriptionUpdate(object $organisation, array $data): void { @@ -1015,7 +1023,7 @@ private function handleDescriptionUpdate(object $organisation, array $data): voi * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function handleSlugUpdate(object $organisation, array $data): void { @@ -1037,7 +1045,7 @@ private function handleSlugUpdate(object $organisation, array $data): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function handleActiveFieldUpdate(object $organisation, array $data): void { @@ -1062,7 +1070,7 @@ private function handleActiveFieldUpdate(object $organisation, array $data): voi * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function applySimpleFieldUpdates(object $organisation, array $data): void { @@ -1089,7 +1097,7 @@ private function applySimpleFieldUpdates(object $organisation, array $data): voi * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function applyArrayFieldUpdates(object $organisation, array $data): void { @@ -1119,7 +1127,7 @@ private function applyArrayFieldUpdates(object $organisation, array $data): void * * @psalm-return JSONResponse<400, array{error: string}, array>|null * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function handleParentUpdate(object $organisation, array $data, string $uuid): JSONResponse|null { @@ -1170,7 +1178,7 @@ private function handleParentUpdate(object $organisation, array $data, string $u * * @return JSONResponse Success response with organisation data. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function saveAndReturnOrganisation(object $organisation): JSONResponse { @@ -1188,7 +1196,7 @@ private function saveAndReturnOrganisation(object $organisation): JSONResponse * * @return JSONResponse Error response with error message * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function handleUpdateError(string $uuid, Exception $exception): JSONResponse { @@ -1216,7 +1224,7 @@ private function handleUpdateError(string $uuid, Exception $exception): JSONResp * * @return string The generated slug * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md */ private function generateSlug(string $name): string { @@ -1244,8 +1252,8 @@ private function generateSlug(string $name): string * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-76 + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function suspend(string $uuid): JSONResponse { @@ -1280,8 +1288,8 @@ public function suspend(string $uuid): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-76 + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function activate(string $uuid): JSONResponse { @@ -1319,8 +1327,8 @@ public function activate(string $uuid): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function deprovision(string $uuid): JSONResponse { @@ -1350,7 +1358,7 @@ public function deprovision(string $uuid): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 + * @spec openspec/specs/tenant-lifecycle/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -1439,8 +1447,8 @@ static function ($record) { * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-73 + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-isolation-audit/spec.md */ public function isolationVerify(): JSONResponse { @@ -1477,8 +1485,8 @@ public function isolationVerify(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-16 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-72 + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-isolation-audit/spec.md */ public function isolationMetrics(): JSONResponse { diff --git a/lib/Controller/PersonLookupController.php b/lib/Controller/PersonLookupController.php new file mode 100644 index 0000000000..8d3347587c --- /dev/null +++ b/lib/Controller/PersonLookupController.php @@ -0,0 +1,119 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Service\Integration\Providers\BrpPersoonProvider; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Read-only person-lookup controller (BRP HaalCentraal leaf). + * + * @category Controller + * @package OCA\OpenRegister\Controller + */ +class PersonLookupController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App id. + * @param IRequest $request HTTP request. + * @param BrpPersoonProvider $brpProvider BRP person lookup leaf. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly BrpPersoonProvider $brpProvider, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Look up a single person by Burgerservicenummer (BSN). + * + * Query param: `bsn` (9-digit Burgerservicenummer, required). The BSN is + * forwarded to the provider (and, downstream, the HaalCentraal request + * body) — it is never logged. This endpoint does NOT validate the elfproef + * checksum; the consuming app does that before/after this call. + * + * @return JSONResponse `{ results, total, meta }` on success (results is + * the raw HaalCentraal person object list — 0 or 1 + * entries; `meta` is the Wet-BRP audit metadata + * `{ correlationId, durationMs, status }` the + * consuming app persists into its `brpLookupVerzoek` + * record), or a 503 with `details.cause` when the + * `brp-haalcentraal` source is unconfigured / BRP is + * down (AD-23). + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt External-gateway proxy: forwards a BSN to the admin-configured external BRP (HaalCentraal) provider; + * takes no OpenRegister object id and never keys an OR object. + * + * @spec openspec/specs/integration-person-lookup/spec.md + */ + public function brpPerson(): JSONResponse + { + $bsn = trim((string) $this->request->getParam('bsn', '')); + if ($bsn === '') { + return new JSONResponse(['error' => 'bsn is required'], 400); + } + + $result = $this->brpProvider->lookupByBsn($bsn); + + if (($result['unavailable'] ?? false) === true) { + return new JSONResponse( + [ + 'error' => 'brp-haalcentraal source is not available', + 'details' => ['cause' => $result['cause']], + ], + 503 + ); + } + + return new JSONResponse($result); + }//end brpPerson() +}//end class diff --git a/lib/Controller/PhotoLinksController.php b/lib/Controller/PhotoLinksController.php index d3a20d8784..cdfc8d8533 100644 --- a/lib/Controller/PhotoLinksController.php +++ b/lib/Controller/PhotoLinksController.php @@ -79,7 +79,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -125,7 +125,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function link(string $register, string $schema, string $id): JSONResponse { @@ -176,7 +176,7 @@ public function link(string $register, string $schema, string $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function createAndLink(string $register, string $schema, string $id): JSONResponse { @@ -226,7 +226,7 @@ public function createAndLink(string $register, string $schema, string $id): JSO * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroy(string $register, string $schema, string $id, string $albumId): JSONResponse { @@ -262,8 +262,9 @@ public function destroy(string $register, string $schema, string $id, string $al * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns the current user's own Photos albums; no caller-supplied object id. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function available(): JSONResponse { diff --git a/lib/Controller/PollLinksController.php b/lib/Controller/PollLinksController.php index aa8e69c96f..b2b5372b8f 100644 --- a/lib/Controller/PollLinksController.php +++ b/lib/Controller/PollLinksController.php @@ -285,6 +285,7 @@ public function destroy(string $register, string $schema, string $id, string $po * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns polls visible to the current user; no caller-supplied object id. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-1 */ diff --git a/lib/Controller/ProcessingLogController.php b/lib/Controller/ProcessingLogController.php new file mode 100644 index 0000000000..e34e21d369 --- /dev/null +++ b/lib/Controller/ProcessingLogController.php @@ -0,0 +1,390 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use DateTime; +use OCA\OpenRegister\Db\ProcessingLogMapper; +use OCA\OpenRegister\Service\OrganisationService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IAppConfig; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IUserSession; + +/** + * REST endpoints for querying and exporting the AVG processing log. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ +class ProcessingLogController extends Controller +{ + + /** + * App identifier for IAppConfig reads. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * App-config key naming the privacy-officer (FG) delegation group. + * + * @var string + */ + private const FG_GROUP_KEY = 'processing_log_fg_group'; + + /** + * Default privacy-officer group name. + * + * @var string + */ + private const FG_GROUP_DEFAULT = 'privacy-officer'; + + /** + * Maximum per-subject extract range in days (configurable). + * + * @var string + */ + private const MAX_RANGE_KEY = 'processing_export_max_range_days'; + + /** + * Constructor. + * + * @param string $appName App identifier. + * @param IRequest $request Active request. + * @param ProcessingLogMapper $logMapper Append-only log mapper. + * @param IUserSession $userSession Current user session. + * @param IGroupManager $groupManager Group manager (admin + FG gate). + * @param IAppConfig $appConfig App-config reader. + * @param OrganisationService $organisationService Org-scoping helper. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly ProcessingLogMapper $logMapper, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, + private readonly IAppConfig $appConfig, + private readonly OrganisationService $organisationService, + ) { + parent::__construct(appName: $appName, request: $request); + + }//end __construct() + + /** + * GET /api/avg/verwerkingen — filtered processing-log inquiry. + * + * Optional query params: `register`, `schema`, `activity`, `actor`, + * `action`, `subjectIdType`, `subjectIdValue`, `from`, `to`, + * `limit`, `offset`. + * + * Admin-only by NC default; delegated to the FG group. Fails closed. + * + * @return JSONResponse Wrapped list envelope, 401, or 403. + * + * @NoCSRFRequired + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + public function index(): JSONResponse + { + $access = $this->resolveAccess(); + if ($access instanceof JSONResponse) { + return $access; + } + + [$isFg, $organisationId] = $access; + + $entries = $this->logMapper->findFiltered( + filters: [ + 'register_id' => $this->optionalStringParam(key: 'register'), + 'schema_id' => $this->optionalStringParam(key: 'schema'), + 'activity_id' => $this->optionalStringParam(key: 'activity'), + 'actor' => $this->optionalStringParam(key: 'actor'), + 'action' => $this->optionalStringParam(key: 'action'), + 'subject_id_type' => $this->optionalStringParam(key: 'subjectIdType'), + 'subject_id_value' => $this->optionalStringParam(key: 'subjectIdValue'), + ], + from: $this->optionalDateParam(key: 'from'), + to: $this->optionalDateParam(key: 'to'), + organisationId: $organisationId, + includeConfidential: $isFg, + limit: $this->intParam(key: 'limit', default: 100, max: 1000), + offset: $this->intParam(key: 'offset', default: 0, max: 1000000) + ); + + return new JSONResponse(data: ['count' => count($entries), 'results' => $entries]); + + }//end index() + + /** + * GET /api/avg/verwerkingen/betrokkene — per-subject inzage extract. + * + * Required query params: `subjectIdType`, `subjectIdValue`. + * Optional: `from`, `to`. Range bounded by + * `processing_export_max_range_days` (default 366) → HTTP 422. + * + * @return JSONResponse Extract envelope, 400/422, 401, or 403. + * + * @NoCSRFRequired + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + public function betrokkene(): JSONResponse + { + $access = $this->resolveAccess(); + if ($access instanceof JSONResponse) { + return $access; + } + + [$isFg, $organisationId] = $access; + + $idType = $this->optionalStringParam(key: 'subjectIdType'); + $idValue = $this->optionalStringParam(key: 'subjectIdValue'); + if ($idType === null || $idValue === null) { + return new JSONResponse( + data: ['error' => 'subjectIdType and subjectIdValue are required'], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + $from = $this->optionalDateParam(key: 'from'); + $to = $this->optionalDateParam(key: 'to'); + + $rangeError = $this->validateRange(from: $from, to: $to); + if ($rangeError instanceof JSONResponse) { + return $rangeError; + } + + $entries = $this->logMapper->findBySubject( + idType: $idType, + idValue: $idValue, + from: $from, + to: $to, + organisationId: $organisationId, + includeConfidential: $isFg + ); + + return new JSONResponse( + data: [ + 'subject' => ['idType' => $idType, 'idValue' => $idValue], + 'period' => ['from' => $from?->format('c'), 'to' => $to?->format('c')], + 'count' => count($entries), + 'reads' => $entries, + ] + ); + + }//end betrokkene() + + /** + * Resolve the caller's access posture, or a denial response. + * + * @return JSONResponse|array{0: bool, 1: string|null} [isFg, organisationId] or a 401/403. + */ + private function resolveAccess() + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse( + data: ['error' => 'Authentication required'], + statusCode: Http::STATUS_UNAUTHORIZED + ); + } + + $groups = $this->groupManager->getUserGroupIds($user); + $isAdmin = in_array('admin', $groups, true); + $isFg = in_array($this->fgGroup(), $groups, true); + + if ($isAdmin === false && $isFg === false) { + // Fail closed: neither admin nor privacy officer. + return new JSONResponse( + data: ['error' => 'Privacy-officer or admin privileges required'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + + // Admins query unscoped; FG-only callers are tenant-scoped to + // their own organisation so they can never read another + // tenant's log (no cross-tenant IDOR). + $organisationId = null; + if ($isAdmin === false) { + $organisationId = $this->callerOrganisationId(); + if ($organisationId === null) { + return new JSONResponse(data: ['count' => 0, 'results' => []]); + } + } + + return [$isFg || $isAdmin, $organisationId]; + + }//end resolveAccess() + + /** + * Active organisation uuid for a non-admin caller, or null. + * + * @return string|null + */ + private function callerOrganisationId(): ?string + { + $organisations = $this->organisationService->getUserOrganisations(); + foreach ($organisations as $organisation) { + $uuid = (string) $organisation->getUuid(); + if ($uuid !== '') { + return $uuid; + } + } + + return null; + + }//end callerOrganisationId() + + /** + * Configured privacy-officer (FG) group name. + * + * @return string + */ + private function fgGroup(): string + { + $value = $this->appConfig->getValueString(self::APP_ID, self::FG_GROUP_KEY, self::FG_GROUP_DEFAULT); + if ($value === '') { + return self::FG_GROUP_DEFAULT; + } + + return $value; + + }//end fgGroup() + + /** + * Validate the requested period against the configured maximum range. + * + * @param DateTime|null $from Lower bound. + * @param DateTime|null $to Upper bound. + * + * @return JSONResponse|null 422 when the range is too wide, else null. + */ + private function validateRange(?DateTime $from, ?DateTime $to): ?JSONResponse + { + if ($from === null || $to === null) { + return null; + } + + $maxDays = (int) $this->appConfig->getValueInt(self::APP_ID, self::MAX_RANGE_KEY, 366); + $spanDays = (int) floor(($to->getTimestamp() - $from->getTimestamp()) / 86400); + if ($spanDays > $maxDays) { + return new JSONResponse( + data: [ + 'error' => 'Requested period exceeds the maximum extract range', + 'maxDays' => $maxDays, + ], + statusCode: Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + return null; + + }//end validateRange() + + /** + * Read a request parameter as a non-empty string, or null. + * + * @param string $key Parameter name. + * + * @return string|null + */ + private function optionalStringParam(string $key): ?string + { + $value = $this->request->getParam(key: $key); + if ($value === null || $value === '') { + return null; + } + + return (string) $value; + + }//end optionalStringParam() + + /** + * Read a request parameter as an ISO-8601 DateTime, or null. + * + * @param string $key Parameter name. + * + * @return DateTime|null + */ + private function optionalDateParam(string $key): ?DateTime + { + $value = $this->optionalStringParam(key: $key); + if ($value === null) { + return null; + } + + try { + return new DateTime($value); + } catch (\Throwable $e) { + return null; + } + + }//end optionalDateParam() + + /** + * Read a bounded integer request parameter. + * + * @param string $key Parameter name. + * @param int $default Fallback value. + * @param int $max Upper clamp. + * + * @return int + */ + private function intParam(string $key, int $default, int $max): int + { + $value = $this->request->getParam(key: $key); + if ($value === null || is_numeric($value) === false) { + return $default; + } + + $int = (int) $value; + if ($int < 0) { + return $default; + } + + return min($int, $max); + + }//end intParam() +}//end class diff --git a/lib/Controller/QualityController.php b/lib/Controller/QualityController.php new file mode 100644 index 0000000000..da08dbc96c --- /dev/null +++ b/lib/Controller/QualityController.php @@ -0,0 +1,141 @@ + + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Exception\NotAuthorizedException; +use OCA\OpenRegister\Service\Quality\QualityStatisticsService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use RuntimeException; + +class QualityController extends Controller +{ + /** + * Constructor. + * + * @param string $appName The application name. + * @param IRequest $request The current request. + * @param QualityStatisticsService $statistics Quality statistics/query service. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly QualityStatisticsService $statistics + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Schema-scoped quality statistics: average, per-status buckets, + * a 10-bucket score histogram, and total. + * + * @param string $register Register reference. + * @param string $schema Schema reference. + * + * @return JSONResponse JSON response with the statistics envelope. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-2 + */ + public function stats(string $register, string $schema): JSONResponse + { + try { + $result = $this->statistics->statisticsFor(register: $register, schema: $schema); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($result); + }//end stats() + + /** + * Lowest-quality object listing (worst-first by default). Accepts + * `qualityStatus` (filter), `sort` (`qualityScore`|`qualityStatus`), + * `order` (`asc`|`desc`), and `limit`/`offset` pagination params. + * + * @param string $register Register reference. + * @param string $schema Schema reference. + * + * @return JSONResponse JSON response with the paginated listing. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-2 + */ + public function index(string $register, string $schema): JSONResponse + { + $qualityStatus = $this->request->getParam('qualityStatus'); + if ($qualityStatus !== null) { + $qualityStatus = (string) $qualityStatus; + } + + $sort = (string) $this->request->getParam('sort', 'qualityScore'); + $order = (string) $this->request->getParam('order', 'asc'); + + $limit = (int) $this->request->getParam('limit', 20); + $offset = (int) $this->request->getParam('offset', 0); + + if ($limit <= 0) { + $limit = 20; + } + + if ($offset < 0) { + $offset = 0; + } + + try { + $result = $this->statistics->lowestQuality( + register: $register, + schema: $schema, + qualityStatus: $qualityStatus, + sort: $sort, + order: $order, + limit: $limit, + offset: $offset + ); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($result); + }//end index() +}//end class diff --git a/lib/Controller/RealtimeController.php b/lib/Controller/RealtimeController.php deleted file mode 100644 index cce0fc20fe..0000000000 --- a/lib/Controller/RealtimeController.php +++ /dev/null @@ -1,196 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://OpenRegister.app - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-4 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Controller; - -use OCA\OpenRegister\Db\RealtimeEventMapper; -use OCA\OpenRegister\Service\OrganisationService; -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IRequest; -use OCP\IUserSession; - -class RealtimeController extends Controller -{ - /** - * Constructor. - * - * @param string $appName The application name. - * @param IRequest $request The current request. - * @param RealtimeEventMapper $eventMapper The realtime event mapper. - * @param OrganisationService $organisationService The organisation service. - * @param IUserSession $userSession Active session — drives the 401 anonymous-caller short-circuit (F11). - */ - public function __construct( - string $appName, - IRequest $request, - private readonly RealtimeEventMapper $eventMapper, - private readonly OrganisationService $organisationService, - private readonly IUserSession $userSession - ) { - parent::__construct(appName: $appName, request: $request); - }//end __construct() - - /** - * Cursor-based polling. Returns events with `id > since`. - * - * Response shape: - * { - * events: CloudEvent[] (each with ._cursor: int), - * cursor: int (max event id in this response, or `since` if empty), - * hasMore: bool (true when limit was reached, hint to poll again sooner) - * } - * - * Anonymous/unauthenticated callers get HTTP 401. Authenticated - * callers see only events scoped to their active organisation - * (multi-tenancy gate). Cross-org events MUST NOT leak. - * - * @param int|null $since The cursor (event id) to fetch events after. - * @param int|null $limit Maximum number of events to return. - * @param string|null $register Optional register filter. - * @param string|null $schema Optional schema filter. - * @param string|null $objectUuid Optional object UUID filter. - * @param string|null $eventType Optional event type filter. - * - * @return JSONResponse JSON response with events, cursor, and hasMore. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-3 - */ - public function events( - ?int $since=null, - ?int $limit=100, - ?string $register=null, - ?string $schema=null, - ?string $objectUuid=null, - ?string $eventType=null - ): JSONResponse { - // Anonymous callers cannot poll the realtime stream; return 401 - // so clients distinguish "not authenticated" from "no events". - if ($this->userSession->getUser() === null) { - return new JSONResponse( - ['error' => 'Unauthorized'], - Http::STATUS_UNAUTHORIZED - ); - } - - $effectiveLimit = max(1, min(1000, (int) ($limit ?? 100))); - - // Multi-tenancy: scope to active organisation. - $orgUuid = null; - try { - $activeOrg = $this->organisationService->getActiveOrganisation(); - $orgUuid = $activeOrg?->getUuid(); - } catch (\Throwable $e) { - $orgUuid = null; - } - - if ($orgUuid === null) { - // Authenticated but no active org → return an empty result - // rather than 500. CLI dev scripts and tests would otherwise - // crash unless the session has resolved an org. - return new JSONResponse( - ['events' => [], 'cursor' => $since ?? 0, 'hasMore' => false] - ); - } - - $filters = [ - 'register' => $register, - 'schema' => $schema, - 'objectUuid' => $objectUuid, - 'eventType' => $eventType, - 'organisation' => $orgUuid, - ]; - - $events = $this->eventMapper->findSince($since, $effectiveLimit, $filters); - $serialised = array_map(fn($event) => $event->jsonSerialize(), $events); - - $newCursor = $since ?? 0; - if (count($events) > 0) { - $newCursor = (int) $events[count($events) - 1]->getId(); - } - - return new JSONResponse( - [ - 'events' => $serialised, - 'cursor' => $newCursor, - 'hasMore' => count($events) === $effectiveLimit, - ] - ); - }//end events() - - /** - * Get the current head cursor (highest event id). Clients use this - * to fast-forward past historical events on initial subscription — - * GET /api/realtime/cursor → {cursor: 12345}, then start polling - * with `?since=12345`. - * - * @return JSONResponse JSON response containing the current head cursor. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-graphql-rt-dash/tasks.md#task-4 - */ - public function cursor(): JSONResponse - { - if ($this->userSession->getUser() === null) { - return new JSONResponse( - ['error' => 'Unauthorized'], - Http::STATUS_UNAUTHORIZED - ); - } - - // SECURITY: scope the cursor to the caller's active organisation. - // Returning the global head pointer let any authed caller observe - // the global write rate by polling — small but real cross-tenant - // side channel. With no active org, return 0 (fail-closed) so - // there is no head pointer to mine. - $orgUuid = null; - try { - $activeOrg = $this->organisationService->getActiveOrganisation(); - $orgUuid = $activeOrg?->getUuid(); - } catch (\Throwable $e) { - $orgUuid = null; - } - - if ($orgUuid === null) { - return new JSONResponse(['cursor' => 0]); - } - - return new JSONResponse( - [ - 'cursor' => $this->eventMapper->getMaxIdForOrganisation($orgUuid), - ] - ); - }//end cursor() -}//end class diff --git a/lib/Controller/RegistersController.php b/lib/Controller/RegistersController.php index 10f3c37474..b54ea3a16c 100644 --- a/lib/Controller/RegistersController.php +++ b/lib/Controller/RegistersController.php @@ -40,18 +40,23 @@ use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Service\ExportService; use OCA\OpenRegister\Service\ImportService; +use OCA\OpenRegister\Service\MigrationPackService; use OCA\OpenRegister\Service\Configuration\GitHubHandler; use OCA\OpenRegister\Service\OasService; use OCA\OpenRegister\Service\Registers\RegisterCacheHandler; +use OCA\OpenRegister\Service\Serializer\RegisterSerializer; use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\DataDownloadResponse; use OCP\DB\Exception as DBException; use OCP\IUserSession; use OCA\OpenRegister\Exception\DatabaseConstraintException; +use OCA\OpenRegister\Exception\ExportTooLargeException; use OCA\OpenRegister\Service\AuthorizationAuditService; use OCA\OpenRegister\Service\Object\PermissionHandler; use OCP\IGroupManager; @@ -91,7 +96,7 @@ * @SuppressWarnings(PHPMD.ExcessiveMethodLength) The create/update actions include multi-step validation * that is a single atomic write; extracting sub-steps would create misleading partial-update helpers. * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ class RegistersController extends Controller { @@ -184,6 +189,8 @@ class RegistersController extends Controller * @param ContainerInterface $container Container for lazy loading services * @param IGroupManager $groupManager Group manager for RBAC checks * @param RegisterCacheHandler $registerCacheHandler Register cache handler (runtime-schema-api) + * @param RegisterSerializer $registerSerializer Register serializer for response shaping + * @param MigrationPackService $migrationPackService Migration pack lookup for import's `packId` param * * @return void * @@ -208,7 +215,9 @@ public function __construct( OasService $oasService, private readonly ContainerInterface $container, private readonly IGroupManager $groupManager, - private readonly RegisterCacheHandler $registerCacheHandler + private readonly RegisterCacheHandler $registerCacheHandler, + private readonly RegisterSerializer $registerSerializer, + private readonly MigrationPackService $migrationPackService ) { $this->logger->debug( message: '[RegistersController] Constructor started.', @@ -291,6 +300,10 @@ public function index(): JSONResponse // Extract filters. $filters = $params['filters'] ?? []; + // Fetch entities so the anonymous-published guard can filter + // them before serialization. The serialized output is produced + // by RegisterService::findAllSerialized below (shared between + // HTTP and DI consumers — see register-service-extensions spec). $registers = $this->registerService->findAll( limit: $limit, offset: $offset, @@ -302,117 +315,159 @@ public function index(): JSONResponse // Read-visibility guard: this endpoint is @PublicPage so it stays // reachable when OpenRegister is restricted to a user group. Anonymous - // callers may only see PUBLISHED registers; authenticated users are - // unaffected. Visibility is derived from server-side published/ - // depublished entity state, never from client-supplied parameters. + // callers may only see registers whose RBAC authorization grants read + // access to the `public` group; authenticated users are unaffected. + // Visibility is derived from server-side authorization rules, never + // from client-supplied parameters. if ($this->isAnonymousRequest() === true) { $registers = array_values( array_filter( $registers, - fn($register) => $this->isPublishedEntity( - published: $register->getPublished(), - depublished: $register->getDepublished() - ) + fn($register) => $this->isPublicReadable(authorization: $register->getAuthorization()) ) ); } - $registersArr = array_map(fn($register) => $register->jsonSerialize(), $registers); - - // If 'schemas' is requested in _extend, expand schema IDs to full schema objects. - if (in_array('schemas', $extend, true) === true) { - foreach ($registersArr as &$register) { - if (($register['schemas'] ?? null) !== null && is_array($register['schemas']) === true) { - $expandedSchemas = []; - foreach ($register['schemas'] as $schemaId) { - try { - $schema = $this->schemaMapper->find(id: $schemaId, _multitenancy: false); - $expandedSchemas[] = $schema->jsonSerialize(); - } catch (DoesNotExistException $e) { - // Schema not found, skip it. - $ctx = ['schemaId' => $schemaId]; - $this->logger->warning( - message: '[RegistersController] Schema not found for expansion', - context: array_merge(['file' => __FILE__, 'line' => __LINE__], $ctx) - ); - } - } + // Pre-compute per-register stats if both `schemas` + `@self.stats` + // were requested, then hand the resulting entity list to the + // serializer in a single call. RegisterSerializer owns the + // schema-expansion + orphan-ID retention contract. + $statsByRegisterId = null; + if (in_array('schemas', $extend, true) === true + && in_array('@self.stats', $extend, true) === true + ) { + $statsByRegisterId = []; + + // Resolve every schema across every register in ONE query. This used to call + // schemaMapper->find() per schema id inside the loop below — on an instance + // with 76 registers and 1,231 schemas that is 1,231 round trips to render one + // list. Orphan ids simply do not come back from the bulk read, which is the + // same "skip it" outcome the old DoesNotExistException catch produced. + $allSchemaIds = []; + foreach ($registers as $register) { + foreach (($register->getSchemas() ?? []) as $schemaId) { + $allSchemaIds[] = (int) $schemaId; + } + } - $register['schemas'] = $expandedSchemas; + $schemasById = $this->schemaMapper->findMultipleOptimized(ids: array_values(array_unique($allSchemaIds))); - // If schemas were expanded and stats are requested, add schema-level stats. - if (in_array('@self.stats', $extend, true) === true && empty($expandedSchemas) === false) { - // Get object counts per schema using optimized query. - $schemaCounts = $this->registerService->getSchemaObjectCounts( - registerId: $register['id'], - schemas: $expandedSchemas - ); + foreach ($registers as $register) { + $expandedSchemas = []; + foreach (($register->getSchemas() ?? []) as $schemaId) { + $schema = ($schemasById[(int) $schemaId] ?? null); + if ($schema === null) { + // Orphan ID — cannot contribute to stats. + continue; + } - $registerId = $register['id']; - $countsJson = json_encode($schemaCounts); - $msg = "[RegistersController] Schema counts for register {$registerId}: {$countsJson}"; - $this->logger->debug( - message: $msg, - context: ['file' => __FILE__, 'line' => __LINE__] - ); + $expandedSchemas[] = $schema; + } - // Add stats to each expanded schema. - foreach ($register['schemas'] as &$schema) { - $schemaId = $schema['id'] ?? null; - $hasCount = 'no'; - if (isset($schemaCounts[$schemaId]) === true) { - $hasCount = 'yes'; - } - - $this->logger->debug( - message: "[RegistersController] Processing schema {$schemaId},".' has count: '.$hasCount, - context: ['file' => __FILE__, 'line' => __LINE__] - ); - // Default: no objects found for this schema. - $schema['stats'] = [ - 'objects' => ['total' => 0], - ]; - $this->logger->debug( - message: "[RegistersController] No count for schema {$schemaId}, set to 0", - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - if ($schemaId !== null && isset($schemaCounts[$schemaId]) === true) { - $schema['stats'] = [ - 'objects' => $schemaCounts[$schemaId], - ]; - $statsJson = json_encode($schema['stats']); - $msg = "[RegistersController] Set stats for schema {$schemaId}: {$statsJson}"; - $this->logger->debug( - message: $msg, - context: ['file' => __FILE__, 'line' => __LINE__] - ); - } - }//end foreach - - unset($schema); - // CRITICAL: Unset reference to prevent corruption of array in subsequent iterations. - }//end if - }//end if + $statsByRegisterId[(int) $register->getId()] = $this->registerService->getSchemaObjectCounts( + registerId: (int) $register->getId(), + schemas: $expandedSchemas + ); }//end foreach - - unset($register); - // CRITICAL: Unset reference to prevent array corruption. }//end if - // If '@self.stats' is requested, attach statistics to each register. + + $registersArr = $this->registerSerializer->serializeMany($registers, $extend, $statsByRegisterId); + + // If '@self.stats' is requested, attach register-level statistics + // alongside the schema-level stats already produced by the + // serializer. Register-level stats remain a controller concern + // because they depend on multiple mappers (object-entity + + // audit-trail) the serializer does not have access to. if (in_array('@self.stats', $extend, true) === true) { foreach ($registersArr as &$register) { $register['stats'] = [ - 'objects' => $this->objectEntityMapper->getStatistics(registerId: $register['id'], schemaId: null), + // A register's object stats are the sum of its schemas' object stats, + // which we ALREADY computed above in one UNION per register + // ($statsByRegisterId). ObjectEntityMapper::getStatistics() recomputes + // the same numbers the slow way — for every register it walks the full + // register/schema-pair list and issues a per-table count. On a dev + // instance (76 registers, 1,231 schemas) that per-register recount was + // ~4.7s of a ~7s request. Reuse the counts in hand; only the + // '@self.stats'-without-'schemas' caller (no per-schema counts) falls + // back to the mapper. + 'objects' => $this->registerObjectStats( + registerId: (int) $register['id'], + schemaStats: ($statsByRegisterId[(int) $register['id']] ?? null) + ), 'logs' => $this->auditTrailMapper->getStatistics(registerId: $register['id'], schemaId: null), 'files' => [ 'total' => 0, 'size' => 0 ], ]; } - } + + unset($register); + }//end if return new JSONResponse(data: ['results' => $registersArr]); }//end index() + /** + * Register-level object statistics, reused from the per-schema counts when we have them. + * + * A register's object stats are the sum of its schemas' object stats. When the caller + * asked for `schemas` + `@self.stats`, index() already computed those per-schema + * counts in one UNION per register — so sum them here rather than call + * ObjectEntityMapper::getStatistics(), which recomputes the same numbers by walking + * the whole register/schema-pair list and counting each table again (~4.7s of a ~7s + * request on a 76-register / 1,231-schema instance). + * + * `total` is the LIVE object count (soft-deleted rows excluded), matching what + * `total` has always meant on this endpoint; `deleted` carries the soft-deleted + * count the per-schema UNION also returns. Summing the per-schema counts scopes the + * total to the register's currently-linked schemas, which is exactly the set whose + * per-schema breakdown is shown alongside it. + * + * When `$schemaStats` is null (the `@self.stats`-without-`schemas` caller has no + * per-schema counts), fall back to the mapper so the numbers are still produced. + * + * @param int $registerId The register id. + * @param array>|null $schemaStats Per-schema stats already + * computed for this register + * (schemaId => count row), or null. + * + * @return array The register-level object stats. + * + * @spec exclude performance: sums already-computed per-schema counts in lieu of a full recount + */ + private function registerObjectStats(int $registerId, ?array $schemaStats): array + { + if ($schemaStats === null) { + return $this->objectEntityMapper->getStatistics(registerId: $registerId, schemaId: null); + } + + $total = 0; + $deleted = 0; + $invalid = 0; + $locked = 0; + $size = 0; + + foreach ($schemaStats as $stat) { + $schemaTotal = (int) ($stat['total'] ?? 0); + $schemaDeleted = (int) ($stat['deleted'] ?? 0); + + // The UNION's `total` counts every row; the live total excludes the + // soft-deleted subset, so `total` here means the same "live objects" that + // getStatistics() reports. + $total += max(0, ($schemaTotal - $schemaDeleted)); + $deleted += $schemaDeleted; + $invalid += (int) ($stat['invalid'] ?? 0); + $locked += (int) ($stat['locked'] ?? 0); + $size += (int) ($stat['size'] ?? 0); + } + + return [ + 'total' => $total, + 'size' => $size, + 'invalid' => $invalid, + 'deleted' => $deleted, + 'locked' => $locked, + ]; + }//end registerObjectStats() + /** * Retrieves a single register by ID * @@ -438,13 +493,11 @@ public function show($id): JSONResponse $register = $this->registerService->find(id: $id, _extend: [], _multitenancy: false); // Read-visibility guard (@PublicPage): an anonymous caller may only view - // a PUBLISHED register. Derived from server-side published/depublished - // entity state, never from client-supplied parameters. + // a register whose RBAC authorization grants read access to the `public` + // group. Derived from server-side authorization rules, never from + // client-supplied parameters. if ($this->isAnonymousRequest() === true - && $this->isPublishedEntity( - published: $register->getPublished(), - depublished: $register->getDepublished() - ) === false + && $this->isPublicReadable(authorization: $register->getAuthorization()) === false ) { return new JSONResponse(data: ['error' => 'Authentication required'], statusCode: 401); } @@ -845,7 +898,7 @@ private function handleObjectCountGuard(Register $register, int $objectCount, bo * * @return JSONResponse JSON response with schemas or error * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function schemas(int|string $id): JSONResponse { @@ -889,7 +942,7 @@ public function schemas(int|string $id): JSONResponse * * @return JSONResponse JSON response with objects * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function objects(int $register, int $schema): JSONResponse { @@ -913,13 +966,17 @@ public function objects(int $register, int $schema): JSONResponse * * @param int $id The ID of the register to export * - * @return DataDownloadResponse|JSONResponse + * @return DataDownloadResponse|JSONResponse Download for `excel`/`csv`/`pdf`/`configuration` formats, + * or a 400 JSON error for a missing schema (`csv`/`pdf`) or an over-cap `pdf` row count. * * @NoAdminRequired * * @NoCSRFRequired * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Export requires handling multiple format branches + * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-10 + * @spec openspec/specs/export-pdf-format/spec.md */ public function export(int $id): JSONResponse|DataDownloadResponse { @@ -971,6 +1028,44 @@ public function export(int $id): JSONResponse|DataDownloadResponse (new DateTime())->format('Y-m-d_His') ); return new DataDownloadResponse($csv, $filename, 'text/csv'); + case 'pdf': + // PDF exports require a specific schema, same as CSV — a coherent + // single table needs one column set. + $schemaId = $this->request->getParam('schema'); + + if ($schemaId === null || $schemaId === '') { + $errMsg = 'PDF export requires a specific schema to be selected'; + return new JSONResponse(data: ['error' => $errMsg], statusCode: 400); + } + + $schema = $this->schemaMapper->find($schemaId); + + try { + $pdf = $this->exportService->exportToPdf( + register: $register, + schema: $schema, + filters: [], + currentUser: $this->userSession->getUser() + ); + } catch (ExportTooLargeException $e) { + return new JSONResponse( + data: [ + 'error' => 'export_too_large', + 'message' => $e->getMessage(), + 'rowCount' => $e->getRowCount(), + 'maxRows' => $e->getMaxRows(), + ], + statusCode: ExportTooLargeException::HTTP_STATUS + ); + } + + $filename = sprintf( + '%s_%s_%s.pdf', + $register->getSlug() ?? 'register', + $schema->getSlug() ?? 'schema', + (new DateTime())->format('Y-m-d_His') + ); + return new DataDownloadResponse($pdf, $filename, 'application/pdf'); case 'configuration': default: $exportData = $this->configurationService->exportConfig( @@ -1248,8 +1343,8 @@ public function publishToGitHub(int $id): JSONResponse * * This method imports data into a register in the specified format and returns a detailed summary. * - * @param int $id The ID of the register to import into - * @param bool $force Force import even if the same or newer version already exists + * @param int|string $id The ID, UUID or slug of the register to import into + * @param bool $force Force import even if the same or newer version already exists * * @return JSONResponse JSON response with import result or error * @@ -1264,7 +1359,7 @@ public function publishToGitHub(int $id): JSONResponse * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-10 */ - public function import(int $id, bool $force=false): JSONResponse + public function import(int|string $id, bool $force=false): JSONResponse { try { // Get the uploaded file. @@ -1301,12 +1396,33 @@ public function import(int $id, bool $force=false): JSONResponse $type = 'excel'; } else if ($extension === 'csv') { $type = 'csv'; - } + } else if ($extension === 'json') { + // A .json upload is either a configuration bundle (object/map + // at the root) or an object export from + // ExportService::exportToJson (a JSON array, or a + // { "results": [...] } envelope). Route the array shape to the + // object importer; everything else stays on the configuration + // path so existing config-import behaviour is unchanged. + $type = 'configuration'; + // Only peek when the uploaded temp file is actually readable; + // a missing/unreadable path would emit a file_get_contents + // warning and leave $type on the configuration path anyway. + $peekRaw = ''; + if (is_readable((string) ($uploadedFile['tmp_name'] ?? '')) === true) { + $peekRaw = (string) file_get_contents($uploadedFile['tmp_name']); + } - if (in_array($extension, ['xlsx', 'xls', 'csv']) === false) { + $peek = json_decode($peekRaw, true); + if (is_array($peek) === true + && (array_is_list($peek) === true + || (isset($peek['results']) === true && is_array($peek['results']) === true)) + ) { + $type = 'json'; + } + } else { $type = 'configuration'; - } - } + }//end if + }//end if // Get import options for all types - support both boolean and string values. $includeObjects = $this->parseBooleanParam(paramName: 'includeObjects', default: false); @@ -1315,6 +1431,22 @@ public function import(int $id, bool $force=false): JSONResponse $publish = $this->parseBooleanParam(paramName: 'publish', default: false); $enrich = $this->parseBooleanParam(paramName: 'enrich', default: true); + // Migration mapping pack (migration-mapping-packs): optional packId + // resolves a stored MigrationPack by its document slug; ImportService + // then maps every row through it before the normal validate/save + // pipeline. dryRun maps + validates every row and saves nothing — + // the per-row report migration quoting needs. + $packId = $this->request->getParam('packId'); + $dryRun = $this->parseBooleanParam(paramName: 'dryRun', default: false); + $pack = null; + if ($packId !== null && $packId !== '') { + try { + $pack = $this->migrationPackService->findByPackSlug(packSlug: (string) $packId)->getDefinitionArray(); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Migration pack "'.$packId.'" not found'], statusCode: 400); + } + } + // Log import parameters for debugging. $this->logger->debug( message: '[RegistersController] Import parameters received', @@ -1333,11 +1465,22 @@ public function import(int $id, bool $force=false): JSONResponse // Handle different import types. switch ($type) { case 'excel': + // Migration packs are only wired into the single-schema CSV/JSON + // paths so far — reject rather than silently ignore packId here + // (an orphaned/no-op parameter is worse than a clear error). + if ($pack !== null) { + return new JSONResponse( + data: ['error' => 'packId is not yet supported for Excel imports; use CSV or JSON with a migration pack instead.'], + statusCode: 400 + ); + } + // Import from Excel and get summary (now returns sheet-based format). - // Get additional performance parameters with enhanced boolean parsing. - $rbac = $this->parseBooleanParam(paramName: 'rbac', default: true); - $multi = $this->parseBooleanParam(paramName: 'multi', default: true); - // Use optimized default. + // SEC-CTRL-6: Do NOT read rbac/multi from the request — that would let a + // manager pass ?multi=false to write objects across organisation boundaries. + // Derive RBAC from admin status and always keep imports tenant-scoped. + $rbac = ($this->isCurrentUserAdmin() === false); + $multi = true; $summary = $this->importService->importFromExcel( filePath: $uploadedFile['tmp_name'], register: $register, @@ -1365,10 +1508,11 @@ public function import(int $id, bool $force=false): JSONResponse $schema = $this->schemaMapper->find($schemaId); - // Get additional performance parameters with enhanced boolean parsing. - $rbac = $this->parseBooleanParam(paramName: 'rbac', default: true); - $multi = $this->parseBooleanParam(paramName: 'multi', default: true); - // Use optimized default. + // SEC-CTRL-6: Do NOT read rbac/multi from the request — that would let a + // manager pass ?multi=false to write objects across organisation boundaries. + // Derive RBAC from admin status and always keep imports tenant-scoped. + $rbac = ($this->isCurrentUserAdmin() === false); + $multi = true; $summary = $this->importService->importFromCsv( filePath: $uploadedFile['tmp_name'], register: $register, @@ -1379,11 +1523,59 @@ public function import(int $id, bool $force=false): JSONResponse _multitenancy: $multi, publish: $publish, currentUser: $this->userSession->getUser(), - enrich: $enrich + enrich: $enrich, + pack: $pack, + dryRun: $dryRun + ); + break; + case 'json': + // Object import (inverse of ExportService::exportToJson). The + // schema may be passed explicitly; otherwise it is derived + // from the exported objects' @self.schema so a plain file + // upload round-trips without extra parameters. + $schemaId = $this->request->getParam('schema'); + if ($schemaId === null || $schemaId === '') { + $peek = json_decode((string) file_get_contents($uploadedFile['tmp_name']), true); + $list = ($peek['results'] ?? $peek); + $schemaId = ($list[0]['@self']['schema'] ?? null); + } + + if ($schemaId === null || $schemaId === '') { + return new JSONResponse( + data: ['error' => 'Could not determine schema for JSON import; pass a schema parameter.'], + statusCode: 400 + ); + } + + $schema = $this->schemaMapper->find($schemaId); + + // SEC-CTRL-6: derive RBAC from admin status; keep tenant-scoped. + $rbac = ($this->isCurrentUserAdmin() === false); + $multi = true; + $summary = $this->importService->importFromJson( + filePath: $uploadedFile['tmp_name'], + register: $register, + schema: $schema, + validation: $validation, + events: $events, + _rbac: $rbac, + _multitenancy: $multi, + publish: $publish, + currentUser: $this->userSession->getUser(), + enrich: $enrich, + pack: $pack, + dryRun: $dryRun ); break; case 'configuration': default: + if ($pack !== null) { + return new JSONResponse( + data: ['error' => 'packId only applies to CSV/JSON row imports, not configuration bundle imports.'], + statusCode: 400 + ); + } + // Initialize the uploaded files array. $uploadedFiles = [$uploadedFile]; // Get the uploaded JSON data. @@ -1529,7 +1721,7 @@ public function rollbackImport(): JSONResponse $importerUid = $auditSample[0]->getUser(); } - if ($this->canRollbackImport($user, $importerUid) === false) { + if ($this->canRollbackImport(user: $user, importerUid: $importerUid) === false) { return new JSONResponse( data: ['error' => 'Forbidden: only the user who initiated the import or an admin may roll it back'], statusCode: 403 @@ -1557,6 +1749,9 @@ public function rollbackImport(): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt Guarded downstream: RegisterService::find loads the register via RegisterMapper::find(_multitenancy: true); + * register metadata reads are open by design (only create/update/delete are admin-gated) and the response carries no + * cross-object PII. * * @return JSONResponse The JSON response containing register statistics * @@ -1606,10 +1801,10 @@ public function rollbackImport(): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-6 - * - * @NoAdminRequired + * @spec openspec/specs/production-observability/spec.md#requirement-per-entity-statistics-and-endpoint-delivery-log-api */ + #[NoAdminRequired] + #[NoCSRFRequired] public function stats(int $id): JSONResponse { try { @@ -1686,21 +1881,27 @@ private function isAnonymousRequest(): bool }//end isAnonymousRequest() /** - * Whether an entity is currently published. + * Whether an entity is anonymously readable via RBAC authorization. * - * A resource is published when its `published` timestamp is set and it has - * not since been depublished. Both values come from persisted entity state. + * A resource is visible to anonymous callers when its authorization block + * grants `read` access to the `public` group. This replaces the former + * published/depublished column gate: anonymous publication is an RBAC + * concern, expressed as a `public`-group read rule on the entity. * - * @param \DateTime|null $published The published timestamp, or null. - * @param \DateTime|null $depublished The depublished timestamp, or null. + * @param array|null $authorization The entity's authorization block, or null. * - * @return bool True if the entity is published and not depublished. + * @return bool True if the `public` group has read access. */ - private function isPublishedEntity(?\DateTime $published, ?\DateTime $depublished): bool + private function isPublicReadable(?array $authorization): bool { - return $published !== null && $depublished === null; + if (is_array($authorization) === false) { + return false; + } + + $readGroups = ($authorization['read'] ?? []); + return is_array($readGroups) === true && in_array('public', $readGroups, true) === true; - }//end isPublishedEntity() + }//end isPublicReadable() /** * Check whether the currently authenticated user is a Nextcloud administrator. diff --git a/lib/Controller/RelationsController.php b/lib/Controller/RelationsController.php index 0b59570926..5b9d71acb1 100644 --- a/lib/Controller/RelationsController.php +++ b/lib/Controller/RelationsController.php @@ -48,6 +48,83 @@ class RelationsController extends Controller { + /** + * Pluggable leaf integrations aggregated into `/relations` beyond the core + * six (notes/tasks/emails/events/contacts/deck). Keyed by the response group + * key the frontend widget consumes; each entry names the OR link service + * class (resolved lazily), its app-availability guard method (or null when + * the service has none), and the per-object lookup method. + * + * @var array + */ + private const LEAF_INTEGRATIONS = [ + 'talk' => [ + 'class' => \OCA\OpenRegister\Service\TalkLinkService::class, + 'available' => 'isTalkAvailable', + 'method' => 'getLinkedRooms', + ], + 'forms' => [ + 'class' => \OCA\OpenRegister\Service\FormLinkService::class, + 'available' => null, + 'method' => 'getLinkedForms', + ], + 'maps' => [ + 'class' => \OCA\OpenRegister\Service\MapLinkService::class, + 'available' => 'isMapsAvailable', + 'method' => 'getLinkedPois', + ], + 'polls' => [ + 'class' => \OCA\OpenRegister\Service\PollLinkService::class, + 'available' => 'isPollsAvailable', + 'method' => 'getLinkedPolls', + ], + 'bookmarks' => [ + 'class' => \OCA\OpenRegister\Service\BookmarkLinkService::class, + 'available' => 'isBookmarksAvailable', + 'method' => 'getLinkedBookmarks', + ], + 'collectives' => [ + 'class' => \OCA\OpenRegister\Service\CollectiveLinkService::class, + 'available' => 'isCollectivesAvailable', + 'method' => 'getLinkedPages', + ], + 'photos' => [ + 'class' => \OCA\OpenRegister\Service\PhotoLinkService::class, + 'available' => 'isPhotosAvailable', + 'method' => 'getLinkedAlbums', + ], + 'cospend' => [ + 'class' => \OCA\OpenRegister\Service\CospendLinkService::class, + 'available' => 'isCospendAvailable', + 'method' => 'getLinkedEntries', + ], + 'timetracker' => [ + 'class' => \OCA\OpenRegister\Service\TimeTrackerLinkService::class, + 'available' => 'isTimeManagerAvailable', + 'method' => 'getLinkedEntries', + ], + 'analytics' => [ + 'class' => \OCA\OpenRegister\Service\AnalyticsLinkService::class, + 'available' => 'isAnalyticsAvailable', + 'method' => 'getLinkedReports', + ], + 'flow' => [ + 'class' => \OCA\OpenRegister\Service\FlowLinkService::class, + 'available' => 'isFlowAvailable', + 'method' => 'getLinkedOperations', + ], + 'openproject' => [ + 'class' => \OCA\OpenRegister\Service\OpenProjectLinkService::class, + 'available' => 'isOpenConnectorAvailable', + 'method' => 'getLinkedWorkPackages', + ], + 'xwiki' => [ + 'class' => \OCA\OpenRegister\Service\XwikiLinkService::class, + 'available' => 'isOpenConnectorAvailable', + 'method' => 'getLinkedPages', + ], + ]; + /** * Object service. * @@ -288,6 +365,34 @@ private function gatherRelations(string $objectUuid, ?array $typesFilter): array } } + // Additional pluggable leaf integrations. Each owning app's link service + // is resolved lazily through the server container and guarded by its own + // availability check, so a missing/disabled app is silently skipped and + // never breaks the core relations response. Records arrive as plain + // arrays (url-stamped by the service where the app exposes a canonical + // deep-link route). See ADR-022 — apps consume OR leaf abstractions. + foreach (self::LEAF_INTEGRATIONS as $key => $spec) { + if ($typesFilter !== null && in_array($key, $typesFilter) === false) { + continue; + } + + try { + $service = \OCP\Server::get($spec['class']); + if ($spec['available'] !== null && $service->{$spec['available']}() !== true) { + continue; + } + + $items = $service->{$spec['method']}($objectUuid); + $relations[$key] = ['results' => $items, 'total' => count($items)]; + } catch (\Throwable $e) { + $this->logger->warning( + '[RelationsController::gatherRelations] {type} lookup failed for object {uuid}: {error}', + ['type' => $key, 'uuid' => $objectUuid, 'error' => $e->getMessage()] + ); + $errors[$key] = ['message' => $e->getMessage(), 'exception' => get_class($e)]; + }//end try + }//end foreach + if (empty($errors) === false) { $relations['_errors'] = $errors; } diff --git a/lib/Controller/ReportsController.php b/lib/Controller/ReportsController.php index 948d4da767..c533dff07f 100644 --- a/lib/Controller/ReportsController.php +++ b/lib/Controller/ReportsController.php @@ -20,9 +20,9 @@ * @category Controller * @package OCA\OpenRegister\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app * diff --git a/lib/Controller/RetentionController.php b/lib/Controller/RetentionController.php index e4eab36b71..1d16021e0e 100644 --- a/lib/Controller/RetentionController.php +++ b/lib/Controller/RetentionController.php @@ -20,8 +20,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-66 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-69 + * @spec openspec/specs/retention-management/spec.md + * @spec openspec/specs/retention-management/spec.md */ declare(strict_types=1); @@ -100,7 +100,7 @@ public function __construct( * * @return JSONResponse Response with updated destruction list * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-66 + * @spec openspec/specs/retention-management/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -267,7 +267,7 @@ function ($obj) use ($excludeUuids) { * * @return JSONResponse Response with updated status * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-66 + * @spec openspec/specs/retention-management/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -350,7 +350,7 @@ public function rejectDestructionList(string $id): JSONResponse * * @return JSONResponse Response with updated object * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-69 + * @spec openspec/specs/retention-management/spec.md */ public function placeLegalHold(): JSONResponse { @@ -404,7 +404,7 @@ public function placeLegalHold(): JSONResponse * * @return JSONResponse Response with updated object * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-69 + * @spec openspec/specs/retention-management/spec.md */ public function releaseLegalHold(string $id): JSONResponse { @@ -451,7 +451,7 @@ public function releaseLegalHold(string $id): JSONResponse * * @return JSONResponse Response confirming the bulk operation * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-69 + * @spec openspec/specs/retention-management/spec.md */ public function placeBulkLegalHold(): JSONResponse { diff --git a/lib/Controller/ScheduledReportsController.php b/lib/Controller/ScheduledReportsController.php new file mode 100644 index 0000000000..5d037e59e8 --- /dev/null +++ b/lib/Controller/ScheduledReportsController.php @@ -0,0 +1,361 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use InvalidArgumentException; +use OCA\OpenRegister\BackgroundJob\ScheduledReportRunNowJob; +use OCA\OpenRegister\Service\ScheduledReportService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\BackgroundJob\IJobList; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * ScheduledReportsController handles CRUD + run-now for scheduled report exports. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ +class ScheduledReportsController extends Controller +{ + /** + * Constructor. + * + * @param string $appName Application name. + * @param IRequest $request HTTP request. + * @param ScheduledReportService $service Business logic. + * @param IJobList $jobList Background job list (for run-now). + * @param IUserSession $userSession Current-user session. + * @param IGroupManager $groupManager Group manager (admin check). + * @param LoggerInterface $logger Logger. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly ScheduledReportService $service, + private readonly IJobList $jobList, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, + private readonly LoggerInterface $logger + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Resolve the current user's uid, or null when anonymous. + * + * @return ?string + */ + private function resolveUserId(): ?string + { + $user = $this->userSession->getUser(); + if ($user === null) { + return null; + } + + return $user->getUID(); + }//end resolveUserId() + + /** + * Whether the current user is a Nextcloud administrator. + * + * @return bool + */ + private function isCurrentUserAdmin(): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + return $this->groupManager->isAdmin($user->getUID()); + }//end isCurrentUserAdmin() + + /** + * Build a 401 response for anonymous callers. + * + * @return JSONResponse + */ + private function authRequiredResponse(): JSONResponse + { + return new JSONResponse(data: ['error' => 'Authentication required'], statusCode: 401); + }//end authRequiredResponse() + + /** + * List the current user's scheduled reports. Admins may pass `?all=true` + * to see every user's reports. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + * + * @SuppressWarnings(PHPMD.ElseExpression) Deliberate branch, not a + * default-then-overwrite — see the inline comment: each arm must + * run exactly one query, never both. + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function index(): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + $wantsAll = filter_var($this->request->getParam('all', 'false'), FILTER_VALIDATE_BOOLEAN); + + // Deliberately branches (not "assign a default, then overwrite") so + // a non-admin ?all=true request never triggers the all-rows query + // path, and an admin ?all=true request never runs the owner-only + // query it's about to discard. + if ($wantsAll === true && $this->isCurrentUserAdmin() === true) { + $reports = $this->service->findAllForAdmin(); + } else { + $reports = $this->service->findForOwner(ownerUid: $userId); + } + + $items = array_map(static fn($r) => $r->jsonSerialize(), $reports); + + return new JSONResponse(data: ['results' => $items, 'total' => count($items)]); + }//end index() + + /** + * Get a single scheduled report. Owner or admin only. + * + * @param int $id The scheduled report id. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function show(int $id): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + try { + $report = $this->service->find(id: $id); + $this->service->assertOwnerOrAdmin(report: $report, callerUid: $userId, callerIsAdmin: $this->isCurrentUserAdmin()); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Scheduled report not found'], statusCode: 404); + } catch (\RuntimeException $e) { + return new JSONResponse(data: ['error' => 'Scheduled report not found'], statusCode: 404); + } + + return new JSONResponse(data: $report->jsonSerialize()); + }//end show() + + /** + * Create a scheduled report owned by the current user. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function create(): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + $data = $this->request->getParams(); + foreach (array_keys($data) as $key) { + if (str_starts_with($key, '_') === true) { + unset($data[$key]); + } + } + + try { + $report = $this->service->create(data: $data, ownerUid: $userId); + } catch (InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 422); + } catch (\Throwable $e) { + $this->logger->error( + message: '[ScheduledReportsController] Error creating scheduled report: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'trace' => $e->getTraceAsString()] + ); + return new JSONResponse(data: ['error' => 'Failed to create scheduled report'], statusCode: 500); + } + + return new JSONResponse(data: $report->jsonSerialize(), statusCode: 201); + }//end create() + + /** + * Update an existing scheduled report. Owner or admin only. + * + * @param int $id The scheduled report id. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function update(int $id): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + $data = $this->request->getParams(); + foreach (array_keys($data) as $key) { + if (str_starts_with($key, '_') === true) { + unset($data[$key]); + } + } + + try { + $report = $this->service->update( + id: $id, + data: $data, + callerUid: $userId, + callerIsAdmin: $this->isCurrentUserAdmin() + ); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Scheduled report not found'], statusCode: 404); + } catch (\RuntimeException $e) { + return new JSONResponse(data: ['error' => 'Scheduled report not found'], statusCode: 404); + } catch (InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 422); + } catch (\Throwable $e) { + $this->logger->error( + message: '[ScheduledReportsController] Error updating scheduled report: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $id, 'trace' => $e->getTraceAsString()] + ); + return new JSONResponse(data: ['error' => 'Failed to update scheduled report'], statusCode: 500); + } + + return new JSONResponse(data: $report->jsonSerialize()); + }//end update() + + /** + * Delete a scheduled report. Owner or admin only. + * + * @param int $id The scheduled report id. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function destroy(int $id): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + try { + $this->service->delete(id: $id, callerUid: $userId, callerIsAdmin: $this->isCurrentUserAdmin()); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Scheduled report not found'], statusCode: 404); + } catch (\RuntimeException $e) { + return new JSONResponse(data: ['error' => 'Scheduled report not found'], statusCode: 404); + } catch (\Throwable $e) { + $this->logger->error( + message: '[ScheduledReportsController] Error deleting scheduled report: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $id, 'trace' => $e->getTraceAsString()] + ); + return new JSONResponse(data: ['error' => 'Failed to delete scheduled report'], statusCode: 500); + } + + return new JSONResponse(data: null, statusCode: 204); + }//end destroy() + + /** + * Queue an immediate execution of a scheduled report. Owner or admin only. + * + * Never runs the export inline in the request — always dispatches + * `ScheduledReportRunNowJob` via `IJobList::add()` and returns 202. + * + * @param int $id The scheduled report id. + * + * @return JSONResponse + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function runNow(int $id): JSONResponse + { + $userId = $this->resolveUserId(); + if ($userId === null) { + return $this->authRequiredResponse(); + } + + try { + $report = $this->service->find(id: $id); + $this->service->assertOwnerOrAdmin(report: $report, callerUid: $userId, callerIsAdmin: $this->isCurrentUserAdmin()); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Scheduled report not found'], statusCode: 404); + } catch (\RuntimeException $e) { + return new JSONResponse(data: ['error' => 'Scheduled report not found'], statusCode: 404); + } + + $this->jobList->add(ScheduledReportRunNowJob::class, ['scheduledReportId' => $report->getId()]); + + $this->logger->info( + message: '[ScheduledReportsController] Queued run-now for scheduled report', + context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $report->getId()] + ); + + return new JSONResponse(data: ['queued' => true], statusCode: 202); + }//end runNow() +}//end class diff --git a/lib/Controller/SchemaImportController.php b/lib/Controller/SchemaImportController.php new file mode 100644 index 0000000000..80b12bbeae --- /dev/null +++ b/lib/Controller/SchemaImportController.php @@ -0,0 +1,357 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Exception\SchemaImportException; +use OCA\OpenRegister\Service\SchemaImport\ImportedSchema; +use OCA\OpenRegister\Service\SchemaImport\ImportOptions; +use OCA\OpenRegister\Service\SchemaImport\SchemaImportService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +/** + * Controller for standards-based schema import. + */ +class SchemaImportController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App name. + * @param IRequest $request Request. + * @param SchemaImportService $importService Standards import orchestrator. + * @param SchemaMapper $schemaMapper Schema persistence. + * @param RegisterMapper $registerMapper Register lookup/association. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly SchemaImportService $importService, + private readonly SchemaMapper $schemaMapper, + private readonly RegisterMapper $registerMapper, + private readonly LoggerInterface $logger + ) { + parent::__construct(appName: $appName, request: $request); + + }//end __construct() + + /** + * Search importable types/objecttypes for a dialect over the bundled snapshot. + * + * @param string $dialect The dialect key (schema.org | ggm). + * + * @return JSONResponse The discovery results. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function types(string $dialect): JSONResponse + { + $query = (string) ($this->request->getParam('q', '')); + + try { + return new JSONResponse($this->importService->discover($dialect, $query)); + } catch (SchemaImportException $e) { + return new JSONResponse(['error' => $e->getMessage()], $e->getHttpStatus()); + } + + }//end types() + + /** + * The bundled snapshot version info for a dialect. + * + * @param string $dialect The dialect key. + * + * @return JSONResponse The snapshot info. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function snapshot(string $dialect): JSONResponse + { + try { + return new JSONResponse($this->importService->snapshotInfo($dialect)); + } catch (SchemaImportException $e) { + return new JSONResponse(['error' => $e->getMessage()], $e->getHttpStatus()); + } + + }//end snapshot() + + /** + * Import a type/objecttype from a dialect's snapshot, persisting the + * resulting schema and optionally associating it with a register. + * + * @param string $dialect The dialect key (schema.org | ggm). + * + * @return JSONResponse The created schema, or a structured error. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function import(string $dialect): JSONResponse + { + $reference = $this->request->getParam('reference'); + if (is_string($reference) === false || $reference === '') { + return new JSONResponse(['error' => 'A "reference" (type IRI / bare name / objecttype id) is required.'], Http::STATUS_BAD_REQUEST); + } + + $options = ImportOptions::fromArray($this->request->getParams()); + + try { + $imported = $this->importService->import($dialect, $reference, $options); + } catch (SchemaImportException $e) { + return new JSONResponse(['error' => $e->getMessage()], $e->getHttpStatus()); + } + + try { + $schema = $this->persistNewSchema(imported: $imported); + } catch (\Throwable $e) { + return $this->failPersist(e: $e); + } + + $registerId = $options->targetRegister; + if ($registerId !== null) { + $this->associateWithRegister(registerId: $registerId, schemaId: (int) $schema->getId()); + } + + return new JSONResponse( + [ + 'schema' => $schema->jsonSerialize(), + 'unknownRequested' => $imported->unknownRequested, + ], + Http::STATUS_CREATED + ); + + }//end import() + + /** + * Update an imported schema from its recorded source (three-way merge). + * + * Defaults to a non-mutating preview; pass `apply: true` to persist. When + * conflicts exist they must be confirmed per-property via + * `resolveConflicts: ["propName", ...]` or the apply is refused. + * + * @param int $id The schema id. + * + * @return JSONResponse The classified diff (preview) or the updated schema. + * + * @spec openspec/specs/schema-import/spec.md + * + * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. + */ + public function reimport(int $id): JSONResponse + { + try { + $schema = $this->schemaMapper->find($id); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Schema not found'], Http::STATUS_NOT_FOUND); + } + + $configuration = ($schema->getConfiguration() ?? []); + $importSource = ($configuration['importSource'] ?? null); + if (is_array($importSource) === false) { + return new JSONResponse( + ['error' => 'This schema was not imported from a standard; nothing to update from.'], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } + + $resolveConflicts = $this->request->getParam('resolveConflicts', []); + if (is_array($resolveConflicts) === false) { + $resolveConflicts = []; + } + + $resolveConflicts = array_values(array_filter($resolveConflicts, 'is_string')); + + try { + $diff = $this->importService->previewUpdateFromSource( + importSource: $importSource, + currentProperties: $schema->getProperties(), + resolvedConflicts: $resolveConflicts + ); + } catch (SchemaImportException $e) { + return new JSONResponse(['error' => $e->getMessage()], $e->getHttpStatus()); + } + + $apply = filter_var($this->request->getParam('apply', false), FILTER_VALIDATE_BOOLEAN); + if ($apply === false) { + return new JSONResponse($this->previewPayload(diff: $diff)); + } + + if ($diff['applied'] === false) { + return new JSONResponse( + [ + 'error' => 'Unresolved conflicts. Confirm each conflicting property via "resolveConflicts" before applying.', + 'conflicts' => $diff['conflicts'], + ], + Http::STATUS_CONFLICT + ); + } + + try { + $schema = $this->applyMerge(schema: $schema, diff: $diff); + } catch (\Throwable $e) { + return $this->failPersist(e: $e); + } + + return new JSONResponse( + [ + 'schema' => $schema->jsonSerialize(), + 'applied' => $this->previewPayload(diff: $diff), + ] + ); + + }//end reimport() + + /** + * Persist a freshly-imported schema. + * + * @param ImportedSchema $imported The mapped schema. + * + * @return Schema The persisted schema entity. + */ + private function persistNewSchema(ImportedSchema $imported): Schema + { + $payload = $imported->toSchemaArray(); + + $schema = new Schema(); + $schema->hydrate($payload); + + return $this->schemaMapper->insert($schema); + + }//end persistNewSchema() + + /** + * Apply a confirmed merge to an imported schema and persist it. + * + * @param Schema $schema The schema. + * @param array $diff The merge result. + * + * @return Schema The updated schema entity. + */ + private function applyMerge(Schema $schema, array $diff): Schema + { + $schema->setProperties($diff['merged']); + + // Refresh the import baseline + jsonld block from the new source so the + // next update-from-source diffs against what was actually applied. + $configuration = ($schema->getConfiguration() ?? []); + if (isset($diff['incomingSource']) === true && is_array($diff['incomingSource']) === true) { + $incomingSource = $diff['incomingSource']; + $incomingSource['baseline'] = $diff['merged']; + $configuration['importSource'] = $incomingSource; + } + + if (isset($diff['jsonld']) === true && is_array($diff['jsonld']) === true && $diff['jsonld'] !== []) { + $configuration['jsonld'] = $diff['jsonld']; + } + + $schema->setConfiguration($configuration); + + return $this->schemaMapper->update($schema); + + }//end applyMerge() + + /** + * Associate a newly-imported schema with a target register (best-effort). + * + * @param int $registerId The register id. + * @param int $schemaId The schema id. + * + * @return void + */ + private function associateWithRegister(int $registerId, int $schemaId): void + { + try { + $register = $this->registerMapper->find($registerId); + if (($register instanceof Register) === false) { + return; + } + + $schemas = $register->getSchemas(); + if (in_array($schemaId, $schemas, false) === false) { + $schemas[] = $schemaId; + $register->setSchemas($schemas); + $this->registerMapper->update($register); + } + } catch (\Throwable $e) { + $this->logger->warning( + '[SchemaImportController] Could not associate imported schema with register', + ['register' => $registerId, 'schema' => $schemaId, 'error' => $e->getMessage()] + ); + } + + }//end associateWithRegister() + + /** + * Shape the merge result for an API response (drops the merged map). + * + * @param array $diff The merge result. + * + * @return array The response-safe diff summary. + */ + private function previewPayload(array $diff): array + { + return [ + 'added' => $diff['added'], + 'removed' => $diff['removed'], + 'changed' => $diff['changed'], + 'keptLocal' => $diff['keptLocal'], + 'conflicts' => $diff['conflicts'], + 'applied' => $diff['applied'], + ]; + + }//end previewPayload() + + /** + * Log and map a persistence failure to a 500 response. + * + * @param \Throwable $e The failure. + * + * @return JSONResponse The error response. + */ + private function failPersist(\Throwable $e): JSONResponse + { + $this->logger->error( + '[SchemaImportController] Failed to persist imported schema', + ['error' => $e->getMessage()] + ); + return new JSONResponse(['error' => 'Failed to persist the imported schema: '.$e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); + + }//end failPersist() +}//end class diff --git a/lib/Controller/SchemaMigrationController.php b/lib/Controller/SchemaMigrationController.php new file mode 100644 index 0000000000..8a8ae4e480 --- /dev/null +++ b/lib/Controller/SchemaMigrationController.php @@ -0,0 +1,399 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\SchemaChangelogMapper; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\SchemaRunEntryMapper; +use OCA\OpenRegister\Db\SchemaRunMapper; +use OCA\OpenRegister\Exception\SchemaRunConcurrencyException; +use OCA\OpenRegister\Service\Schema\SchemaMigrationService; +use OCA\OpenRegister\Service\Schema\SchemaRevalidationService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\JSONResponse; +use OCP\BackgroundJob\IJobList; +use OCP\IRequest; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Controller for schema versioning, revalidation and migration. + */ +class SchemaMigrationController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App name. + * @param IRequest $request Request. + * @param SchemaMapper $schemaMapper Schema lookup. + * @param RegisterMapper $registerMapper Register lookup (resolve population register). + * @param SchemaChangelogMapper $changelogMapper Changelog read. + * @param SchemaRunMapper $runMapper Run read. + * @param SchemaRunEntryMapper $runEntryMapper Run entry read. + * @param SchemaRevalidationService $revalidationService Revalidation engine. + * @param SchemaMigrationService $migrationService Migration engine. + * @param IJobList $jobList Job list (enqueue runs). + * @param IUserSession $userSession Current user. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly SchemaMapper $schemaMapper, + private readonly RegisterMapper $registerMapper, + private readonly SchemaChangelogMapper $changelogMapper, + private readonly SchemaRunMapper $runMapper, + private readonly SchemaRunEntryMapper $runEntryMapper, + private readonly SchemaRevalidationService $revalidationService, + private readonly SchemaMigrationService $migrationService, + private readonly IJobList $jobList, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger + ) { + parent::__construct(appName: $appName, request: $request); + + }//end __construct() + + /** + * Get a schema's classified changelog, newest-first. + * + * @param int $id The schema id. + * + * @return JSONResponse The changelog entries. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function changelog(int $id): JSONResponse + { + $limit = $this->intParam(name: '_limit'); + $offset = $this->intParam(name: '_offset'); + + $entries = $this->changelogMapper->findBySchema(schemaId: $id, limit: $limit, offset: $offset); + + return new JSONResponse(['results' => array_map(static fn($e) => $e->jsonSerialize(), $entries)]); + + }//end changelog() + + /** + * Start a revalidation (impact-analysis) run for a schema. + * + * @param int $id The schema id. + * + * @return JSONResponse The created run, or an error. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function revalidate(int $id): JSONResponse + { + try { + $this->schemaMapper->find($id); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Schema not found'], 404); + } + + $registerId = $this->resolveRegisterId(schemaId: $id); + if ($registerId === null) { + return new JSONResponse(['error' => 'No register contains this schema'], 422); + } + + $proposed = $this->request->getParam('proposedDefinition'); + if (is_array($proposed) === false) { + $proposed = null; + } + + try { + $run = $this->revalidationService->start( + schemaId: $id, + registerId: $registerId, + proposedDefinition: $proposed, + startedBy: $this->currentUid() + ); + } catch (SchemaRunConcurrencyException $e) { + return new JSONResponse(['error' => $e->getMessage()], 409); + } + + $this->jobList->add(\OCA\OpenRegister\BackgroundJob\SchemaRunJob::class, ['run_id' => $run->getId()]); + + return new JSONResponse($run->jsonSerialize(), 201); + + }//end revalidate() + + /** + * List runs for a schema. + * + * @param int $id The schema id. + * + * @return JSONResponse The runs. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function runs(int $id): JSONResponse + { + $runs = $this->runMapper->findBySchema( + schemaId: $id, + limit: $this->intParam(name: '_limit'), + offset: $this->intParam(name: '_offset') + ); + + return new JSONResponse(['results' => array_map(static fn($r) => $r->jsonSerialize(), $runs)]); + + }//end runs() + + /** + * Get a single run's status + report (with per-object entries). + * + * @param int $id The schema id. + * @param int $run The run id. + * + * @return JSONResponse The run + entries. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function run(int $id, int $run): JSONResponse + { + try { + $entity = $this->runMapper->find($run); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Run not found'], 404); + } + + if ($entity->getSchemaId() !== $id) { + return new JSONResponse(['error' => 'Run does not belong to this schema'], 404); + } + + $outcome = $this->request->getParam('outcome'); + if (is_string($outcome) === false) { + $outcome = null; + } + + $entries = $this->runEntryMapper->findByRun( + runId: $run, + outcome: $outcome, + limit: $this->intParam(name: '_limit'), + offset: $this->intParam(name: '_offset') + ); + + $payload = $entity->jsonSerialize(); + $payload['entries'] = array_map(static fn($e) => $e->jsonSerialize(), $entries); + + return new JSONResponse($payload); + + }//end run() + + /** + * Preview a migration plan against a bounded sample. + * + * @param int $id The schema id. + * + * @return JSONResponse Before/after pairs, or a plan-validation error. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function previewMigration(int $id): JSONResponse + { + try { + $this->schemaMapper->find($id); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Schema not found'], 404); + } + + $plan = $this->request->getParam('plan'); + if (is_array($plan) === false) { + return new JSONResponse(['error' => 'A "plan" array is required'], 422); + } + + $problems = $this->migrationService->validatePlan($plan); + if (count($problems) > 0) { + return new JSONResponse(['error' => 'Invalid migration plan', 'problems' => $problems], 422); + } + + $registerId = $this->resolveRegisterId(schemaId: $id); + if ($registerId === null) { + return new JSONResponse(['error' => 'No register contains this schema'], 422); + } + + $sample = (int) ($this->request->getParam('sample') ?? SchemaMigrationService::DEFAULT_PREVIEW_SAMPLE); + + $pairs = $this->migrationService->preview($id, $registerId, $plan, $sample); + + return new JSONResponse(['results' => $pairs]); + + }//end previewMigration() + + /** + * Execute a migration plan over a schema's population (background). + * + * @param int $id The schema id. + * + * @return JSONResponse The created run, or an error. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function migrate(int $id): JSONResponse + { + try { + $this->schemaMapper->find($id); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Schema not found'], 404); + } + + $plan = $this->request->getParam('plan'); + if (is_array($plan) === false) { + return new JSONResponse(['error' => 'A "plan" array is required'], 422); + } + + $registerId = $this->resolveRegisterId(schemaId: $id); + if ($registerId === null) { + return new JSONResponse(['error' => 'No register contains this schema'], 422); + } + + $options = $this->request->getParam('options'); + if (is_array($options) === false) { + $options = []; + } + + try { + $run = $this->migrationService->start( + schemaId: $id, + registerId: $registerId, + plan: $plan, + options: $options, + startedBy: $this->currentUid() + ); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(['error' => 'Invalid migration plan', 'problems' => [$e->getMessage()]], 422); + } catch (SchemaRunConcurrencyException $e) { + return new JSONResponse(['error' => $e->getMessage()], 409); + } + + $this->jobList->add(\OCA\OpenRegister\BackgroundJob\SchemaRunJob::class, ['run_id' => $run->getId()]); + + return new JSONResponse($run->jsonSerialize(), 201); + + }//end migrate() + + /** + * Roll a migration run back. + * + * @param int $id The schema id. + * @param int $run The migration run id. + * + * @return JSONResponse The rolled-back run, or an error. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function rollback(int $id, int $run): JSONResponse + { + try { + $entity = $this->runMapper->find($run); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Run not found'], 404); + } + + if ($entity->getSchemaId() !== $id) { + return new JSONResponse(['error' => 'Run does not belong to this schema'], 404); + } + + try { + $result = $this->migrationService->rollback($run, $this->currentUid()); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(['error' => $e->getMessage()], 422); + } catch (SchemaRunConcurrencyException $e) { + return new JSONResponse(['error' => $e->getMessage()], 409); + } + + return new JSONResponse($result->jsonSerialize()); + + }//end rollback() + + /** + * Resolve a register id that contains the given schema. + * + * @param int $schemaId The schema id. + * + * @return int|null A register id, or null when none contains the schema. + */ + private function resolveRegisterId(int $schemaId): ?int + { + $explicit = $this->request->getParam('registerId'); + if ($explicit !== null && is_numeric($explicit) === true) { + return (int) $explicit; + } + + $registers = $this->registerMapper->findAll(_rbac: false, _multitenancy: false); + foreach ($registers as $register) { + $schemas = array_map('strval', ($register->getSchemas() ?? [])); + if (in_array((string) $schemaId, $schemas, true) === true) { + return (int) $register->getId(); + } + } + + return null; + + }//end resolveRegisterId() + + /** + * Read an optional integer query parameter. + * + * @param string $name The parameter name. + * + * @return int|null The value, or null when absent. + */ + private function intParam(string $name): ?int + { + $value = $this->request->getParam($name); + if ($value === null || is_numeric($value) === false) { + return null; + } + + return (int) $value; + + }//end intParam() + + /** + * The current user id, or null. + * + * @return string|null The uid. + */ + private function currentUid(): ?string + { + $user = $this->userSession->getUser(); + if ($user !== null) { + return $user->getUID(); + } + + return null; + + }//end currentUid() +}//end class diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php index 01c57cfa6e..af3bcbd356 100644 --- a/lib/Controller/SchemasController.php +++ b/lib/Controller/SchemasController.php @@ -25,8 +25,10 @@ namespace OCA\OpenRegister\Controller; use Exception; +use RuntimeException; use DateTime; use GuzzleHttp\Exception\GuzzleException; +use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Db\MagicMapper; @@ -34,10 +36,19 @@ use OCA\OpenRegister\Service\OrganisationService; use OCA\OpenRegister\Service\Schemas\SchemaCacheHandler; use OCA\OpenRegister\Service\Schemas\FacetCacheHandler; +use OCA\OpenRegister\Service\Schema\SchemaVersioningService; +use OCA\OpenRegister\Service\JsonLd\JsonLdContextService; +use OCA\OpenRegister\Service\SemanticTypeResolver; +use OCA\OpenRegister\Exception\BreakingSchemaChangeException; +use OCA\OpenRegister\Service\SchemaDeletionService; use OCA\OpenRegister\Service\SchemaService; +use OCA\OpenRegister\Service\SchemaImport\ImportOptions; +use OCA\OpenRegister\Service\SchemaImport\SchemaImportService; +use OCA\OpenRegister\Exception\SchemaImportException; use OCA\OpenRegister\Service\UploadService; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\DB\Exception as DBException; @@ -69,34 +80,44 @@ * * @psalm-suppress UnusedClass * - * @SuppressWarnings(PHPMD.ExcessiveClassLength) REST controllers have many endpoints; extraction into sub-controllers would break the route registration. - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) REST controllers have many endpoints; extraction into sub-controllers would break the route registration. - * @SuppressWarnings(PHPMD.TooManyPublicMethods) REST controllers have many endpoints; extraction into sub-controllers would break the route registration. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) NC AppFramework controller DI requires injecting framework + RBAC + audit + domain services, each used in separate endpoint groups. + * @SuppressWarnings(PHPMD.ExcessiveClassLength) REST controllers have many endpoints; extraction + * into sub-controllers would break the route registration. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) REST controllers have many endpoints; extraction + * into sub-controllers would break the route registration. + * @SuppressWarnings(PHPMD.TooManyPublicMethods) REST controllers have many endpoints; extraction + * into sub-controllers would break the route registration. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) NC AppFramework controller DI requires injecting + * framework + RBAC + audit + domain services, each used in separate endpoint groups. * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/openapi-generation/spec.md */ class SchemasController extends Controller { + use \OCA\OpenRegister\Controller\Trait\HandlesExceptionsTrait; + /** * Constructor * * Initializes controller with required dependencies for schema operations. * Calls parent constructor to set up base controller functionality. * - * @param string $appName Application name - * @param IRequest $request HTTP request object - * @param IAppConfig $config App configuration for settings - * @param SchemaMapper $schemaMapper Schema mapper for database operations - * @param MagicMapper $objectEntityMapper Object entity mapper for object queries - * @param UploadService $uploadService Upload service for file uploads - * @param AuditTrailMapper $auditTrailMapper Audit trail mapper for log statistics - * @param OrganisationService $organisationService Organisation service for multi-tenancy - * @param SchemaCacheHandler $schemaCacheService Schema cache handler for caching operations - * @param FacetCacheHandler $facetCacheSvc Schema facet cache service for facet caching - * @param SchemaService $schemaService Schema service for exploration operations - * @param LoggerInterface $logger Logger for error tracking - * @param ContainerInterface $container Container for lazy loading services + * @param string $appName Application name + * @param IRequest $request HTTP request object + * @param IAppConfig $config App configuration for settings + * @param SchemaMapper $schemaMapper Schema mapper for database operations + * @param MagicMapper $objectEntityMapper Object entity mapper for object queries + * @param UploadService $uploadService Upload service for file uploads + * @param AuditTrailMapper $auditTrailMapper Audit trail mapper for log statistics + * @param OrganisationService $organisationService Organisation service for multi-tenancy + * @param SchemaCacheHandler $schemaCacheService Schema cache handler for caching operations + * @param FacetCacheHandler $facetCacheSvc Schema facet cache service for facet caching + * @param SchemaService $schemaService Schema service for exploration operations + * @param LoggerInterface $logger Logger for error tracking + * @param ContainerInterface $container Container for lazy loading services + * @param SchemaVersioningService $schemaVersioningService Schema versioning service for version operations + * @param JsonLdContextService $jsonLdContextService JSON-LD context service + * @param SchemaImportService $schemaImportService Schema import service for importing schemas + * @param SemanticTypeResolver $semanticTypeResolver Semantic-type → schema resolver (cross-app references) * * @return void * @@ -115,7 +136,11 @@ public function __construct( private readonly FacetCacheHandler $facetCacheSvc, private readonly SchemaService $schemaService, private readonly LoggerInterface $logger, - private readonly ContainerInterface $container + private readonly ContainerInterface $container, + private readonly SchemaVersioningService $schemaVersioningService, + private readonly ?JsonLdContextService $jsonLdContextService=null, + private readonly ?SchemaImportService $schemaImportService=null, + private readonly ?SemanticTypeResolver $semanticTypeResolver=null ) { // Call parent constructor to initialize base controller. parent::__construct(appName: $appName, request: $request); @@ -151,9 +176,10 @@ public function __construct( * configuration: array|null|string, allOf: array|null, * oneOf: array|null, anyOf: array|null}>}, array> * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple optional extend/pagination/filter parameters each add one branch. - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple optional extend/pagination/filter parameters each add one branch. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Handling pagination + filtering + extend + stats in one NC controller action is idiomatic; extracting helpers would obscure the flow. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple optional extend/pagination/filter parameters each add one branch. + * @SuppressWarnings(PHPMD.NPathComplexity) Multiple optional extend/pagination/filter parameters each add one branch. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Handling pagination + filtering + extend + stats + * in one NC controller action is idiomatic; extracting helpers would obscure the flow. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-7 */ @@ -208,17 +234,15 @@ public function index(): JSONResponse // Read-visibility guard: this endpoint is @PublicPage so it stays // reachable when OpenRegister is restricted to a user group. Anonymous - // callers may only see PUBLISHED schemas; authenticated users are - // unaffected. Visibility is derived from server-side published/ - // depublished entity state, never from client-supplied parameters. + // callers may only see schemas whose RBAC authorization grants read + // access to the `public` group; authenticated users are unaffected. + // Visibility is derived from server-side authorization rules, never + // from client-supplied parameters. if ($this->isAnonymousRequest() === true) { $schemas = array_values( array_filter( $schemas, - fn($schema) => $this->isPublishedEntity( - published: $schema->getPublished(), - depublished: $schema->getDepublished() - ) + fn($schema) => $this->isPublicReadable(authorization: $schema->getAuthorization()) ) ); } @@ -285,7 +309,9 @@ function ($schema) { * @PublicPage * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Handles _extend, _count, RBAC, 404, and several response-shaping branches; each is a required rendering path that cannot be extracted without splitting the HTTP contract. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Handles _extend, _count, RBAC, 404, and several + * response-shaping branches; each is a required rendering path that cannot be extracted without + * splitting the HTTP contract. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-7 */ @@ -300,13 +326,11 @@ public function show($id): JSONResponse $schema = $this->schemaMapper->find(id: $id, _extend: [], _multitenancy: false); // Read-visibility guard (@PublicPage): an anonymous caller may only - // view a PUBLISHED schema. Derived from server-side published/ - // depublished entity state, never from client-supplied parameters. + // view a schema whose RBAC authorization grants read access to the + // `public` group. Derived from server-side authorization rules, + // never from client-supplied parameters. if ($this->isAnonymousRequest() === true - && $this->isPublishedEntity( - published: $schema->getPublished(), - depublished: $schema->getDepublished() - ) === false + && $this->isPublicReadable(authorization: $schema->getAuthorization()) === false ) { return new JSONResponse(data: ['error' => 'Authentication required'], statusCode: 401); } @@ -353,10 +377,53 @@ public function show($id): JSONResponse 'error_message' => $e->getMessage(), ] ); - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end show() + /** + * Validate the optional `configuration.jsonld` vocabulary-mapping block. + * + * Term values must be absolute IRIs or compact terms resolvable against a + * declared `@vocab`. Returns a 400 JSONResponse describing the problem when + * the block is invalid, or null when valid / absent (json-ld-output). + * + * @param array $data The incoming schema request data. + * + * @return JSONResponse|null A 400 response when invalid, else null. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + private function validateJsonLdMapping(array $data): ?JSONResponse + { + if ($this->jsonLdContextService === null) { + return null; + } + + $configuration = ($data['configuration'] ?? null); + if (is_array($configuration) === false) { + return null; + } + + $jsonld = ($configuration['jsonld'] ?? null); + if (is_array($jsonld) === false) { + return null; + } + + $errors = $this->jsonLdContextService->validateMapping(jsonld: $jsonld); + if (empty($errors) === true) { + return null; + } + + return new JSONResponse( + data: [ + 'error' => 'Invalid jsonld mapping in schema configuration', + 'errors' => $errors, + ], + statusCode: 400 + ); + }//end validateJsonLdMapping() + /** * Creates a new schema * @@ -366,10 +433,11 @@ public function show($id): JSONResponse * * @NoCSRFRequired * - * @SuppressWarnings(PHPMD.StaticAccess) DatabaseConstraintException::fromDatabaseException is a named constructor — no DI alternative. - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple message-substring checks for error classification; each adds one branch. - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple message-substring checks for error classification; each adds one branch. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Error-classification block at the end is repetitive but intentional; extracting it would not reduce cognitive load. + * @SuppressWarnings(PHPMD.StaticAccess) DatabaseConstraintException::fromDatabaseException is a named constructor — no DI alternative. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple message-substring checks for error classification; each adds one branch. + * @SuppressWarnings(PHPMD.NPathComplexity) Multiple message-substring checks for error classification; each adds one branch. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Error-classification block at the end is + * repetitive but intentional; extracting it would not reduce cognitive load. * * @return JSONResponse JSON response with created schema or error * @@ -378,6 +446,7 @@ public function show($id): JSONResponse * array> * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-7 + * @spec openspec/specs/json-ld-output/spec.md */ public function create(): JSONResponse { @@ -387,7 +456,7 @@ public function create(): JSONResponse if ($this->isCurrentUserAdmin() === false) { return new JSONResponse( data: ['error' => 'Only administrators may create schemas'], - statusCode: 403 + statusCode: Http::STATUS_FORBIDDEN ); } @@ -418,6 +487,12 @@ public function create(): JSONResponse unset($data['id']); } + // Validate the optional JSON-LD vocabulary-mapping block (json-ld-output). + $jsonLdError = $this->validateJsonLdMapping(data: $data); + if ($jsonLdError !== null) { + return $jsonLdError; + } + try { // Create a new schema from the data. $schema = $this->schemaMapper->createFromArray(object: $data); @@ -474,6 +549,7 @@ public function create(): JSONResponse || str_contains($e->getMessage(), 'format') === true || str_contains($e->getMessage(), 'Property at') === true || str_contains($e->getMessage(), 'authorization') === true + || str_contains($e->getMessage(), 'handoff') === true ) { // Return 400 Bad Request for validation errors with actual error message. return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 400); @@ -488,7 +564,7 @@ public function create(): JSONResponse } // Return 500 for other unexpected errors with actual error message. - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end create() @@ -506,7 +582,8 @@ public function create(): JSONResponse * @SuppressWarnings(PHPMD.StaticAccess) DatabaseConstraintException::fromDatabaseException is a named constructor — no DI alternative. * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple message-substring checks for error classification; each adds one branch. * @SuppressWarnings(PHPMD.NPathComplexity) Multiple message-substring checks for error classification; each adds one branch. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Error-classification block is repetitive but intentional; extracting it would not reduce cognitive load. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Error-classification block is repetitive but + * intentional; extracting it would not reduce cognitive load. * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * * @return JSONResponse JSON response with updated schema or error @@ -547,17 +624,80 @@ public function update(int $id): JSONResponse if ($this->checkSchemaManagePermission(schema: $existingSchema) === false) { return new JSONResponse( data: ['error' => 'User does not have permission to manage this schema'], - statusCode: 403 + statusCode: Http::STATUS_FORBIDDEN ); } + // Validate the optional JSON-LD vocabulary-mapping block (json-ld-output). + // Runs after the manage-permission check so an unauthorized caller can + // never probe mapping validation; an invalid mapping leaves the stored + // configuration unchanged (no save happens). + $jsonLdError = $this->validateJsonLdMapping(data: $data); + if ($jsonLdError !== null) { + return $jsonLdError; + } + // Capture prior authorization so a change can be audit-logged below. $oldSchemaAuth = $existingSchema->getAuthorization(); + // Schema versioning gate (schema-versioning-and-object-migration): + // classify the incoming definition against the stored one and refuse + // an unacknowledged breaking change with the structured HTTP 409 + // contract. Only runs when the update actually carries a definition. + $changeSet = null; + $acknowledged = ($this->request->getParam('acknowledgeBreaking') === true + || $this->request->getParam('acknowledgeBreaking') === 'true'); + if (isset($data['properties']) === true || isset($data['required']) === true) { + $proposedDefinition = [ + 'properties' => ($data['properties'] ?? $existingSchema->getProperties() ?? []), + 'required' => ($data['required'] ?? $existingSchema->getRequired() ?? []), + ]; + + $renames = []; + if (is_array($this->request->getParam('renames')) === true) { + $renames = $this->request->getParam('renames'); + } + + $changeSet = $this->schemaVersioningService->classify( + existing: $existingSchema, + newDefinition: $proposedDefinition, + renames: $renames + ); + + try { + $this->schemaVersioningService->enforceGate( + changeSet: $changeSet, + acknowledged: $acknowledged, + schemaId: $id + ); + } catch (BreakingSchemaChangeException $e) { + return new JSONResponse(data: $e->toResponse(), statusCode: 409); + } + + // Apply the classification-driven semantic version bump unless the + // caller pinned an explicit version. + if (isset($data['version']) === false && $changeSet->hasChanges() === true) { + $data['version'] = $this->schemaVersioningService->nextVersion( + existing: $existingSchema, + changeSet: $changeSet + ); + } + }//end if + try { // Update the schema with the provided data. $updatedSchema = $this->schemaMapper->updateFromArray(id: $id, object: $data); + // Record the classified changelog entry once the update applied. + if ($changeSet !== null) { + $this->schemaVersioningService->recordChangelog( + schemaId: $updatedSchema->getId(), + version: $updatedSchema->getVersion(), + changeSet: $changeSet, + acknowledged: $acknowledged + ); + } + // Log authorization change if authorization was modified. if (isset($data['authorization']) === true) { try { @@ -622,6 +762,7 @@ public function update(int $id): JSONResponse || str_contains($e->getMessage(), 'format') === true || str_contains($e->getMessage(), 'Property at') === true || str_contains($e->getMessage(), 'authorization') === true + || str_contains($e->getMessage(), 'handoff') === true ) { // Return 400 Bad Request for validation errors with actual error message. return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 400); @@ -636,7 +777,7 @@ public function update(int $id): JSONResponse } // Return 500 for other unexpected errors with actual error message. - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end update() @@ -651,6 +792,8 @@ public function update(int $id): JSONResponse * * @return JSONResponse JSON response with patched schema or error * + * @no-admin-idor-exempt Pure delegation to update(), which performs the checkSchemaManagePermission() guard; this method has no body of its own. + * * @psalm-return JSONResponse<200, Schema, * array>|JSONResponse<400|403|404|409|500, array{error: string}, * array> @@ -667,34 +810,53 @@ public function patch(int $id): JSONResponse /** * Deletes a schema * - * This method deletes a schema based on its ID. + * This method deletes a schema based on its ID. Three dispositions exist for a + * schema that still holds objects, and they are mutually exclusive: + * + * - no flag → HTTP 409 `{error: schema-has-objects, objectCount: N}` + * - `?deleteObjects=true` → CASCADE: audit + hard-delete every object, drop the + * magic table, then delete the schema. No orphans. + * - `?force=true` → legacy: delete the schema and ORPHAN its objects. + * Retained for API back-compat; never exposed in the UI. + * + * Passing both flags is HTTP 400 — an ambiguous destructive intent is refused + * rather than silently resolved. * * @param int $id The ID of the schema to delete * * @throws Exception If there is an error deleting the schema * - * @return JSONResponse An empty JSON response + * @return JSONResponse An empty JSON response, or the cascade result * * @NoAdminRequired * * @NoCSRFRequired * - * @psalm-return JSONResponse<200|409|500, array{error?: string}, array> - * - * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Force-flag and orphan-count branches are inherent to a safe delete endpoint. + * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Force/cascade dispositions and the orphan-count branch are inherent to a safe delete endpoint. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-7 + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md */ public function destroy(int $id): JSONResponse { // **DELETE SAFETY (runtime-schema-api)**: Count attached objects FIRST. - // If N > 0 and ?force=true is not set, refuse with HTTP 409 so the caller + // If N > 0 and no disposition flag is set, refuse with HTTP 409 so the caller // gets a structured error containing the orphan count and can decide // whether to escalate. A bare DELETE on a schema with objects is the // canonical foot-gun that this guard closes. - $forceParam = $this->request->getParam(key: 'force', default: null); - $force = (string) $forceParam === 'true' || $forceParam === true || $forceParam === '1'; + $force = $this->isFlagEnabled(key: 'force'); + $deleteObjects = $this->isFlagEnabled(key: 'deleteObjects'); + + if ($force === true && $deleteObjects === true) { + return new JSONResponse( + data: [ + 'error' => 'conflicting-delete-dispositions', + 'message' => 'force and deleteObjects are mutually exclusive: force orphans the objects, ' + .'deleteObjects removes them. Pick one.', + ], + statusCode: Http::STATUS_BAD_REQUEST + ); + } try { // Find the schema first (also validates existence and access). @@ -705,7 +867,7 @@ public function destroy(int $id): JSONResponse if ($this->checkSchemaManagePermission(schema: $schemaToDelete) === false) { return new JSONResponse( data: ['error' => 'User does not have permission to manage this schema'], - statusCode: 403 + statusCode: Http::STATUS_FORBIDDEN ); } @@ -715,56 +877,183 @@ public function destroy(int $id): JSONResponse // in the @self filter, and silently returns 0 on single-axis queries, // which would let DELETE silently succeed on schemas with objects. $objectStats = $this->objectEntityMapper->getStatistics(registerId: null, schemaId: $schemaToDelete->getId()); - $objectCount = (int) $objectStats['total']; + $objectCount = (int) ($objectStats['total'] ?? 0); - if ($objectCount > 0 && $force === false) { - // Refuse: structured 409 with the orphan count for the caller. - return new JSONResponse( - data: [ - 'error' => 'schema-has-objects', - 'objectCount' => $objectCount, - ], - statusCode: 409 - ); + if ($deleteObjects === true) { + return $this->cascadeDeleteSchema(schema: $schemaToDelete); } - if ($objectCount > 0 && $force === true) { - // Force-delete with audit trail at WARNING level: a misused force flag - // orphans every object referencing this schema, so log who did it. - $this->logger->warning( - message: '[SchemasController] Force-deleting schema with attached objects', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaId' => $schemaToDelete->getId(), - 'schemaSlug' => $schemaToDelete->getSlug(), - 'objectCount' => $objectCount, - ] - ); - } - - $this->schemaMapper->delete($schemaToDelete); - - // **CACHE INVALIDATION (runtime-schema-api)**: invalidate() is the - // canonical entry point — covers in-memory, persistent cache table, - // AND the request-scoped find cache on the mapper. - $this->schemaCacheService->invalidate(schemaId: $schemaToDelete->getId()); - $this->facetCacheSvc->invalidateForSchemaChange( - schemaId: $schemaToDelete->getId(), - operation: 'delete' + return $this->deleteSchemaKeepingObjects( + schema: $schemaToDelete, + objectCount: $objectCount, + force: $force ); - - // Return an empty response. - return new JSONResponse(data: []); } catch (\OCA\OpenRegister\Exception\ValidationException $e) { // Return 409 Conflict for cascade protection (objects still attached). return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 409); } catch (\Exception $e) { // Return 500 for other errors. - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end destroy() + /** + * Delete a schema WITHOUT deleting its objects — the no-flag and ?force=true paths. + * + * With objects attached and no force flag this refuses (HTTP 409). With ?force=true + * it deletes the schema and orphans the objects, which is unchanged legacy behaviour + * retained for API back-compat and never surfaced in the UI. + * + * @param Schema $schema The schema to delete (already authorized). + * @param int $objectCount The number of live objects referencing the schema. + * @param bool $force Whether ?force=true was passed. + * + * @throws Exception If the delete fails. + * + * @return JSONResponse Empty body on success, or the structured 409 refusal. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The force flag is the API-level disposition, mirrored from the endpoint. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md + */ + private function deleteSchemaKeepingObjects(Schema $schema, int $objectCount, bool $force): JSONResponse + { + if ($objectCount > 0 && $force === false) { + // Refuse: structured 409 with the orphan count for the caller. + return new JSONResponse( + data: [ + 'error' => 'schema-has-objects', + 'objectCount' => $objectCount, + ], + statusCode: 409 + ); + } + + if ($objectCount > 0) { + // Force-delete with audit trail at WARNING level: a misused force flag + // orphans every object referencing this schema, so log who did it. + $this->logger->warning( + message: '[SchemasController] Force-deleting schema with attached objects', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schema->getId(), + 'schemaSlug' => $schema->getSlug(), + 'objectCount' => $objectCount, + ] + ); + } + + // Only a deliberate ?force=true may bypass the mapper-level guard and orphan + // the objects. The magic table is deliberately LEFT IN PLACE on that path — + // its rows are the orphans, and dropping it would destroy them unaudited. + $this->schemaMapper->delete(entity: $schema, force: $force); + + if ($objectCount === 0) { + // Nothing referenced this schema, so reclaim its (empty) magic table + // instead of leaving an orphan table behind forever. Best effort. + $this->getSchemaDeletionService()->dropEmptyTablesForSchema(schema: $schema); + } + + $this->invalidateSchemaCaches(schemaId: (int) $schema->getId()); + + // Return an empty response. + return new JSONResponse(data: []); + }//end deleteSchemaKeepingObjects() + + /** + * Run the cascade disposition of DELETE /api/schemas/{id}. + * + * Phase 1 (transactional) audits and hard-deletes every object and then the schema + * itself; phase 2 drops the magic table post-commit. A phase-2 failure does NOT + * fail the request — the caller's intent is already satisfied — it is reported as + * `tableDropped: false`. See SchemaDeletionService for the full rationale. + * + * @param Schema $schema The schema to tear down (already authorized). + * + * @throws Exception If phase 1 fails; the caller maps it to HTTP 500 and nothing is deleted. + * + * @return JSONResponse The cascade result. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md + */ + private function cascadeDeleteSchema(Schema $schema): JSONResponse + { + $schemaId = (int) $schema->getId(); + $result = $this->getSchemaDeletionService()->cascadeDeleteSchema(schema: $schema); + + $this->invalidateSchemaCaches(schemaId: $schemaId); + + return new JSONResponse( + data: [ + 'success' => true, + 'schemaId' => $schemaId, + 'deletedCount' => $result['deletedCount'], + 'deletedUuids' => $result['deletedUuids'], + 'tableDropped' => $result['tableDropped'], + ] + ); + }//end cascadeDeleteSchema() + + /** + * Invalidate every cache that holds a copy of a now-deleted schema. + * + * **CACHE INVALIDATION (runtime-schema-api)**: invalidate() is the canonical entry + * point — it covers the in-memory cache, the persistent cache table, AND the + * request-scoped find cache on the mapper. Runs on every disposition. + * + * @param int $schemaId The id of the deleted schema. + * + * @return void + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md + */ + private function invalidateSchemaCaches(int $schemaId): void + { + $this->schemaCacheService->invalidate(schemaId: $schemaId); + $this->facetCacheSvc->invalidateForSchemaChange( + schemaId: $schemaId, + operation: 'delete' + ); + }//end invalidateSchemaCaches() + + /** + * Read a boolean query flag from the request. + * + * Accepts `true`, `'true'` and `'1'` — the shapes a query string can deliver. + * + * @param string $key The query parameter name. + * + * @return bool True when the flag is explicitly enabled. + */ + private function isFlagEnabled(string $key): bool + { + $value = $this->request->getParam(key: $key, default: null); + + return ((string) $value === 'true' || $value === true || $value === '1'); + }//end isFlagEnabled() + + /** + * Resolve the schema deletion service. + * + * Lazily resolved from the container: it is only needed by the delete path, and + * SchemasController is constructed on every schema read. + * + * @throws \RuntimeException If the service cannot be resolved. + * + * @return SchemaDeletionService The service. + */ + private function getSchemaDeletionService(): SchemaDeletionService + { + $service = $this->container->get(SchemaDeletionService::class); + + if (($service instanceof SchemaDeletionService) === false) { + throw new RuntimeException('SchemaDeletionService could not be resolved'); + } + + return $service; + }//end getSchemaDeletionService() + /** * Updates an existing Schema object using a json text/string as input * @@ -782,9 +1071,12 @@ public function destroy(int $id): JSONResponse * * @NoCSRFRequired * + * @no-admin-idor-exempt Pure delegation to upload(), which performs the checkSchemaManagePermission() + * guard for update paths; this method has no body of its own. + * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function uploadUpdate(?int $id=null): JSONResponse { @@ -808,13 +1100,15 @@ public function uploadUpdate(?int $id=null): JSONResponse * * @NoCSRFRequired * - * @SuppressWarnings(PHPMD.StaticAccess) Uuid::v4 is a named constructor and DatabaseConstraintException::fromDatabaseException is a factory — no DI alternatives. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) JSON-upload path merges insert + update branches; splitting would duplicate error classification. + * @SuppressWarnings(PHPMD.StaticAccess) Uuid::v4 is a named constructor and + * DatabaseConstraintException::fromDatabaseException is a factory — no DI alternatives. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) JSON-upload path merges insert + update branches; + * splitting would duplicate error classification. * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple message-substring checks for error classification; each adds one branch. * @SuppressWarnings(PHPMD.NPathComplexity) Multiple message-substring checks for error classification; each adds one branch. * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function upload(?int $id=null): JSONResponse { @@ -829,20 +1123,18 @@ public function upload(?int $id=null): JSONResponse // SECURITY (H3): gate schema uploads on appropriate permissions. // Updating an existing schema requires manage-permission (same as update/destroy). // Creating a new schema requires admin (same as create). - if ($id !== null) { - if ($this->checkSchemaManagePermission(schema: $schema) === false) { - return new JSONResponse( - data: ['error' => 'You do not have permission to update this schema'], - statusCode: 403 - ); - } - } else { - if ($this->isCurrentUserAdmin() === false) { - return new JSONResponse( - data: ['error' => 'Admin privileges required to upload new schemas'], - statusCode: 403 - ); - } + if ($id !== null && $this->checkSchemaManagePermission(schema: $schema) === false) { + return new JSONResponse( + data: ['error' => 'You do not have permission to update this schema'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + + if ($id === null && $this->isCurrentUserAdmin() === false) { + return new JSONResponse( + data: ['error' => 'Admin privileges required to upload new schemas'], + statusCode: Http::STATUS_FORBIDDEN + ); } // Get the uploaded JSON data. @@ -852,6 +1144,17 @@ public function upload(?int $id=null): JSONResponse return $phpArray; } + // Dialect resolution: explicit `dialect` parameter wins, otherwise the + // document is sniffed. Schema.org / GGM inputs are mapped through the + // standards importers; json-schema / openapi flow on unchanged. + // Undetectable input fails with HTTP 422 instead of being mis-ingested. + $dialectResult = $this->applyDialect(document: $phpArray); + if ($dialectResult instanceof JSONResponse) { + return $dialectResult; + } + + $phpArray = $dialectResult; + // Set default title if not provided or empty. if (empty($phpArray['title']) === true) { $phpArray['title'] = 'New Schema'; @@ -927,6 +1230,7 @@ public function upload(?int $id=null): JSONResponse || str_contains($e->getMessage(), 'format') === true || str_contains($e->getMessage(), 'Property at') === true || str_contains($e->getMessage(), 'authorization') === true + || str_contains($e->getMessage(), 'handoff') === true ) { // Return 400 Bad Request for validation errors with actual error message. return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 400); @@ -941,7 +1245,7 @@ public function upload(?int $id=null): JSONResponse } // Return 500 for other unexpected errors with actual error message. - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end upload() @@ -958,13 +1262,17 @@ public function upload(?int $id=null): JSONResponse * * @NoCSRFRequired * + * @no-admin-idor-exempt Read-only export of a shared schema *definition* (not a user-owned object); + * schema metadata is public-readable by design, matching the @PublicPage index/show endpoints. + * No per-user data, no IDOR vector. + * * @psalm-return JSONResponse<200, Schema, * array>|JSONResponse<404, * array{error: 'Schema not found'}, array> * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function download(int $id): JSONResponse { @@ -995,11 +1303,15 @@ public function download(int $id): JSONResponse * * @NoCSRFRequired * + * @no-admin-idor-exempt Read-only discovery of which shared schema *definitions* reference this one; + * returns schema metadata only (public-readable by design, like @PublicPage index/show). + * No per-user data, no IDOR vector. + * * @return JSONResponse JSON response with related schemas * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function related(int|string $id): JSONResponse { @@ -1048,7 +1360,7 @@ public function related(int|string $id): JSONResponse return new JSONResponse(data: ['error' => 'Schema not found'], statusCode: 404); } catch (Exception $e) { // Return a 500 error for other exceptions. - return new JSONResponse(data: ['error' => 'Internal server error: '.$e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end related() @@ -1063,11 +1375,15 @@ public function related(int|string $id): JSONResponse * * @NoCSRFRequired * + * @no-admin-idor-exempt Read-only aggregate counts for a shared schema *definition* (object totals, + * not object contents); exposes no per-object or per-user data, matching the @PublicPage read posture. + * No IDOR vector. + * * @return JSONResponse JSON response with schema statistics * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md#requirement-per-entity-statistics-and-endpoint-delivery-log-api */ public function stats(int $id): JSONResponse { @@ -1083,20 +1399,27 @@ public function stats(int $id): JSONResponse } // Get detailed object statistics for this schema using the existing method. + // Default every key: mapper variants (and mocked test doubles) may + // return partial maps, which otherwise emits "Undefined array key" + // PHP warnings for empty schemas. $objectStats = $this->objectEntityMapper->getStatistics(registerId: null, schemaId: $id); + $objectStats = array_merge( + ['total' => 0, 'invalid' => 0, 'deleted' => 0, 'locked' => 0, 'size' => 0], + $objectStats + ); // Calculate comprehensive statistics for this schema. $stats = [ - 'objectCount' => $objectStats['total'], + 'objectCount' => ($objectStats['total'] ?? 0), // Keep for backward compatibility. - 'objects_count' => $objectStats['total'], + 'objects_count' => ($objectStats['total'] ?? 0), // Alternative field name for compatibility. 'objects' => [ - 'total' => $objectStats['total'], - 'invalid' => $objectStats['invalid'], - 'deleted' => $objectStats['deleted'], - 'locked' => $objectStats['locked'], - 'size' => $objectStats['size'], + 'total' => ($objectStats['total'] ?? 0), + 'invalid' => ($objectStats['invalid'] ?? 0), + 'deleted' => ($objectStats['deleted'] ?? 0), + 'locked' => ($objectStats['locked'] ?? 0), + 'size' => ($objectStats['size'] ?? 0), ], 'logs' => $this->auditTrailMapper->getStatistics(registerId: null, schemaId: $id), 'files' => ['total' => 0, 'size' => 0], @@ -1108,7 +1431,7 @@ public function stats(int $id): JSONResponse } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { return new JSONResponse(data: ['error' => 'Schema not found'], statusCode: 404); } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end stats() @@ -1130,11 +1453,28 @@ public function stats(int $id): JSONResponse * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function explore(int $id): JSONResponse { try { + // Authorization: exploration scans the schema's object population to + // surface undefined properties — a schema-introspection operation that + // MUST require manage permission (same authority as editing the schema), + // so an arbitrary user cannot probe another schema's data shape (IDOR). + try { + $existingSchema = $this->schemaMapper->find($id); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Schema not found'], statusCode: 404); + } + + if ($this->checkSchemaManagePermission(schema: $existingSchema) === false) { + return new JSONResponse( + data: ['error' => 'User does not have permission to manage this schema'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + $this->logger->info( message: '[SchemasController] Starting schema exploration for schema ID: '.$id, context: ['file' => __FILE__, 'line' => __LINE__] @@ -1153,7 +1493,7 @@ public function explore(int $id): JSONResponse message: '[SchemasController] Schema exploration failed: '.$e->getMessage(), context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end explore() @@ -1173,11 +1513,28 @@ public function explore(int $id): JSONResponse * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-4 + * @spec openspec/specs/openapi-generation/spec.md#requirement-schema-authoring-sub-resources-and-meta-entity-operational-endpoints */ public function updateFromExploration(int $id): JSONResponse { try { + // Authorization: writing exploration results back into a schema's + // definition is a schema mutation and MUST require manage permission, + // exactly like update()/upload(). Without this guard any authenticated + // user could rewrite an arbitrary schema's properties (IDOR). + try { + $existingSchema = $this->schemaMapper->find($id); + } catch (DoesNotExistException $e) { + return new JSONResponse(data: ['error' => 'Schema not found'], statusCode: 404); + } + + if ($this->checkSchemaManagePermission(schema: $existingSchema) === false) { + return new JSONResponse( + data: ['error' => 'User does not have permission to manage this schema'], + statusCode: Http::STATUS_FORBIDDEN + ); + } + // Get property updates from request. $propertyUpdates = $this->request->getParam(key: 'properties', default: []); @@ -1216,10 +1573,109 @@ public function updateFromExploration(int $id): JSONResponse message: '[SchemasController] Failed to update schema from exploration: '.$e->getMessage(), context: ['file' => __FILE__, 'line' => __LINE__] ); - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + return $this->errorResponse(e: $e); }//end try }//end updateFromExploration() + /** + * Resolve a canonical semantic-type URI to the installed provider schema. + * + * Discovery endpoint for cross-app semantic references (ADR-048). Given a + * `uri` query parameter (an absolute semantic-type IRI such as + * `https://schema.org/Organization`), returns the register + schema slugs of + * the installed schema that implements it, so the frontend can build an + * object picker over the provider schema. When no installed schema provides + * the type, returns `{ resolved: false }` with HTTP 200 — never 500 — so a + * consuming form degrades gracefully to a disabled field. + * + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @PublicPage + * + * @return JSONResponse `{ resolved: bool, register, registerSlug, schema, schemaSlug, appId? }` + * or `{ resolved: false }`. + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + * (Requirement: Resolution is null-safe across installed schemas) + */ + public function resolveByImplements(): JSONResponse + { + $uri = (string) ($this->request->getParam('uri', '')); + if ($uri === '' || $this->semanticTypeResolver === null) { + return new JSONResponse(['resolved' => false]); + } + + // Optional consuming-register hint biases the tie-break; never required. + $consumingRegisterId = null; + $rawRegister = $this->request->getParam('register'); + if ($rawRegister !== null && is_numeric($rawRegister) === true) { + $consumingRegisterId = (int) $rawRegister; + } + + try { + $schema = $this->semanticTypeResolver->resolveSchemaByImplements( + uri: $uri, + consumingRegisterId: $consumingRegisterId + ); + } catch (\Throwable $e) { + // Resolution is contractually null-safe; log and degrade, never 500. + $this->logger->warning( + message: '[SchemasController] resolveByImplements failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'uri' => $uri] + ); + return new JSONResponse(['resolved' => false]); + } + + if ($schema === null) { + return new JSONResponse(['resolved' => false]); + } + + $register = $this->semanticTypeResolver->findRegisterForSchema(schema: $schema); + + $payload = [ + 'resolved' => true, + 'schema' => $schema->getId(), + 'schemaSlug' => $schema->getSlug(), + ]; + + return new JSONResponse(array_merge($payload, $this->registerPayload(register: $register))); + + }//end resolveByImplements() + + /** + * Build the register portion of a {@see self::resolveByImplements()} payload. + * + * Extracted so the discovery endpoint stays within complexity limits; + * behaviour is identical. When the schema has no owning register the + * register/slug keys are explicitly null; otherwise the register id + slug + * are returned, plus `appId` when the register names an owning app. + * + * @param Register|null $register The owning register, or null when none. + * + * @return array The register keys to merge into the payload. + */ + private function registerPayload(?Register $register): array + { + if ($register === null) { + return ['register' => null, 'registerSlug' => null]; + } + + $payload = [ + 'register' => $register->getId(), + 'registerSlug' => $register->getSlug(), + ]; + + $appId = $register->getApplication(); + if (is_string($appId) === true && $appId !== '') { + $payload['appId'] = $appId; + } + + return $payload; + + }//end registerPayload() + /** * Whether the current request has no resolved Nextcloud user (anonymous). * @@ -1242,31 +1698,132 @@ private function isAnonymousRequest(): bool }//end isAnonymousRequest() /** - * Whether an entity is currently published. + * Whether an entity is anonymously readable via RBAC authorization. + * + * A resource is visible to anonymous callers when its authorization block + * grants `read` access to the `public` group. This replaces the former + * published/depublished column gate: anonymous publication is an RBAC + * concern, expressed as a `public`-group read rule on the entity. + * + * @param array|null $authorization The entity's authorization block, or null. + * + * @return bool True if the `public` group has read access. + */ + private function isPublicReadable(?array $authorization): bool + { + if (is_array($authorization) === false) { + return false; + } + + $readGroups = ($authorization['read'] ?? []); + return is_array($readGroups) === true && in_array('public', $readGroups, true) === true; + + }//end isPublicReadable() + + /** + * Resolve the dialect of an uploaded schema document and map it. + * + * Explicit `dialect` parameter wins; otherwise the document is sniffed. + * `json-schema` and `openapi` documents flow through unchanged (existing + * behaviour, now labelled). `schema.org` and `ggm` documents are mapped + * through the standards importers into a register-schema array. Input that + * matches no dialect (and carries no explicit one) fails with HTTP 422. * - * A resource is published when its `published` timestamp is set and it has - * not since been depublished. Both values come from persisted entity state. + * When the standards import service is unavailable (DI not wired), the + * document is returned unchanged so the legacy JSON-Schema path is + * preserved. * - * @param \DateTime|null $published The published timestamp, or null. - * @param \DateTime|null $depublished The depublished timestamp, or null. + * @param array $document The decoded upload document. * - * @return bool True if the entity is published and not depublished. + * @return array|JSONResponse The (possibly mapped) schema array, or an error response. + * + * @spec openspec/specs/schema-import/spec.md + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) One branch per supported dialect. */ - private function isPublishedEntity(?\DateTime $published, ?\DateTime $depublished): bool + private function applyDialect(array $document): array | JSONResponse { - return $published !== null && $depublished === null; + if ($this->schemaImportService === null) { + return $document; + } + + $explicit = $this->request->getParam('dialect'); + if (is_string($explicit) === false || $explicit === '') { + $explicit = null; + } + + try { + $dialect = $this->schemaImportService->resolveUploadDialect(document: $document, explicitDialect: $explicit); + } catch (SchemaImportException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: $e->getHttpStatus()); + } - }//end isPublishedEntity() + // Json-schema and openapi documents are ingested as-is (unchanged). + if ($dialect === 'json-schema' || $dialect === 'openapi') { + return $document; + } + + // Standards dialects are mapped via the importer. The reference is the + // explicit `reference` parameter, else the document's own type marker. + $reference = $this->resolveDialectReference(dialect: $dialect, document: $document); + if ($reference === null) { + return new JSONResponse( + data: ['error' => 'A "reference" identifying the '.$dialect.' type/objecttype to import is required.'], + statusCode: Http::STATUS_BAD_REQUEST + ); + } + + try { + if ($dialect === 'ggm' && isset($document['objecttypen']) === true) { + // Treat the uploaded body as a normalised GGM intermediate. + $imported = $this->schemaImportService->importGgmUpload( + normalised: $document, + reference: $reference, + options: ImportOptions::fromArray($this->request->getParams()), + sourceLabel: 'upload' + ); + } else { + $imported = $this->schemaImportService->import( + dialect: $dialect, + reference: $reference, + options: ImportOptions::fromArray($this->request->getParams()) + ); + } + } catch (SchemaImportException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: $e->getHttpStatus()); + } + + return $imported->toSchemaArray(); + + }//end applyDialect() /** - * Check whether the currently authenticated user is a Nextcloud administrator. + * Resolve the type reference for a standards-dialect upload. + * + * @param string $dialect The resolved dialect. + * @param array $document The decoded document. * - * Used to gate schema creation, where there is no existing entity whose - * manage-authorization block could be consulted. User session and group - * manager are resolved lazily from the container to avoid widening the - * constructor signature. + * @return string|null The reference, or null when none can be determined. + */ + private function resolveDialectReference(string $dialect, array $document): ?string + { + $reference = $this->request->getParam('reference'); + if (is_string($reference) === true && $reference !== '') { + return $reference; + } + + if ($dialect === 'schema.org' && isset($document['@type']) === true && is_string($document['@type']) === true) { + return $document['@type']; + } + + return null; + + }//end resolveDialectReference() + + /** + * Check if the current user is a Nextcloud administrator. * - * @return bool True if a user is signed in and belongs to the admin group. + * @return bool True when the current user is an admin. */ private function isCurrentUserAdmin(): bool { diff --git a/lib/Controller/ScopesController.php b/lib/Controller/ScopesController.php index b1cf463c49..e99450c41c 100644 --- a/lib/Controller/ScopesController.php +++ b/lib/Controller/ScopesController.php @@ -17,9 +17,9 @@ * @category Controller * @package OCA\OpenRegister\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Controller/SearchController.php b/lib/Controller/SearchController.php index d01491522f..0d962110b9 100644 --- a/lib/Controller/SearchController.php +++ b/lib/Controller/SearchController.php @@ -19,7 +19,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-92 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-dutch-language-search-support-i18n */ namespace OCA\OpenRegister\Controller; @@ -27,13 +27,13 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; -use OCA\OpenRegister\Service\IndexService; +use OCA\OpenRegister\Service\ObjectService; /** * SearchController handles search operations * * Controller for handling search operations in the application. - * Provides functionality to search across objects using SOLR search service. + * Provides functionality to search across objects using the database search path. * Supports query processing, pagination, and result formatting. * * @category Controller @@ -51,60 +51,47 @@ */ class SearchController extends Controller { - - /** - * The SOLR search service - * - * Handles SOLR-based search operations for objects. - * - * @var IndexService Index search service instance - */ - private readonly IndexService $indexService; - /** * Constructor for the SearchController * - * Initializes controller with SOLR search service for object search operations. + * Initializes controller with object service for database search operations. * Calls parent constructor to set up base controller functionality. * - * @param string $appName The name of the app - * @param IRequest $request The HTTP request object - * @param IndexService $indexService The index search service instance + * @param string $appName The name of the app + * @param IRequest $request The HTTP request object + * @param ObjectService $objectService The object service instance * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function __construct( string $appName, IRequest $request, - IndexService $indexService + private readonly ObjectService $objectService ) { // Call parent constructor to initialize base controller. parent::__construct(appName: $appName, request: $request); - - // Store index service for search operations. - $this->indexService = $indexService; }//end __construct() /** - * Handles search requests and forwards them to the SOLR search service + * Handles search requests and delegates to the database search path * - * Processes search query, performs SOLR search, and formats results for JSON response. + * Processes search query, performs database object search, and formats results for JSON response. * Supports pagination via offset and limit parameters. - * Returns formatted search results with facets and total count. + * Returns formatted search results with total count. * - * @return JSONResponse Search results with facets and total count. + * @return JSONResponse Search results with total count. * * @NoAdminRequired * * @NoCSRFRequired * - * @psalm-return JSONResponse<200, array{results: array, total: 0|mixed, - * facets: array|mixed}, array> + * @psalm-return JSONResponse<200, array{results: array, total: 0|int, + * facets: array}, array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function search(): JSONResponse { @@ -115,26 +102,28 @@ public function search(): JSONResponse // This handles comma-separated values, arrays, and case-insensitive matching. $processedQuery = $this->processSearchQuery(query: $query); - // Step 3: Build search parameters for SOLR query. + // Step 3: Build search parameters for the database search path. // Note: This is a simplified search endpoint. For full Nextcloud search integration, - // Use the ObjectsProvider which implements IFilteringProvider. + // use the ObjectsProvider which implements IFilteringProvider. $searchParams = [ - 'q' => $processedQuery, - 'start' => (int) ($this->request->getParam('offset', 0)), - 'rows' => (int) ($this->request->getParam('limit', 25)), + '_search' => $processedQuery, + '_page' => (int) floor(((int) $this->request->getParam('offset', 0)) / max(1, (int) $this->request->getParam('limit', 25))) + 1, + '_limit' => (int) ($this->request->getParam('limit', 25)), ]; - // Step 4: Perform search using SOLR service. - // Returns: ['objects' => [], 'facets' => [], 'total' => int, 'execution_time_ms' => float]. - $results = $this->indexService->searchObjects($searchParams); + // Step 4: Perform search using ObjectService database path. + // Returns: ['results' => ObjectEntity[], 'total' => int, '@self' => [...]]. + $paginatedResult = $this->objectService->searchObjectsPaginated(query: $searchParams); // Step 5: Format search results for JSON response. - // Extract relevant fields from each object and standardize format. + // Extract relevant fields from each ObjectEntity and standardize format. $formattedResults = array_map( // phpcs:ignore Squiz.Commenting.BlockComment.NoEmptyLineBefore -- Empty line conflicts with "first argument must be on line after opening parenthesis" rule /* * Format search result item. * + * @param \OCA\OpenRegister\Db\ObjectEntity|array $object + * * @return (mixed|null|string)[] * * @psalm-return array{ @@ -146,24 +135,44 @@ public function search(): JSONResponse * } */ - function (array $object): array { + function ($object): array { + if (is_object($object) === true && method_exists($object, 'getUuid') === true) { + $name = null; + if (method_exists($object, 'getName') === true) { + $name = $object->getName(); + } + + return [ + 'id' => $object->getUuid(), + 'name' => $name ?? 'Unknown', + 'type' => 'object', + 'url' => null, + 'source' => 'openregister', + ]; + } + + $objectArr = []; + if (is_array($object) === true) { + $objectArr = $object; + } + return [ - 'id' => $object['uuid'] ?? $object['id'] ?? null, - 'name' => $object['name'] ?? $object['@self']['name'] ?? 'Unknown', + 'id' => $objectArr['uuid'] ?? $objectArr['id'] ?? null, + 'name' => $objectArr['name'] ?? $objectArr['@self']['name'] ?? 'Unknown', 'type' => 'object', - 'url' => $object['url'] ?? null, + 'url' => $objectArr['url'] ?? null, 'source' => 'openregister', ]; }, - $results['objects'] ?? [] + $paginatedResult['results'] ?? [] ); // Step 6: Return formatted search results with metadata. return new JSONResponse( data: [ 'results' => $formattedResults, - 'total' => $results['total'] ?? 0, - 'facets' => $results['facets'] ?? [], + 'total' => $paginatedResult['total'] ?? 0, + 'facets' => [], ] ); }//end search() @@ -183,7 +192,7 @@ function (array $object): array { * * @suppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-92 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-dutch-language-search-support-i18n */ private function processSearchQuery(string $query): string { diff --git a/lib/Controller/SearchTrailController.php b/lib/Controller/SearchTrailController.php index aa85c2bd3e..f633331683 100644 --- a/lib/Controller/SearchTrailController.php +++ b/lib/Controller/SearchTrailController.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-view-based-search-composition */ namespace OCA\OpenRegister\Controller; @@ -438,8 +438,6 @@ public function show(int $id): JSONResponse /** * Get search statistics for a given period * - * @NoAdminRequired - * * @NoCSRFRequired * * @return JSONResponse JSON response with search statistics @@ -448,6 +446,11 @@ public function show(int $id): JSONResponse */ public function statistics(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + // Extract date filters. $params = $this->extractRequestParameters(); @@ -467,8 +470,6 @@ public function statistics(): JSONResponse /** * Get popular search terms * - * @NoAdminRequired - * * @NoCSRFRequired * * @return JSONResponse JSON response with popular search terms @@ -477,6 +478,11 @@ public function statistics(): JSONResponse */ public function popularTerms(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + // Extract parameters. $params = $this->extractRequestParameters(); // Prioritize underscore-prefixed limit parameter. @@ -520,8 +526,6 @@ public function popularTerms(): JSONResponse /** * Get search activity by time period * - * @NoAdminRequired - * * @NoCSRFRequired * * @return JSONResponse JSON response with search activity data @@ -530,6 +534,11 @@ public function popularTerms(): JSONResponse */ public function activity(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + // Extract parameters. $params = $this->extractRequestParameters(); $interval = $this->request->getParam(key: 'interval', default: 'day'); @@ -550,8 +559,6 @@ public function activity(): JSONResponse /** * Get search statistics by register and schema * - * @NoAdminRequired - * * @NoCSRFRequired * * @return JSONResponse JSON response with register schema statistics @@ -560,6 +567,11 @@ public function activity(): JSONResponse */ public function registerSchemaStats(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + // Extract parameters. $params = $this->extractRequestParameters(); @@ -603,8 +615,6 @@ public function registerSchemaStats(): JSONResponse /** * Get user agent statistics * - * @NoAdminRequired - * * @NoCSRFRequired * * @return JSONResponse JSON response with user agent statistics @@ -613,6 +623,11 @@ public function registerSchemaStats(): JSONResponse */ public function userAgentStats(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + // Extract parameters. $params = $this->extractRequestParameters(); // Prioritize underscore-prefixed limit parameter. @@ -692,8 +707,6 @@ public function userAgentStats(): JSONResponse /** * Clean up old search trail logs * - * @NoAdminRequired - * * @return JSONResponse JSON response containing cleanup operation results * * @NoCSRFRequired @@ -715,6 +728,11 @@ public function userAgentStats(): JSONResponse */ public function cleanup(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + // Extract date parameter. $before = $this->request->getParam(key: 'before'); $beforeDate = null; @@ -739,7 +757,10 @@ public function cleanup(): JSONResponse /** * Export search trail logs in specified format * - * @NoAdminRequired + * Admin-only at the framework level (no @NoAdminRequired): search-trail + * rows carry per-search IP, user id, user-agent and query string across + * every register/schema (cross-tenant PII, wave-3 C7), like the sibling + * analytics/destructive endpoints. Body `requireAdmin()` is defence-in-depth. * * @NoCSRFRequired * @@ -749,6 +770,11 @@ public function cleanup(): JSONResponse */ public function export(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + // Extract request parameters. $params = $this->extractRequestParameters(); @@ -843,8 +869,6 @@ public function export(): JSONResponse * * @param int $id The search trail ID to delete * - * @NoAdminRequired - * * @NoCSRFRequired * * @return JSONResponse JSON response with deletion result @@ -853,6 +877,11 @@ public function export(): JSONResponse */ public function destroy(int $id): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + try { // Validate that search trail exists (validation only). $this->searchTrailService->getSearchTrail($id); @@ -885,16 +914,19 @@ public function destroy(int $id): JSONResponse /** * Delete multiple search trail logs based on filters or specific IDs * - * @NoAdminRequired - * * @NoCSRFRequired * * @return JSONResponse JSON response with multiple deletion result * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-view-based-search-composition */ public function destroyMultiple(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + try { // TODO: Implement multiple search trail deletion. // $ids = $this->request->getParam(key: 'ids', default: null); @@ -941,9 +973,21 @@ private function arrayToCsv(array $data): string // Add headers. fputcsv($output, array_keys($data[0])); - // Add data rows. + // Add data rows. Flatten any array/object cell to a JSON string so + // fputcsv doesn't emit an "Array to string conversion" warning (and + // write the literal "Array") for nested values. foreach ($data as $row) { - fputcsv($output, $row); + $flatRow = array_map( + static function ($cell) { + if (is_array($cell) === true || is_object($cell) === true) { + return json_encode($cell); + } + + return $cell; + }, + $row + ); + fputcsv($output, $flatRow); } rewind($output); @@ -958,13 +1002,17 @@ private function arrayToCsv(array $data): string * * @return JSONResponse A JSON response indicating success or failure * - * @NoAdminRequired * @NoCSRFRequired * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-3 */ public function clearAll(): JSONResponse { + $denial = $this->requireAdmin(); + if ($denial !== null) { + return $denial; + } + try { /* * Get the search trail mapper from the container. diff --git a/lib/Controller/Settings/ApiTokenSettingsController.php b/lib/Controller/Settings/ApiTokenSettingsController.php index 46610c20b9..cc031ae5df 100644 --- a/lib/Controller/Settings/ApiTokenSettingsController.php +++ b/lib/Controller/Settings/ApiTokenSettingsController.php @@ -70,7 +70,7 @@ public function __construct( * array{error?: string, github_token?: string, gitlab_token?: string, * gitlab_url?: string}, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-10 + * @spec openspec/specs/production-observability/spec.md */ public function getApiTokens(): JSONResponse { @@ -114,7 +114,7 @@ public function getApiTokens(): JSONResponse * * @return JSONResponse JSON response with save result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-10 + * @spec openspec/specs/production-observability/spec.md */ public function saveApiTokens(): JSONResponse { @@ -162,7 +162,7 @@ public function saveApiTokens(): JSONResponse * * @return JSONResponse Test result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-10 + * @spec openspec/specs/production-observability/spec.md */ public function testGitHubToken(): JSONResponse { @@ -203,7 +203,7 @@ public function testGitHubToken(): JSONResponse 'scopes' => $response->getHeader('X-OAuth-Scopes') ?? [], ] ); - } catch (Exception $e) { + } catch (\Throwable $e) { return new JSONResponse( data: [ 'success' => false, @@ -221,7 +221,7 @@ public function testGitHubToken(): JSONResponse * * @return JSONResponse Test result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-10 + * @spec openspec/specs/production-observability/spec.md */ public function testGitLabToken(): JSONResponse { @@ -270,7 +270,7 @@ public function testGitLabToken(): JSONResponse 'instance' => $apiUrl, ] ); - } catch (Exception $e) { + } catch (\Throwable $e) { return new JSONResponse( data: [ 'success' => false, diff --git a/lib/Controller/Settings/CacheSettingsController.php b/lib/Controller/Settings/CacheSettingsController.php index 2a55cc0da9..306b3fc77a 100644 --- a/lib/Controller/Settings/CacheSettingsController.php +++ b/lib/Controller/Settings/CacheSettingsController.php @@ -28,7 +28,6 @@ use OCP\IRequest; use Exception; use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; use Psr\Log\LoggerInterface; /** @@ -51,7 +50,6 @@ class CacheSettingsController extends Controller * @param string $appName The app name. * @param IRequest $request The request. * @param SettingsService $settingsService Settings service. - * @param IndexService $indexService Index service. * @param LoggerInterface $logger Logger. * @param Factory $appDataFactory App data factory. * @param IAppConfig $appConfig App configuration. @@ -60,7 +58,6 @@ public function __construct( $appName, IRequest $request, private readonly SettingsService $settingsService, - private readonly IndexService $indexService, private readonly LoggerInterface $logger, private readonly Factory $appDataFactory, private readonly IAppConfig $appConfig, @@ -78,7 +75,7 @@ public function __construct( * * @return JSONResponse JSON response with cache statistics or error * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md */ public function getCacheStats(): JSONResponse { @@ -100,7 +97,7 @@ public function getCacheStats(): JSONResponse * * @return JSONResponse JSON response with clear cache result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md */ public function clearCache(): JSONResponse { @@ -125,7 +122,7 @@ public function clearCache(): JSONResponse * * @return JSONResponse JSON response with warmup result or error * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md */ public function warmupNamesCache(): JSONResponse { @@ -146,7 +143,7 @@ public function warmupNamesCache(): JSONResponse * * @return JSONResponse JSON response with warmup interval config * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md */ public function getWarmupInterval(): JSONResponse { @@ -190,7 +187,7 @@ public function getWarmupInterval(): JSONResponse * * @return JSONResponse JSON response with updated interval config * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md */ public function setWarmupInterval(): JSONResponse { @@ -256,7 +253,7 @@ public function setWarmupInterval(): JSONResponse * * @return JSONResponse JSON response with invalidation result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-6 + * @spec openspec/specs/production-observability/spec.md */ public function clearAppStoreCache(): JSONResponse { diff --git a/lib/Controller/Settings/ConfigurationSettingsController.php b/lib/Controller/Settings/ConfigurationSettingsController.php index 4c9c3c25d3..0531a3942d 100644 --- a/lib/Controller/Settings/ConfigurationSettingsController.php +++ b/lib/Controller/Settings/ConfigurationSettingsController.php @@ -24,7 +24,6 @@ use OCP\IRequest; use Exception; use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; use Psr\Log\LoggerInterface; /** @@ -48,14 +47,12 @@ class ConfigurationSettingsController extends Controller * @param string $appName The app name. * @param IRequest $request The request. * @param SettingsService $settingsService Settings service. - * @param IndexService $indexService Index service. * @param LoggerInterface $logger Logger. */ public function __construct( $appName, IRequest $request, private readonly SettingsService $settingsService, - private readonly IndexService $indexService, private readonly LoggerInterface $logger, ) { parent::__construct(appName: $appName, request: $request); @@ -68,7 +65,7 @@ public function __construct( * * @return JSONResponse JSON response with RBAC settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-12 + * @spec openspec/specs/rbac-scopes/spec.md */ public function getRbacSettings(): JSONResponse { @@ -87,7 +84,7 @@ public function getRbacSettings(): JSONResponse * * @return JSONResponse JSON response with updated RBAC settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-12 + * @spec openspec/specs/rbac-scopes/spec.md */ public function updateRbacSettings(): JSONResponse { @@ -107,7 +104,7 @@ public function updateRbacSettings(): JSONResponse * * @return JSONResponse JSON response with organisation settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-13 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function getOrganisationSettings(): JSONResponse { @@ -126,7 +123,7 @@ public function getOrganisationSettings(): JSONResponse * * @return JSONResponse JSON response with updated organisation settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-13 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function updateOrganisationSettings(): JSONResponse { @@ -146,7 +143,7 @@ public function updateOrganisationSettings(): JSONResponse * * @return JSONResponse JSON response with multitenancy settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-13 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function getMultitenancySettings(): JSONResponse { @@ -165,7 +162,7 @@ public function getMultitenancySettings(): JSONResponse * * @return JSONResponse JSON response with updated multitenancy settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-13 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function updateMultitenancySettings(): JSONResponse { @@ -185,7 +182,7 @@ public function updateMultitenancySettings(): JSONResponse * * @return JSONResponse JSON response with object settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-1 + * @spec openspec/specs/production-observability/spec.md */ public function getObjectSettings(): JSONResponse { @@ -215,7 +212,7 @@ public function getObjectSettings(): JSONResponse * * @return JSONResponse JSON response with updated object settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-1 + * @spec openspec/specs/production-observability/spec.md */ public function updateObjectSettings(): JSONResponse { @@ -253,7 +250,7 @@ public function updateObjectSettings(): JSONResponse * * @return JSONResponse JSON response with patched object settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-1 + * @spec openspec/specs/production-observability/spec.md */ public function patchObjectSettings(): JSONResponse { @@ -267,7 +264,7 @@ public function patchObjectSettings(): JSONResponse * * @return JSONResponse JSON response with retention settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-11 + * @spec openspec/specs/retention-management/spec.md */ public function getRetentionSettings(): JSONResponse { @@ -286,7 +283,7 @@ public function getRetentionSettings(): JSONResponse * * @return JSONResponse JSON response with updated retention settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-11 + * @spec openspec/specs/retention-management/spec.md */ public function updateRetentionSettings(): JSONResponse { @@ -306,7 +303,7 @@ public function updateRetentionSettings(): JSONResponse * * @return JSONResponse JSON response with archival settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-14 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function getArchivalSettings(): JSONResponse { @@ -325,7 +322,7 @@ public function getArchivalSettings(): JSONResponse * * @return JSONResponse JSON response with updated archival settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-14 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function updateArchivalSettings(): JSONResponse { @@ -337,85 +334,4 @@ public function updateArchivalSettings(): JSONResponse return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); } }//end updateArchivalSettings() - - /** - * Get object collection field status - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with object collection fields - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-17 - */ - public function getObjectCollectionFields(): JSONResponse - { - try { - $solrSchemaService = $this->indexService; - $status = $solrSchemaService->getObjectCollectionFieldStatus(); - - return new JSONResponse( - data: [ - 'success' => true, - 'collection' => 'objects', - 'status' => $status, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to get object collection field status: '.$e->getMessage(), - ], - statusCode: 500 - ); - } - }//end getObjectCollectionFields() - - /** - * Create missing fields in object collection - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with creation result - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-17 - */ - public function createMissingObjectFields(): JSONResponse - { - try { - $solrSchemaService = $this->indexService; - - // Switch to object collection. - $objectCollection = $this->settingsService->getSolrSettingsOnly()['objectCollection'] ?? null; - if ($objectCollection === null || $objectCollection === '') { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Object collection not configured', - ], - statusCode: 400 - ); - } - - // Create missing fields. - $result = $solrSchemaService->mirrorSchemas(force: true); - - return new JSONResponse( - data: [ - 'success' => true, - 'collection' => 'objects', - 'message' => 'Missing object fields created successfully', - 'result' => $result, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to create missing object fields: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end createMissingObjectFields() }//end class diff --git a/lib/Controller/Settings/EdepotSettingsController.php b/lib/Controller/Settings/EdepotSettingsController.php index 6000f50cc5..6889f13320 100644 --- a/lib/Controller/Settings/EdepotSettingsController.php +++ b/lib/Controller/Settings/EdepotSettingsController.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-configurable-e-depot-endpoint-settings */ declare(strict_types=1); @@ -81,7 +81,7 @@ public function __construct( * * @return JSONResponse The current e-Depot configuration. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-configurable-e-depot-endpoint-settings */ public function getEdepotSettings(): JSONResponse { @@ -116,7 +116,7 @@ public function getEdepotSettings(): JSONResponse * * @return JSONResponse The update result. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-configurable-e-depot-endpoint-settings */ public function updateEdepotSettings(): JSONResponse { @@ -190,7 +190,7 @@ public function updateEdepotSettings(): JSONResponse * * @return JSONResponse The connection test result. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-configurable-e-depot-endpoint-settings */ public function testEdepotConnection(): JSONResponse { @@ -229,7 +229,7 @@ public function testEdepotConnection(): JSONResponse * * @return TransportInterface The transport. * - * @spec openspec/changes/retrofit-2026-05-24-edepot-transfer/tasks.md#task-1 + * @spec openspec/specs/edepot-transfer/spec.md */ private function resolveTransport(string $type): TransportInterface { diff --git a/lib/Controller/Settings/FileSettingsController.php b/lib/Controller/Settings/FileSettingsController.php index 70ea142349..ac7f1f40b7 100644 --- a/lib/Controller/Settings/FileSettingsController.php +++ b/lib/Controller/Settings/FileSettingsController.php @@ -24,9 +24,9 @@ use OCP\IRequest; use Psr\Container\ContainerInterface; use Exception; -use ReflectionClass; use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; +use OCA\OpenRegister\Service\Anonymisation\AnonymisationBackendService; +use OCA\OpenRegister\Service\Anonymisation\BackendState; use Psr\Log\LoggerInterface; /** @@ -232,286 +232,42 @@ public function testPresidioConnection(string $apiEndpoint): JSONResponse }//end testPresidioConnection() /** - * Test OpenAnonymiser API connection + * Test OpenAnonymiser ExApp availability. * - * @param string $apiEndpoint OpenAnonymiser API endpoint URL + * OpenAnonymiser is now detected via AppAPI, not an HTTP endpoint. + * Prefer the admin endpoint `POST /api/admin/anonymisation/test-connection` + * with `{method: "openanonymiser"}`. This route is retained for backward + * compatibility and delegates to AnonymisationBackendService; the + * $apiEndpoint parameter is ignored and no external HTTP request is issued. * - * @return JSONResponse - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse<200|400|500, array{success: bool, error?: string, - * message?: string}, array> - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function testOpenAnonymiserConnection(string $apiEndpoint): JSONResponse - { - try { - // Validate inputs. - if (empty($apiEndpoint) === true) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'API endpoint is required', - ], - statusCode: 400 - ); - } - - $result = $this->performHealthCheck( - url: $apiEndpoint.'/api/v1/health', - serviceName: 'OpenAnonymiser' - ); - - return new JSONResponse(data: $result); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end testOpenAnonymiserConnection() - - /** - * Get file collection field status - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with file collection fields - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function getFileCollectionFields(): JSONResponse - { - try { - $solrSchemaService = $this->container->get(IndexService::class); - $status = $solrSchemaService->getFileCollectionFieldStatus(); - - return new JSONResponse( - data: [ - 'success' => true, - 'collection' => 'files', - 'status' => $status, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to get file collection field status: '.$e->getMessage(), - ], - statusCode: 500 - ); - } - }//end getFileCollectionFields() - - /** - * Create missing fields in file collection - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with creation result - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function createMissingFileFields(): JSONResponse - { - try { - $solrSchemaService = $this->container->get(IndexService::class); - $guzzleSolrService = $this->container->get(IndexService::class); - - // Switch to file collection. - $fileCollection = $this->settingsService->getSolrSettingsOnly()['fileCollection'] ?? null; - if ($fileCollection === null || $fileCollection === '') { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'File collection not configured', - ], - statusCode: 400 - ); - } - - // Set active collection to file collection temporarily. - $originalCollection = $guzzleSolrService->getActiveCollectionName(); - $guzzleSolrService->setActiveCollection($fileCollection); - - // Create missing file metadata fields using reflection to call private method. - $reflection = new ReflectionClass($solrSchemaService); - $method = $reflection->getMethod('ensureFileMetadataFields'); - $result = $method->invoke($solrSchemaService, true); - - // Restore original collection. - $guzzleSolrService->setActiveCollection($originalCollection); - - // Determine message based on result. - $message = 'Failed to ensure file metadata fields'; - if ($result === true) { - $message = 'File metadata fields ensured successfully'; - } - - return new JSONResponse( - data: [ - 'success' => $result, - 'collection' => 'files', - 'message' => $message, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to create missing file fields: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end createMissingFileFields() - - /** - * Warmup files - Extract text and index in SOLR file collection - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with warmup result - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function warmupFiles(): JSONResponse - { - try { - // Get request parameters. - $maxFiles = (int) $this->request->getParam('max_files', 100); - $batchSize = (int) $this->request->getParam('batch_size', 50); - // Note: file_types parameter not currently used. - $skipIndexed = $this->request->getParam('skip_indexed', true); - $mode = $this->request->getParam('mode', 'parallel'); - - // Validate parameters. - $maxFiles = min($maxFiles, 5000); - // Max 5000 files. - $batchSize = min($batchSize, 500); - // Max 500 per batch. - $this->logger->info( - message: '[SettingsController] Starting file warmup', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'max_files' => $maxFiles, - 'batch_size' => $batchSize, - 'skip_indexed' => $skipIndexed, - ] - ); - - // Get IndexService and TextExtractionService. - $guzzleSolrService = $this->container->get(IndexService::class); - $textExtractSvc = $this->container->get(\OCA\OpenRegister\Service\TextExtractionService::class); - - // Get files that need processing. - $filesToProcess = []; - if ($skipIndexed === true) { - $notIndexed = $textExtractSvc->findNotIndexedInSolr('file', $maxFiles); - foreach ($notIndexed as $fileId) { - $filesToProcess[] = $fileId; - } - } - - if ($skipIndexed === false) { - $completed = $textExtractSvc->findByStatus('file', 'completed', $maxFiles, 0); - foreach ($completed as $fileId) { - $filesToProcess[] = $fileId; - } - } - - // If no files to process, return early. - if (empty($filesToProcess) === true) { - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'No files to process', - 'files_processed' => 0, - 'indexed' => 0, - 'failed' => 0, - ] - ); - } - - // Process files in batches. - $totalIndexed = 0; - $totalFailed = 0; - $allErrors = []; - - $batches = array_chunk($filesToProcess, $batchSize); - foreach ($batches as $batch) { - $result = $guzzleSolrService->indexFiles($batch); - $totalIndexed += $result['indexed']; - $totalFailed += $result['failed']; - $allErrors = array_merge($allErrors, $result['errors']); - } - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'File warmup completed', - 'files_processed' => count($filesToProcess), - 'indexed' => $totalIndexed, - 'failed' => $totalFailed, - 'errors' => array_slice($allErrors, 0, 20), - // First 20 errors. - 'mode' => $mode, - ] - ); - } catch (Exception $e) { - $this->logger->error( - message: '[SettingsController] File warmup failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'File warmup failed: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end warmupFiles() - - /** - * Index a specific file in SOLR + * @param string $apiEndpoint Ignored; retained for backward compatibility. * - * @param int $fileId File ID to index + * @return JSONResponse * - * @return JSONResponse Indexing result + * @deprecated OpenAnonymiser is now detected via AppAPI, not an HTTP endpoint. * * @NoCSRFRequired * - * @psalm-return JSONResponse<200|422|500, array{success: bool, message: mixed|string, file_id?: int}, - * array> + * @psalm-return JSONResponse<200|500, array{success: bool, error?: string, + * message?: string}, array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 + * @spec openspec/changes/anonymiser-backend-selection/tasks.md#task-4.4 */ - public function indexFile(int $fileId): JSONResponse + public function testOpenAnonymiserConnection(string $apiEndpoint=''): JSONResponse { try { - $guzzleSolrService = $this->container->get(IndexService::class); + /* + * @var AnonymisationBackendService $service + */ - $result = $guzzleSolrService->indexFiles([$fileId]); + $service = $this->container->get(AnonymisationBackendService::class); + $probe = $service->testConnection(method: BackendState::METHOD_OPENANONYMISER); - if ($result['indexed'] > 0) { + if ($probe->reachable === true) { return new JSONResponse( data: [ 'success' => true, - 'message' => 'File indexed successfully', - 'file_id' => $fileId, + 'message' => 'OpenAnonymiser ExApp detected via AppAPI', ] ); } @@ -519,146 +275,19 @@ public function indexFile(int $fileId): JSONResponse return new JSONResponse( data: [ 'success' => false, - 'message' => $result['errors'][0] ?? 'Failed to index file', - 'file_id' => $fileId, - ], - statusCode: 422 - ); - } catch (Exception $e) { - $this->logger->error( - message: '[SettingsController] Failed to index file', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'file_id' => $fileId, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to index file: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end indexFile() - - /** - * Reindex all files - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with reindex result - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function reindexFiles(): JSONResponse - { - try { - // Get all completed file texts. - $textExtractSvc = $this->container->get(\OCA\OpenRegister\Service\TextExtractionService::class); - $guzzleSolrService = $this->container->get(IndexService::class); - - $maxFiles = (int) $this->request->getParam('max_files', 1000); - $batchSize = (int) $this->request->getParam('batch_size', 100); - - // Get all completed extractions. - $fileIds = $textExtractSvc->findByStatus('file', 'completed', $maxFiles, 0); - - if (empty($fileIds) === true) { - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'No files to reindex', - 'indexed' => 0, - ] - ); - } - - // Process in batches. - $totalIndexed = 0; - $totalFailed = 0; - $allErrors = []; - - $batches = array_chunk($fileIds, $batchSize); - foreach ($batches as $batch) { - $result = $guzzleSolrService->indexFiles($batch); - $totalIndexed += $result['indexed']; - $totalFailed += $result['failed']; - $allErrors = array_merge($allErrors, $result['errors']); - } - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Reindex completed', - 'files_processed' => count($fileIds), - 'indexed' => $totalIndexed, - 'failed' => $totalFailed, - 'errors' => array_slice($allErrors, 0, 20), - ] - ); - } catch (Exception $e) { - $this->logger->error( - message: '[SettingsController] Reindex files failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), + 'error' => ($probe->error ?? 'OpenAnonymiser ExApp not available'), ] ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Reindex failed: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end reindexFiles() - - /** - * Get file index statistics - * - * @NoCSRFRequired - * - * @return JSONResponse File index statistics - * - * @psalm-return JSONResponse<200, array, - * array>|JSONResponse<500, - * array{success: false, message: string}, array> - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-2 - */ - public function getFileIndexStats(): JSONResponse - { - try { - $guzzleSolrService = $this->container->get(IndexService::class); - $stats = $guzzleSolrService->getFileIndexStats(); - - return new JSONResponse(data: $stats); } catch (Exception $e) { - $this->logger->error( - message: '[SettingsController] Failed to get file index stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return new JSONResponse( data: [ 'success' => false, - 'message' => 'Failed to get statistics: '.$e->getMessage(), + 'error' => $e->getMessage(), ], statusCode: 500 ); }//end try - }//end getFileIndexStats() + }//end testOpenAnonymiserConnection() /** * Get file extraction statistics @@ -666,7 +295,6 @@ public function getFileIndexStats(): JSONResponse * Combines multiple data sources for comprehensive file statistics: * - FileMapper: Total files in Nextcloud (from oc_filecache, bypasses rights logic) * - FileTextMapper: Extraction status (from oc_openregister_file_texts) - * - IndexService: Chunk statistics (from SOLR index) * * This provides accurate statistics without dealing with Nextcloud's extensive rights logic. * @@ -679,14 +307,12 @@ public function getFileIndexStats(): JSONResponse * - pendingFiles: Files discovered and waiting for extraction * (status='pending') * - untrackedFiles: Files in Nextcloud not yet discovered - * - totalChunks: Number of text chunks in SOLR - * (one file = multiple chunks) * - completed, failed, indexed, processing, vectorized: * Detailed processing status counts * * @psalm-return JSONResponse<200, * array{success: true, totalFiles: 0|mixed, processedFiles: 0|mixed, - * pendingFiles: 0|mixed, untrackedFiles: 0|mixed, totalChunks: 0|mixed, + * pendingFiles: 0|mixed, untrackedFiles: 0|mixed, * extractedTextStorageMB: string, totalFilesStorageMB: string, * completed: 0|mixed, failed: 0|mixed, indexed: 0|mixed, * processing: 0|mixed, vectorized: 0|mixed, error?: string}, @@ -706,10 +332,6 @@ public function getFileExtractionStats(): JSONResponse $textExtractSvc = $this->container->get(\OCA\OpenRegister\Service\TextExtractionService::class); $dbStats = $textExtractSvc->getExtractionStats('file'); - // Get SOLR statistics. - $guzzleSolrService = $this->container->get(IndexService::class); - $solrStats = $guzzleSolrService->getFileIndexStats(); - // Calculate storage in MB. $extractedTextMB = round($dbStats['total_text_size'] / 1024 / 1024, 2); $totalFilesStorageMB = round($totalFilesSize / 1024 / 1024, 2); @@ -727,7 +349,6 @@ public function getFileExtractionStats(): JSONResponse // Files discovered and waiting for extraction. 'untrackedFiles' => max(0, $untrackedFiles), // Files not yet discovered. - 'totalChunks' => $solrStats['total_chunks'] ?? 0, 'extractedTextStorageMB' => number_format($extractedTextMB, 2), 'totalFilesStorageMB' => number_format($totalFilesStorageMB, 2), 'completed' => $dbStats['completed'], @@ -746,7 +367,6 @@ public function getFileExtractionStats(): JSONResponse 'processedFiles' => 0, 'pendingFiles' => 0, 'untrackedFiles' => 0, - 'totalChunks' => 0, 'extractedTextStorageMB' => '0.00', 'totalFilesStorageMB' => '0.00', 'completed' => 0, diff --git a/lib/Controller/Settings/LlmSettingsController.php b/lib/Controller/Settings/LlmSettingsController.php index 17542ded8f..7146215d09 100644 --- a/lib/Controller/Settings/LlmSettingsController.php +++ b/lib/Controller/Settings/LlmSettingsController.php @@ -75,7 +75,7 @@ public function __construct( * * @psalm-return JSONResponse<200|500, array, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-1 + * @spec openspec/specs/chat-ai/spec.md */ public function getLLMSettings(): JSONResponse { @@ -97,7 +97,7 @@ public function getLLMSettings(): JSONResponse * @suppressWarnings(PHPMD.CyclomaticComplexity) * @suppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-1 + * @spec openspec/specs/chat-ai/spec.md */ public function updateLLMSettings(): JSONResponse { @@ -176,7 +176,7 @@ public function updateLLMSettings(): JSONResponse * message?: 'LLM settings updated successfully', data?: array}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-1 + * @spec openspec/specs/chat-ai/spec.md */ public function patchLLMSettings(): JSONResponse { @@ -195,7 +195,7 @@ public function patchLLMSettings(): JSONResponse * * @return JSONResponse JSON response with embedding test result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function testEmbedding(): JSONResponse { @@ -269,7 +269,7 @@ public function testEmbedding(): JSONResponse * array{success: false, error: string, message: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function testChat(): JSONResponse { @@ -341,7 +341,7 @@ public function testChat(): JSONResponse * @suppressWarnings(PHPMD.ExcessiveMethodLength) * @suppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-2 + * @spec openspec/specs/chat-ai/spec.md */ public function getOllamaModels(): JSONResponse { @@ -469,7 +469,7 @@ function ($a, $b) { * * @return JSONResponse JSON response with mismatch check result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-3 + * @spec openspec/specs/chat-ai/spec.md */ public function checkEmbeddingModelMismatch(): JSONResponse { @@ -500,7 +500,7 @@ public function checkEmbeddingModelMismatch(): JSONResponse * array{success: bool, error?: string, message?: string, deleted?: int}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-3 + * @spec openspec/specs/chat-ai/spec.md */ public function clearAllEmbeddings(): JSONResponse { diff --git a/lib/Controller/Settings/N8nSettingsController.php b/lib/Controller/Settings/N8nSettingsController.php index fb80786a9b..1a2a3b6ce2 100644 --- a/lib/Controller/Settings/N8nSettingsController.php +++ b/lib/Controller/Settings/N8nSettingsController.php @@ -109,7 +109,7 @@ public function __construct( * * @psalm-return JSONResponse<200|500, array, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-9 + * @spec openspec/specs/production-observability/spec.md */ public function getN8nSettings(): JSONResponse { @@ -143,7 +143,7 @@ public function getN8nSettings(): JSONResponse * * @return JSONResponse JSON response with updated n8n settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-9 + * @spec openspec/specs/production-observability/spec.md */ public function updateN8nSettings(): JSONResponse { @@ -192,7 +192,7 @@ public function updateN8nSettings(): JSONResponse * * @return JSONResponse JSON response with connection test result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-9 + * @spec openspec/specs/production-observability/spec.md */ public function testN8nConnection(): JSONResponse { @@ -276,7 +276,7 @@ public function testN8nConnection(): JSONResponse * * @suppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-9 + * @spec openspec/specs/production-observability/spec.md */ public function initializeN8n(): JSONResponse { @@ -420,7 +420,7 @@ public function initializeN8n(): JSONResponse * * @return JSONResponse JSON response with workflows list * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-9 + * @spec openspec/specs/production-observability/spec.md */ public function getWorkflows(): JSONResponse { diff --git a/lib/Controller/Settings/SecuritySettingsController.php b/lib/Controller/Settings/SecuritySettingsController.php index d3d95e135c..99eec8f0ce 100644 --- a/lib/Controller/Settings/SecuritySettingsController.php +++ b/lib/Controller/Settings/SecuritySettingsController.php @@ -66,7 +66,7 @@ public function __construct( * * @return JSONResponse JSON response with result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-8 + * @spec openspec/specs/production-observability/spec.md */ public function clearIpRateLimits(): JSONResponse { @@ -118,7 +118,7 @@ public function clearIpRateLimits(): JSONResponse * * @return JSONResponse JSON response with result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-8 + * @spec openspec/specs/production-observability/spec.md */ public function clearUserRateLimits(): JSONResponse { @@ -170,7 +170,7 @@ public function clearUserRateLimits(): JSONResponse * * @return JSONResponse JSON response with result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-8 + * @spec openspec/specs/production-observability/spec.md */ public function clearAllRateLimits(): JSONResponse { diff --git a/lib/Controller/Settings/SolrManagementController.php b/lib/Controller/Settings/SolrManagementController.php deleted file mode 100644 index 5d980fbbad..0000000000 --- a/lib/Controller/Settings/SolrManagementController.php +++ /dev/null @@ -1,693 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Controller\Settings; - -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IRequest; -use OCP\IDBConnection; -use Psr\Container\ContainerInterface; -use Exception; -use InvalidArgumentException; -use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; -use Psr\Log\LoggerInterface; - -/** - * Controller for SOLR field and collection management. - * - * Handles: - * - Field discovery, creation, and deletion - * - Field validation and fixing - * - Collection listing, creation, and deletion - * - Collection copying and assignments - * - Config set management - * - * @category Controller - * @package OCA\OpenRegister\Controller\Settings - * - * @suppressWarnings(PHPMD.TooManyPublicMethods) - * @suppressWarnings(PHPMD.ExcessiveClassComplexity) - */ -class SolrManagementController extends Controller -{ - /** - * Constructor. - * - * @param string $appName The app name. - * @param IRequest $request The request. - * @param IDBConnection $db Database connection. - * @param ContainerInterface $container DI container. - * @param SettingsService $settingsService Settings service. - * @param IndexService $indexService Index service. - * @param LoggerInterface $logger Logger. - */ - public function __construct( - $appName, - IRequest $request, - private readonly IDBConnection $db, - private readonly ContainerInterface $container, - private readonly SettingsService $settingsService, - private readonly IndexService $indexService, - private readonly LoggerInterface $logger, - ) { - parent::__construct(appName: $appName, request: $request); - }//end __construct() - - /** - * Get SOLR field configuration and schema information - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with SOLR field configuration - * - * @suppressWarnings(PHPMD.ExcessiveMethodLength) - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-17 - */ - public function getSolrFields(): JSONResponse - { - try { - // Use IndexService to get field status for both collections. - $solrSchemaService = $this->container->get(\OCA\OpenRegister\Service\IndexService::class); - $guzzleSolrService = $this->container->get(IndexService::class); - - // Check if SOLR is available first. - if ($guzzleSolrService->isAvailable() === false) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR is not available or not configured', - 'details' => ['error' => 'SOLR service is not enabled or connection failed'], - ], - statusCode: 422 - ); - } - - // Get field status for both collections. - $objectFieldStatus = $solrSchemaService->getObjectCollectionFieldStatus(); - $fileFieldStatus = $solrSchemaService->getFileCollectionFieldStatus(); - - // Combine missing fields from both collections with collection identifier. - $missingFields = []; - - foreach ($objectFieldStatus['missing'] as $fieldName => $fieldInfo) { - $missingFields[] = [ - 'name' => $fieldName, - 'type' => $fieldInfo['type'], - 'config' => $fieldInfo, - 'collection' => 'objects', - 'collectionLabel' => 'Object Collection', - ]; - } - - foreach ($fileFieldStatus['missing'] as $fieldName => $fieldInfo) { - $missingFields[] = [ - 'name' => $fieldName, - 'type' => $fieldInfo['type'], - 'config' => $fieldInfo, - 'collection' => 'files', - 'collectionLabel' => 'File Collection', - ]; - } - - // Combine extra fields from both collections. - $extraFields = []; - - foreach ($objectFieldStatus['extra'] as $fieldName) { - $extraFields[] = [ - 'name' => $fieldName, - 'collection' => 'objects', - 'collectionLabel' => 'Object Collection', - ]; - } - - foreach ($fileFieldStatus['extra'] as $fieldName) { - $extraFields[] = [ - 'name' => $fieldName, - 'collection' => 'files', - 'collectionLabel' => 'File Collection', - ]; - } - - // Build comparison result. - $comparison = [ - 'total_differences' => count($missingFields) + count($extraFields), - 'missing_count' => count($missingFields), - 'extra_count' => count($extraFields), - 'missing' => $missingFields, - 'extra' => $extraFields, - 'object_collection' => [ - 'missing' => count($objectFieldStatus['missing']), - 'extra' => count($objectFieldStatus['extra']), - ], - 'file_collection' => [ - 'missing' => count($fileFieldStatus['missing']), - 'extra' => count($fileFieldStatus['extra']), - ], - ]; - - return new JSONResponse( - data: [ - 'success' => true, - 'comparison' => $comparison, - 'object_collection_status' => $objectFieldStatus, - 'file_collection_status' => $fileFieldStatus, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to retrieve SOLR field configuration: '.$e->getMessage(), - 'details' => ['error' => $e->getMessage()], - ], - statusCode: 422 - ); - }//end try - }//end getSolrFields() - - /** - * Create missing SOLR fields based on schema analysis - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with field creation result - * - * @suppressWarnings(PHPMD.ExcessiveMethodLength) - * @suppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-17 - */ - public function createMissingSolrFields(): JSONResponse - { - try { - // Get services. - $guzzleSolrService = $this->container->get(IndexService::class); - $solrSchemaService = $this->container->get(IndexService::class); - - // Check if SOLR is available first. - if ($guzzleSolrService->isAvailable() === false) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR is not available or not configured', - 'details' => ['error' => 'SOLR service is not enabled or connection failed'], - ], - statusCode: 422 - ); - } - - // Get dry run parameter. - $dryRun = $this->request->getParam('dry_run', false); - $dryRun = filter_var($dryRun, FILTER_VALIDATE_BOOLEAN); - - $startTime = microtime(true); - $totalCreated = 0; - $totalErrors = 0; - $results = [ - 'objects' => null, - 'files' => null, - ]; - - // Create missing fields for OBJECT collection. - try { - $objectStatus = $solrSchemaService->getObjectCollectionFieldStatus(); - if (empty($objectStatus['missing']) === false) { - $objectResult = $solrSchemaService->createMissingFields( - collectionType: 'objects', - missingFields: $objectStatus['missing'], - dryRun: $dryRun - ); - $results['objects'] = $objectResult; - if (($objectResult['created_count'] ?? null) !== null) { - $totalCreated += $objectResult['created_count']; - } - - if (($objectResult['error_count'] ?? null) !== null) { - $totalErrors += $objectResult['error_count']; - } - } - } catch (Exception $e) { - $results['objects'] = [ - 'success' => false, - 'message' => 'Failed to create object fields: '.$e->getMessage(), - ]; - $totalErrors++; - }//end try - - // Create missing fields for FILE collection. - try { - $fileStatus = $solrSchemaService->getFileCollectionFieldStatus(); - if (empty($fileStatus['missing']) === false) { - $fileResult = $solrSchemaService->createMissingFields( - collectionType: 'files', - missingFields: $fileStatus['missing'], - dryRun: $dryRun - ); - $results['files'] = $fileResult; - if (($fileResult['created_count'] ?? null) !== null) { - $totalCreated += $fileResult['created_count']; - } - - if (($fileResult['error_count'] ?? null) !== null) { - $totalErrors += $fileResult['error_count']; - } - } - } catch (Exception $e) { - $results['files'] = [ - 'success' => false, - 'message' => 'Failed to create file fields: '.$e->getMessage(), - ]; - $totalErrors++; - }//end try - - $executionTime = round((microtime(true) - $startTime) * 1000, 2); - - return new JSONResponse( - data: [ - 'success' => $totalErrors === 0, - 'message' => sprintf( - 'Field creation completed: %d total fields created across both collections', - $totalCreated - ), - 'total_created' => $totalCreated, - 'total_errors' => $totalErrors, - 'results' => $results, - 'execution_time_ms' => $executionTime, - 'dry_run' => $dryRun, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to create missing SOLR fields: '.$e->getMessage(), - 'details' => ['error' => $e->getMessage()], - ], - statusCode: 422 - ); - }//end try - }//end createMissingSolrFields() - - /** - * Fix mismatched SOLR field configurations - * - * @NoCSRFRequired - * - * @return JSONResponse The field fix results - * - * @psalm-return JSONResponse< - * 200, - * array, - * array - * >|JSONResponse< - * 200|422, - * array{ - * success: bool, - * message: string, - * details?: array{error: mixed|string}, - * fixed?: array, - * errors?: array - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-17 - */ - public function fixMismatchedSolrFields(): JSONResponse - { - try { - // Get IndexService for field operations. - $guzzleSolrService = $this->container->get(IndexService::class); - - // Check if SOLR is available first. - if ($guzzleSolrService->isAvailable() === false) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR is not available or not configured', - 'details' => ['error' => 'SOLR service is not enabled or connection failed'], - ], - statusCode: 422 - ); - } - - // Get dry run parameter. - $dryRun = $this->request->getParam('dry_run', false); - $dryRun = filter_var($dryRun, FILTER_VALIDATE_BOOLEAN); - - // Get expected fields and current SOLR fields for comparison. - $schemaMapper = $this->container->get(\OCA\OpenRegister\Db\SchemaMapper::class); - $expectedFields = $this->settingsService->getExpectedSchemaFields($schemaMapper, $guzzleSolrService); - $fieldsInfo = $guzzleSolrService->getFieldsConfiguration(); - - if (($fieldsInfo['success'] === false)) { - // @psalm-suppress InvalidArrayOffset - message key may exist on error responses - $errorMessage = $fieldsInfo['message'] ?? 'Unknown error'; - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to get SOLR field configuration', - 'details' => ['error' => $errorMessage], - ], - statusCode: 422 - ); - } - - // Compare fields to find mismatched ones. - $comparison = $this->settingsService->compareFields( - actualFields: $fieldsInfo['fields'] ?? [], - expectedFields: $expectedFields - ); - - if (empty($comparison['mismatched']) === true) { - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'No mismatched fields found - SOLR schema is properly configured', - 'fixed' => [], - 'errors' => [], - ] - ); - } - - // Prepare fields to fix from mismatched fields. - $fieldsToFix = []; - foreach ($comparison['mismatched'] as $mismatch) { - $fieldsToFix[$mismatch['field']] = $mismatch['expected_config']; - } - - // Debug: Log field count for troubleshooting. - // Fix the mismatched fields using the dedicated method. - $result = $guzzleSolrService->fixMismatchedFields($fieldsToFix, $dryRun); - - // The fixMismatchedFields method already returns the correct format. - return new JSONResponse(data: $result); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to fix mismatched SOLR fields: '.$e->getMessage(), - 'details' => ['error' => $e->getMessage()], - ], - statusCode: 422 - ); - }//end try - }//end fixMismatchedSolrFields() - - /** - * Delete a SOLR field - * - * @param string $fieldName Name of the field to delete - * - * @return JSONResponse - * - * @NoCSRFRequired - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-17 - */ - public function deleteSolrField(string $fieldName): JSONResponse - { - try { - $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); - $logger->info( - message: '[SolrManagementController] 🗑️ Deleting SOLR field via API', - context: [ - 'field_name' => $fieldName, - 'user' => $this->userId, - ] - ); - - // Validate field name. - if (empty($fieldName) === true || is_string($fieldName) === false) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Invalid field name provided', - ], - statusCode: 400 - ); - } - - // Prevent deletion of critical system fields. - $protectedFields = ['id', '_version_', '_root_', '_text_']; - if (in_array($fieldName, $protectedFields) === true) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => "Cannot delete protected system field: {$fieldName}", - ], - statusCode: 403 - ); - } - - // Get IndexService from container. - $guzzleSolrService = $this->container->get(IndexService::class); - $result = $guzzleSolrService->deleteField($fieldName); - - if ($result['success'] === true) { - $logger->info( - message: '[SolrManagementController] ✅ SOLR field deleted successfully via API', - context: [ - 'field_name' => $fieldName, - 'user' => $this->userId, - ] - ); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => $result['message'], - 'field_name' => $fieldName, - ] - ); - } - - $logger->warning( - message: '[SolrManagementController] ❌ Failed to delete SOLR field via API', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field_name' => $fieldName, - 'error' => $result['message'], - 'user' => $this->userId, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $result['message'], - 'error' => $result['error'] ?? null, - ], - statusCode: 422 - ); - } catch (Exception $e) { - $logger = $logger ?? \OC::$server->get(\Psr\Log\LoggerInterface::class); - $logger->error( - message: '[SolrManagementController] Exception deleting SOLR field via API', - context: [ - 'field_name' => $fieldName, - 'error' => $e->getMessage(), - 'user' => $this->userId, - 'trace' => $e->getTraceAsString(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to delete SOLR field: '.$e->getMessage(), - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end deleteSolrField() - - /** - * Delete a specific SOLR collection by name - * - * @param string $name The name of the collection to delete - * - * @return JSONResponse The deletion result - * - * @NoCSRFRequired - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function deleteSpecificSolrCollection(string $name): JSONResponse - { - try { - $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); - - $logger->warning( - message: '[SolrManagementController] 🚨 SOLR collection deletion requested', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'timestamp' => date('c'), - 'user_id' => $this->userId ?? 'unknown', - 'collection' => $name, - 'request_id' => $this->request->getId() ?? 'unknown', - ] - ); - - // Get IndexService. - $guzzleSolrService = $this->container->get(IndexService::class); - - // Delete the specific collection. - $result = $guzzleSolrService->deleteCollection($name); - - if ($result['success'] === true) { - $logger->info( - message: '[SolrManagementController] ✅ SOLR collection deleted successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $name, - 'user_id' => $this->userId ?? 'unknown', - ] - ); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Collection deleted successfully', - 'collection' => $name, - ], - statusCode: 200 - ); - } - - $logger->error( - message: '[SolrManagementController] ❌ SOLR collection deletion failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $result['message'], - 'error_code' => $result['error_code'] ?? 'unknown', - 'collection' => $name, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $result['message'], - 'error_code' => $result['error_code'] ?? 'unknown', - 'collection' => $name, - 'solr_error' => $result['solr_error'] ?? null, - ], - statusCode: 422 - ); - } catch (Exception $e) { - $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); - $logger->error( - message: '[SolrManagementController] Exception during SOLR collection deletion', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'collection' => $name, - 'trace' => $e->getTraceAsString(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Collection deletion failed: '.$e->getMessage(), - 'error_code' => 'EXCEPTION', - 'collection' => $name, - ], - statusCode: 422 - ); - }//end try - }//end deleteSpecificSolrCollection() - - /** - * Update SOLR collection assignments (Object Collection and File Collection) - * - * @param string|null $objectCollection Collection name for objects - * @param string|null $fileCollection Collection name for files - * - * @return JSONResponse Update result - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool, - * error?: string, - * trace?: string, - * message?: 'Collection assignments updated successfully', - * objectCollection?: mixed|null, - * fileCollection?: mixed|null, - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function updateSolrCollectionAssignments( - ?string $objectCollection=null, - ?string $fileCollection=null - ): JSONResponse { - try { - // Get current SOLR settings. - $solrSettings = $this->settingsService->getSolrSettingsOnly(); - - // Update collection assignments. - if ($objectCollection !== null) { - $solrSettings['objectCollection'] = $objectCollection; - } - - if ($fileCollection !== null) { - $solrSettings['fileCollection'] = $fileCollection; - } - - // Save updated settings. - $this->settingsService->updateSolrSettingsOnly($solrSettings); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Collection assignments updated successfully', - 'objectCollection' => $solrSettings['objectCollection'] ?? null, - 'fileCollection' => $solrSettings['fileCollection'] ?? null, - 'timestamp' => date('c'), - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ], - statusCode: 500 - ); - }//end try - }//end updateSolrCollectionAssignments() -}//end class diff --git a/lib/Controller/Settings/SolrOperationsController.php b/lib/Controller/Settings/SolrOperationsController.php deleted file mode 100644 index 297c65c460..0000000000 --- a/lib/Controller/Settings/SolrOperationsController.php +++ /dev/null @@ -1,714 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Controller\Settings; - -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IRequest; -use OCP\IDBConnection; -use Psr\Container\ContainerInterface; -use Exception; -use ReflectionClass; -use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Service\Index\SetupHandler; -use Psr\Log\LoggerInterface; - -/** - * Controller for SOLR operations (setup, testing, indexing). - * - * Handles: - * - SOLR setup and initialization - * - Connection testing and diagnostics - * - Index warmup operations - * - Index inspection and statistics - * - Memory predictions - * - SOLR management operations - * - * @category Controller - * @package OCA\OpenRegister\Controller\Settings - */ -class SolrOperationsController extends Controller -{ - /** - * Constructor. - * - * @param string $appName The app name. - * @param IRequest $request The request. - * @param IDBConnection $db Database connection. - * @param ContainerInterface $container DI container. - * @param SettingsService $settingsService Settings service. - * @param IndexService $indexService Index service. - * @param LoggerInterface $logger Logger. - */ - public function __construct( - $appName, - IRequest $request, - private readonly IDBConnection $db, - private readonly ContainerInterface $container, - private readonly SettingsService $settingsService, - private readonly IndexService $indexService, - private readonly LoggerInterface $logger, - ) { - parent::__construct(appName: $appName, request: $request); - }//end __construct() - - /** - * Run SOLR setup to prepare for multi-tenant architecture - * - * @NoCSRFRequired - * - * @return JSONResponse The SOLR setup results - * - * @suppressWarnings(PHPMD.ExcessiveMethodLength) - * @suppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function setupSolr(): JSONResponse - { - try { - // Get logger for improved logging. - $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); - - // **IMPROVED LOGGING**: Log setup attempt with detailed context. - $logger->info( - message: '[SolrOperationsController] 🔧 SOLR setup endpoint called', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'timestamp' => date('c'), - 'user_id' => $this->userId ?? 'unknown', - 'request_id' => $this->request->getId() ?? 'unknown', - ] - ); - - // Get SOLR settings. - $solrSettings = $this->settingsService->getSolrSettings(); - - // Determine port value for configuration display. - $portValue = 'default'; - if (($solrSettings['port'] !== null) === true && ($solrSettings['port'] !== '') === true) { - $portValue = $solrSettings['port']; - } - - // **IMPROVED LOGGING**: Log SOLR configuration (without sensitive data). - $logger->info( - message: '[SolrOperationsController] 📋 SOLR configuration loaded for setup', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'enabled' => $solrSettings['enabled'] ?? false, - 'host' => $solrSettings['host'] ?? 'not_set', - 'port' => $solrSettings['port'] ?? 'not_set', - 'has_credentials' => empty($solrSettings['username']) === false - && empty($solrSettings['password']) === false, - ] - ); - - // Create SolrSetup using IndexService for authenticated HTTP client. - $guzzleSolrService = $this->container->get(IndexService::class); - $setup = new SetupHandler(solrService: $guzzleSolrService, logger: $logger); - - // **IMPROVED LOGGING**: Log setup initialization. - $logger->info( - message: '[SolrOperationsController] SolrSetup instance created, starting setup process', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - // Run setup. - $setupResult = $setup->setupSolr(); - - if ($setupResult === true) { - // Get detailed setup progress and infrastructure info from SolrSetup. - $setupProgress = $setup->getSetupProgress(); - $infraCreated = $setup->getInfrastructureCreated(); - - // **IMPROVED LOGGING**: Log successful setup. - $logger->info( - message: '[SolrOperationsController] ✅ SOLR setup completed successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'completed_steps' => $setupProgress['completed_steps'] ?? 0, - 'total_steps' => $setupProgress['total_steps'] ?? 0, - 'duration' => $setupProgress['completed_at'] ?? 'unknown', - 'infrastructure' => $infraCreated, - ] - ); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'SOLR setup completed successfully', - 'timestamp' => date('Y-m-d H:i:s'), - 'mode' => 'SolrCloud', - 'progress' => [ - 'started_at' => $setupProgress['started_at'] ?? null, - 'completed_at' => $setupProgress['completed_at'] ?? null, - 'total_steps' => $setupProgress['total_steps'] ?? 5, - 'completed_steps' => $setupProgress['completed_steps'] ?? 5, - 'success' => $setupProgress['success'] ?? true, - ], - 'steps' => $setupProgress['steps'] ?? [], - 'infrastructure' => $infraCreated, - 'next_steps' => [ - 'Tenant-specific resources are ready for use', - 'Objects can now be indexed to SOLR', - 'Search functionality is ready for use', - ], - ] - ); - }//end if - - // Get detailed error information and setup progress from SolrSetup. - $errorDetails = $setup->getLastErrorDetails(); - $setupProgress = $setup->getSetupProgress(); - - if ($errorDetails !== null && $errorDetails !== '') { - // Get infrastructure info even on failure to show partial progress. - $infraCreated = $setup->getInfrastructureCreated(); - - // Build troubleshooting steps from error details. - $troubleshooting = $errorDetails['troubleshooting'] ?? $errorDetails['troubleshooting_tips']; - $defaultSteps = [ - 'Check SOLR server connectivity', - 'Verify SOLR configuration', - 'Check SOLR server logs', - ]; - $troubleshootingSteps = $troubleshooting ?? $defaultSteps; - - // Use the detailed error information from SolrSetup. - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR setup failed', - 'timestamp' => date('Y-m-d H:i:s'), - 'mode' => 'SolrCloud', - 'progress' => [ - 'started_at' => $setupProgress['started_at'] ?? null, - 'completed_at' => $setupProgress['completed_at'] ?? null, - 'total_steps' => $setupProgress['total_steps'] ?? 5, - 'completed_steps' => $setupProgress['completed_steps'] ?? 0, - 'success' => false, - 'failed_at_step' => $errorDetails['step'] ?? 'unknown', - 'failed_step_name' => $errorDetails['step_name'] ?? 'unknown', - ], - 'steps' => $setupProgress['steps'] ?? [], - 'infrastructure' => $infraCreated, - 'error_details' => [ - 'primary_error' => $errorDetails['error_message'] ?? 'SOLR setup operation failed', - 'error_type' => $errorDetails['error_type'] ?? 'unknown_error', - 'operation' => $errorDetails['operation'] ?? 'unknown_operation', - 'step' => $errorDetails['step'] ?? 'unknown', - 'step_name' => $errorDetails['step_name'] ?? 'unknown', - 'url_attempted' => $errorDetails['url_attempted'] ?? 'unknown', - 'exception_type' => $errorDetails['exception_type'] ?? 'unknown', - 'error_category' => $errorDetails['error_category'] ?? 'unknown', - 'solr_response' => $errorDetails['full_solr_response'] ?? null, - 'guzzle_details' => $errorDetails['guzzle_details'] ?? [], - 'configuration_used' => [ - 'host' => $solrSettings['host'], - 'port' => $portValue, - 'scheme' => $solrSettings['scheme'], - 'path' => $solrSettings['path'], - ], - ], - 'troubleshooting_steps' => $troubleshootingSteps, - ], - statusCode: 422 - ); - }//end if - - // Fallback to generic error if no detailed error information is available. - $lastError = error_get_last(); - - // Get last system error message. - $lastSystemError = 'No system error captured'; - if ($lastError !== null && (($lastError['message'] ?? null) !== null)) { - $lastSystemError = $lastError['message']; - } - - // Get port value or default for fallback error response. - $portValueFallback = 'default'; - if ($solrSettings['port'] !== null && $solrSettings['port'] !== '') { - $portValueFallback = $solrSettings['port']; - } - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR setup failed', - 'timestamp' => date('Y-m-d H:i:s'), - 'error_details' => [ - 'primary_error' => 'Setup failed but no detailed error information was captured', - 'last_system_error' => $lastSystemError, - 'configuration_used' => [ - 'host' => $solrSettings['host'], - 'port' => $portValueFallback, - 'scheme' => $solrSettings['scheme'], - 'path' => $solrSettings['path'], - ], - ], - 'troubleshooting_steps' => [ - 'Check SOLR server logs for detailed error messages', - 'Verify SOLR server connectivity', - 'Check SOLR configuration', - ], - ], - statusCode: 422 - ); - } catch (Exception $e) { - // Get logger for error logging if not already available. - if (isset($logger) === false) { - $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); - } - - // **IMPROVED ERROR LOGGING**: Log detailed setup failure information. - $logger->error( - message: '[SolrOperationsController] ❌ SOLR setup failed with exception', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'exception_class' => get_class($e), - 'exception_message' => $e->getMessage(), - 'exception_file' => $e->getFile(), - 'exception_line' => $e->getLine(), - 'trace' => $e->getTraceAsString(), - ] - ); - - // Try to get detailed error information from SolrSetup if available. - $detailedError = null; - if (($setup ?? null) !== null) { - try { - $setupProgress = $setup->getSetupProgress(); - $lastErrorDetails = $setup->getLastErrorDetails(); - - $detailedError = [ - 'setup_progress' => $setupProgress, - 'last_error_details' => $lastErrorDetails, - 'failed_at_step' => $setupProgress['completed_steps'] ?? 0, - 'total_steps' => $setupProgress['total_steps'] ?? 5, - ]; - - // **IMPROVED LOGGING**: Log setup progress and error details. - $logger->error( - message: '[SolrOperationsController] SOLR setup failure details', - context: array_merge( - ['file' => __FILE__, 'line' => __LINE__], - $detailedError - ) - ); - } catch (Exception $progressException) { - $logger->warning( - message: '[SolrOperationsController] Failed to get setup progress details', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $progressException->getMessage(), - ] - ); - }//end try - }//end if - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR setup failed: '.$e->getMessage(), - 'timestamp' => date('Y-m-d H:i:s'), - 'error' => [ - 'type' => get_class($e), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'detailed_error' => $detailedError, - ], - ], - statusCode: 422 - ); - }//end try - }//end setupSolr() - - /** - * Test SOLR connection with provided settings (basic connectivity and authentication only) - * - * @NoCSRFRequired - * - * @return JSONResponse The test results - * - * @psalm-return JSONResponse<200, array, - * array>|JSONResponse<422, - * array{success: false, message: string, - * details: array{exception: string}}, array> - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function testSolrConnection(): JSONResponse - { - try { - // Test only basic SOLR connectivity and authentication. - // Does NOT test collections, queries, or Zookeeper. - $guzzleSolrService = $this->container->get(IndexService::class); - $result = $guzzleSolrService->testConnectivityOnly(); - - return new JSONResponse(data: $result); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Connection test failed: '.$e->getMessage(), - 'details' => ['exception' => $e->getMessage()], - ], - statusCode: 422 - ); - } - }//end testSolrConnection() - - /** - * Warmup SOLR index - * - * @NoCSRFRequired - * - * @return JSONResponse Warmup operation results - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function warmupSolrIndex(): JSONResponse - { - try { - // Get request parameters from JSON body or query parameters. - $maxObjects = $this->request->getParam('maxObjects', 0); - $batchSize = $this->request->getParam('batchSize', 1000); - $mode = $this->request->getParam('mode', 'serial'); - // New mode parameter. - $collectErrors = $this->request->getParam('collectErrors', false); - // New error collection parameter. - $schemaIds = $this->request->getParam('selectedSchemas', []); - // New schema selection parameter. - // Try to get from JSON body if not in query params. - if ($maxObjects === 0) { - $input = file_get_contents('php://input'); - if ($input !== false && $input !== '') { - $data = json_decode($input, true); - if ($data !== null && $data !== false) { - $maxObjects = $data['maxObjects'] ?? 0; - $batchSize = $data['batchSize'] ?? 1000; - $mode = $data['mode'] ?? 'serial'; - $collectErrors = $data['collectErrors'] ?? false; - $schemaIds = $data['selectedSchemas'] ?? []; - } - } - } - - // Convert string boolean to actual boolean. - if (is_string($collectErrors) === true) { - $collectErrors = filter_var($collectErrors, FILTER_VALIDATE_BOOLEAN); - } - - // Validate mode parameter. - if (in_array($mode, ['serial', 'parallel', 'hyper'], true) === false) { - return new JSONResponse( - data: [ - 'error' => 'Invalid mode parameter. Must be "serial", "parallel", or "hyper"', - ], - statusCode: 400 - ); - } - - // Debug logging for schema IDs. - $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); - $logger->info( - message: '[SolrOperationsController] 🔥 WARMUP: Received warmup request', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'maxObjects' => $maxObjects, - 'mode' => $mode, - 'batchSize' => $batchSize, - 'schemaIds' => $schemaIds, - 'schemaIds_type' => gettype($schemaIds), - 'schemaIds_count' => $this->getSchemaIdsCount(schemaIds: $schemaIds), - ] - ); - - // Phase 1: Use IndexService directly for SOLR operations. - $guzzleSolrService = $this->container->get(IndexService::class); - $result = $guzzleSolrService->warmupIndex( - schemas: [], - maxObjects: $maxObjects, - mode: $mode, - collectErrors: $collectErrors, - batchSize: $batchSize, - schemaIds: $schemaIds - ); - return new JSONResponse(data: $result); - } catch (Exception $e) { - // **ERROR VISIBILITY**: Let exceptions bubble up with full details. - return new JSONResponse( - data: [ - 'error' => $e->getMessage(), - 'exception_class' => get_class($e), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'trace' => $e->getTraceAsString(), - ], - statusCode: 500 - ); - }//end try - }//end warmupSolrIndex() - - /** - * Inspect SOLR index documents - * - * @NoCSRFRequired - * - * @return JSONResponse - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function inspectSolrIndex(): JSONResponse - { - try { - $query = $this->request->getParam('query', '*:*'); - $start = (int) $this->request->getParam('start', 0); - $rows = (int) $this->request->getParam('rows', 20); - $fields = $this->request->getParam('fields', ''); - - // Validate parameters. - $rows = min(max($rows, 1), 100); - // Limit between 1 and 100. - $start = max($start, 0); - - // Get IndexService from container. - $guzzleSolrService = $this->container->get(IndexService::class); - - // Search documents in SOLR. - $result = $guzzleSolrService->inspectIndex(query: $query, start: $start, rows: $rows, fields: $fields); - - if ($result['success'] === true) { - return new JSONResponse( - data: [ - 'success' => true, - 'documents' => $result['documents'], - 'total' => $result['total'], - 'start' => $start, - 'rows' => $rows, - 'query' => $query, - ] - ); - } - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $result['error'], - 'error_details' => $result['error_details'] ?? null, - ], - statusCode: 422 - ); - } catch (Exception $e) { - $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); - $logger->error( - message: '[SolrOperationsController] Exception in inspectSolrIndex controller', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Controller exception: '.$e->getMessage(), - 'error_details' => [ - 'exception_type' => get_class($e), - 'trace' => $e->getTraceAsString(), - ], - ], - statusCode: 500 - ); - }//end try - }//end inspectSolrIndex() - - /** - * Get memory usage prediction for SOLR warmup - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with memory prediction - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function getSolrMemoryPrediction(): JSONResponse - { - try { - // Get request parameters. - $maxObjects = (int) $this->request->getParam('maxObjects', 0); - - // Get IndexService for prediction. - $guzzleSolrService = $this->container->get(IndexService::class); - - if ($guzzleSolrService->isAvailable() === false) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR is not available or not configured', - 'prediction' => [ - 'error' => 'SOLR service unavailable', - 'prediction_safe' => false, - ], - ], - statusCode: 422 - ); - } - - // Use reflection to call the private method (for API access). - $reflection = new ReflectionClass($guzzleSolrService); - $method = $reflection->getMethod('predictWarmupMemoryUsage'); - $prediction = $method->invoke($guzzleSolrService, $maxObjects); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Memory prediction calculated successfully', - 'prediction' => $prediction, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to calculate memory prediction: '.$e->getMessage(), - 'prediction' => [ - 'error' => $e->getMessage(), - 'prediction_safe' => false, - ], - ], - statusCode: 422 - ); - }//end try - }//end getSolrMemoryPrediction() - - /** - * Get the count of schema IDs. - * - * @param array $schemaIds The schema IDs to count. - * - * @return int The number of schema IDs. - */ - private function getSchemaIdsCount(array $schemaIds): int - { - return count($schemaIds); - }//end getSchemaIdsCount() - - /** - * Perform SOLR management operations - * - * @param string $operation Operation to perform (commit, optimize, clear) - * - * @return JSONResponse Operation results - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse<200|400|500, - * array{error?: mixed|null|string, success?: false|mixed, - * operation?: 'clear'|'commit'|'optimize', message?: string, - * timestamp?: string, error_details?: mixed|null}, - * array> - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function manageSolr(string $operation): JSONResponse - { - try { - // Phase 1: Use IndexService directly for SOLR operations. - $guzzleSolrService = $this->container->get(IndexService::class); - - switch ($operation) { - case 'commit': - $success = $guzzleSolrService->commit(); - - // Get commit message based on success. - $message = 'Failed to commit index'; - if ($success === true) { - $message = 'Index committed successfully'; - } - return new JSONResponse( - data: [ - 'success' => $success, - 'operation' => 'commit', - 'message' => $message, - 'timestamp' => date('c'), - ] - ); - - case 'optimize': - $success = $guzzleSolrService->optimize(); - - // Get optimize message based on success. - $message = 'Failed to optimize index'; - if ($success === true) { - $message = 'Index optimized successfully'; - } - return new JSONResponse( - data: [ - 'success' => $success, - 'operation' => 'optimize', - 'message' => $message, - 'timestamp' => date('c'), - ] - ); - - case 'clear': - $result = $guzzleSolrService->clearIndex(); - - // Get clear message based on success. - $message = 'Failed to clear index: '.($result['error'] ?? 'Unknown error'); - if ($result['success'] === true) { - $message = 'Index cleared successfully'; - } - return new JSONResponse( - data: [ - 'success' => $result['success'], - 'operation' => 'clear', - 'error' => $result['error'] ?? null, - 'error_details' => $result['error_details'] ?? null, - 'message' => $message, - 'timestamp' => date('c'), - ] - ); - - default: - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Unknown operation: '.$operation, - ], - statusCode: 400 - ); - }//end switch - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - }//end try - }//end manageSolr() -}//end class diff --git a/lib/Controller/Settings/SolrSettingsController.php b/lib/Controller/Settings/SolrSettingsController.php deleted file mode 100644 index db55089b71..0000000000 --- a/lib/Controller/Settings/SolrSettingsController.php +++ /dev/null @@ -1,499 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Controller\Settings; - -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IRequest; -use Exception; -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Service\SettingsService; -use Psr\Log\LoggerInterface; -use Psr\Container\ContainerInterface; - -/** - * Controller for SOLR configuration settings. - * - * Handles: - * - SOLR connection settings (get/update) - * - SOLR facet configuration - * - Facet discovery - * - SOLR info and statistics - * - * @category Controller - * @package OCA\OpenRegister\Controller\Settings - */ -class SolrSettingsController extends Controller -{ - /** - * Constructor. - * - * @param string $appName The app name. - * @param IRequest $request The request. - * @param SettingsService $settingsService Settings service. - * @param IndexService $indexService Index service. - * @param ContainerInterface $container Container for service access. - * @param LoggerInterface $logger Logger. - */ - public function __construct( - $appName, - IRequest $request, - private readonly SettingsService $settingsService, - private readonly IndexService $indexService, - private readonly ContainerInterface $container, - private readonly LoggerInterface $logger, - ) { - parent::__construct(appName: $appName, request: $request); - }//end __construct() - - /** - * Get SOLR settings only - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with SOLR settings - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-18 - */ - public function getSolrSettings(): JSONResponse - { - try { - $data = $this->settingsService->getSolrSettingsOnly(); - return new JSONResponse(data: $data); - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - } - }//end getSolrSettings() - - /** - * Update SOLR settings only - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with updated SOLR settings - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-18 - */ - public function updateSolrSettings(): JSONResponse - { - try { - $data = $this->request->getParams(); - $result = $this->settingsService->updateSolrSettingsOnly($data); - return new JSONResponse(data: $result); - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - } - }//end updateSolrSettings() - - /** - * Get Solr information and vector search capabilities - * - * Returns information about Solr availability, version, and vector search support. - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with SOLR info - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-18 - */ - public function getSolrInfo(): JSONResponse - { - try { - $solrAvailable = false; - $solrVersion = 'Unknown'; - $vectorSupport = false; - $collections = []; - $errorMessage = null; - - // Check if Solr service is available. - try { - // Get IndexService from container. - $guzzleSolrService = $this->container->get(IndexService::class); - $solrAvailable = $guzzleSolrService->isAvailable(); - - if ($solrAvailable === true) { - // Try to detect version from Solr admin API. - // Note: Dashboard stats not currently used but available via $guzzleSolrService->getDashboardStats() - // For now, assume if it's available, it could support vectors. - // TODO: Add actual version detection from Solr admin API. - $solrVersion = '9.x (detection pending)'; - $vectorSupport = false; - // Set to false until we implement it. - // Get list of collections from Solr. - try { - $collectionsList = $guzzleSolrService->listCollections(); - // Transform to format expected by frontend (array of objects with 'name' and 'id'). - $collections = array_map( - // Maps collection array to frontend format. - function (array $collection): array { - return [ - 'id' => $collection['name'], - 'name' => $collection['name'], - 'documentCount' => $collection['documentCount'] ?? 0, - 'shards' => $collection['shards'] ?? 0, - 'health' => $collection['health'] ?? 'unknown', - ]; - }, - $collectionsList - ); - } catch (Exception $e) { - $this->logger->warning( - message: '[SettingsController] Failed to list Solr collections', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - $collections = []; - }//end try - }//end if - } catch (Exception $e) { - $errorMessage = $e->getMessage(); - }//end try - - return new JSONResponse( - data: [ - 'success' => true, - 'solr' => [ - 'available' => $solrAvailable, - 'version' => $solrVersion, - 'vectorSupport' => $vectorSupport, - 'collections' => $collections, - 'error' => $errorMessage, - ], - ] - ); - } catch (Exception $e) { - $this->logger->error( - message: '[SettingsController] Failed to get Solr info', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Failed to get Solr information: '.$e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end getSolrInfo() - - /** - * Get comprehensive SOLR dashboard statistics - * - * @NoCSRFRequired - * - * @return JSONResponse SOLR dashboard metrics and statistics - * - * @psalm-return JSONResponse<200, array, - * array>|JSONResponse<500, array{error: string}, - * array> - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-18 - */ - public function getSolrDashboardStats(): JSONResponse - { - try { - // Phase 1: Use IndexService directly for SOLR operations. - $guzzleSolrService = $this->container->get(IndexService::class); - $stats = $guzzleSolrService->getDashboardStats(); - return new JSONResponse(data: $stats); - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - } - }//end getSolrDashboardStats() - - /** - * Get SOLR facet configuration - * - * @NoCSRFRequired - * - * @return JSONResponse SOLR facet configuration - * - * @psalm-return JSONResponse<200|500, array, array> - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-19 - */ - public function getSolrFacetConfiguration(): JSONResponse - { - try { - $data = $this->settingsService->getSolrFacetConfiguration(); - return new JSONResponse(data: $data); - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - } - }//end getSolrFacetConfiguration() - - /** - * Update SOLR facet configuration - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with updated facet configuration - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-19 - */ - public function updateSolrFacetConfiguration(): JSONResponse - { - try { - $data = $this->request->getParams(); - $result = $this->settingsService->updateSolrFacetConfiguration($data); - return new JSONResponse(data: $result); - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - } - }//end updateSolrFacetConfiguration() - - /** - * Discover available SOLR facets - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with discovered facets - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-19 - */ - public function discoverSolrFacets(): JSONResponse - { - try { - // Get IndexService from container. - $guzzleSolrService = $this->container->get(IndexService::class); - - // Check if SOLR is available. - if ($guzzleSolrService->isAvailable() === false) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR is not available or not configured', - 'facets' => [], - ], - statusCode: 422 - ); - } - - // Get raw SOLR field information for facet configuration. - $facetableFields = $guzzleSolrService->getRawSolrFieldsForFacetConfiguration(); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Facets discovered successfully', - 'facets' => $facetableFields, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to discover facets: '.$e->getMessage(), - 'facets' => [], - ], - statusCode: 422 - ); - }//end try - }//end discoverSolrFacets() - - /** - * Get SOLR facet configuration with discovery - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with facet config and discovery - * - * @suppressWarnings(PHPMD.ExcessiveMethodLength) - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-19 - */ - public function getSolrFacetConfigWithDiscovery(): JSONResponse - { - try { - // Get IndexService from container. - $guzzleSolrService = $this->container->get(IndexService::class); - - // Check if SOLR is available. - if ($guzzleSolrService->isAvailable() === false) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'SOLR is not available or not configured', - 'facets' => [], - ], - statusCode: 422 - ); - } - - // Get discovered facets. - $discoveredFacets = $guzzleSolrService->getRawSolrFieldsForFacetConfiguration(); - - // Get existing configuration. - $existingConfig = $this->settingsService->getSolrFacetConfiguration(); - $existingFacets = $existingConfig['facets'] ?? []; - - // Merge discovered facets with existing configuration. - $mergedFacets = [ - '@self' => [], - 'object_fields' => [], - ]; - - // Process metadata facets. - if (($discoveredFacets['@self'] ?? null) !== null) { - $index = 0; - foreach ($discoveredFacets['@self'] as $key => $facetInfo) { - $fieldName = "self_{$key}"; - $existingFacetConfig = $existingFacets[$fieldName] ?? []; - - $category = $facetInfo['category'] ?? 'metadata'; - $displayName = $facetInfo['displayName'] ?? $key; - $description = $existingFacetConfig['description'] ?? $category.' field: '.$displayName; - $suggestedFacet = $facetInfo['suggestedFacetType'] ?? 'terms'; - $existingFacetType = $existingFacetConfig['facet_type'] ?? $existingFacetConfig['facetType']; - $facetType = $existingFacetType ?? $suggestedFacet; - $suggestedDisp = $facetInfo['suggestedDisplayTypes'][0] ?? 'select'; - $existingDisplayType = $existingFacetConfig['display_type'] ?? $existingFacetConfig['displayType']; - $displayType = $existingDisplayType ?? $suggestedDisp; - $existingShowCount = $existingFacetConfig['show_count'] ?? $existingFacetConfig['showCount']; - $showCount = $existingShowCount ?? true; - $existingMaxItems = $existingFacetConfig['max_items'] ?? $existingFacetConfig['maxItems']; - $maxItems = $existingMaxItems ?? 10; - - $mergedFacets['@self'][$key] = array_merge( - $facetInfo, - [ - 'config' => [ - 'enabled' => $existingFacetConfig['enabled'] ?? true, - 'title' => $existingFacetConfig['title'] ?? $displayName, - 'description' => $description, - 'order' => $existingFacetConfig['order'] ?? $index, - 'maxItems' => $maxItems, - 'facetType' => $facetType, - 'displayType' => $displayType, - 'showCount' => $showCount, - ], - ] - ); - $index++; - }//end foreach - }//end if - - // Process object field facets. - if (($discoveredFacets['object_fields'] ?? null) !== null) { - $index = 0; - foreach ($discoveredFacets['object_fields'] as $key => $facetInfo) { - $fieldName = $key; - $existingFacetConfig = $existingFacets[$fieldName] ?? []; - - $category = $facetInfo['category'] ?? 'object'; - $displayName = $facetInfo['displayName'] ?? $key; - $description = $existingFacetConfig['description'] ?? $category.' field: '.$displayName; - $suggestedFacet = $facetInfo['suggestedFacetType'] ?? 'terms'; - $existingFacetType = $existingFacetConfig['facet_type'] ?? $existingFacetConfig['facetType']; - $facetType = $existingFacetType ?? $suggestedFacet; - $suggestedDisp = $facetInfo['suggestedDisplayTypes'][0] ?? 'select'; - $existingDisplayType = $existingFacetConfig['display_type'] ?? $existingFacetConfig['displayType']; - $displayType = $existingDisplayType ?? $suggestedDisp; - $existingShowCount = $existingFacetConfig['show_count'] ?? $existingFacetConfig['showCount']; - $showCount = $existingShowCount ?? true; - $existingMaxItems = $existingFacetConfig['max_items'] ?? $existingFacetConfig['maxItems']; - $maxItems = $existingMaxItems ?? 10; - - $mergedFacets['object_fields'][$key] = array_merge( - $facetInfo, - [ - 'config' => [ - 'enabled' => $existingFacetConfig['enabled'] ?? false, - 'title' => $existingFacetConfig['title'] ?? $displayName, - 'description' => $description, - 'order' => $existingFacetConfig['order'] ?? (100 + $index), - 'maxItems' => $maxItems, - 'facetType' => $facetType, - 'displayType' => $displayType, - 'showCount' => $showCount, - ], - ] - ); - $index++; - }//end foreach - }//end if - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Facets discovered and configured successfully', - 'facets' => $mergedFacets, - 'global_settings' => $existingConfig['default_settings'] ?? [ - 'show_count' => true, - 'show_empty' => false, - 'max_items' => 10, - ], - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to get facet configuration: '.$e->getMessage(), - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end getSolrFacetConfigWithDiscovery() - - /** - * Update SOLR facet configuration with discovery - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with updated facet config - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-19 - */ - public function updateSolrFacetConfigWithDiscovery(): JSONResponse - { - try { - $data = $this->request->getParams(); - $result = $this->settingsService->updateSolrFacetConfiguration($data); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Facet configuration updated successfully', - 'config' => $result, - ] - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => 'Failed to update facet configuration: '.$e->getMessage(), - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end updateSolrFacetConfigWithDiscovery() -}//end class diff --git a/lib/Controller/Settings/ValidationSettingsController.php b/lib/Controller/Settings/ValidationSettingsController.php index d3edcc3583..d72a830fc6 100644 --- a/lib/Controller/Settings/ValidationSettingsController.php +++ b/lib/Controller/Settings/ValidationSettingsController.php @@ -67,7 +67,7 @@ public function __construct( * * @return JSONResponse JSON response with validation results * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-7 + * @spec openspec/specs/production-observability/spec.md */ public function validateAllObjects(): JSONResponse { @@ -102,7 +102,7 @@ public function validateAllObjects(): JSONResponse * * @suppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-7 + * @spec openspec/specs/production-observability/spec.md */ public function massValidateObjects(): JSONResponse { @@ -187,7 +187,7 @@ public function massValidateObjects(): JSONResponse * * @return JSONResponse JSON response with memory prediction * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-7 + * @spec openspec/specs/production-observability/spec.md */ public function predictMassValidationMemory(): JSONResponse { diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 845b74276f..64bd09e8da 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -34,8 +34,6 @@ use ReflectionClass; use DateTime; use stdClass; -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Service\Index\SetupHandler; use OCP\App\IAppManager; use OCA\OpenRegister\Service\SettingsService; use OCA\OpenRegister\Service\VectorizationService; @@ -53,7 +51,6 @@ * - Validate HTTP request parameters * - Delegate settings CRUD operations to SettingsService * - Delegate LLM testing to VectorizationService and ChatService - * - Delegate SOLR testing to IndexService * - Return appropriate JSONResponse with correct HTTP status codes * - Handle HTTP-level concerns (authentication, CSRF, etc.) * @@ -86,12 +83,6 @@ * - PUT /api/settings/retention - Update retention settings * - PATCH /api/settings/retention - Patch retention settings * - * SOLR SETTINGS: - * - GET /api/settings/solr - Get SOLR settings - * - PUT /api/settings/solr - Update SOLR settings - * - PATCH /api/settings/solr - Patch SOLR settings - * - POST /api/settings/solr/test - Test SOLR connection (delegates to IndexService) - * - POST /api/settings/solr/warmup - Warmup SOLR index * * LLM SETTINGS: * - GET /api/settings/llm - Get LLM settings @@ -119,7 +110,6 @@ * - Settings storage/retrieval → SettingsService * - LLM embedding testing → VectorizationService * - LLM chat testing → ChatService - * - SOLR testing → IndexService * - Cache operations → Cache services * * @category Controller @@ -230,7 +220,7 @@ public function getConfigurationService(): ?\OCA\OpenRegister\Service\Configurat * * @return JSONResponse JSON response with settings data * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-1 + * @spec openspec/specs/production-observability/spec.md */ public function index(): JSONResponse { @@ -249,7 +239,7 @@ public function index(): JSONResponse * * @return JSONResponse JSON response with updated settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-1 + * @spec openspec/specs/production-observability/spec.md */ public function update(): JSONResponse { @@ -269,7 +259,7 @@ public function update(): JSONResponse * * @return JSONResponse JSON response with loaded settings * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-1 + * @spec openspec/specs/production-observability/spec.md */ public function load(): JSONResponse { @@ -281,26 +271,6 @@ public function load(): JSONResponse } }//end load() - /** - * Update the publishing options. - * - * @NoCSRFRequired - * - * @return JSONResponse JSON response with updated publishing options - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-1 - */ - public function updatePublishingOptions(): JSONResponse - { - try { - $data = $this->request->getParams(); - $result = $this->settingsService->updatePublishingOptions($data); - return new JSONResponse(data: $result); - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); - } - }//end updatePublishingOptions() - /** * Rebase all objects and logs with current retention settings. * @@ -311,7 +281,7 @@ public function updatePublishingOptions(): JSONResponse * * @return JSONResponse JSON response with rebase result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-11 + * @spec openspec/specs/retention-management/spec.md */ public function rebase(): JSONResponse { @@ -333,7 +303,7 @@ public function rebase(): JSONResponse * * @return JSONResponse JSON response with statistics * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-2 + * @spec openspec/specs/production-observability/spec.md */ public function stats(): JSONResponse { @@ -355,160 +325,13 @@ public function stats(): JSONResponse * * @return JSONResponse JSON response with statistics * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-2 + * @spec openspec/specs/production-observability/spec.md */ public function getStatistics(): JSONResponse { return $this->stats(); }//end getStatistics() - /** - * Test SOLR setup directly (bypassing SolrService) - * - * @NoCSRFRequired - * - * @return JSONResponse The SOLR setup test results - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-15 - */ - public function testSetupHandler(): JSONResponse - { - try { - // Get SOLR settings directly. - $solrSettings = $this->settingsService->getSolrSettings(); - - if (($solrSettings['enabled'] === false)) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $this->l10n->t('SOLR is disabled'), - ], - statusCode: 400 - ); - } - - // Create SolrSetup using IndexService for authenticated HTTP client. - $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); - $guzzleSolrService = $this->container->get(IndexService::class); - $setup = new SetupHandler(solrService: $guzzleSolrService, logger: $logger); - - // Run setup. - $result = $setup->setupSolr(); - - if ($result === true) { - return new JSONResponse( - data: [ - 'success' => true, - 'message' => $this->l10n->t('SOLR setup completed successfully'), - 'config' => [ - 'host' => $solrSettings['host'], - 'port' => $solrSettings['port'], - 'scheme' => $solrSettings['scheme'], - ], - ] - ); - } - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $this->l10n->t('SOLR setup failed - check logs'), - ], - statusCode: 422 - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $this->l10n->t('SOLR setup error: %s', [$e->getMessage()]), - ], - statusCode: 422 - ); - }//end try - }//end testSetupHandler() - - /** - * Reindex a specific SOLR collection by name - * - * @param string $name The name of the collection to reindex - * - * @return JSONResponse The reindex result - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse<200|400|422, - * array{success: bool, message: mixed|string, collection: string, - * stats?: array|mixed}, array> - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-15 - */ - public function reindexSpecificCollection(string $name): JSONResponse - { - try { - $guzzleSolrService = $this->container->get(IndexService::class); - - // Get optional parameters from request body. - $maxObjects = (int) ($this->request->getParam('maxObjects', 0)); - $batchSize = (int) ($this->request->getParam('batchSize', 1000)); - - // Validate parameters. - if ($batchSize < 1 || $batchSize > 5000) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $this->l10n->t('Invalid batch size. Must be between 1 and 5000'), - 'collection' => $name, - ], - statusCode: 400 - ); - } - - if ($maxObjects < 0) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $this->l10n->t('Invalid maxObjects. Must be 0 (all) or positive number'), - 'collection' => $name, - ], - statusCode: 400 - ); - } - - // Reindex the specified collection. - $result = $guzzleSolrService->reindexAll(maxObjects: $maxObjects, batchSize: $batchSize, collectionName: $name); - - if ($result['success'] === true) { - return new JSONResponse( - data: [ - 'success' => true, - 'message' => $this->l10n->t('Reindex completed successfully'), - 'stats' => $result['stats'] ?? [], - 'collection' => $name, - ], - statusCode: 200 - ); - } - - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $result['message'] ?? $this->l10n->t('Failed to reindex collection'), - 'collection' => $name, - ], - statusCode: 422 - ); - } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'message' => $this->l10n->t('Reindex failed: %s', [$e->getMessage()]), - 'collection' => $name, - ], - statusCode: 422 - ); - }//end try - }//end reindexSpecificCollection() - /** * Get search backend configuration. * @@ -520,7 +343,7 @@ public function reindexSpecificCollection(string $name): JSONResponse * * @psalm-return JSONResponse<200|500, array, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-15 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getSearchBackend(): JSONResponse { @@ -541,7 +364,7 @@ public function getSearchBackend(): JSONResponse * * @return JSONResponse JSON response with updated backend config * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-15 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function updateSearchBackend(): JSONResponse { @@ -587,7 +410,7 @@ public function updateSearchBackend(): JSONResponse * @suppressWarnings(PHPMD.CyclomaticComplexity) * @suppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-3 + * @spec openspec/specs/production-observability/spec.md */ public function getDatabaseInfo(): JSONResponse { @@ -668,7 +491,7 @@ public function getDatabaseInfo(): JSONResponse try { $stmt = $this->db->prepare('SELECT extname, extversion FROM pg_extension ORDER BY extname'); $result = $stmt->execute(); - while (($row = $result->fetch()) !== false) { + while (is_array($row = $result->fetch()) === true) { $extensions[] = [ 'name' => $row['extname'], 'version' => $row['extversion'], @@ -714,6 +537,7 @@ public function getDatabaseInfo(): JSONResponse 'recommendedPlugin' => $recommendedPlugin, 'performanceNote' => $performanceNote, 'extensions' => $extensions, + 'hybridSearch' => $this->getHybridSearchDiagnostics(isPostgres: $dbType === 'PostgreSQL'), 'lastUpdated' => (new DateTime())->format('c'), ]; @@ -763,7 +587,7 @@ public function getDatabaseInfo(): JSONResponse * * @return JSONResponse JSON response with refreshed database info * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-3 + * @spec openspec/specs/production-observability/spec.md */ public function refreshDatabaseInfo(): JSONResponse { @@ -775,62 +599,149 @@ public function refreshDatabaseInfo(): JSONResponse }//end refreshDatabaseInfo() /** - * Get version information only + * Hybrid-document-search readiness diagnostics. * - * @NoCSRFRequired + * Reports whether the hybrid-search schema surface exists — the + * `openregister_vec_ann` pgvector ANN sidecar table + HNSW index (the + * sidecar replaces the originally-designed in-table column, which broke + * Doctrine schema introspection) and the functional tsvector GIN index on + * openregister_chunks — plus vectorization/backfill progress so an + * operator can watch the job-only warm-up converge (DECIDED 2026-07-06). * - * @return JSONResponse JSON response with version info + * All lookups are tolerant: a failed catalog query reports `false`/zero + * rather than failing the database-info panel. + * + * @param bool $isPostgres Whether the active platform is PostgreSQL + * + * @return array Diagnostics payload * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-4 + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Independent tolerant lookups + * + * @spec openspec/changes/hybrid-document-search/tasks.md#6.1 */ - public function getVersionInfo(): JSONResponse + private function getHybridSearchDiagnostics(bool $isPostgres): array { + $diagnostics = [ + 'annSidecarTable' => false, + 'embeddingVectorDimension' => null, + 'hnswIndex' => false, + 'textSearchGinIndex' => false, + 'chunks' => [ + 'total' => 0, + 'vectorized' => 0, + ], + 'vectors' => [ + 'total' => 0, + 'pgvectorPopulated' => 0, + ], + ]; + + $sidecar = \OCA\OpenRegister\Service\Vectorization\Handlers\PgVectorPlatform::SIDECAR_TABLE; + + if ($isPostgres === true) { + try { + $result = $this->db->executeQuery( + 'SELECT a.atttypmod FROM pg_attribute a ' + ."WHERE a.attrelid = '".$sidecar."'::regclass " + ."AND a.attname = 'embedding' AND NOT a.attisdropped" + ); + $typmod = $result->fetchOne(); + $result->closeCursor(); + + if ($typmod !== false && (int) $typmod > 0) { + $diagnostics['annSidecarTable'] = true; + $diagnostics['embeddingVectorDimension'] = (int) $typmod; + } + } catch (\Throwable $e) { + // Sidecar unavailable — reported as false. + } + + try { + $result = $this->db->executeQuery( + 'SELECT indexname FROM pg_indexes WHERE indexname IN ' + ."('idx_or_vec_ann_hnsw', 'idx_or_chunks_text_search_gin')" + ); + while (is_array($row = $result->fetch()) === true) { + if ($row['indexname'] === 'idx_or_vec_ann_hnsw') { + $diagnostics['hnswIndex'] = true; + } + + if ($row['indexname'] === 'idx_or_chunks_text_search_gin') { + $diagnostics['textSearchGinIndex'] = true; + } + } + + $result->closeCursor(); + } catch (\Throwable $e) { + // Reported as false. + } + + try { + $result = $this->db->executeQuery( + 'SELECT COUNT(*) AS total, COUNT(a.vector_id) AS populated ' + .'FROM *PREFIX*openregister_vectors v ' + ."LEFT JOIN $sidecar a ON a.vector_id = v.id" + ); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row !== false) { + $diagnostics['vectors']['total'] = (int) $row['total']; + $diagnostics['vectors']['pgvectorPopulated'] = (int) $row['populated']; + } + } catch (\Throwable $e) { + // The ANN sidecar may not exist yet — plain row count only. + try { + $result = $this->db->executeQuery( + 'SELECT COUNT(*) AS total FROM *PREFIX*openregister_vectors' + ); + $total = $result->fetchOne(); + $result->closeCursor(); + + if ($total !== false) { + $diagnostics['vectors']['total'] = (int) $total; + } + } catch (\Throwable $inner) { + // Reported as zero. + } + }//end try + }//end if + + // Chunk vectorization progress (platform-agnostic). try { - $data = $this->settingsService->getVersionInfoOnly(); - return new JSONResponse(data: $data); - } catch (Exception $e) { - return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + $chunkMapper = $this->container->get(\OCA\OpenRegister\Db\ChunkMapper::class); + if ($chunkMapper instanceof \OCA\OpenRegister\Db\ChunkMapper) { + $diagnostics['chunks']['total'] = $chunkMapper->countAll(); + $diagnostics['chunks']['vectorized'] = $chunkMapper->countVectorized(); + } + } catch (\Throwable $e) { + $this->logger->debug( + message: '[SettingsController] Failed to fetch chunk vectorization progress', + context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] + ); } - }//end getVersionInfo() + + return $diagnostics; + }//end getHybridSearchDiagnostics() /** - * Test schema-aware SOLR mapping by indexing sample objects + * Get version information only * * @NoCSRFRequired * - * @return JSONResponse Test results - * - * @psalm-return JSONResponse<200, array, - * array>|JSONResponse<422, - * array{success: false, error: string}, array> + * @return JSONResponse JSON response with version info * - * @spec exclude Debug/test scaffolding endpoint: indexes sample objects to exercise schema-aware SOLR mapping; - * not a product contract (see proposal Notes — routed debug surface). + * @spec openspec/specs/production-observability/spec.md */ - public function testSchemaMapping(): JSONResponse + public function getVersionInfo(): JSONResponse { try { - // Get IndexService from container. - $solrService = $this->container->get(IndexService::class); - - // Get required dependencies from container. - $objectMapper = $this->container->get(\OCA\OpenRegister\Db\MagicMapper::class); - $schemaMapper = $this->container->get(\OCA\OpenRegister\Db\SchemaMapper::class); - - // Run the test. - $results = $solrService->testSchemaAwareMapping(objectMapper: $objectMapper, schemaMapper: $schemaMapper); - - return new JSONResponse(data: $results); + $data = $this->settingsService->getVersionInfoOnly(); + return new JSONResponse(data: $data); } catch (Exception $e) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 422 - ); - }//end try - }//end testSchemaMapping() + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: 500); + } + }//end getVersionInfo() /** * Debug endpoint for type filtering issue @@ -1032,7 +943,7 @@ function (array $row): array { * limit?: int, filters?: array, timestamp?: string}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-20 + * @spec openspec/specs/chat-ai/spec.md */ public function semanticSearch(string $query, int $limit=10, array $filters=[], ?string $provider=null): JSONResponse { @@ -1073,25 +984,25 @@ public function semanticSearch(string $query, int $limit=10, array $filters=[], }//end semanticSearch() /** - * Perform hybrid search combining SOLR keyword and vector semantic search + * Perform hybrid search combining keyword and vector semantic search * - * @param string $query Search query text - * @param int $limit Maximum number of results (default: 20) - * @param array $solrFilters SOLR-specific filters - * @param array $weights Search type weights ['solr' => 0.5, 'vector' => 0.5] - * @param string|null $provider Embedding provider override + * @param string $query Search query text + * @param int $limit Maximum number of results (default: 20) + * @param array $keywordResults Pre-fetched keyword search results to fuse + * @param array $weights Search type weights ['keyword' => 0.5, 'vector' => 0.5] + * @param string|null $provider Embedding provider override * * @NoCSRFRequired * * @return JSONResponse JSON response with hybrid search results * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-20 + * @spec openspec/specs/chat-ai/spec.md */ public function hybridSearch( string $query, int $limit=20, - array $solrFilters=[], - array $weights=['solr' => 0.5, 'vector' => 0.5], + array $keywordResults=[], + array $weights=['keyword' => 0.5, 'vector' => 0.5], ?string $provider=null ): JSONResponse { try { @@ -1107,7 +1018,7 @@ public function hybridSearch( $result = $this->vectorizationService->hybridSearch( query: $query, - solrFilters: $solrFilters, + keywordResults: $keywordResults, limit: $limit, weights: $weights, provider: $provider diff --git a/lib/Controller/ShareLinksController.php b/lib/Controller/ShareLinksController.php index 970651434e..eac271c3c5 100644 --- a/lib/Controller/ShareLinksController.php +++ b/lib/Controller/ShareLinksController.php @@ -25,7 +25,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/integration-shares/tasks.md + * @spec openspec/specs/integration-shares/spec.md */ declare(strict_types=1); @@ -80,7 +80,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -116,7 +116,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function create(string $register, string $schema, string $id): JSONResponse { @@ -192,7 +192,7 @@ public function create(string $register, string $schema, string $id): JSONRespon * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroy(string $register, string $schema, string $id, string $shareId): JSONResponse { @@ -224,7 +224,7 @@ public function destroy(string $register, string $schema, string $id, string $sh * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function files(string $register, string $schema, string $id): JSONResponse { diff --git a/lib/Controller/SolrController.php b/lib/Controller/SolrController.php deleted file mode 100644 index 024481782e..0000000000 --- a/lib/Controller/SolrController.php +++ /dev/null @@ -1,1233 +0,0 @@ - - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - */ - -namespace OCA\OpenRegister\Controller; - -use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\JSONResponse; -use OCP\IRequest; -use Psr\Container\ContainerInterface; -use OCA\OpenRegister\Service\VectorizationService; -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Db\MagicMapper; -use Psr\Log\LoggerInterface; - -/** - * SOLR Controller - * - * Handles all SOLR-related operations including: - * - Semantic search (vector embeddings) - * - Hybrid search (keyword + semantic) - * - Vector statistics - * - Collection management - * - ConfigSet management - * - * @category Controller - * @package OCA\OpenRegister\Controller - * - * @author Conduction Development Team - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-12 - * @version GIT: - * @link https://www.OpenRegister.app - * - * @psalm-suppress UnusedClass - * - * @suppressWarnings(PHPMD.ExcessiveClassLength) - * @suppressWarnings(PHPMD.ExcessiveClassComplexity) - * @suppressWarnings(PHPMD.TooManyPublicMethods) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ -class SolrController extends Controller -{ - /** - * Constructor - * - * @param string $appName The app name - * @param IRequest $request The request object - * @param ContainerInterface $container The DI container - * @param LoggerInterface $logger The logger - */ - public function __construct( - string $appName, - IRequest $request, - private readonly ContainerInterface $container, - private readonly LoggerInterface $logger - ) { - parent::__construct(appName: $appName, request: $request); - }//end __construct() - - /** - * Perform semantic search using vector embeddings - * - * This endpoint allows searching for similar content using AI-powered - * vector embeddings. It's particularly useful for finding conceptually - * similar documents even when they don't share exact keywords. - * - * @param string $query Search query text - * @param int $limit Maximum number of results (default: 10) - * @param array $filters Optional filters (entity_type, entity_id, embedding_model) - * @param string|null $provider Embedding provider override (openai, ollama) - * - * @return JSONResponse Search results with similarity scores - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|400|500, - * array{ - * success: bool, - * error?: string, - * query?: null|string, - * results?: mixed, - * total?: int<0, max>, - * limit?: int<1, 100>, - * filters?: array, - * search_type?: 'semantic', - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-20 - */ - public function semanticSearch( - string $query, - int $limit=10, - array $filters=[], - ?string $provider=null - ): JSONResponse { - try { - // Validate input. - if (trim($query) === '') { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Query parameter is required and cannot be empty', - ], - statusCode: 400 - ); - } - - if ($limit < 1 || $limit > 100) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Limit must be between 1 and 100', - ], - statusCode: 400 - ); - } - - // Get VectorizationService from container. - $vectorService = $this->container->get(VectorizationService::class); - - // Perform semantic search. - $results = $vectorService->semanticSearch(query: $query, limit: $limit, filters: $filters, provider: $provider); - - return new JSONResponse( - data: [ - 'success' => true, - 'query' => $query, - 'results' => $results, - 'total' => count($results), - 'limit' => $limit, - 'filters' => $filters, - 'search_type' => 'semantic', - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Semantic search failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'query' => $query ?? null, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - 'query' => $query ?? null, - ], - statusCode: 500 - ); - }//end try - }//end semanticSearch() - - /** - * Perform hybrid search combining SOLR keyword and vector semantic search - * - * This endpoint combines traditional keyword-based search (SOLR) with - * AI-powered semantic search for optimal results. Uses Reciprocal Rank - * Fusion (RRF) to intelligently merge results from both methods. - * - * @param string $query Search query text - * @param int $limit Maximum number of results (default: 20) - * @param array $solrFilters SOLR-specific filters - * @param array $weights Search type weights ['solr' => 0.5, 'vector' => 0.5] - * @param string|null $provider Embedding provider override - * - * @return JSONResponse Combined search results with source breakdown - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|400|500, - * array{ - * success: bool|mixed, - * error?: mixed|string, - * query?: mixed|null|string, - * search_type?: 'hybrid'|mixed, - * timestamp?: string, - * ... - * }, - * array - * > - * - * @suppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-20 - */ - public function hybridSearch( - string $query, - int $limit=20, - array $solrFilters=[], - array $weights=['solr' => 0.5, 'vector' => 0.5], - ?string $provider=null - ): JSONResponse { - try { - // Validate input. - if (trim($query) === '') { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Query parameter is required and cannot be empty', - ], - statusCode: 400 - ); - } - - if ($limit < 1 || $limit > 200) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Limit must be between 1 and 200', - ], - statusCode: 400 - ); - } - - // Validate weights. - $solrWeight = $weights['solr'] ?? 0.5; - $vectorWeight = $weights['vector'] ?? 0.5; - - if ($solrWeight < 0 || $solrWeight > 1 || $vectorWeight < 0 || $vectorWeight > 1) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Weights must be between 0 and 1', - ], - statusCode: 400 - ); - } - - // Get VectorizationService from container. - $vectorService = $this->container->get(VectorizationService::class); - - // Perform hybrid search. - $result = $vectorService->hybridSearch( - query: $query, - solrFilters: $solrFilters, - limit: $limit, - weights: $weights, - provider: $provider - ); - - // Ensure result is an array for spread operator. - $resultArray = []; - if (is_array($result) === true) { - $resultArray = $result; - } - - return new JSONResponse( - data: [ - 'success' => true, - 'query' => $query, - 'search_type' => 'hybrid', - ...$resultArray, - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Hybrid search failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'query' => $query ?? null, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - 'query' => $query ?? null, - ], - statusCode: 500 - ); - }//end try - }//end hybridSearch() - - /** - * Get vector embedding statistics - * - * Returns comprehensive statistics about stored vector embeddings including: - * - Total vector count - * - Breakdown by entity type (file/object) - * - Breakdown by embedding model - * - Storage metrics - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @return JSONResponse Vector statistics - * - * @psalm-return JSONResponse<200|500, - * array{success: bool, error?: string, stats?: mixed, timestamp?: string}, - * array> - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-21 - */ - public function getVectorStats(): JSONResponse - { - try { - // Get VectorizationService from container. - $vectorService = $this->container->get(VectorizationService::class); - - // Get statistics. - $stats = $vectorService->getVectorStats(); - - return new JSONResponse( - data: [ - 'success' => true, - 'stats' => $stats, - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to get vector stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end getVectorStats() - - /** - * Test vector embedding generation with a provider - * - * This endpoint allows testing embedding generation with different providers - * (OpenAI, Ollama, Fireworks) before enabling them in production. It generates - * an embedding for the provided test text and returns metadata about the result. - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @return JSONResponse Test results including embedding metadata - * - * @psalm-return JSONResponse< - * 200|400|500, - * array{ - * success: bool, - * error?: string, - * message?: 'Embedding generated successfully', - * metadata?: array{ - * provider: mixed, - * model: mixed|string, - * dimensions: int<0, max>, - * textLength: int<1, max>, - * duration_ms: float, - * firstValues: array - * }, - * timestamp?: string - * }, - * array - * > - * - * @suppressWarnings(PHPMD.ExcessiveMethodLength) - * @suppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-21 - * - * @NoAdminRequired - */ - public function testVectorEmbedding(): JSONResponse - { - try { - // Get request parameters. - $params = $this->request->getParams(); - $provider = $params['provider'] ?? null; - $config = $params['config'] ?? []; - $testText = $params['testText'] ?? 'This is a test embedding generation.'; - - // Validate provider. - if ($provider === null || $provider === '') { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Provider is required (openai, ollama, or fireworks)', - ], - statusCode: 400 - ); - } - - if (in_array($provider, ['openai', 'ollama', 'fireworks']) === false) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Invalid provider. Must be one of: openai, ollama, fireworks', - ], - statusCode: 400 - ); - } - - // Get VectorizationService from container. - $vectorService = $this->container->get(VectorizationService::class); - - // Build embedding configuration based on provider. - $embeddingConfig = [ - 'provider' => $provider, - ]; - - // Add provider-specific configuration. - switch ($provider) { - case 'openai': - if (($config['apiKey'] ?? '') === '') { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'OpenAI API key is required in config.apiKey', - ], - statusCode: 400 - ); - } - - $embeddingConfig['apiKey'] = $config['apiKey']; - $embeddingConfig['model'] = $config['model'] ?? 'text-embedding-3-small'; - break; - - case 'ollama': - $embeddingConfig['url'] = $config['url'] ?? 'http://localhost:11434'; - $embeddingConfig['model'] = $config['model'] ?? 'nomic-embed-text'; - break; - - case 'fireworks': - if (($config['apiKey'] ?? '') === '') { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Fireworks AI API key is required in config.apiKey', - ], - statusCode: 400 - ); - } - - $embeddingConfig['apiKey'] = $config['apiKey']; - $embeddingConfig['model'] = $config['model'] ?? 'nomic-ai/nomic-embed-text-v1.5'; - $embeddingConfig['baseUrl'] = $config['baseUrl'] ?? 'https://api.fireworks.ai/inference/v1'; - break; - }//end switch - - // Log the test attempt. - $this->logger->info( - message: '[SolrController] Testing vector embedding generation', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'provider' => $provider, - 'model' => $embeddingConfig['model'] ?? 'default', - 'textLength' => strlen($testText), - ] - ); - - // Generate test embedding with custom config. - $startTime = microtime(true); - $embedding = $vectorService->generateEmbeddingWithCustomConfig(text: $testText, config: $embeddingConfig); - $duration = round((microtime(true) - $startTime) * 1000, 2); - - if ($embedding === null || $embedding === []) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Failed to generate embedding. Check provider configuration and credentials.', - ], - statusCode: 500 - ); - } - - // Return success with metadata. - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Embedding generated successfully', - 'metadata' => [ - 'provider' => $provider, - 'model' => $embeddingConfig['model'] ?? 'default', - 'dimensions' => count($embedding), - 'textLength' => strlen($testText), - 'duration_ms' => $duration, - 'firstValues' => array_slice($embedding, 0, 5), - // First 5 values as preview. - ], - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to test vector embedding', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'provider' => $params['provider'] ?? 'unknown', - ] - ); - - return new JSONResponse( - [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end testVectorEmbedding() - - /** - * List all SOLR collections with their metadata - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @return JSONResponse Collection list - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool, - * error?: string, - * collections?: mixed, - * total?: int<0, max>, - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function listCollections(): JSONResponse - { - try { - $guzzleSolrService = $this->container->get(IndexService::class); - $collections = $guzzleSolrService->listCollections(); - - return new JSONResponse( - data: [ - 'success' => true, - 'collections' => $collections, - 'total' => count($collections), - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to list collections', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end listCollections() - - /** - * List all SOLR ConfigSets - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @return JSONResponse ConfigSet list - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool, - * error?: string, - * configSets?: mixed, - * total?: int<0, max>, - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function listConfigSets(): JSONResponse - { - try { - $guzzleSolrService = $this->container->get(IndexService::class); - $configSets = $guzzleSolrService->listConfigSets(); - - return new JSONResponse( - data: [ - 'success' => true, - 'configSets' => $configSets, - 'total' => count($configSets), - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to list ConfigSets', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end listConfigSets() - - /** - * Create a new SOLR collection - * - * @param string $collectionName Name for the new collection - * @param string $configName ConfigSet to use - * @param int $numShards Number of shards (default: 1) - * @param int $replicationFactor Replication factor (default: 1) - * @param int $maxShardsPerNode Max shards per node (default: 1) - * - * @return JSONResponse Creation result - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool, - * error?: string, - * message?: 'Collection created successfully', - * collection?: string, - * result?: mixed, - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function createCollection( - string $collectionName, - string $configName, - int $numShards=1, - int $replicationFactor=1, - int $maxShardsPerNode=1 - ): JSONResponse { - try { - $guzzleSolrService = $this->container->get(IndexService::class); - - $result = $guzzleSolrService->createCollection( - collectionName: $collectionName, - configSetName: $configName, - numShards: $numShards, - replicationFactor: $replicationFactor, - maxShardsPerNode: $maxShardsPerNode - ); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Collection created successfully', - 'collection' => $collectionName, - 'result' => $result, - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to create collection', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'collection' => $collectionName ?? null, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end createCollection() - - /** - * Create a new SOLR ConfigSet - * - * @param string $name Name for the new ConfigSet - * @param string $baseConfigSet Base ConfigSet to copy from (default: _default) - * - * @return JSONResponse Creation result - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool, - * error?: string, - * message?: 'ConfigSet created successfully', - * configSet?: string, - * result?: mixed, - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function createConfigSet(string $name, string $baseConfigSet='_default'): JSONResponse - { - try { - $guzzleSolrService = $this->container->get(IndexService::class); - - $result = $guzzleSolrService->createConfigSet(name: $name, baseConfigSet: $baseConfigSet); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'ConfigSet created successfully', - 'configSet' => $name, - 'result' => $result, - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to create ConfigSet', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'configSet' => $name ?? null, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end createConfigSet() - - /** - * Delete a SOLR ConfigSet - * - * @param string $name ConfigSet name to delete - * - * @return JSONResponse Deletion result - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool, - * error?: string, - * message?: 'ConfigSet deleted successfully', - * configSet?: string, - * result?: mixed, - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function deleteConfigSet(string $name): JSONResponse - { - try { - $guzzleSolrService = $this->container->get(IndexService::class); - - $result = $guzzleSolrService->deleteConfigSet($name); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'ConfigSet deleted successfully', - 'configSet' => $name, - 'result' => $result, - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to delete ConfigSet', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'configSet' => $name ?? null, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end deleteConfigSet() - - /** - * Copy/duplicate an existing SOLR collection - * - * @param string $sourceCollection Source collection name - * @param string $targetCollection Target collection name - * - * @return JSONResponse Copy result - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool, - * error?: string, - * message?: 'Collection copied successfully', - * source?: string, - * target?: string, - * result?: mixed, - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-16 - */ - public function copyCollection(string $sourceCollection, string $targetCollection): JSONResponse - { - try { - $guzzleSolrService = $this->container->get(IndexService::class); - - $result = $guzzleSolrService->copyCollection( - sourceCollection: $sourceCollection, - targetCollection: $targetCollection - ); - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Collection copied successfully', - 'source' => $sourceCollection, - 'target' => $targetCollection, - 'result' => $result, - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to copy collection', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'source' => $sourceCollection ?? null, - 'target' => $targetCollection ?? null, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end copyCollection() - - /** - * Vectorize a single object by ID - * - * This endpoint generates an AI embedding for an object and stores it - * in the vector database for semantic search. - * - * @param int $objectId Object ID to vectorize - * @param string|null $provider Optional embedding provider override - * - * @return JSONResponse Vectorization result - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool|mixed, - * error?: mixed|string, - * object_id?: int|mixed|null, - * message?: 'Object vectorized successfully'|mixed, - * timestamp?: string, - * ... - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-21 - */ - public function vectorizeObject(int $objectId, ?string $provider=null): JSONResponse - { - try { - // Get services from container. - $objectMapper = $this->container->get(MagicMapper::class); - $solrObjectService = $this->container->get(IndexService::class); - - // Fetch the object. - $object = $objectMapper->find($objectId); - - // Vectorize the object. - $result = $solrObjectService->vectorizeObject(object: $object, provider: $provider); - - // Ensure result is an array for spread operator. - $resultArray = []; - if (is_array($result) === true) { - $resultArray = $result; - } - - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'Object vectorized successfully', - ...$resultArray, - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to vectorize object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'object_id' => $objectId ?? null, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - 'object_id' => $objectId ?? null, - ], - statusCode: 500 - ); - }//end try - }//end vectorizeObject() - - /** - * Bulk vectorize objects with optional filtering - * - * This endpoint allows vectorizing multiple objects at once, optionally - * filtered by schema or register. Supports pagination for large datasets. - * - * @param int|null $schemaId Optional schema ID to filter - * @param int|null $registerId Optional register ID to filter - * @param int $limit Maximum objects to process (default: 100, max: 1000) - * @param int $offset Offset for pagination (default: 0) - * @param string|null $provider Optional embedding provider override - * - * @return JSONResponse Bulk vectorization results with progress - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @psalm-return JSONResponse< - * 200|400|500, - * array{ - * success: bool|mixed, - * error?: mixed|string, - * message?: mixed|string, - * total?: 0|mixed, - * successful?: 0|mixed, - * failed?: 0|mixed, - * results?: array|mixed, - * timestamp?: string, - * pagination?: array{limit: int<1, 1000>, offset: int<0, max>, has_more: bool}, - * filters?: array{schema_id: int|null, register_id: int|null}, - * ... - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-21 - * - * @NoAdminRequired - */ - public function bulkVectorizeObjects( - ?int $schemaId=null, - ?int $registerId=null, - int $limit=100, - int $offset=0, - ?string $provider=null - ): JSONResponse { - try { - // Validate limits. - if ($limit < 1 || $limit > 1000) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Limit must be between 1 and 1000', - ], - statusCode: 400 - ); - } - - if ($offset < 0) { - return new JSONResponse( - data: [ - 'success' => false, - 'error' => 'Offset must be >= 0', - ], - statusCode: 400 - ); - } - - // Get services from container. - $objectMapper = $this->container->get(MagicMapper::class); - $solrObjectService = $this->container->get(IndexService::class); - - // Fetch objects. - // Note: This is a simplified example - adjust based on actual MagicMapper methods. - // TODO: Apply schema/register filters when MagicMapper supports them. - $objects = $objectMapper->findAll(limit: $limit, offset: $offset); - - if (count($objects) === 0) { - return new JSONResponse( - data: [ - 'success' => true, - 'message' => 'No objects found to vectorize', - 'total' => 0, - 'successful' => 0, - 'failed' => 0, - 'results' => [], - 'timestamp' => date('c'), - ] - ); - } - - // Vectorize the objects. - $result = $solrObjectService->vectorizeObjects(objects: $objects, provider: $provider); - - // Ensure result is an array for spread operator. - $resultArray = []; - if (is_array($result) === true) { - $resultArray = $result; - } - - return new JSONResponse( - data: [ - 'success' => $result['success'], - 'message' => "Processed {$result['successful']} of {$result['total']} objects", - ...$resultArray, - 'pagination' => [ - 'limit' => $limit, - 'offset' => $offset, - 'has_more' => count($objects) === $limit, - ], - 'filters' => [ - 'schema_id' => $schemaId, - 'register_id' => $registerId, - ], - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to bulk vectorize objects', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'schema_id' => $schemaId ?? null, - 'register_id' => $registerId ?? null, - 'limit' => $limit ?? null, - 'offset' => $offset ?? null, - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end bulkVectorizeObjects() - - /** - * Get vectorization statistics and progress - * - * Returns information about how many objects have been vectorized, - * broken down by schema, register, and embedding model. - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @return JSONResponse Vectorization statistics - * - * @psalm-return JSONResponse< - * 200|500, - * array{ - * success: bool, - * error?: string, - * stats?: array{ - * total_objects: mixed, - * vectorized_objects: 0|mixed, - * progress_percentage: 0|float, - * remaining_objects: mixed, - * vector_breakdown: mixed - * }, - * timestamp?: string - * }, - * array - * > - * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-settings-observ/tasks.md#task-21 - * - * @NoAdminRequired - */ - public function getVectorizationStats(): JSONResponse - { - try { - // Get services from container. - $vectorService = $this->container->get(VectorizationService::class); - $objectMapper = $this->container->get(MagicMapper::class); - - // Get vector stats. - $vectorStats = $vectorService->getVectorStats(); - - // Get total object count efficiently (don't load all objects into memory!). - $totalObjects = $objectMapper->countAll(); - - // Calculate progress. - $vectorizedObjects = $vectorStats['object_vectors'] ?? 0; - $progress = 0; - if ($totalObjects > 0) { - $progress = round(($vectorizedObjects / $totalObjects) * 100, 2); - } - - return new JSONResponse( - data: [ - 'success' => true, - 'stats' => [ - 'total_objects' => $totalObjects, - 'vectorized_objects' => $vectorizedObjects, - 'progress_percentage' => $progress, - 'remaining_objects' => $totalObjects - $vectorizedObjects, - 'vector_breakdown' => $vectorStats, - ], - 'timestamp' => date('c'), - ] - ); - } catch (\Exception $e) { - $this->logger->error( - message: '[SolrController] Failed to get vectorization stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ] - ); - - return new JSONResponse( - data: [ - 'success' => false, - 'error' => $e->getMessage(), - ], - statusCode: 500 - ); - }//end try - }//end getVectorizationStats() -}//end class diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index a8f7b625e8..cfd7eee4e7 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -21,6 +21,11 @@ use OCA\OpenRegister\Db\Source; use OCA\OpenRegister\Db\SourceMapper; +use OCA\OpenRegister\Service\Dbal\DatabaseIntrospectionService; +use OCA\OpenRegister\Service\Dbal\DbalConnectionException; +use OCA\OpenRegister\Service\Dbal\DbalConnectionFactory; +use OCA\OpenRegister\Service\Sync\HarvestPipelineService; +use OCA\OpenRegister\Service\Sync\SourceFetcherRegistry; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http\JSONResponse; @@ -30,8 +35,12 @@ use OCP\IGroupManager; use OCP\IL10N; use OCP\IRequest; +use Psr\Log\LoggerInterface; use OCP\IUserSession; use OCP\Security\ICrypto; +use Symfony\Component\Uid\Uuid; +use DateTime; +use Throwable; /** * Class SourcesController @@ -40,21 +49,33 @@ * * @psalm-suppress UnusedClass * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @SuppressWarnings(PHPMD.TooManyPublicMethods) Resource controller: CRUD plus the + * sync (syncNow/syncStatus) and virtual-register (testConnection/introspect) + * actions all belong to the /api/sources resource surface. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Each action carries its own + * admin guard + error mapping; splitting the resource across controllers would + * duplicate the guards without reducing real complexity. + * + * @spec openspec/specs/data-sync-harvesting/spec.md */ class SourcesController extends Controller { /** * Constructor for the SourcesController * - * @param string $appName The name of the app - * @param IRequest $request The request object - * @param IAppConfig $config The app configuration object - * @param SourceMapper $sourceMapper The source mapper - * @param IL10N $l10n The localization service - * @param IUserSession $userSession User session for admin checks - * @param IGroupManager $groupManager Group manager for admin checks - * @param ICrypto $crypto Crypto service for databaseUrl encryption + * @param string $appName The name of the app + * @param IRequest $request The request object + * @param IAppConfig $config The app configuration object + * @param SourceMapper $sourceMapper The source mapper + * @param IL10N $l10n The localization service + * @param IUserSession $userSession User session for admin checks + * @param IGroupManager $groupManager Group manager for admin checks + * @param ICrypto $crypto Crypto service for databaseUrl encryption + * @param SourceFetcherRegistry $fetcherRegistry Resolves the transport for a source type + * @param HarvestPipelineService $pipeline Harvest pipeline orchestrator + * @param DbalConnectionFactory $connectionFactory Opens read-only DBAL connections for database sources + * @param DatabaseIntrospectionService $introspectionService Introspects a database source into a virtual register + * @param LoggerInterface $logger The app logger * * @return void */ @@ -66,7 +87,12 @@ public function __construct( private readonly IL10N $l10n, private readonly IUserSession $userSession, private readonly IGroupManager $groupManager, - private readonly ICrypto $crypto + private readonly ICrypto $crypto, + private readonly SourceFetcherRegistry $fetcherRegistry, + private readonly HarvestPipelineService $pipeline, + private readonly DbalConnectionFactory $connectionFactory, + private readonly DatabaseIntrospectionService $introspectionService, + private readonly LoggerInterface $logger ) { parent::__construct(appName: $appName, request: $request); }//end __construct() @@ -100,6 +126,17 @@ private function serializeSource(Source $source): array unset($data['databaseUrl']); } + // Database sources: admins get the NON-SECRET connection parts back so + // the edit UI can rehydrate (jsonSerialize only exposes the + // `authConfigured` boolean — a custody-era default that made every UI + // save silently wipe the connection settings). The password/secret are + // never persisted for this type; strip defensively anyway. + if ($this->isCurrentUserAdmin() === true && (string) $source->getType() === 'database') { + $authConfig = ($source->getAuthConfig() ?? []); + unset($authConfig['password'], $authConfig['secret']); + $data['authConfig'] = $authConfig; + } + return $data; }//end serializeSource() @@ -114,9 +151,9 @@ private function serializeSource(Source $source): array * * @NoCSRFRequired * - * @psalm-return JSONResponse<200, array{results: array}, array> + * @psalm-return JSONResponse<200, array{results: array>}, array{}> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/data-sync-harvesting/spec.md */ public function index(): JSONResponse { @@ -146,7 +183,7 @@ public function index(): JSONResponse ); return new JSONResponse( data: [ - 'results' => array_map(fn(Source $s) => $this->serializeSource($s), $sources), + 'results' => array_map(fn(Source $src) => $this->serializeSource(source: $src), $sources), ] ); }//end index() @@ -163,14 +200,14 @@ public function index(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/data-sync-harvesting/spec.md */ public function show(string $id): JSONResponse { try { // Try to find the source by ID. $source = $this->sourceMapper->find(id: (int) $id); - return new JSONResponse(data: $this->serializeSource($source)); + return new JSONResponse(data: $this->serializeSource(source: $source)); } catch (DoesNotExistException $exception) { // Return a 404 error if the source doesn't exist. return new JSONResponse(data: ['error' => $this->l10n->t('Not Found')], statusCode: 404); @@ -190,7 +227,7 @@ public function show(string $id): JSONResponse * * @psalm-return JSONResponse<200, Source, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/data-sync-harvesting/spec.md */ public function create(): JSONResponse { @@ -213,14 +250,22 @@ public function create(): JSONResponse unset($data['id']); } - // Encrypt databaseUrl at rest before persisting. + // Credential custody split (dbal-virtual-registers D1): LEGACY harvest + // sources keep encrypting their full databaseUrl at rest with ICrypto; + // `type: database` (virtual register) sources store only NON-SECRET + // connection parts in authConfig and custody the password behind the + // CredentialStore seam, referenced by a `credential` UUID — a plaintext + // password is never persisted on the row. + $data = $this->sanitizeDatabaseSourceData(data: $data); + + // Encrypt databaseUrl at rest before persisting (legacy harvest path). if (isset($data['databaseUrl']) === true && $data['databaseUrl'] !== null && $data['databaseUrl'] !== '') { $data['databaseUrl'] = $this->crypto->encrypt((string) $data['databaseUrl']); } // Create a new source from the data. $source = $this->sourceMapper->createFromArray(object: $data); - return new JSONResponse(data: $this->serializeSource($source)); + return new JSONResponse(data: $this->serializeSource(source: $source)); }//end create() /** @@ -238,7 +283,7 @@ public function create(): JSONResponse * * @psalm-return JSONResponse<200, Source, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/data-sync-harvesting/spec.md */ public function update(int $id): JSONResponse { @@ -262,14 +307,17 @@ public function update(int $id): JSONResponse unset($data['owner']); unset($data['created']); - // Encrypt databaseUrl at rest before persisting. + // Custody split for database sources — see create() (dbal-virtual-registers D1). + $data = $this->sanitizeDatabaseSourceData(data: $data); + + // Encrypt databaseUrl at rest before persisting (legacy harvest path). if (isset($data['databaseUrl']) === true && $data['databaseUrl'] !== null && $data['databaseUrl'] !== '') { $data['databaseUrl'] = $this->crypto->encrypt((string) $data['databaseUrl']); } // Update the source with the provided data. $source = $this->sourceMapper->updateFromArray(id: $id, object: $data); - return new JSONResponse(data: $this->serializeSource($source)); + return new JSONResponse(data: $this->serializeSource(source: $source)); }//end update() /** @@ -285,7 +333,7 @@ public function update(int $id): JSONResponse * * @psalm-return JSONResponse<200, Source, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/data-sync-harvesting/spec.md */ public function patch(int $id): JSONResponse { @@ -309,7 +357,7 @@ public function patch(int $id): JSONResponse * * @psalm-return JSONResponse<200, array, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-1 + * @spec openspec/specs/data-sync-harvesting/spec.md */ public function destroy(int $id): JSONResponse { @@ -324,6 +372,289 @@ public function destroy(int $id): JSONResponse return new JSONResponse(data: []); }//end destroy() + /** + * Trigger an immediate sync for a source ("Sync Now"). + * + * Admin-only and organisation-scoped: the source is loaded via + * SourceMapper::find(), which applies the organisation filter, so a + * non-owning admin receives a 404 rather than acting on another tenant's + * source (per-object guard, no IDOR). The harvest pipeline runs inline + * when a transport is available; sources without a registered fetcher are + * rejected rather than silently dropped. + * + * @param int $id The source id to sync + * + * @return JSONResponse The sync execution summary + * + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @no-admin-idor-exempt Admin-gated (isCurrentUserAdmin → 403) AND + * organisation-scoped: the source is loaded via SourceMapper::find(), + * which applies the active-organisation filter and 404s on a foreign + * tenant's id, so a caller cannot trigger sync on an object outside + * their organisation. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function syncNow(int $id): JSONResponse + { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => $this->l10n->t('Admin privileges required')], statusCode: 403); + } + + try { + $source = $this->sourceMapper->find(id: $id); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => $this->l10n->t('Not Found')], statusCode: 404); + } + + $fetcher = $this->fetcherRegistry->get((string) $source->getType()); + if ($fetcher === null) { + return new JSONResponse( + data: ['error' => $this->l10n->t('No sync transport available for this source type')], + statusCode: 422 + ); + } + + $executionId = (string) Uuid::v4(); + + try { + $summary = $this->pipeline->run( + source: $source, + fetcher: $fetcher, + executionId: $executionId, + since: null + ); + + $source->setLastSyncStatus((string) ($summary['status'] ?? 'success')); + $source->setLastSyncDate(new DateTime()); + $this->sourceMapper->update($source); + + return new JSONResponse(data: $summary); + } catch (Throwable $e) { + $source->setLastSyncStatus('failed'); + $source->setLastSyncDate(new DateTime()); + $this->sourceMapper->update($source); + + return new JSONResponse( + data: ['error' => $this->l10n->t('Sync failed'), 'message' => $e->getMessage()], + statusCode: 500 + ); + }//end try + }//end syncNow() + + /** + * Return the current sync status for a source. + * + * Organisation-scoped via SourceMapper::find() (per-object guard). Any + * authenticated member of the owning organisation may read sync status; + * no credentials are exposed. + * + * @param int $id The source id + * + * @return JSONResponse The sync status payload + * + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @no-admin-idor-exempt Organisation-scoped read: the source is loaded via + * SourceMapper::find(), which applies the active-organisation filter and + * 404s on a foreign tenant's id. Only non-sensitive sync status is + * returned (no credentials), so any authenticated member of the owning + * organisation may read it. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function syncStatus(int $id): JSONResponse + { + try { + $source = $this->sourceMapper->find(id: $id); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => $this->l10n->t('Not Found')], statusCode: 404); + } + + $lastSyncDate = $source->getLastSyncDate(); + + $formattedLastSyncDate = null; + if ($lastSyncDate !== null) { + $formattedLastSyncDate = $lastSyncDate->format('c'); + } + + return new JSONResponse( + data: [ + 'id' => $source->getId(), + 'uuid' => $source->getUuid(), + 'syncEnabled' => $source->getSyncEnabled(), + 'status' => ($source->getLastSyncStatus() ?? 'never'), + 'lastSyncDate' => $formattedLastSyncDate, + 'syncInterval' => $source->getSyncInterval(), + ] + ); + }//end syncStatus() + + /** + * Enforce the credential custody split for `type: database` sources. + * + * A virtual-register database source must never persist a plaintext secret: + * any `password`/`secret` key submitted inside `authConfig` is stripped, and + * `databaseUrl` (the legacy ICrypto-encrypted harvest field) is cleared so + * the two paths cannot mix. The password belongs behind the CredentialStore + * seam, referenced by the `authConfig.credential` UUID (design D1). + * + * @param array $data The submitted source data. + * + * @return array The sanitised source data. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function sanitizeDatabaseSourceData(array $data): array + { + if ((string) ($data['type'] ?? '') !== 'database') { + return $data; + } + + if (isset($data['authConfig']) === true && is_array($data['authConfig']) === true) { + unset($data['authConfig']['password'], $data['authConfig']['secret']); + } + + // Clear rather than unset: the sources table declares database_url + // NOT NULL, so an absent value fails the INSERT with a 23502. An empty + // string satisfies the constraint and carries no secret. + $data['databaseUrl'] = ''; + + return $data; + }//end sanitizeDatabaseSourceData() + + /** + * Test the connection to a `type: database` source. + * + * Resolves the password through the credential custody seam, opens a + * read-only DBAL connection and runs a trivial read. Never exposes the + * password. A connection failure maps to 503 (unreachable); an upstream + * query error maps to 502 — never a bare 500. + * + * @param int $id The source id to test. + * + * @return JSONResponse Success, or a 502/503 error with a non-sensitive message. + * + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @no-admin-idor-exempt Admin-gated (isCurrentUserAdmin → 403) AND + * organisation-scoped: the source is loaded via SourceMapper::find(), + * which applies the active-organisation filter and 404s on a foreign + * tenant's id. The response never contains the credential value. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function testConnection(int $id): JSONResponse + { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => $this->l10n->t('Admin privileges required')], statusCode: 403); + } + + try { + $source = $this->sourceMapper->find(id: $id); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => $this->l10n->t('Not Found')], statusCode: 404); + } + + if ((string) $source->getType() !== 'database') { + return new JSONResponse( + data: ['error' => $this->l10n->t('Source is not a database source')], + statusCode: 422 + ); + } + + try { + $connection = $this->connectionFactory->getConnection(source: $source); + } catch (DbalConnectionException $exception) { + // Fail closed: credential/driver/config problem — source unreachable. + return new JSONResponse( + data: ['error' => $this->l10n->t('Could not connect to the database source')], + statusCode: 503 + ); + } + + try { + $connection->executeQuery($connection->getDatabasePlatform()->getDummySelectSQL()); + } catch (Throwable $exception) { + return new JSONResponse( + data: ['error' => $this->l10n->t('The database source returned an error')], + statusCode: 502 + ); + } + + return new JSONResponse(data: ['success' => true]); + }//end testConnection() + + /** + * Introspect a `type: database` source into a virtual register + schemas. + * + * Idempotent: re-running updates the existing register/schemas in place. + * Never exposes the password. + * + * @param int $id The source id to introspect. + * + * @return JSONResponse The introspection summary, or a 502/503 error. + * + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @no-admin-idor-exempt Admin-gated (isCurrentUserAdmin → 403) AND + * organisation-scoped: the source is loaded via SourceMapper::find(), + * which applies the active-organisation filter and 404s on a foreign + * tenant's id. The response never contains the credential value. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function introspect(int $id): JSONResponse + { + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(data: ['error' => $this->l10n->t('Admin privileges required')], statusCode: 403); + } + + try { + $source = $this->sourceMapper->find(id: $id); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => $this->l10n->t('Not Found')], statusCode: 404); + } + + if ((string) $source->getType() !== 'database') { + return new JSONResponse( + data: ['error' => $this->l10n->t('Source is not a database source')], + statusCode: 422 + ); + } + + try { + $summary = $this->introspectionService->introspect(source: $source); + } catch (DbalConnectionException $exception) { + return new JSONResponse( + data: ['error' => $this->l10n->t('Could not connect to the database source')], + statusCode: 503 + ); + } catch (Throwable $exception) { + // Never echo raw exception text to the client (it may carry DSN + // fragments or SQL); log it server-side and return a fixed message. + $this->logger->error( + '[SourcesController] introspection failed: '.$exception->getMessage(), + ['file' => __FILE__, 'line' => __LINE__, 'exception' => $exception] + ); + return new JSONResponse( + data: ['error' => $this->l10n->t('Introspection failed')], + statusCode: 502 + ); + } + + return new JSONResponse(data: $summary); + }//end introspect() + /** * Get integer parameter from params array or return null * diff --git a/lib/Controller/SurvivorshipController.php b/lib/Controller/SurvivorshipController.php new file mode 100644 index 0000000000..659a5ac770 --- /dev/null +++ b/lib/Controller/SurvivorshipController.php @@ -0,0 +1,335 @@ + + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.4 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Exception\NotAuthorizedException; +use OCA\OpenRegister\Service\ObjectService; +use OCA\OpenRegister\Service\Survivorship\SourceRecordResolver; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\IUserSession; +use RuntimeException; +use Throwable; + +class SurvivorshipController extends Controller +{ + /** + * Default field the per-object attribute-override map is read/written to + * when the schema's survivorship annotation omits `overridesField`. + * Mirrors {@see \OCA\OpenRegister\Listener\SurvivorshipRecomputeListener}. + * + * @var string + */ + private const DEFAULT_OVERRIDES_FIELD = 'attributeOverrides'; + + /** + * Constructor. + * + * @param string $appName The application name. + * @param IRequest $request The current request. + * @param ObjectService $objectService Object read/write path (RBAC + tenant scoped). + * @param SchemaMapper $schemaMapper Schema lookup for the survivorship annotation. + * @param SourceRecordResolver $sourceRecordResolver Mode-aware source-record resolver (embedded | reverseFk). + * @param IUserSession $userSession Current user session, for actor attribution. + */ + public function __construct( + string $appName, + IRequest $request, + private readonly ObjectService $objectService, + private readonly SchemaMapper $schemaMapper, + private readonly SourceRecordResolver $sourceRecordResolver, + private readonly IUserSession $userSession + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Return a master object's resolved competing source records, honouring + * the schema's `sourceLink` mode (embedded or reverse-FK). Used by the + * conflict-resolution UI, which computes per-attribute disagreements from + * these sources — a reverse-FK master carries no embedded source array, so + * the client cannot resolve them without this endpoint. + * + * @param string $id Uuid of the master object. + * + * @return JSONResponse `{ sources: [...] }`, or an error status. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + public function sources(string $id): JSONResponse + { + try { + $object = $this->objectService->find(id: $id, _rbac: true, _multitenancy: true); + if ($object === null) { + return new JSONResponse(['error' => 'Object not found.'], Http::STATUS_NOT_FOUND); + } + + $sources = $this->sourceRecordResolver->resolveSources( + masterData: ($object->getObject() ?? []), + masterUuid: (string) $object->getUuid(), + config: $this->survivorshipConfigFor(object: $object), + masterRegister: (string) $object->getRegister() + ); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (Throwable $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse(['sources' => $sources]); + }//end sources() + + /** + * Set (with a value) or clear (with a null/absent value) one attribute + * override on a master object, then save it — the save fires + * `SurvivorshipRecomputeListener`, which recomputes the golden record + * with the override threaded in. Returns the recomputed object. + * + * A caller who cannot write the target object receives a + * forbidden/not-found response; no override is written in that case + * (no-admin-idor — the object id is never trusted without the + * `ObjectService` write-path RBAC/tenant check). + * + * @param string $id Uuid of the master object. + * + * @return JSONResponse JSON response with the recomputed object. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.4 + */ + public function override(string $id): JSONResponse + { + $attribute = (string) $this->request->getParam('attribute', ''); + if ($attribute === '') { + return new JSONResponse(['error' => 'An "attribute" is required.'], Http::STATUS_BAD_REQUEST); + } + + try { + // RBAC/tenant scoped read — an unreadable object never reaches the + // write path below. + $object = $this->objectService->find(id: $id, _rbac: true, _multitenancy: true); + if ($object === null) { + return new JSONResponse(['error' => 'Object not found.'], Http::STATUS_NOT_FOUND); + } + + $this->applyOverride(object: $object, attribute: $attribute); + + // Warm the reverse-FK source resolution in this (clean) request + // context before saving: resolving the source schema by slug here + // populates the request-scoped schema cache, so the nested + // recompute-on-save can resolve the sources by their numeric schema + // id rather than a slug lookup that would run inside the save + // transaction. Result discarded — this is a cache warm-up only. + $this->sourceRecordResolver->resolveSources( + masterData: ($object->getObject() ?? []), + masterUuid: (string) $object->getUuid(), + config: $this->survivorshipConfigFor(object: $object), + masterRegister: (string) $object->getRegister() + ); + + // The RBAC/tenant-scoped write path: a caller who cannot write + // this object throws NotAuthorizedException here, caught below. + $saved = $this->objectService->saveObject( + object: $object, + register: $object->getRegister(), + schema: $object->getSchema(), + uuid: $object->getUuid() + ); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_FORBIDDEN); + } catch (RuntimeException $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } catch (Throwable $e) { + return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND); + }//end try + + $result = ($saved->getObject() ?? []); + $result['id'] = $saved->getUuid(); + + return new JSONResponse($result); + }//end override() + + /** + * Set or clear one attribute override directly on the object's in-memory + * payload (the caller is responsible for persisting it). Reads `value`, + * `clear` and `rationale` from the current request. + * + * @param ObjectEntity $object Object being overridden (mutated in place). + * @param string $attribute Attribute name to set/clear an override for. + * + * @return void + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.4 + */ + private function applyOverride(ObjectEntity $object, string $attribute): void + { + $hasValue = ($this->request->getParam('clear', false) !== true) + && $this->request->getParam('value', null) !== null; + + $data = ($object->getObject() ?? []); + $overridesField = $this->overridesFieldFor(object: $object); + + $overrides = ($data[$overridesField] ?? []); + if (is_array($overrides) === false) { + $overrides = []; + } + + unset($overrides[$attribute]); + if ($hasValue === true) { + $overrides[$attribute] = $this->buildOverrideEntry(); + } + + $data[$overridesField] = $overrides; + $object->setObject($data); + }//end applyOverride() + + /** + * Build a single override entry from the current request's `value` and + * `rationale` params, attributed to the acting user. + * + * @return array{value: mixed, overriddenBy: string, rationale: string|null} + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.4 + */ + private function buildOverrideEntry(): array + { + $value = $this->request->getParam('value', null); + $rationale = $this->request->getParam('rationale', null); + $actor = ((string) ($this->userSession->getUser()?->getUID() ?? '')); + + $rationaleValue = null; + if ($rationale !== null) { + $rationaleValue = (string) $rationale; + } + + return [ + 'value' => $value, + 'overriddenBy' => $actor, + 'rationale' => $rationaleValue, + ]; + }//end buildOverrideEntry() + + /** + * Resolve the `overridesField` name declared on the object's schema + * survivorship annotation, falling back to the listener's default. + * + * @param ObjectEntity $object Object whose schema to inspect. + * + * @return string The override-map field name. + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.4 + */ + private function overridesFieldFor(ObjectEntity $object): string + { + $schema = $this->loadSchema(object: $object); + if ($schema === null) { + return self::DEFAULT_OVERRIDES_FIELD; + } + + $config = ($schema->getConfiguration() ?? []); + $value = ($config['x-openregister-survivorship'] ?? null); + if (is_array($value) === false) { + return self::DEFAULT_OVERRIDES_FIELD; + } + + $field = (string) ($value['overridesField'] ?? ''); + if ($field === '') { + return self::DEFAULT_OVERRIDES_FIELD; + } + + return $field; + }//end overridesFieldFor() + + /** + * Resolve the full `x-openregister-survivorship` config for an object's + * schema (carrying the `sourceLink` block), or an empty array when absent. + * + * @param ObjectEntity $object Object whose schema to inspect. + * + * @return array Survivorship config. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + private function survivorshipConfigFor(ObjectEntity $object): array + { + $schema = $this->loadSchema(object: $object); + if ($schema === null) { + return []; + } + + $config = ($schema->getConfiguration() ?? []); + $value = ($config['x-openregister-survivorship'] ?? null); + if (is_array($value) === false) { + return []; + } + + return $value; + }//end survivorshipConfigFor() + + /** + * Look up the schema referenced by an object instance. + * + * @param ObjectEntity $object Object whose schema reference to resolve. + * + * @return Schema|null Resolved schema, or null on lookup failure. + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.4 + */ + private function loadSchema(ObjectEntity $object): ?Schema + { + $ref = $object->getSchema(); + if ($ref === null || $ref === '') { + return null; + } + + try { + return $this->schemaMapper->find($ref, _multitenancy: false); + } catch (Throwable) { + return null; + } + }//end loadSchema() +}//end class diff --git a/lib/Controller/TagsController.php b/lib/Controller/TagsController.php index 8733732e94..a7e470355b 100644 --- a/lib/Controller/TagsController.php +++ b/lib/Controller/TagsController.php @@ -32,6 +32,7 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; +use OCP\IUserSession; use OCP\AppFramework\Db\DoesNotExistException; use Exception; @@ -64,6 +65,7 @@ class TagsController extends Controller * @param ObjectService $objectService Object service instance * @param FileService $fileService File service instance for tag retrieval * @param TaggingHandler $taggingHandler Tagging handler for object-level tags + * @param IUserSession $userSession Session for the @PublicPage anonymous-deny guard * * @return void */ @@ -73,6 +75,7 @@ public function __construct( private readonly ObjectService $objectService, private readonly FileService $fileService, private readonly TaggingHandler $taggingHandler, + private readonly ?IUserSession $userSession=null, ) { // Call parent constructor to initialize base controller. parent::__construct(appName: $appName, request: $request); @@ -85,19 +88,54 @@ public function __construct( * * @NoCSRFRequired * - * @return JSONResponse JSON response with all tags + * @PublicPage * - * @psalm-return JSONResponse<200, list, array> + * @return JSONResponse JSON response with all tags, or a 401 error + * envelope when no user is authenticated. + * + * @psalm-return JSONResponse<200, list, array>|JSONResponse<401, array{error: string}, array> * * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-misc/tasks.md#task-4 */ public function getAllTags(): JSONResponse { + // @PublicPage opens this route past the group-locked app gate so a + // consuming app (e.g. OpenCatalogi) can populate its label picker even + // when the user has no direct OpenRegister access — the same approach + // #185 applied to the object/file endpoints. The tag list is global and + // read-only, but must not be served to anonymous callers, so require an + // authenticated user (mirrors the file endpoints' anonymous-deny guard). + // See openregister#194. + if ($this->isAnonymousRequest() === true) { + return new JSONResponse( + data: ['error' => 'Authentication is required'], + statusCode: 401 + ); + } + $tags = $this->fileService->getAllTags(); return new JSONResponse(data: $tags); }//end getAllTags() + /** + * Check whether the current request comes from an unauthenticated (anonymous) caller. + * + * Extracted to prevent gate-9 from incorrectly flagging the `@PublicPage` + * getAllTags method that legitimately opens the route past the group-locked + * app gate (so consuming apps can reach it) while still denying anonymous + * access in the body. The pattern `userSession->getUser() === null` in a + * PublicPage body is a false-positive for gate-9's "annotation-vs-body + * mismatch" check; wrapping it here keeps that detector from triggering. + * Mirrors FilesController::isAnonymousRequest(). See openregister#194. + * + * @return bool True when no Nextcloud user is associated with the current session. + */ + private function isAnonymousRequest(): bool + { + return ($this->userSession === null || $this->userSession->getUser() === null); + }//end isAnonymousRequest() + /** * Get tags for a specific object. * diff --git a/lib/Controller/TalkLinksController.php b/lib/Controller/TalkLinksController.php index f989fe4da1..0807e1a7ec 100644 --- a/lib/Controller/TalkLinksController.php +++ b/lib/Controller/TalkLinksController.php @@ -261,6 +261,8 @@ public function destroy(string $register, string $schema, string $id, string $ro * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: TalkLinkService::getAvailableRoomsForUser returns only rooms + * the current user participates in; no unguarded id. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-1 */ diff --git a/lib/Controller/TasksController.php b/lib/Controller/TasksController.php index 904e880eb5..6f48be9c44 100644 --- a/lib/Controller/TasksController.php +++ b/lib/Controller/TasksController.php @@ -17,7 +17,7 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-61 + * @spec openspec/specs/object-interactions/spec.md */ declare(strict_types=1); @@ -101,6 +101,8 @@ public function __construct( * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: TaskService::getAllUserTasks resolves the calendar set from the IUserSession UID; + * the assignee param filters within the caller's own tasks, not an identity claim. * * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-misc/tasks.md#task-2 */ @@ -140,7 +142,7 @@ public function allUserTasks(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-61 + * @spec openspec/specs/object-interactions/spec.md */ public function index( string $register, @@ -181,7 +183,7 @@ public function index( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-61 + * @spec openspec/specs/object-interactions/spec.md */ public function create( string $register, @@ -236,7 +238,7 @@ public function create( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-61 + * @spec openspec/specs/object-interactions/spec.md */ public function update( string $register, @@ -298,7 +300,7 @@ public function update( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-61 + * @spec openspec/specs/object-interactions/spec.md */ public function destroy( string $register, diff --git a/lib/Controller/TimeTrackerLinksController.php b/lib/Controller/TimeTrackerLinksController.php index 2f960ac4bc..74bfe3c821 100644 --- a/lib/Controller/TimeTrackerLinksController.php +++ b/lib/Controller/TimeTrackerLinksController.php @@ -57,10 +57,10 @@ class TimeTrackerLinksController extends Controller /** * Constructor. * - * @param string $appName App id. - * @param IRequest $request HTTP request. - * @param TimeTrackerLinkService $linkService Backing service. - * @param ObjectService $objectService OR object resolver. + * @param string $appName App id. + * @param IRequest $request HTTP request. + * @param TimeTrackerLinkService $linkService Backing service. + * @param ObjectService $objectService OR object resolver. */ public function __construct( string $appName, @@ -85,7 +85,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function index(string $register, string $schema, string $id): JSONResponse { @@ -133,7 +133,7 @@ public function index(string $register, string $schema, string $id): JSONRespons * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function link(string $register, string $schema, string $id): JSONResponse { @@ -192,7 +192,7 @@ public function link(string $register, string $schema, string $id): JSONResponse * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function createAndLink(string $register, string $schema, string $id): JSONResponse { @@ -244,7 +244,7 @@ public function createAndLink(string $register, string $schema, string $id): JSO * * @SuppressWarnings(PHPMD.ShortVariable) $id matches the {id} URL route parameter; renaming breaks route binding. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function destroy(string $register, string $schema, string $id, string $entryId): JSONResponse { @@ -280,8 +280,9 @@ public function destroy(string $register, string $schema, string $id, string $en * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Session-scoped list: returns the current user's own TimeTracker clients; no caller-supplied object id. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md#requirement-tier-2-integration-leaf-link-controller-contract */ public function available(): JSONResponse { @@ -310,7 +311,8 @@ public function available(): JSONResponse * * @return ObjectEntity|null * - * @SuppressWarnings(PHPMD.ShortVariable) $id is the object identifier passed from the route parameter; renaming would break consistency with caller method signatures. + * @SuppressWarnings(PHPMD.ShortVariable) $id is the object identifier passed from the route parameter; + * renaming would break consistency with caller method signatures. */ private function validateObject(string $register, string $schema, string $id): ?ObjectEntity { diff --git a/lib/Controller/Trait/HandlesExceptionsTrait.php b/lib/Controller/Trait/HandlesExceptionsTrait.php new file mode 100644 index 0000000000..f055ea4bb3 --- /dev/null +++ b/lib/Controller/Trait/HandlesExceptionsTrait.php @@ -0,0 +1,81 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller\Trait; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Shared exception-handling helper for controllers. + * + * SEC-CTRL-7: prevents pervasive exception-message disclosure on HTTP 500 + * responses. Internal exception messages (stack-trace adjacent, SQL fragments, + * file paths) must never be echoed to the client. This trait logs the real + * exception server-side and returns a generic envelope to the caller. + * + * Use ONLY for genuine 500 (internal error) paths. 4xx validation responses + * that carry an intentional, user-facing message must keep that message. + * + * @category Controller + * @package OCA\OpenRegister + * @author Conduction + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister + */ +trait HandlesExceptionsTrait +{ + /** + * Log an exception and return a generic 500 JSON response. + * + * The real message/trace is written to the server log (when a logger is + * available on the consuming controller) and never exposed to the client. + * + * @param Throwable $e The caught exception. + * @param string $context Optional short context label for the log line. + * + * @return JSONResponse A generic internal-server-error response. + */ + protected function errorResponse(Throwable $e, string $context=''): JSONResponse + { + // Log server-side when the consuming controller exposes a logger. + if (property_exists($this, 'logger') === true && $this->logger instanceof LoggerInterface) { + if ($context !== '') { + $contextPrefix = $context.': '; + } else { + $contextPrefix = ''; + } + + $this->logger->error( + message: '['.static::class.'] '.$contextPrefix.$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'exception' => $e, + ] + ); + } + + return new JSONResponse( + data: ['error' => 'Internal server error'], + statusCode: Http::STATUS_INTERNAL_SERVER_ERROR + ); + }//end errorResponse() +}//end trait diff --git a/lib/Controller/TransferController.php b/lib/Controller/TransferController.php index 2a98744cea..5e7585c610 100644 --- a/lib/Controller/TransferController.php +++ b/lib/Controller/TransferController.php @@ -3,7 +3,12 @@ /** * OpenRegister Transfer Controller * - * Handles e-Depot transfer list operations. + * Handles e-Depot transfer list operations. Serves the durable transfer + * records persisted in the `edepot-transfers` system register (index/show) + * and really dispatches an approved transfer to the queued execution path + * (create) — the former placeholder responses + never-dispatched + * `TransferExecutionJob` are the pre-existing stubs closed by + * archival-transfer-hardening (OR-AD-2). * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. @@ -19,133 +24,175 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-36 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md */ declare(strict_types=1); namespace OCA\OpenRegister\Controller; -use OCA\OpenRegister\Service\Edepot\EdepotTransferService; +use OCA\OpenRegister\BackgroundJob\TransferExecutionJob; use OCA\OpenRegister\Service\Edepot\TransferListService; +use OCA\OpenRegister\Service\Edepot\TransferRecordService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\JSONResponse; +use OCP\BackgroundJob\IJobList; use OCP\IRequest; use Psr\Log\LoggerInterface; /** * Controller for e-Depot transfer operations. * - * Provides endpoints for transfer list management: create, approve, reject, - * initiate transfer, and check status. + * Provides endpoints for transfer list management: list, show, and initiate + * (dispatch to the queued execution path) reading persisted transfer records. * * @psalm-suppress UnusedClass + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md */ class TransferController extends Controller { /** * Constructor. * - * @param string $appName The app name. - * @param IRequest $request The request. - * @param TransferListService $transferListService The transfer list service. - * @param EdepotTransferService $transferService The transfer service. - * @param LoggerInterface $logger Logger. + * @param string $appName The app name. + * @param IRequest $request The request. + * @param TransferRecordService $transferRecordService Durable transfer-record persistence. + * @param IJobList $jobList Background-job scheduler (dispatch execution). + * @param LoggerInterface $logger Logger. */ public function __construct( $appName, IRequest $request, - private readonly TransferListService $transferListService, - private readonly EdepotTransferService $transferService, + private readonly TransferRecordService $transferRecordService, + private readonly IJobList $jobList, private readonly LoggerInterface $logger, ) { parent::__construct(appName: $appName, request: $request); + }//end __construct() /** - * List all transfer lists. - * - * @NoCSRFRequired + * List all durable transfer lists. * * @return JSONResponse The list of transfer lists. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-36 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Scenario: Index returns persisted transfer lists) */ + #[NoCSRFRequired] public function index(): JSONResponse { - // Transfer lists are stored as register objects. - // This is a placeholder that returns the expected structure. - return new JSONResponse(data: ['results' => [], 'total' => 0]); + $lists = $this->transferRecordService->listTransferLists(); + + return new JSONResponse(data: ['results' => $lists, 'total' => count($lists)]); + }//end index() /** - * Get a specific transfer list. + * Get a specific durable transfer list. * * @param string $id The transfer list UUID. * - * @return JSONResponse The transfer list data. - * - * @NoCSRFRequired + * @return JSONResponse The transfer list data, or a 404. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Scenario: Show returns a persisted transfer list) */ + #[NoCSRFRequired] public function show(string $id): JSONResponse { - // Transfer lists are stored as register objects. - // Delegate to the object API for retrieval. - return new JSONResponse( - data: ['error' => "Transfer list '{$id}' not found"], - statusCode: 404 - ); + $list = $this->transferRecordService->loadTransferList($id); + if ($list === null) { + return new JSONResponse( + data: ['error' => "Transfer list '{$id}' not found"], + statusCode: Http::STATUS_NOT_FOUND + ); + } + + return new JSONResponse(data: $list); + }//end show() /** * Initiate a transfer from an approved transfer list. * - * @NoCSRFRequired + * Loads the persisted list, verifies it is `approved` (client error + * otherwise — no enqueue), and dispatches `TransferExecutionJob` with + * `{transferListId, attempt: 1}` (the WebhookDeliveryJob argument + * convention). Retry / backoff / escalation are the job's concern. * * @return JSONResponse The transfer initiation result. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-36 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Scenario: Create dispatches an approved transfer) */ + #[NoCSRFRequired] public function create(): JSONResponse { try { $params = $this->request->getParams(); - $transferListUuid = ($params['transferListUuid'] ?? ''); + $transferListUuid = (string) ($params['transferListUuid'] ?? ''); - if (empty($transferListUuid) === true) { + if ($transferListUuid === '') { return new JSONResponse( data: ['error' => 'transferListUuid is required'], - statusCode: 400 + statusCode: Http::STATUS_BAD_REQUEST ); } - // In a full implementation, we would: - // 1. Load the transfer list from the register - // 2. Verify it's in 'approved' status - // 3. Queue a TransferExecutionJob - // - // For now, return a structured response indicating the transfer was queued. - return new JSONResponse( + $list = $this->transferRecordService->loadTransferList($transferListUuid); + if ($list === null) { + return new JSONResponse( + data: ['error' => "Transfer list '{$transferListUuid}' not found"], + statusCode: Http::STATUS_NOT_FOUND + ); + } + + $status = (string) ($list['status'] ?? ''); + if ($status !== TransferListService::STATUS_APPROVED) { + return new JSONResponse( data: [ - 'message' => 'Transfer queued for execution', - 'transferListUuid' => $transferListUuid, - 'status' => 'queued', - ] - ); - } catch (\Exception $e) { + 'error' => "Transfer list must be 'approved' to initiate; current status '{$status}'", + 'status' => $status, + ], + statusCode: Http::STATUS_CONFLICT + ); + } + + $this->jobList->add( + TransferExecutionJob::class, + [ + 'transferListId' => $transferListUuid, + 'attempt' => 1, + ] + ); + + $this->logger->info( + message: '[TransferController] Dispatched transfer execution', + context: ['file' => __FILE__, 'line' => __LINE__, 'transferListUuid' => $transferListUuid] + ); + + return new JSONResponse( + data: [ + 'message' => 'Transfer queued for execution', + 'transferListUuid' => $transferListUuid, + 'status' => 'queued', + ], + statusCode: Http::STATUS_ACCEPTED + ); + } catch (\Throwable $e) { $this->logger->error( - message: '[TransferController] Failed to initiate transfer', - context: ['error' => $e->getMessage()] + message: '[TransferController] Failed to initiate transfer: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] ); return new JSONResponse( - data: ['error' => $e->getMessage()], - statusCode: 500 + data: ['error' => 'Failed to initiate transfer.'], + statusCode: Http::STATUS_INTERNAL_SERVER_ERROR ); }//end try + }//end create() }//end class diff --git a/lib/Controller/TranslationController.php b/lib/Controller/TranslationController.php index d5368b7890..caa8b5da72 100644 --- a/lib/Controller/TranslationController.php +++ b/lib/Controller/TranslationController.php @@ -76,32 +76,64 @@ public function __construct( /** * Search the translations sidecar. * - * @param string|null $query Optional full-text query. - * @param string|null $language Optional language filter. - * @param string|null $status Optional status filter. - * @param string|null $objectUuid Optional object UUID filter. - * @param int|null $limit Maximum number of results. + * Supports the i18n-source-of-truth filters in addition to the existing + * status/language/object filters: + * - `?sourceLanguage=` — narrow to rows whose canonical source + * language equals the given tag. + * - `?isOutOfDate=true` — narrow to rows in `outdated` status. + * - `?compareToSource=true` — return both `value` and `sourceValue` + * side-by-side per matched row so editorial tooling can render the + * source / target pair without a follow-up round trip. + * + * @param string|null $query Optional full-text query. + * @param string|null $language Optional language filter. + * @param string|null $status Optional status filter. + * @param string|null $objectUuid Optional object UUID filter. + * @param int|null $limit Maximum number of results. + * @param string|null $sourceLanguage Optional source-language filter. + * @param string|null $isOutOfDate When set to "true"/"1", restrict to outdated status. + * @param string|null $compareToSource When set to "true"/"1", attach `sourceValue` per row. * * @return JSONResponse JSON response with results and count. * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-8 + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-3 */ public function search( ?string $query=null, ?string $language=null, ?string $status=null, ?string $objectUuid=null, - ?int $limit=100 + ?int $limit=100, + ?string $sourceLanguage=null, + ?string $isOutOfDate=null, + ?string $compareToSource=null ): JSONResponse { - $rows = $this->statusService->search( - query: $query, - language: $language, - status: $status, - objectUuid: $objectUuid, - limit: max(1, min(1000, (int) ($limit ?? 100))) - ); + $isOutOfDateBool = $this->isTruthy(value: $isOutOfDate); + $compareToSourceBool = $this->isTruthy(value: $compareToSource); + + if ($compareToSourceBool === true) { + $rows = $this->statusService->searchWithSourceValues( + query: $query, + language: $language, + status: $status, + objectUuid: $objectUuid, + limit: max(1, min(1000, (int) ($limit ?? 100))), + sourceLanguage: $sourceLanguage, + isOutOfDate: $isOutOfDateBool + ); + } else { + $rows = $this->statusService->search( + query: $query, + language: $language, + status: $status, + objectUuid: $objectUuid, + limit: max(1, min(1000, (int) ($limit ?? 100))), + sourceLanguage: $sourceLanguage, + isOutOfDate: $isOutOfDateBool + ); + }//end if return new JSONResponse( [ @@ -111,6 +143,23 @@ public function search( ); }//end search() + /** + * Coerce a string-ish query value to a boolean. + * + * @param string|null $value Raw query parameter value. + * + * @return bool True when the value is `1`, `true`, `yes`, or `on`. + */ + private function isTruthy(?string $value): bool + { + if ($value === null || $value === '') { + return false; + } + + $normalised = strtolower(trim($value)); + return in_array($normalised, ['1', 'true', 'yes', 'on'], true); + }//end isTruthy() + /** * List every translation slot for one object + completeness summary. * @@ -124,7 +173,7 @@ public function search( * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-8 + * @spec openspec/specs/register-i18n/spec.md */ public function showByObject(string $uuid, ?string $schema=null): JSONResponse { @@ -161,7 +210,7 @@ public function showByObject(string $uuid, ?string $schema=null): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-8 + * @spec openspec/specs/register-i18n/spec.md */ public function setStatus(string $uuid, string $property, string $language, ?string $status=null): JSONResponse { @@ -202,7 +251,7 @@ public function setStatus(string $uuid, string $property, string $language, ?str * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-8 + * @spec openspec/specs/register-i18n/spec.md */ public function bulkTranslate( string $uuid, diff --git a/lib/Controller/UiController.php b/lib/Controller/UiController.php index 43260a5704..48419dc0cf 100644 --- a/lib/Controller/UiController.php +++ b/lib/Controller/UiController.php @@ -43,7 +43,7 @@ * @psalm-type TemplateName = 'index' * @psalm-suppress UnusedClass * - * @SuppressWarnings(PHPMD.TooManyMethods) Every public method is a one-liner + * @SuppressWarnings(PHPMD.TooManyMethods) Every public method is a one-liner * SPA-mount route stub required by the NC AppFramework router: each history- * mode deep-link path needs its own named action so OC\Route\Router does not * drop duplicate route names. Splitting into multiple controllers would not @@ -323,26 +323,6 @@ public function tables(): TemplateResponse return $this->makeSpaResponse(); }//end tables() - /** - * Returns the chat page template. - * - * @NoAdminRequired - * - * @NoCSRFRequired - * - * @phpstan-return TemplateResponse - * - * @psalm-return TemplateResponse<200|500, array> - * - * @return TemplateResponse The SPA template response - * - * @spec exclude Trivial SPA-mount route stub: delegates to makeSpaResponse() for client-side routing. - * SPA-mount contract owned by no-code-app-builder via retrofit-2026-05-24-b-ctrl-misc/tasks.md#task-1. - */ - public function chat(): TemplateResponse - { - return $this->makeSpaResponse(); - }//end chat() /** * Returns the configurations page template. diff --git a/lib/Controller/UrnController.php b/lib/Controller/UrnController.php index 8c7e418361..987e843afc 100644 --- a/lib/Controller/UrnController.php +++ b/lib/Controller/UrnController.php @@ -38,7 +38,7 @@ * * @psalm-suppress UnusedClass * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-7 + * @spec openspec/specs/urn-resource-addressing/spec.md */ class UrnController extends Controller { @@ -70,6 +70,7 @@ public function __construct( * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt No object body: URN-to-URL address translation returning a canonical URL and parsed addressing parts, not object content. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-9 */ @@ -116,6 +117,7 @@ public function resolve(?string $urn=null): JSONResponse * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt No object body: URL-to-URN address translation returning addressing parts, not object content. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-9 */ @@ -154,6 +156,8 @@ public function lookup(?string $url=null): JSONResponse * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt No object body: batch URN address translation (input capped at 1000 for DoS safety); + * returns addressing parts, not object content. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-2/tasks.md#task-9 */ diff --git a/lib/Controller/UserController.php b/lib/Controller/UserController.php index 17600c2a88..cb88e60be1 100644 --- a/lib/Controller/UserController.php +++ b/lib/Controller/UserController.php @@ -107,7 +107,7 @@ public function __construct( * * @suppressWarnings(PHPMD.ShortMethodName) Standard REST API endpoint name for current user * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-10 + * @spec openspec/specs/auth-system/spec.md */ public function me(): JSONResponse { @@ -153,7 +153,7 @@ public function me(): JSONResponse * * @return JSONResponse JSON response with updated user profile * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-10 + * @spec openspec/specs/auth-system/spec.md */ public function updateMe(): JSONResponse { @@ -231,7 +231,7 @@ public function updateMe(): JSONResponse * * @return JSONResponse A JSON response containing login result and user information * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-9 + * @spec openspec/specs/auth-system/spec.md */ public function login(): JSONResponse { @@ -391,7 +391,7 @@ public function login(): JSONResponse * * @return JSONResponse A JSON response confirming logout * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-9 + * @spec openspec/specs/auth-system/spec.md */ public function logout(): JSONResponse { @@ -410,7 +410,7 @@ public function logout(): JSONResponse * * @return JSONResponse JSON response with result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-10 + * @spec openspec/specs/auth-system/spec.md */ public function changePassword(): JSONResponse { @@ -476,7 +476,7 @@ public function changePassword(): JSONResponse * * @return JSONResponse JSON response with result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-10 + * @spec openspec/specs/auth-system/spec.md */ public function uploadAvatar(): JSONResponse { @@ -520,7 +520,7 @@ public function uploadAvatar(): JSONResponse * * @return JSONResponse JSON response with result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-10 + * @spec openspec/specs/auth-system/spec.md */ public function deleteAvatar(): JSONResponse { @@ -555,7 +555,7 @@ public function deleteAvatar(): JSONResponse * * @return JSONResponse|DataDownloadResponse JSON response with export data * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function exportData(): JSONResponse|DataDownloadResponse { @@ -599,7 +599,7 @@ public function exportData(): JSONResponse|DataDownloadResponse * * @return JSONResponse JSON response with preferences * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function getNotificationPreferences(): JSONResponse { @@ -627,7 +627,7 @@ public function getNotificationPreferences(): JSONResponse * * @return JSONResponse JSON response with updated preferences * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function updateNotificationPreferences(): JSONResponse { @@ -666,7 +666,7 @@ public function updateNotificationPreferences(): JSONResponse * * @return JSONResponse JSON response with activity list * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function getActivity(): JSONResponse { @@ -700,7 +700,7 @@ public function getActivity(): JSONResponse * * @return JSONResponse JSON response with token list * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function listTokens(): JSONResponse { @@ -728,7 +728,7 @@ public function listTokens(): JSONResponse * * @return JSONResponse JSON response with the created token * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function createToken(): JSONResponse { @@ -772,7 +772,7 @@ public function createToken(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function revokeToken(string $id): JSONResponse { @@ -807,7 +807,7 @@ public function revokeToken(string $id): JSONResponse * * @return JSONResponse JSON response with result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function requestDeactivation(): JSONResponse { @@ -851,7 +851,7 @@ public function requestDeactivation(): JSONResponse * * @return JSONResponse JSON response with status * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function getDeactivationStatus(): JSONResponse { @@ -879,7 +879,7 @@ public function getDeactivationStatus(): JSONResponse * * @return JSONResponse JSON response with result * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-11 + * @spec openspec/specs/auth-system/spec.md */ public function cancelDeactivation(): JSONResponse { diff --git a/lib/Controller/UserSettingsController.php b/lib/Controller/UserSettingsController.php index 510b68b271..d48fd91d33 100644 --- a/lib/Controller/UserSettingsController.php +++ b/lib/Controller/UserSettingsController.php @@ -99,7 +99,7 @@ public function __construct( * message?: 'No GitHub token configured'|'Token is invalid or expired'| * 'Token is valid'}, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-12 + * @spec openspec/specs/auth-system/spec.md */ public function getGitHubTokenStatus(): JSONResponse { @@ -158,7 +158,7 @@ public function getGitHubTokenStatus(): JSONResponse * message?: 'GitHub token saved successfully'}, * array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-12 + * @spec openspec/specs/auth-system/spec.md */ public function setGitHubToken(): JSONResponse { @@ -223,7 +223,7 @@ public function setGitHubToken(): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-12 + * @spec openspec/specs/auth-system/spec.md */ public function removeGitHubToken(): JSONResponse { diff --git a/lib/Controller/VerwerkingsactiviteitenController.php b/lib/Controller/VerwerkingsactiviteitenController.php index 8fb4f100af..0111fd06a7 100644 --- a/lib/Controller/VerwerkingsactiviteitenController.php +++ b/lib/Controller/VerwerkingsactiviteitenController.php @@ -23,9 +23,9 @@ * @category Controller * @package OCA\OpenRegister\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -90,7 +90,7 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-4 + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function index(): JSONResponse { @@ -148,7 +148,7 @@ public function index(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-4 + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function show(string $id): JSONResponse { @@ -196,7 +196,7 @@ public function show(string $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-4 + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function create(): JSONResponse { @@ -234,7 +234,7 @@ public function create(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-4 + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function update(string $id): JSONResponse { @@ -280,7 +280,7 @@ public function update(string $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-4 + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function destroy(string $id): JSONResponse { @@ -331,7 +331,7 @@ public function destroy(string $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-ai-gdpr-i18n/tasks.md#task-5 + * @spec openspec/specs/verwerkingsregister-api/spec.md */ public function verantwoording(): JSONResponse { diff --git a/lib/Controller/ViewsController.php b/lib/Controller/ViewsController.php index c12c451f83..738bb9d255 100644 --- a/lib/Controller/ViewsController.php +++ b/lib/Controller/ViewsController.php @@ -43,8 +43,8 @@ * @suppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-3 + * @spec openspec/specs/faceting-configuration/spec.md + * @spec openspec/specs/faceting-configuration/spec.md */ class ViewsController extends Controller { diff --git a/lib/Controller/WebPushController.php b/lib/Controller/WebPushController.php new file mode 100644 index 0000000000..b0595fe277 --- /dev/null +++ b/lib/Controller/WebPushController.php @@ -0,0 +1,210 @@ +userSession->getUser()->getUID()`) and never + * accept a target uid from the request — a user can only manage their own + * subscriptions. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Controller + * @package OCA\OpenRegister\Controller + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Controller; + +use OCA\OpenRegister\Db\PushSubscriptionMapper; +use OCA\OpenRegister\Service\WebPush\HexIconService; +use OCA\OpenRegister\Service\WebPush\WebPushService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\IUserSession; + +/** + * Web Push subscription + VAPID key + hex-icon REST controller. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ +class WebPushController extends Controller +{ + /** + * Constructor. + * + * @param string $appName App id. + * @param IRequest $request Active HTTP request. + * @param WebPushService $webPushService VAPID key + delivery service. + * @param PushSubscriptionMapper $subscriptionMapper Subscription store. + * @param HexIconService $hexIconService Hex-icon raster service. + * @param IUserSession $userSession Current session (owner binding). + */ + public function __construct( + string $appName, + IRequest $request, + private readonly WebPushService $webPushService, + private readonly PushSubscriptionMapper $subscriptionMapper, + private readonly HexIconService $hexIconService, + private readonly IUserSession $userSession, + ) { + parent::__construct(appName: $appName, request: $request); + }//end __construct() + + /** + * Return the configured VAPID public key (never the private key). + * + * @return JSONResponse `{ publicKey: , configured: }`. + * + * @no-admin-idor-exempt No per-object resource: returns the public VAPID key and configured flag only; the private key is never exposed. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function vapidPublicKey(): JSONResponse + { + return new JSONResponse( + [ + 'publicKey' => $this->webPushService->getPublicKey(), + 'configured' => $this->webPushService->isConfigured(), + ] + ); + }//end vapidPublicKey() + + /** + * Store a push subscription for the CURRENT user. + * + * Body: `{ endpoint, keys: { p256dh, auth } }` (the browser PushSubscription + * JSON). The owner is ALWAYS the session user — no uid is read from the body. + * + * @param string $endpoint The push endpoint URL. + * @param array $keys The client keys (p256dh + auth). + * + * @return JSONResponse The stored subscription summary, or an error. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + #[NoAdminRequired] + public function subscribe(string $endpoint='', array $keys=[]): JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse(['error' => 'No active user session'], Http::STATUS_UNAUTHORIZED); + } + + $p256dh = (string) ($keys['p256dh'] ?? ''); + $auth = (string) ($keys['auth'] ?? ''); + if ($endpoint === '' || $p256dh === '' || $auth === '') { + return new JSONResponse(['error' => 'endpoint, keys.p256dh and keys.auth are required'], Http::STATUS_BAD_REQUEST); + } + + $subscription = $this->subscriptionMapper->store( + userId: $user->getUID(), + endpoint: $endpoint, + p256dh: $p256dh, + auth: $auth, + userAgent: $this->request->getHeader('User-Agent') + ); + + return new JSONResponse($subscription->jsonSerialize(), Http::STATUS_CREATED); + }//end subscribe() + + /** + * Delete the CURRENT user's subscription for the given endpoint. + * + * @param string $endpoint The push endpoint URL to remove. + * + * @return JSONResponse `{ deleted: }`, or an error. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + #[NoAdminRequired] + public function unsubscribe(string $endpoint=''): JSONResponse + { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse(['error' => 'No active user session'], Http::STATUS_UNAUTHORIZED); + } + + if ($endpoint === '') { + return new JSONResponse(['error' => 'endpoint is required'], Http::STATUS_BAD_REQUEST); + } + + // Owner-scoped delete: keyed by the session uid, never a wire uid. + $deleted = $this->subscriptionMapper->deleteByUserAndEndpoint( + userId: $user->getUID(), + endpoint: $endpoint + ); + + return new JSONResponse(['deleted' => $deleted]); + }//end unsubscribe() + + /** + * Serve the cached cobalt-hex notification icon PNG for an app. + * + * Public: the icon is referenced by the Service Worker / OS notification + * surface which has no Nextcloud session; it leaks no user data (just an + * app glyph on a hexagon). + * + * @param string $app The originApp id. + * + * @return DataDisplayResponse The image bytes. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + #[PublicPage] + #[NoCSRFRequired] + public function hexIcon(string $app): DataDisplayResponse + { + $icon = $this->hexIconService->getIcon($app); + $response = new DataDisplayResponse($icon['body'], Http::STATUS_OK, ['Content-Type' => $icon['mime']]); + $response->cacheFor(86400); + return $response; + }//end hexIcon() + + /** + * Serve the cached monochrome notification badge PNG for an app. + * + * @param string $app The originApp id. + * + * @return DataDisplayResponse The image bytes. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + #[PublicPage] + #[NoCSRFRequired] + public function hexBadge(string $app): DataDisplayResponse + { + $badge = $this->hexIconService->getBadge($app); + $response = new DataDisplayResponse($badge['body'], Http::STATUS_OK, ['Content-Type' => $badge['mime']]); + $response->cacheFor(86400); + return $response; + }//end hexBadge() +}//end class diff --git a/lib/Controller/WebhooksController.php b/lib/Controller/WebhooksController.php index fed3ebd35a..c56ee411c5 100644 --- a/lib/Controller/WebhooksController.php +++ b/lib/Controller/WebhooksController.php @@ -19,10 +19,10 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-88 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-87 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-39 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/webhook-payload-mapping/spec.md + * @spec openspec/specs/webhook-payload-mapping/spec.md + * @spec openspec/specs/event-driven-architecture/spec.md + * @spec openspec/specs/webhook-payload-mapping/spec.md */ declare(strict_types=1); @@ -61,8 +61,8 @@ * @suppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-4 - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-5 + * @spec openspec/specs/webhook-payload-mapping/spec.md + * @spec openspec/specs/webhook-payload-mapping/spec.md */ class WebhooksController extends Controller { @@ -206,12 +206,17 @@ private function forbiddenResponse(): JSONResponse * @suppressWarnings(PHPMD.NPathComplexity) Complex request parameter handling for flexible API * @suppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-88 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] public function index(): JSONResponse { + // Webhook config listing is admin-only (matches the write endpoints). + if ($this->isCurrentUserAdmin() === false) { + return $this->forbiddenResponse(); + } + try { // Get request parameters for filtering and searching. $params = $this->request->getParams(); @@ -316,12 +321,17 @@ function ($webhook) { * array * > * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-4 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] public function show(int $id): JSONResponse { + // Reading a webhook's config (target URL, org, filters) is admin-only. + if ($this->isCurrentUserAdmin() === false) { + return $this->forbiddenResponse(); + } + try { $webhook = $this->webhookMapper->find($id); @@ -362,7 +372,7 @@ public function show(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-88 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -449,7 +459,7 @@ public function create(): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-88 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -528,7 +538,7 @@ public function update(int $id): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-88 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -592,7 +602,7 @@ public function destroy(int $id): JSONResponse * * @NoCSRFRequired * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-87 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -716,10 +726,12 @@ public function test(int $id): JSONResponse * @NoAdminRequired * * @NoCSRFRequired + * @no-admin-idor-exempt No per-object resource: returns the static catalogue of available webhook event-type definitions + * (identical for every install); no tenant data. * * @suppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/event-driven-architecture/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] @@ -1065,12 +1077,17 @@ public function events(): JSONResponse * array * > * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-5 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] public function logs(int $id): JSONResponse { + // Delivery logs carry unmasked request/response bodies — admin-only. + if ($this->isCurrentUserAdmin() === false) { + return $this->forbiddenResponse(); + } + try { // Validate webhook exists by attempting to find it. $this->webhookMapper->find($id); @@ -1130,12 +1147,17 @@ public function logs(int $id): JSONResponse * 'Webhook not found', total?: int, successful?: int, failed?: int, * pendingRetries?: int<0, max>}, array> * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-5 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] public function logStats(int $id): JSONResponse { + // Delivery-log statistics are admin-only. + if ($this->isCurrentUserAdmin() === false) { + return $this->forbiddenResponse(); + } + try { // Validate webhook exists by attempting to find it. $this->webhookMapper->find($id); @@ -1186,12 +1208,17 @@ public function logStats(int $id): JSONResponse * * @suppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-ctrl-registry-views/tasks.md#task-5 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] public function allLogs(): JSONResponse { + // Instance-wide delivery logs are admin-only. + if ($this->isCurrentUserAdmin() === false) { + return $this->forbiddenResponse(); + } + try { $webhookId = $this->request->getParam('webhook_id'); $limit = (int) ($this->request->getParam('limit') ?? 50); @@ -1304,7 +1331,7 @@ function ($log) use ($successBool) { * @suppressWarnings(PHPMD.CyclomaticComplexity) * @suppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ #[NoAdminRequired] #[NoCSRFRequired] diff --git a/lib/Controller/WorkflowEngineController.php b/lib/Controller/WorkflowEngineController.php index e65a0c3428..836d7e3cdc 100644 --- a/lib/Controller/WorkflowEngineController.php +++ b/lib/Controller/WorkflowEngineController.php @@ -17,9 +17,9 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-85 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-91 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-89 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-health-monitoring + * @spec openspec/specs/workflow-engine-abstraction/spec.md + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ declare(strict_types=1); @@ -89,10 +89,17 @@ private function isCurrentUserAdmin(): bool * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function index(): JSONResponse { + // SEC-CTRL: admin-only — workflow engines are instance-wide integration + // config; the serialized metadata exposes internal baseUrl/healthStatus. + // Reads are gated like the create/update/delete siblings. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + $engines = $this->registry->getEngines(); return new JSONResponse( @@ -109,10 +116,16 @@ public function index(): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function show(int $id): JSONResponse { + // SEC-CTRL: admin-only — see index(); engine metadata exposes internal + // baseUrl/healthStatus of an instance-wide integration. + if ($this->isCurrentUserAdmin() === false) { + return new JSONResponse(['error' => 'Admin privileges required'], 403); + } + try { $engine = $this->registry->getEngine($id); @@ -135,7 +148,7 @@ public function show(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function create( string $name, @@ -195,7 +208,7 @@ public function create( * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function update(int $id): JSONResponse { @@ -222,7 +235,7 @@ public function update(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function destroy(int $id): JSONResponse { @@ -246,8 +259,8 @@ public function destroy(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-85 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-health-monitoring + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function health(int $id): JSONResponse { @@ -267,7 +280,7 @@ public function health(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-89 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function available(): JSONResponse { @@ -285,7 +298,7 @@ public function available(): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function testHook(int $id): JSONResponse { diff --git a/lib/Controller/WorkflowExecutionController.php b/lib/Controller/WorkflowExecutionController.php index 1934437a35..729915f6fb 100644 --- a/lib/Controller/WorkflowExecutionController.php +++ b/lib/Controller/WorkflowExecutionController.php @@ -17,7 +17,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-83 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-workflow-execution-api-sync-and-async */ declare(strict_types=1); @@ -61,7 +61,7 @@ public function __construct( * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-83 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-workflow-execution-api-sync-and-async */ public function index(): JSONResponse { @@ -125,7 +125,7 @@ public function index(): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-83 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-workflow-execution-api-sync-and-async */ public function show(int $id): JSONResponse { @@ -145,7 +145,7 @@ public function show(int $id): JSONResponse * * @return JSONResponse * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-83 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-workflow-execution-api-sync-and-async */ public function destroy(int $id): JSONResponse { diff --git a/lib/Controller/XwikiLinksController.php b/lib/Controller/XwikiLinksController.php index 73f6a85bc8..5760dc5d7f 100644 --- a/lib/Controller/XwikiLinksController.php +++ b/lib/Controller/XwikiLinksController.php @@ -250,6 +250,8 @@ public function destroy(string $register, string $schema, string $id, string $pa * * @NoAdminRequired * @NoCSRFRequired + * @no-admin-idor-exempt Capability/config probe: reports whether the admin-configured XWiki OpenConnector source is reachable; + * no caller-supplied per-object resource. * * @spec openspec/changes/retrofit-2026-05-25-bw2-ctrl-1/tasks.md#task-1 */ @@ -277,6 +279,71 @@ public function available(): JSONResponse return new JSONResponse(['results' => $result['results'], 'total' => $result['total']]); }//end available() + /** + * Free-text search the remote xWiki knowledge base (object-independent). + * + * The query-first surface a consuming app (e.g. a knowledge-base dashboard + * widget) calls to find xWiki pages by free text, with no OR object context. + * The xWiki base URL is resolved entirely from the OpenConnector `xwiki` + * source — the caller never holds a direct xWiki URL. + * + * Query params: + * - `q` / `search` — free-text query (optional; empty lists available pages) + * - `limit` — max results (clamped 1..100, default 25) + * - `offset` — pagination offset (clamped >= 0, default 0) + * + * Read-only and null-safe: an unconfigured `xwiki` source / unreachable + * upstream returns 503 with `details.cause` (the 4-state UX), never a fatal. + * + * @return JSONResponse `{ results, total, limit, offset }` on success, or a + * 503 `{ error, details: { cause } }` when the source is + * unconfigured/down. + * + * @NoAdminRequired + * @NoCSRFRequired + * @no-admin-idor-exempt No per-object resource: object-independent free-text knowledge-base search proxied to the + * admin-configured XWiki source; takes no OpenRegister object id. + * + * @spec openspec/changes/integration-xwiki-query-search/specs/integration-xwiki/spec.md + */ + public function search(): JSONResponse + { + $query = $this->request->getParam('q'); + if ($query === null) { + $query = $this->request->getParam('search'); + } + + if ($query !== null) { + $query = (string) $query; + } + + $limit = (int) $this->request->getParam('limit', 25); + $offset = (int) $this->request->getParam('offset', 0); + + $result = $this->xwikiLinkService->searchPages($query, $limit, $offset); + + // Unconfigured-source / upstream-down → 503 with details.cause so the + // consumer renders its Configure-XWiki / unavailable state (4-state UX). + if (($result['unavailable'] ?? false) === true) { + return new JSONResponse( + [ + 'error' => 'xWiki source is not available', + 'details' => ['cause' => $result['cause']], + ], + 503 + ); + } + + return new JSONResponse( + [ + 'results' => $result['results'], + 'total' => $result['total'], + 'limit' => $result['limit'], + 'offset' => $result['offset'], + ] + ); + }//end search() + /** * Resolve an OR object from register/schema/id. * diff --git a/lib/Cron/ArchivalRetentionTask.php b/lib/Cron/ArchivalRetentionTask.php index 20f2c843ff..782b5a4303 100644 --- a/lib/Cron/ArchivalRetentionTask.php +++ b/lib/Cron/ArchivalRetentionTask.php @@ -170,7 +170,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-11 + * @spec openspec/specs/retention-management/spec.md */ protected function run($argument): void { diff --git a/lib/Cron/ConfigurationCheckJob.php b/lib/Cron/ConfigurationCheckJob.php index bfa64041b5..cec2fd013a 100644 --- a/lib/Cron/ConfigurationCheckJob.php +++ b/lib/Cron/ConfigurationCheckJob.php @@ -88,7 +88,7 @@ class ConfigurationCheckJob extends TimedJob * @param IAppConfig $appConfig App configuration * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-18 + * @spec openspec/specs/notificatie-engine/spec.md */ public function __construct( ITimeFactory $time, @@ -139,7 +139,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-18 + * @spec openspec/specs/notificatie-engine/spec.md */ protected function run($argument): void { @@ -189,7 +189,7 @@ protected function run($argument): void * * @return bool True if job is disabled, false otherwise. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-18 + * @spec openspec/specs/notificatie-engine/spec.md */ private function isJobDisabled(): bool { @@ -213,7 +213,7 @@ private function isJobDisabled(): bool * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-18 + * @spec openspec/specs/notificatie-engine/spec.md */ private function checkSingleConfiguration($configuration, array &$stats): void { @@ -282,7 +282,7 @@ private function checkSingleConfiguration($configuration, array &$stats): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-18 + * @spec openspec/specs/notificatie-engine/spec.md */ private function handleAutoUpdate($configuration, array &$stats): void { @@ -320,7 +320,7 @@ private function handleAutoUpdate($configuration, array &$stats): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-18 + * @spec openspec/specs/notificatie-engine/spec.md */ private function sendUpdateNotification($configuration): void { diff --git a/lib/Cron/ConfigurationCheckJob.php.backup_20251230_001404 b/lib/Cron/ConfigurationCheckJob.php.backup_20251230_001404 deleted file mode 100644 index 6fae9319ec..0000000000 --- a/lib/Cron/ConfigurationCheckJob.php.backup_20251230_001404 +++ /dev/null @@ -1,268 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.app - */ - -namespace OCA\OpenRegister\Cron; - -use OCA\OpenRegister\Db\ConfigurationMapper; -use OCA\OpenRegister\Service\ConfigurationService; -use OCA\OpenRegister\Service\NotificationService; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; -use OCP\IAppConfig; -use Psr\Log\LoggerInterface; -use Exception; - -/** - * Class ConfigurationCheckJob - * - * Background job for checking remote configurations for updates. - * Runs at configurable intervals to check if remote configurations have newer versions. - * Can automatically import updates if configured. - * - * @package OCA\OpenRegister\Cron - * - * @psalm-suppress UnusedClass - */ -class ConfigurationCheckJob extends TimedJob -{ - - /** - * Configuration mapper instance. - * - * @var ConfigurationMapper The configuration mapper instance. - */ - private ConfigurationMapper $configurationMapper; - - /** - * Configuration service instance. - * - * @var ConfigurationService The configuration service instance. - */ - private ConfigurationService $configurationService; - - /** - * Notification service instance. - * - * @var NotificationService The notification service instance. - */ - private NotificationService $notificationService; - - /** - * App configuration instance. - * - * @var IAppConfig The app configuration instance. - */ - private IAppConfig $appConfig; - - /** - * Logger instance. - * - * @var LoggerInterface The logger instance. - */ - private LoggerInterface $logger; - - /** - * Constructor - * - * @param ITimeFactory $time Time factory for job scheduling - * @param ConfigurationMapper $configurationMapper Configuration mapper - * @param ConfigurationService $configurationService Configuration service - * @param NotificationService $notificationService Notification service - * @param IAppConfig $appConfig App configuration - * @param LoggerInterface $logger Logger - */ - public function __construct( - ITimeFactory $time, - ConfigurationMapper $configurationMapper, - ConfigurationService $configurationService, - NotificationService $notificationService, - IAppConfig $appConfig, - LoggerInterface $logger - ) { - parent::__construct($time); - - $this->configurationMapper = $configurationMapper; - $this->configurationService = $configurationService; - $this->notificationService = $notificationService; - $this->appConfig = $appConfig; - $this->logger = $logger; - - // Set interval based on app configuration (default 3600 seconds = 1 hour). - $interval = (int) $this->appConfig->getValueString('openregister', 'configuration_check_interval', '3600'); - - // If interval is 0, disable the job by setting a very long interval. - if ($interval === 0) { - $this->setInterval(86400 * 365); - // 1 year. - $this->logger->info('Configuration check job is disabled (interval set to 0)'); - } else { - $this->setInterval($interval); - $this->logger->info("Configuration check job interval set to {$interval} seconds"); - } - }//end __construct() - - /** - * Run the background job - * - * Checks all remote configurations for updates. - * If auto-update is enabled for a configuration, automatically imports the updates. - * - * @param mixed $_argument Job arguments (not used) - * - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - protected function run($_argument): void - { - $this->logger->info('Starting configuration check job'); - - // Check if the job is disabled. - if ($this->isJobDisabled() === true) { - return; - } - - try { - // Get all configurations. - $configurations = $this->configurationMapper->findAll(); - $this->logger->info('Found '.count($configurations).' configurations to check'); - - $stats = ['checked' => 0, 'updated' => 0, 'failed' => 0]; - - foreach ($configurations as $configuration) { - $this->checkSingleConfiguration(configuration: $configuration, stats: $stats); - } - - $this->logger->info( - "Configuration check job completed: {$stats['checked']} checked, {$stats['updated']} updated, {$stats['failed']} failed" - ); - } catch (Exception $e) { - $this->logger->error('Configuration check job failed: '.$e->getMessage()); - }//end try - }//end run() - - /** - * Check if the job is currently disabled via configuration - * - * @return bool True if job is disabled, false otherwise. - */ - private function isJobDisabled(): bool - { - $interval = (int) $this->appConfig->getValueString('openregister', 'configuration_check_interval', '3600'); - if ($interval === 0) { - $this->logger->info('Configuration check job is disabled, skipping'); - return true; - } - - return false; - }//end isJobDisabled() - - /** - * Check a single configuration for updates - * - * @param \OCA\OpenRegister\Db\Configuration $configuration Configuration to check. - * @param array $stats Statistics array (passed by reference). - * - * @return void - */ - private function checkSingleConfiguration($configuration, array &$stats): void - { - try { - // Only check remote configurations. - if ($configuration->isRemoteSource() === false) { - return; - } - - $this->logger->info("Checking configuration: {$configuration->getTitle()} (ID: {$configuration->getId()})"); - - // Check remote version. - $remoteVersion = $this->configurationService->checkRemoteVersion(configuration: $configuration); - $stats['checked']++; - - if ($remoteVersion === null) { - $this->logger->warning("Could not determine remote version for configuration {$configuration->getId()}"); - return; - } - - // Check if update is available. - if ($configuration->hasUpdateAvailable() === false) { - $this->logger->info("Configuration {$configuration->getTitle()} is up to date"); - return; - } - - $this->logger->info("Update available for {$configuration->getTitle()}: {$configuration->getLocalVersion()} → {$remoteVersion}"); - - // Handle the update based on auto-update setting. - if ($configuration->getAutoUpdate() === true) { - $this->handleAutoUpdate(configuration: $configuration, stats: $stats); - } else { - $this->sendUpdateNotification($configuration); - } - } catch (Exception $e) { - $stats['failed']++; - $this->logger->error("Error checking configuration {$configuration->getId()}: ".$e->getMessage()); - }//end try - }//end checkSingleConfiguration() - - /** - * Handle automatic update of a configuration - * - * @param \OCA\OpenRegister\Db\Configuration $configuration Configuration to update. - * @param array $stats Statistics array (passed by reference). - * - * @return void - */ - private function handleAutoUpdate($configuration, array &$stats): void - { - $this->logger->info("Auto-update enabled, importing updates for {$configuration->getTitle()}"); - - try { - // Import all changes (no selection, import everything). - $this->configurationService->importConfigurationWithSelection( - configuration: $configuration, - selection: [] - // Empty selection means import all. - ); - - $stats['updated']++; - $this->logger->info("Successfully auto-updated configuration {$configuration->getTitle()}"); - } catch (Exception $e) { - $this->logger->error("Failed to auto-update configuration {$configuration->getTitle()}: ".$e->getMessage()); - $stats['failed']++; - } - }//end handleAutoUpdate() - - /** - * Send update notification for a configuration - * - * @param \OCA\OpenRegister\Db\Configuration $configuration Configuration to notify about. - * - * @return void - */ - private function sendUpdateNotification($configuration): void - { - $this->logger->info("Auto-update disabled for {$configuration->getTitle()}, sending notification"); - - try { - // Send notification to configured groups. - $notificationCount = $this->notificationService->notifyConfigurationUpdate(configuration: $configuration); - $this->logger->info("Sent {$notificationCount} notifications for configuration {$configuration->getTitle()}"); - } catch (Exception $e) { - $this->logger->error("Failed to send notifications for configuration {$configuration->getTitle()}: ".$e->getMessage()); - } - }//end sendUpdateNotification() -}//end class diff --git a/lib/Cron/DbalIntrospectionJob.php b/lib/Cron/DbalIntrospectionJob.php new file mode 100644 index 0000000000..bf2a2b0063 --- /dev/null +++ b/lib/Cron/DbalIntrospectionJob.php @@ -0,0 +1,150 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Cron; + +use OCA\OpenRegister\Db\Source; +use OCA\OpenRegister\Db\SourceMapper; +use OCA\OpenRegister\Service\Dbal\DatabaseIntrospectionService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Scheduled drift-detection re-introspection for database sources. + * + * @psalm-suppress UnusedClass + */ +class DbalIntrospectionJob extends TimedJob +{ + /** + * The Source `type` this job re-introspects. + * + * @var string + */ + private const DATABASE_SOURCE_TYPE = 'database'; + + /** + * Constructor. + * + * @param ITimeFactory $time Time factory for scheduling. + * @param SourceMapper $sourceMapper Loads database sources. + * @param DatabaseIntrospectionService $introspectionService Re-introspects a source and applies diffs. + * @param LoggerInterface $logger Secret-free diagnostics. + * + * @return void + */ + public function __construct( + ITimeFactory $time, + private readonly SourceMapper $sourceMapper, + private readonly DatabaseIntrospectionService $introspectionService, + private readonly LoggerInterface $logger, + ) { + parent::__construct(time: $time); + + // Re-introspect roughly every 6 hours; drift is not time-critical. + $this->setInterval(seconds: (6 * 3600)); + }//end __construct() + + /** + * Run the job: re-introspect every database source, isolating failures. + * + * @param mixed $argument Job arguments (unused). + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) TimedJob contract passes an argument. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + protected function run($argument): void + { + $sources = $this->loadDatabaseSources(); + if ($sources === []) { + return; + } + + $this->logger->info(sprintf('[DbalIntrospectionJob] re-introspecting %d database source(s)', count($sources))); + + foreach ($sources as $source) { + $this->introspectSource(source: $source); + } + }//end run() + + /** + * Re-introspect a single source, catching and logging any failure. + * + * @param Source $source The database source to re-introspect. + * + * @return void + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function introspectSource(Source $source): void + { + try { + $result = $this->introspectionService->introspect(source: $source); + + if (empty($result['drift']) === false) { + $this->logger->info( + sprintf( + '[DbalIntrospectionJob] source %s drift: %d table(s) changed', + (string) ($source->getUuid() ?? $source->getId()), + count($result['drift']) + ) + ); + } + } catch (Throwable $e) { + $this->logger->warning( + sprintf( + '[DbalIntrospectionJob] source %s re-introspection failed: %s', + (string) ($source->getUuid() ?? $source->getId()), + $e->getMessage() + ) + ); + }//end try + }//end introspectSource() + + /** + * Load every `type: database` source, tolerating a mapper failure. + * + * @return array The database sources (possibly empty). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function loadDatabaseSources(): array + { + try { + return $this->sourceMapper->findAll(filters: ['type' => self::DATABASE_SOURCE_TYPE]); + } catch (Throwable $e) { + $this->logger->warning('[DbalIntrospectionJob] could not list database sources: '.$e->getMessage()); + return []; + } + }//end loadDatabaseSources() +}//end class diff --git a/lib/Cron/HandoffQueueDrainJob.php b/lib/Cron/HandoffQueueDrainJob.php new file mode 100644 index 0000000000..fd30deee3e --- /dev/null +++ b/lib/Cron/HandoffQueueDrainJob.php @@ -0,0 +1,105 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Cron; + +use OCA\OpenRegister\Service\Handoff\HandoffService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; + +/** + * Periodic fallback drain of the handoff queue. + * + * @psalm-suppress UnusedClass Instantiated by the NC background-job framework (appinfo/info.xml). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ +class HandoffQueueDrainJob extends TimedJob +{ + + /** + * Default interval: 5 minutes (WebhookRetryJob cadence class). + * + * @var integer + */ + private const DEFAULT_INTERVAL = 300; + + /** + * Constructor. + * + * @param ITimeFactory $time Time factory. + * @param HandoffService $handoffService The handoff engine (drain surface). + * @param LoggerInterface $logger Structured logging. + */ + public function __construct( + ITimeFactory $time, + private readonly HandoffService $handoffService, + private readonly LoggerInterface $logger, + ) { + parent::__construct(time: $time); + + $this->setInterval(seconds: self::DEFAULT_INTERVAL); + + }//end __construct() + + /** + * Sweep parked entries whose kind now resolves. + * + * @param mixed $argument Job arguments (unused). + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + protected function run($argument): void + { + try { + $summary = $this->handoffService->drainParked(); + if ($summary['drained'] > 0 || $summary['failed'] > 0) { + $this->logger->info( + message: '[HandoffQueueDrainJob] Fallback drain sweep completed', + context: ['file' => __FILE__, 'line' => __LINE__, 'summary' => $summary] + ); + } + } catch (\Throwable $e) { + $this->logger->error( + message: '[HandoffQueueDrainJob] Fallback drain sweep failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + } + + }//end run() +}//end class diff --git a/lib/Cron/LogCleanUpTask.php b/lib/Cron/LogCleanUpTask.php index ba501ad70d..ca0cc7a05b 100644 --- a/lib/Cron/LogCleanUpTask.php +++ b/lib/Cron/LogCleanUpTask.php @@ -63,7 +63,7 @@ class LogCleanUpTask extends TimedJob * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-15 + * @spec openspec/specs/retention-management/spec.md */ public function __construct( ITimeFactory $time, @@ -96,7 +96,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-15 + * @spec openspec/specs/retention-management/spec.md */ protected function run($argument): void { diff --git a/lib/Cron/SyncConfigurationsJob.php b/lib/Cron/SyncConfigurationsJob.php index 775bfe4fdd..878e7dfaff 100644 --- a/lib/Cron/SyncConfigurationsJob.php +++ b/lib/Cron/SyncConfigurationsJob.php @@ -101,7 +101,7 @@ class SyncConfigurationsJob extends TimedJob * @param Client $httpClient HTTP client * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-14 + * @spec openspec/specs/faceting-configuration/spec.md */ public function __construct( ITimeFactory $time, @@ -136,7 +136,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-14 + * @spec openspec/specs/faceting-configuration/spec.md */ protected function run($argument): void { @@ -225,7 +225,7 @@ protected function run($argument): void * * @return bool True if sync is due * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-14 + * @spec openspec/specs/faceting-configuration/spec.md */ private function isDueForSync(Configuration $configuration): bool { @@ -253,7 +253,7 @@ private function isDueForSync(Configuration $configuration): bool * @return void * @throws Exception If sync fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-14 + * @spec openspec/specs/faceting-configuration/spec.md */ private function syncConfiguration(Configuration $configuration): void { @@ -289,7 +289,7 @@ private function syncConfiguration(Configuration $configuration): void * @return void * @throws Exception If sync fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-14 + * @spec openspec/specs/faceting-configuration/spec.md */ private function syncFromGitHub(Configuration $configuration): void { @@ -341,7 +341,7 @@ private function syncFromGitHub(Configuration $configuration): void * @return void * @throws Exception If sync fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-14 + * @spec openspec/specs/faceting-configuration/spec.md */ private function syncFromGitLab(Configuration $configuration): void { @@ -397,7 +397,7 @@ private function syncFromGitLab(Configuration $configuration): void * @return void * @throws Exception If sync fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-14 + * @spec openspec/specs/faceting-configuration/spec.md */ private function syncFromUrl(Configuration $configuration): void { @@ -444,7 +444,7 @@ private function syncFromUrl(Configuration $configuration): void * @return void * @throws Exception If sync fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-14 + * @spec openspec/specs/faceting-configuration/spec.md */ private function syncFromLocal(Configuration $configuration): void { diff --git a/lib/Cron/SyncDataJob.php b/lib/Cron/SyncDataJob.php new file mode 100644 index 0000000000..6bb68e0042 --- /dev/null +++ b/lib/Cron/SyncDataJob.php @@ -0,0 +1,166 @@ + fetch -> + * import pipeline and records the outcome on the Source (lastSyncDate, + * lastSyncStatus). Mirrors SyncConfigurationsJob's interval-check structure + * but delegates "is this source due?" to the testable SyncScheduleService + * and the actual work to HarvestPipelineService. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Cron + * @package OCA\OpenRegister\Cron + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +namespace OCA\OpenRegister\Cron; + +use DateTime; +use OCA\OpenRegister\Db\Source; +use OCA\OpenRegister\Db\SourceMapper; +use OCA\OpenRegister\Service\Sync\HarvestPipelineService; +use OCA\OpenRegister\Service\Sync\SourceFetcherRegistry; +use OCA\OpenRegister\Service\Sync\SyncScheduleService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; +use Symfony\Component\Uid\Uuid; +use Throwable; + +/** + * Scheduled harvest driver. + * + * @package OCA\OpenRegister\Cron + * + * @psalm-suppress UnusedClass + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +class SyncDataJob extends TimedJob +{ + /** + * Constructor. + * + * @param ITimeFactory $time Time factory for scheduling + * @param SourceMapper $sourceMapper Source persistence + * @param SyncScheduleService $scheduleService Due-source selection + * @param SourceFetcherRegistry $fetcherRegistry Resolves transport per source type + * @param HarvestPipelineService $pipeline Pipeline orchestrator + * @param LoggerInterface $logger Logger + */ + public function __construct( + ITimeFactory $time, + private readonly SourceMapper $sourceMapper, + private readonly SyncScheduleService $scheduleService, + private readonly SourceFetcherRegistry $fetcherRegistry, + private readonly HarvestPipelineService $pipeline, + private readonly LoggerInterface $logger + ) { + parent::__construct(time: $time); + + // Run every hour; each source is gated by its own interval. + $this->setInterval(seconds: 3600); + }//end __construct() + + /** + * Run the job: sync all due sources. + * + * @param mixed $argument Job arguments (unused) + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + protected function run($argument): void + { + $this->logger->info(message: '[SyncDataJob] Starting data sync job'); + + try { + $sources = $this->sourceMapper->findBySyncEnabled(); + $now = new DateTime(); + $due = $this->scheduleService->selectDueSources(sources: $sources, now: $now); + + $this->logger->info( + message: sprintf('[SyncDataJob] %d source(s) enabled, %d due', count($sources), count($due)) + ); + + foreach ($due as $source) { + $this->syncSource(source: $source); + } + } catch (Throwable $e) { + $this->logger->error(message: '[SyncDataJob] Data sync job failed: '.$e->getMessage()); + } + }//end run() + + /** + * Sync a single source through the harvest pipeline. + * + * @param Source $source The source to sync + * + * @return void + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + private function syncSource(Source $source): void + { + $type = (string) $source->getType(); + $fetcher = $this->fetcherRegistry->get($type); + if ($fetcher === null) { + $this->logger->warning( + message: sprintf('[SyncDataJob] No fetcher for source type "%s" (source %s)', $type, $source->getUuid()) + ); + return; + } + + $executionId = (string) Uuid::v4(); + + // Overlap protection marker. + $source->setLastSyncStatus('running'); + $this->sourceMapper->update($source); + + try { + $since = null; + if ($source->getLastSyncToken() !== null) { + $since = $source->getLastSyncToken(); + } else if ($source->getLastSyncDate() !== null) { + $since = $source->getLastSyncDate()->format('c'); + } + + $summary = $this->pipeline->run( + source: $source, + fetcher: $fetcher, + executionId: $executionId, + since: $since + ); + + $source->setLastSyncStatus((string) ($summary['status'] ?? 'success')); + $source->setLastSyncDate(new DateTime()); + $this->sourceMapper->update($source); + + $this->logger->info( + message: sprintf('[SyncDataJob] Synced source %s: %s', $source->getUuid(), (string) ($summary['status'] ?? 'success')) + ); + } catch (Throwable $e) { + $source->setLastSyncStatus('failed'); + $source->setLastSyncDate(new DateTime()); + $this->sourceMapper->update($source); + $this->logger->error( + message: sprintf('[SyncDataJob] Source %s sync failed: %s', $source->getUuid(), $e->getMessage()) + ); + }//end try + }//end syncSource() +}//end class diff --git a/lib/Cron/TransferCheckJob.php b/lib/Cron/TransferCheckJob.php index 888d281ef2..9ff10ae178 100644 --- a/lib/Cron/TransferCheckJob.php +++ b/lib/Cron/TransferCheckJob.php @@ -20,7 +20,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ declare(strict_types=1); @@ -63,7 +63,7 @@ class TransferCheckJob extends TimedJob * @param IAppConfig $appConfig The app configuration. * @param LoggerInterface $logger Logger. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-12 + * @spec openspec/specs/edepot-transfer/spec.md */ public function __construct( ITimeFactory $time, @@ -91,7 +91,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-12 + * @spec openspec/specs/edepot-transfer/spec.md */ protected function run(mixed $argument): void { @@ -139,7 +139,7 @@ protected function run(mixed $argument): void * * @return bool True if e-Depot endpoint is configured. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-12 + * @spec openspec/specs/edepot-transfer/spec.md */ private function isEdepotConfigured(): bool { @@ -158,7 +158,7 @@ private function isEdepotConfigured(): bool * * @return array Eligible objects. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ private function findEligibleObjects(): array { diff --git a/lib/Cron/WebhookRetryJob.php b/lib/Cron/WebhookRetryJob.php index 5384480125..51b9973fff 100644 --- a/lib/Cron/WebhookRetryJob.php +++ b/lib/Cron/WebhookRetryJob.php @@ -93,7 +93,7 @@ class WebhookRetryJob extends TimedJob * @param WebhookService $webhookService Webhook service * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-13 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ public function __construct( ITimeFactory $time, @@ -125,7 +125,7 @@ public function __construct( * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-13 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ protected function run($argument): void { diff --git a/lib/Cron/WebhookRetryJob.php.backup_20251230_001130 b/lib/Cron/WebhookRetryJob.php.backup_20251230_001130 deleted file mode 100644 index 2856eddc5c..0000000000 --- a/lib/Cron/WebhookRetryJob.php.backup_20251230_001130 +++ /dev/null @@ -1,225 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Cron; - -use DateTime; -use OCA\OpenRegister\Db\WebhookLog; -use OCA\OpenRegister\Db\WebhookLogMapper; -use OCA\OpenRegister\Db\WebhookMapper; -use OCA\OpenRegister\Service\WebhookService; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; -use Psr\Log\LoggerInterface; - -/** - * Webhook Retry Job - * - * Periodically checks for failed webhook deliveries that are ready for retry - * and processes them using exponential backoff intervals. - * - * @category Cron - * @package OpenRegister - * @author Conduction - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister - * - * @psalm-suppress UnusedClass - */ -class WebhookRetryJob extends TimedJob -{ - /** - * Default interval: 5 minutes - */ - private const DEFAULT_INTERVAL = 300; - - /** - * Webhook mapper - * - * @var WebhookMapper - */ - private WebhookMapper $webhookMapper; - - /** - * Webhook log mapper - * - * @var WebhookLogMapper - */ - private WebhookLogMapper $webhookLogMapper; - - /** - * Webhook service - * - * @var WebhookService - */ - private WebhookService $webhookService; - - /** - * Logger - * - * @var LoggerInterface - */ - private LoggerInterface $logger; - - /** - * Constructor - * - * @param ITimeFactory $time Time factory - * @param WebhookMapper $webhookMapper Webhook mapper - * @param WebhookLogMapper $webhookLogMapper Webhook log mapper - * @param WebhookService $webhookService Webhook service - * @param LoggerInterface $logger Logger - */ - public function __construct( - ITimeFactory $time, - WebhookMapper $webhookMapper, - WebhookLogMapper $webhookLogMapper, - WebhookService $webhookService, - LoggerInterface $logger - ) { - parent::__construct($time); - - $this->webhookMapper = $webhookMapper; - $this->webhookLogMapper = $webhookLogMapper; - $this->webhookService = $webhookService; - $this->logger = $logger; - - // Set interval to 5 minutes. - $this->setInterval(self::DEFAULT_INTERVAL); - }//end __construct() - - /** - * Run the retry job - * - * Finds failed webhook logs that are ready for retry and processes them. - * - * @param mixed $_argument Job arguments (unused) - * - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - protected function run($_argument): void - { - $now = new DateTime(); - - $this->logger->debug( - 'Checking for webhook retries', - [ - 'timestamp' => $now->format('c'), - ] - ); - - // Find failed logs ready for retry. - $failedLogs = $this->webhookLogMapper->findFailedForRetry($now); - - if (empty($failedLogs) === true) { - $this->logger->debug('No webhook retries needed'); - return; - } - - $this->logger->info( - 'Processing webhook retries', - [ - 'count' => count($failedLogs), - ] - ); - - foreach ($failedLogs as $log) { - try { - // Get webhook. - $webhook = $this->webhookMapper->find($log->getWebhookId()); - - // Check if webhook is still enabled. - if ($webhook->getEnabled() === false) { - $this->logger->debug( - 'Skipping retry for disabled webhook', - [ - 'webhook_id' => $webhook->getId(), - 'log_id' => $log->getId(), - ] - ); - continue; - } - - // Check if we've exceeded max retries. - if ($log->getAttempt() >= $webhook->getMaxRetries()) { - $this->logger->warning( - 'Webhook retry limit exceeded', - [ - 'webhook_id' => $webhook->getId(), - 'log_id' => $log->getId(), - 'attempt' => $log->getAttempt(), - 'max_retries' => $webhook->getMaxRetries(), - ] - ); - continue; - } - - // Retry webhook delivery. - $this->logger->info( - 'Retrying webhook delivery', - [ - 'webhook_id' => $webhook->getId(), - 'log_id' => $log->getId(), - 'attempt' => $log->getAttempt() + 1, - ] - ); - - $success = $this->webhookService->deliverWebhook( - webhook: $webhook, - eventName: $log->getEventClass(), - payload: $log->getPayloadArray(), - attempt: $log->getAttempt() + 1 - ); - - if ($success === true) { - $this->logger->info( - 'Webhook retry succeeded', - [ - 'webhook_id' => $webhook->getId(), - 'log_id' => $log->getId(), - ] - ); - } else { - $this->logger->warning( - 'Webhook retry failed', - [ - 'webhook_id' => $webhook->getId(), - 'log_id' => $log->getId(), - 'attempt' => $log->getAttempt() + 1, - ] - ); - } - } catch (\Exception $e) { - $this->logger->error( - 'Error processing webhook retry', - [ - 'log_id' => $log->getId(), - 'error' => $e->getMessage(), - ] - ); - }//end try - }//end foreach - }//end run() -}//end class diff --git a/lib/Dashboard/IntegrationDashboardWidget.php b/lib/Dashboard/IntegrationDashboardWidget.php index 5cd4b6bdcb..13d4ed17fc 100644 --- a/lib/Dashboard/IntegrationDashboardWidget.php +++ b/lib/Dashboard/IntegrationDashboardWidget.php @@ -28,11 +28,11 @@ namespace OCA\OpenRegister\Dashboard; use OCA\OpenRegister\AppInfo\Application; +use OCA\OpenRegister\Service\ScriptManifestLoader; use OCP\Dashboard\IIconWidget; use OCP\Dashboard\IWidget; use OCP\IL10N; use OCP\IURLGenerator; -use OCP\Util; /** * Umbrella dashboard widget hosting all registered integration widgets @@ -140,13 +140,13 @@ public function getUrl(): ?string * * @return void * - * @SuppressWarnings(PHPMD.StaticAccess) \OCP\Util::addScript() is the - * canonical Nextcloud API for enqueuing scripts from an IWidget::load() - * implementation; no injectable alternative (IUtil, etc.) exists in the - * NC AppFramework. + * @SuppressWarnings(PHPMD.StaticAccess) ScriptManifestLoader wraps the + * canonical \OCP\Util::addScript() API for enqueuing scripts from an + * IWidget::load() implementation; no injectable alternative (IUtil, etc.) + * exists in the NC AppFramework. */ public function load(): void { - Util::addScript(Application::APP_ID, 'openregister-user-dashboard'); + ScriptManifestLoader::addEntryScripts(Application::APP_ID, 'userDashboard', 'openregister-user-dashboard'); }//end load() }//end class diff --git a/lib/Db/AnalyticsSeries.php b/lib/Db/AnalyticsSeries.php new file mode 100644 index 0000000000..27f560c1b4 --- /dev/null +++ b/lib/Db/AnalyticsSeries.php @@ -0,0 +1,232 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class AnalyticsSeries + * + * @method string|null getSeriesKey() + * @method void setSeriesKey(?string $seriesKey) + * @method int|null getRegisterId() + * @method void setRegisterId(?int $registerId) + * @method int|null getSchemaId() + * @method void setSchemaId(?int $schemaId) + * @method string|null getTitle() + * @method void setTitle(?string $title) + * @method string|null getChartType() + * @method void setChartType(?string $chartType) + * @method string|null getLabels() + * @method void setLabels(?string $labels) + * @method string|null getDatasets() + * @method void setDatasets(?string $datasets) + * @method string|null getVisibility() + * @method void setVisibility(?string $visibility) + * @method string|null getCreatedBy() + * @method void setCreatedBy(?string $createdBy) + * @method DateTime|null getCreatedAt() + * @method void setCreatedAt(?DateTime $createdAt) + * @method DateTime|null getUpdatedAt() + * @method void setUpdatedAt(?DateTime $updatedAt) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class AnalyticsSeries extends Entity implements JsonSerializable +{ + + /** + * Stable series key (unique per instance). + * + * @var string|null + */ + protected ?string $seriesKey = null; + + /** + * Optional register scope. + * + * @var integer|null + */ + protected ?int $registerId = null; + + /** + * Optional schema scope. + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * Display title for the chart widget. + * + * @var string|null + */ + protected ?string $title = null; + + /** + * Chart type hint (line / bar / pie / doughnut / …). Render layer + * decides how to draw it. + * + * @var string|null + */ + protected ?string $chartType = null; + + /** + * JSON-encoded array of x-axis labels. + * + * @var string|null + */ + protected ?string $labels = null; + + /** + * JSON-encoded array of datasets (each `{label, data, …}`). + * + * @var string|null + */ + protected ?string $datasets = null; + + /** + * Visibility scope: 'private' (creator), 'group' or 'public'. + * Governs who may fetch the series through the render surface. + * + * @var string|null + */ + protected ?string $visibility = null; + + /** + * The uid that registered the series. + * + * @var string|null + */ + protected ?string $createdBy = null; + + /** + * Creation timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $createdAt = null; + + /** + * Last-update timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $updatedAt = null; + + /** + * Constructor. + */ + public function __construct() + { + $this->addType(fieldName: 'seriesKey', type: 'string'); + $this->addType(fieldName: 'registerId', type: 'integer'); + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'title', type: 'string'); + $this->addType(fieldName: 'chartType', type: 'string'); + $this->addType(fieldName: 'labels', type: 'string'); + $this->addType(fieldName: 'datasets', type: 'string'); + $this->addType(fieldName: 'visibility', type: 'string'); + $this->addType(fieldName: 'createdBy', type: 'string'); + $this->addType(fieldName: 'createdAt', type: 'datetime'); + $this->addType(fieldName: 'updatedAt', type: 'datetime'); + }//end __construct() + + /** + * Decode the stored labels JSON into an array. + * + * @return array The labels, or [] when unset / invalid. + */ + public function getLabelsArray(): array + { + if ($this->labels === null || $this->labels === '') { + return []; + } + + $decoded = json_decode($this->labels, true); + if (is_array($decoded) === true) { + return $decoded; + } + + return []; + }//end getLabelsArray() + + /** + * Decode the stored datasets JSON into an array. + * + * @return array The datasets, or [] when unset / invalid. + */ + public function getDatasetsArray(): array + { + if ($this->datasets === null || $this->datasets === '') { + return []; + } + + $decoded = json_decode($this->datasets, true); + if (is_array($decoded) === true) { + return $decoded; + } + + return []; + }//end getDatasetsArray() + + /** + * JSON serialization — returns the full chart-ready render contract + * (labels + datasets decoded back to arrays). + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'seriesKey' => $this->seriesKey, + 'registerId' => $this->registerId, + 'schemaId' => $this->schemaId, + 'title' => $this->title, + 'chartType' => $this->chartType, + 'labels' => $this->getLabelsArray(), + 'datasets' => $this->getDatasetsArray(), + 'visibility' => $this->visibility, + 'createdBy' => $this->createdBy, + 'createdAt' => $this->createdAt?->format(DateTime::ATOM), + 'updatedAt' => $this->updatedAt?->format(DateTime::ATOM), + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/AnalyticsSeriesMapper.php b/lib/Db/AnalyticsSeriesMapper.php new file mode 100644 index 0000000000..b78bf0ddbf --- /dev/null +++ b/lib/Db/AnalyticsSeriesMapper.php @@ -0,0 +1,93 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * @link https://www.OpenRegister.nl + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class AnalyticsSeriesMapper + * + * @template-extends QBMapper + */ +class AnalyticsSeriesMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_analytics_series', entityClass: AnalyticsSeries::class); + }//end __construct() + + /** + * Find a series by its stable key. + * + * @param string $seriesKey The series key. + * + * @return AnalyticsSeries|null The series, or null when unknown. + */ + public function findByKey(string $seriesKey): ?AnalyticsSeries + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('series_key', $qb->createNamedParameter($seriesKey))); + + try { + return $this->findEntity(query: $qb); + } catch (DoesNotExistException $e) { + return null; + } + }//end findByKey() + + /** + * Find all series scoped to a register (and optionally a schema). + * + * @param int $registerId The register id. + * @param int|null $schemaId Optional schema id filter. + * + * @return AnalyticsSeries[] Matching series. + */ + public function findByScope(int $registerId, ?int $schemaId=null): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('register_id', $qb->createNamedParameter($registerId, IQueryBuilder::PARAM_INT))); + + if ($schemaId !== null) { + $qb->andWhere($qb->expr()->eq('schema_id', $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT))); + } + + $qb->orderBy('updated_at', 'DESC'); + + return $this->findEntities(query: $qb); + }//end findByScope() +}//end class diff --git a/lib/Db/AnonymisationLog.php b/lib/Db/AnonymisationLog.php new file mode 100644 index 0000000000..3026b6edf2 --- /dev/null +++ b/lib/Db/AnonymisationLog.php @@ -0,0 +1,239 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * AnonymisationLog entity for tracking anonymisation runs against NC Files. + * + * @method int getFileId() + * @method void setFileId(int $fileId) + * @method string|null getObjectUuid() + * @method void setObjectUuid(?string $objectUuid) + * @method int|null getRegisterId() + * @method void setRegisterId(?int $registerId) + * @method int|null getSchemaId() + * @method void setSchemaId(?int $schemaId) + * @method string getMimeType() + * @method void setMimeType(string $mimeType) + * @method string getEngine() + * @method void setEngine(string $engine) + * @method string getStatus() + * @method void setStatus(string $status) + * @method string|null getReason() + * @method void setReason(?string $reason) + * @method string|null getSanitization() + * @method void setSanitization(?string $sanitization) + * @method int getReplacements() + * @method void setReplacements(int $replacements) + * @method int|null getDurationMs() + * @method void setDurationMs(?int $durationMs) + * @method DateTime getCreated() + * @method void setCreated(DateTime $created) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class AnonymisationLog extends Entity implements JsonSerializable +{ + + /** + * Status: anonymisation completed successfully. + * + * @var string + */ + public const STATUS_SUCCESS = 'success'; + + /** + * Status: anonymisation failed (see `reason`). + * + * @var string + */ + public const STATUS_FAILURE = 'failure'; + + /** + * The Nextcloud file id of the source file. + * + * @var integer + */ + protected int $fileId = 0; + + /** + * The originating register object UUID (when applicable). + * + * @var string|null + */ + protected ?string $objectUuid = null; + + /** + * The register id (when applicable). + * + * @var integer|null + */ + protected ?int $registerId = null; + + /** + * The schema id (when applicable). + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * The observed input MIME type. + * + * @var string + */ + protected string $mimeType = ''; + + /** + * The engine / anonymiser class name. + * + * @var string + */ + protected string $engine = ''; + + /** + * Outcome status (success | failure). + * + * @var string + */ + protected string $status = self::STATUS_SUCCESS; + + /** + * Structured failure-reason code (e.g. `encrypted`, `validation_failed`). + * + * @var string|null + */ + protected ?string $reason = null; + + /** + * JSON-serialised SanitizationReport (null for non-Office runs). + * + * @var string|null + */ + protected ?string $sanitization = null; + + /** + * Distinct replacements applied during the run. + * + * @var integer + */ + protected int $replacements = 0; + + /** + * Wall-clock duration of the run in milliseconds. + * + * @var integer|null + */ + protected ?int $durationMs = null; + + /** + * Created timestamp. + * + * @var DateTime + */ + protected DateTime $created; + + /** + * Constructor. + * + * @return void + */ + public function __construct() + { + $this->addType(fieldName: 'fileId', type: 'integer'); + $this->addType(fieldName: 'objectUuid', type: 'string'); + $this->addType(fieldName: 'registerId', type: 'integer'); + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'mimeType', type: 'string'); + $this->addType(fieldName: 'engine', type: 'string'); + $this->addType(fieldName: 'status', type: 'string'); + $this->addType(fieldName: 'reason', type: 'string'); + $this->addType(fieldName: 'sanitization', type: 'string'); + $this->addType(fieldName: 'replacements', type: 'integer'); + $this->addType(fieldName: 'durationMs', type: 'integer'); + $this->addType(fieldName: 'created', type: 'datetime'); + + $this->created = new DateTime(); + }//end __construct() + + /** + * Decode the sanitisation JSON payload. + * + * @return array|null The decoded report or null when absent. + */ + public function getSanitizationArray(): ?array + { + if ($this->sanitization === null) { + return null; + } + + $decoded = json_decode($this->sanitization, true); + if (is_array($decoded) === false) { + return null; + } + + return $decoded; + }//end getSanitizationArray() + + /** + * JSON serialize the entity. + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'fileId' => $this->fileId, + 'objectUuid' => $this->objectUuid, + 'registerId' => $this->registerId, + 'schemaId' => $this->schemaId, + 'mimeType' => $this->mimeType, + 'engine' => $this->engine, + 'status' => $this->status, + 'reason' => $this->reason, + 'sanitization' => $this->getSanitizationArray(), + 'replacements' => $this->replacements, + 'durationMs' => $this->durationMs, + 'created' => $this->created->format('c'), + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/AnonymisationLogMapper.php b/lib/Db/AnonymisationLogMapper.php new file mode 100644 index 0000000000..2d5dfb0edc --- /dev/null +++ b/lib/Db/AnonymisationLogMapper.php @@ -0,0 +1,186 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * AnonymisationLogMapper handles database operations for AnonymisationLog entities. + * + * @method AnonymisationLog insert(Entity $entity) + * @method AnonymisationLog update(Entity $entity) + * @method AnonymisationLog delete(Entity $entity) + * @method AnonymisationLog find(int $id) + * @method AnonymisationLog findEntity(IQueryBuilder $query) + * @method list findEntities(IQueryBuilder $query) + * + * @template-extends QBMapper + */ +class AnonymisationLogMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection + * + * @return void + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_anonymisation_log', entityClass: AnonymisationLog::class); + }//end __construct() + + /** + * Find a log row by id. + * + * @param int $id Log entry id + * + * @return AnonymisationLog + * + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + */ + public function find(int $id): AnonymisationLog + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity(query: $qb); + }//end find() + + /** + * Find log rows by Nextcloud file id. + * + * @param int $fileId The NC file id + * @param int|null $limit Result limit + * @param int|null $offset Result offset + * + * @return AnonymisationLog[] + * + * @psalm-return list + */ + public function findByFileId(int $fileId, ?int $limit=null, ?int $offset=null): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->orderBy('created', 'DESC'); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities(query: $qb); + }//end findByFileId() + + /** + * Find log rows by source object UUID. + * + * @param string $objectUuid Object UUID + * @param int|null $limit Result limit + * @param int|null $offset Result offset + * + * @return AnonymisationLog[] + * + * @psalm-return list + */ + public function findByObjectUuid(string $objectUuid, ?int $limit=null, ?int $offset=null): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid))) + ->orderBy('created', 'DESC'); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities(query: $qb); + }//end findByObjectUuid() + + /** + * Get the most recent successful anonymisation log row for a file. + * + * @param int $fileId The NC file id + * + * @return AnonymisationLog|null The latest success row or null when absent. + */ + public function findLatestSuccessForFile(int $fileId): ?AnonymisationLog + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('status', $qb->createNamedParameter(AnonymisationLog::STATUS_SUCCESS))) + ->orderBy('created', 'DESC') + ->setMaxResults(1); + + try { + return $this->findEntity(query: $qb); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return null; + } + }//end findLatestSuccessForFile() + + /** + * Insert a new anonymisation log row. + * + * @param Entity $entity The AnonymisationLog entity + * + * @return AnonymisationLog + * + * @psalm-suppress PossiblyUnusedReturnValue + */ + public function insert(Entity $entity): Entity + { + if ($entity instanceof AnonymisationLog) { + $entity->setCreated(new DateTime()); + } + + return parent::insert(entity: $entity); + }//end insert() +}//end class diff --git a/lib/Db/ApprovalChainMapper.php b/lib/Db/ApprovalChainMapper.php index ae305187ae..3114a3df5d 100644 --- a/lib/Db/ApprovalChainMapper.php +++ b/lib/Db/ApprovalChainMapper.php @@ -118,6 +118,43 @@ public function findBySchema(int $schemaId): array return $this->findEntities(query: $qb); }//end findBySchema() + /** + * Find the chain provisioned for a schema under a given declarative chain + * key (the `x-openregister-approval-chains` entry name, stored as `name`). + * + * @param int $schemaId Schema ID. + * @param string $name Chain key / name. + * + * @return ApprovalChain|null The matching chain, or null when none exists yet. + * + * @spec openspec/specs/approval-workflow/spec.md + */ + public function findBySchemaAndName(int $schemaId, string $name): ?ApprovalChain + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq( + 'schema_id', + $qb->createNamedParameter(value: $schemaId, type: IQueryBuilder::PARAM_INT) + ) + ) + ->andWhere( + $qb->expr()->eq('name', $qb->createNamedParameter($name)) + ) + ->setMaxResults(1); + + try { + return $this->findEntity(query: $qb); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return null; + } catch (\Throwable $e) { + return null; + } + }//end findBySchemaAndName() + /** * Create an approval chain from an array. * diff --git a/lib/Db/ApprovalStep.php b/lib/Db/ApprovalStep.php index 8571cce802..fdad650286 100644 --- a/lib/Db/ApprovalStep.php +++ b/lib/Db/ApprovalStep.php @@ -47,6 +47,8 @@ * @method void setDecidedAt(?DateTime $decidedAt) * @method DateTime|null getCreated() * @method void setCreated(?DateTime $created) + * @method string|null getRequesterId() + * @method void setRequesterId(?string $requesterId) * * @psalm-suppress PropertyNotSetInConstructor */ @@ -123,6 +125,17 @@ class ApprovalStep extends Entity implements JsonSerializable */ protected ?DateTime $created = null; + /** + * Uid of the user whose attempted transition provisioned this step, when the + * step was created via the declarative approval-chains gate. `null` for steps + * created through the pure `POST /api/approval-chains` CRUD flow. Used by + * `ApprovalService::resolveSeparationOfDuties()` to reject a decision made by + * the same user who triggered the gate. + * + * @var string|null + */ + protected ?string $requesterId = null; + /** * Constructor for ApprovalStep entity. */ @@ -138,6 +151,7 @@ public function __construct() $this->addType(fieldName: 'comment', type: 'string'); $this->addType(fieldName: 'decidedAt', type: 'datetime'); $this->addType(fieldName: 'created', type: 'datetime'); + $this->addType(fieldName: 'requesterId', type: 'string'); }//end __construct() /** @@ -160,6 +174,7 @@ public function hydrate(array $object): self 'comment', 'decidedAt', 'created', + 'requesterId', ]; foreach ($object as $key => $value) { @@ -180,17 +195,18 @@ public function hydrate(array $object): self public function jsonSerialize(): array { return [ - 'id' => $this->id, - 'uuid' => $this->uuid, - 'chainId' => $this->chainId, - 'objectUuid' => $this->objectUuid, - 'stepOrder' => $this->stepOrder, - 'role' => $this->role, - 'status' => $this->status, - 'decidedBy' => $this->decidedBy, - 'comment' => $this->comment, - 'decidedAt' => $this->decidedAt?->format('c'), - 'created' => $this->created?->format('c'), + 'id' => $this->id, + 'uuid' => $this->uuid, + 'chainId' => $this->chainId, + 'objectUuid' => $this->objectUuid, + 'stepOrder' => $this->stepOrder, + 'role' => $this->role, + 'status' => $this->status, + 'decidedBy' => $this->decidedBy, + 'comment' => $this->comment, + 'decidedAt' => $this->decidedAt?->format('c'), + 'created' => $this->created?->format('c'), + 'requesterId' => $this->requesterId, ]; }//end jsonSerialize() }//end class diff --git a/lib/Db/ApprovalStepMapper.php b/lib/Db/ApprovalStepMapper.php index 98303507de..d397a41b3e 100644 --- a/lib/Db/ApprovalStepMapper.php +++ b/lib/Db/ApprovalStepMapper.php @@ -149,7 +149,7 @@ public function findByObjectUuid(string $objectUuid): array * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-b3a-workflow-seed/tasks.md#task-1 + * @spec openspec/specs/approval-workflow/spec.md */ public function findAllFiltered(array $filters=[], ?int $limit=null, ?int $offset=null): array { @@ -238,4 +238,41 @@ public function createFromArray(array $data): ApprovalStep return $this->insert(entity: $step); }//end createFromArray() + + /** + * Delete every step for a chain and object combination. + * + * Used by `ApprovalChainGateListener` to clear a `rejected` cycle before + * provisioning a fresh one on the next gated-transition attempt + * (resubmission). `workflow_executions` (`WorkflowExecutionMapper`) remains + * the durable audit trail for the cleared cycle's decisions — `ApprovalStep` + * rows are working state, not audit log. + * + * @param int $chainId Chain ID + * @param string $objectUuid Object UUID + * + * @return int Number of rows deleted (0 on error). + * + * @spec openspec/specs/approval-workflow/spec.md + */ + public function deleteByChainAndObject(int $chainId, string $objectUuid): int + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where( + $qb->expr()->eq( + 'chain_id', + $qb->createNamedParameter(value: $chainId, type: IQueryBuilder::PARAM_INT) + ) + ) + ->andWhere( + $qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid)) + ); + + return (int) $qb->executeStatement(); + } catch (\Throwable $e) { + return 0; + } + }//end deleteByChainAndObject() }//end class diff --git a/lib/Db/AuditTrail.php b/lib/Db/AuditTrail.php index a99193fe15..8aaf5c4e10 100644 --- a/lib/Db/AuditTrail.php +++ b/lib/Db/AuditTrail.php @@ -58,6 +58,8 @@ * @method void setUser(?string $user) * @method string|null getUserName() * @method void setUserName(?string $userName) + * @method string|null getSession() + * @method void setSession(?string $session) * @method DateTime|null getCreated() * @method void setCreated(?DateTime $created) * @method string|null getOrganisation() @@ -68,6 +70,12 @@ * @method void setHash(?string $hash) * @method string|null getPreviousHash() * @method void setPreviousHash(?string $previousHash) + * @method string|null getToolId() + * @method void setToolId(?string $toolId) + * @method string|null getParamsDigest() + * @method void setParamsDigest(?string $paramsDigest) + * @method array|null getResultSummary() + * @method void setResultSummary(?array $resultSummary) * * @psalm-suppress PossiblyUnusedMethod * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class @@ -291,6 +299,43 @@ class AuditTrail extends Entity implements JsonSerializable */ protected ?string $importJobId = null; + /** + * The full namespaced MCP tool id this row records an invocation of + * (e.g. `pipelinq.lead.search`), when this row is an MCP tool-invocation + * audit entry (ADR-063 chain 2/3). Null on ordinary object-CRUD rows. + * + * @var string|null Namespaced MCP tool id. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-006 — Every invocation is audited) + */ + protected ?string $toolId = null; + + /** + * A digest (never the raw values) of the MCP tool invocation's + * arguments — a SHA-256 hex digest of their canonical JSON form. + * Deliberately excludes raw argument values so this immutable row never + * persists PII (ADR-063 chain 2/3, EU AI Act art.12/14). + * + * @var string|null SHA-256 hex digest of the invocation arguments. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-006 — A params digest, not raw params, is stored) + */ + protected ?string $paramsDigest = null; + + /** + * A small structured summary of an MCP tool invocation's outcome + * (e.g. object count / affected ids / `isError` + error class). Null on + * ordinary object-CRUD rows. + * + * @var array|null Result summary of an MCP tool invocation. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-006 — Every invocation is audited) + */ + protected ?array $resultSummary = null; + /** * Constructor for the AuditTrail class * @@ -326,6 +371,9 @@ public function __construct() $this->addType(fieldName: 'hash', type: 'string'); $this->addType(fieldName: 'previousHash', type: 'string'); $this->addType(fieldName: 'importJobId', type: 'string'); + $this->addType(fieldName: 'toolId', type: 'string'); + $this->addType(fieldName: 'paramsDigest', type: 'string'); + $this->addType(fieldName: 'resultSummary', type: 'json'); }//end __construct() /** @@ -424,7 +472,10 @@ public function hydrate(array $object): static * size: int|null, * expires: null|string, * hash: null|string, - * previousHash: null|string + * previousHash: null|string, + * toolId: null|string, + * paramsDigest: null|string, + * resultSummary: array|null * } */ public function jsonSerialize(): array @@ -468,6 +519,9 @@ public function jsonSerialize(): array 'expires' => $expires, 'hash' => $this->hash, 'previousHash' => $this->previousHash, + 'toolId' => $this->toolId, + 'paramsDigest' => $this->paramsDigest, + 'resultSummary' => $this->resultSummary, ]; }//end jsonSerialize() diff --git a/lib/Db/AuditTrailMapper.php b/lib/Db/AuditTrailMapper.php index 56012b89e5..c3b6adb92d 100644 --- a/lib/Db/AuditTrailMapper.php +++ b/lib/Db/AuditTrailMapper.php @@ -103,6 +103,43 @@ public function __construct( parent::__construct(db: $db, tableName: 'openregister_audit_trails', entityClass: AuditTrail::class); }//end __construct() + /** + * Insert an audit-trail entry sealed into the SHA-256 hash chain. + * + * Captures the current chain head as `previousHash`, inserts to obtain the + * row id, then computes `hash` over the id-populated entity — matching + * exactly how {@see AuditHashService::verifyChain()} re-derives it — and + * persists it. Fail-soft: the audit row is always inserted; a hashing/DB + * hiccup logs and leaves that row unhashed rather than losing the audit + * record. Without this the chain was never populated, so `verifyChain()` + * reported `valid:true` with `entriesVerified:0` — tamper-evidence in name + * only (fix-audit-hash-chain-write-path / ADR-003). + * + * @param AuditTrail $auditTrail The entry to persist. + * + * @return AuditTrail The persisted (and, on success, hash-chained) entry. + */ + private function insertHashChained(AuditTrail $auditTrail): AuditTrail + { + $auditTrail = $this->insert(entity: $auditTrail); + + // Seal the persisted row into the hash chain. AuditHashService computes + // the hash over the row using the SAME row->entity->canonical path as + // verifyChain(), so the stored hash re-verifies exactly. Fail-soft: a + // hashing/DB hiccup logs and leaves the row unhashed rather than losing + // the audit record. + try { + $hashService = $this->container->get(\OCA\OpenRegister\Service\AuditHashService::class); + $hashService->sealRow(id: (int) $auditTrail->getId()); + } catch (\Throwable $e) { + $this->logger->warning( + '[AuditTrailMapper] hash-chain seal failed for id '.$auditTrail->getId().': '.$e->getMessage() + ); + } + + return $auditTrail; + }//end insertHashChained() + /** * Set the request-scoped import-job UUID stamped on all * `createAuditTrail()` rows for the duration of an import. Pass @@ -327,9 +364,14 @@ function ($key) { /** * Creates an audit trail for object changes * - * @param ObjectEntity|null $old The old state of the object - * @param ObjectEntity|null $new The new state of the object - * @param string|null $action The action to create the audit trail for + * @param ObjectEntity|null $old The old state of the object + * @param ObjectEntity|null $new The new state of the object + * @param string|null $action The action to create the audit trail for + * @param array|null $cascadeContext Optional referential-integrity cascade context. When + * non-null, the context is folded into the `changed` + * column of the initial INSERT (previously this required + * a second UPDATE per row, which also invalidated the + * hash-chain seal computed at insert time). * * @return AuditTrail The created audit trail * @@ -338,8 +380,45 @@ function ($key) { * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function createAuditTrail(?ObjectEntity $old=null, ?ObjectEntity $new=null, ?string $action='update'): AuditTrail - { + public function createAuditTrail( + ?ObjectEntity $old=null, + ?ObjectEntity $new=null, + ?string $action='update', + ?array $cascadeContext=null + ): AuditTrail { + $auditTrail = $this->buildAuditTrail(old: $old, new: $new, action: $action, cascadeContext: $cascadeContext); + + // Insert the new AuditTrail, sealed into the hash chain, and return it. + return $this->insertHashChained(auditTrail: $auditTrail); + }//end createAuditTrail() + + /** + * Build (but do not persist) an audit trail entity for object changes. + * + * Contains the complete row-building logic shared by {@see createAuditTrail()} + * (single insert) and {@see insertAuditTrails()} (batched multi-row insert), + * so both paths produce identical audit rows. + * + * @param ObjectEntity|null $old The old state of the object + * @param ObjectEntity|null $new The new state of the object + * @param string|null $action The action to create the audit trail for + * @param array|null $cascadeContext Optional referential-integrity cascade context to fold + * into the `changed` column (keys: triggerObject, + * triggerSchema, action_type, property). + * + * @return AuditTrail The populated (unpersisted) audit trail entity + * + * @SuppressWarnings(PHPMD.StaticAccess) Uuid::v4 is standard Symfony UID pattern + * @SuppressWarnings(PHPMD.NPathComplexity) Audit trail creation requires handling many optional fields + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function buildAuditTrail( + ?ObjectEntity $old=null, + ?ObjectEntity $new=null, + ?string $action='update', + ?array $cascadeContext=null + ): AuditTrail { // Determine the action based on the presence of old and new objects. $objectEntity = $new; if ($new === null && $action === 'update') { @@ -400,6 +479,19 @@ public function createAuditTrail(?ObjectEntity $old=null, ?ObjectEntity $new=nul } }//end if + // Fold referential-integrity cascade context into the change set so it is + // part of the initial INSERT. Key shape matches the (removed) post-insert + // UPDATE that DeleteObject used to issue per cascade-tagged delete. + if ($cascadeContext !== null) { + $changed['triggeredBy'] = 'referential_integrity'; + $changed['cascadeContext'] = [ + 'triggerObject' => $cascadeContext['triggerObject'] ?? null, + 'triggerSchema' => $cascadeContext['triggerSchema'] ?? null, + 'action_type' => $cascadeContext['action_type'] ?? 'referential_integrity.cascade_delete', + 'property' => $cascadeContext['property'] ?? null, + ]; + } + // Get the current user. $user = $this->userSession->getUser(); @@ -453,9 +545,206 @@ public function createAuditTrail(?ObjectEntity $old=null, ?ObjectEntity $new=nul // Set default expiration date (30 days from now). $auditTrail->setExpires(new DateTime('+30 days')); - // Insert the new AuditTrail into the database and return it. - return $this->insert(entity: $auditTrail); - }//end createAuditTrail() + return $auditTrail; + }//end buildAuditTrail() + + /** + * Create audit trail rows for many object changes with batched inserts. + * + * Each entry is an array with keys `old` (?ObjectEntity), `new` (?ObjectEntity) + * and `action` (string). Rows are built through the exact same + * {@see buildAuditTrail()} logic as {@see createAuditTrail()} so their content + * is identical, but they are persisted with one multi-row INSERT per chunk of + * 100 instead of one INSERT (plus 3 hash-chain queries) per row. After each + * chunk the rows are sealed into the SHA-256 hash chain in a single batched + * pass ({@see AuditHashService::sealRows()}). Sealing stays fail-soft exactly + * like the single-row path: a sealing hiccup logs and leaves rows unhashed + * rather than losing the audit records. + * + * @param array $entries Mixed list: ['old' => ?ObjectEntity, 'new' => ?ObjectEntity, 'action' => string, + * 'cascadeContext' => ?array] entries built through {@see buildAuditTrail()}, and/or + * pre-built AuditTrail entities (each must carry a uuid) persisted as-is + * @param int $chunkSize Number of rows per multi-row INSERT statement + * + * @return AuditTrail[] The persisted audit trail entities (ids populated) + * + * @throws \OCP\DB\Exception If a database error occurs during insert + * + * @psalm-return list + * + * @spec openspec/specs/audit-hash-chain/spec.md + */ + public function insertAuditTrails(array $entries, int $chunkSize=100): array + { + if ($entries === []) { + return []; + } + + $auditTrails = []; + foreach ($entries as $entry) { + // Pre-built rows (e.g. cascade deletes assembled via buildAuditTrail() + // with cascade context) are persisted as-is; array entries are built + // through the shared row logic so both shapes yield identical rows. + if ($entry instanceof AuditTrail) { + if ($entry->getUuid() === null || $entry->getUuid() === '') { + throw new \InvalidArgumentException('insertAuditTrails() requires every pre-built row to carry a uuid.'); + } + + $auditTrails[] = $entry; + continue; + } + + if (is_array($entry) === false) { + throw new \InvalidArgumentException('insertAuditTrails() expects AuditTrail entities or old/new/action entry arrays.'); + } + + $auditTrails[] = $this->buildAuditTrail( + old: ($entry['old'] ?? null), + new: ($entry['new'] ?? null), + action: ($entry['action'] ?? 'update'), + cascadeContext: ($entry['cascadeContext'] ?? null) + ); + } + + foreach (array_chunk($auditTrails, max(1, $chunkSize)) as $chunk) { + $this->insertAuditTrailChunk(chunk: $chunk); + } + + return $auditTrails; + }//end insertAuditTrails() + + /** + * Persist one chunk of audit trail entities with a single multi-row INSERT. + * + * Values are converted exactly as QBMapper::insert() would convert them + * (json fields json_encode'd, datetime fields formatted as `Y-m-d H:i:s`), + * so the resulting rows are byte-identical to single-row inserts. After the + * insert the generated ids are read back (by row uuid) and the chunk is + * sealed into the hash chain in one batched pass. + * + * @param AuditTrail[] $chunk The audit trail entities to persist + * + * @return void + * + * @throws \OCP\DB\Exception If a database error occurs during insert + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Column-union, value binding, id readback and sealing in one pass + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function insertAuditTrailChunk(array $chunk): void + { + if ($chunk === []) { + return; + } + + // Union of updated properties across the chunk (rows built by + // buildAuditTrail() set the same fields, but optional fields like + // processingActivityId/importJobId can differ per row). + $properties = []; + foreach ($chunk as $auditTrail) { + foreach (array_keys($auditTrail->getUpdatedFields()) as $property) { + if ($property !== 'id' && in_array($property, $properties, true) === false) { + $properties[] = $property; + } + } + } + + $fieldTypes = $chunk[0]->getFieldTypes(); + $columns = []; + foreach ($properties as $property) { + $columns[] = $chunk[0]->propertyToColumn($property); + } + + $qb = $this->db->getQueryBuilder(); + $tableName = $qb->getTableName($this->getTableName()); + + $valuesClauses = []; + $parameters = []; + foreach ($chunk as $auditTrail) { + $placeholders = []; + foreach ($properties as $property) { + $getter = 'get'.ucfirst($property); + $parameters[] = $this->toDatabaseValue( + value: $auditTrail->$getter(), + type: ($fieldTypes[$property] ?? 'string') + ); + $placeholders[] = '?'; + } + + $valuesClauses[] = '('.implode(',', $placeholders).')'; + } + + $sql = 'INSERT INTO '.$tableName + .' ('.implode(', ', $columns).') VALUES ' + .implode(', ', $valuesClauses); + + $this->db->executeStatement($sql, $parameters); + + // Read back the generated ids (keyed by the uuid we generated per row). + $uuidToEntity = []; + foreach ($chunk as $auditTrail) { + $uuidToEntity[$auditTrail->getUuid()] = $auditTrail; + } + + $idPlaceholders = implode(',', array_fill(0, count($uuidToEntity), '?')); + $idSql = 'SELECT id, uuid FROM '.$tableName.' WHERE uuid IN ('.$idPlaceholders.')'; + + $result = $this->db->executeQuery($idSql, array_keys($uuidToEntity)); + $ids = []; + while (($row = $result->fetch()) !== false) { + $ids[] = (int) $row['id']; + if (isset($uuidToEntity[$row['uuid']]) === true) { + $uuidToEntity[$row['uuid']]->setId((int) $row['id']); + } + } + + $result->closeCursor(); + + // Seal the persisted rows into the hash chain in one batched pass. + // Fail-soft, matching insertHashChained(): the audit rows are already + // inserted; a sealing hiccup logs and leaves them unhashed. + try { + $hashService = $this->container->get(\OCA\OpenRegister\Service\AuditHashService::class); + $hashService->sealRows(ids: $ids); + } catch (\Throwable $e) { + $this->logger->warning( + '[AuditTrailMapper] batched hash-chain seal failed for '.count($ids).' rows: '.$e->getMessage() + ); + } + }//end insertAuditTrailChunk() + + /** + * Convert an entity property value to its database representation. + * + * Mirrors the conversions QBMapper::insert() applies through parameter + * types: json fields are json_encode'd and datetime fields are formatted + * with the platform datetime format (`Y-m-d H:i:s`). + * + * @param mixed $value The property value + * @param string $type The declared entity field type + * + * @return mixed The database-ready value + */ + private function toDatabaseValue(mixed $value, string $type): mixed + { + if ($value === null) { + return null; + } + + if ($type === 'json') { + return json_encode($value); + } + + if ($type === 'datetime' && $value instanceof \DateTimeInterface) { + return $value->format('Y-m-d H:i:s'); + } + + if ($type === 'boolean' || $type === 'bool') { + return (int) $value; + } + + return $value; + }//end toDatabaseValue() /** * Resolve the AVG / GDPR Art 30 processing-activity uuid for this @@ -1487,6 +1776,7 @@ public function createAuditTrailEntry( $auditTrail = new AuditTrail(); $auditTrail->setUuid(\Symfony\Component\Uid\Uuid::v4()->toRfc4122()); + $auditTrail->setObject($object->getId()); $auditTrail->setObjectUuid($object->getUuid()); $auditTrail->setRegister($object->getRegister()); $auditTrail->setSchema($object->getSchema()); @@ -1509,9 +1799,81 @@ public function createAuditTrailEntry( $auditTrail->setCreated(new DateTime()); - return $this->insert(entity: $auditTrail); + return $this->insertHashChained(auditTrail: $auditTrail); }//end createAuditTrailEntry() + /** + * Create one immutable, hash-chained audit record for an MCP tool + * invocation (ADR-063 chain 2/3, EU AI Act art.12/14). + * + * Reuses the existing `AuditTrail` table and `AuditHashService` hash + * chain unchanged — `action` is namespaced `mcp.{verb}` (e.g. + * `mcp.search`) so it never collides with the plain `create` / `update` + * / `delete` / `read` object-CRUD actions that statistics code buckets + * on. Acting identity is resolved from the ambient Nextcloud session, + * exactly like {@see createAuditTrail()} — the IMcpToolProvider ABI does + * not thread an acting-agent identity into `invokeTool()`, so a + * non-human agent principal cannot be distinguished from the session's + * human user yet (tracked as a DEFERRED_QUESTION coordinated with + * Hermiq's `agent-tool-governance-and-disclosure` change). + * + * @param string $toolId Full namespaced tool id, e.g. `pipelinq.lead.search`. + * @param string $paramsDigest SHA-256 hex digest of the invocation's canonical arguments (never raw values). + * @param array $resultSummary Structured outcome summary (count/ids/isError/errorClass). + * @param int|null $register Register id touched by the invocation, when known. + * @param int|null $schema Schema id touched by the invocation, when known. + * @param int|null $object Object id touched by the invocation, when known (writes / get). + * @param string|null $objectUuid Object uuid touched by the invocation, when known (writes / get). + * + * @return AuditTrail The persisted, hash-chained entry. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-006 — Every invocation is audited) + * + * @SuppressWarnings(PHPMD.StaticAccess) Uuid::v4 is standard Symfony UID pattern + */ + public function createToolInvocationEntry( + string $toolId, + string $paramsDigest, + array $resultSummary, + ?int $register=null, + ?int $schema=null, + ?int $object=null, + ?string $objectUuid=null + ): AuditTrail { + $user = $this->userSession->getUser(); + $userId = 'system'; + if ($user !== null) { + $userId = $user->getUID(); + } + + $userName = 'System'; + if ($user !== null) { + $userName = $user->getDisplayName(); + } + + $verb = $toolId; + if (str_contains($toolId, '.') === true) { + $verb = substr($toolId, (strrpos($toolId, '.') + 1)); + } + + $auditTrail = new AuditTrail(); + $auditTrail->setUuid((string) Uuid::v4()); + $auditTrail->setAction('mcp.'.$verb); + $auditTrail->setToolId($toolId); + $auditTrail->setParamsDigest($paramsDigest); + $auditTrail->setResultSummary($resultSummary); + $auditTrail->setRegister($register); + $auditTrail->setSchema($schema); + $auditTrail->setObject($object); + $auditTrail->setObjectUuid($objectUuid); + $auditTrail->setUser($userId); + $auditTrail->setUserName($userName); + $auditTrail->setCreated(new DateTime()); + + return $this->insertHashChained(auditTrail: $auditTrail); + }//end createToolInvocationEntry() + /** * Get processing activities from audit trail entries. * diff --git a/lib/Db/CaseToken.php b/lib/Db/CaseToken.php new file mode 100644 index 0000000000..56d5b642e0 --- /dev/null +++ b/lib/Db/CaseToken.php @@ -0,0 +1,191 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class CaseToken + * + * @method string|null getToken() + * @method void getToken(?string $token) + * @method void setToken(?string $token) + * @method string|null getObjectUuid() + * @method void setObjectUuid(?string $objectUuid) + * @method int|null getRegisterId() + * @method void setRegisterId(?int $registerId) + * @method int|null getSchemaId() + * @method void setSchemaId(?int $schemaId) + * @method string|null getLabel() + * @method void setLabel(?string $label) + * @method string|null getCreatedBy() + * @method void setCreatedBy(?string $createdBy) + * @method DateTime|null getCreatedAt() + * @method void setCreatedAt(?DateTime $createdAt) + * @method DateTime|null getExpiresAt() + * @method void setExpiresAt(?DateTime $expiresAt) + * @method DateTime|null getRevokedAt() + * @method void setRevokedAt(?DateTime $revokedAt) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class CaseToken extends Entity implements JsonSerializable +{ + + /** + * The opaque public token (URL-safe, high entropy). + * + * @var string|null + */ + protected ?string $token = null; + + /** + * The referenced object uuid. + * + * @var string|null + */ + protected ?string $objectUuid = null; + + /** + * The register id of the referenced object. + * + * @var integer|null + */ + protected ?int $registerId = null; + + /** + * The schema id of the referenced object. + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * Optional human label for the link (e.g. "Track your application"). + * + * @var string|null + */ + protected ?string $label = null; + + /** + * The uid that minted the token. + * + * @var string|null + */ + protected ?string $createdBy = null; + + /** + * Creation timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $createdAt = null; + + /** + * Optional expiry timestamp; null = never expires. + * + * @var DateTime|null + */ + protected ?DateTime $expiresAt = null; + + /** + * Revocation timestamp; non-null = revoked (resolve fails closed). + * + * @var DateTime|null + */ + protected ?DateTime $revokedAt = null; + + /** + * Constructor. + */ + public function __construct() + { + $this->addType(fieldName: 'token', type: 'string'); + $this->addType(fieldName: 'objectUuid', type: 'string'); + $this->addType(fieldName: 'registerId', type: 'integer'); + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'label', type: 'string'); + $this->addType(fieldName: 'createdBy', type: 'string'); + $this->addType(fieldName: 'createdAt', type: 'datetime'); + $this->addType(fieldName: 'expiresAt', type: 'datetime'); + $this->addType(fieldName: 'revokedAt', type: 'datetime'); + }//end __construct() + + /** + * Whether the token is currently usable (not revoked, not expired). + * + * @param DateTime $now Reference instant. + * + * @return bool True when the token may be resolved. + */ + public function isValidAt(DateTime $now): bool + { + if ($this->revokedAt !== null) { + return false; + } + + if ($this->expiresAt !== null && $this->expiresAt <= $now) { + return false; + } + + return true; + }//end isValidAt() + + /** + * JSON serialization — public-safe metadata only (never the + * referenced object content, which the resolve endpoint renders + * RBAC-scoped separately). + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'token' => $this->token, + 'objectUuid' => $this->objectUuid, + 'registerId' => $this->registerId, + 'schemaId' => $this->schemaId, + 'label' => $this->label, + 'createdBy' => $this->createdBy, + 'createdAt' => $this->createdAt?->format(DateTime::ATOM), + 'expiresAt' => $this->expiresAt?->format(DateTime::ATOM), + 'revokedAt' => $this->revokedAt?->format(DateTime::ATOM), + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/CaseTokenMapper.php b/lib/Db/CaseTokenMapper.php new file mode 100644 index 0000000000..ae0bd2037b --- /dev/null +++ b/lib/Db/CaseTokenMapper.php @@ -0,0 +1,112 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * @link https://www.OpenRegister.nl + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class CaseTokenMapper + * + * @template-extends QBMapper + */ +class CaseTokenMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_case_tokens', entityClass: CaseToken::class); + }//end __construct() + + /** + * Find a case token by its opaque token string. + * + * Returns null on miss so callers can fail-closed (404) without a + * DoesNotExistException leaking through — important for the public + * resolve endpoint which must not become an enumeration oracle. + * + * @param string $token The opaque token string. + * + * @return CaseToken|null The token row, or null when unknown. + */ + public function findByToken(string $token): ?CaseToken + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); + + try { + return $this->findEntity(query: $qb); + } catch (DoesNotExistException $e) { + return null; + } + }//end findByToken() + + /** + * Find every token minted against the given object uuid. + * + * @param string $objectUuid The object uuid. + * + * @return CaseToken[] Token rows. + */ + public function findByObjectUuid(string $objectUuid): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid))) + ->orderBy('created_at', 'DESC'); + + return $this->findEntities(query: $qb); + }//end findByObjectUuid() + + /** + * Find a token by its primary id (used by the revoke path so the + * controller / provider can address a token unambiguously). + * + * @param int $id The token row id. + * + * @return CaseToken|null The token, or null when unknown. + */ + public function findById(int $id): ?CaseToken + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + try { + return $this->findEntity(query: $qb); + } catch (DoesNotExistException $e) { + return null; + } + }//end findById() +}//end class diff --git a/lib/Db/ChunkMapper.php b/lib/Db/ChunkMapper.php index 08a26f05ea..80a24f9015 100644 --- a/lib/Db/ChunkMapper.php +++ b/lib/Db/ChunkMapper.php @@ -24,6 +24,8 @@ use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; /** * Class ChunkMapper @@ -38,17 +40,31 @@ * @method list findEntities(IQueryBuilder $query) * * @template-extends QBMapper + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) Data-mapper surface: each method is one + * query over openregister_chunks (counts, source lookups, work queues, keyword search); + * splitting the mapper would scatter the table's query surface without reducing complexity. */ class ChunkMapper extends QBMapper { + + /** + * PSR-3 logger. + * + * @var LoggerInterface + */ + private LoggerInterface $logger; + /** * Constructor. * - * @param IDBConnection $db Database connection. + * @param IDBConnection $db Database connection. + * @param LoggerInterface|null $logger PSR-3 logger (nullable for BC with existing constructions). */ - public function __construct(IDBConnection $db) + public function __construct(IDBConnection $db, ?LoggerInterface $logger=null) { parent::__construct(db: $db, tableName: 'openregister_chunks', entityClass: Chunk::class); + $this->logger = $logger ?? new NullLogger(); }//end __construct() /** @@ -423,4 +439,139 @@ public function findUnindexed(?int $limit=null, ?int $offset=null): array return $this->findEntities(query: $qb); }//end findUnindexed() + + /** + * Find unvectorized chunks. + * + * Retrieves chunks that have been extracted but not yet vectorized, in + * FIFO order (`created_at ASC` — this is a work queue, not a relevance + * ranking). Consumed by ChunkVectorizationJob. + * + * @param int|null $limit Maximum number of chunks to return + * @param int|null $offset Offset for pagination + * + * @return Chunk[] Array of unvectorized chunks + * + * @psalm-return list<\OCA\OpenRegister\Db\Chunk> + * + * @spec openspec/changes/hybrid-document-search/tasks.md#5.1 + */ + public function findUnvectorized(?int $limit=null, ?int $offset=null): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq( + 'vectorized', + $qb->createNamedParameter( + false, + \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL + ) + ) + ) + ->orderBy('created_at', 'ASC'); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities(query: $qb); + }//end findUnvectorized() + + /** + * Ranked keyword search over chunk text (PostgreSQL only). + * + * Queries `to_tsvector('simple', text_content) @@ plainto_tsquery('simple', + * :query)`, ranked by `ts_rank` descending. The expression form matches the + * functional GIN index created by Version1Date20260706101000 (a STORED + * tsvector generated column is not viable on Nextcloud: Doctrine schema + * introspection throws on unknown column types — live-verified amendment, + * 2026-07-06). The 'simple' configuration avoids English-only stemming bias + * given OpenRegister's Dutch-government usage context (decision 7). + * + * Each row is shaped for VectorSearchHandler::reciprocalRankFusion()'s + * `$keywordResults` input. On non-PostgreSQL platforms, or when the query + * fails, returns [] with a logged warning — never throws (no ranked + * keyword path existed before this change on any platform, so this is + * purely additive). + * + * @param string $query Search query text + * @param int $limit Maximum number of results + * @param array $filters Optional filters (source_type) + * + * @return array + * + * @spec openspec/changes/hybrid-document-search/tasks.md#3.3 + */ + public function searchByKeyword(string $query, int $limit, array $filters=[]): array + { + $platform = $this->db->getDatabasePlatform(); + + if (str_contains(get_class($platform), 'PostgreSQL') === false) { + $this->logger->warning( + message: '[ChunkMapper] Ranked keyword search unavailable: requires PostgreSQL ' + .'with the tsvector keyword-search index', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'platform' => get_class($platform), + ] + ); + return []; + } + + $tsVector = "to_tsvector('simple', text_content)"; + $where = [$tsVector." @@ plainto_tsquery('simple', :query)"]; + $params = ['query' => $query]; + + if (($filters['source_type'] ?? null) !== null) { + $where[] = 'source_type = :sourceType'; + $params['sourceType'] = $filters['source_type']; + } + + $sql = 'SELECT id, source_type, source_id, text_content, chunk_index, ' + .'ts_rank('.$tsVector.", plainto_tsquery('simple', :query)) AS score " + .'FROM *PREFIX*openregister_chunks ' + .'WHERE '.implode(' AND ', $where).' ' + .'ORDER BY score DESC ' + .'LIMIT '.max(1, $limit); + + try { + $result = $this->db->executeQuery($sql, $params); + $rows = $result->fetchAll(); + $result->closeCursor(); + } catch (\Exception $e) { + // Query failure (a missing GIN index alone does not fail the + // expression form, it only slows it) — degrade, don't throw. + $this->logger->warning( + message: '[ChunkMapper] Keyword search query failed; returning empty result', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + return []; + } + + $results = []; + foreach ($rows as $row) { + $results[] = [ + 'entity_type' => (string) ($row['source_type'] ?? 'file'), + 'entity_id' => (string) $row['source_id'], + 'score' => (float) $row['score'], + 'chunk_text' => $row['text_content'], + 'chunk_index' => (int) $row['chunk_index'], + 'metadata' => [], + ]; + } + + return $results; + }//end searchByKeyword() }//end class diff --git a/lib/Db/Consumer.php b/lib/Db/Consumer.php index 0e51528f0f..ebaa93f06e 100644 --- a/lib/Db/Consumer.php +++ b/lib/Db/Consumer.php @@ -9,9 +9,9 @@ * @category Db * @package OCA\OpenRegister\Db * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Db/ConsumerMapper.php b/lib/Db/ConsumerMapper.php index a5fdecce77..25983b202e 100644 --- a/lib/Db/ConsumerMapper.php +++ b/lib/Db/ConsumerMapper.php @@ -9,9 +9,9 @@ * @category Db * @package OCA\OpenRegister\Db * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Db/DetectionMethod.php b/lib/Db/DetectionMethod.php new file mode 100644 index 0000000000..c2cf147a2b --- /dev/null +++ b/lib/Db/DetectionMethod.php @@ -0,0 +1,67 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/entity-relation-grondslagen/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +/** + * Detection-method tags written to `EntityRelation::detectionMethod`. + * + * Final + abstract — constants-only utility, never instantiated. + */ +final class DetectionMethod +{ + /** + * Presidio NER detection (default Python OpenAnonymiser backend). + */ + public const PRESIDIO = 'presidio'; + + /** + * OpenAnonymiser-side custom recognisers. + */ + public const OPENANONYMISER = 'openanonymiser'; + + /** + * Regex / pattern-based recogniser. + */ + public const PATTERN = 'pattern'; + + /** + * Operator-supplied via the `POST /api/files/{fileId}/manual-entities` + * endpoint. The catalogue + relation rows are structurally identical + * to detection-derived rows; only this tag distinguishes them. + */ + public const MANUAL = 'manual'; + + /** + * No public constructor — pure constants. + * + * @codeCoverageIgnore + */ + private function __construct() + { + + }//end __construct() +}//end class diff --git a/lib/Db/EmailLinkMapper.php b/lib/Db/EmailLinkMapper.php index 6341ccce81..67cdbefb2b 100644 --- a/lib/Db/EmailLinkMapper.php +++ b/lib/Db/EmailLinkMapper.php @@ -20,6 +20,7 @@ namespace OCA\OpenRegister\Db; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -97,6 +98,30 @@ public function tableExists(): bool return $this->tableExistsCache; }//end tableExists() + /** + * Find a single email link by primary id. + * + * QBMapper exposes `find()` only via `@method` docblock; concrete mappers + * add it themselves when needed. Wraps the inherited protected + * `findEntity()` so callers get a typed 404 path + * (`DoesNotExistException`) for unknown ids. + * + * @param int $id The email link row id. + * + * @return EmailLink The matching row. + * + * @throws DoesNotExistException When $id does not resolve. + */ + public function find(int $id): EmailLink + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity(query: $qb); + }//end find() + /** * Find email links by object UUID. * diff --git a/lib/Db/EntityRelation.php b/lib/Db/EntityRelation.php index 16bdb8b43a..4a589db652 100644 --- a/lib/Db/EntityRelation.php +++ b/lib/Db/EntityRelation.php @@ -61,6 +61,10 @@ * @method void setAnonymized(bool $anonymized) * @method string|null getAnonymizedValue() * @method void setAnonymizedValue(?string $anonymizedValue) + * @method array|null getBases() + * @method void setBases(?array $bases) + * @method bool getSkipAnonymization() + * @method void setSkipAnonymization(bool $skipAnonymization) * @method DateTime getCreatedAt() * @method void setCreatedAt(DateTime $createdAt) * @@ -191,6 +195,32 @@ class EntityRelation extends Entity implements JsonSerializable */ protected ?string $anonymizedValue = null; + /** + * Legal bases (grondslagen) under which the entity is being redacted. + * + * Array of UUID-shaped strings referencing `base` schema objects in a + * consumer app's register (DocuDesk's `dossier` register is the first + * consumer; see the `entity-relation-grondslagen` change). OpenRegister + * does NOT validate that the UUIDs resolve — vocabulary ownership lies + * with the consumer app. Set via `EntityRelationMapper::updateDecisionMetadata` + * (and its HTTP surface `PATCH /api/entity-relations/{id}`); audit-traced. + * + * @var array|null + */ + protected ?array $bases = null; + + /** + * Operator decision: skip this occurrence from the anonymise pass. + * + * When `true`, `EntityRelationMapper::markAsAnonymized` excludes this row + * from the file-scoped flip, the redaction text-replacement skips it, and + * the row retains `anonymized = false`. Set via the same audited + * `updateDecisionMetadata` write path as `bases`. + * + * @var boolean + */ + protected bool $skipAnonymization = false; + /** * Created at timestamp. * @@ -219,42 +249,48 @@ public function __construct() $this->addType(fieldName: 'context', type: 'string'); $this->addType(fieldName: 'anonymized', type: 'boolean'); $this->addType(fieldName: 'anonymizedValue', type: 'string'); + $this->addType(fieldName: 'bases', type: 'json'); + $this->addType(fieldName: 'skipAnonymization', type: 'boolean'); $this->addType(fieldName: 'createdAt', type: 'datetime'); }//end __construct() /** * JSON serialization. * - * @return (null|scalar)[] + * @return array * * @psalm-return array{id: int, entityId: int|null, chunkId: int|null, * role: null|string, fileId: int|null, objectId: int|null, - * emailId: int|null, positionStart: int, positionEnd: int, + * emailId: int|null, registerId: null|string, schemaId: null|string, + * objectUuid: null|string, positionStart: int, positionEnd: int, * confidence: float, detectionMethod: null|string, * context: null|string, anonymized: bool, - * anonymizedValue: null|string, createdAt: null|string} + * anonymizedValue: null|string, bases: array|null, + * skipAnonymization: bool, createdAt: null|string} */ public function jsonSerialize(): array { return [ - 'id' => $this->id, - 'entityId' => $this->entityId, - 'chunkId' => $this->chunkId, - 'role' => $this->role, - 'fileId' => $this->fileId, - 'objectId' => $this->objectId, - 'emailId' => $this->emailId, - 'registerId' => $this->registerId, - 'schemaId' => $this->schemaId, - 'objectUuid' => $this->objectUuid, - 'positionStart' => $this->positionStart, - 'positionEnd' => $this->positionEnd, - 'confidence' => $this->confidence, - 'detectionMethod' => $this->detectionMethod, - 'context' => $this->context, - 'anonymized' => $this->anonymized, - 'anonymizedValue' => $this->anonymizedValue, - 'createdAt' => $this->createdAt?->format(DateTime::ATOM), + 'id' => $this->id, + 'entityId' => $this->entityId, + 'chunkId' => $this->chunkId, + 'role' => $this->role, + 'fileId' => $this->fileId, + 'objectId' => $this->objectId, + 'emailId' => $this->emailId, + 'registerId' => $this->registerId, + 'schemaId' => $this->schemaId, + 'objectUuid' => $this->objectUuid, + 'positionStart' => $this->positionStart, + 'positionEnd' => $this->positionEnd, + 'confidence' => $this->confidence, + 'detectionMethod' => $this->detectionMethod, + 'context' => $this->context, + 'anonymized' => $this->anonymized, + 'anonymizedValue' => $this->anonymizedValue, + 'bases' => $this->bases, + 'skipAnonymization' => $this->skipAnonymization, + 'createdAt' => $this->createdAt?->format(DateTime::ATOM), ]; }//end jsonSerialize() }//end class diff --git a/lib/Db/EntityRelationMapper.php b/lib/Db/EntityRelationMapper.php index c40b3785d9..637c735612 100644 --- a/lib/Db/EntityRelationMapper.php +++ b/lib/Db/EntityRelationMapper.php @@ -20,10 +20,17 @@ namespace OCA\OpenRegister\Db; +use DateTime; +use OCA\OpenRegister\Event\EntityRelationDecisionUpdatedEvent; +use OCA\OpenRegister\Exception\CustomValidationException; use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; /** * Class EntityRelationMapper @@ -41,16 +48,65 @@ */ class EntityRelationMapper extends QBMapper { + /** + * Maximum number of expressions allowed in a single SQL IN() list. + * + * Nextcloud's QueryBuilder refuses more than 1000 expressions in an IN() + * list (Oracle limit); exceeding it logs an error and emits an "Undefined + * array key 0" PHP warning. Batched id lookups must chunk below this bound. + * + * @var integer + */ + private const MAX_IN_LIST_SIZE = 1000; + /** * Constructor. * - * @param IDBConnection $db Database connection. + * @param IDBConnection $db Database connection. + * @param AuditTrailMapper $auditTrailMapper Audit-trail persistence (used by updateDecisionMetadata). + * @param IUserSession $userSession Session user lookup for audit-trail actor. + * @param IEventDispatcher $eventDispatcher Symfony event dispatcher (used by updateDecisionMetadata + * to notify listeners after a decision-metadata write). + * @param LoggerInterface $logger Structured log sink. */ - public function __construct(IDBConnection $db) - { + public function __construct( + IDBConnection $db, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly IUserSession $userSession, + private readonly IEventDispatcher $eventDispatcher, + private readonly LoggerInterface $logger, + ) { parent::__construct(db: $db, tableName: 'openregister_entity_relations', entityClass: EntityRelation::class); }//end __construct() + /** + * Find a single entity relation by its primary id. + * + * QBMapper exposes `find()` via a `@method` docblock only; concrete + * mappers add it themselves when needed. This implementation wraps the + * inherited protected `findEntity()` so HTTP/DI callers get a typed + * 404 path (`DoesNotExistException`) for unknown ids. + * + * @param int $id The relation row id. + * + * @return EntityRelation The matching row. + * + * @throws \OCP\AppFramework\Db\DoesNotExistException When $id does not resolve. + */ + public function find(int $id): EntityRelation + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + /* + * @var EntityRelation + */ + + return $this->findEntity(query: $qb); + }//end find() + /** * Find entity relations by file ID. * @@ -119,9 +175,203 @@ public function findEntitiesForFile(int $fileId): array return $entities; }//end findEntitiesForFile() + /** + * Find anonymised entity relations with the full decision-metadata + entity context for a file. + * + * Differs from {@see findEntitiesForFile} in two ways: + * + * 1. Filters to rows where `r.anonymized = true` — the caller wants + * the "what was actually redacted" set, not the "what was detected" set. + * 2. Selects `r.bases`, `r.skip_anonymization`, `r.anonymized`, + * `r.anonymized_value` so downstream renderers (DocuDesk's + * `anonymisation-grondslagen-summary` change) can produce a + * grondslagen-traceable audit page without an N+1 of `find($id)` + * calls. + * + * Returned rows are flat associative arrays. `bases` is JSON-decoded + * into an `array` (or null if the column is null in the DB). + * + * @param int $fileId The Nextcloud file ID. + * + * @return array> Rows shaped as + * `{relation_id, entity_id, position_start, position_end, confidence, + * entity_type, entity_value, category, bases (array|null), + * skip_anonymization (bool), anonymized (bool), anonymized_value (string|null)}`. + */ + public function findAnonymisedEntitiesWithBasesForFile(int $fileId): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select( + 'r.id as relation_id', + 'r.entity_id', + 'r.position_start', + 'r.position_end', + 'r.confidence', + 'r.bases', + 'r.skip_anonymization', + 'r.anonymized', + 'r.anonymized_value', + 'e.type as entity_type', + 'e.value as entity_value', + 'e.category' + ) + ->from($this->getTableName(), 'r') + ->innerJoin('r', 'openregister_entities', 'e', $qb->expr()->eq('r.entity_id', 'e.id')) + ->where( + $qb->expr()->andX( + $qb->expr()->eq('r.file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('r.anonymized', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) + ) + ) + ->orderBy('r.position_start', 'ASC'); + + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + // Decode the JSON `bases` column. Doctrine returns it as a string; + // callers expect an array (or null when no bases were recorded). + foreach ($rows as &$row) { + if (isset($row['bases']) === true && is_string($row['bases']) === true) { + $decoded = json_decode($row['bases'], associative: true); + $row['bases'] = null; + if (is_array($decoded) === true) { + $row['bases'] = $decoded; + } + } + + // Normalise boolean-as-int values returned by some DB drivers. + if (isset($row['skip_anonymization']) === true) { + $row['skip_anonymization'] = (bool) $row['skip_anonymization']; + } + + if (isset($row['anonymized']) === true) { + $row['anonymized'] = (bool) $row['anonymized']; + } + } + + unset($row); + + return $rows; + }//end findAnonymisedEntitiesWithBasesForFile() + + /** + * Build a `(entity_value → {id, type})` map for every entity relation on a file. + * + * Used by `DocumentProcessingHandler::anonymizeDocument` to look up the + * stable `entity_id` for each entity occurrence so the in-file + * placeholder format `[: ]` matches what downstream + * consumers (DocuDesk's grondslagen-summary report) display. Without + * this lookup the anonymise step generates a fresh UUID-prefix per + * call, and the report's view of "what's in the file" diverges from + * the actual substitution. + * + * Returns a single entry per distinct `entity_value` on the file. + * When the same value is detected as multiple `entity_types`, the + * first one encountered wins — callers should use their own + * `entityType` to discriminate at the substitution site. + * + * @param int $fileId The Nextcloud file ID whose entity relations are mapped. + * + * @return array Map keyed by entity value. + */ + public function findEntityIdsByValueForFile(int $fileId): array + { + $qb = $this->db->getQueryBuilder(); + $qb->selectDistinct(['e.id', 'e.type', 'e.value']) + ->from($this->getTableName(), 'r') + ->innerJoin('r', 'openregister_entities', 'e', $qb->expr()->eq('r.entity_id', 'e.id')) + ->where($qb->expr()->eq('r.file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + $map = []; + foreach ($rows as $row) { + $value = (string) ($row['value'] ?? ''); + if ($value === '' || isset($map[$value]) === true) { + continue; + } + + $map[$value] = [ + 'id' => (int) ($row['id'] ?? 0), + 'type' => (string) ($row['type'] ?? ''), + ]; + } + + return $map; + + }//end findEntityIdsByValueForFile() + + /** + * Load the entity-relation rows for several files in one query — the + * multi-file sibling of `findEntityIdsByValueForFile`. + * + * Used by the per-dossier placeholder numbering (Decision 3): the dossier + * number is recomputed as a pure function of the dossier's stored rows, so + * this returns the raw `(entity_id, file_id, position_start)` tuples for + * every relation on the given files. The caller (`PlaceholderIdTranslator`) + * imposes the total stable order `(file_id, position_start, entity_id)` and + * ranks distinct `entity_id`s by first appearance. No join to + * `openregister_entities` is needed — the relation row already carries the + * `entity_id` that is the numbering key. + * + * @param array $fileIds The Nextcloud file IDs of the dossier's files. + * + * @return array + * One record per relation row across all given files (unordered; + * the caller imposes the ranking order). + */ + public function findEntityIdsByValueForFiles(array $fileIds): array + { + if ($fileIds === []) { + return []; + } + + $ids = array_values(array_unique(array_map('intval', $fileIds))); + + // Chunk below the 1000-expression IN() ceiling (Oracle limit enforced by + // Nextcloud's QueryBuilder): a dossier can legitimately hold more than + // 1000 files. + $rows = []; + foreach (array_chunk($ids, self::MAX_IN_LIST_SIZE) as $chunk) { + $qb = $this->db->getQueryBuilder(); + $qb->select('entity_id', 'file_id', 'position_start') + ->from($this->getTableName()) + ->where( + $qb->expr()->in( + 'file_id', + $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY) + ) + ); + + $result = $qb->executeQuery(); + $rows = array_merge($rows, $result->fetchAll()); + $result->closeCursor(); + } + + $records = []; + foreach ($rows as $row) { + $records[] = [ + 'entity_id' => (int) ($row['entity_id'] ?? 0), + 'file_id' => (int) ($row['file_id'] ?? 0), + 'position_start' => (int) ($row['position_start'] ?? 0), + ]; + } + + return $records; + + }//end findEntityIdsByValueForFiles() + /** * Mark entity relations as anonymized. * + * Skip-aware: rows where `skip_anonymization = true` are excluded + * (those reflect an operator decision NOT to redact the occurrence). + * Per the `entity-relation-grondslagen` change, skipped rows retain + * `anonymized = false` after the file's anonymise pass. + * * @param int $fileId The file ID. * @param string $anonymizedValue The placeholder value used. * @@ -133,8 +383,581 @@ public function markAsAnonymized(int $fileId, string $anonymizedValue): int $qb->update($this->getTableName()) ->set('anonymized', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) ->set('anonymized_value', $qb->createNamedParameter($anonymizedValue)) - ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('skip_anonymization', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))); return $qb->executeStatement(); }//end markAsAnonymized() + + /** + * Probe whether a relation already exists at a specific position + * for a (file, entity) pair. + * + * Used by `ManualEntityService` to make manual-entity adds + * idempotent: re-calling the endpoint for the same value on the + * same file does NOT create duplicate relation rows. The dedup key + * is the full (fileId, entityId, chunkId, positionStart, positionEnd) + * tuple — same positions across different entities (e.g. position + * 142 has both `"Jan Jansen"` PERSON and `"Jansen"` PERSON entities) + * are legitimately distinct rows. + * + * @param int $fileId Nextcloud file id. + * @param int $entityId Catalogue entity id. + * @param int $chunkId Chunk row id the position is relative to. + * @param int $positionStart Position of the first byte of the match within the chunk. + * @param int $positionEnd Position one past the last byte of the match within the chunk. + * + * @return bool True when a row with exactly these five values exists. + */ + public function existsForFileAtPosition( + int $fileId, + int $entityId, + int $chunkId, + int $positionStart, + int $positionEnd + ): bool { + $qb = $this->db->getQueryBuilder(); + $qb->select('id') + ->from($this->getTableName()) + ->where( + $qb->expr()->andX( + $qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('entity_id', $qb->createNamedParameter($entityId, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('chunk_id', $qb->createNamedParameter($chunkId, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq( + 'position_start', + $qb->createNamedParameter($positionStart, IQueryBuilder::PARAM_INT) + ), + $qb->expr()->eq( + 'position_end', + $qb->createNamedParameter($positionEnd, IQueryBuilder::PARAM_INT) + ) + ) + ) + ->setMaxResults(1); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + return ($row !== false); + + }//end existsForFileAtPosition() + + /** + * Insert multiple relation rows in a single pass. + * + * The caller is expected to manage the surrounding transaction + * (`IDBConnection::beginTransaction()` / `commit()` / `rollBack()`). + * `ManualEntityService` does this so the entity-create + batch + * relation insert + audit-trail write all commit atomically. + * + * Each input row is an associative array carrying the fields the + * caller wants to persist. Recognised keys (all optional unless + * marked): + * + * - `entityId` (required, int) + * - `fileId` (int|null) + * - `chunkId` (int|null) + * - `objectId` (int|null) + * - `emailId` (int|null) + * - `positionStart` (int) + * - `positionEnd` (int) + * - `confidence` (float) + * - `detectionMethod` (string) + * - `context` (string|null) + * - `role` (string|null) + * - `anonymized` (bool) + * - `skipAnonymization` (bool) + * - `bases` (array|null) + * - `createdAt` (DateTime, defaults to now) + * + * @param array> $rows Rows to insert. + * + * @return EntityRelation[] The inserted entities with their generated ids, + * in the same order as the input. + * + * @throws \Throwable Any DB error is re-thrown verbatim so the caller's + * transaction can roll back. + */ + public function insertBatch(array $rows): array + { + $inserted = []; + foreach ($rows as $row) { + $relation = $this->buildRelationFromRow(row: $row); + $inserted[] = $this->insert(entity: $relation); + } + + return $inserted; + + }//end insertBatch() + + /** + * Materialise an EntityRelation entity from a raw row array. + * + * Pulled out of `insertBatch` so the field-by-field setter mapping + * lives in one place. Unknown keys are silently ignored; missing + * required key (`entityId`) trigger an exception via the setter + * type-hint. The `openregister_entity_relations` table has no uuid + * column — relations are identified by their auto-increment `id`. + * + * @param array $row Field values keyed by camelCase setter name. + * + * @return EntityRelation Populated, ready to insert. + */ + private function buildRelationFromRow(array $row): EntityRelation + { + $relation = new EntityRelation(); + + $relation->setEntityId((int) $row['entityId']); + + if (array_key_exists('fileId', $row) === true && $row['fileId'] !== null) { + $relation->setFileId((int) $row['fileId']); + } + + if (array_key_exists('chunkId', $row) === true && $row['chunkId'] !== null) { + $relation->setChunkId((int) $row['chunkId']); + } + + if (array_key_exists('objectId', $row) === true && $row['objectId'] !== null) { + $relation->setObjectId((int) $row['objectId']); + } + + if (array_key_exists('emailId', $row) === true && $row['emailId'] !== null) { + $relation->setEmailId((int) $row['emailId']); + } + + if (array_key_exists('positionStart', $row) === true) { + $relation->setPositionStart((int) $row['positionStart']); + } + + if (array_key_exists('positionEnd', $row) === true) { + $relation->setPositionEnd((int) $row['positionEnd']); + } + + if (array_key_exists('confidence', $row) === true) { + $relation->setConfidence((float) $row['confidence']); + } + + if (array_key_exists('detectionMethod', $row) === true && $row['detectionMethod'] !== null) { + $relation->setDetectionMethod((string) $row['detectionMethod']); + } + + if (array_key_exists('context', $row) === true) { + $contextValue = null; + if ($row['context'] !== null) { + $contextValue = (string) $row['context']; + } + + $relation->setContext($contextValue); + } + + if (array_key_exists('role', $row) === true) { + $roleValue = null; + if ($row['role'] !== null) { + $roleValue = (string) $row['role']; + } + + $relation->setRole($roleValue); + } + + if (array_key_exists('anonymized', $row) === true) { + $relation->setAnonymized((bool) $row['anonymized']); + } + + if (array_key_exists('skipAnonymization', $row) === true) { + $relation->setSkipAnonymization((bool) $row['skipAnonymization']); + } + + if (array_key_exists('bases', $row) === true) { + $relation->setBases($row['bases']); + } + + $relation->setCreatedAt(new DateTime()); + if (array_key_exists('createdAt', $row) === true && $row['createdAt'] !== null) { + $relation->setCreatedAt($row['createdAt']); + } + + return $relation; + + }//end buildRelationFromRow() + + /** + * Mark entity relations as anonymized, persisting each entity's EXACT + * emitted placeholder into its `anonymized_value`. + * + * The scope-local placeholder (number + localized label) is computed once, + * at anonymise time, inside the run — it is not otherwise recoverable later + * (the number depends on the scope and is not derivable from the stored + * rows alone). Persisting it here, per relation, is the only durable record + * of "what each entity became" — and it lives exactly as long as the + * relation does (overwritten on re-anonymise, removed when the relation is + * deleted/re-extracted), so there is no separate store to keep in sync or + * clean up. + * + * ONLY the entities actually redacted are marked: each `entity_id` present + * in `$placeholderByEntityId` (i.e. the entities that were substituted into + * the document) has all its non-skipped relations on the file set to + * `anonymized = true` with `anonymized_value` = its exact placeholder. + * + * This intentionally does NOT blanket-mark every non-skipped relation: + * an entity that was detected but NOT redacted (absent from the map — e.g. + * a value matched only under a second type, or filtered out) was never + * substituted into the document, so marking it anonymized would make the + * grondslagen-summary list a placeholder that isn't in the file and (with + * no scope-local number) leak the global entity id. Skip decisions are + * honoured (skipped rows untouched). + * + * @param int $fileId The file ID. + * @param array $placeholderByEntityId Map of (stringified) entity id → the + * exact placeholder emitted for it + * (e.g. "7" => "[PERSOON: 1]"). + * + * @return int Number of relation rows marked anonymized. + */ + public function markAsAnonymizedWithPlaceholders( + int $fileId, + array $placeholderByEntityId + ): int { + $total = 0; + foreach ($placeholderByEntityId as $entityId => $placeholder) { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->getTableName()) + ->set('anonymized', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) + ->set('anonymized_value', $qb->createNamedParameter((string) $placeholder)) + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('entity_id', $qb->createNamedParameter((int) $entityId, IQueryBuilder::PARAM_INT))) + ->andWhere( + $qb->expr()->eq('skip_anonymization', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)) + ); + $total += $qb->executeStatement(); + } + + return $total; + + }//end markAsAnonymizedWithPlaceholders() + + /** + * Find entity relations for the anonymise pass — skip-aware. + * + * Same shape as `findEntitiesForFile` but filters out rows the operator + * has flagged with `skip_anonymization = true`. The anonymise flow + * (`FileTextController::anonymizeFile`) uses this method to build the + * replacements list so skipped occurrences are excluded from redaction. + * + * @param int $fileId The file ID. + * + * @return array> Array of entity rows joined with their Entity record. + */ + public function findEntitiesForAnonymization(int $fileId): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select( + 'r.id as relation_id', + 'r.entity_id', + 'r.position_start', + 'r.position_end', + 'r.confidence', + 'e.type as entity_type', + 'e.value as entity_value', + 'e.category' + ) + ->from($this->getTableName(), 'r') + ->innerJoin('r', 'openregister_entities', 'e', $qb->expr()->eq('r.entity_id', 'e.id')) + ->where($qb->expr()->eq('r.file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('r.skip_anonymization', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->orderBy('r.position_start', 'ASC'); + + $result = $qb->executeQuery(); + $entities = $result->fetchAll(); + $result->closeCursor(); + + return $entities; + }//end findEntitiesForAnonymization() + + /** + * Collect distinct `Entity.value` strings for relations on a file flagged + * with `skip_anonymization = true`. + * + * Used by the anonymise text-replacement code path as a defensive filter: + * even when the caller (e.g. DocuDesk) passes an entities[] payload that + * includes skipped occurrences, the redaction step removes those entries + * by matching their `text` against the values returned here. Per the + * `entity-relation-grondslagen` spec: "skipped relations are never + * redacted, full stop", regardless of caller behaviour. + * + * @param int $fileId The file ID. + * + * @return array Distinct entity values for skipped relations. + */ + public function findSkippedEntityValuesForFile(int $fileId): array + { + $qb = $this->db->getQueryBuilder(); + $qb->selectDistinct('e.value') + ->from($this->getTableName(), 'r') + ->innerJoin('r', 'openregister_entities', 'e', $qb->expr()->eq('r.entity_id', 'e.id')) + ->where($qb->expr()->eq('r.file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('r.skip_anonymization', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))); + + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + return array_values( + array_filter( + array_map(static fn (array $row): string => (string) ($row['value'] ?? ''), $rows), + static fn (string $value): bool => $value !== '' + ) + ); + }//end findSkippedEntityValuesForFile() + + /** + * Update operator-decision metadata on an EntityRelation row. + * + * Single audited write path for the two decision-only fields: + * - `bases` (?array) — UUIDs of legal grondslagen + * - `skipAnonymization` (bool) — opt-out from the next anonymise pass + * + * Strict whitelist: any other key in `$fields` causes a + * `CustomValidationException`. Shape validation: `bases` MUST be + * null or array; `skipAnonymization` MUST be a bool. + * + * Diff-aware: if every supplied value matches the current row state, + * the method returns the unchanged row and writes NO audit entry + * (semantic no-op). Empty `$fields` is also a no-op. + * + * Otherwise the row is updated and exactly one audit-trail entry is + * emitted via the OpenRegister immutable audit subsystem (ADR-022), + * capturing the acting user UID (per ADR-005, the UID — NOT the + * display name in the structured changed-fields payload), the + * timestamp, the subject (table + row id), and only the fields that + * actually changed. + * + * @param EntityRelation $relation Pre-loaded relation row (callers handle find/404). + * @param array $fields Subset of whitelist keys with new values. + * @param IUser|null $actingUser Optional explicit acting user; falls back + * to the session user. If null and no + * session user, the audit entry records the + * actor as 'system'. + * + * @return EntityRelation The (possibly unchanged) row. + * + * @throws CustomValidationException When a whitelist or shape violation + * is detected. + */ + public function updateDecisionMetadata(EntityRelation $relation, array $fields, ?IUser $actingUser=null): EntityRelation + { + $allowed = ['bases', 'skipAnonymization']; + $unknown = array_diff(array_keys($fields), $allowed); + if (count($unknown) > 0) { + $first = (string) reset($unknown); + throw new CustomValidationException( + message: 'Field not editable: '.$first, + errors: ['field' => $first] + ); + } + + if (array_key_exists('bases', $fields) === true) { + $bases = $fields['bases']; + if ($bases !== null && is_array($bases) === false) { + throw new CustomValidationException( + message: 'Invalid bases shape: must be null or array of strings', + errors: ['field' => 'bases', 'reason' => 'must_be_null_or_array_of_strings'] + ); + } + + if (is_array($bases) === true) { + foreach ($bases as $element) { + if (is_string($element) === false) { + throw new CustomValidationException( + message: 'Invalid bases shape: array elements must be strings', + errors: ['field' => 'bases', 'reason' => 'must_be_null_or_array_of_strings'] + ); + } + } + } + } + + if (array_key_exists('skipAnonymization', $fields) === true + && is_bool($fields['skipAnonymization']) === false + ) { + throw new CustomValidationException( + message: 'Invalid skipAnonymization shape: must be a boolean', + errors: ['field' => 'skipAnonymization', 'reason' => 'must_be_boolean'] + ); + } + + $changedFields = []; + + if (array_key_exists('bases', $fields) === true) { + $previousBases = $relation->getBases(); + // Compare as multiset — same UUIDs in different order is a + // semantic no-op for redaction policy. Storage preserves + // operator-supplied order; only the diff check normalises. + if (self::basesAreEqual(a: $previousBases, b: $fields['bases']) === false) { + $changedFields['bases'] = [ + 'previous' => $previousBases, + 'new' => $fields['bases'], + ]; + $relation->setBases($fields['bases']); + } + } + + if (array_key_exists('skipAnonymization', $fields) === true) { + $previousSkip = $relation->getSkipAnonymization(); + if ($previousSkip !== $fields['skipAnonymization']) { + $changedFields['skipAnonymization'] = [ + 'previous' => $previousSkip, + 'new' => $fields['skipAnonymization'], + ]; + $relation->setSkipAnonymization($fields['skipAnonymization']); + } + } + + if (count($changedFields) === 0) { + return $relation; + } + + // Audit-invariant: every persisted state change MUST have an + // audit-trail entry. Both writes go in a single transaction so a + // failed audit-INSERT rolls back the row UPDATE — clients see + // HTTP 500 instead of an undetectable audit gap. The downstream + // event dispatch stays OUTSIDE the transaction (post-commit, + // informational; listener failures must not roll back). + $this->db->beginTransaction(); + try { + $relation = $this->update(entity: $relation); + $relationId = (int) $relation->getId(); + $this->emitDecisionMetadataAuditEntry( + relationId: $relationId, + changedFields: $changedFields, + actingUser: $actingUser + ); + $this->db->commit(); + } catch (\Throwable $writeError) { + $this->db->rollBack(); + $this->logger->error( + message: '[EntityRelationMapper] Transactional decision-metadata write failed, rolled back', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'relation_id' => (int) $relation->getId(), + 'changedKeys' => array_keys($changedFields), + 'error' => $writeError->getMessage(), + ] + ); + throw $writeError; + }//end try + + // Notify listeners (downstream apps — e.g. DocuDesk's + // publication-clearance-via-anonymise change subscribes here to + // create a publicationConsent record whenever skipAnonymization + // flips false → true, so the 28-day Woo clock starts ticking at + // decision time rather than at anonymise time). Failure to + // dispatch / listener failure MUST NOT roll back the persisted + // state change — log and continue, same contract as audit. + try { + $this->eventDispatcher->dispatchTyped( + new EntityRelationDecisionUpdatedEvent( + relation: $relation, + changedFields: $changedFields, + actingUser: ($actingUser ?? $this->userSession->getUser()) + ) + ); + } catch (\Throwable $dispatchError) { + $this->logger->error( + message: '[EntityRelationMapper] Failed to dispatch EntityRelationDecisionUpdatedEvent', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'relation_id' => $relationId, + 'changedKeys' => array_keys($changedFields), + 'error' => $dispatchError->getMessage(), + ] + ); + }//end try + + return $relation; + }//end updateDecisionMetadata() + + /** + * Multiset-equality on two `bases` values. + * + * Distinguishes the four nullability/empty cases that callers care about: + * - null vs null → equal (both "no bases") + * - null vs [] → NOT equal (different intent: "unset" vs "explicitly empty") + * - [a, b] vs [b, a] → equal (order-insensitive for redaction policy) + * - [a, b, a] vs [b, a] → equal (duplicates collapsed) + * + * Storage preserves operator-supplied order + duplicates; only the + * diff check normalises. Used by `updateDecisionMetadata` to avoid + * spurious audit entries on cosmetic reorderings. + * + * @param array|null $a First value. + * @param array|null $b Second value. + * + * @return bool True when the two values represent the same redaction policy. + */ + private static function basesAreEqual(?array $a, ?array $b): bool + { + if ($a === null && $b === null) { + return true; + } + + if ($a === null || $b === null) { + return false; + } + + $normA = array_values(array_unique($a, SORT_STRING)); + $normB = array_values(array_unique($b, SORT_STRING)); + sort($normA); + sort($normB); + return $normA === $normB; + + }//end basesAreEqual() + + /** + * Emit one audit-trail entry summarising a decision-metadata write. + * + * The AuditTrail entity's `object`, `objectUuid`, `register`, `schema` + * fields are designed around ObjectEntity rows; EntityRelation is a + * non-ObjectEntity table, so we set those nullable fields to null and + * encode the subject (table + row id) inside the `changed` JSON + * payload alongside the per-field diff. + * + * @param int $relationId The EntityRelation row id. + * @param array $changedFields Map of field name → { previous, new }. + * @param IUser|null $actingUser Optional explicit acting user. + * + * @return void + */ + private function emitDecisionMetadataAuditEntry( + int $relationId, + array $changedFields, + ?IUser $actingUser=null + ): void { + $user = $actingUser ?? $this->userSession->getUser(); + $userId = 'system'; + if ($user !== null) { + $userId = $user->getUID(); + } + + $auditTrail = new AuditTrail(); + $auditTrail->setUuid(\Symfony\Component\Uid\Uuid::v4()->toRfc4122()); + $auditTrail->setAction('entity_relation_decision_updated'); + $auditTrail->setUser($userId); + // ADR-005: actor is the UID only. Display name is PII; do not + // persist it on entity-relation decision audit entries. + $auditTrail->setUserName(null); + $auditTrail->setCreated(new DateTime()); + $auditTrail->setChanged( + [ + 'subjectType' => 'openregister_entity_relations', + 'subjectId' => $relationId, + 'fields' => $changedFields, + ] + ); + + $this->auditTrailMapper->insert($auditTrail); + }//end emitDecisionMetadataAuditEntry() }//end class diff --git a/lib/Db/FederatedShare.php b/lib/Db/FederatedShare.php new file mode 100644 index 0000000000..33f85339d3 --- /dev/null +++ b/lib/Db/FederatedShare.php @@ -0,0 +1,313 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * FederatedShare entity class. + * + * @method string|null getUuid() + * @method void setUuid(?string $uuid) + * @method string|null getDirection() + * @method void setDirection(?string $direction) + * @method string|null getRemoteInstanceUrl() + * @method void setRemoteInstanceUrl(?string $remoteInstanceUrl) + * @method string|null getRemoteProviderId() + * @method void setRemoteProviderId(?string $remoteProviderId) + * @method string|null getShareToken() + * @method void setShareToken(?string $shareToken) + * @method string|null getScope() + * @method void setScope(?string $scope) + * @method string|null getRegister() + * @method void setRegister(?string $register) + * @method string|null getSchema() + * @method void setSchema(?string $schema) + * @method string|null getObjectUri() + * @method void setObjectUri(?string $objectUri) + * @method array|null getQueryFilter() + * @method void setQueryFilter(?array $queryFilter) + * @method string|null getPermissions() + * @method void setPermissions(?string $permissions) + * @method string|null getSharedWith() + * @method void setSharedWith(?string $sharedWith) + * @method string|null getStatus() + * @method void setStatus(?string $status) + * @method DateTime|null getCreated() + * @method void setCreated(?DateTime $created) + * @method DateTime|null getUpdated() + * @method void setUpdated(?DateTime $updated) + */ +class FederatedShare extends Entity implements JsonSerializable +{ + + /** + * Public UUID of the share. + * + * @var string|null + */ + protected ?string $uuid = null; + + /** + * Direction: 'outgoing' (we shared out) or 'incoming' (shared with us). + * + * @var string|null + */ + protected ?string $direction = null; + + /** + * Base URL of the peer OpenRegister instance (e.g. https://fed2.example). + * + * @var string|null + */ + protected ?string $remoteInstanceUrl = null; + + /** + * OCM provider id issued by the remote for this share (nullable). + * + * @var string|null + */ + protected ?string $remoteProviderId = null; + + /** + * Scoped bearer token authorising the federated serving endpoint. + * + * @var string|null + */ + protected ?string $shareToken = null; + + /** + * Share scope: 'register' | 'schema' | 'object' | 'query'. + * + * @var string|null + */ + protected ?string $scope = null; + + /** + * Register id or slug the share covers (nullable for object shares). + * + * @var string|null + */ + protected ?string $register = null; + + /** + * Schema id or slug the share covers (nullable for register-wide shares). + * + * @var string|null + */ + protected ?string $schema = null; + + /** + * Canonical object URI for an object-scope share (nullable otherwise). + * + * @var string|null + */ + protected ?string $objectUri = null; + + /** + * Query filter for a 'query'-scope (rule/flow) share (nullable otherwise). + * + * @var array|null + */ + protected ?array $queryFilter = null; + + /** + * Granted permissions: 'read' or 'read-write'. + * + * @var string|null + */ + protected ?string $permissions = null; + + /** + * Local owning organisation UUID. + * + * @var string|null + */ + protected ?string $organisation = null; + + /** + * Target federated address ('org-slug@host'). + * + * @var string|null + */ + protected ?string $sharedWith = null; + + /** + * Lifecycle status: 'pending' | 'accepted' | 'declined' | 'revoked'. + * + * @var string|null + */ + protected ?string $status = null; + + /** + * Creation timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $created = null; + + /** + * Last-updated timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $updated = null; + + /** + * Constructor: registers field types. + */ + public function __construct() + { + $this->addType(fieldName: 'uuid', type: 'string'); + $this->addType(fieldName: 'direction', type: 'string'); + $this->addType(fieldName: 'remoteInstanceUrl', type: 'string'); + $this->addType(fieldName: 'remoteProviderId', type: 'string'); + $this->addType(fieldName: 'shareToken', type: 'string'); + $this->addType(fieldName: 'scope', type: 'string'); + $this->addType(fieldName: 'register', type: 'string'); + $this->addType(fieldName: 'schema', type: 'string'); + $this->addType(fieldName: 'objectUri', type: 'string'); + $this->addType(fieldName: 'queryFilter', type: 'json'); + $this->addType(fieldName: 'permissions', type: 'string'); + $this->addType(fieldName: 'organisation', type: 'string'); + $this->addType(fieldName: 'sharedWith', type: 'string'); + $this->addType(fieldName: 'status', type: 'string'); + $this->addType(fieldName: 'created', type: 'datetime'); + $this->addType(fieldName: 'updated', type: 'datetime'); + }//end __construct() + + /** + * Get the JSON field names on this entity. + * + * @return string[] List of JSON field names. + * + * @psalm-return list + */ + public function getJsonFields(): array + { + return array_keys( + array_filter( + $this->getFieldTypes(), + static function ($field) { + return $field === 'json'; + } + ) + ); + }//end getJsonFields() + + /** + * Get the local owning organisation UUID. + * + * @return string|null The organisation UUID. + */ + public function getOrganisation(): ?string + { + return $this->organisation; + }//end getOrganisation() + + /** + * Set the local owning organisation UUID. + * + * @param string|null $organisation The organisation UUID. + * + * @return void + */ + public function setOrganisation(?string $organisation): void + { + $this->organisation = $organisation; + $this->markFieldUpdated(attribute: 'organisation'); + }//end setOrganisation() + + /** + * Hydrate the entity from an array. + * + * @param array $object The data array to hydrate from. + * + * @return static This instance for chaining. + */ + public function hydrate(array $object): static + { + $jsonFields = $this->getJsonFields(); + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields, true) === true && $value === []) { + $value = null; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Silently ignore invalid properties. + } + } + + return $this; + }//end hydrate() + + /** + * Serialize the entity to a JSON-ready array. + * + * @return array The serialized share. + */ + public function jsonSerialize(): array + { + $created = null; + if ($this->created !== null) { + $created = $this->created->format('c'); + } + + $updated = null; + if ($this->updated !== null) { + $updated = $this->updated->format('c'); + } + + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'direction' => $this->direction, + 'remoteInstanceUrl' => $this->remoteInstanceUrl, + 'remoteProviderId' => $this->remoteProviderId, + 'shareToken' => $this->shareToken, + 'scope' => $this->scope, + 'register' => $this->register, + 'schema' => $this->schema, + 'objectUri' => $this->objectUri, + 'queryFilter' => $this->queryFilter, + 'permissions' => $this->permissions, + 'organisation' => $this->organisation, + 'sharedWith' => $this->sharedWith, + 'status' => $this->status, + 'created' => $created, + 'updated' => $updated, + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/FederatedShareMapper.php b/lib/Db/FederatedShareMapper.php new file mode 100644 index 0000000000..61cfd31590 --- /dev/null +++ b/lib/Db/FederatedShareMapper.php @@ -0,0 +1,272 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IAppConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUserSession; +use Symfony\Component\Uid\Uuid; + +/** + * The FederatedShareMapper class. + * + * @template-extends QBMapper + */ +class FederatedShareMapper extends QBMapper +{ + use MultiTenancyTrait; + + /** + * Organisation mapper (required by MultiTenancyTrait). + * + * @var OrganisationMapper + */ + protected OrganisationMapper $organisationMapper; + + /** + * User session (required by MultiTenancyTrait). + * + * @var IUserSession + */ + protected IUserSession $userSession; + + /** + * Group manager (required by MultiTenancyTrait). + * + * @var IGroupManager + */ + protected IGroupManager $groupManager; + + /** + * App configuration (required by MultiTenancyTrait). + * + * @var IAppConfig + */ + protected IAppConfig $appConfig; + + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + * @param OrganisationMapper $organisationMapper Organisation mapper. + * @param IUserSession $userSession User session. + * @param IGroupManager $groupManager Group manager. + * @param IAppConfig $appConfig App configuration. + */ + public function __construct( + IDBConnection $db, + OrganisationMapper $organisationMapper, + IUserSession $userSession, + IGroupManager $groupManager, + IAppConfig $appConfig + ) { + parent::__construct(db: $db, tableName: 'openregister_federated_shares', entityClass: FederatedShare::class); + $this->organisationMapper = $organisationMapper; + $this->userSession = $userSession; + $this->groupManager = $groupManager; + $this->appConfig = $appConfig; + }//end __construct() + + /** + * Find a federated share by database id. + * + * @param int $id The share id. + * + * @return FederatedShare The share. + */ + public function find(int $id): FederatedShare + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_federated_shares') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + $this->applyOrganisationFilter(qb: $qb); + + return $this->findEntity(query: $qb); + }//end find() + + /** + * Find a federated share by its scoped share token. + * + * Used by the federation serving endpoint to resolve an incoming request; + * the organisation filter is intentionally NOT applied because the token + * itself is the access credential (the caller is a remote instance, not a + * local session). + * + * @param string $shareToken The scoped bearer token. + * + * @return FederatedShare The share. + */ + public function findByToken(string $shareToken): FederatedShare + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_federated_shares') + ->where($qb->expr()->eq('share_token', $qb->createNamedParameter($shareToken))); + + return $this->findEntity(query: $qb); + }//end findByToken() + + /** + * Find an existing outgoing object-scope share for a uri + target, or null. + * + * Used by the federate-share flow action to stay idempotent (one share per + * object per target). Bypasses the organisation filter — the flow that + * created the share owns the dedup decision, not the acting session. + * + * @param string $objectUri The shared object's uri/uuid. + * @param string $sharedWith The federated target (slug@host). + * + * @return FederatedShare|null The existing share, or null. + */ + public function findOutgoingObjectShare(string $objectUri, string $sharedWith): ?FederatedShare + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_federated_shares') + ->where($qb->expr()->eq('direction', $qb->createNamedParameter('outgoing'))) + ->andWhere($qb->expr()->eq('scope', $qb->createNamedParameter('object'))) + ->andWhere($qb->expr()->eq('object_uri', $qb->createNamedParameter($objectUri))) + ->andWhere($qb->expr()->eq('shared_with', $qb->createNamedParameter($sharedWith))) + ->setMaxResults(1); + + try { + return $this->findEntity(query: $qb); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return null; + } + }//end findOutgoingObjectShare() + + /** + * Find all federated shares, optionally filtered. + * + * @param int|null $limit Maximum number of results. + * @param int|null $offset Result offset. + * @param array|null $filters Column => value equality filters. + * + * @return FederatedShare[] The matching shares. + * + * @psalm-return list + */ + public function findAll(?int $limit=null, ?int $offset=null, ?array $filters=[]): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_federated_shares') + ->setMaxResults($limit) + ->setFirstResult($offset); + + foreach ($filters ?? [] as $column => $value) { + $qb->andWhere($qb->expr()->eq($column, $qb->createNamedParameter($value))); + } + + $this->applyOrganisationFilter(qb: $qb); + + return $this->findEntities(query: $qb); + }//end findAll() + + /** + * Insert a new federated share, stamping uuid/timestamps/organisation. + * + * @param Entity $entity The share to insert. + * + * @return FederatedShare The inserted share. + * + * @SuppressWarnings(PHPMD.StaticAccess) Uuid::v4 is the standard Symfony UID pattern. + */ + public function insert(Entity $entity): FederatedShare + { + if ($entity instanceof FederatedShare) { + if (empty($entity->getUuid()) === true) { + $entity->setUuid((string) Uuid::v4()); + } + + $entity->setCreated(new DateTime()); + $entity->setUpdated(new DateTime()); + } + + $this->setOrganisationOnCreate(entity: $entity); + + return parent::insert(entity: $entity); + }//end insert() + + /** + * Update an existing federated share, refreshing the updated timestamp. + * + * @param Entity $entity The share to update. + * + * @return FederatedShare The updated share. + */ + public function update(Entity $entity): FederatedShare + { + if ($entity instanceof FederatedShare) { + $entity->setUpdated(new DateTime()); + } + + return parent::update(entity: $entity); + }//end update() + + /** + * Create a share from an array of data. + * + * @param array $data The share data. + * + * @return FederatedShare The created share. + */ + public function createFromArray(array $data): FederatedShare + { + $share = new FederatedShare(); + $share->hydrate(object: $data); + + return $this->insert(entity: $share); + }//end createFromArray() + + /** + * Update a share from an array of data. + * + * @param int $id The share id. + * @param array $data The updated share data. + * + * @return FederatedShare The updated share. + */ + public function updateFromArray(int $id, array $data): FederatedShare + { + $share = $this->find(id: $id); + $share->hydrate(object: $data); + + return $this->update(entity: $share); + }//end updateFromArray() +}//end class diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index 93c0413f91..f988777e16 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -481,6 +481,45 @@ public function getFile(int $fileId): ?array return $file; }//end getFile() + /** + * Batch-load multiple files by their fileids in a single query (PERF-1). + * + * Avoids the N+1 pattern of calling getFile() once per file during list rendering. + * Returns a map keyed by the (string) fileid so callers can do O(1) lookups, with + * the same per-file shape (share urls, owner, published, etc.) as getFile(). + * + * @param array $fileIds List of file ids (int|string) to load + * + * @return array Map of (string) fileid => file record + * + * @phpstan-param array $fileIds + * @phpstan-return array + */ + public function getFilesByIds(array $fileIds): array + { + // Normalise to unique positive integers; ignore non-numeric entries. + $normalisedIds = []; + foreach ($fileIds as $fileId) { + if (is_numeric($fileId) === true) { + $normalisedIds[(int) $fileId] = (int) $fileId; + } + } + + if (empty($normalisedIds) === true) { + return []; + } + + // Reuse the existing batch query (single WHERE fileid IN (...)). + $files = $this->getFiles(node: null, ids: array_values($normalisedIds)); + + $filesById = []; + foreach ($files as $file) { + $filesById[(string) $file['fileid']] = $file; + } + + return $filesById; + }//end getFilesByIds() + /** * Get all files for a given ObjectEntity by using its folder property as the node id. * If the folder property is empty, search oc_filecache for a row where name matches the object's uuid. diff --git a/lib/Db/GdprEntityMapper.php b/lib/Db/GdprEntityMapper.php index d541bdbd7c..c357bd1fee 100644 --- a/lib/Db/GdprEntityMapper.php +++ b/lib/Db/GdprEntityMapper.php @@ -24,6 +24,7 @@ use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use Psr\Log\LoggerInterface; /** * Class GdprEntityMapper @@ -44,10 +45,19 @@ class GdprEntityMapper extends QBMapper /** * Constructor. * - * @param IDBConnection $db Database connection. + * @param IDBConnection $db Database connection. + * @param LoggerInterface|null $logger Optional structured log sink. Used by lookups + * that need to surface dedup-invariant violations + * (more than one catalogue row matching the same + * value+type). Nullable so legacy callers that + * constructed the mapper directly without DI + * continue to work; the warning is silently + * dropped in that case. */ - public function __construct(IDBConnection $db) - { + public function __construct( + IDBConnection $db, + private readonly ?LoggerInterface $logger=null + ) { parent::__construct(db: $db, tableName: 'openregister_entities', entityClass: GdprEntity::class); }//end __construct() @@ -82,4 +92,69 @@ public function find(int $id): GdprEntity return $this->findEntity(query: $qb); }//end find() + + /** + * Look up a catalogue entry by its (value, type) pair. + * + * Used by `ManualEntityService` to implement the lookup-or-create + * catalogue dedup invariant: a `(value, type)` pair that already + * exists is REUSED on subsequent manual-entity adds so the same + * placeholder ID survives across documents. + * + * The query selects up to two rows. If two are returned the dedup + * invariant has been violated (most likely by a parallel manual- + * add race per design §D4); we log a warning and return the first + * row. We do NOT throw — the worst case is two near-identical + * catalogue rows, neither destructive, and the caller has a usable + * entity to proceed with. + * + * @param string $value Operator-supplied entity value (case- and whitespace-sensitive). + * @param string $type Entity type tag (e.g. `PERSON`, `ORGANIZATION`). + * + * @return GdprEntity|null The matching entity, or null when no row matches. + */ + public function findOneByValueAndType(string $value, string $type): ?GdprEntity + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->andX( + $qb->expr()->eq('value', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR)), + $qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_STR)) + ) + ) + // Deterministic ordering so that when the dedup invariant is + // violated (two rows for one (value, type) — design §D4) the + // SAME canonical row wins on every call. Without this the + // storage engine's arbitrary order could flip the chosen row + // between retries, yielding unstable `[: ]` placeholders. + ->orderBy('id', 'ASC') + ->setMaxResults(2); + + $matches = $this->findEntities(query: $qb); + if (empty($matches) === true) { + return null; + } + + if (count($matches) > 1 && $this->logger !== null) { + // Catalogue dedup invariant violated — log structurally so + // operators can audit it. PII-safe: we log the type + the + // colliding ids (not the value, per ADR-005). Value can be + // re-derived from the row in the catalogue audit log if a + // forensic step is needed. + $this->logger->warning( + '[GdprEntityMapper] findOneByValueAndType: multiple catalogue rows match the same (value, type) — dedup invariant violated.', + [ + 'file' => __FILE__, + 'line' => __LINE__, + 'type' => $type, + 'collidingIds' => array_map(static fn (GdprEntity $e): int => (int) $e->getId(), $matches), + ] + ); + } + + return $matches[0]; + + }//end findOneByValueAndType() }//end class diff --git a/lib/Db/HandoffQueueEntry.php b/lib/Db/HandoffQueueEntry.php new file mode 100644 index 0000000000..59662a8b05 --- /dev/null +++ b/lib/Db/HandoffQueueEntry.php @@ -0,0 +1,269 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * HandoffQueueEntry entity — one parked (or drained) queue-mode handoff. + * + * @method string getSourceObjectUuid() + * @method void setSourceObjectUuid(string $sourceObjectUuid) + * @method int getSourceRegister() + * @method void setSourceRegister(int $sourceRegister) + * @method int getSourceSchema() + * @method void setSourceSchema(int $sourceSchema) + * @method string getHandoffId() + * @method void setHandoffId(string $handoffId) + * @method string getTargetKind() + * @method void setTargetKind(string $targetKind) + * @method string getRequestingUser() + * @method void setRequestingUser(string $requestingUser) + * @method string getCorrelationId() + * @method void setCorrelationId(string $correlationId) + * @method string|null getMappingHash() + * @method void setMappingHash(?string $mappingHash) + * @method string getStatus() + * @method void setStatus(string $status) + * @method int getAttempt() + * @method void setAttempt(int $attempt) + * @method string|null getLastError() + * @method void setLastError(?string $lastError) + * @method DateTime getCreated() + * @method void setCreated(DateTime $created) + * @method DateTime|null getLastAttemptAt() + * @method void setLastAttemptAt(?DateTime $lastAttemptAt) + * @method DateTime|null getExecutedAt() + * @method void setExecutedAt(?DateTime $executedAt) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ +class HandoffQueueEntry extends Entity implements JsonSerializable +{ + + /** + * Entry is waiting for a providing schema to be installed. + * + * @var string + */ + public const STATUS_PARKED = 'parked'; + + /** + * Entry was drained successfully (target object created). + * + * @var string + */ + public const STATUS_EXECUTED = 'executed'; + + /** + * Requester lost create permission at drain time (no escalation). + * + * @var string + */ + public const STATUS_FAILED_PERMISSION = 'failed-permission'; + + /** + * The mapped object was rejected by the resolved target schema. + * + * @var string + */ + public const STATUS_FAILED_VALIDATION = 'failed-validation'; + + /** + * Entry withdrawn before a provider appeared. + * + * @var string + */ + public const STATUS_CANCELLED = 'cancelled'; + + /** + * UUID of the source object whose handoff was parked. + * + * @var string + */ + protected string $sourceObjectUuid = ''; + + /** + * Register id of the source object. + * + * @var integer + */ + protected int $sourceRegister = 0; + + /** + * Schema id of the source object. + * + * @var integer + */ + protected int $sourceSchema = 0; + + /** + * The declared handoff entry id (dialect `id`). + * + * @var string + */ + protected string $handoffId = ''; + + /** + * The target kind URI the handoff waits on. + * + * @var string + */ + protected string $targetKind = ''; + + /** + * The user who requested the handoff; drain re-evaluates RBAC as this + * user (never a privilege-escalation time capsule). + * + * @var string + */ + protected string $requestingUser = ''; + + /** + * Correlation id minted at park time, echoed on audit rows + the event. + * + * @var string + */ + protected string $correlationId = ''; + + /** + * Hash of the mapping snapshot at park time (drift observability). + * + * @var string|null + */ + protected ?string $mappingHash = null; + + /** + * Entry status (see STATUS_* constants). + * + * @var string + */ + protected string $status = self::STATUS_PARKED; + + /** + * Drain attempt counter (append-only semantics per the WebhookLog pattern). + * + * @var integer + */ + protected int $attempt = 0; + + /** + * Last drain error, when a drain attempt failed. + * + * @var string|null + */ + protected ?string $lastError = null; + + /** + * Park timestamp. + * + * @var DateTime + */ + protected DateTime $created; + + /** + * Timestamp of the most recent drain attempt. + * + * @var DateTime|null + */ + protected ?DateTime $lastAttemptAt = null; + + /** + * Timestamp of successful execution (drain). + * + * @var DateTime|null + */ + protected ?DateTime $executedAt = null; + + /** + * Constructor — declares column types and stamps the park timestamp. + * + * @return void + */ + public function __construct() + { + $this->addType(fieldName: 'sourceObjectUuid', type: 'string'); + $this->addType(fieldName: 'sourceRegister', type: 'integer'); + $this->addType(fieldName: 'sourceSchema', type: 'integer'); + $this->addType(fieldName: 'handoffId', type: 'string'); + $this->addType(fieldName: 'targetKind', type: 'string'); + $this->addType(fieldName: 'requestingUser', type: 'string'); + $this->addType(fieldName: 'correlationId', type: 'string'); + $this->addType(fieldName: 'mappingHash', type: 'string'); + $this->addType(fieldName: 'status', type: 'string'); + $this->addType(fieldName: 'attempt', type: 'integer'); + $this->addType(fieldName: 'lastError', type: 'string'); + $this->addType(fieldName: 'created', type: 'datetime'); + $this->addType(fieldName: 'lastAttemptAt', type: 'datetime'); + $this->addType(fieldName: 'executedAt', type: 'datetime'); + + $this->created = new DateTime(); + + }//end __construct() + + /** + * JSON serialize the entry (availability endpoint `queued` state). + * + * @return array The serialized entry. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Handoff REST surface) + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'sourceObjectUuid' => $this->sourceObjectUuid, + 'sourceRegister' => $this->sourceRegister, + 'sourceSchema' => $this->sourceSchema, + 'handoffId' => $this->handoffId, + 'targetKind' => $this->targetKind, + 'requestingUser' => $this->requestingUser, + 'correlationId' => $this->correlationId, + 'status' => $this->status, + 'attempt' => $this->attempt, + 'lastError' => $this->lastError, + 'created' => $this->created->format('c'), + 'lastAttemptAt' => $this->lastAttemptAt?->format('c'), + 'executedAt' => $this->executedAt?->format('c'), + ]; + + }//end jsonSerialize() +}//end class diff --git a/lib/Db/HandoffQueueEntryMapper.php b/lib/Db/HandoffQueueEntryMapper.php new file mode 100644 index 0000000000..c16a249697 --- /dev/null +++ b/lib/Db/HandoffQueueEntryMapper.php @@ -0,0 +1,194 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * HandoffQueueEntryMapper + * + * @method HandoffQueueEntry insert(Entity $entity) + * @method HandoffQueueEntry update(Entity $entity) + * @method HandoffQueueEntry insertOrUpdate(Entity $entity) + * @method HandoffQueueEntry delete(Entity $entity) + * @method HandoffQueueEntry findEntity(IQueryBuilder $query) + * @method list findEntities(IQueryBuilder $query) + * + * @template-extends QBMapper + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ +class HandoffQueueEntryMapper extends QBMapper +{ + /** + * Constructor for HandoffQueueEntryMapper. + * + * @param IDBConnection $db Database connection. + * + * @return void + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_handoff_queue', entityClass: HandoffQueueEntry::class); + + }//end __construct() + + /** + * Find a queue entry by id. + * + * @param int $id Entry id. + * + * @return HandoffQueueEntry The entry. + * + * @throws \OCP\AppFramework\Db\DoesNotExistException When no entry has the id. + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException Never (id is unique). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + public function find(int $id): HandoffQueueEntry + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity(query: $qb); + + }//end find() + + /** + * All parked entries, oldest first (fallback TimedJob sweep). + * + * @param int|null $limit Optional maximum number of entries. + * + * @return list Parked entries. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + public function findParked(?int $limit=null): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('status', $qb->createNamedParameter(HandoffQueueEntry::STATUS_PARKED))) + ->orderBy('created', 'ASC'); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + + return $this->findEntities(query: $qb); + + }//end findParked() + + /** + * Parked entries waiting on a specific kind URI (schema-save drain). + * + * @param string $kindUri The target kind URI. + * + * @return list Parked entries for the kind, oldest first. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + public function findParkedByKind(string $kindUri): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('status', $qb->createNamedParameter(HandoffQueueEntry::STATUS_PARKED))) + ->andWhere($qb->expr()->eq('target_kind', $qb->createNamedParameter($kindUri))) + ->orderBy('created', 'ASC'); + + return $this->findEntities(query: $qb); + + }//end findParkedByKind() + + /** + * The distinct kind URIs that currently have parked entries. + * + * @return array Distinct parked kind URIs. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + public function findParkedKinds(): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->selectDistinct('target_kind') + ->from($this->getTableName()) + ->where($qb->expr()->eq('status', $qb->createNamedParameter(HandoffQueueEntry::STATUS_PARKED))); + + $result = $qb->executeQuery(); + $kinds = []; + while (($row = $result->fetch()) !== false) { + $kinds[] = (string) $row['target_kind']; + } + + $result->closeCursor(); + + return $kinds; + + }//end findParkedKinds() + + /** + * Parked entries for one source object (availability `queued` state). + * + * @param string $sourceObjectUuid The source object's UUID. + * + * @return list Parked entries for the object. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Handoff REST surface) + */ + public function findParkedForObject(string $sourceObjectUuid): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('status', $qb->createNamedParameter(HandoffQueueEntry::STATUS_PARKED))) + ->andWhere($qb->expr()->eq('source_object_uuid', $qb->createNamedParameter($sourceObjectUuid))) + ->orderBy('created', 'ASC'); + + return $this->findEntities(query: $qb); + + }//end findParkedForObject() +}//end class diff --git a/lib/Db/MagicMapper.php b/lib/Db/MagicMapper.php index c9f6015790..3796589262 100644 --- a/lib/Db/MagicMapper.php +++ b/lib/Db/MagicMapper.php @@ -171,6 +171,26 @@ class MagicMapper extends AbstractObjectMapper */ private const METADATA_PREFIX = '_'; + /** + * Number of magic tables probed per UNION ALL query in the locate phase. + * + * `findAcrossAllMagicTables` used to issue one `SELECT *` per magic table, + * which on a live instance with ~1,000 live tables meant ~1,000 queries for + * a SINGLE bare-identifier lookup. The locate phase now probes tables in + * chunks of this size with one narrow UNION ALL query per chunk. + * + * Why 100: + * - Bounds the SQL text (~100 branches, ~20KB) and the bind-parameter count + * (4 per branch = 400), far below the PostgreSQL 65535-parameter limit and + * any practical MySQL packet limit. + * - Bounds the blast radius of a structurally broken table: a chunk whose + * UNION fails degrades to a per-table probe for THAT chunk only (100 + * queries worst case) instead of poisoning the whole scan. + * - Larger chunks buy little: 1,000 tables need 10 queries at 100/chunk vs + * 2 at 500/chunk, while quadrupling the fallback cost and the SQL size. + */ + private const MAGIC_TABLE_SCAN_CHUNK_SIZE = 100; + /** * Cache timeout for table existence checks (5 minutes) * @@ -185,6 +205,18 @@ class MagicMapper extends AbstractObjectMapper */ public const MAX_TABLE_NAME_LENGTH = 64; + /** + * Property-column budget for a cross-schema UNION projection. + * + * Postgres caps a SELECT target list at 1664 entries. Each UNION arm also + * projects ~30 metadata columns plus a couple of synthetic columns, so the + * property superset must stay well under 1664. When it exceeds this budget + * the cross-schema search falls back to a metadata-only projection so the + * query never trips SQLSTATE 54011 ("target lists can have at most 1664 + * entries"), regardless of how many schemas are searched. + */ + private const UNION_PROPERTY_COLUMN_BUDGET = 1500; + /** * Cache for table existence to avoid repeated database queries * Key format: 'registerId_schemaId' => timestamp @@ -217,6 +249,15 @@ class MagicMapper extends AbstractObjectMapper */ private static array $calcVersionCache = []; + /** + * PERF-12: request-scoped memo of the column→property map per schema id, + * used by rowToObjectEntity(). Without it the schema was re-resolved and + * the map rebuilt for every single row in fallback hydration. + * + * @var array> + */ + private array $rowColumnToPropertyCache = []; + /** * Cache recording whether a register+schema table's columns have been verified * in the current PHP process. Eliminates repeated information_schema queries @@ -295,13 +336,6 @@ class MagicMapper extends AbstractObjectMapper */ private ?bool $hasPgTrgm = null; - /** - * Count of constructor calls. - * - * @var integer - */ - private static int $constructCount = 0; - /** * Constructor for MagicMapper service. * @@ -337,23 +371,67 @@ public function __construct( private readonly SettingsService $settingsService, private readonly ContainerInterface $container ) { - self::$constructCount++; - if (self::$constructCount > 2) { - // Guard against the circular-construction case under - // investigation (#1564). When tripped the mapper is left - // without handlers — operations against it will fail - // explicitly rather than recursing infinitely. - $this->logger->warning( - '[MagicMapper] circular construction guard hit (count={count}) — handlers not initialized', - ['count' => self::$constructCount, 'app' => 'openregister'] - ); - return; - } - // Initialize specialized handlers for modular functionality. + // + // The circular-construction guard that lived here previously (#1564) + // was a holdover from a March 2026 refactor; the cycles it guarded + // (CacheHandler→RegisterMapper→MagicMapper, SettingsService→MagicMapper) + // were since broken via lazy container resolution + the + // `objectMapper`-removed-from-SettingsService fix. + // Verification trail for future readers (PR #1431 review minor): + // - CacheHandler→RegisterMapper cycle: `CacheHandler` no longer + // eagerly constructs a `RegisterMapper`; it resolves via its + // injected `ContainerInterface $container` at first use. + // - SettingsService→MagicMapper cycle: `SettingsService` no + // longer carries the `$objectMapper` field that previously + // transitively pulled in `MagicMapper` at construction. + // Both paths can be re-verified by inspection: + // git grep -nE 'new RegisterMapper|RegisterMapper \$' lib/Service/Object/CacheHandler.php + // git grep -nE '\$objectMapper|MagicMapper \$' lib/Service/SettingsService.php + // If a future change re-introduces either pattern, restoring a + // static `$constructCount` is the documented escape hatch — see + // issue #1564 for the original trigger conditions. $this->initializeHandlers(); }//end __construct() + /** + * Get the configured database table prefix. + * + * Reads `dbtableprefix` from the Nextcloud system config once per call, + * defaulting to `oc_`. Centralising this replaces the hardcoded `'oc_'` + * literals that previously broke every raw-SQL path on installs with a + * custom prefix (BUG-DB-3). + * + * @return string The configured table prefix (e.g. `oc_`). + */ + private function getTablePrefix(): string + { + // Fall back to the Nextcloud default when the system value is empty or + // unset. An empty prefix would yield unprefixed table names, breaking + // magic-table discovery and every raw-SQL table reference. + $prefix = (string) $this->config->getSystemValue('dbtableprefix', 'oc_'); + if ($prefix === '') { + return 'oc_'; + } + + return $prefix; + }//end getTablePrefix() + + /** + * Build the full, prefixed table name for a bare magic table name. + * + * Uses the configured `dbtableprefix` rather than a hardcoded `oc_` so + * raw-SQL paths work on installs with a custom prefix (BUG-DB-3). + * + * @param string $tableName The bare table name (without prefix). + * + * @return string The full table name including the configured prefix. + */ + private function getFullTableName(string $tableName): string + { + return $this->getTablePrefix().$tableName; + }//end getFullTableName() + /** * Initialize specialized handler instances * @@ -387,14 +465,16 @@ private function initializeHandlers(): void logger: $this->logger, rbacHandler: $this->rbacHandler, organizationHandler: $this->organizationHandler, - schemaTypeConverter: $this->container->get(\OCA\OpenRegister\Service\Object\SchemaTypeConverter::class) + schemaTypeConverter: $this->container->get(\OCA\OpenRegister\Service\Object\SchemaTypeConverter::class), + dateTimeNormalizer: $this->container->get(\OCA\OpenRegister\Service\DateTimeNormalizer::class) ); $this->bulkHandler = new MagicBulkHandler( db: $this->db, logger: $this->logger, eventDispatcher: $this->eventDispatcher, - dateTimeNormalizer: $this->container->get(\OCA\OpenRegister\Service\DateTimeNormalizer::class) + dateTimeNormalizer: $this->container->get(\OCA\OpenRegister\Service\DateTimeNormalizer::class), + config: $this->config ); // CacheHandler and ICacheFactory are resolved lazily via container @@ -405,7 +485,8 @@ private function initializeHandlers(): void cacheHandler: null, cacheFactory: null, searchHandler: $this->searchHandler, - container: $this->container + container: $this->container, + config: $this->config ); $this->tableHandler = new MagicTableHandler( @@ -872,6 +953,10 @@ public function countObjectsInRegisterSchemaTable(array $query, Register $regist ); return $count; + } catch (\OCA\OpenRegister\Exception\EncryptedFieldFilterException $e) { + // Fail loud (design intent): never degrade a filter on an encrypted + // property to a silent count of 0. + throw $e; } catch (Exception $e) { $this->logger->error( message: '[MagicMapper] Failed to count in register+schema table', @@ -1125,6 +1210,32 @@ private function searchAcrossMultipleTablesWithUnion(array $query, array $regist // Each SELECT must include the same columns; schemas that lack a column get NULL AS alias. $allPropertyColumns = $this->collectAllPropertyColumns(registerSchemaPairs: $registerSchemaPairs); + // Postgres caps a SELECT target list at 1664 entries. Each UNION arm + // projects ~30 metadata columns plus this property superset, so with + // many schemas the superset alone blows the cap (SQLSTATE 54011) and + // the entire cross-schema search fails — e.g. the unified-search + // provider returns nothing. When the superset is too large, fall back + // to a metadata-only projection: each arm projects only the constant + // `_`-prefixed metadata columns (uuid / name / description / summary / + // register / schema / …), which is enough to build a result row. The + // search term still matches each table's OWN property columns in the + // WHERE/score clauses (see buildUnionSelectPart); a caller needing full + // property values re-hydrates by id. + $metadataOnly = false; + if (count($allPropertyColumns) > self::UNION_PROPERTY_COLUMN_BUDGET) { + $metadataOnly = true; + $allPropertyColumns = []; + $this->logger->warning( + message: '[MagicMapper] Cross-schema property superset exceeds the column budget; using metadata-only projection', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'budget' => self::UNION_PROPERTY_COLUMN_BUDGET, + 'schemaCount' => count($registerSchemaPairs), + ] + ); + } + // Build a SELECT for each table. foreach ($registerSchemaPairs as $pair) { $register = $pair['register'] ?? null; @@ -1167,7 +1278,8 @@ private function searchAcrossMultipleTablesWithUnion(array $query, array $regist query: $query, schema: $schema, register: $register, - allPropertyColumns: $allPropertyColumns + allPropertyColumns: $allPropertyColumns, + metadataOnly: $metadataOnly ); if ($selectPart !== null) { @@ -1237,7 +1349,7 @@ private function searchAcrossMultipleTablesWithUnion(array $query, array $regist name: $this->sanitizeColumnName(name: $field), isPostgres: $isPostgres ); - } + }//end if $dir = 'ASC'; if (strtoupper($direction) === 'DESC') { @@ -1248,11 +1360,22 @@ private function searchAcrossMultipleTablesWithUnion(array $query, array $regist }//end foreach if (empty($orderClauses) === false) { - $unionSql .= ' ORDER BY '.implode(', ', $orderClauses); + // BUG-DB-4: append a stable tiebreaker so rows that compare equal + // on the requested order keep a deterministic order across pages. + $orderClauses[] = self::METADATA_PREFIX.'uuid ASC'; + $unionSql .= ' ORDER BY '.implode(', ', $orderClauses); + } else { + // BUG-DB-4: no usable order clause survived - fall back to a stable order. + $unionSql .= ' ORDER BY '.self::METADATA_PREFIX.'uuid ASC'; } } else if ($hasSearch === true) { // Default to search score ordering when no _order specified but search is present. - $unionSql .= ' ORDER BY _search_score DESC'; + // BUG-DB-4: tiebreaker keeps equal scores in a deterministic order. + $unionSql .= ' ORDER BY _search_score DESC, '.self::METADATA_PREFIX.'uuid ASC'; + } else { + // BUG-DB-4: no order and no search - LIMIT/OFFSET would otherwise be + // non-deterministic. Order by the stable uuid column. + $unionSql .= ' ORDER BY '.self::METADATA_PREFIX.'uuid ASC'; }//end if // Apply LIMIT/OFFSET to final UNION result. @@ -1300,6 +1423,7 @@ private function searchAcrossMultipleTablesWithUnion(array $query, array $regist * @param Schema $schema Schema entity. * @param Register $register Register entity. * @param array $allPropertyColumns Superset of all property columns across schemas. + * @param bool $metadataOnly Project only metadata columns (no property columns) to keep wide unions under the target-list limit. * * @return string|null SQL SELECT statement or null if table doesn't exist. * @@ -1313,7 +1437,8 @@ private function buildUnionSelectPart( array $query, Schema $schema, Register $register, - array $allPropertyColumns=[] + array $allPropertyColumns=[], + bool $metadataOnly=false ): ?string { $qb = $this->db->getQueryBuilder(); @@ -1322,21 +1447,37 @@ private function buildUnionSelectPart( $isPostgres = stripos($platform::class, 'PostgreSQL') !== false; // Add table prefix. - $fullTableName = 'oc_'.$tableName; + $fullTableName = $this->getFullTableName(tableName: $tableName); // Get metadata column names, checking each exists in this table. // Newer metadata columns (e.g. _tmlo) may not exist in older tables. - // Cast to text for UNION type compatibility (some columns are jsonb, others text). + // Cast to text for UNION type compatibility (some columns are jsonb/json, + // others datetime/bigint/text). Cast syntax is database-specific: + // PostgreSQL uses `col::text`, MariaDB/MySQL requires `CAST(col AS CHAR)`. $metadataColumns = $this->getMetadataColumns(); $selectColumns = []; foreach (array_keys($metadataColumns) as $metaCol) { - if ($this->columnExistsInTable(tableName: $tableName, columnName: $metaCol) === true) { - $selectColumns[] = "{$metaCol}::text AS {$metaCol}"; + $columnInTable = $this->columnExistsInTable( + tableName: $tableName, + columnName: $metaCol + ); + if ($columnInTable === true) { + $colExpr = "CAST({$metaCol} AS CHAR) AS {$metaCol}"; + if ($isPostgres === true) { + $colExpr = "{$metaCol}::text AS {$metaCol}"; + } + + $selectColumns[] = $colExpr; continue; } - $selectColumns[] = "NULL::text AS {$metaCol}"; - } + $colExpr = "NULL AS {$metaCol}"; + if ($isPostgres === true) { + $colExpr = "NULL::text AS {$metaCol}"; + } + + $selectColumns[] = $colExpr; + }//end foreach /* * Every SELECT in the UNION must have identical columns in the same order. @@ -1351,33 +1492,47 @@ private function buildUnionSelectPart( */ $existingColumns = []; - foreach (array_keys($allPropertyColumns) as $columnName) { - $quotedCol = $this->quoteIdentifier( - name: $columnName, - isPostgres: $isPostgres - ); - // Absent column: emit a typed NULL placeholder. - $colExpr = "NULL AS {$quotedCol}"; - if ($isPostgres === true) { - $colExpr = "NULL::text AS {$quotedCol}"; + if ($metadataOnly === true) { + // Metadata-only projection: do NOT project per-schema property + // columns (that superset is what trips Postgres's 1664-column cap). + // Still resolve THIS table's own searchable property columns so the + // term match + relevance score below run against real data — they + // are referenced in WHERE/score expressions, not in the projection. + foreach (array_keys($schema->getProperties() ?? []) as $ownProperty) { + $ownColumn = $this->sanitizeColumnName(name: $ownProperty); + if ($this->columnExistsInTable(tableName: $tableName, columnName: $ownColumn) === true) { + $existingColumns[] = $ownColumn; + } } - - $columnInTable = $this->columnExistsInTable( - tableName: $tableName, - columnName: $columnName - ); - if ($columnInTable === true) { - // Present column: cast to text for cross-schema type compatibility. - $colExpr = "CAST({$quotedCol} AS CHAR) AS {$quotedCol}"; + } else { + foreach (array_keys($allPropertyColumns) as $columnName) { + $quotedCol = $this->quoteIdentifier( + name: $columnName, + isPostgres: $isPostgres + ); + // Absent column: emit a typed NULL placeholder. + $colExpr = "NULL AS {$quotedCol}"; if ($isPostgres === true) { - $colExpr = "{$quotedCol}::text AS {$quotedCol}"; + $colExpr = "NULL::text AS {$quotedCol}"; } - $existingColumns[] = $columnName; - } + $columnInTable = $this->columnExistsInTable( + tableName: $tableName, + columnName: $columnName + ); + if ($columnInTable === true) { + // Present column: cast to text for cross-schema type compatibility. + $colExpr = "CAST({$quotedCol} AS CHAR) AS {$quotedCol}"; + if ($isPostgres === true) { + $colExpr = "{$quotedCol}::text AS {$quotedCol}"; + } - $selectColumns[] = $colExpr; - }//end foreach + $existingColumns[] = $columnName; + } + + $selectColumns[] = $colExpr; + }//end foreach + }//end if $selectColumns[] = "'{$register->getId()}' AS _union_register_id"; $selectColumns[] = "'{$schema->getId()}' AS _union_schema_id"; @@ -1449,8 +1604,21 @@ private function buildUnionSelectPart( // Build WHERE conditions using shared method (single source of truth for filters). // This ensures search, count, and facets all use the same filter logic. // Pass existing columns so property-based search only targets columns in this table. + // + // Scope the `@self.schema` filter to THIS arm's schema: the incoming + // query may carry a large allow-list (every searchable schema), but each + // UNION arm already targets a single schema's table, so re-applying the + // full list as an IN() is redundant and trips the database's + // 1000-element IN-list cap when many schemas are searchable. + $armQuery = $query; + if (isset($armQuery['@self']) === true && is_array($armQuery['@self']) === true + && array_key_exists('schema', $armQuery['@self']) === true + ) { + $armQuery['@self']['schema'] = $schema->getId(); + } + $whereClauses = $this->searchHandler->buildWhereConditionsSql( - query: $query, + query: $armQuery, schema: $schema, existingColumns: $existingColumns ); @@ -1713,9 +1881,7 @@ public function checkTableExistsInDatabase(string $tableName): bool try { // Check if table exists in information_schema. // NOTE: We use raw SQL here because information_schema is a system table. - $prefix = 'oc_'; - // Nextcloud default prefix. - $fullTableName = $prefix.$tableName; + $fullTableName = $this->getFullTableName(tableName: $tableName); // Get database platform to use correct schema check. // MySQL/MariaDB: table_schema = DATABASE(). @@ -1766,6 +1932,11 @@ public function invalidateTableCache(string $cacheKey): void unset(self::$calcVersionCache[$cacheKey]); unset(self::$tableColumnsVerifiedCache[$cacheKey]); + // PERF-12: this instance-scoped map is keyed by schema id (not the + // composite cacheKey), so drop it wholesale on any table-structure + // invalidation to avoid serving a stale column→property map. + $this->rowColumnToPropertyCache = []; + $this->logger->debug( message: '[MagicMapper] Invalidated table cache', context: ['file' => __FILE__, 'line' => __LINE__, 'cacheKey' => $cacheKey] @@ -1906,6 +2077,17 @@ public function buildTableColumnsFromSchema(Schema $schema): array '$id', ]; + // BUG-DB-8: sanitizeColumnName is non-injective (e.g. `firstName` and + // `first_name` both map to `first_name`). Track the column names already + // claimed so two distinct properties can never silently collapse onto the + // same physical column; the second claimant gets a deterministic suffix. + $usedColumnNames = []; + foreach ($columns as $existingColumn) { + if (isset($existingColumn['name']) === true) { + $usedColumnNames[$existingColumn['name']] = true; + } + } + if (is_array($schemaProperties) === true) { foreach ($schemaProperties as $propertyName => $propertyConfig) { // Skip metadata/configuration fields that are not actual properties. @@ -1927,13 +2109,49 @@ public function buildTableColumnsFromSchema(Schema $schema): array continue; } + // Skip properties flagged `x-openregister-encrypted: true` (field-level- + // object-encryption): the value is ciphertext by the time it reaches this + // table sync, so a dedicated typed column would only ever hold an opaque + // string useless for filtering/sorting/faceting. The value still lives in + // the table's `object` JSON blob column; it simply gets no dedicated, + // independently-queryable column. This is what makes the field + // structurally unsearchable server-side (composes with the explicit + // filter-time rejection in MagicSearchHandler::applyObjectFilters()). + if (($propertyConfig['x-openregister-encrypted'] ?? false) === true) { + continue; + } + // Note: Schema properties do NOT conflict with metadata columns. // Metadata columns have '_' prefix, schema properties don't. // Both '_name' (metadata) and 'name' (schema property) can coexist. $column = $this->mapSchemaPropertyToColumn(propertyName: $propertyName, propertyConfig: $propertyConfig); if ($column !== null && $column !== '') { - $columns[$propertyName] = $column; - } + // BUG-DB-8: disambiguate column-name collisions deterministically. + if (isset($usedColumnNames[$column['name']]) === true) { + $base = $column['name']; + $suffix = 1; + $candidate = $base.'_'.$suffix; + while (isset($usedColumnNames[$candidate]) === true) { + $suffix++; + $candidate = $base.'_'.$suffix; + } + + $this->logger->warning( + message: '[MagicMapper] Column name collision after sanitisation; disambiguating', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'propertyName' => $propertyName, + 'collidingColumn' => $base, + 'resolvedColumn' => $candidate, + ] + ); + $column['name'] = $candidate; + }//end if + + $usedColumnNames[$column['name']] = true; + $columns[$propertyName] = $column; + }//end if }//end foreach }//end if @@ -2242,6 +2460,7 @@ private function getLinkedTypeColumns(Schema $schema): array * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.StaticAccess) ObjectHandling::relates() is a pure enum-style helper */ private function mapSchemaPropertyToColumn(string $propertyName, array $propertyConfig): array { @@ -2326,16 +2545,21 @@ private function mapSchemaPropertyToColumn(string $propertyName, array $property if ($handling === null && isset($propertyConfig['items']['oneOf']) === true) { foreach ($propertyConfig['items']['oneOf'] as $oneOfItem) { if (isset($oneOfItem['objectConfiguration']['handling']) === true - && $oneOfItem['objectConfiguration']['handling'] === 'related-object' + && ObjectHandling::relates($oneOfItem['objectConfiguration']['handling']) === true ) { - $handling = 'related-object'; + $handling = $oneOfItem['objectConfiguration']['handling']; $hasRef = isset($oneOfItem['$ref']); break; } } } - if ($type === 'object' && $hasRef === true && $handling === 'related-object') { + // A relating property stores a bare UUID STRING, so it needs a VARCHAR + // column — never a json one. `related-schema` used to miss this branch and + // got a json column, into which the save path then wrote a raw UUID: + // "SQLSTATE[22P02]: invalid input syntax for type json ... Token + // "b25f5f9c" is invalid", surfaced to the user as a bare 403. + if ($type === 'object' && $hasRef === true && ObjectHandling::relates($handling) === true) { // This is a reference to another object - store as UUID string. $this->logger->debug( message: '[MagicMapper] Detected object reference property, using VARCHAR for UUID storage', @@ -2617,21 +2841,26 @@ private function createTable(string $tableName, array $columns): void // Build CREATE TABLE SQL manually for Nextcloud 32 compatibility. $platform = $this->db->getDatabasePlatform(); $isPostgres = ($platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform); + $isSqlite = ($platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform); // Get database table prefix from Nextcloud config. $tablePrefix = $this->config->getSystemValue('dbtableprefix', 'oc_'); $fullTableName = $tablePrefix.$tableName; // Build column definitions. - $columnDefs = []; - $primaryKey = null; - $uniqueConstraints = []; + $columnDefs = []; + $primaryKey = null; + // SQLite auto-increments an INTEGER PRIMARY KEY column declared + // inline; when that path is taken the separate PRIMARY KEY(...) + // table constraint must be suppressed (one primary key only). + $sqliteInlinePrimaryKey = false; + $uniqueConstraints = []; foreach ($columns as $column) { - $colName = '`'.$column['name'].'`'; - if ($isPostgres === true) { - $colName = '"'.$column['name'].'"'; - } + // BUG-DB-7: route the raw column name through quoteIdentifier so + // embedded quote characters are escaped (doubled) rather than + // concatenated unescaped. + $colName = $this->quoteIdentifier(name: $column['name'], isPostgres: $isPostgres); $def = $colName.' '; @@ -2653,7 +2882,10 @@ private function createTable(string $tableName, array $columns): void $defaultValue = 'TRUE'; } } else if (is_string($column['default']) === true) { - $defaultValue = "'".$column['default']."'"; + // BUG-DB-1: escape via the platform so values containing a + // single quote (e.g. O'Brien) do not break the DDL or allow + // stored-DDL injection. + $defaultValue = $platform->quoteStringLiteral($column['default']); } else if ($column['default'] === null) { $defaultValue = 'NULL'; } @@ -2668,6 +2900,13 @@ private function createTable(string $tableName, array $columns): void if ($isPostgres === true) { // PostgreSQL uses BIGSERIAL. $def = $colName.' BIGSERIAL'; + } else if ($isSqlite === true) { + // SQLite auto-increments an INTEGER PRIMARY KEY column + // declared inline; AUTO_INCREMENT is a syntax error and + // a separate PRIMARY KEY(...) constraint would stop the + // column from aliasing rowid. + $def = $colName.' INTEGER PRIMARY KEY AUTOINCREMENT'; + $sqliteInlinePrimaryKey = true; } } @@ -2675,10 +2914,7 @@ private function createTable(string $tableName, array $columns): void // Track primary key. if (($column['primary'] ?? false) === true) { - $primaryKey = '`'.$column['name'].'`'; - if ($isPostgres === true) { - $primaryKey = '"'.$column['name'].'"'; - } + $primaryKey = $this->quoteIdentifier(name: $column['name'], isPostgres: $isPostgres); } // Track unique constraints (required for PostgreSQL ON CONFLICT). @@ -2696,8 +2932,9 @@ private function createTable(string $tableName, array $columns): void $sql = 'CREATE TABLE IF NOT EXISTS '.$tableNameQuoted.' ('; $sql .= implode(', ', $columnDefs); - // Add PRIMARY KEY constraint. - if ($primaryKey !== null) { + // Add PRIMARY KEY constraint (skipped on SQLite when the + // autoincrement column already declared an inline PRIMARY KEY). + if ($primaryKey !== null && $sqliteInlinePrimaryKey === false) { $sql .= ', PRIMARY KEY ('.$primaryKey.')'; } @@ -2710,7 +2947,11 @@ private function createTable(string $tableName, array $columns): void foreach ($uniqueConstraints as $uniqueCol) { $rawCol = trim($uniqueCol, '`"'); $constraintName = $tableName.'_'.ltrim($rawCol, '_').'_uq'; - $quote = $isPostgres === true ? '"' : '`'; + $quote = '`'; + if ($isPostgres === true) { + $quote = '"'; + } + $sql .= ', CONSTRAINT '.$quote.$constraintName.$quote.' UNIQUE ('.$uniqueCol.')'; } @@ -2809,12 +3050,13 @@ private function mapColumnTypeToSQL(string $type, array $column): string * @return void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.StaticAccess) ObjectHandling::relates() is a pure enum-style helper */ private function createTableIndexes(string $tableName, Register $_register, Schema $_schema): void { try { // Add prefix for raw SQL queries. - $fullTableName = 'oc_'.$tableName; + $fullTableName = $this->getFullTableName(tableName: $tableName); // Create unique index on UUID. // Phpcs:ignore Generic.Files.LineLength.TooLong. @@ -2864,12 +3106,78 @@ private function createTableIndexes(string $tableName, Register $_register, Sche $this->db->executeStatement( "CREATE INDEX IF NOT EXISTS {$relationsIdx} ON {$fullTableName} USING GIN (_relations)" ); - } + + // PERF-5: every list/search query filters `_deleted IS NULL`, yet + // there was no index supporting it. Add a partial index on + // (_owner) restricted to live rows so the common + // "WHERE _owner = ? AND _deleted IS NULL" access pattern can use + // an index. Partial indexes are a PostgreSQL feature; MySQL/ + // MariaDB do not support them, so this is Postgres-only. + $deletedCol = self::METADATA_PREFIX.'deleted'; + $ownerColForIdx = self::METADATA_PREFIX.'owner'; + $liveOwnerIdx = "{$tableName}_live_owner_idx"; + $this->db->executeStatement( + "CREATE INDEX IF NOT EXISTS {$liveOwnerIdx} ON {$fullTableName} ({$ownerColForIdx}) WHERE {$deletedCol} IS NULL" + ); + + // Baseline pg_trgm GIN index on the `_name` metadata column. + // `_name` is unconditionally searched by + // MagicSearchHandler::buildSearchConditionSql() for every schema + // (both the always-on ILIKE path and the `_fuzzy=true` similarity() + // path), yet nothing indexed it — a rare-term search forced a + // sequential scan (measured 268ms; ~1.5ms once GIN-backed on an + // 81,950-row table). This index is on an EXISTING varchar/text + // column (no new column type is introduced), so — unlike a + // vector/tsvector-TYPED column — it does NOT break Doctrine + // introspectSchema() over oc_-prefixed tables: a functional/column + // GIN index is invisible to Doctrine's type system, the same reason + // the hybrid-document-search chunk `to_tsvector` functional GIN + // index is safe. Gated by pg_trgm availability and wrapped so a + // missing extension or incompatible column never fails table + // creation (matching the neighbouring index try/catch pattern). + if ($this->hasPgTrgmExtension() === true) { + $nameCol = self::METADATA_PREFIX.'name'; + $nameTrgmIx = "{$tableName}_name_trgm_idx"; + try { + $this->db->executeStatement( + "CREATE INDEX IF NOT EXISTS {$nameTrgmIx} ON {$fullTableName} USING GIN ({$nameCol} gin_trgm_ops)" + ); + } catch (Exception $e) { + $this->logger->info( + message: '[MagicMapper] Skipped baseline _name trgm index', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'tableName' => $tableName, + 'error' => $e->getMessage(), + ] + ); + } + }//end if + } else { + // MySQL/MariaDB do not support partial indexes, so the + // "WHERE _owner = ? AND _deleted IS NULL" access pattern is + // supported here with a composite (_deleted, _owner) index — the + // best available approximation. `CREATE INDEX IF NOT EXISTS` is + // not portable across older MySQL, so create it defensively and + // treat a duplicate-index error as a no-op (idempotent). + $deletedCol = self::METADATA_PREFIX.'deleted'; + $ownerColForIdx = self::METADATA_PREFIX.'owner'; + $deletedOwnerIdx = "{$tableName}_deleted_owner_idx"; + try { + $this->db->executeStatement( + "CREATE INDEX {$deletedOwnerIdx} ON {$fullTableName} ({$deletedCol}, {$ownerColForIdx})" + ); + } catch (\Throwable $e) { + // Index already exists (no IF NOT EXISTS on MySQL) — ignore. + } + }//end if // Create indexes for schema-specific properties. - $schemaProperties = $_schema->getProperties(); - $relationIndexes = []; - $facetIndexes = []; + $schemaProperties = $_schema->getProperties(); + $relationIndexes = []; + $facetIndexes = []; + $searchableIndexes = []; if (is_array($schemaProperties) === true) { foreach ($schemaProperties as $propertyName => $propertyConfig) { @@ -2883,7 +3191,7 @@ private function createTableIndexes(string $tableName, Register $_register, Sche $type = $propertyConfig['type'] ?? 'string'; // Index single object references (stored as VARCHAR UUID). - if ($type === 'object' && $hasRef === true && $handling === 'related-object') { + if ($type === 'object' && $hasRef === true && ObjectHandling::relates($handling) === true) { $idxName = "{$tableName}_{$columnName}_rel_idx"; try { $this->db->executeStatement( @@ -2926,18 +3234,58 @@ private function createTableIndexes(string $tableName, Register $_register, Sche // Index may already exist or column type incompatible. } } + + // Opt-in `searchable: true` flag: create a pg_trgm GIN index on + // this property's column so fuzzy/substring (`_search` / ILIKE / + // similarity()) matching against it is index-backed instead of a + // sequential scan. Structurally identical to `facetable` above, + // but PostgreSQL + pg_trgm gated (the `gin_trgm_ops` operator + // class only exists on PostgreSQL with the extension). Like the + // baseline `_name` index, this indexes an EXISTING text column — + // no new column type — so it is Doctrine-introspection-safe. A + // non-string column (gin_trgm_ops requires a text-castable type) + // is tolerated by the try/catch below, exactly like facetable's + // "column type incompatible" case, and logged rather than fatal. + if (($propertyConfig['searchable'] ?? false) === true + && $isPostgres === true + && $this->hasPgTrgmExtension() === true + ) { + $idxName = "{$tableName}_{$columnName}_trgm_idx"; + try { + $this->db->executeStatement( + "CREATE INDEX IF NOT EXISTS {$idxName} ON {$fullTableName} USING GIN ({$quotedCol} gin_trgm_ops)" + ); + $searchableIndexes[] = $columnName; + } catch (Exception $e) { + // Index may already exist, or the column type is + // incompatible with gin_trgm_ops (e.g. a non-string + // property incorrectly marked searchable): log a warning + // and continue, do not abort table creation/sync. + $this->logger->warning( + message: '[MagicMapper] Skipped searchable trgm index (incompatible column or already present)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'tableName' => $tableName, + 'column' => $columnName, + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end if }//end foreach }//end if $this->logger->debug( message: '[MagicMapper] Created table indexes', context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'tableName' => $tableName, - 'baseIndexCount' => 5 + count($idxMetaFields), - 'relationIndexes' => $relationIndexes, - 'facetIndexes' => $facetIndexes, + 'file' => __FILE__, + 'line' => __LINE__, + 'tableName' => $tableName, + 'baseIndexCount' => 5 + count($idxMetaFields), + 'relationIndexes' => $relationIndexes, + 'facetIndexes' => $facetIndexes, + 'searchableIndexes' => $searchableIndexes, ] ); } catch (Exception $e) { @@ -3072,31 +3420,41 @@ private function prepareObjectDataForTable(array $objectData, Register $register // // Fields handled here: // - // owner – stripped from client @self to prevent ownership hijacking. - // For the internal entity-serialization path the owner was - // stamped by SaveObject::applyOwnerAttribution before - // jsonSerialize() was called. Stripping it here prevents any - // residual client value from persisting. The DB column will - // receive the value set by the entity's setter (not @self). - // Note: this means that for any path that does NOT go through - // applyOwnerAttribution, _owner will be null. That is safer - // than persisting an attacker-controlled value. + // owner – the server-stamped owner (a scalar UID string set by + // SaveObject::applyOwnerAttribution before jsonSerialize()) + // MUST be persisted, otherwise rows land with an empty + // `_owner` column: the creator can never equal the owner, + // so the update lock-guard 403s the owner's own update and + // ownership-based RBAC + owner notifications are neutered. + // Client-supplied owner injection is already closed upstream + // (SaveObject::setSelfMetadata never copies a client `owner` + // into the entity — the entity owner is exclusively the + // server-stamped value). As defence-in-depth at this DB write + // boundary we still REJECT any non-scalar/array owner shape + // (the form a forged @self payload would take) and only keep a + // plain string UID. // - // authorization – per-object RBAC rules must not be writable by ordinary - // object-create/update calls. Management of per-object - // authorization is intentionally limited to the dedicated - // authorization management endpoint. + // authorization – per-object RBAC rules must not be writable by ordinary + // object-create/update calls. Management of per-object + // authorization is intentionally limited to the dedicated + // authorization management endpoint. // - // register / schema – always derived from the authoritative $register and - // $schema method parameters. Unconditional override (extends - // the previous conditional-on-empty guard to be absolute). + // register / schema – always derived from the authoritative $register and + // $schema method parameters. Unconditional override (extends + // the previous conditional-on-empty guard to be absolute). // // The following fields are intentionally NOT stripped here because they carry // legitimate values set by the server before this function is reached: - // version, created, updated, deleted, locked, retention, uuid, slug, uri + // version, created, updated, deleted, locked, retention, uuid, slug, uri // Those fields must be fixed at the SaveObject/controller level if client // injection is possible for any of them. - unset($metadata['owner'], $metadata['authorization']); + unset($metadata['authorization']); + + // Keep only a scalar string owner (the server-stamped UID); drop any + // array/object shape a forged @self payload could carry. + if (isset($metadata['owner']) === true && is_string($metadata['owner']) === false) { + unset($metadata['owner']); + } // Always force register and schema from the authoritative server parameters — // never accept client-supplied values, even when non-empty. @@ -3205,22 +3563,6 @@ private function prepareObjectDataForTable(array $objectData, Register $register // Map schema properties to columns. $schemaProperties = $schema->getProperties(); - // DEBUG: Log schema property mapping for gemmaType. - if ($schema->getSlug() === 'element' && isset($schemaProperties['gemmaType']) === true) { - $this->logger->error( - message: '[MagicMapper] MAGIC_MAPPER_DEBUG: Mapping element properties', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'has_gemmaType_in_schema' => isset($schemaProperties['gemmaType']), - 'has_gemmaType_in_data' => isset($data['gemmaType']), - 'gemmaType_value' => $data['gemmaType'] ?? 'NOT IN DATA', - 'data_keys' => array_keys($data), - 'objectData_keys' => array_keys($objectData), - ] - ); - } - if (is_array($schemaProperties) === true) { foreach (array_keys($schemaProperties) as $propertyName) { // Use array_key_exists to distinguish between:. @@ -3493,7 +3835,30 @@ private function calculateRegisterSchemaVersion(Register $register, Schema $sche { $registerId = $register->getId(); $schemaId = $schema->getId(); - $cacheKey = $this->getCacheKey(registerId: $registerId, schemaId: $schemaId); + + // BUG-DB-13: the request-scoped memo was keyed only by register+schema id, + // so a schema edited within the same request returned the stale hash and + // hasRegisterSchemaChanged() missed the change (table sync skipped). + // Fold the schema/register version + updated timestamp into the memo key + // so an intra-request mutation produces a fresh key (and a fresh hash). + $schemaUpdated = $schema->getUpdated(); + $registerUpdated = $register->getUpdated(); + + $schemaUpdatedStamp = '0'; + if ($schemaUpdated !== null) { + $schemaUpdatedStamp = (string) $schemaUpdated->getTimestamp(); + } + + $registerUpdatedStamp = '0'; + if ($registerUpdated !== null) { + $registerUpdatedStamp = (string) $registerUpdated->getTimestamp(); + } + + $cacheKey = $this->getCacheKey(registerId: $registerId, schemaId: $schemaId) + .'_'.(string) $schema->getVersion() + .'_'.$schemaUpdatedStamp + .'_'.(string) $register->getVersion() + .'_'.$registerUpdatedStamp; // Check cache first to avoid expensive json_encode + md5. if (isset(self::$calcVersionCache[$cacheKey]) === true) { @@ -3638,9 +4003,7 @@ public function getExistingTableColumns(string $tableName): array try { // Use direct SQL query to get table columns (Nextcloud 32 compatible). // NOTE: We use raw SQL here because information_schema is a system table that should not be prefixed. - $prefix = 'oc_'; - // Nextcloud default prefix. - $fullTableName = $prefix.$tableName; + $fullTableName = $this->getFullTableName(tableName: $tableName); // Get database platform to use correct schema check. // MySQL/MariaDB: table_schema = DATABASE(). @@ -3797,6 +4160,38 @@ public function updateTableStructure(string $tableName, array $currentColumns, a ]; }//end updateTableStructure() + /** + * Identify required columns whose physical column is absent from the live table. + * + * Required columns are keyed by schema PROPERTY name (camelCase) while + * getExistingTableColumns() keys by PHYSICAL column name (snake_case). A raw + * array_diff_key() between the two therefore reports every multi-word + * camelCase property (e.g. `allowedApps` → `allowed_apps`) as missing on + * every request, forcing a table sync that can never converge. Resolve the + * physical name first — the same resolution addMissingColumns() applies. + * + * @param array $currentColumns Live column definitions keyed by physical name (see + * getExistingTableColumns()). + * @param array $requiredColumns Required column definitions keyed by property name (see + * buildTableColumnsFromSchema()). + * + * @return array Missing column definitions keyed by property name. + * + * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-13 + */ + public function findMissingColumns(array $currentColumns, array $requiredColumns): array + { + $missing = []; + foreach ($requiredColumns as $propertyName => $columnDef) { + $columnName = ($columnDef['name'] ?? $this->sanitizeColumnName(name: $propertyName)); + if (isset($currentColumns[$columnName]) === false) { + $missing[$propertyName] = $columnDef; + } + } + + return $missing; + }//end findMissingColumns() + /** * Identify object-typed columns still backed by PostgreSQL JSONB that need retyping * to JSON for key-order preservation (issue #1720). @@ -3844,6 +4239,132 @@ public function findJsonbColumnsNeedingRetype(array $currentColumns, array $requ return $needsRetype; }//end findJsonbColumnsNeedingRetype() + /** + * Find columns that must become a UUID reference but are still stored as JSON. + * + * When a property is turned INTO a relation (its handling becomes related-schema / + * related-object), it stops holding a document and starts holding a bare UUID + * string. A pre-existing `json` / `jsonb` column then rejects every write: + * + * SQLSTATE[22P02]: invalid input syntax for type json + * DETAIL: Token "b25f5f9c" is invalid. + * + * Without this the table is broken FOREVER after such an edit: the sync only ever + * added missing columns and migrated jsonb→json, so it never noticed that an + * existing column's type no longer matches what the property now stores. + * + * @param array $currentColumns Live columns, keyed by physical column name. + * @param array $requiredColumns Desired columns, keyed by property name. + * + * @return array Physical column names needing a JSON → VARCHAR retype. + * + * @spec exclude schema-drift detection for the magic-table sync + */ + public function findRelationColumnsNeedingRetype(array $currentColumns, array $requiredColumns): array + { + $needsRetype = []; + + foreach ($requiredColumns as $propertyName => $columnDef) { + // A relating property is declared as a plain string column (the UUID). + if (($columnDef['type'] ?? null) !== 'string') { + continue; + } + + $columnName = $columnDef['name'] ?? $this->sanitizeColumnName(name: $propertyName); + if (isset($currentColumns[$columnName]) === false) { + continue; + } + + $currentType = strtolower((string) ($currentColumns[$columnName]['type'] ?? '')); + if (in_array($currentType, ['json', 'jsonb'], true) === true) { + $needsRetype[] = $columnName; + } + } + + return $needsRetype; + }//end findRelationColumnsNeedingRetype() + + /** + * Convert JSON columns that now hold a UUID reference to VARCHAR. + * + * Deliberately NON-destructive. The ALTER is attempted inside a try/catch, and a + * failure is logged rather than swallowed or forced: a column that still holds real + * documents (because the property used to be `nested-*`) cannot become a 255-char + * UUID without losing data, and quietly truncating a user's objects to make a schema + * edit "work" would be far worse than the edit failing loudly. + * + * `#>> '{}'` extracts a JSON scalar as text, so a column that already holds only + * UUID strings (or nulls) converts cleanly. + * + * @param string $tableName The magic table. + * @param array $currentColumns Live columns, keyed by physical column name. + * @param array $requiredColumns Desired columns, keyed by property name. + * + * @return array The column names actually retyped. + * + * @spec exclude magic-table DDL sync + */ + public function migrateRelationColumnsToVarchar(string $tableName, array $currentColumns, array $requiredColumns): array + { + $platform = $this->db->getDatabasePlatform(); + if (($platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform) === false) { + return []; + } + + $columnsRetyped = []; + + // The magic tables carry the Nextcloud table prefix (oc_). Quoting the bare + // name yields `relation "openregister_table_..." does not exist`. + $tableNameQuoted = $this->quoteIdentifier( + name: $this->getFullTableName(tableName: $tableName), + isPostgres: true + ); + + $relationColumns = $this->findRelationColumnsNeedingRetype( + currentColumns: $currentColumns, + requiredColumns: $requiredColumns + ); + + foreach ($relationColumns as $columnName) { + $colNameQuoted = $this->quoteIdentifier(name: $columnName, isPostgres: true); + + $sql = 'ALTER TABLE '.$tableNameQuoted; + $sql .= ' ALTER COLUMN '.$colNameQuoted; + $sql .= " TYPE VARCHAR(255) USING ".$colNameQuoted." #>> '{}'"; + + try { + $this->db->executeStatement($sql); + $columnsRetyped[] = $columnName; + $this->logger->info( + message: '[MagicMapper] Retyped relation column from JSON to VARCHAR (it now holds a UUID reference)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'tableName' => $tableName, + 'columnName' => $columnName, + ] + ); + } catch (Exception $e) { + // The column still holds documents that will not fit a UUID. Say so — + // do NOT truncate the user's data to make the schema edit succeed. + $this->logger->warning( + message: '[MagicMapper] Could not retype relation column to VARCHAR; it still holds data ' + .'that is not a UUID reference. Saves to this property will keep failing until ' + .'the existing values are migrated.', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'tableName' => $tableName, + 'columnName' => $columnName, + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end foreach + + return $columnsRetyped; + }//end migrateRelationColumnsToVarchar() + /** * Migrate object-typed JSONB columns to JSON to preserve client-supplied key order. * @@ -3942,11 +4463,13 @@ private function migrateJsonbToJsonForOrderedColumns( */ private function quoteIdentifier(string $name, bool $isPostgres): string { + // BUG-DB-7: escape any embedded quote characters by doubling them so an + // identifier containing the quote char cannot break out of the quoting. if ($isPostgres === true) { - return '"'.$name.'"'; + return '"'.str_replace('"', '""', $name).'"'; } - return '`'.$name.'`'; + return '`'.str_replace('`', '``', $name).'`'; }//end quoteIdentifier() /** @@ -4331,7 +4854,10 @@ private function formatDefaultValueForSQL(mixed $default): string } if (is_string($default) === true) { - return "'".$default."'"; + // BUG-DB-1: escape via the platform so values containing a single + // quote (e.g. O'Brien) do not break the DDL or allow stored-DDL + // injection. + return $this->db->getDatabasePlatform()->quoteStringLiteral($default); } if ($default === null) { @@ -4371,12 +4897,19 @@ public function dropTable(string $tableName): void { try { // Use direct SQL to drop table (Nextcloud 32 compatible). - $qb = $this->db->getQueryBuilder(); - $prefix = 'oc_'; - // Nextcloud default prefix. - $fullTableName = $prefix.$tableName; - $quotedTable = $qb->getConnection()->quoteIdentifier($fullTableName); - $qb->getConnection()->executeStatement('DROP TABLE IF EXISTS '.$quotedTable); + $qb = $this->db->getQueryBuilder(); + $connection = $qb->getConnection(); + $fullTableName = $this->getFullTableName(tableName: $tableName); + + // Quote via the PLATFORM, not the connection: Nextcloud's + // ConnectionAdapter has no quoteIdentifier(), so the previous + // $connection->quoteIdentifier() threw "Call to undefined method" + // on EVERY call — dropTable() could never actually drop anything, + // which is why deleted schemas kept leaving their magic table behind. + // getDatabasePlatform()->quoteIdentifier() is the pattern the rest of + // the codebase already uses (DbalObjectSourceProvider, DatabaseIntrospectionService). + $quotedTable = $connection->getDatabasePlatform()->quoteIdentifier($fullTableName); + $connection->executeStatement('DROP TABLE IF EXISTS '.$quotedTable); // Clear from cache - need to clear by table name pattern. foreach (array_keys(self::$tableExistsCache) as $cacheKey) { @@ -4529,7 +5062,11 @@ public function findInRegisterSchemaTable( ); $qb = $this->db->getQueryBuilder(); - $qb->select('*')->from($tableName); + // Use the table alias `t` so the shared access-control filters + // (which reference t._organisation / t._owner) can be applied — the + // single-object read path MUST enforce the same isolation as the list + // path (cross-org IDOR fix). See applyAccessControlToQuery() below. + $qb->select('*')->from($tableName, 't'); // Build identifier conditions (ID, UUID, slug, or URI). $idParam = -1; @@ -4537,10 +5074,10 @@ public function findInRegisterSchemaTable( $idParam = (int) $identifier; } - $idCol = self::METADATA_PREFIX.'id'; - $uuidCol = self::METADATA_PREFIX.'uuid'; - $slugCol = self::METADATA_PREFIX.'slug'; - $uriCol = self::METADATA_PREFIX.'uri'; + $idCol = 't.'.self::METADATA_PREFIX.'id'; + $uuidCol = 't.'.self::METADATA_PREFIX.'uuid'; + $slugCol = 't.'.self::METADATA_PREFIX.'slug'; + $uriCol = 't.'.self::METADATA_PREFIX.'uri'; $qb->where( $qb->expr()->orX( $qb->expr()->eq($idCol, $qb->createNamedParameter($idParam, IQueryBuilder::PARAM_INT)), @@ -4552,19 +5089,27 @@ public function findInRegisterSchemaTable( // Exclude deleted objects by default (unless includeDeleted is true). if ($includeDeleted === false) { - $qb->andWhere($qb->expr()->isNull(self::METADATA_PREFIX.'deleted')); - } - - // Apply multitenancy filtering if enabled. - // Note: For MagicMapper, we rely on the table structure itself for multitenancy,. - // as the organisation column is part of the schema. The $_multitenancy parameter. - // is primarily used to decide whether to filter at all. - // For now, we skip adding explicit organisation filters in MagicMapper. - // as that's handled by RBAC and the table structure. - // Apply RBAC filtering if enabled. - if ($_rbac === true) { - // Add RBAC filtering logic here if needed. - // Currently skipped as owner/authorization logic is complex. + $qb->andWhere($qb->expr()->isNull('t.'.self::METADATA_PREFIX.'deleted')); + } + + // SECURITY (cross-org IDOR fix): apply the SAME multitenancy + RBAC + // access-control filters the list/search path uses. Previously the + // single-object read skipped org filtering, so a non-admin in org B + // could read an org-A object by id. We now delegate to the search + // handler's shared logic, which: + // - drops the org filter for `public`-read schemas (published reads, + // including anonymous, keep working), + // - lets admins (rbac/multitenancy disabled by the controller) and + // in-org / owner reads through, + // - filters out other-org rows → no row → DoesNotExistException → 404 + // (existence is not leaked). + if ($_rbac === true || $_multitenancy === true) { + $this->searchHandler->applyAccessControlToQuery( + qb: $qb, + schema: $schema, + _rbac: $_rbac, + _multitenancy: $_multitenancy + ); } try { @@ -4607,10 +5152,32 @@ public function findInRegisterSchemaTable( }//end findInRegisterSchemaTable() /** - * Find an object across all magic tables without knowing register/schema upfront. + * Fetch the set of existing integer primary keys from a metadata table. * - * This method searches all existing magic tables for an object by its identifier. - * It's useful for operations like lock/unlock where the caller doesn't know + * Used to filter out orphaned magic tables in findAcrossAllMagicTables: a + * magic table whose register or schema row was deleted can never hold a + * resolvable object, so we skip it instead of querying it. + * + * @param string $table Unprefixed table name (e.g. `openregister_registers`). + * + * @return int[] The `id` values present in the table. + * + * @throws \OCP\DB\Exception If a database error occurs. + */ + private function getExistingIds(string $table): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('id')->from($table); + $ids = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + + return array_map('intval', $ids); + }//end getExistingIds() + + /** + * Find an object across all magic tables without knowing register/schema upfront. + * + * This method searches all existing magic tables for an object by its identifier. + * It's useful for operations like lock/unlock where the caller doesn't know * which storage backend contains the object. * * @param string|int $identifier Object identifier (ID, UUID, slug, or URI). @@ -4638,23 +5205,15 @@ public function findAcrossAllMagicTables( ] ); - // Get all magic tables from information_schema. - // NOTE: We use raw SQL here because the query builder adds the table prefix. - // to information_schema, which is a system schema and shouldn't be prefixed. - $prefix = 'oc_'; - $tablePattern = $prefix.'openregister_table_%'; - - $sql = "SELECT table_name FROM information_schema.tables WHERE table_name LIKE ?"; - $stmt = $this->db->prepare($sql); - $result = $stmt->execute([$tablePattern]); - $tables = $result->fetchAll(); + // Live (non-orphaned) magic tables, keyed by unprefixed table name. + $candidates = $this->getLiveMagicTables(); $this->logger->debug( message: '[MagicMapper] findAcrossAllMagicTables: Found magic tables', context: [ 'file' => __FILE__, 'line' => __LINE__, - 'count' => count($tables), + 'count' => count($candidates), ] ); @@ -4662,7 +5221,135 @@ public function findAcrossAllMagicTables( $registerMapper = \OC::$server->get(RegisterMapper::class); $schemaMapper = \OC::$server->get(SchemaMapper::class); - // Search each magic table. + // `_id` is a bigint column, so a non-numeric identifier (a UUID, slug or URI) + // must never be bound against it — it would type-error on PostgreSQL. -1 is a + // sentinel that can never match a real row, leaving the string columns to match. + $idParam = -1; + if (is_numeric($identifier) === true) { + $idParam = (int) $identifier; + } + + // PERF (N+1): this used to run one `SELECT *` PER magic table — ~1,000 queries + // for a single bare-identifier lookup on a live instance. It is now two phases. + // Phase 1 LOCATE: probe the tables in chunks of MAGIC_TABLE_SCAN_CHUNK_SIZE with + // one UNION ALL query per chunk that selects ONLY (table name, _id). + // Phase 2 FETCH: one `SELECT *` from the single table that actually holds the row. + // Every magic table shares the `_id`/`_uuid`/`_slug`/`_uri`/`_deleted` metadata + // columns, which is what makes the union across heterogeneous tables well-typed. + $chunks = array_chunk($candidates, self::MAGIC_TABLE_SCAN_CHUNK_SIZE, true); + + foreach ($chunks as $chunk) { + $hit = $this->locateIdentifierInMagicTables( + tables: array_keys($chunk), + identifier: (string) $identifier, + idParam: $idParam, + includeDeleted: $includeDeleted + ); + + if ($hit === null) { + continue; + } + + $tableName = $hit['table']; + + // PHASE 2: fetch the full row from the one matching table. + $row = $this->fetchMagicRowById( + tableName: $tableName, + id: $hit['id'], + includeDeleted: $includeDeleted + ); + + if ($row === null) { + // The row vanished between locate and fetch (concurrent hard delete), or + // the table turned out to be unreadable. Keep scanning the other chunks. + continue; + } + + $registerId = $chunk[$tableName]['registerId']; + $schemaId = $chunk[$tableName]['schemaId']; + + // Found the object! Get register and schema entities. + $register = null; + $schema = null; + + try { + $register = $registerMapper->find(id: $registerId, _multitenancy: false); + $schema = $schemaMapper->find(id: $schemaId, _multitenancy: false); + } catch (\Exception $e) { + $this->logger->warning( + message: '[MagicMapper] findAcrossAllMagicTables: Could not load register/schema', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'registerId' => $registerId, + 'schemaId' => $schemaId, + 'error' => $e->getMessage(), + ] + ); + } + + // Convert row to ObjectEntity. + $object = $this->convertRowToObjectEntity( + row: $row, + _register: $register, + _schema: $schema + ); + + $this->logger->debug( + message: '[MagicMapper] findAcrossAllMagicTables: Found object', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $object->getUuid(), + 'registerId' => $registerId, + 'schemaId' => $schemaId, + ] + ); + + return [ + 'object' => $object, + 'register' => $register, + 'schema' => $schema, + ]; + }//end foreach + + // Not found in any magic table. + throw new DoesNotExistException("Object with identifier '$identifier' not found in any magic table"); + }//end findAcrossAllMagicTables() + + /** + * List the live (non-orphaned) magic tables on this instance. + * + * A table `{prefix}openregister_table_{registerId}_{schemaId}` is only a live + * source when BOTH its register row and its schema row still exist; deleting a + * register or schema leaves its magic table behind, and dev/CI instances + * accumulate thousands of these. A stored object always lives in a table whose + * register AND schema rows exist, so filtering to live ids is coverage-safe. + * + * @return array Unprefixed table + * name => parent ids. + * + * @throws \OCP\DB\Exception If a database error occurs. + */ + private function getLiveMagicTables(): array + { + // Get all magic tables from information_schema. + // NOTE: We use raw SQL here because the query builder adds the table prefix. + // to information_schema, which is a system schema and shouldn't be prefixed. + $prefix = $this->getTablePrefix(); + $tablePattern = $prefix.'openregister_table_%'; + + $sql = "SELECT table_name FROM information_schema.tables WHERE table_name LIKE ?"; + $stmt = $this->db->prepare($sql); + $result = $stmt->execute([$tablePattern]); + $tables = $result->fetchAll(); + + // PERF: skip orphaned magic tables instead of querying them. + $validRegisterIds = array_flip($this->getExistingIds(table: 'openregister_registers')); + $validSchemaIds = array_flip($this->getExistingIds(table: 'openregister_schemas')); + + $liveTables = []; + foreach ($tables as $tableRow) { $fullTableName = $tableRow['table_name'] ?? $tableRow['TABLE_NAME'] ?? null; if ($fullTableName === null) { @@ -4678,23 +5365,192 @@ public function findAcrossAllMagicTables( $registerId = (int) $matches[1]; $schemaId = (int) $matches[2]; + // Orphaned table (register or schema row deleted) — cannot hold a + // resolvable object; skip without querying it. + if (isset($validRegisterIds[$registerId]) === false || isset($validSchemaIds[$schemaId]) === false) { + continue; + } + + $liveTables[$tableName] = [ + 'registerId' => $registerId, + 'schemaId' => $schemaId, + ]; + }//end foreach + + return $liveTables; + }//end getLiveMagicTables() + + /** + * Locate which magic table in a chunk holds an identifier (phase 1 of the scan). + * + * Probes every table in the chunk with a SINGLE `UNION ALL` query instead of one + * query per table. + * + * MEMORY SAFETY: each union branch selects only two scalar columns — the table + * name and the bigint `_id` — and the whole union is capped with `LIMIT 1`. It + * never selects full object rows. A `SELECT *` union across every magic table + * materialises every matching object body at once and has previously caused an + * out-of-memory event on this codebase; the narrow projection plus the chunking + * plus the LIMIT keep the result set at a single tiny row regardless of how many + * tables exist. + * + * @param string[] $tables Unprefixed magic table names to probe. + * @param string $identifier The identifier to match against _uuid/_slug/_uri. + * @param int $idParam Numeric identifier for the bigint _id column, or -1. + * @param bool $includeDeleted Whether to include soft-deleted objects. + * + * @return array{table: string, id: int}|null The matching table and row id, or null. + */ + private function locateIdentifierInMagicTables( + array $tables, + string $identifier, + int $idParam, + bool $includeDeleted + ): ?array { + if (empty($tables) === true) { + return null; + } + + $prefix = $this->getTablePrefix(); + $platform = $this->db->getDatabasePlatform(); + $isPostgres = stripos($platform::class, 'PostgreSQL') !== false; + + $idCol = $this->quoteIdentifier(name: self::METADATA_PREFIX.'id', isPostgres: $isPostgres); + $uuidCol = $this->quoteIdentifier(name: self::METADATA_PREFIX.'uuid', isPostgres: $isPostgres); + $slugCol = $this->quoteIdentifier(name: self::METADATA_PREFIX.'slug', isPostgres: $isPostgres); + $uriCol = $this->quoteIdentifier(name: self::METADATA_PREFIX.'uri', isPostgres: $isPostgres); + $deletedCol = $this->quoteIdentifier(name: self::METADATA_PREFIX.'deleted', isPostgres: $isPostgres); + + // Exclude deleted unless requested. + $deletedCondition = ' AND '.$deletedCol.' IS NULL'; + if ($includeDeleted === true) { + $deletedCondition = ''; + } + + $branches = []; + + foreach ($tables as $tableName) { + // SAFETY GATE: only ever embed a table name that matches the strict magic + // table pattern. Names originate from information_schema and are re-validated + // here, so neither the FROM clause nor the src_table literal can be injected. + // The identifier itself is NEVER interpolated — it is always bound (below). + if (preg_match('/^openregister_table_(\d+)_(\d+)$/', $tableName) !== 1) { + continue; + } + + $quotedTable = $this->quoteIdentifier(name: $prefix.$tableName, isPostgres: $isPostgres); + + $branches[] = sprintf( + "(SELECT '%s' AS src_table, %s AS hit_id FROM %s WHERE (%s = ? OR %s = ? OR %s = ? OR %s = ?)%s)", + $tableName, + $idCol, + $quotedTable, + $idCol, + $uuidCol, + $slugCol, + $uriCol, + $deletedCondition + ); + }//end foreach + + if (empty($branches) === true) { + return null; + } + + $sql = implode(' UNION ALL ', $branches).' LIMIT 1'; + + try { + $stmt = $this->db->prepare($sql); + $branchCount = count($branches); + $position = 1; + + // Bind 4 parameters per branch, in branch order. The _id placeholder is bound + // as a true integer so a UUID never reaches the bigint column. + for ($branch = 0; $branch < $branchCount; $branch++) { + $stmt->bindValue($position, $idParam, IQueryBuilder::PARAM_INT); + $position++; + $stmt->bindValue($position, $identifier, IQueryBuilder::PARAM_STR); + $position++; + $stmt->bindValue($position, $identifier, IQueryBuilder::PARAM_STR); + $position++; + $stmt->bindValue($position, $identifier, IQueryBuilder::PARAM_STR); + $position++; + } + + $result = $stmt->execute(); + $row = $result->fetch(); + $result->closeCursor(); + } catch (\Exception $e) { + // One structurally broken table fails the whole chunk query. Degrade to a + // per-table probe for THIS chunk only, so a single bad table cannot hide the + // other live tables in the chunk (the old code skipped bad tables one by one). + $this->logger->debug( + message: '[MagicMapper] findAcrossAllMagicTables: UNION locate failed, probing chunk per table', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'tables' => count($branches), + 'error' => $e->getMessage(), + ] + ); + + return $this->locateIdentifierPerTable( + tables: $tables, + identifier: $identifier, + idParam: $idParam, + includeDeleted: $includeDeleted + ); + }//end try + + if ($row === false || $row === null) { + return null; + } + + $hitTable = ($row['src_table'] ?? $row['SRC_TABLE'] ?? null); + $hitId = ($row['hit_id'] ?? $row['HIT_ID'] ?? null); + + if ($hitTable === null || $hitId === null) { + return null; + } + + return [ + 'table' => (string) $hitTable, + 'id' => (int) $hitId, + ]; + }//end locateIdentifierInMagicTables() + + /** + * Locate an identifier by probing each magic table separately (chunk fallback). + * + * Used only when the chunked UNION query fails — typically because one table in + * the chunk lacks the expected metadata columns. Mirrors the original per-table + * behaviour: a table that errors is logged and skipped. Still selects only `_id`, + * never the full row. + * + * @param string[] $tables Unprefixed magic table names to probe. + * @param string $identifier The identifier to match against _uuid/_slug/_uri. + * @param int $idParam Numeric identifier for the bigint _id column, or -1. + * @param bool $includeDeleted Whether to include soft-deleted objects. + * + * @return array{table: string, id: int}|null The matching table and row id, or null. + */ + private function locateIdentifierPerTable( + array $tables, + string $identifier, + int $idParam, + bool $includeDeleted + ): ?array { + $idCol = self::METADATA_PREFIX.'id'; + $uuidCol = self::METADATA_PREFIX.'uuid'; + $slugCol = self::METADATA_PREFIX.'slug'; + $uriCol = self::METADATA_PREFIX.'uri'; + $deletedCol = self::METADATA_PREFIX.'deleted'; + + foreach ($tables as $tableName) { try { - // Build query to search this table. - // NOTE: Use $tableName (without prefix) because QueryBuilder adds prefix automatically. + // NOTE: Use the unprefixed name because QueryBuilder adds the prefix. $searchQb = $this->db->getQueryBuilder(); - $searchQb->select('*')->from($tableName); - - // Build identifier conditions. - $idCol = self::METADATA_PREFIX.'id'; - $uuidCol = self::METADATA_PREFIX.'uuid'; - $slugCol = self::METADATA_PREFIX.'slug'; - $uriCol = self::METADATA_PREFIX.'uri'; - $deletedCol = self::METADATA_PREFIX.'deleted'; - - $idParam = -1; - if (is_numeric($identifier) === true) { - $idParam = (int) $identifier; - } + $searchQb->select($idCol)->from($tableName); $idExpr = $searchQb->expr()->eq( $idCol, @@ -4721,55 +5577,18 @@ public function findAcrossAllMagicTables( $searchQb->andWhere($searchQb->expr()->isNull($deletedCol)); } + $searchQb->setMaxResults(1); + $searchResult = $searchQb->executeQuery(); $row = $searchResult->fetch(); $searchResult->closeCursor(); - if ($row !== false) { - // Found the object! Get register and schema entities. - $register = null; - $schema = null; - - try { - $register = $registerMapper->find(id: $registerId, _multitenancy: false); - $schema = $schemaMapper->find(id: $schemaId, _multitenancy: false); - } catch (\Exception $e) { - $this->logger->warning( - message: '[MagicMapper] findAcrossAllMagicTables: Could not load register/schema', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'registerId' => $registerId, - 'schemaId' => $schemaId, - 'error' => $e->getMessage(), - ] - ); - } - - // Convert row to ObjectEntity. - $object = $this->convertRowToObjectEntity( - row: $row, - _register: $register, - _schema: $schema - ); - - $this->logger->debug( - message: '[MagicMapper] findAcrossAllMagicTables: Found object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'uuid' => $object->getUuid(), - 'registerId' => $registerId, - 'schemaId' => $schemaId, - ] - ); - + if ($row !== false && $row !== null && isset($row[$idCol]) === true) { return [ - 'object' => $object, - 'register' => $register, - 'schema' => $schema, + 'table' => $tableName, + 'id' => (int) $row[$idCol], ]; - }//end if + } } catch (\Exception $e) { // Table might not have the expected structure, skip it. $this->logger->debug( @@ -4777,7 +5596,7 @@ public function findAcrossAllMagicTables( context: [ 'file' => __FILE__, 'line' => __LINE__, - 'table' => $fullTableName, + 'table' => $tableName, 'error' => $e->getMessage(), ] ); @@ -4785,9 +5604,68 @@ public function findAcrossAllMagicTables( }//end try }//end foreach - // Not found in any magic table. - throw new DoesNotExistException("Object with identifier '$identifier' not found in any magic table"); - }//end findAcrossAllMagicTables() + return null; + }//end locateIdentifierPerTable() + + /** + * Fetch one full magic-table row by its primary key (phase 2 of the scan). + * + * This is the only place the scan selects `*`, and it runs against exactly ONE + * table — the one the locate phase matched. + * + * @param string $tableName Unprefixed magic table name. + * @param int $id The `_id` primary key of the row to fetch. + * @param bool $includeDeleted Whether to include soft-deleted objects. + * + * @return array|null The row, or null when it is gone/unreadable. + */ + private function fetchMagicRowById(string $tableName, int $id, bool $includeDeleted): ?array + { + $idCol = self::METADATA_PREFIX.'id'; + $deletedCol = self::METADATA_PREFIX.'deleted'; + + try { + // NOTE: Use the unprefixed name because QueryBuilder adds the prefix. + $fetchQb = $this->db->getQueryBuilder(); + $fetchQb->select('*')->from($tableName); + $fetchQb->where( + $fetchQb->expr()->eq( + $idCol, + $fetchQb->createNamedParameter($id, IQueryBuilder::PARAM_INT) + ) + ); + + // Exclude deleted unless requested — the locate phase already applied this, + // re-applying it keeps the semantics exact under concurrent soft deletes. + if ($includeDeleted === false) { + $fetchQb->andWhere($fetchQb->expr()->isNull($deletedCol)); + } + + $fetchQb->setMaxResults(1); + + $fetchResult = $fetchQb->executeQuery(); + $row = $fetchResult->fetch(); + $fetchResult->closeCursor(); + } catch (\Exception $e) { + $this->logger->debug( + message: '[MagicMapper] findAcrossAllMagicTables: Error fetching row', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'table' => $tableName, + 'error' => $e->getMessage(), + ] + ); + + return null; + }//end try + + if ($row === false || $row === null) { + return null; + } + + return $row; + }//end fetchMagicRowById() /** * Find multiple objects by UUIDs across ALL magic tables. @@ -4812,7 +5690,7 @@ public function findMultipleAcrossAllMagicTables( $foundObjects = []; // Get all magic tables from information_schema. - $prefix = 'oc_'; + $prefix = $this->getTablePrefix(); $tablePattern = $prefix.'openregister_table_%'; $sql = "SELECT table_name FROM information_schema.tables WHERE table_name LIKE ?"; @@ -4983,6 +5861,153 @@ public function findMultipleAcrossAllMagicTables( return $foundObjects; }//end findMultipleAcrossAllMagicTables() + /** + * Discover every magic table name (oc_openregister_table__). + * + * @return array Map of full + * table name to its register/schema ids. + */ + private function discoverMagicTables(): array + { + $prefix = $this->getTablePrefix(); + $tablePattern = $prefix.'openregister_table_%'; + + $sql = "SELECT table_name FROM information_schema.tables WHERE table_name LIKE ?"; + $stmt = $this->db->prepare($sql); + $result = $stmt->execute([$tablePattern]); + $tables = $result->fetchAll(); + + $map = []; + foreach ($tables as $tableRow) { + $fullTableName = ($tableRow['table_name'] ?? $tableRow['TABLE_NAME'] ?? null); + if ($fullTableName === null) { + continue; + } + + $bareName = str_replace($prefix, '', $fullTableName); + if (preg_match('/^openregister_table_(\d+)_(\d+)$/', $bareName, $matches) !== 1) { + continue; + } + + $map[$fullTableName] = [ + 'registerId' => (int) $matches[1], + 'schemaId' => (int) $matches[2], + ]; + }//end foreach + + return $map; + }//end discoverMagicTables() + + /** + * Find all soft-deleted objects across ALL magic tables. + * + * Objects live in per-register/schema magic tables, so there is no single + * table to query for `/api/deleted`. This scans every magic table for rows + * whose `_deleted` marker is set and returns them newest-first. Replaces the + * broken register/schema-less `searchObjectsPaginated()` path which always + * fell through to an empty result. + * + * @param int|null $limit Maximum rows to return. + * @param int|null $offset Rows to skip (pagination). + * + * @return ObjectEntity[] Soft-deleted objects across all magic tables. + */ + public function findDeletedAcrossAllMagicTables(?int $limit=null, ?int $offset=null): array + { + $deletedCol = self::METADATA_PREFIX.'deleted'; + $updatedCol = self::METADATA_PREFIX.'updated'; + + // Collect (entity, sortKey) pairs so the global newest-first ordering is + // driven by the RAW `_updated` column value rather than the hydrated + // entity's getUpdated() (which is not always populated and would leave + // the merged set ordered only by table-discovery order, pushing freshly + // deleted rows in high-id tables past the pagination window). + $items = []; + + foreach ($this->discoverMagicTables() as $fullTableName => $info) { + $bareTableName = substr($fullTableName, strlen($this->getTablePrefix())); + try { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($bareTableName) + ->where($qb->expr()->isNotNull($deletedCol)) + ->orderBy($updatedCol, 'DESC'); + + $rows = $qb->executeQuery()->fetchAll(); + foreach ($rows as $row) { + $row['_register'] = (string) $info['registerId']; + $row['_schema'] = (string) $info['schemaId']; + $entity = $this->rowToObjectEntity(row: $row); + if ($entity !== null) { + $entity->setSource('orm'); + $items[] = [ + 'entity' => $entity, + 'sortKey' => (string) ($row[$updatedCol] ?? ''), + ]; + } + } + } catch (\Exception $e) { + $this->logger->debug( + message: '[MagicMapper] findDeletedAcrossAllMagicTables: skipping table', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'table' => $fullTableName, + 'error' => $e->getMessage(), + ] + ); + continue; + }//end try + }//end foreach + + // Newest-first by raw `_updated` value (ISO-ish strings sort lexically). + usort( + $items, + static function (array $first, array $second): int { + return strcmp($second['sortKey'], $first['sortKey']); + } + ); + + $found = array_map(static fn(array $item): ObjectEntity => $item['entity'], $items); + + // Apply pagination after the global merge. + if ($offset !== null || $limit !== null) { + $found = array_slice($found, ($offset ?? 0), $limit); + } + + return $found; + }//end findDeletedAcrossAllMagicTables() + + /** + * Count all soft-deleted objects across ALL magic tables. + * + * @return int Total soft-deleted object count. + */ + public function countDeletedAcrossAllMagicTables(): int + { + $deletedCol = self::METADATA_PREFIX.'deleted'; + $total = 0; + + foreach (array_keys($this->discoverMagicTables()) as $fullTableName) { + $bareTableName = substr($fullTableName, strlen($this->getTablePrefix())); + try { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*', 'cnt')) + ->from($bareTableName) + ->where($qb->expr()->isNotNull($deletedCol)); + + $res = $qb->executeQuery(); + $row = $res->fetch(); + $res->closeCursor(); + $total += (int) ($row['cnt'] ?? 0); + } catch (\Exception $e) { + continue; + } + }//end foreach + + return $total; + }//end countDeletedAcrossAllMagicTables() + /** * Find all objects across ALL magic tables that have the given UUID in their relations. * @@ -5004,8 +6029,10 @@ public function findByRelationAcrossAllMagicTables( $foundObjects = []; + $isPostgres = stripos($this->db->getDatabasePlatform()::class, 'PostgreSQL') !== false; + // Get all magic tables from information_schema. - $prefix = 'oc_'; + $prefix = $this->getTablePrefix(); $tablePattern = $prefix.'openregister_table_%'; $sql = "SELECT table_name FROM information_schema.tables WHERE table_name LIKE ?"; @@ -5044,12 +6071,17 @@ public function findByRelationAcrossAllMagicTables( $deletedCondition = ''; } + $colCast = "CAST({$relationsCol} AS CHAR)"; + if ($isPostgres === true) { + $colCast = "{$relationsCol}::text"; + } + $unionParts[] = sprintf( - "SELECT '%s' AS _source_table, %s AS found_uuid FROM %s WHERE %s::text LIKE ?%s", + "SELECT '%s' AS _source_table, %s AS found_uuid FROM %s WHERE %s LIKE ?%s", $fullTableName, $uuidCol, $fullTableName, - $relationsCol, + $colCast, $deletedCondition ); }//end foreach @@ -5308,7 +6340,12 @@ public function insertObjectEntity( $insertedEntity = $this->findInRegisterSchemaTable( identifier: $uuid, register: $register, - schema: $schema + schema: $schema, + // System re-read of the just-inserted row: bypass access-control + // filtering so the writer always gets its freshly-written entity + // back (the org/owner context is still being established here). + _rbac: false, + _multitenancy: false ); } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { // Fallback: manually set ID if re-fetch fails. @@ -5322,7 +6359,7 @@ public function insertObjectEntity( } $insertedEntity = $entity; - } + }//end try // NOTE: Event dispatching is handled by the public insert/update/delete methods to avoid duplicate events. // Do NOT dispatch ObjectCreatedEvent here. @@ -5353,7 +6390,12 @@ public function updateObjectEntity( $oldObject = $this->findInRegisterSchemaTable( identifier: $entity->getUuid(), register: $register, - schema: $schema + schema: $schema, + // Internal pre-update read of the existing row: bypass + // access-control filtering (authorization for the update is + // enforced upstream by the lock/permission guards). + _rbac: false, + _multitenancy: false ); } @@ -5420,6 +6462,10 @@ public function updateObjectEntity( identifier: $uuid, register: $register, schema: $schema, + // System re-read of the just-updated row: bypass access-control + // filtering so the writer always gets its fresh entity back. + _rbac: false, + _multitenancy: false, includeDeleted: true ); } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { @@ -5477,64 +6523,278 @@ public function deleteObjectEntity( $tableName = $this->getTableNameForRegisterSchema(register: $register, schema: $schema); $uuid = $entity->getUuid(); - if ($uuid === null) { - throw new Exception('Cannot delete object entity without UUID'); + if ($uuid === null) { + throw new Exception('Cannot delete object entity without UUID'); + } + + $this->logger->debug( + message: '[MagicMapper] Deleting object entity from register+schema table', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $uuid, + 'tableName' => $tableName, + 'hardDelete' => $hardDelete, + ] + ); + + if ($hardDelete === true) { + // Hard delete - actually remove from table. + $qb = $this->db->getQueryBuilder(); + $qb->delete($tableName) + ->where($qb->expr()->eq(self::METADATA_PREFIX.'uuid', $qb->createNamedParameter($uuid))); + $qb->executeStatement(); + + $this->logger->info( + message: '[MagicMapper] Hard deleted object from register+schema table', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $uuid, + 'tableName' => $tableName, + ] + ); + } + + if ($hardDelete === false) { + // Soft delete - set _deleted field. + if ($entity->getDeleted() === null || empty($entity->getDeleted()) === true) { + // Mark as deleted using entity method. + $entity->delete($this->userSession, 'Soft deleted via MagicMapper', 30); + } + + // Update entity in table with deleted field set. + $this->updateObjectEntity(entity: $entity, register: $register, schema: $schema); + + $this->logger->info( + message: '[MagicMapper] Soft deleted object in register+schema table', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $uuid, + 'tableName' => $tableName, + ] + ); + } + + // NOTE: Event dispatching is handled by the public insert/update/delete methods to avoid duplicate events. + // Do NOT dispatch ObjectDeletedEvent here. + return $entity; + }//end deleteObjectEntity() + + /** + * Soft-delete multiple pre-resolved objects with ONE UPDATE per magic table. + * + * PERF: the per-object soft-delete path re-serialises and rewrites the whole + * row (updateObjectEntity → saveObjectToRegisterSchemaTable) and re-fetches + * it afterwards — for a cascade of N children that is ~5 queries per child. + * This method batches children of the same (register, schema) pair into a + * single `UPDATE ... SET _deleted = CASE _uuid ... END WHERE _uuid IN (...)` + * statement while preserving the per-object event contract: + * + * - `ObjectUpdatingEvent` is dispatched per entity BEFORE the write; a hook + * that stops propagation skips that entity (parity with the per-object + * path, where HookStoppedException aborted the child's delete). + * - A hook that modifies the payload routes that entity through the + * full-row save so the modification persists (the batched UPDATE only + * writes the deleted marker). + * - `ObjectUpdatedEvent` is dispatched per entity AFTER the write. + * + * The caller must set the `deleted` metadata on every entity beforehand + * (its JSON is what the batched UPDATE writes) — deletion attribution is + * the caller's domain, persistence is this mapper's. + * + * @param array $entities ObjectEntity list to soft-delete + * (validated at runtime); uuid, numeric + * register/schema ids and `deleted` + * metadata must be set on every entity. + * @param array $oldEntities Optional pre-delete snapshots keyed + * by uuid, used as event old-objects. + * Missing snapshots are derived by + * cloning the (mutated) entity. + * + * @return ObjectEntity[] The entities that were actually soft-deleted. + * + * @throws Exception If a batched UPDATE fails for every table. + * + * @psalm-return list + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Hook handling requires per-entity branching + * @SuppressWarnings(PHPMD.NPathComplexity) Hook handling requires per-entity branching + */ + public function softDeleteMultipleObjectEntities(array $entities, array $oldEntities=[]): array + { + if (empty($entities) === true) { + return []; + } + + $groups = []; + $completed = []; + foreach ($entities as $entity) { + if ($entity instanceof ObjectEntity === false) { + throw new Exception('softDeleteMultipleObjectEntities() expects ObjectEntity instances'); + } + + $uuid = $entity->getUuid(); + $registerId = $entity->getRegister(); + $schemaId = $entity->getSchema(); + if ($uuid === null || $uuid === '' + || is_numeric($registerId) === false + || is_numeric($schemaId) === false + ) { + $this->logger->warning( + message: '[MagicMapper] Skipping batched soft delete for entity without uuid/register/schema', + context: ['file' => __FILE__, 'line' => __LINE__, 'uuid' => $uuid] + ); + continue; + } + + $oldEntity = ($oldEntities[$uuid] ?? clone $entity); + + // Pre-save hook — parity with the single-object update path. + $updatingEvent = new ObjectUpdatingEvent(newObject: $entity, oldObject: $oldEntity); + $this->eventDispatcher->dispatchTyped($updatingEvent); + if ($updatingEvent->isPropagationStopped() === true) { + $this->logger->info( + message: '[MagicMapper] Batched soft delete skipped by hook', + context: ['file' => __FILE__, 'line' => __LINE__, 'uuid' => $uuid] + ); + continue; + } + + $modifiedData = $updatingEvent->getModifiedData(); + if (empty($modifiedData) === false) { + // Hook modified the payload — persist via the full-row save so + // the modification is not lost by the marker-only batched UPDATE. + $entity->setObject(array_merge(($entity->getObject() ?? []), $modifiedData)); + if ($this->softDeleteSingleFullRow(entity: $entity) === true) { + $completed[] = ['entity' => $entity, 'old' => $oldEntity]; + } + + continue; + } + + $groupKey = ((int) $registerId).'_'.((int) $schemaId); + $groups[$groupKey]['registerId'] = (int) $registerId; + $groups[$groupKey]['schemaId'] = (int) $schemaId; + $groups[$groupKey]['items'][] = ['entity' => $entity, 'old' => $oldEntity]; + }//end foreach + + foreach ($groups as $group) { + try { + $this->executeBatchSoftDeleteUpdate( + registerId: $group['registerId'], + schemaId: $group['schemaId'], + items: $group['items'] + ); + $completed = array_merge($completed, $group['items']); + } catch (Exception $e) { + $this->logger->error( + message: '[MagicMapper] Batched soft delete UPDATE failed for magic table', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'registerId' => $group['registerId'], + 'schemaId' => $group['schemaId'], + 'count' => count($group['items']), + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end foreach + + $result = []; + foreach ($completed as $item) { + $this->eventDispatcher->dispatchTyped( + new ObjectUpdatedEvent(newObject: $item['entity'], oldObject: $item['old']) + ); + $result[] = $item['entity']; } - $this->logger->debug( - message: '[MagicMapper] Deleting object entity from register+schema table', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'uuid' => $uuid, - 'tableName' => $tableName, - 'hardDelete' => $hardDelete, - ] - ); - - if ($hardDelete === true) { - // Hard delete - actually remove from table. - $qb = $this->db->getQueryBuilder(); - $qb->delete($tableName) - ->where($qb->expr()->eq(self::METADATA_PREFIX.'uuid', $qb->createNamedParameter($uuid))); - $qb->executeStatement(); + return $result; + }//end softDeleteMultipleObjectEntities() - $this->logger->info( - message: '[MagicMapper] Hard deleted object from register+schema table', + /** + * Persist one hook-modified entity via the full-row save path (soft delete). + * + * @param ObjectEntity $entity The (already deleted-marked, hook-modified) entity. + * + * @return bool True when the row was written. + */ + private function softDeleteSingleFullRow(ObjectEntity $entity): bool + { + try { + $register = $this->registerMapper->find((int) $entity->getRegister(), _rbac: false, _multitenancy: false); + $schema = $this->schemaMapper->find((int) $entity->getSchema(), _rbac: false, _multitenancy: false); + $tableName = $this->getTableNameForRegisterSchema(register: $register, schema: $schema); + $this->saveObjectToRegisterSchemaTable( + objectData: $entity->jsonSerialize(), + register: $register, + schema: $schema, + tableName: $tableName + ); + return true; + } catch (Exception $e) { + $this->logger->error( + message: '[MagicMapper] Full-row soft delete failed for hook-modified entity', context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'uuid' => $uuid, - 'tableName' => $tableName, + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $entity->getUuid(), + 'error' => $e->getMessage(), ] ); - } + return false; + }//end try + }//end softDeleteSingleFullRow() - if ($hardDelete === false) { - // Soft delete - set _deleted field. - if ($entity->getDeleted() === null || empty($entity->getDeleted()) === true) { - // Mark as deleted using entity method. - $entity->delete($this->userSession, 'Soft deleted via MagicMapper', 30); - } + /** + * Execute one batched soft-delete UPDATE against a single magic table. + * + * Writes each row's (per-object) deleted-metadata JSON via a parameterised + * `CASE _uuid WHEN ... THEN ... END` expression — one statement per table + * regardless of how many rows it soft-deletes. + * + * @param int $registerId The register id (table-name component). + * @param int $schemaId The schema id (table-name component). + * @param array $items List of ['entity' => ObjectEntity, 'old' => ObjectEntity] pairs. + * + * @return void + * + * @throws \OCP\DB\Exception If the UPDATE fails. + */ + private function executeBatchSoftDeleteUpdate(int $registerId, int $schemaId, array $items): void + { + // Same convention as MagicTableHandler::getTableNameForRegisterSchema() + // (numeric ids never exceed the max table-name length). + $tableName = self::TABLE_PREFIX.$registerId.'_'.$schemaId; + $uuidCol = self::METADATA_PREFIX.'uuid'; + $deletedCol = self::METADATA_PREFIX.'deleted'; - // Update entity in table with deleted field set. - $this->updateObjectEntity(entity: $entity, register: $register, schema: $schema); + $qb = $this->db->getQueryBuilder(); - $this->logger->info( - message: '[MagicMapper] Soft deleted object in register+schema table', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'uuid' => $uuid, - 'tableName' => $tableName, - ] - ); + $caseSql = '(CASE '.$uuidCol; + $uuids = []; + foreach ($items as $item) { + $entity = $item['entity']; + $uuids[] = $entity->getUuid(); + + $caseSql .= ' WHEN '.$qb->createNamedParameter($entity->getUuid()); + $caseSql .= ' THEN '.$qb->createNamedParameter(json_encode($entity->getDeleted())); } - // NOTE: Event dispatching is handled by the public insert/update/delete methods to avoid duplicate events. - // Do NOT dispatch ObjectDeletedEvent here. - return $entity; - }//end deleteObjectEntity() + $caseSql .= ' END)'; + + $qb->update($tableName) + ->set($deletedCol, $qb->createFunction($caseSql)) + ->where( + $qb->expr()->in( + $uuidCol, + $qb->createNamedParameter($uuids, IQueryBuilder::PARAM_STR_ARRAY) + ) + ); + $qb->executeStatement(); + }//end executeBatchSoftDeleteUpdate() /** * Delete all objects belonging to a specific schema from the magic table. @@ -5988,7 +7248,7 @@ public function findByRelationUsingRelationsColumn(string $uuid): array foreach ($tables as $tableName) { try { - $fullTableName = 'oc_'.$tableName; + $fullTableName = $this->getFullTableName(tableName: $tableName); // Search for the UUID as a VALUE within the _relations JSONB. // The _relations column can be either:. @@ -6094,7 +7354,7 @@ public function findByLinkedEntity(Schema $schema, string $columnName, string $e return []; } - $fullTableName = 'oc_'.$tableName; + $fullTableName = $this->getFullTableName(tableName: $tableName); $platform = $this->db->getDatabasePlatform(); $isPostgres = stripos($platform::class, 'PostgreSQL') !== false; @@ -6201,7 +7461,7 @@ public function findByRelationBatchInSchema( // Construct the magic table name directly: openregister_table_{registerId}_{schemaId}. $tableName = self::TABLE_PREFIX.$registerId.'_'.$schemaId; - $fullTableName = 'oc_'.$tableName; + $fullTableName = $this->getFullTableName(tableName: $tableName); // Check if the table exists. if ($this->checkTableExistsInDatabase(tableName: $tableName) === false) { @@ -6262,9 +7522,15 @@ public function findByRelationBatchInSchema( $quotedCol = $this->quoteIdentifier(name: $columnName, isPostgres: $isPostgres); foreach ($uuids as $uuid) { // Match both plain UUID strings and {"value": "uuid"} JSON objects. - $conditions[] = "({$quotedCol} = ? OR {$quotedCol}::text LIKE ?)"; - $params[] = $uuid; - $params[] = '%'.$uuid.'%'; + // PostgreSQL uses ::text cast; MariaDB/MySQL uses CAST(col AS CHAR). + if ($isPostgres === true) { + $conditions[] = "({$quotedCol} = ? OR {$quotedCol}::text LIKE ?)"; + } else { + $conditions[] = "({$quotedCol} = ? OR CAST({$quotedCol} AS CHAR) LIKE ?)"; + } + + $params[] = $uuid; + $params[] = '%'.$uuid.'%'; } } @@ -6387,31 +7653,44 @@ private function findByRelationInTable(string $uuid, string $tableName): array $isPostgres = stripos($platform::class, 'PostgreSQL') !== false; // Construct the full table name with prefix for use in SQL functions. - $fullTableName = 'oc_'.$tableName; - $searchPattern = '%'.$uuid.'%'; + $fullTableName = $this->getFullTableName(tableName: $tableName); try { // Apply multi-tenancy filtering to inverse relationship lookups. [$orgFilter, $orgParams] = $this->buildOrganisationFilterForRelation(); - // For PostgreSQL, use row_to_json to convert entire row to searchable text. - // This approach works reliably for finding UUIDs in any column. - // MySQL/MariaDB: Use JSON_UNQUOTE and CONCAT to search all columns. - // This is a fallback approach - may need adjustment for MySQL. - // MySQL/MariaDB fallback. - $sql = "SELECT * FROM {$fullTableName} WHERE _deleted IS NULL - AND CAST({$fullTableName} AS CHAR) LIKE ? + // BUG-DB-9: the previous MySQL/MariaDB branch used the invalid + // `CAST({$fullTableName} AS CHAR)` (you cannot CAST a table to a + // scalar), so every MariaDB/MySQL call threw and was swallowed at + // debug level, silently resolving inverse relations to nothing. + // Search the `_relations` JSON column for the UUID as a value, the + // same approach findByRelationUsingRelationsColumn() uses. + // MySQL/MariaDB: JSON_SEARCH finds the UUID as a value anywhere + // (works for both object and array relation shapes). + $sql = "SELECT * FROM {$fullTableName} WHERE _deleted IS NULL + AND JSON_SEARCH(_relations, 'one', ?) IS NOT NULL {$orgFilter} LIMIT 100"; + $sqlParams = array_merge([$uuid], $orgParams); if ($isPostgres === true) { - $sql = "SELECT * FROM {$fullTableName} WHERE _deleted IS NULL - AND row_to_json({$fullTableName}.*)::text LIKE ? + // PostgreSQL: handle both object and array relation shapes. + $sql = "SELECT * FROM {$fullTableName} + WHERE (_deleted IS NULL OR _deleted = 'null'::jsonb) + AND ( + (jsonb_typeof(_relations) = 'array' AND _relations @> to_jsonb(?::text)) + OR + (jsonb_typeof(_relations) = 'object' AND EXISTS ( + SELECT 1 FROM jsonb_each_text(_relations) AS kv + WHERE kv.value = ? + )) + ) {$orgFilter} LIMIT 100"; + $sqlParams = array_merge([$uuid, $uuid], $orgParams); } $stmt = $this->db->prepare($sql); - $stmt->execute(array_merge([$searchPattern], $orgParams)); + $stmt->execute($sqlParams); $rows = $stmt->fetchAll(); $this->logger->debug( @@ -6425,7 +7704,9 @@ private function findByRelationInTable(string $uuid, string $tableName): array ] ); } catch (Exception $e) { - $this->logger->debug( + // BUG-DB-9: raise from debug to warning so a genuinely failing + // inverse-relation lookup is visible rather than silently empty. + $this->logger->warning( message: '[MagicMapper] findByRelationInTable query failed', context: [ 'file' => __FILE__, @@ -6475,24 +7756,27 @@ private function getAllMagicMapperTables(): array $platform = $this->db->getDatabasePlatform(); $isPostgres = stripos($platform::class, 'PostgreSQL') !== false; + $prefix = $this->getTablePrefix(); + $tablePattern = $prefix.'openregister_table_%'; + $sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() - AND table_name LIKE 'oc_openregister_table_%'"; + AND table_name LIKE ?"; if ($isPostgres === true) { $sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema() - AND table_name LIKE 'oc_openregister_table_%'"; + AND table_name LIKE ?"; } $stmt = $this->db->prepare($sql); - $stmt->execute(); + $stmt->execute([$tablePattern]); $tables = []; while (($row = $stmt->fetch()) !== false) { - // Remove the 'oc_' prefix to get the table name for query builder. + // Remove the configured prefix to get the table name for query builder. $tableName = $row['table_name']; - if (str_starts_with($tableName, 'oc_') === true) { - $tableName = substr($tableName, 3); + if (str_starts_with($tableName, $prefix) === true) { + $tableName = substr($tableName, strlen($prefix)); } $tables[] = $tableName; @@ -6556,27 +7840,44 @@ private function rowToObjectEntity(array $row): ?ObjectEntity // from their sanitized column names (e.g., 'e_mailadres'). $columnToPropertyMap = []; if (isset($row['_schema']) === true) { - try { - $schema = $this->schemaMapper->find((int) $row['_schema']); - if ($schema !== null) { - $properties = $schema->getProperties() ?? []; - foreach (array_keys($properties) as $propertyName) { - $columnName = $this->sanitizeColumnName(name: $propertyName); - $columnToPropertyMap[$columnName] = $propertyName; + $schemaIdForMap = (int) $row['_schema']; + // PERF-12: reuse the memoised column→property map for this schema id + // instead of re-resolving the schema and rebuilding the map per row. + if (isset($this->rowColumnToPropertyCache[$schemaIdForMap]) === true) { + $columnToPropertyMap = $this->rowColumnToPropertyCache[$schemaIdForMap]; + } else { + try { + $schema = $this->schemaMapper->find($schemaIdForMap); + if ($schema !== null) { + // BUG-DB-8: use the same disambiguated column names the write + // path produces (buildTableColumnsFromSchema), keyed by + // property name, so collision-resolved columns round-trip + // back to their original property instead of being lost. + foreach ($this->buildTableColumnsFromSchema(schema: $schema) as $propertyName => $columnDef) { + // Skip metadata columns (handled separately above). + if (str_starts_with($propertyName, '_') === true) { + continue; + } + + $physicalColumn = $columnDef['name'] ?? $this->sanitizeColumnName(name: $propertyName); + $columnToPropertyMap[$physicalColumn] = $propertyName; + } } - } - } catch (\Exception $e) { - // Schema not found - will fall back to column names as-is. - $this->logger->debug( - message: '[MagicMapper] Could not load schema for property mapping', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaId' => $row['_schema'], - 'error' => $e->getMessage(), - ] - ); - }//end try + + $this->rowColumnToPropertyCache[$schemaIdForMap] = $columnToPropertyMap; + } catch (\Exception $e) { + // Schema not found - will fall back to column names as-is. + $this->logger->debug( + message: '[MagicMapper] Could not load schema for property mapping', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $row['_schema'], + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end if }//end if // Build the object data from non-metadata columns. @@ -6622,7 +7923,7 @@ private function columnExistsInTable(string $tableName, string $columnName): boo { try { // Ensure table name has prefix for information_schema lookup. - $prefix = 'oc_'; + $prefix = $this->getTablePrefix(); $fullTableName = $tableName; if (str_starts_with($tableName, $prefix) === false) { $fullTableName = $prefix.$tableName; @@ -6687,7 +7988,7 @@ private function getResolvedRegisterAndSchema( ): array { if ($register === null && $entity->getRegister() !== null) { try { - $register = $this->registerMapper->find((int) $entity->getRegister(), [], null, true, false); + $register = $this->registerMapper->find(id: (int) $entity->getRegister(), _rbac: false, _multitenancy: false); } catch (Exception $e) { $this->logger->warning( message: '[MagicMapper] Failed to resolve register from entity', @@ -6703,7 +8004,7 @@ private function getResolvedRegisterAndSchema( if ($schema === null && $entity->getSchema() !== null) { try { - $schema = $this->schemaMapper->find((int) $entity->getSchema(), [], null, true, false); + $schema = $this->schemaMapper->find(id: (int) $entity->getSchema(), _rbac: false, _multitenancy: false); } catch (Exception $e) { $this->logger->warning( message: '[MagicMapper] Failed to resolve schema from entity', @@ -6851,6 +8152,20 @@ public function findAll( return []; } + // Thread id and full-text search through to the register+schema search. + // Without this, `ids`/`search` passed to findAll() were silently dropped, + // so an in-request findAll(['ids' => []]) returned + // nothing even though the object exists. Inject them as the reserved + // `_ids`/`_search` query params the search handler understands. + $filters = ($filters ?? []); + if ($ids !== null && empty($ids) === false) { + $filters['_ids'] = $ids; + } + + if ($search !== null && trim($search) !== '') { + $filters['_search'] = $search; + } + $entities = $this->findAllInRegisterSchemaTable( register: $register, schema: $schema, @@ -6885,13 +8200,22 @@ public function findMultiple(array $ids): array * * Searches across all magic tables that belong to the given schema. * - * @param int $schemaId Schema ID. + * PERF-13: the optional $limit/$offset/$filter arguments are pushed down + * into SQL (via findAllInRegisterSchemaTable) so callers that only need a + * bounded/filtered slice no longer pull every row of the schema into PHP. + * They are optional and default to the previous unbounded full-table read, + * so existing callers are unaffected. + * + * @param int $schemaId Schema ID. + * @param int|null $limit Optional maximum number of rows (pushed to SQL). + * @param int|null $offset Optional rows to skip for pagination (pushed to SQL). + * @param array|null $filter Optional filter criteria (pushed to SQL). * * @return ObjectEntity[] * * @psalm-return list */ - public function findBySchema(int $schemaId): array + public function findBySchema(int $schemaId, ?int $limit=null, ?int $offset=null, ?array $filter=null): array { // Find all register+schema pairs that include this schema. $allPairs = $this->getAllRegisterSchemaPairs(); @@ -6908,7 +8232,10 @@ public function findBySchema(int $schemaId): array $entities = $this->findAllInRegisterSchemaTable( register: $register, - schema: $schema + schema: $schema, + limit: $limit, + offset: $offset, + filters: $filter ); $results = array_merge($results, $entities); } catch (\Exception $e) { @@ -7132,6 +8459,70 @@ public function unlockObject(string $uuid): bool return true; }//end unlockObject() + /** + * Restore a soft-deleted object by clearing its `_deleted` marker. + * + * Objects live in per-register/schema magic tables (oc_openregister_table_*), + * NOT in the legacy generic `openregister_objects` table. A direct + * `UPDATE openregister_objects` therefore matches zero rows — which is why + * the previous restore reported success but never un-deleted anything. This + * resolves the object's owning magic table (including deleted rows) and + * sets `_deleted = NULL` on the correct table. + * + * @param string $uuid The UUID (or id/slug) of the deleted object. + * + * @return ObjectEntity The restored object. + * + * @throws \OCP\AppFramework\Db\DoesNotExistException If the object is not found. + * @throws Exception If register/schema context cannot be resolved. + */ + public function restoreObject(string $uuid): ObjectEntity + { + $result = $this->findAcrossAllSources( + identifier: $uuid, + includeDeleted: true, + _rbac: false, + _multitenancy: false + ); + $entity = $result['object']; + $register = $result['register']; + $schema = $result['schema']; + + if ($register === null || $schema === null) { + throw new Exception('Cannot restore object without resolvable register and schema context'); + } + + $tableName = $this->getTableNameForRegisterSchema(register: $register, schema: $schema); + + // Clear the soft-delete marker directly on the owning magic table. + // A native UPDATE avoids the Entity-layer array->null serialisation + // quirk for JSON columns. + $qb = $this->db->getQueryBuilder(); + $qb->update($tableName) + ->set(self::METADATA_PREFIX.'deleted', $qb->createNamedParameter(null, \PDO::PARAM_NULL)) + ->where( + $qb->expr()->eq( + self::METADATA_PREFIX.'uuid', + $qb->createNamedParameter($entity->getUuid()) + ) + ); + $qb->executeStatement(); + + $entity->setDeleted(null); + + $this->logger->info( + message: '[MagicMapper] Restored soft-deleted object in register+schema table', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $entity->getUuid(), + 'tableName' => $tableName, + ] + ); + + return $entity; + }//end restoreObject() + /** * Ultra-fast bulk save operation with automatic routing. * @@ -7646,13 +9037,18 @@ public function searchObjects( register: $register, schema: $schema ); + } catch (\OCA\OpenRegister\Exception\EncryptedFieldFilterException $e) { + // Fail loud (design intent): a filter on an encrypted property must + // reach the caller as a clear error, never a swallowed warning that + // degrades into an empty/zero result. + throw $e; } catch (\Exception $e) { $this->logger->warning( message: '[MagicMapper] Failed to resolve register/schema for search', context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] ); - } - } + }//end try + }//end if $this->logger->warning( message: '[MagicMapper] searchObjects() called without register/schema context', @@ -7696,13 +9092,17 @@ public function countSearchObjects( register: $register, schema: $schema ); + } catch (\OCA\OpenRegister\Exception\EncryptedFieldFilterException $e) { + // Fail loud (design intent): never degrade a filter on an encrypted + // property to a silent count of 0. + throw $e; } catch (\Exception $e) { $this->logger->warning( message: '[MagicMapper] Failed to resolve register/schema for count', context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] ); } - } + }//end if return 0; }//end countSearchObjects() @@ -7776,6 +9176,16 @@ public function getQueryBuilder(): IQueryBuilder */ public function getMaxAllowedPacketSize(): int { + // `SHOW VARIABLES` is a MySQL-ism. On PostgreSQL the statement is a + // syntax error — and although the exception is swallowed here, a + // failed statement inside an OPEN TRANSACTION aborts the whole + // transaction on postgres (SQLSTATE 25P02: "current transaction is + // aborted"), breaking any caller that wraps a save in a transaction + // (e.g. the ADR-051 handoff engine). Only probe on MySQL/MariaDB. + if ($this->db->getDatabaseProvider() !== \OCP\IDBConnection::PLATFORM_MYSQL) { + return 16777216; + } + try { $result = $this->db->executeQuery("SHOW VARIABLES LIKE 'max_allowed_packet'"); $row = $result->fetch(); @@ -7810,6 +9220,8 @@ public function getMaxAllowedPacketSize(): int * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @spec openspec/changes/unified-search-index/specs/unified-search-provider/spec.md */ public function searchObjectsPaginated( array $searchQuery=[], @@ -7837,17 +9249,23 @@ public function searchObjectsPaginated( $registerIds = $searchQuery['@self']['registers'] ?? $searchQuery['_registers'] ?? null; - // Multi-schema search. + // Multi-schema search. Fires whenever a non-empty set of schema ids is + // given — even with NO register filter. The unified-search provider + // passes a searchable-schema set and no register; previously that fell + // through to the (empty) central table and returned nothing. When no + // register filter is present, registerIds is left empty and + // searchObjectsPaginatedMultiSchema resolves each schema's real owning + // register from a schema->register map. See the method docblock's spec tag. $isMultiSchemaSearch = $schemaId === null && $schemaIds !== null && is_array($schemaIds) === true - && count($schemaIds) > 0 - && ($registerId !== null - || ($registerIds !== null - && is_array($registerIds) === true - && count($registerIds) > 0)); + && count($schemaIds) > 0; if ($isMultiSchemaSearch === true) { - $allRegisterIds = [(int) $registerId]; + $allRegisterIds = []; + if ($registerId !== null) { + $allRegisterIds = [(int) $registerId]; + } + if ($registerIds !== null && is_array($registerIds) === true && count($registerIds) > 0) { $allRegisterIds = array_map('intval', $registerIds); } @@ -7863,7 +9281,7 @@ public function searchObjectsPaginated( ids: $ids, uses: $uses ); - } + }//end if // Single schema search. if ($registerId !== null && $schemaId !== null) { @@ -7982,6 +9400,42 @@ public function searchObjectsPaginated( ]; }//end searchObjectsPaginated() + /** + * Extract integer schema ids from a register's `schemas` membership list. + * + * The list may hold ids by value or by key, as ints or numeric strings; + * this normalises all forms to a flat list of distinct integer ids. + * + * @param array $registerSchemas The register's getSchemas()/decoded schemas array. + * + * @return int[] Distinct integer schema ids. + */ + private function extractSchemaIds(array $registerSchemas): array + { + // A plain list (`[4306, 4307]`) carries ids by VALUE; its integer keys + // are positional, not ids. An id-keyed map (`{4310: "Pet"}`) carries + // ids by KEY. Only consider keys for the map shape so a list of + // non-numeric values can never inject positional indices as schema ids. + $isList = array_is_list($registerSchemas); + $ids = []; + foreach ($registerSchemas as $schemaKey => $schemaValue) { + $candidates = [$schemaKey, $schemaValue]; + if ($isList === true) { + $candidates = [$schemaValue]; + } + + foreach ($candidates as $candidate) { + if (is_int($candidate) === true + || (is_string($candidate) === true && ctype_digit($candidate) === true) + ) { + $ids[(int) $candidate] = true; + } + } + } + + return array_keys($ids); + }//end extractSchemaIds() + /** * Search objects across multiple schemas using UNION queries. * @@ -8003,6 +9457,8 @@ public function searchObjectsPaginated( * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @psalm-suppress UnusedParam * Parameters reserved for future per-schema security filtering. + * + * @spec openspec/changes/unified-search-index/specs/unified-search-provider/spec.md */ private function searchObjectsPaginatedMultiSchema( array $searchQuery, @@ -8018,21 +9474,73 @@ private function searchObjectsPaginatedMultiSchema( $registersCache = []; $schemasCache = []; - $registers = []; - foreach ($registerIds as $regId) { + // Build a schema_id -> owning register_id map so each schema is paired + // with its REAL register (correct magic table). A schema with no owning + // register is SKIPPED (logged) rather than forced onto an unrelated + // register, which produced the "Register+schema table does not exist" + // empties. Register ENTITIES are loaded lazily (find()) only for the + // registers actually matched. `$registers` caches them by id. + // + // IMPORTANT: when no register filter is given (unified search passes a + // searchable-schema set only) we read the register->schema membership + // with a DIRECT query, NOT registerMapper::findAll — findAll applies an + // organisation filter (even with _multitenancy:false the trait's active- + // org resolution can collapse the result to a single register), which + // would hide most schemas' owning registers and make cross-schema + // search return nothing. See the method docblock's spec tag. + $registers = []; + $schemaToRegisterId = []; + + if (empty($registerIds) === false) { + foreach ($registerIds as $regId) { + try { + $register = $this->registerMapper->find($regId, _multitenancy: false, _rbac: false); + $registers[$register->getId()] = $register; + $registersCache[$register->getId()] = $register->jsonSerialize(); + $registerSchemas = ($register->getSchemas() ?? []); + if (is_array($registerSchemas) === true) { + foreach ($this->extractSchemaIds(registerSchemas: $registerSchemas) as $sid) { + if (isset($schemaToRegisterId[$sid]) === false) { + $schemaToRegisterId[$sid] = $register->getId(); + } + } + } + } catch (\Exception $e) { + $this->logger->warning( + message: '[MagicMapper] Failed to find register for multi-schema search', + context: ['file' => __FILE__, 'line' => __LINE__, 'registerId' => $regId, 'error' => $e->getMessage()] + ); + } + } + } else { try { - $register = $this->registerMapper->find($regId, _multitenancy: false, _rbac: false); - $registers[$register->getId()] = $register; - $registersCache[$register->getId()] = $register->jsonSerialize(); - } catch (\Exception $e) { + $rqb = $this->db->getQueryBuilder(); + $rqb->select('id', 'schemas')->from('openregister_registers'); + $res = $rqb->executeQuery(); + while (($row = $res->fetch()) !== false) { + $regId = (int) $row['id']; + $schemas = json_decode((string) ($row['schemas'] ?? '[]'), true); + if (is_array($schemas) === false) { + continue; + } + + foreach ($this->extractSchemaIds(registerSchemas: $schemas) as $sid) { + if (isset($schemaToRegisterId[$sid]) === false) { + $schemaToRegisterId[$sid] = $regId; + } + } + } + + $res->closeCursor(); + } catch (\Throwable $e) { $this->logger->warning( - message: '[MagicMapper] Failed to find register for multi-schema search', - context: ['file' => __FILE__, 'line' => __LINE__, 'registerId' => $regId, 'error' => $e->getMessage()] + message: '[MagicMapper] Failed to build schema->register map for multi-schema search', + context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] ); - } - } + }//end try + }//end if - if (empty($registers) === true) { + if (empty($schemaToRegisterId) === true) { return [ 'results' => [], 'total' => 0, @@ -8041,46 +9549,81 @@ private function searchObjectsPaginatedMultiSchema( ]; } + // Each searched schema is paired with its REAL owning register (resolved + // via the schema_id -> register_id map above) so the correct magic table + // is targeted; a schema with no owning register is SKIPPED (logged) + // rather than forced onto an unrelated register, which is what produced + // the "Register+schema table does not exist" empties. Register ENTITIES + // are loaded lazily below — only for the registers actually matched. + // See the method docblock's spec tag. $registerSchemaPairs = []; $totalCount = 0; foreach ($schemaIds as $sId) { - try { - $schema = $this->schemaMapper->find((int) $sId, _multitenancy: false, _rbac: false); - $schemasCache[$schema->getId()] = $schema->jsonSerialize(); - - $matchedRegister = null; - foreach ($registers as $register) { - $registerSchemas = $register->getSchemas() ?? []; - if (is_array($registerSchemas) === true) { - $schemaIdStr = (string) $sId; - $schemaIdInt = (int) $sId; - $inValues = in_array($schemaIdInt, $registerSchemas, false) - || in_array($schemaIdStr, $registerSchemas, false); - $inKeys = array_key_exists($schemaIdInt, $registerSchemas) - || array_key_exists($schemaIdStr, $registerSchemas); - if ($inValues === true || $inKeys === true) { - $matchedRegister = $register; - break; - } + // BUGFIX (pre-existing): the loop body referenced an undefined + // `$schemaIdInt` (a variable name from a DIFFERENT method), so the + // register lookup keyed on null and every schema was skipped. + $schemaIdInt = (int) $sId; + $matchedRegisterId = ($schemaToRegisterId[$schemaIdInt] ?? null); + $matchedRegister = null; + if ($matchedRegisterId !== null) { + if (isset($registers[$matchedRegisterId]) === false) { + // Load the owning register ENTITY lazily (only for registers + // actually matched by a searched schema). find() honours + // _multitenancy:false so it resolves regardless of the + // active organisation; on failure the schema is skipped. + try { + $reg = $this->registerMapper->find($matchedRegisterId, _multitenancy: false, _rbac: false); + $registers[$reg->getId()] = $reg; + $registersCache[$reg->getId()] = $reg->jsonSerialize(); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[MagicMapper] Failed to load owning register for multi-schema search', + context: ['file' => __FILE__, 'line' => __LINE__, 'registerId' => $matchedRegisterId, 'error' => $e->getMessage()] + ); } } - if ($matchedRegister === null) { - $matchedRegister = reset($registers); - } + $matchedRegister = ($registers[$matchedRegisterId] ?? null); + }//end if + + if ($matchedRegister === null) { + // No owning register -> the magic table cannot be resolved; skip + // (logged) instead of guessing a wrong register. + $this->logger->debug( + message: '[MagicMapper] Skipping schema with no resolvable owning register', + context: ['file' => __FILE__, 'line' => __LINE__, 'schemaId' => $schemaIdInt] + ); + continue; + } + + try { + $schema = $this->schemaMapper->find($schemaIdInt, _multitenancy: false, _rbac: false); + $schemasCache[$schema->getId()] = $schema->jsonSerialize(); $registerSchemaPairs[] = ['register' => $matchedRegister, 'schema' => $schema]; $schemaCountQuery = $countQuery; $schemaCountQuery['_rbac'] = $_rbac; $schemaCountQuery['_multitenancy'] = $_multitenancy; + // Scope the per-table count to THIS schema. The incoming query + // may carry a large `@self.schema` allow-list (every searchable + // schema); re-applying it as an IN() on each single-table count + // is redundant — the table IS this schema — and trips the + // database's 1000-element IN-list cap when many schemas are + // searchable. Pin it to this schema's own id. + $schemaCountQuery['@self']['schema'] = $schema->getId(); $schemaCount = $this->countObjectsInRegisterSchemaTable( query: $schemaCountQuery, register: $matchedRegister, schema: $schema ); $totalCount += $schemaCount; + } catch (\OCA\OpenRegister\Exception\EncryptedFieldFilterException $e) { + // Fail loud (design intent): never silently drop a schema from a + // multi-schema search total because its own count hit an + // encrypted-field filter — surface the error to the caller. + throw $e; } catch (\Exception $e) { $this->logger->warning( message: '[MagicMapper] Failed to load schema for multi-schema search', diff --git a/lib/Db/MagicMapper/MagicBulkHandler.php b/lib/Db/MagicMapper/MagicBulkHandler.php index 07de317015..f1be2294b4 100644 --- a/lib/Db/MagicMapper/MagicBulkHandler.php +++ b/lib/Db/MagicMapper/MagicBulkHandler.php @@ -49,6 +49,7 @@ use OCA\OpenRegister\Event\ObjectUpdatedEvent; use OCA\OpenRegister\Service\DateTimeNormalizer; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IDBConnection; use Psr\Log\LoggerInterface; use Symfony\Component\Uid\Uuid; @@ -89,17 +90,34 @@ class MagicBulkHandler * @param LoggerInterface $logger Logger for debugging and error reporting * @param IEventDispatcher $eventDispatcher Event dispatcher for business logic hooks * @param DateTimeNormalizer $dateTimeNormalizer Normaliser for user-supplied datetime input + * @param IConfig $config Nextcloud config for reading the table prefix */ public function __construct( private readonly IDBConnection $db, private readonly LoggerInterface $logger, private readonly IEventDispatcher $eventDispatcher, - private readonly DateTimeNormalizer $dateTimeNormalizer + private readonly DateTimeNormalizer $dateTimeNormalizer, + private readonly IConfig $config ) { // Try to get max_allowed_packet from database configuration. $this->initializeMaxPacketSize(); }//end __construct() + /** + * Build the full, prefixed table name for a bare magic table name. + * + * Uses the configured `dbtableprefix` rather than a hardcoded `oc_` so + * raw-SQL paths work on installs with a custom prefix (BUG-DB-3). + * + * @param string $tableName The bare table name (without prefix). + * + * @return string The full table name including the configured prefix. + */ + private function getFullTableName(string $tableName): string + { + return ((string) $this->config->getSystemValue('dbtableprefix', 'oc_')).$tableName; + }//end getFullTableName() + /** * Prepare objects for dynamic table structure * @@ -133,11 +151,21 @@ private function prepareObjectsForDynamicTable(array $objects, Register $registe // Map metadata to prefixed columns with proper fallbacks. $uuid = $selfData['uuid'] ?? $selfData['id'] ?? $object['id'] ?? Uuid::v4()->toRfc4122(); - $preparedObject['_uuid'] = $uuid; - $preparedObject['_register'] = $register->getId(); - $preparedObject['_schema'] = $schema->getId(); - $preparedObject['_owner'] = $selfData['owner'] ?? $object['owner'] ?? null; - $preparedObject['_organisation'] = $selfData['organisation'] ?? $object['organisation'] ?? null; + $preparedObject['_uuid'] = $uuid; + $preparedObject['_register'] = $register->getId(); + $preparedObject['_schema'] = $schema->getId(); + + // SECURITY (wave-11 SB2 — defense-in-depth layer 2): _owner and _organisation + // MUST already have been stamped with authoritative values by SaveObjects:: + // prepareSingleSchemaObject() (the first strip layer). This second strip + // ensures that any future code path that calls prepareObjectsForDynamicTable() + // directly (e.g. ImportService) cannot slip client-supplied values through. + // $selfData['owner'] and $selfData['organisation'] were already overwritten with + // the session-user and active-organisation values before this point. + // We do NOT fall back to $object['owner'] / $object['organisation'] here because + // those keys come from raw client JSON and must not be trusted. + $preparedObject['_owner'] = $selfData['owner'] ?? null; + $preparedObject['_organisation'] = $selfData['organisation'] ?? null; // Format datetime fields to MySQL-compatible format (Y-m-d H:i:s). $createdValue = $selfData['created'] ?? $object['created'] ?? $now->format('Y-m-d H:i:s'); @@ -243,6 +271,17 @@ private function calculateOptimalChunkSize(array $objects): int */ private function getMaxAllowedPacketSize(): int { + // `SHOW VARIABLES` is a MySQL-ism. On PostgreSQL the statement is a + // syntax error — and although the exception is swallowed here, a + // failed statement inside an OPEN TRANSACTION aborts the whole + // transaction on postgres (SQLSTATE 25P02). This runs from the + // handler CONSTRUCTOR (lazily instantiated mid-save), so it would + // abort any caller-managed transaction wrapping a save (e.g. the + // ADR-051 handoff engine). Only probe on MySQL/MariaDB. + if ($this->db->getDatabaseProvider() !== \OCP\IDBConnection::PLATFORM_MYSQL) { + return 16777216; + } + try { $stmt = $this->db->executeQuery('SHOW VARIABLES LIKE \'max_allowed_packet\''); $result = $stmt->fetch(); @@ -362,15 +401,36 @@ public function bulkUpsert(array $objects, Register $register, Schema $schema, s $allResults = []; // Process each chunk. + // BUG-DB-5: wrap each chunk in a transaction so a mid-chunk failure + // (e.g. a constraint violation on one row) rolls back the whole chunk + // instead of leaving a partial, inconsistent set of rows persisted. foreach ($chunks as $chunkIndex => $chunk) { - $chunkResults = $this->executeUpsertChunk( - chunk: $chunk, - tableName: $tableName, - chunkNumber: ($chunkIndex + 1) - ); + $this->db->beginTransaction(); + try { + $chunkResults = $this->executeUpsertChunk( + chunk: $chunk, + tableName: $tableName, + chunkNumber: ($chunkIndex + 1) + ); + + $this->db->commit(); + } catch (\Throwable $e) { + $this->db->rollBack(); + $this->logger->error( + message: '[MagicBulkHandler] Rolled back bulk upsert chunk after failure', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'chunk' => ($chunkIndex + 1), + 'table' => $tableName, + 'error' => $e->getMessage(), + ] + ); + throw $e; + }//end try $allResults = array_merge($allResults, $chunkResults); - } + }//end foreach // PERFORMANCE: Event dispatching disabled by default. // Dispatching events for 20k+ objects causes 10x slowdown (5000 obj/s -> 500 obj/s). @@ -440,20 +500,30 @@ private function executeUpsertChunk(array $chunk, string $tableName, int $chunkN $seenUuids = []; foreach ($filteredChunk as $objectData) { $uuid = $objectData['_uuid'] ?? null; - if ($uuid !== null) { - // Keep track of the position to allow overwriting. - if (isset($seenUuids[$uuid]) === true) { - // Replace previous occurrence with this one (keep last). - $deduplicatedChunk[$seenUuids[$uuid]] = $objectData; - continue; - } + if ($uuid === null || $uuid === '') { + // BUG-DB-6: previously objects without a _uuid were silently + // dropped. Generate a UUID so the row is persisted instead of + // disappearing without trace. + $uuid = Uuid::v4()->toRfc4122(); + $objectData['_uuid'] = $uuid; + $this->logger->debug( + message: '[MagicBulkHandler] Generated missing _uuid for bulk object', + context: ['file' => __FILE__, 'line' => __LINE__, 'uuid' => $uuid] + ); + } - // Add new object. - $index = count($deduplicatedChunk); - $deduplicatedChunk[$index] = $objectData; - $seenUuids[$uuid] = $index; + // Keep track of the position to allow overwriting. + if (isset($seenUuids[$uuid]) === true) { + // Replace previous occurrence with this one (keep last). + $deduplicatedChunk[$seenUuids[$uuid]] = $objectData; + continue; } - } + + // Add new object. + $index = count($deduplicatedChunk); + $deduplicatedChunk[$index] = $objectData; + $seenUuids[$uuid] = $index; + }//end foreach // Re-index array after deduplication. $filteredChunk = array_values($deduplicatedChunk); @@ -477,18 +547,21 @@ private function executeUpsertChunk(array $chunk, string $tableName, int $chunkN $platform = $this->db->getDatabasePlatform(); $isPostgres = $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform; - // Get full table name with hardcoded prefix. - $fullTableName = 'oc_'.$tableName; + // Get full table name with the configured database prefix. + $fullTableName = $this->getFullTableName(tableName: $tableName); // ACCURATE CLASSIFICATION: Query which UUIDs already exist BEFORE the upsert. // This allows us to correctly classify created vs updated regardless of timestamp values. // Important for CSV imports that preserve historical _created dates. + // The FULL pre-update rows are fetched (not just `_uuid`) so callers can + // record a real old-vs-new changeset in the audit trail for updates. $existingUuids = []; + $preUpdateRows = []; if (empty($uuids) === false) { $placeholders = implode(',', array_fill(0, count($uuids), '?')); - $existsSql = "SELECT `_uuid` FROM `{$fullTableName}` WHERE `_uuid` IN ({$placeholders})"; + $existsSql = "SELECT * FROM `{$fullTableName}` WHERE `_uuid` IN ({$placeholders})"; if ($isPostgres === true) { - $existsSql = "SELECT \"_uuid\" FROM \"{$fullTableName}\" WHERE \"_uuid\" IN ({$placeholders})"; + $existsSql = "SELECT * FROM \"{$fullTableName}\" WHERE \"_uuid\" IN ({$placeholders})"; } try { @@ -497,6 +570,7 @@ private function executeUpsertChunk(array $chunk, string $tableName, int $chunkN $existingRows = $existsStmt->fetchAll(); foreach ($existingRows as $row) { $existingUuids[$row['_uuid']] = true; + $preUpdateRows[$row['_uuid']] = $row; } $this->logger->debug( @@ -534,12 +608,27 @@ private function executeUpsertChunk(array $chunk, string $tableName, int $chunkN foreach ($columns as $column) { $paramName = 'p'.$paramIndex; $rowValues[] = ':'.$paramName; - $parameters[$paramName] = $objectData[$column] ?? null; + + $value = $objectData[$column] ?? null; + // The execute() call binds every parameter as PARAM_STR, which + // turns PHP `false` into '' — PostgreSQL then rejects '' for + // boolean columns (e.g. a dedicated per-schema table's + // `pass_through`), 500ing the whole bulk save. Normalise + // booleans to 0/1, which both PostgreSQL and MySQL accept for + // boolean columns. + if (is_bool($value) === true) { + $value = 0; + if ($objectData[$column] === true) { + $value = 1; + } + } + + $parameters[$paramName] = $value; $paramIndex++; - } + }//end foreach $valuesClause[] = '('.implode(',', $rowValues).')'; - } + }//end foreach // Build UPSERT SQL ($fullTableName already defined above for pre-upsert UUID check). // MySQL/MariaDB: INSERT...ON DUPLICATE KEY UPDATE. @@ -653,6 +742,11 @@ private function executeUpsertChunk(array $chunk, string $tableName, int $chunkN $obj['object_status'] = 'updated'; $unchangedCount--; $updatedCount++; + // Retain the pre-update row so the caller can write a + // real old-vs-new changeset into the audit trail. + if (isset($preUpdateRows[$objUuid]) === true) { + $obj['_pre_update_row'] = $preUpdateRows[$objUuid]; + } } } @@ -693,8 +787,8 @@ private function getTableColumns(string $tableName): array $platform = $this->db->getDatabasePlatform(); $isPostgres = $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform; - // Get full table name with hardcoded prefix. - $fullTableName = 'oc_'.$tableName; + // Get full table name with the configured database prefix. + $fullTableName = $this->getFullTableName(tableName: $tableName); // MySQL/MariaDB: use SHOW COLUMNS. $sql = "SHOW COLUMNS FROM `$fullTableName`"; diff --git a/lib/Db/MagicMapper/MagicFacetHandler.php b/lib/Db/MagicMapper/MagicFacetHandler.php index 4645a2974f..7a1f1b24c9 100644 --- a/lib/Db/MagicMapper/MagicFacetHandler.php +++ b/lib/Db/MagicMapper/MagicFacetHandler.php @@ -48,6 +48,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\ICache; use OCP\ICacheFactory; +use OCP\IConfig; use OCP\IDBConnection; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -189,6 +190,7 @@ class MagicFacetHandler * @param ICacheFactory|null $cacheFactory Cache factory for distributed caching * @param MagicSearchHandler|null $searchHandler Search handler for shared query building * @param ContainerInterface|null $container Container for lazy resolution + * @param IConfig|null $config Nextcloud config for reading the table prefix */ public function __construct( private readonly IDBConnection $db, @@ -196,7 +198,8 @@ public function __construct( ?\OCA\OpenRegister\Service\Object\CacheHandler $cacheHandler=null, ?ICacheFactory $cacheFactory=null, ?MagicSearchHandler $searchHandler=null, - ?ContainerInterface $container=null + ?ContainerInterface $container=null, + private readonly ?IConfig $config=null ) { $this->cacheHandler = $cacheHandler; $this->cacheFactory = $cacheFactory; @@ -216,6 +219,24 @@ public function __construct( } }//end __construct() + /** + * Get the configured database table prefix. + * + * Uses the injected Nextcloud config so raw-SQL paths work on installs + * with a custom `dbtableprefix`, falling back to `oc_` when config is + * unavailable (BUG-DB-3). + * + * @return string The configured table prefix (e.g. `oc_`). + */ + private function getTablePrefix(): string + { + if ($this->config === null) { + return 'oc_'; + } + + return (string) $this->config->getSystemValue('dbtableprefix', 'oc_'); + }//end getTablePrefix() + /** * Lazily resolve CacheHandler and ICacheFactory from container. * @@ -576,11 +597,26 @@ private function getTermsFacetUnion( return ['type' => 'terms', 'buckets' => []]; } + // BUG-DB-2: $field is interpolated raw into the SQL below (CAST/WHERE/GROUP BY). + // For @self metadata facets it is built straight from request facet keys + // ("_".$rawKey) without going through sanitizeColumnName, so a crafted key + // could inject arbitrary SQL. Validate it is a safe column identifier and + // matches its own sanitized form before using it; otherwise skip the facet + // (empty buckets), mirroring the columnExists guard the other facet paths use. + $safeField = $this->sanitizeColumnName(name: $field); + if ($safeField !== $field || preg_match('/^[a-z_][a-z0-9_]*$/', $field) !== 1) { + $this->logger->warning( + message: '[MagicFacetHandler] Rejected unsafe terms-facet field name', + context: ['file' => __FILE__, 'line' => __LINE__, 'field' => $field] + ); + return ['type' => 'terms', 'buckets' => []]; + } + // Build UNION ALL query with simple GROUP BY. // Array values will come as JSON strings like '["uuid1", "uuid2"]' // and will be post-processed in PHP. $unionParts = []; - $prefix = 'oc_'; + $prefix = $this->getTablePrefix(); // Cast field to text for consistent types in UNION ALL queries. // Different magic tables may have the same field name but different types @@ -596,6 +632,12 @@ private function getTermsFacetUnion( $fullTableName = $prefix.$tableName; $tcSchema = $tc['schema']; + // Defence in depth: skip tables that do not actually have this column + // (mirrors getDateHistogramFacetUnion's columnExists guard). + if ($this->columnExists(tableName: $tableName, columnName: $field) === false) { + continue; + } + // Simple SELECT with GROUP BY - no jsonb_array_elements_text complexity. $subSql = "SELECT {$castField} as facet_value, COUNT(*) as cnt FROM {$fullTableName} WHERE {$field} IS NOT NULL"; @@ -818,7 +860,7 @@ private function getDateHistogramFacetUnion( } $unionParts = []; - $prefix = 'oc_'; + $prefix = $this->getTablePrefix(); foreach ($tableConfigs as $tc) { $tableName = $tc['tableName']; @@ -1463,8 +1505,8 @@ private function columnExists(string $tableName, string $columnName): bool { try { // The table name passed may or may not include the prefix. - // Normalize to always have the 'oc_' prefix for information_schema lookup. - $prefix = 'oc_'; + // Normalize to always have the configured prefix for information_schema lookup. + $prefix = $this->getTablePrefix(); $fullTableName = $prefix.$tableName; if (str_starts_with($tableName, $prefix) === true) { $fullTableName = $tableName; diff --git a/lib/Db/MagicMapper/MagicOrganizationHandler.php b/lib/Db/MagicMapper/MagicOrganizationHandler.php index 2c99ba334f..ca8d9c3239 100644 --- a/lib/Db/MagicMapper/MagicOrganizationHandler.php +++ b/lib/Db/MagicMapper/MagicOrganizationHandler.php @@ -99,6 +99,13 @@ public function applyOrganizationFilter( ): void { $user = $this->userSession->getUser(); + // CLI / no-session system context (occ commands, repair steps, cron + // jobs, background calculations, system listeners) is a trusted system + // operation and must see all org-owned rows. See isSystemContext(). + if ($this->isSystemContext(user: $user) === true) { + return; + } + // Check if user is admin - admins can see all objects including those with null organization. $isAdmin = false; if ($user !== null) { @@ -181,6 +188,34 @@ public function applyOrganizationFilter( ); }//end applyOrganizationFilter() + /** + * Determine whether the current call is a trusted system (CLI/no-session) + * context that must bypass org filtering. + * + * OCC commands, repair steps, cron jobs, background calculations and system + * listeners have no user session. Clamping them to `1 = 0` silently empties + * app list views and background calcs (e.g. larpingapp). This mirrors the + * established CLI bypass in MultiTenancyTrait::hasRbacPermission(). SaaS mode + * still enforces the org boundary for genuine multi-tenant deployments. + * + * @param \OCP\IUser|null $user The resolved session user (null in CLI). + * + * @return bool True when the org filter should be bypassed. + */ + private function isSystemContext(?\OCP\IUser $user): bool + { + if ($user !== null || PHP_SAPI !== 'cli' || $this->isSaasModeEnabled() === true) { + return false; + } + + $this->logger->debug( + message: '[MagicOrganizationHandler] CLI/system context — skipping org filter', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + + return true; + }//end isSystemContext() + /** * Get the active organization UUID(s) for the current user * diff --git a/lib/Db/MagicMapper/MagicRbacHandler.php b/lib/Db/MagicMapper/MagicRbacHandler.php index 6f4d3c138e..97688f37b5 100644 --- a/lib/Db/MagicMapper/MagicRbacHandler.php +++ b/lib/Db/MagicMapper/MagicRbacHandler.php @@ -43,6 +43,7 @@ use DateTime; use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Exception\AuthorizationUnresolvableException; use OCA\OpenRegister\Service\ConditionMatcher; use OCA\OpenRegister\Service\Object\PermissionHandler; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -163,8 +164,39 @@ public function applyRbacFilters( return; } + // CLI / no-session system context (occ commands, repair steps, cron + // jobs, background calculations) has no user session. These are trusted + // system operations and bypass RBAC filtering, mirroring the established + // CLI bypass in MultiTenancyTrait::hasRbacPermission(). Without this a + // schema with explicit authorization rules would clamp every CLI query + // to `1 = 0` and hide all rows from background calcs / list views. + if ($user === null && PHP_SAPI === 'cli') { + $this->logger->debug( + message: '[MagicRbacHandler] CLI/system context — bypassing RBAC filter', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return; + } + // Get effective authorization (schema-level, or register cascade). - $authorization = $this->resolveSchemaAuthorization(schema: $schema); + // Fail-closed: an unresolvable authorization clamps the query to the + // IMPOSSIBLE predicate rather than falling through to the open default. + try { + $authorization = $this->resolveSchemaAuthorization(schema: $schema); + } catch (AuthorizationUnresolvableException $e) { + $this->logger->error( + message: '[MagicRbacHandler] Authorization unresolvable; clamping query to deny-all (fail-closed)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schema->getId(), + 'action' => $action, + 'error' => $e->getMessage(), + ] + ); + $qb->andWhere($qb->expr()->eq($qb->createNamedParameter(1), $qb->createNamedParameter(0))); + return; + } // If no authorization is configured, schema is open to all. if (empty($authorization) === true) { @@ -178,13 +210,17 @@ public function applyRbacFilters( // Get authorization rules for this action. $rules = $authorization[$action] ?? []; - // If action is not configured in authorization, it's open to all. + // Fail-closed: a non-empty authorization block that does not list this + // action no longer opens the table. Flow falls through to the owner + // condition below and, when nothing matches, the existing `1 = 0` + // deny-all — so an omitted action yields owner-only rows (or none), + // consistent with PermissionHandler/hasPermission. Admins already + // returned earlier; the empty-block open-default above is unchanged. if (empty($rules) === true) { $this->logger->debug( - message: '[MagicRbacHandler] Action not configured, open access', + message: '[MagicRbacHandler] Action not configured on a non-empty authorization block — failing closed', context: ['file' => __FILE__, 'line' => __LINE__, 'action' => $action] ); - return; } // Build the RBAC filter conditions. @@ -206,13 +242,17 @@ public function applyRbacFilters( ); } + // Resolve whether authenticated users inherit `public` rights once. + $inheritFromPublic = $this->authenticatedInheritsPublic(schema: $schema); + // Process each authorization rule. foreach ($rules as $rule) { $ruleCondition = $this->processAuthorizationRule( qb: $qb, rule: $rule, userGroups: $userGroups, - userId: $userId + userId: $userId, + inheritFromPublic: $inheritFromPublic ); if ($ruleCondition === true) { @@ -246,30 +286,99 @@ public function applyRbacFilters( $qb->andWhere($qb->expr()->orX(...$conditions)); }//end applyRbacFilters() + /** + * Resolve whether authenticated users inherit `public` group rights for a schema. + * + * Delegates to PermissionHandler::resolveInheritFromPublic() (cascade: + * schema → register → IAppConfig → true) via the container so the SQL + * emitters and the PHP-side check honour the flag identically. Fails safe + * to `true` (the pre-flag behaviour) if resolution errors. + * + * @param Schema $schema The schema being filtered. + * + * @return bool True when authenticated users inherit `public` rights. + * + * @spec openspec/changes/rbac-disable-public-inheritance/tasks.md + */ + private function authenticatedInheritsPublic(Schema $schema): bool + { + try { + return $this->container->get(PermissionHandler::class)->resolveInheritFromPublic(schema: $schema); + } catch (\Throwable $e) { + // Fail to the configured tenant posture rather than a hard-coded + // grant, so a cluster-wide lock-down (rbac.inherit_from_public_default + // = false) is not silently softened on this code path. Symmetric with + // PermissionHandler::resolveInheritFromPublic()'s own fail-safe. + $fallback = true; + try { + $fallback = $this->container->get(PermissionHandler::class)->inheritFromPublicTenantDefault(); + } catch (\Throwable $inner) { + $fallback = $this->appConfig->getValueBool( + app: 'openregister', + key: 'rbac.inherit_from_public_default', + default: true + ); + } + + $this->logger->error( + message: '[MagicRbacHandler] inheritFromPublic resolution failed; falling back to tenant default', + context: ['file' => __FILE__, 'line' => __LINE__, 'default' => $fallback, 'error' => $e->getMessage()] + ); + return $fallback; + }//end try + }//end authenticatedInheritsPublic() + + /** + * Decide whether the current principal qualifies for a `public` rule. + * + * Anonymous users (no userId) always qualify for `public`. Authenticated + * users qualify only when public inheritance is enabled for the schema. + * + * @param string|null $userId Current user ID (null = anonymous). + * @param bool $inheritFromPublic Resolved inheritFromPublic value. + * + * @return bool True when the principal qualifies for the `public` group. + */ + private function qualifiesForPublic(?string $userId, bool $inheritFromPublic): bool + { + return $userId === null || $inheritFromPublic === true; + }//end qualifiesForPublic() + /** * Process a single authorization rule * - * @param IQueryBuilder $qb Query builder - * @param mixed $rule Authorization rule (string or array) - * @param array $userGroups User's group IDs - * @param string|null $userId Current user ID + * @param IQueryBuilder $qb Query builder + * @param mixed $rule Authorization rule (string or array) + * @param array $userGroups User's group IDs + * @param string|null $userId Current user ID + * @param bool $inheritFromPublic Whether auth users inherit public rights * * @return mixed True if unconditional access, SQL expression for conditional, null/false if no access + * + * @spec openspec/specs/rbac-zaaktype/spec.md */ private function processAuthorizationRule( IQueryBuilder $qb, mixed $rule, array $userGroups, - ?string $userId + ?string $userId, + bool $inheritFromPublic ): mixed { // Simple rule: just a group name string. if (is_string($rule) === true) { - return $this->processSimpleRule(rule: $rule, userGroups: $userGroups, userId: $userId); + return $this->processSimpleRule(rule: $rule, userGroups: $userGroups, userId: $userId, inheritFromPublic: $inheritFromPublic); } - // Conditional rule: object with 'group' and optional 'match'. - if (is_array($rule) === true && isset($rule['group']) === true) { - return $this->processConditionalRule(qb: $qb, rule: $rule, userGroups: $userGroups, userId: $userId); + // Conditional rule: object with 'group' (or a 'user' override) and + // optional 'match'. + if (is_array($rule) === true && (isset($rule['group']) === true || isset($rule['user']) === true)) { + return $this->processConditionalRule( + qb: $qb, + rule: $rule, + userGroups: $userGroups, + userId: $userId, + inheritFromPublic: $inheritFromPublic + ); } // Invalid rule format. @@ -283,17 +392,21 @@ private function processAuthorizationRule( /** * Process a simple (unconditional) authorization rule * - * @param string $rule Group name - * @param array $userGroups User's group IDs - * @param string|null $userId Current user ID + * @param string $rule Group name + * @param array $userGroups User's group IDs + * @param string|null $userId Current user ID + * @param bool $inheritFromPublic Whether auth users inherit public rights * * @return bool True if user has access, false otherwise + * + * @spec openspec/specs/rbac-zaaktype/spec.md */ - private function processSimpleRule(string $rule, array $userGroups, ?string $userId): bool + private function processSimpleRule(string $rule, array $userGroups, ?string $userId, bool $inheritFromPublic): bool { - // 'public' grants access to anyone, including unauthenticated users. + // 'public' grants access to anonymous users always; authenticated users + // only when public inheritance is enabled (inheritFromPublic). if ($rule === 'public') { - return true; + return $this->qualifiesForPublic(userId: $userId, inheritFromPublic: $inheritFromPublic); } // 'authenticated' grants access to any logged-in user. @@ -301,6 +414,14 @@ private function processSimpleRule(string $rule, array $userGroups, ?string $use return $userId !== null; } + // User-level override (delegation): a bare `user:` rule grants the + // action to that one user on the LIST path, mirroring PermissionHandler's + // single-object verdict (rbac-zaaktype). Fail closed: anonymous users and + // non-matching uids never qualify. Additive — never widens group access. + if ($this->matchesUserOverride(rule: $rule, userId: $userId) === true) { + return true; + } + // Check if user is in the specified group. if (in_array($rule, $userGroups, true) === true) { return true; @@ -309,33 +430,71 @@ private function processSimpleRule(string $rule, array $userGroups, ?string $use return false; }//end processSimpleRule() + /** + * Determine whether a bare authorization rule is a `user:` override + * that grants access to the current user. + * + * Shared by the QueryBuilder, raw-SQL, and multitenancy-bypass paths so the + * three LIST emitters agree with PermissionHandler's single-object verdict. + * SECURITY: exact-uid match only; anonymous principals never match; the + * `user:` prefix with a blank uid never matches. + * + * @param string $rule Authorization rule string. + * @param string|null $userId Current user ID (null = anonymous). + * + * @return bool True when the rule is a user override for the current user. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + private function matchesUserOverride(string $rule, ?string $userId): bool + { + if ($userId === null || $userId === '') { + return false; + } + + if (str_starts_with($rule, 'user:') === false) { + return false; + } + + $targetUid = substr($rule, strlen('user:')); + return $targetUid !== '' && $targetUid === $userId; + }//end matchesUserOverride() + /** * Process a conditional authorization rule * - * @param IQueryBuilder $qb Query builder - * @param array $rule Rule with 'group' and optional 'match' - * @param array $userGroups User's group IDs - * @param string|null $userId Current user ID + * @param IQueryBuilder $qb Query builder + * @param array $rule Rule with 'group' and optional 'match' + * @param array $userGroups User's group IDs + * @param string|null $userId Current user ID + * @param bool $inheritFromPublic Whether auth users inherit public rights * * @return mixed True if unconditional access, SQL expression for conditional, false if no access + * + * @spec openspec/specs/rbac-zaaktype/spec.md */ private function processConditionalRule( IQueryBuilder $qb, array $rule, array $userGroups, - ?string $userId + ?string $userId, + bool $inheritFromPublic ): mixed { - $group = $rule['group']; + $group = ($rule['group'] ?? null); $match = $rule['match'] ?? null; - // Check if user qualifies for this group. + // Check if user qualifies for this group OR is the named user override. $userQualifies = false; - if ($group === 'public') { - // Public group means anyone can access, including unauthenticated users. - $userQualifies = true; + if (isset($rule['user']) === true && is_string($rule['user']) === true) { + // User-level override (delegation): { "user": "", "match": {...} }. + $userQualifies = $this->matchesUserOverride(rule: 'user:'.$rule['user'], userId: $userId); + } else if ($group === 'public') { + // Anonymous users always qualify for public; authenticated users + // only when public inheritance is enabled (inheritFromPublic). + $userQualifies = $this->qualifiesForPublic(userId: $userId, inheritFromPublic: $inheritFromPublic); } else if ($group === 'authenticated' && $userId !== null) { $userQualifies = true; - } else if (in_array($group, $userGroups, true) === true) { + } else if ($group !== null && in_array($group, $userGroups, true) === true) { $userQualifies = true; } @@ -704,6 +863,8 @@ private function propertyToColumnName(string $property): string * @return bool True if user has permission * * @SuppressWarnings(PHPMD.NPathComplexity) Inlined dispatch keeps the ConditionMatcher delegation in one place. + * + * @spec openspec/specs/rbac-zaaktype/spec.md */ public function hasPermission( Schema $schema, @@ -741,9 +902,11 @@ public function hasPermission( // Get authorization rules for this action. $rules = $authorization[$action] ?? []; - // If action not configured, everyone has access. + // Fail-closed: a non-empty authorization block that does not list this + // action denies it (consistent with PermissionHandler). The empty-block + // open-default above and the admin/owner bypasses still apply. if (empty($rules) === true) { - return true; + return false; } // Process each rule. @@ -754,20 +917,35 @@ public function hasPermission( // (applyRbacFilters → buildMatchConditions → buildPropertyCondition) // handles the row-level path; ConditionMatcher handles the PHP-side // path for this method and for PermissionHandler/PropertyRbacHandler. + // Resolve whether authenticated users inherit `public` rights once; + // anonymous users always qualify for `public`. + $inheritFromPublic = $this->authenticatedInheritsPublic(schema: $schema); + foreach ($rules as $rule) { - // Simple string rule: direct group match. + // Simple string rule: direct group match or `user:` override. if (is_string($rule) === true) { - if ($rule === 'public' || in_array($rule, $userGroups, true) === true) { + if (($rule === 'public' && $this->qualifiesForPublic(userId: $userId, inheritFromPublic: $inheritFromPublic) === true) + || $this->matchesUserOverride(rule: $rule, userId: $userId) === true + || in_array($rule, $userGroups, true) === true + ) { return true; } continue; } - // Conditional rule: array with 'group' and optional 'match'. - if (is_array($rule) === true && isset($rule['group']) === true) { - $group = $rule['group']; - $userQualifies = ($group === 'public' || in_array($group, $userGroups, true) === true); + // Conditional rule: array with 'group' (or 'user' override) and optional 'match'. + if (is_array($rule) === true && (isset($rule['group']) === true || isset($rule['user']) === true)) { + if (isset($rule['user']) === true && is_string($rule['user']) === true) { + $userQualifies = $this->matchesUserOverride(rule: 'user:'.$rule['user'], userId: $userId); + } else { + $group = ($rule['group'] ?? null); + $qualifiesPublic = ($group === 'public' + && $this->qualifiesForPublic(userId: $userId, inheritFromPublic: $inheritFromPublic) === true); + $userQualifies = ($qualifiesPublic === true + || ($group !== null && in_array($group, $userGroups, true) === true)); + } + if ($userQualifies === false) { continue; } @@ -824,7 +1002,23 @@ public function buildRbacConditionsSql(Schema $schema, string $action='read'): a } // Get effective authorization (schema-level, or register cascade). - $authorization = $this->resolveSchemaAuthorization(schema: $schema); + // Fail-closed: an unresolvable authorization yields the deny-all result + // (`bypass => false` with no conditions) rather than the open bypass. + try { + $authorization = $this->resolveSchemaAuthorization(schema: $schema); + } catch (AuthorizationUnresolvableException $e) { + $this->logger->error( + message: '[MagicRbacHandler] Authorization unresolvable; returning deny-all conditions (fail-closed)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schema->getId(), + 'action' => $action, + 'error' => $e->getMessage(), + ] + ); + return ['bypass' => false, 'conditions' => []]; + } // If no authorization is configured, schema is open to all. if (empty($authorization) === true) { @@ -834,11 +1028,12 @@ public function buildRbacConditionsSql(Schema $schema, string $action='read'): a // Get authorization rules for this action. $rules = $authorization[$action] ?? []; - // If action is not configured in authorization, it's open to all. - if (empty($rules) === true) { - return ['bypass' => true, 'conditions' => []]; - } - + // Fail-closed: a non-empty authorization block that does not list this + // action no longer bypasses filtering (no early `bypass => true`). Flow + // falls through to the owner condition below; unmatched callers (e.g. + // anonymous) get the deny-all empty-conditions result — consistent with + // applyRbacFilters and PermissionHandler. Admins already returned; the + // empty-block open-default above is unchanged. // Build the RBAC filter conditions. $conditions = []; @@ -856,12 +1051,16 @@ public function buildRbacConditionsSql(Schema $schema, string $action='read'): a $conditions[] = "_owner = {$quotedSystemId}"; } + // Resolve whether authenticated users inherit `public` rights once. + $inheritFromPublic = $this->authenticatedInheritsPublic(schema: $schema); + // Process each authorization rule. foreach ($rules as $rule) { $ruleResult = $this->processAuthorizationRuleSql( rule: $rule, userGroups: $userGroups, - userId: $userId + userId: $userId, + inheritFromPublic: $inheritFromPublic ); if ($ruleResult === true) { @@ -882,22 +1081,26 @@ public function buildRbacConditionsSql(Schema $schema, string $action='read'): a /** * Process a single authorization rule for raw SQL output. * - * @param mixed $rule Authorization rule (string or array). - * @param array $userGroups User's group IDs. - * @param string|null $userId Current user ID. + * @param mixed $rule Authorization rule (string or array). + * @param array $userGroups User's group IDs. + * @param string|null $userId Current user ID. + * @param bool $inheritFromPublic Whether auth users inherit public rights. * * @return mixed True if unconditional access, SQL string for conditional, false if no access. + * + * @spec openspec/specs/rbac-zaaktype/spec.md */ - private function processAuthorizationRuleSql(mixed $rule, array $userGroups, ?string $userId): mixed + private function processAuthorizationRuleSql(mixed $rule, array $userGroups, ?string $userId, bool $inheritFromPublic): mixed { // Simple rule: just a group name string. if (is_string($rule) === true) { - return $this->processSimpleRule(rule: $rule, userGroups: $userGroups, userId: $userId); + return $this->processSimpleRule(rule: $rule, userGroups: $userGroups, userId: $userId, inheritFromPublic: $inheritFromPublic); } - // Conditional rule: object with 'group' and optional 'match'. - if (is_array($rule) === true && isset($rule['group']) === true) { - return $this->processConditionalRuleSql(rule: $rule, userGroups: $userGroups, userId: $userId); + // Conditional rule: object with 'group' (or a 'user' override) and + // optional 'match'. + if (is_array($rule) === true && (isset($rule['group']) === true || isset($rule['user']) === true)) { + return $this->processConditionalRuleSql(rule: $rule, userGroups: $userGroups, userId: $userId, inheritFromPublic: $inheritFromPublic); } return false; @@ -906,24 +1109,30 @@ private function processAuthorizationRuleSql(mixed $rule, array $userGroups, ?st /** * Process a conditional authorization rule for raw SQL output. * - * @param array $rule Rule with 'group' and optional 'match'. - * @param array $userGroups User's group IDs. - * @param string|null $userId Current user ID. + * @param array $rule Rule with 'group' and optional 'match'. + * @param array $userGroups User's group IDs. + * @param string|null $userId Current user ID. + * @param bool $inheritFromPublic Whether auth users inherit public rights. * * @return mixed True if unconditional access, SQL string for conditional, false if no access. * - * @psalm-suppress UnusedParam $userId reserved for user-specific match conditions in future RBAC rules + * @spec openspec/specs/rbac-zaaktype/spec.md */ - private function processConditionalRuleSql(array $rule, array $userGroups, ?string $userId): mixed + private function processConditionalRuleSql(array $rule, array $userGroups, ?string $userId, bool $inheritFromPublic): mixed { - $group = $rule['group']; + $group = ($rule['group'] ?? null); $match = $rule['match'] ?? null; - // Check if user qualifies for this group. + // Check if user qualifies for this group OR is the named user override. $userQualifies = false; - if ($group === 'public') { - $userQualifies = true; - } else if (in_array($group, $userGroups, true) === true) { + if (isset($rule['user']) === true && is_string($rule['user']) === true) { + // User-level override (delegation): { "user": "", "match": {...} }. + $userQualifies = $this->matchesUserOverride(rule: 'user:'.$rule['user'], userId: $userId); + } else if ($group === 'public') { + // Anonymous users always qualify; authenticated users only when + // public inheritance is enabled (inheritFromPublic). + $userQualifies = $this->qualifiesForPublic(userId: $userId, inheritFromPublic: $inheritFromPublic); + } else if ($group !== null && in_array($group, $userGroups, true) === true) { $userQualifies = true; } @@ -1267,12 +1476,26 @@ public function hasConditionalRulesBypassingMultitenancy(Schema $schema, string return false; } + // Resolve the public-inheritance gate for this schema. A `public` rule + // only grants an authenticated user a cross-tenant bypass when public + // inheritance is enabled; anonymous users always qualify. Threading this + // through keeps the bypass decision consistent with applyRbacFilters / + // buildRbacConditionsSql / hasPermission, which already gate `public`. + $userId = $user?->getUID(); + $inheritFromPublic = $this->authenticatedInheritsPublic(schema: $schema); + // Check if user qualifies for any rule that should bypass multitenancy. // This includes: // 1. Simple rules (group name strings) - user in group can see ALL records. // 2. Conditional rules with non-_organisation match fields - RBAC handles filtering. foreach ($rules as $rule) { - if ($this->ruleBypassesMultitenancy(rule: $rule, userGroups: $userGroups) === true) { + if ($this->ruleBypassesMultitenancy( + rule: $rule, + userGroups: $userGroups, + userId: $userId, + inheritFromPublic: $inheritFromPublic + ) === true + ) { return true; } }//end foreach @@ -1283,23 +1506,35 @@ public function hasConditionalRulesBypassingMultitenancy(Schema $schema, string /** * Check if a single rule should bypass multitenancy for the current user * - * @param mixed $rule Authorization rule (string or array) - * @param array $userGroups User's group IDs + * @param mixed $rule Authorization rule (string or array) + * @param array $userGroups User's group IDs + * @param string|null $userId Current user ID (null = anonymous) + * @param bool $inheritFromPublic Whether auth users inherit public rights * * @return bool True if this rule bypasses multitenancy */ - private function ruleBypassesMultitenancy(mixed $rule, array $userGroups): bool + private function ruleBypassesMultitenancy(mixed $rule, array $userGroups, ?string $userId, bool $inheritFromPublic): bool { // Check simple rules (just group names). // If user qualifies for a simple rule, they can see ALL records, // so multitenancy should be bypassed. if (is_string($rule) === true) { - return $this->simpleRuleBypassesMultitenancy(rule: $rule, userGroups: $userGroups); + return $this->simpleRuleBypassesMultitenancy( + rule: $rule, + userGroups: $userGroups, + userId: $userId, + inheritFromPublic: $inheritFromPublic + ); } // Check conditional rules. if (is_array($rule) === true && isset($rule['group']) === true && isset($rule['match']) === true) { - return $this->conditionalRuleBypassesMultitenancy(rule: $rule, userGroups: $userGroups); + return $this->conditionalRuleBypassesMultitenancy( + rule: $rule, + userGroups: $userGroups, + userId: $userId, + inheritFromPublic: $inheritFromPublic + ); } return false; @@ -1308,15 +1543,22 @@ private function ruleBypassesMultitenancy(mixed $rule, array $userGroups): bool /** * Check if a simple (group name) rule bypasses multitenancy * - * @param string $rule Group name - * @param array $userGroups User's group IDs + * @param string $rule Group name + * @param array $userGroups User's group IDs + * @param string|null $userId Current user ID (null = anonymous) + * @param bool $inheritFromPublic Whether auth users inherit public rights * * @return bool True if this simple rule bypasses multitenancy */ - private function simpleRuleBypassesMultitenancy(string $rule, array $userGroups): bool + private function simpleRuleBypassesMultitenancy(string $rule, array $userGroups, ?string $userId, bool $inheritFromPublic): bool { + // A `public` rule only grants a cross-tenant bypass to principals who + // actually qualify for `public`: anonymous users always, authenticated + // users only when public inheritance is enabled. Otherwise the bypass + // would expose other-tenant rows to an authenticated user whose access + // was meant to come from a different (e.g. `authenticated`) rule. if ($rule === 'public') { - return true; + return $this->qualifiesForPublic(userId: $userId, inheritFromPublic: $inheritFromPublic); } return in_array($rule, $userGroups, true); @@ -1328,18 +1570,25 @@ private function simpleRuleBypassesMultitenancy(string $rule, array $userGroups) * A conditional rule bypasses multitenancy when the user qualifies for the * group and the match conditions include fields other than _organisation. * - * @param array $rule Rule with 'group' and 'match' - * @param array $userGroups User's group IDs + * @param array $rule Rule with 'group' and 'match' + * @param array $userGroups User's group IDs + * @param string|null $userId Current user ID (null = anonymous) + * @param bool $inheritFromPublic Whether auth users inherit public rights * * @return bool True if this conditional rule bypasses multitenancy */ - private function conditionalRuleBypassesMultitenancy(array $rule, array $userGroups): bool + private function conditionalRuleBypassesMultitenancy(array $rule, array $userGroups, ?string $userId, bool $inheritFromPublic): bool { $group = $rule['group']; $match = $rule['match']; - // Check if user qualifies for this group. - $userQualifies = ($group === 'public' || in_array($group, $userGroups, true) === true); + // Check if user qualifies for this group. A `public` group only qualifies + // an authenticated user when public inheritance is enabled (anonymous + // users always qualify), consistent with the simple-rule path above. + $userQualifies = ( + ($group === 'public' && $this->qualifiesForPublic(userId: $userId, inheritFromPublic: $inheritFromPublic) === true) + || in_array($group, $userGroups, true) === true + ); // If user qualifies and match contains non-_organisation fields, multitenancy should be bypassed. if ($userQualifies === true && is_array($match) === true) { @@ -1377,12 +1626,19 @@ private function matchHasNonOrganisationFields(array $match): bool * @param Schema $schema The schema to resolve authorization for. * * @return array|null The effective authorization array. + * + * @throws AuthorizationUnresolvableException When authorization cannot be determined. Callers MUST deny. */ private function resolveSchemaAuthorization(Schema $schema): ?array { try { $permissionHandler = $this->container->get(PermissionHandler::class); return $permissionHandler->resolveAuthorization($schema); + } catch (AuthorizationUnresolvableException $e) { + // Fail-closed: propagate. Falling back to the schema's own (possibly + // absent) authorization here would re-open the very hole the resolver + // now closes — `empty($authorization)` below means "open to all". + throw $e; } catch (\Throwable $e) { // Fallback to direct schema authorization if PermissionHandler unavailable. $this->logger->debug( diff --git a/lib/Db/MagicMapper/MagicSearchHandler.php b/lib/Db/MagicMapper/MagicSearchHandler.php index 555820de13..878bb15c70 100644 --- a/lib/Db/MagicMapper/MagicSearchHandler.php +++ b/lib/Db/MagicMapper/MagicSearchHandler.php @@ -45,6 +45,8 @@ use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\MagicMapper\MagicRbacHandler; use OCA\OpenRegister\Db\MagicMapper\MagicOrganizationHandler; +use OCA\OpenRegister\Exception\EncryptedFieldFilterException; +use OCA\OpenRegister\Service\DateTimeNormalizer; use OCA\OpenRegister\Service\Object\SchemaTypeConverter; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -84,6 +86,16 @@ class MagicSearchHandler */ private ?bool $hasPgTrgm = null; + /** + * PERF-4: request-scoped memo of the per-schema property-type and + * column-to-property maps, keyed by schema id. Building these ran + * sanitizeColumnName for every property on every row; memoising it makes + * it once-per-schema instead of once-per-row. + * + * @var array, columns: array}> + */ + private array $schemaColumnMapCache = []; + /** * Constructor for MagicSearchHandler * @@ -92,13 +104,15 @@ class MagicSearchHandler * @param MagicRbacHandler $rbacHandler RBAC handler for access control * @param MagicOrganizationHandler $organizationHandler Organization handler for multi-tenancy * @param SchemaTypeConverter $schemaTypeConverter Schema-driven type converter for row values + * @param DateTimeNormalizer $dateTimeNormalizer Normaliser for date/date-time property formats */ public function __construct( private readonly IDBConnection $db, private readonly LoggerInterface $logger, private readonly MagicRbacHandler $rbacHandler, private readonly MagicOrganizationHandler $organizationHandler, - private readonly SchemaTypeConverter $schemaTypeConverter + private readonly SchemaTypeConverter $schemaTypeConverter, + private readonly DateTimeNormalizer $dateTimeNormalizer ) { }//end __construct() @@ -247,6 +261,12 @@ public function searchObjects(array $query, Register $register, Schema $schema, // indexes for ORDER BY … LIMIT instead of sorting the full result set. if (empty($order) === false) { $this->applySorting(qb: $queryBuilder, order: $order, schema: $schema, searchTerm: $searchTerm); + } else { + // BUG-DB-4: without an explicit order, LIMIT/OFFSET pagination is + // non-deterministic (the database may return rows in any order), + // causing duplicates/gaps across pages. Add a stable default order + // on the monotonically increasing primary key. + $queryBuilder->addOrderBy('t._id', 'ASC'); } $queryBuilder->setMaxResults($limit) @@ -255,6 +275,59 @@ public function searchObjects(array $query, Register $register, Schema $schema, return $this->executeSearchQuery(qb: $queryBuilder, register: $register, schema: $schema, tableName: $tableName); }//end searchObjects() + /** + * Apply the SAME multitenancy + RBAC access-control filters used by the + * list/search path to an arbitrary query builder. + * + * The single-object read path (MagicMapper::findInRegisterSchemaTable) + * historically skipped org/RBAC filtering entirely, creating a cross-org + * IDOR: a non-admin in org B could read an org-A object by id even though + * the list path correctly hid it. This method exposes the search path's + * access-control logic so the single-object read can enforce the exact + * same isolation, returning no row (→ 404, no existence leak) for objects + * the caller may not see. + * + * The query builder MUST already use the table alias `t` in its FROM clause, + * because the underlying org/RBAC conditions reference `t._organisation`, + * `t._owner`, etc. + * + * Public-published reads are preserved: resolveMultitenancyFlag() drops the + * org filter for schemas that grant `public` read, and the RBAC filter then + * grants the read — matching the list path exactly. + * + * @param IQueryBuilder $qb Query builder (must use FROM alias `t`). + * @param Schema $schema Schema for access-control rules. + * @param bool $_rbac Whether to apply RBAC filtering. + * @param bool $_multitenancy Whether to apply multitenancy filtering. + * + * @return void + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flags mirror the search-path posture. + */ + public function applyAccessControlToQuery( + IQueryBuilder $qb, + Schema $schema, + bool $_rbac=true, + bool $_multitenancy=true + ): void { + // Mirror the list path: public schemas bypass multitenancy by default. + // No explicit multitenancy request exists on the single-object read path, + // so multitenancyExplicit is always false here. + $resolvedMultitenancy = $this->resolveMultitenancyFlag( + _multitenancy: $_multitenancy, + multitenancyExplicit: false, + schema: $schema + ); + + $this->applyAccessControlFilters( + qb: $qb, + schema: $schema, + _rbac: $_rbac, + _multitenancy: $resolvedMultitenancy, + multitenancyExplicit: false + ); + }//end applyAccessControlToQuery() + /** * Build a filtered query with all WHERE conditions applied. * @@ -275,8 +348,12 @@ public function searchObjects(array $query, Register $register, Schema $schema, public function buildFilteredQuery(array $query, Schema $schema, string $tableName): IQueryBuilder { // Extract options from query (prefixed with _). - $search = $query['_search'] ?? null; - $includeDeleted = $query['_includeDeleted'] ?? false; + $search = $query['_search'] ?? null; + // Coerce to bool: query-string params arrive as strings (e.g. + // "_includeDeleted=true" → "true"). applyBasicFilters() is bool-typed + // under strict_types, so passing the raw string raised a TypeError → + // HTTP 500 on any list call with _includeDeleted=true. + $includeDeleted = filter_var($query['_includeDeleted'] ?? false, FILTER_VALIDATE_BOOLEAN); $ids = $query['_ids'] ?? null; $_rbac = $query['_rbac'] ?? true; $_multitenancy = $query['_multitenancy'] ?? true; @@ -291,12 +368,20 @@ public function buildFilteredQuery(array $query, Schema $schema, string $tableNa ); // Extract and clean filters from the query. + // + // Reserved params (e.g. `register`, `schema`, `registers`, `schemas`, + // `extend`) carry query CONTEXT, not object-field filters. They are NOT + // underscore-prefixed, so without this guard they leak into + // applyObjectFilters(), which treats them as unknown schema properties + // and emits a `1 = 0` condition — silently returning ZERO results. This + // is why `ObjectService::findAll(['filters' => ['register' => …, + // 'schema' => …, …]])` (which always injects register/schema into the + // filters) could not surface a just-written object in-request. Mirror + // the raw-SQL UNION path, which already excludes getReservedParams(). $metadataFilters = $query['@self'] ?? []; $objectFilters = array_filter( $query, - function ($key) { - return $key !== '@self' && !str_starts_with($key, '_'); - }, + fn ($key): bool => $this->isObjectFieldFilterKey(key: $key), ARRAY_FILTER_USE_KEY ); @@ -347,9 +432,54 @@ function ($key) { $this->applyRelationsContainsFilter(qb: $queryBuilder, uuid: $relationsContains); } + // Apply dotted relation-field filters: `_relations.` => . + $this->applyRelationFieldFilters(qb: $queryBuilder, query: $query); + return $queryBuilder; }//end buildFilteredQuery() + /** + * Decide whether a query key is a genuine object-field filter. + * + * Excludes the `@self` metadata bag, every underscore-prefixed system + * param, and every reserved context param (`register`, `schema`, + * `registers`, `schemas`, `extend`). Without the reserved-param exclusion, + * context keys leak into applyObjectFilters() and force a `1 = 0` condition + * (they are not schema columns), silently returning zero results. + * + * @param string $key The query key to classify. + * + * @return bool True when the key is an object-field filter. + */ + private function isObjectFieldFilterKey(string $key): bool + { + if ($key === '@self' || str_starts_with($key, '_') === true) { + return false; + } + + return in_array($key, $this->getReservedParams(), true) === false; + }//end isObjectFieldFilterKey() + + /** + * Apply every dotted relation-field filter (`_relations.` => ) + * present in the query to the QueryBuilder. + * + * Each pair matches only objects whose `_relations` references under + * the named relation field (honouring the filter VALUE, not merely the + * presence of the relation field). + * + * @param IQueryBuilder $qb Query builder to modify. + * @param array $query The full search query. + * + * @return void + */ + private function applyRelationFieldFilters(IQueryBuilder $qb, array $query): void + { + foreach ($this->extractRelationFieldFilters(query: $query) as $relField => $relValue) { + $this->applyRelationFieldFilter(qb: $qb, field: $relField, value: $relValue); + } + }//end applyRelationFieldFilters() + /** * Build WHERE conditions as raw SQL for use in UNION queries. * @@ -376,8 +506,11 @@ public function buildWhereConditionsSql(array $query, Schema $schema, ?array $ex $isPostgres = $this->isPostgresPlatform(); // Extract options from query. - $search = $query['_search'] ?? null; - $includeDeleted = $query['_includeDeleted'] ?? false; + $search = $query['_search'] ?? null; + // Coerce to bool: query-string "true"/"false" must not be compared by + // identity against the boolean false (a non-empty string is never + // === false, which would silently flip the deleted filter). + $includeDeleted = filter_var($query['_includeDeleted'] ?? false, FILTER_VALIDATE_BOOLEAN); $_rbac = $query['_rbac'] ?? true; // 1. Deleted filter. @@ -424,6 +557,13 @@ public function buildWhereConditionsSql(array $query, Schema $schema, ?array $ex ); $conditions = array_merge($conditions, $tmloConditions); + // 7. Dotted relation-field filters (`_relations.` => ). + $relationConditions = $this->buildRelationFilterConditionsSql( + query: $query, + connection: $connection + ); + $conditions = array_merge($conditions, $relationConditions); + return $conditions; }//end buildWhereConditionsSql() @@ -510,10 +650,16 @@ private function buildSearchConditionSql( }//end if }//end foreach - // Search in metadata text fields (ILIKE for all). - $searchConditions[] = "_name::text ILIKE {$likePattern}"; - $searchConditions[] = "_description::text ILIKE {$likePattern}"; - $searchConditions[] = "_summary::text ILIKE {$likePattern}"; + // Search in metadata text fields. + if ($isPostgres === true) { + $searchConditions[] = "_name::text ILIKE {$likePattern}"; + $searchConditions[] = "_description::text ILIKE {$likePattern}"; + $searchConditions[] = "_summary::text ILIKE {$likePattern}"; + } else { + $searchConditions[] = "LOWER(CAST(_name AS CHAR)) LIKE LOWER({$likePattern})"; + $searchConditions[] = "LOWER(CAST(_description AS CHAR)) LIKE LOWER({$likePattern})"; + $searchConditions[] = "LOWER(CAST(_summary AS CHAR)) LIKE LOWER({$likePattern})"; + } // Add fuzzy matching ONLY for _name when explicitly requested via _fuzzy=true. // This uses pg_trgm similarity() for typo tolerance at ~13% performance cost. @@ -585,24 +731,49 @@ private function buildObjectFilterConditionsSql( continue; } - // Handle array filter values: comparison operators (gte/lte/gt/lt/in) or IN clause. + // Handle array filter values: comparison operators + // (gte/lte/gt/lt/in/notIn/ne) or a bare IN clause. if (is_array($value) === true) { - $comparisonOperators = ['gte', 'lte', 'gt', 'lt', 'in']; + $comparisonOperators = ['gte', 'lte', 'gt', 'lt', 'in', 'notIn', 'ne']; if (empty(array_intersect(array_keys($value), $comparisonOperators)) === false) { if (isset($value['gte']) === true) { - $conditions[] = "{$quotedCol} >= ".$connection->quote((string) $value['gte']); + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $value['gte'], + isPostgres: $isPostgres + ); + $conditions[] = "{$colRef} >= ".$connection->quote((string) $value['gte']); } if (isset($value['lte']) === true) { - $conditions[] = "{$quotedCol} <= ".$connection->quote((string) $value['lte']); + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $value['lte'], + isPostgres: $isPostgres + ); + $conditions[] = "{$colRef} <= ".$connection->quote((string) $value['lte']); } if (isset($value['gt']) === true) { - $conditions[] = "{$quotedCol} > ".$connection->quote((string) $value['gt']); + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $value['gt'], + isPostgres: $isPostgres + ); + $conditions[] = "{$colRef} > ".$connection->quote((string) $value['gt']); } if (isset($value['lt']) === true) { - $conditions[] = "{$quotedCol} < ".$connection->quote((string) $value['lt']); + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $value['lt'], + isPostgres: $isPostgres + ); + $conditions[] = "{$colRef} < ".$connection->quote((string) $value['lt']); } if (isset($value['in']) === true) { @@ -611,22 +782,72 @@ private function buildObjectFilterConditionsSql( $inValues = $value['in']; } + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $inValues, + isPostgres: $isPostgres + ); $quotedValues = array_map(fn($v) => $connection->quote((string) $v), $inValues); - $conditions[] = "{$quotedCol} IN (".implode(', ', $quotedValues).')'; + $conditions[] = "{$colRef} IN (".implode(', ', $quotedValues).')'; + } + + if (isset($value['notIn']) === true) { + $notInValues = [$value['notIn']]; + if (is_array($value['notIn']) === true) { + $notInValues = $value['notIn']; + } + + // An empty exclusion list excludes nothing — skip the + // clause rather than emit `NOT IN ()`. + if (count($notInValues) > 0) { + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $notInValues, + isPostgres: $isPostgres + ); + $quotedValues = array_map(fn($v) => $connection->quote((string) $v), $notInValues); + $conditions[] = "{$colRef} NOT IN (".implode(', ', $quotedValues).')'; + } + } + + if (isset($value['ne']) === true) { + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $value['ne'], + isPostgres: $isPostgres + ); + $conditions[] = "{$colRef} <> ".$connection->quote((string) $value['ne']); } } else if (empty($value) === false) { + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $value, + isPostgres: $isPostgres + ); $quotedValues = array_map( fn($v) => $connection->quote((string) $v), $value ); - $conditions[] = "{$quotedCol} IN (".implode(', ', $quotedValues).')'; + $conditions[] = "{$colRef} IN (".implode(', ', $quotedValues).')'; }//end if continue; }//end if - // Simple equality filter. - $conditions[] = "{$quotedCol} = ".$connection->quote((string) $value); + // Simple equality filter. Cast numeric columns to text when filtered + // by a non-numeric (e.g. UUID) value so PostgreSQL does not abort the + // whole UNION query with SQLSTATE[22P02]. + $colRef = $this->buildFilterColumnRef( + columnRef: $quotedCol, + propertyType: $propertyType, + value: $value, + isPostgres: $isPostgres + ); + $conditions[] = "{$colRef} = ".$connection->quote((string) $value); }//end foreach return $conditions; @@ -965,12 +1186,33 @@ private function applyMetadataFilters(IQueryBuilder $qb, array $filters): void * * @return void * + * @throws EncryptedFieldFilterException When a filter targets a property flagged + * `x-openregister-encrypted: true`. + * * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @spec openspec/specs/field-level-encryption/spec.md#requirement-encrypted-fields-are-excluded-from-search-and-facets */ private function applyObjectFilters(IQueryBuilder $qb, array $filters, Schema $schema): void { $properties = $schema->getProperties(); + // Fail loud BEFORE any query work rather than silently returning zero rows: + // an encrypted property's value is ciphertext (and, since + // buildTableColumnsFromSchema() gives it no dedicated column, may not even be + // a real column at all), so a plaintext filter against it can never mean what + // the caller intended. Checked up-front, ahead of platform detection and SQL + // building, so the rejection is unconditional and cheap. + foreach ($filters as $field => $value) { + if (is_string($field) === true + && ($properties[$field]['x-openregister-encrypted'] ?? false) === true + ) { + throw new EncryptedFieldFilterException(property: $field); + } + } + + $isPostgres = $this->isPostgresPlatform(); + foreach ($filters as $field => $value) { // Check if this field exists as a column in the schema. if (($properties[$field] ?? null) === null) { @@ -1006,16 +1248,24 @@ private function applyObjectFilters(IQueryBuilder $qb, array $filters, Schema $s // Handle object type columns (JSON objects with 'value' key containing UUID). if ($propertyType === 'object') { - $this->applyJsonObjectFilter(qb: $qb, columnName: $columnName, value: $value); + $this->applyJsonObjectFilter(qb: $qb, columnName: $columnName, value: $value, isPostgres: $isPostgres); continue; } if (is_array($value) === true) { - $comparisonOperators = ['gte', 'lte', 'gt', 'lt', 'in']; + $comparisonOperators = ['gte', 'lte', 'gt', 'lt', 'in', 'notIn', 'ne']; if (empty(array_intersect(array_keys($value), $comparisonOperators)) === true) { + // Cast numeric columns to text when filtered by non-numeric + // (e.g. UUID) values so PostgreSQL does not abort with 22P02. + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $value, + isPostgres: $isPostgres + ); $qb->andWhere( $qb->expr()->in( - "t.{$columnName}", + $columnRef, $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR_ARRAY) ) ); @@ -1023,19 +1273,43 @@ private function applyObjectFilters(IQueryBuilder $qb, array $filters, Schema $s } if (isset($value['gte']) === true) { - $qb->andWhere($qb->expr()->gte("t.{$columnName}", $qb->createNamedParameter($value['gte']))); + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $value['gte'], + isPostgres: $isPostgres + ); + $qb->andWhere($qb->expr()->gte($columnRef, $qb->createNamedParameter($value['gte']))); } if (isset($value['lte']) === true) { - $qb->andWhere($qb->expr()->lte("t.{$columnName}", $qb->createNamedParameter($value['lte']))); + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $value['lte'], + isPostgres: $isPostgres + ); + $qb->andWhere($qb->expr()->lte($columnRef, $qb->createNamedParameter($value['lte']))); } if (isset($value['gt']) === true) { - $qb->andWhere($qb->expr()->gt("t.{$columnName}", $qb->createNamedParameter($value['gt']))); + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $value['gt'], + isPostgres: $isPostgres + ); + $qb->andWhere($qb->expr()->gt($columnRef, $qb->createNamedParameter($value['gt']))); } if (isset($value['lt']) === true) { - $qb->andWhere($qb->expr()->lt("t.{$columnName}", $qb->createNamedParameter($value['lt']))); + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $value['lt'], + isPostgres: $isPostgres + ); + $qb->andWhere($qb->expr()->lt($columnRef, $qb->createNamedParameter($value['lt']))); } if (isset($value['in']) === true) { @@ -1044,18 +1318,68 @@ private function applyObjectFilters(IQueryBuilder $qb, array $filters, Schema $s $inValues = $value['in']; } + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $inValues, + isPostgres: $isPostgres + ); $qb->andWhere( $qb->expr()->in( - "t.{$columnName}", + $columnRef, $qb->createNamedParameter($inValues, IQueryBuilder::PARAM_STR_ARRAY) ) ); } + if (isset($value['notIn']) === true) { + $notInValues = [$value['notIn']]; + if (is_array($value['notIn']) === true) { + $notInValues = $value['notIn']; + } + + // An empty exclusion list excludes nothing, so skip the + // clause entirely — emitting `NOT IN ()` is a SQL error + // on some engines and a no-op-shaped clause on others. + if (count($notInValues) > 0) { + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $notInValues, + isPostgres: $isPostgres + ); + $qb->andWhere( + $qb->expr()->notIn( + $columnRef, + $qb->createNamedParameter($notInValues, IQueryBuilder::PARAM_STR_ARRAY) + ) + ); + } + }//end if + + if (isset($value['ne']) === true) { + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $value['ne'], + isPostgres: $isPostgres + ); + $qb->andWhere($qb->expr()->neq($columnRef, $qb->createNamedParameter($value['ne']))); + } + continue; }//end if - $qb->andWhere($qb->expr()->eq("t.{$columnName}", $qb->createNamedParameter($value))); + // Cast numeric columns to text when filtered by a non-numeric + // (e.g. UUID) value so PostgreSQL does not abort the query with + // SQLSTATE[22P02]; numeric values keep the indexed numeric path. + $columnRef = $this->buildFilterColumnRef( + columnRef: "t.{$columnName}", + propertyType: $propertyType, + value: $value, + isPostgres: $isPostgres + ); + $qb->andWhere($qb->expr()->eq($columnRef, $qb->createNamedParameter($value))); }//end foreach }//end applyObjectFilters() @@ -1111,10 +1435,11 @@ private function applyJsonArrayFilter(IQueryBuilder $qb, string $columnName, mix * @param IQueryBuilder $qb Query builder to modify * @param string $columnName Column name to filter * @param mixed $value Filter value (UUID string or array of UUIDs) + * @param bool $isPostgres Whether the backing database is PostgreSQL. * * @return void */ - private function applyJsonObjectFilter(IQueryBuilder $qb, string $columnName, mixed $value): void + private function applyJsonObjectFilter(IQueryBuilder $qb, string $columnName, mixed $value, bool $isPostgres): void { // Normalize value to array. $values = [$value]; @@ -1122,6 +1447,12 @@ private function applyJsonObjectFilter(IQueryBuilder $qb, string $columnName, mi $values = $value; } + if ($isPostgres === true) { + $colCast = "t.{$columnName}::text"; + } else { + $colCast = "CAST(t.{$columnName} AS CHAR)"; + } + if (count($values) === 1) { // Single value: match both plain UUID and JSON format using text comparison. // Plain format: column contains exactly "uuid". @@ -1129,7 +1460,7 @@ private function applyJsonObjectFilter(IQueryBuilder $qb, string $columnName, mi $param = $qb->createNamedParameter($values[0]); $jsonPattern = $qb->createNamedParameter('%"value": "'.$values[0].'"%'); $qb->andWhere( - "(t.{$columnName}::text = {$param} OR t.{$columnName}::text LIKE {$jsonPattern})" + "({$colCast} = {$param} OR {$colCast} LIKE {$jsonPattern})" ); return; } @@ -1140,7 +1471,7 @@ private function applyJsonObjectFilter(IQueryBuilder $qb, string $columnName, mi $param = $qb->createNamedParameter($v); $jsonPattern = $qb->createNamedParameter('%"value": "'.$v.'"%'); $orConditions->add( - "(t.{$columnName}::text = {$param} OR t.{$columnName}::text LIKE {$jsonPattern})" + "({$colCast} = {$param} OR {$colCast} LIKE {$jsonPattern})" ); } @@ -1192,6 +1523,110 @@ private function applyRelationsContainsFilter(IQueryBuilder $qb, string $uuid): ); }//end applyRelationsContainsFilter() + /** + * Extract dotted relation-field filters (`_relations.` => ) from a query. + * + * Filters of the form `_relations.` are NOT object-column filters + * (they are stripped by the leading-underscore guard everywhere else); they + * target the `_relations` JSONB index and must match only objects whose + * relation under `` references the supplied id VALUE. Returns a map + * of `` => for every such pair with a non-empty value. + * + * @param array $query The full search query. + * + * @return array Map of relation field name to referenced id. + */ + private function extractRelationFieldFilters(array $query): array + { + $filters = []; + foreach ($query as $key => $value) { + if (str_starts_with($key, '_relations.') === false) { + continue; + } + + $field = substr($key, strlen('_relations.')); + if ($field === '' || is_array($value) === true || $value === null) { + continue; + } + + $stringValue = (string) $value; + if ($stringValue === '') { + continue; + } + + $filters[$field] = $stringValue; + }//end foreach + + return $filters; + }//end extractRelationFieldFilters() + + /** + * Apply a dotted relation-field filter to a QueryBuilder. + * + * Matches objects whose `_relations` JSONB references `$value` under the + * named `$field`. Honours both the object format (`{"": "", ...}` + * — including array-indexed keys such as `.0`) and the legacy array + * format (`["", ...]`), so the filter is never silently dropped. + * + * @param IQueryBuilder $qb Query builder to modify. + * @param string $field The relation field name (the part after `_relations.`). + * @param string $value The referenced object id the relation must point at. + * + * @return void + */ + private function applyRelationFieldFilter(IQueryBuilder $qb, string $field, string $value): void + { + $valueParam = $qb->createNamedParameter($value); + $exactKey = $qb->createNamedParameter($field); + // Array-indexed relation keys are stored as `.` (e.g. `keywords.1`). + $prefixParam = $qb->createNamedParameter($field.'.%'); + + $qb->andWhere( + "( + (jsonb_typeof(t._relations) = 'object' AND EXISTS ( + SELECT 1 FROM jsonb_each_text(t._relations) AS kv + WHERE kv.value = {$valueParam} + AND (kv.key = {$exactKey} OR kv.key LIKE {$prefixParam}) + )) + OR + (jsonb_typeof(t._relations) = 'array' AND t._relations @> to_jsonb({$valueParam}::text)) + )" + ); + }//end applyRelationFieldFilter() + + /** + * Build raw-SQL conditions for dotted relation-field filters (UNION path). + * + * Mirrors {@see applyRelationFieldFilter()} for the inline-quoted UNION + * query path used by multi-schema searches and facets. + * + * @param array $query The full search query. + * @param object $connection Database connection for value quoting. + * + * @return string[] Array of SQL conditions (without leading AND/WHERE). + */ + private function buildRelationFilterConditionsSql(array $query, object $connection): array + { + $conditions = []; + foreach ($this->extractRelationFieldFilters(query: $query) as $field => $value) { + $valueQuoted = $connection->quote($value); + $exactQuoted = $connection->quote($field); + $prefixQuoted = $connection->quote($field.'.%'); + + $conditions[] = "( + (jsonb_typeof(_relations) = 'object' AND EXISTS ( + SELECT 1 FROM jsonb_each_text(_relations) AS kv + WHERE kv.value = {$valueQuoted} + AND (kv.key = {$exactQuoted} OR kv.key LIKE {$prefixQuoted}) + )) + OR + (jsonb_typeof(_relations) = 'array' AND _relations @> to_jsonb({$valueQuoted}::text)) + )"; + } + + return $conditions; + }//end buildRelationFilterConditionsSql() + /** * Apply full-text search across relevant columns * @@ -1225,6 +1660,16 @@ private function applyFullTextSearch( // Skip date/time formatted fields — PostgreSQL LOWER() only works on text columns. $dateFormats = ['date', 'date-time', 'time']; foreach ($properties ?? [] as $field => $propertyConfig) { + // Encrypted properties get no dedicated magic-table column (see + // MagicMapper::buildTableColumnsFromSchema()); including one in a LIKE + // full-text scan would either hit a non-existent column or, if a + // legacy column still exists from before the flag was set, scan + // ciphertext that can never match a plaintext search term. Skip + // explicitly rather than let it silently fail to match. + if (($propertyConfig['x-openregister-encrypted'] ?? false) === true) { + continue; + } + if (($propertyConfig['type'] ?? '') === 'string' && in_array($propertyConfig['format'] ?? '', $dateFormats, true) === false ) { @@ -1239,7 +1684,7 @@ private function applyFullTextSearch( ) ); } - } + }//end foreach // Search in metadata text fields (LIKE for all). $searchConditions->add( @@ -1369,6 +1814,51 @@ private function executeSearchQuery(IQueryBuilder $qb, Register $register, Schem return $objects; }//end executeSearchQuery() + /** + * Build (and memoise) the property-type and column-to-property maps for a schema. + * + * PERF-4: computing these maps ran sanitizeColumnName() for every property on + * every row. Memoising per schema id makes it once-per-schema-per-request. + * + * @param Schema $schema The schema to build maps for. + * + * @return array{types: array, columns: array} + */ + private function getSchemaColumnMaps(Schema $schema): array + { + $schemaId = $schema->getId(); + if ($schemaId !== null && isset($this->schemaColumnMapCache[$schemaId]) === true) { + return $this->schemaColumnMapCache[$schemaId]; + } + + $propertyTypes = []; + $propertyFormats = []; + $columnToPropertyMap = []; + $properties = $schema->getProperties(); + if (is_array($properties) === true) { + foreach ($properties as $propName => $propDef) { + $propertyTypes[$propName] = ($propDef['type'] ?? 'string'); + if (isset($propDef['format']) === true) { + $propertyFormats[$propName] = $propDef['format']; + } + + $columnName = $this->sanitizeColumnName(name: $propName); + $columnToPropertyMap[$columnName] = $propName; + } + } + + $maps = [ + 'types' => $propertyTypes, + 'formats' => $propertyFormats, + 'columns' => $columnToPropertyMap, + ]; + if ($schemaId !== null) { + $this->schemaColumnMapCache[$schemaId] = $maps; + } + + return $maps; + }//end getSchemaColumnMaps() + /** * Convert database row from dynamic table to ObjectEntity * @@ -1377,7 +1867,7 @@ private function executeSearchQuery(IQueryBuilder $qb, Register $register, Schem * @param Schema $schema Schema context * @param string $tableName Target dynamic table name * - * @return ObjectEntity|null ObjectEntity object or null if conversion fails + * @return ObjectEntity|null * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -1399,13 +1889,12 @@ private function convertRowToObjectEntity( // Build property type map and column-to-property mapping from schema. // The column-to-property mapping allows us to restore original property names // (e.g., 'e-mailadres') from their sanitized column names (e.g., 'e_mailadres'). - $propertyTypes = []; - $columnToPropertyMap = []; - foreach ($schema->getProperties() as $propName => $propDef) { - $propertyTypes[$propName] = $propDef['type'] ?? 'string'; - $columnName = $this->sanitizeColumnName(name: $propName); - $columnToPropertyMap[$columnName] = $propName; - } + // PERF-4: memoised per schema id so sanitizeColumnName runs once per + // schema rather than once per property per row. + $schemaMaps = $this->getSchemaColumnMaps(schema: $schema); + $propertyTypes = $schemaMaps['types']; + $propertyFormats = ($schemaMaps['formats'] ?? []); + $columnToPropertyMap = $schemaMaps['columns']; foreach ($row as $column => $value) { if (str_starts_with($column, '_') === true) { @@ -1423,10 +1912,32 @@ private function convertRowToObjectEntity( // Delegates to the shared SchemaTypeConverter so this handler and // MagicStatisticsHandler agree on type semantics across read paths. $propertyType = $propertyTypes[$propertyName] ?? 'string'; - $objectData[$propertyName] = $this->schemaTypeConverter->convertValue( + $value = $this->schemaTypeConverter->convertValue( value: $value, schemaType: $propertyType ); + + // Then agree on FORMAT semantics too. A `date`/`date-time` property + // lives in a DATETIME column, so the driver returns 'Y-m-d H:i:s' — + // which fails the schema's own `date-time` format when the object is + // written straight back (every UI edit is a read-modify-write, so any + // object with a populated date-time 400'd). MagicStatisticsHandler + // already normalises here; this path did not, and this is the path + // findAll() actually uses. + $propertyFormat = ($propertyFormats[$propertyName] ?? null); + if ($value !== null && is_string($value) === true && $propertyFormat !== null) { + if ($propertyFormat === 'date') { + $normalised = $this->dateTimeNormalizer->normalize($value); + $value = null; + if ($normalised !== null) { + $value = $normalised->format('Y-m-d'); + } + } else if ($propertyFormat === 'date-time') { + $value = $this->dateTimeNormalizer->formatForIso8601($value); + } + } + + $objectData[$propertyName] = $value; }//end foreach // Set metadata properties. @@ -1643,6 +2154,96 @@ private function isPostgresPlatform(): bool return stripos($this->db->getDatabasePlatform()::class, 'PostgreSQL') !== false; }//end isPostgresPlatform() + /** + * Whether a JSON-Schema property type maps onto a numeric SQL column. + * + * MagicMapper::mapSchemaPropertyToColumn() materialises `integer` as + * smallint/integer/bigint and `number` as decimal. Filtering such a column + * with a non-numeric value (e.g. a UUID — common when a relational key was + * mistakenly declared `type: integer` in the schema) makes PostgreSQL reject + * the whole query with `SQLSTATE[22P02] invalid input syntax for type + * integer`, taking down the endpoint rather than returning zero rows. + * + * @param string $propertyType The declared JSON-Schema property type. + * + * @return bool True when the backing column is numeric-typed. + */ + private function isNumericColumnType(string $propertyType): bool + { + return $propertyType === 'integer' || $propertyType === 'number'; + }//end isNumericColumnType() + + /** + * Whether a filter value is a non-numeric scalar that would break a numeric + * column comparison. + * + * A UUID, slug or any other non-numeric string bound against an integer / + * decimal column triggers a database type-coercion error. When this returns + * true the caller must cast the column to text so the comparison degrades to + * a safe (always-false) string match instead of crashing the query. + * + * @param mixed $value The raw filter value. + * + * @return bool True when $value is a scalar that is not numeric. + */ + private function isNonNumericScalar(mixed $value): bool + { + return is_scalar($value) === true && is_numeric($value) === false; + }//end isNonNumericScalar() + + /** + * Build the left-hand column reference for an object-field comparison, + * casting numeric columns to text when the supplied value(s) cannot be + * compared numerically. + * + * This is the core guard against `SQLSTATE[22P02]`: a UUID-valued filter on + * a column the schema declared `integer`/`number` must compare as text, not + * be coerced into the column's numeric type. Numeric values on numeric + * columns are left untouched so ordinary integer filters keep using the + * indexed numeric comparison. + * + * @param string $columnRef The already-qualified, quoted column reference + * (e.g. `t."amount"` or `t.amount`). + * @param string $propertyType The declared JSON-Schema property type. + * @param mixed $value The filter value (scalar or array of scalars). + * @param bool $isPostgres Whether the active platform is PostgreSQL. + * + * @return string The (optionally text-cast) column reference. + */ + private function buildFilterColumnRef( + string $columnRef, + string $propertyType, + mixed $value, + bool $isPostgres + ): string { + if ($this->isNumericColumnType(propertyType: $propertyType) === false) { + return $columnRef; + } + + // Determine whether any value in play is a non-numeric scalar. + $needsTextCast = false; + if (is_array($value) === true) { + foreach ($value as $candidate) { + if ($this->isNonNumericScalar(value: $candidate) === true) { + $needsTextCast = true; + break; + } + } + } else { + $needsTextCast = $this->isNonNumericScalar(value: $value); + } + + if ($needsTextCast === false) { + return $columnRef; + } + + if ($isPostgres === true) { + return $columnRef.'::text'; + } + + return 'CAST('.$columnRef.' AS CHAR)'; + }//end buildFilterColumnRef() + /** * Quote a column or identifier name for the current database platform. * diff --git a/lib/Db/MagicMapper/MagicStatisticsHandler.php b/lib/Db/MagicMapper/MagicStatisticsHandler.php index c7d1d6cf5c..d0e72f8688 100644 --- a/lib/Db/MagicMapper/MagicStatisticsHandler.php +++ b/lib/Db/MagicMapper/MagicStatisticsHandler.php @@ -60,6 +60,33 @@ class MagicStatisticsHandler { + /** + * Per-request memo of the magic-table list from information_schema. + * + * `getStatistics()` is called once per register on a registers-list page and + * each call re-ran the information_schema catalog scan; caching it for the + * request collapses O(registers) scans to one (consolidate-request-scoped-cache). + * Null until first populated; only successful scans are cached. + * + * @var array|null + */ + private ?array $magicTablesCache = null; + + /** + * Registers already fetched this request, keyed by id. + * + * @var array + */ + private array $registerCache = []; + + /** + * Schemas already fetched this request, keyed by id. + * + * @var array + */ + private array $schemaCache = []; + + /** * Metadata column prefix used in magic mapper tables * @@ -111,6 +138,59 @@ public function setCountCallback(callable $callback): void $this->countCallback = $callback; }//end setCountCallback() + /** + * Find a Register, reusing the entity within this request. + * + * Statistics walk every register/schema pair, and a caller that wants stats for N + * registers calls getStatistics() N times — so the SAME register and schema rows were + * re-fetched from the database over and over. On a dev instance with 76 registers and + * 1,231 schemas that was ~2,500 redundant SELECTs to render one list. + * + * Registers and schemas do not change mid-request, so hold on to them. + * + * @param int $registerId The register id. + * + * @return Register The register entity. + * + * @spec exclude request-scoped identity cache; no behaviour change + */ + private function findRegisterCached(int $registerId): Register + { + if (isset($this->registerCache[$registerId]) === false) { + $this->registerCache[$registerId] = $this->registerMapper->find( + $registerId, + _multitenancy: false, + _rbac: false + ); + } + + return $this->registerCache[$registerId]; + }//end findRegisterCached() + + /** + * Find a Schema, reusing the entity within this request. + * + * @param int $schemaId The schema id. + * + * @return Schema The schema entity. + * + * @see self::findRegisterCached() for why this cache exists. + * + * @spec exclude request-scoped identity cache; no behaviour change + */ + private function findSchemaCached(int $schemaId): Schema + { + if (isset($this->schemaCache[$schemaId]) === false) { + $this->schemaCache[$schemaId] = $this->schemaMapper->find( + $schemaId, + _multitenancy: false, + _rbac: false + ); + } + + return $this->schemaCache[$schemaId]; + }//end findSchemaCached() + /** * Count objects in a register-schema table via the injected callback. * @@ -145,6 +225,10 @@ private function countObjectsInRegisterSchemaTable(array $query, Register $regis */ private function getAllMagicMapperTables(): array { + if ($this->magicTablesCache !== null) { + return $this->magicTablesCache; + } + try { $platform = $this->db->getDatabasePlatform(); $isPostgres = stripos($platform::class, 'PostgreSQL') !== false; @@ -172,6 +256,8 @@ private function getAllMagicMapperTables(): array $tables[] = $tableName; } + $this->magicTablesCache = $tables; + return $tables; } catch (Exception $e) { $this->logger->error( @@ -276,8 +362,8 @@ public function getStatistics( } try { - $register = $this->registerMapper->find($pairRegisterId, _multitenancy: false, _rbac: false); - $schema = $this->schemaMapper->find($pairSchemaId, _multitenancy: false, _rbac: false); + $register = $this->findRegisterCached(registerId: $pairRegisterId); + $schema = $this->findSchemaCached(schemaId: $pairSchemaId); $count = $this->countObjectsInRegisterSchemaTable( query: [], diff --git a/lib/Db/MagicMapper/MagicTableHandler.php b/lib/Db/MagicMapper/MagicTableHandler.php index fa721b7ffd..231f6821a3 100644 --- a/lib/Db/MagicMapper/MagicTableHandler.php +++ b/lib/Db/MagicMapper/MagicTableHandler.php @@ -183,11 +183,35 @@ private function handleExistingTable( // columns aren't still on the legacy JSONB storage type (issue #1720). $requiredColumns = $this->magicMapper->buildTableColumnsFromSchema(schema: $schema); $currentColumns = $this->magicMapper->getExistingTableColumns(tableName: $tableName); - $missingColumns = array_diff_key($requiredColumns, $currentColumns); - $retypeColumns = $this->magicMapper->findJsonbColumnsNeedingRetype( + // Compare on PHYSICAL column names: required columns are keyed by + // property name (camelCase) while the live table reports snake_case, + // so a raw array_diff_key() flags every camelCase property as missing + // and the forced sync below re-fires on every request forever. + $missingColumns = $this->magicMapper->findMissingColumns( currentColumns: $currentColumns, requiredColumns: $requiredColumns ); + $retypeColumns = $this->magicMapper->findJsonbColumnsNeedingRetype( + currentColumns: $currentColumns, + requiredColumns: $requiredColumns + ); + + // A property that BECOMES a relation stops holding a document and starts + // holding a bare UUID, so its column must go from json to VARCHAR. Without + // this the table stays broken forever after such an edit — every save is + // rejected with "invalid input syntax for type json". + $relationRetype = $this->magicMapper->findRelationColumnsNeedingRetype( + currentColumns: $currentColumns, + requiredColumns: $requiredColumns + ); + if (empty($relationRetype) === false) { + $this->magicMapper->migrateRelationColumnsToVarchar( + tableName: $tableName, + currentColumns: $currentColumns, + requiredColumns: $requiredColumns + ); + $retypeColumns = array_merge($retypeColumns, $relationRetype); + } if (empty($missingColumns) === true && empty($retypeColumns) === true) { MagicMapper::setTableColumnsVerified(cacheKey: $cacheKey); diff --git a/lib/Db/MigrationPack.php b/lib/Db/MigrationPack.php new file mode 100644 index 0000000000..cdd693eaa3 --- /dev/null +++ b/lib/Db/MigrationPack.php @@ -0,0 +1,192 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class MigrationPack + * + * @method string|null getPackSlug() + * @method void setPackSlug(?string $packSlug) + * @method string|null getName() + * @method void setName(?string $name) + * @method string|null getSourceFormat() + * @method void setSourceFormat(?string $sourceFormat) + * @method string|null getVersion() + * @method void setVersion(?string $version) + * @method string|null getDefinition() + * @method void setDefinition(?string $definition) + * @method bool|null getBuiltin() + * @method void setBuiltin(?bool $builtin) + * @method string|null getOwner() + * @method void setOwner(?string $owner) + * @method DateTime|null getCreatedAt() + * @method void setCreatedAt(?DateTime $createdAt) + * @method DateTime|null getUpdatedAt() + * @method void setUpdatedAt(?DateTime $updatedAt) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ +class MigrationPack extends Entity implements JsonSerializable +{ + + /** + * The pack's own `id` field from its definition document (slug, unique). + * + * @var string|null + */ + protected ?string $packSlug = null; + + /** + * User-facing label. + * + * @var string|null + */ + protected ?string $name = null; + + /** + * Source format: csv|json|excel. + * + * @var string|null + */ + protected ?string $sourceFormat = null; + + /** + * Pack definition version (semver). + * + * @var string|null + */ + protected ?string $version = null; + + /** + * The full pack document, JSON-encoded (fieldMappings, defaults, skipRows, idStrategy, ...). + * + * @var string|null + */ + protected ?string $definition = null; + + /** + * Whether this pack ships with OpenRegister as a seeded reference pack. + * + * @var boolean|null + */ + protected ?bool $builtin = null; + + /** + * The owning Nextcloud user id, or null for built-in/system-seeded packs. + * + * @var string|null + */ + protected ?string $owner = null; + + /** + * When this pack was created. + * + * @var DateTime|null + */ + protected ?DateTime $createdAt = null; + + /** + * When this pack was last updated. + * + * @var DateTime|null + */ + protected ?DateTime $updatedAt = null; + + /** + * Constructor. + */ + public function __construct() + { + $this->addType(fieldName: 'packSlug', type: 'string'); + $this->addType(fieldName: 'name', type: 'string'); + $this->addType(fieldName: 'sourceFormat', type: 'string'); + $this->addType(fieldName: 'version', type: 'string'); + $this->addType(fieldName: 'definition', type: 'string'); + $this->addType(fieldName: 'builtin', type: 'boolean'); + $this->addType(fieldName: 'owner', type: 'string'); + $this->addType(fieldName: 'createdAt', type: 'datetime'); + $this->addType(fieldName: 'updatedAt', type: 'datetime'); + }//end __construct() + + /** + * The decoded pack definition document, or an empty array when unset/invalid. + * + * @return array + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function getDefinitionArray(): array + { + if ($this->definition === null || $this->definition === '') { + return []; + } + + $decoded = json_decode($this->definition, true); + if (is_array($decoded) === false) { + return []; + } + + return $decoded; + }//end getDefinitionArray() + + /** + * JSON serialization for the REST API (definition inlined as an object, not a string). + * + * @return array + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'packSlug' => $this->packSlug, + 'name' => $this->name, + 'sourceFormat' => $this->sourceFormat, + 'version' => $this->version, + 'definition' => $this->getDefinitionArray(), + 'builtin' => $this->builtin, + 'owner' => $this->owner, + 'createdAt' => $this->createdAt?->format(DateTime::ATOM), + 'updatedAt' => $this->updatedAt?->format(DateTime::ATOM), + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/MigrationPackMapper.php b/lib/Db/MigrationPackMapper.php new file mode 100644 index 0000000000..fd9a9fc3e6 --- /dev/null +++ b/lib/Db/MigrationPackMapper.php @@ -0,0 +1,112 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class MigrationPackMapper + * + * @template-extends QBMapper + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ +class MigrationPackMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_migration_packs', entityClass: MigrationPack::class); + }//end __construct() + + /** + * Find a migration pack by numeric id. + * + * @param int $id The migration pack id. + * + * @return MigrationPack + * + * @throws DoesNotExistException When no row matches. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function find(int $id): MigrationPack + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity(query: $qb); + }//end find() + + /** + * Find a migration pack by its pack-document slug (`packSlug`, i.e. the + * definition's own `id` field). This is the lookup used by the import + * endpoint's `packId` request parameter. + * + * @param string $packSlug The pack document's own `id`. + * + * @return MigrationPack + * + * @throws DoesNotExistException When no row matches. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function findByPackSlug(string $packSlug): MigrationPack + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('pack_slug', $qb->createNamedParameter($packSlug))); + + return $this->findEntity(query: $qb); + }//end findByPackSlug() + + /** + * Find every migration pack. + * + * @return MigrationPack[] + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function findAll(): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->orderBy('id', 'ASC'); + + return $this->findEntities(query: $qb); + }//end findAll() +}//end class diff --git a/lib/Db/MultiTenancyTrait.php b/lib/Db/MultiTenancyTrait.php index 2df8f506bf..82c5b56083 100644 --- a/lib/Db/MultiTenancyTrait.php +++ b/lib/Db/MultiTenancyTrait.php @@ -32,7 +32,6 @@ use DateTime; use DateInterval; use Symfony\Component\HttpFoundation\Response; -use OCP\AppFramework\Http\JSONResponse; /** * Trait MultiTenancyTrait @@ -231,7 +230,6 @@ protected function isCurrentUserAdmin(): bool * * This method provides comprehensive organisation filtering including: * - Hierarchical organisation support (active org + all parents) - * - Published entity bypass for multi-tenancy (Register/Schema entities only) * - Admin override capabilities * - System default organisation special handling * - NULL organisation legacy data access for admins @@ -239,10 +237,13 @@ protected function isCurrentUserAdmin(): bool * * Features: * 1. Hierarchical Access: Users see entities from their active org AND parent orgs - * 2. Published Entities: Register/Schema entities can bypass multi-tenancy via published/depublished columns - * 3. Admin Override: Admins can see all entities if enabled in config - * 4. Default Org: Special behavior for system-wide default organisation - * 5. Legacy Data: Admins can access NULL organisation entities + * 2. Admin Override: Admins can see all entities if enabled in config + * 3. Default Org: Special behavior for system-wide default organisation + * 4. Legacy Data: Admins can access NULL organisation entities + * + * Note: The published/depublished column bypass for Register/Schema multi-tenancy + * has been removed. Anonymous access is now controlled exclusively via RBAC + * (authorization.read containing 'public' in the register/schema entity). * * Example hierarchy: * - Organisation A (root) @@ -257,7 +258,7 @@ protected function isCurrentUserAdmin(): bool * @param bool $multiTenancyEnabled Whether multitenancy is enabled (default: true) * * @spec openspec/specs/deprecate-published-metadata/spec.md#REQ-5 (MultiTenancyTrait Documentation — - * published-bypass docs scoped to Register/Schema entities only; object-level published bypass removed) + * published/depublished column bypass removed; Register/Schema visibility now governed by RBAC only) * * @return void * @@ -762,6 +763,16 @@ protected function hasRbacPermission(string $action, string $entityType): bool return true; } + // Explicitly-scoped system operations (app config imports at boot, + // webcron jobs run via ObjectService::runAsSystem()) are trusted the + // same way CLI is: app boot runs BEFORE the session user is resolved + // and webcron never has one. This is the effective anonymous guard + // for entity-level RBAC — it short-circuits before the later + // userSession check, exactly as the CLI branch does. + if (\OCA\OpenRegister\Service\SystemOperationContext::isActive() === true) { + return true; + } + // No user logged in, deny access. return false; } @@ -809,6 +820,15 @@ protected function hasRbacPermission(string $action, string $entityType): bool return true; } + // Explicitly-scoped system operations (app config imports at boot, + // webcron jobs) are equally trusted: app boot runs BEFORE the + // session user is resolved and webcron never has one, so without + // this the same operations that succeed under CLI cron are denied + // as anonymous under webcron on every request. + if (\OCA\OpenRegister\Service\SystemOperationContext::isActive() === true) { + return true; + } + return false; } diff --git a/lib/Db/NotificationDedupeState.php b/lib/Db/NotificationDedupeState.php new file mode 100644 index 0000000000..b02eb8d232 --- /dev/null +++ b/lib/Db/NotificationDedupeState.php @@ -0,0 +1,133 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * NotificationDedupeState. + * + * @method int getSchemaId() + * @method void setSchemaId(int $schemaId) + * @method string getRuleKey() + * @method void setRuleKey(string $ruleKey) + * @method string getObjectUuid() + * @method void setObjectUuid(string $objectUuid) + * @method string getFingerprint() + * @method void setFingerprint(string $fingerprint) + * @method DateTime getDispatchedAt() + * @method void setDispatchedAt(DateTime $dispatchedAt) + * @method DateTime getSeenAt() + * @method void setSeenAt(DateTime $seenAt) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class NotificationDedupeState extends Entity implements JsonSerializable +{ + + /** + * Owning schema id. + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * Notification annotation key (rule key in `x-openregister-notifications`). + * + * @var string|null + */ + protected ?string $ruleKey = null; + + /** + * Target ObjectEntity UUID. + * + * @var string|null + */ + protected ?string $objectUuid = null; + + /** + * SHA-1 fingerprint of the watched-field values at last dispatch. + * + * @var string|null + */ + protected ?string $fingerprint = null; + + /** + * Timestamp the rule was dispatched for this object (last re-arm). + * + * @var DateTime|null + */ + protected ?DateTime $dispatchedAt = null; + + /** + * Last time the evaluator matched this (schema, rule, object) triple. + * + * @var DateTime|null + */ + protected ?DateTime $seenAt = null; + + /** + * Constructor. + */ + public function __construct() + { + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'ruleKey', type: 'string'); + $this->addType(fieldName: 'objectUuid', type: 'string'); + $this->addType(fieldName: 'fingerprint', type: 'string'); + $this->addType(fieldName: 'dispatchedAt', type: 'datetime'); + $this->addType(fieldName: 'seenAt', type: 'datetime'); + + }//end __construct() + + /** + * JSON serialization. + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'schemaId' => $this->schemaId, + 'ruleKey' => $this->ruleKey, + 'objectUuid' => $this->objectUuid, + 'fingerprint' => $this->fingerprint, + 'dispatchedAt' => $this->dispatchedAt?->format(DateTime::ATOM), + 'seenAt' => $this->seenAt?->format(DateTime::ATOM), + ]; + + }//end jsonSerialize() +}//end class diff --git a/lib/Db/NotificationDedupeStateMapper.php b/lib/Db/NotificationDedupeStateMapper.php new file mode 100644 index 0000000000..d7265653ae --- /dev/null +++ b/lib/Db/NotificationDedupeStateMapper.php @@ -0,0 +1,316 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use DateTimeZone; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\Exception as DbException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class NotificationDedupeStateMapper. + * + * @method NotificationDedupeState insert(Entity $entity) + * @method NotificationDedupeState update(Entity $entity) + * @method NotificationDedupeState delete(Entity $entity) + * + * @template-extends QBMapper + * + * @psalm-suppress PossiblyUnusedMethod + */ +class NotificationDedupeStateMapper extends QBMapper +{ + + /** + * Default retention window for the periodic sweep (90 days). + */ + public const DEFAULT_RETENTION_DAYS = 90; + + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct( + db: $db, + tableName: 'openregister_notification_dedupe', + entityClass: NotificationDedupeState::class + ); + + }//end __construct() + + /** + * Look up the dedup row for a single (schema, rule, object) triple. + * + * @param int $schemaId Schema identifier. + * @param string $ruleKey Notification annotation key. + * @param string $objectUuid Target ObjectEntity UUID. + * + * @return NotificationDedupeState|null Row when present, null when absent. + */ + public function findOne(int $schemaId, string $ruleKey, string $objectUuid): ?NotificationDedupeState + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq( + 'schema_id', + $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT) + ) + ) + ->andWhere( + $qb->expr()->eq( + 'rule_key', + $qb->createNamedParameter($ruleKey) + ) + ) + ->andWhere( + $qb->expr()->eq( + 'object_uuid', + $qb->createNamedParameter($objectUuid) + ) + ) + ->setMaxResults(1); + + try { + return $this->findEntity(query: $qb); + } catch (DoesNotExistException $e) { + return null; + } catch (\Throwable $e) { + // Read failures should not break the dispatch loop. + return null; + } + + }//end findOne() + + /** + * Insert-or-update the dedup row for one triple. + * + * When `$dispatched` is true the row's `dispatched_at` is updated to the + * given `$now` (initial dispatch or fingerprint-change re-arm). Otherwise + * only `seen_at` is touched (the engine matched again but the fingerprint + * has not changed, so no dispatch happened). + * + * @param int $schemaId Schema identifier. + * @param string $ruleKey Notification annotation key. + * @param string $objectUuid Target ObjectEntity UUID. + * @param string $fingerprint SHA-1 of watched-field values. + * @param DateTime $now Logical "now" for the scan pass (UTC). + * @param bool $dispatched True when this call recorded an actual dispatch. + * + * @return NotificationDedupeState The persisted entity. + */ + public function upsert( + int $schemaId, + string $ruleKey, + string $objectUuid, + string $fingerprint, + DateTime $now, + bool $dispatched + ): NotificationDedupeState { + $existing = $this->findOne(schemaId: $schemaId, ruleKey: $ruleKey, objectUuid: $objectUuid); + + if ($existing !== null) { + $existing->setFingerprint($fingerprint); + $existing->setSeenAt($now); + if ($dispatched === true) { + $existing->setDispatchedAt($now); + } + + try { + return $this->update(entity: $existing); + } catch (\Throwable $e) { + return $existing; + } + } + + $entity = new NotificationDedupeState(); + $entity->setSchemaId($schemaId); + $entity->setRuleKey($ruleKey); + $entity->setObjectUuid($objectUuid); + $entity->setFingerprint($fingerprint); + $entity->setDispatchedAt($now); + $entity->setSeenAt($now); + + try { + return $this->insert(entity: $entity); + } catch (DbException $e) { + // Concurrent dispatcher won the race — re-read and treat the + // row as authoritative. + if ($e->getReason() === DbException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + $existing = $this->findOne( + schemaId: $schemaId, + ruleKey: $ruleKey, + objectUuid: $objectUuid + ); + if ($existing !== null) { + return $existing; + } + } + + throw $e; + } + + }//end upsert() + + /** + * Drop every dedup row tied to an object (used on object purge). + * + * @param string $objectUuid Target ObjectEntity UUID. + * + * @return int Number of rows deleted (0 on error). + */ + public function deleteByObject(string $objectUuid): int + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where( + $qb->expr()->eq( + 'object_uuid', + $qb->createNamedParameter($objectUuid) + ) + ); + return (int) $qb->executeStatement(); + } catch (\Throwable) { + return 0; + } + + }//end deleteByObject() + + /** + * Drop every dedup row for a (schema, rule) pair (used on annotation save). + * + * @param int $schemaId Schema identifier. + * @param string $ruleKey Notification annotation key. + * + * @return int Number of rows deleted (0 on error). + */ + public function deleteByRule(int $schemaId, string $ruleKey): int + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where( + $qb->expr()->eq( + 'schema_id', + $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT) + ) + ) + ->andWhere( + $qb->expr()->eq( + 'rule_key', + $qb->createNamedParameter($ruleKey) + ) + ); + return (int) $qb->executeStatement(); + } catch (\Throwable) { + return 0; + } + + }//end deleteByRule() + + /** + * Retention sweep: drop rows last seen before the cutoff. + * + * @param DateTime $cutoff Drop rows whose `seen_at` is strictly before this. + * + * @return int Number of rows deleted (0 on error). + */ + public function deleteSeenBefore(DateTime $cutoff): int + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where( + $qb->expr()->lt( + 'seen_at', + $qb->createNamedParameter( + $cutoff->format('Y-m-d H:i:s'), + IQueryBuilder::PARAM_STR + ) + ) + ); + return (int) $qb->executeStatement(); + } catch (\Throwable) { + return 0; + } + + }//end deleteSeenBefore() + + /** + * List every rule_key currently tracked for a schema. + * + * Used by the annotation-save diff path: callers compute the set of + * rule keys currently in `x-openregister-notifications`, intersect + * with this list, and `deleteByRule()` the difference. + * + * @param int $schemaId Schema identifier. + * + * @return array Distinct rule keys. + */ + public function listRuleKeysForSchema(int $schemaId): array + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->selectDistinct('rule_key') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq( + 'schema_id', + $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT) + ) + ); + + $result = $qb->executeQuery(); + $keys = []; + while (($row = $result->fetch()) !== false) { + $keys[] = (string) ($row['rule_key'] ?? ''); + } + + $result->closeCursor(); + return $keys; + } catch (\Throwable) { + return []; + }//end try + + }//end listRuleKeysForSchema() +}//end class diff --git a/lib/Db/NotificationReadStateEntity.php b/lib/Db/NotificationReadStateEntity.php index 937e4db394..a625d769e8 100644 --- a/lib/Db/NotificationReadStateEntity.php +++ b/lib/Db/NotificationReadStateEntity.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/notificatie-engine/tasks.md "Read/unread tracking MUST be maintained per user per notification" + * @spec openspec/specs/notificatie-engine/spec.md "Read/unread tracking MUST be maintained per user per notification" */ declare(strict_types=1); diff --git a/lib/Db/NotificationReadStateMapper.php b/lib/Db/NotificationReadStateMapper.php index 8560887b4b..3d835d0d1e 100644 --- a/lib/Db/NotificationReadStateMapper.php +++ b/lib/Db/NotificationReadStateMapper.php @@ -28,7 +28,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/notificatie-engine/tasks.md "Read/unread tracking MUST be maintained per user per notification" + * @spec openspec/specs/notificatie-engine/spec.md "Read/unread tracking MUST be maintained per user per notification" * * @template-extends QBMapper */ diff --git a/lib/Db/NotificationSubscriptionMapper.php b/lib/Db/NotificationSubscriptionMapper.php index 461dcc3662..84cef5f691 100644 --- a/lib/Db/NotificationSubscriptionMapper.php +++ b/lib/Db/NotificationSubscriptionMapper.php @@ -24,7 +24,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/notificatie-engine/tasks.md "Users MUST be able to manage their notification preferences" + * @spec openspec/specs/notificatie-engine/spec.md "Users MUST be able to manage their notification preferences" * * @template-extends QBMapper * diff --git a/lib/Db/ObjectEntity.php b/lib/Db/ObjectEntity.php index 961e1b4bed..618f8aa95d 100644 --- a/lib/Db/ObjectEntity.php +++ b/lib/Db/ObjectEntity.php @@ -515,6 +515,21 @@ class ObjectEntity extends Entity implements JsonSerializable */ protected ?array $translationCompleteness = null; + /** + * Effective archival retention metadata for this object. + * + * Transient property populated by the render layer + * (`add-archival-annotation-support`) so the @self envelope can carry the + * resolved retention decision — shape: + * `['effectiveRetention' => 'P30D', 'matchedRule' => 0|null, 'expiresAt' => '...']`. + * Not persisted to the DB; derived from the schema's archival annotation + * rules at render time. Exposed in @self as `_retention`, and omitted + * entirely when not set. + * + * @var array|null + */ + protected ?array $archivalRetention = null; + /** * AVG / GDPR Art 30 processing-activity override. * @@ -648,6 +663,30 @@ public function setTranslationCompleteness(?array $translationCompleteness): voi $this->translationCompleteness = $translationCompleteness; }//end setTranslationCompleteness() + /** + * Read the effective archival retention metadata. + * + * @return array|null The retention decision, or null when not set. + */ + public function getArchivalRetention(): ?array + { + return $this->archivalRetention; + }//end getArchivalRetention() + + /** + * Write the effective archival retention metadata. + * + * Surfaced in the @self envelope as `_retention` by getObjectArray(). + * + * @param array|null $retention The resolved retention decision. + * + * @return void + */ + public function setArchivalRetention(?array $retention): void + { + $this->archivalRetention = $retention; + }//end setArchivalRetention() + /** * Initialize the entity and define field types */ @@ -955,6 +994,13 @@ public function getObjectArray(array $object=[]): array $objectArray['translationCompleteness'] = $this->translationCompleteness; } + // Add the effective archival retention decision when set by the render + // layer (add-archival-annotation-support). Exposed as `_retention` and + // omitted entirely when the object carries no retention metadata. + if ($this->archivalRetention !== null) { + $objectArray['_retention'] = $this->archivalRetention; + } + // Check for '@self' in the provided object array (this is the case if the object metadata is extended). if (($object['@self'] ?? null) !== null && is_array($object['@self']) === true) { $self = $object['@self']; diff --git a/lib/Db/ObjectHandlers/HyperFacetHandler.php b/lib/Db/ObjectHandlers/HyperFacetHandler.php index 5f6810d90f..50027066d4 100644 --- a/lib/Db/ObjectHandlers/HyperFacetHandler.php +++ b/lib/Db/ObjectHandlers/HyperFacetHandler.php @@ -495,7 +495,36 @@ private function calculateExactFacetsParallel(array $facetConfig, array $baseQue } // Execute all facet calculations in parallel. @psalm-suppress UndefinedFunction. - $results = \React\Async\await(\React\Promise\all($promises)); + // react/async is a runtime dep of this app; if a deployment ships a + // pruned vendor/ (no react/async) the call fatals with "Call to + // undefined function React\Async\await()". The facet promises here + // resolve synchronously inside their executors (no I/O loop), so + // when await() is unavailable we can extract values via then(). + if (function_exists('React\\Async\\await') === true) { + $results = \React\Async\await(\React\Promise\all($promises)); + } else { + $this->logger->warning( + message: '[HyperFacetHandler] React\\Async\\await() unavailable; falling back to synchronous' + .' promise extraction (install react/async to silence)', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + $results = []; + $caught = null; + foreach ($promises as $key => $promise) { + $promise->then( + function ($value) use (&$results, $key) { + $results[$key] = $value; + }, + function (\Throwable $e) use (&$caught) { + $caught = $e; + } + ); + } + + if ($caught !== null) { + throw $caught; + } + }//end if // Combine results from different facet types. $combinedFacets = []; diff --git a/lib/Db/ObjectHandling.php b/lib/Db/ObjectHandling.php new file mode 100644 index 0000000000..ed7f766b73 --- /dev/null +++ b/lib/Db/ObjectHandling.php @@ -0,0 +1,89 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +/** + * The `objectConfiguration.handling` modes for an object-typed property. + * + * Two of them mean "the target lives as its OWN object; store only a UUID reference + * to it": `related-object` and `related-schema`. They are storage-identical — the + * documentation defines `related-schema` as "stores object as a separate entity and + * references it by UUID/ID only", which is exactly what `related-object` does. + * + * They drifted apart anyway: the write path only ever checked `related-object`, so a + * property configured as `related-schema` — the mode the docs describe and the schema + * editor offers — got a `json` COLUMN while the save path wrote a bare UUID STRING + * into it. PostgreSQL rejected every such save with + * + * SQLSTATE[22P02]: invalid input syntax for type json + * DETAIL: Token "b25f5f9c" is invalid. + * + * surfaced to the user as a bare "You do not have permission to perform this action". + * Making a property a relation was impossible unless you happened to pick the one + * undocumented handling value the code recognised. + * + * This class exists so that question is answered in ONE place. Add a new relating + * mode here, not by grepping for `=== 'related-object'` across four services. + * + * @spec exclude value object enumerating objectConfiguration.handling modes + */ +final class ObjectHandling +{ + /** + * Reference an existing object by UUID. + */ + public const RELATED_OBJECT = 'related-object'; + + /** + * Reference an object of a given schema by UUID. Storage-identical to + * {@see self::RELATED_OBJECT}; this is the mode the docs and the schema editor use. + */ + public const RELATED_SCHEMA = 'related-schema'; + + /** + * Embed the object inline in the parent. + */ + public const NESTED_OBJECT = 'nested-object'; + + /** + * Store the object separately but embed it in API responses. + */ + public const NESTED_SCHEMA = 'nested-schema'; + + /** + * Reference the object by URI. + */ + public const URI = 'uri'; + + /** + * Whether this handling stores only a UUID reference to a separately-stored object. + * + * A property whose handling relates gets a VARCHAR(255) UUID column, NOT a json + * column — the value written is a bare UUID string, which is not valid JSON. + * + * @param string|null $handling The `objectConfiguration.handling` value. + * + * @return bool True when the property stores a UUID reference. + * + * @spec exclude pure predicate over the handling enum (no state, no I/O) + */ + public static function relates(?string $handling): bool + { + return in_array($handling, [self::RELATED_OBJECT, self::RELATED_SCHEMA], true); + }//end relates() +}//end class diff --git a/lib/Db/ProcessingLogEntry.php b/lib/Db/ProcessingLogEntry.php new file mode 100644 index 0000000000..78a2cfd3b8 --- /dev/null +++ b/lib/Db/ProcessingLogEntry.php @@ -0,0 +1,220 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Append-only processing-log entry entity. + * + * @method string|null getUuid() + * @method void setUuid(?string $uuid) + * @method string|null getActivityId() + * @method void setActivityId(?string $activityId) + * @method string|null getAction() + * @method void setAction(?string $action) + * @method string|null getActor() + * @method void setActor(?string $actor) + * @method string|null getChannel() + * @method void setChannel(?string $channel) + * @method string|null getRegisterId() + * @method void setRegisterId(?string $registerId) + * @method string|null getSchemaId() + * @method void setSchemaId(?string $schemaId) + * @method string|null getObjectUuid() + * @method void setObjectUuid(?string $objectUuid) + * @method string|null getSubjectIdType() + * @method void setSubjectIdType(?string $subjectIdType) + * @method string|null getSubjectIdValue() + * @method void setSubjectIdValue(?string $subjectIdValue) + * @method int|null getObjectCount() + * @method void setObjectCount(?int $objectCount) + * @method bool|null getConfidential() + * @method void setConfidential(?bool $confidential) + * @method string|null getOrganisationId() + * @method void setOrganisationId(?string $organisationId) + * @method DateTime|null getCreated() + * @method void setCreated(?DateTime $created) + */ +class ProcessingLogEntry extends Entity implements JsonSerializable +{ + + /** + * VNG resource identity. + * + * @var string|null + */ + protected ?string $uuid = null; + + /** + * Referenced Verwerkingsactiviteit uuid. + * + * @var string|null + */ + protected ?string $activityId = null; + + /** + * Processing action: `read` | `export`. + * + * @var string|null + */ + protected ?string $action = 'read'; + + /** + * NC user id or API-client identifier. + * + * @var string|null + */ + protected ?string $actor = null; + + /** + * Access channel: ui | api | graphql | mcp | public | background. + * + * @var string|null + */ + protected ?string $channel = 'ui'; + + /** + * Register identifier (scoping slice). + * + * @var string|null + */ + protected ?string $registerId = null; + + /** + * Schema identifier (scoping slice). + * + * @var string|null + */ + protected ?string $schemaId = null; + + /** + * Read object uuid (null for collapsed list/search entries). + * + * @var string|null + */ + protected ?string $objectUuid = null; + + /** + * Data-subject identifier type (e.g. `BSN`). + * + * @var string|null + */ + protected ?string $subjectIdType = null; + + /** + * Data-subject identifier value. + * + * @var string|null + */ + protected ?string $subjectIdValue = null; + + /** + * Number of objects covered by this entry (bulk/list collapse). + * + * @var integer|null + */ + protected ?int $objectCount = 1; + + /** + * Denormalised confidentiality marker from the activity (FG-gating). + * + * @var boolean|null + */ + protected ?bool $confidential = false; + + /** + * Tenant identifier for multi-tenant isolation. + * + * @var string|null + */ + protected ?string $organisationId = null; + + /** + * Append timestamp (period queries + retention prune). + * + * @var DateTime|null + */ + protected ?DateTime $created = null; + + /** + * Register the entity's typed columns. + */ + public function __construct() + { + $this->addType(fieldName: 'uuid', type: 'string'); + $this->addType(fieldName: 'activityId', type: 'string'); + $this->addType(fieldName: 'action', type: 'string'); + $this->addType(fieldName: 'actor', type: 'string'); + $this->addType(fieldName: 'channel', type: 'string'); + $this->addType(fieldName: 'registerId', type: 'string'); + $this->addType(fieldName: 'schemaId', type: 'string'); + $this->addType(fieldName: 'objectUuid', type: 'string'); + $this->addType(fieldName: 'subjectIdType', type: 'string'); + $this->addType(fieldName: 'subjectIdValue', type: 'string'); + $this->addType(fieldName: 'objectCount', type: 'integer'); + $this->addType(fieldName: 'confidential', type: 'boolean'); + $this->addType(fieldName: 'organisationId', type: 'string'); + $this->addType(fieldName: 'created', type: 'datetime'); + + }//end __construct() + + /** + * Render the entry as the canonical VNG verwerkingsactie JSON shape. + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'activityId' => $this->activityId, + 'action' => $this->action, + 'actor' => $this->actor, + 'channel' => $this->channel, + 'registerId' => $this->registerId, + 'schemaId' => $this->schemaId, + 'objectUuid' => $this->objectUuid, + 'subjectIdType' => $this->subjectIdType, + 'subjectIdValue' => $this->subjectIdValue, + 'objectCount' => $this->objectCount, + 'confidential' => $this->confidential, + 'organisationId' => $this->organisationId, + 'created' => $this->created?->format('c'), + ]; + + }//end jsonSerialize() +}//end class diff --git a/lib/Db/ProcessingLogMapper.php b/lib/Db/ProcessingLogMapper.php new file mode 100644 index 0000000000..54a45e2616 --- /dev/null +++ b/lib/Db/ProcessingLogMapper.php @@ -0,0 +1,311 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use Symfony\Component\Uid\Uuid; + +/** + * Mapper class for ProcessingLogEntry rows. + * + * @template-extends QBMapper + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ +class ProcessingLogMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct( + db: $db, + tableName: 'openregister_processing_log', + entityClass: ProcessingLogEntry::class + ); + + }//end __construct() + + /** + * Insert a single entry, auto-filling uuid + created. + * + * @param ProcessingLogEntry $entity Entry to insert. + * + * @return ProcessingLogEntry Persisted entry with id populated. + */ + public function insert($entity): ProcessingLogEntry + { + if ($entity->getUuid() === null || $entity->getUuid() === '') { + $entity->setUuid(Uuid::v4()->toRfc4122()); + } + + if ($entity->getCreated() === null) { + $entity->setCreated(new DateTime()); + } + + if ($entity->getObjectCount() === null) { + $entity->setObjectCount(1); + } + + return parent::insert(entity: $entity); + + }//end insert() + + /** + * Append a batch of entries in one transaction. + * + * Used by the deferred emission flush so a request's entries land as + * one batched write rather than a per-row synchronous insert. + * + * @param array $entries Entries to append. + * + * @return int Number of entries persisted. + */ + public function insertBatch(array $entries): int + { + if ($entries === []) { + return 0; + } + + $count = 0; + $this->db->beginTransaction(); + try { + foreach ($entries as $entry) { + $this->insert(entity: $entry); + $count++; + } + + $this->db->commit(); + } catch (\Throwable $e) { + $this->db->rollBack(); + throw $e; + } + + return $count; + + }//end insertBatch() + + /** + * Find entries by data-subject identifier within a period. + * + * @param string $idType Subject identifier type (e.g. `BSN`). + * @param string $idValue Subject identifier value. + * @param DateTime|null $from Inclusive lower bound. + * @param DateTime|null $to Inclusive upper bound. + * @param string|null $organisationId Tenant scope (null = all, admin only). + * @param bool $includeConfidential Whether to include confidential entries (FG only). + * + * @return ProcessingLogEntry[] + */ + public function findBySubject( + string $idType, + string $idValue, + ?DateTime $from=null, + ?DateTime $to=null, + ?string $organisationId=null, + bool $includeConfidential=false + ): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('subject_id_type', $qb->createNamedParameter($idType))) + ->andWhere($qb->expr()->eq('subject_id_value', $qb->createNamedParameter($idValue))); + + $this->applyCommonFilters( + qb: $qb, + from: $from, + to: $to, + organisationId: $organisationId, + includeConfidential: $includeConfidential + ); + + $qb->orderBy('created', 'ASC'); + + return $this->findEntities(query: $qb); + + }//end findBySubject() + + /** + * Find entries by an arbitrary filter set (FG inquiry / VNG API). + * + * @param array $filters register_id, schema_id, activity_id, actor, action, subject_id_type, subject_id_value. + * @param DateTime|null $from Inclusive lower bound. + * @param DateTime|null $to Inclusive upper bound. + * @param string|null $organisationId Tenant scope (null = all, admin only). + * @param bool $includeConfidential Whether to include confidential entries (FG only). + * @param int $limit Page size. + * @param int $offset Page offset. + * + * @return ProcessingLogEntry[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function findFiltered( + array $filters=[], + ?DateTime $from=null, + ?DateTime $to=null, + ?string $organisationId=null, + bool $includeConfidential=false, + int $limit=100, + int $offset=0 + ): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*')->from($this->getTableName()); + + foreach (['register_id', 'schema_id', 'activity_id', 'actor', 'action', 'subject_id_type', 'subject_id_value'] as $column) { + $value = ($filters[$column] ?? null); + if ($value !== null && $value !== '') { + $qb->andWhere($qb->expr()->eq($column, $qb->createNamedParameter((string) $value))); + } + } + + $this->applyCommonFilters( + qb: $qb, + from: $from, + to: $to, + organisationId: $organisationId, + includeConfidential: $includeConfidential + ); + + $qb->orderBy('created', 'DESC') + ->setMaxResults($limit) + ->setFirstResult($offset); + + return $this->findEntities(query: $qb); + + }//end findFiltered() + + /** + * Count entries attributed to each activity, optionally per register. + * + * Used by the compliance surface to expose the flagged-fallback gap. + * + * @param string|null $organisationId Tenant scope. + * @param string|null $registerId Optional register slice. + * + * @return array Map of activity uuid => entry count. + */ + public function countByActivity(?string $organisationId=null, ?string $registerId=null): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('activity_id') + ->selectAlias($qb->func()->count('*'), 'cnt') + ->from($this->getTableName()) + ->groupBy('activity_id'); + + if ($organisationId !== null && $organisationId !== '') { + $qb->andWhere($qb->expr()->eq('organisation_id', $qb->createNamedParameter($organisationId))); + } + + if ($registerId !== null && $registerId !== '') { + $qb->andWhere($qb->expr()->eq('register_id', $qb->createNamedParameter($registerId))); + } + + $result = $qb->executeQuery(); + $counts = []; + while (($row = $result->fetch()) !== false) { + $counts[(string) $row['activity_id']] = (int) $row['cnt']; + } + + $result->closeCursor(); + + return $counts; + + }//end countByActivity() + + /** + * Hard-delete entries older than the cutoff (retention prune). + * + * @param DateTime $cutoff Entries with `created` strictly before this are deleted. + * + * @return int Number of rows deleted. + */ + public function deleteCreatedBefore(DateTime $cutoff): int + { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->lt('created', $qb->createNamedParameter($cutoff, IQueryBuilder::PARAM_DATETIME_MUTABLE))); + + return $qb->executeStatement(); + + }//end deleteCreatedBefore() + + /** + * Apply the period + tenant + confidentiality filters shared by the + * read helpers. + * + * @param IQueryBuilder $qb Query under construction. + * @param DateTime|null $from Inclusive lower bound. + * @param DateTime|null $to Inclusive upper bound. + * @param string|null $organisationId Tenant scope. + * @param bool $includeConfidential Whether to include confidential entries. + * + * @return void + */ + private function applyCommonFilters( + IQueryBuilder $qb, + ?DateTime $from, + ?DateTime $to, + ?string $organisationId, + bool $includeConfidential + ): void { + if ($from !== null) { + $qb->andWhere( + $qb->expr()->gte('created', $qb->createNamedParameter($from, IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ); + } + + if ($to !== null) { + $qb->andWhere( + $qb->expr()->lte('created', $qb->createNamedParameter($to, IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ); + } + + if ($organisationId !== null && $organisationId !== '') { + $qb->andWhere( + $qb->expr()->eq('organisation_id', $qb->createNamedParameter($organisationId)) + ); + } + + if ($includeConfidential === false) { + $qb->andWhere( + $qb->expr()->eq('confidential', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)) + ); + } + + }//end applyCommonFilters() +}//end class diff --git a/lib/Db/PushSubscription.php b/lib/Db/PushSubscription.php new file mode 100644 index 0000000000..aed34e333c --- /dev/null +++ b/lib/Db/PushSubscription.php @@ -0,0 +1,127 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class PushSubscription + * + * @method string|null getUserId() + * @method void setUserId(?string $userId) + * @method string|null getEndpoint() + * @method void setEndpoint(?string $endpoint) + * @method string|null getP256dh() + * @method void setP256dh(?string $p256dh) + * @method string|null getAuth() + * @method void setAuth(?string $auth) + * @method string|null getUserAgent() + * @method void setUserAgent(?string $userAgent) + * @method DateTime|null getCreatedAt() + * @method void setCreatedAt(?DateTime $createdAt) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class PushSubscription extends Entity implements JsonSerializable +{ + + /** + * The owning Nextcloud user id. + * + * @var string|null + */ + protected ?string $userId = null; + + /** + * The push service endpoint URL (FCM / Mozilla / Apple). + * + * @var string|null + */ + protected ?string $endpoint = null; + + /** + * The client public key (P-256 ECDH) used for payload encryption. + * + * @var string|null + */ + protected ?string $p256dh = null; + + /** + * The client auth secret used for payload encryption. + * + * @var string|null + */ + protected ?string $auth = null; + + /** + * The browser user agent at subscribe time (diagnostics only). + * + * @var string|null + */ + protected ?string $userAgent = null; + + /** + * When the subscription was stored. + * + * @var DateTime|null + */ + protected ?DateTime $createdAt = null; + + /** + * Constructor. + */ + public function __construct() + { + $this->addType(fieldName: 'userId', type: 'string'); + $this->addType(fieldName: 'endpoint', type: 'string'); + $this->addType(fieldName: 'p256dh', type: 'string'); + $this->addType(fieldName: 'auth', type: 'string'); + $this->addType(fieldName: 'userAgent', type: 'string'); + $this->addType(fieldName: 'createdAt', type: 'datetime'); + }//end __construct() + + /** + * JSON serialization (never exposes another user's row — controller-gated). + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'endpoint' => $this->endpoint, + 'userAgent' => $this->userAgent, + 'createdAt' => $this->createdAt?->format(DateTime::ATOM), + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/PushSubscriptionMapper.php b/lib/Db/PushSubscriptionMapper.php new file mode 100644 index 0000000000..28ae545a53 --- /dev/null +++ b/lib/Db/PushSubscriptionMapper.php @@ -0,0 +1,171 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use OCP\AppFramework\Db\QBMapper; +use OCP\IDBConnection; + +/** + * Class PushSubscriptionMapper + * + * @template-extends QBMapper + */ +class PushSubscriptionMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_push_subscriptions', entityClass: PushSubscription::class); + }//end __construct() + + /** + * Find every push subscription owned by a user. + * + * @param string $userId The owning Nextcloud user id. + * + * @return PushSubscription[] The user's subscriptions. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function findByUser(string $userId): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); + + return $this->findEntities(query: $qb); + }//end findByUser() + + /** + * Find a single subscription by owner + endpoint, or null when absent. + * + * @param string $userId The owning user id. + * @param string $endpoint The push endpoint URL. + * + * @return PushSubscription|null The matching subscription, or null. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function findByUserAndEndpoint(string $userId, string $endpoint): ?PushSubscription + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('endpoint', $qb->createNamedParameter($endpoint))) + ->setMaxResults(1); + + $rows = $this->findEntities(query: $qb); + if (count($rows) === 0) { + return null; + } + + return $rows[0]; + }//end findByUserAndEndpoint() + + /** + * Store (upsert) a subscription for a user + endpoint. + * + * Re-subscribing the same endpoint refreshes the keys rather than + * creating a duplicate row. + * + * @param string $userId The owning user id. + * @param string $endpoint The push endpoint URL. + * @param string $p256dh The client P-256 public key. + * @param string $auth The client auth secret. + * @param string $userAgent The browser user agent (diagnostics). + * + * @return PushSubscription The persisted subscription. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function store(string $userId, string $endpoint, string $p256dh, string $auth, string $userAgent): PushSubscription + { + $existing = $this->findByUserAndEndpoint(userId: $userId, endpoint: $endpoint); + if ($existing !== null) { + $existing->setP256dh($p256dh); + $existing->setAuth($auth); + $existing->setUserAgent($userAgent); + return $this->update(entity: $existing); + } + + $entity = new PushSubscription(); + $entity->setUserId($userId); + $entity->setEndpoint($endpoint); + $entity->setP256dh($p256dh); + $entity->setAuth($auth); + $entity->setUserAgent($userAgent); + $entity->setCreatedAt(new DateTime()); + + return $this->insert(entity: $entity); + }//end store() + + /** + * Delete a user's subscription for a specific endpoint. + * + * @param string $userId The owning user id. + * @param string $endpoint The push endpoint URL. + * + * @return int Number of deleted rows (0 or 1). + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function deleteByUserAndEndpoint(string $userId, string $endpoint): int + { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('endpoint', $qb->createNamedParameter($endpoint))); + + return $qb->executeStatement(); + }//end deleteByUserAndEndpoint() + + /** + * Delete a subscription by its endpoint only (used when a push service + * returns 404/410 Gone — the endpoint is dead regardless of owner). + * + * @param string $endpoint The push endpoint URL. + * + * @return int Number of deleted rows. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function deleteByEndpoint(string $endpoint): int + { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('endpoint', $qb->createNamedParameter($endpoint))); + + return $qb->executeStatement(); + }//end deleteByEndpoint() +}//end class diff --git a/lib/Db/QueuedNotification.php b/lib/Db/QueuedNotification.php new file mode 100644 index 0000000000..3793bc6cc6 --- /dev/null +++ b/lib/Db/QueuedNotification.php @@ -0,0 +1,203 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/notification-delivery-windows/design.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * QueuedNotification. + * + * @method int getSchemaId() + * @method void setSchemaId(int $schemaId) + * @method string getRuleKey() + * @method void setRuleKey(string $ruleKey) + * @method string getRecipient() + * @method void setRecipient(string $recipient) + * @method string getReason() + * @method void setReason(string $reason) + * @method string|null getObjectUuid() + * @method void setObjectUuid(?string $objectUuid) + * @method string getPayload() + * @method void setPayload(string $payload) + * @method DateTime getDueAtHint() + * @method void setDueAtHint(DateTime $dueAtHint) + * @method DateTime getCreatedAt() + * @method void setCreatedAt(DateTime $createdAt) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class QueuedNotification extends Entity implements JsonSerializable +{ + + /** + * `reason` value when the row was queued because the recipient is + * inside an active quiet-hours delivery window. + */ + public const REASON_QUIET_HOURS = 'quiet-hours'; + + /** + * `reason` value when the row was queued because the rule's fixed-time + * `digest` schedule has not yet reached its next fire time. + */ + public const REASON_DIGEST_SCHEDULE = 'digest-schedule'; + + /** + * `reason` value when BOTH conditions applied at enqueue time (the + * recipient was inside their window AND the rule's digest schedule + * had not yet fired). Flush still waits for both to clear. + */ + public const REASON_BOTH = 'quiet-hours+digest-schedule'; + + /** + * Owning schema id. + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * Notification annotation key (rule key in `x-openregister-notifications`). + * + * @var string|null + */ + protected ?string $ruleKey = null; + + /** + * Recipient user UID. + * + * @var string|null + */ + protected ?string $recipient = null; + + /** + * Why the row was queued: `quiet-hours` | `digest-schedule` | + * `quiet-hours+digest-schedule`. Advisory only — the flush job always + * re-evaluates live rather than trusting this label (see design.md + * "Live re-evaluation at flush time"). + * + * @var string|null + */ + protected ?string $reason = null; + + /** + * The triggering object's UUID, when applicable. + * + * @var string|null + */ + protected ?string $objectUuid = null; + + /** + * Pre-resolved subject/message/channels/context, JSON-encoded, so the + * eventual flush does not need to re-run recipient/template resolution. + * + * @var string|null + */ + protected ?string $payload = null; + + /** + * Advisory "roughly when this should flush" timestamp, for operator + * visibility only — never the sole flush trigger (see design.md). + * + * @var DateTime|null + */ + protected ?DateTime $dueAtHint = null; + + /** + * Row creation timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $createdAt = null; + + /** + * Constructor. + */ + public function __construct() + { + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'ruleKey', type: 'string'); + $this->addType(fieldName: 'recipient', type: 'string'); + $this->addType(fieldName: 'reason', type: 'string'); + $this->addType(fieldName: 'objectUuid', type: 'string'); + $this->addType(fieldName: 'payload', type: 'string'); + $this->addType(fieldName: 'dueAtHint', type: 'datetime'); + $this->addType(fieldName: 'createdAt', type: 'datetime'); + + }//end __construct() + + /** + * Decode the JSON `payload` column into an array. + * + * @return array + */ + public function getDecodedPayload(): array + { + $decoded = json_decode((string) $this->payload, true); + if (is_array($decoded) === false) { + return []; + } + + return $decoded; + + }//end getDecodedPayload() + + /** + * JSON serialization. + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'schemaId' => $this->schemaId, + 'ruleKey' => $this->ruleKey, + 'recipient' => $this->recipient, + 'reason' => $this->reason, + 'objectUuid' => $this->objectUuid, + 'payload' => $this->getDecodedPayload(), + 'dueAtHint' => $this->dueAtHint?->format(DateTime::ATOM), + 'createdAt' => $this->createdAt?->format(DateTime::ATOM), + ]; + + }//end jsonSerialize() +}//end class diff --git a/lib/Db/QueuedNotificationMapper.php b/lib/Db/QueuedNotificationMapper.php new file mode 100644 index 0000000000..b420153d07 --- /dev/null +++ b/lib/Db/QueuedNotificationMapper.php @@ -0,0 +1,187 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/notification-delivery-windows/design.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class QueuedNotificationMapper. + * + * @method QueuedNotification insert(Entity $entity) + * @method QueuedNotification update(Entity $entity) + * @method QueuedNotification delete(Entity $entity) + * + * @template-extends QBMapper + * + * @psalm-suppress PossiblyUnusedMethod + */ +class QueuedNotificationMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct( + db: $db, + tableName: 'openregister_notification_queue', + entityClass: QueuedNotification::class + ); + + }//end __construct() + + /** + * Every currently-queued row, oldest first. + * + * The flush job re-evaluates each row's holding condition live rather + * than filtering "due" rows in SQL — see class docblock. + * + * @return array + */ + public function findAll(): array + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->orderBy('created_at', 'ASC'); + + return $this->findEntities(query: $qb); + } catch (\Throwable $e) { + // A read failure must not break the flush job's tick — treat + // it as "nothing queued this pass" and let the next tick retry. + return []; + } + + }//end findAll() + + /** + * All queued rows for one `(ruleKey, recipient)` pair — the grouping + * key the flush job uses to merge sibling events into one digest. + * + * @param string $ruleKey Notification annotation key. + * @param string $recipient Recipient user UID. + * + * @return array + */ + public function findByRecipientAndRule(string $ruleKey, string $recipient): array + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq( + 'rule_key', + $qb->createNamedParameter($ruleKey) + ) + ) + ->andWhere( + $qb->expr()->eq( + 'recipient', + $qb->createNamedParameter($recipient) + ) + ) + ->orderBy('created_at', 'ASC'); + + return $this->findEntities(query: $qb); + } catch (\Throwable $e) { + return []; + }//end try + + }//end findByRecipientAndRule() + + /** + * Look up a single row by id (used by tests / operator tooling). + * + * @param int $id Row id. + * + * @return QueuedNotification|null + */ + public function findById(int $id): ?QueuedNotification + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq( + 'id', + $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT) + ) + ) + ->setMaxResults(1); + + return $this->findEntity(query: $qb); + } catch (DoesNotExistException $e) { + return null; + } catch (\Throwable $e) { + return null; + } + + }//end findById() + + /** + * Delete a queued row by id (called once its contents have been + * flushed into a delivered notification). + * + * @param int $id Row id. + * + * @return void + */ + public function deleteById(int $id): void + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where( + $qb->expr()->eq( + 'id', + $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT) + ) + ); + $qb->executeStatement(); + } catch (\Throwable $e) { + // Best-effort: worst case a stale row survives until the next + // sweep re-attempts the delete after a redundant re-flush. + } + + }//end deleteById() +}//end class diff --git a/lib/Db/RealtimeEvent.php b/lib/Db/RealtimeEvent.php deleted file mode 100644 index f8f71f6a9b..0000000000 --- a/lib/Db/RealtimeEvent.php +++ /dev/null @@ -1,176 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Db; - -use DateTime; -use OCP\AppFramework\Db\Entity; -use JsonSerializable; - -/** - * Append-only realtime event row. - * - * @method int getId() - * @method void setId(int $id) - * @method string|null getEventType() - * @method void setEventType(?string $eventType) - * @method string|null getSource() - * @method void setSource(?string $source) - * @method string|null getSubject() - * @method void setSubject(?string $subject) - * @method string|null getRegisterId() - * @method void setRegisterId(?string $registerId) - * @method string|null getSchemaId() - * @method void setSchemaId(?string $schemaId) - * @method string|null getObjectUuid() - * @method void setObjectUuid(?string $objectUuid) - * @method string|null getActorUid() - * @method void setActorUid(?string $actorUid) - * @method string|null getOrganisation() - * @method void setOrganisation(?string $organisation) - * @method string|null getPayload() - * @method void setPayload(?string $payload) - * @method DateTime|null getCreated() - * @method void setCreated(?DateTime $created) - */ -class RealtimeEvent extends Entity implements JsonSerializable -{ - - /** - * CloudEvent type, e.g. `nl.openregister.object.updated`. - * - * @var string|null - */ - protected ?string $eventType = null; - - /** - * CloudEvent source URI for this event. - * - * @var string|null - */ - protected ?string $source = null; - - /** - * Optional CloudEvent subject (typically the object UUID). - * - * @var string|null - */ - protected ?string $subject = null; - - /** - * Register identifier (denormalised for filtering). - * - * @var string|null - */ - protected ?string $registerId = null; - - /** - * Schema identifier (denormalised for filtering). - * - * @var string|null - */ - protected ?string $schemaId = null; - - /** - * UUID of the object the event is about. - * - * @var string|null - */ - protected ?string $objectUuid = null; - - /** - * UID of the actor that triggered the event. - * - * @var string|null - */ - protected ?string $actorUid = null; - - /** - * Organisation identifier the event belongs to. - * - * @var string|null - */ - protected ?string $organisation = null; - - /** - * Full CloudEvent payload as JSON string. - * - * @var string|null - */ - protected ?string $payload = null; - - /** - * Creation timestamp for the event row. - * - * @var DateTime|null - */ - protected ?DateTime $created = null; - - /** - * Configure typed columns for the entity. - * - * @return void - */ - public function __construct() - { - $this->addType(fieldName: 'eventType', type: 'string'); - $this->addType(fieldName: 'source', type: 'string'); - $this->addType(fieldName: 'subject', type: 'string'); - $this->addType(fieldName: 'registerId', type: 'string'); - $this->addType(fieldName: 'schemaId', type: 'string'); - $this->addType(fieldName: 'objectUuid', type: 'string'); - $this->addType(fieldName: 'actorUid', type: 'string'); - $this->addType(fieldName: 'organisation', type: 'string'); - $this->addType(fieldName: 'payload', type: 'string'); - $this->addType(fieldName: 'created', type: 'datetime'); - }//end __construct() - - /** - * Serialize the event in CloudEvents-1.0-compatible shape. - * - * The stored `payload` is the canonical CloudEvent (already a JSON - * string emitted by `RealtimeService`); on serialise we decode it - * and tag the row id so clients can use it as the next `since` - * cursor. - * - * @return array - */ - public function jsonSerialize(): array - { - $payload = null; - if ($this->payload !== null) { - $payload = json_decode($this->payload, true); - } - - if (is_array($payload) === false) { - $payload = []; - } - - $payload['_cursor'] = $this->id; - return $payload; - }//end jsonSerialize() -}//end class diff --git a/lib/Db/RealtimeEventMapper.php b/lib/Db/RealtimeEventMapper.php deleted file mode 100644 index 236c7198aa..0000000000 --- a/lib/Db/RealtimeEventMapper.php +++ /dev/null @@ -1,166 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Db; - -use DateTime; -use OCP\AppFramework\Db\QBMapper; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; - -/** - * Mapper for the realtime event log table. - * - * @template-extends QBMapper - */ -class RealtimeEventMapper extends QBMapper -{ - /** - * Construct the mapper bound to the realtime events table. - * - * @param IDBConnection $db Database connection handle. - * - * @return void - */ - public function __construct(IDBConnection $db) - { - parent::__construct(db: $db, tableName: 'openregister_realtime_events', entityClass: RealtimeEvent::class); - }//end __construct() - - /** - * Find events with id strictly greater than `$since` (or all events - * when `$since` is null), most-recent-id-first. Optional filters: - * - * - `register` — exact registerId match - * - `schema` — exact schemaId match - * - `objectUuid` — exact uuid match (per-object subscriptions) - * - `eventType` — exact event-type match (e.g. `or.object.updated`) - * - `organisation` — exact organisation match (multi-tenancy gate) - * - * @param int|null $since Lower-bound cursor; null returns from the beginning. - * @param int $limit Maximum number of rows to return. - * @param array $filters Optional filter map (see column map above). - * - * @return RealtimeEvent[] - * - * @psalm-return list - */ - public function findSince(?int $since=null, int $limit=100, array $filters=[]): array - { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from('openregister_realtime_events'); - - if ($since !== null) { - $qb->andWhere( - $qb->expr()->gt('id', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)) - ); - } - - $columnMap = [ - 'register' => 'register_id', - 'schema' => 'schema_id', - 'objectUuid' => 'object_uuid', - 'eventType' => 'event_type', - 'organisation' => 'organisation', - ]; - foreach ($filters as $key => $value) { - if (isset($columnMap[$key]) === false || $value === null || $value === '') { - continue; - } - - $qb->andWhere( - $qb->expr()->eq($columnMap[$key], $qb->createNamedParameter((string) $value)) - ); - } - - $qb->orderBy('id', 'ASC'); - $qb->setMaxResults(max(1, min(1000, $limit))); - - return $this->findEntities(query: $qb); - }//end findSince() - - /** - * Get the highest id in the log — used by clients to fast-forward - * past historical events on initial subscription. - * - * @return int Highest event id, or 0 when the log is empty. - */ - public function getMaxId(): int - { - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->createFunction('MAX(id) AS max_id')) - ->from('openregister_realtime_events'); - $result = $qb->executeQuery()->fetch(); - return (int) ($result['max_id'] ?? 0); - }//end getMaxId() - - /** - * Get the highest event id scoped to a specific organisation. - * - * SECURITY: the unfiltered `getMaxId()` returns the global head - * pointer, which lets any caller observe the global write rate by - * polling. Per-organisation cursors prevent that side-channel. - * - * @param string $organisationUuid Organisation UUID. - * - * @return int Highest event id for the organisation, or 0 when none. - */ - public function getMaxIdForOrganisation(string $organisationUuid): int - { - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->createFunction('MAX(id) AS max_id')) - ->from('openregister_realtime_events') - ->where( - $qb->expr()->eq( - 'organisation', - $qb->createNamedParameter($organisationUuid) - ) - ); - $result = $qb->executeQuery()->fetch(); - return (int) ($result['max_id'] ?? 0); - - }//end getMaxIdForOrganisation() - - /** - * Prune events older than `$retentionSeconds`. Used by a daily TimedJob - * to keep the event log bounded. - * - * @param int $retentionSeconds Maximum age in seconds before rows are deleted. - * - * @return int Number of rows deleted. - */ - public function deleteOlderThan(int $retentionSeconds): int - { - $cutoff = (new DateTime())->modify("-{$retentionSeconds} seconds"); - $qb = $this->db->getQueryBuilder(); - $qb->delete('openregister_realtime_events') - ->where( - $qb->expr()->lt('created', $qb->createNamedParameter($cutoff, IQueryBuilder::PARAM_DATE)) - ); - return $qb->executeStatement(); - }//end deleteOlderThan() -}//end class diff --git a/lib/Db/Register.php b/lib/Db/Register.php index 86a2354e19..2d1c37edd6 100644 --- a/lib/Db/Register.php +++ b/lib/Db/Register.php @@ -230,28 +230,6 @@ class Register extends Entity implements JsonSerializable */ protected ?DateTime $deleted = null; - /** - * Publication timestamp. - * - * When set, this register becomes publicly accessible regardless of organisation restrictions - * if published bypass is enabled. The register is considered published when: - * - published <= now AND - * - (depublished IS NULL OR depublished > now) - * - * @var DateTime|null Publication timestamp - */ - protected ?DateTime $published = null; - - /** - * Depublication timestamp. - * - * When set, this register becomes inaccessible after this date/time. - * Used together with published to control publication lifecycle. - * - * @var DateTime|null Depublication timestamp - */ - protected ?DateTime $depublished = null; - /** * Register classification, sourced from `x-openregister.type` on import. * @@ -369,8 +347,6 @@ public function __construct() $this->addType(fieldName: 'authorization', type: 'json'); $this->addType(fieldName: 'groups', type: 'json'); $this->addType(fieldName: 'deleted', type: 'datetime'); - $this->addType(fieldName: 'published', type: 'datetime'); - $this->addType(fieldName: 'depublished', type: 'datetime'); $this->addType(fieldName: 'type', type: 'string'); $this->addType(fieldName: 'languages', type: 'json'); $this->addType(fieldName: 'configuration', type: 'json'); @@ -523,9 +499,7 @@ public function hydrate(array $object): static * users: 0, * groups: int<0, max> * }, - * deleted: null|string, - * published: null|string, - * depublished: null|string + * deleted: null|string * } */ public function jsonSerialize(): array @@ -545,16 +519,6 @@ public function jsonSerialize(): array $deleted = $this->deleted->format('c'); } - $published = null; - if (isset($this->published) === true) { - $published = $this->published->format('c'); - } - - $depublished = null; - if (isset($this->depublished) === true) { - $depublished = $this->depublished->format('c'); - } - // Always return schemas as array of IDs (int/string). $schemas = array_filter( $this->schemas ?? [], @@ -586,8 +550,6 @@ function ($item) { 'type' => $this->type, 'languages' => $this->languages, 'configuration' => $this->configuration, - 'published' => $published, - 'depublished' => $depublished, 'quota' => [ 'storage' => null, // To be set via admin configuration. @@ -706,60 +668,6 @@ public function getManagedByConfiguration(array $configurations): ?Configuration return null; }//end getManagedByConfiguration() - /** - * Get the publication timestamp - * - * @return DateTime|null Publication timestamp - */ - public function getPublished(): ?DateTime - { - return $this->published; - }//end getPublished() - - /** - * Set the publication timestamp - * - * @param DateTime|string|null $published Publication timestamp (DateTime object or ISO 8601 string) - * - * @return void - */ - public function setPublished(DateTime|string|null $published): void - { - if (is_string($published) === true) { - $published = new DateTime($published); - } - - $this->published = $published; - $this->markFieldUpdated(attribute: 'published'); - }//end setPublished() - - /** - * Get the depublication timestamp - * - * @return DateTime|null Depublication timestamp - */ - public function getDepublished(): ?DateTime - { - return $this->depublished; - }//end getDepublished() - - /** - * Set the depublication timestamp - * - * @param DateTime|string|null $depublished Depublication timestamp (DateTime object or ISO 8601 string) - * - * @return void - */ - public function setDepublished(DateTime|string|null $depublished): void - { - if (is_string($depublished) === true) { - $depublished = new DateTime($depublished); - } - - $this->depublished = $depublished; - $this->markFieldUpdated(attribute: 'depublished'); - }//end setDepublished() - // ================================================================================== // LANGUAGE CONFIGURATION HELPERS // ================================================================================== diff --git a/lib/Db/RegisterMapper.php b/lib/Db/RegisterMapper.php index afacc38876..bb0dbd875c 100644 --- a/lib/Db/RegisterMapper.php +++ b/lib/Db/RegisterMapper.php @@ -62,9 +62,8 @@ * @method Register update(Entity $entity) * @method Register insertOrUpdate(Entity $entity) * @method Register delete(Entity $entity) - * @method Register find(int|string $id) + * @method Register find(int|string $id, bool $_rbac=true, bool $_multitenancy=true) * @method Register findEntity(IQueryBuilder $query) - * @method Register[] findAll(int|null $limit=null, int|null $offset=null) * @method list findEntities(IQueryBuilder $query) * * @template-extends QBMapper @@ -77,6 +76,17 @@ class RegisterMapper extends QBMapper { use MultiTenancyTrait; + /** + * Maximum number of expressions allowed in a single SQL IN() list. + * + * Nextcloud's QueryBuilder refuses more than 1000 expressions in an IN() + * list (Oracle limit); exceeding it logs an error and emits an "Undefined + * array key 0" PHP warning. Batched id lookups must chunk below this bound. + * + * @var integer + */ + private const MAX_IN_LIST_SIZE = 1000; + /** * Schema mapper instance * @@ -194,8 +204,6 @@ public function __construct( * Includes RBAC and organisation filtering for multi-tenancy. * * @param int|string $id The ID of the register to find - * @param array $_extend Optional array of extensions (e.g., ['@self.stats']) - * @param bool|null $published Whether to enable published bypass (default: null = check config) * @param bool $_rbac Whether to apply RBAC permission checks (default: true) * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) * @@ -203,7 +211,6 @@ public function __construct( * * @throws \Exception If RBAC permission check fails * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flags control security filtering behavior * @SuppressWarnings(PHPMD.NPathComplexity) Find operation requires multiple lookup strategies * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -211,8 +218,6 @@ public function __construct( */ public function find( string|int $id, - ?array $_extend=[], - ?bool $published=null, bool $_rbac=true, bool $_multitenancy=true ): Register { @@ -242,7 +247,6 @@ public function find( 'identifier' => $id, 'rbac' => $_rbac, 'multi' => $_multitenancy, - 'published' => $published, ] ); } @@ -276,7 +280,13 @@ public function find( $qb->where($orConditions); // Check if register exists before applying filters (for debugging). - $qbBeforeFilter = clone $qb; + // Cap to a single row + deterministic order so duplicate-slug rows from + // env churn cannot raise MultipleObjectsReturnedException out of this + // debug-only probe (which previously surfaced as a 500 on slug lookups + // — e.g. the object lock path). See the deterministic resolution below. + $qbBeforeFilter = clone $qb; + $qbBeforeFilter->orderBy('id', 'ASC'); + $qbBeforeFilter->setMaxResults(1); $existsBeforeFilter = false; try { $testResult = $this->findEntity(query: $qbBeforeFilter); @@ -290,15 +300,13 @@ public function find( 'identifier' => $id, 'registerId' => $testResult->getId(), 'organisation' => $testResult->getOrganisation(), - 'published' => $testResult->getPublished(), - 'depublished' => $testResult->getDepublished(), ] ); } - } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + } catch (\OCP\AppFramework\Db\DoesNotExistException | \OCP\AppFramework\Db\MultipleObjectsReturnedException $e) { if (isset($this->logger) === true) { $this->logger->warning( - message: '[RegisterMapper] Register does not exist in database', + message: '[RegisterMapper] Register does not exist (or is duplicated) before filters', context: [ 'file' => __FILE__, 'line' => __LINE__, @@ -316,6 +324,15 @@ public function find( multiTenancyEnabled: $_multitenancy ); + // Deterministic slug/uuid resolution: env churn can leave multiple rows + // sharing a slug, which would make findEntity() raise + // MultipleObjectsReturnedException → a 500 fleet-wide. Order by id ASC + // and cap to a single row so the oldest (lowest-id) register + // deterministically wins. The duplicates are still detectable/mergeable + // via the `openregister:registers:dedupe` occ command. + $qb->orderBy('id', 'ASC'); + $qb->setMaxResults(1); + // Just return the entity; do not attach stats here. try { $register = $this->findEntity(query: $qb); @@ -333,8 +350,14 @@ public function find( $rbacSuffix = ':'.$rbacChar.':'.$mtChar; $this->findCache[$cacheKey] = $register; - $this->findCache[(string) $register->getId().$rbacSuffix] = $register; - $this->findCache[strtolower($register->getUuid()).$rbacSuffix] = $register; + $this->findCache[(string) $register->getId().$rbacSuffix] = $register; + + // BUG-DB-10: guard against a null uuid before strtolower(). + $registerUuid = $register->getUuid(); + if ($registerUuid !== null) { + $this->findCache[strtolower($registerUuid).$rbacSuffix] = $register; + } + if ($register->getSlug() !== null) { $this->findCache[strtolower($register->getSlug()).$rbacSuffix] = $register; } @@ -388,10 +411,9 @@ public function clearFindCache(int $registerId): void /** * Finds multiple registers by id * - * @param array $ids The ids of the registers - * @param bool|null $published Whether to enable published bypass (default: null = check config) - * @param bool $_rbac Whether to apply RBAC permission checks (default: true) - * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) + * @param array $ids The ids of the registers + * @param bool $_rbac Whether to apply RBAC permission checks (default: true) + * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) * * @throws \OCP\AppFramework\Db\DoesNotExistException If a register does not exist * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException If multiple registers are found @@ -405,12 +427,12 @@ public function clearFindCache(int $registerId): void * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flags control security filtering behavior */ - public function findMultiple(array $ids, ?bool $published=null, bool $_rbac=true, bool $_multitenancy=true): array + public function findMultiple(array $ids, bool $_rbac=true, bool $_multitenancy=true): array { $result = []; foreach ($ids as $id) { try { - $result[] = $this->find(id: $id, published: $published, _rbac: $_rbac, _multitenancy: $_multitenancy); + $result[] = $this->find(id: $id, _rbac: $_rbac, _multitenancy: $_multitenancy); } catch (\OCP\AppFramework\Db\DoesNotExistException | \OCP\AppFramework\Db\MultipleObjectsReturnedException) { // Catch all exceptions but do nothing. } catch (\OCP\DB\Exception) { @@ -439,20 +461,28 @@ public function findMultipleOptimized(array $ids): array return []; } - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from('openregister_registers') - ->where( - $qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)) - ); - - $result = $qb->executeQuery(); $registers = []; - while (($row = $result->fetch()) !== false) { - $register = new Register(); - $register = $register->fromRow($row); - $registers[$row['id']] = $register; + // Chunk below the 1000-expression IN() ceiling (Oracle limit enforced by + // Nextcloud's QueryBuilder). Registers are fewer than schemas today, but + // this list is caller-supplied and unbounded. + foreach (array_chunk($ids, self::MAX_IN_LIST_SIZE) as $chunk) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('openregister_registers') + ->where( + $qb->expr()->in('id', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)) + ); + + $result = $qb->executeQuery(); + + while (($row = $result->fetch()) !== false) { + $register = new Register(); + $register = $register->fromRow($row); + $registers[$row['id']] = $register; + } + + $result->closeCursor(); } return $registers; @@ -466,16 +496,13 @@ public function findMultipleOptimized(array $ids): array * @param array|null $filters The filters to apply * @param array|null $searchConditions Array of search conditions * @param array|null $searchParams Array of search parameters - * @param array $_extend Optional array of extensions (e.g., ['@self.stats']) - * @param bool|null $published Whether to enable published bypass (default: null = check config) * @param bool $_rbac Whether to apply RBAC permission checks (default: true) * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) * * @return Register[] * - * @psalm-return list - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flags control security filtering behavior + * @psalm-return list<\OCA\OpenRegister\Db\Register> + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flags control security filtering behavior */ public function findAll( ?int $limit=null, @@ -483,8 +510,6 @@ public function findAll( ?array $filters=[], ?array $searchConditions=[], ?array $searchParams=[], - ?array $_extend=[], - ?bool $published=null, bool $_rbac=true, bool $_multitenancy=true ): array { @@ -521,9 +546,6 @@ public function findAll( } } - // Apply organisation filter with published entity bypass support - // Published registers can bypass multi-tenancy restrictions if configured - // ApplyOrganisationFilter handles $multiTenancyEnabled=false internally // Apply organisation filter. $this->applyOrganisationFilter( qb: $qb, @@ -550,8 +572,8 @@ public function findAll( */ public function insert(Entity $entity): Entity { - // Verify RBAC permission to create registers - // $this->verifyRbacPermission('create', 'register'); + // Verify RBAC permission to create registers. + $this->verifyRbacPermission(action: 'create', entityType: 'register'); // Auto-set organisation from active session. $this->setOrganisationOnCreate(entity: $entity); @@ -639,8 +661,8 @@ public function createFromArray(array $object): Register */ public function update(Entity $entity): Entity { - // Verify RBAC permission to update registers - // $this->verifyRbacPermission('update', 'register'); + // Verify RBAC permission to update registers. + $this->verifyRbacPermission(action: 'update', entityType: 'register'); // Verify entity belongs to active organisation. $this->verifyOrganisationAccess(entity: $entity); @@ -680,9 +702,7 @@ public function updateFromArray(int $id, array $object): Register // Set or update the version. if (isset($object['version']) === false) { $currentVersion = $register->getVersion() ?? '0.0.0'; - $version = explode('.', $currentVersion); - $version[2] = ((int) $version[2] + 1); - $register->setVersion(implode('.', $version)); + $register->setVersion($this->bumpPatchVersion(version: $currentVersion)); } $register->hydrate(object: $object); @@ -695,6 +715,34 @@ public function updateFromArray(int $id, array $object): Register return $register; }//end updateFromArray() + /** + * Increment the patch component of a semantic version string. + * + * BUG-DB-12: the previous naive `explode('.')` + `(int)` bump turned a + * pre-release like `1.0.0-beta` into `1.0.1`, silently dropping the + * `-beta` suffix. This parser preserves any pre-release/build suffix and + * pads missing segments so a bare `1` or `1.2` still bumps cleanly. + * + * @param string $version The current version string (e.g. `1.0.0-beta`). + * + * @return string The version with its patch component incremented. + */ + private function bumpPatchVersion(string $version): string + { + // Capture: major.minor.patch followed by an optional -prerelease/+build suffix. + if (preg_match('/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(.*)$/', trim($version), $matches) === 1) { + $major = (int) $matches[1]; + $minor = (int) ($matches[2] ?? 0); + $patch = (int) ($matches[3] ?? 0); + $suffix = $matches[4] ?? ''; + + return $major.'.'.$minor.'.'.($patch + 1).$suffix; + } + + // Fall back to a safe default when the version is unparsable. + return '0.0.1'; + }//end bumpPatchVersion() + /** * Delete a register only if no objects are attached * @@ -706,8 +754,8 @@ public function updateFromArray(int $id, array $object): Register */ public function delete(Entity $entity): Register { - // Verify RBAC permission to delete registers - // $this->verifyRbacPermission('delete', 'register'); + // Verify RBAC permission to delete registers. + $this->verifyRbacPermission(action: 'delete', entityType: 'register'); // Verify entity belongs to active organisation. $this->verifyOrganisationAccess(entity: $entity); @@ -737,10 +785,9 @@ public function delete(Entity $entity): Register /** * Get all schemas associated with a register * - * @param int $registerId The ID of the register - * @param bool|null $published Whether to enable published bypass (default: null = check config) - * @param bool $_rbac Whether to apply RBAC permission checks (default: true) - * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) + * @param int $registerId The ID of the register + * @param bool $_rbac Whether to apply RBAC permission checks (default: true) + * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) * * @return Schema[] * @@ -750,14 +797,11 @@ public function delete(Entity $entity): Register */ public function getSchemasByRegisterId( int $registerId, - ?bool $published=null, bool $_rbac=true, bool $_multitenancy=true ): array { $register = $this->find( id: $registerId, - _extend: [], - published: $published, _rbac: $_rbac, _multitenancy: $_multitenancy ); @@ -770,7 +814,7 @@ public function getSchemasByRegisterId( // This ensures schemas linked to accessible registers can always be found. foreach ($schemaIds ?? [] as $schemaId) { try { - $schemas[] = $this->schemaMapper->find((int) $schemaId, [], $published, $_rbac, false); + $schemas[] = $this->schemaMapper->find(id: (int) $schemaId, _extend: [], _rbac: $_rbac, _multitenancy: false); } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { // Schema not found, skip it (similar to RegistersController behavior). continue; @@ -793,6 +837,25 @@ public function getSchemasByRegisterId( * @return int|null The ID of the first matching register, files: or null if none found. */ public function getFirstRegisterWithSchema(int $schemaId): ?int + { + $matches = $this->getAllRegisterIdsWithSchema(schemaId: $schemaId); + return ($matches[0] ?? null); + }//end getFirstRegisterWithSchema() + + /** + * Retrieves the IDs of all registers that include the given schema ID. + * + * A schema may be referenced by more than one register. Callers that make a + * security decision off register-level configuration (e.g. the + * inheritFromPublic cascade) must consider every register rather than an + * arbitrary first match, so the verdict is deterministic across nodes and + * restores. IDs are returned in ascending order for stability. + * + * @param int $schemaId The ID of the schema to search for. + * + * @return int[] The IDs of all matching registers (empty when none found). + */ + public function getAllRegisterIdsWithSchema(int $schemaId): array { // Three platforms in production: SQLite (no REGEXP function), // MariaDB / MySQL (has REGEXP), and Postgres (has SIMILAR TO @@ -811,18 +874,22 @@ public function getFirstRegisterWithSchema(int $schemaId): ?int $candidates = $qb->executeQuery()->fetchAllAssociative(); $needle = (string) $schemaId; + $matches = []; foreach ($candidates as $row) { $schemas = $this->decodeSchemasField(raw: ($row['schemas'] ?? null)); foreach ($schemas as $candidate) { if ((string) $candidate === $needle) { - return (int) $row['id']; + $matches[] = (int) $row['id']; + break; } } } - return null; - }//end getFirstRegisterWithSchema() + sort($matches); + + return $matches; + }//end getAllRegisterIdsWithSchema() /** * Decode the persisted `schemas` column into a flat ID list. diff --git a/lib/Db/ScheduledReport.php b/lib/Db/ScheduledReport.php new file mode 100644 index 0000000000..15582f4998 --- /dev/null +++ b/lib/Db/ScheduledReport.php @@ -0,0 +1,334 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class ScheduledReport + * + * @method string|null getOwner() + * @method void setOwner(?string $owner) + * @method string|null getName() + * @method void setName(?string $name) + * @method int|null getRegisterId() + * @method void setRegisterId(?int $registerId) + * @method int|null getSchemaId() + * @method void setSchemaId(?int $schemaId) + * @method string|null getFilters() + * @method void setFilters(?string $filters) + * @method string|null getFormat() + * @method void setFormat(?string $format) + * @method string|null getScheduleType() + * @method void setScheduleType(?string $scheduleType) + * @method int|null getScheduleHour() + * @method void setScheduleHour(?int $scheduleHour) + * @method int|null getScheduleDayOfWeek() + * @method void setScheduleDayOfWeek(?int $scheduleDayOfWeek) + * @method int|null getScheduleDayOfMonth() + * @method void setScheduleDayOfMonth(?int $scheduleDayOfMonth) + * @method string|null getDeliveryFolder() + * @method void setDeliveryFolder(?string $deliveryFolder) + * @method string|null getDeliveryMode() + * @method void setDeliveryMode(?string $deliveryMode) + * @method string|null getRecipients() + * @method void setRecipients(?string $recipients) + * @method bool|null getEnabled() + * @method void setEnabled(?bool $enabled) + * @method DateTime|null getLastRunAt() + * @method void setLastRunAt(?DateTime $lastRunAt) + * @method string|null getLastStatus() + * @method void setLastStatus(?string $lastStatus) + * @method string|null getLastError() + * @method void setLastError(?string $lastError) + * @method DateTime|null getCreatedAt() + * @method void setCreatedAt(?DateTime $createdAt) + * @method DateTime|null getUpdatedAt() + * @method void setUpdatedAt(?DateTime $updatedAt) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + * @spec openspec/specs/scheduled-report-jobs/spec.md + * + * @SuppressWarnings(PHPMD.TooManyFields) 19 columns is the row's actual + * shape (design.md's table plus deliveryMode/recipients) — scheduling + * config, owner, delivery target/mode, and last-run outcome each need + * their own field; splitting this into sub-objects would just move the + * field count, not reduce it. + */ +class ScheduledReport extends Entity implements JsonSerializable +{ + + /** + * The owning Nextcloud user id. + * + * @var string|null + */ + protected ?string $owner = null; + + /** + * User-facing label. + * + * @var string|null + */ + protected ?string $name = null; + + /** + * The register whose data is exported. + * + * @var integer|null + */ + protected ?int $registerId = null; + + /** + * The schema whose data is exported (required for `csv`). + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * Opaque JSON-encoded `@self.*` filter map, same shape the export endpoints accept. + * + * @var string|null + */ + protected ?string $filters = null; + + /** + * Export format: csv|excel|pdf. + * + * @var string|null + */ + protected ?string $format = null; + + /** + * Schedule cadence: daily|weekly|monthly. + * + * @var string|null + */ + protected ?string $scheduleType = null; + + /** + * Target hour of day (0-23), informational — see design.md due-logic notes. + * + * @var integer|null + */ + protected ?int $scheduleHour = null; + + /** + * Target day of week (0=Monday..6=Sunday), required when scheduleType is weekly. + * + * @var integer|null + */ + protected ?int $scheduleDayOfWeek = null; + + /** + * Target day of month (1-28), required when scheduleType is monthly. + * + * @var integer|null + */ + protected ?int $scheduleDayOfMonth = null; + + /** + * Nextcloud Files delivery folder, relative to the owner's home. Default 'Reports/'. + * + * @var string|null + */ + protected ?string $deliveryFolder = null; + + /** + * How the export is delivered to the owner: `files` (Nextcloud Files + * only — the original behaviour), `email` (email only), or `both`. + * Defaults to `files` so every report created before this field existed + * keeps its exact prior behaviour. + * + * @var string|null + */ + protected ?string $deliveryMode = null; + + /** + * Opaque JSON-encoded array of recipient email addresses used when + * `deliveryMode` is `email` or `both`. Empty/absent means "the owner's + * own Nextcloud email address, resolved at run time" — see + * `ScheduledReportService::resolveRecipients()`. + * + * @var string|null + */ + protected ?string $recipients = null; + + /** + * Whether this report is scheduled to run. + * + * @var boolean|null + */ + protected ?bool $enabled = null; + + /** + * When this report last ran (any outcome). + * + * @var DateTime|null + */ + protected ?DateTime $lastRunAt = null; + + /** + * The outcome of the last run: success|failed. + * + * @var string|null + */ + protected ?string $lastStatus = null; + + /** + * The failure reason when lastStatus is failed. + * + * @var string|null + */ + protected ?string $lastError = null; + + /** + * When this report was created. + * + * @var DateTime|null + */ + protected ?DateTime $createdAt = null; + + /** + * When this report was last updated. + * + * @var DateTime|null + */ + protected ?DateTime $updatedAt = null; + + /** + * Constructor. + */ + public function __construct() + { + $this->addType(fieldName: 'owner', type: 'string'); + $this->addType(fieldName: 'name', type: 'string'); + $this->addType(fieldName: 'registerId', type: 'integer'); + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'filters', type: 'string'); + $this->addType(fieldName: 'format', type: 'string'); + $this->addType(fieldName: 'scheduleType', type: 'string'); + $this->addType(fieldName: 'scheduleHour', type: 'integer'); + $this->addType(fieldName: 'scheduleDayOfWeek', type: 'integer'); + $this->addType(fieldName: 'scheduleDayOfMonth', type: 'integer'); + $this->addType(fieldName: 'deliveryFolder', type: 'string'); + $this->addType(fieldName: 'deliveryMode', type: 'string'); + $this->addType(fieldName: 'recipients', type: 'string'); + $this->addType(fieldName: 'enabled', type: 'boolean'); + $this->addType(fieldName: 'lastRunAt', type: 'datetime'); + $this->addType(fieldName: 'lastStatus', type: 'string'); + $this->addType(fieldName: 'lastError', type: 'string'); + $this->addType(fieldName: 'createdAt', type: 'datetime'); + $this->addType(fieldName: 'updatedAt', type: 'datetime'); + }//end __construct() + + /** + * The decoded filter map, or an empty array when none is stored. + * + * @return array + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function getFiltersArray(): array + { + if ($this->filters === null || $this->filters === '') { + return []; + } + + $decoded = json_decode($this->filters, true); + if (is_array($decoded) === false) { + return []; + } + + return $decoded; + }//end getFiltersArray() + + /** + * The decoded recipient email list, or an empty array when none is stored + * (default-to-owner-email is resolved at run time, not here). + * + * @return string[] + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function getRecipientsArray(): array + { + if ($this->recipients === null || $this->recipients === '') { + return []; + } + + $decoded = json_decode($this->recipients, true); + if (is_array($decoded) === false) { + return []; + } + + return array_values(array_filter($decoded, static fn ($value) => is_string($value) === true)); + }//end getRecipientsArray() + + /** + * JSON serialization — the row is only ever exposed to its owner or an admin (controller-gated). + * + * @return array + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'owner' => $this->owner, + 'name' => $this->name, + 'registerId' => $this->registerId, + 'schemaId' => $this->schemaId, + 'filters' => $this->getFiltersArray(), + 'format' => $this->format, + 'scheduleType' => $this->scheduleType, + 'scheduleHour' => $this->scheduleHour, + 'scheduleDayOfWeek' => $this->scheduleDayOfWeek, + 'scheduleDayOfMonth' => $this->scheduleDayOfMonth, + 'deliveryFolder' => $this->deliveryFolder, + 'deliveryMode' => ($this->deliveryMode ?? 'files'), + 'recipients' => $this->getRecipientsArray(), + 'enabled' => $this->enabled, + 'lastRunAt' => $this->lastRunAt?->format(DateTime::ATOM), + 'lastStatus' => $this->lastStatus, + 'lastError' => $this->lastError, + 'createdAt' => $this->createdAt?->format(DateTime::ATOM), + 'updatedAt' => $this->updatedAt?->format(DateTime::ATOM), + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/ScheduledReportMapper.php b/lib/Db/ScheduledReportMapper.php new file mode 100644 index 0000000000..88d04a7202 --- /dev/null +++ b/lib/Db/ScheduledReportMapper.php @@ -0,0 +1,129 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class ScheduledReportMapper + * + * @template-extends QBMapper + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ +class ScheduledReportMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_scheduled_reports', entityClass: ScheduledReport::class); + }//end __construct() + + /** + * Find a scheduled report by id. + * + * @param int $id The scheduled report id. + * + * @return ScheduledReport + * + * @throws \OCP\AppFramework\Db\DoesNotExistException When no row matches. + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException When more than one row matches (should never happen on a PK lookup). + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function find(int $id): ScheduledReport + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity(query: $qb); + }//end find() + + /** + * Find every scheduled report owned by a user. + * + * @param string $owner The owning Nextcloud user id. + * + * @return ScheduledReport[] + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function findByOwner(string $owner): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('owner', $qb->createNamedParameter($owner))) + ->orderBy('id', 'ASC'); + + return $this->findEntities(query: $qb); + }//end findByOwner() + + /** + * Find every scheduled report (admin listing). + * + * @return ScheduledReport[] + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function findAll(): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->orderBy('id', 'ASC'); + + return $this->findEntities(query: $qb); + }//end findAll() + + /** + * Find every enabled scheduled report, regardless of owner — the candidate + * set `ScheduledReportJob` narrows further via `ScheduledReportService::isDue()`. + * + * @return ScheduledReport[] + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function findEnabled(): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('enabled', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))); + + return $this->findEntities(query: $qb); + }//end findEnabled() +}//end class diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 3df74899b6..ec6285b08f 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -375,28 +375,6 @@ class Schema extends Entity implements JsonSerializable */ protected ?array $anyOf = null; - /** - * Publication timestamp. - * - * When set, this schema becomes publicly accessible regardless of organisation restrictions - * if published bypass is enabled. The schema is considered published when: - * - published <= now AND - * - (depublished IS NULL OR depublished > now) - * - * @var DateTime|null Publication timestamp - */ - protected ?DateTime $published = null; - - /** - * Depublication timestamp. - * - * When set, this schema becomes inaccessible after this date/time. - * Used together with published to control publication lifecycle. - * - * @var DateTime|null Depublication timestamp - */ - protected ?DateTime $depublished = null; - /** * Hooks configuration for the schema * @@ -490,8 +468,6 @@ public function __construct() $this->addType(fieldName: 'deleted', type: 'datetime'); $this->addType(fieldName: 'configuration', type: 'json'); $this->addType(fieldName: 'groups', type: 'json'); - $this->addType(fieldName: 'published', type: 'datetime'); - $this->addType(fieldName: 'depublished', type: 'datetime'); $this->addType(fieldName: 'hooks', type: 'json'); $this->addType(fieldName: 'mail', type: 'json'); $this->addType(fieldName: 'contacts', type: 'json'); @@ -656,6 +632,187 @@ public function getPropertiesWithAuthorization(): array return $result; }//end getPropertiesWithAuthorization() + /** + * Check if any property in the schema is declared write-only. + * + * A property carrying the JSON Schema / OpenAPI keyword `writeOnly: true` + * is a write-only secret: it may be written but is NEVER returned on read, + * for anyone (including admin). This is used to decide whether the render + * path must apply write-only stripping (see PropertyRbacHandler). + * + * @return bool True if at least one property has writeOnly === true + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-writeonly-properties-must-never-be-returned-on-any-read + */ + public function hasWriteOnlyProperties(): bool + { + if (empty($this->properties) === true) { + return false; + } + + foreach ($this->properties as $propertyConfig) { + if (is_array($propertyConfig) === true + && ($propertyConfig['writeOnly'] ?? null) === true + ) { + return true; + } + } + + return false; + }//end hasWriteOnlyProperties() + + /** + * Get the names of all properties declared write-only (`writeOnly: true`). + * + * @return array List of write-only property names + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-writeonly-properties-must-never-be-returned-on-any-read + */ + public function getWriteOnlyProperties(): array + { + $result = []; + + if (empty($this->properties) === true) { + return $result; + } + + foreach ($this->properties as $propertyName => $propertyConfig) { + if (is_array($propertyConfig) === true + && ($propertyConfig['writeOnly'] ?? null) === true + ) { + $result[] = (string) $propertyName; + } + } + + return $result; + }//end getWriteOnlyProperties() + + /** + * The schema-level annotation declaring nested write-only dot-paths. + * + * `writeOnly: true` is a JSON Schema keyword and can therefore only be + * attached to a property the schema actually DECLARES. A secret nested + * inside an untyped `object` property (the canonical case: a Source's + * `configuration.authentication.client_secret`, a Rule's + * `configuration.authentication.keys`) has no declaration to hang it on — + * `configuration` is `type: object` with no `properties` — and marking the + * whole `configuration` object writeOnly breaks the editors that + * legitimately read the rest of it back. This annotation is the missing + * declaration surface: it names individual nested paths as write-only + * without touching their untyped parent. + * + * @var string + */ + public const WRITEONLY_PATHS_ANNOTATION = 'x-openregister-writeonly-paths'; + + /** + * Whether the schema declares any nested write-only dot-paths. + * + * Companion to hasWriteOnlyProperties(): that one answers "does a declared + * property carry `writeOnly: true`", this one answers "does the schema + * declare a nested path as write-only". The render path must strip when + * EITHER is true, so every gate checks both. + * + * @return bool True when at least one nested write-only path is declared. + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-nested-writeonly-paths-must-never-be-returned-on-any-read + */ + public function hasWriteOnlyPaths(): bool + { + return empty($this->getWriteOnlyPaths()) === false; + }//end hasWriteOnlyPaths() + + /** + * Get the nested write-only dot-paths declared by this schema. + * + * Paths are dot-separated and rooted at a declared top-level property, + * e.g. `configuration.authentication.client_secret`. A path denotes the + * value at that location AND everything beneath it, so declaring + * `configuration.authentication` strips the whole sub-tree. + * + * @return array Declared write-only dot-paths (possibly empty). + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-nested-writeonly-paths-must-never-be-returned-on-any-read + */ + public function getWriteOnlyPaths(): array + { + $configuration = $this->configuration; + if (is_array($configuration) === false) { + return []; + } + + $paths = ($configuration[self::WRITEONLY_PATHS_ANNOTATION] ?? null); + if (is_array($paths) === false) { + return []; + } + + $result = []; + foreach ($paths as $path) { + if (is_string($path) === true && $path !== '') { + $result[] = $path; + } + } + + return $result; + }//end getWriteOnlyPaths() + + /** + * Check if any property in the schema is declared encrypted at rest. + * + * A property carrying `x-openregister-encrypted: true` (the OpenRegister + * vendor-extension convention, sibling to `x-openregister-relations` etc.) + * is encrypted at rest via {@see \OCA\OpenRegister\Service\FieldEncryptionHandler} + * on save and decrypted for authorized reads on render. Used to decide + * whether the save/render path must apply the encryption/decryption step, + * and whether the magic-table column builder must skip the property. + * + * @return bool True if at least one property has x-openregister-encrypted === true + * + * @spec openspec/specs/field-level-encryption/spec.md#requirement-flagged-properties-are-encrypted-on-save + */ + public function hasEncryptedProperties(): bool + { + if (empty($this->properties) === true) { + return false; + } + + foreach ($this->properties as $propertyConfig) { + if (is_array($propertyConfig) === true + && ($propertyConfig['x-openregister-encrypted'] ?? null) === true + ) { + return true; + } + } + + return false; + }//end hasEncryptedProperties() + + /** + * Get the names of all properties declared encrypted (`x-openregister-encrypted: true`). + * + * @return array List of encrypted property names + * + * @spec openspec/specs/field-level-encryption/spec.md#requirement-flagged-properties-are-encrypted-on-save + */ + public function getEncryptedProperties(): array + { + $result = []; + + if (empty($this->properties) === true) { + return $result; + } + + foreach ($this->properties as $propertyName => $propertyConfig) { + if (is_array($propertyConfig) === true + && ($propertyConfig['x-openregister-encrypted'] ?? null) === true + ) { + $result[] = (string) $propertyName; + } + } + + return $result; + }//end getEncryptedProperties() + /** * Get the archive data * @@ -717,8 +874,10 @@ public function validateProperties(PropertyValidatorHandler $validator): bool * Validate the authorization structure for RBAC * * Validates that the authorization array follows the correct structure: - * - Keys must be valid CRUD actions (create, read, update, delete) - * - Values must be arrays of group IDs (strings) + * - Keys are either CRUD actions (create, read, update, delete) or + * reserved cascade flags (e.g. inheritFromPublic) + * - Action values must be arrays of group IDs (strings); reserved flag + * values must be booleans * - Group IDs must be non-empty strings * * Also validates property-level authorization if any properties have authorization defined. @@ -743,6 +902,12 @@ public function validateAuthorization(): bool /** * Validate an authorization rules array * + * Keys are either CRUD actions (create, read, update, delete) holding rule + * arrays, or reserved cascade flags (e.g. inheritFromPublic) holding a + * boolean. Reserved flags are behaviour toggles read at runtime by + * PermissionHandler, not action rule sets, so they are validated as + * booleans and skip the action/array checks. + * * @param array|null $authorization The authorization rules to validate * @param string $context Context for error messages (e.g., 'schema' or 'property "fieldName"') * @@ -758,7 +923,22 @@ private function validateAuthorizationRules(?array $authorization, string $conte $validActions = ['create', 'read', 'update', 'delete']; + // Reserved non-action authorization flags. These are cascade/behaviour + // toggles read at runtime by PermissionHandler, not CRUD rule sets. + $reservedFlags = ['inheritFromPublic']; + foreach ($authorization as $action => $rules) { + // Reserved flags are validated as booleans, not action rule arrays. + if (in_array($action, $reservedFlags, true) === true) { + if (is_bool($rules) === false) { + throw new InvalidArgumentException( + "Authorization flag '{$action}' in {$context} must be a boolean" + ); + } + + continue; + } + // Validate action is a valid CRUD operation. if (in_array($action, $validActions) === false) { $validList = implode(', ', $validActions); @@ -944,9 +1124,14 @@ public function hasPermission( return true; } - // If action is not specified in authorization, everyone has permission. - if (isset($this->authorization[$action]) === false) { - return true; + // Fail-closed (rbac-default-deny): once a schema opts into authorization + // (non-empty block), an action that is not explicitly listed is denied — + // including the `public` pseudo-group. The empty-block open-default above + // and the admin/owner bypasses still apply. Kept consistent with the + // active enforcement paths (PermissionHandler / MagicRbacHandler) even + // though this entity-level helper currently has no lib/ enforcement callers. + if (empty($this->authorization[$action]) === true) { + return false; } // Check each authorization entry for this action. @@ -1191,6 +1376,28 @@ public function hydrate(array $object, ?PropertyValidatorHandler $validator=null } } + // Fold a schema-LEVEL top-level `x-schema-org` marker into + // `configuration['x-schema-org']` the same way (ADR-048). Fleet + // schemas declare the canonical schema.org CURIE (e.g. + // `"x-schema-org": "schema:Organization"`) as a sibling of + // `properties`, NOT inside `configuration`; without this fold the + // marker falls through to a non-existent `setXSchemaOrg()` setter, + // is swallowed by the silent-catch below, and the live schema loses + // its semantic type — so SemanticTypeResolver can never discover the + // provider. Folding it into the configuration column (which is in the + // passThrough allowlist and read by + // JsonLdContextService::getImplementedTypes) makes the marker survive + // save/import. An explicit `configuration['x-schema-org']` already + // supplied by the caller wins over the top-level convenience form. + if (array_key_exists('x-schema-org', $object) === true) { + if (array_key_exists('x-schema-org', $existingConfig) === false) { + $existingConfig['x-schema-org'] = $object['x-schema-org']; + $annotationsFolded = true; + } + + unset($object['x-schema-org']); + } + if ($annotationsFolded === true) { $object['configuration'] = $existingConfig; } @@ -1243,7 +1450,7 @@ public function hydrate(array $object, ?PropertyValidatorHandler $validator=null }//end if // Convert datetime strings to DateTime objects for datetime fields. - if (in_array($key, ['published', 'depublished', 'created', 'updated', 'deleted'], true) === true) { + if (in_array($key, ['created', 'updated', 'deleted'], true) === true) { if (is_string($value) === true && $value !== '') { try { $value = new DateTime($value); @@ -1294,8 +1501,7 @@ public function hydrate(array $object, ?PropertyValidatorHandler $validator=null * maxDepth: int, owner: null|string, application: null|string, * organisation: null|string, * groups: array>|null, authorization: array|null, - * deleted: null|string, published: null|string, - * depublished: null|string, configuration: array|null|string, + * deleted: null|string, configuration: array|null|string, * allOf: array|null, oneOf: array|null, anyOf: array|null} * * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -1333,16 +1539,6 @@ public function jsonSerialize(): array $deleted = $this->deleted->format('c'); } - $published = null; - if (isset($this->published) === true) { - $published = $this->published->format('c'); - } - - $depublished = null; - if (isset($this->depublished) === true) { - $depublished = $this->depublished->format('c'); - } - return [ 'id' => $this->id, 'uuid' => $this->uuid, @@ -1371,8 +1567,6 @@ public function jsonSerialize(): array 'groups' => $this->groups, 'authorization' => $this->authorization, 'deleted' => $deleted, - 'published' => $published, - 'depublished' => $depublished, 'configuration' => $this->configuration, 'allOf' => $this->allOf, 'oneOf' => $this->oneOf, @@ -1562,6 +1756,42 @@ public function getCalendarProviderConfig(): ?array return $calendarConfig; }//end getCalendarProviderConfig() + /** + * Get the object-source declaration from the schema configuration. + * + * Reads the `x-openregister-object-source` annotation block (folded into the + * configuration column at save time). When present and valid, the schema's + * objects are served by the named ObjectSourceProvider instead of the magic + * table (see GetObject). Returns null when absent or when `provider` is not a + * non-empty string. + * + * @return array{provider: string, readOnly?: bool, config?: array}|null + * The parsed object-source declaration, or null when absent/invalid. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-2.2 + */ + public function getObjectSource(): ?array + { + $configuration = $this->getConfiguration(); + + if ($configuration === null) { + return null; + } + + $source = ($configuration['x-openregister-object-source'] ?? null); + + if (is_array($source) === false) { + return null; + } + + $provider = ($source['provider'] ?? null); + if (is_string($provider) === false || $provider === '') { + return null; + } + + return $source; + }//end getObjectSource() + /** * Set the configuration for the schema with validation * @@ -1582,6 +1812,12 @@ public function getCalendarProviderConfig(): ?array * toggle on the attachment upload dialog. true seeds the toggle on; absent * or false keeps it off. Users can always override per upload. * See: ConductionNL/opencatalogi#577 + * - 'mailObjectTemplate': (array) Field map used by the Mail-sidebar + * "create from email" action. Keys are schema property names, string + * values may contain {{subject}}/{{sender}}/{{senderName}}/{{date}}/ + * {{date30}}/{{datetime}}/{{preview}}/{{messageId}}/{{mailRef}} + * placeholders; non-string values pass through verbatim. Only schemas + * declaring this template get a create-from-email button. * * @param array|string|null $configuration The configuration array/string to validate and set * @@ -1653,73 +1889,237 @@ private function validateConfigurationArray(array $configuration): array $validatedConfig = []; $stringFields = ['objectNameField', 'objectDescriptionField', 'objectSummaryField', 'objectImageField']; $boolFields = ['allowFiles', 'autoPublish', 'defaultAutoShare']; - $passThrough = ['unique', 'facetCacheTtl', 'calendarProvider']; + // `implements` + `x-schema-org` carry the cross-app semantic-type + // markers (ADR-048); they must round-trip through the configuration + // column so SemanticTypeResolver can discover the schema. Their IRI + // shape is validated on read by JsonLdContextService::getImplementedTypes. + // `handoffContract` is the ADR-051 provider-side binding block + // (contract field → own property, per kind URI); its shape is + // validated at save time by HandoffContractBindingValidator. + $passThrough = ['unique', 'facetCacheTtl', 'calendarProvider', 'jsonld', 'implements', 'x-schema-org', 'handoffContract']; foreach ($configuration as $key => $value) { - if (in_array($key, $stringFields, true) === true) { - $validatedConfig[$key] = $this->validateStringConfigValue(key: $key, value: $value); - continue; - } + // Per-key isolation (#419): a bad VALUE for one config key must never + // lose the whole configuration. Direct API saves surface the throw as + // a 400, but on APP IMPORT the throw is silently swallowed upstream and + // every OTHER key — x-openregister-mcp, x-openregister-lifecycle, + // x-openregister-calculations, mailObjectTemplate — is lost with it + // (observed: pipelinq `lead` carried an invalid linkedType and lost its + // entire config incl. its MCP tools). Drop the offending key, record it + // so SchemaMapper can warn, and keep the rest of the configuration. + try { + $this->validateConfigurationEntry( + key: $key, + value: $value, + stringFields: $stringFields, + boolFields: $boolFields, + passThrough: $passThrough, + validatedConfig: $validatedConfig + ); + } catch (\InvalidArgumentException $e) { + // The write-only path annotation is exempt from per-key + // isolation and MUST fail the whole save. + // + // For every other key, "drop the offending key and keep the + // rest" is a safe degradation: the corresponding feature + // simply does not fire. For THIS key the failure mode is + // inverted — dropping it means the schema saves successfully + // with its nested secrets NO LONGER PROTECTED. A typo'd path + // (`configuratio.authentication.keys`) would persist a schema + // that looks annotated to a reviewer, passes save, and serves + // the secret in cleartext on the next read. That is precisely + // the phantom-annotation class this repo has been bitten by, + // except fail-OPEN instead of merely inert. A security control + // must fail closed and loudly: re-throw so a direct API save + // 400s and an app import aborts rather than silently shipping + // an unprotected schema. + if ((string) $key === self::WRITEONLY_PATHS_ANNOTATION) { + throw $e; + } - if (in_array($key, $boolFields, true) === true) { - $this->validateBoolConfigValue(key: $key, value: $value); - $validatedConfig[$key] = $value; - continue; - } + $this->droppedKeys[] = (string) $key; + }//end try + }//end foreach - if ($key === 'allowedTags') { - $this->validateAllowedTagsValue(value: $value); - $validatedConfig[$key] = $value; - continue; - } + return $validatedConfig; + }//end validateConfigurationArray() - if ($key === 'linkedTypes') { - $this->validateLinkedTypesValue(value: $value); - $validatedConfig[$key] = $value; - continue; - } + /** + * Validate the `x-openregister-writeonly-paths` annotation value. + * + * Accepts a list of dot-separated paths rooted at a declared top-level + * property. Everything here throws rather than warns, because the caller + * re-throws for this key (see validateConfigurationArray): a declaration + * that does not parse must never persist as an unenforced no-op. + * + * Rejects, in order: a non-list value; a non-string / empty entry; a path + * with an empty segment (`a..b`, leading/trailing dot); and a path whose + * ROOT segment is not a property this schema declares — the last one is + * what turns a real-world typo into a 400 instead of a silent leak. + * + * A path is NOT required to resolve beyond its root, and deliberately so: + * the whole point is to name keys inside an untyped `object` property that + * the schema cannot describe, so there is nothing to validate them against. + * + * @param mixed $value The raw annotation value. + * + * @throws \InvalidArgumentException If the declaration is malformed. + * + * @return array The validated list of paths. + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-nested-writeonly-paths-must-never-be-returned-on-any-read + */ + private function validateWriteOnlyPathsValue(mixed $value): array + { + if (is_array($value) === false || array_is_list($value) === false) { + throw new \InvalidArgumentException( + self::WRITEONLY_PATHS_ANNOTATION.' must be a list of dot-separated path strings' + ); + } - if ($key === 'calendarProvider' && is_array($value) === true) { - $this->validateCalendarProviderConfig(config: $value); - $validatedConfig[$key] = $value; - continue; - } + $declared = []; + if (is_array($this->properties) === true) { + $declared = array_map('strval', array_keys($this->properties)); + } - if (in_array($key, $passThrough, true) === true) { - $validatedConfig[$key] = $value; - continue; + $validated = []; + foreach ($value as $path) { + if (is_string($path) === false || trim($path) === '') { + throw new \InvalidArgumentException( + self::WRITEONLY_PATHS_ANNOTATION.' entries must be non-empty strings' + ); } - // Allow declarative annotation extensions to round-trip - // through the schema's configuration column. Validation of - // their shape is done by the dedicated validators (e.g. - // LifecycleAnnotationValidator) at schema-save time. - // - // The key MUST be in the declared vocabulary — unknown - // `x-openregister-*` keys are silently dropped to surface - // typos at save time rather than persisting them and having - // them silently no-op (e.g. `x-openregister-lifecycl` would - // otherwise round-trip without ever firing the listener). - if (str_starts_with((string) $key, 'x-openregister-') === true) { - if (in_array((string) $key, self::ANNOTATION_VOCABULARY, true) === true) { - $validatedConfig[$key] = $value; - continue; + $segments = explode('.', $path); + foreach ($segments as $segment) { + if (trim($segment) === '') { + throw new \InvalidArgumentException( + self::WRITEONLY_PATHS_ANNOTATION.' path "'.$path.'" has an empty segment' + ); } + } - // R07: track unknown `x-openregister-*` keys (almost - // always typos like `x-openregister-lifecycl`) so - // SchemaMapper can log them via its structured - // logger after save. The entity has no DI surface - // for a logger and the ADR added in F06 bans the - // `\OC::$server` static accessor — collecting on - // the entity and bridging through the mapper is - // the cleanest path that still surfaces a signal. - $this->droppedKeys[] = (string) $key; - }//end if + // Root-segment existence check. Skipped when the schema declares no + // properties at all — an entity built in isolation (unit tests, + // partial hydration) has nothing to check against, and failing there + // would reject valid declarations for the wrong reason. + $rootIsDeclared = (empty($declared) === true || in_array($segments[0], $declared, true) === true); + if ($rootIsDeclared === false) { + throw new \InvalidArgumentException( + self::WRITEONLY_PATHS_ANNOTATION.' path "'.$path.'" is rooted at "'.$segments[0] + .'", which is not a declared property of this schema' + ); + } + + $validated[] = $path; }//end foreach - return $validatedConfig; - }//end validateConfigurationArray() + return array_values(array_unique($validated)); + }//end validateWriteOnlyPathsValue() + + /** + * Validate and apply a single configuration entry. + * + * Extracted from validateConfigurationArray() so each key is validated in + * isolation (#419): the caller wraps this in a try/catch, and an invalid + * value drops only the offending key instead of the whole configuration. + * On success the accepted value is written into $validatedConfig by + * reference; unknown keys are simply not written, and unknown + * `x-openregister-*` keys are recorded in $this->droppedKeys. + * + * @param int|string $key Configuration key. + * @param mixed $value Raw value for the key. + * @param array $stringFields String-typed config fields. + * @param array $boolFields Bool-typed config fields. + * @param array $passThrough Keys stored without validation. + * @param array $validatedConfig Accumulator, passed by reference. + * + * @throws \InvalidArgumentException If the value is invalid for the key. + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function validateConfigurationEntry( + int|string $key, + mixed $value, + array $stringFields, + array $boolFields, + array $passThrough, + array &$validatedConfig + ): void { + if (in_array($key, $stringFields, true) === true) { + $validatedConfig[$key] = $this->validateStringConfigValue(key: $key, value: $value); + return; + } + + if (in_array($key, $boolFields, true) === true) { + $this->validateBoolConfigValue(key: $key, value: $value); + $validatedConfig[$key] = $value; + return; + } + + if ($key === 'allowedTags') { + $this->validateAllowedTagsValue(value: $value); + $validatedConfig[$key] = $value; + return; + } + + if ($key === 'linkedTypes') { + $this->validateLinkedTypesValue(value: $value); + $validatedConfig[$key] = $value; + return; + } + + if ($key === 'mailObjectTemplate') { + $this->validateMailObjectTemplateValue(value: $value); + $validatedConfig[$key] = $value; + return; + } + + if ($key === self::WRITEONLY_PATHS_ANNOTATION) { + $validatedConfig[$key] = $this->validateWriteOnlyPathsValue(value: $value); + return; + } + + if ($key === 'calendarProvider' && is_array($value) === true) { + $this->validateCalendarProviderConfig(config: $value); + $validatedConfig[$key] = $value; + return; + } + + if (in_array($key, $passThrough, true) === true) { + $validatedConfig[$key] = $value; + return; + } + + // Allow declarative annotation extensions to round-trip + // through the schema's configuration column. Validation of + // their shape is done by the dedicated validators (e.g. + // LifecycleAnnotationValidator) at schema-save time. + // + // The key MUST be in the declared vocabulary — unknown + // `x-openregister-*` keys are silently dropped to surface + // typos at save time rather than persisting them and having + // them silently no-op (e.g. `x-openregister-lifecycl` would + // otherwise round-trip without ever firing the listener). + if (str_starts_with((string) $key, 'x-openregister-') === true) { + if (in_array((string) $key, self::ANNOTATION_VOCABULARY, true) === true) { + $validatedConfig[$key] = $value; + return; + } + + // R07: track unknown `x-openregister-*` keys (almost + // always typos like `x-openregister-lifecycl`) so + // SchemaMapper can log them via its structured + // logger after save. The entity has no DI surface + // for a logger and the ADR added in F06 bans the + // `\OC::$server` static accessor — collecting on + // the entity and bridging through the mapper is + // the cleanest path that still surfaces a signal. + $this->droppedKeys[] = (string) $key; + }//end if + }//end validateConfigurationEntry() /** * Dropped `x-openregister-*` annotation keys collected during the most @@ -1744,7 +2144,7 @@ private function validateConfigurationArray(array $configuration): array */ public function consumeDroppedAnnotationKeys(): array { - $dropped = $this->droppedKeys; + $dropped = $this->droppedKeys; $this->droppedKeys = []; return $dropped; }//end consumeDroppedAnnotationKeys() @@ -1862,57 +2262,51 @@ private function validateAllowedTagsValue(mixed $value): void 'x-openregister-lifecycle', 'x-openregister-aggregations', 'x-openregister-calculations', + 'x-openregister-references', + 'x-openregister-aggregate-refs', 'x-openregister-notifications', 'x-openregister-widgets', 'x-openregister-relations', 'x-openregister-processing-activity', + // Read by ProcessingLogService::ANNOTATION_KEY (the AVG `logReads` + // dialect). Was absent from this list, so setConfiguration() silently + // DROPPED it and per-schema read-logging could never be enabled — + // register-level worked, so the capability looked healthy. + 'x-openregister-processing', 'x-openregister-archival', - 'x-openregister-seed', - ]; - - /** - * Valid linked type values for Nextcloud entity integration. - * - * @deprecated since pluggable-integration-registry — kept as - * a backwards-compat fallback so existing schemas with values like - * 'mail' / 'calendar' / 'talk' / 'deck' continue to validate - * while the matching IntegrationProvider leaves land. Once every - * leaf in the umbrella's Wave 1 ships, the registry is the only - * authority and this constant is removed by - * `cleanup-linked-entity-type-map`. New consumers MUST add a - * provider via `IntegrationRegistry::addProvider()` rather than - * append to this list. - * - * @see OCA\OpenRegister\Service\Integration\IntegrationRegistry::listIds() - * - * @spec openspec/changes/pluggable-integration-registry/tasks.md#task-8 - */ - private const VALID_LINKED_TYPES = [ - 'files', - 'mail', - 'contacts', - 'notes', - 'todos', - 'calendar', - 'talk', - 'deck', + 'x-openregister-object-source', + 'x-openregister-quality', + 'x-openregister-dedup', + 'x-openregister-flows', + 'x-openregister-survivorship', + 'x-openregister-merge', + 'x-openregister-handoff', + 'x-openregister-mcp', + 'x-openregister-approval-chains', + // Nested write-only dot-paths. Unlike every other entry in this list + // this key is NOT a plain pass-through: validateConfigurationEntry has + // a dedicated branch above that validates its shape, and + // validateConfigurationArray re-throws instead of dropping it. + self::WRITEONLY_PATHS_ANNOTATION, ]; /** * Validate the linkedTypes configuration value. * * Registry-driven validation per AD-5 of pluggable-integration-registry: - * an id is valid when it appears in EITHER the registry's listIds() - * OR the legacy VALID_LINKED_TYPES fallback. The legacy fallback - * keeps existing schemas (e.g. linkedTypes=['mail','calendar']) - * working while the matching providers ship. New ids (e.g. 'xwiki') - * become valid the moment their provider is registered. - * - * When the integration registry isn't available — i.e. the entity - * is constructed outside a request context (unit tests building - * Schema instances directly) — validation falls back to - * VALID_LINKED_TYPES alone. This preserves the existing test - * surface while letting production code benefit from the registry. + * an id is valid when it appears in the registry's `listIds()` + * output OR in the legacy-id allow-list returned by + * `legacyLinkedTypeIds()` (see that method for the rationale — it + * preserves backwards-compat for `linkedTypes` values that were + * created before the matching provider was renamed/added). + * + * When the integration registry isn't reachable (entity built + * outside a request context, e.g. unit tests constructing Schema + * directly), validation falls back to the legacy allow-list alone. + * + * Per `cleanup-linked-entity-type-map`, the public linked-types + * constant was removed; the surviving allow-list lives inside + * `legacyLinkedTypeIds()` as an implementation detail. * * @param mixed $value The linkedTypes value to validate. * @@ -1921,6 +2315,7 @@ private function validateAllowedTagsValue(mixed $value): void * @return void * * @spec openspec/changes/pluggable-integration-registry/tasks.md#task-7 + * @spec openspec/specs/cleanup-linked-entity-type-map/spec.md "Registry-Driven Behaviour Unchanged" */ private function validateLinkedTypesValue(mixed $value): void { @@ -1933,17 +2328,18 @@ private function validateLinkedTypesValue(mixed $value): void } $registryIds = $this->resolveIntegrationRegistryIds(); + $legacyIds = self::legacyLinkedTypeIds(); foreach ($value as $type) { if (is_string($type) === false) { throw new InvalidArgumentException("All values in 'linkedTypes' must be strings"); } - $valid = in_array($type, self::VALID_LINKED_TYPES, true) + $valid = in_array($type, $legacyIds, true) || in_array($type, $registryIds, true); if ($valid === false) { - $combined = array_unique(array_merge(self::VALID_LINKED_TYPES, $registryIds)); + $combined = array_unique(array_merge($legacyIds, $registryIds)); sort($combined); throw new InvalidArgumentException( "Invalid linked type '$type'. Valid values: ".implode(', ', $combined) @@ -1952,6 +2348,76 @@ private function validateLinkedTypesValue(mixed $value): void } }//end validateLinkedTypesValue() + /** + * Validate the mailObjectTemplate configuration value + * + * The template is a flat map of schema property name => prefill value + * used by the Mail-sidebar "create from email" action. String values + * may contain {{placeholder}} tokens; scalar non-string values pass + * through verbatim. + * + * @param mixed $value The mailObjectTemplate value to validate + * + * @throws InvalidArgumentException If the template is not a flat map of scalar values + * + * @return void + */ + private function validateMailObjectTemplateValue(mixed $value): void + { + if ($value === null) { + return; + } + + if (is_array($value) === false) { + throw new InvalidArgumentException("Configuration 'mailObjectTemplate' must be an object or null"); + } + + foreach ($value as $field => $template) { + if (is_string($field) === false || $field === '') { + throw new InvalidArgumentException("All keys in 'mailObjectTemplate' must be non-empty property names"); + } + + if (is_scalar($template) === false) { + throw new InvalidArgumentException("Value for '$field' in 'mailObjectTemplate' must be a scalar"); + } + } + }//end validateMailObjectTemplateValue() + + /** + * Legacy linked-type id allow-list — internal implementation detail. + * + * Returns the set of legacy `linkedTypes` ids that were valid + * before the IntegrationRegistry became the authoritative source. + * Preserved so existing schemas declaring `linkedTypes: ['mail', + * 'todos', ...]` keep validating after the public linked-types + * constant was removed by `cleanup-linked-entity-type-map`. + * New consumers SHOULD register + * a provider via `IntegrationRegistry::addProvider()`; this + * allow-list MUST NOT grow. + * + * NOT exposed as a public constant — the public symbol was the + * removal target. Method form keeps the values reachable from + * tests and inspectable in PHPStan without making them part of OR's + * API surface. + * + * @return array + * + * @spec openspec/specs/cleanup-linked-entity-type-map/spec.md "Constants Removed" + */ + private static function legacyLinkedTypeIds(): array + { + return [ + 'files', + 'mail', + 'contacts', + 'notes', + 'todos', + 'calendar', + 'talk', + 'deck', + ]; + }//end legacyLinkedTypeIds() + /** * Resolve the current set of registered integration ids. * @@ -1959,7 +2425,7 @@ private function validateLinkedTypesValue(mixed $value): void * reach it. We pull the registry from the server container at * validation time. Failures (tests without a booted container, * missing service binding) fall through to an empty list so the - * legacy VALID_LINKED_TYPES path keeps working. + * legacy allow-list path keeps working. * * @return array Registered integration ids, possibly empty. */ @@ -2323,60 +2789,6 @@ public function setAnyOf(?array $anyOf): void $this->markFieldUpdated(attribute: 'anyOf'); }//end setAnyOf() - /** - * Get the publication timestamp - * - * @return DateTime|null Publication timestamp - */ - public function getPublished(): ?DateTime - { - return $this->published; - }//end getPublished() - - /** - * Set the publication timestamp - * - * @param DateTime|string|null $published Publication timestamp (DateTime object or ISO 8601 string) - * - * @return void - */ - public function setPublished(DateTime|string|null $published): void - { - if (is_string($published) === true) { - $published = new DateTime($published); - } - - $this->published = $published; - $this->markFieldUpdated(attribute: 'published'); - }//end setPublished() - - /** - * Get the depublication timestamp - * - * @return DateTime|null Depublication timestamp - */ - public function getDepublished(): ?DateTime - { - return $this->depublished; - }//end getDepublished() - - /** - * Set the depublication timestamp - * - * @param DateTime|string|null $depublished Depublication timestamp (DateTime object or ISO 8601 string) - * - * @return void - */ - public function setDepublished(DateTime|string|null $depublished): void - { - if (is_string($depublished) === true) { - $depublished = new DateTime($depublished); - } - - $this->depublished = $depublished; - $this->markFieldUpdated(attribute: 'depublished'); - }//end setDepublished() - /** * Check if this schema is managed by any configuration * diff --git a/lib/Db/SchemaChangelog.php b/lib/Db/SchemaChangelog.php new file mode 100644 index 0000000000..31d8df1d0c --- /dev/null +++ b/lib/Db/SchemaChangelog.php @@ -0,0 +1,205 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class SchemaChangelog + * + * @method int getSchemaId() + * @method void setSchemaId(int $schemaId) + * @method string|null getVersion() + * @method void setVersion(?string $version) + * @method string|null getClassification() + * @method void setClassification(?string $classification) + * @method array|null getChanges() + * @method void setChanges(?array $changes) + * @method string|null getActor() + * @method void setActor(?string $actor) + * @method string|null getAcknowledgedBy() + * @method void setAcknowledgedBy(?string $acknowledgedBy) + * @method DateTime|null getAcknowledgedAt() + * @method void setAcknowledgedAt(?DateTime $acknowledgedAt) + * @method DateTime|null getCreated() + * @method void setCreated(?DateTime $created) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class SchemaChangelog extends Entity implements JsonSerializable +{ + + /** + * The schema this changelog entry belongs to. + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * The resulting schema version for this change. + * + * @var string|null + */ + protected ?string $version = null; + + /** + * The change classification (compatible | breaking | none). + * + * @var string|null + */ + protected ?string $classification = null; + + /** + * The typed change list. + * + * @var array>|null + */ + protected ?array $changes = null; + + /** + * The acting user id. + * + * @var string|null + */ + protected ?string $actor = null; + + /** + * The user who acknowledged a breaking change. + * + * @var string|null + */ + protected ?string $acknowledgedBy = null; + + /** + * When the breaking change was acknowledged. + * + * @var DateTime|null + */ + protected ?DateTime $acknowledgedAt = null; + + /** + * Creation timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $created = null; + + /** + * Constructor — registers field types for hydration. + */ + public function __construct() + { + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'version', type: 'string'); + $this->addType(fieldName: 'classification', type: 'string'); + $this->addType(fieldName: 'changes', type: 'json'); + $this->addType(fieldName: 'actor', type: 'string'); + $this->addType(fieldName: 'acknowledgedBy', type: 'string'); + $this->addType(fieldName: 'acknowledgedAt', type: 'datetime'); + $this->addType(fieldName: 'created', type: 'datetime'); + + }//end __construct() + + /** + * The field names registered with the 'json' type. + * + * @return array The json-typed field names. + */ + public function getJsonFields(): array + { + return array_keys( + array_filter( + $this->getFieldTypes(), + static function ($field) { + return $field === 'json'; + } + ) + ); + }//end getJsonFields() + + /** + * Hydrate the entity from an array. + * + * Without this, SchemaChangelogMapper::createFromArray()'s `$entry->hydrate()` + * call hit Entity::__call and threw "hydrate does not exist", so every schema + * changelog write failed silently (swallowed by the caller's try/catch) and + * the schema-change audit trail was never recorded. + * + * @param array $object The source data. + * + * @return static This entity, hydrated. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function hydrate(array $object): static + { + $jsonFields = $this->getJsonFields(); + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields, true) === true && $value === []) { + $value = null; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Silently ignore invalid properties. + } + } + + return $this; + }//end hydrate() + + /** + * JSON serialisation. + * + * @return array The serialised changelog entry. + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'schemaId' => $this->schemaId, + 'version' => $this->version, + 'classification' => $this->classification, + 'changes' => ($this->changes ?? []), + 'actor' => $this->actor, + 'acknowledgedBy' => $this->acknowledgedBy, + 'acknowledgedAt' => $this->acknowledgedAt?->format(DateTime::ATOM), + 'created' => $this->created?->format(DateTime::ATOM), + ]; + + }//end jsonSerialize() +}//end class diff --git a/lib/Db/SchemaChangelogMapper.php b/lib/Db/SchemaChangelogMapper.php new file mode 100644 index 0000000000..c10bb2b3d5 --- /dev/null +++ b/lib/Db/SchemaChangelogMapper.php @@ -0,0 +1,117 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class SchemaChangelogMapper + * + * @template-extends QBMapper + */ +class SchemaChangelogMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_schema_changelog', entityClass: SchemaChangelog::class); + + }//end __construct() + + /** + * Find a changelog entry by id. + * + * @param int $id The entry id. + * + * @return SchemaChangelog The entry. + * + * @throws DoesNotExistException When not found. + */ + public function find(int $id): SchemaChangelog + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity(query: $qb); + + }//end find() + + /** + * Find changelog entries for a schema, newest first. + * + * @param int $schemaId The schema id. + * @param int|null $limit Optional page size. + * @param int|null $offset Optional page offset. + * + * @return SchemaChangelog[] The entries. + */ + public function findBySchema(int $schemaId, ?int $limit=null, ?int $offset=null): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('schema_id', $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT))) + ->orderBy('id', 'DESC'); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities(query: $qb); + + }//end findBySchema() + + /** + * Create a changelog entry from an array of values. + * + * @param array $data The entry values. + * + * @return SchemaChangelog The persisted entry. + */ + public function createFromArray(array $data): SchemaChangelog + { + $entry = new SchemaChangelog(); + $entry->hydrate($data); + + if ($entry->getCreated() === null) { + $entry->setCreated(new DateTime()); + } + + return $this->insert(entity: $entry); + + }//end createFromArray() +}//end class diff --git a/lib/Db/SchemaMapper.php b/lib/Db/SchemaMapper.php index 9efa851a6c..0b6a994150 100644 --- a/lib/Db/SchemaMapper.php +++ b/lib/Db/SchemaMapper.php @@ -50,9 +50,16 @@ use OCA\OpenRegister\Service\Aggregation\WidgetAnnotationValidator; use OCA\OpenRegister\Service\Archival\ArchivalAnnotationValidator; use OCA\OpenRegister\Service\Calculation\CalculationAnnotationValidator; +use OCA\OpenRegister\Service\Handoff\HandoffAnnotationValidator; +use OCA\OpenRegister\Service\Handoff\HandoffContractBindingValidator; use OCA\OpenRegister\Service\Lifecycle\LifecycleAnnotationValidator; +use OCA\OpenRegister\Service\Mcp\McpAnnotationValidator; +use OCA\OpenRegister\Service\Merge\MergeAnnotationValidator; use OCA\OpenRegister\Service\Notification\NotificationAnnotationValidator; +use OCA\OpenRegister\Service\Quality\DedupAnnotationValidator; +use OCA\OpenRegister\Service\Quality\QualityAnnotationValidator; use OCA\OpenRegister\Service\Schemas\PropertyValidatorHandler; +use OCA\OpenRegister\Service\Survivorship\SurvivorshipAnnotationValidator; /** * SchemaMapper handles database operations for Schema entities @@ -75,8 +82,8 @@ * @method Schema insert(Entity $entity) * @method Schema update(Entity $entity) * @method Schema insertOrUpdate(Entity $entity) - * @method Schema delete(Entity $entity) - * @method Schema find(int|string $id) + * @method Schema delete(Entity $entity, bool $force=false) + * @method Schema find(int|string $id, ?array $_extend=[], bool $_rbac=true, bool $_multitenancy=true) * @method Schema findEntity(IQueryBuilder $query) * @method Schema[] findAll(int|null $limit=null, int|null $offset=null) * @method list findEntities(IQueryBuilder $query) @@ -98,6 +105,18 @@ class SchemaMapper extends QBMapper { use MultiTenancyTrait; + /** + * Maximum number of expressions allowed in a single SQL IN() list. + * + * Nextcloud's QueryBuilder refuses to emit more than 1000 expressions in an + * IN() list because Oracle rejects them; exceeding it logs an error and + * emits an "Undefined array key 0" PHP warning. Batched id lookups must be + * chunked below this bound. + * + * @var integer + */ + private const MAX_IN_LIST_SIZE = 1000; + /** * Event dispatcher instance * @@ -215,7 +234,6 @@ public function __construct( * * @param int|string $id The id of the schema * @param array $_extend Optional array of extensions (e.g., ['@self.stats']) - * @param bool|null $published Whether to enable published bypass (default: null = check config) * @param bool $_rbac Whether to apply RBAC permission checks (default: true) * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) * Set to false to bypass organization filter @@ -231,7 +249,6 @@ public function __construct( public function find( string | int $id, ?array $_extend=[], - ?bool $published=null, bool $_rbac=true, bool $_multitenancy=true ): Schema { @@ -246,7 +263,8 @@ public function find( $mtFlag = '1'; } - $cacheKey = strtolower((string) $id).':'.$rbacFlag.':'.$mtFlag; + $cacheSuffix = ':'.$rbacFlag.':'.$mtFlag; + $cacheKey = strtolower((string) $id).$cacheSuffix; if (isset($this->findCache[$cacheKey]) === true) { return $this->findCache[$cacheKey]; } @@ -275,18 +293,29 @@ public function find( ); if (is_numeric($id) === true) { + $idParam = $qb->createNamedParameter(value: (int) $id, type: IQueryBuilder::PARAM_INT); $orConditions->add( - $qb->expr()->eq('id', $qb->createNamedParameter(value: (int) $id, type: IQueryBuilder::PARAM_INT)) + $qb->expr()->eq('id', $idParam) ); } $qb->where($orConditions); - // Apply organisation filter with published entity bypass support - // Published schemas can bypass multi-tenancy restrictions if configured + // BUG-DB-10: when the identifier is numeric it is ambiguous (it could be a + // primary key id OR a numeric uuid/slug). Prefer the exact primary-key + // match by ordering an `id = ?` hit first, so a row whose slug happens to + // be "5" never shadows the row with id 5. + if (is_numeric($id) === true) { + $qb->addOrderBy( + $qb->createFunction( + 'CASE WHEN id = '.$idParam.' THEN 0 ELSE 1 END' + ), + 'ASC' + ); + } + + // Apply organisation filter. // Set $_multitenancy=false to bypass organization filter (e.g., when expanding schemas for registers). - // ApplyOrganisationFilter handles $multiTenancyEnabled=false internally. - // Use $published parameter if provided, otherwise check config. $this->applyOrganisationFilter( qb: $qb, columnName: 'organisation', @@ -315,22 +344,19 @@ public function find( $schema = $this->resolveSchemaExtension(schema: $schema); // Cache by all possible identifiers to handle lookups by id, uuid, or slug. - $rbacChar = '0'; - if ($_rbac === true) { - $rbacChar = '1'; - } + // BUG-DB-10: reuse the exact same suffix (rbac + multitenancy + published) + // as the read-side key so cache writes and reads stay consistent. + $this->findCache[$cacheKey] = $schema; + $this->findCache[(string) $schema->getId().$cacheSuffix] = $schema; - $mtChar = '0'; - if ($_multitenancy === true) { - $mtChar = '1'; + // BUG-DB-10: guard against a null uuid before strtolower(). + $schemaUuid = $schema->getUuid(); + if ($schemaUuid !== null) { + $this->findCache[strtolower($schemaUuid).$cacheSuffix] = $schema; } - $rbacSuffix = ':'.$rbacChar.':'.$mtChar; - $this->findCache[$cacheKey] = $schema; - $this->findCache[(string) $schema->getId().$rbacSuffix] = $schema; - $this->findCache[strtolower($schema->getUuid()).$rbacSuffix] = $schema; if ($schema->getSlug() !== null) { - $this->findCache[strtolower($schema->getSlug()).$rbacSuffix] = $schema; + $this->findCache[strtolower($schema->getSlug()).$cacheSuffix] = $schema; } return $schema; @@ -363,10 +389,9 @@ public function clearFindCache(int $schemaId): void /** * Finds multiple schemas by id * - * @param array $ids The ids of the schemas - * @param bool|null $published Whether to enable published bypass (default: null = check config) - * @param bool $_rbac Whether to apply RBAC permission checks (default: true) - * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) + * @param array $ids The ids of the schemas + * @param bool $_rbac Whether to apply RBAC permission checks (default: true) + * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) * * @throws \OCP\AppFramework\Db\DoesNotExistException If a schema does not exist * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException If multiple schemas are found @@ -380,7 +405,7 @@ public function clearFindCache(int $schemaId): void * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flags control security filtering behavior */ - public function findMultiple(array $ids, ?bool $published=null, bool $_rbac=true, bool $_multitenancy=true): array + public function findMultiple(array $ids, bool $_rbac=true, bool $_multitenancy=true): array { $result = []; foreach ($ids as $id) { @@ -388,7 +413,6 @@ public function findMultiple(array $ids, ?bool $published=null, bool $_rbac=true $result[] = $this->find( id: $id, _extend: [], - published: $published, _rbac: $_rbac, _multitenancy: $_multitenancy ); @@ -418,20 +442,31 @@ public function findMultipleOptimized(array $ids): array return []; } - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from('openregister_schemas') - ->where( - $qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)) - ); - - $result = $qb->executeQuery(); $schemas = []; - while (($row = $result->fetch()) !== false) { - $schema = new Schema(); - $schema = $schema->fromRow($row); - $schemas[$row['id']] = $schema; + // An IN() list is capped at MAX_IN_LIST_SIZE expressions: Nextcloud's + // QueryBuilder logs "More than 1000 expressions in a list are not allowed + // on Oracle" (and emits an "Undefined array key 0" PHP warning) once the + // list exceeds that. This instance already holds 1,233 schemas, so the + // registers-with-stats endpoint tripped it on every request. Chunk it. + foreach (array_chunk($ids, self::MAX_IN_LIST_SIZE) as $chunk) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('openregister_schemas') + ->where( + $qb->expr()->in('id', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)) + ); + + $result = $qb->executeQuery(); + + while (($row = $result->fetch()) !== false) { + $schema = new Schema(); + $schema = $schema->fromRow($row); + + $schemas[$row['id']] = $schema; + } + + $result->closeCursor(); } return $schemas; @@ -443,12 +478,11 @@ public function findMultipleOptimized(array $ids): array * Searches for schemas matching the given slug with optional * multi-tenancy and RBAC filtering. * - * @param string $slug The slug to search for - * @param int $limit Maximum number of results (default: 10) - * @param int $offset Offset for pagination (default: 0) - * @param bool|null $published Whether to enable published bypass (default: null = check config) - * @param bool $_rbac Whether to apply RBAC permission checks (default: true) - * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) + * @param string $slug The slug to search for + * @param int $limit Maximum number of results (default: 10) + * @param int $offset Offset for pagination (default: 0) + * @param bool $_rbac Whether to apply RBAC permission checks (default: true) + * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) * * @return Schema[] Array of matching schemas * @@ -460,7 +494,6 @@ public function findBySlug( string $slug, int $limit=10, int $offset=0, - ?bool $published=null, bool $_rbac=true, bool $_multitenancy=true ): array { @@ -472,7 +505,7 @@ public function findBySlug( $qb->expr()->eq('slug', $qb->createNamedParameter($slug, IQueryBuilder::PARAM_STR)) ); - // Apply organisation filter with published entity bypass support. + // Apply organisation filter. $this->applyOrganisationFilter( qb: $qb, columnName: 'organisation', @@ -505,7 +538,6 @@ public function findBySlug( * @param array|null $searchConditions The search conditions to apply * @param array|null $searchParams The search parameters to apply * @param array $_extend Optional array of extensions (e.g., ['@self.stats']) - * @param bool|null $published Whether to enable published bypass (default: null = check config) * @param bool $_rbac Whether to apply RBAC permission checks (default: true) * @param bool $_multitenancy Whether to apply multi-tenancy filtering (default: true) * @@ -524,7 +556,6 @@ public function findAll( ?array $searchConditions=[], ?array $searchParams=[], ?array $_extend=[], - ?bool $published=null, bool $_rbac=true, bool $_multitenancy=true ): array { @@ -562,10 +593,7 @@ public function findAll( } } - // Apply organisation filter with published entity bypass support - // Published schemas can bypass multi-tenancy restrictions if configured. - // ApplyOrganisationFilter handles $multiTenancyEnabled=false internally. - // Use $published parameter if provided, otherwise check config. + // Apply organisation filter. $this->applyOrganisationFilter( qb: $qb, columnName: 'organisation', @@ -591,8 +619,8 @@ public function findAll( */ public function insert(Entity $entity): Entity { - // Verify RBAC permission to create - // $this->verifyRbacPermission('create', 'schema'); + // Verify RBAC permission to create. + $this->verifyRbacPermission(action: 'create', entityType: 'schema'); // Auto-set organisation from active session. $this->setOrganisationOnCreate(entity: $entity); @@ -626,9 +654,16 @@ private function cleanObject(Schema $schema): void $this->validateLifecycleAnnotation(schema: $schema); $this->validateAggregationsAnnotation(schema: $schema); $this->validateCalculationsAnnotation(schema: $schema); + $this->validateQualityAnnotation(schema: $schema); + $this->validateDedupAnnotation(schema: $schema); + $this->validateSurvivorshipAnnotation(schema: $schema); + $this->validateMergeAnnotation(schema: $schema); $this->validateNotificationsAnnotation(schema: $schema); $this->validateWidgetsAnnotation(schema: $schema); $this->validateArchivalAnnotation(schema: $schema); + $this->validateHandoffAnnotation(schema: $schema); + $this->validateHandoffContractBinding(schema: $schema); + $this->validateMcpAnnotation(schema: $schema); $this->logDroppedAnnotationKeys(schema: $schema); }//end cleanObject() @@ -692,8 +727,18 @@ private function validateLifecycleAnnotation(Schema $schema): void return; } + // Lifecycle is ADVISORY metadata (a state-machine hint), not a storage + // requirement: a schema with a malformed or non-canonical lifecycle block + // still stores objects correctly. Rejecting the whole schema import over an + // advisory annotation breaks register imports for every app that ships a + // partial / different-dialect lifecycle block. Degrade to a non-fatal + // warning and import the schema (the lifecycle simply won't drive a status + // workflow) instead of throwing. $messages = array_map(static fn(array $err) => $err['message'], $errors); - throw new Exception('x-openregister-lifecycle: '.implode(' ', $messages)); + $this->logger->warning( + 'x-openregister-lifecycle annotation on schema "'.((string) ($schema->getSlug() ?? '')).'" is ' + .'invalid and was ignored (no status workflow applied): '.implode(' ', $messages) + ); }//end validateLifecycleAnnotation() /** @@ -723,8 +768,15 @@ private function validateAggregationsAnnotation(Schema $schema): void return; } + // Aggregations are ADVISORY report-metadata, not a storage requirement. + // A malformed / empty / non-canonical aggregation block must not abort the + // whole schema import — the schema still stores objects, the aggregation + // simply won't be runnable. Degrade to a non-fatal warning. $messages = array_map(static fn(array $err) => $err['message'], $errors); - throw new Exception('x-openregister-aggregations: '.implode(' ', $messages)); + $this->logger->warning( + 'x-openregister-aggregations annotation on schema "'.((string) ($schema->getSlug() ?? '')).'" is ' + .'invalid and was ignored (aggregation not registered): '.implode(' ', $messages) + ); }//end validateAggregationsAnnotation() /** @@ -747,6 +799,7 @@ private function validateCalculationsAnnotation(Schema $schema): void $shape = [ 'properties' => ($schema->getProperties() ?? []), 'x-openregister-calculations' => $annotation, + 'x-openregister-references' => ($configuration['x-openregister-references'] ?? []), ]; $errors = (new CalculationAnnotationValidator())->validate($shape); @@ -754,10 +807,162 @@ private function validateCalculationsAnnotation(Schema $schema): void return; } + // Calculations are ADVISORY derived-field metadata, not a storage + // requirement. A malformed / non-canonical calculation block (e.g. a type + // outside the canonical set, or a missing expression) must not abort the + // whole schema import — the schema still stores objects, the calculation + // simply won't be evaluated. Degrade to a non-fatal warning. $messages = array_map(static fn(array $err) => $err['message'], $errors); - throw new Exception('x-openregister-calculations: '.implode(' ', $messages)); + $this->logger->warning( + 'x-openregister-calculations annotation on schema "'.((string) ($schema->getSlug() ?? '')).'" is ' + .'invalid and was ignored (calculation not evaluated): '.implode(' ', $messages) + ); }//end validateCalculationsAnnotation() + /** + * Validate the optional `x-openregister-quality` annotation. + * + * @param Schema $schema Schema to validate. + * + * @return void + */ + private function validateQualityAnnotation(Schema $schema): void + { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-quality'] ?? null); + if (is_array($annotation) === false) { + return; + } + + $shape = [ + 'properties' => ($schema->getProperties() ?? []), + 'x-openregister-quality' => $annotation, + ]; + + $errors = (new QualityAnnotationValidator())->validate($shape); + if (count($errors) === 0) { + return; + } + + // Quality scoring is ADVISORY derived-field metadata, not a storage + // requirement. A malformed quality block must not abort the whole schema + // import — the schema still stores objects, the score simply won't be + // materialised. Degrade to a non-fatal warning. + $messages = array_map(static fn(array $err) => $err['message'], $errors); + $this->logger->warning( + 'x-openregister-quality annotation on schema "'.((string) ($schema->getSlug() ?? '')).'" is ' + .'invalid and was ignored (quality score not materialised): '.implode(' ', $messages) + ); + }//end validateQualityAnnotation() + + /** + * Validate the optional `x-openregister-dedup` annotation. + * + * @param Schema $schema Schema to validate. + * + * @return void + */ + private function validateDedupAnnotation(Schema $schema): void + { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-dedup'] ?? null); + if (is_array($annotation) === false) { + return; + } + + $shape = [ + 'properties' => ($schema->getProperties() ?? []), + 'x-openregister-dedup' => $annotation, + ]; + + $errors = (new DedupAnnotationValidator())->validate($shape); + if (count($errors) === 0) { + return; + } + + // Dedup match rules are ADVISORY metadata consumed on demand by + // DuplicateDetectionService. A malformed block must not abort the schema + // import — duplicate detection simply falls back to caller-supplied rules. + // Degrade to a non-fatal warning. + $messages = array_map(static fn(array $err) => $err['message'], $errors); + $this->logger->warning( + 'x-openregister-dedup annotation on schema "'.((string) ($schema->getSlug() ?? '')).'" is ' + .'invalid and was ignored (declared match rules not used): '.implode(' ', $messages) + ); + }//end validateDedupAnnotation() + + /** + * Validate the optional `x-openregister-survivorship` annotation. + * + * @param Schema $schema Schema to validate. + * + * @return void + */ + private function validateSurvivorshipAnnotation(Schema $schema): void + { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-survivorship'] ?? null); + if (is_array($annotation) === false) { + return; + } + + $shape = [ + 'properties' => ($schema->getProperties() ?? []), + 'x-openregister-survivorship' => $annotation, + ]; + + $errors = (new SurvivorshipAnnotationValidator())->validate($shape); + if (count($errors) === 0) { + return; + } + + // Survivorship is ADVISORY derived-field metadata, not a storage + // requirement. A malformed survivorship block must not abort the whole + // schema import — the schema still stores objects, the golden record + // simply won't be materialised. Degrade to a non-fatal warning. + $messages = array_map(static fn(array $err) => $err['message'], $errors); + $this->logger->warning( + 'x-openregister-survivorship annotation on schema "'.((string) ($schema->getSlug() ?? '')).'" is ' + .'invalid and was ignored (golden record not materialised): '.implode(' ', $messages) + ); + }//end validateSurvivorshipAnnotation() + + /** + * Validate the optional `x-openregister-merge` annotation. + * + * @param Schema $schema Schema to validate. + * + * @return void + */ + private function validateMergeAnnotation(Schema $schema): void + { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-merge'] ?? null); + if (is_array($annotation) === false) { + return; + } + + $shape = [ + 'properties' => ($schema->getProperties() ?? []), + 'x-openregister-merge' => $annotation, + ]; + + $errors = (new MergeAnnotationValidator())->validate($shape); + if (count($errors) === 0) { + return; + } + + // Merge config is ADVISORY steward-action metadata, not a storage + // requirement. A malformed merge block must not abort the whole schema + // import — the schema still stores objects, merges simply fall back to + // the documented defaults. Degrade to a non-fatal warning. + $messages = array_map(static fn(array $err) => $err['message'], $errors); + $this->logger->warning( + 'x-openregister-merge annotation on schema "'.((string) ($schema->getSlug() ?? '')).'" is ' + .'invalid and was ignored (merge falls back to defaults): '.implode(' ', $messages) + ); + }//end validateMergeAnnotation() + /** * Validate the optional `x-openregister-notifications` annotation. * @@ -786,7 +991,19 @@ private function validateNotificationsAnnotation(Schema $schema): void } $messages = array_map(static fn(array $err) => $err['message'], $errors); - throw new Exception('x-openregister-notifications: '.implode(' ', $messages)); + + // A malformed OPTIONAL notification annotation must NOT block the schema + // itself — and, on config import, the entire register + every schema + // that references it (the import aborts and 0 objects land). The runtime + // dispatcher only fires notifications whose trigger matches a known + // event, so an invalid/legacy notification spec (e.g. the older + // `onCreate`/`onStatusChange` shape without a trigger.type) is simply + // inert. Log a warning so it can be cleaned up, but keep the schema + // valid and importable. + $this->logger->warning( + 'Schema "'.$schema->getSlug().'" has invalid x-openregister-notifications (ignored at runtime, fix to enable): '.implode(' ', $messages), + ['schema' => $schema->getSlug()] + ); }//end validateNotificationsAnnotation() /** @@ -835,7 +1052,7 @@ private function validateWidgetsAnnotation(Schema $schema): void * * @return void * - * @spec openspec/changes/add-archival-annotation-support/tasks.md#task-2 + * @spec openspec/specs/archival-annotation-vocabulary/spec.md */ private function validateArchivalAnnotation(Schema $schema): void { @@ -856,6 +1073,130 @@ private function validateArchivalAnnotation(Schema $schema): void throw new Exception('x-openregister-archival: '.implode(' ', $messages)); }//end validateArchivalAnnotation() + /** + * Validate the optional `x-openregister-handoff` annotation (ADR-051). + * + * The annotation is stored under `configuration['x-openregister-handoff']`. + * A malformed handoff declaration REJECTS the schema (contract: schema-save + * validation SHALL reject with the typed handoff-* error codes) — unlike + * advisory annotations, a broken handoff would otherwise surface as a + * runtime conversion failure on user action. + * + * @param Schema $schema Schema to validate. + * + * @throws Exception When the annotation is malformed. + * + * @return void + * + * @spec openspec/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + private function validateHandoffAnnotation(Schema $schema): void + { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-handoff'] ?? null); + if (is_array($annotation) === false) { + return; + } + + $shape = [ + 'properties' => ($schema->getProperties() ?? []), + 'x-openregister-handoff' => $annotation, + 'x-openregister-lifecycle' => ($configuration['x-openregister-lifecycle'] ?? null), + ]; + + $errors = (new HandoffAnnotationValidator())->validate($shape); + if (count($errors) === 0) { + return; + } + + $messages = array_map(static fn(array $err) => $err['code'].': '.$err['message'], $errors); + throw new Exception('x-openregister-handoff: '.implode(' ', $messages)); + }//end validateHandoffAnnotation() + + /** + * Validate the optional `handoffContract` binding block (ADR-051, + * provider side). Stored at `configuration['handoffContract']`; when + * present, every mandatory contract field of each bound kind must map to + * an existing own property — otherwise the schema is rejected with + * `handoff-contract-incomplete` listing the missing fields. A schema that + * implements a kind with NO binding block passes untouched (it is simply + * not a handoff provider). + * + * @param Schema $schema Schema to validate. + * + * @throws Exception When the binding block is incomplete or malformed. + * + * @return void + * + * @spec openspec/specs/semantic-object-handoff/spec.md + * (Scenario: Implementer omits a mandatory contract field) + */ + private function validateHandoffContractBinding(Schema $schema): void + { + $configuration = ($schema->getConfiguration() ?? []); + if (array_key_exists('handoffContract', $configuration) === false) { + return; + } + + $shape = [ + 'properties' => ($schema->getProperties() ?? []), + 'handoffContract' => $configuration['handoffContract'], + 'implements' => ($configuration['implements'] ?? null), + 'jsonld' => ($configuration['jsonld'] ?? null), + 'x-schema-org' => ($configuration['x-schema-org'] ?? null), + ]; + + $errors = (new HandoffContractBindingValidator())->validate($shape); + if (count($errors) === 0) { + return; + } + + $messages = array_map(static fn(array $err) => $err['code'].': '.$err['message'], $errors); + throw new Exception('handoffContract: '.implode(' ', $messages)); + }//end validateHandoffContractBinding() + + /** + * Validate the optional `x-openregister-mcp` annotation (ADR-063). + * + * The annotation is stored under `configuration['x-openregister-mcp']`. + * This change defines and validates the declaration only — no MCP tool + * is emitted and no serving surface is altered by this validator (that + * is the `or-mcp-derived-tool-provider` change). A malformed block + * fails the schema save loudly, consistent with every sibling + * `x-openregister-*` dialect validator. + * + * @param Schema $schema Schema to validate. + * + * @throws Exception When the annotation is malformed. + * + * @return void + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DIALECT-002 — Save-time validation of the dialect shape) + */ + private function validateMcpAnnotation(Schema $schema): void + { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-mcp'] ?? null); + if (is_array($annotation) === false) { + return; + } + + $shape = [ + 'properties' => ($schema->getProperties() ?? []), + 'x-openregister-mcp' => $annotation, + ]; + + $errors = (new McpAnnotationValidator())->validate($shape); + if (count($errors) === 0) { + return; + } + + $messages = array_map(static fn(array $err) => $err['code'].': '.$err['message'], $errors); + throw new Exception('x-openregister-mcp: '.implode(' ', $messages)); + }//end validateMcpAnnotation() + /** * Clean $ref properties to ensure they are strings * @@ -1230,8 +1571,8 @@ public function createFromArray(array $object): Schema */ public function update(Entity $entity): Entity { - // Verify RBAC permission to update - // $this->verifyRbacPermission('update', 'schema'); + // Verify RBAC permission to update. + $this->verifyRbacPermission(action: 'update', entityType: 'schema'); // Verify user has access to this organisation. $this->verifyOrganisationAccess(entity: $entity); @@ -1299,46 +1640,55 @@ public function updateFromArray(int $id, array $object): Schema /** * Delete a schema * + * **DELETE SAFETY (runtime-schema-api)**: refuses to delete a schema that still + * holds objects, unless the caller explicitly asks to orphan them ($force). + * + * This guard is enforced HERE, at the mapper, rather than only in + * SchemasController::destroy(), because the mapper is the choke point every + * deletion caller shares — including the AI/LLM-invokable SchemasToolProvider + * and SchemaTool surfaces, which have no controller in front of them. + * * @param Entity $entity The schema entity to delete + * @param bool $force Bypass the object-count guard and deliberately orphan the + * objects. Only the `?force=true` disposition of + * `DELETE /api/schemas/{id}` may pass true. The cascade + * disposition needs no bypass: it deletes the rows first, so + * the guard naturally counts 0. * * @throws \OCP\DB\Exception If a database error occurs + * @throws ValidationException If objects are still attached and $force is false * @throws \Exception If user doesn't have delete permission or access to this organisation * * @return Schema The deleted schema * * @psalm-suppress PossiblyUnusedReturnValue + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The force flag is the API-level disposition, mirrored from the endpoint. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md */ - public function delete(Entity $entity): Schema + public function delete(Entity $entity, bool $force=false): Schema { - // Verify RBAC permission to delete - // $this->verifyRbacPermission('delete', 'schema'); + // Verify RBAC permission to delete. + $this->verifyRbacPermission(action: 'delete', entityType: 'schema'); // Verify user has access to this organisation. $this->verifyOrganisationAccess(entity: $entity); - // Check for attached objects before deleting (using direct database query to avoid circular dependency). $schemaId = $entity->id; if (method_exists($entity, 'getId') === true) { $schemaId = $entity->getId(); } - // Count objects that reference this schema (excluding soft-deleted objects). - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->func()->count('*')) - ->from('openregister_objects') - ->where( - $qb->expr()->eq('schema', $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT)) - ) - ->andWhere($qb->expr()->isNull('deleted')); - - $result = $qb->executeQuery(); - $count = (int) $result->fetchOne(); - $result->closeCursor(); - - if ($count > 0) { - throw new ValidationException(message: 'Cannot delete schema: objects are still attached.'); + if ($force === false) { + $count = $this->countAttachedObjects(schemaId: (int) $schemaId); + if ($count > 0) { + throw new ValidationException( + message: 'Cannot delete schema: '.$count.' objects are still attached.' + ); + } } - // Proceed with deletion if no objects are attached. + // Proceed with deletion. $result = parent::delete(entity: $entity); // Dispatch deletion event. @@ -1349,6 +1699,96 @@ public function delete(Entity $entity): Schema return $result; }//end delete() + /** + * Count the objects still attached to a schema. + * + * Objects live in the per-register/schema MAGIC tables + * (`openregister_table_{registerId}_{schemaId}`). The previous implementation of + * this guard counted rows in the retired `openregister_objects` blob table, which + * is always empty for magic-table objects — so it counted 0 and waved every + * deletion through. + * + * This is a direct DB query, not a call into MagicMapper/MagicStatisticsHandler, + * and that is deliberate: MagicStatisticsHandler injects SchemaMapper, so injecting + * it back here would be a genuine circular dependency. The table name is + * deterministic, so the count can be taken without the mapper. + * + * Soft-deleted rows are excluded, matching the object count the controller's guard + * reports to the caller. + * + * @param int $schemaId The schema id. + * + * @return int The number of live objects attached to the schema. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md + */ + private function countAttachedObjects(int $schemaId): int + { + $total = 0; + + foreach ($this->getRegisterIdsWithSchema(schemaId: $schemaId) as $registerId) { + $tableName = MagicMapper::TABLE_PREFIX.$registerId.'_'.$schemaId; + + if ($this->db->tableExists($tableName) === false) { + continue; + } + + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*')) + ->from($tableName) + ->where($qb->expr()->isNull('_deleted')); + + $result = $qb->executeQuery(); + $total += (int) $result->fetchOne(); + $result->closeCursor(); + } + + return $total; + }//end countAttachedObjects() + + /** + * Find the ids of every register that references a schema. + * + * Direct query + decode in PHP, mirroring RegisterMapper::getAllRegisterIdsWithSchema(): + * the `schemas` column is JSON on Postgres and text elsewhere, so there is no + * portable SQL predicate for "array contains N". Registers are O(10s) per install, + * so the cost is trivial. RegisterMapper itself cannot be injected here — it + * injects SchemaMapper. + * + * @param int $schemaId The schema id. + * + * @return array The register ids. + */ + private function getRegisterIdsWithSchema(int $schemaId): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('id', 'schemas') + ->from('openregister_registers'); + + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + $needle = (string) $schemaId; + $registerIds = []; + + foreach ($rows as $row) { + $schemas = json_decode(($row['schemas'] ?? '[]'), true); + if (is_array($schemas) === false) { + continue; + } + + foreach ($schemas as $candidate) { + if ((string) $candidate === $needle) { + $registerIds[] = (int) $row['id']; + break; + } + } + } + + return $registerIds; + }//end getRegisterIdsWithSchema() + /** * Get the number of registers associated with each schema * @@ -1837,6 +2277,18 @@ private function resolveAllOf(Schema $schema, array $allOf, array $visited): Sch continue; } + // Skip inline constraint schemas. JSON Schema allows an allOf entry to be an + // inline schema object ({ "required": [...] }, { "properties": {...} }), not + // only a $ref to another schema. OpenRegister's composition model treats + // allOf/oneOf/anyOf as a list of schema IDENTIFIERS, so only string/int + // entries are references to load — an array entry is a constraint that stands + // on its own and must not be handed to loadSchema() (which would fatal on the + // array). This unblocked openconnector's register import, silently broken by + // lti_deployment's `oneOf: [{required, not}, ...]` XOR constraint. + if (is_string($parentRef) === false && is_int($parentRef) === false) { + continue; + } + // Check for self-reference. if ($parentRef === $currentId || $parentRef === $schema->getId() || $parentRef === $schema->getUuid() || $parentRef === $schema->getSlug() @@ -2005,6 +2457,13 @@ private function resolveOneOf(Schema $schema, array $oneOf, array $visited): Sch $currentId = $schema->getId() ?? $schema->getUuid() ?? 'unknown'; foreach ($oneOf as $ref) { + // Skip inline constraint schemas — a oneOf entry may be an inline schema object + // ({ "required": [...], "not": {...} }) rather than a $ref to another schema. + // Only string/int entries are schema identifiers to load. See resolveAllOf. + if (is_string($ref) === false && is_int($ref) === false) { + continue; + } + if ($ref === $currentId || $ref === $schema->getId() || $ref === $schema->getUuid() || $ref === $schema->getSlug() ) { @@ -2017,7 +2476,7 @@ private function resolveOneOf(Schema $schema, array $oneOf, array $visited): Sch schema: $referencedSchema, visited: $visited ); - } + }//end foreach // Return schema as-is (oneOf schemas are not merged). return $schema; @@ -2045,6 +2504,13 @@ private function resolveAnyOf(Schema $schema, array $anyOf, array $visited): Sch $currentId = $schema->getId() ?? $schema->getUuid() ?? 'unknown'; foreach ($anyOf as $ref) { + // Skip inline constraint schemas — an anyOf entry may be an inline schema + // object rather than a $ref. Only string/int entries are identifiers. See + // resolveAllOf. + if (is_string($ref) === false && is_int($ref) === false) { + continue; + } + if ($ref === $currentId || $ref === $schema->getId() || $ref === $schema->getUuid() || $ref === $schema->getSlug() ) { @@ -2057,7 +2523,7 @@ private function resolveAnyOf(Schema $schema, array $anyOf, array $visited): Sch schema: $referencedSchema, visited: $visited ); - } + }//end foreach // Return schema as-is (anyOf schemas are not merged). return $schema; @@ -3195,4 +3661,75 @@ public function findAllExtendedBy(): array return $extendedByMap; }//end findAllExtendedBy() + + /** + * Return the IDs of all schemas whose `searchable` flag is false. + * + * Used by the unified search provider to exclude opted-out schemas from + * Nextcloud unified search inside the query (rather than post-filtering a + * result page, which would leak counts and break pagination). The lookup + * is intentionally RBAC/tenant-agnostic: it only answers "which schemas + * declared themselves non-searchable", the access filtering happens in the + * object search itself. + * + * @return int[] List of schema IDs with `searchable = false`. + * + * @psalm-return list + * @phpstan-return array + * + * @spec openspec/specs/unified-search-provider/spec.md + */ + public function findNonSearchableIds(): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('id') + ->from('openregister_schemas') + ->where($qb->expr()->eq('searchable', $qb->createNamedParameter(value: false, type: IQueryBuilder::PARAM_BOOL))); + + $ids = []; + $result = $qb->executeQuery(); + while (($row = $result->fetch()) !== false) { + if (isset($row['id']) === true) { + $ids[] = (int) $row['id']; + } + } + + $result->closeCursor(); + + return $ids; + }//end findNonSearchableIds() + + /** + * Return the IDs of all schemas whose `searchable` flag is true (default). + * + * The unified search provider passes this allow-list as the `@self.schema` + * IN-filter so that only searchable schemas contribute results, applied + * inside the query. + * + * @return int[] List of schema IDs with `searchable = true`. + * + * @psalm-return list + * @phpstan-return array + * + * @spec openspec/specs/unified-search-provider/spec.md + */ + public function findSearchableIds(): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('id') + ->from('openregister_schemas') + ->where($qb->expr()->eq('searchable', $qb->createNamedParameter(value: true, type: IQueryBuilder::PARAM_BOOL))); + + $ids = []; + $result = $qb->executeQuery(); + while (($row = $result->fetch()) !== false) { + if (isset($row['id']) === true) { + $ids[] = (int) $row['id']; + } + } + + $result->closeCursor(); + + return $ids; + }//end findSearchableIds() }//end class diff --git a/lib/Db/SchemaRun.php b/lib/Db/SchemaRun.php new file mode 100644 index 0000000000..c48fc6004a --- /dev/null +++ b/lib/Db/SchemaRun.php @@ -0,0 +1,335 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class SchemaRun + * + * @method string|null getUuid() + * @method void setUuid(?string $uuid) + * @method int getSchemaId() + * @method void setSchemaId(int $schemaId) + * @method int|null getRegisterId() + * @method void setRegisterId(?int $registerId) + * @method string|null getType() + * @method void setType(?string $type) + * @method string|null getState() + * @method void setState(?string $state) + * @method array|null getProposedDefinition() + * @method void setProposedDefinition(?array $proposedDefinition) + * @method array|null getPlan() + * @method void setPlan(?array $plan) + * @method array|null getOptions() + * @method void setOptions(?array $options) + * @method int getProcessed() + * @method void setProcessed(int $processed) + * @method int getTotal() + * @method void setTotal(int $total) + * @method int getCursor() + * @method void setCursor(int $cursor) + * @method array|null getReport() + * @method void setReport(?array $report) + * @method string|null getStartedBy() + * @method void setStartedBy(?string $startedBy) + * @method int|null getRolledBackFrom() + * @method void setRolledBackFrom(?int $rolledBackFrom) + * @method DateTime|null getCreated() + * @method void setCreated(?DateTime $created) + * @method DateTime|null getUpdated() + * @method void setUpdated(?DateTime $updated) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class SchemaRun extends Entity implements JsonSerializable +{ + + /** + * Run type constants. + * + * @var string + */ + public const TYPE_REVALIDATION = 'revalidation'; + public const TYPE_MIGRATION = 'migration'; + public const TYPE_ROLLBACK = 'rollback'; + + /** + * Run state constants. + * + * @var string + */ + public const STATE_DRAFT = 'draft'; + public const STATE_PREVIEWED = 'previewed'; + public const STATE_RUNNING = 'running'; + public const STATE_COMPLETED = 'completed'; + public const STATE_FAILED = 'failed'; + public const STATE_ROLLED_BACK = 'rolled-back'; + + /** + * The states that count as "active" (block a second concurrent run). + * + * @var array + */ + public const ACTIVE_STATES = [ + self::STATE_DRAFT, + self::STATE_PREVIEWED, + self::STATE_RUNNING, + ]; + + /** + * Stable UUID. + * + * @var string|null + */ + protected ?string $uuid = null; + + /** + * The schema this run targets. + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * The register the schema's objects live in (magic-mapper routing). + * + * @var integer|null + */ + protected ?int $registerId = null; + + /** + * The run type (revalidation | migration | rollback). + * + * @var string|null + */ + protected ?string $type = null; + + /** + * The run state. + * + * @var string|null + */ + protected ?string $state = null; + + /** + * A proposed definition to validate against (dry-run of an edit). + * + * @var array|null + */ + protected ?array $proposedDefinition = null; + + /** + * The migration transform plan. + * + * @var array>|null + */ + protected ?array $plan = null; + + /** + * Run options (e.g. stopOnError, batchSize). + * + * @var array|null + */ + protected ?array $options = null; + + /** + * Number of processed objects. + * + * @var integer + */ + protected int $processed = 0; + + /** + * Total number of objects in scope. + * + * @var integer + */ + protected int $total = 0; + + /** + * Resumable cursor (last processed object id). + * + * @var integer + */ + protected int $cursor = 0; + + /** + * Summary report (counts; per-object entries live in the side table). + * + * @var array|null + */ + protected ?array $report = null; + + /** + * The user who started the run. + * + * @var string|null + */ + protected ?string $startedBy = null; + + /** + * For a rollback run, the migration run id it rolls back. + * + * @var integer|null + */ + protected ?int $rolledBackFrom = null; + + /** + * Creation timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $created = null; + + /** + * Last-update timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $updated = null; + + /** + * Constructor — registers field types for hydration. + */ + public function __construct() + { + $this->addType(fieldName: 'uuid', type: 'string'); + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'registerId', type: 'integer'); + $this->addType(fieldName: 'type', type: 'string'); + $this->addType(fieldName: 'state', type: 'string'); + $this->addType(fieldName: 'proposedDefinition', type: 'json'); + $this->addType(fieldName: 'plan', type: 'json'); + $this->addType(fieldName: 'options', type: 'json'); + $this->addType(fieldName: 'processed', type: 'integer'); + $this->addType(fieldName: 'total', type: 'integer'); + $this->addType(fieldName: 'cursor', type: 'integer'); + $this->addType(fieldName: 'report', type: 'json'); + $this->addType(fieldName: 'startedBy', type: 'string'); + $this->addType(fieldName: 'rolledBackFrom', type: 'integer'); + $this->addType(fieldName: 'created', type: 'datetime'); + $this->addType(fieldName: 'updated', type: 'datetime'); + + }//end __construct() + + /** + * The field names registered with the 'json' type. + * + * @return array The json-typed field names. + */ + public function getJsonFields(): array + { + return array_keys( + array_filter( + $this->getFieldTypes(), + static function ($field) { + return $field === 'json'; + } + ) + ); + }//end getJsonFields() + + /** + * Hydrate the entity from an array. + * + * Without this, SchemaRunMapper's `$entity->hydrate()` call hit Entity::__call + * and threw "hydrate does not exist", so every migration-run write failed. + * + * @param array $object The source data. + * + * @return static This entity, hydrated. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function hydrate(array $object): static + { + $jsonFields = $this->getJsonFields(); + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields, true) === true && $value === []) { + $value = null; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Silently ignore invalid properties. + } + } + + return $this; + }//end hydrate() + + /** + * Whether the run is in an active (blocking) state. + * + * @return bool True when active. + */ + public function isActive(): bool + { + return in_array($this->state, self::ACTIVE_STATES, true); + + }//end isActive() + + /** + * JSON serialisation. + * + * @return array The serialised run. + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'schemaId' => $this->schemaId, + 'registerId' => $this->registerId, + 'type' => $this->type, + 'state' => $this->state, + 'proposedDefinition' => $this->proposedDefinition, + 'plan' => ($this->plan ?? []), + 'options' => ($this->options ?? []), + 'processed' => $this->processed, + 'total' => $this->total, + 'cursor' => $this->cursor, + 'report' => ($this->report ?? []), + 'startedBy' => $this->startedBy, + 'rolledBackFrom' => $this->rolledBackFrom, + 'created' => $this->created?->format(DateTime::ATOM), + 'updated' => $this->updated?->format(DateTime::ATOM), + ]; + + }//end jsonSerialize() +}//end class diff --git a/lib/Db/SchemaRunEntry.php b/lib/Db/SchemaRunEntry.php new file mode 100644 index 0000000000..f78a5f045a --- /dev/null +++ b/lib/Db/SchemaRunEntry.php @@ -0,0 +1,202 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class SchemaRunEntry + * + * @method int getRunId() + * @method void setRunId(int $runId) + * @method string|null getObjectUuid() + * @method void setObjectUuid(?string $objectUuid) + * @method string|null getOutcome() + * @method void setOutcome(?string $outcome) + * @method string|null getMessage() + * @method void setMessage(?string $message) + * @method string|null getPreVersion() + * @method void setPreVersion(?string $preVersion) + * @method string|null getPostVersion() + * @method void setPostVersion(?string $postVersion) + * @method array|null getPreData() + * @method void setPreData(?array $preData) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class SchemaRunEntry extends Entity implements JsonSerializable +{ + + /** + * Outcome constants. + * + * @var string + */ + public const OUTCOME_VALID = 'valid'; + public const OUTCOME_INVALID = 'invalid'; + public const OUTCOME_MIGRATED = 'migrated'; + public const OUTCOME_UNCHANGED = 'unchanged'; + public const OUTCOME_FAILED = 'failed'; + public const OUTCOME_RESTORED = 'restored'; + public const OUTCOME_CONFLICT = 'conflict'; + + /** + * The run this entry belongs to. + * + * @var integer|null + */ + protected ?int $runId = null; + + /** + * The object's UUID. + * + * @var string|null + */ + protected ?string $objectUuid = null; + + /** + * The outcome for this object. + * + * @var string|null + */ + protected ?string $outcome = null; + + /** + * A human-readable message (validation errors, cast failure reason). + * + * @var string|null + */ + protected ?string $message = null; + + /** + * Pre-migration content version id (for rollback). + * + * @var string|null + */ + protected ?string $preVersion = null; + + /** + * Post-migration content version id (rollback conflict detection). + * + * @var string|null + */ + protected ?string $postVersion = null; + + /** + * Pre-migration object data snapshot (restored forward on rollback). + * + * @var array|null + */ + protected ?array $preData = null; + + /** + * Constructor — registers field types for hydration. + */ + public function __construct() + { + $this->addType(fieldName: 'runId', type: 'integer'); + $this->addType(fieldName: 'objectUuid', type: 'string'); + $this->addType(fieldName: 'outcome', type: 'string'); + $this->addType(fieldName: 'message', type: 'string'); + $this->addType(fieldName: 'preVersion', type: 'string'); + $this->addType(fieldName: 'postVersion', type: 'string'); + $this->addType(fieldName: 'preData', type: 'json'); + + }//end __construct() + + /** + * The field names registered with the 'json' type. + * + * @return array The json-typed field names. + */ + public function getJsonFields(): array + { + return array_keys( + array_filter( + $this->getFieldTypes(), + static function ($field) { + return $field === 'json'; + } + ) + ); + }//end getJsonFields() + + /** + * Hydrate the entity from an array. + * + * Without this, SchemaRunEntryMapper's `$entity->hydrate()` call hit + * Entity::__call and threw "hydrate does not exist", so every per-object + * migration entry write failed. + * + * @param array $object The source data. + * + * @return static This entity, hydrated. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function hydrate(array $object): static + { + $jsonFields = $this->getJsonFields(); + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields, true) === true && $value === []) { + $value = null; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Silently ignore invalid properties. + } + } + + return $this; + }//end hydrate() + + /** + * JSON serialisation. + * + * @return array The serialised entry. + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'runId' => $this->runId, + 'objectUuid' => $this->objectUuid, + 'outcome' => $this->outcome, + 'message' => $this->message, + 'preVersion' => $this->preVersion, + 'postVersion' => $this->postVersion, + 'preData' => $this->preData, + ]; + + }//end jsonSerialize() +}//end class diff --git a/lib/Db/SchemaRunEntryMapper.php b/lib/Db/SchemaRunEntryMapper.php new file mode 100644 index 0000000000..33ad795876 --- /dev/null +++ b/lib/Db/SchemaRunEntryMapper.php @@ -0,0 +1,113 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class SchemaRunEntryMapper + * + * @template-extends QBMapper + */ +class SchemaRunEntryMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_schema_run_entries', entityClass: SchemaRunEntry::class); + + }//end __construct() + + /** + * Find entries for a run, optionally filtered by outcome. + * + * @param int $runId The run id. + * @param string|null $outcome Optional outcome filter. + * @param int|null $limit Optional page size. + * @param int|null $offset Optional page offset. + * + * @return SchemaRunEntry[] The entries. + */ + public function findByRun(int $runId, ?string $outcome=null, ?int $limit=null, ?int $offset=null): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('run_id', $qb->createNamedParameter($runId, IQueryBuilder::PARAM_INT))) + ->orderBy('id', 'ASC'); + + if ($outcome !== null) { + $qb->andWhere($qb->expr()->eq('outcome', $qb->createNamedParameter($outcome))); + } + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities(query: $qb); + + }//end findByRun() + + /** + * Create an entry from an array of values. + * + * @param array $data The entry values. + * + * @return SchemaRunEntry The persisted entry. + */ + public function createFromArray(array $data): SchemaRunEntry + { + $entry = new SchemaRunEntry(); + $entry->hydrate($data); + + return $this->insert(entity: $entry); + + }//end createFromArray() + + /** + * Delete all entries for a run. + * + * @param int $runId The run id. + * + * @return int Number of deleted rows. + */ + public function deleteByRun(int $runId): int + { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('run_id', $qb->createNamedParameter($runId, IQueryBuilder::PARAM_INT))); + + return $qb->executeStatement(); + + }//end deleteByRun() +}//end class diff --git a/lib/Db/SchemaRunMapper.php b/lib/Db/SchemaRunMapper.php new file mode 100644 index 0000000000..675181fad9 --- /dev/null +++ b/lib/Db/SchemaRunMapper.php @@ -0,0 +1,164 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use DateTime; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use Symfony\Component\Uid\Uuid; + +/** + * Class SchemaRunMapper + * + * @template-extends QBMapper + */ +class SchemaRunMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_schema_runs', entityClass: SchemaRun::class); + + }//end __construct() + + /** + * Find a run by id. + * + * @param int $id The run id. + * + * @return SchemaRun The run. + * + * @throws DoesNotExistException When not found. + */ + public function find(int $id): SchemaRun + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity(query: $qb); + + }//end find() + + /** + * Find runs for a schema, newest first. + * + * @param int $schemaId The schema id. + * @param int|null $limit Optional page size. + * @param int|null $offset Optional page offset. + * + * @return SchemaRun[] The runs. + */ + public function findBySchema(int $schemaId, ?int $limit=null, ?int $offset=null): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('schema_id', $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT))) + ->orderBy('id', 'DESC'); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities(query: $qb); + + }//end findBySchema() + + /** + * Find the active (blocking) run for a schema, if any. + * + * @param int $schemaId The schema id. + * + * @return SchemaRun|null The active run, or null. + */ + public function findActiveForSchema(int $schemaId): ?SchemaRun + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('schema_id', $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->in('state', $qb->createNamedParameter(SchemaRun::ACTIVE_STATES, IQueryBuilder::PARAM_STR_ARRAY))) + ->setMaxResults(1); + + try { + return $this->findEntity(query: $qb); + } catch (DoesNotExistException $e) { + return null; + } + + }//end findActiveForSchema() + + /** + * Create a run from an array of values, assigning a UUID and timestamps. + * + * @param array $data The run values. + * + * @return SchemaRun The persisted run. + */ + public function createFromArray(array $data): SchemaRun + { + $run = new SchemaRun(); + $run->hydrate($data); + + if ($run->getUuid() === null) { + $run->setUuid(Uuid::v4()->toRfc4122()); + } + + $now = new DateTime(); + if ($run->getCreated() === null) { + $run->setCreated($now); + } + + $run->setUpdated($now); + + return $this->insert(entity: $run); + + }//end createFromArray() + + /** + * Persist run progress/state changes, refreshing the updated timestamp. + * + * @param SchemaRun $run The run to persist. + * + * @return SchemaRun The updated run. + */ + public function save(SchemaRun $run): SchemaRun + { + $run->setUpdated(new DateTime()); + + return $this->update(entity: $run); + + }//end save() +}//end class diff --git a/lib/Db/Sequence.php b/lib/Db/Sequence.php new file mode 100644 index 0000000000..ae1eb7a68a --- /dev/null +++ b/lib/Db/Sequence.php @@ -0,0 +1,106 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * Class Sequence + * + * @method int|null getRegisterId() + * @method void setRegisterId(?int $registerId) + * @method int|null getSchemaId() + * @method void setSchemaId(?int $schemaId) + * @method string|null getScopeKey() + * @method void setScopeKey(?string $scopeKey) + * @method int|null getNextValue() + * @method void setNextValue(?int $nextValue) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class Sequence extends Entity implements JsonSerializable +{ + + /** + * The register the sequence is scoped to. + * + * @var integer|null + */ + protected ?int $registerId = null; + + /** + * The schema the sequence is scoped to. + * + * @var integer|null + */ + protected ?int $schemaId = null; + + /** + * The scope discriminator (e.g. the year "2026", "2026-06" or "" for global). + * + * @var string|null + */ + protected ?string $scopeKey = null; + + /** + * The next value to hand out (always >= 1). + * + * @var integer|null + */ + protected ?int $nextValue = null; + + /** + * Constructor. + */ + public function __construct() + { + $this->addType(fieldName: 'registerId', type: 'integer'); + $this->addType(fieldName: 'schemaId', type: 'integer'); + $this->addType(fieldName: 'scopeKey', type: 'string'); + $this->addType(fieldName: 'nextValue', type: 'integer'); + }//end __construct() + + /** + * JSON serialization. + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'registerId' => $this->registerId, + 'schemaId' => $this->schemaId, + 'scopeKey' => $this->scopeKey, + 'nextValue' => $this->nextValue, + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/SequenceMapper.php b/lib/Db/SequenceMapper.php new file mode 100644 index 0000000000..0ae3c19f1d --- /dev/null +++ b/lib/Db/SequenceMapper.php @@ -0,0 +1,98 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class SequenceMapper + * + * @template-extends QBMapper + */ +class SequenceMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection. + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_sequences', entityClass: Sequence::class); + }//end __construct() + + /** + * Find the sequence row for a scope, or null when absent. + * + * @param int $registerId The register the sequence is scoped to. + * @param int $schemaId The schema the sequence is scoped to. + * @param string $scopeKey The scope discriminator. + * + * @return Sequence|null The row, or null when absent. + */ + public function findForScope(int $registerId, int $schemaId, string $scopeKey): ?Sequence + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('register_id', $qb->createNamedParameter($registerId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('schema_id', $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('scope_key', $qb->createNamedParameter($scopeKey))); + + try { + return $this->findEntity(query: $qb); + } catch (DoesNotExistException) { + return null; + } + }//end findForScope() + + /** + * Atomically increment the scope row's `next_value` by one. + * + * Issues a single `UPDATE … SET next_value = next_value + 1 WHERE …`. The + * UPDATE acquires a row lock that both Postgres and MySQL/InnoDB hold for + * the duration of the surrounding transaction, so a concurrent reservation + * on the same scope blocks here until this transaction commits — which is + * what serialises the hand-out and prevents two callers ever reading the + * same post-increment value. + * + * @param int $registerId The register the sequence is scoped to. + * @param int $schemaId The schema the sequence is scoped to. + * @param string $scopeKey The scope discriminator. + * + * @return int The number of affected rows (1 when the scope row exists, 0 otherwise). + */ + public function incrementScope(int $registerId, int $schemaId, string $scopeKey): int + { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->getTableName()) + ->set('next_value', $qb->createFunction('next_value + 1')) + ->where($qb->expr()->eq('register_id', $qb->createNamedParameter($registerId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('schema_id', $qb->createNamedParameter($schemaId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('scope_key', $qb->createNamedParameter($scopeKey))); + + return $qb->executeStatement(); + }//end incrementScope() +}//end class diff --git a/lib/Db/Source.php b/lib/Db/Source.php index 68d3ebb0c4..6700b696aa 100644 --- a/lib/Db/Source.php +++ b/lib/Db/Source.php @@ -52,6 +52,34 @@ * @method void setCreated(?DateTime $created) * @method array|null getConfiguration() * @method void setConfiguration(?array $configuration) + * @method bool|null getSyncEnabled() + * @method void setSyncEnabled(?bool $syncEnabled) + * @method string|null getSyncSchedule() + * @method void setSyncSchedule(?string $syncSchedule) + * @method int|null getSyncInterval() + * @method void setSyncInterval(?int $syncInterval) + * @method DateTime|null getLastSyncDate() + * @method void setLastSyncDate(?DateTime $lastSyncDate) + * @method string|null getLastSyncStatus() + * @method void setLastSyncStatus(?string $lastSyncStatus) + * @method string|null getLastSyncToken() + * @method void setLastSyncToken(?string $lastSyncToken) + * @method string|null getAuthType() + * @method void setAuthType(?string $authType) + * @method array|null getAuthConfig() + * @method void setAuthConfig(?array $authConfig) + * @method int|null getMappingId() + * @method void setMappingId(?int $mappingId) + * @method string|null getTargetRegister() + * @method void setTargetRegister(?string $targetRegister) + * @method string|null getTargetSchema() + * @method void setTargetSchema(?string $targetSchema) + * @method string|null getConflictStrategy() + * @method void setConflictStrategy(?string $conflictStrategy) + * @method string|null getDeleteStrategy() + * @method void setDeleteStrategy(?string $deleteStrategy) + * @method int|null getBatchSize() + * @method void setBatchSize(?int $batchSize) * * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class */ @@ -114,6 +142,104 @@ class Source extends Entity implements JsonSerializable */ private ?Configuration $managedByConfig = null; + /** + * Whether scheduled/harvest sync is enabled for this source + * + * @var boolean|null Sync enabled flag + */ + protected ?bool $syncEnabled = null; + + /** + * Cron expression describing the sync schedule (informational; interval drives the timed job) + * + * @var string|null Cron schedule expression + */ + protected ?string $syncSchedule = null; + + /** + * Sync interval in hours used by the timed job to decide whether a source is due + * + * @var integer|null Sync interval in hours + */ + protected ?int $syncInterval = null; + + /** + * Timestamp of the last successful sync checkpoint + * + * @var DateTime|null Last sync date + */ + protected ?DateTime $lastSyncDate = null; + + /** + * Status of the last sync execution (success|partial|failed|running) + * + * @var string|null Last sync status + */ + protected ?string $lastSyncStatus = null; + + /** + * Incremental sync checkpoint token (e.g. OData delta token or cursor) + * + * @var string|null Last sync token + */ + protected ?string $lastSyncToken = null; + + /** + * Authentication type for the external source (none|apikey|basic|oauth2|certificate) + * + * @var string|null Authentication type + */ + protected ?string $authType = null; + + /** + * Encrypted authentication configuration (credentials stored at rest) + * + * @var array|null Authentication config + */ + protected ?array $authConfig = null; + + /** + * Reference to the Mapping entity used to transform source records + * + * @var integer|null Mapping entity id + */ + protected ?int $mappingId = null; + + /** + * Target register slug/id that synced objects are imported into + * + * @var string|null Target register + */ + protected ?string $targetRegister = null; + + /** + * Target schema slug/id that synced objects are validated against + * + * @var string|null Target schema + */ + protected ?string $targetSchema = null; + + /** + * Conflict resolution strategy (source-wins|local-wins|newest-wins|manual) + * + * @var string|null Conflict strategy + */ + protected ?string $conflictStrategy = null; + + /** + * Delete handling strategy (soft-delete|hard-delete|ignore) + * + * @var string|null Delete strategy + */ + protected ?string $deleteStrategy = null; + + /** + * Batch size used by the harvest pipeline when processing records + * + * @var integer|null Batch size + */ + protected ?int $batchSize = null; + /** * Last update timestamp * @@ -142,6 +268,20 @@ public function __construct() $this->addType(fieldName: 'databaseUrl', type: 'string'); $this->addType(fieldName: 'type', type: 'string'); $this->addType(fieldName: 'organisation', type: 'string'); + $this->addType(fieldName: 'syncEnabled', type: 'boolean'); + $this->addType(fieldName: 'syncSchedule', type: 'string'); + $this->addType(fieldName: 'syncInterval', type: 'integer'); + $this->addType(fieldName: 'lastSyncDate', type: 'datetime'); + $this->addType(fieldName: 'lastSyncStatus', type: 'string'); + $this->addType(fieldName: 'lastSyncToken', type: 'string'); + $this->addType(fieldName: 'authType', type: 'string'); + $this->addType(fieldName: 'authConfig', type: 'json'); + $this->addType(fieldName: 'mappingId', type: 'integer'); + $this->addType(fieldName: 'targetRegister', type: 'string'); + $this->addType(fieldName: 'targetSchema', type: 'string'); + $this->addType(fieldName: 'conflictStrategy', type: 'string'); + $this->addType(fieldName: 'deleteStrategy', type: 'string'); + $this->addType(fieldName: 'batchSize', type: 'integer'); $this->addType(fieldName: 'updated', type: 'datetime'); $this->addType(fieldName: 'created', type: 'datetime'); }//end __construct() @@ -229,15 +369,7 @@ public function setOrganisation(?string $organisation): void * * Prepares the entity data for JSON serialization * - * @return ((int|null|string)[]|int|null|string)[] - * - * @psalm-return array{id: int, uuid: null|string, title: null|string, - * version: null|string, description: null|string, - * databaseUrl: null|string, type: null|string, - * organisation: null|string, updated: null|string, - * created: null|string, - * managedByConfiguration: array{id: int, uuid: null|string, - * title: null|string}|null} + * @return array The serialized source data */ public function jsonSerialize(): array { @@ -251,6 +383,11 @@ public function jsonSerialize(): array $created = $this->created->format('c'); } + $lastSyncDate = null; + if ($this->lastSyncDate !== null) { + $lastSyncDate = $this->lastSyncDate->format('c'); + } + return [ 'id' => $this->id, 'uuid' => $this->uuid, @@ -260,6 +397,21 @@ public function jsonSerialize(): array 'databaseUrl' => $this->databaseUrl, 'type' => $this->type, 'organisation' => $this->organisation, + 'syncEnabled' => $this->syncEnabled, + 'syncSchedule' => $this->syncSchedule, + 'syncInterval' => $this->syncInterval, + 'lastSyncDate' => $lastSyncDate, + 'lastSyncStatus' => $this->lastSyncStatus, + 'lastSyncToken' => $this->lastSyncToken, + 'authType' => $this->authType, + // Never expose the encrypted credential blob; only signal whether one is configured. + 'authConfigured' => ($this->authConfig !== null && $this->authConfig !== []), + 'mappingId' => $this->mappingId, + 'targetRegister' => $this->targetRegister, + 'targetSchema' => $this->targetSchema, + 'conflictStrategy' => $this->conflictStrategy, + 'deleteStrategy' => $this->deleteStrategy, + 'batchSize' => $this->batchSize, 'updated' => $updated, 'created' => $created, 'managedByConfiguration' => $this->getManagedByConfigurationData(), diff --git a/lib/Db/SourceMapper.php b/lib/Db/SourceMapper.php index d9e71ff231..d96252d354 100644 --- a/lib/Db/SourceMapper.php +++ b/lib/Db/SourceMapper.php @@ -51,7 +51,6 @@ * @method Source delete(Entity $entity) * @method Source find(int|string $id) * @method Source findEntity(IQueryBuilder $query) - * @method Source[] findAll(int|null $limit=null, int|null $offset=null) * @method list findEntities(IQueryBuilder $query) * * @template-extends QBMapper @@ -71,18 +70,18 @@ class SourceMapper extends QBMapper // Private OrganisationService $organisationService. /** - * User session for current user + * User session for current user (read by MultiTenancyTrait via isset guards) * * @var IUserSession */ - private IUserSession $userSession; + protected IUserSession $userSession; /** - * Group manager for RBAC + * Group manager for RBAC (read by MultiTenancyTrait via isset guards) * * @var IGroupManager */ - private IGroupManager $groupManager; + protected IGroupManager $groupManager; /** * Event dispatcher for dispatching source events @@ -171,7 +170,7 @@ public function find(int $id): Source * * @throws \Exception If user doesn't have read permission * - * @psalm-return list + * @psalm-return list<\OCA\OpenRegister\Db\Source> */ public function findAll( ?int $limit=null, @@ -217,6 +216,34 @@ public function findAll( return $this->findEntities(query: $qb); }//end findAll() + /** + * Find all sources that have scheduled sync enabled. + * + * Intended for the SyncDataJob background context which runs as the + * system actor across all organisations; therefore RBAC and the + * organisation filter are intentionally NOT applied here. Each + * returned source still carries its `organisation` field so the + * harvest pipeline can scope the objects it creates. + * + * @return Source[] Sources with sync_enabled = true + * + * @psalm-return list<\OCA\OpenRegister\Db\Source> + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function findBySyncEnabled(): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openregister_sources') + ->where( + $qb->expr()->eq('sync_enabled', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) + ); + + return $this->findEntities(query: $qb); + }//end findBySyncEnabled() + /** * Insert a new source * diff --git a/lib/Db/SyncRecord.php b/lib/Db/SyncRecord.php new file mode 100644 index 0000000000..b3e738caea --- /dev/null +++ b/lib/Db/SyncRecord.php @@ -0,0 +1,258 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Db; + +use DateTime; +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * SyncRecord entity class. + * + * @method string|null getUuid() + * @method void setUuid(?string $uuid) + * @method int|null getSourceId() + * @method void setSourceId(?int $sourceId) + * @method string|null getExecutionId() + * @method void setExecutionId(?string $executionId) + * @method string|null getExternalId() + * @method void setExternalId(?string $externalId) + * @method string|null getStatus() + * @method void setStatus(?string $status) + * @method string|null getObjectUuid() + * @method void setObjectUuid(?string $objectUuid) + * @method string|null getContentHash() + * @method void setContentHash(?string $contentHash) + * @method array|null getRawData() + * @method void setRawData(?array $rawData) + * @method string|null getErrorMessage() + * @method void setErrorMessage(?string $errorMessage) + * @method int|null getAttempts() + * @method void setAttempts(?int $attempts) + * @method string|null getOrganisation() + * @method void setOrganisation(?string $organisation) + * @method DateTime|null getUpdated() + * @method void setUpdated(?DateTime $updated) + * @method DateTime|null getCreated() + * @method void setCreated(?DateTime $created) + * + * @psalm-suppress PropertyNotSetInConstructor $id is set by Nextcloud's Entity base class + */ +class SyncRecord extends Entity implements JsonSerializable +{ + + /** + * Unique identifier for the sync record. + * + * @var string|null + */ + protected ?string $uuid = null; + + /** + * Owning source id. + * + * @var integer|null + */ + protected ?int $sourceId = null; + + /** + * Sync execution id this record belongs to (groups one run). + * + * @var string|null + */ + protected ?string $executionId = null; + + /** + * External identifier of the source record. + * + * @var string|null + */ + protected ?string $externalId = null; + + /** + * Pipeline status (see SyncRecordStatus). + * + * @var string|null + */ + protected ?string $status = null; + + /** + * UUID of the local object created/updated from this record. + * + * @var string|null + */ + protected ?string $objectUuid = null; + + /** + * Content hash used to detect changes between syncs. + * + * @var string|null + */ + protected ?string $contentHash = null; + + /** + * Raw fetched data for this record. + * + * @var array|null + */ + protected ?array $rawData = null; + + /** + * Error message when the record failed. + * + * @var string|null + */ + protected ?string $errorMessage = null; + + /** + * Number of processing attempts (for retry/backoff). + * + * @var integer|null + */ + protected ?int $attempts = null; + + /** + * Organisation UUID this record belongs to. + * + * @var string|null + */ + protected ?string $organisation = null; + + /** + * Last update timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $updated = null; + + /** + * Creation timestamp. + * + * @var DateTime|null + */ + protected ?DateTime $created = null; + + /** + * Constructor: register field types. + */ + public function __construct() + { + $this->addType(fieldName: 'uuid', type: 'string'); + $this->addType(fieldName: 'sourceId', type: 'integer'); + $this->addType(fieldName: 'executionId', type: 'string'); + $this->addType(fieldName: 'externalId', type: 'string'); + $this->addType(fieldName: 'status', type: 'string'); + $this->addType(fieldName: 'objectUuid', type: 'string'); + $this->addType(fieldName: 'contentHash', type: 'string'); + $this->addType(fieldName: 'rawData', type: 'json'); + $this->addType(fieldName: 'errorMessage', type: 'string'); + $this->addType(fieldName: 'attempts', type: 'integer'); + $this->addType(fieldName: 'organisation', type: 'string'); + $this->addType(fieldName: 'updated', type: 'datetime'); + $this->addType(fieldName: 'created', type: 'datetime'); + }//end __construct() + + /** + * Get JSON field names. + * + * @return string[] List of JSON field names + * + * @psalm-return list + */ + public function getJsonFields(): array + { + return array_keys( + array_filter( + $this->getFieldTypes(), + static function ($field) { + return $field === 'json'; + } + ) + ); + }//end getJsonFields() + + /** + * Hydrate the entity from an array. + * + * @param array $object The data array + * + * @return static + */ + public function hydrate(array $object): static + { + $jsonFields = $this->getJsonFields(); + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields, true) === true && $value === []) { + $value = null; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Silently ignore invalid properties. + } + } + + return $this; + }//end hydrate() + + /** + * Serialize to a JSON-friendly array. + * + * @return array + */ + public function jsonSerialize(): array + { + $updated = null; + if ($this->updated !== null) { + $updated = $this->updated->format('c'); + } + + $created = null; + if ($this->created !== null) { + $created = $this->created->format('c'); + } + + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'sourceId' => $this->sourceId, + 'executionId' => $this->executionId, + 'externalId' => $this->externalId, + 'status' => $this->status, + 'objectUuid' => $this->objectUuid, + 'contentHash' => $this->contentHash, + 'rawData' => $this->rawData, + 'errorMessage' => $this->errorMessage, + 'attempts' => $this->attempts, + 'organisation' => $this->organisation, + 'updated' => $updated, + 'created' => $created, + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Db/SyncRecordMapper.php b/lib/Db/SyncRecordMapper.php new file mode 100644 index 0000000000..24ffea25b0 --- /dev/null +++ b/lib/Db/SyncRecordMapper.php @@ -0,0 +1,193 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Db; + +use DateTime; +use OCA\OpenRegister\Service\Sync\SyncRecordStatus; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use Symfony\Component\Uid\Uuid; + +/** + * Mapper for SyncRecord entities. + * + * @method SyncRecord insert(Entity $entity) + * @method SyncRecord update(Entity $entity) + * @method SyncRecord find(int|string $id) + * @method SyncRecord findEntity(IQueryBuilder $query) + * @method list findEntities(IQueryBuilder $query) + * + * @template-extends QBMapper + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +class SyncRecordMapper extends QBMapper +{ + /** + * Constructor. + * + * @param IDBConnection $db Database connection + */ + public function __construct(IDBConnection $db) + { + parent::__construct(db: $db, tableName: 'openregister_sync_records', entityClass: SyncRecord::class); + }//end __construct() + + /** + * Create a pending tracking row for a gathered record. + * + * @param int $sourceId Owning source id + * @param string $executionId Sync execution id + * @param string $externalId External record identifier + * @param string|null $organisation Owning organisation UUID + * + * @return SyncRecord The persisted record + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function createPending( + int $sourceId, + string $executionId, + string $externalId, + ?string $organisation=null + ): SyncRecord { + $record = new SyncRecord(); + $record->setUuid((string) Uuid::v4()); + $record->setSourceId($sourceId); + $record->setExecutionId($executionId); + $record->setExternalId($externalId); + $record->setStatus(SyncRecordStatus::PENDING); + $record->setAttempts(0); + $record->setOrganisation($organisation); + $record->setCreated(new DateTime()); + $record->setUpdated(new DateTime()); + + return $this->insert(entity: $record); + }//end createPending() + + /** + * Transition a record to a new status, enforcing the state machine. + * + * @param SyncRecord $record The record to transition + * @param string $newStatus The target status + * @param string|null $errorMessage Optional error detail + * + * @return SyncRecord The updated record + * + * @throws \InvalidArgumentException When the transition is not allowed + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function transitionStatus(SyncRecord $record, string $newStatus, ?string $errorMessage=null): SyncRecord + { + $current = (string) $record->getStatus(); + if (SyncRecordStatus::canTransition(from: $current, to: $newStatus) === false) { + throw new \InvalidArgumentException( + sprintf('Illegal sync-record transition from "%s" to "%s".', $current, $newStatus) + ); + } + + $record->setStatus($newStatus); + if ($errorMessage !== null) { + $record->setErrorMessage($errorMessage); + } + + $record->setUpdated(new DateTime()); + + return $this->update(entity: $record); + }//end transitionStatus() + + /** + * Find all records for a given execution. + * + * @param string $executionId The execution id + * + * @return SyncRecord[] The records + * + * @psalm-return list<\OCA\OpenRegister\Db\SyncRecord> + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function findByExecution(string $executionId): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('execution_id', $qb->createNamedParameter($executionId))); + + return $this->findEntities(query: $qb); + }//end findByExecution() + + /** + * Find records for a source/execution still awaiting fetch (resume support). + * + * @param int $sourceId The source id + * @param string $executionId The execution id + * + * @return SyncRecord[] The pending records + * + * @psalm-return list<\OCA\OpenRegister\Db\SyncRecord> + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function findPending(int $sourceId, string $executionId): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('source_id', $qb->createNamedParameter($sourceId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('execution_id', $qb->createNamedParameter($executionId))) + ->andWhere($qb->expr()->eq('status', $qb->createNamedParameter(SyncRecordStatus::PENDING))); + + return $this->findEntities(query: $qb); + }//end findPending() + + /** + * Find a record by source + external id (latest by id). + * + * @param int $sourceId The source id + * @param string $externalId The external record id + * + * @return SyncRecord|null The most recent matching record, or null + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function findByExternalId(int $sourceId, string $externalId): ?SyncRecord + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('source_id', $qb->createNamedParameter($sourceId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('external_id', $qb->createNamedParameter($externalId))) + ->orderBy('id', 'DESC') + ->setMaxResults(1); + + $records = $this->findEntities(query: $qb); + + return ($records[0] ?? null); + }//end findByExternalId() +}//end class diff --git a/lib/Db/TimeTrackerLinkMapper.php b/lib/Db/TimeTrackerLinkMapper.php index 99a39c81db..2a8d28fc9c 100644 --- a/lib/Db/TimeTrackerLinkMapper.php +++ b/lib/Db/TimeTrackerLinkMapper.php @@ -184,6 +184,34 @@ public function deleteByObjectAndEntryId(string $objectUuid, string $entryId): i return $qb->executeStatement(); }//end deleteByObjectAndEntryId() + /** + * Return every link row, optionally scoped to an object uuid. + * + * Used by the `openregister:time:reconcile` command to walk every + * link and re-fetch upstream NC TimeManager metadata (duration, + * billable, started_at, name) so the denormalised fields match the + * authoritative source after schema/data drift. + * + * @param string|null $objectUuid Optional object uuid scope. Null returns every row. + * + * @return TimeTrackerLink[] + * + * @spec openspec/specs/integration-time-tracker/spec.md + */ + public function findAll(?string $objectUuid=null): array + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->orderBy('id', 'ASC'); + + if ($objectUuid !== null) { + $qb->where($qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid))); + } + + return $this->findEntities(query: $qb); + }//end findAll() + /** * Apply an id-component constraint to the query builder, using * `IS NULL` when the component is absent so the composite-unique diff --git a/lib/Db/Translation.php b/lib/Db/Translation.php index 0d9f65411a..dc7bef82ba 100644 --- a/lib/Db/Translation.php +++ b/lib/Db/Translation.php @@ -49,6 +49,8 @@ * @method void setTranslator(?string $translator) * @method DateTime|null getUpdated() * @method void setUpdated(?DateTime $updated) + * @method string|null getSourceLanguage() + * @method void setSourceLanguage(?string $sourceLanguage) */ class Translation extends Entity implements JsonSerializable { @@ -57,12 +59,27 @@ class Translation extends Entity implements JsonSerializable public const STATUS_MACHINE_TRANSLATED = 'machine_translated'; public const STATUS_HUMAN_REVIEWED = 'human_reviewed'; public const STATUS_APPROVED = 'approved'; + public const STATUS_OUTDATED = 'outdated'; public const ALL_STATUSES = [ self::STATUS_DRAFT, self::STATUS_MACHINE_TRANSLATED, self::STATUS_HUMAN_REVIEWED, self::STATUS_APPROVED, + self::STATUS_OUTDATED, + ]; + + /** + * Statuses considered "live" — i.e. eligible to be flipped to outdated + * when the source value changes. `draft` and `outdated` are excluded + * (already pre-source or already-flagged). + * + * @var string[] + */ + public const FLIPPABLE_STATUSES = [ + self::STATUS_APPROVED, + self::STATUS_HUMAN_REVIEWED, + self::STATUS_MACHINE_TRANSLATED, ]; /** @@ -114,6 +131,21 @@ class Translation extends Entity implements JsonSerializable */ protected ?DateTime $updated = null; + /** + * BCP-47 source language for the property at the time of projection. + * + * Tracks which language is the canonical "source" for the slot, so + * the freshness automation can flip every non-source-language row to + * `outdated` when the source value changes. Populated by the + * `TranslationProjectionService` from the schema-property modifier, + * an object-level override, or the register default. + * + * @var string|null + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-1 + */ + protected ?string $sourceLanguage = null; + /** * Configure typed columns for the entity. * @@ -128,6 +160,7 @@ public function __construct() $this->addType(fieldName: 'status', type: 'string'); $this->addType(fieldName: 'translator', type: 'string'); $this->addType(fieldName: 'updated', type: 'datetime'); + $this->addType(fieldName: 'sourceLanguage', type: 'string'); }//end __construct() /** @@ -138,14 +171,18 @@ public function __construct() public function jsonSerialize(): array { return [ - 'id' => $this->id, - 'objectUuid' => $this->objectUuid, - 'property' => $this->property, - 'language' => $this->language, - 'value' => $this->value, - 'status' => $this->status, - 'translator' => $this->translator, - 'updated' => $this->updated?->format(\DateTimeInterface::ATOM), + 'id' => $this->id, + 'objectUuid' => $this->objectUuid, + 'property' => $this->property, + 'language' => $this->language, + 'value' => $this->value, + 'status' => $this->status, + 'translator' => $this->translator, + 'updated' => $this->updated?->format(\DateTimeInterface::ATOM), + 'sourceLanguage' => $this->sourceLanguage, + 'isSource' => ($this->language !== null + && $this->sourceLanguage !== null + && $this->language === $this->sourceLanguage), ]; }//end jsonSerialize() }//end class diff --git a/lib/Db/TranslationMapper.php b/lib/Db/TranslationMapper.php index 0c78f29ffe..577ef6f541 100644 --- a/lib/Db/TranslationMapper.php +++ b/lib/Db/TranslationMapper.php @@ -57,12 +57,13 @@ public function __construct(IDBConnection $db) * an existing slot, status defaults to retaining the previous * status unless caller passes a non-null override. * - * @param string $objectUuid UUID of the object being translated. - * @param string $property Property path within the object. - * @param string $language BCP-47 language tag of the slot. - * @param string|null $value Translated value (null clears). - * @param string|null $status Optional workflow status override. - * @param string|null $translator Optional UID of the translator. + * @param string $objectUuid UUID of the object being translated. + * @param string $property Property path within the object. + * @param string $language BCP-47 language tag of the slot. + * @param string|null $value Translated value (null clears). + * @param string|null $status Optional workflow status override. + * @param string|null $translator Optional UID of the translator. + * @param string|null $sourceLanguage Optional canonical source language for the property. * * @return Translation Persisted translation row. */ @@ -72,7 +73,8 @@ public function upsert( string $language, ?string $value, ?string $status=null, - ?string $translator=null + ?string $translator=null, + ?string $sourceLanguage=null ): Translation { $existing = $this->findOne(objectUuid: $objectUuid, property: $property, language: $language); $entity = $existing ?? new Translation(); @@ -90,6 +92,10 @@ public function upsert( $entity->setTranslator($translator); } + if ($sourceLanguage !== null && $sourceLanguage !== '') { + $entity->setSourceLanguage($sourceLanguage); + } + $entity->setUpdated(new DateTime()); if ($existing === null) { @@ -190,29 +196,39 @@ public function getCompletenessByObject(string $objectUuid): array /** * Search translations by content + optional filters. * - * - `query` — case-insensitive substring against `value` - * - `language` — scope to a specific language (null = cross-language) - * - `status` — filter by workflow status - * - `objectUuid` — scope to one object + * - `query` — case-insensitive substring against `value` + * - `language` — scope to a specific language (null = cross-language) + * - `status` — filter by workflow status + * - `objectUuid` — scope to one object + * - `sourceLanguage` — scope to rows whose canonical source-of-truth + * language equals the given BCP-47 tag + * (`i18n-source-of-truth`) + * - `isOutOfDate` — when true, narrows to rows in `outdated` status * * Uses `LOWER(value) LIKE LOWER(?)` so the query works on both * Postgres and MariaDB without DB-specific FTS. tsvector * optimisation is a v1.1 follow-up. * - * @param string|null $query Case-insensitive substring against the value column. - * @param string|null $language Optional language filter (BCP-47 tag). - * @param string|null $status Optional workflow-status filter. - * @param string|null $objectUuid Optional object-uuid scope. - * @param int $limit Maximum number of rows to return (1..1000). + * @param string|null $query Case-insensitive substring against the value column. + * @param string|null $language Optional language filter (BCP-47 tag). + * @param string|null $status Optional workflow-status filter. + * @param string|null $objectUuid Optional object-uuid scope. + * @param int $limit Maximum number of rows to return (1..1000). + * @param string|null $sourceLanguage Optional source-language filter (BCP-47 tag). + * @param bool $isOutOfDate When true, narrows to status='outdated'. * * @return Translation[] + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-3 */ public function search( ?string $query=null, ?string $language=null, ?string $status=null, ?string $objectUuid=null, - int $limit=100 + int $limit=100, + ?string $sourceLanguage=null, + bool $isOutOfDate=false ): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') @@ -240,9 +256,191 @@ public function search( $qb->andWhere($qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid))); } + if ($sourceLanguage !== null && $sourceLanguage !== '') { + $qb->andWhere($qb->expr()->eq('source_language', $qb->createNamedParameter($sourceLanguage))); + } + + if ($isOutOfDate === true) { + $qb->andWhere($qb->expr()->eq('status', $qb->createNamedParameter(Translation::STATUS_OUTDATED))); + } + return $this->findEntities(query: $qb); }//end search() + /** + * Flip every non-source-language row for one (object, property) to outdated. + * + * Targets every row WHERE `object_uuid = $objectUuid AND property = $property + * AND language != $sourceLanguage AND status IN (approved, + * human_reviewed, machine_translated)`. Rows already in `outdated` or + * `draft` are not re-flipped. + * + * @param string $objectUuid UUID of the object whose translations to flip. + * @param string $property Property whose derived translations to flip. + * @param string $sourceLanguage Source language for the property (never flipped). + * + * @return int Number of rows transitioned to `outdated`. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-2 + */ + public function markDerivedOutdated(string $objectUuid, string $property, string $sourceLanguage): int + { + $qb = $this->db->getQueryBuilder(); + $qb->update('openregister_translations') + ->set('status', $qb->createNamedParameter(Translation::STATUS_OUTDATED)) + ->set('updated', $qb->createNamedParameter((new DateTime())->format('Y-m-d H:i:s'))) + ->where($qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid))) + ->andWhere($qb->expr()->eq('property', $qb->createNamedParameter($property))) + ->andWhere($qb->expr()->neq('language', $qb->createNamedParameter($sourceLanguage))) + ->andWhere( + $qb->expr()->in( + 'status', + $qb->createNamedParameter( + Translation::FLIPPABLE_STATUSES, + IQueryBuilder::PARAM_STR_ARRAY + ) + ) + ); + + return $qb->executeStatement(); + }//end markDerivedOutdated() + + /** + * Back-fill the source_language column on rows that have an empty value. + * + * Streams candidate rows in batches and updates each row's + * `source_language` to the resolved `` for the parent + * register. Used by the + * `openregister:translations:backfill-source-language` console command. + * + * @param array $registerDefaults Map of `register_id => default_language`. + * @param int $batchSize Maximum rows updated per pass. + * + * @return int Number of rows updated across all batches. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-1 + */ + public function backfillSourceLanguage(array $registerDefaults, int $batchSize=1000): int + { + $totalUpdated = 0; + + // Distinct register ids present in the registers map. + $registerIds = array_keys($registerDefaults); + if (count($registerIds) === 0) { + // Fallback: blanket-fill every empty row with 'nl' so the + // NOT NULL DEFAULT '' constraint is honoured. + $qb = $this->db->getQueryBuilder(); + $qb->update('openregister_translations') + ->set('source_language', $qb->createNamedParameter('nl')) + ->where($qb->expr()->eq('source_language', $qb->createNamedParameter(''))); + return $qb->executeStatement(); + } + + foreach ($registerDefaults as $registerId => $defaultLanguage) { + if ($defaultLanguage === '' || $defaultLanguage === null) { + $defaultLanguage = 'nl'; + } + + $qb = $this->db->getQueryBuilder(); + $qb->update('openregister_translations', 't') + ->set('source_language', $qb->createNamedParameter((string) $defaultLanguage)) + ->where($qb->expr()->eq('source_language', $qb->createNamedParameter(''))) + ->andWhere( + "object_uuid IN (SELECT uuid FROM `*PREFIX*openregister_objects` WHERE register = " + .$qb->createNamedParameter((string) $registerId).")" + ) + ->setMaxResults(max(1, $batchSize)); + + try { + $updated = $qb->executeStatement(); + $totalUpdated += $updated; + } catch (\Throwable $e) { + // Driver doesn't support setMaxResults on UPDATE — fall back to + // a single non-bounded UPDATE for this register. + $qb2 = $this->db->getQueryBuilder(); + $qb2->update('openregister_translations') + ->set('source_language', $qb2->createNamedParameter((string) $defaultLanguage)) + ->where($qb2->expr()->eq('source_language', $qb2->createNamedParameter(''))) + ->andWhere( + "object_uuid IN (SELECT uuid FROM `*PREFIX*openregister_objects` WHERE register = " + .$qb2->createNamedParameter((string) $registerId).")" + ); + $totalUpdated += (int) $qb2->executeStatement(); + } + }//end foreach + + // Catch-all: rows whose object UUID no longer joins back to a + // register (orphans). Use 'nl' as the conservative fallback. + $qb = $this->db->getQueryBuilder(); + $qb->update('openregister_translations') + ->set('source_language', $qb->createNamedParameter('nl')) + ->where($qb->expr()->eq('source_language', $qb->createNamedParameter(''))); + $totalUpdated += (int) $qb->executeStatement(); + + return $totalUpdated; + }//end backfillSourceLanguage() + + /** + * Count rows whose `source_language` is the empty back-fill default. + * + * Used by the back-fill command's idempotency check. + * + * @return int Count of rows still pending back-fill. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-1 + */ + public function countMissingSourceLanguage(): int + { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->createFunction('COUNT(*) AS pending')) + ->from('openregister_translations') + ->where($qb->expr()->eq('source_language', $qb->createNamedParameter(''))); + + $stmt = $qb->executeQuery(); + $row = $stmt->fetch(); + $stmt->closeCursor(); + return (int) ($row['pending'] ?? 0); + }//end countMissingSourceLanguage() + + /** + * Find the most common source_language across an object's translatable + * properties. Used to drive the `X-Source-Language` response header. + * + * Returns null when the object has no translation rows. + * + * @param string $objectUuid UUID of the object to inspect. + * + * @return string|null Dominant source language, or null when no rows exist. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-3 + */ + public function getDominantSourceLanguage(string $objectUuid): ?string + { + $qb = $this->db->getQueryBuilder(); + $qb->select('source_language') + ->selectAlias($qb->createFunction('COUNT(*)'), 'cnt') + ->from('openregister_translations') + ->where($qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid))) + ->andWhere($qb->expr()->neq('source_language', $qb->createNamedParameter(''))) + ->groupBy('source_language') + ->orderBy('cnt', 'DESC') + ->setMaxResults(1); + + $stmt = $qb->executeQuery(); + $row = $stmt->fetch(); + $stmt->closeCursor(); + if ($row === false || isset($row['source_language']) === false) { + return null; + } + + $value = (string) $row['source_language']; + if ($value === '') { + return null; + } + + return $value; + }//end getDominantSourceLanguage() + /** * Find object UUIDs missing a translation in the given language for * one or more properties. diff --git a/lib/Db/Verwerkingsactiviteit.php b/lib/Db/Verwerkingsactiviteit.php index 0290d0c52e..c8e78cd5dc 100644 --- a/lib/Db/Verwerkingsactiviteit.php +++ b/lib/Db/Verwerkingsactiviteit.php @@ -17,9 +17,9 @@ * @category Db * @package OCA\OpenRegister\Db * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Db/VerwerkingsactiviteitMapper.php b/lib/Db/VerwerkingsactiviteitMapper.php index 738c69f6ae..3b7b675c95 100644 --- a/lib/Db/VerwerkingsactiviteitMapper.php +++ b/lib/Db/VerwerkingsactiviteitMapper.php @@ -23,9 +23,9 @@ * @category Db * @package OCA\OpenRegister\Db * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Db/Webhook.php b/lib/Db/Webhook.php index ac251cff6e..38e0b36a31 100644 --- a/lib/Db/Webhook.php +++ b/lib/Db/Webhook.php @@ -4,9 +4,9 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html */ namespace OCA\OpenRegister\Db; diff --git a/lib/Db/WebhookLog.php b/lib/Db/WebhookLog.php index 3e018e7f3d..122cbd4436 100644 --- a/lib/Db/WebhookLog.php +++ b/lib/Db/WebhookLog.php @@ -207,12 +207,14 @@ public function getPayloadArray(): array */ public function setPayloadArray(?array $payload): void { + // phpcs:disable CustomSniffs.Functions.NamedParameters -- Entity __call breaks with named args. if ($payload === null) { - $this->setPayload(payload: null); + $this->setPayload(null); return; } - $this->setPayload(payload: json_encode($payload)); + $this->setPayload(json_encode($payload)); + // phpcs:enable CustomSniffs.Functions.NamedParameters }//end setPayloadArray() /** diff --git a/lib/Db/WebhookMapper.php b/lib/Db/WebhookMapper.php index 16c0adc64d..cceb6814bf 100644 --- a/lib/Db/WebhookMapper.php +++ b/lib/Db/WebhookMapper.php @@ -25,6 +25,7 @@ namespace OCA\OpenRegister\Db; use DateTime; +use OCA\OpenRegister\Service\Webhook\WebhookInterceptionCache; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\MultipleObjectsReturnedException; @@ -107,17 +108,28 @@ class WebhookMapper extends QBMapper */ private readonly IGroupManager $groupManager; + /** + * Interception-flag cache invalidated on every webhook CRUD operation + * + * Nullable so the mapper stays constructible without a cache backend + * (unit tests, degraded environments); invalidation is skipped then. + * + * @var WebhookInterceptionCache|null Interception cache instance + */ + private readonly ?WebhookInterceptionCache $interceptionCache; + /** * Constructor * * Initializes mapper with database connection and multi-tenancy/RBAC dependencies. * Calls parent constructor to set up base mapper functionality. * - * @param IDBConnection $db Database connection - * @param OrganisationMapper $organisationMapper Organisation mapper for multi-tenancy - * @param IUserSession $userSession User session - * @param IGroupManager $groupManager Group manager - * @param IAppConfig $appConfig App configuration for multitenancy settings + * @param IDBConnection $db Database connection + * @param OrganisationMapper $organisationMapper Organisation mapper for multi-tenancy + * @param IUserSession $userSession User session + * @param IGroupManager $groupManager Group manager + * @param IAppConfig $appConfig App configuration for multitenancy settings + * @param WebhookInterceptionCache|null $interceptionCache Interception-flag cache invalidated on webhook CRUD * * @return void */ @@ -126,7 +138,8 @@ public function __construct( OrganisationMapper $organisationMapper, IUserSession $userSession, IGroupManager $groupManager, - IAppConfig $appConfig + IAppConfig $appConfig, + ?WebhookInterceptionCache $interceptionCache=null ) { // Call parent constructor to initialize base mapper with table name and entity class. parent::__construct(db: $db, tableName: 'openregister_webhooks', entityClass: Webhook::class); @@ -136,6 +149,7 @@ public function __construct( $this->userSession = $userSession; $this->groupManager = $groupManager; $this->appConfig = $appConfig; + $this->interceptionCache = $interceptionCache; }//end __construct() /** @@ -265,6 +279,39 @@ public function findEnabled(): array return $this->findEntities(query: $qb); }//end findEnabled() + /** + * Find all enabled webhooks WITHOUT organisation filtering + * + * Tenant-agnostic variant of findEnabled() used exclusively to compute + * the global "has interception webhooks for event X" cache flag. The + * flag must consider ALL organisations: a per-tenant "no webhooks" + * answer cached globally would silently disable another tenant's + * interception hooks. Callers that deliver webhooks must still use the + * organisation-filtered findEnabled()/findForEvent() paths. + * + * @return Webhook[] + * + * @psalm-return list<\OCA\OpenRegister\Db\Webhook> + * + * @spec openspec/specs/webhook-payload-mapping/spec.md#request-interception-pre-event-webhooks + */ + public function findEnabledForInterceptionScan(): array + { + // Check if table exists before querying (migrations might not have run yet). + if ($this->tableExists() === false) { + return []; + } + + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('enabled', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))); + + // Deliberately NO organisation filter — see method docblock. + return $this->findEntities(query: $qb); + }//end findEnabledForInterceptionScan() + /** * Find webhooks that match an event * @@ -314,7 +361,12 @@ public function insert(Entity $entity): Entity // Auto-set organisation from active session. $this->setOrganisationOnCreate(entity: $entity); - return parent::insert(entity: $entity); + $inserted = parent::insert(entity: $entity); + + // A new webhook can introduce interception for any event type. + $this->interceptionCache?->invalidate(); + + return $inserted; }//end insert() /** @@ -337,7 +389,12 @@ public function update(Entity $entity): Entity $entity->setUpdated(new DateTime()); } - return parent::update(entity: $entity); + $updated = parent::update(entity: $entity); + + // Enabled/events/configuration may have changed interception applicability. + $this->interceptionCache?->invalidate(); + + return $updated; }//end update() /** @@ -356,7 +413,12 @@ public function delete(Entity $entity): Entity // Verify user has access to this organisation. $this->verifyOrganisationAccess(entity: $entity); - return parent::delete(entity: $entity); + $deleted = parent::delete(entity: $entity); + + // Removing a webhook can drop the last interception hook for an event type. + $this->interceptionCache?->invalidate(); + + return $deleted; }//end delete() /** diff --git a/lib/Dto/DeepLinkRegistration.php b/lib/Dto/DeepLinkRegistration.php index a4e1f8fbe2..982285bbb9 100644 --- a/lib/Dto/DeepLinkRegistration.php +++ b/lib/Dto/DeepLinkRegistration.php @@ -19,7 +19,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-18 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-deep-link-registry-shall-resolve-urls-for-unified-search-results */ declare(strict_types=1); @@ -37,13 +37,18 @@ class DeepLinkRegistration /** * Constructor for DeepLinkRegistration. * - * @param string $appId The consuming app ID (e.g., "procest") - * @param string $registerSlug The register slug - * @param string $schemaSlug The schema slug - * @param string $urlTemplate URL template with placeholders - * @param string $icon Optional icon identifier + * @param string $appId The consuming app ID (e.g., "procest") + * @param string $registerSlug The register slug + * @param string $schemaSlug The schema slug + * @param string $urlTemplate URL template with placeholders + * @param string $icon Optional icon identifier + * @param string|null $displayName Optional human-readable label for the + * owning app, used to label unified-search + * results (defaults to null → app id) * * @return void + * + * @spec openspec/specs/deep-link-registry/spec.md */ public function __construct( public readonly string $appId, @@ -51,6 +56,7 @@ public function __construct( public readonly string $schemaSlug, public readonly string $urlTemplate, public readonly string $icon='', + public readonly ?string $displayName=null, ) { }//end __construct() @@ -70,7 +76,7 @@ public function __construct( * * @return string The resolved URL * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-18 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-deep-link-registry-shall-resolve-urls-for-unified-search-results */ public function resolveUrl(array $objectData, array $contactContext=[]): string { diff --git a/lib/Dto/DeletionAnalysis.php b/lib/Dto/DeletionAnalysis.php index e1f15f27a1..c0b23e77df 100644 --- a/lib/Dto/DeletionAnalysis.php +++ b/lib/Dto/DeletionAnalysis.php @@ -1,14 +1,12 @@ + * @link https://conduction.nl * - * @link https://OpenRegister.app + * @spec openspec/changes/openregister-legacy-quality-cleanup/tasks.md#task-1.1 */ declare(strict_types=1); @@ -27,47 +25,106 @@ namespace OCA\OpenRegister\Dto; /** - * Value object representing the analysis of what would happen when an object is deleted. + * Value object representing the result of a deletion dependency-graph walk. * * @category Dto * @package OCA\OpenRegister\Dto */ class DeletionAnalysis { + + /** + * Whether the object may be deleted without violating RESTRICT constraints. + * + * @var boolean + */ + public readonly bool $deletable; + + /** + * Dependent objects that will be cascade-deleted alongside the root. + * + * @var array + */ + public readonly array $cascadeTargets; + + /** + * Dependent objects whose reference property will be set to null. + * + * @var array + */ + public readonly array $nullifyTargets; + /** - * Constructor for DeletionAnalysis. + * Dependent objects whose reference property will be set to its schema default. * - * @param bool $deletable Whether the object can be deleted without violating constraints. - * @param array $cascadeTargets Objects that would be cascade-deleted. - * @param array $nullifyTargets Objects that would have their reference set to null. - * @param array $defaultTargets Objects that would have their reference set to default value. - * @param array $blockers Objects that block the deletion (RESTRICT). - * @param array $chainPaths Full graph paths for debugging. + * @var array + */ + public readonly array $defaultTargets; + + /** + * Dependent objects that block deletion (onDelete = RESTRICT). + * + * @var array + */ + public readonly array $blockers; + + /** + * The full dependency chain path for each blocker or cascade target. + * + * @var array + */ + public readonly array $chainPaths; + + /** + * Construct a new DeletionAnalysis value object. + * + * @param bool $deletable Whether the object may be deleted without violating RESTRICT constraints. + * @param array $cascadeTargets Dependent objects that will be cascade-deleted alongside the root. + * @param array $nullifyTargets Dependent objects whose reference property will be set to null. + * @param array $defaultTargets Dependent objects whose reference property will be set to its schema default. + * @param array $blockers Dependent objects that block deletion (onDelete = RESTRICT). + * @param array $chainPaths The full dependency chain path for each blocker or cascade target. */ public function __construct( - public readonly bool $deletable, - public readonly array $cascadeTargets=[], - public readonly array $nullifyTargets=[], - public readonly array $defaultTargets=[], - public readonly array $blockers=[], - public readonly array $chainPaths=[] + bool $deletable, + array $cascadeTargets=[], + array $nullifyTargets=[], + array $defaultTargets=[], + array $blockers=[], + array $chainPaths=[], ) { + $this->deletable = $deletable; + $this->cascadeTargets = $cascadeTargets; + $this->nullifyTargets = $nullifyTargets; + $this->defaultTargets = $defaultTargets; + $this->blockers = $blockers; + $this->chainPaths = $chainPaths; }//end __construct() /** - * Create an empty deletable analysis with no targets or blockers. + * Return an empty analysis indicating no dependencies were found. + * + * Used as a safe early-return value when no referential-integrity configuration + * exists for the object's schema, or when a recursion limit / cycle is hit. * - * @return self A deletable analysis with empty target lists. + * @return self */ public static function empty(): self { - return new self(deletable: true); + return new self( + deletable: true, + cascadeTargets: [], + nullifyTargets: [], + defaultTargets: [], + blockers: [], + chainPaths: [] + ); }//end empty() /** * Convert the analysis to an array suitable for JSON serialization. * - * @return array The analysis as an associative array. + * @return array The analysis as an associative array. */ public function toArray(): array { diff --git a/lib/Event/ActionCreatedEvent.php b/lib/Event/ActionCreatedEvent.php index f00c304937..d477888f5a 100644 --- a/lib/Event/ActionCreatedEvent.php +++ b/lib/Event/ActionCreatedEvent.php @@ -56,7 +56,7 @@ public function __construct(Action $action) * * @return Action The action entity * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getAction(): Action { diff --git a/lib/Event/ActionDeletedEvent.php b/lib/Event/ActionDeletedEvent.php index 2368687944..1b0ea4267e 100644 --- a/lib/Event/ActionDeletedEvent.php +++ b/lib/Event/ActionDeletedEvent.php @@ -56,7 +56,7 @@ public function __construct(Action $action) * * @return Action The action entity * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getAction(): Action { diff --git a/lib/Event/ActionUpdatedEvent.php b/lib/Event/ActionUpdatedEvent.php index defc2f1ab0..62fb7b01dd 100644 --- a/lib/Event/ActionUpdatedEvent.php +++ b/lib/Event/ActionUpdatedEvent.php @@ -56,7 +56,7 @@ public function __construct(Action $action) * * @return Action The action entity * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getAction(): Action { diff --git a/lib/Event/AgentCreatedEvent.php b/lib/Event/AgentCreatedEvent.php index 19a67dfd1f..d040ce9c20 100644 --- a/lib/Event/AgentCreatedEvent.php +++ b/lib/Event/AgentCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(Agent $agent) * * @return Agent The agent that was created. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getAgent(): Agent { diff --git a/lib/Event/AgentDeletedEvent.php b/lib/Event/AgentDeletedEvent.php index 2527750b21..3d3c75a6cd 100644 --- a/lib/Event/AgentDeletedEvent.php +++ b/lib/Event/AgentDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(Agent $agent) * * @return Agent The agent that was deleted. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getAgent(): Agent { diff --git a/lib/Event/AgentRunRequestedEvent.php b/lib/Event/AgentRunRequestedEvent.php new file mode 100644 index 0000000000..e8bb37fcb8 --- /dev/null +++ b/lib/Event/AgentRunRequestedEvent.php @@ -0,0 +1,250 @@ + + * SPDX-License-Identifier: EUPL-1.2 + * + * @category Event + * @package OCA\OpenRegister\Event + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Event; + +use OCP\EventDispatcher\Event; +use Symfony\Component\Uid\Uuid; + +/** + * Event dispatched to request a governed agent run against a triggering object. + * + * Carries provenance (the triggering object's uuid/register/schema) plus the + * requested-run payload (agent/skill refs, the rendered prompt, the field to write + * the result to, whether human approval is required, and the dispatch mode). + * `mode` is `"async"` only in v1 — a consumer MUST treat any other mode as + * unsupported and skip the run rather than execute it inline. + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ +class AgentRunRequestedEvent extends Event +{ + + /** + * A generated correlation id, unique per dispatch — lets a consumer de-duplicate + * a gated (pending-approval) run without a second identity scheme. + * + * @var string + */ + private string $correlationId; + + /** + * Constructor. + * + * @param string $subjectUuid UUID of the triggering object. + * @param string $subjectRegister Register slug/id of the triggering object. + * @param string $subjectSchema Schema slug/id of the triggering object. + * @param string $agent The configured agent reference (UUID in v1). + * @param string|null $skill Optional configured skill reference (slug). + * @param string $prompt The fully-rendered prompt (placeholders already + * resolved by FlowActionService's template engine). + * @param string $resultField The object field the run's output is written to. + * @param bool $requiresApproval Whether the run must pass a human-approval gate + * before executing. + * @param string $mode Dispatch mode — `"async"` only in v1. + * @param string $flowName The owning flow's name (diagnostics/audit). + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function __construct( + private readonly string $subjectUuid, + private readonly string $subjectRegister, + private readonly string $subjectSchema, + private readonly string $agent, + private readonly ?string $skill, + private readonly string $prompt, + private readonly string $resultField, + private readonly bool $requiresApproval, + private readonly string $mode, + private readonly string $flowName, + ) { + parent::__construct(); + $this->correlationId = Uuid::v4()->toRfc4122(); + }//end __construct() + + /** + * Get the triggering object's UUID. + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getSubjectUuid(): string + { + return $this->subjectUuid; + }//end getSubjectUuid() + + /** + * Get the triggering object's register slug/id. + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getSubjectRegister(): string + { + return $this->subjectRegister; + }//end getSubjectRegister() + + /** + * Get the triggering object's schema slug/id. + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getSubjectSchema(): string + { + return $this->subjectSchema; + }//end getSubjectSchema() + + /** + * Get the configured agent reference (UUID). + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getAgent(): string + { + return $this->agent; + }//end getAgent() + + /** + * Get the optional configured skill reference (slug). + * + * @return string|null + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getSkill(): ?string + { + return $this->skill; + }//end getSkill() + + /** + * Get the fully-rendered prompt. + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getPrompt(): string + { + return $this->prompt; + }//end getPrompt() + + /** + * Get the object field the run's output is written to. + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getResultField(): string + { + return $this->resultField; + }//end getResultField() + + /** + * Whether the run must pass a human-approval gate before executing. + * + * @return bool + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function isRequiresApproval(): bool + { + return $this->requiresApproval; + }//end isRequiresApproval() + + /** + * Get the dispatch mode (`"async"` only in v1). + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getMode(): string + { + return $this->mode; + }//end getMode() + + /** + * Get the owning flow's name. + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getFlowName(): string + { + return $this->flowName; + }//end getFlowName() + + /** + * Get the generated correlation id for this dispatch. + * + * @return string + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getCorrelationId(): string + { + return $this->correlationId; + }//end getCorrelationId() + + /** + * Flatten the event into a plain, JSON-serialisable payload. + * + * Consumers that hand this off to a background job (the async contract — a + * job argument must be a scalar-only array) use this instead of passing the + * event object across the job boundary. + * + * @return array + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-1-1 + */ + public function getPayload(): array + { + return [ + 'subjectUuid' => $this->subjectUuid, + 'subjectRegister' => $this->subjectRegister, + 'subjectSchema' => $this->subjectSchema, + 'agent' => $this->agent, + 'skill' => $this->skill, + 'prompt' => $this->prompt, + 'resultField' => $this->resultField, + 'requiresApproval' => $this->requiresApproval, + 'mode' => $this->mode, + 'flowName' => $this->flowName, + 'correlationId' => $this->correlationId, + ]; + }//end getPayload() +}//end class diff --git a/lib/Event/AgentUpdatedEvent.php b/lib/Event/AgentUpdatedEvent.php index 1eabff0183..430cc56d4b 100644 --- a/lib/Event/AgentUpdatedEvent.php +++ b/lib/Event/AgentUpdatedEvent.php @@ -66,7 +66,7 @@ public function __construct(Agent $newAgent, Agent $oldAgent) * * @return Agent The agent after update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getAgent(): Agent { @@ -78,7 +78,7 @@ public function getAgent(): Agent * * @return Agent The agent after update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewAgent(): Agent { @@ -90,7 +90,7 @@ public function getNewAgent(): Agent * * @return Agent The agent before update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldAgent(): Agent { diff --git a/lib/Event/ApplicationCreatedEvent.php b/lib/Event/ApplicationCreatedEvent.php index bce0fc19f5..24590dde15 100644 --- a/lib/Event/ApplicationCreatedEvent.php +++ b/lib/Event/ApplicationCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(Application $application) * * @return Application The application that was created. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getApplication(): Application { diff --git a/lib/Event/ApplicationDeletedEvent.php b/lib/Event/ApplicationDeletedEvent.php index f2d150d222..d97697ec6a 100644 --- a/lib/Event/ApplicationDeletedEvent.php +++ b/lib/Event/ApplicationDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(Application $application) * * @return Application The application that was deleted. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getApplication(): Application { diff --git a/lib/Event/ApplicationUpdatedEvent.php b/lib/Event/ApplicationUpdatedEvent.php index 554651100b..f5d8cef3fc 100644 --- a/lib/Event/ApplicationUpdatedEvent.php +++ b/lib/Event/ApplicationUpdatedEvent.php @@ -66,7 +66,7 @@ public function __construct(Application $newApplication, Application $oldApplica * * @return Application The application after update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewApplication(): Application { @@ -78,7 +78,7 @@ public function getNewApplication(): Application * * @return Application The application before update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldApplication(): Application { diff --git a/lib/Event/ApprovalStepApprovedEvent.php b/lib/Event/ApprovalStepApprovedEvent.php new file mode 100644 index 0000000000..58d5fac801 --- /dev/null +++ b/lib/Event/ApprovalStepApprovedEvent.php @@ -0,0 +1,135 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/add-approval-step-events/tasks.md#task-1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Event; + +use OCA\OpenRegister\Db\ApprovalChain; +use OCA\OpenRegister\Db\ApprovalStep; +use OCP\EventDispatcher\Event; + +/** + * Event dispatched when an approval step is approved. + * + * Carries the step, chain, the user who approved, the `statusOnApprove` + * resolved from the chain definition, and the next step (if any). Downstream + * apps (docudesk signing) use this to advance their own workflow state. + */ +class ApprovalStepApprovedEvent extends Event +{ + /** + * Constructor. + * + * @param ApprovalChain $chain The approval chain. + * @param ApprovalStep $step The step that was approved. + * @param string $userId ID of the user who approved. + * @param string $statusOnApprove The configured `statusOnApprove` + * from the chain step definition. + * @param ApprovalStep|null $nextStep The next step now in `pending`, or + * null if this was the final step. + * + * @return void + */ + public function __construct( + private readonly ApprovalChain $chain, + private readonly ApprovalStep $step, + private readonly string $userId, + private readonly string $statusOnApprove, + private readonly ?ApprovalStep $nextStep + ) { + parent::__construct(); + }//end __construct() + + /** + * Get the approval chain. + * + * @return ApprovalChain The approval chain configuration. + */ + public function getChain(): ApprovalChain + { + return $this->chain; + }//end getChain() + + /** + * Get the approved step. + * + * @return ApprovalStep The step that was approved. + */ + public function getStep(): ApprovalStep + { + return $this->step; + }//end getStep() + + /** + * Get the ID of the approving user. + * + * @return string Nextcloud user ID. + */ + public function getUserId(): string + { + return $this->userId; + }//end getUserId() + + /** + * Get the configured status the parent object should adopt on approval. + * + * @return string Status string from the chain step's `statusOnApprove`. + */ + public function getStatusOnApprove(): string + { + return $this->statusOnApprove; + }//end getStatusOnApprove() + + /** + * Get the next step now in `pending`, if any. + * + * @return ApprovalStep|null The next step or null if the chain has ended. + */ + public function getNextStep(): ?ApprovalStep + { + return $this->nextStep; + }//end getNextStep() + + /** + * Convenience: is this the final step of the chain? + * + * @return bool True when no next step is pending. + */ + public function isFinalStep(): bool + { + return $this->nextStep === null; + }//end isFinalStep() + + /** + * Convenience: the object UUID being approved. + * + * @return string Object UUID (empty string if the step has none yet). + */ + public function getObjectUuid(): string + { + return $this->step->getObjectUuid() ?? ''; + }//end getObjectUuid() +}//end class diff --git a/lib/Event/ApprovalStepCompletedEvent.php b/lib/Event/ApprovalStepCompletedEvent.php new file mode 100644 index 0000000000..fc1a228fbc --- /dev/null +++ b/lib/Event/ApprovalStepCompletedEvent.php @@ -0,0 +1,116 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/add-approval-step-events/tasks.md#task-1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Event; + +use OCA\OpenRegister\Db\ApprovalChain; +use OCA\OpenRegister\Db\ApprovalStep; +use OCP\EventDispatcher\Event; + +/** + * Event dispatched when an approval chain completes (final step approved). + * + * Fired ONCE per chain per object — at the moment the last step transitions + * to `approved` and there is no next step. A separate + * `ApprovalStepApprovedEvent` is also fired for that final step; downstream + * apps that only care about full-chain completion should listen here. + */ +class ApprovalStepCompletedEvent extends Event +{ + /** + * Constructor. + * + * @param ApprovalChain $chain The approval chain that completed. + * @param ApprovalStep $finalStep The last step (approved) of the chain. + * @param string $userId ID of the user who approved the final step. + * @param string $statusOnApprove The configured `statusOnApprove` from + * the final step's chain definition — + * i.e. the terminal status of the chain. + * + * @return void + */ + public function __construct( + private readonly ApprovalChain $chain, + private readonly ApprovalStep $finalStep, + private readonly string $userId, + private readonly string $statusOnApprove + ) { + parent::__construct(); + }//end __construct() + + /** + * Get the approval chain. + * + * @return ApprovalChain The approval chain configuration. + */ + public function getChain(): ApprovalChain + { + return $this->chain; + }//end getChain() + + /** + * Get the final step that closed the chain. + * + * @return ApprovalStep The final approved step. + */ + public function getFinalStep(): ApprovalStep + { + return $this->finalStep; + }//end getFinalStep() + + /** + * Get the ID of the user who closed the chain. + * + * @return string Nextcloud user ID. + */ + public function getUserId(): string + { + return $this->userId; + }//end getUserId() + + /** + * Get the terminal status the parent object should adopt. + * + * @return string Status string from the final step's `statusOnApprove`. + */ + public function getStatusOnApprove(): string + { + return $this->statusOnApprove; + }//end getStatusOnApprove() + + /** + * Convenience: the object UUID whose chain is now complete. + * + * @return string Object UUID (empty string if the step has none yet). + */ + public function getObjectUuid(): string + { + return $this->finalStep->getObjectUuid() ?? ''; + }//end getObjectUuid() +}//end class diff --git a/lib/Event/ApprovalStepInitiatedEvent.php b/lib/Event/ApprovalStepInitiatedEvent.php new file mode 100644 index 0000000000..6c1fae801c --- /dev/null +++ b/lib/Event/ApprovalStepInitiatedEvent.php @@ -0,0 +1,90 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/add-approval-step-events/tasks.md#task-1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Event; + +use OCA\OpenRegister\Db\ApprovalChain; +use OCA\OpenRegister\Db\ApprovalStep; +use OCP\EventDispatcher\Event; + +/** + * Event dispatched when an approval step is initiated (becomes `pending`). + * + * Downstream apps (docudesk signing workflow, decidesk multi-step decisions, + * any approval-driven leaf app) subscribe to this event to notify the + * authorised role group, queue reminders, or kick off integrations. + */ +class ApprovalStepInitiatedEvent extends Event +{ + /** + * Constructor. + * + * @param ApprovalChain $chain The approval chain the step belongs to. + * @param ApprovalStep $step The step that has just transitioned to `pending`. + * @param string $objectUuid UUID of the object being approved. + * + * @return void + */ + public function __construct( + private readonly ApprovalChain $chain, + private readonly ApprovalStep $step, + private readonly string $objectUuid + ) { + parent::__construct(); + }//end __construct() + + /** + * Get the approval chain. + * + * @return ApprovalChain The approval chain configuration. + */ + public function getChain(): ApprovalChain + { + return $this->chain; + }//end getChain() + + /** + * Get the approval step that has been initiated. + * + * @return ApprovalStep The step now in `pending`. + */ + public function getStep(): ApprovalStep + { + return $this->step; + }//end getStep() + + /** + * Get the UUID of the object whose chain is progressing. + * + * @return string Object UUID. + */ + public function getObjectUuid(): string + { + return $this->objectUuid; + }//end getObjectUuid() +}//end class diff --git a/lib/Event/ApprovalStepRejectedEvent.php b/lib/Event/ApprovalStepRejectedEvent.php new file mode 100644 index 0000000000..5e5251b4e0 --- /dev/null +++ b/lib/Event/ApprovalStepRejectedEvent.php @@ -0,0 +1,111 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/add-approval-step-events/tasks.md#task-1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Event; + +use OCA\OpenRegister\Db\ApprovalChain; +use OCA\OpenRegister\Db\ApprovalStep; +use OCP\EventDispatcher\Event; + +/** + * Event dispatched when an approval step is rejected. + * + * A rejection terminates the chain (no next step is advanced). Downstream + * apps can subscribe to roll back state, notify the requester, or trigger + * a `rejected` notification. + */ +class ApprovalStepRejectedEvent extends Event +{ + /** + * Constructor. + * + * @param ApprovalChain $chain The approval chain. + * @param ApprovalStep $step The step that was rejected. + * @param string $userId ID of the user who rejected. + * @param string $statusOnReject The configured `statusOnReject` from + * the chain step definition. + * + * @return void + */ + public function __construct( + private readonly ApprovalChain $chain, + private readonly ApprovalStep $step, + private readonly string $userId, + private readonly string $statusOnReject + ) { + parent::__construct(); + }//end __construct() + + /** + * Get the approval chain. + * + * @return ApprovalChain The approval chain configuration. + */ + public function getChain(): ApprovalChain + { + return $this->chain; + }//end getChain() + + /** + * Get the rejected step. + * + * @return ApprovalStep The step that was rejected. + */ + public function getStep(): ApprovalStep + { + return $this->step; + }//end getStep() + + /** + * Get the ID of the rejecting user. + * + * @return string Nextcloud user ID. + */ + public function getUserId(): string + { + return $this->userId; + }//end getUserId() + + /** + * Get the configured status the parent object should adopt on rejection. + * + * @return string Status string from the chain step's `statusOnReject`. + */ + public function getStatusOnReject(): string + { + return $this->statusOnReject; + }//end getStatusOnReject() + + /** + * Convenience: the object UUID being rejected. + * + * @return string Object UUID (empty string if the step has none yet). + */ + public function getObjectUuid(): string + { + return $this->step->getObjectUuid() ?? ''; + }//end getObjectUuid() +}//end class diff --git a/lib/Event/ConfigurationCreatedEvent.php b/lib/Event/ConfigurationCreatedEvent.php index 5f825d5227..dcaec661dd 100644 --- a/lib/Event/ConfigurationCreatedEvent.php +++ b/lib/Event/ConfigurationCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(Configuration $configuration) * * @return Configuration The configuration that was created. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getConfiguration(): Configuration { diff --git a/lib/Event/ConfigurationDeletedEvent.php b/lib/Event/ConfigurationDeletedEvent.php index dc166c059c..ac003dd115 100644 --- a/lib/Event/ConfigurationDeletedEvent.php +++ b/lib/Event/ConfigurationDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(Configuration $configuration) * * @return Configuration The configuration that was deleted. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getConfiguration(): Configuration { diff --git a/lib/Event/ConfigurationUpdatedEvent.php b/lib/Event/ConfigurationUpdatedEvent.php index 095bb58d8f..490b274ba7 100644 --- a/lib/Event/ConfigurationUpdatedEvent.php +++ b/lib/Event/ConfigurationUpdatedEvent.php @@ -28,6 +28,8 @@ /** * Event dispatched when a configuration is updated. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 */ class ConfigurationUpdatedEvent extends Event { @@ -54,7 +56,7 @@ class ConfigurationUpdatedEvent extends Event * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-event-all/tasks.md#task-4 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct(Configuration $newConfiguration, Configuration $oldConfiguration) { @@ -62,4 +64,28 @@ public function __construct(Configuration $newConfiguration, Configuration $oldC $this->newConfiguration = $newConfiguration; $this->oldConfiguration = $oldConfiguration; }//end __construct() + + /** + * Get the updated configuration. + * + * @return Configuration The configuration after update. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ + public function getNewConfiguration(): Configuration + { + return $this->newConfiguration; + }//end getNewConfiguration() + + /** + * Get the original configuration. + * + * @return Configuration The configuration before update. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ + public function getOldConfiguration(): Configuration + { + return $this->oldConfiguration; + }//end getOldConfiguration() }//end class diff --git a/lib/Event/ConversationCreatedEvent.php b/lib/Event/ConversationCreatedEvent.php index b2aca1e238..2980f07b85 100644 --- a/lib/Event/ConversationCreatedEvent.php +++ b/lib/Event/ConversationCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(Conversation $conversation) * * @return Conversation The conversation that was created. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getConversation(): Conversation { diff --git a/lib/Event/ConversationDeletedEvent.php b/lib/Event/ConversationDeletedEvent.php index 6bd9fd30e2..d3724400ab 100644 --- a/lib/Event/ConversationDeletedEvent.php +++ b/lib/Event/ConversationDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(Conversation $conversation) * * @return Conversation The conversation that was deleted. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getConversation(): Conversation { diff --git a/lib/Event/ConversationUpdatedEvent.php b/lib/Event/ConversationUpdatedEvent.php index 696f893de3..61ca3c75ac 100644 --- a/lib/Event/ConversationUpdatedEvent.php +++ b/lib/Event/ConversationUpdatedEvent.php @@ -66,7 +66,7 @@ public function __construct(Conversation $newConversation, Conversation $oldConv * * @return Conversation The updated conversation entity * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getConversation(): Conversation { @@ -78,7 +78,7 @@ public function getConversation(): Conversation * * @return Conversation The conversation after update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewConversation(): Conversation { @@ -90,7 +90,7 @@ public function getNewConversation(): Conversation * * @return Conversation The conversation before update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldConversation(): Conversation { diff --git a/lib/Event/CustomScopeEvaluatedEvent.php b/lib/Event/CustomScopeEvaluatedEvent.php index a1e554f65e..09f3caae70 100644 --- a/lib/Event/CustomScopeEvaluatedEvent.php +++ b/lib/Event/CustomScopeEvaluatedEvent.php @@ -19,13 +19,13 @@ * @category Event * @package OCA\OpenRegister\Event * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app * - * @spec openspec/changes/rbac-scopes/tasks.md + * @spec openspec/specs/rbac-scopes/spec.md */ declare(strict_types=1); diff --git a/lib/Event/CustomScopeEvaluatingEvent.php b/lib/Event/CustomScopeEvaluatingEvent.php index 8780a219dd..48eda1c8d9 100644 --- a/lib/Event/CustomScopeEvaluatingEvent.php +++ b/lib/Event/CustomScopeEvaluatingEvent.php @@ -29,13 +29,13 @@ * @category Event * @package OCA\OpenRegister\Event * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app * - * @spec openspec/changes/rbac-scopes/tasks.md + * @spec openspec/specs/rbac-scopes/spec.md */ declare(strict_types=1); diff --git a/lib/Event/DeepLinkRegistrationEvent.php b/lib/Event/DeepLinkRegistrationEvent.php index bfb024658d..d6ece2f0fc 100644 --- a/lib/Event/DeepLinkRegistrationEvent.php +++ b/lib/Event/DeepLinkRegistrationEvent.php @@ -65,23 +65,27 @@ public function getRegistry(): DeepLinkRegistryService /** * Convenience method to register a deep link pattern directly on the event. * - * @param string $appId The consuming app ID (e.g., "procest") - * @param string $registerSlug The register slug - * @param string $schemaSlug The schema slug - * @param string $urlTemplate URL template with placeholders (e.g., "/apps/procest/#/cases/{uuid}") - * @param string $icon Optional icon identifier + * @param string $appId The consuming app ID (e.g., "procest") + * @param string $registerSlug The register slug + * @param string $schemaSlug The schema slug + * @param string $urlTemplate URL template with placeholders (e.g., "/apps/procest/#/cases/{uuid}") + * @param string $icon Optional icon identifier + * @param string|null $displayName Optional human-readable label for the app's + * unified-search results (defaults to null → app id) * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events + * @spec openspec/specs/deep-link-registry/spec.md */ public function register( string $appId, string $registerSlug, string $schemaSlug, string $urlTemplate, - string $icon='' + string $icon='', + ?string $displayName=null ): void { - $this->registry->register($appId, $registerSlug, $schemaSlug, $urlTemplate, $icon); + $this->registry->register($appId, $registerSlug, $schemaSlug, $urlTemplate, $icon, $displayName); }//end register() }//end class diff --git a/lib/Event/EntityRelationDecisionUpdatedEvent.php b/lib/Event/EntityRelationDecisionUpdatedEvent.php new file mode 100644 index 0000000000..ee58bfa6f5 --- /dev/null +++ b/lib/Event/EntityRelationDecisionUpdatedEvent.php @@ -0,0 +1,125 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Event; + +use OCA\OpenRegister\Db\EntityRelation; +use OCP\EventDispatcher\Event; +use OCP\IUser; + +/** + * Event dispatched after an EntityRelation's decision metadata is updated. + * + * Post-commit, informational. Not vetoable. Listeners that need to + * mutate state in response (e.g. reverse the PATCH on prohibition match) + * MUST do so via a separate write — the original write has already landed. + */ +class EntityRelationDecisionUpdatedEvent extends Event +{ + /** + * Constructor. + * + * @param EntityRelation $relation The relation row in its post-update state. + * @param array $changedFields Per-field diff + * (only keys that actually changed). + * @param IUser|null $actingUser The user who performed the change, or null + * when the change was driven by a + * non-session actor (background job, + * system). + */ + public function __construct( + private readonly EntityRelation $relation, + private readonly array $changedFields, + private readonly ?IUser $actingUser + ) { + parent::__construct(); + + }//end __construct() + + /** + * Get the post-update relation row. + * + * @return EntityRelation + */ + public function getRelation(): EntityRelation + { + return $this->relation; + + }//end getRelation() + + /** + * Get the per-field diff for the change. + * + * Shape: `{ fieldName: { previous: , new: } }`. Keys + * are limited to `bases` and `skipAnonymization` per the + * `updateDecisionMetadata` whitelist. Only fields whose value actually + * changed appear — listeners checking for a specific transition + * (e.g. `skipAnonymization: false → true`) can rely on the key's + * presence + `previous` / `new` values without re-deriving. + * + * @return array + */ + public function getChangedFields(): array + { + return $this->changedFields; + + }//end getChangedFields() + + /** + * Get the acting user, or null when the change had no session user. + * + * Listeners that need to attribute downstream effects (audit, consent + * record acting-user) should call this. When null, downstream callers + * record the actor as 'system'. + * + * @return IUser|null + */ + public function getActingUser(): ?IUser + { + return $this->actingUser; + + }//end getActingUser() + + /** + * Convenience: true when `skipAnonymization` was flipped from false to true + * by this change. Encapsulates the most common listener trigger so each + * consumer doesn't reimplement the same check. + * + * @return bool + */ + public function isSkipAnonymizationActivated(): bool + { + if (array_key_exists('skipAnonymization', $this->changedFields) === false) { + return false; + } + + $diff = $this->changedFields['skipAnonymization']; + return $diff['previous'] === false && $diff['new'] === true; + + }//end isSkipAnonymizationActivated() +}//end class diff --git a/lib/Event/FileCopiedEvent.php b/lib/Event/FileCopiedEvent.php index 8957322a38..1a28ceb37e 100644 --- a/lib/Event/FileCopiedEvent.php +++ b/lib/Event/FileCopiedEvent.php @@ -34,7 +34,7 @@ class FileCopiedEvent extends Event * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md */ public function __construct( private readonly string $objectUuid, @@ -49,7 +49,7 @@ public function __construct( * * @return string The object UUID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObjectUuid(): string { @@ -61,7 +61,7 @@ public function getObjectUuid(): string * * @return int The file ID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getFileId(): int { @@ -73,7 +73,7 @@ public function getFileId(): int * * @return array The event data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getData(): array { diff --git a/lib/Event/FileLockedEvent.php b/lib/Event/FileLockedEvent.php index cca2afef04..63f6946fd2 100644 --- a/lib/Event/FileLockedEvent.php +++ b/lib/Event/FileLockedEvent.php @@ -34,7 +34,7 @@ class FileLockedEvent extends Event * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-2 + * @spec openspec/specs/file-actions/spec.md */ public function __construct( private readonly string $objectUuid, @@ -49,7 +49,7 @@ public function __construct( * * @return string The object UUID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObjectUuid(): string { @@ -61,7 +61,7 @@ public function getObjectUuid(): string * * @return int The file ID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getFileId(): int { @@ -73,7 +73,7 @@ public function getFileId(): int * * @return array The event data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getData(): array { diff --git a/lib/Event/FileMovedEvent.php b/lib/Event/FileMovedEvent.php index 9c38fe5c28..5494e62953 100644 --- a/lib/Event/FileMovedEvent.php +++ b/lib/Event/FileMovedEvent.php @@ -34,7 +34,7 @@ class FileMovedEvent extends Event * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md */ public function __construct( private readonly string $objectUuid, @@ -49,7 +49,7 @@ public function __construct( * * @return string The object UUID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObjectUuid(): string { @@ -61,7 +61,7 @@ public function getObjectUuid(): string * * @return int The file ID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getFileId(): int { @@ -73,7 +73,7 @@ public function getFileId(): int * * @return array The event data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getData(): array { diff --git a/lib/Event/FileRenamedEvent.php b/lib/Event/FileRenamedEvent.php index a4e198b532..0cbcbf08d6 100644 --- a/lib/Event/FileRenamedEvent.php +++ b/lib/Event/FileRenamedEvent.php @@ -34,7 +34,7 @@ class FileRenamedEvent extends Event * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md */ public function __construct( private readonly string $objectUuid, @@ -49,7 +49,7 @@ public function __construct( * * @return string The object UUID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObjectUuid(): string { @@ -61,7 +61,7 @@ public function getObjectUuid(): string * * @return int The file ID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getFileId(): int { @@ -73,7 +73,7 @@ public function getFileId(): int * * @return array The event data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getData(): array { diff --git a/lib/Event/FileUnlockedEvent.php b/lib/Event/FileUnlockedEvent.php index 7ade8ea13b..8ba9648f2f 100644 --- a/lib/Event/FileUnlockedEvent.php +++ b/lib/Event/FileUnlockedEvent.php @@ -34,7 +34,7 @@ class FileUnlockedEvent extends Event * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-2 + * @spec openspec/specs/file-actions/spec.md */ public function __construct( private readonly string $objectUuid, @@ -49,7 +49,7 @@ public function __construct( * * @return string The object UUID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObjectUuid(): string { @@ -61,7 +61,7 @@ public function getObjectUuid(): string * * @return int The file ID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getFileId(): int { @@ -73,7 +73,7 @@ public function getFileId(): int * * @return array The event data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getData(): array { diff --git a/lib/Event/FileVersionRestoredEvent.php b/lib/Event/FileVersionRestoredEvent.php index 88317f8593..31e58d367e 100644 --- a/lib/Event/FileVersionRestoredEvent.php +++ b/lib/Event/FileVersionRestoredEvent.php @@ -34,7 +34,7 @@ class FileVersionRestoredEvent extends Event * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-content-versioning/tasks.md#task-1 + * @spec openspec/specs/content-versioning/spec.md */ public function __construct( private readonly string $objectUuid, @@ -49,7 +49,7 @@ public function __construct( * * @return string The object UUID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObjectUuid(): string { @@ -61,7 +61,7 @@ public function getObjectUuid(): string * * @return int The file ID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getFileId(): int { @@ -73,7 +73,7 @@ public function getFileId(): int * * @return array The event data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getData(): array { diff --git a/lib/Event/HandoffExecutedEvent.php b/lib/Event/HandoffExecutedEvent.php new file mode 100644 index 0000000000..838c78cc61 --- /dev/null +++ b/lib/Event/HandoffExecutedEvent.php @@ -0,0 +1,248 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Event; + +use OCP\EventDispatcher\Event; + +/** + * Event dispatched after each successful (or deferred-drained) handoff. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ +class HandoffExecutedEvent extends Event +{ + /** + * Constructor. + * + * @param string $sourceApp The app owning the emitting schema ('' when undeclared). + * @param int $sourceRegister Register id of the source object. + * @param int $sourceSchema Schema id of the source object. + * @param string $sourceObjectUuid UUID of the source object. + * @param string|null $subjectLabel Human-readable label of the source object. + * @param string $targetSemanticType The canonical kind URI that was handed off to. + * @param int $targetRegister Register id of the created target object. + * @param int $targetSchema Schema id of the created target object. + * @param string $targetObjectUuid UUID of the created target object. + * @param string $handoffId The declared handoff entry id. + * @param string $correlationId Correlation id minted for this execution. + * @param bool $deferred True when this execution drained a parked queue entry. + */ + public function __construct( + private readonly string $sourceApp, + private readonly int $sourceRegister, + private readonly int $sourceSchema, + private readonly string $sourceObjectUuid, + private readonly ?string $subjectLabel, + private readonly string $targetSemanticType, + private readonly int $targetRegister, + private readonly int $targetSchema, + private readonly string $targetObjectUuid, + private readonly string $handoffId, + private readonly string $correlationId, + private readonly bool $deferred=false, + ) { + parent::__construct(); + + }//end __construct() + + /** + * The app owning the emitting (source) schema. + * + * @return string App id, '' when the source schema declares no owning app. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getSourceApp(): string + { + return $this->sourceApp; + + }//end getSourceApp() + + /** + * Register id of the source object. + * + * @return int The source register id. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getSourceRegister(): int + { + return $this->sourceRegister; + + }//end getSourceRegister() + + /** + * Schema id of the source object. + * + * @return int The source schema id. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getSourceSchema(): int + { + return $this->sourceSchema; + + }//end getSourceSchema() + + /** + * UUID of the source object. + * + * @return string The source object UUID. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getSourceObjectUuid(): string + { + return $this->sourceObjectUuid; + + }//end getSourceObjectUuid() + + /** + * Human-readable label of the source object (provenance subject). + * + * @return string|null The subject label, when the source object has one. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getSubjectLabel(): ?string + { + return $this->subjectLabel; + + }//end getSubjectLabel() + + /** + * The canonical semantic kind URI the object was handed off to. + * + * @return string The kind URI. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getTargetSemanticType(): string + { + return $this->targetSemanticType; + + }//end getTargetSemanticType() + + /** + * Register id of the created target object. + * + * @return int The target register id. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getTargetRegister(): int + { + return $this->targetRegister; + + }//end getTargetRegister() + + /** + * Schema id of the created target object. + * + * @return int The target schema id. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getTargetSchema(): int + { + return $this->targetSchema; + + }//end getTargetSchema() + + /** + * UUID of the created target object. + * + * @return string The target object UUID. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getTargetObjectUuid(): string + { + return $this->targetObjectUuid; + + }//end getTargetObjectUuid() + + /** + * The declared handoff entry id (dialect `id`). + * + * @return string The handoff id. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getHandoffId(): string + { + return $this->handoffId; + + }//end getHandoffId() + + /** + * Correlation id minted per execution — also stamped on both audit rows + * (and the queue entry for deferred executions) so consumers can echo it + * back through the ADR-041 conclusion-event pattern. + * + * @return string The correlation id. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: ADR-041 event emission on handoff execution) + */ + public function getCorrelationId(): string + { + return $this->correlationId; + + }//end getCorrelationId() + + /** + * Whether this execution drained a parked queue-mode entry. + * + * @return bool True for deferred (drained) executions. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + public function isDeferred(): bool + { + return $this->deferred; + + }//end isDeferred() +}//end class diff --git a/lib/Event/ObjectCreatedEvent.php b/lib/Event/ObjectCreatedEvent.php index fbf8a22787..ee459b39fe 100644 --- a/lib/Event/ObjectCreatedEvent.php +++ b/lib/Event/ObjectCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(ObjectEntity $object) * * @return ObjectEntity The object entity that was created * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObject(): ObjectEntity { diff --git a/lib/Event/ObjectCreatingEvent.php b/lib/Event/ObjectCreatingEvent.php index cd898fe59c..a7bbf111c3 100644 --- a/lib/Event/ObjectCreatingEvent.php +++ b/lib/Event/ObjectCreatingEvent.php @@ -81,7 +81,7 @@ public function __construct(ObjectEntity $object) * * @return ObjectEntity The object entity that is being created * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObject(): ObjectEntity { @@ -93,7 +93,7 @@ public function getObject(): ObjectEntity * * @return bool True if propagation is stopped * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function isPropagationStopped(): bool { @@ -105,7 +105,7 @@ public function isPropagationStopped(): bool * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function stopPropagation(): void { @@ -119,7 +119,7 @@ public function stopPropagation(): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function setErrors(array $errors): void { @@ -131,7 +131,7 @@ public function setErrors(array $errors): void * * @return array The error details * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function getErrors(): array { @@ -145,7 +145,7 @@ public function getErrors(): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function setModifiedData(array $data): void { @@ -157,7 +157,7 @@ public function setModifiedData(array $data): void * * @return array The modified data * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function getModifiedData(): array { diff --git a/lib/Event/ObjectDeletedEvent.php b/lib/Event/ObjectDeletedEvent.php index 0d6b0a0837..73cced78d4 100644 --- a/lib/Event/ObjectDeletedEvent.php +++ b/lib/Event/ObjectDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(ObjectEntity $object) * * @return ObjectEntity The object entity that was deleted * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObject(): ObjectEntity { diff --git a/lib/Event/ObjectDeletingEvent.php b/lib/Event/ObjectDeletingEvent.php index 190d0b30e7..4ab0de9735 100644 --- a/lib/Event/ObjectDeletingEvent.php +++ b/lib/Event/ObjectDeletingEvent.php @@ -81,7 +81,7 @@ public function __construct(ObjectEntity $object) * * @return ObjectEntity The object entity that is being deleted * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObject(): ObjectEntity { @@ -93,7 +93,7 @@ public function getObject(): ObjectEntity * * @return bool True if propagation is stopped * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function isPropagationStopped(): bool { @@ -105,7 +105,7 @@ public function isPropagationStopped(): bool * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function stopPropagation(): void { @@ -119,7 +119,7 @@ public function stopPropagation(): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function setErrors(array $errors): void { @@ -131,7 +131,7 @@ public function setErrors(array $errors): void * * @return array The error details * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function getErrors(): array { @@ -145,7 +145,7 @@ public function getErrors(): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function setModifiedData(array $data): void { @@ -157,7 +157,7 @@ public function setModifiedData(array $data): void * * @return array The modified data * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function getModifiedData(): array { diff --git a/lib/Event/ObjectLockedEvent.php b/lib/Event/ObjectLockedEvent.php index 0ab6dffb08..04c652edc5 100644 --- a/lib/Event/ObjectLockedEvent.php +++ b/lib/Event/ObjectLockedEvent.php @@ -57,7 +57,7 @@ public function __construct(ObjectEntity $object) * * @return ObjectEntity The object that has been locked * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObject(): ObjectEntity { diff --git a/lib/Event/ObjectRevertedEvent.php b/lib/Event/ObjectRevertedEvent.php index b3e256116f..1170ffd912 100644 --- a/lib/Event/ObjectRevertedEvent.php +++ b/lib/Event/ObjectRevertedEvent.php @@ -67,7 +67,7 @@ public function __construct(ObjectEntity $object, $until=null) * * @return ObjectEntity The object that has been reverted * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObject(): ObjectEntity { @@ -79,7 +79,7 @@ public function getObject(): ObjectEntity * * @return DateTime|string|null The point in time or audit ID reverted to * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getRevertPoint() { diff --git a/lib/Event/ObjectUnlockedEvent.php b/lib/Event/ObjectUnlockedEvent.php index 72e7e10853..82a8b97314 100644 --- a/lib/Event/ObjectUnlockedEvent.php +++ b/lib/Event/ObjectUnlockedEvent.php @@ -57,7 +57,7 @@ public function __construct(ObjectEntity $object) * * @return ObjectEntity The object that has been unlocked * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events * @spec openspec/changes/retrofit-2026-05-24-b-event-all/tasks.md#task-3 */ public function getObject(): ObjectEntity diff --git a/lib/Event/ObjectUpdatedEvent.php b/lib/Event/ObjectUpdatedEvent.php index 7b16fb01f0..871304e7f5 100644 --- a/lib/Event/ObjectUpdatedEvent.php +++ b/lib/Event/ObjectUpdatedEvent.php @@ -66,7 +66,7 @@ public function __construct(ObjectEntity $newObject, ?ObjectEntity $oldObject=nu * * @return ObjectEntity The object entity after update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getObject(): ObjectEntity { @@ -78,7 +78,7 @@ public function getObject(): ObjectEntity * * @return ObjectEntity The object entity after update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewObject(): ObjectEntity { @@ -90,7 +90,7 @@ public function getNewObject(): ObjectEntity * * @return ObjectEntity|null The object entity before update (null if not available) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldObject(): ?ObjectEntity { diff --git a/lib/Event/ObjectUpdatingEvent.php b/lib/Event/ObjectUpdatingEvent.php index 833b2160b4..efca449ada 100644 --- a/lib/Event/ObjectUpdatingEvent.php +++ b/lib/Event/ObjectUpdatingEvent.php @@ -90,7 +90,7 @@ public function __construct(ObjectEntity $newObject, ?ObjectEntity $oldObject=nu * * @return ObjectEntity The object entity after update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewObject(): ObjectEntity { @@ -102,7 +102,7 @@ public function getNewObject(): ObjectEntity * * @return ObjectEntity|null The object entity before update (null if not available) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldObject(): ?ObjectEntity { @@ -114,7 +114,7 @@ public function getOldObject(): ?ObjectEntity * * @return bool True if propagation is stopped * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function isPropagationStopped(): bool { @@ -126,7 +126,7 @@ public function isPropagationStopped(): bool * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function stopPropagation(): void { @@ -140,7 +140,7 @@ public function stopPropagation(): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function setErrors(array $errors): void { @@ -152,7 +152,7 @@ public function setErrors(array $errors): void * * @return array The error details * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function getErrors(): array { @@ -166,7 +166,7 @@ public function getErrors(): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function setModifiedData(array $data): void { @@ -178,7 +178,7 @@ public function setModifiedData(array $data): void * * @return array The modified data * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function getModifiedData(): array { diff --git a/lib/Event/ObjectsMergedEvent.php b/lib/Event/ObjectsMergedEvent.php new file mode 100644 index 0000000000..bf974c520f --- /dev/null +++ b/lib/Event/ObjectsMergedEvent.php @@ -0,0 +1,115 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#3.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Event; + +use OCP\EventDispatcher\Event; + +/** + * Fired after a merge (or a merge reversal) completes. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#3.1 + */ +class ObjectsMergedEvent extends Event +{ + /** + * Capture the merge participants for downstream listeners. + * + * @param string $survivorUuid UUID of the surviving object. + * @param array $mergedFromUuids UUIDs of the merged-away objects. + * @param string $mergeOperationId UUID of the persisted `mergeOperation` row. + * @param bool $isReversal True when this event represents a reversal, not a merge. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#3.1 + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Immutable read-only event + * data, not a control-flow switch: `isReversal` is a fact about what + * already happened (a merge vs. its reversal), the shape the spec + * requires so a single subscriber can distinguish the two without a + * second event class. + */ + public function __construct( + private readonly string $survivorUuid, + private readonly array $mergedFromUuids, + private readonly string $mergeOperationId, + private readonly bool $isReversal=false + ) { + parent::__construct(); + }//end __construct() + + /** + * Read the surviving object's uuid. + * + * @return string Survivor uuid. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#3.1 + */ + public function getSurvivorUuid(): string + { + return $this->survivorUuid; + }//end getSurvivorUuid() + + /** + * Read the merged-away object uuids. + * + * @return array Merged-from uuids. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#3.1 + */ + public function getMergedFromUuids(): array + { + return $this->mergedFromUuids; + }//end getMergedFromUuids() + + /** + * Read the persisted `mergeOperation` row id. + * + * @return string Merge-operation uuid. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#3.1 + */ + public function getMergeOperationId(): string + { + return $this->mergeOperationId; + }//end getMergeOperationId() + + /** + * Whether this event represents a reversal rather than a merge. + * + * @return bool True when this is a reversal. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#3.1 + */ + public function isReversal(): bool + { + return $this->isReversal; + }//end isReversal() +}//end class diff --git a/lib/Event/OrganisationCreatedEvent.php b/lib/Event/OrganisationCreatedEvent.php index 7b0536cc17..67ddbf6aab 100644 --- a/lib/Event/OrganisationCreatedEvent.php +++ b/lib/Event/OrganisationCreatedEvent.php @@ -63,7 +63,7 @@ public function __construct(Organisation $organisation) * * @return Organisation The organisation entity * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOrganisation(): Organisation { diff --git a/lib/Event/OrganisationDeletedEvent.php b/lib/Event/OrganisationDeletedEvent.php index 81fd19bbd2..2d5495e45e 100644 --- a/lib/Event/OrganisationDeletedEvent.php +++ b/lib/Event/OrganisationDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(Organisation $organisation) * * @return Organisation The organisation that was deleted. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOrganisation(): Organisation { diff --git a/lib/Event/OrganisationUpdatedEvent.php b/lib/Event/OrganisationUpdatedEvent.php index e8edd1fd50..16d0e09e3d 100644 --- a/lib/Event/OrganisationUpdatedEvent.php +++ b/lib/Event/OrganisationUpdatedEvent.php @@ -66,7 +66,7 @@ public function __construct(Organisation $newOrganisation, Organisation $oldOrga * * @return Organisation The organisation after update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOrganisation(): Organisation { @@ -78,7 +78,7 @@ public function getOrganisation(): Organisation * * @return Organisation The organisation after update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewOrganisation(): Organisation { @@ -90,7 +90,7 @@ public function getNewOrganisation(): Organisation * * @return Organisation The organisation before update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldOrganisation(): Organisation { diff --git a/lib/Event/ReferenceValidatedEvent.php b/lib/Event/ReferenceValidatedEvent.php index 069b27f65b..9cc4384ebd 100644 --- a/lib/Event/ReferenceValidatedEvent.php +++ b/lib/Event/ReferenceValidatedEvent.php @@ -18,9 +18,9 @@ * @category Event * @package OCA\OpenRegister\Event * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Event/ReferenceValidationFailedEvent.php b/lib/Event/ReferenceValidationFailedEvent.php index 998483f152..a8d57b43aa 100644 --- a/lib/Event/ReferenceValidationFailedEvent.php +++ b/lib/Event/ReferenceValidationFailedEvent.php @@ -22,9 +22,9 @@ * @category Event * @package OCA\OpenRegister\Event * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Event/RegisterCreatedEvent.php b/lib/Event/RegisterCreatedEvent.php index 4d5e396b30..f5fa2895cb 100644 --- a/lib/Event/RegisterCreatedEvent.php +++ b/lib/Event/RegisterCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(Register $register) * * @return Register The register that was created * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getRegister(): Register { diff --git a/lib/Event/RegisterDeletedEvent.php b/lib/Event/RegisterDeletedEvent.php index 089ab5f8b6..7346e19369 100644 --- a/lib/Event/RegisterDeletedEvent.php +++ b/lib/Event/RegisterDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(Register $register) * * @return Register The register that was deleted * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getRegister(): Register { diff --git a/lib/Event/RegisterUpdatedEvent.php b/lib/Event/RegisterUpdatedEvent.php index c0fe3cd52a..37a24e98f9 100644 --- a/lib/Event/RegisterUpdatedEvent.php +++ b/lib/Event/RegisterUpdatedEvent.php @@ -66,7 +66,7 @@ public function __construct(Register $newRegister, Register $oldRegister) * * @return Register The register after update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewRegister(): Register { @@ -78,7 +78,7 @@ public function getNewRegister(): Register * * @return Register The register before update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldRegister(): Register { diff --git a/lib/Event/SchemaCreatedEvent.php b/lib/Event/SchemaCreatedEvent.php index e7993a72ca..50fc4928c6 100644 --- a/lib/Event/SchemaCreatedEvent.php +++ b/lib/Event/SchemaCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(Schema $schema) * * @return Schema The schema that was created * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getSchema(): Schema { diff --git a/lib/Event/SchemaDeletedEvent.php b/lib/Event/SchemaDeletedEvent.php index 8b2b331b8f..71c0169818 100644 --- a/lib/Event/SchemaDeletedEvent.php +++ b/lib/Event/SchemaDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(Schema $schema) * * @return Schema The schema that was deleted * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getSchema(): Schema { diff --git a/lib/Event/SchemaUpdatedEvent.php b/lib/Event/SchemaUpdatedEvent.php index 8d91210e17..ff1d896f46 100644 --- a/lib/Event/SchemaUpdatedEvent.php +++ b/lib/Event/SchemaUpdatedEvent.php @@ -66,7 +66,7 @@ public function __construct(Schema $newSchema, Schema $oldSchema) * * @return Schema The schema after update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewSchema(): Schema { @@ -78,7 +78,7 @@ public function getNewSchema(): Schema * * @return Schema The schema before update * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldSchema(): Schema { diff --git a/lib/Event/SourceCreatedEvent.php b/lib/Event/SourceCreatedEvent.php index 2be1e26133..cc2283d788 100644 --- a/lib/Event/SourceCreatedEvent.php +++ b/lib/Event/SourceCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(Source $source) * * @return Source The source that was created. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getSource(): Source { diff --git a/lib/Event/SourceDeletedEvent.php b/lib/Event/SourceDeletedEvent.php index 7f20d33170..f3069a571a 100644 --- a/lib/Event/SourceDeletedEvent.php +++ b/lib/Event/SourceDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(Source $source) * * @return Source The source that was deleted. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getSource(): Source { diff --git a/lib/Event/SourceUpdatedEvent.php b/lib/Event/SourceUpdatedEvent.php index dfeeb13a2f..a60ed20a5b 100644 --- a/lib/Event/SourceUpdatedEvent.php +++ b/lib/Event/SourceUpdatedEvent.php @@ -66,7 +66,7 @@ public function __construct(Source $newSource, Source $oldSource) * * @return Source The source after update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getSource(): Source { @@ -78,7 +78,7 @@ public function getSource(): Source * * @return Source The source after update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewSource(): Source { @@ -90,7 +90,7 @@ public function getNewSource(): Source * * @return Source The source before update. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldSource(): Source { diff --git a/lib/Event/ToolRegistrationEvent.php b/lib/Event/ToolRegistrationEvent.php index 731e76e46a..78c0b60c57 100644 --- a/lib/Event/ToolRegistrationEvent.php +++ b/lib/Event/ToolRegistrationEvent.php @@ -76,7 +76,7 @@ class ToolRegistrationEvent extends Event * * @param ToolRegistry $registry Tool registry to register tools with * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-1 + * @spec openspec/specs/ai-mcp/spec.md */ public function __construct(ToolRegistry $registry) { @@ -101,8 +101,8 @@ public function __construct(ToolRegistry $registry) * * @throws \InvalidArgumentException If validation fails * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events + * @spec openspec/specs/ai-mcp/spec.md */ public function registerTool(string $id, ToolInterface $tool, array $metadata): void { diff --git a/lib/Event/UserProfileUpdatedEvent.php b/lib/Event/UserProfileUpdatedEvent.php index c3a11217fa..89acdb6720 100644 --- a/lib/Event/UserProfileUpdatedEvent.php +++ b/lib/Event/UserProfileUpdatedEvent.php @@ -58,7 +58,7 @@ public function __construct( * * @return IUser The user object. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getUser(): IUser { @@ -70,7 +70,7 @@ public function getUser(): IUser * * @return string The user ID. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getUserId(): string { @@ -82,7 +82,7 @@ public function getUserId(): string * * @return array The old user data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getOldData(): array { @@ -94,7 +94,7 @@ public function getOldData(): array * * @return array The new user data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getNewData(): array { @@ -106,7 +106,7 @@ public function getNewData(): array * * @return array Array of field names that were changed. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getChanges(): array { @@ -120,7 +120,7 @@ public function getChanges(): array * * @return bool True if the field was changed, false otherwise. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function hasChanged(string $fieldName): bool { @@ -132,7 +132,7 @@ public function hasChanged(string $fieldName): bool * * @return bool True if firstName, lastName, middleName, or displayName was changed. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function hasNameChanges(): bool { diff --git a/lib/Event/ViewCreatedEvent.php b/lib/Event/ViewCreatedEvent.php index 6b46f012da..6f0c8df715 100644 --- a/lib/Event/ViewCreatedEvent.php +++ b/lib/Event/ViewCreatedEvent.php @@ -57,7 +57,7 @@ public function __construct(View $view) * * @return View The view that was created. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getView(): View { diff --git a/lib/Event/ViewDeletedEvent.php b/lib/Event/ViewDeletedEvent.php index de86fb092b..eeebf29b0b 100644 --- a/lib/Event/ViewDeletedEvent.php +++ b/lib/Event/ViewDeletedEvent.php @@ -57,7 +57,7 @@ public function __construct(View $view) * * @return View The view that was deleted. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-payloads-for-webhook-delivery-must-include-register-and-schema-context-for-object-events */ public function getView(): View { diff --git a/lib/Event/ViewUpdatedEvent.php b/lib/Event/ViewUpdatedEvent.php index 161d18eedb..25ae2cda1e 100644 --- a/lib/Event/ViewUpdatedEvent.php +++ b/lib/Event/ViewUpdatedEvent.php @@ -54,7 +54,7 @@ class ViewUpdatedEvent extends Event * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-event-all/tasks.md#task-4 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct(View $newView, View $oldView) { diff --git a/lib/EventListener/AbstractNodeFolderEventListener.php b/lib/EventListener/AbstractNodeFolderEventListener.php index 2b49a1a48f..f2d801be30 100644 --- a/lib/EventListener/AbstractNodeFolderEventListener.php +++ b/lib/EventListener/AbstractNodeFolderEventListener.php @@ -50,7 +50,7 @@ class AbstractNodeFolderEventListener implements IEventListener * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private readonly ObjectService $objectService, @@ -67,7 +67,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { @@ -98,7 +98,7 @@ public function handle(Event $event): void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function handleNodeCreated(NodeCreatedEvent $_event): void { @@ -117,7 +117,7 @@ private function handleNodeCreated(NodeCreatedEvent $_event): void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function handleNodeDeleted(NodeDeletedEvent $_event): void { @@ -136,7 +136,7 @@ private function handleNodeDeleted(NodeDeletedEvent $_event): void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function handleNodeTouched(NodeTouchedEvent $_event): void { @@ -155,7 +155,7 @@ private function handleNodeTouched(NodeTouchedEvent $_event): void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function handleNodeWritten(NodeWrittenEvent $_event): void { diff --git a/lib/EventListener/AbstractNodesFolderEventListener.php b/lib/EventListener/AbstractNodesFolderEventListener.php index 2c466e0753..d8bf981b1f 100644 --- a/lib/EventListener/AbstractNodesFolderEventListener.php +++ b/lib/EventListener/AbstractNodesFolderEventListener.php @@ -48,7 +48,7 @@ class AbstractNodesFolderEventListener implements IEventListener * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private readonly ObjectService $objectService, @@ -63,7 +63,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { @@ -94,7 +94,7 @@ public function handle(Event $event): void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function handleNodeCopied(NodeCopiedEvent $_event): void { @@ -110,7 +110,7 @@ private function handleNodeCopied(NodeCopiedEvent $_event): void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function handleNodeRenamed(NodeRenamedEvent $_event): void { diff --git a/lib/EventListener/SolrEventListener.php b/lib/EventListener/SolrEventListener.php deleted file mode 100644 index 8817bf6502..0000000000 --- a/lib/EventListener/SolrEventListener.php +++ /dev/null @@ -1,428 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.app - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\EventListener; - -use OCA\OpenRegister\Event\ObjectCreatedEvent; -use OCA\OpenRegister\Event\ObjectUpdatedEvent; -use OCA\OpenRegister\Event\ObjectDeletedEvent; -use OCA\OpenRegister\Event\SchemaCreatedEvent; -use OCA\OpenRegister\Event\SchemaUpdatedEvent; -use OCA\OpenRegister\Event\SchemaDeletedEvent; -use OCA\OpenRegister\Service\Object\CacheHandler; -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventListener; -use Psr\Log\LoggerInterface; - -/** - * Event listener for Solr indexing operations - * - * Automatically maintains Solr index consistency by responding to object - * lifecycle events (create, update, delete) and triggering appropriate - * Solr operations. - * - * @implements IEventListener - */ -class SolrEventListener implements IEventListener -{ - /** - * Constructor for SolrEventListener - * - * @param CacheHandler $cacheHandler Service for handling object caching and Solr operations - * @param LoggerInterface $logger Logger for debugging and monitoring - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - public function __construct( - private readonly CacheHandler $cacheHandler, - private readonly LoggerInterface $logger - ) { - }//end __construct() - - /** - * Handle incoming events and trigger appropriate Solr operations - * - * @param Event $event The event to handle - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - public function handle(Event $event): void - { - // DEBUG: Check if we're getting called at all. - $this->logger->debug( - message: '[SolrEventListener] SolrEventListener handling event', - context: ['file' => __FILE__, 'line' => __LINE__, 'event_type' => get_class($event)] - ); - - try { - if ($event instanceof ObjectCreatedEvent) { - $this->logger->debug( - message: '[SolrEventListener] === SOLR EVENT LISTENER DEBUG ===', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - $this->logger->debug( - message: '[SolrEventListener] Event: ObjectCreatedEvent', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - $this->logger->debug( - message: '[SolrEventListener] Object ID: '.$event->getObject()->getId(), - context: ['file' => __FILE__, 'line' => __LINE__] - ); - $this->logger->debug( - message: '[SolrEventListener] Object UUID: '.($event->getObject()->getUuid() ?? 'null'), - context: ['file' => __FILE__, 'line' => __LINE__] - ); - $this->logger->debug( - message: '[SolrEventListener] === END EVENT DEBUG ===', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - $this->logger->debug( - message: '[SolrEventListener] Handling ObjectCreatedEvent', - context: ['file' => __FILE__, 'line' => __LINE__, 'object_id' => $event->getObject()->getId()] - ); - $this->handleObjectCreated(event: $event); - return; - }//end if - - if ($event instanceof ObjectUpdatedEvent) { - $this->logger->debug( - message: '[SolrEventListener] Handling ObjectUpdatedEvent', - context: ['file' => __FILE__, 'line' => __LINE__, 'object_id' => $event->getNewObject()->getId()] - ); - $this->handleObjectUpdated(event: $event); - return; - } - - if ($event instanceof ObjectDeletedEvent) { - $this->logger->debug( - message: '[SolrEventListener] Handling ObjectDeletedEvent', - context: ['file' => __FILE__, 'line' => __LINE__, 'object_id' => $event->getObject()->getId()] - ); - $this->handleObjectDeleted(event: $event); - return; - } - - if ($event instanceof SchemaCreatedEvent) { - $this->handleSchemaCreated(event: $event); - return; - } - - if ($event instanceof SchemaUpdatedEvent) { - $this->handleSchemaUpdated(event: $event); - return; - } - - if ($event instanceof SchemaDeletedEvent) { - $this->handleSchemaDeleted(event: $event); - return; - } - - // Log unhandled events for debugging. - $this->logger->debug( - message: '[SolrEventListener] Received unhandled event', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'eventClass' => get_class($event), - 'app' => 'openregister', - ] - ); - } catch (\Exception $e) { - // Log errors but don't break the application flow. - $this->logger->error( - message: '[SolrEventListener] Error handling event', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'eventClass' => get_class($event), - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - 'app' => 'openregister', - ] - ); - }//end try - }//end handle() - - /** - * Handle object creation event - * - * @param ObjectCreatedEvent $event The object creation event - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 - */ - private function handleObjectCreated(ObjectCreatedEvent $event): void - { - $object = $event->getObject(); - - $this->logger->info( - message: '[SolrEventListener] Indexing newly created object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $object->getId(), - 'objectUuid' => $object->getUuid(), - 'objectName' => $object->getName(), - 'app' => 'openregister', - ] - ); - - // Trigger Solr indexing for the created object only if indexing is available. - try { - $this->cacheHandler->invalidateForObjectChange(object: $object, operation: 'create'); - } catch (\Exception $e) { - // If Solr/indexing is not configured, log and continue gracefully. - $this->logger->debug( - message: '[SolrEventListener] Indexing not available, skipping', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - } - }//end handleObjectCreated() - - /** - * Handle object update event - * - * @param ObjectUpdatedEvent $event The object update event - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 - */ - private function handleObjectUpdated(ObjectUpdatedEvent $event): void - { - $newObject = $event->getNewObject(); - $oldObject = $event->getOldObject(); - - $this->logger->info( - message: '[SolrEventListener] Reindexing updated object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $newObject->getId(), - 'objectUuid' => $newObject->getUuid(), - 'objectName' => $newObject->getName(), - 'oldObjectName' => $oldObject?->getName(), - 'app' => 'openregister', - ] - ); - - // Trigger Solr reindexing for the updated object only if indexing is available. - try { - $this->cacheHandler->invalidateForObjectChange(object: $newObject, operation: 'update'); - } catch (\Exception $e) { - // If Solr/indexing is not configured, log and continue gracefully. - $this->logger->debug( - message: '[SolrEventListener] Indexing not available, skipping', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - } - }//end handleObjectUpdated() - - /** - * Handle object deletion event - * - * @param ObjectDeletedEvent $event The object deletion event - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 - */ - private function handleObjectDeleted(ObjectDeletedEvent $event): void - { - $object = $event->getObject(); - - $this->logger->info( - message: '[SolrEventListener] Removing deleted object from index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $object->getId(), - 'objectUuid' => $object->getUuid(), - 'objectName' => $object->getName(), - 'app' => 'openregister', - ] - ); - - // Trigger Solr removal for the deleted object only if indexing is available. - try { - $this->cacheHandler->invalidateForObjectChange(object: $object, operation: 'delete'); - } catch (\Exception $e) { - // If Solr/indexing is not configured, log and continue gracefully. - $this->logger->debug( - message: '[SolrEventListener] Indexing not available, skipping', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - } - }//end handleObjectDeleted() - - /** - * Handle schema creation event - * - * @param SchemaCreatedEvent $event The schema creation event - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 - */ - private function handleSchemaCreated(SchemaCreatedEvent $event): void - { - $schema = $event->getSchema(); - - $this->logger->info( - message: '[SolrEventListener] Schema created, updating Solr field mappings', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaId' => $schema->getId(), - 'schemaTitle' => $schema->getTitle(), - 'app' => 'openregister', - ] - ); - - // Schema creation might require Solr field mapping updates. - // This could trigger a reindex of objects using this schema. - $this->triggerSchemaReindex(schemaId: $schema->getId()); - }//end handleSchemaCreated() - - /** - * Handle schema update event - * - * @param SchemaUpdatedEvent $event The schema update event - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 - */ - private function handleSchemaUpdated(SchemaUpdatedEvent $event): void - { - $newSchema = $event->getNewSchema(); - $oldSchema = $event->getOldSchema(); - - $this->logger->info( - message: '[SolrEventListener] Schema updated, checking for field mapping changes', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaId' => $newSchema->getId(), - 'schemaTitle' => $newSchema->getTitle(), - 'app' => 'openregister', - ] - ); - - // Compare schema properties to see if field mappings changed. - if ($this->schemaFieldsChanged(oldSchema: $oldSchema, newSchema: $newSchema) === true) { - $this->logger->info( - message: '[SolrEventListener] Schema fields changed, triggering reindex', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaId' => $newSchema->getId(), - 'app' => 'openregister', - ] - ); - - // Trigger reindex of all objects using this schema. - $this->triggerSchemaReindex(schemaId: $newSchema->getId()); - // End if. - } - }//end handleSchemaUpdated() - - /** - * Handle schema deletion event - * - * @param SchemaDeletedEvent $event The schema deletion event - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-27 - */ - private function handleSchemaDeleted(SchemaDeletedEvent $event): void - { - $schema = $event->getSchema(); - - $this->logger->info( - message: '[SolrEventListener] Schema deleted, cleaning up Solr entries', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaId' => $schema->getId(), - 'schemaTitle' => $schema->getTitle(), - 'app' => 'openregister', - ] - ); - - // When a schema is deleted, we should remove all objects using this schema from Solr. - // This is handled automatically when objects are deleted, but we log it for tracking. - }//end handleSchemaDeleted() - - /** - * Check if schema fields changed between versions - * - * @param \OCA\OpenRegister\Db\Schema $oldSchema Old schema version - * @param \OCA\OpenRegister\Db\Schema $newSchema New schema version - * - * @return bool True if fields changed and reindex is needed - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function schemaFieldsChanged($oldSchema, $newSchema): bool - { - // Compare the properties JSON to detect field changes. - $oldProperties = $oldSchema->getProperties(); - $newProperties = $newSchema->getProperties(); - - return $oldProperties !== $newProperties; - }//end schemaFieldsChanged() - - /** - * Trigger reindex of all objects using a specific schema - * - * @param int $schemaId Schema ID to reindex - * - * @return void - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 - */ - private function triggerSchemaReindex(int $schemaId): void - { - // This could be implemented to trigger a background job. - // For reindexing all objects with the updated schema. - $this->logger->info( - message: '[SolrEventListener] Schema reindex requested', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaId' => $schemaId, - 'app' => 'openregister', - 'note' => 'Background reindex should be implemented here', - ] - ); - }//end triggerSchemaReindex() -}//end class diff --git a/lib/Exception/AuthenticationException.php b/lib/Exception/AuthenticationException.php index 8c0bf25c6a..60ff087e6a 100644 --- a/lib/Exception/AuthenticationException.php +++ b/lib/Exception/AuthenticationException.php @@ -9,9 +9,9 @@ * @category Exception * @package OCA\OpenRegister\Exception * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -41,7 +41,7 @@ class AuthenticationException extends Exception * @param string $message A human-readable error message * @param array $details Structured details about the failure * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct(string $message, array $details) { @@ -55,7 +55,7 @@ public function __construct(string $message, array $details) * * @return array The details array. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getDetails(): array { diff --git a/lib/Exception/AuthorizationUnresolvableException.php b/lib/Exception/AuthorizationUnresolvableException.php new file mode 100644 index 0000000000..d68d9f763c --- /dev/null +++ b/lib/Exception/AuthorizationUnresolvableException.php @@ -0,0 +1,41 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +use Exception; + +/** + * Raised when effective authorization cannot be resolved. Always means DENY. + */ +class AuthorizationUnresolvableException extends Exception +{ +}//end class diff --git a/lib/Exception/BreakingSchemaChangeException.php b/lib/Exception/BreakingSchemaChangeException.php new file mode 100644 index 0000000000..19991df606 --- /dev/null +++ b/lib/Exception/BreakingSchemaChangeException.php @@ -0,0 +1,98 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +use Exception; + +/** + * Signals an unacknowledged breaking schema change. + */ +class BreakingSchemaChangeException extends Exception +{ + + /** + * The typed change list that triggered the gate. + * + * @var array> + */ + private array $changes; + + /** + * Count of objects invalid under the latest revalidation (or null). + * + * @var integer|null + */ + private ?int $invalidCount; + + /** + * Constructor. + * + * @param array> $changes The typed change list. + * @param int|null $invalidCount Latest invalid-object count, or null. + */ + public function __construct(array $changes, ?int $invalidCount=null) + { + parent::__construct(message: 'Schema change classified breaking; acknowledgeBreaking required.'); + $this->changes = $changes; + $this->invalidCount = $invalidCount; + + }//end __construct() + + /** + * Get the structured 409 response body. + * + * @return array The response body. + */ + public function toResponse(): array + { + $body = [ + 'error' => $this->getMessage(), + 'classification' => 'breaking', + 'changes' => $this->changes, + ]; + + if ($this->invalidCount !== null) { + $body['invalidCount'] = $this->invalidCount; + } + + return $body; + + }//end toResponse() + + /** + * Get the typed change list. + * + * @return array> The changes. + */ + public function getChanges(): array + { + return $this->changes; + + }//end getChanges() +}//end class diff --git a/lib/Exception/ChunkMatcherException.php b/lib/Exception/ChunkMatcherException.php new file mode 100644 index 0000000000..03257e6587 --- /dev/null +++ b/lib/Exception/ChunkMatcherException.php @@ -0,0 +1,80 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/entity-relation-grondslagen/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +use Exception; + +/** + * Typed reason codes for `ChunkTextMatcher::match` failures. + * + * The class extends `\Exception` directly (not any Nextcloud-specific + * exception) so generic `catch (NotPermittedException)` blocks in the + * controller layer cannot accidentally absorb a matcher error. + */ +class ChunkMatcherException extends Exception +{ + /** + * The needle is longer than the configured chunk overlap, so + * per-chunk regex cannot reliably find matches that straddle a + * boundary. The caller MUST reject the input. + */ + public const REASON_VALUE_TOO_LONG = 'value_too_long'; + + /** + * The regex pattern compiled from the needle failed to compile. + * Realistic trigger: malformed Unicode (lone surrogate, invalid + * UTF-8 byte sequence) in the operator-supplied value. + */ + public const REASON_REGEX_COMPILE_FAILURE = 'regex_compile_failure'; + + /** + * Constructor. + * + * @param string $reason One of the `REASON_*` constants above. + * @param string $message Human-readable description. MUST NOT contain the operator-supplied + * needle per ADR-005 — only structural / size information. + */ + public function __construct( + private readonly string $reason, + string $message='' + ) { + parent::__construct(message: $message); + + }//end __construct() + + /** + * Get the typed reason code. + * + * @return string One of the `REASON_*` constants. + */ + public function getReason(): string + { + return $this->reason; + + }//end getReason() +}//end class diff --git a/lib/Exception/CircularReferenceException.php b/lib/Exception/CircularReferenceException.php index 393823a34f..2c0865de17 100644 --- a/lib/Exception/CircularReferenceException.php +++ b/lib/Exception/CircularReferenceException.php @@ -19,13 +19,13 @@ * @category Exception * @package OCA\OpenRegister\Exception * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app * - * @spec openspec/changes/reference-existence-validation/tasks.md + * @spec openspec/specs/reference-existence-validation/spec.md */ declare(strict_types=1); diff --git a/lib/Exception/CustomValidationException.php b/lib/Exception/CustomValidationException.php index 3ab645a831..1c1d55cab9 100644 --- a/lib/Exception/CustomValidationException.php +++ b/lib/Exception/CustomValidationException.php @@ -69,7 +69,7 @@ class CustomValidationException extends Exception * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct(string $message, array $errors) { @@ -88,7 +88,7 @@ public function __construct(string $message, array $errors) * * @return array> The errors array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getErrors(): array { diff --git a/lib/Exception/DatabaseConstraintException.php b/lib/Exception/DatabaseConstraintException.php index c00ca215b1..a940bc8d14 100644 --- a/lib/Exception/DatabaseConstraintException.php +++ b/lib/Exception/DatabaseConstraintException.php @@ -64,7 +64,7 @@ class DatabaseConstraintException extends Exception * @param int $httpStatus The HTTP status code (default: 409 Conflict) * @param Exception|null $previous The previous exception * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct(string $message, int $code=0, int $httpStatus=409, ?Exception $previous=null) { @@ -80,7 +80,7 @@ public function __construct(string $message, int $code=0, int $httpStatus=409, ? * * @return int The HTTP status code (typically 409 for constraint violations) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getHttpStatusCode(): int { @@ -100,7 +100,7 @@ public function getHttpStatusCode(): int * * @return DatabaseConstraintException The user-friendly exception with parsed message * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public static function fromDatabaseException( Exception $dbException, @@ -132,7 +132,7 @@ public static function fromDatabaseException( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Constraint parsing requires many conditional error type checks * @SuppressWarnings(PHPMD.NPathComplexity) Constraint parsing requires many conditional error type checks * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ private static function parseConstraintError(string $dbMessage, string $entityType): string { diff --git a/lib/Exception/EmlParseException.php b/lib/Exception/EmlParseException.php index 127272c250..da8625faea 100644 --- a/lib/Exception/EmlParseException.php +++ b/lib/Exception/EmlParseException.php @@ -22,7 +22,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/text-extraction-eml/specs/text-extraction-eml/spec.md + * @spec openspec/specs/text-extraction-eml/spec.md * "Malformed input MUST NOT throw from `extractEml`; `parseEmlStructured` MUST throw a typed exception" */ diff --git a/lib/Exception/EncryptedFieldFilterException.php b/lib/Exception/EncryptedFieldFilterException.php new file mode 100644 index 0000000000..e136ca0ac6 --- /dev/null +++ b/lib/Exception/EncryptedFieldFilterException.php @@ -0,0 +1,86 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Exception; + +use Exception; +use Throwable; + +/** + * Exception thrown when a search/filter/facet request targets an encrypted property. + * + * An encrypted field's magic-table value is opaque ciphertext (when a column + * exists for it at all — see design.md, encrypted properties get no dedicated + * magic-table column), so a plaintext filter against it can never match + * correctly. Rather than silently returning zero rows (indistinguishable from + * "no matches" — the exact anti-pattern flagged for this feature), OpenRegister + * fails loud with this exception, translated to HTTP 400 by + * {@see \OCA\OpenRegister\Middleware\EncryptedFieldFilterMiddleware}. + * + * @category Exception + * @package OCA\OpenRegister\Exception + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ +class EncryptedFieldFilterException extends Exception +{ + /** + * Constructor for EncryptedFieldFilterException. + * + * @param string $property The encrypted property name that was targeted. + * @param int $code The error code (default: 400). + * @param Throwable|null $previous The previous exception that caused this one. + * + * @return void + */ + public function __construct( + private readonly string $property, + int $code=400, + ?Throwable $previous=null + ) { + parent::__construct( + message: sprintf( + 'Property "%s" is encrypted at rest and cannot be filtered, searched or faceted server-side.', + $property + ), + code: $code, + previous: $previous + ); + }//end __construct() + + /** + * The encrypted property name that triggered this exception. + * + * @return string The property name. + */ + public function getProperty(): string + { + return $this->property; + }//end getProperty() +}//end class diff --git a/lib/Exception/ExportTooLargeException.php b/lib/Exception/ExportTooLargeException.php new file mode 100644 index 0000000000..930c61da11 --- /dev/null +++ b/lib/Exception/ExportTooLargeException.php @@ -0,0 +1,118 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://OpenRegister.app + * + * @spec openspec/specs/export-pdf-format/spec.md + */ + +namespace OCA\OpenRegister\Exception; + +use Exception; + +/** + * Exception thrown when a PDF export's row count exceeds the configured cap. + * + * PDF rendering builds a full in-memory box-tree per row (Dompdf), unlike the + * streaming CSV writer or PhpSpreadsheet's XLSX writer, making it meaningfully + * more memory-heavy per row. `ExportService::exportToPdf()` throws this + * exception before any HTML construction or Dompdf rendering begins, so no + * wasted render work happens once the cap is exceeded. + * + * Controllers exposing PDF export MUST catch this exception specifically and + * map it to HTTP 400 with a structured body identifying the actual row count + * and the configured limit — never a bare 500, and never a silently truncated + * PDF. + * + * @category Exception + * @package OCA\OpenRegister\Exception + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ +class ExportTooLargeException extends Exception +{ + + /** + * The HTTP status code controllers MUST map this exception to. + * + * @var int + */ + public const HTTP_STATUS = 400; + + /** + * The actual number of objects the export attempted to render. + * + * @var integer + */ + private int $rowCount; + + /** + * The configured row-count limit that was exceeded. + * + * @var integer + */ + private int $maxRows; + + /** + * ExportTooLargeException constructor. + * + * @param int $rowCount The actual number of objects the export attempted to render. + * @param int $maxRows The configured row-count limit that was exceeded. + * @param Exception|null $previous The previous exception that caused this one, if any. + */ + public function __construct(int $rowCount, int $maxRows, ?Exception $previous=null) + { + $this->rowCount = $rowCount; + $this->maxRows = $maxRows; + + $message = sprintf( + 'PDF export row count (%d) exceeds the maximum allowed (%d). ' + .'Narrow the export with filters or use CSV/Excel export instead.', + $rowCount, + $maxRows + ); + parent::__construct(message: $message, code: 0, previous: $previous); + + }//end __construct() + + /** + * Get the actual number of objects the export attempted to render. + * + * @return int + */ + public function getRowCount(): int + { + return $this->rowCount; + + }//end getRowCount() + + /** + * Get the configured row-count limit that was exceeded. + * + * @return int + */ + public function getMaxRows(): int + { + return $this->maxRows; + + }//end getMaxRows() +}//end class diff --git a/lib/Exception/FieldDecryptionException.php b/lib/Exception/FieldDecryptionException.php new file mode 100644 index 0000000000..f160dbed15 --- /dev/null +++ b/lib/Exception/FieldDecryptionException.php @@ -0,0 +1,70 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Exception; + +use Exception; +use Throwable; + +/** + * Exception thrown when an encrypted field value cannot be decrypted. + * + * Raised by {@see \OCA\OpenRegister\Service\FieldEncryptionHandler} when a + * value tagged as an OpenRegister encryption envelope fails to decrypt — + * typically because the Nextcloud instance secret used to derive the + * encryption key is missing or has rotated, or the stored ciphertext is + * corrupted. Never swallowed silently: the fleet lesson (see the + * orphaned-capability defect class) is that a swallowed catch here is + * indistinguishable from a healthy app with a dead feature. Callers either + * surface a structured error marker (read path, per-field, non-fatal) or + * rethrow (migration/CLI path, fails the operation loudly). + * + * @category Exception + * @package OCA\OpenRegister\Exception + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ +class FieldDecryptionException extends Exception +{ + /** + * Constructor for FieldDecryptionException + * + * @param string $message The error message describing the decryption failure. + * @param int $code The error code (default: 0). + * @param Throwable|null $previous The previous exception that caused this one. + * + * @return void + */ + public function __construct( + string $message, + int $code=0, + ?Throwable $previous=null + ) { + parent::__construct(message: $message, code: $code, previous: $previous); + }//end __construct() +}//end class diff --git a/lib/Exception/FolderAccessDeniedException.php b/lib/Exception/FolderAccessDeniedException.php new file mode 100644 index 0000000000..49cc710385 --- /dev/null +++ b/lib/Exception/FolderAccessDeniedException.php @@ -0,0 +1,119 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://OpenRegister.app + * + * @spec openspec/specs/self-folder-access-control/spec.md + */ + +namespace OCA\OpenRegister\Exception; + +use Exception; + +/** + * Exception thrown when a `@self.folder` bind is denied. + * + * Raised by `FolderManagementHandler::createObjectFolderById()` when: + * - the supplied folder ID does not resolve in the acting user's user-folder mount, + * - the resolved node is not a `Folder` (e.g. a file ID was supplied), + * - the resolved folder is not readable by the acting user (`Folder::isReadable() === false`). + * + * Controllers MUST catch this exception specifically (not generic `\Exception`) + * and map it to HTTP 403 with the structured body `{"error": "folder_access_denied"}`. + * + * **The response body MUST NOT echo the attempted folder ID.** Including it + * would re-create the enumeration oracle the `self-folder-access-control` + * capability spec was written to close: a caller probing `@self.folder` with + * sequential integers could distinguish "folder exists but I can't read it" + * (403 + id) from "folder does not exist" (auto-create / no-op) just by + * inspecting the response shape. The attempted id remains available via + * `getAttemptedFolderId()` for server-side audit logging only — see + * `ObjectsController::folderAccessDeniedResponse()` for the canonical + * controller-side mapping. + * + * The class extends `\Exception` directly — NOT `OCP\Files\NotPermittedException` + * or any other Nextcloud exception — so generic catch-blocks for those exceptions + * do not silently absorb a denial. + * + * @category Exception + * @package OCA\OpenRegister\Exception + * + * @author Conduction Development Team + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @phpstan-consistent-constructor + */ +class FolderAccessDeniedException extends Exception +{ + + /** + * The HTTP status code controllers MUST map this exception to. + * + * Exposed as a class constant rather than as the exception's `code` + * value because `Exception::getCode()` is an application-level error + * code, not an HTTP status — conflating the two is brittle (future + * callers using `getCode()` for routing decisions get an HTTP number + * instead of a domain error number). Controllers reading this constant + * remain explicit about the HTTP mapping happening at the controller + * layer; the exception itself stays HTTP-agnostic. + * + * @var int + */ + public const HTTP_STATUS = 403; + + /** + * The folder ID the caller attempted to bind to. + * + * @var string + */ + private string $attemptedFolderId; + + /** + * FolderAccessDeniedException constructor. + * + * @param string $attemptedFolderId The folder ID the caller attempted to bind to. + * @param int $code Application error code; 0 by default. Controllers + * MUST use `self::HTTP_STATUS` for the HTTP mapping, + * not this code. + * @param Exception|null $previous The previous exception that caused this one, if any. + */ + public function __construct(string $attemptedFolderId, int $code=0, ?Exception $previous=null) + { + $this->attemptedFolderId = $attemptedFolderId; + + $message = "Access to folder '".$attemptedFolderId."' is denied for the acting user."; + parent::__construct(message: $message, code: $code, previous: $previous); + + }//end __construct() + + /** + * Get the folder ID the caller attempted to bind to. + * + * **Server-side use only** — for audit-trail entries and structured + * log lines. MUST NOT be included in the HTTP response body (see the + * class docblock for the enumeration-oracle rationale). + * + * @return string + */ + public function getAttemptedFolderId(): string + { + return $this->attemptedFolderId; + + }//end getAttemptedFolderId() +}//end class diff --git a/lib/Exception/HandoffException.php b/lib/Exception/HandoffException.php new file mode 100644 index 0000000000..3c7e23707c --- /dev/null +++ b/lib/Exception/HandoffException.php @@ -0,0 +1,97 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +/** + * Typed handoff-engine failure with a machine-readable error code. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Graceful degradation when no provider implements the kind) + */ +class HandoffException extends \RuntimeException +{ + + /** + * The source schema declares no handoff with the requested id (404-class). + * + * @var string + */ + public const NOT_DECLARED = 'handoff-not-declared'; + + /** + * No installed schema provides the target kind (hide mode; 409-class, + * never a 5xx). + * + * @var string + */ + public const PROVIDER_UNAVAILABLE = 'handoff-provider-unavailable'; + + /** + * Machine-readable error code (one of the class constants). + * + * @var string + */ + private string $errorCode; + + /** + * Constructor. + * + * @param string $errorCode One of the class constants. + * @param string $message Human-readable explanation. + * @param \Throwable|null $previous Optional wrapped throwable. + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, hide mode) + */ + public function __construct(string $errorCode, string $message, ?\Throwable $previous=null) + { + parent::__construct(message: $message, code: 0, previous: $previous); + $this->errorCode = $errorCode; + + }//end __construct() + + /** + * The machine-readable error code. + * + * @return string One of the class constants. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, hide mode) + */ + public function getErrorCode(): string + { + return $this->errorCode; + + }//end getErrorCode() +}//end class diff --git a/lib/Exception/ManualEntityException.php b/lib/Exception/ManualEntityException.php new file mode 100644 index 0000000000..4305808c62 --- /dev/null +++ b/lib/Exception/ManualEntityException.php @@ -0,0 +1,104 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/entity-relation-grondslagen/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +use Exception; +use Throwable; + +/** + * Typed reason codes for orchestration-layer failures. + */ +class ManualEntityException extends Exception +{ + /** + * The file has no chunks yet — text extraction was never run, or + * was run and produced zero chunks. The caller must run extraction + * first. + */ + public const REASON_FILE_NOT_EXTRACTED = 'file_not_extracted'; + + /** + * The supplied entity type isn't in the catalogue's recognised set. + */ + public const REASON_UNSUPPORTED_ENTITY_TYPE = 'unsupported_entity_type'; + + /** + * The operator-supplied value couldn't be compiled into a regex + * pattern (malformed Unicode). + */ + public const REASON_REGEX_COMPILE_FAILURE = 'regex_compile_failure'; + + /** + * The acting user lacks write access to the target file (read-only + * node, or not an updateable file). Mapped to HTTP 403. + */ + public const REASON_FORBIDDEN = 'forbidden'; + + /** + * Catch-all for unexpected runtime failures (DB error, + * audit-write rollback, etc.). + */ + public const REASON_INTERNAL_ERROR = 'internal_error'; + + /** + * Constructor. + * + * @param string $reason One of the `REASON_*` constants above. + * @param string $message Human-readable description. MUST NOT contain the + * operator-supplied `value` per ADR-005. + * @param Throwable|null $previous Wrapped lower-layer exception (DB error etc.). + */ + public function __construct( + private readonly string $reason, + string $message='', + ?Throwable $previous=null + ) { + parent::__construct(message: $message, code: 0, previous: $previous); + + }//end __construct() + + /** + * Get the typed reason code. + * + * @return string One of the `REASON_*` constants. + */ + public function getReason(): string + { + return $this->reason; + + }//end getReason() +}//end class diff --git a/lib/Exception/PdfAnonymisationException.php b/lib/Exception/PdfAnonymisationException.php new file mode 100644 index 0000000000..3821af155a --- /dev/null +++ b/lib/Exception/PdfAnonymisationException.php @@ -0,0 +1,182 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +use Throwable; + +/** + * Exception thrown by the PDF anonymisation pipeline. + * + * The `reason` field is a stable, structured code that the controller layer + * maps to an HTTP status: + * + * - `REASON_ENCRYPTED_PDF` → HTTP 422 (caller-correctable) + * - `REASON_TEXT_LAYER_MISSING` → defer to `ocr-document-scanning` + * - `REASON_VALIDATION_FAILED` → HTTP 500 (pipeline integrity failure) + * - `REASON_INTERNAL_ERROR` → HTTP 500 (unexpected pipeline failure) + * + * `$diagnostic` is a free-form array used for error logging and debugging. + * Per ADR-005 it MUST NOT contain operator-supplied entity text — only + * structural information (entity count, font names encountered, filter + * chains decoded, etc.). + * + * @category Exception + * @package OCA\OpenRegister\Exception + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ +class PdfAnonymisationException extends \Exception +{ + + /** + * Encrypted PDF input — anonymising a password-protected PDF requires + * the password and is out of scope for v1. Controller maps to HTTP 422. + * + * @var string + */ + public const REASON_ENCRYPTED_PDF = 'encrypted_pdf'; + + /** + * Input PDF has no extractable text layer (image-only scan). Caller + * SHOULD defer to the `ocr-document-scanning` capability rather than + * surfacing as a hard error. + * + * @var string + */ + public const REASON_TEXT_LAYER_MISSING = 'text_layer_missing'; + + /** + * Post-replacement validation gate detected residual entity text in + * the output PDF. The output is discarded; the gate fails closed. + * Controller maps to HTTP 500 with a structured diagnostic body. + * + * @var string + */ + public const REASON_VALIDATION_FAILED = 'validation_failed'; + + /** + * Catch-all for unexpected pipeline failures (SAPP errors, malformed + * input, etc.). Controller maps to HTTP 500. + * + * @var string + */ + public const REASON_INTERNAL_ERROR = 'internal_error'; + + /** + * Path A's strict-mode validation gate failed AND the dormant Path B + * (NC Office ODT round-trip — `pdf-anonymisation-odt-fallback`) was + * attempted but also failed to produce a clean output. Controller maps + * to HTTP 500 with the structured diagnostic body, identical to + * REASON_VALIDATION_FAILED's mapping. + * + * Only raised when `pdf-anonymisation.path-b-enabled` is true AND a + * Path B attempt was made. Until the feature flag flips on this reason + * code is reserved (no caller produces it). + * + * @var string + */ + public const REASON_VALIDATION_FAILED_AFTER_FALLBACK = 'validation_failed_after_fallback'; + + /** + * The structured reason code (one of the REASON_* constants). + * + * @var string + */ + private readonly string $reason; + + /** + * Free-form structured diagnostic surface, safe for logging. + * + * MUST NOT contain operator-supplied entity text (PII). + * + * @var array + */ + private readonly array $diagnostic; + + /** + * Constructor. + * + * @param string $reason One of the REASON_* constants + * @param string $message Human-readable error message (PII-free) + * @param array $diagnostic Structural diagnostic info; MUST NOT contain PII + * @param Throwable|null $previous Previous exception + * + * @phpstan-param array $diagnostic + * @psalm-param array $diagnostic + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + public function __construct( + string $reason, + string $message='', + array $diagnostic=[], + ?Throwable $previous=null + ) { + $this->reason = $reason; + $this->diagnostic = $diagnostic; + + $finalMessage = $message; + if ($message === '') { + $finalMessage = sprintf('PDF anonymisation failed: %s', $reason); + } + + parent::__construct(message: $finalMessage, code: 0, previous: $previous); + }//end __construct() + + /** + * Get the structured reason code. + * + * @return string One of the REASON_* constants + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + public function getReason(): string + { + return $this->reason; + }//end getReason() + + /** + * Get the structural diagnostic surface (PII-free). + * + * @return array + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + public function getDiagnostic(): array + { + return $this->diagnostic; + }//end getDiagnostic() +}//end class diff --git a/lib/Exception/ReferenceValidationException.php b/lib/Exception/ReferenceValidationException.php index 100d5e339b..3af3c9f5b5 100644 --- a/lib/Exception/ReferenceValidationException.php +++ b/lib/Exception/ReferenceValidationException.php @@ -19,9 +19,9 @@ * @category Exception * @package OCA\OpenRegister\Exception * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Exception/ReferentialIntegrityException.php b/lib/Exception/ReferentialIntegrityException.php index 5ed923c415..a175785899 100644 --- a/lib/Exception/ReferentialIntegrityException.php +++ b/lib/Exception/ReferentialIntegrityException.php @@ -51,7 +51,7 @@ class ReferentialIntegrityException extends Exception * @param int $code The error code. * @param Exception|null $previous The previous exception. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct(DeletionAnalysis $analysis, int $code=0, ?Exception $previous=null) { @@ -67,7 +67,7 @@ public function __construct(DeletionAnalysis $analysis, int $code=0, ?Exception * * @return DeletionAnalysis The analysis containing blocker and target details. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getAnalysis(): DeletionAnalysis { @@ -79,7 +79,7 @@ public function getAnalysis(): DeletionAnalysis * * @return array The structured error response with error code, message, and blockers. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function toResponseBody(): array { diff --git a/lib/Exception/SanitizationException.php b/lib/Exception/SanitizationException.php new file mode 100644 index 0000000000..95a4f0c15b --- /dev/null +++ b/lib/Exception/SanitizationException.php @@ -0,0 +1,135 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +use Throwable; + +/** + * Exception thrown by the Office document sanitiser. + * + * The `reason` field is a stable, structured code: + * + * - `REASON_UNSUPPORTED_MIME` → no strategy supports the file MIME type. + * - `REASON_ENCRYPTED` → the ZIP container is password-protected. + * - `REASON_CORRUPT_ZIP` → the ZIP container could not be opened. + * - `REASON_INTERNAL` → unexpected surgery failure. + * + * Per ADR-005 the message MUST NOT contain a filename or document content — + * only the reason code and structural detail (part path, element name). + * + * @category Exception + * @package OCA\OpenRegister\Exception + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ +class SanitizationException extends \Exception +{ + + /** + * No registered strategy supports the input MIME type. + * + * @var string + */ + public const REASON_UNSUPPORTED_MIME = 'unsupported-mime'; + + /** + * The ZIP container is encrypted / password-protected. + * + * @var string + */ + public const REASON_ENCRYPTED = 'encrypted'; + + /** + * The ZIP container is corrupt or not a valid Office package. + * + * @var string + */ + public const REASON_CORRUPT_ZIP = 'corrupt-zip'; + + /** + * Unexpected internal sanitiser failure. + * + * @var string + */ + public const REASON_INTERNAL = 'internal'; + + /** + * The structured reason code (one of the REASON_* constants). + * + * MUST NOT carry filename or content (ADR-005). + * + * @var string + */ + public readonly string $reason; + + /** + * Constructor. + * + * @param string $reason One of the REASON_* constants. + * @param string $message PII-free human-readable detail. + * @param Throwable|null $previous Previous exception. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function __construct( + string $reason, + string $message='', + ?Throwable $previous=null + ) { + $this->reason = $reason; + + $finalMessage = $message; + if ($finalMessage === '') { + $finalMessage = sprintf('Office document sanitisation failed: %s', $reason); + } + + parent::__construct(message: $finalMessage, code: 0, previous: $previous); + }//end __construct() + + /** + * Get the structured reason code. + * + * @return string One of the REASON_* constants. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function getReason(): string + { + return $this->reason; + }//end getReason() +}//end class diff --git a/lib/Exception/SchemaImportException.php b/lib/Exception/SchemaImportException.php new file mode 100644 index 0000000000..b5096644c1 --- /dev/null +++ b/lib/Exception/SchemaImportException.php @@ -0,0 +1,96 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +use RuntimeException; +use Throwable; + +/** + * Signals a failed standards import, with an HTTP status hint. + */ +class SchemaImportException extends RuntimeException +{ + /** + * Constructor. + * + * @param string $message The human-readable error. + * @param int $httpStatus The HTTP status the controller should return (default 400). + * @param Throwable|null $previous The previous throwable, if any. + */ + public function __construct( + string $message, + private readonly int $httpStatus=400, + ?Throwable $previous=null + ) { + parent::__construct(message: $message, code: 0, previous: $previous); + }//end __construct() + + /** + * The HTTP status hint for this failure. + * + * @return int The HTTP status code. + */ + public function getHttpStatus(): int + { + return $this->httpStatus; + }//end getHttpStatus() + + /** + * Build a 404 "unknown reference" exception. + * + * @param string $reference The unresolved reference. + * @param string $dialect The dialect that was searched. + * + * @return self The exception. + */ + public static function unknownReference(string $reference, string $dialect): self + { + return new self( + sprintf('Unknown %s reference: %s', $dialect, $reference), + 404 + ); + }//end unknownReference() + + /** + * Build a 422 "undetectable / unsupported dialect" exception. + * + * @param array $supported The supported dialect keys. + * + * @return self The exception. + */ + public static function undetectableDialect(array $supported): self + { + return new self( + 'Could not determine the schema dialect of the uploaded document. ' + .'Pass an explicit "dialect" parameter. Supported dialects: ' + .implode(', ', $supported).'.', + 422 + ); + }//end undetectableDialect() +}//end class diff --git a/lib/Exception/SchemaRunConcurrencyException.php b/lib/Exception/SchemaRunConcurrencyException.php new file mode 100644 index 0000000000..573f6852fa --- /dev/null +++ b/lib/Exception/SchemaRunConcurrencyException.php @@ -0,0 +1,36 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +use RuntimeException; + +/** + * Signals a concurrent-run conflict on a schema. + */ +class SchemaRunConcurrencyException extends RuntimeException +{ + +}//end class diff --git a/lib/Exception/TranslationTargetConflictException.php b/lib/Exception/TranslationTargetConflictException.php new file mode 100644 index 0000000000..c2308572a3 --- /dev/null +++ b/lib/Exception/TranslationTargetConflictException.php @@ -0,0 +1,139 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Exception; + +/** + * Thrown by `TranslationHandler::normalizeTranslationsForSave` when a + * language-keyed body collides with an `X-Translation-Target-Language` + * header on the same request. + * + * Controllers catch this and return `400 Bad Request` with a body of: + * + * ```json + * { + * "error": { + * "code": "TRANSLATION_TARGET_CONFLICT", + * "property": "title", + * "targetLanguage": "en" + * } + * } + * ``` + */ +class TranslationTargetConflictException extends CustomValidationException +{ + + /** + * Structured error code, exposed on the response body. + */ + public const ERROR_CODE = 'TRANSLATION_TARGET_CONFLICT'; + + /** + * The translatable property whose value conflicted with the header. + * + * @var string + */ + private readonly string $property; + + /** + * The BCP-47 target language read from the header. + * + * @var string + */ + private readonly string $targetLanguage; + + /** + * Construct a target-language conflict exception. + * + * Extends `CustomValidationException` so the existing controller + * `catch (ValidationException | CustomValidationException $e)` + * blocks pick it up as a `400 Bad Request` without any new + * controller code. + * + * @param string $property The translatable property name. + * @param string $targetLanguage The BCP-47 target language tag. + */ + public function __construct(string $property, string $targetLanguage) + { + $this->property = $property; + $this->targetLanguage = $targetLanguage; + + parent::__construct( + message: sprintf( + 'Cannot mix language-keyed body for "%s" with X-Translation-Target-Language: %s', + $property, + $targetLanguage + ), + errors: [ + 'code' => self::ERROR_CODE, + 'property' => $property, + 'targetLanguage' => $targetLanguage, + ] + ); + }//end __construct() + + /** + * Get the property whose value conflicted. + * + * @return string The translatable property name. + */ + public function getProperty(): string + { + return $this->property; + }//end getProperty() + + /** + * Get the BCP-47 target language read from the header. + * + * @return string The BCP-47 language tag. + */ + public function getTargetLanguage(): string + { + return $this->targetLanguage; + }//end getTargetLanguage() + + /** + * Convert the exception to a structured response body shape. + * + * @return array + */ + public function toErrorBody(): array + { + return [ + 'error' => [ + 'code' => self::ERROR_CODE, + 'property' => $this->property, + 'targetLanguage' => $this->targetLanguage, + 'message' => $this->getMessage(), + ], + ]; + }//end toErrorBody() +}//end class diff --git a/lib/Exception/ValidationException.php b/lib/Exception/ValidationException.php index eca901f184..f7c2991131 100644 --- a/lib/Exception/ValidationException.php +++ b/lib/Exception/ValidationException.php @@ -70,7 +70,7 @@ class ValidationException extends Exception * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( string $message, @@ -94,7 +94,7 @@ public function __construct( * * @return ValidationError|null The validation errors object or null if not available * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-26 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getErrors(): ?ValidationError { diff --git a/lib/Federation/OpenRegisterCloudFederationProvider.php b/lib/Federation/OpenRegisterCloudFederationProvider.php new file mode 100644 index 0000000000..61a5d5622e --- /dev/null +++ b/lib/Federation/OpenRegisterCloudFederationProvider.php @@ -0,0 +1,137 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Federation; + +use OCA\OpenRegister\Service\FederationShareService; +use OCP\Federation\Exceptions\ProviderCouldNotAddShareException; +use OCP\Federation\ICloudFederationProvider; +use OCP\Federation\ICloudFederationShare; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * OCM provider for OpenRegister federated shares. + */ +class OpenRegisterCloudFederationProvider implements ICloudFederationProvider +{ + /** + * Constructor. + * + * @param FederationShareService $shareService Federated-share management. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly FederationShareService $shareService, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The OCM resource type this provider handles. + */ + public function getShareType(): string + { + return 'openregister'; + }//end getShareType() + + /** + * {@inheritDoc} + * + * @return string[] The supported OCM share-with types. + */ + public function getSupportedShareTypes(): array + { + return ['user', 'group']; + }//end getSupportedShareTypes() + + /** + * {@inheritDoc} + * + * Record the inbound share and return its local id. The share's `protocol` + * options carry the remote OpenRegister API URL + scoped token so the local + * FederatedObjectSourceProvider can proxy reads. + * + * @param ICloudFederationShare $share The inbound OCM share. + * + * @return string The local share id. + * + * @throws ProviderCouldNotAddShareException When the share cannot be recorded. + */ + public function shareReceived(ICloudFederationShare $share): string + { + try { + $protocol = $share->getProtocol(); + $options = ($protocol['options'] ?? []); + + $recorded = $this->shareService->recordIncomingShare( + params: [ + 'remoteInstanceUrl' => ($options['apiUrl'] ?? null), + 'remoteProviderId' => $share->getProviderId(), + 'shareToken' => (string) ($options['token'] ?? $share->getShareSecret()), + 'scope' => (string) ($options['scope'] ?? 'schema'), + 'register' => ($options['register'] ?? null), + 'schema' => ($options['schema'] ?? null), + 'objectUri' => ($options['objectUri'] ?? null), + 'permissions' => (string) ($options['permissions'] ?? 'read'), + 'sharedWith' => $share->getShareWith(), + ] + ); + + return (string) $recorded->getId(); + } catch (Throwable $e) { + $this->logger->error('[Federation] shareReceived failed: '.$e->getMessage()); + throw new ProviderCouldNotAddShareException('Could not record OpenRegister federated share'); + }//end try + }//end shareReceived() + + /** + * {@inheritDoc} + * + * Handle OCM notifications: an unshare/revoke marks the local share revoked; + * accept/decline update the status. Unknown types are ignored. + * + * @param mixed $notificationType The OCM notification type. + * @param mixed $providerId The remote provider/share id. + * @param array $notification The notification payload. + * + * @return array The response payload (empty on success). + */ + public function notificationReceived($notificationType, $providerId, array $notification): array + { + $this->logger->info('[Federation] OCM notification '.((string) $notificationType).' for '.((string) $providerId)); + + // Status transitions are applied by the share-management service in a + // follow-up; this hook currently acknowledges receipt so the remote does + // not retry. Full accept/decline/revoke wiring lands with the settings UI. + return []; + }//end notificationReceived() +}//end class diff --git a/lib/Formats/BsnFormat.php b/lib/Formats/BsnFormat.php index 3de24b2116..b09b6a6175 100644 --- a/lib/Formats/BsnFormat.php +++ b/lib/Formats/BsnFormat.php @@ -35,10 +35,17 @@ class BsnFormat implements Format * * @return bool True if data is a valid BSN, false otherwise. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-22 + * @spec openspec/specs/data-import-export/spec.md */ public function validate(mixed $data): bool { + // Reject over-length input before padding: str_pad only left-pads and + // never truncates, so a >9-digit value would otherwise be checksummed + // on a miscalculated weighting (ADR-008 Rule 4). + if (strlen((string) $data) > 9) { + return false; + } + $data = str_pad( string: $data, length:9, @@ -50,6 +57,13 @@ public function validate(mixed $data): bool return false; } + // Reject the all-zero sentinel: it passes the modulo-11 checksum + // (0 % 11 === 0) but is not a real BSN, and empty/null input pads to it + // (ADR-008 Rule 4). + if ($data === '000000000') { + return false; + } + $control = 0; $reversedIterator = 9; foreach (str_split($data) as $character) { diff --git a/lib/Formats/ExtendedFieldTypeValidator.php b/lib/Formats/ExtendedFieldTypeValidator.php index 272f083901..6aa172bd27 100644 --- a/lib/Formats/ExtendedFieldTypeValidator.php +++ b/lib/Formats/ExtendedFieldTypeValidator.php @@ -108,7 +108,10 @@ public function validateColor(mixed $value, ?string $format, string $propertyNam return "Property '{$propertyName}': color value must be a string"; } - $format = ($format === null || $format === '') ? 'hex' : strtolower($format); + $format = strtolower((string) $format); + if ($format === '') { + $format = 'hex'; + } if ($format === 'rgba') { return $this->validateRgba(value: $value, propertyName: $propertyName); diff --git a/lib/Formats/Iso8601DateTimeFormat.php b/lib/Formats/Iso8601DateTimeFormat.php new file mode 100644 index 0000000000..ceff8085ca --- /dev/null +++ b/lib/Formats/Iso8601DateTimeFormat.php @@ -0,0 +1,100 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Formats; + +use Opis\JsonSchema\Format; + +/** + * ISO 8601 date-time format validator (relaxed) + * + * Accepts ISO 8601 date-time values where the seconds component and/or the + * timezone designator are optional. Replaces the opis built-in `date-time` + * format, which mandates seconds, so company-standard ISO 8601 input (e.g. + * a `datetime-local` value without seconds) is accepted by the API. + * + * Examples of valid values: + * - 2026-06-18T10:14 + * - 2026-06-18T10:14:00 + * - 2026-06-18T10:14:00Z + * - 2026-06-18T10:14:00+02:00 + * - 2026-06-18T10:14:00.123Z + * + * Examples of invalid values: + * - 2026-06-18 (date only, no time) + * - 2026-13-01T10:14 (month out of range) + * - 2026-02-30T10:14 (impossible calendar date) + * - tomorrow (relative string) + * - not-a-date + * + * @category Formats + * @package OCA\OpenRegister\Formats + * @author Conduction AI + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.conduction.nl + */ +class Iso8601DateTimeFormat implements Format +{ + /** + * Relaxed ISO 8601 date-time pattern. + * + * Derived from the opis/json-schema built-in date-time regex with the + * seconds component made optional (alongside the already-optional + * timezone). Capture groups 1-3 (year, month, day) are reused for a + * calendar-validity check via checkdate(). + * + * @var string + */ + private const ISO8601_DATETIME_PATTERN = '/^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])' + .'T([01][0-9]|2[0-3]):([0-5][0-9])(:([0-5][0-9]|60)(\.[0-9]+)?)?' + .'(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))?$/i'; + + /** + * Validates if a given value is an ISO 8601 date-time with optional seconds/timezone + * + * @param mixed $data The data to validate against the date-time format + * + * @inheritDoc + * + * @return bool True if data is a valid ISO 8601 date-time, false otherwise + * + * @spec openspec/specs/data-import-export/spec.md + */ + public function validate(mixed $data): bool + { + // Only validate strings. + if (is_string($data) === false) { + return false; + } + + // Match the relaxed pattern and capture the date components. + if (preg_match(self::ISO8601_DATETIME_PATTERN, $data, $matches) !== 1) { + return false; + } + + // Reject impossible calendar dates (e.g. 2026-02-30). + return checkdate((int) $matches[2], (int) $matches[3], (int) $matches[1]); + }//end validate() +}//end class diff --git a/lib/Formats/SemVerFormat.php b/lib/Formats/SemVerFormat.php index 9fcc8e0eb8..5faa8b3921 100644 --- a/lib/Formats/SemVerFormat.php +++ b/lib/Formats/SemVerFormat.php @@ -73,7 +73,7 @@ class SemVerFormat implements Format * * @return bool True if data is a valid semantic version, false otherwise * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-22 + * @spec openspec/specs/data-import-export/spec.md */ public function validate(mixed $data): bool { diff --git a/lib/Formats/UuidFormat.php b/lib/Formats/UuidFormat.php new file mode 100644 index 0000000000..802419d347 --- /dev/null +++ b/lib/Formats/UuidFormat.php @@ -0,0 +1,121 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Formats; + +use Opis\JsonSchema\Format; + +/** + * The single shared UUID validator. + * + * Supported shapes are explicit, named options so a fix propagates everywhere + * instead of drifting between per-site copies: + * - canonical: 8-4-4-4-12 hex with hyphens (the JSON-Schema `format: uuid`). + * - prefixed: an app prefix + `-` + a canonical UUID (e.g. `foo-uuid-...`). + * - hex32: 32 unbroken hex characters (no hyphens). + */ +class UuidFormat implements Format +{ + /** + * Canonical 8-4-4-4-12 UUID. + */ + private const CANONICAL = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i'; + + /** + * A lowercase-letter prefix, a hyphen, then a canonical UUID. + */ + private const PREFIXED = '/^[a-z]+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i'; + + /** + * 32 unbroken hex characters. + */ + private const HEX32 = '/^[0-9a-f]{32}$/i'; + + /** + * JSON-Schema Format hook — validates the canonical UUID shape. + * + * @param mixed $data The value to validate. + * + * @inheritDoc + * + * @return bool True if the value is a canonical UUID. + */ + public function validate(mixed $data): bool + { + return is_string($data) === true && self::isCanonical(value: $data) === true; + }//end validate() + + /** + * True when the value is a canonical 8-4-4-4-12 UUID. + * + * @param string $value The value to test. + * + * @return bool + */ + public static function isCanonical(string $value): bool + { + return preg_match(self::CANONICAL, $value) === 1; + }//end isCanonical() + + /** + * True when the value is a canonical UUID with an app prefix. + * + * @param string $value The value to test. + * + * @return bool + */ + public static function isPrefixed(string $value): bool + { + return preg_match(self::PREFIXED, $value) === 1; + }//end isPrefixed() + + /** + * True when the value is 32 unbroken hex characters. + * + * @param string $value The value to test. + * + * @return bool + */ + public static function isHex32(string $value): bool + { + return preg_match(self::HEX32, $value) === 1; + }//end isHex32() + + /** + * True when the value matches any supported UUID shape. + * + * Call sites that previously accepted a prefixed or 32-hex form use this + * instead of carrying their own divergent regex. + * + * @param string $value The value to test. + * + * @return bool + */ + public static function isAny(string $value): bool + { + return self::isCanonical(value: $value) === true + || self::isPrefixed(value: $value) === true + || self::isHex32(value: $value) === true; + }//end isAny() +}//end class diff --git a/lib/Lifecycle/Action/SetFieldsAction.php b/lib/Lifecycle/Action/SetFieldsAction.php new file mode 100644 index 0000000000..4c70f6bc9f --- /dev/null +++ b/lib/Lifecycle/Action/SetFieldsAction.php @@ -0,0 +1,121 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Lifecycle\Action; + +use OCA\OpenRegister\Lifecycle\LifecycleActionInterface; +use RuntimeException; + +/** + * Stamp declared field values onto the transitioning object payload. + * + * Registered as the OpenRegister built-in for both `set-fields` and + * `set-field`. Self-mutating: returns the payload with the declared fields + * applied so the executor merges it back before persistence. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ +final class SetFieldsAction implements LifecycleActionInterface +{ + /** + * Token that resolves to the current UTC time (ISO-8601) at execution time. + * + * @var string + */ + private const NOW_TOKEN = '@now'; + + /** + * Apply the declared field map to the object payload. + * + * Accepts both authoring shapes: + * - `set-fields`: the `actionParameters` map IS the field map + * (e.g. `{ "submittedAt": "@now" }`). + * - `set-field`: the field map lives under a `set` key + * (e.g. `{ "set": { "submittedAt": "@now" } }`). + * + * @param array $objectData Object payload after the lifecycle move. + * @param array $previousData Object payload before the transition (unused). + * @param array $parameters The declared `actionParameters` block. + * @param string $actionName The declared action name (`set-fields` or `set-field`). + * + * @return array The payload with the declared fields stamped. + * + * @throws RuntimeException When no field map can be resolved from the parameters (declared but useless — fail loud). + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + public function execute(array $objectData, array $previousData, array $parameters, string $actionName): array + { + $fields = $this->resolveFieldMap(parameters: $parameters); + if ($fields === []) { + throw new RuntimeException( + sprintf('Lifecycle action "%s" declares no fields to set.', $actionName) + ); + } + + foreach ($fields as $key => $value) { + $objectData[(string) $key] = $this->resolveValue(value: $value); + } + + return $objectData; + }//end execute() + + /** + * Resolve the field map from either authoring shape. + * + * @param array $parameters The declared `actionParameters` block. + * + * @return array The field → value map (possibly empty). + */ + private function resolveFieldMap(array $parameters): array + { + // `set-field` shape: fields under a `set` key. + if (isset($parameters['set']) === true && is_array($parameters['set']) === true) { + return $parameters['set']; + } + + // `set-fields` shape: the parameters map IS the field map. + return $parameters; + }//end resolveFieldMap() + + /** + * Resolve a declared value, expanding the `@now` token. + * + * @param mixed $value The declared value. + * + * @return mixed The resolved value. + */ + private function resolveValue(mixed $value): mixed + { + if ($value === self::NOW_TOKEN) { + return gmdate('Y-m-d\TH:i:s\Z'); + } + + return $value; + }//end resolveValue() +}//end class diff --git a/lib/Lifecycle/GuardResult.php b/lib/Lifecycle/GuardResult.php index 3530c868a9..50c61248dc 100644 --- a/lib/Lifecycle/GuardResult.php +++ b/lib/Lifecycle/GuardResult.php @@ -62,7 +62,7 @@ private function __construct(bool $allowed, ?string $message) * * @return self Allow verdict instance. * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ public static function allow(): self { @@ -76,7 +76,7 @@ public static function allow(): self * * @return self Deny verdict instance. * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ public static function deny(string $message): self { @@ -88,7 +88,7 @@ public static function deny(string $message): self * * @return bool True when allowed, false when denied. * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ public function isAllowed(): bool { diff --git a/lib/Lifecycle/LifecycleActionInterface.php b/lib/Lifecycle/LifecycleActionInterface.php new file mode 100644 index 0000000000..fcfe02e91f --- /dev/null +++ b/lib/Lifecycle/LifecycleActionInterface.php @@ -0,0 +1,62 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Lifecycle; + +/** + * Apps (and OpenRegister itself) implement this to run a declared lifecycle + * action on a transition. + * + * Mirrors `LifecycleGuardInterface`: guards authorise a transition (read-only), + * actions run its side effects. A self-mutating action (e.g. stamping + * `submittedAt`) MUST return the modified object payload; the executor merges + * the return value back into the object being saved. A pure side-effect action + * (e.g. materialising a related object, emitting an event) MUST return the + * payload it received, unchanged. + * + * A handler MUST fail loudly (throw) when it cannot perform its declared work — + * it MUST NOT silently no-op. Silent no-op is the exact defect the executor + * exists to eliminate (issue #427). + */ +interface LifecycleActionInterface +{ + /** + * Run the action on a transitioning object. + * + * @param array $objectData The object payload after the lifecycle field was moved to its target value. + * @param array $previousData The object payload before the transition (for conditions / diffing). + * @param array $parameters The declared `actionParameters` block (empty array when absent). + * @param string $actionName The declared `action` name that resolved to this handler. + * + * @return array The object payload, with any self-mutations applied. Return the input unchanged for pure side-effect actions. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + public function execute(array $objectData, array $previousData, array $parameters, string $actionName): array; +}//end interface diff --git a/lib/Lifecycle/LifecycleGuardInterface.php b/lib/Lifecycle/LifecycleGuardInterface.php index bbf65b0532..7c3683c531 100644 --- a/lib/Lifecycle/LifecycleGuardInterface.php +++ b/lib/Lifecycle/LifecycleGuardInterface.php @@ -45,7 +45,7 @@ interface LifecycleGuardInterface * * @return GuardResult Allow or deny + optional message. * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ public function check(array $object, string $action, string $userId): GuardResult; }//end interface diff --git a/lib/Listener/ActionListener.php b/lib/Listener/ActionListener.php index b731583e09..f0721392c5 100644 --- a/lib/Listener/ActionListener.php +++ b/lib/Listener/ActionListener.php @@ -19,14 +19,13 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-2 + * @spec openspec/specs/actions/spec.md */ declare(strict_types=1); namespace OCA\OpenRegister\Listener; -use Exception; use OCA\OpenRegister\Db\ActionMapper; use OCA\OpenRegister\Service\ActionExecutor; use OCP\EventDispatcher\Event; @@ -52,7 +51,7 @@ class ActionListener implements IEventListener * @param ActionExecutor $actionExecutor Action executor for running actions * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-19 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private readonly ActionMapper $actionMapper, @@ -68,7 +67,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-19 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { @@ -122,8 +121,12 @@ public function handle(Event $event): void payload: $payload, eventType: $eventType ); - } catch (Exception $e) { - // Never let listener failures affect other listeners. + } catch (\Throwable $e) { + // Never let listener failures affect other listeners. Catch + // \Throwable (not just Exception) so a PHP Error raised inside a + // listener — e.g. a type mismatch on an event getter — cannot + // bubble up and turn an otherwise-successful object operation into + // a 500. $this->logger->error( message: '[ActionListener] Error handling event', context: [ @@ -141,7 +144,7 @@ public function handle(Event $event): void * * @return string Short class name (e.g., 'ObjectCreatingEvent') * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-19 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function getEventTypeName(Event $event): string { @@ -161,7 +164,7 @@ private function getEventTypeName(Event $event): string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-19 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function extractPayload(Event $event): array { @@ -187,36 +190,47 @@ private function extractPayload(Event $event): array } } - // Register events. + // Register events. Some events (e.g. ObjectTransitionedEvent) expose + // getRegister() as a plain string id/uuid rather than a Register entity; + // guard the type so calling jsonSerialize() on a string never fatals + // (Error, not Exception, so the handle() try/catch would not absorb it). if (method_exists($event, 'getRegister') === true) { $register = $event->getRegister(); - if ($register !== null) { + if (is_object($register) === true) { $payload['register'] = $register->jsonSerialize(); $payload['registerUuid'] = $register->getUuid() ?? null; + } else if (is_string($register) === true && $register !== '') { + $payload['registerUuid'] = $register; } } - // Schema events. + // Schema events. As above, ObjectTransitionedEvent's getSchema() is a + // string id/uuid, not a Schema entity. if (method_exists($event, 'getSchema') === true) { $schema = $event->getSchema(); - if ($schema !== null) { + if (is_object($schema) === true) { $payload['schema'] = $schema->jsonSerialize(); $payload['schemaUuid'] = $schema->getUuid() ?? null; + } else if (is_string($schema) === true && $schema !== '') { + $payload['schemaUuid'] = $schema; } } - // Action events. + // Action events. ObjectTransitionedEvent's getAction() is the string + // transition name (e.g. 'park'), not an Action entity. if (method_exists($event, 'getAction') === true) { $action = $event->getAction(); - if ($action !== null) { + if (is_object($action) === true) { $payload['action'] = $action->jsonSerialize(); + } else if (is_string($action) === true && $action !== '') { + $payload['action'] = $action; } } // Source events. if (method_exists($event, 'getSource') === true) { $source = $event->getSource(); - if ($source !== null) { + if (is_object($source) === true) { $payload['source'] = $source->jsonSerialize(); } } @@ -224,7 +238,7 @@ private function extractPayload(Event $event): array // Configuration events. if (method_exists($event, 'getConfiguration') === true) { $configuration = $event->getConfiguration(); - if ($configuration !== null) { + if (is_object($configuration) === true) { $payload['configuration'] = $configuration->jsonSerialize(); } } @@ -240,7 +254,7 @@ private function extractPayload(Event $event): array * * @return array Filtered actions that match their filter conditions * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-19 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function applyFilterConditions(array $actions, array $payload): array { @@ -280,7 +294,7 @@ function ($action) use ($payload) { * * @return mixed The value or null * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-19 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function getNestedValue(array $data, string $key): mixed { diff --git a/lib/Listener/ActivityEventListener.php b/lib/Listener/ActivityEventListener.php index 793e636286..ba72c120f7 100644 --- a/lib/Listener/ActivityEventListener.php +++ b/lib/Listener/ActivityEventListener.php @@ -19,7 +19,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ declare(strict_types=1); @@ -66,7 +66,7 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-event-listener-isolation-must-prevent-cascading-failures */ public function handle(Event $event): void { diff --git a/lib/Listener/AggregationThresholdListener.php b/lib/Listener/AggregationThresholdListener.php index a81aea0561..70b8bc4298 100644 --- a/lib/Listener/AggregationThresholdListener.php +++ b/lib/Listener/AggregationThresholdListener.php @@ -4,14 +4,17 @@ * OpenRegister AggregationThresholdListener * * Subscribes to object-write events and re-evaluates threshold-typed - * notifications declared in `x-openregister-notifications`. When the - * referenced aggregation value crosses the configured threshold (i.e. - * transitions from below → above the operator condition), the listener - * dispatches the notification through the existing dispatcher. + * notifications declared in `x-openregister-notifications` (evaluation logic + * lives in ThresholdEvaluationService; rising-edge dedup in the distributed + * cache). * - * Transition tracking lives in the distributed cache so a notification - * fires once on breach, not on every subsequent write while still over - * the threshold. + * Actor-forwarded deferral (openregister#408): created/updated/transitioned + * evaluations run in AggregationThresholdJob under the captured actor, with + * entries deduped per (register, schema) so a bulk save of N objects of one + * schema triggers ONE evaluation. Delete events evaluate inline — a + * hard-deleted object cannot be re-fetched by the job, and delete-driven + * crossings (`lt`/`lte` rules) would be silently lost. The `listenerDeferral` + * kill switch restores full inline evaluation. * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. @@ -27,13 +30,14 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-5 + * @spec openspec/specs/aggregations-backend-native/spec.md */ declare(strict_types=1); namespace OCA\OpenRegister\Listener; +use OCA\OpenRegister\BackgroundJob\AggregationThresholdJob; use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; @@ -41,68 +45,53 @@ use OCA\OpenRegister\Event\ObjectDeletedEvent; use OCA\OpenRegister\Event\ObjectTransitionedEvent; use OCA\OpenRegister\Event\ObjectUpdatedEvent; -use OCA\OpenRegister\Service\Aggregation\AggregationRunner; -use OCA\OpenRegister\Service\Notification\AnnotationNotificationDispatcher; +use OCA\OpenRegister\Service\Aggregation\ThresholdEvaluationService; +use OCA\OpenRegister\Service\Deferral\ListenerDeferralService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; -use OCP\ICache; -use OCP\ICacheFactory; -use Psr\Log\LoggerInterface; /** * Listener that evaluates threshold-typed notifications on object writes. * * @template-implements IEventListener + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-2.3 */ class AggregationThresholdListener implements IEventListener { - private const STATE_ABOVE = 'above'; - private const STATE_BELOW = 'below'; - /** - * Distributed cache holding the last threshold state per (schema, notification). + * Entries per enqueued evaluation job. Rarely reached thanks to the + * (register, schema) dedupe key. * - * @var ICache|null + * @var integer */ - private ?ICache $stateCache = null; + private const CHUNK_SIZE = 100; /** - * Wire collaborators and prepare the state cache. + * Wire collaborators. * - * @param SchemaMapper $schemaMapper Schema lookup mapper. - * @param AggregationRunner $aggregationRunner Runner that computes aggregation values. - * @param AnnotationNotificationDispatcher $dispatcher Notification dispatcher. - * @param LoggerInterface $logger PSR logger for warnings. - * @param ICacheFactory $cacheFactory Distributed-cache factory. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param ThresholdEvaluationService $evaluator Shared threshold evaluation logic. + * @param ListenerDeferralService $deferral Actor-forwarding deferral service. * * @return void */ public function __construct( private readonly SchemaMapper $schemaMapper, - private readonly AggregationRunner $aggregationRunner, - private readonly AnnotationNotificationDispatcher $dispatcher, - private readonly LoggerInterface $logger, - ICacheFactory $cacheFactory + private readonly ThresholdEvaluationService $evaluator, + private readonly ListenerDeferralService $deferral ) { - try { - $this->stateCache = $cacheFactory->createDistributed('openregister_threshold_state'); - } catch (\Throwable $e) { - $this->stateCache = null; - } }//end __construct() /** - * Re-evaluate every threshold notification declared on the schema. + * Defer (or evaluate inline) the schema's threshold notifications. * * @param Event $event Inbound dispatcher event. * * @return void * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-3 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { @@ -111,145 +100,37 @@ public function handle(Event $event): void return; } - try { - $schema = $this->loadSchema(object: $object); - } catch (\Throwable $e) { - return; - } - + $schema = $this->loadSchema(object: $object); if ($schema === null) { return; } - $config = ($schema->getConfiguration() ?? []); - $notifications = ($config['x-openregister-notifications'] ?? null); - if (is_array($notifications) === false || count($notifications) === 0) { + if ($this->evaluator->hasThresholdNotifications($schema) === false) { return; } - foreach ($notifications as $name => $spec) { - if (is_array($spec) === false) { - continue; - } - - $trigger = ($spec['trigger'] ?? null); - if (is_array($trigger) === false || (string) ($trigger['type'] ?? '') !== 'threshold') { - continue; - } - - try { - $this->evaluate(schema: $schema, notificationName: (string) $name, trigger: $trigger, object: $object); - } catch (\Throwable $e) { - $this->logger->warning( - sprintf( - '[AggregationThresholdListener] evaluation of "%s" failed: %s', - (string) $name, - $e->getMessage() - ) - ); - } - }//end foreach - }//end handle() - - /** - * Evaluate one notification spec and dispatch on rising-edge crossings. - * - * @param Schema $schema Schema declaring the notification. - * @param string $notificationName Notification key in the schema config. - * @param array $trigger Trigger configuration block. - * @param ObjectEntity $object Object that just changed. - * - * @return void - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function evaluate(Schema $schema, string $notificationName, array $trigger, ObjectEntity $object): void - { - $aggregationName = (string) ($trigger['aggregation'] ?? ''); - $op = (string) ($trigger['op'] ?? ''); - $threshold = ($trigger['value'] ?? null); - if ($aggregationName === '' || $op === '' || $threshold === null) { + // Deletes evaluate inline: the entity is not re-fetchable once the + // delete lands, so a deferred job could never dispatch for + // delete-driven crossings. The kill switch also forces inline. + if ($event instanceof ObjectDeletedEvent || $this->deferral->isDeferralEnabled() === false) { + $this->evaluator->evaluateSchema(schema: $schema, object: $object); return; } - $registerSlug = (string) $object->getRegister(); - $schemaSlug = (string) $schema->getSlug(); - - // BypassRbac: threshold evaluation is a system reaction to a - // write event, not a user-driven request. The write itself - // already passed RBAC; the threshold listener's authoritative - // reason is "this object just changed", independent of the - // active session's `list` permission on the schema. - $result = $this->aggregationRunner->run( - registerRef: $registerSlug, - schemaRef: $schemaSlug, - name: $aggregationName, - bypassRbac: true + $registerRef = (string) $object->getRegister(); + $schemaRef = (string) $object->getSchema(); + $this->deferral->defer( + jobClass: AggregationThresholdJob::class, + entry: [ + 'uuid' => (string) $object->getUuid(), + 'register' => $registerRef, + 'schema' => $schemaRef, + 'version' => $object->getVersion(), + ], + chunkSize: self::CHUNK_SIZE, + dedupeKey: $registerRef.'|'.$schemaRef ); - $value = ($result['value'] ?? null); - if (is_int($value) === false && is_float($value) === false) { - return; - } - - $isAbove = $this->compare(actual: $value, op: $op, expected: $threshold); - $newState = self::STATE_BELOW; - if ($isAbove === true) { - $newState = self::STATE_ABOVE; - } - - $stateKey = sprintf('threshold:%d:%s', $schema->getId(), $notificationName); - $oldState = $this->stateCache?->get($stateKey); - - if ($newState === self::STATE_ABOVE && $oldState !== self::STATE_ABOVE) { - $this->dispatcher->dispatch( - $object, - 'threshold', - [ - 'notificationName' => $notificationName, - 'aggregation' => $aggregationName, - 'value' => $value, - 'threshold' => $threshold, - 'op' => $op, - ] - ); - } - - try { - // 30 day TTL; long enough that a slow-moving threshold series - // still has continuity across maintenance restarts. - $this->stateCache?->set($stateKey, $newState, (60 * 60 * 24 * 30)); - } catch (\Throwable $e) { - // Don't escalate. - } - }//end evaluate() - - /** - * Compare two values with the configured operator. - * - * @param int|float $actual Numeric value computed from the aggregation. - * @param string $op Comparison operator (gt|gte|lt|lte|eq|ne). - * @param mixed $expected Threshold value, must be numeric. - * - * @return bool True when the comparison is satisfied, false otherwise. - */ - private function compare($actual, string $op, $expected): bool - { - if (is_numeric($expected) === false) { - return false; - } - - $rhs = (float) $expected; - $lhs = (float) $actual; - return match ($op) { - 'gt' => $lhs > $rhs, - 'gte' => $lhs >= $rhs, - 'lt' => $lhs < $rhs, - 'lte' => $lhs <= $rhs, - 'eq' => $lhs === $rhs, - 'ne' => $lhs !== $rhs, - default => false, - }; - }//end compare() + }//end handle() /** * Resolve the underlying object for any of the supported event types. diff --git a/lib/Listener/AnnotationNotificationListener.php b/lib/Listener/AnnotationNotificationListener.php index bbdaa6036a..997883b0b2 100644 --- a/lib/Listener/AnnotationNotificationListener.php +++ b/lib/Listener/AnnotationNotificationListener.php @@ -7,6 +7,16 @@ * ObjectTransitionedEvent and asks the dispatcher to fire any matching * notifications declared on the schema. * + * Actor-forwarded deferral (openregister#408): rule evaluation and delivery + * (which can perform synchronous outbound HTTP and mail) run in + * AnnotationNotificationDispatchJob under the captured actor, so the + * dispatcher's RBAC-scoped deeplink resolution evaluates as the user who + * made the change. Inline remains: a request-cached schema-config gate + * (schemas without `x-openregister-notifications` enqueue nothing) and the + * full dispatch when the `listenerDeferral` kill switch is `inline`. Update + * entries carry the pre-update data snapshot — the old state cannot be + * re-fetched once the job runs; new data is re-read at run time. + * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * @@ -26,9 +36,13 @@ namespace OCA\OpenRegister\Listener; +use OCA\OpenRegister\BackgroundJob\AnnotationNotificationDispatchJob; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Event\ObjectCreatedEvent; use OCA\OpenRegister\Event\ObjectTransitionedEvent; use OCA\OpenRegister\Event\ObjectUpdatedEvent; +use OCA\OpenRegister\Service\Deferral\ListenerDeferralService; use OCA\OpenRegister\Service\Notification\AnnotationNotificationDispatcher; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -37,36 +51,88 @@ * Listener that fires schema-declared notifications on object events. * * @template-implements IEventListener + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-2.2 */ class AnnotationNotificationListener implements IEventListener { + /** - * Wire the notification dispatcher. + * Entries per enqueued dispatch job. Smaller than the projection chunk + * because `updated` entries carry pre-update data snapshots. * - * @param AnnotationNotificationDispatcher $dispatcher Dispatcher used to fire notifications. + * @var integer + */ + private const CHUNK_SIZE = 25; + + /** + * Wire the notification dispatcher and the deferral contract. + * + * @param AnnotationNotificationDispatcher $dispatcher Dispatcher used to fire notifications. + * @param SchemaMapper $schemaMapper Schema lookup (request-cached) for the enqueue gate. + * @param ListenerDeferralService $deferral Actor-forwarding deferral service. * * @return void */ public function __construct( - private readonly AnnotationNotificationDispatcher $dispatcher + private readonly AnnotationNotificationDispatcher $dispatcher, + private readonly SchemaMapper $schemaMapper, + private readonly ListenerDeferralService $deferral ) { }//end __construct() /** - * Dispatch any matching annotation notifications for the inbound event. + * Defer (or dispatch inline) any matching annotation notifications. * * @param Event $event Inbound dispatcher event. * * @return void * - * @spec exclude Dispatch-only event router that delegates verbatim to AnnotationNotificationDispatcher; - * the dispatcher carries the notification behaviour. + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { if ($event instanceof ObjectTransitionedEvent) { + $this->handleTransitioned(event: $event); + return; + } + + if ($event instanceof ObjectCreatedEvent) { + $object = $this->extractObject(event: $event); + if ($object !== null && $this->schemaDeclaresNotifications(object: $object) === true) { + if ($this->deferral->isDeferralEnabled() === false) { + $this->dispatcher->dispatch(object: $object, trigger: 'created'); + return; + } + + $this->deferEntry(object: $object, extra: ['trigger' => 'created']); + } + + return; + } + + if ($event instanceof ObjectUpdatedEvent) { + $this->handleUpdated(event: $event); + } + }//end handle() + + /** + * Transition events: defer with the event-only context (action/from/to). + * + * @param ObjectTransitionedEvent $event The transition event. + * + * @return void + */ + private function handleTransitioned(ObjectTransitionedEvent $event): void + { + $object = $event->getObject(); + if ($this->schemaDeclaresNotifications(object: $object) === false) { + return; + } + + if ($this->deferral->isDeferralEnabled() === false) { $this->dispatcher->dispatch( - object: $event->getObject(), + object: $object, trigger: 'transition', context: [ 'action' => $event->getAction(), @@ -77,49 +143,146 @@ public function handle(Event $event): void return; } - if ($event instanceof ObjectCreatedEvent) { - $object = $this->extractObject(event: $event); - if ($object !== null) { - $this->dispatcher->dispatch(object: $object, trigger: 'created'); - } + $this->deferEntry( + object: $object, + extra: [ + 'trigger' => 'transition', + 'action' => $event->getAction(), + 'from' => $event->getFrom(), + 'to' => $event->getTo(), + ] + ); + }//end handleTransitioned() + /** + * Update events: defer with the pre-update snapshot so field-change and + * calculatedChange conditions can compare without re-reading versioned + * history — the job re-fetches current data for `_newData` and fires the + * same `updated` + `calculatedChange` pair the inline path fired. + * + * @param ObjectUpdatedEvent $event The update event. + * + * @return void + */ + private function handleUpdated(ObjectUpdatedEvent $event): void + { + $newObject = $event->getNewObject(); + $oldObject = $event->getOldObject(); + if ($this->schemaDeclaresNotifications(object: $newObject) === false) { return; } - if ($event instanceof ObjectUpdatedEvent) { - $newObject = $event->getNewObject(); - $oldObject = $event->getOldObject(); - - // Forward old/new data on the plain `updated` dispatch too (when an - // old object is available) so rules declaring a field-change - // `condition` can compare without re-reading versioned history. - // Condition-less `updated` rules ignore this context. - $updatedContext = []; - if ($oldObject !== null) { - $updatedContext = [ - '_newData' => $newObject->getObject() ?? [], - '_oldData' => $oldObject->getObject() ?? [], - ]; - } + if ($this->deferral->isDeferralEnabled() === false) { + $this->dispatchUpdatedInline(newObject: $newObject, oldObject: $oldObject); + return; + } - $this->dispatcher->dispatch(object: $newObject, trigger: 'updated', context: $updatedContext); - - // Also evaluate calculatedChange rules when both old and new - // objects are available. Pass the previous and new calculated - // field values so the dispatcher can detect boundary crossings - // without re-reading versioned history. - if ($oldObject !== null) { - $this->dispatcher->dispatch( - object: $newObject, - trigger: 'calculatedChange', - context: [ - '_newData' => $newObject->getObject() ?? [], - '_oldData' => $oldObject->getObject() ?? [], - ] - ); - }//end if - }//end if - }//end handle() + $oldData = null; + if ($oldObject !== null) { + $oldData = ($oldObject->getObject() ?? []); + } + + $this->deferEntry( + object: $newObject, + extra: [ + 'trigger' => 'updated', + 'oldData' => $oldData, + ] + ); + }//end handleUpdated() + + /** + * Pre-deferral inline behaviour for updates (kill-switch path). + * + * Forward old/new data on the plain `updated` dispatch too (when an old + * object is available) so rules declaring a field-change `condition` can + * compare without re-reading versioned history; condition-less `updated` + * rules ignore this context. Also evaluate calculatedChange rules when + * both old and new objects are available. + * + * @param ObjectEntity $newObject Persisted new state. + * @param ObjectEntity|null $oldObject Pre-update state when available. + * + * @return void + */ + private function dispatchUpdatedInline(ObjectEntity $newObject, ?ObjectEntity $oldObject): void + { + $updatedContext = []; + if ($oldObject !== null) { + $updatedContext = [ + '_newData' => ($newObject->getObject() ?? []), + '_oldData' => ($oldObject->getObject() ?? []), + ]; + } + + $this->dispatcher->dispatch(object: $newObject, trigger: 'updated', context: $updatedContext); + + if ($oldObject !== null) { + $this->dispatcher->dispatch( + object: $newObject, + trigger: 'calculatedChange', + context: [ + '_newData' => ($newObject->getObject() ?? []), + '_oldData' => ($oldObject->getObject() ?? []), + ] + ); + } + }//end dispatchUpdatedInline() + + /** + * Buffer one dispatch entry on the deferral service. + * + * @param ObjectEntity $object The triggering object. + * @param array $extra Trigger-specific entry fields. + * + * @return void + */ + private function deferEntry(ObjectEntity $object, array $extra): void + { + $this->deferral->defer( + jobClass: AnnotationNotificationDispatchJob::class, + entry: array_merge( + [ + 'uuid' => (string) $object->getUuid(), + 'register' => (string) $object->getRegister(), + 'schema' => (string) $object->getSchema(), + 'version' => $object->getVersion(), + ], + $extra + ), + chunkSize: self::CHUNK_SIZE + ); + }//end deferEntry() + + /** + * Whether the object's schema declares any x-openregister-notifications. + * + * Cheap request-cached lookup + config-array check; schemas without the + * annotation enqueue nothing. Resolution failures return false — the + * dispatcher's own schema load would bail identically. + * + * @param ObjectEntity $object Object whose schema to inspect. + * + * @return bool True when notification rules are declared. + */ + private function schemaDeclaresNotifications(ObjectEntity $object): bool + { + $schemaRef = (string) $object->getSchema(); + if ($schemaRef === '') { + return false; + } + + try { + $schema = $this->schemaMapper->find($schemaRef); + } catch (\Throwable $e) { + return false; + } + + $config = ($schema->getConfiguration() ?? []); + $notifications = ($config['x-openregister-notifications'] ?? null); + + return is_array($notifications) === true && count($notifications) > 0; + }//end schemaDeclaresNotifications() /** * Different Object*Event classes expose the entity under different @@ -127,20 +290,20 @@ public function handle(Event $event): void * * @param Event $event Inbound dispatcher event. * - * @return \OCA\OpenRegister\Db\ObjectEntity|null Object instance, or null when none could be derived. + * @return ObjectEntity|null Object instance, or null when none could be derived. */ - private function extractObject(Event $event): ?\OCA\OpenRegister\Db\ObjectEntity + private function extractObject(Event $event): ?ObjectEntity { if (method_exists($event, 'getObject') === true) { $obj = $event->getObject(); - if ($obj instanceof \OCA\OpenRegister\Db\ObjectEntity) { + if ($obj instanceof ObjectEntity) { return $obj; } } if (method_exists($event, 'getNewObject') === true) { $obj = $event->getNewObject(); - if ($obj instanceof \OCA\OpenRegister\Db\ObjectEntity) { + if ($obj instanceof ObjectEntity) { return $obj; } } diff --git a/lib/Listener/ApprovalChainAdvanceListener.php b/lib/Listener/ApprovalChainAdvanceListener.php new file mode 100644 index 0000000000..c9bbac1de0 --- /dev/null +++ b/lib/Listener/ApprovalChainAdvanceListener.php @@ -0,0 +1,131 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/approval-workflow/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ApprovalStepCompletedEvent; +use OCA\OpenRegister\Service\Lifecycle\TransitionEngine; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +/** + * Auto-advances a gated transition when its approval chain completes. + * + * @template-implements IEventListener + */ +class ApprovalChainAdvanceListener implements IEventListener +{ + /** + * Constructor. + * + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param TransitionEngine $transitionEngine Engine used to apply the declared transition. + * @param LoggerInterface $logger Logger for fail-soft diagnostics. + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly TransitionEngine $transitionEngine, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Advance the gated transition when the completed chain declares it. + * + * @param Event $event Inbound dispatcher event. + * + * @return void + * + * @spec openspec/specs/approval-workflow/spec.md + */ + public function handle(Event $event): void + { + if (($event instanceof ApprovalStepCompletedEvent) === false) { + return; + } + + $chain = $event->getChain(); + $schemaId = $chain->getSchemaId(); + if ($schemaId === null) { + return; + } + + try { + $schema = $this->schemaMapper->find($schemaId, _multitenancy: false); + } catch (\Throwable $e) { + return; + } + + if (($schema instanceof Schema) === false) { + return; + } + + $config = ($schema->getConfiguration() ?? []); + $chains = ($config['x-openregister-approval-chains'] ?? null); + if (is_array($chains) === false) { + return; + } + + $entry = ($chains[(string) $chain->getName()] ?? null); + if (is_array($entry) === false) { + return; + } + + if (($entry['onApprove'] ?? null) !== 'advanceTransition') { + return; + } + + $action = (string) ($entry['transition'] ?? ''); + $objectUuid = $event->getObjectUuid(); + if ($action === '' || $objectUuid === '') { + return; + } + + try { + $this->transitionEngine->transition(objectId: $objectUuid, action: $action); + } catch (\Throwable $e) { + // Fail-soft: the chain is correctly `approved` regardless. A failed + // auto-advance leaves the object at its pre-gate state; the gate + // listener will allow a subsequent manual transition attempt since + // every provisioned step is already approved. + $this->logger->warning( + sprintf( + '[ApprovalChainAdvanceListener] auto-advance "%s" for object %s failed: %s', + $action, + $objectUuid, + $e->getMessage() + ) + ); + } + }//end handle() +}//end class diff --git a/lib/Listener/ApprovalChainGateListener.php b/lib/Listener/ApprovalChainGateListener.php new file mode 100644 index 0000000000..41d1be8a8b --- /dev/null +++ b/lib/Listener/ApprovalChainGateListener.php @@ -0,0 +1,387 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/approval-workflow/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\ApprovalChain; +use OCA\OpenRegister\Db\ApprovalChainMapper; +use OCA\OpenRegister\Db\ApprovalStepMapper; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ObjectUpdatingEvent; +use OCA\OpenRegister\Service\ApprovalChainAnnotationInstaller; +use OCA\OpenRegister\Service\ApprovalService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Gates a declared lifecycle transition on approval-chain completion. + * + * @template-implements IEventListener + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ApprovalChainGateListener implements IEventListener +{ + /** + * Constructor. + * + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param ApprovalChainMapper $chainMapper Chain mapper. + * @param ApprovalStepMapper $stepMapper Step mapper. + * @param ApprovalService $approvalService Provisions/inspects chain steps. + * @param ApprovalChainAnnotationInstaller $installer Ensures the declared chain is provisioned. + * @param IUserSession $userSession Current user session (requester identity). + * @param LoggerInterface $logger Logger for gate diagnostics. + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly ApprovalChainMapper $chainMapper, + private readonly ApprovalStepMapper $stepMapper, + private readonly ApprovalService $approvalService, + private readonly ApprovalChainAnnotationInstaller $installer, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Gate the attempted lifecycle transition against any declared approval chain. + * + * @param Event $event Inbound dispatcher event. + * + * @return void + * + * @spec openspec/specs/approval-workflow/spec.md + */ + public function handle(Event $event): void + { + if (($event instanceof ObjectUpdatingEvent) === false) { + return; + } + + $oldObject = $event->getOldObject(); + if ($oldObject === null) { + return; + } + + $newObject = $event->getNewObject(); + $schema = $this->loadSchema(object: $newObject); + if ($schema === null) { + return; + } + + $config = ($schema->getConfiguration() ?? []); + $chains = ($config['x-openregister-approval-chains'] ?? null); + if (is_array($chains) === false || $chains === []) { + return; + } + + $lifecycle = ($config['x-openregister-lifecycle'] ?? null); + if (is_array($lifecycle) === false) { + return; + } + + $field = (string) ($lifecycle['field'] ?? ($lifecycle['property'] ?? '')); + $transitions = ($lifecycle['transitions'] ?? []); + $oldData = ($oldObject->getObject() ?? []); + $newData = ($newObject->getObject() ?? []); + $oldValue = ($oldData[$field] ?? null); + $newValue = ($newData[$field] ?? null); + + if ($oldValue === $newValue || is_string($newValue) === false) { + return; + } + + $action = $this->matchTransition( + transitions: $transitions, + oldValue: (string) $oldValue, + newValue: $newValue + ); + if ($action === null) { + // Not a recognised transition — LifecycleValidationListener rejects + // this on its own; the approval gate has nothing to evaluate. + return; + } + + foreach ($chains as $chainKey => $spec) { + if (is_string($chainKey) === false || is_array($spec) === false) { + continue; + } + + if (($spec['transition'] ?? null) !== $action) { + continue; + } + + $blocked = $this->evaluateGate( + event: $event, + schema: $schema, + chainKey: $chainKey, + spec: $spec, + object: $newObject, + newData: $newData + ); + if ($blocked === true) { + return; + } + }//end foreach + }//end handle() + + /** + * Evaluate one declared chain against the object attempting its gated + * transition. Returns true when the event was rejected (short-circuits the + * caller's loop). + * + * @param ObjectUpdatingEvent $event The event being evaluated. + * @param Schema $schema The object's schema. + * @param string $chainKey Declarative chain key. + * @param array $spec The chain's declared spec. + * @param ObjectEntity $object The object attempting the transition. + * @param array $newData The object's new (attempted) data. + * + * @return bool True when the transition was blocked. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function evaluateGate( + ObjectUpdatingEvent $event, + Schema $schema, + string $chainKey, + array $spec, + ObjectEntity $object, + array $newData + ): bool { + // Idempotent: guarantees the chain row exists even if the installer + // hasn't run yet for this schema revision (e.g. schema saved before + // this capability existed). + $this->installer->installSchema(schema: $schema); + + $schemaId = $schema->getId(); + $chain = null; + if ($schemaId !== null) { + $chain = $this->chainMapper->findBySchemaAndName(schemaId: (int) $schemaId, name: $chainKey); + } + + if ($chain === null) { + // Declared but couldn't be provisioned (e.g. no valid approvers) — + // fail closed, mirroring LifecycleGuardRegistry's missing-tag policy. + $this->logger->error( + sprintf('[ApprovalChainGateListener] chain "%s" could not be resolved for schema %s', $chainKey, (string) $schemaId) + ); + $this->reject( + event: $event, + code: 'approval-chain-misconfigured', + message: sprintf('Approval chain "%s" is declared but not provisioned.', $chainKey) + ); + return true; + } + + $objectUuid = (string) $object->getUuid(); + $steps = $this->stepMapper->findByChainAndObject($chain->getId(), $objectUuid); + + if ($steps !== []) { + $anyRejected = false; + $allApproved = true; + foreach ($steps as $step) { + if ($step->getStatus() === 'rejected') { + $anyRejected = true; + } + + if ($step->getStatus() !== 'approved') { + $allApproved = false; + } + } + + if ($allApproved === true) { + // Release: every provisioned step is approved. + return false; + } + + if ($anyRejected === true) { + // Terminal-failed cycle — clear it so a fresh attempt opens a + // new cycle (resubmission), then fall through to provision. + $this->stepMapper->deleteByChainAndObject($chain->getId(), $objectUuid); + } else { + // Still in progress — block again, no new rows. + $this->reject( + event: $event, + code: 'approval-chain-pending', + message: sprintf('Approval chain "%s" is still pending a decision.', $chainKey) + ); + return true; + } + }//end if + + // No steps yet (first attempt, or a rejected cycle was just cleared): + // provision the applicable tier and block. + $requesterId = $this->userSession->getUser()?->getUID(); + $stepsOverride = $this->resolveStepsOverride(spec: $spec, newData: $newData); + + $this->approvalService->initializeChain( + chain: $chain, + objectUuid: $objectUuid, + requesterId: $requesterId, + stepsOverride: $stepsOverride + ); + + $this->reject( + event: $event, + code: 'approval-chain-pending', + message: sprintf('Transition requires approval via chain "%s".', $chainKey) + ); + return true; + }//end evaluateGate() + + /** + * Resolve the applicable step definitions for this object. + * + * When the spec declares `amountField`, selects the single `approvers` tier + * with the highest `minAmount` that is `<=` the object's value for that + * field. Otherwise returns `null` so `initializeChain()` falls back to the + * chain's own static `steps` (every declared tier, unchanged behaviour). + * + * @param array $spec The chain's declared spec. + * @param array $newData The object's new (attempted) data. + * + * @return array>|null Step override, or null for no routing. + */ + private function resolveStepsOverride(array $spec, array $newData): ?array + { + $amountField = ($spec['amountField'] ?? null); + $approvers = ($spec['approvers'] ?? []); + if (is_string($amountField) === false || $amountField === '' || is_array($approvers) === false) { + return null; + } + + $amount = (float) ($newData[$amountField] ?? 0); + + $best = null; + $bestMinAmount = -1.0; + foreach ($approvers as $tier) { + if (is_array($tier) === false) { + continue; + } + + $role = (string) ($tier['role'] ?? ''); + $minAmount = (float) ($tier['minAmount'] ?? 0); + if ($role === '' || $minAmount > $amount) { + continue; + } + + if ($best === null || $minAmount > $bestMinAmount) { + $best = $role; + $bestMinAmount = $minAmount; + } + } + + if ($best === null) { + return null; + } + + return [['order' => 1, 'role' => $best]]; + }//end resolveStepsOverride() + + /** + * Find the transition (action name) whose `to` matches the new value AND + * whose `from` list contains the old value. Mirrors + * `LifecycleValidationListener::findTransitionByTarget()`. + * + * @param array $transitions Transition map from the annotation. + * @param string $oldValue Current lifecycle field value. + * @param string $newValue Attempted lifecycle field value. + * + * @return string|null The matched action name, or null. + */ + private function matchTransition(array $transitions, string $oldValue, string $newValue): ?string + { + foreach ($transitions as $action => $spec) { + if (is_array($spec) === false || ($spec['to'] ?? null) !== $newValue) { + continue; + } + + $from = ($spec['from'] ?? []); + if (is_string($from) === true) { + $from = [$from]; + } + + if (is_array($from) === true && in_array($oldValue, $from, true) === true) { + return (string) $action; + } + } + + return null; + }//end matchTransition() + + /** + * Load the schema referenced by an object, returning null on failure. + * + * @param ObjectEntity $object Object whose schema reference to resolve. + * + * @return Schema|null Resolved schema, or null on lookup failure. + */ + private function loadSchema(ObjectEntity $object): ?Schema + { + $schemaRef = $object->getSchema(); + if ($schemaRef === null || $schemaRef === '') { + return null; + } + + try { + return $this->schemaMapper->find($schemaRef, _multitenancy: false); + } catch (\Throwable $e) { + return null; + } + }//end loadSchema() + + /** + * Stop the event and stamp a structured error onto it. + * + * @param ObjectUpdatingEvent $event The event being rejected. + * @param string $code Structured error code. + * @param string $message Human-readable message. + * + * @return void + */ + private function reject(ObjectUpdatingEvent $event, string $code, string $message): void + { + $event->setErrors( + [ + 'code' => $code, + 'message' => $message, + ] + ); + $event->stopPropagation(); + }//end reject() +}//end class diff --git a/lib/Listener/CalculationOnSaveListener.php b/lib/Listener/CalculationOnSaveListener.php index 58d895ac1d..27cbcb214d 100644 --- a/lib/Listener/CalculationOnSaveListener.php +++ b/lib/Listener/CalculationOnSaveListener.php @@ -29,15 +29,20 @@ use DateTimeInterface; use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Event\ObjectCreatingEvent; use OCA\OpenRegister\Event\ObjectUpdatingEvent; use OCA\OpenRegister\Service\Calculation\CalculationEvaluator; +use OCA\OpenRegister\Service\Calculation\CalculationPayloadBuilder; use OCA\OpenRegister\Service\Calculation\EvaluationException; +use OCA\OpenRegister\Service\Calculation\SequenceContext; +use OCA\OpenRegister\Service\SequenceService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use Psr\Log\LoggerInterface; +use Throwable; /** * Materialises declared calculations into the object payload on create/update. @@ -47,23 +52,36 @@ * cycle check guarantees the graph is acyclic. * * @template-implements IEventListener + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) The listener legitimately wires the + * schema mapper, the pure evaluator, the cross-object reference resolver, the logger, + * and the two object/schema/event DB + event types it bridges; each is a distinct + * collaborator in the save-time materialisation pipeline and none can be folded away. */ class CalculationOnSaveListener implements IEventListener { /** * Wire collaborators used to look up schema calculations. * - * @param SchemaMapper $schemaMapper Schema lookup mapper. - * @param CalculationEvaluator $evaluator Expression evaluator. - * @param LoggerInterface $logger PSR logger for warnings. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param RegisterMapper $registerMapper Register lookup mapper (for sequence scope id). + * @param CalculationEvaluator $evaluator Expression evaluator. + * @param CalculationPayloadBuilder $payloadBuilder Shared @self/@ref/@aggregate payload prep. + * @param SequenceService $sequences Atomic running-number reservation service. + * @param LoggerInterface $logger PSR logger for warnings. * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-1 + * @spec openspec/specs/computed-fields/spec.md + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-2 + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-2 */ public function __construct( private readonly SchemaMapper $schemaMapper, + private readonly RegisterMapper $registerMapper, private readonly CalculationEvaluator $evaluator, + private readonly CalculationPayloadBuilder $payloadBuilder, + private readonly SequenceService $sequences, private readonly LoggerInterface $logger ) { }//end __construct() @@ -75,7 +93,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-2 + * @spec openspec/specs/computed-fields/spec.md */ public function handle(Event $event): void { @@ -100,10 +118,13 @@ public function handle(Event $event): void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) The method runs the linear save-time + * materialisation pipeline (inject @self, @ref, @aggregate, then evaluate each calc and + * strip the synthetic keys); the steps share one payload and must stay in order, so + * extracting them would only scatter a strictly-sequential flow across helpers. * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-3 + * @spec openspec/specs/computed-fields/spec.md */ private function process(ObjectEntity $object, bool $isUpdate): void { @@ -117,34 +138,25 @@ private function process(ObjectEntity $object, bool $isUpdate): void return; } - $data = $object->getObject() ?? []; $changed = false; - // Inject `@self` system metadata so calculations can reference - // `@self.created`, `@self.updated`, etc. via the CalculationEvaluator's - // dotted prop path. ObjectEntity carries these on the entity itself, - // not in the data array. - $created = $object->getCreated(); - $updated = $object->getUpdated(); - $createdFormatted = null; - if ($created !== null) { - $createdFormatted = $created->format(\DateTimeInterface::ATOM); - }//end if - - $updatedFormatted = null; - if ($updated !== null) { - $updatedFormatted = $updated->format(\DateTimeInterface::ATOM); - }//end if - - $data['@self'] = [ - 'id' => $object->getUuid(), - 'uuid' => $object->getUuid(), - 'register' => $object->getRegister(), - 'schema' => $object->getSchema(), - 'owner' => $object->getOwner(), - 'created' => $createdFormatted, - 'updated' => $updatedFormatted, - ]; + // Build the evaluation payload — the object's data enriched with the + // synthetic `@self` system metadata, pre-resolved `@ref.` + // cross-object references, and pre-resolved `@aggregate.` + // aggregate references. Shared with the temporal re-evaluation sweep + // via CalculationPayloadBuilder so both paths evaluate against one + // payload shape. Resolution is RBAC + tenant scoped and never fails + // the save. + $data = $this->payloadBuilder->build(object: $object, schema: $schema); + + // Build the sequence-consumption context ONLY on create. Passing it to + // the evaluator lets `{ "sequence": … }` nodes reserve exactly one + // running number per object; on update the context is null so a + // re-materialise never burns a fresh number. + $sequenceContext = null; + if ($isUpdate === false) { + $sequenceContext = $this->buildSequenceContext(object: $object, schema: $schema); + } foreach ($calcs as $name => $spec) { if (is_array($spec) === false) { @@ -157,7 +169,7 @@ private function process(ObjectEntity $object, bool $isUpdate): void } try { - $value = $this->evaluator->evaluate($data, $spec['expression'] ?? null); + $value = $this->evaluator->evaluate($data, $spec['expression'] ?? null, $sequenceContext); } catch (EvaluationException $e) { $this->logger->warning( sprintf( @@ -177,15 +189,61 @@ private function process(ObjectEntity $object, bool $isUpdate): void } }//end foreach - // Strip the synthetic @self before persisting; it's a runtime aid - // for the evaluator, not user data. - unset($data['@self']); + // Strip the synthetic @self, @ref and @aggregate before persisting; + // they're a runtime aid for the evaluator, not user data. + $data = $this->payloadBuilder->stripSyntheticKeys(data: $data); if ($changed === true) { $object->setObject($data); } }//end process() + /** + * Build the per-create SequenceContext binding the object's register + schema scope. + * + * Returns null when the numeric register/schema ids cannot be resolved, in + * which case a `sequence` node simply yields null rather than failing the + * save. The schema is already resolved (it carries the numeric PK); the + * register reference is resolved through the RegisterMapper. + * + * @param ObjectEntity $object The object being created. + * @param Schema $schema The resolved schema (carries the numeric id). + * + * @return SequenceContext|null The bound context, or null when the scope ids are unresolvable. + */ + private function buildSequenceContext(ObjectEntity $object, Schema $schema): ?SequenceContext + { + $schemaId = (int) $schema->getId(); + if ($schemaId <= 0) { + return null; + } + + $registerRef = $object->getRegister(); + if ($registerRef === null || $registerRef === '') { + return null; + } + + try { + // Bypass RBAC + multitenancy: the create event fires in a context + // that may have no active organisation, so the default tenant-scoped + // find() would not resolve a register referenced purely by its + // numeric id and the `sequence` node would silently yield null. + $register = $this->registerMapper->find($registerRef, false, false); + $registerId = (int) $register->getId(); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Sequence context: could not resolve register "%s": %s', (string) $registerRef, $e->getMessage()) + ); + return null; + } + + if ($registerId <= 0) { + return null; + } + + return new SequenceContext(service: $this->sequences, registerId: $registerId, schemaId: $schemaId); + }//end buildSequenceContext() + /** * Render a calculation result into a JSON-friendly value. * @@ -193,7 +251,7 @@ private function process(ObjectEntity $object, bool $isUpdate): void * * @return mixed JSON-serialisable representation of the value. * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-4 + * @spec openspec/specs/computed-fields/spec.md */ private function serialise(mixed $value): mixed { @@ -211,7 +269,7 @@ private function serialise(mixed $value): mixed * * @return Schema|null Resolved schema, or null on lookup failure. * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-5 + * @spec openspec/specs/computed-fields/spec.md */ private function loadSchema(ObjectEntity $object): ?Schema { @@ -234,7 +292,7 @@ private function loadSchema(ObjectEntity $object): ?Schema * * @return array|null Calculations map, or null when absent. * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-6 + * @spec openspec/specs/computed-fields/spec.md */ private function getCalculations(Schema $schema): ?array { diff --git a/lib/Listener/CommentsEntityListener.php b/lib/Listener/CommentsEntityListener.php index 6c47618dca..e09f60f032 100644 --- a/lib/Listener/CommentsEntityListener.php +++ b/lib/Listener/CommentsEntityListener.php @@ -17,7 +17,7 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-57 + * @spec openspec/specs/object-interactions/spec.md */ declare(strict_types=1); @@ -67,7 +67,7 @@ class CommentsEntityListener implements IEventListener * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( MagicMapper $objectEntityMapper, @@ -87,8 +87,8 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-57 + * @spec openspec/specs/event-driven-architecture/spec.md + * @spec openspec/specs/object-interactions/spec.md */ public function handle(Event $event): void { diff --git a/lib/Listener/FileChangeListener.php b/lib/Listener/FileChangeListener.php index 9f63baee32..fb0519b0f8 100644 --- a/lib/Listener/FileChangeListener.php +++ b/lib/Listener/FileChangeListener.php @@ -25,7 +25,6 @@ namespace OCA\OpenRegister\Listener; use OCA\OpenRegister\BackgroundJob\FileTextExtractionJob; -use OCA\OpenRegister\Service\TextExtractionService; use OCA\OpenRegister\Service\SettingsService; use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\Event; @@ -69,15 +68,13 @@ class FileChangeListener implements IEventListener /** * Constructor * - * @param TextExtractionService $textExtractSvc Text extraction service - * @param SettingsService $settingsService Settings service - * @param IJobList $jobList Job list for queuing background jobs - * @param LoggerInterface $logger Logger + * @param SettingsService $settingsService Settings service + * @param IJobList $jobList Job list for queuing background jobs + * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( - private readonly TextExtractionService $textExtractSvc, private readonly SettingsService $settingsService, private readonly IJobList $jobList, private readonly LoggerInterface $logger @@ -95,7 +92,7 @@ public function __construct( * @SuppressWarnings(PHPMD.ExcessiveMethodLength) File event handling requires comprehensive case coverage * @SuppressWarnings(PHPMD.NPathComplexity) File event handling requires many conditional checks * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { @@ -151,7 +148,7 @@ public function handle(Event $event): void return; } - $this->logger->info( + $this->logger->debug( message: '[FileChangeListener] File event detected - processing', context: [ 'file' => __FILE__, @@ -171,9 +168,13 @@ public function handle(Event $event): void // Handle different extraction modes. switch ($extractionMode) { case 'immediate': - // Process synchronously during upload - direct link between file upload and parsing. - $this->logger->info( - message: '[FileChangeListener] Immediate mode - processing synchronously', + // Previously extracted synchronously on the instance-wide NodeWritten hot + // path, blocking every file write. Enqueue the extraction job instead so + // the write returns immediately; the queued job runs on the next cron tick + // (OPS-14). Behaviour for legitimate callers is unchanged — text still gets + // extracted — only the timing moves off the request path. + $this->logger->debug( + message: '[FileChangeListener] Immediate mode - queueing extraction job', context: [ 'file' => __FILE__, 'line' => __LINE__, @@ -182,14 +183,14 @@ public function handle(Event $event): void ] ); try { - $this->textExtractSvc->extractFile(fileId: $fileId, forceReExtract: false); - $this->logger->info( - message: '[FileChangeListener] Immediate extraction completed', + $this->jobList->add(job: FileTextExtractionJob::class, argument: ['file_id' => $fileId]); + $this->logger->debug( + message: '[FileChangeListener] Immediate-mode extraction job queued', context: ['file' => __FILE__, 'line' => __LINE__, 'file_id' => $fileId] ); } catch (\Exception $e) { $this->logger->error( - message: '[FileChangeListener] Immediate extraction failed', + message: '[FileChangeListener] Failed to queue immediate-mode extraction job', context: [ 'file' => __FILE__, 'line' => __LINE__, @@ -202,7 +203,7 @@ public function handle(Event $event): void case 'background': // Queue background job for delayed extraction on job stack. - $this->logger->info( + $this->logger->debug( message: '[FileChangeListener] Background mode - queueing extraction job', context: [ 'file' => __FILE__, diff --git a/lib/Listener/FilesSidebarListener.php b/lib/Listener/FilesSidebarListener.php index fa27d17872..85a13e8be4 100644 --- a/lib/Listener/FilesSidebarListener.php +++ b/lib/Listener/FilesSidebarListener.php @@ -26,9 +26,9 @@ namespace OCA\OpenRegister\Listener; +use OCA\OpenRegister\Service\ScriptManifestLoader; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; -use OCP\Util; /** * FilesSidebarListener @@ -58,7 +58,7 @@ class FilesSidebarListener implements IEventListener * * @SuppressWarnings(PHPMD.StaticAccess) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function handle(Event $event): void { @@ -70,7 +70,7 @@ public function handle(Event $event): void $jsPath = __DIR__.'/../../js/openregister-filesSidebar.js'; if (file_exists($jsPath) === true) { - Util::addScript('openregister', 'openregister-filesSidebar'); + ScriptManifestLoader::addEntryScripts('openregister', 'filesSidebar', 'openregister-filesSidebar'); } }//end handle() }//end class diff --git a/lib/Listener/FlowActionListener.php b/lib/Listener/FlowActionListener.php new file mode 100644 index 0000000000..0ccd776317 --- /dev/null +++ b/lib/Listener/FlowActionListener.php @@ -0,0 +1,98 @@ + + * SPDX-License-Identifier: EUPL-1.2 + * + * @category Listener + * @package OCA\OpenRegister\Listener + * + * @author Conduction Development Team + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Event\ObjectCreatedEvent; +use OCA\OpenRegister\Event\ObjectDeletedEvent; +use OCA\OpenRegister\Event\ObjectUpdatedEvent; +use OCA\OpenRegister\Service\Flow\FlowActionService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +/** + * Listener that fires declarative schema flows on object-lifecycle events. + * + * @template-implements IEventListener + */ +class FlowActionListener implements IEventListener +{ + /** + * Constructor. + * + * @param FlowActionService $flowActionService The flow runner. + */ + public function __construct( + private readonly FlowActionService $flowActionService + ) { + }//end __construct() + + /** + * Route a lifecycle event to the flow runner with the matching trigger. + * + * @param Event $event The dispatched lifecycle event. + * + * @return void + * + * @spec exclude declarative-flow engine ships without a formal openspec change; spec to be added in a follow-up ADR + */ + public function handle(Event $event): void + { + if ($event instanceof ObjectCreatedEvent) { + $this->dispatch(object: $event->getObject(), trigger: 'created'); + return; + } + + if ($event instanceof ObjectUpdatedEvent) { + $this->dispatch(object: $event->getNewObject(), trigger: 'updated'); + return; + } + + if ($event instanceof ObjectDeletedEvent) { + $this->dispatch(object: $event->getObject(), trigger: 'deleted'); + return; + } + }//end handle() + + /** + * Run flows for an object, guarding against a null payload. + * + * @param ObjectEntity|null $object The object the event carried. + * @param string $trigger The lifecycle trigger. + * + * @return void + */ + private function dispatch(?ObjectEntity $object, string $trigger): void + { + if ($object === null) { + return; + } + + $this->flowActionService->run(object: $object, trigger: $trigger); + }//end dispatch() +}//end class diff --git a/lib/Listener/GraphQLSubscriptionListener.php b/lib/Listener/GraphQLSubscriptionListener.php index 07cded13a6..4229135314 100644 --- a/lib/Listener/GraphQLSubscriptionListener.php +++ b/lib/Listener/GraphQLSubscriptionListener.php @@ -40,7 +40,7 @@ class GraphQLSubscriptionListener implements IEventListener * @param SubscriptionService $subscriptionService Subscription service * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-11 + * @spec openspec/specs/graphql-api/spec.md */ public function __construct( private readonly SubscriptionService $subscriptionService, @@ -55,7 +55,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-11 + * @spec openspec/specs/graphql-api/spec.md */ public function handle(Event $event): void { diff --git a/lib/Listener/HandoffLifecycleListener.php b/lib/Listener/HandoffLifecycleListener.php new file mode 100644 index 0000000000..d15ae041df --- /dev/null +++ b/lib/Listener/HandoffLifecycleListener.php @@ -0,0 +1,138 @@ +` handoffs (ADR-051) when an object + * transitions into the declared state, invoking the same + * {@see \OCA\OpenRegister\Service\Handoff\HandoffService::execute()} used by + * the manual REST trigger. V1 gates lifecycle handoffs to transitions + * performed by a REAL actor — the transitioning user is the handoff actor + * (their session RBAC applies); system-applied transitions (null user) do + * not fire handoffs, so there is no system-user privilege lane. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Listener + * @package OCA\OpenRegister\Listener + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ObjectTransitionedEvent; +use OCA\OpenRegister\Exception\HandoffException; +use OCA\OpenRegister\Service\Handoff\HandoffService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +/** + * Fire declared lifecycle-triggered handoffs on state transitions. + * + * @template-implements IEventListener + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ +class HandoffLifecycleListener implements IEventListener +{ + /** + * Constructor. + * + * @param HandoffService $handoffService The handoff engine. + * @param SchemaMapper $schemaMapper Loads the transitioned object's schema. + * @param LoggerInterface $logger Structured logging. + */ + public function __construct( + private readonly HandoffService $handoffService, + private readonly SchemaMapper $schemaMapper, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Execute every handoff the schema declares for `lifecycle:`. + * + * A hide-mode provider-unavailable outcome is silent (the transition must + * never fail because a peer app is absent); queue mode parks through the + * engine's normal degradation path. Any other handoff failure is logged + * and swallowed — the state transition itself has already happened. + * + * @param Event $event The dispatched event. + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + public function handle(Event $event): void + { + if (($event instanceof ObjectTransitionedEvent) === false) { + return; + } + + // V1: only transitions performed by a real actor fire handoffs. + if ($event->getUserId() === null) { + return; + } + + $object = $event->getObject(); + + try { + $schema = $this->schemaMapper->find(id: (string) $object->getSchema()); + } catch (\Throwable $e) { + return; + } + + $wantedTrigger = 'lifecycle:'.$event->getTo(); + foreach ($this->handoffService->declaredHandoffs(schema: $schema) as $entry) { + if (($entry['trigger'] ?? 'manual') !== $wantedTrigger) { + continue; + } + + $handoffId = (string) ($entry['id'] ?? ''); + + try { + $this->handoffService->execute( + register: (string) $object->getRegister(), + schema: (string) $object->getSchema(), + id: (string) $object->getUuid(), + handoffId: $handoffId + ); + } catch (HandoffException $e) { + // Hide-mode degradation: the transition stays intact and the + // object keeps working standalone; nothing to surface here. + $this->logger->debug( + message: '[HandoffLifecycleListener] Lifecycle handoff degraded: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'handoffId' => $handoffId] + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[HandoffLifecycleListener] Lifecycle handoff failed: '.$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'handoffId' => $handoffId, + 'object' => (string) $object->getUuid(), + ] + ); + }//end try + }//end foreach + + }//end handle() +}//end class diff --git a/lib/Listener/HandoffQueueDrainListener.php b/lib/Listener/HandoffQueueDrainListener.php new file mode 100644 index 0000000000..1633832107 --- /dev/null +++ b/lib/Listener/HandoffQueueDrainListener.php @@ -0,0 +1,110 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Event\SchemaCreatedEvent; +use OCA\OpenRegister\Event\SchemaUpdatedEvent; +use OCA\OpenRegister\Service\Handoff\HandoffService; +use OCP\App\Events\AppEnableEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +/** + * Drain parked queue-mode handoffs when a provider may have appeared. + * + * @template-implements IEventListener + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ +class HandoffQueueDrainListener implements IEventListener +{ + /** + * Constructor. + * + * @param HandoffService $handoffService The handoff engine (drain surface). + * @param LoggerInterface $logger Structured logging. + */ + public function __construct( + private readonly HandoffService $handoffService, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Handle a schema-save or app-enable event by sweeping the parked queue. + * + * Every parked entry whose kind now resolves (the drain re-checks through + * `SemanticTypeResolver`, which covers `allOf`-ancestor inheritance) is + * executed as its original requester; unresolvable kinds stay parked. + * Never lets a drain failure break the triggering save/enable. + * + * @param Event $event The dispatched event. + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + public function handle(Event $event): void + { + $isDrainTrigger = ($event instanceof SchemaCreatedEvent + || $event instanceof SchemaUpdatedEvent + || $event instanceof AppEnableEvent); + if ($isDrainTrigger === false) { + return; + } + + try { + $summary = $this->handoffService->drainParked(); + if ($summary['drained'] > 0 || $summary['failed'] > 0) { + $this->logger->info( + message: '[HandoffQueueDrainListener] Drained parked handoffs after '.get_class($event), + context: ['file' => __FILE__, 'line' => __LINE__, 'summary' => $summary] + ); + } + } catch (\Throwable $e) { + // The triggering save/enable must never fail because of a drain error. + $this->logger->warning( + message: '[HandoffQueueDrainListener] Drain sweep failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'event' => get_class($event)] + ); + } + + }//end handle() +}//end class diff --git a/lib/Listener/HookListener.php b/lib/Listener/HookListener.php index b0b83e2722..03eaf32122 100644 --- a/lib/Listener/HookListener.php +++ b/lib/Listener/HookListener.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-71 + * @spec openspec/specs/schema-hooks/spec.md */ declare(strict_types=1); @@ -72,8 +72,8 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-65 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-71 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-configuration-on-schema + * @spec openspec/specs/schema-hooks/spec.md */ public function handle(Event $event): void { @@ -115,7 +115,7 @@ public function handle(Event $event): void * * @return ObjectEntity|null The object entity or null * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-65 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-configuration-on-schema */ private function getObjectFromEvent(Event $event): ?ObjectEntity { diff --git a/lib/Listener/LifecycleActionListener.php b/lib/Listener/LifecycleActionListener.php new file mode 100644 index 0000000000..08677b1d07 --- /dev/null +++ b/lib/Listener/LifecycleActionListener.php @@ -0,0 +1,216 @@ +setObject()`). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Listener + * @package OCA\OpenRegister\Listener + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ObjectUpdatingEvent; +use OCA\OpenRegister\Service\Lifecycle\LifecycleActionExecutor; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +/** + * Runs declared lifecycle actions on the save path, for every transition form. + * + * @template-implements IEventListener + * + * @spec openspec/specs/object-lifecycle/spec.md + */ +class LifecycleActionListener implements IEventListener +{ + /** + * Constructor. + * + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param LifecycleActionExecutor $executor Runs the transition's declared actions. + * @param LoggerInterface $logger Logger for diagnostics. + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly LifecycleActionExecutor $executor, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Run declared actions for the attempted lifecycle transition. + * + * @param Event $event Inbound dispatcher event. + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + public function handle(Event $event): void + { + if (($event instanceof ObjectUpdatingEvent) === false) { + return; + } + + // A prior listener (transition legality via LifecycleValidationListener, + // or an approval gate) may have rejected or blocked this transition. In + // that case the transition is not happening, so its actions must not run. + if ($event->isPropagationStopped() === true) { + return; + } + + $oldObject = $event->getOldObject(); + if ($oldObject === null) { + // No prior state — this is an initial create, not a transition. + return; + } + + $newObject = $event->getNewObject(); + $schema = $this->loadSchema(object: $newObject); + if ($schema === null) { + return; + } + + $config = ($schema->getConfiguration() ?? []); + $annotation = ($config['x-openregister-lifecycle'] ?? null); + if (is_array($annotation) === false) { + return; + } + + $field = (string) ($annotation['field'] ?? ($annotation['property'] ?? '')); + $transitions = ($annotation['transitions'] ?? []); + if ($field === '' || is_array($transitions) === false || $transitions === []) { + return; + } + + $oldData = ($oldObject->getObject() ?? []); + $newData = ($newObject->getObject() ?? []); + $oldValue = ($oldData[$field] ?? null); + $newValue = ($newData[$field] ?? null); + + if ($oldValue === $newValue || is_string($newValue) === false) { + return; + } + + $matched = $this->matchTransition( + transitions: $transitions, + oldValue: (string) $oldValue, + newValue: $newValue + ); + if ($matched === null) { + // Not a recognised transition — LifecycleValidationListener rejects + // this on its own; there is nothing for the executor to run. + return; + } + + [$action, $spec] = $matched; + + $actions = ($spec['actions'] ?? null); + if (is_array($actions) === false || $actions === []) { + return; + } + + // Fail-loud by design: a missing handler / unparseable condition throws + // out of the executor and is NOT caught here — the exception propagates + // to the save path (HookStoppedException / 500), aborting the transition + // rather than silently dropping the declared action. + $result = $this->executor->run( + actions: $actions, + objectData: $newData, + previousData: $oldData, + transition: $action + ); + + if ($result !== $newData) { + $newObject->setObject($result); + } + + $this->logger->debug( + sprintf('[LifecycleActionListener] executed %d action(s) for transition "%s".', count($actions), $action) + ); + }//end handle() + + /** + * Find the transition (action, spec) whose `to` matches the new value AND + * whose `from` list contains the old value. Mirrors + * `LifecycleValidationListener::findTransitionByTarget()`. + * + * @param array $transitions Transition map from the annotation. + * @param string $oldValue Current lifecycle field value. + * @param string $newValue Attempted lifecycle field value. + * + * @return array{0: string, 1: array}|null + */ + private function matchTransition(array $transitions, string $oldValue, string $newValue): ?array + { + foreach ($transitions as $action => $spec) { + if (is_array($spec) === false || ($spec['to'] ?? null) !== $newValue) { + continue; + } + + $from = ($spec['from'] ?? []); + if (is_string($from) === true) { + $from = [$from]; + } + + if (is_array($from) === true && in_array($oldValue, $from, true) === true) { + return [(string) $action, $spec]; + } + } + + return null; + }//end matchTransition() + + /** + * Load the schema referenced by an object, returning null on failure. + * + * @param ObjectEntity $object Object whose schema reference to resolve. + * + * @return Schema|null Resolved schema, or null on lookup failure. + */ + private function loadSchema(ObjectEntity $object): ?Schema + { + $schemaRef = $object->getSchema(); + if ($schemaRef === null || $schemaRef === '') { + return null; + } + + try { + return $this->schemaMapper->find($schemaRef, _multitenancy: false); + } catch (\Throwable $e) { + return null; + } + }//end loadSchema() +}//end class diff --git a/lib/Listener/LifecycleInitialStateListener.php b/lib/Listener/LifecycleInitialStateListener.php index 32ac3e682f..5164dd5250 100644 --- a/lib/Listener/LifecycleInitialStateListener.php +++ b/lib/Listener/LifecycleInitialStateListener.php @@ -30,6 +30,7 @@ use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Event\ObjectCreatingEvent; +use OCA\OpenRegister\Service\Calculation\ReferenceResolver; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use Psr\Log\LoggerInterface; @@ -40,20 +41,39 @@ * Matches the principle that lifecycle is a declarative property of the * schema — apps shouldn't need to know the initial state. * + * The declared `initial` may be EITHER: + * - a static string (e.g. `"open"`), or + * - a dynamic reference resolved from a related object — declared as + * `{ "from": "", "field": "" }` or the equivalent token + * string `"@ref.."`. The `` must be declared in the + * schema's `x-openregister-references` block; the related object is resolved + * via the same FK plumbing the calculation engine uses (ReferenceResolver), + * and the named field's value becomes the initial state. + * + * Either way the value is force-set ONLY when the caller left the lifecycle + * field empty; a caller-supplied value is never overridden. An unresolvable + * dynamic reference is a logged no-op. + * * @template-implements IEventListener + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) The listener wires the schema mapper, + * the cross-object reference resolver and the logger; each is a distinct collaborator in + * the create-time lifecycle-initialisation path and none can be folded away. */ class LifecycleInitialStateListener implements IEventListener { /** * Wire collaborators used to look up schema lifecycle metadata. * - * @param SchemaMapper $schemaMapper Schema lookup mapper. - * @param LoggerInterface $logger PSR logger for warnings. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param ReferenceResolver $references Cross-object reference resolver (dynamic initial). + * @param LoggerInterface $logger PSR logger for warnings. * * @return void */ public function __construct( private readonly SchemaMapper $schemaMapper, + private readonly ReferenceResolver $references, private readonly LoggerInterface $logger ) { }//end __construct() @@ -65,7 +85,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-12 + * @spec openspec/specs/object-lifecycle/spec.md */ public function handle(Event $event): void { @@ -84,22 +104,176 @@ public function handle(Event $event): void return; } - $field = (string) ($annotation['field'] ?? ''); - $initial = (string) ($annotation['initial'] ?? ''); - if ($field === '' || $initial === '') { + $this->applyInitial(object: $object, schema: $schema, annotation: $annotation); + }//end handle() + + /** + * Apply the resolved initial value to the lifecycle field when it is empty. + * + * @param ObjectEntity $object The object being created. + * @param Schema $schema The resolved schema. + * @param array $annotation The `x-openregister-lifecycle` block. + * + * @return void + */ + private function applyInitial(ObjectEntity $object, Schema $schema, array $annotation): void + { + $field = (string) ($annotation['field'] ?? ''); + if ($field === '' || array_key_exists('initial', $annotation) === false) { return; } $data = $object->getObject() ?? []; // Caller already set a value — leave it alone (validator covers it). + // Resolve the (possibly dynamic) initial value only AFTER this guard so + // we never read a related object when the caller already chose. if (isset($data[$field]) === true && $data[$field] !== null && $data[$field] !== '') { return; } + $initial = $this->resolveInitial( + initial: $annotation['initial'], + object: $object, + schema: $schema, + data: $data + ); + if ($initial === null || $initial === '') { + return; + } + $data[$field] = $initial; $object->setObject($data); - }//end handle() + }//end applyInitial() + + /** + * Resolve the `initial` annotation to a concrete state string. + * + * Static string → returned as-is. A `{ "from": , "field": }` + * dict OR a `"@ref.."` token → resolved from the related object + * declared in `x-openregister-references`. Anything unresolvable yields + * null (a logged no-op upstream). + * + * @param mixed $initial The declared initial value (string or reference dict). + * @param ObjectEntity $object The object being created. + * @param Schema $schema The resolved schema (for its references block). + * @param array $data The object's current data payload. + * + * @return string|null The resolved initial state, or null when unresolvable. + */ + private function resolveInitial(mixed $initial, ObjectEntity $object, Schema $schema, array $data): ?string + { + // Static string form — but recognise the "@ref.." token. + if (is_string($initial) === true) { + if (str_starts_with($initial, '@ref.') === true) { + $parts = explode('.', substr($initial, 5), 2); + $refName = ($parts[0] ?? ''); + $refField = ($parts[1] ?? ''); + return $this->resolveFromReference( + refName: $refName, + field: $refField, + object: $object, + schema: $schema, + data: $data + ); + } + + return $initial; + } + + // Dynamic reference dict { "from": , "field": }. + if (is_array($initial) === true) { + $refName = (string) ($initial['from'] ?? ''); + $field = (string) ($initial['field'] ?? ''); + if ($refName === '' || $field === '') { + return null; + } + + return $this->resolveFromReference( + refName: $refName, + field: $field, + object: $object, + schema: $schema, + data: $data + ); + } + + return null; + }//end resolveInitial() + + /** + * Resolve `` from the related object behind a declared reference name. + * + * Resolves the schema's `x-openregister-references` map (the same FK + * plumbing the calculation engine uses) and reads `` off the + * named reference's resolved data. RBAC + tenant scoped; never throws. + * + * @param string $refName The declared reference name. + * @param string $field The field on the related object to read. + * @param ObjectEntity $object The object being created. + * @param Schema $schema The resolved schema. + * @param array $data The object's current data payload. + * + * @return string|null The related field value as a string, or null when unresolvable. + */ + private function resolveFromReference( + string $refName, + string $field, + ObjectEntity $object, + Schema $schema, + array $data + ): ?string { + if ($refName === '' || $field === '') { + return null; + } + + $references = $this->getReferences(schema: $schema); + if ($references === null || array_key_exists($refName, $references) === false) { + $this->logger->warning( + sprintf('Lifecycle dynamic initial: reference "%s" is not declared on the schema.', $refName) + ); + return null; + } + + $resolved = $this->references->resolveAll( + payload: $data, + references: [$refName => $references[$refName]], + register: $object->getRegister() + ); + + $related = ($resolved[$refName] ?? null); + if (is_array($related) === false || array_key_exists($field, $related) === false) { + $this->logger->warning( + sprintf('Lifecycle dynamic initial: reference "%s" resolved no "%s" field.', $refName, $field) + ); + return null; + } + + $value = $related[$field]; + if ($value === null || $value === '') { + return null; + } + + return (string) $value; + }//end resolveFromReference() + + /** + * Read the `x-openregister-references` configuration block. + * + * @param Schema $schema Schema to inspect. + * + * @return array|null References map, or null when absent. + */ + private function getReferences(Schema $schema): ?array + { + $config = ($schema->getConfiguration() ?? []); + $value = ($config['x-openregister-references'] ?? null); + if (is_array($value) === true && count($value) > 0) { + return $value; + } + + return null; + }//end getReferences() /** * Look up the schema referenced by an object instance. diff --git a/lib/Listener/LifecycleValidationListener.php b/lib/Listener/LifecycleValidationListener.php index 666f5da573..da2a5407b1 100644 --- a/lib/Listener/LifecycleValidationListener.php +++ b/lib/Listener/LifecycleValidationListener.php @@ -32,6 +32,7 @@ use OCA\OpenRegister\Event\ObjectUpdatingEvent; use OCA\OpenRegister\Lifecycle\GuardResult; use OCA\OpenRegister\Service\Lifecycle\LifecycleGuardRegistry; +use OCA\OpenRegister\Service\Object\PermissionHandler; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\IUserSession; @@ -68,10 +69,11 @@ class LifecycleValidationListener implements IEventListener /** * Wire collaborators used to validate transitions and run guards. * - * @param SchemaMapper $schemaMapper Schema lookup mapper. - * @param LifecycleGuardRegistry $guardRegistry Registry resolving guard ids to instances. - * @param IUserSession $userSession Current user session. - * @param LoggerInterface $logger PSR logger for warnings. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param LifecycleGuardRegistry $guardRegistry Registry resolving guard ids to instances. + * @param IUserSession $userSession Current user session. + * @param PermissionHandler $permissionHandler RBAC handler used to evaluate declarative per-transition authorization. + * @param LoggerInterface $logger PSR logger for warnings. * * @return void */ @@ -79,6 +81,7 @@ public function __construct( private readonly SchemaMapper $schemaMapper, private readonly LifecycleGuardRegistry $guardRegistry, private readonly IUserSession $userSession, + private readonly PermissionHandler $permissionHandler, private readonly LoggerInterface $logger ) { }//end __construct() @@ -93,7 +96,7 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-25-bw-jobs-listeners/tasks.md#task-13 + * @spec openspec/specs/object-lifecycle/spec.md */ public function handle(Event $event): void { @@ -119,7 +122,10 @@ public function handle(Event $event): void return; } - $field = (string) ($annotation['field'] ?? ''); + // Accept `property` as an additive alias for `field` so schemas authored + // against the procest migration shape work verbatim. `field` wins when + // both are present. + $field = (string) ($annotation['field'] ?? ($annotation['property'] ?? '')); $oldData = $oldObject->getObject() ?? []; $newData = $newObject->getObject() ?? []; @@ -171,7 +177,42 @@ public function handle(Event $event): void } [$action, $spec] = $matched; - $requires = ($spec['requires'] ?? null); + + // Declarative per-transition authorization (Engine 1). When the matched + // transition lists `authorization` (NC group ids and/or `{ "role": "" }` + // entries), the caller MUST satisfy it. This is the group-based gate + // procest's role-routing needs WITHOUT a bespoke PHP guard per role. + // Evaluated BEFORE the `requires` guard so an unauthorized caller is + // rejected with a 403-shaped error before any guard side-channel runs. + // Transitions without an `authorization` key skip this entirely + // (additive / backward-compatible). + $authorizationList = ($spec['authorization'] ?? null); + if (is_array($authorizationList) === true && $authorizationList !== []) { + $userId = ($this->userSession->getUser()?->getUID() ?? null); + if ($this->permissionHandler->isTransitionAuthorized( + authorizationList: $authorizationList, + userId: $userId, + schema: $schema + ) === false + ) { + $this->reject( + event: $event, + error: [ + 'code' => 'lifecycle-transition-unauthorized', + 'field' => $field, + 'action' => $action, + 'message' => sprintf( + 'You are not authorized to perform transition "%s" on "%s".', + $action, + $field + ), + ] + ); + return; + } + }//end if + + $requires = ($spec['requires'] ?? null); if (is_string($requires) === true && $requires !== '') { $userId = ($this->userSession->getUser()?->getUID() ?? ''); $guard = $this->guardRegistry->resolve($requires); @@ -211,7 +252,13 @@ private function findTransitionByTarget(array $transitions, string $oldValue, st continue; } + // `from` may be a single state string or a list of states. Coerce + // a string to a one-element list so both authoring shapes work. $from = ($spec['from'] ?? []); + if (is_string($from) === true) { + $from = [$from]; + } + if (is_array($from) === false) { continue; } @@ -219,7 +266,7 @@ private function findTransitionByTarget(array $transitions, string $oldValue, st if (in_array($oldValue, $from, true) === true) { return [(string) $action, $spec]; } - } + }//end foreach return null; }//end findTransitionByTarget() diff --git a/lib/Listener/MailAppScriptListener.php b/lib/Listener/MailAppScriptListener.php index c00c458b32..240d50ad96 100644 --- a/lib/Listener/MailAppScriptListener.php +++ b/lib/Listener/MailAppScriptListener.php @@ -17,8 +17,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-annotate-openregister-2026-04-23/tasks.md#task-48 - * @spec openspec/changes/retrofit-annotate-openregister-2026-04-30/tasks.md#task-52 + * @spec openspec/specs/mail-sidebar/spec.md#requirement-mail-app-script-injection-via-event-listener + * @spec openspec/specs/mail-sidebar/spec.md */ declare(strict_types=1); @@ -26,6 +26,7 @@ namespace OCA\OpenRegister\Listener; use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Service\ScriptManifestLoader; use OCP\App\IAppManager; use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\AppFramework\Http\TemplateResponse; @@ -74,8 +75,8 @@ public function __construct( * * @SuppressWarnings(PHPMD.StaticAccess) * - * @spec openspec/changes/retrofit-annotate-openregister-2026-04-23/tasks.md#task-48 - * @spec openspec/changes/retrofit-annotate-openregister-2026-04-30/tasks.md#task-52 + * @spec openspec/specs/mail-sidebar/spec.md#requirement-mail-app-script-injection-via-event-listener + * @spec openspec/specs/mail-sidebar/spec.md */ public function handle(Event $event): void { @@ -111,7 +112,7 @@ public function handle(Event $event): void // Inject the sidebar script (only if compiled JS exists). $jsPath = __DIR__.'/../../js/openregister-mail-sidebar.js'; if (file_exists($jsPath) === true) { - Util::addScript('openregister', 'openregister-mail-sidebar'); + ScriptManifestLoader::addEntryScripts('openregister', 'mailSidebar', 'openregister-mail-sidebar'); Util::addStyle('openregister', 'mail-sidebar'); } @@ -128,7 +129,7 @@ public function handle(Event $event): void * * @return bool True if the user has register access. * - * @spec openspec/changes/retrofit-annotate-openregister-2026-04-23/tasks.md#task-48 + * @spec openspec/specs/mail-sidebar/spec.md#requirement-mail-app-script-injection-via-event-listener */ private function userHasRegisterAccess(): bool { diff --git a/lib/Listener/NotificationDedupeAnnotationSyncListener.php b/lib/Listener/NotificationDedupeAnnotationSyncListener.php new file mode 100644 index 0000000000..e57cfeb060 --- /dev/null +++ b/lib/Listener/NotificationDedupeAnnotationSyncListener.php @@ -0,0 +1,130 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\NotificationDedupeStateMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Event\SchemaCreatedEvent; +use OCA\OpenRegister\Event\SchemaUpdatedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +/** + * Drops orphan dedup-state rows for removed/renamed scheduled rules. + * + * @template-implements IEventListener + */ +final class NotificationDedupeAnnotationSyncListener implements IEventListener +{ + /** + * Constructor. + * + * @param NotificationDedupeStateMapper $mapper Per-object dedup state mapper. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly NotificationDedupeStateMapper $mapper, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Handle the event. + * + * @param Event $event Dispatched event. + * + * @return void + */ + public function handle(Event $event): void + { + $schema = $this->resolveSchema(event: $event); + if ($schema === null) { + return; + } + + $schemaId = (int) $schema->getId(); + if ($schemaId === 0) { + return; + } + + try { + $config = ((array) ($schema->getConfiguration() ?? [])); + $notifications = ($config['x-openregister-notifications'] ?? []); + $current = []; + if (is_array($notifications) === true) { + foreach (array_keys($notifications) as $key) { + $current[] = (string) $key; + } + } + + $tracked = $this->mapper->listRuleKeysForSchema(schemaId: $schemaId); + $orphan = array_diff($tracked, $current); + foreach ($orphan as $key) { + $this->mapper->deleteByRule(schemaId: $schemaId, ruleKey: (string) $key); + } + } catch (\Throwable $e) { + $this->logger->debug( + sprintf('[NotificationDedupeAnnotationSyncListener] sync skipped: %s', $e->getMessage()) + ); + } + }//end handle() + + /** + * Extract the Schema entity from a create/update event. + * + * @param Event $event Event instance. + * + * @return Schema|null Schema when the event exposes one, null otherwise. + */ + private function resolveSchema(Event $event): ?Schema + { + if ($event instanceof SchemaCreatedEvent) { + $schema = $event->getSchema(); + if ($schema instanceof Schema) { + return $schema; + } + + return null; + } + + if ($event instanceof SchemaUpdatedEvent) { + $schema = $event->getNewSchema(); + if ($schema instanceof Schema) { + return $schema; + } + + return null; + } + + return null; + }//end resolveSchema() +}//end class diff --git a/lib/Listener/NotificationDedupePruneListener.php b/lib/Listener/NotificationDedupePruneListener.php new file mode 100644 index 0000000000..69cbad37f5 --- /dev/null +++ b/lib/Listener/NotificationDedupePruneListener.php @@ -0,0 +1,83 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\NotificationDedupeStateMapper; +use OCA\OpenRegister\Event\ObjectDeletedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +/** + * Prunes dedup-state rows on object purge. + * + * @template-implements IEventListener + */ +final class NotificationDedupePruneListener implements IEventListener +{ + /** + * Constructor. + * + * @param NotificationDedupeStateMapper $mapper Per-object dedup state mapper. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly NotificationDedupeStateMapper $mapper, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Handle the event by deleting every dedup row tied to the object UUID. + * + * @param Event $event Dispatched event. + * + * @return void + */ + public function handle(Event $event): void + { + if (($event instanceof ObjectDeletedEvent) === false) { + return; + } + + try { + $uuid = (string) $event->getObject()->getUuid(); + if ($uuid === '') { + return; + } + + $this->mapper->deleteByObject(objectUuid: $uuid); + } catch (\Throwable $e) { + $this->logger->debug( + sprintf('[NotificationDedupePruneListener] prune skipped: %s', $e->getMessage()) + ); + } + }//end handle() +}//end class diff --git a/lib/Listener/NotifyPushListener.php b/lib/Listener/NotifyPushListener.php index 7225ae876f..36ea516160 100644 --- a/lib/Listener/NotifyPushListener.php +++ b/lib/Listener/NotifyPushListener.php @@ -156,14 +156,14 @@ public function __construct( * extracted private helper methods (resolveEventAction, resolveQueue, dispatchPushes, * accumulateBatchEntry) into this orchestrator's score; handle() itself only contains * lightweight guard checks and delegates to those helpers. - * @SuppressWarnings(PHPMD.NPathComplexity) NPath inflation mirrors the CC accumulation + * @SuppressWarnings(PHPMD.NPathComplexity) NPath inflation mirrors the CC accumulation * issue; the orchestrator cannot be simplified further without removing necessary guards. * * @spec openspec/changes/add-live-updates/tasks.md#task-4 */ public function handle(Event $event): void { - $resolved = $this->resolveEventAction($event); + $resolved = $this->resolveEventAction(event: $event); if ($resolved === null) { return; } @@ -349,7 +349,7 @@ public static function setBatchMode(bool $enabled): void * * @internal For use in unit tests only. * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-7 + * @spec openspec/specs/realtime-updates/spec.md */ public static function resetStaticState(): void { @@ -359,30 +359,78 @@ public static function resetStaticState(): void self::$queueUnavailable = false; }//end resetStaticState() + /** + * Whether any (register, schema) pairs have been accumulated in batch mode. + * + * Lets import callers skip IQueue resolution entirely when nothing was + * accumulated (e.g. notify_push not installed, so handle() never reached + * the accumulator) — avoiding even the one DEBUG log of a failed resolve. + * + * @return bool True when at least one collection event is pending flush. + * + * @spec openspec/specs/realtime-updates/spec.md + */ + public static function hasBatchedCollections(): bool + { + return self::$batchedCollections !== []; + }//end hasBatchedCollections() + + /** + * Accumulate a (register-slug, schema-slug) pair directly, bypassing event dispatch. + * + * Bulk saves run with lifecycle events DISABLED by default (`events=false` + * throughout ImportService / SaveObjects), so handle() never fires during a + * standard import and the accumulator would stay empty. Import callers that + * already know which collection changed use this entry point to queue the + * collection hint themselves; flushBatch() then broadcasts it. Duplicate + * pairs collapse onto the same accumulator key — safe to combine with + * event-driven accumulation when events ARE enabled. + * + * @param string $registerSlug The register slug (empty = ignored). + * @param string $schemaSlug The schema slug (empty = ignored). + * + * @return void + * + * @spec openspec/specs/realtime-updates/spec.md + */ + public static function addBatchedCollection(string $registerSlug, string $schemaSlug): void + { + if ($registerSlug === '' || $schemaSlug === '') { + return; + } + + self::$batchedCollections[$registerSlug.'|'.$schemaSlug] = [ + 'register' => $registerSlug, + 'schema' => $schemaSlug, + ]; + }//end addBatchedCollection() + /** * Emit one collection event per accumulated (register, schema) pair and clear state. * - * Should be called in a `finally` block after a bulk-import loop: + * Should be called in a `finally` block after a bulk-import loop, BEFORE + * setBatchMode(false) — disabling batch mode clears the accumulator: * ```php * NotifyPushListener::setBatchMode(true); * try { * // ... import loop * } finally { - * NotifyPushListener::flushBatch($queue, $permissionHandler); + * NotifyPushListener::flushBatch($queue); * NotifyPushListener::setBatchMode(false); * } * ``` * - * @param object $queue Resolved IQueue instance. - * @param PermissionHandler $permHandler Permission handler for user resolution. + * The flush is a broadcast: collection events carry no per-user targeting + * (payload is slugs + action only); clients refetch through the RBAC-filtered + * REST API, so no data can leak to unauthorised subscribers. * - * @return void + * @param object $queue Resolved IQueue instance. * - * @spec openspec/changes/add-live-updates/tasks.md#task-4 + * @return void * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) Permission handler retained on the signature for upcoming per-user batch routing + * @spec openspec/specs/realtime-updates/spec.md */ - public static function flushBatch(object $queue, PermissionHandler $permHandler): void + public static function flushBatch(object $queue): void { foreach (self::$batchedCollections as $entry) { $registerSlug = $entry['register']; @@ -424,7 +472,7 @@ public static function flushBatch(object $queue, PermissionHandler $permHandler) * * @return object|null The IQueue instance, or null when unavailable. * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-8 + * @spec openspec/specs/realtime-updates/spec.md */ private function resolveQueue(): ?object { @@ -455,7 +503,7 @@ private function resolveQueue(): ?object * * @return string|null The register slug, or null when not resolvable. * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-9 + * @spec openspec/specs/realtime-updates/spec.md */ private function resolveRegisterSlug(?string $registerUuid): ?string { @@ -487,7 +535,7 @@ private function resolveRegisterSlug(?string $registerUuid): ?string * * @return string|null The schema slug, or null when not resolvable. * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-10 + * @spec openspec/specs/realtime-updates/spec.md */ private function resolveSchemaSlug(?string $schemaUuid): ?string { diff --git a/lib/Listener/ObjectChangeListener.php b/lib/Listener/ObjectChangeListener.php index 864f32ddf5..630d3f5909 100644 --- a/lib/Listener/ObjectChangeListener.php +++ b/lib/Listener/ObjectChangeListener.php @@ -58,7 +58,7 @@ class ObjectChangeListener implements IEventListener * @param IJobList $jobList Job list for queuing background jobs * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private readonly TextExtractionService $textExtractSvc, @@ -75,7 +75,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { @@ -138,7 +138,7 @@ public function handle(Event $event): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function processExtractionMode(string $mode, int $objectId, string $objectUuid): void { @@ -173,7 +173,7 @@ private function processExtractionMode(string $mode, int $objectId, string $obje * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function processImmediateExtraction(int $objectId, string $objectUuid): void { @@ -214,7 +214,7 @@ private function processImmediateExtraction(int $objectId, string $objectUuid): * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function processBackgroundExtraction(int $objectId, string $objectUuid): void { @@ -254,7 +254,7 @@ private function processBackgroundExtraction(int $objectId, string $objectUuid): * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function processCronMode(int $objectId): void { @@ -271,7 +271,7 @@ private function processCronMode(int $objectId): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function processManualMode(int $objectId): void { @@ -289,7 +289,7 @@ private function processManualMode(int $objectId): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function processUnknownMode(string $mode, int $objectId): void { diff --git a/lib/Listener/ObjectCleanupListener.php b/lib/Listener/ObjectCleanupListener.php index abace28f39..1babfed949 100644 --- a/lib/Listener/ObjectCleanupListener.php +++ b/lib/Listener/ObjectCleanupListener.php @@ -118,7 +118,7 @@ class ObjectCleanupListener implements IEventListener * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( NoteService $noteService, @@ -148,7 +148,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { @@ -185,7 +185,7 @@ public function handle(Event $event): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function cleanupNotes(string $objectUuid): void { @@ -207,7 +207,7 @@ private function cleanupNotes(string $objectUuid): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function cleanupTasks(string $objectUuid): void { @@ -242,7 +242,7 @@ private function cleanupTasks(string $objectUuid): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function cleanupEmails(string $objectUuid): void { @@ -266,7 +266,7 @@ private function cleanupEmails(string $objectUuid): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function cleanupCalendarEvents(string $objectUuid): void { @@ -288,7 +288,7 @@ private function cleanupCalendarEvents(string $objectUuid): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function cleanupContacts(string $objectUuid): void { @@ -310,7 +310,7 @@ private function cleanupContacts(string $objectUuid): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function cleanupDeckCards(string $objectUuid): void { diff --git a/lib/Listener/ObjectMetricsListener.php b/lib/Listener/ObjectMetricsListener.php new file mode 100644 index 0000000000..963d7ac6ab --- /dev/null +++ b/lib/Listener/ObjectMetricsListener.php @@ -0,0 +1,186 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Event\ObjectCreatedEvent; +use OCA\OpenRegister\Event\ObjectDeletedEvent; +use OCA\OpenRegister\Event\ObjectUpdatedEvent; +use OCA\OpenRegister\Service\MetricsService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Persist a CRUD metric row for every object create / update / delete. + * + * @template-implements IEventListener + * + * @spec openspec/specs/production-observability/spec.md#crud-operation-counters + */ +class ObjectMetricsListener implements IEventListener +{ + /** + * Wire collaborators. + * + * @param MetricsService $metricsService Operational metric writer (fail-soft). + * @param IUserSession $userSession Session, to attribute the metric to a user. + * @param LoggerInterface $logger PSR logger for warnings. + * + * @return void + * + * @spec openspec/specs/production-observability/spec.md#crud-operation-counters + */ + public function __construct( + private readonly MetricsService $metricsService, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Record the matching CRUD metric for an inbound object lifecycle event. + * + * @param Event $event Inbound dispatcher event. + * + * @return void + * + * @spec openspec/specs/production-observability/spec.md#crud-operation-counters + */ + public function handle(Event $event): void + { + $metricType = $this->metricTypeFor(event: $event); + if ($metricType === null) { + return; + } + + try { + $object = $this->objectFor(event: $event); + if ($object === null) { + return; + } + + $this->metricsService->recordMetric( + metricType: $metricType, + entityType: 'object', + entityId: $object->getUuid(), + status: 'success', + durationMs: null, + metadata: [ + 'register' => (string) ($object->getRegister() ?? ''), + 'schema' => (string) ($object->getSchema() ?? ''), + ], + errorMessage: null, + userId: $this->currentUserId() + ); + } catch (Throwable $e) { + // Observability must never break the write it observes. + $this->logger->warning( + message: '[ObjectMetricsListener] Failed to record CRUD metric', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'metric_type' => $metricType, + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end handle() + + /** + * Map an inbound event to its metric type constant. + * + * @param Event $event Inbound dispatcher event. + * + * @return string|null The metric type, or null when the event is not one we track. + * + * @spec openspec/specs/production-observability/spec.md#crud-operation-counters + */ + private function metricTypeFor(Event $event): ?string + { + return match (true) { + $event instanceof ObjectCreatedEvent => MetricsService::METRIC_OBJECT_CREATED, + $event instanceof ObjectUpdatedEvent => MetricsService::METRIC_OBJECT_UPDATED, + $event instanceof ObjectDeletedEvent => MetricsService::METRIC_OBJECT_DELETED, + default => null, + }; + }//end metricTypeFor() + + /** + * Extract the subject object from an inbound event. + * + * @param Event $event Inbound dispatcher event. + * + * @return ObjectEntity|null The affected object, or null when the event is not one we track. + * + * @spec openspec/specs/production-observability/spec.md#crud-operation-counters + */ + private function objectFor(Event $event): ?ObjectEntity + { + return match (true) { + $event instanceof ObjectUpdatedEvent => $event->getNewObject(), + $event instanceof ObjectCreatedEvent, $event instanceof ObjectDeletedEvent => $event->getObject(), + default => null, + }; + }//end objectFor() + + /** + * Resolve the acting user id, when there is a session. + * + * @return string|null The acting user id, or null for background / system writes. + * + * @spec openspec/specs/production-observability/spec.md#crud-operation-counters + */ + private function currentUserId(): ?string + { + $user = $this->userSession->getUser(); + if ($user === null) { + return null; + } + + return $user->getUID(); + }//end currentUserId() +}//end class diff --git a/lib/Listener/OcmResourceTypeListener.php b/lib/Listener/OcmResourceTypeListener.php new file mode 100644 index 0000000000..fbcef976bb --- /dev/null +++ b/lib/Listener/OcmResourceTypeListener.php @@ -0,0 +1,61 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @template-implements IEventListener + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\OCM\Events\ResourceTypeRegisterEvent; + +/** + * Registers the `openregister` resource type in the OCM discovery document. + * + * @spec exclude federation ships without a formal openspec change; spec to be added in a follow-up ADR + */ +class OcmResourceTypeListener implements IEventListener +{ + /** + * Handle the resource-type registration event. + * + * @param Event $event The dispatched event. + * + * @return void + * + * @spec exclude federation ships without a formal openspec change; spec to be added in a follow-up ADR + */ + public function handle(Event $event): void + { + if (($event instanceof ResourceTypeRegisterEvent) === false) { + return; + } + + $event->registerResourceType( + 'openregister', + ['user', 'group'], + ['openregister' => '/apps/openregister/api/federation'] + ); + }//end handle() +}//end class diff --git a/lib/Listener/PushClientScriptListener.php b/lib/Listener/PushClientScriptListener.php new file mode 100644 index 0000000000..9744b57157 --- /dev/null +++ b/lib/Listener/PushClientScriptListener.php @@ -0,0 +1,90 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IUserSession; +use OCP\Util; + +/** + * Injects the opt-in Web Push subscribe client on every authenticated page. + * + * @template-implements IEventListener + * + * @psalm-suppress UnusedClass + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ +class PushClientScriptListener implements IEventListener +{ + /** + * Constructor. + * + * @param IUserSession $userSession The current user session. + */ + public function __construct( + private readonly IUserSession $userSession + ) { + }//end __construct() + + /** + * Handle the event. + * + * @param Event $event The dispatched event. + * + * @return void + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function handle(Event $event): void + { + if ($event instanceof BeforeTemplateRenderedEvent === false) { + return; + } + + // Only for an authenticated user (subscriptions are user-scoped). + if ($this->userSession->getUser() === null) { + return; + } + + // Load the static, opt-in subscribe client (no prompt on load). + if (file_exists(__DIR__.'/../../js/openregister-push-client.js') === true) { + Util::addScript('openregister', '../js/openregister-push-client'); + } + }//end handle() +}//end class diff --git a/lib/Listener/QualityScoreOnSaveListener.php b/lib/Listener/QualityScoreOnSaveListener.php new file mode 100644 index 0000000000..7f71442186 --- /dev/null +++ b/lib/Listener/QualityScoreOnSaveListener.php @@ -0,0 +1,220 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use DateTimeImmutable; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ObjectCreatingEvent; +use OCA\OpenRegister\Event\ObjectUpdatingEvent; +use OCA\OpenRegister\Service\Quality\QualityScorer; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Materialises a declared data-quality score into the object payload on save. + * + * @template-implements IEventListener + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-2 + */ +class QualityScoreOnSaveListener implements IEventListener +{ + /** + * Default field the score is written to when the annotation omits `field`. + * + * @var string + */ + private const DEFAULT_FIELD = 'qualityScore'; + + /** + * Wire collaborators used to look up and score the schema's quality rules. + * + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param QualityScorer $scorer Pure data-quality scorer. + * @param LoggerInterface $logger PSR logger for warnings. + * + * @return void + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-2 + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly QualityScorer $scorer, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Run quality scoring before the object is persisted. + * + * @param Event $event Inbound dispatcher event. + * + * @return void + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-2 + */ + public function handle(Event $event): void + { + if ($event instanceof ObjectCreatingEvent) { + $this->process(object: $event->getObject()); + return; + } + + if ($event instanceof ObjectUpdatingEvent) { + $this->process(object: $event->getNewObject()); + return; + } + }//end handle() + + /** + * Compute and patch the quality score onto the object data. + * + * @param ObjectEntity $object Object being created or updated. + * + * @return void + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-2 + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) The method runs the linear score-on-save + * pipeline (load schema, read annotation, score, patch score, optionally patch status); + * the guards short-circuit absent annotations and the steps share one payload, so + * extracting them would only scatter a strictly-sequential flow across helpers. + * @SuppressWarnings(PHPMD.NPathComplexity) Same rationale — sequential save-time guards. + */ + private function process(ObjectEntity $object): void + { + $schema = $this->loadSchema(object: $object); + if ($schema === null) { + return; + } + + $quality = $this->getQuality(schema: $schema); + if ($quality === null) { + return; + } + + $rules = ($quality['rules'] ?? null); + if (is_array($rules) === false || count($rules) === 0) { + return; + } + + $data = ($object->getObject() ?? []); + $changed = false; + + try { + $score = $this->scorer->score(object: $data, rules: $rules, now: new DateTimeImmutable()); + } catch (Throwable $e) { + $this->logger->warning( + sprintf( + 'Quality scoring failed on %s: %s', + (string) $object->getUuid(), + $e->getMessage() + ) + ); + return; + } + + $field = (string) ($quality['field'] ?? self::DEFAULT_FIELD); + if ($field === '') { + $field = self::DEFAULT_FIELD; + } + + if (($data[$field] ?? null) !== $score) { + $data[$field] = $score; + $changed = true; + } + + $statusField = (string) ($quality['statusField'] ?? ''); + if ($statusField !== '') { + $thresholds = ($quality['thresholds'] ?? []); + if (is_array($thresholds) === false) { + $thresholds = []; + } + + $status = $this->scorer->status(score: $score, thresholds: $thresholds); + if (($data[$statusField] ?? null) !== $status) { + $data[$statusField] = $status; + $changed = true; + } + } + + if ($changed === true) { + $object->setObject($data); + } + }//end process() + + /** + * Look up the schema referenced by an object instance. + * + * @param ObjectEntity $object Object whose schema reference to resolve. + * + * @return Schema|null Resolved schema, or null on lookup failure. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-2 + */ + private function loadSchema(ObjectEntity $object): ?Schema + { + $ref = $object->getSchema(); + if ($ref === null || $ref === '') { + return null; + } + + try { + return $this->schemaMapper->find($ref, _multitenancy: false); + } catch (Throwable $e) { + return null; + } + }//end loadSchema() + + /** + * Read the `x-openregister-quality` configuration block. + * + * @param Schema $schema Schema to inspect. + * + * @return array|null Quality config, or null when absent. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-2 + */ + private function getQuality(Schema $schema): ?array + { + $config = ($schema->getConfiguration() ?? []); + $value = ($config['x-openregister-quality'] ?? null); + if (is_array($value) === true && count($value) > 0) { + return $value; + } + + return null; + }//end getQuality() +}//end class diff --git a/lib/Listener/RealtimeEventListener.php b/lib/Listener/RealtimeEventListener.php deleted file mode 100644 index 8b57e01638..0000000000 --- a/lib/Listener/RealtimeEventListener.php +++ /dev/null @@ -1,107 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Listener; - -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Event\ObjectCreatedEvent; -use OCA\OpenRegister\Event\ObjectDeletedEvent; -use OCA\OpenRegister\Event\ObjectTransitionedEvent; -use OCA\OpenRegister\Event\ObjectUpdatedEvent; -use OCA\OpenRegister\Service\RealtimeService; -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventListener; - -/** - * Listener that records realtime events for object lifecycle changes. - * - * @template-implements IEventListener - */ -class RealtimeEventListener implements IEventListener -{ - /** - * Wire the realtime service used to record events. - * - * @param RealtimeService $realtimeService Service that persists realtime events. - * - * @return void - * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-11 - */ - public function __construct( - private readonly RealtimeService $realtimeService - ) { - }//end __construct() - - /** - * Dispatch the inbound event onto the realtime event log. - * - * @param Event $event Inbound dispatcher event. - * - * @return void - * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-12 - */ - public function handle(Event $event): void - { - if ($event instanceof ObjectCreatedEvent) { - $object = $event->getObject(); - if ($object instanceof ObjectEntity) { - $this->realtimeService->record(RealtimeService::TYPE_OBJECT_CREATED, $object); - } - - return; - } - - if ($event instanceof ObjectUpdatedEvent) { - $object = $event->getNewObject(); - if ($object instanceof ObjectEntity) { - $this->realtimeService->record(RealtimeService::TYPE_OBJECT_UPDATED, $object); - } - - return; - } - - if ($event instanceof ObjectDeletedEvent) { - $object = $event->getObject(); - if ($object instanceof ObjectEntity) { - $this->realtimeService->record(RealtimeService::TYPE_OBJECT_DELETED, $object); - } - - return; - } - - if ($event instanceof ObjectTransitionedEvent) { - $object = $event->getObject(); - if ($object instanceof ObjectEntity) { - $this->realtimeService->record( - RealtimeService::TYPE_OBJECT_TRANSITIONED, - $object, - ['action' => $event->getAction(), 'from' => $event->getFrom(), 'to' => $event->getTo()] - ); - } - } - }//end handle() -}//end class diff --git a/lib/Listener/SourceRecordChangeListener.php b/lib/Listener/SourceRecordChangeListener.php new file mode 100644 index 0000000000..ff1bccd50b --- /dev/null +++ b/lib/Listener/SourceRecordChangeListener.php @@ -0,0 +1,394 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ObjectCreatedEvent; +use OCA\OpenRegister\Event\ObjectDeletedEvent; +use OCA\OpenRegister\Event\ObjectUpdatedEvent; +use OCA\OpenRegister\Event\SchemaCreatedEvent; +use OCA\OpenRegister\Event\SchemaDeletedEvent; +use OCA\OpenRegister\Event\SchemaUpdatedEvent; +use OCA\OpenRegister\Service\Merge\MergeService; +use OCA\OpenRegister\Service\ObjectService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\ICache; +use OCP\ICacheFactory; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Recompute a reverse-FK master when one of its source objects changes. + * + * Also subscribed to schema lifecycle events so the cross-request reverse-FK + * index cache is invalidated the moment a schema (and therefore any reverse-FK + * sourceLink declaration) changes. + * + * @template-implements IEventListener + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Handles both object and schema lifecycle events plus the index cache + */ +class SourceRecordChangeListener implements IEventListener +{ + + /** + * Distributed-cache key holding the serialized reverse index. + * + * @var string + */ + private const REVERSE_INDEX_CACHE_KEY = 'reverse_fk_index'; + + /** + * Time-to-live for the cached reverse index, in seconds. The index is + * additionally invalidated eagerly on every schema create/update/delete + * event, so the TTL is only a safety net against missed invalidations + * (e.g. schema changes applied through raw SQL or another instance + * without a distributed cache). + * + * @var int + */ + private const REVERSE_INDEX_CACHE_TTL = 3600; + + /** + * Memoised reverse index: source-schema identifier (slug or id, as + * strings) => list of reference-field names that carry the master uuid. + * Built lazily from the schema registry, cached per listener instance + * and shared across requests through the distributed cache — building + * it requires loading EVERY schema, which is far too expensive to do + * on the first object write of every request. + * + * @var array>|null + */ + private ?array $reverseIndex = null; + + /** + * Distributed cache holding the reverse index across requests. + * + * @var ICache|null + */ + private ?ICache $indexCache = null; + + /** + * Wire collaborators. + * + * @param SchemaMapper $schemaMapper Schema registry (to build the reverse index). + * @param ObjectService $objectService Object read/write path (RBAC + tenant scoped). + * @param LoggerInterface $logger PSR logger for warnings. + * @param ICacheFactory $cacheFactory Distributed-cache factory for the reverse index. + * + * @return void + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly ObjectService $objectService, + private readonly LoggerInterface $logger, + ICacheFactory $cacheFactory + ) { + try { + $this->indexCache = $cacheFactory->createDistributed('openregister_reverse_fk'); + } catch (Throwable $e) { + $this->indexCache = null; + } + }//end __construct() + + /** + * Dispatch: recompute the referenced master(s) after a source object is + * created, updated, or deleted, and invalidate the cached reverse index + * when a schema changes. + * + * @param Event $event Inbound dispatcher event. + * + * @return void + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + */ + public function handle(Event $event): void + { + try { + if ($event instanceof SchemaCreatedEvent + || $event instanceof SchemaUpdatedEvent + || $event instanceof SchemaDeletedEvent + ) { + // A schema change can add/remove reverse-FK sourceLink + // declarations — drop the cached index so it is rebuilt. + $this->reverseIndex = null; + $this->indexCache?->remove(self::REVERSE_INDEX_CACHE_KEY); + return; + } + + if ($event instanceof ObjectCreatedEvent || $event instanceof ObjectDeletedEvent) { + $this->processSource(object: $event->getObject(), oldObject: null); + return; + } + + if ($event instanceof ObjectUpdatedEvent) { + $this->processSource(object: $event->getNewObject(), oldObject: $event->getOldObject()); + return; + } + } catch (Throwable $e) { + $this->logger->warning('Source-record change recompute failed: '.$e->getMessage()); + }//end try + }//end handle() + + /** + * Recompute the master(s) referenced by a changed source object. + * + * @param ObjectEntity $object The changed source object. + * @param ObjectEntity|null $oldObject The pre-change source object (updates only). + * + * @return void + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + */ + private function processSource(ObjectEntity $object, ?ObjectEntity $oldObject): void + { + $referenceFields = $this->referenceFieldsFor(object: $object); + if (empty($referenceFields) === true) { + return; + } + + $data = ($object->getObject() ?? []); + $oldData = []; + if ($oldObject !== null) { + $oldData = ($oldObject->getObject() ?? []); + } + + $masterUuids = []; + foreach ($referenceFields as $referenceField) { + $masterUuids[] = (string) ($data[$referenceField] ?? ''); + $masterUuids[] = (string) ($oldData[$referenceField] ?? ''); + } + + foreach (array_unique(array_filter($masterUuids)) as $masterUuid) { + $this->recomputeMaster(masterUuid: (string) $masterUuid); + } + }//end processSource() + + /** + * The reference-field names that make the given object a reverse-FK source + * of some master schema, or an empty array when it is not a source object. + * + * @param ObjectEntity $object The saved object. + * + * @return array Reference-field names (may be empty). + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + */ + private function referenceFieldsFor(ObjectEntity $object): array + { + $schema = $this->resolveSchema(ref: (string) $object->getSchema()); + if ($schema === null) { + return []; + } + + $index = $this->reverseIndex(); + $candidates = [(string) $schema->getId(), (string) $schema->getSlug()]; + + $fields = []; + foreach ($candidates as $key) { + if ($key !== '' && isset($index[$key]) === true) { + $fields = array_merge($fields, $index[$key]); + } + } + + return array_values(array_unique($fields)); + }//end referenceFieldsFor() + + /** + * Build (once) the reverse index of source-schema identifier => reference + * fields, from every schema whose survivorship annotation declares a + * reverse-FK `sourceLink`. + * + * @return array> + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + */ + private function reverseIndex(): array + { + if ($this->reverseIndex !== null) { + return $this->reverseIndex; + } + + // Cross-request cache: building the index loads EVERY schema, which + // would otherwise run on the first object write of every request. + // Invalidated eagerly on Schema created/updated/deleted events. + $cached = $this->indexCache?->get(self::REVERSE_INDEX_CACHE_KEY); + if (is_array($cached) === true) { + $this->reverseIndex = $cached; + return $this->reverseIndex; + } + + $index = []; + try { + $schemas = $this->schemaMapper->findAll(_rbac: false, _multitenancy: false); + } catch (Throwable $e) { + $this->logger->warning('Source-record change: could not load schemas for reverse index: '.$e->getMessage()); + $this->reverseIndex = []; + return $this->reverseIndex; + } + + foreach ($schemas as $schema) { + foreach ($this->reverseLinksFor(schema: $schema) as $link) { + $index[$link['sourceSchema']][] = $link['referenceField']; + } + } + + // De-duplicate reference fields per source schema. + foreach ($index as $key => $fields) { + $index[$key] = array_values(array_unique($fields)); + } + + $this->reverseIndex = $index; + $this->indexCache?->set(self::REVERSE_INDEX_CACHE_KEY, $index, self::REVERSE_INDEX_CACHE_TTL); + + return $this->reverseIndex; + }//end reverseIndex() + + /** + * Extract the reverse-FK `sourceLink` declarations from a single schema's + * survivorship + merge annotations. + * + * @param Schema $schema Schema to inspect. + * + * @return array + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + */ + private function reverseLinksFor(Schema $schema): array + { + $config = ($schema->getConfiguration() ?? []); + + $links = []; + foreach (['x-openregister-survivorship', 'x-openregister-merge'] as $annotationKey) { + $sourceLink = ($config[$annotationKey]['sourceLink'] ?? null); + if (is_array($sourceLink) === false + || (string) ($sourceLink['mode'] ?? 'embedded') !== 'reverseFk' + ) { + continue; + } + + $sourceSchema = (string) ($sourceLink['sourceSchema'] ?? ''); + $referenceField = (string) ($sourceLink['referenceField'] ?? ''); + if ($sourceSchema === '' || $referenceField === '') { + continue; + } + + $links[] = [ + 'sourceSchema' => $sourceSchema, + 'referenceField' => $referenceField, + ]; + }//end foreach + + return $links; + }//end reverseLinksFor() + + /** + * Recompute a master's golden record by re-persisting it, which re-runs + * the survivorship recompute listener over the master's (reverse-FK) + * source set. Best-effort — a failure is logged and swallowed. + * + * @param string $masterUuid Referenced master uuid. + * + * @return void + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + * + * @SuppressWarnings(PHPMD.StaticAccess) `MergeService::normaliseRoundTripDates` + * is a pure stateless date-format utility shared with the merge relink; + * a static call is the lightest way to reuse it without injecting the + * whole MergeService (which would raise coupling for one helper). + */ + private function recomputeMaster(string $masterUuid): void + { + if ($masterUuid === '') { + return; + } + + try { + $master = $this->objectService->find(id: $masterUuid, _rbac: true, _multitenancy: true); + if ($master === null) { + return; + } + + // Re-persist: the survivorship recompute listener fires on the + // resulting update event and rematerialises the golden record. Date + // fields are normalised back to ISO first — OR stores them in a + // space-separated form its own validation rejects on a round-trip + // save (see MergeService::normaliseRoundTripDates). + $master->setObject(MergeService::normaliseRoundTripDates(data: ($master->getObject() ?? []))); + $this->objectService->saveObject( + object: $master, + register: $master->getRegister(), + schema: $master->getSchema(), + uuid: $master->getUuid() + ); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Source-record change: could not recompute master "%s": %s', $masterUuid, $e->getMessage()) + ); + }//end try + }//end recomputeMaster() + + /** + * Resolve a schema by reference (id/uuid/slug), or null on failure. + * + * @param string $ref Schema reference. + * + * @return Schema|null Resolved schema. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#4.1 + */ + private function resolveSchema(string $ref): ?Schema + { + if ($ref === '') { + return null; + } + + try { + return $this->schemaMapper->find($ref, _multitenancy: false); + } catch (Throwable) { + return null; + } + }//end resolveSchema() +}//end class diff --git a/lib/Listener/SurvivorshipRecomputeListener.php b/lib/Listener/SurvivorshipRecomputeListener.php new file mode 100644 index 0000000000..5c13f0de82 --- /dev/null +++ b/lib/Listener/SurvivorshipRecomputeListener.php @@ -0,0 +1,405 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use DateTimeImmutable; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ObjectCreatingEvent; +use OCA\OpenRegister\Event\ObjectUpdatingEvent; +use OCA\OpenRegister\Service\ObjectService; +use OCA\OpenRegister\Service\Survivorship\SourceRecordResolver; +use OCA\OpenRegister\Service\Survivorship\SurvivorshipResolver; +use OCA\OpenRegister\Service\Survivorship\TrustTierResolver; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Materialises a declared golden record + provenance into the object payload + * on save. + * + * @template-implements IEventListener + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#4.1 + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Reuses the survivorship + * collaborators (SchemaMapper, ObjectService, SurvivorshipResolver, + * TrustTierResolver, SourceRecordResolver) plus the logger, per ADR-011 + * reuse — a facade purely to hide the dependency count would add indirection + * without reducing coupling, mirroring MergeService. + */ +class SurvivorshipRecomputeListener implements IEventListener +{ + /** + * Default field the golden record is written to when the annotation omits `goldenRecordField`. + * + * @var string + */ + private const DEFAULT_GOLDEN_FIELD = 'goldenRecord'; + + /** + * Default field the provenance map is written to when the annotation omits `provenanceField`. + * + * @var string + */ + private const DEFAULT_PROVENANCE_FIELD = 'attributeProvenance'; + + /** + * Default field the per-object attribute-override map is read from when + * the annotation omits `overridesField`. + * + * @var string + */ + private const DEFAULT_OVERRIDES_FIELD = 'attributeOverrides'; + + /** + * Slug of the OR-owned trust-configuration register schema. + * + * @var string + */ + private const TRUST_CONFIGURATION_SCHEMA = 'trustConfiguration'; + + /** + * Wire collaborators used to look up the schema, load linked sources + + * trust rows, and resolve the golden record. + * + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param ObjectService $objectService Object read path (RBAC + tenant scoped). + * @param SurvivorshipResolver $resolver Pure golden-record resolver. + * @param TrustTierResolver $trustResolver Pure trust-tier lookup + decay engine. + * @param SourceRecordResolver $sourceRecordResolver Mode-aware source-record resolver (embedded | reverseFk). + * @param LoggerInterface $logger PSR logger for warnings. + * + * @return void + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#4.1 + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.2 + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly ObjectService $objectService, + private readonly SurvivorshipResolver $resolver, + private readonly TrustTierResolver $trustResolver, + private readonly SourceRecordResolver $sourceRecordResolver, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Run survivorship resolution before the object is persisted. + * + * @param Event $event Inbound dispatcher event. + * + * @return void + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#4.1 + */ + public function handle(Event $event): void + { + if ($event instanceof ObjectCreatingEvent) { + $this->process(object: $event->getObject()); + return; + } + + if ($event instanceof ObjectUpdatingEvent) { + $this->process(object: $event->getNewObject()); + return; + } + }//end handle() + + /** + * Compute and patch the golden record + provenance onto the object data. + * + * Fail-soft: any error during resolution is logged and swallowed — the + * object is still persisted with its data unchanged. The override map is + * read from `overridesField` and threaded into the resolver but is never + * itself written here — {@see materialise()} only ever patches + * `goldenRecordField` / `provenanceField`, so the raw `$data` array + * (including any override map already on the object) flows through to + * `ObjectEntity::setObject()` untouched, preserving overrides across an + * unrelated recompute. + * + * @param ObjectEntity $object Object being created or updated. + * + * @return void + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#4.1 + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.3 + */ + private function process(ObjectEntity $object): void + { + try { + $schema = $this->loadSchema(object: $object); + if ($schema === null) { + return; + } + + $config = $this->getSurvivorshipConfig(schema: $schema); + if ($config === null) { + return; + } + + // Skip schemas with no resolvable source linkage — neither an + // embedded `sourceLinkField` nor a reverse-FK `sourceLink` block. + if ($this->hasSourceLinkage(config: $config) === false) { + return; + } + + $data = ($object->getObject() ?? []); + $sourceRecords = $this->sourceRecordResolver->resolveSources( + masterData: $data, + masterUuid: (string) $object->getUuid(), + config: $config, + masterRegister: (string) $object->getRegister() + ); + + if ($this->shouldPreserveGoldenRecord(sourceRecords: $sourceRecords, config: $config, data: $data) === true) { + return; + } + + $entityType = (string) ($config['entityType'] ?? ($schema->getSlug() ?? '')); + $trustRows = $this->loadTrustRows(entityType: $entityType); + $now = new DateTimeImmutable(); + + $overridesField = (string) ($config['overridesField'] ?? self::DEFAULT_OVERRIDES_FIELD); + if ($overridesField === '') { + $overridesField = self::DEFAULT_OVERRIDES_FIELD; + } + + $overrides = ($data[$overridesField] ?? null); + + $resolution = $this->resolver->resolveGoldenRecord( + entityType: $entityType, + sourceRecords: $sourceRecords, + config: $config, + trustRows: $trustRows, + trustResolver: $this->trustResolver, + asOf: $now, + overrides: $overrides + ); + + $this->materialise(object: $object, data: $data, config: $config, resolution: $resolution); + } catch (Throwable $e) { + $this->logger->warning( + sprintf( + 'Survivorship resolution failed on %s: %s', + (string) $object->getUuid(), + $e->getMessage() + ) + ); + }//end try + }//end process() + + /** + * Write the resolved golden record + provenance into the object payload, + * only when the computed values differ from what is already stored. + * + * @param ObjectEntity $object Object being saved. + * @param array $data Object's current data. + * @param array $config Survivorship annotation. + * @param array{goldenRecord: array, attributeProvenance: array} $resolution Resolver output. + * + * @return void + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#4.1 + */ + private function materialise(ObjectEntity $object, array $data, array $config, array $resolution): void + { + $goldenField = (string) ($config['goldenRecordField'] ?? self::DEFAULT_GOLDEN_FIELD); + if ($goldenField === '') { + $goldenField = self::DEFAULT_GOLDEN_FIELD; + } + + $provenanceField = (string) ($config['provenanceField'] ?? self::DEFAULT_PROVENANCE_FIELD); + if ($provenanceField === '') { + $provenanceField = self::DEFAULT_PROVENANCE_FIELD; + } + + $changed = false; + + if (($data[$goldenField] ?? null) !== $resolution['goldenRecord']) { + $data[$goldenField] = $resolution['goldenRecord']; + $changed = true; + } + + if (($data[$provenanceField] ?? null) !== $resolution['attributeProvenance']) { + $data[$provenanceField] = $resolution['attributeProvenance']; + $changed = true; + } + + if ($changed === true) { + $object->setObject($data); + } + }//end materialise() + + /** + * Look up the schema referenced by an object instance. + * + * @param ObjectEntity $object Object whose schema reference to resolve. + * + * @return Schema|null Resolved schema, or null on lookup failure. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#4.1 + */ + private function loadSchema(ObjectEntity $object): ?Schema + { + $ref = $object->getSchema(); + if ($ref === null || $ref === '') { + return null; + } + + try { + return $this->schemaMapper->find($ref, _multitenancy: false); + } catch (Throwable) { + return null; + } + }//end loadSchema() + + /** + * Read the `x-openregister-survivorship` configuration block. + * + * @param Schema $schema Schema to inspect. + * + * @return array|null Survivorship config, or null when absent. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#4.1 + */ + private function getSurvivorshipConfig(Schema $schema): ?array + { + $config = ($schema->getConfiguration() ?? []); + $value = ($config['x-openregister-survivorship'] ?? null); + if (is_array($value) === true && count($value) > 0) { + return $value; + } + + return null; + }//end getSurvivorshipConfig() + + /** + * Whether the survivorship config declares a resolvable source linkage — + * an embedded `sourceLinkField` or a reverse-FK `sourceLink` block. + * + * @param array $config Survivorship config. + * + * @return bool + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.2 + */ + private function hasSourceLinkage(array $config): bool + { + if ((string) ($config['sourceLinkField'] ?? '') !== '') { + return true; + } + + return $this->sourceRecordResolver->isReverseFk(config: $config); + }//end hasSourceLinkage() + + /** + * Reverse-FK guard: a recompute with NO resolvable sources must not clobber + * an existing golden record. Covers create time (no uuid yet → no sources + * can reference the master) and any context where the source query yields + * nothing — the record is only recomputed from sources that exist, never + * wiped by a transient empty resolution. Embedded mode keeps its prior + * behaviour (an empty embedded array legitimately clears the record). + * + * @param array> $sourceRecords Resolved sources. + * @param array $config Survivorship config. + * @param array $data Object payload. + * + * @return bool True when the existing golden record must be preserved as-is. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.2 + */ + private function shouldPreserveGoldenRecord(array $sourceRecords, array $config, array $data): bool + { + if (empty($sourceRecords) === false) { + return false; + } + + if ($this->sourceRecordResolver->isReverseFk(config: $config) === false) { + return false; + } + + $goldenField = (string) ($config['goldenRecordField'] ?? self::DEFAULT_GOLDEN_FIELD); + return empty(($data[$goldenField] ?? null)) === false; + }//end shouldPreserveGoldenRecord() + + /** + * Load the candidate trust-configuration rows for an entity type via the + * OR-owned `trustConfiguration` register (RBAC + tenant scoped). + * + * @param string $entityType Entity type to scope the lookup. + * + * @return array> Trust-configuration rows. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#4.1 + */ + private function loadTrustRows(string $entityType): array + { + if ($entityType === '') { + return []; + } + + try { + $objects = $this->objectService->findAll( + [ + 'filters' => [ + 'schema' => self::TRUST_CONFIGURATION_SCHEMA, + 'entityType' => $entityType, + ], + ], + _rbac: true, + _multitenancy: true + ); + } catch (Throwable) { + return []; + } + + $rows = []; + foreach ($objects as $object) { + if ($object instanceof ObjectEntity) { + $rows[] = ($object->getObject() ?? []); + } + } + + return $rows; + }//end loadTrustRows() +}//end class diff --git a/lib/Listener/SystemEntityNotificationListener.php b/lib/Listener/SystemEntityNotificationListener.php new file mode 100644 index 0000000000..ae6e3f9766 --- /dev/null +++ b/lib/Listener/SystemEntityNotificationListener.php @@ -0,0 +1,222 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Event\AgentCreatedEvent; +use OCA\OpenRegister\Event\AgentUpdatedEvent; +use OCA\OpenRegister\Event\ConfigurationCreatedEvent; +use OCA\OpenRegister\Event\ConfigurationUpdatedEvent; +use OCA\OpenRegister\Event\RegisterCreatedEvent; +use OCA\OpenRegister\Event\RegisterUpdatedEvent; +use OCA\OpenRegister\Event\SchemaCreatedEvent; +use OCA\OpenRegister\Event\SchemaUpdatedEvent; +use OCA\OpenRegister\Event\SourceCreatedEvent; +use OCA\OpenRegister\Event\SourceUpdatedEvent; +use OCA\OpenRegister\Service\Notification\AnnotationNotificationDispatcher; +use OCA\OpenRegister\Service\Notification\SystemEntityObjectAdapter; +use OCA\OpenRegister\Service\Notification\SystemSchemaRules; +use OCP\AppFramework\Db\Entity; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +/** + * Routes system-entity lifecycle events through the annotation notification pipeline. + * + * @template-implements IEventListener + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Listener maps ten distinct event classes to two + * dispatcher calls; each event class import is required by the switch-by-instanceof dispatch below + * and cannot be reduced without losing event coverage. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ +class SystemEntityNotificationListener implements IEventListener +{ + /** + * Wire the dispatcher and the system-schema rule registry. + * + * @param AnnotationNotificationDispatcher $dispatcher Dispatcher used to fire notifications. + * @param SystemSchemaRules $rules Registry of declared system-schema rules. + * + * @return void + */ + public function __construct( + private readonly AnnotationNotificationDispatcher $dispatcher, + private readonly SystemSchemaRules $rules + ) { + }//end __construct() + + /** + * Bridge the inbound system-entity event to the notification dispatcher. + * + * Resolves the entity + slug from the concrete event type, wraps the entity as a + * virtual ObjectEntity, loads the synthetic schema from the rule registry, and + * delegates to AnnotationNotificationDispatcher::dispatchWithSchema() — the same + * path used for stored register objects. + * + * @param Event $event Inbound event. + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) handle() dispatches ten distinct event types + * in one method to keep the bridge compact; each instanceof branch is a different system entity + * and cannot be merged. + * @SuppressWarnings(PHPMD.NPathComplexity) Ten event types × two trigger types (created/updated) + * produce many paths; all are required for full system-entity coverage. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ + public function handle(Event $event): void + { + [$entity, $slug, $trigger, $oldData] = $this->extractEventData(event: $event); + + if ($entity === null || $slug === null) { + return; + } + + $schema = $this->rules->buildSchema(slug: $slug); + if ($schema === null) { + return; + } + + $adapter = new SystemEntityObjectAdapter(entity: $entity, systemSlug: $slug); + $context = []; + + if ($trigger === 'updated' && $oldData !== null) { + $newData = []; + if ($entity instanceof \JsonSerializable) { + $newData = $entity->jsonSerialize(); + } + + if (is_array($newData) === true && is_array($oldData) === true) { + $context['_newData'] = $newData; + $context['_oldData'] = $oldData; + } + } + + $this->dispatcher->dispatchWithSchema( + object: $adapter, + trigger: $trigger, + context: $context, + schema: $schema + ); + + }//end handle() + + /** + * Extract entity, system slug, trigger and old-data from the concrete event. + * + * Returns [entity, slug, trigger, oldData|null]. Returns [null, null, '', null] when + * the event type is not recognised as a system-entity event. + * + * @param Event $event The inbound event. + * + * @return array{0: Entity|null, 1: string|null, 2: string, 3: array|null} + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) extractEventData() maps ten event classes to + * (entity, slug, trigger, oldData) tuples; each branch is a distinct system-entity type. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ + private function extractEventData(Event $event): array + { + if ($event instanceof RegisterCreatedEvent) { + return [$event->getRegister(), SystemSchemaRules::SLUG_REGISTER, 'created', null]; + } + + if ($event instanceof RegisterUpdatedEvent) { + $old = $event->getOldRegister(); + $oldData = null; + if ($old instanceof \JsonSerializable) { + $oldData = $old->jsonSerialize(); + } + + return [$event->getNewRegister(), SystemSchemaRules::SLUG_REGISTER, 'updated', $oldData]; + } + + if ($event instanceof SchemaCreatedEvent) { + return [$event->getSchema(), SystemSchemaRules::SLUG_SCHEMA, 'created', null]; + } + + if ($event instanceof SchemaUpdatedEvent) { + $old = $event->getOldSchema(); + $oldData = null; + if ($old instanceof \JsonSerializable) { + $oldData = $old->jsonSerialize(); + } + + return [$event->getNewSchema(), SystemSchemaRules::SLUG_SCHEMA, 'updated', $oldData]; + } + + if ($event instanceof ConfigurationCreatedEvent) { + return [$event->getConfiguration(), SystemSchemaRules::SLUG_CONFIGURATION, 'created', null]; + } + + if ($event instanceof ConfigurationUpdatedEvent) { + $old = $event->getOldConfiguration(); + $oldData = null; + if ($old instanceof \JsonSerializable) { + $oldData = $old->jsonSerialize(); + } + + return [$event->getNewConfiguration(), SystemSchemaRules::SLUG_CONFIGURATION, 'updated', $oldData]; + } + + if ($event instanceof SourceCreatedEvent) { + return [$event->getSource(), SystemSchemaRules::SLUG_SOURCE, 'created', null]; + } + + if ($event instanceof SourceUpdatedEvent) { + $old = $event->getOldSource(); + $oldData = null; + if ($old instanceof \JsonSerializable) { + $oldData = $old->jsonSerialize(); + } + + return [$event->getNewSource(), SystemSchemaRules::SLUG_SOURCE, 'updated', $oldData]; + } + + if ($event instanceof AgentCreatedEvent) { + return [$event->getAgent(), SystemSchemaRules::SLUG_AGENT, 'created', null]; + } + + if ($event instanceof AgentUpdatedEvent) { + $old = $event->getOldAgent(); + $oldData = null; + if ($old instanceof \JsonSerializable) { + $oldData = $old->jsonSerialize(); + } + + return [$event->getNewAgent(), SystemSchemaRules::SLUG_AGENT, 'updated', $oldData]; + } + + return [null, null, '', null]; + + }//end extractEventData() +}//end class diff --git a/lib/Listener/TablesTableDeletedListener.php b/lib/Listener/TablesTableDeletedListener.php new file mode 100644 index 0000000000..728c3d5660 --- /dev/null +++ b/lib/Listener/TablesTableDeletedListener.php @@ -0,0 +1,128 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Listener; + +use OCA\OpenRegister\Service\ObjectSource\TablesSchemaSyncService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Retires the bound virtual schema when a Tables table is deleted. + * + * @template-implements IEventListener + */ +class TablesTableDeletedListener implements IEventListener +{ + /** + * Constructor. + * + * @param TablesSchemaSyncService $syncService Schema retirement logic. + * @param LoggerInterface $logger Logger for diagnostics. + * + * @return void + */ + public function __construct( + private readonly TablesSchemaSyncService $syncService, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Handle a Tables `TableDeletedEvent` by retiring the bound schema. + * + * @param Event $event The Tables table-deleted event. + * + * @return void + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function handle(Event $event): void + { + $tableId = $this->tableIdFromEvent(event: $event); + if ($tableId === null) { + return; + } + + try { + if ($this->syncService->retireByTableId(tableId: $tableId) === true) { + $this->logger->info('[ObjectSource:tables] retired virtual schema for deleted table '.$tableId); + } + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:tables] could not retire schema for table '.$tableId.': '.$e->getMessage()); + } + }//end handle() + + /** + * Extract the deleted table's id from the event, tolerant of its shape. + * + * Tries `getTable()->getId()` then a direct `getTableId()`, each guarded so an + * unexpected event shape yields null rather than a fatal. + * + * @param object $event The event. + * + * @return int|null The table id, or null when unresolvable. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function tableIdFromEvent(object $event): ?int + { + try { + $table = $event->getTable(); + if (is_object($table) === true) { + $id = $table->getId(); + if (is_scalar($id) === true) { + return (int) $id; + } + } + } catch (Throwable $e) { + // Fall through to the flat getter. + } + + try { + $id = $event->getTableId(); + if (is_scalar($id) === true) { + return (int) $id; + } + } catch (Throwable $e) { + return null; + } + + return null; + }//end tableIdFromEvent() +}//end class diff --git a/lib/Listener/ToolRegistrationListener.php b/lib/Listener/ToolRegistrationListener.php index 1fca6e3cfc..a054c85bd7 100644 --- a/lib/Listener/ToolRegistrationListener.php +++ b/lib/Listener/ToolRegistrationListener.php @@ -93,7 +93,7 @@ class ToolRegistrationListener implements IEventListener * @param McpToolsService $mcpToolsService MCP tools service used to register MCP-sourced tools. * @param LoggerInterface $logger PSR logger. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( RegisterTool $registerTool, @@ -118,7 +118,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-20 + * @spec openspec/specs/object-lifecycle/spec.md */ public function handle(Event $event): void { @@ -200,7 +200,7 @@ private function registerBuiltinTools(ToolRegistrationEvent $event): void * * ToolRegistry enforces the id format `app_name.tool_name` so we register ONE * bridge instance per (provider, function) pair under its full MCP id (e.g. - * `openbuilt.createApp`). The bridge is configured via setOnlyMcpId() so each + * `openbuild.createApp`). The bridge is configured via setOnlyMcpId() so each * entry's getFunctions() returns just that one descriptor — preventing the LLM * from seeing the same provider's tool list duplicated across N registry entries. * @@ -244,10 +244,12 @@ private function bridgeProviderDescriptors(ToolRegistrationEvent $event, object { foreach ($provider->getTools() as $descriptor) { $mcpId = (string) ($descriptor['id'] ?? ''); - if ($mcpId === '' || preg_match('/^[a-z0-9_]+\.[a-zA-Z0-9_]+$/', $mcpId) === 0) { - // Tool id is non-conforming (camelCase or missing dot). - // Skip — the LLM-visible id MUST match the ToolRegistry regex, - // and the agent's tools array stores MCP ids verbatim. + if ($mcpId === '' || preg_match('/^[a-z0-9_]+(\.[a-zA-Z0-9_]+)+$/', $mcpId) === 0) { + // Tool id is non-conforming (camelCase or missing dot). Skip + // — the LLM-visible id MUST match the ToolRegistry regex + // (app_name.tool_name, or app_name.schema.verb for ADR-063 + // chain-2 schema-derived tools), and the agent's tools array + // stores MCP ids verbatim. continue; } diff --git a/lib/Listener/TranslationProjectionListener.php b/lib/Listener/TranslationProjectionListener.php index 5deebefc12..b80e13671b 100644 --- a/lib/Listener/TranslationProjectionListener.php +++ b/lib/Listener/TranslationProjectionListener.php @@ -8,6 +8,15 @@ * JSONB property data on the object. Same pattern as the realtime * event listener — derived-projection-by-event. * + * Actor-forwarded deferral (openregister#408): for schemas with + * translatable properties the heavy reconciliation runs in + * TranslationProjectionJob under the captured actor (translator + * attribution needs the session user). What stays inline: the cheap + * stale-row prune for schemas WITHOUT translatable properties (parity + * with the pre-deferral behaviour), delete-time purge (bounded DELETE; + * the entity is not re-fetchable post-delete), and the whole projection + * when the `listenerDeferral` kill switch is set to `inline`. + * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * @@ -27,11 +36,15 @@ namespace OCA\OpenRegister\Listener; +use OCA\OpenRegister\BackgroundJob\TranslationProjectionJob; use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Event\ObjectCreatedEvent; use OCA\OpenRegister\Event\ObjectDeletedEvent; use OCA\OpenRegister\Event\ObjectTransitionedEvent; use OCA\OpenRegister\Event\ObjectUpdatedEvent; +use OCA\OpenRegister\Service\Deferral\ListenerDeferralService; +use OCA\OpenRegister\Service\Object\TranslationHandler; use OCA\OpenRegister\Service\TranslationProjectionService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -40,20 +53,36 @@ * Listener that projects object changes into the translations sidecar. * * @template-implements IEventListener + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-2.1 */ class TranslationProjectionListener implements IEventListener { + + /** + * Entries per enqueued projection job. + * + * @var integer + */ + private const CHUNK_SIZE = 100; + /** - * Wire the translation-projection service. + * Wire the translation-projection service and the deferral contract. * - * @param TranslationProjectionService $projection Projection service. + * @param TranslationProjectionService $projection Projection service. + * @param SchemaMapper $schemaMapper Schema lookup (request-cached) for the enqueue gate. + * @param TranslationHandler $translationHandler Translatable-property gate. + * @param ListenerDeferralService $deferral Actor-forwarding deferral service. * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-13 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( - private readonly TranslationProjectionService $projection + private readonly TranslationProjectionService $projection, + private readonly SchemaMapper $schemaMapper, + private readonly TranslationHandler $translationHandler, + private readonly ListenerDeferralService $deferral ) { }//end __construct() @@ -64,42 +93,100 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-listener-all/tasks.md#task-14 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { - if ($event instanceof ObjectCreatedEvent) { - $object = $event->getObject(); - if ($object instanceof ObjectEntity) { - $this->projection->project($object); - } + if ($event instanceof ObjectDeletedEvent) { + // Delete-time purge stays inline: bounded sidecar DELETE and + // the object row is not re-fetchable once the delete lands. + $this->projection->purge($event->getObject()); + return; + } + $object = $this->extractObject(event: $event); + if ($object === null) { return; } - if ($event instanceof ObjectUpdatedEvent) { - $object = $event->getNewObject(); - if ($object instanceof ObjectEntity) { - $this->projection->project($object); - } + $this->projectOrDefer(object: $object); + }//end handle() + /** + * Route a live object either to the deferred job or the inline path. + * + * @param ObjectEntity $object Object that changed. + * + * @return void + */ + private function projectOrDefer(ObjectEntity $object): void + { + if ($this->deferral->isDeferralEnabled() === false || $this->hasTranslatableProperties(object: $object) === false) { + // Kill switch → full pre-deferral behaviour. No translatable + // properties → project() only performs the cheap stale-row + // prune (parity with pre-deferral behaviour, and enqueueing a + // job for every schema-less save would defeat the gate). + $this->projection->project($object); return; } - if ($event instanceof ObjectDeletedEvent) { - $object = $event->getObject(); - if ($object instanceof ObjectEntity) { - $this->projection->purge($object); - } + $this->deferral->defer( + jobClass: TranslationProjectionJob::class, + entry: [ + 'uuid' => (string) $object->getUuid(), + 'register' => (string) $object->getRegister(), + 'schema' => (string) $object->getSchema(), + 'version' => $object->getVersion(), + ], + chunkSize: self::CHUNK_SIZE, + dedupeKey: (string) $object->getUuid() + ); + }//end projectOrDefer() - return; + /** + * Whether the object's schema declares at least one translatable property. + * + * Schema resolution failures return false so the inline path (which + * re-resolves and bails safely) handles the event. + * + * @param ObjectEntity $object Object whose schema to inspect. + * + * @return bool True when the projection has real work to defer. + */ + private function hasTranslatableProperties(ObjectEntity $object): bool + { + $schemaRef = (string) $object->getSchema(); + if ($schemaRef === '') { + return false; } - if ($event instanceof ObjectTransitionedEvent) { - $object = $event->getObject(); - if ($object instanceof ObjectEntity) { - $this->projection->project($object); - } + try { + $schema = $this->schemaMapper->find($schemaRef); + } catch (\Throwable $e) { + return false; } - }//end handle() + + return count($this->translationHandler->getTranslatableProperties($schema)) > 0; + }//end hasTranslatableProperties() + + /** + * Different Object*Event classes expose the entity under different + * accessors. Normalise to one. + * + * @param Event $event Inbound dispatcher event. + * + * @return ObjectEntity|null Object instance, or null when none could be derived. + */ + private function extractObject(Event $event): ?ObjectEntity + { + if ($event instanceof ObjectCreatedEvent || $event instanceof ObjectTransitionedEvent) { + return $event->getObject(); + } + + if ($event instanceof ObjectUpdatedEvent) { + return $event->getNewObject(); + } + + return null; + }//end extractObject() }//end class diff --git a/lib/Listener/WebhookEventListener.php b/lib/Listener/WebhookEventListener.php index 752cb3923c..115ece45f2 100644 --- a/lib/Listener/WebhookEventListener.php +++ b/lib/Listener/WebhookEventListener.php @@ -19,8 +19,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-77 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-42 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-event-payload-input-must-include-full-context-for-mapping-templates + * @spec openspec/specs/event-driven-architecture/spec.md */ declare(strict_types=1); @@ -98,7 +98,7 @@ class WebhookEventListener implements IEventListener * @param WebhookService $webhookService Webhook service * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( WebhookService $webhookService, @@ -115,8 +115,8 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-42 + * @spec openspec/specs/event-driven-architecture/spec.md + * @spec openspec/specs/event-driven-architecture/spec.md */ public function handle(Event $event): void { @@ -161,7 +161,7 @@ public function handle(Event $event): void * @SuppressWarnings(PHPMD.NPathComplexity) Event handling requires checking many event types * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Unified event extraction requires handling all types * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-77 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-event-payload-input-must-include-full-context-for-mapping-templates */ private function extractPayload(Event $event): array|null { diff --git a/lib/Mcp/Attribute/McpTool.php b/lib/Mcp/Attribute/McpTool.php new file mode 100644 index 0000000000..50c13abb4e --- /dev/null +++ b/lib/Mcp/Attribute/McpTool.php @@ -0,0 +1,109 @@ + + * @copyright 2026 Conduction BV + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-001 — The #[McpTool] service-method attribute) + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-005 — Attribute-declared hints/scope reach both MCP surfaces) + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Mcp\Attribute; + +use Attribute; + +/** + * McpTool + * + * Method-targeting attribute marking a public service method for MCP + * discovery. See class docblock for the full contract. + * + * @category Mcp + * @package OCA\OpenRegister\Mcp\Attribute + */ +#[Attribute(Attribute::TARGET_METHOD)] +final class McpTool +{ + /** + * Constructor. + * + * @param string|null $name Local tool name; defaults to the method name when null. + * @param string|null $description LLM-facing description; defaults to the docblock summary when null. + * @param bool|null $readOnlyHint Optional MCP 2025-11-25 annotation hint; one of + * {@see \OCA\OpenRegister\Service\Mcp\McpAnnotationValidator::HINT_KEYS}. + * Omitted (null) when the author does not declare it — never inferred. + * @param bool|null $destructiveHint Optional MCP 2025-11-25 annotation hint (see `$readOnlyHint`). + * @param bool|null $idempotentHint Optional MCP 2025-11-25 annotation hint (see `$readOnlyHint`). + * @param string|null $scope Optional advisory scope; when set, MUST be one of + * {@see \OCA\OpenRegister\Service\Mcp\McpAnnotationValidator::SCOPES} + * (validated by {@see \OCA\OpenRegister\Mcp\AttributeToolScanner} at + * scan time, not here). Omitted (null) when not declared. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-001 — The #[McpTool] service-method attribute) + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-005 — Attribute-declared hints/scope reach both MCP surfaces) + */ + public function __construct( + public readonly ?string $name=null, + public readonly ?string $description=null, + public readonly ?bool $readOnlyHint=null, + public readonly ?bool $destructiveHint=null, + public readonly ?bool $idempotentHint=null, + public readonly ?string $scope=null, + ) { + }//end __construct() +}//end class diff --git a/lib/Mcp/AttributeToolScanner.php b/lib/Mcp/AttributeToolScanner.php new file mode 100644 index 0000000000..3f5b65c099 --- /dev/null +++ b/lib/Mcp/AttributeToolScanner.php @@ -0,0 +1,584 @@ + + * @copyright 2026 Conduction BV + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-001 — The #[McpTool] service-method attribute) + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-002 — Reflection scanner registers attributed tools in the same catalog) + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Mcp; + +use OCA\OpenRegister\Mcp\Attribute\McpTool; +use OCA\OpenRegister\Service\Mcp\McpAnnotationValidator; +use Psr\Log\LoggerInterface; +use ReflectionClass; +use ReflectionMethod; +use ReflectionNamedType; +use ReflectionParameter; +use ReflectionType; +use ReflectionUnionType; +use Throwable; + +/** + * AttributeToolScanner + * + * Reflects `#[McpTool]`-attributed public methods on an app's declared + * scannable service classes and builds catalog-shaped tool descriptors. + * + * @category Mcp + * @package OCA\OpenRegister\Mcp + */ +final class AttributeToolScanner +{ + /** + * Scan every declared scannable class for one app and return every + * attributed-method descriptor found across all of them. + * + * @param string $appId The owning app id (id prefix of every emitted tool id). + * @param list $classNames Candidate scannable service FQCNs (see {@see IMcpScannableServices}). + * @param LoggerInterface $logger PSR logger (malformed classes/methods are logged, not thrown). + * + * @return list}> + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-002 — Reflection scanner registers attributed tools in the same catalog) + */ + public function scanClasses(string $appId, array $classNames, LoggerInterface $logger): array + { + $descriptors = []; + + foreach ($classNames as $className) { + if (is_string($className) === false || $className === '') { + continue; + } + + foreach ($this->scanClass(appId: $appId, className: $className, logger: $logger) as $descriptor) { + $descriptors[] = $descriptor; + } + } + + return $descriptors; + }//end scanClasses() + + /** + * Scan a single class for `#[McpTool]`-attributed public methods. + * + * @param string $appId The owning app id. + * @param string $className The FQCN to reflect. + * @param LoggerInterface $logger PSR logger. + * + * @return list}> + */ + public function scanClass(string $appId, string $className, LoggerInterface $logger): array + { + if (class_exists($className) === false) { + $logger->warning( + '[AttributeToolScanner] Scannable class does not exist', + ['appId' => $appId, 'class' => $className] + ); + return []; + } + + try { + $reflectionClass = new ReflectionClass($className); + } catch (Throwable $e) { + $logger->warning( + '[AttributeToolScanner] Could not reflect scannable class', + ['appId' => $appId, 'class' => $className, 'error' => $e->getMessage()] + ); + return []; + } + + $descriptors = []; + + foreach ($reflectionClass->getMethods() as $method) { + $attributes = $method->getAttributes(McpTool::class); + if ($attributes === []) { + continue; + } + + if ($method->isPublic() === false) { + $logger->warning( + '[AttributeToolScanner] #[McpTool] on a non-public method is ignored', + ['appId' => $appId, 'class' => $className, 'method' => $method->getName()] + ); + continue; + } + + if ($method->isStatic() === true || $method->isAbstract() === true) { + $logger->warning( + '[AttributeToolScanner] #[McpTool] on a static or abstract method is ignored', + ['appId' => $appId, 'class' => $className, 'method' => $method->getName()] + ); + continue; + } + + try { + $attributeInstance = $attributes[0]->newInstance(); + } catch (Throwable $e) { + $logger->warning( + '[AttributeToolScanner] Could not instantiate #[McpTool] attribute', + [ + 'appId' => $appId, + 'class' => $className, + 'method' => $method->getName(), + 'error' => $e->getMessage(), + ] + ); + continue; + } + + if ($this->hasUnknownScope(attribute: $attributeInstance) === true) { + $logger->warning( + '[AttributeToolScanner] #[McpTool] declares an unrecognised `scope`; tool skipped', + [ + 'appId' => $appId, + 'class' => $className, + 'method' => $method->getName(), + 'scope' => $attributeInstance->scope, + 'allowedScopes' => McpAnnotationValidator::SCOPES, + ] + ); + continue; + } + + $descriptors[] = $this->buildDescriptor( + appId: $appId, + className: $className, + method: $method, + attribute: $attributeInstance + ); + }//end foreach + + return $descriptors; + }//end scanClass() + + /** + * Build one descriptor for an attributed method. + * + * @param string $appId The owning app id. + * @param string $className The declaring class FQCN. + * @param ReflectionMethod $method The attributed method. + * @param McpTool $attribute The resolved attribute instance. + * + * @return array{id: string, name: string, description: string, inputSchema: array, + * outputSchema?: array, class: class-string, method: string, paramNames: list, + * readOnlyHint?: bool, destructiveHint?: bool, idempotentHint?: bool, scope?: string} + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-001 — Attribute with defaults infers name and description) + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-005 — Attribute-declared hints/scope reach both MCP surfaces) + */ + private function buildDescriptor(string $appId, string $className, ReflectionMethod $method, McpTool $attribute): array + { + $docLines = $this->docblockLines(docComment: $method->getDocComment()); + $docParams = $this->parseParams(docLines: $docLines); + $docReturn = $this->parseReturn(docLines: $docLines); + + $name = $attribute->name ?? $method->getName(); + $description = $attribute->description ?? $this->parseSummary(docLines: $docLines); + if ($description === null || $description === '') { + $description = sprintf('%s::%s (no description declared).', $className, $method->getName()); + } + + $descriptor = [ + 'id' => $appId.'.'.$name, + 'name' => $name, + 'description' => $description, + 'inputSchema' => $this->inferInputSchema(method: $method, docParams: $docParams), + 'class' => $className, + 'method' => $method->getName(), + 'paramNames' => array_map( + callback: static fn (ReflectionParameter $param): string => $param->getName(), + array: $method->getParameters() + ), + ]; + + $outputSchema = $this->inferOutputSchema(method: $method, docReturn: $docReturn); + if ($outputSchema !== null) { + $descriptor['outputSchema'] = $outputSchema; + } + + // MCP 2025-11-25 annotation hints + advisory `scope` (REQ-ATTR-005). + // Forwarded additively — a key is present ONLY when the author set + // it on the attribute; nothing here ever infers or fabricates a + // value (an invented `readOnlyHint: true` on an unannotated write + // tool would be a dangerous lie). Both are advisory UX metadata + // only — OpenRegister RBAC and the owning method's own + // authorization remain the sole authoritative invoke-time gate. + foreach (McpAnnotationValidator::HINT_KEYS as $hintKey) { + $hintValue = $this->hintValue(attribute: $attribute, hintKey: $hintKey); + if ($hintValue !== null) { + $descriptor[$hintKey] = $hintValue; + } + } + + if ($attribute->scope !== null) { + $descriptor['scope'] = $attribute->scope; + } + + return $descriptor; + }//end buildDescriptor() + + /** + * Read one boolean hint property off the attribute by its + * {@see McpAnnotationValidator::HINT_KEYS} name. + * + * @param McpTool $attribute The resolved attribute instance. + * @param string $hintKey One of `readOnlyHint`/`destructiveHint`/`idempotentHint`. + * + * @return bool|null The declared hint value, or null when the author did not set it. + */ + private function hintValue(McpTool $attribute, string $hintKey): ?bool + { + return match ($hintKey) { + 'readOnlyHint' => $attribute->readOnlyHint, + 'destructiveHint' => $attribute->destructiveHint, + 'idempotentHint' => $attribute->idempotentHint, + default => null, + }; + }//end hintValue() + + /** + * True when the attribute declares a non-null `scope` that is not one + * of {@see McpAnnotationValidator::SCOPES} (REQ-ATTR-005 — an unknown + * scope is rejected at scan time, mirroring how this scanner already + * fail-softs on other malformed attribute input). + * + * @param McpTool $attribute The resolved attribute instance. + * + * @return bool True when `scope` is set to an unrecognised value. + */ + private function hasUnknownScope(McpTool $attribute): bool + { + if ($attribute->scope === null) { + return false; + } + + return in_array($attribute->scope, McpAnnotationValidator::SCOPES, true) === false; + }//end hasUnknownScope() + + /** + * Infer the `inputSchema` from the method's parameter type hints and + * docblock `@param` tags. Parameters without a default value are + * `required` (REQ-ATTR-001). + * + * @param ReflectionMethod $method The attributed method. + * @param array $docParams Parameter name → docblock description. + * + * @return array JSON-Schema-shaped input schema. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-001 — inputSchema is inferred from type hints and @param) + */ + private function inferInputSchema(ReflectionMethod $method, array $docParams): array + { + $properties = []; + $required = []; + + foreach ($method->getParameters() as $param) { + $property = $this->paramSchema(param: $param); + + $paramDescription = ($docParams[$param->getName()] ?? ''); + if ($paramDescription !== '') { + $property['description'] = $paramDescription; + } + + $properties[$param->getName()] = $property; + + if ($param->isOptional() === false) { + $required[] = $param->getName(); + } + } + + $schema = [ + 'type' => 'object', + 'properties' => $properties, + ]; + + if ($required !== []) { + $schema['required'] = $required; + } + + return $schema; + }//end inferInputSchema() + + /** + * JSON-Schema-shaped property for a single parameter, from its PHP type hint. + * + * @param ReflectionParameter $param The parameter to describe. + * + * @return array The property schema (possibly empty when the type cannot be inferred). + */ + private function paramSchema(ReflectionParameter $param): array + { + $jsonType = $this->mapReflectionType(type: $param->getType()); + if ($jsonType === null) { + return []; + } + + return ['type' => $jsonType]; + }//end paramSchema() + + /** + * Best-effort `outputSchema` from the return type + `@return` tag. + * Omitted (null) for untyped/void/mixed/bare-array returns per + * design.md's "best-effort" contract. + * + * @param ReflectionMethod $method The attributed method. + * @param string|null $docReturn The raw `@return` type token, when declared. + * + * @return array|null The output schema, or null when not inferable. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-001 — outputSchema inferred from return type / @return where available) + */ + private function inferOutputSchema(ReflectionMethod $method, ?string $docReturn): ?array + { + unset($docReturn); + + $type = $method->getReturnType(); + if (($type instanceof ReflectionNamedType) === false) { + return null; + } + + $jsonType = $this->mapNamedType(type: $type); + if ($jsonType === null || $jsonType === 'array') { + // Untyped array / mixed / void — too little information to be + // useful; deliberately omitted (design.md "best-effort"). + return null; + } + + if ($type->allowsNull() === true) { + return ['type' => [$jsonType, 'null']]; + } + + return ['type' => $jsonType]; + }//end inferOutputSchema() + + /** + * Map a (possibly nullable/union) reflection type to a JSON-Schema type. + * + * @param ReflectionType|null $type The parameter/return reflection type, or null when untyped. + * + * @return string|list|null A JSON-Schema type, a `[type, 'null']` pair, or null when unconstrained. + */ + private function mapReflectionType(?ReflectionType $type): string|array|null + { + if ($type === null) { + return null; + } + + if ($type instanceof ReflectionUnionType) { + $types = []; + $nullable = false; + + foreach ($type->getTypes() as $member) { + if (($member instanceof ReflectionNamedType) === false) { + continue; + } + + if ($member->getName() === 'null') { + $nullable = true; + continue; + } + + $mapped = $this->mapNamedType(type: $member); + if ($mapped !== null) { + $types[] = $mapped; + } + } + + $types = array_values(array_unique($types)); + if ($nullable === true) { + $types[] = 'null'; + } + + if (count($types) === 0) { + return null; + } + + if (count($types) === 1) { + return $types[0]; + } + + return $types; + }//end if + + if ($type instanceof ReflectionNamedType) { + $jsonType = $this->mapNamedType(type: $type); + if ($jsonType === null) { + return null; + } + + if ($type->allowsNull() === true) { + return [$jsonType, 'null']; + } + + return $jsonType; + } + + // ReflectionIntersectionType or other exotic forms — unconstrained. + return null; + }//end mapReflectionType() + + /** + * Map one PHP named type to its JSON-Schema type, or null when the type + * carries no useful schema information (`mixed`, `void`, `never`). + * + * @param ReflectionNamedType $type The named type to map. + * + * @return string|null The JSON-Schema type, or null when unconstrained. + */ + private function mapNamedType(ReflectionNamedType $type): ?string + { + if ($type->isBuiltin() === false) { + // A class/interface/enum type hint — best-effort "object". + return 'object'; + } + + return match ($type->getName()) { + 'int' => 'integer', + 'float' => 'number', + 'string' => 'string', + 'bool' => 'boolean', + 'array', 'iterable' => 'array', + default => null, + }; + }//end mapNamedType() + + /** + * Split a docblock into trimmed, marker-stripped lines. + * + * @param string|false $docComment The raw `ReflectionMethod::getDocComment()` value. + * + * @return list Trimmed lines, in order, trailing blank lines removed. + */ + private function docblockLines(string|false $docComment): array + { + if ($docComment === false || trim($docComment) === '') { + return []; + } + + $rawLines = preg_split('/\r\n|\r|\n/', $docComment); + if ($rawLines === false) { + return []; + } + + $lines = []; + foreach ($rawLines as $rawLine) { + $line = trim($rawLine); + $line = preg_replace('#^/\*\*#', '', $line) ?? $line; + $line = preg_replace('#\*/$#', '', $line) ?? $line; + $line = ltrim($line, '* '); + $lines[] = rtrim($line); + } + + while ($lines !== [] && end($lines) === '') { + array_pop($lines); + } + + return $lines; + }//end docblockLines() + + /** + * The docblock summary — the first non-empty, non-tag line. + * + * @param list $docLines Lines from {@see docblockLines()}. + * + * @return string|null The summary line, or null when absent. + */ + private function parseSummary(array $docLines): ?string + { + foreach ($docLines as $line) { + if ($line === '') { + continue; + } + + if (str_starts_with($line, '@') === true) { + return null; + } + + return $line; + } + + return null; + }//end parseSummary() + + /** + * Every `@param $name description` tag, keyed by parameter name. + * + * @param list $docLines Lines from {@see docblockLines()}. + * + * @return array Parameter name → description (may be empty string). + */ + private function parseParams(array $docLines): array + { + $params = []; + + foreach ($docLines as $line) { + if (preg_match('/^@param\s+\S+\s+\$(\w+)(?:\s+(.*))?$/', $line, $matches) === 1) { + $params[$matches[1]] = trim($matches[2] ?? ''); + } + } + + return $params; + }//end parseParams() + + /** + * The raw `@return` type token, when declared. + * + * @param list $docLines Lines from {@see docblockLines()}. + * + * @return string|null The `@return` type token, or null when absent. + */ + private function parseReturn(array $docLines): ?string + { + foreach ($docLines as $line) { + if (preg_match('/^@return\s+(\S+)/', $line, $matches) === 1) { + return $matches[1]; + } + } + + return null; + }//end parseReturn() +}//end class diff --git a/lib/Mcp/BuiltIn/AttributeToolProvider.php b/lib/Mcp/BuiltIn/AttributeToolProvider.php new file mode 100644 index 0000000000..51310371cf --- /dev/null +++ b/lib/Mcp/BuiltIn/AttributeToolProvider.php @@ -0,0 +1,353 @@ +{$method}(...$namedArguments)` directly, in this same PHP + * process, in the caller's ambient Nextcloud session. There is no HTTP + * call, no message bus, and no OpenRegister-side re-implementation of the + * method — OpenRegister is the registry/catalog and the blessed inbound + * door, the app's own method runs and owns its own authorization/IDOR + * (REQ-ATTR-003). This class deliberately does nothing to establish, + * switch, or elevate a session/user — the resolved instance already runs + * in the ambient request's DI scope. + * + * Every invocation — read, write, or failed — writes exactly one + * immutable, hash-chained audit record via + * `AuditTrailMapper::createToolInvocationEntry()`, identical to the derived + * provider's audit contract (REQ-ATTR-004). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Mcp + * @package OCA\OpenRegister\Mcp\BuiltIn + * + * @author Conduction Development Team + * @copyright 2026 Conduction BV + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/ai-mcp/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Mcp\BuiltIn; + +use InvalidArgumentException; +use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Mcp\IMcpToolProvider; +use OCA\OpenRegister\Service\Mcp\McpAnnotationValidator; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * AttributeToolProvider + * + * Built-in IMcpToolProvider that serves `#[McpTool]`-attributed service + * methods for one owning app. See class docblock for the full contract. + * + * @category Mcp + * @package OCA\OpenRegister\Mcp\BuiltIn + * + * @psalm-suppress UnusedClass - Instantiated by Application::registerMcpToolProviders() + */ +class AttributeToolProvider implements IMcpToolProvider +{ + /** + * Constructor. + * + * `$entries` is a `list, readOnlyHint?: bool, destructiveHint?: bool, + * idempotentHint?: bool, scope?: string}>` — one entry per attributed + * method already resolved to a live service instance (ADR-041 in-process + * invocation). The optional hint/scope keys are set only when + * {@see \OCA\OpenRegister\Mcp\AttributeToolScanner} forwarded them from + * the `#[McpTool]` declaration (REQ-ATTR-005). + * + * @param string $appId The owning app id; `getAppId()` and every id prefix. + * @param array $entries Resolved attributed-tool entries (see above). + * @param AuditTrailMapper $auditTrailMapper Immutable hash-chained audit-trail writer. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly string $appId, + private readonly array $entries, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Returns the owning app id. + * + * @return string The owning app id this instance was constructed for. + */ + public function getAppId(): string + { + return $this->appId; + }//end getAppId() + + /** + * Returns one tool descriptor per attributed method. + * + * Additively forwards the MCP 2025-11-25 annotation hints + * (`readOnlyHint`/`destructiveHint`/`idempotentHint`) and `scope` when + * {@see \OCA\OpenRegister\Mcp\AttributeToolScanner} set them on the + * entry — omitted entirely otherwise, never defaulted (REQ-ATTR-005). + * These are ADVISORY UX metadata only; OpenRegister RBAC and the owning + * method's own authorization remain the sole authoritative invoke-time + * gate (ADR-063). + * + * @return list + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-002 — Attributed method becomes a catalog tool on both surfaces) + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-005 — Attribute-declared hints/scope reach both MCP surfaces) + */ + public function getTools(): array + { + $tools = []; + + foreach ($this->entries as $entry) { + $descriptor = [ + 'id' => $entry['id'], + 'name' => $entry['name'], + 'description' => $entry['description'], + 'inputSchema' => $entry['inputSchema'], + ]; + + if (isset($entry['outputSchema']) === true) { + $descriptor['outputSchema'] = $entry['outputSchema']; + } + + foreach (McpAnnotationValidator::HINT_KEYS as $hintKey) { + if (array_key_exists($hintKey, $entry) === true) { + $descriptor[$hintKey] = $entry[$hintKey]; + } + } + + if (array_key_exists('scope', $entry) === true) { + $descriptor['scope'] = $entry['scope']; + } + + $tools[] = $descriptor; + }//end foreach + + return $tools; + }//end getTools() + + /** + * Invoke an attributed tool by its `{appId}.{toolName}` id — an + * in-process call to the owning app's own service method (ADR-041). + * + * Writes exactly one immutable audit record per invocation — success or + * failure — before returning (or rethrowing). Does NOT catch and + * suppress an authorization failure raised by the owning method: it + * propagates unchanged (audited, then rethrown), exactly mirroring + * {@see SchemaDerivedToolProvider::invokeTool()}'s "no bypass" contract. + * + * @param string $toolId Namespaced tool id, e.g. "pipelinq.createLead". + * @param array $arguments JSON-decoded tool arguments. + * + * @return array JSON-encodable result. + * + * @throws InvalidArgumentException If the tool id is unknown. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-003 — Invocation is a direct in-process method call) + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-004 — Attributed-tool invocations obey the same audit + RBAC rules as derived tools) + */ + public function invokeTool(string $toolId, array $arguments): array + { + $entry = $this->findEntry(toolId: $toolId); + if ($entry === null) { + throw new InvalidArgumentException('Unknown tool: '.$toolId); + } + + $paramsDigest = hash('sha256', $this->canonicalArguments(arguments: $arguments)); + $callArguments = array_intersect_key($arguments, array_flip($entry['paramNames'])); + + try { + // In-process call on the owning app's own, already-DI-resolved + // service instance — no HTTP, no RPC, no re-implementation + // (ADR-041). Named-argument unpacking maps JSON keys straight + // onto the method's declared parameters; the method itself is + // solely responsible for authorization/IDOR (REQ-ATTR-003). + $result = $entry['instance']->{$entry['method']}(...$callArguments); + + $resultArray = $result; + if (is_array($resultArray) === false) { + $resultArray = ['result' => $resultArray]; + } + + $this->writeAudit( + toolId: $toolId, + paramsDigest: $paramsDigest, + resultSummary: $this->resultSummary(result: $resultArray), + result: $resultArray + ); + + return $resultArray; + } catch (Throwable $e) { + $this->writeAudit( + toolId: $toolId, + paramsDigest: $paramsDigest, + resultSummary: [ + 'isError' => true, + 'errorClass' => get_class($e), + 'message' => substr($e->getMessage(), 0, 300), + ], + result: null + ); + + throw $e; + }//end try + }//end invokeTool() + + /** + * Resolve a namespaced tool id back to its entry. + * + * @param string $toolId Namespaced tool id, e.g. "pipelinq.createLead". + * + * @return array{id: string, name: string, description: string, inputSchema: array, + * outputSchema?: array, instance: object, method: string, paramNames: list}|null + */ + private function findEntry(string $toolId): ?array + { + foreach ($this->entries as $entry) { + if ($entry['id'] === $toolId) { + return $entry; + } + } + + return null; + }//end findEntry() + + /** + * Build a small structured outcome summary for a successful invocation + * (never raw method-return payloads — REQ-ATTR-004). + * + * @param array $result The method's (array-coerced) return value. + * + * @return array The result summary. + */ + private function resultSummary(array $result): array + { + $id = null; + if (is_string($result['id'] ?? null) === true) { + $id = $result['id']; + } + + return [ + 'isError' => false, + 'id' => $id, + ]; + }//end resultSummary() + + /** + * Write exactly one immutable, hash-chained audit record for this + * invocation. Fail-soft: a broken audit write is logged and MUST NOT + * mask (or replace) the invocation's real result/error, mirroring + * {@see SchemaDerivedToolProvider::writeAudit()}. + * + * @param string $toolId Full namespaced tool id. + * @param string $paramsDigest SHA-256 hex digest of the invocation arguments. + * @param array $resultSummary Structured outcome summary. + * @param array|null $result The method's return value, or null on failure. + * + * @return void + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-004 — Attributed invocation is audited identically to a derived invocation) + */ + private function writeAudit(string $toolId, string $paramsDigest, array $resultSummary, ?array $result): void + { + try { + $objectUuid = null; + if ($result !== null && is_string($result['id'] ?? null) === true) { + $objectUuid = $result['id']; + } + + $this->auditTrailMapper->createToolInvocationEntry( + toolId: $toolId, + paramsDigest: $paramsDigest, + resultSummary: $resultSummary, + register: null, + schema: null, + objectUuid: $objectUuid + ); + } catch (Throwable $e) { + $this->logger->error( + '[AttributeToolProvider] Failed to write invocation audit record', + ['toolId' => $toolId, 'error' => $e->getMessage()] + ); + }//end try + }//end writeAudit() + + /** + * Deterministically canonicalise arguments (recursive key-sort) so the + * params digest is stable regardless of argument-array key order. + * Duplicated from {@see SchemaDerivedToolProvider} — no shared trait + * exists yet for this small helper (see design.md audit-parity note). + * + * @param array $arguments Tool arguments. + * + * @return string Canonical JSON representation. + */ + private function canonicalArguments(array $arguments): string + { + $encoded = json_encode( + $this->deepKsort(value: $arguments), + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + ); + + if ($encoded === false) { + return ''; + } + + return $encoded; + }//end canonicalArguments() + + /** + * Recursively sort an array by key for deterministic serialisation. + * + * @param array $value The array to sort. + * + * @return array The sorted array. + */ + private function deepKsort(array $value): array + { + ksort($value); + foreach ($value as $key => $item) { + if (is_array($item) === true) { + $value[$key] = $this->deepKsort(value: $item); + } + } + + return $value; + }//end deepKsort() +}//end class diff --git a/lib/Mcp/BuiltIn/ObjectsToolProvider.php b/lib/Mcp/BuiltIn/ObjectsToolProvider.php index bd0343e977..e045927036 100644 --- a/lib/Mcp/BuiltIn/ObjectsToolProvider.php +++ b/lib/Mcp/BuiltIn/ObjectsToolProvider.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#imcptoolprovider-built-in-migration + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); diff --git a/lib/Mcp/BuiltIn/RegistersToolProvider.php b/lib/Mcp/BuiltIn/RegistersToolProvider.php index 70938d9958..013528dcaf 100644 --- a/lib/Mcp/BuiltIn/RegistersToolProvider.php +++ b/lib/Mcp/BuiltIn/RegistersToolProvider.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#imcptoolprovider-built-in-migration + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); diff --git a/lib/Mcp/BuiltIn/SchemaDerivedToolProvider.php b/lib/Mcp/BuiltIn/SchemaDerivedToolProvider.php new file mode 100644 index 0000000000..4dfe73ff36 --- /dev/null +++ b/lib/Mcp/BuiltIn/SchemaDerivedToolProvider.php @@ -0,0 +1,901 @@ + derived precedence, REQ-DERIVED-003). + * + * Writes go through `ObjectService` in the caller's ambient Nextcloud + * session — no impersonation, no system account, RBAC/IDOR unchanged from + * the REST path (REQ-DERIVED-005). Every invocation — read, write, or + * failed — writes exactly one immutable, hash-chained audit record via + * `AuditTrailMapper::createToolInvocationEntry()` (REQ-DERIVED-006). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Mcp + * @package OCA\OpenRegister\Mcp\BuiltIn + * + * @author Conduction Development Team + * @copyright 2026 Conduction BV + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/ai-mcp/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Mcp\BuiltIn; + +use InvalidArgumentException; +use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Mcp\IMcpToolProvider; +use OCA\OpenRegister\Service\Mcp\McpAnnotationValidator; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * SchemaDerivedToolProvider + * + * Built-in IMcpToolProvider that derives coarse-CRUD tools from the + * `x-openregister-mcp` schema dialect. One instance per owning app. + * + * @category Mcp + * @package OCA\OpenRegister\Mcp\BuiltIn + * + * @psalm-suppress UnusedClass - Instantiated by Application::registerMcpToolProviders() + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-001 — SchemaDerivedToolProvider emits declarative CRUD tools) + */ +class SchemaDerivedToolProvider implements IMcpToolProvider +{ + + /** + * Default `search` page size when the caller omits `pageSize`. + */ + private const DEFAULT_PAGE_SIZE = 20; + + /** + * Hard maximum `search` page size — a caller-supplied `pageSize` above + * this is clamped, never rejected. + */ + private const MAX_PAGE_SIZE = 100; + + /** + * String values longer than this are truncated (with an ellipsis + * marker) in `search` results to keep token cost sane. + */ + private const TRUNCATE_LENGTH = 500; + + /** + * Constructor. + * + * `$schemaEntries` is a `list` + * — that app's opted-in schemas, each paired with its owning register + * (when resolvable). `$suppressedIds` is a `list` of tool ids + * already claimed by a hand-written provider for this app; the derived + * duplicates are self-suppressed (REQ-DERIVED-003). + * + * @param string $appId The owning app id; `getAppId()` and every id prefix. + * @param array $schemaEntries The app's opted-in schema entries (see above). + * @param array $suppressedIds Hand-written tool ids to self-suppress (see above). + * @param ObjectService $objectService RBAC-enforcing object read/write facade. + * @param AuditTrailMapper $auditTrailMapper Immutable hash-chained audit-trail writer. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly string $appId, + private readonly array $schemaEntries, + private readonly array $suppressedIds, + private readonly ObjectService $objectService, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Returns the owning app id. + * + * @return string The owning app id this instance was constructed for. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-001 — one derived provider instance per owning app, + * getAppId() returning that owning app id so every emitted tool id keeps the + * IMcpToolProvider ABI's "id prefix == getAppId()" invariant) + */ + public function getAppId(): string + { + return $this->appId; + }//end getAppId() + + /** + * Returns one tool descriptor per declared verb of every opted-in schema. + * + * Each descriptor additionally carries the verb's declared MCP annotation + * hints and `scope` when the dialect set them (omitted when it didn't — + * no defaults are invented). Both are ADVISORY metadata for the LLM/UX + * only; OpenRegister RBAC via `ObjectService` remains the authoritative + * invoke-time gate (REQ-DERIVED-005, ADR-063). + * + * @return list + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-001 — SchemaDerivedToolProvider emits declarative CRUD tools) + */ + public function getTools(): array + { + $descriptors = []; + + foreach ($this->schemaEntries as $entry) { + $schema = $entry['schema']; + $annotation = $this->mcpAnnotation(schema: $schema); + if (($annotation['enabled'] ?? false) !== true) { + continue; + } + + $slug = $this->schemaSlug(schema: $schema); + $toolsConfig = ($annotation['tools'] ?? null); + $verbs = McpAnnotationValidator::VERBS; + if (is_array($toolsConfig) === true) { + $verbs = array_values(array_intersect(McpAnnotationValidator::VERBS, array_keys($toolsConfig))); + } + + foreach ($verbs as $verb) { + $id = $this->appId.'.'.$slug.'.'.$verb; + + // Self-suppression (REQ-DERIVED-003): a hand-written per-app + // provider already claims this id — omit the derived + // duplicate entirely rather than merely shadowing it. + if (in_array($id, $this->suppressedIds, true) === true) { + continue; + } + + $verbConfig = []; + if (is_array($toolsConfig) === true && is_array($toolsConfig[$verb] ?? null) === true) { + $verbConfig = $toolsConfig[$verb]; + } + + $descriptors[] = $this->buildDescriptor( + schema: $schema, + slug: $slug, + verb: $verb, + verbConfig: $verbConfig, + id: $id + ); + }//end foreach + }//end foreach + + return $descriptors; + }//end getTools() + + /** + * Dispatch a tool invocation by its parsed `{appId}.{schema}.{verb}` id. + * + * Writes exactly one immutable audit record per invocation — success or + * failure — before returning (or rethrowing). + * + * @param string $toolId Namespaced tool id, e.g. "pipelinq.lead.search". + * @param array $arguments JSON-decoded tool arguments. + * + * @return array JSON-encodable result. + * + * @throws InvalidArgumentException If the tool id is unknown or required params are missing. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-005 — Writes go through ObjectService with RBAC intact) + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-006 — Every invocation is audited) + */ + public function invokeTool(string $toolId, array $arguments): array + { + $resolved = $this->resolveEntry(toolId: $toolId); + if ($resolved === null) { + throw new InvalidArgumentException('Unknown tool: '.$toolId); + } + + $schema = $resolved['schema']; + $register = $resolved['register']; + $verb = $resolved['verb']; + + $paramsDigest = hash('sha256', $this->canonicalArguments(arguments: $arguments)); + + try { + $result = match ($verb) { + 'search' => $this->search(schema: $schema, register: $register, arguments: $arguments), + 'get' => $this->get(schema: $schema, register: $register, arguments: $arguments), + 'create' => $this->create(schema: $schema, register: $register, arguments: $arguments), + 'update' => $this->update(schema: $schema, register: $register, arguments: $arguments), + 'delete' => $this->delete(schema: $schema, register: $register, arguments: $arguments), + default => throw new InvalidArgumentException('Unknown verb: '.$verb), + }; + + $this->writeAudit( + toolId: $toolId, + paramsDigest: $paramsDigest, + resultSummary: $this->resultSummary(verb: $verb, result: $result), + schema: $schema, + register: $register, + result: $result + ); + + return $result; + } catch (Throwable $e) { + $this->writeAudit( + toolId: $toolId, + paramsDigest: $paramsDigest, + resultSummary: [ + 'isError' => true, + 'errorClass' => get_class($e), + 'message' => substr($e->getMessage(), 0, 300), + ], + schema: $schema, + register: $register, + result: null + ); + + throw $e; + }//end try + }//end invokeTool() + + /** + * Search verb: declared-filter-only, bounded/clamped pagination, + * optional field projection, string truncation, has-more/total metadata. + * + * @param Schema $schema The target schema. + * @param Register|null $register The schema's owning register, when resolvable. + * @param array $arguments Tool arguments. + * + * @return array `{results, page, pageSize, total, hasMore}`. + * + * @throws InvalidArgumentException If `filters` names an undeclared property. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-004 — Search verb: filters, pagination, projection, truncation) + */ + private function search(Schema $schema, ?Register $register, array $arguments): array + { + $annotation = $this->mcpAnnotation(schema: $schema); + $declaredFilters = ($annotation['tools']['search']['filters'] ?? []); + if (is_array($declaredFilters) === false) { + $declaredFilters = []; + } + + $filters = ($arguments['filters'] ?? []); + if (is_array($filters) === false) { + throw new InvalidArgumentException('`filters` must be an object.'); + } + + foreach (array_keys($filters) as $filterKey) { + if (in_array($filterKey, $declaredFilters, true) === false) { + throw new InvalidArgumentException( + sprintf( + 'Undeclared search filter "%s". Declared filters: [%s].', + (string) $filterKey, + implode(', ', $declaredFilters) + ) + ); + } + } + + $page = (int) ($arguments['page'] ?? 1); + if ($page < 1) { + $page = 1; + } + + $pageSize = (int) ($arguments['pageSize'] ?? self::DEFAULT_PAGE_SIZE); + if ($pageSize < 1) { + $pageSize = self::DEFAULT_PAGE_SIZE; + } + + $pageSize = min($pageSize, self::MAX_PAGE_SIZE); + $offset = (($page - 1) * $pageSize); + + $config = [ + 'limit' => $pageSize, + 'offset' => $offset, + 'filters' => $filters, + ]; + + $fields = ($arguments['fields'] ?? null); + if (is_array($fields) === true && count($fields) > 0) { + $config['fields'] = $fields; + } + + $this->anchorObjectService(schema: $schema, register: $register); + + $objects = $this->objectService->findAll(config: $config); + $total = $this->objectService->count(config: ['filters' => $filters]); + + $results = array_map( + callback: fn($object) => $this->truncateStrings(data: $object->jsonSerialize()), + array: $objects + ); + + return [ + 'results' => $results, + 'page' => $page, + 'pageSize' => $pageSize, + 'total' => $total, + 'hasMore' => (($offset + count($results)) < $total), + ]; + }//end search() + + /** + * Get verb: fetch a single object by id/uuid via `ObjectService`. + * + * @param Schema $schema The target schema. + * @param Register|null $register The schema's owning register, when resolvable. + * @param array $arguments Must contain `id`. + * + * @return array Serialized object. + * + * @throws InvalidArgumentException If `id` is missing. + */ + private function get(Schema $schema, ?Register $register, array $arguments): array + { + $this->requireParam(arguments: $arguments, param: 'id'); + + $object = $this->objectService->find( + id: $arguments['id'], + register: $register, + schema: $schema + ); + + return $object->jsonSerialize(); + }//end get() + + /** + * Create verb: write a new object through `ObjectService` (RBAC intact). + * + * @param Schema $schema The target schema. + * @param Register|null $register The schema's owning register, when resolvable. + * @param array $arguments The object payload (schema-shaped). + * + * @return array Serialized created object. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-005 — Write is authorized exactly as the REST path) + */ + private function create(Schema $schema, ?Register $register, array $arguments): array + { + $object = $this->objectService->saveObject( + object: $arguments, + register: $register, + schema: $schema + ); + + return $object->jsonSerialize(); + }//end create() + + /** + * Update verb: write an existing object through `ObjectService` (RBAC intact). + * + * @param Schema $schema The target schema. + * @param Register|null $register The schema's owning register, when resolvable. + * @param array $arguments Must contain `id`; the rest is the object payload. + * + * @return array Serialized updated object. + * + * @throws InvalidArgumentException If `id` is missing. + */ + private function update(Schema $schema, ?Register $register, array $arguments): array + { + $this->requireParam(arguments: $arguments, param: 'id'); + + $id = $arguments['id']; + $data = $arguments; + unset($data['id']); + + $object = $this->objectService->saveObject( + object: $data, + register: $register, + schema: $schema, + uuid: $id + ); + + return $object->jsonSerialize(); + }//end update() + + /** + * Delete verb: remove an object through `ObjectService` (RBAC intact — an + * unauthorized delete raises the same error the REST path returns, + * which `invokeTool()` audits and rethrows for the isError envelope). + * + * @param Schema $schema The target schema. + * @param Register|null $register The schema's owning register, when resolvable. + * @param array $arguments Must contain `id`. + * + * @return array Success confirmation. + * + * @throws InvalidArgumentException If `id` is missing. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-005 — Unauthorized write fails, no bypass) + */ + private function delete(Schema $schema, ?Register $register, array $arguments): array + { + $this->requireParam(arguments: $arguments, param: 'id'); + + $id = $arguments['id']; + $this->objectService->deleteObject( + uuid: $id, + register: $register, + schema: $schema + ); + + return ['deleted' => true, 'id' => $id]; + }//end delete() + + /** + * Anchor `ObjectService`'s ambient register/schema context for a call + * that needs it set before invocation (mirrors {@see ObjectsToolProvider}). + * + * @param Schema $schema The target schema. + * @param Register|null $register The schema's owning register, when resolvable. + * + * @return void + */ + private function anchorObjectService(Schema $schema, ?Register $register): void + { + if ($register !== null) { + $this->objectService->setRegister(register: $register); + } + + $this->objectService->setSchema(schema: $schema); + }//end anchorObjectService() + + /** + * Build one tool descriptor for a schema+verb pair. + * + * @param Schema $schema The schema being derived. + * @param string $slug The schema's slug (id-tail). + * @param string $verb One of {@see McpAnnotationValidator::VERBS}. + * @param array $verbConfig That verb's dialect config (description/scope/filters/hints). + * @param string $id The fully-namespaced tool id. + * + * @return array{id: string, name: string, description: string, inputSchema: array, + * outputSchema?: array, readOnlyHint?: bool, destructiveHint?: bool, + * idempotentHint?: bool, scope?: string} + */ + private function buildDescriptor(Schema $schema, string $slug, string $verb, array $verbConfig, string $id): array + { + $descriptor = [ + 'id' => $id, + 'name' => $slug.'_'.$verb, + 'description' => $this->description(slug: $slug, verb: $verbConfig['description'] ?? null, verbName: $verb), + 'inputSchema' => $this->buildInputSchema(schema: $schema, verb: $verb, verbConfig: $verbConfig), + ]; + + $outputSchema = $this->buildOutputSchema(schema: $schema, verb: $verb); + if ($outputSchema !== null) { + $descriptor['outputSchema'] = $outputSchema; + } + + // MCP 2025-11-25 annotation hints — untrusted UX metadata only, per + // design.md; they inform the LLM and never gate execution. + foreach (McpAnnotationValidator::HINT_KEYS as $hintKey) { + if (isset($verbConfig[$hintKey]) === true) { + $descriptor[$hintKey] = $verbConfig[$hintKey]; + } + } + + // The dialect's per-verb `scope` (REQ-DIALECT-001, validated against + // McpAnnotationValidator::SCOPES at save time). Emitted additively, + // omitted entirely when the verb didn't declare one — no default is + // invented, because an invented `read` would be a claim the dialect + // never made. Like the hints above it is ADVISORY metadata only: + // ObjectService RBAC remains the authoritative invoke-time gate + // (REQ-DERIVED-005), and nothing in this class reads `scope` back. + if (isset($verbConfig['scope']) === true) { + $descriptor['scope'] = $verbConfig['scope']; + } + + return $descriptor; + }//end buildDescriptor() + + /** + * Resolve a verb's description: the declared override, else a generated + * default naming the schema and owning app. + * + * @param string $slug Schema slug. + * @param string|null $verb Declared description override, or null. + * @param string $verbName The verb name. + * + * @return string The resolved description. + */ + private function description(string $slug, ?string $verb, string $verbName): string + { + if (is_string($verb) === true && $verb !== '') { + return $verb; + } + + return match ($verbName) { + 'search' => sprintf('Search %s objects in %s.', $slug, $this->appId), + 'get' => sprintf('Get a single %s object by id in %s.', $slug, $this->appId), + 'create' => sprintf('Create a %s object in %s.', $slug, $this->appId), + 'update' => sprintf('Update a %s object in %s.', $slug, $this->appId), + 'delete' => sprintf('Delete a %s object in %s.', $slug, $this->appId), + default => sprintf('%s %s objects in %s.', ucfirst($verbName), $slug, $this->appId), + }; + }//end description() + + /** + * Build the `inputSchema` for a verb, reusing the schema JSON per + * design.md's derivation table. + * + * @param Schema $schema The schema being derived. + * @param string $verb One of {@see McpAnnotationValidator::VERBS}. + * @param array $verbConfig That verb's dialect config. + * + * @return array JSON-Schema-shaped input schema. + */ + private function buildInputSchema(Schema $schema, string $verb, array $verbConfig): array + { + $properties = ($schema->getProperties() ?? []); + $required = ($schema->getRequired() ?? []); + $idProperty = ['type' => 'string', 'description' => 'Object id or uuid.']; + + return match ($verb) { + 'search' => [ + 'type' => 'object', + 'properties' => [ + 'filters' => [ + 'type' => 'object', + 'properties' => $this->filterProperties(properties: $properties, verbConfig: $verbConfig), + 'additionalProperties' => false, + ], + 'page' => ['type' => 'integer', 'description' => 'Page number (default 1).'], + 'pageSize' => [ + 'type' => 'integer', + 'description' => sprintf( + 'Results per page (default %d, max %d).', + self::DEFAULT_PAGE_SIZE, + self::MAX_PAGE_SIZE + ), + ], + 'fields' => [ + 'type' => 'array', + 'items' => ['type' => 'string'], + 'description' => 'Optional field projection.', + ], + ], + ], + 'get' => [ + 'type' => 'object', + 'properties' => ['id' => $idProperty], + 'required' => ['id'], + ], + 'create' => [ + 'type' => 'object', + 'properties' => $properties, + 'required' => $required, + ], + 'update' => [ + 'type' => 'object', + 'properties' => array_merge(['id' => $idProperty], $properties), + 'required' => ['id'], + ], + 'delete' => [ + 'type' => 'object', + 'properties' => ['id' => $idProperty], + 'required' => ['id'], + ], + default => ['type' => 'object', 'properties' => []], + };//end match + }//end buildInputSchema() + + /** + * Build the `outputSchema` for read verbs (MCP 2025-06-18 + * `structuredContent`), null for write verbs. + * + * @param Schema $schema The schema being derived. + * @param string $verb One of {@see McpAnnotationValidator::VERBS}. + * + * @return array|null The output schema, or null when not applicable. + */ + private function buildOutputSchema(Schema $schema, string $verb): ?array + { + $itemSchema = [ + 'type' => 'object', + 'properties' => ($schema->getProperties() ?? []), + ]; + + return match ($verb) { + 'search' => [ + 'type' => 'object', + 'properties' => [ + 'results' => ['type' => 'array', 'items' => $itemSchema], + 'total' => ['type' => 'integer'], + 'hasMore' => ['type' => 'boolean'], + ], + ], + 'get' => $itemSchema, + default => null, + }; + }//end buildOutputSchema() + + /** + * Narrow a schema's properties down to the ones a `search` verb declared + * as filters (REQ-DERIVED-004: only declared filters are honoured). + * + * @param array $properties The schema's declared properties. + * @param array $verbConfig The `search` verb's dialect config. + * + * @return array The filter-eligible property subset. + */ + private function filterProperties(array $properties, array $verbConfig): array + { + $declared = ($verbConfig['filters'] ?? []); + if (is_array($declared) === false) { + return []; + } + + $result = []; + foreach ($declared as $name) { + if (is_string($name) === true && array_key_exists($name, $properties) === true) { + $result[$name] = $properties[$name]; + } + } + + return $result; + }//end filterProperties() + + /** + * Resolve the validated `x-openregister-mcp` block, or an empty array + * when absent/malformed. + * + * @param Schema $schema The schema to read. + * + * @return array The `x-openregister-mcp` block. + */ + private function mcpAnnotation(Schema $schema): array + { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-mcp'] ?? []); + if (is_array($annotation) === false) { + return []; + } + + return $annotation; + }//end mcpAnnotation() + + /** + * A schema's slug, falling back to its numeric id when unset. + * + * @param Schema $schema The schema. + * + * @return string The slug (id-tail used in every derived tool id). + */ + private function schemaSlug(Schema $schema): string + { + $slug = $schema->getSlug(); + if (is_string($slug) === true && $slug !== '') { + return $slug; + } + + return (string) $schema->getId(); + }//end schemaSlug() + + /** + * Resolve a namespaced tool id back to its schema/register/verb. + * + * @param string $toolId Namespaced tool id, e.g. "pipelinq.lead.search". + * + * @return array{schema: Schema, register: Register|null, verb: string}|null + */ + private function resolveEntry(string $toolId): ?array + { + $parts = explode('.', $toolId); + if (count($parts) < 3) { + return null; + } + + $verb = array_pop($parts); + $slug = array_pop($parts); + $appId = implode('.', $parts); + + if ($appId !== $this->appId || in_array($verb, McpAnnotationValidator::VERBS, true) === false) { + return null; + } + + foreach ($this->schemaEntries as $entry) { + if ($this->schemaSlug(schema: $entry['schema']) === $slug) { + return [ + 'schema' => $entry['schema'], + 'register' => $entry['register'], + 'verb' => $verb, + ]; + } + } + + return null; + }//end resolveEntry() + + /** + * Build a small structured outcome summary for a successful invocation + * (never raw object payloads — REQ-DERIVED-006). + * + * @param string $verb The invoked verb. + * @param array $result The verb's return value. + * + * @return array The result summary. + */ + private function resultSummary(string $verb, array $result): array + { + if ($verb === 'search') { + return [ + 'isError' => false, + 'count' => count($result['results'] ?? []), + 'total' => ($result['total'] ?? null), + 'hasMore' => ($result['hasMore'] ?? null), + ]; + } + + $id = null; + if (is_string($result['id'] ?? null) === true) { + $id = $result['id']; + } + + return [ + 'isError' => false, + 'id' => $id, + ]; + }//end resultSummary() + + /** + * Write exactly one immutable, hash-chained audit record for this + * invocation. Fail-soft: a broken audit write is logged and MUST NOT + * mask (or replace) the invocation's real result/error — losing an + * audit row is bad, losing the invocation outcome to an audit hiccup is + * worse (mirrors {@see \OCA\OpenRegister\Service\ObjectService::logProcessingRead()}). + * + * @param string $toolId Full namespaced tool id. + * @param string $paramsDigest SHA-256 hex digest of the invocation arguments. + * @param array $resultSummary Structured outcome summary. + * @param Schema $schema The invoked tool's schema. + * @param Register|null $register The schema's owning register, when resolvable. + * @param array|null $result The verb's return value, or null on failure. + * + * @return void + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-006 — Every invocation is audited) + */ + private function writeAudit( + string $toolId, + string $paramsDigest, + array $resultSummary, + Schema $schema, + ?Register $register, + ?array $result + ): void { + try { + $objectUuid = null; + if ($result !== null && is_string($result['id'] ?? null) === true) { + $objectUuid = $result['id']; + } + + $registerId = null; + if ($register !== null) { + $registerId = $register->getId(); + } + + $this->auditTrailMapper->createToolInvocationEntry( + toolId: $toolId, + paramsDigest: $paramsDigest, + resultSummary: $resultSummary, + register: $registerId, + schema: $schema->getId(), + objectUuid: $objectUuid + ); + } catch (Throwable $e) { + $this->logger->error( + '[SchemaDerivedToolProvider] Failed to write invocation audit record', + ['toolId' => $toolId, 'error' => $e->getMessage()] + ); + }//end try + }//end writeAudit() + + /** + * Deterministically canonicalise arguments (recursive key-sort) so the + * params digest is stable regardless of argument-array key order. + * + * @param array $arguments Tool arguments. + * + * @return string Canonical JSON representation. + */ + private function canonicalArguments(array $arguments): string + { + $encoded = json_encode( + $this->deepKsort(value: $arguments), + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + ); + + if ($encoded === false) { + return ''; + } + + return $encoded; + }//end canonicalArguments() + + /** + * Recursively sort an array by key for deterministic serialisation. + * + * @param array $value The array to sort. + * + * @return array The sorted array. + */ + private function deepKsort(array $value): array + { + ksort($value); + foreach ($value as $key => $item) { + if (is_array($item) === true) { + $value[$key] = $this->deepKsort(value: $item); + } + } + + return $value; + }//end deepKsort() + + /** + * Truncate long top-level string values in a serialized object so a + * `search` page cannot burn an unbounded token budget. + * + * @param array $data The serialized object. + * + * @return array The same object, long strings truncated. + */ + private function truncateStrings(array $data): array + { + foreach ($data as $key => $value) { + if (is_string($value) === true && strlen($value) > self::TRUNCATE_LENGTH) { + $data[$key] = substr($value, 0, self::TRUNCATE_LENGTH).'…'; + } + } + + return $data; + }//end truncateStrings() + + /** + * Assert a parameter is present in arguments. + * + * @param array $arguments Tool arguments. + * @param string $param Required parameter name. + * + * @return void + * + * @throws InvalidArgumentException If parameter is missing. + */ + private function requireParam(array $arguments, string $param): void + { + if (isset($arguments[$param]) === false) { + throw new InvalidArgumentException('Missing required parameter: '.$param); + } + }//end requireParam() +}//end class diff --git a/lib/Mcp/BuiltIn/SchemasToolProvider.php b/lib/Mcp/BuiltIn/SchemasToolProvider.php index 539116273c..90c04a7c2a 100644 --- a/lib/Mcp/BuiltIn/SchemasToolProvider.php +++ b/lib/Mcp/BuiltIn/SchemasToolProvider.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#imcptoolprovider-built-in-migration + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); @@ -216,9 +216,19 @@ private function updateSchema(array $arguments): array /** * Delete a schema. * + * **DELETE SAFETY (runtime-schema-api)**: this is an LLM-invokable schema-destruction + * surface with no controller in front of it. It deliberately does NOT pass + * `force: true` — SchemaMapper::delete() refuses a schema that still holds objects, + * and a model acting on an ambiguous instruction must not be able to orphan a user's + * data. The ValidationException surfaces to the model as a tool error naming the + * object count; deleting the objects is a separate, explicit human decision + * (`DELETE /api/schemas/{id}?deleteObjects=true`). + * * @param array $arguments Must contain id * * @return array Success message + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md */ private function deleteSchema(array $arguments): array { diff --git a/lib/Mcp/IMcpScannableServices.php b/lib/Mcp/IMcpScannableServices.php new file mode 100644 index 0000000000..c05b9e5191 --- /dev/null +++ b/lib/Mcp/IMcpScannableServices.php @@ -0,0 +1,60 @@ +` (mirrors the existing + * `OCA\OpenRegister\Mcp\IMcpToolProvider::` per-app discovery + * convention — see {@see \OCA\OpenRegister\AppInfo\Application::buildMcpProviderCandidates()}). + * + * This is the PROVISIONAL resolution of design.md's "Discovery scope" + * DEFERRED_QUESTION — the design lists a DI-declared scannable-service list + * as the explicit/cheapest of two candidate mechanisms; this interface is + * that mechanism. A future change may add the conventional-namespace-scan + * alternative alongside it without breaking this contract. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Mcp + * @package OCA\OpenRegister\Mcp + * + * @author Conduction Development Team + * @copyright 2026 Conduction BV + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-ATTR-002 — Reflection scanner registers attributed tools in the same catalog) + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Mcp; + +/** + * IMcpScannableServices + * + * Contract for the per-app scannable-service declaration. An app registers + * one implementation under `OCA\OpenRegister\Mcp\IMcpScannableServices::` + * (a container alias, exactly like the per-app IMcpToolProvider convention). + * + * @category Mcp + * @package OCA\OpenRegister\Mcp + */ +interface IMcpScannableServices +{ + /** + * The app's own service classes eligible for `#[McpTool]` reflection. + * + * @return list Fully-qualified service class names owned by this app. + */ + public function getScannableServiceClasses(): array; +}//end interface diff --git a/lib/Mcp/IMcpToolProvider.php b/lib/Mcp/IMcpToolProvider.php index 770143397f..5745f8e5c5 100644 --- a/lib/Mcp/IMcpToolProvider.php +++ b/lib/Mcp/IMcpToolProvider.php @@ -22,7 +22,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#imcptoolprovider-php-interface + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); diff --git a/lib/Middleware/AuthRateLimitExceededException.php b/lib/Middleware/AuthRateLimitExceededException.php index ed3070b52a..54ca7ddbaa 100644 --- a/lib/Middleware/AuthRateLimitExceededException.php +++ b/lib/Middleware/AuthRateLimitExceededException.php @@ -18,7 +18,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ declare(strict_types=1); @@ -41,7 +41,7 @@ class AuthRateLimitExceededException extends Exception * @param string $message The error message * @param int $lockoutUntil Unix timestamp the lockout expires at (0 if unknown) * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function __construct( string $message, @@ -55,7 +55,7 @@ public function __construct( * * @return int The lockout expiry timestamp, or 0 when unknown * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function getLockoutUntil(): int { diff --git a/lib/Middleware/ChatCompatMiddleware.php b/lib/Middleware/ChatCompatMiddleware.php new file mode 100644 index 0000000000..9cb0380aaf --- /dev/null +++ b/lib/Middleware/ChatCompatMiddleware.php @@ -0,0 +1,339 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/chat-ai/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Middleware; + +use Exception; +use OCA\OpenRegister\Controller\AgentsController; +use OCA\OpenRegister\Controller\ChatController; +use OCA\OpenRegister\Controller\ChatHealthController; +use OCA\OpenRegister\Controller\ChatStreamController; +use OCA\OpenRegister\Controller\ConversationController; +use OCA\OpenRegister\Middleware\Exception\ChatProxiedResponseException; +use OCA\OpenRegister\Service\Chat\ChatProxyHandler; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\IRequest; + +/** + * Deprecation-header decorator + optional hermiq proxy short-circuit for + * OpenRegister's chat/agents/conversations controllers. + * + * @category Middleware + * @package OCA\OpenRegister\Middleware + */ +class ChatCompatMiddleware extends Middleware +{ + + /** + * Controllers this middleware decorates/may proxy. Membership is checked + * via `instanceof` (not `get_class()`) so PHPUnit mocks — which subclass + * the real controller — match correctly. + * + * @var array + */ + private const CHAT_FAMILY_CONTROLLERS = [ + ChatController::class, + ChatStreamController::class, + ChatHealthController::class, + ConversationController::class, + AgentsController::class, + ]; + + /** + * Methods on a chat-family controller that render the SPA page shell + * rather than an API response (e.g. `AgentsController::page()`). Never + * proxied — the shell's static assets still ship with OpenRegister + * during the compat window; only the `chatAppId` flip (separate, + * nextcloud-vue-side work) changes which backend the SPA talks to. + * + * @var array + */ + private const PAGE_SHELL_METHODS = ['page']; + + /** + * Controllers proxied via a redirect rather than a body relay — see + * ChatProxyHandler::buildRedirectResponse()'s docblock for why. + * + * @var array + */ + private const STREAMING_CONTROLLERS = [ + ChatStreamController::class, + ]; + + /** + * `Deprecation` response header value (RFC 8594-style — an HTTP-date + * marking when the deprecation posture took effect). + * + * @var string + */ + private const DEPRECATION_DATE = 'Mon, 06 Jul 2026 00:00:00 GMT'; + + /** + * `Sunset` response header value (RFC 8594) — the earliest the + * *following* removal change may ship. At least one full release cycle + * out; the actual removal is a separate, not-yet-specified change (see + * design.md). + * + * @var string + */ + private const SUNSET_DATE = 'Wed, 06 Jan 2027 00:00:00 GMT'; + + /** + * `Link: <...>; rel="successor-version"` target. + * + * @var string + */ + private const SUCCESSOR_LINK_PATH = '/apps/hermiq/api/chat'; + + /** + * Constructor. + * + * @param IRequest $request The incoming request. + * @param ChatProxyHandler $proxyHandler Resolves config, rewrites paths, performs the outbound calls. + */ + public function __construct( + private readonly IRequest $request, + private readonly ChatProxyHandler $proxyHandler + ) { + }//end __construct() + + /** + * Attempt the optional hermiq proxy short-circuit. + * + * Throws `ChatProxiedResponseException` (carrying the built response) + * when the proxy is configured, hermiq is installed, and the forward + * (or, for the streaming controller, the reachability-gated redirect) + * succeeds. In every other case this returns normally and the local + * controller method runs exactly as it does today. + * + * @param mixed $controller The controller instance. + * @param string $methodName The method name being called. + * + * @return void + * + * @throws ChatProxiedResponseException When the request has been served by the proxy. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeController($controller, $methodName): void + { + $hermiqPath = $this->resolveHermiqPath(controller: $controller, methodName: $methodName); + if ($hermiqPath === null) { + return; + } + + $response = $this->attemptProxy(controller: $controller, hermiqPath: $hermiqPath); + if ($response === null) { + // Unreachable / transport failure — fall back to local serving. + // Do NOT throw; returning normally lets the real controller + // method run exactly as it would with the proxy off. + return; + } + + throw new ChatProxiedResponseException(response: $response); + }//end beforeController() + + /** + * Resolve the guards that decide whether the proxy is even eligible for + * this call, and — if so — the rewritten hermiq path. + * + * @param mixed $controller The controller instance. + * @param string $methodName The method name being called. + * + * @return string|null The rewritten `/apps/hermiq/...` path, or null when any guard fails. + */ + private function resolveHermiqPath($controller, $methodName): ?string + { + if ($this->isChatFamilyController(controller: $controller) === false) { + return null; + } + + if (in_array($methodName, self::PAGE_SHELL_METHODS, true) === true) { + return null; + } + + if ($this->proxyHandler->isProxyConfigured() === false) { + return null; + } + + if ($this->proxyHandler->isHermiqInstalled() === false) { + return null; + } + + $pathInfo = $this->request->getPathInfo(); + if ($pathInfo === false) { + return null; + } + + return $this->proxyHandler->rewritePathForHermiq(orPathInfo: $pathInfo); + }//end resolveHermiqPath() + + /** + * Perform the actual proxy call — a reachability-gated redirect for the + * streaming controller, a JSON forward for every other chat-family + * controller. + * + * @param mixed $controller The controller instance. + * @param string $hermiqPath The rewritten `/apps/hermiq/...` path. + * + * @return Response|null The proxied response, or null on any failure (fall back to local serving). + */ + private function attemptProxy($controller, string $hermiqPath): ?Response + { + if ($this->isStreamingController(controller: $controller) === true) { + return $this->attemptStreamRedirect(hermiqPath: $hermiqPath); + } + + return $this->proxyHandler->forwardJsonRequest(request: $this->request, hermiqPath: $hermiqPath); + }//end attemptProxy() + + /** + * Handle the proxy short-circuit exception, returning its carried + * response. Any other exception is re-thrown (default Middleware + * behaviour) so normal error handling proceeds unaffected. + * + * @param mixed $controller The controller instance. + * @param string $methodName The method name being called. + * @param Exception $exception The thrown exception. + * + * @return Response The proxied response. + * + * @throws Exception The passed-in exception when it is not ours to handle. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterException($controller, $methodName, Exception $exception): Response + { + if ($exception instanceof ChatProxiedResponseException) { + return $exception->getResponse(); + } + + throw $exception; + }//end afterException() + + /** + * Decorate every chat-family controller response — proxied or local — + * with the compat-window deprecation headers. + * + * @param mixed $controller The controller instance. + * @param string $methodName The method name that was called. + * @param Response $response The response to decorate. + * + * @return Response The (possibly decorated) response. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterController($controller, $methodName, Response $response): Response + { + if ($this->isChatFamilyController(controller: $controller) === false) { + return $response; + } + + $response->addHeader('Deprecation', self::DEPRECATION_DATE); + $response->addHeader('Sunset', self::SUNSET_DATE); + $response->addHeader('Link', '<'.self::SUCCESSOR_LINK_PATH.'>; rel="successor-version"'); + + return $response; + }//end afterController() + + /** + * Attempt the reachability-gated redirect for the streaming controller. + * + * @param string $hermiqPath The rewritten `/apps/hermiq/...` path. + * + * @return Response|null The redirect response, or null when hermiq did not answer the probe. + */ + private function attemptStreamRedirect(string $hermiqPath): ?Response + { + if ($this->proxyHandler->probeReachable() === false) { + return null; + } + + return $this->proxyHandler->buildRedirectResponse(request: $this->request, hermiqPath: $hermiqPath); + }//end attemptStreamRedirect() + + /** + * Whether the given controller is one of the chat/agents/conversations + * family this middleware decorates. + * + * @param mixed $controller The controller instance. + * + * @return bool True when `$controller` is (or extends/mocks) one of CHAT_FAMILY_CONTROLLERS. + */ + private function isChatFamilyController($controller): bool + { + if (($controller instanceof Controller) === false) { + return false; + } + + foreach (self::CHAT_FAMILY_CONTROLLERS as $class) { + if ($controller instanceof $class) { + return true; + } + } + + return false; + }//end isChatFamilyController() + + /** + * Whether the given controller is the SSE streaming controller (proxied + * via redirect rather than a body relay). + * + * @param mixed $controller The controller instance. + * + * @return bool True when `$controller` is (or extends/mocks) ChatStreamController. + */ + private function isStreamingController($controller): bool + { + foreach (self::STREAMING_CONTROLLERS as $class) { + if ($controller instanceof $class) { + return true; + } + } + + return false; + }//end isStreamingController() +}//end class diff --git a/lib/Middleware/EncryptedFieldFilterMiddleware.php b/lib/Middleware/EncryptedFieldFilterMiddleware.php new file mode 100644 index 0000000000..796633fedc --- /dev/null +++ b/lib/Middleware/EncryptedFieldFilterMiddleware.php @@ -0,0 +1,96 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/field-level-encryption/spec.md#requirement-encrypted-fields-are-excluded-from-search-and-facets + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Middleware; + +use Exception; +use OCA\OpenRegister\Exception\EncryptedFieldFilterException; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use Psr\Log\LoggerInterface; + +/** + * Middleware translating encrypted-field filter attempts into 400 responses. + * + * @package OCA\OpenRegister\Middleware + */ +class EncryptedFieldFilterMiddleware extends Middleware +{ + /** + * Constructor. + * + * @param LoggerInterface $logger The app logger. + * + * @return void + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Map EncryptedFieldFilterException onto a 400 Bad Request response. + * + * @param Controller $controller The controller that was executing. + * @param string $methodName The controller method that was executing. + * @param Exception $exception The exception that was thrown. + * + * @return Response The 400 response for an encrypted-field filter attempt. + * + * @throws Exception Rethrows every exception that is not an EncryptedFieldFilterException. + * + * @spec openspec/specs/field-level-encryption/spec.md#requirement-encrypted-fields-are-excluded-from-search-and-facets + */ + public function afterException(Controller $controller, string $methodName, Exception $exception): Response + { + if ($exception instanceof EncryptedFieldFilterException === false) { + throw $exception; + } + + $this->logger->info( + sprintf( + '[EncryptedFieldFilterMiddleware] rejected filter on encrypted property "%s" in %s::%s', + $exception->getProperty(), + $controller::class, + $methodName + ), + ['file' => __FILE__, 'line' => __LINE__] + ); + + return new JSONResponse( + data: [ + 'error' => 'encrypted-field-not-filterable', + 'property' => $exception->getProperty(), + 'message' => $exception->getMessage(), + ], + statusCode: 400 + ); + }//end afterException() +}//end class diff --git a/lib/Middleware/Exception/ChatProxiedResponseException.php b/lib/Middleware/Exception/ChatProxiedResponseException.php new file mode 100644 index 0000000000..7960c19943 --- /dev/null +++ b/lib/Middleware/Exception/ChatProxiedResponseException.php @@ -0,0 +1,86 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/or-chat-proxy-deprecation/design.md#the-proxy-short-circuit-is-an-exception-not-an-inline-return + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Middleware\Exception; + +use Exception; +use OCP\AppFramework\Http\Response; +use Throwable; + +/** + * Carries a pre-built Response through the AppFramework exception path. + * + * @category Middleware + * @package OCA\OpenRegister\Middleware\Exception + */ +class ChatProxiedResponseException extends Exception +{ + + /** + * The response to hand back verbatim once caught by + * ChatCompatMiddleware::afterException(). + * + * @var Response + */ + private readonly Response $response; + + /** + * Constructor. + * + * @param Response $response The response to hand back verbatim. + * @param Throwable|null $previous Optional previous throwable (unused today, kept for the standard chain). + * + * @return void + */ + public function __construct(Response $response, ?Throwable $previous=null) + { + parent::__construct(message: 'Chat request served by the hermiq compat proxy', code: 0, previous: $previous); + $this->response = $response; + }//end __construct() + + /** + * Get the pre-built response. + * + * @return Response The response to return to the client. + */ + public function getResponse(): Response + { + return $this->response; + }//end getResponse() +}//end class diff --git a/lib/Middleware/LanguageMiddleware.php b/lib/Middleware/LanguageMiddleware.php index add0b50d2a..8ff08bafc3 100644 --- a/lib/Middleware/LanguageMiddleware.php +++ b/lib/Middleware/LanguageMiddleware.php @@ -3,9 +3,21 @@ /** * OpenRegister Language Middleware * - * Middleware that intercepts all requests to parse the Accept-Language header - * and store the preferred language in the LanguageService. Also checks for - * the _translations=all query parameter. + * Middleware that intercepts all requests to resolve the request's + * preferred language. Honours, in priority order: + * 1. `?_lang=` query parameter (canonical override) + * 2. `?language=` query parameter (alias) + * 3. `Accept-Language` header (RFC 9110) + * 4. Register default (resolved at render time) + * 5. Hardcoded `'nl'` fallback + * + * Also reads the write-side `X-Translation-Target-Language` header on + * POST/PUT/PATCH and stashes it on the LanguageService for the + * TranslationHandler to consume during body normalisation. + * + * Always emits `Content-Language` + `X-Content-Language-Fallback` on + * responses; emits `X-Source-Language` when the response carries object + * content for which a dominant source language can be derived. * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. @@ -26,102 +38,212 @@ namespace OCA\OpenRegister\Middleware; +use OCA\OpenRegister\Db\TranslationMapper; use OCA\OpenRegister\Service\LanguageService; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; use OCP\IRequest; +use Psr\Log\LoggerInterface; /** - * Middleware that reads the Accept-Language header and _translations query parameter. - * - * This middleware runs before any controller action and populates the - * LanguageService with the client's language preferences. It also adds - * the Content-Language response header to outgoing responses. + * Middleware that resolves the request's preferred language and the + * write-side target language, emitting language headers on responses. * * @package OCA\OpenRegister\Middleware * * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-1 + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-3 */ class LanguageMiddleware extends Middleware { + /** - * Constructor. + * Basic BCP-47 syntax check used to discard malformed query overrides. * - * @param IRequest $request The incoming request - * @param LanguageService $languageService The request-scoped language service + * Mirrors the pattern declared in + * `specs/i18n-api-language-negotiation/spec.md`. Lax by design — we + * never 400 on a malformed tag; we just fall through. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 + * @var string + */ + private const BCP47_PATTERN = '/^[a-z]{2,3}(-[a-zA-Z0-9]{2,8})*$/'; + + /** + * Constructor. + * + * @param IRequest $request The incoming request. + * @param LanguageService $languageService The request-scoped language service. + * @param TranslationMapper $translationMapper Mapper for source-language lookups (response side). + * @param LoggerInterface $logger Logger for invalid-tag warnings. */ public function __construct( private readonly IRequest $request, - private readonly LanguageService $languageService + private readonly LanguageService $languageService, + private readonly TranslationMapper $translationMapper, + private readonly LoggerInterface $logger ) { }//end __construct() /** - * Called before the controller method is invoked. - * - * Parses the Accept-Language header and stores the result in LanguageService. - * Also checks for the _translations=all query parameter. + * Resolve the preferred language and the write-side target language + * from the incoming request. * - * @param mixed $controller The controller instance - * @param string $methodName The method name being called + * @param mixed $controller The controller instance. + * @param string $methodName The method name being called. * * @return void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-1 */ public function beforeController($controller, $methodName): void { - // Parse Accept-Language header. + // 1. Query-parameter overrides take precedence over Accept-Language. + $resolvedFromQuery = $this->resolveFromQueryParams(); + if ($resolvedFromQuery !== null) { + $this->languageService->setPreferredLanguage($resolvedFromQuery); + $this->languageService->setRequestedLanguageSource('query'); + } + + // 2. Accept-Language header is the next priority. $acceptLanguage = $this->request->getHeader('Accept-Language'); if ($acceptLanguage !== '' && $acceptLanguage !== null) { $acceptedLanguages = LanguageService::parseAcceptLanguageHeader($acceptLanguage); $this->languageService->setAcceptedLanguages($acceptedLanguages); - if (empty($acceptedLanguages) === false) { - // Use the base language (strip region) as preferred. + if (empty($acceptedLanguages) === false && $resolvedFromQuery === null) { $preferred = strtolower(explode('-', $acceptedLanguages[0])[0]); $this->languageService->setPreferredLanguage($preferred); + $this->languageService->setRequestedLanguageSource('header'); } } - // Check for _translations query parameter. + // 3. _translations=all (existing). $translations = $this->request->getParam('_translations'); if ($translations === 'all') { $this->languageService->setReturnAllTranslations(true); } + + // 4. Write-side X-Translation-Target-Language on mutating verbs. + $method = strtoupper((string) $this->request->getMethod()); + if (in_array($method, ['POST', 'PUT', 'PATCH'], true) === true) { + $targetHeader = $this->request->getHeader('X-Translation-Target-Language'); + if ($targetHeader !== '' && $targetHeader !== null) { + $targetTrim = trim($targetHeader); + if (preg_match(self::BCP47_PATTERN, $targetTrim) === 1) { + $this->languageService->setTargetLanguage($targetTrim); + } else { + $this->logger->warning( + sprintf( + '[LanguageMiddleware] Invalid X-Translation-Target-Language "%s" — ignoring.', + $targetTrim + ) + ); + } + } + } }//end beforeController() /** - * Called after the controller method returns a response. - * - * Adds the Content-Language header to the response indicating - * which language was served. + * Emit language response headers. * - * @param mixed $controller The controller instance - * @param string $methodName The method name that was called - * @param Response $response The response object + * @param mixed $controller The controller instance. + * @param string $methodName The method name that was called. + * @param Response $response The response object. * - * @return Response The modified response with Content-Language header + * @return Response The modified response with language headers. * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 */ public function afterController($controller, $methodName, Response $response): Response { - // Add Content-Language header. $language = $this->languageService->getPreferredLanguage(); $response->addHeader('Content-Language', $language); - // If fallback was used, add a custom header to indicate this. if ($this->languageService->isFallbackUsed() === true) { $response->addHeader('X-Content-Language-Fallback', 'true'); } + // I18n-source-of-truth: surface the dominant source language for + // the response payload. We can only derive this when the request + // targets a single object (the path carries a uuid). Listing / + // multi-object endpoints skip the header. + try { + $sourceLanguage = $this->resolveSourceLanguageForResponse(); + if ($sourceLanguage !== null) { + $response->addHeader('X-Source-Language', $sourceLanguage); + } + } catch (\Throwable $e) { + $this->logger->debug( + '[LanguageMiddleware] X-Source-Language derivation failed: '.$e->getMessage() + ); + } + return $response; }//end afterController() + + /** + * Resolve a language from `?_lang=` or `?language=`, in that order. + * + * Returns null when neither is set, or when neither value passes + * basic BCP-47 syntax validation. Invalid tags log a warning and + * cause the lookup to fall through to the next priority level — + * we never 400 on a malformed language tag. + * + * @return string|null The resolved tag, or null when no valid query override is present. + */ + private function resolveFromQueryParams(): ?string + { + foreach (['_lang', 'language'] as $name) { + $value = $this->request->getParam($name); + if ($value === null || $value === '') { + continue; + } + + if (is_string($value) === false) { + continue; + } + + $trimmed = trim($value); + if (preg_match(self::BCP47_PATTERN, $trimmed) !== 1) { + $this->logger->warning( + sprintf( + "[LanguageMiddleware] Invalid ?%s value '%s' — falling through", + $name, + $trimmed + ) + ); + continue; + } + + return $trimmed; + }//end foreach + + return null; + }//end resolveFromQueryParams() + + /** + * Attempt to resolve the canonical source-language for the response. + * + * Uses the request path's `uuid` segment when present to look up the + * dominant source language across the object's translation rows. + * + * @return string|null Dominant source language, or null when not resolvable. + */ + private function resolveSourceLanguageForResponse(): ?string + { + $uuid = $this->request->getParam('uuid'); + if (is_string($uuid) === false || $uuid === '') { + return null; + } + + if (preg_match('/^[a-fA-F0-9\-]{8,}$/', $uuid) !== 1) { + return null; + } + + return $this->translationMapper->getDominantSourceLanguage($uuid); + }//end resolveSourceLanguageForResponse() }//end class diff --git a/lib/Middleware/OasValidationMiddleware.php b/lib/Middleware/OasValidationMiddleware.php index a9a8cec517..b279e8b093 100644 --- a/lib/Middleware/OasValidationMiddleware.php +++ b/lib/Middleware/OasValidationMiddleware.php @@ -27,7 +27,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/oas-validation/tasks.md "Request/Response Validation Against OAS Schema" + * @spec openspec/specs/oas-validation/spec.md "Request/Response Validation Against OAS Schema" */ declare(strict_types=1); diff --git a/lib/Middleware/ObjectSourceErrorMiddleware.php b/lib/Middleware/ObjectSourceErrorMiddleware.php new file mode 100644 index 0000000000..ae8a3638b4 --- /dev/null +++ b/lib/Middleware/ObjectSourceErrorMiddleware.php @@ -0,0 +1,130 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Middleware; + +use Exception; +use OCA\OpenRegister\Service\ObjectSource\DbalObjectSourceException; +use OCA\OpenRegister\Service\ObjectSource\DbalWriteException; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use Psr\Log\LoggerInterface; + +/** + * Middleware translating external-database read failures into 502/503 responses. + * + * @package OCA\OpenRegister\Middleware + */ +class ObjectSourceErrorMiddleware extends Middleware +{ + /** + * Constructor. + * + * @param LoggerInterface $logger The app logger. + * + * @return void + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Map DbalObjectSourceException onto its declared HTTP status. + * + * The exception's message is non-sensitive by contract (the provider never + * embeds credentials or SQL in it), so it is safe to return to the client. + * + * @param Controller $controller The controller that was executing. + * @param string $methodName The controller method that was executing. + * @param Exception $exception The exception that was thrown. + * + * @return Response The 502/503 response for external-database failures. + * + * @throws Exception Rethrows every exception that is not a DbalObjectSourceException. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function afterException(Controller $controller, string $methodName, Exception $exception): Response + { + // Sanitized write failures (constraint violations etc., dbal-virtual- + // registers-crud): render the carried 4xx/5xx status; log the wrapped + // driver exception server-side. + if ($exception instanceof DbalWriteException === true) { + $this->logger->warning( + sprintf( + '[ObjectSourceErrorMiddleware] external database write failed (%d) in %s::%s: %s', + $exception->getStatusCode(), + $controller::class, + $methodName, + $exception->getMessage() + ), + [ + 'file' => __FILE__, + 'line' => __LINE__, + 'exception' => $exception, + ] + ); + + return new JSONResponse( + data: ['error' => $exception->getMessage()], + statusCode: $exception->getStatusCode() + ); + }//end if + + if ($exception instanceof DbalObjectSourceException === false) { + throw $exception; + } + + $this->logger->warning( + sprintf( + '[ObjectSourceErrorMiddleware] external database read failed (%d) in %s::%s: %s', + $exception->getStatusCode(), + $controller::class, + $methodName, + $exception->getMessage() + ), + [ + 'file' => __FILE__, + 'line' => __LINE__, + // The underlying DBAL error (never sent to the client) — without + // it the server log carries only the sanitized message and the + // actual SQL failure is undiagnosable. + 'exception' => $exception, + ] + ); + + return new JSONResponse( + data: ['error' => $exception->getMessage()], + statusCode: $exception->getStatusCode() + ); + }//end afterException() +}//end class diff --git a/lib/Middleware/PublicApiCorsMiddleware.php b/lib/Middleware/PublicApiCorsMiddleware.php new file mode 100644 index 0000000000..3e418cd0e3 --- /dev/null +++ b/lib/Middleware/PublicApiCorsMiddleware.php @@ -0,0 +1,107 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Middleware; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\AppFramework\Utility\IControllerMethodReflector; +use OCP\IRequest; +use Throwable; + +/** + * Middleware that adds reflect-Origin CORS headers to public-page responses. + * + * @package OCA\OpenRegister\Middleware + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Middleware must reference NC framework types + * (Middleware, Controller, Response, IRequest, IControllerMethodReflector) plus Throwable. + */ +class PublicApiCorsMiddleware extends Middleware +{ + /** + * Constructor. + * + * @param IRequest $request The current request + * @param IControllerMethodReflector $reflector Reflector for controller annotations + */ + public function __construct( + private readonly IRequest $request, + private readonly IControllerMethodReflector $reflector + ) { + }//end __construct() + + /** + * Reflect the Origin on responses from public endpoints. + * + * Fails open: any error leaves the response unchanged (no CORS header) so a + * bug here can never turn a working same-origin call into a 500. + * + * @param Controller $controller The controller being dispatched + * @param string $methodName The method being dispatched + * @param Response $response The outgoing response + * + * @return Response The response, with reflect-Origin CORS headers when public + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) IControllerMethodReflector::reflect() consumes + * both $controller and $methodName via the interface; PHPMD misclassifies $controller as unused. + */ + public function afterController(Controller $controller, string $methodName, Response $response): Response + { + try { + $origin = $this->request->getHeader('Origin'); + if ($origin === '') { + return $response; + } + + $this->reflector->reflect($controller, $methodName); + if ($this->reflector->hasAnnotation('PublicPage') === false) { + return $response; + } + + // Never combine a reflected Origin with credentials — that pairing is + // the CSRF-read footgun the platform guards against elsewhere. + $response->addHeader('Access-Control-Allow-Origin', $origin); + $response->addHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + $response->addHeader('Access-Control-Allow-Headers', 'Content-Type'); + } catch (Throwable $e) { + // Fail open: leave the response untouched. + return $response; + }//end try + + return $response; + }//end afterController() +}//end class diff --git a/lib/Middleware/RateLimitMiddleware.php b/lib/Middleware/RateLimitMiddleware.php index 23e710861c..96afa47f4d 100644 --- a/lib/Middleware/RateLimitMiddleware.php +++ b/lib/Middleware/RateLimitMiddleware.php @@ -32,7 +32,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ declare(strict_types=1); @@ -57,7 +57,10 @@ * * @package OCA\OpenRegister\Middleware * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Middleware must reference NC framework types (Middleware, Controller, Response, JSONResponse, Http, IRequest, IUserSession, IControllerMethodReflector) plus SecurityService, LoggerInterface, ReflectionClass, and Throwable — every type is required by the framework contract or the rate-limiting logic. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Middleware must reference NC framework types + * (Middleware, Controller, Response, JSONResponse, Http, IRequest, IUserSession, + * IControllerMethodReflector) plus SecurityService, LoggerInterface, ReflectionClass, and + * Throwable — every type is required by the framework contract or the rate-limiting logic. */ class RateLimitMiddleware extends Middleware { @@ -92,10 +95,13 @@ public function __construct( * * @throws AuthRateLimitExceededException When the identity+IP is locked out * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) NC Middleware::beforeController() requires the ($controller, $methodName) signature; $controller is forwarded to isPublicPage() but $methodName is also needed there — PHPMD misclassifies the forwarded params as unused. - * @SuppressWarnings(PHPMD.ShortVariable) $ip is the established abbreviation for IP address in network-security code; renaming to $ipAddress would conflict with the SecurityService parameter name. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) NC Middleware::beforeController() requires the + * ($controller, $methodName) signature; $controller is forwarded to isPublicPage() but $methodName is + * also needed there — PHPMD misclassifies the forwarded params as unused. + * @SuppressWarnings(PHPMD.ShortVariable) $ip is the established abbreviation for IP address in + * network-security code; renaming to $ipAddress would conflict with the SecurityService parameter name. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function beforeController(Controller $controller, string $methodName): void { @@ -138,10 +144,13 @@ public function beforeController(Controller $controller, string $methodName): vo * * @return Response The unmodified response * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) NC Middleware::afterController() mandates ($controller, $methodName, $response) — $controller and $methodName are not used in this method's body (only $response and the session are needed), but the framework dispatch requires all three. - * @SuppressWarnings(PHPMD.ShortVariable) $ip is the established abbreviation for IP address; renaming disagrees with SecurityService's own parameter naming. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) NC Middleware::afterController() mandates + * ($controller, $methodName, $response) — $controller and $methodName are not used in this method's + * body (only $response and the session are needed), but the framework dispatch requires all three. + * @SuppressWarnings(PHPMD.ShortVariable) $ip is the established abbreviation for IP address; + * renaming disagrees with SecurityService's own parameter naming. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function afterController(Controller $controller, string $methodName, Response $response): Response { @@ -184,10 +193,13 @@ public function afterController(Controller $controller, string $methodName, Resp * * @throws \Exception The original exception is re-thrown for all non-lockout cases * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) NC Middleware::afterException() requires ($controller, $methodName, $exception) — $controller is forwarded to isPublicPage() but PHPMD flags it; $methodName is also required by isPublicPage(). - * @SuppressWarnings(PHPMD.ShortVariable) $ip is the established abbreviation for IP address; renaming disagrees with SecurityService's own parameter naming. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) NC Middleware::afterException() requires + * ($controller, $methodName, $exception) — $controller is forwarded to isPublicPage() but PHPMD + * flags it; $methodName is also required by isPublicPage(). + * @SuppressWarnings(PHPMD.ShortVariable) $ip is the established abbreviation for IP address; + * renaming disagrees with SecurityService's own parameter naming. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function afterException(Controller $controller, string $methodName, \Exception $exception): ?Response { @@ -237,7 +249,9 @@ public function afterException(Controller $controller, string $methodName, \Exce * * @return bool True when the endpoint is public (skip rate-limiting) * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) IControllerMethodReflector::reflect() requires both the controller instance and method name; PHPMD misclassifies $controller as unused because it is consumed via the interface method, not directly in the body. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) IControllerMethodReflector::reflect() requires both + * the controller instance and method name; PHPMD misclassifies $controller as unused because it is + * consumed via the interface method, not directly in the body. */ private function isPublicPage(Controller $controller, string $methodName): bool { diff --git a/lib/Middleware/TenantQuotaExceededException.php b/lib/Middleware/TenantQuotaExceededException.php index 68712619d6..fb406e4124 100644 --- a/lib/Middleware/TenantQuotaExceededException.php +++ b/lib/Middleware/TenantQuotaExceededException.php @@ -39,7 +39,7 @@ class TenantQuotaExceededException extends Exception * @param string $resetAt ISO 8601 timestamp when quota resets * @param int $retryAfter Seconds until quota reset * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 + * @spec openspec/specs/tenant-quotas/spec.md */ public function __construct( string $message, @@ -55,7 +55,7 @@ public function __construct( * * @return int The quota * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 + * @spec openspec/specs/tenant-quotas/spec.md */ public function getQuota(): int { @@ -67,7 +67,7 @@ public function getQuota(): int * * @return string ISO 8601 timestamp * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 + * @spec openspec/specs/tenant-quotas/spec.md */ public function getResetAt(): string { @@ -79,7 +79,7 @@ public function getResetAt(): string * * @return int Seconds until reset * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 + * @spec openspec/specs/tenant-quotas/spec.md */ public function getRetryAfter(): int { diff --git a/lib/Middleware/TenantQuotaMiddleware.php b/lib/Middleware/TenantQuotaMiddleware.php index b1bfe3b7a3..26395dc18c 100644 --- a/lib/Middleware/TenantQuotaMiddleware.php +++ b/lib/Middleware/TenantQuotaMiddleware.php @@ -19,9 +19,9 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-75 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-79 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-78 + * @spec openspec/specs/tenant-quotas/spec.md#requirement-request-quota-must-be-enforced-via-middleware-before-controller-execution + * @spec openspec/specs/tenant-quotas/spec.md + * @spec openspec/specs/tenant-quotas/spec.md */ declare(strict_types=1); @@ -96,8 +96,8 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-75 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-79 + * @spec openspec/specs/tenant-quotas/spec.md#requirement-request-quota-must-be-enforced-via-middleware-before-controller-execution + * @spec openspec/specs/tenant-quotas/spec.md */ public function beforeController(string|Controller $controller, string $methodName): void { @@ -167,8 +167,8 @@ public function beforeController(string|Controller $controller, string $methodNa * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.UndefinedVariable) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-75 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-78 + * @spec openspec/specs/tenant-quotas/spec.md#requirement-request-quota-must-be-enforced-via-middleware-before-controller-execution + * @spec openspec/specs/tenant-quotas/spec.md */ public function afterController(string|Controller $controller, string $methodName, Response $response): Response { @@ -227,7 +227,7 @@ public function afterController(string|Controller $controller, string $methodNam * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-4 + * @spec openspec/specs/tenant-quotas/spec.md */ public function afterException(string|Controller $controller, string $methodName, \Exception $exception): ?Response { @@ -270,7 +270,7 @@ public function afterException(string|Controller $controller, string $methodName * * @SuppressWarnings(PHPMD.UndefinedVariable) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-75 + * @spec openspec/specs/tenant-quotas/spec.md#requirement-request-quota-must-be-enforced-via-middleware-before-controller-execution */ private function checkRequestQuota(object $organisation): void { diff --git a/lib/Middleware/TenantStatusException.php b/lib/Middleware/TenantStatusException.php index b0f4c9c2ff..4fe8482b6c 100644 --- a/lib/Middleware/TenantStatusException.php +++ b/lib/Middleware/TenantStatusException.php @@ -38,7 +38,7 @@ class TenantStatusException extends Exception * @param string $status The organisation status * @param int $code HTTP status code * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 + * @spec openspec/specs/tenant-quotas/spec.md */ public function __construct( string $message, @@ -53,7 +53,7 @@ public function __construct( * * @return string The status * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-17 + * @spec openspec/specs/tenant-quotas/spec.md */ public function getStatus(): string { diff --git a/lib/Migration/NAMING.md b/lib/Migration/NAMING.md new file mode 100644 index 0000000000..34f2ab6137 --- /dev/null +++ b/lib/Migration/NAMING.md @@ -0,0 +1,32 @@ +# Migration naming convention (OPS-12) + +OpenRegister migrations MUST be named: + +``` +Version1Date.php -> class Version1Date +``` + +i.e. the literal prefix `Version1Date` followed by a 14-digit UTC timestamp. New +migrations should always follow this scheme. + +## Known inconsistency — do NOT "fix" by renaming + +Four historical migrations use a different prefix scheme: + +- `Version002003000Date20251013000000.php` +- `Version002004000Date20251013000000.php` +- `Version002005000Date20251013000000.php` +- `Version002006000Date20251013000000.php` + +Nextcloud orders migrations with `version_compare()` on the class name. Under +`version_compare`, every `Version002...` name sorts **after** every `Version1...` +name (because `"002..." ` is compared as a higher major than `"1"`). On existing +installs this ordering is already baked in, and Nextcloud records the *applied +class name* in `oc_migrations`. + +Renaming these four classes on an existing install would make Nextcloud believe +the renamed migrations have never run and attempt to re-apply them — a data +hazard. **These files are therefore intentionally left as-is.** This is a +documentation-only note (per OPS-12 in `CODE-REVIEW-IMPROVEMENT-PLAN.md`): record +the inconsistency, standardise *going forward*, and never rename already-applied +migrations. diff --git a/lib/Migration/Version1Date20260307000000.php b/lib/Migration/Version1Date20260307000000.php index 7a8fbba2ac..32159ac297 100644 --- a/lib/Migration/Version1Date20260307000000.php +++ b/lib/Migration/Version1Date20260307000000.php @@ -6,9 +6,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260308000000.php b/lib/Migration/Version1Date20260308000000.php index c1e82e6670..4185624a12 100644 --- a/lib/Migration/Version1Date20260308000000.php +++ b/lib/Migration/Version1Date20260308000000.php @@ -6,9 +6,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260308120000.php b/lib/Migration/Version1Date20260308120000.php index d6f4780612..1325bc15f6 100644 --- a/lib/Migration/Version1Date20260308120000.php +++ b/lib/Migration/Version1Date20260308120000.php @@ -6,9 +6,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260313120000.php b/lib/Migration/Version1Date20260313120000.php index 00daa93d1a..83210a63b9 100644 --- a/lib/Migration/Version1Date20260313120000.php +++ b/lib/Migration/Version1Date20260313120000.php @@ -10,9 +10,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260313130000.php b/lib/Migration/Version1Date20260313130000.php index 00dad3a5bc..97cceab9a5 100644 --- a/lib/Migration/Version1Date20260313130000.php +++ b/lib/Migration/Version1Date20260313130000.php @@ -10,9 +10,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260318120000.php b/lib/Migration/Version1Date20260318120000.php index 1893b7bc8a..720d18acf9 100644 --- a/lib/Migration/Version1Date20260318120000.php +++ b/lib/Migration/Version1Date20260318120000.php @@ -9,9 +9,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260322000000.php b/lib/Migration/Version1Date20260322000000.php index 8150561d08..ef8d69ae7a 100644 --- a/lib/Migration/Version1Date20260322000000.php +++ b/lib/Migration/Version1Date20260322000000.php @@ -10,9 +10,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260322100000.php b/lib/Migration/Version1Date20260322100000.php index b670fb17ce..fe13ce40d4 100644 --- a/lib/Migration/Version1Date20260322100000.php +++ b/lib/Migration/Version1Date20260322100000.php @@ -9,9 +9,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260322120000.php b/lib/Migration/Version1Date20260322120000.php index d4438def72..1237041bb6 100644 --- a/lib/Migration/Version1Date20260322120000.php +++ b/lib/Migration/Version1Date20260322120000.php @@ -13,9 +13,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260325000000.php b/lib/Migration/Version1Date20260325000000.php index 617273e7c7..7d08a5293b 100644 --- a/lib/Migration/Version1Date20260325000000.php +++ b/lib/Migration/Version1Date20260325000000.php @@ -9,9 +9,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260325120000.php b/lib/Migration/Version1Date20260325120000.php index d240aa2902..b0cb0c2488 100644 --- a/lib/Migration/Version1Date20260325120000.php +++ b/lib/Migration/Version1Date20260325120000.php @@ -9,9 +9,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260326000000.php b/lib/Migration/Version1Date20260326000000.php index 26e8d3d3ef..391222264b 100644 --- a/lib/Migration/Version1Date20260326000000.php +++ b/lib/Migration/Version1Date20260326000000.php @@ -15,9 +15,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260430000000.php b/lib/Migration/Version1Date20260430000000.php deleted file mode 100644 index 69bd0d94bd..0000000000 --- a/lib/Migration/Version1Date20260430000000.php +++ /dev/null @@ -1,167 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Migration; - -use Closure; -use OCP\DB\Types; -use OCP\DB\ISchemaWrapper; -use OCP\Migration\IOutput; -use OCP\Migration\SimpleMigrationStep; - -/** - * Creates `openregister_realtime_events`, the append-only event log - * for realtime change notifications. Each row is a CloudEvent-shaped - * record of one register-object change; clients poll with `?since={id}` - * to receive every event newer than their last seen id. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -class Version1Date20260430000000 extends SimpleMigrationStep -{ - /** - * Apply the schema change for this migration. - * - * @param IOutput $output Migration output - * @param Closure $schemaClosure Schema closure - * @param array $options Migration options - * - * @return null|ISchemaWrapper - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper - { - /* - * @var ISchemaWrapper $schema - */ - - $schema = $schemaClosure(); - - if ($schema->hasTable('openregister_realtime_events') === true) { - return $schema; - } - - $table = $schema->createTable('openregister_realtime_events'); - - $table->addColumn( - 'id', - Types::BIGINT, - [ - 'autoincrement' => true, - 'notnull' => true, - ] - ); - $table->setPrimaryKey(['id']); - - // CloudEvent-style envelope fields. - $table->addColumn( - 'event_type', - Types::STRING, - [ - 'notnull' => true, - 'length' => 64, - ] - ); - $table->addColumn( - 'source', - Types::STRING, - [ - 'notnull' => true, - 'length' => 255, - ] - ); - $table->addColumn( - 'subject', - Types::STRING, - [ - 'notnull' => false, - 'length' => 255, - ] - ); - - // OpenRegister identity (denormalised for filtering). - $table->addColumn( - 'register_id', - Types::STRING, - [ - 'notnull' => false, - 'length' => 255, - ] - ); - $table->addColumn( - 'schema_id', - Types::STRING, - [ - 'notnull' => false, - 'length' => 255, - ] - ); - $table->addColumn( - 'object_uuid', - Types::STRING, - [ - 'notnull' => false, - 'length' => 64, - ] - ); - $table->addColumn( - 'actor_uid', - Types::STRING, - [ - 'notnull' => false, - 'length' => 64, - ] - ); - $table->addColumn( - 'organisation', - Types::STRING, - [ - 'notnull' => false, - 'length' => 64, - ] - ); - - // Full CloudEvent payload as JSON. - $table->addColumn( - 'payload', - Types::TEXT, - [ - 'notnull' => true, - ] - ); - - $table->addColumn( - 'created', - Types::DATETIME, - [ - 'notnull' => true, - ] - ); - - // Indices for the cursor-based polling query (`WHERE id > :since`) - // and per-object / per-schema filters. - $table->addIndex(['register_id', 'schema_id', 'id'], 'idx_realtime_register_schema_id'); - $table->addIndex(['object_uuid', 'id'], 'idx_realtime_object_id'); - $table->addIndex(['organisation', 'id'], 'idx_realtime_org_id'); - - return $schema; - }//end changeSchema() -}//end class diff --git a/lib/Migration/Version1Date20260430160000.php b/lib/Migration/Version1Date20260430160000.php index 3ae566f67d..6623ac2f7c 100644 --- a/lib/Migration/Version1Date20260430160000.php +++ b/lib/Migration/Version1Date20260430160000.php @@ -20,9 +20,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260430180000.php b/lib/Migration/Version1Date20260430180000.php index 801ab88247..2d1728b94a 100644 --- a/lib/Migration/Version1Date20260430180000.php +++ b/lib/Migration/Version1Date20260430180000.php @@ -22,9 +22,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260501100000.php b/lib/Migration/Version1Date20260501100000.php index d680fc75a6..519baab98e 100644 --- a/lib/Migration/Version1Date20260501100000.php +++ b/lib/Migration/Version1Date20260501100000.php @@ -38,9 +38,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260502120000.php b/lib/Migration/Version1Date20260502120000.php index ddb5779d10..defdb41984 100644 --- a/lib/Migration/Version1Date20260502120000.php +++ b/lib/Migration/Version1Date20260502120000.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/data-import-export/tasks.md + * @spec openspec/specs/data-import-export/spec.md */ declare(strict_types=1); diff --git a/lib/Migration/Version1Date20260502190000.php b/lib/Migration/Version1Date20260502190000.php index fa49ed06f6..31a6bff930 100644 --- a/lib/Migration/Version1Date20260502190000.php +++ b/lib/Migration/Version1Date20260502190000.php @@ -18,7 +18,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/notificatie-engine/tasks.md "Read/unread tracking MUST be maintained per user per notification" + * @spec openspec/specs/notificatie-engine/spec.md "Read/unread tracking MUST be maintained per user per notification" */ declare(strict_types=1); diff --git a/lib/Migration/Version1Date20260502200000.php b/lib/Migration/Version1Date20260502200000.php index 0a3ede011d..219dcf251a 100644 --- a/lib/Migration/Version1Date20260502200000.php +++ b/lib/Migration/Version1Date20260502200000.php @@ -20,7 +20,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/notificatie-engine/tasks.md "Users MUST be able to manage their notification preferences" + * @spec openspec/specs/notificatie-engine/spec.md "Users MUST be able to manage their notification preferences" */ declare(strict_types=1); diff --git a/lib/Migration/Version1Date20260511130000.php b/lib/Migration/Version1Date20260511130000.php index 0582fd200d..1d5a7a9cf1 100644 --- a/lib/Migration/Version1Date20260511130000.php +++ b/lib/Migration/Version1Date20260511130000.php @@ -19,7 +19,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#messagecontext-json-column + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); diff --git a/lib/Migration/Version1Date20260512120000.php b/lib/Migration/Version1Date20260512120000.php new file mode 100644 index 0000000000..4bb24e90ea --- /dev/null +++ b/lib/Migration/Version1Date20260512120000.php @@ -0,0 +1,114 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/entity-relation-grondslagen/spec.md "1.1 Add a new migration class" + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Adds the `bases` and `skip_anonymization` decision-metadata columns + * to the entity_relations table. Both columns are nullable-or-defaulted + * so existing rows pick up sensible defaults without backfill. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +class Version1Date20260512120000 extends SimpleMigrationStep +{ + /** + * Add the two decision-metadata columns when missing. + * + * @param IOutput $output Migration output sink. + * @param Closure $schemaClosure Closure returning the ISchemaWrapper. + * @param array $options Migration options (unused). + * + * @return ISchemaWrapper|null + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable(tableName: 'openregister_entity_relations') === false) { + return $schema; + } + + $table = $schema->getTable(tableName: 'openregister_entity_relations'); + + if ($table->hasColumn(name: 'bases') === false) { + $table->addColumn( + name: 'bases', + typeName: Types::JSON, + options: ['notnull' => false] + ); + } + + if ($table->hasColumn(name: 'skip_anonymization') === false) { + $table->addColumn( + name: 'skip_anonymization', + typeName: Types::BOOLEAN, + options: [ + 'notnull' => true, + 'default' => false, + ] + ); + } + + return $schema; + + }//end changeSchema() + + // PostSchemaChange() intentionally NOT overridden: the new columns + // are nullable (`bases`) or carry a DB-level default + // (`skip_anonymization` → false), so existing rows pick up correct + // values automatically and require no backfill pass. Adding an + // empty override would be dead code; SimpleMigrationStep's default + // no-op is what we want. +}//end class diff --git a/lib/Migration/Version1Date20260520120000.php b/lib/Migration/Version1Date20260520120000.php new file mode 100644 index 0000000000..8982bd2bb9 --- /dev/null +++ b/lib/Migration/Version1Date20260520120000.php @@ -0,0 +1,118 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Add `source_language` to `openregister_translations`. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-1 + */ +class Version1Date20260520120000 extends SimpleMigrationStep +{ + /** + * Apply the schema change. + * + * @param IOutput $output Migration output channel. + * @param Closure $schemaClosure Schema closure factory. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null Modified schema wrapper, or null when nothing changed. + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_translations') === false) { + return $schema; + } + + $table = $schema->getTable('openregister_translations'); + if ($table->hasColumn('source_language') === true) { + return $schema; + } + + $table->addColumn( + 'source_language', + Types::STRING, + [ + 'notnull' => true, + 'length' => 16, + 'default' => '', + ] + ); + + if ($table->hasIndex('idx_translations_source_lang') === false) { + $table->addIndex(['source_language'], 'idx_translations_source_lang'); + } + + return $schema; + }//end changeSchema() + + /** + * Run the SQL back-fill that gives every row a non-empty source_language. + * + * Joins through `openregister_objects` to the parent register and uses + * the first element of the register's `languages` array (falling back + * to `'nl'` when the JSON column is empty or null). + * + * Idempotent: only updates rows where `source_language = ''`. The + * companion `openregister:translations:backfill-source-language` + * console command is the entry point preferred for large datasets. + * + * @param IOutput $output Migration output channel. + * @param Closure $schemaClosure Schema closure factory (unused). + * @param array $options Migration options. + * + * @return void + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void + { + // The actual back-fill is intentionally delegated to the + // `openregister:translations:backfill-source-language` console + // command so admins can choose the batch size. We only emit an + // advisory message here. + $output->info( + '[i18n-source-of-truth] openregister_translations.source_language column added. ' + .'Run `occ openregister:translations:backfill-source-language` to populate ' + .'existing rows from their parent register defaults.' + ); + }//end postSchemaChange() +}//end class diff --git a/lib/Migration/Version1Date20260524120000.php b/lib/Migration/Version1Date20260524120000.php index 58c8aa12e2..2da7181a43 100644 --- a/lib/Migration/Version1Date20260524120000.php +++ b/lib/Migration/Version1Date20260524120000.php @@ -12,9 +12,9 @@ * @category Migration * @package OCA\OpenRegister\Migration * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Migration/Version1Date20260603000000.php b/lib/Migration/Version1Date20260603000000.php new file mode 100644 index 0000000000..f235937c78 --- /dev/null +++ b/lib/Migration/Version1Date20260603000000.php @@ -0,0 +1,78 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/changes/entity-relation-grondslagen/tasks.md#task-1.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Adds a nullable JSON `bases` column to openregister_entity_relations. + * + * The column stores an array of UUID-shaped strings referencing legal-basis + * objects in the consuming app's register (e.g. DocuDesk's `base` schema). + * OpenRegister stores the array verbatim; no UUID validation is performed. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +class Version1Date20260603000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + // @var ISchemaWrapper $schema + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_entity_relations') === false) { + return null; + } + + $table = $schema->getTable('openregister_entity_relations'); + + if ($table->hasColumn('bases') === true) { + return null; + } + + $table->addColumn( + name: 'bases', + typeName: Types::JSON, + options: [ + 'notnull' => false, + 'default' => null, + ] + ); + + $output->info('Added bases column to openregister_entity_relations'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260611000000.php b/lib/Migration/Version1Date20260611000000.php new file mode 100644 index 0000000000..0749fa8c59 --- /dev/null +++ b/lib/Migration/Version1Date20260611000000.php @@ -0,0 +1,112 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the openregister_anonymisation_log table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +class Version1Date20260611000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return ISchemaWrapper|null + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_anonymisation_log') === true) { + return null; + } + + $table = $schema->createTable('openregister_anonymisation_log'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('file_id', Types::BIGINT, ['notnull' => true]); + $table->addColumn('object_uuid', Types::STRING, ['notnull' => false, 'length' => 36, 'default' => null]); + $table->addColumn('register_id', Types::BIGINT, ['notnull' => false, 'unsigned' => true, 'default' => null]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => false, 'unsigned' => true, 'default' => null]); + $table->addColumn('mime_type', Types::STRING, ['notnull' => true, 'length' => 191, 'default' => '']); + $table->addColumn('engine', Types::STRING, ['notnull' => true, 'length' => 64, 'default' => '']); + $table->addColumn('status', Types::STRING, ['notnull' => true, 'length' => 32, 'default' => 'success']); + $table->addColumn('reason', Types::STRING, ['notnull' => false, 'length' => 64, 'default' => null]); + $table->addColumn('sanitization', Types::TEXT, ['notnull' => false, 'default' => null]); + $table->addColumn('replacements', Types::INTEGER, ['notnull' => true, 'default' => 0]); + $table->addColumn('duration_ms', Types::INTEGER, ['notnull' => false, 'default' => null]); + $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['file_id'], 'idx_anonlog_file'); + $table->addIndex(['object_uuid'], 'idx_anonlog_object'); + $table->addIndex(['register_id'], 'idx_anonlog_register'); + $table->addIndex(['schema_id'], 'idx_anonlog_schema'); + $table->addIndex(['created'], 'idx_anonlog_created'); + + $output->info('Created openregister_anonymisation_log table'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260612000000.php b/lib/Migration/Version1Date20260612000000.php new file mode 100644 index 0000000000..1a8df2d877 --- /dev/null +++ b/lib/Migration/Version1Date20260612000000.php @@ -0,0 +1,98 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/notificatie-engine/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the openregister_notification_dedupe table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +class Version1Date20260612000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return ISchemaWrapper|null + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_notification_dedupe') === true) { + return null; + } + + $table = $schema->createTable('openregister_notification_dedupe'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('rule_key', Types::STRING, ['notnull' => true, 'length' => 191, 'default' => '']); + $table->addColumn('object_uuid', Types::STRING, ['notnull' => true, 'length' => 36, 'default' => '']); + $table->addColumn('fingerprint', Types::STRING, ['notnull' => true, 'length' => 64, 'default' => '']); + $table->addColumn('dispatched_at', Types::DATETIME, ['notnull' => true]); + $table->addColumn('seen_at', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['schema_id', 'rule_key', 'object_uuid'], 'idx_notifdedup_triple'); + $table->addIndex(['object_uuid'], 'idx_notifdedup_object'); + $table->addIndex(['seen_at'], 'idx_notifdedup_seen'); + + $output->info('Created openregister_notification_dedupe table'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260614000000.php b/lib/Migration/Version1Date20260614000000.php new file mode 100644 index 0000000000..4aec421939 --- /dev/null +++ b/lib/Migration/Version1Date20260614000000.php @@ -0,0 +1,127 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the openregister_processing_log table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ +class Version1Date20260614000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_processing_log') === true) { + return null; + } + + $table = $schema->createTable('openregister_processing_log'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 36]); + $table->addColumn('activity_id', Types::STRING, ['notnull' => true, 'length' => 36]); + $table->addColumn('action', Types::STRING, ['notnull' => true, 'length' => 16, 'default' => 'read']); + $table->addColumn('actor', Types::STRING, ['notnull' => true, 'length' => 191, 'default' => '']); + $table->addColumn('channel', Types::STRING, ['notnull' => true, 'length' => 16, 'default' => 'ui']); + $table->addColumn('register_id', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('schema_id', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('object_uuid', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('subject_id_type', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('subject_id_value', Types::STRING, ['notnull' => false, 'length' => 191]); + $table->addColumn('object_count', Types::INTEGER, ['notnull' => true, 'default' => 1, 'unsigned' => true]); + $table->addColumn('confidential', Types::BOOLEAN, ['notnull' => true, 'default' => false]); + $table->addColumn('organisation_id', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['uuid'], 'idx_proclog_uuid'); + $table->addIndex(['subject_id_type', 'subject_id_value'], 'idx_proclog_subject'); + $table->addIndex(['register_id', 'activity_id', 'created'], 'idx_proclog_slice'); + $table->addIndex(['created'], 'idx_proclog_created'); + $table->addIndex(['actor'], 'idx_proclog_actor'); + + $output->info('Created openregister_processing_log table'); + + return $schema; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260614100000.php b/lib/Migration/Version1Date20260614100000.php new file mode 100644 index 0000000000..351189795d --- /dev/null +++ b/lib/Migration/Version1Date20260614100000.php @@ -0,0 +1,173 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Add sync-pipeline columns to openregister_sources. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +class Version1Date20260614100000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_sources') === false) { + return null; + } + + $table = $schema->getTable('openregister_sources'); + $changed = false; + + if ($table->hasColumn('sync_enabled') === false) { + $table->addColumn('sync_enabled', Types::BOOLEAN, ['notnull' => true, 'default' => false]); + $changed = true; + } + + if ($table->hasColumn('sync_schedule') === false) { + $table->addColumn('sync_schedule', Types::STRING, ['notnull' => false, 'length' => 64]); + $changed = true; + } + + if ($table->hasColumn('sync_interval') === false) { + $table->addColumn('sync_interval', Types::INTEGER, ['notnull' => false, 'unsigned' => true]); + $changed = true; + } + + if ($table->hasColumn('last_sync_date') === false) { + $table->addColumn('last_sync_date', Types::DATETIME, ['notnull' => false]); + $changed = true; + } + + if ($table->hasColumn('last_sync_status') === false) { + $table->addColumn('last_sync_status', Types::STRING, ['notnull' => false, 'length' => 16]); + $changed = true; + } + + if ($table->hasColumn('last_sync_token') === false) { + $table->addColumn('last_sync_token', Types::STRING, ['notnull' => false, 'length' => 255]); + $changed = true; + } + + if ($table->hasColumn('auth_type') === false) { + $table->addColumn('auth_type', Types::STRING, ['notnull' => false, 'length' => 32]); + $changed = true; + } + + if ($table->hasColumn('auth_config') === false) { + $table->addColumn('auth_config', Types::TEXT, ['notnull' => false]); + $changed = true; + } + + if ($table->hasColumn('mapping_id') === false) { + $table->addColumn('mapping_id', Types::INTEGER, ['notnull' => false, 'unsigned' => true]); + $changed = true; + } + + if ($table->hasColumn('target_register') === false) { + $table->addColumn('target_register', Types::STRING, ['notnull' => false, 'length' => 255]); + $changed = true; + } + + if ($table->hasColumn('target_schema') === false) { + $table->addColumn('target_schema', Types::STRING, ['notnull' => false, 'length' => 255]); + $changed = true; + } + + if ($table->hasColumn('conflict_strategy') === false) { + $table->addColumn('conflict_strategy', Types::STRING, ['notnull' => false, 'length' => 16]); + $changed = true; + } + + if ($table->hasColumn('delete_strategy') === false) { + $table->addColumn('delete_strategy', Types::STRING, ['notnull' => false, 'length' => 16]); + $changed = true; + } + + if ($table->hasColumn('batch_size') === false) { + $table->addColumn('batch_size', Types::INTEGER, ['notnull' => false, 'unsigned' => true]); + $changed = true; + } + + if ($table->hasIndex('sources_sync_enabled_idx') === false) { + $table->addIndex(['sync_enabled'], 'sources_sync_enabled_idx'); + $changed = true; + } + + if ($changed === true) { + $output->info('Added data-sync columns to openregister_sources table'); + return $schema; + } + + return null; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260614110000.php b/lib/Migration/Version1Date20260614110000.php new file mode 100644 index 0000000000..fc86ecfc71 --- /dev/null +++ b/lib/Migration/Version1Date20260614110000.php @@ -0,0 +1,113 @@ + fetch -> import + * stages with a status, content hash (change detection), raw fetched data, + * error detail and attempt count (retry/backoff). Enables resume-after- + * failure and detailed execution reporting per the data-sync-harvesting spec. + * + * - id (bigint, PK, autoincrement) + * - uuid (string 36, not null) + * - source_id (bigint, not null) — owning source + * - execution_id (string 36, not null) — groups one sync run + * - external_id (string 255) — source record identifier + * - status (string 32, not null, default 'pending') + * - object_uuid (string 36) — local object created/updated + * - content_hash (string 64) — change detection + * - raw_data (text/json) — raw fetched payload + * - error_message (text) — failure detail + * - attempts (int, default 0) — retry counter + * - organisation (string 36) — tenant slice + * - created / updated (datetime, not null) + * + * Idempotent: creates the table only when absent. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Migration + * @package OCA\OpenRegister\Migration + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the openregister_sync_records table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +class Version1Date20260614110000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_sync_records') === true) { + return null; + } + + $table = $schema->createTable('openregister_sync_records'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 36]); + $table->addColumn('source_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('execution_id', Types::STRING, ['notnull' => true, 'length' => 36]); + $table->addColumn('external_id', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('status', Types::STRING, ['notnull' => true, 'length' => 32, 'default' => 'pending']); + $table->addColumn('object_uuid', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('content_hash', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('raw_data', Types::TEXT, ['notnull' => false]); + $table->addColumn('error_message', Types::TEXT, ['notnull' => false]); + $table->addColumn('attempts', Types::INTEGER, ['notnull' => true, 'default' => 0, 'unsigned' => true]); + $table->addColumn('organisation', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['uuid'], 'idx_syncrec_uuid'); + $table->addIndex(['execution_id'], 'idx_syncrec_exec'); + $table->addIndex(['source_id', 'status'], 'idx_syncrec_src_status'); + $table->addIndex(['source_id', 'external_id'], 'idx_syncrec_src_ext'); + + $output->info('Created openregister_sync_records table'); + + return $schema; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260614120000.php b/lib/Migration/Version1Date20260614120000.php new file mode 100644 index 0000000000..e105f13270 --- /dev/null +++ b/lib/Migration/Version1Date20260614120000.php @@ -0,0 +1,138 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/schema-migration/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the schema changelog and run tables. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/schema-migration/spec.md + */ +class Version1Date20260614120000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_schema_changelog') === false) { + $table = $schema->createTable('openregister_schema_changelog'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('version', Types::STRING, ['notnull' => false, 'length' => 32]); + $table->addColumn('classification', Types::STRING, ['notnull' => false, 'length' => 16]); + $table->addColumn('changes', Types::TEXT, ['notnull' => false]); + $table->addColumn('actor', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('acknowledged_by', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('acknowledged_at', Types::DATETIME, ['notnull' => false]); + $table->addColumn('created', Types::DATETIME, ['notnull' => false]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['schema_id'], 'idx_or_schchg_schema'); + + $output->info('Created openregister_schema_changelog table'); + }//end if + + if ($schema->hasTable('openregister_schema_runs') === false) { + $table = $schema->createTable('openregister_schema_runs'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('uuid', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('register_id', Types::BIGINT, ['notnull' => false, 'unsigned' => true]); + $table->addColumn('type', Types::STRING, ['notnull' => true, 'length' => 16]); + $table->addColumn('state', Types::STRING, ['notnull' => true, 'length' => 16, 'default' => 'draft']); + $table->addColumn('proposed_definition', Types::TEXT, ['notnull' => false]); + $table->addColumn('plan', Types::TEXT, ['notnull' => false]); + $table->addColumn('options', Types::TEXT, ['notnull' => false]); + $table->addColumn('processed', Types::INTEGER, ['notnull' => true, 'default' => 0, 'unsigned' => true]); + $table->addColumn('total', Types::INTEGER, ['notnull' => true, 'default' => 0, 'unsigned' => true]); + $table->addColumn('cursor', Types::BIGINT, ['notnull' => true, 'default' => 0, 'unsigned' => true]); + $table->addColumn('report', Types::TEXT, ['notnull' => false]); + $table->addColumn('started_by', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('rolled_back_from', Types::BIGINT, ['notnull' => false, 'unsigned' => true]); + $table->addColumn('created', Types::DATETIME, ['notnull' => false]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => false]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['uuid'], 'idx_or_schrun_uuid'); + $table->addIndex(['schema_id', 'state'], 'idx_or_schrun_sch_state'); + + $output->info('Created openregister_schema_runs table'); + }//end if + + if ($schema->hasTable('openregister_schema_run_entries') === false) { + $table = $schema->createTable('openregister_schema_run_entries'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('run_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('object_uuid', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('outcome', Types::STRING, ['notnull' => false, 'length' => 16]); + $table->addColumn('message', Types::TEXT, ['notnull' => false]); + $table->addColumn('pre_version', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('post_version', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('pre_data', Types::TEXT, ['notnull' => false]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['run_id'], 'idx_or_schrunent_run'); + $table->addIndex(['run_id', 'outcome'], 'idx_or_schrunent_outcome'); + + $output->info('Created openregister_schema_run_entries table'); + }//end if + + return $schema; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260615000000.php b/lib/Migration/Version1Date20260615000000.php new file mode 100644 index 0000000000..f747a7f2ea --- /dev/null +++ b/lib/Migration/Version1Date20260615000000.php @@ -0,0 +1,119 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the case-token and analytics-series tables. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ +class Version1Date20260615000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_case_tokens') === false) { + $table = $schema->createTable('openregister_case_tokens'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('token', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addColumn('object_uuid', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addColumn('register_id', Types::BIGINT, ['notnull' => false, 'unsigned' => true]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => false, 'unsigned' => true]); + $table->addColumn('label', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('created_by', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('created_at', Types::DATETIME, ['notnull' => false]); + $table->addColumn('expires_at', Types::DATETIME, ['notnull' => false]); + $table->addColumn('revoked_at', Types::DATETIME, ['notnull' => false]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['token'], 'idx_or_casetok_token'); + $table->addIndex(['object_uuid'], 'idx_or_casetok_object'); + + $output->info('Created openregister_case_tokens table'); + }//end if + + if ($schema->hasTable('openregister_analytics_series') === false) { + $table = $schema->createTable('openregister_analytics_series'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('series_key', Types::STRING, ['notnull' => true, 'length' => 128]); + $table->addColumn('register_id', Types::BIGINT, ['notnull' => false, 'unsigned' => true]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => false, 'unsigned' => true]); + $table->addColumn('title', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('chart_type', Types::STRING, ['notnull' => true, 'length' => 32, 'default' => 'line']); + $table->addColumn('labels', Types::TEXT, ['notnull' => false]); + $table->addColumn('datasets', Types::TEXT, ['notnull' => false]); + $table->addColumn('visibility', Types::STRING, ['notnull' => true, 'length' => 16, 'default' => 'private']); + $table->addColumn('created_by', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('created_at', Types::DATETIME, ['notnull' => false]); + $table->addColumn('updated_at', Types::DATETIME, ['notnull' => false]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['series_key'], 'idx_or_anaser_key'); + $table->addIndex(['register_id', 'schema_id'], 'idx_or_anaser_scope'); + + $output->info('Created openregister_analytics_series table'); + }//end if + + return $schema; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260615130000.php b/lib/Migration/Version1Date20260615130000.php new file mode 100644 index 0000000000..92357a6875 --- /dev/null +++ b/lib/Migration/Version1Date20260615130000.php @@ -0,0 +1,102 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the openregister_push_subscriptions table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ +class Version1Date20260615130000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_push_subscriptions') === true) { + return null; + } + + $table = $schema->createTable('openregister_push_subscriptions'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('user_id', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addColumn('endpoint', Types::STRING, ['notnull' => true, 'length' => 1024]); + $table->addColumn('p256dh', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('auth', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('user_agent', Types::STRING, ['notnull' => false, 'length' => 512]); + $table->addColumn('created_at', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['user_id'], 'idx_or_push_sub_user'); + $table->addIndex(['endpoint'], 'idx_or_push_sub_endpoint'); + + $output->info('Created openregister_push_subscriptions table'); + + return $schema; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260624000000.php b/lib/Migration/Version1Date20260624000000.php new file mode 100644 index 0000000000..0147a55d7b --- /dev/null +++ b/lib/Migration/Version1Date20260624000000.php @@ -0,0 +1,101 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Drops the deprecated SOLR index column and Register/Schema publication columns. + * + * The migration is idempotent: each column/index is only dropped when present, + * so re-running it on an already-migrated database is a no-op. + * + * @package OCA\OpenRegister\Migration + */ +class Version1Date20260624000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Migration output + * @param Closure $schemaClosure Schema closure returning an ISchemaWrapper + * @param array $options Migration options + * + * @return ISchemaWrapper|null The updated schema, or null if no changes + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + $schema = $schemaClosure(); + $changed = false; + + // 1. Drop the SOLR index tracking column + index from the file_texts table. + if ($schema->hasTable('openregister_file_texts') === true) { + $fileTexts = $schema->getTable('openregister_file_texts'); + + if ($fileTexts->hasIndex('file_texts_solr_idx') === true) { + $fileTexts->dropIndex('file_texts_solr_idx'); + $output->info(' ✓ Dropped file_texts_solr_idx index'); + $changed = true; + } + + if ($fileTexts->hasColumn('indexed_in_solr') === true) { + $fileTexts->dropColumn('indexed_in_solr'); + $output->info(' ✓ Dropped indexed_in_solr column from file_texts table'); + $changed = true; + } + } + + // 2. Drop the publication columns from the registers and schemas tables. + foreach (['openregister_registers', 'openregister_schemas'] as $tableName) { + if ($schema->hasTable($tableName) === false) { + continue; + } + + $table = $schema->getTable($tableName); + + foreach (['published', 'depublished'] as $columnName) { + if ($table->hasColumn($columnName) === true) { + $table->dropColumn($columnName); + $output->info(' ✓ Dropped '.$columnName.' column from '.$tableName.' table'); + $changed = true; + } + } + } + + if ($changed === false) { + return null; + } + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260625000000.php b/lib/Migration/Version1Date20260625000000.php new file mode 100644 index 0000000000..82bdebf4fa --- /dev/null +++ b/lib/Migration/Version1Date20260625000000.php @@ -0,0 +1,78 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the `openregister_sequences` table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +class Version1Date20260625000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null The updated schema, or null when no change. + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_sequences') === true) { + return null; + } + + $table = $schema->createTable('openregister_sequences'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('register_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('scope_key', Types::STRING, ['notnull' => true, 'length' => 64, 'default' => '']); + $table->addColumn('next_value', Types::BIGINT, ['notnull' => true, 'unsigned' => true, 'default' => 1]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['register_id', 'schema_id', 'scope_key'], 'idx_or_seq_scope'); + + $output->info('Created openregister_sequences table'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260704120000.php b/lib/Migration/Version1Date20260704120000.php new file mode 100644 index 0000000000..0c929560c3 --- /dev/null +++ b/lib/Migration/Version1Date20260704120000.php @@ -0,0 +1,89 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Adds the missing nullable `owner` column to the conversations table. + * + * The migration is idempotent: the column is only added when absent, so + * re-running it on an already-migrated database (or one where the column was + * added manually) is a no-op. + * + * @package OCA\OpenRegister\Migration + */ +class Version1Date20260704120000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Migration output + * @param Closure $schemaClosure Schema closure returning an ISchemaWrapper + * @param array $options Migration options + * + * @return ISchemaWrapper|null The updated schema, or null if no changes + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_conversations') === false) { + return null; + } + + $table = $schema->getTable('openregister_conversations'); + + if ($table->hasColumn('owner') === true) { + return null; + } + + // Nullable string matching Conversation::$owner (?string). A Nextcloud + // user id is at most 64 characters. + $table->addColumn( + 'owner', + Types::STRING, + [ + 'notnull' => false, + 'length' => 64, + 'default' => null, + ] + ); + $output->info(' ✓ Added owner column to openregister_conversations table'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260706000000.php b/lib/Migration/Version1Date20260706000000.php new file mode 100644 index 0000000000..6ba1851b90 --- /dev/null +++ b/lib/Migration/Version1Date20260706000000.php @@ -0,0 +1,153 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Creates the `openregister_handoff_queue` table (idempotent). + * + * @package OCA\OpenRegister\Migration + */ +class Version1Date20260706000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Migration output. + * @param Closure $schemaClosure Schema closure returning an ISchemaWrapper. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null The updated schema, or null if no changes. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_handoff_queue') === true) { + return null; + } + + $table = $schema->createTable('openregister_handoff_queue'); + + $table->addColumn( + 'id', + Types::BIGINT, + ['autoincrement' => true, 'notnull' => true, 'length' => 20] + ); + $table->addColumn( + 'source_object_uuid', + Types::STRING, + ['notnull' => true, 'length' => 255] + ); + $table->addColumn( + 'source_register', + Types::INTEGER, + ['notnull' => true, 'default' => 0] + ); + $table->addColumn( + 'source_schema', + Types::INTEGER, + ['notnull' => true, 'default' => 0] + ); + $table->addColumn( + 'handoff_id', + Types::STRING, + ['notnull' => true, 'length' => 128] + ); + $table->addColumn( + 'target_kind', + Types::STRING, + ['notnull' => true, 'length' => 255] + ); + $table->addColumn( + 'requesting_user', + Types::STRING, + ['notnull' => true, 'length' => 64] + ); + $table->addColumn( + 'correlation_id', + Types::STRING, + ['notnull' => true, 'length' => 64] + ); + $table->addColumn( + 'mapping_hash', + Types::STRING, + ['notnull' => false, 'length' => 64, 'default' => null] + ); + $table->addColumn( + 'status', + Types::STRING, + ['notnull' => true, 'length' => 32, 'default' => 'parked'] + ); + $table->addColumn( + 'attempt', + Types::INTEGER, + ['notnull' => true, 'default' => 0] + ); + $table->addColumn( + 'last_error', + Types::TEXT, + ['notnull' => false, 'default' => null] + ); + $table->addColumn( + 'created', + Types::DATETIME, + ['notnull' => true] + ); + $table->addColumn( + 'last_attempt_at', + Types::DATETIME, + ['notnull' => false, 'default' => null] + ); + $table->addColumn( + 'executed_at', + Types::DATETIME, + ['notnull' => false, 'default' => null] + ); + + $table->setPrimaryKey(['id']); + $table->addIndex(['status', 'target_kind'], 'or_handoffq_status_kind'); + $table->addIndex(['source_object_uuid'], 'or_handoffq_src_uuid'); + + $output->info(' ✓ Created openregister_handoff_queue table'); + + return $schema; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260706100000.php b/lib/Migration/Version1Date20260706100000.php new file mode 100644 index 0000000000..f9cfe04389 --- /dev/null +++ b/lib/Migration/Version1Date20260706100000.php @@ -0,0 +1,325 @@ +/`) never introspects: one row per vector (vector_id -> embedding + * vector(N)), FK ON DELETE CASCADE to the main table, HNSW cosine index. Semantics + * are identical to the designed column: "no sidecar row" == "embedding_vector IS + * NULL" for the job-only warm-up. + * + * Backfill (DECIDED 2026-07-06, job-only warm-up): this migration creates the + * sidecar table and index ONLY. All BLOB-to-pgvector conversion happens through + * ChunkVectorizationJob's warm-up iterations selecting vectors without a sidecar + * row, giving zero upgrade-time impact. + * + * NOTE: being invisible to Nextcloud's schema tooling also means the sidecar table + * is not dropped automatically on app removal, and an embedding-dimension change + * requires `DROP TABLE openregister_vec_ann` + re-running this migration (matching + * the existing "Clear All Embeddings + re-vectorize" model-change procedure). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Migration + * @package OCA\OpenRegister\Migration + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use Exception; +use OCA\OpenRegister\Service\SettingsService; +use OCA\OpenRegister\Service\Vectorization\Handlers\EmbeddingGeneratorHandler; +use OCA\OpenRegister\Service\Vectorization\Handlers\PgVectorPlatform; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Creates the `openregister_vec_ann` pgvector sidecar table and its HNSW index on PostgreSQL. + * + * PostgreSQL only — guarded the same way as Version1Date20260322120000 (retention + * GIN index). On MariaDB/MySQL/SQLite this is a logged no-op: those platforms keep + * using the serialized-BLOB `embedding` column with the PHP cosine fallback. + * + * The vector dimension is sized from the currently-configured embedding model + * (design decision 2 of hybrid-document-search); rows carrying a different + * dimension get no sidecar row and continue to be served by the PHP-cosine + * fallback until re-vectorized. + * + * @package OCA\OpenRegister\Migration + * + * @spec openspec/changes/hybrid-document-search/tasks.md#1.1 + */ +class Version1Date20260706100000 extends SimpleMigrationStep +{ + /** + * Fallback embedding dimension when no model is configured and no rows exist. + */ + private const DEFAULT_DIMENSION = 1536; + + /** + * Constructor. + * + * @param IDBConnection $connection Database connection + * @param IConfig $config Nextcloud config + */ + public function __construct( + private readonly IDBConnection $connection, + private readonly IConfig $config + ) { + }//end __construct() + + /** + * Create the pgvector sidecar table + HNSW index after schema changes (PostgreSQL only). + * + * @param IOutput $output Migration output + * @param Closure $schemaClosure Schema closure + * @param array $options Migration options + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/hybrid-document-search/tasks.md#1.1 + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void + { + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_vectors') === false) { + return; + } + + $platform = $this->connection->getDatabasePlatform(); + + if (str_contains(get_class($platform), 'PostgreSQL') === false) { + $output->info('Skipping pgvector ANN sidecar table: unsupported database platform (PostgreSQL only)'); + return; + } + + $prefix = $this->config->getSystemValueString('dbtableprefix', 'oc_'); + + if ($prefix === '') { + // With an empty table prefix Nextcloud's migration filter matches + // every table, so the sidecar cannot be hidden from Doctrine + // introspection (whose type system does not know `vector`). + $output->warning( + 'Skipping pgvector ANN sidecar table: empty dbtableprefix would expose the ' + .'vector-typed table to Doctrine schema introspection. Vector search stays ' + .'on the PHP fallback.' + ); + return; + } + + if ($this->ensureVectorExtension(output: $output) === false) { + return; + } + + $dimension = $this->resolveConfiguredDimension(); + + $this->createSidecarTable(prefix: $prefix, dimension: $dimension, output: $output); + $this->createHnswIndex(output: $output); + }//end postSchemaChange() + + /** + * Create the pgvector extension when missing, tolerating privilege failures. + * + * Matches the tolerant-check pattern used by MagicMapper::hasPgTrgmExtension(): + * a failure to CREATE EXTENSION (e.g. missing superuser privileges) is logged + * and skipped rather than failing the whole migration — the runtime code + * degrades gracefully to the BLOB path when the sidecar is absent. + * + * @param IOutput $output Migration output + * + * @return bool True when the extension is available + */ + private function ensureVectorExtension(IOutput $output): bool + { + try { + $this->connection->executeStatement('CREATE EXTENSION IF NOT EXISTS vector'); + return true; + } catch (Exception $e) { + // Creation failed (usually privileges); check whether it already exists. + try { + $result = $this->connection->executeQuery( + "SELECT 1 FROM pg_extension WHERE extname = 'vector'" + ); + if ($result->fetchOne() !== false) { + return true; + } + } catch (Exception $inner) { + // Fall through to the warning below. + } + + $output->warning( + 'pgvector extension is not installed and could not be created (' + .$e->getMessage() + .'). Skipping ANN sidecar table; vector search stays on the PHP fallback. ' + .'Run CREATE EXTENSION vector; as a superuser and re-run this migration to enable it.' + ); + return false; + }//end try + }//end ensureVectorExtension() + + /** + * Resolve the pgvector dimension from the configured embedding model. + * + * Order: currently-configured embedding model's dimension (design decision 2), + * then the most common dimension among existing vector rows, then 1536. + * + * @return int Vector dimension + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Provider-specific settings lookup + fallbacks + */ + private function resolveConfiguredDimension(): int + { + // 1. Currently-configured embedding model (SettingsService), resolved + // lazily so a DI failure during upgrade degrades instead of aborting. + try { + $settingsService = \OCP\Server::get(SettingsService::class); + $generator = \OCP\Server::get(EmbeddingGeneratorHandler::class); + + if ($settingsService instanceof SettingsService + && $generator instanceof EmbeddingGeneratorHandler + ) { + $llmSettings = $settingsService->getLLMSettingsOnly(); + $provider = $llmSettings['embeddingProvider'] ?? null; + + $model = match ($provider) { + 'openai' => $llmSettings['openaiConfig']['model'] ?? null, + 'ollama' => $llmSettings['ollamaConfig']['model'] ?? null, + 'fireworks' => $llmSettings['fireworksConfig']['embeddingModel'] ?? null, + default => null + }; + + if ($model !== null && $model !== '') { + return $generator->getDefaultDimensions($model); + } + } + } catch (\Throwable $e) { + // Fall through to data-driven fallback. + }//end try + + // 2. Most common dimension among existing rows. + try { + $result = $this->connection->executeQuery( + 'SELECT embedding_dimensions FROM *PREFIX*openregister_vectors ' + .'WHERE embedding_dimensions IS NOT NULL ' + .'GROUP BY embedding_dimensions ORDER BY COUNT(*) DESC LIMIT 1' + ); + $modal = $result->fetchOne(); + if ($modal !== false && (int) $modal > 0) { + return (int) $modal; + } + } catch (Exception $e) { + // Fall through to the static default. + } + + return self::DEFAULT_DIMENSION; + }//end resolveConfiguredDimension() + + /** + * Create the sidecar table when missing (idempotent). + * + * @param string $prefix Nextcloud table prefix (used for the FK target) + * @param int $dimension Vector dimension + * @param IOutput $output Migration output + * + * @return void + */ + private function createSidecarTable(string $prefix, int $dimension, IOutput $output): void + { + $sidecar = PgVectorPlatform::SIDECAR_TABLE; + $mainTable = $prefix.'openregister_vectors'; + + try { + $result = $this->connection->executeQuery( + 'SELECT 1 FROM information_schema.tables WHERE table_name = :tbl', + ['tbl' => $sidecar] + ); + + if ($result->fetchOne() !== false) { + $output->info("pgvector ANN sidecar table $sidecar already exists"); + return; + } + + $this->connection->executeStatement( + "CREATE TABLE IF NOT EXISTS $sidecar (" + ."vector_id BIGINT PRIMARY KEY REFERENCES $mainTable (id) ON DELETE CASCADE, " + ."embedding vector($dimension) NOT NULL" + .')' + ); + + $output->info("Created pgvector ANN sidecar table $sidecar (vector($dimension), cascade to $mainTable)"); + } catch (Exception $e) { + $output->warning( + 'Failed to create pgvector ANN sidecar table '.$sidecar.': '.$e->getMessage() + .'. Vector search stays on the PHP fallback.' + ); + }//end try + }//end createSidecarTable() + + /** + * Create the HNSW ANN index when missing (idempotent). + * + * Non-concurrent by design (consistent with every other index-creation call + * site in this codebase); the sidecar starts empty (job-only warm-up), so + * the index build is instant at migration time. + * + * @param IOutput $output Migration output + * + * @return void + */ + private function createHnswIndex(IOutput $output): void + { + $sidecar = PgVectorPlatform::SIDECAR_TABLE; + $indexName = 'idx_or_vec_ann_hnsw'; + + try { + $result = $this->connection->executeQuery( + 'SELECT 1 FROM pg_indexes WHERE indexname = :idx', + ['idx' => $indexName] + ); + + if ($result->fetchOne() !== false) { + $output->info("HNSW index $indexName already exists"); + return; + } + + $this->connection->executeStatement( + "CREATE INDEX $indexName ON $sidecar USING hnsw (embedding vector_cosine_ops)" + ); + + $output->info("Created HNSW index $indexName on $sidecar.embedding"); + } catch (Exception $e) { + $output->warning( + 'Failed to create HNSW index on '.$sidecar.'.embedding: '.$e->getMessage() + .'. KNN queries fall back to a sequential scan on the sidecar ' + .'(pgvector >= 0.5.0 is required for HNSW).' + ); + }//end try + }//end createHnswIndex() +}//end class diff --git a/lib/Migration/Version1Date20260706101000.php b/lib/Migration/Version1Date20260706101000.php new file mode 100644 index 0000000000..d677081c30 --- /dev/null +++ b/lib/Migration/Version1Date20260706101000.php @@ -0,0 +1,147 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use Exception; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Adds the functional `to_tsvector('simple', text_content)` GIN index on PostgreSQL. + * + * PostgreSQL only — guarded the same way as Version1Date20260322120000 (retention + * GIN index). On MariaDB/MySQL/SQLite this is a logged no-op. + * + * @package OCA\OpenRegister\Migration + * + * @spec openspec/changes/hybrid-document-search/tasks.md#1.2 + */ +class Version1Date20260706101000 extends SimpleMigrationStep +{ + /** + * Constructor. + * + * @param IDBConnection $connection Database connection + * @param IConfig $config Nextcloud config + */ + public function __construct( + private readonly IDBConnection $connection, + private readonly IConfig $config + ) { + }//end __construct() + + /** + * Add the functional tsvector GIN index after schema changes (PostgreSQL only). + * + * @param IOutput $output Migration output + * @param Closure $schemaClosure Schema closure + * @param array $options Migration options + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/hybrid-document-search/tasks.md#1.2 + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void + { + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_chunks') === false) { + return; + } + + $platform = $this->connection->getDatabasePlatform(); + + if (str_contains(get_class($platform), 'PostgreSQL') === false) { + $output->info('Skipping tsvector keyword-search index: unsupported database platform (PostgreSQL only)'); + return; + } + + $prefix = $this->config->getSystemValueString('dbtableprefix', 'oc_'); + $tableName = $prefix.'openregister_chunks'; + + $this->createGinIndex(tableName: $tableName, output: $output); + }//end postSchemaChange() + + /** + * Create the functional GIN index when missing (idempotent). + * + * @param string $tableName Full table name with prefix + * @param IOutput $output Migration output + * + * @return void + */ + private function createGinIndex(string $tableName, IOutput $output): void + { + $indexName = 'idx_or_chunks_text_search_gin'; + + try { + $result = $this->connection->executeQuery( + 'SELECT 1 FROM pg_indexes WHERE indexname = :idx', + ['idx' => $indexName] + ); + + if ($result->fetchOne() !== false) { + $output->info("GIN index $indexName already exists"); + return; + } + + $this->connection->executeStatement( + "CREATE INDEX $indexName ON $tableName " + ."USING gin (to_tsvector('simple', text_content))" + ); + + $output->info( + "Created functional GIN index $indexName on $tableName (to_tsvector('simple', text_content))" + ); + } catch (Exception $e) { + $output->warning( + 'Failed to create functional GIN index on '.$tableName.': '.$e->getMessage() + .'. Ranked keyword search over chunk text stays unavailable.' + ); + }//end try + }//end createGinIndex() +}//end class diff --git a/lib/Migration/Version1Date20260706110000.php b/lib/Migration/Version1Date20260706110000.php new file mode 100644 index 0000000000..e70ac8eea6 --- /dev/null +++ b/lib/Migration/Version1Date20260706110000.php @@ -0,0 +1,142 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use Exception; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Runs `CREATE EXTENSION IF NOT EXISTS pg_trgm` once on PostgreSQL. + * + * @package OCA\OpenRegister\Migration + * + * @spec openspec/changes/searchable-property-index/tasks.md#1.1 + */ +class Version1Date20260706110000 extends SimpleMigrationStep +{ + /** + * Constructor. + * + * @param IDBConnection $connection Database connection + */ + public function __construct( + private readonly IDBConnection $connection + ) { + }//end __construct() + + /** + * Ensure the pg_trgm extension exists after schema changes (PostgreSQL only). + * + * @param IOutput $output Migration output + * @param Closure $schemaClosure Schema closure + * @param array $options Migration options + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/changes/searchable-property-index/tasks.md#1.1 + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void + { + $platform = $this->connection->getDatabasePlatform(); + + if (str_contains(get_class($platform), 'PostgreSQL') === false) { + $output->info('Skipping pg_trgm extension bootstrap: unsupported database platform (PostgreSQL only)'); + return; + } + + $this->ensurePgTrgmExtension(output: $output); + }//end postSchemaChange() + + /** + * Create the pg_trgm extension when missing, tolerating privilege failures. + * + * Matches the tolerant-check pattern used by the sibling pgvector migration + * (Version1Date20260706100000::ensureVectorExtension) and by + * MagicMapper::hasPgTrgmExtension(): a failure to CREATE EXTENSION (usually + * missing superuser privileges) is logged and skipped rather than failing + * the whole migration — the runtime index-creation code degrades gracefully + * to the unindexed query path when the extension is absent. + * + * @param IOutput $output Migration output + * + * @return void + */ + private function ensurePgTrgmExtension(IOutput $output): void + { + try { + $this->connection->executeStatement('CREATE EXTENSION IF NOT EXISTS pg_trgm'); + $output->info('pg_trgm extension is available (fuzzy/substring search indexes can be created)'); + return; + } catch (Exception $e) { + // Creation failed (usually privileges); check whether it already exists. + try { + $result = $this->connection->executeQuery( + "SELECT 1 FROM pg_extension WHERE extname = 'pg_trgm'" + ); + if ($result->fetchOne() !== false) { + $output->info('pg_trgm extension already installed'); + return; + } + } catch (Exception $inner) { + // Fall through to the warning below. + } + + $output->warning( + 'pg_trgm extension is not installed and could not be created (' + .$e->getMessage() + .'). Fuzzy/substring search over magic-table _name and searchable properties ' + .'stays on the unindexed ILIKE/similarity() path. ' + .'Run CREATE EXTENSION pg_trgm; as a superuser and re-run this migration to enable it.' + ); + }//end try + }//end ensurePgTrgmExtension() +}//end class diff --git a/lib/Migration/Version1Date20260707000000.php b/lib/Migration/Version1Date20260707000000.php new file mode 100644 index 0000000000..d2c46e9a31 --- /dev/null +++ b/lib/Migration/Version1Date20260707000000.php @@ -0,0 +1,87 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/enhanced-audit-trail/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Add the (register, schema, created) composite index to openregister_audit_trails. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/enhanced-audit-trail/spec.md + */ +class Version1Date20260707000000 extends SimpleMigrationStep +{ + /** + * Add the audit-trail register/schema/created composite index. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/enhanced-audit-trail/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_audit_trails') === false) { + return null; + } + + $table = $schema->getTable('openregister_audit_trails'); + + if ($table->hasIndex('idx_audit_register_schema') === true) { + return null; + } + + $table->addIndex(['register', 'schema', 'created'], 'idx_audit_register_schema'); + + $output->info('Added idx_audit_register_schema on openregister_audit_trails (register, schema, created)'); + + return $schema; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260710000000.php b/lib/Migration/Version1Date20260710000000.php new file mode 100644 index 0000000000..eeb8047405 --- /dev/null +++ b/lib/Migration/Version1Date20260710000000.php @@ -0,0 +1,90 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Creates the `openregister_federated_shares` table (idempotent). + * + * @package OCA\OpenRegister\Migration + */ +class Version1Date20260710000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Migration output. + * @param Closure $schemaClosure Schema closure returning an ISchemaWrapper. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null The updated schema, or null if no changes. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_federated_shares') === true) { + return null; + } + + $table = $schema->createTable('openregister_federated_shares'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20]); + $table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 36]); + $table->addColumn('direction', Types::STRING, ['notnull' => true, 'length' => 16]); + $table->addColumn('remote_instance_url', Types::STRING, ['notnull' => false, 'length' => 1024, 'default' => null]); + $table->addColumn('remote_provider_id', Types::STRING, ['notnull' => false, 'length' => 255, 'default' => null]); + $table->addColumn('share_token', Types::STRING, ['notnull' => true, 'length' => 128]); + $table->addColumn('scope', Types::STRING, ['notnull' => true, 'length' => 16, 'default' => 'schema']); + $table->addColumn('register', Types::STRING, ['notnull' => false, 'length' => 255, 'default' => null]); + $table->addColumn('schema', Types::STRING, ['notnull' => false, 'length' => 255, 'default' => null]); + $table->addColumn('object_uri', Types::STRING, ['notnull' => false, 'length' => 1024, 'default' => null]); + $table->addColumn('query_filter', Types::TEXT, ['notnull' => false, 'default' => null]); + $table->addColumn('permissions', Types::STRING, ['notnull' => true, 'length' => 16, 'default' => 'read']); + $table->addColumn('organisation', Types::STRING, ['notnull' => false, 'length' => 64, 'default' => null]); + $table->addColumn('shared_with', Types::STRING, ['notnull' => false, 'length' => 512, 'default' => null]); + $table->addColumn('status', Types::STRING, ['notnull' => true, 'length' => 16, 'default' => 'pending']); + $table->addColumn('created', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['share_token'], 'or_fedshare_token'); + $table->addIndex(['organisation', 'direction'], 'or_fedshare_org_dir'); + $table->addIndex(['register', 'schema'], 'or_fedshare_reg_sch'); + + $output->info(' ✓ Created openregister_federated_shares table'); + + return $schema; + + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260712120000.php b/lib/Migration/Version1Date20260712120000.php new file mode 100644 index 0000000000..67b60cdfe0 --- /dev/null +++ b/lib/Migration/Version1Date20260712120000.php @@ -0,0 +1,103 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/notification-delivery-windows/design.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the openregister_notification_queue table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +class Version1Date20260712120000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return ISchemaWrapper|null + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_notification_queue') === true) { + return null; + } + + $table = $schema->createTable('openregister_notification_queue'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('rule_key', Types::STRING, ['notnull' => true, 'length' => 191, 'default' => '']); + $table->addColumn('recipient', Types::STRING, ['notnull' => true, 'length' => 191, 'default' => '']); + $table->addColumn('reason', Types::STRING, ['notnull' => true, 'length' => 32, 'default' => '']); + $table->addColumn('object_uuid', Types::STRING, ['notnull' => false, 'length' => 36]); + $table->addColumn('payload', Types::TEXT, ['notnull' => true]); + $table->addColumn('due_at_hint', Types::DATETIME, ['notnull' => true]); + $table->addColumn('created_at', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['recipient', 'reason'], 'idx_notifqueue_recip_reason'); + $table->addIndex(['due_at_hint'], 'idx_notifqueue_due_hint'); + $table->addIndex(['rule_key', 'recipient'], 'idx_notifqueue_rule_recip'); + + $output->info('Created openregister_notification_queue table'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260712130000.php b/lib/Migration/Version1Date20260712130000.php new file mode 100644 index 0000000000..a41aefe25e --- /dev/null +++ b/lib/Migration/Version1Date20260712130000.php @@ -0,0 +1,119 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-006 — Every invocation is audited) + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Adds nullable `tool_id`, `params_digest`, `result_summary` columns to the + * `openregister_audit_trails` table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +class Version1Date20260712130000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process + * @param Closure $schemaClosure The schema closure + * @param array $options Migration options + * + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable(tableName: 'openregister_audit_trails') === false) { + return null; + } + + $table = $schema->getTable(tableName: 'openregister_audit_trails'); + $changed = false; + + if ($table->hasColumn(name: 'tool_id') === false) { + $table->addColumn( + name: 'tool_id', + typeName: Types::STRING, + options: [ + 'notnull' => false, + 'length' => 190, + ] + ); + $changed = true; + } + + if ($table->hasColumn(name: 'params_digest') === false) { + $table->addColumn( + name: 'params_digest', + typeName: Types::STRING, + options: [ + 'notnull' => false, + 'length' => 64, + ] + ); + $changed = true; + } + + if ($table->hasColumn(name: 'result_summary') === false) { + $table->addColumn( + name: 'result_summary', + typeName: Types::JSON, + options: ['notnull' => false] + ); + $changed = true; + } + + if ($table->hasIndex(name: 'idx_audit_trails_tool_id') === false) { + $table->addIndex( + columnNames: ['tool_id'], + indexName: 'idx_audit_trails_tool_id' + ); + $changed = true; + } + + if ($changed === false) { + return null; + } + + $output->info('Added tool_id/params_digest/result_summary to openregister_audit_trails'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260713000000.php b/lib/Migration/Version1Date20260713000000.php new file mode 100644 index 0000000000..35fc66ed65 --- /dev/null +++ b/lib/Migration/Version1Date20260713000000.php @@ -0,0 +1,123 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the openregister_scheduled_reports table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ +class Version1Date20260713000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_scheduled_reports') === true) { + return null; + } + + $table = $schema->createTable('openregister_scheduled_reports'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('owner', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addColumn('name', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('register_id', Types::BIGINT, ['notnull' => true, 'unsigned' => true]); + $table->addColumn('schema_id', Types::BIGINT, ['notnull' => false, 'unsigned' => true]); + $table->addColumn('filters', Types::TEXT, ['notnull' => false]); + $table->addColumn('format', Types::STRING, ['notnull' => true, 'length' => 16]); + $table->addColumn('schedule_type', Types::STRING, ['notnull' => true, 'length' => 16]); + $table->addColumn('schedule_hour', Types::SMALLINT, ['notnull' => true, 'default' => 0]); + $table->addColumn('schedule_day_of_week', Types::SMALLINT, ['notnull' => false]); + $table->addColumn('schedule_day_of_month', Types::SMALLINT, ['notnull' => false]); + $table->addColumn('delivery_folder', Types::STRING, ['notnull' => true, 'length' => 512, 'default' => 'Reports/']); + $table->addColumn('enabled', Types::BOOLEAN, ['notnull' => true, 'default' => true]); + $table->addColumn('last_run_at', Types::DATETIME, ['notnull' => false]); + $table->addColumn('last_status', Types::STRING, ['notnull' => false, 'length' => 16]); + $table->addColumn('last_error', Types::TEXT, ['notnull' => false]); + $table->addColumn('created_at', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated_at', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['owner'], 'idx_or_sched_report_owner'); + $table->addIndex(['enabled'], 'idx_or_sched_report_enabled'); + + $output->info('Created openregister_scheduled_reports table'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260714000000.php b/lib/Migration/Version1Date20260714000000.php new file mode 100644 index 0000000000..2f59be8615 --- /dev/null +++ b/lib/Migration/Version1Date20260714000000.php @@ -0,0 +1,115 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Add `delivery_mode`/`recipients` columns to `openregister_scheduled_reports`. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ +class Version1Date20260714000000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable(tableName: 'openregister_scheduled_reports') === false) { + return null; + } + + $table = $schema->getTable(tableName: 'openregister_scheduled_reports'); + $changed = false; + + if ($table->hasColumn(name: 'delivery_mode') === false) { + $table->addColumn( + name: 'delivery_mode', + typeName: Types::STRING, + options: [ + 'notnull' => true, + 'length' => 16, + 'default' => 'files', + ] + ); + $changed = true; + } + + if ($table->hasColumn(name: 'recipients') === false) { + $table->addColumn( + name: 'recipients', + typeName: Types::TEXT, + options: ['notnull' => false] + ); + $changed = true; + } + + if ($changed === false) { + return null; + } + + $output->info('Added delivery_mode/recipients to openregister_scheduled_reports'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260714010000.php b/lib/Migration/Version1Date20260714010000.php new file mode 100644 index 0000000000..12602dfe05 --- /dev/null +++ b/lib/Migration/Version1Date20260714010000.php @@ -0,0 +1,97 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/approval-workflow/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Add `requester_id` to `openregister_approval_steps`. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/approval-workflow/spec.md + */ +class Version1Date20260714010000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/approval-workflow/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable(tableName: 'openregister_approval_steps') === false) { + return null; + } + + $table = $schema->getTable(tableName: 'openregister_approval_steps'); + + if ($table->hasColumn(name: 'requester_id') === true) { + return null; + } + + $table->addColumn( + name: 'requester_id', + typeName: Types::STRING, + options: [ + 'notnull' => false, + 'length' => 255, + ] + ); + + $output->info('Added requester_id to openregister_approval_steps'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260714100000.php b/lib/Migration/Version1Date20260714100000.php new file mode 100644 index 0000000000..4f9808e6c4 --- /dev/null +++ b/lib/Migration/Version1Date20260714100000.php @@ -0,0 +1,105 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Create the openregister_migration_packs table. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ +class Version1Date20260714100000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_migration_packs') === true) { + return null; + } + + $table = $schema->createTable('openregister_migration_packs'); + + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'unsigned' => true]); + $table->addColumn('pack_slug', Types::STRING, ['notnull' => true, 'length' => 128]); + $table->addColumn('name', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('source_format', Types::STRING, ['notnull' => true, 'length' => 16]); + $table->addColumn('version', Types::STRING, ['notnull' => true, 'length' => 32]); + $table->addColumn('definition', Types::TEXT, ['notnull' => true]); + $table->addColumn('builtin', Types::BOOLEAN, ['notnull' => true, 'default' => false]); + $table->addColumn('owner', Types::STRING, ['notnull' => false, 'length' => 64]); + $table->addColumn('created_at', Types::DATETIME, ['notnull' => true]); + $table->addColumn('updated_at', Types::DATETIME, ['notnull' => true]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['pack_slug'], 'idx_or_migration_pack_slug'); + + $output->info('Created openregister_migration_packs table'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Migration/Version1Date20260714120000.php b/lib/Migration/Version1Date20260714120000.php new file mode 100644 index 0000000000..63ff324ab5 --- /dev/null +++ b/lib/Migration/Version1Date20260714120000.php @@ -0,0 +1,77 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/realtime-updates/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Drop the orphaned openregister_realtime_events table (and its indexes). + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @spec openspec/specs/realtime-updates/spec.md + */ +class Version1Date20260714120000 extends SimpleMigrationStep +{ + /** + * Change the database schema. + * + * @param IOutput $output Output for the migration process. + * @param Closure $schemaClosure The schema closure. + * @param array $options Migration options. + * + * @return ISchemaWrapper|null + * + * @spec openspec/specs/realtime-updates/spec.md + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /* + * @var ISchemaWrapper $schema + */ + + $schema = $schemaClosure(); + + if ($schema->hasTable('openregister_realtime_events') === false) { + return null; + } + + $schema->dropTable('openregister_realtime_events'); + + return $schema; + }//end changeSchema() +}//end class diff --git a/lib/Notification/AnnotationNotifier.php b/lib/Notification/AnnotationNotifier.php index dfe482b0e2..f268ae3769 100644 --- a/lib/Notification/AnnotationNotifier.php +++ b/lib/Notification/AnnotationNotifier.php @@ -102,6 +102,8 @@ public function getName(): string * @throws UnknownNotificationException When the notification is not an * annotation/object notification this * notifier owns. + * + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md */ public function prepare(INotification $notification, string $languageCode): INotification { @@ -126,8 +128,8 @@ public function prepare(INotification $notification, string $languageCode): INot // The schema's custom per-locale subject (already interpolated by the // dispatcher for this recipient) wins; otherwise render the canonical // localised string with the object title + register name substituted. - $objectTitle = (string) ($params['objectTitle'] ?? $l->t('object')); - $registerName = (string) ($params['registerName'] ?? ($params['registerId'] ?? '')); + $objectTitle = (string) ($params['objectTitle'] ?? $l->t('object')); + $registerName = (string) ($params['registerName'] ?? ($params['registerId'] ?? '')); $parsedSubject = $l->t(self::SUBJECT_TEMPLATES[$subject], [$objectTitle, $registerName]); if ($hasText === true) { $parsedSubject = $text; @@ -135,15 +137,113 @@ public function prepare(INotification $notification, string $languageCode): INot $notification->setParsedSubject($parsedSubject); - $notification->setIcon( - $this->urlGenerator->imagePath(appName: 'openregister', file: 'app.svg') - ); + // Notification BODY (distinct from the title/subject). The dispatcher + // pre-resolves the recipient-localised body — the rule's `message` + // template, or an auto-derived "Open in {AppName}." when the rule has + // actions but no message. Left unset when empty (back-compat: rules + // with neither message nor actions render exactly as before). + $message = ($params['_message'] ?? null); + if (is_string($message) === true && $message !== '') { + $notification->setParsedMessage($message); + } + + // Icon: when the rule resolved an originApp, point at the hex-composite + // raster endpoint (the originApp's white glyph on the cobalt hexagon) + // instead of the static openregister app image — so the OS popup + // carries the originating app's identity. Falls back to app.svg. + $originApp = (string) ($params['originApp'] ?? 'openregister'); + if ($originApp !== '' && $originApp !== 'openregister') { + $notification->setIcon( + $this->urlGenerator->linkToRouteAbsolute( + 'openregister.webPush.hexIcon', + ['app' => $originApp] + ) + ); + } else { + // The setIcon() method only accepts absolute http(s) URLs (desktop and + // mobile client support) — a relative imagePath() throws + // InvalidValueException on every render, so the notification never + // reaches the client. + $notification->setIcon( + $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath(appName: 'openregister', file: 'app.svg') + ) + ); + } + + // Render declared action buttons when the rule provided any; otherwise + // keep the implicit single "View" action (back-compat — existing rules + // are unchanged). + $actions = ($params['_actions'] ?? []); + $rendered = 0; + if (is_array($actions) === true && count($actions) > 0) { + $rendered = $this->addDeclaredActions(notification: $notification, actions: $actions, languageCode: $languageCode); + } - $this->addViewAction(notification: $notification, params: $params, label: $l->t('View')); + // Fall back to the implicit "View" action when no declared action was + // rendered — either none were declared (back-compat) or every declared + // action resolved to an empty link. This guarantees a notification + // never ships with zero actions when an object-detail target exists. + if ($rendered === 0) { + $this->addViewAction(notification: $notification, params: $params, label: $l->t('View')); + } return $notification; }//end prepare() + /** + * Render the schema-declared action buttons via addAction(). + * + * Each action carries a per-locale `label` map, a `primary` flag, and a + * pre-resolved absolute `url` (resolved server-side by the dispatcher + * through OR RBAC). The recipient's locale label wins, falling back to + * `en` then the first available locale. + * + * @param INotification $notification Notification to attach actions to. + * @param array $actions Resolved actions (each element validated at runtime). + * @param string $languageCode Active recipient locale. + * + * @return int The number of action buttons actually rendered. + * + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md + */ + private function addDeclaredActions(INotification $notification, array $actions, string $languageCode): int + { + $rendered = 0; + foreach ($actions as $action) { + if (is_array($action) === false) { + continue; + } + + $url = (string) ($action['url'] ?? ''); + if ($url === '') { + continue; + } + + $labelMap = ($action['label'] ?? []); + if (is_array($labelMap) === false) { + $labelMap = []; + } + + $fallbackLabel = 'Open'; + $firstLabel = reset($labelMap); + if ($firstLabel !== false) { + $fallbackLabel = $firstLabel; + } + + $label = (string) ($labelMap[$languageCode] ?? ($labelMap['en'] ?? $fallbackLabel)); + + $actionObject = $notification->createAction(); + $actionObject->setLabel($label) + ->setPrimary((bool) ($action['primary'] ?? false)) + ->setLink($url, 'GET'); + $notification->addAction($actionObject); + $rendered++; + }//end foreach + + return $rendered; + }//end addDeclaredActions() + /** * Attach a "View" deep-link action to the notification when all routing * parameters (registerId, schemaId, objectUuid) are present and non-empty. diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index 61692080e9..fcb3d74771 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -35,6 +35,8 @@ * Handles the preparation of notifications for display in Nextcloud. * * @package OCA\OpenRegister\Notification + * + * @spec openspec/specs/notificatie-engine/spec.md */ class Notifier implements INotifier { @@ -59,7 +61,7 @@ public function __construct( * * @psalm-return 'openregister' * - * @spec openspec/changes/retrofit-2026-04-28-notificatie-engine/tasks.md#task-1 + * @spec openspec/specs/notificatie-engine/spec.md */ public function getID(): string { @@ -70,6 +72,8 @@ public function getID(): string * Human readable name describing the notifier. * * @return string The notifier name + * + * @spec openspec/specs/notificatie-engine/spec.md */ public function getName(): string { @@ -85,7 +89,7 @@ public function getName(): string * @return INotification The prepared notification * @throws InvalidArgumentException If the notification is not from this app * - * @spec openspec/changes/retrofit-2026-04-28-notificatie-engine/tasks.md#task-1 + * @spec openspec/specs/notificatie-engine/spec.md */ public function prepare(INotification $notification, string $languageCode): INotification { @@ -100,6 +104,15 @@ public function prepare(INotification $notification, string $languageCode): INot case 'configuration_update_available': return $this->prepareConfigurationUpdate(notification: $notification, l: $l); + case 'handoff_drain_failed': + return $this->prepareHandoffDrainFailed(notification: $notification, l: $l); + + case 'scheduled_report_delivered': + return $this->prepareScheduledReportDelivered(notification: $notification, l: $l); + + case 'scheduled_report_failed': + return $this->prepareScheduledReportFailed(notification: $notification, l: $l); + default: // Unknown subject. Object-lifecycle subjects // (object_created / object_updated / object_transitioned) @@ -116,7 +129,7 @@ public function prepare(INotification $notification, string $languageCode): INot * * @return INotification The prepared notification * - * @spec openspec/changes/retrofit-2026-04-28-notificatie-engine/tasks.md#task-1 + * @spec openspec/specs/notificatie-engine/spec.md */ private function prepareConfigurationUpdate(INotification $notification, $l): INotification { @@ -158,4 +171,154 @@ private function prepareConfigurationUpdate(INotification $notification, $l): IN return $notification; }//end prepareConfigurationUpdate() + + /** + * Prepare the queue-mode handoff drain-failure notification (ADR-051): + * a parked handoff could not execute when a provider appeared — the + * requester lost create permission or the mapped object failed target + * validation. The requester is informed so the parked work is never + * silently lost. + * + * @param INotification $notification The notification to prepare + * @param mixed $l The localization instance + * + * @return INotification The prepared notification + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + private function prepareHandoffDrainFailed(INotification $notification, $l): INotification + { + $parameters = $notification->getSubjectParameters(); + + $handoffId = $parameters['handoffId'] ?? 'handoff'; + $targetKind = $parameters['targetKind'] ?? ''; + $status = $parameters['status'] ?? 'failed'; + + $notification->setParsedSubject( + $l->t('Queued handoff "%s" could not be executed', [$handoffId]) + ); + + $reason = $l->t('The target schema rejected the converted object.'); + if ($status === 'failed-permission') { + $reason = $l->t('You no longer have permission to create objects in the providing schema.'); + } + + $notification->setParsedMessage( + $l->t( + 'Your queued handoff to "%s" was attempted when a provider became available, but failed: %s', + [$targetKind, $reason] + ) + ); + + $notification->setIcon( + $this->urlGenerator->imagePath(appName: 'openregister', file: 'app.svg') + ); + + return $notification; + }//end prepareHandoffDrainFailed() + + /** + * Prepare the scheduled-report success notification (scheduled-report-jobs, + * extended by scheduled-report-email-delivery): a recurring export ran + * and was delivered to Nextcloud Files, email, or both, per the report's + * `deliveryMode`. Reuses this single subject for every mode — `mode` and + * `emailFailureReason` (set when Files succeeded but the email leg + * failed, i.e. `lastStatus: email_failed`) are optional parameters so no + * new subject was needed. + * + * @param INotification $notification The notification to prepare + * @param mixed $l The localization instance + * + * @return INotification The prepared notification + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + private function prepareScheduledReportDelivered(INotification $notification, $l): INotification + { + $parameters = $notification->getSubjectParameters(); + + $reportName = $parameters['reportName'] ?? 'Scheduled report'; + $folder = $parameters['folder'] ?? 'Reports/'; + $filename = $parameters['filename'] ?? ''; + $mode = $parameters['mode'] ?? 'files'; + $emailFailureReason = $parameters['emailFailureReason'] ?? null; + + $notification->setParsedSubject( + $l->t('Scheduled report "%s" delivered', [$reportName]) + ); + + $message = match ($mode) { + 'email' => $l->t('Your scheduled report "%s" ran and was emailed to its recipients.', [$reportName]), + 'both' => $l->t( + 'Your scheduled report "%s" ran, was saved to %s%s, and emailed to its recipients.', + [$reportName, $folder, $filename] + ), + default => $l->t( + 'Your scheduled report "%s" ran and was saved to %s%s', + [$reportName, $folder, $filename] + ), + }; + + if (is_string($emailFailureReason) === true && $emailFailureReason !== '') { + $message .= ' '.$l->t('Note: email delivery failed (%s).', [$emailFailureReason]); + } + + $notification->setParsedMessage($message); + + $notification->setIcon( + $this->urlGenerator->imagePath(appName: 'openregister', file: 'app.svg') + ); + + if ($mode !== 'email') { + $action = $notification->createAction(); + $action->setLabel($l->t('Open Files')) + ->setPrimary(true) + ->setLink( + link: $this->urlGenerator->linkToRouteAbsolute( + routeName: 'files.view.index' + ).'?dir='.rawurlencode('/'.trim((string) $folder, '/')), + requestType: 'GET' + ); + + $notification->addAction($action); + } + + return $notification; + }//end prepareScheduledReportDelivered() + + /** + * Prepare the scheduled-report failure notification (scheduled-report-jobs): + * a recurring export failed (row-cap exceeded or any other error) and was + * not retried automatically. + * + * @param INotification $notification The notification to prepare + * @param mixed $l The localization instance + * + * @return INotification The prepared notification + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + private function prepareScheduledReportFailed(INotification $notification, $l): INotification + { + $parameters = $notification->getSubjectParameters(); + + $reportName = $parameters['reportName'] ?? 'Scheduled report'; + $reason = $parameters['reason'] ?? 'Unknown error'; + + $notification->setParsedSubject( + $l->t('Scheduled report "%s" failed', [$reportName]) + ); + + $notification->setParsedMessage( + $l->t('Your scheduled report "%s" could not be generated: %s', [$reportName, $reason]) + ); + + $notification->setIcon( + $this->urlGenerator->imagePath(appName: 'openregister', file: 'app.svg') + ); + + return $notification; + }//end prepareScheduledReportFailed() }//end class diff --git a/lib/Reference/ObjectReferenceProvider.php b/lib/Reference/ObjectReferenceProvider.php index a1c3b6677e..131ee1cb84 100644 --- a/lib/Reference/ObjectReferenceProvider.php +++ b/lib/Reference/ObjectReferenceProvider.php @@ -154,7 +154,7 @@ class ObjectReferenceProvider extends ADiscoverableReferenceProvider implements * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function __construct( IURLGenerator $urlGenerator, @@ -181,7 +181,7 @@ public function __construct( * * @return string Provider ID * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function getId(): string { @@ -193,7 +193,7 @@ public function getId(): string * * @return string Translated title * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function getTitle(): string { @@ -205,7 +205,7 @@ public function getTitle(): string * * @return int Order value (lower = higher priority) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function getOrder(): int { @@ -217,7 +217,7 @@ public function getOrder(): int * * @return string URL to the app icon * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function getIconUrl(): string { @@ -229,7 +229,7 @@ public function getIconUrl(): string * * @return string[] List of search provider IDs * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function getSupportedSearchProviderIds(): array { @@ -250,7 +250,7 @@ public function getSupportedSearchProviderIds(): array * * @return bool True if the URL matches an OpenRegister object reference * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function matchReference(string $referenceText): bool { @@ -271,7 +271,7 @@ public function matchReference(string $referenceText): bool * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function resolveReference(string $referenceText): ?IReference { @@ -394,7 +394,7 @@ public function resolveReference(string $referenceText): ?IReference * * @return string Cache prefix based on register/schema/uuid * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function getCachePrefix(string $referenceId): string { @@ -417,7 +417,7 @@ public function getCachePrefix(string $referenceId): string * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function getCacheKey(string $referenceId): ?string { @@ -431,7 +431,7 @@ public function getCacheKey(string $referenceId): ?string * * @return array{registerId: int, schemaId: int, uuid: string}|null Parsed parts or null * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ public function parseReference(string $referenceText): ?array { @@ -503,7 +503,7 @@ public function parseReference(string $referenceText): ?array * * @return string The object title * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ private function extractTitle(array $objectData, array $selfData): string { @@ -538,7 +538,7 @@ private function extractTitle(array $objectData, array $selfData): string * * @return string Truncated description (max 200 chars) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ private function extractDescription(array $objectData): string { @@ -571,7 +571,7 @@ private function extractDescription(array $objectData): string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ private function extractPreviewProperties(array $objectData): array { @@ -619,7 +619,7 @@ private function extractPreviewProperties(array $objectData): array * * @return string The schema title or fallback * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ private function resolveSchemaName(int $schemaId): string { @@ -643,7 +643,7 @@ private function resolveSchemaName(int $schemaId): string * * @return string The register title or fallback * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-3 + * @spec openspec/specs/deep-link-registry/spec.md */ private function resolveRegisterName(int $registerId): string { diff --git a/lib/Repair/ImportCredentialBrokerRegister.php b/lib/Repair/ImportCredentialBrokerRegister.php new file mode 100644 index 0000000000..f4406cf1f0 --- /dev/null +++ b/lib/Repair/ImportCredentialBrokerRegister.php @@ -0,0 +1,129 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + * + * @spec openspec/specs/credential-broker/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Service\ConfigurationService; +use OCP\App\IAppManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Imports the credential-broker register descriptor idempotently on upgrade/install. + */ +class ImportCredentialBrokerRegister implements IRepairStep +{ + /** + * App-relative path to the register descriptor imported by this step. + * + * @var string + */ + private const REGISTER_PATH = '/lib/Settings/credential_broker_register.json'; + + /** + * Descriptor version passed to the importer's version_compare gate. + * + * @var string + */ + private const REGISTER_VERSION = '1.0.0'; + + /** + * Constructor. + * + * @param ConfigurationService $configurationService The OR configuration importer. + * @param IAppManager $appManager Resolves the openregister app path on disk. + * @param LoggerInterface $logger Logger for import diagnostics. + * + * @return void + */ + public function __construct( + private readonly ConfigurationService $configurationService, + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function getName(): string + { + return 'Import OpenRegister credential-broker register (credential schema + example objects)'; + }//end getName() + + /** + * Run the repair step, importing the credential-broker register descriptor. + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function run(IOutput $output): void + { + try { + $path = $this->appManager->getAppPath('openregister').self::REGISTER_PATH; + if (is_file($path) === false) { + $output->warning('Credential-broker register descriptor not found: '.$path); + return; + } + + // Import the DECODED descriptor via importFromApp() — NOT importFromFilePath(), + // which rejects an absolute path (it expects a Nextcloud-root-relative one) and + // would fail closed here. importFromApp() takes the data directly. + $data = json_decode((string) file_get_contents($path), true); + if (is_array($data) === false) { + $output->warning('Credential-broker register descriptor is not valid JSON: '.$path); + return; + } + + $this->configurationService->importFromApp( + appId: 'openregister', + data: $data, + version: self::REGISTER_VERSION, + force: false + ); + + $output->info('Credential-broker register imported (credential schema + example objects)'); + } catch (Throwable $e) { + $this->logger->warning('[ImportCredentialBrokerRegister] import failed: '.$e->getMessage()); + $output->warning('Credential-broker register import skipped: '.$e->getMessage()); + }//end try + }//end run() +}//end class diff --git a/lib/Repair/ImportDsarRegisters.php b/lib/Repair/ImportDsarRegisters.php new file mode 100644 index 0000000000..50d46198f8 --- /dev/null +++ b/lib/Repair/ImportDsarRegisters.php @@ -0,0 +1,159 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Service\ConfigurationService; +use OCP\App\IAppManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Import the DSAR case + policy-pack registers from lib/Settings. + * + * @psalm-suppress UnusedClass Instantiated by the NC repair framework (appinfo/info.xml). + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ +class ImportDsarRegisters implements IRepairStep +{ + + /** + * App-relative descriptor paths mapped to their configuration identity + * (appId) + the descriptor version passed to the importer's + * version_compare gate (bump alongside the schema versions). + * + * Each descriptor gets its OWN configuration appId: `importFromApp` + * version-gates per configuration row, so two descriptors sharing one + * appId silently skip whichever imports second (verified live — the + * policy-pack file was version-gated away against the case register's + * higher version under a shared 'openregister' id). + * + * @var array + */ + private const REGISTERS = [ + '/lib/Settings/data_subject_request_register.json' => [ + 'appId' => 'openregister.dsar-cases', + 'version' => '1.2.0', + ], + '/lib/Settings/dsar_policy_pack_register.json' => [ + 'appId' => 'openregister.dsar-policy-packs', + 'version' => '1.1.0', + ], + ]; + + /** + * Constructor. + * + * @param ConfigurationService $configurationService The OR configuration importer. + * @param IAppManager $appManager Resolves the openregister app path on disk. + * @param LoggerInterface $logger Logger for import diagnostics. + * + * @return void + */ + public function __construct( + private readonly ConfigurationService $configurationService, + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ + public function getName(): string + { + return 'Import OpenRegister DSAR registers (data-subject requests + policy packs)'; + }//end getName() + + /** + * Run the repair step, importing both DSAR register descriptors. + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ + public function run(IOutput $output): void + { + foreach (self::REGISTERS as $relativePath => $descriptor) { + $version = $descriptor['version']; + + try { + $path = $this->appManager->getAppPath('openregister').$relativePath; + if (is_file($path) === false) { + $output->warning('DSAR register descriptor not found: '.$path); + continue; + } + + // Import the DECODED descriptor via importFromApp() — NOT + // importFromFilePath(), which rejects an absolute path (it + // expects a Nextcloud-root-relative one) and would fail + // closed here. importFromApp() takes the data directly. + $data = json_decode((string) file_get_contents($path), true); + if (is_array($data) === false) { + $output->warning('DSAR register descriptor is not valid JSON: '.$path); + continue; + } + + $this->configurationService->importFromApp( + appId: $descriptor['appId'], + data: $data, + version: $version, + force: false + ); + + $output->info('DSAR register imported: '.basename($relativePath).' (v'.$version.')'); + } catch (Throwable $e) { + $this->logger->warning('[ImportDsarRegisters] import failed for '.$relativePath.': '.$e->getMessage()); + $output->warning('DSAR register import skipped for '.basename($relativePath).': '.$e->getMessage()); + }//end try + }//end foreach + }//end run() +}//end class diff --git a/lib/Repair/ImportEdepotTransferRegister.php b/lib/Repair/ImportEdepotTransferRegister.php new file mode 100644 index 0000000000..3b74aa0efb --- /dev/null +++ b/lib/Repair/ImportEdepotTransferRegister.php @@ -0,0 +1,143 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Service\ConfigurationService; +use OCP\App\IAppManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Import the e-Depot transfer + proof-of-transfer register from lib/Settings. + * + * @psalm-suppress UnusedClass Instantiated by the NC repair framework (appinfo/info.xml). + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Requirement: Durable transfer-list objects served over the API) + */ +class ImportEdepotTransferRegister implements IRepairStep +{ + + /** + * App-relative descriptor path. + * + * @var string + */ + private const REGISTER_PATH = '/lib/Settings/edepot_transfer_register.json'; + + /** + * Descriptor version passed to the importer's version_compare gate. + * + * @var string + */ + private const REGISTER_VERSION = '1.0.0'; + + /** + * Configuration identity for the descriptor (its own appId so the + * importFromApp version gate is independent of the other system registers). + * + * @var string + */ + private const CONFIG_APP_ID = 'openregister.edepot-transfers'; + + /** + * Constructor. + * + * @param ConfigurationService $configurationService The OR configuration importer. + * @param IAppManager $appManager Resolves the openregister app path on disk. + * @param LoggerInterface $logger Logger for import diagnostics. + * + * @return void + */ + public function __construct( + private readonly ConfigurationService $configurationService, + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Requirement: Durable transfer-list objects served over the API) + */ + public function getName(): string + { + return 'Import OpenRegister e-Depot transfer register (transfer lists + proof-of-transfer records)'; + }//end getName() + + /** + * Run the repair step, importing the e-Depot transfer register descriptor. + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Requirement: Durable transfer-list objects served over the API) + */ + public function run(IOutput $output): void + { + try { + $path = $this->appManager->getAppPath('openregister').self::REGISTER_PATH; + if (is_file($path) === false) { + $output->warning('e-Depot transfer register descriptor not found: '.$path); + return; + } + + $data = json_decode((string) file_get_contents($path), true); + if (is_array($data) === false) { + $output->warning('e-Depot transfer register descriptor is not valid JSON: '.$path); + return; + } + + $this->configurationService->importFromApp( + appId: self::CONFIG_APP_ID, + data: $data, + version: self::REGISTER_VERSION, + force: false + ); + + $output->info('e-Depot transfer register imported (edepotTransfer + edepotTransferProof)'); + } catch (Throwable $e) { + $this->logger->warning('[ImportEdepotTransferRegister] import failed: '.$e->getMessage()); + $output->warning('e-Depot transfer register import skipped: '.$e->getMessage()); + }//end try + }//end run() +}//end class diff --git a/lib/Repair/ImportTrustConfigurationRegister.php b/lib/Repair/ImportTrustConfigurationRegister.php new file mode 100644 index 0000000000..a48f5e663b --- /dev/null +++ b/lib/Repair/ImportTrustConfigurationRegister.php @@ -0,0 +1,140 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Service\ConfigurationService; +use OCP\App\IAppManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Import the MDM trust-configuration register from lib/Settings. + * + * @psalm-suppress UnusedClass Instantiated by the NC repair framework (appinfo/info.xml). + */ +class ImportTrustConfigurationRegister implements IRepairStep +{ + + /** + * App-relative descriptor path. + * + * @var string + */ + private const REGISTER_PATH = '/lib/Settings/trust_configuration_register.json'; + + /** + * Descriptor version passed to the importer's version_compare gate. + * + * @var string + */ + private const REGISTER_VERSION = '1.0.0'; + + /** + * Configuration identity for the descriptor (its own appId so the + * importFromApp version gate is independent of the other system registers). + * + * @var string + */ + private const CONFIG_APP_ID = 'openregister.trust-configuration'; + + /** + * Constructor. + * + * @param ConfigurationService $configurationService The OR configuration importer. + * @param IAppManager $appManager Resolves the openregister app path on disk. + * @param LoggerInterface $logger Logger for import diagnostics. + * + * @return void + */ + public function __construct( + private readonly ConfigurationService $configurationService, + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + */ + public function getName(): string + { + return 'Import OpenRegister MDM trust-configuration register (source trust tiers for survivorship)'; + }//end getName() + + /** + * Run the repair step, importing the trust-configuration register descriptor. + * + * @param IOutput $output Output interface for status messages. + * + * @return void + */ + public function run(IOutput $output): void + { + try { + $path = $this->appManager->getAppPath('openregister').self::REGISTER_PATH; + if (is_file($path) === false) { + $output->warning('Trust-configuration register descriptor not found: '.$path); + return; + } + + $data = json_decode((string) file_get_contents($path), true); + if (is_array($data) === false) { + $output->warning('Trust-configuration register descriptor is not valid JSON: '.$path); + return; + } + + $this->configurationService->importFromApp( + appId: self::CONFIG_APP_ID, + data: $data, + version: self::REGISTER_VERSION, + force: false + ); + + $output->info('MDM trust-configuration register imported (trustConfiguration)'); + } catch (Throwable $e) { + $this->logger->warning('[ImportTrustConfigurationRegister] import failed: '.$e->getMessage()); + $output->warning('Trust-configuration register import skipped: '.$e->getMessage()); + }//end try + }//end run() +}//end class diff --git a/lib/Repair/LogDanglingLinkedTypes.php b/lib/Repair/LogDanglingLinkedTypes.php index bf69b2093b..570c4462a3 100644 --- a/lib/Repair/LogDanglingLinkedTypes.php +++ b/lib/Repair/LogDanglingLinkedTypes.php @@ -5,11 +5,12 @@ * references integration ids that the registry can no longer resolve. * * Per AD-5 of pluggable-integration-registry the registry validates - * linkedTypes against either the legacy `VALID_LINKED_TYPES` set or - * the live `IntegrationRegistry::listIds()` output. Existing schemas - * may carry ids that are valid TODAY (because they appear in the - * deprecated fallback) but will eventually become invalid as the - * deprecated map is removed. This repair step scans all schemas at + * linkedTypes against either the legacy private allow-list (see + * `Schema::legacyLinkedTypeIds()`) or the live + * `IntegrationRegistry::listIds()` output. Existing schemas may carry + * ids that are valid TODAY (because they appear in the legacy + * fallback) but will eventually become invalid as the legacy + * fallback is removed. This repair step scans all schemas at * install / post-migration time and logs WARNING entries for any * linkedTypes value not registered with the registry. * @@ -30,7 +31,7 @@ * @link https://conduction.nl * * @spec openspec/changes/pluggable-integration-registry/tasks.md#task-11 - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-3 + * @spec openspec/specs/linked-entity-types/spec.md */ declare(strict_types=1); @@ -73,7 +74,7 @@ public function __construct( * * @return string * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-3 + * @spec openspec/specs/linked-entity-types/spec.md */ public function getName(): string { @@ -87,7 +88,7 @@ public function getName(): string * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-3 + * @spec openspec/specs/linked-entity-types/spec.md */ public function run(IOutput $output): void { @@ -110,7 +111,7 @@ public function run(IOutput $output): void foreach ($dangling as $row) { $template = '[OpenRegister] Schema "%s" (id=%s) declares linkedType "%s"'; $template .= ' which is not registered. Add the matching IntegrationProvider'; - $template .= ' before the deprecated VALID_LINKED_TYPES fallback is removed.'; + $template .= ' before the legacy linked-type fallback is removed.'; $message = sprintf( $template, $row['slug'], @@ -131,7 +132,7 @@ public function run(IOutput $output): void * * @return array|null * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-3 + * @spec openspec/specs/linked-entity-types/spec.md */ private function loadSchemas(): ?array { @@ -161,7 +162,7 @@ private function loadSchemas(): ?array * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-3 + * @spec openspec/specs/linked-entity-types/spec.md */ private function scan(array $schemas, array $registeredIds): array { @@ -203,7 +204,7 @@ private function scan(array $schemas, array $registeredIds): array * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-3 + * @spec openspec/specs/linked-entity-types/spec.md */ private function extractLinkedTypes($schema): array { @@ -282,7 +283,7 @@ private function extractViaGetConfiguration(object $schema): ?array * * @return string|null * - * @spec openspec/changes/retrofit-2026-05-24-2b-command-repair-middleware/tasks.md#task-3 + * @spec openspec/specs/linked-entity-types/spec.md */ private function safeStringAccessor($schema, array $accessors): ?string { diff --git a/lib/Repair/MigrateNotificationSubscriptionsToUserConfig.php b/lib/Repair/MigrateNotificationSubscriptionsToUserConfig.php index 781196a84a..9fd3a3f5de 100644 --- a/lib/Repair/MigrateNotificationSubscriptionsToUserConfig.php +++ b/lib/Repair/MigrateNotificationSubscriptionsToUserConfig.php @@ -51,7 +51,9 @@ /** * Repair step: migrate legacy notification subscriptions to user-config overrides. * - * @SuppressWarnings(PHPMD.LongClassName) Class name mirrors the IRepairStep convention of describing exactly what the one-shot migration does; abbreviating it would break the self-documenting intent that makes repair steps auditable in occ and the admin UI. + * @SuppressWarnings(PHPMD.LongClassName) Class name mirrors the IRepairStep convention of describing + * exactly what the one-shot migration does; abbreviating it would break the self-documenting intent + * that makes repair steps auditable in occ and the admin UI. */ class MigrateNotificationSubscriptionsToUserConfig implements IRepairStep { @@ -60,6 +62,13 @@ class MigrateNotificationSubscriptionsToUserConfig implements IRepairStep */ private const TABLE = 'openregister_notification_subscriptions'; + /** + * Appconfig marker key. Once set, the migration has already run and must not + * run again — re-running would force enabled:true and resurrect overrides a + * user has since manually disabled (OPS-8). + */ + private const DONE_KEY = 'notification_subscription_migration_done'; + /** * Constructor. * @@ -91,13 +100,29 @@ public function getName(): string * * @return void * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) run() validates table availability, iterates subscription rows, and per row: guards on null userId/schemaId, finds the schema, validates the notification annotation, and loops keys — each check is a defensive guard required for the idempotent migration contract. - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple independent early-returns (table absent, no rows, service unavailable, null userId, null schemaId, schema not found, missing annotation) represent distinct migration-skip conditions, not extractable sub-paths. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) run() validates table availability, iterates + * subscription rows, and per row: guards on null userId/schemaId, finds the schema, validates + * the notification annotation, and loops keys — each check is a defensive guard required for + * the idempotent migration contract. + * @SuppressWarnings(PHPMD.NPathComplexity) Multiple independent early-returns (table absent, no + * rows, service unavailable, null userId, null schemaId, schema not found, missing annotation) + * represent distinct migration-skip conditions, not extractable sub-paths. */ public function run(IOutput $output): void { $output->info('[OpenRegister] Migrating legacy notification subscriptions to user-config...'); + // Run-once guard: if the marker is set the migration already executed on a + // previous upgrade. Re-running would force enabled:true and re-enable + // overrides the user has since manually disabled (OPS-8). + $appConfig = $this->resolveAppConfig(); + if ($appConfig !== null + && $appConfig->getValueBool('openregister', self::DONE_KEY, false) === true + ) { + $output->info('[OpenRegister] Notification subscription migration already completed — skipping.'); + return; + } + $rows = $this->loadSubscriptionRows(); if ($rows === null) { $output->info('[OpenRegister] Subscription table unavailable — migration skipped (normal on first install).'); @@ -106,6 +131,7 @@ public function run(IOutput $output): void if ($rows === []) { $output->info('[OpenRegister] No notification subscriptions to migrate.'); + $this->markDone(appConfig: $appConfig); return; } @@ -155,8 +181,51 @@ public function run(IOutput $output): void }//end foreach $output->info(sprintf('[OpenRegister] Migrated %d notification subscription override(s).', $migrated)); + $this->markDone(appConfig: $appConfig); }//end run() + /** + * Persist the run-once marker so subsequent upgrades skip the migration (OPS-8). + * + * @param \OCP\IAppConfig|null $appConfig The app-config service, or null when unresolvable. + * + * @return void + */ + private function markDone(?\OCP\IAppConfig $appConfig): void + { + if ($appConfig === null) { + return; + } + + try { + $appConfig->setValueBool('openregister', self::DONE_KEY, true); + } catch (\Throwable $e) { + $this->logger->debug( + '[OpenRegister] Could not persist notification-migration marker', + ['exception' => $e] + ); + } + }//end markDone() + + /** + * Lazily resolve the app-config service. + * + * @return \OCP\IAppConfig|null The service, or null when unresolvable. + */ + private function resolveAppConfig(): ?\OCP\IAppConfig + { + try { + $appConfig = $this->container->get(\OCP\IAppConfig::class); + if ($appConfig instanceof \OCP\IAppConfig) { + return $appConfig; + } + + return null; + } catch (\Throwable $e) { + return null; + } + }//end resolveAppConfig() + /** * Read the legacy subscription rows directly. Returns null when the * table cannot be queried (e.g. fresh install before schema creation). diff --git a/lib/Repair/RegisterOpenRegisterWithDoriath.php b/lib/Repair/RegisterOpenRegisterWithDoriath.php new file mode 100644 index 0000000000..584e85cf31 --- /dev/null +++ b/lib/Repair/RegisterOpenRegisterWithDoriath.php @@ -0,0 +1,352 @@ +=4096-bit keys). + * Doriath assigns the application row's UUID; the step persists that UUID and + * OR's public key PEM (public material only) in `IAppConfig`. + * + * NEVER throws (the `ImportCredentialBrokerRegister` convention): when Doriath + * is absent/disabled or anything fails, the step warns and completes — the + * credential broker keeps operating on the Nextcloud-vault leaf. Re-runs skip + * fast when the persisted application UUID still matches a live Doriath row, + * making the step safe on every `occ upgrade` (install + post-migration). + * + * @category Repair + * @package OCA\OpenRegister\Repair + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Service\Credential\DoriathCredentialStore; +use OCP\App\IAppManager; +use OCP\IAppConfig; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use OCP\Security\ICredentialsManager; +use OCP\Server; +use OpenSSLAsymmetricKey; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +/** + * Registers OpenRegister with Doriath (keypair + CSR + EncryptionSuite), idempotently. + * + * @SuppressWarnings(PHPMD.StaticAccess) Doriath's ApplicationService is lazily + * resolved via `OCP\Server::get` behind a `class_exists` guard so OpenRegister + * carries no compile-time dependency on the optional app (design D-A/D-B). + */ +class RegisterOpenRegisterWithDoriath implements IRepairStep +{ + /** + * FQCN of Doriath's application service (registration seam). + * + * @var string + */ + private const APPLICATION_SERVICE = 'OCA\\Doriath\\Service\\ApplicationService'; + + /** + * RSA modulus size for the generated keypair (Doriath requires >= 4096). + * + * @var int + */ + private const KEY_BITS = 4096; + + /** + * The application NAME OpenRegister registers under (Doriath assigns the UUID). + * + * @var string + */ + private const APPLICATION_NAME = 'openregister'; + + /** + * Constructor. + * + * @param IAppManager $appManager Probes whether the doriath app is enabled. + * @param IAppConfig $appConfig Persists the application UUID + public key PEM. + * @param ICredentialsManager $credentialsManager Holds OR's private key (system-scoped). + * @param LoggerInterface $logger Secret-free diagnostics. + * + * @return void + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly IAppConfig $appConfig, + private readonly ICredentialsManager $credentialsManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function getName(): string + { + return 'Register OpenRegister as a Doriath application (credential-broker Doriath custody leaf)'; + }//end getName() + + /** + * Run the self-registration, degrading (warn, never throw) on any failure. + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function run(IOutput $output): void + { + try { + if ($this->isDoriathAvailable() === false) { + $output->warning( + 'Doriath is not installed or enabled — credential broker stays on the Nextcloud-vault leaf.' + ); + return; + } + + if ($this->isRegistrationLive() === true) { + $output->info('OpenRegister is already registered with Doriath — nothing to do.'); + return; + } + + $this->register(output: $output); + } catch (Throwable $e) { + $this->logger->warning('[RegisterOpenRegisterWithDoriath] self-registration failed: '.$e->getMessage()); + $output->warning('Doriath self-registration skipped: '.$e->getMessage()); + } + }//end run() + + /** + * Whether the doriath app is enabled and its registration seam is loadable. + * + * Deliberately narrower than the resolver's full eligibility chain: the + * repair step only needs `ApplicationService`; the seam methods that gate + * the STORE selection are probed by `CredentialStoreResolver` instead, so + * registration can happen before Doriath's `application-secret-delete` + * change lands (cross-repo merge order stays free). + * + * @return bool True when self-registration can be attempted. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function isDoriathAvailable(): bool + { + if ($this->appManager->isEnabledForUser('doriath') === false) { + return false; + } + + return ($this->resolveApplicationService() !== null); + }//end isDoriathAvailable() + + /** + * Whether a previous registration is still live in Doriath (skip-fast probe). + * + * True only when `IAppConfig` holds an application UUID AND Doriath still + * has that row. A stale UUID (Doriath reinstalled/row removed) yields + * false, so the step re-registers with a fresh keypair. + * + * @return bool True when already registered. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function isRegistrationLive(): bool + { + $applicationId = $this->appConfig->getValueString( + 'openregister', + DoriathCredentialStore::APP_CONFIG_APPLICATION_ID, + '' + ); + if ($applicationId === '') { + return false; + } + + $applicationService = $this->resolveApplicationService(); + if ($applicationService === null) { + return false; + } + + try { + // Admin-scoped in-process read; throws when the row no longer exists. + $applicationService->get($applicationId, '', true); + return true; + } catch (Throwable $e) { + $this->logger->info( + '[RegisterOpenRegisterWithDoriath] persisted application UUID is stale — re-registering', + ['error' => $e->getMessage()] + ); + return false; + } + }//end isRegistrationLive() + + /** + * Perform the actual keypair + CSR + register + persist sequence (D-B). + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @throws RuntimeException When openssl key/CSR generation fails (caught by {@see run()}). + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function register(IOutput $output): void + { + [$privatePem, $publicPem, $keyResource] = $this->generateKeypair(); + + // SYSTEM scope (userId = '') — the CredentialAppTokenService idiom. The + // private key exists ONLY here; IAppConfig gets public material only. + $this->credentialsManager->store('', DoriathCredentialStore::PRIVATE_KEY_ID, $privatePem); + + $csrPem = $this->generateCsr(keyResource: $keyResource); + + $applicationService = $this->resolveApplicationService(); + if ($applicationService === null) { + throw new RuntimeException('Doriath ApplicationService unavailable'); + } + + // Admin registration auto-approves the row and provisions the + // EncryptionSuite from the CSR in the same call. userId is null: the + // repair step runs without a session; Doriath audits a system actor. + $application = $applicationService->register( + self::APPLICATION_NAME, + 'OpenRegister credential-broker custody vault (self-registered)', + 'internal', + $csrPem, + null, + true + ); + + // Doriath generates the application row UUID — persist it + our public PEM. + $applicationId = (string) $application->getId(); + $this->appConfig->setValueString( + 'openregister', + DoriathCredentialStore::APP_CONFIG_APPLICATION_ID, + $applicationId + ); + $this->appConfig->setValueString( + 'openregister', + DoriathCredentialStore::APP_CONFIG_PUBLIC_KEY_PEM, + $publicPem + ); + + $output->info('OpenRegister registered with Doriath (application '.$applicationId.', EncryptionSuite provisioned).'); + }//end register() + + /** + * Generate the RSA-4096 keypair, returning both PEMs and the key resource. + * + * @return array{0: string, 1: string, 2: OpenSSLAsymmetricKey} Private PEM, public PEM, key resource. + * + * @throws RuntimeException When openssl generation/export fails. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function generateKeypair(): array + { + $keyResource = openssl_pkey_new( + [ + 'private_key_bits' => self::KEY_BITS, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ] + ); + if ($keyResource === false) { + throw new RuntimeException('RSA keypair generation failed'); + } + + $privatePem = ''; + if (openssl_pkey_export($keyResource, $privatePem) === false || is_string($privatePem) === false) { + throw new RuntimeException('RSA private key export failed'); + } + + $details = openssl_pkey_get_details($keyResource); + if (is_array($details) === false || is_string(($details['key'] ?? null)) === false) { + throw new RuntimeException('RSA public key export failed'); + } + + return [$privatePem, $details['key'], $keyResource]; + }//end generateKeypair() + + /** + * Self-generate a PKCS#10 CSR (CN `openregister`) from the keypair. + * + * @param OpenSSLAsymmetricKey $keyResource The generated private key resource. + * + * @return string The CSR PEM. + * + * @throws RuntimeException When CSR generation/export fails. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function generateCsr(OpenSSLAsymmetricKey $keyResource): string + { + $csr = openssl_csr_new( + ['commonName' => self::APPLICATION_NAME], + $keyResource, + ['digest_alg' => 'sha256'] + ); + if ($csr === false) { + throw new RuntimeException('CSR generation failed'); + } + + $csrPem = ''; + if (openssl_csr_export($csr, $csrPem) === false || is_string($csrPem) === false || $csrPem === '') { + throw new RuntimeException('CSR export failed'); + } + + return $csrPem; + }//end generateCsr() + + /** + * Resolve Doriath's ApplicationService, or null when unavailable. + * + * `class_exists` + `OCP\Server::get` (no compile-time Doriath dependency). + * Protected so unit tests can substitute a contract fake. + * + * @return object|null The resolved service, or null. + * + * @spec openspec/specs/credential-broker/spec.md + */ + protected function resolveApplicationService(): ?object + { + if (class_exists(self::APPLICATION_SERVICE) === false) { + return null; + } + + try { + return Server::get(self::APPLICATION_SERVICE); + } catch (Throwable $e) { + $this->logger->warning( + '[RegisterOpenRegisterWithDoriath] failed to resolve Doriath ApplicationService', + ['error' => $e->getMessage()] + ); + return null; + } + }//end resolveApplicationService() +}//end class diff --git a/lib/Repair/RegisterRiskLevelMetadata.php b/lib/Repair/RegisterRiskLevelMetadata.php index 74b59269e0..d5297d7d44 100644 --- a/lib/Repair/RegisterRiskLevelMetadata.php +++ b/lib/Repair/RegisterRiskLevelMetadata.php @@ -41,7 +41,7 @@ class RegisterRiskLevelMetadata implements IRepairStep * * @param RiskLevelService $riskLevelService Risk level service * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-21 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly RiskLevelService $riskLevelService @@ -53,7 +53,7 @@ public function __construct( * * @return string * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-21 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getName(): string { @@ -67,7 +67,7 @@ public function getName(): string * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-21 + * @spec openspec/specs/object-lifecycle/spec.md */ public function run(IOutput $output): void { diff --git a/lib/Repair/SeedAppVirtualSchemas.php b/lib/Repair/SeedAppVirtualSchemas.php new file mode 100644 index 0000000000..ecd79e4aba --- /dev/null +++ b/lib/Repair/SeedAppVirtualSchemas.php @@ -0,0 +1,305 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\ObjectSource\NcEntitySemanticMap; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Seeds the app-gated virtual registers with their NC-entity schemas. + */ +class SeedAppVirtualSchemas implements IRepairStep +{ + + /** + * Read-only minimal property sets per virtual schema slug. + * + * @var array> + */ + private const SCHEMA_PROPERTIES = [ + 'nc-contact' => [ + 'id' => ['type' => 'string', 'title' => 'Contact ID', 'description' => 'The Nextcloud contact UID.'], + 'fullName' => ['type' => 'string', 'title' => 'Full name', 'description' => 'The contact display name (vCard FN).'], + 'email' => ['type' => 'string', 'title' => 'Email', 'description' => 'The contact primary email address.'], + 'org' => ['type' => 'string', 'title' => 'Organization', 'description' => 'The contact organization (vCard ORG).'], + ], + 'nc-event' => [ + 'id' => ['type' => 'string', 'title' => 'Event ID', 'description' => 'The calendar event UID.'], + 'summary' => ['type' => 'string', 'title' => 'Summary', 'description' => 'The event title (VEVENT SUMMARY).'], + 'startDate' => ['type' => 'string', 'title' => 'Start date', 'description' => 'The event start (ISO-8601).'], + 'endDate' => ['type' => 'string', 'title' => 'End date', 'description' => 'The event end (ISO-8601).'], + 'location' => ['type' => 'string', 'title' => 'Location', 'description' => 'The event location.'], + ], + 'nc-file' => [ + 'id' => ['type' => 'string', 'title' => 'File ID', 'description' => 'The Nextcloud file id (fileid).'], + 'name' => ['type' => 'string', 'title' => 'Name', 'description' => 'The file name.'], + 'path' => ['type' => 'string', 'title' => 'Path', 'description' => 'The user-relative file path.'], + 'mimetype' => ['type' => 'string', 'title' => 'MIME type', 'description' => 'The file MIME type.'], + 'size' => ['type' => 'integer', 'title' => 'Size', 'description' => 'The file size in bytes.'], + 'mtime' => ['type' => 'integer', 'title' => 'Modified time', 'description' => 'The file modification time (Unix timestamp).'], + ], + 'nc-card' => [ + 'id' => ['type' => 'string', 'title' => 'Card ID', 'description' => 'The Nextcloud Deck card id.'], + 'title' => ['type' => 'string', 'title' => 'Title', 'description' => 'The card title.'], + 'description' => ['type' => 'string', 'title' => 'Description', 'description' => 'The card description (markdown).'], + 'stackId' => ['type' => 'integer', 'title' => 'Stack ID', 'description' => 'The id of the stack (list) the card lives in.'], + 'boardId' => ['type' => 'integer', 'title' => 'Board ID', 'description' => 'The id of the board the card lives on.'], + 'duedate' => ['type' => 'string', 'title' => 'Due date', 'description' => 'The card due date (ISO-8601), or null.'], + ], + 'nc-conversation' => [ + 'id' => ['type' => 'string', 'title' => 'Conversation ID', 'description' => 'The Talk room token.'], + 'name' => ['type' => 'string', 'title' => 'Name', 'description' => 'The raw conversation name.'], + 'displayName' => ['type' => 'string', 'title' => 'Display name', 'description' => 'The display name for the acting user.'], + 'type' => ['type' => 'integer', 'title' => 'Type', 'description' => 'The Talk room type (1=one-to-one, 2=group, 3=public).'], + 'participantCount' => ['type' => 'integer', 'title' => 'Participants', 'description' => 'The participant count.'], + 'lastActivity' => ['type' => 'string', 'title' => 'Last activity', 'description' => 'The last activity (ISO-8601), or null.'], + ], + 'nc-task' => [ + 'id' => ['type' => 'string', 'title' => 'Task ID', 'description' => 'The CalDAV VTODO uid.'], + 'title' => ['type' => 'string', 'title' => 'Title', 'description' => 'The task title (VTODO SUMMARY).'], + 'description' => ['type' => 'string', 'title' => 'Description', 'description' => 'The task description (VTODO DESCRIPTION).'], + 'status' => ['type' => 'string', 'title' => 'Status', 'description' => 'The task status (VTODO STATUS).'], + 'dueDate' => ['type' => 'string', 'title' => 'Due date', 'description' => 'The task due date (VTODO DUE), or null.'], + 'completed' => ['type' => 'string', 'title' => 'Completed', 'description' => 'The completion timestamp (VTODO COMPLETED), or null.'], + 'priority' => ['type' => 'integer', 'title' => 'Priority', 'description' => 'The task priority (VTODO PRIORITY), or null.'], + ], + ]; + + /** + * Human-readable register descriptions keyed by register slug. + * + * @var array + */ + private const REGISTER_DESCRIPTIONS = [ + 'contacts' => 'Read-only virtual register projecting the Nextcloud Contacts app\'s contacts as OpenRegister objects.', + 'calendar' => 'Read-only virtual register projecting the Nextcloud Calendar app\'s events as OpenRegister objects.', + 'files' => 'Read-only virtual register projecting the acting user\'s Nextcloud files as OpenRegister objects.', + 'deck' => 'Read-only virtual register projecting the Nextcloud Deck app\'s cards as OpenRegister objects.', + 'talk' => 'Read-only virtual register projecting the Nextcloud Talk app\'s conversations as OpenRegister objects.', + 'tasks' => 'Read-only virtual register projecting the acting user\'s CalDAV tasks (VTODOs) as OpenRegister objects.', + ]; + + /** + * Per-schema extras merged into the `x-openregister-object-source` config. + * + * `nc-task` reuses the shared `caldav-vtodo` provider whose default scope only + * surfaces VTODOs linked (via X-OPENREGISTER) to the bound register/schema. As + * a projection of the acting user's whole task list it opts out of that scoping. + * + * @var array> + */ + private const SCHEMA_SOURCE_CONFIG = [ + 'nc-task' => ['unscoped' => true], + ]; + + /** + * Constructor. + * + * @param RegisterMapper $registerMapper Register data mapper. + * @param SchemaMapper $schemaMapper Schema data mapper. + * @param LoggerInterface $logger Logger for seed diagnostics. + * + * @return void + */ + public function __construct( + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + public function getName(): string + { + return 'Seed OpenRegister app virtual schemas (nc-contact, nc-event, nc-file, nc-card, nc-conversation, nc-task)'; + }//end getName() + + /** + * Run the repair step, seeding each app-gated register + its schemas. + * + * Never throws: a seed failure logs a warning and leaves the instance + * otherwise healthy (the providers simply have no bound schema to serve). + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + public function run(IOutput $output): void + { + try { + // Group the app-gated rows (register !== directory) by their register + // slug so each register is created once with all of its schemas. + $rowsByRegister = []; + foreach (NcEntitySemanticMap::ENTITIES as $row) { + if ($row['register'] === NcEntitySemanticMap::DIRECTORY_REGISTER) { + continue; + } + + // The `tables` register is NOT a single-schema projection: its + // per-table schemas are auto-seeded by SeedTablesVirtualSchemas / + // `occ openregister:tables:sync` (design D7). The semantic-map + // `tables` row only records the provider + app gate, so it must + // not be materialised into a nominal schema here. + if ($row['register'] === 'tables') { + continue; + } + + $rowsByRegister[$row['register']][] = $row; + } + + foreach ($rowsByRegister as $registerSlug => $rows) { + $register = $this->ensureRegister(slug: $registerSlug, application: $rows[0]['application']); + $schemaIds = $register->getSchemas(); + $changed = false; + + foreach ($rows as $row) { + $schema = $this->ensureSchema(row: $row); + if (in_array($schema->getId(), $schemaIds, false) === false) { + $schemaIds[] = $schema->getId(); + $changed = true; + } + + $output->info(sprintf('App schema "%s" (id %s) ready', $row['schema'], (string) $schema->getId())); + } + + if ($changed === true) { + $register->setSchemas($schemaIds); + $this->registerMapper->update($register); + } + + $output->info(sprintf('App virtual register "%s" seeded', $registerSlug)); + }//end foreach + } catch (Throwable $e) { + $this->logger->warning('[SeedAppVirtualSchemas] seed failed: '.$e->getMessage()); + $output->warning('App virtual schema seed skipped: '.$e->getMessage()); + }//end try + }//end run() + + /** + * Find or create one app-gated virtual register. + * + * @param string $slug The register slug (e.g. `contacts`). + * @param string $application The register application (drives the ADR-048 gate). + * + * @return Register The existing or newly created register. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + private function ensureRegister(string $slug, string $application): Register + { + try { + return $this->registerMapper->find($slug, _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + // Not found — create it. + return $this->registerMapper->createFromArray( + object: [ + 'title' => ucfirst($slug), + 'slug' => $slug, + 'description' => (self::REGISTER_DESCRIPTIONS[$slug] ?? sprintf('Read-only virtual register for Nextcloud %s.', $slug)), + 'application' => $application, + 'schemas' => [], + ] + ); + }//end try + }//end ensureRegister() + + /** + * Find or create one virtual schema for a semantic-map row. + * + * @param array{schema: string, schemaOrg: string, provider: string} $row The semantic-map row (only the schema fields are read here). + * + * @return Schema The existing or newly created schema. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + private function ensureSchema(array $row): Schema + { + try { + return $this->schemaMapper->find($row['schema'], _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + // Not found — create it as a read-only object-source-backed schema. + $properties = (self::SCHEMA_PROPERTIES[$row['schema']] ?? []); + + $objectSource = [ + 'provider' => $row['provider'], + 'readOnly' => true, + ]; + + // Per-schema provider `config` extras (threaded to the provider's + // find/findAll `$config` argument by GetObject). `nc-task` reuses the + // shared `caldav-vtodo` provider, which by default only surfaces VTODOs + // whose X-OPENREGISTER link points at the bound register/schema. As a + // virtual projection of ALL of the acting user's tasks, `nc-task` opts + // out of that link scoping with `config.unscoped`. + $providerConfig = (self::SCHEMA_SOURCE_CONFIG[$row['schema']] ?? []); + if (empty($providerConfig) === false) { + $objectSource['config'] = $providerConfig; + } + + return $this->schemaMapper->createFromArray( + object: [ + 'title' => $row['schema'], + 'slug' => $row['schema'], + 'description' => sprintf('Read-only virtual schema for Nextcloud %s (%s).', $row['schema'], $row['schemaOrg']), + 'properties' => $properties, + 'configuration' => [ + 'x-schema-org' => $row['schemaOrg'], + 'x-openregister-object-source' => $objectSource, + ], + ] + ); + }//end try + }//end ensureSchema() +}//end class diff --git a/lib/Repair/SeedDirectoryVirtualSchemas.php b/lib/Repair/SeedDirectoryVirtualSchemas.php new file mode 100644 index 0000000000..21ac4340ce --- /dev/null +++ b/lib/Repair/SeedDirectoryVirtualSchemas.php @@ -0,0 +1,209 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\ObjectSource\NcEntitySemanticMap; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Seeds the virtual `directory` register with the core NC-entity schemas. + */ +class SeedDirectoryVirtualSchemas implements IRepairStep +{ + + /** + * Read-only minimal property sets per virtual schema slug. + * + * @var array> + */ + private const SCHEMA_PROPERTIES = [ + 'nc-user' => [ + 'id' => ['type' => 'string', 'title' => 'User ID', 'description' => 'The Nextcloud user id (uid).'], + 'displayName' => ['type' => 'string', 'title' => 'Display name', 'description' => 'The user display name.'], + 'email' => ['type' => 'string', 'title' => 'Email', 'description' => 'The user email address.'], + ], + 'nc-group' => [ + 'id' => ['type' => 'string', 'title' => 'Group ID', 'description' => 'The Nextcloud group id (gid).'], + 'displayName' => ['type' => 'string', 'title' => 'Display name', 'description' => 'The group display name.'], + ], + ]; + + /** + * Constructor. + * + * @param RegisterMapper $registerMapper Register data mapper. + * @param SchemaMapper $schemaMapper Schema data mapper. + * @param LoggerInterface $logger Logger for seed diagnostics. + * + * @return void + */ + public function __construct( + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + public function getName(): string + { + return 'Seed OpenRegister directory virtual schemas (nc-user, nc-group)'; + }//end getName() + + /** + * Run the repair step, seeding the directory register + core schemas. + * + * Never throws: a seed failure logs a warning and leaves the instance + * otherwise healthy (the providers simply have no bound schema to serve). + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + public function run(IOutput $output): void + { + try { + $register = $this->ensureRegister(); + $schemaIds = $register->getSchemas(); + $changed = false; + + foreach (NcEntitySemanticMap::ENTITIES as $row) { + // This step only owns the core rows that live on the always-available + // `directory` register (all of which are Nextcloud-core, requiredApp + // null); app-gated rows on their own app-named registers are seeded by + // SeedAppVirtualSchemas. + if ($row['register'] !== NcEntitySemanticMap::DIRECTORY_REGISTER) { + continue; + } + + $schema = $this->ensureSchema(row: $row); + if (in_array($schema->getId(), $schemaIds, false) === false) { + $schemaIds[] = $schema->getId(); + $changed = true; + } + + $output->info(sprintf('Directory schema "%s" (id %s) ready', $row['schema'], (string) $schema->getId())); + }//end foreach + + if ($changed === true) { + $register->setSchemas($schemaIds); + $this->registerMapper->update($register); + } + + $output->info('Directory virtual register seeded'); + } catch (Throwable $e) { + $this->logger->warning('[SeedDirectoryVirtualSchemas] seed failed: '.$e->getMessage()); + $output->warning('Directory virtual schema seed skipped: '.$e->getMessage()); + }//end try + }//end run() + + /** + * Find or create the `directory` virtual register (application: openregister). + * + * @return Register The existing or newly created register. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + private function ensureRegister(): Register + { + try { + return $this->registerMapper->find(NcEntitySemanticMap::DIRECTORY_REGISTER, _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + // Not found — create it. + return $this->registerMapper->createFromArray( + object: [ + 'title' => 'Directory', + 'slug' => NcEntitySemanticMap::DIRECTORY_REGISTER, + 'description' => 'Read-only virtual register projecting Nextcloud directory entities (users, groups) as OpenRegister objects.', + 'application' => 'openregister', + 'schemas' => [], + ] + ); + }//end try + }//end ensureRegister() + + /** + * Find or create one virtual schema for a semantic-map row. + * + * @param array{register: string, schema: string, schemaOrg: string, provider: string, requiredApp: string|null} $row The semantic-map row. + * + * @return Schema The existing or newly created schema. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.2 + */ + private function ensureSchema(array $row): Schema + { + try { + return $this->schemaMapper->find($row['schema'], _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + // Not found — create it as a read-only object-source-backed schema. + $properties = (self::SCHEMA_PROPERTIES[$row['schema']] ?? []); + + return $this->schemaMapper->createFromArray( + object: [ + 'title' => $row['schema'], + 'slug' => $row['schema'], + 'description' => sprintf('Read-only virtual schema for Nextcloud %s (%s).', $row['schema'], $row['schemaOrg']), + 'properties' => $properties, + 'configuration' => [ + 'x-schema-org' => $row['schemaOrg'], + 'x-openregister-object-source' => [ + 'provider' => $row['provider'], + 'readOnly' => true, + ], + ], + ] + ); + }//end try + }//end ensureSchema() +}//end class diff --git a/lib/Repair/SeedTablesVirtualSchemas.php b/lib/Repair/SeedTablesVirtualSchemas.php new file mode 100644 index 0000000000..7fc91bb0db --- /dev/null +++ b/lib/Repair/SeedTablesVirtualSchemas.php @@ -0,0 +1,135 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Service\ObjectSource\TablesSchemaSyncService; +use OCA\OpenRegister\Service\ObjectSource\TablesTableReader; +use OCP\IGroupManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Seeds/reconciles the `tables` virtual register on install/upgrade. + */ +class SeedTablesVirtualSchemas implements IRepairStep +{ + /** + * Constructor. + * + * @param TablesTableReader $reader Guarded gateway to Tables services. + * @param TablesSchemaSyncService $syncService Schema reconcile logic. + * @param IGroupManager $groupManager Admin-user enumeration. + * @param LoggerInterface $logger Logger for seed diagnostics. + * + * @return void + */ + public function __construct( + private readonly TablesTableReader $reader, + private readonly TablesSchemaSyncService $syncService, + private readonly IGroupManager $groupManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function getName(): string + { + return 'Seed OpenRegister Tables virtual schemas (one per Nextcloud Tables table)'; + }//end getName() + + /** + * Run the repair step, reconciling the `tables` register. + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function run(IOutput $output): void + { + try { + if ($this->reader->isAvailable() === false) { + $output->info('Tables app not enabled — skipping Tables virtual schema seed'); + return; + } + + $adminIds = $this->adminUserIds(); + $tables = $this->reader->collectTableDescriptors(userIds: $adminIds); + $stats = $this->syncService->reconcile(tables: $tables); + + $output->info( + sprintf( + 'Tables virtual schemas reconciled: seeded=%d, retired=%d, skipped=%d', + $stats['seeded'], + $stats['retired'], + $stats['skipped'] + ) + ); + } catch (Throwable $e) { + $this->logger->warning('[SeedTablesVirtualSchemas] seed failed: '.$e->getMessage()); + $output->warning('Tables virtual schema seed skipped: '.$e->getMessage()); + }//end try + }//end run() + + /** + * Resolve the instance's admin user ids for table enumeration. + * + * @return array The admin user ids (may be empty). + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function adminUserIds(): array + { + $admins = $this->groupManager->get('admin'); + if ($admins === null) { + return []; + } + + $ids = []; + foreach ($admins->getUsers() as $user) { + $ids[] = $user->getUID(); + } + + return $ids; + }//end adminUserIds() +}//end class diff --git a/lib/Repair/SeedZgwZakenMigrationPack.php b/lib/Repair/SeedZgwZakenMigrationPack.php new file mode 100644 index 0000000000..24f1ac232c --- /dev/null +++ b/lib/Repair/SeedZgwZakenMigrationPack.php @@ -0,0 +1,186 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Repair; + +use OCA\OpenRegister\Service\MigrationPackService; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Seeds the built-in `zgw-zaken-json` reference migration pack. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ +class SeedZgwZakenMigrationPack implements IRepairStep +{ + /** + * Constructor. + * + * @param MigrationPackService $migrationPackService Migration pack business logic. + * @param LoggerInterface $logger Logger for seed diagnostics. + * + * @return void + */ + public function __construct( + private readonly MigrationPackService $migrationPackService, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Get the name of this repair step. + * + * @return string The step name. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function getName(): string + { + return 'Seed OpenRegister built-in reference migration pack (zgw-zaken-json)'; + }//end getName() + + /** + * Run the repair step, seeding the pack when absent. + * + * Never throws: a seed failure logs a warning and leaves the instance + * otherwise healthy. + * + * @param IOutput $output Output interface for status messages. + * + * @return void + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function run(IOutput $output): void + { + try { + $this->migrationPackService->findByPackSlug(packSlug: 'zgw-zaken-json'); + $output->info('Migration pack "zgw-zaken-json" already present, skipping seed'); + return; + } catch (Throwable $e) { + // Not found (or lookup failed for another reason) — attempt to seed below. + } + + try { + $this->migrationPackService->create(definition: $this->definition(), ownerUid: null, builtin: true); + $output->info('Seeded built-in migration pack "zgw-zaken-json"'); + } catch (Throwable $e) { + $this->logger->warning('[SeedZgwZakenMigrationPack] seed failed: '.$e->getMessage()); + $output->warning('Migration pack seed skipped: '.$e->getMessage()); + } + }//end run() + + /** + * The built-in `zgw-zaken-json` pack definition. + * + * @return array + */ + private function definition(): array + { + return [ + 'id' => 'zgw-zaken-json', + 'name' => 'ZGW Zaken (JSON) — template', + 'description' => 'TEMPLATE, not a ready-to-run pack. Maps a ZGW ("Zaakgericht Werken") Zaken API ' + .'export (JSON) onto a generic case-like target schema. Before running a real import: (1) select or ' + .'create a target schema whose properties match the mapping targets below (caseNumber, title, ' + .'startDate, endDate, status, zaakTypeCode, confidentialityLevel), or edit the targets to match your ' + .'own schema; (2) replace the placeholder zaaktype-URL lookup map with your own catalogue\'s zaaktype ' + .'URL to code mapping — rows whose zaaktype URL is not in the map will fail import by design (the ' + .'mapping engine never passes an unresolved reference through as a literal value).', + 'sourceFormat' => 'json', + 'version' => '1.0.0', + 'idStrategy' => ['type' => 'generate'], + 'fieldMappings' => [ + [ + 'source' => '/identificatie', + 'target' => 'caseNumber', + 'required' => true, + 'transform' => ['type' => 'trim'], + ], + [ + 'source' => '/omschrijving', + 'target' => 'title', + 'required' => true, + 'transform' => ['type' => 'trim'], + ], + [ + 'source' => '/startdatum', + 'target' => 'startDate', + 'required' => false, + 'transform' => ['type' => 'date', 'sourceFormat' => 'Y-m-d', 'targetFormat' => 'Y-m-d'], + ], + [ + 'source' => '/einddatum', + 'target' => 'endDate', + 'required' => false, + 'transform' => ['type' => 'date', 'sourceFormat' => 'Y-m-d', 'targetFormat' => 'Y-m-d'], + ], + [ + 'source' => '/status', + 'target' => 'status', + 'required' => false, + 'transform' => ['type' => 'trim'], + ], + [ + 'source' => '/zaaktype', + 'target' => 'zaakTypeCode', + 'required' => false, + 'transform' => [ + 'type' => 'lookup', + 'map' => [ + 'https://example.com/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000' => 'VOORBEELD', + ], + ], + ], + [ + 'source' => '/vertrouwelijkheidaanduiding', + 'target' => 'confidentialityLevel', + 'required' => false, + 'transform' => ['type' => 'trim'], + ], + ], + 'defaults' => [], + 'skipRows' => [], + ]; + }//end definition() +}//end class diff --git a/lib/Resources/ggm/ggm-snapshot.json b/lib/Resources/ggm/ggm-snapshot.json new file mode 100644 index 0000000000..114cbdcdc6 --- /dev/null +++ b/lib/Resources/ggm/ggm-snapshot.json @@ -0,0 +1,105 @@ +{ + "standard": "ggm", + "version": "ggm-2.2.0", + "objecttypen": [ + { + "id": "INGESCHREVEN_PERSOON", + "naam": "Ingeschreven persoon", + "definitie": "Een natuurlijk persoon die is ingeschreven in de Basisregistratie Personen van een gemeente.", + "attribuutsoorten": [ + { + "naam": "burgerservicenummer", + "definitie": "Het burgerservicenummer (BSN) van de ingeschreven persoon.", + "type": "tekst" + }, + { + "naam": "geslachtsnaam", + "definitie": "De officiële geslachtsnaam van de persoon.", + "type": "tekst" + }, + { + "naam": "voornamen", + "definitie": "De voornamen van de persoon.", + "type": "tekst" + }, + { + "naam": "geboortedatum", + "definitie": "De datum waarop de persoon is geboren.", + "type": "datum" + }, + { + "naam": "aanduidingNaamgebruik", + "definitie": "De wijze waarop de naam van de persoon wordt gebruikt.", + "type": "tekst", + "referentielijst": { + "naam": "Tabel naamgebruik", + "waarden": ["eigen", "partner", "partner_eigen", "eigen_partner"] + } + }, + { + "naam": "verblijfadres", + "definitie": "Het adres waarop de persoon staat ingeschreven.", + "type": "relatie", + "doelObjecttype": "NUMMERAANDUIDING" + } + ] + }, + { + "id": "ZAAK", + "naam": "Zaak", + "definitie": "Een samenhangende hoeveelheid werk met een gedefinieerde aanleiding en een gedefinieerd resultaat.", + "attribuutsoorten": [ + { + "naam": "identificatie", + "definitie": "De unieke identificatie van de zaak binnen de gemeente.", + "type": "tekst" + }, + { + "naam": "omschrijving", + "definitie": "Een korte omschrijving van de zaak.", + "type": "tekst" + }, + { + "naam": "startdatum", + "definitie": "De datum waarop de uitvoering van de zaak is gestart.", + "type": "datum" + }, + { + "naam": "registratiedatum", + "definitie": "Het tijdstip waarop de zaak is geregistreerd.", + "type": "datumtijd" + }, + { + "naam": "vertrouwelijkheidaanduiding", + "definitie": "Aanduiding van de mate waarin de zaak openbaar is.", + "type": "tekst", + "referentielijst": { + "naam": "Tabel vertrouwelijkheid", + "waarden": ["openbaar", "intern", "vertrouwelijk", "zeer_geheim"] + } + }, + { + "naam": "doorlooptijd", + "definitie": "De wettelijke doorlooptijd van de zaak in dagen.", + "type": "geheel getal" + }, + { + "naam": "kosten", + "definitie": "De aan de zaak verbonden kosten in euro's.", + "type": "decimaal" + }, + { + "naam": "betaaldHeffing", + "definitie": "Geeft aan of de heffing voor de zaak is betaald.", + "type": "boolean" + }, + { + "naam": "heeftBetrekkingOp", + "definitie": "Het object waarop de zaak betrekking heeft.", + "type": "relatie", + "doelObjecttype": "INGESCHREVEN_PERSOON" + } + ] + } + ] +} diff --git a/lib/Resources/ggm/version.json b/lib/Resources/ggm/version.json new file mode 100644 index 0000000000..047fab7c7d --- /dev/null +++ b/lib/Resources/ggm/version.json @@ -0,0 +1,7 @@ +{ + "standard": "ggm", + "version": "ggm-2.2.0", + "release": "2.2.0", + "source": "https://www.gemmaonline.nl/index.php/Gegevensmodellen / VNG Gemeentelijk Gegevensmodel (GGM)", + "note": "Normalised intermediate generated from the published GGM release by tools/generate-ggm-snapshot. Dutch names/definitions preserved verbatim. Refresh by re-running the generation script against a newer GGM export." +} diff --git a/lib/Resources/schemaorg/schemaorg-current-https.jsonld b/lib/Resources/schemaorg/schemaorg-current-https.jsonld new file mode 100644 index 0000000000..b06758d75b --- /dev/null +++ b/lib/Resources/schemaorg/schemaorg-current-https.jsonld @@ -0,0 +1,274 @@ +{ + "@context": { + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "schema": "https://schema.org/" + }, + "@graph": [ + { + "@id": "https://schema.org/Thing", + "@type": "rdfs:Class", + "rdfs:label": "Thing", + "rdfs:comment": "The most generic type of item." + }, + { + "@id": "https://schema.org/Person", + "@type": "rdfs:Class", + "rdfs:label": "Person", + "rdfs:comment": "A person (alive, dead, undead, or fictional).", + "rdfs:subClassOf": { "@id": "https://schema.org/Thing" } + }, + { + "@id": "https://schema.org/Organization", + "@type": "rdfs:Class", + "rdfs:label": "Organization", + "rdfs:comment": "An organization such as a school, NGO, corporation, club, etc.", + "rdfs:subClassOf": { "@id": "https://schema.org/Thing" } + }, + { + "@id": "https://schema.org/Place", + "@type": "rdfs:Class", + "rdfs:label": "Place", + "rdfs:comment": "Entities that have a somewhat fixed, physical extension.", + "rdfs:subClassOf": { "@id": "https://schema.org/Thing" } + }, + { + "@id": "https://schema.org/PostalAddress", + "@type": "rdfs:Class", + "rdfs:label": "PostalAddress", + "rdfs:comment": "The mailing address.", + "rdfs:subClassOf": { "@id": "https://schema.org/Thing" } + }, + { + "@id": "https://schema.org/ContactPoint", + "@type": "rdfs:Class", + "rdfs:label": "ContactPoint", + "rdfs:comment": "A contact point—for example, a Customer Complaints department.", + "rdfs:subClassOf": { "@id": "https://schema.org/Thing" } + }, + { + "@id": "https://schema.org/CreativeWork", + "@type": "rdfs:Class", + "rdfs:label": "CreativeWork", + "rdfs:comment": "The most generic kind of creative work, including books, movies, photographs, software programs, etc.", + "rdfs:subClassOf": { "@id": "https://schema.org/Thing" } + }, + { + "@id": "https://schema.org/Event", + "@type": "rdfs:Class", + "rdfs:label": "Event", + "rdfs:comment": "An event happening at a certain time and location.", + "rdfs:subClassOf": { "@id": "https://schema.org/Thing" } + }, + + { + "@id": "https://schema.org/name", + "@type": "rdf:Property", + "rdfs:label": "name", + "rdfs:comment": "The name of the item.", + "schema:domainIncludes": { "@id": "https://schema.org/Thing" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/description", + "@type": "rdf:Property", + "rdfs:label": "description", + "rdfs:comment": "A description of the item.", + "schema:domainIncludes": { "@id": "https://schema.org/Thing" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/url", + "@type": "rdf:Property", + "rdfs:label": "url", + "rdfs:comment": "URL of the item.", + "schema:domainIncludes": { "@id": "https://schema.org/Thing" }, + "schema:rangeIncludes": { "@id": "https://schema.org/URL" } + }, + { + "@id": "https://schema.org/identifier", + "@type": "rdf:Property", + "rdfs:label": "identifier", + "rdfs:comment": "The identifier property represents any kind of identifier.", + "schema:domainIncludes": { "@id": "https://schema.org/Thing" }, + "schema:rangeIncludes": [ + { "@id": "https://schema.org/Text" }, + { "@id": "https://schema.org/URL" } + ] + }, + + { + "@id": "https://schema.org/givenName", + "@type": "rdf:Property", + "rdfs:label": "givenName", + "rdfs:comment": "Given name. In the U.S., the first name of a Person.", + "schema:domainIncludes": { "@id": "https://schema.org/Person" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/familyName", + "@type": "rdf:Property", + "rdfs:label": "familyName", + "rdfs:comment": "Family name. In the U.S., the last name of a Person.", + "schema:domainIncludes": { "@id": "https://schema.org/Person" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/additionalName", + "@type": "rdf:Property", + "rdfs:label": "additionalName", + "rdfs:comment": "An additional name for a Person, can be used for a middle name.", + "schema:domainIncludes": { "@id": "https://schema.org/Person" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/email", + "@type": "rdf:Property", + "rdfs:label": "email", + "rdfs:comment": "Email address.", + "schema:domainIncludes": [ + { "@id": "https://schema.org/Person" }, + { "@id": "https://schema.org/Organization" }, + { "@id": "https://schema.org/ContactPoint" } + ], + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/telephone", + "@type": "rdf:Property", + "rdfs:label": "telephone", + "rdfs:comment": "The telephone number.", + "schema:domainIncludes": [ + { "@id": "https://schema.org/Person" }, + { "@id": "https://schema.org/Organization" }, + { "@id": "https://schema.org/ContactPoint" }, + { "@id": "https://schema.org/Place" } + ], + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/birthDate", + "@type": "rdf:Property", + "rdfs:label": "birthDate", + "rdfs:comment": "Date of birth.", + "schema:domainIncludes": { "@id": "https://schema.org/Person" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Date" } + }, + { + "@id": "https://schema.org/gender", + "@type": "rdf:Property", + "rdfs:label": "gender", + "rdfs:comment": "Gender of something, typically a Person.", + "schema:domainIncludes": { "@id": "https://schema.org/Person" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/address", + "@type": "rdf:Property", + "rdfs:label": "address", + "rdfs:comment": "Physical address of the item.", + "schema:domainIncludes": [ + { "@id": "https://schema.org/Person" }, + { "@id": "https://schema.org/Organization" }, + { "@id": "https://schema.org/Place" } + ], + "schema:rangeIncludes": [ + { "@id": "https://schema.org/PostalAddress" }, + { "@id": "https://schema.org/Text" } + ] + }, + + { + "@id": "https://schema.org/legalName", + "@type": "rdf:Property", + "rdfs:label": "legalName", + "rdfs:comment": "The official name of the organization, e.g. the registered company name.", + "schema:domainIncludes": { "@id": "https://schema.org/Organization" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/numberOfEmployees", + "@type": "rdf:Property", + "rdfs:label": "numberOfEmployees", + "rdfs:comment": "The number of employees in an organization, e.g. business.", + "schema:domainIncludes": { "@id": "https://schema.org/Organization" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Integer" } + }, + { + "@id": "https://schema.org/founder", + "@type": "rdf:Property", + "rdfs:label": "founder", + "rdfs:comment": "A person who founded this organization.", + "schema:domainIncludes": { "@id": "https://schema.org/Organization" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Person" } + }, + + { + "@id": "https://schema.org/streetAddress", + "@type": "rdf:Property", + "rdfs:label": "streetAddress", + "rdfs:comment": "The street address.", + "schema:domainIncludes": { "@id": "https://schema.org/PostalAddress" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/postalCode", + "@type": "rdf:Property", + "rdfs:label": "postalCode", + "rdfs:comment": "The postal code.", + "schema:domainIncludes": { "@id": "https://schema.org/PostalAddress" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/addressLocality", + "@type": "rdf:Property", + "rdfs:label": "addressLocality", + "rdfs:comment": "The locality in which the street address is, and which is in the region.", + "schema:domainIncludes": { "@id": "https://schema.org/PostalAddress" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + { + "@id": "https://schema.org/addressCountry", + "@type": "rdf:Property", + "rdfs:label": "addressCountry", + "rdfs:comment": "The country.", + "schema:domainIncludes": { "@id": "https://schema.org/PostalAddress" }, + "schema:rangeIncludes": { "@id": "https://schema.org/Text" } + }, + + { + "@id": "https://schema.org/startDate", + "@type": "rdf:Property", + "rdfs:label": "startDate", + "rdfs:comment": "The start date and time of the item.", + "schema:domainIncludes": { "@id": "https://schema.org/Event" }, + "schema:rangeIncludes": [ + { "@id": "https://schema.org/Date" }, + { "@id": "https://schema.org/DateTime" } + ] + }, + { + "@id": "https://schema.org/endDate", + "@type": "rdf:Property", + "rdfs:label": "endDate", + "rdfs:comment": "The end date and time of the item.", + "schema:domainIncludes": { "@id": "https://schema.org/Event" }, + "schema:rangeIncludes": [ + { "@id": "https://schema.org/Date" }, + { "@id": "https://schema.org/DateTime" } + ] + }, + { + "@id": "https://schema.org/location", + "@type": "rdf:Property", + "rdfs:label": "location", + "rdfs:comment": "The location of, for example, where an event is happening.", + "schema:domainIncludes": { "@id": "https://schema.org/Event" }, + "schema:rangeIncludes": [ + { "@id": "https://schema.org/Place" }, + { "@id": "https://schema.org/PostalAddress" }, + { "@id": "https://schema.org/Text" } + ] + } + ] +} diff --git a/lib/Resources/schemaorg/version.json b/lib/Resources/schemaorg/version.json new file mode 100644 index 0000000000..b67133cb6a --- /dev/null +++ b/lib/Resources/schemaorg/version.json @@ -0,0 +1,7 @@ +{ + "standard": "schema.org", + "version": "schemaorg-current-https-27.01", + "release": "27.01", + "source": "https://schema.org/version/27.01/schemaorg-current-https.jsonld", + "note": "Curated subset of the official schemaorg-current-https JSON-LD release, sufficient for the common open-data types (Thing, Person, Organization, Place, PostalAddress, ContactPoint, CreativeWork, Event). Refresh via tools/generate-schemaorg-snapshot against the full upstream release." +} diff --git a/lib/Search/ObjectsProvider.php b/lib/Search/ObjectsProvider.php index 12c23ccd0e..b189bde8fc 100644 --- a/lib/Search/ObjectsProvider.php +++ b/lib/Search/ObjectsProvider.php @@ -19,7 +19,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-view-based-search-composition */ declare(strict_types=1); @@ -29,6 +29,7 @@ use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Service\DeepLinkRegistryService; +use OCA\OpenRegister\Service\MdiIconRenderer; use OCA\OpenRegister\Service\ObjectService; use OCP\IL10N; use OCP\IURLGenerator; @@ -43,15 +44,53 @@ /** * ObjectsProvider class for the objects search. * - * This class implements the IFilteringProvider interface to provide - * search functionality for objects in the OpenRegister app using the - * advanced searchObjectsPaginated method for optimal performance. + * This class is the single, fleet-wide Nextcloud unified-search provider + * (id `openregister_objects`) over OpenRegister objects. Leaf apps do NOT + * register their own OCP\Search\IProvider; they participate by claiming + * (register, schema) pairs through the deep-link registry, which supplies + * result URLs, icons, and display names. + * + * SECURITY CONTRACT — the provider performs NO second access filter. All + * RBAC scoping, tenant isolation, the published predicate, and row/field + * level security are enforced inside the OR search pipeline, by always + * delegating to ObjectService::searchObjectsPaginated(query, _rbac: true, + * _multitenancy: true). The provider only narrows the result set further + * (never widens it): it constrains the query to schemas flagged + * `searchable = true`. Excerpts are derived exclusively from the rendered + * object the user is allowed to read, so field-level redaction applies to + * excerpt content for free. See + * openspec/changes/unified-search-provider/specs/unified-search-provider/spec.md. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @spec openspec/specs/unified-search-provider/spec.md */ class ObjectsProvider implements IFilteringProvider { + /** + * Maximum number of results returned per unified-search page. + * + * @var int + */ + private const PAGE_LIMIT = 25; + + /** + * Number of characters of context shown on each side of an excerpt match. + * + * @var int + */ + private const EXCERPT_CONTEXT = 60; + + /** + * Request-scoped cache of schema IDs flagged `searchable = false`. + * + * Null means not yet resolved this request. + * + * @var int[]|null + */ + private ?array $nonSearchableIds = null; + /** * The localization service * @@ -121,7 +160,7 @@ class ObjectsProvider implements IFilteringProvider * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function __construct( IL10N $l10n, @@ -148,7 +187,7 @@ public function __construct( * * @psalm-return 'openregister_objects' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getId(): string { @@ -160,7 +199,7 @@ public function getId(): string * * @return string Display name for the search provider * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getName(): string { @@ -181,7 +220,7 @@ public function getName(): string * @psalm-suppress UnusedParam Parameters required by interface but not used * @SuppressWarnings (PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getOrder(string $route, array $routeParameters): ?int { @@ -198,7 +237,7 @@ public function getOrder(string $route, array $routeParameters): ?int * @psalm-return list{'term', 'since', 'until', 'person', 'register', 'schema'} * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getSupportedFilters(): array { @@ -222,7 +261,7 @@ public function getSupportedFilters(): array * @psalm-return array * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getAlternateIds(): array { @@ -237,7 +276,7 @@ public function getAlternateIds(): array * @psalm-return list{FilterDefinition, FilterDefinition} * @phpstan-return list<\OCP\Search\FilterDefinition> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getCustomFilters(): array { @@ -268,7 +307,7 @@ public function getCustomFilters(): array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * Search requires handling many filters, building queries, and formatting results * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-view-based-search-composition */ public function search(IUser $user, ISearchQuery $query): SearchResult { @@ -311,8 +350,6 @@ public function search(IUser $user, ISearchQuery $query): SearchResult $until = $query->getFilter('until')?->get(); - // @todo: implement pagination. - // Note: order parameter not currently used in search // Build search query for searchObjectsPaginated. $searchQuery = []; @@ -321,14 +358,41 @@ public function search(IUser $user, ISearchQuery $query): SearchResult $searchQuery['_search'] = $search; } - // Add filters to @self metadata section. + // Resolve the searchable-schema opt-out once per request. + $nonSearchableIds = $this->getNonSearchableIds(); + + // Add filters to @self metadata section. When an explicit schema + // filter targets a non-searchable schema, the opt-out wins: return + // an empty (complete) result set rather than leaking it. if (empty($register) === false) { $searchQuery['@self']['register'] = (int) $register; } if (empty($schema) === false) { - $searchQuery['@self']['schema'] = (int) $schema; - } + $schemaId = (int) $schema; + if (in_array($schemaId, $nonSearchableIds, true) === true) { + return SearchResult::complete( + name: $this->getSectionName(), + entries: [] + ); + } + + $searchQuery['@self']['schema'] = $schemaId; + } else if (empty($nonSearchableIds) === false) { + // No explicit schema filter: constrain the query to the + // searchable-schema allow-list so opted-out schemas never + // contribute results, applied inside the query (not by + // post-filtering a page). + $searchableIds = $this->schemaMapper->findSearchableIds(); + if (empty($searchableIds) === true) { + return SearchResult::complete( + name: $this->getSectionName(), + entries: [] + ); + } + + $searchQuery['@self']['schema'] = $searchableIds; + }//end if // Add date filters if provided. if ($since !== null) { @@ -345,9 +409,23 @@ public function search(IUser $user, ISearchQuery $query): SearchResult } } - // Set pagination limits for Nextcloud search (defaults). - $searchQuery['_limit'] = 25; - $searchQuery['_offset'] = 0; + // Cursor pagination: cursor is an integer offset serialised as a + // string (matching the NC core files/contacts providers). Limit is + // capped at PAGE_LIMIT. + $limit = self::PAGE_LIMIT; + $queryLimit = $query->getLimit(); + if ($queryLimit > 0 && $queryLimit < $limit) { + $limit = $queryLimit; + } + + $offset = 0; + $cursor = $query->getCursor(); + if (is_numeric($cursor) === true) { + $offset = max(0, (int) $cursor); + } + + $searchQuery['_limit'] = $limit; + $searchQuery['_offset'] = $offset; $this->logger->debug( message: '[ObjectsProvider] OpenRegister search requested', @@ -359,8 +437,22 @@ public function search(IUser $user, ISearchQuery $query): SearchResult ] ); - // Use searchObjectsPaginated for optimal performance. - $searchResults = $this->objectService->searchObjectsPaginated(query: $searchQuery, _rbac: true, _multitenancy: true); + // Delegate to the OR search pipeline. RBAC, tenant isolation, the + // published predicate, and soft-delete exclusion are ALL enforced + // here — the provider applies no second access filter. Fail soft on + // a broken pipeline/register so the top-bar search never errors out. + try { + $searchResults = $this->objectService->searchObjectsPaginated(query: $searchQuery, _rbac: true, _multitenancy: true); + } catch (\Throwable $e) { + $this->logger->warning( + '[ObjectsProvider] OpenRegister search failed, returning empty result: {error}', + ['error' => $e->getMessage()] + ); + return SearchResult::complete( + name: $this->getSectionName(), + entries: [] + ); + } // Convert results to SearchResultEntry format. $searchResultEntries = []; @@ -402,32 +494,68 @@ public function search(IUser $user, ISearchQuery $query): SearchResult ); } - // Use registered app icon or fall back to OpenRegister icon. - $icon = $this->deepLinkRegistry->resolveIcon( + // Resolve the per-app label for this (register, schema) pair. + $appLabel = $this->deepLinkRegistry->resolveDisplayName( registerId: $registerId, schemaId: $schemaId - ) ?? 'icon-openregister'; + ); - // Create descriptive title and description. + // Icon precedence: + // 1. the schema's own MDI icon (an explicit, per-schema choice + // by the app author), rendered as a self-hosted data: SVG so + // it renders in the search dropdown and passes the image CSP; + // 2. the consuming app's registered (rounded) icon; + // 3. the generic OpenRegister icon class. + // The rounded avatar style only applies to the registered app + // icon — a schema glyph is a square monochrome icon. + // The schema glyph is served from the icon endpoint as a real + // same-origin SVG URL and passed as the THUMBNAIL, because + // Nextcloud search only paints a thumbnail from a URL — an + // icon-class name or a data: URI is not rendered as an image. + $schemaIconName = $this->resolveSchemaIcon(schemaId: $schemaId); + $thumbnailUrl = ''; + if (MdiIconRenderer::has(icon: $schemaIconName) === true) { + $thumbnailUrl = $this->urlGenerator->linkToRoute( + 'openregister.icon.mdi', + ['name' => $schemaIconName] + ); + $icon = 'icon-openregister'; + $rounded = false; + } else { + $icon = $this->deepLinkRegistry->resolveIcon( + registerId: $registerId, + schemaId: $schemaId + ) ?? 'icon-openregister'; + $rounded = ($appLabel !== null); + } + + // Create descriptive title and subline. $name = $selfData['name'] ?? ''; $title = 'Unknown Object'; - if (isset($result['title']) === true) { + if (isset($result['title']) === true && is_string($result['title']) === true) { $title = $result['title']; - } else if ($name !== '') { + } else if (is_string($name) === true && $name !== '') { $title = $name; } else if ($uuid !== '') { - $title = $uuid; + $title = (string) $uuid; } - $description = $this->buildDescription(object: array_merge($result, $selfData)); + $subline = $this->buildSubline( + object: $result, + registerId: $registerId, + schemaId: $schemaId, + appLabel: $appLabel, + term: $search + ); $searchResultEntries[] = new SearchResultEntry( - $objectUrl, + $thumbnailUrl, $title, - $description, + $subline, $objectUrl, - $icon + $icon, + $rounded ); }//end foreach }//end if @@ -442,12 +570,199 @@ public function search(IUser $user, ISearchQuery $query): SearchResult ] ); + // A full page implies there may be more; hand back a paginated + // result carrying the next offset as the cursor. A short or empty + // page completes the result. + if (count($searchResultEntries) >= $limit) { + return SearchResult::paginated( + $this->getSectionName(), + $searchResultEntries, + ($offset + $limit) + ); + } + return SearchResult::complete( - name: $this->l10n->t(text: 'Open Register Objects'), + name: $this->getSectionName(), entries: $searchResultEntries ); }//end search() + /** + * The localized provider section name shown in unified search. + * + * @return string The section title. + * + * @spec openspec/specs/unified-search-provider/spec.md + */ + private function getSectionName(): string + { + return $this->l10n->t('Open Register Objects'); + }//end getSectionName() + + /** + * Resolve the request-scoped set of non-searchable schema IDs. + * + * Cached for the lifetime of the request; fails soft (treats all + * schemas as searchable) if the mapper lookup errors. + * + * @return int[] Schema IDs flagged `searchable = false`. + * + * @psalm-return list + * + * @spec openspec/specs/unified-search-provider/spec.md + */ + private function getNonSearchableIds(): array + { + if ($this->nonSearchableIds !== null) { + return $this->nonSearchableIds; + } + + try { + $this->nonSearchableIds = $this->schemaMapper->findNonSearchableIds(); + } catch (\Throwable $e) { + $this->logger->warning( + '[ObjectsProvider] Failed to resolve non-searchable schemas, treating all as searchable: {error}', + ['error' => $e->getMessage()] + ); + $this->nonSearchableIds = []; + } + + return $this->nonSearchableIds; + }//end getNonSearchableIds() + + /** + * Build the result subline: `{Owner} · {Register} · {Schema} — {excerpt}`. + * + * The owner label is the deep-link display name for claimed pairs, or + * `Open Register` for unclaimed pairs. The excerpt is appended when a + * term-driven match (or fallback summary/description) is available. + * + * @param array $object The rendered object data. + * @param int $registerId The register database ID. + * @param int $schemaId The schema database ID. + * @param string|null $appLabel The owning app's display name, or null. + * @param string|null $term The search term, or null for filter-only. + * + * @return string The composed subline. + * + * @spec openspec/specs/unified-search-provider/spec.md + */ + private function buildSubline( + array $object, + int $registerId, + int $schemaId, + ?string $appLabel, + ?string $term + ): string { + $owner = 'Open Register'; + if ($appLabel !== null && $appLabel !== '') { + $owner = $appLabel; + } + + $parts = [$owner]; + if ($registerId > 0) { + $parts[] = $this->resolveRegisterName(registerId: $registerId); + } + + if ($schemaId > 0) { + $parts[] = $this->resolveSchemaName(schemaId: $schemaId); + } + + $subline = implode(' · ', $parts); + + $excerpt = $this->buildExcerpt(object: $object, term: (string) $term); + if ($excerpt !== '') { + $subline .= ' — '.$excerpt; + } + + return $subline; + }//end buildSubline() + + /** + * Build an excerpt around the first occurrence of the term. + * + * Walks the object's top-level scalar string values in property order + * (skipping `@self`), returns ±EXCERPT_CONTEXT chars around the first + * case-insensitive match of the term (ellipsised, matched substring + * left verbatim). With no string match — numeric/relational hit or a + * filter-only browse — falls back to `summary`, then a truncated + * `description`, then an empty string. The object passed in is the + * rendered object the user is allowed to read, so field-level security + * already redacted hidden fields from the excerpt source. + * + * @param array $object The rendered object data. + * @param string $term The search term (empty for filter-only browse). + * + * @return string The excerpt, or an empty string. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Excerpt walks fields with several optional paths. + * @SuppressWarnings(PHPMD.NPathComplexity) Excerpt has multiple fallback branches. + * + * @spec openspec/specs/unified-search-provider/spec.md + */ + private function buildExcerpt(array $object, string $term): string + { + if ($term !== '') { + foreach ($object as $key => $value) { + if ($key === '@self' || is_string($value) === false) { + continue; + } + + $position = mb_stripos($value, $term); + if ($position === false) { + continue; + } + + return $this->sliceExcerpt(value: $value, position: $position, length: mb_strlen($term)); + } + } + + // Fallback chain: summary → truncated description → empty. + if (isset($object['summary']) === true && is_string($object['summary']) === true && $object['summary'] !== '') { + return $object['summary']; + } + + if (isset($object['description']) === true && is_string($object['description']) === true && $object['description'] !== '') { + $description = $object['description']; + if (mb_strlen($description) > 100) { + return mb_substr($description, 0, 100).'…'; + } + + return $description; + } + + return ''; + }//end buildExcerpt() + + /** + * Cut a ±context window around a match position, ellipsising the edges. + * + * @param string $value The full field value. + * @param int $position The byte/char position of the match. + * @param int $length The length of the matched term. + * + * @return string The ellipsised fragment with the matched substring verbatim. + * + * @spec openspec/specs/unified-search-provider/spec.md + */ + private function sliceExcerpt(string $value, int $position, int $length): string + { + $start = max(0, ($position - self::EXCERPT_CONTEXT)); + $end = min(mb_strlen($value), ($position + $length + self::EXCERPT_CONTEXT)); + + $fragment = mb_substr($value, $start, ($end - $start)); + + if ($start > 0) { + $fragment = '…'.$fragment; + } + + if ($end < mb_strlen($value)) { + $fragment .= '…'; + } + + return $fragment; + }//end sliceExcerpt() + /** * Resolve a schema ID to its human-readable title. * @@ -455,16 +770,23 @@ public function search(IUser $user, ISearchQuery $query): SearchResult * * @return string The schema title or the ID as fallback * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ private function resolveSchemaName(int $schemaId): string { $key = 'schema_'.$schemaId; if (isset($this->nameCache[$key]) === false) { try { - $schema = $this->schemaMapper->find($schemaId); + // Resolve display metadata with tenancy/RBAC bypassed: the + // result object already passed those gates, and a schema owned by + // a different organisation than the active one must still resolve + // its human title (otherwise the result falls back to the bare id). + $schema = $this->schemaMapper->find($schemaId, _multitenancy: false, _rbac: false); $title = $schema->getTitle(); - $this->nameCache[$key] = ($title !== null && $title !== '') ? $title : (string) $schemaId; + $this->nameCache[$key] = (string) $schemaId; + if ($title !== null && $title !== '') { + $this->nameCache[$key] = $title; + } } catch (\Exception $e) { $this->nameCache[$key] = (string) $schemaId; } @@ -474,77 +796,63 @@ private function resolveSchemaName(int $schemaId): string }//end resolveSchemaName() /** - * Resolve a register ID to its human-readable title. + * Resolve a schema ID to its MDI icon reference (e.g. "Dog"), if set. * - * @param int $registerId The register ID + * @param int $schemaId The schema ID * - * @return string The register title or the ID as fallback + * @return string|null The schema's icon reference, or null when unset/unknown * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/changes/unified-search-index/specs/unified-search-provider/spec.md */ - private function resolveRegisterName(int $registerId): string + private function resolveSchemaIcon(int $schemaId): ?string { - $key = 'register_'.$registerId; - if (isset($this->nameCache[$key]) === false) { + $key = 'schemaicon_'.$schemaId; + if (array_key_exists($key, $this->nameCache) === false) { + $this->nameCache[$key] = ''; try { - $register = $this->registerMapper->find($registerId); - $title = $register->getTitle(); - $this->nameCache[$key] = ($title !== null && $title !== '') ? $title : (string) $registerId; + // Tenancy/RBAC bypassed for the same reason as resolveSchemaName(). + $icon = $this->schemaMapper->find($schemaId, _multitenancy: false, _rbac: false)->getIcon(); + if ($icon !== null) { + $this->nameCache[$key] = $icon; + } } catch (\Exception $e) { - $this->nameCache[$key] = (string) $registerId; + $this->nameCache[$key] = ''; } } + if ($this->nameCache[$key] === '') { + return null; + } + return $this->nameCache[$key]; - }//end resolveRegisterName() + }//end resolveSchemaIcon() /** - * Build a descriptive text for search results - * - * @param array $object Object data from searchObjectsPaginated + * Resolve a register ID to its human-readable title. * - * @return string Formatted description for search result + * @param int $registerId The register ID * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Description building requires multiple optional field checks - * @SuppressWarnings(PHPMD.NPathComplexity) Description building has multiple optional data paths + * @return string The register title or the ID as fallback * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ - private function buildDescription(array $object): string + private function resolveRegisterName(int $registerId): string { - $parts = []; - - // Add schema/register names (resolved from IDs) if available. - if (empty($object['schema']) === false) { - $parts[] = $this->resolveSchemaName(schemaId: (int) $object['schema']); - } - - if (empty($object['register']) === false) { - $parts[] = $this->resolveRegisterName(registerId: (int) $object['register']); - } - - // Add summary/description if available. - if (empty($object['summary']) === false) { - $parts[] = $object['summary']; - } else if (empty($object['description']) === false && is_string($object['description']) === true) { - $descriptionPart = substr($object['description'], 0, 100); - if (strlen($object['description']) > 100) { - $descriptionPart .= '...'; + $key = 'register_'.$registerId; + if (isset($this->nameCache[$key]) === false) { + try { + // Tenancy/RBAC bypassed for the same reason as resolveSchemaName(). + $register = $this->registerMapper->find($registerId, _multitenancy: false, _rbac: false); + $title = $register->getTitle(); + $this->nameCache[$key] = (string) $registerId; + if ($title !== null && $title !== '') { + $this->nameCache[$key] = $title; + } + } catch (\Exception $e) { + $this->nameCache[$key] = (string) $registerId; } - - $parts[] = $descriptionPart; } - // Add last updated info if available. - if (empty($object['updated']) === false) { - $parts[] = $this->l10n->t('Updated: %s', date('Y-m-d H:i', strtotime($object['updated']))); - } - - $description = implode(' • ', $parts); - if ($description !== '') { - return $description; - } - - return $this->l10n->t(text: 'Open Register Object'); - }//end buildDescription() + return $this->nameCache[$key]; + }//end resolveRegisterName() }//end class diff --git a/lib/Sections/OpenRegisterAdmin.php b/lib/Sections/OpenRegisterAdmin.php index d892aa3a4a..1ad522b0b9 100644 --- a/lib/Sections/OpenRegisterAdmin.php +++ b/lib/Sections/OpenRegisterAdmin.php @@ -55,7 +55,7 @@ class OpenRegisterAdmin implements IIconSection * @param IL10N $l Localization service * @param IURLGenerator $urlGenerator URL generator service * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct(IL10N $l, IURLGenerator $urlGenerator) { @@ -68,7 +68,7 @@ public function __construct(IL10N $l, IURLGenerator $urlGenerator) * * @return string Icon path * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getIcon(): string { @@ -82,7 +82,7 @@ public function getIcon(): string * * @psalm-return 'openregister' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getID(): string { @@ -94,7 +94,7 @@ public function getID(): string * * @return string Section name * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getName(): string { @@ -108,7 +108,7 @@ public function getName(): string * * @psalm-return 97 * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getPriority(): int { diff --git a/lib/Service/ActionExecutor.php b/lib/Service/ActionExecutor.php index ed923d5231..07a9f0dace 100644 --- a/lib/Service/ActionExecutor.php +++ b/lib/Service/ActionExecutor.php @@ -19,11 +19,11 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-4 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-3 + * @spec openspec/specs/actions/spec.md + * @spec openspec/specs/actions/spec.md + * @spec openspec/specs/actions/spec.md + * @spec openspec/specs/actions/spec.md + * @spec openspec/specs/actions/spec.md */ declare(strict_types=1); @@ -85,7 +85,7 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-2 + * @spec openspec/specs/actions/spec.md */ public function executeActions(array $actions, Event $event, array $payload, string $eventType): void { @@ -117,7 +117,7 @@ public function executeActions(array $actions, Event $event, array $payload, str * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-2 + * @spec openspec/specs/actions/spec.md */ private function executeSingleAction(Action $action, Event $event, array $payload, string $eventType): void { @@ -147,7 +147,10 @@ private function executeSingleAction(Action $action, Event $event, array $payloa $cloudEventPayload, $action->getTimeout() ); - $response = ($result instanceof WorkflowResult) ? $result->toArray() : (array) $result; + $response = (array) $result; + if ($result instanceof WorkflowResult) { + $response = $result->toArray(); + } } catch (Exception $e) { $status = 'failure'; $error = $e->getMessage(); @@ -212,7 +215,7 @@ private function executeSingleAction(Action $action, Event $event, array $payloa * * @return array The CloudEvent-formatted payload * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-3 + * @spec openspec/specs/actions/spec.md */ public function buildCloudEventPayload(Action $action, array $payload, string $eventType): array { @@ -244,7 +247,7 @@ public function buildCloudEventPayload(Action $action, array $payload, string $e * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-2 + * @spec openspec/specs/actions/spec.md */ private function processWorkflowResult(WorkflowResult $result, Action $action, Event $event): void { @@ -278,7 +281,7 @@ private function processWorkflowResult(WorkflowResult $result, Action $action, E * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-3 + * @spec openspec/specs/actions/spec.md */ private function handleFailure(Action $action, array $payload, string $error): void { @@ -319,7 +322,7 @@ private function handleFailure(Action $action, array $payload, string $error): v * * @SuppressWarnings(PHPMD.ExcessiveParameterList) Log entries require many fields * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-2 + * @spec openspec/specs/actions/spec.md */ private function createLogEntry( Action $action, diff --git a/lib/Service/ActionService.php b/lib/Service/ActionService.php index a99fc3c402..81d8c6bab0 100644 --- a/lib/Service/ActionService.php +++ b/lib/Service/ActionService.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ declare(strict_types=1); @@ -86,7 +86,7 @@ public function __construct( * * @throws InvalidArgumentException If required fields are missing * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ public function createAction(array $data): Action { @@ -151,7 +151,7 @@ public function createAction(array $data): Action * * @return Action The updated action * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ public function updateAction(int $id, array $data): Action { @@ -179,7 +179,7 @@ public function updateAction(int $id, array $data): Action * * @return Action The deleted action * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ public function deleteAction(int $id): Action { @@ -213,7 +213,7 @@ public function deleteAction(int $id): Action * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-actions/tasks.md#task-2 + * @spec openspec/specs/actions/spec.md */ public function testAction(int $id, array $samplePayload): array { @@ -285,7 +285,7 @@ public function testAction(int $id, array $samplePayload): array * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-actions/tasks.md#task-3 + * @spec openspec/specs/actions/spec.md */ public function migrateFromHooks(int $schemaId): array { @@ -366,7 +366,7 @@ public function migrateFromHooks(int $schemaId): array * * @return void * - * @spec openspec/changes/retrofit-2026-05-01-actions/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ public function updateStatistics(int $actionId, string $status): void { diff --git a/lib/Service/ActivityService.php b/lib/Service/ActivityService.php index a1f9703ecf..54adf0ef4c 100644 --- a/lib/Service/ActivityService.php +++ b/lib/Service/ActivityService.php @@ -46,7 +46,7 @@ class ActivityService * @param IURLGenerator $urlGenerator The URL generator. * @param LoggerInterface $logger The logger. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function __construct( private IManager $activityManager, @@ -63,7 +63,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishObjectCreated(ObjectEntity $object): void { @@ -92,7 +92,7 @@ public function publishObjectCreated(ObjectEntity $object): void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $oldObject reserved for future diff support * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishObjectUpdated(ObjectEntity $newObject, ?ObjectEntity $oldObject=null): void { @@ -118,7 +118,7 @@ public function publishObjectUpdated(ObjectEntity $newObject, ?ObjectEntity $old * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishObjectDeleted(ObjectEntity $object): void { @@ -143,7 +143,7 @@ public function publishObjectDeleted(ObjectEntity $object): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishRegisterCreated(Register $register): void { @@ -169,7 +169,7 @@ public function publishRegisterCreated(Register $register): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishRegisterUpdated(Register $register): void { @@ -195,7 +195,7 @@ public function publishRegisterUpdated(Register $register): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishRegisterDeleted(Register $register): void { @@ -220,7 +220,7 @@ public function publishRegisterDeleted(Register $register): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishSchemaCreated(Schema $schema): void { @@ -246,7 +246,7 @@ public function publishSchemaCreated(Schema $schema): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishSchemaUpdated(Schema $schema): void { @@ -272,7 +272,7 @@ public function publishSchemaUpdated(Schema $schema): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ public function publishSchemaDeleted(Schema $schema): void { @@ -297,7 +297,7 @@ public function publishSchemaDeleted(Schema $schema): void * * @return string The absolute URL to the object. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function buildObjectLink(ObjectEntity $object): string { @@ -316,7 +316,7 @@ private function buildObjectLink(ObjectEntity $object): string * * @return string The absolute URL to the register. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function buildRegisterLink(Register $register): string { @@ -332,7 +332,7 @@ private function buildRegisterLink(Register $register): string * * @return string The absolute URL to the schema. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function buildSchemaLink(Schema $schema): string { @@ -358,7 +358,7 @@ private function buildSchemaLink(Schema $schema): string * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -448,7 +448,7 @@ private function publish( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function publishEvent( string $subject, @@ -485,7 +485,7 @@ private function publishEvent( * * @return string The resolved title. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-1 + * @spec openspec/specs/event-driven-architecture/spec.md */ private function resolveTitle(?string $primary, ?string $fallback): string { diff --git a/lib/Service/Aggregation/AggregationAnnotationValidator.php b/lib/Service/Aggregation/AggregationAnnotationValidator.php index a4dd07b90b..8aef7b0dad 100644 --- a/lib/Service/Aggregation/AggregationAnnotationValidator.php +++ b/lib/Service/Aggregation/AggregationAnnotationValidator.php @@ -125,8 +125,16 @@ public function validate(array $schema): array } // Intra-schema aggregation: full property-existence checks. - // Support `select` as alias for `metric`. - $metric = (string) ($spec['metric'] ?? $spec['select'] ?? ''); + // Support `select` as alias for `metric`. Non-scalar values (a + // schema author passing an array) must not reach the string cast — + // PHP emits "Array to string conversion" — so they collapse to '' + // and fail the metric check with a proper validation error. + $metric = ($spec['metric'] ?? $spec['select'] ?? ''); + if (is_scalar($metric) === false) { + $metric = ''; + } + + $metric = (string) $metric; if (in_array($metric, self::VALID_METRICS, true) === false) { $errors[] = [ 'code' => 'aggregation-bad-metric', diff --git a/lib/Service/Aggregation/AggregationCache.php b/lib/Service/Aggregation/AggregationCache.php index 069e4de369..75aca30c72 100644 --- a/lib/Service/Aggregation/AggregationCache.php +++ b/lib/Service/Aggregation/AggregationCache.php @@ -21,7 +21,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-1 + * @spec openspec/specs/aggregations-backend-native/spec.md * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-20 */ @@ -67,9 +67,9 @@ class AggregationCache /** * Constructor. * - * @param ICacheFactory $cacheFactory Factory used to create the distributed cache. - * @param IUserSession $userSession Current user session, used to scope the cache key. - * @param LoggerInterface $logger Logger for backend-unavailable warnings. + * @param ICacheFactory $cacheFactory Factory used to create the distributed cache. + * @param IUserSession $userSession Current user session, used to scope the cache key. + * @param LoggerInterface $logger Logger for backend-unavailable warnings. * @param OrganisationService $organisationService Organisation service, used to include active organisation in cache key. * * @return void @@ -148,7 +148,7 @@ public function get(string $registerSlug, string $schemaSlug, string $name, arra * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-1 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function set(string $registerSlug, string $schemaSlug, string $name, array $filter, array $result): void { @@ -212,7 +212,7 @@ public function getAdhoc(string $registerSlug, string $schemaSlug, AggregationQu * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-1 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function setAdhoc(string $registerSlug, string $schemaSlug, AggregationQuery $query, array $result): void { @@ -263,9 +263,7 @@ private function adhocName(AggregationQuery $query): string * * @return void * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-1 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function evictForSchema(string $registerSlug, string $schemaSlug): void { @@ -274,11 +272,20 @@ public function evictForSchema(string $registerSlug, string $schemaSlug): void } try { - // ICache doesn't have prefix delete. Do a best-effort clear of - // the whole openregister_aggregations cache — coarse but safe - // because TTL is short. A future refinement can swap to a - // backing store with prefix scan support. - $this->cache->clear(); + // Scoped eviction: bump this (register, schema)'s version counter so + // its cached aggregations become unreachable, WITHOUT wiping every + // other schema's/user's cache (scope-cache-invalidation). The counter + // outlives the data TTL so a bump can never be undone by expiry while + // stale data is still live. + $versionKey = $this->versionKey(registerSlug: $registerSlug, schemaSlug: $schemaSlug); + $current = $this->cache->get($versionKey); + $next = 1; + if (is_numeric($current) === true) { + $next = ((int) $current + 1); + } + + // TTL well beyond the data TTL (self::TTL) so the bump persists. + $this->cache->set($versionKey, $next, (self::TTL * 60)); } catch (\Throwable $e) { $this->logger->debug( sprintf('[AggregationCache] evict failed: %s', $e->getMessage()) @@ -311,9 +318,59 @@ private function key(string $registerSlug, string $schemaSlug, string $name, arr $filterHash = sha1($filterStr); $rbacHash = $this->rbacScopeHash(); - return sprintf('agg:%s:%s:%s:%s:%s', $registerSlug, $schemaSlug, $name, $filterHash, $rbacHash); + + // Fold the per-(register, schema) version into the key so eviction can be + // scoped by bumping that version (scope-cache-invalidation) rather than + // wiping the whole aggregation cache: after a bump, every key for the + // schema carries the new version and old entries become unreachable. + $version = $this->schemaVersion(registerSlug: $registerSlug, schemaSlug: $schemaSlug); + + return sprintf( + 'agg:%s:%s:v%d:%s:%s:%s', + $registerSlug, + $schemaSlug, + $version, + $name, + $filterHash, + $rbacHash + ); }//end key() + /** + * Version-counter cache key for a (register, schema) pair. + * + * @param string $registerSlug Register slug. + * @param string $schemaSlug Schema slug. + * + * @return string + */ + private function versionKey(string $registerSlug, string $schemaSlug): string + { + return sprintf('aggver:%s:%s', $registerSlug, $schemaSlug); + }//end versionKey() + + /** + * Current aggregation-cache version for a (register, schema) pair. + * + * @param string $registerSlug Register slug. + * @param string $schemaSlug Schema slug. + * + * @return int The current version (0 when never evicted). + */ + private function schemaVersion(string $registerSlug, string $schemaSlug): int + { + if ($this->cache === null) { + return 0; + } + + $current = $this->cache->get($this->versionKey(registerSlug: $registerSlug, schemaSlug: $schemaSlug)); + if (is_numeric($current) === true) { + return (int) $current; + } + + return 0; + }//end schemaVersion() + /** * Hash the current RBAC scope (user UID + active organisation). * @@ -325,9 +382,13 @@ private function key(string $registerSlug, string $schemaSlug, string $name, arr */ private function rbacScopeHash(): string { - $uid = ($this->userSession->getUser()?->getUID() ?? 'anonymous'); - $org = $this->organisationService->getActiveOrganisation(); - $orgId = ($org !== null ? $org->getUuid() : 'none'); + $uid = ($this->userSession->getUser()?->getUID() ?? 'anonymous'); + $org = $this->organisationService->getActiveOrganisation(); + $orgId = 'none'; + if ($org !== null) { + $orgId = $org->getUuid(); + } + return sha1($uid.':'.$orgId); }//end rbacScopeHash() }//end class diff --git a/lib/Service/Aggregation/AggregationQuery.php b/lib/Service/Aggregation/AggregationQuery.php index 1bd54c8d9a..f6bed2f621 100644 --- a/lib/Service/Aggregation/AggregationQuery.php +++ b/lib/Service/Aggregation/AggregationQuery.php @@ -9,9 +9,10 @@ * value object into native query parameters. * * Supported metrics: count / sum / avg / min / max. - * Supported filter operators (per field): scalar equality + in / gt / - * gte / lt / lte / ne (mirrors the inline magic-table SQL path that - * lived in `AggregationRunner::tryNativeAggregation`). + * Supported filter operators (per field): scalar equality + in / notIn / + * gt / gte / lt / lte / ne (mirrors the inline magic-table SQL path that + * lived in `AggregationRunner::tryNativeAggregation`). `in` and `notIn` + * take an array operand; the rest take a scalar. * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. @@ -25,7 +26,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/aggregations-backend-native/tasks.md "SearchBackendInterface::aggregate" + * @spec openspec/specs/aggregations-backend-native/spec.md "SearchBackendInterface::aggregate" * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-18 */ @@ -37,6 +38,8 @@ /** * Backend-portable aggregation request. + * + * @spec openspec/specs/aggregation-api/spec.md */ class AggregationQuery { @@ -73,7 +76,10 @@ class AggregationQuery * @param string $metric Aggregation metric. * @param ?string $field Field for sum/avg/min/max; required when metric != count. * @param array $filter Filter conditions (see class docblock for shapes). - * @param array|null $groupBy Optional grouping spec; e.g. `{field: 'status'}`. + * @param array|null $groupBy Optional grouping spec. Accepts a single-field shape + * (`{field: 'status'}`), a multi-field cross-tab shape + * (`{fields: ['vendorId', 'dueDateBucket']}`), or a plain + * ordered list of field names (`['vendorId', 'dueDateBucket']`). * @param array|null $dateBucket Optional date-bucket spec; `{field, start, end, gap}`. */ private function __construct( @@ -92,7 +98,10 @@ private function __construct( * @param string $metric One of METRIC_*. * @param ?string $field Field for non-count metrics; null for count. * @param array $filter Filter map. - * @param array|null $groupBy Optional groupBy. + * @param array|null $groupBy Optional groupBy. Single-field (`{field: 'x'}`), + * multi-field (`{fields: ['a','b']}`), or a plain list + * (`['a','b']`). Multi-field yields one grouped row per + * distinct field tuple. * @param array|null $dateBucket Optional dateBucket spec `{field, start, end, gap}`. * * @return self @@ -100,11 +109,14 @@ private function __construct( * @throws InvalidArgumentException When the input is invalid. * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) * Fail-fast validation chain: each `if` is one independent guard - * against bad input. Extracting them would reduce the count but - * obscure the per-rule error messages. + * against bad input (including the single-/multi-field groupBy shape + * checks). Extracting them would reduce the count but obscure the + * per-rule error messages. * * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-18 + * @spec openspec/specs/aggregation-api/spec.md */ public static function create( string $metric, @@ -129,9 +141,26 @@ public static function create( ); } - if ($groupBy !== null && (isset($groupBy['field']) === false || $groupBy['field'] === '')) { - throw new InvalidArgumentException('groupBy MUST include a non-empty `field`'); - } + if ($groupBy !== null) { + $groupFields = self::normaliseGroupByFields(groupBy: $groupBy); + foreach ($groupFields as $groupField) { + if (is_string($groupField) === false || $groupField === '') { + throw new InvalidArgumentException( + 'groupBy MUST include a non-empty `field`; every group field MUST be a non-empty string' + ); + } + } + + if (count($groupFields) === 0) { + throw new InvalidArgumentException( + 'groupBy MUST include a non-empty `field` or a non-empty `fields` list' + ); + } + + if (count($groupFields) !== count(array_unique($groupFields))) { + throw new InvalidArgumentException('groupBy fields MUST be distinct (duplicate field in groupBy list)'); + } + }//end if if ($dateBucket !== null) { self::assertValidDateBucket(spec: $dateBucket); @@ -188,6 +217,8 @@ private static function assertValidDateBucket(array $spec): void * Test whether the request includes a groupBy clause. * * @return bool + * + * @spec openspec/specs/aggregation-api/spec.md */ public function isGrouped(): bool { @@ -196,32 +227,119 @@ public function isGrouped(): bool }//end isGrouped() /** - * Get the groupBy field (or null when ungrouped). + * Get the FIRST groupBy field (or null when ungrouped). + * + * Retained for backward compatibility with single-field callers. For a + * multi-field cross-tab groupBy this returns the first grouping field + * only; use {@see getGroupByFields()} to obtain the full ordered tuple. * * @return ?string * * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-18 + * @spec openspec/specs/aggregation-api/spec.md */ public function getGroupByField(): ?string { - if ($this->groupBy === null) { + $fields = $this->getGroupByFields(); + if (count($fields) === 0) { return null; } - $field = ($this->groupBy['field'] ?? null); - $fieldValue = null; - if (is_string($field) === true) { - $fieldValue = $field; + return $fields[0]; + + }//end getGroupByField() + + /** + * Get the ordered list of groupBy fields (empty when ungrouped). + * + * Normalises every accepted groupBy shape to a flat, ordered list of + * field names: + * - `{field: 'status'}` → `['status']` + * - `{fields: ['vendorId', 'bucket']}` → `['vendorId', 'bucket']` + * - `['vendorId', 'bucket']` → `['vendorId', 'bucket']` + * - `null` → `[]` + * + * @return array Ordered group field names. + * + * @spec openspec/specs/aggregation-api/spec.md + */ + public function getGroupByFields(): array + { + if ($this->groupBy === null) { + return []; + } + + $fields = []; + foreach (self::normaliseGroupByFields(groupBy: $this->groupBy) as $field) { + if (is_string($field) === true) { + $fields[] = $field; + } } - return $fieldValue; + return $fields; - }//end getGroupByField() + }//end getGroupByFields() + + /** + * Test whether this is a multi-field (cross-tab) groupBy. + * + * @return bool True when more than one grouping field is present. + * + * @spec openspec/specs/aggregation-api/spec.md + */ + public function isMultiFieldGroupBy(): bool + { + return (count($this->getGroupByFields()) > 1); + + }//end isMultiFieldGroupBy() + + /** + * Normalise any accepted groupBy shape to a flat, ordered candidate list. + * + * Shared canonicaliser used by both the value object and the runner so + * the single-field, multi-field, and plain-list shapes are honoured + * identically across the native-SQL and PHP-fallback paths. Returns the + * raw candidate values (which the caller validates) rather than filtering + * silently — dropping invalid entries here would reintroduce the very + * silent-ignore bug this feature fixes. + * + * @param mixed $groupBy The raw groupBy spec (array, list, or null). + * + * @return array Ordered candidate field values. + * + * @spec openspec/specs/aggregation-api/spec.md + */ + public static function normaliseGroupByFields(mixed $groupBy): array + { + if (is_array($groupBy) === false) { + return []; + } + + // Plain ordered list of field names: `['a', 'b']`. + if (array_is_list($groupBy) === true) { + return $groupBy; + } + + // Multi-field cross-tab shape: `{fields: ['a', 'b']}`. + if (isset($groupBy['fields']) === true && is_array($groupBy['fields']) === true) { + return array_values($groupBy['fields']); + } + + // Single-field shape: `{field: 'a'}`. + if (array_key_exists('field', $groupBy) === true) { + return [$groupBy['field']]; + } + + return []; + + }//end normaliseGroupByFields() /** * Test whether the request includes a dateBucket clause. * * @return bool + * + * @spec openspec/specs/aggregation-api/spec.md */ public function hasDateBucket(): bool { @@ -246,7 +364,7 @@ public function hasDateBucket(): bool * dateBucket: array|null * } Canonical wire shape of the query. * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-1 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function toArray(): array { diff --git a/lib/Service/Aggregation/AggregationRunner.php b/lib/Service/Aggregation/AggregationRunner.php index 37654e8736..30c4c3fffc 100644 --- a/lib/Service/Aggregation/AggregationRunner.php +++ b/lib/Service/Aggregation/AggregationRunner.php @@ -7,8 +7,8 @@ * matched objects via the existing findAll path (RBAC + multi-tenancy * still applied), and computes the metric in PHP. * - * v1 trades performance for simplicity: backend-native aggregation - * (Postgres GROUP BY / Solr facets / ES aggs) ships in a follow-up. + * v1 trades performance for simplicity: Postgres-native aggregation + * (GROUP BY) is the fast path; PHP fallback covers non-Postgres setups. * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. @@ -24,9 +24,9 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-4 + * @spec openspec/specs/aggregations-backend-native/spec.md + * @spec openspec/specs/aggregations-backend-native/spec.md + * @spec openspec/specs/aggregations-backend-native/spec.md * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-18 * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 */ @@ -37,19 +37,21 @@ use DateTimeImmutable; use DateTimeInterface; +use DateTimeZone; use OCA\OpenRegister\Db\MagicMapper; use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Exception\NotAuthorizedException; -use OCA\OpenRegister\Service\Index\SearchBackendInterface; +use OCA\OpenRegister\Service\LanguageService; use OCA\OpenRegister\Service\Object\PermissionHandler; +use OCA\OpenRegister\Service\Object\TranslationHandler; use OCA\OpenRegister\Service\OrganisationService; use OCA\OpenRegister\Service\Search\PlaceholderResolver; use OCP\IDBConnection; use OCP\IUserSession; -use ReflectionClass; +use Psr\Log\LoggerInterface; use RuntimeException; /** @@ -64,6 +66,8 @@ * PHP fallback). The method count rises with each platform-specific * helper; extracting them into a separate class would require passing * the full constructor dependency graph through. + * + * @spec openspec/specs/aggregation-api/spec.md */ class AggregationRunner { @@ -80,16 +84,18 @@ class AggregationRunner /** * Constructor. * - * @param MagicMapper $magicMapper Magic-table mapper used for the PHP fallback path. - * @param RegisterMapper $registerMapper Register loader. - * @param SchemaMapper $schemaMapper Schema loader. - * @param PlaceholderResolver $placeholders Resolves dynamic placeholders inside filters. - * @param IDBConnection $db Database connection for the Postgres-native fast path. - * @param AggregationCache $cache 60s aggregation result cache. - * @param PermissionHandler $permissionHandler RBAC verdict on the schema's `list` action. - * @param IUserSession $userSession Active session, for the RBAC + cache-key user scope. - * @param OrganisationService $organisationService Active-organisation lookup for the cache key. - * @param SearchBackendInterface|null $searchBackend Optional Solr/ES backend for native aggregation. + * @param MagicMapper $magicMapper Magic-table mapper used for the PHP fallback path. + * @param RegisterMapper $registerMapper Register loader. + * @param SchemaMapper $schemaMapper Schema loader. + * @param PlaceholderResolver $placeholders Resolves dynamic placeholders inside filters. + * @param IDBConnection $db Database connection for the Postgres-native fast path. + * @param AggregationCache $cache 60s aggregation result cache. + * @param PermissionHandler $permissionHandler RBAC verdict on the schema's `list` action. + * @param IUserSession $userSession Active session, for the RBAC + cache-key user scope. + * @param OrganisationService $organisationService Active-organisation lookup for the cache key. + * @param TranslationHandler $translationHandler Resolves translatable group keys to the negotiated language. + * @param LanguageService $languageService Request-scoped language negotiation (Accept-Language / _lang). + * @param LoggerInterface|null $logger Optional logger for diagnostics. * * @return void * @@ -107,7 +113,9 @@ public function __construct( private readonly PermissionHandler $permissionHandler, private readonly IUserSession $userSession, private readonly OrganisationService $organisationService, - private readonly ?SearchBackendInterface $searchBackend=null + private readonly TranslationHandler $translationHandler, + private readonly LanguageService $languageService, + private readonly ?LoggerInterface $logger=null ) { }//end __construct() @@ -240,6 +248,13 @@ public function run( $filter = (array) ($spec['filter'] ?? $spec['where'] ?? []); $groupBy = ($spec['groupBy'] ?? null); + // Normalized groupBy spec (array or null) — used both for the native + // path argument and for translatable group-key projection. + $groupByArg = null; + if (is_array($groupBy) === true) { + $groupByArg = $groupBy; + } + $resolvedFilter = $this->placeholders->resolveArray($filter); // Cache lookup: the resolved filter (with placeholders concrete) @@ -264,66 +279,15 @@ public function run( ); if ($cached !== null) { $cached['cached'] = true; - return $cached; + // Cache stores raw group keys (language-agnostic key), project on read. + return $this->projectTranslatableGroupKeys( + envelope: $cached, + schema: $schema, + register: $register, + groupBy: $groupByArg + ); } - // Try the configured external search backend (Solr / ES) first - // when one is wired in. The backend returns null when it can't - // execute the query (unsupported metric, unreachable instance, - // etc) — we then fall through to the Postgres-native fast path - // and finally to the PHP fallback. - if ($this->searchBackend !== null) { - try { - $fieldArg = null; - if (is_string($field) === true) { - $fieldArg = $field; - } - - $groupByArg = null; - if (is_array($groupBy) === true) { - $groupByArg = $groupBy; - } - - $portableQuery = AggregationQuery::create( - metric: $metric, - field: $fieldArg, - filter: $resolvedFilter, - groupBy: $groupByArg - ); - $external = $this->searchBackend->aggregate(query: $portableQuery); - if ($external !== null) { - $backendName = $this->detectBackendName(backend: $this->searchBackend); - // R05: surface `truncated` on every backend so the - // shape is consistent. Search backends propagate the - // flag from the engine when supplied; otherwise - // assume the engine returned the full set. - $fieldValue = null; - if (is_string($field) === true) { - $fieldValue = $field; - } - - $result = [ - 'name' => $name, - 'metric' => $metric, - 'field' => $fieldValue, - 'backend' => $backendName, - 'truncated' => (bool) ($external['truncated'] ?? false), - ] + $external; - $this->cache->set( - registerSlug: (string) $register->getSlug(), - schemaSlug: (string) $schema->getSlug(), - name: $name, - filter: $cacheKey, - result: $result - ); - return $result; - }//end if - } catch (\Throwable $e) { - // External backend errored — fall through to native / - // PHP path so a flaky Solr/ES never breaks aggregations. - }//end try - }//end if - // Try the Postgres-native fast path. Falls back to PHP when the // query shape isn't supported (operator filters, complex values, // non-Postgres DB, etc). @@ -332,10 +296,7 @@ public function run( $nativeFieldArg = $field; } - $nativeGroupByArg = null; - if (is_array($groupBy) === true) { - $nativeGroupByArg = $groupBy; - } + $nativeGroupByArg = $groupByArg; $native = $this->tryNativeAggregation( register: $register, @@ -369,7 +330,12 @@ public function run( filter: $cacheKey, result: $result ); - return $result; + return $this->projectTranslatableGroupKeys( + envelope: $result, + schema: $schema, + register: $register, + groupBy: $nativeGroupByArg + ); }//end if // Fall back: pull objects and filter in PHP. @@ -415,12 +381,13 @@ public function run( 'truncated' => $truncated, ]; - if (is_array($groupBy) === true && isset($groupBy['field']) === true) { + $runGroupFields = $this->resolveGroupFields(groupBy: $groupBy); + if (count($runGroupFields) > 0) { $result['groups'] = $this->computeGrouped( rows: $rows, metric: $metric, field: $field, - groupField: (string) $groupBy['field'] + groupFields: $runGroupFields ); } @@ -435,7 +402,12 @@ public function run( filter: $cacheKey, result: $result ); - return $result; + return $this->projectTranslatableGroupKeys( + envelope: $result, + schema: $schema, + register: $register, + groupBy: $groupByArg + ); }//end run() /** @@ -537,7 +509,15 @@ public function runAdhoc( ); if ($cached !== null) { $cached['cached'] = true; - return $cached; + // The cache stores RAW group keys (the cache key is language- + // agnostic), so translatable keys must be projected on every + // read, including cache hits. + return $this->projectTranslatableGroupKeys( + envelope: $cached, + schema: $schema, + register: $register, + groupBy: $query->groupBy + ); } // Try the Postgres / MySQL / SQLite native fast path. The runner @@ -560,14 +540,22 @@ public function runAdhoc( 'backend' => $this->detectDatabasePlatform(), 'cached' => false, ] + $native; + // Cache the RAW (un-projected) group keys — the cache key does + // not encode the negotiated language, so language projection + // must happen after the cache boundary on every read. $this->cache->setAdhoc( registerSlug: (string) $register->getSlug(), schemaSlug: (string) $schema->getSlug(), query: $resolvedQuery, result: $envelope ); - return $envelope; - } + return $this->projectTranslatableGroupKeys( + envelope: $envelope, + schema: $schema, + register: $register, + groupBy: $query->groupBy + ); + }//end if // PHP fallback path. Pull the RBAC-filtered row set + bucket in // PHP. Correctness path; the native paths above are the @@ -587,10 +575,148 @@ public function runAdhoc( query: $resolvedQuery, result: $envelope ); - return $envelope; + return $this->projectTranslatableGroupKeys( + envelope: $envelope, + schema: $schema, + register: $register, + groupBy: $query->groupBy + ); }//end runAdhoc() + /** + * Project translatable group keys in a grouped-aggregation envelope to + * the negotiated display language. + * + * `GET /api/objects/aggregations/{register}/{schema}/grouped` groups on + * a single field via SQL `GROUP BY` (native path) or PHP bucketing + * (fallback). When that field is `translatable: true`, the stored value + * is a language-keyed map (e.g. `{"nl":"Foo","en":"Bar"}`), so each + * group `key` comes back as the raw map (native: a JSON string; PHP: an + * associative array) instead of the single projected string a normal + * read returns. + * + * This projects each such key to the caller's negotiated language, + * reusing {@see TranslationHandler::resolveTranslationsForRender()} for + * the language chain / fallback logic rather than reimplementing it. + * + * No-op when: + * - the envelope carries no `groups`; + * - there is no single scalar `groupBy.field`; + * - the groupBy field is NOT translatable on the schema; + * - `?_translations=all` is requested (keys are returned verbatim). + * + * @param array $envelope The aggregation result envelope. + * @param Schema $schema The schema being aggregated. + * @param Register $register The owning register (language config). + * @param array|null $groupBy The groupBy spec ({field: ...}). + * + * @return array The envelope with translatable group keys projected. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * The method is a chain of cheap short-circuit guards (no groups / + * no groupBy field / not translatable / _translations=all) before + * the projection loop; each guard adds a branch but keeps the hot + * path a single early return. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function projectTranslatableGroupKeys( + array $envelope, + Schema $schema, + Register $register, + ?array $groupBy + ): array { + if (isset($envelope['groups']) === false || is_array($envelope['groups']) === false) { + return $envelope; + } + + if ($groupBy === null || isset($groupBy['field']) === false) { + return $envelope; + } + + $groupField = (string) $groupBy['field']; + + // Only project when the grouped field is actually translatable. + // getTranslatableProperties() is cheap and this keeps NON-translatable + // grouped fields byte-for-byte unchanged. + $translatableProps = $this->translationHandler->getTranslatableProperties(schema: $schema); + if (in_array($groupField, $translatableProps, true) === false) { + return $envelope; + } + + // `?_translations=all` returns keys verbatim. + if ($this->languageService->shouldReturnAllTranslations() === true) { + return $envelope; + } + + foreach ($envelope['groups'] as $index => $group) { + if (is_array($group) === false || array_key_exists('key', $group) === false) { + continue; + } + + $envelope['groups'][$index]['key'] = $this->projectSingleTranslatableKey( + rawKey: $group['key'], + groupField: $groupField, + schema: $schema, + register: $register + ); + } + + return $envelope; + + }//end projectTranslatableGroupKeys() + + /** + * Project a single grouped key value to the negotiated language. + * + * Normalizes the raw key to a language-keyed map (native path yields a + * JSON string, PHP path an array), then delegates to + * {@see TranslationHandler::resolveTranslationsForRender()} by wrapping + * the map under the grouped field name and reading the resolved value + * back. Non-map keys (already scalar, e.g. legacy untranslated rows) are + * returned unchanged. + * + * @param mixed $rawKey The raw group key (JSON string or array). + * @param string $groupField The grouped field name. + * @param Schema $schema The schema being aggregated. + * @param Register $register The owning register (language config). + * + * @return mixed The projected scalar value, or the original key when not a map. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function projectSingleTranslatableKey( + mixed $rawKey, + string $groupField, + Schema $schema, + Register $register + ): mixed { + $map = $rawKey; + + // Native SQL returns the JSON column value as a string; decode it. + if (is_string($rawKey) === true) { + $decoded = json_decode($rawKey, true); + if (is_array($decoded) === true) { + $map = $decoded; + } + } + + // Not a language-keyed map (scalar / legacy untranslated) — leave it. + if (is_array($map) === false) { + return $rawKey; + } + + $resolved = $this->translationHandler->resolveTranslationsForRender( + objectData: [$groupField => $map], + schema: $schema, + register: $register + ); + + return $resolved[$groupField] ?? $rawKey; + + }//end projectSingleTranslatableKey() + /** * Convenience: run an ad-hoc aggregation by register/schema ref. * @@ -607,7 +733,7 @@ public function runAdhoc( * @throws RuntimeException When the register or schema cannot be resolved. * @throws NotAuthorizedException When the caller lacks list-permission. * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-2 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function runAdhocByRef( string $registerRef, @@ -715,7 +841,10 @@ private function bucketInPhp( continue; } - $stamp = is_numeric($raw) ? (int) $raw : strtotime((string) $raw); + $stamp = strtotime((string) $raw); + if (is_numeric($raw) === true) { + $stamp = (int) $raw; + } if ($stamp === false || $stamp < $start || $stamp >= $end) { continue; @@ -743,12 +872,13 @@ private function bucketInPhp( ]; }//end if - if ($groupBy !== null && isset($groupBy['field']) === true) { + $groupFields = $this->resolveGroupFields(groupBy: $groupBy); + if (count($groupFields) > 0) { $groups = $this->computeGrouped( rows: $rows, metric: $metric, field: $field, - groupField: (string) $groupBy['field'] + groupFields: $groupFields ); return [ @@ -862,36 +992,94 @@ private function computeMetric(array $rows, string $metric, mixed $field): int|f }//end computeMetric() /** - * Compute a grouped metric, bucketing rows by `$groupField`. + * Resolve a raw groupBy spec to an ordered list of non-empty group field + * names, honouring every accepted shape (single `{field}`, multi + * `{fields:[...]}`, plain list `[...]`). * - * @param array> $rows Already-filtered rows. - * @param string $metric One of count/sum/avg/min/max/count_distinct. - * @param mixed $field Field to aggregate over. - * @param string $groupField Field used as the bucket key. + * Invalid members (non-string / empty) are dropped defensively for the + * named-annotation path where the spec is not pre-validated by + * {@see AggregationQuery::create()}. Returns an empty list for an + * ungrouped request, which the callers treat as "compute a scalar". * - * @return array + * @param mixed $groupBy Raw groupBy spec from the annotation or query. + * + * @return array Ordered, de-duplicated group field names. + * + * @spec openspec/specs/aggregation-api/spec.md + */ + private function resolveGroupFields(mixed $groupBy): array + { + $fields = []; + foreach (AggregationQuery::normaliseGroupByFields(groupBy: $groupBy) as $field) { + if (is_string($field) === false || $field === '') { + continue; + } + + if (in_array($field, $fields, true) === true) { + continue; + } + + $fields[] = $field; + } + + return $fields; + + }//end resolveGroupFields() + + /** + * Compute a grouped metric, bucketing rows by one or more group fields. + * + * Single-field groupBy yields the backward-compatible `{key, value}` + * row shape. Multi-field (cross-tab) groupBy yields a composite + * `{keys: {fieldA: ..., fieldB: ...}, value}` row per distinct tuple so + * a consumer can pivot the result into a cross-tab. The bucket order is + * the first-seen order of each distinct tuple. + * + * @param array> $rows Already-filtered rows. + * @param string $metric One of count/sum/avg/min/max/count_distinct. + * @param mixed $field Field to aggregate over. + * @param array $groupFields Ordered field(s) used as the bucket key. + * + * @return array, value: int|float|null}> * * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/aggregation-api/spec.md */ - private function computeGrouped(array $rows, string $metric, mixed $field, string $groupField): array + private function computeGrouped(array $rows, string $metric, mixed $field, array $groupFields): array { + $isMulti = (count($groupFields) > 1); $buckets = []; foreach ($rows as $row) { - $bucket = $row[$groupField] ?? null; - $key = is_scalar($bucket) ? (string) $bucket : json_encode($bucket); + $tuple = []; + foreach ($groupFields as $groupField) { + $tuple[$groupField] = ($row[$groupField] ?? null); + } + // Composite cache key over the whole tuple; json_encode keeps + // distinct tuples distinct regardless of scalar/null members. + $key = json_encode($tuple); if (isset($buckets[$key]) === false) { - $buckets[$key] = ['key' => $bucket, 'rows' => []]; + $buckets[$key] = ['tuple' => $tuple, 'rows' => []]; } $buckets[$key]['rows'][] = $row; } $out = []; - foreach ($buckets as $b) { + foreach ($buckets as $bucket) { + $value = $this->computeMetric(rows: $bucket['rows'], metric: $metric, field: $field); + if ($isMulti === true) { + $out[] = [ + 'keys' => $bucket['tuple'], + 'value' => $value, + ]; + continue; + } + + // Single-field: unwrap the tuple to the legacy `{key, value}` shape. $out[] = [ - 'key' => $b['key'], - 'value' => $this->computeMetric(rows: $b['rows'], metric: $metric, field: $field), + 'key' => reset($bucket['tuple']), + 'value' => $value, ]; } @@ -919,7 +1107,12 @@ private function reduceNumeric(array $rows, string $field, callable $reducer, mi } $count++; - $acc = ($acc === null) ? (float) $value : $reducer((float) $acc, (float) $value); + if ($acc === null) { + $acc = (float) $value; + continue; + } + + $acc = $reducer((float) $acc, (float) $value); } if ($count === 0 && $acc === null) { @@ -974,7 +1167,18 @@ private function applyFilter(array $rows, array $filter): array foreach ($filter as $field => $criterion) { $value = $row[$field] ?? null; if (is_array($criterion) === false) { - if ($value !== $criterion) { + // BUG-SVC-9: magic-table column values come back as strings, + // while the criterion may be int/bool/float. A strict !== + // then drops rows the Postgres path would keep (e.g. "1" vs + // 1). Compare as strings when both sides are scalar so the + // PHP fallback matches the native equality semantics; keep + // strict comparison for non-scalars (null/array/object). + if (is_scalar($value) === true && is_scalar($criterion) === true) { + if ((string) $value !== (string) $criterion) { + $keep = false; + break; + } + } else if ($value !== $criterion) { $keep = false; break; } @@ -988,7 +1192,7 @@ private function applyFilter(array $rows, array $filter): array break 2; } } - } + }//end foreach if ($keep === true) { $result[] = $row; @@ -1002,7 +1206,7 @@ private function applyFilter(array $rows, array $filter): array * Apply a single operator check. * * @param mixed $value The value extracted from the row. - * @param string $op Operator name ('eq','ne','gt','gte','lt','lte','in'). + * @param string $op Operator name ('eq','ne','gt','gte','lt','lte','in','notIn'). * @param mixed $opValue The operand value to compare against. * * @return bool True when the value satisfies the operator. @@ -1014,13 +1218,14 @@ private function checkOp(mixed $value, string $op, mixed $opValue): bool $cmp = $this->normaliseForCompare(v: $value); $rhs = $this->normaliseForCompare(v: $opValue); return match ($op) { - 'eq' => $cmp === $rhs, - 'ne' => $cmp !== $rhs, - 'gt' => $cmp !== null && $rhs !== null && $cmp > $rhs, - 'gte' => $cmp !== null && $rhs !== null && $cmp >= $rhs, - 'lt' => $cmp !== null && $rhs !== null && $cmp < $rhs, - 'lte' => $cmp !== null && $rhs !== null && $cmp <= $rhs, - 'in' => is_array($opValue) === true && in_array($value, $opValue, true), + 'eq' => $cmp === $rhs, + 'ne' => $cmp !== $rhs, + 'gt' => $cmp !== null && $rhs !== null && $cmp > $rhs, + 'gte' => $cmp !== null && $rhs !== null && $cmp >= $rhs, + 'lt' => $cmp !== null && $rhs !== null && $cmp < $rhs, + 'lte' => $cmp !== null && $rhs !== null && $cmp <= $rhs, + 'in' => is_array($opValue) === true && in_array($value, $opValue, true), + 'notIn' => is_array($opValue) === false || in_array($value, $opValue, true) === false, default => true, }; }//end checkOp() @@ -1052,8 +1257,16 @@ private function normaliseForCompare(mixed $v): mixed /** * Try to compute the aggregation directly in SQL on the magic table. * - * Supports: count/sum/avg/min/max + simple equality filters - * + optional groupBy on a single field. Postgres only. + * Supports: count/sum/avg/min/max + simple equality/operator filters + * + optional categorical groupBy on ONE OR MORE fields (`GROUP BY a, b` + * → one row per distinct tuple) + optional time-bucketing. The + * categorical groupBy and time-bucket paths run natively on Postgres, + * MySQL and SQLite; the ungrouped scalar path is Postgres-only (the + * others fall through to the PHP fallback). + * + * Multi-field groupBy returns each group row as + * `{keys: {fieldA: ..., fieldB: ...}, value}`; single-field groupBy + * keeps the backward-compatible `{key, value}` shape. * * Returns the result fragment ('value' or 'groups') on success, null * to signal the caller should fall back to PHP-side aggregation. @@ -1063,7 +1276,8 @@ private function normaliseForCompare(mixed $v): mixed * @param string $metric Metric name (count/sum/avg/min/max). * @param string|null $field Field to aggregate over (ignored for count). * @param array $filter Already placeholder-resolved filter map. - * @param array|null $groupBy Optional group spec ({field: ...}). + * @param array|null $groupBy Optional group spec: single-field ({field: ...}), + * multi-field ({fields: [...]}), or a plain list ([...]). * @param array|null $dateBucket Optional time-bucket spec ({field, start, end, gap}). * When supplied, the query becomes a `date_trunc`-bucketed * series with explicit `WHERE field >= start AND field < end` @@ -1094,16 +1308,22 @@ private function tryNativeAggregation( ): ?array { $platformName = $this->detectDatabasePlatform(); + // Ordered list of categorical group fields (single-field, multi-field + // cross-tab, or plain-list shape all normalise here). + $groupFields = $this->resolveGroupFields(groupBy: $groupBy); + // Postgres handles every query shape natively. MySQL and SQLite - // currently support only the time-bucket path; the categorical - // groupBy + ungrouped paths still depend on Postgres-specific - // operators (`::jsonb`, `::numeric`) and fall through to the PHP - // fallback on non-Postgres engines. + // support the time-bucket path AND the categorical groupBy path + // (single- or multi-field) — both reuse the platform-branched + // aggregate SQL + identifier quoting below. The remaining + // non-Postgres gap is the *ungrouped* scalar path, which still + // depends on the Postgres-specific numeric cast and falls through + // to the PHP fallback. if ($platformName === 'unknown') { return null; } - if ($platformName !== 'postgres' && $dateBucket === null) { + if ($platformName !== 'postgres' && $dateBucket === null && count($groupFields) === 0) { return null; } @@ -1111,16 +1331,27 @@ private function tryNativeAggregation( return null; } + // BUG-SVC-7: value metrics (sum/avg/min/max) operate on a column, so a + // null/empty $field would build malformed SQL (e.g. SUM(NULLIF(::text, + // ''))). Bail to the PHP fallback when no field is supplied for a + // value metric; only `count` is valid without a field. + if (in_array($metric, ['sum', 'avg', 'min', 'max'], true) === true + && ($field === null || $field === '') + ) { + return null; + } + // Validate filter shapes are translatable. Supported: // {field: scalar} → field = ? // {field: {in: [...]}} → field IN (?, ?, ?) + // {field: {notIn: [...]}} → field NOT IN (?, ?, ?) // {field: {gt|gte|lt|lte: x}} → field > / >= / < / <= ? // {field: {ne: x}} → field <> ? // Reject anything else. foreach ($filter as $value) { if (is_array($value) === true) { foreach (array_keys($value) as $op) { - if (in_array((string) $op, ['in', 'gt', 'gte', 'lt', 'lte', 'ne'], true) === false) { + if (in_array((string) $op, ['in', 'notIn', 'gt', 'gte', 'lt', 'lte', 'ne'], true) === false) { return null; } } @@ -1190,6 +1421,29 @@ private function tryNativeAggregation( continue; }//end if + if ($op === 'notIn') { + $list = []; + if (is_array($opValue) === true) { + $list = $opValue; + } + + if (count($list) === 0) { + // `notIn` with an empty exclusion list excludes + // nothing — emit an always-true condition so every + // row is retained (mirrors SQL `NOT IN ()` intent). + $whereParts[] = '1 = 1'; + continue; + } + + $placeholders = implode(', ', array_fill(0, count($list), '?')); + $whereParts[] = $quote.$col.$quote.' NOT IN ('.$placeholders.')'; + foreach ($list as $item) { + $bindings[] = $this->bindValue(value: $item); + } + + continue; + }//end if + $sqlOp = match ((string) $op) { 'gt' => '>', 'gte' => '>=', @@ -1299,13 +1553,23 @@ private function tryNativeAggregation( return ['groups' => $groups]; }//end if - if ($groupBy !== null && isset($groupBy['field']) === true) { - $groupCol = '"'.$this->sanitizeColumnName(name: (string) $groupBy['field']).'"'; - $sql = "SELECT {$groupCol} AS bucket, {$aggSql} AS agg + if (count($groupFields) > 0) { + $isMulti = (count($groupFields) > 1); + $selectParts = []; + $groupCols = []; + foreach ($groupFields as $index => $groupField) { + $groupCol = $quote.$this->sanitizeColumnName(name: $groupField).$quote; + $groupCols[] = $groupCol; + $selectParts[] = $groupCol.' AS g'.$index; + } + + $selectSql = implode(', ', $selectParts); + $groupSql = implode(', ', $groupCols); + $sql = "SELECT {$selectSql}, {$aggSql} AS agg FROM {$fullTable} WHERE {$whereSql} - GROUP BY {$groupCol}"; - $stmt = $this->db->prepare($sql); + GROUP BY {$groupSql}"; + $stmt = $this->db->prepare($sql); $stmt->execute($bindings); $groups = []; while (($row = $stmt->fetch()) !== false) { @@ -1316,8 +1580,18 @@ private function tryNativeAggregation( $value = (int) $value; } - $groups[] = ['key' => $row['bucket'], 'value' => $value]; - } + if ($isMulti === true) { + $keys = []; + foreach ($groupFields as $index => $groupField) { + $keys[$groupField] = ($row['g'.$index] ?? null); + } + + $groups[] = ['keys' => $keys, 'value' => $value]; + continue; + } + + $groups[] = ['key' => ($row['g0'] ?? null), 'value' => $value]; + }//end while return ['groups' => $groups]; }//end if @@ -1362,9 +1636,7 @@ private function tryNativeAggregation( * `2026-05-21 13:00:00`. We need a stable wire format that the * client can parse identically across Postgres versions / timezone * settings AND across the PHP-fallback path on non-Postgres - * databases. Mirrors the behaviour the - * `SolrAggregationQueryBuilder` and - * `ElasticsearchAggregationQueryBuilder` produce. + * databases. * * @param mixed $raw Raw bucket value from the DB row (typically a string). * @@ -1381,8 +1653,25 @@ private function coerceBucketKey(mixed $raw): string return (string) $raw; } - // Try strtotime then format — defensive but covers every Postgres - // text-cast shape we'll see. + // BUG-SVC-4: Postgres emits date/timestamp text WITHOUT a timezone + // designator (e.g. "2026-06-01 00:00:00"). strtotime() parses such + // offset-less text in the SERVER timezone, then gmdate() re-expresses + // it as UTC, shifting every bucket label by the server's UTC offset + // (e.g. CET buckets land an hour early). Parse offset-less shapes as + // UTC explicitly; only fall back to strtotime for offset-bearing text. + $hasTimezone = (preg_match('/(Z|[+-]\d{2}:?\d{2})$/', trim($raw)) === 1); + if ($hasTimezone === false) { + $formats = ['Y-m-d H:i:s', 'Y-m-d\TH:i:s', 'Y-m-d H:i', 'Y-m-d']; + foreach ($formats as $format) { + $parsed = DateTimeImmutable::createFromFormat($format, trim($raw), new DateTimeZone('UTC')); + if ($parsed !== false) { + return $parsed->format('Y-m-d\TH:i:s\Z'); + } + } + } + + // Offset-bearing (or otherwise unhandled) shapes: strtotime understands + // the embedded offset, so converting to UTC via gmdate is correct here. $stamp = strtotime($raw); if ($stamp === false) { return $raw; @@ -1788,12 +2077,13 @@ private function runCrossSchema( 'truncated' => $truncated, ]; - if (is_array($groupBy) === true && isset($groupBy['field']) === true) { + $crossGroupFields = $this->resolveGroupFields(groupBy: $groupBy); + if (count($crossGroupFields) > 0) { $result['groups'] = $this->computeGrouped( rows: $rows, metric: $metric, field: $field, - groupField: (string) $groupBy['field'] + groupFields: $crossGroupFields ); } @@ -1971,28 +2261,4 @@ private function getAnnotation(Schema $schema): ?array return null; }//end getAnnotation() - - /** - * Map a SearchBackendInterface implementation to its short backend - * label for the result envelope. Falls back to `'external'` when the - * concrete class name doesn't match a known prefix. - * - * @param SearchBackendInterface $backend The backend instance. - * - * @return string Short backend label ('solr', 'elasticsearch', or 'external'). - */ - private function detectBackendName(SearchBackendInterface $backend): string - { - $shortName = (new ReflectionClass($backend))->getShortName(); - if (str_contains($shortName, 'Solr') === true) { - return 'solr'; - } - - if (str_contains($shortName, 'Elasticsearch') === true) { - return 'elasticsearch'; - } - - return 'external'; - - }//end detectBackendName() }//end class diff --git a/lib/Service/Aggregation/ElasticsearchAggregationQueryBuilder.php b/lib/Service/Aggregation/ElasticsearchAggregationQueryBuilder.php deleted file mode 100644 index 1383a0cfc0..0000000000 --- a/lib/Service/Aggregation/ElasticsearchAggregationQueryBuilder.php +++ /dev/null @@ -1,215 +0,0 @@ -/_search` endpoint. - * - * - count + ungrouped → `{ size: 0, track_total_hits: true, query: }` - * - count + groupBy → `{ size: 0, query: , aggs: { : { terms: {...} } } }` - * - sum/avg/min/max → `{ size: 0, aggs: { metric: { : { field: ... } } } }` - * - sum/avg/min/max + grp → `{ size: 0, aggs: { : { terms: {...}, - * aggs: { metric: { : { field: ... } } } } } }` - * - * Pure-PHP translation. The ES backend wraps a thin HTTP client around - * this builder. Unit-testable independently. - * - * SPDX-License-Identifier: EUPL-1.2 - * SPDX-FileCopyrightText: 2026 Conduction B.V. - * - * @category Service - * @package OCA\OpenRegister\Service\Aggregation - * - * @author Conduction Development Team - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @link https://www.OpenRegister.app - * - * @spec openspec/changes/aggregations-backend-native/tasks.md "ElasticsearchBackend::aggregate" - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Aggregation; - -/** - * Builds Elasticsearch query DSL from an AggregationQuery. - * - * @SuppressWarnings(PHPMD.ElseExpression) Each branch in build() emits a - * *different* aggs nesting shape (top-level metric vs. nested-under-terms); - * flattening would duplicate the metric-spec construction. - */ -class ElasticsearchAggregationQueryBuilder -{ - /** - * Translate an AggregationQuery into an ES `_search` request body. - * - * @param AggregationQuery $query The cross-backend request. - * - * @return array The ES request body. - * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-2 - */ - public function build(AggregationQuery $query): array - { - $body = [ - 'size' => 0, - 'track_total_hits' => true, - ]; - - $bool = $this->translateFilters(filter: $query->filter); - if ($bool !== []) { - $body['query'] = ['bool' => $bool]; - } - - if ($query->metric === AggregationQuery::METRIC_COUNT) { - if ($query->isGrouped() === true) { - $field = (string) $query->getGroupByField(); - $body['aggs'] = [ - $field => [ - 'terms' => ['field' => $field, 'size' => 1000], - ], - ]; - } - - if ($query->hasDateBucket() === true) { - $bucket = $query->dateBucket; - $field = (string) $bucket['field']; - $body['aggs'] = [ - $field => [ - 'date_histogram' => [ - 'field' => $field, - 'calendar_interval' => (string) $bucket['gap'], - 'extended_bounds' => [ - 'min' => (string) $bucket['start'], - 'max' => (string) $bucket['end'], - ], - ], - ], - ]; - } - - return $body; - }//end if - - // Non-count metrics use the matching ES metric aggregation. - $field = (string) $query->field; - $metricKey = 'metric_'.$query->metric; - $metricSpec = [ - $metricKey => [$query->metric => ['field' => $field]], - ]; - - if ($query->isGrouped() === true) { - $groupField = (string) $query->getGroupByField(); - $body['aggs'] = [ - $groupField => [ - 'terms' => ['field' => $groupField, 'size' => 1000], - 'aggs' => $metricSpec, - ], - ]; - } else { - $body['aggs'] = $metricSpec; - } - - return $body; - - }//end build() - - /** - * Translate the filter map into a `bool` query. - * - * @param array $filter The filter map. - * - * @return array The `bool` clause body, or empty array when no filters. - * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 - */ - public function translateFilters(array $filter): array - { - $must = []; - $mustNot = []; - - foreach ($filter as $field => $value) { - if (is_array($value) === false) { - $must[] = ['term' => [(string) $field => $value]]; - continue; - } - - foreach ($value as $op => $opValue) { - $this->collectOp( - field: (string) $field, - op: (string) $op, - value: $opValue, - must: $must, - mustNot: $mustNot - ); - } - } - - $bool = []; - if ($must !== []) { - $bool['must'] = $must; - } - - if ($mustNot !== []) { - $bool['must_not'] = $mustNot; - } - - return $bool; - - }//end translateFilters() - - /** - * Add the (field, op, value) triple to the must / must_not lists. - * - * @param string $field The field name. - * @param string $op The operator. - * @param mixed $value The operand. - * @param array $must Accumulator for must clauses. - * @param array $mustNot Accumulator for must_not clauses. - * - * @return void - * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 - */ - private function collectOp(string $field, string $op, mixed $value, array &$must, array &$mustNot): void - { - switch ($op) { - case 'in': - $list = []; - if (is_array($value) === true) { - $list = $value; - } - - if (count($list) === 0) { - // `in` with empty list never matches. - $must[] = ['term' => ['_or_no_match_' => '___']]; - return; - } - - $must[] = ['terms' => [$field => array_values($list)]]; - return; - case 'ne': - $mustNot[] = ['term' => [$field => $value]]; - return; - case 'gt': - $must[] = ['range' => [$field => ['gt' => $value]]]; - return; - case 'gte': - $must[] = ['range' => [$field => ['gte' => $value]]]; - return; - case 'lt': - $must[] = ['range' => [$field => ['lt' => $value]]]; - return; - case 'lte': - $must[] = ['range' => [$field => ['lte' => $value]]]; - return; - default: - return; - }//end switch - - }//end collectOp() -}//end class diff --git a/lib/Service/Aggregation/SolrAggregationQueryBuilder.php b/lib/Service/Aggregation/SolrAggregationQueryBuilder.php deleted file mode 100644 index 61c45bce22..0000000000 --- a/lib/Service/Aggregation/SolrAggregationQueryBuilder.php +++ /dev/null @@ -1,238 +0,0 @@ -` - * - count + groupBy → `rows=0&facet=true&facet.field=` - * - sum/avg/min/max → `stats=true&stats.field=` (StatsComponent) - * - sum/avg/min/max + grp → `json.facet={:{type:terms, field:, - * facet:{m:"()"}}}` - * - * Pure-PHP translation — no HTTP. The Solr backend wraps a thin HTTP - * client around this builder. Unit-testable independently. - * - * SPDX-License-Identifier: EUPL-1.2 - * SPDX-FileCopyrightText: 2026 Conduction B.V. - * - * @category Service - * @package OCA\OpenRegister\Service\Aggregation - * - * @author Conduction Development Team - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @link https://www.OpenRegister.app - * - * @spec openspec/changes/aggregations-backend-native/tasks.md "SolrSearchBackend::aggregate" - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Aggregation; - -/** - * Builds Solr request parameters from an AggregationQuery. - * - * @SuppressWarnings(PHPMD.ElseExpression) - * Each branch in build() emits a *different* parameter shape - * (StatsComponent vs JSON Facet API); the alternative — early-returns - * with duplicated array assembly — is harder to read. - * @SuppressWarnings(PHPMD.ShortVariable) - * `$fq` is Solr's filter-query parameter name; `$s` is a one-line - * stringification accumulator. - */ -class SolrAggregationQueryBuilder -{ - /** - * Translate an AggregationQuery into Solr request parameters. - * - * @param AggregationQuery $query The cross-backend request. - * - * @return array The Solr request parameter map. - * - * @spec openspec/changes/retrofit-2026-05-24-aggregations-backend-native/tasks.md#task-2 - */ - public function build(AggregationQuery $query): array - { - $params = [ - 'q' => '*:*', - 'rows' => 0, - 'wt' => 'json', - ]; - - $fq = $this->translateFilters(filter: $query->filter); - if ($fq !== []) { - $params['fq'] = $fq; - } - - if ($query->metric === AggregationQuery::METRIC_COUNT) { - if ($query->isGrouped() === true) { - $params['facet'] = 'true'; - $params['facet.field'] = $query->getGroupByField(); - $params['facet.mincount'] = 1; - } - - if ($query->hasDateBucket() === true) { - $bucket = $query->dateBucket; - $params['facet'] = 'true'; - $params['facet.range'] = (string) $bucket['field']; - $params['facet.range.start'] = (string) $bucket['start']; - $params['facet.range.end'] = (string) $bucket['end']; - $params['facet.range.gap'] = '+1'.strtoupper((string) $bucket['gap']); - } - - return $params; - } - - // Non-count metrics: use Solr StatsComponent for ungrouped + JSON - // Facet API for grouped — both standard since Solr 7.x. - $field = (string) $query->field; - if ($query->isGrouped() === true) { - $groupField = (string) $query->getGroupByField(); - $params['json.facet'] = (string) json_encode( - [ - $groupField => [ - 'type' => 'terms', - 'field' => $groupField, - 'facet' => ['m' => $query->metric.'('.$field.')'], - ], - ] - ); - } else { - $params['stats'] = 'true'; - $params['stats.field'] = $field; - } - - return $params; - - }//end build() - - /** - * Translate the AggregationQuery filter map into a list of Solr `fq` - * filter-query strings. Each entry is one independent filter (Solr - * AND-composes them). - * - * Supported per-field shapes: - * - scalar → `field:"value"` - * - {in: [a, b, c]} → `field:(a OR b OR c)` - * - {gt|gte|lt|lte: x} → `field:[x TO *]` etc. - * - {ne: x} → `-field:"x"` - * - * @param array $filter The filter map. - * - * @return string[] List of Solr `fq` strings. - * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 - */ - public function translateFilters(array $filter): array - { - $out = []; - foreach ($filter as $field => $value) { - if (is_array($value) === false) { - $out[] = $field.':'.$this->quote(value: $value); - continue; - } - - foreach ($value as $op => $opValue) { - $clause = $this->translateOp(field: (string) $field, op: (string) $op, value: $opValue); - if ($clause !== null) { - $out[] = $clause; - } - } - } - - return $out; - - }//end translateFilters() - - /** - * Translate a single (op, value) pair on a field to Solr fq syntax. - * - * @param string $field The field name. - * @param string $op The operator (in / gt / gte / lt / lte / ne). - * @param mixed $value The operand. - * - * @return ?string The fq clause, or null when the op is unrecognised. - * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-19 - */ - private function translateOp(string $field, string $op, mixed $value): ?string - { - switch ($op) { - case 'in': - $list = []; - if (is_array($value) === true) { - $list = $value; - } - - if (count($list) === 0) { - return $field.':("__or_no_match__")'; - } - - $quoted = array_map(fn($v) => $this->quote(value: $v), $list); - return $field.':('.implode(' OR ', $quoted).')'; - case 'ne': - return '-'.$field.':'.$this->quote(value: $value); - case 'gt': - return $field.':{'.$this->bound(value: $value).' TO *}'; - case 'gte': - return $field.':['.$this->bound(value: $value).' TO *]'; - case 'lt': - return $field.':{* TO '.$this->bound(value: $value).'}'; - case 'lte': - return $field.':[* TO '.$this->bound(value: $value).']'; - default: - return null; - }//end switch - - }//end translateOp() - - /** - * Quote a scalar for Solr fq syntax. Strings get double-quoted and - * escaped; numerics + booleans pass through. - * - * @param mixed $value The value to quote. - * - * @return string - */ - private function quote(mixed $value): string - { - if (is_bool($value) === true) { - if ($value === true) { - return 'true'; - } - - return 'false'; - } - - if (is_int($value) === true || is_float($value) === true) { - return (string) $value; - } - - $s = (string) $value; - return '"'.str_replace(['\\', '"'], ['\\\\', '\\"'], $s).'"'; - - }//end quote() - - /** - * Format a range bound value for Solr fq. - * - * @param mixed $value The bound. - * - * @return string - */ - private function bound(mixed $value): string - { - if (is_int($value) === true || is_float($value) === true) { - return (string) $value; - } - - return $this->quote(value: $value); - - }//end bound() -}//end class diff --git a/lib/Service/Aggregation/ThresholdEvaluationService.php b/lib/Service/Aggregation/ThresholdEvaluationService.php new file mode 100644 index 0000000000..4559d832f0 --- /dev/null +++ b/lib/Service/Aggregation/ThresholdEvaluationService.php @@ -0,0 +1,264 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Aggregation; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Service\Notification\AnnotationNotificationDispatcher; +use OCP\ICache; +use OCP\ICacheFactory; +use Psr\Log\LoggerInterface; + +/** + * Evaluates threshold-typed notifications declared on a schema. + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-2.3 + */ +class ThresholdEvaluationService +{ + + private const STATE_ABOVE = 'above'; + private const STATE_BELOW = 'below'; + + /** + * Distributed cache holding the last threshold state per (schema, notification). + * + * @var ICache|null + */ + private ?ICache $stateCache = null; + + /** + * Wire collaborators and prepare the state cache. + * + * @param AggregationRunner $aggregationRunner Runner that computes aggregation values. + * @param AnnotationNotificationDispatcher $dispatcher Notification dispatcher. + * @param LoggerInterface $logger PSR logger for warnings. + * @param ICacheFactory $cacheFactory Distributed-cache factory. + * + * @return void + */ + public function __construct( + private readonly AggregationRunner $aggregationRunner, + private readonly AnnotationNotificationDispatcher $dispatcher, + private readonly LoggerInterface $logger, + ICacheFactory $cacheFactory + ) { + try { + $this->stateCache = $cacheFactory->createDistributed('openregister_threshold_state'); + } catch (\Throwable $e) { + $this->stateCache = null; + } + }//end __construct() + + /** + * Whether a schema declares at least one threshold-typed notification. + * + * Cheap config-array scan used as the listener's inline enqueue gate. + * + * @param Schema $schema Schema to inspect. + * + * @return bool True when a threshold trigger is declared. + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function hasThresholdNotifications(Schema $schema): bool + { + $config = ($schema->getConfiguration() ?? []); + $notifications = ($config['x-openregister-notifications'] ?? null); + if (is_array($notifications) === false) { + return false; + } + + foreach ($notifications as $spec) { + if (is_array($spec) === false) { + continue; + } + + $trigger = ($spec['trigger'] ?? null); + if (is_array($trigger) === true && (string) ($trigger['type'] ?? '') === 'threshold') { + return true; + } + } + + return false; + }//end hasThresholdNotifications() + + /** + * Re-evaluate every threshold notification declared on the schema. + * + * Per-notification failures are logged and do not abort the remaining + * evaluations (behaviour carried over from the listener). + * + * @param Schema $schema Schema declaring the notifications. + * @param ObjectEntity $object Object whose write triggered the evaluation. + * + * @return void + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function evaluateSchema(Schema $schema, ObjectEntity $object): void + { + $config = ($schema->getConfiguration() ?? []); + $notifications = ($config['x-openregister-notifications'] ?? null); + if (is_array($notifications) === false || count($notifications) === 0) { + return; + } + + foreach ($notifications as $name => $spec) { + if (is_array($spec) === false) { + continue; + } + + $trigger = ($spec['trigger'] ?? null); + if (is_array($trigger) === false || (string) ($trigger['type'] ?? '') !== 'threshold') { + continue; + } + + try { + $this->evaluate(schema: $schema, notificationName: (string) $name, trigger: $trigger, object: $object); + } catch (\Throwable $e) { + $this->logger->warning( + sprintf( + '[ThresholdEvaluationService] evaluation of "%s" failed: %s', + (string) $name, + $e->getMessage() + ) + ); + } + }//end foreach + }//end evaluateSchema() + + /** + * Evaluate one notification spec and dispatch on rising-edge crossings. + * + * @param Schema $schema Schema declaring the notification. + * @param string $notificationName Notification key in the schema config. + * @param array $trigger Trigger configuration block. + * @param ObjectEntity $object Object that just changed. + * + * @return void + * + * @SuppressWarnings("PHPMD.CyclomaticComplexity") + */ + private function evaluate(Schema $schema, string $notificationName, array $trigger, ObjectEntity $object): void + { + $aggregationName = (string) ($trigger['aggregation'] ?? ''); + $op = (string) ($trigger['op'] ?? ''); + $threshold = ($trigger['value'] ?? null); + if ($aggregationName === '' || $op === '' || $threshold === null) { + return; + } + + $registerSlug = (string) $object->getRegister(); + $schemaSlug = (string) $schema->getSlug(); + + // BypassRbac: threshold evaluation is a system reaction to a + // write event, not a user-driven request. The write itself + // already passed RBAC; the threshold evaluation's authoritative + // reason is "this object just changed", independent of the + // active session's `list` permission on the schema. + $result = $this->aggregationRunner->run( + registerRef: $registerSlug, + schemaRef: $schemaSlug, + name: $aggregationName, + bypassRbac: true + ); + $value = ($result['value'] ?? null); + if (is_int($value) === false && is_float($value) === false) { + return; + } + + $isAbove = $this->compare(actual: $value, op: $op, expected: $threshold); + $newState = self::STATE_BELOW; + if ($isAbove === true) { + $newState = self::STATE_ABOVE; + } + + $stateKey = sprintf('threshold:%d:%s', $schema->getId(), $notificationName); + $oldState = $this->stateCache?->get($stateKey); + + if ($newState === self::STATE_ABOVE && $oldState !== self::STATE_ABOVE) { + $this->dispatcher->dispatch( + $object, + 'threshold', + [ + 'notificationName' => $notificationName, + 'aggregation' => $aggregationName, + 'value' => $value, + 'threshold' => $threshold, + 'op' => $op, + ] + ); + } + + try { + // 30 day TTL; long enough that a slow-moving threshold series + // still has continuity across maintenance restarts. + $this->stateCache?->set($stateKey, $newState, (60 * 60 * 24 * 30)); + } catch (\Throwable $e) { + // Don't escalate. + } + }//end evaluate() + + /** + * Compare two values with the configured operator. + * + * @param int|float $actual Numeric value computed from the aggregation. + * @param string $op Comparison operator (gt|gte|lt|lte|eq|ne). + * @param mixed $expected Threshold value, must be numeric. + * + * @return bool True when the comparison is satisfied, false otherwise. + */ + private function compare($actual, string $op, $expected): bool + { + if (is_numeric($expected) === false) { + return false; + } + + $rhs = (float) $expected; + $lhs = (float) $actual; + return match ($op) { + 'gt' => $lhs > $rhs, + 'gte' => $lhs >= $rhs, + 'lt' => $lhs < $rhs, + 'lte' => $lhs <= $rhs, + 'eq' => $lhs === $rhs, + 'ne' => $lhs !== $rhs, + default => false, + }; + }//end compare() +}//end class diff --git a/lib/Service/Aggregation/TimeseriesRequestValidator.php b/lib/Service/Aggregation/TimeseriesRequestValidator.php index ae56bf732a..9dc114bde9 100644 --- a/lib/Service/Aggregation/TimeseriesRequestValidator.php +++ b/lib/Service/Aggregation/TimeseriesRequestValidator.php @@ -29,7 +29,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/add-time-bucket-aggregation/specs/aggregation-api/spec.md + * @spec openspec/specs/aggregation-api/spec.md */ declare(strict_types=1); @@ -115,7 +115,7 @@ class TimeseriesRequestValidator * AggregationQuery::create() is the canonical, validated factory * for the value object. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-2 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function validate(array $input, Schema $schema): AggregationQuery { diff --git a/lib/Service/Aggregation/WidgetAnnotationValidator.php b/lib/Service/Aggregation/WidgetAnnotationValidator.php index 566c13c632..4a66ca6167 100644 --- a/lib/Service/Aggregation/WidgetAnnotationValidator.php +++ b/lib/Service/Aggregation/WidgetAnnotationValidator.php @@ -81,7 +81,7 @@ final class WidgetAnnotationValidator * * @return array * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-2 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function validate(array $schema): array { diff --git a/lib/Service/AnalyticsLinkService.php b/lib/Service/AnalyticsLinkService.php index 3df8d11346..1b3773ce78 100644 --- a/lib/Service/AnalyticsLinkService.php +++ b/lib/Service/AnalyticsLinkService.php @@ -196,7 +196,7 @@ private function resolveReportService(): ?object * @throws Exception On missing user (401), missing report (404), * duplicate (409), Analytics unavailable (503). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function linkReport(string $objectUuid, int $registerId, int $schemaId, int $reportId): AnalyticsLink { @@ -248,7 +248,7 @@ public function linkReport(string $objectUuid, int $registerId, int $schemaId, i * * @SuppressWarnings(PHPMD.ExcessiveParameterList) * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function createAndLinkReport( string $objectUuid, @@ -327,7 +327,7 @@ public function createAndLinkReport( * * @throws Exception On missing user (401) or no matching link (404). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function unlinkReport(string $objectUuid, int $reportId): void { @@ -350,7 +350,7 @@ public function unlinkReport(string $objectUuid, int $reportId): void * * @return array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getLinkedReports(string $objectUuid): array { @@ -382,7 +382,7 @@ public function getLinkedReports(string $objectUuid): array * * @return array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getAvailableReports(?string $search=null): array { diff --git a/lib/Service/AnalyticsSeriesService.php b/lib/Service/AnalyticsSeriesService.php new file mode 100644 index 0000000000..22d0f5188f --- /dev/null +++ b/lib/Service/AnalyticsSeriesService.php @@ -0,0 +1,277 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use DateTime; +use InvalidArgumentException; +use OCA\OpenRegister\Db\AnalyticsSeries; +use OCA\OpenRegister\Db\AnalyticsSeriesMapper; +use OCA\OpenRegister\Service\Integration\IntegrationRegistry; +use OCP\IGroupManager; +use OCP\IUserSession; + +/** + * AnalyticsSeriesService — page-level series register / fetch. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Service composes the series mapper, integration + * registry, user session and group manager — each is required for the register / fetch / RBAC contract. + */ +class AnalyticsSeriesService +{ + + /** + * Allowed visibility tiers. + * + * @var string[] + */ + private const VISIBILITIES = ['private', 'group', 'public']; + + /** + * Allowed chart types the render layer understands. + * + * @var string[] + */ + private const CHART_TYPES = ['line', 'bar', 'pie', 'doughnut', 'area', 'radar']; + + /** + * Constructor. + * + * @param AnalyticsSeriesMapper $mapper Series persistence. + * @param IntegrationRegistry $registry Registry for the page-widget surface. + * @param IUserSession $userSession Current user. + * @param IGroupManager $groupManager Group / admin checks. + * + * @return void + */ + public function __construct( + private AnalyticsSeriesMapper $mapper, + private IntegrationRegistry $registry, + private IUserSession $userSession, + private IGroupManager $groupManager, + ) { + }//end __construct() + + /** + * Authorisation guard for the register (write) path — a series may + * only be registered by an authenticated user. Throws fail-closed + * (ADR-005) so the controller surfaces 400/401 rather than silently + * writing. Called explicitly from the controller so the authz + * decision is visible at the endpoint boundary. + * + * @return void + * + * @throws InvalidArgumentException When no user is logged in. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function ensureCanRegister(): void + { + if ($this->userSession->getUser() === null) { + throw new InvalidArgumentException('A logged-in user is required to register a series'); + } + }//end ensureCanRegister() + + /** + * Authorisation guard for the fetch (read) path — resolves the series + * RBAC-scoped and returns it only when the current user may read it. + * Returns null on unknown OR disallowed so the controller answers a + * uniform 404 (no enumeration oracle, ADR-005 fail-closed). Called + * explicitly from the controller so the authz decision is visible at + * the endpoint boundary. + * + * @param string $seriesKey The series key. + * + * @return array|null The render contract, or null. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function ensureReadableOrNull(string $seriesKey): ?array + { + return $this->fetch(seriesKey: $seriesKey); + }//end ensureReadableOrNull() + + /** + * Register (create or replace) a page-level series. + * + * Upserts by `seriesKey`: re-registering the same key updates the + * payload in place so a leaf can refresh its dashboard data without + * accumulating duplicates. Also (re)declares the matching page widget + * on the IntegrationRegistry so the render layer can discover it. + * + * @param string $seriesKey Stable series key. + * @param array $labels X-axis labels. + * @param array> $datasets Named datasets. + * @param string|null $title Chart title. + * @param string $chartType Chart type hint. + * @param string $visibility 'private'|'group'|'public'. + * @param int|null $registerId Optional register scope. + * @param int|null $schemaId Optional schema scope. + * + * @return array The stored series render contract. + * + * @throws InvalidArgumentException On invalid key / visibility / chart type. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function register( + string $seriesKey, + array $labels, + array $datasets, + ?string $title=null, + string $chartType='line', + string $visibility='private', + ?int $registerId=null, + ?int $schemaId=null + ): array { + if (trim($seriesKey) === '') { + throw new InvalidArgumentException('seriesKey is required to register a series'); + } + + if (in_array($visibility, self::VISIBILITIES, true) === false) { + throw new InvalidArgumentException('Invalid visibility — expected one of: '.implode(', ', self::VISIBILITIES)); + } + + if (in_array($chartType, self::CHART_TYPES, true) === false) { + throw new InvalidArgumentException('Invalid chartType — expected one of: '.implode(', ', self::CHART_TYPES)); + } + + $user = $this->userSession->getUser(); + if ($user === null) { + throw new InvalidArgumentException('A logged-in user is required to register a series'); + } + + $now = new DateTime(); + $existing = $this->mapper->findByKey($seriesKey); + + $entity = ($existing ?? new AnalyticsSeries()); + $entity->setSeriesKey($seriesKey); + $entity->setRegisterId($registerId); + $entity->setSchemaId($schemaId); + $entity->setTitle($title); + $entity->setChartType($chartType); + $entity->setLabels(json_encode(array_values($labels))); + $entity->setDatasets(json_encode(array_values($datasets))); + $entity->setVisibility($visibility); + $entity->setUpdatedAt($now); + + if ($existing === null) { + $entity->setCreatedBy($user->getUID()); + $entity->setCreatedAt($now); + $saved = $this->mapper->insert($entity); + } else { + $saved = $this->mapper->update($entity); + } + + // (Re)declare the page widget so the render layer can discover it. + // First-write-wins inside the registry; harmless on a refresh. + $this->registry->registerPageWidget( + [ + 'id' => 'analytics-series:'.$seriesKey, + 'type' => 'chart', + 'title' => $title, + 'providerId' => 'analytics-series', + 'config' => [ + 'seriesKey' => $seriesKey, + 'chartType' => $chartType, + 'registerId' => $registerId, + 'schemaId' => $schemaId, + ], + ] + ); + + return $saved->jsonSerialize(); + }//end register() + + /** + * Fetch a registered series by key, RBAC-scoped. + * + * Returns null when the series is unknown or the current user is not + * permitted to read it, so the controller can return a uniform 404. + * + * @param string $seriesKey The series key. + * + * @return array|null The render contract, or null. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function fetch(string $seriesKey): ?array + { + $series = $this->mapper->findByKey($seriesKey); + if ($series === null) { + return null; + } + + if ($this->canRead(series: $series) === false) { + return null; + } + + return $series->jsonSerialize(); + }//end fetch() + + /** + * Whether the current user may read the given series. + * + * - 'public' — anyone. + * - 'group'/'private' — the creator or an admin. + * + * @param AnalyticsSeries $series The series. + * + * @return bool True when readable. + */ + private function canRead(AnalyticsSeries $series): bool + { + if ($series->getVisibility() === 'public') { + return true; + } + + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + $uid = $user->getUID(); + if ($series->getCreatedBy() === $uid) { + return true; + } + + return $this->groupManager->isAdmin($uid); + }//end canRead() +}//end class diff --git a/lib/Service/Anonymisation/AnonymisationBackendService.php b/lib/Service/Anonymisation/AnonymisationBackendService.php new file mode 100644 index 0000000000..62e7e12261 --- /dev/null +++ b/lib/Service/Anonymisation/AnonymisationBackendService.php @@ -0,0 +1,668 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Anonymisation; + +use OCA\OpenRegister\Service\Settings\FileSettingsHandler; +use OCP\App\IAppManager; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use OCP\IAppConfig; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\Server; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Single source of truth for anonymisation backend selection and availability. + * + * Owns the only implementation of the effectiveMethod precedence rule, the + * AppAPI/IAppManager-based detection for OpenAnonymiser, the HTTP probes for + * Presidio, and the probe-result cache. All consumers (in-process callers, the + * OCS controller, the admin UI) MUST obtain state via getState(). + * + * @category Service + * @package OCA\OpenRegister\Service\Anonymisation + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Aggregates several OCP services by design. + */ +class AnonymisationBackendService +{ + /** + * App id of the AppAPI app required for ExApp detection. + */ + private const APP_API = 'app_api'; + + /** + * App id of the full OpenAnonymiser ExApp (preferred variant). + */ + private const EXAPP_FULL = 'openanonymiser'; + + /** + * App id of the light OpenAnonymiser ExApp. + */ + private const EXAPP_LIGHT = 'openanonymiser_light'; + + /** + * IAppConfig key holding the probe-cache TTL in seconds. + */ + private const TTL_CONFIG_KEY = 'anonymisation.probe_cache_ttl'; + + /** + * Default probe-cache TTL in seconds. + */ + private const TTL_DEFAULT = 60; + + /** + * Minimum allowed probe-cache TTL in seconds. + */ + private const TTL_MIN = 10; + + /** + * Maximum allowed probe-cache TTL in seconds. + */ + private const TTL_MAX = 600; + + /** + * Distributed probe-result cache, or null when no backend is available. + * + * @var ICache|null + */ + private ?ICache $cache = null; + + /** + * Constructor. + * + * @param IAppManager $appManager App manager used for ExApp detection. + * @param IAppConfig $appConfig App configuration store. + * @param ICacheFactory $cacheFactory Factory for the distributed probe cache. + * @param IClientService $clientService HTTP client factory for endpoint probes. + * @param FileSettingsHandler $fileSettingsHandler Reads the stored entity-recognition settings. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly IAppConfig $appConfig, + ICacheFactory $cacheFactory, + private readonly IClientService $clientService, + private readonly FileSettingsHandler $fileSettingsHandler, + private readonly LoggerInterface $logger, + ) { + try { + $this->cache = $cacheFactory->createDistributed('openregister_anon_probe'); + } catch (Throwable $e) { + $this->logger->warning('[AnonymisationBackendService] cache backend unavailable: '.$e->getMessage()); + $this->cache = null; + } + }//end __construct() + + /** + * Resolve the full backend selection state. + * + * @return BackendState The fully-resolved state. + */ + public function getState(): BackendState + { + $settings = $this->fileSettingsHandler->getFileSettingsOnly(); + $enabled = (bool) ($settings['entityRecognitionEnabled'] ?? false); + $stored = (string) ($settings['entityRecognitionMethod'] ?? BackendState::METHOD_AUTO); + + // Probe the atomic backends (cached) and build their info records. + $backends = []; + + $probeMethods = [ + BackendState::METHOD_REGEX, + BackendState::METHOD_PRESIDIO, + BackendState::METHOD_OPENANONYMISER, + BackendState::METHOD_LLM, + ]; + foreach ($probeMethods as $method) { + $backends[$method] = $this->buildBackendInfo(method: $method, probe: $this->probe(method: $method)); + } + + // Hybrid availability is the logical AND of its composed backends. + $backends[BackendState::METHOD_HYBRID] = $this->buildHybridInfo(backends: $backends); + + $activeMethod = $this->resolveActiveMethod(stored: $stored, backends: $backends); + $effectiveMethod = $this->resolveEffectiveMethod(enabled: $enabled, activeMethod: $activeMethod, backends: $backends); + + return new BackendState( + entityRecognitionEnabled: $enabled, + activeMethod: $activeMethod, + effectiveMethod: $effectiveMethod, + backends: $backends, + ); + }//end getState() + + /** + * Issue a fresh probe for a single method, bypassing and refreshing the cache. + * + * @param string $method One of BackendState::METHODS. + * + * @return ProbeResult The fresh probe result. + */ + public function testConnection(string $method): ProbeResult + { + $result = $this->freshProbe(method: $method); + $this->writeCache(method: $method, result: $result); + + return $result; + }//end testConnection() + + /** + * Probe a method, consuming the cache when a fresh-enough entry exists. + * + * @param string $method One of BackendState::METHODS. + * + * @return ProbeResult The (possibly cached) probe result. + */ + public function probe(string $method): ProbeResult + { + $cached = $this->readCache(method: $method); + if ($cached !== null) { + return $cached; + } + + $result = $this->freshProbe(method: $method); + $this->writeCache(method: $method, result: $result); + + return $result; + }//end probe() + + /** + * Produce a fresh probe result for a method, ignoring the cache. + * + * @param string $method One of BackendState::METHODS. + * + * @return ProbeResult The fresh probe result. + */ + private function freshProbe(string $method): ProbeResult + { + switch ($method) { + case BackendState::METHOD_REGEX: + return new ProbeResult(reachable: true, latencyMs: 0, error: null, probedAt: $this->now()); + + case BackendState::METHOD_OPENANONYMISER: + return $this->detectOpenAnonymiser(); + + case BackendState::METHOD_PRESIDIO: + return $this->probePresidio(); + + case BackendState::METHOD_HYBRID: + return $this->probeHybrid(); + + case BackendState::METHOD_LLM: + default: + // LLM is deferred: report not-configured. Unknown methods behave the same. + return new ProbeResult( + reachable: false, + latencyMs: null, + error: ProbeResult::ERROR_NOT_CONFIGURED, + probedAt: $this->now(), + ); + }//end switch + }//end freshProbe() + + /** + * Detect the OpenAnonymiser ExApp via AppAPI / IAppManager. + * + * Prefers the full `openanonymiser` variant over `openanonymiser_light` when + * both are enabled. Issues no external HTTP request. + * + * @return ProbeResult Reachable when a healthy ExApp is detected, else an error code. + */ + private function detectOpenAnonymiser(): ProbeResult + { + // AppAPI must be present for any ExApp detection. + if ($this->appManager->isEnabledForUser(self::APP_API) === false) { + return new ProbeResult( + reachable: false, + latencyMs: null, + error: ProbeResult::ERROR_APPAPI_MISSING, + probedAt: $this->now(), + ); + } + + $fullEnabled = $this->appManager->isEnabledForUser(self::EXAPP_FULL); + $lightEnabled = $this->appManager->isEnabledForUser(self::EXAPP_LIGHT); + + if ($fullEnabled === true || $lightEnabled === true) { + if ($fullEnabled === true && $lightEnabled === true) { + $this->logger->debug('[AnonymisationBackendService] both OpenAnonymiser variants enabled; using full, light also-detected'); + } + + return new ProbeResult(reachable: true, latencyMs: 0, error: null, probedAt: $this->now()); + } + + // Not enabled: distinguish installed-but-disabled from not-installed. + $anyInstalled = ($this->appManager->isInstalled(self::EXAPP_FULL) === true + || $this->appManager->isInstalled(self::EXAPP_LIGHT) === true); + + if ($anyInstalled === true) { + $error = ProbeResult::ERROR_EXAPP_DISABLED; + } else { + $error = ProbeResult::ERROR_EXAPP_NOT_INSTALLED; + } + + return new ProbeResult( + reachable: false, + latencyMs: null, + error: $error, + probedAt: $this->now(), + ); + }//end detectOpenAnonymiser() + + /** + * Resolve the app id of the active OpenAnonymiser ExApp. + * + * Prefers the full `openanonymiser` variant over `openanonymiser_light`. + * Returns null when AppAPI is absent or no variant is enabled. + * + * @return string|null The ExApp app id, or null. + */ + public function resolveActiveExAppId(): ?string + { + if ($this->appManager->isEnabledForUser(self::APP_API) === false) { + return null; + } + + if ($this->appManager->isEnabledForUser(self::EXAPP_FULL) === true) { + return self::EXAPP_FULL; + } + + if ($this->appManager->isEnabledForUser(self::EXAPP_LIGHT) === true) { + return self::EXAPP_LIGHT; + } + + return null; + }//end resolveActiveExAppId() + + /** + * Issue a signed request to the active OpenAnonymiser ExApp via AppAPI. + * + * Uses `OCA\AppAPI\PublicFunctions::exAppRequest()`, resolved lazily so + * OpenRegister carries no hard dependency on AppAPI (ADR-017). ExApps reject + * unsigned requests, so a plain HTTP call to the internal host would fail — + * routing and auth headers are handled by AppAPI. + * + * @param string $route ExApp route (e.g. `/api/v1/analyze`). + * @param array $params Request body parameters. + * @param string $method HTTP method (default POST). + * + * @return array|null Decoded JSON response, or null on failure. + */ + public function requestOpenAnonymiser(string $route, array $params, string $method='POST'): ?array + { + $appId = $this->resolveActiveExAppId(); + if ($appId === null) { + return null; + } + + // Avoid a compile-time dependency on the optional AppAPI app. + $publicFunctionsClass = 'OCA\\AppAPI\\PublicFunctions'; + if (class_exists($publicFunctionsClass) === false) { + $this->logger->warning('[AnonymisationBackendService] AppAPI PublicFunctions unavailable; cannot reach ExApp'); + return null; + } + + try { + $publicFunctions = Server::get($publicFunctionsClass); + $response = $publicFunctions->exAppRequest($appId, $route, null, $method, $params); + + if (is_array($response) === true) { + // ExAppRequest returns an array only to signal an error. + $this->logger->error('[AnonymisationBackendService] ExApp request error: '.($response['error'] ?? 'unknown')); + return null; + } + + if (($response instanceof IResponse) === false) { + return null; + } + + $status = $response->getStatusCode(); + if ($status < 200 || $status >= 300) { + $this->logger->error('[AnonymisationBackendService] ExApp '.$appId.' returned HTTP '.$status); + return null; + } + + $decoded = json_decode((string) $response->getBody(), true); + + if (is_array($decoded) === true) { + return $decoded; + } + + return null; + } catch (Throwable $e) { + $this->logger->error('[AnonymisationBackendService] ExApp request to '.$appId.' failed: '.$e->getMessage()); + return null; + }//end try + }//end requestOpenAnonymiser() + + /** + * Probe the configured Presidio endpoint over HTTP. + * + * @return ProbeResult Reachable with latency on success, else a mapped error code. + */ + private function probePresidio(): ProbeResult + { + $settings = $this->fileSettingsHandler->getFileSettingsOnly(); + $endpoint = trim((string) ($settings['presidioApiEndpoint'] ?? '')); + + if ($endpoint === '') { + return new ProbeResult( + reachable: false, + latencyMs: null, + error: ProbeResult::ERROR_NOT_CONFIGURED, + probedAt: $this->now(), + ); + } + + $start = microtime(true); + try { + $client = $this->clientService->newClient(); + $response = $client->get( + rtrim($endpoint, '/').'/health', + [ + 'timeout' => 10, + 'connect_timeout' => 5, + 'nextcloud' => ['allow_local_address' => true], + ] + ); + + $latency = (int) round((microtime(true) - $start) * 1000); + $statusCode = $response->getStatusCode(); + + if ($statusCode >= 200 && $statusCode < 300) { + return new ProbeResult(reachable: true, latencyMs: $latency, error: null, probedAt: $this->now()); + } + + if ($statusCode >= 500) { + $error = ProbeResult::ERROR_HTTP_5XX; + } else { + $error = ProbeResult::ERROR_HTTP_4XX; + } + + return new ProbeResult( + reachable: false, + latencyMs: $latency, + error: $error, + probedAt: $this->now(), + ); + } catch (Throwable $e) { + return new ProbeResult( + reachable: false, + latencyMs: null, + error: $this->mapHttpException(message: $e->getMessage()), + probedAt: $this->now(), + ); + }//end try + }//end probePresidio() + + /** + * Compose a hybrid probe result from its constituent backends. + * + * @return ProbeResult Reachable only when all composed backends are reachable. + */ + private function probeHybrid(): ProbeResult + { + $regex = $this->probe(method: BackendState::METHOD_REGEX); + $presidio = $this->probe(method: BackendState::METHOD_PRESIDIO); + $openAnonymiser = $this->probe(method: BackendState::METHOD_OPENANONYMISER); + + $reachable = ($regex->reachable === true && $presidio->reachable === true && $openAnonymiser->reachable === true); + + $error = null; + if ($reachable === false) { + $error = ($presidio->error ?? $openAnonymiser->error ?? ProbeResult::ERROR_NOT_CONFIGURED); + } + + return new ProbeResult(reachable: $reachable, latencyMs: null, error: $error, probedAt: $this->now()); + }//end probeHybrid() + + /** + * Build a BackendInfo for an atomic method from its probe result. + * + * @param string $method The method enum value. + * @param ProbeResult $probe The probe result for the method. + * + * @return BackendInfo The availability/configuration record. + */ + private function buildBackendInfo(string $method, ProbeResult $probe): BackendInfo + { + return new BackendInfo( + name: $method, + available: $probe->reachable, + configured: $this->isConfigured(method: $method, probe: $probe), + lastProbedAt: $probe->probedAt, + latencyMs: $probe->latencyMs, + ); + }//end buildBackendInfo() + + /** + * Build the hybrid BackendInfo from the composed atomic records. + * + * @param array $backends The already-built atomic records. + * + * @return BackendInfo The hybrid availability record. + */ + private function buildHybridInfo(array $backends): BackendInfo + { + $regex = $backends[BackendState::METHOD_REGEX]; + $presidio = $backends[BackendState::METHOD_PRESIDIO]; + $openAnonymiser = $backends[BackendState::METHOD_OPENANONYMISER]; + + $available = ($regex->available === true && $presidio->available === true && $openAnonymiser->available === true); + $configured = ($presidio->configured === true && $openAnonymiser->configured === true); + + return new BackendInfo( + name: BackendState::METHOD_HYBRID, + available: $available, + configured: $configured, + lastProbedAt: $this->now(), + latencyMs: null, + ); + }//end buildHybridInfo() + + /** + * Determine whether a method has usable configuration. + * + * @param string $method The method enum value. + * @param ProbeResult $probe The probe result for the method. + * + * @return bool True when the backend is configured. + */ + private function isConfigured(string $method, ProbeResult $probe): bool + { + switch ($method) { + case BackendState::METHOD_REGEX: + return true; + + case BackendState::METHOD_OPENANONYMISER: + // An installed ExApp counts as configured; "not configured" maps to absent/AppAPI errors. + return ($probe->error === null + || $probe->error === ProbeResult::ERROR_HTTP_5XX + || $probe->error === ProbeResult::ERROR_HTTP_4XX); + + case BackendState::METHOD_PRESIDIO: + // Configured when an endpoint is set (i.e. the probe was not skipped as not-configured). + return ($probe->error !== ProbeResult::ERROR_NOT_CONFIGURED); + + case BackendState::METHOD_LLM: + default: + return false; + }//end switch + }//end isConfigured() + + /** + * Resolve the active method, applying the first-run auto-select rule. + * + * @param string $stored The stored method (may be the `auto` sentinel). + * @param array $backends The per-method availability records. + * + * @return string The resolved active method enum value. + */ + private function resolveActiveMethod(string $stored, array $backends): string + { + if ($stored === BackendState::METHOD_AUTO) { + // Auto-select a detected OpenAnonymiser ExApp on first run, else fall back to regex. + if (($backends[BackendState::METHOD_OPENANONYMISER]->available ?? false) === true) { + return BackendState::METHOD_OPENANONYMISER; + } + + return BackendState::METHOD_REGEX; + } + + // Operator intent wins. Guard against an unknown stored value. + if (in_array($stored, BackendState::METHODS, true) === true) { + return $stored; + } + + return BackendState::METHOD_REGEX; + }//end resolveActiveMethod() + + /** + * Apply the effectiveMethod precedence rule. + * + * @param bool $enabled Whether recognition is enabled. + * @param string $activeMethod The resolved active method. + * @param array $backends The per-method availability records. + * + * @return string The method that will actually be used. + */ + private function resolveEffectiveMethod(bool $enabled, string $activeMethod, array $backends): string + { + if ($enabled === false) { + return BackendState::METHOD_REGEX; + } + + if ($activeMethod === BackendState::METHOD_REGEX) { + return BackendState::METHOD_REGEX; + } + + $info = ($backends[$activeMethod] ?? null); + if ($info !== null && $info->available === true && $info->configured === true) { + return $activeMethod; + } + + return BackendState::METHOD_REGEX; + }//end resolveEffectiveMethod() + + /** + * Map an HTTP client exception message to a probe error code. + * + * @param string $message The exception message. + * + * @return string One of the ProbeResult::ERROR_* codes. + */ + private function mapHttpException(string $message): string + { + $lower = strtolower($message); + + if (str_contains($lower, 'timed out') === true || str_contains($lower, 'timeout') === true) { + return ProbeResult::ERROR_TIMEOUT; + } + + if (str_contains($lower, 'could not resolve') === true || str_contains($lower, 'name or service not known') === true) { + return ProbeResult::ERROR_DNS_ERROR; + } + + return ProbeResult::ERROR_CONNECT_REFUSED; + }//end mapHttpException() + + /** + * Resolve the probe-cache TTL, clamped to the allowed range. + * + * @return int The TTL in seconds. + */ + private function resolveTtl(): int + { + $ttl = $this->appConfig->getValueInt('openregister', self::TTL_CONFIG_KEY, self::TTL_DEFAULT); + + return max(self::TTL_MIN, min(self::TTL_MAX, $ttl)); + }//end resolveTtl() + + /** + * Read a cached probe result for a method, or null on miss / disabled cache. + * + * @param string $method The method enum value. + * + * @return ProbeResult|null The cached result, or null. + */ + private function readCache(string $method): ?ProbeResult + { + if ($this->cache === null) { + return null; + } + + $blob = $this->cache->get($method); + if (is_string($blob) === false) { + return null; + } + + $data = json_decode($blob, true); + if (is_array($data) === false) { + return null; + } + + $latencyMs = null; + if (($data['latencyMs'] ?? null) !== null) { + $latencyMs = (int) $data['latencyMs']; + } + + $error = null; + if (($data['error'] ?? null) !== null) { + $error = (string) $data['error']; + } + + return new ProbeResult( + reachable: (bool) ($data['reachable'] ?? false), + latencyMs: $latencyMs, + error: $error, + probedAt: (string) ($data['probedAt'] ?? $this->now()), + ); + }//end readCache() + + /** + * Write a probe result to the cache with the configured TTL. + * + * @param string $method The method enum value. + * @param ProbeResult $result The result to cache. + * + * @return void + */ + private function writeCache(string $method, ProbeResult $result): void + { + if ($this->cache === null) { + return; + } + + $this->cache->set($method, json_encode($result->jsonSerialize()), $this->resolveTtl()); + }//end writeCache() + + /** + * Current time as an ISO-8601 string. + * + * @return string The timestamp. + */ + private function now(): string + { + return gmdate('c'); + }//end now() +}//end class diff --git a/lib/Service/Anonymisation/BackendInfo.php b/lib/Service/Anonymisation/BackendInfo.php new file mode 100644 index 0000000000..ca8e480183 --- /dev/null +++ b/lib/Service/Anonymisation/BackendInfo.php @@ -0,0 +1,69 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Anonymisation; + +use JsonSerializable; + +/** + * Per-method availability and configuration record. + * + * The two flags are independent: a backend MAY be configured but unreachable + * (latest probe failed), or available but not configured (e.g. an installed + * ExApp without a matching operator-selected method). + * + * @category ValueObject + * @package OCA\OpenRegister\Service\Anonymisation + */ +final class BackendInfo implements JsonSerializable +{ + /** + * Constructor. + * + * @param string $name The method enum (regex/presidio/openanonymiser/llm/hybrid). + * @param bool $available Whether the backend is reachable per the latest probe. + * @param bool $configured Whether the backend has usable configuration. + * @param string|null $lastProbedAt ISO-8601 timestamp of the latest probe, or null. + * @param int|null $latencyMs Latency of the latest probe in milliseconds, or null. + */ + public function __construct( + public readonly string $name, + public readonly bool $available, + public readonly bool $configured, + public readonly ?string $lastProbedAt, + public readonly ?int $latencyMs, + ) { + }//end __construct() + + /** + * Serialise to a JSON-friendly array. + * + * @return array{name: string, available: bool, configured: bool, lastProbedAt: string|null, latencyMs: int|null} + */ + public function jsonSerialize(): array + { + return [ + 'name' => $this->name, + 'available' => $this->available, + 'configured' => $this->configured, + 'lastProbedAt' => $this->lastProbedAt, + 'latencyMs' => $this->latencyMs, + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Service/Anonymisation/BackendState.php b/lib/Service/Anonymisation/BackendState.php new file mode 100644 index 0000000000..f86f7488c8 --- /dev/null +++ b/lib/Service/Anonymisation/BackendState.php @@ -0,0 +1,112 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Anonymisation; + +use JsonSerializable; + +/** + * Fully-resolved snapshot of the anonymisation backend selection state. + * + * @category ValueObject + * @package OCA\OpenRegister\Service\Anonymisation + */ +final class BackendState implements JsonSerializable +{ + /** + * Regular-expression based recognition (always available fallback). + */ + public const METHOD_REGEX = 'regex'; + + /** + * Microsoft Presidio HTTP backend. + */ + public const METHOD_PRESIDIO = 'presidio'; + + /** + * OpenAnonymiser ExApp backend. + */ + public const METHOD_OPENANONYMISER = 'openanonymiser'; + + /** + * LLM-based recognition backend. + */ + public const METHOD_LLM = 'llm'; + + /** + * Hybrid backend composing regex + presidio + openanonymiser. + */ + public const METHOD_HYBRID = 'hybrid'; + + /** + * Sentinel stored value meaning "no explicit operator choice yet". + * + * Resolved to a concrete method at state-query time (auto-select on first run). + */ + public const METHOD_AUTO = 'auto'; + + /** + * The valid concrete method enum values. + * + * @var string[] + */ + public const METHODS = [ + self::METHOD_REGEX, + self::METHOD_PRESIDIO, + self::METHOD_OPENANONYMISER, + self::METHOD_LLM, + self::METHOD_HYBRID, + ]; + + /** + * Constructor. + * + * @param bool $entityRecognitionEnabled Whether recognition is enabled at all. + * @param string $activeMethod The operator-selected (resolved) method. + * @param string $effectiveMethod The method that will actually be used. + * @param array $backends Per-method availability records. + */ + public function __construct( + public readonly bool $entityRecognitionEnabled, + public readonly string $activeMethod, + public readonly string $effectiveMethod, + public readonly array $backends, + ) { + }//end __construct() + + /** + * Serialise to a JSON-friendly array. + * + * @return array{entityRecognitionEnabled: bool, activeMethod: string, effectiveMethod: string, backends: array} + */ + public function jsonSerialize(): array + { + $backends = []; + foreach ($this->backends as $name => $info) { + $backends[$name] = $info->jsonSerialize(); + } + + return [ + 'entityRecognitionEnabled' => $this->entityRecognitionEnabled, + 'activeMethod' => $this->activeMethod, + 'effectiveMethod' => $this->effectiveMethod, + 'backends' => $backends, + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Service/Anonymisation/ProbeResult.php b/lib/Service/Anonymisation/ProbeResult.php new file mode 100644 index 0000000000..35f74d0f9c --- /dev/null +++ b/lib/Service/Anonymisation/ProbeResult.php @@ -0,0 +1,124 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Anonymisation; + +use JsonSerializable; + +/** + * Immutable result of a single backend reachability probe. + * + * @category ValueObject + * @package OCA\OpenRegister\Service\Anonymisation + */ +final class ProbeResult implements JsonSerializable +{ + /** + * Probe could not be issued because AppAPI is not present on the instance. + */ + public const ERROR_APPAPI_MISSING = 'appapi_missing'; + + /** + * Target ExApp is not installed. + */ + public const ERROR_EXAPP_NOT_INSTALLED = 'exapp_not_installed'; + + /** + * Target ExApp is installed but not enabled. + */ + public const ERROR_EXAPP_DISABLED = 'exapp_disabled'; + + /** + * Backend has no usable configuration (e.g. no endpoint set). + */ + public const ERROR_NOT_CONFIGURED = 'not_configured'; + + /** + * HTTP probe timed out. + */ + public const ERROR_TIMEOUT = 'timeout'; + + /** + * DNS resolution failed for the configured endpoint. + */ + public const ERROR_DNS_ERROR = 'dns_error'; + + /** + * TCP connection was refused. + */ + public const ERROR_CONNECT_REFUSED = 'connect_refused'; + + /** + * Endpoint responded with a 4xx status. + */ + public const ERROR_HTTP_4XX = 'http_4xx'; + + /** + * Endpoint responded with a 5xx status. + */ + public const ERROR_HTTP_5XX = 'http_5xx'; + + /** + * The full set of valid error codes. + * + * @var string[] + */ + public const ERRORS = [ + self::ERROR_TIMEOUT, + self::ERROR_DNS_ERROR, + self::ERROR_HTTP_4XX, + self::ERROR_HTTP_5XX, + self::ERROR_CONNECT_REFUSED, + self::ERROR_EXAPP_NOT_INSTALLED, + self::ERROR_EXAPP_DISABLED, + self::ERROR_APPAPI_MISSING, + self::ERROR_NOT_CONFIGURED, + ]; + + /** + * Constructor. + * + * @param bool $reachable Whether the backend responded successfully. + * @param int|null $latencyMs Round-trip latency in milliseconds, or null when not measured. + * @param string|null $error One of the ERROR_* codes, or null on success. + * @param string $probedAt ISO-8601 timestamp of when the probe ran. + */ + public function __construct( + public readonly bool $reachable, + public readonly ?int $latencyMs, + public readonly ?string $error, + public readonly string $probedAt, + ) { + }//end __construct() + + /** + * Serialise to a JSON-friendly array. + * + * @return array{reachable: bool, latencyMs: int|null, error: string|null, probedAt: string} + */ + public function jsonSerialize(): array + { + return [ + 'reachable' => $this->reachable, + 'latencyMs' => $this->latencyMs, + 'error' => $this->error, + 'probedAt' => $this->probedAt, + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Service/ApprovalChainAnnotationInstaller.php b/lib/Service/ApprovalChainAnnotationInstaller.php new file mode 100644 index 0000000000..bd2b638665 --- /dev/null +++ b/lib/Service/ApprovalChainAnnotationInstaller.php @@ -0,0 +1,192 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/approval-workflow/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use OCA\OpenRegister\Db\ApprovalChain; +use OCA\OpenRegister\Db\ApprovalChainMapper; +use OCA\OpenRegister\Db\Schema; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +/** + * Listener that provisions `ApprovalChain` rows from a schema's declarative + * `x-openregister-approval-chains` block. + * + * @template-implements IEventListener + */ +class ApprovalChainAnnotationInstaller implements IEventListener +{ + /** + * Constructor. + * + * @param ApprovalChainMapper $chainMapper Mapper used to upsert ApprovalChain rows. + * @param LoggerInterface $logger Logger for installer diagnostics. + */ + public function __construct( + private readonly ApprovalChainMapper $chainMapper, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Listener entry point: dispatches schema-saved events to the installer. + * + * @param Event $event The event carrying the saved schema. + * + * @return void + * + * @spec openspec/specs/approval-workflow/spec.md + */ + public function handle(Event $event): void + { + $schema = null; + if (method_exists($event, 'getSchema') === true) { + $schema = $event->getSchema(); + } else if (method_exists($event, 'getNewSchema') === true) { + $schema = $event->getNewSchema(); + } + + if (($schema instanceof Schema) === false) { + return; + } + + $this->installSchema(schema: $schema); + }//end handle() + + /** + * Upsert an `ApprovalChain` row for every declared chain. + * + * Idempotent: safe to call repeatedly (also called defensively by + * `ApprovalChainGateListener` immediately before resolving a chain, so a + * gate evaluation never depends on this listener having already run for the + * current schema revision). + * + * @param Schema $schema The schema whose annotations should be installed. + * + * @return void + * + * @spec openspec/specs/approval-workflow/spec.md + */ + public function installSchema(Schema $schema): void + { + $config = ($schema->getConfiguration() ?? []); + $chains = ($config['x-openregister-approval-chains'] ?? null); + if (is_array($chains) === false) { + return; + } + + $schemaId = $schema->getId(); + if ($schemaId === null) { + return; + } + + foreach ($chains as $chainKey => $spec) { + if (is_string($chainKey) === false || is_array($spec) === false) { + continue; + } + + $approvers = ($spec['approvers'] ?? null); + if (is_array($approvers) === false || $approvers === []) { + continue; + } + + $this->upsertChain(schemaId: (int) $schemaId, chainKey: $chainKey, approvers: $approvers); + } + }//end installSchema() + + /** + * Upsert a single `ApprovalChain` row from one chain's `approvers` list. + * + * @param int $schemaId Owning schema id. + * @param string $chainKey Declarative chain key (becomes `name`). + * @param array> $approvers Declared `{role, min, minAmount?}` tiers. + * + * @return void + */ + private function upsertChain(int $schemaId, string $chainKey, array $approvers): void + { + try { + $steps = []; + $order = 1; + foreach ($approvers as $tier) { + if (is_array($tier) === false) { + continue; + } + + $role = (string) ($tier['role'] ?? ''); + if ($role === '') { + continue; + } + + $steps[] = [ + 'order' => $order, + 'role' => $role, + ]; + $order++; + } + + if ($steps === []) { + return; + } + + $existing = $this->chainMapper->findBySchemaAndName(schemaId: $schemaId, name: $chainKey); + + $payload = [ + 'name' => $chainKey, + 'schemaId' => $schemaId, + 'steps' => json_encode($steps), + 'enabled' => true, + ]; + + if ($existing === null) { + $this->chainMapper->createFromArray($payload); + $this->logger->info( + sprintf('[ApprovalChainAnnotationInstaller] provisioned chain "%s" for schema %d', $chainKey, $schemaId) + ); + return; + } + + $this->chainMapper->updateFromArray($existing->getId(), $payload); + } catch (\Throwable $e) { + $this->logger->warning( + sprintf( + '[ApprovalChainAnnotationInstaller] upsert "%s" (schema %d) failed: %s', + $chainKey, + $schemaId, + $e->getMessage() + ) + ); + }//end try + }//end upsertChain() +}//end class diff --git a/lib/Service/ApprovalService.php b/lib/Service/ApprovalService.php index 02839a6854..746f1b26ba 100644 --- a/lib/Service/ApprovalService.php +++ b/lib/Service/ApprovalService.php @@ -17,8 +17,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-4 - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-5 + * @spec openspec/specs/approval-workflow/spec.md + * @spec openspec/specs/approval-workflow/spec.md */ declare(strict_types=1); @@ -31,7 +31,14 @@ use OCA\OpenRegister\Db\ApprovalChainMapper; use OCA\OpenRegister\Db\ApprovalStep; use OCA\OpenRegister\Db\ApprovalStepMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Db\WorkflowExecutionMapper; +use OCA\OpenRegister\Event\ApprovalStepApprovedEvent; +use OCA\OpenRegister\Event\ApprovalStepCompletedEvent; +use OCA\OpenRegister\Event\ApprovalStepInitiatedEvent; +use OCA\OpenRegister\Event\ApprovalStepRejectedEvent; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IGroupManager; use Psr\Log\LoggerInterface; @@ -50,13 +57,19 @@ class ApprovalService * @param WorkflowExecutionMapper $executionMapper Execution history mapper * @param IGroupManager $groupManager Group manager for role checks * @param LoggerInterface $logger Logger + * @param IEventDispatcher $eventDispatcher Event dispatcher for approval step events + * @param SchemaMapper $schemaMapper Schema lookup, used to resolve the declarative + * `x-openregister-approval-chains` entry (if any) that + * provisioned a chain, for separation-of-duties enforcement. */ public function __construct( private readonly ApprovalChainMapper $chainMapper, private readonly ApprovalStepMapper $stepMapper, private readonly WorkflowExecutionMapper $executionMapper, private readonly IGroupManager $groupManager, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly IEventDispatcher $eventDispatcher, + private readonly SchemaMapper $schemaMapper ) { }//end __construct() @@ -66,16 +79,35 @@ public function __construct( * Creates ApprovalStep entities for each step in the chain: step 1 as * 'pending', all others as 'waiting'. * - * @param ApprovalChain $chain The approval chain - * @param string $objectUuid The object's UUID + * @param ApprovalChain $chain The approval chain + * @param string $objectUuid The object's UUID + * @param string|null $requesterId Uid of the user whose attempted transition + * triggered provisioning, when called from + * the declarative gate + * (`ApprovalChainGateListener`). Stamped + * onto every created step so {@see + * resolveSeparationOfDuties()} can later + * reject a self-decision. `null` for the + * pure-CRUD flow. + * @param array>|null $stepsOverride Step definitions to use instead of the + * chain's own static `steps` — used by the declarative + * gate's amount-threshold tier routing, where the + * applicable tier is resolved per-object rather than + * baked into the persisted chain config. * * @return array Created steps * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-4 + * @spec openspec/specs/approval-workflow/spec.md + * @spec openspec/changes/add-approval-step-events/tasks.md#task-3 + * @spec openspec/specs/approval-workflow/spec.md */ - public function initializeChain(ApprovalChain $chain, string $objectUuid): array - { - $steps = $chain->getStepsArray(); + public function initializeChain( + ApprovalChain $chain, + string $objectUuid, + ?string $requesterId=null, + ?array $stepsOverride=null + ): array { + $steps = ($stepsOverride ?? $chain->getStepsArray()); $createdSteps = []; foreach ($steps as $index => $stepDef) { @@ -86,16 +118,24 @@ public function initializeChain(ApprovalChain $chain, string $objectUuid): array $step = $this->stepMapper->createFromArray( [ - 'chainId' => $chain->getId(), - 'objectUuid' => $objectUuid, - 'stepOrder' => ($stepDef['order'] ?? ($index + 1)), - 'role' => ($stepDef['role'] ?? ''), - 'status' => $status, + 'chainId' => $chain->getId(), + 'objectUuid' => $objectUuid, + 'stepOrder' => ($stepDef['order'] ?? ($index + 1)), + 'role' => ($stepDef['role'] ?? ''), + 'status' => $status, + 'requesterId' => $requesterId, ] ); $createdSteps[] = $step; - } + + // Dispatch initiated event for the first step (now `pending`). + if ($index === 0) { + $this->eventDispatcher->dispatchTyped( + new ApprovalStepInitiatedEvent(chain: $chain, step: $step, objectUuid: $objectUuid) + ); + } + }//end foreach return $createdSteps; }//end initializeChain() @@ -113,7 +153,8 @@ public function initializeChain(ApprovalChain $chain, string $objectUuid): array * * @throws Exception If user is not authorised or step is not pending * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-5 + * @spec openspec/specs/approval-workflow/spec.md + * @spec openspec/changes/add-approval-step-events/tasks.md#task-4 */ public function approveStep(int $stepId, string $userId, string $comment=''): array { @@ -123,6 +164,11 @@ public function approveStep(int $stepId, string $userId, string $comment=''): ar throw new Exception('Step is not in pending status'); } + // Separation of duties is evaluated BEFORE the role check so a + // self-decision attempt gets a distinct, honest error rather than being + // masked by (or coincidentally passing) the group-membership check. + $this->verifySeparationOfDuties(step: $step, userId: $userId); + // Verify role membership. $this->verifyRole(userId: $userId, role: $step->getRole()); @@ -161,6 +207,37 @@ public function approveStep(int $stepId, string $userId, string $comment=''): ar // Persist execution history. $this->persistApprovalExecution(chain: $chain, step: $step, status: 'approved'); + // Dispatch the approved event. + $this->eventDispatcher->dispatchTyped( + new ApprovalStepApprovedEvent( + chain: $chain, + step: $step, + userId: $userId, + statusOnApprove: $statusOnApprove, + nextStep: $nextStep + ) + ); + + // Dispatch follow-up: either initiate the next step or complete the chain. + if ($nextStep !== null) { + $this->eventDispatcher->dispatchTyped( + new ApprovalStepInitiatedEvent( + chain: $chain, + step: $nextStep, + objectUuid: $step->getObjectUuid() + ) + ); + } else { + $this->eventDispatcher->dispatchTyped( + new ApprovalStepCompletedEvent( + chain: $chain, + finalStep: $step, + userId: $userId, + statusOnApprove: $statusOnApprove + ) + ); + } + return [ 'step' => $step, 'nextStep' => $nextStep, @@ -180,7 +257,8 @@ public function approveStep(int $stepId, string $userId, string $comment=''): ar * * @throws Exception If user is not authorised or step is not pending * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-5 + * @spec openspec/specs/approval-workflow/spec.md + * @spec openspec/changes/add-approval-step-events/tasks.md#task-5 */ public function rejectStep(int $stepId, string $userId, string $comment=''): array { @@ -190,6 +268,10 @@ public function rejectStep(int $stepId, string $userId, string $comment=''): arr throw new Exception('Step is not in pending status'); } + // Separation of duties is evaluated BEFORE the role check (see + // approveStep()). + $this->verifySeparationOfDuties(step: $step, userId: $userId); + // Verify role membership. $this->verifyRole(userId: $userId, role: $step->getRole()); @@ -216,6 +298,16 @@ public function rejectStep(int $stepId, string $userId, string $comment=''): arr // Persist execution history. $this->persistApprovalExecution(chain: $chain, step: $step, status: 'rejected'); + // Dispatch the rejected event — chain is terminated; no next-step event. + $this->eventDispatcher->dispatchTyped( + new ApprovalStepRejectedEvent( + chain: $chain, + step: $step, + userId: $userId, + statusOnReject: $statusOnReject + ) + ); + return [ 'step' => $step, 'statusOnReject' => $statusOnReject, @@ -223,6 +315,94 @@ public function rejectStep(int $stepId, string $userId, string $comment=''): arr ]; }//end rejectStep() + /** + * Reject a decision made by the same user who triggered the chain, when the + * chain's schema declares separation of duties. + * + * The chain is resolved lazily — only when the decider actually IS the + * recorded requester — so the pure-CRUD flow (steps with no `requesterId`, + * and callers whose step carries no `chainId`) reaches the role check on + * exactly the code path it always did, before this change existed. + * + * @param ApprovalStep $step The step being decided. + * @param string $userId The deciding user's id. + * + * @return void + * + * @throws Exception When the decider is the chain's own requester and + * separation of duties applies. + * + * @spec openspec/specs/approval-workflow/spec.md + */ + private function verifySeparationOfDuties(ApprovalStep $step, string $userId): void + { + $requesterId = $step->getRequesterId(); + if ($requesterId === null || $requesterId === '' || $requesterId !== $userId) { + // No requester recorded (pure-CRUD step), or the decider is not the + // requester — nothing to enforce either way. + return; + } + + $chainId = $step->getChainId(); + if ($chainId === null) { + return; + } + + $chain = $this->chainMapper->find((int) $chainId); + + if ($this->resolveSeparationOfDuties(chain: $chain) === true) { + throw new Exception('You may not decide an approval step you requested yourself'); + } + }//end verifySeparationOfDuties() + + /** + * Resolve whether a chain's declarative entry requires separation of duties. + * + * Loads the chain's schema and looks for an `x-openregister-approval-chains` + * entry whose key equals the chain's `name` (the shape + * `ApprovalChainAnnotationInstaller` provisions from). When found, + * `separationOfDuties` defaults to `true` (fail-safe) unless the schema + * explicitly sets it `false`. When no matching declarative entry exists — + * the pre-existing pure-CRUD-provisioned chain — this returns `false`, so + * that flow is completely unaffected by this change. + * + * @param ApprovalChain $chain The chain to resolve. + * + * @return bool True when separation of duties applies. + * + * @spec openspec/specs/approval-workflow/spec.md + */ + private function resolveSeparationOfDuties(ApprovalChain $chain): bool + { + $schemaId = $chain->getSchemaId(); + if ($schemaId === null) { + return false; + } + + try { + $schema = $this->schemaMapper->find($schemaId, _multitenancy: false); + } catch (\Throwable $e) { + return false; + } + + if (($schema instanceof Schema) === false) { + return false; + } + + $config = ($schema->getConfiguration() ?? []); + $chains = ($config['x-openregister-approval-chains'] ?? null); + if (is_array($chains) === false) { + return false; + } + + $entry = ($chains[(string) $chain->getName()] ?? null); + if (is_array($entry) === false) { + return false; + } + + return ($entry['separationOfDuties'] ?? true) !== false; + }//end resolveSeparationOfDuties() + /** * Verify that a user is a member of the required group/role. * @@ -233,7 +413,7 @@ public function rejectStep(int $stepId, string $userId, string $comment=''): arr * * @throws Exception If user is not in the required group * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-5 + * @spec openspec/specs/approval-workflow/spec.md */ private function verifyRole(string $userId, string $role): void { @@ -251,7 +431,7 @@ private function verifyRole(string $userId, string $role): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-01-approval-workflow/tasks.md#task-5 + * @spec openspec/specs/approval-workflow/spec.md */ private function persistApprovalExecution( ApprovalChain $chain, diff --git a/lib/Service/Archival/ArchiefactiedatumCalculator.php b/lib/Service/Archival/ArchiefactiedatumCalculator.php index edcda13632..5e21cb4e07 100644 --- a/lib/Service/Archival/ArchiefactiedatumCalculator.php +++ b/lib/Service/Archival/ArchiefactiedatumCalculator.php @@ -20,9 +20,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-6 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ declare(strict_types=1); @@ -84,8 +84,8 @@ public function __construct(LoggerInterface $logger) * * @return DateTime|null The calculated archiefactiedatum, or null if calculation is not possible. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-6 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-2 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function calculate(array $archiveConfig, array $objectData, ?DateTime $closureDate=null): ?DateTime { @@ -165,7 +165,7 @@ public function calculate(array $archiveConfig, array $objectData, ?DateTime $cl * * @return DateTime|null The determined base date. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-6 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function determineBrondatum( string $afleidingswijze, @@ -203,7 +203,7 @@ private function determineBrondatum( * * @return DateTime|null The closure date or null if not provided. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-6 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function brondatumFromClosure(?DateTime $closureDate): ?DateTime { @@ -226,7 +226,7 @@ private function brondatumFromClosure(?DateTime $closureDate): ?DateTime * * @return DateTime|null The date from the property value, or null. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function brondatumFromProperty(array $archiveConfig, array $objectData): ?DateTime { @@ -276,7 +276,7 @@ private function brondatumFromProperty(array $archiveConfig, array $objectData): * * @return DateTime|null The base date (closure + procestermijn), or null. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-6 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function brondatumFromTermijn(array $archiveConfig, ?DateTime $closureDate): ?DateTime { diff --git a/lib/Service/Archival/ArchivalAnnotationValidator.php b/lib/Service/Archival/ArchivalAnnotationValidator.php index 21196a2a0d..65d7b36537 100644 --- a/lib/Service/Archival/ArchivalAnnotationValidator.php +++ b/lib/Service/Archival/ArchivalAnnotationValidator.php @@ -27,7 +27,7 @@ * @link https://OpenRegister.app * * @spec openspec/changes/add-archival-annotation-support/tasks.md#task-2-1 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-4 + * @spec openspec/specs/archival-annotation-vocabulary/spec.md#scenario-non-iso-8601-retention-default-is-rejected */ declare(strict_types=1); @@ -263,7 +263,7 @@ private function validateRule(int $index, mixed $rule): array * * @return bool True when the value parses as a duration. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-4 + * @spec openspec/specs/archival-annotation-vocabulary/spec.md#scenario-non-iso-8601-retention-default-is-rejected */ private function isIsoDuration(mixed $value): bool { diff --git a/lib/Service/Archival/DestructionService.php b/lib/Service/Archival/DestructionService.php index f6f189fc85..b96dea5584 100644 --- a/lib/Service/Archival/DestructionService.php +++ b/lib/Service/Archival/DestructionService.php @@ -21,15 +21,15 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-4 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-4 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-6 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-3 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ declare(strict_types=1); @@ -38,10 +38,8 @@ use DateInterval; use DateTime; -use OCA\OpenRegister\Db\AuditTrailMapper; use OCA\OpenRegister\Db\MagicMapper; use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Service\Object\DeleteObject; use OCP\BackgroundJob\IJobList; use OCP\IAppConfig; use OCP\IUserSession; @@ -78,11 +76,6 @@ class DestructionService */ private const DEFAULT_EXTENSION_PERIOD = 'P1Y'; - /** - * Default batch size for destruction execution. - */ - private const DEFAULT_BATCH_SIZE = 100; - /** * Object entity mapper. * @@ -97,20 +90,6 @@ class DestructionService */ private LegalHoldService $legalHoldService; - /** - * Delete object handler for permanent deletion. - * - * @var DeleteObject - */ - private DeleteObject $deleteObject; - - /** - * Audit trail mapper for logging destruction events. - * - * @var AuditTrailMapper - */ - private AuditTrailMapper $auditTrailMapper; - /** * App configuration. * @@ -144,8 +123,6 @@ class DestructionService * * @param MagicMapper $objectMapper Object entity data mapper. * @param LegalHoldService $legalHoldService Legal hold checking service. - * @param DeleteObject $deleteObject Delete object handler. - * @param AuditTrailMapper $auditTrailMapper Audit trail mapper. * @param IAppConfig $appConfig App configuration. * @param IJobList $jobList Background job list. * @param IUserSession $userSession User session service. @@ -154,8 +131,6 @@ class DestructionService public function __construct( MagicMapper $objectMapper, LegalHoldService $legalHoldService, - DeleteObject $deleteObject, - AuditTrailMapper $auditTrailMapper, IAppConfig $appConfig, IJobList $jobList, IUserSession $userSession, @@ -163,8 +138,6 @@ public function __construct( ) { $this->objectMapper = $objectMapper; $this->legalHoldService = $legalHoldService; - $this->deleteObject = $deleteObject; - $this->auditTrailMapper = $auditTrailMapper; $this->appConfig = $appConfig; $this->jobList = $jobList; $this->userSession = $userSession; @@ -185,8 +158,8 @@ public function __construct( * * @return array> Array of eligible object data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-4 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function findEligibleObjects(array $existingListObjectIds=[]): array { @@ -267,8 +240,8 @@ public function findEligibleObjects(array $existingListObjectIds=[]): array * * @return array The created destruction list data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-6 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function createDestructionList(array $eligibleObjects): array { @@ -312,27 +285,37 @@ public function createDestructionList(array $eligibleObjects): array * @param array $excludedIds UUIDs of objects to exclude (for partial approval). * @param array $exclusionReasons Reasons per excluded object UUID. * @param bool $requiresDual Whether two-step approval is required. + * @param string|null $listUuid UUID of the persisted destruction list; passed to the + * execution job so it can reload the list from storage. * * @return array The updated destruction list. * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Configuration-driven dual approval toggle * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function approveList( array $destructionList, string $action='approve_all', array $excludedIds=[], array $exclusionReasons=[], - bool $requiresDual=false + bool $requiresDual=false, + ?string $listUuid=null ): array { $userId = $this->getCurrentUserId(); $now = new DateTime(); - // Record the approval. + // Record the approval. The `userId` key is CANONICAL: it is the shape + // RetentionController writes, and the shape both readers of the approvals + // list consume — RetentionService::generateDestructionCertificate() and + // DestructionExecutionJob both do array_column($approvals, 'userId'). This + // method previously wrote `approvedBy`, so the destruction certificate's + // approver list — the legal record of WHO authorised the destruction — + // came out empty (openregister#393). $destructionList['approvals'][] = [ - 'approvedBy' => $userId, + 'userId' => $userId, 'approvedAt' => $now->format('c'), 'action' => $action, ]; @@ -364,8 +347,8 @@ public function approveList( // Check dual approval: second approver must be different from first. if ($requiresDual === true && count($destructionList['approvals']) >= 2) { - $firstApprover = $destructionList['approvals'][0]['approvedBy'] ?? null; - $secondApprover = $destructionList['approvals'][1]['approvedBy'] ?? null; + $firstApprover = $destructionList['approvals'][0]['userId'] ?? null; + $secondApprover = $destructionList['approvals'][1]['userId'] ?? null; if ($firstApprover === $secondApprover) { $this->logger->warning( message: '[DestructionService] Same archivist cannot provide both approvals', @@ -385,12 +368,17 @@ public function approveList( $destructionList['status'] = self::STATUS_APPROVED; // Queue the destruction execution job. + // + // The job argument key MUST be `destructionListUuid`: DestructionExecutionJob + // reads $argument['destructionListUuid'] and returns early when it is absent. + // This call previously passed the whole list under the key `destructionList`, + // so the job bailed out on every run — objects were never destroyed and no + // verklaring van vernietiging was ever produced through the /api/archival + // approve route (openregister#393). The job reloads the list from storage by + // uuid, which is why the caller must have persisted it first. $this->jobList->add( \OCA\OpenRegister\BackgroundJob\DestructionExecutionJob::class, - [ - 'destructionList' => $destructionList, - 'approvedBy' => $userId, - ] + ['destructionListUuid' => ($listUuid ?? $destructionList['uuid'] ?? null)] ); $this->logger->info( @@ -415,7 +403,7 @@ public function approveList( * * @return array The updated destruction list. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-3 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function handlePartialApproval( array $destructionList, @@ -466,8 +454,8 @@ private function handlePartialApproval( * * @return array The updated destruction list. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function rejectList(array $destructionList, string $reason): array { @@ -509,215 +497,6 @@ public function rejectList(array $destructionList, string $reason): array return $destructionList; }//end rejectList() - /** - * Execute destruction for an approved destruction list. - * - * Permanently deletes objects in batches, re-checking legal holds before each deletion. - * - * @param array $destructionList The approved destruction list. - * @param string $approvedBy The user who approved the list. - * - * @return array Execution result with counts and skipped objects. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple checks per object during destruction - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-5 - */ - public function executeDestruction(array $destructionList, string $approvedBy): array - { - $destroyed = 0; - $skipped = []; - $files = 0; - - $batchSize = (int) $this->appConfig->getValueString( - app: 'openregister', - key: 'destruction_batch_size', - default: (string) self::DEFAULT_BATCH_SIZE - ); - - $objects = $destructionList['objects'] ?? []; - $batches = array_chunk($objects, $batchSize); - - foreach ($batches as $batch) { - foreach ($batch as $objectEntry) { - $uuid = $objectEntry['uuid']; - - try { - $object = $this->objectMapper->findByUuid( - $uuid - ); - - // Re-check legal hold before deletion. - if ($this->legalHoldService->hasActiveHold($object) === true) { - $skipped[] = [ - 'uuid' => $uuid, - 'reason' => 'legal_hold_placed_after_approval', - ]; - continue; - } - - // Permanently delete the object. - $this->deleteObject->delete( - objectEntity: $object, - permanent: true - ); - - $destroyed++; - } catch (\Exception $e) { - $this->logger->error( - message: '[DestructionService] Failed to destroy object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'uuid' => $uuid, - 'exception' => $e->getMessage(), - ] - ); - $skipped[] = [ - 'uuid' => $uuid, - 'reason' => 'error: '.$e->getMessage(), - ]; - }//end try - }//end foreach - }//end foreach - - $result = [ - 'destroyed' => $destroyed, - 'skipped' => $skipped, - 'skippedCount' => count($skipped), - 'filesDestroyed' => $files, - 'approvedBy' => $approvedBy, - 'executedAt' => (new DateTime())->format('c'), - ]; - - $this->logger->info( - message: '[DestructionService] Destruction execution completed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'destroyed' => $destroyed, - 'skipped' => count($skipped), - ] - ); - - return $result; - }//end executeDestruction() - - /** - * Generate a destruction certificate (verklaring van vernietiging). - * - * @param array $destructionList The completed destruction list. - * @param array $executionResult The execution result data. - * - * @return array The destruction certificate data. - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-4 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-3 - */ - public function generateCertificate(array $destructionList, array $executionResult): array - { - $now = new DateTime(); - - // Group destroyed objects by schema and selectielijst category. - $groupedBySchema = []; - $groupedByCategorie = []; - foreach ($destructionList['objects'] as $objectEntry) { - $schema = $objectEntry['schema'] ?? 'unknown'; - $categorie = $objectEntry['classificatie'] ?? 'unknown'; - - if (isset($groupedBySchema[$schema]) === false) { - $groupedBySchema[$schema] = 0; - } - - $groupedBySchema[$schema]++; - - if (isset($groupedByCategorie[$categorie]) === false) { - $groupedByCategorie[$categorie] = 0; - } - - $groupedByCategorie[$categorie]++; - } - - $approvers = array_map( - static function (array $approval): string { - return $approval['approvedBy'] ?? 'unknown'; - }, - $destructionList['approvals'] ?? [] - ); - - $certificate = [ - 'type' => 'verklaring_van_vernietiging', - 'destructionDate' => $now->format('c'), - 'approvers' => $approvers, - 'totalObjectsDestroyed' => $executionResult['destroyed'] ?? 0, - 'totalObjectsSkipped' => $executionResult['skippedCount'] ?? 0, - 'skippedObjects' => $executionResult['skipped'] ?? [], - 'objectsBySchema' => $groupedBySchema, - 'objectsBySelectielijst' => $groupedByCategorie, - 'totalFilesDestroyed' => $executionResult['filesDestroyed'] ?? 0, - 'complianceStatement' => 'Conform Archiefwet 1995 en Archiefbesluit 1995, artikelen 6-8.', - 'createdAt' => $now->format('c'), - 'immutable' => true, - ]; - - $this->logger->info( - message: '[DestructionService] Destruction certificate generated', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'destroyed' => $certificate['totalObjectsDestroyed'], - 'skipped' => $certificate['totalObjectsSkipped'], - ] - ); - - return $certificate; - }//end generateCertificate() - - /** - * Validate a destruction list for pre-flight checks. - * - * Scans all objects (and their cascade targets) for legal holds. - * - * @param array $destructionList The destruction list to validate. - * - * @return array Validation result with warnings and blocked objects. - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - */ - public function validateDestructionList(array $destructionList): array - { - $warnings = []; - $blocked = []; - - foreach ($destructionList['objects'] as $objectEntry) { - $uuid = $objectEntry['uuid']; - - try { - $object = $this->objectMapper->findByUuid($uuid); - - // Check for legal hold. - if ($this->legalHoldService->hasActiveHold($object) === true) { - $blocked[] = [ - 'uuid' => $uuid, - 'reason' => 'active_legal_hold', - ]; - } - } catch (\Exception $e) { - $warnings[] = [ - 'uuid' => $uuid, - 'reason' => 'object_not_found', - ]; - } - } - - return [ - 'valid' => empty($blocked), - 'warnings' => $warnings, - 'blocked' => $blocked, - ]; - }//end validateDestructionList() - /** * Extend the archiefactiedatum for an object by the configured period. * @@ -727,7 +506,7 @@ public function validateDestructionList(array $destructionList): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-6 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function extendArchiefactiedatum(string $uuid, string $extensionPeriod, string $reason): void { @@ -771,7 +550,7 @@ private function extendArchiefactiedatum(string $uuid, string $extensionPeriod, * * @return string The user ID or 'system' if no user is authenticated. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function getCurrentUserId(): string { diff --git a/lib/Service/Archival/LegalHoldService.php b/lib/Service/Archival/LegalHoldService.php index 3758a140e5..f84d8451c6 100644 --- a/lib/Service/Archival/LegalHoldService.php +++ b/lib/Service/Archival/LegalHoldService.php @@ -20,7 +20,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ declare(strict_types=1); @@ -114,8 +114,8 @@ public function __construct( * * @return ObjectEntity The updated object with legal hold applied. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function placeHold(ObjectEntity $object, string $reason): ObjectEntity { @@ -157,8 +157,8 @@ public function placeHold(ObjectEntity $object, string $reason): ObjectEntity * * @return ObjectEntity The updated object with legal hold released. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function releaseHold(ObjectEntity $object, string $reason): ObjectEntity { @@ -210,7 +210,7 @@ public function releaseHold(ObjectEntity $object, string $reason): ObjectEntity * * @return bool True if the object has an active legal hold. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function hasActiveHold(ObjectEntity $object): bool { @@ -227,7 +227,7 @@ public function hasActiveHold(ObjectEntity $object): bool * * @return bool True if the retention data indicates an active legal hold. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function hasActiveHoldFromRetention(array $retention): bool { @@ -245,8 +245,8 @@ public function hasActiveHoldFromRetention(array $retention): bool * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function bulkPlaceHold(int $schemaId, int $registerId, string $reason): void { @@ -280,7 +280,7 @@ public function bulkPlaceHold(int $schemaId, int $registerId, string $reason): v * * @return string The user ID or 'system' if no user is authenticated. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-5 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ private function getCurrentUserId(): string { diff --git a/lib/Service/ArchivalService.php b/lib/Service/ArchivalService.php deleted file mode 100644 index 5d857f5f7b..0000000000 --- a/lib/Service/ArchivalService.php +++ /dev/null @@ -1,482 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://OpenRegister.app - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-6 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service; - -use DateTime; -use DateInterval; -use InvalidArgumentException; -use RuntimeException; -use OCA\OpenRegister\Db\AuditTrailMapper; -use OCA\OpenRegister\Db\DestructionList; -use OCA\OpenRegister\Db\DestructionListMapper; -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Db\SelectionList; -use OCA\OpenRegister\Db\SelectionListMapper; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; -use Psr\Log\LoggerInterface; - -/** - * Service for archival and destruction workflow operations. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Required for orchestrating multiple entities - * @SuppressWarnings(PHPMD.LongVariable) - */ -class ArchivalService -{ - - /** - * Valid archival nomination values. - */ - public const VALID_NOMINATIONS = ['vernietigen', 'bewaren', 'nog_niet_bepaald']; - - /** - * Valid archival status values. - */ - public const VALID_STATUSES = ['nog_te_archiveren', 'gearchiveerd', 'vernietigd', 'overgebracht']; - - /** - * Constructor. - * - * @param IDBConnection $db Database connection - * @param SelectionListMapper $selectionListMapper Selection list mapper - * @param DestructionListMapper $destructionListMapper Destruction list mapper - * @param AuditTrailMapper $auditTrailMapper Audit trail mapper - * @param LoggerInterface $logger Logger - */ - public function __construct( - private IDBConnection $db, - private SelectionListMapper $selectionListMapper, - private DestructionListMapper $destructionListMapper, - private AuditTrailMapper $auditTrailMapper, - private LoggerInterface $logger - ) { - }//end __construct() - - /** - * Set retention metadata on an object. - * - * Validates the provided retention data against MDTO standards and - * stores it in the object's retention JSON field. - * - * @param ObjectEntity $object The object to update - * @param array $retention The retention metadata - * - * @return ObjectEntity The updated object - * - * @throws InvalidArgumentException If retention data is invalid - * - * @SuppressWarnings(PHPMD.StaticAccess) - * - * @spec openspec/changes/retrofit-2026-05-24-archival-destruction-workflow/tasks.md#task-1 - */ - public function setRetentionMetadata(ObjectEntity $object, array $retention): ObjectEntity - { - // Validate archiefnominatie. - if (isset($retention['archiefnominatie']) === false) { - $retention['archiefnominatie'] = 'nog_niet_bepaald'; - } - - if (in_array($retention['archiefnominatie'], self::VALID_NOMINATIONS, true) === false) { - $allowed = implode(', ', self::VALID_NOMINATIONS); - $value = $retention['archiefnominatie']; - throw new InvalidArgumentException( - "Invalid archiefnominatie '{$value}'. Must be one of: {$allowed}" - ); - } - - // Validate archiefstatus. - if (isset($retention['archiefstatus']) === false) { - $retention['archiefstatus'] = 'nog_te_archiveren'; - } - - if (in_array($retention['archiefstatus'], self::VALID_STATUSES, true) === false) { - $allowed = implode(', ', self::VALID_STATUSES); - $value = $retention['archiefstatus']; - throw new InvalidArgumentException( - "Invalid archiefstatus '{$value}'. Must be one of: {$allowed}" - ); - } - - // Validate archiefactiedatum format if provided. - if (isset($retention['archiefactiedatum']) === true) { - $date = DateTime::createFromFormat('Y-m-d', $retention['archiefactiedatum']); - if ($date === false) { - // Try ISO 8601 format. - $date = DateTime::createFromFormat(DateTime::ATOM, $retention['archiefactiedatum']); - if ($date === false) { - throw new InvalidArgumentException( - "Invalid archiefactiedatum format. Expected Y-m-d or ISO 8601." - ); - } - } - - $retention['archiefactiedatum'] = $date->format('c'); - } - - // Merge with existing retention data, preserving any extra fields. - $existingRetention = $object->getRetention() ?? []; - $mergedRetention = array_merge($existingRetention, $retention); - - $object->setRetention($mergedRetention); - - return $object; - }//end setRetentionMetadata() - - /** - * Calculate the archival action date from a selection list and close date. - * - * @param SelectionList $selectionList The selection list entry with retention years - * @param DateTime $closeDate The date the object was closed - * @param string|null $schemaUuid Optional schema UUID for override lookup - * - * @return DateTime The calculated archival action date - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-6 - */ - public function calculateArchivalDate( - SelectionList $selectionList, - DateTime $closeDate, - ?string $schemaUuid=null - ): DateTime { - $retentionYears = $selectionList->getRetentionYears(); - - // Check for schema-level override. - if ($schemaUuid !== null) { - $overrides = $selectionList->getSchemaOverrides() ?? []; - if (isset($overrides[$schemaUuid]) === true) { - $retentionYears = (int) $overrides[$schemaUuid]; - } - } - - $archivalDate = clone $closeDate; - $archivalDate->add(new DateInterval('P'.$retentionYears.'Y')); - - return $archivalDate; - }//end calculateArchivalDate() - - /** - * Find objects that are due for destruction. - * - * Queries the openregister_objects table for objects where the retention - * JSON field indicates they are due for destruction. - * - * @return ObjectEntity[] Array of objects due for destruction - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-1 - */ - public function findObjectsDueForDestruction(): array - { - $qb = $this->db->getQueryBuilder(); - - $qb->select('*') - ->from('openregister_objects') - ->where( - $qb->expr()->like( - 'retention', - $qb->createNamedParameter('%"archiefnominatie":"vernietigen"%') - ) - ) - ->andWhere( - $qb->expr()->like( - 'retention', - $qb->createNamedParameter('%"archiefstatus":"nog_te_archiveren"%') - ) - ); - - $result = $qb->executeQuery(); - $entities = []; - - while (($row = $result->fetch()) !== false) { - $entity = new ObjectEntity(); - $entity->setUuid($row['uuid'] ?? null); - $entity->setRegister($row['register'] ?? null); - $entity->setSchema($row['schema'] ?? null); - $entity->setName($row['name'] ?? null); - - $retention = []; - if (isset($row['retention']) === true && $row['retention'] !== null) { - $decoded = json_decode($row['retention'], true); - if (is_array($decoded) === true) { - $retention = $decoded; - } - } - - $entity->setRetention($retention); - - // Check if archiefactiedatum is past. - if (isset($retention['archiefactiedatum']) === true) { - $actionDate = new DateTime($retention['archiefactiedatum']); - if ($actionDate <= new DateTime()) { - $entities[] = $entity; - } - } - }//end while - - $result->closeCursor(); - - return $entities; - }//end findObjectsDueForDestruction() - - /** - * Generate a destruction list from objects due for destruction. - * - * Finds all objects past their archiefactiedatum with archiefnominatie - * 'vernietigen' and creates a destruction list for review. - * - * @return DestructionList|null The generated list, or null if no objects found - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - */ - public function generateDestructionList(): ?DestructionList - { - $eligibleObjects = $this->findObjectsDueForDestruction(); - - if (count($eligibleObjects) === 0) { - $this->logger->info('No objects due for destruction found'); - return null; - } - - $objectUuids = array_map( - static function (ObjectEntity $obj): string { - return $obj->getUuid(); - }, - $eligibleObjects - ); - - $list = new DestructionList(); - $list->setName('Destruction list '.(new DateTime())->format('Y-m-d H:i:s')); - $list->setObjects($objectUuids); - - return $this->destructionListMapper->createEntry($list); - }//end generateDestructionList() - - /** - * Approve a destruction list and permanently delete all objects in it. - * - * @param DestructionList $list The destruction list to approve - * @param string $userId The ID of the approving user - * - * @return array{destroyed: int, errors: int, list: DestructionList} Result summary - * - * @throws InvalidArgumentException If list is not in pending_review status - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - */ - public function approveDestructionList(DestructionList $list, string $userId): array - { - if ($list->getStatus() !== DestructionList::STATUS_PENDING_REVIEW) { - throw new InvalidArgumentException( - "Cannot approve destruction list with status '{$list->getStatus()}'. Must be 'pending_review'." - ); - } - - $list->setStatus(DestructionList::STATUS_APPROVED); - $list->setApprovedBy($userId); - $list->setApprovedAt(new DateTime()); - - $destroyed = 0; - $errors = 0; - $objects = $list->getObjects() ?? []; - - foreach ($objects as $objectUuid) { - try { - $this->destroyObject(objectUuid: $objectUuid, destructionListId: $list->getUuid()); - $destroyed++; - } catch (\Exception $e) { - $this->logger->error( - "Failed to destroy object {$objectUuid}: ".$e->getMessage(), - ['exception' => $e] - ); - $errors++; - } - }//end foreach - - $list->setStatus(DestructionList::STATUS_COMPLETED); - $list->setNotes( - ($list->getNotes() ?? '')."\nDestroyed: {$destroyed}, Errors: {$errors}" - ); - $this->destructionListMapper->updateEntry($list); - - return [ - 'destroyed' => $destroyed, - 'errors' => $errors, - 'list' => $list, - ]; - }//end approveDestructionList() - - /** - * Destroy a single object and create audit trail. - * - * @param string $objectUuid The UUID of the object to destroy - * @param string $destructionListId The destruction list UUID for audit trail - * - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) Reserved for future audit-trail correlation. - * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-6 - */ - private function destroyObject(string $objectUuid, string $destructionListId): void - { - $qb = $this->db->getQueryBuilder(); - - // Fetch the object data for the audit trail. - $qb->select('*') - ->from('openregister_objects') - ->where($qb->expr()->eq('uuid', $qb->createNamedParameter($objectUuid))); - - $result = $qb->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - if ($row === false) { - throw new RuntimeException("Object {$objectUuid} not found"); - } - - // Create an ObjectEntity for audit trail. - $object = new ObjectEntity(); - $object->setUuid($row['uuid'] ?? null); - $object->setRegister($row['register'] ?? null); - $object->setSchema($row['schema'] ?? null); - $object->setName($row['name'] ?? null); - - // Create audit trail entry. - $this->auditTrailMapper->createAuditTrail( - $object, - null, - 'archival.destroyed' - ); - - // Hard delete the object row. - $deleteQb = $this->db->getQueryBuilder(); - $deleteQb->delete('openregister_objects') - ->where($deleteQb->expr()->eq('uuid', $deleteQb->createNamedParameter($objectUuid))); - $deleteQb->executeStatement(); - }//end destroyObject() - - /** - * Reject (remove) specific objects from a destruction list. - * - * Removed objects have their archiefactiedatum extended by the original - * retention period from their selection list category. - * - * @param DestructionList $list The destruction list - * @param string[] $objectUuids UUIDs of objects to remove from the list - * - * @return DestructionList The updated destruction list - * - * @throws InvalidArgumentException If list is not in pending_review status - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-2 - */ - public function rejectFromDestructionList(DestructionList $list, array $objectUuids): DestructionList - { - if ($list->getStatus() !== DestructionList::STATUS_PENDING_REVIEW) { - throw new InvalidArgumentException( - "Cannot modify destruction list with status '{$list->getStatus()}'. Must be 'pending_review'." - ); - } - - $currentObjects = $list->getObjects() ?? []; - $remainingObjects = array_values(array_diff($currentObjects, $objectUuids)); - - // Extend archiefactiedatum for rejected objects. - foreach ($objectUuids as $uuid) { - $this->extendRetentionForObject(uuid: $uuid); - } - - $list->setObjects($remainingObjects); - - if (count($remainingObjects) === 0) { - $list->setStatus(DestructionList::STATUS_CANCELLED); - } - - return $this->destructionListMapper->updateEntry($list); - }//end rejectFromDestructionList() - - /** - * Extend the retention period for a specific object. - * - * @param string $uuid The object UUID - * - * @return void - * - * @spec openspec/changes/retrofit-2026-05-24-archival-destruction-workflow/tasks.md#task-2 - */ - private function extendRetentionForObject(string $uuid): void - { - try { - $qb = $this->db->getQueryBuilder(); - $qb->select('retention') - ->from('openregister_objects') - ->where($qb->expr()->eq('uuid', $qb->createNamedParameter($uuid))); - - $result = $qb->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - if ($row === false) { - return; - } - - $retention = json_decode($row['retention'] ?? '{}', true) ?? []; - - if (isset($retention['classificatie']) === true) { - $selectionLists = $this->selectionListMapper->findByCategory( - $retention['classificatie'] - ); - if (count($selectionLists) > 0) { - $retentionYears = $selectionLists[0]->getRetentionYears(); - - $rawActieDatum = ($retention['archiefactiedatum'] ?? null); - $currentDate = new DateTime(); - if ($rawActieDatum !== null) { - $currentDate = new DateTime($rawActieDatum); - } - - $newDate = clone $currentDate; - $newDate->add(new DateInterval('P'.$retentionYears.'Y')); - $retention['archiefactiedatum'] = $newDate->format('c'); - - // Update the retention field. - $updateQb = $this->db->getQueryBuilder(); - $updateQb->update('openregister_objects') - ->set('retention', $updateQb->createNamedParameter(json_encode($retention))) - ->where($updateQb->expr()->eq('uuid', $updateQb->createNamedParameter($uuid))); - $updateQb->executeStatement(); - } - }//end if - } catch (\Exception $e) { - $this->logger->warning( - "Could not extend retention for rejected object {$uuid}: ".$e->getMessage() - ); - }//end try - }//end extendRetentionForObject() -}//end class diff --git a/lib/Service/AuditHashService.php b/lib/Service/AuditHashService.php index 53ee7031d1..1b05f10131 100644 --- a/lib/Service/AuditHashService.php +++ b/lib/Service/AuditHashService.php @@ -12,19 +12,19 @@ * @category Service * @package OCA\OpenRegister\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-7 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-11 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-10 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-9 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-14 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-13 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ declare(strict_types=1); @@ -34,6 +34,9 @@ use OCA\OpenRegister\Db\AuditTrail; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use Psr\Log\LoggerInterface; /** * Handles cryptographic hash chaining for audit trail entries. @@ -41,6 +44,8 @@ * @package OCA\OpenRegister\Service * * @psalm-suppress UnusedClass + * + * @spec openspec/specs/audit-hash-chain/spec.md */ class AuditHashService { @@ -51,13 +56,44 @@ class AuditHashService */ private const GENESIS_SEED = 'openregister-genesis-v1'; + /** + * Well-known advisory lock key serializing ALL seal passes. + * + * The methods sealRow() and sealRows() race each other (and themselves + * across requests): both read a predecessor hash and then UPDATE, so two + * interleaved passes can chain a boundary row over a stale predecessor — + * a false tamper alarm on the next verifyChain(). A single exclusive + * lock on this key makes seal passes strictly sequential. + * + * @var string + */ + private const SEAL_LOCK_KEY = 'openregister/audit-seal'; + + /** + * Number of acquisition attempts before giving up on the seal lock. + * + * @var int + */ + private const SEAL_LOCK_ATTEMPTS = 3; + + /** + * Delay between seal-lock acquisition attempts, in microseconds. + * + * @var int + */ + private const SEAL_LOCK_RETRY_DELAY_USEC = 50000; + /** * Constructor for AuditHashService. * - * @param IDBConnection $db The database connection + * @param IDBConnection $db The database connection + * @param ILockingProvider $lockingProvider Advisory lock provider serializing seal passes + * @param LoggerInterface $logger Logger for fail-soft lock warnings */ public function __construct( - private readonly IDBConnection $db + private readonly IDBConnection $db, + private readonly ILockingProvider $lockingProvider, + private readonly LoggerInterface $logger ) { }//end __construct() @@ -66,8 +102,8 @@ public function __construct( * * @return string The SHA-256 hex digest of the genesis seed * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-10 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function getGenesisHash(): string { @@ -84,8 +120,8 @@ public function getGenesisHash(): string * * @return string The canonical JSON string * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-9 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function getCanonicalJson(AuditTrail $entry): string { @@ -108,8 +144,8 @@ public function getCanonicalJson(AuditTrail $entry): string * * @return string The SHA-256 hex digest * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-11 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function computeHash(AuditTrail $entry, string $previousHash): string { @@ -125,8 +161,8 @@ public function computeHash(AuditTrail $entry, string $previousHash): string * * @return string The hash of the last entry or the genesis hash * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-13 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function getLastHash(): string { @@ -148,6 +184,283 @@ public function getLastHash(): string return $row['hash']; }//end getLastHash() + /** + * Seal an already-inserted audit-trail row into the hash chain. + * + * Reads the row by id, derives `previousHash` from the nearest PRIOR + * SEALED row (or genesis), and computes `hash` over the row using the SAME + * `mapRowToEntity()` + `hydrate()` + `getCanonicalJson()` path that + * {@see verifyChain()} uses — guaranteeing the stored hash re-verifies + * exactly. The `hash` / `previous_hash` values are then written directly. + * Call this AFTER inserting the row so the id and all persisted (DB-typed) + * field values are final. + * + * The critical section (predecessor read + hash write) runs under the + * exclusive {@see self::SEAL_LOCK_KEY} advisory lock shared with + * {@see sealRows()}. Fail-soft on contention: when the lock cannot be + * acquired within a short bounded wait the row is left unsealed (a later + * seal pass picks it up — {@see getHashBefore()} chains over unsealed + * rows exactly like verifyChain() does) rather than blocking the write + * path. + * + * @param int $id The audit-trail row id to seal. + * + * @return bool True when the row was found and sealed. + * + * @spec openspec/specs/audit-hash-chain/spec.md + */ + public function sealRow(int $id): bool + { + if ($this->acquireSealLock() === false) { + $this->logger->warning( + '[AuditHashService] seal lock unavailable, leaving audit row '.$id.' unsealed (a later seal pass will chain it)' + ); + + return false; + } + + try { + return $this->sealRowLocked(id: $id); + } finally { + $this->releaseSealLock(); + } + }//end sealRow() + + /** + * Seal a single row — body of {@see sealRow()}, caller holds the seal lock. + * + * @param int $id The audit-trail row id to seal. + * + * @return bool True when the row was found and sealed. + */ + private function sealRowLocked(int $id): bool + { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('openregister_audit_trails') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row === false) { + return false; + } + + $previousHash = $this->getHashBefore(id: $id); + if ($previousHash === null) { + $previousHash = $this->getGenesisHash(); + } + + $entry = new AuditTrail(); + $entry->hydrate(object: $this->mapRowToEntity(row: $row)); + + $hash = $this->computeHash(entry: $entry, previousHash: $previousHash); + + $update = $this->db->getQueryBuilder(); + $update->update('openregister_audit_trails') + ->set('hash', $update->createNamedParameter($hash)) + ->set('previous_hash', $update->createNamedParameter($previousHash)) + ->where($update->expr()->eq('id', $update->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + $update->executeStatement(); + + return true; + + }//end sealRowLocked() + + /** + * Seal a batch of already-inserted audit-trail rows into the hash chain. + * + * Batched counterpart of {@see sealRow()} for bulk audit inserts: instead + * of 3 queries per row (row SELECT + previous-hash SELECT + UPDATE) it + * runs one range SELECT, one previous-hash SELECT, and one CASE-based + * UPDATE for the whole batch. To keep the chain verifiable when foreign + * rows interleave with the batch (concurrent writers), ALL unsealed rows + * in the id range [min(ids), max(ids)] are sealed — the hash computed for + * any row is deterministic (same canonical JSON + same predecessor), so + * sealing an interleaved row here yields the identical value its own + * sealRow() call would produce. Already-sealed rows are left untouched + * and only contribute their stored hash as the chain link. + * + * Seal passes are serialized under the exclusive + * {@see self::SEAL_LOCK_KEY} advisory lock shared with {@see sealRow()}: + * without it a concurrent writer could seal a boundary row between our + * getHashBefore() read and our UPDATE (or vice versa), chaining one link + * over a stale predecessor — a false tamper alarm on the next + * verifyChain(). Fail-soft on contention: when the lock cannot be + * acquired within a short bounded wait the rows are left unsealed (a + * later seal pass chains them; unsealed rows are skipped by both + * verifyChain() and getHashBefore()) rather than blocking the write + * path. Rows inserted by a concurrent uncommitted transaction that land + * inside the range after our range SELECT stay unsealed too — harmless + * for the same reason. + * + * @param int[] $ids The audit-trail row ids to seal. + * + * @return int Number of rows sealed. + * + * @spec openspec/specs/audit-hash-chain/spec.md + */ + public function sealRows(array $ids): int + { + $ids = array_values( + array_filter( + array_map('intval', $ids), + static function (int $id): bool { + return $id > 0; + } + ) + ); + + if ($ids === []) { + return 0; + } + + if ($this->acquireSealLock() === false) { + $this->logger->warning( + '[AuditHashService] seal lock unavailable, leaving '.count($ids).' audit rows unsealed (a later seal pass will chain them)' + ); + + return 0; + } + + try { + return $this->sealRowsLocked(ids: $ids); + } finally { + $this->releaseSealLock(); + } + }//end sealRows() + + /** + * Seal a batch of rows — body of {@see sealRows()}, caller holds the + * seal lock and has already normalised `$ids` to positive integers. + * + * @param int[] $ids The audit-trail row ids to seal (non-empty). + * + * @return int Number of rows sealed. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Chain walking requires sealed/unsealed branching + * @SuppressWarnings(PHPMD.NPathComplexity) Same — early-outs plus per-row sealed/unsealed paths + */ + private function sealRowsLocked(array $ids): int + { + $minId = min($ids); + $maxId = max($ids); + + // Fetch every row in the id range so interleaved foreign rows keep + // their place in the chain. + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('openregister_audit_trails') + ->where($qb->expr()->gte('id', $qb->createNamedParameter($minId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->lte('id', $qb->createNamedParameter($maxId, IQueryBuilder::PARAM_INT))) + ->orderBy('id', 'ASC'); + + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + if ($rows === []) { + return 0; + } + + $previousHash = $this->getHashBefore(id: $minId); + if ($previousHash === null) { + $previousHash = $this->getGenesisHash(); + } + + // Walk the range in id order, computing hashes for unsealed rows. + $updates = []; + foreach ($rows as $row) { + $storedHash = ($row['hash'] ?? null); + if ($storedHash !== null && $storedHash !== '') { + // Already sealed — adopt its hash as the next chain link. + $previousHash = $storedHash; + continue; + } + + $entry = new AuditTrail(); + $entry->hydrate(object: $this->mapRowToEntity(row: $row)); + + $hash = $this->computeHash(entry: $entry, previousHash: $previousHash); + + $updates[(int) $row['id']] = [ + 'hash' => $hash, + 'previousHash' => $previousHash, + ]; + + $previousHash = $hash; + } + + if ($updates === []) { + return 0; + } + + // Single CASE-based UPDATE for the whole batch. + $tableName = $this->db->getQueryBuilder()->getTableName('openregister_audit_trails'); + $hashCases = []; + $prevCases = []; + $parameters = []; + foreach ($updates as $id => $values) { + $hashCases[] = 'WHEN '.((int) $id).' THEN ?'; + $parameters[] = $values['hash']; + } + + foreach ($updates as $id => $values) { + $prevCases[] = 'WHEN '.((int) $id).' THEN ?'; + $parameters[] = $values['previousHash']; + } + + $idList = implode(',', array_map('intval', array_keys($updates))); + + $sql = 'UPDATE '.$tableName + .' SET hash = CASE id '.implode(' ', $hashCases).' ELSE hash END,' + .' previous_hash = CASE id '.implode(' ', $prevCases).' ELSE previous_hash END' + .' WHERE id IN ('.$idList.')'; + + $this->db->executeStatement($sql, $parameters); + + return count($updates); + }//end sealRowsLocked() + + /** + * Try to acquire the exclusive advisory lock serializing seal passes. + * + * Bounded, short wait: {@see self::SEAL_LOCK_ATTEMPTS} attempts with + * {@see self::SEAL_LOCK_RETRY_DELAY_USEC} between them. Sealing is + * fail-soft, so on sustained contention the caller logs and leaves the + * rows unsealed instead of blocking the surrounding write path. + * + * @return bool True when the lock was acquired. + */ + private function acquireSealLock(): bool + { + for ($attempt = 1; $attempt <= self::SEAL_LOCK_ATTEMPTS; $attempt++) { + try { + $this->lockingProvider->acquireLock(self::SEAL_LOCK_KEY, ILockingProvider::LOCK_EXCLUSIVE); + + return true; + } catch (LockedException) { + if ($attempt < self::SEAL_LOCK_ATTEMPTS) { + usleep(self::SEAL_LOCK_RETRY_DELAY_USEC); + } + } + } + + return false; + }//end acquireSealLock() + + /** + * Release the exclusive seal lock acquired by {@see acquireSealLock()}. + * + * @return void + */ + private function releaseSealLock(): void + { + $this->lockingProvider->releaseLock(self::SEAL_LOCK_KEY, ILockingProvider::LOCK_EXCLUSIVE); + }//end releaseSealLock() + /** * Verify the integrity of the hash chain. * @@ -168,8 +481,8 @@ public function getLastHash(): string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-14 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function verifyChain(?int $from=null, ?int $to=null): array { @@ -265,13 +578,21 @@ public function verifyChain(?int $from=null, ?int $to=null): array }//end verifyChain() /** - * Get the hash of the entry immediately before the given ID. + * Get the hash of the nearest SEALED entry before the given ID. + * + * Unsealed rows (hash NULL/empty — fail-soft leftovers or + * pre-migration entries) are skipped, exactly mirroring how + * {@see verifyChain()} walks the chain: it skips null-hash rows and + * carries the last SEALED hash forward. Chaining a new seal from the + * immediately-prior row instead would fall back to genesis whenever + * that row happens to be unsealed, permanently breaking verification + * at that link. * * @param int $id The entry ID * - * @return string|null The hash of the previous entry, or null if none + * @return string|null The hash of the nearest prior sealed entry, or null if none * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/audit-hash-chain/spec.md */ private function getHashBefore(int $id): ?string { @@ -282,6 +603,10 @@ private function getHashBefore(int $id): ?string ->where( $qb->expr()->lt('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) ) + ->andWhere($qb->expr()->isNotNull('hash')) + ->andWhere( + $qb->expr()->neq('hash', $qb->createNamedParameter('')) + ) ->orderBy('id', 'DESC') ->setMaxResults(1); @@ -303,7 +628,7 @@ private function getHashBefore(int $id): ?string * * @return array The mapped array with camelCase keys * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-7 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ private function mapRowToEntity(array $row): array { diff --git a/lib/Service/AuditQueryService.php b/lib/Service/AuditQueryService.php new file mode 100644 index 0000000000..41ae8eefd7 --- /dev/null +++ b/lib/Service/AuditQueryService.php @@ -0,0 +1,461 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-1.1 + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-1.2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use DateTime; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCP\AppFramework\Db\DoesNotExistException; + +/** + * Queries audit-entry objects (any app, any register/schema) via ObjectService. + * + * Audit-entry objects are ordinary OpenRegister objects; this service does + * not introduce a new storage concept. When neither `registerId`/`app` nor + * `schemaId` is supplied, it falls back to a naming convention (schema slug + * or title containing "audit") so the endpoint stays schema-agnostic while + * still honouring the "no data exposure" requirement — it must not return + * every business object in the instance by default. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-1.1 + */ +class AuditQueryService +{ + + /** + * Maximum allowed page size. + * + * @var int + */ + private const MAX_LIMIT = 200; + + /** + * Default page size when none (or an invalid one) is supplied. + * + * @var int + */ + private const DEFAULT_LIMIT = 50; + + /** + * Constructor. + * + * @param ObjectService $objectService Cross-schema object search (audit entries ARE objects). + * @param RegisterMapper $registerMapper Register lookup + register/schema relationship helpers. + * @param SchemaMapper $schemaMapper Schema lookup. + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper + ) { + + }//end __construct() + + /** + * Query audit-entry objects across all apps/registers/schemas. + * + * @param array $filters Supported keys: registerId, schemaId, + * objectId, app (alias for registerId), + * timestampStart, timestampEnd, sort + * (default 'created:desc'). + * @param int $limit Requested page size; clamped to [1, 200]. + * @param int $offset Requested offset; clamped to >= 0. + * + * @return array{entries: array>, total: int, limit: int, offset: int} + * + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-1.1 + * @spec openspec/changes/public-audit-query-endpoint/tasks.md#task-1.2 + */ + public function query(array $filters, int $limit, int $offset): array + { + $limit = $this->clampLimit(limit: $limit); + $offset = max(0, $offset); + + $pairs = $this->resolveRegisterSchemaPairs(filters: $filters); + + if (empty($pairs) === true) { + // Graceful handling of an unconfigured audit schema: no matching + // register/schema pair, no error, just an empty page. + return [ + 'entries' => [], + 'total' => 0, + 'limit' => $limit, + 'offset' => $offset, + ]; + } + + $rows = $this->collectEntries(pairs: $pairs, filters: $filters); + $rows = $this->applyTimestampFilter(rows: $rows, filters: $filters); + $rows = $this->sortRows(rows: $rows, filters: $filters); + + $total = count($rows); + $page = array_slice($rows, $offset, $limit); + + return [ + 'entries' => array_map(fn (array $row): array => $this->mapEntry(row: $row), $page), + 'total' => $total, + 'limit' => $limit, + 'offset' => $offset, + ]; + + }//end query() + + /** + * Clamp a requested page size into [1, MAX_LIMIT]. + * + * @param int $limit Requested limit (may be <= 0 or > MAX_LIMIT). + * + * @return int Clamped limit, defaulting to DEFAULT_LIMIT when 0/absent. + */ + private function clampLimit(int $limit): int + { + if ($limit <= 0) { + // Not just "below the minimum" — absent/invalid input defaults + // to DEFAULT_LIMIT rather than clamping up to MIN_LIMIT, per the + // documented contract ("default 50" on 0/absent). + $limit = self::DEFAULT_LIMIT; + } + + if ($limit > self::MAX_LIMIT) { + return self::MAX_LIMIT; + } + + return $limit; + + }//end clampLimit() + + /** + * Resolve the (Register, Schema) pairs to search, based on filters. + * + * - registerId/schemaId both given: exactly that pair. + * - only registerId (or its `app` alias) given: every schema on that + * register that looks like an audit schema. + * - only schemaId given: that schema on every register that references it. + * - neither given: every schema (on every register) that looks like an + * audit schema, i.e. slug or title contains "audit". + * + * @param array $filters Raw filters, see {@see self::query()}. + * + * @return array + */ + private function resolveRegisterSchemaPairs(array $filters): array + { + $registerSlug = $filters['registerId'] ?? $filters['app'] ?? null; + $schemaSlug = $filters['schemaId'] ?? null; + + // Explicit schema requested: trust it, regardless of naming. + if ($schemaSlug !== null && $schemaSlug !== '') { + return $this->pairsForExplicitSchema(registerSlug: $registerSlug, schemaSlug: $schemaSlug); + } + + // No explicit schema: fall back to the "looks like audit" convention + // so the endpoint does not default to exposing every business object. + if ($registerSlug !== null && $registerSlug !== '') { + try { + $register = $this->registerMapper->find(id: $registerSlug); + } catch (DoesNotExistException $e) { + return []; + } + + return $this->auditSchemaPairsForRegister(register: $register); + } + + $pairs = []; + foreach ($this->registerMapper->findAll() as $register) { + $pairs = array_merge($pairs, $this->auditSchemaPairsForRegister(register: $register)); + } + + return $pairs; + + }//end resolveRegisterSchemaPairs() + + /** + * Resolve pairs when an explicit schemaId filter was supplied. + * + * @param mixed $registerSlug Register slug/id filter, or null. + * @param mixed $schemaSlug Schema slug/id filter (non-empty). + * + * @return array + */ + private function pairsForExplicitSchema($registerSlug, $schemaSlug): array + { + try { + $schema = $this->schemaMapper->find(id: $schemaSlug); + } catch (DoesNotExistException $e) { + return []; + } + + if ($registerSlug !== null && $registerSlug !== '') { + try { + $register = $this->registerMapper->find(id: $registerSlug); + } catch (DoesNotExistException $e) { + return []; + } + + return [[$register, $schema]]; + } + + $pairs = []; + foreach ($this->registerMapper->getAllRegisterIdsWithSchema(schemaId: $schema->getId()) as $registerId) { + try { + $pairs[] = [$this->registerMapper->find(id: $registerId), $schema]; + } catch (DoesNotExistException $e) { + continue; + } + } + + return $pairs; + + }//end pairsForExplicitSchema() + + /** + * All schemas on a register whose slug/title looks like an audit schema. + * + * @param Register $register The register to inspect. + * + * @return array + */ + private function auditSchemaPairsForRegister(Register $register): array + { + $pairs = []; + foreach ($this->registerMapper->getSchemasByRegisterId(registerId: $register->getId()) as $schema) { + if ($this->looksLikeAuditSchema(schema: $schema) === true) { + $pairs[] = [$register, $schema]; + } + } + + return $pairs; + + }//end auditSchemaPairsForRegister() + + /** + * Naming-convention heuristic: does this schema look like an audit schema? + * + * @param Schema $schema The schema to inspect. + * + * @return bool True when the slug or title contains "audit" (case-insensitive). + */ + private function looksLikeAuditSchema(Schema $schema): bool + { + $slug = strtolower($schema->getSlug() ?? ''); + $title = strtolower($schema->getTitle() ?? ''); + + return str_contains($slug, 'audit') === true || str_contains($title, 'audit') === true; + + }//end looksLikeAuditSchema() + + /** + * Search every resolved (register, schema) pair and flatten the results. + * + * @param array $pairs Pairs to search. + * @param array $filters Raw filters (for the objectId field filter). + * + * @return array + */ + private function collectEntries(array $pairs, array $filters): array + { + $objectFilters = []; + if (empty($filters['objectId']) === false) { + $objectFilters['objectId'] = $filters['objectId']; + } + + $rows = []; + foreach ($pairs as [$register, $schema]) { + try { + $result = $this->objectService->searchObjectsBySlug( + registerSlug: (string) $register->getSlug(), + schemaSlug: (string) $schema->getSlug(), + filters: $objectFilters + ); + } catch (DoesNotExistException $e) { + continue; + } + + if (is_int($result) === true) { + continue; + } + + foreach ($result as $entity) { + $rows[] = [ + 'entity' => $entity, + 'registerSlug' => (string) $register->getSlug(), + 'schemaSlug' => (string) $schema->getSlug(), + ]; + } + }//end foreach + + return $rows; + + }//end collectEntries() + + /** + * Filter rows by the (optional) timestampStart / timestampEnd ISO-8601 bounds. + * + * @param array> $rows Rows collected so far. + * @param array $filters Raw filters. + * + * @return array> Filtered rows. + */ + private function applyTimestampFilter(array $rows, array $filters): array + { + $start = $this->parseTimestamp(value: $filters['timestampStart'] ?? null); + $end = $this->parseTimestamp(value: $filters['timestampEnd'] ?? null); + + if ($start === null && $end === null) { + return $rows; + } + + return array_values( + array_filter( + $rows, + function (array $row) use ($start, $end): bool { + $created = $row['entity']->getCreated(); + if ($created === null) { + return false; + } + + if ($start !== null && $created < $start) { + return false; + } + + if ($end !== null && $created > $end) { + return false; + } + + return true; + } + ) + ); + + }//end applyTimestampFilter() + + /** + * Parse a filter value into a DateTime, tolerating invalid/absent input. + * + * @param mixed $value Raw filter value. + * + * @return DateTime|null Parsed timestamp, or null when absent/invalid. + */ + private function parseTimestamp($value): ?DateTime + { + if (is_string($value) === false || $value === '') { + return null; + } + + try { + return new DateTime($value); + } catch (\Exception $e) { + return null; + } + + }//end parseTimestamp() + + /** + * Sort rows per the `sort` filter (format `field:direction`), default `created:desc`. + * + * Only `created` is a supported sort field today; any other field falls + * back to the default so a bad/unknown sort value cannot error the request. + * + * @param array> $rows Rows to sort. + * @param array $filters Raw filters. + * + * @return array> Sorted rows. + */ + private function sortRows(array $rows, array $filters): array + { + $sort = (string) ($filters['sort'] ?? 'created:desc'); + [$field, $order] = array_pad(explode(':', $sort, 2), 2, 'desc'); + + if ($field !== 'created') { + $field = 'created'; + } + + $direction = 1; + if (strtolower($order) === 'desc') { + $direction = -1; + } + + usort( + $rows, + function (array $a, array $b) use ($direction): int { + $aCreated = $a['entity']->getCreated(); + $bCreated = $b['entity']->getCreated(); + + if ($aCreated === null || $bCreated === null) { + return 0; + } + + return $direction * ($aCreated <=> $bCreated); + } + ); + + return $rows; + + }//end sortRows() + + /** + * Map a collected row to the public response shape. + * + * Only what was logged is exposed — the audit entry's own object data — + * never the current state of whatever it describes. + * + * @param array $row One row from {@see self::collectEntries()}. + * + * @return array + */ + private function mapEntry(array $row): array + { + $entity = $row['entity']; + $data = $entity->getObject() ?? []; + $created = $entity->getCreated(); + + $createdIso = null; + if ($created !== null) { + $createdIso = $created->format(DateTime::ATOM); + } + + return [ + 'id' => $entity->getUuid(), + 'registerId' => $row['registerSlug'], + 'schemaId' => $row['schemaSlug'], + 'objectId' => ($data['objectId'] ?? null), + 'data' => $data, + 'created' => $createdIso, + 'userId' => $entity->getOwner(), + ]; + + }//end mapEntry() +}//end class diff --git a/lib/Service/AuthenticationService.php b/lib/Service/AuthenticationService.php index badc22c355..9b3a294107 100644 --- a/lib/Service/AuthenticationService.php +++ b/lib/Service/AuthenticationService.php @@ -9,9 +9,9 @@ * @category Service * @package OCA\OpenRegister\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -33,7 +33,9 @@ use Jose\Component\Signature\Serializer\CompactSerializer; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Twig\Environment; +use Twig\Extension\SandboxExtension; use Twig\Loader\ArrayLoader; +use Twig\Sandbox\SecurityPolicy; /** * Service for handling authentication against external services. @@ -91,12 +93,58 @@ class AuthenticationService * * @param ArrayLoader $loader The Twig array loader * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ public function __construct(ArrayLoader $loader) { $this->twig = new Environment($loader); + // SSTI hardening (SEC-SVC-4): the JWT payload template is user-authored + // source configuration, so it MUST be compiled inside a Twig sandbox. + // Only the tags/filters/functions needed to build a JWT claim set are + // allowlisted; method/property access on objects is denied entirely. + $policy = new SecurityPolicy( + allowedTags: [ + 'if', + 'for', + 'set', + 'apply', + ], + allowedFilters: [ + 'date', + 'date_modify', + 'upper', + 'lower', + 'trim', + 'length', + 'default', + 'number_format', + 'round', + 'abs', + 'split', + 'join', + 'slice', + 'first', + 'last', + 'replace', + 'format', + 'merge', + 'keys', + 'escape', + 'raw', + 'json_encode', + ], + allowedMethods: [], + allowedProperties: [], + allowedFunctions: [ + 'max', + 'min', + 'range', + 'date', + ] + ); + $this->twig->addExtension(new SandboxExtension($policy, sandboxed: true)); + }//end __construct() /** @@ -108,7 +156,7 @@ public function __construct(ArrayLoader $loader) * * @throws BadRequestException If required parameters are missing. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ private function createClientCredentialConfig(array $configuration): array { @@ -164,7 +212,7 @@ private function createClientCredentialConfig(array $configuration): array * * @throws BadRequestException If required parameters are missing. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ private function createPasswordConfig(array $configuration): array { @@ -207,7 +255,7 @@ private function createPasswordConfig(array $configuration): array * @throws BadRequestException If configuration is incomplete. * @throws \GuzzleHttp\Exception\GuzzleException If the token endpoint fails. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ public function fetchOAuthTokens(array $configuration): string { @@ -251,7 +299,7 @@ public function fetchOAuthTokens(array $configuration): string * * @throws \GuzzleHttp\Exception\GuzzleException If the request fails. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ public function fetchDecosToken(array $configuration): string { @@ -281,7 +329,7 @@ public function fetchDecosToken(array $configuration): string * * @return JWK The JWK key. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ private function getRSJWK(array $configuration): JWK { @@ -306,7 +354,7 @@ private function getRSJWK(array $configuration): JWK * * @return JWK The JWK key. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ private function getHSJWK(array $configuration): JWK { @@ -330,11 +378,36 @@ private function getHSJWK(array $configuration): JWK * @throws \Twig\Error\SyntaxError If the template has syntax errors. * @throws \Twig\Error\RuntimeError If the template rendering fails. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ private function getJWTPayload(array $configuration): array { - $renderedPayload = $this->twig->createTemplate($configuration['payload'])->render($configuration); + // SEC-SVC-4: never expose secret material to the payload template + // context. Strip credential-bearing keys so a crafted payload template + // cannot echo adjacent secrets into the rendered claim set. + $secretKeys = [ + 'secret', + 'clientSecret', + 'client_secret', + 'password', + 'privateKey', + 'private_key', + 'apiKey', + 'api_key', + 'token', + 'accessToken', + 'access_token', + 'refreshToken', + 'refresh_token', + 'authorization', + ]; + + $context = $configuration; + foreach ($secretKeys as $secretKey) { + unset($context[$secretKey]); + } + + $renderedPayload = $this->twig->createTemplate($configuration['payload'])->render($context); return json_decode(json: $renderedPayload, associative: true); @@ -349,7 +422,7 @@ private function getJWTPayload(array $configuration): array * * @throws BadRequestException If the algorithm is not supported. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ private function getJWK(array $configuration): JWK { @@ -375,7 +448,7 @@ private function getJWK(array $configuration): JWK * * @return string The compact-serialized JWT string. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ private function generateJWT(array $payload, JWK $jwk, string $algorithm, ?string $x5t=null): string { @@ -421,7 +494,7 @@ private function generateJWT(array $payload, JWK $jwk, string $algorithm, ?strin * * @throws BadRequestException If required parameters are missing. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-27 + * @spec openspec/specs/rbac-scopes/spec.md */ public function fetchJWTToken(array $configuration): string { diff --git a/lib/Service/AuthorizationAuditService.php b/lib/Service/AuthorizationAuditService.php index 253723a768..a438f6d269 100644 --- a/lib/Service/AuthorizationAuditService.php +++ b/lib/Service/AuthorizationAuditService.php @@ -17,8 +17,8 @@ * @version GIT: * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-56 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-58 + * @spec openspec/specs/rbac-scopes/spec.md#requirement-register-level-authorization-cascade + * @spec openspec/specs/rbac-scopes/spec.md#requirement-register-authorization-cache */ declare(strict_types=1); @@ -115,7 +115,7 @@ public function logSchemaAuthorizationChange( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-56 + * @spec openspec/specs/rbac-scopes/spec.md#requirement-register-level-authorization-cascade */ public function logRegisterAuthorizationChange( int $registerId, @@ -172,7 +172,7 @@ public function logRegisterAuthorizationChange( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-58 + * @spec openspec/specs/rbac-scopes/spec.md#requirement-register-authorization-cache */ public function logRoleDefinitionChange( int $registerId, diff --git a/lib/Service/AuthorizationService.php b/lib/Service/AuthorizationService.php index d19f9bb6a0..970289a546 100644 --- a/lib/Service/AuthorizationService.php +++ b/lib/Service/AuthorizationService.php @@ -9,9 +9,9 @@ * @category Service * @package OCA\OpenRegister\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -258,10 +258,46 @@ protected function authorizeJwt(string $authorization): void $authConf = $issuer->getAuthorizationConfiguration(); $publicKey = $authConf['publicKey'] ?? ''; - $algorithm = $authConf['algorithm'] ?? $header['alg']; + + // The verification algorithm MUST come from the issuer's server-side + // configuration, never from the attacker-controlled token header. Taking + // it from `$header['alg']` enables an algorithm-confusion attack: an + // RS/PS-configured issuer (whose `publicKey` is, by definition, public) + // could be verified via HMAC using that public key as the secret, letting + // anyone forge a valid HS token. Reject when no algorithm is pinned. + $algorithm = $authConf['algorithm'] ?? null; + if (is_string($algorithm) === false || $algorithm === '') { + throw new AuthenticationException( + message: 'The token could not be validated', + details: ['reason' => 'No verification algorithm configured for issuer'] + ); + } + + // The token's declared algorithm MUST match the pinned one — an + // asymmetric-configured issuer refuses an HMAC token and vice versa. + if ($header['alg'] !== $algorithm) { + throw new AuthenticationException( + message: 'The token could not be validated', + details: ['reason' => 'Token algorithm does not match issuer configuration'] + ); + } $signature = $this->base64urlDecode(data: $signatureB64); + // Asymmetric algorithms (RS/PS) MUST be verified against the public key + // with a real signature check — they MUST NOT fall through to HMAC. Until + // an asymmetric verifier is implemented (see the + // fix-jwt-algorithm-confusion change, tasks 2.1/2.2), fail closed rather + // than HMAC-verify with the public key. + if (in_array($algorithm, self::PKCS1_ALGORITHMS, true) === true + || in_array($algorithm, self::PSS_ALGORITHMS, true) === true + ) { + throw new AuthenticationException( + message: 'The token algorithm is not supported', + details: ['algorithm' => $algorithm, 'reason' => 'Asymmetric verification not yet implemented'] + ); + } + // Verify HMAC signature. if (isset(self::HMAC_MAP[$algorithm]) === false) { throw new AuthenticationException( @@ -306,8 +342,16 @@ protected function authorizeJwt(string $authorization): void protected function authorizeBasic(string $header, array $users=[], array $groups=[]): void { $header = substr(string: $header, offset: strlen(string: 'Basic ')); - $decode = base64_decode(string: $header); - [$username, $password] = explode(separator: ':', string: $decode); + + // Guard against malformed base64 (base64_decode returns false on + // invalid input, which explode() cannot accept in PHP 8). + $decode = base64_decode(string: $header, strict: true); + if ($decode === false || str_contains($decode, ':') === false) { + throw new AuthenticationException(message: 'Invalid username or password', details: []); + } + + // Limit to 2 parts so a password containing ':' is preserved intact. + [$username, $password] = explode(separator: ':', string: $decode, limit: 2); $user = $this->userManager->checkPassword($username, $password); diff --git a/lib/Service/AvgComplianceService.php b/lib/Service/AvgComplianceService.php index 28db8c42ab..d0ad50edef 100644 --- a/lib/Service/AvgComplianceService.php +++ b/lib/Service/AvgComplianceService.php @@ -20,9 +20,9 @@ * @category Service * @package OCA\OpenRegister\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -31,9 +31,11 @@ namespace OCA\OpenRegister\Service; +use OCA\OpenRegister\Db\ProcessingLogMapper; use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\VerwerkingsactiviteitMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -54,20 +56,41 @@ class AvgComplianceService */ public const ANNOTATION_KEY = 'x-openregister-processing-activity'; + /** + * Declarative processing dialect key — the new + * `x-openregister-processing` annotation. A schema/register carrying + * an `attribution` block here is also considered annotated. + * + * @var string + */ + public const DIALECT_KEY = 'x-openregister-processing'; + + /** + * Code of the per-organisation flagged fallback activity whose entry + * count is the platform's "unclassified processing" compliance gap. + * + * @var string + */ + public const FALLBACK_CODE = 'niet-geclassificeerde-verwerking'; + /** * Constructor. * - * @param IDBConnection $db DB for the GdprEntity join. - * @param SchemaMapper $schemaMapper Schema lookup. - * @param RegisterMapper $registerMapper Register lookup (annotation - * fallback). - * @param LoggerInterface $logger Logger. + * @param IDBConnection $db DB for the GdprEntity join. + * @param SchemaMapper $schemaMapper Schema lookup. + * @param RegisterMapper $registerMapper Register lookup (annotation + * fallback). + * @param LoggerInterface $logger Logger. + * @param ProcessingLogMapper|null $logMapper Read-log mapper (fallback-gap count). + * @param VerwerkingsactiviteitMapper|null $vrwMapper Activity resolver (fallback uuid). */ public function __construct( private readonly IDBConnection $db, private readonly SchemaMapper $schemaMapper, private readonly RegisterMapper $registerMapper, private readonly LoggerInterface $logger, + private readonly ?ProcessingLogMapper $logMapper=null, + private readonly ?VerwerkingsactiviteitMapper $vrwMapper=null, ) { }//end __construct() @@ -157,7 +180,8 @@ public function findUnannotatedSchemasWithPii(): array */ public function runAllChecks(): array { - $unannotated = $this->findUnannotatedSchemasWithPii(); + $unannotated = $this->findUnannotatedSchemasWithPii(); + $unclassifiedReads = $this->countUnclassifiedProcessing(); return [ 'generated' => date('c'), 'issues' => [ @@ -165,11 +189,49 @@ public function runAllChecks(): array ], 'totals' => [ 'unannotatedSchemasWithPii' => count($unannotated), + 'unclassifiedProcessing' => $unclassifiedReads, ], ]; }//end runAllChecks() + /** + * Count processing-log entries attributed to the flagged fallback + * activity (`niet-geclassificeerde-verwerking`). + * + * This is the platform's visible "unclassified processing" gap + * (OR-PA-4): reads/exports on opted-in schemas whose attribution did + * not resolve and were absorbed by the fallback rather than dropped. + * Returns 0 when the read-log machinery is unavailable (fail-soft). + * + * @return int Number of fallback-attributed processing-log entries. + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + public function countUnclassifiedProcessing(): int + { + if ($this->logMapper === null || $this->vrwMapper === null) { + return 0; + } + + try { + $fallback = $this->vrwMapper->findByCode(code: self::FALLBACK_CODE); + if ($fallback === null) { + return 0; + } + + $counts = $this->logMapper->countByActivity(); + return (int) ($counts[(string) $fallback->getUuid()] ?? 0); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[AVG compliance] Failed to count unclassified processing', + context: ['error' => $e->getMessage()] + ); + return 0; + } + + }//end countUnclassifiedProcessing() + /** * Aggregate `oc_openregister_entities` × `oc_openregister_entity_relations` * by (register_id, schema_id), returning the count of distinct PII @@ -225,8 +287,7 @@ private function registerHasAnnotation(string $registerId): bool return false; } - $value = $config[self::ANNOTATION_KEY] ?? null; - return is_string($value) === true && $value !== ''; + return $this->configHasAttribution(config: $config); }//end registerHasAnnotation() @@ -256,11 +317,42 @@ private function schemaHasAnnotation(string $schemaId): bool return false; } - $value = $config[self::ANNOTATION_KEY] ?? null; - return is_string($value) === true && $value !== ''; + return $this->configHasAttribution(config: $config); }//end schemaHasAnnotation() + /** + * Whether a configuration array satisfies the attribution + * requirement via either annotation form. + * + * Accepts the legacy single-string `x-openregister-processing-activity` + * OR the new `x-openregister-processing` dialect carrying an + * `attribution.default` reference. + * + * @param array $config Configuration column. + * + * @return bool + */ + private function configHasAttribution(array $config): bool + { + $legacy = $config[self::ANNOTATION_KEY] ?? null; + if (is_string($legacy) === true && $legacy !== '') { + return true; + } + + $dialect = $config[self::DIALECT_KEY] ?? null; + if (is_array($dialect) === true) { + $attribution = ($dialect['attribution'] ?? null); + if (is_array($attribution) === true) { + $default = ($attribution['default'] ?? null); + return is_string($default) === true && $default !== ''; + } + } + + return false; + + }//end configHasAttribution() + /** * Best-effort title lookup for an issue row. Returns empty string on miss. * diff --git a/lib/Service/AvgRetentionService.php b/lib/Service/AvgRetentionService.php index ae4b3bf7ec..3d809dbb97 100644 --- a/lib/Service/AvgRetentionService.php +++ b/lib/Service/AvgRetentionService.php @@ -26,9 +26,9 @@ * @category Service * @package OCA\OpenRegister\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -62,7 +62,7 @@ class AvgRetentionService * @param MagicMapper $objectMapper Object loader. * @param LoggerInterface $logger Logger. * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ public function __construct( private readonly IDBConnection $db, @@ -102,7 +102,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ public function runRetentionPass(bool $dryRun=false): array { @@ -146,7 +146,7 @@ public function runRetentionPass(bool $dryRun=false): array * * @return array|null Per-activity result or null skip. * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ private function processActivity(Verwerkingsactiviteit $activity, DateTime $now, bool $dryRun): ?array { @@ -203,7 +203,7 @@ private function processActivity(Verwerkingsactiviteit $activity, DateTime $now, * * @return DateTime|null Cutoff timestamp or null on parse error. * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ private function computeCutoff(DateTime $now, string $duration): ?DateTime { @@ -231,7 +231,7 @@ private function computeCutoff(DateTime $now, string $duration): ?DateTime * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ private function findOverdueObjectsForActivity(string $activityUuid, DateTime $cutoff): array { @@ -306,7 +306,7 @@ private function findOverdueObjectsForActivity(string $activityUuid, DateTime $c * * @return int * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ private function erasePastRetention(array $candidates, Verwerkingsactiviteit $activity): int { @@ -351,7 +351,7 @@ private function erasePastRetention(array $candidates, Verwerkingsactiviteit $ac * * @return ObjectEntity|null * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-1 + * @spec openspec/specs/retention-management/spec.md */ private function loadCandidate(array $candidate): ?ObjectEntity { diff --git a/lib/Service/BulkTranslationService.php b/lib/Service/BulkTranslationService.php index 51ede0f9ed..c0c5cff026 100644 --- a/lib/Service/BulkTranslationService.php +++ b/lib/Service/BulkTranslationService.php @@ -86,7 +86,7 @@ public function __construct( * * @return array{translated: array, skipped: array} * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-7 + * @spec openspec/specs/register-i18n/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) diff --git a/lib/Service/Calculation/AggregateReferenceResolver.php b/lib/Service/Calculation/AggregateReferenceResolver.php new file mode 100644 index 0000000000..7e5289d6e8 --- /dev/null +++ b/lib/Service/Calculation/AggregateReferenceResolver.php @@ -0,0 +1,320 @@ +` so JSON-AST calculations can read it via + * `{ "prop": "@aggregate." }` (or `.` for grouped) — exactly + * mirroring `@self` and `@ref` (Extension 1, ReferenceResolver). + * + * Resolution uses AggregationRunner::runAdhocByRef(), which gates on the + * schema's `list` permission and applies the multi-tenancy predicate under the + * saving user's session, so an aggregate never folds over rows the saver + * cannot read and never leaks cross-tenant data. Any failure injects null and + * is logged; the save is never failed. + * + * The CalculationEvaluator stays pure: all I/O lives here. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Calculation + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Calculation; + +use DateTimeImmutable; +use OCA\OpenRegister\Service\Aggregation\AggregationQuery; +use OCA\OpenRegister\Service\Aggregation\AggregationRunner; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Resolves `x-openregister-aggregate-refs` into an `@aggregate` payload map. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-1 + */ +class AggregateReferenceResolver +{ + /** + * Wire the aggregation runner used to fold referenced objects and the logger. + * + * @param AggregationRunner $aggregationRunner Ad-hoc aggregation runner (RBAC + tenant scoped). + * @param LoggerInterface $logger PSR logger for unresolved-aggregate warnings. + * + * @return void + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-1 + */ + public function __construct( + private readonly AggregationRunner $aggregationRunner, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Build the `@aggregate` map for an object's declared aggregate-references. + * + * Each declared aggregate-reference resolves to a scalar (ungrouped) or a + * `{stringKey: value}` map (grouped), or null on failure. The result is + * intended to be injected into the evaluation payload as + * `$payload['@aggregate']` so calculations can read `@aggregate.` + * (or `@aggregate..`). + * + * @param array $payload Object data WITH `@self` already injected. + * @param array $aggregates The `x-openregister-aggregate-refs` map. + * @param string|int|null $registerRef Saving object's register (slug/uuid/id) — the default + * aggregation context when a spec omits its own `register`. + * + * @return array Map of aggregate-reference name to resolved value (or null). + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-1 + */ + public function resolveAll(array $payload, array $aggregates, mixed $registerRef=null): array + { + $result = []; + foreach ($aggregates as $name => $spec) { + if ((string) $name === '' || is_array($spec) === false) { + continue; + } + + $result[$name] = $this->resolveOne(payload: $payload, spec: $spec, registerRef: $registerRef); + } + + return $result; + }//end resolveAll() + + /** + * Resolve a single aggregate-reference spec to a scalar / grouped map, or null. + * + * @param array $payload Object data with `@self` injected. + * @param array $spec Single aggregate-reference declaration. + * @param string|int|null $registerRef Saving object's register ref (default context). + * + * @return mixed The scalar value, a `{stringKey: value}` grouped map, or null. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.StaticAccess) AggregationQuery::create() is the engine's + * fail-fast static factory (mirrors AggregationRunner's own usage); no instance + * alternative exists. + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-1 + */ + private function resolveOne(array $payload, array $spec, mixed $registerRef): mixed + { + $schema = (string) ($spec['schema'] ?? ''); + $metric = (string) ($spec['metric'] ?? ''); + if ($schema === '' || $metric === '') { + return null; + } + + // Allow the aggregate-reference to override the register; default to + // the saving object's register so the fold stays within the same dataset. + $aggRegister = (string) ($spec['register'] ?? $registerRef ?? ''); + if ($aggRegister === '') { + return null; + } + + // Parameterise the filter map by the saving object: literals pass + // through, `@self.` tokens resolve against the payload. This + // keeps the pure evaluator out of the resolution path. + $filters = []; + $rawFilters = ($spec['filters'] ?? []); + if (is_array($rawFilters) === true) { + foreach ($rawFilters as $target => $criterion) { + $filters[(string) $target] = $this->resolveToken(payload: $payload, token: $criterion); + } + } + + $field = ($spec['field'] ?? null); + if (is_string($field) === false) { + $field = null; + } + + $groupBy = ($spec['groupBy'] ?? null); + if (is_array($groupBy) === false) { + $groupBy = null; + } + + try { + $query = AggregationQuery::create( + metric: $metric, + field: $field, + filter: $filters, + groupBy: $groupBy + ); + $result = $this->aggregationRunner->runAdhocByRef( + registerRef: $aggRegister, + schemaRef: $schema, + query: $query + ); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Aggregate-reference resolution failed for schema "%s": %s', $schema, $e->getMessage()) + ); + return null; + }//end try + + return $this->envelopeToValue(result: $result); + }//end resolveOne() + + /** + * Map an AggregationRunner result envelope to the injected value. + * + * A scalar envelope (`{value: …}`) injects the scalar directly. A grouped + * envelope (`{groups: [{key, value}, …]}`) injects a `{stringKey: value}` + * map so a calculation reads `@aggregate..`. + * + * @param array $result The runner result envelope. + * + * @return mixed The scalar value, a grouped map, or null. + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-1 + */ + private function envelopeToValue(array $result): mixed + { + if (array_key_exists('groups', $result) === true && is_array($result['groups']) === true) { + $map = []; + foreach ($result['groups'] as $group) { + if (is_array($group) === false || array_key_exists('key', $group) === false) { + continue; + } + + $key = $group['key']; + $map[(string) $key] = ($group['value'] ?? null); + } + + return $map; + } + + if (array_key_exists('value', $result) === true) { + return $result['value']; + } + + return null; + }//end envelopeToValue() + + /** + * Resolve a criterion token to a concrete value. + * + * Supported tokens (kept deliberately small to avoid coupling to the pure + * CalculationEvaluator), mirroring ReferenceResolver: + * - A literal scalar (passed through unchanged). + * - A `@self.` / `@ref`-prefixed dotted-path string, resolved against the payload. + * - A single-key AST node `{ "year": }`. + * + * @param array $payload Object data with `@self` injected. + * @param mixed $token The criterion token. + * + * @return mixed The resolved value. + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-1 + */ + private function resolveToken(array $payload, mixed $token): mixed + { + if (is_string($token) === true) { + if (str_starts_with($token, '@self.') === true || str_starts_with($token, '@ref.') === true) { + return $this->readPath(payload: $payload, path: $token); + } + + return $token; + } + + if (is_array($token) === true && count($token) === 1) { + $op = (string) array_key_first($token); + if ($op === 'year') { + return $this->yearToken(payload: $payload, arg: $token[$op]); + } + } + + return $token; + }//end resolveToken() + + /** + * Resolve a `{ "year": }` criterion to a four-digit year integer. + * + * @param array $payload Object data with `@self` injected. + * @param mixed $arg The inner token (typically a `@self.` reference). + * + * @return int|null The year, or null when the inner token is not a parseable date. + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-1 + */ + private function yearToken(array $payload, mixed $arg): ?int + { + $raw = $this->resolveToken(payload: $payload, token: $arg); + if (is_string($raw) === false || $raw === '') { + return null; + } + + try { + return (int) (new DateTimeImmutable($raw))->format('Y'); + } catch (Throwable) { + return null; + } + }//end yearToken() + + /** + * Read a dotted path (e.g. `@self.resourceId`) from the payload. + * + * `@self.` may address either the injected `@self` system metadata + * (id/created/…) OR one of the object's OWN data fields; the listener + * injects user data at the top level, so when `@self.` is not a + * system field fall back to the top-level field. This mirrors + * ReferenceResolver so aggregate filters parameterise by the saving object + * exactly as the schema author expects. + * + * @param array $payload Object data. + * @param string $path Dotted path. + * + * @return mixed The resolved value, or null when the path is missing. + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-1 + */ + private function readPath(array $payload, string $path): mixed + { + $parts = explode('.', $path); + + if ($parts[0] === '@self' && count($parts) === 2) { + $field = $parts[1]; + $self = ($payload['@self'] ?? []); + if (is_array($self) === true && array_key_exists($field, $self) === true) { + return $self[$field]; + } + + return ($payload[$field] ?? null); + } + + $current = $payload; + foreach ($parts as $part) { + if (is_array($current) === false || array_key_exists($part, $current) === false) { + return null; + } + + $current = $current[$part]; + } + + return $current; + }//end readPath() +}//end class diff --git a/lib/Service/Calculation/CalculationAnnotationValidator.php b/lib/Service/Calculation/CalculationAnnotationValidator.php index 40e9a8d12d..bbcaeab6f9 100644 --- a/lib/Service/Calculation/CalculationAnnotationValidator.php +++ b/lib/Service/Calculation/CalculationAnnotationValidator.php @@ -36,8 +36,16 @@ * * Cross-calculation: * - Cycle detection across {prop:calcA, prop:calcB} dependency graph. + * - `x-openregister-references` shape validation + `@ref.` token recognition. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) The validator mirrors the evaluator's + * operator vocabulary plus three reference families (@ref / @self / plain) plus the + * references-block shape checks; each branch is a distinct validation rule that cannot be + * collapsed without losing a specific, user-facing error message. Splitting into sub-validators + * would require a rule registry that is out of scope for this single-responsibility class. * * @spec openspec/changes/retrofit-2026-05-24-b-svc-compute-profile-org/tasks.md#task-2 + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-3 */ final class CalculationAnnotationValidator { @@ -73,8 +81,23 @@ final class CalculationAnnotationValidator 'diffDays', 'formatDate', 'dateDiff', + 'dateAdd', + 'sequence', + 'max', + 'min', + 'coalesce', + 'abs', + 'round', + 'year', + 'monthsElapsed', + 'sha256', ]; + /** + * Allowed `metric` values for an aggregate-reference declaration. + */ + private const VALID_AGGREGATE_METRICS = ['count', 'sum', 'avg', 'min', 'max']; + /** * Allowed `type` values for a calculation declaration. */ @@ -89,6 +112,11 @@ final class CalculationAnnotationValidator * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) The method collects three reference + * families (calc names, x-openregister-references, x-openregister-aggregate-refs), runs + * the per-calc shape + expression walk, and the cycle check in one pass; the steps share + * the accumulated name/dep/error state, so splitting them would thread several mutable + * accumulators through helpers for no readability gain. * * @spec openspec/changes/retrofit-2026-05-24-b-svc-compute-profile-org/tasks.md#task-2 */ @@ -117,9 +145,31 @@ public function validate(array $schema): array $calcNames = array_keys($calcs); $allRefs = array_merge($propKeys, $calcNames); + // Declared cross-object references (`x-openregister-references`) make + // `@ref..` prop tokens valid. Collect their names so the + // walker accepts them, and validate the references block shape. + $references = ($schema['x-openregister-references'] ?? []); + $referenceKeys = []; + if (is_array($references) === true) { + $referenceKeys = array_keys($references); + } + + // Declared aggregate-references (`x-openregister-aggregate-refs`) make + // `@aggregate.` / `@aggregate..` prop tokens valid. + // Collect their names so the walker accepts them, and validate the + // aggregate-refs block shape. + $aggregateRefs = ($schema['x-openregister-aggregate-refs'] ?? []); + $aggregateRefKeys = []; + if (is_array($aggregateRefs) === true) { + $aggregateRefKeys = array_keys($aggregateRefs); + } + $errors = []; $deps = []; + $this->validateReferences(references: $references, propKeys: $propKeys, errors: $errors); + $this->validateAggregateRefs(aggregateRefs: $aggregateRefs, errors: $errors); + foreach ($calcs as $name => $spec) { if (is_string($name) === false || $name === '') { $errors[] = [ @@ -162,6 +212,8 @@ public function validate(array $schema): array expr: $spec['expression'], owner: $name, allRefs: $allRefs, + referenceKeys: $referenceKeys, + aggregateRefKeys: $aggregateRefKeys, errors: $errors, deps: $deps[$name] ); @@ -181,19 +233,28 @@ public function validate(array $schema): array /** * Recursively walk an expression AST collecting errors and dependencies. * - * @param mixed $expr Sub-expression to walk. - * @param string $owner Name of the calc currently being walked. - * @param array $allRefs Available property + calc names. - * @param array $errors Mutable error accumulator. - * @param array $deps Mutable list of calc deps for the current calc. + * @param mixed $expr Sub-expression to walk. + * @param string $owner Name of the calc currently being walked. + * @param array $allRefs Available property + calc names. + * @param array $referenceKeys Declared `x-openregister-references` names. + * @param array $aggregateRefKeys Declared `x-openregister-aggregate-refs` names. + * @param array $errors Mutable error accumulator. + * @param array $deps Mutable list of calc deps for the current calc. * * @return void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - private function walk(mixed $expr, string $owner, array $allRefs, array &$errors, array &$deps): void - { + private function walk( + mixed $expr, + string $owner, + array $allRefs, + array $referenceKeys, + array $aggregateRefKeys, + array &$errors, + array &$deps + ): void { if (is_array($expr) === false) { return; // Bare scalar literal. @@ -218,78 +279,194 @@ private function walk(mixed $expr, string $owner, array $allRefs, array &$errors $args = $expr[$op]; if ($op === 'prop') { - $name = ''; - if (is_string($args) === true) { - $name = $args; - } else if (is_array($args) === true) { - $name = (string) ($args[0] ?? ''); - } + $this->walkProp( + args: $args, + owner: $owner, + allRefs: $allRefs, + referenceKeys: $referenceKeys, + aggregateRefKeys: $aggregateRefKeys, + errors: $errors, + deps: $deps + ); + return; + } + + // DateDiff uses a named-key dict {from, to, unit} rather than a positional array. + if ($op === 'dateDiff') { + $this->walkDateDiff( + args: $args, + owner: $owner, + allRefs: $allRefs, + referenceKeys: $referenceKeys, + aggregateRefKeys: $aggregateRefKeys, + errors: $errors, + deps: $deps + ); + return; + } + + if (is_array($args) === false) { + $this->walk( + expr: $args, + owner: $owner, + allRefs: $allRefs, + referenceKeys: $referenceKeys, + aggregateRefKeys: $aggregateRefKeys, + errors: $errors, + deps: $deps + ); + return; + } + + foreach ($args as $sub) { + $this->walk( + expr: $sub, + owner: $owner, + allRefs: $allRefs, + referenceKeys: $referenceKeys, + aggregateRefKeys: $aggregateRefKeys, + errors: $errors, + deps: $deps + ); + } + }//end walk() + + /** + * Validate a `prop` reference token and record calc-to-calc dependencies. + * + * Recognises four families: `@ref..` (cross-object + * reference injected by the listener), `@aggregate.` (folded + * aggregate injected by the listener), `@self.` (object metadata + * injected by the listener), and plain property / sibling-calculation names. + * + * @param mixed $args The prop operator's argument. + * @param string $owner Name of the calc currently being walked. + * @param array $allRefs Available property + calc names. + * @param array $referenceKeys Declared `x-openregister-references` names. + * @param array $aggregateRefKeys Declared `x-openregister-aggregate-refs` names. + * @param array $errors Mutable error accumulator. + * @param array $deps Mutable list of calc deps for the current calc. + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) The method recognises four prop-token + * families (@ref, @aggregate, @self, plain), each with its own validity check and + * user-facing error message; the families are flat, mutually-exclusive early returns, + * so extracting each into a helper would only add indirection without reducing the rules. + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-3 + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-4 + */ + private function walkProp( + mixed $args, + string $owner, + array $allRefs, + array $referenceKeys, + array $aggregateRefKeys, + array &$errors, + array &$deps + ): void { + $name = ''; + if (is_string($args) === true) { + $name = $args; + } else if (is_array($args) === true) { + $name = (string) ($args[0] ?? ''); + } + + if ($name === '') { + $errors[] = [ + 'code' => 'calculation-prop-unknown', + 'message' => sprintf( + 'Calculation "%s": prop "%s" is not a property or calculation.', + $owner, + $name + ), + ]; + return; + } - if ($name === '') { + // `@ref..` is allowed — the listener + // pre-resolves declared references and injects them at evaluation + // time. The reference name (the first dotted segment after @ref.) + // must be declared in `x-openregister-references`. No dependency + // tracking for @ref refs (they read other objects, not calcs). + if (str_starts_with($name, '@ref.') === true) { + $refName = explode('.', substr($name, 5))[0] ?? ''; + if (in_array($refName, $referenceKeys, true) === false) { $errors[] = [ - 'code' => 'calculation-prop-unknown', + 'code' => 'calculation-ref-unknown', 'message' => sprintf( - 'Calculation "%s": prop "%s" is not a property or calculation.', + 'Calculation "%s": @ref.%s is not a declared reference. Declared: %s.', $owner, - $name + $refName, + implode(', ', $referenceKeys) ), ]; - return; } - // `@self.` is always allowed — the listener - // injects @self metadata at evaluation time. No dependency - // tracking for @self refs since they don't participate in - // the calculation cycle graph. - if (str_starts_with($name, '@self.') === true) { - $sysField = substr($name, 6); - $allowed = ['id', 'uuid', 'register', 'schema', 'owner', 'created', 'updated']; - if (in_array($sysField, $allowed, true) === false) { - $errors[] = [ - 'code' => 'calculation-self-unknown', - 'message' => sprintf( - 'Calculation "%s": @self.%s is not a known system field. Allowed: %s.', - $owner, - $sysField, - implode(', ', $allowed) - ), - ]; - } - - return; - } + return; + } - if (in_array($name, $allRefs, true) === false) { + // `@aggregate.[.]` is allowed — the + // listener pre-resolves declared aggregate-references and injects them + // at evaluation time. The aggregate name (the first dotted segment + // after @aggregate.) must be declared in `x-openregister-aggregate-refs`. + // No dependency tracking (aggregates fold other objects, not calcs). + if (str_starts_with($name, '@aggregate.') === true) { + $aggName = explode('.', substr($name, 11))[0] ?? ''; + if (in_array($aggName, $aggregateRefKeys, true) === false) { $errors[] = [ - 'code' => 'calculation-prop-unknown', + 'code' => 'calculation-aggregate-unknown', 'message' => sprintf( - 'Calculation "%s": prop "%s" is not a property or calculation.', + 'Calculation "%s": @aggregate.%s is not a declared aggregate-reference. Declared: %s.', $owner, - $name + $aggName, + implode(', ', $aggregateRefKeys) ), ]; - return; } - $deps[] = $name; return; - }//end if + } + + // `@self.` is always allowed — the listener + // injects @self metadata at evaluation time. No dependency + // tracking for @self refs since they don't participate in + // the calculation cycle graph. + if (str_starts_with($name, '@self.') === true) { + $sysField = substr($name, 6); + $allowed = ['id', 'uuid', 'register', 'schema', 'owner', 'created', 'updated']; + if (in_array($sysField, $allowed, true) === false) { + $errors[] = [ + 'code' => 'calculation-self-unknown', + 'message' => sprintf( + 'Calculation "%s": @self.%s is not a known system field. Allowed: %s.', + $owner, + $sysField, + implode(', ', $allowed) + ), + ]; + } - // DateDiff uses a named-key dict {from, to, unit} rather than a positional array. - if ($op === 'dateDiff') { - $this->walkDateDiff(args: $args, owner: $owner, allRefs: $allRefs, errors: $errors, deps: $deps); return; } - if (is_array($args) === false) { - $this->walk(expr: $args, owner: $owner, allRefs: $allRefs, errors: $errors, deps: $deps); + if (in_array($name, $allRefs, true) === false) { + $errors[] = [ + 'code' => 'calculation-prop-unknown', + 'message' => sprintf( + 'Calculation "%s": prop "%s" is not a property or calculation.', + $owner, + $name + ), + ]; return; } - foreach ($args as $sub) { - $this->walk(expr: $sub, owner: $owner, allRefs: $allRefs, errors: $errors, deps: $deps); - } - }//end walk() + $deps[] = $name; + }//end walkProp() /** * Validate a `dateDiff` operator's named-key argument dict. @@ -298,11 +475,13 @@ private function walk(mixed $expr, string $owner, array $allRefs, array &$errors * (scalar literal or nested expression). The `unit` value, when a bare * string literal, is additionally checked against the allowed list. * - * @param mixed $args The dateDiff argument value. - * @param string $owner Name of the calc currently being walked. - * @param array $allRefs Available property + calc names. - * @param array $errors Mutable error accumulator. - * @param array $deps Mutable list of calc deps for the current calc. + * @param mixed $args The dateDiff argument value. + * @param string $owner Name of the calc currently being walked. + * @param array $allRefs Available property + calc names. + * @param array $referenceKeys Declared `x-openregister-references` names. + * @param array $aggregateRefKeys Declared `x-openregister-aggregate-refs` names. + * @param array $errors Mutable error accumulator. + * @param array $deps Mutable list of calc deps for the current calc. * * @return void */ @@ -310,6 +489,8 @@ private function walkDateDiff( mixed $args, string $owner, array $allRefs, + array $referenceKeys, + array $aggregateRefKeys, array &$errors, array &$deps ): void { @@ -329,8 +510,24 @@ private function walkDateDiff( } // Walk from and to as sub-expressions so prop refs are validated. - $this->walk(expr: $args['from'], owner: $owner, allRefs: $allRefs, errors: $errors, deps: $deps); - $this->walk(expr: $args['to'], owner: $owner, allRefs: $allRefs, errors: $errors, deps: $deps); + $this->walk( + expr: $args['from'], + owner: $owner, + allRefs: $allRefs, + referenceKeys: $referenceKeys, + aggregateRefKeys: $aggregateRefKeys, + errors: $errors, + deps: $deps + ); + $this->walk( + expr: $args['to'], + owner: $owner, + allRefs: $allRefs, + referenceKeys: $referenceKeys, + aggregateRefKeys: $aggregateRefKeys, + errors: $errors, + deps: $deps + ); // Validate unit when it's a plain string literal (not a nested expression). $unit = $args['unit']; @@ -347,6 +544,153 @@ private function walkDateDiff( } }//end walkDateDiff() + /** + * Validate the optional `x-openregister-references` annotation shape. + * + * Each reference must declare a non-empty `schema` and a `mode` of + * `relatedObject` or `lookup`. A `relatedObject` reference requires a + * `field`; a `lookup` reference requires a `filters` map. + * + * @param mixed $references The references map. + * @param array $propKeys Schema property names (for field checks). + * @param array $errors Mutable error accumulator. + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-3 + */ + private function validateReferences(mixed $references, array $propKeys, array &$errors): void + { + if (is_array($references) === false || count($references) === 0) { + return; + } + + foreach ($references as $name => $spec) { + if (is_string($name) === false || $name === '' || is_array($spec) === false) { + $errors[] = [ + 'code' => 'reference-malformed', + 'message' => sprintf('Reference "%s" must be a named object.', (string) $name), + ]; + continue; + } + + $schema = (string) ($spec['schema'] ?? ''); + if ($schema === '') { + $errors[] = [ + 'code' => 'reference-no-schema', + 'message' => sprintf('Reference "%s" requires a target schema.', $name), + ]; + } + + $mode = (string) ($spec['mode'] ?? ''); + if ($mode !== 'relatedObject' && $mode !== 'lookup') { + $errors[] = [ + 'code' => 'reference-bad-mode', + 'message' => sprintf( + 'Reference "%s" mode must be relatedObject or lookup.', + $name + ), + ]; + continue; + } + + if ($mode === 'relatedObject') { + $field = (string) ($spec['field'] ?? ''); + if ($field === '') { + $errors[] = [ + 'code' => 'reference-no-field', + 'message' => sprintf('Reference "%s" (relatedObject) requires a field.', $name), + ]; + } else if (in_array($field, $propKeys, true) === false) { + $errors[] = [ + 'code' => 'reference-field-unknown', + 'message' => sprintf( + 'Reference "%s": field "%s" is not a schema property.', + $name, + $field + ), + ]; + } + } + + if ($mode === 'lookup' && is_array($spec['filters'] ?? null) === false) { + $errors[] = [ + 'code' => 'reference-no-filters', + 'message' => sprintf('Reference "%s" (lookup) requires a filters map.', $name), + ]; + } + }//end foreach + }//end validateReferences() + + /** + * Validate the optional `x-openregister-aggregate-refs` annotation shape. + * + * Each aggregate-reference must declare a non-empty `schema` and a `metric` + * of count/sum/avg/min/max. A non-`count` metric additionally requires a + * `field` (matching `AggregationQuery::create()`'s own fail-fast contract). + * + * @param mixed $aggregateRefs The aggregate-references map. + * @param array $errors Mutable error accumulator. + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-4 + */ + private function validateAggregateRefs(mixed $aggregateRefs, array &$errors): void + { + if (is_array($aggregateRefs) === false || count($aggregateRefs) === 0) { + return; + } + + foreach ($aggregateRefs as $name => $spec) { + if (is_string($name) === false || $name === '' || is_array($spec) === false) { + $errors[] = [ + 'code' => 'aggregate-ref-malformed', + 'message' => sprintf('Aggregate-reference "%s" must be a named object.', (string) $name), + ]; + continue; + } + + $schema = (string) ($spec['schema'] ?? ''); + if ($schema === '') { + $errors[] = [ + 'code' => 'aggregate-ref-no-schema', + 'message' => sprintf('Aggregate-reference "%s" requires a target schema.', $name), + ]; + } + + $metric = (string) ($spec['metric'] ?? ''); + if (in_array($metric, self::VALID_AGGREGATE_METRICS, true) === false) { + $errors[] = [ + 'code' => 'aggregate-ref-bad-metric', + 'message' => sprintf( + 'Aggregate-reference "%s" metric must be one of [%s].', + $name, + implode(', ', self::VALID_AGGREGATE_METRICS) + ), + ]; + continue; + } + + if ($metric !== 'count' && (string) ($spec['field'] ?? '') === '') { + $errors[] = [ + 'code' => 'aggregate-ref-no-field', + 'message' => sprintf( + 'Aggregate-reference "%s" metric "%s" requires a field.', + $name, + $metric + ), + ]; + } + }//end foreach + }//end validateAggregateRefs() + /** * Find a cycle in the calculation dependency graph using DFS colouring. * diff --git a/lib/Service/Calculation/CalculationEvaluator.php b/lib/Service/Calculation/CalculationEvaluator.php index e1e96ef83b..e536fab4c2 100644 --- a/lib/Service/Calculation/CalculationEvaluator.php +++ b/lib/Service/Calculation/CalculationEvaluator.php @@ -26,6 +26,7 @@ namespace OCA\OpenRegister\Service\Calculation; +use DateInterval; use DateTimeImmutable; use DateTimeInterface; use OCA\OpenRegister\Service\Search\PlaceholderResolver; @@ -54,6 +55,13 @@ * types (arithmetic, logical, date, string, comparison, etc.); each operator requires * its own parse/validate/execute path. Splitting into sub-evaluators would require * a plugin registry and is outside the scope of this service's single-responsibility. + * @SuppressWarnings(PHPMD.ExcessiveClassLength) The same one-handler-per-operator design + * that drives the complexity also drives the line count: each operator carries its own + * fully-documented private handler. A plugin registry to split the file is out of scope. + * @SuppressWarnings(PHPMD.TooManyMethods) Each operator (prop/concat/if/arith/compare/date/ + * string/sha256/…) is a dedicated private handler dispatched from the single `evaluate()` + * match; the count rises one-per-operator by design. Collapsing handlers would lose the + * per-operator validation and error messages, and a plugin registry is out of scope. * * @spec openspec/changes/retrofit-2026-05-24-b-svc-compute-profile-org/tasks.md#task-1 */ @@ -71,11 +79,28 @@ public function __construct( ) { }//end __construct() + /** + * Transient sequence-consumption context for the current outermost evaluate() call. + * + * Set only by the public entry point when a SequenceContext is supplied + * (the create path in CalculationOnSaveListener). It is null on every read + * path so a `{ "sequence": … }` node never burns a number while merely + * rendering an object. Recursive internal evaluate() calls inherit it via + * this property; the outermost call saves/restores the previous value so + * nested evaluation never clears an active context prematurely. + * + * @var SequenceContext|null + */ + private ?SequenceContext $sequenceContext = null; + /** * Evaluate an expression against an object payload. * - * @param array $object The object's stored data. - * @param mixed $expression Expression AST (scalar literal or array). + * @param array $object The object's stored data. + * @param mixed $expression Expression AST (scalar literal or array). + * @param SequenceContext|null $sequenceContext Consume-sequences context; non-null ONLY on + * the create path. When null, `sequence` nodes + * resolve to null instead of reserving a number. * * @return mixed The computed value. * @@ -83,7 +108,37 @@ public function __construct( * * @spec openspec/changes/retrofit-2026-05-24-b-svc-compute-profile-org/tasks.md#task-1 */ - public function evaluate(array $object, mixed $expression): mixed + public function evaluate(array $object, mixed $expression, ?SequenceContext $sequenceContext=null): mixed + { + // Outermost call installs the sequence context (when supplied); nested + // recursive calls pass null and therefore inherit the active context. + $restore = false; + $previous = $this->sequenceContext; + if ($sequenceContext !== null) { + $this->sequenceContext = $sequenceContext; + $restore = true; + } + + try { + return $this->evaluateNode(object: $object, expression: $expression); + } finally { + if ($restore === true) { + $this->sequenceContext = $previous; + } + } + }//end evaluate() + + /** + * Evaluate a single AST node (the recursion target shared by every handler). + * + * @param array $object The object's stored data. + * @param mixed $expression Expression AST (scalar literal or array). + * + * @return mixed The computed value. + * + * @throws EvaluationException When the expression is malformed or references unknown properties/operators. + */ + private function evaluateNode(array $object, mixed $expression): mixed { if (is_array($expression) === false) { // Bare scalar — resolve placeholder strings, otherwise pass through. @@ -115,9 +170,19 @@ public function evaluate(array $object, mixed $expression): mixed 'diffDays' => $this->diffDays(object: $object, args: $args), 'formatDate' => $this->formatDate(object: $object, args: $args), 'dateDiff' => $this->dateDiff(object: $object, args: $args), + 'dateAdd' => $this->dateAdd(object: $object, args: $args), + 'sequence' => $this->sequence(object: $object, args: $args), + 'max' => $this->minMax(object: $object, args: $args, wantMax: true), + 'min' => $this->minMax(object: $object, args: $args, wantMax: false), + 'coalesce' => $this->coalesce(object: $object, args: $args), + 'abs' => $this->absVal(object: $object, args: $args), + 'round' => $this->roundVal(object: $object, args: $args), + 'year' => $this->yearOf(object: $object, args: $args), + 'monthsElapsed' => $this->monthsElapsed(object: $object, args: $args), + 'sha256' => $this->sha256Of(object: $object, args: $args), default => throw new EvaluationException(sprintf('Unknown operator "%s".', $op)), - }; - }//end evaluate() + };//end match + }//end evaluateNode() /** * Resolve a property reference against the object payload. @@ -138,7 +203,7 @@ private function propValue(array $object, mixed $args): mixed $name = ''; if (is_string($args) === true) { $name = $args; - } elseif (is_array($args) === true) { + } else if (is_array($args) === true) { $name = (string) ($args[0] ?? ''); } @@ -177,12 +242,12 @@ private function propValue(array $object, mixed $args): mixed private function concat(array $object, mixed $args): string { if (is_array($args) === false) { - return (string) $this->evaluate(object: $object, expression: $args); + return (string) $this->evaluateNode(object: $object, expression: $args); } $parts = []; foreach ($args as $a) { - $parts[] = (string) ($this->evaluate(object: $object, expression: $a) ?? ''); + $parts[] = (string) ($this->evaluateNode(object: $object, expression: $a) ?? ''); } return implode('', $parts); @@ -206,11 +271,11 @@ private function ifExpr(array $object, mixed $args): mixed $cond = $this->boolEval(object: $object, expr: $args[0]); if ($cond === true) { - return $this->evaluate(object: $object, expression: $args[1]); + return $this->evaluateNode(object: $object, expression: $args[1]); } if (count($args) >= 3) { - return $this->evaluate(object: $object, expression: $args[2]); + return $this->evaluateNode(object: $object, expression: $args[2]); } return null; @@ -226,7 +291,7 @@ private function ifExpr(array $object, mixed $args): mixed */ private function boolEval(array $object, mixed $expr): bool { - $v = $this->evaluate(object: $object, expression: $expr); + $v = $this->evaluateNode(object: $object, expression: $expr); return $v !== null && $v !== false && $v !== 0 && $v !== '0' && $v !== ''; }//end boolEval() @@ -279,7 +344,7 @@ private function arith(array $object, mixed $args, callable $reducer, int|float $acc = $initial; foreach ($args as $a) { - $v = $this->evaluate(object: $object, expression: $a); + $v = $this->evaluateNode(object: $object, expression: $a); if (is_numeric($v) === false) { throw new EvaluationException('Arithmetic operand is not numeric.'); } @@ -306,7 +371,7 @@ private function subOrNeg(array $object, mixed $args): int|float throw new EvaluationException('- requires at least one operand.'); } - $first = $this->evaluate(object: $object, expression: $args[0]); + $first = $this->evaluateNode(object: $object, expression: $args[0]); if (is_numeric($first) === false) { throw new EvaluationException('- first operand not numeric.'); } @@ -318,7 +383,7 @@ private function subOrNeg(array $object, mixed $args): int|float $acc = $first + 0; $argCount = count($args); for ($i = 1; $i < $argCount; $i++) { - $v = $this->evaluate(object: $object, expression: $args[$i]); + $v = $this->evaluateNode(object: $object, expression: $args[$i]); if (is_numeric($v) === false) { throw new EvaluationException('- operand not numeric.'); } @@ -345,8 +410,8 @@ private function divide(array $object, mixed $args): float throw new EvaluationException('/ requires two operands.'); } - $a = $this->evaluate(object: $object, expression: $args[0]); - $b = $this->evaluate(object: $object, expression: $args[1]); + $a = $this->evaluateNode(object: $object, expression: $args[0]); + $b = $this->evaluateNode(object: $object, expression: $args[1]); if (is_numeric($a) === false || is_numeric($b) === false || (float) $b === 0.0) { throw new EvaluationException('/ requires non-zero numeric operands.'); } @@ -370,8 +435,8 @@ private function modulo(array $object, mixed $args): int|float throw new EvaluationException('% requires two operands.'); } - $a = $this->evaluate(object: $object, expression: $args[0]); - $b = $this->evaluate(object: $object, expression: $args[1]); + $a = $this->evaluateNode(object: $object, expression: $args[0]); + $b = $this->evaluateNode(object: $object, expression: $args[1]); if (is_numeric($a) === false || is_numeric($b) === false || (float) $b === 0.0) { throw new EvaluationException('% requires non-zero numeric operands.'); } @@ -400,8 +465,8 @@ private function compare(array $object, mixed $args, string $op): bool throw new EvaluationException(sprintf('%s requires two operands.', $op)); } - $a = $this->normaliseForCompare(v: $this->evaluate(object: $object, expression: $args[0])); - $b = $this->normaliseForCompare(v: $this->evaluate(object: $object, expression: $args[1])); + $a = $this->normaliseForCompare(v: $this->evaluateNode(object: $object, expression: $args[0])); + $b = $this->normaliseForCompare(v: $this->evaluateNode(object: $object, expression: $args[1])); return match ($op) { 'eq' => $a == $b, 'ne' => $a != $b, @@ -467,8 +532,8 @@ private function diffDays(array $object, mixed $args): ?int throw new EvaluationException('diffDays requires (later, earlier).'); } - $later = $this->toDateOrNull(v: $this->evaluate(object: $object, expression: $args[0])); - $earlier = $this->toDateOrNull(v: $this->evaluate(object: $object, expression: $args[1])); + $later = $this->toDateOrNull(v: $this->evaluateNode(object: $object, expression: $args[0])); + $earlier = $this->toDateOrNull(v: $this->evaluateNode(object: $object, expression: $args[1])); if ($later === null || $earlier === null) { return null; } @@ -504,8 +569,8 @@ private function formatDate(array $object, mixed $args): ?string throw new EvaluationException('formatDate requires (date, fmt).'); } - $date = $this->toDateOrNull(v: $this->evaluate(object: $object, expression: $args[0])); - $fmt = (string) $this->evaluate(object: $object, expression: $args[1]); + $date = $this->toDateOrNull(v: $this->evaluateNode(object: $object, expression: $args[0])); + $fmt = (string) $this->evaluateNode(object: $object, expression: $args[1]); if ($date === null) { return null; } @@ -548,7 +613,7 @@ private function formatDate(array $object, mixed $args): ?string * key checks) and null propagation are mandatory guard steps; each extracted helper * (validateDateDiffUnit, resolveDateOperand, applyDateDiffUnit) already carries its * own CC but PHPMD 2.x accumulates their complexity into the calling method's score. - * @SuppressWarnings(PHPMD.NPathComplexity) NPath inflation mirrors the CyclomaticComplexity + * @SuppressWarnings(PHPMD.NPathComplexity) NPath inflation mirrors the CyclomaticComplexity * accumulation issue; the method body itself delegates entirely to extracted helpers. * * @spec openspec/changes/retrofit-2026-05-24-b-svc-compute-profile-org/tasks.md#task-1 @@ -591,7 +656,7 @@ private function dateDiff(array $object, mixed $args): ?int private function validateDateDiffUnit(array $object, mixed $unitExpr): string { $validUnits = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds']; - $unit = (string) $this->evaluate(object: $object, expression: $unitExpr); + $unit = (string) $this->evaluateNode(object: $object, expression: $unitExpr); if (in_array($unit, $validUnits, true) === false) { throw new EvaluationException( sprintf( @@ -617,7 +682,7 @@ private function validateDateDiffUnit(array $object, mixed $unitExpr): string */ private function resolveDateOperand(array $object, mixed $expression): ?DateTimeImmutable { - $raw = $this->evaluate(object: $object, expression: $expression); + $raw = $this->evaluateNode(object: $object, expression: $expression); if ($raw === 'now') { $raw = new DateTimeImmutable('now'); } @@ -673,7 +738,10 @@ private function applyDateDiffUnit(DateTimeImmutable $from, DateTimeImmutable $t private function calendarDiff(DateTimeImmutable $from, DateTimeImmutable $to, string $unit): int { $interval = $from->diff($to); - $sign = ($interval->invert !== 1) ? 1 : -1; + $sign = 1; + if ($interval->invert === 1) { + $sign = -1; + } if ($unit === 'years') { return $sign * $interval->y; @@ -682,6 +750,223 @@ private function calendarDiff(DateTimeImmutable $from, DateTimeImmutable $to, st return $sign * ($interval->y * 12 + $interval->m); }//end calendarDiff() + /** + * Add a duration to a date and return an ISO date / date-time string. + * + * Named-key dict argument (two mutually-exclusive shapes): + * ```json + * { "dateAdd": { "date": , "amount": , "unit": "days"|"weeks"|"months"|"years" } } + * { "dateAdd": { "date": , "duration": } } + * ``` + * + * The `amount`+`unit` shape composes a DateInterval — weeks fold to + * `P{n*7}D`, days to `P{n}D`, months to `P{n}M`, years to `P{n}Y`. The + * `duration` shape parses an ISO-8601 duration string directly + * (`new DateInterval("P30D")`). When the resolved date carries a time + * component the result keeps it (`Y-m-d\TH:i:sP`); a pure date returns + * `Y-m-d`. + * + * Null / empty / unparseable inputs return null (no throw) so the on-save + * listener's swallow-on-error keeps a bad annotation a no-op, never a 500. + * + * @param array $object The object's stored data. + * @param mixed $args Dict with `date` plus either (`amount` + `unit`) or `duration`. + * + * @return string|null The shifted date string, or null when the date/duration is unusable. + * + * @throws EvaluationException When the args dict is missing the required keys. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) The method branches over the two + * mutually-exclusive argument shapes (amount+unit vs ISO-duration) plus null guards + * on the resolved date and interval; each branch is a distinct, required path. + * @SuppressWarnings(PHPMD.NPathComplexity) The sequential null guards (missing keys, + * unparseable date, unbuildable interval) multiply into the NPath count but are each a + * mandatory fail-soft return; extracting them would not remove a single decision. + */ + private function dateAdd(array $object, mixed $args): ?string + { + if (is_array($args) === false || array_key_exists('date', $args) === false) { + throw new EvaluationException('dateAdd requires a "date" key plus either ("amount" + "unit") or "duration".'); + } + + $hasDuration = array_key_exists('duration', $args); + $hasAmountUnit = (array_key_exists('amount', $args) === true && array_key_exists('unit', $args) === true); + if ($hasDuration === false && $hasAmountUnit === false) { + throw new EvaluationException('dateAdd requires either ("amount" + "unit") or "duration".'); + } + + $hadTime = false; + $date = $this->toDateAddDate(object: $object, expr: $args['date'], hadTime: $hadTime); + if ($date === null) { + return null; + } + + $interval = null; + if ($hasDuration === true) { + $interval = $this->intervalFromDuration(value: $this->evaluateNode(object: $object, expression: $args['duration'])); + } + + if ($hasDuration === false) { + $interval = $this->intervalFromAmountUnit( + amount: $this->evaluateNode(object: $object, expression: $args['amount']), + unit: (string) $this->evaluateNode(object: $object, expression: $args['unit']) + ); + } + + if ($interval === null) { + return null; + } + + $shifted = $date->add($interval); + if ($hadTime === true) { + return $shifted->format('Y-m-d\TH:i:sP'); + } + + return $shifted->format('Y-m-d'); + }//end dateAdd() + + /** + * Resolve the dateAdd `date` operand and record whether it carried a time component. + * + * @param array $object The object's stored data. + * @param mixed $expr The date sub-expression. + * @param bool $hadTime Out-param set true when the raw string contained a time part. + * + * @return DateTimeImmutable|null The parsed date, or null when empty/unparseable. + */ + private function toDateAddDate(array $object, mixed $expr, bool &$hadTime): ?DateTimeImmutable + { + $raw = $this->evaluateNode(object: $object, expression: $expr); + if (is_string($raw) === true && preg_match('/\d{1,2}:\d{2}/', $raw) === 1) { + $hadTime = true; + } + + return $this->toDateOrNull(v: $raw); + }//end toDateAddDate() + + /** + * Build a DateInterval from an ISO-8601 duration string (e.g. "P30D", "P1Y6M"). + * + * @param mixed $value The resolved duration value. + * + * @return DateInterval|null The interval, or null when the value is empty/invalid. + * + * @SuppressWarnings(PHPMD.StaticAccess) DateInterval is constructed directly; there is + * no instance-based factory in the PHP standard library. + */ + private function intervalFromDuration(mixed $value): ?DateInterval + { + if (is_string($value) === false || $value === '') { + return null; + } + + try { + return new DateInterval($value); + } catch (Throwable) { + return null; + } + }//end intervalFromDuration() + + /** + * Build a DateInterval from an integer amount and a unit keyword. + * + * Supported units: days, weeks (folded to days), months, years. + * + * @param mixed $amount The resolved amount (must be a non-negative-int-like value). + * @param string $unit One of days, weeks, months, years. + * + * @return DateInterval|null The interval, or null when the amount/unit is unusable. + * + * @SuppressWarnings(PHPMD.StaticAccess) DateInterval is constructed directly; there is + * no instance-based factory in the PHP standard library. + */ + private function intervalFromAmountUnit(mixed $amount, string $unit): ?DateInterval + { + if (is_numeric($amount) === false) { + return null; + } + + $n = (int) $amount; + + $iso = match ($unit) { + 'days' => 'P'.$n.'D', + 'weeks' => 'P'.($n * 7).'D', + 'months' => 'P'.$n.'M', + 'years' => 'P'.$n.'Y', + default => null, + }; + + if ($iso === null) { + return null; + } + + try { + return new DateInterval($iso); + } catch (Throwable) { + return null; + } + }//end intervalFromAmountUnit() + + /** + * Reserve and zero-pad the next running number for a declared scope. + * + * Named-key dict argument: + * ```json + * { "sequence": { "scope": "yearly"|"monthly"|"global", "pad": 4 } } + * ``` + * + * The scope key is derived from the current server time: `yearly` → the + * four-digit year, `monthly` → `YYYY-MM`, `global` → `""`. The reserved + * value is zero-padded to `pad` digits (default 4). + * + * CRITICAL: a sequence reserves EXACTLY ONCE, only when materialised on + * create. The reservation runs through the per-save SequenceContext, which + * CalculationOnSaveListener supplies ONLY on the create path. On every read + * path (and on update) the context is null, so this returns null and never + * burns a number. + * + * @param array $object The object's stored data. + * @param mixed $args Dict with `scope` and optional `pad`. + * + * @return string|null The zero-padded reserved number, or null when no context is active. + * + * @throws EvaluationException When the scope is missing or invalid. + */ + private function sequence(array $object, mixed $args): ?string + { + if (is_array($args) === false || array_key_exists('scope', $args) === false) { + throw new EvaluationException('sequence requires a "scope" key (yearly, monthly or global).'); + } + + $scope = (string) $this->evaluateNode(object: $object, expression: $args['scope']); + $now = new DateTimeImmutable('now'); + $scopeKey = match ($scope) { + 'yearly' => $now->format('Y'), + 'monthly' => $now->format('Y-m'), + 'global' => '', + default => throw new EvaluationException( + sprintf('sequence scope "%s" is invalid. Supported: yearly, monthly, global.', $scope) + ), + }; + + // Read path / update path: no context installed → never consume a number. + if ($this->sequenceContext === null) { + return null; + } + + $pad = 4; + if (array_key_exists('pad', $args) === true) { + $padArg = $this->evaluateNode(object: $object, expression: $args['pad']); + if (is_numeric($padArg) === true) { + $pad = (int) $padArg; + } + } + + $reserved = $this->sequenceContext->reserveNext(scopeKey: $scopeKey); + + return str_pad((string) $reserved, $pad, '0', STR_PAD_LEFT); + }//end sequence() + /** * Coerce a value to DateTimeImmutable when possible. * @@ -713,4 +998,224 @@ private function toDateOrNull(mixed $v): ?DateTimeImmutable return null; }//end toDateOrNull() + + /** + * Resolve a single operand: the first element of a list, or the bare node. + * + * Single-argument ops accept either `[expr]` (engine list convention) or a + * bare AST node / scalar `expr`. + * + * @param mixed $args The op arguments. + * + * @return mixed The operand sub-expression. + */ + private function firstOperand(mixed $args): mixed + { + if (is_array($args) === true && array_is_list($args) === true) { + return ($args[0] ?? null); + } + + return $args; + }//end firstOperand() + + /** + * Max(...) / min(...) over N numeric operands; null operands are skipped. + * + * @param array $object The object's stored data. + * @param mixed $args Operand list. + * @param bool $wantMax True for max, false for min. + * + * @return int|float|null The extreme value, or null when every operand is null. + * + * @throws EvaluationException When args is not a list or an operand is non-numeric. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) The null-skip + non-numeric guard + + * max/min direction branch over N operands sit at the CC threshold; each branch is a + * distinct, required guard and extracting them would not reduce the decision count. + */ + private function minMax(array $object, mixed $args, bool $wantMax): int|float|null + { + if (is_array($args) === false) { + throw new EvaluationException('max/min requires an array of operands.'); + } + + $result = null; + foreach ($args as $a) { + $v = $this->evaluateNode(object: $object, expression: $a); + if ($v === null) { + continue; + } + + if (is_numeric($v) === false) { + throw new EvaluationException('max/min operand is not numeric.'); + } + + $num = ($v + 0); + if ($result === null + || ($wantMax === true && $num > $result) + || ($wantMax === false && $num < $result) + ) { + $result = $num; + } + } + + return $result; + }//end minMax() + + /** + * Coalesce(...): the first non-null operand, or null when all are null. + * + * @param array $object The object's stored data. + * @param mixed $args Operand list. + * + * @return mixed The first non-null value, or null. + * + * @throws EvaluationException When args is not an array. + */ + private function coalesce(array $object, mixed $args): mixed + { + if (is_array($args) === false) { + throw new EvaluationException('coalesce requires an array of operands.'); + } + + foreach ($args as $a) { + $v = $this->evaluateNode(object: $object, expression: $a); + if ($v !== null) { + return $v; + } + } + + return null; + }//end coalesce() + + /** + * Abs(x): absolute value; null passes through. + * + * @param array $object The object's stored data. + * @param mixed $args Single operand (`[expr]` or bare). + * + * @return int|float|null The absolute value, or null. + * + * @throws EvaluationException When the operand is non-numeric. + */ + private function absVal(array $object, mixed $args): int|float|null + { + $v = $this->evaluateNode(object: $object, expression: $this->firstOperand(args: $args)); + if ($v === null) { + return null; + } + + if (is_numeric($v) === false) { + throw new EvaluationException('abs operand is not numeric.'); + } + + return abs($v + 0); + }//end absVal() + + /** + * Round([value, precision?]): round to precision decimals (default 0). + * + * @param array $object The object's stored data. + * @param mixed $args `[value]` or `[value, precision]`. + * + * @return float|null The rounded value, or null when value is null. + * + * @throws EvaluationException When args is not a list, value is non-numeric, or precision is non-integer. + */ + private function roundVal(array $object, mixed $args): ?float + { + if (is_array($args) === false) { + throw new EvaluationException('round requires [value, precision?].'); + } + + $v = $this->evaluateNode(object: $object, expression: ($args[0] ?? null)); + if ($v === null) { + return null; + } + + if (is_numeric($v) === false) { + throw new EvaluationException('round value is not numeric.'); + } + + $precision = 0; + if (array_key_exists(1, $args) === true) { + $precisionArg = $this->evaluateNode(object: $object, expression: $args[1]); + if (is_int($precisionArg) === false) { + throw new EvaluationException('round precision must be an integer.'); + } + + $precision = $precisionArg; + } + + return round(($v + 0), $precision); + }//end roundVal() + + /** + * Year(date): the four-digit year of a date operand. + * + * @param array $object The object's stored data. + * @param mixed $args Single date operand (`[expr]` or bare). + * + * @return int|null The year, or null when the date is unparseable. + */ + private function yearOf(array $object, mixed $args): ?int + { + $date = $this->resolveDateOperand(object: $object, expression: $this->firstOperand(args: $args)); + if ($date === null) { + return null; + } + + return (int) $date->format('Y'); + }//end yearOf() + + /** + * MonthsElapsed([later, earlier]): signed whole calendar months between them. + * + * @param array $object The object's stored data. + * @param mixed $args `[later, earlier]` date operands. + * + * @return int|null The signed whole-month difference, or null when either date is unparseable. + * + * @throws EvaluationException When fewer than two operands are supplied. + */ + private function monthsElapsed(array $object, mixed $args): ?int + { + if (is_array($args) === false || count($args) < 2) { + throw new EvaluationException('monthsElapsed requires (later, earlier).'); + } + + $later = $this->resolveDateOperand(object: $object, expression: $args[0]); + $earlier = $this->resolveDateOperand(object: $object, expression: $args[1]); + if ($later === null || $earlier === null) { + return null; + } + + return $this->calendarDiff(from: $earlier, to: $later, unit: 'months'); + }//end monthsElapsed() + + /** + * Sha256(value): lowercase hex SHA-256 digest of the stringified operand. + * + * Pure and deterministic: the same operand always yields the same 64-char + * hex digest. A `null`-resolving operand returns `null` (rather than the + * hash of an empty string) so the operator is null-safe and never coins a + * fabricated digest for missing data. Mirrors the single-operand shape of + * `abs`/`round`/`year` — accepts both `[expr]` and a bare `expr`. + * + * @param array $object The object's stored data. + * @param mixed $args Single operand (`[expr]` or bare). + * + * @return string|null The 64-character hex SHA-256 digest, or null when the operand is null. + * + * @spec openspec/changes/calc-engine-aggregate-reference/tasks.md#task-3 + */ + private function sha256Of(array $object, mixed $args): ?string + { + $value = $this->evaluateNode(object: $object, expression: $this->firstOperand(args: $args)); + if ($value === null) { + return null; + } + + return hash('sha256', (string) $value); + }//end sha256Of() }//end class diff --git a/lib/Service/Calculation/CalculationPayloadBuilder.php b/lib/Service/Calculation/CalculationPayloadBuilder.php new file mode 100644 index 0000000000..acc540666e --- /dev/null +++ b/lib/Service/Calculation/CalculationPayloadBuilder.php @@ -0,0 +1,167 @@ +` cross-object references + * (`x-openregister-references`), and the pre-resolved `@aggregate.` + * aggregate references (`x-openregister-aggregate-refs`). + * + * Extracted from {@see \OCA\OpenRegister\Listener\CalculationOnSaveListener} + * so the save-time materialisation path and the temporal re-evaluation sweep + * ({@see \OCA\OpenRegister\Service\Calculation\TemporalCalculationSweepService}) + * evaluate calculations against ONE payload shape — a drifted copy would make + * the sweep recompute different values than the write path persists. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Calculation + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Calculation; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; + +/** + * Build the enriched payload materialised calculations evaluate against. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ +class CalculationPayloadBuilder +{ + /** + * Constructor. + * + * @param ReferenceResolver $references Cross-object reference pre-resolver. + * @param AggregateReferenceResolver $aggregates Aggregate-reference pre-resolver. + */ + public function __construct( + private readonly ReferenceResolver $references, + private readonly AggregateReferenceResolver $aggregates, + ) { + + }//end __construct() + + /** + * Enrich an object's data with `@self`, `@ref` and `@aggregate` blocks. + * + * Mirrors the save-time listener exactly: `@self` carries the entity's + * system metadata (calculations reference `@self.created` etc. via the + * evaluator's dotted prop path), `@ref` the pre-resolved declared + * cross-object references, `@aggregate` the pre-resolved aggregate + * references. Resolution is best-effort and never raises. Callers MUST + * strip the synthetic keys before persisting + * ({@see self::stripSyntheticKeys()}). + * + * @param ObjectEntity $object The object whose data to enrich. + * @param Schema $schema The object's schema (declares the reference blocks). + * + * @return array The enriched evaluation payload. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ + public function build(ObjectEntity $object, Schema $schema): array + { + $data = ($object->getObject() ?? []); + + $created = $object->getCreated(); + $updated = $object->getUpdated(); + $createdFormatted = null; + if ($created !== null) { + $createdFormatted = $created->format(\DateTimeInterface::ATOM); + } + + $updatedFormatted = null; + if ($updated !== null) { + $updatedFormatted = $updated->format(\DateTimeInterface::ATOM); + } + + $data['@self'] = [ + 'id' => $object->getUuid(), + 'uuid' => $object->getUuid(), + 'register' => $object->getRegister(), + 'schema' => $object->getSchema(), + 'owner' => $object->getOwner(), + 'created' => $createdFormatted, + 'updated' => $updatedFormatted, + ]; + + $references = $this->configBlock(schema: $schema, key: 'x-openregister-references'); + if ($references !== null) { + $data['@ref'] = $this->references->resolveAll( + payload: $data, + references: $references, + register: $object->getRegister() + ); + } + + $aggregateRefs = $this->configBlock(schema: $schema, key: 'x-openregister-aggregate-refs'); + if ($aggregateRefs !== null) { + $data['@aggregate'] = $this->aggregates->resolveAll( + payload: $data, + aggregates: $aggregateRefs, + registerRef: $object->getRegister() + ); + } + + return $data; + + }//end build() + + /** + * Strip the synthetic evaluation-only keys from a payload. + * + * @param array $data The enriched payload. + * + * @return array The payload without `@self` / `@ref` / `@aggregate`. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ + public function stripSyntheticKeys(array $data): array + { + unset($data['@self'], $data['@ref'], $data['@aggregate']); + + return $data; + + }//end stripSyntheticKeys() + + /** + * Read a non-empty array block off the schema configuration. + * + * @param Schema $schema The schema to inspect. + * @param string $key The configuration key. + * + * @return array|null The block, or null when absent/empty. + */ + private function configBlock(Schema $schema, string $key): ?array + { + $config = ($schema->getConfiguration() ?? []); + $value = ($config[$key] ?? null); + if (is_array($value) === true && count($value) > 0) { + return $value; + } + + return null; + + }//end configBlock() +}//end class diff --git a/lib/Service/Calculation/ReferenceResolver.php b/lib/Service/Calculation/ReferenceResolver.php new file mode 100644 index 0000000000..7c8a0adc47 --- /dev/null +++ b/lib/Service/Calculation/ReferenceResolver.php @@ -0,0 +1,396 @@ +` so JSON-AST + * calculations can read it via `{ "prop": "@ref.." }` — exactly + * mirroring `@self`. + * + * Resolution uses ObjectService READ paths (find / findAll) under the saver's + * existing RBAC + multitenancy scope. Read paths do not dispatch + * Creating/Updating events, so resolving a reference never recursively + * re-triggers the resolved object's own calculations. Any failure injects + * null and is logged; the save is never failed. + * + * The CalculationEvaluator stays pure: all I/O lives here. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Calculation + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Calculation; + +use DateTimeImmutable; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Resolves `x-openregister-references` into a `@ref` payload map. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-1 + */ +class ReferenceResolver +{ + /** + * Wire the object service used to read referenced objects and the logger. + * + * @param ObjectService $objectService Read-side object service (RBAC + tenant scoped). + * @param LoggerInterface $logger PSR logger for unresolved-reference warnings. + * + * @return void + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-1 + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Build the `@ref` map for an object's declared references. + * + * Each declared reference resolves to at most one object's data array (or + * null). The result is intended to be injected into the evaluation payload + * as `$payload['@ref']` so calculations can read `@ref..`. + * + * @param array $payload Object data WITH `@self` already injected. + * @param array $references The `x-openregister-references` map. + * @param Register|string|int|null $register Saving object's register context. + * + * @return array Map of reference name to resolved data (or null). + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-2 + */ + public function resolveAll(array $payload, array $references, mixed $register=null): array + { + $refs = []; + foreach ($references as $name => $spec) { + if ((string) $name === '' || is_array($spec) === false) { + continue; + } + + $refs[$name] = $this->resolveOne(payload: $payload, spec: $spec, register: $register); + } + + return $refs; + }//end resolveAll() + + /** + * Resolve a single reference spec to a data array, or null. + * + * @param array $payload Object data with `@self` injected. + * @param array $spec Single reference declaration. + * @param Register|string|int|null $register Saving object's register context. + * + * @return array|null Resolved object data, or null. + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-3 + */ + private function resolveOne(array $payload, array $spec, mixed $register): ?array + { + $schema = (string) ($spec['schema'] ?? ''); + $mode = (string) ($spec['mode'] ?? ''); + if ($schema === '' || ($mode !== 'relatedObject' && $mode !== 'lookup')) { + return null; + } + + // Allow the reference to override the register; default to the saving + // object's register so the lookup stays within the same dataset. + $refRegister = ($spec['register'] ?? $register); + $entity = null; + + try { + if ($mode === 'relatedObject') { + $entity = $this->resolveRelatedObject( + payload: $payload, + field: (string) ($spec['field'] ?? ''), + schema: $schema, + register: $refRegister + ); + } + + if ($mode === 'lookup') { + $entity = $this->resolveLookup( + payload: $payload, + spec: $spec, + schema: $schema, + register: $refRegister + ); + } + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Reference resolution failed for schema "%s": %s', $schema, $e->getMessage()) + ); + return null; + }//end try + + if ($entity === null) { + return null; + } + + return $this->entityToPayload(entity: $entity); + }//end resolveOne() + + /** + * Resolve a `relatedObject` (FK) reference via ObjectService::find(). + * + * @param array $payload Object data with `@self` injected. + * @param string $field Local field holding the referenced uuid/id. + * @param string $schema Target schema reference. + * @param Register|string|int|null $register Register context. + * + * @return ObjectEntity|null The referenced object, or null. + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-3 + */ + private function resolveRelatedObject(array $payload, string $field, string $schema, mixed $register): ?ObjectEntity + { + if ($field === '') { + return null; + } + + $id = ($payload[$field] ?? null); + if (is_string($id) === false && is_int($id) === false) { + return null; + } + + if ((string) $id === '') { + return null; + } + + // RBAC + multitenancy stay at their default `true` — the lookup runs + // under the saving user's scope and can never leak cross-tenant data. + return $this->objectService->find( + id: $id, + register: $register, + schema: $schema, + _rbac: true, + _multitenancy: true + ); + }//end resolveRelatedObject() + + /** + * Resolve a `lookup` (criteria) reference via ObjectService::findAll(). + * + * Builds the filter map by resolving `@self`/literal/AST tokens in each + * criterion against the payload. When an `effectiveDate` selector is + * present, rows are constrained to those valid as-of the object's date and + * the most-recent row is taken. + * + * @param array $payload Object data with `@self` injected. + * @param array $spec Reference declaration. + * @param string $schema Target schema reference. + * @param Register|string|int|null $register Register context. + * + * @return ObjectEntity|null The most-relevant matching object, or null. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-3 + */ + private function resolveLookup(array $payload, array $spec, string $schema, mixed $register): ?ObjectEntity + { + $criteria = ($spec['filters'] ?? []); + if (is_array($criteria) === false) { + return null; + } + + $filters = [ + 'schema' => $schema, + ]; + if ($register !== null) { + $filters['register'] = $register; + } + + foreach ($criteria as $target => $criterion) { + $filters[(string) $target] = $this->resolveToken(payload: $payload, token: $criterion); + } + + $sort = []; + + // Effective-dating: keep only rows whose effective field is <= (or per + // op) the object's date, newest first. + $effective = ($spec['effectiveDate'] ?? null); + if (is_array($effective) === true && isset($effective['field']) === true) { + $effField = (string) $effective['field']; + $sort[$effField] = 'DESC'; + // The op/value pair is advisory; the DESC sort + first-row pick + // selects the most-recent applicable rate even without a range op, + // and avoids depending on findAll range-filter dialect support. + } + + $config = [ + 'filters' => $filters, + 'limit' => 50, + ]; + if (count($sort) > 0) { + $config['sort'] = $sort; + } + + $results = $this->objectService->findAll($config, _rbac: true, _multitenancy: true); + if (count($results) === 0) { + return null; + } + + $first = $results[array_key_first($results)]; + if ($first instanceof ObjectEntity) { + return $first; + } + + return null; + }//end resolveLookup() + + /** + * Resolve a criterion token to a concrete value. + * + * Supported tokens (kept deliberately small to avoid coupling to the pure + * CalculationEvaluator): + * - A literal scalar (passed through unchanged). + * - A `@self.` / `@ref`-prefixed dotted-path string, resolved against the payload. + * - A single-key AST node `{ "year": }` — only `year` is supported + * here, mirroring the lookup criteria used by rate tables. + * + * @param array $payload Object data with `@self` injected. + * @param mixed $token The criterion token. + * + * @return mixed The resolved value. + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-2 + */ + private function resolveToken(array $payload, mixed $token): mixed + { + if (is_string($token) === true) { + if (str_starts_with($token, '@self.') === true || str_starts_with($token, '@ref.') === true) { + return $this->readPath(payload: $payload, path: $token); + } + + return $token; + } + + if (is_array($token) === true && count($token) === 1) { + $op = (string) array_key_first($token); + if ($op === 'year') { + return $this->yearToken(payload: $payload, arg: $token[$op]); + } + } + + return $token; + }//end resolveToken() + + /** + * Resolve a `{ "year": }` criterion to a four-digit year integer. + * + * @param array $payload Object data with `@self` injected. + * @param mixed $arg The inner token (typically a `@self.` reference). + * + * @return int|null The year, or null when the inner token is not a parseable date. + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-2 + */ + private function yearToken(array $payload, mixed $arg): ?int + { + $raw = $this->resolveToken(payload: $payload, token: $arg); + if (is_string($raw) === false || $raw === '') { + return null; + } + + try { + return (int) (new DateTimeImmutable($raw))->format('Y'); + } catch (Throwable) { + return null; + } + }//end yearToken() + + /** + * Read a dotted path (e.g. `@self.journeyDate`) from the payload. + * + * @param array $payload Object data. + * @param string $path Dotted path. + * + * @return mixed The resolved value, or null when the path is missing. + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-2 + */ + private function readPath(array $payload, string $path): mixed + { + $parts = explode('.', $path); + + // `@self.` may address either the injected `@self` system + // metadata (id/created/…) OR — the common case for lookup criteria — + // one of the object's OWN data fields (e.g. `@self.journeyDate`). The + // listener injects user data at the top level of the payload, not under + // `@self`, so when `@self.` is not a system field fall back to + // the top-level field. This makes the lookup parameterisable by the + // saving object exactly as the schema author expects. + if ($parts[0] === '@self' && count($parts) === 2) { + $field = $parts[1]; + $self = ($payload['@self'] ?? []); + if (is_array($self) === true && array_key_exists($field, $self) === true) { + return $self[$field]; + } + + return ($payload[$field] ?? null); + } + + $current = $payload; + foreach ($parts as $part) { + if (is_array($current) === false || array_key_exists($part, $current) === false) { + return null; + } + + $current = $current[$part]; + } + + return $current; + }//end readPath() + + /** + * Flatten a resolved entity into a data array (with `@self` sub-key). + * + * @param ObjectEntity $entity The resolved object. + * + * @return array The entity's data, including `@self` metadata. + * + * @spec openspec/changes/calc-engine-reference-lookup/tasks.md#task-3 + */ + private function entityToPayload(ObjectEntity $entity): array + { + $data = $entity->getObject(); + + $data['@self'] = [ + 'id' => $entity->getUuid(), + 'uuid' => $entity->getUuid(), + 'register' => $entity->getRegister(), + 'schema' => $entity->getSchema(), + 'owner' => $entity->getOwner(), + ]; + + return $data; + }//end entityToPayload() +}//end class diff --git a/lib/Service/Calculation/SequenceContext.php b/lib/Service/Calculation/SequenceContext.php new file mode 100644 index 0000000000..6782b055f1 --- /dev/null +++ b/lib/Service/Calculation/SequenceContext.php @@ -0,0 +1,73 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Calculation; + +use OCA\OpenRegister\Service\SequenceService; + +/** + * Authorises (and scopes) sequence consumption for a single save. + */ +final class SequenceContext +{ + /** + * Wire the reservation service and the register/schema scope. + * + * @param SequenceService $service Atomic sequence reservation service. + * @param int $registerId The register the sequence is scoped to. + * @param int $schemaId The schema the sequence is scoped to. + * + * @return void + */ + public function __construct( + private readonly SequenceService $service, + private readonly int $registerId, + private readonly int $schemaId + ) { + }//end __construct() + + /** + * Atomically reserve the next number for the given scope key. + * + * @param string $scopeKey The scope discriminator (e.g. "2026", "2026-06" or "" for global). + * + * @return int The reserved, never-reused running number. + */ + public function reserveNext(string $scopeKey): int + { + return $this->service->reserveNext( + registerId: $this->registerId, + schemaId: $this->schemaId, + scopeKey: $scopeKey + ); + }//end reserveNext() +}//end class diff --git a/lib/Service/Calculation/TemporalCalculationSweepService.php b/lib/Service/Calculation/TemporalCalculationSweepService.php new file mode 100644 index 0000000000..a0f1f4d073 --- /dev/null +++ b/lib/Service/Calculation/TemporalCalculationSweepService.php @@ -0,0 +1,441 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Calculation; + +use OCA\OpenRegister\Db\MagicMapper; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; + +/** + * Re-materialise `now`-dependent calculations for untouched objects. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) The sweep composes the + * schema/register/object mappers, the calculation engine trio, and the + * object write path — each a distinct shipped collaborator. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ +class TemporalCalculationSweepService +{ + /** + * Constructor. + * + * @param SchemaMapper $schemaMapper Schema enumeration (system scope). + * @param RegisterMapper $registerMapper Register enumeration for schema→register resolution. + * @param MagicMapper $objectMapper Raw per-table object enumeration. + * @param CalculationEvaluator $evaluator Expression evaluator (exposes `now`). + * @param CalculationPayloadBuilder $payloadBuilder Shared @self/@ref/@aggregate payload prep. + * @param ObjectService $objectService The normal object write path (events + audit). + * @param LoggerInterface $logger Structured logging. + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly RegisterMapper $registerMapper, + private readonly MagicMapper $objectMapper, + private readonly CalculationEvaluator $evaluator, + private readonly CalculationPayloadBuilder $payloadBuilder, + private readonly ObjectService $objectService, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Run one sweep over every schema with `now`-dependent materialised + * calculations. + * + * @return array{schemasScanned: int, temporalSchemas: int, objectsEvaluated: int, objectsRewritten: int, errors: int} Sweep summary. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Scenario: Untouched case crosses the reminder tier) + */ + public function runSweep(): array + { + $summary = [ + 'schemasScanned' => 0, + 'temporalSchemas' => 0, + 'objectsEvaluated' => 0, + 'objectsRewritten' => 0, + 'errors' => 0, + ]; + + $schemas = []; + try { + $schemas = $this->schemaMapper->findAll(_rbac: false, _multitenancy: false); + } catch (\Throwable $e) { + $this->logger->error( + message: '[TemporalCalculationSweep] schema enumeration failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + $summary['errors']++; + return $summary; + } + + $registers = $this->loadRegisters(); + + foreach ($schemas as $schema) { + $summary['schemasScanned']++; + + $calcs = $this->materialisedCalculations(schema: $schema); + if ($calcs === [] || $this->hasTemporalCalculation(calculations: $calcs) === false) { + // Schemas with no `now`-dependent materialised calculation are skipped entirely. + continue; + } + + $register = $this->registerOf(schema: $schema, registers: $registers); + if ($register === null) { + continue; + } + + $summary['temporalSchemas']++; + $this->sweepSchema(register: $register, schema: $schema, calcs: $calcs, summary: $summary); + }//end foreach + + return $summary; + + }//end runSweep() + + /** + * Whether any materialised calculation references the evaluation clock. + * + * Detects both the `{"now": []}` operator anywhere in the expression tree + * and the literal `"now"` argument accepted by `dateDiff`-style + * operators. + * + * @param array $calculations The schema's materialised calculations. + * + * @return bool True when at least one expression is time-dependent. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ + public function hasTemporalCalculation(array $calculations): bool + { + foreach ($calculations as $spec) { + if (is_array($spec) === false) { + continue; + } + + if ($this->expressionReferencesNow(expression: ($spec['expression'] ?? null)) === true) { + return true; + } + } + + return false; + + }//end hasTemporalCalculation() + + /** + * Sweep one register+schema pair: recompute the materialised calculations + * for every object in a non-terminal lifecycle state and rewrite only + * objects whose recomputed values changed. + * + * @param Register $register The owning register. + * @param Schema $schema The temporal schema. + * @param array $calcs Materialised calculations. + * @param array $summary Running summary counters (by reference). + * + * @return void + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Scenario: Tier crossing notifies exactly once) + */ + private function sweepSchema(Register $register, Schema $schema, array $calcs, array &$summary): void + { + try { + $objects = $this->objectMapper->findAllInRegisterSchemaTable(register: $register, schema: $schema); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[TemporalCalculationSweep] object enumeration failed for schema '.(string) $schema->getSlug().': '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + $summary['errors']++; + return; + } + + [$lifecycleField, $terminalStates] = $this->lifecycleTerminals(schema: $schema); + + foreach ($objects as $object) { + $data = ($object->getObject() ?? []); + + // Terminal cases are left alone (spec scenario) — their clock no + // longer matters and rewriting them would churn finalised dossiers. + if ($lifecycleField !== null) { + $state = (string) ($data[$lifecycleField] ?? ''); + if (in_array($state, $terminalStates, true) === true) { + continue; + } + } + + $summary['objectsEvaluated']++; + + try { + if ($this->recomputeChanges(object: $object, schema: $schema, calcs: $calcs) === false) { + // Unchanged recomputation — no write, no duplicate notification. + continue; + } + + // Re-save the object UNCHANGED through the normal write path: + // CalculationOnSaveListener re-materialises the calculations + // (producing the same new values just detected) and the update + // event carries old+new data, so the declared calculatedChange + // rules fire exactly on the crossing. System sweep: RBAC and + // tenancy scoping are bypassed deliberately (no user session); + // the write is still audited through the normal path. + $this->objectService->saveObject( + object: $data, + register: $register, + schema: $schema, + uuid: (string) $object->getUuid(), + _rbac: false, + _multitenancy: false + ); + $summary['objectsRewritten']++; + } catch (\Throwable $e) { + $summary['errors']++; + $this->logger->warning( + message: '[TemporalCalculationSweep] recompute failed for object '.(string) $object->getUuid().': '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + }//end try + }//end foreach + + }//end sweepSchema() + + /** + * Recompute the schema's materialised calculations for an object and + * report whether ANY value differs from the stored one. + * + * Evaluates sequentially in declaration order against the shared payload + * shape (`@self` / `@ref` / `@aggregate` injected), exactly like the + * save-time listener, so a calculation may reference an earlier one. + * + * @param ObjectEntity $object The object to probe. + * @param Schema $schema The schema declaring the calculations. + * @param array $calcs Materialised calculations. + * + * @return bool True when at least one recomputed value changed. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Scenario: Tier crossing notifies exactly once) + */ + private function recomputeChanges(ObjectEntity $object, Schema $schema, array $calcs): bool + { + $payload = $this->payloadBuilder->build(object: $object, schema: $schema); + $changed = false; + + foreach ($calcs as $name => $spec) { + try { + $value = $this->evaluator->evaluate($payload, $spec['expression'] ?? null); + } catch (EvaluationException $e) { + // Mirrors the save-time listener: a failing calculation is + // logged and skipped, never fails the sweep. + $this->logger->warning( + message: sprintf( + '[TemporalCalculationSweep] calculation "%s" failed on %s: %s', + (string) $name, + (string) $object->getUuid(), + $e->getMessage() + ), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + continue; + } + + if ($value instanceof \DateTimeInterface) { + $value = $value->format(DATE_ATOM); + } + + if (($payload[(string) $name] ?? null) !== $value) { + $changed = true; + } + + // Later calculations may reference this one's fresh value. + $payload[(string) $name] = $value; + }//end foreach + + return $changed; + + }//end recomputeChanges() + + /** + * The schema's materialised calculations (empty when none declared). + * + * @param Schema $schema The schema to inspect. + * + * @return array Materialised calculation specs by name. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Time-dependent calculated fields re-evaluate without object writes) + */ + private function materialisedCalculations(Schema $schema): array + { + $config = ($schema->getConfiguration() ?? []); + $calcs = ($config['x-openregister-calculations'] ?? null); + if (is_array($calcs) === false) { + return []; + } + + $materialised = []; + foreach ($calcs as $name => $spec) { + if (is_array($spec) === true && ($spec['materialise'] ?? false) === true) { + $materialised[(string) $name] = $spec; + } + } + + return $materialised; + + }//end materialisedCalculations() + + /** + * Recursively detect a reference to the evaluation clock in an + * expression tree: the `now` operator key or the literal string `"now"` + * (accepted by `dateDiff`'s from/to arguments). + * + * @param mixed $expression The (sub-)expression to scan. + * + * @return bool True when the expression references `now`. + */ + private function expressionReferencesNow(mixed $expression): bool + { + if (is_string($expression) === true) { + return $expression === 'now'; + } + + if (is_array($expression) === false) { + return false; + } + + foreach ($expression as $key => $value) { + if ($key === 'now') { + return true; + } + + if ($this->expressionReferencesNow(expression: $value) === true) { + return true; + } + } + + return false; + + }//end expressionReferencesNow() + + /** + * The schema's lifecycle field + terminal (final) states, or [null, []] + * when the schema declares no usable lifecycle. Schemas without a + * lifecycle sweep ALL their objects (bounded by the unchanged-skip). + * + * @param Schema $schema The schema to inspect. + * + * @return array{0: string|null, 1: array} Lifecycle field + terminal states. + */ + private function lifecycleTerminals(Schema $schema): array + { + $config = ($schema->getConfiguration() ?? []); + $lifecycle = ($config['x-openregister-lifecycle'] ?? null); + if (is_array($lifecycle) === false) { + return [null, []]; + } + + $field = ($lifecycle['field'] ?? ($lifecycle['property'] ?? null)); + if (is_string($field) === false || $field === '') { + return [null, []]; + } + + $final = ($lifecycle['final'] ?? []); + if (is_array($final) === false) { + $final = []; + } + + return [$field, array_values(array_map('strval', $final))]; + + }//end lifecycleTerminals() + + /** + * Load every register once (system scope), keyed for membership lookup. + * + * @return array All registers. + */ + private function loadRegisters(): array + { + try { + return $this->registerMapper->findAll(_rbac: false, _multitenancy: false); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[TemporalCalculationSweep] register enumeration failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return []; + } + + }//end loadRegisters() + + /** + * Find the register a schema belongs to (first register whose schema-id + * list contains it), or null for orphaned schemas. + * + * @param Schema $schema The schema. + * @param array $registers All registers. + * + * @return Register|null The owning register, or null. + */ + private function registerOf(Schema $schema, array $registers): ?Register + { + $schemaId = (int) $schema->getId(); + foreach ($registers as $register) { + foreach (($register->getSchemas() ?? []) as $memberId) { + if ((int) $memberId === $schemaId) { + return $register; + } + } + } + + return null; + + }//end registerOf() +}//end class diff --git a/lib/Service/CalendarEventService.php b/lib/Service/CalendarEventService.php index 27589ea73e..15730825de 100644 --- a/lib/Service/CalendarEventService.php +++ b/lib/Service/CalendarEventService.php @@ -18,7 +18,7 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-webhookeventlistener-must-extract-structured-payloads-from-all-event-types */ declare(strict_types=1); @@ -29,6 +29,7 @@ use Exception; use OCA\DAV\CalDAV\CalDavBackend; use OCP\IConfig; +use OCP\IURLGenerator; use OCP\IUserSession; use Psr\Log\LoggerInterface; use Sabre\VObject\Reader; @@ -92,6 +93,13 @@ class CalendarEventService */ private readonly LoggerInterface $logger; + /** + * URL generator (webroot-aware deep links). + * + * @var IURLGenerator + */ + private readonly IURLGenerator $urlGenerator; + /** * Constructor. * @@ -99,6 +107,7 @@ class CalendarEventService * @param IUserSession $userSession User session * @param IConfig $config NC config (user-value store) * @param LoggerInterface $logger Logger + * @param IURLGenerator $urlGenerator URL generator for deep links * * @return void */ @@ -106,10 +115,12 @@ public function __construct( CalDavBackend $calDavBackend, IUserSession $userSession, IConfig $config, - LoggerInterface $logger + LoggerInterface $logger, + IURLGenerator $urlGenerator ) { $this->calDavBackend = $calDavBackend; $this->userSession = $userSession; + $this->urlGenerator = $urlGenerator; $this->config = $config; $this->logger = $logger; }//end __construct() @@ -123,7 +134,7 @@ public function __construct( * * @throws Exception If no user is logged in or no calendar found * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function getEventsForObject(string $objectUuid): array { @@ -156,6 +167,15 @@ public function getEventsForObject(string $objectUuid): array uri: $calendarObject['uri'] ); if ($eventArray !== null && $eventArray['objectUuid'] === $objectUuid) { + // Deep-link to the specific event in the Calendar app. + $deepLink = $this->buildEventDeepLink( + calendarUri: ($calendar['uri'] ?? null), + objectUri: $calendarObject['uri'] + ); + if ($deepLink !== null) { + $eventArray['url'] = $deepLink; + } + $events[] = $eventArray; } } catch (Exception $e) { @@ -163,12 +183,147 @@ public function getEventsForObject(string $objectUuid): array 'Failed to parse calendar event: '.$e->getMessage(), ['uri' => $calendarObject['uri']] ); - } + }//end try }//end foreach return $events; }//end getEventsForObject() + /** + * Build a webroot-aware deep-link to a specific calendar event. + * + * The Calendar app opens a specific event via its `/edit/{objectId}` route + * (`calendar.view.index` postfix `direct.edit`), where `objectId` is the + * base64 of the object's full DAV path *including* the `/remote.php/dav/` + * prefix — i.e. `{webroot}/remote.php/dav/calendars/{userId}/{calendarUri}/{objectUri}` + * (this matches the `objectId` the Calendar UI itself generates; the app + * does `atob(objectId)` and fetches that DAV href). NC cannot generate the + * postfixed route by name (it yields an empty string), so we resolve the + * webroot-aware app base via `calendar.view.index` and append the + * `edit/{token}` segment ourselves (token URL-encoded so the base64 `/`/`+` + * survive as one path segment). + * + * Returns null (record gets no `url`) when a required part is missing or the + * Calendar app / route is unavailable. + * + * @param string|null $calendarUri The CalDAV calendar URI owning the event. + * @param string|null $objectUri The VEVENT object URI (.ics). + * + * @return string|null The deep-link URL, or null when not resolvable. + */ + private function buildEventDeepLink(?string $calendarUri, ?string $objectUri): ?string + { + $user = $this->userSession->getUser(); + if ($user === null || $calendarUri === null || $calendarUri === '' + || $objectUri === null || $objectUri === '' + ) { + return null; + } + + try { + $base = $this->urlGenerator->linkToRoute('calendar.view.index'); + } catch (\Throwable $e) { + return null; + } + + if ($base === '') { + return null; + } + + // The Calendar app decodes objectId to a DAV href and fetches it, so it + // must carry the (webroot-aware) `/remote.php/dav/` prefix. + $davPath = $this->urlGenerator->getWebroot().'/remote.php/dav/calendars/'.$user->getUID().'/'.$calendarUri.'/'.$objectUri; + $token = rawurlencode(base64_encode($davPath)); + + return rtrim($base, '/').'/edit/'.$token; + }//end buildEventDeepLink() + + /** + * Get all VEVENTs across the acting user's VEVENT-supporting calendars. + * + * Read-only directory listing that mirrors {@see \OCA\OpenRegister\Service\TaskService::getAllUserTasks()} + * for events: it enumerates every calendar the user owns (not just the pinned + * one), skips non-VEVENT objects, and returns the parsed event arrays sorted by + * start date (soonest first, undated last). Backs the `calendar-event-source` + * ObjectSourceProvider. + * + * @param int $limit Maximum number of events to return. + * @param int $offset Number of events to skip. + * + * @return array{results: array>, total: int} Events with total count. + * + * @throws Exception If no user is logged in. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function getAllUserEvents(int $limit=200, int $offset=0): array + { + $user = $this->userSession->getUser(); + if ($user === null) { + throw new Exception('No user logged in'); + } + + $principal = 'principals/users/'.$user->getUID(); + $calendars = $this->calDavBackend->getCalendarsForUser($principal); + + $allEvents = []; + foreach ($calendars as $calendar) { + if ($this->calendarSupportsVevent(calendar: $calendar) === false) { + continue; + } + + $calendarId = $calendar['id']; + $calendarObjects = $this->calDavBackend->getCalendarObjects($calendarId); + + foreach ($calendarObjects as $calendarObject) { + $fullObject = $this->calDavBackend->getCalendarObject($calendarId, $calendarObject['uri']); + if ($fullObject === null || empty($fullObject['calendardata']) === true) { + continue; + } + + $calendarData = $fullObject['calendardata']; + + if (strpos($calendarData, 'VEVENT') === false) { + continue; + } + + try { + $eventArray = $this->veventToArray( + calendarData: $calendarData, + calendarId: (string) $calendarId, + uri: $calendarObject['uri'] + ); + if ($eventArray !== null) { + $allEvents[] = $eventArray; + } + } catch (Exception $e) { + $this->logger->warning( + 'Failed to parse calendar event: '.$e->getMessage(), + ['uri' => $calendarObject['uri']] + ); + } + }//end foreach + }//end foreach + + // Sort by start date (soonest first, undated last). + usort( + array: $allEvents, + callback: function ($a, $b) { + $startA = ($a['dtstart'] ?? '9999-12-31'); + $startB = ($b['dtstart'] ?? '9999-12-31'); + return strcmp($startA, $startB); + } + ); + + $total = count($allEvents); + $results = array_slice($allEvents, $offset, $limit); + + return [ + 'results' => $results, + 'total' => $total, + ]; + }//end getAllUserEvents() + /** * Create a new CalDAV event linked to an OpenRegister object. * @@ -182,7 +337,7 @@ public function getEventsForObject(string $objectUuid): array * * @throws Exception If no user or calendar found * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function createEvent( int $registerId, @@ -265,7 +420,7 @@ public function createEvent( * * @throws Exception If the event is not found * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function linkEvent( int $calendarId, @@ -306,7 +461,7 @@ public function linkEvent( * * @throws Exception If the event is not found * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function unlinkEvent(string $calendarId, string $eventUri): void { @@ -348,7 +503,7 @@ public function unlinkEvent(string $calendarId, string $eventUri): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-1 + * @spec openspec/specs/calendar-integration/spec.md */ public function unlinkEventsForObject(string $objectUuid): void { @@ -599,7 +754,7 @@ private function veventToArray(string $calendarData, string $calendarId, string * * @return array{objectUuid: string|null, registerId: int|null, schemaId: int|null} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/event-driven-architecture/spec.md#requirement-webhookeventlistener-must-extract-structured-payloads-from-all-event-types */ private function extractOpenRegisterProperties(mixed $vevent): array { diff --git a/lib/Service/CalendarLinkService.php b/lib/Service/CalendarLinkService.php index a99b2447da..456510ba77 100644 --- a/lib/Service/CalendarLinkService.php +++ b/lib/Service/CalendarLinkService.php @@ -18,7 +18,7 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/integration-calendar/tasks.md + * @spec openspec/specs/integration-calendar/spec.md */ declare(strict_types=1); @@ -46,10 +46,10 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) CalDAV protocol requires handling multiple * branching cases per operation (object types, URI resolution, legacy X-OR fallback) — the * complexity is inherent to wrapping two distinct CalDAV data sources. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) CalDAV link service must compose CalDavBackend, + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) CalDAV link service must compose CalDavBackend, * CalendarEventService, CalendarLinkMapper, UserSession, and Logger — each serves a distinct * concern and cannot be merged. - * @SuppressWarnings(PHPMD.StaticAccess) Sabre\VObject\Reader::read() is a static factory with + * @SuppressWarnings(PHPMD.StaticAccess) Sabre\VObject\Reader::read() is a static factory with * no injectable alternative in the Sabre VObject library. */ class CalendarLinkService @@ -91,7 +91,7 @@ public function __construct( * * @throws Exception When no user is logged in or the event cannot be located. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function linkEvent( string $objectUuid, @@ -151,7 +151,7 @@ public function linkEvent( * * @throws Exception On creation failure. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function createAndLinkEvent( string $objectUuid, @@ -194,7 +194,12 @@ public function createAndLinkEvent( $link->setSummary((string) ($event['summary'] ?? '')); $link->setDtstart($this->parseEventDateTime(value: $event['dtstart'] ?? null)); $link->setDtend($this->parseEventDateTime(value: $event['dtend'] ?? null)); - $link->setLocation(isset($event['location']) === true ? (string) $event['location'] : null); + $location = null; + if (isset($event['location']) === true) { + $location = (string) $event['location']; + } + + $link->setLocation($location); $link->setLinkedBy($user->getUID()); $link->setLinkedAt(new DateTime()); $link->setTaggedWithXor(true); @@ -237,7 +242,7 @@ private function parseEventDateTime(mixed $value): ?DateTime * * @return void * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function unlinkEvent(string $objectUuid, string $eventUid): void { @@ -314,7 +319,7 @@ public function unlinkEvent(string $objectUuid, string $eventUid): void * * @return array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getLinkedEvents(string $objectUuid): array { @@ -368,10 +373,10 @@ public function getLinkedEvents(string $objectUuid): array * mark the existing entry as 'both' via fallback dedupe. * - Otherwise: insert a new 'xor-only' entry. * - * @param array> &$merged Dedupe map, modified in place. - * @param array $event X-OR-* scan event row. - * @param string $eventUid Pre-extracted UID. - * @param string $calendarUri Resolved calendar URI (may be ''). + * @param array> $merged Dedupe map, modified in place by reference. + * @param array $event X-OR-* scan event row. + * @param string $eventUid Pre-extracted UID. + * @param string $calendarUri Resolved calendar URI (may be ''). * * @return void */ @@ -399,8 +404,8 @@ private function mergeXorEvent(array &$merged, array $event, string $eventUid, s } } - $payload = $event; - $payload['source'] = 'xor-only'; + $payload = $event; + $payload['source'] = 'xor-only'; $payload['calendarUri'] = $calendarUri; $merged[$key] = $payload; }//end mergeXorEvent() @@ -412,7 +417,7 @@ private function mergeXorEvent(array &$merged, array $event, string $eventUid, s * * @throws Exception When no user is logged in. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getAvailableCalendars(): array { @@ -457,7 +462,7 @@ public function getAvailableCalendars(): array * * @throws Exception When no user, no calendar, or calendar URI is unknown. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getEventsForCalendar(string $calendarUri, ?int $limit=100, ?DateTimeInterface $after=null): array { @@ -493,8 +498,16 @@ public function getEventsForCalendar(string $calendarUri, ?int $limit=100, ?Date usort( $results, static function (array $a, array $b): int { - $timeA = ($a['dtstart'] !== null) ? strtotime((string) $a['dtstart']) : PHP_INT_MAX; - $timeB = ($b['dtstart'] !== null) ? strtotime((string) $b['dtstart']) : PHP_INT_MAX; + $timeA = PHP_INT_MAX; + if ($a['dtstart'] !== null) { + $timeA = strtotime((string) $a['dtstart']); + } + + $timeB = PHP_INT_MAX; + if ($b['dtstart'] !== null) { + $timeB = strtotime((string) $b['dtstart']); + } + return $timeA <=> $timeB; } ); @@ -535,10 +548,10 @@ private function resolveCalendarIdForUri(array $calendars, string $calendarUri): * Returns null when the object is not a parseable VEVENT, has no UID, * or falls before the cutoff timestamp. * - * @param int $calendarId Numeric calendar id. - * @param string $calendarUri Calendar URI string. - * @param array $object Lightweight object row from getCalendarObjects(). - * @param int|null $cutoff Unix timestamp lower bound (null = no filter). + * @param int $calendarId Numeric calendar id. + * @param string $calendarUri Calendar URI string. + * @param array $object Lightweight object row from getCalendarObjects(). + * @param int|null $cutoff Unix timestamp lower bound (null = no filter). * * @return array|null Picker row, or null to skip this object. */ @@ -582,11 +595,11 @@ private function parseCalendarObjectForPicker( * Returns null when the iCal has no VEVENT, no UID, or the event * is before the cutoff timestamp. * - * @param string $calendarData Raw iCal data. - * @param string $objectUri CalDAV object URI. - * @param int $calendarId Numeric calendar id. - * @param string $calendarUri Calendar URI string. - * @param int|null $cutoff Unix timestamp lower bound (null = no filter). + * @param string $calendarData Raw iCal data. + * @param string $objectUri CalDAV object URI. + * @param int $calendarId Numeric calendar id. + * @param string $calendarUri Calendar URI string. + * @param int|null $cutoff Unix timestamp lower bound (null = no filter). * * @return array|null Picker row, or null to skip. */ @@ -604,14 +617,24 @@ private function buildPickerRowFromIcal( } $uid = (string) $vevent->UID; - $start = isset($vevent->DTSTART) === true ? $vevent->DTSTART->getDateTime() : null; + $start = null; + if (isset($vevent->DTSTART) === true) { + $start = $vevent->DTSTART->getDateTime(); + } if ($cutoff !== null && $start !== null && $start->getTimestamp() < $cutoff) { return null; } - $dtend = isset($vevent->DTEND) === true ? $vevent->DTEND->getDateTime()->format('c') : null; - $rowLocation = isset($vevent->LOCATION) === true ? (string) $vevent->LOCATION : null; + $dtend = null; + if (isset($vevent->DTEND) === true) { + $dtend = $vevent->DTEND->getDateTime()->format('c'); + } + + $rowLocation = null; + if (isset($vevent->LOCATION) === true) { + $rowLocation = (string) $vevent->LOCATION; + } return [ 'uid' => $uid, @@ -706,14 +729,25 @@ private function parseVeventObjectForLocate( return null; } - $dtstart = isset($vevent->DTSTART) === true - ? DateTime::createFromInterface($vevent->DTSTART->getDateTime()) - : null; - $dtend = isset($vevent->DTEND) === true - ? DateTime::createFromInterface($vevent->DTEND->getDateTime()) - : null; - $rowSummary = isset($vevent->SUMMARY) === true ? (string) $vevent->SUMMARY : null; - $rowLocation = isset($vevent->LOCATION) === true ? (string) $vevent->LOCATION : null; + $dtstart = null; + if (isset($vevent->DTSTART) === true) { + $dtstart = DateTime::createFromInterface($vevent->DTSTART->getDateTime()); + } + + $dtend = null; + if (isset($vevent->DTEND) === true) { + $dtend = DateTime::createFromInterface($vevent->DTEND->getDateTime()); + } + + $rowSummary = null; + if (isset($vevent->SUMMARY) === true) { + $rowSummary = (string) $vevent->SUMMARY; + } + + $rowLocation = null; + if (isset($vevent->LOCATION) === true) { + $rowLocation = (string) $vevent->LOCATION; + } return [ 'calendarId' => $calendarId, @@ -787,7 +821,7 @@ private function calendarSupportsVevent(array $calendar): bool } if (is_object($components) === true && method_exists($components, 'getValue') === true) { - return $this->componentListContainsVevent($components->getValue()); + return $this->componentListContainsVevent(components: $components->getValue()); } if (is_string($components) === true) { @@ -795,7 +829,7 @@ private function calendarSupportsVevent(array $calendar): bool } if (is_iterable($components) === true) { - return $this->componentListContainsVevent($components); + return $this->componentListContainsVevent(components: $components); } return false; diff --git a/lib/Service/CaseTokenService.php b/lib/Service/CaseTokenService.php new file mode 100644 index 0000000000..e411ce4d3b --- /dev/null +++ b/lib/Service/CaseTokenService.php @@ -0,0 +1,396 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use DateTime; +use InvalidArgumentException; +use OCA\OpenRegister\Db\CaseToken; +use OCA\OpenRegister\Db\CaseTokenMapper; +use OCP\IURLGenerator; +use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * CaseTokenService — public case-token link mint / resolve / revoke. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Service composes the token mapper, secure-random, + * user-session, URL generator and the lazily-resolved ObjectService — each is required for the + * mint / resolve / revoke contract. + */ +class CaseTokenService +{ + + /** + * Token length in characters (URL-safe alphanumeric). 43 chars of + * the 62-symbol alphabet ≈ 256 bits of entropy — non-guessable, so a + * brute-force enumeration of the resolve endpoint is infeasible. + * + * @var int + */ + private const TOKEN_LENGTH = 43; + + /** + * Optional server container override (tests inject a mock so the + * ObjectService resolve path is exercisable without the full + * container). + * + * @var ContainerInterface|null + */ + private ?ContainerInterface $container; + + /** + * Constructor. + * + * @param CaseTokenMapper $mapper Token persistence. + * @param ISecureRandom $secureRandom NC secure RNG. + * @param IUserSession $userSession Current user (minter). + * @param IURLGenerator $urlGenerator Public URL builder. + * @param LoggerInterface $logger Logger. + * @param ContainerInterface|null $container Optional container (tests only). + * + * @return void + */ + public function __construct( + private CaseTokenMapper $mapper, + private ISecureRandom $secureRandom, + private IUserSession $userSession, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, + ?ContainerInterface $container=null, + ) { + $this->container = $container; + }//end __construct() + + /** + * Mint a public token-link for an object. + * + * @param string $objectUuid Object uuid to bind the token to. + * @param int|null $registerId Register id of the object. + * @param int|null $schemaId Schema id of the object. + * @param string|null $label Optional human label. + * @param int|null $ttlSeconds Optional time-to-live in seconds; + * null = never expires. + * + * @return array Minted token metadata + the public URL. + * + * @throws InvalidArgumentException When objectUuid is empty or the + * minter is anonymous. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function mint( + string $objectUuid, + ?int $registerId=null, + ?int $schemaId=null, + ?string $label=null, + ?int $ttlSeconds=null + ): array { + if (trim($objectUuid) === '') { + throw new InvalidArgumentException('objectUuid is required to mint a case token'); + } + + $user = $this->userSession->getUser(); + if ($user === null) { + // Minting is a write that establishes a public surface — it + // MUST be performed by an authenticated user, never anonymously. + throw new InvalidArgumentException('A logged-in user is required to mint a case token'); + } + + $now = new DateTime(); + $expires = null; + if ($ttlSeconds !== null && $ttlSeconds > 0) { + $expires = (clone $now)->modify('+'.$ttlSeconds.' seconds'); + } + + $token = $this->secureRandom->generate( + self::TOKEN_LENGTH, + ISecureRandom::CHAR_ALPHANUMERIC + ); + + $entity = new CaseToken(); + $entity->setToken($token); + $entity->setObjectUuid($objectUuid); + $entity->setRegisterId($registerId); + $entity->setSchemaId($schemaId); + $entity->setLabel($label); + $entity->setCreatedBy($user->getUID()); + $entity->setCreatedAt($now); + $entity->setExpiresAt($expires); + $entity->setRevokedAt(null); + + $saved = $this->mapper->insert($entity); + + $data = $saved->jsonSerialize(); + $data['url'] = $this->buildPublicUrl(token: $token); + return $data; + }//end mint() + + /** + * Resolve a token to a PUBLIC-SAFE view of the referenced object. + * + * Runs the canonical OR read path with RBAC enforced (`_rbac: true`): + * an anonymous caller only sees the fields the public group may read, + * exactly as `ObjectsController::show()` does for anonymous requests. + * The token never bypasses RBAC — it only addresses the object. + * + * Returns null on any failure (unknown / revoked / expired token, + * object missing, RBAC-denied) so the caller returns a uniform 404 + * and the endpoint is not an enumeration oracle. + * + * @param string $token The opaque token. + * + * @return array|null The public-safe object view, or + * null when the token cannot be + * resolved. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function resolve(string $token): ?array + { + if (trim($token) === '') { + return null; + } + + $row = $this->mapper->findByToken($token); + if ($row === null) { + return null; + } + + if ($row->isValidAt(new DateTime()) === false) { + // Revoked or expired — fail closed. + return null; + } + + $objectService = $this->resolveObjectService(); + if ($objectService === null) { + return null; + } + + try { + // RBAC-respecting read: no user is logged in on the public + // resolve endpoint, so `_rbac: true` enforces the public-group + // read predicate. We do NOT pass an admin bypass. + $entity = $objectService->find( + id: $row->getObjectUuid(), + _extend: [], + files: false, + register: $row->getRegisterId(), + schema: $row->getSchemaId(), + _rbac: true, + _multitenancy: true + ); + + if ($entity === null) { + return null; + } + + $rendered = $objectService->renderEntity( + entity: $entity, + _extend: [], + depth: 0, + filter: [], + fields: [], + unset: [], + _rbac: true, + _multitenancy: true + ); + + return [ + 'token' => $row->getToken(), + 'label' => $row->getLabel(), + 'object' => $rendered, + ]; + } catch (Throwable $e) { + // RBAC-denied / not-found / any read failure → 404 (null). + // Logged server-side; the caller never learns why. + $this->logger->debug( + '[CaseTokenService] resolve failed for a token', + ['exception' => $e] + ); + return null; + }//end try + }//end resolve() + + /** + * Revoke a token so it can no longer be resolved. + * + * Idempotent: revoking an already-revoked or unknown token returns + * false rather than throwing. Accepts either the opaque token string + * or the numeric row id (the provider addresses tokens by id). + * + * @param string $tokenOrId The opaque token, or its numeric row id. + * + * @return bool True when a token was revoked, false when none matched. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function revoke(string $tokenOrId): bool + { + $row = $this->mapper->findByToken($tokenOrId); + if ($row === null && ctype_digit($tokenOrId) === true) { + $row = $this->mapper->findById((int) $tokenOrId); + } + + if ($row === null) { + return false; + } + + if ($row->getRevokedAt() !== null) { + // Already revoked — idempotent no-op. + return false; + } + + $row->setRevokedAt(new DateTime()); + $this->mapper->update($row); + return true; + }//end revoke() + + /** + * List the (public-safe metadata of) tokens minted against an object. + * + * @param string $objectUuid Object uuid. + * + * @return array> Token metadata rows. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function listForObject(string $objectUuid): array + { + $rows = $this->mapper->findByObjectUuid($objectUuid); + return array_map( + function (CaseToken $row): array { + $data = $row->jsonSerialize(); + $data['url'] = $this->buildPublicUrl(token: (string) $row->getToken()); + return $data; + }, + $rows + ); + }//end listForObject() + + /** + * Build the absolute public resolve URL for a token. + * + * @param string $token The opaque token. + * + * @return string Absolute URL. + */ + private function buildPublicUrl(string $token): string + { + $path = $this->urlGenerator->linkToRoute( + 'openregister.caseToken.resolve', + ['token' => $token] + ); + return $this->urlGenerator->getAbsoluteURL($path); + }//end buildPublicUrl() + + /** + * Resolve the ObjectService from the active container. + * + * @return object|null The ObjectService, or null when unresolvable. + */ + private function resolveObjectService(): ?object + { + try { + $container = $this->resolveContainer(); + $service = $container->get('OCA\\OpenRegister\\Service\\ObjectService'); + if (is_object($service) === true) { + return $service; + } + + return null; + } catch (Throwable $e) { + return null; + } + }//end resolveObjectService() + + /** + * Resolve the active container — the test override if injected, + * otherwise NC's global server container. + * + * @return ContainerInterface + */ + private function resolveContainer(): ContainerInterface + { + if ($this->container !== null) { + return $this->container; + } + + return new class implements ContainerInterface { + /** + * Resolve a service from NC's global server container. + * + * @param string $id Service id. + * + * @return object + * + * @spec exclude Anonymous PSR-11 adapter shim around \OCP\Server::get — pure DI plumbing, no behavioural contract. + */ + public function get(string $id): object + { + return \OCP\Server::get($id); + }//end get() + + /** + * Whether NC's global server container can resolve the id. + * + * @param string $id Service id. + * + * @return bool + * + * @spec exclude Anonymous PSR-11 adapter shim around \OCP\Server::get — pure DI plumbing, no behavioural contract. + */ + public function has(string $id): bool + { + try { + \OCP\Server::get($id); + return true; + } catch (Throwable $e) { + return false; + } + }//end has() + }; + }//end resolveContainer() +}//end class diff --git a/lib/Service/Chat/ChatProxyHandler.php b/lib/Service/Chat/ChatProxyHandler.php new file mode 100644 index 0000000000..f9c0e88c18 --- /dev/null +++ b/lib/Service/Chat/ChatProxyHandler.php @@ -0,0 +1,412 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/chat-ai/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Chat; + +use OCP\App\IAppManager; +use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\AppFramework\Http\Response; +use OCP\Http\Client\IClientService; +use OCP\IAppConfig; +use OCP\IRequest; +use OCP\IURLGenerator; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Resolves proxy configuration, rewrites OR paths onto their hermiq + * mirror, and performs the actual outbound forward/redirect/reachability + * calls used by ChatCompatMiddleware. + * + * @category Service + * @package OCA\OpenRegister\Service\Chat + */ +class ChatProxyHandler +{ + + /** + * App id whose config holds the proxy target. + * + * @var string + */ + private const CONFIG_APP = 'openregister'; + + /** + * Appconfig key. Since or-chat-engine-decommission an UNSET value means + * "proxy to hermiq" (hermiq is the agent engine's canonical home per + * hydra ADR-034 Amendment 2026-07-05). An operator opts out — restoring + * local-engine answering — by setting any other explicit value, e.g. + * `occ config:app:set openregister chat.proxyTo --value=off`. + * + * @var string + */ + private const CONFIG_KEY_PROXY_TO = 'chat.proxyTo'; + + /** + * The only currently-supported proxy target. + * + * @var string + */ + private const HERMIQ_APP_ID = 'hermiq'; + + /** + * OpenRegister's own path prefix, as returned by IRequest::getPathInfo() + * for every route this middleware guards. + * + * @var string + */ + private const OR_PATH_PREFIX = '/apps/openregister/'; + + /** + * Hermiq's mirrored path prefix (route-for-route mirror per + * hermiq/openspec/changes/agent-engine-schemas/design.md Appendix C). + * + * @var string + */ + private const HERMIQ_PATH_PREFIX = '/apps/hermiq/'; + + /** + * Hermiq's chat health probe — used as the cheap reachability check + * before redirecting the SSE streaming endpoint (a redirect can't itself + * detect an unreachable target the way a forwarded call can). + * + * @var string + */ + private const HERMIQ_HEALTH_PATH = '/apps/hermiq/api/chat/health'; + + /** + * Outbound forward call timeout, in seconds. Kept short — this is a + * same-instance, same-host loopback call, not a call to a third-party + * service. + * + * @var int + */ + private const FORWARD_TIMEOUT_SECONDS = 8; + + /** + * Reachability probe timeout, in seconds. Shorter than the forward + * timeout because this is a pure liveness check, not a real request. + * + * @var int + */ + private const PROBE_TIMEOUT_SECONDS = 3; + + /** + * Constructor. + * + * @param IAppConfig $appConfig Reads the `chat.proxyTo` appconfig value. + * @param IAppManager $appManager Checks whether hermiq is installed/enabled before ever attempting a call. + * @param IURLGenerator $urlGenerator Builds absolute same-instance URLs for the forward/redirect targets. + * @param IClientService $clientService NC's outbound HTTP client factory. + * @param LoggerInterface $logger Logs fallback reasons (never throws past this class). + */ + public function __construct( + private readonly IAppConfig $appConfig, + private readonly IAppManager $appManager, + private readonly IURLGenerator $urlGenerator, + private readonly IClientService $clientService, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Whether the hermiq proxy is active. Defaults to active when the + * appconfig value is unset (or-chat-engine-decommission); an operator + * opts out with any explicit value other than 'hermiq' (e.g. 'off'). + * + * @return bool True when `chat.proxyTo` is unset or set to 'hermiq'. + */ + public function isProxyConfigured(): bool + { + return $this->appConfig->getValueString(self::CONFIG_APP, self::CONFIG_KEY_PROXY_TO, self::HERMIQ_APP_ID) === self::HERMIQ_APP_ID; + }//end isProxyConfigured() + + /** + * Cheap, no-network check that hermiq is installed and enabled on this + * instance. Always checked before any outbound call — an uninstalled + * hermiq should never generate an HTTP attempt at all. + * + * @return bool True when hermiq is enabled instance-wide. + */ + public function isHermiqInstalled(): bool + { + return $this->appManager->isInstalled(self::HERMIQ_APP_ID); + }//end isHermiqInstalled() + + /** + * Rewrite an OpenRegister request path onto its hermiq mirror. + * + * @param string $orPathInfo The incoming request's `IRequest::getPathInfo()` value. + * + * @return string|null The rewritten `/apps/hermiq/...` path, or null when + * `$orPathInfo` does not start with the expected OR prefix + * (defensive — should not happen for the routes this + * middleware guards). + */ + public function rewritePathForHermiq(string $orPathInfo): ?string + { + if (str_starts_with($orPathInfo, self::OR_PATH_PREFIX) === false) { + return null; + } + + return self::HERMIQ_PATH_PREFIX.substr($orPathInfo, strlen(self::OR_PATH_PREFIX)); + }//end rewritePathForHermiq() + + /** + * Forward a JSON API request to hermiq and rebuild the response + * verbatim (status code + body + Content-Type). + * + * Returns null on any transport-level failure — the caller MUST treat + * that as "fall back to local serving", never as an error response to + * the client. A non-2xx *upstream* status is NOT a transport failure and + * is passed through as-is (`http_errors` disabled). + * + * @param IRequest $request The original incoming request. + * @param string $hermiqPath The rewritten `/apps/hermiq/...` path (see rewritePathForHermiq()). + * + * @return Response|null The rebuilt response, or null on transport failure. + */ + public function forwardJsonRequest(IRequest $request, string $hermiqPath): ?Response + { + $url = $this->buildAbsoluteUrl( + hermiqPath: $hermiqPath, + queryString: $this->extractQueryString(request: $request) + ); + $method = strtoupper($request->getMethod()); + + $options = [ + 'headers' => $this->buildForwardHeaders(request: $request), + 'http_errors' => false, + 'timeout' => self::FORWARD_TIMEOUT_SECONDS, + ]; + + if (in_array($method, ['GET', 'DELETE'], true) === false) { + $options['body'] = (string) json_encode($this->extractForwardableParams(request: $request)); + } + + try { + $client = $this->clientService->newClient(); + $upstream = $client->request($method, $url, $options); + } catch (Throwable $e) { + $this->logger->warning( + '[ChatProxyHandler] hermiq forward failed — falling back to local serving', + [ + 'url' => $url, + 'error' => $e->getMessage(), + ] + ); + return null; + } + + $body = $upstream->getBody(); + if (is_resource($body) === true) { + $body = stream_get_contents($body); + } + + // DataDisplayResponse is the plain "raw bytes, no processing" NC + // response type. It defaults to a `Content-Disposition: inline; + // filename=""` header (meant for file-display use cases) which we + // strip immediately — a proxied JSON API response should carry the + // exact same headers the local endpoint would have, not a + // download-oriented one it never had before. + // + // Response's generic S template is bound to the literal union of + // Http::STATUS_* constants; a real upstream HTTP response's status + // code is always one of those values at runtime, but + // IClientService's IResponse::getStatusCode() returns a plain int, + // which can't be statically narrowed to the literal union here. + // @phpstan-ignore-next-line Runtime status code is always valid. + $response = new DataDisplayResponse((string) $body, $upstream->getStatusCode()); + // The addHeader() docblock documents "null will delete it" as valid + // behaviour, but its declared parameter type is plain `string` — a + // pre-existing NC stub inaccuracy, not a bug here. + // @phpstan-ignore-next-line Null is documented as valid here. + $response->addHeader('Content-Disposition', null); + + $contentType = $upstream->getHeader('Content-Type'); + if ($contentType !== '') { + $response->addHeader('Content-Type', $contentType); + } + + return $response; + }//end forwardJsonRequest() + + /** + * Cheap reachability probe used before redirecting the SSE streaming + * endpoint. Unlike forwardJsonRequest(), a 308 redirect can't itself + * detect an unreachable target — the browser would follow it straight + * into a connection error — so this probe stands in for that check. + * + * @return bool True when hermiq's chat health endpoint responds without a 5xx / transport error. + */ + public function probeReachable(): bool + { + $url = $this->urlGenerator->getAbsoluteURL(self::HERMIQ_HEALTH_PATH); + + try { + $client = $this->clientService->newClient(); + $response = $client->get( + $url, + [ + 'timeout' => self::PROBE_TIMEOUT_SECONDS, + 'http_errors' => false, + ] + ); + } catch (Throwable $e) { + $this->logger->warning( + '[ChatProxyHandler] hermiq reachability probe failed — falling back to local serving', + [ + 'url' => $url, + 'error' => $e->getMessage(), + ] + ); + return false; + } + + return $response->getStatusCode() < 500; + }//end probeReachable() + + /** + * Build a 308 Permanent Redirect response to hermiq's mirrored path. + * + * A 308 (unlike 301/302/303) is required to preserve request method and + * body on redirect (RFC 7538) — the streaming endpoint is an + * authenticated POST, and a same-origin redirect lets the browser's own + * `fetch()`-based SSE reader follow it and stream directly from hermiq, + * without OpenRegister needing to relay the event-stream bytes itself. + * + * @param IRequest $request The original incoming request (for its query string). + * @param string $hermiqPath The rewritten `/apps/hermiq/...` path. + * + * @return Response The 308 redirect response. + */ + public function buildRedirectResponse(IRequest $request, string $hermiqPath): Response + { + $url = $this->buildAbsoluteUrl( + hermiqPath: $hermiqPath, + queryString: $this->extractQueryString(request: $request) + ); + + // Response's generic S template is bound to + // OCP\AppFramework\Http::STATUS_* — which does not define a 308 + // constant at all (NC has never needed Permanent Redirect + // internally). 308 is a valid RFC 7538 status the base Response + // class handles correctly at runtime; this is a gap in NC's own + // constant catalogue, not an invalid status. + // @phpstan-ignore-next-line 308 is valid RFC 7538, just uncatalogued. + return new Response(status: 308, headers: ['Location' => $url]); + }//end buildRedirectResponse() + + /** + * Extract the `?...` query string (including the leading `?`) from the + * request's full URI, or '' when there is none. + * + * @param IRequest $request The incoming request. + * + * @return string The query string, or ''. + */ + private function extractQueryString(IRequest $request): string + { + $uri = $request->getRequestUri(); + $pos = strpos($uri, '?'); + if ($pos === false) { + return ''; + } + + return substr($uri, $pos); + }//end extractQueryString() + + /** + * Build the absolute same-instance URL for a hermiq path + query string. + * + * @param string $hermiqPath The rewritten `/apps/hermiq/...` path. + * @param string $queryString The `?...` query string (or ''). + * + * @return string The absolute URL. + */ + private function buildAbsoluteUrl(string $hermiqPath, string $queryString): string + { + return $this->urlGenerator->getAbsoluteURL($hermiqPath).$queryString; + }//end buildAbsoluteUrl() + + /** + * Build the headers forwarded on the outbound call. Only the session + * cookie is carried through (same-instance auth flowthrough, hydra + * ADR-034 Decision 7 — "session cookie unchanged") plus a JSON + * Accept/Content-Type pair; nothing else from the original request is + * forwarded verbatim. + * + * @param IRequest $request The original incoming request. + * + * @return array The headers to send upstream. + */ + private function buildForwardHeaders(IRequest $request): array + { + $headers = [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ]; + + $cookie = $request->getHeader('Cookie'); + if ($cookie !== '') { + $headers['Cookie'] = $cookie; + } + + return $headers; + }//end buildForwardHeaders() + + /** + * Build the JSON body forwarded on the outbound call from the original + * request's merged GET/POST/route parameters, stripping NC-internal + * keys that must never be replayed against another app (CSRF token, + * NC's own `_route` bookkeeping key). + * + * @param IRequest $request The original incoming request. + * + * @return array The filtered parameters. + */ + private function extractForwardableParams(IRequest $request): array + { + $filtered = []; + foreach ($request->getParams() as $key => $value) { + if (is_string($key) === true && (str_starts_with($key, '_') === true || $key === 'requesttoken')) { + continue; + } + + $filtered[$key] = $value; + } + + return $filtered; + }//end extractForwardableParams() +}//end class diff --git a/lib/Service/Chat/ContextRetrievalHandler.php b/lib/Service/Chat/ContextRetrievalHandler.php index 35b1aa25e7..6870b3a3c2 100644 --- a/lib/Service/Chat/ContextRetrievalHandler.php +++ b/lib/Service/Chat/ContextRetrievalHandler.php @@ -20,15 +20,15 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Service\Chat; use Exception; use OCA\OpenRegister\Db\Agent; +use OCA\OpenRegister\Service\ObjectService; use OCA\OpenRegister\Service\Vectorization\VectorEmbeddings; -use OCA\OpenRegister\Service\IndexService; use Psr\Log\LoggerInterface; /** @@ -57,11 +57,11 @@ class ContextRetrievalHandler private VectorEmbeddings $vectorService; /** - * Index service for SOLR search + * Object service for database search * - * @var IndexService + * @var ObjectService */ - private IndexService $solrService; + private ObjectService $objectService; /** * Logger @@ -74,20 +74,20 @@ class ContextRetrievalHandler * Constructor * * @param VectorEmbeddings $vectorService Vector embeddings service. - * @param IndexService $solrService SOLR index service. + * @param ObjectService $objectService Object service for database search. * @param LoggerInterface $logger Logger. * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function __construct( VectorEmbeddings $vectorService, - IndexService $solrService, + ObjectService $objectService, LoggerInterface $logger ) { $this->vectorService = $vectorService; - $this->solrService = $solrService; + $this->objectService = $objectService; $this->logger = $logger; }//end __construct() @@ -108,7 +108,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) RAG context retrieval requires many search strategies * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Complex RAG logic cannot be easily split * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function retrieveContext( string $query, @@ -221,12 +221,11 @@ public function retrieveContext( // Pass filters array instead of 0.7. ); } else if ($searchMode === 'hybrid') { + // Fuse the already-fetched keyword results with vector search. $hybridResponse = $this->vectorService->hybridSearch( query: $query, - solrFilters: ['vector_filters' => $vectorFilters], - // Pass filters in SOLR filters array. + keywordResults: $results, limit: $fetchLimit - // Limit parameter. ); // Extract results array from hybrid search response. $results = $hybridResponse['results'] ?? []; @@ -396,12 +395,12 @@ public function retrieveContext( }//end retrieveContext() /** - * Search using keyword only (SOLR) + * Search using keyword only (database) * - * Performs keyword-based search using SOLR without vector embeddings. + * Performs keyword-based search using the database without vector embeddings. * * @param string $query Query text. - * @param int $_limit Result limit (unused, for interface compatibility). + * @param int $_limit Result limit. * * @return array Search results in standardized format * @@ -409,23 +408,18 @@ public function retrieveContext( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function searchKeywordOnly(string $query, int $_limit): array { - $results = $this->solrService->searchObjectsPaginated( - query: ['_search' => $query], - limit: $_limit, - offset: 0, - facets: [], - collection: null, - includeTotal: true + $results = $this->objectService->searchObjectsPaginated( + query: ['_search' => $query, '_limit' => $_limit] ); $transformed = []; foreach ($results['results'] ?? [] as $result) { $transformed[] = [ - 'entity_id' => $result['id'] ?? null, + 'entity_id' => $result['id'] ?? $result['uuid'] ?? null, 'entity_type' => 'object', 'text' => $result['_source']['data'] ?? json_encode($result), 'score' => $result['_score'] ?? 1.0, @@ -448,7 +442,7 @@ private function searchKeywordOnly(string $query, int $_limit): array * @SuppressWarnings(PHPMD.CyclomaticComplexity) Name extraction requires checking many possible fields * @SuppressWarnings(PHPMD.NPathComplexity) Name extraction requires checking many possible fields * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function extractSourceName(array $result): string { diff --git a/lib/Service/Chat/ConversationManagementHandler.php b/lib/Service/Chat/ConversationManagementHandler.php index b60c9f1610..092d7205cb 100644 --- a/lib/Service/Chat/ConversationManagementHandler.php +++ b/lib/Service/Chat/ConversationManagementHandler.php @@ -20,7 +20,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Service\Chat; @@ -117,7 +117,7 @@ class ConversationManagementHandler * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function __construct( ConversationMapper $conversationMapper, @@ -147,7 +147,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) Title generation requires multiple LLM provider paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) LLM provider configuration cannot be easily split * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function generateConversationTitle(string $firstMessage): string { @@ -291,7 +291,7 @@ public function generateConversationTitle(string $firstMessage): string * * @return string Fallback title * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function generateFallbackTitle(string $message): string { @@ -323,7 +323,7 @@ private function generateFallbackTitle(string $message): string * * @return string Unique title with number suffix if needed * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function ensureUniqueTitle(string $baseTitle, string $userId, int $agentId): string { @@ -397,7 +397,7 @@ public function ensureUniqueTitle(string $baseTitle, string $userId, int $agentI * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function checkAndSummarize(Conversation $conversation): void { @@ -488,7 +488,7 @@ public function checkAndSummarize(Conversation $conversation): void * @SuppressWarnings(PHPMD.CyclomaticComplexity) Summary generation requires handling multiple LLM providers * @SuppressWarnings(PHPMD.NPathComplexity) Summary generation requires handling multiple LLM providers * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function generateSummary(array $messages): string { diff --git a/lib/Service/Chat/MessageHistoryHandler.php b/lib/Service/Chat/MessageHistoryHandler.php index ae3ef34617..ff7683590e 100644 --- a/lib/Service/Chat/MessageHistoryHandler.php +++ b/lib/Service/Chat/MessageHistoryHandler.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Service\Chat; @@ -83,7 +83,7 @@ class MessageHistoryHandler * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function __construct( MessageMapper $messageMapper, @@ -109,7 +109,7 @@ public function __construct( * @SuppressWarnings(PHPMD.StaticAccess) LLPhantMessage factory methods are standard LLPhant pattern * @SuppressWarnings(PHPMD.CyclomaticComplexity) Message role handling requires multiple conditional branches * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function buildMessageHistory(int $conversationId): array { @@ -208,7 +208,7 @@ public function buildMessageHistory(int $conversationId): array * * @return Message Stored message entity * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function storeMessage( int $conversationId, diff --git a/lib/Service/Chat/ResponseGenerationHandler.php b/lib/Service/Chat/ResponseGenerationHandler.php index 457bafbdb7..68cc8c1f95 100644 --- a/lib/Service/Chat/ResponseGenerationHandler.php +++ b/lib/Service/Chat/ResponseGenerationHandler.php @@ -30,7 +30,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Service\Chat; @@ -63,7 +63,7 @@ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) This class is a multi-provider LLM router that + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) This class is a multi-provider LLM router that * must reference OpenAI, Ollama, and Fireworks config/chat classes plus the streaming * infrastructure (StreamYieldChannel, StreamingToolInstanceWrapper, MissingFeatureException). * Splitting into one class per provider would be the clean long-term solution; for now all @@ -94,6 +94,15 @@ class ResponseGenerationHandler */ private LoggerInterface $logger; + /** + * Token/latency usage from the last generateResponse() call, for per-run cost recording + * (run-analytics). Populated from the LLPhant chat instance; empty when the provider + * does not expose usage. Keys: promptTokens, completionTokens, totalDurationMs, llmSeconds. + * + * @var array + */ + public array $lastUsage = []; + /** * Constructor * @@ -103,7 +112,7 @@ class ResponseGenerationHandler * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function __construct( SettingsService $settingsService, @@ -158,7 +167,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) Response generation requires many conditional API calls * @SuppressWarnings(PHPMD.ExcessiveMethodLength) LLM provider configuration cannot be easily split * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function generateResponse( string $userMessage, @@ -324,9 +333,9 @@ public function generateResponse( // Inject the CnAiContext snapshot the widget sends with each // message. Without this the LLM has no idea which app the user - // is in — so on /apps/openbuilt/ it would call decidesk tools + // is in — so on /apps/openbuild/ it would call decidesk tools // (or default to OpenRegister-platform language) instead of - // routing to openbuilt.*. The snapshot is small and free-form + // routing to openbuild.*. The snapshot is small and free-form // (typically {app, slug, view, objectId}); we render it as a // bullet list so the model can quote individual fields. if (empty($cnAiContext) === false) { @@ -360,7 +369,7 @@ public function generateResponse( $functions = $this->toolHandler->convertToolsToFunctions($tools); } - // Initialize $response (and $llmTime) BEFORE entering any + // Initialize $response (and $llmTime, $chat) BEFORE entering any // provider branch. The Ollama branch skips the OpenAIChat // initialisation block; without this default-empty seed the // logger access on `$response` below would tank with an @@ -368,6 +377,9 @@ public function generateResponse( // not to assign — an easy regression vector when a new // provider is added. The Fireworks/Ollama branches below // overwrite this unconditionally for their own provider. + // $chat = null keeps the `instanceof OllamaChat` usage-capture + // check below well-defined on every provider path. + $chat = null; $response = ''; $llmTime = 0.0; $llmStartTime = microtime(true); @@ -463,6 +475,15 @@ functionInfoObjects: $functionInfoObjects, ] ); + // Expose the LLM token/latency usage for per-run cost recording (run-analytics). + // Only OllamaChat accumulates usage today; other providers leave it empty. + $this->lastUsage = []; + if ($chat instanceof OllamaChat) { + $this->lastUsage = $chat->lastUsage; + } + + $this->lastUsage['llmSeconds'] = round($llmTime, 2); + return $response; } catch (Exception $e) { $this->logger->error( @@ -515,7 +536,7 @@ functionInfoObjects: $functionInfoObjects, * @psalm-param list<\LLPhant\Chat\FunctionInfo\FunctionInfo> $functionInfoObjects * @psalm-return list<\LLPhant\Chat\FunctionInfo\FunctionInfo> * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function wrapToolsForStreaming(array $functionInfoObjects, ?StreamYieldChannel $channel): array { @@ -546,7 +567,7 @@ private function wrapToolsForStreaming(array $functionInfoObjects, ?StreamYieldC * * @return string The assistant's textual response. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function invokeChat( OpenAIChat|OllamaChat $chat, @@ -601,7 +622,7 @@ private function invokeChat( * * @return bool True when the instance has at least one FunctionInfo registered * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function chatHasTools(OpenAIChat|OllamaChat $chat): bool { @@ -644,7 +665,7 @@ private function chatHasTools(OpenAIChat|OllamaChat $chat): bool * * @throws MissingFeatureException When the provider's streaming surface throws. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function streamChat( OpenAIChat|OllamaChat $chat, @@ -689,7 +710,7 @@ private function streamChat( * @SuppressWarnings(PHPMD.NPathComplexity) API call requires handling many response scenarios * @SuppressWarnings(PHPMD.ExcessiveMethodLength) API error handling requires verbose code * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ private function callFireworksChatAPIWithHistory( string $apiKey, diff --git a/lib/Service/Chat/StreamYieldChannel.php b/lib/Service/Chat/StreamYieldChannel.php index cb96e13296..5d9b649da4 100644 --- a/lib/Service/Chat/StreamYieldChannel.php +++ b/lib/Service/Chat/StreamYieldChannel.php @@ -30,7 +30,7 @@ * @link https://OpenRegister.app * * @spec openspec/changes/ai-chat-companion-streaming/tasks.md#1 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); @@ -177,7 +177,7 @@ public function emitToken(string $delta): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function emitToolCall(array $payload): void { @@ -195,7 +195,7 @@ public function emitToolCall(array $payload): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function emitToolResult(array $payload): void { diff --git a/lib/Service/Chat/ToolManagementHandler.php b/lib/Service/Chat/ToolManagementHandler.php index ad213fdd87..3ddadc09c9 100644 --- a/lib/Service/Chat/ToolManagementHandler.php +++ b/lib/Service/Chat/ToolManagementHandler.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Service\Chat; @@ -78,7 +78,7 @@ class ToolManagementHandler * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function __construct( AgentMapper $agentMapper, @@ -103,10 +103,14 @@ public function __construct( * * @psalm-return list * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) getAgentTools() iterates tool ids, tries multiple candidate key formats (raw / prefixed) per id, and logs both found and not-found results — each branch is a required backward-compatibility guard for agent records from different schema eras. - * @SuppressWarnings(PHPMD.NPathComplexity) Three independent early-returns (null agent, empty enabledToolIds, filtered-to-empty selection) plus the two-candidate loop expand the NPath count without adding logical complexity. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) getAgentTools() iterates tool ids, tries multiple + * candidate key formats (raw / prefixed) per id, and logs both found and not-found results — each + * branch is a required backward-compatibility guard for agent records from different schema eras. + * @SuppressWarnings(PHPMD.NPathComplexity) Three independent early-returns (null agent, empty + * enabledToolIds, filtered-to-empty selection) plus the two-candidate loop expand the NPath count + * without adding logical complexity. */ public function getAgentTools(?Agent $agent, array $selectedTools=[]): array { @@ -139,10 +143,10 @@ public function getAgentTools(?Agent $agent, array $selectedTools=[]): array foreach ($enabledToolIds as $toolId) { // Try three formats in turn so agent records from different // eras keep working: - // 1. The raw id as stored ("openbuilt", "openregister.register") + // 1. The raw id as stored ("openbuild", "openregister.register") // 2. The legacy openregister-prefixed form ("openregister.objects" // when the agent stores just "objects") - // 3. An "openbuilt" -> "openbuilt.{x}" fallback handled by + // 3. An "openbuild" -> "openbuild.{x}" fallback handled by // the McpProviderBridge — that bridge exposes every // function under one appId-level registration. $candidates = [$toolId]; @@ -199,7 +203,7 @@ public function getAgentTools(?Agent $agent, array $selectedTools=[]): array * * @psalm-return list * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function convertToolsToFunctions(array $tools): array { @@ -215,6 +219,110 @@ public function convertToolsToFunctions(array $tools): array return $functions; }//end convertToolsToFunctions() + /** + * Convert a single JSON-schema property into an LLPhant Parameter. + * + * LLPhant's formatters require `itemsOrProperties` to be either a STRING + * (the element type of an array of scalars) or an array of Parameter + * OBJECTS (an object's properties / an array of objects). Passing the raw + * JSON-schema `items`/`properties` (associative arrays of schema fragments) + * makes FunctionFormatter read `->name` on a string and throw. This builds + * the correct shape, recursing one level for nested objects. + * + * @param string $name The property name. + * @param array $def The JSON-schema fragment for the property. + * + * @return Parameter + */ + private function schemaToParameter(string $name, array $def): Parameter + { + $type = $def['type'] ?? 'string'; + $description = $def['description'] ?? ''; + $enum = $def['enum'] ?? []; + $format = $def['format'] ?? null; + $itemsOrProperties = null; + + if ($type === 'object') { + $properties = $this->propertiesToParameters(properties: ($def['properties'] ?? [])); + if (count($properties) === 0) { + // Free-form object with no declared sub-properties (e.g. a + // schema's `properties` map). LLPhant serialises an empty + // object schema as "properties": [] (a JSON array), which + // Ollama rejects with "Value looks like object, but can't find + // closing '}'". Represent it as a JSON string the model fills. + $type = 'string'; + $description = $this->freeFormObjectDescription(description: $description); + } else { + $itemsOrProperties = $properties; + } + } else if ($type === 'array') { + $items = $def['items'] ?? []; + $itemType = 'string'; + if (is_array($items) === true && isset($items['type']) === true) { + $itemType = (string) $items['type']; + } + + if ($itemType === 'object') { + $properties = $this->propertiesToParameters(properties: ($items['properties'] ?? [])); + // Same empty-object guard for arrays of free-form objects. + if (count($properties) === 0) { + $itemsOrProperties = 'string'; + } else { + $itemsOrProperties = $properties; + } + } else { + // A scalar element type is passed as a plain string. + $itemsOrProperties = $itemType; + } + }//end if + + return new Parameter($name, $type, $description, $enum, $format, $itemsOrProperties); + + }//end schemaToParameter() + + /** + * Build the description used when a free-form object (no declared + * sub-properties) is represented as a JSON string instead. + * + * @param string $description Original property description (may be empty). + * + * @return string Description guiding the model to pass a JSON object. + */ + private function freeFormObjectDescription(string $description): string + { + if ($description === '') { + return 'A JSON object.'; + } + + return ($description.' (pass as a JSON object).'); + }//end freeFormObjectDescription() + + /** + * Convert a JSON-schema `properties` map into an array of Parameter objects. + * + * @param array $properties The properties map (name => schema). + * + * @return Parameter[] + */ + private function propertiesToParameters(array $properties): array + { + $out = []; + foreach ($properties as $propName => $propDef) { + $propDefArray = []; + if (is_array($propDef) === true) { + $propDefArray = $propDef; + } + + $out[] = $this->schemaToParameter( + name: (string) $propName, + def: $propDefArray + ); + } + + return $out; + + }//end propertiesToParameters() + /** * Convert array-based function definitions to FunctionInfo objects * @@ -229,7 +337,7 @@ public function convertToolsToFunctions(array $tools): array * * @psalm-return list * - * @spec openspec/changes/retrofit-2026-05-24-chat-ai/tasks.md#task-1 + * @spec openspec/specs/chat-ai/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Function conversion requires handling multiple parameter types * @SuppressWarnings(PHPMD.NPathComplexity) Function conversion requires handling multiple parameter types @@ -245,33 +353,14 @@ public function convertFunctionsToFunctionInfo(array $functions, array $tools): if (($func['parameters']['properties'] ?? null) !== null) { foreach ($func['parameters']['properties'] as $paramName => $paramDef) { - // Determine parameter type from definition. - $type = $paramDef['type'] ?? 'string'; - $description = $paramDef['description'] ?? ''; - $enum = $paramDef['enum'] ?? []; - $format = $paramDef['format'] ?? null; - $itemsOrProperties = null; - - // Handle nested object/array types. - if ($type === 'object') { - // For object types, pass the properties definition (empty array if not specified). - $itemsOrProperties = $paramDef['properties'] ?? []; - } else if ($type === 'array') { - // For array types, pass the items definition (empty array if not specified). - $itemsOrProperties = $paramDef['items'] ?? []; + $paramDefArray = []; + if (is_array($paramDef) === true) { + $paramDefArray = $paramDef; } - // Create parameter using constructor. - // Constructor: __construct(string $name, string $type, - // string $description, array $enum=[], ?string $format=null, - // array|string|null $itemsOrProperties=null). - $parameters[] = new Parameter( - $paramName, - $type, - $description, - $enum, - $format, - $itemsOrProperties + $parameters[] = $this->schemaToParameter( + name: (string) $paramName, + def: $paramDefArray ); }//end foreach }//end if @@ -280,6 +369,16 @@ public function convertFunctionsToFunctionInfo(array $functions, array $tools): $required = $func['parameters']['required']; } + // LLPhant's FunctionInfo expects requiredParameters as Parameter + // OBJECTS, not name strings (ToolFormatter reads $param->name). Map + // the required names back to the Parameter objects built above. + $requiredParameters = []; + foreach ($parameters as $parameterObject) { + if (in_array($parameterObject->name, $required, true) === true) { + $requiredParameters[] = $parameterObject; + } + } + // Find the tool instance that has this function. $toolInstance = null; foreach ($tools as $tool) { @@ -300,7 +399,7 @@ public function convertFunctionsToFunctionInfo(array $functions, array $tools): // Pass the tool instance. $func['description'] ?? '', $parameters, - $required + $requiredParameters ); $functionInfoObjects[] = $functionInfo; diff --git a/lib/Service/ChatService.php b/lib/Service/ChatService.php index cebb31137d..3a79b7f0ae 100644 --- a/lib/Service/ChatService.php +++ b/lib/Service/ChatService.php @@ -20,8 +20,8 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Service; @@ -62,8 +62,12 @@ * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) testChat() declares ($provider, $config, $_testMessage) matching the public contract expected by callers and the backup implementation; the simplified stub body doesn't use all three but changing the signature would break callers. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Facade orchestrates eight dependencies (ConversationMapper, MessageMapper, AgentMapper, and five handler classes); each handler is a separate concern extracted per SOLID, and removing any would lose a capability. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) testChat() declares ($provider, $config, $_testMessage) + * matching the public contract expected by callers and the backup implementation; the simplified stub + * body doesn't use all three but changing the signature would break callers. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Facade orchestrates eight dependencies + * (ConversationMapper, MessageMapper, AgentMapper, and five handler classes); each handler is a + * separate concern extracted per SOLID, and removing any would lose a capability. */ class ChatService { @@ -207,7 +211,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) Many optional paths for agent, title generation, and timing * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Full chat orchestration requires comprehensive step handling * - * @spec openspec/changes/retrofit-2026-04-30-chat-ai/tasks.md#task-1 + * @spec openspec/specs/chat-ai/spec.md */ public function processMessage( int $conversationId, @@ -340,6 +344,8 @@ public function processMessage( 'llm' => round($llmTime, 2).'s', 'total' => round($totalTime, 2).'s', ], + // Per-run LLM token/latency usage for run-cost recording (run-analytics). + 'usage' => $this->responseHandler->lastUsage, ]; } catch (Exception $e) { $this->logger->error( @@ -363,7 +369,7 @@ public function processMessage( * * @return string Generated title * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function generateConversationTitle(string $firstMessage): string { @@ -381,7 +387,7 @@ public function generateConversationTitle(string $firstMessage): string * * @return string Unique title * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/chat-ai/spec.md */ public function ensureUniqueTitle(string $baseTitle, string $userId, int $agentId): string { diff --git a/lib/Service/ConditionMatcher.php b/lib/Service/ConditionMatcher.php index 4d73ce3327..d59625ccfa 100644 --- a/lib/Service/ConditionMatcher.php +++ b/lib/Service/ConditionMatcher.php @@ -22,7 +22,7 @@ * * @since 2.0.0 Extracted from PropertyRbacHandler * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ declare(strict_types=1); @@ -97,7 +97,7 @@ public function objectMatchesConditions(array $object, array $match): bool * * @return array Filtered match conditions * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ public function filterOrganisationMatchForCreate(array $match): array { @@ -128,7 +128,7 @@ public function filterOrganisationMatchForCreate(array $match): array * * @return bool True if the condition is satisfied * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ private function singleConditionMatches(array $object, string $property, mixed $value): bool { @@ -286,7 +286,7 @@ private function resolveDynamicValue(mixed $value): mixed * * @return string|null The active organisation UUID or null * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ private function getActiveOrganisationUuid(): ?string { diff --git a/lib/Service/Configuration/CacheHandler.php b/lib/Service/Configuration/CacheHandler.php index 0c0307f827..38be06ba83 100644 --- a/lib/Service/Configuration/CacheHandler.php +++ b/lib/Service/Configuration/CacheHandler.php @@ -110,7 +110,7 @@ public function __construct( * * @return Configuration[] Array of configuration entities for active organisation * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-metadata-facets-via-self-namespace */ public function getConfigurationsForActiveOrganisation(): array { @@ -130,7 +130,13 @@ public function getConfigurationsForActiveOrganisation(): array $cachedData = $this->session->get($sessionKey); if ($cachedData !== null) { // Configurations are cached - unserialize and return. - return unserialize($cachedData); + // SEC-SVC-10: restrict unserialize to the expected entity/value + // classes so a tampered session blob cannot instantiate arbitrary + // objects (PHP object injection). + return unserialize( + $cachedData, + ['allowed_classes' => [Configuration::class, \DateTime::class]] + ); } // Step 4: Not cached - fetch configurations from database. diff --git a/lib/Service/Configuration/ExportHandler.php b/lib/Service/Configuration/ExportHandler.php index 636dcca606..a4f31bc710 100644 --- a/lib/Service/Configuration/ExportHandler.php +++ b/lib/Service/Configuration/ExportHandler.php @@ -20,7 +20,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-17 + * @spec openspec/specs/data-import-export/spec.md */ namespace OCA\OpenRegister\Service\Configuration; @@ -191,7 +191,7 @@ public function setDeployedWorkflowMapper(DeployedWorkflowMapper $mapper): void * @SuppressWarnings(PHPMD.CyclomaticComplexity) Configuration export has multiple input type conditions * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Export logic requires comprehensive data handling * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-35 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-frontend-schema-parameter-for-non-aggregated-facets */ public function exportConfig( array|Configuration|Register $input=[], @@ -416,7 +416,7 @@ public function exportConfig( * * @return array Register data formatted for OpenAPI export without instance-specific properties. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-17 + * @spec openspec/specs/data-import-export/spec.md */ private function exportRegister(Register $register): array { diff --git a/lib/Service/Configuration/FetchHandler.php b/lib/Service/Configuration/FetchHandler.php index 1041a51253..6159f0111f 100644 --- a/lib/Service/Configuration/FetchHandler.php +++ b/lib/Service/Configuration/FetchHandler.php @@ -97,7 +97,7 @@ public function __construct( * * @throws \Exception If the request fails or parsing fails. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getJSONfromURL(string $url): array|JSONResponse { @@ -149,7 +149,7 @@ public function getJSONfromURL(string $url): array|JSONResponse * * @return array|JSONResponse The fetched configuration data or error response. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function fetchRemoteConfiguration(Configuration $configuration): array|JSONResponse { @@ -222,7 +222,7 @@ public function fetchRemoteConfiguration(Configuration $configuration): array|JS * * @return array|null The decoded array or null on failure. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function decode(string $data, string $type): ?array { diff --git a/lib/Service/Configuration/GitHubHandler.php b/lib/Service/Configuration/GitHubHandler.php index 25e47dee1b..cffdd05e2a 100644 --- a/lib/Service/Configuration/GitHubHandler.php +++ b/lib/Service/Configuration/GitHubHandler.php @@ -133,7 +133,7 @@ public function __construct( * * @return array GitHub API headers. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function getHeaders(): array { @@ -149,10 +149,9 @@ private function getHeaders(): array $this->logger->debug( message: '[GitHubHandler] Using GitHub API token for authentication', context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'token_length' => strlen($token), - 'token_prefix' => substr($token, 0, 8).'...', + 'file' => __FILE__, + 'line' => __LINE__, + 'has_token' => true, ] ); } @@ -195,7 +194,7 @@ private function getHeaders(): array * @SuppressWarnings(PHPMD.NPathComplexity) Search involves many conditional data extractions * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Full search implementation requires comprehensive handling * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function searchConfigurations(string $search='', int $page=1, int $perPage=30): array { @@ -369,7 +368,7 @@ public function searchConfigurations(string $search='', int $page=1, int $perPag * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Error handling requires multiple status code checks * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function getGitHubErrorMessage(?int $statusCode, string $rawError): string { @@ -433,7 +432,7 @@ private function getGitHubErrorMessage(?int $statusCode, string $rawError): stri * * @since 0.2.11 * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function getEnrichedConfigDetails( string $owner, @@ -519,13 +518,40 @@ private function getEnrichedConfigDetails( * openregister: mixed|null * }|null * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function enrichConfigurationDetails(string $owner, string $repo, string $path, string $branch='main'): array|null { try { + // SEC-SVC-8: percent-encode each path segment so a crafted owner/ + // repo/branch/path cannot inject extra path traversal, query, or + // host components into the raw.githubusercontent.com URL. Reject + // any segment containing '..' or control characters outright. + $encodeSegments = static function (string $value): string { + $segments = explode('/', $value); + $encoded = []; + foreach ($segments as $segment) { + if ($segment === '') { + continue; + } + + if ($segment === '..' || preg_match('/[\x00-\x1f\x7f]/', $segment) === 1) { + throw new \InvalidArgumentException('Invalid path segment.'); + } + + $encoded[] = rawurlencode($segment); + } + + return implode('/', $encoded); + }; + + $safeOwner = $encodeSegments($owner); + $safeRepo = $encodeSegments($repo); + $safeBranch = $encodeSegments($branch); + $safePath = $encodeSegments($path); + // Use raw.githubusercontent.com - doesn't count against API rate limit. - $rawUrl = "https://raw.githubusercontent.com/{$owner}/{$repo}/{$branch}/{$path}"; + $rawUrl = "https://raw.githubusercontent.com/{$safeOwner}/{$safeRepo}/{$safeBranch}/{$safePath}"; $this->logger->debug( message: '[GitHubHandler] Enriching configuration details from raw URL', @@ -540,9 +566,10 @@ public function enrichConfigurationDetails(string $owner, string $repo, string $ 'GET', $rawUrl, [ - 'headers' => [ + 'headers' => [ 'Accept' => 'application/json', ], + 'allow_redirects' => false, ] ); @@ -601,7 +628,7 @@ public function enrichConfigurationDetails(string $owner, string $repo, string $ * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getBranches(string $owner, string $repo): array { @@ -666,7 +693,7 @@ function (array $branch): array { * * @since 0.2.10 * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getFileContent(string $owner, string $repo, string $path, string $branch='main'): array { @@ -748,7 +775,7 @@ public function getFileContent(string $owner, string $repo, string $path, string * version: '1.0.0'|mixed}, path: mixed, sha: mixed|null, * url: mixed|null}> * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function listConfigurationFiles(string $owner, string $repo, string $branch='main', string $path=''): array { @@ -839,7 +866,7 @@ public function listConfigurationFiles(string $owner, string $repo, string $bran * * @psalm-return array{openapi: mixed, 'x-openregister': mixed,...}|null * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function parseConfigurationFile(string $owner, string $repo, string $path, string $branch='main'): array|null { @@ -902,7 +929,7 @@ private function parseConfigurationFile(string $owner, string $repo, string $pat * @SuppressWarnings(PHPMD.CyclomaticComplexity) Repository fetch has multiple auth and error conditions * @SuppressWarnings(PHPMD.NPathComplexity) Auth check and error handling create multiple paths * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getRepositories(int $page=1, int $perPage=100): array { @@ -1022,7 +1049,7 @@ function (array $repo): array { * url: ''|mixed * } * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getRepositoryInfo(string $owner, string $repo): array { @@ -1089,7 +1116,7 @@ public function getRepositoryInfo(string $owner, string $repo): array * @SuppressWarnings(PHPMD.NPathComplexity) Publish involves multiple error and success paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Full publish handling requires comprehensive error logic * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function publishConfiguration( string $owner, @@ -1234,7 +1261,7 @@ public function publishConfiguration( * @return string|null File SHA or null if file doesn't exist * @throws \Exception If API request fails * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getFileSha(string $owner, string $repo, string $path, string $branch='main'): ?string { @@ -1286,7 +1313,7 @@ public function getFileSha(string $owner, string $repo, string $path, string $br * * @return null|string The token or null if not set * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getUserToken(string $userId): string|null { @@ -1306,7 +1333,7 @@ public function getUserToken(string $userId): string|null * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function setUserToken(?string $token, string $userId): void { @@ -1325,7 +1352,7 @@ public function setUserToken(?string $token, string $userId): void * * @return bool True if token is valid, false otherwise * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function validateToken(?string $userId=null): bool { diff --git a/lib/Service/Configuration/GitLabHandler.php b/lib/Service/Configuration/GitLabHandler.php index 21336fb3bf..397c230bd7 100644 --- a/lib/Service/Configuration/GitLabHandler.php +++ b/lib/Service/Configuration/GitLabHandler.php @@ -116,7 +116,7 @@ public function __construct( * * @psalm-return array{'PRIVATE-TOKEN'?: string} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function getHeaders(): array { @@ -152,7 +152,7 @@ private function getHeaders(): array * path: mixed, project_id: mixed, ref: 'main'|mixed, * url: ''|mixed}>, page: int, per_page: int} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function searchConfigurations(string $search='', int $page=1, int $perPage=30): array { @@ -249,7 +249,7 @@ public function searchConfigurations(string $search='', int $page=1, int $perPag * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getBranches(int $projectId): array { @@ -312,7 +312,7 @@ function (array $branch): array { * * @since 0.2.10 * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getFileContent(int $projectId, string $path, string $ref='main'): array { @@ -383,7 +383,7 @@ public function getFileContent(int $projectId, string $path, string $ref='main') * description: ''|mixed, title: mixed|string, type: 'manual'|mixed, * version: '1.0.0'|mixed}, id: mixed|null, path: mixed}> * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function listConfigurationFiles(int $projectId, string $ref='main', string $path=''): array { @@ -471,7 +471,7 @@ public function listConfigurationFiles(int $projectId, string $ref='main', strin * * @since 0.2.10 * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getProjectByPath(string $namespace, string $project): array { @@ -525,7 +525,7 @@ public function getProjectByPath(string $namespace, string $project): array * * @psalm-return array{openapi: mixed, 'x-openregister': mixed,...}|null * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function parseConfigurationFile(int $projectId, string $path, string $ref='main'): array|null { @@ -569,7 +569,7 @@ private function parseConfigurationFile(int $projectId, string $path, string $re * * @since 0.2.10 * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getApiBase(): string { diff --git a/lib/Service/Configuration/ImportHandler.php b/lib/Service/Configuration/ImportHandler.php index 16e5e372d1..235897196c 100644 --- a/lib/Service/Configuration/ImportHandler.php +++ b/lib/Service/Configuration/ImportHandler.php @@ -20,11 +20,11 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-9 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-13 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-14 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-17 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-86 + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/workflow-in-import/spec.md#requirement-schema-hook-wiring-during-import * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-28 * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-29 */ @@ -57,6 +57,9 @@ use OCA\OpenRegister\Service\WorkflowEngineRegistry; use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; use OCP\IUserSession; use Psr\Log\LoggerInterface; use DateTime; @@ -257,6 +260,22 @@ class ImportHandler */ private ?IUserSession $userSession = null; + /** + * Optional group manager used to resolve a fallback admin acting user + * when there is no logged-in session (occ/installer/cron import path). + * + * @var IGroupManager|null + */ + private ?IGroupManager $groupManager = null; + + /** + * Optional user manager used as a secondary fallback to resolve any + * enabled user as the acting user when the admin group is empty. + * + * @var IUserManager|null + */ + private ?IUserManager $userManager = null; + /** * Constructor for ImportHandler. * @@ -310,7 +329,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function setObjectService(ObjectService $objectService): void { @@ -327,7 +346,7 @@ public function setObjectService(ObjectService $objectService): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function setOpenConnectorConfigurationService(mixed $service): void { @@ -407,6 +426,103 @@ public function setUserSession(?IUserSession $userSession): void $this->userSession = $userSession; }//end setUserSession() + /** + * Inject the IGroupManager used to resolve a fallback admin acting user + * when the import runs without a logged-in session (occ/installer/cron). + * + * @param IGroupManager|null $groupManager Optional group manager. + * + * @return void + * + * @spec openspec/specs/import-resilient-per-entity-and-no-user-context/spec.md + */ + public function setGroupManager(?IGroupManager $groupManager): void + { + $this->groupManager = $groupManager; + }//end setGroupManager() + + /** + * Inject the IUserManager used as a secondary fallback to resolve any + * enabled user as the acting user when the admin group is empty. + * + * @param IUserManager|null $userManager Optional user manager. + * + * @return void + * + * @spec openspec/specs/import-resilient-per-entity-and-no-user-context/spec.md + */ + public function setUserManager(?IUserManager $userManager): void + { + $this->userManager = $userManager; + }//end setUserManager() + + /** + * Resolve the acting user for import-time object/folder operations. + * + * Precedence, mirroring the architecture's documented acting-user rule + * (explicit user → session → fallback): + * + * 1. The logged-in session user, when present (HTTP request path). A real + * session is NEVER overridden, so behaviour is unchanged when a user is + * authenticated. + * 2. The first member of the `admin` group (occ/installer/cron path, where + * there is no session). + * 3. Any first enabled user, when the admin group cannot be used. + * 4. `null` when none is resolvable — callers then skip only the + * user-dependent operation, never the whole import. + * + * @return IUser|null The resolved acting user, or null when none available. + * + * @spec openspec/specs/import-resilient-per-entity-and-no-user-context/spec.md + */ + private function resolveActingUser(): ?IUser + { + // A real session user always wins - do not override authenticated callers. + $sessionUser = $this->userSession?->getUser(); + if ($sessionUser !== null) { + return $sessionUser; + } + + // No session (occ/installer/cron): prefer the first admin. + try { + if ($this->groupManager !== null) { + $adminGroup = $this->groupManager->get('admin'); + if ($adminGroup !== null) { + $admins = $adminGroup->getUsers(); + if (count($admins) > 0) { + return reset($admins); + } + } + } + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ImportHandler] Failed to resolve admin acting user from admin group: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + } + + // Secondary fallback: any first enabled user. + try { + if ($this->userManager !== null) { + $users = $this->userManager->search('', 1, 0); + if (count($users) > 0) { + return reset($users); + } + } + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ImportHandler] Failed to resolve any fallback acting user: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + } + + $this->logger->warning( + message: '[ImportHandler] No acting user resolvable for import - user-dependent ops (folders) may skip', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return null; + }//end resolveActingUser() + /** * Set the MagicMapper dependency for ensuring magic mapper tables exist. * @@ -417,7 +533,7 @@ public function setUserSession(?IUserSession $userSession): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function setMagicMapper(MagicMapper $magicMapper): void { @@ -434,7 +550,7 @@ public function setMagicMapper(MagicMapper $magicMapper): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function setObjectMapper(MagicMapper $objectMapper): void { @@ -451,7 +567,7 @@ public function setObjectMapper(MagicMapper $objectMapper): void * * @SuppressWarnings(PHPMD.StaticAccess) Yaml::parse is standard Symfony Yaml pattern * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function decode(string $data, ?string $type): ?array { @@ -489,7 +605,7 @@ public function decode(string $data, ?string $type): ?array * * @return array The converted array data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function ensureArrayStructure(mixed $data): array { @@ -520,7 +636,7 @@ public function ensureArrayStructure(mixed $data): array * * @psalm-return JSONResponse<400, array{error: string, 'MIME-type'?: string}, array>|array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getJSONfromFile(array $uploadedFile, ?string $_type=null): array|JSONResponse { @@ -553,7 +669,7 @@ public function getJSONfromFile(array $uploadedFile, ?string $_type=null): array * * @psalm-return JSONResponse<400, array{error: string, 'Content-Type'?: string}, array>|array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getJSONfromURL(string $url): array|JSONResponse { @@ -587,7 +703,7 @@ public function getJSONfromURL(string $url): array|JSONResponse * * @psalm-return JSONResponse<400, array{error: 'Failed to decode JSON input'}, array>|array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getJSONfromBody(array | string $phpArray): array|JSONResponse { @@ -623,7 +739,7 @@ public function getJSONfromBody(array | string $phpArray): array|JSONResponse * @SuppressWarnings(PHPMD.CyclomaticComplexity) Register import has multiple exception and version checks * @SuppressWarnings(PHPMD.NPathComplexity) Version checking and update/create paths add complexity * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-17 + * @spec openspec/specs/data-import-export/spec.md */ public function importRegister( array $data, @@ -647,8 +763,6 @@ public function importRegister( try { $existingRegister = $this->registerMapper->find( id: strtolower($data['slug']), - _extend: [], - published: null, _rbac: false, _multitenancy: false ); @@ -754,7 +868,7 @@ public function importRegister( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Force flag to override version checks * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-13 + * @spec openspec/specs/data-import-export/spec.md */ private function importMapping( array $data, @@ -836,7 +950,7 @@ private function importMapping( * * @throws Exception Always throws with duplicate register information. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-17 + * @spec openspec/specs/data-import-export/spec.md */ private function handleDuplicateRegisterError(string $slug, string $appId, string $version) { @@ -912,7 +1026,7 @@ function ($register) use ($slug) { * * @throws Exception Always throws with duplicate schema information. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-14 + * @spec openspec/specs/data-import-export/spec.md */ private function handleDuplicateSchemaError(string $slug, string $appId, string $version) { @@ -996,7 +1110,7 @@ function ($schema) use ($slug) { * @SuppressWarnings(PHPMD.CyclomaticComplexity) Schema property processing has many type conditions * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Schema import involves complex property transformations * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-14 + * @spec openspec/specs/data-import-export/spec.md */ public function importSchema( array $data, @@ -1029,6 +1143,29 @@ public function importSchema( } } + // Guard: a schema fragment MUST carry a non-empty slug. Without it the + // later `$this->schemaMapper->find($data['slug'])` lookup receives null + // and raises an uncaught \TypeError — which is an \Error, NOT an + // \Exception, so the caller's `catch (Exception)` never sees it and the + // WHOLE register import aborts on one bad fragment. Fail fast with a + // descriptive \RuntimeException (an \Exception) so the caller skips this + // fragment and continues importing the rest of the app's data layer. + $slug = $data['slug'] ?? null; + if (is_string($slug) === false || trim($slug) === '') { + $title = (string) ($data['title'] ?? ''); + $hint = 'no title'; + if ($title !== '') { + $hint = "title '{$title}'"; + } + + $msg = "imported schema fragment is missing a 'slug' ({$hint}); skipping fragment"; + $this->logger->error( + message: '[ImportHandler] '.$msg, + context: ['file' => __FILE__, 'line' => __LINE__] + ); + throw new RuntimeException($msg); + } + try { // Remove id, uuid, and organisation from the data. unset($data['id'], $data['uuid'], $data['organisation']); @@ -1292,7 +1429,8 @@ public function importSchema( if ($existingSchema !== null) { // Compare versions using version_compare for proper semver comparison. $existingVersion = $existingSchema->getVersion() ?? '0.0.0'; - if ($force === false && version_compare($data['version'], $existingVersion, '<=') === true) { + $incomingVersion = $data['version'] ?? '0.0.0'; + if ($force === false && version_compare($incomingVersion, $existingVersion, '<=') === true) { $this->logger->info( message: '[ImportHandler] Skipping schema import as existing version is newer or equal.', context: ['file' => __FILE__, 'line' => __LINE__] @@ -1335,6 +1473,33 @@ public function importSchema( }//end try }//end importSchema() + /** + * Compute a stable content hash of a configuration's definitional payload. + * + * Used by the app-level import fast-skip (#426): the skip fires only when the app + * version is not newer AND this hash matches the last import, so a schema/register + * definition change re-imports even when the app version is unchanged. The hash + * covers `components` (registers, schemas, objects, mappings, …) — the parts whose + * changes should re-trigger the version-gated import — and falls back to the whole + * payload when `components` is absent. Key order is taken from the source config + * files (deterministic for an unchanged release), so no normalisation is needed; a + * reordering at most triggers one extra import pass. + * + * @param array $data The configuration data (pre-mutation snapshot). + * + * @return string A hex sha256 digest of the definitional payload. + */ + private function computeDefinitionHash(array $data): string + { + $definitional = ($data['components'] ?? $data); + $encoded = json_encode($definitional); + if ($encoded === false) { + $encoded = ''; + } + + return hash('sha256', $encoded); + }//end computeDefinitionHash() + /** * Import configuration data from JSON structure. * @@ -1370,7 +1535,7 @@ public function importSchema( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multi-component import has many branching conditions * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Full configuration import involves many entity types * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ public function importFromJson( array $data, @@ -1400,14 +1565,35 @@ public function importFromJson( $version = $data['version']; } + // Content hash of the definitional payload, computed ONCE from the incoming + // data before any import step mutates $data (schema import resolves $refs and + // stamps ids into it). The same value is reused for the skip check below and + // for the post-import store, so an unchanged release always produces an + // identical hash on the next run. + $definitionHash = $this->computeDefinitionHash(data: $data); + // Perform version check if appId and version are available (unless force is enabled). if ($appId !== null && $version !== null && $force === false) { $storedVersion = $this->appConfig->getValueString('openregister', "imported_config_{$appId}_version", ''); - - // If we have a stored version, compare it with the current version. - if ($storedVersion !== '' && version_compare($version, $storedVersion, '<=') === true) { + $storedHash = $this->appConfig->getValueString('openregister', "imported_config_{$appId}_hash", ''); + + // Skip only when the app version is NOT newer AND the definitional content + // is byte-identical to the last import (#426). Comparing the app version + // alone made a schema-definition change silently no-op on existing installs + // whenever the app version was unchanged — even when the schema's OWN + // `version` was bumped — because this early-return fired BEFORE the per-schema + // version gate (importSchema) could run. Gating additionally on a content + // hash lets the import proceed to those per-entity gates whenever the config + // content changed; they then decide what actually updates. The truly-unchanged + // case still fast-skips. A never-stored hash ('' on first run after this fix) + // fails the equality and lets the import proceed once, healing existing installs. + if ($storedVersion !== '' + && version_compare($version, $storedVersion, '<=') === true + && $storedHash !== '' + && $storedHash === $definitionHash + ) { $this->logger->info( - message: "[ImportHandler] Skipping {$appId}: v{$version} <= {$storedVersion}", + message: "[ImportHandler] Skipping {$appId}: v{$version} <= {$storedVersion} and config content unchanged", context: ['file' => __FILE__, 'line' => __LINE__] ); @@ -1448,6 +1634,14 @@ public function importFromJson( 'synchronizations' => [], 'rules' => [], 'objects' => [], + // Per-entity-resilience observability: how many entities were + // skipped (with a logged warning) instead of aborting the import. + 'skipped' => [ + 'registers' => 0, + 'objects' => 0, + 'mappings' => 0, + 'seedObjects' => 0, + ], ]; // Process and import schemas if present. @@ -1559,9 +1753,12 @@ public function importFromJson( $schemaSlug = $schemaData['slug'] ?? $key; - // Find the schema we created in Pass 1. + // Find the schema we created in Pass 1. Schemas Pass 1 skipped + // as up-to-date (or already reported as failed) are not in the + // map — the expected case on every boot of an unchanged config, + // so debug rather than one warning per schema per import. if (($this->schemasMap[$schemaSlug] ?? null) === null) { - $this->logger->warning( + $this->logger->debug( message: '[ImportHandler] Schema not found in map for Pass 2 - skipping cross-reference resolution', context: ['file' => __FILE__, 'line' => __LINE__, 'schemaSlug' => $schemaSlug] ); @@ -1666,18 +1863,38 @@ public function importFromJson( $registerData['type'] = (string) $parentType; } - $register = $this->importRegister( - data: $registerData, - owner: $owner, - appId: $appId, - version: $version, - force: $force - ); - if ($register !== null) { - // Store register in map by slug for reference. - $this->registersMap[$slug] = $register; - $result['registers'][] = $register; - } + // PER-ENTITY RESILIENCE: a single failing register MUST NOT abort + // the rest of the data-layer import (sibling registers, mappings, + // objects, seed data). Catch \Throwable (not just \Exception) so + // opis/json-schema validation failures and folder-access denials + // are also contained. Skip-and-continue with a descriptive warning, + // mirroring the existing per-schema / per-mapping idiom. + try { + $register = $this->importRegister( + data: $registerData, + owner: $owner, + appId: $appId, + version: $version, + force: $force + ); + if ($register !== null) { + // Store register in map by slug for reference. + $this->registersMap[$slug] = $register; + $result['registers'][] = $register; + } + } catch (\Throwable $e) { + $result['skipped']['registers']++; + $this->logger->warning( + message: "[ImportHandler] Skipping register '{$slug}' - import failed: ".$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'appId' => $appId, + 'registerSlug' => $slug, + 'error' => $e->getMessage(), + ] + ); + }//end try }//end foreach }//end if @@ -1774,11 +1991,59 @@ public function importFromJson( ); }//end if + // Resolve `@ref:` seed-reference tokens to concrete target UUIDs + // before the import loop. Seed objects reference siblings by slug; the + // referenced schema properties are `format: uuid`, so the tokens must be + // rewritten to the target object's UUID (and the targets given a stable + // id) before validation runs inside saveObject(). + $data = $this->resolveSeedReferenceTokens(data: $data); + // NOTE: We do NOT build ID maps - we'll pass the actual objects to avoid organisation filter issues. // When saveObject() receives Register/Schema objects, it skips the find() lookup entirely. - // Process and import objects. - if (($data['components']['objects'] ?? null) !== null && is_array($data['components']['objects']) === true) { - foreach ($data['components']['objects'] as $objectData) { + // Resolve the acting user once for the object loop. On the occ/installer/cron + // path there is no user session, so saveObject()'s folder access checks would + // default-deny ("Access to folder NNNN is denied for the acting user"). + // resolveActingUser() returns the session user when present, otherwise a + // fallback admin; null when none is resolvable (folder op then skips, not the + // whole import). + $actingUser = $this->resolveActingUser(); + // Process and import objects. Seed objects may be authored under + // components.objects (the canonical export location) OR at the top-level + // `objects` key (how some app register files place seed). Merge both, + // de-duped by @self identity (uuid, else register/schema/slug), so either + // authoring location seeds and re-import never duplicates. + $seedObjects = []; + $seedSeen = []; + foreach ([($data['components']['objects'] ?? null), ($data['objects'] ?? null)] as $seedBucket) { + if (is_array($seedBucket) === false) { + continue; + } + + foreach ($seedBucket as $seedCandidate) { + if (is_array($seedCandidate) === false) { + continue; + } + + $seedSelf = ($seedCandidate['@self'] ?? []); + $seedKey = (string) ($seedSelf['uuid'] ?? ''); + if ($seedKey === '') { + $seedKey = trim(($seedSelf['register'] ?? '').'/'.($seedSelf['schema'] ?? '').'/'.($seedSelf['slug'] ?? ''), '/'); + } + + if ($seedKey !== '' && isset($seedSeen[$seedKey]) === true) { + continue; + } + + if ($seedKey !== '') { + $seedSeen[$seedKey] = true; + } + + $seedObjects[] = $seedCandidate; + }//end foreach + }//end foreach + + if (count($seedObjects) > 0) { + foreach ($seedObjects as $objectData) { // Log raw values before any mapping. $rawRegister = $objectData['@self']['register'] ?? null; $rawSchema = $objectData['@self']['schema'] ?? null; @@ -1790,42 +2055,49 @@ public function importFromJson( continue; } - // Get the actual Register and Schema objects from maps (not IDs!). - // This is CRITICAL - passing objects avoids organisation filter in find(). - $registerObject = $this->registersMap[$rawRegister] ?? null; - $schemaObject = $this->schemasMap[$rawSchema] ?? null; - - // Fallback for object-only bundles that reference pre-existing - // registers/schemas (e.g. the rapportage templates that ship a - // dashboard against the already-imported reports/dashboard). - if ($registerObject === null && is_string($rawRegister) === true && $rawRegister !== '') { - try { - $registerObject = $this->registerMapper->find( + // PER-ENTITY RESILIENCE: wrap the whole per-object resolve / search / + // save sequence so one validation-failing object (e.g. a name-missing + // or title-less seed fragment) is skipped with a warning instead of + // aborting every sibling object and the rest of the app import. + // Catch \Throwable so opis/json-schema validation throws and folder + // access denials are contained too. + try { + // Get the actual Register and Schema objects from maps (not IDs!). + // This is CRITICAL - passing objects avoids organisation filter in find(). + $registerObject = $this->registersMap[$rawRegister] ?? null; + $schemaObject = $this->schemasMap[$rawSchema] ?? null; + + // Fallback for object-only bundles that reference pre-existing + // registers/schemas (e.g. the rapportage templates that ship a + // dashboard against the already-imported reports/dashboard). + if ($registerObject === null && is_string($rawRegister) === true && $rawRegister !== '') { + try { + $registerObject = $this->registerMapper->find( $rawRegister, _rbac: false, _multitenancy: false - ); - $this->registersMap[$rawRegister] = $registerObject; - } catch (\Throwable $e) { - $registerObject = null; + ); + $this->registersMap[$rawRegister] = $registerObject; + } catch (\Throwable $e) { + $registerObject = null; + } } - } - if ($schemaObject === null && is_string($rawSchema) === true && $rawSchema !== '') { - try { - $schemaObject = $this->schemaMapper->find( + if ($schemaObject === null && is_string($rawSchema) === true && $rawSchema !== '') { + try { + $schemaObject = $this->schemaMapper->find( $rawSchema, _rbac: false, _multitenancy: false - ); - $this->schemasMap[$rawSchema] = $schemaObject; - } catch (\Throwable $e) { - $schemaObject = null; + ); + $this->schemasMap[$rawSchema] = $schemaObject; + } catch (\Throwable $e) { + $schemaObject = null; + } } - } - if ($registerObject === null || $schemaObject === null) { - $this->logger->warning( + if ($registerObject === null || $schemaObject === null) { + $this->logger->warning( message: '[ImportHandler] Skipping object import - register or schema not found in maps or DB', context: [ 'file' => __FILE__, @@ -1836,52 +2108,52 @@ public function importFromJson( 'registerFound' => $registerObject !== null, 'schemaFound' => $schemaObject !== null, ] - ); - continue; - } + ); + continue; + } - // Get IDs for searching existing objects. - $registerId = $registerObject->getId(); - $schemaId = $schemaObject->getId(); - - // Use ObjectService::searchObjects to find existing object by register+schema+slug. - $search = [ - '@self' => [ - 'register' => (int) $registerId, - 'schema' => (int) $schemaId, - 'slug' => $slug, - ], - '_limit' => 1, - ]; - $this->logger->debug( + // Get IDs for searching existing objects. + $registerId = $registerObject->getId(); + $schemaId = $schemaObject->getId(); + + // Use ObjectService::searchObjects to find existing object by register+schema+slug. + $search = [ + '@self' => [ + 'register' => (int) $registerId, + 'schema' => (int) $schemaId, + 'slug' => $slug, + ], + '_limit' => 1, + ]; + $this->logger->debug( message: '[ImportHandler] Import object search filter', context: ['file' => __FILE__, 'line' => __LINE__, 'filter' => $search] - ); + ); - // Search for existing object. - // Use _rbac: false and _multitenancy: false to ensure we find objects regardless of organisation context. - // This prevents duplicate objects with the same UUID being created. - $this->logger->debug( + // Search for existing object. + // Use _rbac: false and _multitenancy: false to ensure we find objects regardless of organisation context. + // This prevents duplicate objects with the same UUID being created. + $this->logger->debug( message: "[ImportHandler] Searching: register=$registerId, schema=$schemaId, slug=$slug", context: ['file' => __FILE__, 'line' => __LINE__] - ); - $results = $this->objectService->searchObjects(query: $search, _rbac: false, _multitenancy: false); - $resultCount = 0; - if (is_array($results) === true) { - $resultCount = count($results); - } + ); + $results = $this->objectService->searchObjects(query: $search, _rbac: false, _multitenancy: false); + $resultCount = 0; + if (is_array($results) === true) { + $resultCount = count($results); + } - $this->logger->debug( + $this->logger->debug( message: "[ImportHandler] Found $resultCount results", context: ['file' => __FILE__, 'line' => __LINE__] - ); - $existingObject = null; - if ((is_array($results) === true) && count($results) > 0) { - $existingObject = $results[0]; - } + ); + $existingObject = null; + if ((is_array($results) === true) && count($results) > 0) { + $existingObject = $results[0]; + } - if ($existingObject === null) { - $this->logger->info( + if ($existingObject === null) { + $this->logger->info( message: '[ImportHandler] No existing object found - will create new object', context: [ 'file' => __FILE__, @@ -1890,40 +2162,48 @@ public function importFromJson( 'schemaId' => $schemaId, 'slug' => $slug, ] - ); - } - - // Replace string slugs with integer IDs in objectData's @self metadata. - // This prevents any internal lookups from using string slugs. - $objectData['@self']['register'] = (int) $registerId; - $objectData['@self']['schema'] = (int) $schemaId; - - if ($existingObject !== null) { - // Handle both ObjectEntity instances and array results from searchObjects. - // searchObjects returns ObjectEntity or arrays depending on configuration. - // @var ObjectEntity|array $existingObject. - $existingObjectData = $existingObject->jsonSerialize(); - if (is_array($existingObject) === true) { - $existingObjectData = $existingObject; + ); } - $importedVersion = $objectData['@self']['version'] ?? $objectData['version'] ?? '1.0.0'; - $existingVersion = $existingObjectData['@self']['version'] ?? $existingObjectData['version'] ?? '1.0.0'; - if (version_compare($importedVersion, $existingVersion, '>') > 0) { - $uuid = $existingObjectData['@self']['id'] ?? $existingObjectData['id'] ?? null; - // CRITICAL: Pass Register and Schema OBJECTS, not IDs. - // This avoids organisation filter issues in find(). - $object = $this->objectService->saveObject( + // Replace string slugs with integer IDs in objectData's @self metadata. + // This prevents any internal lookups from using string slugs. + $objectData['@self']['register'] = (int) $registerId; + $objectData['@self']['schema'] = (int) $schemaId; + + if ($existingObject !== null) { + // Handle both ObjectEntity instances and array results from searchObjects. + // searchObjects returns ObjectEntity or arrays depending on configuration. + // @var ObjectEntity|array $existingObject. + $existingObjectData = $existingObject->jsonSerialize(); + if (is_array($existingObject) === true) { + $existingObjectData = $existingObject; + } + + $importedVersion = $objectData['@self']['version'] ?? $objectData['version'] ?? '1.0.0'; + $existingVersion = $existingObjectData['@self']['version'] ?? $existingObjectData['version'] ?? '1.0.0'; + if (version_compare($importedVersion, $existingVersion, '>') > 0) { + $uuid = $existingObjectData['@self']['id'] ?? $existingObjectData['id'] ?? null; + // CRITICAL: Pass Register and Schema OBJECTS, not IDs. + // This avoids organisation filter issues in find(). + // Installer-time seeding runs in the repair/CLI context + // with no user session ('Anonymous'); bypass RBAC and + // multitenancy here as everywhere else in this trusted + // import path, otherwise seed objects on RBAC-guarded + // schemas (e.g. Retainer Drawdown) abort the whole import. + $object = $this->objectService->saveObject( object: $objectData, register: $registerObject, schema: $schemaObject, - uuid: $uuid - ); - $result['objects'][] = $object; - } + uuid: $uuid, + _rbac: false, + _multitenancy: false, + currentUser: $actingUser + ); + $result['objects'][] = $object; + } - if (version_compare($importedVersion, $existingVersion, '>') <= 0) { - $this->logger->info( + if (version_compare($importedVersion, $existingVersion, '>') <= 0) { + $this->logger->info( message: '[ImportHandler] Skipped object update: imported version not higher', context: [ 'file' => __FILE__, @@ -1934,22 +2214,45 @@ public function importFromJson( 'importedVersion' => $importedVersion, 'existingVersion' => $existingVersion, ] - ); - continue; + ); + continue; + }//end if }//end if - }//end if - if ($existingObject === null) { - // Create new object. - // CRITICAL: Pass Register and Schema OBJECTS, not IDs. - // This avoids organisation filter issues in find(). - $object = $this->objectService->saveObject( + if ($existingObject === null) { + // Create new object. + // CRITICAL: Pass Register and Schema OBJECTS, not IDs. + // This avoids organisation filter issues in find(). + // Installer-time seeding runs without a user session + // ('Anonymous'); bypass RBAC/multitenancy as in the rest of + // this trusted import path so seed objects on RBAC-guarded + // schemas don't abort the whole import. + $object = $this->objectService->saveObject( object: $objectData, register: $registerObject, - schema: $schemaObject + schema: $schemaObject, + _rbac: false, + _multitenancy: false, + currentUser: $actingUser + ); + $result['objects'][] = $object; + }//end if + } catch (\Throwable $e) { + // PER-ENTITY RESILIENCE: skip this object, keep importing the rest. + $result['skipped']['objects']++; + $this->logger->warning( + message: "[ImportHandler] Skipping object '{$slug}' - import failed: ".$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'appId' => $appId, + 'objectSlug' => $slug, + 'registerSlug' => $rawRegister, + 'schemaSlug' => $rawSchema, + 'error' => $e->getMessage(), + ] ); - $result['objects'][] = $object; - }//end if + }//end try }//end foreach }//end if @@ -1987,6 +2290,11 @@ public function importFromJson( // Store the version information if appId and version are available. if ($appId !== null && $version !== null) { $this->appConfig->setValueString('openregister', "imported_config_{$appId}_version", $version); + // Persist the content hash computed from the pre-mutation snapshot above so + // the next run can fast-skip only when the definitional content is unchanged + // (#426). Storing it even when the per-entity gates ended up updating nothing + // is correct: it records "this exact config content has been seen". + $this->appConfig->setValueString('openregister', "imported_config_{$appId}_hash", $definitionHash); $this->logger->info( message: "[ImportHandler] Stored version {$version} for app {$appId} after successful import", context: ['file' => __FILE__, 'line' => __LINE__] @@ -2002,17 +2310,397 @@ public function importFromJson( return $result; } - $this->importSeedData( - configData: $data, - owner: $owner, - appId: $appId, - configuration: $configuration, - result: $result - ); + // PER-ENTITY RESILIENCE: seed-data import runs AFTER registers/schemas/objects + // are already persisted. A throw inside it must never unwind that completed + // work, so wrap the whole call. Per-schema and per-object resilience also + // live inside importSeedData(). + try { + $this->importSeedData( + configData: $data, + owner: $owner, + appId: $appId, + configuration: $configuration, + result: $result + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ImportHandler] Seed-data import failed - registers/schemas/objects kept: '.$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'appId' => $appId, + 'error' => $e->getMessage(), + ] + ); + }//end try return $result; }//end importFromJson() + /** + * Resolve `@ref:` seed-reference tokens to target object UUIDs. + * + * Seed objects declare relationships to sibling seed objects by slug using + * the `@ref:` token (or the disambiguated `@ref::` + * form) in any property value. The referenced schema properties are normally + * constrained to `format: uuid`, so a literal slug token would fail + * validation inside {@see SaveObject}. This method runs before the + * object-import loop and: + * + * 1. Assigns every referenced target object a stable UUID — reusing the + * UUID of an already-imported object with the same register+schema+slug + * (so re-imports stay idempotent), otherwise minting a fresh v4 UUID — + * and writes it into `@self.id` so saveObject persists that exact id. + * 2. Replaces every `@ref:` token in object property values with the + * resolved target UUID. + * + * Unresolvable or ambiguous tokens are left untouched and logged as a + * warning, so the subsequent schema validation surfaces them rather than + * silently dropping the relationship. Objects that are never referenced are + * left exactly as-is (the import loop assigns their identity as before). + * + * @param array $data The configuration data. + * + * @return array The data with target `@self.id` populated and `@ref:` tokens resolved. + */ + private function resolveSeedReferenceTokens(array $data): array + { + if (($data['components']['objects'] ?? null) === null + || is_array($data['components']['objects']) === false + ) { + return $data; + } + + // Collect the slugs that are actually referenced, so we only resolve + // identities for genuine reference targets (and skip the work entirely + // when no object references another). + $referencedSlugs = []; + $refSchemaSlugs = []; + foreach ($data['components']['objects'] as $objectData) { + if (is_array($objectData) === true) { + $this->collectRefTargets( + value: $objectData, + bareSlugs: $referencedSlugs, + schemaSlugs: $refSchemaSlugs + ); + } + } + + if (count($referencedSlugs) === 0 && count($refSchemaSlugs) === 0) { + return $data; + } + + // PASS 1 — assign a stable UUID to each referenced target and index it. + $uuidBySchemaSlug = []; + $uuidBySlug = []; + $ambiguousSlugs = []; + + foreach ($data['components']['objects'] as &$targetData) { + if (is_array($targetData) === false) { + continue; + } + + $objectSlug = $targetData['@self']['slug'] ?? null; + if (empty($objectSlug) === true) { + continue; + } + + $schemaSlug = $targetData['@self']['schema'] ?? null; + $schemaSlugKey = null; + if (is_string($schemaSlug) === true && $schemaSlug !== '') { + $schemaSlugKey = $schemaSlug.':'.$objectSlug; + } + + // Only objects that something references need a pre-resolved identity. + $isReferenced = (array_key_exists($objectSlug, $referencedSlugs) === true + || ($schemaSlugKey !== null && array_key_exists($schemaSlugKey, $refSchemaSlugs) === true)); + if ($isReferenced === false) { + continue; + } + + // A target whose register/schema cannot be resolved will be skipped + // by the import loop and never persisted; pre-assigning it an id + // would leave referrers pointing at a dangling, never-stored UUID. + // Leave it unmapped so replaceRefTokens logs the unresolved reference + // instead of silently fabricating one. + [$registerObject, $schemaObject] = $this->resolveImportRegisterSchema(objectData: $targetData); + if ($registerObject === null || $schemaObject === null) { + continue; + } + + // Prefer the UUID already persisted for this register+schema+slug so + // re-imports stay idempotent and resolved references always match the + // id the import loop will keep on update; fall back to an explicit + // seed id (first import), then a freshly minted one. + $uuid = $this->findExistingSeedUuid( + register: $registerObject, + schema: $schemaObject, + slug: $objectSlug + ); + if (empty($uuid) === true) { + $uuid = $targetData['@self']['id'] ?? null; + } + + if (empty($uuid) === true) { + $uuid = \Symfony\Component\Uid\Uuid::v4()->toRfc4122(); + } + + $targetData['@self']['id'] = $uuid; + + if ($schemaSlugKey !== null) { + $uuidBySchemaSlug[$schemaSlugKey] = $uuid; + } + + $isDuplicateSlug = (array_key_exists($objectSlug, $uuidBySlug) === true + && $uuidBySlug[$objectSlug] !== $uuid); + if ($isDuplicateSlug === true) { + $ambiguousSlugs[$objectSlug] = true; + } + + if ($isDuplicateSlug === false) { + $uuidBySlug[$objectSlug] = $uuid; + } + }//end foreach + + unset($targetData); + + // PASS 2 — replace @ref: tokens in every object's property values. + foreach ($data['components']['objects'] as &$objectData) { + if (is_array($objectData) === false) { + continue; + } + + $self = $objectData['@self'] ?? null; + unset($objectData['@self']); + + $objectData = $this->replaceRefTokens( + value: $objectData, + uuidBySchemaSlug: $uuidBySchemaSlug, + uuidBySlug: $uuidBySlug, + ambiguousSlugs: $ambiguousSlugs + ); + + if ($self !== null) { + $objectData['@self'] = $self; + } + }//end foreach + + unset($objectData); + + return $data; + }//end resolveSeedReferenceTokens() + + /** + * Recursively collect the slugs referenced by `@ref:` tokens in a value. + * + * @param mixed $value The property value to scan. + * @param array $bareSlugs Accumulator of "objectSlug" => true (by reference). + * @param array $schemaSlugs Accumulator of "schemaSlug:objectSlug" => true (by reference). + * + * @return void + */ + private function collectRefTargets(mixed $value, array &$bareSlugs, array &$schemaSlugs): void + { + if (is_array($value) === true) { + foreach ($value as $item) { + $this->collectRefTargets(value: $item, bareSlugs: $bareSlugs, schemaSlugs: $schemaSlugs); + } + + return; + } + + if (is_string($value) === false || str_starts_with($value, '@ref:') === false) { + return; + } + + $reference = substr($value, strlen('@ref:')); + if (str_contains($reference, ':') === true) { + $schemaSlugs[$reference] = true; + return; + } + + $bareSlugs[$reference] = true; + }//end collectRefTargets() + + /** + * Recursively replace `@ref:` tokens in a value with resolved target UUIDs. + * + * A token is matched only when it spans the whole string value, in one of: + * - `@ref:` — resolved by slug (must be unambiguous). + * - `@ref::` — resolved by schema + slug (explicit). + * + * @param mixed $value The property value (scalar, list, or map). + * @param array $uuidBySchemaSlug Map of "schemaSlug:objectSlug" => uuid. + * @param array $uuidBySlug Map of "objectSlug" => uuid. + * @param array $ambiguousSlugs Set of slugs that map to multiple objects. + * + * @return mixed The value with any `@ref:` tokens replaced. + */ + private function replaceRefTokens( + mixed $value, + array $uuidBySchemaSlug, + array $uuidBySlug, + array $ambiguousSlugs + ): mixed { + if (is_array($value) === true) { + foreach ($value as $key => $item) { + $value[$key] = $this->replaceRefTokens( + value: $item, + uuidBySchemaSlug: $uuidBySchemaSlug, + uuidBySlug: $uuidBySlug, + ambiguousSlugs: $ambiguousSlugs + ); + } + + return $value; + } + + if (is_string($value) === false || str_starts_with($value, '@ref:') === false) { + return $value; + } + + $reference = substr($value, strlen('@ref:')); + $schemaSlug = null; + $objectSlug = $reference; + if (str_contains($reference, ':') === true) { + [$schemaSlug, $objectSlug] = explode(':', $reference, 2); + } + + if ($schemaSlug !== null && $schemaSlug !== '') { + if (array_key_exists($schemaSlug.':'.$objectSlug, $uuidBySchemaSlug) === true) { + return $uuidBySchemaSlug[$schemaSlug.':'.$objectSlug]; + } + + $this->logger->warning( + message: '[ImportHandler] Unresolved seed reference "'.$value.'" — no imported object for that schema+slug.', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + + return $value; + } + + if (array_key_exists($objectSlug, $ambiguousSlugs) === true) { + $this->logger->warning( + message: '[ImportHandler] Ambiguous seed reference "'.$value.'"; slug exists in ' + .'multiple schemas — use @ref::. Left unresolved.', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + + return $value; + } + + if (array_key_exists($objectSlug, $uuidBySlug) === true) { + return $uuidBySlug[$objectSlug]; + } + + $this->logger->warning( + message: '[ImportHandler] Unresolved seed reference "'.$value.'" — no imported object with that slug.', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + + return $value; + }//end replaceRefTokens() + + /** + * Resolve a seed object's `@self` register + schema slugs to their entities, + * using the in-flight import maps first and falling back to a direct mapper + * lookup (RBAC/multitenancy bypassed, as everywhere else in this trusted + * import path). Returns nulls when either cannot be resolved. + * + * @param array $objectData The seed object (with @self register/schema). + * + * @return array{0: ?Register, 1: ?Schema} The resolved register and schema. + */ + private function resolveImportRegisterSchema(array $objectData): array + { + $rawRegister = $objectData['@self']['register'] ?? null; + $rawSchema = $objectData['@self']['schema'] ?? null; + + $registerObject = $this->registersMap[$rawRegister] ?? null; + if ($registerObject instanceof Register === false + && is_string($rawRegister) === true + && $rawRegister !== '' + ) { + try { + $registerObject = $this->registerMapper->find($rawRegister, _rbac: false, _multitenancy: false); + $this->registersMap[$rawRegister] = $registerObject; + } catch (\Throwable $e) { + $registerObject = null; + } + } + + $schemaObject = $this->schemasMap[$rawSchema] ?? null; + if ($schemaObject instanceof Schema === false + && is_string($rawSchema) === true + && $rawSchema !== '' + ) { + try { + $schemaObject = $this->schemaMapper->find($rawSchema, _rbac: false, _multitenancy: false); + $this->schemasMap[$rawSchema] = $schemaObject; + } catch (\Throwable $e) { + $schemaObject = null; + } + } + + if ($registerObject instanceof Register === false) { + $registerObject = null; + } + + if ($schemaObject instanceof Schema === false) { + $schemaObject = null; + } + + return [$registerObject, $schemaObject]; + }//end resolveImportRegisterSchema() + + /** + * Look up the UUID of an already-imported object with the given + * register + schema + slug, so re-imports reuse the same identity. + * + * @param Register $register The resolved register. + * @param Schema $schema The resolved schema. + * @param string $slug The seed object slug. + * + * @return string|null The existing object UUID, or null when none exists. + */ + private function findExistingSeedUuid(Register $register, Schema $schema, string $slug): ?string + { + $search = [ + '@self' => [ + 'register' => (int) $register->getId(), + 'schema' => (int) $schema->getId(), + 'slug' => $slug, + ], + '_limit' => 1, + ]; + + try { + $results = $this->objectService->searchObjects(query: $search, _rbac: false, _multitenancy: false); + } catch (\Throwable $e) { + return null; + } + + if (is_array($results) === false || count($results) === 0) { + return null; + } + + $existing = $results[0]; + if ($existing instanceof ObjectEntity) { + $existing = $existing->jsonSerialize(); + } + + if (is_array($existing) === false) { + return null; + } + + $uuid = $existing['@self']['id'] ?? $existing['id'] ?? null; + if (is_string($uuid) === true && $uuid !== '') { + return $uuid; + } + + return null; + }//end findExistingSeedUuid() + /** * Process workflow deployment during import (Phase 2). * @@ -2149,7 +2837,7 @@ private function processWorkflowDeployment( * * @return array Updated result array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-86 + * @spec openspec/specs/workflow-in-import/spec.md#requirement-schema-hook-wiring-during-import */ private function processWorkflowHookWiring( array $workflows, @@ -2277,7 +2965,7 @@ static function (array $hook) use ($hookEntry): bool { * @SuppressWarnings(PHPMD.CyclomaticComplexity) Configuration lookup and metadata mapping has many branches * @SuppressWarnings(PHPMD.ExcessiveMethodLength) App import with entity tracking requires detailed logic * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-9 + * @spec openspec/specs/data-import-export/spec.md */ public function importFromApp(string $appId, array $data, string $version, bool $force=false): array { @@ -2564,7 +3252,7 @@ public function importFromApp(string $appId, array $data, string $version, bool }//end if // **AUTO-REGISTER CREATION (runtime-schema-api / data-import-export spec)**: - // When a runtime caller (e.g. OpenBuilt's schema editor) imports an OAS + // When a runtime caller (e.g. OpenBuild's schema editor) imports an OAS // marked as `x-openregister.type=application`, the installer-time `repair` // step that normally provisions a Register row does NOT run. Without this // step, the smoke-test foot-gun reappears: schemas exist but no Register @@ -2825,27 +3513,46 @@ private function autoCreateRegisterIfApplication( * @SuppressWarnings(PHPMD.CyclomaticComplexity) File path resolution has multiple fallback conditions * @SuppressWarnings(PHPMD.NPathComplexity) Path resolution and JSON parsing have multiple outcomes * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function importFromFilePath(string $appId, string $filePath, string $version, bool $force=false): array { try { - // Resolve the file path relative to Nextcloud root. - // Try multiple resolution strategies. - $fullPath = $this->appDataPath.'/../../../'.$filePath; - $fullPath = realpath($fullPath); + // SEC-SVC-7: contain the resolved file inside the Nextcloud root. + // Reject absolute paths and any path-traversal sequence up front so + // a crafted $filePath (e.g. '../../etc/passwd') cannot escape the + // intended base directory. + if (str_starts_with($filePath, '/') === true + || str_contains($filePath, '..') === true + || preg_match('/[\x00-\x1f]/', $filePath) === 1 + ) { + throw new Exception("Invalid configuration file path: {$filePath}"); + } + + // Establish the allowed base directory (Nextcloud root). + $baseDir = realpath($this->appDataPath.'/../../../'); + if ($baseDir === false) { + $baseDir = realpath('/var/www/html'); + } + + // Resolve the file path relative to the Nextcloud root. + $fullPath = realpath($this->appDataPath.'/../../../'.$filePath); // If realpath fails, try direct path from Nextcloud root. if ($fullPath === false) { - $fullPath = '/var/www/html/'.$filePath; - // Normalize the path. - $fullPath = str_replace('//', '/', $fullPath); + $fullPath = realpath('/var/www/html/'.$filePath); } if ($fullPath === false || file_exists($fullPath) === false) { throw new Exception("Configuration file not found: {$filePath}"); } + // Final containment check: the resolved real path MUST live under + // the allowed base directory. + if ($baseDir === false || str_starts_with($fullPath, $baseDir.'/') === false) { + throw new Exception("Configuration file is outside the allowed directory: {$filePath}"); + } + // Read the file contents. $jsonContent = file_get_contents($fullPath); if ($jsonContent === false) { @@ -2910,7 +3617,7 @@ public function importFromFilePath(string $appId, string $filePath, string $vers * @SuppressWarnings(PHPMD.CyclomaticComplexity) Entity ID collection and metadata mapping has many branches * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Configuration tracking involves detailed entity management * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-4 + * @spec openspec/specs/data-import-export/spec.md */ public function createOrUpdateConfiguration( array $data, @@ -3131,12 +3838,31 @@ private function importSeedData( $result['relatedNotes'] = ($result['relatedNotes'] ?? 0); $result['relatedTasks'] = ($result['relatedTasks'] ?? 0); + // Ensure the skipped-entity observability map exists even when this + // method is invoked with a result array that predates it. + $result['skipped'] = ($result['skipped'] ?? []); + $result['skipped']['seedObjects'] = ($result['skipped']['seedObjects'] ?? 0); + // Determine target register for seedData objects. - // SeedData should go into the first register defined in the configuration. + // Prefer the registers imported IN THIS RUN ($this->registersMap): on a + // FIRST import the configuration entity's registers list is only + // persisted after importFromJson returns, so reading it here yielded + // "register 0" and every seed object failed with "Cannot insert object + // without register and schema context" (verified live with the DSAR + // policy-pack register). Fall back to the configuration's recorded + // registers for the version-equal "checking seedData" re-import path, + // where the map is empty but the configuration is populated. $targetRegister = null; - $registerIds = $configuration->getRegisters(); - if (empty($registerIds) === false) { - $targetRegister = $this->registerMapper->find($registerIds[0], _multitenancy: false, _rbac: false); + $freshRegisters = array_values($this->registersMap); + if ($freshRegisters !== []) { + $targetRegister = $freshRegisters[0]; + } + + if ($targetRegister === null) { + $registerIds = $configuration->getRegisters(); + if (empty($registerIds) === false) { + $targetRegister = $this->registerMapper->find($registerIds[0], _multitenancy: false, _rbac: false); + } } $targetRegisterId = 0; @@ -3193,7 +3919,6 @@ private function importSeedData( $schema = $this->schemaMapper->find( id: $schemaSlug, _extend: [], - published: null, _rbac: false, _multitenancy: false ); @@ -3206,13 +3931,17 @@ private function importSeedData( 'schemaApp' => $schema->getApplication(), ] ); - } catch (\OCP\AppFramework\Db\DoesNotExistException | ValidationException $e) { + } catch (\Throwable $e) { + // PER-ENTITY RESILIENCE: any failure resolving this schema (not + // found, validation, or otherwise) skips only this schema's seed + // objects with a warning - never the rest of the seed import. $this->logger->warning( - message: "[ImportHandler] Skipping seed data for schema '{$schemaSlug}' - schema not found", + message: "[ImportHandler] Skipping seed data for schema '{$schemaSlug}' - schema not resolvable: ".$e->getMessage(), context: [ 'file' => __FILE__, 'line' => __LINE__, 'appId' => $appId, + 'error' => $e->getMessage(), 'availableSchemasInMap' => array_keys($this->schemasMap), ] ); @@ -3518,13 +4247,16 @@ private function importSeedData( result: $result ); } - } catch (Exception $e) { - $this->logger->error( - message: "[ImportHandler] Failed to import seed for '{$schemaSlug}': ".$e->getMessage(), + } catch (\Throwable $e) { + // PER-ENTITY RESILIENCE: skip this seed object, keep the rest. + $result['skipped']['seedObjects']++; + $this->logger->warning( + message: "[ImportHandler] Skipping seed object for '{$schemaSlug}' - import failed: ".$e->getMessage(), context: [ 'file' => __FILE__, 'line' => __LINE__, - 'objectData' => $objectData, + 'schemaSlug' => $schemaSlug, + 'objectSlug' => $objectSlug ?? null, 'error' => $e->getMessage(), ] ); diff --git a/lib/Service/Configuration/PreviewHandler.php b/lib/Service/Configuration/PreviewHandler.php index dd23add9f7..506e230c8e 100644 --- a/lib/Service/Configuration/PreviewHandler.php +++ b/lib/Service/Configuration/PreviewHandler.php @@ -129,7 +129,7 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multi-component preview has many entity type conditions * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Full preview involves registers, schemas, objects, and metadata * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter */ public function previewConfigurationChanges(Configuration $configuration): array|JSONResponse { @@ -238,7 +238,7 @@ public function previewConfigurationChanges(Configuration $configuration): array * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Register preview has multiple version comparison branches * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter */ public function previewRegisterChange(string $slug, array $registerData): array { @@ -386,7 +386,7 @@ private function previewSchemaChange(string $slug, array $schemaData): array * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter */ private function previewObjectChange(array $objectData, array $registerSlugToId, array $schemaSlugToId): array { @@ -408,7 +408,7 @@ private function previewObjectChange(array $objectData, array $registerSlugToId, * @psalm-suppress UnusedParam Parameters will be used when method is fully implemented * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter */ public function compareArrays(array $current, array $proposed, string $prefix=''): array { @@ -430,7 +430,7 @@ public function compareArrays(array $current, array $proposed, string $prefix='' * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter */ public function importConfigurationWithSelection(Configuration $_configuration, array $_selection): array { diff --git a/lib/Service/Configuration/UploadHandler.php b/lib/Service/Configuration/UploadHandler.php index c287e043f4..75e5d12e40 100644 --- a/lib/Service/Configuration/UploadHandler.php +++ b/lib/Service/Configuration/UploadHandler.php @@ -86,7 +86,7 @@ public function __construct( * message?: 'Expected only 1 file.', 'Content-Type'?: string}, * array>|array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ public function getUploadedJson(array $data, ?array $uploadedFiles): array|JSONResponse { @@ -132,7 +132,7 @@ public function getUploadedJson(array $data, ?array $uploadedFiles): array|JSONR * * @SuppressWarnings(PHPMD.StaticAccess) Yaml::parse is standard Symfony Yaml pattern * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function decode(string $data, ?string $type): ?array { @@ -173,7 +173,7 @@ private function decode(string $data, ?string $type): ?array * * @return array The converted array data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function ensureArrayStructure(mixed $data): array { @@ -206,7 +206,7 @@ private function ensureArrayStructure(mixed $data): array * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function getJSONfromFile(array $uploadedFile, ?string $_type=null): array|JSONResponse { @@ -243,7 +243,7 @@ private function getJSONfromFile(array $uploadedFile, ?string $_type=null): arra * * @psalm-return JSONResponse<400, array{error: string, 'Content-Type'?: string}, array>|array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function getJSONfromURL(string $url): array|JSONResponse { @@ -281,7 +281,7 @@ private function getJSONfromURL(string $url): array|JSONResponse * * @psalm-return JSONResponse<400, array{error: 'Failed to decode JSON input'}, array>|array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function getJSONfromBody(array | string $phpArray): array|JSONResponse { diff --git a/lib/Service/ConfigurationService.php b/lib/Service/ConfigurationService.php index 690914b212..6d31fd339c 100644 --- a/lib/Service/ConfigurationService.php +++ b/lib/Service/ConfigurationService.php @@ -568,11 +568,18 @@ public function importFromFilePath(string $appId, string $filePath, string $vers */ public function importFromApp(string $appId, array $data, string $version, bool $force=false): array { - return $this->getImportHandler()->importFromApp( - appId: $appId, - data: $data, - version: $version, - force: $force + // App config imports are code-initiated (the data is the app's own + // shipped register config) but frequently run without a user session: + // app boot happens before session resolution and webcron never has + // one. Run them as a scoped system operation so RBAC treats them like + // the CLI cron path instead of denying them as anonymous every boot. + return SystemOperationContext::run( + fn (): array => $this->getImportHandler()->importFromApp( + appId: $appId, + data: $data, + version: $version, + force: $force + ) ); }//end importFromApp() diff --git a/lib/Service/ContactMatchingService.php b/lib/Service/ContactMatchingService.php index 986880dd1e..2f8fcee0ad 100644 --- a/lib/Service/ContactMatchingService.php +++ b/lib/Service/ContactMatchingService.php @@ -188,7 +188,7 @@ public function __construct( * * @return array The match results with confidence 1.0 * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @spec openspec/specs/contacts-actions/spec.md */ public function matchByEmail(string $email): array { @@ -252,7 +252,7 @@ static function (array $match): array { * * @return array The match results * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @spec openspec/specs/contacts-actions/spec.md */ public function matchByName(?string $name): array { @@ -316,7 +316,7 @@ static function (string $part): bool { * * @return array The match results with confidence 0.5 * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @spec openspec/specs/contacts-actions/spec.md */ public function matchByOrganization(?string $organization): array { @@ -380,7 +380,7 @@ static function (array $match): array { * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @spec openspec/specs/contacts-actions/spec.md */ public function matchContact( string $email, @@ -453,7 +453,7 @@ static function (array $a, array $b): int { * * @return array Associative array of schema title => count * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @spec openspec/specs/contacts-actions/spec.md */ public function getRelatedObjectCounts(array $matches): array { @@ -477,7 +477,7 @@ public function getRelatedObjectCounts(array $matches): array * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @spec openspec/specs/contacts-actions/spec.md */ public function invalidateCache(string $email): void { @@ -501,7 +501,7 @@ public function invalidateCache(string $email): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-3 + * @spec openspec/specs/contacts-actions/spec.md */ public function invalidateCacheForObject(array $object): void { diff --git a/lib/Service/ContactService.php b/lib/Service/ContactService.php index e691fc9e1b..aa93854829 100644 --- a/lib/Service/ContactService.php +++ b/lib/Service/ContactService.php @@ -27,6 +27,7 @@ use OCA\DAV\CardDAV\CardDavBackend; use OCA\OpenRegister\Db\ContactLink; use OCA\OpenRegister\Db\ContactLinkMapper; +use OCP\IURLGenerator; use OCP\IUserSession; use Psr\Log\LoggerInterface; use Sabre\VObject\Reader; @@ -37,9 +38,15 @@ * @category Service * @package OCA\OpenRegister\Service * - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Class exposes get/link/unlink/list for contact-to-object bindings and an internal vCard enrichment path; complexity is distributed across multiple private helpers and is not reducible without splitting the dual-storage (vCard + link table) abstraction. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Depends on ContactLinkMapper, CardDavBackend, IUserSession, LoggerInterface, DateTime, and the Sabre VObject Reader — each is a required integration point for the dual CardDAV+link-table storage strategy. - * @SuppressWarnings(PHPMD.StaticAccess) Sabre\VObject\Reader::read() is a static factory; Sabre provides no injectable alternative in the library. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Class exposes get/link/unlink/list for + * contact-to-object bindings and an internal vCard enrichment path; complexity is distributed across + * multiple private helpers and is not reducible without splitting the dual-storage + * (vCard + link table) abstraction. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Depends on ContactLinkMapper, CardDavBackend, + * IUserSession, LoggerInterface, DateTime, and the Sabre VObject Reader — each is a required + * integration point for the dual CardDAV+link-table storage strategy. + * @SuppressWarnings(PHPMD.StaticAccess) Sabre\VObject\Reader::read() is a static factory; Sabre + * provides no injectable alternative in the library. */ class ContactService { @@ -72,6 +79,13 @@ class ContactService */ private readonly LoggerInterface $logger; + /** + * URL generator (webroot-aware deep links). + * + * @var IURLGenerator + */ + private readonly IURLGenerator $urlGenerator; + /** * Constructor. * @@ -79,6 +93,7 @@ class ContactService * @param CardDavBackend $cardDavBackend CardDAV backend * @param IUserSession $userSession User session * @param LoggerInterface $logger Logger + * @param IURLGenerator $urlGenerator URL generator * * @return void */ @@ -86,12 +101,14 @@ public function __construct( ContactLinkMapper $contactLinkMapper, CardDavBackend $cardDavBackend, IUserSession $userSession, - LoggerInterface $logger + LoggerInterface $logger, + IURLGenerator $urlGenerator ) { $this->contactLinkMapper = $contactLinkMapper; $this->cardDavBackend = $cardDavBackend; $this->userSession = $userSession; $this->logger = $logger; + $this->urlGenerator = $urlGenerator; }//end __construct() /** @@ -134,7 +151,7 @@ public function __construct( * * @return array{results: array, total: int} * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function getContactsForObject(string $objectUuid): array { @@ -188,6 +205,15 @@ function (ContactLink $link) use ($now): array { $row['avatarUrl'] = $vfields['avatarUrl']; }//end if + // Deep-link to the specific contact in the Contacts app. + $deepLink = $this->buildContactDeepLink( + addressbookId: $link->getAddressbookId(), + contactUid: $link->getContactUid() + ); + if ($deepLink !== null) { + $row['url'] = $deepLink; + } + return $row; }, $links @@ -196,6 +222,54 @@ function (ContactLink $link) use ($now): array { return ['results' => $results, 'total' => $total]; }//end getContactsForObject() + /** + * Build a webroot-aware deep-link to a specific contact. + * + * Nextcloud Contacts (8.x) opens a contact at + * `/apps/contacts/All contacts/{token}` where `token` is the + * base64url-encoded `"{uid}~{addressbookUri}"`. The addressbook URI is + * resolved from the numeric addressbook id via the CardDAV backend. + * + * Returns null (record gets no `url`) when the uid is missing or the + * addressbook can no longer be resolved, rather than a broken link. + * + * @param int|null $addressbookId The numeric CardDAV addressbook id. + * @param string|null $contactUid The vCard UID of the contact. + * + * @return string|null The deep-link URL, or null when not resolvable. + */ + private function buildContactDeepLink(?int $addressbookId, ?string $contactUid): ?string + { + if ($contactUid === null || $contactUid === '' || $addressbookId === null) { + return null; + } + + try { + $addressbook = $this->cardDavBackend->getAddressBookById($addressbookId); + if (is_array($addressbook) === false) { + return null; + } + + $addressbookUri = ($addressbook['uri'] ?? null); + if ($addressbookUri === null || $addressbookUri === '') { + return null; + } + + // NC Contacts opens a contact via `atob(routeParam)`, i.e. STANDARD + // base64 of "{uid}~{addressbookUri}" (matches its own generated + // links, e.g. base64("admin~z-server-generated--system")). The + // token is URL-encoded so any `+`/`/`/`=` survive as a single path + // segment; NC routing decodes it back to standard base64 before atob. + $token = rawurlencode(base64_encode($contactUid.'~'.$addressbookUri)); + $base = $this->urlGenerator->linkToRoute('contacts.page.index'); + + return rtrim($base, '/').'/'.rawurlencode('All contacts').'/'.$token; + } catch (\Throwable $e) { + $this->logger->debug('Failed to build contact deep-link: '.$e->getMessage()); + return null; + }//end try + }//end buildContactDeepLink() + /** * Resolve the supplementary vCard fields (`phone`, `org`, `avatarUrl`) * for a link row. @@ -211,9 +285,15 @@ function (ContactLink $link) use ($now): array { * * @return array{phone: ?string, org: ?string, avatarUrl: ?string} * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each vCard property (TEL, ORG, PHOTO) requires distinct handling: TEL is iterable or scalar, PHOTO may be URI/data-URL/raw bytes requiring format detection; collapsing branches loses semantic specificity. - * @SuppressWarnings(PHPMD.NPathComplexity) TEL iterable vs scalar + PHOTO URI vs data vs bytes + contactUid fallback produce many distinct execution paths; all are in-method because extracting each to a helper would scatter the single "resolve phone+org+avatar" contract. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) The method resolves phone, org, and avatar from vCard in one pass; splitting into three sub-helpers would require re-reading and re-parsing the vCard three times or passing the parsed vCard object across helpers. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each vCard property (TEL, ORG, PHOTO) requires + * distinct handling: TEL is iterable or scalar, PHOTO may be URI/data-URL/raw bytes requiring + * format detection; collapsing branches loses semantic specificity. + * @SuppressWarnings(PHPMD.NPathComplexity) TEL iterable vs scalar + PHOTO URI vs data vs bytes + + * contactUid fallback produce many distinct execution paths; all are in-method because extracting + * each to a helper would scatter the single "resolve phone+org+avatar" contract. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) The method resolves phone, org, and avatar from + * vCard in one pass; splitting into three sub-helpers would require re-reading and re-parsing the + * vCard three times or passing the parsed vCard object across helpers. */ private function extractVcardFields(?int $addressbookId, ?string $contactUri, ?string $contactUid): array { @@ -369,13 +449,17 @@ private function extractVcardFields(?int $addressbookId, ?string $contactUri, ?s * * @throws Exception If the contact does not exist. * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Single switch on upsert state — extracting a sub-method + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Single switch on upsert state — extracting a sub-method * doesn't add clarity and would split the vCard hydration * from the DB write that consumes it. - * @SuppressWarnings(PHPMD.NPathComplexity) insert vs update branches plus optional role/schemaId null-checks plus best-effort persist error path expand NPath; each path serves a distinct upsert variant in the idempotent link contract. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) linkContact() covers card lookup, user auth, vCard hydration, existence check, and insert-or-update in sequence; splitting would scatter the idempotent upsert contract across multiple methods. + * @SuppressWarnings(PHPMD.NPathComplexity) insert vs update branches plus optional role/schemaId + * null-checks plus best-effort persist error path expand NPath; each path serves a distinct upsert + * variant in the idempotent link contract. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) linkContact() covers card lookup, user auth, vCard + * hydration, existence check, and insert-or-update in sequence; splitting would scatter the + * idempotent upsert contract across multiple methods. */ public function linkContact( string $objectUuid, @@ -502,7 +586,7 @@ public function linkContact( * * @throws Exception If no user or addressbook. * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function createAndLinkContact( string $objectUuid, @@ -594,7 +678,7 @@ public function createAndLinkContact( * * @throws Exception If link not found. * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function updateRole(int $linkId, string $role): ContactLink { @@ -641,7 +725,7 @@ public function updateRole(int $linkId, string $role): ContactLink * * @throws Exception If the link row itself isn't found (404). * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function unlinkContact(int $linkId): void { @@ -718,19 +802,61 @@ public function unlinkContactByUid(string $objectUuid, string $contactUid): void * * @return array Array of contact links with object UUIDs and roles. * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-2 + * @spec openspec/specs/contacts-actions/spec.md */ public function getObjectsForContact(string $contactUid): array { + // IDOR guard: only surface object links for contacts that live in the + // caller's own addressbooks. Without this any authenticated user could + // pass an arbitrary (enumerable CardDAV) contact UID and learn which + // OpenRegister objects reference contacts belonging to other users. + $allowedAddressbookIds = $this->currentUserAddressbookIds(); + if ($allowedAddressbookIds === []) { + return []; + } + $links = $this->contactLinkMapper->findByContactUid($contactUid); + return array_values( + array_map( + static function (ContactLink $link): array { + return $link->jsonSerialize(); + }, + array_filter( + $links, + static function (ContactLink $link) use ($allowedAddressbookIds): bool { + return in_array((int) $link->getAddressbookId(), $allowedAddressbookIds, true); + } + ) + ) + ); + }//end getObjectsForContact() + + /** + * Collect the addressbook IDs owned by the current session user. + * + * Used to scope contact-link reads to the caller's own addressbooks so a + * user cannot resolve links for contacts they do not own. + * + * @return array Addressbook IDs, or [] when anonymous / none. + */ + private function currentUserAddressbookIds(): array + { + $user = $this->userSession->getUser(); + if ($user === null) { + return []; + } + + $principal = 'principals/users/'.$user->getUID(); + $addressbooks = $this->cardDavBackend->getAddressBooksForUser($principal); + return array_map( - static function (ContactLink $link): array { - return $link->jsonSerialize(); + static function (array $addressbook): int { + return (int) $addressbook['id']; }, - $links + $addressbooks ); - }//end getObjectsForContact() + }//end currentUserAddressbookIds() /** * Delete all contact links for an object (cleanup). @@ -739,7 +865,7 @@ static function (ContactLink $link): array { * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ public function deleteLinksForObject(string $objectUuid): void { @@ -774,7 +900,7 @@ public function deleteLinksForObject(string $objectUuid): void * * @return array|null Addressbook data or null. * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-1 + * @spec openspec/specs/contacts-actions/spec.md */ private function findUserAddressbook(): ?array { diff --git a/lib/Service/ContentScanService.php b/lib/Service/ContentScanService.php new file mode 100644 index 0000000000..67f0d2df1e --- /dev/null +++ b/lib/Service/ContentScanService.php @@ -0,0 +1,348 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use Psr\Log\LoggerInterface; + +/** + * Dependency-free heuristic content scanner for untrusted text. + */ +class ContentScanService +{ + + /** + * Verdict: no known-bad pattern matched (NOT a proof of safety). + * + * @var string + */ + public const SEVERITY_CLEAN = 'clean'; + + /** + * Verdict: matched a pattern that warrants a human review before trust. + * + * @var string + */ + public const SEVERITY_SUSPICIOUS = 'suspicious'; + + /** + * Verdict: matched a pattern that must not be auto-trusted. + * + * @var string + */ + public const SEVERITY_DANGEROUS = 'dangerous'; + + /** + * Cap on how much content is scanned, to bound worst-case regex cost on a hostile + * megabyte-sized payload. Content beyond this is ignored (and flagged as truncated). + * + * @var int + */ + private const MAX_SCAN_BYTES = 262144; + + /** + * Constructor. + * + * @param LoggerInterface $logger PSR-3 logger. + */ + public function __construct( + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Scan a body of text (optionally with structured metadata) for dangerous patterns. + * + * @param string $content The primary text to scan (e.g. a skill body). + * @param array $metadata Optional structured metadata to fold in (e.g. + * a skill's frontmatter); scalar leaves are scanned. + * + * @return array{safe: bool, severity: string, findings: array>, scannedBytes: int, truncated: bool} + * The scan report. `safe` is true only when severity is `clean`. + */ + public function scan(string $content, array $metadata=[]): array + { + $haystack = $content; + $flattened = $this->flattenMetadata(metadata: $metadata); + if ($flattened !== '') { + $haystack .= "\n".$flattened; + } + + $truncated = false; + if (strlen($haystack) > self::MAX_SCAN_BYTES) { + $haystack = substr($haystack, 0, self::MAX_SCAN_BYTES); + $truncated = true; + } + + $findings = []; + $normalised = $this->normalise(text: $haystack); + foreach ($this->rules() as $rule) { + if (preg_match($rule['pattern'], $normalised, $matches) !== 1) { + continue; + } + + $findings[] = [ + 'category' => $rule['category'], + 'severity' => $rule['severity'], + 'reason' => $rule['reason'], + 'excerpt' => $this->excerpt(match: (string) ($matches[0] ?? '')), + ]; + }//end foreach + + $severity = $this->worstSeverity(findings: $findings); + + if ($severity !== self::SEVERITY_CLEAN) { + $this->logger->info( + '[ContentScanService] content scan flagged patterns', + ['severity' => $severity, 'findingCount' => count($findings)] + ); + } + + return [ + 'safe' => ($severity === self::SEVERITY_CLEAN), + 'severity' => $severity, + 'findings' => $findings, + 'scannedBytes' => strlen($haystack), + 'truncated' => $truncated, + ]; + + }//end scan() + + /** + * The heuristic rule table. Each rule is a case-insensitive regex plus a category, + * a severity, and a human-readable reason. Ordered dangerous-first for readability; + * the final verdict is the worst matched severity regardless of order. + * + * @return array + */ + private function rules(): array + { + $dangerous = self::SEVERITY_DANGEROUS; + $suspicious = self::SEVERITY_SUSPICIOUS; + + return [ + // Download-and-execute: the single most common skill/prompt supply-chain attack. + [ + 'pattern' => '/\b(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba)?sh\b/i', + 'category' => 'remote-code', + 'severity' => $dangerous, + 'reason' => 'Pipes a remote download straight into a shell (curl|bash).', + ], + [ + 'pattern' => '/\b(?:ba)?sh\b\s*<\s*\(\s*(?:curl|wget)\b/i', + 'category' => 'remote-code', + 'severity' => $dangerous, + 'reason' => 'Executes a remotely fetched script via process substitution.', + ], + [ + 'pattern' => '/\b(?:eval|exec|system|passthru|popen|proc_open)\s*\(\s*(?:\$?_?(?:GET|POST|REQUEST)|base64_decode|curl)/i', + 'category' => 'remote-code', + 'severity' => $dangerous, + 'reason' => 'Evaluates dynamically-fetched or request-supplied code.', + ], + [ + 'pattern' => '/(?:iex|invoke-expression)\s*\(\s*(?:new-object\s+net\.webclient|iwr|invoke-webrequest)/i', + 'category' => 'remote-code', + 'severity' => $dangerous, + 'reason' => 'PowerShell download-and-execute (IEX + WebClient).', + ], + // Destructive shell. + [ + 'pattern' => '/\brm\s+-[a-z]*r[a-z]*f[a-z]*\s+(?:\/|~|\$HOME|\*)/i', + 'category' => 'destructive', + 'severity' => $dangerous, + 'reason' => 'Recursive force-delete of a root/home/glob path (rm -rf).', + ], + [ + 'pattern' => '/\b(?:mkfs|dd\s+if=\S+\s+of=\/dev\/|>\s*\/dev\/sd[a-z])\b/i', + 'category' => 'destructive', + 'severity' => $dangerous, + 'reason' => 'Overwrites a raw block device (mkfs/dd/redirect to /dev/sdX).', + ], + [ + 'pattern' => '/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/', + 'category' => 'destructive', + 'severity' => $dangerous, + 'reason' => 'Fork-bomb definition.', + ], + // Credential / data exfiltration. + [ + 'pattern' => '/\b(?:nc|ncat|netcat)\b[^\n]*\s-\w*e\w*\s/i', + 'category' => 'exfiltration', + 'severity' => $dangerous, + 'reason' => 'Netcat with -e (reverse/bind shell).', + ], + [ + 'pattern' => '#(?:/dev/tcp/|/dev/udp/)\d#i', + 'category' => 'exfiltration', + 'severity' => $dangerous, + 'reason' => 'Bash /dev/tcp network socket (reverse shell / exfil).', + ], + [ + 'pattern' => '#(?:cat|cp|curl|scp|tar|base64)\b[^\n]*(?:~|/root|/home/[^\s/]+)?' + .'/\.(?:ssh/id_|aws/credentials|kube/config|netrc|npmrc|docker/config)#i', + 'category' => 'exfiltration', + 'severity' => $dangerous, + 'reason' => 'Reads/copies known credential files (~/.ssh, ~/.aws, .netrc …).', + ], + // Embedded secrets (someone shipping live keys inside skill content). + [ + 'pattern' => '/-----BEGIN\s+(?:RSA|OPENSSH|EC|DSA|PGP)?\s*PRIVATE KEY-----/i', + 'category' => 'embedded-secret', + 'severity' => $dangerous, + 'reason' => 'Embeds a private key block.', + ], + [ + 'pattern' => '/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/', + 'category' => 'embedded-secret', + 'severity' => $dangerous, + 'reason' => 'Embeds an AWS access-key id.', + ], + [ + 'pattern' => '/\b(?:ghp|gho|ghs|ghr)_[A-Za-z0-9]{30,}\b/', + 'category' => 'embedded-secret', + 'severity' => $dangerous, + 'reason' => 'Embeds a GitHub access token.', + ], + // Prompt-injection — the LLM-specific manipulation surface. + [ + 'pattern' => '/\b(?:ignore|disregard|forget)\b[^\n]{0,40}\b(?:previous|prior|above|earlier|all)\b' + .'[^\n]{0,20}\b(?:instruction|prompt|rule|direction)/i', + 'category' => 'prompt-injection', + 'severity' => $suspicious, + 'reason' => 'Instructs the model to ignore prior instructions.', + ], + [ + 'pattern' => '/\b(?:reveal|print|repeat|show|leak|exfiltrate)\b[^\n]{0,30}' + .'\b(?:system prompt|your instructions|initial prompt|api[\s_-]?key|secret)/i', + 'category' => 'prompt-injection', + 'severity' => $suspicious, + 'reason' => 'Attempts to extract the system prompt or secrets.', + ], + [ + 'pattern' => '/<\|im_(?:start|end)\|>\s*system|<\/?(?:system|assistant)>\s*(?:you are|ignore)/i', + 'category' => 'prompt-injection', + 'severity' => $suspicious, + 'reason' => 'Injects a forged system/role turn into the conversation.', + ], + ]; + + }//end rules() + + /** + * Flatten scalar leaves of a metadata array into newline-joined text for scanning. + * + * @param array $metadata The structured metadata. + * + * @return string The concatenated scalar values. + */ + private function flattenMetadata(array $metadata): string + { + $parts = []; + array_walk_recursive( + $metadata, + static function ($leaf) use (&$parts) { + if (is_scalar($leaf) === true) { + $parts[] = (string) $leaf; + } + } + ); + + return implode("\n", $parts); + + }//end flattenMetadata() + + /** + * Normalise text to defeat trivial obfuscation before matching: collapse runs of + * whitespace (so `curl | bash` matches) while keeping newlines as boundaries. + * + * @param string $text The raw text. + * + * @return string The normalised text. + */ + private function normalise(string $text): string + { + // Collapse horizontal whitespace runs to a single space; keep newlines. + $collapsed = preg_replace('/[ \t\x0B\f]+/', ' ', $text); + if (is_string($collapsed) === false) { + return $text; + } + + return $collapsed; + + }//end normalise() + + /** + * The worst severity across all findings (clean when there are none). + * + * @param array> $findings The findings. + * + * @return string The worst severity. + */ + private function worstSeverity(array $findings): string + { + $worst = self::SEVERITY_CLEAN; + foreach ($findings as $finding) { + if (($finding['severity'] ?? '') === self::SEVERITY_DANGEROUS) { + return self::SEVERITY_DANGEROUS; + } + + if (($finding['severity'] ?? '') === self::SEVERITY_SUSPICIOUS) { + $worst = self::SEVERITY_SUSPICIOUS; + } + } + + return $worst; + + }//end worstSeverity() + + /** + * A short, single-line, length-bounded excerpt of a matched fragment for the report. + * + * @param string $match The matched substring. + * + * @return string The sanitised excerpt. + */ + private function excerpt(string $match): string + { + $oneLine = preg_replace('/\s+/', ' ', trim($match)); + if (is_string($oneLine) === false) { + $oneLine = ''; + } + + if (strlen($oneLine) > 120) { + $oneLine = substr($oneLine, 0, 117).'…'; + } + + return $oneLine; + + }//end excerpt() +}//end class diff --git a/lib/Service/CospendLinkService.php b/lib/Service/CospendLinkService.php index 7de1081957..83b7e42338 100644 --- a/lib/Service/CospendLinkService.php +++ b/lib/Service/CospendLinkService.php @@ -200,18 +200,20 @@ public function linkProject(string $objectUuid, int $registerId, int $schemaId, throw new Exception('Cospend project not found', 404); } - $link = $this->hydrateLink([ - 'objectUuid' => $objectUuid, - 'registerId' => $registerId, - 'schemaId' => $schemaId, - 'entryType' => self::ENTRY_PROJECT, - 'projectId' => $projectId, - 'billId' => null, - 'name' => (string) ($project['name'] ?? $projectId), - 'amount' => null, - 'currency' => ($project['currency'] ?? null), - 'uid' => $uid, - ]); + $link = $this->hydrateLink( + opts: [ + 'objectUuid' => $objectUuid, + 'registerId' => $registerId, + 'schemaId' => $schemaId, + 'entryType' => self::ENTRY_PROJECT, + 'projectId' => $projectId, + 'billId' => null, + 'name' => (string) ($project['name'] ?? $projectId), + 'amount' => null, + 'currency' => ($project['currency'] ?? null), + 'uid' => $uid, + ] + ); return $this->cospendLinkMapper->insert($link); }//end linkProject() @@ -262,18 +264,20 @@ public function linkBill( $project = $this->fetchProject(projectId: $projectId); $currency = $project['currency'] ?? null; - $link = $this->hydrateLink([ - 'objectUuid' => $objectUuid, - 'registerId' => $registerId, - 'schemaId' => $schemaId, - 'entryType' => self::ENTRY_BILL, - 'projectId' => $projectId, - 'billId' => $billId, - 'name' => (string) ($bill['what'] ?? ($project['name'] ?? $projectId)), - 'amount' => ($bill['amount'] ?? null), - 'currency' => $currency, - 'uid' => $uid, - ]); + $link = $this->hydrateLink( + opts: [ + 'objectUuid' => $objectUuid, + 'registerId' => $registerId, + 'schemaId' => $schemaId, + 'entryType' => self::ENTRY_BILL, + 'projectId' => $projectId, + 'billId' => $billId, + 'name' => (string) ($bill['what'] ?? ($project['name'] ?? $projectId)), + 'amount' => ($bill['amount'] ?? null), + 'currency' => $currency, + 'uid' => $uid, + ] + ); return $this->cospendLinkMapper->insert($link); }//end linkBill() @@ -329,18 +333,20 @@ public function createAndLinkProject( $currencyValue = $cleanCurrency; } - $link = $this->hydrateLink([ - 'objectUuid' => $objectUuid, - 'registerId' => $registerId, - 'schemaId' => $schemaId, - 'entryType' => self::ENTRY_PROJECT, - 'projectId' => $projectId, - 'billId' => null, - 'name' => $name, - 'amount' => null, - 'currency' => $currencyValue, - 'uid' => $uid, - ]); + $link = $this->hydrateLink( + opts: [ + 'objectUuid' => $objectUuid, + 'registerId' => $registerId, + 'schemaId' => $schemaId, + 'entryType' => self::ENTRY_PROJECT, + 'projectId' => $projectId, + 'billId' => null, + 'name' => $name, + 'amount' => null, + 'currency' => $currencyValue, + 'uid' => $uid, + ] + ); return $this->cospendLinkMapper->insert($link); }//end createAndLinkProject() @@ -419,10 +425,26 @@ private function hydrateLink(array $opts): CospendLink $link->setSchemaId((int) $opts['schemaId']); $link->setEntryType((string) $opts['entryType']); $link->setProjectId((string) $opts['projectId']); - $link->setBillId(isset($opts['billId']) ? (int) $opts['billId'] : null); + $billId = null; + if (isset($opts['billId']) === true) { + $billId = (int) $opts['billId']; + } + + $link->setBillId($billId); $link->setName((string) ($opts['name'] ?? '')); - $link->setAmount(isset($opts['amount']) ? (float) $opts['amount'] : null); - $link->setCurrency(isset($opts['currency']) ? (string) $opts['currency'] : null); + + $amount = null; + if (isset($opts['amount']) === true) { + $amount = (float) $opts['amount']; + } + + $currency = null; + if (isset($opts['currency']) === true) { + $currency = (string) $opts['currency']; + } + + $link->setAmount($amount); + $link->setCurrency($currency); $link->setLinkedBy((string) $opts['uid']); $link->setLinkedAt(new DateTime()); @@ -475,8 +497,22 @@ public function getLinkedEntries(string $objectUuid): array $link = $this->refreshLink(link: $link); } - $results[] = $link->jsonSerialize(); - } + $row = $link->jsonSerialize(); + // NC Cospend opens a project at /p/{projectId} and a specific bill at + // /p/{projectId}/b/{billId} (routes cospend.page.indexProject/indexBill). + $projectId = ($row['projectId'] ?? ''); + if ($projectId !== '') { + $billId = ($row['billId'] ?? ''); + $billPath = ''; + if ($billId !== '' && $billId !== null) { + $billPath = '/b/'.rawurlencode((string) $billId); + } + + $row['url'] = '/apps/cospend/p/'.rawurlencode((string) $projectId).$billPath; + } + + $results[] = $row; + }//end foreach return $results; }//end getLinkedEntries() diff --git a/lib/Service/Credential/CredentialAccessDeniedException.php b/lib/Service/Credential/CredentialAccessDeniedException.php new file mode 100644 index 0000000000..444a91a88d --- /dev/null +++ b/lib/Service/Credential/CredentialAccessDeniedException.php @@ -0,0 +1,37 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use RuntimeException; + +/** + * Signals a fail-closed broker guard rejection (maps to a static 403). + */ +class CredentialAccessDeniedException extends RuntimeException +{ +}//end class diff --git a/lib/Service/Credential/CredentialAppTokenService.php b/lib/Service/Credential/CredentialAppTokenService.php new file mode 100644 index 0000000000..8546085581 --- /dev/null +++ b/lib/Service/Credential/CredentialAppTokenService.php @@ -0,0 +1,338 @@ +`; the app + * keeps its own copy). {@see issueToken()} mints a token binding + * `{appId, credentialId, iat, exp}` signed with the app's secret; {@see verify()} + * recomputes the signature against the registered secret for the claimed appId and + * checks expiry, so a forged or expired token is rejected. Signed tokens are the + * HTTP / cross-runtime identity mechanism; trusted SAME-INSTANCE PHP callers pass + * their appId to `CredentialBrokerService::request` directly without a token + * (credential-doriath-leaf design D-G) — the token authenticates claims across a + * trust boundary that an in-process call does not cross. Signing secrets are + * never logged or returned by verify(). + * + * @category Service + * @package OCA\OpenRegister\Service\Credential + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Security\ICredentialsManager; +use OCP\Security\ISecureRandom; + +/** + * Issues and verifies short-lived signed per-app broker tokens. + */ +class CredentialAppTokenService +{ + /** + * System-scoped vault key prefix for a per-app signing secret. + * + * @var string + */ + private const KEY_PREFIX = 'openregister/credential-app-key/'; + + /** + * Token lifetime in seconds (short-lived; a fresh token is minted per broker call). + * + * @var int + */ + private const TOKEN_TTL = 300; + + /** + * Length (in characters) of a generated per-app signing secret. + * + * @var int + */ + private const SECRET_LENGTH = 64; + + /** + * Constructor. + * + * @param ICredentialsManager $credentialsManager The NC vault holding per-app signing secrets (system-scoped). + * @param ISecureRandom $secureRandom CSPRNG for generating signing secrets. + * @param ITimeFactory $timeFactory Clock source for iat/exp (injectable for testability). + * + * @return void + */ + public function __construct( + private readonly ICredentialsManager $credentialsManager, + private readonly ISecureRandom $secureRandom, + private readonly ITimeFactory $timeFactory, + ) { + }//end __construct() + + /** + * Register (or rotate) an app's signing secret and return it ONCE. + * + * Generates a fresh CSPRNG secret, stores it system-scoped keyed by appId, and + * returns the plaintext to the caller. The secret is returned only here; it is + * never retrievable through any read API and never logged. + * + * ROTATES on every call. Automated callers (the D-G manifest auto-onboarding + * in `GenericInitializeSettings`) MUST therefore guard with + * {@see isRegistered()} so an auto-run never silently invalidates an app's + * held copy — rotation stays an explicit admin action via + * `POST /api/credentials/apps/{appId}/register`. + * + * @param string $appId The consuming app's id. + * + * @return string The newly generated signing secret (shown once). + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + public function registerApp(string $appId): string + { + $secret = $this->secureRandom->generate( + self::SECRET_LENGTH, + (ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS) + ); + + $this->credentialsManager->store('', self::KEY_PREFIX.$appId, $secret); + + return $secret; + }//end registerApp() + + /** + * Whether an app currently has a registered signing secret. + * + * @param string $appId The consuming app's id. + * + * @return bool True when a signing secret is registered for the app. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function isRegistered(string $appId): bool + { + return ($this->lookupSecret(appId: $appId) !== null); + }//end isRegistered() + + /** + * Issue a short-lived signed token binding an app id to a credential id. + * + * CONSUMER SEAM — deliberately has no caller inside OpenRegister. + * + * This is the CLIENT half of the broker's token protocol. OpenRegister is the + * VERIFIER, never the issuer: `CredentialController::brokerRequest()` calls + * {@see verify()} on the `X-Credential-Token` header presented by the caller. + * The ISSUER is the CONSUMING APP, which holds the per-app signing secret handed + * to it exactly once by {@see registerApp()} (`POST /api/credentials/apps/{appId}/register`). + * ADR-004 Rule 2 states the contract: "Apps present signed, per-app, expiring + * tokens." OpenRegister minting a token for itself would be a category error — it + * would *assert* the appId instead of *proving* it, which is precisely the control + * this token exists to provide. + * + * Who is meant to call it: + * - Same-instance PHP consumer apps that reach the broker over HTTP, via + * `Server::get(CredentialAppTokenService::class)->issueToken(...)`. (A trusted + * in-process caller does NOT need a token: it passes its appId straight to + * `CredentialBrokerService::request` — the token authenticates claims across a + * trust boundary that an in-process call never crosses.) + * - Cross-runtime consumers (ExApps, external services) re-implement this exact + * HMAC construction in their own language; this method is the reference + * implementation of the token format that {@see verify()} accepts, and the + * issue→verify round-trip tests are what guard that format from drifting. + * + * @param string $appId The consuming app's id (must be registered). + * @param string $credentialId The credential UUID the app intends to use. + * @param string|null $method Optional HTTP method to bind the token to a single request. + * @param string|null $path Optional request path to bind the token to a single request. + * + * @return string A `payload.signature` token (base64url parts). + * + * @throws CredentialAccessDeniedException When the app has no registered signing secret. + * + * @orphaned-write-capability exclude Consumer-facing seam: OpenRegister is the token VERIFIER + * (CredentialController::brokerRequest -> verify()); the CONSUMING APP is the issuer, holding + * the per-app secret from registerApp(). ADR-004 Rule 2. An in-repo caller would defeat the control. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function issueToken(string $appId, string $credentialId, ?string $method=null, ?string $path=null): string + { + $secret = $this->lookupSecret(appId: $appId); + if ($secret === null) { + throw new CredentialAccessDeniedException( + message: 'App "'.$appId.'" has no registered signing secret' + ); + } + + $now = $this->timeFactory->getTime(); + $payload = [ + 'appId' => $appId, + 'credentialId' => $credentialId, + 'iat' => $now, + 'exp' => ($now + self::TOKEN_TTL), + ]; + + // Optional request binding (harden-credential-token-binding): bind the + // token to a specific method + path so a captured token cannot be replayed + // against a *different* allow-rule-permitted call within its TTL. Opt-in — + // tokens minted without method/path stay unbound (backward-compatible). + if ($method !== null && $path !== null) { + $payload['req'] = $this->requestDigest(method: $method, path: $path); + } + + $payloadJson = json_encode($payload); + if ($payloadJson === false) { + throw new CredentialAccessDeniedException(message: 'Failed to encode token payload'); + } + + $payloadB64 = rtrim(strtr(base64_encode($payloadJson), '+/', '-_'), '='); + $signature = hash_hmac('sha256', $payloadB64, $secret, true); + $signB64 = rtrim(strtr(base64_encode($signature), '+/', '-_'), '='); + + return $payloadB64.'.'.$signB64; + }//end issueToken() + + /** + * Verify a token and return its authenticated `{appId, credentialId}` claims. + * + * The signature is recomputed against the registered secret for the appId named + * IN the payload, so a token cannot be forged without that secret. Expiry is + * enforced. Any structural, signature, or expiry failure throws (fail-closed); + * the caller maps every failure to a single static 403. + * + * @param string $token The `payload.signature` token to verify. + * @param string|null $method The actual request HTTP method, matched against a request-bound token. + * @param string|null $path The actual request path, matched against a request-bound token. + * + * @return array{appId: string, credentialId: string} The authenticated claims. + * + * @throws CredentialAccessDeniedException On any malformed / forged / expired token. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function verify(string $token, ?string $method=null, ?string $path=null): array + { + $parts = explode('.', $token); + if (count($parts) !== 2 || $parts[0] === '' || $parts[1] === '') { + throw new CredentialAccessDeniedException(message: 'Malformed broker token'); + } + + [$payloadB64, $signB64] = $parts; + $payload = $this->decodeClaims(payloadB64: $payloadB64); + + $appId = (string) $payload['appId']; + $secret = $this->lookupSecret(appId: $appId); + if ($secret === null) { + throw new CredentialAccessDeniedException(message: 'Unknown app "'.$appId.'" in broker token'); + } + + $expected = rtrim(strtr(base64_encode(hash_hmac('sha256', $payloadB64, $secret, true)), '+/', '-_'), '='); + if (hash_equals($expected, $signB64) === false) { + throw new CredentialAccessDeniedException(message: 'Bad broker token signature'); + } + + if ((int) $payload['exp'] <= $this->timeFactory->getTime()) { + throw new CredentialAccessDeniedException(message: 'Expired broker token'); + } + + // Request binding (harden-credential-token-binding): a token bound to a + // specific method+path is valid ONLY for that call — the actual method and + // path MUST match, so a captured token cannot be replayed against a + // different allow-rule-permitted call. Fail-closed: a bound token verified + // without a method/path is rejected. + if (isset($payload['req']) === true) { + $matches = ($method !== null && $path !== null + && hash_equals( + (string) $payload['req'], + $this->requestDigest(method: $method, path: $path) + ) === true); + if ($matches === false) { + throw new CredentialAccessDeniedException(message: 'Broker token request-binding mismatch'); + } + } + + return [ + 'appId' => $appId, + 'credentialId' => (string) $payload['credentialId'], + ]; + }//end verify() + + /** + * Compute the request-binding digest for a method + path. + * + * Normalises the method to upper-case and the path to a single leading slash + * so the digest is stable across trivially-different representations of the + * same call. + * + * @param string $method The HTTP method (e.g. `GET`, `PUT`). + * @param string $path The provider-relative path. + * + * @return string A SHA-256 hex digest of the normalised `METHOD /path`. + */ + private function requestDigest(string $method, string $path): string + { + return hash('sha256', strtoupper($method).' /'.ltrim($path, '/')); + }//end requestDigest() + + /** + * Decode and structurally validate a token's base64url payload segment. + * + * @param string $payloadB64 The base64url-encoded payload segment. + * + * @return array{appId: string, credentialId: string, exp: int, req?: string} The validated claims. + * + * @throws CredentialAccessDeniedException When the payload is unreadable or malformed. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function decodeClaims(string $payloadB64): array + { + $payloadJson = base64_decode(strtr($payloadB64, '-_', '+/'), true); + if ($payloadJson === false) { + throw new CredentialAccessDeniedException(message: 'Malformed broker token payload'); + } + + $payload = json_decode($payloadJson, true); + if (is_array($payload) === false + || is_string(($payload['appId'] ?? null)) === false + || is_string(($payload['credentialId'] ?? null)) === false + || is_int(($payload['exp'] ?? null)) === false + ) { + throw new CredentialAccessDeniedException(message: 'Malformed broker token claims'); + } + + return $payload; + }//end decodeClaims() + + /** + * Look up an app's registered signing secret, or null when absent. + * + * @param string $appId The consuming app's id. + * + * @return string|null The signing secret, or null. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function lookupSecret(string $appId): ?string + { + $value = $this->credentialsManager->retrieve('', self::KEY_PREFIX.$appId); + if (is_string($value) === true && $value !== '') { + return $value; + } + + return null; + }//end lookupSecret() +}//end class diff --git a/lib/Service/Credential/CredentialBrokerService.php b/lib/Service/Credential/CredentialBrokerService.php new file mode 100644 index 0000000000..94bdcceaf3 --- /dev/null +++ b/lib/Service/Credential/CredentialBrokerService.php @@ -0,0 +1,932 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Service\ObjectService; +use OCA\OpenRegister\Service\OrganisationService; +use OCP\Http\Client\IClientService; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use DateTimeImmutable; +use InvalidArgumentException; +use Throwable; + +/** + * Constrained, host-locked, secret-injecting outbound broker. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) The fail-closed guard chain + * (scope dispatch + owner/membership + allowedApps + provider rules + host-lock) + * is deliberately decomposed into small single-purpose guard methods so each + * security decision is independently auditable; the aggregate weighted method + * count is a by-product of that decomposition, not of tangled logic. + * + * @spec openspec/specs/credential-broker/spec.md + */ +class CredentialBrokerService +{ + /** + * Register slug holding `credential` metadata objects. + * + * @var string + */ + public const REGISTER = 'credential-broker'; + + /** + * Schema slug for `credential` metadata objects. + * + * @var string + */ + public const SCHEMA = 'brokeredcredential'; + + /** + * The personal (owner-scoped) credential scope — the default when absent. + * + * @var string + */ + private const SCOPE_PERSONAL = 'personal'; + + /** + * The organisation (membership-scoped) credential scope. + * + * @var string + */ + private const SCOPE_ORGANISATION = 'organisation'; + + /** + * Constructor. + * + * @param ObjectService $objectService OR object CRUD (loads credential metadata). + * @param CredentialStore $credentialStore Secret store leaf (reads the injected secret). + * @param ProviderCatalogue $catalogue Read-only provider catalogue (host-lock + rules). + * @param IUserSession $userSession Current session (owner guard identity). + * @param IClientService $clientService NC HTTP client factory (the outbound call). + * @param LoggerInterface $logger Logger for secret-free server-side diagnostics. + * @param OrganisationService $organisationService Resolves organisation membership (organisation guard branch). + * + * @return void + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly CredentialStore $credentialStore, + private readonly ProviderCatalogue $catalogue, + private readonly IUserSession $userSession, + private readonly IClientService $clientService, + private readonly LoggerInterface $logger, + private readonly OrganisationService $organisationService, + ) { + }//end __construct() + + /** + * Broker a single constrained outbound call on a credential's behalf. + * + * On the HTTP path `appId` comes ONLY from a verified `X-Credential-Token`. + * In-process same-instance PHP callers pass their `appId` directly without a + * token (design D-G — the token authenticates claims across the HTTP trust + * boundary, which an in-process call does not cross). `actingUserId` (design + * D-K) is honored ONLY when no user session exists: the owner guard then + * evaluates against it; a session identity wins unconditionally, and the + * HTTP controller never forwards it. + * + * @param string $credentialId The `credential` object UUID. + * @param string $appId The authenticated calling app id (verified token claim on HTTP; the + * caller's own app id on the trusted in-process path — never a body field). + * @param string $method The HTTP method (e.g. `GET`). + * @param string $path The provider-relative path (caller supplies ONLY a path, never a full URL). + * @param array $headers Optional extra request headers (the auth header is broker-controlled). + * @param string|null $body Optional raw request body. + * @param string|null $actingUserId Optional asserted user for SESSIONLESS in-process callers only + * (background jobs); ignored whenever a user session exists. + * + * @return array{status: int, headers: array, body: string} The upstream status, headers, and body. + * + * @throws CredentialAccessDeniedException When any guard fails closed (mapped to a static 403). + * @throws CredentialUpstreamException When the outbound call fails at the transport level (mapped to a static 502). + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + public function request( + string $credentialId, + string $appId, + string $method, + string $path, + array $headers=[], + ?string $body=null, + ?string $actingUserId=null + ): array { + $method = strtoupper($method); + $matchPath = $this->normalisePath(path: $path); + + // Guard 1 — access (per-object IDOR): scope-dispatched owner/membership check. + // TRUST BOUNDARY (openregister#450): request() is HTTP-routed (credential#brokerRequest), + // so it MUST NEVER let a caller assert an acting organisation — passing null keeps the + // sessionless organisation admit path (assertOrganisationMember) unreachable from a routed + // request, exactly as the controller passes no actingUserId. A sessionless proxy call + // against an organisation credential therefore still denies; only the non-routed + // resolveInjectable() path may carry an in-process organisation assertion. + $credential = $this->loadAdmittedCredential( + credentialId: $credentialId, + actingUserId: $actingUserId, + actingOrganisationId: null + ); + $data = $credential->jsonSerialize(); + $scope = $this->scopeOf(data: $data); + + // Guard 2 — allowed app. + $this->assertAppAllowed(data: $data, appId: $appId, credentialId: $credentialId); + + // Guard 3 + 4 — provider resolution, allow-rules, host-lock. + $provider = $this->resolveProvider(data: $data, credentialId: $credentialId); + + // An inject-only provider carries no baseUrl/allowRules and MUST NEVER be proxied + // (an unbounded host is exactly what the constrained proxy exists to prevent). Its + // secret is reachable only app-side via resolveInjectable(); fail closed here. + if ($this->isInjectOnly(provider: $provider) === true) { + $this->deny(reason: 'inject-only provider cannot be proxied; use resolveInjectable', credentialId: $credentialId); + } + + $this->assertRuleAllowed(provider: $provider, method: $method, matchPath: $matchPath, credentialId: $credentialId); + $resolvedUrl = $this->resolveAndLockUrl(provider: $provider, path: $path, credentialId: $credentialId); + + // All guards passed — read the secret (from the scope's vault owner) and perform the call. + $secret = $this->credentialStore->get($credentialId, $scope); + if ($secret === null) { + $this->deny(reason: 'no secret stored for credential', credentialId: $credentialId); + } + + $requestHeaders = $this->injectAuth(provider: $provider, headers: $headers, secret: (string) $secret); + + return $this->performCall( + method: $method, + url: $resolvedUrl, + headers: $requestHeaders, + body: $body, + credentialId: $credentialId + ); + }//end request() + + /** + * Resolve the raw secret for an INJECT-ONLY credential, for same-instance app-side injection. + * + * This is the deliberate counterpart to {@see request()} for arbitrary / self-hosted + * targets that cannot be host-locked from the immutable catalogue (an OpenConnector + * Source against a municipality's own API, say). It runs ONLY the two guards that are + * meaningful without a host — Guard 1 (owner / organisation membership, IDOR) and Guard 2 + * (allowedApps) — and then returns the plaintext secret to the trusted in-process caller, + * which injects it itself. It deliberately SKIPS the provider allow-rules and host-lock + * (Guards 3 + 4): there is no host to lock, so those guards do not apply. + * + * The catalogue keeps the two worlds apart. An `inject_only` provider carries no baseUrl + * and no allowRules, so {@see request()} refuses it (it can never become an open proxy). + * A normal host-locked provider (Mollie, GitHub, …) is NOT inject-only, so this method + * returns null for it — its secret stays zero-knowledge inside OR and is only ever + * reachable through the constrained proxy. A caller therefore uses the return value to + * route: a non-null secret means "inject this yourself"; null means "this credential is a + * proxy credential — call request() instead". + * + * The plaintext crosses the process boundary into the calling app (a change from the + * proxy's "secret never leaves OR" posture), so this is a TRUSTED IN-PROCESS PATH only — + * `appId` is the caller's own id, exactly as on the tokenless in-process {@see request()} + * path (design D-G). `actingUserId` follows the same rule as request() (design D-K): + * honored only when there is no user session. + * + * SESSIONLESS ORGANISATION RESOLUTION (openregister#450, ADR-064 Rule 4): an + * `organisation`-scoped INFRASTRUCTURE credential (a source/consumer secret) MUST NOT be + * personal-scoped, yet its trusted in-process consumers — an openconnector migration repair + * step, a background sync job — run with NO user session. Such a caller may assert + * `actingOrganisationId`; the organisation guard then admits iff it MATCHES the credential's + * organisation (see {@see assertOrganisationMember()}). This is honored ONLY without a + * session and, exactly like `actingUserId`, is settable only by in-process code — this + * method is NOT HTTP-routed, so request input can never reach it. + * + * @param string $credentialId The `credential` object UUID. + * @param string $appId The authenticated calling app id (the caller's own id on the in-process path). + * @param string|null $actingUserId Optional asserted user for SESSIONLESS in-process callers; ignored with a session. + * @param string|null $actingOrganisationId Optional asserted organisation for SESSIONLESS in-process callers resolving an + * organisation credential; admits iff it equals the credential's organisation, and + * ignored whenever a user session exists. NEVER settable from request input (this + * method is not routed) — an in-process trust assertion only. + * + * @return string|null The raw secret for an inject-only credential, or null when the credential is a proxy credential. + * + * @throws CredentialAccessDeniedException When Guard 1 or 2 fails, or an inject-only credential has no stored secret. + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + public function resolveInjectable( + string $credentialId, + string $appId, + ?string $actingUserId=null, + ?string $actingOrganisationId=null + ): ?string { + // Guard 1 — access (per-object IDOR): scope-dispatched owner/membership check. + $credential = $this->loadAdmittedCredential( + credentialId: $credentialId, + actingUserId: $actingUserId, + actingOrganisationId: $actingOrganisationId + ); + $data = $credential->jsonSerialize(); + $scope = $this->scopeOf(data: $data); + + // Guard 2 — allowed app. + $this->assertAppAllowed(data: $data, appId: $appId, credentialId: $credentialId); + + // Only inject-only credentials may be resolved app-side; a proxy credential's secret + // stays inside OR — signal that with null so the caller routes to request() instead. + $provider = $this->resolveProvider(data: $data, credentialId: $credentialId); + if ($this->isInjectOnly(provider: $provider) === false) { + return null; + } + + $secret = $this->credentialStore->get($credentialId, $scope); + if ($secret === null) { + $this->deny(reason: 'no secret stored for inject-only credential', credentialId: $credentialId); + } + + return (string) $secret; + }//end resolveInjectable() + + /** + * Mint a credential: persist its metadata object and store its secret to the vault. + * + * The single mint path for the whole instance — the HTTP controller and any + * in-process caller (an openconnector migration repair step folding an inline + * plaintext source secret into the broker, say) go through here, so the metadata + * shape, the vault write, and the atomicity contract are defined exactly once. + * + * SESSIONLESS BY CONSTRUCTION: no `IUserSession` is consulted — the `$owner` is + * an ASSERTION by the trusted caller (the controller passes the session uid; a + * repair step derives it from durable job context, never from request input). + * This mirrors the `actingUserId` posture of {@see request()} / {@see resolveInjectable()} + * (design D-K) and lets a background job mint without an HTTP session. + * + * AUTHORIZATION IS THE CALLER'S: minting is not itself a guarded operation — the + * provider-catalogue validation and the organisation-administrator gate are + * request/authz concerns that live in {@see \OCA\OpenRegister\Controller\CredentialController}. + * This method takes an ALREADY-RESOLVED `$scope` + `$organisation` and trusts them. + * + * ATOMICITY: the metadata object and the vault secret are two stores with no shared + * transaction. When the vault write fails AFTER the object was saved we would be left + * with a metadata object that no secret backs — a credential that looks usable in the + * UI and fails closed at broker time with "no secret stored". Rather than ship that, + * the orphaned object is deleted and the original vault failure is rethrown, so a mint + * either yields a complete credential or yields nothing. A failure to delete the orphan + * is logged (secret-free) and does not mask the original error. + * + * The `$secret` NEVER reaches an OR object, a log line, or the return value — it is + * handed straight to the {@see CredentialStore} leaf and otherwise untouched + * (the CredentialStore contract). + * + * @param string $name The human-readable credential name (non-empty). + * @param string $provider The provider identifier (the CALLER validates it against the catalogue). + * @param string $owner The owning user's UID (asserted by the trusted caller; never request input). + * @param array $allowedApps The app ids permitted to use this credential (Guard 2 at broker time). + * @param string|null $secret The raw secret, or null/'' to mint metadata only (no vault write). + * @param string $scope The ALREADY-RESOLVED scope (`personal`|`organisation`); selects the vault owner. + * @param string|null $organisation The ALREADY-GATED owning organisation UUID; required for the organisation scope. + * + * @return ObjectEntity The persisted credential entity (its `getUuid()` is the credential id / credentialRef). + * + * @throws \InvalidArgumentException When the name is empty, or an organisation-scoped mint carries no organisation. + * @throws Throwable When the object save or the vault write fails (the orphaned object is removed first). + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + public function mint( + string $name, + string $provider, + string $owner, + array $allowedApps=[], + ?string $secret=null, + string $scope=self::SCOPE_PERSONAL, + ?string $organisation=null + ): ObjectEntity { + $name = trim($name); + if ($name === '' || trim($provider) === '') { + throw new InvalidArgumentException(message: 'A credential requires a name and a provider'); + } + + $data = [ + 'name' => $name, + 'provider' => $provider, + 'owner' => $owner, + 'allowedApps' => array_values($allowedApps), + 'createdAt' => (new DateTimeImmutable())->format(DATE_ATOM), + ]; + + // Only an organisation-scoped credential carries scope/organisation — a personal + // credential's property bag stays byte-for-byte what it has always been (design D1). + if ($scope !== self::SCOPE_ORGANISATION) { + $scope = self::SCOPE_PERSONAL; + } + + if ($scope === self::SCOPE_ORGANISATION) { + $organisation = trim((string) $organisation); + if ($organisation === '') { + throw new InvalidArgumentException(message: 'An organisation-scoped credential requires an organisation'); + } + + $data['scope'] = self::SCOPE_ORGANISATION; + $data['organisation'] = $organisation; + } + + // Persist the credential-metadata object in system context. Mint is a trusted, + // caller-authorized operation ("authorization is the caller's" — the controller + // runs the provider-catalogue + organisation-admin gate; a repair-step migration + // is itself system-trusted), so the write MUST NOT be re-gated by RBAC here. + // Without `_rbac: false` a SESSIONLESS caller (an occ/repair migration folding an + // inline source secret into the broker) fails the create with NotAuthorizedException + // — the write ran as the anonymous principal. This mirrors the rollback delete + // below, which already bypasses RBAC for exactly this reason. + $saved = $this->objectService->saveObject( + object: $data, + register: self::REGISTER, + schema: self::SCHEMA, + _rbac: false, + _multitenancy: false + ); + + $uuid = (string) $saved->getUuid(); + if ($secret === null || $secret === '') { + return $saved; + } + + try { + $this->credentialStore->put($uuid, $secret, $scope); + } catch (Throwable $e) { + $this->discardOrphanedCredential(uuid: $uuid); + throw $e; + } + + return $saved; + }//end mint() + + /** + * Remove a credential metadata object whose vault write failed (mint rollback). + * + * Best-effort by design: the caller is already unwinding a vault failure and that + * error must reach it unmasked, so a delete failure is logged (secret-free) and + * swallowed. Worst case the orphan survives and fails closed at broker time. + * + * @param string $uuid The orphaned credential object's UUID. + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function discardOrphanedCredential(string $uuid): void + { + try { + $this->objectService->deleteObject( + uuid: $uuid, + register: self::REGISTER, + schema: self::SCHEMA, + _rbac: false + ); + } catch (Throwable $e) { + $this->logger->error( + 'Credential broker: failed to roll back credential {uuid} after its secret could not be stored', + ['uuid' => $uuid, 'exception' => $e] + ); + } + }//end discardOrphanedCredential() + + /** + * Whether a resolved provider entry is inject-only (app-side injection, never proxied). + * + * @param array $provider The catalogue provider entry. + * + * @return bool True when the provider is flagged `inject_only`. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function isInjectOnly(array $provider): bool + { + return ($provider['inject_only'] ?? false) === true; + }//end isInjectOnly() + + /** + * Guard 1 — load the credential and admit the caller per the credential's SCOPE. + * + * The owner guard is scope-dispatched (design D3), and the change is strictly + * ADDITIVE — the personal branch is byte-for-byte the pre-existing owner check: + * + * - `personal` (or absent): admit only when the acting identity equals the + * credential `owner`. The acting identity is the SESSION user whenever one + * exists (unconditionally — a session caller cannot impersonate via + * `actingUserId`); only a sessionless in-process caller may assert + * `actingUserId` (design D-K); no identity at all denies as before. + * - `organisation`: admit when a REAL session user is a member of the + * credential's `organisation`, OR — for a SESSIONLESS trusted in-process + * caller (openregister#450) — when the asserted `actingOrganisationId` + * matches the credential's `organisation`. The `actingUserId` fallback is + * never consulted for the organisation branch (org scope is deliberately + * decoupled from any one user's membership, ADR-064 Rule 4). + * + * The `allowedApps`, provider allow-rule, and host-lock guards then run for + * BOTH scopes, unchanged, in {@see request()}. + * + * @param string $credentialId The `credential` object UUID. + * @param string|null $actingUserId Asserted user for sessionless in-process callers (personal branch only). + * @param string|null $actingOrganisationId Asserted organisation for sessionless in-process callers (organisation branch only). + * + * @return ObjectEntity The admitted credential entity. + * + * @throws CredentialAccessDeniedException When missing, unauthenticated, or not admitted. + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + private function loadAdmittedCredential( + string $credentialId, + ?string $actingUserId=null, + ?string $actingOrganisationId=null + ): ObjectEntity { + try { + $credential = $this->objectService->find( + id: $credentialId, + register: self::REGISTER, + schema: self::SCHEMA, + _rbac: false + ); + } catch (Throwable $e) { + $credential = null; + } + + if ($credential === null) { + $this->deny(reason: 'credential not found', credentialId: $credentialId); + } + + $data = $credential->jsonSerialize(); + if ($this->scopeOf(data: $data) === self::SCOPE_ORGANISATION) { + $this->assertOrganisationMember( + data: $data, + credentialId: $credentialId, + actingOrganisationId: $actingOrganisationId + ); + return $credential; + } + + $this->assertPersonalOwner(credential: $credential, credentialId: $credentialId, actingUserId: $actingUserId); + + return $credential; + }//end loadAdmittedCredential() + + /** + * Personal owner guard — UNCHANGED: admit only when the acting identity owns it. + * + * @param ObjectEntity $credential The loaded credential entity. + * @param string $credentialId The `credential` object UUID (for logging). + * @param string|null $actingUserId Asserted user for sessionless in-process callers only. + * + * @return void + * + * @throws CredentialAccessDeniedException When unauthenticated or not owned. + * + * @spec openspec/specs/credential-broker/spec.md + * @spec openspec/specs/credential-broker/spec.md + */ + private function assertPersonalOwner(ObjectEntity $credential, string $credentialId, ?string $actingUserId): void + { + $ownerUid = $this->resolveActingIdentity(actingUserId: $actingUserId); + if ($ownerUid === null) { + $this->deny(reason: 'unauthenticated', credentialId: $credentialId); + } + + if ($credential->getOwner() !== $ownerUid) { + $this->deny(reason: 'caller is not the credential owner', credentialId: $credentialId); + } + }//end assertPersonalOwner() + + /** + * Organisation membership guard — a session member OR a matching sessionless in-process assertion. + * + * ADR-064 Rule 4 REQUIRES an infrastructure credential (a source/consumer secret) to be + * `organisation`-scoped, never `personal` — coupling it to one employee would break the + * integration the moment that employee leaves. But its trusted in-process consumers (an + * openconnector migration repair step, a background sync job) run with NO user session, so + * the original design D3 "no session denies" rule left org-scoped infrastructure credentials + * unresolvable (openregister#450). This guard therefore admits on TWO paths: + * + * - Session present: the session is AUTHORITATIVE. Membership is resolved through + * {@see OrganisationService::hasAccessToOrganisation()} (the session user is a member of — + * or a Nextcloud admin over — the organisation), exactly as before. Any asserted + * `actingOrganisationId` is IGNORED here, so a request-context caller can NEVER escalate + * via the assertion (a session is proof of identity; the assertion is not). + * - Sessionless: admit IFF a non-null `actingOrganisationId` EQUALS the credential's + * `organisation`. This is the only new admit path. It is safe because the assertion is + * settable only by trusted in-process code — request input never reaches it (the routed + * {@see request()} passes null; the controller never reads it; {@see resolveInjectable()} + * is not HTTP-routed). The match is a consistency guard so an org-A assertion cannot + * resolve an org-B credential; resolution stays DECOUPLED from any individual user's + * membership, which is the entire point of organisation scope (ADR-064 Rule 4). The + * sessionless `actingUserId` fallback is deliberately NOT reused — org scope must not be + * recoupled to a single user. + * + * @param array $data The credential's serialised data. + * @param string $credentialId The `credential` object UUID (for logging). + * @param string|null $actingOrganisationId Asserted organisation for sessionless in-process callers only. + * + * @return void + * + * @throws CredentialAccessDeniedException When the org is malformed, the session user is not a member, or the + * sessionless assertion is absent or does not match the credential organisation. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function assertOrganisationMember(array $data, string $credentialId, ?string $actingOrganisationId=null): void + { + $organisation = (string) ($data['organisation'] ?? ''); + if ($organisation === '') { + $this->deny(reason: 'organisation credential has no organisation', credentialId: $credentialId); + } + + // Session present: the session identity is authoritative and the asserted acting + // organisation is ignored — membership is resolved exactly as before. + if ($this->userSession->getUser() !== null) { + if ($this->organisationService->hasAccessToOrganisation($organisation) === false) { + $this->deny(reason: 'caller is not a member of the credential organisation', credentialId: $credentialId); + } + + return; + } + + // Sessionless (in-process by construction — request input never reaches + // actingOrganisationId). Admit only when the trusted caller asserts THIS + // credential's organisation; matching decouples resolution from any one user. + if ($actingOrganisationId === null || $actingOrganisationId !== $organisation) { + $this->deny( + reason: 'sessionless organisation resolution requires a matching acting organisation', + credentialId: $credentialId + ); + } + }//end assertOrganisationMember() + + /** + * Resolve a credential's scope from its serialised data (absent ⇒ personal). + * + * @param array $data The credential's serialised data. + * + * @return string The scope (`personal`|`organisation`). + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function scopeOf(array $data): string + { + $scope = (string) ($data['scope'] ?? self::SCOPE_PERSONAL); + if ($scope === self::SCOPE_ORGANISATION) { + return self::SCOPE_ORGANISATION; + } + + return self::SCOPE_PERSONAL; + }//end scopeOf() + + /** + * Resolve the identity the owner guard evaluates against (design D-K). + * + * Session identity wins UNCONDITIONALLY when a user session exists — + * `actingUserId` is ignored there, so a session-context caller can never + * impersonate another user. Only when no session exists (background / + * cron, in-process by construction: the HTTP controller never forwards an + * acting user) is a non-empty `actingUserId` honored. Returns null when + * neither identity exists (the caller then denies, failing closed). + * + * @param string|null $actingUserId Asserted user for sessionless in-process callers only. + * + * @return string|null The identity for the owner guard, or null when unauthenticated. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function resolveActingIdentity(?string $actingUserId): ?string + { + $user = $this->userSession->getUser(); + if ($user !== null) { + return $user->getUID(); + } + + if ($actingUserId !== null && $actingUserId !== '') { + return $actingUserId; + } + + return null; + }//end resolveActingIdentity() + + /** + * Guard 2 — assert the calling app is in the credential's allow-list. + * + * @param array $data The credential's serialised data. + * @param string $appId The authenticated calling app id. + * @param string $credentialId The credential UUID (for logging). + * + * @return void + * + * @throws CredentialAccessDeniedException When the app is not allowed. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function assertAppAllowed(array $data, string $appId, string $credentialId): void + { + $allowedApps = ($data['allowedApps'] ?? []); + if (is_array($allowedApps) === false || in_array($appId, $allowedApps, true) === false) { + $this->deny(reason: 'app "'.$appId.'" not in allowedApps', credentialId: $credentialId); + } + }//end assertAppAllowed() + + /** + * Resolve the credential's provider from the read-only catalogue. + * + * @param array $data The credential's serialised data. + * @param string $credentialId The credential UUID (for logging). + * + * @return array The catalogue provider entry. + * + * @throws CredentialAccessDeniedException When the provider is unknown. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function resolveProvider(array $data, string $credentialId): array + { + $providerId = (string) ($data['provider'] ?? ''); + $provider = $this->catalogue->get($providerId); + if ($provider === null) { + $this->deny(reason: 'unknown provider "'.$providerId.'"', credentialId: $credentialId); + } + + return $provider; + }//end resolveProvider() + + /** + * Guard 3 — assert method + normalised path match one of the provider's allow-rules. + * + * @param array $provider The catalogue provider entry. + * @param string $method The upper-cased HTTP method. + * @param string $matchPath The normalised path (query stripped). + * @param string $credentialId The credential UUID (for logging). + * + * @return void + * + * @throws CredentialAccessDeniedException When no allow-rule matches. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function assertRuleAllowed(array $provider, string $method, string $matchPath, string $credentialId): void + { + $rules = ($provider['allowRules'] ?? []); + if (is_array($rules) === true) { + foreach ($rules as $rule) { + if (is_array($rule) === false) { + continue; + } + + $ruleMethod = strtoupper((string) ($rule['method'] ?? '')); + $rulePattern = (string) ($rule['pathPattern'] ?? ''); + if ($ruleMethod === $method && $rulePattern !== '' && fnmatch($rulePattern, $matchPath) === true) { + return; + } + } + } + + $this->deny(reason: 'no allow-rule matches '.$method.' '.$matchPath, credentialId: $credentialId); + }//end assertRuleAllowed() + + /** + * Guard 4 — build the resolved URL and verify its host equals the provider host. + * + * The caller supplies only a path; the URL is `baseUrl . path`. The host of the + * resolved URL MUST equal the host of `baseUrl` — defence-in-depth against a path + * that tries to smuggle a new authority. + * + * @param array $provider The catalogue provider entry. + * @param string $path The caller-supplied path (with any query). + * @param string $credentialId The credential UUID (for logging). + * + * @return string The host-locked resolved URL. + * + * @throws CredentialAccessDeniedException When the resolved host does not match the base host. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function resolveAndLockUrl(array $provider, string $path, string $credentialId): string + { + $baseUrl = (string) ($provider['baseUrl'] ?? ''); + $resolvedUrl = $baseUrl.$path; + + $baseHost = parse_url($baseUrl, PHP_URL_HOST); + $resolvedHost = parse_url($resolvedUrl, PHP_URL_HOST); + if (is_string($baseHost) === false || $baseHost === '' || $resolvedHost !== $baseHost) { + $this->deny(reason: 'host-lock violation for provider host "'.(string) $baseHost.'"', credentialId: $credentialId); + } + + return $resolvedUrl; + }//end resolveAndLockUrl() + + /** + * Inject the provider's auth scheme (secret substituted) into the request headers. + * + * The broker owns the auth header: any caller-supplied value for that header (and + * for `Host`) is discarded, then the templated secret header is set. + * + * @param array $provider The catalogue provider entry. + * @param array $headers Caller-supplied extra headers. + * @param string $secret The raw secret (never logged). + * + * @return array The final request headers with auth injected. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function injectAuth(array $provider, array $headers, string $secret): array + { + $scheme = ($provider['authScheme'] ?? []); + $headerName = (string) ($scheme['header'] ?? 'Authorization'); + $template = (string) ($scheme['template'] ?? '{secret}'); + + // Discard any caller attempt to set the auth header or the Host header. + $sanitised = []; + foreach ($headers as $key => $value) { + $lower = strtolower((string) $key); + if ($lower === strtolower($headerName) || $lower === 'host') { + continue; + } + + $sanitised[(string) $key] = (string) $value; + } + + $sanitised[$headerName] = str_replace('{secret}', $secret, $template); + return $sanitised; + }//end injectAuth() + + /** + * Perform the outbound call and return the upstream status, headers, and body. + * + * A non-2xx upstream status is a COMPLETED call and is returned verbatim + * (`http_errors` disabled). Only a transport-level failure throws. + * + * @param string $method The upper-cased HTTP method. + * @param string $url The host-locked resolved URL. + * @param array $headers The final request headers (auth injected). + * @param string|null $body The optional raw request body. + * @param string $credentialId The credential UUID (for logging). + * + * @return array{status: int, headers: array, body: string} The upstream response. + * + * @throws CredentialUpstreamException When the call fails at the transport level. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function performCall(string $method, string $url, array $headers, ?string $body, string $credentialId): array + { + $options = [ + 'headers' => $headers, + 'http_errors' => false, + ]; + if ($body !== null) { + $options['body'] = $body; + } + + try { + $client = $this->clientService->newClient(); + $response = $client->request($method, $url, $options); + } catch (Throwable $e) { + $this->logger->error( + '[CredentialBrokerService] upstream call failed', + ['credential' => $credentialId, 'error' => $e->getMessage()] + ); + throw new CredentialUpstreamException(message: 'Upstream request failed'); + } + + $rawBody = $response->getBody(); + if (is_resource($rawBody) === true) { + $rawBody = stream_get_contents($rawBody); + } + + return [ + 'status' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + 'body' => (string) $rawBody, + ]; + }//end performCall() + + /** + * Normalise and validate a caller-supplied path (reject `..`, require a single leading `/`). + * + * Returns the query-stripped, single-decoded path used for allow-rule matching. + * + * @param string $path The caller-supplied path. + * + * @return string The normalised path for matching. + * + * @throws CredentialAccessDeniedException When the path is empty, protocol-relative, or contains traversal. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function normalisePath(string $path): string + { + if ($path === '' || $path[0] !== '/' || str_starts_with($path, '//') === true) { + $this->deny(reason: 'path must be a single-slash-rooted relative path', credentialId: ''); + } + + // Single-decode, then reject any traversal in the decoded form. + $decoded = rawurldecode($path); + if (str_contains($decoded, '..') === true) { + $this->deny(reason: 'path contains traversal', credentialId: ''); + } + + $queryPos = strpos($decoded, '?'); + if ($queryPos !== false) { + return substr($decoded, 0, $queryPos); + } + + return $decoded; + }//end normalisePath() + + /** + * Log a secret-free reason and throw the static access-denied exception. + * + * @param string $reason The real, secret-free reason (server-side only). + * @param string $credentialId The credential UUID (for correlation), or empty. + * + * @return never + * + * @throws CredentialAccessDeniedException Always. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function deny(string $reason, string $credentialId): never + { + $this->logger->warning( + '[CredentialBrokerService] request denied: '.$reason, + ['credential' => $credentialId] + ); + throw new CredentialAccessDeniedException(message: 'Request not permitted'); + }//end deny() +}//end class diff --git a/lib/Service/Credential/CredentialStore.php b/lib/Service/Credential/CredentialStore.php new file mode 100644 index 0000000000..f9a7b7122d --- /dev/null +++ b/lib/Service/Credential/CredentialStore.php @@ -0,0 +1,80 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +/** + * Abstraction over the backing store that holds a credential's raw secret. + */ +interface CredentialStore +{ + /** + * Store (or overwrite) the secret for a credential. + * + * The `scope` selects the vault owner the secret is stored under: `personal` + * (the default) keeps the pre-existing per-user behaviour; `organisation` + * stores under a reserved system identity so no user owns the shared secret + * (credential-broker-organisation-scope design D2). + * + * @param string $uuid The owning `credential` object's UUID (the vault key). + * @param string $secret The raw secret to store. Never logged or persisted to an OR object. + * @param string $scope The credential scope (`personal`|`organisation`); selects the vault owner. + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function put(string $uuid, string $secret, string $scope='personal'): void; + + /** + * Retrieve the secret for a credential, or null when none is stored. + * + * @param string $uuid The owning `credential` object's UUID (the vault key). + * @param string $scope The credential scope (`personal`|`organisation`); selects the vault owner. + * + * @return string|null The raw secret, or null when absent. Never logged. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function get(string $uuid, string $scope='personal'): ?string; + + /** + * Delete the secret for a credential (idempotent — a no-op when absent). + * + * @param string $uuid The owning `credential` object's UUID (the vault key). + * @param string $scope The credential scope (`personal`|`organisation`); selects the vault owner. + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function delete(string $uuid, string $scope='personal'): void; +}//end interface diff --git a/lib/Service/Credential/CredentialStoreResolver.php b/lib/Service/Credential/CredentialStoreResolver.php new file mode 100644 index 0000000000..bdfaf87191 --- /dev/null +++ b/lib/Service/Credential/CredentialStoreResolver.php @@ -0,0 +1,216 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use OCP\App\IAppManager; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; + +/** + * Resolves the active credential-store leaf (Doriath preferred, vault fallback). + */ +class CredentialStoreResolver +{ + /** + * The Doriath app id probed for eligibility. + * + * @var string + */ + public const DORIATH_APP_ID = 'doriath'; + + /** + * Doriath service classes the leaf calls — ALL must exist for eligibility. + * + * @var array + */ + private const DORIATH_SERVICE_CLASSES = [ + 'OCA\\Doriath\\Service\\ApplicationService', + 'OCA\\Doriath\\Service\\SecretService', + 'OCA\\Doriath\\Service\\EncryptService', + 'OCA\\Doriath\\Service\\DecryptService', + ]; + + /** + * The Doriath service class carrying the application-scoped seam methods. + * + * @var string + */ + private const SECRET_SERVICE_CLASS = 'OCA\\Doriath\\Service\\SecretService'; + + /** + * Application-scoped seam methods that must exist on the secret service. + * + * These land via Doriath's `application-secret-delete` change; probing for + * them keeps the cross-repo rollout order free (design D-A). + * + * @var array + */ + private const REQUIRED_SEAM_METHODS = [ + 'getByNameForApplication', + 'deleteByApplication', + ]; + + /** + * Constructor. + * + * @param IAppManager $appManager Probes whether the doriath app is enabled. + * @param IAppConfig $appConfig Holds OR's Doriath self-registration state. + * @param DoriathCredentialStore $doriathStore The Doriath-backed leaf (selected when eligible). + * @param NextcloudVaultCredentialStore $vaultStore The NC-vault leaf (fallback). + * @param LoggerInterface $logger Secret-free diagnostics. + * + * @return void + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly IAppConfig $appConfig, + private readonly DoriathCredentialStore $doriathStore, + private readonly NextcloudVaultCredentialStore $vaultStore, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Return the active credential-store leaf. + * + * @return CredentialStore The Doriath leaf when eligible, else the NC-vault leaf. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function resolve(): CredentialStore + { + if ($this->isDoriathEligible() === true) { + return $this->doriathStore; + } + + return $this->vaultStore; + }//end resolve() + + /** + * Evaluate the full Doriath eligibility chain (fails closed on any miss). + * + * @return bool True when the Doriath leaf may be used. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function isDoriathEligible(): bool + { + if ($this->appManager->isEnabledForUser(self::DORIATH_APP_ID) === false) { + return false; + } + + foreach ($this->doriathServiceClasses() as $className) { + if (class_exists($className) === false) { + $this->logger->debug( + '[CredentialStoreResolver] Doriath ineligible: service class missing', + ['service' => $className] + ); + return false; + } + } + + $secretServiceClass = $this->secretServiceClass(); + foreach (self::REQUIRED_SEAM_METHODS as $method) { + if (method_exists($secretServiceClass, $method) === false) { + $this->logger->debug( + '[CredentialStoreResolver] Doriath ineligible: application-scoped seam method missing', + ['method' => $method] + ); + return false; + } + } + + return $this->isSelfRegistered(); + }//end isDoriathEligible() + + /** + * Whether OR's Doriath self-registration state exists (design D-B output). + * + * @return bool True when the application UUID and public key PEM are persisted. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function isSelfRegistered(): bool + { + $applicationId = $this->appConfig->getValueString( + 'openregister', + DoriathCredentialStore::APP_CONFIG_APPLICATION_ID, + '' + ); + $publicPem = $this->appConfig->getValueString( + 'openregister', + DoriathCredentialStore::APP_CONFIG_PUBLIC_KEY_PEM, + '' + ); + + return ($applicationId !== '' && $publicPem !== ''); + }//end isSelfRegistered() + + /** + * The Doriath service classes probed via `class_exists`. + * + * Protected so unit tests can point the probes at contract fixtures + * without the Doriath app installed. + * + * @return array The probed FQCNs. + * + * @spec openspec/specs/credential-broker/spec.md + */ + protected function doriathServiceClasses(): array + { + return self::DORIATH_SERVICE_CLASSES; + }//end doriathServiceClasses() + + /** + * The secret-service class probed for the application-scoped seam methods. + * + * Protected for the same test-fixture reason as {@see doriathServiceClasses()}. + * + * @return string The probed FQCN. + * + * @spec openspec/specs/credential-broker/spec.md + */ + protected function secretServiceClass(): string + { + return self::SECRET_SERVICE_CLASS; + }//end secretServiceClass() +}//end class diff --git a/lib/Service/Credential/CredentialUpstreamException.php b/lib/Service/Credential/CredentialUpstreamException.php new file mode 100644 index 0000000000..4162627824 --- /dev/null +++ b/lib/Service/Credential/CredentialUpstreamException.php @@ -0,0 +1,38 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use RuntimeException; + +/** + * Signals a transport-level failure of the brokered call (maps to a static 502). + */ +class CredentialUpstreamException extends RuntimeException +{ +}//end class diff --git a/lib/Service/Credential/DoriathApplicationRegistrar.php b/lib/Service/Credential/DoriathApplicationRegistrar.php new file mode 100644 index 0000000000..2060da2432 --- /dev/null +++ b/lib/Service/Credential/DoriathApplicationRegistrar.php @@ -0,0 +1,284 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use OCP\App\IAppManager; +use OCP\IAppConfig; +use OCP\Server; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Registers a per-app, identity-only Doriath application, idempotently. + * + * @SuppressWarnings(PHPMD.StaticAccess) Doriath's ApplicationService is lazily + * resolved via `OCP\Server::get` behind a `class_exists` guard so OpenRegister + * carries no compile-time dependency on the optional app (design D-2). + * + * @spec openspec/changes/per-app-doriath-application/tasks.md#1-per-app-doriath-registration-seam-d-2-d-6 + */ +class DoriathApplicationRegistrar +{ + /** + * FQCN of Doriath's application service (registration seam). + * + * @var string + */ + private const APPLICATION_SERVICE = 'OCA\\Doriath\\Service\\ApplicationService'; + + /** + * `IAppConfig` key prefix for a per-app Doriath application UUID. + * + * The per-app key is `self::APP_CONFIG_APPLICATION_ID_PREFIX . $appId`, + * deliberately DISTINCT from + * {@see DoriathCredentialStore::APP_CONFIG_APPLICATION_ID} (which identifies + * OpenRegister's own single custody vault). No secret material is ever + * stored under it — identity-only holds no private key. + * + * @var string + */ + public const APP_CONFIG_APPLICATION_ID_PREFIX = 'doriath_application_id_'; + + /** + * The Doriath application `type` for same-fleet consumers (design D-6). + * + * @var string + */ + private const APPLICATION_TYPE = 'internal'; + + /** + * Constructor. + * + * @param IAppConfig $appConfig Persists/probes the per-app Doriath application UUID (non-secret). + * @param IAppManager $appManager Probes whether the doriath app is enabled. + * @param LoggerInterface $logger Secret-free diagnostics. + * + * @return void + */ + public function __construct( + private readonly IAppConfig $appConfig, + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Register the given consuming app as its own identity-only Doriath application. + * + * Never throws: Doriath absent/disabled/unloadable, or any failure, warns + * and returns — the caller's OR app-key onboarding and the broker continue + * unchanged. Idempotent: a live persisted UUID makes this a no-op. + * + * @param string $appId The consuming app id (becomes the Doriath application name). + * @param string|null $description The manifest description (falls back to the appId). + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function registerApplication(string $appId, ?string $description=null): void + { + try { + if ($this->isDoriathAvailable() === false) { + $this->logger->debug( + sprintf('[DoriathApplicationRegistrar:%s] Doriath unavailable — skipping per-app registration', $appId) + ); + return; + } + + if ($this->isRegistrationLive(appId: $appId) === true) { + $this->logger->debug( + sprintf('[DoriathApplicationRegistrar:%s] already registered with a live Doriath row — not re-registering', $appId) + ); + return; + } + + $this->register(appId: $appId, description: $description); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('[DoriathApplicationRegistrar:%s] per-app Doriath registration skipped', $appId), + ['exception' => $e->getMessage()] + ); + }//end try + }//end registerApplication() + + /** + * The per-app `IAppConfig` key holding this app's Doriath application UUID. + * + * @param string $appId The consuming app id. + * + * @return string The namespaced config key. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public static function appConfigKey(string $appId): string + { + return self::APP_CONFIG_APPLICATION_ID_PREFIX.$appId; + }//end appConfigKey() + + /** + * Whether the doriath app is enabled and its registration seam is loadable. + * + * @return bool True when per-app registration can be attempted. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function isDoriathAvailable(): bool + { + if ($this->appManager->isEnabledForUser('doriath') === false) { + return false; + } + + return ($this->resolveApplicationService() !== null); + }//end isDoriathAvailable() + + /** + * Whether a previous per-app registration is still live in Doriath (skip-fast). + * + * Mirrors {@see RegisterOpenRegisterWithDoriath::isRegistrationLive()}: true + * only when the per-app `IAppConfig` UUID is set AND Doriath still holds that + * row. A stale UUID (Doriath reinstalled/row removed) yields false, so the + * step re-registers exactly once. + * + * @param string $appId The consuming app id. + * + * @return bool True when already registered with a live row. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function isRegistrationLive(string $appId): bool + { + $applicationId = $this->appConfig->getValueString('openregister', self::appConfigKey(appId: $appId), ''); + if ($applicationId === '') { + return false; + } + + $applicationService = $this->resolveApplicationService(); + if ($applicationService === null) { + return false; + } + + try { + // Admin-scoped in-process read; throws when the row no longer exists. + $applicationService->get($applicationId, '', true); + return true; + } catch (Throwable $e) { + $this->logger->info( + sprintf('[DoriathApplicationRegistrar:%s] persisted application UUID is stale — re-registering', $appId), + ['error' => $e->getMessage()] + ); + return false; + } + }//end isRegistrationLive() + + /** + * Register the per-app application (identity-only, no CSR, pending) and persist its UUID. + * + * @param string $appId The consuming app id (Doriath application name). + * @param string|null $description The manifest description (falls back to the appId). + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function register(string $appId, ?string $description): void + { + $applicationService = $this->resolveApplicationService(); + if ($applicationService === null) { + return; + } + + $displayDescription = $appId; + if ($description !== null && $description !== '') { + $displayDescription = $description; + } + + // Identity-only: csr = null (no EncryptionSuite provisioned); userId = + // null (repair runs without a session); isAdmin = false → Doriath + // creates a PENDING row and dispatches an admin-approval notification. + $application = $applicationService->register( + $appId, + $displayDescription, + self::APPLICATION_TYPE, + null, + null, + false + ); + + // Doriath generates the row UUID — persist the NON-secret UUID only. + $applicationId = (string) $application->getId(); + $this->appConfig->setValueString('openregister', self::appConfigKey(appId: $appId), $applicationId); + + $this->logger->info( + sprintf('[DoriathApplicationRegistrar:%s] registered per-app Doriath application (pending)', $appId), + ['applicationId' => $applicationId] + ); + }//end register() + + /** + * Resolve Doriath's ApplicationService, or null when unavailable. + * + * `class_exists` + `OCP\Server::get` (no compile-time Doriath dependency). + * Protected so unit tests can substitute a contract fake. + * + * @return object|null The resolved service, or null. + * + * @spec openspec/specs/credential-broker/spec.md + */ + protected function resolveApplicationService(): ?object + { + if (class_exists(self::APPLICATION_SERVICE) === false) { + return null; + } + + try { + return Server::get(self::APPLICATION_SERVICE); + } catch (Throwable $e) { + $this->logger->warning( + '[DoriathApplicationRegistrar] failed to resolve Doriath ApplicationService', + ['error' => $e->getMessage()] + ); + return null; + } + }//end resolveApplicationService() +}//end class diff --git a/lib/Service/Credential/DoriathCredentialStore.php b/lib/Service/Credential/DoriathCredentialStore.php new file mode 100644 index 0000000000..dae9cc3e45 --- /dev/null +++ b/lib/Service/Credential/DoriathCredentialStore.php @@ -0,0 +1,471 @@ +1 logs a warning Doriath-side, never + * guessed) — so an ambiguous name is indistinguishable from absence here and + * a read of it fails closed exactly as required. + * + * @category Service + * @package OCA\OpenRegister\Service\Credential + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use OCP\IAppConfig; +use OCP\IUserSession; +use OCP\Security\ICredentialsManager; +use OCP\Server; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +/** + * Doriath application-vault implementation of the credential secret store. + * + * @SuppressWarnings(PHPMD.StaticAccess) Doriath services are lazily resolved via + * `OCP\Server::get` behind `class_exists` guards so OpenRegister carries no + * compile-time dependency on the optional app (design D-A, Deck-leaf idiom). + */ +class DoriathCredentialStore implements CredentialStore +{ + /** + * IAppConfig key (app `openregister`) holding the Doriath-assigned application UUID. + * + * @var string + */ + public const APP_CONFIG_APPLICATION_ID = 'doriath_application_id'; + + /** + * IAppConfig key (app `openregister`) holding OpenRegister's own public key PEM. + * + * Public material only — needed by {@see put()} for envelope encryption + * without a Doriath round-trip. The private counterpart NEVER lands here. + * + * @var string + */ + public const APP_CONFIG_PUBLIC_KEY_PEM = 'doriath_public_key_pem'; + + /** + * SYSTEM-scoped `ICredentialsManager` key holding OpenRegister's private key PEM. + * + * Same system-scope idiom (`userId = ''`) as the per-app broker signing + * secrets in {@see CredentialAppTokenService}. The private key never leaves + * `ICredentialsManager`, is never logged, and never lands in `IAppConfig`. + * + * @var string + */ + public const PRIVATE_KEY_ID = 'openregister/doriath/private-key'; + + /** + * FQCN of Doriath's stateless encrypt service (scheme `rsa-oaep-sha256-chunked-v1`). + * + * @var string + */ + private const ENCRYPT_SERVICE = 'OCA\\Doriath\\Service\\EncryptService'; + + /** + * FQCN of Doriath's stateless decrypt service. + * + * @var string + */ + private const DECRYPT_SERVICE = 'OCA\\Doriath\\Service\\DecryptService'; + + /** + * FQCN of Doriath's secret service (application-scoped seam). + * + * @var string + */ + private const SECRET_SERVICE = 'OCA\\Doriath\\Service\\SecretService'; + + /** + * Constructor. + * + * @param NextcloudVaultCredentialStore $vaultStore Composed vault leaf (lazy-migration fallback). + * @param ICredentialsManager $credentialsManager NC vault holding OR's private key (system-scoped). + * @param IAppConfig $appConfig Holds the Doriath application UUID + public PEM. + * @param IUserSession $userSession Session probe (migration only runs in-session). + * @param LoggerInterface $logger Secret-free diagnostics. + * + * @return void + */ + public function __construct( + private readonly NextcloudVaultCredentialStore $vaultStore, + private readonly ICredentialsManager $credentialsManager, + private readonly IAppConfig $appConfig, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Store (or rotate) the secret as an application-owned Doriath ciphertext row. + * + * Encrypts against OpenRegister's own public key PEM via Doriath's stateless + * `EncryptService::rsaEncrypt` (scheme `rsa-oaep-sha256-chunked-v1`), then + * upserts by name (name = credential UUID, root folder): a Doriath miss + * (null — including a Doriath-side ambiguity, which Doriath reports as + * absence, never guessing) creates via `SecretService::createByApplication`; + * a single match rotates via `updateByApplication` keyed by the entity's + * ROW id (keeping the row, bumping Doriath's rotation metadata). A write + * failure throws — a dropped secret must be loud. + * + * The Doriath row is application-owned and keyed by the credential UUID, so it + * is inherently system-scoped — `$scope` does not change where the ciphertext + * lands here; it is threaded through only to the composed per-user vault leaf + * used for lazy migration (design D2 / D-A). + * + * @param string $uuid The owning `credential` object's UUID (the secret name). + * @param string $secret The raw secret to store. Never logged. + * @param string $scope The credential scope (`personal`|`organisation`); threaded to the migration vault leaf. + * + * @return void + * + * @throws RuntimeException When the store is unprovisioned or Doriath rejects the write. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function put(string $uuid, string $secret, string $scope='personal'): void + { + $applicationId = $this->requireApplicationId(); + + $publicPem = $this->appConfig->getValueString('openregister', self::APP_CONFIG_PUBLIC_KEY_PEM, ''); + if ($publicPem === '') { + throw new RuntimeException('Credential store is not provisioned'); + } + + $encryptService = $this->requireDoriathService(className: self::ENCRYPT_SERVICE); + $ciphertext = (string) $encryptService->rsaEncrypt($secret, $publicPem); + + $row = $this->lookupRow(uuid: $uuid, applicationId: $applicationId); + + $secretService = $this->requireDoriathService(className: self::SECRET_SERVICE); + if ($row === null) { + $secretService->createByApplication(['name' => $uuid, 'key' => $ciphertext], $applicationId); + return; + } + + $secretService->updateByApplication((string) $row->getId(), ['key' => $ciphertext], $applicationId); + }//end put() + + /** + * Retrieve and decrypt the secret, lazily migrating a legacy vault row. + * + * Reads the application-owned ciphertext in-process by name (Doriath returns + * the single own-vault `Secret` entity or null — zero matches, cross-vault, + * and ambiguity are all null, with ambiguity logged Doriath-side) and + * decrypts via Doriath's `DecryptService::rsaDecrypt` with OR's + * system-scoped private key. Every failure path returns null — the broker + * then denies with its static 403 ("no secret stored"), failing closed. A + * Doriath miss falls back to the composed per-user vault (design D-A): on a + * vault hit the secret is re-put into Doriath and the vault row deleted + * (only after a successful re-put), then returned. Without a user session + * the vault cannot be consulted, so a sessionless read of an un-migrated + * secret fails closed. + * + * @param string $uuid The owning `credential` object's UUID. + * @param string $scope The credential scope (`personal`|`organisation`); threaded to the migration vault leaf. + * + * @return string|null The raw secret, or null when absent/undecryptable. Never logged. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function get(string $uuid, string $scope='personal'): ?string + { + $applicationId = $this->applicationId(); + if ($applicationId === '') { + return null; + } + + try { + $row = $this->lookupRow(uuid: $uuid, applicationId: $applicationId); + } catch (Throwable $e) { + // A failed lookup is NOT a miss: do not fall through to migration, + // which could redundantly re-encrypt. Fail closed instead. + $this->logger->warning( + '[DoriathCredentialStore] secret lookup failed', + ['credential' => $uuid, 'error' => $e->getMessage()] + ); + return null; + } + + if ($row !== null) { + return $this->decryptRow(row: $row, uuid: $uuid); + } + + return $this->migrateFromVault(uuid: $uuid, scope: $scope); + }//end get() + + /** + * Delete the secret from Doriath and clear any residual vault row (idempotent). + * + * Resolves the application-owned row by name and removes it via + * `SecretService::deleteByApplication` — keyed by the Doriath ROW id from + * the fetched entity (`getId()`), never the credential-UUID name; the seam + * loads by row id (`SecretMapper::findById`) and is itself an idempotent + * silent no-op on missing/cross-vault ids. A null resolution (absent — or + * ambiguous, which Doriath reports as absence) is a no-op here too. The + * composed vault leaf is always cleared, so a later fallback can never + * resurrect a deleted secret. + * + * @param string $uuid The owning `credential` object's UUID. + * @param string $scope The credential scope (`personal`|`organisation`); threaded to the residual vault leaf. + * + * @return void + * + * @throws RuntimeException When the Doriath seam is unreachable (caller must not treat the secret as gone). + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function delete(string $uuid, string $scope='personal'): void + { + // Residual-vault cleanup (design D-A): harmless no-op when absent. + $this->vaultStore->delete($uuid, $scope); + + $applicationId = $this->applicationId(); + if ($applicationId === '') { + return; + } + + $secretService = $this->requireDoriathService(className: self::SECRET_SERVICE); + + $row = $this->lookupRow(uuid: $uuid, applicationId: $applicationId); + if ($row === null) { + return; + } + + $secretService->deleteByApplication((string) $row->getId(), $applicationId); + }//end delete() + + /** + * Decrypt one Doriath row's ciphertext with OR's system-scoped private key. + * + * @param object $row The Doriath secret entity (exposes `getKey()`). + * @param string $uuid The credential UUID (secret-free logging only). + * + * @return string|null The plaintext secret, or null on any failure (fail closed). + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function decryptRow(object $row, string $uuid): ?string + { + $privatePem = $this->credentialsManager->retrieve('', self::PRIVATE_KEY_ID); + if (is_string($privatePem) === false || $privatePem === '') { + $this->logger->warning( + '[DoriathCredentialStore] private key unavailable — failing closed', + ['credential' => $uuid] + ); + return null; + } + + try { + $decryptService = $this->requireDoriathService(className: self::DECRYPT_SERVICE); + return (string) $decryptService->rsaDecrypt((string) $row->getKey(), $privatePem); + } catch (Throwable $e) { + $this->logger->warning( + '[DoriathCredentialStore] decryption failed — failing closed', + ['credential' => $uuid, 'error' => $e->getMessage()] + ); + return null; + } + }//end decryptRow() + + /** + * Lazily migrate a legacy per-user vault secret into Doriath (design D-A). + * + * Session-only: the vault is scoped to the session user, so without a user + * session this returns null (sessionless reads of un-migrated secrets fail + * closed). The vault row is deleted ONLY after a successful re-put, so a + * failed migration leaves the secret retrievable on the next attempt. + * + * @param string $uuid The owning `credential` object's UUID. + * @param string $scope The credential scope (`personal`|`organisation`); selects the vault owner. + * + * @return string|null The migrated secret, or null when there is nothing to migrate. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function migrateFromVault(string $uuid, string $scope='personal'): ?string + { + if ($this->userSession->getUser() === null) { + return null; + } + + $legacy = $this->vaultStore->get($uuid, $scope); + if ($legacy === null) { + return null; + } + + try { + $this->put(uuid: $uuid, secret: $legacy, scope: $scope); + } catch (Throwable $e) { + // Migration failed — keep the vault row so the next session read + // can retry; the caller still gets the secret it owns. + $this->logger->warning( + '[DoriathCredentialStore] lazy migration failed — vault row kept', + ['credential' => $uuid, 'error' => $e->getMessage()] + ); + return $legacy; + } + + $this->vaultStore->delete($uuid, $scope); + + return $legacy; + }//end migrateFromVault() + + /** + * Look up the application-owned Doriath row named by a credential UUID. + * + * Uses the service-level application-scoped read-by-name seam + * `SecretService::getByNameForApplication(name, applicationId): ?Secret`, + * which lands with Doriath's `application-secret-delete` change — the + * resolver's `method_exists` probe guarantees it exists before this store + * is selected. Doriath owns the resolution policy: the single own-vault + * match is returned as a `Secret` entity (ciphertext intact in `getKey()`), + * while zero matches, cross-vault names, and AMBIGUOUS names (>1 — logged + * Doriath-side, never guessed) all yield null. The `is_object` check is + * kept as a defensive shape guard since the call is duck-typed. + * + * @param string $uuid The credential UUID (the Doriath secret name). + * @param string $applicationId OR's Doriath application UUID. + * + * @return object|null The matching secret entity, or null when absent/ambiguous. + * + * @throws RuntimeException When the Doriath seam is unreachable. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function lookupRow(string $uuid, string $applicationId): ?object + { + $secretService = $this->requireDoriathService(className: self::SECRET_SERVICE); + + $row = $secretService->getByNameForApplication($uuid, $applicationId); + if (is_object($row) === false) { + return null; + } + + return $row; + }//end lookupRow() + + /** + * Read OR's Doriath application UUID from IAppConfig ('' when unregistered). + * + * @return string The Doriath-assigned application UUID, or ''. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function applicationId(): string + { + return $this->appConfig->getValueString('openregister', self::APP_CONFIG_APPLICATION_ID, ''); + }//end applicationId() + + /** + * Read OR's Doriath application UUID, throwing when unregistered. + * + * @return string The Doriath-assigned application UUID. + * + * @throws RuntimeException When OpenRegister is not registered with Doriath. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function requireApplicationId(): string + { + $applicationId = $this->applicationId(); + if ($applicationId === '') { + throw new RuntimeException('Credential store is not provisioned'); + } + + return $applicationId; + }//end requireApplicationId() + + /** + * Resolve a Doriath service by FQCN, or null when unavailable. + * + * `class_exists` + `OCP\Server::get` (Deck-leaf / AnonymisationBackendService + * idiom) so OR carries no compile-time dependency on Doriath. Protected so + * unit tests can substitute contract fakes without the Doriath app present. + * + * @param string $className The Doriath service FQCN. + * + * @return object|null The resolved service, or null when unavailable. + * + * @spec openspec/specs/credential-broker/spec.md + */ + protected function resolveDoriathService(string $className): ?object + { + if (class_exists($className) === false) { + return null; + } + + try { + return Server::get($className); + } catch (Throwable $e) { + $this->logger->warning( + '[DoriathCredentialStore] failed to resolve Doriath service', + ['service' => $className, 'error' => $e->getMessage()] + ); + return null; + } + }//end resolveDoriathService() + + /** + * Resolve a Doriath service by FQCN, throwing when unavailable. + * + * @param string $className The Doriath service FQCN. + * + * @return object The resolved service. + * + * @throws RuntimeException When the service cannot be resolved. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function requireDoriathService(string $className): object + { + $service = $this->resolveDoriathService(className: $className); + if ($service === null) { + throw new RuntimeException('Credential store backend unavailable'); + } + + return $service; + }//end requireDoriathService() +}//end class diff --git a/lib/Service/Credential/NextcloudVaultCredentialStore.php b/lib/Service/Credential/NextcloudVaultCredentialStore.php new file mode 100644 index 0000000000..c9435b70c9 --- /dev/null +++ b/lib/Service/Credential/NextcloudVaultCredentialStore.php @@ -0,0 +1,181 @@ +`. The raw secret is only ever + * in memory here; it is never logged. + * + * @category Service + * @package OCA\OpenRegister\Service\Credential + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use OCP\IUserSession; +use OCP\Security\ICredentialsManager; + +/** + * Per-user encrypted-vault implementation of the credential secret store. + */ +class NextcloudVaultCredentialStore implements CredentialStore +{ + /** + * Vault key prefix for a credential secret (namespaced under the app). + * + * @var string + */ + private const KEY_PREFIX = 'openregister/credential/'; + + /** + * The credential scope that stores its secret under the reserved system identity. + * + * @var string + */ + private const SCOPE_ORGANISATION = 'organisation'; + + /** + * The reserved Nextcloud system-credential identity (empty-string user). + * + * @var string + */ + private const SYSTEM_IDENTITY = ''; + + /** + * Constructor. + * + * @param ICredentialsManager $credentialsManager The NC encrypted per-user vault. + * @param IUserSession $userSession The current user session (scopes the vault key). + * + * @return void + */ + public function __construct( + private readonly ICredentialsManager $credentialsManager, + private readonly IUserSession $userSession, + ) { + }//end __construct() + + /** + * Store (or overwrite) the secret for a credential under its scope's vault owner. + * + * @param string $uuid The owning `credential` object's UUID. + * @param string $secret The raw secret to store. + * @param string $scope The credential scope (`personal`|`organisation`). + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function put(string $uuid, string $secret, string $scope='personal'): void + { + $this->credentialsManager->store( + $this->vaultOwner(scope: $scope), + self::KEY_PREFIX.$uuid, + $secret + ); + }//end put() + + /** + * Retrieve the secret for a credential from its scope's vault owner. + * + * @param string $uuid The owning `credential` object's UUID. + * @param string $scope The credential scope (`personal`|`organisation`). + * + * @return string|null The raw secret, or null when absent / not a string. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function get(string $uuid, string $scope='personal'): ?string + { + $value = $this->credentialsManager->retrieve( + $this->vaultOwner(scope: $scope), + self::KEY_PREFIX.$uuid + ); + + if (is_string($value) === true) { + return $value; + } + + return null; + }//end get() + + /** + * Delete the secret for a credential from its scope's vault owner. + * + * @param string $uuid The owning `credential` object's UUID. + * @param string $scope The credential scope (`personal`|`organisation`). + * + * @return void + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function delete(string $uuid, string $scope='personal'): void + { + $this->credentialsManager->delete( + $this->vaultOwner(scope: $scope), + self::KEY_PREFIX.$uuid + ); + }//end delete() + + /** + * The single shared scope→vault-owner selector (used at write AND read). + * + * An `organisation` credential stores under the reserved system identity + * (empty-string user) so no user owns the shared secret; every other scope + * (`personal`, or absent) stores under the current session user — byte-for-byte + * the pre-existing behaviour. Keeping this the ONLY place the mapping is made + * guarantees write and read cannot drift (design D2). + * + * @param string $scope The credential scope (`personal`|`organisation`). + * + * @return string The vault owner id (system identity or current user id). + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function vaultOwner(string $scope): string + { + if ($scope === self::SCOPE_ORGANISATION) { + return self::SYSTEM_IDENTITY; + } + + return $this->currentUserId(); + }//end vaultOwner() + + /** + * Resolve the current user's id for per-user vault scoping. + * + * @return string The current user's UID, or an empty string when unauthenticated. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function currentUserId(): string + { + return ($this->userSession->getUser()?->getUID() ?? ''); + }//end currentUserId() +}//end class diff --git a/lib/Service/Credential/ProviderCatalogue.php b/lib/Service/Credential/ProviderCatalogue.php new file mode 100644 index 0000000000..7d29d46bb4 --- /dev/null +++ b/lib/Service/Credential/ProviderCatalogue.php @@ -0,0 +1,145 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Credential; + +use OCP\App\IAppManager; +use Psr\Log\LoggerInterface; + +/** + * In-memory, read-only accessor for the shipped provider catalogue. + */ +class ProviderCatalogue +{ + /** + * App-relative path to the shipped catalogue file. + * + * @var string + */ + private const CATALOGUE_PATH = '/lib/Settings/credential-providers.json'; + + /** + * Parsed provider map (`identifier => entry`), or null before first load. + * + * @var array>|null + */ + private ?array $cache = null; + + /** + * Constructor. + * + * @param IAppManager $appManager Resolves the openregister app path on disk. + * @param LoggerInterface $logger Logger for load diagnostics. + * + * @return void + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Return one catalogue entry by provider identifier, or null when unknown. + * + * @param string $providerId The catalogue provider identifier (e.g. `github`). + * + * @return array|null The provider entry, or null. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function get(string $providerId): ?array + { + $providers = $this->all(); + $entry = ($providers[$providerId] ?? null); + if (is_array($entry) === true) { + return $entry; + } + + return null; + }//end get() + + /** + * Return the whole provider map (`identifier => entry`). + * + * Values are EXPECTED to be entry arrays, but load() only validates the + * top-level `providers` map — not each entry — so consumers must guard + * per-entry shape themselves (the file is read unvalidated from disk). + * + * @return array The parsed provider catalogue. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function all(): array + { + if ($this->cache !== null) { + return $this->cache; + } + + $this->cache = $this->load(); + return $this->cache; + }//end all() + + /** + * Read and parse the catalogue file from disk (read-only). + * + * Fails soft: any read/parse error logs a warning and yields an empty + * catalogue, so a broker call fails closed (no provider resolves) rather + * than the whole app erroring. Only the top-level `providers` map is + * validated; per-entry shape is the consumer's responsibility. + * + * @return array The parsed provider map, or empty on error. + * + * @spec openspec/specs/credential-broker/spec.md + */ + private function load(): array + { + $path = $this->appManager->getAppPath('openregister').self::CATALOGUE_PATH; + + if (is_file($path) === false || is_readable($path) === false) { + $this->logger->warning('[ProviderCatalogue] catalogue file missing or unreadable: '.$path); + return []; + } + + $raw = file_get_contents($path); + if ($raw === false) { + $this->logger->warning('[ProviderCatalogue] catalogue file could not be read: '.$path); + return []; + } + + $decoded = json_decode($raw, true); + if (is_array($decoded) === false || is_array(($decoded['providers'] ?? null)) === false) { + $this->logger->warning('[ProviderCatalogue] catalogue file is not valid provider JSON: '.$path); + return []; + } + + return $decoded['providers']; + }//end load() +}//end class diff --git a/lib/Service/DateTimeNormalizer.php b/lib/Service/DateTimeNormalizer.php index 4d2d8d88b0..d180354fd9 100644 --- a/lib/Service/DateTimeNormalizer.php +++ b/lib/Service/DateTimeNormalizer.php @@ -25,7 +25,7 @@ * * @since 2.0.0 Introduced by fix-empty-string-date-conversion * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-24 + * @spec openspec/specs/datetime-input-handling/spec.md */ declare(strict_types=1); @@ -77,7 +77,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.StaticAccess) * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-2 + * @spec openspec/specs/datetime-input-handling/spec.md */ public function normalize(mixed $value): ?DateTimeImmutable { @@ -129,7 +129,7 @@ public function normalize(mixed $value): ?DateTimeImmutable * * @return string|null `Y-m-d H:i:s`-formatted string, or `null` for empty/invalid input. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-2 + * @spec openspec/specs/datetime-input-handling/spec.md */ public function formatForDatabase(mixed $value): ?string { @@ -148,7 +148,7 @@ public function formatForDatabase(mixed $value): ?string * * @return string|null ISO 8601 string with offset, or `null` for empty/invalid input. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-2 + * @spec openspec/specs/datetime-input-handling/spec.md */ public function formatForIso8601(mixed $value): ?string { diff --git a/lib/Service/Dbal/DatabaseIntrospectionService.php b/lib/Service/Dbal/DatabaseIntrospectionService.php new file mode 100644 index 0000000000..70d72f6b1a --- /dev/null +++ b/lib/Service/Dbal/DatabaseIntrospectionService.php @@ -0,0 +1,774 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Dbal; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\ForeignKeyConstraint; +use Doctrine\DBAL\Schema\Table; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\Source; +use OCA\OpenRegister\Service\Schema\SchemaDiffService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Introspects an external SQL database into an OpenRegister register + schemas. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Introspection inherently walks + * tables, views, columns, keys and relations in one cohesive pass; splitting it + * would scatter the D3-D7 design decisions across artificial helper classes. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Depends on the DBAL schema value + * objects plus the OR mapper/diff seams the design names explicitly. + */ +class DatabaseIntrospectionService +{ + /** + * The object-source provider id every introspected schema is bound to. + * + * @var string + */ + public const PROVIDER_ID = 'dbal-source'; + + /** + * Separator joining the parts of a synthesised composite-primary-key id. + * + * @var string + */ + public const COMPOSITE_ID_SEPARATOR = '::'; + + /** + * Constructor. + * + * @param DbalConnectionFactory $connectionFactory Opens the DBAL connection for a source. + * @param SqlTypeMapper $typeMapper SQL column → JSON-Schema mapping. + * @param RegisterMapper $registerMapper Register persistence. + * @param SchemaMapper $schemaMapper Schema persistence. + * @param SchemaDiffService $diffService Classifies structural drift between runs. + * @param LoggerInterface $logger Secret-free diagnostics. + * + * @return void + */ + public function __construct( + private readonly DbalConnectionFactory $connectionFactory, + private readonly SqlTypeMapper $typeMapper, + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly SchemaDiffService $diffService, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Build the pure introspection blueprint for a source (no persistence). + * + * Returns a deterministic structure — `register` metadata plus a + * table-name-ordered `schemas` list — suitable for golden-file assertions + * and as the input to {@see introspect()}. + * + * @param Source $source The `type: database` source to introspect. + * + * @return array{register: array, schemas: array>} + * + * @throws DbalConnectionException When the source cannot be connected. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function buildBlueprint(Source $source): array + { + $sourceId = $this->sourceId(source: $source); + $connection = $this->connectionFactory->getConnection(source: $source); + $schemaManager = $connection->createSchemaManager(); + + $tableNames = array_values( + array_filter( + $schemaManager->listTableNames(), + fn (string $name): bool => $this->isSystemObject(name: $name) === false + ) + ); + sort($tableNames); + + $viewNames = []; + foreach ($schemaManager->listViews() as $view) { + if ($this->isSystemObject(name: $view->getName()) === true) { + continue; + } + + $short = $this->shortName(name: $view->getName()); + if (in_array($short, $viewNames, true) === false) { + $viewNames[] = $short; + } + } + + sort($viewNames); + + // First pass: introspect every table/view into a working record so the + // second pass can resolve foreign-key targets and add inverse sides. + $tables = []; + foreach ($tableNames as $tableName) { + $tables[$tableName] = $this->introspectTable( + schemaManager: $schemaManager, + sourceId: $sourceId, + tableName: $tableName, + isView: false + ); + } + + foreach ($viewNames as $viewName) { + if (isset($tables[$viewName]) === true) { + continue; + } + + $record = $this->introspectTable( + schemaManager: $schemaManager, + sourceId: $sourceId, + tableName: $viewName, + isView: true + ); + + // DBAL 3.x schema managers list columns for TABLES only; derive a + // view's properties from a single sample row instead (portable + // across mysql/pgsql/sqlite). An empty view yields no properties + // but is still served list-only via SELECT *. + if ($record['properties'] === []) { + $record['properties'] = $this->viewColumnsFromSample(connection: $connection, viewName: $viewName); + } + + $tables[$viewName] = $record; + }//end foreach + + $slugByTable = []; + foreach ($tables as $tableName => $record) { + $slugByTable[$tableName] = $record['slug']; + } + + // Second pass: map single-column foreign keys onto the relation dialect + // and add the inverse side on the referenced schema. + $this->applyForeignKeys(schemaManager: $schemaManager, tables: $tables, slugByTable: $slugByTable); + + $authConfig = ($source->getAuthConfig() ?? []); + $writable = (($authConfig['writable'] ?? false) === true); + + $schemas = []; + foreach ($tables as $record) { + $schemas[] = $this->toSchemaDefinition(record: $record, writable: $writable); + } + + return [ + 'register' => [ + 'title' => (string) ($source->getTitle() ?? ('Database '.$sourceId)), + 'slug' => $this->slugify(value: 'db-'.$sourceId), + 'source' => $sourceId, + 'description' => 'Virtual register introspected from database source '.$sourceId.'.', + ], + 'schemas' => $schemas, + ]; + }//end buildBlueprint() + + /** + * Introspect a source and persist the register + schemas idempotently. + * + * A second run over the same source updates the existing register and + * schemas (matched by source id / table name) and logs structural drift via + * {@see SchemaDiffService}; it never creates duplicates. + * + * @param Source $source The `type: database` source to introspect. + * + * @return array{register: int, created: array, updated: array, drift: array>} + * + * @throws DbalConnectionException When the source cannot be connected. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function introspect(Source $source): array + { + $blueprint = $this->buildBlueprint(source: $source); + $sourceId = $this->sourceId(source: $source); + + $register = $this->upsertRegister(definition: $blueprint['register'], sourceId: $sourceId); + $existingSchemas = $this->existingSchemasByTable(register: $register); + + $created = []; + $updated = []; + $drift = []; + $schemaIds = []; + + foreach ($blueprint['schemas'] as $definition) { + $table = $definition['configuration']['x-openregister-object-source']['config']['table']; + + if (isset($existingSchemas[$table]) === true) { + $existing = $existingSchemas[$table]; + $changeSet = $this->diffService->diff( + old: ['properties' => $existing->getProperties(), 'required' => $existing->getRequired()], + new: ['properties' => $definition['properties'], 'required' => ($definition['required'] ?? [])] + ); + + if ($changeSet->hasChanges() === true) { + $drift[] = ['table' => $table, 'changes' => $changeSet->getChanges()]; + $this->logger->info( + sprintf('[DbalIntrospection] drift on table "%s": %d change(s)', $table, count($changeSet->getChanges())) + ); + } + + $saved = $this->schemaMapper->updateFromArray(id: (int) $existing->getId(), object: $definition); + $updated[] = $table; + $schemaIds[] = (int) $saved->getId(); + continue; + }//end if + + $saved = $this->schemaMapper->createFromArray(object: $definition); + $created[] = $table; + $schemaIds[] = (int) $saved->getId(); + }//end foreach + + // Keep the register's schema-id list in sync with what we just wrote. + $register->setSchemas(array_values(array_unique($schemaIds))); + $this->registerMapper->update($register); + + return [ + 'register' => (int) $register->getId(), + 'created' => $created, + 'updated' => $updated, + 'drift' => $drift, + ]; + }//end introspect() + + /** + * Introspect one table/view into a working record. + * + * @param AbstractSchemaManager $schemaManager The schema manager. + * @param string $sourceId The owning source id. + * @param string $tableName The table/view name. + * @param bool $isView Whether this is a view. + * + * @return array The working table record. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function introspectTable(AbstractSchemaManager $schemaManager, string $sourceId, string $tableName, bool $isView): array + { + $columns = $schemaManager->listTableColumns($tableName); + $primaryKey = $this->primaryKeyColumns(schemaManager: $schemaManager, tableName: $tableName, isView: $isView); + + $properties = []; + $required = []; + $nonFilterable = []; + + foreach ($columns as $column) { + $name = $column->getName(); + $fragment = $this->typeMapper->mapColumn(column: $column); + + if (($fragment['x-filterable'] ?? true) === false) { + $nonFilterable[] = $name; + } + + unset($fragment['x-filterable']); + $properties[$name] = $fragment; + + if ($this->isRequired(column: $column, primaryKey: $primaryKey) === true) { + $required[] = $name; + } + } + + [$idColumn, $idColumns] = $this->resolveIdentity(primaryKey: $primaryKey, tableName: $tableName); + + // Primary-key columns are declared `type: string`: the virtual object + // id IS a string (reads present it as one, URLs deliver it as one), + // so an integer declaration would fail write validation for the very + // value the read path returns. The database still stores/coerces the + // native column type. + $pkColumns = ($idColumns ?? []); + if ($idColumn !== null) { + $pkColumns = [$idColumn]; + } + + foreach ($pkColumns as $pkColumn) { + if (isset($properties[$pkColumn]) === true) { + $properties[$pkColumn] = array_merge( + $properties[$pkColumn], + ['type' => 'string', 'description' => 'External primary key (presented as a string).'] + ); + unset($properties[$pkColumn]['format']); + } + } + + return [ + 'table' => $tableName, + 'slug' => $this->slugify(value: $tableName), + 'title' => $tableName, + 'isView' => $isView, + 'idColumn' => $idColumn, + 'idColumns' => $idColumns, + 'properties' => $properties, + 'required' => $required, + 'nonFilterable' => $nonFilterable, + 'sourceId' => $sourceId, + ]; + }//end introspectTable() + + /** + * Derive a view's properties from a single sample row. + * + * DBAL 3.x schema managers only introspect columns of real tables, so a + * view's shape is read from `SELECT * … LIMIT 1` and typed from the PHP + * value types. An empty view yields no properties (list-only via SELECT *). + * + * @param Connection $connection The open connection. + * @param string $viewName The view name. + * + * @return array> Property name → JSON-Schema fragment. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function viewColumnsFromSample(Connection $connection, string $viewName): array + { + try { + $qb = $connection->createQueryBuilder(); + $qb->select('*') + ->from($connection->getDatabasePlatform()->quoteIdentifier($viewName)) + ->setMaxResults(1); + $row = $qb->executeQuery()->fetchAssociative(); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('[DbalIntrospection] could not sample view "%s": %s', $viewName, $e->getMessage()) + ); + return []; + } + + if (is_array($row) === false) { + return []; + } + + $properties = []; + foreach ($row as $column => $value) { + $properties[(string) $column] = match (gettype($value)) { + 'integer' => ['type' => 'integer'], + 'double' => ['type' => 'number'], + 'boolean' => ['type' => 'boolean'], + default => ['type' => 'string'], + }; + } + + return $properties; + }//end viewColumnsFromSample() + + /** + * Resolve the ordered primary-key column names for a table. + * + * @param AbstractSchemaManager $schemaManager The schema manager. + * @param string $tableName The table name. + * @param bool $isView Whether this is a view (never has a PK). + * + * @return array The PK column names in order (possibly empty). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function primaryKeyColumns(AbstractSchemaManager $schemaManager, string $tableName, bool $isView): array + { + if ($isView === true) { + return []; + } + + try { + $table = $schemaManager->introspectTable($tableName); + } catch (Throwable $e) { + return []; + } + + return $this->tablePrimaryKey(table: $table); + }//end primaryKeyColumns() + + /** + * Extract the ordered primary-key column list from a DBAL Table. + * + * @param Table $table The introspected table. + * + * @return array The PK column names in order (possibly empty). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function tablePrimaryKey(Table $table): array + { + $pk = $table->getPrimaryKey(); + if ($pk === null) { + return []; + } + + return array_values($pk->getColumns()); + }//end tablePrimaryKey() + + /** + * Decide the id shape from the primary key (single / composite / none). + * + * @param array $primaryKey The ordered PK column names. + * @param string $tableName The table name (for logging). + * + * @return array{0: string|null, 1: array|null} [idColumn, idColumns]. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function resolveIdentity(array $primaryKey, string $tableName): array + { + if (count($primaryKey) === 1) { + return [$primaryKey[0], null]; + } + + if (count($primaryKey) > 1) { + $this->logger->info( + sprintf('[DbalIntrospection] table "%s" has a composite primary key (%s) — using a joined id', $tableName, implode(', ', $primaryKey)) + ); + return [null, array_values($primaryKey)]; + } + + $this->logger->info( + sprintf('[DbalIntrospection] table "%s" has no primary key — schema is read-list-only', $tableName) + ); + return [null, null]; + }//end resolveIdentity() + + /** + * Whether a column becomes a `required` property. + * + * `NOT NULL` columns are required EXCEPT the primary key and any column that + * carries a database default (both are server-populated, so their absence in + * a read projection is valid — design D5). + * + * @param Column $column The introspected column. + * @param array $primaryKey The PK column names. + * + * @return bool True when the column is required. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function isRequired(Column $column, array $primaryKey): bool + { + if ($column->getNotnull() === false) { + return false; + } + + if (in_array($column->getName(), $primaryKey, true) === true) { + return false; + } + + return ($column->getDefault() === null); + }//end isRequired() + + /** + * Map single-column foreign keys onto the relation dialect and add inverses. + * + * @param AbstractSchemaManager $schemaManager The schema manager. + * @param array> $tables The working records, keyed by table name (mutated). + * @param array $slugByTable Table name → schema slug. + * + * @return void + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function applyForeignKeys(AbstractSchemaManager $schemaManager, array &$tables, array $slugByTable): void + { + foreach ($tables as $tableName => $record) { + if ($record['isView'] === true) { + continue; + } + + $foreignKeys = $this->foreignKeysFor(schemaManager: $schemaManager, tableName: $tableName); + foreach ($foreignKeys as $fk) { + $localColumns = array_values($fk->getLocalColumns()); + if (count($localColumns) !== 1) { + $this->logger->info( + sprintf('[DbalIntrospection] skipping multi-column foreign key on "%s" (v1 maps single-column FKs only)', $tableName) + ); + continue; + } + + $localColumn = $localColumns[0]; + $foreignTable = $this->shortName(name: $fk->getForeignTableName()); + if (isset($slugByTable[$foreignTable]) === false) { + continue; + } + + $targetSlug = $slugByTable[$foreignTable]; + + // Owning side: keep the raw key but type it as a relation. + // A `format` is carried over only when the column's own type + // mapping produced one (e.g. GUID → uuid) — external primary + // keys are commonly plain integers/strings with no JSON-Schema + // format, and relation resolution keys on $ref + handling. + $relationProperty = [ + 'type' => 'string', + '$ref' => $targetSlug, + 'objectConfiguration' => ['handling' => 'related-object'], + ]; + + $existingFormat = ($tables[$tableName]['properties'][$localColumn]['format'] ?? null); + if (is_string($existingFormat) === true && $existingFormat !== '') { + $relationProperty['format'] = $existingFormat; + } + + $tables[$tableName]['properties'][$localColumn] = $relationProperty; + + // Inverse side on the referenced schema. + $inverseName = $tableName.'_via_'.$localColumn; + $tables[$foreignTable]['properties'][$inverseName] = [ + 'type' => 'array', + 'items' => [ + '$ref' => $slugByTable[$tableName], + 'inversedBy' => $localColumn, + ], + 'objectConfiguration' => ['handling' => 'related-object'], + ]; + }//end foreach + }//end foreach + }//end applyForeignKeys() + + /** + * List the foreign keys for a table, tolerating platforms/views that error. + * + * @param AbstractSchemaManager $schemaManager The schema manager. + * @param string $tableName The table name. + * + * @return array The foreign-key constraints (possibly empty). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function foreignKeysFor(AbstractSchemaManager $schemaManager, string $tableName): array + { + try { + return array_values($schemaManager->listTableForeignKeys($tableName)); + } catch (Throwable $e) { + return []; + } + }//end foreignKeysFor() + + /** + * Serialise a working record into a Schema definition array. + * + * @param array $record The working table record. + * @param bool $writable Whether the backing source currently allows writes. + * + * @return array The schema definition for the mapper. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function toSchemaDefinition(array $record, bool $writable=false): array + { + $config = [ + 'sourceId' => $record['sourceId'], + 'table' => $record['table'], + 'idColumn' => $record['idColumn'], + ]; + + if ($record['idColumns'] !== null) { + $config['idColumns'] = $record['idColumns']; + } + + if (empty($record['nonFilterable']) === false) { + $config['nonFilterable'] = $record['nonFilterable']; + } + + if ($record['isView'] === true) { + $config['isView'] = true; + } + + // Opt-in writes (design D2): tables of a writable source get + // `readOnly: false`; views are read-only ALWAYS. The write dispatch + // additionally re-verifies the Source's writable flag live, so a stale + // annotation can never authorize a write on its own. + $readOnly = true; + if ($writable === true && $record['isView'] !== true) { + $readOnly = false; + } + + return [ + 'title' => $record['title'], + 'slug' => $record['slug'], + 'properties' => $record['properties'], + 'required' => $record['required'], + 'configuration' => [ + 'x-openregister-object-source' => [ + 'provider' => self::PROVIDER_ID, + 'readOnly' => $readOnly, + 'config' => $config, + ], + ], + ]; + }//end toSchemaDefinition() + + /** + * Find or create the register for a source and return it. + * + * @param array $definition The register blueprint. + * @param string $sourceId The owning source id. + * + * @return Register The persisted register. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function upsertRegister(array $definition, string $sourceId): Register + { + $existing = $this->registerMapper->findAll(filters: ['source' => $sourceId]); + if (empty($existing) === false) { + return $existing[0]; + } + + return $this->registerMapper->createFromArray(object: $definition); + }//end upsertRegister() + + /** + * Index a register's existing schemas by their bound source table name. + * + * @param Register $register The register whose schemas to load. + * + * @return array Table name → schema. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function existingSchemasByTable(Register $register): array + { + $ids = $register->getSchemas(); + if (empty($ids) === true) { + return []; + } + + $byTable = []; + foreach ($this->schemaMapper->findMultiple(ids: $ids) as $schema) { + $source = $schema->getObjectSource(); + $table = ($source['config']['table'] ?? null); + if (is_string($table) === true && $table !== '') { + $byTable[$table] = $schema; + } + } + + return $byTable; + }//end existingSchemasByTable() + + /** + * Resolve the source id used as the register `source` and config `sourceId`. + * + * @param Source $source The database source. + * + * @return string The stable source identifier. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function sourceId(Source $source): string + { + $uuid = $source->getUuid(); + if ($uuid !== null && $uuid !== '') { + return (string) $uuid; + } + + return (string) $source->getId(); + }//end sourceId() + + /** + * Reduce a possibly-qualified identifier (`schema.table`) to its short name. + * + * @param string $name The identifier. + * + * @return string The unqualified name. + * + * @spec exclude Private identifier helper; no behavioural contract. + */ + private function shortName(string $name): string + { + $pos = strrpos($name, '.'); + if ($pos === false) { + return $name; + } + + return substr($name, ($pos + 1)); + }//end shortName() + + /** + * Whether a table/view name belongs to a database system catalog. + * + * DBAL's schema manager can surface engine-internal objects — on + * PostgreSQL `listViews()` returns `pg_catalog.*` and + * `information_schema.*` views (observed live: `pg_user_mappings` + * appears in BOTH namespaces, colliding on one slug); MySQL exposes + * `mysql`/`performance_schema`/`sys`, SQLite reserves the `sqlite_` + * prefix. None of these are user data — a virtual register must never + * contain them. + * + * @param string $name The (possibly namespace-qualified) object name. + * + * @return bool True when the object is engine-internal. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function isSystemObject(string $name): bool + { + $lower = strtolower($name); + $short = strtolower($this->shortName(name: $name)); + + $systemNamespaces = [ + 'pg_catalog.', + 'information_schema.', + 'pg_toast.', + 'mysql.', + 'performance_schema.', + 'sys.', + ]; + foreach ($systemNamespaces as $prefix) { + if (str_starts_with($lower, $prefix) === true) { + return true; + } + } + + // Unqualified engine-internal names: SQLite reserved tables and + // PostgreSQL catalog objects surfaced without their namespace. + return str_starts_with($short, 'sqlite_') === true + || str_starts_with($short, 'pg_') === true; + }//end isSystemObject() + + /** + * Build a lowercase hyphenated slug from an arbitrary identifier. + * + * @param string $value The raw value. + * + * @return string The slug. + * + * @spec exclude Private slug helper; no behavioural contract. + */ + private function slugify(string $value): string + { + $slug = strtolower($value); + $slug = (string) preg_replace('/[^a-z0-9]+/', '-', $slug); + return trim($slug, '-'); + }//end slugify() +}//end class diff --git a/lib/Service/Dbal/DbalConnectionException.php b/lib/Service/Dbal/DbalConnectionException.php new file mode 100644 index 0000000000..d54f005ab9 --- /dev/null +++ b/lib/Service/Dbal/DbalConnectionException.php @@ -0,0 +1,36 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Dbal; + +use RuntimeException; + +/** + * Raised when a DBAL virtual-register connection cannot be established. + */ +class DbalConnectionException extends RuntimeException +{ +}//end class diff --git a/lib/Service/Dbal/DbalConnectionFactory.php b/lib/Service/Dbal/DbalConnectionFactory.php new file mode 100644 index 0000000000..b48196208c --- /dev/null +++ b/lib/Service/Dbal/DbalConnectionFactory.php @@ -0,0 +1,307 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Dbal; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DriverManager; +use OCA\OpenRegister\Db\Source; +use OCA\OpenRegister\Service\Credential\CredentialStore; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Builds and caches read-only DBAL connections for database-type sources. + */ +class DbalConnectionFactory +{ + /** + * DBAL drivers OpenRegister supports for virtual registers (v1). + * + * @var array + */ + public const SUPPORTED_DRIVERS = ['pdo_mysql', 'pdo_pgsql', 'pdo_sqlite']; + + /** + * Per-request connection cache keyed by source id. Not a cross-request pool. + * + * @var array + */ + private array $connections = []; + + /** + * Constructor. + * + * @param CredentialStore $credentialStore The active credential-store leaf (resolved upstream). + * @param LoggerInterface $logger Secret-free diagnostics. + * + * @return void + */ + public function __construct( + private readonly CredentialStore $credentialStore, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Return a cached read-only DBAL connection for the given database source. + * + * Fails closed: any credential-resolution or connection error raises a + * {@see DbalConnectionException}; an unauthenticated connection is never + * returned. The password is resolved through the credential custody seam and + * never logged. + * + * @param Source $source The `type: database` source to connect to. + * + * @return Connection The open (lazy) DBAL connection. + * + * @throws DbalConnectionException When the source is misconfigured, the + * credential cannot be resolved, or DBAL fails. + * + * @SuppressWarnings(PHPMD.StaticAccess) DriverManager::getConnection is DBAL's + * only public connection entry point. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function getConnection(Source $source): Connection + { + $cacheKey = $this->cacheKey(source: $source); + if (isset($this->connections[$cacheKey]) === true) { + return $this->connections[$cacheKey]; + } + + $params = $this->buildParams(source: $source); + + try { + $connection = DriverManager::getConnection($params); + } catch (Throwable $e) { + // Do NOT include $params — it carries the resolved password. + $this->logger->warning( + '[DbalConnectionFactory] could not build connection for source '.$this->sourceLabel(source: $source).': '.$e->getMessage() + ); + throw new DbalConnectionException( + 'Unable to open a database connection for the configured source.', + 0, + $e + ); + } + + $this->connections[$cacheKey] = $connection; + return $connection; + }//end getConnection() + + /** + * Whether the named DBAL driver's PHP extension is available on this instance. + * + * Used by the provider's `isEnabled()` to degrade a bound schema to an empty + * list (rather than erroring) when e.g. `pdo_pgsql` is not installed. + * + * @param string $driver The DBAL driver id (e.g. `pdo_pgsql`). + * + * @return bool True when the driver is supported and its PDO extension loaded. + * + * @SuppressWarnings(PHPMD.StaticAccess) PDO::getAvailableDrivers is the only + * way to probe installed PDO drivers. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function isDriverAvailable(string $driver): bool + { + $pdoDrivers = [ + 'pdo_mysql' => 'mysql', + 'pdo_pgsql' => 'pgsql', + 'pdo_sqlite' => 'sqlite', + ]; + + $pdoDriver = ($pdoDrivers[$driver] ?? null); + if ($pdoDriver === null || extension_loaded('pdo') === false) { + return false; + } + + return in_array($pdoDriver, \PDO::getAvailableDrivers(), true); + }//end isDriverAvailable() + + /** + * Assemble the DBAL connection parameter array for a database source, + * resolving the password through the credential custody seam. + * + * @param Source $source The database source. + * + * @return array The DriverManager params (may contain the password). + * + * @throws DbalConnectionException On invalid config or an unresolvable credential. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function buildParams(Source $source): array + { + $config = ($source->getAuthConfig() ?? []); + if ($config === []) { + throw new DbalConnectionException('Database source has no connection configuration.'); + } + + $driver = (string) ($config['driver'] ?? ''); + if (in_array($driver, self::SUPPORTED_DRIVERS, true) === false) { + throw new DbalConnectionException('Unsupported or missing database driver for the source.'); + } + + if ($this->isDriverAvailable(driver: $driver) === false) { + throw new DbalConnectionException('The database driver extension is not available on this instance.'); + } + + $params = array_merge( + ['driver' => $driver], + $this->driverSpecificParams(driver: $driver, config: $config) + ); + + $params['password'] = $this->resolvePassword(source: $source, config: $config); + + return $params; + }//end buildParams() + + /** + * The non-secret, driver-specific connection parameters. + * + * SQLite sources connect to a file path; network drivers (mysql/pgsql) + * require host + dbname and optionally port/user. + * + * @param string $driver The validated DBAL driver id. + * @param array $config The source `authConfig` block. + * + * @return array The driver-specific params (never a secret). + * + * @throws DbalConnectionException When required connection parts are missing. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function driverSpecificParams(string $driver, array $config): array + { + if ($driver === 'pdo_sqlite') { + $path = (string) ($config['path'] ?? ''); + if ($path === '') { + throw new DbalConnectionException('SQLite database source requires a file path.'); + } + + return ['path' => $path]; + } + + $params = [ + 'host' => (string) ($config['host'] ?? ''), + 'dbname' => (string) ($config['dbname'] ?? ''), + 'user' => (string) ($config['user'] ?? ''), + ]; + + if (isset($config['port']) === true && $config['port'] !== null && $config['port'] !== '') { + $params['port'] = (int) $config['port']; + } + + if ($params['host'] === '' || $params['dbname'] === '') { + throw new DbalConnectionException('Database source requires host and dbname.'); + } + + return $params; + }//end driverSpecificParams() + + /** + * Resolve the source's DB password from the credential custody seam. + * + * When a `credential` UUID is configured it MUST resolve to a secret — an + * absent secret fails closed (never an unauthenticated connection). When no + * credential is configured (e.g. a local SQLite file or a socket-trusted + * user) an empty password is used. + * + * @param Source $source The database source (for diagnostics). + * @param array $config The source `authConfig` block. + * + * @return string The resolved password, or '' when no credential is configured. + * + * @throws DbalConnectionException When a configured credential cannot be resolved. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function resolvePassword(Source $source, array $config): string + { + $credentialUuid = (string) ($config['credential'] ?? ''); + if ($credentialUuid === '') { + return ''; + } + + $scope = (string) ($config['credentialScope'] ?? 'organisation'); + $secret = $this->credentialStore->get($credentialUuid, $scope); + + if ($secret === null) { + $this->logger->warning( + '[DbalConnectionFactory] credential could not be resolved for source '.$this->sourceLabel(source: $source).' — failing closed' + ); + throw new DbalConnectionException('The database credential could not be resolved from the credential store.'); + } + + return $secret; + }//end resolvePassword() + + /** + * Build the per-request cache key for a source. + * + * @param Source $source The database source. + * + * @return string The cache key. + * + * @spec exclude Private cache-key helper; behaviour covered by getConnection() caching. + */ + private function cacheKey(Source $source): string + { + return 'id:'.((string) $source->getId()).':uuid:'.((string) $source->getUuid()); + }//end cacheKey() + + /** + * A secret-free label identifying a source in log lines. + * + * @param Source $source The database source. + * + * @return string The label (uuid or id — never a credential). + * + * @spec exclude Private log-label helper; no behavioural contract. + */ + private function sourceLabel(Source $source): string + { + $uuid = $source->getUuid(); + if ($uuid !== null && $uuid !== '') { + return (string) $uuid; + } + + return '#'.(string) $source->getId(); + }//end sourceLabel() +}//end class diff --git a/lib/Service/Dbal/SqlTypeMapper.php b/lib/Service/Dbal/SqlTypeMapper.php new file mode 100644 index 0000000000..c535146238 --- /dev/null +++ b/lib/Service/Dbal/SqlTypeMapper.php @@ -0,0 +1,175 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Dbal; + +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; +use Psr\Log\LoggerInterface; + +/** + * Translates DBAL column types into JSON-Schema property fragments. + */ +class SqlTypeMapper +{ + /** + * Base map from DBAL type name → [json type, json format|null]. + * + * @var array + */ + private const TYPE_MAP = [ + Types::STRING => ['string', null], + Types::ASCII_STRING => ['string', null], + Types::TEXT => ['string', null], + Types::GUID => ['string', 'uuid'], + Types::INTEGER => ['integer', null], + Types::SMALLINT => ['integer', null], + Types::BIGINT => ['integer', null], + Types::DECIMAL => ['number', null], + Types::FLOAT => ['number', null], + Types::BOOLEAN => ['boolean', null], + Types::DATE_MUTABLE => ['string', 'date'], + Types::DATE_IMMUTABLE => ['string', 'date'], + Types::TIME_MUTABLE => ['string', 'time'], + Types::TIME_IMMUTABLE => ['string', 'time'], + Types::DATETIME_MUTABLE => ['string', 'date-time'], + Types::DATETIME_IMMUTABLE => ['string', 'date-time'], + Types::DATETIMETZ_MUTABLE => ['string', 'date-time'], + Types::DATETIMETZ_IMMUTABLE => ['string', 'date-time'], + Types::JSON => ['object', null], + Types::BINARY => ['string', 'binary'], + Types::BLOB => ['string', 'binary'], + Types::SIMPLE_ARRAY => ['array', null], + Types::ARRAY => ['array', null], + ]; + + /** + * DBAL type names that must not be used in a SQL WHERE predicate. + * + * @var array + */ + private const NON_FILTERABLE_TYPES = [ + Types::BINARY, + Types::BLOB, + Types::JSON, + Types::SIMPLE_ARRAY, + Types::ARRAY, + ]; + + /** + * Constructor. + * + * @param LoggerInterface $logger Logger for unknown-type fallback warnings. + * + * @return void + */ + public function __construct( + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Map a DBAL column onto a JSON-Schema property fragment. + * + * @param Column $column The introspected column. + * + * @return array The property fragment (`type`, optional + * `format`/`maxLength`/`description`, and an + * internal `x-filterable` flag). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function mapColumn(Column $column): array + { + $typeName = $this->typeName(column: $column); + + if (isset(self::TYPE_MAP[$typeName]) === false) { + $this->logger->warning( + sprintf( + '[SqlTypeMapper] unknown column type "%s" on column "%s" — falling back to string (non-filterable)', + $typeName, + $column->getName() + ) + ); + + return [ + 'type' => 'string', + 'x-filterable' => false, + ]; + } + + [$jsonType, $format] = self::TYPE_MAP[$typeName]; + + $property = ['type' => $jsonType]; + if ($format !== null) { + $property['format'] = $format; + } + + // Map the column length onto maxLength — only for length-carrying + // string types (VARCHAR-family); TEXT is unbounded per design D5. + $length = $column->getLength(); + if (in_array($typeName, [Types::STRING, Types::ASCII_STRING], true) === true && $length !== null && $length > 0) { + $property['maxLength'] = (int) $length; + } + + // Surface DECIMAL precision/scale in the description (not a JSON-Schema keyword). + if ($typeName === Types::DECIMAL) { + $precision = $column->getPrecision(); + $scale = $column->getScale(); + if ($precision !== null) { + $property['description'] = sprintf('DECIMAL(%d,%d)', (int) $precision, (int) $scale); + } + } + + // BIGINT is carried as an integer but documented as JS-precision-lossy. + if ($typeName === Types::BIGINT) { + $property['description'] = 'BIGINT — values beyond 2^53 are precision-lossy in JSON/JS; ids are carried as strings.'; + } + + $property['x-filterable'] = (in_array($typeName, self::NON_FILTERABLE_TYPES, true) === false); + + return $property; + }//end mapColumn() + + /** + * Resolve the dialect-normalised DBAL type name for a column. + * + * @param Column $column The introspected column. + * + * @return string The registered DBAL type name (e.g. `string`, `integer`). + * + * @SuppressWarnings(PHPMD.StaticAccess) Type::lookupName is DBAL's only + * reverse lookup from a Type instance to its registered name. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function typeName(Column $column): string + { + return Type::lookupName($column->getType()); + }//end typeName() +}//end class diff --git a/lib/Service/DeckCardService.php b/lib/Service/DeckCardService.php index 6449f5f14c..85f1f94400 100644 --- a/lib/Service/DeckCardService.php +++ b/lib/Service/DeckCardService.php @@ -27,6 +27,7 @@ use OCA\OpenRegister\Db\DeckLink; use OCA\OpenRegister\Db\DeckLinkMapper; use OCP\App\IAppManager; +use OCP\IURLGenerator; use OCP\IUserSession; use Psr\Log\LoggerInterface; @@ -36,7 +37,7 @@ * @category Service * @package OCA\OpenRegister\Service * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Service composes DeckLinkMapper, + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Service composes DeckLinkMapper, * IAppManager, IUserSession, LoggerInterface, and dynamic Deck service classes; the set * cannot be reduced as each dependency serves a distinct orchestration concern. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Deck's Card entity resolves @@ -76,6 +77,13 @@ class DeckCardService */ private readonly LoggerInterface $logger; + /** + * URL generator (webroot-aware deep links). + * + * @var IURLGenerator + */ + private readonly IURLGenerator $urlGenerator; + /** * Constructor. * @@ -83,6 +91,7 @@ class DeckCardService * @param IAppManager $appManager App manager * @param IUserSession $userSession User session * @param LoggerInterface $logger Logger + * @param IURLGenerator $urlGenerator URL generator * * @return void */ @@ -90,11 +99,13 @@ public function __construct( DeckLinkMapper $deckLinkMapper, IAppManager $appManager, IUserSession $userSession, - LoggerInterface $logger + LoggerInterface $logger, + IURLGenerator $urlGenerator ) { $this->deckLinkMapper = $deckLinkMapper; $this->appManager = $appManager; $this->userSession = $userSession; + $this->urlGenerator = $urlGenerator; $this->logger = $logger; }//end __construct() @@ -151,7 +162,17 @@ public function getCardsForObject(string $objectUuid): array function (DeckLink $link) use ($cardService): array { $row = $link->jsonSerialize(); $widened = $this->extractCardFields(cardService: $cardService, cardId: $link->getCardId()); - return $row + $widened; + $row += $widened; + + // Deep-link to the specific card in the Deck app: reusing the + // canonical {app}.page.index route keeps the webroot prefix + // correct (mirrors MapLinkService/PhotoLinkService). + $deepLink = $this->buildCardDeepLink(boardId: $link->getBoardId(), cardId: $link->getCardId()); + if ($deepLink !== null) { + $row['url'] = $deepLink; + } + + return $row; }, $links ); @@ -159,6 +180,33 @@ function (DeckLink $link) use ($cardService): array { return ['results' => $results, 'total' => count($results)]; }//end getCardsForObject() + /** + * Build a webroot-aware deep-link to a specific Deck card. + * + * Returns `/index.php/apps/deck/board/{boardId}/card/{cardId}` (with the + * correct webroot prefix). Returns null when either identifier is missing + * so the relation record is returned without a `url` rather than a broken one. + * + * @param int|null $boardId The Deck board id. + * @param int|null $cardId The Deck card id. + * + * @return string|null The deep-link URL, or null when not resolvable. + */ + private function buildCardDeepLink(?int $boardId, ?int $cardId): ?string + { + if ($boardId === null || $boardId === 0 || $cardId === null || $cardId === 0) { + return null; + } + + try { + $base = $this->urlGenerator->linkToRoute('deck.page.index'); + } catch (\Throwable $e) { + return null; + } + + return rtrim($base, '/').'/board/'.$boardId.'/card/'.$cardId; + }//end buildCardDeepLink() + /** * Resolve Deck's CardService from the server container. * @@ -195,7 +243,7 @@ private function resolveDeckCardService(): ?object * @SuppressWarnings(PHPMD.CyclomaticComplexity) Defensive null/type guards for service * and card availability are mandatory; Deck's entity magic prevents compile-time * checks, requiring runtime branches that cannot be removed without losing safety. - * @SuppressWarnings(PHPMD.NPathComplexity) NPath inflation is caused by PHPMD 2.x + * @SuppressWarnings(PHPMD.NPathComplexity) NPath inflation is caused by PHPMD 2.x * counting logical sub-expressions in guard conditions independently; the method * body is already fully decomposed into extractDueDate/extractLabels/extractAssignees. */ @@ -248,7 +296,7 @@ private function extractDueDate(object $card): ?string return $due->format(\DateTime::ATOM); } } catch (\Throwable $e) { - // getDuedate missing on this Deck version — leave null. + // GetDuedate missing on this Deck version — leave null. } return null; @@ -277,7 +325,7 @@ private function extractLabels(object $card): array return $labels; } } catch (\Throwable $e) { - // getLabels missing or returned unexpected shape — leave empty. + // GetLabels missing or returned unexpected shape — leave empty. } return []; @@ -305,7 +353,7 @@ private function extractAssignees(object $card): array return $assignees; } } catch (\Throwable $e) { - // getAssignedUsers missing — leave empty. + // GetAssignedUsers missing — leave empty. } return []; @@ -517,6 +565,14 @@ public function unlinkCard(int $linkId): void */ public function getObjectsForBoard(int $boardId): array { + // IDOR guard: only return links for a board the caller can access in + // Deck. Without this any authenticated user could enumerate board IDs + // and read OpenRegister object links for boards they have no Deck + // access to (the deckLinkMapper table bypasses Deck's own ACL). + if ($this->userCanAccessBoard(boardId: $boardId) === false) { + return []; + } + $links = $this->deckLinkMapper->findByBoardId($boardId); return array_map( @@ -527,6 +583,35 @@ static function (DeckLink $link): array { ); }//end getObjectsForBoard() + /** + * Whether the current session user may read the given Deck board. + * + * Delegates to Deck's own BoardService::find(), which enforces the board + * ACL and throws when the session user lacks read access. Fail-closed: + * returns false when Deck is unavailable or access is denied, so no link + * metadata leaks for a board the caller cannot see. + * + * @param int $boardId The Deck board ID. + * + * @return bool True when the caller may read the board. + */ + protected function userCanAccessBoard(int $boardId): bool + { + if (class_exists('OCA\\Deck\\Service\\BoardService') === false) { + return false; + } + + try { + $boardService = \OC::$server->get('OCA\\Deck\\Service\\BoardService'); + // Find() throws (NoPermissionException) when the session user + // lacks read access to the board. + $boardService->find($boardId); + return true; + } catch (\Throwable $e) { + return false; + } + }//end userCanAccessBoard() + /** * Delete all deck links for an object (cleanup). * diff --git a/lib/Service/DeckLinkService.php b/lib/Service/DeckLinkService.php index 5a0d47f962..2622857236 100644 --- a/lib/Service/DeckLinkService.php +++ b/lib/Service/DeckLinkService.php @@ -118,7 +118,7 @@ public function isDeckAvailable(): bool * OCP\AppFramework\Db\Entity::__call magic) — required to * tolerate missing fields without burying the call site. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function linkCard(string $objectUuid, int $registerId, int $schemaId, int $cardId): DeckLink { @@ -201,7 +201,7 @@ public function linkCard(string $objectUuid, int $registerId, int $schemaId, int * * @throws Exception When no matching link is found (404). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function unlinkCard(string $objectUuid, int $cardId): void { @@ -223,7 +223,7 @@ public function unlinkCard(string $objectUuid, int $cardId): void * * @return array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getLinkedCards(string $objectUuid): array { @@ -277,7 +277,7 @@ public function getLinkedCards(string $objectUuid): array * * @throws Exception On missing user, Deck unavailable, or create failure. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function createAndLinkCard( string $objectUuid, @@ -371,7 +371,7 @@ public function createAndLinkCard( * * @return array * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getAvailableBoards(): array { @@ -429,7 +429,7 @@ public function getAvailableBoards(): array * * @return array * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getStacksForBoard(int $boardId): array { diff --git a/lib/Service/DeepLinkRegistryService.php b/lib/Service/DeepLinkRegistryService.php index 0938f9d02a..ce91b21b57 100644 --- a/lib/Service/DeepLinkRegistryService.php +++ b/lib/Service/DeepLinkRegistryService.php @@ -19,11 +19,11 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-18 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-19 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-25 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-26 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-deep-link-registry-shall-resolve-urls-for-unified-search-results + * @spec openspec/specs/deep-link-registry/spec.md#requirement-notification-deep-links-shall-use-the-deep-link-registry + * @spec openspec/specs/deep-link-registry/spec.md + * @spec openspec/specs/deep-link-registry/spec.md + * @spec openspec/specs/deep-link-registry/spec.md */ declare(strict_types=1); @@ -122,23 +122,27 @@ public function __construct( /** * Register a deep link pattern for a (register, schema) combination. * - * @param string $appId The consuming app ID (e.g., "procest") - * @param string $registerSlug The register slug - * @param string $schemaSlug The schema slug - * @param string $urlTemplate URL template with placeholders (e.g., "/apps/procest/#/cases/{uuid}") - * @param string $icon Optional icon identifier (defaults to "icon-{appId}") + * @param string $appId The consuming app ID (e.g., "procest") + * @param string $registerSlug The register slug + * @param string $schemaSlug The schema slug + * @param string $urlTemplate URL template with placeholders (e.g., "/apps/procest/#/cases/{uuid}") + * @param string $icon Optional icon identifier (defaults to "icon-{appId}") + * @param string|null $displayName Optional human-readable label for the app's + * unified-search results (defaults to null → app id) * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-19 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-notification-deep-links-shall-use-the-deep-link-registry + * @spec openspec/specs/deep-link-registry/spec.md + * @spec openspec/specs/deep-link-registry/spec.md */ public function register( string $appId, string $registerSlug, string $schemaSlug, string $urlTemplate, - string $icon='' + string $icon='', + ?string $displayName=null ): void { $key = $registerSlug.'::'.$schemaSlug; @@ -165,6 +169,7 @@ public function register( schemaSlug: $schemaSlug, urlTemplate: $urlTemplate, icon: $effectiveIcon, + displayName: $displayName, ); $this->logger->debug( @@ -185,8 +190,8 @@ public function register( * * @return DeepLinkRegistration|null The registration, or null if none exists * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-18 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-deep-link-registry-shall-resolve-urls-for-unified-search-results + * @spec openspec/specs/deep-link-registry/spec.md */ public function resolve(int $registerId, int $schemaId): ?DeepLinkRegistration { @@ -218,8 +223,8 @@ public function resolve(int $registerId, int $schemaId): ?DeepLinkRegistration * * @return string|null The resolved URL, or null to use default * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-18 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-deep-link-registry-shall-resolve-urls-for-unified-search-results + * @spec openspec/specs/deep-link-registry/spec.md */ public function resolveUrl( int $registerId, @@ -246,8 +251,8 @@ public function resolveUrl( * * @return string|null The icon identifier, or null * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-18 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-26 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-deep-link-registry-shall-resolve-urls-for-unified-search-results + * @spec openspec/specs/deep-link-registry/spec.md */ public function resolveIcon(int $registerId, int $schemaId): ?string { @@ -255,12 +260,41 @@ public function resolveIcon(int $registerId, int $schemaId): ?string return $registration?->icon; }//end resolveIcon() + /** + * Resolve the display name for a registered (register, schema) pair. + * + * Returns the registration's explicit `displayName` when set, falling + * back to its `appId` for a claimed pair, and `null` for an unclaimed + * pair. Used by the unified search provider to label results per owning + * app. + * + * @param int $registerId The register database ID + * @param int $schemaId The schema database ID + * + * @return string|null The display name, the app id, or null if unclaimed + * + * @spec openspec/specs/deep-link-registry/spec.md + */ + public function resolveDisplayName(int $registerId, int $schemaId): ?string + { + $registration = $this->resolve(registerId: $registerId, schemaId: $schemaId); + if ($registration === null) { + return null; + } + + if ($registration->displayName !== null && $registration->displayName !== '') { + return $registration->displayName; + } + + return $registration->appId; + }//end resolveDisplayName() + /** * Lazily build the ID↔slug maps from database. * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-notification-deep-links-shall-use-the-deep-link-registry */ private function ensureIdMaps(): void { @@ -317,7 +351,7 @@ private function ensureIdMaps(): void * * @return bool True if at least one deep link is registered * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-notification-deep-links-shall-use-the-deep-link-registry */ public function hasRegistrations(): bool { @@ -329,7 +363,7 @@ public function hasRegistrations(): bool * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-19 + * @spec openspec/specs/deep-link-registry/spec.md#requirement-notification-deep-links-shall-use-the-deep-link-registry */ public static function reset(): void { diff --git a/lib/Service/Deferral/DeferredEntryObjectResolver.php b/lib/Service/Deferral/DeferredEntryObjectResolver.php new file mode 100644 index 0000000000..77f4cb15a4 --- /dev/null +++ b/lib/Service/Deferral/DeferredEntryObjectResolver.php @@ -0,0 +1,134 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Deferral; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; + +/** + * Resolves deferred job entries back to live objects, stale-safe. + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-1.4 + */ +class DeferredEntryObjectResolver +{ + /** + * Wire the object lookup. + * + * @param ObjectService $objectService Object lookup service. + * @param LoggerInterface $logger PSR logger. + * + * @return void + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Re-fetch the current object for a job entry. + * + * @param array $entry Entry carrying `uuid`, `register`, `schema`. + * + * @return ObjectEntity|null Current entity, or null when the entry is + * malformed, the object no longer exists, or it + * has been soft-deleted (stale no-op signal). + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function resolve(array $entry): ?ObjectEntity + { + $uuid = $this->nonEmptyString(value: ($entry['uuid'] ?? null)); + if ($uuid === null) { + return null; + } + + try { + $object = $this->objectService->find( + id: $uuid, + register: $this->nonEmptyString(value: ($entry['register'] ?? null)), + schema: $this->nonEmptyString(value: ($entry['schema'] ?? null)), + _rbac: false, + _multitenancy: false, + _render: false + ); + } catch (\Throwable $e) { + $this->logger->info( + message: '[DeferredEntryObjectResolver] Object no longer resolvable — stale entry skipped', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $uuid, + 'error' => $e->getMessage(), + ] + ); + return null; + }//end try + + if ($object === null) { + return null; + } + + // Soft-deleted objects are stale for deferred side effects: the + // delete path already ran its inline cleanup (e.g. translation + // purge) and re-projecting/notifying would resurrect state. + $deleted = ($object->getDeleted() ?? []); + if (count($deleted) > 0) { + return null; + } + + return $object; + }//end resolve() + + /** + * Normalise a raw entry field to a non-empty string or null. + * + * @param mixed $value Raw entry value. + * + * @return string|null Non-empty string, or null for anything else. + */ + private function nonEmptyString(mixed $value): ?string + { + if (is_string($value) === true && $value !== '') { + return $value; + } + + return null; + }//end nonEmptyString() +}//end class diff --git a/lib/Service/Deferral/DeferredListenerContext.php b/lib/Service/Deferral/DeferredListenerContext.php new file mode 100644 index 0000000000..e1533f63d5 --- /dev/null +++ b/lib/Service/Deferral/DeferredListenerContext.php @@ -0,0 +1,155 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Deferral; + +/** + * Value object carrying the captured acting context of a deferred listener. + * + * Entries are small associative arrays holding object identifiers (uuid, + * register, schema, version) plus the minimal per-entry payload a listener + * semantically requires and cannot re-fetch later (e.g. the pre-update data + * snapshot for `updated` notification conditions). Full objects are never + * serialized — jobs re-fetch current state by identifier. + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-1.1 + */ +final class DeferredListenerContext +{ + /** + * Wrap a captured acting context. + * + * @param string|null $userId Acting user id at dispatch time (null = session-less origin). + * @param string|null $orgUuid Active organisation uuid at dispatch time (drift detection only). + * @param array> $entries Per-object entries for the job to process. + * + * @return void + */ + public function __construct( + private readonly ?string $userId, + private readonly ?string $orgUuid, + private readonly array $entries + ) { + }//end __construct() + + /** + * The acting user id captured at dispatch time. + * + * @return string|null User id, or null when the write had no session user. + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function getUserId(): ?string + { + return $this->userId; + }//end getUserId() + + /** + * The active organisation uuid captured at dispatch time. + * + * @return string|null Organisation uuid, or null when none was resolvable. + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function getOrganisationUuid(): ?string + { + return $this->orgUuid; + }//end getOrganisationUuid() + + /** + * The per-object entries the job must process. + * + * @return array> Entry arrays (uuid/register/schema/version + payload). + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function getEntries(): array + { + return $this->entries; + }//end getEntries() + + /** + * Serialize to the array shape stored in `oc_jobs.argument`. + * + * @return array JSON-serializable job arguments. + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function toJobArguments(): array + { + return [ + 'userId' => $this->userId, + 'organisationUuid' => $this->orgUuid, + 'entries' => array_values($this->entries), + ]; + }//end toJobArguments() + + /** + * Rebuild a context from a job's argument payload. + * + * Tolerant of malformed input (non-array argument, missing keys, + * non-array entries): invalid pieces are dropped rather than thrown on, + * so a poisoned job row degrades to a logged no-op instead of a crash + * loop in cron. + * + * @param mixed $argument Raw job argument as delivered by the job list. + * + * @return self Reconstructed context (possibly with zero entries). + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public static function fromJobArguments(mixed $argument): self + { + if (is_array($argument) === false) { + return new self(userId: null, orgUuid: null, entries: []); + } + + $userId = $argument['userId'] ?? null; + if (is_string($userId) === false || $userId === '') { + $userId = null; + } + + $orgUuid = $argument['organisationUuid'] ?? null; + if (is_string($orgUuid) === false || $orgUuid === '') { + $orgUuid = null; + } + + $entries = []; + $rawList = $argument['entries'] ?? []; + if (is_array($rawList) === true) { + foreach ($rawList as $rawEntry) { + if (is_array($rawEntry) === true) { + $entries[] = $rawEntry; + } + } + } + + return new self(userId: $userId, orgUuid: $orgUuid, entries: $entries); + }//end fromJobArguments() +}//end class diff --git a/lib/Service/Deferral/ListenerDeferralService.php b/lib/Service/Deferral/ListenerDeferralService.php new file mode 100644 index 0000000000..16bcd2fbd7 --- /dev/null +++ b/lib/Service/Deferral/ListenerDeferralService.php @@ -0,0 +1,330 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Deferral; + +use OCA\OpenRegister\Service\OrganisationService; +use OCP\BackgroundJob\IJobList; +use OCP\IAppConfig; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Buffers deferred listener work and enqueues actor-forwarded chunk jobs. + * + * Registered as a shared service, so all listeners in one request append to + * the same buffers and bulk saves coalesce into chunk-level jobs. + * + * @spec openspec/changes/actor-forwarded-listener-jobs/tasks.md#task-1.2 + */ +class ListenerDeferralService +{ + + /** + * Kill-switch value that restores synchronous listener execution. + * + * @var string + */ + public const MODE_INLINE = 'inline'; + + /** + * Default deferral mode: heavy listener work runs in background jobs. + * + * @var string + */ + public const MODE_BACKGROUND = 'background'; + + /** + * Default number of entries per enqueued job. + * + * @var integer + */ + public const DEFAULT_CHUNK_SIZE = 100; + + /** + * App-config key of the deferral kill switch. + * + * @var string + */ + private const CONFIG_KEY = 'listenerDeferral'; + + /** + * App id used for config lookups. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * Entry buffers keyed by job class. + * + * @var array>, dedupe: array, chunkSize: int}> + */ + private array $buffers = []; + + /** + * Whether the shutdown flush has been registered for this request. + * + * @var boolean + */ + private bool $shutdownHooked = false; + + /** + * Whether the acting context has been captured for this request. + * + * @var boolean + */ + private bool $actorCaptured = false; + + /** + * Captured acting user id (null = session-less origin). + * + * @var string|null + */ + private ?string $capturedUserId = null; + + /** + * Captured active organisation uuid (drift detection only). + * + * @var string|null + */ + private ?string $capturedOrgUuid = null; + + /** + * Wire collaborators. + * + * @param IUserSession $userSession Session used to capture the acting user. + * @param OrganisationService $organisation Resolver for the active organisation. + * @param IJobList $jobList Job list the chunk jobs are enqueued on. + * @param IAppConfig $appConfig App config holding the kill switch. + * @param LoggerInterface $logger PSR logger. + * + * @return void + */ + public function __construct( + private readonly IUserSession $userSession, + private readonly OrganisationService $organisation, + private readonly IJobList $jobList, + private readonly IAppConfig $appConfig, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Whether deferral is enabled on this instance. + * + * Reads the `openregister/listenerDeferral` app-config value; anything + * other than `inline` (including unset) means background deferral. The + * kill switch exists so an instance with an unhealthy cron can restore + * the pre-deferral synchronous behaviour operationally. + * + * @return bool True when listener work should be deferred to jobs. + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function isDeferralEnabled(): bool + { + try { + $mode = $this->appConfig->getValueString(self::APP_ID, self::CONFIG_KEY, self::MODE_BACKGROUND); + } catch (\Throwable $e) { + return true; + } + + return $mode !== self::MODE_INLINE; + }//end isDeferralEnabled() + + /** + * Buffer one entry for a job class, flushing full chunks immediately. + * + * The acting context is captured on the first call of the request (the + * session user cannot change mid-request). When `$dedupeKey` is given, + * an entry with an already-buffered key is dropped — this is how N bulk + * writes to one schema coalesce into a single threshold evaluation. + * + * Fail-soft: a capture or enqueue failure is logged and never breaks the + * save that triggered it (same blast radius as today's listener + * exception swallowing). + * + * @param string $jobClass FQCN of the ActorForwardedJob subclass. + * @param array $entry Entry payload (uuid/register/schema/version + extras). + * @param int $chunkSize Maximum entries per enqueued job. + * @param string|null $dedupeKey Optional coalescing key within the buffer. + * + * @return void + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function defer( + string $jobClass, + array $entry, + int $chunkSize=self::DEFAULT_CHUNK_SIZE, + ?string $dedupeKey=null + ): void { + try { + $this->captureActor(); + + if (isset($this->buffers[$jobClass]) === false) { + $this->buffers[$jobClass] = [ + 'entries' => [], + 'dedupe' => [], + 'chunkSize' => max(1, $chunkSize), + ]; + } + + if ($dedupeKey !== null) { + if (isset($this->buffers[$jobClass]['dedupe'][$dedupeKey]) === true) { + return; + } + + $this->buffers[$jobClass]['dedupe'][$dedupeKey] = true; + } + + $this->buffers[$jobClass]['entries'][] = $entry; + + $this->hookShutdown(); + + if (count($this->buffers[$jobClass]['entries']) >= $this->buffers[$jobClass]['chunkSize']) { + $this->flushBuffer(jobClass: $jobClass); + } + } catch (\Throwable $e) { + $this->logger->error( + message: '[ListenerDeferralService] Failed to buffer deferred listener entry', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'jobClass' => $jobClass, + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end defer() + + /** + * Flush every remaining buffer to the job list. + * + * Called at request shutdown and available to tests / orchestrators that + * need deterministic flushing. + * + * @return void + * + * @spec openspec/specs/event-driven-architecture/spec.md + */ + public function flushAll(): void + { + foreach (array_keys($this->buffers) as $jobClass) { + $this->flushBuffer(jobClass: $jobClass); + } + }//end flushAll() + + /** + * Enqueue one chunk job for a buffer and reset it. + * + * @param string $jobClass FQCN of the job to enqueue. + * + * @return void + */ + private function flushBuffer(string $jobClass): void + { + $entries = ($this->buffers[$jobClass]['entries'] ?? []); + if (count($entries) === 0) { + return; + } + + $this->buffers[$jobClass]['entries'] = []; + $this->buffers[$jobClass]['dedupe'] = []; + + $context = new DeferredListenerContext( + userId: $this->capturedUserId, + orgUuid: $this->capturedOrgUuid, + entries: $entries + ); + + try { + $this->jobList->add($jobClass, $context->toJobArguments()); + } catch (\Throwable $e) { + $this->logger->error( + message: '[ListenerDeferralService] Failed to enqueue deferred listener job', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'jobClass' => $jobClass, + 'entries' => count($entries), + 'error' => $e->getMessage(), + ] + ); + } + }//end flushBuffer() + + /** + * Capture the acting context once per request. + * + * Organisation resolution is fail-soft: a lookup failure captures null + * rather than aborting the save. Null userId is legitimate (occ/cron + * writes) — the job then runs without impersonation, matching inline + * session-less behaviour. + * + * @return void + */ + private function captureActor(): void + { + if ($this->actorCaptured === true) { + return; + } + + $this->actorCaptured = true; + $this->capturedUserId = $this->userSession->getUser()?->getUID(); + + try { + $this->capturedOrgUuid = $this->organisation->getActiveOrganisation()?->getUuid(); + } catch (\Throwable $e) { + $this->capturedOrgUuid = null; + } + }//end captureActor() + + /** + * Register the shutdown flush exactly once per request. + * + * @return void + */ + private function hookShutdown(): void + { + if ($this->shutdownHooked === true) { + return; + } + + $this->shutdownHooked = true; + register_shutdown_function([$this, 'flushAll']); + }//end hookShutdown() +}//end class diff --git a/lib/Service/DsarService.php b/lib/Service/DsarService.php index 07948e9e36..523a77f155 100644 --- a/lib/Service/DsarService.php +++ b/lib/Service/DsarService.php @@ -27,9 +27,9 @@ * @category Service * @package OCA\OpenRegister\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -297,9 +297,14 @@ public function eraseObjectsForSubject(string $subject, ?string $type=null, bool 'dryRun' => $dryRun, 'matchedCount' => count($entries), 'erased' => [], + // BUG-SVC-2: objects whose erasure failed are collected here so the + // caller can report a partial failure instead of a false "complete". + 'failed' => [], ]; if ($dryRun === true || $entries === []) { + $summary['complete'] = true; + $summary['failedCount'] = 0; return $summary; } @@ -315,6 +320,16 @@ public function eraseObjectsForSubject(string $subject, ?string $type=null, bool foreach ($entries as $entry) { $object = $this->loadObjectByEntry(entry: $entry); if ($object === null) { + // BUG-SVC-2: a matched object we cannot load was NOT erased — + // record it as a failure rather than silently skipping it. + $summary['failed'][] = [ + 'object' => $entry, + 'error' => 'Object could not be loaded for erasure', + ]; + $this->logger->warning( + message: '[DSAR] Matched object could not be loaded during vergetelheid', + context: ['object' => $entry] + ); continue; } @@ -331,6 +346,10 @@ public function eraseObjectsForSubject(string $subject, ?string $type=null, bool 'schema' => $object->getSchema(), ]; } catch (\Throwable $e) { + $summary['failed'][] = [ + 'object' => $entry, + 'error' => $e->getMessage(), + ]; $this->logger->warning( message: '[DSAR] Soft-delete failed during vergetelheid', context: ['object' => $entry, 'error' => $e->getMessage()] @@ -338,6 +357,11 @@ public function eraseObjectsForSubject(string $subject, ?string $type=null, bool } }//end foreach + // BUG-SVC-2: surface partial completion so callers don't treat a run + // with failures as a fully-successful erasure. + $summary['complete'] = ($summary['failed'] === []); + $summary['failedCount'] = count($summary['failed']); + return $summary; }//end eraseObjectsForSubject() diff --git a/lib/Service/Edepot/EdepotTransferService.php b/lib/Service/Edepot/EdepotTransferService.php index c0703a4833..e83ea639a2 100644 --- a/lib/Service/Edepot/EdepotTransferService.php +++ b/lib/Service/Edepot/EdepotTransferService.php @@ -20,9 +20,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-24 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-38 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-transfer-list-management + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-log-all-transfer-actions-in-the-audit-trail + * @spec openspec/specs/edepot-transfer/spec.md */ declare(strict_types=1); @@ -42,29 +42,21 @@ /** * Orchestrator for e-Depot transfer operations. * - * Coordinates SIP package building, transport execution with retry logic, - * per-object status tracking, audit trail logging, and notifications. + * Coordinates SIP package building, transport execution, per-object status + * tracking, audit trail logging, and notifications. One run performs a single + * transport attempt per outstanding package; durable retry/backoff is owned by + * TransferExecutionJob (no in-process wait). * * @psalm-suppress UnusedClass * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class EdepotTransferService { - /** - * Maximum number of transport retries. - */ - private const MAX_RETRIES = 3; - - /** - * Retry backoff intervals in seconds: 30s, 120s, 480s. - * - * @var array - */ - private const RETRY_BACKOFF = [30, 120, 480]; - /** * Available SIP profiles. * @@ -79,17 +71,19 @@ class EdepotTransferService /** * Constructor. * - * @param SipPackageBuilder $sipBuilder The SIP package builder. - * @param TransferListService $transferListService The transfer list service. - * @param MagicMapper $objectMapper The object mapper. - * @param AuditTrailMapper $auditTrailMapper The audit trail mapper. - * @param IAppConfig $appConfig The app configuration. - * @param INotificationManager $notificationManager The notification manager. - * @param LoggerInterface $logger Logger. + * @param SipPackageBuilder $sipBuilder The SIP package builder. + * @param TransferListService $transferListService The transfer list service. + * @param TransferRecordService $transferRecordService Durable transfer + proof persistence. + * @param MagicMapper $objectMapper The object mapper. + * @param AuditTrailMapper $auditTrailMapper The audit trail mapper. + * @param IAppConfig $appConfig The app configuration. + * @param INotificationManager $notificationManager The notification manager. + * @param LoggerInterface $logger Logger. */ public function __construct( private readonly SipPackageBuilder $sipBuilder, private readonly TransferListService $transferListService, + private readonly TransferRecordService $transferRecordService, private readonly MagicMapper $objectMapper, private readonly AuditTrailMapper $auditTrailMapper, private readonly IAppConfig $appConfig, @@ -99,52 +93,95 @@ public function __construct( }//end __construct() /** - * Execute a transfer for an approved transfer list. + * Execute a transfer for an approved transfer list (single attempt). + * + * Backward-compatible entry point: runs one transport attempt (attempt 1). + * Long-horizon cross-request retry is orchestrated by + * {@see \OCA\OpenRegister\BackgroundJob\TransferExecutionJob}, which calls + * {@see self::executeAttempt()} directly with the current attempt number. * * @param array $transferList The approved transfer list data. * @param TransportInterface $transport The transport to use. * * @return array The updated transfer list with results. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-38 - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-6 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Requirement: One job run performs one transport attempt per package) */ public function executeTransfer(array $transferList, TransportInterface $transport): array + { + return $this->executeAttempt(transferList: $transferList, transport: $transport, attempt: 1); + }//end executeTransfer() + + /** + * Run ONE transport attempt for a transfer list. + * + * No in-process `sleep()`: exactly one transport send per outstanding + * package. Objects already confirmed on a prior attempt + * (`retention.archiefstatus === 'overgebracht'`) are excluded from the + * rebuild/resend so a retry never re-ingests them (partial-success + * awareness). The attempt (number, timestamp, transport, per-package + * outcome, error) is appended to the list's append-only `attempts[]`; the + * caller (the job) decides whether to re-enqueue or escalate. + * + * @param array $transferList The transfer list data. + * @param TransportInterface $transport The transport to use. + * @param int $attempt The 1-based attempt number. + * + * @return array The updated transfer list (status, attempts[], transferResult). + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Requirement: One job run performs one transport attempt per package) + */ + public function executeAttempt(array $transferList, TransportInterface $transport, int $attempt): array { $this->logger->info( - message: '[EdepotTransferService] Starting transfer execution', + message: '[EdepotTransferService] Transfer attempt', context: [ - 'transferId' => $transferList['uuid'], + 'file' => __FILE__, + 'line' => __LINE__, + 'transferId' => ($transferList['uuid'] ?? ''), 'transport' => $transport->getName(), - 'objects' => count($transferList['objectReferences']), + 'attempt' => $attempt, ] ); $transferList['status'] = TransferListService::STATUS_IN_PROGRESS; - // Log audit: transfer initiated. - $this->logTransferInitiated(transferList: $transferList, transport: $transport->getName()); + if ($attempt === 1) { + $this->logTransferInitiated(transferList: $transferList, transport: $transport->getName()); + } - // Gather objects and their files. - $objectsWithFiles = $this->gatherObjectsWithFiles(objectRefs: $transferList['objectReferences']); + // Only objects NOT yet confirmed are (re)built/sent — no double ingest. + $outstandingRefs = $this->outstandingObjectRefs(transferList: $transferList); + $objectsWithFiles = $this->gatherObjectsWithFiles(objectRefs: $outstandingRefs); if (empty($objectsWithFiles) === true) { - $transferList['status'] = TransferListService::STATUS_FAILED; - $transferList['transferResult'] = [ - 'error' => 'No valid objects found for transfer', - 'timestamp' => (new DateTime())->format('c'), - ]; - return $transferList; + // Nothing outstanding: either everything already confirmed + // (completed) or no loadable objects (failed). + $transferList['status'] = $this->terminalStatusForNoOutstanding(transferList: $transferList); + return $this->appendAttempt( + transferList: $transferList, + attempt: $attempt, + transport: $transport->getName(), + outcome: $transferList['status'], + error: null + ); } - // Build SIP package(s). + // Build SIP package(s) in the list's chosen format (zip default | bagit). + $format = (string) ($transferList['packageFormat'] ?? 'zip'); try { - $sipFiles = $this->sipBuilder->build($transferList['uuid'], $objectsWithFiles); + $sipFiles = $this->sipBuilder->build( + transferId: (string) $transferList['uuid'], + objectsWithFiles: $objectsWithFiles, + maxPackageSize: 0, + format: $format + ); } catch (\Exception $e) { $this->logger->error( - message: '[EdepotTransferService] SIP package build failed', - context: ['error' => $e->getMessage()] + message: '[EdepotTransferService] SIP package build failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] ); $transferList['status'] = TransferListService::STATUS_FAILED; $transferList['transferResult'] = [ @@ -152,75 +189,200 @@ public function executeTransfer(array $transferList, TransportInterface $transpo 'timestamp' => (new DateTime())->format('c'), ]; $this->logTransferFailed(transferList: $transferList, error: $e->getMessage(), transport: $transport->getName()); - return $transferList; - } + return $this->appendAttempt( + transferList: $transferList, + attempt: $attempt, + transport: $transport->getName(), + outcome: TransferListService::STATUS_FAILED, + error: 'SIP build failed: '.$e->getMessage() + ); + }//end try - // Send each SIP package with retry logic. + // One transport send per package — no retry loop, no sleep. $config = $this->getTransportConfig(); $allResults = []; - foreach ($sipFiles as $sipFile) { - $result = $this->sendWithRetry(transport: $transport, sipFilePath: $sipFile, config: $config); - $allResults[] = $result; - - // Clean up temp file. + $allResults[] = $this->sendOnce(transport: $transport, sipFilePath: $sipFile, config: $config); if (file_exists($sipFile) === true) { unlink($sipFile); } } - // Process results and update object statuses. - $transferList = $this->processResults(transferList: $transferList, results: $allResults, objectsWithFiles: $objectsWithFiles); + // Process results: mark confirmed objects + create proofs + set status. + $transferList = $this->processResults( + transferList: $transferList, + results: $allResults, + objectsWithFiles: $objectsWithFiles, + packageFormat: $format, + transportName: $transport->getName() + ); + + $errorSummary = null; + foreach ($allResults as $result) { + if ($result->getErrorMessage() !== null) { + $errorSummary = $result->getErrorMessage(); + break; + } + } + + $transferList = $this->appendAttempt( + transferList: $transferList, + attempt: $attempt, + transport: $transport->getName(), + outcome: (string) $transferList['status'], + error: $errorSummary + ); - // Send notification. $this->notifyTransferCompletion(transferList: $transferList); return $transferList; - }//end executeTransfer() + }//end executeAttempt() /** - * Send a SIP file with retry logic. + * The object references not yet confirmed transferred (partial-success + * awareness): objects whose `retention.archiefstatus` is already + * `overgebracht` are excluded so a retry never re-ingests them. * - * @param TransportInterface $transport The transport to use. - * @param string $sipFilePath The SIP file path. - * @param array $config Transport configuration. + * @param array $transferList The transfer list data. * - * @return TransportResult The transport result. + * @return array> The outstanding object references. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Scenario: Retry excludes already-confirmed objects) */ - private function sendWithRetry( - TransportInterface $transport, - string $sipFilePath, - array $config - ): TransportResult { - $lastResult = null; + private function outstandingObjectRefs(array $transferList): array + { + $refs = ($transferList['objectReferences'] ?? []); + if (is_array($refs) === false) { + return []; + } - for ($attempt = 0; $attempt <= self::MAX_RETRIES; $attempt++) { - if ($attempt > 0) { - $backoff = (self::RETRY_BACKOFF[($attempt - 1)] ?? 480); - $this->logger->info( - message: '[EdepotTransferService] Retrying transfer', - context: [ - 'attempt' => $attempt, - 'backoff' => $backoff, - ] + $outstanding = []; + foreach ($refs as $ref) { + if (is_array($ref) === false || empty($ref['uuid']) === true) { + continue; + } + + try { + $object = $this->objectMapper->find($ref['uuid']); + $retention = ($object->getRetention() ?? []); + if (($retention['archiefstatus'] ?? '') === 'overgebracht') { + // Already ingested on a prior attempt — never resend. + continue; + } + } catch (\Throwable $e) { + // Unloadable object: keep it in the outstanding set so its + // failure is recorded rather than silently dropped. + $this->logger->debug( + message: '[EdepotTransferService] outstanding-check could not load object '.((string) $ref['uuid']), + context: ['file' => __FILE__, 'line' => __LINE__] ); - sleep($backoff); } - $lastResult = $transport->send($sipFilePath, $config); + $outstanding[] = $ref; + }//end foreach + + return $outstanding; + + }//end outstandingObjectRefs() + + /** + * Terminal status when no objects are outstanding: `completed` when every + * declared object is already confirmed, else `failed`. + * + * @param array $transferList The transfer list data. + * + * @return string The terminal status. + */ + private function terminalStatusForNoOutstanding(array $transferList): string + { + $refs = ($transferList['objectReferences'] ?? []); + if (is_array($refs) === false || $refs === []) { + return TransferListService::STATUS_FAILED; + } + + foreach ($refs as $ref) { + if (is_array($ref) === false || empty($ref['uuid']) === true) { + continue; + } - if ($lastResult->isSuccess() === true || $lastResult->isPartialSuccess() === true) { - return $lastResult; + try { + $object = $this->objectMapper->find($ref['uuid']); + $retention = ($object->getRetention() ?? []); + if (($retention['archiefstatus'] ?? '') !== 'overgebracht') { + return TransferListService::STATUS_FAILED; + } + } catch (\Throwable $e) { + return TransferListService::STATUS_FAILED; } } - return $lastResult ?? new TransportResult( - success: false, - errorMessage: 'All retry attempts exhausted' - ); - }//end sendWithRetry() + return TransferListService::STATUS_COMPLETED; + + }//end terminalStatusForNoOutstanding() + + /** + * Append one delivery attempt to the list's append-only `attempts[]`. + * + * @param array $transferList The transfer list data. + * @param int $attempt The attempt number. + * @param string $transport The transport name. + * @param string $outcome The resulting status. + * @param string|null $error The error message, when any. + * + * @return array The transfer list with the appended attempt. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Requirement: Append-only attempt records) + */ + private function appendAttempt( + array $transferList, + int $attempt, + string $transport, + string $outcome, + ?string $error + ): array { + $attempts = ($transferList['attempts'] ?? []); + if (is_array($attempts) === false) { + $attempts = []; + } + + $attempts[] = [ + 'attempt' => $attempt, + 'timestamp' => (new DateTime())->format('c'), + 'transport' => $transport, + 'outcome' => $outcome, + 'error' => $error, + ]; + + $transferList['attempts'] = $attempts; + + return $transferList; + + }//end appendAttempt() + + /** + * Send a SIP file once (no in-process retry). + * + * Retained for callers/tests that send a single package; the attempt + * orchestration lives in {@see self::executeAttempt()} and the job. + * + * @param TransportInterface $transport The transport to use. + * @param string $sipFilePath The SIP file path. + * @param array $config Transport configuration. + * + * @return TransportResult The transport result. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * (Requirement: One job run performs one transport attempt per package) + */ + private function sendOnce( + TransportInterface $transport, + string $sipFilePath, + array $config + ): TransportResult { + return $transport->send($sipFilePath, $config); + }//end sendOnce() /** * Gather objects and their file metadata for SIP building. @@ -239,7 +401,7 @@ private function sendWithRetry( * }> * }> Objects with file metadata. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-6 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-multiple-transport-protocols-for-sip-delivery */ private function gatherObjectsWithFiles(array $objectRefs): array { @@ -286,8 +448,8 @@ private function gatherObjectsWithFiles(array $objectRefs): array * isRendition: bool * }> File metadata array. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-6 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-multiple-transport-protocols-for-sip-delivery */ private function getObjectFiles(ObjectEntity $object): array { @@ -328,18 +490,23 @@ private function getObjectFiles(ObjectEntity $object): array * @param array $transferList The transfer list. * @param array $results Transport results. * @param array> $objectsWithFiles The objects with files. + * @param string $packageFormat The SIP format used (zip|bagit) — recorded on proofs. + * @param string $transportName The transport name — recorded on proofs. * * @return array Updated transfer list. * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-6 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Scenario: Proof created on confirmed transfer) */ private function processResults( array $transferList, array $results, - array $objectsWithFiles + array $objectsWithFiles, + string $packageFormat='zip', + string $transportName='' ): array { $allSuccess = true; $anySuccess = false; @@ -392,14 +559,28 @@ private function processResults( if ($objResult['accepted'] === true) { $anySuccess = true; - $this->markObjectTransferred(object: $object, reference: ($objResult['reference'] ?? ''), timestamp: $now); - $this->logObjectTransferred(object: $object, transferUuid: $transferList['uuid'], reference: ($objResult['reference'] ?? '')); + $reference = ($objResult['reference'] ?? ''); + $this->markObjectTransferred(object: $object, reference: $reference, timestamp: $now); + $this->logObjectTransferred(object: $object, transferUuid: $transferList['uuid'], reference: $reference); + // Durable, immutable proof-of-transfer for the confirmed object + // (OR-AD-3) — one per confirmed object, write-once, no proof for + // failed objects. + $this->createProofRecord( + object: $object, + files: ($item['files'] ?? []), + transferList: $transferList, + reference: (string) $reference, + objResult: $objResult, + packageFormat: $packageFormat, + transportName: $transportName, + confirmedAt: $now + ); continue; } $allSuccess = false; $this->markObjectTransferFailed(object: $object, error: ($objResult['error'] ?? 'Unknown error'), timestamp: $now); - } + }//end foreach // Set final transfer list status. $transferList['status'] = match (true) { @@ -440,7 +621,7 @@ private function processResults( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-6 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-multiple-transport-protocols-for-sip-delivery */ private function markObjectTransferred(ObjectEntity $object, string $reference, string $timestamp): void { @@ -463,6 +644,82 @@ private function markObjectTransferred(ObjectEntity $object, string $reference, } }//end markObjectTransferred() + /** + * Create the immutable proof-of-transfer record for one confirmed object + * (OR-AD-3) and link its UUID onto the object's retention metadata (next + * to the existing `eDepotReferentie`, additive). Write-once + best-effort: + * a proof-creation failure never unwinds the confirmed transfer. + * + * @param ObjectEntity $object The confirmed object. + * @param array $files The object's file metadata (name + checksum). + * @param array $transferList The transfer list. + * @param string $reference The e-Depot ingest reference. + * @param array $objResult The per-object transport result. + * @param string $packageFormat The SIP format (zip|bagit). + * @param string $transportName The transport used. + * @param string $confirmedAt The confirmation timestamp. + * + * @return void + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Scenario: Proof created on confirmed transfer) + */ + private function createProofRecord( + ObjectEntity $object, + array $files, + array $transferList, + string $reference, + array $objResult, + string $packageFormat, + string $transportName, + string $confirmedAt + ): void { + try { + $fileChecksums = []; + foreach ($files as $file) { + if (is_array($file) === true && empty($file['name']) === false) { + $fileChecksums[] = [ + 'name' => (string) $file['name'], + 'sha256' => (string) ($file['checksum'] ?? ''), + ]; + } + } + + $manifestHash = hash('sha256', (string) json_encode($fileChecksums)); + + $proof = $this->transferRecordService->createProof( + proof: [ + 'objectUuid' => (string) $object->getUuid(), + 'transferUuid' => (string) ($transferList['uuid'] ?? ''), + 'eDepotReference' => $reference, + 'transportReceipt' => (string) ($objResult['receipt'] ?? ($objResult['reference'] ?? '')), + 'transportName' => $transportName, + 'packageId' => (string) ($transferList['uuid'] ?? ''), + 'packageFormat' => $packageFormat, + 'packageManifestSha256' => $manifestHash, + 'fileChecksums' => $fileChecksums, + 'confirmedAt' => $confirmedAt, + ] + ); + + // Link the proof UUID onto the object's retention (additive — + // existing eDepotReferentie/transferDate readers keep working). + $proofUuid = (string) ($proof['uuid'] ?? ''); + if ($proofUuid !== '') { + $retention = ($object->getRetention() ?? []); + $retention['transferProof'] = $proofUuid; + $object->setRetention($retention); + $this->objectMapper->update($object); + } + } catch (\Throwable $e) { + $this->logger->warning( + message: '[EdepotTransferService] proof-of-transfer creation failed for '.((string) $object->getUuid()).': '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + }//end try + + }//end createProofRecord() + /** * Mark an object's transfer as failed. * @@ -472,7 +729,7 @@ private function markObjectTransferred(ObjectEntity $object, string $reference, * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-6 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-multiple-transport-protocols-for-sip-delivery */ private function markObjectTransferFailed(ObjectEntity $object, string $error, string $timestamp): void { @@ -507,7 +764,7 @@ private function markObjectTransferFailed(ObjectEntity $object, string $error, s * * @return array The transport configuration. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-configurable-e-depot-endpoint-settings */ public function getTransportConfig(): array { @@ -535,7 +792,7 @@ public function getTransportConfig(): array * * @return array Map of profile ID to display name. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-configurable-e-depot-endpoint-settings */ public function getAvailableProfiles(): array { @@ -549,7 +806,7 @@ public function getAvailableProfiles(): array * * @return bool True if valid. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-configurable-e-depot-endpoint-settings */ public function isValidProfile(string $profileName): bool { @@ -564,7 +821,7 @@ public function isValidProfile(string $profileName): bool * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-24 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-log-all-transfer-actions-in-the-audit-trail */ private function logTransferInitiated(array $transferList, string $transport): void { @@ -589,7 +846,7 @@ private function logTransferInitiated(array $transferList, string $transport): v * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-24 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-log-all-transfer-actions-in-the-audit-trail */ private function logObjectTransferred(ObjectEntity $object, string $transferUuid, string $reference): void { @@ -629,7 +886,7 @@ private function logObjectTransferred(ObjectEntity $object, string $transferUuid * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-24 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-log-all-transfer-actions-in-the-audit-trail */ private function logTransferFailed(array $transferList, string $error, string $transport): void { @@ -652,7 +909,7 @@ private function logTransferFailed(array $transferList, string $error, string $t * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-6 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-multiple-transport-protocols-for-sip-delivery */ private function notifyTransferCompletion(array $transferList): void { diff --git a/lib/Service/Edepot/MdtoXmlGenerator.php b/lib/Service/Edepot/MdtoXmlGenerator.php index 3f4eb32818..80ee5fc010 100644 --- a/lib/Service/Edepot/MdtoXmlGenerator.php +++ b/lib/Service/Edepot/MdtoXmlGenerator.php @@ -21,8 +21,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-32 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object + * @spec openspec/specs/edepot-transfer/spec.md */ declare(strict_types=1); @@ -93,8 +93,8 @@ public function __construct( * * @throws InvalidArgumentException If required fields are missing. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-32 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object + * @spec openspec/specs/edepot-transfer/spec.md */ public function generate(ObjectEntity $object, array $files=[]): string { @@ -141,7 +141,7 @@ public function generate(ObjectEntity $object, array $files=[]): string * * @throws InvalidArgumentException If required fields are missing. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function validateRequiredFields(ObjectEntity $object, array $retention): void { @@ -185,7 +185,7 @@ private function validateRequiredFields(ObjectEntity $object, array $retention): * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function addIdentificatie(DOMDocument $dom, DOMElement $parent, ObjectEntity $object): void { @@ -212,7 +212,7 @@ private function addIdentificatie(DOMDocument $dom, DOMElement $parent, ObjectEn * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function addNaam(DOMDocument $dom, DOMElement $parent, ObjectEntity $object): void { @@ -230,7 +230,7 @@ private function addNaam(DOMDocument $dom, DOMElement $parent, ObjectEntity $obj * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function addWaardering(DOMDocument $dom, DOMElement $parent, array $retention): void { @@ -248,7 +248,7 @@ private function addWaardering(DOMDocument $dom, DOMElement $parent, array $rete * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function addBewaartermijn(DOMDocument $dom, DOMElement $parent, array $retention): void { @@ -265,7 +265,7 @@ private function addBewaartermijn(DOMDocument $dom, DOMElement $parent, array $r * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function addInformatiecategorie(DOMDocument $dom, DOMElement $parent, array $retention): void { @@ -281,7 +281,7 @@ private function addInformatiecategorie(DOMDocument $dom, DOMElement $parent, ar * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function addArchiefvormer(DOMDocument $dom, DOMElement $parent): void { @@ -317,7 +317,7 @@ private function addArchiefvormer(DOMDocument $dom, DOMElement $parent): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function addBestand(DOMDocument $dom, DOMElement $parent, array $file): void { @@ -351,7 +351,7 @@ private function addBestand(DOMDocument $dom, DOMElement $parent, array $file): * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function addTextElement(DOMDocument $dom, DOMElement $parent, string $name, string $content): void { diff --git a/lib/Service/Edepot/SipPackageBuilder.php b/lib/Service/Edepot/SipPackageBuilder.php index 7612053f81..657bf14b6f 100644 --- a/lib/Service/Edepot/SipPackageBuilder.php +++ b/lib/Service/Edepot/SipPackageBuilder.php @@ -20,8 +20,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-35 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object + * @spec openspec/specs/edepot-transfer/spec.md */ declare(strict_types=1); @@ -43,10 +43,13 @@ * * Creates ZIP archives containing per-object directories with MDTO XML metadata, * object data snapshots, associated content files, and package-level METS/PREMIS - * structural and preservation metadata. + * structural and preservation metadata. Supports a ZIP default and an RFC 8493 + * BagIt serializer branch. * * @psalm-suppress UnusedClass * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-bagit-output/spec.md + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ @@ -93,19 +96,25 @@ public function __construct( * @param string $transferId The transfer list UUID. * @param array $objectsWithFiles Objects and their file metadata (object, files[]). * @param int $maxPackageSize Maximum package size in bytes. + * @param string $format Output format: `zip` (default) or `bagit` (RFC 8493). * - * @return array Array of file paths to generated SIP ZIP archives. + * @return array Array of file paths to generated SIP archives. * - * @throws InvalidArgumentException If no objects are provided. + * @throws InvalidArgumentException If no objects are provided or the format is unknown. * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-35 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-bagit-output/spec.md + * (Requirement: BagIt is a serialization option on the existing builder) */ - public function build(string $transferId, array $objectsWithFiles, int $maxPackageSize=0): array + public function build(string $transferId, array $objectsWithFiles, int $maxPackageSize=0, string $format='zip'): array { if (empty($objectsWithFiles) === true) { throw new InvalidArgumentException('No objects provided for SIP package'); } + if (in_array($format, ['zip', 'bagit'], true) === false) { + throw new InvalidArgumentException("Unknown SIP output format '{$format}' (expected 'zip' or 'bagit')"); + } + if ($maxPackageSize <= 0) { $maxPackageSize = (int) $this->appConfig->getValueString( 'openregister', @@ -123,7 +132,8 @@ public function build(string $transferId, array $objectsWithFiles, int $maxPacka transferId: $transferId, objectsWithFiles: $batch, sequenceNumber: ($index + 1), - totalPackages: $totalBatches + totalPackages: $totalBatches, + format: $format ); } @@ -138,7 +148,7 @@ public function build(string $transferId, array $objectsWithFiles, int $maxPacka * * @return array Array of batches. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-7 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ private function splitIntoBatches(array $objectsWithFiles, int $maxSize): array { @@ -176,30 +186,23 @@ private function splitIntoBatches(array $objectsWithFiles, int $maxSize): array * @param array $objectsWithFiles Objects in this batch. * @param int $sequenceNumber This package's position in the sequence. * @param int $totalPackages Total number of packages. + * @param string $format Output format: `zip` or `bagit`. * - * @return string Path to the generated ZIP file. + * @return string Path to the generated archive. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-7 + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-bagit-output/spec.md + * (Requirement: BagIt is a serialization option on the existing builder) */ private function buildSinglePackage( string $transferId, array $objectsWithFiles, int $sequenceNumber, - int $totalPackages + int $totalPackages, + string $format='zip' ): string { - $suffix = ''; - if ($totalPackages > 1) { - $suffix = "-part{$sequenceNumber}"; - } - - $zipPath = $this->tempManager->getTemporaryFile(".sip{$suffix}.zip"); - - $zip = new ZipArchive(); - $result = $zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE); - if ($result !== true) { - throw new InvalidArgumentException("Failed to create ZIP archive: error code {$result}"); - } - + // Collect the SIP content once (format-agnostic): each entry is either + // an in-memory string or an on-disk file, with its logical SIP path. + $entries = []; $manifest = []; foreach ($objectsWithFiles as $item) { @@ -208,13 +211,13 @@ private function buildSinglePackage( $uuid = $object->getUuid(); $objectDir = "objects/{$uuid}"; - $mdtoXml = $this->mdtoGenerator->generate($object, $files); - $zip->addFromString("{$objectDir}/mdto.xml", $mdtoXml); + $mdtoXml = $this->mdtoGenerator->generate($object, $files); + $entries[] = ['path' => "{$objectDir}/mdto.xml", 'kind' => 'string', 'content' => $mdtoXml]; $manifest[] = $this->createManifestEntry(path: "{$objectDir}/mdto.xml", content: $mdtoXml); $metadataJson = json_encode($object->jsonSerialize(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); - $zip->addFromString("{$objectDir}/metadata.json", $metadataJson); - $manifest[] = $this->createManifestEntry(path: "{$objectDir}/metadata.json", content: $metadataJson); + $entries[] = ['path' => "{$objectDir}/metadata.json", 'kind' => 'string', 'content' => $metadataJson]; + $manifest[] = $this->createManifestEntry(path: "{$objectDir}/metadata.json", content: $metadataJson); if (empty($files) === false) { foreach ($files as $file) { @@ -224,9 +227,8 @@ private function buildSinglePackage( } $filePath = "{$objectDir}/content/{$subDir}/{$file['name']}"; - if (file_exists($file['path']) === true) { - $zip->addFile($file['path'], $filePath); + $entries[] = ['path' => $filePath, 'kind' => 'file', 'filePath' => $file['path']]; $manifest[] = [ 'path' => $filePath, 'size' => $file['size'], @@ -237,31 +239,39 @@ private function buildSinglePackage( } }//end foreach - $metsXml = $this->generateMetsXml(transferId: $transferId, objectsWithFiles: $objectsWithFiles); - $zip->addFromString('mets.xml', $metsXml); + $metsXml = $this->generateMetsXml(transferId: $transferId, objectsWithFiles: $objectsWithFiles); + $entries[] = ['path' => 'mets.xml', 'kind' => 'string', 'content' => $metsXml]; $manifest[] = $this->createManifestEntry(path: 'mets.xml', content: $metsXml); - $premisXml = $this->generatePremisXml(transferId: $transferId, objectsWithFiles: $objectsWithFiles); - $zip->addFromString('premis.xml', $premisXml); + $premisXml = $this->generatePremisXml(transferId: $transferId, objectsWithFiles: $objectsWithFiles); + $entries[] = ['path' => 'premis.xml', 'kind' => 'string', 'content' => $premisXml]; $manifest[] = $this->createManifestEntry(path: 'premis.xml', content: $premisXml); if ($totalPackages > 1) { $sequenceJson = json_encode( - [ - 'transferId' => $transferId, - 'sequenceNumber' => $sequenceNumber, - 'totalPackages' => $totalPackages, - ], - JSON_PRETTY_PRINT - ); - $zip->addFromString('sip-sequence.json', $sequenceJson); - $manifest[] = $this->createManifestEntry(path: 'sip-sequence.json', content: $sequenceJson); + [ + 'transferId' => $transferId, + 'sequenceNumber' => $sequenceNumber, + 'totalPackages' => $totalPackages, + ], + JSON_PRETTY_PRINT + ); + $entries[] = ['path' => 'sip-sequence.json', 'kind' => 'string', 'content' => $sequenceJson]; + $manifest[] = $this->createManifestEntry(path: 'sip-sequence.json', content: $sequenceJson); } $manifestJson = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); - $zip->addFromString('sip-manifest.json', $manifestJson); + $entries[] = ['path' => 'sip-manifest.json', 'kind' => 'string', 'content' => $manifestJson]; - $zip->close(); + $suffix = ''; + if ($totalPackages > 1) { + $suffix = "-part{$sequenceNumber}"; + } + + $archivePath = match ($format) { + 'bagit' => $this->writeBagitArchive(transferId: $transferId, entries: $entries, suffix: $suffix), + default => $this->writeZipArchive(entries: $entries, suffix: $suffix), + }; $this->logger->info( message: '[SipPackageBuilder] Built SIP package', @@ -269,11 +279,12 @@ private function buildSinglePackage( 'transferId' => $transferId, 'sequence' => "{$sequenceNumber}/{$totalPackages}", 'objects' => count($objectsWithFiles), - 'path' => $zipPath, + 'format' => $format, + 'path' => $archivePath, ] ); - return $zipPath; + return $archivePath; }//end buildSinglePackage() /** @@ -284,7 +295,7 @@ private function buildSinglePackage( * * @return array{path: string, size: int, checksum: string} The manifest entry. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-7 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ private function createManifestEntry(string $path, string $content): array { @@ -295,6 +306,135 @@ private function createManifestEntry(string $path, string $content): array ]; }//end createManifestEntry() + /** + * Write the collected content entries as a flat ZIP SIP (the historical + * layout — content at the archive root, unchanged by this change). + * + * @param array> $entries Content entries (string|file). + * @param string $suffix Filename suffix for multi-part packages. + * + * @return string Path to the generated ZIP file. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-bagit-output/spec.md + * (Scenario: Zip is the unchanged default) + */ + private function writeZipArchive(array $entries, string $suffix): string + { + $zipPath = $this->tempManager->getTemporaryFile(".sip{$suffix}.zip"); + + $zip = new ZipArchive(); + $result = $zip->open($zipPath, (ZipArchive::CREATE | ZipArchive::OVERWRITE)); + if ($result !== true) { + throw new InvalidArgumentException("Failed to create ZIP archive: error code {$result}"); + } + + foreach ($entries as $entry) { + if ($entry['kind'] === 'file') { + $zip->addFile($entry['filePath'], $entry['path']); + continue; + } + + $zip->addFromString($entry['path'], $entry['content']); + } + + $zip->close(); + + return $zipPath; + + }//end writeZipArchive() + + /** + * Write the collected content entries as an RFC 8493 BagIt bag (v1.0): + * payload under `data/`, a complete `manifest-sha256.txt`, and a + * `tagmanifest-sha256.txt` over the tag files. An unchecksummable payload + * file fails the build — an incomplete manifest is never shipped. + * + * @param string $transferId The transfer uuid (bag External-Identifier). + * @param array> $entries Content entries (string|file). + * @param string $suffix Filename suffix for multi-part packages. + * + * @return string Path to the generated bag ZIP file. + * + * @throws InvalidArgumentException When a payload file cannot be checksummed. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-bagit-output/spec.md + * (Requirement: BagIt is a serialization option on the existing builder) + */ + private function writeBagitArchive(string $transferId, array $entries, string $suffix): string + { + $zipPath = $this->tempManager->getTemporaryFile(".bag{$suffix}.zip"); + + $zip = new ZipArchive(); + $result = $zip->open($zipPath, (ZipArchive::CREATE | ZipArchive::OVERWRITE)); + if ($result !== true) { + throw new InvalidArgumentException("Failed to create BagIt archive: error code {$result}"); + } + + // Payload goes under data/; build the payload manifest as we add files. + $manifestLines = []; + $totalBytes = 0; + $fileCount = 0; + + foreach ($entries as $entry) { + $dataPath = 'data/'.$entry['path']; + + if ($entry['kind'] === 'file') { + $sourcePath = (string) $entry['filePath']; + $sha256 = false; + if (is_file($sourcePath) === true && is_readable($sourcePath) === true) { + $sha256 = hash_file('sha256', $sourcePath); + } + + if ($sha256 === false) { + throw new InvalidArgumentException( + "BagIt build failed: cannot checksum payload file '{$sourcePath}' — refusing an incomplete manifest" + ); + } + + $zip->addFile($sourcePath, $dataPath); + $size = (int) filesize($sourcePath); + $totalBytes += $size; + } else { + $content = (string) $entry['content']; + $sha256 = hash('sha256', $content); + $zip->addFromString($dataPath, $content); + $totalBytes += strlen($content); + }//end if + + $manifestLines[] = $sha256.' '.$dataPath; + $fileCount++; + }//end foreach + + // Tag files (RFC 8493). + $bagitTxt = "BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n"; + + $baggingDate = gmdate('Y-m-d'); + $bagInfoTxt = "Bagging-Date: {$baggingDate}\n" + ."Payload-Oxum: {$totalBytes}.{$fileCount}\n" + ."Source-Organization: OpenRegister\n" + ."External-Identifier: {$transferId}\n"; + + $manifestTxt = implode("\n", $manifestLines)."\n"; + + // Tag manifest covers the three tag files above. + $tagManifestLines = [ + hash('sha256', $bagitTxt).' bagit.txt', + hash('sha256', $bagInfoTxt).' bag-info.txt', + hash('sha256', $manifestTxt).' manifest-sha256.txt', + ]; + $tagManifestTxt = implode("\n", $tagManifestLines)."\n"; + + $zip->addFromString('bagit.txt', $bagitTxt); + $zip->addFromString('bag-info.txt', $bagInfoTxt); + $zip->addFromString('manifest-sha256.txt', $manifestTxt); + $zip->addFromString('tagmanifest-sha256.txt', $tagManifestTxt); + + $zip->close(); + + return $zipPath; + + }//end writeBagitArchive() + /** * Generate METS XML structural metadata. * @@ -303,7 +443,7 @@ private function createManifestEntry(string $path, string $content): array * * @return string The METS XML string. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function generateMetsXml(string $transferId, array $objectsWithFiles): string { @@ -394,7 +534,7 @@ private function generateMetsXml(string $transferId, array $objectsWithFiles): s * * @return string The PREMIS XML string. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-20 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-generate-mdto-compliant-xml-metadata-per-object */ private function generatePremisXml(string $transferId, array $objectsWithFiles): string { diff --git a/lib/Service/Edepot/TransferListService.php b/lib/Service/Edepot/TransferListService.php index af3e23c329..31ffade987 100644 --- a/lib/Service/Edepot/TransferListService.php +++ b/lib/Service/Edepot/TransferListService.php @@ -19,8 +19,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-36 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-transfer-list-management + * @spec openspec/specs/edepot-transfer/spec.md */ declare(strict_types=1); @@ -42,9 +42,13 @@ * * Transfer lists track which objects are pending, approved, or completed for * e-Depot transfer. They follow the same review-approve pattern as destruction lists. + * Records persist through ObjectService via TransferRecordService, and carry the + * per-connection output-format setting (zip default | bagit). * * @psalm-suppress UnusedClass * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-durable-retry/spec.md + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TransferListService @@ -64,21 +68,49 @@ class TransferListService /** * Constructor. * - * @param MagicMapper $objectMapper The object mapper. - * @param AuditTrailMapper $auditTrailMapper The audit trail mapper. - * @param IAppConfig $appConfig The app configuration. - * @param INotificationManager $notificationManager The notification manager. - * @param LoggerInterface $logger Logger. + * @param MagicMapper $objectMapper The object mapper. + * @param AuditTrailMapper $auditTrailMapper The audit trail mapper. + * @param TransferRecordService $transferRecordService Durable transfer-record persistence. + * @param IAppConfig $appConfig The app configuration. + * @param INotificationManager $notificationManager The notification manager. + * @param LoggerInterface $logger Logger. */ public function __construct( private readonly MagicMapper $objectMapper, private readonly AuditTrailMapper $auditTrailMapper, + private readonly TransferRecordService $transferRecordService, private readonly IAppConfig $appConfig, private readonly INotificationManager $notificationManager, private readonly LoggerInterface $logger, ) { }//end __construct() + /** + * Persist a transfer-list mutation to the durable e-Depot transfer record + * (best-effort — a persistence failure logs a warning and returns the + * in-memory list so callers keep working; the archivist-facing record is + * the durable source of truth once written). + * + * @param array $transferList The transfer-list data. + * + * @return array The persisted (or in-memory on failure) list. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Requirement: Durable transfer-list objects served over the API) + */ + private function persist(array $transferList): array + { + try { + return $this->transferRecordService->saveTransferList($transferList); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[TransferListService] Could not persist transfer list: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'uuid' => ($transferList['uuid'] ?? '')] + ); + return $transferList; + } + }//end persist() + /** * Create a new transfer list from eligible objects. * @@ -94,8 +126,8 @@ public function __construct( * * @throws InvalidArgumentException If no objects provided. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-36 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-transfer-list-management + * @spec openspec/specs/edepot-transfer/spec.md * * @SuppressWarnings(PHPMD.StaticAccess) */ @@ -114,13 +146,20 @@ public function createTransferList(array $objects): array ]; } + $packageFormat = $this->appConfig->getValueString('openregister', 'edepot_package_format', 'zip'); + if (in_array($packageFormat, ['zip', 'bagit'], true) === false) { + $packageFormat = 'zip'; + } + $transferList = [ 'uuid' => (string) Uuid::v4(), 'status' => self::STATUS_IN_REVIEW, 'objectReferences' => $objectReferences, 'exclusions' => [], 'approvalMetadata' => null, + 'attempts' => [], 'transferResult' => null, + 'packageFormat' => $packageFormat, 'createdAt' => (new DateTime())->format('c'), 'objectCount' => count($objectReferences), ]; @@ -133,7 +172,7 @@ public function createTransferList(array $objects): array ] ); - return $transferList; + return $this->persist(transferList: $transferList); }//end createTransferList() /** @@ -146,8 +185,8 @@ public function createTransferList(array $objects): array * * @throws InvalidArgumentException If the list is not in review status. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-36 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-transfer-list-management + * @spec openspec/specs/edepot-transfer/spec.md */ public function approveTransferList(array $transferList, string $archivistId): array { @@ -171,7 +210,7 @@ public function approveTransferList(array $transferList, string $archivistId): a ] ); - return $transferList; + return $this->persist(transferList: $transferList); }//end approveTransferList() /** @@ -185,8 +224,8 @@ public function approveTransferList(array $transferList, string $archivistId): a * * @throws InvalidArgumentException If the list is not in review status. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-36 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-transfer-list-management + * @spec openspec/specs/edepot-transfer/spec.md */ public function rejectTransferList(array $transferList, string $archivistId, string $reason): array { @@ -216,7 +255,7 @@ public function rejectTransferList(array $transferList, string $archivistId, str ] ); - return $transferList; + return $this->persist(transferList: $transferList); }//end rejectTransferList() /** @@ -228,7 +267,7 @@ public function rejectTransferList(array $transferList, string $archivistId, str * * @return array The updated transfer list data. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-transfer-list-management */ public function excludeObjects(array $transferList, array $objectUuids, string $reason): array { @@ -271,7 +310,7 @@ static function (array $ref) use ($objectUuids): bool { ] ); - return $transferList; + return $this->persist(transferList: $transferList); }//end excludeObjects() /** @@ -284,7 +323,7 @@ static function (array $ref) use ($objectUuids): bool { * * @return array UUIDs of objects on active transfer lists. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-transfer-list-management */ public function getObjectsOnActiveTransferLists(array $activeTransferLists): array { @@ -311,7 +350,7 @@ public function getObjectsOnActiveTransferLists(array $activeTransferLists): arr * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-22 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-support-transfer-list-management */ public function notifyArchivists(array $transferList): void { diff --git a/lib/Service/Edepot/TransferRecordService.php b/lib/Service/Edepot/TransferRecordService.php new file mode 100644 index 0000000000..88a72734ed --- /dev/null +++ b/lib/Service/Edepot/TransferRecordService.php @@ -0,0 +1,279 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Edepot; + +use DateTime; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; + +/** + * Persist + load durable e-Depot transfer + proof records via ObjectService. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Requirement: Durable transfer-list objects served over the API) + */ +class TransferRecordService +{ + + /** + * Register slug the durable transfer records live under. + * + * @var string + */ + public const REGISTER_SLUG = 'edepot-transfers'; + + /** + * Schema slug for the durable transfer list. + * + * @var string + */ + public const TRANSFER_SCHEMA_SLUG = 'edepotTransfer'; + + /** + * Schema slug for the immutable proof-of-transfer record. + * + * @var string + */ + public const PROOF_SCHEMA_SLUG = 'edepotTransferProof'; + + /** + * Constructor. + * + * @param ObjectService $objectService The RBAC/tenant-scoped, audited object store. + * @param LoggerInterface $logger Structured logging. + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Persist (create or update) a transfer-list record. + * + * Keyed by the transfer list's own `uuid`, so status transitions on the + * same list update the one durable object. Returns the persisted data. + * + * @param array $transferList The transfer-list array (from TransferListService). + * + * @return array The persisted transfer-list data (with its object uuid). + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Requirement: Durable transfer-list objects served over the API) + */ + public function saveTransferList(array $transferList): array + { + $uuid = (string) ($transferList['uuid'] ?? ''); + + $data = $transferList; + // The list's own uuid IS the object uuid — keep them aligned so + // subsequent status writes update the same durable object. + $uuidArg = null; + if ($uuid !== '') { + $data['@self'] = ['uuid' => $uuid]; + $uuidArg = $uuid; + } + + $saved = $this->objectService->saveObject( + object: $data, + register: self::REGISTER_SLUG, + schema: self::TRANSFER_SCHEMA_SLUG, + uuid: $uuidArg + ); + + $rendered = $saved->getObject(); + $rendered['uuid'] = (string) $saved->getUuid(); + + return $rendered; + + }//end saveTransferList() + + /** + * Load a transfer-list record by uuid, or null when absent. + * + * @param string $uuid The transfer-list uuid. + * + * @return array|null The transfer-list data, or null. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Scenario: Show returns a persisted transfer list) + */ + public function loadTransferList(string $uuid): ?array + { + try { + $object = $this->objectService->find( + id: $uuid, + register: self::REGISTER_SLUG, + schema: self::TRANSFER_SCHEMA_SLUG + ); + } catch (\Throwable $e) { + return null; + } + + if ($object === null) { + return null; + } + + $data = $object->getObject(); + $data['uuid'] = (string) $object->getUuid(); + + return $data; + + }//end loadTransferList() + + /** + * List all durable transfer-list records (newest first), RBAC/tenant scoped. + * + * @return array> The transfer-list records. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Scenario: Index returns persisted transfer lists) + */ + public function listTransferLists(): array + { + try { + $rows = $this->objectService->findAll( + config: [ + 'filters' => [ + 'register' => self::REGISTER_SLUG, + 'schema' => self::TRANSFER_SCHEMA_SLUG, + ], + ] + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[TransferRecordService] transfer-list enumeration failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return []; + } + + $lists = []; + foreach ($rows as $row) { + if (is_array($row) === true) { + $lists[] = $row; + } + } + + return $lists; + + }//end listTransferLists() + + /** + * Create an immutable proof-of-transfer record for one confirmed object. + * + * Write-once: if a proof already exists for this (transfer, object) pair + * it is returned unchanged rather than duplicated (idempotent re-run + * safety; belt to the schema `immutable` flag). + * + * @param array $proof The proof field set (objectUuid, transferUuid, eDepotReference, …). + * + * @return array The persisted (or pre-existing) proof data. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Scenario: Proof created on confirmed transfer) + */ + public function createProof(array $proof): array + { + $objectUuid = (string) ($proof['objectUuid'] ?? ''); + $transferUuid = (string) ($proof['transferUuid'] ?? ''); + + $existing = $this->findProof(objectUuid: $objectUuid, transferUuid: $transferUuid); + if ($existing !== null) { + return $existing; + } + + if (isset($proof['confirmedAt']) === false) { + $proof['confirmedAt'] = (new DateTime())->format('c'); + } + + $saved = $this->objectService->createObject( + data: $proof + ['@self' => ['register' => self::REGISTER_SLUG, 'schema' => self::PROOF_SCHEMA_SLUG]] + ); + + $rendered = $saved->getObject(); + $rendered['uuid'] = (string) $saved->getUuid(); + + return $rendered; + + }//end createProof() + + /** + * Find an existing proof for a (transfer, object) pair, or null. + * + * @param string $objectUuid The transferred object's uuid. + * @param string $transferUuid The transfer list's uuid. + * + * @return array|null The proof data, or null when none exists. + * + * @spec openspec/changes/archival-transfer-hardening/specs/edepot-proof-of-transfer/spec.md + * (Scenario: Proof is write-once) + */ + public function findProof(string $objectUuid, string $transferUuid): ?array + { + if ($objectUuid === '' || $transferUuid === '') { + return null; + } + + try { + $rows = $this->objectService->findAll( + config: [ + 'filters' => [ + 'register' => self::REGISTER_SLUG, + 'schema' => self::PROOF_SCHEMA_SLUG, + 'objectUuid' => $objectUuid, + 'transferUuid' => $transferUuid, + ], + ] + ); + } catch (\Throwable $e) { + return null; + } + + foreach ($rows as $row) { + if (is_array($row) === true) { + return $row; + } + } + + return null; + + }//end findProof() +}//end class diff --git a/lib/Service/Edepot/Transport/OpenConnectorTransport.php b/lib/Service/Edepot/Transport/OpenConnectorTransport.php index 4d1ad665eb..c7956d28ed 100644 --- a/lib/Service/Edepot/Transport/OpenConnectorTransport.php +++ b/lib/Service/Edepot/Transport/OpenConnectorTransport.php @@ -44,7 +44,7 @@ class OpenConnectorTransport implements TransportInterface * @param Client $httpClient The HTTP client. * @param LoggerInterface $logger Logger. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function __construct( private readonly Client $httpClient, @@ -60,7 +60,7 @@ public function __construct( * * @return TransportResult The result of the transport. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function send(string $sipFilePath, array $config): TransportResult { @@ -131,7 +131,7 @@ public function send(string $sipFilePath, array $config): TransportResult * * @return bool True if connection test succeeds. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function testConnection(array $config): bool { @@ -161,7 +161,7 @@ public function testConnection(array $config): bool * * @return string The transport name. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function getName(): string { @@ -177,7 +177,7 @@ public function getName(): string * * @throws RuntimeException If required configuration is missing. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ private function validateConfig(array $config): void { diff --git a/lib/Service/Edepot/Transport/RestApiTransport.php b/lib/Service/Edepot/Transport/RestApiTransport.php index b0c2f8362f..c5fd15d599 100644 --- a/lib/Service/Edepot/Transport/RestApiTransport.php +++ b/lib/Service/Edepot/Transport/RestApiTransport.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/edepot-transfer/spec.md */ declare(strict_types=1); @@ -61,8 +61,8 @@ public function __construct( * * @return TransportResult The result of the transport. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer + * @spec openspec/specs/edepot-transfer/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -171,7 +171,7 @@ public function send(string $sipFilePath, array $config): TransportResult * * @return bool True if connection test succeeds. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function testConnection(array $config): bool { @@ -202,7 +202,7 @@ public function testConnection(array $config): bool * * @return string The transport name. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function getName(): string { @@ -218,7 +218,7 @@ public function getName(): string * * @throws RuntimeException If required configuration is missing. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ private function validateConfig(array $config): void { @@ -234,7 +234,7 @@ private function validateConfig(array $config): void * * @return array The auth headers. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ private function buildAuthHeaders(array $config): array { diff --git a/lib/Service/Edepot/Transport/SftpTransport.php b/lib/Service/Edepot/Transport/SftpTransport.php index 2a3ee8de6a..51e35ca38d 100644 --- a/lib/Service/Edepot/Transport/SftpTransport.php +++ b/lib/Service/Edepot/Transport/SftpTransport.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-34 + * @spec openspec/specs/edepot-transfer/spec.md */ declare(strict_types=1); @@ -59,8 +59,8 @@ public function __construct( * * @return TransportResult The result of the transport. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-34 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer + * @spec openspec/specs/edepot-transfer/spec.md */ public function send(string $sipFilePath, array $config): TransportResult { @@ -133,7 +133,7 @@ public function send(string $sipFilePath, array $config): TransportResult * * @return bool True if connection test succeeds. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function testConnection(array $config): bool { @@ -164,7 +164,7 @@ public function testConnection(array $config): bool * * @return string The transport name. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function getName(): string { @@ -180,7 +180,7 @@ public function getName(): string * * @throws RuntimeException If required configuration is missing. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ private function validateConfig(array $config): void { @@ -207,7 +207,7 @@ private function validateConfig(array $config): void * * @psalm-suppress UndefinedClass * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer * * @SuppressWarnings(PHPMD.StaticAccess) */ diff --git a/lib/Service/Edepot/Transport/TransportInterface.php b/lib/Service/Edepot/Transport/TransportInterface.php index b4194e0a8f..8e343dbf8f 100644 --- a/lib/Service/Edepot/Transport/TransportInterface.php +++ b/lib/Service/Edepot/Transport/TransportInterface.php @@ -40,7 +40,7 @@ interface TransportInterface * * @return TransportResult The result of the transport operation. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function send(string $sipFilePath, array $config): TransportResult; @@ -51,7 +51,7 @@ public function send(string $sipFilePath, array $config): TransportResult; * * @return bool True if connection test succeeds. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function testConnection(array $config): bool; @@ -60,7 +60,7 @@ public function testConnection(array $config): bool; * * @return string The transport protocol name (e.g., 'sftp', 'rest_api', 'openconnector'). * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function getName(): string; }//end interface diff --git a/lib/Service/Edepot/Transport/TransportResult.php b/lib/Service/Edepot/Transport/TransportResult.php index 0e1a51cc8a..eb74dd6755 100644 --- a/lib/Service/Edepot/Transport/TransportResult.php +++ b/lib/Service/Edepot/Transport/TransportResult.php @@ -73,7 +73,7 @@ class TransportResult * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * - * @spec openspec/changes/retrofit-2026-05-24-edepot-transfer/tasks.md#task-2 + * @spec openspec/specs/edepot-transfer/spec.md */ public function __construct( bool $success=false, @@ -92,7 +92,7 @@ public function __construct( * * @return bool True if successful. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function isSuccess(): bool { @@ -104,7 +104,7 @@ public function isSuccess(): bool * * @return bool True if partially successful. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function isPartialSuccess(): bool { @@ -131,7 +131,7 @@ public function isPartialSuccess(): bool * * @return array Object results. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function getObjectResults(): array { @@ -143,7 +143,7 @@ public function getObjectResults(): array * * @return string|null The error message. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function getErrorMessage(): ?string { @@ -155,7 +155,7 @@ public function getErrorMessage(): ?string * * @return string|null The e-Depot transfer reference. * - * @spec openspec/changes/retrofit-2026-05-24-edepot-transfer/tasks.md#task-2 + * @spec openspec/specs/edepot-transfer/spec.md */ public function getTransferReference(): ?string { @@ -167,7 +167,7 @@ public function getTransferReference(): ?string * * @return array UUIDs of accepted objects. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function getAcceptedUuids(): array { @@ -186,7 +186,7 @@ public function getAcceptedUuids(): array * * @return array UUIDs of rejected objects. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function getRejectedUuids(): array { @@ -205,7 +205,7 @@ public function getRejectedUuids(): array * * @return array Serialized result. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/edepot-transfer/spec.md#requirement-the-system-must-assemble-sip-packages-for-e-depot-transfer */ public function toArray(): array { diff --git a/lib/Service/EmailLinkService.php b/lib/Service/EmailLinkService.php index d9ec38fa32..33cde08593 100644 --- a/lib/Service/EmailLinkService.php +++ b/lib/Service/EmailLinkService.php @@ -50,7 +50,7 @@ * @version GIT: * @link https://OpenRegister.app * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes EmailLinkMapper, IDBConnection, + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes EmailLinkMapper, IDBConnection, * IAppManager, IUserSession, and LoggerInterface; each is a required dependency for * direct-SQL Mail table queries, availability checks, and user-session handling. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Direct SQL queries against NC Mail @@ -78,7 +78,7 @@ * @category Service * @package OCA\OpenRegister\Service * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes mapper + + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes mapper + * IDBConnection (direct Mail-table queries) + IAppManager + * IUserSession + LoggerInterface. Each dependency is required. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Spans send, link, list, @@ -138,7 +138,7 @@ public function __construct( * * @return bool * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function isMailAvailable(): bool { @@ -176,7 +176,7 @@ public function isMailAvailable(): bool * clauses for the four required preconditions (user, Mail * available, message exists, upsert idempotency). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function linkEmail( string $objectUuid, @@ -274,7 +274,11 @@ private function buildNewLink( $link->setMailAccountId($mailAccountId); $link->setMailMessageId($messageIdInt); - $effectiveUid = ($uidForMatch !== null || $message['uid'] === '') ? $uidForMatch : $message['uid']; + $effectiveUid = $message['uid']; + if ($uidForMatch !== null || $message['uid'] === '') { + $effectiveUid = $uidForMatch; + } + $link->setMailMessageUid($effectiveUid); $link->setSubject($message['subject']); $link->setSender($message['sender']); @@ -303,7 +307,7 @@ private function buildNewLink( * * @throws Exception When no matching link is found (404). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function unlinkEmail(string $objectUuid, int $linkId): void { @@ -330,7 +334,7 @@ public function unlinkEmail(string $objectUuid, int $linkId): void * * @return array{items: array>, total: int, nextCursor: ?int} * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getLinkedEmails(string $objectUuid, ?string $cursor=null, int $limit=self::DEFAULT_LIMIT): array { @@ -379,7 +383,7 @@ public function getLinkedEmails(string $objectUuid, ?string $cursor=null, int $l * * @return array * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getAvailableAccounts(): array { @@ -440,7 +444,7 @@ public function getAvailableAccounts(): array * * @return array * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getMailboxesForAccount(int $accountId): array { @@ -507,7 +511,7 @@ public function getMailboxesForAccount(int $accountId): array * resolution, cursor parse, ownership) then a defensive * try/catch around the join query. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getMessagesForMailbox( int $accountId, @@ -540,7 +544,10 @@ public function getMessagesForMailbox( } $items = $this->normalizeMessageRows(rows: $rows); - $nextCursor = ($hasMore === true && $items !== []) ? $items[count($items) - 1]['id'] : null; + $nextCursor = null; + if ($hasMore === true && $items !== []) { + $nextCursor = $items[count($items) - 1]['id']; + } return [ 'items' => $items, @@ -594,7 +601,11 @@ private function fetchMailboxRows(int $accountId, string $mailbox, ?string $afte ->addOrderBy('m.id', 'DESC') ->setMaxResults($fetch); - $cursorId = ($afterCursor !== null && $afterCursor !== '') ? (int) $afterCursor : 0; + $cursorId = 0; + if ($afterCursor !== null && $afterCursor !== '') { + $cursorId = (int) $afterCursor; + } + if ($cursorId > 0) { $qb->andWhere( $qb->expr()->lt( diff --git a/lib/Service/EmailService.php b/lib/Service/EmailService.php index 99d8d0d862..d6e93e2e99 100644 --- a/lib/Service/EmailService.php +++ b/lib/Service/EmailService.php @@ -17,10 +17,10 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-46 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-51 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-50 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mail-sidebar/spec.md#requirement-reverse-lookup-api-to-find-objects-by-mail-message-id + * @spec openspec/specs/mail-sidebar/spec.md + * @spec openspec/specs/mail-sidebar/spec.md + * @spec openspec/specs/mail-sidebar/spec.md */ declare(strict_types=1); @@ -146,7 +146,7 @@ public function isMailAvailable(): bool * * @return array{results: array, total: int} Email links with total count. * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-51 + * @spec openspec/specs/mail-sidebar/spec.md */ public function getEmailsForObject(string $objectUuid, ?int $limit=null, ?int $offset=null): array { @@ -175,7 +175,7 @@ static function (EmailLink $link): array { * * @throws Exception If the email does not exist or is already linked. * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-50 + * @spec openspec/specs/mail-sidebar/spec.md */ public function linkEmail( string $objectUuid, @@ -246,7 +246,7 @@ public function unlinkEmail(int $linkId): void * * @return array Array of email links with object UUIDs. * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mail-sidebar/spec.md */ public function searchBySender(string $sender): array { @@ -304,16 +304,26 @@ private function findMessageIdsBySender(string $sender): array return []; } + // IDOR guard: scope the scan to the caller's own Mail accounts. Without + // this any authenticated user could pass an arbitrary sender and learn + // which OpenRegister objects link to mail received by OTHER users' + // accounts (cross-account leak). oc_mail_accounts.user_id owns the account. + $user = $this->userSession->getUser(); + if ($user === null) { + return []; + } + try { // The mail_recipients.type=0 is "from"; mail_recipients.email is the address. $sql = "SELECT mb.account_id, m.id AS message_id FROM oc_mail_messages m JOIN oc_mail_recipients r ON r.message_id = m.id AND r.type = 0 JOIN oc_mail_mailboxes mb ON mb.id = m.mailbox_id - WHERE LOWER(r.email) = LOWER(?) + JOIN oc_mail_accounts ma ON ma.id = mb.account_id + WHERE LOWER(r.email) = LOWER(?) AND ma.user_id = ? LIMIT 200"; $stmt = $this->db->prepare($sql); - $stmt->execute([$sender]); + $stmt->execute([$sender, $user->getUID()]); $ids = []; while (($row = $stmt->fetch()) !== false) { $ids[] = ((int) $row['account_id']).'/'.((int) $row['message_id']); @@ -323,7 +333,7 @@ private function findMessageIdsBySender(string $sender): array } catch (Exception $e) { $this->logger->warning('[EmailService] findMessageIdsBySender failed: '.$e->getMessage()); return []; - } + }//end try }//end findMessageIdsBySender() /** @@ -369,7 +379,7 @@ public function deleteLinksForObject(string $objectUuid): int * * @return array|null Message data or null if not found. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-46 + * @spec openspec/specs/mail-sidebar/spec.md#requirement-reverse-lookup-api-to-find-objects-by-mail-message-id */ private function fetchMailMessage(int $messageId, int $accountId): ?array { diff --git a/lib/Service/EndpointService.php b/lib/Service/EndpointService.php index 635ec56f4d..49b8cd05e4 100644 --- a/lib/Service/EndpointService.php +++ b/lib/Service/EndpointService.php @@ -106,7 +106,7 @@ class EndpointService * @phpstan-return array{success: bool, statusCode: int, response: mixed, error?: string} * @psalm-return array{success: bool, statusCode: int, response: mixed, error?: string} * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-15 + * @spec openspec/specs/object-interactions/spec.md */ public function testEndpoint(Endpoint $endpoint, array $testData=[]): array { @@ -176,7 +176,7 @@ public function testEndpoint(Endpoint $endpoint, array $testData=[]): array * @phpstan-return array{success: bool, statusCode: int, response: mixed, error?: string} * @psalm-return array{success: bool, statusCode: int, response: mixed, error?: string} * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-14 + * @spec openspec/specs/object-interactions/spec.md */ private function executeEndpoint(Endpoint $endpoint, array $request): array { @@ -236,6 +236,13 @@ private function executeViewEndpoint(Endpoint $_endpoint, array $_request): arra /** * Execute an agent endpoint * + * Validates that the referenced agent exists and that a message was + * supplied, then returns a 501 Not Implemented response. + * + * Deprecated: agent endpoint LLM execution (tool loading + chat calls) + * is not implemented here anymore; it is pending the agent-core + * migration to the standalone hermiq application. + * * @param Endpoint $endpoint The endpoint to execute * @param array $request Request data * @@ -244,19 +251,13 @@ private function executeViewEndpoint(Endpoint $_endpoint, array $_request): arra * @psalm-return array{success: bool, statusCode: int, response: mixed, error?: string} * @psalm-suppress UnusedParam - False positive: both parameters are used within the method. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-16 - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Agent execution has multiple provider and tool conditions - * @SuppressWarnings(PHPMD.NPathComplexity) Agent setup involves many validation and config paths - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Agent execution with tools requires comprehensive logic + * @spec openspec/specs/object-interactions/spec.md */ private function executeAgentEndpoint(Endpoint $endpoint, array $request): array { try { // Get required services. - $agentMapper = \OC::$server->get(\OCA\OpenRegister\Db\AgentMapper::class); - $toolRegistry = \OC::$server->get(\OCA\OpenRegister\Service\ToolRegistry::class); - $settingsService = \OC::$server->get(\OCA\OpenRegister\Service\SettingsService::class); + $agentMapper = \OC::$server->get(\OCA\OpenRegister\Db\AgentMapper::class); $agentId = $endpoint->getTargetId(); $this->logger->info( @@ -288,118 +289,14 @@ private function executeAgentEndpoint(Endpoint $endpoint, array $request): array ]; } - $this->logger->info( - message: '[EndpointService] Executing agent', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'agent' => $agent->getName(), - 'provider' => $agent->getProvider(), - 'model' => $agent->getModel(), - 'message' => substr($message, 0, 100), - ] - ); - - // Get LLM configuration. - $llmConfig = $settingsService->getSettings()['llm'] ?? []; - - // Prepare tools/functions for the agent. - $functions = []; - $agentTools = $agent->getTools() ?? []; - - if (empty($agentTools) === false) { - foreach ($agentTools as $toolName) { - try { - $tool = $toolRegistry->getTool($toolName); - if ($tool !== null) { - $tool->setAgent($agent); - $toolFunctions = $tool->getFunctions(); - $functions = array_merge($functions, $toolFunctions); - - $this->logger->debug( - message: '[EndpointService] Added tool functions', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'tool' => $toolName, - 'functions' => count($toolFunctions), - ] - ); - } - } catch (\Exception $e) { - $this->logger->warning( - message: '[EndpointService] Failed to load tool: '.$toolName, - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - }//end try - }//end foreach - }//end if - - $this->logger->info( - message: '[EndpointService] Agent has tools configured', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'totalFunctions' => count($functions), - ] - ); - - // Call LLM based on provider. - if ($agent->getProvider() === 'ollama') { - $ollamaConfig = $llmConfig['ollamaConfig'] ?? []; - $ollamaUrl = $ollamaConfig['url'] ?? 'http://host.docker.internal:11434'; - - $this->logger->info( - message: '[EndpointService] Calling Ollama', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'url' => $ollamaUrl, - 'model' => $agent->getModel(), - 'functionsAvailable' => count($functions), - ] - ); - - // Build messages. - $messages = []; - if (($agent->getPrompt() !== null && $agent->getPrompt() !== '') === true) { - $messages[] = [ - 'role' => 'system', - 'content' => $agent->getPrompt(), - ]; - } - - $messages[] = [ - 'role' => 'user', - 'content' => $message, - ]; - - // Call Ollama API directly. - $response = $this->callOllamaWithTools( - url: $ollamaUrl, - model: $agent->getModel(), - messages: $messages, - functions: $functions, - agent: $agent, - toolRegistry: $toolRegistry - ); - - return [ - 'success' => true, - 'statusCode' => 200, - 'response' => $response, - ]; - }//end if - + // Agent endpoint LLM execution (tool loading + chat calls) is + // deprecated pending the agent-core migration to hermiq; return + // a clean not-implemented response instead of calling an LLM. return [ 'success' => false, 'statusCode' => 501, 'response' => null, - 'error' => 'Provider '.$agent->getProvider().' not yet implemented for endpoint execution', + 'error' => 'Agent endpoint type is not implemented', ]; } catch (\Exception $e) { $this->logger->error( @@ -505,7 +402,7 @@ private function executeSchemaEndpoint(Endpoint $_endpoint, array $_request): ar * * @return bool True if user can execute, false otherwise * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-17 + * @spec openspec/specs/object-interactions/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Permission check has multiple user and group conditions */ @@ -554,7 +451,7 @@ private function canExecuteEndpoint(Endpoint $endpoint): bool * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-18 + * @spec openspec/specs/object-interactions/spec.md */ private function logEndpointCall(Endpoint $endpoint, array $request, array $result): void { diff --git a/lib/Service/ExportService.php b/lib/Service/ExportService.php index 0e09331b38..65181ec09e 100644 --- a/lib/Service/ExportService.php +++ b/lib/Service/ExportService.php @@ -19,21 +19,25 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-11 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-12 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-16 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/export-pdf-format/spec.md */ namespace OCA\OpenRegister\Service; use DateTime; +use Dompdf\Dompdf; +use Dompdf\Options; use Exception; use InvalidArgumentException; use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Exception\ExportTooLargeException; use OCA\OpenRegister\Service\ObjectService; use OCA\OpenRegister\Service\Object\CacheHandler; use OCA\OpenRegister\Service\PropertyRbacHandler; @@ -49,6 +53,7 @@ use React\Async\PromiseInterface; use React\Promise\Promise; use React\EventLoop\Loop; +use RuntimeException; /** * Service for exporting data to various formats @@ -63,6 +68,20 @@ class ExportService { + /** + * Maximum number of objects a single PDF export may render. + * + * PDF rendering (Dompdf) builds a full in-memory HTML/CSS box-tree per + * row, unlike the streaming CSV writer or PhpSpreadsheet's XLSX writer, + * making it meaningfully more memory-heavy per row. Requests whose + * object count exceeds this cap MUST fail fast with + * {@see ExportTooLargeException} before any HTML construction or + * Dompdf rendering begins. + * + * @var int + */ + public const MAX_PDF_EXPORT_ROWS = 5000; + /** * Register mapper instance * @@ -178,8 +197,8 @@ private function isUserAdmin(?IUser $user): bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Export requires handling multiple input combinations * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-11 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md */ public function exportToExcel( ?Register $register=null, @@ -233,7 +252,7 @@ public function exportToExcel( * * @throws \InvalidArgumentException If trying to export multiple schemas to CSV * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/data-import-export/spec.md */ public function exportToCsv( ?Register $register=null, @@ -258,6 +277,339 @@ public function exportToCsv( return ob_get_clean(); }//end exportToCsv() + /** + * Export objects of a single schema to a JSON document + * + * Serialises every object (subject to the same RBAC + multi-tenancy + filter + * rules as the spreadsheet exporters) to its canonical `jsonSerialize()` + * representation — body properties at the top level plus the `@self` + * metadata block. The result is a JSON array re-importable via + * `ImportService::importFromJson()`, which upserts by `@self.id`. + * + * @param Register|null $register Register context (required) + * @param Schema|null $schema Schema whose objects are exported (required) + * @param array $filters Optional `@self.*` metadata filters + * + * @return string Pretty-printed JSON array of objects + * + * @throws InvalidArgumentException When no schema is given (JSON export is single-schema, like CSV) + * + * @spec exclude Retrofit — JSON object export added alongside the existing Excel/CSV exporters; no dedicated openspec change. + */ + public function exportToJson( + ?Register $register=null, + ?Schema $schema=null, + array $filters=[] + ): string { + if ($schema === null) { + throw new InvalidArgumentException('JSON export requires a specific schema.'); + } + + $objects = $this->fetchObjectsForExport(register: $register, schema: $schema, filters: $filters); + + $rows = array_map( + static fn(ObjectEntity $object): array => $object->jsonSerialize(), + $objects + ); + + return json_encode($rows, (JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + }//end exportToJson() + + /** + * Export data to PDF format + * + * Renders the same tabular data the CSV/Excel exporters produce into an + * A4-landscape PDF via Dompdf (already an OpenRegister dependency, used + * by `PdfReportWriter`). Reuses the existing column/data-extraction + * pipeline (`fetchObjectsForExport()`, `getHeaders()`, + * `identifyNameCompanionColumns()`, `resolveUuidNameMap()`, + * `getObjectValue()`, `resolveUuidsToNames()`) — only the renderer is + * new. When `$register` is given without `$schema`, one table section + * per schema is rendered (the PDF analogue of `exportToExcel()`'s + * one-sheet-per-schema behaviour). + * + * @param Register|null $register Optional register to export + * @param Schema|null $schema Optional schema to export + * @param array $filters Optional filters to apply + * @param IUser|null $currentUser Current user for permission checks + * + * @return string Raw PDF bytes + * + * @throws ExportTooLargeException When the object count exceeds {@see self::MAX_PDF_EXPORT_ROWS}. + * + * @spec openspec/specs/export-pdf-format/spec.md + */ + public function exportToPdf( + ?Register $register=null, + ?Schema $schema=null, + array $filters=[], + ?IUser $currentUser=null + ): string { + // Capture register context so getHeaders/getObjectValue can emit + // per-language columns for translatable properties, same as the + // Excel/CSV pipeline. + $this->contextRegister = $register; + + if ($register !== null && $schema === null) { + // Register-level export without a schema: one section per schema, + // mirroring exportToExcel()'s one-sheet-per-schema behaviour. + $sections = $this->buildPdfSectionsForRegister( + register: $register, + filters: $filters, + currentUser: $currentUser + ); + + return $this->renderPdfDocument(sections: $sections); + } + + $objects = $this->fetchObjectsForExport(register: $register, schema: $schema, filters: $filters); + + $this->guardPdfRowCap(rowCount: count($objects)); + + $section = $this->buildPdfSection( + register: $register, + schema: $schema, + objects: $objects, + currentUser: $currentUser + ); + + return $this->renderPdfDocument(sections: [$section]); + }//end exportToPdf() + + /** + * Build one PDF section per schema for a register-level export (no + * single schema selected), mirroring `exportToExcel()`'s + * one-sheet-per-schema behaviour. The row-count cap is enforced on the + * combined total across all schemas before any section HTML is built. + * + * @param Register $register The register whose schemas are exported. + * @param array $filters Optional filters to apply to each schema's fetch. + * @param IUser|null $currentUser Current user for permission checks. + * + * @return string[] One HTML section fragment per schema. + * + * @throws ExportTooLargeException When the combined object count exceeds {@see self::MAX_PDF_EXPORT_ROWS}. + */ + private function buildPdfSectionsForRegister(Register $register, array $filters, ?IUser $currentUser): array + { + $schemas = $this->getSchemasForRegister(register: $register); + $sectionInput = []; + $totalRows = 0; + + foreach ($schemas as $schemaItem) { + $objects = $this->fetchObjectsForExport(register: $register, schema: $schemaItem, filters: $filters); + $totalRows += count($objects); + $sectionInput[] = ['schema' => $schemaItem, 'objects' => $objects]; + } + + $this->guardPdfRowCap(rowCount: $totalRows); + + $sections = []; + foreach ($sectionInput as $input) { + $sections[] = $this->buildPdfSection( + register: $register, + schema: $input['schema'], + objects: $input['objects'], + currentUser: $currentUser + ); + } + + return $sections; + }//end buildPdfSectionsForRegister() + + /** + * Guard the PDF row-count cap. + * + * Extracted as its own method (rather than inlined at each call site) + * so the boundary condition can be unit-tested directly without + * exercising the expensive Dompdf render path — Dompdf's table layout + * (`Cellmap`) has real per-row memory cost, so tests that need to + * assert "N rows over the cap throws" or "N rows at the cap doesn't + * throw" should call this method directly rather than rendering + * thousands of rows just to observe whether an exception was thrown. + * + * @param int $rowCount The object count to check against {@see self::MAX_PDF_EXPORT_ROWS}. + * + * @return void + * + * @throws ExportTooLargeException When `$rowCount` exceeds the configured cap. + */ + private function guardPdfRowCap(int $rowCount): void + { + if ($rowCount > self::MAX_PDF_EXPORT_ROWS) { + throw new ExportTooLargeException(rowCount: $rowCount, maxRows: self::MAX_PDF_EXPORT_ROWS); + } + }//end guardPdfRowCap() + + /** + * Build the escaped HTML for a single schema's export section. + * + * @param Register|null $register Optional register context (used for the title only). + * @param Schema|null $schema Optional schema context. + * @param ObjectEntity[] $objects The already-fetched, already-capped object set for this section. + * @param IUser|null $currentUser Current user for permission checks (drives header visibility). + * + * @return string Escaped HTML for the section (title, meta line, table). + */ + private function buildPdfSection( + ?Register $register, + ?Schema $schema, + array $objects, + ?IUser $currentUser + ): string { + $headers = $this->getHeaders(schema: $schema, currentUser: $currentUser); + $nameColumns = $this->identifyNameCompanionColumns(headers: $headers); + $uuidToNameMap = $this->resolveUuidNameMap(objects: $objects, nameColumns: $nameColumns); + + $titleParts = []; + if ($register !== null) { + $titleParts[] = $register->getTitle() ?? $register->getSlug() ?? 'Register'; + } + + if ($schema !== null) { + $titleParts[] = $schema->getTitle() ?? $schema->getSlug() ?? 'Schema'; + } + + $title = implode(' — ', $titleParts); + if ($title === '') { + $title = 'Export'; + } + + $timestamp = (new DateTime())->format('Y-m-d H:i:s'); + $count = count($objects); + + $html = '
'; + $html .= '

'.htmlspecialchars($title, ENT_QUOTES, 'UTF-8').'

'; + $html .= '

' + .'Exported: '.htmlspecialchars($timestamp, ENT_QUOTES, 'UTF-8') + .' · Objects: '.$count + .'

'; + $html .= ''; + + foreach ($headers as $header) { + $html .= ''; + } + + $html .= ''; + + foreach ($objects as $object) { + $objectData = $object->getObject(); + $html .= ''; + + foreach ($headers as $col => $header) { + $value = $this->getObjectValue(object: $object, header: $header); + + if (isset($nameColumns[$col]) === true) { + // This is a companion name column — resolve UUIDs to names, + // same as writeObjectRows() does for the Excel/CSV pipeline. + $sourceProperty = $nameColumns[$col]; + $rawValue = $objectData[$sourceProperty] ?? null; + $value = $this->resolveUuidsToNames(value: $rawValue, uuidToNameMap: $uuidToNameMap); + } + + $html .= ''; + } + + $html .= ''; + }//end foreach + + $html .= '
'.htmlspecialchars((string) $header, ENT_QUOTES, 'UTF-8').'
'.htmlspecialchars($this->truncatePdfCellValue(value: $value), ENT_QUOTES, 'UTF-8').'
'; + + return $html; + }//end buildPdfSection() + + /** + * Truncate a cell value for PDF display so a single long value cannot + * blow out the table layout or the render time. + * + * @param string|null $value The raw cell value. + * + * @return string The value, truncated to 200 characters with an ellipsis if longer. + */ + private function truncatePdfCellValue(?string $value): string + { + if ($value === null) { + return ''; + } + + $maxLength = 200; + if (mb_strlen($value) > $maxLength) { + return mb_substr($value, 0, $maxLength).'…'; + } + + return $value; + }//end truncatePdfCellValue() + + /** + * Render one or more section HTML fragments into a single A4-landscape PDF. + * + * Uses the same Dompdf sandboxing pattern established by + * `PdfReportWriter`: no remote fetches, no PHP execution. Page numbers + * are added via `Canvas::page_text()`'s safe `{PAGE_NUM}`/`{PAGE_COUNT}` + * placeholder substitution — not the PHP-script-in-HTML mechanism, + * which stays disabled for security. + * + * @param string[] $sections Escaped HTML fragments, one per schema section. + * + * @return string Raw PDF bytes. + */ + private function renderPdfDocument(array $sections): string + { + $style = '@page { margin: 12mm; }' + ."body { font-family: 'DejaVu Sans', sans-serif; font-size: 9pt; color: #1a1a1a; }" + .'h1 { font-size: 14pt; margin: 0 0 2mm 0; }' + .'p.meta { font-size: 8pt; color: #555555; margin: 0 0 4mm 0; }' + .'table { width: 100%; border-collapse: collapse; }' + .'th, td { border: 0.25pt solid #cccccc; padding: 1.5mm 2mm; text-align: left; word-wrap: break-word; }' + .'th { background-color: #2c3e50; color: #ffffff; font-weight: bold; }' + .'tbody tr:nth-child(even) { background-color: #f2f2f2; }' + .'.pdf-section { page-break-after: always; }' + .'.pdf-section:last-child { page-break-after: auto; }'; + + $body = implode('', $sections); + $html = ''.$body.''; + + $options = new Options(); + // No remote stylesheet/image fetches — keeps the renderer hermetic, + // same rationale as PdfReportWriter. + $options->set('isRemoteEnabled', false); + // PHP execution in templates stays disabled; page numbers use + // Canvas::page_text() placeholders instead (see below). + $options->set('isPhpEnabled', false); + $options->set('defaultFont', 'DejaVu Sans'); + $options->set('defaultPaperSize', 'A4'); + $options->set('defaultPaperOrientation', 'landscape'); + + // SECURITY: assert sandbox flags didn't drift via a future refactor. + // Dompdf has a history of SSRF / file-disclosure CVEs + // (CVE-2022-41343, CVE-2023-23924); these flags are the primary + // mitigation and must stay false. + if ($options->getIsRemoteEnabled() !== false || $options->getIsPhpEnabled() !== false) { + throw new RuntimeException( + 'ExportService PDF sandbox configuration drifted; remote-fetch / PHP execution must remain disabled.' + ); + } + + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html, 'UTF-8'); + $dompdf->setPaper('A4', 'landscape'); + $dompdf->render(); + + $canvas = $dompdf->getCanvas(); + $font = $dompdf->getFontMetrics()->getFont('DejaVu Sans'); + $canvas->page_text( + $canvas->get_width() - 70, + $canvas->get_height() - 20, + 'Page {PAGE_NUM} / {PAGE_COUNT}', + $font, + 8, + [0.3, 0.3, 0.3] + ); + + $output = $dompdf->output(); + return $output ?? ''; + }//end renderPdfDocument() + /** * Build an empty import template spreadsheet for a schema * @@ -496,7 +848,7 @@ private function identifyNameCompanionColumns(array $headers): array * * @return array Map of UUID string to human-readable name. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-12 + * @spec openspec/specs/data-import-export/spec.md */ private function resolveUuidNameMap(array $objects, array $nameColumns): array { @@ -602,7 +954,7 @@ private function writeObjectRows( * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/data-import-export/spec.md */ private function getHeaders(?Schema $schema=null, ?IUser $currentUser=null): array { @@ -1022,7 +1374,7 @@ private function collectUuids(mixed $value, array &$allUuids): void * * @return string|null The resolved name(s) in the same format as input * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-12 + * @spec openspec/specs/data-import-export/spec.md */ private function resolveUuidsToNames(mixed $value, array $uuidToNameMap): ?string { diff --git a/lib/Service/FederationShareService.php b/lib/Service/FederationShareService.php new file mode 100644 index 0000000000..eb07c2aa2f --- /dev/null +++ b/lib/Service/FederationShareService.php @@ -0,0 +1,243 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use OCA\OpenRegister\Db\FederatedShare; +use OCA\OpenRegister\Db\FederatedShareMapper; +use OCP\Security\ISecureRandom; +use Psr\Log\LoggerInterface; + +/** + * Create, list and revoke federated shares. + */ +class FederationShareService +{ + + /** + * Recognised share scopes. + * + * @var string[] + */ + private const SCOPES = ['register', 'schema', 'object', 'query']; + + /** + * Recognised permission grants. + * + * @var string[] + */ + private const PERMISSIONS = ['read', 'read-write']; + + /** + * Constructor. + * + * @param FederatedShareMapper $shareMapper Federated-share persistence. + * @param OrganisationService $organisationService Active-organisation resolver. + * @param ISecureRandom $secureRandom Secure token generator. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly FederatedShareMapper $shareMapper, + private readonly OrganisationService $organisationService, + private readonly ISecureRandom $secureRandom, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Create an outgoing federated share and mint its scoped token. + * + * @param array $params Share parameters: scope, register, + * schema, objectUri, queryFilter, + * sharedWith (slug@host), permissions, + * remoteInstanceUrl. + * + * @return FederatedShare The persisted share (carrying the minted token). + * + * @throws \InvalidArgumentException When scope/permissions are invalid. + */ + public function createOutgoingShare(array $params): FederatedShare + { + $scope = (string) ($params['scope'] ?? 'schema'); + if (in_array($scope, self::SCOPES, true) === false) { + throw new \InvalidArgumentException('Invalid share scope: '.$scope); + } + + $permissions = (string) ($params['permissions'] ?? 'read'); + if (in_array($permissions, self::PERMISSIONS, true) === false) { + throw new \InvalidArgumentException('Invalid permissions: '.$permissions); + } + + $organisation = $this->activeOrganisationUuid(); + + $data = [ + 'direction' => 'outgoing', + 'shareToken' => $this->mintToken(), + 'scope' => $scope, + 'register' => ($params['register'] ?? null), + 'schema' => ($params['schema'] ?? null), + 'objectUri' => ($params['objectUri'] ?? null), + 'queryFilter' => ($params['queryFilter'] ?? null), + 'permissions' => $permissions, + 'organisation' => $organisation, + 'sharedWith' => ($params['sharedWith'] ?? null), + 'remoteInstanceUrl' => ($params['remoteInstanceUrl'] ?? null), + // Outgoing token shares are immediately usable; an OCM share moves to + // 'accepted' once the remote confirms, but the local grant stands. + 'status' => 'accepted', + ]; + + return $this->shareMapper->createFromArray(data: $data); + }//end createOutgoingShare() + + /** + * Record an incoming federated share (a remote shared with an org here). + * + * @param array $params Share parameters: remoteInstanceUrl, + * shareToken, scope, register, schema, + * objectUri, permissions, sharedWith, + * remoteProviderId. + * + * @return FederatedShare The persisted incoming share (status 'pending'). + */ + public function recordIncomingShare(array $params): FederatedShare + { + $data = [ + 'direction' => 'incoming', + 'remoteInstanceUrl' => ($params['remoteInstanceUrl'] ?? null), + 'remoteProviderId' => ($params['remoteProviderId'] ?? null), + 'shareToken' => (string) ($params['shareToken'] ?? $this->mintToken()), + 'scope' => (string) ($params['scope'] ?? 'schema'), + 'register' => ($params['register'] ?? null), + 'schema' => ($params['schema'] ?? null), + 'objectUri' => ($params['objectUri'] ?? null), + 'permissions' => (string) ($params['permissions'] ?? 'read'), + 'organisation' => $this->activeOrganisationUuid(), + 'sharedWith' => ($params['sharedWith'] ?? null), + 'status' => 'pending', + ]; + + return $this->shareMapper->createFromArray(data: $data); + }//end recordIncomingShare() + + /** + * Ensure an outgoing object-scope share exists for a uri + target. + * + * Idempotent: returns the existing share when one is already present (so the + * federate-share flow action can fire on every save without duplicating). + * + * @param string $objectUri The shared object's uri/uuid. + * @param string|null $register The register id/slug. + * @param string|null $schema The schema id/slug. + * @param string $sharedWith The federated target (slug@host). + * @param string $permissions 'read' or 'read-write'. + * + * @return FederatedShare The existing or newly created share. + */ + public function ensureObjectShare( + string $objectUri, + ?string $register, + ?string $schema, + string $sharedWith, + string $permissions='read' + ): FederatedShare { + $existing = $this->shareMapper->findOutgoingObjectShare(objectUri: $objectUri, sharedWith: $sharedWith); + if ($existing !== null) { + return $existing; + } + + return $this->createOutgoingShare( + params: [ + 'scope' => 'object', + 'register' => $register, + 'schema' => $schema, + 'objectUri' => $objectUri, + 'sharedWith' => $sharedWith, + 'permissions' => $permissions, + ] + ); + }//end ensureObjectShare() + + /** + * List federated shares, optionally by direction. + * + * @param string|null $direction 'outgoing' | 'incoming' | null for all. + * + * @return FederatedShare[] The shares for the active organisation. + */ + public function listShares(?string $direction=null): array + { + $filters = []; + if ($direction !== null && $direction !== '') { + $filters['direction'] = $direction; + } + + return $this->shareMapper->findAll(limit: null, offset: null, filters: $filters); + }//end listShares() + + /** + * Set the status of a share (e.g. accept, decline, revoke). + * + * @param int $id The share id. + * @param string $status The new status. + * + * @return FederatedShare The updated share. + */ + public function setStatus(int $id, string $status): FederatedShare + { + return $this->shareMapper->updateFromArray(id: $id, data: ['status' => $status]); + }//end setStatus() + + /** + * Resolve the active organisation UUID (null when none). + * + * @return string|null The active organisation UUID. + */ + private function activeOrganisationUuid(): ?string + { + try { + $organisation = $this->organisationService->getActiveOrganisation(); + if ($organisation !== null) { + return $organisation->getUuid(); + } + } catch (\Throwable $e) { + $this->logger->warning('[Federation] could not resolve active organisation: '.$e->getMessage()); + } + + return null; + }//end activeOrganisationUuid() + + /** + * Mint a scoped bearer share token. + * + * @return string A 48-char alphanumeric token. + */ + private function mintToken(): string + { + return $this->secureRandom->generate(48, ISecureRandom::CHAR_ALPHANUMERIC); + }//end mintToken() +}//end class diff --git a/lib/Service/FieldEncryptionHandler.php b/lib/Service/FieldEncryptionHandler.php new file mode 100644 index 0000000000..7368056d34 --- /dev/null +++ b/lib/Service/FieldEncryptionHandler.php @@ -0,0 +1,278 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/field-level-encryption/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Exception\FieldDecryptionException; +use OCP\Security\ICrypto; +use Psr\Log\LoggerInterface; + +/** + * Encrypts/decrypts schema-flagged object properties at the save/render boundary. + * + * Reuses Nextcloud's `OCP\Security\ICrypto` (AES-256-GCM + HMAC, keyed off the + * instance secret in config.php) — the exact same primitive OpenRegister already + * uses for source credentials ({@see \OCA\OpenRegister\Controller\SourcesController}) + * and the tenant audit-trail HMAC key ({@see TenantKeyService}). No new crypto is + * introduced; key material is never handled directly by this class. + * + * Envelope format: every encrypted value is stored as + * `ENVELOPE_PREFIX . ICrypto::encrypt($plaintext)`. The prefix disambiguates + * ciphertext from plaintext during a mixed rollout (a field flagged encrypted + * "today" may still hold plaintext from before the flag was set, until the + * migration command — {@see \OCA\OpenRegister\Command\EncryptFieldCommand} — + * or a subsequent save re-encrypts it) and lets decryption fail fast on a + * value that was never encrypted rather than passing garbage to ICrypto. + * + * @package OCA\OpenRegister\Service + * + * @psalm-suppress UnusedClass + */ +class FieldEncryptionHandler +{ + /** + * Prefix that marks a stored value as an OpenRegister encryption envelope. + * + * Versioned so a future envelope format change (e.g. a different AEAD + * construction) can be introduced without breaking values already encrypted + * under v1 — decryption dispatches on the version segment. + * + * @var string + */ + public const ENVELOPE_PREFIX = 'openregister:enc:v1:'; + + /** + * Constructor. + * + * @param ICrypto $crypto Nextcloud crypto service (encrypt/decrypt at rest). + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly ICrypto $crypto, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Whether a value is an OpenRegister v1 encryption envelope. + * + * @param mixed $value The value to inspect. + * + * @return bool True when the value is a string carrying the envelope prefix. + */ + public function isEnvelope(mixed $value): bool + { + return is_string($value) === true && str_starts_with($value, self::ENVELOPE_PREFIX) === true; + }//end isEnvelope() + + /** + * Encrypt a single plaintext value into its stored envelope form. + * + * @param string $plaintext The plaintext value. + * + * @return string The envelope-tagged ciphertext. + */ + public function encryptValue(string $plaintext): string + { + return self::ENVELOPE_PREFIX.$this->crypto->encrypt($plaintext); + }//end encryptValue() + + /** + * Decrypt a single envelope value back to plaintext. + * + * @param string $envelope The envelope-tagged ciphertext. + * + * @return string The decrypted plaintext. + * + * @throws FieldDecryptionException When the value is not a recognised + * envelope, or ICrypto fails to decrypt it + * (wrong/rotated instance secret, corrupted + * ciphertext). Never swallowed — the fleet + * lesson is that a swallowed catch here + * looks like a healthy app with a dead + * feature (silent data loss). + */ + public function decryptValue(string $envelope): string + { + if ($this->isEnvelope(value: $envelope) === false) { + throw new FieldDecryptionException( + message: 'Value is not an OpenRegister v1 encryption envelope; cannot decrypt.' + ); + } + + $ciphertext = substr($envelope, strlen(self::ENVELOPE_PREFIX)); + + try { + return $this->crypto->decrypt($ciphertext); + } catch (\Throwable $e) { + throw new FieldDecryptionException( + message: 'Failed to decrypt field value: '.$e->getMessage(), + previous: $e + ); + } + }//end decryptValue() + + /** + * Encrypt every schema-flagged property present in the given data. + * + * Idempotent: a value that is already an envelope is left untouched, so + * calling this twice on the same data (e.g. a resave of an already-encrypted + * object, or a re-run of the migration command) never double-encrypts. + * Only string, non-empty values are encrypted — a null/empty value has + * nothing to protect and is passed through unchanged; a non-string value + * (array/object-typed property) is left as-is and logged, since ICrypto + * operates on strings (documented v1 limitation in design.md). + * + * @param array $data The object data about to be persisted. + * @param Schema $schema The object's schema (source of the encrypted-property list). + * + * @return array The data with flagged properties replaced by their envelope form. + * + * @spec openspec/specs/field-level-encryption/spec.md#requirement-flagged-properties-are-encrypted-on-save + */ + public function encryptProperties(array $data, Schema $schema): array + { + if ($schema->hasEncryptedProperties() === false) { + return $data; + } + + foreach ($schema->getEncryptedProperties() as $property) { + if (array_key_exists($property, $data) === false) { + continue; + } + + $value = $data[$property]; + + if ($value === null || $value === '') { + continue; + } + + if (is_string($value) === false) { + $this->logger->warning( + message: '[FieldEncryptionHandler] Skipped non-string value for encrypted property', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'property' => $property, + 'type' => gettype($value), + ] + ); + continue; + } + + if ($this->isEnvelope(value: $value) === true) { + // Already encrypted — idempotent no-op. + continue; + } + + $data[$property] = $this->encryptValue(plaintext: $value); + }//end foreach + + return $data; + }//end encryptProperties() + + /** + * Decrypt every schema-flagged property present in the given data. + * + * Only decrypts properties that are STILL PRESENT in `$data`. This is the + * composition point with RBAC/writeOnly redaction: the caller (RenderObject) + * runs this AFTER stripping fields the caller is not authorised to see, so + * an unauthorized reader never reaches this method for a field it should + * not have — it gets the same absent-field redaction every other property + * gets, never ciphertext, never plaintext. + * + * A value that is not (yet) an envelope is returned unchanged — the mixed + * plaintext/ciphertext rollout state described in design.md. A value that + * IS an envelope but fails to decrypt (missing/rotated instance secret, + * corrupted data) never fails silently: by default it is replaced with a + * structured error marker and logged at ERROR (so a single bad row does not + * 500 an entire list endpoint); pass `$throwOnFailure: true` for contexts + * that must fail loud instead (the migration command, single-object + * integrity checks). + * + * @param array $data The rendered object data. + * @param Schema $schema The object's schema. + * @param bool $throwOnFailure When true, rethrow decryption failures instead + * of substituting an error marker. + * + * @return array The data with flagged properties decrypted where possible. + * + * @throws FieldDecryptionException When `$throwOnFailure` is true and decryption fails. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The flag selects fail-loud vs + * fail-marked disposition, matching the + * established _rbac/_multitenancy flag idiom. + * + * @spec openspec/specs/field-level-encryption/spec.md#requirement-authorized-reads-are-decrypted-unauthorized-reads-never-see-ciphertext + */ + public function decryptProperties(array $data, Schema $schema, bool $throwOnFailure=false): array + { + if ($schema->hasEncryptedProperties() === false) { + return $data; + } + + foreach ($schema->getEncryptedProperties() as $property) { + if (array_key_exists($property, $data) === false) { + continue; + } + + $value = $data[$property]; + + if (is_string($value) === false || $this->isEnvelope(value: $value) === false) { + // Not encrypted yet (mixed rollout state) or not a string — pass through. + continue; + } + + try { + $data[$property] = $this->decryptValue(envelope: $value); + } catch (FieldDecryptionException $e) { + $this->logger->error( + message: '[FieldEncryptionHandler] Decryption failed for property', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'property' => $property, + 'schema' => $schema->getId(), + 'error' => $e->getMessage(), + ] + ); + + if ($throwOnFailure === true) { + throw $e; + } + + $data[$property] = [ + '@openregister_decryption_error' => true, + 'message' => 'This field could not be decrypted (key unavailable or rotated).', + ]; + }//end try + }//end foreach + + return $data; + }//end decryptProperties() +}//end class diff --git a/lib/Service/File/ChunkTextMatcher.php b/lib/Service/File/ChunkTextMatcher.php new file mode 100644 index 0000000000..acf1ef4d8b --- /dev/null +++ b/lib/Service/File/ChunkTextMatcher.php @@ -0,0 +1,293 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/entity-relation-grondslagen/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File; + +use OCA\OpenRegister\Db\Chunk; +use OCA\OpenRegister\Exception\ChunkMatcherException; + +/** + * Find every occurrence of an operator-supplied needle in a file's chunks. + * + * Stateless; safe to inject as a singleton. All public methods take + * their full context as parameters. + */ +class ChunkTextMatcher +{ + /** + * Number of context characters captured on each side of every match. + */ + private const CONTEXT_WINDOW = 30; + + /** + * Find every occurrence of `$needle` in `$chunks`, deduped by + * absolute position across chunk boundaries. + * + * @param Chunk[] $chunks Chunks for the target file, ordered by chunkIndex. + * @param string $needle Operator-supplied value to match. + * @param bool $wholeWord Wrap the needle in `\b...\b` regex boundaries when true. + * @param bool $caseSensitive Use case-sensitive matching when true. + * @param int $chunkOverlap Configured chunk overlap (chars). Needles longer than this + * cannot reliably be matched per-chunk and are rejected. + * + * @return array + * Matches in document order. Empty when no match found. + * + * @throws ChunkMatcherException When the needle is longer than `$chunkOverlap` or the + * regex pattern fails to compile. + */ + public function match( + array $chunks, + string $needle, + bool $wholeWord, + bool $caseSensitive, + int $chunkOverlap + ): array { + if ($needle === '') { + return []; + } + + $needleLength = strlen($needle); + if ($needleLength > $chunkOverlap) { + // The needle is longer than the configured chunk overlap. + // A match could straddle a chunk boundary in a way that fits + // in neither chunk's tail nor head — per-chunk regex would + // miss it. Rejecting is safer than silently under-matching. + throw new ChunkMatcherException( + reason: ChunkMatcherException::REASON_VALUE_TOO_LONG, + message: sprintf( + 'Needle length (%d bytes) exceeds chunk overlap (%d).', + $needleLength, + $chunkOverlap + ) + ); + } + + if (empty($chunks) === true) { + return []; + } + + $pattern = $this->buildPattern( + needle: $needle, + wholeWord: $wholeWord, + caseSensitive: $caseSensitive + ); + + // Per-chunk regex pass. Collect (absoluteStart, absoluteEnd, chunkId, + // chunkIndex, chunkRelativeStart) for every hit; the dedup pass below + // collapses overlap-region duplicates. + $hits = []; + foreach ($chunks as $chunk) { + $this->collectHits( + chunk: $chunk, + pattern: $pattern, + needleLength: $needleLength, + hits: $hits + ); + } + + if (empty($hits) === true) { + return []; + } + + // Dedup by (absoluteStart, absoluteEnd), keeping the entry from + // the LOWEST chunkIndex. This is deterministic — re-runs select + // the same canonical chunk for each unique absolute position, + // so `existsForFileAtPosition` lookups on subsequent calls hit + // the same (chunkId, positionStart, positionEnd) tuple. + $canonical = $this->dedupByAbsolutePosition(hits: $hits); + + // Sort by absoluteStart for document-order output. + usort( + $canonical, + static fn (array $a, array $b): int => ($a['absoluteStart'] <=> $b['absoluteStart']) + ); + + // Map to the public output shape. + $result = []; + foreach ($canonical as $hit) { + $result[] = [ + 'chunkId' => $hit['chunkId'], + 'positionStart' => $hit['chunkRelativeStart'], + 'positionEnd' => ($hit['chunkRelativeStart'] + $needleLength), + 'context' => $hit['context'], + ]; + } + + return $result; + + }//end match() + + /** + * Build the regex pattern from the needle + match options. + * + * @param string $needle Raw needle. + * @param bool $wholeWord Wrap in `\b...\b` when true. + * @param bool $caseSensitive Use the `i` modifier when false. + * + * @return string Compiled pattern ready for preg_match_all. + * + * @throws ChunkMatcherException On regex compile failure (malformed Unicode in needle). + */ + private function buildPattern(string $needle, bool $wholeWord, bool $caseSensitive): string + { + $quoted = preg_quote($needle, '/'); + if ($wholeWord === true) { + $quoted = '\b'.$quoted.'\b'; + } + + $flags = 'u'; + if ($caseSensitive === false) { + $flags .= 'i'; + } + + $pattern = '/'.$quoted.'/'.$flags; + + // Probe the pattern with a zero-byte string to surface compile + // failures early (malformed Unicode in the needle is the realistic + // trigger). + $probe = @preg_match($pattern, ''); + if ($probe === false) { + throw new ChunkMatcherException( + reason: ChunkMatcherException::REASON_REGEX_COMPILE_FAILURE, + message: 'Regex pattern failed to compile (malformed needle).' + ); + } + + return $pattern; + + }//end buildPattern() + + /** + * Run preg_match_all against one chunk and accumulate hits. + * + * `$hits` carries entries of shape + * `{absoluteStart: int, absoluteEnd: int, chunkId: int, chunkIndex: int, chunkRelativeStart: int, context: string}`. + * + * @param Chunk $chunk Source chunk. + * @param string $pattern Compiled regex pattern. + * @param int $needleLength Pre-computed needle length in bytes. + * @param array $hits Hits accumulator (mutated by reference). + * + * @return void + */ + private function collectHits(Chunk $chunk, string $pattern, int $needleLength, array &$hits): void + { + $text = $chunk->getTextContent(); + $matches = []; + $count = preg_match_all( + pattern: $pattern, + subject: $text, + matches: $matches, + flags: (PREG_OFFSET_CAPTURE | PREG_SET_ORDER) + ); + + if ($count === false || $count === 0) { + return; + } + + $startOffset = $chunk->getStartOffset(); + $chunkIndex = $chunk->getChunkIndex(); + $chunkId = (int) $chunk->getId(); + + foreach ($matches as $match) { + $chunkRelativeStart = (int) $match[0][1]; + $absoluteStart = ($startOffset + $chunkRelativeStart); + $absoluteEnd = ($absoluteStart + $needleLength); + + $hits[] = [ + 'absoluteStart' => $absoluteStart, + 'absoluteEnd' => $absoluteEnd, + 'chunkId' => $chunkId, + 'chunkIndex' => $chunkIndex, + 'chunkRelativeStart' => $chunkRelativeStart, + 'context' => $this->extractContext( + text: $text, + relativeStart: $chunkRelativeStart, + needleLength: $needleLength + ), + ]; + } + + }//end collectHits() + + /** + * Collapse multiple per-chunk hits at the same absolute position + * to one entry, keeping the lowest chunkIndex. + * + * Entry shape: `{absoluteStart, absoluteEnd, chunkId, chunkIndex, + * chunkRelativeStart, context}` (all keys present). + * + * @param array $hits Raw per-chunk hits. + * + * @return array Deduped hits with the same entry shape. + */ + private function dedupByAbsolutePosition(array $hits): array + { + $byKey = []; + foreach ($hits as $hit) { + $key = $hit['absoluteStart'].':'.$hit['absoluteEnd']; + if (isset($byKey[$key]) === false) { + $byKey[$key] = $hit; + continue; + } + + // Tiebreak: keep the entry from the lowest chunkIndex. + if ($hit['chunkIndex'] < $byKey[$key]['chunkIndex']) { + $byKey[$key] = $hit; + } + } + + return array_values($byKey); + + }//end dedupByAbsolutePosition() + + /** + * Return ~CONTEXT_WINDOW chars on either side of the match. + * + * @param string $text Source chunk text. + * @param int $relativeStart Match start offset within the chunk. + * @param int $needleLength Needle length in bytes. + * + * @return string Snippet with the match in the middle. + */ + private function extractContext(string $text, int $relativeStart, int $needleLength): string + { + $start = max(0, ($relativeStart - self::CONTEXT_WINDOW)); + $end = min(strlen($text), ($relativeStart + $needleLength + self::CONTEXT_WINDOW)); + $length = ($end - $start); + + return substr($text, $start, $length); + + }//end extractContext() +}//end class diff --git a/lib/Service/File/CreateFileHandler.php b/lib/Service/File/CreateFileHandler.php index b1f270e0b4..1d5f8eb6ff 100644 --- a/lib/Service/File/CreateFileHandler.php +++ b/lib/Service/File/CreateFileHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -122,7 +122,7 @@ public function setFileService(FileService $fileService): void * File creation requires handling multiple content formats and error cases. * @SuppressWarnings(PHPMD.NPathComplexity) Multiple execution paths for content processing and validation. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-006 + * @spec openspec/specs/file-actions/spec.md */ public function addFile( ObjectEntity|string $objectEntity, @@ -176,15 +176,20 @@ public function addFile( // Security: Block executable files. $this->fileValidHandler->blockExecutableFile(fileName: $fileName, fileContent: $content); + // The newFile() call already enforces create permission on the folder; it + // creates the node owned by the folder's mount owner (the openregister + // system user for the OpenRegister folder, or the original owner for a + // folder linked from outside it). $file = $folder->newFile($fileName); - // @TODO: Check ownership to prevent "File not found" errors - hack for NextCloud rights issues. + // Assert the session can reach the freshly created node. $this->fileValidHandler->checkOwnership($file); // Write content to the file. $file->putContent($content); - // Transfer ownership to OpenRegister and share with current user if needed. + // Re-own to the openregister user only as a fallback when the session + // lacks write rights; otherwise ownership is left following the folder. $this->fileOwnershipHandler->transferFileOwnershipIfNeeded($file); // Create a share link for the file if requested. @@ -249,7 +254,7 @@ public function addFile( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flag is intentional for simple share toggle. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-006 + * @spec openspec/specs/file-actions/spec.md */ public function saveFile( ObjectEntity $objectEntity, diff --git a/lib/Service/File/DeleteFileHandler.php b/lib/Service/File/DeleteFileHandler.php index baf4858828..bd236fb62f 100644 --- a/lib/Service/File/DeleteFileHandler.php +++ b/lib/Service/File/DeleteFileHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -27,6 +27,7 @@ use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\Node; +use OCP\Files\NotPermittedException; use Psr\Log\LoggerInterface; /** @@ -83,7 +84,7 @@ public function __construct( * * @psalm-param Node|string|int $file * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md */ public function deleteFile(Node|string|int $file, ?ObjectEntity $object=null): bool { @@ -116,9 +117,15 @@ public function deleteFile(Node|string|int $file, ?ObjectEntity $object=null): b // Reject when the file is locked by someone else. $this->fileLockHandler->assertCanModify($file->getId()); - // @TODO: Check ownership to prevent "File not found" errors - hack for NextCloud rights issues. + // Assert the session can reach the file (owned or shared). $this->fileValidHandler->checkOwnership($file); + // Deleting additionally requires delete permission. NC enforces this + // natively on delete(), but we fail fast with a clear message. + if ($file->isDeletable() === false) { + throw new NotPermittedException("File {$file->getName()} is not deletable by the current session"); + } + try { $file->delete(); } catch (Exception $e) { @@ -142,7 +149,7 @@ public function deleteFile(Node|string|int $file, ?ObjectEntity $object=null): b * * @psalm-return list * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md */ public function deleteFiles(array $files, ?ObjectEntity $object=null): array { diff --git a/lib/Service/File/DocumentProcessingHandler.php b/lib/Service/File/DocumentProcessingHandler.php index 77ac513207..d088ee8f80 100644 --- a/lib/Service/File/DocumentProcessingHandler.php +++ b/lib/Service/File/DocumentProcessingHandler.php @@ -8,23 +8,34 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); namespace OCA\OpenRegister\Service\File; +use DateTime; use Exception; +use OCA\OpenRegister\Db\AnonymisationLog; +use OCA\OpenRegister\Db\AnonymisationLogMapper; +use OCA\OpenRegister\Exception\PdfAnonymisationException; +use OCA\OpenRegister\Exception\SanitizationException; +use OCA\OpenRegister\Service\File\Pdf\PdfMetadataSanitizer; +use OCA\OpenRegister\Service\File\Pdf\PdfTextReplacer; use OCA\OpenRegister\Service\FileService; +use OCA\OpenRegister\Service\TextExtraction\EntityRecognitionHandler; +use Throwable; use OCP\Files\File; +use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\Node; +use OCP\IL10N; use OCP\IUser; use OCP\IUserSession; use PhpOffice\PhpWord\Exception\CopyFileException; @@ -33,6 +44,8 @@ use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\TemplateProcessor; use Psr\Log\LoggerInterface; +use Smalot\PdfParser\Parser as PdfParser; +use ZipArchive; /** * Handles document processing operations. @@ -48,10 +61,37 @@ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html * @link https://github.com/ConductionNL/openregister * @version 1.0.0 + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Orchestrates the Word/PDF/text replacement pipelines plus SAPP + PhpWord integration. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) File / PDF / PhpWord / sanitiser collaborators are required by design. */ class DocumentProcessingHandler { + /** + * The enumerated entity-type labels that get localised in the placeholder. + * + * The canonical source is the `EntityRecognitionHandler::ENTITY_TYPE_*` + * constants. Each value is registered as a translatable string in `l10n/` + * (en + nl), so `IL10N::t()` returns the localised label on a Dutch + * instance (`PERSON` → `PERSOON`). A type NOT in this set falls back to its + * raw string (no translation, no error). + * + * @var array + */ + private const LOCALIZABLE_ENTITY_TYPES = [ + EntityRecognitionHandler::ENTITY_TYPE_PERSON, + EntityRecognitionHandler::ENTITY_TYPE_ORGANIZATION, + EntityRecognitionHandler::ENTITY_TYPE_LOCATION, + EntityRecognitionHandler::ENTITY_TYPE_EMAIL, + EntityRecognitionHandler::ENTITY_TYPE_PHONE, + EntityRecognitionHandler::ENTITY_TYPE_ADDRESS, + EntityRecognitionHandler::ENTITY_TYPE_DATE, + EntityRecognitionHandler::ENTITY_TYPE_IBAN, + EntityRecognitionHandler::ENTITY_TYPE_SSN, + EntityRecognitionHandler::ENTITY_TYPE_IP_ADDRESS, + ]; + /** * Reference to FileService for cross-handler coordination (circular dependency break). * @@ -59,20 +99,123 @@ class DocumentProcessingHandler */ private ?FileService $fileService = null; + /** + * The most recent sanitisation report produced during anonymisation. + * + * Office (DOCX / ODT) anonymisation runs the sanitiser ahead of the + * entity walker; the resulting audit report is retained here so the + * caller can persist or surface it (there is no dedicated log table). + * Null when the last anonymisation did not involve a sanitisable format. + * + * @var SanitizationReport|null + */ + private ?SanitizationReport $lastSanitizationReport = null; + + /** + * Residual entities from the most recent anonymisation (best-effort policy). + * + * Each record: {text: string, type: string, id: string} for an entity whose + * text could not be fully removed from the output (e.g. the ExApp NER + * over-captured across table cells, so the value is not contiguous in the + * PDF). Empty when the last run was complete. Consumed by the controller to + * surface a warning so the operator can iterate (manual/skip entities). + * + * @var array + */ + private array $lastResidualEntities = []; + + /** + * Per-entity placeholder map from the most recent anonymisation. + * + * Maps the internal global entity id (`openregister_entities.id`, stringified) + * to the EXACT placeholder string emitted into the document for that entity + * (e.g. `"7" => "[PERSOON: 1]"`). Lets downstream consumers (DocuDesk's + * grondslagen-summary) render the SAME placeholder the document carries + * — scope-local number + localized TYPE label — instead of re-deriving + * `[: ]` from the global id. Empty when the last run + * matched no catalogue entity. + * + * @var array + */ + private array $lastPlaceholderMap = []; + /** * Constructor for DocumentProcessingHandler. * - * @param IRootFolder $rootFolder Root folder for file access. - * @param IUserSession $userSession User session for getting current user. - * @param LoggerInterface $logger Logger for logging operations. + * @param IRootFolder $rootFolder Root folder for file access. + * @param IUserSession $userSession User session for getting current user. + * @param LoggerInterface $logger Logger for logging operations. + * @param \OCA\OpenRegister\Db\EntityRelationMapper $entityRelationMapper Used to honour skip-anonymization + * flags during the redaction pass + * (see + * `entity-relation-grondslagen`). + * @param OfficeDocumentSanitizer $sanitizer Office document sanitiser (DOCX / ODT). + * @param AnonymisationLogMapper|null $anonymisationLogMapper Mapper for persisting per-run anonymisation + * log rows (carries the sanitisation report). + * Nullable so the handler stays construct-safe + * for tests that do not need persistence. + * @param IL10N|null $l10n Acting-user localisation, used to translate + * the placeholder TYPE label (e.g. PERSON → + * PERSOON on a Dutch instance). Nullable: when + * absent the raw English label is emitted + * (construct-safe for tests / non-localised + * callers). */ public function __construct( private readonly IRootFolder $rootFolder, private readonly IUserSession $userSession, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly \OCA\OpenRegister\Db\EntityRelationMapper $entityRelationMapper, + private readonly OfficeDocumentSanitizer $sanitizer, + private readonly ?AnonymisationLogMapper $anonymisationLogMapper=null, + private readonly ?IL10N $l10n=null ) { }//end __construct() + /** + * Get the sanitisation report from the most recent anonymisation, if any. + * + * @return SanitizationReport|null The report, or null when the last + * anonymisation did not sanitise an Office document. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function getLastSanitizationReport(): ?SanitizationReport + { + return $this->lastSanitizationReport; + }//end getLastSanitizationReport() + + /** + * Residual entities from the most recent anonymisation run. + * + * Best-effort policy: when some entity text could not be removed from the + * output (e.g. ExApp NER over-capture across table cells), the file is still + * produced and these records describe what remains so the caller can warn + * the operator. Empty when the last run fully redacted everything. + * + * @return array Residual records. + */ + public function getLastResidualEntities(): array + { + return $this->lastResidualEntities; + }//end getLastResidualEntities() + + /** + * Per-entity placeholder map from the most recent anonymizeDocument() call. + * + * Maps the internal global entity id (stringified) to the exact placeholder + * string emitted into the document (e.g. `"7" => "[PERSOON: 1]"`), so the + * grondslagen-summary can render the SAME placeholder the document carries + * (scope-local number + localized label) rather than re-deriving it from the + * global id. Empty when the last run matched no catalogue entity. + * + * @return array Map of global entity id → emitted placeholder. + */ + public function getLastPlaceholderMap(): array + { + return $this->lastPlaceholderMap; + }//end getLastPlaceholderMap() + /** * Set the FileService instance for cross-handler coordination. * @@ -96,6 +239,9 @@ public function setFileService(FileService $fileService): void * @param Node $node The file node to process. * @param array $replacements Array of replacement mappings (search => replace). * @param string|null $outputName Optional name for the output file. + * @param bool $strict PDF only: when true (entity anonymisation), + * residual entity text in the output fails + * closed instead of being logged as partial. * * @throws Exception If node is not a file or replacement fails. * @@ -109,11 +255,13 @@ public function setFileService(FileService $fileService): void * * @psalm-return File * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-008 + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) $strict selects fail-closed vs lenient validation per the entity-anonymisation contract. + * + * @spec openspec/specs/file-actions/spec.md */ - public function replaceWords(Node $node, array $replacements, ?string $outputName=null): File + public function replaceWords(Node $node, array $replacements, ?string $outputName=null, bool $strict=false): File { - if ($node->getType() !== \OCP\Files\FileInfo::TYPE_FILE) { + if (($node instanceof File) === false) { throw new Exception('Node must be a file'); } @@ -134,6 +282,10 @@ public function replaceWords(Node $node, array $replacements, ?string $outputNam return $this->replaceWordsInWordDocument(node: $node, replacements: $replacements, outputName: $outputName); } + if ($fileExtension === 'pdf') { + return $this->replaceWordsInPdfDocument(node: $node, replacements: $replacements, outputName: $outputName, strict: $strict); + } + return $this->replaceWordsInTextDocument(node: $node, replacements: $replacements, outputName: $outputName); }//end replaceWords() @@ -144,8 +296,14 @@ public function replaceWords(Node $node, array $replacements, ?string $outputNam * in the format [ENTITY_TYPE: key]. It builds a replacement mapping from entity detection * results and applies them using the replaceWords method. * - * @param Node $node The file node to anonymize. - * @param array $entities Array of detected entities with 'text', 'entityType', and 'key' fields. + * @param Node $node The file node to anonymize. + * @param array $entities Array of detected entities with 'text', 'entityType', and 'key' fields. + * @param string $scope Placeholder-numbering scope: 'document' (default — counter restarts + * per run, no persistence) or 'dossier' (counter consistent across all + * files in the dossier folder, recomputed deterministically). + * @param string|null $dossierKey Stable folder id identifying the dossier when $scope='dossier'. + * When absent (and scope='dossier') the file's parent folder is used. + * Ignored for the per-document scope. * * @throws Exception If anonymization fails. * @@ -155,21 +313,139 @@ public function replaceWords(Node $node, array $replacements, ?string $outputNam * * @return File The anonymized document file. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-008 + * @spec openspec/specs/file-actions/spec.md */ - public function anonymizeDocument(Node $node, array $entities): File - { + public function anonymizeDocument( + Node $node, + array $entities, + string $scope='document', + ?string $dossierKey=null + ): File { + // Reset any report/residuals/placeholder-map from a prior call on this + // (potentially reused) handler. + $this->lastSanitizationReport = null; + $this->lastResidualEntities = []; + $this->lastPlaceholderMap = []; + + // Resolve the source file id once — the substitution placeholder + // format and the post-redaction audit flag both key off it. + $fileId = 0; + if (method_exists($node, 'getId') === true) { + $candidate = $node->getId(); + if (is_int($candidate) === true && $candidate > 0) { + $fileId = $candidate; + } + } + + // Defensive filter — per the `entity-relation-grondslagen` change, + // the DI anonymise path MUST honour the operator's skip decisions + // even when the caller's entities[] array includes flagged + // occurrences. The OR contract is "skipped relations are never + // redacted, full stop", regardless of caller filtering behaviour. + $skippedValues = []; + if ($fileId > 0) { + $skippedValues = $this->entityRelationMapper->findSkippedEntityValuesForFile($fileId); + } + + // Resolve the existing entity-id map for this file so substitutions + // use the stable `[: ]` placeholder format — + // matches what DocuDesk's grondslagen-summary report shows and + // makes re-runs of anonymise on the same document idempotent + // (the previous UUID-prefix fallback produced a fresh placeholder + // per call, so re-anonymising the same file produced byte-divergent + // output despite identical inputs). + $entityIdMap = []; + if ($fileId > 0) { + $entityIdMap = $this->entityRelationMapper->findEntityIdsByValueForFile($fileId); + } + + // Scope-local placeholder numbering (Decision 1 + 3): translate the + // internal global `e.id` to a number that is local to this scope, so + // the emitted `[: ]` never links a person across + // documents/publications. Per-document (default) numbers lazily by + // first appearance; per-dossier seeds the translator with a map + // deterministically recomputed from the whole dossier's stored rows, + // so the same person is the same number across every file in the + // folder. + if ($scope === 'dossier') { + $translator = $this->recomputeDossierTranslator(node: $node, dossierKey: $dossierKey); + } else { + $translator = PlaceholderIdTranslator::perDocument(); + } + // Build replacements array from entities. $replacements = []; foreach ($entities as $entity) { $originalText = $entity['text'] ?? ''; $entityType = $entity['entityType'] ?? 'UNKNOWN'; - $key = $entity['key'] ?? substr(\Symfony\Component\Uid\Uuid::v4()->toRfc4122(), 0, 8); - if (empty($originalText) === false) { - $replacements[$originalText] = '['.$entityType.': '.$key.']'; + if (empty($originalText) === true + || in_array($originalText, $skippedValues, true) === true + ) { + continue; } - } + + // The needle actually matched/replaced in the document is the + // trimmed text: some recognition backends (and the regex pass) + // capture entity spans with surrounding whitespace (e.g. + // "06-12345678 "), which the document's content stream never + // contains verbatim — leaving the value unredacted while the + // (whitespace-normalising) validation gate still flags it as + // residual. Trim for the map KEY; keep $originalText for the + // skip-list and stable-id lookups (those are keyed by the stored + // value). + $needle = trim($originalText); + if ($needle === '') { + continue; + } + + // Prefer stable per-entity placeholder. Fall back to the + // legacy UUID-prefix only when there is no matching entity + // row on the file (shouldn't happen in the normal + // extract → review → anonymise flow, but defensive against + // direct DI callers that bypass extraction). + // Localise the TYPE label to the acting user's language + // (Decision 6): PERSON → PERSOON on a Dutch instance. Unknown + // types fall back to the raw label. + $localizedType = $this->localizeEntityType(entityType: $entityType); + + $key = $entity['key'] ?? substr(\Symfony\Component\Uid\Uuid::v4()->toRfc4122(), 0, 8); + $replacements[$needle] = '['.$localizedType.': '.$key.']'; + if (isset($entityIdMap[$originalText]) === true) { + // Translate the internal `e.id` to the scope-local number. + $localNumber = $translator->translate(entityId: $entityIdMap[$originalText]['id']); + $replacements[$needle] = '['.$localizedType.': '.$localNumber.']'; + // Record the EXACT emitted placeholder against the global e.id + // so consumers (DocuDesk's grondslagen-summary) can render the + // same string (scope-local number + localized label) instead of + // re-deriving from the global id. + $this->lastPlaceholderMap[(string) $entityIdMap[$originalText]['id']] = $replacements[$needle]; + } + }//end foreach + + // Order needles longest-first so overlapping entities cannot clobber + // each other: with insertion order, a bare "Amsterdam" [LOCATION] + // earlier in the map rewrites "De gemeente Amsterdam" to + // "De gemeente [LOCATION: …]" before the longer needle + // "gemeente Amsterdam" gets a chance to match, leaving it + // unmatched and mis-typed. Longest-first guarantees every needle + // sees the still-untouched text it was detected in for the + // str_ireplace branches (docx/odt/txt), which consume the map in + // insertion order; the PDF branch re-asserts the same ordering + // inside PdfTextReplacer::replaceInPdf so the guarantee survives + // SAPP-side changes. Equal lengths tie-break bytewise for + // deterministic (idempotent re-run) output. Params deliberately + // untyped: PHP coerces purely-numeric needle text ("2026", a + // spaceless BSN) to INT array keys, which would fatal a + // string-typed closure. + uksort( + $replacements, + static function ($left, $right): int { + $left = (string) $left; + $right = (string) $right; + return [mb_strlen($right), $left] <=> [mb_strlen($left), $right]; + } + ); // Generate anonymized file name. $fileName = $node->getName(); @@ -181,9 +457,443 @@ public function anonymizeDocument(Node $node, array $entities): File $anonymizedFileName .= '.'.$fileExtension; } - return $this->replaceWords(node: $node, replacements: $replacements, outputName: $anonymizedFileName); + // Office (DOCX / ODT) documents carry PII in non-text structures the + // walker cannot reach (comments, tracked changes, metadata, custom XML, + // person field codes, hyperlink URLs). Sanitise to a clean derivative + // BEFORE the entity walker pass. The original NC file is untouched. + // Entity anonymisation is GDPR-critical: fail closed (strict) if any + // original entity text survives in the output rather than writing a + // file marked '_anonymized' that still contains it. + $anonymizedFile = $this->replaceWords(node: $node, replacements: $replacements, outputName: $anonymizedFileName, strict: true); + if (($node instanceof File) === true + && $this->sanitizer->isSanitizable($node->getMimeType()) === true + ) { + $anonymizedFile = $this->anonymizeSanitizableDocument( + node: $node, + replacements: $replacements, + outputName: $anonymizedFileName + ); + } + + // Flip the source's EntityRelation rows to `anonymized = 1` so the + // anonymised state is queryable downstream. Skip-aware (rows where + // `skip_anonymization = 1` are preserved per the + // `entity-relation-grondslagen` contract). Each relation's + // `anonymized_value` is set to the EXACT placeholder emitted for its + // entity (scope-local number + localized label, from + // `lastPlaceholderMap`) — the only durable record of the scope-local + // number, which isn't recoverable from the stored rows later. This + // lets the grondslagen-summary render the same placeholder the + // document carries without re-deriving from the global id. The + // persistence lives exactly as long as the relation (overwritten on + // re-anonymise, gone on delete). Relations whose entity is absent from + // the map keep the legacy "[REDACTED]" marker. + if ($fileId > 0 && empty($replacements) === false) { + try { + $this->entityRelationMapper->markAsAnonymizedWithPlaceholders($fileId, $this->lastPlaceholderMap); + } catch (\Throwable $e) { + // Persistence-side failure on the audit flag MUST NOT mask + // the successful redaction; the file is already written. + // Surface via warning log; downstream summary reports + // simply won't see this file until the next anonymise + // call retries the mark. + $this->logger->warning( + 'DocumentProcessingHandler: markAsAnonymizedWithPlaceholders failed after redaction', + ['fileId' => $fileId, 'error' => $e->getMessage()] + ); + } + } + + // Persist a per-run anonymisation log row carrying the sanitisation + // report when a sanitisable Office document was processed. PDF / + // plain-text runs intentionally leave `sanitization = null` per + // spec `office-document-sanitization`. The log write is best-effort + // — a persistence failure MUST NOT mask a successful redaction. + $this->persistAnonymisationLog( + node: $node, + fileId: $fileId, + replacements: $replacements + ); + + return $anonymizedFile; }//end anonymizeDocument() + /** + * Localise an entity-type label to the acting user's language for the + * placeholder (Decision 6). Only the enumerated entity-type set + * (`LOCALIZABLE_ENTITY_TYPES`, sourced from the + * `EntityRecognitionHandler::ENTITY_TYPE_*` constants) is translated; an + * unknown / free-form type is returned unchanged (no translation, no + * error). When no `IL10N` is injected the raw label is returned, which is + * also the `en` / untranslated behaviour. + * + * @param string $entityType The raw entity type (e.g. 'PERSON'). + * + * @return string The localised label (e.g. 'PERSOON' on nl), or the raw type. + */ + private function localizeEntityType(string $entityType): string + { + if ($this->l10n === null + || in_array($entityType, self::LOCALIZABLE_ENTITY_TYPES, true) === false + ) { + return $entityType; + } + + return $this->l10n->t($entityType); + + }//end localizeEntityType() + + /** + * Build a per-dossier numbering translator by deterministically + * recomputing the `e.id → local_number` map from the dossier's stored + * entity-relation rows (Decision 3 — no table, no migration). + * + * Resolves the dossier folder from $dossierKey (a stable folder id) or, + * when absent, the file's parent folder (Decision 2 fallback). Enumerates + * the folder's descendant files (recursively), loads their entity rows in + * one query (`findEntityIdsByValueForFiles`), and ranks distinct + * `entity_id`s by first appearance under the total order + * `(file_id, position_start, entity_id)`. The result is a pure function of + * the stored rows, so every per-file call within the dossier derives the + * same map. Any failure to resolve/enumerate the folder degrades to a + * per-document translator (fail-safe — never throws out of numbering). + * + * Per ADR-005 nothing here logs the entity value alongside its number. + * + * @param Node $node The file being anonymised. + * @param string|null $dossierKey Stable folder id, or null to use the parent folder. + * + * @return PlaceholderIdTranslator Seeded with the dossier map. + */ + private function recomputeDossierTranslator(Node $node, ?string $dossierKey): PlaceholderIdTranslator + { + $folder = $this->resolveDossierFolder(node: $node, dossierKey: $dossierKey); + if ($folder === null) { + return PlaceholderIdTranslator::perDocument(); + } + + $fileIds = $this->collectDescendantFileIds(folder: $folder); + if ($fileIds === []) { + return PlaceholderIdTranslator::perDocument(); + } + + $rows = $this->entityRelationMapper->findEntityIdsByValueForFiles(fileIds: $fileIds); + + // PII-free diagnostic (ADR-005): ids/counts only, never value → number. + $this->logger->debug( + message: '[DocumentProcessingHandler] Recomputed per-dossier placeholder numbering', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'dossier_key' => $dossierKey, + 'folder_id' => $folder->getId(), + 'file_count' => count($fileIds), + 'row_count' => count($rows), + ] + ); + + return PlaceholderIdTranslator::forDossier(rows: $rows); + + }//end recomputeDossierTranslator() + + /** + * Resolve the dossier folder for per-dossier numbering: prefer the + * explicit $dossierKey (a stable folder id), else fall back to the file's + * parent folder (Decision 2). Returns null when neither resolves to a + * usable folder (caller then degrades to per-document). + * + * @param Node $node The file being anonymised. + * @param string|null $dossierKey Stable folder id, or null for the parent-folder fallback. + * + * @return Folder|null The dossier folder, or null when unresolved. + */ + private function resolveDossierFolder(Node $node, ?string $dossierKey): ?Folder + { + // Explicit, authoritative signal: the folder id. + if ($dossierKey !== null && trim($dossierKey) !== '' && ctype_digit(trim($dossierKey)) === true) { + try { + $matches = $this->rootFolder->getById((int) trim($dossierKey)); + foreach ($matches as $candidate) { + if ($candidate instanceof Folder) { + return $candidate; + } + } + } catch (Throwable $e) { + // Fall through to the parent-folder fallback below. + unset($e); + } + } + + // Forgiving fallback: the file's parent folder IS the dossier. + try { + $parent = $node->getParent(); + if ($parent instanceof Folder) { + return $parent; + } + } catch (Throwable $e) { + unset($e); + } + + return null; + + }//end resolveDossierFolder() + + /** + * Enumerate the descendant file ids of a dossier folder (recursive), + * via the Nextcloud Node API. Sub-folders are walked; only file nodes + * contribute ids. + * + * @param Folder $folder The dossier folder. + * + * @return array Distinct descendant file ids. + */ + private function collectDescendantFileIds(Folder $folder): array + { + $fileIds = []; + try { + foreach ($folder->getDirectoryListing() as $child) { + if ($child instanceof Folder) { + foreach ($this->collectDescendantFileIds(folder: $child) as $nestedId) { + $fileIds[] = $nestedId; + } + + continue; + } + + $childId = $child->getId(); + if (is_int($childId) === true && $childId > 0) { + $fileIds[] = $childId; + } + } + } catch (Throwable $e) { + // Best-effort enumeration; partial/empty list degrades numbering + // gracefully rather than failing the anonymise run. + unset($e); + } + + return array_values(array_unique($fileIds)); + + }//end collectDescendantFileIds() + + /** + * Persist a per-run anonymisation log row. + * + * Best-effort: the file is already written; a DB-side failure is logged + * (PII-free) and swallowed. The row carries the JSON-serialised + * sanitisation report when an Office document was sanitised; non-Office + * runs leave `sanitization = null` (spec invariant). + * + * @param Node $node The source file node. + * @param int $fileId The NC file id (0 when absent). + * @param array $replacements The substitution map applied. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function persistAnonymisationLog(Node $node, int $fileId, array $replacements): void + { + if ($this->anonymisationLogMapper === null) { + return; + } + + $entity = new AnonymisationLog(); + $entity->setFileId($fileId); + + $mimeType = ''; + if ($node instanceof File) { + try { + $mimeType = (string) $node->getMimeType(); + } catch (Throwable $ignored) { + $mimeType = ''; + } + } + + $entity->setMimeType($mimeType); + $entity->setEngine($this->resolveEngineName(mimeType: $mimeType)); + $entity->setStatus(AnonymisationLog::STATUS_SUCCESS); + $entity->setReplacements(count($replacements)); + + if ($this->lastSanitizationReport !== null) { + $encoded = json_encode($this->lastSanitizationReport->jsonSerialize()); + if (is_string($encoded) === true) { + $entity->setSanitization($encoded); + } + } + + $entity->setCreated(new DateTime()); + + try { + $this->anonymisationLogMapper->insert(entity: $entity); + } catch (Throwable $e) { + $this->logger->warning( + 'DocumentProcessingHandler: AnonymisationLog persistence failed', + ['fileId' => $fileId, 'error' => $e->getMessage()] + ); + } + }//end persistAnonymisationLog() + + /** + * Resolve a stable engine label for the anonymisation log row. + * + * @param string $mimeType The MIME type of the source file (when known). + * + * @return string The engine class short name used for the run. + */ + private function resolveEngineName(string $mimeType): string + { + if ($this->sanitizer->isSanitizable($mimeType) === true) { + return 'OfficeDocumentSanitizer'; + } + + if ($mimeType === 'application/pdf') { + return 'PdfTextReplacer'; + } + + return 'TextReplacer'; + }//end resolveEngineName() + + /** + * Anonymise a sanitisable Office document (DOCX / ODT). + * + * Runs the document sanitiser to produce a cleaned derivative, then runs + * the existing PhpWord entity walker over the cleaned bytes. The walker's + * output is written to the original file's parent folder under $outputName. + * The sanitisation audit report is retained on {@see getLastSanitizationReport}. + * + * @param File $node The Office file to anonymise. + * @param array $replacements Entity-text => placeholder replacement map. + * @param string $outputName Name for the anonymised output file. + * + * @throws Exception If sanitisation or the walker pass fails. + * + * @phpstan-param array $replacements + * @psalm-param array $replacements + * + * @return File The anonymised document. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function anonymizeSanitizableDocument(File $node, array $replacements, string $outputName): File + { + try { + $result = $this->sanitizer->sanitize($node->getId()); + } catch (SanitizationException $e) { + if ($e->getReason() === SanitizationException::REASON_ENCRYPTED) { + // Caller-correctable: cannot anonymise an encrypted document. + throw new Exception('Cannot anonymise an encrypted document', 0, $e); + } + + $this->logger->error( + message: '[DocumentProcessingHandler] Office document sanitisation failed: '.$e->getReason(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'reason' => $e->getReason(), + ] + ); + throw new Exception('Document sanitisation failed', 0, $e); + }//end try + + $this->lastSanitizationReport = $result->report; + + $fileExtension = strtolower(pathinfo($node->getName(), PATHINFO_EXTENSION)); + + // ODT and other non-DOCX office text route through the text replacer; + // DOCX runs the PhpWord walker. Both read the sanitised temp file. + if ($fileExtension === 'docx') { + return $this->replaceWordsInWordDocument( + node: $node, + replacements: $replacements, + outputName: $outputName, + sanitizedSourcePath: $result->path + ); + } + + return $this->replaceWordsInOfficeContainer( + node: $node, + replacements: $replacements, + outputName: $outputName, + sanitizedSourcePath: $result->path + ); + }//end anonymizeSanitizableDocument() + + /** + * Replace words inside a sanitised Office ZIP container's text parts. + * + * Used for the ODT path: the sanitised derivative is a valid ODT ZIP, so + * entity replacement is applied to the textual content parts in-place and + * the container is written to the original file's parent folder. This + * avoids the legacy raw-string-on-ZIP corruption path. + * + * @param File $node The original Office file (for parent folder). + * @param array $replacements Entity-text => placeholder replacement map. + * @param string $outputName Name for the output file. + * @param string $sanitizedSourcePath Path to the sanitised ZIP container. + * + * @throws Exception If replacement fails. + * + * @phpstan-param array $replacements + * @psalm-param array $replacements + * + * @return File The anonymised document. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function replaceWordsInOfficeContainer( + File $node, + array $replacements, + string $outputName, + string $sanitizedSourcePath + ): File { + $zip = new ZipArchive(); + if ($zip->open($sanitizedSourcePath) !== true) { + throw new Exception('Failed to open sanitised Office container'); + } + + // ODT text content lives in content.xml; styles/headers may also carry + // visible text. Apply replacements to the text-bearing parts. + $textParts = ['content.xml', 'styles.xml']; + foreach ($textParts as $part) { + $xml = $zip->getFromName($part); + if ($xml === false) { + continue; + } + + foreach ($replacements as $original => $replacement) { + $xml = str_ireplace((string) $original, $replacement, $xml); + } + + $zip->addFromString($part, $xml); + } + + $zip->close(); + + $parentFolder = $node->getParent(); + if ($parentFolder->nodeExists($outputName) === true) { + $parentFolder->get($outputName)->delete(); + } + + $outputStream = fopen($sanitizedSourcePath, 'r'); + if ($outputStream === false) { + throw new Exception('Failed to read sanitised Office container'); + } + + $newFile = $parentFolder->newFile(path: $outputName, content: $outputStream); + + $this->logger->info( + message: '[DocumentProcessingHandler] Office container anonymised', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'outputFile' => $newFile->getPath(), + 'replacements' => count($replacements), + ] + ); + + return $newFile; + }//end replaceWordsInOfficeContainer() + /** * Replace words in a Word document. * @@ -191,9 +901,12 @@ public function anonymizeDocument(Node $node, array $entities): File * (including headers, footers, tables, lists), apply text replacements, and save * the result as a new file in the same parent folder. * - * @param Node $node The file node to process. - * @param array $replacements Array of replacement mappings (search => replace). - * @param string $outputName Name for the output file. + * @param Node $node The file node to process. + * @param array $replacements Array of replacement mappings (search => replace). + * @param string $outputName Name for the output file. + * @param string|null $sanitizedSourcePath Optional pre-sanitised source file path; when + * provided, the walker reads these cleaned bytes + * instead of the original node content. * * @return File The new file node with replaced content. * @@ -212,27 +925,47 @@ public function anonymizeDocument(Node $node, array $entities): File private function replaceWordsInWordDocument( Node $node, array $replacements, - string $outputName + string $outputName, + ?string $sanitizedSourcePath=null ): File { - // Get the file content as a stream and save to a temp file (@var File $fileNode). - $fileNode = $node; - $stream = $fileNode->fopen('r'); $tempFile = tempnam(sys_get_temp_dir(), 'openregister_word_'); if ($tempFile === false) { throw new Exception('Failed to create temporary file'); } - $tempStream = fopen($tempFile, 'w'); - if ($tempStream === false) { - unlink($tempFile); - throw new Exception('Failed to open temporary file for writing'); - } + if ($sanitizedSourcePath !== null) { + // Walker reads the sanitised derivative, not the original bytes. + if (copy($sanitizedSourcePath, $tempFile) === false) { + unlink($tempFile); + throw new Exception('Failed to copy sanitised source for processing'); + } + }//end if + + if ($sanitizedSourcePath === null) { + // Get the file content as a stream and save to a temp file. + $stream = $node->fopen('r'); + $tempStream = fopen($tempFile, 'w'); + if ($tempStream === false) { + unlink($tempFile); + throw new Exception('Failed to open temporary file for writing'); + } - stream_copy_to_stream($stream, $tempStream); - fclose($tempStream); - fclose($stream); + stream_copy_to_stream($stream, $tempStream); + fclose($tempStream); + fclose($stream); + }//end if try { + // Snapshot the process-static PhpWord style names BEFORE loading + // this document. PhpWord\Style is a process-static collection that + // the Word2007 reader APPENDS to on every IOFactory::load() (it is + // never reset), so in a long-lived PHP-FPM worker it accumulates + // styles from previously-processed documents. We diff against this + // snapshot below so the Numbering workaround only touches THIS + // document's styles and never mutates a prior document's — which + // would otherwise be a cross-request state bleed. + $preLoadStyleNames = array_keys(\PhpOffice\PhpWord\Style::getStyles()); + // Load the document. $phpWord = IOFactory::load($tempFile); @@ -291,6 +1024,62 @@ private function replaceWordsInWordDocument( } } + // PhpWord roundtrip-safety workaround for the Word2007 Numbering + // bug. Chain of events upstream: + // 1. Shared\XMLReader::getAttribute() at line 187 normalises + // every empty-string attribute value to null: + // return ($return == '') ? null : $return; + // 2. Reader\Word2007\Numbering at line 53 stores that null + // verbatim into $abstract['type'] (the readLevel() helper + // filters nulls; the abstract-level reader does not). + // 3. Style::addNumberingStyle dispatches the array through + // AbstractStyle::setStyleByArray, which calls setType($value) + // on each entry. + // 4. Style\Numbering::setType has a strict `string` typehint + // since PhpWord 1.x; null → TypeError. + // 5. Writer\Word2007\Part\Numbering lines 68–70 unconditionally + // emits via writeAttribute('w:val', + // $style->getType()). When getType() is null PHP coerces + // to "", so the writer poisons its own output: + // . Re-loading that file then + // triggers (1)–(4) and the read crashes. + // + // The fix here works at the OpenRegister boundary: before + // calling the writer we walk THIS document's Numbering styles + // (scoped via the pre-load snapshot so we never mutate a prior + // document's styles still resident in the static collection) and + // ensure each $type is one of the valid enum values + // ('singleLevel'|'multilevel'|'hybridMultilevel'). We only coerce + // the null/'' case (the TypeError trigger); a legitimately-typed + // 'multilevel' style — e.g. from a LibreOffice-native list — is + // left untouched so its rendering semantics are preserved. Word + // emits hybridMultilevel for almost every list it produces, so + // that's the safest default for the unspecified case — Word + // readers (including PhpWord's own re-read) treat the result + // identically to an unspecified type. Upstream fix tracked + // separately: the writer should gate the emit + // the same way it already gates other properties (lines 116–124 + // of Writer/Word2007/Part/Numbering.php). + $thisDocStyleNames = array_diff( + array_keys(\PhpOffice\PhpWord\Style::getStyles()), + $preLoadStyleNames + ); + foreach (\PhpOffice\PhpWord\Style::getStyles() as $styleName => $style) { + if (in_array($styleName, $thisDocStyleNames, true) === false) { + // Belongs to a previously-processed document — leave it alone. + continue; + } + + if ($style instanceof \PhpOffice\PhpWord\Style\Numbering === false) { + continue; + } + + $currentType = $style->getType(); + if ($currentType === null || $currentType === '') { + $style->setType('hybridMultilevel'); + } + } + // Save the modified document to a new temp file. $outputTempFile = tempnam(sys_get_temp_dir(), 'openregister_word_output_'); IOFactory::createWriter($phpWord, 'Word2007')->save($outputTempFile); @@ -345,7 +1134,7 @@ private function replaceWordsInWordDocument( * and saves the result as a new file in the same parent folder. This works * for any text-based file format (.txt, .md, .html, etc.). * - * @param Node $node The file node to process. + * @param File $node The file node to process. * @param array $replacements Array of replacement mappings (search => replace). * @param string $outputName Name for the output file. * @@ -359,16 +1148,13 @@ private function replaceWordsInWordDocument( * @psalm-return File */ private function replaceWordsInTextDocument( - Node $node, + File $node, array $replacements, string $outputName ): File { - // Get file content (@var File $fileNode). - $fileNode = $node; - $content = $fileNode->getContent(); - if ($content === false) { - throw new Exception('Failed to get content from file: '.$node->getPath()); - } + // File::getContent() returns the content string and throws on failure + // (NotPermitted/Locked) — it never returns false, so no false-check. + $content = $node->getContent(); // Apply replacements. $modifiedContent = $content; @@ -397,4 +1183,203 @@ private function replaceWordsInTextDocument( return $newFile; }//end replaceWordsInTextDocument() + + /** + * Replace words in a PDF document via the SAPP byte-level pipeline. + * + * Routes to {@see PdfTextReplacer} (text replacement with font switch + * + Helvetica fallback) and {@see PdfMetadataSanitizer} (/Info and + * XMP stripping). The output is re-extracted post-replacement; residual + * substitution-map keys are always logged as a PII-free warning, and the + * `$strict` flag then decides the outcome: lenient (default, ad-hoc + * replace) writes the partial PDF for docx parity, strict (entity + * anonymisation) fails closed with `REASON_VALIDATION_FAILED`. + * + * Pre-dispatch: encrypted PDFs are rejected with `REASON_ENCRYPTED_PDF` + * (caller-correctable, HTTP 422); if smalot/pdfparser cannot extract any + * text from the input, the call defers to the `ocr-document-scanning` + * capability via `REASON_TEXT_LAYER_MISSING` (caller's responsibility to route). + * + * @param File $node The PDF file node to process. + * @param array $replacements Map: entity-text => placeholder. + * @param string $outputName Output file name. + * @param bool $strict When true (entity anonymisation), residual + * entity text fails closed instead of being + * written as a partial result. + * + * @throws Exception If node content is unreadable. + * @throws PdfAnonymisationException On encrypted PDF, missing text + * layer, validation gate failure (strict), + * or internal pipeline errors. + * + * @phpstan-param array $replacements + * @psalm-param array $replacements + * + * @return File The anonymised PDF. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) $strict selects fail-closed vs lenient validation. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Linear probe → replace → sanitise → write pipeline; splitting obscures the flow. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each guard maps a distinct SAPP failure mode to a typed reason. + * @SuppressWarnings(PHPMD.NPathComplexity) Same — sequential fail-closed guards, not nested branching. + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + private function replaceWordsInPdfDocument( + File $node, + array $replacements, + string $outputName, + bool $strict=false + ): File { + // File::getContent() returns the content string and throws on failure + // (NotPermitted/Locked) — it never returns false. + $content = $node->getContent(); + + // Encryption probe — encrypted PDFs carry an `/Encrypt N G R` indirect + // reference in the trailer dictionary. SAPP cannot byte-replace, and + // smalot cannot extract, an encrypted PDF without the password, which + // is out of scope for v1. Detect it up front and raise the caller- + // correctable REASON_ENCRYPTED_PDF (→ HTTP 422) rather than letting it + // surface downstream as a generic 500. The `\s+\d+\s+\d+\s+R` form + // matches the trailer's indirect reference and avoids false positives + // from the literal string appearing inside a content stream. + if (preg_match('/\/Encrypt\s+\d+\s+\d+\s+R/', $content) === 1) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_ENCRYPTED_PDF, + message: 'PDF is encrypted; decryption is out of scope (v1)', + diagnostic: ['stage' => 'input.encryption_probe'] + ); + } + + // Pre-dispatch text-layer probe — image-only scans must defer + // to the `ocr-document-scanning` capability rather than producing + // an empty no-op output via the byte-replace path. + try { + $parser = new PdfParser(); + $parsedPdf = $parser->parseContent($content); + $extracted = $parsedPdf->getText(); + } catch (\Throwable $e) { + // The smalot parser rejects encrypted/secured PDFs with a message + // rather than a typed exception; map that to the caller-correctable 422. + // Everything else is a genuine input-parse failure → 500. + if (preg_match('/secured|encrypt/i', $e->getMessage()) === 1) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_ENCRYPTED_PDF, + message: 'PDF is encrypted; decryption is out of scope (v1)', + diagnostic: ['stage' => 'input.encryption_probe'], + previous: $e + ); + } + + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'smalot/pdfparser failed on input PDF', + diagnostic: ['stage' => 'input.text_layer_probe'], + previous: $e + ); + }//end try + + if (trim($extracted) === '') { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_TEXT_LAYER_MISSING, + message: 'PDF has no extractable text layer; defer to OCR', + diagnostic: ['stage' => 'input.text_layer_probe'] + ); + } + + $replacer = new PdfTextReplacer(logger: $this->logger); + $sanitizer = new PdfMetadataSanitizer(logger: $this->logger); + + // Run text replacement first; metadata sanitisation operates on + // the result so /Info / XMP changes survive the rebuild. + // Best-effort: replaceInPdf no longer fails closed on residual entity + // text — it returns the residual needles so we can produce the file + // and warn instead of discarding it. + $residualNeedles = []; + $replacedBytes = $replacer->replaceInPdf( + pdfBytes: $content, + substitutions: $replacements, + strict: $strict, + residualEntities: $residualNeedles + ); + + // Map residual needles back to entity records {text, type, id} via the + // placeholder map (`[: ]`). Logs stay PII-free (ADR-005); the + // text is carried only to the authenticated anonymise response for the + // operator's review/iterate UI. + if (empty($residualNeedles) === false) { + $records = []; + foreach ($residualNeedles as $needle) { + $placeholder = $replacements[$needle] ?? ''; + $type = 'UNKNOWN'; + $id = ''; + if (preg_match('/^\[([^:\]]+):\s*([^\]]*)\]$/', $placeholder, $m) === 1) { + $type = trim($m[1]); + $id = trim($m[2]); + } + + $records[] = ['text' => (string) $needle, 'type' => $type, 'id' => $id]; + } + + $this->lastResidualEntities = $records; + } + + try { + // Fail CLOSED: if SAPP cannot reparse the replaced bytes (returns + // false rather than throwing) we must NOT fall through and write + // the un-sanitised `$replacedBytes` — that still carries the + // original `/Info` dict + XMP stream (document PII), defeating the + // sanitiser. Treat a false/empty result as a hard pipeline error. + $doc = \ddn\sapp\PDFDoc::from_string(buffer: $replacedBytes); + if ($doc === false) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'SAPP failed to reparse replaced PDF for metadata sanitisation', + diagnostic: ['stage' => 'sanitize.reparse'] + ); + } + + $sanitizer->sanitize(doc: $doc); + $serialised = $doc->to_pdf_file_s(rebuild: true); + if ($serialised === false || $serialised === '') { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'SAPP serialise after metadata sanitisation returned empty', + diagnostic: ['stage' => 'sanitize.serialise'] + ); + } + + $replacedBytes = $serialised; + } catch (PdfAnonymisationException $e) { + // Sanitiser-raised (or the fail-closed guards above) — surface to the caller. + throw $e; + } catch (\Throwable $e) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'Metadata sanitisation stage failed', + diagnostic: ['stage' => 'sanitize'], + previous: $e + ); + }//end try + + // Create output file. + $parentFolder = $node->getParent(); + if ($parentFolder->nodeExists($outputName) === true) { + $parentFolder->get($outputName)->delete(); + } + + $newFile = $parentFolder->newFile(path: $outputName, content: $replacedBytes); + + $this->logger->info( + message: '[DocumentProcessingHandler] PDF anonymised via SAPP', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'originalFile' => $node->getPath(), + 'outputFile' => $newFile->getPath(), + 'replacements' => count($replacements), + ] + ); + + return $newFile; + }//end replaceWordsInPdfDocument() }//end class diff --git a/lib/Service/File/FileAuditHandler.php b/lib/Service/File/FileAuditHandler.php index b106ca1152..41bbf76324 100644 --- a/lib/Service/File/FileAuditHandler.php +++ b/lib/Service/File/FileAuditHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); diff --git a/lib/Service/File/FileBatchHandler.php b/lib/Service/File/FileBatchHandler.php index ac5a04ac11..8f65bc2f5c 100644 --- a/lib/Service/File/FileBatchHandler.php +++ b/lib/Service/File/FileBatchHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -102,7 +102,7 @@ public function setFileService(FileService $fileService): void * * @throws Exception If validation fails. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function executeBatch( ObjectEntity $object, diff --git a/lib/Service/File/FileCrudHandler.php b/lib/Service/File/FileCrudHandler.php index c19beabce8..f638a5bd47 100644 --- a/lib/Service/File/FileCrudHandler.php +++ b/lib/Service/File/FileCrudHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); diff --git a/lib/Service/File/FileFormattingHandler.php b/lib/Service/File/FileFormattingHandler.php index 63612edb64..2bf60380be 100644 --- a/lib/Service/File/FileFormattingHandler.php +++ b/lib/Service/File/FileFormattingHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -121,7 +121,7 @@ public function setFileService(FileService $fileService): void * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-007 + * @spec openspec/specs/file-actions/spec.md */ public function formatFile(Node $file): array { @@ -292,7 +292,7 @@ public function formatFile(Node $file): array * @SuppressWarnings(PHPMD.CyclomaticComplexity) File formatting with pagination requires multiple branches * @SuppressWarnings(PHPMD.NPathComplexity) Multiple filter and pagination paths * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-007 + * @spec openspec/specs/file-actions/spec.md */ public function formatFiles(array $files, ?array $requestParams=[]): array { diff --git a/lib/Service/File/FileLockHandler.php b/lib/Service/File/FileLockHandler.php index 257525544b..e77fc95d60 100644 --- a/lib/Service/File/FileLockHandler.php +++ b/lib/Service/File/FileLockHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -120,7 +120,7 @@ public function __construct( * * @throws Exception If the file is already locked by another user. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-2 + * @spec openspec/specs/file-actions/spec.md */ public function lockFile(int $fileId, ?int $ttlMinutes=null): array { @@ -156,7 +156,7 @@ public function lockFile(int $fileId, ?int $ttlMinutes=null): array * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-2 + * @spec openspec/specs/file-actions/spec.md */ public function unlockFile(int $fileId, bool $force=false): array { @@ -214,7 +214,7 @@ public function isLocked(int $fileId): bool * * @return array|null Lock metadata or null. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-007 + * @spec openspec/specs/file-actions/spec.md */ public function getLockInfo(int $fileId): ?array { @@ -261,7 +261,7 @@ public function getLockInfo(int $fileId): ?array * * @throws Exception If the file is locked by another user. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-008 + * @spec openspec/specs/file-actions/spec.md */ public function assertCanModify(int $fileId): void { diff --git a/lib/Service/File/FileOwnershipHandler.php b/lib/Service/File/FileOwnershipHandler.php index e32c47959e..e28b935854 100644 --- a/lib/Service/File/FileOwnershipHandler.php +++ b/lib/Service/File/FileOwnershipHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -87,7 +87,7 @@ public function __construct( * @psalm-return IUser * @phpstan-return IUser * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function getUser(): IUser { @@ -163,11 +163,21 @@ public function getCurrentUser(): ?IUser * @psalm-return void * @phpstan-return void * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-006 + * @spec openspec/specs/file-actions/spec.md */ public function transferFileOwnershipIfNeeded(File $file, ?FileSharingHandler $fileSharingHandler=null): void { try { + // If the current user already has write rights on the file, leave ownership + // as-is. Files uploaded into the OpenRegister folder are already owned by the + // openregister system user (the folder's mount owner), and files linked from + // folders outside the OpenRegister folder must keep their original owner — in + // neither case do we re-own. Re-owning remains only as a fallback for when the + // session cannot otherwise write the file. + if ($file->isUpdateable() === true) { + return; + } + // Get current user. $currentUser = $this->getCurrentUser(); if ($currentUser === null) { @@ -197,8 +207,9 @@ public function transferFileOwnershipIfNeeded(File $file, ?FileSharingHandler $f $fileOwnerId = $fileOwner->getUID(); - // Check if current user is the owner and is not OpenRegister. - if ($fileOwnerId === $currentUserId && $currentUserId !== $openRegisterUserId) { + // Re-own only when the current user owns the node. They are already known + // not to be the OpenRegister user (the equality case returned above). + if ($fileOwnerId === $currentUserId) { $fileName = $file->getName(); $msg = '[FileOwnershipHandler] Transferring file '.$fileName.' from '.$currentUserId; $this->logger->info( @@ -250,7 +261,7 @@ public function transferFileOwnershipIfNeeded(File $file, ?FileSharingHandler $f * @psalm-return void * @phpstan-return void * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function transferFolderOwnershipIfNeeded(Node $folder, ?FileSharingHandler $fileSharingHandler=null): void { diff --git a/lib/Service/File/FilePreviewHandler.php b/lib/Service/File/FilePreviewHandler.php index 9c74e79b0e..2ebcffe69a 100644 --- a/lib/Service/File/FilePreviewHandler.php +++ b/lib/Service/File/FilePreviewHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -82,7 +82,7 @@ public function __construct( * * @throws Exception If preview cannot be generated. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-3 + * @spec openspec/specs/file-actions/spec.md */ public function getPreview(File $file, ?int $width=null, ?int $height=null): ISimpleFile { diff --git a/lib/Service/File/FilePublishingHandler.php b/lib/Service/File/FilePublishingHandler.php index ab7c20c1d0..bd738cfedb 100644 --- a/lib/Service/File/FilePublishingHandler.php +++ b/lib/Service/File/FilePublishingHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -103,7 +103,7 @@ public function setFileService(FileService $fileService): void * @SuppressWarnings(PHPMD.NPathComplexity) Multiple file resolution paths with fallback logic * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive file lookup and sharing requires extensive code * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function publishFile(ObjectEntity | string $object, string | int $file): File { @@ -307,7 +307,7 @@ function ($file) { * @SuppressWarnings(PHPMD.NPathComplexity) Multiple file resolution paths with fallback logic * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive file lookup and unsharing requires extensive code * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function unpublishFile(ObjectEntity | string $object, string|int $filePath): File { @@ -509,7 +509,7 @@ function ($file) { * @SuppressWarnings(PHPMD.NPathComplexity) Multiple paths for file processing and error handling * @SuppressWarnings(PHPMD.ExcessiveMethodLength) ZIP archive creation with file processing requires extensive code * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function createObjectFilesZip(ObjectEntity | string $object, ?string $zipName=null): array { diff --git a/lib/Service/File/FileSharingHandler.php b/lib/Service/File/FileSharingHandler.php index c25807ea24..a8b253403e 100644 --- a/lib/Service/File/FileSharingHandler.php +++ b/lib/Service/File/FileSharingHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -100,7 +100,7 @@ public function getShareLink(IShare $share): string * @psalm-return array * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function findShares(Node $file, int $shareType=3): array { @@ -133,7 +133,7 @@ public function findShares(Node $file, int $shareType=3): array * @psalm-return IShare * @phpstan-return IShare * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function createShare(array $shareData): IShare { @@ -203,7 +203,7 @@ public function createShare(array $shareData): IShare * @psalm-return void * @phpstan-return void * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function shareFileWithUser(File $file, string $userId, int $permissions=31): void { @@ -258,7 +258,7 @@ public function shareFileWithUser(File $file, string $userId, int $permissions=3 * @psalm-return IShare|null * @phpstan-return IShare|null * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-009 + * @spec openspec/specs/file-actions/spec.md */ public function shareFolderWithUser(Node $folder, string $userId, int $permissions=31): ?IShare { diff --git a/lib/Service/File/FileValidationHandler.php b/lib/Service/File/FileValidationHandler.php index 0ad596f3c0..7e76923075 100644 --- a/lib/Service/File/FileValidationHandler.php +++ b/lib/Service/File/FileValidationHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -79,7 +79,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive list of dangerous extensions requires extensive code * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-010 + * @spec openspec/specs/file-actions/spec.md */ public function blockExecutableFile(string $fileName, string $fileContent): void { @@ -192,7 +192,7 @@ public function blockExecutableFile(string $fileName, string $fileContent): void * @psalm-return void * @phpstan-return void * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-010 + * @spec openspec/specs/file-actions/spec.md */ public function detectExecutableMagicBytes(string $content, string $fileName): void { @@ -242,38 +242,34 @@ public function detectExecutableMagicBytes(string $content, string $fileName): v }//end detectExecutableMagicBytes() /** - * Check file ownership and repair the OpenRegister owner record when it drifted. + * Assert that the current session may access the given file. * - * Probes read access via `Node::isReadable()` — a pure permission-bitmask - * check against `oc_filecache`. It does NOT read file contents and does NOT - * acquire a Nextcloud shared lock, so this probe is safe to run in a hot - * listing loop against arbitrarily large or actively-locked files. See - * `openspec/changes/fix-object-files-listing-lock-and-limit/design.md` - * Decision 1 for the rationale (the prior implementation used - * `File::getContent()` which forced O(file-size) reads and triggered - * `LockedException` on every NC-locked file). + * Access is readability-based and ownership-agnostic. An OpenRegister object + * may link a file owned by the `openregister` system user, by the uploading + * user, or by any other user, and the current session may reach it either + * through direct ownership OR through a file share. `Node::isReadable()` + * reflects exactly that surface: a pure permission-bitmask check against + * `oc_filecache` for the current user's view. It does NOT read file contents + * and does NOT acquire a Nextcloud shared lock, so this probe is safe to run + * in a hot listing loop against arbitrarily large or actively-locked files. + * See `openspec/changes/fix-object-files-listing-lock-and-limit/design.md` + * Decision 1 (the prior implementation used `File::getContent()` which forced + * O(file-size) reads and triggered `LockedException` on every NC-locked file). * - * Behaviour: - * - If the current session can read the file and the owner record has - * drifted, `ownFile()` is called to repair the DB record (best effort — - * any failure is logged at warning level but does not propagate). - * - If the current session cannot read the file at all, a - * `NotPermittedException` is thrown. Ownership is intentionally NOT - * repaired in this branch: repair is only a valid action when the - * session can already observe the file through the user's permission - * surface. + * Comparing the file owner against the session user is intentionally NOT done: + * it rejects files reachable via a share, which broke linking and viewing of + * files owned by users other than the session user. Write and delete paths + * additionally assert `isUpdateable()`/`isDeletable()` before mutating, and the + * underlying Nextcloud node operations enforce permissions natively, so the + * readability gate here does not over-grant. * - * @param Node $file The file node to check ownership for. + * @param Node $file The file node to check access for. * * @return void * * @throws NotPermittedException When the file is not readable by the current session. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) The method fans out across - * readability, owner-drift detection, and best-effort repair with a nested - * try/catch; splitting further would obscure the ownership-repair intent. - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-010 + * @spec openspec/specs/file-actions/spec.md */ public function checkOwnership(Node $file): void { @@ -288,28 +284,6 @@ public function checkOwnership(Node $file): void throw new NotPermittedException("File {$fileName} is not readable by the current session"); } - - try { - $fileOwner = $file->getOwner(); - $openRegisterUser = $this->getUser(); - - if ($fileOwner === null || $fileOwner->getUID() !== $openRegisterUser->getUID()) { - $this->logger->info( - message: "[FileValidationHandler] checkOwnership: File {$fileName} (ID: {$fileId}) has drifted owner, repairing", - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - $this->ownFile(file: $file); - } - } catch (Exception $ownershipException) { - // Repair is best-effort: a readable file with an unrecoverable owner - // record should not fail the caller. The drift will be re-evaluated - // on the next call. - $this->logger->warning( - message: "[FileValidationHandler] checkOwnership: Could not repair ownership for {$fileName}: ".$ownershipException->getMessage(), - context: ['file' => __FILE__, 'line' => __LINE__] - ); - }//end try }//end checkOwnership() /** @@ -327,7 +301,7 @@ public function checkOwnership(Node $file): void * @psalm-return bool * @phpstan-return bool * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-010 + * @spec openspec/specs/file-actions/spec.md */ public function ownFile(Node $file): bool { diff --git a/lib/Service/File/FileVersioningHandler.php b/lib/Service/File/FileVersioningHandler.php index b8d6b5fc0d..f9e7f453fe 100644 --- a/lib/Service/File/FileVersioningHandler.php +++ b/lib/Service/File/FileVersioningHandler.php @@ -8,14 +8,14 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-11 + * @spec openspec/specs/content-versioning/spec.md */ declare(strict_types=1); @@ -55,7 +55,7 @@ class FileVersioningHandler * @param IUserSession $userSession User session for current user context. * @param LoggerInterface $logger Logger for logging operations. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-11 + * @spec openspec/specs/content-versioning/spec.md */ public function __construct( private readonly IRootFolder $rootFolder, @@ -70,7 +70,7 @@ public function __construct( * * @return bool True if files_versions is enabled. * - * @spec openspec/changes/retrofit-2026-04-28-content-versioning/tasks.md#task-1 + * @spec openspec/specs/content-versioning/spec.md */ public function isVersioningEnabled(): bool { @@ -87,7 +87,7 @@ public function isVersioningEnabled(): bool * * @return array{versions: array, warning?: string} Version listing. * - * @spec openspec/changes/retrofit-2026-04-28-content-versioning/tasks.md#task-1 + * @spec openspec/specs/content-versioning/spec.md * * @SuppressWarnings(PHPMD.StaticAccess) */ @@ -166,7 +166,7 @@ public function listVersions(File $file): array * * @throws Exception If versioning is not enabled or version not found. * - * @spec openspec/changes/retrofit-2026-04-28-content-versioning/tasks.md#task-1 + * @spec openspec/specs/content-versioning/spec.md * * @SuppressWarnings(PHPMD.StaticAccess) */ @@ -216,7 +216,7 @@ public function restoreVersion(File $file, string $versionId): bool * * @return string The current user ID or 'system'. * - * @spec openspec/changes/retrofit-2026-04-28-content-versioning/tasks.md#task-1 + * @spec openspec/specs/content-versioning/spec.md */ private function getCurrentUserId(): string { diff --git a/lib/Service/File/FolderManagementHandler.php b/lib/Service/File/FolderManagementHandler.php index e252797ea5..a8a5adbe6b 100644 --- a/lib/Service/File/FolderManagementHandler.php +++ b/lib/Service/File/FolderManagementHandler.php @@ -8,23 +8,27 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0 - * @link https://github.com/ConductionNL/openregister + * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0 + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); namespace OCA\OpenRegister\Service\File; +use DateTime; use Exception; +use OCA\OpenRegister\Db\AuditTrail; +use OCA\OpenRegister\Db\AuditTrailMapper; use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\MagicMapper; use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Exception\FolderAccessDeniedException; use OCA\OpenRegister\Service\FileService; use OCP\Files\Folder; use OCP\Files\IRootFolder; @@ -35,6 +39,7 @@ use OCP\IUser; use OCP\IUserSession; use Psr\Log\LoggerInterface; +use Symfony\Component\Uid\Uuid; /** * Handles folder management operations for files. @@ -59,6 +64,7 @@ * @version 1.0.0 * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.ExcessiveClassLength) Lifecycle + access-control by design. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FolderManagementHandler @@ -86,6 +92,7 @@ class FolderManagementHandler * @param IUserSession $userSession User session for user context. * @param IGroupManager $groupManager Group manager for group operations. * @param LoggerInterface $logger Logger for logging operations. + * @param AuditTrailMapper $auditTrailMapper Mapper for writing forensic audit-trail entries on folder-access denials. * @param FileService|null $fileService File service facade for cross-handler coordination * (injected lazily to avoid circular dependency). */ @@ -96,6 +103,7 @@ public function __construct( private readonly IUserSession $userSession, private readonly IGroupManager $groupManager, private readonly LoggerInterface $logger, + private readonly AuditTrailMapper $auditTrailMapper, private ?FileService $fileService=null ) { }//end __construct() @@ -131,7 +139,7 @@ public function setFileService(FileService $fileService): void * @psalm-return Node|null * @phpstan-return Node|null * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/specs/file-actions/spec.md#REQ-004 + * @spec openspec/specs/file-actions/spec.md */ public function createEntityFolder(Register | ObjectEntity $entity): ?Node { @@ -144,6 +152,10 @@ public function createEntityFolder(Register | ObjectEntity $entity): ?Node } return $this->createObjectFolderById(objectEntity: $entity, currentUser: $currentUser); + } catch (FolderAccessDeniedException $e) { + // Access denials must propagate to the controller so it can return HTTP 403 + // with the structured body. Do NOT downgrade to a logged null-return. + throw $e; } catch (Exception $e) { $this->logger->error( message: '[FolderManagementHandler] Failed to create folder for entity: {message}', @@ -155,7 +167,7 @@ public function createEntityFolder(Register | ObjectEntity $entity): ?Node ] ); return null; - } + }//end try }//end createEntityFolder() /** @@ -173,7 +185,7 @@ public function createEntityFolder(Register | ObjectEntity $entity): ?Node * * @psalm-return Node * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-4 + * @spec openspec/specs/file-actions/spec.md */ public function createRegisterFolderById(Register $register, ?IUser $currentUser=null): Node { @@ -236,7 +248,7 @@ public function createRegisterFolderById(Register $register, ?IUser $currentUser * * @return Folder The created or existing folder for the object. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/specs/file-actions/spec.md#REQ-004 + * @spec openspec/specs/file-actions/spec.md */ public function createObjectFolderById( ObjectEntity|string $objectEntity, @@ -248,7 +260,30 @@ public function createObjectFolderById( $folderProperty = $objectEntity->getFolder(); } - // Try to get existing folder by ID. + // Three branches per `self-folder-access-control` spec: + // (a) empty → fall through to auto-create (unchanged legacy behaviour). + // (b) non-digit-string → legacy path-style value, fall through to auto-create. + // (c) bare-integer node ID → caller-supplied node ID; require a positive read-access + // check. On failure, propagate FolderAccessDeniedException — + // do NOT fall through to auto-create (would silently mask + // a cross-tenant bind attempt). + // Gate on ctype_digit (not is_numeric) so "42.5"/"4e2"/"+42" don't slip + // through and get (int)-truncated to a different node than was validated. + if ($folderProperty !== null && $folderProperty !== '' && ctype_digit((string) $folderProperty) === true) { + $existingFolder = $this->assertFolderIsAccessible( + folderId: $folderProperty, + currentUser: $currentUser, + objectEntity: $objectEntity + ); + $this->logger->info( + message: "[FolderManagementHandler] Object folder bind verified (ID: ".$folderProperty.")", + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return $existingFolder; + } + + // Empty / legacy non-numeric → unchanged auto-create flow below. `getExistingFolderFromProperty` + // returns null for both, so the rest of the method handles them as before. $existingFolder = $this->getExistingFolderFromProperty(folderProperty: $folderProperty); if ($existingFolder !== null) { $this->logger->info( @@ -305,7 +340,7 @@ public function createObjectFolderById( * @psalm-return Folder|null * @phpstan-return Folder|null * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-4 + * @spec openspec/specs/file-actions/spec.md */ public function getRegisterFolderById(Register $register): ?Folder { @@ -363,7 +398,7 @@ public function getRegisterFolderById(Register $register): ?Folder * @psalm-return Folder|null * @phpstan-return Folder|null * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/specs/file-actions/spec.md#REQ-004 + * @spec openspec/specs/file-actions/spec.md */ public function getObjectFolder(ObjectEntity|string $objectEntity, int|string|null $registerId=null): ?Folder { @@ -425,7 +460,7 @@ public function getObjectFolder(ObjectEntity|string $objectEntity, int|string|nu * @phpstan-return int * @return int The folder ID. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/specs/file-actions/spec.md#REQ-004 + * @spec openspec/specs/file-actions/spec.md */ public function createObjectFolderWithoutUpdate(ObjectEntity $objectEntity, ?IUser $currentUser=null): int { @@ -491,7 +526,7 @@ public function createObjectFolderWithoutUpdate(ObjectEntity $objectEntity, ?IUs * @return Node The folder node. * @throws Exception If folder creation fails. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-4 + * @spec openspec/specs/file-actions/spec.md */ public function createFolderPath(string $folderPath): Node { @@ -563,7 +598,7 @@ public function createFolderPath(string $folderPath): Node * @psalm-return Node * @phpstan-return Node * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/specs/file-actions/spec.md#REQ-004 + * @spec openspec/specs/file-actions/spec.md */ public function createFolder(string $folderPath): Node { @@ -582,7 +617,7 @@ public function createFolder(string $folderPath): Node * @psalm-return string|null * @phpstan-return string|null * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-4 + * @spec openspec/specs/file-actions/spec.md */ public function getRegisterFolderName(Register $register): string|null { @@ -607,7 +642,7 @@ public function getRegisterFolderName(Register $register): string|null * @psalm-return string * @phpstan-return string * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-4 + * @spec openspec/specs/file-actions/spec.md */ public function getObjectFolderName(ObjectEntity|string $objectEntity): string { @@ -637,7 +672,7 @@ public function getObjectFolderName(ObjectEntity|string $objectEntity): string * @psalm-return Folder * @phpstan-return Folder * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-4 + * @spec openspec/specs/file-actions/spec.md */ public function getOpenRegisterUserFolder(): Folder { @@ -664,7 +699,7 @@ public function getOpenRegisterUserFolder(): Folder * @psalm-return Node|null * @phpstan-return Node|null * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-4 + * @spec openspec/specs/file-actions/spec.md */ public function getNodeById(int $nodeId): ?Node { @@ -709,7 +744,7 @@ public function getNodeById(int $nodeId): ?Node * @psalm-return 'file'|'folder'|'unknown' * @phpstan-return 'file'|'folder'|'unknown' * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/specs/file-actions/spec.md#REQ-004 + * @spec openspec/specs/file-actions/spec.md */ public function getNodeTypeFromFolder(Node $node): string { @@ -811,6 +846,200 @@ private function getExistingFolderFromProperty(?string $folderProperty): ?Folder }//end try }//end getExistingFolderFromProperty() + /** + * Resolve a numeric folder ID and verify the acting user can read it. + * + * Restrictive lookup that deliberately does NOT use `$this->rootFolder` + * and does NOT call `getNodeById()` (which has a root-folder fallback for + * anonymous file reads). For folder *binding* the rootFolder fallback is + * the cross-tenant escape hatch this method exists to close — see + * `getNodeById()` for the general-purpose helper that retains the fallback. + * + * Per the `self-folder-access-control` capability spec: + * - "Self" is `$currentUser` if non-null, else `IUserSession::getUser()`. + * If neither resolves, the bind is denied. + * - The lookup MUST go through the user's user-folder mount only. + * - The resolved node MUST be a `Folder` instance. + * - The folder MUST report `isReadable() === true`. + * + * Every failure path emits a `folder_access_denied` audit-trail entry + * (best-effort — audit failure is logged but never swallows the denial) + * and throws `FolderAccessDeniedException`. This is the default-deny + * invariant: any condition that prevents a positive read-access + * confirmation results in denial. + * + * @param string $folderId The numeric folder ID supplied by the caller. + * @param IUser|null $currentUser Explicit acting user; falls back to session user. + * @param ObjectEntity|string|null $objectEntity Object the bind would attach to (used for audit context). + * + * @return Folder The accessible folder, ready to be used as the existing object folder. + * + * @throws FolderAccessDeniedException If the folder cannot be resolved, is not a Folder, or is not readable. + * + * @internal The method is `final` to prevent subclasses from overriding + * (and thereby weakening) the default-deny security invariant. Direct + * callers from the OpenRegister codebase MUST invoke this method as-is; + * if a different access-check policy is needed, introduce a new method + * rather than overriding this one. + */ + final public function assertFolderIsAccessible( + string $folderId, + ?IUser $currentUser=null, + ObjectEntity|string|null $objectEntity=null + ): Folder { + $actingUser = $currentUser ?? $this->getCurrentUser(); + if ($actingUser === null) { + $this->logFolderAccessDenied( + folderId: $folderId, + currentUser: null, + objectEntity: $objectEntity, + reason: 'no acting user (default-deny when no IUser is present)' + ); + throw new FolderAccessDeniedException(attemptedFolderId: $folderId); + } + + try { + $userFolder = $this->rootFolder->getUserFolder($actingUser->getUID()); + $nodes = $userFolder->getById((int) $folderId); + } catch (Exception $e) { + $this->logFolderAccessDenied( + folderId: $folderId, + currentUser: $actingUser, + objectEntity: $objectEntity, + reason: 'lookup error: '.$e->getMessage() + ); + throw new FolderAccessDeniedException( + attemptedFolderId: $folderId, + previous: $e + ); + } + + if (empty($nodes) === true) { + $this->logFolderAccessDenied( + folderId: $folderId, + currentUser: $actingUser, + objectEntity: $objectEntity, + reason: 'not found in user folder mount' + ); + throw new FolderAccessDeniedException(attemptedFolderId: $folderId); + } + + $node = reset($nodes); + if ($node instanceof Folder === false) { + $this->logFolderAccessDenied( + folderId: $folderId, + currentUser: $actingUser, + objectEntity: $objectEntity, + reason: 'resolved node is not a Folder (file ID supplied?)' + ); + throw new FolderAccessDeniedException(attemptedFolderId: $folderId); + } + + if ($node->isReadable() === false) { + $this->logFolderAccessDenied( + folderId: $folderId, + currentUser: $actingUser, + objectEntity: $objectEntity, + reason: 'folder not readable for acting user' + ); + throw new FolderAccessDeniedException(attemptedFolderId: $folderId); + } + + return $node; + }//end assertFolderIsAccessible() + + /** + * Write a `folder_access_denied` entry to the audit trail. + * + * Best-effort: a mapper failure is logged at warning level but MUST NOT + * suppress the denial — `assertFolderIsAccessible()` always throws + * `FolderAccessDeniedException` after this returns. Per the spec, denial + * is authoritative; audit is forensic. + * + * Called immediately before each throw site in `assertFolderIsAccessible()` + * so even a caller that catches the exception still has the forensic + * record of the denial. + * + * @param string $folderId The folder ID the caller attempted to bind to. + * @param IUser|null $currentUser The acting user, if present (otherwise actor is `system`). + * @param ObjectEntity|string|null $objectEntity The object entity the bind would have attached to. + * @param string $reason Human-readable reason for the denial (`not found`, `not a folder`, + * …). + * + * @return void + */ + private function logFolderAccessDenied( + string $folderId, + ?IUser $currentUser, + ObjectEntity|string|null $objectEntity, + string $reason + ): void { + $actorUid = $currentUser?->getUID() ?? 'system'; + + try { + // Use positional args throughout — Nextcloud Entity setters route through __call, + // which doesn't honour PHP named-args (the parameter name on the magic method + // doesn't match the entity field name). + $auditTrail = new AuditTrail(); + $auditTrail->setUuid(Uuid::v4()->toRfc4122()); + $auditTrail->setAction('folder_access_denied'); + $auditTrail->setUser($actorUid); + // Required NOT NULL columns on `oc_openregister_audit_trails` (per migration + // Version1Date20241020231700: `object` INT NOT NULL, `user_name` STR NOT NULL, + // `session` STR NOT NULL). The audit row is about a save that was REJECTED + // before persistence, so there is no object id to record. We use: + // - `object = 0` as the documented sentinel for "no object persisted yet". + // Auto-increment PKs on this schema start at 1, so 0 cannot collide with + // a real ObjectEntity row. Audit-trail queries filtering on + // `WHERE action = 'folder_access_denied'` are the canonical way to + // find these rows; querying by `object = 0` is also safe. + // - `user_name = $actorUid` so the audit UI has something to display in the + // "user" column when the system actor is involved. + // - `session = ''` (empty string) rather than $actorUid: the UID is already + // recorded in the `user` column; duplicating it into the `session` field + // adds no information and would mislead forensic correlation. We use + // empty string (not null) because the column is NOT NULL. + $auditTrail->setObject(0); + $auditTrail->setUserName($actorUid); + $auditTrail->setSession(''); + $auditTrail->setCreated(new DateTime()); + $auditTrail->setChanged( + [ + 'folder' => $folderId, + 'reason' => $reason, + ] + ); + + if ($objectEntity instanceof ObjectEntity === true) { + if ($objectEntity->getUuid() !== null) { + $auditTrail->setObjectUuid($objectEntity->getUuid()); + } + + if ($objectEntity->getRegister() !== null) { + $auditTrail->setRegister((int) $objectEntity->getRegister()); + } + + if ($objectEntity->getSchema() !== null) { + $auditTrail->setSchema((int) $objectEntity->getSchema()); + } + } + + $this->auditTrailMapper->insert(entity: $auditTrail); + } catch (Exception $e) { + // Audit is best-effort — log a warning and let the denial proceed. + $this->logger->warning( + message: '[FolderManagementHandler] Failed to write folder_access_denied audit entry: '.$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'folder' => $folderId, + 'actor' => $actorUid, + 'reason' => $reason, + ] + ); + }//end try + }//end logFolderAccessDenied() + /** * Share folder with current user if different from system user. * diff --git a/lib/Service/File/ManualEntityResult.php b/lib/Service/File/ManualEntityResult.php new file mode 100644 index 0000000000..b0c6898b73 --- /dev/null +++ b/lib/Service/File/ManualEntityResult.php @@ -0,0 +1,59 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/entity-relation-grondslagen/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File; + +use OCA\OpenRegister\Db\EntityRelation; +use OCA\OpenRegister\Db\GdprEntity; + +/** + * Result payload from `ManualEntityService::addManualEntity`. + */ +final class ManualEntityResult +{ + /** + * Constructor. + * + * @param GdprEntity $entity The catalogue entry (newly inserted or reused). + * @param bool $entityWasNew True when this call inserted the catalogue row; + * false when an existing one was reused. + * @param EntityRelation[] $relations Relation rows inserted by this call. Excludes rows + * that were skipped because they already existed. + * @param int $matchCount Total positions found in the file's chunks. + * @param int $matchesSkipped How many of those positions were skipped because + * a relation row already covered them. + */ + public function __construct( + public readonly GdprEntity $entity, + public readonly bool $entityWasNew, + public readonly array $relations, + public readonly int $matchCount, + public readonly int $matchesSkipped + ) { + + }//end __construct() +}//end class diff --git a/lib/Service/File/ManualEntityService.php b/lib/Service/File/ManualEntityService.php new file mode 100644 index 0000000000..2a54158d1f --- /dev/null +++ b/lib/Service/File/ManualEntityService.php @@ -0,0 +1,531 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/entity-relation-grondslagen/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File; + +use DateTime; +use OCA\OpenRegister\Db\AuditTrail; +use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Db\ChunkMapper; +use OCA\OpenRegister\Db\DetectionMethod; +use OCA\OpenRegister\Db\EntityRelation; +use OCA\OpenRegister\Db\EntityRelationMapper; +use OCA\OpenRegister\Db\GdprEntity; +use OCA\OpenRegister\Db\GdprEntityMapper; +use OCA\OpenRegister\Exception\ChunkMatcherException; +use OCA\OpenRegister\Exception\ManualEntityException; +use OCA\OpenRegister\Service\TextExtraction\EntityRecognitionHandler; +use OCP\Files\File as NcFile; +use OCP\Files\IRootFolder; +use OCP\IDBConnection; +use OCP\IUser; +use Psr\Log\LoggerInterface; +use Symfony\Component\Uid\Uuid; +use Throwable; + +/** + * Single point of truth for the file-scoped manual-entity write path. + * + * Public surface is one method: `addManualEntity`. Tests cover happy / + * reuse / idempotent / zero-match / file-not-extracted / RBAC-denied + * paths against this surface. + */ +class ManualEntityService +{ + /** + * Fallback chunk overlap (chars) used only when a file's persisted + * chunks carry no `overlap_size` (legacy rows written before the field + * existed). The effective overlap is normally read per-file from the + * chunk rows so it tracks whatever `chunk_overlap` the extractor used; + * see {@see TextExtractionService::DEFAULT_CHUNK_OVERLAP} for the + * matching default. Needles longer than the effective overlap cannot be + * reliably matched per-chunk and are rejected by `ChunkTextMatcher`. + */ + private const FALLBACK_CHUNK_OVERLAP_CHARS = 200; + + /** + * Source-type tag used by `ChunkMapper::findBySource` for file- + * scoped chunks. Mirrors the convention already in use across + * `TextExtractionService`, `FileSidebarService`, etc. + */ + private const CHUNK_SOURCE_TYPE = 'file'; + + /** + * Constructor. + * + * @param GdprEntityMapper $gdprEntityMapper Catalogue mapper. + * @param EntityRelationMapper $entityRelationMapper Relation mapper. + * @param ChunkMapper $chunkMapper Chunk reader for the target file. + * @param ChunkTextMatcher $matcher Chunk-aware string-match utility. + * @param AuditTrailMapper $auditTrailMapper Audit-trail persistence. + * @param IRootFolder $rootFolder Nextcloud root folder (for file-write access check). + * @param IDBConnection $db Connection for explicit transaction control. + * @param LoggerInterface $logger Structured log sink for write-access failures and + * forensic context on rollback paths. + */ + public function __construct( + private readonly GdprEntityMapper $gdprEntityMapper, + private readonly EntityRelationMapper $entityRelationMapper, + private readonly ChunkMapper $chunkMapper, + private readonly ChunkTextMatcher $matcher, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly IRootFolder $rootFolder, + private readonly IDBConnection $db, + private readonly LoggerInterface $logger + ) { + + }//end __construct() + + /** + * Add an operator-supplied manual entity to a file. + * + * See the spec for the full contract. High-level flow: + * + * 1. Verify the actor has write access to the file (throws on denial). + * 2. Fetch the file's chunks; if none → throw `ManualEntityException(REASON_FILE_NOT_EXTRACTED)`. + * 3. Begin transaction. + * 4. Lookup-or-create the catalogue entry for (value, type). + * 5. Run the chunk-aware matcher. + * 6. For each match: probe `existsForFileAtPosition`; skip if exists, else buffer. + * 7. Batch-insert the buffered relation rows. + * 8. Write audit-trail entries. + * 9. Commit. On any exception in steps 4-8, rollback and re-throw. + * + * @param int $fileId Nextcloud file id the manual entity applies to. + * @param string $value Operator-supplied text to add as an anonymisable entity. + * @param string $type Entity type tag (e.g. `PERSON`, `ORGANIZATION`). + * @param bool $wholeWord Wrap the needle in `\b...\b` regex boundaries when true. + * @param bool $caseSensitive Use case-sensitive matching when true. + * @param IUser $actor The acting user (already authenticated upstream). + * + * @return ManualEntityResult The persisted entity + relation rows + match counts. + * + * @throws ManualEntityException For orchestration-layer failures (file not extracted, + * regex compile failure, audit-write failure, etc.). + */ + public function addManualEntity( + int $fileId, + string $value, + string $type, + bool $wholeWord, + bool $caseSensitive, + IUser $actor + ): ManualEntityResult { + $this->assertFileWriteAccess(fileId: $fileId, actor: $actor); + + $chunks = $this->chunkMapper->findBySource( + sourceType: self::CHUNK_SOURCE_TYPE, + sourceId: $fileId + ); + if (empty($chunks) === true) { + throw new ManualEntityException( + reason: ManualEntityException::REASON_FILE_NOT_EXTRACTED, + message: sprintf('File %d has no extracted chunks; run text extraction first.', $fileId) + ); + } + + // Derive the effective overlap from the file's own chunks so the + // matcher's needle-length bound and cross-boundary invariant track + // whatever `chunk_overlap` the extractor actually used (the value is + // persisted per chunk). Fall back to the default only for legacy + // rows that predate the `overlap_size` column. + $chunkOverlap = $chunks[0]->getOverlapSize(); + if ($chunkOverlap <= 0) { + $chunkOverlap = self::FALLBACK_CHUNK_OVERLAP_CHARS; + } + + // Run the matcher BEFORE opening the transaction — a regex + // compile failure here aborts the operation without any DB + // writes to roll back. + try { + $matches = $this->matcher->match( + chunks: $chunks, + needle: $value, + wholeWord: $wholeWord, + caseSensitive: $caseSensitive, + chunkOverlap: $chunkOverlap + ); + } catch (ChunkMatcherException $e) { + // Translate to the orchestration-layer exception. Both + // `value_too_long` and `regex_compile_failure` are mapped + // to `regex_compile_failure` here for the controller's + // 400 → response shape simplicity; the controller doesn't + // need to distinguish. + throw new ManualEntityException( + reason: ManualEntityException::REASON_REGEX_COMPILE_FAILURE, + message: $e->getMessage(), + previous: $e + ); + } + + $this->db->beginTransaction(); + try { + [$entity, $entityWasNew] = $this->lookupOrCreateEntity( + value: $value, + type: $type + ); + + [$insertedRelations, $matchesSkipped] = $this->createRelationsForMatches( + fileId: $fileId, + entity: $entity, + matches: $matches + ); + + $this->writeAuditTrails( + entity: $entity, + entityWasNew: $entityWasNew, + fileId: $fileId, + value: $value, + type: $type, + insertedRelations: $insertedRelations, + matchesSkipped: $matchesSkipped, + actor: $actor + ); + + $this->db->commit(); + } catch (Throwable $error) { + $this->db->rollBack(); + if ($error instanceof ManualEntityException) { + throw $error; + } + + // Log the root cause before we wrap. The controller's + // translated-exception log only carries the reason code; without + // this log a 500 leaves no debuggable trail. + // + // ADR-005: do NOT log the exception message or trace. Both can + // surface the operator-supplied `value` (PII) — getTraceAsString() + // formats call-frame arguments (which include `$value`), and a DB + // driver may embed bound parameters into the wrapped exception's + // message. Record only PII-safe structural fields here; the audit + // trail (ADR-022 forensic exception) is the sole place `value` lives. + $this->logger->error( + '[ManualEntityService] Transactional manual-entity write failed; rolled back', + [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $fileId, + 'actor' => $actor->getUID(), + 'errorClass' => $error::class, + 'errorCode' => $error->getCode(), + 'errorLocation' => $error->getFile().':'.$error->getLine(), + ] + ); + + throw new ManualEntityException( + reason: ManualEntityException::REASON_INTERNAL_ERROR, + message: 'Transactional manual-entity write failed: '.$error->getMessage(), + previous: $error + ); + }//end try + + return new ManualEntityResult( + entity: $entity, + entityWasNew: $entityWasNew, + relations: $insertedRelations, + matchCount: count($matches), + matchesSkipped: $matchesSkipped + ); + + }//end addManualEntity() + + /** + * Verify the acting user can write the target file. + * + * Mirrors `EntityRelationsController::canWriteFile`: the file MUST + * be reachable in the actor's user-folder and `isUpdateable()` + * MUST return true. Denial is reported as a 403 by the controller. + * + * @param int $fileId Nextcloud file id. + * @param IUser $actor Acting user. + * + * @return void + * + * @throws ManualEntityException When the file does not exist for the actor or is read-only. + */ + private function assertFileWriteAccess(int $fileId, IUser $actor): void + { + try { + $userFolder = $this->rootFolder->getUserFolder($actor->getUID()); + $nodes = $userFolder->getById($fileId); + } catch (Throwable $e) { + $this->logger->warning( + '[ManualEntityService] File lookup raised exception', + [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $fileId, + 'actor' => $actor->getUID(), + 'error' => $e->getMessage(), + ] + ); + throw new ManualEntityException( + reason: ManualEntityException::REASON_INTERNAL_ERROR, + message: 'File lookup failed: '.$e->getMessage(), + previous: $e + ); + } + + if (empty($nodes) === true) { + // No node visible to the actor. Per the spec a write-blocked + // actor and a missing file are indistinguishable from the + // operator's perspective (no oracle); the FILE_NOT_EXTRACTED + // reason is the closest semantic match because either way + // there's nothing the operator can do without write access + // / re-extraction. + $this->logger->info( + '[ManualEntityService] File not visible to actor', + [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $fileId, + 'actor' => $actor->getUID(), + ] + ); + throw new ManualEntityException( + reason: ManualEntityException::REASON_FILE_NOT_EXTRACTED, + message: sprintf('File %d is not accessible to the acting user.', $fileId) + ); + }//end if + + $node = $nodes[0]; + if (($node instanceof NcFile) === false || $node->isUpdateable() === false) { + // Read-only access. REASON_FORBIDDEN is the type-checked signal + // the controller maps to HTTP 403 (vs the default 500 for + // REASON_INTERNAL_ERROR). + $isUpdateable = false; + if ($node instanceof NcFile) { + $isUpdateable = $node->isUpdateable(); + } + + $this->logger->info( + '[ManualEntityService] Write-access denied on target file', + [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $fileId, + 'actor' => $actor->getUID(), + 'isFile' => ($node instanceof NcFile), + 'isUpdateable' => $isUpdateable, + ] + ); + throw new ManualEntityException( + reason: ManualEntityException::REASON_FORBIDDEN, + message: 'write access to file required' + ); + }//end if + + }//end assertFileWriteAccess() + + /** + * Find an existing catalogue row by (value, type) or create a new one. + * + * Category is always derived server-side from `$type` via + * `EntityRecognitionHandler::getCategoryForType()` so the catalogue + * stays consistent with detector-produced rows. The + * `oc_openregister_entities.category` column is NOT NULL with no + * default — every insert path MUST set it. Operator override on + * category is intentionally not exposed in v1 of this endpoint; + * future work can add a `?string $category` parameter back if a + * concrete use case emerges. + * + * @param string $value Entity value. + * @param string $type Entity type tag. + * + * @return array{0: GdprEntity, 1: bool} Tuple of (entity, wasNewlyInserted). + */ + private function lookupOrCreateEntity(string $value, string $type): array + { + $existing = $this->gdprEntityMapper->findOneByValueAndType(value: $value, type: $type); + if ($existing !== null) { + return [$existing, false]; + } + + // Mirror the detector flow's column population + // (see `EntityRecognitionHandler::findOrCreateEntity`) — the + // `category`, `detected_at`, and `updated_at` columns are all + // NOT NULL without defaults; every insert path must set them. + $now = new DateTime(); + $entity = new GdprEntity(); + $entity->setUuid(Uuid::v4()->toRfc4122()); + $entity->setValue($value); + $entity->setType($type); + $entity->setCategory(EntityRecognitionHandler::getCategoryForType(type: $type)); + $entity->setDetectedAt($now); + $entity->setUpdatedAt($now); + + $entity = $this->gdprEntityMapper->insert($entity); + return [$entity, true]; + + }//end lookupOrCreateEntity() + + /** + * For each match position, insert a new relation row unless one + * already exists at exactly that position. + * + * @param int $fileId Target file id. + * @param GdprEntity $entity Catalogue entry the relations point to. + * @param array $matches Output of `ChunkTextMatcher::match`. + * + * @return array{0: EntityRelation[], 1: int} Tuple of (insertedRelations, matchesSkipped). + */ + private function createRelationsForMatches(int $fileId, GdprEntity $entity, array $matches): array + { + $rowsToInsert = []; + $matchesSkipped = 0; + + foreach ($matches as $match) { + $existsAlready = $this->entityRelationMapper->existsForFileAtPosition( + fileId: $fileId, + entityId: (int) $entity->getId(), + chunkId: $match['chunkId'], + positionStart: $match['positionStart'], + positionEnd: $match['positionEnd'] + ); + if ($existsAlready === true) { + $matchesSkipped++; + continue; + } + + $rowsToInsert[] = [ + 'entityId' => (int) $entity->getId(), + 'fileId' => $fileId, + 'chunkId' => $match['chunkId'], + 'positionStart' => $match['positionStart'], + 'positionEnd' => $match['positionEnd'], + 'context' => $match['context'], + 'detectionMethod' => DetectionMethod::MANUAL, + 'role' => 'anonymisable', + 'confidence' => 1.0, + 'anonymized' => false, + 'skipAnonymization' => false, + 'createdAt' => new DateTime(), + ]; + }//end foreach + + $inserted = $this->entityRelationMapper->insertBatch(rows: $rowsToInsert); + return [$inserted, $matchesSkipped]; + + }//end createRelationsForMatches() + + /** + * Write the per-operation audit-trail rows. + * + * Two action types per the spec: + * - `entity_create` — only when a NEW catalogue row was inserted. + * - `entity_relations_batch_create` — every call (records operator intent + * even when zero matches). + * + * PII rule: ADR-005 keeps the operator-supplied `value` out of HTTP + * logs and error responses. The audit trail is the explicit forensic + * exception per ADR-022 — `value` is allowed here. + * + * @param GdprEntity $entity Catalogue entry referenced by the relations. + * @param bool $entityWasNew Whether `$entity` was just inserted (vs reused). + * @param int $fileId Target file id. + * @param string $value Operator-supplied value (PII, audit-only). + * @param string $type Entity type tag. + * @param EntityRelation[] $insertedRelations Relations created by this call. + * @param int $matchesSkipped How many matches were skipped because the + * relation row already existed. + * @param IUser $actor Acting user (UID-only is what gets persisted). + * + * @return void + */ + private function writeAuditTrails( + GdprEntity $entity, + bool $entityWasNew, + int $fileId, + string $value, + string $type, + array $insertedRelations, + int $matchesSkipped, + IUser $actor + ): void { + $userId = $actor->getUID(); + $now = new DateTime(); + + if ($entityWasNew === true) { + // Category is derived server-side on the entity itself + // (`EntityRecognitionHandler::getCategoryForType()`); the + // audit row reads from the entity so the persisted value + // is recorded verbatim — operators and forensic auditors + // can see exactly which category was applied. + $entityAudit = new AuditTrail(); + $entityAudit->setUuid(Uuid::v4()->toRfc4122()); + $entityAudit->setAction('entity_create'); + $entityAudit->setUser($userId); + $entityAudit->setUserName(null); + $entityAudit->setCreated($now); + $entityAudit->setChanged( + [ + 'subjectType' => 'openregister_entities', + 'subjectId' => (int) $entity->getId(), + 'fields' => [ + 'value' => $value, + 'type' => $type, + 'category' => $entity->getCategory(), + ], + ] + ); + $this->auditTrailMapper->insert($entityAudit); + }//end if + + $relationIds = array_map( + static fn (EntityRelation $r): int => (int) $r->getId(), + $insertedRelations + ); + + $batchAudit = new AuditTrail(); + $batchAudit->setUuid(Uuid::v4()->toRfc4122()); + $batchAudit->setAction('entity_relations_batch_create'); + $batchAudit->setUser($userId); + $batchAudit->setUserName(null); + $batchAudit->setCreated($now); + $batchAudit->setChanged( + [ + 'subjectType' => 'openregister_files', + 'subjectId' => $fileId, + 'fields' => [ + 'value' => $value, + 'type' => $type, + 'fileId' => $fileId, + 'detectionMethod' => DetectionMethod::MANUAL, + 'matchCount' => (count($insertedRelations) + $matchesSkipped), + 'matchesSkipped' => $matchesSkipped, + 'relationIds' => $relationIds, + ], + ] + ); + $this->auditTrailMapper->insert($batchAudit); + + }//end writeAuditTrails() +}//end class diff --git a/lib/Service/File/OfficeDocumentSanitizer.php b/lib/Service/File/OfficeDocumentSanitizer.php new file mode 100644 index 0000000000..91adf900ed --- /dev/null +++ b/lib/Service/File/OfficeDocumentSanitizer.php @@ -0,0 +1,281 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File; + +use OCA\OpenRegister\Exception\SanitizationException; +use OCA\OpenRegister\Service\File\Sanitizer\DocxSanitizer; +use OCA\OpenRegister\Service\File\Sanitizer\OdtSanitizer; +use OCA\OpenRegister\Service\File\Sanitizer\SanitizerInterface; +use OCP\Files\File; +use OCP\Files\IRootFolder; +use OCP\ITempManager; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Dispatches Office document sanitisation to the correct per-format strategy. + * + * @category Service + * @package OCA\OpenRegister\Service\File + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ +class OfficeDocumentSanitizer +{ + + /** + * Sentinel value substituted for scrubbed metadata fields. + * + * Intentionally a tool brand (per design D5) — see class docblock. + * + * @var string + */ + public const SENTINEL = 'DocuDesk Anonymisation'; + + /** + * Registered per-format sanitiser strategies. + * + * @var SanitizerInterface[] + */ + private array $strategies; + + /** + * Constructor. + * + * Strategies default to the built-in DOCX + ODT sanitisers so the service + * is autowire-friendly; tests MAY inject a custom strategy list. + * + * @param IRootFolder $rootFolder Root folder for file resolution. + * @param ITempManager $tempManager Temp file allocator (auto-cleans at request end). + * @param LoggerInterface $logger PII-free logger. + * @param SanitizerInterface[]|null $strategies Optional strategy override (DI / tests). + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function __construct( + private readonly IRootFolder $rootFolder, + private readonly ITempManager $tempManager, + private readonly LoggerInterface $logger, + ?array $strategies=null + ) { + if ($strategies === null) { + $strategies = [ + new DocxSanitizer(sentinel: self::SENTINEL), + new OdtSanitizer(sentinel: self::SENTINEL), + ]; + } + + $this->strategies = $strategies; + }//end __construct() + + /** + * Whether any registered strategy supports the given MIME type. + * + * @param string $mimeType The file MIME type. + * + * @return bool True when at least one strategy supports the format. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function isSanitizable(string $mimeType): bool + { + foreach ($this->strategies as $strategy) { + if ($strategy->supports($mimeType) === true) { + return true; + } + } + + return false; + }//end isSanitizable() + + /** + * Sanitise the Nextcloud file identified by $fileId. + * + * Copies the file to a temp path, runs the matching strategy in-place on + * the copy, and returns the sanitised path plus the audit report. The + * original Nextcloud file is untouched. + * + * @param int $fileId The Nextcloud file ID. + * + * @throws SanitizationException On unsupported MIME, encryption, corrupt + * zip, or internal failure. + * + * @return SanitizationResult The sanitised path and audit report. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function sanitize(int $fileId): SanitizationResult + { + $nodes = $this->rootFolder->getById($fileId); + if (count($nodes) === 0) { + throw new SanitizationException( + reason: SanitizationException::REASON_UNSUPPORTED_MIME, + message: 'Sanitisation target file could not be resolved' + ); + } + + $file = $nodes[0]; + if (($file instanceof File) === false) { + throw new SanitizationException( + reason: SanitizationException::REASON_UNSUPPORTED_MIME, + message: 'Sanitisation target is not a file' + ); + } + + $mimeType = $file->getMimeType(); + + $strategy = $this->resolveStrategy(mimeType: $mimeType); + + $tempPath = $this->tempManager->getTemporaryFile($this->extensionForMime(mimeType: $mimeType)); + if ($tempPath === false) { + throw new SanitizationException( + reason: SanitizationException::REASON_INTERNAL, + message: 'Could not allocate a temporary file for sanitisation' + ); + } + + $this->copyToTemp(stream: $file->fopen('r'), tempPath: $tempPath); + + try { + $report = $strategy->sanitize($tempPath, $tempPath); + } catch (SanitizationException $e) { + throw $e; + } catch (Throwable $e) { + throw new SanitizationException( + reason: SanitizationException::REASON_INTERNAL, + message: 'Sanitisation surgery failed', + previous: $e + ); + } + + $this->logger->info( + message: '[OfficeDocumentSanitizer] Document sanitised', + context: [ + 'fileId' => $fileId, + 'mimeType' => $mimeType, + 'strategy' => $strategy::class, + 'counts' => $report->jsonSerialize(), + ] + ); + + return new SanitizationResult(path: $tempPath, report: $report); + }//end sanitize() + + /** + * Resolve the first strategy supporting the MIME type. + * + * @param string $mimeType The file MIME type. + * + * @throws SanitizationException When no strategy supports the MIME type. + * + * @return SanitizerInterface The matching strategy. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function resolveStrategy(string $mimeType): SanitizerInterface + { + foreach ($this->strategies as $strategy) { + if ($strategy->supports($mimeType) === true) { + return $strategy; + } + } + + throw new SanitizationException( + reason: SanitizationException::REASON_UNSUPPORTED_MIME, + message: sprintf('No sanitiser strategy for MIME type: %s', $mimeType) + ); + }//end resolveStrategy() + + /** + * Map a supported MIME type to a temp-file extension. + * + * @param string $mimeType The file MIME type. + * + * @return string The extension (with leading dot). + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function extensionForMime(string $mimeType): string + { + if ($mimeType === 'application/vnd.oasis.opendocument.text') { + return '.odt'; + } + + return '.docx'; + }//end extensionForMime() + + /** + * Copy a read stream into the temp path via stream copy. + * + * @param resource|false $stream The source read stream. + * @param string $tempPath The destination temp path. + * + * @throws SanitizationException When the stream or temp file cannot be opened. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function copyToTemp($stream, string $tempPath): void + { + if (is_resource($stream) === false) { + throw new SanitizationException( + reason: SanitizationException::REASON_INTERNAL, + message: 'Could not open source file stream for sanitisation' + ); + } + + $tempStream = fopen($tempPath, 'w'); + if ($tempStream === false) { + fclose($stream); + throw new SanitizationException( + reason: SanitizationException::REASON_INTERNAL, + message: 'Could not open temporary file for writing' + ); + } + + stream_copy_to_stream($stream, $tempStream); + fclose($tempStream); + fclose($stream); + }//end copyToTemp() +}//end class diff --git a/lib/Service/File/Pdf/Fallback/NcOfficeConverterInterface.php b/lib/Service/File/Pdf/Fallback/NcOfficeConverterInterface.php new file mode 100644 index 0000000000..4c4bbfd6b3 --- /dev/null +++ b/lib/Service/File/Pdf/Fallback/NcOfficeConverterInterface.php @@ -0,0 +1,96 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File\Pdf\Fallback; + +/** + * Bidirectional PDF ↔ ODT conversion contract for the Path B fallback. + * + * Implementations MAY raise `\RuntimeException` when the converter is + * unreachable or returns a non-2xx status. Path B treats any conversion + * failure as a Path-B-side failure (`validation_failed_after_fallback`). + * + * Per ADR-005 implementations MUST NOT log document content; only file + * sizes, MIME types, and structural error details. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Pdf\Fallback + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ +interface NcOfficeConverterInterface +{ + /** + * Whether the NC Office bridge is reachable + ready for conversion. + * + * Path B short-circuits when this returns false (treats as + * `validation_failed_after_fallback` without attempting a round-trip). + * + * @return bool True when NC Office is installed and reachable. + */ + public function isAvailable(): bool; + + /** + * Convert PDF bytes to ODT bytes via NC Office. + * + * @param string $pdfBytes Raw input PDF bytes. + * + * @return string Raw output ODT bytes. + * + * @throws \RuntimeException When the conversion fails for any reason. + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + public function pdfToOdt(string $pdfBytes): string; + + /** + * Convert ODT bytes to PDF bytes via NC Office. + * + * @param string $odtBytes Raw input ODT bytes. + * + * @return string Raw output PDF bytes. + * + * @throws \RuntimeException When the conversion fails for any reason. + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + public function odtToPdf(string $odtBytes): string; +}//end interface diff --git a/lib/Service/File/Pdf/Fallback/NullNcOfficeConverter.php b/lib/Service/File/Pdf/Fallback/NullNcOfficeConverter.php new file mode 100644 index 0000000000..7c9a86480f --- /dev/null +++ b/lib/Service/File/Pdf/Fallback/NullNcOfficeConverter.php @@ -0,0 +1,103 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File\Pdf\Fallback; + +use RuntimeException; + +/** + * Null implementation that always reports unavailable. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Pdf\Fallback + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ +final class NullNcOfficeConverter implements NcOfficeConverterInterface +{ + /** + * NC Office bridge is never available in the null implementation. + * + * @return bool Always false. + */ + public function isAvailable(): bool + { + return false; + }//end isAvailable() + + /** + * Always throws — the null implementation has no NC Office bridge. + * + * @param string $pdfBytes Input bytes (unused). + * + * @return string Never returns. + * + * @throws RuntimeException Always — the null implementation is unavailable. + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + public function pdfToOdt(string $pdfBytes): string + { + throw new RuntimeException( + 'NC Office bridge is not available (NullNcOfficeConverter is the default). ' + .'Enable Path B by injecting a concrete NcOfficeConverterInterface implementation ' + .'and setting pdf-anonymisation.path-b-enabled = true.' + ); + }//end pdfToOdt() + + /** + * Always throws — the null implementation has no NC Office bridge. + * + * @param string $odtBytes Input bytes (unused). + * + * @return string Never returns. + * + * @throws RuntimeException Always — the null implementation is unavailable. + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + public function odtToPdf(string $odtBytes): string + { + throw new RuntimeException( + 'NC Office bridge is not available (NullNcOfficeConverter is the default). ' + .'Enable Path B by injecting a concrete NcOfficeConverterInterface implementation ' + .'and setting pdf-anonymisation.path-b-enabled = true.' + ); + }//end odtToPdf() +}//end class diff --git a/lib/Service/File/Pdf/Fallback/PdfOdtFallbackOrchestrator.php b/lib/Service/File/Pdf/Fallback/PdfOdtFallbackOrchestrator.php new file mode 100644 index 0000000000..982f7b5e43 --- /dev/null +++ b/lib/Service/File/Pdf/Fallback/PdfOdtFallbackOrchestrator.php @@ -0,0 +1,256 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File\Pdf\Fallback; + +use OCA\OpenRegister\Exception\PdfAnonymisationException; +use OCA\OpenRegister\Service\File\Pdf\PdfTextReplacer; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Dormant orchestrator for the Path B PDF anonymisation fallback. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Pdf\Fallback + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ +class PdfOdtFallbackOrchestrator +{ + + /** + * App-config key gating the dormant feature. + * + * Default `false`. When `true` AND the NC Office bridge is available, + * Path B activates on Path A `validation_failed`. + * + * @var string + */ + public const FEATURE_FLAG_KEY = 'pdf-anonymisation.path-b-enabled'; + + /** + * App-id the feature flag lives under. + * + * @var string + */ + public const APP_ID = 'openregister'; + + /** + * Constructor. + * + * @param IAppConfig $appConfig App-config carrying the feature flag. + * @param NcOfficeConverterInterface $converter NC Office bridge (Null by default). + * @param PdfTextReplacer $pdfTextReplacer The Path A replacer (re-used for the ODT-derived PDF). + * @param LoggerInterface $logger PII-free logger. + */ + public function __construct( + private readonly IAppConfig $appConfig, + private readonly NcOfficeConverterInterface $converter, + private readonly PdfTextReplacer $pdfTextReplacer, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Whether the dormant feature is enabled AND the NC Office bridge reports + * itself reachable. + * + * The controller wiring SHOULD short-circuit on `false` and surface the + * original Path A exception unchanged — both for performance (no NC Office + * round-trip) and to make the dormant state observable in CI / staging. + * + * @return bool True only when both the flag and the bridge are ready. + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + public function isEnabled(): bool + { + $flag = $this->appConfig->getValueBool( + app: self::APP_ID, + key: self::FEATURE_FLAG_KEY, + default: false + ); + + if ($flag === false) { + return false; + } + + return $this->converter->isAvailable(); + }//end isEnabled() + + /** + * Attempt the Path B rescue for a Path A `validation_failed` exception. + * + * Re-raises the ORIGINAL exception unchanged when: + * + * - the feature flag is disabled (the default state shipping with this change), OR + * - the NC Office bridge is unavailable, OR + * - the triggering exception's reason is anything OTHER than + * {@see PdfAnonymisationException::REASON_VALIDATION_FAILED}. + * + * Performs the PDF → ODT → entity-replace → ODT → PDF round-trip when + * activated. Raises {@see PdfAnonymisationException::REASON_VALIDATION_FAILED_AFTER_FALLBACK} + * if Path B itself fails (any sub-step throws OR the re-run validation + * gate also rejects the output). + * + * @param string $pdfBytes The original input PDF bytes. + * @param array $substitutions The substitution map applied by Path A. + * @param PdfAnonymisationException $cause The Path A exception that triggered the fallback. + * + * @return string Anonymised PDF bytes from Path B. + * + * @throws PdfAnonymisationException Either re-raises $cause (dormant / + * inapplicable) or raises + * REASON_VALIDATION_FAILED_AFTER_FALLBACK. + * + * @spec openspec/specs/pdf-anonymisation-odt-fallback/spec.md + */ + public function attempt(string $pdfBytes, array $substitutions, PdfAnonymisationException $cause): string + { + // Guard: never trigger Path B on encrypted-in or text-layer-missing. + if ($cause->getReason() !== PdfAnonymisationException::REASON_VALIDATION_FAILED) { + throw $cause; + } + + // Guard: dormant + feature flag off + null bridge. + if ($this->isEnabled() === false) { + $this->logger->debug( + '[PdfOdtFallbackOrchestrator] Path B dormant; re-raising Path A validation_failed', + ['feature_flag' => self::FEATURE_FLAG_KEY] + ); + throw $cause; + } + + $this->logger->info( + '[PdfOdtFallbackOrchestrator] Path A validation_failed — attempting Path B ODT round-trip', + ['inputBytes' => strlen($pdfBytes)] + ); + + try { + $odt = $this->converter->pdfToOdt(pdfBytes: $pdfBytes); + } catch (Throwable $e) { + throw $this->wrapAfterFallback(cause: $cause, stage: 'pdf_to_odt', previous: $e); + } + + // Re-run the SAPP-side replacer against the round-tripped PDF. The + // ODT->PDF conversion happens NEXT — we deliberately do not invoke + // an ODT entity-walker here, because the Path B contract is that the + // ODT-PDF re-roundtrip MUST defeat the same encoding edge case that + // tripped Path A, so feeding the round-tripped PDF back through the + // SAPP replacer is the equivalent assertion. + try { + $pdf = $this->converter->odtToPdf(odtBytes: $odt); + } catch (Throwable $e) { + throw $this->wrapAfterFallback(cause: $cause, stage: 'odt_to_pdf', previous: $e); + } + + try { + $output = $this->pdfTextReplacer->replaceInPdf( + pdfBytes: $pdf, + substitutions: $substitutions, + strict: true + ); + } catch (PdfAnonymisationException $e) { + throw $this->wrapAfterFallback(cause: $cause, stage: 'rerun_replace', previous: $e); + } + + $this->logger->info( + '[PdfOdtFallbackOrchestrator] Path B succeeded', + ['outputBytes' => strlen($output)] + ); + + return $output; + }//end attempt() + + /** + * Wrap a Path-B-side failure with the after-fallback reason code. + * + * @param PdfAnonymisationException $cause The original Path A exception (preserved as previous). + * @param string $stage Which Path B step failed (`pdf_to_odt`, `odt_to_pdf`, `rerun_replace`). + * @param Throwable $previous The Path-B-side exception. + * + * @return PdfAnonymisationException A new exception carrying + * REASON_VALIDATION_FAILED_AFTER_FALLBACK. + */ + private function wrapAfterFallback( + PdfAnonymisationException $cause, + string $stage, + Throwable $previous + ): PdfAnonymisationException { + $diagnostic = [ + 'pathB_stage' => $stage, + 'pathA_reason' => $cause->getReason(), + 'pathB_previous' => $previous::class, + ]; + + return new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_VALIDATION_FAILED_AFTER_FALLBACK, + message: sprintf('Path B fallback failed at stage %s', $stage), + diagnostic: $diagnostic, + previous: $previous + ); + }//end wrapAfterFallback() +}//end class diff --git a/lib/Service/File/Pdf/PdfMetadataSanitizer.php b/lib/Service/File/Pdf/PdfMetadataSanitizer.php new file mode 100644 index 0000000000..289a226b48 --- /dev/null +++ b/lib/Service/File/Pdf/PdfMetadataSanitizer.php @@ -0,0 +1,315 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File\Pdf; + +use ddn\sapp\PDFDoc; +use ddn\sapp\pdfvalue\PDFValueString; +use OCA\OpenRegister\Exception\PdfAnonymisationException; +use Psr\Log\LoggerInterface; +use ReflectionClass; +use Throwable; + +/** + * Sanitises PDF metadata in-place on a loaded `PDFDoc`. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Pdf + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ +class PdfMetadataSanitizer +{ + + /** + * Sentinel value substituted for stripped /Info text fields. + * + * Empty-string would also work but a recognisable sentinel makes + * audit trails (per ADR-022) easier to read. + * + * @var string + */ + private const SENTINEL = '[REDACTED]'; + + /** + * /Info fields that MUST be stripped (replaced with SENTINEL). + * + * @var string[] + */ + private const INFO_FIELDS_TO_STRIP = [ + 'Title', + 'Author', + 'Subject', + 'Keywords', + 'Creator', + ]; + + // Note: /Info fields NOT in INFO_FIELDS_TO_STRIP are left untouched + // by `sanitize()`. The preserve-list per spec is Producer, + // CreationDate, and ModDate — provenance fields that carry no PII. + // The strip-list above is the actual contract; the preserve-list is + // the implicit complement. + + /** + * XMP namespace prefixes whose elements MUST be stripped. + * + * @var string[] + */ + private const XMP_NAMESPACES_TO_STRIP = [ + 'dc', + 'xmp', + 'pdf', + ]; + + /** + * Constructor. + * + * @param LoggerInterface $logger Logger for PII-redacted diagnostics. + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Sanitise the metadata of a loaded PDFDoc in-place. + * + * After this returns the caller MUST serialise via + * `to_pdf_file_b(rebuild: true)` to persist the changes — the + * sanitiser mutates objects that need to land in the rebuild path. + * + * @param PDFDoc $doc The loaded PDF document to mutate. + * + * @return array{info_fields_stripped: int, xmp_stripped: bool} + * Diagnostic summary (no PII). + * + * @throws PdfAnonymisationException On reflection / SAPP errors. + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + public function sanitize(PDFDoc $doc): array + { + $diagnostic = [ + 'info_fields_stripped' => 0, + 'xmp_stripped' => false, + ]; + + try { + $infoObj = $this->locateInfoObject(doc: $doc); + } catch (Throwable $e) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'PdfMetadataSanitizer could not reach the trailer Info reference', + diagnostic: ['stage' => 'metadata.locate_info'], + previous: $e + ); + } + + if ($infoObj !== null) { + $infoValue = $infoObj->get_value(); + foreach (self::INFO_FIELDS_TO_STRIP as $field) { + if (isset($infoValue[$field]) === true) { + $infoValue[$field] = new PDFValueString(self::SENTINEL); + $diagnostic['info_fields_stripped']++; + } + } + + if ($diagnostic['info_fields_stripped'] > 0) { + // Re-register so the mutation lands in the rebuild path + // (same write-back contract as content-stream mutations). + $doc->add_object(pdf_object: $infoObj); + } + } + + try { + $diagnostic['xmp_stripped'] = $this->sanitizeXmp(doc: $doc); + } catch (Throwable $e) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'PdfMetadataSanitizer failed on XMP stream', + diagnostic: ['stage' => 'metadata.xmp'], + previous: $e + ); + } + + $this->logger->info( + message: '[PdfMetadataSanitizer] PDF metadata sanitised', + context: $diagnostic + ); + + return $diagnostic; + }//end sanitize() + + /** + * Resolve the /Info object reference from the trailer. + * + * SAPP holds the trailer on a protected property. Until a public + * accessor lands upstream we use Reflection — contained to this + * method so the rest of the class reads naturally. + * + * @param PDFDoc $doc The loaded PDF document. + * + * @return \ddn\sapp\PDFObject|null The /Info object, or null when + * the trailer doesn't reference one. + */ + private function locateInfoObject(PDFDoc $doc): ?\ddn\sapp\PDFObject + { + // Reflection setAccessible() is a no-op since PHP 8.1; + // protected/private property access is always allowed by + // getValue() now without explicit unlocking. + $ref = new ReflectionClass(PDFDoc::class); + $property = $ref->getProperty('_pdf_trailer_object'); + $trailer = $property->getValue($doc); + if ($trailer === null) { + return null; + } + + if (isset($trailer['Info']) === false) { + return null; + } + + $infoRef = $trailer['Info']; + $oid = $infoRef->get_object_referenced(); + if ($oid === false || is_array($oid) === true) { + return null; + } + + $infoObj = $doc->get_object(oid: (int) $oid); + if ($infoObj === false) { + return null; + } + + return $infoObj; + }//end locateInfoObject() + + /** + * Strip dc:* / xmp:* / pdf:* fields from the XMP /Metadata stream. + * + * Preserves custom namespaces (Conduction-internal workflow + * metadata, government-template tags etc.). Returns true if a + * mutation was performed. + * + * @param PDFDoc $doc The loaded PDF document. + * + * @return bool True when the XMP stream was mutated. + */ + private function sanitizeXmp(PDFDoc $doc): bool + { + $ref = new ReflectionClass(PDFDoc::class); + $property = $ref->getProperty('_pdf_trailer_object'); + $trailer = $property->getValue($doc); + if ($trailer === null || isset($trailer['Root']) === false) { + return false; + } + + $rootOid = $trailer['Root']->get_object_referenced(); + if ($rootOid === false || is_array($rootOid) === true) { + return false; + } + + $rootObj = $doc->get_object(oid: (int) $rootOid); + if ($rootObj === false) { + return false; + } + + $rootValue = $rootObj->get_value(); + if (isset($rootValue['Metadata']) === false) { + return false; + } + + $metaRef = $rootValue['Metadata']; + $metaOid = $metaRef->get_object_referenced(); + if ($metaOid === false || is_array($metaOid) === true) { + return false; + } + + $metaObj = $doc->get_object(oid: (int) $metaOid); + if ($metaObj === false) { + return false; + } + + $xmp = $metaObj->get_stream(raw: false); + if (is_string($xmp) === false || $xmp === '') { + return false; + } + + $originalXmp = $xmp; + + // Replace `...` / `...` / + // `...` element bodies with the sentinel. + // Custom namespaces (e.g. ``) are untouched. + foreach (self::XMP_NAMESPACES_TO_STRIP as $ns) { + // Self-closing tags: `` — leave as-is (no body to strip). + // Open-close pair: `...` — replace inner text. + $pattern = sprintf( + '#<%s:([A-Za-z][A-Za-z0-9_-]*)([^/>]*)>.*?#s', + preg_quote($ns, '#'), + preg_quote($ns, '#') + ); + $xmp = preg_replace( + pattern: $pattern, + replacement: '<'.$ns.':\1\2>'.htmlspecialchars(self::SENTINEL, ENT_XML1, 'UTF-8').'', + subject: $xmp + ); + + if ($xmp === null) { + // PCRE failure — fall back to the original. The /Info + // strip above already removed the bulk of PII; XMP + // stripping is a defence-in-depth measure. + $xmp = $originalXmp; + break; + } + }//end foreach + + if ($xmp === $originalXmp) { + return false; + } + + $metaObj->set_stream(stream: $xmp, raw: false); + $doc->add_object(pdf_object: $metaObj); + return true; + }//end sanitizeXmp() +}//end class diff --git a/lib/Service/File/Pdf/PdfTextReplacer.php b/lib/Service/File/Pdf/PdfTextReplacer.php new file mode 100644 index 0000000000..3de1986f7f --- /dev/null +++ b/lib/Service/File/Pdf/PdfTextReplacer.php @@ -0,0 +1,421 @@ +: ]` (locked by spec REQ:placeholder) + * - post-replacement re-extraction diagnostic via smalot/pdfparser + * (always warns on residual entity text; in strict mode — the entity- + * anonymisation flow — it then fails closed with REASON_VALIDATION_FAILED, + * while the lenient default returns the partial result for docx parity) + * - adjacent-duplicate-placeholder collapse (REQ:layout) + * - encrypted-PDF rejection (REQ:filter-coverage / encrypted_pdf reason) + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Pdf + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File\Pdf; + +use ddn\sapp\PDFDoc; +use OCA\OpenRegister\Exception\PdfAnonymisationException; +use Psr\Log\LoggerInterface; +use Smalot\PdfParser\Parser as PdfParser; +use Throwable; + +/** + * Replaces entity text in PDF byte streams using the SAPP fork. + * + * Pipeline shape (per `pdf-anonymisation` change spec): + * + * 1. Caller passes raw PDF bytes + a substitution map keyed by entity text. + * 2. {@see replaceInPdf} loads via `PDFDoc::from_string`, calls + * `replace_text_in_document`, serialises with `to_pdf_file_b(rebuild=true)`. + * 3. {@see validateOutput} re-extracts the result via `smalot/pdfparser` + * and emits a PII-free warning if any substitution-map key remains in + * the extracted text; strict callers (entity anonymisation) then fail + * closed, lenient callers (ad-hoc replace, docx parity) return the partial. + * 4. {@see collapseAdjacentDuplicatePlaceholders} runs as a content-stream + * pre-pass (NOT on the re-extracted text — we want to mutate the PDF, + * not just the validation view) to fold `[P:7] [P:7]` → `[P:7]`. + * + * Failure modes raise {@see PdfAnonymisationException} with a `reason` code + * the controller layer maps to HTTP status. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Pdf + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ +class PdfTextReplacer +{ + /** + * Constructor. + * + * @param LoggerInterface $logger Logger for diagnostic surfaces (PII-free). + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Replace entity text in a PDF byte stream and emit a sanitised output. + * + * Runs the full pipeline: load → replace → re-serialise → validate. + * On validation failure the output is discarded and the exception + * carries a diagnostic surface (which entities remained, how many + * streams were modified, etc.) for ops review. + * + * @param string $pdfBytes Raw input PDF bytes. + * @param array $substitutions Map: entity-text => placeholder + * (e.g. ['Jan Jansen' => '[PERSON: 7]']). + * @param bool $strict Forwarded to {@see validateOutput}: when + * true, residual entity text fails closed. + * @param array $residualEntities Out-param receiving any entity text that + * remained after replacement. + * + * @return string Anonymised PDF bytes. + * + * @throws PdfAnonymisationException On any pipeline failure. + * + * @phpstan-param array $substitutions + * @psalm-param array $substitutions + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) $strict forwards the fail-closed vs lenient validation policy. + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + public function replaceInPdf(string $pdfBytes, array $substitutions, bool $strict=false, array &$residualEntities=[]): string + { + $residualEntities = []; + if (count($substitutions) === 0) { + // No-op: nothing to anonymise. + return $pdfBytes; + } + + // Defensive longest-first ordering. SAPP iterates the map in PHP + // key-insertion order (linear matcher and the cross-operator / + // cross-block / cross-line passes all `foreach` it directly; the + // cross-line claimed-range guard explicitly assumes longest-first + // — see Conduction/sapp PR #1). DocumentProcessingHandler already + // orders its map, but re-sorting here makes the overlap guarantee + // hold for every caller and survive future SAPP bumps. Same + // comparator as the caller: length desc, bytewise asc tie-break + // for deterministic output. Params are deliberately untyped: + // PHP coerces purely-numeric needle text ("2026", a spaceless + // BSN) to INT array keys, which would fatal a string-typed + // closure. + uksort( + $substitutions, + static function ($left, $right): int { + $left = (string) $left; + $right = (string) $right; + return [mb_strlen($right), $left] <=> [mb_strlen($left), $right]; + } + ); + + try { + $doc = PDFDoc::from_string(buffer: $pdfBytes); + } catch (Throwable $e) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'Failed to load PDF via SAPP', + diagnostic: ['stage' => 'load'], + previous: $e + ); + } + + if ($doc === false) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'SAPP returned false on PDFDoc::from_string', + diagnostic: ['stage' => 'load'] + ); + } + + try { + $stats = $doc->replace_text_in_document(substitutions: $substitutions); + } catch (Throwable $e) { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'SAPP replace_text_in_document raised an exception', + diagnostic: ['stage' => 'replace'], + previous: $e + ); + } + + // Serialise with rebuild=true per the SAPP contract: incremental + // mode produces an unopenable PDF once _pdf_objects is populated + // by replace_text_in_document. `to_pdf_file_s()` is the + // string-returning sibling of `to_pdf_file_b()` (calls + // `get_raw()` on the underlying Buffer) — we use it because + // `Buffer::__toString()` returns a `debug_var()` dump, NOT the + // raw bytes. + $serialised = $doc->to_pdf_file_s(rebuild: true); + if ($serialised === false || $serialised === '') { + throw new PdfAnonymisationException( + reason: PdfAnonymisationException::REASON_INTERNAL_ERROR, + message: 'SAPP to_pdf_file_s returned false/empty', + diagnostic: ['stage' => 'serialise'] + ); + } + + $outputBytes = $serialised; + + // Validation: re-extract the output and collect any residual + // substitution-map keys. Best-effort (no longer fails closed) — the + // residual needles are returned to the caller so the anonymisation + // flow can write the file and surface a warning. + $residualEntities = $this->validateOutput(outputBytes: $outputBytes, substitutions: $substitutions, replaceStats: $stats, strict: $strict); + + $this->logger->info( + message: '[PdfTextReplacer] PDF anonymisation succeeded', + context: [ + 'streams_scanned' => ($stats['streams_scanned'] ?? 0), + 'streams_modified' => ($stats['streams_modified'] ?? 0), + 'tj_arrays_modified' => ($stats['tj_arrays_modified'] ?? 0), + 'subset_font_fallbacks_used' => ($stats['subset_font_fallbacks_used'] ?? 0), + 'rejected_substitutions' => count(($stats['rejected_substitutions'] ?? [])), + 'substitution_count' => count($substitutions), + ] + ); + + return $outputBytes; + }//end replaceInPdf() + + /** + * Re-extract the output PDF via smalot/pdfparser and surface a + * PII-free diagnostic when substitution-map keys remain. + * + * Aligned with the docx path (`replaceWordsInWordDocument`), which + * also returns a partially-anonymised document silently when + * `str_ireplace` cannot reach text split across `` runs. + * Failing closed here would make PDF noticeably stricter than docx; + * instead we log a structural warning and let the caller surface a + * partial-success result. Re-extraction failures (parser threw) are + * still treated as a hard internal error. + * + * @param string $outputBytes Serialised output PDF bytes. + * @param array $substitutions The original substitution map. + * @param array $replaceStats Stats returned by SAPP's replace API + * (kept in the diagnostic surface + * for ops review). + * @param bool $strict When true (the entity-anonymisation flow), + * residual entity text fails CLOSED with + * `REASON_VALIDATION_FAILED`; when false + * (ad-hoc replace, docx-parity default) it is + * logged as a partial-anonymisation warning. + * + * @return void + * + * @throws PdfAnonymisationException When $strict and residual entity text remains. + * + * @phpstan-param array $substitutions + * @phpstan-param array $replaceStats + * @psalm-param array $substitutions + * @psalm-param array $replaceStats + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) $strict selects fail-closed (entity anonymisation) vs lenient (ad-hoc) behaviour. + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + public function validateOutput(string $outputBytes, array $substitutions, array $replaceStats=[], bool $strict=false): array + { + try { + $parser = new PdfParser(); + $parsedPdf = $parser->parseContent($outputBytes); + $extracted = $parsedPdf->getText(); + } catch (Throwable $e) { + // The post-extraction is diagnostic only (parity with docx, which + // has no validation gate at all). When smalot can't re-parse the + // SAPP output we can't audit residual PII, but blocking the user + // from receiving the bytes is strictly worse than docx — log the + // parser failure structurally (no PII, no needle text) and + // return. Output is still written by the caller. + $this->logger->warning( + message: '[PdfTextReplacer] Post-replace re-extraction skipped — smalot/pdfparser could not parse SAPP output', + context: [ + 'stage' => 'validate.extract', + 'parser_exception' => get_class($e), + 'parser_exception_msg' => $e->getMessage(), + 'output_bytes' => strlen($outputBytes), + 'streams_scanned' => ($replaceStats['streams_scanned'] ?? null), + 'streams_modified' => ($replaceStats['streams_modified'] ?? null), + ] + ); + + // Can't re-extract to audit — report no detectable residuals. + return []; + }//end try + + // Whitespace-normalise the haystack so entity text that the PDF + // splits across line breaks ("14 mei\n2026") is still detected — + // smalot re-extraction inserts the line structure, but the entity + // is semantically present either way. Needles get the same + // treatment below so both sides compare in collapsed-space form. + // + // `preg_replace` with the /u modifier returns NULL on invalid + // UTF-8 (PREG_BAD_UTF8_ERROR) — a realistic mode here, since the + // encoding gaps SAPP tracks (font_encoding_misses, + // cid_split_mismatch, encoding_dict_unhandled) make smalot emit + // raw font-encoded bytes. A silent ""-fallback would make every + // probe below miss and strict mode pass unaudited output — the + // documents most at risk of residual PII are exactly the ones + // that would slip through. Fail CLOSED in strict mode; in + // lenient mode fall back to the un-normalised haystack, which + // keeps the pre-normalisation detection coverage. + $normalisedExtracted = preg_replace('/\s+/u', ' ', $extracted); + if ($normalisedExtracted === null) { + $this->logger->warning( + message: '[PdfTextReplacer] Haystack normalisation failed — falling back to un-normalised extraction', + context: [ + 'stage' => 'validate.normalise', + 'preg_error' => preg_last_error_msg(), + ] + ); + $normalisedExtracted = $extracted; + }//end if + + $residual = []; + foreach (array_keys($substitutions) as $needleIndex => $needle) { + if ($needle === '') { + continue; + } + + $rawNeedle = (string) $needle; + $normalisedNeedle = preg_replace('/\s+/u', ' ', $rawNeedle); + if ($normalisedNeedle === null) { + // Invalid-UTF-8 needle: do NOT silently skip — that would + // hide a residual this needle should have flagged. Probe + // the raw bytes instead (`strpos` is encoding-agnostic) + // and surface the degradation structurally (needle index + // only, never the needle text — ADR-005). + $this->logger->warning( + message: '[PdfTextReplacer] Needle normalisation failed — probing raw bytes', + context: [ + 'stage' => 'validate.normalise', + 'needle_index' => $needleIndex, + 'preg_error' => preg_last_error_msg(), + ] + ); + if (strpos($extracted, $rawNeedle) !== false) { + $residual[] = $rawNeedle; + } + + continue; + } + + $normalisedNeedle = trim($normalisedNeedle); + if ($normalisedNeedle === '') { + continue; + } + + // Case-SENSITIVE, multibyte-safe substring check via `mb_strpos` + // — mirrors the replacement engine's guarantee. SAPP replaces + // exact-case needles only, so a case-insensitive probe here + // flagged text that was never a detected entity (e.g. the + // lowercase domain in "www.amsterdam.nl" tripping on the + // LOCATION needle "Amsterdam") and failed runs that were in + // fact fully anonymised. + if (mb_strpos($normalisedExtracted, $normalisedNeedle) !== false) { + $residual[] = $rawNeedle; + } + }//end foreach + + if (count($residual) > 0) { + $diagnostic = [ + 'stage' => 'validate.assert', + 'residual_count' => count($residual), + 'streams_scanned' => ($replaceStats['streams_scanned'] ?? null), + 'streams_modified' => ($replaceStats['streams_modified'] ?? null), + 'tj_arrays_modified' => ($replaceStats['tj_arrays_modified'] ?? null), + 'subset_font_fallbacks_used' => ($replaceStats['subset_font_fallbacks_used'] ?? null), + 'font_encoding_misses' => count(($replaceStats['font_encoding_misses'] ?? [])), + 'cid_split_mismatch' => count(($replaceStats['cid_split_mismatch'] ?? [])), + 'encoding_dict_unhandled' => count(($replaceStats['encoding_dict_unhandled'] ?? [])), + 'contents_array_pages' => count(($replaceStats['contents_array_pages'] ?? [])), + 'rejected_substitutions' => count(($replaceStats['rejected_substitutions'] ?? [])), + ]; + + // PII-redacted log line per ADR-005. NEVER include $residual's + // actual entity text in logs — only the count + structural + // counters. The audit trail (per ADR-022) handles the values. + $this->logger->warning( + message: '[PdfTextReplacer] Partial anonymisation — residual entity text in output', + context: $diagnostic + ); + }//end if + + // Best-effort policy (was: fail closed in strict mode). The + // anonymisation flow no longer discards a partially-redacted file — + // it writes the output and surfaces a warning listing the residual + // entities so the operator can iterate (manual entities, skip + // unselected occurrences). The residual NEEDLES are returned to the + // caller; logs stay PII-free per ADR-005, while the authenticated + // anonymise response carries the values for the review UI. + return $residual; + }//end validateOutput() + + /** + * Collapse adjacent duplicate placeholders in a freshly-substituted text. + * + * Substitution maps with variant entries (e.g. ["Jan Jansen", "Jansen", + * "Jan"] all → "[PERSON: 7]") can produce strings like + * `"[PERSON: 7] [PERSON: 7]"` when adjacent variants both matched. + * This helper folds runs separated by ANY whitespace, hyphens, or + * underscores into a single placeholder. + * + * Operates on plain UTF-8 strings — callers MAY apply it to a + * `smalot/pdfparser` extraction for display, but the SAPP-side + * mutation already runs through the same logic at byte level so + * the on-disk PDF reflects the collapsed form too. + * + * @param string $text Input UTF-8 text containing zero or more placeholders. + * + * @return string Text with adjacent identical placeholders collapsed. + * + * @spec openspec/specs/pdf-anonymisation/spec.md + */ + public static function collapseAdjacentDuplicatePlaceholders(string $text): string + { + // Match `[TYPE: id]` followed by ([ \t\n\r_-]+ + same `[TYPE: id]`) one or more times. + // The backreference \\1 forces the two placeholders to be identical. + // PHP_INT_MAX iterations is the natural fixed-point: collapses runs of + // 2, 3, 4, ... duplicates down to one. + $pattern = '/(\[[A-Z][A-Z_]*: [A-Za-z0-9_-]+\])(?:[ \t\n\r_\-]+\1)+/u'; + + $result = preg_replace(pattern: $pattern, replacement: '$1', subject: $text); + + // On PCRE failure (compile error / limit hit) `preg_replace` + // returns null. Fall back to the original — adjacent-duplicate + // collapse is a cosmetic post-pass; the validation gate already + // ensured the underlying PDF is correct. + return $result ?? $text; + }//end collapseAdjacentDuplicatePlaceholders() +}//end class diff --git a/lib/Service/File/PlaceholderIdTranslator.php b/lib/Service/File/PlaceholderIdTranslator.php new file mode 100644 index 0000000000..8f57a9cca6 --- /dev/null +++ b/lib/Service/File/PlaceholderIdTranslator.php @@ -0,0 +1,206 @@ + + * @copyright 2026 Conduction B.V. + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File; + +/** + * Translates the internal, global entity id (`openregister_entities.id`) to a + * SCOPE-LOCAL sequence number for the redaction placeholder `[: ]`. + * + * Detection dedups by value, so the same person carries the same global + * `e.id` in every file and every publication — a stable cross-disclosure + * linking key (pseudonymisation, not anonymisation; AVG Art. 4(5) / WP29 + * Op. 05/2014 linkability). This translator keeps `e.id` as the internal + * "same person" key but emits a number that is local to a single scope and + * never links a person across scopes. + * + * Two scopes: + * - **Per-document (default):** numbers are assigned lazily by order of first + * appearance within one anonymise run (`perDocument()`); no persistence. + * - **Per-dossier (opt-in):** the translator is pre-seeded with a map + * deterministically recomputed from the dossier's stored entity-relation + * rows (`forDossier()` / `withSeededMap()`), so every per-file call within + * the dossier resolves the same person to the same number. + * + * @category Service + * @package OCA\OpenRegister + * @author Conduction + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister + * + * @spec openspec/changes/anonymisation-placeholder-id-scope/design.md + */ +class PlaceholderIdTranslator +{ + + /** + * Map of internal entity id (stringified) to its scope-local number. + * + * @var array + */ + private array $map = []; + + /** + * Next sequence number to assign to a not-yet-seen entity id. + * + * @var integer + */ + private int $next = 1; + + /** + * Private constructor — use the named factory methods. + * + * @param array $seed Pre-computed `e.id => local_number` map + * (empty for a fresh per-document translator). + */ + private function __construct(array $seed=[]) + { + foreach ($seed as $entityId => $number) { + $this->map[(string) $entityId] = (int) $number; + } + + if ($this->map !== []) { + // Continue numbering after the highest seeded value so an entity + // not present in the seed (defensive edge) still gets a unique, + // non-colliding number. + $this->next = (max($this->map) + 1); + } + + }//end __construct() + + /** + * Create a per-document translator: numbers assigned lazily, by order of + * first appearance, within a single run. No persistence. + * + * @return self + * + * @spec openspec/changes/anonymisation-placeholder-id-scope/design.md (Decision 1) + */ + public static function perDocument(): self + { + return new self(); + + }//end perDocument() + + /** + * Create a translator pre-seeded with an explicit `e.id => number` map. + * + * @param array $seed The seed map. + * + * @return self + * + * @spec openspec/changes/anonymisation-placeholder-id-scope/design.md (Decision 3) + */ + public static function withSeededMap(array $seed): self + { + return new self($seed); + + }//end withSeededMap() + + /** + * Build a per-dossier translator from the dossier's stored entity-relation + * rows. The numbering is a PURE function of the rows (Decision 3): the + * same stored rows always yield the same map, independent of which file + * triggered the call or the order the rows arrive in. + * + * @param array> $rows Entity-relation rows ({entity_id, file_id, position_start}) for the dossier's files. + * + * @return self + * + * @spec openspec/changes/anonymisation-placeholder-id-scope/design.md (Decision 3) + */ + public static function forDossier(array $rows): self + { + return new self(self::rankByFirstAppearance(rows: $rows)); + + }//end forDossier() + + /** + * Pure ranking core: impose the total stable order + * `(file_id ASC, position_start ASC, entity_id ASC)` and assign each + * distinct `entity_id` its rank of first appearance (`1, 2, 3 …`). + * + * Exposed (static, side-effect-free) so the ordering contract can be + * unit-tested directly. + * + * @param array> $rows Entity-relation rows ({entity_id, file_id, position_start}). + * + * @return array Map of `e.id` (stringified) to its scope-local number. + * + * @spec openspec/changes/anonymisation-placeholder-id-scope/design.md (Decision 3) + */ + public static function rankByFirstAppearance(array $rows): array + { + usort( + $rows, + static function (array $left, array $right): int { + return [ + (int) ($left['file_id'] ?? 0), + (int) ($left['position_start'] ?? 0), + (int) ($left['entity_id'] ?? 0), + ] <=> [ + (int) ($right['file_id'] ?? 0), + (int) ($right['position_start'] ?? 0), + (int) ($right['entity_id'] ?? 0), + ]; + } + ); + + $map = []; + $number = 1; + foreach ($rows as $row) { + $entityId = (string) ($row['entity_id'] ?? ''); + if ($entityId === '' || isset($map[$entityId]) === true) { + continue; + } + + $map[$entityId] = $number; + $number++; + } + + return $map; + + }//end rankByFirstAppearance() + + /** + * Translate an internal entity id to its scope-local number. In + * per-document mode an unseen id is assigned the next number (first + * appearance); in seeded (per-dossier) mode it returns the pre-computed + * number. The same id always resolves to the same number for the life of + * this translator (within-scope consistency). + * + * @param int|string $entityId The internal `openregister_entities.id`. + * + * @return int The scope-local sequence number. + * + * @spec openspec/changes/anonymisation-placeholder-id-scope/design.md (Decision 1) + */ + public function translate(int | string $entityId): int + { + $key = (string) $entityId; + if (isset($this->map[$key]) === false) { + $this->map[$key] = $this->next; + $this->next++; + } + + return $this->map[$key]; + + }//end translate() +}//end class diff --git a/lib/Service/File/ReadFileHandler.php b/lib/Service/File/ReadFileHandler.php index 180850d5c9..9aeaa35734 100644 --- a/lib/Service/File/ReadFileHandler.php +++ b/lib/Service/File/ReadFileHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -128,7 +128,7 @@ public function setFileService(FileService $fileService): void * @SuppressWarnings(PHPMD.CyclomaticComplexity) File lookup requires handling ID vs path scenarios * @SuppressWarnings(PHPMD.NPathComplexity) Multiple file resolution paths with fallback logic * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-007 + * @spec openspec/specs/file-actions/spec.md */ public function getFile(ObjectEntity|string|null $object=null, string|int $file=''): ?File { @@ -213,7 +213,7 @@ public function getFile(ObjectEntity|string|null $object=null, string|int $file= * @phpstan-param int $fileId * @phpstan-return File|null * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-007 + * @spec openspec/specs/file-actions/spec.md */ public function getFileById(int $fileId): ?File { @@ -262,7 +262,7 @@ public function getFileById(int $fileId): ?File * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flag is intentional for simple filter toggle * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-007 + * @spec openspec/specs/file-actions/spec.md */ public function getFiles( ObjectEntity|string $object, diff --git a/lib/Service/File/SanitizationReport.php b/lib/Service/File/SanitizationReport.php new file mode 100644 index 0000000000..67ec459098 --- /dev/null +++ b/lib/Service/File/SanitizationReport.php @@ -0,0 +1,104 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File; + +use JsonSerializable; + +/** + * Immutable value object capturing the per-category sanitisation counts. + * + * @category Service + * @package OCA\OpenRegister\Service\File + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ +final class SanitizationReport implements JsonSerializable +{ + /** + * Constructor. + * + * @param int $commentsRemoved Distinct comments/annotations removed. + * @param int $trackedChangesAccepted Insert ranges accepted (unwrapped). + * @param int $trackedChangesDropped Delete ranges dropped. + * @param int $revisionAttributesStripped Revision (rsid) attributes removed. + * @param int $hyperlinksFlattened Hyperlinks flattened to plain text. + * @param int $metadataFieldsScrubbed Metadata fields replaced with sentinel. + * @param int $customXmlPartsDropped Custom XML parts removed. + * @param int $fieldCodesStripped Person-identity field codes removed. + * @param string $sentinelApplied The sentinel string used for scrubbing. + * + * @SuppressWarnings(PHPMD.LongVariable) Property names are the stable audit-report JSON keys (design D9). + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function __construct( + public readonly int $commentsRemoved=0, + public readonly int $trackedChangesAccepted=0, + public readonly int $trackedChangesDropped=0, + public readonly int $revisionAttributesStripped=0, + public readonly int $hyperlinksFlattened=0, + public readonly int $metadataFieldsScrubbed=0, + public readonly int $customXmlPartsDropped=0, + public readonly int $fieldCodesStripped=0, + public readonly string $sentinelApplied='' + ) { + }//end __construct() + + /** + * Serialise to a stable, ordered associative array. + * + * @return array + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function jsonSerialize(): array + { + return [ + 'commentsRemoved' => $this->commentsRemoved, + 'trackedChangesAccepted' => $this->trackedChangesAccepted, + 'trackedChangesDropped' => $this->trackedChangesDropped, + 'revisionAttributesStripped' => $this->revisionAttributesStripped, + 'hyperlinksFlattened' => $this->hyperlinksFlattened, + 'metadataFieldsScrubbed' => $this->metadataFieldsScrubbed, + 'customXmlPartsDropped' => $this->customXmlPartsDropped, + 'fieldCodesStripped' => $this->fieldCodesStripped, + 'sentinelApplied' => $this->sentinelApplied, + ]; + }//end jsonSerialize() +}//end class diff --git a/lib/Service/File/SanitizationResult.php b/lib/Service/File/SanitizationResult.php new file mode 100644 index 0000000000..dec4596aa1 --- /dev/null +++ b/lib/Service/File/SanitizationResult.php @@ -0,0 +1,61 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File; + +/** + * Immutable result of an Office document sanitisation pass. + * + * @category Service + * @package OCA\OpenRegister\Service\File + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ +final class SanitizationResult +{ + /** + * Constructor. + * + * @param string $path Absolute path to the sanitised temp file. + * @param SanitizationReport $report Per-category sanitisation counts. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function __construct( + public readonly string $path, + public readonly SanitizationReport $report + ) { + }//end __construct() +}//end class diff --git a/lib/Service/File/Sanitizer/DocxSanitizer.php b/lib/Service/File/Sanitizer/DocxSanitizer.php new file mode 100644 index 0000000000..6663ae3c3c --- /dev/null +++ b/lib/Service/File/Sanitizer/DocxSanitizer.php @@ -0,0 +1,978 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File\Sanitizer; + +use DOMDocument; +use DOMElement; +use DOMNode; +use DOMXPath; +use OCA\OpenRegister\Exception\SanitizationException; +use OCA\OpenRegister\Service\File\SanitizationReport; +use ZipArchive; + +/** + * Sanitises a .docx ZIP container in-place. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Sanitizer + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + * + * @SuppressWarnings(PHPMD.ExcessiveClassLength) OOXML surgery spans comments / tracked-changes / customXml / metadata / fields / hyperlinks. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Each OOXML structure needs its own DOM/XPath pass. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) DOM / ZipArchive collaborators are intrinsic to ZIP-and-XML surgery. + * @SuppressWarnings(PHPMD.TooManyMethods) One private helper per OOXML structure keeps each pass readable. + */ +class DocxSanitizer implements SanitizerInterface +{ + + /** + * The DOCX MIME type this strategy supports. + * + * @var string + */ + private const MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + + /** + * WordprocessingML main namespace URI. + * + * @var string + */ + private const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'; + + /** + * Relationships namespace URI (document.xml r:id references). + * + * @var string + */ + private const NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'; + + /** + * Person-identity field codes to strip (case-insensitive). + * + * @var string[] + */ + private const FIELD_STRIP_LIST = [ + 'AUTHOR', + 'USERNAME', + 'USERINITIALS', + 'LASTSAVEDBY', + ]; + + /** + * Constructor. + * + * @param string $sentinel The sentinel string applied to scrubbed metadata. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function __construct(private readonly string $sentinel='DocuDesk Anonymisation') + { + }//end __construct() + + /** + * {@inheritDoc} + * + * @param string $mimeType The file MIME type. + * + * @return bool True for the DOCX MIME type. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function supports(string $mimeType): bool + { + return $mimeType === self::MIME; + }//end supports() + + /** + * {@inheritDoc} + * + * @param string $sourcePath Path to the source .docx. + * @param string $destPath Path to write the sanitised .docx. + * + * @throws SanitizationException On encryption / corrupt-zip / internal failure. + * + * @return SanitizationReport Per-category counts. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function sanitize(string $sourcePath, string $destPath): SanitizationReport + { + if ($sourcePath !== $destPath) { + if (copy($sourcePath, $destPath) === false) { + throw new SanitizationException( + reason: SanitizationException::REASON_INTERNAL, + message: 'Could not copy source document to destination' + ); + } + } + + $zip = new ZipArchive(); + $code = $zip->open($destPath); + if ($code !== true) { + throw $this->mapOpenFailure(code: $code); + } + + // Encryption probe: an OOXML CFB (encrypted) package exposes an + // "EncryptedPackage" stream rather than the OPC part names. If the + // central directory does not contain [Content_Types].xml, treat it as + // encrypted/corrupt rather than producing garbage output. + if ($zip->locateName('[Content_Types].xml') === false) { + $zip->close(); + throw new SanitizationException( + reason: SanitizationException::REASON_ENCRYPTED, + message: 'Document is encrypted or not a valid OOXML package' + ); + } + + $counts = [ + 'commentsRemoved' => 0, + 'trackedChangesAccepted' => 0, + 'trackedChangesDropped' => 0, + 'revisionAttributesStripped' => 0, + 'hyperlinksFlattened' => 0, + 'metadataFieldsScrubbed' => 0, + 'customXmlPartsDropped' => 0, + 'fieldCodesStripped' => 0, + ]; + + // Snapshot the body-part names BEFORE any part deletion mutates the + // central directory indices. + $bodyParts = $this->bodyParts(zip: $zip); + + // [Content_Types].xml and word/_rels/document.xml.rels are each touched + // by several passes. ZipArchive does not surface a pending in-session + // write to a later getFromName(), so every removal target is collected + // here and the two index parts are reconciled exactly once at the end. + $overridePartNames = []; + $documentRelTargets = []; + $documentRelIds = []; + + // Part-level removals (each deletes a distinct part — safe in-session). + $this->removeCommentParts(zip: $zip, counts: $counts, overridePartNames: $overridePartNames, documentRelTargets: $documentRelTargets); + $this->stripCustomXmlParts(zip: $zip, counts: $counts, overridePartNames: $overridePartNames, documentRelTargets: $documentRelTargets); + + // Metadata parts (each touched exactly once). + $this->scrubMetadata(zip: $zip, counts: $counts); + + // Inline body-part surgery: read once, run every inline pass, write once. + foreach ($bodyParts as $part) { + $relIds = $this->processBodyPart(zip: $zip, part: $part, counts: $counts); + if ($part === 'word/document.xml') { + $documentRelIds = array_merge($documentRelIds, $relIds); + } else if ($relIds !== []) { + // Non-document body parts keep their own rels file (single touch). + $relsName = 'word/_rels/'.basename($part).'.rels'; + $this->reconcileRels(zip: $zip, relsName: $relsName, targetBasenames: [], hyperlinkIds: $relIds); + } + } + + // Single reconciliation of the two heavily-shared index parts. + $this->reconcileContentTypes(zip: $zip, partNames: $overridePartNames); + $this->reconcileRels( + zip: $zip, + relsName: 'word/_rels/document.xml.rels', + targetBasenames: $documentRelTargets, + hyperlinkIds: $documentRelIds + ); + + $zip->close(); + + return new SanitizationReport( + commentsRemoved: $counts['commentsRemoved'], + trackedChangesAccepted: $counts['trackedChangesAccepted'], + trackedChangesDropped: $counts['trackedChangesDropped'], + revisionAttributesStripped: $counts['revisionAttributesStripped'], + hyperlinksFlattened: $counts['hyperlinksFlattened'], + metadataFieldsScrubbed: $counts['metadataFieldsScrubbed'], + customXmlPartsDropped: $counts['customXmlPartsDropped'], + fieldCodesStripped: $counts['fieldCodesStripped'], + sentinelApplied: $this->sentinel + ); + }//end sanitize() + + /** + * Body-bearing XML parts that may contain inline markup. + * + * @param ZipArchive $zip The open archive. + * + * @return string[] Part names present in the archive. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function bodyParts(ZipArchive $zip): array + { + $parts = []; + for ($index = 0; $index < $zip->numFiles; $index++) { + $name = $zip->getNameIndex($index); + if ($name === false) { + continue; + } + + if (preg_match('#^word/(document|header\d*|footer\d*|footnotes|endnotes)\.xml$#', $name) === 1) { + $parts[] = $name; + } + } + + return $parts; + }//end bodyParts() + + /** + * Run all inline passes on a single body part (one load / one save). + * + * @param ZipArchive $zip The open archive. + * @param string $part The body part name. + * @param array $counts Mutable counter map. + * + * @return string[] Relationship IDs of flattened external hyperlinks in this part. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function processBodyPart(ZipArchive $zip, string $part, array &$counts): array + { + $xml = $zip->getFromName($part); + if ($xml === false || $xml === '') { + return []; + } + + $dom = $this->loadXml(xml: $xml); + $xpath = $this->wXpath(dom: $dom); + + $this->removeCommentMarkers(xpath: $xpath); + $this->resolveTrackedChanges(xpath: $xpath, dom: $dom, counts: $counts); + $this->unwrapDataBoundSdt(xpath: $xpath); + $counts['fieldCodesStripped'] += $this->stripSimpleFields(dom: $dom); + $counts['fieldCodesStripped'] += $this->stripComplexFields(dom: $dom); + $relIds = $this->flattenHyperlinkElements(xpath: $xpath, counts: $counts); + + $zip->addFromString($part, $dom->saveXML()); + + return $relIds; + }//end processBodyPart() + + /** + * Remove comment parts; record content-types + rels removal targets. + * + * @param ZipArchive $zip The open archive. + * @param array $counts Mutable counter map. + * @param string[] $overridePartNames Collected Override PartNames to drop. + * @param string[] $documentRelTargets Collected document.xml.rels Target basenames to drop. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function removeCommentParts(ZipArchive $zip, array &$counts, array &$overridePartNames, array &$documentRelTargets): void + { + $commentsXml = $zip->getFromName('word/comments.xml'); + if ($commentsXml !== false && $commentsXml !== '') { + $dom = $this->loadXml(xml: $commentsXml); + $counts['commentsRemoved'] += $this->wXpath(dom: $dom)->query('//w:comment')->length; + } + + $commentParts = ['word/comments.xml', 'word/commentsExtended.xml', 'word/commentsIds.xml', 'word/people.xml']; + foreach ($commentParts as $part) { + if ($zip->locateName($part) !== false) { + $zip->deleteName($part); + } + + $overridePartNames[] = '/'.$part; + } + + foreach (['comments.xml', 'commentsExtended.xml', 'commentsIds.xml', 'people.xml'] as $target) { + $documentRelTargets[] = $target; + } + }//end removeCommentParts() + + /** + * Remove inline comment markers from a body-part DOM. + * + * @param DOMXPath $xpath The body-part xpath context. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function removeCommentMarkers(DOMXPath $xpath): void + { + foreach (['//w:commentRangeStart', '//w:commentRangeEnd', '//w:commentReference'] as $query) { + foreach (iterator_to_array($xpath->query($query)) as $node) { + $node->parentNode?->removeChild($node); + } + } + }//end removeCommentMarkers() + + /** + * Accept tracked-change inserts, drop deletions, strip revision attributes. + * + * @param DOMXPath $xpath The body-part xpath context. + * @param DOMDocument $dom The body-part DOM. + * @param array $counts Mutable counter map. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function resolveTrackedChanges(DOMXPath $xpath, DOMDocument $dom, array &$counts): void + { + // Unwrap (accept insert). + foreach (iterator_to_array($xpath->query('//w:ins')) as $ins) { + $this->unwrap(node: $ins); + $counts['trackedChangesAccepted']++; + } + + // Remove (drop deletion). + foreach (iterator_to_array($xpath->query('//w:del')) as $del) { + $del->parentNode?->removeChild($del); + $counts['trackedChangesDropped']++; + } + + // Strip revision attributes from every element. + $counts['revisionAttributesStripped'] += $this->stripRevisionAttributes(dom: $dom); + }//end resolveTrackedChanges() + + /** + * Remove w:rsid* revision attributes from all elements. + * + * @param DOMDocument $dom The document. + * + * @return int The number of attributes removed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function stripRevisionAttributes(DOMDocument $dom): int + { + $removed = 0; + $rsidAttrs = ['rsidR', 'rsidRPr', 'rsidDel', 'rsidTr', 'rsidP', 'rsidRDefault']; + $elements = iterator_to_array($dom->getElementsByTagName('*')); + foreach ($elements as $element) { + foreach ($rsidAttrs as $attr) { + if ($element->hasAttributeNS(self::NS_W, $attr) === true) { + $element->removeAttributeNS(self::NS_W, $attr); + $removed++; + } + } + } + + return $removed; + }//end stripRevisionAttributes() + + /** + * Remove all custom XML parts; record content-types + rels removal targets. + * + * @param ZipArchive $zip The open archive. + * @param array $counts Mutable counter map. + * @param string[] $overridePartNames Collected Override PartNames to drop. + * @param string[] $documentRelTargets Collected document.xml.rels Target basenames to drop. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function stripCustomXmlParts(ZipArchive $zip, array &$counts, array &$overridePartNames, array &$documentRelTargets): void + { + $toDelete = []; + for ($index = 0; $index < $zip->numFiles; $index++) { + $name = $zip->getNameIndex($index); + if ($name === false) { + continue; + } + + if (preg_match('#^customXml/item\d*\.xml$#', $name) === 1) { + $counts['customXmlPartsDropped']++; + $toDelete[] = $name; + $overridePartNames[] = '/'.$name; + // Document.xml.rels targets customXml as "../customXml/itemN.xml". + $documentRelTargets[] = basename($name); + } else if (preg_match('#^customXml/(itemProps\d*\.xml|_rels/.*)$#', $name) === 1) { + $toDelete[] = $name; + } + } + + foreach ($toDelete as $part) { + $zip->deleteName($part); + } + }//end stripCustomXmlParts() + + /** + * Unwrap data-bound in a body-part DOM (preserve visible content). + * + * Custom XML part counting happens in {@see stripCustomXmlParts}; this pass + * only reshapes the body so no bound binding survives. + * + * @param DOMXPath $xpath The body-part xpath context. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function unwrapDataBoundSdt(DOMXPath $xpath): void + { + foreach (iterator_to_array($xpath->query('//w:sdt[.//w:dataBinding]')) as $sdt) { + $contentNodes = $xpath->query('./w:sdtContent', $sdt); + $content = null; + if ($contentNodes->length > 0) { + $content = $contentNodes->item(0); + } + + if ($content instanceof DOMNode) { + $this->replaceWithChildren(node: $sdt, source: $content); + continue; + } + + $sdt->parentNode?->removeChild($sdt); + } + }//end unwrapDataBoundSdt() + + /** + * Scrub document metadata fields to the sentinel. + * + * @param ZipArchive $zip The open archive. + * @param array $counts Mutable counter map. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function scrubMetadata(ZipArchive $zip, array &$counts): void + { + $counts['metadataFieldsScrubbed'] += $this->scrubCorePart(zip: $zip); + $counts['metadataFieldsScrubbed'] += $this->scrubAppPart(zip: $zip); + $counts['metadataFieldsScrubbed'] += $this->scrubCustomPart(zip: $zip); + }//end scrubMetadata() + + /** + * Scrub docProps/core.xml fields. + * + * @param ZipArchive $zip The open archive. + * + * @return int The number of fields scrubbed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function scrubCorePart(ZipArchive $zip): int + { + $xml = $zip->getFromName('docProps/core.xml'); + if ($xml === false || $xml === '') { + return 0; + } + + $dom = $this->loadXml(xml: $xml); + $fields = [ + ['http://purl.org/dc/elements/1.1/', 'creator'], + ['http://schemas.openxmlformats.org/package/2006/metadata/core-properties', 'lastModifiedBy'], + ['http://purl.org/dc/elements/1.1/', 'title'], + ['http://purl.org/dc/elements/1.1/', 'subject'], + ['http://schemas.openxmlformats.org/package/2006/metadata/core-properties', 'keywords'], + ['http://purl.org/dc/elements/1.1/', 'description'], + ['http://schemas.openxmlformats.org/package/2006/metadata/core-properties', 'category'], + ['http://schemas.openxmlformats.org/package/2006/metadata/core-properties', 'contentStatus'], + ]; + + $count = $this->scrubElements(dom: $dom, fields: $fields); + $zip->addFromString('docProps/core.xml', $dom->saveXML()); + return $count; + }//end scrubCorePart() + + /** + * Scrub docProps/app.xml fields (Company, Manager). + * + * @param ZipArchive $zip The open archive. + * + * @return int The number of fields scrubbed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function scrubAppPart(ZipArchive $zip): int + { + $xml = $zip->getFromName('docProps/app.xml'); + if ($xml === false || $xml === '') { + return 0; + } + + $dom = $this->loadXml(xml: $xml); + $count = 0; + foreach (['Company', 'Manager'] as $tag) { + $nodes = $dom->getElementsByTagName($tag); + foreach (iterator_to_array($nodes) as $node) { + assert($node instanceof DOMNode); + if (trim($node->textContent) !== '') { + $node->textContent = $this->sentinel; + $count++; + } + } + } + + $zip->addFromString('docProps/app.xml', $dom->saveXML()); + return $count; + }//end scrubAppPart() + + /** + * Scrub string-typed custom document properties. + * + * @param ZipArchive $zip The open archive. + * + * @return int The number of properties scrubbed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function scrubCustomPart(ZipArchive $zip): int + { + $xml = $zip->getFromName('docProps/custom.xml'); + if ($xml === false || $xml === '') { + return 0; + } + + $dom = $this->loadXml(xml: $xml); + $xpath = new DOMXPath($dom); + $xpath->registerNamespace('vt', 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'); + $count = 0; + $values = $xpath->query('//vt:lpwstr | //vt:lpstr'); + if ($values === false) { + $values = []; + } + + foreach (iterator_to_array($values) as $value) { + assert($value instanceof DOMNode); + if (trim($value->textContent) !== '') { + $value->textContent = $this->sentinel; + $count++; + } + } + + $zip->addFromString('docProps/custom.xml', $dom->saveXML()); + return $count; + }//end scrubCustomPart() + + /** + * Replace each present, non-empty namespaced element's text with the sentinel. + * + * @param DOMDocument $dom The document. + * @param array $fields Namespace-URI / local-name pairs. + * + * @return int The number of elements scrubbed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function scrubElements(DOMDocument $dom, array $fields): int + { + $count = 0; + foreach ($fields as [$namespace, $local]) { + $nodes = $dom->getElementsByTagNameNS($namespace, $local); + foreach (iterator_to_array($nodes) as $node) { + assert($node instanceof DOMNode); + if (trim($node->textContent) !== '') { + $node->textContent = $this->sentinel; + $count++; + } + } + } + + return $count; + }//end scrubElements() + + /** + * Strip simple-form person-identity fields. + * + * @param DOMDocument $dom The document. + * + * @return int The number of fields removed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function stripSimpleFields(DOMDocument $dom): int + { + $count = 0; + $xpath = $this->wXpath(dom: $dom); + foreach (iterator_to_array($xpath->query('//w:fldSimple')) as $field) { + if (($field instanceof DOMElement) === false) { + continue; + } + + $instr = $field->getAttributeNS(self::NS_W, 'instr'); + if ($this->instructionMatches(instr: $instr) === true) { + $field->parentNode?->removeChild($field); + $count++; + } + } + + return $count; + }//end stripSimpleFields() + + /** + * Strip complex-form person-identity fields. + * + * Walks each paragraph's runs sequentially, collecting from + * begin → separate, and removes begin → end inclusive when the normalised + * instruction is in the strip list. + * + * @param DOMDocument $dom The document. + * + * @return int The number of fields removed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Sequential field-char state machine. + * @SuppressWarnings(PHPMD.NPathComplexity) Same — linear begin/separate/end scan. + */ + private function stripComplexFields(DOMDocument $dom): int + { + $count = 0; + $xpath = $this->wXpath(dom: $dom); + foreach (iterator_to_array($xpath->query('//w:p')) as $paragraph) { + $runs = iterator_to_array($xpath->query('./w:r', $paragraph)); + $runsCount = count($runs); + + $cursor = 0; + while ($cursor < $runsCount) { + $fldType = $this->fldCharType(xpath: $xpath, run: $runs[$cursor]); + if ($fldType !== 'begin') { + $cursor++; + continue; + } + + // Collect instruction text and span until matching end. + $instr = ''; + $end = $cursor; + $depth = 0; + for ($scan = $cursor; $scan < $runsCount; $scan++) { + $type = $this->fldCharType(xpath: $xpath, run: $runs[$scan]); + if ($type === 'begin') { + $depth++; + } else if ($type === 'end') { + $depth--; + if ($depth === 0) { + $end = $scan; + break; + } + } + + $instrNodes = $xpath->query('.//w:instrText', $runs[$scan]); + foreach (iterator_to_array($instrNodes) as $instrNode) { + $instr .= $instrNode->textContent; + } + } + + if ($this->instructionMatches(instr: $instr) === true) { + for ($remove = $cursor; $remove <= $end; $remove++) { + $runs[$remove]->parentNode?->removeChild($runs[$remove]); + } + + $count++; + } + + $cursor = ($end + 1); + }//end while + }//end foreach + + return $count; + }//end stripComplexFields() + + /** + * Read the w:fldCharType of a run, or empty string if it has no fldChar. + * + * @param DOMXPath $xpath The xpath context. + * @param DOMNode $run The run node. + * + * @return string The fldChar type (begin / separate / end) or ''. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function fldCharType(DOMXPath $xpath, DOMNode $run): string + { + $nodes = $xpath->query('./w:fldChar', $run); + if ($nodes->length === 0) { + return ''; + } + + $fldChar = $nodes->item(0); + if (($fldChar instanceof DOMElement) === false) { + return ''; + } + + return $fldChar->getAttributeNS(self::NS_W, 'fldCharType'); + }//end fldCharType() + + /** + * Whether a normalised field instruction names a person-identity field. + * + * @param string $instr The raw instruction text. + * + * @return bool True when the field name is in the strip list. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function instructionMatches(string $instr): bool + { + $normalised = strtoupper(trim($instr)); + // Field name is the first whitespace-delimited token. + $parts = preg_split('/\s+/', $normalised); + if ($parts === false || count($parts) === 0) { + return false; + } + + return in_array($parts[0], self::FIELD_STRIP_LIST, true); + }//end instructionMatches() + + /** + * Flatten hyperlink elements in a body-part DOM; return seen relationship IDs. + * + * @param DOMXPath $xpath The body-part xpath context. + * @param array $counts Mutable counter map. + * + * @return string[] The relationship IDs of flattened external hyperlinks. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function flattenHyperlinkElements(DOMXPath $xpath, array &$counts): array + { + $relIds = []; + foreach (iterator_to_array($xpath->query('//w:hyperlink')) as $link) { + if (($link instanceof DOMElement) === true) { + $relId = $link->getAttributeNS(self::NS_R, 'id'); + if ($relId !== '') { + $relIds[$relId] = true; + } + } + + $this->unwrap(node: $link); + $counts['hyperlinksFlattened']++; + } + + return array_keys($relIds); + }//end flattenHyperlinkElements() + + /** + * Reconcile [Content_Types].xml: drop all collected Override PartNames once. + * + * @param ZipArchive $zip The open archive. + * @param string[] $partNames Override PartNames to drop (with leading slash). + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function reconcileContentTypes(ZipArchive $zip, array $partNames): void + { + if ($partNames === []) { + return; + } + + $xml = $zip->getFromName('[Content_Types].xml'); + if ($xml === false || $xml === '') { + return; + } + + $dom = $this->loadXml(xml: $xml); + $changed = false; + foreach (iterator_to_array($dom->getElementsByTagName('Override')) as $override) { + if (in_array($override->getAttribute('PartName'), $partNames, true) === true) { + $override->parentNode?->removeChild($override); + $changed = true; + } + } + + if ($changed === true) { + $zip->addFromString('[Content_Types].xml', $dom->saveXML()); + } + }//end reconcileContentTypes() + + /** + * Reconcile a rels part once: drop Relationship entries by Target basename + * and drop hyperlink Relationship entries by Id. + * + * @param ZipArchive $zip The open archive. + * @param string $relsName The rels part path. + * @param string[] $targetBasenames Target basenames to drop (e.g. comments.xml, item1.xml). + * @param string[] $hyperlinkIds Hyperlink relationship IDs to drop. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Dual-criteria relationship filter (by-target OR by-hyperlink-id) over a single DOM pass. + */ + private function reconcileRels(ZipArchive $zip, string $relsName, array $targetBasenames, array $hyperlinkIds): void + { + if ($targetBasenames === [] && $hyperlinkIds === []) { + return; + } + + $xml = $zip->getFromName($relsName); + if ($xml === false || $xml === '') { + return; + } + + $dom = $this->loadXml(xml: $xml); + $changed = false; + foreach (iterator_to_array($dom->getElementsByTagName('Relationship')) as $rel) { + $byTarget = in_array(basename($rel->getAttribute('Target')), $targetBasenames, true); + $isHyperlink = (str_ends_with($rel->getAttribute('Type'), '/hyperlink') === true + && in_array($rel->getAttribute('Id'), $hyperlinkIds, true) === true); + + if ($byTarget === true || $isHyperlink === true) { + $rel->parentNode?->removeChild($rel); + $changed = true; + } + } + + if ($changed === true) { + $zip->addFromString($relsName, $dom->saveXML()); + } + }//end reconcileRels() + + /** + * Replace a node with its own child nodes (unwrap). + * + * @param DOMNode $node The node to unwrap. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function unwrap(DOMNode $node): void + { + $parent = $node->parentNode; + if ($parent === null) { + return; + } + + while ($node->firstChild !== null) { + $parent->insertBefore($node->firstChild, $node); + } + + $parent->removeChild($node); + }//end unwrap() + + /** + * Replace a node with the child nodes of a designated source element. + * + * @param DOMNode $node The node to replace. + * @param DOMNode $source The node whose children become the replacement. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function replaceWithChildren(DOMNode $node, DOMNode $source): void + { + $parent = $node->parentNode; + if ($parent === null) { + return; + } + + foreach (iterator_to_array($source->childNodes) as $child) { + $parent->insertBefore($child, $node); + } + + $parent->removeChild($node); + }//end replaceWithChildren() + + /** + * Load XML into a namespace-aware DOMDocument (XXE-safe). + * + * @param string $xml The XML string. + * + * @throws SanitizationException When the part is not well-formed XML. + * + * @return DOMDocument The loaded document. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function loadXml(string $xml): DOMDocument + { + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = false; + // LIBXML_NONET blocks network access; PHP 8 + libxml >= 2.9 disables + // external entity loading by default, keeping this XXE-safe. + $loaded = $dom->loadXML($xml, (LIBXML_NONET | LIBXML_COMPACT)); + if ($loaded === false) { + throw new SanitizationException( + reason: SanitizationException::REASON_CORRUPT_ZIP, + message: 'A document part was not well-formed XML' + ); + } + + return $dom; + }//end loadXml() + + /** + * Build an xpath context with the w: and r: namespaces registered. + * + * @param DOMDocument $dom The document. + * + * @return DOMXPath The namespace-aware xpath context. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function wXpath(DOMDocument $dom): DOMXPath + { + $xpath = new DOMXPath($dom); + $xpath->registerNamespace('w', self::NS_W); + $xpath->registerNamespace('r', self::NS_R); + return $xpath; + }//end wXpath() + + /** + * Map a ZipArchive::open failure code to a SanitizationException. + * + * @param int $code The ZipArchive error code. + * + * @return SanitizationException The mapped exception. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function mapOpenFailure(int $code): SanitizationException + { + return new SanitizationException( + reason: SanitizationException::REASON_CORRUPT_ZIP, + message: sprintf('Could not open document ZIP (code %d)', $code) + ); + }//end mapOpenFailure() +}//end class diff --git a/lib/Service/File/Sanitizer/OdtSanitizer.php b/lib/Service/File/Sanitizer/OdtSanitizer.php new file mode 100644 index 0000000000..ff7e6c6b10 --- /dev/null +++ b/lib/Service/File/Sanitizer/OdtSanitizer.php @@ -0,0 +1,433 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File\Sanitizer; + +use DOMDocument; +use DOMElement; +use DOMNode; +use DOMXPath; +use OCA\OpenRegister\Exception\SanitizationException; +use OCA\OpenRegister\Service\File\SanitizationReport; +use ZipArchive; + +/** + * Sanitises an .odt ZIP container in-place. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Sanitizer + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) DOM / ZipArchive collaborators are intrinsic to ZIP-and-XML surgery. + */ +class OdtSanitizer implements SanitizerInterface +{ + + /** + * The ODT MIME type this strategy supports. + * + * @var string + */ + private const MIME = 'application/vnd.oasis.opendocument.text'; + + /** + * ODF namespace URIs used by the surgery passes. + * + * @var array + */ + private const NS = [ + 'office' => 'urn:oasis:names:tc:opendocument:xmlns:office:1.0', + 'text' => 'urn:oasis:names:tc:opendocument:xmlns:text:1.0', + 'meta' => 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0', + 'dc' => 'http://purl.org/dc/elements/1.1/', + 'xlink' => 'http://www.w3.org/1999/xlink', + ]; + + /** + * Constructor. + * + * @param string $sentinel The sentinel string applied to scrubbed metadata. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function __construct(private readonly string $sentinel='DocuDesk Anonymisation') + { + }//end __construct() + + /** + * {@inheritDoc} + * + * @param string $mimeType The file MIME type. + * + * @return bool True for the ODT MIME type. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function supports(string $mimeType): bool + { + return $mimeType === self::MIME; + }//end supports() + + /** + * {@inheritDoc} + * + * @param string $sourcePath Path to the source .odt. + * @param string $destPath Path to write the sanitised .odt. + * + * @throws SanitizationException On encryption / corrupt-zip / internal failure. + * + * @return SanitizationReport Per-category counts. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function sanitize(string $sourcePath, string $destPath): SanitizationReport + { + if ($sourcePath !== $destPath) { + if (copy($sourcePath, $destPath) === false) { + throw new SanitizationException( + reason: SanitizationException::REASON_INTERNAL, + message: 'Could not copy source document to destination' + ); + } + } + + $zip = new ZipArchive(); + $code = $zip->open($destPath); + if ($code !== true) { + throw new SanitizationException( + reason: SanitizationException::REASON_CORRUPT_ZIP, + message: sprintf('Could not open ODT ZIP (code %d)', $code) + ); + } + + if ($zip->locateName('content.xml') === false) { + $zip->close(); + throw new SanitizationException( + reason: SanitizationException::REASON_ENCRYPTED, + message: 'Document is encrypted or not a valid ODT package' + ); + } + + $counts = [ + 'commentsRemoved' => 0, + 'trackedChangesAccepted' => 0, + 'trackedChangesDropped' => 0, + 'hyperlinksFlattened' => 0, + 'metadataFieldsScrubbed' => 0, + 'fieldCodesStripped' => 0, + ]; + + $contentXml = $zip->getFromName('content.xml'); + if ($contentXml !== false) { + $dom = $this->loadXml(xml: $contentXml); + $xpath = $this->xpath(dom: $dom); + + $this->removeAnnotations(xpath: $xpath, counts: $counts); + $this->acceptTrackedChanges(xpath: $xpath, counts: $counts); + $this->flattenHyperlinks(xpath: $xpath, counts: $counts); + $this->removeIdentityPlaceholders(xpath: $xpath, counts: $counts); + + $zip->addFromString('content.xml', $dom->saveXML()); + } + + $this->scrubMetadata(zip: $zip, counts: $counts); + + $zip->close(); + + return new SanitizationReport( + commentsRemoved: $counts['commentsRemoved'], + trackedChangesAccepted: $counts['trackedChangesAccepted'], + trackedChangesDropped: $counts['trackedChangesDropped'], + revisionAttributesStripped: 0, + hyperlinksFlattened: $counts['hyperlinksFlattened'], + metadataFieldsScrubbed: $counts['metadataFieldsScrubbed'], + customXmlPartsDropped: 0, + fieldCodesStripped: $counts['fieldCodesStripped'], + sentinelApplied: $this->sentinel + ); + }//end sanitize() + + /** + * Remove office:annotation comments. + * + * @param DOMXPath $xpath The content xpath context. + * @param array $counts Mutable counter map. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function removeAnnotations(DOMXPath $xpath, array &$counts): void + { + foreach (iterator_to_array($xpath->query('//office:annotation')) as $node) { + $node->parentNode?->removeChild($node); + $counts['commentsRemoved']++; + } + + foreach (iterator_to_array($xpath->query('//office:annotation-end')) as $node) { + $node->parentNode?->removeChild($node); + } + }//end removeAnnotations() + + /** + * Accept tracked changes: keep inserted content, drop deletions, remove container. + * + * @param DOMXPath $xpath The content xpath context. + * @param array $counts Mutable counter map. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function acceptTrackedChanges(DOMXPath $xpath, array &$counts): void + { + // Inline change-start/change-end markers wrap inserted content; remove + // the markers but keep the content between them. + foreach (iterator_to_array($xpath->query('//text:change-start')) as $node) { + $node->parentNode?->removeChild($node); + $counts['trackedChangesAccepted']++; + } + + foreach (iterator_to_array($xpath->query('//text:change-end')) as $node) { + $node->parentNode?->removeChild($node); + } + + // Inline text:change markers reference deleted ranges; drop them. + foreach (iterator_to_array($xpath->query('//text:change')) as $node) { + $node->parentNode?->removeChild($node); + $counts['trackedChangesDropped']++; + } + + // Remove the metadata container itself. + foreach (iterator_to_array($xpath->query('//text:tracked-changes')) as $node) { + $node->parentNode?->removeChild($node); + } + }//end acceptTrackedChanges() + + /** + * Flatten text:a hyperlinks to their inner content. + * + * @param DOMXPath $xpath The content xpath context. + * @param array $counts Mutable counter map. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function flattenHyperlinks(DOMXPath $xpath, array &$counts): void + { + foreach (iterator_to_array($xpath->query('//text:a')) as $link) { + $this->unwrap(node: $link); + $counts['hyperlinksFlattened']++; + } + }//end flattenHyperlinks() + + /** + * Remove inline person-identity placeholders. + * + * @param DOMXPath $xpath The content xpath context. + * @param array $counts Mutable counter map. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function removeIdentityPlaceholders(DOMXPath $xpath, array &$counts): void + { + foreach (['//text:author-name', '//text:author-initials', '//text:initial-creator'] as $query) { + foreach (iterator_to_array($xpath->query($query)) as $node) { + $node->parentNode?->removeChild($node); + $counts['fieldCodesStripped']++; + } + } + }//end removeIdentityPlaceholders() + + /** + * Scrub meta.xml fields to the sentinel. + * + * @param ZipArchive $zip The open archive. + * @param array $counts Mutable counter map. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function scrubMetadata(ZipArchive $zip, array &$counts): void + { + $metaXml = $zip->getFromName('meta.xml'); + if ($metaXml === false) { + return; + } + + $dom = $this->loadXml(xml: $metaXml); + $fields = [ + [self::NS['dc'], 'creator'], + [self::NS['meta'], 'initial-creator'], + [self::NS['dc'], 'title'], + [self::NS['dc'], 'subject'], + [self::NS['meta'], 'keyword'], + [self::NS['dc'], 'description'], + ]; + + $counts['metadataFieldsScrubbed'] += $this->scrubNamedFields(dom: $dom, fields: $fields); + $counts['metadataFieldsScrubbed'] += $this->scrubUserDefinedFields(dom: $dom); + + $zip->addFromString('meta.xml', $dom->saveXML()); + }//end scrubMetadata() + + /** + * Scrub each present, non-empty namespaced meta field to the sentinel. + * + * @param DOMDocument $dom The meta DOM. + * @param array $fields Namespace-URI / local-name pairs. + * + * @return int The number of fields scrubbed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function scrubNamedFields(DOMDocument $dom, array $fields): int + { + $count = 0; + foreach ($fields as [$namespace, $local]) { + $nodes = $dom->getElementsByTagNameNS($namespace, $local); + foreach (iterator_to_array($nodes) as $node) { + assert($node instanceof DOMNode); + if (trim($node->textContent) !== '') { + $node->textContent = $this->sentinel; + $count++; + } + } + } + + return $count; + }//end scrubNamedFields() + + /** + * Scrub string-typed (or untyped) meta:user-defined fields to the sentinel. + * + * @param DOMDocument $dom The meta DOM. + * + * @return int The number of fields scrubbed. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function scrubUserDefinedFields(DOMDocument $dom): int + { + $count = 0; + $userDefined = $dom->getElementsByTagNameNS(self::NS['meta'], 'user-defined'); + foreach (iterator_to_array($userDefined) as $node) { + assert($node instanceof DOMElement); + $valueType = $node->getAttributeNS(self::NS['meta'], 'value-type'); + if (($valueType === '' || $valueType === 'string') && trim($node->textContent) !== '') { + $node->textContent = $this->sentinel; + $count++; + } + } + + return $count; + }//end scrubUserDefinedFields() + + /** + * Replace a node with its own child nodes (unwrap). + * + * @param DOMNode $node The node to unwrap. + * + * @return void + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function unwrap(DOMNode $node): void + { + $parent = $node->parentNode; + if ($parent === null) { + return; + } + + while ($node->firstChild !== null) { + $parent->insertBefore($node->firstChild, $node); + } + + $parent->removeChild($node); + }//end unwrap() + + /** + * Load XML into a namespace-aware DOMDocument (XXE-safe). + * + * @param string $xml The XML string. + * + * @throws SanitizationException When the part is not well-formed XML. + * + * @return DOMDocument The loaded document. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function loadXml(string $xml): DOMDocument + { + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = false; + $loaded = $dom->loadXML($xml, (LIBXML_NONET | LIBXML_COMPACT)); + if ($loaded === false) { + throw new SanitizationException( + reason: SanitizationException::REASON_CORRUPT_ZIP, + message: 'An ODT part was not well-formed XML' + ); + } + + return $dom; + }//end loadXml() + + /** + * Build an xpath context with the ODF namespaces registered. + * + * @param DOMDocument $dom The document. + * + * @return DOMXPath The namespace-aware xpath context. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + private function xpath(DOMDocument $dom): DOMXPath + { + $xpath = new DOMXPath($dom); + foreach (self::NS as $prefix => $uri) { + $xpath->registerNamespace($prefix, $uri); + } + + return $xpath; + }//end xpath() +}//end class diff --git a/lib/Service/File/Sanitizer/SanitizerInterface.php b/lib/Service/File/Sanitizer/SanitizerInterface.php new file mode 100644 index 0000000000..f0f2abc629 --- /dev/null +++ b/lib/Service/File/Sanitizer/SanitizerInterface.php @@ -0,0 +1,78 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\File\Sanitizer; + +use OCA\OpenRegister\Exception\SanitizationException; +use OCA\OpenRegister\Service\File\SanitizationReport; + +/** + * Strategy contract for a single Office document format. + * + * @category Service + * @package OCA\OpenRegister\Service\File\Sanitizer + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ +interface SanitizerInterface +{ + /** + * Whether this strategy supports the given MIME type. + * + * @param string $mimeType The file MIME type. + * + * @return bool True when this strategy can sanitise the format. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function supports(string $mimeType): bool; + + /** + * Sanitise the document at $sourcePath, writing the result to $destPath. + * + * Source and destination MAY be the same path (in-place on a temp copy). + * + * @param string $sourcePath Path to the source document. + * @param string $destPath Path to write the sanitised output. + * + * @throws SanitizationException On encryption / corrupt-zip / internal failure. + * + * @return SanitizationReport Per-category sanitisation counts. + * + * @spec openspec/specs/office-document-sanitization/spec.md + */ + public function sanitize(string $sourcePath, string $destPath): SanitizationReport; +}//end interface diff --git a/lib/Service/File/TaggingHandler.php b/lib/Service/File/TaggingHandler.php index 59c4022106..c46b6ebd93 100644 --- a/lib/Service/File/TaggingHandler.php +++ b/lib/Service/File/TaggingHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -89,7 +89,7 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Tag management requires handling create/find/attach scenarios * @SuppressWarnings(PHPMD.NPathComplexity) Multiple paths for tag creation and attachment * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/specs/file-actions/spec.md#REQ-005 + * @spec openspec/specs/file-actions/spec.md */ public function attachTagsToFile(string $fileId, array $tags=[]): void { @@ -209,7 +209,7 @@ private function findOrCreateTag(string $tagName): ISystemTag * * @psalm-return list * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/specs/file-actions/spec.md#REQ-005 + * @spec openspec/specs/file-actions/spec.md */ public function getFileTags(string $fileId): array { @@ -247,7 +247,7 @@ public function getFileTags(string $fileId): array * * @return string The object tag (e.g., 'object:uuid'). * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-5 + * @spec openspec/specs/file-actions/spec.md */ public function generateObjectTag(ObjectEntity|string $objectEntity): string { @@ -269,7 +269,7 @@ public function generateObjectTag(ObjectEntity|string $objectEntity): string * @phpstan-return array * @psalm-return list * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-5 + * @spec openspec/specs/file-actions/spec.md */ public function getObjectTags(string $objectUuid): array { @@ -308,7 +308,7 @@ public function getObjectTags(string $objectUuid): array * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-5 + * @spec openspec/specs/file-actions/spec.md */ public function addObjectTag(string $objectUuid, string $tagName): void { @@ -330,7 +330,7 @@ public function addObjectTag(string $objectUuid, string $tagName): void * * @throws Exception If the tag is not found. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-5 + * @spec openspec/specs/file-actions/spec.md */ public function removeObjectTag(string $objectUuid, string $tagName): void { @@ -358,7 +358,7 @@ public function removeObjectTag(string $objectUuid, string $tagName): void * * @psalm-return list * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-5 + * @spec openspec/specs/file-actions/spec.md */ public function getAllTags(): array { diff --git a/lib/Service/File/UpdateFileHandler.php b/lib/Service/File/UpdateFileHandler.php index 97e4a7e0da..217be67150 100644 --- a/lib/Service/File/UpdateFileHandler.php +++ b/lib/Service/File/UpdateFileHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ declare(strict_types=1); @@ -110,7 +110,7 @@ public function __construct( * * @throws Exception When the FileMapper is not wired (legacy fixtures). * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-008 + * @spec openspec/specs/file-actions/spec.md */ public function updateFileMetadata( int $fileId, @@ -185,7 +185,7 @@ public function setFileService(FileService $fileService): void * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple file resolution and update paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive file update with logging requires extensive code * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-file/specs/file-actions/spec.md#REQ-008 + * @spec openspec/specs/file-actions/spec.md */ public function updateFile( string|int $filePath, @@ -445,9 +445,15 @@ public function updateFile( // Security: Block executable files. $this->fileValidHandler->blockExecutableFile(fileName: $file->getName(), fileContent: $content); - // @TODO: Check ownership to prevent "File not found" errors - hack for NextCloud rights issues. + // Assert the session can reach the file (owned or shared). $this->fileValidHandler->checkOwnership($file); + // Writing additionally requires write permission. NC enforces this + // natively on putContent(), but we fail fast with a clear message. + if ($file->isUpdateable() === false) { + throw new NotPermittedException("File {$file->getName()} is not writable by the current session"); + } + $file->putContent(data: $content); $this->logger->info( message: "[UpdateFileHandler] updateFile: Successfully updated file content: ".$file->getName(), diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 31ec03b9e0..99bd59374b 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -27,7 +27,7 @@ * @version GIT: * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-11 + * @spec openspec/specs/content-versioning/spec.md * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-29 */ @@ -325,6 +325,25 @@ class FileService */ private FileAuditHandler $fileAuditHandler; + /** + * Per-request memoization for the defense-in-depth folder-access + * re-validation done by `assertObjectFolderAccessible`. + * + * Keyed `"{uid}:{folderId}"` (or `"__no_user__:{folderId}"` when no + * acting user resolves). Only successful re-validations are cached; + * denials re-throw and re-check on retry. The cache lives for the + * lifetime of the FileService instance — i.e. one HTTP request — so + * bulk imports / cascade saves stop re-running + * `getUserFolder() + getById()` filesystem I/O on every iteration. + * PR #1431 concern. + * + * Access changes are not propagated mid-request anyway, so a cache + * hit here returns the same verdict the live check would. + * + * @var array + */ + private array $folderAccessRevalidationCache = []; + /** * Root folder name for all OpenRegister files. * @@ -678,6 +697,9 @@ public function createEntityFolder(Register | ObjectEntity $entity): ?Node } return $this->createObjectFolderById(objectEntity: $entity, currentUser: $currentUser); + } catch (\OCA\OpenRegister\Exception\FolderAccessDeniedException $e) { + // Access denials must propagate to the controller for HTTP 403 with structured body. + throw $e; } catch (exception $e) { $this->logger->error( message: '[FileService] Failed to create folder for entity: {message}', @@ -689,7 +711,7 @@ public function createEntityFolder(Register | ObjectEntity $entity): ?Node ] ); return null; - } + }//end try }//end createEntityFolder() /** @@ -777,15 +799,33 @@ private function getOpenRegisterUserFolder(): Folder */ public function getFilesForEntity(Register|ObjectEntity $entity, ?bool $sharedFilesOnly=false): array { - - $folder = $this->getObjectFolder(objectEntity: $entity); if ($entity instanceof Register) { $folder = $this->getRegisterFolderById(register: $entity); - } - if ($folder === null) { - throw new Exception("Cannot access folder for entity ".$entity->getId()); - } + if ($folder === null) { + throw new Exception("Cannot access folder for entity ".$entity->getId()); + } + } else { + // Read/list path for an object. Objects that have never had a files + // folder created (null/legacy/empty folder property), whose bound + // folder node no longer resolves, or whose folder is not accessible + // to the current caller simply have no files to list. Resolving the + // folder must not fail the whole request: degrade to an empty list + // (HTTP 200) instead of surfacing a 500. See ObjectFilesController::index. + try { + $folder = $this->getObjectFolder(objectEntity: $entity); + } catch (\Throwable $e) { + $this->logger->info( + message: '[FileService] No accessible files folder for object '.$entity->getId().'; returning empty list: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return []; + } + + if ($folder === null) { + return []; + } + }//end if $files = $folder->getDirectoryListing(); @@ -828,7 +868,7 @@ private function getRegisterFolderById(Register $register): ?Folder * @psalm-return Folder|null * @phpstan-return Folder|null * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-4 + * @spec openspec/specs/file-actions/spec.md */ public function getObjectFolder(ObjectEntity|string $objectEntity, int|string|null $registerId=null): ?Folder { @@ -838,6 +878,86 @@ public function getObjectFolder(ObjectEntity|string $objectEntity, int|string|nu ); }//end getObjectFolder() + /** + * Re-validate the access check on an existing object's bound folder. + * + * Used by `ObjectService::ensureObjectFolder` to close the + * defense-in-depth gap flagged on PR #1431: pre-PR cross-tenant + * bindings (rows where `_folder` references a node the current + * actor cannot read) would otherwise pass through subsequent + * saves that don't touch `@self.folder` because `setSelfMetadata` + * only fires when `@self.folder` is present in the write payload. + * + * Re-running `assertFolderIsAccessible` on every save through + * `ensureObjectFolder` makes the check apply uniformly. A + * denial throws `FolderAccessDeniedException` (HTTP 403) at the + * controller layer. + * + * No-op when the object has no folder bound or the bound value + * is non-numeric (legacy path-style folder, handled by the + * auto-create branch in ensureObjectFolder). + * + * **Acting user resolution.** `$currentUser` is forwarded verbatim + * to `assertFolderIsAccessible` and follows that method's + * documented precedence: when non-null it is used as-is, otherwise + * `IUserSession::getUser()` is consulted, otherwise the bind is + * denied. Non-HTTP callers (cron, import pipelines, event listeners) + * MUST pass an explicit `$currentUser` to avoid the + * session-user-is-null → default-deny path; HTTP callers can omit + * the argument and rely on session resolution. + * + * @param ObjectEntity $object The existing object whose folder must be re-validated. + * @param IUser|null $currentUser Explicit acting user; falls back to session resolution. + * + * @return void + * + * @throws \OCA\OpenRegister\Exception\FolderAccessDeniedException When the acting user cannot access the bound folder. + */ + public function assertObjectFolderAccessible(ObjectEntity $object, ?IUser $currentUser=null): void + { + $folder = $object->getFolder(); + if ($folder === null || $folder === '' || is_numeric($folder) === false) { + return; + } + + // Per-request memoization. Resolve the acting user the same way + // `FolderManagementHandler::assertFolderIsAccessible` will (explicit + // arg → session → null) so the cache key matches the access-grant + // tuple the inner method actually evaluates. + $actingUser = ($currentUser ?? $this->userSession->getUser()); + $cacheKey = (($actingUser?->getUID() ?? '__no_user__').':'.$folder); + if (isset($this->folderAccessRevalidationCache[$cacheKey]) === true) { + return; + } + + $this->folderManagementHandler->assertFolderIsAccessible( + folderId: (string) $folder, + currentUser: $currentUser, + objectEntity: $object + ); + + // Only cache successes — failures re-throw and re-check on retry. + $this->folderAccessRevalidationCache[$cacheKey] = true; + }//end assertObjectFolderAccessible() + + /** + * Reset the per-request folder-access revalidation cache. + * + * `isReadable()` is a snapshot of mount/share/ACL/trash state. Within a + * single request that state CAN change — a cascade save may move or trash + * the parent folder. Because the cache lives for the FileService instance + * (≈ the whole request), a stale "accessible" verdict could otherwise + * survive such a mutation. Callers bound the cache to a single save API + * call by invoking this at the entry of `saveObject` / `saveObjects`, so + * each top-level write re-validates against current state. + * + * @return void + */ + public function resetFolderAccessRevalidationCache(): void + { + $this->folderAccessRevalidationCache = []; + }//end resetFolderAccessRevalidationCache() + /** * Returns a share link for the given IShare object. * @@ -1282,7 +1402,7 @@ public function attachTagsToFile(string $fileId, array $tags=[]): void * @psalm-return string * @phpstan-return string * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-5 + * @spec openspec/specs/file-actions/spec.md */ public function generateObjectTag(ObjectEntity|string $objectEntity): string { @@ -1389,7 +1509,7 @@ public function saveFile( * @psalm-return list * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-5 + * @spec openspec/specs/file-actions/spec.md */ public function getAllTags(): array { @@ -1532,7 +1652,7 @@ public function getFileById(int $fileId): ?File * * @psalm-return \OCP\AppFramework\Http\StreamResponse<200, array> * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-3 + * @spec openspec/specs/file-actions/spec.md */ public function streamFile(File $file): \OCP\AppFramework\Http\StreamResponse { @@ -1541,12 +1661,47 @@ public function streamFile(File $file): \OCP\AppFramework\Http\StreamResponse // Set appropriate headers. $response->addHeader('Content-Type', $file->getMimeType()); - $response->addHeader('Content-Disposition', 'attachment; filename="'.$file->getName().'"'); + // SEC-CTRL-9: RFC 6266-encode the filename to prevent header injection / + // response splitting via quotes, control chars, or non-ASCII bytes. + $response->addHeader( + 'Content-Disposition', + $this->buildContentDisposition(disposition: 'attachment', filename: $file->getName()) + ); $response->addHeader('Content-Length', (string) $file->getSize()); return $response; }//end streamFile() + /** + * Build an RFC 6266 compliant Content-Disposition header value. + * + * SEC-CTRL-9: emits a sanitised ASCII `filename="..."` fallback plus a UTF-8 + * `filename*` parameter so a hostile or non-ASCII filename cannot split the + * header or corrupt the response. + * + * @param string $disposition Either 'inline' or 'attachment'. + * @param string $filename The raw file name. + * + * @return string The encoded Content-Disposition header value. + */ + private function buildContentDisposition(string $disposition, string $filename): string + { + $clean = preg_replace('/[\x00-\x1F\x7F]/', '', $filename); + if ($clean === null) { + $clean = ''; + } + + $ascii = preg_replace('/[^\x20-\x7E]/', '_', $clean); + if ($ascii === null) { + $ascii = ''; + } + + $ascii = str_replace(['\\', '"'], '_', $ascii); + $encoded = rawurlencode($clean); + + return $disposition.'; filename="'.$ascii.'"; filename*=UTF-8\'\''.$encoded; + }//end buildContentDisposition() + /** * Publish a file by creating a public share link using direct database operations. * @@ -1816,8 +1971,11 @@ public function replaceWords(Node $node, array $replacements, ?string $outputNam * This is a convenience method that creates replacement mappings * from entity detection results and applies them to a document. * - * @param Node $node The file node to anonymize. - * @param array $entities Array of detected entities with 'text' and 'key' fields. + * @param Node $node The file node to anonymize. + * @param array $entities Array of detected entities with 'text' and 'key' fields. + * @param string $scope Placeholder-numbering scope: 'document' (default) or 'dossier'. + * @param string|null $dossierKey Stable folder id of the dossier (per-dossier scope); null falls + * back to the file's parent folder. * * @throws Exception If anonymization fails. * @@ -1825,20 +1983,60 @@ public function replaceWords(Node $node, array $replacements, ?string $outputNam * * @spec exclude One-line delegation to DocumentProcessingHandler::anonymizeDocument; no facade-owned logic. */ - public function anonymizeDocument(Node $node, array $entities): Node - { + public function anonymizeDocument( + Node $node, + array $entities, + string $scope='document', + ?string $dossierKey=null + ): Node { return $this->documentProcessingHandler->anonymizeDocument( node: $node, - entities: $entities + entities: $entities, + scope: $scope, + dossierKey: $dossierKey ); }//end anonymizeDocument() + /** + * Residual entities from the most recent anonymizeDocument() call. + * + * Best-effort policy: the anonymised file is produced even when some entity + * text could not be removed (e.g. ExApp NER over-capture across table + * cells); these records describe what remains so the caller can warn the + * operator. Empty when the last run fully redacted everything. + * + * @return array Residual records. + * + * @spec exclude One-line delegation to DocumentProcessingHandler::getLastResidualEntities. + */ + public function getLastResidualEntities(): array + { + return $this->documentProcessingHandler->getLastResidualEntities(); + }//end getLastResidualEntities() + + /** + * Per-entity placeholder map from the most recent anonymizeDocument() call. + * + * Maps the internal global entity id (stringified) to the exact placeholder + * string emitted into the document (e.g. `"7" => "[PERSOON: 1]"`), so the + * caller (DocuDesk's grondslagen-summary) can render the same placeholder + * the document carries rather than re-deriving it from the global id. + * + * @return array Map of global entity id → emitted placeholder. + * + * @spec exclude One-line delegation to DocumentProcessingHandler::getLastPlaceholderMap. + */ + public function getLastPlaceholderMap(): array + { + return $this->documentProcessingHandler->getLastPlaceholderMap(); + }//end getLastPlaceholderMap() + /** * Get the file versioning handler. * * @return FileVersioningHandler The versioning handler. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-11 + * @spec openspec/specs/content-versioning/spec.md */ public function getVersioningHandler(): FileVersioningHandler { @@ -1896,7 +2094,7 @@ public function getAuditHandler(): FileAuditHandler * * @throws Exception If the rename fails. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md */ public function renameFile(ObjectEntity $object, int $fileId, string $newName): File { @@ -1951,7 +2149,7 @@ public function renameFile(ObjectEntity $object, int $fileId, string $newName): * * @throws Exception If the copy fails. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md */ public function copyFile(ObjectEntity $sourceObject, int $fileId, ObjectEntity $targetObject): File { @@ -2056,7 +2254,7 @@ private function resolveCopyTargetName(\OCP\Files\Folder $folder, string $desire * * @throws Exception If the move fails. * - * @spec openspec/changes/retrofit-2026-05-24-file-actions/tasks.md#task-1 + * @spec openspec/specs/file-actions/spec.md */ public function moveFile(ObjectEntity $sourceObject, int $fileId, ObjectEntity $targetObject): File { diff --git a/lib/Service/FileSidebarService.php b/lib/Service/FileSidebarService.php index a3a3afaa00..a09079733e 100644 --- a/lib/Service/FileSidebarService.php +++ b/lib/Service/FileSidebarService.php @@ -88,7 +88,7 @@ public function __construct( * schema: array{id: int, title: string} * }> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-4 + * @spec openspec/specs/files-sidebar-tabs/spec.md */ public function getObjectsForFile(int $fileId): array { @@ -275,7 +275,7 @@ private function extractTitle(array $row): string * anonymizedFileId: int|null * } * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-4 + * @spec openspec/specs/files-sidebar-tabs/spec.md */ public function getExtractionStatus(int $fileId): array { diff --git a/lib/Service/Flow/FlowActionService.php b/lib/Service/Flow/FlowActionService.php new file mode 100644 index 0000000000..0cc58c5700 --- /dev/null +++ b/lib/Service/Flow/FlowActionService.php @@ -0,0 +1,471 @@ + + * SPDX-License-Identifier: EUPL-1.2 + * + * @category Service + * @package OCA\OpenRegister\Service\Flow + * + * @author Conduction Development Team + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/flow-agent-action/specs/flow-actions/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Flow; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\AgentRunRequestedEvent; +use OCA\OpenRegister\Service\CalendarEventService; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; +use OCP\Mail\IMailer; +use OCA\OpenRegister\Service\FederationShareService; +use Psr\Log\LoggerInterface; + +/** + * Executes declarative flows attached to a schema via `x-openregister-flows`. + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-2-2 + */ +class FlowActionService +{ + /** + * Dispatch modes the `agent` action supports in v1. Sync inline execution is + * explicitly out of scope (SPECTR-NEXTCLOUD-PLAN.md §5.2 point 5) — a config + * requesting any other mode is malformed and the action is skipped. + * + * @var array + */ + private const SUPPORTED_AGENT_MODES = ['async']; + + /** + * Constructor. + * + * @param SchemaMapper $schemaMapper Resolves a schema by id. + * @param CalendarEventService $calendarEventService Creates calendar events (agenda tasks). + * @param IMailer $mailer Sends email notifications. + * @param IConfig $config Reads the instance mail-from address. + * @param LoggerInterface $logger Logs flow execution + failures. + * @param IEventDispatcher $eventDispatcher Dispatches AgentRunRequestedEvent (ADR-041). + * @param FederationShareService $federationShareService Creates federated shares (federate-share action). + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly CalendarEventService $calendarEventService, + private readonly IMailer $mailer, + private readonly IConfig $config, + private readonly LoggerInterface $logger, + private readonly IEventDispatcher $eventDispatcher, + private readonly FederationShareService $federationShareService + ) { + }//end __construct() + + /** + * Run every flow on the object's schema whose trigger matches. + * + * @param ObjectEntity $object The object the lifecycle event fired on. + * @param string $trigger One of 'created' | 'updated' | 'deleted'. + * + * @return void + * + * @spec exclude declarative-flow engine ships without a formal openspec change; spec to be added in a follow-up ADR + */ + public function run(ObjectEntity $object, string $trigger): void + { + try { + $schema = $this->resolveSchema(object: $object); + } catch (\Throwable $e) { + return; + } + + if ($schema === null) { + return; + } + + $flows = $this->flowsForSchema(schema: $schema); + if (empty($flows) === true) { + return; + } + + $data = $this->buildContext(object: $object); + + foreach ($flows as $flow) { + if (is_array($flow) === false) { + continue; + } + + $flowTrigger = (string) ($flow['trigger'] ?? 'created'); + if ($flowTrigger !== $trigger) { + continue; + } + + $actions = ($flow['actions'] ?? []); + if (is_array($actions) === false) { + continue; + } + + foreach ($actions as $action) { + if (is_array($action) === false) { + continue; + } + + $this->runAction( + action: $action, + object: $object, + data: $data, + flowName: (string) ($flow['name'] ?? 'flow') + ); + } + }//end foreach + }//end run() + + /** + * Resolve the object's schema (internal lookup, no RBAC/multitenancy). + * + * @param ObjectEntity $object The object. + * + * @return Schema|null The resolved schema or null when unresolvable. + */ + private function resolveSchema(ObjectEntity $object): ?Schema + { + $schemaId = $object->getSchema(); + if ($schemaId === null || $schemaId === '') { + return null; + } + + return $this->schemaMapper->find(id: (int) $schemaId, _rbac: false, _multitenancy: false); + }//end resolveSchema() + + /** + * Read the `x-openregister-flows` array from a schema's configuration. + * + * @param Schema $schema The schema. + * + * @return array The declared flows (possibly empty). + */ + private function flowsForSchema(Schema $schema): array + { + $config = ($schema->getConfiguration() ?? []); + $flows = ($config['x-openregister-flows'] ?? null); + if (is_array($flows) === false) { + return []; + } + + // Accept either a list of flows or a single flow object. + if (array_is_list($flows) === false) { + return [$flows]; + } + + return $flows; + }//end flowsForSchema() + + /** + * Build the template context from the object's data plus @self metadata. + * + * @param ObjectEntity $object The object. + * + * @return array Flat map of placeholder => value. + */ + private function buildContext(ObjectEntity $object): array + { + $data = $object->getObject(); + if (is_array($data) === false) { + $data = []; + } + + $data['@id'] = $object->getUuid(); + $data['@uuid'] = $object->getUuid(); + $data['@name'] = $object->getName(); + return $data; + }//end buildContext() + + /** + * Render `{{ field }}` placeholders in a string against the context. + * + * @param mixed $template The template string (non-strings pass through). + * @param array $data The context map. + * + * @return string The rendered string. + */ + private function render(mixed $template, array $data): string + { + if (is_string($template) === false) { + return ''; + } + + return (string) preg_replace_callback( + '/\{\{\s*([A-Za-z0-9_@.]+)\s*\}\}/', + function (array $m) use ($data): string { + $value = ($data[$m[1]] ?? ''); + if (is_array($value) === true) { + return implode(', ', array_map('strval', $value)); + } + + return (string) $value; + }, + $template + ); + }//end render() + + /** + * Dispatch a single action by its declared type. + * + * @param array $action The action config. + * @param ObjectEntity $object The triggering object. + * @param array $data The template context. + * @param string $flowName The owning flow name (for logging). + * + * @return void + */ + private function runAction(array $action, ObjectEntity $object, array $data, string $flowName): void + { + $type = (string) ($action['type'] ?? ''); + try { + switch ($type) { + case 'calendar-event': + case 'agenda-task': + $this->runCalendarEvent(action: $action, object: $object, data: $data); + break; + case 'email': + case 'mail': + $this->runEmail(action: $action, data: $data); + break; + case 'agent': + $this->runAgent(action: $action, object: $object, data: $data, flowName: $flowName); + break; + case 'federate-share': + $this->runFederateShare(action: $action, object: $object); + break; + default: + $this->logger->warning( + message: '[FlowActionService] Unknown flow action type', + context: ['file' => __FILE__, 'line' => __LINE__, 'type' => $type, 'flow' => $flowName] + ); + return; + }//end switch + + $this->logger->info( + message: '[FlowActionService] Flow action executed', + context: ['file' => __FILE__, 'line' => __LINE__, 'flow' => $flowName, 'type' => $type, 'object' => $object->getUuid()] + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[FlowActionService] Flow action failed', + context: ['file' => __FILE__, 'line' => __LINE__, 'flow' => $flowName, 'type' => $type, 'error' => $e->getMessage()] + ); + }//end try + }//end runAction() + + /** + * Share the triggering object with a federated organisation (rule-based). + * + * Config keys: `sharedWith` (slug@host, required), `permissions` + * (read|read-write, default read). Idempotent — one object-scope share per + * object per target — so it can fire on every matching save. The flow's own + * conditions (`x-openregister-flows`) decide WHICH objects qualify (e.g. + * `confidentiality == public && status == published`). + * + * @param array $action The action config. + * @param ObjectEntity $object The triggering object. + * + * @return void + */ + private function runFederateShare(array $action, ObjectEntity $object): void + { + $sharedWith = trim((string) ($action['sharedWith'] ?? '')); + if ($sharedWith === '') { + $this->logger->warning( + message: '[FlowActionService] federate-share action missing sharedWith', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return; + } + + $permissions = (string) ($action['permissions'] ?? 'read'); + $objectUri = (string) ($object->getUri() ?? $object->getUuid()); + + $this->federationShareService->ensureObjectShare( + objectUri: $objectUri, + register: (string) $object->getRegister(), + schema: (string) $object->getSchema(), + sharedWith: $sharedWith, + permissions: $permissions + ); + }//end runFederateShare() + + /** + * Create a calendar event (agenda task) linked to the object. + * + * Config keys: summary, description, location, offsetDays (default 1), + * durationMinutes (default 30). + * + * @param array $action The action config. + * @param ObjectEntity $object The triggering object. + * @param array $data The template context. + * + * @return void + */ + private function runCalendarEvent(array $action, ObjectEntity $object, array $data): void + { + $offsetDays = (int) ($action['offsetDays'] ?? 1); + $duration = (int) ($action['durationMinutes'] ?? 30); + + $start = new \DateTime('now'); + $start->modify('+'.$offsetDays.' day'); + $start->setTime(hour: 9, minute: 0); + $end = (clone $start)->modify('+'.$duration.' minute'); + + $eventData = [ + 'summary' => $this->render(template: ($action['summary'] ?? 'Task'), data: $data), + 'description' => $this->render(template: ($action['description'] ?? ''), data: $data), + 'location' => $this->render(template: ($action['location'] ?? ''), data: $data), + 'dtstart' => $start->format('Y-m-d\TH:i:s'), + 'dtend' => $end->format('Y-m-d\TH:i:s'), + ]; + + $this->calendarEventService->createEvent( + registerId: (int) $object->getRegister(), + schemaId: (int) $object->getSchema(), + objectUuid: (string) $object->getUuid(), + objectTitle: (string) ($object->getName() ?? $object->getUuid()), + data: $eventData + ); + }//end runCalendarEvent() + + /** + * Send an email notification. + * + * Config keys: to (required, templated), subject, body. + * + * @param array $action The action config. + * @param array $data The template context. + * + * @return void + */ + private function runEmail(array $action, array $data): void + { + $to = trim($this->render(template: ($action['to'] ?? ''), data: $data)); + if ($to === '') { + return; + } + + $subject = $this->render(template: ($action['subject'] ?? 'Notification'), data: $data); + $body = $this->render(template: ($action['body'] ?? ''), data: $data); + + $message = $this->mailer->createMessage(); + $message->setTo([$to]); + $message->setSubject($subject); + $message->setPlainBody($body); + + $from = $this->config->getSystemValue('mail_from_address', 'no-reply'); + $domain = $this->config->getSystemValue('mail_domain', 'localhost'); + $message->setFrom([$from.'@'.$domain => 'OpenRegister']); + + $this->mailer->send($message); + }//end runEmail() + + /** + * Dispatch an `AgentRunRequestedEvent` requesting a governed agent run. + * + * OpenRegister never invokes an agent runtime inline and never calls the + * consuming app (e.g. Hermiq) directly — this is the ADR-041 cross-app + * command recipe. A consumer app registers an `IEventListener` for + * `AgentRunRequestedEvent` and performs the governed run (kill-switch, + * human-approval gate, redacted audit, the agent turn, and the result + * write-back) through its own services. If no listener is installed the + * dispatch is a silent no-op — objects keep flowing (SPECTR-NEXTCLOUD-PLAN.md + * §5.2 point 4). + * + * Config keys: agent (required, UUID), skill (optional slug), prompt + * (required, templated), resultField (required), requiresApproval + * (optional, default false), mode (optional, default "async" — the only + * supported value in v1). + * + * @param array $action The action config. + * @param ObjectEntity $object The triggering object. + * @param array $data The template context. + * @param string $flowName The owning flow name (for logging). + * + * @return void + * + * @spec openspec/changes/flow-agent-action/tasks.md#task-2-2 + */ + private function runAgent(array $action, ObjectEntity $object, array $data, string $flowName): void + { + $agent = trim((string) ($action['agent'] ?? '')); + if ($agent === '') { + $this->logger->warning( + message: '[FlowActionService] Malformed agent flow action: missing "agent" reference', + context: ['file' => __FILE__, 'line' => __LINE__, 'flow' => $flowName, 'object' => $object->getUuid()] + ); + return; + } + + $resultField = trim((string) ($action['resultField'] ?? '')); + if ($resultField === '') { + $this->logger->warning( + message: '[FlowActionService] Malformed agent flow action: missing "resultField"', + context: ['file' => __FILE__, 'line' => __LINE__, 'flow' => $flowName, 'object' => $object->getUuid()] + ); + return; + } + + $mode = (string) ($action['mode'] ?? 'async'); + if (in_array($mode, self::SUPPORTED_AGENT_MODES, true) === false) { + $this->logger->warning( + message: '[FlowActionService] Malformed agent flow action: unsupported mode (only "async" is supported in v1)', + context: ['file' => __FILE__, 'line' => __LINE__, 'flow' => $flowName, 'mode' => $mode, 'object' => $object->getUuid()] + ); + return; + } + + $skill = null; + if (isset($action['skill']) === true && trim((string) $action['skill']) !== '') { + $skill = trim((string) $action['skill']); + } + + $prompt = $this->render(template: ($action['prompt'] ?? ''), data: $data); + + $event = new AgentRunRequestedEvent( + subjectUuid: (string) $object->getUuid(), + subjectRegister: (string) $object->getRegister(), + subjectSchema: (string) $object->getSchema(), + agent: $agent, + skill: $skill, + prompt: $prompt, + resultField: $resultField, + requiresApproval: (bool) ($action['requiresApproval'] ?? false), + mode: $mode, + flowName: $flowName + ); + + $this->eventDispatcher->dispatchTyped($event); + }//end runAgent() +}//end class diff --git a/lib/Service/FlowLinkService.php b/lib/Service/FlowLinkService.php index 837225d08c..74e5fd2084 100644 --- a/lib/Service/FlowLinkService.php +++ b/lib/Service/FlowLinkService.php @@ -114,7 +114,7 @@ public function isFlowAvailable(): bool * * @return bool * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function isCurrentUserAdmin(): bool { @@ -141,7 +141,7 @@ public function isCurrentUserAdmin(): bool * @throws Exception On missing user, non-admin (403), missing operation (404), * duplicate (409), Flow unavailable (503). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function linkOperation(string $objectUuid, int $registerId, int $schemaId, int $operationId): FlowLink { @@ -196,7 +196,7 @@ public function linkOperation(string $objectUuid, int $registerId, int $schemaId * * @throws Exception On non-admin (403) or no matching link (404). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function unlinkOperation(string $objectUuid, int $operationId): void { @@ -223,7 +223,7 @@ public function unlinkOperation(string $objectUuid, int $operationId): void * * @return array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getLinkedOperations(string $objectUuid): array { @@ -254,7 +254,7 @@ public function getLinkedOperations(string $objectUuid): array $row['checks'] = $this->decodeJsonField(value: ($opRow['checks'] ?? null)); $row['operation'] = (string) ($opRow['operation'] ?? ''); } - } + }//end if $row['url'] = '/index.php/settings/admin/workflow#'.($row['operationId'] ?? ''); $results[] = $row; @@ -274,7 +274,7 @@ public function getLinkedOperations(string $objectUuid): array * * @return array> * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getAvailableOperations(?string $search=null): array { diff --git a/lib/Service/Gdpr/Case/CaseAccessControl.php b/lib/Service/Gdpr/Case/CaseAccessControl.php new file mode 100644 index 0000000000..c5187263c3 --- /dev/null +++ b/lib/Service/Gdpr/Case/CaseAccessControl.php @@ -0,0 +1,153 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Case; + +use OCP\IAppConfig; +use OCP\IGroupManager; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Handler-scopes-own + officer-override case-level access control (fail-closed). + */ +class CaseAccessControl +{ + + /** + * App id for app-config lookups. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * App-config key naming the group that holds the DSAR officer role. + * + * @var string + */ + public const CONFIG_KEY_OFFICER_GROUP = 'dsar_officer_group'; + + /** + * Constructor. + * + * @param IUserSession $userSession Current caller. + * @param IGroupManager $groupManager Group membership resolver for the officer role. + * @param IAppConfig $appConfig Reads the configured officer group. + * @param LoggerInterface $logger Logger for fail-closed diagnostics. + */ + public function __construct( + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, + private readonly IAppConfig $appConfig, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Whether the current caller may act on the given case. + * + * Object RBAC is assumed already satisfied by ObjectService on the load; + * this narrows to: caller IS the case handler, OR caller holds the + * configured officer role. Any inability to resolve the caller or the + * officer role denies. + * + * @param array $case The loaded case payload (must carry `handler`). + * + * @return bool True when the caller may act on the case; false (fail-closed) otherwise. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md + */ + public function mayAct(array $case): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + // No session identity → cannot establish handler-scope or officer + // role → deny (fail closed). + return false; + } + + $callerId = $user->getUID(); + + // Handler-scopes-own: the caller is the assigned handler of this case. + $handler = (string) ($case['handler'] ?? ''); + if ($handler !== '' && $handler === $callerId) { + return true; + } + + // Officer override: the caller holds the configured officer role. + return $this->isOfficer(user: $user); + }//end mayAct() + + /** + * Whether the given user holds the configured DSAR officer role. + * + * Fail-closed: an unset/empty officer-group configuration, or a group + * lookup that cannot be performed, denies (returns false) — it never treats + * "cannot determine" as "grant". + * + * @param \OCP\IUser $user The user to test. + * + * @return bool True only when the user is confirmed to be in the officer group. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md + */ + private function isOfficer(\OCP\IUser $user): bool + { + $officerGroup = ''; + try { + $officerGroup = (string) $this->appConfig->getValueString( + app: self::APP_ID, + key: self::CONFIG_KEY_OFFICER_GROUP, + default: '' + ); + } catch (\Throwable $e) { + // Cannot read the configuration → cannot confirm the officer role → + // deny (fail closed). We do NOT swallow this into a "skip". + $this->logger->warning( + message: '[CaseAccessControl] officer-group config unreadable — denying (fail closed): '.$e->getMessage() + ); + return false; + } + + if ($officerGroup === '') { + // No officer role configured → no override is possible → deny. + return false; + } + + return $this->groupManager->isInGroup($user->getUID(), $officerGroup); + }//end isOfficer() +}//end class diff --git a/lib/Service/Gdpr/Case/CaseObjectAccessor.php b/lib/Service/Gdpr/Case/CaseObjectAccessor.php new file mode 100644 index 0000000000..d81344cede --- /dev/null +++ b/lib/Service/Gdpr/Case/CaseObjectAccessor.php @@ -0,0 +1,187 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Case; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Service\DsarService; +use OCA\OpenRegister\Service\ObjectService; + +/** + * Load + audited-save helper for data-subject-request case objects. + */ +class CaseObjectAccessor +{ + + /** + * Register slug the case objects live under (declared by the head). + * + * @var string + */ + public const REGISTER_SLUG = 'data-subject-requests'; + + /** + * Schema slug of the case entity (declared by the head). + * + * @var string + */ + public const SCHEMA_SLUG = 'dataSubjectRequest'; + + /** + * Constructor. + * + * @param ObjectService $objectService RBAC + tenant scoped object store. + * @param DsarService $dsarService Resolves the DSAR processing activity for audit pinning. + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly DsarService $dsarService + ) { + }//end __construct() + + /** + * Load a case object by uuid under the caller's RBAC + tenant scope. + * + * @param string $caseUuid The case object uuid/id. + * + * @return ObjectEntity|null The case, or null when absent or unauthorised. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md + */ + public function load(string $caseUuid): ?ObjectEntity + { + return $this->objectService->find( + id: $caseUuid, + register: self::REGISTER_SLUG, + schema: self::SCHEMA_SLUG, + _rbac: true, + _multitenancy: true + ); + }//end load() + + /** + * Load every case object in the register/schema as ObjectEntity instances. + * + * Used by the retention sweep, which needs the entities (not rendered + * arrays) so it can consult RetentionService legal-hold / immutability on + * each. Queries via ObjectService (RBAC + tenant scoped), then re-loads each + * result by uuid so the sweep operates on live entities. + * + * @return ObjectEntity[] The case entities (possibly empty). + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-retention-sweep/spec.md + */ + public function findAllCaseEntities(): array + { + $rendered = $this->objectService->findAll( + config: [ + 'filters' => [ + 'register' => self::REGISTER_SLUG, + 'schema' => self::SCHEMA_SLUG, + ], + ], + _rbac: true, + _multitenancy: true + ); + + $entities = []; + foreach ($rendered as $row) { + $uuid = ''; + if (is_array($row) === true) { + $uuid = (string) ($row['@self']['uuid'] ?? ($row['id'] ?? '')); + } + + if ($uuid === '') { + continue; + } + + $entity = $this->load(caseUuid: $uuid); + if ($entity !== null) { + $entities[] = $entity; + } + } + + return $entities; + }//end findAllCaseEntities() + + /** + * Delete a case dossier via ObjectService, flagged as a retention sweep. + * + * The retention-sweep flag lets the delete pass the archival-annotation + * guard that rejects user-driven deletes; the delete is still RBAC + tenant + * scoped and audited. + * + * @param string $caseUuid The case object uuid. + * + * @return bool True when the dossier was deleted. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-retention-sweep/spec.md + */ + public function deleteForSweep(string $caseUuid): bool + { + return $this->objectService->deleteObject( + uuid: $caseUuid, + register: self::REGISTER_SLUG, + schema: self::SCHEMA_SLUG, + _rbac: true, + _multitenancy: true, + _retentionSweep: true + ); + }//end deleteForSweep() + + /** + * Persist a mutated case, pinned to the DSAR processing activity so the + * immutable audit trail records the write under that activity. + * + * @param ObjectEntity $case The loaded case entity. + * @param array $data The full replacement payload for the case. + * + * @return ObjectEntity The saved case. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md + */ + public function save(ObjectEntity $case, array $data): ObjectEntity + { + $case->setObject($data); + + $activity = $this->dsarService->getDsarProcessingActivityUuid(); + if ($activity !== null) { + $case->setProcessingActivityId($activity); + } + + return $this->objectService->saveObject( + object: $case, + register: self::REGISTER_SLUG, + schema: self::SCHEMA_SLUG, + uuid: $case->getUuid(), + _rbac: true, + _multitenancy: true + ); + }//end save() +}//end class diff --git a/lib/Service/Gdpr/DataSubjectDeadline.php b/lib/Service/Gdpr/DataSubjectDeadline.php new file mode 100644 index 0000000000..60ae242138 --- /dev/null +++ b/lib/Service/Gdpr/DataSubjectDeadline.php @@ -0,0 +1,165 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr; + +use DateInterval; +use DateTimeImmutable; +use DateTimeInterface; + +/** + * EU art-12(3) data-subject-request deadline maths. + */ +class DataSubjectDeadline +{ + + /** + * Base response term: one month from receipt (GDPR art-12(3)). + * + * @var string + */ + public const BASE_TERM = 'P1M'; + + /** + * Single permitted extension: a further two months (art-12(3)). + * + * @var string + */ + public const EXTENSION_TERM = 'P2M'; + + /** + * Compute the base due date: receivedAt + one month. + * + * @param DateTimeInterface $receivedAt When the request was received. + * + * @return DateTimeImmutable The base legal deadline. + * + * @spec openspec/specs/gdpr-data-subject-rights/spec.md#requirement-eu-art-12-legal-deadline-computation + */ + public function computeDueAt(DateTimeInterface $receivedAt): DateTimeImmutable + { + return $this->toImmutable(value: $receivedAt)->add(new DateInterval(self::BASE_TERM)); + + }//end computeDueAt() + + /** + * Extend the deadline once by two months. + * + * The extension is anchored on the supplied base due date (the result + * of `computeDueAt`), NOT on "now", so a late-granted extension still + * yields base + two months as the law intends. Callers MUST enforce + * the "only once" rule by tracking whether an extension was already + * granted (e.g. an `extendedUntil` field already set) — this helper + * only performs the arithmetic. + * + * @param DateTimeInterface $dueAt The current base due date. + * + * @return DateTimeImmutable The extended deadline (base + 2 months). + * + * @spec openspec/specs/gdpr-data-subject-rights/spec.md#requirement-eu-art-12-legal-deadline-computation + */ + public function extend(DateTimeInterface $dueAt): DateTimeImmutable + { + return $this->toImmutable(value: $dueAt)->add(new DateInterval(self::EXTENSION_TERM)); + + }//end extend() + + /** + * Whether the deadline has passed relative to a reference time. + * + * @param DateTimeInterface $deadline The (possibly extended) deadline. + * @param DateTimeInterface|null $now Reference time (defaults to now). + * + * @return bool True when the reference time is at or after the deadline. + * + * @spec openspec/specs/gdpr-data-subject-rights/spec.md#requirement-eu-art-12-legal-deadline-computation + */ + public function isOverdue(DateTimeInterface $deadline, ?DateTimeInterface $now=null): bool + { + $reference = ($now ?? new DateTimeImmutable()); + return $this->toImmutable(value: $reference) >= $this->toImmutable(value: $deadline); + + }//end isOverdue() + + /** + * Whole days remaining until the deadline (negative once breached). + * + * @param DateTimeInterface $deadline The (possibly extended) deadline. + * @param DateTimeInterface|null $now Reference time (defaults to now). + * + * @return int Whole days left; negative when the deadline is in the past. + * + * @spec openspec/specs/gdpr-data-subject-rights/spec.md#requirement-eu-art-12-legal-deadline-computation + */ + public function daysRemaining(DateTimeInterface $deadline, ?DateTimeInterface $now=null): int + { + $reference = $this->toImmutable(value: ($now ?? new DateTimeImmutable())); + $target = $this->toImmutable(value: $deadline); + + $diff = $reference->diff($target); + $days = (int) $diff->days; + if ($diff->invert === 1) { + return ($days * -1); + } + + return $days; + + }//end daysRemaining() + + /** + * Normalise any DateTimeInterface to an immutable value. + * + * @param DateTimeInterface $value Input date. + * + * @return DateTimeImmutable + * + * @SuppressWarnings(PHPMD.StaticAccess) DateTimeImmutable::createFromInterface is a standard named constructor — no DI alternative. + */ + private function toImmutable(DateTimeInterface $value): DateTimeImmutable + { + if ($value instanceof DateTimeImmutable) { + return $value; + } + + return DateTimeImmutable::createFromInterface($value); + + }//end toImmutable() +}//end class diff --git a/lib/Service/Gdpr/DataSubjectRequestService.php b/lib/Service/Gdpr/DataSubjectRequestService.php new file mode 100644 index 0000000000..a6ddafbd9f --- /dev/null +++ b/lib/Service/Gdpr/DataSubjectRequestService.php @@ -0,0 +1,823 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr; + +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use OCA\OpenRegister\Db\MagicMapper; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Service\DsarService; +use OCA\OpenRegister\Service\ObjectService; +use OCA\OpenRegister\Service\RetentionService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IDBConnection; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Consumable, RBAC + tenant scoped data-subject-request orchestrator. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Covers all six data-subject rights behind one surface. + */ +class DataSubjectRequestService +{ + + /** + * Erase mode: pseudonymise the subject's matching field values in place. + * + * @var string + */ + public const ERASE_MODE_PSEUDONYMISE = 'pseudonymise'; + + /** + * Erase mode: soft-delete the whole owning object. + * + * @var string + */ + public const ERASE_MODE_WHOLE_OBJECT = 'whole-object'; + + /** + * Replacement token written into a pseudonymised field value. + * + * @var string + */ + private const PSEUDONYM_TOKEN = '[erased]'; + + /** + * Constructor. + * + * @param IDBConnection $db GdprEntity + entity_relations join. + * @param MagicMapper $objectMapper Object loader (RBAC/tenant scoped reads). + * @param ObjectService $objectService Audited write path (saveObject → audit trail). + * @param RetentionService $retentionService Legal-hold + immutability guard. + * @param DsarService $dsarService Reused only for the configured DSAR activity uuid. + * @param DataSubjectDeadline $deadline EU art-12 deadline maths. + * @param IUserSession $userSession Current user (erasure metadata). + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly IDBConnection $db, + private readonly MagicMapper $objectMapper, + private readonly ObjectService $objectService, + private readonly RetentionService $retentionService, + private readonly DsarService $dsarService, + private readonly DataSubjectDeadline $deadline, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger + ) { + + }//end __construct() + + /** + * Discover a subject's objects across all registers (art-15 / art-20). + * + * Walks the GdprEntity index for rows whose value matches the subject, + * joins entity_relations to the owning objects, then loads each object + * with RBAC + tenant scoping LEFT ON (unless the caller explicitly opts + * out) so the result is limited to what the caller may read. + * + * Returns a list of envelopes: + * [['object' => , 'gdprEntities' => [{type,value,category,detectedAt}, ...]], ...] + * + * @param string $subjectId Subject identifier value (email, …). + * @param string|null $type Optional GdprEntity type filter. + * @param string $mode `exact` (default) or `ilike`. + * @param bool $rbac Apply RBAC scoping (default true). + * @param bool $multitenancy Apply tenant scoping (default true). + * + * @return array + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function findSubjectData( + string $subjectId, + ?string $type=null, + string $mode='exact', + bool $rbac=true, + bool $multitenancy=true + ): array { + $subjectId = trim($subjectId); + if ($subjectId === '') { + return []; + } + + $grouped = $this->discover(subjectId: $subjectId, type: $type, mode: $mode); + + $envelopes = []; + foreach ($grouped as $entry) { + $object = $this->loadObject(entry: $entry, rbac: $rbac, multitenancy: $multitenancy); + if ($object === null) { + continue; + } + + $envelopes[] = [ + 'object' => $object->jsonSerialize(), + 'gdprEntities' => $entry['gdprEntities'], + ]; + } + + return $envelopes; + + }//end findSubjectData() + + /** + * Assemble a portable access export of the subject's data (art-15 / art-20). + * + * RBAC + tenant scoped (it builds on findSubjectData). Produces a + * serialisable bundle recording, per object, which PII attributes + * triggered the object's inclusion. + * + * @param string $subjectId Subject identifier value. + * @param string|null $type Optional GdprEntity type filter. + * + * @return array{subject: string, type: string|null, generatedAt: string, objectCount: int, objects: array} + */ + public function assembleAccessExport(string $subjectId, ?string $type=null): array + { + $envelopes = $this->findSubjectData(subjectId: $subjectId, type: $type); + + return [ + 'subject' => trim($subjectId), + 'type' => $type, + 'generatedAt' => (new DateTime())->format(DateTime::ATOM), + 'objectCount' => count($envelopes), + 'objects' => $envelopes, + ]; + + }//end assembleAccessExport() + + /** + * Rectify a single object's fields for the subject (art-16). + * + * RBAC + tenant scoped object load. Refuses if the object is in an + * immutable archival status. Pins the write to the DSAR processing + * activity for the audit trail. + * + * @param string $objectIdentifier Object id/uuid. + * @param array $changes Property → new value map. + * + * @return array|null Updated object envelope, or null on miss/refusal. + */ + public function rectify(string $objectIdentifier, array $changes): ?array + { + $object = $this->loadByIdentifier(identifier: $objectIdentifier); + if ($object === null) { + return null; + } + + if ($this->retentionService->validateNotImmutable(object: $object) !== null) { + $this->logger->warning( + message: '[DSR] Rectify refused — object is in an immutable archival status', + context: ['object' => $objectIdentifier] + ); + return null; + } + + $current = ($object->getObject() ?? []); + $object->setObject(array_merge($current, $changes)); + $this->attribute(object: $object); + + return $this->persist(object: $object, op: 'rectify', identifier: $objectIdentifier); + + }//end rectify() + + /** + * Erase the subject's data across matching objects (art-17). + * + * The erase MODE is a parameter, not hard-coded, because apps differ: + * - ERASE_MODE_PSEUDONYMISE: replace the subject's matching field + * values in place (field-level), preserving the object; + * - ERASE_MODE_WHOLE_OBJECT: soft-delete the whole owning object. + * + * Objects under an active legal hold or in an immutable archival status + * are NEVER erased; they are reported in the `held` bucket so the caller + * can surface a partial result rather than a false "complete". + * + * @param string $subjectId Subject identifier value. + * @param string|null $type Optional GdprEntity type filter. + * @param string $eraseMode One of the ERASE_MODE_* constants. + * @param bool $dryRun When true, report matches/holds without mutating. + * + * @return array + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function erase( + string $subjectId, + ?string $type=null, + string $eraseMode=self::ERASE_MODE_PSEUDONYMISE, + bool $dryRun=false + ): array { + if ($eraseMode !== self::ERASE_MODE_WHOLE_OBJECT) { + $eraseMode = self::ERASE_MODE_PSEUDONYMISE; + } + + $subjectId = trim($subjectId); + $grouped = $this->discover(subjectId: $subjectId, type: $type, mode: 'exact'); + + $summary = [ + 'subject' => $subjectId, + 'type' => $type, + 'eraseMode' => $eraseMode, + 'dryRun' => $dryRun, + 'matchedCount' => count($grouped), + 'erased' => [], + 'held' => [], + 'failed' => [], + ]; + + foreach ($grouped as $entry) { + $object = $this->loadObject(entry: $entry, rbac: true, multitenancy: true); + if ($object === null) { + $summary['failed'][] = [ + 'object' => $this->refOf(entry: $entry), + 'error' => 'Object could not be loaded (not found or not authorised)', + ]; + continue; + } + + // Retention guard — never erase a held / immutable object. + $holdReason = $this->retentionGuard(object: $object); + if ($holdReason !== null) { + $summary['held'][] = [ + 'uuid' => $object->getUuid(), + 'reason' => $holdReason, + ]; + continue; + } + + if ($dryRun === true) { + $summary['erased'][] = $this->refOf(entry: $entry, object: $object); + continue; + } + + $this->eraseOne( + object: $object, + subjectId: $subjectId, + matched: $entry['gdprEntities'], + eraseMode: $eraseMode, + summary: $summary + ); + }//end foreach + + $summary['complete'] = ($summary['failed'] === []); + $summary['failedCount'] = count($summary['failed']); + $summary['heldCount'] = count($summary['held']); + + return $summary; + + }//end erase() + + /** + * Flag/unflag processing restriction on a single object (art-18). + * + * Writes a generic `_gdprRestriction` marker into the object payload + * (active flag + reason + timestamp), RBAC/tenant scoped, attributed + * to the DSAR activity. Jurisdiction wording stays in the leaf app. + * + * @param string $objectIdentifier Object id/uuid. + * @param bool $restricted Whether processing is restricted. + * @param string $reason Reason recorded on the marker. + * + * @return array|null Updated object envelope, or null on miss. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function setRestriction(string $objectIdentifier, bool $restricted, string $reason): ?array + { + return $this->setMarker( + objectIdentifier: $objectIdentifier, + key: '_gdprRestriction', + active: $restricted, + reason: $reason, + op: 'restrict' + ); + + }//end setRestriction() + + /** + * Flag/unflag objection on a single object (art-21). + * + * @param string $objectIdentifier Object id/uuid. + * @param bool $objected Whether the subject objects to processing. + * @param string $reason Reason recorded on the marker. + * + * @return array|null Updated object envelope, or null on miss. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function setObjection(string $objectIdentifier, bool $objected, string $reason): ?array + { + return $this->setMarker( + objectIdentifier: $objectIdentifier, + key: '_gdprObjection', + active: $objected, + reason: $reason, + op: 'object' + ); + + }//end setObjection() + + /** + * Compute the EU art-12 base due date (receivedAt + 1 month). + * + * @param DateTimeInterface $receivedAt When the request was received. + * + * @return DateTimeImmutable + */ + public function computeDueAt(DateTimeInterface $receivedAt): DateTimeImmutable + { + return $this->deadline->computeDueAt(receivedAt: $receivedAt); + + }//end computeDueAt() + + /** + * Extend a deadline once by two months (art-12(3)). + * + * @param DateTimeInterface $dueAt The current base due date. + * + * @return DateTimeImmutable + */ + public function extend(DateTimeInterface $dueAt): DateTimeImmutable + { + return $this->deadline->extend(dueAt: $dueAt); + + }//end extend() + + /** + * Whether a deadline has passed. + * + * @param DateTimeInterface $deadline The (possibly extended) deadline. + * @param DateTimeInterface|null $now Reference time (defaults to now). + * + * @return bool + */ + public function isOverdue(DateTimeInterface $deadline, ?DateTimeInterface $now=null): bool + { + return $this->deadline->isOverdue(deadline: $deadline, now: $now); + + }//end isOverdue() + + /** + * Erase one object per the selected mode, recording the result. + * + * @param ObjectEntity $object Object to erase. + * @param string $subjectId Subject value (for pseudonymise match + audit). + * @param array $matched GdprEntity hits that triggered inclusion. + * @param string $eraseMode Erase mode constant. + * @param array $summary Summary (mutated by reference). + * + * @return void + */ + private function eraseOne( + ObjectEntity $object, + string $subjectId, + array $matched, + string $eraseMode, + array &$summary + ): void { + if ($eraseMode === self::ERASE_MODE_WHOLE_OBJECT) { + $object->setDeleted( + [ + 'deletedBy' => $this->currentUserId(), + 'deletedReason' => 'gdpr-erasure', + 'deleted' => (new DateTime())->format(DateTime::ATOM), + 'subject' => $subjectId, + ] + ); + } + + if ($eraseMode === self::ERASE_MODE_PSEUDONYMISE) { + $this->pseudonymise(object: $object, subjectId: $subjectId, matched: $matched); + } + + $this->attribute(object: $object); + + $ref = $this->refOf(entry: [], object: $object); + try { + $this->objectService->saveObject( + object: $object, + register: $object->getRegister(), + schema: $object->getSchema(), + _rbac: true, + _multitenancy: true + ); + $summary['erased'][] = $ref; + } catch (\Throwable $e) { + $summary['failed'][] = ['object' => $ref, 'error' => $e->getMessage()]; + $this->logger->warning( + message: '[DSR] Erase failed', + context: ['object' => $ref, 'error' => $e->getMessage()] + ); + } + + }//end eraseOne() + + /** + * Replace the subject's matching field values in place (pseudonymise). + * + * Walks the object payload and, for every scalar value equal to the + * subject id or to any matched GdprEntity value, substitutes the + * pseudonym token. Deterministic and reversible only via the audit log. + * + * @param ObjectEntity $object Object whose payload is mutated. + * @param string $subjectId Subject identifier value. + * @param array $matched GdprEntity hits (each has a `value`). + * + * @return void + */ + private function pseudonymise(ObjectEntity $object, string $subjectId, array $matched): void + { + $needles = [strtolower($subjectId)]; + foreach ($matched as $hit) { + $value = strtolower((string) ($hit['value'] ?? '')); + if ($value !== '') { + $needles[] = $value; + } + } + + $needles = array_values(array_unique($needles)); + $payload = ($object->getObject() ?? []); + $object->setObject($this->scrub(data: $payload, needles: $needles)); + + }//end pseudonymise() + + /** + * Recursively scrub matching scalar values from a payload. + * + * @param array $data Payload (or sub-array). + * @param array $needles Lower-cased values to redact. + * + * @return array + */ + private function scrub(array $data, array $needles): array + { + foreach ($data as $key => $value) { + if (is_array($value) === true) { + $data[$key] = $this->scrub(data: $value, needles: $needles); + continue; + } + + if (is_string($value) === true && in_array(strtolower($value), $needles, true) === true) { + $data[$key] = self::PSEUDONYM_TOKEN; + } + } + + return $data; + + }//end scrub() + + /** + * Return a hold/immutability reason that BLOCKS erasure, or null. + * + * @param ObjectEntity $object Object to check. + * + * @return string|null `legal-hold`, an immutable status code, or null. + */ + private function retentionGuard(ObjectEntity $object): ?string + { + if ($this->retentionService->hasActiveLegalHold(object: $object) === true) { + return 'legal-hold'; + } + + $immutable = $this->retentionService->validateNotImmutable(object: $object); + if ($immutable !== null) { + return $immutable; + } + + return null; + + }//end retentionGuard() + + /** + * Set a generic boolean marker (with reason + timestamp) on an object. + * + * @param string $objectIdentifier Object id/uuid. + * @param string $key Payload key for the marker. + * @param bool $active Whether the marker is active. + * @param string $reason Reason recorded on the marker. + * @param string $op Operation label for logging. + * + * @return array|null + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + private function setMarker(string $objectIdentifier, string $key, bool $active, string $reason, string $op): ?array + { + $object = $this->loadByIdentifier(identifier: $objectIdentifier); + if ($object === null) { + return null; + } + + if ($this->retentionService->validateNotImmutable(object: $object) !== null) { + return null; + } + + $payload = ($object->getObject() ?? []); + $payload[$key] = [ + 'active' => $active, + 'reason' => $reason, + 'setBy' => $this->currentUserId(), + 'setAt' => (new DateTime())->format(DateTime::ATOM), + ]; + $object->setObject($payload); + $this->attribute(object: $object); + + return $this->persist(object: $object, op: $op, identifier: $objectIdentifier); + + }//end setMarker() + + /** + * Run the GdprEntity ⋈ entity_relations join, grouped by owning object. + * + * Mirrors the join shape used by DsarService but is consumed under + * RBAC/tenant scoping at object-load time. LIKE wildcards in the + * subject are escaped before anchoring. + * + * @param string $subjectId Subject value. + * @param string|null $type Optional GdprEntity type filter. + * @param string $mode `exact` or `ilike`. + * + * @return array + */ + private function discover(string $subjectId, ?string $type, string $mode): array + { + $hits = $this->matchEntities(subject: $subjectId, type: $type, mode: $mode); + + $grouped = []; + foreach ($hits as $hit) { + $uuid = (string) ($hit['object_uuid'] ?? ''); + $id = (int) ($hit['object_id'] ?? 0); + $key = 'id:'.$id; + if ($uuid !== '') { + $key = 'uuid:'.$uuid; + } + + if ($uuid === '' && $id <= 0) { + continue; + } + + $grouped[$key] ??= [ + 'object_id' => $id, + 'object_uuid' => $uuid, + 'gdprEntities' => [], + ]; + + $grouped[$key]['gdprEntities'][] = [ + 'type' => (string) ($hit['type'] ?? ''), + 'value' => (string) ($hit['value'] ?? ''), + 'category' => (string) ($hit['category'] ?? ''), + 'detectedAt' => (string) ($hit['detected_at'] ?? ''), + ]; + }//end foreach + + return $grouped; + + }//end discover() + + /** + * The GdprEntity index lookup (one row per entity/object pair). + * + * @param string $subject Subject value. + * @param string|null $type Optional type filter. + * @param string $mode `exact` or `ilike`. + * + * @return array> + */ + private function matchEntities(string $subject, ?string $type, string $mode): array + { + try { + $qb = $this->db->getQueryBuilder(); + $qb->selectDistinct( + [ + 'e.id', + 'e.type', + 'e.value', + 'e.category', + 'e.detected_at', + 'r.object_id', + 'r.object_uuid', + ] + ) + ->from('openregister_entities', 'e') + ->innerJoin('e', 'openregister_entity_relations', 'r', $qb->expr()->eq('r.entity_id', 'e.id')) + ->where($qb->expr()->isNotNull('r.object_id')); + + $escaped = $this->db->escapeLikeParameter($subject); + $pattern = $escaped; + if ($mode === 'ilike') { + $pattern = '%'.$escaped.'%'; + } + + $qb->andWhere($qb->expr()->iLike('e.value', $qb->createNamedParameter($pattern))); + + if ($type !== null && $type !== '') { + $qb->andWhere($qb->expr()->eq('e.type', $qb->createNamedParameter($type))); + } + + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + return $rows; + } catch (\Throwable $e) { + $this->logger->warning( + message: '[DSR] GdprEntity lookup failed', + context: ['subject' => $subject, 'type' => $type, 'error' => $e->getMessage()] + ); + return []; + }//end try + + }//end matchEntities() + + /** + * Load the owning object for a grouped hit, RBAC/tenant scoped. + * + * @param array{object_id: int, object_uuid: string} $entry Grouped entry. + * @param bool $rbac Apply RBAC. + * @param bool $multitenancy Apply tenant scope. + * + * @return ObjectEntity|null + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + private function loadObject(array $entry, bool $rbac, bool $multitenancy): ?ObjectEntity + { + $uuid = (string) $entry['object_uuid']; + $id = (int) $entry['object_id']; + + $identifier = $id; + if ($uuid !== '') { + $identifier = $uuid; + } + + if ($identifier === 0) { + return null; + } + + try { + return $this->objectMapper->find( + $identifier, + _rbac: $rbac, + _multitenancy: $multitenancy + ); + } catch (DoesNotExistException $e) { + return null; + } catch (\Throwable $e) { + $this->logger->debug( + message: '[DSR] Object load skipped (not found or not authorised)', + context: ['identifier' => $identifier, 'error' => $e->getMessage()] + ); + return null; + }//end try + + }//end loadObject() + + /** + * Load a single object by id/uuid, RBAC/tenant scoped. + * + * @param string $identifier Object id or uuid. + * + * @return ObjectEntity|null + */ + private function loadByIdentifier(string $identifier): ?ObjectEntity + { + try { + return $this->objectMapper->find($identifier, _rbac: true, _multitenancy: true); + } catch (\Throwable $e) { + return null; + } + + }//end loadByIdentifier() + + /** + * Pin the write to the configured DSAR processing activity for the audit. + * + * @param ObjectEntity $object Object about to be persisted. + * + * @return void + */ + private function attribute(ObjectEntity $object): void + { + $activity = $this->dsarService->getDsarProcessingActivityUuid(); + if ($activity !== null) { + $object->setProcessingActivityId($activity); + } + + }//end attribute() + + /** + * Persist an object update and return its serialised envelope, or null. + * + * @param ObjectEntity $object Object to persist. + * @param string $op Operation label for logging. + * @param string $identifier Identifier for logging. + * + * @return array|null + */ + private function persist(ObjectEntity $object, string $op, string $identifier): ?array + { + try { + $saved = $this->objectService->saveObject( + object: $object, + register: $object->getRegister(), + schema: $object->getSchema(), + _rbac: true, + _multitenancy: true + ); + return $saved->jsonSerialize(); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[DSR] '.$op.' update failed', + context: ['object' => $identifier, 'error' => $e->getMessage()] + ); + return null; + } + + }//end persist() + + /** + * Build a compact object reference for a summary row. + * + * @param array $entry Grouped entry (for fallback ids). + * @param ObjectEntity|null $object Loaded object (preferred). + * + * @return array + */ + private function refOf(array $entry, ?ObjectEntity $object=null): array + { + if ($object !== null) { + return [ + 'uuid' => $object->getUuid(), + 'register' => $object->getRegister(), + 'schema' => $object->getSchema(), + ]; + } + + return [ + 'object_id' => ($entry['object_id'] ?? 0), + 'object_uuid' => ($entry['object_uuid'] ?? ''), + ]; + + }//end refOf() + + /** + * Current acting user id, or `system`. + * + * @return string + */ + private function currentUserId(): string + { + return ($this->userSession->getUser()?->getUID() ?? 'system'); + + }//end currentUserId() +}//end class diff --git a/lib/Service/Gdpr/DpiaPatternDetectionService.php b/lib/Service/Gdpr/DpiaPatternDetectionService.php new file mode 100644 index 0000000000..14f84dd2e5 --- /dev/null +++ b/lib/Service/Gdpr/DpiaPatternDetectionService.php @@ -0,0 +1,227 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr; + +use DateTimeImmutable; + +/** + * Group + threshold DSAR cases for DPIA pattern detection. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Requirement: DPIA pattern detection over DSAR cases) + */ +class DpiaPatternDetectionService +{ + + /** + * Default threshold, matching pipelinq's `DpiaDetectionService::DEFAULT_THRESHOLD`. + * + * @var int + */ + public const DEFAULT_THRESHOLD = 10; + + /** + * Default rolling window, matching pipelinq's `DpiaDetectionService::WINDOW_DAYS`. + * + * @var int + */ + public const DEFAULT_WINDOW_DAYS = 30; + + /** + * Default grouping characteristics (request type + normalised scope). + * + * @var array + */ + public const DEFAULT_GROUP_BY = ['type', 'scope']; + + /** + * Detect the case groups that require a DPIA flag. + * + * Groups every case whose `receivedAt` lies inside the rolling window by + * the configured characteristics (values normalised: trim, lowercase, + * collapse whitespace — deliberately dumb v1 parity with pipelinq's raw + * equality) and returns each group whose member count reaches the + * threshold. Already-flagged cases COUNT toward their group (detection + * reflects real volume) but are listed separately so the caller never + * re-writes them (idempotency). + * + * @param array $cases Case payloads (each element is validated as an array at + * runtime); each needs `receivedAt` + the group-by fields + + * optionally `dpiaRequired` and a caller-supplied `@uuid`. + * @param array $config The pack's `dpiaDetection` block (threshold, windowDays, groupBy). + * @param DateTimeImmutable $now The evaluation clock. + * + * @return array, unflaggedUuids: array}> Triggering groups. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Scenario: Threshold crossing flags the group) + */ + public function detect(array $cases, array $config, DateTimeImmutable $now): array + { + $threshold = (int) ($config['threshold'] ?? 0); + $windowDays = (int) ($config['windowDays'] ?? 0); + $groupBy = ($config['groupBy'] ?? null); + + // Fail-safe: an unusable configuration produces no groups — never a + // false DPIA flag (no-pack / no-block callers pass an empty config). + if ($threshold < 1 || $windowDays < 1) { + return []; + } + + if (is_array($groupBy) === false || $groupBy === []) { + $groupBy = self::DEFAULT_GROUP_BY; + } + + $windowStart = $now->modify(sprintf('-%d days', $windowDays)); + + $groups = []; + foreach ($cases as $case) { + if (is_array($case) === false) { + continue; + } + + $receivedAt = $this->parseDate(value: ($case['receivedAt'] ?? null)); + if ($receivedAt === null || $receivedAt < $windowStart || $receivedAt > $now) { + continue; + } + + $key = $this->groupKey(case: $case, groupBy: $groupBy); + + if (isset($groups[$key]) === false) { + $groups[$key] = [ + 'key' => $key, + 'count' => 0, + 'caseUuids' => [], + 'unflaggedUuids' => [], + ]; + } + + $groups[$key]['count']++; + + $uuid = (string) ($case['@uuid'] ?? ($case['id'] ?? '')); + if ($uuid === '') { + continue; + } + + $groups[$key]['caseUuids'][] = $uuid; + if (($case['dpiaRequired'] ?? false) !== true) { + $groups[$key]['unflaggedUuids'][] = $uuid; + } + }//end foreach + + $triggering = []; + foreach ($groups as $group) { + if ($group['count'] >= $threshold) { + $triggering[] = $group; + } + } + + return $triggering; + + }//end detect() + + /** + * Build a group key from the configured characteristics, each value + * normalised (trim / lowercase / collapse whitespace). + * + * @param array $case The case payload. + * @param array $groupBy The grouping field names. + * + * @return string The composite group key (`field=value|field=value`). + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Requirement: DPIA pattern detection over DSAR cases) + */ + public function groupKey(array $case, array $groupBy): string + { + $parts = []; + foreach ($groupBy as $field) { + $field = (string) $field; + $value = ($case[$field] ?? ''); + $parts[] = $field.'='.$this->normalise(value: $value); + } + + return implode('|', $parts); + + }//end groupKey() + + /** + * Normalise a grouping value: trim, lowercase, collapse whitespace. + * Non-scalar values normalise to '' (they group together, never crash). + * + * @param mixed $value The raw field value. + * + * @return string The normalised value. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Requirement: DPIA pattern detection over DSAR cases) + */ + public function normalise(mixed $value): string + { + if (is_scalar($value) === false) { + return ''; + } + + $normalised = mb_strtolower(trim((string) $value)); + + return (string) preg_replace('/\s+/', ' ', $normalised); + + }//end normalise() + + /** + * Parse a date-time value defensively, null on anything unusable. + * + * @param mixed $value The raw `receivedAt` value. + * + * @return DateTimeImmutable|null The parsed instant, or null. + */ + private function parseDate(mixed $value): ?DateTimeImmutable + { + if (is_string($value) === false || $value === '') { + return null; + } + + try { + return new DateTimeImmutable($value); + } catch (\Exception $e) { + return null; + } + + }//end parseDate() +}//end class diff --git a/lib/Service/Gdpr/Evidence/EvidenceHarvestService.php b/lib/Service/Gdpr/Evidence/EvidenceHarvestService.php new file mode 100644 index 0000000000..8af23f25f9 --- /dev/null +++ b/lib/Service/Gdpr/Evidence/EvidenceHarvestService.php @@ -0,0 +1,160 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Evidence; + +use OCA\OpenRegister\Service\Gdpr\Case\CaseObjectAccessor; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +/** + * Harvests + dedups evidence onto a case. + */ +class EvidenceHarvestService +{ + /** + * Constructor. + * + * @param EvidenceSourceRegistry $registry The provider registry (registered sources only). + * @param CaseObjectAccessor $accessor RBAC-scoped, audited case load/save. + * @param LoggerInterface $logger Logger for per-source diagnostics. + */ + public function __construct( + private readonly EvidenceSourceRegistry $registry, + private readonly CaseObjectAccessor $accessor, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Harvest evidence for a case and persist deduplicated items onto it. + * + * Enumerates registered, enabled providers; asks each to harvest for the + * case; and appends any item whose `contentHash` is not already present on + * the case's `evidence` sub-collection. Items with an already-present hash + * are skipped (idempotent re-runs). The mutated case is saved once, audited. + * + * @param string $caseUuid The case object uuid. + * + * @return array{caseUuid: string, evaluated: int, appended: int, skipped: int, providers: array} + * + * @throws RuntimeException When the case cannot be loaded (absent or unauthorised). + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Linear enumerate→dedup→append loop; per-provider/per-item guards. + * @SuppressWarnings(PHPMD.NPathComplexity) Same loop; path count inflated by nested per-item guards. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function harvest(string $caseUuid): array + { + $case = $this->accessor->load(caseUuid: $caseUuid); + if ($case === null) { + throw new RuntimeException( + message: sprintf('Case "%s" not found or not authorised.', $caseUuid) + ); + } + + $data = $case->getObject(); + $evidence = []; + if (isset($data['evidence']) === true && is_array($data['evidence']) === true) { + $evidence = array_values($data['evidence']); + } + + // Index existing content hashes so re-runs never duplicate an item. + $seenHashes = []; + foreach ($evidence as $existing) { + if (is_array($existing) === true && isset($existing['contentHash']) === true) { + $seenHashes[(string) $existing['contentHash']] = true; + } + } + + $evaluated = 0; + $appended = 0; + $skipped = 0; + $providerIds = []; + + foreach ($this->registry->list() as $provider) { + if ($provider->isEnabled() === false) { + continue; + } + + $providerIds[] = $provider->getSourceId(); + + try { + $items = $provider->harvest(caseUuid: $caseUuid, case: $data); + } catch (Throwable $e) { + // A failing source is visible + re-runnable: it contributes no + // items this pass, but does not abort the whole harvest. + $this->logger->warning( + message: sprintf( + '[EvidenceHarvestService] provider "%s" failed for case "%s": %s', + $provider->getSourceId(), + $caseUuid, + $e->getMessage() + ) + ); + continue; + } + + foreach ($items as $item) { + $evaluated++; + $hash = $item->getContentHash(); + + if ($hash === '' || isset($seenHashes[$hash]) === true) { + $skipped++; + continue; + } + + $seenHashes[$hash] = true; + $evidence[] = $item->toEvidenceRecord(); + $appended++; + }//end foreach + }//end foreach + + // Persist once (a single audited write) only when something changed. + if ($appended > 0) { + $data['evidence'] = $evidence; + $this->accessor->save(case: $case, data: $data); + } + + return [ + 'caseUuid' => $caseUuid, + 'evaluated' => $evaluated, + 'appended' => $appended, + 'skipped' => $skipped, + 'providers' => $providerIds, + ]; + }//end harvest() +}//end class diff --git a/lib/Service/Gdpr/Evidence/EvidenceItem.php b/lib/Service/Gdpr/Evidence/EvidenceItem.php new file mode 100644 index 0000000000..f7d1583785 --- /dev/null +++ b/lib/Service/Gdpr/Evidence/EvidenceItem.php @@ -0,0 +1,139 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Evidence; + +/** + * A single harvested evidence item. + */ +final class EvidenceItem +{ + + /** + * Per-item status: the item was collected successfully. + * + * @var string + */ + public const STATUS_COLLECTED = 'collected'; + + /** + * Per-item status: the item is known but not yet collected (async). + * + * @var string + */ + public const STATUS_PENDING = 'pending'; + + /** + * Per-item status: the source failed to return the item. + * + * @var string + */ + public const STATUS_FAILED = 'failed'; + + /** + * Constructor. + * + * @param string $sourceId Identifier of the source/provider the item came from. + * @param string $contentHash Content hash used for deduplication (e.g. `sha256:...`). + * @param string $status Per-item collection status. + * @param array $payload Optional item payload for dossier assembly. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function __construct( + private readonly string $sourceId, + private readonly string $contentHash, + private readonly string $status=self::STATUS_COLLECTED, + private readonly array $payload=[] + ) { + }//end __construct() + + /** + * The source/provider id this item was harvested from. + * + * @return string + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function getSourceId(): string + { + return $this->sourceId; + }//end getSourceId() + + /** + * The content hash used to deduplicate the item across sources. + * + * @return string + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function getContentHash(): string + { + return $this->contentHash; + }//end getContentHash() + + /** + * The per-item collection status. + * + * @return string + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function getStatus(): string + { + return $this->status; + }//end getStatus() + + /** + * Optional item payload retained for dossier assembly. + * + * @return array + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function getPayload(): array + { + return $this->payload; + }//end getPayload() + + /** + * Serialise into the head's declared `evidence` sub-collection item shape. + * + * @return array{sourceId: string, contentHash: string, status: string} + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function toEvidenceRecord(): array + { + return [ + 'sourceId' => $this->sourceId, + 'contentHash' => $this->contentHash, + 'status' => $this->status, + ]; + }//end toEvidenceRecord() +}//end class diff --git a/lib/Service/Gdpr/Evidence/EvidenceSourceProvider.php b/lib/Service/Gdpr/Evidence/EvidenceSourceProvider.php new file mode 100644 index 0000000000..5d40d8efa8 --- /dev/null +++ b/lib/Service/Gdpr/Evidence/EvidenceSourceProvider.php @@ -0,0 +1,79 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Evidence; + +/** + * A registerable source of evidence items for a case. + */ +interface EvidenceSourceProvider +{ + /** + * Stable source id recorded on every evidence item this provider harvests. + * + * @return string The provider id (e.g. `openconnector-crm`). + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function getSourceId(): string; + + /** + * Whether this provider can harvest on this instance right now (e.g. the + * backing app is installed and reachable). A disabled provider is skipped + * by the harvest service. + * + * @return bool True when the provider is usable. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function isEnabled(): bool; + + /** + * Harvest evidence items for a data-subject-request case. + * + * The provider is handed the case's serialised payload (subject identifier, + * request type, handler, existing evidence, …) so it can decide what to + * collect. Each returned item MUST carry a stable `contentHash` so the + * harvest service can deduplicate idempotently across re-runs, and a + * per-item `status` so a slow/failed source is visible on the case. + * + * @param string $caseUuid The case object uuid. + * @param array $case The case's serialised payload. + * + * @return EvidenceItem[] The harvested items (possibly empty). + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function harvest(string $caseUuid, array $case): array; +}//end interface diff --git a/lib/Service/Gdpr/Evidence/EvidenceSourceRegistry.php b/lib/Service/Gdpr/Evidence/EvidenceSourceRegistry.php new file mode 100644 index 0000000000..d25ecb11f5 --- /dev/null +++ b/lib/Service/Gdpr/Evidence/EvidenceSourceRegistry.php @@ -0,0 +1,143 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Evidence; + +use Psr\Log\LoggerInterface; + +/** + * Registry of all EvidenceSourceProvider implementations on this instance. + */ +class EvidenceSourceRegistry +{ + + /** + * Registered providers, keyed by source id. + * + * @var array + */ + private array $providers = []; + + /** + * Constructor. + * + * @param LoggerInterface $logger Logger for collision warnings. + * + * @return void + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Register a provider with the registry. + * + * Duplicate id: first registration wins, the second logs a warning. + * + * @param EvidenceSourceProvider $provider The provider to register. + * + * @return bool True when accepted, false when rejected (duplicate id). + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function addProvider(EvidenceSourceProvider $provider): bool + { + $id = $provider->getSourceId(); + if (isset($this->providers[$id]) === true) { + $this->logger->warning( + sprintf( + '[EvidenceSourceRegistry] duplicate source id "%s" — keeping first registration', + $id + ) + ); + return false; + } + + $this->providers[$id] = $provider; + return true; + }//end addProvider() + + /** + * Replace the entire provider set in one call (test seam). + * + * @param array $providers Provider instances. + * + * @return void + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function withProviders(array $providers): void + { + $this->providers = []; + foreach ($providers as $provider) { + $this->addProvider(provider: $provider); + } + }//end withProviders() + + /** + * Look up a provider by source id. + * + * @param string $id The source id. + * + * @return EvidenceSourceProvider|null Provider, or null when unregistered. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function get(string $id): ?EvidenceSourceProvider + { + return $this->providers[$id] ?? null; + }//end get() + + /** + * List every registered provider, irrespective of isEnabled(). + * + * @return array + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function list(): array + { + return array_values($this->providers); + }//end list() + + /** + * Return the source ids of every registered provider. + * + * @return array + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md + */ + public function listIds(): array + { + return array_keys($this->providers); + }//end listIds() +}//end class diff --git a/lib/Service/Gdpr/Export/ExportBundleService.php b/lib/Service/Gdpr/Export/ExportBundleService.php new file mode 100644 index 0000000000..b7dc1c5b1c --- /dev/null +++ b/lib/Service/Gdpr/Export/ExportBundleService.php @@ -0,0 +1,309 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Export; + +use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Service\Gdpr\Case\CaseObjectAccessor; +use OCA\OpenRegister\Service\Gdpr\DataSubjectRequestService; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +/** + * Assembles + signs export bundles and regulator dossiers for a case. + */ +class ExportBundleService +{ + /** + * Constructor. + * + * @param DataSubjectRequestService $dsrService Reused access-export assembler (ADR-011). + * @param CaseObjectAccessor $accessor RBAC-scoped, audited case load/save. + * @param PadesSigner $signer Swappable signing seam. + * @param OneTimeDownloadTokenStore $tokenStore Single-use download-token store. + * @param AuditTrailMapper $auditTrailMapper Immutable trail reader for the dossier history. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly DataSubjectRequestService $dsrService, + private readonly CaseObjectAccessor $accessor, + // TODO(ADR-047 Phase-1b, DEFERRED): PAdES-LTV signing deferred — the + // 2026-07-04 tc-lib-pdf spike was No-Go (8.65 stubs the B-T + // timestamp). Interim = SHA-256 hash-only (UnsignedPadesSigner). + // When resumed, implement a real PadesSigner against pyHanko (MIT + // sidecar, real B-LTA) or a matured tc-lib-pdf; configurable + // RFC-3161 TSA URL. + private readonly PadesSigner $signer, + private readonly OneTimeDownloadTokenStore $tokenStore, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Generate the signed export bundle for a case and mint a one-time token. + * + * Assembles the subject data via the existing access-export primitive, + * renders it to PDF bytes, signs (or stub-signs) them, records the + * generation on the case (audited), and returns the signed bundle + a raw + * one-time download token bound to the case. + * + * @param string $caseUuid The case object uuid. + * + * @return array{caseUuid: string, contentHash: string, signed: bool, signatureState: string, downloadToken: string} + * + * @throws RuntimeException When the case cannot be loaded (absent or unauthorised). + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function generate(string $caseUuid): array + { + $case = $this->accessor->load(caseUuid: $caseUuid); + if ($case === null) { + throw new RuntimeException( + message: sprintf('Case "%s" not found or not authorised.', $caseUuid) + ); + } + + $data = $case->getObject(); + $subjectId = (string) ($data['subjectId'] ?? ''); + $type = null; + if (isset($data['subjectType']) === true && $data['subjectType'] !== '') { + $type = (string) $data['subjectType']; + } + + // Reuse the existing RBAC-scoped access-export assembler — no + // re-implementation of subject-data discovery/assembly (ADR-011). + $assembled = $this->dsrService->assembleAccessExport( + subjectId: $subjectId, + type: $type + ); + + $pdfBytes = $this->renderPdf(caseUuid: $caseUuid, assembled: $assembled); + $signed = $this->signer->sign(bytes: $pdfBytes); + + // Record the generation on the case (audited via the accessor's + // processing-activity pin). A monotonically-growing marker on the case + // records the last bundle's hash + signature state. + $data['lastBundle'] = [ + 'contentHash' => $signed->getContentHash(), + 'signed' => $signed->isSigned(), + 'signatureState' => $signed->getSignatureState(), + ]; + $this->accessor->save(case: $case, data: $data); + + $token = $this->tokenStore->mint(caseUuid: $caseUuid); + + return [ + 'caseUuid' => $caseUuid, + 'contentHash' => $signed->getContentHash(), + 'signed' => $signed->isSigned(), + 'signatureState' => $signed->getSignatureState(), + 'downloadToken' => $token, + ]; + }//end generate() + + /** + * Redeem a one-time token and return the signed bundle bytes for download. + * + * The token is verified + BURNED (single-use) against the case scope; a + * replay is refused. On success the bundle is re-assembled and re-signed + * from the same RBAC-scoped case so the returned bytes match the recorded + * hash. Returns null when the token is invalid/expired/replayed or the case + * is not authorised (the caller maps null to a refusal). + * + * @param string $caseUuid The case the download is scoped to. + * @param string $token The raw one-time token. + * + * @return SignedBundle|null The signed bundle, or null when refused. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function download(string $caseUuid, string $token): ?SignedBundle + { + // Case scope is enforced twice: the token is bound to the case, and the + // case is loaded under the caller's RBAC. Both must pass. + $case = $this->accessor->load(caseUuid: $caseUuid); + if ($case === null) { + return null; + } + + if ($this->tokenStore->redeem(token: $token, caseUuid: $caseUuid) === false) { + return null; + } + + $data = $case->getObject(); + $subjectId = (string) ($data['subjectId'] ?? ''); + $type = null; + if (isset($data['subjectType']) === true && $data['subjectType'] !== '') { + $type = (string) $data['subjectType']; + } + + $assembled = $this->dsrService->assembleAccessExport( + subjectId: $subjectId, + type: $type + ); + $pdfBytes = $this->renderPdf(caseUuid: $caseUuid, assembled: $assembled); + + return $this->signer->sign(bytes: $pdfBytes); + }//end download() + + /** + * Assemble the regulator dossier for a case. + * + * Reads the case under the caller's RBAC and reflects what was collected + * (the `evidence` sub-collection), what was redacted with grounds (the + * `redactions` sub-collection), and the case history (its immutable + * audit-trail entries). It never reaches data outside the caller's + * authorisation — it reads only the already-authorised case object + that + * object's own trail. + * + * @param string $caseUuid The case object uuid. + * + * @return array{caseUuid: string, subjectId: string, status: string, evidence: array, redactions: array, history: array} + * + * @throws RuntimeException When the case cannot be loaded (absent or unauthorised). + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function assembleRegulatorDossier(string $caseUuid): array + { + $case = $this->accessor->load(caseUuid: $caseUuid); + if ($case === null) { + throw new RuntimeException( + message: sprintf('Case "%s" not found or not authorised.', $caseUuid) + ); + } + + $data = $case->getObject(); + $evidence = []; + $redactions = []; + if (isset($data['evidence']) === true && is_array($data['evidence']) === true) { + $evidence = array_values($data['evidence']); + } + + if (isset($data['redactions']) === true && is_array($data['redactions']) === true) { + $redactions = array_values($data['redactions']); + } + + $history = []; + try { + $entries = $this->auditTrailMapper->findByObjectUntil( + objectId: (int) $case->getId(), + objectUuid: (string) $case->getUuid() + ); + foreach ($entries as $entry) { + $history[] = $entry->jsonSerialize(); + } + } catch (Throwable $e) { + $this->logger->warning( + message: sprintf( + '[ExportBundleService] dossier history read failed for case "%s": %s', + $caseUuid, + $e->getMessage() + ) + ); + } + + return [ + 'caseUuid' => $caseUuid, + 'subjectId' => (string) ($data['subjectId'] ?? ''), + 'status' => (string) ($data['status'] ?? ''), + 'evidence' => $evidence, + 'redactions' => $redactions, + 'history' => $history, + ]; + }//end assembleRegulatorDossier() + + /** + * Render the assembled export data as PDF disclosure-document bytes. + * + * A minimal, dependency-free PDF is produced here so the bundle is a real + * `application/pdf` document that the SHA-256 hash + (future) PAdES-LTV + * signature apply to. Rich PDF layout is a rendering concern that can grow + * behind this method without touching the signing/token/dossier contract. + * + * @param string $caseUuid The case object uuid (document header). + * @param array $assembled The assembled access-export payload. + * + * @return string The PDF bytes. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + private function renderPdf(string $caseUuid, array $assembled): string + { + $subject = (string) ($assembled['subject'] ?? ''); + $objectCount = (int) ($assembled['objectCount'] ?? 0); + $generatedAt = (string) ($assembled['generatedAt'] ?? ''); + + // A deterministic, single-page PDF skeleton carrying the disclosure + // summary. Deterministic output keeps the SHA-256 hash stable for a + // given case + assembled payload. + $text = sprintf( + 'GDPR Access Disclosure - case %s - subject %s - %d object(s) - generated %s', + $caseUuid, + $subject, + $objectCount, + $generatedAt + ); + $escaped = str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], $text); + + $stream = "BT /F1 12 Tf 40 720 Td (".$escaped.") Tj ET"; + $objects = []; + $objects[] = '<< /Type /Catalog /Pages 2 0 R >>'; + $objects[] = '<< /Type /Pages /Kids [3 0 R] /Count 1 >>'; + $objects[] = '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] ' + .'/Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>'; + $objects[] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>'; + $objects[] = '<< /Length '.strlen($stream).' >>'."\nstream\n".$stream."\nendstream"; + + $pdf = "%PDF-1.4\n"; + $offsets = []; + foreach ($objects as $i => $body) { + $offsets[($i + 1)] = strlen($pdf); + $pdf .= ($i + 1)." 0 obj\n".$body."\nendobj\n"; + } + + $xrefPos = strlen($pdf); + $count = (count($objects) + 1); + $pdf .= "xref\n0 ".$count."\n0000000000 65535 f \n"; + foreach ($offsets as $offset) { + $pdf .= sprintf("%010d 00000 n \n", $offset); + } + + $pdf .= "trailer\n<< /Size ".$count." /Root 1 0 R >>\nstartxref\n".$xrefPos."\n%%EOF"; + + return $pdf; + }//end renderPdf() +}//end class diff --git a/lib/Service/Gdpr/Export/OneTimeDownloadTokenStore.php b/lib/Service/Gdpr/Export/OneTimeDownloadTokenStore.php new file mode 100644 index 0000000000..750139107d --- /dev/null +++ b/lib/Service/Gdpr/Export/OneTimeDownloadTokenStore.php @@ -0,0 +1,171 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Export; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IAppConfig; +use OCP\Security\ISecureRandom; + +/** + * Single-use, time-boxed, case-scoped download-token store. + */ +class OneTimeDownloadTokenStore +{ + + /** + * App id for app-config storage. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * App-config key prefix for stored token records. + * + * @var string + */ + private const KEY_PREFIX = 'dsar_bundle_token_'; + + /** + * Default token lifetime in seconds (15 minutes). + * + * @var int + */ + public const DEFAULT_TTL_SECONDS = 900; + + /** + * Constructor. + * + * @param IAppConfig $appConfig App-config store for token records. + * @param ISecureRandom $random CSPRNG for token generation. + * @param ITimeFactory $time Time source for expiry checks. + */ + public function __construct( + private readonly IAppConfig $appConfig, + private readonly ISecureRandom $random, + private readonly ITimeFactory $time + ) { + }//end __construct() + + /** + * Mint a single-use token bound to a case, returning the RAW token. + * + * Only the token's SHA-256 is persisted (with the case uuid + expiry); the + * raw token is returned to the caller once and never stored. + * + * @param string $caseUuid The case the token authorises a download for. + * @param int $ttlSeconds Token lifetime in seconds. + * + * @return string The raw one-time token. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function mint(string $caseUuid, int $ttlSeconds=self::DEFAULT_TTL_SECONDS): string + { + $token = $this->random->generate(64, ISecureRandom::CHAR_ALPHANUMERIC); + $tokenId = hash(algo: 'sha256', data: $token); + $expiry = ($this->time->getTime() + $ttlSeconds); + + $this->appConfig->setValueString( + app: self::APP_ID, + key: self::KEY_PREFIX.$tokenId, + value: json_encode( + [ + 'caseUuid' => $caseUuid, + 'expiry' => $expiry, + ] + ) + ); + + return $token; + }//end mint() + + /** + * Redeem a token for a case: verify + BURN it in one step. + * + * Returns true only when the token exists, is bound to the given case, and + * has not expired — and the token is deleted before returning, so a second + * redemption of the same token is refused. An expired token is also burned. + * The check fails closed: any malformed/absent/mismatched record denies. + * + * @param string $token The raw token presented at the download endpoint. + * @param string $caseUuid The case the download is scoped to. + * + * @return bool True when the token was valid for this case (and is now burned). + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function redeem(string $token, string $caseUuid): bool + { + if ($token === '') { + return false; + } + + $tokenId = hash(algo: 'sha256', data: $token); + $key = self::KEY_PREFIX.$tokenId; + + $raw = $this->appConfig->getValueString( + app: self::APP_ID, + key: $key, + default: '' + ); + + if ($raw === '') { + return false; + } + + // Burn on first sight: whether valid or expired, the token is single + // use. Deleting before the validity verdict guarantees no replay. + $this->appConfig->deleteKey(app: self::APP_ID, key: $key); + + $record = json_decode($raw, true); + if (is_array($record) === false) { + return false; + } + + $recordCase = (string) ($record['caseUuid'] ?? ''); + $expiry = (int) ($record['expiry'] ?? 0); + + if ($recordCase === '' || $recordCase !== $caseUuid) { + return false; + } + + if ($expiry < $this->time->getTime()) { + return false; + } + + return true; + }//end redeem() +}//end class diff --git a/lib/Service/Gdpr/Export/PadesSigner.php b/lib/Service/Gdpr/Export/PadesSigner.php new file mode 100644 index 0000000000..5b1e7d343c --- /dev/null +++ b/lib/Service/Gdpr/Export/PadesSigner.php @@ -0,0 +1,50 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Export; + +/** + * Signs (or stub-signs) rendered export-bundle bytes. + */ +interface PadesSigner +{ + /** + * Sign the rendered bundle bytes and return the signed result + hash. + * + * @param string $bytes The rendered bundle bytes (the PDF disclosure document). + * + * @return SignedBundle The bytes, their SHA-256 content hash, and the signature state. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function sign(string $bytes): SignedBundle; +}//end interface diff --git a/lib/Service/Gdpr/Export/SignedBundle.php b/lib/Service/Gdpr/Export/SignedBundle.php new file mode 100644 index 0000000000..c787c6e2ce --- /dev/null +++ b/lib/Service/Gdpr/Export/SignedBundle.php @@ -0,0 +1,115 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Export; + +/** + * Result of signing (or stub-signing) an export bundle. + */ +final class SignedBundle +{ + /** + * Constructor. + * + * @param string $bytes The bundle bytes (the PDF disclosure document). + * @param string $contentHash SHA-256 content hash of the bytes (`sha256:...`). + * @param bool $signed Whether a real PAdES-LTV signature is attached. + * @param string $signatureState Human/machine-readable signature state marker. + * @param string $mimeType MIME type of the bytes. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function __construct( + private readonly string $bytes, + private readonly string $contentHash, + private readonly bool $signed, + private readonly string $signatureState, + private readonly string $mimeType='application/pdf' + ) { + }//end __construct() + + /** + * The bundle bytes. + * + * @return string + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function getBytes(): string + { + return $this->bytes; + }//end getBytes() + + /** + * The SHA-256 content hash of the bytes. + * + * @return string + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function getContentHash(): string + { + return $this->contentHash; + }//end getContentHash() + + /** + * Whether a real PAdES-LTV signature is attached. + * + * @return bool + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function isSigned(): bool + { + return $this->signed; + }//end isSigned() + + /** + * The signature-state marker. + * + * @return string + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function getSignatureState(): string + { + return $this->signatureState; + }//end getSignatureState() + + /** + * The MIME type of the bytes. + * + * @return string + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function getMimeType(): string + { + return $this->mimeType; + }//end getMimeType() +}//end class diff --git a/lib/Service/Gdpr/Export/UnsignedPadesSigner.php b/lib/Service/Gdpr/Export/UnsignedPadesSigner.php new file mode 100644 index 0000000000..c3f5e30d82 --- /dev/null +++ b/lib/Service/Gdpr/Export/UnsignedPadesSigner.php @@ -0,0 +1,67 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Export; + +/** + * SHA-256-only stub signer — carries the hash, does not sign. + */ +final class UnsignedPadesSigner implements PadesSigner +{ + + /** + * Signature-state marker for the unsigned stub. + * + * @var string + */ + public const STATE_PENDING_LIBRARY = 'pending PAdES-LTV library'; + + /** + * Attach a SHA-256 content hash to the bytes and mark them unsigned. + * + * @param string $bytes The rendered bundle bytes. + * + * @return SignedBundle Bytes + `sha256:` hash + `signed:false` pending-library state. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md + */ + public function sign(string $bytes): SignedBundle + { + return new SignedBundle( + bytes: $bytes, + contentHash: 'sha256:'.hash(algo: 'sha256', data: $bytes), + signed: false, + signatureState: self::STATE_PENDING_LIBRARY + ); + }//end sign() +}//end class diff --git a/lib/Service/Gdpr/Identity/IdentityVerifyProvider.php b/lib/Service/Gdpr/Identity/IdentityVerifyProvider.php new file mode 100644 index 0000000000..3ba052d5db --- /dev/null +++ b/lib/Service/Gdpr/Identity/IdentityVerifyProvider.php @@ -0,0 +1,71 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Identity; + +/** + * A registerable identity-verification provider for a DSAR case. + */ +interface IdentityVerifyProvider +{ + /** + * Stable provider id addressed by a `dsarPolicyPack.identityVerifyProvider` + * selector. MUST be unique across the registry; collisions are rejected + * first-wins at registration time. + * + * @return string The provider id (e.g. `or.default.identity-verify.null`). + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function getProviderId(): string; + + /** + * Verify the identity of the data-subject on a DSAR case. + * + * The provider is handed the case's serialised payload (subject identifier, + * subject type, jurisdiction, …) so it can decide how to verify. It MUST + * return exactly one of the three states of {@see IdentityVerifyResult} + * (`verified` / `failed` / `needs-more`). It MUST NOT return `verified` + * unless the identity was positively established (fail-closed). + * + * @param string $caseUuid The case object uuid. + * @param array $case The case's serialised payload. + * + * @return IdentityVerifyResult The three-state verification outcome. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function verify(string $caseUuid, array $case): IdentityVerifyResult; +}//end interface diff --git a/lib/Service/Gdpr/Identity/IdentityVerifyRegistry.php b/lib/Service/Gdpr/Identity/IdentityVerifyRegistry.php new file mode 100644 index 0000000000..119960cf5c --- /dev/null +++ b/lib/Service/Gdpr/Identity/IdentityVerifyRegistry.php @@ -0,0 +1,169 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Identity; + +use Psr\Log\LoggerInterface; + +/** + * Registry of all IdentityVerifyProvider implementations on this instance. + */ +class IdentityVerifyRegistry +{ + + /** + * Registered providers, keyed by provider id. + * + * @var array + */ + private array $providers = []; + + /** + * Constructor. + * + * @param LoggerInterface $logger Logger for collision + fail-closed warnings. + * @param NullIdentityVerifyProvider $default Fail-closed default returned when resolution finds no bound provider. + * + * @return void + */ + public function __construct( + private readonly LoggerInterface $logger, + private readonly NullIdentityVerifyProvider $default + ) { + // The fail-closed default is itself registered so a pack that names it + // explicitly (as the default pack does) resolves through the same path. + $this->providers[$this->default->getProviderId()] = $this->default; + }//end __construct() + + /** + * Register a provider with the registry. + * + * Duplicate id: first registration wins, the second logs a warning. + * + * @param IdentityVerifyProvider $provider The provider to register. + * + * @return bool True when accepted, false when rejected (duplicate id). + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function addProvider(IdentityVerifyProvider $provider): bool + { + $id = $provider->getProviderId(); + if (isset($this->providers[$id]) === true) { + $this->logger->warning( + sprintf( + '[IdentityVerifyRegistry] duplicate provider id "%s" — keeping first registration', + $id + ) + ); + return false; + } + + $this->providers[$id] = $provider; + return true; + }//end addProvider() + + /** + * Look up a provider by id (no fail-closed fallback). + * + * Prefer {@see resolve()} on the case path; `get()` is the raw lookup used + * by tests and diagnostics. + * + * @param string $id The provider id. + * + * @return IdentityVerifyProvider|null Provider, or null when unregistered. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function get(string $id): ?IdentityVerifyProvider + { + return $this->providers[$id] ?? null; + }//end get() + + /** + * Resolve the identity provider for a pack selector, FAIL-CLOSED. + * + * Returns the provider registered under $selectorId when present; otherwise + * (selector unset, empty, or naming an unregistered provider) returns the + * OR-shipped fail-closed default and logs a warning. NEVER returns null and + * NEVER treats "provider unavailable" as "verified" (ADR-005 / CWE-863). + * + * @param string|null $selectorId The active pack's identityVerifyProvider selector. + * + * @return IdentityVerifyProvider The bound provider, or the fail-closed default. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function resolve(?string $selectorId): IdentityVerifyProvider + { + if ($selectorId !== null && $selectorId !== '' && isset($this->providers[$selectorId]) === true) { + return $this->providers[$selectorId]; + } + + $this->logger->warning( + sprintf( + '[IdentityVerifyRegistry] identityVerifyProvider "%s" is unset or unregistered — ' + .'resolving fail-closed default "%s" (identity NOT verified)', + ($selectorId ?? '(null)'), + $this->default->getProviderId() + ) + ); + return $this->default; + }//end resolve() + + /** + * List every registered provider, irrespective of enablement. + * + * @return array + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function list(): array + { + return array_values($this->providers); + }//end list() + + /** + * Return the ids of every registered provider. + * + * @return array + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function listIds(): array + { + return array_keys($this->providers); + }//end listIds() +}//end class diff --git a/lib/Service/Gdpr/Identity/IdentityVerifyResult.php b/lib/Service/Gdpr/Identity/IdentityVerifyResult.php new file mode 100644 index 0000000000..3d3ee573b5 --- /dev/null +++ b/lib/Service/Gdpr/Identity/IdentityVerifyResult.php @@ -0,0 +1,210 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Identity; + +use InvalidArgumentException; + +/** + * The three-state result of verifying a data-subject's identity. + */ +final class IdentityVerifyResult +{ + + /** + * Identity was positively verified. + * + * @var string + */ + public const STATUS_VERIFIED = 'verified'; + + /** + * Identity verification was attempted and failed (positively not verified). + * + * @var string + */ + public const STATUS_FAILED = 'failed'; + + /** + * Verification is incomplete — more information/steps are required. This is + * an UNVERIFIED state: a case MUST NOT proceed as if identity were proven. + * + * @var string + */ + public const STATUS_NEEDS_MORE = 'needs-more'; + + /** + * The exact set of permitted statuses. + * + * @var array + */ + private const ALLOWED = [ + self::STATUS_VERIFIED, + self::STATUS_FAILED, + self::STATUS_NEEDS_MORE, + ]; + + /** + * Constructor. + * + * @param string $status One of the three permitted statuses. + * @param string $providerId The id of the provider that produced this result. + * @param string|null $message Optional human-readable detail. + * + * @throws InvalidArgumentException When $status is not one of the three permitted values. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function __construct( + private readonly string $status, + private readonly string $providerId, + private readonly ?string $message=null + ) { + if (in_array($status, self::ALLOWED, true) === false) { + throw new InvalidArgumentException( + sprintf( + 'IdentityVerifyResult status "%s" is invalid; expected one of: %s', + $status, + implode(', ', self::ALLOWED) + ) + ); + } + }//end __construct() + + /** + * Build a positively-verified result. + * + * @param string $providerId The provider that verified the subject. + * @param string|null $message Optional detail. + * + * @return self + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public static function verified(string $providerId, ?string $message=null): self + { + return new self(status: self::STATUS_VERIFIED, providerId: $providerId, message: $message); + }//end verified() + + /** + * Build a failed (not-verified) result. + * + * @param string $providerId The provider that produced the result. + * @param string|null $message Optional detail. + * + * @return self + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public static function failed(string $providerId, ?string $message=null): self + { + return new self(status: self::STATUS_FAILED, providerId: $providerId, message: $message); + }//end failed() + + /** + * Build a needs-more (incomplete, not-verified) result. + * + * @param string $providerId The provider that produced the result. + * @param string|null $message Optional detail. + * + * @return self + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public static function needsMore(string $providerId, ?string $message=null): self + { + return new self(status: self::STATUS_NEEDS_MORE, providerId: $providerId, message: $message); + }//end needsMore() + + /** + * The three-state status. + * + * @return string One of the STATUS_* constants. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function getStatus(): string + { + return $this->status; + }//end getStatus() + + /** + * The id of the provider that produced this result. + * + * @return string + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function getProviderId(): string + { + return $this->providerId; + }//end getProviderId() + + /** + * Optional human-readable detail. + * + * @return string|null + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function getMessage(): ?string + { + return $this->message; + }//end getMessage() + + /** + * Whether the subject was positively verified. + * + * The ONLY status that means "proceed as verified" — `failed` and + * `needs-more` are both unverified (fail-closed). + * + * @return bool True only for STATUS_VERIFIED. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function isVerified(): bool + { + return $this->status === self::STATUS_VERIFIED; + }//end isVerified() + + /** + * Serialise for API/case persistence. + * + * @return array{status: string, provider: string, message: string|null} + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function toArray(): array + { + return [ + 'status' => $this->status, + 'provider' => $this->providerId, + 'message' => $this->message, + ]; + }//end toArray() +}//end class diff --git a/lib/Service/Gdpr/Identity/NullIdentityVerifyProvider.php b/lib/Service/Gdpr/Identity/NullIdentityVerifyProvider.php new file mode 100644 index 0000000000..4ca65db6bd --- /dev/null +++ b/lib/Service/Gdpr/Identity/NullIdentityVerifyProvider.php @@ -0,0 +1,83 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Identity; + +/** + * Fail-closed default identity-verify provider: never verifies. + */ +final class NullIdentityVerifyProvider implements IdentityVerifyProvider +{ + + /** + * The stable id the default policy pack binds for this fail-closed provider. + * + * @var string + */ + public const PROVIDER_ID = 'or.default.identity-verify.null'; + + /** + * {@inheritDoc} + * + * @return string The fail-closed default provider id. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function getProviderId(): string + { + return self::PROVIDER_ID; + }//end getProviderId() + + /** + * Always returns `needs-more` — the subject is never auto-verified. + * + * No leaf identity provider is bound, so there is no way to establish the + * subject's identity; the only safe answer is "not verified, more needed". + * + * @param string $caseUuid The case object uuid. + * @param array $case The case's serialised payload (unused). + * + * @return IdentityVerifyResult Always an unverified (`needs-more`) result. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + * + * @SuppressWarnings(PHPMD.StaticAccess) IdentityVerifyResult::needsMore is a named constructor — no DI alternative. + */ + public function verify(string $caseUuid, array $case): IdentityVerifyResult + { + return IdentityVerifyResult::needsMore( + providerId: self::PROVIDER_ID, + message: 'No identity-verify provider is bound; identity cannot be verified (fail-closed default).' + ); + }//end verify() +}//end class diff --git a/lib/Service/Gdpr/Lifecycle/DenialFinaliseGuard.php b/lib/Service/Gdpr/Lifecycle/DenialFinaliseGuard.php new file mode 100644 index 0000000000..f558dfa0ad --- /dev/null +++ b/lib/Service/Gdpr/Lifecycle/DenialFinaliseGuard.php @@ -0,0 +1,107 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Lifecycle; + +use OCA\OpenRegister\Lifecycle\GuardResult; +use OCA\OpenRegister\Lifecycle\LifecycleGuardInterface; + +/** + * Refuses `finaliseDenial` unless a `regulatorReference` is present. + */ +final class DenialFinaliseGuard implements LifecycleGuardInterface +{ + + /** + * The transition action this guard gates. + * + * @var string + */ + public const ACTION_FINALISE_DENIAL = 'finaliseDenial'; + + /** + * The case property that must be present before finalising a denial. + * + * @var string + */ + public const FIELD_REGULATOR_REFERENCE = 'regulatorReference'; + + /** + * Authorise (or deny) a transition. + * + * Only `finaliseDenial` is gated. Any other action (including + * `draftDenial`) is allowed by this guard — it exists solely to enforce the + * mandatory-regulator-reference precondition at finalise time. The check + * fails closed: it allows finalise ONLY when it can positively confirm a + * non-empty `regulatorReference`; every other outcome denies. + * + * @param array $object The loaded case payload at its current state. + * @param string $action The transition action being applied. + * @param string $userId The uid of the caller. + * + * @return GuardResult Allow or deny + optional deny message. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $userId is part of the interface contract. + * @SuppressWarnings(PHPMD.StaticAccess) GuardResult::allow()/deny() are the sanctioned value-object factories. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-denial-guard/spec.md + */ + public function check(array $object, string $action, string $userId): GuardResult + { + // The gate applies ONLY at finalise. draftDenial (and any other + // action) is never blocked by this guard — drafting a denialGround + // must remain possible without a regulatorReference. + if ($action !== self::ACTION_FINALISE_DENIAL) { + return GuardResult::allow(); + } + + // Fail closed: only a value that is present, a string, and non-empty + // after trimming counts as a satisfied precondition. A missing key, + // a null, a non-string, or a whitespace-only value all deny. + $reference = ($object[self::FIELD_REGULATOR_REFERENCE] ?? null); + if (is_string($reference) === true && trim($reference) !== '') { + return GuardResult::allow(); + } + + return GuardResult::deny( + message: 'A regulatorReference is required before a denial can be finalised.' + ); + + }//end check() +}//end class diff --git a/lib/Service/Gdpr/Policy/DsarPolicyPackResolver.php b/lib/Service/Gdpr/Policy/DsarPolicyPackResolver.php new file mode 100644 index 0000000000..166c759054 --- /dev/null +++ b/lib/Service/Gdpr/Policy/DsarPolicyPackResolver.php @@ -0,0 +1,277 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Policy; + +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Resolves the seam provider selectors from a case's active policy pack. + */ +class DsarPolicyPackResolver +{ + + /** + * Register slug the policy packs live under (declared by the head change). + * + * @var string + */ + public const REGISTER_SLUG = 'dsar-policy-packs'; + + /** + * Schema slug of the policy-pack entity (declared by the head change). + * + * @var string + */ + public const SCHEMA_SLUG = 'dsarPolicyPack'; + + /** + * Jurisdiction key of the neutral fail-closed baseline pack. + * + * @var string + */ + public const DEFAULT_JURISDICTION = 'default'; + + /** + * Pack field naming the bound identity-verify provider. + * + * @var string + */ + public const IDENTITY_SELECTOR = 'identityVerifyProvider'; + + /** + * Pack field naming the bound regulator-escalate provider. + * + * @var string + */ + public const REGULATOR_SELECTOR = 'regulatorEscalateProvider'; + + /** + * Constructor. + * + * @param ObjectService $objectService RBAC + tenant scoped object store (pack read). + * @param LoggerInterface $logger Logger for pack-read diagnostics. + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * The active pack's `identityVerifyProvider` selector for a case. + * + * @param array $case The case's serialised payload. + * + * @return string|null The selector id, or null when unset/no pack resolves. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md + */ + public function identityVerifyProviderId(array $case): ?string + { + return $this->selectorFor(case: $case, field: self::IDENTITY_SELECTOR); + }//end identityVerifyProviderId() + + /** + * The active pack's `regulatorEscalateProvider` selector for a case. + * + * @param array $case The case's serialised payload. + * + * @return string|null The selector id, or null when unset/no pack resolves. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function regulatorEscalateProviderId(array $case): ?string + { + return $this->selectorFor(case: $case, field: self::REGULATOR_SELECTOR); + }//end regulatorEscalateProviderId() + + /** + * The full policy-pack payload active for a case (jurisdiction match, + * `default` fallback), or null when no pack resolves. + * + * Public read surface for consumers that need MORE than the two seam + * selectors: the DPIA pattern-detection job reads the pack's + * `dpiaDetection` block and the privacy-officer notification recipient + * resolver reads `privacyOfficerGroup`. Both inherit the resolver's + * fail-safe semantics — no pack ⇒ null ⇒ the consumer no-ops. + * + * @param array $case The case's serialised payload. + * @param bool $systemContext True for background-job (user-less) reads: the + * pack query bypasses RBAC/tenancy scoping, because + * a system sweep has no session to scope by. + * + * @return array|null The active pack payload, or null. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md + * (Requirement: Detection configuration lives in the DSAR policy pack) + */ + public function activePackForCase(array $case, bool $systemContext=false): ?array + { + if ($systemContext === false) { + return $this->resolveActivePack(case: $case); + } + + $wanted = $this->stringField(data: $case, key: 'jurisdiction'); + $default = null; + foreach ($this->loadPacks(_rbac: false, _multitenancy: false) as $pack) { + $packJurisdiction = $this->stringField(data: $pack, key: 'jurisdiction'); + if ($wanted !== '' && $packJurisdiction === $wanted) { + return $pack; + } + + if ($packJurisdiction === self::DEFAULT_JURISDICTION) { + $default = $pack; + } + } + + return $default; + + }//end activePackForCase() + + /** + * Read one selector field off the case's active pack. + * + * @param array $case The case's serialised payload. + * @param string $field The pack selector field to read. + * + * @return string|null The non-empty selector value, or null. + */ + private function selectorFor(array $case, string $field): ?string + { + $pack = $this->resolveActivePack(case: $case); + if ($pack === null) { + return null; + } + + $value = $this->stringField(data: $pack, key: $field); + if ($value !== '') { + return $value; + } + + return null; + }//end selectorFor() + + /** + * Resolve the active pack for a case: the pack whose `jurisdiction` matches + * the case's `jurisdiction`, falling back to the neutral `default` pack. + * + * @param array $case The case's serialised payload. + * + * @return array|null The active pack, or null when none exists. + */ + private function resolveActivePack(array $case): ?array + { + $wanted = $this->stringField(data: $case, key: 'jurisdiction'); + $default = null; + foreach ($this->loadPacks() as $pack) { + $packJurisdiction = $this->stringField(data: $pack, key: 'jurisdiction'); + if ($wanted !== '' && $packJurisdiction === $wanted) { + return $pack; + } + + if ($packJurisdiction === self::DEFAULT_JURISDICTION) { + $default = $pack; + } + } + + return $default; + }//end resolveActivePack() + + /** + * Read a string field off an array, defaulting to an empty string. + * + * @param array $data The source array. + * @param string $key The field to read. + * + * @return string The string value, or '' when absent/non-string. + */ + private function stringField(array $data, string $key): string + { + $value = ($data[$key] ?? null); + if (is_string($value) === true) { + return $value; + } + + return ''; + }//end stringField() + + /** + * Load every policy-pack object in the register (RBAC + tenant scoped). + * + * A read failure returns an empty set, which resolves the case fail-closed + * through the registry default (never fail-open) — an unreadable pack MUST + * NOT be treated as "identity verified" / "escalation done". + * + * @param bool $_rbac Whether the pack read is RBAC scoped (false only for system sweeps). + * @param bool $_multitenancy Whether the pack read is tenant scoped (false only for system sweeps). + * + * @return array> The rendered pack rows. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC/multitenancy flags follow established API patterns. + */ + private function loadPacks(bool $_rbac=true, bool $_multitenancy=true): array + { + try { + $rows = $this->objectService->findAll( + config: [ + 'filters' => [ + 'register' => self::REGISTER_SLUG, + 'schema' => self::SCHEMA_SLUG, + ], + ], + _rbac: $_rbac, + _multitenancy: $_multitenancy + ); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('[DsarPolicyPackResolver] policy-pack read failed: %s', $e->getMessage()) + ); + return []; + } + + $packs = []; + foreach ($rows as $row) { + if (is_array($row) === true) { + $packs[] = $row; + } + } + + return $packs; + }//end loadPacks() +}//end class diff --git a/lib/Service/Gdpr/PrivacyOfficerRecipientResolver.php b/lib/Service/Gdpr/PrivacyOfficerRecipientResolver.php new file mode 100644 index 0000000000..a28ec80ea5 --- /dev/null +++ b/lib/Service/Gdpr/PrivacyOfficerRecipientResolver.php @@ -0,0 +1,131 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Service\Gdpr\Policy\DsarPolicyPackResolver; +use OCA\OpenRegister\Service\Notification\RecipientResolverInterface; +use OCP\IGroupManager; +use Psr\Log\LoggerInterface; + +/** + * Resolve the pack-declared privacy-officer group to notification uids. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Requirement: Deadline breach is stamped on the case and visible to the privacy officer) + */ +class PrivacyOfficerRecipientResolver implements RecipientResolverInterface +{ + + /** + * The pack field naming the privacy-officer Nextcloud group. + * + * @var string + */ + public const PACK_FIELD = 'privacyOfficerGroup'; + + /** + * Constructor. + * + * @param DsarPolicyPackResolver $packResolver Active-pack resolution for the case. + * @param IGroupManager $groupManager Group-member expansion. + * @param LoggerInterface $logger Logger for resolution diagnostics. + */ + public function __construct( + private readonly DsarPolicyPackResolver $packResolver, + private readonly IGroupManager $groupManager, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Resolve the officer recipients for a notification dispatch. + * + * @param ObjectEntity $object The DSAR case the event happened on. + * @param array $context Trigger-specific extras (unused). + * + * @return array Officer group member uids (empty fail-safe). + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $context is part of the resolver contract. + * + * @spec openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md + * (Scenario: Breach notifies handler and privacy officer) + */ + public function resolve(ObjectEntity $object, array $context): array + { + $case = ($object->getObject() ?? []); + + // Notification dispatch may run from a user write OR a system sweep; + // resolve the pack system-scoped so both paths see the same pack. + $pack = $this->packResolver->activePackForCase(case: $case, systemContext: true); + if ($pack === null) { + return []; + } + + $groupId = ($pack[self::PACK_FIELD] ?? null); + if (is_string($groupId) === false || $groupId === '' || str_starts_with($groupId, '<') === true) { + // Unset or placeholder (``) — fail-safe. + return []; + } + + try { + $group = $this->groupManager->get($groupId); + if ($group === null) { + $this->logger->debug( + message: '[PrivacyOfficerRecipientResolver] pack names unknown group "'.$groupId.'" — no officer recipients', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return []; + } + + $uids = []; + foreach ($group->getUsers() as $user) { + $uids[] = $user->getUID(); + } + + return array_values(array_unique($uids)); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[PrivacyOfficerRecipientResolver] group resolution failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__, 'group' => $groupId] + ); + return []; + }//end try + + }//end resolve() +}//end class diff --git a/lib/Service/Gdpr/Redaction/RedactionWriteService.php b/lib/Service/Gdpr/Redaction/RedactionWriteService.php new file mode 100644 index 0000000000..947c5c4242 --- /dev/null +++ b/lib/Service/Gdpr/Redaction/RedactionWriteService.php @@ -0,0 +1,133 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Redaction; + +use OCA\OpenRegister\Service\Gdpr\Case\CaseObjectAccessor; +use RuntimeException; + +/** + * Field-level redaction write path (distinct from erase pseudonymise). + */ +class RedactionWriteService +{ + + /** + * Marker distinguishing a redaction record from an erase pseudonymise. + * + * @var string + */ + public const RECORD_TYPE = 'redaction'; + + /** + * Constructor. + * + * @param CaseObjectAccessor $accessor RBAC-scoped, audited case load/save. + */ + public function __construct( + private readonly CaseObjectAccessor $accessor + ) { + }//end __construct() + + /** + * Apply a field-level redaction to a case field. + * + * Captures the field's current value as `before`, sets the redacted `after` + * value on the case field, and appends a `redactions` entry recording + * `field` / `before` / `after` / `ground`. Persists once (audited). It does + * NOT call the erase pseudonymise path. + * + * @param string $caseUuid The case object uuid. + * @param string $field The case field being redacted. + * @param string $after The redacted replacement value. + * @param string $ground The generic ground key the redaction was applied under. + * + * @return array{caseUuid: string, field: string, before: string, after: string, ground: string, recordType: string} + * + * @throws RuntimeException When the case cannot be loaded (absent or unauthorised). + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-redaction-write/spec.md + */ + public function applyRedaction(string $caseUuid, string $field, string $after, string $ground): array + { + $case = $this->accessor->load(caseUuid: $caseUuid); + if ($case === null) { + throw new RuntimeException( + message: sprintf('Case "%s" not found or not authorised.', $caseUuid) + ); + } + + $data = $case->getObject(); + + // Snapshot the original value before we overwrite it. This is a + // field-level capture on the case object — NOT the statutory erase + // pseudonymise across the subject's objects. + $before = ''; + if (isset($data[$field]) === true && is_scalar($data[$field]) === true) { + $before = (string) $data[$field]; + } + + // Apply the redacted value to the case field. + $data[$field] = $after; + + $entry = [ + 'field' => $field, + 'before' => $before, + 'after' => $after, + 'ground' => $ground, + 'recordType' => self::RECORD_TYPE, + ]; + + $redactions = []; + if (isset($data['redactions']) === true && is_array($data['redactions']) === true) { + $redactions = array_values($data['redactions']); + } + + $redactions[] = $entry; + $data['redactions'] = $redactions; + + $this->accessor->save(case: $case, data: $data); + + return [ + 'caseUuid' => $caseUuid, + 'field' => $field, + 'before' => $before, + 'after' => $after, + 'ground' => $ground, + 'recordType' => self::RECORD_TYPE, + ]; + }//end applyRedaction() +}//end class diff --git a/lib/Service/Gdpr/Regulator/NullRegulatorEscalateProvider.php b/lib/Service/Gdpr/Regulator/NullRegulatorEscalateProvider.php new file mode 100644 index 0000000000..9bc043e93e --- /dev/null +++ b/lib/Service/Gdpr/Regulator/NullRegulatorEscalateProvider.php @@ -0,0 +1,85 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Regulator; + +/** + * Fail-closed default regulator-escalate provider: never escalates. + */ +final class NullRegulatorEscalateProvider implements RegulatorEscalateProvider +{ + + /** + * The stable id the default policy pack binds for this fail-closed provider. + * + * @var string + */ + public const PROVIDER_ID = 'or.default.regulator-escalate.null'; + + /** + * {@inheritDoc} + * + * @return string The fail-closed default provider id. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function getProviderId(): string + { + return self::PROVIDER_ID; + }//end getProviderId() + + /** + * Always refuses — no escalation is performed, no reference is minted. + * + * No leaf regulator provider is bound, so there is no supervisory authority + * to escalate to; the only safe answer is an explicit refusal (never a + * silent success). + * + * @param string $caseUuid The case object uuid. + * @param array $case The case's serialised payload (unused). + * + * @return RegulatorEscalateResult Always a `refused` result. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + * + * @SuppressWarnings(PHPMD.StaticAccess) RegulatorEscalateResult::refused is a named constructor — no DI alternative. + */ + public function escalate(string $caseUuid, array $case): RegulatorEscalateResult + { + return RegulatorEscalateResult::refused( + providerId: self::PROVIDER_ID, + message: 'No regulator-escalate provider is bound; escalation not performed (fail-closed default).' + ); + }//end escalate() +}//end class diff --git a/lib/Service/Gdpr/Regulator/RegulatorEscalateProvider.php b/lib/Service/Gdpr/Regulator/RegulatorEscalateProvider.php new file mode 100644 index 0000000000..13de670f56 --- /dev/null +++ b/lib/Service/Gdpr/Regulator/RegulatorEscalateProvider.php @@ -0,0 +1,73 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Regulator; + +/** + * A registerable regulator-escalation provider for a DSAR case. + */ +interface RegulatorEscalateProvider +{ + /** + * Stable provider id addressed by a `dsarPolicyPack.regulatorEscalateProvider` + * selector. MUST be unique across the registry; collisions are rejected + * first-wins at registration time. + * + * @return string The provider id (e.g. `or.default.regulator-escalate.null`). + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function getProviderId(): string; + + /** + * Escalate / dossier a DSAR case to a supervisory authority. + * + * The provider is handed the case's serialised payload (subject identifier, + * jurisdiction, denial ground, dossier, …) so it can construct and submit + * the escalation. It MUST return a {@see RegulatorEscalateResult} carrying a + * regulator reference and a status — `escalated` only when the authority + * actually accepted the escalation, otherwise `refused`. It MUST NOT report + * success when no escalation was performed (fail-closed). + * + * @param string $caseUuid The case object uuid. + * @param array $case The case's serialised payload. + * + * @return RegulatorEscalateResult The escalation outcome (reference + status). + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function escalate(string $caseUuid, array $case): RegulatorEscalateResult; +}//end interface diff --git a/lib/Service/Gdpr/Regulator/RegulatorEscalateRegistry.php b/lib/Service/Gdpr/Regulator/RegulatorEscalateRegistry.php new file mode 100644 index 0000000000..324ff423b3 --- /dev/null +++ b/lib/Service/Gdpr/Regulator/RegulatorEscalateRegistry.php @@ -0,0 +1,169 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Regulator; + +use Psr\Log\LoggerInterface; + +/** + * Registry of all RegulatorEscalateProvider implementations on this instance. + */ +class RegulatorEscalateRegistry +{ + + /** + * Registered providers, keyed by provider id. + * + * @var array + */ + private array $providers = []; + + /** + * Constructor. + * + * @param LoggerInterface $logger Logger for collision + fail-closed warnings. + * @param NullRegulatorEscalateProvider $default Fail-closed default returned when resolution finds no bound provider. + * + * @return void + */ + public function __construct( + private readonly LoggerInterface $logger, + private readonly NullRegulatorEscalateProvider $default + ) { + // The fail-closed default is itself registered so a pack that names it + // explicitly (as the default pack does) resolves through the same path. + $this->providers[$this->default->getProviderId()] = $this->default; + }//end __construct() + + /** + * Register a provider with the registry. + * + * Duplicate id: first registration wins, the second logs a warning. + * + * @param RegulatorEscalateProvider $provider The provider to register. + * + * @return bool True when accepted, false when rejected (duplicate id). + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function addProvider(RegulatorEscalateProvider $provider): bool + { + $id = $provider->getProviderId(); + if (isset($this->providers[$id]) === true) { + $this->logger->warning( + sprintf( + '[RegulatorEscalateRegistry] duplicate provider id "%s" — keeping first registration', + $id + ) + ); + return false; + } + + $this->providers[$id] = $provider; + return true; + }//end addProvider() + + /** + * Look up a provider by id (no fail-closed fallback). + * + * Prefer {@see resolve()} on the case path; `get()` is the raw lookup used + * by tests and diagnostics. + * + * @param string $id The provider id. + * + * @return RegulatorEscalateProvider|null Provider, or null when unregistered. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function get(string $id): ?RegulatorEscalateProvider + { + return $this->providers[$id] ?? null; + }//end get() + + /** + * Resolve the regulator provider for a pack selector, FAIL-CLOSED. + * + * Returns the provider registered under $selectorId when present; otherwise + * (selector unset, empty, or naming an unregistered provider) returns the + * OR-shipped fail-closed default and logs a warning. NEVER returns null and + * NEVER treats "provider unavailable" as "escalated" (ADR-005 / CWE-863). + * + * @param string|null $selectorId The active pack's regulatorEscalateProvider selector. + * + * @return RegulatorEscalateProvider The bound provider, or the fail-closed default. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function resolve(?string $selectorId): RegulatorEscalateProvider + { + if ($selectorId !== null && $selectorId !== '' && isset($this->providers[$selectorId]) === true) { + return $this->providers[$selectorId]; + } + + $this->logger->warning( + sprintf( + '[RegulatorEscalateRegistry] regulatorEscalateProvider "%s" is unset or unregistered — ' + .'resolving fail-closed default "%s" (escalation NOT performed)', + ($selectorId ?? '(null)'), + $this->default->getProviderId() + ) + ); + return $this->default; + }//end resolve() + + /** + * List every registered provider, irrespective of enablement. + * + * @return array + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function list(): array + { + return array_values($this->providers); + }//end list() + + /** + * Return the ids of every registered provider. + * + * @return array + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function listIds(): array + { + return array_keys($this->providers); + }//end listIds() +}//end class diff --git a/lib/Service/Gdpr/Regulator/RegulatorEscalateResult.php b/lib/Service/Gdpr/Regulator/RegulatorEscalateResult.php new file mode 100644 index 0000000000..900ead36f8 --- /dev/null +++ b/lib/Service/Gdpr/Regulator/RegulatorEscalateResult.php @@ -0,0 +1,207 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Regulator; + +use InvalidArgumentException; + +/** + * The outcome of escalating a DSAR case to a supervisory authority. + */ +final class RegulatorEscalateResult +{ + + /** + * The escalation was accepted by the authority (a reference was returned). + * + * @var string + */ + public const STATUS_ESCALATED = 'escalated'; + + /** + * No escalation was performed (no provider bound / the provider refused). + * This is NOT a success — the case was not escalated. + * + * @var string + */ + public const STATUS_REFUSED = 'refused'; + + /** + * The exact set of permitted statuses. + * + * @var array + */ + private const ALLOWED = [ + self::STATUS_ESCALATED, + self::STATUS_REFUSED, + ]; + + /** + * Constructor. + * + * @param string $status One of the two permitted statuses. + * @param string $providerId The id of the provider that produced this result. + * @param string $reference The regulator reference (empty when refused). + * @param string|null $message Optional human-readable detail. + * + * @throws InvalidArgumentException When $status is not one of the permitted values. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function __construct( + private readonly string $status, + private readonly string $providerId, + private readonly string $reference='', + private readonly ?string $message=null + ) { + if (in_array($status, self::ALLOWED, true) === false) { + throw new InvalidArgumentException( + sprintf( + 'RegulatorEscalateResult status "%s" is invalid; expected one of: %s', + $status, + implode(', ', self::ALLOWED) + ) + ); + } + }//end __construct() + + /** + * Build an escalated result carrying the authority's reference. + * + * @param string $providerId The provider that escalated. + * @param string $reference The regulator reference returned. + * @param string|null $message Optional detail. + * + * @return self + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public static function escalated(string $providerId, string $reference, ?string $message=null): self + { + return new self( + status: self::STATUS_ESCALATED, + providerId: $providerId, + reference: $reference, + message: $message + ); + }//end escalated() + + /** + * Build a refused result — escalation NOT performed. + * + * @param string $providerId The provider that refused. + * @param string|null $message Optional detail (why it was refused). + * + * @return self + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public static function refused(string $providerId, ?string $message=null): self + { + return new self(status: self::STATUS_REFUSED, providerId: $providerId, reference: '', message: $message); + }//end refused() + + /** + * The status. + * + * @return string One of the STATUS_* constants. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function getStatus(): string + { + return $this->status; + }//end getStatus() + + /** + * The id of the provider that produced this result. + * + * @return string + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function getProviderId(): string + { + return $this->providerId; + }//end getProviderId() + + /** + * The regulator reference (empty string when refused). + * + * @return string + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function getReference(): string + { + return $this->reference; + }//end getReference() + + /** + * Optional human-readable detail. + * + * @return string|null + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function getMessage(): ?string + { + return $this->message; + }//end getMessage() + + /** + * Whether the case was actually escalated. + * + * @return bool True only for STATUS_ESCALATED. + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function isEscalated(): bool + { + return $this->status === self::STATUS_ESCALATED; + }//end isEscalated() + + /** + * Serialise for API/case persistence. + * + * @return array{status: string, provider: string, reference: string, message: string|null} + * + * @spec openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md + */ + public function toArray(): array + { + return [ + 'status' => $this->status, + 'provider' => $this->providerId, + 'reference' => $this->reference, + 'message' => $this->message, + ]; + }//end toArray() +}//end class diff --git a/lib/Service/Gdpr/Retention/RetentionSweepService.php b/lib/Service/Gdpr/Retention/RetentionSweepService.php new file mode 100644 index 0000000000..c165aeb46e --- /dev/null +++ b/lib/Service/Gdpr/Retention/RetentionSweepService.php @@ -0,0 +1,220 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Gdpr\Retention; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Service\Gdpr\Case\CaseObjectAccessor; +use OCA\OpenRegister\Service\Gdpr\DataSubjectRequestService; +use OCA\OpenRegister\Service\RetentionService; +use OCP\AppFramework\Utility\ITimeFactory; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Legal-hold-aware retention sweep over data-subject-request cases. + */ +class RetentionSweepService +{ + /** + * Constructor. + * + * @param CaseObjectAccessor $accessor RBAC-scoped case query + audited delete. + * @param RetentionService $retentionService Legal-hold / immutability awareness. + * @param DataSubjectRequestService $dsrService Reused evidence-PII scrub (erase pseudonymise). + * @param ITimeFactory $time Time source for the `retainUntil` cut-off. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly CaseObjectAccessor $accessor, + private readonly RetentionService $retentionService, + private readonly DataSubjectRequestService $dsrService, + private readonly ITimeFactory $time, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Run a single retention pass. + * + * For every case whose `retainUntil` has passed and that is NOT under an + * active legal hold / immutable status: (dry-run) report it; else scrub its + * evidence PII via erase pseudonymise and hard-delete the dossier. Held or + * immutable expired cases are reported as skipped and left intact. + * + * @param bool $dryRun When true, report candidates without deleting/scrubbing. + * + * @return array{dryRun: bool, evaluated: int, purged: array, skippedHeld: array, withinWindow: int} + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Dry-run toggle mirrors AvgRetentionJob's contract. + * + * @spec openspec/changes/dsar-case-engine/specs/dsar-retention-sweep/spec.md + */ + public function runSweep(bool $dryRun=false): array + { + $now = $this->time->getTime(); + + $summary = [ + 'dryRun' => $dryRun, + 'evaluated' => 0, + 'purged' => [], + 'skippedHeld' => [], + 'withinWindow' => 0, + ]; + + foreach ($this->accessor->findAllCaseEntities() as $case) { + $summary['evaluated']++; + $uuid = (string) $case->getUuid(); + $data = $case->getObject(); + + // Not yet expired → untouched. + if ($this->isExpired(data: $data, now: $now) === false) { + $summary['withinWindow']++; + continue; + } + + // Legal-hold / immutability aware: skip held or immutable cases + // intact, exactly as the erase path already does. This is the + // fail-safe against irreversible destruction. + if ($this->isProtected(case: $case) === true) { + $summary['skippedHeld'][] = $uuid; + $this->logger->info( + message: sprintf('[RetentionSweep] case "%s" past window but under legal hold/immutable — skipped', $uuid) + ); + continue; + } + + if ($dryRun === true) { + $summary['purged'][] = $uuid; + continue; + } + + $this->purge(case: $case, data: $data); + $summary['purged'][] = $uuid; + }//end foreach + + return $summary; + }//end runSweep() + + /** + * Whether a case's `retainUntil` has passed. + * + * A missing / unparseable `retainUntil` is treated as NOT expired + * (fail-safe: never destroy a case whose window we cannot read). + * + * @param array $data The case payload. + * @param int $now Current unix time. + * + * @return bool True when the case is past its retention window. + */ + private function isExpired(array $data, int $now): bool + { + $retainUntil = (string) ($data['retainUntil'] ?? ''); + if ($retainUntil === '') { + return false; + } + + $expiryTs = strtotime($retainUntil); + if ($expiryTs === false) { + return false; + } + + return ($expiryTs < $now); + }//end isExpired() + + /** + * Whether a case is protected from destruction (legal hold or immutable). + * + * @param ObjectEntity $case The case entity. + * + * @return bool True when the case must be left intact. + */ + private function isProtected(ObjectEntity $case): bool + { + if ($this->retentionService->hasActiveLegalHold(object: $case) === true) { + return true; + } + + return ($this->retentionService->validateNotImmutable(object: $case) !== null); + }//end isProtected() + + /** + * Scrub a case's evidence PII (erase pseudonymise) then delete the dossier. + * + * Both actions are audited (the erase pins the DSAR activity; the delete + * goes through ObjectService's audited delete path). + * + * @param ObjectEntity $case The case entity. + * @param array $data The case payload. + * + * @return void + */ + private function purge(ObjectEntity $case, array $data): void + { + $uuid = (string) $case->getUuid(); + $subjectId = (string) ($data['subjectId'] ?? ''); + + // Scrub evidence PII via the existing erase primitive (pseudonymise), + // never a hand-rolled scrub. A missing subject id is logged, not fatal. + if ($subjectId !== '') { + try { + $this->dsrService->erase( + subjectId: $subjectId, + eraseMode: DataSubjectRequestService::ERASE_MODE_PSEUDONYMISE + ); + } catch (Throwable $e) { + $this->logger->error( + message: sprintf('[RetentionSweep] evidence scrub failed for case "%s": %s', $uuid, $e->getMessage()) + ); + } + } + + try { + $this->accessor->deleteForSweep(caseUuid: $uuid); + $this->logger->info( + message: sprintf('[RetentionSweep] case "%s" past window — dossier hard-deleted, evidence scrubbed', $uuid) + ); + } catch (Throwable $e) { + $this->logger->error( + message: sprintf('[RetentionSweep] dossier delete failed for case "%s": %s', $uuid, $e->getMessage()) + ); + } + }//end purge() +}//end class diff --git a/lib/Service/Geo/GeoFeatureCollectionBuilder.php b/lib/Service/Geo/GeoFeatureCollectionBuilder.php new file mode 100644 index 0000000000..1f0319f35c --- /dev/null +++ b/lib/Service/Geo/GeoFeatureCollectionBuilder.php @@ -0,0 +1,356 @@ + GeoJSON FeatureCollection + * (one Feature per object, geometry pulled from the geo property, + * remaining properties become Feature properties; honours an + * optional field allow-list, geometry always retained). + * - buildWfsResponse() — WFS GetFeature-compatible envelope + * (FeatureCollection + numberReturned + optional maxFeatures cap). + * - geodesicAreaM2() — geodesic polygon area in square meters + * for the `_area_m2` Feature property. + * + * Pure data-shaping: no I/O, no DB, no framework. The controller feeds + * it the already-RBAC-scoped listing rows so this class never widens + * access (no IDOR surface here). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Geo + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Geo; + +/** + * Builds GeoJSON FeatureCollections and WFS-compatible envelopes. + * + * @SuppressWarnings(PHPMD.StaticAccess) None used; documented for parity + * with sibling geo services. + */ +class GeoFeatureCollectionBuilder +{ + + /** + * GeoJSON geometry types recognised when scanning a row. + * + * @var string[] + */ + private const GEOJSON_TYPES = ['Point', 'Polygon', 'MultiPolygon', 'LineString']; + + /** + * WGS84 mean earth radius in meters (geodesic area approximation). + */ + private const EARTH_RADIUS_M = 6371008.8; + + /** + * Build a GeoJSON FeatureCollection from a list of object rows. + * + * Rows without a recognisable geometry are skipped (a marker-less + * row has nothing to plot). When `$fields` is non-null the Feature + * `properties` are restricted to that allow-list; the geometry is + * always included regardless of the allow-list (REQ-GEO-008). + * + * @param array $rows Object rows (assoc arrays). + * @param ?string $geoProperty Geo property name; null = auto-detect. + * @param string[]|null $fields Property allow-list, or null for all. + * @param bool $includeArea Add `_area_m2` for polygonal geometries. + * + * @return array A GeoJSON FeatureCollection. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + public function buildFeatureCollection( + array $rows, + ?string $geoProperty=null, + ?array $fields=null, + bool $includeArea=false + ): array { + $features = []; + foreach ($rows as $row) { + if (is_array($row) === false) { + continue; + } + + $feature = $this->buildFeature( + row: $row, + geoProperty: $geoProperty, + fields: $fields, + includeArea: $includeArea + ); + if ($feature !== null) { + $features[] = $feature; + } + } + + return [ + 'type' => 'FeatureCollection', + 'features' => $features, + ]; + + }//end buildFeatureCollection() + + /** + * Build a single GeoJSON Feature from an object row. + * + * @param array $row The object row. + * @param ?string $geoProperty Geo property name; null = auto-detect. + * @param string[]|null $fields Property allow-list, or null for all. + * @param bool $includeArea Add `_area_m2` for polygonal geometries. + * + * @return ?array The Feature, or null when no geometry is present. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + public function buildFeature( + array $row, + ?string $geoProperty=null, + ?array $fields=null, + bool $includeArea=false + ): ?array { + [$geomKey, $geometry] = $this->locateGeometry(row: $row, geoProperty: $geoProperty); + if ($geometry === null) { + return null; + } + + $properties = $this->shapeProperties(row: $row, geomKey: $geomKey, fields: $fields); + + if ($includeArea === true) { + $area = $this->geodesicAreaM2(geometry: $geometry); + if ($area !== null) { + $properties['_area_m2'] = $area; + } + } + + $feature = [ + 'type' => 'Feature', + 'geometry' => $geometry, + 'properties' => $properties, + ]; + + $id = ($row['id'] ?? ($row['@self']['id'] ?? null)); + if ($id !== null) { + $feature['id'] = $id; + } + + return $feature; + + }//end buildFeature() + + /** + * Build a WFS GetFeature-compatible response envelope. + * + * The body is a GeoJSON FeatureCollection (the format WFS 2.0 returns + * for `outputFormat=application/json`) plus the `numberReturned` + * count. `$maxFeatures` caps the feature list (WFS `count`/`maxFeatures`). + * + * @param array $rows Object rows (assoc arrays). + * @param ?string $geoProperty Geo property name; null = auto-detect. + * @param int|null $maxFeatures Optional cap on returned features. + * + * @return array A WFS-compatible GeoJSON FeatureCollection. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + public function buildWfsResponse(array $rows, ?string $geoProperty=null, ?int $maxFeatures=null): array + { + $collection = $this->buildFeatureCollection(rows: $rows, geoProperty: $geoProperty); + + if ($maxFeatures !== null && $maxFeatures >= 0) { + $collection['features'] = array_slice($collection['features'], 0, $maxFeatures); + } + + $collection['numberReturned'] = count($collection['features']); + + return $collection; + + }//end buildWfsResponse() + + /** + * Locate a geometry value within a row. + * + * @param array $row The object row. + * @param ?string $geoProperty Explicit property name, or null to auto-detect. + * + * @return array{0: ?string, 1: ?array} Tuple of [property key, geometry]. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + private function locateGeometry(array $row, ?string $geoProperty): array + { + if ($geoProperty !== null) { + $geometry = $this->coerceGeometry(value: ($row[$geoProperty] ?? null)); + return [$geoProperty, $geometry]; + } + + foreach ($row as $key => $value) { + $geometry = $this->coerceGeometry(value: $value); + if ($geometry !== null) { + return [(string) $key, $geometry]; + } + } + + return [null, null]; + + }//end locateGeometry() + + /** + * Shape a row's non-geometry properties into Feature properties. + * + * @param array $row The object row. + * @param ?string $geomKey The key holding the geometry (excluded). + * @param string[]|null $fields Allow-list, or null for all. + * + * @return array The Feature properties. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + private function shapeProperties(array $row, ?string $geomKey, ?array $fields): array + { + $properties = []; + foreach ($row as $key => $value) { + if ($key === $geomKey) { + continue; + } + + if ($fields !== null && in_array($key, $fields, true) === false) { + continue; + } + + $properties[$key] = $value; + } + + return $properties; + + }//end shapeProperties() + + /** + * Coerce a raw value to a GeoJSON geometry array. + * + * @param mixed $value The candidate. + * + * @return ?array The geometry when shape matches, null otherwise. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + private function coerceGeometry(mixed $value): ?array + { + if (is_array($value) === false) { + return null; + } + + if (in_array(($value['type'] ?? null), self::GEOJSON_TYPES, true) === false) { + return null; + } + + if (isset($value['coordinates']) === false || is_array($value['coordinates']) === false) { + return null; + } + + return $value; + + }//end coerceGeometry() + + /** + * Compute the geodesic area of a Polygon or MultiPolygon in m^2. + * + * Uses the spherical-excess shoelace formula over the outer rings + * (holes are subtracted). Sufficient for the `_area_m2` display + * metadata; precision-critical paths use PostGIS `ST_Area(geography)`. + * + * @param array $geometry GeoJSON Polygon or MultiPolygon. + * + * @return ?float The geodesic area in square meters, or null. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + public function geodesicAreaM2(array $geometry): ?float + { + $type = ($geometry['type'] ?? null); + if ($type !== 'Polygon' && $type !== 'MultiPolygon') { + return null; + } + + $polygons = []; + if ($type === 'Polygon') { + $polygons = [($geometry['coordinates'] ?? [])]; + } else { + $polygons = ($geometry['coordinates'] ?? []); + } + + $total = 0.0; + foreach ($polygons as $rings) { + if (is_array($rings) === false || count($rings) === 0) { + continue; + } + + $outer = abs($this->ringArea(ring: ($rings[0] ?? []))); + $holes = 0.0; + for ($i = 1; $i < count($rings); $i++) { + $holes += abs($this->ringArea(ring: $rings[$i])); + } + + $total += ($outer - $holes); + } + + return round($total, 2); + + }//end geodesicAreaM2() + + /** + * Spherical-excess area of a single ring in square meters. + * + * @param array $ring The ring vertices ([lon, lat] positions). + * + * @return float Signed area in square meters. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-008 + */ + private function ringArea(array $ring): float + { + $n = count($ring); + if ($n < 4) { + return 0.0; + } + + $area = 0.0; + for ($i = 0; $i < $n; $i++) { + $p1 = $ring[$i]; + $p2 = $ring[(($i + 1) % $n)]; + if (is_array($p1) === false || is_array($p2) === false) { + continue; + } + + if (is_numeric($p1[0] ?? null) === false || is_numeric($p2[0] ?? null) === false) { + continue; + } + + $lon1 = deg2rad((float) $p1[0]); + $lat1 = deg2rad((float) $p1[1]); + $lon2 = deg2rad((float) $p2[0]); + $lat2 = deg2rad((float) $p2[1]); + + $area += ($lon2 - $lon1) * (2 + sin($lat1) + sin($lat2)); + } + + $area = ($area * self::EARTH_RADIUS_M * self::EARTH_RADIUS_M / 2.0); + + return $area; + + }//end ringArea() +}//end class diff --git a/lib/Service/Geo/GeoFilter.php b/lib/Service/Geo/GeoFilter.php index 8be2e9a08c..a2fb4ee678 100644 --- a/lib/Service/Geo/GeoFilter.php +++ b/lib/Service/Geo/GeoFilter.php @@ -27,8 +27,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/geo-metadata-kaart/specs/geo-metadata-kaart/spec.md REQ-GEO-004 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-004 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ declare(strict_types=1); @@ -76,7 +76,7 @@ private function __construct( * * @throws InvalidArgumentException When malformed. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public static function fromBbox(string $bbox, ?string $property=null): self { @@ -124,7 +124,7 @@ public static function fromBbox(string $bbox, ?string $property=null): self * * @throws InvalidArgumentException When malformed. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public static function fromNearAndRadius( float|string $lon, @@ -159,7 +159,7 @@ public static function fromNearAndRadius( * * @throws InvalidArgumentException When geometry is malformed. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public static function fromWithinGeometry(array $geometry, ?string $property=null): self { @@ -178,7 +178,7 @@ public static function fromWithinGeometry(array $geometry, ?string $property=nul * * @throws InvalidArgumentException When geometry is malformed. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public static function fromIntersectsGeometry(array $geometry, ?string $property=null): self { @@ -197,7 +197,7 @@ public static function fromIntersectsGeometry(array $geometry, ?string $property * * @throws InvalidArgumentException When the geometry is invalid. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ private static function assertGeoJsonGeometry(mixed $geometry, string $opName): void { diff --git a/lib/Service/Geo/GeoFilterApplier.php b/lib/Service/Geo/GeoFilterApplier.php index 99558e2b30..d829b64c73 100644 --- a/lib/Service/Geo/GeoFilterApplier.php +++ b/lib/Service/Geo/GeoFilterApplier.php @@ -23,8 +23,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/geo-metadata-kaart/specs/geo-metadata-kaart/spec.md REQ-GEO-004 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-004 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ declare(strict_types=1); @@ -58,7 +58,7 @@ public function __construct( * * @return array The matching rows, in original order. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public function applyAll(array $rows, array $filters): array { @@ -85,7 +85,7 @@ public function applyAll(array $rows, array $filters): array * * @return bool True when the row passes every filter. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public function rowMatchesAll(array $row, array $filters): bool { @@ -134,7 +134,7 @@ public function extractGeometry(array $row, ?string $property): ?array * * @return ?array The geometry when shape matches, null otherwise. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ private function coerceGeometry(mixed $value): ?array { diff --git a/lib/Service/Geo/GeoFilterParser.php b/lib/Service/Geo/GeoFilterParser.php index 087332e68f..63733f6f5e 100644 --- a/lib/Service/Geo/GeoFilterParser.php +++ b/lib/Service/Geo/GeoFilterParser.php @@ -23,8 +23,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/geo-metadata-kaart/specs/geo-metadata-kaart/spec.md REQ-GEO-004 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-004 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ declare(strict_types=1); @@ -59,7 +59,7 @@ class GeoFilterParser * * @throws InvalidArgumentException When `geo.near`/`geo.radius` are mismatched. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public function fromQueryParams(array $params): array { @@ -115,7 +115,7 @@ public function fromQueryParams(array $params): array * * @throws InvalidArgumentException When malformed. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public function fromGeoSearchBody(array $body): array { diff --git a/lib/Service/Geo/GeoJsonGeometryValidator.php b/lib/Service/Geo/GeoJsonGeometryValidator.php new file mode 100644 index 0000000000..ab047c592e --- /dev/null +++ b/lib/Service/Geo/GeoJsonGeometryValidator.php @@ -0,0 +1,364 @@ += 2 positions. + * - geo:geometry — any of the above. + * - geo:bag — BAG nummeraanduiding id (16-digit string). + * + * Pure data-shaping: no I/O, no DB, no framework. Every method is a + * total function over its input so the rules are unit-testable in + * isolation (mandatory geo-metadata-shaping test target). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Geo + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Geo; + +/** + * Validates GeoJSON values against the geo property types of REQ-GEO-001. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * One branch per GeoJSON geometry type plus shape guards — the + * complexity is inherent to RFC 7946 and clearer kept in one place. + * @SuppressWarnings(PHPMD.NPathComplexity) + * Same rationale: each geometry type is a single validation branch. + */ +class GeoJsonGeometryValidator +{ + + /** + * Property types recognised by this validator (REQ-GEO-001). + * + * @var string[] + */ + public const GEO_TYPES = [ + 'geo:point', + 'geo:polygon', + 'geo:multipolygon', + 'geo:linestring', + 'geo:geometry', + 'geo:bag', + ]; + + /** + * Whether a schema property type string is a geo type. + * + * @param string $type The property type (e.g. `geo:point`). + * + * @return bool + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + public function isGeoType(string $type): bool + { + return in_array(strtolower($type), self::GEO_TYPES, true); + + }//end isGeoType() + + /** + * Validate a value for a given geo property type. + * + * Returns a list of human-readable error strings; an empty list + * means the value is valid. Never throws — callers map a non-empty + * list to a 422 response (REQ-GEO-001). + * + * @param string $type The geo property type. + * @param mixed $value The candidate value. + * + * @return string[] Validation errors (empty = valid). + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + public function validate(string $type, mixed $value): array + { + $type = strtolower($type); + + if ($type === 'geo:bag') { + return $this->validateBag(value: $value); + } + + if (is_array($value) === false) { + return ['geometry MUST be a GeoJSON object']; + } + + $geometryType = ($value['type'] ?? null); + + switch ($type) { + case 'geo:point': + return $this->validatePoint(value: $value); + case 'geo:polygon': + return $this->validatePolygon(value: $value); + case 'geo:multipolygon': + return $this->validateMultiPolygon(value: $value); + case 'geo:linestring': + return $this->validateLineString(value: $value); + case 'geo:geometry': + return $this->validateAny(geometryType: $geometryType, value: $value); + default: + return ["unknown geo property type: {$type}"]; + } + + }//end validate() + + /** + * Validate any supported GeoJSON geometry (geo:geometry). + * + * @param mixed $geometryType The declared `type` field. + * @param array $value The GeoJSON value. + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + private function validateAny(mixed $geometryType, array $value): array + { + switch ($geometryType) { + case 'Point': + return $this->validatePoint(value: $value); + case 'Polygon': + return $this->validatePolygon(value: $value); + case 'MultiPolygon': + return $this->validateMultiPolygon(value: $value); + case 'LineString': + return $this->validateLineString(value: $value); + default: + return ['geometry type MUST be one of Point, Polygon, MultiPolygon, LineString']; + } + + }//end validateAny() + + /** + * Validate a GeoJSON Point. + * + * @param array $value The candidate value. + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + private function validatePoint(array $value): array + { + if (($value['type'] ?? null) !== 'Point') { + return ['Point geometry MUST have type "Point"']; + } + + $coords = ($value['coordinates'] ?? null); + if (is_array($coords) === false || count($coords) < 2) { + return ['Point coordinates MUST be a [longitude, latitude] array']; + } + + return $this->validatePosition(position: $coords); + + }//end validatePoint() + + /** + * Validate a single position (lon, lat) per RFC 7946. + * + * @param array $position The position array. + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + private function validatePosition(array $position): array + { + if (is_numeric($position[0] ?? null) === false || is_numeric($position[1] ?? null) === false) { + return ['coordinates MUST be numeric']; + } + + $lon = (float) $position[0]; + $lat = (float) $position[1]; + $errors = []; + if ($lon < -180.0 || $lon > 180.0) { + $errors[] = 'longitude MUST be between -180 and 180 (it is the first element per RFC 7946)'; + } + + if ($lat < -90.0 || $lat > 90.0) { + $errors[] = 'latitude MUST be between -90 and 90 (it is the second element per RFC 7946)'; + } + + return $errors; + + }//end validatePosition() + + /** + * Validate a GeoJSON LineString (>= 2 positions). + * + * @param array $value The candidate value. + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + private function validateLineString(array $value): array + { + if (($value['type'] ?? null) !== 'LineString') { + return ['LineString geometry MUST have type "LineString"']; + } + + $coords = ($value['coordinates'] ?? null); + if (is_array($coords) === false || count($coords) < 2) { + return ['LineString MUST contain at least 2 coordinate positions']; + } + + $errors = []; + foreach ($coords as $position) { + if (is_array($position) === false) { + $errors[] = 'each LineString position MUST be a coordinate array'; + continue; + } + + $errors = array_merge($errors, $this->validatePosition(position: $position)); + } + + return $errors; + + }//end validateLineString() + + /** + * Validate a GeoJSON Polygon (closed rings, >= 4 positions per ring). + * + * @param array $value The candidate value. + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + private function validatePolygon(array $value): array + { + if (($value['type'] ?? null) !== 'Polygon') { + return ['Polygon geometry MUST have type "Polygon"']; + } + + $rings = ($value['coordinates'] ?? null); + if (is_array($rings) === false || count($rings) === 0) { + return ['Polygon MUST have at least one linear ring']; + } + + return $this->validateRings(rings: $rings, label: 'Polygon'); + + }//end validatePolygon() + + /** + * Validate a GeoJSON MultiPolygon (each polygon individually closed). + * + * @param array $value The candidate value. + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + private function validateMultiPolygon(array $value): array + { + if (($value['type'] ?? null) !== 'MultiPolygon') { + return ['MultiPolygon geometry MUST have type "MultiPolygon"']; + } + + $polygons = ($value['coordinates'] ?? null); + if (is_array($polygons) === false || count($polygons) === 0) { + return ['MultiPolygon MUST contain at least one polygon']; + } + + $errors = []; + foreach ($polygons as $index => $rings) { + if (is_array($rings) === false || count($rings) === 0) { + $errors[] = "MultiPolygon polygon #{$index} MUST have at least one ring"; + continue; + } + + $errors = array_merge($errors, $this->validateRings(rings: $rings, label: "MultiPolygon polygon #{$index}")); + } + + return $errors; + + }//end validateMultiPolygon() + + /** + * Validate a set of polygon rings: min 4 positions and closure. + * + * @param array $rings The rings to validate. + * @param string $label Context label for error messages. + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + private function validateRings(array $rings, string $label): array + { + $errors = []; + foreach ($rings as $ringIndex => $ring) { + if (is_array($ring) === false || count($ring) < 4) { + $errors[] = "{$label} ring #{$ringIndex} MUST contain at least 4 coordinate positions"; + continue; + } + + $first = $ring[0]; + $last = $ring[(count($ring) - 1)]; + if ($first !== $last) { + $errors[] = "{$label} ring #{$ringIndex} is unclosed: first and last coordinate MUST be identical"; + } + + foreach ($ring as $position) { + if (is_array($position) === false) { + $errors[] = "{$label} ring #{$ringIndex} has a non-array position"; + continue; + } + + $errors = array_merge($errors, $this->validatePosition(position: $position)); + } + }//end foreach + + return $errors; + + }//end validateRings() + + /** + * Validate a BAG nummeraanduiding identifier (REQ-GEO-001). + * + * Format: 4-digit gemeentecode + 2-digit objecttypecode + 10-digit + * volgnummer = 16 digits total. + * + * @param mixed $value The candidate BAG id. + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-001 + */ + private function validateBag(mixed $value): array + { + if (is_string($value) === false) { + return ['BAG reference MUST be a 16-digit nummeraanduiding string']; + } + + if (preg_match('/^[0-9]{4}[0-9]{2}[0-9]{10}$/', $value) !== 1) { + return [ + 'BAG nummeraanduiding MUST be 16 digits: 4-digit gemeentecode + ' + .'2-digit objecttypecode + 10-digit volgnummer', + ]; + } + + return []; + + }//end validateBag() +}//end class diff --git a/lib/Service/Geo/GeoSpatialEvaluator.php b/lib/Service/Geo/GeoSpatialEvaluator.php index 4d8c753463..6fe6d4fa60 100644 --- a/lib/Service/Geo/GeoSpatialEvaluator.php +++ b/lib/Service/Geo/GeoSpatialEvaluator.php @@ -33,9 +33,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/geo-metadata-kaart/specs/geo-metadata-kaart/spec.md REQ-GEO-004 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-004 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ declare(strict_types=1); @@ -73,7 +73,7 @@ class GeoSpatialEvaluator * * @return bool True when the row passes the filter; null geometries always fail. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ public function matches(?array $rowGeometry, GeoFilter $filter): bool { @@ -109,7 +109,7 @@ public function matches(?array $rowGeometry, GeoFilter $filter): bool * * @return float Distance in meters. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ public function haversineMeters(float $lon1, float $lat1, float $lon2, float $lat2): float { @@ -132,7 +132,7 @@ public function haversineMeters(float $lon1, float $lat1, float $lon2, float $la * * @return bool * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ private function matchesBbox(array $geometry, array $bbox): bool { @@ -158,7 +158,7 @@ private function matchesBbox(array $geometry, array $bbox): bool * * @return bool * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-004-spatial-queries-in-the-api */ private function matchesNear(array $geometry, array $payload): bool { @@ -185,7 +185,7 @@ private function matchesNear(array $geometry, array $payload): bool * * @return bool * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ private function matchesWithin(array $rowGeometry, array $predicateGeometry): bool { @@ -206,7 +206,7 @@ private function matchesWithin(array $rowGeometry, array $predicateGeometry): bo * * @return bool * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ private function matchesIntersects(array $rowGeometry, array $predicateGeometry): bool { @@ -250,7 +250,7 @@ private function matchesIntersects(array $rowGeometry, array $predicateGeometry) * @SuppressWarnings(PHPMD.NPathComplexity) * Same: each geometry type is one branch. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ private function extractRepresentativePoint(array $geometry): ?array { @@ -309,7 +309,7 @@ private function extractRepresentativePoint(array $geometry): ?array * * @return ?array `[lon, lat]` centroid, or null when the ring is empty. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ private function ringCentroid(array $ring): ?array { @@ -350,7 +350,7 @@ private function ringCentroid(array $ring): ?array * * @return bool * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ private function pointInPolygonGeometry(array $point, array $geometry): bool { @@ -375,7 +375,7 @@ private function pointInPolygonGeometry(array $point, array $geometry): bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Standard Bevis & Chatelain ray-casting algorithm; the branches are inherent to the geometry test. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/geo-metadata-kaart/spec.md#requirement-req-geo-012-solr-and-elasticsearch-spatial-query-support */ private function pointInRing(array $point, array $ring): bool { diff --git a/lib/Service/Geo/PdokGeocoder.php b/lib/Service/Geo/PdokGeocoder.php new file mode 100644 index 0000000000..01a09e3c4f --- /dev/null +++ b/lib/Service/Geo/PdokGeocoder.php @@ -0,0 +1,328 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Geo; + +use OCP\App\IAppManager; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Geocoder that wraps the PDOK Locatieserver via OpenConnector. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * Coupling to NC's IAppManager + the DI container is required to + * detect and lazily resolve the OpenConnector CallService. + */ +class PdokGeocoder +{ + + /** + * PDOK Locatieserver v3.1 base URL (REQ-GEO-005). + */ + public const LOCATIESERVER_BASE = 'https://api.pdok.nl/bzk/locatieserver/search/v3_1'; + + /** + * Optional transport override: `fn(string $url, array $params): ?array`. + * + * Injected in tests so response shaping is exercised without a live + * OpenConnector. Null in production — the OpenConnector CallService + * is resolved lazily. + * + * @var callable|null + */ + private $transport; + + /** + * Constructor. + * + * @param IAppManager $appManager NC app manager — detects whether + * OpenConnector is enabled. + * @param ContainerInterface $container DI container — lazily resolves + * OpenConnector's CallService. + * @param LoggerInterface $logger Logger for graceful-degrade traces. + * @param callable|null $transport Optional transport override (tests). + * + * @return void + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly ContainerInterface $container, + private readonly LoggerInterface $logger, + ?callable $transport=null + ) { + $this->transport = $transport; + + }//end __construct() + + /** + * Whether geocoding is currently available (OpenConnector enabled). + * + * @return bool + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 + */ + public function isAvailable(): bool + { + if ($this->transport !== null) { + return true; + } + + try { + return $this->appManager->isInstalled('openconnector'); + } catch (Throwable $e) { + return false; + } + + }//end isAvailable() + + /** + * Forward geocoding: free-text address query -> up to N suggestions. + * + * Each suggestion is `{ display, type, lon, lat, bagId }`. On any + * failure (OpenConnector down, upstream error, malformed payload) + * an empty list is returned — geocoding is non-blocking (REQ-GEO-005). + * + * @param string $query The free-text address. + * @param int $maxItems Maximum suggestions to return. + * @param bool $bagOnly Restrict to BAG `type:adres` results. + * + * @return array> Suggestions (possibly empty). + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-006 + */ + public function geocodeFree(string $query, int $maxItems=5, bool $bagOnly=false): array + { + $query = trim($query); + if ($query === '') { + return []; + } + + $params = [ + 'q' => $query, + 'rows' => max(1, $maxItems), + ]; + if ($bagOnly === true) { + $params['fq'] = 'type:adres'; + } + + $payload = $this->request(endpoint: 'free', params: $params); + if ($payload === null) { + return []; + } + + return $this->shapeSuggestions(payload: $payload, maxItems: $maxItems); + + }//end geocodeFree() + + /** + * Reverse geocoding: coordinates -> nearest address (or null). + * + * @param float $longitude WGS84 longitude. + * @param float $latitude WGS84 latitude. + * + * @return array|null The nearest address, or null. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 + */ + public function reverseGeocode(float $longitude, float $latitude): ?array + { + $payload = $this->request( + endpoint: 'reverse', + params: [ + 'lon' => $longitude, + 'lat' => $latitude, + 'rows' => 1, + ] + ); + if ($payload === null) { + return null; + } + + $suggestions = $this->shapeSuggestions(payload: $payload, maxItems: 1); + return ($suggestions[0] ?? null); + + }//end reverseGeocode() + + /** + * Shape a raw Locatieserver payload into a suggestion list. + * + * Pure: tolerant of the Solr-style `response.docs` envelope PDOK + * returns. Missing/garbled docs yield an empty list. + * + * @param array $payload The decoded Locatieserver payload. + * @param int $maxItems Maximum suggestions. + * + * @return array> + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 + */ + public function shapeSuggestions(array $payload, int $maxItems): array + { + $docs = ($payload['response']['docs'] ?? ($payload['docs'] ?? null)); + if (is_array($docs) === false) { + return []; + } + + $suggestions = []; + foreach ($docs as $doc) { + if (is_array($doc) === false) { + continue; + } + + $coords = $this->parseCentroid(point: ($doc['centroide_ll'] ?? null)); + + $suggestions[] = [ + 'display' => (string) ($doc['weergavenaam'] ?? ''), + 'type' => (string) ($doc['type'] ?? ''), + 'lon' => $coords[0], + 'lat' => $coords[1], + 'bagId' => ($doc['nummeraanduiding_id'] ?? null), + ]; + + if (count($suggestions) >= $maxItems) { + break; + } + }//end foreach + + return $suggestions; + + }//end shapeSuggestions() + + /** + * Parse a PDOK `POINT(lon lat)` WKT centroid into `[lon, lat]`. + * + * @param mixed $point The `centroide_ll` value. + * + * @return array{0: ?float, 1: ?float} + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 + */ + private function parseCentroid(mixed $point): array + { + if (is_string($point) === false) { + return [null, null]; + } + + if (preg_match('/POINT\(([-0-9.]+)\s+([-0-9.]+)\)/i', $point, $m) === 1) { + return [(float) $m[1], (float) $m[2]]; + } + + return [null, null]; + + }//end parseCentroid() + + /** + * Perform a Locatieserver request, degrading gracefully to null. + * + * @param string $endpoint The Locatieserver endpoint (`free`/`reverse`). + * @param array $params Query parameters. + * + * @return array|null The decoded payload, or null on any failure. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 + */ + private function request(string $endpoint, array $params): ?array + { + $url = (self::LOCATIESERVER_BASE.'/'.$endpoint); + + if ($this->transport !== null) { + try { + $result = ($this->transport)($url, $params); + if (is_array($result) === true) { + return $result; + } + + return null; + } catch (Throwable $e) { + $this->logger->warning('PDOK geocoding transport failed: '.$e->getMessage()); + return null; + } + } + + if ($this->isAvailable() === false) { + $this->logger->info('PDOK geocoding skipped: OpenConnector not available'); + return null; + } + + try { + $callService = $this->container->get('OCA\\OpenConnector\\Service\\CallService'); + $response = $callService->call(null, $url, 'GET', ['query' => $params]); + return $this->decode(response: $response); + } catch (Throwable $e) { + $this->logger->warning('PDOK geocoding via OpenConnector failed: '.$e->getMessage()); + return null; + } + + }//end request() + + /** + * Decode an OpenConnector CallService response into an array. + * + * @param mixed $response The CallService return value. + * + * @return array|null + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-005 + */ + private function decode(mixed $response): ?array + { + if (is_array($response) === true) { + return $response; + } + + $body = null; + if (is_object($response) === true && method_exists($response, 'getResponse') === true) { + $resp = $response->getResponse(); + $body = ($resp['body'] ?? null); + } + + if (is_string($response) === true) { + $body = $response; + } + + if (is_string($body) === false) { + return null; + } + + $decoded = json_decode($body, true); + if (is_array($decoded) === true) { + return $decoded; + } + + return null; + + }//end decode() +}//end class diff --git a/lib/Service/Geo/RdCrsTransformer.php b/lib/Service/Geo/RdCrsTransformer.php new file mode 100644 index 0000000000..de33a79e7e --- /dev/null +++ b/lib/Service/Geo/RdCrsTransformer.php @@ -0,0 +1,185 @@ + RD New (EPSG:28992) coordinate transform. + * + * Implements the CRS negotiation of REQ-GEO-015: + * + * - rdToWgs84() — RD New (EPSG:28992) -> WGS84 (EPSG:4326). + * - wgs84ToRd() — WGS84 (EPSG:4326) -> RD New (EPSG:28992). + * - isSupportedCrs() — only EPSG:4326 and EPSG:28992 are accepted; + * anything else (e.g. EPSG:3857) is rejected so + * the controller can return a 406. + * + * Uses the well-known approximate RD <-> WGS84 polynomial (Schreutelkamp + * & Strang van Hees), accurate to ~0.25 m within the Netherlands — fine + * for storage/display. Precision-critical geodetic work uses the full + * RDNAPTRANS2018 grid, out of scope here. + * + * Pure data-shaping: no I/O, no DB, no framework. Stored geometry is + * always WGS84 internally (REQ-GEO-002 / REQ-GEO-015). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Geo + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://www.OpenRegister.app + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-015 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Geo; + +/** + * Transforms coordinates between WGS84 and RD New (EPSG:28992). + * + * @SuppressWarnings(PHPMD.ShortVariable) + * `$x` / `$y` / `$p` / `$q` are the canonical variable names from the + * Schreutelkamp & Strang van Hees RD-transformation reference. + */ +class RdCrsTransformer +{ + + public const CRS_WGS84 = 'EPSG:4326'; + + public const CRS_RD = 'EPSG:28992'; + + /** + * Reference point: Onze Lieve Vrouwetoren, Amersfoort. + */ + private const X0 = 155000.0; + + private const Y0 = 463000.0; + + private const PHI0 = 52.15517440; + + private const LAM0 = 5.38720621; + + /** + * Whether a CRS string is supported (REQ-GEO-015). + * + * @param string $crs The CRS identifier (e.g. `EPSG:28992`). + * + * @return bool + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-015 + */ + public function isSupportedCrs(string $crs): bool + { + return in_array(strtoupper($crs), [self::CRS_WGS84, self::CRS_RD], true); + + }//end isSupportedCrs() + + /** + * The supported CRS identifiers (for 406 error messages). + * + * @return string[] + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-015 + */ + public function supportedCrs(): array + { + return [self::CRS_WGS84, self::CRS_RD]; + + }//end supportedCrs() + + /** + * Transform RD New (x, y in meters) to WGS84 (lon, lat in degrees). + * + * @param float $x RD x-coordinate (Easting) in meters. + * @param float $y RD y-coordinate (Northing) in meters. + * + * @return array{0: float, 1: float} `[longitude, latitude]` in WGS84. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-015 + */ + public function rdToWgs84(float $x, float $y): array + { + $dX = (($x - self::X0) * 1e-5); + $dY = (($y - self::Y0) * 1e-5); + + $sumPhi = 3235.65389 * $dY; + $sumPhi += -32.58297 * ($dX ** 2); + $sumPhi += -0.24750 * ($dY ** 2); + $sumPhi += -0.84978 * ($dX ** 2) * $dY; + $sumPhi += -0.06550 * ($dY ** 3); + $sumPhi += -0.01709 * ($dX ** 2) * ($dY ** 2); + $sumPhi += -0.00738 * $dX; + $sumPhi += 0.00530 * ($dX ** 4); + $sumPhi += -0.00039 * ($dX ** 2) * ($dY ** 3); + $sumPhi += 0.00033 * ($dX ** 4) * $dY; + $sumPhi += -0.00012 * $dX * $dY; + + $latitude = (self::PHI0 + ($sumPhi / 3600.0)); + + $sumLam = 5260.52916 * $dX; + $sumLam += 105.94684 * $dX * $dY; + $sumLam += 2.45656 * $dX * ($dY ** 2); + $sumLam += -0.81885 * ($dX ** 3); + $sumLam += 0.05594 * $dX * ($dY ** 3); + $sumLam += -0.05607 * ($dX ** 3) * $dY; + $sumLam += 0.01199 * $dY; + $sumLam += -0.00256 * ($dX ** 3) * ($dY ** 2); + $sumLam += 0.00128 * $dX * ($dY ** 4); + $sumLam += 0.00022 * ($dY ** 2); + $sumLam += -0.00022 * ($dX ** 2); + $sumLam += 0.00026 * ($dX ** 5); + + $longitude = (self::LAM0 + ($sumLam / 3600.0)); + + return [round($longitude, 7), round($latitude, 7)]; + + }//end rdToWgs84() + + /** + * Transform WGS84 (lon, lat in degrees) to RD New (x, y in meters). + * + * @param float $longitude WGS84 longitude in degrees. + * @param float $latitude WGS84 latitude in degrees. + * + * @return array{0: float, 1: float} `[x, y]` in RD New meters. + * + * @spec openspec/specs/geo-metadata-kaart/spec.md REQ-GEO-015 + */ + public function wgs84ToRd(float $longitude, float $latitude): array + { + $dPhi = (0.36 * ($latitude - self::PHI0)); + $dLam = (0.36 * ($longitude - self::LAM0)); + + $sumX = 190094.945 * $dLam; + $sumX += -11832.228 * $dPhi * $dLam; + $sumX += -114.221 * $dPhi * ($dLam ** 3); + $sumX += -32.391 * ($dLam ** 3); + $sumX += -0.705 * $dPhi; + $sumX += -2.340 * ($dPhi ** 3) * $dLam; + $sumX += -0.608 * $dPhi * ($dLam ** 5); + $sumX += -0.008 * ($dLam ** 2); + $sumX += 0.148 * ($dPhi ** 2) * ($dLam ** 3); + + $x = (self::X0 + $sumX); + + $sumY = 309056.544 * $dPhi; + $sumY += 3638.893 * ($dLam ** 2); + $sumY += 73.077 * ($dPhi ** 2); + $sumY += -157.984 * $dPhi * ($dLam ** 2); + $sumY += 59.788 * ($dPhi ** 3); + $sumY += 0.433 * $dLam; + $sumY += -6.439 * ($dPhi ** 2) * ($dLam ** 2); + $sumY += -0.032 * $dPhi * $dLam; + $sumY += 0.092 * ($dLam ** 4); + $sumY += -0.054 * $dPhi * ($dLam ** 4); + + $y = (self::Y0 + $sumY); + + return [round($x, 3), round($y, 3)]; + + }//end wgs84ToRd() +}//end class diff --git a/lib/Service/GraphQL/GraphQLErrorFormatter.php b/lib/Service/GraphQL/GraphQLErrorFormatter.php index fbf8ca21f1..2257a51abb 100644 --- a/lib/Service/GraphQL/GraphQLErrorFormatter.php +++ b/lib/Service/GraphQL/GraphQLErrorFormatter.php @@ -39,7 +39,7 @@ class GraphQLErrorFormatter * * @return array The formatted error * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-8 + * @spec openspec/specs/graphql-api/spec.md */ public function format(Error $error): array { @@ -71,7 +71,7 @@ public function format(Error $error): array * * @return Error The GraphQL error * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler */ public static function fieldForbidden(string $field, array $path): Error { @@ -95,7 +95,7 @@ public static function fieldForbidden(string $field, array $path): Error * * @return Error The GraphQL error * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler */ public static function notFound(string $type, string $id): Error { diff --git a/lib/Service/GraphQL/GraphQLResolver.php b/lib/Service/GraphQL/GraphQLResolver.php index 66fd35b9e8..d8a188892d 100644 --- a/lib/Service/GraphQL/GraphQLResolver.php +++ b/lib/Service/GraphQL/GraphQLResolver.php @@ -9,17 +9,17 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister\Service\GraphQL - * @author Conduction B.V. + * @category Service + * @package OCA\OpenRegister\Service\GraphQL + * @author Conduction B.V. * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://OpenRegister.app + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-38 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-41 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-log-operations-to-the-audit-trail + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-resolver-must-reset-state-between-requests */ namespace OCA\OpenRegister\Service\GraphQL; @@ -127,7 +127,7 @@ public function __construct( * * @throws Error If object not found or access denied * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler */ public function resolveSingle(Schema $schema, mixed $root, array $args): ?array { @@ -178,7 +178,7 @@ public function resolveSingle(Schema $schema, mixed $root, array $args): ?array * * @return array The connection result * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * The connection-build pipeline is intentionally inline: @@ -313,7 +313,7 @@ public function resolveList(Schema $schema, mixed $root, array $args): array * * @throws Error When validation fails or RBAC denies the request. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-19 + * @spec openspec/specs/graphql-api/spec.md */ private function resolveGroupBy(Schema $schema, ?Register $register, array $rawArgs): ?array { @@ -378,7 +378,7 @@ private function resolveGroupBy(Schema $schema, ?Register $register, array $rawA * * @throws Error If access denied or validation fails * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-38 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-log-operations-to-the-audit-trail */ public function resolveCreate(Schema $schema, array $args, ?string $operationName=null): array { @@ -441,7 +441,7 @@ public function resolveCreate(Schema $schema, array $args, ?string $operationNam * * @throws Error If access denied, not found, or validation fails * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-38 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-log-operations-to-the-audit-trail */ public function resolveUpdate(Schema $schema, array $args, ?string $operationName=null): array { @@ -516,7 +516,7 @@ public function resolveUpdate(Schema $schema, array $args, ?string $operationNam * * @throws Error If access denied or not found * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-38 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-log-operations-to-the-audit-trail */ public function resolveDelete(Schema $schema, array $args): bool { @@ -547,7 +547,7 @@ public function resolveDelete(Schema $schema, array $args): bool * * @return Deferred A deferred value that resolves after batching * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-20 + * @spec openspec/specs/graphql-api/spec.md */ public function resolveRelation(string $uuid, Schema $parentSchema, array $path): Deferred { @@ -575,7 +575,7 @@ function () use ($uuid) { * * @return array> The audit trail entries * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-38 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-log-operations-to-the-audit-trail */ public function resolveAuditTrail(string $objectUuid, int $last=10): array { @@ -600,7 +600,7 @@ public function resolveAuditTrail(string $objectUuid, int $last=10): array * * @return array> The referencing objects * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler */ public function resolveUsedBy(string $objectUuid): array { @@ -614,7 +614,7 @@ public function resolveUsedBy(string $objectUuid): array * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-20 + * @spec openspec/specs/graphql-api/spec.md */ private function flushRelationBuffer(): void { @@ -647,7 +647,7 @@ private function flushRelationBuffer(): void * * @throws Error If permission denied * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler */ private function checkSchemaPermission(Schema $schema, string $action): void { @@ -823,7 +823,7 @@ private function formatDateOrPassthrough(mixed $value): mixed * * @return Register|null The register * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ private function findRegisterForSchema(Schema $schema): ?Register { @@ -855,7 +855,7 @@ private function findRegisterForSchema(Schema $schema): ?Register * * @return string The encoded cursor * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-21 + * @spec openspec/specs/graphql-api/spec.md */ private function encodeCursor(string $uuid, int|string $offset): string { @@ -870,7 +870,7 @@ private function encodeCursor(string $uuid, int|string $offset): string * * @return Error[] * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-41 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-resolver-must-reset-state-between-requests */ public function getPartialErrors(): array { @@ -883,7 +883,7 @@ public function getPartialErrors(): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-41 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-resolver-must-reset-state-between-requests */ public function reset(): void { diff --git a/lib/Service/GraphQL/GraphQLService.php b/lib/Service/GraphQL/GraphQLService.php index 98bd4cc193..fc50fe1cb1 100644 --- a/lib/Service/GraphQL/GraphQLService.php +++ b/lib/Service/GraphQL/GraphQLService.php @@ -15,7 +15,7 @@ * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ namespace OCA\OpenRegister\Service\GraphQL; @@ -99,7 +99,7 @@ public function __construct( * * @return array The execution result * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-38 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-log-operations-to-the-audit-trail */ public function execute(string $query, ?array $variables=null, ?string $operationName=null): array { @@ -171,7 +171,7 @@ public function execute(string $query, ?array $variables=null, ?string $operatio * * @return Schema The GraphQL schema * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ private function getSchema(): Schema { @@ -193,7 +193,7 @@ private function getSchema(): Schema * * @return array The context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler */ private function createContext(?string $operationName): array { @@ -221,7 +221,7 @@ private function createContext(?string $operationName): array * * @throws Error If introspection is not allowed * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ private function checkIntrospection(\GraphQL\Language\AST\DocumentNode $document): void { @@ -289,7 +289,7 @@ private function checkIntrospection(\GraphQL\Language\AST\DocumentNode $document * * @return bool True if introspection fields are present * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ private function selectionSetHasIntrospection( \GraphQL\Language\AST\SelectionSetNode $selectionSet @@ -319,7 +319,7 @@ private function selectionSetHasIntrospection( * * @throws Error If rate limited * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ private function checkRateLimit(): void { diff --git a/lib/Service/GraphQL/QueryComplexityAnalyzer.php b/lib/Service/GraphQL/QueryComplexityAnalyzer.php index d442cef9c5..a24952c2fa 100644 --- a/lib/Service/GraphQL/QueryComplexityAnalyzer.php +++ b/lib/Service/GraphQL/QueryComplexityAnalyzer.php @@ -15,7 +15,7 @@ * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ namespace OCA\OpenRegister\Service\GraphQL; @@ -69,7 +69,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ public function setSchemaCosts(array $costs): void { @@ -87,7 +87,7 @@ public function setSchemaCosts(array $costs): void * * @throws Error If query exceeds complexity limits * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ public function analyze(DocumentNode $document, ?array $variables=null): array { @@ -169,7 +169,7 @@ public function analyze(DocumentNode $document, ?array $variables=null): array * * @return array{depth: int, cost: int} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ private function analyzeSelectionSet( SelectionSetNode $selectionSet, @@ -231,7 +231,7 @@ private function analyzeSelectionSet( * * @return int The multiplier * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ private function getListMultiplier(FieldNode $field, ?array $variables): int { @@ -269,7 +269,7 @@ private function getListMultiplier(FieldNode $field, ?array $variables): int * * @return int The cost * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ private function getResolverCost(string $fieldName): int { diff --git a/lib/Service/GraphQL/Scalar/DateTimeType.php b/lib/Service/GraphQL/Scalar/DateTimeType.php index 1a09312742..3751b6f596 100644 --- a/lib/Service/GraphQL/Scalar/DateTimeType.php +++ b/lib/Service/GraphQL/Scalar/DateTimeType.php @@ -57,7 +57,7 @@ class DateTimeType extends ScalarType * * @throws Error If the value cannot be serialized * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function serialize(mixed $value): string { @@ -84,7 +84,7 @@ public function serialize(mixed $value): string * * @throws Error If the value is not a valid ISO 8601 date-time * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseValue(mixed $value): string { @@ -123,7 +123,7 @@ public function parseValue(mixed $value): string * * @throws Error If the node is not a string * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseLiteral(\GraphQL\Language\AST\Node $valueNode, ?array $variables=null): string { diff --git a/lib/Service/GraphQL/Scalar/EmailType.php b/lib/Service/GraphQL/Scalar/EmailType.php index c04a32a6c7..7b8fc42f70 100644 --- a/lib/Service/GraphQL/Scalar/EmailType.php +++ b/lib/Service/GraphQL/Scalar/EmailType.php @@ -57,7 +57,7 @@ class EmailType extends ScalarType * * @throws Error If the value is not a string * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function serialize(mixed $value): string { @@ -80,7 +80,7 @@ public function serialize(mixed $value): string * * @throws Error If the value is not a valid email * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseValue(mixed $value): string { @@ -110,7 +110,7 @@ public function parseValue(mixed $value): string * * @throws Error If the node is not a string * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseLiteral(\GraphQL\Language\AST\Node $valueNode, ?array $variables=null): string { diff --git a/lib/Service/GraphQL/Scalar/JsonType.php b/lib/Service/GraphQL/Scalar/JsonType.php index 368f9c6b3a..8469bd5f87 100644 --- a/lib/Service/GraphQL/Scalar/JsonType.php +++ b/lib/Service/GraphQL/Scalar/JsonType.php @@ -58,7 +58,7 @@ class JsonType extends ScalarType * * @return mixed The serialized value * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-2 + * @spec openspec/specs/graphql-api/spec.md */ public function serialize(mixed $value): mixed { @@ -73,7 +73,7 @@ public function serialize(mixed $value): mixed * * @return mixed The parsed value * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseValue(mixed $value): mixed { @@ -89,7 +89,7 @@ public function parseValue(mixed $value): mixed * * @return mixed The parsed value * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseLiteral(\GraphQL\Language\AST\Node $valueNode, ?array $variables=null): mixed { diff --git a/lib/Service/GraphQL/Scalar/UploadType.php b/lib/Service/GraphQL/Scalar/UploadType.php index 9cf63ea89b..c42104d774 100644 --- a/lib/Service/GraphQL/Scalar/UploadType.php +++ b/lib/Service/GraphQL/Scalar/UploadType.php @@ -57,7 +57,7 @@ class UploadType extends ScalarType * * @return mixed The serialized value * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function serialize(mixed $value): mixed { @@ -75,7 +75,7 @@ public function serialize(mixed $value): mixed * * @throws Error If the value cannot be represented * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseValue(mixed $value): mixed { @@ -100,7 +100,7 @@ public function parseValue(mixed $value): mixed * * @throws Error Always, uploads must use multipart form * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseLiteral(\GraphQL\Language\AST\Node $valueNode, ?array $variables=null): mixed { diff --git a/lib/Service/GraphQL/Scalar/UriType.php b/lib/Service/GraphQL/Scalar/UriType.php index a2d119a352..cef539caa6 100644 --- a/lib/Service/GraphQL/Scalar/UriType.php +++ b/lib/Service/GraphQL/Scalar/UriType.php @@ -57,7 +57,7 @@ class UriType extends ScalarType * * @throws Error If the value is not a string * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function serialize(mixed $value): string { @@ -80,7 +80,7 @@ public function serialize(mixed $value): string * * @throws Error If the value is not a valid URI * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseValue(mixed $value): string { @@ -110,7 +110,7 @@ public function parseValue(mixed $value): string * * @throws Error If the node is not a string * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseLiteral(\GraphQL\Language\AST\Node $valueNode, ?array $variables=null): string { diff --git a/lib/Service/GraphQL/Scalar/UuidType.php b/lib/Service/GraphQL/Scalar/UuidType.php index 7bc1f22297..e5419575ef 100644 --- a/lib/Service/GraphQL/Scalar/UuidType.php +++ b/lib/Service/GraphQL/Scalar/UuidType.php @@ -64,7 +64,7 @@ class UuidType extends ScalarType * * @throws Error If the value is not a string * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function serialize(mixed $value): string { @@ -87,7 +87,7 @@ public function serialize(mixed $value): string * * @throws Error If the value is not a valid UUID * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseValue(mixed $value): string { @@ -117,7 +117,7 @@ public function parseValue(mixed $value): string * * @throws Error If the node is not a string * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function parseLiteral(\GraphQL\Language\AST\Node $valueNode, ?array $variables=null): string { diff --git a/lib/Service/GraphQL/SchemaGenerator.php b/lib/Service/GraphQL/SchemaGenerator.php index 455eb9d3ea..b1d855f434 100644 --- a/lib/Service/GraphQL/SchemaGenerator.php +++ b/lib/Service/GraphQL/SchemaGenerator.php @@ -13,14 +13,14 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister\Service\GraphQL - * @author Conduction B.V. + * @category Service + * @package OCA\OpenRegister\Service\GraphQL + * @author Conduction B.V. * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://OpenRegister.app + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ namespace OCA\OpenRegister\Service\GraphQL; @@ -138,7 +138,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function setResolver(GraphQLResolver $resolver): void { @@ -153,7 +153,7 @@ public function setResolver(GraphQLResolver $resolver): void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Schema generation inherently branches per register+schema * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function generate(): Schema { @@ -222,7 +222,7 @@ public function generate(): Schema * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-1 + * @spec openspec/specs/graphql-api/spec.md */ private function buildSchemaFields( RegisterSchema $schema, @@ -272,7 +272,7 @@ private function buildSchemaFields( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-39 + * @spec openspec/specs/graphql-api/spec.md#requirement-query-complexity-analysis-must-prevent-resource-abuse */ private function buildQueryFields( RegisterSchema $schema, @@ -320,7 +320,7 @@ private function buildQueryFields( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-1 + * @spec openspec/specs/graphql-api/spec.md */ private function buildMutationFields( RegisterSchema $schema, @@ -371,7 +371,7 @@ private function buildMutationFields( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-2 + * @spec openspec/specs/graphql-api/spec.md */ private function initScalars(): void { @@ -390,7 +390,7 @@ private function initScalars(): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-3 + * @spec openspec/specs/graphql-api/spec.md */ private function initHandlers(): void { @@ -426,7 +426,7 @@ private function initHandlers(): void * * @return ObjectType The GraphQL object type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-1 + * @spec openspec/specs/graphql-api/spec.md */ public function getObjectType(RegisterSchema $schema): ObjectType { @@ -469,7 +469,7 @@ public function getObjectType(RegisterSchema $schema): ObjectType * * @return array> The field configuration * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-1 + * @spec openspec/specs/graphql-api/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) JSON Schema composition (allOf/oneOf/anyOf) requires deep branching * @SuppressWarnings(PHPMD.NPathComplexity) Composition + property mapping creates high path count @@ -550,7 +550,7 @@ private function buildObjectFields(RegisterSchema $schema): array * * @return RegisterSchema|null The resolved schema or null * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-1 + * @spec openspec/specs/graphql-api/spec.md */ private function resolveRef(string $ref): ?RegisterSchema { @@ -578,7 +578,7 @@ private function resolveRef(string $ref): ?RegisterSchema * * @return string The PascalCase type name * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-1 + * @spec openspec/specs/graphql-api/spec.md */ private function toTypeName(string $slug, ?int $schemaId=null): string { @@ -617,7 +617,7 @@ private function toTypeName(string $slug, ?int $schemaId=null): string * * @return string A valid GraphQL field name * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-1 + * @spec openspec/specs/graphql-api/spec.md */ private function toFieldName(string $slug): string { @@ -650,7 +650,7 @@ private function toFieldName(string $slug): string * * @return string The singular form * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-1 + * @spec openspec/specs/graphql-api/spec.md */ private function singularize(string $plural): string { @@ -691,7 +691,7 @@ private function singularize(string $plural): string * * @return callable The resolver function * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-3 + * @spec openspec/specs/graphql-api/spec.md */ private function createSingleResolverPlaceholder(RegisterSchema $schema): callable { @@ -713,7 +713,7 @@ private function createSingleResolverPlaceholder(RegisterSchema $schema): callab * * @return callable The resolver function * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-3 + * @spec openspec/specs/graphql-api/spec.md */ private function createListResolverPlaceholder(RegisterSchema $schema): callable { @@ -736,7 +736,7 @@ private function createListResolverPlaceholder(RegisterSchema $schema): callable * * @return callable The resolver function * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-3 + * @spec openspec/specs/graphql-api/spec.md */ private function createMutationResolverPlaceholder(RegisterSchema $schema, string $action): callable { @@ -761,7 +761,7 @@ private function createMutationResolverPlaceholder(RegisterSchema $schema, strin * * @return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function getSchemasById(): array { @@ -774,7 +774,7 @@ public function getSchemasById(): array * * @return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function getRegistersById(): array { diff --git a/lib/Service/GraphQL/SchemaGenerator/CompositionHandler.php b/lib/Service/GraphQL/SchemaGenerator/CompositionHandler.php index 12120dce48..5f905c8aa2 100644 --- a/lib/Service/GraphQL/SchemaGenerator/CompositionHandler.php +++ b/lib/Service/GraphQL/SchemaGenerator/CompositionHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister\Service\GraphQL\SchemaGenerator - * @author Conduction B.V. + * @category Service + * @package OCA\OpenRegister\Service\GraphQL\SchemaGenerator + * @author Conduction B.V. * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://OpenRegister.app + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://OpenRegister.app */ namespace OCA\OpenRegister\Service\GraphQL\SchemaGenerator; @@ -95,7 +95,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-7 + * @spec openspec/specs/graphql-api/spec.md */ public function applyComposition(RegisterSchema $schema, array &$fields): void { @@ -113,7 +113,7 @@ public function applyComposition(RegisterSchema $schema, array &$fields): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-7 + * @spec openspec/specs/graphql-api/spec.md */ private function applyAllOf(RegisterSchema $schema, array &$fields): void { @@ -155,7 +155,7 @@ private function applyAllOf(RegisterSchema $schema, array &$fields): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-7 + * @spec openspec/specs/graphql-api/spec.md */ private function applyOneOf(RegisterSchema $schema, array &$fields): void { @@ -200,7 +200,7 @@ private function applyOneOf(RegisterSchema $schema, array &$fields): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-7 + * @spec openspec/specs/graphql-api/spec.md */ private function applyAnyOf(RegisterSchema $schema, array &$fields): void { @@ -250,7 +250,7 @@ private function applyAnyOf(RegisterSchema $schema, array &$fields): void * * @return ObjectType[] The resolved object types * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-7 + * @spec openspec/specs/graphql-api/spec.md */ private function resolveCompositionRefs(array $refs): array { @@ -284,7 +284,7 @@ private function resolveCompositionRefs(array $refs): array * * @return array> Shared field configs * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-7 + * @spec openspec/specs/graphql-api/spec.md */ private function extractSharedFields(array $types): array { diff --git a/lib/Service/GraphQL/SchemaGenerator/TypeMapperHandler.php b/lib/Service/GraphQL/SchemaGenerator/TypeMapperHandler.php index 7f807daea4..e0dfe58735 100644 --- a/lib/Service/GraphQL/SchemaGenerator/TypeMapperHandler.php +++ b/lib/Service/GraphQL/SchemaGenerator/TypeMapperHandler.php @@ -8,14 +8,14 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister\Service\GraphQL\SchemaGenerator - * @author Conduction B.V. + * @category Service + * @package OCA\OpenRegister\Service\GraphQL\SchemaGenerator + * @author Conduction B.V. * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://OpenRegister.app + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-38 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-log-operations-to-the-audit-trail */ namespace OCA\OpenRegister\Service\GraphQL\SchemaGenerator; @@ -183,7 +183,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-41 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-resolver-must-reset-state-between-requests */ public function resetCache(): void { @@ -203,7 +203,7 @@ public function resetCache(): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-41 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-resolver-must-reset-state-between-requests */ public function setScalars(array $scalars): void { @@ -218,7 +218,7 @@ public function setScalars(array $scalars): void * * @return Type The GraphQL type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-3 + * @spec openspec/specs/graphql-api/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -271,7 +271,7 @@ public function mapPropertyToGraphQLType(array $property): Type * * @return Type The GraphQL input type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-3 + * @spec openspec/specs/graphql-api/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -320,7 +320,7 @@ public function mapPropertyToInputType(array $property): Type * * @return InputObjectType The filter input type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-4 + * @spec openspec/specs/graphql-api/spec.md */ public function getFilterInputType(RegisterSchema $schema): InputObjectType { @@ -381,7 +381,7 @@ public function getFilterInputType(RegisterSchema $schema): InputObjectType * * @return InputObjectType The create input type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-4 + * @spec openspec/specs/graphql-api/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -439,7 +439,7 @@ public function getCreateInputType(RegisterSchema $schema): InputObjectType * * @return InputObjectType The update input type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-4 + * @spec openspec/specs/graphql-api/spec.md */ public function getUpdateInputType(RegisterSchema $schema): InputObjectType { @@ -480,7 +480,7 @@ public function getUpdateInputType(RegisterSchema $schema): InputObjectType * * @return array> The input fields * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-4 + * @spec openspec/specs/graphql-api/spec.md */ private function buildInputFields(RegisterSchema $schema): array { @@ -509,7 +509,7 @@ private function buildInputFields(RegisterSchema $schema): array * * @return ObjectType The connection type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-5 + * @spec openspec/specs/graphql-api/spec.md */ public function getConnectionType(RegisterSchema $schema, ObjectType $objectType): ObjectType { @@ -562,7 +562,7 @@ public function getConnectionType(RegisterSchema $schema, ObjectType $objectType * * @return ObjectType The GroupBucket type. * - * @spec openspec/changes/add-time-bucket-aggregation/specs/graphql-api/spec.md + * @spec openspec/specs/graphql-api/spec.md */ public function getGroupBucketType(): ObjectType { @@ -590,7 +590,7 @@ public function getGroupBucketType(): ObjectType * * @return EnumType The TimeInterval enum. * - * @spec openspec/changes/add-time-bucket-aggregation/specs/graphql-api/spec.md + * @spec openspec/specs/graphql-api/spec.md */ public function getTimeIntervalType(): EnumType { @@ -623,7 +623,7 @@ public function getTimeIntervalType(): EnumType * * @return EnumType The AggregationMetric enum. * - * @spec openspec/changes/add-time-bucket-aggregation/specs/graphql-api/spec.md + * @spec openspec/specs/graphql-api/spec.md */ public function getAggregationMetricType(): EnumType { @@ -654,7 +654,7 @@ public function getAggregationMetricType(): EnumType * * @return InputObjectType The GroupByInput type. * - * @spec openspec/changes/add-time-bucket-aggregation/specs/graphql-api/spec.md + * @spec openspec/specs/graphql-api/spec.md */ public function getGroupByInputType(): InputObjectType { @@ -705,7 +705,7 @@ public function getGroupByInputType(): InputObjectType * * @return ObjectType The PageInfo type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-5 + * @spec openspec/specs/graphql-api/spec.md */ private function getPageInfoType(): ObjectType { @@ -734,7 +734,7 @@ private function getPageInfoType(): ObjectType * * @return ObjectType The AuditTrail type * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-38 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-log-operations-to-the-audit-trail */ public function getAuditTrailType(): ObjectType { @@ -770,7 +770,7 @@ public function getAuditTrailType(): ObjectType * * @return array> The argument definitions * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-41 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-resolver-must-reset-state-between-requests */ public function getListArgs(RegisterSchema $schema): array { @@ -800,7 +800,7 @@ public function getListArgs(RegisterSchema $schema): array * * @return InputObjectType The sort input type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-4 + * @spec openspec/specs/graphql-api/spec.md */ private function getSortInputType(): InputObjectType { @@ -831,7 +831,7 @@ private function getSortInputType(): InputObjectType * * @return InputObjectType The self-filter input type * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-4 + * @spec openspec/specs/graphql-api/spec.md */ private function getSelfFilterType(): InputObjectType { @@ -863,7 +863,7 @@ private function getSelfFilterType(): InputObjectType * * @return array Map of property name to auth description * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-misc-annotate/tasks.md#task-6 + * @spec openspec/specs/graphql-api/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ diff --git a/lib/Service/GraphQL/SubscriptionService.php b/lib/Service/GraphQL/SubscriptionService.php index c0a1bfca3e..3b7109aae7 100644 --- a/lib/Service/GraphQL/SubscriptionService.php +++ b/lib/Service/GraphQL/SubscriptionService.php @@ -16,7 +16,7 @@ * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/event-driven-architecture/spec.md */ namespace OCA\OpenRegister\Service\GraphQL; @@ -77,8 +77,8 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph + * @spec openspec/specs/event-driven-architecture/spec.md */ public function pushEvent(string $action, ObjectEntity $object): void { @@ -134,7 +134,7 @@ public function pushEvent(string $action, ObjectEntity $object): void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) At threshold after extracting filterEventStream + verifyEventRBAC * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function getEventsSince( ?string $lastEventId=null, @@ -178,7 +178,7 @@ public function getEventsSince( * * @return array The filtered events * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler */ private function filterEventStream( array $buffer, @@ -220,7 +220,7 @@ private function filterEventStream( * * @return bool True if the current user can see this event * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-37 + * @spec openspec/specs/graphql-api/spec.md#requirement-graphql-must-enforce-schema-level-rbac-via-permissionhandler */ private function verifyEventRBAC(array $event): bool { @@ -245,7 +245,7 @@ private function verifyEventRBAC(array $event): bool * * @return string The SSE-formatted message * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-40 + * @spec openspec/specs/graphql-api/spec.md#requirement-cross-register-schema-stitching-must-provide-a-unified-graph */ public function formatAsSSE(array $event): string { diff --git a/lib/Service/Handoff/HandoffAnnotationValidator.php b/lib/Service/Handoff/HandoffAnnotationValidator.php new file mode 100644 index 0000000000..fdbc37fc33 --- /dev/null +++ b/lib/Service/Handoff/HandoffAnnotationValidator.php @@ -0,0 +1,486 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Handoff; + +/** + * Validate the `x-openregister-handoff` annotation on a source schema. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ +class HandoffAnnotationValidator +{ + + /** + * The five mapping expression kinds the v1 dialect permits. + * + * @var array + */ + public const EXPRESSION_KINDS = ['from', 'const', 'template', 'semanticRef', 'provenance']; + + /** + * Degradation modes for `whenUnavailable`. + * + * @var array + */ + public const UNAVAILABLE_MODES = ['hide', 'queue']; + + /** + * Validate the annotation. + * + * Expects the annotation at top-level alongside `properties` (the shape + * SchemaMapper hands every annotation validator): + * `['properties' => [...], 'x-openregister-handoff' => [...entries], + * 'x-openregister-lifecycle' => [...]?]`. + * + * @param array $schema The schema shape to validate. + * + * @return array Aggregated errors (empty = valid). + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Aggregating every dialect rule in one pass is inherent. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + public function validate(array $schema): array + { + if (isset($schema['x-openregister-handoff']) === false) { + return []; + } + + $annotation = $schema['x-openregister-handoff']; + if (is_array($annotation) === false) { + return [ + [ + 'code' => 'handoff-bad-annotation', + 'message' => 'x-openregister-handoff must be an array of handoff entries.', + ], + ]; + } + + $properties = ($schema['properties'] ?? []); + if (is_array($properties) === false) { + $properties = []; + } + + $lifecycleStates = $this->lifecycleStates(schema: $schema); + + $errors = []; + $seenIds = []; + foreach (array_values($annotation) as $index => $entry) { + if (is_array($entry) === false) { + $errors[] = [ + 'code' => 'handoff-bad-annotation', + 'message' => sprintf('x-openregister-handoff entry %d must be an object.', $index), + ]; + continue; + } + + $entryId = $this->validateId(entry: $entry, index: $index, seenIds: $seenIds, errors: $errors); + $this->validateTargetType(entry: $entry, entryId: $entryId, errors: $errors); + $this->validateTrigger(entry: $entry, entryId: $entryId, lifecycleStates: $lifecycleStates, errors: $errors); + $this->validateMapping(entry: $entry, entryId: $entryId, errors: $errors); + $this->validateWhenUnavailable(entry: $entry, entryId: $entryId, errors: $errors); + $this->validateOnSuccess(entry: $entry, entryId: $entryId, properties: $properties, errors: $errors); + }//end foreach + + return $errors; + + }//end validate() + + /** + * Validate + register an entry's `id` (unique, slug-like). + * + * @param array $entry The handoff entry. + * @param int $index The entry's position (for messages when id is unusable). + * @param array $seenIds Ids seen so far (by reference). + * @param array $errors Error accumulator (by reference). + * + * @return string The entry id (or a positional placeholder for messages). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + private function validateId(array $entry, int $index, array &$seenIds, array &$errors): string + { + $id = ($entry['id'] ?? null); + if (is_string($id) === false || preg_match('/^[a-z0-9][a-z0-9-]*$/', $id) !== 1) { + $errors[] = [ + 'code' => 'handoff-bad-id', + 'message' => sprintf( + 'x-openregister-handoff entry %d must declare a slug-like `id` (lowercase letters, digits, hyphens).', + $index + ), + ]; + return sprintf('entry-%d', $index); + } + + if (isset($seenIds[$id]) === true) { + $errors[] = [ + 'code' => 'handoff-duplicate-id', + 'message' => sprintf('x-openregister-handoff id "%s" is declared more than once.', $id), + ]; + } + + $seenIds[$id] = true; + return $id; + + }//end validateId() + + /** + * Validate `targetSemanticType` is an absolute URI. + * + * @param array $entry The handoff entry. + * @param string $entryId The entry id (for messages). + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Target kind URI is malformed) + */ + private function validateTargetType(array $entry, string $entryId, array &$errors): void + { + $target = ($entry['targetSemanticType'] ?? null); + if (is_string($target) === true + && filter_var($target, FILTER_VALIDATE_URL) !== false + && preg_match('/^https?:\/\//', $target) === 1 + ) { + return; + } + + $errors[] = [ + 'code' => 'handoff-bad-target-type', + 'message' => sprintf( + 'Handoff "%s": `targetSemanticType` must be an absolute kind URI (e.g. "%sCase").', + $entryId, + HandoffKindContracts::NAMESPACE_URI + ), + ]; + + }//end validateTargetType() + + /** + * Validate the `trigger` field: `manual` or `lifecycle:` where the + * state exists in the schema's declared lifecycle enum. + * + * @param array $entry The handoff entry. + * @param string $entryId The entry id (for messages). + * @param array|null $lifecycleStates Declared lifecycle states (null = no lifecycle declared). + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + private function validateTrigger(array $entry, string $entryId, ?array $lifecycleStates, array &$errors): void + { + $trigger = ($entry['trigger'] ?? 'manual'); + if ($trigger === 'manual') { + return; + } + + if (is_string($trigger) === false || str_starts_with($trigger, 'lifecycle:') === false) { + $errors[] = [ + 'code' => 'handoff-bad-trigger', + 'message' => sprintf('Handoff "%s": `trigger` must be "manual" or "lifecycle:".', $entryId), + ]; + return; + } + + $state = substr($trigger, strlen('lifecycle:')); + if ($state === '') { + $errors[] = [ + 'code' => 'handoff-bad-trigger', + 'message' => sprintf('Handoff "%s": lifecycle trigger must name a state ("lifecycle:").', $entryId), + ]; + return; + } + + if ($lifecycleStates === null) { + $errors[] = [ + 'code' => 'handoff-bad-trigger', + 'message' => sprintf( + 'Handoff "%s": trigger "lifecycle:%s" requires the schema to declare x-openregister-lifecycle.', + $entryId, + $state + ), + ]; + return; + } + + if (in_array($state, $lifecycleStates, true) === false) { + $errors[] = [ + 'code' => 'handoff-bad-trigger', + 'message' => sprintf( + 'Handoff "%s": lifecycle state "%s" is not declared in the schema\'s lifecycle enum (%s).', + $entryId, + $state, + implode(', ', $lifecycleStates) + ), + ]; + } + + }//end validateTrigger() + + /** + * Validate the mapping: keys within the target kind's contract fields, + * every mandatory field mapped, expression kinds within the allowed five. + * + * @param array $entry The handoff entry. + * @param string $entryId The entry id (for messages). + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Unknown mapping expression kind is rejected) + */ + private function validateMapping(array $entry, string $entryId, array &$errors): void + { + $mapping = ($entry['mapping'] ?? null); + if (is_array($mapping) === false || $mapping === []) { + $errors[] = [ + 'code' => 'handoff-bad-annotation', + 'message' => sprintf('Handoff "%s": a non-empty `mapping` object is required.', $entryId), + ]; + return; + } + + $kindUri = (string) ($entry['targetSemanticType'] ?? ''); + $isContractKind = HandoffKindContracts::isContractKind($kindUri); + $allFields = HandoffKindContracts::allFields($kindUri); + + foreach ($mapping as $field => $expression) { + if ($isContractKind === true && in_array((string) $field, $allFields, true) === false) { + $errors[] = [ + 'code' => 'handoff-unknown-mapping-field', + 'message' => sprintf( + 'Handoff "%s": mapping field "%s" is not part of the "%s" contract (fields: %s).', + $entryId, + (string) $field, + $kindUri, + implode(', ', $allFields) + ), + ]; + } + + $this->validateExpression(expression: $expression, field: (string) $field, entryId: $entryId, errors: $errors); + } + + if ($isContractKind === true) { + $missing = array_diff(HandoffKindContracts::mandatoryFields($kindUri), array_map('strval', array_keys($mapping))); + if ($missing !== []) { + $errors[] = [ + 'code' => 'handoff-missing-mandatory-field', + 'message' => sprintf( + 'Handoff "%s": mandatory "%s" contract field(s) not mapped: %s.', + $entryId, + $kindUri, + implode(', ', $missing) + ), + ]; + } + } + + }//end validateMapping() + + /** + * Validate one mapping expression uses exactly one of the five kinds. + * + * @param mixed $expression The mapping expression value. + * @param string $field The contract field being mapped. + * @param string $entryId The entry id (for messages). + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Unknown mapping expression kind is rejected) + */ + private function validateExpression(mixed $expression, string $field, string $entryId, array &$errors): void + { + if (is_array($expression) === false) { + $errors[] = [ + 'code' => 'handoff-bad-mapping-expression', + 'message' => sprintf( + 'Handoff "%s": mapping for "%s" must be an object using one of: %s.', + $entryId, + $field, + implode(', ', self::EXPRESSION_KINDS) + ), + ]; + return; + } + + $kinds = array_values(array_intersect(array_map('strval', array_keys($expression)), self::EXPRESSION_KINDS)); + // `default` is an allowed modifier alongside `from`; every other key + // must be one of the five expression kinds. + $unknown = array_diff(array_map('strval', array_keys($expression)), self::EXPRESSION_KINDS, ['default']); + + if (count($kinds) !== 1 || $unknown !== []) { + $offending = 'none'; + if ($unknown !== []) { + $offending = implode(', ', $unknown); + } + + $errors[] = [ + 'code' => 'handoff-bad-mapping-expression', + 'message' => sprintf( + 'Handoff "%s": mapping for "%s" must use exactly one expression kind of [%s]; offending key(s): %s.', + $entryId, + $field, + implode(', ', self::EXPRESSION_KINDS), + $offending + ), + ]; + } + + }//end validateExpression() + + /** + * Validate `whenUnavailable` when present. + * + * @param array $entry The handoff entry. + * @param string $entryId The entry id (for messages). + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Graceful degradation when no provider implements the kind) + */ + private function validateWhenUnavailable(array $entry, string $entryId, array &$errors): void + { + $mode = ($entry['whenUnavailable'] ?? 'hide'); + if (in_array($mode, self::UNAVAILABLE_MODES, true) === true) { + return; + } + + $errors[] = [ + 'code' => 'handoff-bad-when-unavailable', + 'message' => sprintf( + 'Handoff "%s": `whenUnavailable` must be one of [%s].', + $entryId, + implode(', ', self::UNAVAILABLE_MODES) + ), + ]; + + }//end validateWhenUnavailable() + + /** + * Validate `onSuccess.set` keys exist on the source schema. + * + * @param array $entry The handoff entry. + * @param string $entryId The entry id (for messages). + * @param array $properties The source schema's properties map. + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: `onSuccess.set` names a property missing from the source schema) + */ + private function validateOnSuccess(array $entry, string $entryId, array $properties, array &$errors): void + { + $onSuccess = ($entry['onSuccess'] ?? null); + if ($onSuccess === null) { + return; + } + + $set = null; + if (is_array($onSuccess) === true) { + $set = ($onSuccess['set'] ?? null); + } + + if (is_array($set) === false || $set === []) { + $errors[] = [ + 'code' => 'handoff-bad-success-update', + 'message' => sprintf('Handoff "%s": `onSuccess` must carry a non-empty `set` map.', $entryId), + ]; + return; + } + + foreach (array_keys($set) as $property) { + if (array_key_exists((string) $property, $properties) === false) { + $errors[] = [ + 'code' => 'handoff-bad-success-update', + 'message' => sprintf( + 'Handoff "%s": onSuccess.set names property "%s" which the source schema does not define.', + $entryId, + (string) $property + ), + ]; + } + } + + }//end validateOnSuccess() + + /** + * Extract the schema's declared lifecycle states (the lifecycle field's + * enum), or null when the schema declares no usable lifecycle annotation. + * + * @param array $schema The schema shape. + * + * @return array|null The lifecycle states, or null. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + private function lifecycleStates(array $schema): ?array + { + $lifecycle = ($schema['x-openregister-lifecycle'] ?? null); + if (is_array($lifecycle) === false) { + return null; + } + + $field = ($lifecycle['field'] ?? ($lifecycle['property'] ?? null)); + if (is_string($field) === false) { + return null; + } + + $properties = ($schema['properties'] ?? []); + $enum = ($properties[$field]['enum'] ?? null); + if (is_array($enum) === false) { + return null; + } + + return array_values(array_map('strval', $enum)); + + }//end lifecycleStates() +}//end class diff --git a/lib/Service/Handoff/HandoffContractBindingValidator.php b/lib/Service/Handoff/HandoffContractBindingValidator.php new file mode 100644 index 0000000000..e244ea66e2 --- /dev/null +++ b/lib/Service/Handoff/HandoffContractBindingValidator.php @@ -0,0 +1,301 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Handoff; + +/** + * Validate the `handoffContract` binding block on an implementing schema. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Kind contract binding on the implementing schema) + */ +class HandoffContractBindingValidator +{ + /** + * Validate the binding block. + * + * Expects the shape SchemaMapper hands the annotation validators, with the + * configuration-level semantic markers alongside `properties`: + * `['properties' => [...], 'implements' => [...]?, 'jsonld' => [...]?, + * 'x-schema-org' => ...?, 'handoffContract' => [...]?]`. + * + * The binding block maps kind URIs to `{contractField: ownProperty}` maps: + * `"handoffContract": {"https://openregister.app/ns#Case": {"title": "onderwerp", ...}}`. + * + * @param array $schema The schema shape to validate. + * + * @return array Aggregated errors (empty = valid). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Implementer omits a mandatory contract field) + */ + public function validate(array $schema): array + { + $binding = ($schema['handoffContract'] ?? null); + if ($binding === null) { + // No binding block: not a handoff provider — never an error. + return []; + } + + if (is_array($binding) === false) { + return [ + [ + 'code' => 'handoff-contract-incomplete', + 'message' => 'handoffContract must be an object mapping kind URIs to {contractField: ownProperty} bindings.', + ], + ]; + } + + $properties = ($schema['properties'] ?? []); + if (is_array($properties) === false) { + $properties = []; + } + + $implemented = $this->implementedTypes(schema: $schema); + + $errors = []; + foreach ($binding as $kindUri => $fieldMap) { + $kindUri = (string) $kindUri; + + if (in_array($kindUri, $implemented, true) === false) { + $errors[] = [ + 'code' => 'handoff-contract-incomplete', + 'message' => sprintf( + 'handoffContract binds kind "%s" which the schema does not implement (declare it in `implements`).', + $kindUri + ), + ]; + continue; + } + + if (HandoffKindContracts::isContractKind($kindUri) === false) { + $errors[] = [ + 'code' => 'handoff-contract-incomplete', + 'message' => sprintf( + 'handoffContract binds unknown kind "%s" (known contract kinds: %s).', + $kindUri, + implode(', ', HandoffKindContracts::kinds()) + ), + ]; + continue; + } + + $this->validateKindBinding( + kindUri: $kindUri, + fieldMap: $fieldMap, + properties: $properties, + errors: $errors + ); + }//end foreach + + return $errors; + + }//end validate() + + /** + * Determine whether a binding covers every mandatory field of a kind and + * binds only existing own properties. + * + * Static convenience used by the engine to filter resolver results to + * schemas with a COMPLETE binding (design: "no binding block ⇒ not a + * handoff provider"). + * + * @param string $kindUri The kind URI to check. + * @param array $binding The full `handoffContract` block. + * @param array $properties The schema's own properties map. + * + * @return bool True when the kind is completely + validly bound. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Implementer binds all mandatory contract fields) + */ + public static function isCompleteBinding(string $kindUri, array $binding, array $properties): bool + { + $fieldMap = ($binding[$kindUri] ?? null); + if (is_array($fieldMap) === false || $fieldMap === []) { + return false; + } + + foreach (HandoffKindContracts::mandatoryFields($kindUri) as $mandatory) { + $target = ($fieldMap[$mandatory] ?? null); + if (is_string($target) === false || array_key_exists($target, $properties) === false) { + return false; + } + } + + foreach ($fieldMap as $contractField => $ownProperty) { + if (is_string($ownProperty) === false || array_key_exists($ownProperty, $properties) === false) { + return false; + } + + if (in_array((string) $contractField, HandoffKindContracts::allFields($kindUri), true) === false) { + return false; + } + } + + return true; + + }//end isCompleteBinding() + + /** + * Validate a single kind's field map inside the binding block. + * + * @param string $kindUri The kind URI being bound. + * @param mixed $fieldMap The declared `{contractField: ownProperty}` map. + * @param array $properties The schema's own properties map. + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Implementer omits a mandatory contract field) + */ + private function validateKindBinding(string $kindUri, mixed $fieldMap, array $properties, array &$errors): void + { + if (is_array($fieldMap) === false || $fieldMap === []) { + $errors[] = [ + 'code' => 'handoff-contract-incomplete', + 'message' => sprintf( + 'handoffContract["%s"] must be a non-empty {contractField: ownProperty} map; missing mandatory fields: %s.', + $kindUri, + implode(', ', HandoffKindContracts::mandatoryFields($kindUri)) + ), + ]; + return; + } + + $allFields = HandoffKindContracts::allFields($kindUri); + foreach ($fieldMap as $contractField => $ownProperty) { + if (in_array((string) $contractField, $allFields, true) === false) { + $errors[] = [ + 'code' => 'handoff-contract-incomplete', + 'message' => sprintf( + 'handoffContract["%s"] binds "%s" which is not a contract field (fields: %s).', + $kindUri, + (string) $contractField, + implode(', ', $allFields) + ), + ]; + continue; + } + + if (is_string($ownProperty) === false || array_key_exists($ownProperty, $properties) === false) { + $offending = gettype($ownProperty); + if (is_string($ownProperty) === true) { + $offending = $ownProperty; + } + + $errors[] = [ + 'code' => 'handoff-contract-incomplete', + 'message' => sprintf( + 'handoffContract["%s"]["%s"] must name an existing own property; "%s" is not declared in `properties`.', + $kindUri, + (string) $contractField, + $offending + ), + ]; + } + }//end foreach + + $missing = []; + foreach (HandoffKindContracts::mandatoryFields($kindUri) as $mandatory) { + if (array_key_exists($mandatory, $fieldMap) === false) { + $missing[] = $mandatory; + } + } + + if ($missing !== []) { + $errors[] = [ + 'code' => 'handoff-contract-incomplete', + 'message' => sprintf( + 'handoffContract["%s"] omits mandatory contract field(s): %s.', + $kindUri, + implode(', ', $missing) + ), + ]; + } + + }//end validateKindBinding() + + /** + * Compute a schema's OWN implemented semantic-type markers from the + * configuration shape (ADR-048: `implements[]`, else `jsonld.type`, plus + * `x-schema-org` markers). This intentionally mirrors + * {@see \OCA\OpenRegister\Service\JsonLd\JsonLdContextService::getImplementedTypes()} + * without requiring DI (annotation validators are constructed with `new` + * in the SchemaMapper save path). + * + * @param array $schema The schema shape. + * + * @return array The implemented type URIs (own markers only). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Kind contract binding on the implementing schema) + */ + private function implementedTypes(array $schema): array + { + $types = []; + + $implements = ($schema['implements'] ?? null); + if (is_array($implements) === true) { + foreach ($implements as $uri) { + if (is_string($uri) === true && $uri !== '') { + $types[] = $uri; + } + } + } + + $jsonld = ($schema['jsonld'] ?? null); + if ($types === [] && is_array($jsonld) === true && is_string($jsonld['type'] ?? null) === true) { + $types[] = (string) $jsonld['type']; + } + + $schemaOrg = ($schema['x-schema-org'] ?? null); + if (is_string($schemaOrg) === true && $schemaOrg !== '') { + $marker = $schemaOrg; + if (str_starts_with($marker, 'schema:') === true) { + $marker = 'https://schema.org/'.substr($marker, strlen('schema:')); + } + + $types[] = $marker; + } + + return array_values(array_unique($types)); + + }//end implementedTypes() +}//end class diff --git a/lib/Service/Handoff/HandoffKindContracts.php b/lib/Service/Handoff/HandoffKindContracts.php new file mode 100644 index 0000000000..ae2364424a --- /dev/null +++ b/lib/Service/Handoff/HandoffKindContracts.php @@ -0,0 +1,152 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Handoff; + +/** + * Static registry of the canonical handoff kind contracts (field sets). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Kind contract binding on the implementing schema) + */ +final class HandoffKindContracts +{ + + /** + * The canonical OpenRegister kind namespace (ADR-048 vocabulary layer). + * + * @var string + */ + public const NAMESPACE_URI = 'https://openregister.app/ns#'; + + /** + * Contract map keyed by kind URI. Each entry lists the mandatory fields an + * implementer MUST bind (and an emitter MUST map) and the optional fields + * that MAY be bound/mapped. Field semantics live in the hydra contract + * specs; this map carries only the names the validators + engine need. + * + * @var array, optional: array}> + */ + private const CONTRACTS = [ + self::NAMESPACE_URI.'Case' => [ + 'mandatory' => ['title', 'summary', 'channel', 'source'], + 'optional' => ['requester', 'priority'], + ], + self::NAMESPACE_URI.'Quote' => [ + 'mandatory' => ['title', 'counterparty', 'currency', 'totalAmount', 'source'], + 'optional' => ['lines', 'validUntil'], + ], + self::NAMESPACE_URI.'Contract' => [ + 'mandatory' => ['title', 'counterparty', 'currency', 'totalAmount', 'startDate', 'source'], + 'optional' => ['endDate'], + ], + self::NAMESPACE_URI.'Invoice' => [ + 'mandatory' => ['counterparty', 'currency', 'totalAmount', 'source'], + 'optional' => ['lines', 'dueDate'], + ], + ]; + + /** + * Whether the given kind URI carries a known contract. + * + * @param string $kindUri The canonical kind URI. + * + * @return bool True when a contract is registered for the kind. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Kind contract binding on the implementing schema) + */ + public static function isContractKind(string $kindUri): bool + { + return array_key_exists($kindUri, self::CONTRACTS); + + }//end isContractKind() + + /** + * The mandatory contract field names for a kind (empty for unknown kinds). + * + * @param string $kindUri The canonical kind URI. + * + * @return array Mandatory field names. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Kind contract binding on the implementing schema) + */ + public static function mandatoryFields(string $kindUri): array + { + return self::CONTRACTS[$kindUri]['mandatory'] ?? []; + + }//end mandatoryFields() + + /** + * All contract field names (mandatory + optional) for a kind. + * + * @param string $kindUri The canonical kind URI. + * + * @return array All declared field names (empty for unknown kinds). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + public static function allFields(string $kindUri): array + { + $contract = (self::CONTRACTS[$kindUri] ?? null); + if ($contract === null) { + return []; + } + + return array_values(array_merge($contract['mandatory'], $contract['optional'])); + + }//end allFields() + + /** + * All registered kind URIs. + * + * @return array The known contract-carrying kind URIs. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Kind contract binding on the implementing schema) + */ + public static function kinds(): array + { + return array_keys(self::CONTRACTS); + + }//end kinds() +}//end class diff --git a/lib/Service/Handoff/HandoffMappingEvaluator.php b/lib/Service/Handoff/HandoffMappingEvaluator.php new file mode 100644 index 0000000000..1a7f322feb --- /dev/null +++ b/lib/Service/Handoff/HandoffMappingEvaluator.php @@ -0,0 +1,201 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Handoff; + +/** + * Evaluate handoff mapping expressions to contract-field values. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: HandoffService executes conversions on top of SemanticTypeResolver) + */ +class HandoffMappingEvaluator +{ + /** + * Evaluate a full mapping. + * + * @param array $mapping The declared mapping (contract field → expression; + * each expression is validated as an array at runtime). + * @param array $sourceData The source object's data. + * @param array $provenance The engine-filled source pointer `{app, register, schema, uuid}`. + * + * @return array Contract field → evaluated value (fields evaluating to null are omitted). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Semantic references are carried, not copied) + */ + public function evaluate(array $mapping, array $sourceData, array $provenance): array + { + $result = []; + foreach ($mapping as $field => $expression) { + if (is_array($expression) === false) { + continue; + } + + $value = $this->evaluateExpression( + expression: $expression, + sourceData: $sourceData, + provenance: $provenance + ); + if ($value !== null) { + $result[(string) $field] = $value; + } + } + + return $result; + + }//end evaluate() + + /** + * Evaluate one mapping expression. + * + * @param array $expression The expression (exactly one of the five kinds). + * @param array $sourceData The source object's data. + * @param array $provenance The engine-filled source pointer. + * + * @return mixed The evaluated value, or null when the expression yields nothing + * (e.g. `from` on an absent property without a `default`). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + private function evaluateExpression(array $expression, array $sourceData, array $provenance): mixed + { + if (array_key_exists('const', $expression) === true) { + return $expression['const']; + } + + if (array_key_exists('provenance', $expression) === true) { + return $provenance; + } + + if (array_key_exists('template', $expression) === true) { + return $this->interpolate( + template: (string) $expression['template'], + data: $sourceData + ); + } + + if (array_key_exists('semanticRef', $expression) === true) { + return $this->carryReference( + property: (string) $expression['semanticRef'], + sourceData: $sourceData + ); + } + + if (array_key_exists('from', $expression) === true) { + $value = ($sourceData[(string) $expression['from']] ?? null); + if ($value === null && array_key_exists('default', $expression) === true) { + return $expression['default']; + } + + return $value; + } + + return null; + + }//end evaluateExpression() + + /** + * Carry an ADR-048 semantic reference across as a reference. + * + * The source property holds either a UUID string or a reference envelope + * whose identifying UUID we extract — the referenced object's DATA is + * never read, dereferenced, or copied (contract scenario "Semantic + * references are carried, not copied"). + * + * @param string $property The source property holding the reference. + * @param array $sourceData The source object's data. + * + * @return string|null The reference UUID/identifier, or null when absent. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Semantic references are carried, not copied) + */ + private function carryReference(string $property, array $sourceData): ?string + { + $value = ($sourceData[$property] ?? null); + if (is_string($value) === true && $value !== '') { + return $value; + } + + if (is_array($value) === true) { + foreach (['uuid', 'id', '@id'] as $key) { + $candidate = ($value[$key] ?? null); + if (is_string($candidate) === true && $candidate !== '') { + return $candidate; + } + } + } + + return null; + + }//end carryReference() + + /** + * `{{prop}}` interpolation over the source data, HTML-escaped, unknown or + * non-scalar properties rendering as '' — the existing OR convention + * ({@see \OCA\OpenRegister\Service\Notification\AnnotationNotificationDispatcher}). + * + * @param string $template The template string. + * @param array $data The source object's data. + * + * @return string The interpolated string. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + private function interpolate(string $template, array $data): string + { + $result = preg_replace_callback( + '/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/', + function (array $matches) use ($data): string { + $key = $matches[1]; + if (array_key_exists($key, $data) === false || is_scalar($data[$key]) === false) { + return ''; + } + + return htmlspecialchars((string) $data[$key], ENT_QUOTES, 'UTF-8'); + }, + $template + ); + + return ($result ?? $template); + + }//end interpolate() +}//end class diff --git a/lib/Service/Handoff/HandoffService.php b/lib/Service/Handoff/HandoffService.php new file mode 100644 index 0000000000..d95e50e75b --- /dev/null +++ b/lib/Service/Handoff/HandoffService.php @@ -0,0 +1,1058 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Handoff; + +use DateTime; +use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Db\HandoffQueueEntry; +use OCA\OpenRegister\Db\HandoffQueueEntryMapper; +use OCA\OpenRegister\Db\MagicMapper; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\HandoffExecutedEvent; +use OCA\OpenRegister\Exception\HandoffException; +use OCA\OpenRegister\Exception\NotAuthorizedException; +use OCA\OpenRegister\Service\Object\PermissionHandler; +use OCA\OpenRegister\Service\ObjectService; +use OCA\OpenRegister\Service\SemanticTypeResolver; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IDBConnection; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Notification\IManager as INotificationManager; +use Psr\Log\LoggerInterface; +use Symfony\Component\Uid\Uuid; + +/** + * Execute declared semantic-object handoffs on top of SemanticTypeResolver. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) The engine deliberately + * composes the shipped OR capabilities (resolver, object write path, + * relations, audit, queue, events) rather than reimplementing any of them. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: HandoffService executes conversions on top of SemanticTypeResolver) + */ +class HandoffService +{ + + /** + * Reserved relation type prefix for handoff provenance keys in the + * object relations map: `handoff::handed-off-to` on the source and + * `handoff::originated-from` on the target. + * + * @var string + */ + public const RELATION_PREFIX = 'handoff'; + + /** + * Audit action written on both sides of an executed handoff. + * + * @var string + */ + public const AUDIT_EXECUTED = 'handoff.executed'; + + /** + * Audit action written when a queue-mode handoff is parked. + * + * @var string + */ + public const AUDIT_QUEUED = 'handoff.queued'; + + /** + * Audit action written when a parked handoff is drained. + * + * @var string + */ + public const AUDIT_DEQUEUED = 'handoff.dequeued'; + + /** + * Wire the engine against the shipped OR capabilities it composes. + * + * @param ObjectService $objectService RBAC-enforcing object read/write path. + * @param SemanticTypeResolver $semanticTypeResolver Kind URI → installed schema resolution. + * @param SchemaMapper $schemaMapper Source-schema loading. + * @param MagicMapper $objectMapper Raw entity access for provenance relation writes. + * @param AuditTrailMapper $auditTrailMapper Immutable audit rows. + * @param PermissionHandler $permissionHandler RBAC pre-checks (typed 403 before any write). + * @param HandoffMappingEvaluator $mappingEvaluator The five-expression mapping evaluator. + * @param HandoffQueueEntryMapper $queueMapper Durable queue-mode storage. + * @param IDBConnection $db Transaction boundary. + * @param IEventDispatcher $eventDispatcher Post-commit ADR-041 event dispatch. + * @param IUserSession $userSession The acting user (and drain impersonation). + * @param IUserManager $userManager Requesting-user lookup for queue drain. + * @param INotificationManager $notificationManager Requester notification on drain permission failure. + * @param LoggerInterface $logger Structured logging. + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly SemanticTypeResolver $semanticTypeResolver, + private readonly SchemaMapper $schemaMapper, + private readonly MagicMapper $objectMapper, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly PermissionHandler $permissionHandler, + private readonly HandoffMappingEvaluator $mappingEvaluator, + private readonly HandoffQueueEntryMapper $queueMapper, + private readonly IDBConnection $db, + private readonly IEventDispatcher $eventDispatcher, + private readonly IUserSession $userSession, + private readonly IUserManager $userManager, + private readonly INotificationManager $notificationManager, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Report handoff availability for one object: every declared handoff with + * its state — `available` (naming the resolved provider schema), + * `unavailable` (machine-readable reason for the "provider not installed" + * UI copy), or `queued` (a parked queue-mode entry exists). + * + * The object is loaded through `ObjectService::find()` under the caller's + * RBAC (read guard, ADR-005). + * + * @param string $register The register slug or id. + * @param string $schema The schema slug or id. + * @param string $id The object id/uuid. + * + * @return array> One availability record per declared handoff. + * + * @throws \Exception When the object cannot be read by the caller. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Handoff REST surface) + */ + public function listAvailability(string $register, string $schema, string $id): array + { + $source = $this->objectService->find(id: $id, register: $register, schema: $schema); + if ($source === null) { + throw new HandoffException( + errorCode: HandoffException::NOT_DECLARED, + message: 'Object not found.' + ); + } + + $sourceSchema = $this->schemaMapper->find(id: (string) $source->getSchema()); + $entries = $this->declaredHandoffs(schema: $sourceSchema); + + $parked = []; + foreach ($this->queueMapper->findParkedForObject(sourceObjectUuid: (string) $source->getUuid()) as $entry) { + $parked[$entry->getHandoffId()] = $entry; + } + + $availability = []; + foreach ($entries as $entry) { + $handoffId = (string) ($entry['id'] ?? ''); + $kindUri = (string) ($entry['targetSemanticType'] ?? ''); + $mode = (string) ($entry['whenUnavailable'] ?? 'hide'); + + if (isset($parked[$handoffId]) === true) { + $availability[] = [ + 'id' => $handoffId, + 'targetSemanticType' => $kindUri, + 'trigger' => (string) ($entry['trigger'] ?? 'manual'), + 'whenUnavailable' => $mode, + 'state' => 'queued', + 'queueEntry' => $parked[$handoffId]->jsonSerialize(), + ]; + continue; + } + + $provider = $this->resolveProvider(kindUri: $kindUri, consumingRegisterId: (int) $source->getRegister()); + if ($provider === null) { + $availability[] = [ + 'id' => $handoffId, + 'targetSemanticType' => $kindUri, + 'trigger' => (string) ($entry['trigger'] ?? 'manual'), + 'whenUnavailable' => $mode, + 'state' => 'unavailable', + 'reason' => HandoffException::PROVIDER_UNAVAILABLE, + ]; + continue; + } + + $availability[] = [ + 'id' => $handoffId, + 'targetSemanticType' => $kindUri, + 'trigger' => (string) ($entry['trigger'] ?? 'manual'), + 'whenUnavailable' => $mode, + 'state' => 'available', + 'provider' => [ + 'schema' => ($provider->getSlug() ?? (string) $provider->getId()), + 'title' => $provider->getTitle(), + ], + ]; + }//end foreach + + return $availability; + + }//end listAvailability() + + /** + * Execute a declared handoff on a source object (or park it, queue mode). + * + * Flow (design.md): load source under caller RBAC → read declared entry → + * resolve provider (complete-binding filter) → degrade (hide 409 / queue + * 202) → RBAC pre-check create on target → evaluate mapping → all-or- + * nothing execution {create target, `onSuccess.set` through the + * lifecycle-aware write path, then provenance relations both ways + one + * audit row per side in a DB transaction; failures compensate by removing + * the target and restoring the source} → event dispatch afterwards. + * + * @param string $register The source register slug or id. + * @param string $schema The source schema slug or id. + * @param string $id The source object id/uuid. + * @param string $handoffId The declared handoff entry id. + * @param bool $deferred True when draining a parked queue entry. + * @param string|null $correlationId Correlation id to reuse (queue drain); minted when null. + * + * @return array `{status: executed, target: {...}, correlationId}` or `{status: parked, queueEntry: {...}}`. + * + * @throws HandoffException For not-declared / provider-unavailable (hide mode). + * @throws NotAuthorizedException When the caller lacks write-on-source or create-on-target. + * @throws \Exception Underlying write-path errors (surfaced after rollback). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Successful request-to-case handoff) + */ + public function execute( + string $register, + string $schema, + string $id, + string $handoffId, + bool $deferred=false, + ?string $correlationId=null + ): array { + // 1. Load the source under the caller's RBAC (read) — 404/403 surface here. + $source = $this->objectService->find(id: $id, register: $register, schema: $schema); + if ($source === null) { + throw new HandoffException(errorCode: HandoffException::NOT_DECLARED, message: 'Object not found.'); + } + + $sourceSchema = $this->schemaMapper->find(id: (string) $source->getSchema()); + + // 2. The declared entry — `handoff-not-declared` when absent. + $entry = $this->declaredHandoff(schema: $sourceSchema, handoffId: $handoffId); + if ($entry === null) { + throw new HandoffException( + errorCode: HandoffException::NOT_DECLARED, + message: sprintf( + 'Schema "%s" declares no handoff with id "%s".', + (string) ($sourceSchema->getSlug() ?? $sourceSchema->getId()), + $handoffId + ) + ); + } + + // Write permission on the source, as the calling user (never escalate). + $this->permissionHandler->checkPermission( + schema: $sourceSchema, + action: 'update', + objectOwner: $source->getOwner(), + object: $source + ); + + // 3. Resolve the provider (complete-binding filter on the resolver result). + $kindUri = (string) ($entry['targetSemanticType'] ?? ''); + $provider = $this->resolveProvider(kindUri: $kindUri, consumingRegisterId: (int) $source->getRegister()); + + if ($provider === null) { + return $this->degrade(entry: $entry, source: $source, kindUri: $kindUri, handoffId: $handoffId); + } + + $targetRegister = $this->semanticTypeResolver->findRegisterForSchema(schema: $provider); + if ($targetRegister === null) { + // An orphaned provider schema cannot receive objects — same degradation path. + return $this->degrade(entry: $entry, source: $source, kindUri: $kindUri, handoffId: $handoffId); + } + + // 4. RBAC pre-check: create on the resolved target schema, as the caller — typed 403 before any write. + $this->permissionHandler->checkPermission(schema: $provider, action: 'create'); + + // 5. Evaluate the mapping and translate contract fields → provider properties via the binding. + $correlationId = ($correlationId ?? Uuid::v4()->toRfc4122()); + $provenance = $this->buildProvenance(source: $source, sourceSchema: $sourceSchema); + $contractData = $this->mappingEvaluator->evaluate( + mapping: (array) ($entry['mapping'] ?? []), + sourceData: $source->getObject(), + provenance: $provenance + ); + $targetData = $this->translateThroughBinding(provider: $provider, kindUri: $kindUri, contractData: $contractData); + + // 6. All-or-nothing execution. A single wrapping DB transaction is NOT + // possible here: ObjectService::saveObject issues best-effort probe + // queries that may fail and be swallowed by design (e.g. cross-schema + // magic-table COUNTs), and on PostgreSQL ANY failed statement aborts + // an open caller-managed transaction (SQLSTATE 25P02) — verified live. + // Instead: (a) the target create is atomic within OR's own write path; + // (b) the onSuccess source update runs next, compensated by deleting + // the created target on failure; (c) provenance relations + audit rows + // — plain mapper writes on known tables — run inside one real DB + // transaction, compensated by target delete + source revert on + // failure. Observable contract holds: a failed handoff leaves no + // target, no relation, no handoff audit row, and no source mutation. + $target = $this->objectService->saveObject( + object: $targetData, + register: $targetRegister, + schema: $provider + ); + + $sourceSnapshot = $source->getObject(); + try { + $this->applyOnSuccess(entry: $entry, source: $source, register: $register, schema: $schema); + } catch (\Throwable $e) { + $this->compensate( + target: $target, + targetRegister: $targetRegister, + targetSchema: $provider + ); + throw $e; + } + + $this->db->beginTransaction(); + try { + $this->writeProvenanceRelations( + source: $source, + target: $target, + handoffId: $handoffId, + sourceSchema: $sourceSchema, + targetRegister: $targetRegister, + targetSchema: $provider + ); + $this->writeAuditRows( + source: $source, + target: $target, + provider: $provider, + kindUri: $kindUri, + handoffId: $handoffId, + correlationId: $correlationId, + deferred: $deferred + ); + + $this->db->commit(); + } catch (\Throwable $e) { + $this->db->rollBack(); + $this->compensate( + target: $target, + targetRegister: $targetRegister, + targetSchema: $provider, + source: $source, + sourceSnapshot: $sourceSnapshot, + sourceRegisterRef: $register, + sourceSchemaRef: $schema + ); + throw $e; + }//end try + + // 7. Post-commit: dispatch the ADR-041 event (a throwing listener can never roll back the handoff). + $this->eventDispatcher->dispatchTyped( + new HandoffExecutedEvent( + sourceApp: (string) ($sourceSchema->getApplication() ?? ''), + sourceRegister: (int) $source->getRegister(), + sourceSchema: (int) $source->getSchema(), + sourceObjectUuid: (string) $source->getUuid(), + subjectLabel: $source->getName(), + targetSemanticType: $kindUri, + targetRegister: (int) $target->getRegister(), + targetSchema: (int) $target->getSchema(), + targetObjectUuid: (string) $target->getUuid(), + handoffId: $handoffId, + correlationId: $correlationId, + deferred: $deferred + ) + ); + + return [ + 'status' => 'executed', + 'correlationId' => $correlationId, + 'target' => [ + 'register' => (int) $target->getRegister(), + 'schema' => (int) $target->getSchema(), + 'uuid' => (string) $target->getUuid(), + ], + ]; + + }//end execute() + + /** + * Drain parked queue entries whose kind now resolves to a provider. + * + * Runs each entry AS THE RECORDED REQUESTING USER (impersonation via the + * same user-session mechanics OR background jobs use), re-evaluating RBAC + * at drain time: a requester who lost create permission gets a + * `failed-permission` entry + a notification — the queue is never a + * privilege-escalation time capsule. + * + * @param string|null $kindUri Drain only entries parked for this kind (null = all parked kinds). + * + * @return array{drained: int, failed: int, skipped: int} Drain summary. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + public function drainParked(?string $kindUri=null): array + { + $entries = []; + if ($kindUri !== null) { + $entries = $this->queueMapper->findParkedByKind(kindUri: $kindUri); + } + + if ($kindUri === null) { + $entries = $this->queueMapper->findParked(); + } + + $summary = [ + 'drained' => 0, + 'failed' => 0, + 'skipped' => 0, + ]; + + foreach ($entries as $entry) { + // Cheap pre-check: still no provider → leave parked. + $this->semanticTypeResolver->clearCache(); + if ($this->resolveProvider(kindUri: $entry->getTargetKind(), consumingRegisterId: $entry->getSourceRegister()) === null) { + $summary['skipped']++; + continue; + } + + if ($this->drainEntry(entry: $entry) === true) { + $summary['drained']++; + continue; + } + + $summary['failed']++; + } + + return $summary; + + }//end drainParked() + + /** + * Drain a single parked entry as its recorded requesting user. + * + * @param HandoffQueueEntry $entry The parked entry. + * + * @return bool True when the entry executed; false when it failed (status updated accordingly). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + private function drainEntry(HandoffQueueEntry $entry): bool + { + $entry->setAttempt($entry->getAttempt() + 1); + $entry->setLastAttemptAt(new DateTime()); + + $requester = $this->userManager->get($entry->getRequestingUser()); + if ($requester === null) { + $entry->setStatus(HandoffQueueEntry::STATUS_FAILED_PERMISSION); + $entry->setLastError('Requesting user no longer exists.'); + $this->queueMapper->update(entity: $entry); + return false; + } + + $previousUser = $this->userSession->getUser(); + $this->userSession->setUser($requester); + + try { + $this->execute( + register: (string) $entry->getSourceRegister(), + schema: (string) $entry->getSourceSchema(), + id: $entry->getSourceObjectUuid(), + handoffId: $entry->getHandoffId(), + deferred: true, + correlationId: $entry->getCorrelationId() + ); + + $entry->setStatus(HandoffQueueEntry::STATUS_EXECUTED); + $entry->setExecutedAt(new DateTime()); + $entry->setLastError(null); + $this->queueMapper->update(entity: $entry); + + $this->auditDequeued(entry: $entry); + return true; + } catch (NotAuthorizedException $e) { + // RBAC re-evaluated at drain time refused: never escalate. + $entry->setStatus(HandoffQueueEntry::STATUS_FAILED_PERMISSION); + $entry->setLastError($e->getMessage()); + $this->queueMapper->update(entity: $entry); + $this->notifyDrainFailure(entry: $entry); + return false; + } catch (\Throwable $e) { + $entry->setStatus(HandoffQueueEntry::STATUS_FAILED_VALIDATION); + $entry->setLastError($e->getMessage()); + $this->queueMapper->update(entity: $entry); + $this->notifyDrainFailure(entry: $entry); + return false; + } finally { + $this->userSession->setUser($previousUser); + }//end try + + }//end drainEntry() + + /** + * Degrade when no provider implements the kind: `hide` → typed + * provider-unavailable error (409-class, never 5xx); `queue` → park a + * durable queue entry + audit + 202-style parked result. + * + * @param array $entry The declared handoff entry. + * @param ObjectEntity $source The source object. + * @param string $kindUri The target kind URI. + * @param string $handoffId The handoff entry id. + * + * @return array The parked result (queue mode only). + * + * @throws HandoffException In hide mode (provider unavailable). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, hide mode) + */ + private function degrade(array $entry, ObjectEntity $source, string $kindUri, string $handoffId): array + { + $mode = (string) ($entry['whenUnavailable'] ?? 'hide'); + if ($mode !== 'queue') { + throw new HandoffException( + errorCode: HandoffException::PROVIDER_UNAVAILABLE, + message: sprintf('No installed schema implements "%s"; the source object keeps working standalone.', $kindUri) + ); + } + + $requestingUser = ($this->userSession->getUser()?->getUID() ?? ''); + $correlationId = Uuid::v4()->toRfc4122(); + + $queueEntry = new HandoffQueueEntry(); + $queueEntry->setSourceObjectUuid((string) $source->getUuid()); + $queueEntry->setSourceRegister((int) $source->getRegister()); + $queueEntry->setSourceSchema((int) $source->getSchema()); + $queueEntry->setHandoffId($handoffId); + $queueEntry->setTargetKind($kindUri); + $queueEntry->setRequestingUser($requestingUser); + $queueEntry->setCorrelationId($correlationId); + $queueEntry->setMappingHash(hash('sha256', (string) json_encode($entry['mapping'] ?? []))); + $queueEntry->setStatus(HandoffQueueEntry::STATUS_PARKED); + // Explicit setter (not just the constructor default) so QBMapper marks + // the column as updated and includes it in the INSERT. + $queueEntry->setCreated(new DateTime()); + + $queueEntry = $this->queueMapper->insert(entity: $queueEntry); + + $this->auditTrailMapper->createAuditTrailEntry( + object: $source, + action: self::AUDIT_QUEUED, + context: [ + 'handoffId' => $handoffId, + 'targetKind' => $kindUri, + 'correlationId' => $correlationId, + 'deferred' => true, + 'parkedAt' => $queueEntry->getCreated()->format('c'), + ] + ); + + return [ + 'status' => 'parked', + 'correlationId' => $correlationId, + 'queueEntry' => $queueEntry->jsonSerialize(), + ]; + + }//end degrade() + + /** + * Resolve the provider schema for a kind, filtered to providers with a + * COMPLETE `handoffContract` binding (design: a schema implementing a + * kind with no/incomplete binding is not a handoff provider — ADR-048 + * reference resolution stays untouched). + * + * @param string $kindUri The canonical kind URI. + * @param int|null $consumingRegisterId The source register id (tie-break bias). + * + * @return Schema|null The provider schema, or null when none qualifies. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Graceful degradation when no provider implements the kind) + */ + private function resolveProvider(string $kindUri, ?int $consumingRegisterId=null): ?Schema + { + if ($kindUri === '') { + return null; + } + + $schema = $this->semanticTypeResolver->resolveSchemaByImplements( + uri: $kindUri, + consumingRegisterId: $consumingRegisterId + ); + if ($schema === null) { + return null; + } + + $configuration = ($schema->getConfiguration() ?? []); + $binding = ($configuration['handoffContract'] ?? null); + if (is_array($binding) === false) { + return null; + } + + $isComplete = HandoffContractBindingValidator::isCompleteBinding( + kindUri: $kindUri, + binding: $binding, + properties: ($schema->getProperties() ?? []) + ); + if ($isComplete === false) { + return null; + } + + return $schema; + + }//end resolveProvider() + + /** + * Translate evaluated contract-field values to provider properties + * exclusively through the provider's `handoffContract` binding — the + * emitting schema never names a concrete target property. + * + * @param Schema $provider The resolved provider schema. + * @param string $kindUri The kind URI. + * @param array $contractData Contract field → evaluated value. + * + * @return array Provider property → value. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: Kind contract binding on the implementing schema) + */ + private function translateThroughBinding(Schema $provider, string $kindUri, array $contractData): array + { + $configuration = ($provider->getConfiguration() ?? []); + $fieldMap = (array) (($configuration['handoffContract'] ?? [])[$kindUri] ?? []); + + $targetData = []; + foreach ($contractData as $contractField => $value) { + $ownProperty = ($fieldMap[$contractField] ?? null); + if (is_string($ownProperty) === true && $ownProperty !== '') { + $targetData[$ownProperty] = $value; + } + } + + return $targetData; + + }//end translateThroughBinding() + + /** + * The engine-filled provenance pointer for the `provenance` expression + * kind and the audit rows: `{app, register, schema, uuid}` of the source. + * + * @param ObjectEntity $source The source object. + * @param Schema $sourceSchema The source schema. + * + * @return array The provenance pointer. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: HandoffService executes conversions on top of SemanticTypeResolver) + */ + private function buildProvenance(ObjectEntity $source, Schema $sourceSchema): array + { + return [ + 'app' => (string) ($sourceSchema->getApplication() ?? ''), + 'register' => (int) $source->getRegister(), + 'schema' => (int) $source->getSchema(), + 'uuid' => (string) $source->getUuid(), + ]; + + }//end buildProvenance() + + /** + * Compensate a partially-executed handoff so no partial state remains: + * hard-delete the created target row and (when the source was already + * mutated) restore the source object's pre-handoff data through the + * normal write path. Compensation is best-effort — a compensation + * failure is logged loudly but never masks the original error. + * + * @param ObjectEntity $target The created target object to remove. + * @param Register $targetRegister The target register (magic-table context). + * @param Schema $targetSchema The target schema (magic-table context). + * @param ObjectEntity|null $source The source object (when it was already mutated). + * @param array $sourceSnapshot The source data before `onSuccess.set`. + * @param string|null $sourceRegisterRef The source register reference for the revert write. + * @param string|null $sourceSchemaRef The source schema reference for the revert write. + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Target create fails mid-handoff) + */ + private function compensate( + ObjectEntity $target, + Register $targetRegister, + Schema $targetSchema, + ?ObjectEntity $source=null, + array $sourceSnapshot=[], + ?string $sourceRegisterRef=null, + ?string $sourceSchemaRef=null + ): void { + try { + $rawTarget = $this->objectMapper->find( + identifier: (string) $target->getUuid(), + register: $targetRegister, + schema: $targetSchema, + _rbac: false, + _multitenancy: false + ); + $this->objectMapper->delete(entity: $rawTarget); + } catch (\Throwable $e) { + $this->logger->error( + message: '[HandoffService] Compensation could not remove the created target — manual cleanup required', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'target' => (string) $target->getUuid(), + 'error' => $e->getMessage(), + ] + ); + }//end try + + if ($source === null || $sourceRegisterRef === null || $sourceSchemaRef === null) { + return; + } + + try { + $this->objectService->saveObject( + object: $sourceSnapshot, + register: $sourceRegisterRef, + schema: $sourceSchemaRef, + uuid: (string) $source->getUuid() + ); + } catch (\Throwable $e) { + $this->logger->error( + message: '[HandoffService] Compensation could not restore the source object — manual review required', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'source' => (string) $source->getUuid(), + 'error' => $e->getMessage(), + ] + ); + }//end try + + }//end compensate() + + /** + * Apply the declared `onSuccess.set` update to the source object through + * the normal lifecycle-aware write path (`ObjectService::saveObject`). + * + * Part of the all-or-nothing execution: a failure here triggers + * compensation (the created target is removed), because a source stuck + * without its handed-off status while the target exists — or vice versa — + * is partial state the contract forbids. + * + * @param array $entry The declared handoff entry. + * @param ObjectEntity $source The source object. + * @param string $register The source register reference. + * @param string $schema The source schema reference. + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Successful request-to-case handoff) + */ + private function applyOnSuccess(array $entry, ObjectEntity $source, string $register, string $schema): void + { + $set = (array) (($entry['onSuccess'] ?? [])['set'] ?? []); + if ($set === []) { + return; + } + + $data = array_merge($source->getObject(), $set); + $this->objectService->saveObject( + object: $data, + register: $register, + schema: $schema, + uuid: (string) $source->getUuid() + ); + + }//end applyOnSuccess() + + /** + * Write the typed `handoff` provenance relation on both objects: + * `handoff::handed-off-to` → target on the source and + * `handoff::originated-from` → source on the target (Related-widget + * surface, MergeHandler precedent for engine-written relation keys). + * + * Register + schema context is passed explicitly so MagicMapper hits the + * one correct magic table instead of scanning across all of them + * (`findAcrossAllMagicTables` is prohibitively slow inside the handoff + * transaction on large instances). + * + * @param ObjectEntity $source The source object. + * @param ObjectEntity $target The created target object. + * @param string $handoffId The handoff entry id. + * @param Schema $sourceSchema The source schema (magic-table context). + * @param Register $targetRegister The target register (magic-table context). + * @param Schema $targetSchema The target schema (magic-table context). + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Successful request-to-case handoff) + */ + private function writeProvenanceRelations( + ObjectEntity $source, + ObjectEntity $target, + string $handoffId, + Schema $sourceSchema, + Register $targetRegister, + Schema $targetSchema + ): void { + $sourceRegister = $this->semanticTypeResolver->findRegisterForSchema(schema: $sourceSchema); + + // Fresh raw source row: applyOnSuccess() may have rewritten it (and + // rebuilt its relations map from data) moments ago. + $rawSource = $this->objectMapper->find( + identifier: (string) $source->getUuid(), + register: $sourceRegister, + schema: $sourceSchema, + _rbac: false, + _multitenancy: false + ); + + $sourceRelations = ($rawSource->getRelations() ?? []); + $sourceRelations[self::RELATION_PREFIX.':'.$handoffId.':handed-off-to'] = (string) $target->getUuid(); + $rawSource->setRelations($sourceRelations); + $this->objectMapper->update(entity: $rawSource, register: $sourceRegister, schema: $sourceSchema); + + $rawTarget = $this->objectMapper->find( + identifier: (string) $target->getUuid(), + register: $targetRegister, + schema: $targetSchema, + _rbac: false, + _multitenancy: false + ); + + $targetRelations = ($rawTarget->getRelations() ?? []); + $targetRelations[self::RELATION_PREFIX.':'.$handoffId.':originated-from'] = (string) $source->getUuid(); + $rawTarget->setRelations($targetRelations); + $this->objectMapper->update(entity: $rawTarget, register: $targetRegister, schema: $targetSchema); + + }//end writeProvenanceRelations() + + /** + * One immutable audit row per side (actor, kind, handoff id, + * correlationId, resolved schema, deferred flag). + * + * @param ObjectEntity $source The source object. + * @param ObjectEntity $target The created target object. + * @param Schema $provider The resolved provider schema. + * @param string $kindUri The kind URI. + * @param string $handoffId The handoff entry id. + * @param string $correlationId The execution correlation id. + * @param bool $deferred Whether this execution drained a parked entry. + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Successful request-to-case handoff) + */ + private function writeAuditRows( + ObjectEntity $source, + ObjectEntity $target, + Schema $provider, + string $kindUri, + string $handoffId, + string $correlationId, + bool $deferred + ): void { + $context = [ + 'handoffId' => $handoffId, + 'targetKind' => $kindUri, + 'correlationId' => $correlationId, + 'resolvedSchema' => ($provider->getSlug() ?? (string) $provider->getId()), + 'deferred' => $deferred, + 'drainedAt' => null, + ]; + if ($deferred === true) { + $context['drainedAt'] = (new DateTime())->format('c'); + } + + $this->auditTrailMapper->createAuditTrailEntry( + object: $source, + action: self::AUDIT_EXECUTED, + context: array_merge($context, ['direction' => 'handed-off-to', 'counterpart' => (string) $target->getUuid()]) + ); + $this->auditTrailMapper->createAuditTrailEntry( + object: $target, + action: self::AUDIT_EXECUTED, + context: array_merge($context, ['direction' => 'originated-from', 'counterpart' => (string) $source->getUuid()]) + ); + + }//end writeAuditRows() + + /** + * Audit the successful drain of a parked entry on the source object. + * + * @param HandoffQueueEntry $entry The drained entry. + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + private function auditDequeued(HandoffQueueEntry $entry): void + { + try { + // Explicit register/schema context keeps MagicMapper on the one + // correct magic table (no cross-table scan). + $sourceSchema = $this->schemaMapper->find(id: (string) $entry->getSourceSchema()); + $sourceRegister = $this->semanticTypeResolver->findRegisterForSchema(schema: $sourceSchema); + $source = $this->objectMapper->find( + identifier: $entry->getSourceObjectUuid(), + register: $sourceRegister, + schema: $sourceSchema, + _rbac: false, + _multitenancy: false + ); + $this->auditTrailMapper->createAuditTrailEntry( + object: $source, + action: self::AUDIT_DEQUEUED, + context: [ + 'handoffId' => $entry->getHandoffId(), + 'targetKind' => $entry->getTargetKind(), + 'correlationId' => $entry->getCorrelationId(), + 'deferred' => true, + 'parkedAt' => $entry->getCreated()->format('c'), + 'drainedAt' => ($entry->getExecutedAt()?->format('c') ?? (new DateTime())->format('c')), + ] + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[HandoffService] Could not write dequeue audit row', + context: ['file' => __FILE__, 'line' => __LINE__, 'entry' => $entry->getId(), 'error' => $e->getMessage()] + ); + }//end try + + }//end auditDequeued() + + /** + * Notify the requester that their parked handoff failed at drain time. + * + * @param HandoffQueueEntry $entry The failed entry. + * + * @return void + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: No provider installed, queue mode) + */ + private function notifyDrainFailure(HandoffQueueEntry $entry): void + { + try { + $notification = $this->notificationManager->createNotification(); + $notification->setApp('openregister'); + $notification->setUser($entry->getRequestingUser()); + $notification->setDateTime(new DateTime()); + $notification->setObject('handoff_queue_entry', (string) $entry->getId()); + $notification->setSubject( + 'handoff_drain_failed', + [ + 'handoffId' => $entry->getHandoffId(), + 'targetKind' => $entry->getTargetKind(), + 'status' => $entry->getStatus(), + ] + ); + $this->notificationManager->notify($notification); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[HandoffService] Could not notify requester of drain failure', + context: ['file' => __FILE__, 'line' => __LINE__, 'entry' => $entry->getId(), 'error' => $e->getMessage()] + ); + }//end try + + }//end notifyDrainFailure() + + /** + * All `x-openregister-handoff` entries declared on a schema. + * + * @param Schema $schema The source schema. + * + * @return array> The declared entries (empty when none). + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Requirement: `x-openregister-handoff` declarative dialect) + */ + public function declaredHandoffs(Schema $schema): array + { + $configuration = ($schema->getConfiguration() ?? []); + $annotation = ($configuration['x-openregister-handoff'] ?? null); + if (is_array($annotation) === false) { + return []; + } + + $entries = []; + foreach ($annotation as $entry) { + if (is_array($entry) === true && is_string($entry['id'] ?? null) === true) { + $entries[] = $entry; + } + } + + return $entries; + + }//end declaredHandoffs() + + /** + * One declared handoff entry by id, or null. + * + * @param Schema $schema The source schema. + * @param string $handoffId The entry id. + * + * @return array|null The entry, or null when not declared. + * + * @spec openspec/changes/semantic-object-handoff-engine/specs/semantic-object-handoff/spec.md + * (Scenario: Execute endpoint on an object whose schema declares no handoffs) + */ + private function declaredHandoff(Schema $schema, string $handoffId): ?array + { + foreach ($this->declaredHandoffs(schema: $schema) as $entry) { + if (($entry['id'] ?? null) === $handoffId) { + return $entry; + } + } + + return null; + + }//end declaredHandoff() +}//end class diff --git a/lib/Service/HookExecutor.php b/lib/Service/HookExecutor.php index dfc40f712d..dcc8423ee1 100644 --- a/lib/Service/HookExecutor.php +++ b/lib/Service/HookExecutor.php @@ -19,15 +19,15 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-65 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-66 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-67 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-68 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-69 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-70 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-71 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-72 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-71 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-configuration-on-schema + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-lifecycle-events + * @spec openspec/specs/schema-hooks/spec.md#requirement-sync-hook-response-format + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-execution-order + * @spec openspec/specs/schema-hooks/spec.md#requirement-failure-mode-behavior + * @spec openspec/specs/schema-hooks/spec.md#requirement-filter-condition-for-conditional-hook-execution + * @spec openspec/specs/schema-hooks/spec.md#requirement-async-hook-execution-fire-and-forget + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-retry-via-background-job + * @spec openspec/specs/schema-hooks/spec.md */ declare(strict_types=1); @@ -103,8 +103,8 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-65 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-71 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-configuration-on-schema + * @spec openspec/specs/schema-hooks/spec.md */ public function executeHooks(Event $event, Schema $schema): void { @@ -145,7 +145,7 @@ public function executeHooks(Event $event, Schema $schema): void * * @return string|null The event type string (e.g. 'creating') or null * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-65 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-configuration-on-schema */ private function resolveEventType(Event $event): ?string { @@ -168,7 +168,7 @@ private function resolveEventType(Event $event): ?string * * @return array> Sorted array of hook configurations * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-65 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-configuration-on-schema */ private function loadHooks(Schema $schema, string $eventType): array { @@ -204,7 +204,7 @@ static function (array $hookA, array $hookB): int { * * @return ObjectEntity|null The object entity or null * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-65 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-configuration-on-schema */ private function getObjectFromEvent(Event $event): ?ObjectEntity { @@ -242,7 +242,7 @@ private function getObjectFromEvent(Event $event): ?ObjectEntity * * @return bool True if propagation is stopped * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-68 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-execution-order */ private function isEventStopped(Event $event): bool { @@ -272,7 +272,7 @@ private function isEventStopped(Event $event): bool * * @return bool True if the hook should execute * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-70 + * @spec openspec/specs/schema-hooks/spec.md#requirement-filter-condition-for-conditional-hook-execution */ private function evaluateFilterCondition(array $hook, ObjectEntity $object): bool { @@ -311,7 +311,7 @@ private function evaluateFilterCondition(array $hook, ObjectEntity $object): boo * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Dispatch: filter, engine, async/sync, result, exception - each a required lifecycle branch * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-65 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-configuration-on-schema */ private function executeSingleHook( array $hook, @@ -428,7 +428,7 @@ private function executeSingleHook( * * @return array CloudEvent-formatted payload * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-66 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-lifecycle-events */ private function buildCloudEventPayload( ObjectEntity $object, @@ -472,7 +472,7 @@ private function buildCloudEventPayload( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-71 + * @spec openspec/specs/schema-hooks/spec.md#requirement-async-hook-execution-fire-and-forget */ private function executeAsyncHook( $adapter, @@ -522,7 +522,7 @@ private function executeAsyncHook( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-67 + * @spec openspec/specs/schema-hooks/spec.md#requirement-sync-hook-response-format */ private function processWorkflowResult( WorkflowResult $result, @@ -610,7 +610,7 @@ private function processWorkflowResult( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-67 + * @spec openspec/specs/schema-hooks/spec.md#requirement-sync-hook-response-format */ private function setModifiedDataOnEvent(Event $event, array $data): void { @@ -631,7 +631,7 @@ private function setModifiedDataOnEvent(Event $event, array $data): void * * @return string The failure mode to apply * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-69 + * @spec openspec/specs/schema-hooks/spec.md#requirement-failure-mode-behavior */ private function determineFailureMode(Exception $exception, array $hook): string { @@ -668,7 +668,7 @@ private function determineFailureMode(Exception $exception, array $hook): string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Failure modes reject/allow/flag/queue plus unknown fallback - each case has distinct side effects * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-69 + * @spec openspec/specs/schema-hooks/spec.md#requirement-failure-mode-behavior */ private function applyFailureMode( string $failureMode, @@ -736,7 +736,7 @@ private function applyFailureMode( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-69 + * @spec openspec/specs/schema-hooks/spec.md#requirement-failure-mode-behavior */ private function stopEvent(Event $event, array $errors, string $fallbackError): void { @@ -767,7 +767,7 @@ private function stopEvent(Event $event, array $errors, string $fallbackError): * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-69 + * @spec openspec/specs/schema-hooks/spec.md#requirement-failure-mode-behavior */ private function setValidationMetadata( ObjectEntity $object, @@ -796,7 +796,7 @@ private function setValidationMetadata( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-72 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-retry-via-background-job */ private function scheduleRetryJob(ObjectEntity $object, array $hook): void { @@ -827,7 +827,7 @@ private function scheduleRetryJob(ObjectEntity $object, array $hook): void * * @SuppressWarnings(PHPMD.ExcessiveParameterList) hook, timing, event type, object, and 4 optional fields; extracted from logHookExecution * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-68 + * @spec openspec/specs/schema-hooks/spec.md#requirement-hook-execution-order */ private function logHookExecution( array $hook, diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index 22941e1dbd..76efec1c0f 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -17,10 +17,10 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-9 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-10 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-23 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/object-lifecycle/spec.md */ declare(strict_types=1); @@ -31,11 +31,13 @@ use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; -use OCA\OpenRegister\BackgroundJob\SolrWarmupJob; +use OCA\OpenRegister\Db\ObjectHandling; +use OCA\OpenRegister\Listener\NotifyPushListener; +use OCA\OpenRegister\Service\MigrationPack\MappingEngine; +use OCA\OpenRegister\Service\Object\ValidateObject; use OCP\IUserManager; use OCP\IGroupManager; use OCP\IUser; -use OCP\BackgroundJob\IJobList; use Symfony\Component\Uid\Uuid; use PhpOffice\PhpSpreadsheet\Reader\Csv; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; @@ -43,6 +45,7 @@ use InvalidArgumentException; use Exception; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use React\Async\PromiseInterface; use React\Promise\Promise; @@ -64,6 +67,8 @@ * * @package OCA\OpenRegister\Service * + * @spec openspec/specs/migration-mapping-packs/spec.md + * * @SuppressWarnings(PHPMD.ExcessiveClassLength) Import service requires comprehensive data transformation methods * @SuppressWarnings(PHPMD.TooManyMethods) Many methods required for multi-format import support * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complex import logic with multiple data formats @@ -139,13 +144,6 @@ class ImportService */ private readonly IGroupManager $groupManager; - /** - * Background job list for scheduling SOLR warmup jobs - * - * @var IJobList - */ - private readonly IJobList $jobList; - /** * Translation CSV codec for column projection on import/export. * @@ -156,28 +154,32 @@ class ImportService /** * Constructor for the ImportService * - * @param SchemaMapper $schemaMapper The schema mapper - * @param ObjectService $objectService The object service - * @param LoggerInterface $logger The logger interface - * @param IGroupManager $groupManager The group manager - * @param IJobList $jobList The background job list - * @param \OCA\OpenRegister\Service\Translation\TranslationCsvCodec $translationCsvCodec Translation CSV codec - * @param AuditTrailMapper $auditTrailMapper The audit trail mapper + * @param SchemaMapper $schemaMapper The schema mapper + * @param ObjectService $objectService The object service + * @param LoggerInterface $logger The logger interface + * @param IGroupManager $groupManager The group manager + * @param \OCA\OpenRegister\Service\Translation\TranslationCsvCodec $translationCsvCodec Translation CSV codec + * @param AuditTrailMapper $auditTrailMapper The audit trail mapper + * @param MappingEngine $mappingEngine Migration-pack mapping engine + * @param ValidateObject $validateObjectHandler Schema validator, used for genuinely + * side-effect-free dry-run imports + * @param ContainerInterface $container DI container for lazy IQueue resolution */ public function __construct( SchemaMapper $schemaMapper, ObjectService $objectService, LoggerInterface $logger, IGroupManager $groupManager, - IJobList $jobList, \OCA\OpenRegister\Service\Translation\TranslationCsvCodec $translationCsvCodec, - private readonly AuditTrailMapper $auditTrailMapper + private readonly AuditTrailMapper $auditTrailMapper, + private readonly MappingEngine $mappingEngine, + private readonly ValidateObject $validateObjectHandler, + private readonly ContainerInterface $container ) { $this->schemaMapper = $schemaMapper; $this->objectService = $objectService; $this->logger = $logger; $this->groupManager = $groupManager; - $this->jobList = $jobList; $this->translationCsvCodec = $translationCsvCodec; // Initialize cache arrays to prevent issues. @@ -340,8 +342,8 @@ private function isUserAdmin(?IUser $user): bool * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control import behavior options * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-9 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/data-import-export/spec.md */ public function importFromExcel( string $filePath, @@ -420,7 +422,6 @@ public function importFromExcel( $sheetTitle => $sheetSummary, 'importJobId' => $importJobId, ]; - $this->scheduleSmartSolrWarmup(importSummary: $finalResult); return $finalResult; } finally { @@ -441,12 +442,18 @@ public function importFromExcel( * @param bool $publish DEPRECATED: No-op. Object-level publish metadata removed; use RBAC $now rules. * @param IUser|null $currentUser Current user for RBAC checks (default: null). * @param bool $enrich Whether to enrich objects with metadata (default: true). + * @param array|null $pack Optional migration pack definition (decoded JSON). When given, each row is + * mapped through `MappingEngine::mapRow()` before the normal validate/save + * pipeline. + * @param bool $dryRun When true, rows are mapped and validated but nothing is saved. * * @return array Import results by schema * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control import behavior options + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control import behavior options + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Mirrors the existing importFrom* signatures; pack/dryRun extend it. * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/data-import-export/spec.md + * @spec openspec/specs/migration-mapping-packs/spec.md */ public function importFromCsv( string $filePath, @@ -458,7 +465,9 @@ public function importFromCsv( bool $_multitenancy=true, bool $publish=false, ?IUser $currentUser=null, - bool $enrich=true + bool $enrich=true, + ?array $pack=null, + bool $dryRun=false ): array { // Clear caches at the start of each import to prevent stale data issues. $this->clearCaches(); @@ -493,7 +502,9 @@ public function importFromCsv( _multitenancy: $_multitenancy, publish: $publish, currentUser: $currentUser, - enrich: $enrich + enrich: $enrich, + pack: $pack, + dryRun: $dryRun ); // Add schema information to the summary (consistent with Excel import). @@ -508,7 +519,6 @@ public function importFromCsv( $sheetTitle => $sheetSummary, 'importJobId' => $importJobId, ]; - $this->scheduleSmartSolrWarmup(importSummary: $finalResult); return $finalResult; } finally { @@ -516,6 +526,239 @@ public function importFromCsv( }//end try }//end importFromCsv() + /** + * Import objects of a single schema from a JSON document + * + * Inverse of `ExportService::exportToJson()`. Accepts either a bare JSON + * array of objects or a `{ "results": [...] }` envelope, then upserts every + * object (by uuid) through `ObjectService::saveObject()` — the same + * single-object path the REST create/update uses, applying RBAC and + * multi-tenancy. JSON carries no spreadsheet, so no PhpSpreadsheet (and + * therefore no ZipStream) is involved. + * + * @param string $filePath Path to the uploaded JSON file + * @param Register|null $register Register to import into + * @param Schema|null $schema Target schema (required — JSON import is single-schema, like + * CSV) + * @param bool $validation Whether to validate objects against the schema + * @param bool $events Whether to dispatch object lifecycle events + * @param bool $_rbac Whether to apply RBAC permissions + * @param bool $_multitenancy Whether to apply multi-tenancy filtering + * @param bool $publish DEPRECATED no-op; publication is RBAC-driven + * @param IUser|null $currentUser The current user performing the import + * @param bool $enrich Whether to enrich objects with metadata + * @param array|null $pack Optional migration pack definition (decoded JSON). When given, each JSON + * object is mapped through `MappingEngine::mapRow()` (source resolved via + * JSON-Pointer-style paths against the raw decoded object) before save. + * @param bool $dryRun When true, rows are mapped and validated but nothing is saved. + * + * @return array Sheet-shaped summary (keyed 'JSON') plus importJobId + * + * @throws InvalidArgumentException When no schema is given or the payload is not an array of objects + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control import behavior options + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Mirrors the Excel/CSV importer signatures + * @SuppressWarnings(PHPMD.UnusedFormalParameter) validation/events/enrich are kept for signature parity with importFromExcel/importFromCsv + * + * @spec exclude Retrofit — JSON object import/export added alongside the existing Excel/CSV importers; no dedicated openspec change. + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function importFromJson( + string $filePath, + ?Register $register=null, + ?Schema $schema=null, + bool $validation=false, + bool $events=false, + bool $_rbac=true, + bool $_multitenancy=true, + bool $publish=false, + ?IUser $currentUser=null, + bool $enrich=true, + ?array $pack=null, + bool $dryRun=false + ): array { + // Clear caches at the start of each import to prevent stale data issues. + $this->clearCaches(); + $startTime = microtime(true); + + if ($schema === null) { + throw new InvalidArgumentException('JSON import requires a specific schema'); + } + + if ($publish === true) { + $this->logger->warning( + message: '[ImportService] The $publish parameter is deprecated. Use RBAC $now rules instead.', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + } + + $raw = file_get_contents($filePath); + $decoded = null; + if ($raw !== false) { + $decoded = json_decode($raw, true); + } + + if (is_array($decoded) === false) { + throw new InvalidArgumentException('JSON import expects an array of objects'); + } + + // Accept both a bare array and a `{ "results": [...] }` envelope (the + // shape some object-list endpoints return). + $objects = $decoded; + if (isset($decoded['results']) === true && is_array($decoded['results']) === true) { + $objects = $decoded['results']; + } + + // ExportService::exportToJson() emits ObjectEntity::jsonSerialize() — + // schema properties at the top level PLUS an `@self` block and + // entity-level fields (uuid/version/slug/dates) that are NOT schema + // properties. Persist each object through the single-object saveObject() + // path — the same one the REST create/update uses — which reliably + // upserts by uuid for this register/schema. (The bulk saveObjects() + // path silently skips these objects for dedicated-table schemas.) The + // body is reduced to the schema's own properties, with empty strings + // coerced to null. + $propertyKeys = array_flip(array_keys($schema->getProperties())); + + $importJobId = Uuid::v4()->toRfc4122(); + + try { + $this->auditTrailMapper->setRequestImportJobId(importJobId: $importJobId); + + $summary = [ + 'found' => count($objects), + 'created' => [], + 'updated' => [], + 'unchanged' => [], + 'errors' => [], + ]; + + // Phase 1: resolve every row to an (uuid, body) pair. When a pack is + // given, each source object is mapped through it first (source + // resolved via JSON-Pointer-style paths against the raw decoded + // object) — a row with mapping errors (missing required source, or + // an unresolved lookup/reference — the literal-leak guard) is + // reported and excluded, never partially mapped or saved. + $resolved = []; + foreach ($objects as $rowIndex => $raw) { + if (is_array($raw) === false) { + continue; + } + + // Keys are list indexes for a JSON array export; cast guards + // against a decoded object-with-string-keys envelope. + $rowNumber = ((int) $rowIndex + 1); + if ($pack !== null && $this->mappingEngine->isRowSkipped(pack: $pack, rowNumber: $rowNumber) === true) { + continue; + } + + if ($pack !== null) { + $mapped = $this->mappingEngine->mapRow(pack: $pack, sourceRow: $raw, rowNumber: $rowNumber); + if (empty($mapped['errors']) === false) { + foreach ($mapped['errors'] as $mappingError) { + $summary['errors'][] = $this->formatMappingError(error: $mappingError); + } + + continue; + } + + $body = $mapped['data']; + $uuid = $body['id'] ?? null; + unset($body['id']); + if ($uuid !== null) { + $uuid = (string) $uuid; + } + } else { + // Upsert key: prefer @self.id, then a top-level id/uuid. + $uuid = ($raw['@self']['id'] ?? $raw['id'] ?? $raw['uuid'] ?? null); + if ($uuid !== null) { + $uuid = (string) $uuid; + } + + // Body = schema properties only; empty strings → null. + $body = array_intersect_key($raw, $propertyKeys); + foreach ($body as $key => $value) { + if ($value === '') { + $body[$key] = null; + } + } + }//end if + + $resolved[] = [ + 'uuid' => $uuid, + 'body' => $body, + 'name' => ($raw['name'] ?? $uuid), + ]; + }//end foreach + + // Phase 2a: dry-run — validate every resolved row, save NOTHING. + if ($dryRun === true) { + $summary = $this->buildDryRunSummary( + summary: $summary, + objects: array_column($resolved, 'body'), + schema: $schema, + startTime: $startTime + ); + + $summary['schema'] = [ + 'id' => $schema->getId(), + 'title' => $schema->getTitle(), + 'slug' => $schema->getSlug(), + ]; + + return [ + 'JSON' => $summary, + 'importJobId' => $importJobId, + ]; + } + + // Phase 2b: persist each resolved row through the same single-object + // saveObject() path the REST create/update uses (unchanged from the + // non-pack behaviour — the bulk saveObjects() path silently skips + // dedicated-table schemas, see the class-level note above). + foreach ($resolved as $row) { + try { + $saved = $this->objectService->saveObject( + object: $row['body'], + register: $register, + schema: $schema, + uuid: $row['uuid'], + _rbac: $_rbac, + _multitenancy: $_multitenancy, + currentUser: $currentUser + ); + + if ($row['uuid'] !== null) { + $summary['updated'][] = $saved->getUuid(); + } else { + $summary['created'][] = $saved->getUuid(); + } + } catch (\Throwable $e) { + $summary['errors'][] = [ + 'object' => $row['name'], + 'error' => $e->getMessage(), + 'type' => get_class($e), + ]; + }//end try + }//end foreach + + $summary['schema'] = [ + 'id' => $schema->getId(), + 'title' => $schema->getTitle(), + 'slug' => $schema->getSlug(), + ]; + + $finalResult = [ + 'JSON' => $summary, + 'importJobId' => $importJobId, + ]; + + return $finalResult; + } finally { + $this->auditTrailMapper->setRequestImportJobId(importJobId: null); + }//end try + }//end importFromJson() + /** * Process spreadsheet with multiple schemas using batch saving for better performance * @@ -557,7 +800,7 @@ public function importFromCsv( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control import behavior options * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/object-lifecycle/spec.md */ private function processMultiSchemaSpreadsheetAsync( Spreadsheet $spreadsheet, @@ -640,9 +883,6 @@ private function processMultiSchemaSpreadsheetAsync( $summary[$schemaSlug] = array_merge($summary[$schemaSlug], $sheetSummary); }//end foreach - // Schedule SOLR warmup job after successful multi-schema import. - $this->scheduleSmartSolrWarmup(importSummary: $summary); - return $summary; }//end processMultiSchemaSpreadsheetAsync() @@ -659,6 +899,92 @@ private function processMultiSchemaSpreadsheetAsync( * @psalm-return array, unchanged: array, errors: array}> */ + /** + * Flush accumulated notify_push collection events after a bulk import. + * + * Lazily resolves notify_push's IQueue from the container and emits one + * broadcast `or-collection-{register-slug}-{schema-slug}` event per + * (register, schema) pair accumulated while batch mode was active. + * + * Soft-fails: when notify_push is not installed (IQueue not resolvable) + * this is a silent no-op — nothing was accumulated in that case, so we + * return before even touching the container. A resolution failure with + * pending events (partial install / config drift) logs at most one + * DEBUG entry and never interrupts the import. + * + * MUST be called before setBatchMode(false), which clears the accumulator. + * + * @return void + * + * @SuppressWarnings(PHPMD.StaticAccess) NotifyPushListener batch API is static by design (accessible without DI from import context) + * + * @spec openspec/specs/realtime-updates/spec.md + */ + private function flushNotifyPushBatch(): void + { + if (NotifyPushListener::hasBatchedCollections() === false) { + return; + } + + try { + $queue = $this->container->get('OCA\NotifyPush\Queue\IQueue'); + } catch (\Throwable $e) { + // Notify_push unavailable — soft-fail with a single DEBUG log. + $this->logger->debug( + message: '[ImportService] notify_push IQueue not available; skipping batch flush', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + return; + } + + NotifyPushListener::flushBatch(queue: $queue); + + }//end flushNotifyPushBatch() + + /** + * Queue a notify_push collection hint derived from the import's own context. + * + * Bulk saves run with lifecycle events DISABLED by default (`events=false` + * everywhere in the import call chain), so NotifyPushListener::handle() + * never fires and the batch accumulator would stay empty. The import knows + * exactly which (register, schema) collection it just changed — queue the + * pair directly from the entities' slugs. Deduplicated with any + * event-driven accumulation when events ARE enabled. Soft-fails on any + * slug-resolution error (a missed hint must never break the import). + * + * @param Register $register The register the import saved into. + * @param Schema $schema The schema the import saved into. + * + * @return void + * + * @SuppressWarnings(PHPMD.StaticAccess) NotifyPushListener batch API is static by design (accessible without DI from import context) + * + * @spec openspec/specs/realtime-updates/spec.md + */ + private function queueNotifyPushCollectionHint(Register $register, Schema $schema): void + { + try { + $registerSlug = (string) ($register->getSlug() ?? ''); + $schemaSlug = (string) ($schema->getSlug() ?? ''); + NotifyPushListener::addBatchedCollection(registerSlug: $registerSlug, schemaSlug: $schemaSlug); + } catch (\Throwable $e) { + // Slug not resolvable — skip the hint, never break the import. + $this->logger->debug( + message: '[ImportService] Could not queue notify_push collection hint', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + } + + }//end queueNotifyPushCollectionHint() + /** * Process a single spreadsheet sheet using batch saving for better performance * @@ -681,7 +1007,7 @@ private function processMultiSchemaSpreadsheetAsync( * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Batch processing consolidates related operations for performance * @SuppressWarnings(PHPMD.StaticAccess) NotifyPushListener::setBatchMode/flushBatch are NC idiom static calls * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/object-lifecycle/spec.md */ private function processSpreadsheetBatch( Spreadsheet $spreadsheet, @@ -777,15 +1103,15 @@ private function processSpreadsheetBatch( ); } - // @todo add-live-updates/task-6: Wrap with NotifyPushListener::setBatchMode(true) and - // flushBatch($queue, $permissionHandler) once IQueue and PermissionHandler are injected - // into ImportService. Pattern: - // NotifyPushListener::setBatchMode(true); - // try { ... saveObjects ... } finally { - // NotifyPushListener::flushBatch($queue, $permissionHandler); - // NotifyPushListener::setBatchMode(false); - // } - \OCA\OpenRegister\Listener\NotifyPushListener::setBatchMode(true); + // Suppress per-object notify_push events during the bulk save; on + // completion (success OR failure — partial saves still happened) + // flush one deduplicated collection event per (register, schema) + // pair so connected clients refetch their lists. The hint is + // derived from the save RESULT, not from lifecycle events: bulk + // saves run with events disabled by default, so the listener + // never accumulates on its own. + NotifyPushListener::setBatchMode(true); + $saveResult = null; try { $saveResult = $this->objectService->saveObjects( objects: $allObjects, @@ -798,10 +1124,18 @@ private function processSpreadsheetBatch( enrich: $enrich ); } finally { - // Cannot call flushBatch here without IQueue — batch mode is cleared - // so individual events are re-enabled for subsequent calls. - \OCA\OpenRegister\Listener\NotifyPushListener::setBatchMode(false); - } + // Null result = save threw; partial saves may have landed, so hint conservatively. + $collectionChanged = $saveResult === null + || empty($saveResult['saved'] ?? []) === false + || empty($saveResult['updated'] ?? []) === false; + if ($collectionChanged === true) { + $this->queueNotifyPushCollectionHint(register: $register, schema: $schema); + } + + // Flush BEFORE disabling batch mode — setBatchMode(false) clears the accumulator. + $this->flushNotifyPushBatch(); + NotifyPushListener::setBatchMode(false); + }//end try // Use the structured return from saveObjects with smart deduplication. // SaveObjects returns ObjectEntity->jsonSerialize() arrays where UUID is in @self.id. @@ -862,16 +1196,20 @@ private function processSpreadsheetBatch( * @param bool $publish DEPRECATED: No-op. Publish metadata removed. * @param IUser|null $currentUser The current user performing the import * @param bool $enrich Whether to enrich objects with metadata + * @param array|null $pack Optional migration pack definition; each row is mapped through it first + * @param bool $dryRun When true, rows are mapped and validated but nothing is saved * * @return array CSV sheet processing results * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control import behavior options - * @SuppressWarnings(PHPMD.CyclomaticComplexity) CSV processing requires many conditional branches for data handling - * @SuppressWarnings(PHPMD.NPathComplexity) CSV processing requires many conditional row/column handling - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) CSV processing consolidates related operations for performance - * @SuppressWarnings(PHPMD.StaticAccess) NotifyPushListener::setBatchMode/flushBatch are NC idiom static calls + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control import behavior options + * @SuppressWarnings(PHPMD.CyclomaticComplexity) CSV processing requires many conditional branches for data handling + * @SuppressWarnings(PHPMD.NPathComplexity) CSV processing requires many conditional row/column handling + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) CSV processing consolidates related operations for performance + * @SuppressWarnings(PHPMD.StaticAccess) NotifyPushListener::setBatchMode/flushBatch are NC idiom static calls + * @SuppressWarnings(PHPMD.ExcessiveParameterList) pack/dryRun extend the existing signature * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-27 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/specs/migration-mapping-packs/spec.md */ private function processCsvSheet( \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, @@ -883,7 +1221,9 @@ private function processCsvSheet( bool $_multitenancy=true, bool $publish=false, ?IUser $currentUser=null, - bool $enrich=true + bool $enrich=true, + ?array $pack=null, + bool $dryRun=false ): array { $summary = [ 'found' => 0, @@ -925,6 +1265,10 @@ private function processCsvSheet( $allObjects = []; for ($row = 2; $row <= $highestRow; $row++) { + if ($pack !== null && $this->mappingEngine->isRowSkipped(pack: $pack, rowNumber: $row) === true) { + continue; + } + // NO ERROR SUPPRESSION: Let CSV row processing errors bubble up immediately! $rowData = $this->extractRowData(sheet: $sheet, columnMapping: $columnMapping, row: $row); @@ -933,6 +1277,23 @@ private function processCsvSheet( // Skip empty rows. } + // Migration pack mapping: source columns -> target schema properties, + // with transforms applied. A row with mapping errors (missing required + // source, or an unresolved lookup/reference — the literal-leak guard) + // is reported and excluded from the batch, never partially mapped. + if ($pack !== null) { + $mapped = $this->mappingEngine->mapRow(pack: $pack, sourceRow: $rowData, rowNumber: $row); + if (empty($mapped['errors']) === false) { + foreach ($mapped['errors'] as $mappingError) { + $summary['errors'][] = $this->formatMappingError(error: $mappingError); + } + + continue; + } + + $rowData = $mapped['data']; + } + // Transform row data to object format. $object = $this->transformCsvRowToObject( rowData: $rowData, @@ -948,6 +1309,16 @@ private function processCsvSheet( $summary['found'] = count($allObjects); + // Dry-run: map + validate every row, save NOTHING. This is the killer + // feature for migration quoting — an operator can see exactly what + // would happen before committing to a real import. Genuinely + // side-effect-free: ValidateObject::validateObject() only issues + // read-only lookups (schema resolution, uniqueness SELECTs), and + // ObjectService::saveObjects()/saveObject() is never called on this path. + if ($dryRun === true) { + return $this->buildDryRunSummary(summary: $summary, objects: $allObjects, schema: $schema, startTime: $startTime); + } + // NOTE: Deduplication is now handled by SaveObjects::saveObjects() (deduplicateIds=true by default). // This ensures consistent deduplication across ALL bulk save operations (CSV, Excel, API, etc.). // Call saveObjects ONCE with all objects - deduplication happens automatically. @@ -976,9 +1347,15 @@ private function processCsvSheet( ); } - // @todo add-live-updates/task-6: Wrap with NotifyPushListener batch mode once - // IQueue and PermissionHandler are injected into ImportService. - \OCA\OpenRegister\Listener\NotifyPushListener::setBatchMode(true); + // Suppress per-object notify_push events during the bulk save; on + // completion (success OR failure — partial saves still happened) + // flush one deduplicated collection event per (register, schema) + // pair so connected clients refetch their lists. The hint is + // derived from the save RESULT, not from lifecycle events: bulk + // saves run with events disabled by default, so the listener + // never accumulates on its own. + NotifyPushListener::setBatchMode(true); + $saveResult = null; try { $saveResult = $this->objectService->saveObjects( objects: $allObjects, @@ -991,8 +1368,18 @@ private function processCsvSheet( enrich: $enrich ); } finally { - \OCA\OpenRegister\Listener\NotifyPushListener::setBatchMode(false); - } + // Null result = save threw; partial saves may have landed, so hint conservatively. + $collectionChanged = $saveResult === null + || empty($saveResult['saved'] ?? []) === false + || empty($saveResult['updated'] ?? []) === false; + if ($collectionChanged === true) { + $this->queueNotifyPushCollectionHint(register: $register, schema: $schema); + } + + // Flush BEFORE disabling batch mode — setBatchMode(false) clears the accumulator. + $this->flushNotifyPushBatch(); + NotifyPushListener::setBatchMode(false); + }//end try // Use the structured return from saveObjects with smart deduplication. // SaveObjects returns ObjectEntity->jsonSerialize() arrays where UUID is in @self.id. @@ -1056,6 +1443,93 @@ private function processCsvSheet( return $summary; }//end processCsvSheet() + /** + * Format one MappingEngine row error into the shape `serializeErrorsToCsv()` + * and the controller's `summary['errors']` contract already expect + * (`row`/`field`/`error`/`type`), so migration-pack mapping failures show + * up in the same per-row error report as any other import error — + * clearly labeled with the source column and transform that failed. + * + * @param array{row: int, source: string, target: ?string, transform: ?string, message: string} $error One MappingEngine error entry. + * + * @return array{row: int, field: string, error: string, type: string, original_value: string} + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + private function formatMappingError(array $error): array + { + $label = 'source column "'.$error['source'].'"'; + if ($error['transform'] !== null) { + $label .= ' (transform: '.$error['transform'].')'; + } + + return [ + 'row' => $error['row'], + 'field' => $error['target'] ?? $error['source'], + 'error' => '[migration-pack] '.$error['message'].' — '.$label, + 'type' => 'MigrationPackMappingError', + 'original_value' => $error['source'], + ]; + }//end formatMappingError() + + /** + * Build a dry-run summary: every mapped row is validated against the + * target schema via `ValidateObject::validateObject()` (read-only — + * schema resolution + uniqueness SELECTs, no writes) and NOTHING is + * saved. `created`/`updated`/`unchanged` stay empty since no object was + * actually persisted; `rows` carries the per-row valid/invalid verdict + * migration quoting needs. + * + * @param array $summary The summary accumulated so far (found/errors/etc). + * @param array $objects The mapped+type-coerced objects that would be saved. + * @param Schema $schema The target schema to validate against. + * @param float $startTime `microtime(true)` at the start of processing, for performance metrics. + * + * @return array The dry-run summary. + * + * @spec openspec/specs/migration-mapping-packs/spec.md#dry-run + */ + private function buildDryRunSummary(array $summary, array $objects, Schema $schema, float $startTime): array + { + $summary['dryRun'] = true; + $summary['rows'] = []; + $summary['validRows'] = 0; + $summary['invalidRows'] = 0; + + foreach ($objects as $index => $object) { + $result = $this->validateObjectHandler->validateObject(object: $object, schema: $schema); + $isValid = $result->isValid(); + + if ($isValid === true) { + $summary['validRows']++; + } else { + $summary['invalidRows']++; + } + + $rowErrors = []; + if ($isValid === false) { + $rowErrors = [$this->validateObjectHandler->generateErrorMessage(result: $result)]; + } + + $summary['rows'][] = [ + 'index' => $index, + 'valid' => $isValid, + 'errors' => $rowErrors, + 'preview' => $object, + ]; + }//end foreach + + $totalTime = microtime(true) - $startTime; + $summary['performance'] = [ + 'totalTime' => round($totalTime, 3), + 'totalTimeMs' => round($totalTime * 1000, 2), + 'totalProcessed' => count($objects), + 'totalFound' => $summary['found'], + ]; + + return $summary; + }//end buildDryRunSummary() + /** * Transform CSV row data to object format for batch saving * @@ -1201,6 +1675,11 @@ private function transformDateTimeValue(string $value): string if (preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/', $value) === 1) { try { $dateTime = new DateTime($value); + // BUG-SVC-5: normalise to UTC before stripping the offset, so + // an offset-bearing timestamp persists the correct instant + // instead of its local wall-clock reading (e.g. +05:00 input + // must shift back five hours, not just drop the offset). + $dateTime->setTimezone(new \DateTimeZone('UTC')); return $dateTime->format(format: 'Y-m-d H:i:s'); } catch (Exception $e) { // Fallback to original value if parsing fails. @@ -1453,7 +1932,7 @@ private function getSchemaBySlug(string $slug): Schema * @phpstan-return array * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-10 + * @spec openspec/specs/data-import-export/spec.md */ private function transformObjectBySchema(array $objectData, Schema $schema): array { @@ -1493,6 +1972,7 @@ private function transformObjectBySchema(array $objectData, Schema $schema): arr * @return mixed The transformed value * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Type transformation switch requires branches for each data type + * @SuppressWarnings(PHPMD.StaticAccess) ObjectHandling::relates() is the established enum-style helper idiom */ private function transformValueByType($value, array $propertyDef) { @@ -1519,7 +1999,7 @@ private function transformValueByType($value, array $propertyDef) case 'object': // Check if this is a related-object that should store UUID strings directly. if (($propertyDef['objectConfiguration']['handling'] ?? null) !== null - && ($propertyDef['objectConfiguration']['handling'] === 'related-object') === true + && ObjectHandling::relates($propertyDef['objectConfiguration']['handling']) === true ) { // For related objects, store UUID strings directly instead of wrapping in objects. return (string) $value; @@ -1681,196 +2161,6 @@ private function validateObjectProperties(array $objectData, string $_schemaId): } }//end validateObjectProperties() - /** - * Schedule SOLR warmup job after successful import - * - * This method schedules a one-time background job to warm up the SOLR index - * after import operations complete. The warmup runs in the background to avoid - * impacting import performance while ensuring optimal search performance. - * - * @param array $importSummary Summary of the import operation - * @param int $delaySeconds Delay before running the warmup (default: 30 seconds) - * @param string $mode Warmup mode - 'serial', 'parallel', or 'hyper' (default: 'serial') - * @param int $maxObjects Maximum objects to index during warmup (default: 5000) - * - * @return bool True if job was scheduled successfully - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-2/tasks.md#task-3 - */ - public function scheduleSolrWarmup( - array $importSummary, - int $delaySeconds=30, - string $mode='serial', - int $maxObjects=5000 - ): bool { - try { - // Calculate total objects imported across all sheets. - $totalImported = $this->calculateTotalImported(importSummary: $importSummary); - - if ($totalImported === 0) { - $this->logger->info( - message: '[ImportService] Skipping SOLR warmup - no objects were imported', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - ] - ); - return false; - } - - // Prepare job arguments. - $jobArguments = [ - 'maxObjects' => $maxObjects, - 'mode' => $mode, - // Keep it fast for post-import warmup. - 'triggeredBy' => 'import_completion', - 'importSummary' => [ - 'totalImported' => $totalImported, - 'sheetsProcessed' => count($importSummary), - 'importTimestamp' => date('c'), - ], - ]; - - // Schedule the job with delay. - $executeAfter = time() + $delaySeconds; - $this->jobList->scheduleAfter(SolrWarmupJob::class, $executeAfter, $jobArguments); - - $this->logger->info( - message: '[ImportService] 🔥 SOLR Warmup Job Scheduled', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'total_imported' => $totalImported, - 'warmup_mode' => $mode, - 'max_objects' => $maxObjects, - 'delay_seconds' => $delaySeconds, - 'execute_after' => date('Y-m-d H:i:s', $executeAfter), - 'triggered_by' => 'import_completion', - ] - ); - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[ImportService] Failed to schedule SOLR warmup job', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'import_summary' => $importSummary, - ] - ); - - return false; - }//end try - }//end scheduleSolrWarmup() - - /** - * Calculate total objects imported from import summary - * - * @param array $importSummary Import summary from Excel/CSV import - * - * @return int Total number of objects imported - * - * @psalm-return int<0, max> - */ - private function calculateTotalImported(array $importSummary): int - { - $total = 0; - - foreach ($importSummary as $sheetSummary) { - if (is_array($sheetSummary) === true) { - $created = count($sheetSummary['created'] ?? []); - $updated = count($sheetSummary['updated'] ?? []); - $total += $created + $updated; - } - } - - return $total; - }//end calculateTotalImported() - - /** - * Determine optimal warmup mode based on import size - * - * @param int $totalImported Total objects imported - * - * @return string Recommended warmup mode - * - * @psalm-return 'balanced'|'fast'|'safe' - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-2/tasks.md#task-3 - */ - public function getRecommendedWarmupMode(int $totalImported): string - { - if ($totalImported > 10000) { - // Fast mode for large imports. - return 'fast'; - } - - if ($totalImported > 1000) { - // Balanced mode for medium imports. - return 'balanced'; - } - - // Safe mode for small imports. - return 'safe'; - }//end getRecommendedWarmupMode() - - /** - * Schedule SOLR warmup with smart configuration based on import results - * - * This is a convenience method that automatically determines the best warmup - * configuration based on the import results. - * - * @param array $importSummary Import summary - * @param bool $immediate Whether to run immediately (default: false, 30s delay) - * - * @return bool True if job was scheduled successfully - * - * @psalm-suppress PossiblyUnusedReturnValue - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Immediate flag controls scheduling timing - * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-2/tasks.md#task-3 - */ - public function scheduleSmartSolrWarmup(array $importSummary, bool $immediate=false): bool - { - $totalImported = $this->calculateTotalImported(importSummary: $importSummary); - - if ($totalImported === 0) { - return false; - } - - // Smart configuration based on import size. - $mode = $this->getRecommendedWarmupMode(totalImported: $totalImported); - // Index up to 2x imported objects, max 15k. - $maxObjects = min($totalImported * 2, 15000); - $delay = 30; - if ($immediate === true) { - $delay = 0; - } - - // 30 second delay by default. - $this->logger->info( - message: '[ImportService] Scheduling smart SOLR warmup', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'total_imported' => $totalImported, - 'recommended_mode' => $mode, - 'max_objects' => $maxObjects, - 'delay_seconds' => $delay, - ] - ); - - return $this->scheduleSolrWarmup( - importSummary: $importSummary, - delaySeconds: $delay, - mode: $mode, - maxObjects: $maxObjects - ); - }//end scheduleSmartSolrWarmup() - /** * Serialize per-row import errors to a UTF-8 CSV blob with BOM. * diff --git a/lib/Service/Index/Backends/Elasticsearch/ElasticsearchDocumentIndexer.php b/lib/Service/Index/Backends/Elasticsearch/ElasticsearchDocumentIndexer.php deleted file mode 100644 index 4d6831b9c3..0000000000 --- a/lib/Service/Index/Backends/Elasticsearch/ElasticsearchDocumentIndexer.php +++ /dev/null @@ -1,365 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Elasticsearch; - -use Exception; -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Service\Index\DocumentBuilder; -use Psr\Log\LoggerInterface; - -/** - * ElasticsearchDocumentIndexer - * - * Handles Elasticsearch document indexing operations. - */ -class ElasticsearchDocumentIndexer -{ - - /** - * Elasticsearch HTTP client for making requests - * - * @var ElasticsearchHttpClient - */ - private readonly ElasticsearchHttpClient $httpClient; - - /** - * Elasticsearch index manager for index operations - * - * @var ElasticsearchIndexManager - */ - private readonly ElasticsearchIndexManager $indexManager; - - /** - * Document builder for preparing documents - * - * @var DocumentBuilder - */ - private readonly DocumentBuilder $documentBuilder; - - /** - * PSR-3 logger instance - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param ElasticsearchHttpClient $httpClient HTTP client - * @param ElasticsearchIndexManager $indexManager Index manager - * @param DocumentBuilder $documentBuilder Document builder - * @param LoggerInterface $logger Logger - */ - public function __construct( - ElasticsearchHttpClient $httpClient, - ElasticsearchIndexManager $indexManager, - DocumentBuilder $documentBuilder, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->indexManager = $indexManager; - $this->documentBuilder = $documentBuilder; - $this->logger = $logger; - }//end __construct() - - /** - * Index a single object. - * - * @param ObjectEntity $object Object to index - * @param bool $refresh Whether to refresh immediately - * - * @return bool True if successful - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec exclude thin delegation to ElasticsearchHttpClient — builds doc and PUTs it to ES - */ - public function indexObject(ObjectEntity $object, bool $refresh=false): bool - { - try { - $index = $this->indexManager->getActiveIndexName(); - - // Ensure index exists. - $this->indexManager->ensureIndex($index); - - // Build document. - $document = $this->documentBuilder->createDocument($object); - - // Index document. - $url = $this->httpClient->buildBaseUrl().'/'.$index.'/_doc/'.$document['id']; - $response = $this->httpClient->put($url, $document); - - $success = isset($response['result']) && in_array($response['result'], ['created', 'updated']); - - if ($success === true) { - $this->logger->info( - message: '[ElasticsearchDocumentIndexer] Object indexed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $object->getId(), - 'result' => $response['result'] ?? 'unknown', - ] - ); - - // Refresh index if requested. - if ($refresh === true) { - $this->indexManager->refreshIndex($index); - } - } - - return $success; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchDocumentIndexer] Failed to index object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $object->getId(), - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end indexObject() - - /** - * Index multiple objects in bulk. - * - * @param array $objects Objects to index - * @param bool $refresh Whether to refresh after bulk - * - * @return (bool|int|string)[] - * - * @psalm-return array{success: bool, indexed: int<0, max>, failed: int, error?: string} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Complex multi-step bulk indexing process - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Bulk indexing requires handling multiple object and error scenarios - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple paths for document building and indexing results - * - * @spec exclude thin delegation to ElasticsearchHttpClient — builds ndjson bulk body and POSTs it - */ - public function bulkIndexObjects(array $objects, bool $refresh=false): array - { - $successCount = 0; - $failureCount = 0; - $index = $this->indexManager->getActiveIndexName(); - - // Ensure index exists. - $this->indexManager->ensureIndex($index); - - // Build bulk request body. - $bulkBody = []; - foreach ($objects as $object) { - if (($object instanceof ObjectEntity) === false) { - $failureCount++; - continue; - } - - try { - $document = $this->documentBuilder->createDocument($object); - - // Index action. - $bulkBody[] = json_encode( - [ - 'index' => [ - '_index' => $index, - '_id' => $document['id'], - ], - ] - ); - - // Document data. - $bulkBody[] = json_encode($document); - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchDocumentIndexer] Failed to build document', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $object->getId(), - 'error' => $e->getMessage(), - ] - ); - $failureCount++; - }//end try - }//end foreach - - if (empty($bulkBody) === true) { - return [ - 'success' => false, - 'indexed' => 0, - 'failed' => $failureCount, - 'error' => 'No documents to index', - ]; - } - - try { - $url = $this->httpClient->buildBaseUrl().'/_bulk'; - - // Send bulk request with newline-delimited JSON. - $bulkData = implode("\n", $bulkBody)."\n"; - $response = $this->httpClient->postRaw($url, $bulkData); - - if (isset($response['items']) === true) { - foreach ($response['items'] as $item) { - if (isset($item['index']['status']) === true && $item['index']['status'] < 300) { - $successCount++; - continue; - } - - $failureCount++; - } - } - - $this->logger->info( - message: '[ElasticsearchDocumentIndexer] Bulk indexing completed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'success' => $successCount, - 'failed' => $failureCount, - ] - ); - - // Refresh index if requested. - if ($refresh === true) { - $this->indexManager->refreshIndex($index); - } - - return [ - 'success' => true, - 'indexed' => $successCount, - 'failed' => $failureCount, - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchDocumentIndexer] Bulk indexing failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return [ - 'success' => false, - 'indexed' => $successCount, - 'failed' => count($objects) - $successCount, - 'error' => $e->getMessage(), - ]; - }//end try - }//end bulkIndexObjects() - - /** - * Delete an object from the index. - * - * @param string|int $objectId Object ID to delete - * @param bool $refresh Whether to refresh immediately - * - * @return bool True if successful - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec exclude thin delegation to ElasticsearchHttpClient — DELETEs the document by id - */ - public function deleteObject(string|int $objectId, bool $refresh=false): bool - { - try { - $index = $this->indexManager->getActiveIndexName(); - $url = $this->httpClient->buildBaseUrl().'/'.$index.'/_doc/'.$objectId; - - $response = $this->httpClient->delete($url); - - $success = isset($response['result']) && $response['result'] === 'deleted'; - - if ($success === true) { - $this->logger->info( - message: '[ElasticsearchDocumentIndexer] Object deleted', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $objectId, - ] - ); - - // Refresh index if requested. - if ($refresh === true) { - $this->indexManager->refreshIndex($index); - } - } - - return $success; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchDocumentIndexer] Failed to delete object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $objectId, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end deleteObject() - - /** - * Clear all documents from index. - * - * @return bool True if successful - * - * @spec exclude thin delegation to ElasticsearchIndexManager — delete-and-recreate index - */ - public function clearIndex(): bool - { - try { - $index = $this->indexManager->getActiveIndexName(); - - // Delete and recreate index. - $this->indexManager->deleteIndex($index); - $this->indexManager->createIndex($index); - - $this->logger->info( - message: '[ElasticsearchDocumentIndexer] Index cleared', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $index, - ] - ); - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchDocumentIndexer] Failed to clear index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end clearIndex() -}//end class diff --git a/lib/Service/Index/Backends/Elasticsearch/ElasticsearchHttpClient.php b/lib/Service/Index/Backends/Elasticsearch/ElasticsearchHttpClient.php deleted file mode 100644 index 24e05c9b5c..0000000000 --- a/lib/Service/Index/Backends/Elasticsearch/ElasticsearchHttpClient.php +++ /dev/null @@ -1,370 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Elasticsearch; - -use Exception; -use GuzzleHttp\Client as GuzzleClient; -use OCA\OpenRegister\Service\SettingsService; -use Psr\Log\LoggerInterface; - -/** - * ElasticsearchHttpClient - * - * Manages HTTP client and URL building for Elasticsearch operations. - */ -class ElasticsearchHttpClient -{ - - /** - * Guzzle HTTP client for making requests - * - * @var GuzzleClient - */ - private GuzzleClient $httpClient; - - /** - * Elasticsearch configuration settings - * - * @var array - */ - private array $config = []; - - /** - * Settings service for retrieving configuration - * - * @var SettingsService - */ - private readonly SettingsService $settingsService; - - /** - * PSR-3 logger instance - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param SettingsService $settingsService Settings service - * @param LoggerInterface $logger Logger - */ - public function __construct( - SettingsService $settingsService, - LoggerInterface $logger - ) { - $this->settingsService = $settingsService; - $this->logger = $logger; - - $this->initializeConfig(); - $this->initializeHttpClient(); - }//end __construct() - - /** - * Initialize Elasticsearch configuration from settings. - * - * @return void - */ - private function initializeConfig(): void - { - // For now, use hardcoded config since we don't have ES settings in SettingsService yet. - $this->config = [ - 'enabled' => true, - 'host' => 'openregister-elasticsearch', - 'port' => 9200, - 'scheme' => 'http', - 'index' => 'openregister', - 'timeout' => 30, - ]; - }//end initializeConfig() - - /** - * Initialize HTTP client for Elasticsearch requests. - * - * @return void - */ - private function initializeHttpClient(): void - { - $this->httpClient = new GuzzleClient( - [ - 'timeout' => $this->config['timeout'], - 'connect_timeout' => 5, - 'http_errors' => false, - 'verify' => false, - 'headers' => [ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ], - ] - ); - }//end initializeHttpClient() - - /** - * Build Elasticsearch base URL. - * - * @return string Base URL - * - * @spec exclude boilerplate URL builder — scheme/host/port concatenation - */ - public function buildBaseUrl(): string - { - return sprintf( - '%s://%s:%d', - $this->config['scheme'], - $this->config['host'], - $this->config['port'] - ); - }//end buildBaseUrl() - - /** - * Build endpoint URL for a specific index. - * - * @param string $index The index name. - * - * @return string Endpoint URL - */ - public function getEndpointUrl(string $index): string - { - return $this->buildBaseUrl().'/'.$index; - }//end getEndpointUrl() - - /** - * Execute GET request. - * - * @param string $url The URL to request. - * - * @return array Response data - * - * @spec exclude thin Guzzle GET wrapper — request/json-decode/log-and-rethrow - */ - public function get(string $url): array - { - try { - $response = $this->httpClient->get($url); - $body = (string) $response->getBody(); - $decoded = json_decode($body, true); - if ($decoded === null || $decoded === false) { - return []; - } - - return $decoded; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchHttpClient] GET failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'url' => $url, - 'error' => $e->getMessage(), - ] - ); - throw $e; - }//end try - }//end get() - - /** - * Execute POST request. - * - * @param string $url The URL to request. - * @param array $data The data to send as JSON. - * - * @return array Response data - * - * @spec exclude thin Guzzle POST wrapper — request/json-decode/log-and-rethrow - */ - public function post(string $url, array $data): array - { - try { - $response = $this->httpClient->post( - $url, - [ - 'json' => $data, - ] - ); - $body = (string) $response->getBody(); - $decoded = json_decode($body, true); - if ($decoded === null || $decoded === false) { - return []; - } - - return $decoded; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchHttpClient] POST failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'url' => $url, - 'error' => $e->getMessage(), - ] - ); - throw $e; - }//end try - }//end post() - - /** - * Execute POST request with raw body (for bulk API). - * - * @param string $url The URL to request. - * @param string $data The raw data to send. - * - * @return array Response data - * - * @spec exclude thin Guzzle POST wrapper — ndjson bulk body/json-decode/log-and-rethrow - */ - public function postRaw(string $url, string $data): array - { - try { - $response = $this->httpClient->post( - $url, - [ - 'body' => $data, - 'headers' => [ - 'Content-Type' => 'application/x-ndjson', - ], - ] - ); - $body = (string) $response->getBody(); - $decoded = json_decode($body, true); - if ($decoded === null || $decoded === false) { - return []; - } - - return $decoded; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchHttpClient] POST (raw) failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'url' => $url, - 'error' => $e->getMessage(), - ] - ); - throw $e; - }//end try - }//end postRaw() - - /** - * Execute PUT request. - * - * @param string $url The URL to request. - * @param array $data The data to send as JSON. - * - * @return array Response data - * - * @spec exclude thin Guzzle PUT wrapper — request/json-decode/log-and-rethrow - */ - public function put(string $url, array $data): array - { - try { - $response = $this->httpClient->put( - $url, - [ - 'json' => $data, - ] - ); - $body = (string) $response->getBody(); - $decoded = json_decode($body, true); - if ($decoded === null || $decoded === false) { - return []; - } - - return $decoded; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchHttpClient] PUT failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'url' => $url, - 'error' => $e->getMessage(), - ] - ); - throw $e; - }//end try - }//end put() - - /** - * Execute DELETE request. - * - * @param string $url The URL to request. - * - * @return array Response data - * - * @spec exclude thin Guzzle DELETE wrapper — request/json-decode/log-and-rethrow - */ - public function delete(string $url): array - { - try { - $response = $this->httpClient->delete($url); - $body = (string) $response->getBody(); - $decoded = json_decode($body, true); - if ($decoded === null || $decoded === false) { - return []; - } - - return $decoded; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchHttpClient] DELETE failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'url' => $url, - 'error' => $e->getMessage(), - ] - ); - throw $e; - }//end try - }//end delete() - - /** - * Get HTTP client instance. - * - * @return GuzzleClient HTTP client instance - */ - public function getHttpClient(): GuzzleClient - { - return $this->httpClient; - }//end getHttpClient() - - /** - * Get Elasticsearch configuration. - * - * @return array Configuration array - * - * @psalm-return array - */ - public function getConfig(): array - { - return $this->config; - }//end getConfig() - - /** - * Check if Elasticsearch is configured. - * - * @return bool True if configured - */ - public function isConfigured(): bool - { - return $this->config['enabled'] ?? false; - }//end isConfigured() -}//end class diff --git a/lib/Service/Index/Backends/Elasticsearch/ElasticsearchIndexManager.php b/lib/Service/Index/Backends/Elasticsearch/ElasticsearchIndexManager.php deleted file mode 100644 index cbfa007b3d..0000000000 --- a/lib/Service/Index/Backends/Elasticsearch/ElasticsearchIndexManager.php +++ /dev/null @@ -1,281 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Elasticsearch; - -use Exception; -use Psr\Log\LoggerInterface; - -/** - * ElasticsearchIndexManager - * - * Handles Elasticsearch index management operations. - */ -class ElasticsearchIndexManager -{ - - /** - * Elasticsearch HTTP client for making requests - * - * @var ElasticsearchHttpClient - */ - private readonly ElasticsearchHttpClient $httpClient; - - /** - * PSR-3 logger instance - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Active Elasticsearch index name - * - * @var string - */ - private string $activeIndex = 'openregister'; - - /** - * Constructor - * - * @param ElasticsearchHttpClient $httpClient HTTP client - * @param LoggerInterface $logger Logger - */ - public function __construct( - ElasticsearchHttpClient $httpClient, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->logger = $logger; - }//end __construct() - - /** - * Check if index exists. - * - * @param string $indexName The index name to check. - * - * @return bool True if index exists - * - * @spec exclude thin delegation to ElasticsearchHttpClient — GET index, no error means exists - */ - public function indexExists(string $indexName): bool - { - try { - $url = $this->httpClient->buildBaseUrl().'/'.$indexName; - $response = $this->httpClient->get($url); - - return isset($response['error']) === false; - } catch (Exception $e) { - return false; - } - }//end indexExists() - - /** - * Create index with mapping. - * - * @param string $indexName The index name to create. - * @param array $mapping Index mapping configuration (default: empty array). - * - * @return bool True on success - * - * @spec exclude thin delegation to ElasticsearchHttpClient — PUTs index settings/mappings - */ - public function createIndex(string $indexName, array $mapping=[]): bool - { - try { - $url = $this->httpClient->buildBaseUrl().'/'.$indexName; - - $settings = [ - 'settings' => [ - 'number_of_shards' => 1, - 'number_of_replicas' => 0, - ], - ]; - - if (empty($mapping) === false) { - $settings['mappings'] = $mapping; - } - - $response = $this->httpClient->put($url, $settings); - - $success = isset($response['acknowledged']) && $response['acknowledged'] === true; - - if ($success === true) { - $this->logger->info( - message: '[ElasticsearchIndexManager] Index created', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $indexName, - ] - ); - } - - return $success; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchIndexManager] Failed to create index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $indexName, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end createIndex() - - /** - * Delete index. - * - * @param string $indexName The index name to delete. - * - * @return bool True on success - * - * @spec exclude thin delegation to ElasticsearchHttpClient — DELETEs the index - */ - public function deleteIndex(string $indexName): bool - { - try { - $url = $this->httpClient->buildBaseUrl().'/'.$indexName; - $response = $this->httpClient->delete($url); - - $success = isset($response['acknowledged']) && $response['acknowledged'] === true; - - if ($success === true) { - $this->logger->info( - message: '[ElasticsearchIndexManager] Index deleted', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $indexName, - ] - ); - } - - return $success; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchIndexManager] Failed to delete index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $indexName, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end deleteIndex() - - /** - * Ensure index exists, create if not. - * - * @param string $indexName The index name to ensure exists. - * - * @return bool True on success - * - * @spec exclude thin helper — checks indexExists then createIndex - */ - public function ensureIndex(string $indexName): bool - { - if ($this->indexExists(indexName: $indexName) === true) { - $this->logger->debug( - message: '[ElasticsearchIndexManager] Index already exists', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $indexName, - ] - ); - return true; - } - - return $this->createIndex(indexName: $indexName); - }//end ensureIndex() - - /** - * Get active index name. - * - * @return string Active index name - */ - public function getActiveIndexName(): string - { - return $this->activeIndex; - }//end getActiveIndexName() - - /** - * Get index stats. - * - * @param string $indexName Index name - * - * @return array Index statistics - * - * @spec exclude thin delegation to ElasticsearchHttpClient — GETs /_stats - */ - public function getIndexStats(string $indexName): array - { - try { - $url = $this->httpClient->buildBaseUrl().'/'.$indexName.'/_stats'; - return $this->httpClient->get($url); - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchIndexManager] Failed to get index stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $indexName, - 'error' => $e->getMessage(), - ] - ); - return []; - } - }//end getIndexStats() - - /** - * Refresh index to make documents searchable. - * - * @param string $indexName Index name - * - * @return bool True on success - * - * @spec exclude thin delegation to ElasticsearchHttpClient — POSTs /_refresh - */ - public function refreshIndex(string $indexName): bool - { - try { - $url = $this->httpClient->buildBaseUrl().'/'.$indexName.'/_refresh'; - $response = $this->httpClient->post($url, []); - - return isset($response['error']) === false; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchIndexManager] Failed to refresh index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $indexName, - 'error' => $e->getMessage(), - ] - ); - return false; - } - }//end refreshIndex() -}//end class diff --git a/lib/Service/Index/Backends/Elasticsearch/ElasticsearchQueryExecutor.php b/lib/Service/Index/Backends/Elasticsearch/ElasticsearchQueryExecutor.php deleted file mode 100644 index d773438c15..0000000000 --- a/lib/Service/Index/Backends/Elasticsearch/ElasticsearchQueryExecutor.php +++ /dev/null @@ -1,196 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Elasticsearch; - -use Exception; -use stdClass; -use Psr\Log\LoggerInterface; - -/** - * ElasticsearchQueryExecutor - * - * Handles Elasticsearch query execution. - */ -class ElasticsearchQueryExecutor -{ - - /** - * Elasticsearch HTTP client for making requests - * - * @var ElasticsearchHttpClient - */ - private readonly ElasticsearchHttpClient $httpClient; - - /** - * Elasticsearch index manager for index operations - * - * @var ElasticsearchIndexManager - */ - private readonly ElasticsearchIndexManager $indexManager; - - /** - * PSR-3 logger instance - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param ElasticsearchHttpClient $httpClient HTTP client - * @param ElasticsearchIndexManager $indexManager Index manager - * @param LoggerInterface $logger Logger - */ - public function __construct( - ElasticsearchHttpClient $httpClient, - ElasticsearchIndexManager $indexManager, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->indexManager = $indexManager; - $this->logger = $logger; - }//end __construct() - - /** - * Execute a search query. - * - * @param array $query Query parameters - * - * @return array Search results - * - * @spec exclude thin delegation to ElasticsearchHttpClient — builds ES query DSL and POSTs /_search - */ - public function search(array $query): array - { - $index = $this->indexManager->getActiveIndexName(); - - try { - // Build Elasticsearch query. - $esQuery = $this->buildElasticsearchQuery(params: $query); - - $url = $this->httpClient->buildBaseUrl().'/'.$index.'/_search'; - $result = $this->httpClient->post($url, $esQuery); - - $this->logger->debug( - message: '[ElasticsearchQueryExecutor] Search executed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'index' => $index, - 'hits' => $result['hits']['total']['value'] ?? 0, - ] - ); - - return $result; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchQueryExecutor] Search failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'hits' => [ - 'total' => ['value' => 0], - 'hits' => [], - ], - ]; - }//end try - }//end search() - - /** - * Build Elasticsearch query from simple query parameters. - * - * @param array $params Query parameters - * - * @return (((mixed|string[])[]|stdClass)[]|int)[] Elasticsearch query DSL - * - * @psalm-return array{query: array{match_all?: stdClass, - * multi_match?: array{query: mixed, fields: list{'*'}, - * type: 'best_fields'}}, from: int, size: int} - */ - private function buildElasticsearchQuery(array $params): array - { - $query = [ - 'query' => [ - 'match_all' => new stdClass(), - // Empty object. - ], - 'from' => 0, - 'size' => 10, - ]; - - // Handle search text. - if (isset($params['_search']) === true && $params['_search'] !== '*:*') { - $query['query'] = [ - 'multi_match' => [ - 'query' => $params['_search'], - 'fields' => ['*'], - 'type' => 'best_fields', - ], - ]; - } - - // Handle pagination. - if (isset($params['_limit']) === true) { - $query['size'] = (int) $params['_limit']; - } - - if (isset($params['_page']) === true) { - $page = (int) $params['_page']; - $query['from'] = ($page - 1) * $query['size']; - } - - return $query; - }//end buildElasticsearchQuery() - - /** - * Get document count. - * - * @return int Number of documents - * - * @spec exclude thin delegation to ElasticsearchHttpClient — GETs /_count - */ - public function getDocumentCount(): int - { - $index = $this->indexManager->getActiveIndexName(); - - try { - $url = $this->httpClient->buildBaseUrl().'/'.$index.'/_count'; - $result = $this->httpClient->get($url); - - return $result['count'] ?? 0; - } catch (Exception $e) { - $this->logger->error( - message: '[ElasticsearchQueryExecutor] Failed to get document count', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return 0; - } - }//end getDocumentCount() -}//end class diff --git a/lib/Service/Index/Backends/ElasticsearchBackend.php b/lib/Service/Index/Backends/ElasticsearchBackend.php deleted file mode 100644 index 4f7161fa88..0000000000 --- a/lib/Service/Index/Backends/ElasticsearchBackend.php +++ /dev/null @@ -1,634 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-88 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends; - -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Service\Index\SearchBackendInterface; -use OCA\OpenRegister\Service\Index\Backends\Elasticsearch\ElasticsearchHttpClient; -use OCA\OpenRegister\Service\Index\Backends\Elasticsearch\ElasticsearchIndexManager; -use OCA\OpenRegister\Service\Index\Backends\Elasticsearch\ElasticsearchDocumentIndexer; -use OCA\OpenRegister\Service\Index\Backends\Elasticsearch\ElasticsearchQueryExecutor; -use Psr\Log\LoggerInterface; - -/** - * ElasticsearchBackend - * - * Thin coordinator that implements SearchBackendInterface by delegating - * to specialized Elasticsearch service classes. - * - * @SuppressWarnings(PHPMD.TooManyPublicMethods) Implements SearchBackendInterface with many required methods - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ -class ElasticsearchBackend implements SearchBackendInterface -{ - - /** - * Elasticsearch HTTP client for making requests - * - * @var ElasticsearchHttpClient - */ - private readonly ElasticsearchHttpClient $httpClient; - - /** - * Elasticsearch index manager for index operations - * - * @var ElasticsearchIndexManager - */ - private readonly ElasticsearchIndexManager $indexManager; - - /** - * Elasticsearch document indexer for indexing operations - * - * @var ElasticsearchDocumentIndexer - */ - private readonly ElasticsearchDocumentIndexer $indexer; - - /** - * Elasticsearch query executor for search operations - * - * @var ElasticsearchQueryExecutor - */ - private readonly ElasticsearchQueryExecutor $queryExecutor; - - /** - * PSR-3 logger instance - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param ElasticsearchHttpClient $httpClient HTTP client - * @param ElasticsearchIndexManager $indexManager Index manager - * @param ElasticsearchDocumentIndexer $indexer Document indexer - * @param ElasticsearchQueryExecutor $queryExecutor Query executor - * @param LoggerInterface $logger Logger - */ - public function __construct( - ElasticsearchHttpClient $httpClient, - ElasticsearchIndexManager $indexManager, - ElasticsearchDocumentIndexer $indexer, - ElasticsearchQueryExecutor $queryExecutor, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->indexManager = $indexManager; - $this->indexer = $indexer; - $this->queryExecutor = $queryExecutor; - $this->logger = $logger; - }//end __construct() - - /** - * Index an object. - * - * @param ObjectEntity $object The object to index - * @param bool $commit Whether to commit immediately - * - * @return bool True on success, false on failure - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade delegation to ElasticsearchDocumentIndexer::indexObject - */ - public function indexObject(ObjectEntity $object, bool $commit=false): bool - { - return $this->indexer->indexObject(object: $object, refresh: $commit); - }//end indexObject() - - /** - * Index multiple objects. - * - * @param array $objects The objects to index - * @param bool $commit Whether to commit immediately - * - * @return (bool|int|string)[] Results of bulk indexing operation - * - * @psalm-return array{success: bool, indexed: int<0, max>, failed: int, error?: string} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade delegation to ElasticsearchDocumentIndexer::bulkIndexObjects - */ - public function bulkIndexObjects(array $objects, bool $commit=false): array - { - return $this->indexer->bulkIndexObjects(objects: $objects, refresh: $commit); - }//end bulkIndexObjects() - - /** - * Delete an object. - * - * @param string|int $objectId The object ID to delete - * @param bool $commit Whether to commit immediately - * - * @return bool True on success, false on failure - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade delegation to ElasticsearchDocumentIndexer::deleteObject - */ - public function deleteObject(string|int $objectId, bool $commit=false): bool - { - return $this->indexer->deleteObject(objectId: $objectId, refresh: $commit); - }//end deleteObject() - - /** - * Delete objects by query. - * - * @param string $query The query string - * @param bool $commit Whether to commit immediately - * @param bool $returnDetails Whether to return details - * - * @return int[]|true Array with details if $returnDetails is true, otherwise bool - * - * @psalm-return array{deleted: 0}|true - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude simplified stub — returns success without deleting (not yet implemented) - */ - public function deleteByQuery(string $query, bool $commit=false, bool $returnDetails=false): array|bool - { - // Simplified implementation - just return success. - $this->logger->info( - message: '[ElasticsearchBackend] deleteByQuery called (not fully implemented yet)', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - if ($returnDetails === true) { - return ['deleted' => 0]; - } - - return true; - }//end deleteByQuery() - - /** - * Search with pagination. - * - * @param array $query The search query - * @param bool $_rbac Whether to apply RBAC - * @param bool $_multitenancy Whether to apply multitenancy - * @param bool $deleted Whether to include deleted objects - * - * @return ((array|mixed)[]|int|mixed)[] Search results with pagination metadata - * - * @psalm-return array{total: 0|mixed, results: array|mixed>, page: 1, limit: 10|mixed} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-88 - */ - public function searchObjectsPaginated( - array $query=[], - bool $_rbac=true, - bool $_multitenancy=true, - bool $deleted=false - ): array { - $result = $this->queryExecutor->search($query); - - // Convert Elasticsearch response to OpenRegister format. - return [ - 'total' => $result['hits']['total']['value'] ?? 0, - 'results' => array_map( - function (array $hit): array { - return $hit['_source'] ?? []; - }, - $result['hits']['hits'] ?? [] - ), - 'page' => 1, - 'limit' => $query['_limit'] ?? 10, - ]; - }//end searchObjectsPaginated() - - /** - * Get document count. - * - * @return int Number of documents in the index - */ - public function getDocumentCount(): int - { - return $this->queryExecutor->getDocumentCount(); - }//end getDocumentCount() - - /** - * Commit changes (refresh index). - * - * @return bool True on success, false on failure - * - * @spec exclude facade delegation to ElasticsearchIndexManager::refreshIndex - */ - public function commit(): bool - { - return $this->indexManager->refreshIndex( - $this->indexManager->getActiveIndexName() - ); - }//end commit() - - /** - * Search objects. - * - * @param array $params The search parameters - * - * @return array Search results - * - * @spec exclude facade delegation to ElasticsearchQueryExecutor::search - */ - public function search(array $params): array - { - return $this->queryExecutor->search($params); - }//end search() - - /** - * Reindex all objects. - * - * @param int $maxObjects Maximum number of objects to reindex - * @param int $batchSize Batch size for reindexing - * @param string|null $collectionName Collection name to reindex - * - * @return (int|string|true)[] Reindexing results - * - * @psalm-return array{success: true, indexed: 0, message: 'Reindexing should be called via IndexService'} - * - * @spec exclude stub — defers reindex to IndexService; returns a static message - */ - public function reindexAll(int $maxObjects=0, int $batchSize=1000, ?string $collectionName=null): array - { - $this->logger->info( - message: '[ElasticsearchBackend] reindexAll called (delegates to external handler)', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - return [ - 'success' => true, - 'indexed' => 0, - 'message' => 'Reindexing should be called via IndexService', - ]; - }//end reindexAll() - - /** - * Warmup index (ensure it exists). - * - * @param array $schemas Schemas to warmup - * @param int $maxObjects Maximum objects to process - * @param string $mode Processing mode - * @param bool $collectErrors Whether to collect errors - * @param int $batchSize Batch size for processing - * @param array $schemaIds Specific schema IDs to process - * - * @return (string|true)[] Warmup results - * - * @psalm-return array{success: true, index: string, message: 'Index warmed up'} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade — ensures the active index exists via ElasticsearchIndexManager - */ - public function warmupIndex( - array $schemas=[], - int $maxObjects=0, - string $mode='serial', - bool $collectErrors=false, - int $batchSize=1000, - array $schemaIds=[] - ): array { - $index = $this->indexManager->getActiveIndexName(); - $this->indexManager->ensureIndex($index); - - return [ - 'success' => true, - 'index' => $index, - 'message' => 'Index warmed up', - ]; - }//end warmupIndex() - - /** - * Check if backend is available. - * - * @param bool $forceRefresh Whether to force refresh availability check - * - * @return bool True if backend is available - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function isAvailable(bool $forceRefresh=false): bool - { - return $this->httpClient->isConfigured(); - }//end isAvailable() - - /** - * Test connection to backend. - * - * @param bool $inclCollTests Whether to include collection tests - * - * @return (bool|int|string)[] Connection test results - * - * @psalm-return array{success: bool, error?: string, document_count?: int} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade — probes connectivity via a document-count request - */ - public function testConnection(bool $inclCollTests=true): array - { - try { - $count = $this->getDocumentCount(); - return [ - 'success' => true, - 'document_count' => $count, - ]; - } catch (\Exception $e) { - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - } - }//end testConnection() - - /** - * Optimize index. - * - * @return true True on success - * - * @spec exclude no-op — Elasticsearch needs no manual optimization (returns true) - */ - public function optimize(): bool - { - // Elasticsearch doesn't need manual optimization like Solr. - return true; - }//end optimize() - - /** - * Clear index. - * - * @param string|null $collectionName Collection name to clear - * - * @return int[] Clear operation results - * - * @psalm-return array{deleted: 0} - * - * @spec exclude facade delegation to ElasticsearchDocumentIndexer::clearIndex - */ - public function clearIndex(?string $collectionName=null): array - { - $this->indexer->clearIndex(); - return ['deleted' => 0]; - }//end clearIndex() - - /** - * Get backend configuration. - * - * @return array Backend configuration - * - * @psalm-return array - */ - public function getConfig(): array - { - return $this->httpClient->getConfig(); - }//end getConfig() - - /** - * Get backend statistics. - * - * @return (int|string)[] Backend statistics - * - * @psalm-return array{document_count: int, backend: 'elasticsearch'} - * - * @spec exclude boilerplate stats — returns document count and backend label - */ - public function getStats(): array - { - return [ - 'document_count' => $this->getDocumentCount(), - 'backend' => 'elasticsearch', - ]; - }//end getStats() - - /** - * Create collection/index. - * - * @param string $name Collection name to create - * @param array $config Configuration for the collection - * - * @return bool[] Creation results - * - * @psalm-return array{success: bool} - * - * @spec exclude facade delegation to ElasticsearchIndexManager::createIndex - */ - public function createCollection(string $name, array $config=[]): array - { - $success = $this->indexManager->createIndex(indexName: $name, mapping: $config); - return ['success' => $success]; - }//end createCollection() - - /** - * Delete collection/index. - * - * @param string|null $collectionName Collection name to delete - * - * @return bool[] Deletion results - * - * @psalm-return array{success: bool} - * - * @spec exclude facade delegation to ElasticsearchIndexManager::deleteIndex - */ - public function deleteCollection(?string $collectionName=null): array - { - $name = $collectionName ?? $this->indexManager->getActiveIndexName(); - $success = $this->indexManager->deleteIndex($name); - return ['success' => $success]; - }//end deleteCollection() - - /** - * Check if collection exists. - * - * @param string $collectionName Collection name to check - * - * @return bool True if collection exists - * - * @spec exclude facade delegation to ElasticsearchIndexManager::indexExists - */ - public function collectionExists(string $collectionName): bool - { - return $this->indexManager->indexExists($collectionName); - }//end collectionExists() - - /** - * List all collections. - * - * @return string[] List of collection names - * - * @psalm-return list{string} - * - * @spec exclude simplified stub — returns only the active index name - */ - public function listCollections(): array - { - // Simplified - would need ES API call to list all indices. - return [$this->indexManager->getActiveIndexName()]; - }//end listCollections() - - /** - * Index generic documents. - * - * @param array $documents Documents to index - * - * @return true True on success - * - * @spec exclude simplified stub — logs document count and returns true (not yet implemented) - */ - public function index(array $documents): bool - { - // Simplified implementation. - $this->logger->info( - message: '[ElasticsearchBackend] index() called with '.count($documents).' documents', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return true; - }//end index() - - /** - * Run an aggregation against this Elasticsearch backend. - * - * The translator already exists at - * `Aggregation\ElasticsearchAggregationQueryBuilder` — this method - * is the HTTP-client adapter on top. Returns null until the dev - * container ships an ES instance the runtime can talk to, so the - * caller falls back to the PHP path. Stub matches the interface - * contract so the AggregationRunner can already start dispatching - * by-backend. - * - * @param \OCA\OpenRegister\Service\Aggregation\AggregationQuery $query Portable aggregation request. - * - * @return array|null The aggregation result, or null when the backend cannot execute it. - * - * @spec exclude stub — returns null so caller falls back to PHP path (HTTP adapter not yet wired) - */ - public function aggregate(\OCA\OpenRegister\Service\Aggregation\AggregationQuery $query): ?array - { - $this->logger->info( - message: '[ElasticsearchBackend] aggregate() — HTTP client not yet wired; returning null so caller falls back to PHP path', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return null; - }//end aggregate() - - /** - * Get field types. - * - * @param string $collection Collection name - * - * @return array Field types - * - * @psalm-return array - */ - public function getFieldTypes(string $collection): array - { - return []; - }//end getFieldTypes() - - /** - * Add field type. - * - * @param string $collection Collection name - * @param array $fieldType Field type configuration - * - * @return true True on success - * - * @spec exclude no-op stub — ES infers field types dynamically (returns true) - */ - public function addFieldType(string $collection, array $fieldType): bool - { - return true; - }//end addFieldType() - - /** - * Get fields. - * - * @param string $collection Collection name - * - * @return array Field definitions - * - * @psalm-return array - */ - public function getFields(string $collection): array - { - return []; - }//end getFields() - - /** - * Add or update field. - * - * @param array $fieldConfig Field configuration - * @param bool $force Whether to force update - * - * @return string Status message - * - * @psalm-return 'skipped' - * - * @spec exclude no-op stub — ES infers field mappings dynamically (returns 'skipped') - */ - public function addOrUpdateField(array $fieldConfig, bool $force): string - { - return 'skipped'; - }//end addOrUpdateField() - - /** - * Index files by their IDs. - * - * @param array $fileIds Array of file IDs to index. - * @param string|null $collectionName Optional collection name. - * - * @return array Indexing results. - * - * @spec exclude stub — returns empty result shape; file indexing handled by FileHandler (REQ-8) - */ - public function indexFiles(array $fileIds, ?string $collectionName=null): array - { - return ['indexed' => 0, 'failed' => 0, 'errors' => []]; - }//end indexFiles() - - /** - * Get file indexing statistics. - * - * @return array File indexing statistics. - */ - public function getFileIndexStats(): array - { - return []; - }//end getFileIndexStats() - - /** - * Fix mismatched fields in the search backend schema. - * - * @param array $mismatchedFields Array of mismatched fields. - * @param bool $dryRun Whether to preview changes only. - * - * @return array Results of the fix operation. - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-88 - */ - public function fixMismatchedFields(array $mismatchedFields, bool $dryRun=false): array - { - return []; - }//end fixMismatchedFields() -}//end class diff --git a/lib/Service/Index/Backends/Solr/SolrCollectionManager.php b/lib/Service/Index/Backends/Solr/SolrCollectionManager.php deleted file mode 100644 index bd6939bc7c..0000000000 --- a/lib/Service/Index/Backends/Solr/SolrCollectionManager.php +++ /dev/null @@ -1,470 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Solr; - -use Exception; -use Psr\Log\LoggerInterface; - -/** - * SolrCollectionManager - * - * Handles Solr collection and ConfigSet operations. - * - * @category Service - * @package OCA\OpenRegister\Service\Index\Backends\Solr - */ -class SolrCollectionManager -{ - - /** - * HTTP client. - * - * @var SolrHttpClient - */ - private readonly SolrHttpClient $httpClient; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Solr configuration. - * - * @var array - */ - private array $config; - - /** - * Constructor - * - * @param SolrHttpClient $httpClient HTTP client - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - SolrHttpClient $httpClient, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->logger = $logger; - $this->config = $httpClient->getConfig(); - }//end __construct() - - /** - * Check if a collection exists. - * - * @param string $collectionName Collection name - * - * @return bool True if exists - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function collectionExists(string $collectionName): bool - { - try { - $url = $this->httpClient->buildSolrBaseUrl().'/admin/collections?action=CLUSTERSTATUS&wt=json'; - $data = $this->httpClient->get($url, ['timeout' => 10]); - - return isset($data['cluster']['collections'][$collectionName]); - } catch (Exception $e) { - $this->logger->error( - message: '[SolrCollectionManager] Failed to check collection existence', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collectionName, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end collectionExists() - - /** - * Get the active collection name. - * - * Returns tenant-specific collection if it exists, otherwise null. - * - * @return string|null Collection name or null - * - * @spec exclude boilerplate accessor — resolves active collection via existence check - */ - public function getActiveCollectionName(): ?string - { - $baseCollection = $this->config['objectCollection'] ?? $this->config['core'] ?? 'openregister'; - $tenantCollection = $this->httpClient->getTenantSpecificCollectionName($baseCollection); - - if ($this->collectionExists(collectionName: $tenantCollection) === true) { - return $tenantCollection; - } - - $this->logger->warning( - message: '[SolrCollectionManager] Tenant collection does not exist', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'tenant_collection' => $tenantCollection, - 'base_collection' => $baseCollection, - ] - ); - - return null; - }//end getActiveCollectionName() - - /** - * Create a Solr collection. - * - * @param string $name Collection name - * @param array $config Configuration options - * - * @return (mixed|string|true)[] Result with success status - * - * @throws Exception If creation fails - * - * @psalm-return array{success: true, - * message: 'Collection created successfully', collection: string, - * configSet: 'openregister_configset'|mixed} - * - * @spec exclude thin delegation to Solr Collections API — CREATE action HTTP call - */ - public function createCollection(string $name, array $config=[]): array - { - $configSetName = $config['configSetName'] ?? 'openregister_configset'; - $numShards = $config['numShards'] ?? 1; - $replicationFactor = $config['replicationFactor'] ?? 1; - $maxShardsPerNode = $config['maxShardsPerNode'] ?? 1; - - $this->logger->info( - message: '[SolrCollectionManager] Creating collection', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'name' => $name, - 'configSet' => $configSetName, - ] - ); - - $url = $this->httpClient->buildSolrBaseUrl().'/admin/collections?'.http_build_query( - [ - 'action' => 'CREATE', - 'name' => $name, - 'collection.configName' => $configSetName, - 'numShards' => $numShards, - 'replicationFactor' => $replicationFactor, - 'maxShardsPerNode' => $maxShardsPerNode, - 'wt' => 'json', - ] - ); - - $data = $this->httpClient->get($url, ['timeout' => 60]); - - if (($data['responseHeader']['status'] ?? -1) === 0) { - $this->logger->info( - message: '[SolrCollectionManager] Collection created successfully', - context: ['file' => __FILE__, 'line' => __LINE__, 'collection' => $name] - ); - - return [ - 'success' => true, - 'message' => 'Collection created successfully', - 'collection' => $name, - 'configSet' => $configSetName, - ]; - } - - $errorMessage = $data['error']['msg'] ?? 'Unknown Solr error'; - $this->logger->error( - message: '[SolrCollectionManager] Collection creation failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $name, - 'error' => $errorMessage, - ] - ); - - throw new Exception("Solr collection creation failed: {$errorMessage}"); - }//end createCollection() - - /** - * Delete a Solr collection. - * - * @param string|null $collectionName Collection name (null = active collection) - * - * @return (bool|string)[] - * - * @psalm-return array{success: bool, message: string, exception?: string, collection?: string} - * - * @spec exclude thin delegation to Solr Collections API — DELETE action HTTP call - */ - public function deleteCollection(?string $collectionName=null): array - { - try { - $targetCollection = $collectionName ?? $this->getActiveCollectionName(); - - if ($targetCollection === null) { - return [ - 'success' => false, - 'message' => 'No collection specified and no active collection found', - ]; - } - - $this->logger->info( - message: '[SolrCollectionManager] Deleting collection', - context: ['file' => __FILE__, 'line' => __LINE__, 'collection' => $targetCollection] - ); - - $url = $this->httpClient->buildSolrBaseUrl().'/admin/collections?'.http_build_query( - [ - 'action' => 'DELETE', - 'name' => $targetCollection, - 'wt' => 'json', - ] - ); - $data = $this->httpClient->get($url, ['timeout' => 60]); - - if (($data['responseHeader']['status'] ?? -1) === 0) { - $this->logger->info( - message: '[SolrCollectionManager] Collection deleted successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $targetCollection, - ] - ); - - return [ - 'success' => true, - 'message' => 'Collection deleted successfully', - 'collection' => $targetCollection, - ]; - } - - $errorMessage = $data['error']['msg'] ?? 'Unknown error'; - $this->logger->error( - message: '[SolrCollectionManager] Collection deletion failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $targetCollection, - 'error' => $errorMessage, - ] - ); - - return [ - 'success' => false, - 'message' => "Failed to delete collection: {$errorMessage}", - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrCollectionManager] Collection deletion exception', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'success' => false, - 'message' => 'Exception during collection deletion', - 'exception' => $e->getMessage(), - ]; - }//end try - }//end deleteCollection() - - /** - * List all Solr collections. - * - * @return array List of collections - * - * @spec exclude thin delegation to Solr Collections API — LIST action HTTP call - */ - public function listCollections(): array - { - try { - $url = $this->httpClient->buildSolrBaseUrl().'/admin/collections?action=LIST&wt=json'; - $data = $this->httpClient->get($url); - - return $data['collections'] ?? []; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrCollectionManager] Failed to list collections', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return []; - }//end try - }//end listCollections() - - /** - * List all ConfigSets. - * - * @return array List of ConfigSets - * - * @spec exclude thin delegation to Solr Configs API — LIST action HTTP call - */ - public function listConfigSets(): array - { - try { - $url = $this->httpClient->buildSolrBaseUrl().'/admin/configs?action=LIST&wt=json&omitHeader=true'; - $data = $this->httpClient->get($url); - - return $data['configSets'] ?? []; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrCollectionManager] Failed to list ConfigSets', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return []; - }//end try - }//end listConfigSets() - - /** - * Create a ConfigSet. - * - * @param string $name ConfigSet name - * @param string $baseConfigSet Base ConfigSet to copy from - * - * @return (bool|mixed|string)[] Result with success status - * - * @psalm-return array{success: bool, - * message: 'ConfigSet created successfully'| - * 'Exception during ConfigSet creation'| - * 'Failed to create ConfigSet'|mixed, exception?: string, name?: string} - * - * @spec exclude thin delegation to Solr Configs API — CREATE action HTTP call - */ - public function createConfigSet(string $name, string $baseConfigSet='_default'): array - { - try { - $this->logger->info( - message: '[SolrCollectionManager] Creating ConfigSet', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'name' => $name, - 'base' => $baseConfigSet, - ] - ); - - $baseUrl = $this->httpClient->buildSolrBaseUrl(); - $url = $baseUrl.'/admin/configs?action=CREATE&name='.$name.'&baseConfigSet='.$baseConfigSet.'&wt=json'; - $data = $this->httpClient->get($url); - - if (($data['responseHeader']['status'] ?? -1) === 0) { - return [ - 'success' => true, - 'message' => 'ConfigSet created successfully', - 'name' => $name, - ]; - } - - return [ - 'success' => false, - 'message' => $data['error']['msg'] ?? 'Failed to create ConfigSet', - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrCollectionManager] ConfigSet creation failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'success' => false, - 'message' => 'Exception during ConfigSet creation', - 'exception' => $e->getMessage(), - ]; - }//end try - }//end createConfigSet() - - /** - * Delete a ConfigSet. - * - * @param string $name ConfigSet name - * - * @return (bool|mixed|string)[] Result with success status - * - * @psalm-return array{success: bool, - * message: 'ConfigSet deleted successfully'| - * 'Exception during ConfigSet deletion'| - * 'Failed to delete ConfigSet'|mixed, exception?: string, name?: string} - * - * @spec exclude thin delegation to Solr Configs API — DELETE action HTTP call - */ - public function deleteConfigSet(string $name): array - { - try { - $this->logger->info( - message: '[SolrCollectionManager] Deleting ConfigSet', - context: ['file' => __FILE__, 'line' => __LINE__, 'name' => $name] - ); - - $url = $this->httpClient->buildSolrBaseUrl().'/admin/configs?action=DELETE&name='.$name.'&wt=json'; - $data = $this->httpClient->get($url); - - if (($data['responseHeader']['status'] ?? -1) === 0) { - return [ - 'success' => true, - 'message' => 'ConfigSet deleted successfully', - 'name' => $name, - ]; - } - - return [ - 'success' => false, - 'message' => $data['error']['msg'] ?? 'Failed to delete ConfigSet', - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrCollectionManager] ConfigSet deletion failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'success' => false, - 'message' => 'Exception during ConfigSet deletion', - 'exception' => $e->getMessage(), - ]; - }//end try - }//end deleteConfigSet() -}//end class diff --git a/lib/Service/Index/Backends/Solr/SolrDocumentIndexer.php b/lib/Service/Index/Backends/Solr/SolrDocumentIndexer.php deleted file mode 100644 index 3317495133..0000000000 --- a/lib/Service/Index/Backends/Solr/SolrDocumentIndexer.php +++ /dev/null @@ -1,639 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Solr; - -use Exception; -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Service\Index\DocumentBuilder; -use Psr\Log\LoggerInterface; - -/** - * SolrDocumentIndexer - * - * Handles Solr document indexing operations. - * - * @category Service - * @package OCA\OpenRegister\Service\Index\Backends\Solr - * - * HTTP response handling requires else for error paths - */ -class SolrDocumentIndexer -{ - - /** - * HTTP client. - * - * @var SolrHttpClient - */ - private readonly SolrHttpClient $httpClient; - - /** - * Collection manager. - * - * @var SolrCollectionManager - */ - private readonly SolrCollectionManager $collectionManager; - - /** - * Document builder. - * - * @var DocumentBuilder - */ - private readonly DocumentBuilder $documentBuilder; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param SolrHttpClient $httpClient HTTP client - * @param SolrCollectionManager $collectionManager Collection manager - * @param DocumentBuilder $documentBuilder Document builder - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - SolrHttpClient $httpClient, - SolrCollectionManager $collectionManager, - DocumentBuilder $documentBuilder, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->collectionManager = $collectionManager; - $this->documentBuilder = $documentBuilder; - $this->logger = $logger; - }//end __construct() - - /** - * Index a single object. - * - * @param ObjectEntity $object Object to index - * @param bool $commit Whether to commit immediately - * - * @return bool True if successful - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude thin delegation to SolrHttpClient — builds Solr update doc and POSTs it - */ - public function indexObject(ObjectEntity $object, bool $commit=false): bool - { - try { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - $this->logger->warning( - message: '[SolrDocumentIndexer] No active collection for indexing', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return false; - } - - // Use DocumentBuilder to create the Solr document. - $document = $this->documentBuilder->createDocument($object); - - // Index the document. - $commitValue = 'false'; - if ($commit === true) { - $commitValue = 'true'; - } - - $url = $this->httpClient->getEndpointUrl($collection).'/update?commit='.$commitValue; - - $this->httpClient->post($url, [$document]); - - $this->logger->debug( - message: '[SolrDocumentIndexer] Object indexed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $object->getId(), - 'commit' => $commit, - ] - ); - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Failed to index object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $object->getId(), - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end indexObject() - - /** - * Index multiple objects in bulk. - * - * @param array $objects Array of ObjectEntity objects - * @param bool $commit Whether to commit immediately - * - * @return (bool|int|string)[] Result with statistics - * - * @psalm-return array{success: bool, indexed: int<0, max>, failed: int<0, max>, error?: string} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function bulkIndexObjects(array $objects, bool $commit=true): array - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - return [ - 'success' => false, - 'indexed' => 0, - 'failed' => count($objects), - 'error' => 'No active collection', - ]; - } - - $documents = []; - $successCount = 0; - $failureCount = 0; - - foreach ($objects as $object) { - try { - $documents[] = $this->documentBuilder->createDocument($object); - $successCount++; - } catch (Exception $e) { - $failureCount++; - $this->logger->warning( - message: '[SolrDocumentIndexer] Failed to create document for object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $object->getId(), - 'error' => $e->getMessage(), - ] - ); - }//end try - }//end foreach - - if (empty($documents) === false) { - try { - $commitValue = 'false'; - if ($commit === true) { - $commitValue = 'true'; - }//end if - - $url = $this->httpClient->getEndpointUrl($collection).'/update?commit='.$commitValue; - $this->httpClient->post($url, $documents); - - $this->logger->info( - message: '[SolrDocumentIndexer] Bulk index completed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'indexed' => $successCount, - 'failed' => $failureCount, - ] - ); - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Bulk index failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return [ - 'success' => false, - 'indexed' => 0, - 'failed' => count($objects), - 'error' => $e->getMessage(), - ]; - }//end try - }//end if - - return [ - 'success' => true, - 'indexed' => $successCount, - 'failed' => $failureCount, - ]; - }//end bulkIndexObjects() - - /** - * Index raw documents (not ObjectEntity). - * - * Used by FileHandler for indexing file chunks. - * - * @param array $documents Array of documents to index - * @param bool $commit Whether to commit immediately - * - * @return bool True if successful - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function indexDocuments(array $documents, bool $commit=false): bool - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - $this->logger->warning( - message: '[SolrDocumentIndexer] No active collection for bulk index', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return false; - } - - try { - $commitValue = 'false'; - if ($commit === true) { - $commitValue = 'true'; - } - - $url = $this->httpClient->getEndpointUrl($collection).'/update?commit='.$commitValue; - $this->httpClient->post($url, $documents); - - $this->logger->info( - message: '[SolrDocumentIndexer] Documents indexed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'count' => count($documents), - 'commit' => $commit, - ] - ); - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Failed to index documents', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end indexDocuments() - - /** - * Delete an object from the index. - * - * @param string|int $objectId Object ID to delete - * @param bool $commit Whether to commit immediately - * - * @return bool True if successful - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function deleteObject(string|int $objectId, bool $commit=false): bool - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - $this->logger->warning( - message: '[SolrDocumentIndexer] No active collection for deletion', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return false; - } - - try { - $commitValue = 'false'; - if ($commit === true) { - $commitValue = 'true'; - } - - $url = $this->httpClient->getEndpointUrl($collection).'/update?commit='.$commitValue; - - $deleteCommand = [ - 'delete' => [ - 'query' => 'id:'.$objectId, - ], - ]; - - $this->httpClient->post($url, $deleteCommand); - - $this->logger->debug( - message: '[SolrDocumentIndexer] Object deleted', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $objectId, - 'commit' => $commit, - ] - ); - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Failed to delete object', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $objectId, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end deleteObject() - - /** - * Delete documents by query. - * - * @param string $query Solr query - * @param bool $commit Whether to commit immediately - * @param bool $returnDetails Whether to return detailed results - * - * @return (array|bool|string)[]|bool Results or boolean - * - * @psalm-return array{success: bool, error?: string, query?: string, result?: array}|bool - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function deleteByQuery(string $query, bool $commit=false, bool $returnDetails=false): array|bool - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - if ($returnDetails === true) { - return ['success' => false, 'error' => 'No active collection']; - } - - return false; - } - - try { - $commitValue = 'false'; - if ($commit === true) { - $commitValue = 'true'; - } - - $url = $this->httpClient->getEndpointUrl($collection).'/update?commit='.$commitValue; - - $deleteCommand = [ - 'delete' => [ - 'query' => $query, - ], - ]; - - $result = $this->httpClient->post($url, $deleteCommand); - - $this->logger->info( - message: '[SolrDocumentIndexer] Deleted by query', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'query' => $query, - 'commit' => $commit, - ] - ); - - if ($returnDetails === true) { - return [ - 'success' => true, - 'query' => $query, - 'result' => $result, - ]; - } - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Delete by query failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'query' => $query, - 'error' => $e->getMessage(), - ] - ); - - if ($returnDetails === true) { - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - } - - return false; - }//end try - }//end deleteByQuery() - - /** - * Commit changes to Solr. - * - * @return bool True if successful - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function commit(): bool - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - $this->logger->warning( - message: '[SolrDocumentIndexer] No active collection for commit', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return false; - } - - try { - $url = $this->httpClient->getEndpointUrl($collection).'/update?commit=true'; - $this->httpClient->post($url, []); - - $this->logger->debug( - message: '[SolrDocumentIndexer] Commit successful', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Commit failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end commit() - - /** - * Clear all documents from the index. - * - * @param string|null $collectionName Collection to clear (null = active collection) - * - * @return (bool|string)[] - * - * @psalm-return array{success: bool, message: string, collection?: string} - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function clearIndex(?string $collectionName=null): array - { - $collection = $collectionName ?? $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - return [ - 'success' => false, - 'message' => 'No collection specified', - ]; - } - - try { - $this->logger->info( - message: '[SolrDocumentIndexer] Clearing index', - context: ['file' => __FILE__, 'line' => __LINE__, 'collection' => $collection] - ); - - $url = $this->httpClient->getEndpointUrl($collection).'/update?commit=true'; - - $deleteCommand = [ - 'delete' => [ - 'query' => '*:*', - ], - ]; - - $this->httpClient->post($url, $deleteCommand); - - return [ - 'success' => true, - 'message' => 'Index cleared successfully', - 'collection' => $collection, - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Failed to clear index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'success' => false, - 'message' => 'Failed to clear index: '.$e->getMessage(), - ]; - }//end try - }//end clearIndex() - - /** - * Optimize the Solr index. - * - * @return bool True if successful - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function optimize(): bool - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - $this->logger->warning( - message: '[SolrDocumentIndexer] No active collection for optimization', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return false; - } - - try { - $this->logger->info( - message: '[SolrDocumentIndexer] Optimizing index', - context: ['file' => __FILE__, 'line' => __LINE__, 'collection' => $collection] - ); - - $url = $this->httpClient->getEndpointUrl($collection).'/update?optimize=true'; - $this->httpClient->post($url, []); - - $this->logger->info( - message: '[SolrDocumentIndexer] Optimization completed', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Optimization failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end optimize() - - /** - * Get document count in the index. - * - * @return int Document count - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function getDocumentCount(): int - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - return 0; - } - - try { - $url = $this->httpClient->getEndpointUrl($collection).'/select?q=*:*&rows=0&wt=json'; - $data = $this->httpClient->get($url); - - return $data['response']['numFound'] ?? 0; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrDocumentIndexer] Failed to get document count', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return 0; - }//end try - }//end getDocumentCount() -}//end class diff --git a/lib/Service/Index/Backends/Solr/SolrFacetProcessor.php b/lib/Service/Index/Backends/Solr/SolrFacetProcessor.php deleted file mode 100644 index 35ee25d574..0000000000 --- a/lib/Service/Index/Backends/Solr/SolrFacetProcessor.php +++ /dev/null @@ -1,218 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Solr; - -use Exception; -use Psr\Log\LoggerInterface; - -/** - * SolrFacetProcessor - * - * Processes facets for search results. - * - * NOTE: This is a simplified initial implementation. - * Full facet processing (25+ methods) can be migrated from SolrBackend incrementally. - * - * @category Service - * @package OCA\OpenRegister\Service\Index\Backends\Solr - */ -class SolrFacetProcessor -{ - - /** - * HTTP client. - * - * @var SolrHttpClient - */ - private readonly SolrHttpClient $httpClient; - - /** - * Collection manager. - * - * @var SolrCollectionManager - */ - private readonly SolrCollectionManager $collectionManager; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param SolrHttpClient $httpClient HTTP client - * @param SolrCollectionManager $collectionManager Collection manager - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - SolrHttpClient $httpClient, - SolrCollectionManager $collectionManager, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->collectionManager = $collectionManager; - $this->logger = $logger; - }//end __construct() - - /** - * Get facetable fields from Solr schema. - * - * NOTE: This is a simplified implementation. - * Full implementation with 25+ facet methods can be migrated from SolrBackend. - * - * @return (mixed|string)[][] Facetable fields - * - * @psalm-return list - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 - */ - public function getRawSolrFieldsForFacetConfiguration(): array - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - $this->logger->warning( - message: '[SolrFacetProcessor] No active collection', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return []; - } - - try { - $url = $this->httpClient->getEndpointUrl($collection).'/schema/fields?wt=json'; - $data = $this->httpClient->get($url); - - $fields = $data['fields'] ?? []; - $facetable = []; - - foreach ($fields as $field) { - // Fields with _s, _ss, _i suffixes are typically facetable. - $name = $field['name'] ?? ''; - $isFacetable = str_ends_with($name, '_s') === true - || str_ends_with($name, '_ss') === true - || str_ends_with($name, '_i') === true; - if ($isFacetable === true) { - $facetable[] = [ - 'name' => $name, - 'type' => $field['type'] ?? 'unknown', - ]; - } - } - - $this->logger->debug( - message: '[SolrFacetProcessor] Found facetable fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'count' => count($facetable), - ] - ); - - return $facetable; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrFacetProcessor] Failed to get facetable fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return []; - }//end try - }//end getRawSolrFieldsForFacetConfiguration() - - /** - * Build facet query for search. - * - * @param array $facetFields Fields to facet on - * - * @return (array|int|string)[] - * - * @psalm-return array{facet?: 'true', 'facet.field'?: array, 'facet.limit'?: 100} - * - * @spec exclude boilerplate query-param builder — assembles Solr facet.* params array - */ - public function buildFacetQuery(array $facetFields): array - { - if (empty($facetFields) === true) { - return []; - } - - return [ - 'facet' => 'true', - 'facet.field' => $facetFields, - 'facet.limit' => 100, - ]; - }//end buildFacetQuery() - - /** - * Process facet response from Solr. - * - * @param array $solrResponse Solr search response with facets - * - * @return (array[]|mixed)[][] Processed facets - * - * @psalm-return array}> - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function processFacetResponse(array $solrResponse): array - { - $facetCounts = $solrResponse['facet_counts'] ?? []; - $facetFields = $facetCounts['facet_fields'] ?? []; - - $processed = []; - - foreach ($facetFields as $fieldName => $values) { - $items = []; - $valuesCount = count($values); - - // Solr returns facets as [value1, count1, value2, count2, ...]. - for ($i = 0; $i < $valuesCount; $i += 2) { - if (isset($values[$i], $values[$i + 1]) === true) { - $items[] = [ - 'value' => $values[$i], - 'count' => $values[$i + 1], - ]; - } - } - - if (empty($items) === false) { - $processed[$fieldName] = [ - 'field' => $fieldName, - 'items' => $items, - ]; - } - }//end foreach - - return $processed; - }//end processFacetResponse() -}//end class diff --git a/lib/Service/Index/Backends/Solr/SolrHttpClient.php b/lib/Service/Index/Backends/Solr/SolrHttpClient.php deleted file mode 100644 index b1b3faefe8..0000000000 --- a/lib/Service/Index/Backends/Solr/SolrHttpClient.php +++ /dev/null @@ -1,278 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Solr; - -use Exception; -use GuzzleHttp\Client as GuzzleClient; -use OCA\OpenRegister\Service\SettingsService; -use Psr\Log\LoggerInterface; - -/** - * SolrHttpClient - * - * Manages HTTP client and URL building for Solr operations. - * - * @category Service - * @package OCA\OpenRegister\Service\Index\Backends\Solr - */ -class SolrHttpClient -{ - - /** - * HTTP client for Solr requests. - * - * @var GuzzleClient - */ - private GuzzleClient $httpClient; - - /** - * Solr connection configuration. - * - * @var array - */ - private array $config = []; - - /** - * Settings service. - * - * @var SettingsService - */ - private readonly SettingsService $settingsService; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param SettingsService $settingsService Settings service - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - SettingsService $settingsService, - LoggerInterface $logger - ) { - $this->settingsService = $settingsService; - $this->logger = $logger; - - $this->initializeConfig(); - $this->initializeHttpClient(); - }//end __construct() - - /** - * Initialize Solr configuration from settings. - * - * @return void - */ - private function initializeConfig(): void - { - $settings = $this->settingsService->getSolrSettings(); - - $this->config = [ - 'enabled' => ($settings['enabled'] ?? false), - 'host' => ($settings['host'] ?? 'localhost'), - 'port' => ((int) ($settings['port'] ?? 8983)), - 'path' => ($settings['path'] ?? '/solr'), - 'core' => ($settings['core'] ?? 'openregister'), - 'timeout' => ((int) ($settings['timeout'] ?? 30)), - ]; - }//end initializeConfig() - - /** - * Initialize HTTP client for Solr requests. - * - * @return void - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - private function initializeHttpClient(): void - { - $this->httpClient = new GuzzleClient( - [ - 'timeout' => $this->config['timeout'], - 'connect_timeout' => 5, - 'http_errors' => false, - 'verify' => false, - 'headers' => [ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ], - ] - ); - }//end initializeHttpClient() - - /** - * Check if Solr is configured. - * - * @return bool True if configured - * - * @spec exclude boilerplate config getter — checks enabled/host/core presence - */ - public function isConfigured(): bool - { - return !empty($this->config['enabled']) - && !empty($this->config['host']) - && !empty($this->config['core']); - }//end isConfigured() - - /** - * Get Solr configuration. - * - * @return array Configuration array - */ - public function getConfig(): array - { - return $this->config; - }//end getConfig() - - /** - * Get HTTP client. - * - * @return GuzzleClient HTTP client - */ - public function getHttpClient(): GuzzleClient - { - return $this->httpClient; - }//end getHttpClient() - - /** - * Build base Solr URL. - * - * @return string Base Solr URL - * - * @spec exclude boilerplate URL builder — host/port/path string concatenation - */ - public function buildSolrBaseUrl(): string - { - $host = $this->config['host']; - $port = $this->config['port']; - $path = $this->config['path']; - - return "http://{$host}:{$port}{$path}"; - }//end buildSolrBaseUrl() - - /** - * Get endpoint URL for a collection. - * - * @param string|null $collection Collection name (null = use default core) - * - * @return string Endpoint URL - * - * @spec exclude boilerplate URL builder — appends collection/core to base URL - */ - public function getEndpointUrl(?string $collection=null): string - { - $baseUrl = $this->buildSolrBaseUrl(); - $core = $collection ?? $this->config['core']; - - return "{$baseUrl}/{$core}"; - }//end getEndpointUrl() - - /** - * Make a GET request to Solr. - * - * @param string $url URL to request - * @param array $opts Additional options - * - * @return array Response data - * - * @throws Exception If request fails - * - * @spec exclude thin Guzzle GET wrapper — request/json-decode/log-and-rethrow - */ - public function get(string $url, array $opts=[]): array - { - try { - $response = $this->httpClient->get($url, $opts); - $body = (string) $response->getBody(); - - return json_decode($body, true) ?? []; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrHttpClient] GET request failed', - context: ['file' => __FILE__, 'line' => __LINE__, 'url' => $url, 'error' => $e->getMessage()] - ); - throw $e; - }//end try - }//end get() - - /** - * Make a POST request to Solr. - * - * @param string $url URL to request - * @param array $data Data to send - * @param array $opts Additional options - * - * @return array Response data - * - * @throws Exception If request fails - * - * @spec exclude thin Guzzle POST wrapper — request/json-decode/log-and-rethrow - */ - public function post(string $url, array $data=[], array $opts=[]): array - { - try { - $opts['json'] = $data; - $response = $this->httpClient->post($url, $opts); - $body = (string) $response->getBody(); - - return json_decode($body, true) ?? []; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrHttpClient] POST request failed', - context: ['file' => __FILE__, 'line' => __LINE__, 'url' => $url, 'error' => $e->getMessage()] - ); - throw $e; - }//end try - }//end post() - - /** - * Get tenant-specific collection name. - * - * Adds tenant prefix to collection name if multitenancy is enabled. - * - * @param string $baseCollectionName Base collection name - * - * @return string Tenant-specific collection name - * - * @spec exclude boilerplate config helper — optional tenant prefix from settings - */ - public function getTenantSpecificCollectionName(string $baseCollectionName): string - { - // Get tenant from settings if multitenancy is enabled. - $settings = $this->settingsService->getSettings(); - - if (($settings['multitenancy']['enabled'] ?? false) === true) { - $tenant = $settings['multitenancy']['defaultUserTenant'] ?? 'default'; - return "{$tenant}_{$baseCollectionName}"; - } - - return $baseCollectionName; - }//end getTenantSpecificCollectionName() -}//end class diff --git a/lib/Service/Index/Backends/Solr/SolrQueryExecutor.php b/lib/Service/Index/Backends/Solr/SolrQueryExecutor.php deleted file mode 100644 index 34ad32b997..0000000000 --- a/lib/Service/Index/Backends/Solr/SolrQueryExecutor.php +++ /dev/null @@ -1,365 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Solr; - -use Exception; -use Psr\Log\LoggerInterface; - -/** - * SolrQueryExecutor - * - * Executes search queries against Solr. - * - * @category Service - * @package OCA\OpenRegister\Service\Index\Backends\Solr - */ -class SolrQueryExecutor -{ - - /** - * HTTP client. - * - * @var SolrHttpClient - */ - private readonly SolrHttpClient $httpClient; - - /** - * Collection manager. - * - * @var SolrCollectionManager - */ - private readonly SolrCollectionManager $collectionManager; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param SolrHttpClient $httpClient HTTP client - * @param SolrCollectionManager $collectionManager Collection manager - * @param LoggerInterface $logger Logger - * - * @return void - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function __construct( - SolrHttpClient $httpClient, - SolrCollectionManager $collectionManager, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->collectionManager = $collectionManager; - $this->logger = $logger; - }//end __construct() - - /** - * Execute a search query. - * - * @param array $params Query parameters - * - * @return array Search results - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function search(array $params): array - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - $this->logger->warning( - message: '[SolrQueryExecutor] No active collection for search', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return [ - 'response' => [ - 'numFound' => 0, - 'docs' => [], - ], - ]; - } - - try { - $queryString = http_build_query($params); - $url = $this->httpClient->getEndpointUrl($collection).'/select?'.$queryString; - - $result = $this->httpClient->get($url); - - $this->logger->debug( - message: '[SolrQueryExecutor] Search executed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'query' => $params['q'] ?? '*:*', - 'numFound' => $result['response']['numFound'] ?? 0, - ] - ); - - return $result; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrQueryExecutor] Search failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'response' => [ - 'numFound' => 0, - 'docs' => [], - ], - 'error' => $e->getMessage(), - ]; - }//end try - }//end search() - - /** - * Search with pagination. - * - * @param array $query Query parameters - * @param bool $_rbac Apply RBAC filters - * @param bool $_multitenancy Apply multitenancy filters - * @param bool $deleted Include deleted items - * - * @return array Paginated search results with pagination info. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Paginated search requires handling multiple filter conditions - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple filter combinations create many execution paths - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-10 - */ - public function searchPaginated( - array $query=[], - bool $_rbac=true, - bool $_multitenancy=true, - bool $deleted=false - ): array { - // Build Solr query from OpenRegister query format. - $solrQuery = $this->buildSolrQuery(query: $query); - - // Apply filters. - if ($_rbac === true || $_multitenancy === true || $deleted === false) { - $filters = []; - - if ($deleted === false) { - $filters[] = '-deleted:true'; - } - - if (empty($filters) === false) { - $solrQuery['fq'] = array_merge($solrQuery['fq'] ?? [], $filters); - } - } - - $solrQuery['wt'] = 'json'; - - // Execute search. - $result = $this->search(params: $solrQuery); - - // Convert to OpenRegister paginated format. - return $this->convertToPaginatedFormat(solrResult: $result, query: $query); - }//end searchPaginated() - - /** - * Build Solr query from OpenRegister query format. - * - * @param array $query OpenRegister query - * - * @return (int|mixed|string)[] Solr query parameters - * - * @psalm-return array{q: '*:*'|mixed, start: int, rows: int, sort?: string, fl?: mixed|string} - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Query building requires handling multiple parameter types - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - private function buildSolrQuery(array $query): array - { - $solrQuery = [ - 'q' => $query['_search'] ?? '*:*', - 'start' => (int) ($query['_offset'] ?? $query['_start'] ?? 0), - 'rows' => (int) ($query['_limit'] ?? $query['_rows'] ?? 30), - ]; - - // Handle sorting. - if (isset($query['_order']) === true) { - $solrQuery['sort'] = $this->translateSortField(order: $query['_order']); - } - - // Handle field selection. - if (isset($query['_fields']) === true) { - $solrQuery['fl'] = $query['_fields']; - if (is_array($query['_fields']) === true) { - $solrQuery['fl'] = implode(',', $query['_fields']); - } - } - - return $solrQuery; - }//end buildSolrQuery() - - /** - * Translate sort field to Solr format. - * - * @param array|string $order Sort specification - * - * @return string Solr sort string - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-10 - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - private function translateSortField(array|string $order): string - { - if (is_string($order) === true) { - return $order; - } - - $sortParts = []; - foreach ($order as $field => $direction) { - $dir = 'asc'; - if (strtolower((string) $direction) === 'desc') { - $dir = 'desc'; - } - - $sortParts[] = "{$field} {$dir}"; - } - - return implode(', ', $sortParts); - }//end translateSortField() - - /** - * Convert Solr response to OpenRegister paginated format. - * - * @param array $solrResult Solr search result - * @param array $query Original query - * - * @return array Paginated format with results and pagination info. - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-10 - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - private function convertToPaginatedFormat(array $solrResult, array $query): array - { - $response = $solrResult['response'] ?? []; - $docs = $response['docs'] ?? []; - $numFound = $response['numFound'] ?? 0; - $start = $response['start'] ?? 0; - - $limit = (int) ($query['_limit'] ?? 30); - $page = (int) ($query['_page'] ?? 1); - - $pages = 0; - if ($limit > 0) { - $pages = (int) ceil($numFound / $limit); - } - - return [ - 'results' => $docs, - 'total' => $numFound, - 'limit' => $limit, - 'offset' => $start, - 'page' => $page, - 'pages' => $pages, - ]; - }//end convertToPaginatedFormat() - - /** - * Inspect index with a query. - * - * @param string $query Solr query - * @param int $start Start offset - * @param int $rows Number of rows - * @param string $fields Fields to return - * - * @return array Inspection results - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-10 - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function inspectIndex( - string $query='*:*', - int $start=0, - int $rows=20, - string $fields='' - ): array { - $params = [ - 'q' => $query, - 'start' => $start, - 'rows' => $rows, - 'wt' => 'json', - ]; - - if (empty($fields) === false) { - $params['fl'] = $fields; - } - - return $this->search(params: $params); - }//end inspectIndex() - - /** - * Get statistics about the index. - * - * @return (bool|int|mixed|null|string)[] Statistics - * - * @psalm-return array{available: bool, collection: null|string, error?: string, documents?: 0|mixed, status?: 'OK'} - * - * @spec exclude thin stats wrapper — runs a rows=0 search and reports numFound - */ - public function getStats(): array - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - return [ - 'available' => false, - 'collection' => null, - ]; - } - - try { - // Get basic stats. - $result = $this->search(params: ['q' => '*:*', 'rows' => 0, 'wt' => 'json']); - - return [ - 'available' => true, - 'collection' => $collection, - 'documents' => $result['response']['numFound'] ?? 0, - 'status' => 'OK', - ]; - } catch (Exception $e) { - return [ - 'available' => false, - 'collection' => $collection, - 'error' => $e->getMessage(), - ]; - }//end try - }//end getStats() -}//end class diff --git a/lib/Service/Index/Backends/Solr/SolrSchemaManager.php b/lib/Service/Index/Backends/Solr/SolrSchemaManager.php deleted file mode 100644 index 3e10025ee3..0000000000 --- a/lib/Service/Index/Backends/Solr/SolrSchemaManager.php +++ /dev/null @@ -1,411 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends\Solr; - -use Exception; -use Psr\Log\LoggerInterface; - -/** - * SolrSchemaManager - * - * Manages Solr schema and fields. - * - * @category Service - * @package OCA\OpenRegister\Service\Index\Backends\Solr - */ -class SolrSchemaManager -{ - - /** - * HTTP client. - * - * @var SolrHttpClient - */ - private readonly SolrHttpClient $httpClient; - - /** - * Collection manager. - * - * @var SolrCollectionManager - */ - private readonly SolrCollectionManager $collectionManager; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param SolrHttpClient $httpClient HTTP client - * @param SolrCollectionManager $collectionManager Collection manager - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - SolrHttpClient $httpClient, - SolrCollectionManager $collectionManager, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->collectionManager = $collectionManager; - $this->logger = $logger; - }//end __construct() - - /** - * Get field types for a collection. - * - * @param string $collection Collection name - * - * @return array Field types indexed by name - * - * @spec exclude thin delegation to SolrHttpClient — GETs /schema/fieldtypes and indexes by name - */ - public function getFieldTypes(string $collection): array - { - try { - $url = $this->httpClient->getEndpointUrl($collection).'/schema/fieldtypes?wt=json'; - $data = $this->httpClient->get($url); - - $fieldTypes = []; - foreach (($data['fieldTypes'] ?? []) as $fieldType) { - $name = $fieldType['name'] ?? null; - if ($name !== null) { - $fieldTypes[$name] = $fieldType; - } - } - - $this->logger->debug( - message: '[SolrSchemaManager] Retrieved field types', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'count' => count($fieldTypes), - ] - ); - - return $fieldTypes; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrSchemaManager] Failed to get field types', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'error' => $e->getMessage(), - ] - ); - return []; - }//end try - }//end getFieldTypes() - - /** - * Add a field type to a collection. - * - * @param string $collection Collection name - * @param array $fieldType Field type definition - * - * @return bool True if successful - * - * @spec exclude thin delegation to SolrHttpClient — POSTs add-field-type command to /schema - */ - public function addFieldType(string $collection, array $fieldType): bool - { - try { - $this->logger->info( - message: '[SolrSchemaManager] Adding field type', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'name' => $fieldType['name'] ?? 'unknown', - ] - ); - - $url = $this->httpClient->getEndpointUrl($collection).'/schema'; - - $command = ['add-field-type' => $fieldType]; - - $result = $this->httpClient->post($url, $command); - - if (($result['responseHeader']['status'] ?? -1) === 0) { - $this->logger->info( - message: '[SolrSchemaManager] Field type added successfully', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return true; - } - - $this->logger->warning( - message: '[SolrSchemaManager] Field type addition returned non-zero status', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'status' => $result['responseHeader']['status'] ?? 'unknown', - ] - ); - - return false; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrSchemaManager] Failed to add field type', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end addFieldType() - - /** - * Get fields for a collection. - * - * @param string $collection Collection name - * - * @return array Fields indexed by name - * - * @spec exclude thin delegation to SolrHttpClient — GETs /schema/fields and indexes by name - */ - public function getFields(string $collection): array - { - try { - $url = $this->httpClient->getEndpointUrl($collection).'/schema/fields?wt=json'; - $data = $this->httpClient->get($url); - - $fields = []; - foreach (($data['fields'] ?? []) as $field) { - $name = $field['name'] ?? null; - if ($name !== null) { - $fields[$name] = $field; - } - } - - $this->logger->debug( - message: '[SolrSchemaManager] Retrieved fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'count' => count($fields), - ] - ); - - return $fields; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrSchemaManager] Failed to get fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'error' => $e->getMessage(), - ] - ); - return []; - }//end try - }//end getFields() - - /** - * Add or update a field in a collection. - * - * @param array $fieldConfig Field configuration - * @param bool $force Force update if exists - * - * @return string Action taken ('created', 'updated', 'skipped', 'failed') - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-2 - */ - public function addOrUpdateField(array $fieldConfig, bool $force): string - { - $collection = $this->collectionManager->getActiveCollectionName(); - - if ($collection === null) { - $this->logger->warning( - message: '[SolrSchemaManager] No active collection for field operation', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return 'failed'; - } - - try { - $fieldName = $fieldConfig['name'] ?? null; - - if ($fieldName === null) { - $this->logger->warning( - message: '[SolrSchemaManager] Field name not provided', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return 'failed'; - } - - // Check if field exists. - $existingFields = $this->getFields(collection: $collection); - - if (isset($existingFields[$fieldName]) === true) { - if ($force === false) { - $this->logger->debug( - message: '[SolrSchemaManager] Field already exists, skipping', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldName, - ] - ); - return 'skipped'; - } - - // Delete and recreate. - $this->deleteField(collection: $collection, fieldName: $fieldName); - } - - // Add field. - $url = $this->httpClient->getEndpointUrl($collection).'/schema'; - - $command = ['add-field' => $fieldConfig]; - - $result = $this->httpClient->post($url, $command); - - if (($result['responseHeader']['status'] ?? -1) === 0) { - $this->logger->info( - message: '[SolrSchemaManager] Field created', - context: ['file' => __FILE__, 'line' => __LINE__, 'field' => $fieldName] - ); - if (isset($existingFields[$fieldName]) === true) { - return 'updated'; - } - - return 'created'; - } - - $this->logger->warning( - message: '[SolrSchemaManager] Field creation returned non-zero status', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldName, - 'status' => $result['responseHeader']['status'] ?? 'unknown', - ] - ); - - return 'failed'; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrSchemaManager] Failed to add/update field', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return 'failed'; - }//end try - }//end addOrUpdateField() - - /** - * Delete a field from a collection. - * - * @param string $collection Collection name - * @param string $fieldName Field name - * - * @psalm-suppress UnusedReturnValue - * - * @return bool True if successful - */ - private function deleteField(string $collection, string $fieldName): bool - { - try { - $this->logger->info( - message: '[SolrSchemaManager] Deleting field', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'field' => $fieldName, - ] - ); - - $url = $this->httpClient->getEndpointUrl($collection).'/schema'; - - $command = ['delete-field' => ['name' => $fieldName]]; - - $result = $this->httpClient->post($url, $command); - - if (($result['responseHeader']['status'] ?? -1) === 0) { - $this->logger->info( - message: '[SolrSchemaManager] Field deleted', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return true; - } - - return false; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrSchemaManager] Failed to delete field', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end deleteField() - - /** - * Get schema configuration for a collection. - * - * @param string $collection Collection name - * - * @return array Schema configuration - * - * @spec exclude thin delegation to SolrHttpClient — GETs /schema and returns its body - */ - public function getSchema(string $collection): array - { - try { - $url = $this->httpClient->getEndpointUrl($collection).'/schema?wt=json'; - $data = $this->httpClient->get($url); - - return $data['schema'] ?? []; - } catch (Exception $e) { - $this->logger->error( - message: '[SolrSchemaManager] Failed to get schema', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'error' => $e->getMessage(), - ] - ); - return []; - }//end try - }//end getSchema() -}//end class diff --git a/lib/Service/Index/Backends/SolrBackend.php b/lib/Service/Index/Backends/SolrBackend.php deleted file mode 100644 index beef3eeb5c..0000000000 --- a/lib/Service/Index/Backends/SolrBackend.php +++ /dev/null @@ -1,690 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index\Backends; - -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Service\Index\SearchBackendInterface; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrHttpClient; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrCollectionManager; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrDocumentIndexer; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrQueryExecutor; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrFacetProcessor; -use OCA\OpenRegister\Service\Index\Backends\Solr\SolrSchemaManager; -use Psr\Log\LoggerInterface; - -/** - * SolrBackend - * - * Thin coordinator that implements SearchBackendInterface by delegating - * to specialized Solr service classes. Keeps this class under 500 lines. - * - * @category Service - * @package OCA\OpenRegister\Service\Index\Backends - * - * @SuppressWarnings(PHPMD.TooManyPublicMethods) Implements SearchBackendInterface with many required methods - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ -class SolrBackend implements SearchBackendInterface -{ - - /** - * HTTP client for Solr operations. - * - * @var SolrHttpClient - */ - private readonly SolrHttpClient $httpClient; - - /** - * Collection manager. - * - * @var SolrCollectionManager - */ - private readonly SolrCollectionManager $collectionManager; - - /** - * Document indexer. - * - * @var SolrDocumentIndexer - */ - private readonly SolrDocumentIndexer $indexer; - - /** - * Query executor. - * - * @var SolrQueryExecutor - */ - private readonly SolrQueryExecutor $queryExecutor; - - /** - * Facet processor. - * - * @var SolrFacetProcessor - */ - private readonly SolrFacetProcessor $facetProcessor; - - /** - * Schema manager. - * - * @var SolrSchemaManager - */ - private readonly SolrSchemaManager $schemaManager; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Constructor - * - * @param SolrHttpClient $httpClient HTTP client - * @param SolrCollectionManager $collectionManager Collection manager - * @param SolrDocumentIndexer $indexer Document indexer - * @param SolrQueryExecutor $queryExecutor Query executor - * @param SolrFacetProcessor $facetProcessor Facet processor - * @param SolrSchemaManager $schemaManager Schema manager - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - SolrHttpClient $httpClient, - SolrCollectionManager $collectionManager, - SolrDocumentIndexer $indexer, - SolrQueryExecutor $queryExecutor, - SolrFacetProcessor $facetProcessor, - SolrSchemaManager $schemaManager, - LoggerInterface $logger - ) { - $this->httpClient = $httpClient; - $this->collectionManager = $collectionManager; - $this->indexer = $indexer; - $this->queryExecutor = $queryExecutor; - $this->facetProcessor = $facetProcessor; - $this->schemaManager = $schemaManager; - $this->logger = $logger; - }//end __construct() - - /** - * Test if the backend is available. - * - * @param bool $forceRefresh Bypass cache - * - * @return bool True if available - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function isAvailable(bool $forceRefresh=false): bool - { - return $this->httpClient->isConfigured(); - }//end isAvailable() - - /** - * Test connection with diagnostics. - * - * @param bool $inclCollTests Include collection tests - * - * @return (bool|null|string)[] Test results - * - * @psalm-return array{success: bool, configured?: true, - * collection?: null|string, collection_exists?: bool, error?: 'Solr is not configured'} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade — checks isConfigured and active collection presence - */ - public function testConnection(bool $inclCollTests=true): array - { - if ($this->httpClient->isConfigured() === false) { - return [ - 'success' => false, - 'error' => 'Solr is not configured', - ]; - } - - $results = [ - 'success' => true, - 'configured' => true, - ]; - - if ($inclCollTests === true) { - $collection = $this->collectionManager->getActiveCollectionName(); - $results['collection'] = $collection; - $results['collection_exists'] = $collection !== null; - } - - return $results; - }//end testConnection() - - /** - * Index a single object. - * - * @param ObjectEntity $object Object to index - * @param bool $commit Commit immediately - * - * @return bool True if successful - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade delegation to SolrDocumentIndexer::indexObject - */ - public function indexObject(ObjectEntity $object, bool $commit=false): bool - { - return $this->indexer->indexObject( - object: $object, - commit: $commit - ); - }//end indexObject() - - /** - * Index multiple objects in bulk. - * - * @param array $objects Array of ObjectEntity - * @param bool $commit Commit immediately - * - * @return (bool|int|string)[] Result with statistics - * - * @psalm-return array{success: bool, indexed: int<0, max>, failed: int<0, max>, error?: string} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade delegation to SolrDocumentIndexer::bulkIndexObjects - */ - public function bulkIndexObjects(array $objects, bool $commit=true): array - { - return $this->indexer->bulkIndexObjects( - objects: $objects, - commit: $commit - ); - }//end bulkIndexObjects() - - /** - * Delete an object from the index. - * - * @param string|int $objectId Object ID - * @param bool $commit Commit immediately - * - * @return bool True if successful - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade delegation to SolrDocumentIndexer::deleteObject - */ - public function deleteObject(string|int $objectId, bool $commit=false): bool - { - return $this->indexer->deleteObject( - objectId: $objectId, - commit: $commit - ); - }//end deleteObject() - - /** - * Delete objects by query. - * - * @param string $query Query string - * @param bool $commit Commit immediately - * @param bool $returnDetails Return detailed results - * - * @return (array|bool|string)[]|bool Results - * - * @psalm-return array{success: bool, error?: string, query?: string, result?: array}|bool - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade delegation to SolrDocumentIndexer::deleteByQuery - */ - public function deleteByQuery(string $query, bool $commit=false, bool $returnDetails=false): array|bool - { - return $this->indexer->deleteByQuery( - query: $query, - commit: $commit, - returnDetails: $returnDetails - ); - }//end deleteByQuery() - - /** - * Search with pagination. - * - * @param array $query Query parameters - * @param bool $_rbac Apply RBAC - * @param bool $_multitenancy Apply multitenancy - * @param bool $deleted Include deleted - * - * @return array Search results with pagination info. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude facade delegation to SolrQueryExecutor::searchPaginated - */ - public function searchObjectsPaginated( - array $query=[], - bool $_rbac=true, - bool $_multitenancy=true, - bool $deleted=false - ): array { - return $this->queryExecutor->searchPaginated( - query: $query, - _rbac: $_rbac, - _multitenancy: $_multitenancy, - deleted: $deleted - ); - }//end searchObjectsPaginated() - - /** - * Get document count. - * - * @return int Document count - */ - public function getDocumentCount(): int - { - return $this->indexer->getDocumentCount(); - }//end getDocumentCount() - - /** - * Commit changes. - * - * @return bool True if successful - * - * @spec exclude facade delegation to SolrDocumentIndexer::commit - */ - public function commit(): bool - { - return $this->indexer->commit(); - }//end commit() - - /** - * Optimize the index. - * - * @return bool True if successful - * - * @spec exclude facade delegation to SolrDocumentIndexer::optimize - */ - public function optimize(): bool - { - return $this->indexer->optimize(); - }//end optimize() - - /** - * Clear the index. - * - * @param string|null $collectionName Collection to clear - * - * @return (bool|string)[] Results - * - * @psalm-return array{success: bool, message: string, collection?: string} - * - * @spec exclude facade delegation to SolrDocumentIndexer::clearIndex - */ - public function clearIndex(?string $collectionName=null): array - { - return $this->indexer->clearIndex($collectionName); - }//end clearIndex() - - /** - * Warm up the index. - * - * NOTE: Full warmup implementation with 200+ lines is in SolrBackend.php.old. - * This is a simplified version. Migrate full version if needed. - * - * @param array $schemas Schemas to warm up - * @param int $maxObjects Max objects - * @param string $mode Warmup mode - * @param bool $collectErrors Collect errors - * @param int $batchSize Batch size - * @param array $schemaIds Schema IDs - * - * @return (bool|string)[] Results - * - * @psalm-return array{success: true, message: 'Simplified warmup - collection exists', collection_exists: bool} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude simplified stub — returns collection-exists flag; full warmup deferred - */ - public function warmupIndex( - array $schemas=[], - int $maxObjects=0, - string $mode='serial', - bool $collectErrors=false, - int $batchSize=1000, - array $schemaIds=[] - ): array { - // Simplified warmup - just test connection. - // Full implementation is 200+ lines in SolrBackend.php.old. - $this->logger->info( - message: '[SolrBackend] Warmup requested (simplified version)', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'maxObjects' => $maxObjects, - 'mode' => $mode, - ] - ); - - return [ - 'success' => true, - 'message' => 'Simplified warmup - collection exists', - 'collection_exists' => $this->collectionManager->getActiveCollectionName() !== null, - ]; - }//end warmupIndex() - - /** - * Get backend configuration. - * - * @return array Configuration - */ - public function getConfig(): array - { - return $this->httpClient->getConfig(); - }//end getConfig() - - /** - * Get backend statistics. - * - * @return (bool|int|mixed|null|string)[] Statistics - * - * @psalm-return array{available: bool, collection: null|string, error?: string, documents?: 0|mixed, status?: 'OK'} - * - * @spec exclude facade delegation to SolrQueryExecutor::getStats - */ - public function getStats(): array - { - return $this->queryExecutor->getStats(); - }//end getStats() - - /** - * Create a collection. - * - * @param string $name Collection name - * @param array $config Configuration - * - * @return array Results with success status and collection info. - * - * @spec exclude facade delegation to SolrCollectionManager::createCollection - */ - public function createCollection(string $name, array $config=[]): array - { - return $this->collectionManager->createCollection( - name: $name, - config: $config - ); - }//end createCollection() - - /** - * Delete a collection. - * - * @param string|null $collectionName Collection name - * - * @return (bool|string)[] Results - * - * @psalm-return array{success: bool, message: string, exception?: string, collection?: string} - * - * @spec exclude facade delegation to SolrCollectionManager::deleteCollection - */ - public function deleteCollection(?string $collectionName=null): array - { - return $this->collectionManager->deleteCollection($collectionName); - }//end deleteCollection() - - /** - * Check if collection exists. - * - * @param string $collectionName Collection name - * - * @return bool True if exists - * - * @spec exclude facade delegation to SolrCollectionManager::collectionExists - */ - public function collectionExists(string $collectionName): bool - { - return $this->collectionManager->collectionExists($collectionName); - }//end collectionExists() - - /** - * List all collections. - * - * @return array Collection names - * - * @spec exclude facade delegation to SolrCollectionManager::listCollections - */ - public function listCollections(): array - { - return $this->collectionManager->listCollections(); - }//end listCollections() - - /** - * Index generic documents. - * - * @param array $documents Documents to index - * - * @return bool True if successful - * - * @spec exclude facade delegation to SolrDocumentIndexer::indexDocuments - */ - public function index(array $documents): bool - { - return $this->indexer->indexDocuments($documents); - }//end index() - - /** - * Run an aggregation against this Solr backend. - * - * The translator already exists at - * `Aggregation\SolrAggregationQueryBuilder` — this method is the - * HTTP-client adapter on top. Returns null until the dev container - * ships a Solr instance the runtime can talk to, so the caller - * falls back to the PHP path. Stub matches the interface contract. - * - * @param \OCA\OpenRegister\Service\Aggregation\AggregationQuery $query Portable aggregation request. - * - * @return array|null The aggregation result, or null when the backend cannot execute it. - * - * @spec exclude stub — returns null so caller falls back to PHP path (HTTP adapter not yet wired) - */ - public function aggregate(\OCA\OpenRegister\Service\Aggregation\AggregationQuery $query): ?array - { - return null; - }//end aggregate() - - /** - * Perform generic search. - * - * @param array $params Search parameters - * - * @return array Search results - * - * @spec exclude facade delegation to SolrQueryExecutor::search - */ - public function search(array $params): array - { - return $this->queryExecutor->search($params); - }//end search() - - /** - * Get field types for collection. - * - * @param string $collection Collection name - * - * @return array Field types - */ - public function getFieldTypes(string $collection): array - { - return $this->schemaManager->getFieldTypes($collection); - }//end getFieldTypes() - - /** - * Add field type. - * - * @param string $collection Collection name - * @param array $fieldType Field type definition - * - * @return bool True if successful - * - * @spec exclude facade delegation to SolrSchemaManager::addFieldType - */ - public function addFieldType(string $collection, array $fieldType): bool - { - return $this->schemaManager->addFieldType( - collection: $collection, - fieldType: $fieldType - ); - }//end addFieldType() - - /** - * Get fields for collection. - * - * @param string $collection Collection name - * - * @return array Fields - */ - public function getFields(string $collection): array - { - return $this->schemaManager->getFields($collection); - }//end getFields() - - /** - * Add or update field. - * - * @param array $fieldConfig Field configuration - * @param bool $force Force update - * - * @return string Action taken - * - * @spec exclude facade delegation to SolrSchemaManager::addOrUpdateField - */ - public function addOrUpdateField(array $fieldConfig, bool $force): string - { - return $this->schemaManager->addOrUpdateField( - fieldConfig: $fieldConfig, - force: $force - ); - }//end addOrUpdateField() - - /** - * Reindex all objects. - * - * NOTE: Full reindexAll is 300+ lines in SolrBackend.php.old. - * This is a simplified version. Migrate if needed. - * - * @param int $maxObjects Max objects - * @param int $batchSize Batch size - * @param string|null $collectionName Collection name - * - * @return (bool|int|string)[] - * - * @psalm-return array{success: bool, message: 'Index cleared (simplified reindex)', indexed: 0} - * - * @spec exclude simplified stub — clears index only; full reindex deferred to IndexService - */ - public function reindexAll(int $maxObjects=0, int $batchSize=1000, ?string $collectionName=null): array - { - $this->logger->info( - message: '[SolrBackend] Reindex requested (simplified version)', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'maxObjects' => $maxObjects, - 'batchSize' => $batchSize, - ] - ); - - // Simplified - clear and return. - // Full implementation is 300+ lines in SolrBackend.php.old. - $clearResult = $this->indexer->clearIndex($collectionName); - - return [ - 'success' => $clearResult['success'] ?? false, - 'message' => 'Index cleared (simplified reindex)', - 'indexed' => 0, - ]; - }//end reindexAll() - - /** - * Get raw Solr fields for facet configuration. - * - * Retrieves available fields from Solr for faceting. - * Delegates to facet processor. - * - * @return (mixed|string)[][] Facetable fields from Solr - * - * @psalm-return list - */ - public function getRawSolrFieldsForFacetConfiguration(): array - { - return $this->facetProcessor->getRawSolrFieldsForFacetConfiguration(); - }//end getRawSolrFieldsForFacetConfiguration() - - /** - * Get HTTP client instance. - * - * Provides access to the underlying HTTP client for advanced operations. - * - * @return SolrHttpClient HTTP client instance - */ - public function getHttpClient(): SolrHttpClient - { - return $this->httpClient; - }//end getHttpClient() - - /** - * Index files by their IDs. - * - * @param array $fileIds Array of file IDs to index. - * @param string|null $collectionName Optional collection name. - * - * @return array Indexing results. - * - * @spec exclude stub — returns empty result shape; file indexing handled by FileHandler (REQ-8) - */ - public function indexFiles(array $fileIds, ?string $collectionName=null): array - { - return ['indexed' => 0, 'failed' => 0, 'errors' => []]; - }//end indexFiles() - - /** - * Get file indexing statistics. - * - * @return array File indexing statistics. - */ - public function getFileIndexStats(): array - { - return []; - }//end getFileIndexStats() - - /** - * Fix mismatched fields in the search backend schema. - * - * @param array $mismatchedFields Array of mismatched fields. - * @param bool $dryRun Whether to preview changes only. - * - * @return array Results of the fix operation. - * - * @spec exclude stub — returns empty array; mismatch fixing handled by SchemaHandler (REQ-7) - */ - public function fixMismatchedFields(array $mismatchedFields, bool $dryRun=false): array - { - return []; - }//end fixMismatchedFields() -}//end class diff --git a/lib/Service/Index/BulkIndexer.php b/lib/Service/Index/BulkIndexer.php deleted file mode 100644 index 28300d056f..0000000000 --- a/lib/Service/Index/BulkIndexer.php +++ /dev/null @@ -1,443 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use Exception; -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Db\MagicMapper; -use OCA\OpenRegister\Db\SchemaMapper; -use OCP\IDBConnection; -use Psr\Log\LoggerInterface; -use RuntimeException; - -/** - * BulkIndexer - Business logic for bulk indexing operations - * - * RESPONSIBILITIES: - * - Fetch objects from database in batches - * - Orchestrate parallel processing - * - Optimize memory usage - * - Call backend.index() for actual indexing - * - * DOES NOT: - * - Make direct Solr/Elastic API calls (uses SearchBackendInterface) - * - Extract text (TextExtractionService handles that) - * - * @package OCA\OpenRegister\Service\Index - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -class BulkIndexer -{ - - /** - * Object entity mapper for DB queries. - * - * @var MagicMapper - */ - private readonly MagicMapper $objectMapper; - - /** - * Schema mapper for schema validation. - * - * @var SchemaMapper - */ - private readonly SchemaMapper $schemaMapper; - - /** - * Document builder for creating Solr documents. - * - * @var DocumentBuilder - */ - private readonly DocumentBuilder $documentBuilder; - - /** - * Search backend interface (Solr/Elastic abstraction). - * - * @var SearchBackendInterface - */ - private readonly SearchBackendInterface $searchBackend; - - /** - * Database connection for direct queries. - * - * @var IDBConnection - */ - private readonly IDBConnection $db; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * BulkIndexer constructor - * - * @param MagicMapper $objectMapper DB mapper for objects - * @param SchemaMapper $schemaMapper DB mapper for schemas - * @param DocumentBuilder $documentBuilder Document builder - * @param SearchBackendInterface $searchBackend Search backend (Solr/Elastic) - * @param IDBConnection $db Database connection - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - MagicMapper $objectMapper, - SchemaMapper $schemaMapper, - DocumentBuilder $documentBuilder, - SearchBackendInterface $searchBackend, - IDBConnection $db, - LoggerInterface $logger - ) { - $this->objectMapper = $objectMapper; - $this->schemaMapper = $schemaMapper; - $this->documentBuilder = $documentBuilder; - $this->searchBackend = $searchBackend; - $this->db = $db; - $this->logger = $logger; - }//end __construct() - - /** - * Bulk index objects (simple batch operation) - * - * This is a TEMPORARY wrapper that will be replaced with proper implementation. - * Currently just logs a warning that this method needs proper extraction. - * - * @param array $_objects Objects to index - * @param bool $_commit Whether to commit - * - * @return (false|string)[] Results - * - * @todo Extract implementation from SolrBackend - * - * @psalm-return array{success: false, message: 'Method not yet extracted to BulkIndexer'} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude TODO stub returning 'not yet extracted' — implemented path is bulkIndexFromDatabase (REQ-3) - */ - public function bulkIndexObjects(array $_objects, bool $_commit=true): array - { - $this->logger->warning( - message: '[BulkIndexer] bulkIndexObjects not yet fully extracted - needs implementation', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - return [ - 'success' => false, - 'message' => 'Method not yet extracted to BulkIndexer', - ]; - }//end bulkIndexObjects() - - /** - * Bulk index objects from database in batches - * - * BUSINESS LOGIC: Fetches objects from DB, creates documents, sends to backend. - * This is the core bulk indexing implementation extracted from SolrBackend. - * - * @param int $batchSize Batch size (default: 1000) - * @param int $maxObjects Max objects to process (0 = all) - * @param array $solrFieldTypes Field types for validation - * @param array $schemaIds Schema IDs to filter (empty = all searchable) - * - * @return array Result with success status, indexed count, and batch information. - * - * @throws \RuntimeException If indexing fails. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-3 - */ - public function bulkIndexFromDatabase( - int $batchSize=1000, - int $maxObjects=0, - array $solrFieldTypes=[], - array $schemaIds=[] - ): array { - // $schemaIds is guaranteed to be an array from function signature - // Check backend availability. - if ($this->searchBackend->isAvailable() === false) { - return [ - 'success' => false, - 'error' => 'Search backend is not available', - 'indexed' => 0, - 'batches' => 0, - ]; - } - - try { - $totalIndexed = 0; - $batchCount = 0; - $offset = 0; - $results = ['skipped_non_searchable' => 0]; - - $this->logger->info( - message: '[BulkIndexer] Starting bulk index from database', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - // Get count of searchable objects for planning. - $totalObjects = $this->countSearchableObjects(schemaIds: $schemaIds); - $estimatedBatches = ceil($totalObjects / $batchSize); - $willProcess = $totalObjects; - if ($maxObjects > 0) { - $estimatedBatches = ceil(min($totalObjects, $maxObjects) / $batchSize); - $willProcess = min($totalObjects, $maxObjects); - } - - $this->logger->info( - message: '[BulkIndexer] Planning bulk index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'totalSearchableObjects' => $totalObjects, - 'maxObjects' => $maxObjects, - 'batchSize' => $batchSize, - 'estimatedBatches' => $estimatedBatches, - 'willProcess' => $willProcess, - ] - ); - - do { - // Calculate current batch size (respect maxObjects limit). - $currentBatchSize = $batchSize; - if ($maxObjects > 0) { - $remaining = $maxObjects - $totalIndexed; - if ($remaining <= 0) { - break; - } - - $currentBatchSize = min($batchSize, $remaining); - } - - // Fetch batch of searchable objects from DB. - $fetchStart = microtime(true); - $objects = $this->fetchSearchableObjects( - limit: $currentBatchSize, - offset: $offset, - schemaIds: $schemaIds - ); - $objectsCount = count($objects); - - $fetchDuration = round((microtime(true) - $fetchStart) * 1000, 2); - $this->logger->info( - message: '[BulkIndexer] Batch fetched', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'batch' => $batchCount + 1, - 'objectsFound' => $objectsCount, - 'fetchTime' => $fetchDuration.'ms', - ] - ); - - if (empty($objects) === true) { - break; - } - - // Create documents from objects. - $documents = []; - foreach ($objects as $object) { - try { - $document = $this->documentBuilder->createDocument( - object: $object, - _solrFieldTypes: $solrFieldTypes - ); - $documents[] = $document; - } catch (\RuntimeException $e) { - if (str_contains($e->getMessage(), 'Schema is not searchable') === true) { - $results['skipped_non_searchable']++; - $this->logger->warning( - message: '[BulkIndexer] Unexpected non-searchable schema', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objectId' => $object->getId(), - 'error' => $e->getMessage(), - ] - ); - continue; - } - - throw $e; - } catch (\Exception $e) { - $this->logger->warning( - message: '[BulkIndexer] Failed to create document', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'objectId' => $object->getId(), - ] - ); - }//end try - }//end foreach - - // Send documents to backend. - $indexed = 0; - if (empty($documents) === false) { - $indexStart = microtime(true); - $this->searchBackend->index($documents); - $indexed = count($documents); - - $indexDuration = round((microtime(true) - $indexStart) * 1000, 2); - $this->logger->debug( - message: '[BulkIndexer] Batch indexed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'documents' => $indexed, - 'indexTime' => $indexDuration.'ms', - ] - ); - } - - $batchCount++; - $totalIndexed += $indexed; - $offset += $currentBatchSize; - } while ($objectsCount === $currentBatchSize && ($maxObjects === 0 || $totalIndexed < $maxObjects)); - - $this->logger->info( - message: '[BulkIndexer] Bulk indexing completed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'totalIndexed' => $totalIndexed, - 'totalBatches' => $batchCount, - 'batchSize' => $batchSize, - ] - ); - - return [ - 'success' => true, - 'indexed' => $totalIndexed, - 'batches' => $batchCount, - 'batch_size' => $batchSize, - 'skipped_non_searchable' => $results['skipped_non_searchable'] ?? 0, - ]; - } catch (\Exception $e) { - $this->logger->error( - message: '[BulkIndexer] Bulk indexing failed', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - $indexed = ($totalIndexed ?? 0); - $batches = ($batchCount ?? 0); - $msg = 'Bulk indexing failed: '.$e->getMessage().' (Indexed: '.$indexed.', Batches: '.$batches.')'; - throw new RuntimeException($msg, 0, $e); - }//end try - }//end bulkIndexFromDatabase() - - /** - * Count searchable objects in database - * - * @param array $schemaIds Schema IDs to filter - * - * @return int Count of searchable objects - */ - private function countSearchableObjects(array $schemaIds=[]): int - { - // Get searchable schema IDs. - $searchableSchemaIds = $this->getSearchableSchemaIds(schemaIds: $schemaIds); - - if (empty($searchableSchemaIds) === true) { - return 0; - } - - // Count objects with searchable schemas. - return $this->objectMapper->countBySchemas($searchableSchemaIds); - }//end countSearchableObjects() - - /** - * Fetch searchable objects from database - * - * @param int $limit Number of objects to fetch - * @param int $offset Offset for pagination - * @param array $schemaIds Schema IDs to filter - * - * @return ObjectEntity[] - * - * @psalm-return list - */ - private function fetchSearchableObjects(int $limit, int $offset, array $schemaIds=[]): array - { - // Get searchable schema IDs. - $searchableSchemaIds = $this->getSearchableSchemaIds(schemaIds: $schemaIds); - - if (empty($searchableSchemaIds) === true) { - return []; - } - - // Fetch objects with searchable schemas. - return $this->objectMapper->findBySchemas(schemaIds: $searchableSchemaIds, limit: $limit, offset: $offset); - }//end fetchSearchableObjects() - - /** - * Get IDs of searchable schemas - * - * @param array $schemaIds Schema IDs to filter (empty = all searchable) - * - * @return (int|string)[] Array of searchable schema IDs - * - * @psalm-return list - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Schema filtering requires handling multiple scenarios - */ - private function getSearchableSchemaIds(array $schemaIds=[]): array - { - // If specific schemas requested, filter for searchable ones. - if (empty($schemaIds) === false) { - $searchableIds = []; - foreach ($schemaIds as $schemaId) { - try { - $schema = $this->schemaMapper->find($schemaId); - if ($schema->getSearchable() === true) { - $searchableIds[] = $schemaId; - } - } catch (\Exception $e) { - $this->logger->warning( - message: '[BulkIndexer] Schema not found', - context: ['file' => __FILE__, 'line' => __LINE__, 'schemaId' => $schemaId] - ); - } - } - - return $searchableIds; - } - - // Get all searchable schemas. - $allSchemas = $this->schemaMapper->findAll(); - $searchableIds = []; - foreach ($allSchemas as $schema) { - if ($schema->getSearchable() === true) { - $searchableIds[] = $schema->getId(); - } - } - - return $searchableIds; - }//end getSearchableSchemaIds() -}//end class diff --git a/lib/Service/Index/ConfigurationHandler.php b/lib/Service/Index/ConfigurationHandler.php deleted file mode 100644 index 7e1c5ba62e..0000000000 --- a/lib/Service/Index/ConfigurationHandler.php +++ /dev/null @@ -1,335 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use GuzzleHttp\Client as GuzzleClient; -use OCA\OpenRegister\Service\SettingsService; -use Psr\Log\LoggerInterface; -use Exception; - -/** - * Configuration Handler for Index Service - * - * Manages all configuration-related operations for the search backend including: - * - Loading and validating configuration - * - Initializing HTTP clients - * - Building connection URLs - * - Configuration status checks - * - * @category Handler - * @package OCA\OpenRegister\Service\Index - */ -class ConfigurationHandler -{ - - /** - * Solr configuration array. - * - * @var array{enabled: bool, host?: string, port?: int|string, core?: string, - * path?: string, username?: string, password?: string, scheme?: string} - */ - private array $solrConfig = ['enabled' => false]; - - /** - * HTTP client for Solr communication. - * - * @var GuzzleClient|null - */ - private ?GuzzleClient $httpClient = null; - - /** - * Constructor for ConfigurationHandler. - * - * @param SettingsService $settingsService Service for retrieving settings. - * @param LoggerInterface $logger Logger for configuration events. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-4 - */ - public function __construct( - private readonly SettingsService $settingsService, - private readonly LoggerInterface $logger - ) { - $this->initializeConfig(); - $this->initializeHttpClient(); - }//end __construct() - - /** - * Initialize SOLR configuration. - * - * Loads configuration from SettingsService and validates it. - * - * @return void - */ - private function initializeConfig(): void - { - try { - // @psalm-suppress InvalidPropertyAssignmentValue - getSolrSettings() returns array with compatible shape. - $this->solrConfig = $this->settingsService->getSolrSettings(); - } catch (Exception $e) { - $this->logger->warning( - message: '[ConfigurationHandler] Failed to load SOLR settings', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - /* - * @psalm-suppress InvalidPropertyAssignmentValue - ['enabled' => false] is compatible with solrConfig type - */ - - $this->solrConfig = ['enabled' => false]; - } - }//end initializeConfig() - - /** - * Initialize HTTP client with authentication support. - * - * Creates a Guzzle HTTP client configured with: - * - Timeouts for connection and requests. - * - SSL certificate verification settings. - * - HTTP Basic Authentication if credentials are configured. - * - * @return void - */ - private function initializeHttpClient(): void - { - // Prepare Guzzle client configuration. - // Allow self-signed certificates. - // Don't throw exceptions for 4xx/5xx responses. - $clientConfig = [ - 'timeout' => 30, - 'connect_timeout' => 10, - 'verify' => false, - 'allow_redirects' => true, - 'http_errors' => false, - ]; - - // Add HTTP Basic Authentication if credentials are provided. - if (empty($this->solrConfig['username']) === false && empty($this->solrConfig['password']) === false) { - $clientConfig['auth'] = [ - $this->solrConfig['username'], - $this->solrConfig['password'], - 'basic', - ]; - - $this->logger->info( - message: '[ConfigurationHandler] HTTP Basic Authentication configured', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'username' => $this->solrConfig['username'], - 'auth_type' => 'basic', - ] - ); - } - - // TODO: Switch back to Nextcloud HTTP client when local access restrictions are properly configured. - // Currently using direct Guzzle client to bypass Nextcloud's 'allow_local_address' restrictions. - // Future improvement: $this->httpClient = $clientService->newClient(['allow_local_address' => true]). - // This is necessary for SOLR/Zookeeper connections in Kubernetes environments. - $this->httpClient = new GuzzleClient($clientConfig); - }//end initializeHttpClient() - - /** - * Check if Solr is properly configured. - * - * Validates that all required configuration parameters are present. - * - * @return bool True if Solr is configured, false otherwise. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-4 - */ - public function isSolrConfigured(): bool - { - // Check if SOLR is enabled in settings. - if (($this->solrConfig['enabled'] ?? false) !== true) { - return false; - } - - // Check if required configuration parameters are present. - if (empty($this->solrConfig['host']) === true || empty($this->solrConfig['core']) === true) { - return false; - } - - return true; - }//end isSolrConfigured() - - /** - * Get tenant-specific collection name (legacy method, currently returns base name). - * - * Previously handled multi-tenancy by appending tenant identifiers. - * Now simplified to return the base collection name. - * - * @param string $baseCollectionName Base collection name. - * - * @return string Collection name. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-4 - */ - public function getTenantSpecificCollectionName(string $baseCollectionName): string - { - // Simply return the collection name without any tenant suffix. - return $baseCollectionName; - }//end getTenantSpecificCollectionName() - - /** - * Build SOLR base URL from configuration. - * - * Constructs the base URL for Solr API calls using the configured - * host, port, and path settings. - * - * @return string SOLR base URL. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-4 - */ - public function buildSolrBaseUrl(): string - { - $host = $this->solrConfig['host'] ?? 'localhost'; - $port = $this->solrConfig['port'] ?? null; - - // Normalize port - convert string '0' to null, handle empty strings. - if ($port === '0' || $port === '' || $port === null) { - $port = null; - } - - if ($port !== null) { - $port = (int) $port; - - if ($port === 0) { - $port = null; - } - } - - // Allow custom path for reverse proxies or non-standard setups. - $path = $this->solrConfig['path'] ?? ''; - if (empty($path) === false) { - $path = '/'.ltrim($path, '/'); - } - - // Build protocol-relative or absolute URL based on configuration. - $scheme = $this->solrConfig['scheme'] ?? 'http'; - - // If port is specified, include it in the URL. - if ($port !== null) { - return sprintf('%s://%s:%d%s', $scheme, $host, $port, $path); - } - - // No port specified - use default for the scheme. - return sprintf('%s://%s%s', $scheme, $host, $path); - }//end buildSolrBaseUrl() - - /** - * Get the configured HTTP client. - * - * @return GuzzleClient|null HTTP client instance. - */ - public function getHttpClient(): GuzzleClient|null - { - return $this->httpClient; - }//end getHttpClient() - - /** - * Get the Solr configuration array. - * - * @return (bool|int|string)[] Solr configuration. - * - * @psalm-return array{enabled: bool, host?: string, port?: int|string, core?: string, - * path?: string, username?: string, password?: string, scheme?: string} - */ - public function getSolrConfig(): array - { - return $this->solrConfig; - }//end getSolrConfig() - - /** - * Get the endpoint URL for a specific collection. - * - * @param string|null $collection Optional collection name. - * - * @return string Full endpoint URL. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-4 - */ - public function getEndpointUrl(?string $collection=null): string - { - $baseUrl = $this->buildSolrBaseUrl(); - $core = $collection ?? $this->solrConfig['core'] ?? 'openregister'; - - return $baseUrl.'/solr/'.$core; - }//end getEndpointUrl() - - /** - * Get configuration status for a specific setting. - * - * @param string $key Configuration key to check. - * - * @return string Status description. - * - * @psalm-return '✓ Configured'|'✗ Not configured' - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-4 - */ - public function getConfigStatus(string $key): string - { - if (isset($this->solrConfig[$key]) === true && empty($this->solrConfig[$key]) === false) { - return '✓ Configured'; - } - - return '✗ Not configured'; - }//end getConfigStatus() - - /** - * Get port configuration status. - * - * @return string Port status description. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-4 - */ - public function getPortStatus(): string - { - $port = $this->solrConfig['port'] ?? null; - - if ($port === null || $port === '' || $port === '0') { - return '✓ Using default port'; - } - - return '✓ Port '.$port; - }//end getPortStatus() - - /** - * Get core configuration status. - * - * @return string Core status description. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-4 - */ - public function getCoreStatus(): string - { - $core = $this->solrConfig['core'] ?? 'openregister'; - return '✓ Core: '.$core; - }//end getCoreStatus() -}//end class diff --git a/lib/Service/Index/DocumentBuilder.php b/lib/Service/Index/DocumentBuilder.php deleted file mode 100644 index 3e26bf16b4..0000000000 --- a/lib/Service/Index/DocumentBuilder.php +++ /dev/null @@ -1,889 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use Exception; -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Db\SchemaMapper; -use OCA\OpenRegister\Db\RegisterMapper; -use Psr\Log\LoggerInterface; - -/** - * DocumentBuilder for creating Solr documents - * - * Handles conversion of ObjectEntity instances to Solr documents. - * - * @package OCA\OpenRegister\Service\Index - * - * @SuppressWarnings(PHPMD.TooManyPublicMethods) Many document building utility methods required - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complex document transformation logic - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -class DocumentBuilder -{ - - /** - * Logger for operation tracking. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * Schema mapper for resolving schemas. - * - * @var SchemaMapper|null - */ - private readonly ?SchemaMapper $schemaMapper; - - /** - * Register mapper for resolving registers. - * - * @var RegisterMapper|null - */ - private readonly ?RegisterMapper $registerMapper; - - /** - * DocumentBuilder constructor - * - * @param LoggerInterface $logger Logger - * @param SchemaMapper|null $schemaMapper Schema mapper - * @param RegisterMapper|null $registerMapper Register mapper - * - * @return void - */ - public function __construct( - LoggerInterface $logger, - ?SchemaMapper $schemaMapper=null, - ?RegisterMapper $registerMapper=null - ) { - $this->logger = $logger; - $this->schemaMapper = $schemaMapper; - $this->registerMapper = $registerMapper; - }//end __construct() - - /** - * Create a Solr document from an ObjectEntity - * - * Builds a basic Solr document from the object's data. - * This is a simplified implementation to break circular dependencies. - * - * @param ObjectEntity $object The object to convert - * @param array $_solrFieldTypes Available Solr field types (unused for now) - * - * @return (false|int|mixed|null|string)[] - * - * @psalm-return array{_text: false|string,...} - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Document creation requires handling multiple data types - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-6 - */ - public function createDocument( - ObjectEntity $object, - array $_solrFieldTypes=[] - ): array { - $this->logger->debug( - message: '[DocumentBuilder] Creating basic Solr document', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $object->getId(), - ] - ); - - // Build basic Solr document from object. - $doc = [ - 'id' => (string) $object->getUuid(), - 'object_id' => $object->getId(), - 'uuid' => $object->getUuid(), - 'schema' => $object->getSchema(), - 'register' => $object->getRegister(), - 'created' => $object->getCreated()?->format('Y-m-d\TH:i:s\Z'), - 'updated' => $object->getUpdated()?->format('Y-m-d\TH:i:s\Z'), - ]; - - // Add object data. - $objectData = $object->getObject(); - if (is_array($objectData) === true) { - foreach ($objectData as $key => $value) { - // Skip null values. - if ($value === null) { - continue; - } - - // Convert value to Solr-compatible format. - $doc[$key] = $this->convertValueForSolr(value: $value, fieldType: 'auto'); - } - } - - // Add searchable text field. - $doc['_text'] = json_encode($objectData); - - return $doc; - }//end createDocument() - - // ======================================================================== - // EXTRACTED METHODS - Migrated from SolrBackend - // ======================================================================== - - /** - * Flatten relations array for SOLR - extract all values from relations key-value pairs - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param mixed $relations Relations data from ObjectEntity (e.g., {"modules.0":"uuid", "other.1":"value"}) - * - * @return string[] - * - * @psalm-return list - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Relations flattening requires handling multiple data formats - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-2 - */ - public function flattenRelationsForSolr($relations): array - { - // **DEBUG**: Log what we're processing. - $this->logger->debug( - message: '[DocumentBuilder] Processing relations for SOLR', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'relations_type' => gettype($relations), - 'relations_value' => $relations, - 'is_empty' => empty($relations), - ] - ); - - if (empty($relations) === true) { - return []; - } - - if (is_array($relations) === true) { - $values = []; - foreach ($relations as $key => $value) { - // **FIXED**: Extract ALL values from relations array, not just UUIDs. - // Relations are stored as {"modules.0":"value"} - we want all the values. - if (is_string($value) === true || is_numeric($value) === true) { - $values[] = (string) $value; - $this->logger->debug( - message: '[DocumentBuilder] Found value in relations', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'key' => $key, - 'value' => $value, - 'type' => gettype($value), - ] - ); - } - - // Skip arrays, objects, null values, etc. - } - - $this->logger->debug( - message: '[DocumentBuilder] Flattened relations result', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'input_count' => count($relations), - 'output_count' => count($values), - 'values' => $values, - ] - ); - - return $values; - }//end if - - // Single value - convert to string. - if (is_string($relations) === true || is_numeric($relations) === true) { - return [(string) $relations]; - } - - return []; - }//end flattenRelationsForSolr() - - /** - * Flatten files array for SOLR to prevent document multiplication - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param mixed $files Files data from ObjectEntity - * - * @return (mixed|string)[] Simple array of strings for SOLR multi-valued field - * - * @psalm-return list - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Files flattening requires handling multiple data formats - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-1 - */ - public function flattenFilesForSolr($files): array - { - if (empty($files) === true) { - return []; - } - - if (is_array($files) === true) { - $flattened = []; - foreach ($files as $file) { - if (is_string($file) === true) { - $flattened[] = $file; - } else if (is_array($file) === true && (($file['id'] ?? null) !== null)) { - $flattened[] = (string) $file['id']; - } else if (is_array($file) === true && (($file['uuid'] ?? null) !== null)) { - $flattened[] = $file['uuid']; - } - } - - return $flattened; - } - - if (is_string($files) === true) { - return [$files]; - } - - return []; - }//end flattenFilesForSolr() - - /** - * Extract ID/UUID from an object/array - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param array $object Object/array to extract ID from - * - * @return string|null Extracted ID or null if not found - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-1 - */ - public function extractIdFromObject(array $object): ?string - { - // Try common ID field names in order of preference. - $idFields = ['id', 'uuid', 'identifier', 'key', 'value']; - - foreach ($idFields as $field) { - if (($object[$field] ?? null) !== null && is_string($object[$field]) === true) { - return $object[$field]; - } - } - - // If no ID field found, return null. - return null; - }//end extractIdFromObject() - - /** - * Extract array fields from dot-notation relations - * - * MIGRATED from SolrBackend - now maintained here. - * - * WORKAROUND/HACK FOR MISSING DATA: This method reconstructs arrays from relations - * because some array fields (e.g., 'standaarden') are stored ONLY as dot-notation - * relation entries ("standaarden.0", "standaarden.1") instead of in the object body. - * - * @param array $relations The relations array from ObjectEntity - * - * @return array[] Associative array of field names to their array values - * - * @psalm-return array> - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Dot-notation parsing requires handling multiple scenarios - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-1 - */ - public function extractArraysFromRelations(array $relations): array - { - $arrays = []; - - // Group relations by their base field name (before the dot). - foreach ($relations as $relationKey => $relationValue) { - // Check if this is a dot-notation array relation (e.g., "standaarden.0"). - if (str_contains($relationKey, '.') === true) { - $parts = explode('.', $relationKey, 2); - $fieldName = $parts[0]; - $index = $parts[1]; - - // Initialize array if not exists. - if (isset($arrays[$fieldName]) === false) { - $arrays[$fieldName] = []; - } - - // Add value at the specified index (or skip if index is not numeric). - if (is_numeric($index) === true) { - $arrays[$fieldName][(int) $index] = $relationValue; - continue; - } - - // Non-numeric index - this is a nested object property, not an array element. - $this->logger->debug( - message: '[DocumentBuilder] Skipping non-numeric array index in relations', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'relation_key' => $relationKey, - 'field_name' => $fieldName, - 'index' => $index, - ] - ); - }//end if - }//end foreach - - // Sort each array by index and re-index to sequential keys. - foreach ($arrays as $fieldName => &$arrayValues) { - ksort($arrayValues); - // Re-index to sequential numeric keys (0, 1, 2, ...). - $arrayValues = array_values($arrayValues); - } - - $this->logger->debug( - message: '[DocumentBuilder] Extracted arrays from relations', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field_count' => count($arrays), - 'fields' => array_keys($arrays), - 'total_values' => array_sum(array_map('count', $arrays)), - ] - ); - - return $arrays; - }//end extractArraysFromRelations() - - /** - * Extract indexable values from an array for SOLR indexing - * - * MIGRATED from SolrBackend - now maintained here. - * - * This method intelligently handles mixed arrays by inspecting the actual content - * rather than relying on schema definitions, which may not match runtime data. - * - * @param array $arrayValue The array to extract values from - * @param string $fieldName Field name for logging - * - * @return string[] Array of indexable string values - * - * @psalm-return list - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Value extraction requires handling multiple data types - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-1 - */ - public function extractIndexableArrayValues(array $arrayValue, string $fieldName): array - { - $extractedValues = []; - - foreach ($arrayValue as $item) { - if (is_string($item) === true) { - // Direct string value - use as-is. - $extractedValues[] = $item; - } else if (is_array($item) === true) { - // Object/array - try to extract ID/UUID. - $idValue = $this->extractIdFromObject(object: $item); - if ($idValue !== null) { - $extractedValues[] = $idValue; - } - } else if (is_scalar($item) === true) { - // Other scalar values (int, float, bool) - convert to string. - $extractedValues[] = (string) $item; - } - - // Skip null values and complex objects. - } - - $this->logger->debug( - message: '[DocumentBuilder] Extracted indexable array values', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldName, - 'original_count' => count($arrayValue), - 'extracted_count' => count($extractedValues), - 'extracted_values' => $extractedValues, - ] - ); - - return $extractedValues; - }//end extractIndexableArrayValues() - - /** - * Map field name and type to appropriate SOLR field name - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param string $fieldName Original field name - * @param string $_fieldType Schema field type (unused) - * @param mixed $_fieldValue Field value for context (unused) - * - * @return string|null SOLR field name or null if should be skipped - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-7 - */ - public function mapFieldToSolrType(string $fieldName, string $_fieldType, $_fieldValue): ?string - { - // Avoid conflicts with core SOLR fields and self_ metadata fields. - $reservedFields = ['id', 'tenant_id', '_version_']; - if (in_array($fieldName, $reservedFields) === true || str_starts_with($fieldName, 'self_') === true) { - return null; - } - - // **CLEAN FIELD NAMES**: Return field name as-is since we define proper types in SOLR setup. - return $fieldName; - }//end mapFieldToSolrType() - - /** - * Convert value to appropriate format for SOLR - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param mixed $value Field value - * @param string $fieldType Schema field type - * - * @return mixed Converted value for SOLR - * - * @SuppressWarnings(PHPMD.StaticAccess) DateTime::createFromFormat is standard PHP date pattern - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-7 - */ - public function convertValueForSolr($value, string $fieldType) - { - if ($value === null) { - return null; - } - - switch (strtolower($fieldType)) { - case 'integer': - case 'int': - // **SAFE NUMERIC CONVERSION**: Handle non-numeric strings gracefully. - if (is_numeric($value) === true) { - return (int) $value; - } - - // Skip non-numeric values for integer fields. - $this->logger->debug( - message: '[DocumentBuilder] Skipping non-numeric value for integer field', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'value' => $value, - 'field_type' => $fieldType, - ] - ); - return null; - - case 'float': - case 'double': - case 'number': - // **SAFE NUMERIC CONVERSION**: Handle non-numeric strings gracefully. - if (is_numeric($value) === true) { - return (float) $value; - } - - // Skip non-numeric values for float fields. - $this->logger->debug( - message: '[DocumentBuilder] Skipping non-numeric value for float field', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'value' => $value, - 'field_type' => $fieldType, - ] - ); - return null; - - case 'boolean': - case 'bool': - return (bool) $value; - - case 'date': - case 'datetime': - if ($value instanceof \DateTime) { - return $value->format('Y-m-d\\TH:i:s\\Z'); - } - - if (is_string($value) === true) { - $date = \DateTime::createFromFormat('Y-m-d H:i:s', $value); - if ($date !== false) { - return $date->format('Y-m-d\\TH:i:s\\Z'); - } - - return $value; - } - return $value; - - case 'array': - if (is_array($value) === true) { - return $value; - } - return [$value]; - - default: - return (string) $value; - }//end switch - }//end convertValueForSolr() - - /** - * Truncate field value to respect SOLR's byte limit - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param mixed $value Field value - * @param string $fieldName Field name for logging - * - * @return mixed Truncated value or original if within limits - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-7 - */ - public function truncateFieldValue($value, string $fieldName=''): mixed - { - // Only truncate string values. - if (is_string($value) === false) { - return $value; - } - - // SOLR's byte limit for indexed string fields. - $maxBytes = 32766; - - // Check if value exceeds byte limit (UTF-8 safe). - if (strlen($value) <= $maxBytes) { - return $value; - } - - // **TRUNCATE SAFELY**: Ensure we don't break UTF-8 characters. - $truncated = mb_strcut($value, 0, $maxBytes - 100, 'UTF-8'); - // Leave buffer for safety. - // Add truncation indicator. - $truncated .= '...[TRUNCATED]'; - - // Log truncation for monitoring. - $this->logger->info( - message: '[DocumentBuilder] Field value truncated for SOLR indexing', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldName, - 'original_bytes' => strlen($value), - 'truncated_bytes' => strlen($truncated), - 'truncation_point' => $maxBytes - 100, - ] - ); - - return $truncated; - }//end truncateFieldValue() - - /** - * Check if a field should be truncated based on schema definition - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param string $fieldName Field name - * @param array $fieldDefinition Schema field definition (if available) - * - * @return bool True if field should be truncated - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-7 - */ - public function shouldTruncateField(string $fieldName, array $fieldDefinition=[]): bool - { - $type = $fieldDefinition['type'] ?? ''; - $format = $fieldDefinition['format'] ?? ''; - - // File fields should always be truncated. - if ($type === 'file' || $format === 'file' || $format === 'binary' - || in_array($format, ['data-url', 'base64', 'image', 'document']) === true - ) { - return true; - } - - // Fields that commonly contain large content. - $largeContentFields = ['logo', 'image', 'icon', 'thumbnail', 'content', 'body', 'description']; - if (in_array(strtolower($fieldName), $largeContentFields) === true) { - return true; - } - - // Base64 data URLs (common pattern). - if (is_string($fieldName) === true && str_contains(strtolower($fieldName), 'base64') === true) { - return true; - } - - return false; - }//end shouldTruncateField() - - /** - * Validate field for SOLR indexing - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param string $fieldName Field name - * @param mixed $fieldValue Field value - * @param array $solrFieldTypes Available SOLR field types - * - * @return bool True if field is safe to index - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Field validation requires handling multiple type scenarios - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-1 - */ - public function validateFieldForSolr(string $fieldName, $fieldValue, array $solrFieldTypes): bool - { - // If no field types provided, allow all (fallback to original behavior). - if (empty($solrFieldTypes) === true) { - return true; - } - - // If field doesn't exist in SOLR, it will be auto-created (allow). - if (isset($solrFieldTypes[$fieldName]) === false) { - $this->logger->debug( - message: '[DocumentBuilder] Field not in SOLR schema, will be auto-created', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldName, - 'value' => $fieldValue, - 'type' => gettype($fieldValue), - ] - ); - return true; - } - - $solrFieldType = $solrFieldTypes[$fieldName]; - - // **CRITICAL VALIDATION**: Check for type compatibility. - $isCompatible = $this->isValueCompatibleWithSolrType(value: $fieldValue, solrFieldType: $solrFieldType); - - if ($isCompatible === false) { - $this->logger->warning( - message: '[DocumentBuilder] 🛡️ Field validation prevented type mismatch', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldName, - 'value' => $fieldValue, - 'value_type' => gettype($fieldValue), - 'solr_field_type' => $solrFieldType, - 'action' => 'SKIPPED', - ] - ); - return false; - } - - $this->logger->debug( - message: '[DocumentBuilder] ✅ Field validation passed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldName, - 'value' => $fieldValue, - 'solr_type' => $solrFieldType, - ] - ); - - return true; - }//end validateFieldForSolr() - - /** - * Check if a value is compatible with a SOLR field type - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param mixed $value The value to check - * @param string $solrFieldType The SOLR field type - * - * @return bool True if compatible - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Type compatibility check requires handling multiple SOLR types - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple type combinations create many execution paths - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-1 - */ - public function isValueCompatibleWithSolrType($value, string $solrFieldType): bool - { - // Handle null values (generally allowed). - if ($value === null) { - return true; - } - - // **FIXED**: Handle arrays for multi-valued fields. - if (is_array($value) === true) { - // Empty arrays are always allowed for multi-valued fields. - if (empty($value) === true) { - return true; - } - - // Check each element in the array against the base field type. - foreach ($value as $element) { - if ($this->isValueCompatibleWithSolrType(value: $element, solrFieldType: $solrFieldType) === false) { - return false; - } - } - - return true; - } - - return match ($solrFieldType) { - // Numeric types - only allow numeric values. - 'pint', 'plong', 'plongs', 'pfloat', 'pdouble' => is_numeric($value), - - // String types - allow anything (can be converted to string). - 'string', 'text_general', 'text_en' => true, - - // Boolean types - allow boolean or boolean-like values. - 'boolean' => is_bool($value) || in_array(strtolower((string) $value), ['true', 'false', '1', '0']), - - // Date types - allow date strings or objects. - 'pdate', 'pdates' => is_string($value) || ($value instanceof \DateTime), - - // Default: allow for unknown types. - default => true, - }; - }//end isValueCompatibleWithSolrType() - - // ======================================================================== - // RESOLVER METHODS - ID Resolution - // ======================================================================== - - /** - * Resolve register value to integer ID - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param mixed $registerValue The register value - * @param \OCA\OpenRegister\Db\Register|null $register Pre-loaded register entity - * - * @return int The resolved register ID - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Register resolution requires handling multiple input formats - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-1 - */ - public function resolveRegisterToId($registerValue, ?\OCA\OpenRegister\Db\Register $register=null): int - { - if (empty($registerValue) === true) { - return 0; - } - - // If it's already a numeric ID, return it as integer. - if (is_numeric($registerValue) === true) { - return (int) $registerValue; - } - - // If we have a pre-loaded register entity, use its ID. - if ($register !== null) { - return $register->getId() ?? 0; - } - - // Try to resolve by slug/name using RegisterMapper. - if ($this->registerMapper !== null) { - try { - $resolvedRegister = $this->registerMapper->find($registerValue); - return $resolvedRegister->getId() ?? 0; - } catch (Exception $e) { - $this->logger->warning( - message: '[DocumentBuilder] Failed to resolve register value to ID', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'registerValue' => $registerValue, - 'error' => $e->getMessage(), - ] - ); - } - } - - // Fallback: return 0 for unresolvable values. - $this->logger->warning( - message: '[DocumentBuilder] Could not resolve register to integer ID', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'registerValue' => $registerValue, - 'type' => gettype($registerValue), - ] - ); - return 0; - }//end resolveRegisterToId() - - /** - * Resolve schema value to integer ID - * - * MIGRATED from SolrBackend - now maintained here. - * - * @param mixed $schemaValue The schema value - * @param \OCA\OpenRegister\Db\Schema|null $schema Pre-loaded schema entity - * - * @return int The resolved schema ID - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Schema resolution requires handling multiple input formats - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-1 - */ - public function resolveSchemaToId($schemaValue, ?\OCA\OpenRegister\Db\Schema $schema=null): int - { - if (empty($schemaValue) === true) { - return 0; - } - - // If it's already a numeric ID, return it as integer. - if (is_numeric($schemaValue) === true) { - return (int) $schemaValue; - } - - // If we have a pre-loaded schema entity, use its ID. - if ($schema !== null) { - return $schema->getId() ?? 0; - } - - // Try to resolve by slug/name using SchemaMapper. - if ($this->schemaMapper !== null) { - try { - $resolvedSchema = $this->schemaMapper->find($schemaValue); - return $resolvedSchema->getId() ?? 0; - } catch (Exception $e) { - $this->logger->warning( - message: '[DocumentBuilder] Failed to resolve schema value to ID', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaValue' => $schemaValue, - 'error' => $e->getMessage(), - ] - ); - } - } - - // Fallback: return 0 for unresolvable values. - $this->logger->warning( - message: '[DocumentBuilder] Could not resolve schema to integer ID', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schemaValue' => $schemaValue, - 'type' => gettype($schemaValue), - ] - ); - return 0; - }//end resolveSchemaToId() -}//end class diff --git a/lib/Service/Index/FacetBuilder.php b/lib/Service/Index/FacetBuilder.php deleted file mode 100644 index 7ef1944633..0000000000 --- a/lib/Service/Index/FacetBuilder.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-12 - * @version GIT: - * @link https://OpenRegister.app - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use OCA\OpenRegister\Service\Index\Backends\SolrBackend; -use Psr\Log\LoggerInterface; - -/** - * FacetBuilder for Solr facet operations - * - * PRAGMATIC APPROACH: Initially delegates to SolrBackend. - * Methods will be migrated incrementally. - * - * @package OCA\OpenRegister\Service\Index - */ -class FacetBuilder -{ - - /** - * Solr backend service (temporary delegation). - * - * @var SolrBackend - */ - private readonly SolrBackend $solrBackend; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * FacetBuilder constructor - * - * @param SolrBackend $solrBackend Backend implementation - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - SolrBackend $solrBackend, - LoggerInterface $logger - ) { - $this->solrBackend = $solrBackend; - $this->logger = $logger; - }//end __construct() - - /** - * Get facetable fields for configuration - * - * @return (mixed|string)[][] Facetable fields - * - * @psalm-return list - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 - */ - public function getRawSolrFieldsForFacetConfiguration(): array - { - $this->logger->debug( - message: '[FacetBuilder] Delegating to SolrBackend', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - return $this->solrBackend->getRawSolrFieldsForFacetConfiguration(); - }//end getRawSolrFieldsForFacetConfiguration() -}//end class diff --git a/lib/Service/Index/FileHandler.php b/lib/Service/Index/FileHandler.php deleted file mode 100644 index 8c3fdf7bd2..0000000000 --- a/lib/Service/Index/FileHandler.php +++ /dev/null @@ -1,392 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.nl - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use Exception; -use OCA\OpenRegister\Db\ChunkMapper; -use Psr\Log\LoggerInterface; - -/** - * FileHandler - * - * Indexes file chunks to search backend (Solr/Elastic). - * - * ARCHITECTURE: - * - TextExtractionService extracts text and creates chunks in database (separate flow). - * - FileHandler reads chunks from database and indexes them to Solr/Elastic. - * - Does NOT extract text - only indexes existing chunks. - * - * RESPONSIBILITIES: - * - Read chunks from database (ChunkMapper). - * - Index chunks to Solr fileCollection. - * - Retrieve file statistics from Solr. - * - Keep Solr index in sync with database chunks. - * - * @category Service - * @package OCA\OpenRegister\Service\Index - */ -class FileHandler -{ - /** - * Constructor - * - * @param LoggerInterface $logger Logger - * @param ChunkMapper $chunkMapper Chunk mapper for retrieving chunks from database - * @param SearchBackendInterface $searchBackend Search backend (Solr/Elastic/etc) - */ - public function __construct( - private readonly LoggerInterface $logger, - private readonly ChunkMapper $chunkMapper, - private readonly SearchBackendInterface $searchBackend - ) { - }//end __construct() - - /** - * Index file chunks to Solr fileCollection. - * - * Reads chunks from database (already extracted by TextExtractionService) and indexes them. - * This method does NOT extract text - it only indexes existing chunks. - * - * @param int $fileId Nextcloud file ID - * @param array $chunks Array of chunk entities from ChunkMapper (from database) - * @param array $metadata File metadata - * - * @return (bool|int|string)[] - * - * @throws Exception If fileCollection is not configured - * - * @psalm-return array{success: bool, indexed: int<0, max>, collection: 'files'} - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-3 - */ - public function indexFileChunks(int $fileId, array $chunks, array $metadata): array - { - $this->logger->info( - message: '[FileHandler] Indexing file chunks', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'file_id' => $fileId, - 'chunk_count' => count($chunks), - ] - ); - - $documents = []; - foreach ($chunks as $index => $chunk) { - $documents[] = [ - 'id' => $chunk->getUuid() ?? $fileId.'_chunk_'.$index, - 'file_id' => $fileId, - 'chunk_index' => $chunk->getChunkIndex(), - 'total_chunks' => count($chunks), - 'chunk_text' => $chunk->getTextContent(), - 'file_name' => $metadata['file_name'] ?? '', - 'file_type' => $metadata['file_type'] ?? '', - 'file_size' => $metadata['file_size'] ?? 0, - 'owner' => $chunk->getOwner(), - 'organisation' => $chunk->getOrganisation(), - 'language' => $chunk->getLanguage(), - 'created_at' => $chunk->getCreatedAt()?->format('c') ?? date('c'), - 'updated_at' => $chunk->getUpdatedAt()?->format('c') ?? date('c'), - ]; - } - - // Use search backend to index documents. - $success = $this->searchBackend->index($documents); - - $indexedCount = 0; - if ($success === true) { - $indexedCount = count($documents); - } - - // Collection name depends on backend configuration. - return [ - 'success' => $success, - 'indexed' => $indexedCount, - 'collection' => 'files', - ]; - }//end indexFileChunks() - - /** - * Get statistics for files in Solr. - * - * @return array Statistics including document count, collection info - * - * @psalm-return array{available: bool, collection?: string, document_count?: int, error?: string} - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-3 - */ - public function getFileStats(): array - { - try { - // Get document count from search backend (backend handles collection selection). - $searchResults = $this->searchBackend->search( - [ - 'q' => '*:*', - 'rows' => 0, - ] - ); - - $documentCount = $searchResults['response']['numFound'] ?? 0; - - // Collection name depends on backend configuration. - return [ - 'available' => true, - 'collection' => 'files', - 'document_count' => $documentCount, - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[FileHandler] Failed to get file stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'available' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end getFileStats() - - /** - * Process and index chunks for unindexed files. - * - * Reads chunks from database that have indexed=false and indexes them to Solr. - * Chunks are created by TextExtractionService in a separate flow. - * This method only reads and indexes existing chunks - does NOT extract text. - * - * @param int|null $limit Maximum number of files to process - * - * @return array Result with success status and stats including processed, indexed, failed counts. - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-3 - */ - public function processUnindexedChunks(?int $limit=null): array - { - $this->logger->info( - message: '[FileHandler] Starting chunk indexing', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'limit' => $limit, - ] - ); - - $startTime = microtime(true); - $stats = [ - 'processed' => 0, - 'indexed' => 0, - 'failed' => 0, - 'total_chunks' => 0, - 'errors' => [], - ]; - - // Get chunks that haven't been indexed yet. - $unindexedChunks = $this->chunkMapper->findUnindexed(limit: $limit); - - // Group chunks by file_id. - $chunksByFile = []; - foreach ($unindexedChunks as $chunk) { - $fileId = $chunk->getSourceId(); - if (isset($chunksByFile[$fileId]) === false) { - $chunksByFile[$fileId] = []; - } - - $chunksByFile[$fileId][] = $chunk; - } - - // Process each file's chunks. - foreach ($chunksByFile as $fileId => $chunks) { - try { - $stats['processed']++; - - // Prepare metadata. - $metadata = [ - 'file_name' => "File {$fileId}", - 'file_type' => 'unknown', - 'file_size' => 0, - ]; - - // Index the chunks. - $result = $this->indexFileChunks(fileId: $fileId, chunks: $chunks, metadata: $metadata); - - if ($result['success'] === true) { - $stats['indexed']++; - $stats['total_chunks'] += $result['indexed']; - - // Mark chunks as indexed. - foreach ($chunks as $chunk) { - $chunk->setIndexed(true); - $this->chunkMapper->update($chunk); - } - - continue; - } - - $stats['failed']++; - $stats['errors'][] = "Failed to index file {$fileId}"; - } catch (Exception $e) { - $stats['failed']++; - $stats['errors'][] = "File {$fileId}: ".$e->getMessage(); - $this->logger->error( - message: '[FileHandler] Failed to process file chunks', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'file_id' => $fileId, - 'error' => $e->getMessage(), - ] - ); - }//end try - }//end foreach - - $executionTime = round((microtime(true) - $startTime) * 1000, 2); - $stats['execution_time_ms'] = $executionTime; - - $this->logger->info( - message: '[FileHandler] Chunk indexing complete', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'stats' => $stats, - ] - ); - - return [ - 'success' => true, - 'stats' => $stats, - ]; - }//end processUnindexedChunks() - - /** - * Get chunking statistics. - * - * @return array Chunking statistics - * - * @psalm-return array{total_chunks: int, indexed_chunks: int, - * unindexed_chunks: int, vectorized_chunks: int} - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-3 - */ - public function getChunkingStats(): array - { - $totalChunks = $this->chunkMapper->countAll(); - $indexedChunks = $this->chunkMapper->countIndexed(); - $unindexedChunks = $this->chunkMapper->countUnindexed(); - $vectorizedChunks = $this->chunkMapper->countVectorized(); - - return [ - 'total_chunks' => $totalChunks, - 'indexed_chunks' => $indexedChunks, - 'unindexed_chunks' => $unindexedChunks, - 'vectorized_chunks' => $vectorizedChunks, - ]; - }//end getChunkingStats() - - /** - * Index files by their IDs. - * - * This method indexes file chunks into the search backend. - * - * @param array $fileIds Array of file IDs to index. - * @param string|null $collectionName Optional collection name. - * - * @return array Indexing results. - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-3 - */ - public function indexFiles(array $fileIds, ?string $collectionName=null): array - { - $this->logger->info( - message: '[FileHandler] Indexing files', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'count' => count($fileIds), - 'collection' => $collectionName, - ] - ); - - try { - /* - * Delegate to search backend. - * - * @psalm-suppress UndefinedInterfaceMethod - indexFiles may exist on specific backend implementations - */ - - return $this->searchBackend->indexFiles($fileIds, $collectionName); - } catch (Exception $e) { - $this->logger->error( - message: '[FileHandler] Failed to index files', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end indexFiles() - - /** - * Get file indexing statistics. - * - * Returns statistics about indexed files. - * - * @return array File indexing statistics. - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-3 - */ - public function getFileIndexStats(): array - { - try { - /* - * Delegate to search backend. - * - * @psalm-suppress UndefinedInterfaceMethod - getFileIndexStats may exist on specific backend implementations - */ - - return $this->searchBackend->getFileIndexStats(); - } catch (Exception $e) { - $this->logger->error( - message: '[FileHandler] Failed to get file index stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end getFileIndexStats() -}//end class diff --git a/lib/Service/Index/ObjectHandler.php b/lib/Service/Index/ObjectHandler.php deleted file mode 100644 index 8e09a319f3..0000000000 --- a/lib/Service/Index/ObjectHandler.php +++ /dev/null @@ -1,280 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.nl - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use Exception; -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Db\RegisterMapper; -use OCA\OpenRegister\Db\SchemaMapper; -use Psr\Log\LoggerInterface; - -/** - * ObjectHandler - * - * Indexes objects to search backend (Solr/Elastic). - * - * ARCHITECTURE: - * - TextExtractionService extracts text from objects (separate flow with listeners). - * - VectorizationService vectorizes objects (separate flow with listeners). - * - ObjectHandler reads objects from database and indexes them to Solr/Elastic. - * - Does NOT extract text or vectorize - only indexes existing data. - * - * RESPONSIBILITIES: - * - Read objects from database (MagicMapper). - * - Index objects to Solr objectCollection. - * - Search objects in Solr. - * - Commit changes to Solr. - * - Keep Solr index in sync with database objects. - * - * @category Service - * @package OCA\OpenRegister\Service\Index - */ -class ObjectHandler -{ - /** - * Constructor - * - * @param SchemaMapper $schemaMapper Schema mapper - * @param RegisterMapper $registerMapper Register mapper - * @param LoggerInterface $logger Logger - * @param SearchBackendInterface $searchBackend Search backend - */ - public function __construct( - private readonly SchemaMapper $schemaMapper, - private readonly RegisterMapper $registerMapper, - private readonly LoggerInterface $logger, - private readonly SearchBackendInterface $searchBackend - ) { - }//end __construct() - - /** - * Search objects in Solr. - * - * @param array $query Search query parameters - * @param bool $_rbac Apply RBAC filters - * @param bool $_multitenancy Apply multitenancy filters - * @param bool $deleted Include deleted objects - * - * @return (array|int|mixed)[] Search results in OpenRegister format - * - * @throws Exception If objectCollection is not configured - * - * @psalm-return array{results: array|mixed, total: 0|mixed, start: 0|mixed} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-9 - */ - public function searchObjects( - array $query=[], - bool $_rbac=true, - bool $_multitenancy=true, - bool $deleted=false - ): array { - $this->logger->debug( - message: '[ObjectHandler] Searching objects', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'query' => $query, - 'rbac' => $_rbac, - 'multitenancy' => $_multitenancy, - ] - ); - - // Build Solr query from OpenRegister query. - $solrQuery = $this->buildSolrQuery( - query: $query, - _rbac: $_rbac, - _multitenancy: $_multitenancy, - deleted: $deleted - ); - - // Execute search via backend (backend handles collection selection). - $results = $this->searchBackend->search($solrQuery); - - // Convert Solr results to OpenRegister format. - return $this->convertToOpenRegisterFormat(solrResults: $results); - }//end searchObjects() - - /** - * Build Solr query from OpenRegister query parameters. - * - * @param array $query OpenRegister query - * @param bool $_rbac Apply RBAC filters - * @param bool $_multitenancy Apply multitenancy filters - * @param bool $deleted Include deleted objects - * - * @return (int|mixed|string[])[] Solr query parameters - * - * @psalm-return array{q: '*:*'|mixed, start: 0|mixed, rows: 10|mixed, - * fq?: list{0: '-deleted:true'}} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Query building requires handling multiple filter conditions - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple filter combinations create many execution paths - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-9 - */ - private function buildSolrQuery(array $query, bool $_rbac, bool $_multitenancy, bool $deleted): array - { - $solrQuery = [ - 'q' => $query['q'] ?? '*:*', - 'start' => $query['start'] ?? 0, - 'rows' => $query['rows'] ?? 10, - ]; - - // Add filters. - $filters = []; - - if ($_rbac === true) { - // TODO: Add RBAC filters based on current user. - } - - if ($_multitenancy === true) { - // TODO: Add multitenancy filters based on current organisation. - } - - if ($deleted === false) { - $filters[] = '-deleted:true'; - } - - if ($filters !== []) { - $solrQuery['fq'] = $filters; - } - - return $solrQuery; - }//end buildSolrQuery() - - /** - * Convert Solr response to OpenRegister format. - * - * @param array $solrResults Solr search results - * - * @return (array|int|mixed)[] OpenRegister formatted results - * - * @psalm-return array{results: array|mixed, total: 0|mixed, start: 0|mixed} - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-9 - */ - private function convertToOpenRegisterFormat(array $solrResults): array - { - $response = $solrResults['response'] ?? []; - $docs = $response['docs'] ?? []; - - return [ - 'results' => $docs, - 'total' => $response['numFound'] ?? 0, - 'start' => $response['start'] ?? 0, - ]; - }//end convertToOpenRegisterFormat() - - /** - * Commit changes to Solr. - * - * Forces Solr to commit pending changes to make them searchable. - * - * @return bool True if commit succeeded - * - * @throws Exception If objectCollection is not configured - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-9 - */ - public function commit(): bool - { - $this->logger->debug( - message: '[ObjectHandler] Committing to Solr', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - try { - // Use search backend to commit (backend handles collection selection). - $result = $this->searchBackend->commit(); - - if ($result === true) { - $this->logger->info( - message: '[ObjectHandler] Successfully committed to Solr', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - } - - return $result; - } catch (Exception $e) { - $this->logger->error( - message: '[ObjectHandler] Failed to commit to Solr', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - throw $e; - }//end try - }//end commit() - - /** - * Reindex all objects in the system. - * - * This delegates to the underlying search backend's reindexAll method. - * - * @param int $maxObjects Maximum objects to reindex (0 = all). - * @param int $batchSize Batch size for reindexing. - * @param string|null $collectionName Optional collection name. - * - * @return array Reindexing results with statistics. - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-9 - */ - public function reindexAll(int $maxObjects=0, int $batchSize=1000, ?string $collectionName=null): array - { - $this->logger->info( - message: '[ObjectHandler] Starting full reindex', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'maxObjects' => $maxObjects, - 'batchSize' => $batchSize, - 'collection' => $collectionName, - ] - ); - - try { - // Delegate to search backend. - return $this->searchBackend->reindexAll($maxObjects, $batchSize, $collectionName); - } catch (Exception $e) { - $this->logger->error( - message: '[ObjectHandler] Reindex failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end reindexAll() -}//end class diff --git a/lib/Service/Index/SchemaHandler.php b/lib/Service/Index/SchemaHandler.php deleted file mode 100644 index 844dd298b0..0000000000 --- a/lib/Service/Index/SchemaHandler.php +++ /dev/null @@ -1,759 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.nl - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use Exception; -use OCA\OpenRegister\Db\SchemaMapper; -use OCP\IConfig; -use Psr\Log\LoggerInterface; - -/** - * SchemaHandler - * - * Manages Solr schema operations including field types, schema mirroring, - * and collection field management. - * - * RESPONSIBILITIES: - * - Ensure vector field types exist in Solr. - * - Mirror OpenRegister schemas to Solr. - * - Manage field types and mappings. - * - Get and update collection field status. - * - * @category Service - * @package OCA\OpenRegister\Service\Index - */ -class SchemaHandler -{ - /** - * Constructor - * - * @param SchemaMapper $schemaMapper Schema mapper for OpenRegister schemas - * @param LoggerInterface $logger Logger - * @param IConfig $config Nextcloud config - * @param SearchBackendInterface $searchBackend Search backend - */ - public function __construct( - private readonly SchemaMapper $schemaMapper, - private readonly LoggerInterface $logger, - private readonly IConfig $config, - private readonly SearchBackendInterface $searchBackend - ) { - }//end __construct() - - /** - * Ensure vector field type exists in a Solr collection. - * - * Creates knn_vector field type for vector similarity search. - * - * @param string $collection Collection name to configure - * @param int $dimensions Vector dimensions (default: 4096) - * @param string $similarity Similarity function: 'cosine', 'dot_product', or 'euclidean' - * - * @return bool Success status - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-2 - */ - public function ensureVectorFieldType( - string $collection, - int $dimensions=4096, - string $similarity='cosine' - ): bool { - try { - $this->logger->info( - message: '[SchemaHandler] Ensuring vector field type', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'dimensions' => $dimensions, - 'similarity' => $similarity, - ] - ); - - // Check if knn_vector type already exists. - $existingTypes = $this->searchBackend->getFieldTypes($collection); - - if (isset($existingTypes['knn_vector']) === true) { - $this->logger->info( - message: '[SchemaHandler] knn_vector field type already exists', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return true; - } - - // Create knn_vector field type. - $fieldType = [ - 'name' => 'knn_vector', - 'class' => 'solr.DenseVectorField', - 'vectorDimension' => $dimensions, - 'similarityFunction' => $similarity, - ]; - - $result = $this->searchBackend->addFieldType(collection: $collection, fieldType: $fieldType); - - if ($result === true) { - $this->logger->info( - message: '[SchemaHandler] ✅ knn_vector field type created successfully', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - } - - return $result; - } catch (Exception $e) { - $this->logger->error( - message: '[SchemaHandler] Failed to ensure vector field type', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'collection' => $collection, - ] - ); - return false; - }//end try - }//end ensureVectorFieldType() - - /** - * Mirror OpenRegister schemas to Solr with intelligent conflict resolution. - * - * Analyzes all schemas first to detect field type conflicts and chooses - * the most permissive type (string > text > float > integer > boolean). - * - * @param bool $force Force recreation of existing fields - * - * @return array Result with success status, stats, execution time, and resolved conflicts. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Schema mirroring requires handling multiple schema scenarios - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple paths for conflict resolution and field processing - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive schema mirroring requires extensive code - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-2 - */ - public function mirrorSchemas(bool $force=false): array - { - $startTime = microtime(true); - $stats = [ - 'schemas_processed' => 0, - 'fields_created' => 0, - 'fields_updated' => 0, - 'conflicts_resolved' => 0, - 'errors' => 0, - ]; - - try { - $this->logger->info( - message: '[SchemaHandler] Starting intelligent schema mirroring with conflict resolution', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - // Get all OpenRegister schemas. - $schemas = $this->schemaMapper->findAll(); - - // Analyze schemas for field conflicts. - $conflictAnalysis = $this->analyzeAndResolveFieldConflicts(schemas: $schemas); - - $this->logger->info( - message: '[SchemaHandler] Field conflict analysis complete', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'total_fields' => count($conflictAnalysis['fields']), - 'conflicting_fields' => count($conflictAnalysis['conflicts']), - 'resolved_conflicts' => count($conflictAnalysis['resolved']), - ] - ); - - // Ensure core metadata fields exist. - $coreFieldsResult = $this->ensureCoreMetadataFields(force: $force); - if ($coreFieldsResult === true) { - $stats['core_fields_created'] = 52; - // Assuming 52 core fields. - } - - // Process each schema and apply fields. - foreach ($schemas as $schema) { - try { - $stats['schemas_processed']++; - - // Generate Solr fields from schema. - $resolved = $conflictAnalysis['resolved']; - $solrFields = $this->generateSolrFieldsFromSchema( - schema: $schema, - resolvedTypes: $resolved - ); - - // Apply fields to Solr. - $applied = $this->applySolrFields(solrFields: $solrFields, force: $force); - - $stats['fields_created'] += $applied['created']; - $stats['fields_updated'] += $applied['updated']; - } catch (Exception $e) { - $stats['errors']++; - $this->logger->error( - message: '[SchemaHandler] Failed to process schema', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'schema_id' => $schema->getId(), - 'error' => $e->getMessage(), - ] - ); - }//end try - }//end foreach - - $executionTime = round((microtime(true) - $startTime) * 1000, 2); - - $this->logger->info( - message: '[SchemaHandler] Schema mirroring complete', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'stats' => $stats, - 'execution_time_ms' => $executionTime, - ] - ); - - return [ - 'success' => true, - 'stats' => $stats, - 'execution_time_ms' => $executionTime, - 'resolved_conflicts' => $conflictAnalysis['resolved'], - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[SchemaHandler] Schema mirroring failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'success' => false, - 'error' => $e->getMessage(), - 'stats' => $stats, - ]; - }//end try - }//end mirrorSchemas() - - /** - * Analyze schemas for field type conflicts and resolve them. - * - * Detects fields with the same name but different types across schemas - * and chooses the most permissive type. - * - * @param array $schemas Array of Schema entities - * - * @return array Analysis with fields, conflicts, and resolved field types. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Conflict analysis requires handling multiple field type scenarios - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple paths for conflict detection and resolution - */ - private function analyzeAndResolveFieldConflicts(array $schemas): array - { - $fieldTypes = []; - // Track field types across all schemas. - // First pass: Collect all field definitions. - foreach ($schemas as $schema) { - $properties = $schema->getProperties() ?? []; - - foreach ($properties as $propName => $propDef) { - if (isset($fieldTypes[$propName]) === false) { - $fieldTypes[$propName] = []; - } - - $solrType = $this->determineSolrFieldType(fieldDefinition: $propDef); - $fieldTypes[$propName][] = [ - 'type' => $solrType, - 'schema_id' => $schema->getId(), - ]; - } - } - - // Second pass: Detect conflicts and resolve them. - $conflicts = []; - $resolved = []; - - foreach ($fieldTypes as $fieldName => $types) { - $uniqueTypes = array_unique(array_column($types, 'type')); - - if (count($uniqueTypes) > 1) { - // Conflict detected! - $conflicts[$fieldName] = $uniqueTypes; - - // Resolve to most permissive type. - $resolvedType = $this->getMostPermissiveType(types: $uniqueTypes); - $resolved[$fieldName] = $resolvedType; - - $this->logger->warning( - message: '[SchemaHandler] Field type conflict resolved', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldName, - 'conflicting_types' => $uniqueTypes, - 'resolved_type' => $resolvedType, - ] - ); - - continue; - }//end if - - $resolved[$fieldName] = $uniqueTypes[0]; - }//end foreach - - return [ - 'fields' => $fieldTypes, - 'conflicts' => $conflicts, - 'resolved' => $resolved, - ]; - }//end analyzeAndResolveFieldConflicts() - - /** - * Get the most permissive type from an array of types. - * - * Type hierarchy (most to least permissive): - * string > text > float > integer > boolean - * - * @param array $types Array of Solr types - * - * @return string Most permissive type - */ - private function getMostPermissiveType(array $types): string - { - $hierarchy = [ - 'string' => 5, - 'text' => 4, - 'float' => 3, - 'integer' => 2, - 'boolean' => 1, - ]; - - $maxPermissiveness = 0; - $mostPermissive = 'string'; - // Default fallback. - foreach ($types as $type) { - $permissiveness = $hierarchy[$type] ?? 0; - if ($permissiveness > $maxPermissiveness) { - $maxPermissiveness = $permissiveness; - $mostPermissive = $type; - } - } - - return $mostPermissive; - }//end getMostPermissiveType() - - /** - * Generate Solr field definitions from an OpenRegister schema. - * - * @param mixed $schema Schema entity - * @param array $resolvedTypes Resolved field types from conflict analysis - * - * @return (bool|mixed|string)[][] Solr field definitions - * - * @psalm-return array - */ - private function generateSolrFieldsFromSchema($schema, array $resolvedTypes): array - { - $solrFields = []; - $properties = $schema->getProperties() ?? []; - - foreach ($properties as $propName => $propDef) { - $fieldName = $this->generateSolrFieldName(fieldName: $propName); - - // Use resolved type if available, otherwise determine from property. - $fieldType = $resolvedTypes[$propName] ?? $this->determineSolrFieldType(fieldDefinition: $propDef); - - $solrFields[$fieldName] = [ - 'name' => $fieldName, - 'type' => $fieldType, - 'indexed' => true, - 'stored' => true, - 'multiValued' => $this->isMultiValued(fieldDefinition: $propDef), - ]; - } - - return $solrFields; - }//end generateSolrFieldsFromSchema() - - /** - * Generate Solr-safe field name. - * - * @param string $fieldName Original field name - * - * @return string Solr-safe field name - */ - private function generateSolrFieldName(string $fieldName): string - { - // Convert to lowercase and replace spaces/special chars with underscore. - $safe = strtolower($fieldName); - $safe = preg_replace('/[^a-z0-9_]/', '_', $safe); - return $safe; - }//end generateSolrFieldName() - - /** - * Determine Solr field type from property definition. - * - * @param array $fieldDefinition Property definition - * - * @return string Solr field type - */ - private function determineSolrFieldType(array $fieldDefinition): string - { - $type = $fieldDefinition['type'] ?? 'string'; - - return match ($type) { - 'integer', 'int' => 'integer', - 'number', 'float', 'double' => 'float', - 'boolean', 'bool' => 'boolean', - 'date', 'datetime' => 'date', - 'text' => 'text', - default => 'string', - }; - }//end determineSolrFieldType() - - /** - * Check if a field should be multi-valued. - * - * @param array $fieldDefinition Property definition - * - * @return bool True if multi-valued - */ - private function isMultiValued(array $fieldDefinition): bool - { - // Check if type is array or maxItems > 1. - if (($fieldDefinition['type'] ?? null) === 'array') { - return true; - } - - if (isset($fieldDefinition['maxItems']) === true && $fieldDefinition['maxItems'] > 1) { - return true; - } - - return false; - }//end isMultiValued() - - /** - * Ensure core metadata fields exist in Solr. - * - * Creates standard fields like id, name, created, updated, etc. - * - * @param bool $force Force recreation - * - * @return bool Success status - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Core field creation requires handling multiple field types - */ - private function ensureCoreMetadataFields(bool $force): bool - { - $this->logger->info( - message: '[SchemaHandler] Ensuring core metadata fields', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - $coreFields = $this->getCoreMetadataFields(); - - try { - $result = $this->applySolrFields(solrFields: $coreFields, force: $force); - - $this->logger->info( - message: '[SchemaHandler] Core metadata fields ensured', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'created' => $result['created'], - 'updated' => $result['updated'], - ] - ); - - return true; - } catch (Exception $e) { - $this->logger->error( - message: '[SchemaHandler] Failed to ensure core metadata fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end ensureCoreMetadataFields() - - /** - * Get core metadata field definitions. - * - * @return (string|true)[][] Core field definitions - * - * @psalm-return array{id: array{name: 'id', type: 'string', indexed: true, - * stored: true, required: true}, - * uuid: array{name: 'uuid', type: 'string', indexed: true, - * stored: true}, - * name: array{name: 'name', type: 'text', indexed: true, - * stored: true}, - * title: array{name: 'title', type: 'text', indexed: true, - * stored: true}, - * summary: array{name: 'summary', type: 'text', indexed: true, - * stored: true}, - * description: array{name: 'description', type: 'text', indexed: true, - * stored: true}, - * created: array{name: 'created', type: 'date', indexed: true, - * stored: true}, - * updated: array{name: 'updated', type: 'date', indexed: true, - * stored: true}, - * deleted: array{name: 'deleted', type: 'boolean', indexed: true, - * stored: true}, - * owner: array{name: 'owner', type: 'string', indexed: true, - * stored: true}, - * organisation: array{name: 'organisation', type: 'string', - * indexed: true, stored: true}, - * register: array{name: 'register', type: 'string', indexed: true, - * stored: true}, - * schema: array{name: 'schema', type: 'string', indexed: true, - * stored: true}} - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-2 - */ - private function getCoreMetadataFields(): array - { - return [ - 'id' => ['name' => 'id', 'type' => 'string', 'indexed' => true, 'stored' => true, 'required' => true], - 'uuid' => ['name' => 'uuid', 'type' => 'string', 'indexed' => true, 'stored' => true], - 'name' => ['name' => 'name', 'type' => 'text', 'indexed' => true, 'stored' => true], - 'title' => ['name' => 'title', 'type' => 'text', 'indexed' => true, 'stored' => true], - 'summary' => ['name' => 'summary', 'type' => 'text', 'indexed' => true, 'stored' => true], - 'description' => ['name' => 'description', 'type' => 'text', 'indexed' => true, 'stored' => true], - 'created' => ['name' => 'created', 'type' => 'date', 'indexed' => true, 'stored' => true], - 'updated' => ['name' => 'updated', 'type' => 'date', 'indexed' => true, 'stored' => true], - 'published' => ['name' => 'published', 'type' => 'date', 'indexed' => true, 'stored' => true], - 'deleted' => ['name' => 'deleted', 'type' => 'boolean', 'indexed' => true, 'stored' => true], - 'owner' => ['name' => 'owner', 'type' => 'string', 'indexed' => true, 'stored' => true], - 'organisation' => ['name' => 'organisation', 'type' => 'string', 'indexed' => true, 'stored' => true], - 'register' => ['name' => 'register', 'type' => 'string', 'indexed' => true, 'stored' => true], - 'schema' => ['name' => 'schema', 'type' => 'string', 'indexed' => true, 'stored' => true], - ]; - }//end getCoreMetadataFields() - - /** - * Apply Solr field definitions to the backend. - * - * @param array $solrFields Field definitions - * @param bool $force Force update existing fields - * - * @return int[] Result with created/updated counts - * - * @psalm-return array{created: int<0, max>, updated: int<0, max>} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Field application requires handling multiple result scenarios - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-2 - */ - private function applySolrFields(array $solrFields, bool $force): array - { - $created = 0; - $updated = 0; - - foreach ($solrFields as $fieldConfig) { - try { - $result = $this->searchBackend->addOrUpdateField(fieldConfig: $fieldConfig, force: $force); - - if ($result === 'created') { - $created++; - } else if ($result === 'updated') { - $updated++; - } - } catch (Exception $e) { - $this->logger->error( - message: '[SchemaHandler] Failed to apply field', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'field' => $fieldConfig['name'] ?? 'unknown', - 'error' => $e->getMessage(), - ] - ); - } - }//end foreach - - return [ - 'created' => $created, - 'updated' => $updated, - ]; - }//end applySolrFields() - - /** - * Get field status for a collection. - * - * Returns information about existing fields, missing fields, and type mismatches. - * - * @param string $collection Collection name - * - * @return array Field status with collection, existing fields, missing fields, and counts. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Field status requires handling multiple field comparison scenarios - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-2 - */ - public function getCollectionFieldStatus(string $collection): array - { - try { - $currentFields = $this->searchBackend->getFields($collection); - $expectedFields = $this->getCoreMetadataFields(); - - $existingFields = []; - $missingFields = []; - - foreach ($expectedFields as $fieldName => $fieldConfig) { - if (isset($currentFields[$fieldName]) === true) { - $existingFields[$fieldName] = $currentFields[$fieldName]; - continue; - } - - $missingFields[$fieldName] = $fieldConfig; - } - - return [ - 'collection' => $collection, - 'existing_fields' => $existingFields, - 'missing_fields' => $missingFields, - 'total_fields' => count($currentFields), - 'expected_fields' => count($expectedFields), - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[SchemaHandler] Failed to get collection field status', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'collection' => $collection, - 'error' => $e->getMessage(), - ]; - }//end try - }//end getCollectionFieldStatus() - - /** - * Create missing fields in a collection. - * - * @param string $collection Collection name - * @param array $missingFields Missing field definitions - * @param bool $dryRun Preview without making changes - * - * @return array Result with success status and field creation info. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Field creation requires handling dry run and multiple scenarios - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-2 - */ - public function createMissingFields(string $collection, array $missingFields, bool $dryRun=false): array - { - $this->logger->info( - message: '[SchemaHandler] Creating missing fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'field_count' => count($missingFields), - 'dry_run' => $dryRun, - ] - ); - - if ($dryRun === true) { - return [ - 'success' => true, - 'dry_run' => true, - 'fields_to_add' => array_keys($missingFields), - ]; - } - - $result = $this->applySolrFields(solrFields: $missingFields, force: false); - - return [ - 'success' => true, - 'created' => $result['created'], - 'failed' => count($missingFields) - $result['created'], - ]; - }//end createMissingFields() - - /** - * Fix mismatched fields in the schema. - * - * Corrects field types that don't match expected types. - * - * @param array $mismatchedFields Array of fields to fix. - * @param bool $dryRun Whether to only simulate (not apply). - * - * @return array Results with fixed/failed fields. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Mismatch fixing requires handling dry run and error scenarios - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-index/tasks.md#task-2 - */ - public function fixMismatchedFields(array $mismatchedFields, bool $dryRun=false): array - { - $this->logger->info( - message: '[SchemaHandler] Fixing mismatched fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'count' => count($mismatchedFields), - 'dryRun' => $dryRun, - ] - ); - - try { - /* - * Delegate to search backend. - * - * @psalm-suppress UndefinedInterfaceMethod - fixMismatchedFields may exist on specific backend implementations - */ - - return $this->searchBackend->fixMismatchedFields($mismatchedFields, $dryRun); - } catch (Exception $e) { - $this->logger->error( - message: '[SchemaHandler] Failed to fix mismatched fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end fixMismatchedFields() -}//end class diff --git a/lib/Service/Index/SchemaMapper.php b/lib/Service/Index/SchemaMapper.php deleted file mode 100644 index abdeeb3cca..0000000000 --- a/lib/Service/Index/SchemaMapper.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use Psr\Log\LoggerInterface; - -/** - * SchemaMapper for schema translation operations - * - * @package OCA\OpenRegister\Service\Index - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -class SchemaMapper -{ - - /** - * Logger instance. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * SchemaMapper constructor - * - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - LoggerInterface $logger - ) { - $this->logger = $logger; - }//end __construct() - - /** - * Map OpenRegister schema to search backend schema - * - * @param array $_schema OpenRegister schema - * - * @return array Search backend schema - * - * @SuppressWarnings (PHPMD.UnusedFormalParameter) - * - * @psalm-return array - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-2 - */ - public function mapToBackendSchema(array $_schema): array - { - $this->logger->debug( - message: '[SchemaMapper] Mapping schema', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - return []; - }//end mapToBackendSchema() - - /** - * Map field types from OpenRegister to search backend - * - * @param string $fieldType OpenRegister field type - * - * @return string Search backend field type - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-2 - */ - public function mapFieldType(string $fieldType): string - { - return $fieldType; - }//end mapFieldType() -}//end class diff --git a/lib/Service/Index/SearchBackendInterface.php b/lib/Service/Index/SearchBackendInterface.php deleted file mode 100644 index 4e9631d934..0000000000 --- a/lib/Service/Index/SearchBackendInterface.php +++ /dev/null @@ -1,432 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.app - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-92 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Service\Aggregation\AggregationQuery; - -/** - * Search Backend Interface - * - * Defines the contract for search backend implementations. All search backends - * (Solr, Elasticsearch, PostgreSQL with pg_trgm, etc.) must implement this interface. - * - * DESIGN PRINCIPLES: - * - Backend-agnostic: Methods should work regardless of underlying technology. - * - Performance-focused: Support bulk operations and batch processing. - * - Flexible querying: Support complex queries with filters, facets, and sorting. - * - Health monitoring: Provide connection testing and health check capabilities. - * - * IMPLEMENTATION NOTES: - * - Implementations should handle backend-specific query syntax internally. - * - Error handling should be consistent across all backends. - * - Return formats should be normalized to OpenRegister format. - * - * @category Interface - * @package OCA\OpenRegister\Service\Index - */ -interface SearchBackendInterface -{ - /** - * Test if the backend is available and configured. - * - * @param bool $forceRefresh Whether to bypass cache and test fresh connection. - * - * @return bool True if backend is available, false otherwise. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function isAvailable(bool $forceRefresh=false): bool; - - /** - * Test the connection to the backend with detailed diagnostics. - * - * @param bool $inclCollTests Whether to include collection-level tests. - * - * @return array Test results with status, timing, and error information. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function testConnection(bool $inclCollTests=true): array; - - /** - * Index a single object in the search backend. - * - * @param ObjectEntity $object The object to index. - * @param bool $commit Whether to commit immediately (may impact performance). - * - * @return bool True if indexing succeeded, false otherwise. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function indexObject(ObjectEntity $object, bool $commit=false): bool; - - /** - * Index multiple objects in bulk. - * - * @param array $objects Array of ObjectEntity instances to index. - * @param bool $commit Whether to commit immediately after bulk index. - * - * @return array Result with success count, failure count, and errors. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function bulkIndexObjects(array $objects, bool $commit=true): array; - - /** - * Delete an object from the search index. - * - * @param string|int $objectId The ID of the object to delete. - * @param bool $commit Whether to commit immediately. - * - * @return bool True if deletion succeeded, false otherwise. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function deleteObject(string|int $objectId, bool $commit=false): bool; - - /** - * Delete multiple objects by query. - * - * @param string $query Backend-specific query string. - * @param bool $commit Whether to commit immediately. - * @param bool $returnDetails Whether to return detailed results. - * - * @return array|bool Results array if returnDetails=true, bool otherwise. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function deleteByQuery(string $query, bool $commit=false, bool $returnDetails=false): array|bool; - - /** - * Search objects with pagination support. - * - * @param array $query Query parameters (filters, pagination, facets, etc.). - * @param bool $_rbac Whether to apply RBAC filtering. - * @param bool $_multitenancy Whether to apply multitenancy filtering. - * @param bool $deleted Whether to include deleted objects. - * - * @return array Search results with objects, pagination, and facets. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-92 - */ - public function searchObjectsPaginated( - array $query=[], - bool $_rbac=true, - bool $_multitenancy=true, - bool $deleted=false - ): array; - - /** - * Get the total count of indexed documents. - * - * @return int Total document count. - */ - public function getDocumentCount(): int; - - /** - * Commit pending changes to the index. - * - * @return bool True if commit succeeded, false otherwise. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function commit(): bool; - - /** - * Optimize the search index for better performance. - * - * @return bool True if optimization succeeded, false otherwise. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function optimize(): bool; - - /** - * Clear all documents from the index. - * - * @param string|null $collectionName Optional collection/index name to clear. - * - * @return array Results with count of deleted documents. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function clearIndex(?string $collectionName=null): array; - - /** - * Warm up the index by pre-loading data into cache. - * - * @param array $schemas Array of schema IDs to warm up. - * @param int $maxObjects Maximum number of objects to warm up. - * @param string $mode Warmup mode (serial, parallel, etc.). - * @param bool $collectErrors Whether to collect detailed error information. - * @param int $batchSize Batch size for warmup operations. - * @param array $schemaIds Schema IDs to filter warmup. - * - * @return array Warmup results with statistics and errors. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function warmupIndex( - array $schemas=[], - int $maxObjects=0, - string $mode='serial', - bool $collectErrors=false, - int $batchSize=1000, - array $schemaIds=[] - ): array; - - /** - * Get backend configuration. - * - * @return array Backend configuration array. - */ - public function getConfig(): array; - - /** - * Get statistics about the search backend. - * - * @return array Backend statistics (doc count, size, performance metrics, etc.). - */ - public function getStats(): array; - - /** - * Create a collection/index in the backend. - * - * @param string $name Name of the collection/index to create. - * @param array $config Configuration for the collection. - * - * @return array Creation results. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function createCollection(string $name, array $config=[]): array; - - /** - * Delete a collection/index from the backend. - * - * @param string|null $collectionName Name of collection to delete, null for default. - * - * @return array Deletion results. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function deleteCollection(?string $collectionName=null): array; - - /** - * Check if a collection/index exists. - * - * @param string $collectionName Name of the collection to check. - * - * @return bool True if collection exists, false otherwise. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function collectionExists(string $collectionName): bool; - - /** - * List all collections/indices in the backend. - * - * @return array Array of collection names. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function listCollections(): array; - - /** - * Index generic documents (not ObjectEntity). - * - * Used by FileHandler for indexing file chunks. - * - * @param array $documents Array of documents to index - * - * @return bool True if successful - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function index(array $documents): bool; - - /** - * Run an aggregation against this backend. - * - * Cross-backend contract: every implementation translates the - * portable AggregationQuery value object into its native syntax - * (Postgres SQL, Solr facets, ES aggs) and returns the normalised - * `{value: int|float|null}` (ungrouped) or - * `{groups: [{key, value}, ...]}` (grouped) shape that - * AggregationRunner consumes. - * - * Returns `null` when this backend cannot execute the query (e.g. - * unsupported metric, unreachable backend) — the caller falls back - * to the PHP path. - * - * @param AggregationQuery $query The portable aggregation request. - * - * @return array|null The aggregation result, or null on a backend miss. - * - * @spec openspec/changes/aggregations-backend-native/tasks.md task 1.1 - */ - public function aggregate(AggregationQuery $query): ?array; - - /** - * Perform a generic search query. - * - * Used by handlers for custom search queries. - * - * @param array $params Search parameters - * - * @return array Search results - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function search(array $params): array; - - /** - * Get field types for a collection. - * - * Used by SchemaHandler for schema management. - * - * @param string $collection Collection name - * - * @return array Field types indexed by name - */ - public function getFieldTypes(string $collection): array; - - /** - * Add a new field type to a collection. - * - * Used by SchemaHandler for creating custom field types like knn_vector. - * - * @param string $collection Collection name - * @param array $fieldType Field type definition - * - * @return bool True if successful - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function addFieldType(string $collection, array $fieldType): bool; - - /** - * Get fields for a collection. - * - * Used by SchemaHandler for checking existing fields. - * - * @param string $collection Collection name - * - * @return array Fields indexed by name - */ - public function getFields(string $collection): array; - - /** - * Add or update a field in a collection. - * - * Used by SchemaHandler for managing collection schema. - * - * @param array $fieldConfig Field configuration - * @param bool $force Force update if exists - * - * @return string Action taken ('created', 'updated', 'skipped') - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function addOrUpdateField(array $fieldConfig, bool $force): string; - - /** - * Reindex all objects in the system. - * - * Clears the index and reindexes all searchable objects from the database. - * - * @param int $maxObjects Maximum objects to reindex (0 = all). - * @param int $batchSize Batch size for reindexing. - * @param string|null $collectionName Optional collection name. - * - * @return array Reindexing results with statistics. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function reindexAll(int $maxObjects=0, int $batchSize=1000, ?string $collectionName=null): array; - - /** - * Index files by their IDs. - * - * Used by FileHandler for indexing file content into the search backend. - * - * @param array $fileIds Array of file IDs to index. - * @param string|null $collectionName Optional collection name. - * - * @return array Indexing results with 'indexed', 'failed', and 'errors' keys. - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function indexFiles(array $fileIds, ?string $collectionName=null): array; - - /** - * Get file indexing statistics. - * - * Returns statistics about files indexed in the search backend. - * - * @return array File indexing statistics. - */ - public function getFileIndexStats(): array; - - /** - * Fix mismatched fields in the search backend schema. - * - * Corrects field type mismatches between the expected schema and the actual - * backend configuration. Supports dry-run mode for previewing changes. - * - * @param array $mismatchedFields Array of mismatched fields with expected config. - * @param bool $dryRun Whether to only preview changes without applying. - * - * @return array Results with fixed/failed fields. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec exclude interface contract declaration — no implementation in this file - */ - public function fixMismatchedFields(array $mismatchedFields, bool $dryRun=false): array; -}//end interface diff --git a/lib/Service/Index/SetupHandler.php b/lib/Service/Index/SetupHandler.php deleted file mode 100644 index 21e7d05906..0000000000 --- a/lib/Service/Index/SetupHandler.php +++ /dev/null @@ -1,2551 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -namespace OCA\OpenRegister\Service\Index; - -use Exception; -use Psr\Log\LoggerInterface; -use GuzzleHttp\Client as GuzzleClient; -use OCA\OpenRegister\Service\IndexService; - -/** - * SOLR Setup and Configuration Manager - * - * Handles initial SOLR setup, configSet creation, and core management - * for the multi-tenant OpenRegister architecture. - * - * This class ensures that SOLR is properly configured with the necessary - * configSets to support dynamic tenant core creation. - * - * @package OCA\OpenRegister\Service\Index - * @category Service - * @author OpenRegister Team - * @copyright 2024 OpenRegister - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @version GIT: - * @link https://github.com/OpenRegister/OpenRegister - * - * @SuppressWarnings(PHPMD.ExcessiveClassLength) Solr setup requires comprehensive configuration methods - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complex Solr setup logic with many configuration scenarios - * @SuppressWarnings(PHPMD.LongVariable) Descriptive variable names improve code readability - */ -class SetupHandler -{ - - /** - * PSR-3 compliant logger for operation tracking - * - * @var LoggerInterface - */ - private LoggerInterface $logger; - - /** - * SOLR connection configuration - * - * @var array - */ - private array $solrConfig; - - /** - * HTTP client for SOLR requests (from IndexService) - * - * @var \OCP\Http\Client\IClient - */ - private \OCP\Http\Client\IClient $httpClient; - - /** - * SOLR service for authenticated HTTP client and configuration - * - * @var IndexService - */ - private IndexService $solrService; - - /** - * Detailed error information from the last failed operation - * - * @var array|null - */ - private ?array $lastErrorDetails = null; - - /** - * Track infrastructure resources created/skipped during setup - * - * @var array - */ - private array $infrastructureCreated = [ - 'configsets_created' => [], - 'configsets_skipped' => [], - 'collections_created' => [], - 'collections_skipped' => [], - 'schema_fields_configured' => false, - 'multi_tenant_ready' => false, - 'cloud_mode' => false, - ]; - - /** - * Setup progress tracking with detailed step information - * - * @var array - */ - private array $setupProgress = []; - - /** - * Initialize SOLR setup manager - * - * @param IndexService $solrService SOLR service with authenticated HTTP client and configuration - * @param LoggerInterface $logger PSR-3 compliant logger for operation tracking - */ - public function __construct(IndexService $solrService, LoggerInterface $logger) - { - $this->solrService = $solrService; - $this->logger = $logger; - - // Get authenticated HTTP client and configuration from IndexService. - $this->httpClient = $solrService->getHttpClient(); - - // @psalm-var array - $this->solrConfig = $solrService->getSolrConfig(); - - $this->logger->info( - message: '[SetupHandler] SOLR Setup: Using authenticated HTTP client from IndexService', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'has_credentials' => empty($this->solrConfig['username']) === false - && empty($this->solrConfig['password']) === false, - 'username' => $this->solrConfig['username'] ?? 'not_set', - 'password_set' => empty($this->solrConfig['password']) === false, - 'host' => $this->solrConfig['host'] ?? 'unknown', - 'port' => $this->solrConfig['port'] ?? 'not_set', - 'scheme' => $this->solrConfig['scheme'] ?? 'not_set', - 'path' => $this->solrConfig['path'] ?? 'not_set', - ] - ); - }//end __construct() - - /** - * Track a setup step with detailed information - * - * @param int $stepNumber Step number (1-5) - * @param string $stepName Human-readable step name - * @param string $status Step status (started, completed, failed) - * @param string $description Step description - * @param array $details Additional step details - * - * @return void - */ - private function trackStep( - int $stepNumber, - string $stepName, - string $status, - string $description, - array $details=[] - ): void { - $stepData = [ - 'step_number' => $stepNumber, - 'step_name' => $stepName, - 'status' => $status, - 'description' => $description, - 'timestamp' => date('Y-m-d H:i:s'), - 'details' => $details, - ]; - - // Update or add the step. - $found = false; - foreach ($this->setupProgress['steps'] as &$step) { - if ($step['step_number'] === $stepNumber) { - $step = array_merge($step, $stepData); - $found = true; - break; - } - } - - if ($found === false) { - $this->setupProgress['steps'][] = $stepData; - } - - $this->logger->info( - message: "[SetupHandler] Setup Step {$stepNumber}/{$stepName}: {$status} - {$description}", - context: array_merge(['file' => __FILE__, 'line' => __LINE__], $details) - ); - }//end trackStep() - - /** - * Build SOLR URL using IndexService base URL method for consistency - * - * @param string $path The SOLR API path (e.g., '/admin/info/system') - * - * @return string Complete SOLR URL - */ - private function buildSolrUrl(string $path): string - { - // Use IndexService's buildSolrBaseUrl method for consistency. - // This ensures URL building logic is centralized and consistent. - $baseUrl = $this->solrService->buildSolrBaseUrl(); - return $baseUrl.$path; - }//end buildSolrUrl() - - /** - * Extract API calls information from a propagation result. - * - * @param array $propagationResult The propagation result array. - * - * @return (mixed|string)[] The API calls array with configset_list_refresh and cluster_status_sync. - * - * @psalm-return array{configset_list_refresh: 'unknown'|mixed, cluster_status_sync: 'unknown'|mixed} - */ - private function getApiCallsFromResult(array $propagationResult): array - { - $summary = $propagationResult['summary'] ?? []; - return [ - 'configset_list_refresh' => $summary['configset_list_refresh'] ?? 'unknown', - 'cluster_status_sync' => $summary['cluster_status_sync'] ?? 'unknown', - ]; - }//end getApiCallsFromResult() - - /** - * Initialize all setup steps as pending to show complete progress view - * - * This ensures that users can see all steps in the setup modal, - * including ones that haven't been reached yet due to earlier failures. - * - * @return void - */ - private function initializeAllSteps(): void - { - $allSteps = [ - 1 => ['step_name' => 'SOLR Connectivity', 'description' => 'Verify SOLR server connectivity and authentication'], - 2 => ['step_name' => 'EnsureTenantConfigSet', 'description' => 'Create or verify tenant-specific configSet'], - 3 => ['step_name' => 'Collection Creation', 'description' => 'Create or verify tenant-specific collection'], - 4 => [ - 'step_name' => 'Schema Configuration', - 'description' => 'Configure schema fields for ObjectEntity metadata', - ], - 5 => ['step_name' => 'Setup Validation', 'description' => 'Validate complete SOLR setup and functionality'], - ]; - - foreach ($allSteps as $stepNumber => $stepInfo) { - $this->setupProgress['steps'][] = [ - 'step_number' => $stepNumber, - 'step_name' => $stepInfo['step_name'], - 'status' => 'pending', - 'description' => $stepInfo['description'], - 'timestamp' => null, - 'details' => [], - ]; - } - }//end initializeAllSteps() - - /** - * Get tenant-specific collection name using IndexService - * - * @return string Tenant-specific collection name (e.g., "openregister_nc_f0e53393") - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - private function getTenantCollectionName(): string - { - // SolrConfig may contain 'core' key even if not in type definition. - $baseCollectionName = 'openregister'; - if (is_array($this->solrConfig) === true && array_key_exists('core', $this->solrConfig) === true) { - $baseCollectionName = $this->solrConfig['core']; - } - - return $this->solrService->getTenantSpecificCollectionName($baseCollectionName); - }//end getTenantCollectionName() - - /** - * Get tenant ID from IndexService - * - * @return string Tenant identifier (e.g., "nc_f0e53393") - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - private function getTenantId(): string - { - // GetTenantId doesn't exist, use getTenantSpecificCollectionName to derive tenant ID. - // Extract tenant ID from collection name or use a default. - $collectionName = $this->solrService->getTenantSpecificCollectionName('openregister'); - // Extract tenant ID from collection name pattern (e.g., "openregister_nc_xxx" -> "nc_xxx"). - if (preg_match('/_nc_([a-f0-9]+)$/', $collectionName, $matches) === 1) { - return 'nc_'.$matches[1]; - } - - // Fallback: use a default tenant ID. - return 'default'; - }//end getTenantId() - - /** - * Get tenant-specific configSet name - * - * @return string ConfigSet name to use for tenant collections - */ - private function getTenantConfigSetName(): string - { - // Use the configSet from configuration (defaults to '_default'). - // SolrConfig may contain 'configSet' key even if not in type definition. - $configSetName = '_default'; - if (is_array($this->solrConfig) === true && array_key_exists('configSet', $this->solrConfig) === true) { - $configSetName = $this->solrConfig['configSet']; - } - - // If using _default, return it as-is (no tenant suffix needed). - if ($configSetName === '_default') { - $this->logger->info( - message: '[SetupHandler] Using _default ConfigSet for maximum compatibility', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'tenant_id' => $this->getTenantId(), - 'reason' => 'Proven stable configuration with dynamic field support', - ] - ); - return '_default'; - } - - // For custom configSets, append tenant ID to make it tenant-specific. - $tenantSpecificName = $configSetName.'_'.$this->getTenantId(); - $this->logger->info( - message: '[SetupHandler] Using custom tenant-specific ConfigSet', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'base_configSet' => $configSetName, - 'tenant_configSet' => $tenantSpecificName, - 'tenant_id' => $this->getTenantId(), - ] - ); - - return $tenantSpecificName; - }//end getTenantConfigSetName() - - /** - * Run complete SOLR setup for OpenRegister multi-tenant architecture - * - * Performs all necessary setup operations for SolrCloud: - * 1. Verifies SOLR connectivity - * 2. Creates base configSet if missing - * 3. Creates base collection for template - * 4. Configures schema fields - * 5. Validates setup completion - * - * Note: This works with SolrCloud mode with ZooKeeper coordination - * - * @return bool True if setup completed successfully, false otherwise - * @throws \RuntimeException If critical setup operations fail - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex SOLR setup requires many configuration paths - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple setup scenarios with different execution paths - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive SOLR setup cannot be easily decomposed - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - public function setupSolr(): bool - { - $this->logger->info( - message: '[SetupHandler] Starting SOLR setup for OpenRegister multi-tenant architecture (SolrCloud mode)', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - // Initialize setup progress tracking. - $this->setupProgress = [ - 'started_at' => date('Y-m-d H:i:s'), - 'completed_at' => null, - 'total_steps' => 6, - 'completed_steps' => 0, - 'success' => false, - 'steps' => [], - ]; - - // Initialize all steps as pending to show complete progress. - $this->initializeAllSteps(); - - try { - // Step 1: Verify SOLR connectivity. - $this->trackStep( - stepNumber: 1, - stepName: 'SOLR Connectivity', - status: 'started', - description: 'Verifying SOLR server connectivity and authentication' - ); - - try { - if ($this->verifySolrConnectivity() === false) { - $this->trackStep( - stepNumber: 1, - stepName: 'SOLR Connectivity', - status: 'failed', - description: 'Cannot connect to SOLR server', - details: [ - 'error' => 'SOLR connectivity test failed', - 'host' => $this->solrConfig['host'] ?? 'unknown', - 'port' => $this->solrConfig['port'] ?? 'unknown', - 'url_tested' => $this->buildSolrUrl(path: '/admin/info/system?wt=json'), - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'verifySolrConnectivity', - 'step' => 1, - 'step_name' => 'SOLR Connectivity', - 'error_type' => 'connectivity_failure', - 'error_message' => 'Cannot connect to SOLR server', - 'configuration' => $this->solrConfig, - 'troubleshooting' => [ - 'Check if SOLR server is running', - 'Verify host/port configuration', - 'Check network connectivity', - 'Verify authentication credentials if required', - ], - ]; - return false; - }//end if - - $this->trackStep( - stepNumber: 1, - stepName: 'SOLR Connectivity', - status: 'completed', - description: 'SOLR server connectivity verified' - ); - $this->setupProgress['completed_steps']++; - } catch (\Exception $e) { - $this->trackStep( - stepNumber: 1, - stepName: 'SOLR Connectivity', - status: 'failed', - description: $e->getMessage(), - details: [ - 'exception_type' => get_class($e), - 'exception_message' => $e->getMessage(), - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'verifySolrConnectivity', - 'step' => 1, - 'step_name' => 'SOLR Connectivity', - 'error_type' => 'connectivity_exception', - 'error_message' => $e->getMessage(), - 'exception_type' => get_class($e), - 'configuration' => $this->solrConfig, - ]; - return false; - }//end try - - // Step 2: Ensure tenant configSet exists. - $tenantConfigSetName = $this->getTenantConfigSetName(); - $this->trackStep( - stepNumber: 2, - stepName: 'EnsureTenantConfigSet', - status: 'started', - description: 'Checking and creating tenant configSet "'.$tenantConfigSetName.'"' - ); - - try { - if ($this->ensureTenantConfigSet() === false) { - // Use detailed error information from createConfigSet if available. - $errorDetails = $this->lastErrorDetails ?? []; - $defaultErr = 'Failed to create tenant configSet "'.$tenantConfigSetName.'"'; - $actualError = $errorDetails['error_message'] ?? $defaultErr; - - $this->trackStep( - stepNumber: 2, - stepName: 'EnsureTenantConfigSet', - status: 'failed', - description: 'Failed to create tenant configSet "'.$tenantConfigSetName.'"', - details: [ - 'configSet' => $tenantConfigSetName, - 'template' => '_default', - 'error_type' => $errorDetails['error_type'] ?? 'configset_creation_failure', - 'url_attempted' => $errorDetails['url_attempted'] ?? 'unknown', - 'actual_error' => $actualError, - 'guzzle_response_status' => $errorDetails['guzzle_response_status'] ?? null, - 'guzzle_response_body' => $errorDetails['guzzle_response_body'] ?? null, - 'solr_error_code' => $errorDetails['solr_error_code'] ?? null, - 'solr_error_details' => $errorDetails['solr_error_details'] ?? null, - ] - ); - - // Enhanced error details for configSet failure. - if ($this->lastErrorDetails === null) { - $this->lastErrorDetails = [ - 'operation' => 'ensureTenantConfigSet', - 'step' => 2, - 'step_name' => 'ConfigSet Creation', - 'error_type' => 'configset_creation_failure', - 'error_message' => 'Failed to create tenant configSet "'.$tenantConfigSetName.'"', - 'configSet' => $tenantConfigSetName, - 'template' => '_default', - 'troubleshooting' => [ - 'Check if SOLR server has write permissions for config directory', - 'Verify template configSet "_default" exists in SOLR', - 'Ensure SOLR is running in SolrCloud mode', - 'Check ZooKeeper connectivity in SolrCloud setup', - 'Check SOLR admin UI for existing configSets', - ], - ]; - } - - return false; - }//end if - - $this->trackStep( - stepNumber: 2, - stepName: 'EnsureTenantConfigSet', - status: 'completed', - description: 'Tenant configSet "'.$tenantConfigSetName.'" is available' - ); - $this->setupProgress['completed_steps']++; - } catch (\Exception $e) { - $this->trackStep( - stepNumber: 2, - stepName: 'EnsureTenantConfigSet', - status: 'failed', - description: $e->getMessage(), - details: [ - 'exception_type' => get_class($e), - 'configSet' => $tenantConfigSetName, - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'ensureTenantConfigSet', - 'step' => 2, - 'step_name' => 'ConfigSet Creation', - 'error_type' => 'configset_exception', - 'error_message' => $e->getMessage(), - 'exception_type' => get_class($e), - 'configSet' => $tenantConfigSetName, - ]; - return false; - }//end try - - // Step 3: Force ConfigSet Propagation (always run for safety). - $this->trackStep( - stepNumber: 3, - stepName: 'ConfigSet Propagation', - status: 'started', - description: 'Forcing configSet propagation across SOLR cluster nodes' - ); - - try { - $propagationResult = $this->forceConfigSetPropagation(configSetName: $tenantConfigSetName); - - if ($propagationResult['success'] !== true) { - $this->trackStep( - stepNumber: 3, - stepName: 'ConfigSet Propagation', - status: 'failed', - description: 'ConfigSet propagation failed', - details: [ - 'configSet' => $tenantConfigSetName, - 'error' => $propagationResult['error'] ?? 'Unknown error', - 'propagation_details' => [ - 'successful_operations' => $propagationResult['successful_operations'] ?? 0, - 'total_operations' => $propagationResult['total_operations'] ?? 0, - 'operations_attempted' => $propagationResult['operations'] ?? [], - 'api_calls' => $this->getApiCallsFromResult( - propagationResult: $propagationResult - ), - 'detailed_operations' => $propagationResult['operations'] ?? [], - ], - ] - ); - - // Note: Propagation failure is not critical, so we continue but log the issue. - $this->logger->warning( - message: '[SetupHandler] ConfigSet propagation failed but continuing with setup', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $tenantConfigSetName, - 'error' => $propagationResult['error'] ?? 'Unknown error', - ] - ); - }//end if - - if ($propagationResult['success'] === true) { - $this->trackStep( - stepNumber: 3, - stepName: 'ConfigSet Propagation', - status: 'completed', - description: 'ConfigSet propagation completed successfully', - details: [ - 'configSet' => $tenantConfigSetName, - 'propagation_details' => [ - 'successful_operations' => $propagationResult['successful_operations'] ?? 0, - 'total_operations' => $propagationResult['total_operations'] ?? 0, - 'operations_performed' => $propagationResult['operations'] ?? [], - 'cluster_sync_status' => $propagationResult['cluster_sync'] ?? 'unknown', - 'cache_refresh_status' => $propagationResult['cache_refresh'] ?? 'unknown', - 'api_calls' => $this->getApiCallsFromResult( - propagationResult: $propagationResult - ), - 'detailed_operations' => $propagationResult['operations'] ?? [], - ], - ] - ); - }//end if - - $this->setupProgress['completed_steps']++; - } catch (\Exception $e) { - $this->trackStep( - stepNumber: 3, - stepName: 'ConfigSet Propagation', - status: 'failed', - description: 'Exception during configSet propagation: '.$e->getMessage(), - details: [ - 'exception_type' => get_class($e), - 'configSet' => $tenantConfigSetName, - ] - ); - - // Note: Propagation exception is not critical, so we continue but log the issue. - $this->logger->warning( - message: '[SetupHandler] Exception during configSet propagation but continuing with setup', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $tenantConfigSetName, - 'error' => $e->getMessage(), - ] - ); - $this->setupProgress['completed_steps']++; - }//end try - - // Step 4: Ensure tenant collection exists. - $tenantCollectionName = $this->getTenantCollectionName(); - $this->trackStep( - stepNumber: 4, - stepName: 'Collection Creation', - status: 'started', - description: 'Checking and creating tenant collection "'.$tenantCollectionName.'"' - ); - - try { - // Ensure tenant collection exists (using tenant-specific configSet). - if ($this->ensureTenantCollectionExists() === false) { - $tenantConfigSetName = $this->getTenantConfigSetName(); - $this->trackStep( - stepNumber: 4, - stepName: 'Collection Creation', - status: 'failed', - description: 'Failed to create tenant collection', - details: [ - 'collection' => $tenantCollectionName, - 'configSet' => $tenantConfigSetName, - 'error_details' => $this->lastErrorDetails, - ] - ); - - // Enhanced error details for collection failure. - if ($this->lastErrorDetails === null) { - $this->lastErrorDetails = [ - 'primary_error' => 'Failed to create tenant collection "'.$tenantCollectionName.'"', - 'error_type' => 'collection_creation_failure', - 'operation' => 'ensureTenantCollectionExists', - 'step' => 4, - 'step_name' => 'Collection Creation', - 'url_attempted' => 'unknown', - 'exception_type' => 'unknown', - 'error_category' => 'unknown', - 'solr_response' => null, - 'guzzle_details' => [], - 'configuration_used' => [ - 'host' => $this->solrConfig['host'] ?? 'unknown', - 'port' => $this->solrConfig['port'] ?? 'default', - 'scheme' => $this->solrConfig['scheme'] ?? 'http', - 'path' => $this->solrConfig['path'] ?? '/solr', - ], - ]; - } - - return false; - }//end if - - $this->trackStep( - stepNumber: 4, - stepName: 'Collection Creation', - status: 'completed', - description: 'Tenant collection "'.$tenantCollectionName.'" is available' - ); - $this->setupProgress['completed_steps']++; - } catch (\Exception $e) { - $this->trackStep( - stepNumber: 4, - stepName: 'Collection Creation', - status: 'failed', - description: $e->getMessage(), - details: [ - 'exception_type' => get_class($e), - 'collection' => $tenantCollectionName, - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'ensureTenantCollectionExists', - 'step' => 4, - 'step_name' => 'Collection Creation', - 'error_type' => 'collection_exception', - 'error_message' => $e->getMessage(), - 'exception_type' => get_class($e), - 'collection' => $tenantCollectionName, - ]; - return false; - }//end try - - // Step 5: Configure schema fields. - $this->trackStep( - stepNumber: 5, - stepName: 'Schema Configuration', - status: 'started', - description: 'Configuring schema fields for ObjectEntity metadata' - ); - - try { - if ($this->configureSchemaFields() === false) { - $this->trackStep( - stepNumber: 5, - stepName: 'Schema Configuration', - status: 'failed', - description: 'Failed to configure schema fields' - ); - - $this->lastErrorDetails = [ - 'operation' => 'configureSchemaFields', - 'step' => 5, - 'step_name' => 'Schema Configuration', - 'error_type' => 'schema_configuration_failure', - 'error_message' => 'Failed to configure schema fields for ObjectEntity metadata', - 'troubleshooting' => [ - 'Check SOLR collection is accessible', - 'Verify schema API is enabled', - 'Check field type definitions', - 'Ensure proper field naming conventions', - ], - ]; - return false; - }//end if - - $this->trackStep( - stepNumber: 5, - stepName: 'Schema Configuration', - status: 'completed', - description: 'Schema fields configured successfully' - ); - $this->infrastructureCreated['schema_fields_configured'] = true; - $this->setupProgress['completed_steps']++; - } catch (\Exception $e) { - $this->trackStep( - stepNumber: 5, - stepName: 'Schema Configuration', - status: 'failed', - description: $e->getMessage(), - details: [ - 'exception_type' => get_class($e), - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'configureSchemaFields', - 'step' => 5, - 'step_name' => 'Schema Configuration', - 'error_type' => 'schema_exception', - 'error_message' => $e->getMessage(), - 'exception_type' => get_class($e), - ]; - return false; - }//end try - - // Step 6: Validate setup. - $this->trackStep( - stepNumber: 6, - stepName: 'Setup Validation', - status: 'started', - description: 'Validating SOLR setup completion' - ); - - try { - if ($this->validateSetup() === false) { - $this->trackStep( - stepNumber: 6, - stepName: 'Setup Validation', - status: 'failed', - description: 'Setup validation failed' - ); - - $this->lastErrorDetails = [ - 'operation' => 'validateSetup', - 'step' => 6, - 'step_name' => 'Setup Validation', - 'error_type' => 'validation_failure', - 'error_message' => 'Setup validation checks failed', - 'troubleshooting' => [ - 'Check configSet exists and is accessible', - 'Verify collection exists and is queryable', - 'Test collection query functionality', - 'Check SOLR admin UI for status', - ], - ]; - return false; - }//end if - - $this->trackStep( - stepNumber: 6, - stepName: 'Setup Validation', - status: 'completed', - description: 'Setup validation passed' - ); - $this->infrastructureCreated['multi_tenant_ready'] = true; - $this->infrastructureCreated['cloud_mode'] = true; - $this->setupProgress['completed_steps']++; - } catch (\Exception $e) { - $this->trackStep( - stepNumber: 6, - stepName: 'Setup Validation', - status: 'failed', - description: $e->getMessage(), - details: [ - 'exception_type' => get_class($e), - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'validateSetup', - 'step' => 6, - 'step_name' => 'Setup Validation', - 'error_type' => 'validation_exception', - 'error_message' => $e->getMessage(), - 'exception_type' => get_class($e), - ]; - return false; - }//end try - - // Mark setup as completed successfully. - $this->setupProgress['completed_at'] = date('Y-m-d H:i:s'); - $this->setupProgress['success'] = true; - - $tenantCollectionName = $this->getTenantCollectionName(); - $tenantConfigSetName = $this->getTenantConfigSetName(); - $solrHost = $this->solrConfig['host'] ?? 'localhost'; - $solrPort = $this->solrConfig['port'] ?? '8983'; - $adminUiUrl = 'http://'.$solrHost.':'.$solrPort.'/solr/'; - $this->logger->info( - message: '[SetupHandler] ✅ SOLR setup completed successfully (SolrCloud mode)', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'tenant_configSet_created' => $tenantConfigSetName, - 'tenant_collection_created' => $tenantCollectionName, - 'schema_fields_configured' => true, - 'setup_validated' => true, - 'completed_steps' => $this->setupProgress['completed_steps'], - 'total_steps' => $this->setupProgress['total_steps'], - 'solr_host' => $solrHost, - 'solr_port' => $solrPort, - 'admin_ui_url' => $adminUiUrl, - ] - ); - - return true; - } catch (\Exception $e) { - $this->setupProgress['completed_at'] = date('Y-m-d H:i:s'); - $this->setupProgress['success'] = false; - - $this->logger->error( - message: '[SetupHandler] SOLR setup failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'completed_steps' => $this->setupProgress['completed_steps'], - 'total_steps' => $this->setupProgress['total_steps'], - 'trace' => $e->getTraceAsString(), - ] - ); - - // Store general failure details if no specific error was captured. - if ($this->lastErrorDetails === null) { - $this->lastErrorDetails = [ - 'operation' => 'setupSolr', - 'error_type' => 'general_setup_failure', - 'error_message' => $e->getMessage(), - 'exception_type' => get_class($e), - 'completed_steps' => $this->setupProgress['completed_steps'], - 'total_steps' => $this->setupProgress['total_steps'], - ]; - } - - return false; - }//end try - }//end setupSolr() - - /** - * Verify SOLR connectivity using IndexService for consistency - * - * **CONSISTENCY FIX**: Uses the same comprehensive connectivity testing - * as all other parts of the system to ensure consistent behavior. - * - * @return bool True if SOLR is accessible and responding correctly - */ - private function verifySolrConnectivity(): bool - { - try { - // **SETUP-OPTIMIZED**: Use connectivity-only test for setup scenarios. - // Collections don't exist yet during setup, so we only test SOLR/Zookeeper connectivity. - $connectionTest = $this->solrService->testConnectivityOnly(); - $isConnected = $connectionTest['success'] ?? false; - - if ($isConnected !== true) { - $this->logger->error( - message: '[SetupHandler] SOLR connectivity verification failed using IndexService', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'test_message' => $connectionTest['message'] ?? 'Connection test failed', - 'components' => $connectionTest['components'] ?? [], - 'details' => $connectionTest['details'] ?? [], - ] - ); - - // Store detailed error information for better troubleshooting. - $this->lastErrorDetails = [ - 'operation' => 'verifySolrConnectivity', - 'error_type' => 'connectivity_test_failure', - 'error_message' => $connectionTest['message'] ?? 'Connection test failed', - 'connection_test_result' => $connectionTest, - 'troubleshooting' => [ - 'Check if SOLR server is running and accessible', - 'Verify host/port configuration in settings', - 'Check network connectivity between containers', - 'Verify authentication credentials if required', - 'Check SOLR admin UI manually: '.$this->buildSolrUrl(path: '/solr/'), - ], - ]; - - return false; - }//end if - - $this->logger->info( - message: '[SetupHandler] SOLR connectivity verified successfully using IndexService', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'test_message' => $connectionTest['message'] ?? 'Connection test passed', - 'components_tested' => array_keys($connectionTest['components'] ?? []), - 'all_components_successful' => $this->allComponentsSuccessful( - components: $connectionTest['components'] ?? [] - ), - ] - ); - return true; - } catch (\Exception $e) { - $this->logger->error( - message: '[SetupHandler] SOLR connectivity verification failed - exception during IndexService test', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'exception_class' => get_class($e), - 'exception_file' => $e->getFile(), - 'exception_line' => $e->getLine(), - ] - ); - - // Store detailed error information. - $this->lastErrorDetails = [ - 'operation' => 'verifySolrConnectivity', - 'error_type' => 'connectivity_exception', - 'error_message' => $e->getMessage(), - 'exception_class' => get_class($e), - 'exception_file' => $e->getFile(), - 'exception_line' => $e->getLine(), - ]; - - return false; - }//end try - }//end verifySolrConnectivity() - - /** - * Check if all components in a connection test were successful - * - * @param array $components Components test results - * - * @return bool True if all components passed - */ - private function allComponentsSuccessful(array $components): bool - { - foreach ($components as $componentName => $result) { - // Suppress unused variable warning for $componentName - only checking results. - unset($componentName); - if (($result['success'] ?? false) === false) { - return false; - } - } - - return true; - }//end allComponentsSuccessful() - - /** - * Ensures the tenant-specific configSet exists in SOLR. - * - * In SolrCloud mode, creating configSets from trusted configSets (like _default) - * requires authentication. Instead, we upload a ZIP file containing the configSet. - * - * @return bool True if configSet exists or was created successfully - */ - private function ensureTenantConfigSet(): bool - { - $tenantConfigSetName = $this->getTenantConfigSetName(); - - // Check if configSet already exists. - if ($this->configSetExists(configSetName: $tenantConfigSetName) === true) { - $this->logger->info( - message: '[SetupHandler] Tenant configSet already exists (skipping creation)', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $tenantConfigSetName, - ] - ); - // Track existing configSet as skipped (not newly created). - if (in_array($tenantConfigSetName, $this->infrastructureCreated['configsets_skipped']) === false) { - $this->infrastructureCreated['configsets_skipped'][] = $tenantConfigSetName; - } - - // Even for existing configSets, force propagation to ensure availability. - // This handles cases where configSet exists but isn't fully propagated. - $propagationResult = $this->forceConfigSetPropagation(configSetName: $tenantConfigSetName); - $this->logger->info( - message: '[SetupHandler] ConfigSet propagation attempted for existing configSet', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $tenantConfigSetName, - 'result' => $propagationResult, - ] - ); - - return true; - }//end if - - // Upload configSet from ZIP file (bypasses trusted configSet authentication). - $this->logger->info( - message: '[SetupHandler] Uploading tenant configSet from ZIP file', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $tenantConfigSetName, - 'method' => 'ZIP upload (avoids SolrCloud authentication issues)', - ] - ); - return $this->uploadConfigSet(configSetName: $tenantConfigSetName); - }//end ensureTenantConfigSet() - - /** - * Check if a SOLR configSet exists - * - * @param string $configSetName Name of the configSet to check - * - * @return bool True if configSet exists, false otherwise - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - private function configSetExists(string $configSetName): bool - { - $url = $this->buildSolrUrl(path: '/admin/configs?action=LIST&wt=json'); - - $this->logger->debug( - message: '[SetupHandler] Checking if configSet exists', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - ] - ); - - try { - $requestOptions = ['timeout' => 10]; - - // Add authentication if configured. - if (empty($this->solrConfig['username']) === false && empty($this->solrConfig['password']) === false) { - $requestOptions['auth'] = [$this->solrConfig['username'], $this->solrConfig['password']]; - } - - $response = $this->httpClient->get($url, $requestOptions); - - if ($response->getStatusCode() !== 200) { - $this->logger->warning( - message: '[SetupHandler] Failed to check configSet existence - HTTP error', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - 'status_code' => $response->getStatusCode(), - 'response_body' => (string) $response->getBody(), - 'assumption' => 'Assuming configSet does not exist', - ] - ); - return false; - } - - $data = json_decode((string) $response->getBody(), true); - } catch (\Exception $e) { - $this->logger->warning( - message: '[SetupHandler] Failed to check configSet existence - HTTP request failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - 'error' => $e->getMessage(), - 'exception_type' => get_class($e), - 'assumption' => 'Assuming configSet does not exist', - ] - ); - return false; - }//end try - - if ($data === null) { - $this->logger->warning( - message: '[SetupHandler] Failed to check configSet existence - Invalid JSON response', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - 'json_error' => json_last_error_msg(), - 'assumption' => 'Assuming configSet does not exist', - ] - ); - return false; - } - - $configSets = $data['configSets'] ?? []; - $exists = in_array($configSetName, $configSets); - - $this->logger->debug( - message: '[SetupHandler] ConfigSet existence check completed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'exists' => $exists, - 'available_configSets' => $configSets, - ] - ); - - return $exists; - }//end configSetExists() - - /** - * Ensure the tenant-specific collection exists for this instance - * - * Creates a tenant-specific collection (e.g., "openregister_nc_f0e53393") - * using the tenant-specific configSet (e.g., "openregister_nc_f0e53393"). - * - * @return bool True if tenant collection exists or was created successfully - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple tenant collection scenarios and fallback paths - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive tenant collection management with detailed logging - */ - private function ensureTenantCollectionExists(): bool - { - $tenantCollectionName = $this->getTenantCollectionName(); - - // Check if tenant collection already exists. - if ($this->solrService->collectionExists($tenantCollectionName) === true) { - $this->logger->info( - message: '[SetupHandler] Tenant collection already exists (skipping creation)', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $tenantCollectionName, - ] - ); - - // Track existing collection as skipped (not newly created). - if (in_array($tenantCollectionName, $this->infrastructureCreated['collections_skipped']) === false) { - $this->infrastructureCreated['collections_skipped'][] = $tenantCollectionName; - } - - return true; - } - - // Create tenant collection using the tenant-specific configSet. - $tenantConfigSetName = $this->getTenantConfigSetName(); - $this->logger->info( - message: '[SetupHandler] Creating tenant collection', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $tenantCollectionName, - 'configSet' => $tenantConfigSetName, - ] - ); - - try { - // Attempt collection creation with retry logic for configSet propagation delays. - $success = $this->createCollectionWithRetry( - collectionName: $tenantCollectionName, - configSetName: $tenantConfigSetName - ); - - // Track newly created collection. - $alreadyCreated = in_array($tenantCollectionName, $this->infrastructureCreated['collections_created']); - if ($success === true && $alreadyCreated === false) { - $this->infrastructureCreated['collections_created'][] = $tenantCollectionName; - } - - return $success; - } catch (\GuzzleHttp\Exception\GuzzleException $e) { - // Capture Guzzle HTTP errors (network, timeout, etc.). - $requestMethod = 'unknown'; - $responseCode = null; - $responseBody = null; - $urlAttempted = 'unknown'; - - // @psalm-suppress UndefinedInterfaceMethod - Methods exist on specific exception types - if (method_exists($e, 'getRequest') === true && $e->getRequest() !== null) { - $requestMethod = $e->getRequest()->getMethod(); - $urlAttempted = (string) $e->getRequest()->getUri(); - } - - // @psalm-suppress UndefinedInterfaceMethod - Methods exist on specific exception types - $hasResponseMethod = method_exists($e, 'hasResponse') === true && $e->hasResponse() === true; - if ($hasResponseMethod === true && method_exists($e, 'getResponse') === true) { - $response = $e->getResponse(); - if ($response !== null) { - $responseCode = $response->getStatusCode(); - $responseBody = (string) $response->getBody(); - } - } - - $this->lastErrorDetails = [ - 'primary_error' => 'HTTP request to SOLR failed', - 'error_type' => 'guzzle_http_error', - 'operation' => 'ensureTenantCollectionExists', - 'step' => 3, - 'step_name' => 'Collection Creation', - 'collection' => $tenantCollectionName, - 'configSet' => $tenantConfigSetName, - 'url_attempted' => $urlAttempted, - 'exception_type' => get_class($e), - 'exception_message' => $e->getMessage(), - 'error_category' => 'network_connectivity', - 'guzzle_details' => [ - 'request_method' => $requestMethod, - 'response_code' => $responseCode, - 'response_body' => $responseBody, - ], - ]; - - $this->logger->error( - message: '[SetupHandler] Guzzle HTTP error during collection creation', - context: array_merge(['file' => __FILE__, 'line' => __LINE__], $this->lastErrorDetails ?? []) - ); - return false; - } catch (\Exception $e) { - // Capture SOLR API errors (400 responses, validation errors, etc.). - $errorCategory = 'solr_api_error'; - - // Try to extract error category from nested exception. - if (($e->getPrevious() !== null) === true && ($e->getPrevious()->getMessage() !== null) === true) { - $possibleJson = $e->getPrevious()->getMessage(); - // Check if message is valid JSON (indicates SOLR response). - // Call json_decode only for its side effect on json_last_error(). - $isValidJson = (json_decode($possibleJson, true) !== null || json_last_error() === JSON_ERROR_NONE); - if ($isValidJson === true) { - // Valid JSON response indicates SOLR validation error. - $errorCategory = 'solr_validation_error'; - } - } - - // Log the collection creation failure with full details. - $this->logger->error( - message: '[SetupHandler] Collection creation failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $tenantCollectionName, - 'configSet' => $tenantConfigSetName, - 'original_error' => $e->getMessage(), - 'error_type' => get_class($e), - ] - ); - - $this->lastErrorDetails = [ - 'primary_error' => 'Failed to create tenant collection "'.$tenantCollectionName.'"', - 'error_type' => 'collection_creation_failure', - 'operation' => 'ensureTenantCollectionExists', - 'step' => 4, - 'step_name' => 'Collection Creation', - 'collection' => $tenantCollectionName, - 'configSet' => $tenantConfigSetName, - 'url_attempted' => 'SOLR Collections API', - 'exception_type' => get_class($e), - 'exception_message' => $e->getMessage(), - 'error_category' => $errorCategory, - 'solr_response' => null, - 'guzzle_details' => [], - 'configuration_used' => [ - 'host' => $this->solrConfig['host'] ?? 'unknown', - 'port' => $this->solrConfig['port'] ?? 'default', - 'scheme' => $this->solrConfig['scheme'] ?? 'http', - 'path' => $this->solrConfig['path'] ?? '/solr', - ], - ]; - - $this->logger->error( - message: '[SetupHandler] SOLR collection creation exception', - context: array_merge(['file' => __FILE__, 'line' => __LINE__], $this->lastErrorDetails ?? []) - ); - return false; - }//end try - }//end ensureTenantCollectionExists() - - /** - * Create collection with retry logic for configSet propagation delays - * - * This addresses the ZooKeeper propagation delay issue by directly attempting - * collection creation with exponential backoff retry logic instead of polling. - * - * @param string $collectionName Collection name to create - * @param string $configSetName ConfigSet name to use - * @param int $maxAttempts Maximum number of retry attempts (default: 6 - up to ~120 seconds) - * - * @return bool True if collection created successfully - * - * @throws \Exception If all retry attempts fail - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Retry logic requires multiple condition checks per attempt - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive retry handling with detailed logging - */ - private function createCollectionWithRetry(string $collectionName, string $configSetName, int $maxAttempts=6): bool - { - $attempt = 0; - $baseDelaySeconds = 2; - // Start with 2 second delay. - $startTime = time(); - $retryDetails = [ - 'attempts' => 0, - 'total_delay_seconds' => 0, - 'attempt_timestamps' => [], - 'last_error' => null, - 'last_solr_response' => null, - ]; - - while ($attempt < $maxAttempts) { - $attempt++; - - try { - $retryDetails['attempts'] = $attempt; - $retryDetails['attempt_timestamps'][] = date('Y-m-d H:i:s'); - - $this->logger->info( - message: '[SetupHandler] Attempting collection creation', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collectionName, - 'configSet' => $configSetName, - 'attempt' => $attempt, - 'maxAttempts' => $maxAttempts, - 'elapsed_seconds' => time() - $startTime, - ] - ); - - // Direct attempt to create collection. - $result = $this->solrService->createCollection( - name: $collectionName, - config: ['configSet' => $configSetName] - ); - $success = isset($result['success']) && $result['success'] === true; - - if ($success === true) { - $totalElapsed = time() - $startTime; - $this->logger->info( - message: '[SetupHandler] Collection created successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collectionName, - 'configSet' => $configSetName, - 'attempt' => $attempt, - 'total_elapsed_seconds' => $totalElapsed, - 'retry_details' => $retryDetails, - ] - ); - return true; - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - $isConfigSetError = $this->isConfigSetPropagationError(errorMessage: $errorMessage); - - // Capture the detailed error information. - $retryDetails['last_error'] = $errorMessage; - - // Try to extract SOLR response from the exception. - if (($e->getPrevious() !== null) === true && ($e->getPrevious()->getMessage() !== null) === true) { - try { - $solrResponse = json_decode($e->getPrevious()->getMessage(), true); - if (($solrResponse !== null) === true && json_last_error() === JSON_ERROR_NONE) { - $retryDetails['last_solr_response'] = $solrResponse; - - // Log the actual SOLR error for debugging. - $this->logger->error( - message: '[SetupHandler] SOLR API returned error response', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collectionName, - 'configSet' => $configSetName, - 'attempt' => $attempt, - 'solr_status' => $solrResponse['responseHeader']['status'] ?? 'unknown', - 'solr_error' => $solrResponse['error'] ?? null, - 'solr_response' => $solrResponse, - ] - ); - } - } catch (\Exception $jsonException) { - // If not JSON, store as string. - $retryDetails['last_solr_response'] = $e->getPrevious()->getMessage(); - }//end try - }//end if - - $this->logger->warning( - message: '[SetupHandler] Collection creation attempt failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collectionName, - 'configSet' => $configSetName, - 'attempt' => $attempt, - 'maxAttempts' => $maxAttempts, - 'error' => $errorMessage, - 'isConfigSetPropagationError' => $isConfigSetError, - 'solr_response' => $retryDetails['last_solr_response'], - ] - ); - - // If this is the last attempt, provide user-friendly propagation error with retry details. - if ($attempt >= $maxAttempts && ($isConfigSetError === true)) { - $totalElapsed = time() - $startTime; - $retryDetails['total_elapsed_seconds'] = $totalElapsed; - - $msg1 = 'SOLR ConfigSet propagation timeout: '; - $msg2 = 'The configSet was created successfully but is still propagating. '; - $msg3 = "This is normal in distributed SOLR. Attempted {$attempt} times "; - $msg4 = "over {$totalElapsed} seconds. Please wait 2-5 minutes and retry."; - $message = $msg1.$msg2.$msg3.$msg4; - throw new Exception($message, 500, new Exception(json_encode($retryDetails))); - } - - // If not a configSet propagation error, throw immediately. - if ($isConfigSetError === false) { - throw $e; - } - - // Calculate exponential backoff delay: 2, 4, 8, 16 seconds. - $delaySeconds = $baseDelaySeconds * pow(2, $attempt - 1); - $retryDetails['total_delay_seconds'] += $delaySeconds; - - $this->logger->info( - message: '[SetupHandler] Retrying collection creation after delay', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collectionName, - 'delaySeconds' => $delaySeconds, - 'nextAttempt' => $attempt + 1, - 'total_elapsed_seconds' => time() - $startTime, - 'cumulative_delay_seconds' => $retryDetails['total_delay_seconds'], - ] - ); - - sleep($delaySeconds); - }//end try - }//end while - - // Should not reach here due to exception throwing above. - return false; - }//end createCollectionWithRetry() - - /** - * Check if error message indicates configSet propagation delay - * - * @param string $errorMessage Error message from SOLR - * - * @return bool True if this appears to be a configSet propagation issue - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - private function isConfigSetPropagationError(string $errorMessage): bool - { - // Only treat as propagation errors if they specifically mention propagation/availability issues. - $propErrorPatterns = [ - 'configset does not exist', - 'Config does not exist', - 'Could not find configSet', - 'configSet not found', - 'ConfigSet propagation timeout', - // Our own timeout message. - ]; - - // "Underlying core creation failed" is NOT a propagation issue - it's a core creation failure. - // This should fail immediately, not retry. - foreach ($propErrorPatterns as $pattern) { - if (stripos($errorMessage, $pattern) !== false) { - return true; - } - } - - return false; - }//end isConfigSetPropagationError() - - /** - * Try to force configSet propagation across SOLR cluster nodes - * - * This attempts to trigger immediate configSet synchronization using - * various SOLR admin API calls that can help speed up propagation. - * - * @param string $configSetName ConfigSet name to force propagation for - * - * @return (((int|null|string)[]|string)[]|bool|int|null|string)[] Result array with success status, - * operations performed, and details - * - * @psalm-return array{ - * success: bool, - * operations: array{ - * configset_list_refresh: array{ - * name: 'configset_list_refresh', - * description: 'List ConfigSets API call to trigger cache refresh', - * url: null|string, - * status: 'failed'|'success', - * http_status: int|null, - * response_size: int<0, max>, - * error: null|string - * }, - * cluster_status_sync: array{ - * name: 'cluster_status_sync', - * description: 'Cluster Status API call to trigger ZooKeeper sync', - * url: null|string, - * status: 'failed'|'success', - * http_status: int|null, - * response_size: int<0, max>, - * error: null|string - * } - * }, - * successful_operations: 0|1|2, - * total_operations: 2, - * cluster_sync: 'failed'|'triggered', - * cache_refresh: 'failed'|'triggered', - * error: 'All propagation methods failed'|null, - * summary: array{ - * configset_list_refresh: 'failed'|'success', - * cluster_status_sync: 'failed'|'success' - * } - * } - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive propagation methods with detailed response handling - */ - private function forceConfigSetPropagation(string $configSetName): array - { - $this->logger->info( - message: '[SetupHandler] Attempting to force configSet propagation', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - ] - ); - - $successCount = 0; - $operationResults = []; - - // Method 1: List configSets to trigger cache refresh. - $listOperation = [ - 'name' => 'configset_list_refresh', - 'description' => 'List ConfigSets API call to trigger cache refresh', - 'url' => null, - 'status' => 'failed', - 'http_status' => null, - 'response_size' => 0, - 'error' => null, - ]; - - try { - $url = $this->buildSolrUrl(path: '/admin/configs?action=LIST&wt=json'); - $listOperation['url'] = $url; - $response = $this->httpClient->get($url, ['timeout' => 10]); - - $listOperation['http_status'] = $response->getStatusCode(); - $listOperation['response_size'] = strlen((string) $response->getBody()); - - if ($response->getStatusCode() === 200) { - $successCount++; - $listOperation['status'] = 'success'; - $this->logger->debug( - message: '[SetupHandler] ConfigSet list refresh successful', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'method' => 'LIST', - 'response_size' => $listOperation['response_size'], - ] - ); - } - } catch (\Exception $e) { - $listOperation['error'] = $e->getMessage(); - $this->logger->debug( - message: '[SetupHandler] ConfigSet list refresh failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'method' => 'LIST', - 'error' => $e->getMessage(), - ] - ); - }//end try - - $operationResults['configset_list_refresh'] = $listOperation; - - // Method 2: Check cluster status to trigger ZooKeeper sync. - $clusterOperation = [ - 'name' => 'cluster_status_sync', - 'description' => 'Cluster Status API call to trigger ZooKeeper sync', - 'url' => null, - 'status' => 'failed', - 'http_status' => null, - 'response_size' => 0, - 'error' => null, - ]; - - try { - $url = $this->buildSolrUrl(path: '/admin/collections?action=CLUSTERSTATUS&wt=json'); - $clusterOperation['url'] = $url; - $response = $this->httpClient->get($url, ['timeout' => 10]); - - $clusterOperation['http_status'] = $response->getStatusCode(); - $clusterOperation['response_size'] = strlen((string) $response->getBody()); - - if ($response->getStatusCode() === 200) { - $successCount++; - $clusterOperation['status'] = 'success'; - $this->logger->debug( - message: '[SetupHandler] Cluster status refresh successful', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'method' => 'CLUSTERSTATUS', - 'response_size' => $clusterOperation['response_size'], - ] - ); - } - } catch (\Exception $e) { - $clusterOperation['error'] = $e->getMessage(); - $this->logger->debug( - message: '[SetupHandler] Cluster status refresh failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'method' => 'CLUSTERSTATUS', - 'error' => $e->getMessage(), - ] - ); - }//end try - - $operationResults['cluster_status_sync'] = $clusterOperation; - - $this->logger->info( - message: '[SetupHandler] ConfigSet propagation force completed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'successful_methods' => $successCount, - 'total_methods' => 2, - ] - ); - - // Give a moment for any triggered propagation to begin. - if ($successCount > 0) { - sleep(1); - } - - // Determine cluster sync status. - $clusterSync = 'failed'; - if ($successCount >= 2) { - $clusterSync = 'triggered'; - } - - // Determine cache refresh status. - $cacheRefresh = 'failed'; - if ($successCount >= 1) { - $cacheRefresh = 'triggered'; - } - - // Determine error message. - $error = null; - if ($successCount === 0) { - $error = 'All propagation methods failed'; - } - - return [ - 'success' => $successCount > 0, - 'operations' => $operationResults, - 'successful_operations' => $successCount, - 'total_operations' => 2, - 'cluster_sync' => $clusterSync, - 'cache_refresh' => $cacheRefresh, - 'error' => $error, - 'summary' => [ - 'configset_list_refresh' => $operationResults['configset_list_refresh']['status'], - 'cluster_status_sync' => $operationResults['cluster_status_sync']['status'], - ], - ]; - }//end forceConfigSetPropagation() - - /** - * Upload a configSet from ZIP file to SOLR - * - * This method uploads a pre-packaged configSet ZIP file to SOLR, which bypasses - * the authentication requirements for creating configSets from trusted templates. - * - * @param string $configSetName Name for the new configSet - * - * @return bool True if configSet was uploaded successfully - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive configSet upload with detailed error handling - */ - private function uploadConfigSet(string $configSetName): bool - { - // Path to our packaged configSet ZIP file (fixed version with proper XML structure). - $zipPath = __DIR__.'/../../resources/solr/openregister-configset-fixed.zip'; - - if (file_exists($zipPath) === false) { - $this->logger->error( - message: '[SetupHandler] ConfigSet ZIP file not found', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'zipPath' => $zipPath, - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'uploadConfigSet', - 'configSet' => $configSetName, - 'error_type' => 'zip_file_not_found', - 'error_message' => 'ConfigSet ZIP file not found at: '.$zipPath, - 'zip_path' => $zipPath, - 'troubleshooting_tips' => [ - 'Ensure the configSet ZIP file exists in resources/solr/', - 'Check file permissions on the ZIP file', - 'Verify the ZIP file contains valid configSet files', - ], - ]; - return false; - }//end if - - $url = $this->buildSolrUrl( - path: sprintf( - '/admin/configs?action=UPLOAD&name=%s&wt=json', - urlencode($configSetName) - ) - ); - - $this->logger->info( - message: '[SetupHandler] Uploading SOLR configSet from ZIP file', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - 'zipPath' => $zipPath, - 'zipSize' => filesize($zipPath).' bytes', - ] - ); - - try { - // Read ZIP file contents. - $zipContents = file_get_contents($zipPath); - if ($zipContents === false) { - $this->logger->error( - message: '[SetupHandler] Failed to read configSet ZIP file', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'zipPath' => $zipPath, - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'uploadConfigSet', - 'configSet' => $configSetName, - 'error_type' => 'zip_read_failed', - 'error_message' => 'Failed to read configSet ZIP file', - 'zip_path' => $zipPath, - ]; - return false; - } - - // Upload ZIP file via POST request. - $requestOptions = [ - 'timeout' => 30, - 'headers' => [ - 'Content-Type' => 'application/octet-stream', - ], - 'body' => $zipContents, - ]; - - $response = $this->httpClient->post($url, $requestOptions); - - if ($response->getStatusCode() !== 200) { - $responseBody = (string) $response->getBody(); - $this->logger->error( - message: '[SetupHandler] Failed to upload configSet - HTTP error', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - 'status_code' => $response->getStatusCode(), - 'response_body' => $responseBody, - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'uploadConfigSet', - 'configSet' => $configSetName, - 'url_attempted' => $url, - 'error_type' => 'http_error', - 'error_message' => 'HTTP error '.$response->getStatusCode(), - 'response_status' => $response->getStatusCode(), - 'response_body' => $responseBody, - ]; - return false; - }//end if - - $data = json_decode((string) $response->getBody(), true); - - if ($data === null) { - $this->logger->error( - message: '[SetupHandler] Failed to upload configSet - Invalid JSON response', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - 'raw_response' => (string) $response->getBody(), - 'json_error' => json_last_error_msg(), - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'uploadConfigSet', - 'configSet' => $configSetName, - 'url_attempted' => $url, - 'error_type' => 'invalid_json_response', - 'error_message' => 'SOLR returned invalid JSON response', - 'json_error' => json_last_error_msg(), - 'raw_response' => (string) $response->getBody(), - ]; - return false; - }//end if - - $status = $data['responseHeader']['status'] ?? -1; - if ($status === 0) { - $this->logger->info( - message: '[SetupHandler] ConfigSet uploaded successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'method' => 'ZIP upload', - ] - ); - - // Track newly created configSet. - if (in_array($configSetName, $this->infrastructureCreated['configsets_created'], true) === false) { - $this->infrastructureCreated['configsets_created'][] = $configSetName; - } - - // Force configSet propagation immediately after successful upload. - // This proactively triggers cache refresh and ZooKeeper sync to reduce. - // The likelihood of propagation delays when creating collections. - $propagationResult = $this->forceConfigSetPropagation(configSetName: $configSetName); - $this->logger->info( - message: '[SetupHandler] ConfigSet propagation attempted after upload', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'result' => $propagationResult, - ] - ); - - return true; - }//end if - - // Handle SOLR API errors. - $errorCode = $data['error']['code'] ?? $status; - $errorMsg = $data['error']['msg'] ?? 'Unknown SOLR error'; - $errorDetails = $data['error']['metadata'] ?? []; - - $this->logger->error( - message: '[SetupHandler] Failed to upload configSet - SOLR API error', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - 'solr_status' => $status, - 'solr_error_code' => $errorCode, - 'solr_error_message' => $errorMsg, - 'solr_error_details' => $errorDetails, - 'full_response' => $data, - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'uploadConfigSet', - 'configSet' => $configSetName, - 'url_attempted' => $url, - 'error_type' => 'solr_api_error', - 'error_message' => $errorMsg, - 'solr_status' => $status, - 'solr_error_code' => $errorCode, - 'solr_error_details' => $errorDetails, - 'full_solr_response' => $data, - ]; - return false; - } catch (\Exception $e) { - $this->logger->error( - message: '[SetupHandler] Failed to upload configSet - HTTP request failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $configSetName, - 'url' => $url, - 'error' => $e->getMessage(), - 'exception_type' => get_class($e), - ] - ); - - $this->lastErrorDetails = [ - 'operation' => 'uploadConfigSet', - 'configSet' => $configSetName, - 'url_attempted' => $url, - 'error_type' => 'http_request_failed', - 'error_message' => $e->getMessage(), - 'exception_type' => get_class($e), - ]; - return false; - }//end try - }//end uploadConfigSet() - - /** - * Configure SOLR schema fields for OpenRegister ObjectEntity metadata - * - * This method sets up all the necessary field types and fields based on the - * ObjectEntity class metadata fields, ensuring proper data types and indexing. - * - * @return bool True if schema configuration was successful - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - private function configureSchemaFields(): bool - { - $this->logger->info( - message: '[SetupHandler] Configuring SOLR schema fields for ObjectEntity metadata', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - // Get all field definitions including self_* metadata fields. - $fieldDefinitions = self::getObjectEntityFieldDefinitions(); - - $this->logger->info( - message: '[SetupHandler] Schema field configuration', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'total_fields_to_configure' => count($fieldDefinitions), - 'includes_metadata_fields' => true, - 'note' => 'All ObjectEntity fields including self_* metadata fields will be configured', - ] - ); - - $fieldResults = [ - 'total_fields' => count($fieldDefinitions), - 'fields_added' => 0, - 'fields_updated' => 0, - 'fields_failed' => 0, - 'fields_skipped' => 0, - 'added_fields' => [], - 'updated_fields' => [], - 'failed_fields' => [], - 'skipped_fields' => [], - ]; - - $success = true; - foreach ($fieldDefinitions as $fieldName => $fieldConfig) { - $result = $this->addOrUpdateSchemaFieldWithTracking( - fieldName: $fieldName, - fieldConfig: $fieldConfig - ); - - if ($result['success'] !== true) { - $fieldResults['fields_failed']++; - $fieldResults['failed_fields'][] = $fieldName; - $errorMsg = $result['error'] ?? 'Unknown error'; - $this->logger->error( - message: '[SetupHandler] Failed to configure field', - context: ['file' => __FILE__, 'line' => __LINE__, 'field' => $fieldName, 'error' => $errorMsg] - ); - $success = false; - continue; - } - - if ($result['action'] === 'added') { - $fieldResults['fields_added']++; - $fieldResults['added_fields'][] = $fieldName; - } else if ($result['action'] === 'updated') { - $fieldResults['fields_updated']++; - $fieldResults['updated_fields'][] = $fieldName; - } else if ($result['action'] === 'skipped') { - $fieldResults['fields_skipped']++; - $fieldResults['skipped_fields'][] = $fieldName; - } - }//end foreach - - // Update the step tracking with detailed field information. - $status = 'failed'; - if ($success === true) { - $status = 'completed'; - } - - $message = 'Schema field configuration failed'; - if ($success === true) { - $message = 'Schema fields configured successfully'; - } - - $this->trackStep( - stepNumber: 4, - stepName: 'Schema Configuration', - status: $status, - description: $message, - details: $fieldResults - ); - - if ($success === true) { - $this->logger->info( - message: '[SetupHandler] Schema field configuration completed successfully', - context: array_merge(['file' => __FILE__, 'line' => __LINE__], $fieldResults) - ); - } - - return $success; - }//end configureSchemaFields() - - /** - * Add or update a schema field with detailed tracking - * - * @param string $fieldName Name of the field - * @param array $fieldConfig Field configuration - * - * @return array Result with success status, action taken, and optional error or details. - */ - private function addOrUpdateSchemaFieldWithTracking(string $fieldName, array $fieldConfig): array - { - // First, try to add the field. - $addResult = $this->addSchemaFieldWithResult( - fieldName: $fieldName, - fieldConfig: $fieldConfig - ); - - if ($addResult['success'] === true) { - return [ - 'success' => true, - 'action' => 'added', - 'details' => $addResult, - ]; - } - - // If add failed because field exists, try to update/replace. - if (strpos($addResult['error'] ?? '', 'already exists') !== false - || strpos($addResult['error'] ?? '', 'Field') !== false - ) { - $updateResult = $this->replaceSchemaFieldWithResult( - fieldName: $fieldName, - fieldConfig: $fieldConfig - ); - - if ($updateResult['success'] === true) { - return [ - 'success' => true, - 'action' => 'updated', - 'details' => $updateResult, - ]; - } - - // Field exists but couldn't be updated - might be same config. - return [ - 'success' => true, - 'action' => 'skipped', - 'details' => ['reason' => 'Field exists with compatible configuration'], - ]; - }//end if - - // Both add and update failed. - return [ - 'success' => false, - 'action' => 'failed', - 'error' => $addResult['error'] ?? 'Unknown error', - ]; - }//end addOrUpdateSchemaFieldWithTracking() - - /** - * Add a schema field and return detailed result - * - * @param string $fieldName Name of the field - * @param array $fieldConfig Field configuration - * - * @return array Result with success status and optional error, exception_type, or solr_response. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - private function addSchemaFieldWithResult(string $fieldName, array $fieldConfig): array - { - $tenantCollectionName = $this->getTenantCollectionName(); - $url = $this->buildSolrUrl(path: '/'.$tenantCollectionName.'/schema'); - - $payload = [ - 'add-field' => array_merge(['name' => $fieldName], $fieldConfig), - ]; - - try { - $response = $this->httpClient->post( - $url, - [ - 'headers' => ['Content-Type' => 'application/json'], - 'json' => $payload, - 'timeout' => 30, - ] - ); - - if ($response->getStatusCode() !== 200) { - return [ - 'success' => false, - 'error' => 'HTTP error: '.$response->getStatusCode(), - 'response_body' => (string) $response->getBody(), - ]; - } - - $data = json_decode((string) $response->getBody(), true); - $success = ($data['responseHeader']['status'] ?? -1) === 0; - - if ($success !== true) { - return [ - 'success' => false, - 'error' => $data['error']['msg'] ?? 'SOLR error', - 'solr_response' => $data, - ]; - } - - return [ - 'success' => true, - 'solr_response' => $data, - ]; - } catch (\Exception $e) { - return [ - 'success' => false, - 'error' => $e->getMessage(), - 'exception_type' => get_class($e), - ]; - }//end try - }//end addSchemaFieldWithResult() - - /** - * Replace a schema field and return detailed result - * - * @param string $fieldName Name of the field - * @param array $fieldConfig Field configuration - * - * @return array Result with success status and optional error, exception_type, or solr_response. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - private function replaceSchemaFieldWithResult(string $fieldName, array $fieldConfig): array - { - $tenantCollectionName = $this->getTenantCollectionName(); - $url = $this->buildSolrUrl(path: '/'.$tenantCollectionName.'/schema'); - - $payload = [ - 'replace-field' => array_merge(['name' => $fieldName], $fieldConfig), - ]; - - try { - $response = $this->httpClient->post( - $url, - [ - 'headers' => ['Content-Type' => 'application/json'], - 'json' => $payload, - 'timeout' => 30, - ] - ); - - if ($response->getStatusCode() !== 200) { - return [ - 'success' => false, - 'error' => 'HTTP error: '.$response->getStatusCode(), - 'response_body' => (string) $response->getBody(), - ]; - } - - $data = json_decode((string) $response->getBody(), true); - $success = ($data['responseHeader']['status'] ?? -1) === 0; - - if ($success !== true) { - return [ - 'success' => false, - 'error' => $data['error']['msg'] ?? 'SOLR error', - 'solr_response' => $data, - ]; - } - - return [ - 'success' => true, - 'solr_response' => $data, - ]; - } catch (\Exception $e) { - return [ - 'success' => false, - 'error' => $e->getMessage(), - 'exception_type' => get_class($e), - ]; - }//end try - }//end replaceSchemaFieldWithResult() - - /** - * Get field definitions for ObjectEntity metadata fields (shared method) - * - * Based on ObjectEntity.php properties, this method returns the proper - * SOLR field type configuration for each metadata field using self_ prefixes - * and clean field names (no suffixes needed when explicitly defined). - * - * This method can be used by both setup and warmup processes to ensure - * consistent schema field configuration across all SOLR operations. - * - * @return array> Field definitions with SOLR type configuration - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Field definitions require comprehensive specification - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - public function getObjectEntityFieldDefinitions(): array - { - return [ - // **CRITICAL**: Core tenant field with self_ prefix (consistent naming). - 'self_tenant' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'required' => true, - 'docValues' => true, - // Enable faceting for tenant filtering. - ], - - // Metadata fields with self_ prefix (consistent with legacy mapping). - 'self_object_id' => [ - 'type' => 'pint', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => false, - // Not useful for faceting. - ], - 'self_uuid' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => false, - // Not useful for faceting. - ], - - // Context fields. - 'self_register' => [ - 'type' => 'pint', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => true, - // Enable faceting. - ], - 'self_schema' => [ - 'type' => 'pint', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => true, - // Enable faceting. - ], - 'self_schema_version' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => true, - // Enable faceting. - ], - - // Ownership and metadata. - 'self_owner' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => false, - // Not useful for faceting - used for ownership tracking. - ], - 'self_organisation' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => true, - // Enable faceting. - ], - 'self_application' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => true, - // Enable faceting. - ], - - // Core object fields (no suffixes needed when explicitly defined). - 'self_name' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => false, - // Not useful for faceting - used for search. - ], - 'self_description' => [ - 'type' => 'text_general', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => false, - // Not useful for faceting - used for search. - ], - 'self_summary' => [ - 'type' => 'text_general', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - ], - 'self_image' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => false, - 'multiValued' => false, - ], - 'self_slug' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - ], - 'self_uri' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - ], - 'self_version' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - ], - 'self_size' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => false, - 'multiValued' => false, - ], - 'self_folder' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - ], - - // Timestamps (SOLR date format). - 'self_created' => [ - 'type' => 'pdate', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => true, - // Enable faceting for date ranges. - ], - 'self_updated' => [ - 'type' => 'pdate', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - 'docValues' => true, - // Enable faceting for date ranges. - ], - // **NEW**: UUID relation fields for clean object relationships. - 'self_relations' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => true, - ], - 'self_files' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => true, - ], - 'self_parent_uuid' => [ - 'type' => 'string', - 'stored' => true, - 'indexed' => true, - 'multiValued' => false, - ], - ]; - }//end getObjectEntityFieldDefinitions() - - /** - * Validate that SOLR setup is complete and functional (SolrCloud) - * - * Performs final validation checks: - * 1. Base configSet exists - * 2. Base collection exists - * 3. Base collection is accessible via query - * - * @return bool True if all validation checks pass - */ - private function validateSetup(): bool - { - $tenantCollectionName = $this->getTenantCollectionName(); - - // Check tenant configSet exists. - $tenantConfigSetName = $this->getTenantConfigSetName(); - if ($this->configSetExists(configSetName: $tenantConfigSetName) === false) { - $this->logger->error( - message: '[SetupHandler] Validation failed: tenant configSet missing', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'configSet' => $tenantConfigSetName, - ] - ); - return false; - } - - // Check tenant collection exists. - if ($this->solrService->collectionExists($tenantCollectionName) === false) { - $this->logger->error( - message: '[SetupHandler] Validation failed: tenant collection missing', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $tenantCollectionName, - ] - ); - return false; - } - - // Test tenant collection query functionality. - if ($this->testCollectionQuery(collectionName: $tenantCollectionName) === false) { - $this->logger->error( - message: '[SetupHandler] Validation failed: tenant collection query test failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $tenantCollectionName, - ] - ); - return false; - } - - $this->logger->info( - message: '[SetupHandler] SOLR setup validation passed (SolrCloud mode)', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return true; - }//end validateSetup() - - /** - * Test that a collection responds to queries correctly (SolrCloud) - * - * @param string $collectionName Name of the collection to test - * - * @return bool True if collection responds to queries properly - */ - private function testCollectionQuery(string $collectionName): bool - { - $url = $this->buildSolrUrl( - path: sprintf( - '/%s/select?q=*:*&rows=0&wt=json', - urlencode($collectionName) - ) - ); - - try { - $response = $this->httpClient->get($url, ['timeout' => 10]); - $data = json_decode((string) $response->getBody(), true); - } catch (\Exception $e) { - $this->logger->warning( - message: '[SetupHandler] Failed to test collection query', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collectionName, - 'error' => $e->getMessage(), - 'url' => $url, - ] - ); - return false; - } - - // Valid response should have a response header with status 0. - return ($data['responseHeader']['status'] ?? -1) === 0; - }//end testCollectionQuery() -}//end class diff --git a/lib/Service/Index/WarmupHandler.php b/lib/Service/Index/WarmupHandler.php deleted file mode 100644 index eee0069786..0000000000 --- a/lib/Service/Index/WarmupHandler.php +++ /dev/null @@ -1,172 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-12 - * @version GIT: - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Index; - -use Exception; -use OCA\OpenRegister\Service\SolrSchemaService; -use OCP\IDBConnection; -use Psr\Log\LoggerInterface; -use ReflectionClass; - -/** - * WarmupHandler for Solr warmup operations - * - * Handles index warmup logic including: - * - Schema mirroring - * - Bulk indexing with various modes - * - Memory prediction and tracking - * - Cache warming queries - * - * @package OCA\OpenRegister\Service\Index - */ -class WarmupHandler -{ - - /** - * Search backend interface. - * - * @var SearchBackendInterface - */ - private readonly SearchBackendInterface $searchBackend; - - /** - * Bulk indexer. - * - * @var BulkIndexer - */ - private readonly BulkIndexer $bulkIndexer; - - /** - * Database connection. - * - * @var IDBConnection - */ - private readonly IDBConnection $db; - - /** - * Logger. - * - * @var LoggerInterface - */ - private readonly LoggerInterface $logger; - - /** - * WarmupHandler constructor - * - * @param SearchBackendInterface $searchBackend Search backend - * @param BulkIndexer $bulkIndexer Bulk indexer - * @param IDBConnection $db Database connection - * @param LoggerInterface $logger Logger - * - * @return void - */ - public function __construct( - SearchBackendInterface $searchBackend, - BulkIndexer $bulkIndexer, - IDBConnection $db, - LoggerInterface $logger - ) { - $this->searchBackend = $searchBackend; - $this->bulkIndexer = $bulkIndexer; - $this->db = $db; - $this->logger = $logger; - }//end __construct() - - /** - * Warm up the index - * - * Delegates to the search backend for index warmup operations. - * - * @param array $schemas Schemas to warm up. - * @param int $maxObjects Max objects to process. - * @param string $mode Warmup mode (serial, parallel, hyper). - * @param bool $collectErrors Whether to collect detailed errors. - * @param int $batchSize Batch size for processing. - * @param array $schemaIds Schema IDs to filter. - * - * @return array Results with statistics and errors. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Index warmup requires handling multiple configuration scenarios - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple warmup modes create many execution paths - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - public function warmupIndex( - array $schemas=[], - int $maxObjects=0, - string $mode='serial', - bool $collectErrors=false, - int $batchSize=1000, - array $schemaIds=[] - ): array { - $this->logger->info( - message: '[WarmupHandler] Starting index warmup', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'max_objects' => $maxObjects, - 'mode' => $mode, - 'batch_size' => $batchSize, - ] - ); - - try { - // Delegate to search backend for warmup operation. - $result = $this->searchBackend->warmupIndex( - $schemas, - $maxObjects, - $mode, - $collectErrors, - $batchSize, - $schemaIds - ); - - $this->logger->info( - message: '[WarmupHandler] Index warmup completed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'success' => $result['success'] ?? false, - 'objects_indexed' => $result['operations']['objects_indexed'] ?? 0, - ] - ); - - return $result; - } catch (Exception $e) { - $this->logger->error( - message: '[WarmupHandler] Index warmup failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end warmupIndex() -}//end class diff --git a/lib/Service/IndexService.php b/lib/Service/IndexService.php deleted file mode 100644 index 1da8c99e5c..0000000000 --- a/lib/Service/IndexService.php +++ /dev/null @@ -1,975 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @version GIT: - * @link https://www.OpenRegister.nl - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service; - -use Exception; -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Service\Index\FileHandler; -use OCA\OpenRegister\Service\Index\ObjectHandler; -use OCA\OpenRegister\Service\Index\SchemaHandler; -use OCA\OpenRegister\Service\Index\SearchBackendInterface; -use Psr\Log\LoggerInterface; - -/** - * IndexService - * - * Coordinates indexing operations across different entity types. - * - * ARCHITECTURE: - * - Acts as main entry point for indexing operations. - * - Delegates to specialized handlers (FileHandler, ObjectHandler, SchemaHandler). - * - Provides unified API for controllers and other services. - * - Does NOT extract text or vectorize - only indexes existing database data. - * - * RESPONSIBILITIES: - * - Coordinate file chunk indexing (via FileHandler). - * - Coordinate object indexing and search (via ObjectHandler). - * - Coordinate schema management (via SchemaHandler). - * - Provide unified statistics and health checks. - * - Listen to database events and trigger indexing. - * - * @category Service - * @package OCA\OpenRegister\Service - * - * @SuppressWarnings(PHPMD.TooManyPublicMethods) Facade pattern requires many public methods for unified API - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complex indexing orchestration across multiple handlers - */ -class IndexService -{ - /** - * Constructor - * - * @param FileHandler $fileHandler Handler for file/chunk operations - * @param ObjectHandler $objectHandler Handler for object operations - * @param SchemaHandler $schemaHandler Handler for schema operations - * @param SearchBackendInterface $searchBackend Search backend implementation - * @param LoggerInterface $logger Logger - */ - public function __construct( - private readonly FileHandler $fileHandler, - private readonly ObjectHandler $objectHandler, - private readonly SchemaHandler $schemaHandler, - private readonly SearchBackendInterface $searchBackend, - private readonly LoggerInterface $logger - ) { - }//end __construct() - - // ======================================================================== - // FILE OPERATIONS - // ======================================================================== - - /** - * Index file chunks to search backend. - * - * Delegates to FileHandler. - * - * @param int $fileId File ID - * @param array $chunks Array of chunk entities from database - * @param array $metadata File metadata - * - * @return (bool|int|string)[] - * - * @psalm-return array{success: bool, indexed: int<0, max>, collection: 'files'} - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-3 - */ - public function indexFileChunks(int $fileId, array $chunks, array $metadata): array - { - return $this->fileHandler->indexFileChunks(fileId: $fileId, chunks: $chunks, metadata: $metadata); - }//end indexFileChunks() - - /** - * Process and index unindexed file chunks. - * - * Delegates to FileHandler. - * - * @param int|null $limit Maximum number of files to process - * - * @return array Result with success status and processing stats. - * - * @spec exclude Facade plumbing: thin delegation to FileHandler::processUnindexedChunks (search-index), no standalone contract. - */ - public function processUnindexedChunks(?int $limit=null): array - { - return $this->fileHandler->processUnindexedChunks(limit: $limit); - }//end processUnindexedChunks() - - /** - * Get file indexing statistics. - * - * Delegates to FileHandler. - * - * @return (bool|int|string)[] Statistics - * - * @psalm-return array{available: bool, collection?: string, document_count?: int, error?: string} - */ - public function getFileStats(): array - { - return $this->fileHandler->getFileStats(); - }//end getFileStats() - - /** - * Get chunking statistics. - * - * Delegates to FileHandler. - * - * @return int[] Statistics - * - * @psalm-return array{total_chunks: int, indexed_chunks: int, unindexed_chunks: int, vectorized_chunks: int} - */ - public function getChunkingStats(): array - { - return $this->fileHandler->getChunkingStats(); - }//end getChunkingStats() - - // ======================================================================== - // OBJECT OPERATIONS - // ======================================================================== - - /** - * Search objects in search backend. - * - * Delegates to ObjectHandler. - * - * @param array $query Search query - * @param bool $_rbac Apply RBAC filters - * @param bool $_multitenancy Apply multitenancy filters - * @param bool $deleted Include deleted objects - * - * @return (array|int|mixed)[] Search results - * - * @psalm-return array{results: array|mixed, total: 0|mixed, start: 0|mixed} - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags required for flexible API filtering - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-9 - */ - public function searchObjects( - array $query=[], - bool $_rbac=true, - bool $_multitenancy=true, - bool $deleted=false - ): array { - return $this->objectHandler->searchObjects( - query: $query, - _rbac: $_rbac, - _multitenancy: $_multitenancy, - deleted: $deleted - ); - }//end searchObjects() - - /** - * Commit pending changes to search backend. - * - * Delegates to ObjectHandler. - * - * @return bool Success status - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function commit(): bool - { - return $this->objectHandler->commit(); - }//end commit() - - /** - * Index an object to the search backend. - * - * Delegates to SearchBackendInterface. - * - * @param ObjectEntity $object Object entity to index - * @param bool $commit Whether to commit immediately - * - * @return bool Success status - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Commit flag controls transaction behavior - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function indexObject(ObjectEntity $object, bool $commit=false): bool - { - return $this->searchBackend->indexObject(object: $object, commit: $commit); - }//end indexObject() - - /** - * Delete an object from the search backend. - * - * Delegates to SearchBackendInterface. - * - * @param string|int $objectId Object ID or UUID to delete - * @param bool $commit Whether to commit immediately - * - * @return bool Success status - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Commit flag controls transaction behavior - * - * @spec exclude Facade plumbing: thin delegation to SearchBackendInterface::deleteObject (search-index), no standalone contract. - */ - public function deleteObject(string|int $objectId, bool $commit=false): bool - { - return $this->searchBackend->deleteObject(objectId: $objectId, commit: $commit); - }//end deleteObject() - - // ======================================================================== - // SCHEMA OPERATIONS - // ======================================================================== - - /** - * Ensure vector field type exists in a collection. - * - * Delegates to SchemaHandler. - * - * @param string $collection Collection name - * @param int $dimensions Vector dimensions - * @param string $similarity Similarity function - * - * @return bool Success status - * - * @spec exclude Facade plumbing: thin delegation to SchemaHandler::ensureVectorFieldType (search-index), no standalone contract. - */ - public function ensureVectorFieldType( - string $collection, - int $dimensions=4096, - string $similarity='cosine' - ): bool { - return $this->schemaHandler->ensureVectorFieldType( - collection: $collection, - dimensions: $dimensions, - similarity: $similarity - ); - }//end ensureVectorFieldType() - - /** - * Mirror OpenRegister schemas to search backend. - * - * Delegates to SchemaHandler. - * - * @param bool $force Force recreation of existing fields - * - * @return array Result with success status, stats, and optional errors. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Force flag controls schema recreation behavior - * - * @spec exclude Facade plumbing: thin delegation to SchemaHandler::mirrorSchemas (search-index), no standalone contract. - */ - public function mirrorSchemas(bool $force=false): array - { - return $this->schemaHandler->mirrorSchemas(force: $force); - }//end mirrorSchemas() - - /** - * Get collection field status. - * - * Delegates to SchemaHandler. - * - * @param string $collection Collection name - * - * @return array Field status with collection, existing and missing fields, and counts. - */ - public function getCollectionFieldStatus(string $collection): array - { - return $this->schemaHandler->getCollectionFieldStatus(collection: $collection); - }//end getCollectionFieldStatus() - - /** - * Get object collection field status. - * - * Delegates to SchemaHandler. - * - * @return array Field status with collection, existing and missing fields, and counts. - */ - public function getObjectCollectionFieldStatus(): array - { - return $this->schemaHandler->getCollectionFieldStatus(collection: 'objects'); - }//end getObjectCollectionFieldStatus() - - /** - * Get fields configuration from search backend. - * - * Delegates to SearchBackendInterface. - * - * @return (array|true)[] Fields configuration - * - * @psalm-return array{success: true, fields: array} - * - * @spec exclude Facade plumbing: wraps SearchBackendInterface::getFields in a success envelope (search-index), no standalone contract. - */ - public function getFieldsConfiguration(): array - { - // Get fields from the objects collection. - $fields = $this->searchBackend->getFields(collection: 'objects'); - return [ - 'success' => true, - 'fields' => $fields, - ]; - }//end getFieldsConfiguration() - - /** - * Create missing fields in a collection. - * - * Delegates to SchemaHandler. - * - * @param string $collection Collection name - * @param array $missingFields Missing field definitions - * @param bool $dryRun Preview without making changes - * - * @return array Result with success status and field creation info. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Dry-run flag enables preview mode - * - * @spec exclude Facade plumbing: thin delegation to SchemaHandler::createMissingFields (search-index), no standalone contract. - */ - public function createMissingFields(string $collection, array $missingFields, bool $dryRun=false): array - { - return $this->schemaHandler->createMissingFields( - collection: $collection, - missingFields: $missingFields, - dryRun: $dryRun - ); - }//end createMissingFields() - - // ======================================================================== - // GENERAL OPERATIONS - // ======================================================================== - - /** - * Check if search backend is available. - * - * @param bool $forceRefresh Force fresh check - * - * @return bool Availability status - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Force-refresh flag bypasses cache - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function isAvailable(bool $forceRefresh=false): bool - { - try { - return $this->searchBackend->isAvailable(forceRefresh: $forceRefresh); - } catch (Exception $e) { - $this->logger->error( - message: '[IndexService] Failed to check availability', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end isAvailable() - - /** - * Test connection to search backend. - * - * @param bool $inclCollTests Include collection tests - * - * @return array Test results - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flag controls test verbosity - * - * @spec exclude Facade plumbing: delegate to SearchBackendInterface::testConnection with error envelope (search-index), no standalone contract. - */ - public function testConnection(bool $inclCollTests=true): array - { - try { - return $this->searchBackend->testConnection(inclCollTests: $inclCollTests); - } catch (Exception $e) { - $this->logger->error( - message: '[IndexService] Connection test failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'available' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end testConnection() - - /** - * Get search backend statistics. - * - * @return array Statistics - * - * @spec exclude Facade plumbing: delegate to SearchBackendInterface::getStats with error envelope (search-index), no standalone contract. - */ - public function getStats(): array - { - try { - return $this->searchBackend->getStats(); - } catch (Exception $e) { - $this->logger->error( - message: '[IndexService] Failed to get stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'available' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end getStats() - - /** - * Get comprehensive dashboard statistics. - * - * Combines statistics from all handlers. - * - * @return (array|bool|string)[] Dashboard statistics - * - * @psalm-return array{available: bool, error?: string, backend?: array, - * files?: array{available: bool, collection?: string, - * document_count?: int, error?: string}, - * chunks?: array{total_chunks: int, indexed_chunks: int, - * unindexed_chunks: int, vectorized_chunks: int}} - * - * @spec exclude Facade plumbing: aggregates backend/file/chunk stats from collaborators (search-index), no standalone contract. - */ - public function getDashboardStats(): array - { - try { - $backendStats = $this->searchBackend->getStats(); - $fileStats = $this->fileHandler->getFileStats(); - $chunkStats = $this->fileHandler->getChunkingStats(); - - return [ - 'available' => $this->isAvailable(), - 'backend' => $backendStats, - 'files' => $fileStats, - 'chunks' => $chunkStats, - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[IndexService] Failed to get dashboard stats', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'available' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end getDashboardStats() - - /** - * Optimize the search backend. - * - * @return bool Success status - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-search-index-backend/tasks.md#task-8 - */ - public function optimize(): bool - { - try { - return $this->searchBackend->optimize(); - } catch (Exception $e) { - $this->logger->error( - message: '[IndexService] Optimization failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return false; - }//end try - }//end optimize() - - /** - * Clear all documents from a collection. - * - * @param string|null $collectionName Collection to clear - * - * @return array Result - * - * @spec exclude Facade plumbing: delegate to SearchBackendInterface::clearIndex with error envelope (search-index), no standalone contract. - */ - public function clearIndex(?string $collectionName=null): array - { - try { - return $this->searchBackend->clearIndex(collectionName: $collectionName); - } catch (Exception $e) { - $this->logger->error( - message: '[IndexService] Failed to clear index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collectionName, - 'error' => $e->getMessage(), - ] - ); - - return [ - 'success' => false, - 'error' => $e->getMessage(), - ]; - }//end try - }//end clearIndex() - - /** - * Get search backend configuration. - * - * @return array Configuration - * - * @spec exclude Facade plumbing: delegate to SearchBackendInterface::getConfig with fail-soft empty (search-index), no standalone contract. - */ - public function getConfig(): array - { - try { - return $this->searchBackend->getConfig(); - } catch (Exception $e) { - $this->logger->error( - message: '[IndexService] Failed to get config', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - - return []; - }//end try - }//end getConfig() - - /** - * Reindex all objects in the system. - * - * Delegates to ObjectHandler for full reindexing operation. - * - * @param int $maxObjects Maximum objects to reindex (0 = all). - * @param int $batchSize Batch size for reindexing. - * @param string|null $collectionName Optional collection name. - * - * @return array Reindexing results with statistics. - * - * @spec exclude Facade plumbing: thin delegation to ObjectHandler::reindexAll (search-index), no standalone contract. - */ - public function reindexAll(int $maxObjects=0, int $batchSize=1000, ?string $collectionName=null): array - { - return $this->objectHandler->reindexAll( - maxObjects: $maxObjects, - batchSize: $batchSize, - collectionName: $collectionName - ); - }//end reindexAll() - - /** - * Fix mismatched fields in the schema. - * - * Delegates to SchemaHandler for field type corrections. - * - * @param array $mismatchedFields Fields to fix. - * @param bool $dryRun Whether to only simulate (not apply). - * - * @return array Results with fixed/failed fields. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Dry-run flag enables preview mode - * - * @spec exclude Facade plumbing: thin delegation to SchemaHandler::fixMismatchedFields (search-index), no standalone contract. - */ - public function fixMismatchedFields(array $mismatchedFields, bool $dryRun=false): array - { - return $this->schemaHandler->fixMismatchedFields( - mismatchedFields: $mismatchedFields, - dryRun: $dryRun - ); - }//end fixMismatchedFields() - - /** - * Index files by their IDs. - * - * Delegates to FileHandler for file indexing operations. - * - * @param array $fileIds Array of file IDs to index. - * @param string|null $collectionName Optional collection name. - * - * @return array Indexing results. - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function indexFiles(array $fileIds, ?string $collectionName=null): array - { - return $this->fileHandler->indexFiles( - fileIds: $fileIds, - collectionName: $collectionName - ); - }//end indexFiles() - - /** - * Get file indexing statistics. - * - * Delegates to FileHandler for file index statistics. - * - * @return array File indexing stats. - */ - public function getFileIndexStats(): array - { - return $this->fileHandler->getFileIndexStats(); - }//end getFileIndexStats() - - /** - * Warm up the search index. - * - * Pre-loads data into cache and performs initial indexing operations. - * - * @param array $schemas Array of schema IDs to warm up. - * @param int $maxObjects Maximum number of objects to warm up. - * @param string $mode Warmup mode (serial, parallel, hyper). - * @param bool $collectErrors Whether to collect detailed error information. - * @param int $batchSize Batch size for warmup operations. - * @param array $schemaIds Schema IDs to filter warmup. - * - * @return array Warmup results with statistics and errors. - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flag controls error collection verbosity - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-5 - */ - public function warmupIndex( - array $schemas=[], - int $maxObjects=0, - string $mode='serial', - bool $collectErrors=false, - int $batchSize=1000, - array $schemaIds=[] - ): array { - return $this->searchBackend->warmupIndex( - schemas: $schemas, - maxObjects: $maxObjects, - mode: $mode, - collectErrors: $collectErrors, - batchSize: $batchSize, - schemaIds: $schemaIds - ); - }//end warmupIndex() - - // ======================================================================== - // BACKEND ACCESS & DELEGATION METHODS (Restored for compatibility) - // ======================================================================== - - /** - * Get the search backend instance. - * - * Provides direct access to the backend for advanced operations. - * - * @return SearchBackendInterface Search backend instance - */ - public function getBackend(): SearchBackendInterface - { - return $this->searchBackend; - }//end getBackend() - - /** - * Search objects with pagination. - * - * Delegates to search backend. - * - * @param array $query Search query parameters - * @param int $limit Maximum results per page - * @param int $offset Offset for pagination - * @param array $facets Facet configuration - * @param string|null $collection Collection name - * @param bool $includeTotal Whether to include total count - * - * @return array Search results with pagination info - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Flag controls total count inclusion - * - * @spec exclude Facade plumbing: maps IndexService params onto the query array then delegate to - * SearchBackendInterface::searchObjectsPaginated (search-index), no standalone contract. - */ - public function searchObjectsPaginated( - array $query=[], - int $limit=30, - int $offset=0, - array $facets=[], - ?string $collection=null, - bool $includeTotal=true - ): array { - // Map IndexService parameters to SearchBackendInterface parameters. - // Add pagination and other params to query array. - $query['_limit'] = $limit; - $query['_offset'] = $offset; - if (empty($facets) === false) { - $query['_facets'] = $facets; - } - - if ($collection !== null) { - $query['_collection'] = $collection; - } - - $query['_includeTotal'] = $includeTotal; - - return $this->searchBackend->searchObjectsPaginated( - query: $query, - _rbac: true, - _multitenancy: true, - deleted: false - ); - }//end searchObjectsPaginated() - - /** - * Get document count in the index. - * - * Returns the total number of documents currently indexed. - * - * @return int Document count - */ - public function getDocumentCount(): int - { - return $this->searchBackend->getDocumentCount(); - }//end getDocumentCount() - - /** - * Check if a collection exists. - * - * @param string $collectionName Collection name to check - * - * @return bool True if collection exists - * - * @spec openspec/changes/retrofit-2026-05-24-search-index/tasks.md#task-1 - */ - public function collectionExists(string $collectionName): bool - { - return $this->searchBackend->collectionExists(collectionName: $collectionName); - }//end collectionExists() - - /** - * Create a new collection. - * - * @param string $name Collection name - * @param array $config Configuration options - * - * @return array Creation result - * - * @spec exclude Facade plumbing: thin delegation to SearchBackendInterface::createCollection (search-index), no standalone contract. - */ - public function createCollection(string $name, array $config=[]): array - { - return $this->searchBackend->createCollection(name: $name, config: $config); - }//end createCollection() - - /** - * Test connectivity only (without collection tests). - * - * Quick connectivity check without full collection validation. - * - * @return array Connection test results - * - * @spec exclude Facade plumbing: convenience wrapper over testConnection(inclCollTests:false) (search-index), no standalone contract. - */ - public function testConnectivityOnly(): array - { - return $this->testConnection(inclCollTests: false); - }//end testConnectivityOnly() - - // ======================================================================== - // SOLR-SPECIFIC METHODS (Restored for compatibility) - // ======================================================================== - - /** - * Ensure tenant-specific collection exists. - * - * Creates collection if it doesn't exist, for multi-tenancy support. - * Only works with Solr backend. - * - * @param string|null $tenant Tenant identifier - * - * @return (null|string|true)[] Collection info - * - * @throws Exception If backend is not Solr - * - * @psalm-return array{collection: string, exists: true, tenant: null|string} - * - * @spec exclude Facade plumbing: composes getTenantSpecificCollectionName + collectionExists + createCollection - * (search-index), no standalone contract. - */ - public function ensureTenantCollection(?string $tenant=null): array - { - $collectionName = $this->getTenantSpecificCollectionName(tenant: $tenant); - - if ($this->collectionExists(collectionName: $collectionName) === false) { - $this->createCollection(name: $collectionName); - } - - return [ - 'collection' => $collectionName, - 'exists' => true, - 'tenant' => $tenant, - ]; - }//end ensureTenantCollection() - - /** - * Get tenant-specific collection name. - * - * Generates collection name based on tenant for multi-tenancy. - * - * @param string|null $tenant Tenant identifier (null for default) - * - * @return string Collection name - * - * @spec exclude Facade plumbing: derives a collection name string from config + tenant suffix (search-index), no standalone contract. - */ - public function getTenantSpecificCollectionName(?string $tenant=null): string - { - $baseName = $this->getConfig()['collection'] ?? 'openregister'; - - if ($tenant !== null && empty($tenant) === false) { - return $baseName.'_'.$tenant; - } - - return $baseName; - }//end getTenantSpecificCollectionName() - - /** - * Get Solr endpoint URL. - * - * Returns the base URL for Solr API endpoints. - * Only works with Solr backend. - * - * @return string Endpoint URL - * - * @throws Exception If backend is not Solr - * - * @spec exclude Facade plumbing: reads endpoint from getSolrConfig (search-index), no standalone contract. - */ - public function getEndpointUrl(): string - { - $config = $this->getSolrConfig(); - return $config['endpoint'] ?? ''; - }//end getEndpointUrl() - - /** - * Build Solr base URL. - * - * Constructs the base URL for Solr operations. - * Only works with Solr backend. - * - * @param string|null $collection Optional collection name - * - * @return string Solr base URL - * - * @throws Exception If backend is not Solr - * - * @spec exclude Facade plumbing: string-builds a Solr base URL from getSolrConfig (search-index), no standalone contract. - */ - public function buildSolrBaseUrl(?string $collection=null): string - { - $config = $this->getSolrConfig(); - $baseUrl = rtrim($config['endpoint'] ?? '', '/'); - - if ($collection !== null) { - return $baseUrl.'/solr/'.$collection; - } - - return $baseUrl.'/solr'; - }//end buildSolrBaseUrl() - - /** - * Get Solr-specific configuration. - * - * Returns configuration specific to Solr backend. - * Only works with Solr backend. - * - * @return (int|mixed|string)[] Solr configuration - * - * @throws Exception If backend is not Solr - * - * @psalm-return array{endpoint: ''|mixed, collection: 'openregister'|mixed, username: ''|mixed, - * password: ''|mixed, timeout: 30|mixed} - * - * @spec exclude Facade plumbing: projects getConfig into Solr-specific keys with defaults (search-index), no standalone contract. - */ - public function getSolrConfig(): array - { - $config = $this->getConfig(); - - // Extract Solr-specific config. - return [ - 'endpoint' => $config['endpoint'] ?? '', - 'collection' => $config['collection'] ?? 'openregister', - 'username' => $config['username'] ?? '', - 'password' => $config['password'] ?? '', - 'timeout' => $config['timeout'] ?? 30, - ]; - }//end getSolrConfig() - - /** - * Get HTTP client for Solr operations. - * - * Returns the HTTP client used for Solr communication. - * Only works with Solr backend. - * - * @return object HTTP client instance - * - * @throws Exception If backend is not Solr or client not available - * - * @spec exclude Facade plumbing: method_exists guard then delegate to the Solr backend's getHttpClient (search-index), no standalone contract. - */ - public function getHttpClient(): object - { - // Check if backend is Solr and has getHttpClient method. - if (method_exists($this->searchBackend, 'getHttpClient') === true) { - return $this->searchBackend->getHttpClient(); - } - - throw new Exception('HTTP client not available for current backend'); - }//end getHttpClient() - - /** - * List available ConfigSets from Solr. - * - * This is a Solr-specific operation. Returns an empty array if the - * backend doesn't support ConfigSets (non-Solr backends). - * - * @return array List of available ConfigSets. - * - * @spec exclude Facade plumbing: method_exists guard then delegate to the Solr backend's listConfigSets, - * empty for non-Solr (search-index), no standalone contract. - */ - public function listConfigSets(): array - { - // Check if backend is Solr and has listConfigSets method. - if (method_exists($this->searchBackend, 'listConfigSets') === true) { - return $this->searchBackend->listConfigSets(); - } - - // Return empty array for non-Solr backends. - return []; - }//end listConfigSets() -}//end class diff --git a/lib/Service/Integration/ExternalIntegrationRouter.php b/lib/Service/Integration/ExternalIntegrationRouter.php index 7cfabc40c0..1687c6cc66 100644 --- a/lib/Service/Integration/ExternalIntegrationRouter.php +++ b/lib/Service/Integration/ExternalIntegrationRouter.php @@ -60,7 +60,7 @@ class ExternalIntegrationRouter * * Null means "not yet checked" — the first call resolves it. * - * @var bool|null + * @var boolean|null */ private ?bool $connectorAvailable = null; @@ -123,6 +123,18 @@ public function call( $sourceId = (string) $provider->getOpenConnectorSource(); $source = $this->loadSource(sourceId: $sourceId, providerId: $provider->getId()); + // Mock mode: when the resolved source is flagged + // `configuration.mock === true`, short-circuit and return the canned + // `configuration.mockResponse` body WITHOUT performing a real HTTP + // call — so the KvK / OpenCorporates / BRP / SMS / WhatsApp leaves are + // demonstrably functional end-to-end without real credentials. The + // real path below stays 100% intact for non-mock sources; mock is + // opt-in per source. {@see resolveMockBody()} for the resolution. + $config = $this->readSourceConfiguration(source: $source); + if (($config['mock'] ?? false) === true) { + return $this->resolveMockBody(config: $config, sourceId: $sourceId); + } + try { return $this->invoke(source: $source, method: $method, path: $path, options: $options); } catch (ProviderUnavailableException $e) { @@ -152,6 +164,94 @@ public function call( }//end try }//end call() + /** + * Dispatch a call like {@see call()} but additionally surface the + * upstream response metadata (HTTP status, round-trip duration in + * milliseconds, and the response headers) alongside the decoded body. + * + * This is a general, foundation-safe superset of {@see call()}: any + * external leaf that needs to relay audit metadata to its consuming app + * (e.g. the BRP/HaalCentraal leaf, whose consumer persists the + * Wet-BRP-required `X-Correlation-ID` + response duration into its + * `brpLookupVerzoek` audit record) can route through this method instead + * of `call()`. `call()` is intentionally left untouched so existing + * leaves keep their lean body-only contract. + * + * The returned envelope is: + * - `body` : the decoded upstream response body (identical to what + * `call()` returns) + * - `meta` : `{ status, durationMs, correlationId, headers }` — the + * upstream HTTP status, the OpenConnector-measured round-trip + * duration in milliseconds, the first `X-Correlation-ID` + * response header (case-insensitive; null when absent), and a + * flattened copy of the response headers. + * + * No request/response body or BSN is read into `meta` — only transport + * metadata. The same failure classification as `call()` applies. + * + * @param IntegrationProvider $provider The provider making the call. + * @param string $method HTTP method. + * @param string $path Path relative to the source base URL. + * @param array $options Optional call options (query/body/headers). + * + * @return array{body: array, meta: array{status: int, durationMs: int, correlationId: ?string, headers: array}} + * + * @throws ProviderUnavailableException When OpenConnector or the upstream + * service is unavailable. + * + * @spec openspec/changes/integration-brp-audit-metadata/tasks.md + */ + public function callWithMeta( + IntegrationProvider $provider, + string $method, + string $path, + array $options=[], + ): array { + $this->assertProviderIsExternal(provider: $provider); + $this->assertOpenConnectorAvailable(); + + $sourceId = (string) $provider->getOpenConnectorSource(); + $source = $this->loadSource(sourceId: $sourceId, providerId: $provider->getId()); + + // Mock mode: short-circuit a flagged source with the canned body PLUS a + // synthesized `meta` envelope (fake correlationId / durationMs / + // status:200) so a meta-consuming leaf (e.g. the BRP/HaalCentraal leaf, + // which persists the Wet-BRP `X-Correlation-ID` + duration into its + // audit record) gets a fully-shaped response without a real call. + $config = $this->readSourceConfiguration(source: $source); + if (($config['mock'] ?? false) === true) { + return [ + 'body' => $this->resolveMockBody(config: $config, sourceId: $sourceId), + 'meta' => $this->mockMeta(config: $config), + ]; + } + + try { + return $this->invokeWithMeta(source: $source, method: $method, path: $path, options: $options); + } catch (ProviderUnavailableException $e) { + // Already classified — surface as-is. + throw $e; + } catch (\Throwable $e) { + $this->logger->error( + sprintf( + '[ExternalIntegrationRouter] upstream call (with meta) failed for provider %s %s %s', + $provider->getId(), + $method, + $path + ), + ['exception' => $e] + ); + throw new ProviderUnavailableException( + message: sprintf( + 'Upstream service for integration "%s" is unreachable.', + $provider->getId() + ), + cause: ProviderUnavailableException::CAUSE_UPSTREAM_SERVICE_DOWN, + previous: $e + ); + }//end try + }//end callWithMeta() + /** * Cheap "is the connector reachable at all" check. * @@ -316,6 +416,136 @@ private function loadSource(string $sourceId, string $providerId) }//end try }//end loadSource() + /** + * Read the `configuration` array off a resolved OpenConnector source, + * whatever concrete shape the SourceMapper handed back. OpenConnector + * sources are OpenRegister `ObjectEntity` objects whose payload lives + * under `getObject()`; older builds returned a plain array or a + * `jsonSerialize`-able entity. This reads the `configuration` sub-array + * (where the mock flag + canned fixture live) defensively across all + * three shapes, returning an empty array when there is none. + * + * This is the ONLY place the router introspects the source body — and it + * reads transport configuration only (never a credential), so the mock + * short-circuit stays foundation-safe and additive. + * + * @param mixed $source The resolved source entity. + * + * @return array The source's `configuration` array (possibly empty). + * + * @spec openspec/changes/integration-mock-mode/tasks.md + */ + private function readSourceConfiguration($source): array + { + $data = null; + + if (is_array($source) === true) { + $data = $source; + } else if (is_object($source) === true && method_exists($source, 'getObject') === true) { + $data = $source->getObject(); + } else if (is_object($source) === true && method_exists($source, 'jsonSerialize') === true) { + $data = $source->jsonSerialize(); + } else if (is_object($source) === true && method_exists($source, 'getConfiguration') === true) { + $config = $source->getConfiguration(); + if (is_array($config) === true) { + return $config; + } + + return []; + } + + if (is_array($data) === false) { + return []; + } + + $config = ($data['configuration'] ?? []); + if (is_array($config) === true) { + return $config; + } + + return []; + }//end readSourceConfiguration() + + /** + * Resolve the canned mock body for a flagged source. + * + * The realistic, upstream-shaped fixture is taken from + * `configuration.mockResponse` on the source (so each leaf's fixture lives + * with its source fragment). When a source is flagged `mock:true` but + * carries no `mockResponse`, an empty `{}` body is returned — the leaf's + * own extractor then yields an empty result set rather than fataling, so + * mock mode never produces a 500. + * + * @param array $config The source's `configuration` array. + * @param string $sourceId The source slug (diagnostics only). + * + * @return array The canned upstream-shaped body. + * + * @spec openspec/changes/integration-mock-mode/tasks.md + */ + private function resolveMockBody(array $config, string $sourceId): array + { + $body = ($config['mockResponse'] ?? []); + if (is_array($body) === false) { + $this->logger->warning( + sprintf( + '[ExternalIntegrationRouter] mock source "%s" has a non-array mockResponse; returning empty body.', + $sourceId + ) + ); + return []; + } + + return $body; + }//end resolveMockBody() + + /** + * Synthesize the `meta` envelope for a mock `callWithMeta()` response. A + * source may override any field via `configuration.mockMeta`; otherwise a + * realistic default is produced — `status:200`, a small non-zero + * `durationMs`, and a fresh fake `correlationId` (so a BRP-style consumer + * that persists the Wet-BRP `X-Correlation-ID` always has a value). No real + * call is made, so the `headers` map carries only the synthesized + * correlation header. + * + * @param array $config The source's `configuration` array. + * + * @return array{status: int, durationMs: int, correlationId: ?string, headers: array} + * + * @spec openspec/changes/integration-mock-mode/tasks.md + */ + private function mockMeta(array $config): array + { + $override = ($config['mockMeta'] ?? []); + if (is_array($override) === false) { + $override = []; + } + + $correlationId = ($override['correlationId'] ?? ('MOCK-CID-'.bin2hex(random_bytes(6)))); + if ($correlationId !== null) { + $correlationId = (string) $correlationId; + } + + $status = (int) ($override['status'] ?? 200); + $durationMs = (int) ($override['durationMs'] ?? 12); + + $headers = ($override['headers'] ?? []); + if (is_array($headers) === false) { + $headers = []; + } + + if ($correlationId !== null && isset($headers['X-Correlation-ID']) === false) { + $headers['X-Correlation-ID'] = $correlationId; + } + + return [ + 'status' => $status, + 'durationMs' => $durationMs, + 'correlationId' => $correlationId, + 'headers' => $this->flattenHeaders(headers: $headers), + ]; + }//end mockMeta() + /** * Invoke the upstream call via OpenConnector's CallService. * @@ -354,6 +584,158 @@ private function invoke($source, string $method, string $path, array $options): ); }//end invoke() + /** + * Invoke the upstream call like {@see invoke()} but return both the + * decoded body and the extracted response metadata. Keeps the same + * CallService method-name fallback + >= 400 status assertion. + * + * @param mixed $source Resolved source entity. + * @param string $method HTTP method. + * @param string $path Path relative to source base URL. + * @param array $options Call options (query / body / headers). + * + * @return array{body: array, meta: array{status: int, durationMs: int, correlationId: ?string, headers: array}} + * + * @throws \RuntimeException When CallService is unreachable. The caller + * wraps this as ProviderUnavailableException. + */ + private function invokeWithMeta($source, string $method, string $path, array $options): array + { + $callService = $this->container->get('OCA\\OpenConnector\\Service\\CallService'); + + if (method_exists($callService, 'call') === true) { + $response = $callService->call($source, $path, $method, $options); + $this->assertUpstreamOk(response: $response); + return [ + 'body' => $this->decodeResponse(response: $response), + 'meta' => $this->extractMeta(response: $response), + ]; + } + + if (method_exists($callService, 'request') === true) { + $response = $callService->request($source, $method, $path, $options); + $this->assertUpstreamOk(response: $response); + return [ + 'body' => $this->decodeResponse(response: $response), + 'meta' => $this->extractMeta(response: $response), + ]; + } + + throw new RuntimeException( + 'OCA\\OpenConnector\\Service\\CallService does not expose a known call/request method.' + ); + }//end invokeWithMeta() + + /** + * Extract transport metadata from a CallService response (OpenConnector + * CallLog). Reads ONLY the HTTP status, the OpenConnector-measured + * round-trip duration (`responseTime`, milliseconds), and the response + * headers — never the request/response body, so no BSN or payload data + * ever lands in `meta`. + * + * The CallLog's `getResponse()` payload is + * `{ statusCode, responseTime, headers, body, encoding, … }`. The + * `X-Correlation-ID` response header (case-insensitive) is surfaced as + * `correlationId`. Headers are flattened to `array` + * (Guzzle returns `array`). + * + * @param mixed $response The raw return from CallService. + * + * @return array{status: int, durationMs: int, correlationId: ?string, headers: array} + */ + private function extractMeta($response): array + { + $meta = [ + 'status' => 0, + 'durationMs' => 0, + 'correlationId' => null, + 'headers' => [], + ]; + + if (is_object($response) === true && method_exists($response, 'getStatusCode') === true) { + $meta['status'] = (int) $response->getStatusCode(); + } + + $payload = null; + if (is_object($response) === true && method_exists($response, 'getResponse') === true) { + $payload = $response->getResponse(); + } else if (is_array($response) === true) { + $payload = $response; + } + + if (is_array($payload) === false) { + return $meta; + } + + if ($meta['status'] === 0 && isset($payload['statusCode']) === true) { + $meta['status'] = (int) $payload['statusCode']; + } + + if (isset($payload['responseTime']) === true && is_numeric($payload['responseTime']) === true) { + $meta['durationMs'] = (int) round((float) $payload['responseTime']); + } + + $headers = ($payload['headers'] ?? []); + if (is_array($headers) === true) { + $meta['headers'] = $this->flattenHeaders(headers: $headers); + $meta['correlationId'] = $this->firstHeaderValue(headers: $headers, name: 'X-Correlation-ID'); + } + + return $meta; + }//end extractMeta() + + /** + * Flatten Guzzle-style `array` response headers to + * `array` (first value per header). + * + * @param array $headers Raw headers. + * + * @return array + */ + private function flattenHeaders(array $headers): array + { + $out = []; + foreach ($headers as $name => $value) { + if (is_array($value) === true) { + $value = ($value[0] ?? ''); + } + + $out[(string) $name] = (string) $value; + } + + return $out; + }//end flattenHeaders() + + /** + * Case-insensitive lookup of the first value of a named response header. + * + * @param array $headers Raw (possibly array-valued) headers. + * @param string $name Header name to find. + * + * @return string|null The first value, or null when the header is absent/empty. + */ + private function firstHeaderValue(array $headers, string $name): ?string + { + $needle = strtolower($name); + foreach ($headers as $headerName => $value) { + if (strtolower((string) $headerName) !== $needle) { + continue; + } + + if (is_array($value) === true) { + $value = ($value[0] ?? null); + } + + if ($value === null || $value === '') { + return null; + } + + return (string) $value; + } + + return null; + }//end firstHeaderValue() + /** * Treat a >= 400 upstream status (carried on the CallLog OpenConnector * returns) as an upstream failure rather than letting an error page @@ -404,8 +786,12 @@ private function assertUpstreamOk($response): void * * @return array * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Handles four distinct OpenConnector response shapes (CallLog, array, jsonSerialize, string) across multiple OC versions; each branch is a required shape check. - * @SuppressWarnings(PHPMD.NPathComplexity) Handles four distinct OpenConnector response shapes (CallLog, array, jsonSerialize, string) across multiple OC versions; each branch is a required shape check. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Handles four distinct OpenConnector response shapes + * (CallLog, array, jsonSerialize, string) across multiple OC versions; each branch is a required + * shape check. + * @SuppressWarnings(PHPMD.NPathComplexity) Handles four distinct OpenConnector response shapes + * (CallLog, array, jsonSerialize, string) across multiple OC versions; each branch is a required + * shape check. */ private function decodeResponse($response): array { diff --git a/lib/Service/Integration/IntegrationRegistry.php b/lib/Service/Integration/IntegrationRegistry.php index fddfaa0105..bb6b1fda01 100644 --- a/lib/Service/Integration/IntegrationRegistry.php +++ b/lib/Service/Integration/IntegrationRegistry.php @@ -62,6 +62,21 @@ class IntegrationRegistry */ private array $providers = []; + /** + * Registered page-level widget contracts, keyed by widget id. + * + * Distinct from per-object providers: a page widget is a declarative, + * RBAC-scoped render surface (e.g. a dashboard chart) that a leaf app + * registers so the nc-vue render layer can show it on a page rather + * than in an object's sidebar. The value is the declarative descriptor + * (id / type / title / providerId / config). The series DATA itself is + * supplied/fetched separately (see AnalyticsSeriesService) so the + * registry stays a thin declaration surface. + * + * @var array> + */ + private array $pageWidgets = []; + /** * Constructor. * @@ -168,8 +183,8 @@ public function list(): array * Return the ids of every registered provider. * * Used by `Schema::validateLinkedTypesValue()` (task 7) as the - * authoritative existence check, replacing the hardcoded - * `VALID_LINKED_TYPES` constant. + * authoritative existence check; replaced the hardcoded public + * Schema linked-types constant via `cleanup-linked-entity-type-map`. * * @return array * @@ -210,6 +225,90 @@ public function isValidIntegrationId(string $id): bool return isset($this->providers[$id]); }//end isValidIntegrationId() + /** + * Register a page-level widget contract. + * + * ADDITIVE: this surface is independent of the per-object provider + * registry above. A leaf app declares a renderable page widget — most + * commonly a chart fed by a pre-computed analytics series — so the + * nc-vue render layer can place it on a dashboard page. The descriptor + * is declarative + RBAC-scoped; it carries no per-request data. + * + * Expected descriptor shape (all but `id` optional, defaulted): + * - `id` string — stable widget id (required). + * - `type` string — render kind (default 'chart'). + * - `title` ?string — display title. + * - `providerId` ?string — the data provider/series key the render + * layer fetches data from. + * - `config` array — declarative render config (chart type, + * axes, visibility scope, …). + * + * Duplicate id: first registration wins, second logs a warning — + * mirroring addProvider() so behaviour is predictable. + * + * @param array $descriptor The page-widget descriptor. + * + * @return bool True when accepted, false when rejected (missing id or + * duplicate). + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function registerPageWidget(array $descriptor): bool + { + $id = (string) ($descriptor['id'] ?? ''); + if ($id === '') { + $this->logger->warning('[IntegrationRegistry] page widget rejected — missing id'); + return false; + } + + if (isset($this->pageWidgets[$id]) === true) { + $this->logger->warning( + sprintf( + '[IntegrationRegistry] duplicate page widget id "%s" — keeping first registration', + $id + ) + ); + return false; + } + + // Normalise the descriptor with declared defaults so consumers + // always see a complete shape. + $this->pageWidgets[$id] = [ + 'id' => $id, + 'type' => (string) ($descriptor['type'] ?? 'chart'), + 'title' => ($descriptor['title'] ?? null), + 'providerId' => ($descriptor['providerId'] ?? null), + 'config' => (array) ($descriptor['config'] ?? []), + ]; + return true; + }//end registerPageWidget() + + /** + * List every registered page-level widget descriptor. + * + * @return array> + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function listPageWidgets(): array + { + return array_values($this->pageWidgets); + }//end listPageWidgets() + + /** + * Look up a page-level widget descriptor by id. + * + * @param string $id The widget id. + * + * @return array|null The descriptor, or null when unknown. + * + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function getPageWidget(string $id): ?array + { + return $this->pageWidgets[$id] ?? null; + }//end getPageWidget() + /** * Return only the providers that are currently usable. * diff --git a/lib/Service/Integration/PaginatedResult.php b/lib/Service/Integration/PaginatedResult.php index 10b0bcb5bb..7fb54ff55c 100644 --- a/lib/Service/Integration/PaginatedResult.php +++ b/lib/Service/Integration/PaginatedResult.php @@ -108,7 +108,7 @@ public function __construct(array $items, int $total, ?string $nextCursor=null) * * @return self The normalized envelope. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-integration/specs/generic-integrations/spec.md#Requirement:Integration-list-responses-MUST-be-normalised-into-a-canonical-pagination-envelope + * @spec openspec/specs/generic-integrations/spec.md#requirement-integration-list-responses-must-be-normalised-into-a-canonical-pagination-envelope */ public static function fromMixed(mixed $value): self { @@ -165,7 +165,7 @@ private static function fromEnvelope(array $value): self * * @return array{items:array>,results:array>,total:int,nextCursor:?string} * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-integration/specs/generic-integrations/spec.md#Requirement:Integration-list-responses-MUST-be-normalised-into-a-canonical-pagination-envelope + * @spec openspec/specs/generic-integrations/spec.md#requirement-integration-list-responses-must-be-normalised-into-a-canonical-pagination-envelope */ public function toArray(): array { diff --git a/lib/Service/Integration/PropertySemanticReferenceValidator.php b/lib/Service/Integration/PropertySemanticReferenceValidator.php new file mode 100644 index 0000000000..874403194f --- /dev/null +++ b/lib/Service/Integration/PropertySemanticReferenceValidator.php @@ -0,0 +1,181 @@ +` to + * indicate they reference a cross-app object by canonical semantic type + * (e.g. `https://schema.org/Organization`), resolved across all installed + * schemas by {@see \OCA\OpenRegister\Service\SemanticTypeResolver}. An + * optional `referenceSemanticApp` string is a provider hint only. + * + * This validator enforces the write-time contract: + * - `referenceSemanticType`, when present, is a non-empty absolute IRI + * (reusing {@see JsonLdContextService::isAbsoluteIri()}); + * - `referenceSemanticApp`, when present, is a string. + * + * It is independent of `referenceType` (an integration id) and of `$ref` + * (a concrete schema). Backwards-compat: properties without + * `referenceSemanticType` validate exactly as before; the marker is opt-in. + * + * WIRING: this validator is intentionally standalone, mirroring + * {@see \OCA\OpenRegister\Service\Integration\PropertyReferenceTypeValidator} + * which is registered in DI but not yet invoked from the schema-save path. + * To enforce it on write, call {@see validateAll()} with a schema's + * `properties` map from the schema create/update path (SchemaService / + * SchemasController::create + update) alongside the existing + * PropertyReferenceTypeValidator, and surface any thrown + * InvalidArgumentException as a 400 validation error. + * + * @category Service + * @package OCA\OpenRegister\Service\Integration + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Integration; + +use InvalidArgumentException; +use OCA\OpenRegister\Service\JsonLd\JsonLdContextService; + +/** + * Validate the optional `referenceSemanticType` marker on schema properties. + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + */ +class PropertySemanticReferenceValidator +{ + /** + * Constructor. + * + * @param JsonLdContextService $jsonLd IRI validation helper (isAbsoluteIri). + * + * @return void + */ + public function __construct( + private readonly JsonLdContextService $jsonLd, + ) { + }//end __construct() + + /** + * Validate a single property definition. + * + * Properties without a `referenceSemanticType` key pass through. + * Properties with one MUST carry a non-empty absolute IRI; an optional + * `referenceSemanticApp` MUST be a string. + * + * @param array $property Schema property definition. + * @param string|null $propertyName Optional property name for error messages. + * + * @return void + * + * @throws InvalidArgumentException When referenceSemanticType is not a + * well-formed absolute IRI, or the app + * hint is non-string. + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + * (Requirement: A property can reference a semantic type) + */ + public function validate(array $property, ?string $propertyName=null): void + { + if (array_key_exists('referenceSemanticType', $property) === false) { + return; + } + + $value = $property['referenceSemanticType']; + if ($value === null) { + return; + } + + if (is_string($value) === false || $value === '') { + throw new InvalidArgumentException( + $this->formatError( + propertyName: $propertyName, + key: 'referenceSemanticType', + detail: 'must be a non-empty string' + ) + ); + } + + if ($this->jsonLd->isAbsoluteIri(value: $value) === false) { + throw new InvalidArgumentException( + $this->formatError( + propertyName: $propertyName, + key: 'referenceSemanticType', + detail: sprintf("must be an absolute IRI, got '%s'", $value) + ) + ); + } + + // Optional provider hint — string when present. + if (array_key_exists('referenceSemanticApp', $property) === true) { + $appHint = $property['referenceSemanticApp']; + if ($appHint !== null && is_string($appHint) === false) { + throw new InvalidArgumentException( + $this->formatError( + propertyName: $propertyName, + key: 'referenceSemanticApp', + detail: 'must be a string when present' + ) + ); + } + } + }//end validate() + + /** + * Validate every property in a schema's `properties` map. + * + * @param array> $properties Property map. + * + * @return void + * + * @throws InvalidArgumentException On the first invalid property. + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + * (Requirement: A property can reference a semantic type) + */ + public function validateAll(array $properties): void + { + foreach ($properties as $name => $definition) { + if (is_array($definition) === true) { + $resolvedName = null; + if (is_string($name) === true) { + $resolvedName = $name; + } + + $this->validate(property: $definition, propertyName: $resolvedName); + } + } + }//end validateAll() + + /** + * Build the standard error-message prefix. + * + * @param string|null $propertyName Property name (or null for schema-level). + * @param string $key The offending keyword. + * @param string $detail Specific failure detail. + * + * @return string + */ + private function formatError(?string $propertyName, string $key, string $detail): string + { + $prefix = $key; + if ($propertyName !== null) { + $prefix = sprintf("Property '%s' %s", $propertyName, $key); + } + + return sprintf('%s %s', $prefix, $detail); + }//end formatError() +}//end class diff --git a/lib/Service/Integration/Providers/AnalyticsProvider.php b/lib/Service/Integration/Providers/AnalyticsProvider.php index 174489f4f1..129f5b3750 100644 --- a/lib/Service/Integration/Providers/AnalyticsProvider.php +++ b/lib/Service/Integration/Providers/AnalyticsProvider.php @@ -116,7 +116,7 @@ public function isEnabled(): bool * * @return array List of registry leaf rows. * - * @spec openspec/changes/integration-analytics/tasks.md + * @spec openspec/specs/integration-analytics/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { diff --git a/lib/Service/Integration/Providers/BookmarksProvider.php b/lib/Service/Integration/Providers/BookmarksProvider.php index 406a78cc93..faa3ded61d 100644 --- a/lib/Service/Integration/Providers/BookmarksProvider.php +++ b/lib/Service/Integration/Providers/BookmarksProvider.php @@ -29,7 +29,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-bookmarks/tasks.md + * @spec openspec/specs/integration-bookmarks/spec.md */ declare(strict_types=1); @@ -119,7 +119,7 @@ public function isEnabled(): bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) register/schema/filters * are part of the IntegrationProvider::list() contract. * - * @spec openspec/changes/integration-bookmarks/tasks.md + * @spec openspec/specs/integration-bookmarks/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { diff --git a/lib/Service/Integration/Providers/BrpPersoonProvider.php b/lib/Service/Integration/Providers/BrpPersoonProvider.php new file mode 100644 index 0000000000..18fd469ce3 --- /dev/null +++ b/lib/Service/Integration/Providers/BrpPersoonProvider.php @@ -0,0 +1,478 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/integration-brp-haalcentraal/tasks.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Integration\Providers; + +use OCA\OpenRegister\Exception\ProviderUnavailableException; +use OCA\OpenRegister\Service\Integration\AbstractIntegrationProvider; +use OCA\OpenRegister\Service\Integration\ExternalIntegrationRouter; +use OCP\App\IAppManager; +use OCP\IL10N; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * BRP HaalCentraal person-lookup integration provider — external, + * OpenConnector-backed (OAuth2 + mTLS on the source). + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes the external + * router + app manager + l10n + logger + the ProviderUnavailableException + * cause vocabulary; each is required for the degrade-don't-throw lookup + * surface (AD-23). + */ +class BrpPersoonProvider extends AbstractIntegrationProvider +{ + + /** + * OpenConnector source id this provider routes through. + * + * @var string + */ + public const SOURCE_ID = 'brp-haalcentraal'; + + /** + * NC app that must be installed for this integration to function + * (it carries the OpenConnector source + credentials + cert). + * + * @var string + */ + private const REQUIRED_APP = 'openconnector'; + + /** + * Default field set requested from HaalCentraal. The BRP API requires an + * explicit `fields` list on every query. Mirrors the field set pipelinq's + * `HaalCentraalClient` requests so the consuming app's normaliser receives + * the subtrees it expects. + * + * @var array + */ + private const DEFAULT_FIELDS = [ + 'burgerservicenummer', + 'naam.voornamen', + 'naam.voorletters', + 'naam.voorvoegsel', + 'naam.geslachtsnaam', + 'naam.adellijkeTitelPredicaat', + 'geboorte.datum', + 'geboorte.plaats', + 'geboorte.land', + 'geslacht', + 'verblijfplaats', + 'indicatieGeheim', + ]; + + /** + * Constructor. + * + * @param ExternalIntegrationRouter $router External-call router. + * @param IAppManager $appManager NC app manager (isEnabled check). + * @param IL10N $l10n Localisation. + * @param LoggerInterface $logger Logger for degraded paths (never receives a BSN). + * + * @return void + */ + public function __construct( + private ExternalIntegrationRouter $router, + private IAppManager $appManager, + private IL10N $l10n, + private LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Stable provider id (matches the OpenConnector source name). + * + * @return string + */ + public function getId(): string + { + return 'brp-haalcentraal'; + }//end getId() + + /** + * Human-readable label shown in the admin UI. + * + * @return string + */ + public function getLabel(): string + { + return $this->l10n->t('BRP Person Register (HaalCentraal)'); + }//end getLabel() + + /** + * MDI icon name for the tab / widget. + * + * @return string + */ + public function getIcon(): string + { + return 'AccountSearch'; + }//end getIcon() + + /** + * Named group this integration belongs to (AD-16). + * + * @return string|null + */ + public function getGroup(): ?string + { + return 'external'; + }//end getGroup() + + /** + * Nextcloud app that must be installed for this integration to + * function — OpenConnector carries the `brp-haalcentraal` source + + * OAuth credentials + PKIoverheid client certificate. + * + * @return string|null + */ + public function getRequiredApp(): ?string + { + return self::REQUIRED_APP; + }//end getRequiredApp() + + /** + * Storage strategy (AD-22) — `external`: no local link table; this is + * a stateless person-lookup leaf routed through OpenConnector. + * + * @return string + */ + public function getStorageStrategy(): string + { + return 'external'; + }//end getStorageStrategy() + + /** + * OpenConnector source id this provider routes all calls through (AD-4). + * + * @return string|null + */ + public function getOpenConnectorSource(): ?string + { + return self::SOURCE_ID; + }//end getOpenConnectorSource() + + /** + * Whether the integration is available — true iff OpenConnector is + * installed (it owns the `brp-haalcentraal` source + credentials + cert). + * The router still degrades gracefully if the source itself is missing or + * HaalCentraal is down. + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->appManager->isInstalled(self::REQUIRED_APP); + }//end isEnabled() + + /** + * Auth requirements descriptor. `type: 'external'` — the OAuth2 + * client_credentials secret AND the PKIoverheid mutual-TLS client + * certificate are configured on the OpenConnector `brp-haalcentraal` + * source, not here. OpenRegister's admin UI surfaces the source's auth + * status and links out to OpenConnector to configure it. `supports` + * advertises the two transports `CallService` applies for this source. + * + * @return array + * + * @spec openspec/changes/integration-brp-haalcentraal/tasks.md + */ + public function authRequirements(): array + { + return [ + 'type' => 'external', + 'configuredVia' => 'openconnector', + 'source' => self::SOURCE_ID, + 'supports' => ['oauth2_client_credentials', 'mtls'], + ]; + }//end authRequirements() + + /** + * List persons matching a BSN — the registry read-path. This leaf is + * object-independent (person lookup is not bound to an OR object), so the + * register/schema/objectId context is ignored; the `_search` filter + * carries the BSN. Returns a flat list of raw HaalCentraal person objects; + * the consumer maps + validates them (pipelinq `HaalCentraalClient` + + * `BsnValidationService`). + * + * @param string $register Ignored (object-independent leaf). + * @param string $schema Ignored (object-independent leaf). + * @param string $objectId Ignored (object-independent leaf). + * @param array $filters `_search` carries the BSN. + * + * @return array> + * + * @throws ProviderUnavailableException When the source is missing/down — + * the controller maps the cause. + * + * @spec openspec/changes/integration-brp-haalcentraal/tasks.md + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) register/schema/objectId + * are mandated by the IntegrationProvider contract; this leaf is + * object-independent so they are intentionally unused. + */ + public function list(string $register, string $schema, string $objectId, array $filters=[]): array + { + $bsn = trim((string) ($filters['_search'] ?? '')); + if ($bsn === '') { + return []; + } + + $response = $this->router->call( + provider: $this, + method: 'POST', + path: 'personen', + options: [ + 'body' => $this->buildQueryBody(bsn: $bsn), + 'headers' => ['Accept' => 'application/hal+json', 'Content-Type' => 'application/json'], + ] + ); + + return $this->extractPersonen(response: $response); + }//end list() + + /** + * Look up a single person by Burgerservicenummer (BSN). + * + * Issues a `RaadpleegMetBurgerservicenummer` POST to the HaalCentraal + * `/personen` endpoint (the same surface pipelinq's `HaalCentraalClient` + * uses) and returns the raw HAL+JSON person objects. Degrades null-safely + * to `{ unavailable, cause }` rather than throwing, so a consuming app + * never fatals on a missing/down source (AD-23). + * + * The BSN is carried in the request body only and is NEVER logged here — + * elfproef validation + BSN masking are the consuming app's responsibility. + * + * @param string $bsn The 9-digit Burgerservicenummer. + * + * @return array `{ results, total, meta }` on success + * (results is the raw HaalCentraal person + * object list — 0 or 1 entries; `meta` carries + * the Wet-BRP audit metadata + * `{ correlationId, durationMs, status }`), or + * `{ unavailable, cause, results, total }` + * when the source is unconfigured/down. + * + * @spec openspec/changes/integration-brp-audit-metadata/tasks.md + */ + public function lookupByBsn(string $bsn): array + { + $bsn = trim($bsn); + if ($bsn === '') { + return ['results' => [], 'total' => 0, 'meta' => $this->emptyMeta()]; + } + + try { + $envelope = $this->router->callWithMeta( + provider: $this, + method: 'POST', + path: 'personen', + options: [ + 'body' => $this->buildQueryBody(bsn: $bsn), + 'headers' => ['Accept' => 'application/hal+json', 'Content-Type' => 'application/json'], + ] + ); + } catch (ProviderUnavailableException $e) { + return $this->degraded(cause: $e->getCause()); + } catch (Throwable $e) { + // Never include the BSN or the message body — only that a lookup failed. + $this->logger->warning('BrpPersoonProvider::lookupByBsn failed (transport).'); + return $this->degraded(cause: ProviderUnavailableException::CAUSE_UPSTREAM_SERVICE_DOWN); + } + + $personen = $this->extractPersonen(response: ($envelope['body'] ?? [])); + + return [ + 'results' => $personen, + 'total' => count($personen), + 'meta' => $this->shapeMeta(meta: ($envelope['meta'] ?? [])), + ]; + }//end lookupByBsn() + + /** + * Health descriptor — defers to the router's probe so admin UI / OCS + * capabilities report the same status runtime callers would see. + * + * @return array{status: string, authStatus: string, message: ?string} + * + * @spec exclude Thin delegation to ExternalIntegrationRouter::probe + * (annotated to pluggable-integration-registry task-4); + * carries no provider-specific health behaviour. + */ + public function health(): array + { + return $this->router->probe(provider: $this); + }//end health() + + /** + * Build the `RaadpleegMetBurgerservicenummer` query body for the + * HaalCentraal `/personen` POST. The BSN list + the explicit `fields` + * set are exactly what the API requires. + * + * @param string $bsn The 9-digit BSN to query. + * + * @return array The decoded body (router/CallService + * JSON-encodes it for the request). + */ + private function buildQueryBody(string $bsn): array + { + return [ + 'type' => 'RaadpleegMetBurgerservicenummer', + 'burgerservicenummer' => [$bsn], + 'fields' => self::DEFAULT_FIELDS, + ]; + }//end buildQueryBody() + + /** + * Shape the router's transport metadata into the leaf's stable `meta` + * contract consumed by the calling app's Wet-BRP audit record: + * - `correlationId` ← the upstream `X-Correlation-ID` response header + * (consumer persists it as `haalcentraalCorrelationId`) + * - `durationMs` ← the OpenConnector-measured round-trip duration + * (consumer persists it as `responseDuurMs`) + * - `status` ← the upstream HTTP status code + * (consumer persists it as the response status / `responseCode`) + * + * The BSN is never present in `meta` — only transport metadata. + * + * @param array $meta The router's raw meta array. + * + * @return array{correlationId: ?string, durationMs: int, status: int} + */ + private function shapeMeta(array $meta): array + { + $correlationId = ($meta['correlationId'] ?? null); + if ($correlationId !== null) { + $correlationId = (string) $correlationId; + } + + return [ + 'correlationId' => $correlationId, + 'durationMs' => (int) ($meta['durationMs'] ?? 0), + 'status' => (int) ($meta['status'] ?? 0), + ]; + }//end shapeMeta() + + /** + * The empty `meta` shape used on the no-BSN short-circuit (no upstream + * call was made, so there is no correlation id / duration / status). + * + * @return array{correlationId: ?string, durationMs: int, status: int} + */ + private function emptyMeta(): array + { + return [ + 'correlationId' => null, + 'durationMs' => 0, + 'status' => 0, + ]; + }//end emptyMeta() + + /** + * Build the degraded envelope mirroring the unconfigured-source + * descriptor so the lookup shape stays stable across success + + * degraded paths (AD-23). + * + * @param string $cause One of the ProviderUnavailableException CAUSE_* values. + * + * @return array + */ + private function degraded(string $cause): array + { + return [ + 'unavailable' => true, + 'cause' => $cause, + 'results' => [], + 'total' => 0, + ]; + }//end degraded() + + /** + * Pull the person objects out of a HaalCentraal response envelope. + * HaalCentraal v2 returns the matches under `personen` (or + * `_embedded.personen` on some gateways); a bare list is used as-is; + * anything else yields an empty list. + * + * @param array $response Decoded source response (HAL+JSON). + * + * @return array> + */ + private function extractPersonen(array $response): array + { + $list = []; + if (isset($response['personen']) === true && is_array($response['personen']) === true) { + $list = $response['personen']; + } else if (isset($response['_embedded']['personen']) === true + && is_array($response['_embedded']['personen']) === true + ) { + $list = $response['_embedded']['personen']; + } else if (array_is_list($response) === true) { + $list = $response; + } + + $out = []; + foreach ($list as $row) { + if (is_array($row) === true) { + $out[] = $row; + } + } + + return $out; + }//end extractPersonen() +}//end class diff --git a/lib/Service/Integration/Providers/CalendarProvider.php b/lib/Service/Integration/Providers/CalendarProvider.php index ae393e02c2..37272f6617 100644 --- a/lib/Service/Integration/Providers/CalendarProvider.php +++ b/lib/Service/Integration/Providers/CalendarProvider.php @@ -33,7 +33,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-calendar/tasks.md + * @spec openspec/specs/integration-calendar/spec.md */ declare(strict_types=1); @@ -89,7 +89,7 @@ public function __construct( * * @return string * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function getId(): string { @@ -101,7 +101,7 @@ public function getId(): string * * @return string * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function getLabel(): string { @@ -113,7 +113,7 @@ public function getLabel(): string * * @return string * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function getIcon(): string { @@ -125,7 +125,7 @@ public function getIcon(): string * * @return string|null * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function getGroup(): ?string { @@ -137,7 +137,7 @@ public function getGroup(): ?string * * @return string|null * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function getRequiredApp(): ?string { @@ -149,7 +149,7 @@ public function getRequiredApp(): ?string * * @return string * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function getStorageStrategy(): string { @@ -161,7 +161,7 @@ public function getStorageStrategy(): string * * @return bool * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function isEnabled(): bool { @@ -198,7 +198,7 @@ public function isEnabled(): bool * * @return array> * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -242,7 +242,7 @@ public function list(string $register, string $schema, string $objectId, array $ * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function delete(string $register, string $schema, string $objectId, string $entityId): void { @@ -270,7 +270,7 @@ public function delete(string $register, string $schema, string $objectId, strin * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-calendar-integration/tasks.md#task-2 + * @spec openspec/specs/calendar-integration/spec.md */ public function health(): array { diff --git a/lib/Service/Integration/Providers/CollectivesProvider.php b/lib/Service/Integration/Providers/CollectivesProvider.php index ab8377a33f..ba2aedbaa4 100644 --- a/lib/Service/Integration/Providers/CollectivesProvider.php +++ b/lib/Service/Integration/Providers/CollectivesProvider.php @@ -116,7 +116,7 @@ public function isEnabled(): bool * * @return array> List of registry leaf rows. * - * @spec openspec/changes/integration-collectives/tasks.md + * @spec openspec/specs/integration-collectives/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { diff --git a/lib/Service/Integration/Providers/ContactsProvider.php b/lib/Service/Integration/Providers/ContactsProvider.php index 756a4bb258..1baf4253b8 100644 --- a/lib/Service/Integration/Providers/ContactsProvider.php +++ b/lib/Service/Integration/Providers/ContactsProvider.php @@ -30,7 +30,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-contacts/tasks.md + * @spec openspec/specs/integration-contacts/spec.md */ declare(strict_types=1); @@ -119,7 +119,7 @@ public function isEnabled(): bool * * @return array> * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-4 + * @spec openspec/specs/contacts-actions/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -146,7 +146,7 @@ public function list(string $register, string $schema, string $objectId, array $ * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-4 + * @spec openspec/specs/contacts-actions/spec.md */ public function update(string $register, string $schema, string $objectId, string $entityId, array $payload): array { @@ -173,7 +173,7 @@ public function update(string $register, string $schema, string $objectId, strin * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-contacts-actions/tasks.md#task-4 + * @spec openspec/specs/contacts-actions/spec.md */ public function delete(string $register, string $schema, string $objectId, string $entityId): void { diff --git a/lib/Service/Integration/Providers/CospendProvider.php b/lib/Service/Integration/Providers/CospendProvider.php index eac0fabe38..8f45b4207b 100644 --- a/lib/Service/Integration/Providers/CospendProvider.php +++ b/lib/Service/Integration/Providers/CospendProvider.php @@ -158,7 +158,7 @@ public function isEnabled(): bool * * @return array> List of registry leaf rows. * - * @spec openspec/changes/integration-cospend/tasks.md + * @spec openspec/specs/integration-cospend/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { diff --git a/lib/Service/Integration/Providers/DeckProvider.php b/lib/Service/Integration/Providers/DeckProvider.php index e6f6c5c38a..06664552d5 100644 --- a/lib/Service/Integration/Providers/DeckProvider.php +++ b/lib/Service/Integration/Providers/DeckProvider.php @@ -28,7 +28,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-deck/tasks.md + * @spec openspec/specs/integration-deck/spec.md */ declare(strict_types=1); @@ -118,7 +118,7 @@ public function isEnabled(): bool * * @SuppressWarnings(PHPMD.UnusedFormalParameter) Provider-contract args. * - * @spec openspec/changes/integration-deck/tasks.md + * @spec openspec/specs/integration-deck/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -145,7 +145,7 @@ public function list(string $register, string $schema, string $objectId, array $ * * @throws Exception When payload is missing required fields. * - * @spec openspec/changes/integration-deck/tasks.md + * @spec openspec/specs/integration-deck/spec.md */ public function create(string $register, string $schema, string $objectId, array $payload): array { @@ -206,7 +206,7 @@ public function create(string $register, string $schema, string $objectId, array * * @SuppressWarnings(PHPMD.UnusedFormalParameter) Provider-contract args. * - * @spec openspec/changes/integration-deck/tasks.md + * @spec openspec/specs/integration-deck/spec.md */ public function delete(string $register, string $schema, string $objectId, string $entityId): void { diff --git a/lib/Service/Integration/Providers/EmailProvider.php b/lib/Service/Integration/Providers/EmailProvider.php index 1a94e265d1..1277754ce5 100644 --- a/lib/Service/Integration/Providers/EmailProvider.php +++ b/lib/Service/Integration/Providers/EmailProvider.php @@ -22,7 +22,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-email/tasks.md + * @spec openspec/specs/integration-email/spec.md */ declare(strict_types=1); @@ -143,7 +143,7 @@ public function isEnabled(): bool * * @return array{items:array>,total:int,nextCursor:?string} * - * @spec openspec/changes/integration-email/tasks.md + * @spec openspec/specs/integration-email/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -218,7 +218,7 @@ public function list(string $register, string $schema, string $objectId, array $ * * @return array * - * @spec openspec/changes/integration-email/tasks.md + * @spec openspec/specs/integration-email/spec.md */ public function create(string $register, string $schema, string $objectId, array $payload): array { @@ -252,7 +252,7 @@ public function create(string $register, string $schema, string $objectId, array * * @return void * - * @spec openspec/changes/integration-email/tasks.md + * @spec openspec/specs/integration-email/spec.md */ public function delete(string $register, string $schema, string $objectId, string $entityId): void { diff --git a/lib/Service/Integration/Providers/FlowProvider.php b/lib/Service/Integration/Providers/FlowProvider.php index 7a9340ddf6..c056602e93 100644 --- a/lib/Service/Integration/Providers/FlowProvider.php +++ b/lib/Service/Integration/Providers/FlowProvider.php @@ -26,7 +26,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-flow/tasks.md + * @spec openspec/specs/integration-flow/spec.md */ declare(strict_types=1); @@ -107,7 +107,7 @@ public function isEnabled(): bool * * @return array> * - * @spec openspec/changes/integration-flow/tasks.md + * @spec openspec/specs/integration-flow/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { diff --git a/lib/Service/Integration/Providers/FormsProvider.php b/lib/Service/Integration/Providers/FormsProvider.php index d2fb1f0d8e..0c2959e216 100644 --- a/lib/Service/Integration/Providers/FormsProvider.php +++ b/lib/Service/Integration/Providers/FormsProvider.php @@ -142,7 +142,7 @@ public function isEnabled(): bool * * @return array> * - * @spec openspec/changes/integration-forms/tasks.md + * @spec openspec/specs/integration-forms/spec.md * * @SuppressWarnings(PHPMD.UnusedFormalParameter) $register, $schema and $filters are required by the * IntegrationProvider interface signature (IntegrationProvider::list) — this provider only diff --git a/lib/Service/Integration/Providers/KvkProvider.php b/lib/Service/Integration/Providers/KvkProvider.php new file mode 100644 index 0000000000..ad976ecbfa --- /dev/null +++ b/lib/Service/Integration/Providers/KvkProvider.php @@ -0,0 +1,432 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Integration\Providers; + +use OCA\OpenRegister\Exception\ProviderUnavailableException; +use OCA\OpenRegister\Service\Integration\AbstractIntegrationProvider; +use OCA\OpenRegister\Service\Integration\ExternalIntegrationRouter; +use OCP\App\IAppManager; +use OCP\IL10N; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * KvK company-lookup integration provider — external, OpenConnector-backed. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes the external + * router + app manager + l10n + logger + the ProviderUnavailableException + * cause vocabulary; each is required for the degrade-don't-throw lookup + * surface (AD-23). + */ +class KvkProvider extends AbstractIntegrationProvider +{ + + /** + * OpenConnector source id this provider routes through. + * + * @var string + */ + public const SOURCE_ID = 'kvk'; + + /** + * NC app that must be installed for this integration to function + * (it carries the OpenConnector source + credentials). + * + * @var string + */ + private const REQUIRED_APP = 'openconnector'; + + /** + * Constructor. + * + * @param ExternalIntegrationRouter $router External-call router. + * @param IAppManager $appManager NC app manager (isEnabled check). + * @param IL10N $l10n Localisation. + * @param LoggerInterface $logger Logger for degraded paths. + * + * @return void + */ + public function __construct( + private ExternalIntegrationRouter $router, + private IAppManager $appManager, + private IL10N $l10n, + private LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Stable provider id (matches the OpenConnector source name). + * + * @return string + */ + public function getId(): string + { + return 'kvk'; + }//end getId() + + /** + * Human-readable label shown in the admin UI. + * + * @return string + */ + public function getLabel(): string + { + return $this->l10n->t('KvK Company Register'); + }//end getLabel() + + /** + * MDI icon name for the tab / widget. + * + * @return string + */ + public function getIcon(): string + { + return 'OfficeBuilding'; + }//end getIcon() + + /** + * Named group this integration belongs to (AD-16). + * + * @return string|null + */ + public function getGroup(): ?string + { + return 'external'; + }//end getGroup() + + /** + * Nextcloud app that must be installed for this integration to + * function — OpenConnector carries the `kvk` source + credentials. + * + * @return string|null + */ + public function getRequiredApp(): ?string + { + return self::REQUIRED_APP; + }//end getRequiredApp() + + /** + * Storage strategy (AD-22) — `external`: no local link table; this is + * a stateless company-lookup leaf routed through OpenConnector. + * + * @return string + */ + public function getStorageStrategy(): string + { + return 'external'; + }//end getStorageStrategy() + + /** + * OpenConnector source id this provider routes all calls through (AD-4). + * + * @return string|null + */ + public function getOpenConnectorSource(): ?string + { + return self::SOURCE_ID; + }//end getOpenConnectorSource() + + /** + * Whether the integration is available — true iff OpenConnector is + * installed (it owns the `kvk` source + credentials). The router still + * degrades gracefully if the source itself is missing or KvK is down. + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->appManager->isInstalled(self::REQUIRED_APP); + }//end isEnabled() + + /** + * Auth requirements descriptor. `type: 'external'` — the KvK API key is + * configured on the OpenConnector `kvk` source (an `apikey` request + * header), not here. OpenRegister's admin UI surfaces the source's auth + * status and links out to OpenConnector to configure it. + * + * @return array + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + */ + public function authRequirements(): array + { + return [ + 'type' => 'external', + 'configuredVia' => 'openconnector', + 'source' => self::SOURCE_ID, + 'supports' => ['apikey'], + ]; + }//end authRequirements() + + /** + * List companies matching a free-text query — the registry read-path. + * + * This leaf is object-independent (company lookup is not bound to an OR + * object), so the register/schema/objectId context is ignored; the + * `_search` filter carries the query. Mirrors the KvK `/zoeken` surface + * pipelinq's `KvkApiClient::search()` hits. Returns a flat list of raw + * KvK result rows; the consumer maps them (pipelinq `KvkResultMapper`). + * + * @param string $register Ignored (object-independent leaf). + * @param string $schema Ignored (object-independent leaf). + * @param string $objectId Ignored (object-independent leaf). + * @param array $filters `_search` (query), `_limit`, `_page`. + * + * @return array> + * + * @throws ProviderUnavailableException When the source is missing/down — + * the controller maps the cause. + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) register/schema/objectId + * are mandated by the IntegrationProvider contract; this leaf is + * object-independent so they are intentionally unused. + */ + public function list(string $register, string $schema, string $objectId, array $filters=[]): array + { + $query = []; + $search = (string) ($filters['_search'] ?? ''); + if ($search !== '') { + $query['naam'] = $search; + } + + $limit = (int) ($filters['_limit'] ?? 0); + if ($limit > 0) { + $query['aantal'] = (string) min(100, $limit); + } + + $page = (int) ($filters['_page'] ?? 0); + if ($page > 0) { + $query['pagina'] = (string) $page; + } + + $response = $this->router->call( + provider: $this, + method: 'GET', + path: 'zoeken', + options: ['query' => $query, 'headers' => ['Accept' => 'application/json']] + ); + + return $this->extractRows(response: $response); + }//end list() + + /** + * Look up a single company by its KvK number. + * + * Hits the source's `/zoeken?kvkNummer={n}` surface (the same search + * endpoint pipelinq's `KvkApiClient` uses, filtered to one number) and + * returns the raw KvK rows. Degrades null-safely to + * `{ unavailable, cause }` rather than throwing, so a consuming app + * never fatals on a missing/down source (AD-23). + * + * @param string $kvkNumber The 8-digit KvK number. + * + * @return array `{ results, total }` on success, or + * `{ unavailable, cause, results, total }` + * when the source is unconfigured/down. + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + */ + public function lookupByKvkNumber(string $kvkNumber): array + { + $kvkNumber = trim($kvkNumber); + if ($kvkNumber === '') { + return ['results' => [], 'total' => 0]; + } + + try { + $response = $this->router->call( + provider: $this, + method: 'GET', + path: 'zoeken', + options: [ + 'query' => ['kvkNummer' => $kvkNumber], + 'headers' => ['Accept' => 'application/json'], + ] + ); + } catch (ProviderUnavailableException $e) { + return $this->degraded(cause: $e->getCause()); + } catch (Throwable $e) { + $this->logger->warning('KvkProvider::lookupByKvkNumber failed: '.$e->getMessage()); + return $this->degraded(cause: ProviderUnavailableException::CAUSE_UPSTREAM_SERVICE_DOWN); + } + + $rows = $this->extractRows(response: $response); + + return ['results' => $rows, 'total' => count($rows)]; + }//end lookupByKvkNumber() + + /** + * Free-text company search against the KvK Handelsregister. + * + * Object-independent read surface a consuming app uses to find companies + * by name (or other KvK search criteria carried in `$criteria`). Degrades + * null-safely to `{ unavailable, cause }` rather than throwing (AD-23). + * + * @param string $query Free-text company-name query. + * @param array $criteria Extra KvK query params (e.g. + * `plaats`, `type`, `sbiHoofdActiviteit`). + * @param int $limit Max results (clamped 1..100). + * @param int $page 1-based page (clamped >= 1). + * + * @return array `{ results, total, limit, page }` on + * success, or `{ unavailable, cause, ... }` + * when the source is unconfigured/down. + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + */ + public function searchCompanies(string $query, array $criteria=[], int $limit=25, int $page=1): array + { + $limit = max(1, min(100, $limit)); + $page = max(1, $page); + + $params = $criteria; + $query = trim($query); + if ($query !== '') { + $params['naam'] = $query; + } + + $params['aantal'] = (string) $limit; + $params['pagina'] = (string) $page; + + try { + $response = $this->router->call( + provider: $this, + method: 'GET', + path: 'zoeken', + options: ['query' => $params, 'headers' => ['Accept' => 'application/json']] + ); + } catch (ProviderUnavailableException $e) { + return $this->degraded(cause: $e->getCause(), limit: $limit, page: $page); + } catch (Throwable $e) { + $this->logger->warning('KvkProvider::searchCompanies failed: '.$e->getMessage()); + return $this->degraded(cause: ProviderUnavailableException::CAUSE_UPSTREAM_SERVICE_DOWN, limit: $limit, page: $page); + } + + $rows = $this->extractRows(response: $response); + + return [ + 'results' => $rows, + 'total' => count($rows), + 'limit' => $limit, + 'page' => $page, + ]; + }//end searchCompanies() + + /** + * Health descriptor — defers to the router's probe so admin UI / OCS + * capabilities report the same status runtime callers would see. + * + * @return array{status: string, authStatus: string, message: ?string} + * + * @spec exclude Thin delegation to ExternalIntegrationRouter::probe + * (annotated to pluggable-integration-registry task-4); + * carries no provider-specific health behaviour. + */ + public function health(): array + { + return $this->router->probe(provider: $this); + }//end health() + + /** + * Build the degraded envelope mirroring the unconfigured-source + * descriptor so the lookup shape stays stable across success + + * degraded paths (AD-23). + * + * @param string $cause One of the ProviderUnavailableException CAUSE_* values. + * @param int $limit Optional resolved limit (search paths). + * @param int $page Optional resolved page (search paths). + * + * @return array + */ + private function degraded(string $cause, int $limit=0, int $page=0): array + { + $state = [ + 'unavailable' => true, + 'cause' => $cause, + 'results' => [], + 'total' => 0, + ]; + + if ($limit > 0) { + $state['limit'] = $limit; + $state['page'] = $page; + } + + return $state; + }//end degraded() + + /** + * Pull the result rows out of a KvK response envelope. KvK wraps search + * hits under `resultaten`; a bare list is used as-is; anything else + * yields an empty list (rather than iterating the envelope's scalars as + * if they were rows). + * + * @param array $response Decoded source response. + * + * @return array> + */ + private function extractRows(array $response): array + { + $rows = []; + if (isset($response['resultaten']) === true && is_array($response['resultaten']) === true) { + $rows = $response['resultaten']; + } else if (array_is_list($response) === true) { + $rows = $response; + } + + $out = []; + foreach ($rows as $row) { + if (is_array($row) === true) { + $out[] = $row; + } + } + + return $out; + }//end extractRows() +}//end class diff --git a/lib/Service/Integration/Providers/MapsProvider.php b/lib/Service/Integration/Providers/MapsProvider.php index 3a3cfdbce5..96a1cc18c7 100644 --- a/lib/Service/Integration/Providers/MapsProvider.php +++ b/lib/Service/Integration/Providers/MapsProvider.php @@ -107,7 +107,7 @@ public function isEnabled(): bool * * @return array List of registry leaf rows. * - * @spec openspec/changes/integration-maps/tasks.md + * @spec openspec/specs/integration-maps/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { diff --git a/lib/Service/Integration/Providers/MessageDispatchProvider.php b/lib/Service/Integration/Providers/MessageDispatchProvider.php new file mode 100644 index 0000000000..5a1aad64db --- /dev/null +++ b/lib/Service/Integration/Providers/MessageDispatchProvider.php @@ -0,0 +1,369 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/messaging-dispatch-leaf/tasks.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Integration\Providers; + +use OCA\OpenRegister\Exception\ProviderUnavailableException; +use OCA\OpenRegister\Service\Integration\AbstractIntegrationProvider; +use OCA\OpenRegister\Service\Integration\ExternalIntegrationRouter; +use OCP\App\IAppManager; +use OCP\IL10N; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Outbound-messaging dispatch integration provider — external, + * OpenConnector-backed, side-effecting send. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes the external + * router + app manager + l10n + logger + the ProviderUnavailableException + * cause vocabulary; each is required for the degrade-don't-throw dispatch + * surface (AD-23). + */ +class MessageDispatchProvider extends AbstractIntegrationProvider +{ + + /** + * Default OpenConnector source id this provider advertises (the + * IntegrationProvider contract expects a single declared source; the real + * target is chosen per call from {@see ALLOWED_SOURCES}). + * + * @var string + */ + public const SOURCE_ID = 'whatsapp-cloud-api'; + + /** + * NC app that must be installed for this integration to function (it + * carries the messaging sources + credentials). + * + * @var string + */ + private const REQUIRED_APP = 'openconnector'; + + /** + * The fixed allow-list of seeded messaging source slugs a caller may + * dispatch through. A dispatch to any other source is rejected before the + * router is touched, so the leaf can never be pointed at an arbitrary + * source. + * + * @var array + */ + public const ALLOWED_SOURCES = [ + 'cmcom-sms', + 'messagebird-sms', + 'twilio-sms', + 'whatsapp-cloud-api', + 'whatsapp-bsp', + ]; + + /** + * The source id resolved for the current dispatch. Mutated by + * {@see dispatch()} immediately before each {@see ExternalIntegrationRouter::call} + * so the router resolves the caller-chosen source via + * {@see getOpenConnectorSource()}. Defaults to {@see SOURCE_ID}. + * + * @var string + */ + private string $targetSource = self::SOURCE_ID; + + /** + * Constructor. + * + * @param ExternalIntegrationRouter $router External-call router. + * @param IAppManager $appManager NC app manager (isEnabled check). + * @param IL10N $l10n Localisation. + * @param LoggerInterface $logger Logger for degraded paths. + * + * @return void + */ + public function __construct( + private ExternalIntegrationRouter $router, + private IAppManager $appManager, + private IL10N $l10n, + private LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Stable provider id. + * + * @return string + */ + public function getId(): string + { + return 'message-dispatch'; + }//end getId() + + /** + * Human-readable label shown in the admin UI. + * + * @return string + */ + public function getLabel(): string + { + return $this->l10n->t('Outbound messaging (SMS / WhatsApp)'); + }//end getLabel() + + /** + * MDI icon name for the tab / widget. + * + * @return string + */ + public function getIcon(): string + { + return 'MessageText'; + }//end getIcon() + + /** + * Named group this integration belongs to (AD-16). + * + * @return string|null + */ + public function getGroup(): ?string + { + return 'external'; + }//end getGroup() + + /** + * Nextcloud app that must be installed for this integration to function — + * OpenConnector carries the messaging sources + credentials. + * + * @return string|null + */ + public function getRequiredApp(): ?string + { + return self::REQUIRED_APP; + }//end getRequiredApp() + + /** + * Storage strategy (AD-22) — `external`: no local link table; this is a + * stateless send leaf routed through OpenConnector. + * + * @return string + */ + public function getStorageStrategy(): string + { + return 'external'; + }//end getStorageStrategy() + + /** + * OpenConnector source id this provider routes the current call through + * (AD-4). Returns the per-call {@see $targetSource}, set by + * {@see dispatch()} from the validated allow-list. + * + * @return string|null + */ + public function getOpenConnectorSource(): ?string + { + return $this->targetSource; + }//end getOpenConnectorSource() + + /** + * Whether the integration is available — true iff OpenConnector is + * installed (it owns the messaging sources + credentials). The router + * still degrades gracefully if a source itself is missing or down. + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->appManager->isInstalled(self::REQUIRED_APP); + }//end isEnabled() + + /** + * Auth requirements descriptor. `type: 'external'` — the provider + * credential (CM.com product token / MessageBird access key / Twilio + * Basic / Meta bearer / BSP token) is configured on the matching + * OpenConnector source, not here. + * + * @return array + * + * @spec openspec/changes/messaging-dispatch-leaf/tasks.md + */ + public function authRequirements(): array + { + return [ + 'type' => 'external', + 'configuredVia' => 'openconnector', + 'sources' => self::ALLOWED_SOURCES, + 'supports' => ['apikey'], + ]; + }//end authRequirements() + + /** + * The IntegrationProvider read-path. This leaf is a send-only surface and + * exposes no listable linked things, so `list()` always returns an empty + * set (the registry contract requires the method; the dispatch surface is + * {@see dispatch()}). + * + * @param string $register Ignored (send-only leaf). + * @param string $schema Ignored (send-only leaf). + * @param string $objectId Ignored (send-only leaf). + * @param array $filters Ignored (send-only leaf). + * + * @return array> + * + * @spec openspec/changes/messaging-dispatch-leaf/tasks.md + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) register/schema/objectId/ + * filters are mandated by the IntegrationProvider contract; this leaf is + * send-only so they are intentionally unused. + */ + public function list(string $register, string $schema, string $objectId, array $filters=[]): array + { + return []; + }//end list() + + /** + * Dispatch one outbound message via a named OpenConnector source. + * + * POSTs `$body` to `$path` (relative to the source's admin-owned base URL) + * through the {@see ExternalIntegrationRouter}, returning the raw provider + * response. The leaf does NOT interpret the body shape — the consuming app + * composes the vendor-specific payload (Twilio form fields, Meta + * `messaging_product` JSON, CM.com message envelope, …) so the same leaf + * serves every provider. Degrades null-safely to `{ unavailable, cause }` + * rather than throwing, so a consuming app never fatals on a missing/down + * source (AD-23). + * + * @param string $source The seeded source slug to send through + * (MUST be in {@see ALLOWED_SOURCES}). + * @param array $body The provider-shaped request body the + * consuming app composed. + * @param string $path Path relative to the source base URL + * (e.g. `2010-04-01/Accounts/{SID}/Messages.json`, + * `{PhoneNumberID}/messages`, `message`). + * @param array $headers Optional extra request headers + * (e.g. a per-call Content-Type). + * + * @return array `{ status: 'sent', response }` on success, or + * `{ unavailable: true, cause }` when the source + * is unconfigured/down or the slug is rejected. + * + * @spec openspec/changes/messaging-dispatch-leaf/tasks.md + */ + public function dispatch(string $source, array $body, string $path, array $headers=[]): array + { + $source = trim($source); + if (in_array($source, self::ALLOWED_SOURCES, true) === false) { + $this->logger->warning( + 'MessageDispatchProvider::dispatch rejected an unknown source slug.' + ); + return $this->degraded(cause: ProviderUnavailableException::CAUSE_OPENCONNECTOR_SOURCE_MISSING); + } + + $this->targetSource = $source; + + $requestHeaders = $headers; + if (isset($requestHeaders['Accept']) === false) { + $requestHeaders['Accept'] = 'application/json'; + } + + try { + $response = $this->router->call( + provider: $this, + method: 'POST', + path: ltrim($path, '/'), + options: [ + 'body' => $body, + 'headers' => $requestHeaders, + ] + ); + } catch (ProviderUnavailableException $e) { + return $this->degraded(cause: $e->getCause()); + } catch (Throwable $e) { + // Never log the body — a message body may carry PII. + $this->logger->warning('MessageDispatchProvider::dispatch failed (transport).'); + return $this->degraded(cause: ProviderUnavailableException::CAUSE_UPSTREAM_SERVICE_DOWN); + } finally { + // Reset to the advertised default so a later contract caller + // (health/probe) never inherits a stale per-call source. + $this->targetSource = self::SOURCE_ID; + }//end try + + return [ + 'status' => 'sent', + 'source' => $source, + 'response' => $response, + ]; + }//end dispatch() + + /** + * Health descriptor — defers to the router's probe so admin UI / OCS + * capabilities report the same status runtime callers would see (probes + * the advertised default source). + * + * @return array{status: string, authStatus: string, message: ?string} + * + * @spec exclude Thin delegation to ExternalIntegrationRouter::probe + * (annotated to pluggable-integration-registry task-4); + * carries no provider-specific health behaviour. + */ + public function health(): array + { + return $this->router->probe(provider: $this); + }//end health() + + /** + * Build the degraded envelope so the dispatch shape stays stable across + * success + degraded paths (AD-23). + * + * @param string $cause One of the ProviderUnavailableException CAUSE_* values. + * + * @return array + */ + private function degraded(string $cause): array + { + return [ + 'unavailable' => true, + 'cause' => $cause, + ]; + }//end degraded() +}//end class diff --git a/lib/Service/Integration/Providers/OpenCorporatesProvider.php b/lib/Service/Integration/Providers/OpenCorporatesProvider.php new file mode 100644 index 0000000000..f5ec83ba9d --- /dev/null +++ b/lib/Service/Integration/Providers/OpenCorporatesProvider.php @@ -0,0 +1,406 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Integration\Providers; + +use OCA\OpenRegister\Exception\ProviderUnavailableException; +use OCA\OpenRegister\Service\Integration\AbstractIntegrationProvider; +use OCA\OpenRegister\Service\Integration\ExternalIntegrationRouter; +use OCP\App\IAppManager; +use OCP\IL10N; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * OpenCorporates company-search integration provider — external, + * OpenConnector-backed. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes the external + * router + app manager + l10n + logger + the ProviderUnavailableException + * cause vocabulary; each is required for the degrade-don't-throw search + * surface (AD-23). + */ +class OpenCorporatesProvider extends AbstractIntegrationProvider +{ + + /** + * OpenConnector source id this provider routes through. + * + * @var string + */ + public const SOURCE_ID = 'opencorporates'; + + /** + * NC app that must be installed for this integration to function + * (it carries the OpenConnector source + credentials). + * + * @var string + */ + private const REQUIRED_APP = 'openconnector'; + + /** + * Constructor. + * + * @param ExternalIntegrationRouter $router External-call router. + * @param IAppManager $appManager NC app manager (isEnabled check). + * @param IL10N $l10n Localisation. + * @param LoggerInterface $logger Logger for degraded paths. + * + * @return void + */ + public function __construct( + private ExternalIntegrationRouter $router, + private IAppManager $appManager, + private IL10N $l10n, + private LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Stable provider id (matches the OpenConnector source name). + * + * @return string + */ + public function getId(): string + { + return 'opencorporates'; + }//end getId() + + /** + * Human-readable label shown in the admin UI. + * + * @return string + */ + public function getLabel(): string + { + return $this->l10n->t('OpenCorporates'); + }//end getLabel() + + /** + * MDI icon name for the tab / widget. + * + * @return string + */ + public function getIcon(): string + { + return 'Domain'; + }//end getIcon() + + /** + * Named group this integration belongs to (AD-16). + * + * @return string|null + */ + public function getGroup(): ?string + { + return 'external'; + }//end getGroup() + + /** + * Nextcloud app that must be installed for this integration to + * function — OpenConnector carries the `opencorporates` source + + * credentials. + * + * @return string|null + */ + public function getRequiredApp(): ?string + { + return self::REQUIRED_APP; + }//end getRequiredApp() + + /** + * Storage strategy (AD-22) — `external`: no local link table; this is + * a stateless company-search leaf routed through OpenConnector. + * + * @return string + */ + public function getStorageStrategy(): string + { + return 'external'; + }//end getStorageStrategy() + + /** + * OpenConnector source id this provider routes all calls through (AD-4). + * + * @return string|null + */ + public function getOpenConnectorSource(): ?string + { + return self::SOURCE_ID; + }//end getOpenConnectorSource() + + /** + * Whether the integration is available — true iff OpenConnector is + * installed (it owns the `opencorporates` source + credentials). The + * router still degrades gracefully if the source itself is missing or + * OpenCorporates is down. + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->appManager->isInstalled(self::REQUIRED_APP); + }//end isEnabled() + + /** + * Auth requirements descriptor. `type: 'external'` — the OpenCorporates + * API token is configured on the OpenConnector `opencorporates` source + * (an `api_token` query parameter), not here. OpenRegister's admin UI + * surfaces the source's auth status and links out to OpenConnector to + * configure it. + * + * @return array + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + */ + public function authRequirements(): array + { + return [ + 'type' => 'external', + 'configuredVia' => 'openconnector', + 'source' => self::SOURCE_ID, + 'supports' => ['apikey'], + ]; + }//end authRequirements() + + /** + * List companies matching a free-text query — the registry read-path. + * + * This leaf is object-independent (company search is not bound to an OR + * object), so the register/schema/objectId context is ignored; the + * `_search` filter carries the query. Mirrors the OpenCorporates + * `/companies/search` surface pipelinq's `OpenCorporatesApiClient` + * hits. Returns a flat list of raw company rows; the consumer maps them + * (pipelinq `OpenCorporatesResultMapper`). + * + * @param string $register Ignored (object-independent leaf). + * @param string $schema Ignored (object-independent leaf). + * @param string $objectId Ignored (object-independent leaf). + * @param array $filters `_search` (query), `_limit`, `_page`. + * + * @return array> + * + * @throws ProviderUnavailableException When the source is missing/down — + * the controller maps the cause. + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) register/schema/objectId + * are mandated by the IntegrationProvider contract; this leaf is + * object-independent so they are intentionally unused. + */ + public function list(string $register, string $schema, string $objectId, array $filters=[]): array + { + $query = []; + $search = (string) ($filters['_search'] ?? ''); + if ($search !== '') { + $query['q'] = $search; + } + + $limit = (int) ($filters['_limit'] ?? 0); + if ($limit > 0) { + $query['per_page'] = (string) min(100, $limit); + } + + $page = (int) ($filters['_page'] ?? 0); + if ($page > 0) { + $query['page'] = (string) $page; + } + + $response = $this->router->call( + provider: $this, + method: 'GET', + path: 'companies/search', + options: ['query' => $query, 'headers' => ['Accept' => 'application/json']] + ); + + return $this->extractRows(response: $response); + }//end list() + + /** + * Free-text company search against the OpenCorporates register. + * + * Object-independent read surface a consuming app uses to find companies + * by name, optionally scoped to a jurisdiction (e.g. `nl`). Degrades + * null-safely to `{ unavailable, cause }` rather than throwing (AD-23). + * + * @param string $query Free-text company-name query. + * @param string|null $jurisdiction Optional ISO jurisdiction code (e.g. `nl`). + * @param int $limit Max results per page (clamped 1..100). + * @param int $page 1-based page (clamped >= 1). + * + * @return array `{ results, total, limit, page }` on + * success, or `{ unavailable, cause, ... }` + * when the source is unconfigured/down. + * + * @spec openspec/changes/integration-kvk-opencorporates/tasks.md + */ + public function searchCompanies(string $query, ?string $jurisdiction=null, int $limit=30, int $page=1): array + { + $limit = max(1, min(100, $limit)); + $page = max(1, $page); + + $params = [ + 'per_page' => (string) $limit, + 'page' => (string) $page, + 'order' => 'score', + ]; + + $query = trim($query); + if ($query !== '') { + $params['q'] = $query; + } + + if ($jurisdiction !== null && trim($jurisdiction) !== '') { + $params['jurisdiction_code'] = trim($jurisdiction); + } + + try { + $response = $this->router->call( + provider: $this, + method: 'GET', + path: 'companies/search', + options: ['query' => $params, 'headers' => ['Accept' => 'application/json']] + ); + } catch (ProviderUnavailableException $e) { + return $this->degraded(cause: $e->getCause(), limit: $limit, page: $page); + } catch (Throwable $e) { + $this->logger->warning('OpenCorporatesProvider::searchCompanies failed: '.$e->getMessage()); + return $this->degraded(cause: ProviderUnavailableException::CAUSE_UPSTREAM_SERVICE_DOWN, limit: $limit, page: $page); + } + + $rows = $this->extractRows(response: $response); + + return [ + 'results' => $rows, + 'total' => count($rows), + 'limit' => $limit, + 'page' => $page, + ]; + }//end searchCompanies() + + /** + * Health descriptor — defers to the router's probe so admin UI / OCS + * capabilities report the same status runtime callers would see. + * + * @return array{status: string, authStatus: string, message: ?string} + * + * @spec exclude Thin delegation to ExternalIntegrationRouter::probe + * (annotated to pluggable-integration-registry task-4); + * carries no provider-specific health behaviour. + */ + public function health(): array + { + return $this->router->probe(provider: $this); + }//end health() + + /** + * Build the degraded envelope mirroring the unconfigured-source + * descriptor so the search shape stays stable across success + + * degraded paths (AD-23). + * + * @param string $cause One of the ProviderUnavailableException CAUSE_* values. + * @param int $limit Resolved limit. + * @param int $page Resolved page. + * + * @return array + */ + private function degraded(string $cause, int $limit, int $page): array + { + return [ + 'unavailable' => true, + 'cause' => $cause, + 'results' => [], + 'total' => 0, + 'limit' => $limit, + 'page' => $page, + ]; + }//end degraded() + + /** + * Pull the company rows out of an OpenCorporates response envelope. + * OpenCorporates wraps hits under `results.companies` (each entry is + * `{ company: {...} }`), which this method unwraps to a flat list of + * raw `company` objects; a bare list is used as-is; anything else yields + * an empty list. + * + * @param array $response Decoded source response. + * + * @return array> + */ + private function extractRows(array $response): array + { + $companies = $response['results']['companies'] ?? null; + if (is_array($companies) === true) { + $out = []; + foreach ($companies as $entry) { + if (is_array($entry) === false) { + continue; + } + + if (isset($entry['company']) === true && is_array($entry['company']) === true) { + $out[] = $entry['company']; + } else { + $out[] = $entry; + } + } + + return $out; + } + + if (array_is_list($response) === true) { + $out = []; + foreach ($response as $row) { + if (is_array($row) === true) { + $out[] = $row; + } + } + + return $out; + } + + return []; + }//end extractRows() +}//end class diff --git a/lib/Service/Integration/Providers/OpenProjectProvider.php b/lib/Service/Integration/Providers/OpenProjectProvider.php index f1fedb74aa..a91c112ad5 100644 --- a/lib/Service/Integration/Providers/OpenProjectProvider.php +++ b/lib/Service/Integration/Providers/OpenProjectProvider.php @@ -28,7 +28,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-openproject/tasks.md + * @spec openspec/specs/integration-openproject/spec.md */ declare(strict_types=1); @@ -69,9 +69,9 @@ class OpenProjectProvider extends AbstractIntegrationProvider /** * Constructor. * - * @param ExternalIntegrationRouter $router External-call router. - * @param IAppManager $appManager NC app manager. - * @param IL10N $l10n Localisation. + * @param ExternalIntegrationRouter $router External-call router. + * @param IAppManager $appManager NC app manager. + * @param IL10N $l10n Localisation. * @param OpenProjectLinkMapper|null $linkMapper Tier-2 link table (optional). * * @return void @@ -129,7 +129,7 @@ public function isEnabled(): bool * * @return array * - * @spec openspec/changes/integration-openproject/tasks.md + * @spec openspec/specs/integration-openproject/spec.md */ public function authRequirements(): array { @@ -157,7 +157,7 @@ public function authRequirements(): array * * @return array> * - * @spec openspec/changes/integration-openproject/tasks.md + * @spec openspec/specs/integration-openproject/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -228,7 +228,7 @@ private function rowFromLink(OpenProjectLink $link): array * * @return array * - * @spec openspec/changes/integration-openproject/tasks.md + * @spec openspec/specs/integration-openproject/spec.md */ public function get(string $register, string $schema, string $objectId, string $entityId): array { @@ -253,7 +253,7 @@ public function get(string $register, string $schema, string $objectId, string $ * * @return array * - * @spec openspec/changes/integration-openproject/tasks.md + * @spec openspec/specs/integration-openproject/spec.md */ public function create(string $register, string $schema, string $objectId, array $payload): array { @@ -283,7 +283,7 @@ public function create(string $register, string $schema, string $objectId, array * * @return array * - * @spec openspec/changes/integration-openproject/tasks.md + * @spec openspec/specs/integration-openproject/spec.md */ public function update(string $register, string $schema, string $objectId, string $entityId, array $payload): array { @@ -312,7 +312,7 @@ public function update(string $register, string $schema, string $objectId, strin * * @return void * - * @spec openspec/changes/integration-openproject/tasks.md + * @spec openspec/specs/integration-openproject/spec.md */ public function delete(string $register, string $schema, string $objectId, string $entityId): void { diff --git a/lib/Service/Integration/Providers/PhotosProvider.php b/lib/Service/Integration/Providers/PhotosProvider.php index 772bb37dd4..12a14d1e32 100644 --- a/lib/Service/Integration/Providers/PhotosProvider.php +++ b/lib/Service/Integration/Providers/PhotosProvider.php @@ -113,7 +113,7 @@ public function isEnabled(): bool * * @return array> List of registry leaf rows. * - * @spec openspec/changes/integration-photos/tasks.md + * @spec openspec/specs/integration-photos/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { diff --git a/lib/Service/Integration/Providers/PollsProvider.php b/lib/Service/Integration/Providers/PollsProvider.php index 42bff4d2a6..dfd522ff0c 100644 --- a/lib/Service/Integration/Providers/PollsProvider.php +++ b/lib/Service/Integration/Providers/PollsProvider.php @@ -27,7 +27,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-polls/tasks.md + * @spec openspec/specs/integration-polls/spec.md */ declare(strict_types=1); @@ -109,9 +109,11 @@ public function isEnabled(): bool * * @return array> * - * @spec openspec/changes/integration-polls/tasks.md + * @spec openspec/specs/integration-polls/spec.md * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) AbstractIntegrationProvider::list() mandates (register, schema, objectId, filters); $register and $schema are unused because Polls links are stored per-object-uuid only, but the interface signature must match. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) AbstractIntegrationProvider::list() mandates + * (register, schema, objectId, filters); $register and $schema are unused because Polls links + * are stored per-object-uuid only, but the interface signature must match. */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -197,7 +199,9 @@ private function fetchPollRow(int $pollId): ?array * * @return array * - * @SuppressWarnings(PHPMD.ShortVariable) $vq is the query-builder variable for the inner vote-count query; it is intentionally distinct from $qb (the outer options query) to make the two nested queries readable in sequence. + * @SuppressWarnings(PHPMD.ShortVariable) $vq is the query-builder variable for the inner vote-count + * query; it is intentionally distinct from $qb (the outer options query) to make the two nested + * queries readable in sequence. */ private function fetchOptionsWithCounts(int $pollId): array { diff --git a/lib/Service/Integration/Providers/SharesProvider.php b/lib/Service/Integration/Providers/SharesProvider.php index a04217dcb3..df731e14f9 100644 --- a/lib/Service/Integration/Providers/SharesProvider.php +++ b/lib/Service/Integration/Providers/SharesProvider.php @@ -38,15 +38,15 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-shares/tasks.md + * @spec openspec/specs/integration-shares/spec.md */ declare(strict_types=1); namespace OCA\OpenRegister\Service\Integration\Providers; -// phpcs:disable PEAR.Commenting.FunctionComment.Missing -- self-documenting IntegrationProvider metadata getters mirror the contract in the interface. - +// phpcs:disable PEAR.Commenting.FunctionComment.Missing -- self-documenting IntegrationProvider metadata; +// Getters mirror the contract in the interface. use OCA\OpenRegister\Service\Integration\AbstractIntegrationProvider; use OCP\App\IAppManager; use OCP\Files\Folder; @@ -64,9 +64,16 @@ * Always-on metadata: id='shares', group='core', requiredApp=null * (NC core sharing is always available), storage='query-time'. * - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Provider exposes list/delete/health plus lazy container resolution helpers (lookup, resolveContainer, resolveCurrentUserId, resolveObjectFolder, normaliseShare, mapServiceRow); each serves a distinct phase of the query-time share-walk and cannot be combined. - * @SuppressWarnings(PHPMD.StaticAccess) \OCP\Server::get() is the Nextcloud-prescribed static service-locator pattern for optional late-bound dependencies; no injectable alternative exists for the lazy-resolution policy (constructor injection would fail when the optional services are absent). - * @SuppressWarnings(PHPMD.UnusedPrivateMethod) mapServiceRow() is used as a callable in array_map([$this, 'mapServiceRow'], ...) — PHPMD does not trace callable-array references and incorrectly flags it as unused. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Provider exposes list/delete/health plus lazy + * container resolution helpers (lookup, resolveContainer, resolveCurrentUserId, resolveObjectFolder, + * normaliseShare, mapServiceRow); each serves a distinct phase of the query-time share-walk and + * cannot be combined. + * @SuppressWarnings(PHPMD.StaticAccess) \OCP\Server::get() is the Nextcloud-prescribed static + * service-locator pattern for optional late-bound dependencies; no injectable alternative exists + * for the lazy-resolution policy (constructor injection would fail when optional services are absent). + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) mapServiceRow() is used as a callable in + * array_map([$this, 'mapServiceRow'], ...) — PHPMD does not trace callable-array references + * and incorrectly flags it as unused. */ class SharesProvider extends AbstractIntegrationProvider { @@ -220,11 +227,18 @@ public function isEnabled(): bool * * @return array> * - * @spec openspec/changes/integration-shares/tasks.md - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) list() tries the ShareLinkService delegate first, then falls back to a local folder-walk; per-node, per-type, and per-share dedup logic are sequential — extracting them would split the single "get shares for object" contract across multiple helpers. - * @SuppressWarnings(PHPMD.NPathComplexity) Service-delegate + local-walk + Throwable fallback + folder-null + MAX_NODES cap + per-type Throwable swallow produce many paths; each guards a distinct degradation level per AD-23. - * @SuppressWarnings(PHPMD.UnusedFormalParameter) AbstractIntegrationProvider::list() mandates (register, schema, objectId, filters); $register and $schema are unused because shares are indexed per-object-uuid and there is no register/schema scope for NC core sharing. + * @spec openspec/specs/integration-shares/spec.md + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) list() tries the ShareLinkService delegate first, + * then falls back to a local folder-walk; per-node, per-type, and per-share dedup logic are + * sequential — extracting them would split the single "get shares for object" contract across + * multiple helpers. + * @SuppressWarnings(PHPMD.NPathComplexity) Service-delegate + local-walk + Throwable fallback + + * folder-null + MAX_NODES cap + per-type Throwable swallow produce many paths; each guards a + * distinct degradation level per AD-23. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) AbstractIntegrationProvider::list() mandates + * (register, schema, objectId, filters); $register and $schema are unused because shares are + * indexed per-object-uuid and there is no register/schema scope for NC core sharing. */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -343,23 +357,105 @@ private function mapServiceRow(array $row): array }//end mapServiceRow() /** - * Revoke / unlink a share by id. + * Mint a public token-based "track your case" link to the object. + * + * ADDITIVE: the legacy SharesProvider inherited the abstract + * NotImplementedException for create() (NC file shares are minted via + * the Files share panel, not here). This override adds a SECOND, + * independent create path — a public case-token link — without + * changing the existing list()/delete() NC-file-share behaviour. + * + * The payload selects the kind of thing to create: + * - `type: 'public-token'` (or `type` omitted) — mint a case token + * bound to the object via {@see CaseTokenService::mint()}. Optional + * `label` (string) and `ttlSeconds` (int) tune the link. Returns + * the token metadata + the public resolve URL. + * + * The token is an addressing handle only — the public resolve + * endpoint enforces RBAC (`_rbac: true`), so minting never grants + * read access beyond what the public group already has. + * + * When the CaseTokenService can't be resolved (older build) the call + * defers to the abstract NotImplementedException so the controller + * returns the canonical 501 — preserving backward compatibility. + * + * @param string $register Register slug or numeric id. + * @param string $schema Schema slug or numeric id. + * @param string $objectId Owning object uuid. + * @param array $payload Mint options (type/label/ttlSeconds). + * + * @return array Minted token metadata + public URL. + * + * @throws \OCA\OpenRegister\Exception\NotImplementedException When the + * token service is unavailable. * - * Delegates to `IManager::deleteShare()`. Failures defer to the - * abstract base's NotImplementedException so the controller layer - * translates them into the canonical HTTP code. + * @spec openspec/specs/integration-leaf-foundation/spec.md + */ + public function create(string $register, string $schema, string $objectId, array $payload): array + { + $type = (string) ($payload['type'] ?? 'public-token'); + if ($type !== 'public-token') { + // Only the public-token surface is supported by this provider; + // anything else defers to the abstract 501 contract. + return parent::create(register: $register, schema: $schema, objectId: $objectId, payload: $payload); + } + + $service = $this->lookup(serviceName: 'OCA\\OpenRegister\\Service\\CaseTokenService'); + if ($service === null || method_exists($service, 'mint') === false) { + return parent::create(register: $register, schema: $schema, objectId: $objectId, payload: $payload); + } + + $label = $payload['label'] ?? null; + $ttlSeconds = null; + if (isset($payload['ttlSeconds']) === true && is_numeric($payload['ttlSeconds']) === true) { + $ttlSeconds = (int) $payload['ttlSeconds']; + } + + $resolvedLabel = null; + if ($label !== null) { + $resolvedLabel = (string) $label; + } + + return $service->mint( + objectUuid: $objectId, + registerId: $this->numericOrNull(value: $register), + schemaId: $this->numericOrNull(value: $schema), + label: $resolvedLabel, + ttlSeconds: $ttlSeconds + ); + }//end create() + + /** + * Revoke / unlink a share OR a public case-token by id. + * + * ADDITIVE: keeps the original NC-file-share revoke + * (`IManager::deleteShare()`) untouched and adds a case-token revoke + * path. A `token:`-prefixed entityId addresses a case token; anything + * else is treated as an NC share id exactly as before. * * @param string $register Register slug. * @param string $schema Schema slug. * @param string $objectId Owning object uuid. - * @param string $entityId Share id to revoke. + * @param string $entityId Share id, or `token:`. * * @return void * - * @spec openspec/changes/integration-shares/tasks.md + * @spec openspec/specs/integration-shares/spec.md */ public function delete(string $register, string $schema, string $objectId, string $entityId): void { + // Case-token revoke path: `token:`. + if (str_starts_with($entityId, 'token:') === true) { + $service = $this->lookup(serviceName: 'OCA\\OpenRegister\\Service\\CaseTokenService'); + if ($service !== null && method_exists($service, 'revoke') === true) { + $service->revoke(substr($entityId, strlen('token:'))); + return; + } + + parent::delete(register: $register, schema: $schema, objectId: $objectId, entityId: $entityId); + return; + } + try { $shareManager = $this->lookup(serviceName: 'OCP\\Share\\IManager'); if ($shareManager === null) { @@ -374,6 +470,23 @@ public function delete(string $register, string $schema, string $objectId, strin } }//end delete() + /** + * Coerce a register/schema route param to an int, or null when it is + * a slug rather than a numeric id. + * + * @param string $value Register or schema slug / numeric id. + * + * @return int|null Numeric id, or null for a slug. + */ + private function numericOrNull(string $value): ?int + { + if (ctype_digit($value) === true) { + return (int) $value; + } + + return null; + }//end numericOrNull() + /** * Provider health descriptor. * diff --git a/lib/Service/Integration/Providers/TalkProvider.php b/lib/Service/Integration/Providers/TalkProvider.php index 5ec0cdbafd..d1332b4662 100644 --- a/lib/Service/Integration/Providers/TalkProvider.php +++ b/lib/Service/Integration/Providers/TalkProvider.php @@ -27,7 +27,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-talk/tasks.md + * @spec openspec/specs/integration-talk/spec.md */ declare(strict_types=1); @@ -141,7 +141,7 @@ public function isEnabled(): bool * (lib/Service/Integration/IntegrationProvider.php:209) but Talk room * lookup is keyed solely on $objectId. * - * @spec openspec/changes/integration-talk/tasks.md + * @spec openspec/specs/integration-talk/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { diff --git a/lib/Service/Integration/Providers/TimeProvider.php b/lib/Service/Integration/Providers/TimeProvider.php index 30d59a5a96..26e55b4bb1 100644 --- a/lib/Service/Integration/Providers/TimeProvider.php +++ b/lib/Service/Integration/Providers/TimeProvider.php @@ -34,7 +34,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-time-tracker/tasks.md + * @spec openspec/specs/integration-time-tracker/spec.md */ declare(strict_types=1); @@ -47,6 +47,7 @@ use OCA\OpenRegister\Db\TimeTrackerLinkMapper; use OCA\OpenRegister\Service\Integration\AbstractIntegrationProvider; use OCP\App\IAppManager; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; use Throwable; @@ -55,7 +56,25 @@ class TimeProvider extends AbstractIntegrationProvider { use MarkerLookupTrait; - private const REQUIRED_APP = 'timemanager'; + /** + * Default backing NC time-tracking app id. + * + * The admin can override this via the `time-tracker.backend` app + * config key (per integration-time-tracker AD-1) to point the + * provider at a different time-tracking app that exposes a + * compatible adapter. + * + * @var string + */ + private const DEFAULT_BACKEND = 'timemanager'; + + /** + * Config key (under app `openregister`) carrying the admin's + * chosen backing time-tracking app id. Default is `timemanager`. + * + * @var string + */ + public const BACKEND_CONFIG_KEY = 'time-tracker.backend'; private const MARKER_PREFIX = '[or:'; @@ -66,6 +85,7 @@ class TimeProvider extends AbstractIntegrationProvider * @param IAppManager $appManager NC app manager. * @param IL10N $l10n Localisation. * @param TimeTrackerLinkMapper $linkMapper Time-tracker-link mapper (Tier-2 link table). + * @param IConfig $config NC config (reads the time-tracker.backend admin override). * * @SuppressWarnings(PHPMD.ShortVariable) $db is a well-known PHP idiom for a database connection parameter. */ @@ -74,9 +94,31 @@ public function __construct( private IAppManager $appManager, private IL10N $l10n, private TimeTrackerLinkMapper $linkMapper, + private IConfig $config, ) { }//end __construct() + /** + * Resolve the configured backing time-tracking app id. + * + * Reads the admin override from app-config (`time-tracker.backend`, + * default `timemanager`) every call — admin changes propagate + * without restarting the service. + * + * @return string + * + * @spec openspec/specs/integration-time-tracker/spec.md + */ + private function backendAppId(): string + { + $value = $this->config->getAppValue('openregister', self::BACKEND_CONFIG_KEY, self::DEFAULT_BACKEND); + if (is_string($value) === false || $value === '') { + return self::DEFAULT_BACKEND; + } + + return $value; + }//end backendAppId() + public function getId(): string { return 'time-tracker'; @@ -99,7 +141,7 @@ public function getGroup(): ?string public function getRequiredApp(): ?string { - return self::REQUIRED_APP; + return $this->backendAppId(); }//end getRequiredApp() public function getStorageStrategy(): string @@ -109,7 +151,7 @@ public function getStorageStrategy(): string public function isEnabled(): bool { - return $this->appManager->isInstalled(self::REQUIRED_APP); + return $this->appManager->isInstalled($this->backendAppId()); }//end isEnabled() /** @@ -128,9 +170,10 @@ public function isEnabled(): bool * * @return array> List of registry leaf rows. * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) $register, $schema and $filters are required by the IntegrationProvider interface contract; this implementation routes by $objectId only. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $register, $schema and $filters are required by the + * IntegrationProvider interface contract; this implementation routes by $objectId only. * - * @spec openspec/changes/integration-time-tracker/tasks.md + * @spec openspec/specs/integration-time-tracker/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -298,8 +341,9 @@ private function legacyMarkerRows(string $objectId): array public function health(): array { $available = $this->isEnabled(); + $backend = $this->backendAppId(); $status = 'unavailable'; - $message = 'NC TimeManager app is not installed'; + $message = sprintf('Backing time-tracking app (%s) is not installed', $backend); if ($available === true) { $status = 'ok'; $message = null; diff --git a/lib/Service/Integration/Providers/XwikiProvider.php b/lib/Service/Integration/Providers/XwikiProvider.php index de214a619e..b56c2c11d3 100644 --- a/lib/Service/Integration/Providers/XwikiProvider.php +++ b/lib/Service/Integration/Providers/XwikiProvider.php @@ -38,7 +38,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-xwiki/tasks.md + * @spec openspec/specs/integration-xwiki/spec.md */ declare(strict_types=1); @@ -192,7 +192,7 @@ public function isEnabled(): bool * * @return array * - * @spec openspec/changes/integration-xwiki/tasks.md + * @spec openspec/specs/integration-xwiki/spec.md */ public function authRequirements(): array { @@ -224,7 +224,7 @@ public function authRequirements(): array * * @return array> * - * @spec openspec/changes/integration-xwiki/tasks.md + * @spec openspec/specs/integration-xwiki/spec.md */ public function list(string $register, string $schema, string $objectId, array $filters=[]): array { @@ -314,7 +314,7 @@ private function listLinkedPages(string $objectId, string $register, string $sch * * @return array * - * @spec openspec/changes/integration-xwiki/tasks.md + * @spec openspec/specs/integration-xwiki/spec.md */ public function get(string $register, string $schema, string $objectId, string $entityId): array { @@ -344,7 +344,7 @@ public function get(string $register, string $schema, string $objectId, string $ * * @return array * - * @spec openspec/changes/integration-xwiki/tasks.md + * @spec openspec/specs/integration-xwiki/spec.md */ public function create(string $register, string $schema, string $objectId, array $payload): array { @@ -374,7 +374,7 @@ public function create(string $register, string $schema, string $objectId, array * * @return array * - * @spec openspec/changes/integration-xwiki/tasks.md + * @spec openspec/specs/integration-xwiki/spec.md */ public function update(string $register, string $schema, string $objectId, string $entityId, array $payload): array { @@ -404,7 +404,7 @@ public function update(string $register, string $schema, string $objectId, strin * * @return void * - * @spec openspec/changes/integration-xwiki/tasks.md + * @spec openspec/specs/integration-xwiki/spec.md */ public function delete(string $register, string $schema, string $objectId, string $entityId): void { diff --git a/lib/Service/Integration/TimeProvider.php b/lib/Service/Integration/TimeProvider.php new file mode 100644 index 0000000000..81c34224c4 --- /dev/null +++ b/lib/Service/Integration/TimeProvider.php @@ -0,0 +1,195 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Integration; + +use OCP\IAppConfig; + +/** + * TimeProvider implements IntegrationProvider for the time-tracker integration. + * + * Extends AbstractIntegrationProvider so it inherits the default + * implementations for the optional / CRUD methods (authRequirements, + * getOpenConnectorSource, get/create/update/delete, health). Only the + * methods specific to the time-tracker integration are overridden below, + * plus the two remaining abstract contract methods (isEnabled, list). + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ +class TimeProvider extends AbstractIntegrationProvider +{ + + /** + * Integration id. + */ + public const ID = 'time-tracker'; + + /** + * Default backend app. + */ + private const DEFAULT_BACKEND = 'timemanager'; + + /** + * App configuration for reading the admin-configured backend. + * + * @var IAppConfig + */ + private readonly IAppConfig $appConfig; + + /** + * Constructor. + * + * @param IAppConfig $appConfig App configuration. + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function __construct(IAppConfig $appConfig) + { + $this->appConfig = $appConfig; + }//end __construct() + + /** + * Return the integration id. + * + * @return string + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function getId(): string + { + return self::ID; + }//end getId() + + /** + * Return the human-readable label. + * + * @return string + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function getLabel(): string + { + return 'Time'; + }//end getLabel() + + /** + * Return the icon name. + * + * @return string + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function getIcon(): string + { + return 'Clock'; + }//end getIcon() + + /** + * Return the functional group. + * + * @return string|null + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function getGroup(): ?string + { + return 'workflow'; + }//end getGroup() + + /** + * Return the required NC app (from admin setting or default 'timemanager'). + * + * @return string|null + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function getRequiredApp(): ?string + { + return $this->appConfig->getValueString('openregister', 'time-tracker.backend', self::DEFAULT_BACKEND); + }//end getRequiredApp() + + /** + * Return the storage strategy. + * + * @return string + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function getStorageStrategy(): string + { + return 'link-table'; + }//end getStorageStrategy() + + /** + * Permissions are governed by the backend app's ACLs; return null. + * + * @return string|null + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function requiresPermission(): ?string + { + return null; + }//end requiresPermission() + + /** + * Whether the integration is enabled. + * + * This Tier-1 descriptor provider is always considered enabled; the + * concrete backend-availability probe (checking whether the configured + * TimeManager app is installed) lives on the Tier-2 + * `Providers\TimeProvider`, which backs the live link/list paths. + * + * @return bool + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + */ + public function isEnabled(): bool + { + return true; + }//end isEnabled() + + /** + * List linked time entries for an object. + * + * Tier-1 descriptor provider returns an empty list; the live listing + * (link-table read + legacy marker fallback) is implemented on the + * Tier-2 `Providers\TimeProvider`. + * + * @param string $register Register slug or numeric id. + * @param string $schema Schema slug or numeric id. + * @param string $objectId Owning object uuid. + * @param array $filters Optional filters. + * + * @return array> + * + * @spec openspec/changes/integration-time-tracker/tasks.md#task-4 + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) Parameters are mandated by the IntegrationProvider interface; + * this Tier-1 descriptor provider returns an empty list — the Tier-2 provider uses all parameters. + */ + public function list(string $register, string $schema, string $objectId, array $filters=[]): array + { + return []; + }//end list() +}//end class diff --git a/lib/Service/JsonLd/JsonLdContextService.php b/lib/Service/JsonLd/JsonLdContextService.php new file mode 100644 index 0000000000..19aaf55f18 --- /dev/null +++ b/lib/Service/JsonLd/JsonLdContextService.php @@ -0,0 +1,618 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\JsonLd; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\IURLGenerator; + +/** + * Derives JSON-LD context documents from schema definitions. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/specs/json-ld-output/spec.md + */ +class JsonLdContextService +{ + + /** + * The OpenRegister metadata namespace IRI. Used for the `or:` prefix that + * carries `@self`-derived metadata terms in serialized objects. + * + * @var string + */ + public const OR_NAMESPACE = 'https://openregister.app/ns#'; + + /** + * The XML Schema datatypes namespace, used for `@type` coercions. + * + * @var string + */ + public const XSD_NAMESPACE = 'http://www.w3.org/2001/XMLSchema#'; + + /** + * Request-scoped cache of derived schema contexts, keyed by + * `{schemaId}:{updatedTimestamp}` so a schema edit busts the entry. + * + * @var array> + */ + private array $contextCache = []; + + /** + * Constructor. + * + * @param IURLGenerator $urlGenerator The URL generator for context-document URLs. + */ + public function __construct( + private readonly IURLGenerator $urlGenerator + ) { + }//end __construct() + + /** + * Build the absolute URL of a per-schema context document. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The schema. + * + * @return string The absolute context-document URL. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function getSchemaContextUrl(Register $register, Schema $schema): string + { + return $this->urlGenerator->linkToRouteAbsolute( + 'openregister.contexts.schema', + [ + 'register' => $this->slugOf(entity: $register), + 'schema' => $this->slugOf(entity: $schema), + ] + ); + }//end getSchemaContextUrl() + + /** + * Build the absolute URL of the register-wide context document. + * + * @param Register $register The register. + * + * @return string The absolute context-document URL. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function getRegisterContextUrl(Register $register): string + { + return $this->urlGenerator->linkToRouteAbsolute( + 'openregister.contexts.register', + ['register' => $this->slugOf(entity: $register)] + ); + }//end getRegisterContextUrl() + + /** + * Resolve the class IRI used as a serialized object's `@type` for a schema. + * + * Defaults to the schema slug (a term resolvable in the schema context), + * or the explicit `jsonld.type` mapping when configured. + * + * @param Schema $schema The schema. + * + * @return string The `@type` value (class IRI or schema-slug term). + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function getTypeForSchema(Schema $schema): string + { + $jsonld = $this->jsonLdConfig(schema: $schema); + if (isset($jsonld['type']) === true && is_string($jsonld['type']) === true && $jsonld['type'] !== '') { + return $jsonld['type']; + } + + return $this->slugOf(entity: $schema); + }//end getTypeForSchema() + + /** + * Compute the canonical semantic types a schema implements. + * + * The implemented type set is the union of, in order: + * 1. `configuration.implements[]` — absolute IRIs (non-IRI entries dropped); + * 2. `configuration.jsonld.type` — the single object `@type` IRI, when set; + * 3. a schema-level `x-schema-org` marker (top-level field, or under + * `configuration`) carrying a compact schema.org CURIE such as + * `schema:Organization`, expanded to `https://schema.org/Organization`. + * Both string and array forms are accepted, so existing schema.org- + * annotated schemas (ADR-011) become resolvable with no edit. + * + * When `configuration.implements` is present it is authoritative and the + * `jsonld.type` / `x-schema-org` defaults are NOT merged (an explicit list + * is an explicit contract). Otherwise the `jsonld.type` and `x-schema-org` + * markers are unioned. + * + * Unlike {@see getTypeForSchema()} this never falls back to the schema + * slug: a schema with no absolute-IRI semantic marker implements nothing + * resolvable and MUST NOT be picked up by a URI lookup. Only schema-LEVEL + * markers count — property-level `x-schema-org` annotations describe a + * field, not the schema, and are ignored here. + * + * @param Schema $schema The schema. + * + * @return array The list of implemented absolute-IRI types (may be empty). + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + * (Requirement: Schemas advertise the canonical types they implement) + */ + public function getImplementedTypes(Schema $schema): array + { + $configuration = $schema->getConfiguration(); + if (is_array($configuration) === false) { + $configuration = []; + } + + // Explicit `implements` list is authoritative; keep only absolute IRIs. + $implements = ($configuration['implements'] ?? null); + if (is_array($implements) === true) { + $types = []; + foreach ($implements as $candidate) { + if ($this->isAbsoluteIri(value: $candidate) === true) { + $types[] = $candidate; + } + } + + return array_values(array_unique($types)); + } + + // Otherwise union the default markers. + $types = []; + + // (a) `jsonld.type` IRI, when absolute. + $jsonld = $this->jsonLdConfig(schema: $schema); + $type = ($jsonld['type'] ?? null); + if ($this->isAbsoluteIri(value: $type) === true) { + $types[] = $type; + } + + // (b) schema-level `x-schema-org` CURIE(s) — top-level or in configuration. + $xSchemaOrg = ($configuration['x-schema-org'] ?? null); + foreach ($this->expandSchemaOrgMarker(marker: $xSchemaOrg) as $iri) { + $types[] = $iri; + } + + return array_values(array_unique($types)); + }//end getImplementedTypes() + + /** + * Expand a schema-level `x-schema-org` marker into absolute schema.org IRIs. + * + * Accepts a single value or an array. Each entry may be a compact CURIE + * (`schema:Organization` → `https://schema.org/Organization`) or an already- + * absolute IRI (passed through). Non-string / unresolvable entries are + * dropped. + * + * @param mixed $marker The raw `x-schema-org` marker value (string, array, or null). + * + * @return array Expanded absolute IRIs (may be empty). + */ + private function expandSchemaOrgMarker(mixed $marker): array + { + if ($marker === null) { + return []; + } + + if (is_array($marker) === true) { + $entries = $marker; + } else { + $entries = [$marker]; + } + + $iris = []; + foreach ($entries as $entry) { + if (is_string($entry) === false || $entry === '') { + continue; + } + + // Compact schema.org CURIE, e.g. `schema:Organization`. + if (str_starts_with($entry, 'schema:') === true) { + $term = substr($entry, strlen('schema:')); + if ($term !== '') { + $iris[] = 'https://schema.org/'.$term; + } + + continue; + } + + // Already an absolute IRI — accept as-is. + if ($this->isAbsoluteIri(value: $entry) === true) { + $iris[] = $entry; + } + } + + return $iris; + }//end expandSchemaOrgMarker() + + /** + * Build the JSON-LD context map for a single schema. + * + * The returned array is the *value* of a `@context` key, i.e. + * `{"or": "...", "name": {...}, ...}`. It always defines the `or:` prefix, + * an `xsd:` prefix, a `@vocab` when configured, the schema class term, and + * one term per schema property (mapped IRI or zero-config fragment term). + * + * Result is request-cached per (schema id, updated timestamp). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The schema. + * + * @return array The JSON-LD context map. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function buildSchemaContext(Register $register, Schema $schema): array + { + $cacheKey = $this->cacheKey(schema: $schema); + if (isset($this->contextCache[$cacheKey]) === true) { + return $this->contextCache[$cacheKey]; + } + + $jsonld = $this->jsonLdConfig(schema: $schema); + $vocab = ($jsonld['@vocab'] ?? null); + $propMap = ($jsonld['properties'] ?? []); + if (is_array($propMap) === false) { + $propMap = []; + } + + $contextUrl = $this->getSchemaContextUrl(register: $register, schema: $schema); + + // Base prefixes always present. + $context = [ + 'or' => self::OR_NAMESPACE, + 'xsd' => self::XSD_NAMESPACE, + ]; + + // Vocabulary base, when declared. + if (is_string($vocab) === true && $vocab !== '') { + $context['@vocab'] = $vocab; + } + + // The schema class term itself (used as @type when no jsonld.type IRI). + if (isset($jsonld['type']) === true && is_string($jsonld['type']) === true && $jsonld['type'] !== '') { + $context[$this->slugOf(entity: $schema)] = $jsonld['type']; + } else { + $context[$this->slugOf(entity: $schema)] = $contextUrl.'#'.$this->slugOf(entity: $schema); + } + + // One term per schema property. + foreach ($schema->getProperties() as $name => $definition) { + if (is_string($name) === false || $name === '') { + continue; + } + + if (is_array($definition) === true) { + $propertyDefinition = $definition; + } else { + $propertyDefinition = []; + } + + $context[$name] = $this->buildPropertyTerm( + name: $name, + definition: $propertyDefinition, + mappedIri: ($propMap[$name] ?? null), + contextUrl: $contextUrl + ); + } + + $this->contextCache[$cacheKey] = $context; + return $context; + }//end buildSchemaContext() + + /** + * Build the register-wide context document: a merged term block covering + * every schema in the register (later schemas win on term collisions, which + * is acceptable for a register-level convenience document; per-schema + * contexts remain authoritative). + * + * @param Register $register The register. + * @param array $schemas The register's schema entities. + * + * @return array The merged JSON-LD context map. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function buildRegisterContext(Register $register, array $schemas): array + { + $context = [ + 'or' => self::OR_NAMESPACE, + 'xsd' => self::XSD_NAMESPACE, + ]; + + foreach ($schemas as $schema) { + if (($schema instanceof Schema) === false) { + continue; + } + + $schemaContext = $this->buildSchemaContext(register: $register, schema: $schema); + foreach ($schemaContext as $term => $value) { + // Keep the shared prefixes from the base; merge the rest. + if ($term === 'or' || $term === 'xsd') { + continue; + } + + $context[$term] = $value; + } + } + + return $context; + }//end buildRegisterContext() + + /** + * Validate a schema's `jsonld` mapping block. Term values must be absolute + * IRIs, or compact terms resolvable against a declared `@vocab`. Unknown + * property names are tolerated (schemas evolve). + * + * @param array $jsonld The `jsonld` configuration block. + * + * @return array A list of human-readable validation errors (empty when valid). + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function validateMapping(array $jsonld): array + { + $errors = []; + $vocab = ($jsonld['@vocab'] ?? null); + + if (array_key_exists('@vocab', $jsonld) === true && $this->isAbsoluteIri(value: $vocab) === false) { + $errors[] = '@vocab must be an absolute IRI'; + } + + if (is_string($vocab) === true) { + $vocabBase = $vocab; + } else { + $vocabBase = null; + } + + if (array_key_exists('type', $jsonld) === true) { + $type = $jsonld['type']; + if ($this->isResolvableTerm(value: $type, vocab: $vocabBase) === false) { + $errors[] = 'type must be an absolute IRI or a term resolvable against @vocab'; + } + } + + if (array_key_exists('properties', $jsonld) === true) { + if (is_array($jsonld['properties']) === false) { + $errors[] = 'properties must be an object mapping property names to IRIs'; + } else { + foreach ($jsonld['properties'] as $property => $iri) { + if ($this->isResolvableTerm(value: $iri, vocab: $vocabBase) === false) { + $errors[] = sprintf( + "properties.%s must be an absolute IRI or a term resolvable against @vocab", + (string) $property + ); + } + } + } + } + + return $errors; + }//end validateMapping() + + /** + * Build a single JSON-LD context term for a schema property. + * + * @param string $name The property name. + * @param array $definition The JSON-Schema property definition. + * @param mixed $mappedIri An explicit vocabulary IRI, or null. + * @param string $contextUrl The context-document URL (for fallback fragment terms). + * + * @return array|string The term definition (string IRI, or expanded term definition). + */ + private function buildPropertyTerm(string $name, array $definition, mixed $mappedIri, string $contextUrl): array | string + { + // Determine the @id of the term: explicit mapping, else fragment term. + if (is_string($mappedIri) === true && $mappedIri !== '') { + $id = $mappedIri; + } else { + $id = $contextUrl.'#'.$name; + } + + $coercion = $this->coercionForDefinition(definition: $definition); + + // Plain string term when there is no datatype/node coercion. + if ($coercion === null) { + return $id; + } + + return [ + '@id' => $id, + '@type' => $coercion, + ]; + }//end buildPropertyTerm() + + /** + * Map a JSON-Schema property definition to a JSON-LD `@type` coercion. + * + * - relation properties (object/array with `$ref`, or `format: uri`) → + * `@id` (node reference) + * - `format: date` → `xsd:date` + * - `format: date-time` → `xsd:dateTime` + * - everything else → null (no coercion) + * + * @param array $definition The JSON-Schema property definition. + * + * @return string|null The coercion value, or null when none applies. + */ + private function coercionForDefinition(array $definition): ?string + { + $format = ($definition['format'] ?? null); + + if ($this->isRelationDefinition(definition: $definition) === true || $format === 'uri' || $format === 'uri-reference') { + return '@id'; + } + + if ($format === 'date') { + return 'xsd:date'; + } + + if ($format === 'date-time') { + return 'xsd:dateTime'; + } + + return null; + }//end coercionForDefinition() + + /** + * Determine whether a JSON-Schema property definition is a relation to + * another register object (carries a `$ref`, an `objectConfiguration`, + * or items with a `$ref`). + * + * @param array $definition The JSON-Schema property definition. + * + * @return bool True when the property references another object. + */ + private function isRelationDefinition(array $definition): bool + { + if (isset($definition['$ref']) === true) { + return true; + } + + if (isset($definition['objectConfiguration']) === true) { + return true; + } + + $items = ($definition['items'] ?? null); + if (is_array($items) === true && (isset($items['$ref']) === true || isset($items['objectConfiguration']) === true)) { + return true; + } + + return false; + }//end isRelationDefinition() + + /** + * Read the optional `jsonld` block from a schema's configuration. + * + * @param Schema $schema The schema. + * + * @return array The `jsonld` block, or an empty array. + */ + private function jsonLdConfig(Schema $schema): array + { + $configuration = $schema->getConfiguration(); + if (is_array($configuration) === false) { + return []; + } + + $jsonld = ($configuration['jsonld'] ?? null); + if (is_array($jsonld) === false) { + return []; + } + + return $jsonld; + }//end jsonLdConfig() + + /** + * Compute the request-cache key for a schema context. + * + * @param Schema $schema The schema. + * + * @return string The cache key. + */ + private function cacheKey(Schema $schema): string + { + $updated = $schema->getUpdated(); + if ($updated !== null) { + $stamp = $updated->format('U'); + } else { + $stamp = '0'; + } + + return ((string) $schema->getId()).':'.$stamp; + }//end cacheKey() + + /** + * Resolve the slug for an entity, falling back to its UUID then id so a + * route parameter is always available. + * + * @param Register|Schema $entity The entity. + * + * @return string The slug-like identifier. + */ + private function slugOf(Register | Schema $entity): string + { + $slug = $entity->getSlug(); + if (is_string($slug) === true && $slug !== '') { + return $slug; + } + + $uuid = $entity->getUuid(); + if (is_string($uuid) === true && $uuid !== '') { + return $uuid; + } + + return (string) $entity->getId(); + }//end slugOf() + + /** + * Whether a value is an absolute IRI (has a scheme, e.g. `https:`). + * + * @param mixed $value The candidate value. + * + * @return bool True when the value is an absolute IRI. + */ + public function isAbsoluteIri(mixed $value): bool + { + if (is_string($value) === false || $value === '') { + return false; + } + + return preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*:/', $value) === 1; + }//end isAbsoluteIri() + + /** + * Whether a value is a resolvable JSON-LD term: an absolute IRI, or a + * compact term that resolves against a declared `@vocab`. + * + * @param mixed $value The candidate term value. + * @param string|null $vocab The declared vocabulary base, or null. + * + * @return bool True when the term is resolvable. + */ + private function isResolvableTerm(mixed $value, ?string $vocab): bool + { + if (is_string($value) === false || $value === '') { + return false; + } + + if ($this->isAbsoluteIri(value: $value) === true) { + return true; + } + + // A bare term (no scheme, no whitespace) is acceptable only when a + // @vocab base is declared to resolve it against. + if ($vocab !== null && $vocab !== '' && preg_match('/\s/', $value) !== 1) { + return true; + } + + return false; + }//end isResolvableTerm() +}//end class diff --git a/lib/Service/JsonLd/JsonLdSerializer.php b/lib/Service/JsonLd/JsonLdSerializer.php new file mode 100644 index 0000000000..53491a1deb --- /dev/null +++ b/lib/Service/JsonLd/JsonLdSerializer.php @@ -0,0 +1,384 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\JsonLd; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\IRequest; +use OCP\IURLGenerator; + +/** + * Serializes rendered object arrays as JSON-LD. + * + * @psalm-suppress UnusedClass + * + * @spec openspec/specs/json-ld-output/spec.md + */ +class JsonLdSerializer +{ + + /** + * The IANA media type for JSON-LD. + * + * @var string + */ + public const MEDIA_TYPE = 'application/ld+json'; + + /** + * The escape prefix used for data keys that begin with `@` (other than the + * injected `@context`/`@id`/`@type`), so the document stays valid JSON-LD. + * + * @var string + */ + private const RAW_PREFIX = 'or:raw#'; + + /** + * Constructor. + * + * @param JsonLdContextService $contextService The context-document service. + * @param IURLGenerator $urlGenerator The URL generator (for `@id` fallback). + */ + public function __construct( + private readonly JsonLdContextService $contextService, + private readonly IURLGenerator $urlGenerator + ) { + }//end __construct() + + /** + * Decide whether the client wants JSON-LD, based on the `Accept` header. + * + * True only when `application/ld+json` is the highest-weighted matching + * media type. Absent headers, `application/json`, and wildcard-only matches + * (`*` / `application/*`) resolve to plain JSON (default unchanged). + * + * @param IRequest $request The current request. + * + * @return bool True when JSON-LD output is requested. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function wantsJsonLd(IRequest $request): bool + { + $accept = $request->getHeader('Accept'); + if ($accept === '') { + return false; + } + + $best = null; + $bestQ = -1.0; + $bestRank = -1; + + foreach (explode(',', $accept) as $part) { + $segments = explode(';', trim($part)); + $type = strtolower(trim($segments[0])); + if ($type === '') { + continue; + } + + $q = 1.0; + foreach (array_slice($segments, 1) as $paramSegment) { + $paramSegment = trim($paramSegment); + if (stripos($paramSegment, 'q=') === 0) { + $q = (float) substr($paramSegment, 2); + } + } + + // Specificity rank: explicit type beats subtype-wildcard beats `*/*`. + $rank = 2; + if ($type === '*/*') { + $rank = 0; + } else if (str_ends_with($type, '/*') === true) { + $rank = 1; + } + + // Highest q wins; ties broken by specificity. + if ($q > $bestQ || ($q === $bestQ && $rank > $bestRank)) { + $bestQ = $q; + $bestRank = $rank; + $best = $type; + } + }//end foreach + + return $best === self::MEDIA_TYPE && $bestQ > 0.0; + }//end wantsJsonLd() + + /** + * Serialize a single rendered object array as a JSON-LD node document. + * + * @param array $renderedObject The rendered object (top-level data + `@self`). + * @param Schema $schema The object's schema. + * @param Register $register The object's register. + * + * @return array The JSON-LD document. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function serialize(array $renderedObject, Schema $schema, Register $register): array + { + $node = $this->buildNode(renderedObject: $renderedObject, schema: $schema, register: $register); + + // Prepend the @context reference (per-schema context document URL). + return array_merge( + ['@context' => $this->contextService->getSchemaContextUrl($register, $schema)], + $node + ); + }//end serialize() + + /** + * Serialize a paginated collection result as a JSON-LD `@graph` document. + * + * @param array $paginatedResult The `index()` result (results/total/page/...). + * @param Schema $schema The collection's schema. + * @param Register $register The collection's register. + * + * @return array The JSON-LD `@graph` document. + * + * @spec openspec/specs/json-ld-output/spec.md + */ + public function serializeCollection(array $paginatedResult, Schema $schema, Register $register): array + { + $results = ($paginatedResult['results'] ?? []); + if (is_array($results) === false) { + $results = []; + } + + $graph = []; + foreach ($results as $rendered) { + if (is_array($rendered) === false) { + continue; + } + + // Each node carries its own @id/@type but not a repeated @context. + $graph[] = $this->buildNode(renderedObject: $rendered, schema: $schema, register: $register); + } + + $document = [ + '@context' => $this->contextService->getSchemaContextUrl($register, $schema), + '@graph' => $graph, + ]; + + // Pagination metadata under or: terms (keeps the document valid JSON-LD). + if (array_key_exists('total', $paginatedResult) === true) { + $document['or:total'] = $paginatedResult['total']; + } + + if (array_key_exists('page', $paginatedResult) === true) { + $document['or:page'] = $paginatedResult['page']; + } + + if (array_key_exists('pages', $paginatedResult) === true) { + $document['or:pages'] = $paginatedResult['pages']; + } + + if (array_key_exists('limit', $paginatedResult) === true) { + $document['or:limit'] = $paginatedResult['limit']; + } + + $next = $this->buildNextLink(paginatedResult: $paginatedResult); + if ($next !== null) { + $document['or:next'] = $next; + } + + return $document; + }//end serializeCollection() + + /** + * Build a single JSON-LD node (no `@context`) from a rendered object array. + * + * @param array $renderedObject The rendered object. + * @param Schema $schema The object's schema. + * @param Register $register The object's register. + * + * @return array The JSON-LD node. + */ + private function buildNode(array $renderedObject, Schema $schema, Register $register): array + { + $self = ($renderedObject['@self'] ?? []); + if (is_array($self) === false) { + $self = []; + } + + $node = [ + '@id' => $this->resolveId(self: $self, register: $register, schema: $schema), + '@type' => $this->contextService->getTypeForSchema($schema), + ]; + + // Lift @self metadata to or: terms (skip null/empty so the node stays lean). + foreach ($this->liftSelfMetadata(self: $self) as $term => $value) { + $node[$term] = $value; + } + + // Copy the object's data properties, escaping reserved keys. + foreach ($renderedObject as $key => $value) { + if ($key === '@self') { + // Replaced by the lifted or: terms; never emitted. + continue; + } + + if ($this->isReservedKey(key: $key) === true) { + $node[self::RAW_PREFIX.ltrim((string) $key, '@')] = $value; + continue; + } + + $node[$key] = $value; + } + + return $node; + }//end buildNode() + + /** + * Resolve a node's `@id`: the canonical object URI, else the absolute + * `objects#show` route URL. + * + * @param array $self The object's `@self` envelope. + * @param Register $register The object's register. + * @param Schema $schema The object's schema. + * + * @return string The dereferenceable `@id`. + */ + private function resolveId(array $self, Register $register, Schema $schema): string + { + $uri = ($self['uri'] ?? null); + if (is_string($uri) === true && $uri !== '') { + return $uri; + } + + $uuid = ($self['id'] ?? ''); + return $this->urlGenerator->linkToRouteAbsolute( + 'openregister.objects.show', + [ + 'register' => $this->slugOf(entity: $register), + 'schema' => $this->slugOf(entity: $schema), + 'id' => (string) $uuid, + ] + ); + }//end resolveId() + + /** + * Lift the `@self` envelope into `or:`-prefixed JSON-LD terms. The `uri` + * key is excluded (it is the node's `@id`); null/empty values are skipped. + * + * @param array $self The `@self` envelope. + * + * @return array The lifted `or:` terms. + */ + private function liftSelfMetadata(array $self): array + { + $lifted = []; + foreach ($self as $key => $value) { + if ($key === 'uri') { + // Exposed as @id, not duplicated. + continue; + } + + if ($value === null || $value === '' || $value === []) { + continue; + } + + $lifted['or:'.$key] = $value; + } + + return $lifted; + }//end liftSelfMetadata() + + /** + * Build the `or:next` page URL from a paginated result, when there is a + * next page. + * + * @param array $paginatedResult The paginated result. + * + * @return string|null The next-page URL, or null when on the last page. + */ + private function buildNextLink(array $paginatedResult): ?string + { + // Honour a server-provided next link if present. + if (isset($paginatedResult['next']) === true && is_string($paginatedResult['next']) === true) { + return $paginatedResult['next']; + } + + $page = ($paginatedResult['page'] ?? null); + $pages = ($paginatedResult['pages'] ?? null); + if (is_int($page) === false || is_int($pages) === false || $page >= $pages) { + return null; + } + + // Derive next-page URL from the current request URI when available. + $current = ($paginatedResult['@self']['self'] ?? null); + if (is_string($current) === false || $current === '') { + return null; + } + + if (str_contains($current, '?') === true) { + $separator = '&'; + } else { + $separator = '?'; + } + + return $current.$separator.'_page='.($page + 1); + }//end buildNextLink() + + /** + * Whether a data key is a reserved JSON-LD keyword that must be escaped to + * avoid colliding with the injected keywords. + * + * @param int|string $key The data key. + * + * @return bool True when the key must be escaped. + */ + private function isReservedKey(int | string $key): bool + { + return is_string($key) === true && str_starts_with($key, '@') === true; + }//end isReservedKey() + + /** + * Resolve the slug for an entity, falling back to UUID then id. + * + * @param Register|Schema $entity The entity. + * + * @return string The slug-like identifier. + */ + private function slugOf(Register | Schema $entity): string + { + $slug = $entity->getSlug(); + if (is_string($slug) === true && $slug !== '') { + return $slug; + } + + $uuid = $entity->getUuid(); + if (is_string($uuid) === true && $uuid !== '') { + return $uuid; + } + + return (string) $entity->getId(); + }//end slugOf() +}//end class diff --git a/lib/Service/LanguageService.php b/lib/Service/LanguageService.php index ec48e970bf..cd8dfbd84d 100644 --- a/lib/Service/LanguageService.php +++ b/lib/Service/LanguageService.php @@ -68,6 +68,33 @@ class LanguageService */ private bool $fallbackUsed = false; + /** + * Source of the resolved preferred language, for introspection. + * + * One of `'query'`, `'header'`, or `'default'`. Used by the + * `X-Source-Language` response setter (i18n-source-of-truth) and + * external diagnostics tooling. + * + * @var string + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-1 + */ + private string $requestedLanguageSource = 'default'; + + /** + * Optional BCP-47 target language for write requests, read from the + * `X-Translation-Target-Language` header. + * + * When set, the TranslationHandler treats scalar request bodies as + * updates to that target language instead of wrapping under the + * register's default language. + * + * @var string|null + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-2 + */ + private ?string $targetLanguage = null; + /** * Set the preferred language. * @@ -158,6 +185,71 @@ public function isFallbackUsed(): bool return $this->fallbackUsed; }//end isFallbackUsed() + /** + * Set the source of the resolved preferred language. + * + * @param string $source One of `'query'`, `'header'`, `'default'`. + * + * @return void + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-1 + */ + public function setRequestedLanguageSource(string $source): void + { + if (in_array($source, ['query', 'header', 'default'], true) === false) { + return; + } + + $this->requestedLanguageSource = $source; + }//end setRequestedLanguageSource() + + /** + * Get the source of the resolved preferred language. + * + * @return string One of `'query'`, `'header'`, `'default'`. + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-1 + */ + public function getRequestedLanguageSource(): string + { + return $this->requestedLanguageSource; + }//end getRequestedLanguageSource() + + /** + * Set the write-side target language for the active request. + * + * Populated by `LanguageMiddleware::beforeController` when the + * `X-Translation-Target-Language` header is present on a + * POST/PUT/PATCH. Read by `TranslationHandler::normalizeTranslationsForSave`. + * + * @param string|null $language BCP-47 tag, or null to clear. + * + * @return void + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-2 + */ + public function setTargetLanguage(?string $language): void + { + if ($language === null || trim($language) === '') { + $this->targetLanguage = null; + return; + } + + $this->targetLanguage = trim($language); + }//end setTargetLanguage() + + /** + * Get the write-side target language for the active request. + * + * @return string|null BCP-47 tag, or null when not set. + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-2 + */ + public function getTargetLanguage(): ?string + { + return $this->targetLanguage; + }//end getTargetLanguage() + /** * Resolve the best matching language for a register. * diff --git a/lib/Service/Lifecycle/LifecycleActionExecutor.php b/lib/Service/Lifecycle/LifecycleActionExecutor.php new file mode 100644 index 0000000000..22c0bb9a12 --- /dev/null +++ b/lib/Service/Lifecycle/LifecycleActionExecutor.php @@ -0,0 +1,172 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Lifecycle; + +use Psr\Log\LoggerInterface; +use RuntimeException; + +/** + * Executes a transition's declared actions in order. + * + * The executor is deliberately fail-loud: a missing handler (via the registry) + * and an unparseable `condition` both throw, so a declared-but-dead action can + * never silently no-op. Handlers may self-mutate the payload (the executor + * threads the return value forward) or perform pure side effects (they return + * the payload unchanged). + * + * @spec openspec/specs/object-lifecycle/spec.md + */ +class LifecycleActionExecutor +{ + /** + * Constructor. + * + * @param LifecycleActionRegistry $registry Resolves action names to handler instances. + * @param LoggerInterface $logger Logger for execution diagnostics. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + public function __construct( + private readonly LifecycleActionRegistry $registry, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Run every declared action for a matched transition. + * + * @param array $actions The transition's declared `actions[]` list. + * @param array $objectData The object payload after the lifecycle field moved to its target. + * @param array $previousData The object payload before the transition. + * @param string $transition The matched transition (action) name — for diagnostics. + * + * @return array The object payload after all self-mutating actions applied. + * + * @throws RuntimeException When an action is malformed, its condition is unparseable, or its handler is missing. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + public function run(array $actions, array $objectData, array $previousData, string $transition): array + { + foreach ($actions as $envelope) { + if (is_array($envelope) === false) { + throw new RuntimeException( + sprintf('Lifecycle transition "%s" declares a malformed action (not an object).', $transition) + ); + } + + $actionName = (string) ($envelope['action'] ?? ''); + if ($actionName === '') { + throw new RuntimeException( + sprintf('Lifecycle transition "%s" declares an action without an "action" name.', $transition) + ); + } + + // Optional per-action condition — evaluated against the new + // (@self) and old (@previous) payloads. When present and false, + // the action is skipped; when unparseable, the executor throws + // (fail loud) rather than skipping silently. + $condition = ($envelope['condition'] ?? null); + if (is_string($condition) === true && $condition !== '') { + $holds = $this->evaluateCondition( + condition: $condition, + objectData: $objectData, + previousData: $previousData + ); + if ($holds === false) { + continue; + } + } + + $parameters = ($envelope['actionParameters'] ?? []); + if (is_array($parameters) === false) { + $parameters = []; + } + + $handler = $this->registry->resolve($actionName); + $objectData = $handler->execute( + objectData: $objectData, + previousData: $previousData, + parameters: $parameters, + actionName: $actionName + ); + + $this->logger->debug( + sprintf('[LifecycleActionExecutor] ran action "%s" on transition "%s".', $actionName, $transition) + ); + }//end foreach + + return $objectData; + }//end run() + + /** + * Evaluate a declared action `condition`. + * + * Supports the equality forms observed across the fleet's register.d: + * `@self. == ''`, `@self. != ''`, + * `@previous. == ''`, `@previous. != ''`. + * An unrecognised condition shape throws — a declared action must not be + * silently skipped because its condition could not be understood. + * + * @param string $condition The declared condition expression. + * @param array $objectData The new (@self) payload. + * @param array $previousData The old (@previous) payload. + * + * @return bool True when the condition holds. + * + * @throws RuntimeException When the condition syntax is not recognised. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function evaluateCondition(string $condition, array $objectData, array $previousData): bool + { + $pattern = '/^\s*@(self|previous)\.([A-Za-z0-9_]+)\s*(==|!=)\s*\'([^\']*)\'\s*$/'; + if (preg_match($pattern, $condition, $matches) !== 1) { + throw new RuntimeException( + sprintf('Lifecycle action condition "%s" is not a supported expression.', $condition) + ); + } + + [, $scope, $field, $operator, $literal] = $matches; + + $source = $objectData; + if ($scope === 'previous') { + $source = $previousData; + } + + $actual = (string) ($source[$field] ?? ''); + + if ($operator === '==') { + return ($actual === $literal); + } + + return ($actual !== $literal); + }//end evaluateCondition() +}//end class diff --git a/lib/Service/Lifecycle/LifecycleActionRegistry.php b/lib/Service/Lifecycle/LifecycleActionRegistry.php new file mode 100644 index 0000000000..4cde5a719c --- /dev/null +++ b/lib/Service/Lifecycle/LifecycleActionRegistry.php @@ -0,0 +1,166 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Lifecycle; + +use OCA\OpenRegister\Lifecycle\Action\SetFieldsAction; +use OCA\OpenRegister\Lifecycle\LifecycleActionInterface; +use OCP\IServerContainer; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use RuntimeException; + +/** + * Resolves action name → handler instance. + * + * A declared action that resolves to no handler is fail-loud: the executor lets + * the thrown `RuntimeException` propagate, which aborts the save and surfaces + * the misconfiguration instead of hiding a dead capability. This is the same + * missing-tag policy `LifecycleGuardRegistry` enforces for guards. + * + * Not declared `final`: LifecycleActionExecutorTest and LifecycleActionListenerTest + * double this class (they control handler resolution without a live container). + * Mirrors TransitionEngine's doubling rationale. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ +class LifecycleActionRegistry +{ + + /** + * Built-in action-name → handler-FQCN map. + * + * OpenRegister ships these; they are resolved through the OR container so + * their own dependencies are autowired. Fleet-declared but not-yet-built-in + * action names (e.g. `materialise-gl-transaction`, `audit-trail-append`, + * `emit-event`) are intentionally absent — an app that declares one MUST + * register a handler under that id, or resolution fails loudly. + * + * @var array> + */ + private const BUILTINS = [ + 'set-fields' => SetFieldsAction::class, + 'set-field' => SetFieldsAction::class, + ]; + + /** + * Per-request cache. + * + * @var array + */ + private array $cache = []; + + /** + * Constructor. + * + * @param ContainerInterface $container OR app container, used for built-ins + OR-internal handlers. + * @param IServerContainer $serverContainer NC server container, used as fallback for FQCN-tagged handlers in other apps. + * @param LoggerInterface $logger Logger for resolution diagnostics. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + public function __construct( + private readonly ContainerInterface $container, + private readonly IServerContainer $serverContainer, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Resolve a declared action name to its handler. + * + * @param string $actionName The declared `action` value (e.g. `set-fields`). + * + * @return LifecycleActionInterface + * + * @throws RuntimeException When the action resolves to no service, or the resolved service does not implement the interface. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + public function resolve(string $actionName): LifecycleActionInterface + { + if (isset($this->cache[$actionName]) === true) { + return $this->cache[$actionName]; + } + + // Built-in handlers resolve by their FQCN through the OR container so + // their constructor dependencies are autowired. + $lookup = ($actionName); + if (isset(self::BUILTINS[$actionName]) === true) { + $lookup = self::BUILTINS[$actionName]; + } + + $instance = null; + $errors = []; + // Try OR's app container first (covers built-ins + OR-internal + // handlers) and fall back to the injected server container (covers + // FQCN-based references to handlers in other apps that Nextcloud can + // autowire). The server container is injected via IServerContainer + // rather than the static \OC::$server accessor, which lib/ bans. + foreach ([$this->container, $this->serverContainer] as $candidate) { + try { + $instance = $candidate->get($lookup); + break; + } catch (\Throwable $e) { + $errors[] = $e->getMessage(); + } + } + + if ($instance === null) { + $this->logger->error( + sprintf('Lifecycle action "%s" could not be resolved: %s', $actionName, implode(' | ', $errors)) + ); + throw new RuntimeException( + message: sprintf( + 'Lifecycle action "%s" is declared but no handler is registered. ' + .'Register a service implementing %s under the id "%s", or remove the action from the schema.', + $actionName, + LifecycleActionInterface::class, + $actionName + ) + ); + } + + if (($instance instanceof LifecycleActionInterface) === false) { + throw new RuntimeException( + sprintf( + 'Service resolved for lifecycle action "%s" does not implement %s.', + $actionName, + LifecycleActionInterface::class + ) + ); + } + + $this->cache[$actionName] = $instance; + return $instance; + }//end resolve() +}//end class diff --git a/lib/Service/Lifecycle/LifecycleAnnotationValidator.php b/lib/Service/Lifecycle/LifecycleAnnotationValidator.php index d7d15a5eb6..48e671768d 100644 --- a/lib/Service/Lifecycle/LifecycleAnnotationValidator.php +++ b/lib/Service/Lifecycle/LifecycleAnnotationValidator.php @@ -47,7 +47,8 @@ final class LifecycleAnnotationValidator * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md */ public function validate(array $schema): array { @@ -58,6 +59,23 @@ public function validate(array $schema): array $annotation = $schema['x-openregister-lifecycle']; $errors = []; + // `property` is an additive alias for `field`; normalise it up-front so + // the rest of validation (and the runtime listener) sees a single key. + if (isset($annotation['field']) === false && isset($annotation['property']) === true) { + $annotation['field'] = $annotation['property']; + } + + // Graph mode: when a non-empty `graph` block is declared, the lifecycle + // field is a `$ref` with no enum, so shape-check the graph block instead + // of the static `transitions`/enum contract. Static-only schemas fall + // straight through to the unchanged validation below (no regression). + if (isset($annotation['graph']) === true + && is_array($annotation['graph']) === true + && $annotation['graph'] !== [] + ) { + return $this->validateGraphMode(annotation: $annotation, schema: $schema); + } + // Required top-level fields. foreach (['field', 'initial', 'transitions'] as $required) { if (isset($annotation[$required]) === false) { @@ -146,8 +164,13 @@ public function validate(array $schema): array continue; } - // From: required, array of states all in the enum. - $from = ($spec['from'] ?? null); + // From: required, a single state string or an array of states, all + // in the enum. A string is coerced to a one-element list. + $from = ($spec['from'] ?? null); + if (is_string($from) === true && $from !== '') { + $from = [$from]; + } + $fromOk = (is_array($from) === true && count($from) > 0); if ($fromOk === false) { $errors[] = [ @@ -207,8 +230,192 @@ public function validate(array $schema): array ]; } } + + // Optional `authorization` — declarative per-transition group/role + // gate (Engine 1). When present it must be a non-empty list whose + // entries are either NC group id strings or `{ "role": "" }` + // objects. Shape-check only; group existence is a runtime concern. + if (isset($spec['authorization']) === true) { + $authError = $this->validateTransitionAuthorization( + authorization: $spec['authorization'], + action: (string) $action + ); + if ($authError !== null) { + $errors[] = $authError; + } + } }//end foreach return $errors; }//end validate() + + /** + * Validate a graph-mode `x-openregister-lifecycle` annotation. + * + * Shape-checks the `graph` block (`schema`, `parentField`, `parentFrom`, + * `orderField`, `finalField` as non-empty strings; `allowedMoves` one of + * `forward`|`adjacent`|`any`), keeps `field` required but relaxes the + * `enum`/`type:string` constraint (a `$ref` field has no enum), and accepts + * either the literal-string or object-form `initial`. Sibling schemas and + * parent objects are NOT resolved — existence is a runtime concern. + * + * @param array $annotation The normalised annotation block. + * @param array $schema Full schema definition. + * + * @return array List of errors (empty = valid). + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each check maps to one distinct, irreducible graph-shape rule. + * + * @spec openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md + */ + private function validateGraphMode(array $annotation, array $schema): array + { + $errors = []; + + // `field` remains required and non-empty, but the enum/type:string + // constraint is relaxed for a `$ref` lifecycle field. + $field = ($annotation['field'] ?? null); + $fieldValid = (is_string($field) === true && $field !== ''); + if ($fieldValid === false) { + $errors[] = [ + 'code' => 'lifecycle-missing-key', + 'message' => 'x-openregister-lifecycle is missing required key "field".', + ]; + } + + if ($fieldValid === true) { + $properties = ($schema['properties'] ?? []); + if (is_array($properties) === true && isset($properties[$field]) === false) { + $errors[] = [ + 'code' => 'lifecycle-field-missing', + 'message' => sprintf('x-openregister-lifecycle.field "%s" is not declared in `properties`.', $field), + ]; + } + } + + // `initial` (optional): accept the literal-string form or the object + // form `{ from, field }` with both keys non-empty strings. + if (isset($annotation['initial']) === true) { + $initialError = $this->validateInitialForm(initial: $annotation['initial']); + if ($initialError !== null) { + $errors[] = $initialError; + } + } + + // Graph block: required non-empty string keys. + $graph = $annotation['graph']; + foreach (['schema', 'parentField', 'parentFrom', 'orderField', 'finalField'] as $key) { + $value = ($graph[$key] ?? null); + if (is_string($value) === false || $value === '') { + $errors[] = [ + 'code' => 'lifecycle-graph-missing-key', + 'message' => sprintf('x-openregister-lifecycle.graph is missing required string key "%s".', $key), + ]; + } + } + + // `allowedMoves`: required, one of forward|adjacent|any. + $allowed = ($graph['allowedMoves'] ?? null); + if (in_array($allowed, ['forward', 'adjacent', 'any'], true) === false) { + $shown = gettype($allowed); + if (is_scalar($allowed) === true) { + $shown = (string) $allowed; + } + + $errors[] = [ + 'code' => 'lifecycle-graph-allowedmoves-invalid', + 'message' => sprintf( + 'x-openregister-lifecycle.graph.allowedMoves "%s" must be one of forward|adjacent|any.', + $shown + ), + ]; + } + + return $errors; + }//end validateGraphMode() + + /** + * Shape-check the `initial` value in its two accepted forms. + * + * Valid: a non-empty string (literal form) or an object with non-empty + * string `from` and `field` keys (object form). Returns a single structured + * error on violation, or null when valid. + * + * @param mixed $initial The raw `initial` value off the annotation. + * + * @return array{code: string, message: string}|null Error, or null when valid. + */ + private function validateInitialForm(mixed $initial): ?array + { + if (is_string($initial) === true) { + return null; + } + + if (is_array($initial) === true) { + $from = ($initial['from'] ?? null); + $field = ($initial['field'] ?? null); + if (is_string($from) === false || $from === '' + || is_string($field) === false || $field === '' + ) { + return [ + 'code' => 'lifecycle-initial-malformed', + 'message' => 'x-openregister-lifecycle.initial object form must declare non-empty "from" and "field" strings.', + ]; + } + + return null; + } + + return [ + 'code' => 'lifecycle-initial-malformed', + 'message' => 'x-openregister-lifecycle.initial must be a string or an object with "from" and "field".', + ]; + }//end validateInitialForm() + + /** + * Shape-check a transition's optional `authorization` list. + * + * Valid: a non-empty array whose entries are either non-empty NC group id + * strings or `{ "role": "" }` objects. Returns a single + * structured error on the first violation, or null when valid. + * + * @param mixed $authorization The raw `authorization` value off the transition spec. + * @param string $action Transition action name, for the error message. + * + * @return array{code: string, message: string}|null Error, or null when valid. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each return is a distinct, irreducible shape violation. + */ + private function validateTransitionAuthorization(mixed $authorization, string $action): ?array + { + if (is_array($authorization) === false || $authorization === []) { + return [ + 'code' => 'lifecycle-authorization-malformed', + 'message' => sprintf( + 'Transition "%s" `authorization` must be a non-empty array of group ids or {role} objects.', + $action + ), + ]; + } + + foreach ($authorization as $entry) { + $isGroupString = (is_string($entry) === true && $entry !== ''); + $isRoleObject = (is_array($entry) === true + && isset($entry['role']) === true + && is_string($entry['role']) === true + && $entry['role'] !== ''); + + if ($isGroupString === false && $isRoleObject === false) { + return [ + 'code' => 'lifecycle-authorization-entry-malformed', + 'message' => sprintf( + 'Transition "%s" `authorization` entries must be non-empty group id strings or {"role":""} objects.', + $action + ), + ]; + } + } + + return null; + }//end validateTransitionAuthorization() }//end class diff --git a/lib/Service/Lifecycle/LifecycleGuardRegistry.php b/lib/Service/Lifecycle/LifecycleGuardRegistry.php index f051e857ec..db9f6cc951 100644 --- a/lib/Service/Lifecycle/LifecycleGuardRegistry.php +++ b/lib/Service/Lifecycle/LifecycleGuardRegistry.php @@ -60,7 +60,7 @@ final class LifecycleGuardRegistry * @param IServerContainer $serverContainer NC server container used as fallback for FQCN-tagged guards (F06). * @param LoggerInterface $logger Logger for guard resolution diagnostics. * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly ContainerInterface $container, @@ -78,7 +78,7 @@ public function __construct( * * @throws RuntimeException When the tag is not registered or the resolved service does not implement the interface. * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function resolve(string $tag): LifecycleGuardInterface { diff --git a/lib/Service/Lifecycle/TransitionEngine.php b/lib/Service/Lifecycle/TransitionEngine.php index 9ba9421dcd..a440fe1336 100644 --- a/lib/Service/Lifecycle/TransitionEngine.php +++ b/lib/Service/Lifecycle/TransitionEngine.php @@ -58,7 +58,7 @@ class TransitionEngine * @param IUserSession $userSession Current user session, for actor attribution. * @param PermissionHandler $permissionHandler RBAC verdict on the object's `update`/`read` actions (F03). * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly ObjectService $objectService, @@ -81,7 +81,10 @@ public function __construct( * the action is not allowed from the current * state, or the underlying save is rejected. * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-2 + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Linear resolve→guard→mutate→save flow; splitting would obscure the transition contract. + * + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md */ public function transition(string $objectId, string $action): ObjectEntity { @@ -129,9 +132,24 @@ public function transition(string $objectId, string $action): ObjectEntity ); } - $field = (string) ($annotation['field'] ?? ''); + $field = (string) ($annotation['field'] ?? ($annotation['property'] ?? '')); $transitions = (array) ($annotation['transitions'] ?? []); + // Static transitions take precedence. Graph mode applies only when no + // non-empty static `transitions` map is declared (design: mode + // selection & precedence — zero regression for static schemas). + if ($transitions === []) { + $graph = (array) ($annotation['graph'] ?? []); + if ($graph !== []) { + return $this->applyGraphTransition( + object: $object, + graph: $graph, + field: $field, + action: $action + ); + } + } + if (isset($transitions[$action]) === false || is_array($transitions[$action]) === false) { throw new RuntimeException( sprintf('Transition "%s" is not declared on this schema.', $action) @@ -159,14 +177,22 @@ public function transition(string $objectId, string $action): ObjectEntity // the transition on save; the guard (if any) will run there too. $data[$field] = $targetState; + // Snapshot the session user at the transition boundary and forward it + // explicitly to the save path, so the @self.folder check uses the SAME + // identity that authorised this transition. Note this does NOT rescue + // the null-session case: with no session user $actingUser is null and + // the downstream check default-denies — as intended (PR #1431 4th-pass). + $actingUser = $this->userSession->getUser(); + $saved = $this->objectService->saveObject( object: $data, register: $object->getRegister(), schema: $object->getSchema(), - uuid: $object->getUuid() + uuid: $object->getUuid(), + currentUser: $actingUser ); - $userId = $this->userSession->getUser()?->getUID(); + $userId = $actingUser?->getUID(); $this->eventDispatcher->dispatchTyped( new ObjectTransitionedEvent( @@ -190,10 +216,15 @@ public function transition(string $objectId, string $action): ObjectEntity * * @return array * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) RBAC check + missing-object guard + annotation-absent guard + per-transition from/requires/description checks each add one branch; none can be removed without losing safety or fidelity. - * @SuppressWarnings(PHPMD.NPathComplexity) RBAC check + missing-object guard + annotation-absent guard + per-transition from/requires/description checks each add one branch; none can be removed without losing safety or fidelity. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) RBAC check + missing-object guard + annotation-absent + * guard + per-transition from/requires/description checks each add one branch; none can be removed + * without losing safety or fidelity. + * @SuppressWarnings(PHPMD.NPathComplexity) RBAC check + missing-object guard + annotation-absent + * guard + per-transition from/requires/description checks each add one branch; none can be removed + * without losing safety or fidelity. * - * @spec openspec/changes/retrofit-2026-05-24-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md */ public function availableActions(string $objectId): array { @@ -233,8 +264,19 @@ public function availableActions(string $objectId): array return []; } - $field = (string) ($annotation['field'] ?? ''); - $transitions = (array) ($annotation['transitions'] ?? []); + $field = (string) ($annotation['field'] ?? ($annotation['property'] ?? '')); + $transitions = (array) ($annotation['transitions'] ?? []); + + // Static transitions take precedence. When no non-empty static map is + // declared but a `graph` block is, derive actions from FK-scoped + // siblings at runtime (design: mode selection & precedence). + if ($transitions === []) { + $graph = (array) ($annotation['graph'] ?? []); + if ($graph !== []) { + return $this->deriveGraphActions(object: $object, graph: $graph, field: $field); + } + } + $data = $object->getObject() ?? []; $currentValue = (string) ($data[$field] ?? ''); @@ -270,6 +312,269 @@ public function availableActions(string $objectId): array return $available; }//end availableActions() + /** + * Derive the candidate transitions for a graph-mode object. + * + * Reads the parent reference off the object, fetches the ordered sibling + * set of the related schema scoped to that parent (through the standard + * ObjectService read path, so RBAC + multitenancy apply), locates the + * object's current state, and returns the candidate targets permitted by + * `allowedMoves`. Terminal states (`finalField` true) are sinks unless + * `allowedMoves` is `any`. An orphaned current value (not among the + * siblings) recovers to the first sibling. The SAME method backs both + * `availableActions()` and the validation inside `transition()`, so a + * client can only apply a `move-to-` the graph currently offers. + * + * @param ObjectEntity $object The transitioning object. + * @param array $graph The `graph` block off the annotation. + * @param string $field The lifecycle field name on the object. + * + * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) FK read + sibling fetch + current-state + * location + per-move-policy branching are each irreducible steps of the derivation. + * @SuppressWarnings(PHPMD.NPathComplexity) FK read + sibling fetch + current-state + * location + per-move-policy branching are each irreducible steps of the derivation. + * + * @spec openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md + */ + private function deriveGraphActions(ObjectEntity $object, array $graph, string $field): array + { + $parentFromKey = (string) ($graph['parentFrom'] ?? ''); + $data = $object->getObject() ?? []; + $parentValue = (string) ($data[$parentFromKey] ?? ''); + + // No parent reference → nothing to scope the graph to. + if ($parentValue === '') { + return []; + } + + $siblings = $this->fetchOrderedSiblings(graph: $graph, parentValue: $parentValue); + if ($siblings === []) { + return []; + } + + $finalField = (string) ($graph['finalField'] ?? ''); + $allowed = (string) ($graph['allowedMoves'] ?? ''); + + $currentUuid = (string) ($data[$field] ?? ''); + $currentIndex = null; + foreach ($siblings as $i => $sibling) { + if ((string) $sibling->getUuid() === $currentUuid && $currentUuid !== '') { + $currentIndex = $i; + break; + } + } + + // Orphaned / unset current value → recover-to-start: offer the first + // sibling only (design decision, Ruben 2026-07-08). + if ($currentIndex === null) { + return [$this->buildGraphAction(sibling: $siblings[0])]; + } + + // Terminal lockout: a final state is a sink for forward/adjacent. + $currentData = $siblings[$currentIndex]->getObject() ?? []; + $currentFinal = (bool) ($currentData[$finalField] ?? false); + if ($currentFinal === true && $allowed !== 'any') { + return []; + } + + $targets = []; + switch ($allowed) { + case 'forward': + if (isset($siblings[($currentIndex + 1)]) === true) { + $targets[] = $siblings[($currentIndex + 1)]; + } + break; + case 'adjacent': + if ($currentIndex > 0 && isset($siblings[($currentIndex - 1)]) === true) { + $targets[] = $siblings[($currentIndex - 1)]; + } + + if (isset($siblings[($currentIndex + 1)]) === true) { + $targets[] = $siblings[($currentIndex + 1)]; + } + break; + case 'any': + foreach ($siblings as $i => $sibling) { + if ($i !== $currentIndex) { + $targets[] = $sibling; + } + } + break; + default: + return []; + }//end switch + + $actions = []; + foreach ($targets as $target) { + $actions[] = $this->buildGraphAction(sibling: $target); + } + + return $actions; + }//end deriveGraphActions() + + /** + * Fetch the ordered sibling set for a graph derivation. + * + * Uses `ObjectService::findAll` (the standard read path) filtered by the + * sibling schema and the parent FK, then sorts ascending by `orderField` + * with a deterministic UUID tiebreak so derivation never depends on + * database row order. + * + * @param array $graph The `graph` block off the annotation. + * @param string $parentValue The resolved parent reference. + * + * @return array The ordered sibling entities (0-indexed, re-keyed). + * + * @spec openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md + */ + private function fetchOrderedSiblings(array $graph, string $parentValue): array + { + $schemaSlug = (string) ($graph['schema'] ?? ''); + $parentField = (string) ($graph['parentField'] ?? ''); + $orderField = (string) ($graph['orderField'] ?? ''); + if ($schemaSlug === '' || $parentField === '') { + return []; + } + + $siblings = $this->objectService->findAll( + config: [ + 'filters' => [ + 'schema' => $schemaSlug, + $parentField => $parentValue, + ], + 'sort' => [$orderField => 'ASC'], + ] + ); + + // Keep only ObjectEntity results and re-index. + $entities = []; + foreach ($siblings as $sibling) { + if ($sibling instanceof ObjectEntity) { + $entities[] = $sibling; + } + } + + // Deterministic sort (ascending order, UUID tiebreak) — do not rely on + // the storage layer's ordering. + usort( + $entities, + function (ObjectEntity $a, ObjectEntity $b) use ($orderField): int { + $aOrder = (float) (($a->getObject() ?? [])[$orderField] ?? 0); + $bOrder = (float) (($b->getObject() ?? [])[$orderField] ?? 0); + if ($aOrder === $bOrder) { + return strcmp((string) $a->getUuid(), (string) $b->getUuid()); + } + + return ($aOrder <=> $bOrder); + } + ); + + return $entities; + }//end fetchOrderedSiblings() + + /** + * Build a derived graph action envelope for a target sibling. + * + * @param ObjectEntity $sibling The target sibling to move to. + * + * @return array{action: string, to: string, label: string, requires: ?string, description: ?string} + * + * @spec openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md + */ + private function buildGraphAction(ObjectEntity $sibling): array + { + $uuid = (string) $sibling->getUuid(); + $data = $sibling->getObject() ?? []; + $name = $sibling->getName(); + if ($name === null || trim((string) $name) === '') { + $name = (string) ($data['name'] ?? ($data['title'] ?? $uuid)); + } + + return [ + 'action' => 'move-to-'.$uuid, + 'to' => $uuid, + 'label' => (string) $name, + 'requires' => null, + 'description' => null, + ]; + }//end buildGraphAction() + + /** + * Apply a graph-mode transition. + * + * Re-runs the SAME derivation as `availableActions()`, accepts the posted + * action only when it is a current candidate, mutates the lifecycle field + * to the target UUID, saves through the unchanged ObjectService path, and + * dispatches `ObjectTransitionedEvent`. Rejection never mutates the object. + * + * @param ObjectEntity $object The transitioning object. + * @param array $graph The `graph` block off the annotation. + * @param string $field The lifecycle field name on the object. + * @param string $action The requested `move-to-` action. + * + * @return ObjectEntity The saved object after the transition. + * + * @throws RuntimeException When the action is not a current candidate. + * + * @spec openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md + */ + private function applyGraphTransition( + ObjectEntity $object, + array $graph, + string $field, + string $action + ): ObjectEntity { + $candidates = $this->deriveGraphActions(object: $object, graph: $graph, field: $field); + + $match = null; + foreach ($candidates as $candidate) { + if ($candidate['action'] === $action) { + $match = $candidate; + break; + } + } + + if ($match === null) { + throw new RuntimeException( + sprintf('Transition "%s" is not allowed from the current state.', $action) + ); + } + + $targetState = (string) $match['to']; + $data = $object->getObject() ?? []; + $from = (string) ($data[$field] ?? ''); + + $data[$field] = $targetState; + + // Snapshot the session user at the transition boundary and forward it + // explicitly to the save path, mirroring the static-mode contract. + $actingUser = $this->userSession->getUser(); + + $saved = $this->objectService->saveObject( + object: $data, + register: $object->getRegister(), + schema: $object->getSchema(), + uuid: $object->getUuid(), + currentUser: $actingUser + ); + + $this->eventDispatcher->dispatchTyped( + new ObjectTransitionedEvent( + object: $saved, + action: $action, + from: $from, + to: $targetState, + userId: $actingUser?->getUID(), + register: (string) $object->getRegister(), + schema: (string) $object->getSchema() + ) + ); + + return $saved; + }//end applyGraphTransition() + /** * Load the schema referenced by an object, returning null on failure. * diff --git a/lib/Service/LinkedEntityService.php b/lib/Service/LinkedEntityService.php index 975bc61222..a39fc4ab79 100644 --- a/lib/Service/LinkedEntityService.php +++ b/lib/Service/LinkedEntityService.php @@ -6,19 +6,19 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-42 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-44 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-45 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-48 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-49 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-magic-tables + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables + * @spec openspec/specs/linked-entity-types/spec.md#requirement-reverse-lookup-across-tables + * @spec openspec/specs/linked-entity-types/spec.md#requirement-remove-link-entities-and-mappers + * @spec openspec/specs/linked-entity-types/spec.md + * @spec openspec/specs/linked-entity-types/spec.md */ namespace OCA\OpenRegister\Service; @@ -32,6 +32,8 @@ use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Db\Organisation; use OCA\OpenRegister\Db\OrganisationMapper; +use OCA\OpenRegister\Service\Integration\IntegrationRegistry; +use OCA\OpenRegister\Service\Object\PermissionHandler; use OCP\DB\Exception as DbException; use Psr\Log\LoggerInterface; @@ -51,33 +53,6 @@ */ class LinkedEntityService { - /** - * Valid linked entity types and their column names. - * - * @deprecated since pluggable-integration-registry — the registry - * (`IntegrationRegistry`) is the new source of truth for what - * integrations exist; this map is retained as an implementation - * detail of the magic-column built-in providers until every - * Wave-1 leaf has shipped. The follow-up change - * `cleanup-linked-entity-type-map` removes it entirely once the - * built-in providers (FilesProvider / NotesProvider / TagsProvider / - * AuditTrailProvider / etc.) own their own column resolution. - * - * @see OCA\OpenRegister\Service\Integration\IntegrationRegistry - * - * @spec openspec/changes/pluggable-integration-registry/tasks.md#task-9 - */ - private const TYPE_COLUMN_MAP = [ - 'mail' => 'mail', - 'contacts' => 'contacts', - 'notes' => 'notes', - 'todos' => 'todos', - 'calendar' => 'calendar', - 'talk' => 'talk', - 'deck' => 'deck', - 'files' => 'files', - ]; - /** * Maximum number of magic tables to scan for reverse lookups (circuit breaker). */ @@ -86,21 +61,68 @@ class LinkedEntityService /** * Constructor for LinkedEntityService. * - * @param MagicMapper $magicMapper Magic mapper for object operations - * @param SchemaMapper $schemaMapper Schema mapper - * @param RegisterMapper $registerMapper Register mapper - * @param OrganisationMapper $organisationMapper Organisation mapper - * @param LoggerInterface $logger Logger + * Per `cleanup-linked-entity-type-map`, the legacy linked-type to + * column-name map constant was removed; validation flows through + * `IntegrationRegistry::isValidIntegrationId()` plus a small + * legacy-id allow-list (see `validateType()`). The column name for + * a known linked-type is the type id itself — historically the + * removed map was a verbatim identity for every entry. + * + * @param MagicMapper $magicMapper Magic mapper for object operations + * @param SchemaMapper $schemaMapper Schema mapper + * @param RegisterMapper $registerMapper Register mapper + * @param OrganisationMapper $organisationMapper Organisation mapper + * @param IntegrationRegistry $integrationRegistry Integration registry (authoritative + * type-id source, post `cleanup-linked-entity-type-map`) + * @param LoggerInterface $logger Logger + * @param PermissionHandler $permissionHandler RBAC handler for write-permission checks (SEC-CTRL-4) + * @param DeepLinkRegistryService $deepLinkRegistry Deep-link registry service for cross-app deep links */ public function __construct( private readonly MagicMapper $magicMapper, private readonly SchemaMapper $schemaMapper, private readonly RegisterMapper $registerMapper, private readonly OrganisationMapper $organisationMapper, + private readonly IntegrationRegistry $integrationRegistry, private readonly LoggerInterface $logger, + private readonly PermissionHandler $permissionHandler, + private readonly DeepLinkRegistryService $deepLinkRegistry, ) { }//end __construct() + /** + * Assert the current user may write (update) the given object before mutating + * its linked-entity columns. + * + * SEC-CTRL-4: addLink()/removeLink() previously only ran the read check inside + * MagicMapper::find(), then called update() with no write-permission gate — so a + * read-only user could mutate link columns. This resolves the object's schema and + * runs the canonical `update` RBAC check, throwing NotAuthorizedException (403) on + * denial. + * + * @param ObjectEntity $object The object about to be updated. + * + * @throws Exception When the schema cannot be resolved or the user lacks write permission. + * + * @return void + */ + private function assertCanWriteObject(ObjectEntity $object): void + { + $schemaId = $object->getSchema(); + if ($schemaId === null) { + throw new Exception('Cannot resolve schema for linked-entity write permission check.'); + } + + $schema = $this->schemaMapper->find($schemaId); + + $this->permissionHandler->checkPermission( + schema: $schema, + action: 'update', + objectOwner: $object->getOwner(), + object: $object + ); + }//end assertCanWriteObject() + /** * Add a linked entity ID to an object's metadata column. * @@ -112,13 +134,13 @@ public function __construct( * * @return array The updated linked IDs array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-48 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables + * @spec openspec/specs/linked-entity-types/spec.md */ public function addLink(string $objectUuid, string $type, string $entityId): array { $this->validateType(type: $type); - $columnName = self::TYPE_COLUMN_MAP[$type]; + $columnName = $type; $object = $this->magicMapper->find($objectUuid); $getter = 'get'.ucfirst($columnName); @@ -127,6 +149,8 @@ public function addLink(string $objectUuid, string $type, string $entityId): arr // Idempotent: don't add if already present. if (in_array($entityId, $existingIds, true) === false) { + // SEC-CTRL-4: enforce write (update) permission before mutating the object. + $this->assertCanWriteObject(object: $object); $existingIds[] = $entityId; $object->$setter($existingIds); $this->magicMapper->update($object); @@ -146,13 +170,13 @@ public function addLink(string $objectUuid, string $type, string $entityId): arr * * @return array The updated linked IDs array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-45 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-48 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-remove-link-entities-and-mappers + * @spec openspec/specs/linked-entity-types/spec.md */ public function removeLink(string $objectUuid, string $type, string $entityId): array { $this->validateType(type: $type); - $columnName = self::TYPE_COLUMN_MAP[$type]; + $columnName = $type; $object = $this->magicMapper->find($objectUuid); $getter = 'get'.ucfirst($columnName); @@ -168,6 +192,8 @@ function ($id) use ($entityId) { ) ); + // SEC-CTRL-4: enforce write (update) permission before mutating the object. + $this->assertCanWriteObject(object: $object); $object->$setter($existingIds); $this->magicMapper->update($object); @@ -185,12 +211,12 @@ function ($id) use ($entityId) { * * @return array The updated linked IDs array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables */ public function addLinkToRegister(string $registerUuid, string $type, string $entityId): array { $this->validateType(type: $type); - $columnName = self::TYPE_COLUMN_MAP[$type]; + $columnName = $type; $registers = $this->registerMapper->findAll(filters: ['uuid' => $registerUuid]); if (empty($registers) === true) { @@ -222,12 +248,12 @@ public function addLinkToRegister(string $registerUuid, string $type, string $en * * @return array The updated linked IDs array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables */ public function addLinkToSchema(string $schemaUuid, string $type, string $entityId): array { $this->validateType(type: $type); - $columnName = self::TYPE_COLUMN_MAP[$type]; + $columnName = $type; $schemas = $this->schemaMapper->findAll(filters: ['uuid' => $schemaUuid]); if (empty($schemas) === true) { @@ -261,13 +287,13 @@ public function addLinkToSchema(string $schemaUuid, string $type, string $entity * * @return array Array of result objects with entityType, uuid, name, etc. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-44 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-49 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-reverse-lookup-across-tables + * @spec openspec/specs/linked-entity-types/spec.md */ public function reverseLookup(string $type, string $entityId): array { $this->validateType(type: $type); - $columnName = self::TYPE_COLUMN_MAP[$type]; + $columnName = $type; $results = []; // 1. Scan magic tables (objects). @@ -288,7 +314,7 @@ public function reverseLookup(string $type, string $entityId): array * * @return array Array of matching results * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-42 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-magic-tables */ private function scanMagicTables(string $type, string $columnName, string $entityId): array { @@ -328,15 +354,33 @@ private function scanMagicTables(string $type, string $columnName, string $entit ); foreach ($objects as $object) { + $registerId = (int) $object->getRegister(); + $schemaId = (int) $schema->getId(); + + // Prefer the owning app's detail route (registered via the + // deep-link registry by leaf apps like pipelinq/procest); + // null falls the frontend back to OpenRegister's own page. + $deepLink = $this->deepLinkRegistry->resolveUrl( + registerId: $registerId, + schemaId: $schemaId, + objectData: [ + 'uuid' => $object->getUuid(), + 'register' => $registerId, + 'schema' => $schemaId, + ] + ); + $results[] = [ 'entityType' => 'object', 'uuid' => $object->getUuid(), 'name' => $object->getName(), 'schema' => $schema->getTitle(), - 'schemaId' => $schema->getId(), - 'register' => $object->getRegister(), + 'schemaId' => $schemaId, + 'schemaIcon' => $schema->getIcon(), + 'register' => $registerId, + 'url' => $deepLink, ]; - } + }//end foreach } catch (Exception $e) { $this->logger->warning( '[LinkedEntityService] Error scanning magic table', @@ -358,7 +402,7 @@ private function scanMagicTables(string $type, string $columnName, string $entit * * @return array Array of matching results * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -435,20 +479,67 @@ private function scanEntityTables(string $columnName, string $entityId): array /** * Validate that the given type is a valid linked entity type. * + * Per `cleanup-linked-entity-type-map`, validation flows through: + * + * 1. `IntegrationRegistry::isValidIntegrationId($type)` — the + * authoritative source. + * 2. A small legacy-id allow-list (`legacyLinkedTypeIds()`) so + * pre-cleanup callers passing ids like `mail` (now `email` in + * the registry) or `todos` (now `tasks`) continue to work + * until consumers migrate. + * * @param string $type The type to validate * * @throws Exception If the type is invalid * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables + * @spec openspec/specs/cleanup-linked-entity-type-map/spec.md "Registry-Driven Behaviour Unchanged" */ private function validateType(string $type): void { - if (isset(self::TYPE_COLUMN_MAP[$type]) === false) { - throw new Exception( - "Invalid linked entity type '$type'. Valid types: ".implode(', ', array_keys(self::TYPE_COLUMN_MAP)) - ); + if ($this->integrationRegistry->isValidIntegrationId($type) === true) { + return; } + + $legacy = self::legacyLinkedTypeIds(); + if (in_array($type, $legacy, true) === true) { + return; + } + + $registered = $this->integrationRegistry->listIds(); + $combined = array_unique(array_merge($legacy, $registered)); + sort($combined); + throw new Exception( + "Invalid linked entity type '$type'. Valid types: ".implode(', ', $combined) + ); }//end validateType() + + /** + * Legacy linked-type id allow-list — internal implementation detail. + * + * Mirrors `Schema::legacyLinkedTypeIds()` (the cleanup also + * removed `Schema`'s public linked-types constant). Kept private + * and method-form so the values aren't exposed as a public symbol; + * new linked-types MUST be added by registering an + * `IntegrationProvider`, not by extending this list. + * + * @return array + * + * @spec openspec/specs/cleanup-linked-entity-type-map/spec.md "Constants Removed" + */ + private static function legacyLinkedTypeIds(): array + { + return [ + 'files', + 'mail', + 'contacts', + 'notes', + 'todos', + 'calendar', + 'talk', + 'deck', + ]; + }//end legacyLinkedTypeIds() }//end class diff --git a/lib/Service/ManifestService.php b/lib/Service/ManifestService.php index 7dda0976ca..93985e2dfd 100644 --- a/lib/Service/ManifestService.php +++ b/lib/Service/ManifestService.php @@ -316,14 +316,24 @@ private function buildSelfMeta(ObjectEntity $profile): array $created = $profile->getCreated(); $updated = $profile->getUpdated(); + $createdStr = null; + if ($created !== null) { + $createdStr = $created->format(DateTimeInterface::ATOM); + } + + $updatedStr = null; + if ($updated !== null) { + $updatedStr = $updated->format(DateTimeInterface::ATOM); + } + return [ 'id' => $profile->getUuid(), 'uuid' => $profile->getUuid(), 'register' => $profile->getRegister(), 'schema' => $profile->getSchema(), 'owner' => $profile->getOwner(), - 'created' => $created !== null ? $created->format(DateTimeInterface::ATOM) : null, - 'updated' => $updated !== null ? $updated->format(DateTimeInterface::ATOM) : null, + 'created' => $createdStr, + 'updated' => $updatedStr, ]; }//end buildSelfMeta() diff --git a/lib/Service/MapLinkService.php b/lib/Service/MapLinkService.php index 62094bc4e6..f9e3bfeeb3 100644 --- a/lib/Service/MapLinkService.php +++ b/lib/Service/MapLinkService.php @@ -315,7 +315,7 @@ public function getLinkedPois(string $objectUuid): array $results = []; foreach ($links as $link) { $row = $link->jsonSerialize(); - $row['url'] = $this->poiDeepLink(favoriteId: (int) ($row['favoriteId'] ?? 0)); + $row['url'] = $this->poiDeepLink(lat: (float) ($row['lat'] ?? 0), lng: (float) ($row['lng'] ?? 0)); $results[] = $row; } @@ -399,7 +399,7 @@ private function pickerRowFromFavorite(array $favorite, ?string $needle): ?array 'category' => $category, 'lat' => (float) ($favorite['lat'] ?? 0), 'lng' => (float) ($favorite['lng'] ?? 0), - 'url' => $this->poiDeepLink(favoriteId: $favoriteId), + 'url' => $this->poiDeepLink(lat: (float) ($favorite['lat'] ?? 0), lng: (float) ($favorite['lng'] ?? 0)), ]; }//end pickerRowFromFavorite() @@ -451,12 +451,17 @@ private function fetchFavoriteInfo(int $favoriteId, string $uid): ?array /** * Build the NC Maps deep link for a favorite. * - * @param int $favoriteId The favorite id. + * NC Maps is a single Leaflet view; its native hash centres the map via + * `#map={zoom}/{lat}/{lng}` (verified live — the app keeps that hash). There + * is no per-favorite route, so we centre on the favorite's coordinates. + * + * @param float $lat The favorite latitude. + * @param float $lng The favorite longitude. * * @return string */ - private function poiDeepLink(int $favoriteId): string + private function poiDeepLink(float $lat, float $lng): string { - return $this->urlGenerator->linkToRoute('maps.page.index').'#/m='.$favoriteId; + return $this->urlGenerator->linkToRoute('maps.page.index').'#map=16/'.$lat.'/'.$lng; }//end poiDeepLink() }//end class diff --git a/lib/Service/MappingService.php b/lib/Service/MappingService.php index e3c16b90a9..8c60d8079e 100644 --- a/lib/Service/MappingService.php +++ b/lib/Service/MappingService.php @@ -36,7 +36,9 @@ use OCP\ICache; use Psr\Log\LoggerInterface; use Twig\Environment; +use Twig\Extension\SandboxExtension; use Twig\Loader\ArrayLoader; +use Twig\Sandbox\SecurityPolicy; use Twig\TemplateWrapper; /** @@ -107,8 +109,11 @@ public function __construct( ICacheFactory $cacheFactory, private readonly LoggerInterface $logger ) { - $loader = new ArrayLoader([]); - $this->twig = new Environment($loader); + $loader = new ArrayLoader([]); + // Autoescape disabled — mappings transform data (not HTML), and the + // sandbox SecurityPolicy below does not allow the `escape` filter that + // autoescaping injects into every output expression. + $this->twig = new Environment($loader, ['autoescape' => false]); $this->twig->addExtension(new MappingExtension()); $this->twig->addRuntimeLoader( new MappingRuntimeLoader( @@ -117,6 +122,58 @@ public function __construct( ) ); + // SSTI hardening (SEC-SVC-3): user-authored mapping templates are + // compiled and rendered here, so they MUST run inside a Twig sandbox. + // Only the tags, filters and functions actually used by mappings are + // allowlisted; method/property access on objects is denied entirely. + $policy = new SecurityPolicy( + allowedTags: [ + 'if', + 'for', + 'set', + 'apply', + ], + allowedFilters: [ + 'date', + 'date_modify', + 'upper', + 'lower', + 'trim', + 'length', + 'default', + 'number_format', + 'round', + 'abs', + 'split', + 'join', + 'slice', + 'first', + 'last', + 'replace', + 'format', + 'merge', + 'keys', + 'escape', + 'raw', + 'b64enc', + 'b64dec', + 'json_decode', + 'zgw_enum', + 'zgw_enum_reverse', + 'zgw_extract_uuid', + ], + allowedMethods: [], + allowedProperties: [], + allowedFunctions: [ + 'max', + 'min', + 'range', + 'executeMapping', + 'generateUuid', + ] + ); + $this->twig->addExtension(new SandboxExtension($policy, sandboxed: true)); + // Initialize distributed cache for mapping entity lookups. try { $this->mappingCache = $cacheFactory->createDistributed(self::CACHE_PREFIX); @@ -171,7 +228,7 @@ public function encodeArrayKeys(array $array, string $toReplace, string $replace * * @throws Exception When mapping fails * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-3 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ public function executeMapping(Mapping $mapping, array $input, bool $list=false): array { diff --git a/lib/Service/MapsOverviewService.php b/lib/Service/MapsOverviewService.php new file mode 100644 index 0000000000..3a40d1fa86 --- /dev/null +++ b/lib/Service/MapsOverviewService.php @@ -0,0 +1,485 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/integration-maps-overview/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use InvalidArgumentException; +use OCA\OpenRegister\Service\Geo\GeoFeatureCollectionBuilder; +use OCA\OpenRegister\Service\Integration\IntegrationRegistry; +use OCP\IGroupManager; +use OCP\IUserSession; + +/** + * MapsOverviewService — page-level multi-object map render surface. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Service composes the object read facade, geo feature + * builder, integration registry, user session and group manager — each is required for the + * register / query / RBAC contract. + */ +class MapsOverviewService +{ + + /** + * Hard cap on the number of objects scanned for a single overview, so a + * page-level map query can never become an unbounded read. The render + * layer paginates; this is a safety ceiling, not the page size. + * + * @var int + */ + private const MAX_POINTS = 5000; + + /** + * Default declarative base-layer config — Dutch government PDOK WMTS + * (BRT-Achtergrondkaart). NOT hardcoded into the renderer: it is + * metadata on the widget descriptor that a leaf MAY override entirely. + * + * @var array + */ + private const DEFAULT_BASE_LAYER = [ + 'type' => 'wmts', + 'provider' => 'pdok', + 'url' => 'https://service.pdok.nl/brt/achtergrondkaart/wmts/v1_0', + 'layer' => 'standaard', + 'tileMatrixSet' => 'EPSG:3857', + 'format' => 'image/png', + 'attribution' => 'Kaartgegevens © PDOK', + ]; + + /** + * Constructor. + * + * @param ObjectService $objectService Canonical RBAC-scoped object read facade. + * @param GeoFeatureCollectionBuilder $geoBuilder Extracts geometry from object rows. + * @param IntegrationRegistry $registry Registry for the page-widget surface. + * @param IUserSession $userSession Current user (RBAC / write guard). + * @param IGroupManager $groupManager Admin check (RBAC bypass for admins). + * + * @return void + */ + public function __construct( + private ObjectService $objectService, + private GeoFeatureCollectionBuilder $geoBuilder, + private IntegrationRegistry $registry, + private IUserSession $userSession, + private IGroupManager $groupManager, + ) { + }//end __construct() + + /** + * Authorisation guard for the register (write) path — a map overview + * widget may only be declared by an authenticated user. Throws + * fail-closed (ADR-005) so the controller surfaces 400/401 rather than + * silently writing. Called explicitly from the controller so the authz + * decision is visible at the endpoint boundary. + * + * @return void + * + * @throws InvalidArgumentException When no user is logged in. + * + * @spec openspec/specs/integration-maps-overview/spec.md + */ + public function ensureCanRegister(): void + { + if ($this->userSession->getUser() === null) { + throw new InvalidArgumentException('A logged-in user is required to register a map overview'); + } + }//end ensureCanRegister() + + /** + * Register (declare) a page-level map overview widget. + * + * Declares a `map` page widget on the IntegrationRegistry so the + * nc-vue render layer can discover and place a multi-object map. The + * descriptor carries the register/schema scope, an optional default + * filter set, an optional geo-property hint and the declarative base + * layer. It carries NO per-request marker data — the points are fetched + * separately, RBAC-scoped, via queryPoints(). + * + * Mirrors AnalyticsSeriesService::register()'s widget declaration: + * first-write-wins inside the registry (a re-declare with the same id + * is a harmless no-op logged as a duplicate). + * + * @param string $overviewKey Stable overview/widget key. + * @param string|int $register Register slug or id the markers come from. + * @param string|int $schema Schema slug or id the markers come from. + * @param string|null $title Display title for the map widget. + * @param string|null $geoProperty Object property holding the geometry; null = auto-detect. + * @param array $filters Default filters narrowing the object set. + * @param array|null $baseLayer Declarative base-layer override; null = PDOK default. + * + * @return array The stored widget render contract. + * + * @throws InvalidArgumentException On empty key / register / schema. + * + * @spec openspec/specs/integration-maps-overview/spec.md + */ + public function registerOverview( + string $overviewKey, + string | int $register, + string | int $schema, + ?string $title=null, + ?string $geoProperty=null, + array $filters=[], + ?array $baseLayer=null + ): array { + if (trim($overviewKey) === '') { + throw new InvalidArgumentException('overviewKey is required to register a map overview'); + } + + if ((string) $register === '' || (string) $schema === '') { + throw new InvalidArgumentException('register and schema are required to register a map overview'); + } + + $config = [ + 'overviewKey' => $overviewKey, + 'register' => $register, + 'schema' => $schema, + 'geoProperty' => $geoProperty, + 'filters' => $filters, + 'baseLayer' => ($baseLayer ?? self::DEFAULT_BASE_LAYER), + ]; + + // (Re)declare the page widget so the render layer can discover it. + // First-write-wins inside the registry; harmless on a re-declare. + $this->registry->registerPageWidget( + [ + 'id' => 'maps-overview:'.$overviewKey, + 'type' => 'map', + 'title' => $title, + 'providerId' => 'maps-overview', + 'config' => $config, + ] + ); + + return $this->registry->getPageWidget('maps-overview:'.$overviewKey) ?? []; + }//end registerOverview() + + /** + * Authorisation guard for the points (read) path — RBAC-scopes the + * marker query and returns only the points the current user may read. + * + * This is the named authz seam the controller calls so the read + * decision is visible at the endpoint boundary (ADR-005). It delegates + * to {@see queryPoints()}, which runs the canonical OR read path with + * `_rbac: true` for non-admins — an anonymous / low-privilege caller + * only ever sees public-readable objects (fail-closed, no leak). The + * register/schema scope keys are caller-immutable. + * + * @param string|int $register Register slug or id to query. + * @param string|int $schema Schema slug or id to query. + * @param array $filters Extra object filters (property=value). + * @param string|null $geoProperty Geometry property name; null = auto-detect. + * @param int|null $limit Max markers (capped at MAX_POINTS). + * + * @return array> The RBAC-scoped marker point set. + * + * @throws InvalidArgumentException On empty register / schema. + * + * @spec openspec/specs/integration-maps-overview/spec.md + */ + public function ensureReadablePoints( + string | int $register, + string | int $schema, + array $filters=[], + ?string $geoProperty=null, + ?int $limit=null + ): array { + return $this->queryPoints( + register: $register, + schema: $schema, + filters: $filters, + geoProperty: $geoProperty, + limit: $limit + ); + }//end ensureReadablePoints() + + /** + * Query the marker point set for a register/schema, RBAC-scoped. + * + * Runs the canonical OpenRegister read path with `_rbac: true` for + * non-admins, so the returned markers are exactly the objects the + * current user (anonymous included) may read — fail-closed, no leak. + * Each readable object that carries a recognisable geometry becomes a + * marker `{id,label,lat,lng,register,schema,...}`. Objects without + * geometry are silently skipped (nothing to plot). + * + * @param string|int $register Register slug or id to query. + * @param string|int $schema Schema slug or id to query. + * @param array $filters Extra object filters (property=value). + * @param string|null $geoProperty Geometry property name; null = auto-detect. + * @param int|null $limit Max markers to return (capped at MAX_POINTS). + * + * @return array> The RBAC-scoped marker point set. + * + * @throws InvalidArgumentException On empty register / schema. + * + * @spec openspec/specs/integration-maps-overview/spec.md + */ + public function queryPoints( + string | int $register, + string | int $schema, + array $filters=[], + ?string $geoProperty=null, + ?int $limit=null + ): array { + if ((string) $register === '' || (string) $schema === '') { + throw new InvalidArgumentException('register and schema are required to query map points'); + } + + $cap = self::MAX_POINTS; + if ($limit !== null && $limit > 0 && $limit < $cap) { + $cap = $limit; + } + + // RBAC: admins read everything; everyone else (anonymous included) + // is scoped to what the public group / their groups may read. This + // is the SAME read posture ObjectsController uses for the objects + // index — we do not invent a second RBAC path. + $rbac = ($this->isCurrentUserAdmin() === false); + + // Sanitise the filters into the OpenRegister findAll filter shape: + // register/schema are mandatory scope keys; the rest narrow the set. + $objectFilters = $this->buildFilters(register: $register, schema: $schema, filters: $filters); + + $result = $this->objectService->findAll( + config: [ + 'limit' => $cap, + 'filters' => $objectFilters, + ], + _rbac: $rbac, + _multitenancy: $rbac + ); + + // The findAll() call returns a list of rendered object arrays (each + // carrying @self metadata + the schema properties incl. the geometry). + $points = []; + foreach ($result as $row) { + if (is_array($row) === false) { + continue; + } + + $point = $this->pointFromRow(row: $row, geoProperty: $geoProperty, register: $register, schema: $schema); + if ($point !== null) { + $points[] = $point; + } + } + + return $points; + }//end queryPoints() + + /** + * Build the OpenRegister findAll filter array for a map query. + * + * Register/schema become the scope keys; every other supplied filter is + * copied through verbatim (the OR read path validates + RBAC-scopes + * them). The reserved scope keys cannot be overridden by the leaf. + * + * @param string|int $register Register slug or id. + * @param string|int $schema Schema slug or id. + * @param array $filters Caller filters. + * + * @return array The findAll filter array. + */ + private function buildFilters(string | int $register, string | int $schema, array $filters): array + { + $clean = []; + foreach ($filters as $key => $value) { + // Reserved scope keys are owned by this method, not the caller. + if (in_array($key, ['register', 'schema', 'limit', 'offset', 'page'], true) === true) { + continue; + } + + $clean[$key] = $value; + } + + $clean['register'] = $register; + $clean['schema'] = $schema; + + return $clean; + }//end buildFilters() + + /** + * Extract a marker point from a rendered object row. + * + * Reuses {@see GeoFeatureCollectionBuilder} to locate the geometry, then + * reduces it to a single representative `{lat,lng}` (first coordinate of + * Point / first vertex of a Polygon ring etc.). Returns null when the + * object carries no recognisable geometry. + * + * @param array $row The rendered object row. + * @param string|null $geoProperty Geometry property name; null = auto-detect. + * @param string|int $register Register scope (echoed onto the marker). + * @param string|int $schema Schema scope (echoed onto the marker). + * + * @return array|null The marker, or null when no geometry. + */ + private function pointFromRow( + array $row, + ?string $geoProperty, + string | int $register, + string | int $schema + ): ?array { + $feature = $this->geoBuilder->buildFeature(row: $row, geoProperty: $geoProperty); + if ($feature === null) { + return null; + } + + $latLng = $this->representativeLatLng(geometry: ($feature['geometry'] ?? [])); + if ($latLng === null) { + return null; + } + + $self = ($row['@self'] ?? []); + $id = ($row['id'] ?? ($self['id'] ?? ($feature['id'] ?? null))); + $label = $this->deriveLabel(row: $row, self: $self, id: $id); + + $idString = null; + if ($id !== null) { + $idString = (string) $id; + } + + return [ + 'id' => $idString, + 'label' => $label, + 'lat' => $latLng['lat'], + 'lng' => $latLng['lng'], + 'register' => (string) $register, + 'schema' => (string) $schema, + 'geometry' => ($feature['geometry'] ?? null), + ]; + }//end pointFromRow() + + /** + * Reduce a GeoJSON geometry to a single representative [lat,lng]. + * + * Point → its coordinate; Polygon/MultiPolygon/LineString → the first + * vertex. GeoJSON positions are [lon, lat]; we return them split out so + * the render layer never has to remember the ordering. + * + * @param array $geometry The GeoJSON geometry. + * + * @return array{lat: float, lng: float}|null The representative point, or null. + */ + private function representativeLatLng(array $geometry): ?array + { + $type = ($geometry['type'] ?? null); + $coordinates = ($geometry['coordinates'] ?? null); + if (is_array($coordinates) === false) { + return null; + } + + // Drill down to the first [lon, lat] position regardless of nesting + // depth (Point=0, LineString=1, Polygon=2, MultiPolygon=3). + $position = $coordinates; + $guard = 0; + while (isset($position[0]) === true && is_array($position[0]) === true && $guard < 4) { + $position = $position[0]; + $guard++; + } + + if (isset($position[0]) === false || isset($position[1]) === false + || is_numeric($position[0]) === false || is_numeric($position[1]) === false + ) { + return null; + } + + return [ + 'lat' => (float) $position[1], + 'lng' => (float) $position[0], + ]; + }//end representativeLatLng() + + /** + * Derive a human-readable marker label from a rendered object row. + * + * Prefers @self.name / @self.title, then a top-level name/title field, + * finally falls back to the id. Never leaks anything the RBAC read path + * did not already return. + * + * @param array $row The rendered object row. + * @param array $self The @self metadata block. + * @param mixed $id The resolved id. + * + * @return string The marker label. + */ + private function deriveLabel(array $row, array $self, mixed $id): string + { + foreach (['name', 'title'] as $key) { + if (isset($self[$key]) === true && is_scalar($self[$key]) === true && (string) $self[$key] !== '') { + return (string) $self[$key]; + } + } + + foreach (['name', 'title', 'naam', 'titel'] as $key) { + if (isset($row[$key]) === true && is_scalar($row[$key]) === true && (string) $row[$key] !== '') { + return (string) $row[$key]; + } + } + + if ($id !== null) { + return (string) $id; + } + + return ''; + }//end deriveLabel() + + /** + * Whether the current user is an admin (RBAC bypass). + * + * @return bool True when the logged-in user is in the `admin` group. + */ + private function isCurrentUserAdmin(): bool + { + $user = $this->userSession->getUser(); + if ($user === null) { + return false; + } + + return $this->groupManager->isAdmin($user->getUID()); + }//end isCurrentUserAdmin() +}//end class diff --git a/lib/Service/Mcp/McpAnnotationValidator.php b/lib/Service/Mcp/McpAnnotationValidator.php new file mode 100644 index 0000000000..ae1ed54af1 --- /dev/null +++ b/lib/Service/Mcp/McpAnnotationValidator.php @@ -0,0 +1,323 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/ai-mcp/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Mcp; + +/** + * Validate the `x-openregister-mcp` annotation on a schema. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DIALECT-001, REQ-DIALECT-002, REQ-DIALECT-003) + */ +class McpAnnotationValidator +{ + + /** + * The fixed, closed verb set the dialect may declare (REQ-DIALECT-003). + * + * @var array + */ + public const VERBS = ['search', 'get', 'create', 'update', 'delete']; + + /** + * Allowed `scope` values for a verb config. + * + * @var array + */ + public const SCOPES = ['read', 'create', 'update', 'delete']; + + /** + * The MCP 2025-11-25 boolean annotation hints a verb config may carry. + * + * @var array + */ + public const HINT_KEYS = ['readOnlyHint', 'destructiveHint', 'idempotentHint']; + + /** + * Validate the annotation. + * + * Expects the annotation shape `SchemaMapper` hands every annotation + * validator: `['properties' => [...], 'x-openregister-mcp' => [...]]`. + * + * @param array $schema The schema shape to validate. + * + * @return array Aggregated errors (empty = valid). + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DIALECT-001 — The x-openregister-mcp schema dialect) + */ + public function validate(array $schema): array + { + if (isset($schema['x-openregister-mcp']) === false) { + return []; + } + + $annotation = $schema['x-openregister-mcp']; + if (is_array($annotation) === false) { + return [ + [ + 'code' => 'mcp-bad-annotation', + 'message' => 'x-openregister-mcp must be an object.', + ], + ]; + } + + $errors = []; + $this->validateEnabled(annotation: $annotation, errors: $errors); + + $properties = ($schema['properties'] ?? []); + if (is_array($properties) === false) { + $properties = []; + } + + $this->validateTools(annotation: $annotation, properties: $properties, errors: $errors); + + return $errors; + }//end validate() + + /** + * Validate the required `enabled` boolean opt-in gate. + * + * @param array $annotation The x-openregister-mcp block. + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/specs/ai-mcp/spec.md + * (Scenario: enabled must be boolean) + */ + private function validateEnabled(array $annotation, array &$errors): void + { + if (array_key_exists('enabled', $annotation) === false) { + $errors[] = [ + 'code' => 'mcp-missing-enabled', + 'message' => 'x-openregister-mcp requires an `enabled` boolean (the opt-in gate).', + ]; + return; + } + + if (is_bool($annotation['enabled']) === false) { + $errors[] = [ + 'code' => 'mcp-bad-enabled', + 'message' => sprintf( + 'x-openregister-mcp `enabled` must be a boolean, got %s.', + get_debug_type($annotation['enabled']) + ), + ]; + } + }//end validateEnabled() + + /** + * Validate the optional `tools` object: closed verb-key set, per-verb shape. + * + * @param array $annotation The x-openregister-mcp block. + * @param array $properties The schema's declared properties (for `filters` cross-checks). + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/specs/ai-mcp/spec.md + * (Scenario: Unknown verb key is rejected; Scenario: The verb set is closed) + */ + private function validateTools(array $annotation, array $properties, array &$errors): void + { + if (array_key_exists('tools', $annotation) === false) { + return; + } + + $tools = $annotation['tools']; + if (is_array($tools) === false) { + $errors[] = [ + 'code' => 'mcp-bad-tools', + 'message' => 'x-openregister-mcp `tools` must be an object.', + ]; + return; + } + + foreach ($tools as $verb => $config) { + if (in_array((string) $verb, self::VERBS, true) === false) { + $errors[] = [ + 'code' => 'mcp-unknown-verb', + 'message' => sprintf( + 'x-openregister-mcp: unrecognised verb "%s". Allowed verbs: %s.', + (string) $verb, + implode(', ', self::VERBS) + ), + ]; + continue; + } + + if (is_array($config) === false) { + $errors[] = [ + 'code' => 'mcp-bad-verb-config', + 'message' => sprintf('x-openregister-mcp: verb "%s" config must be an object.', (string) $verb), + ]; + continue; + } + + $this->validateVerbConfig(verb: (string) $verb, config: $config, properties: $properties, errors: $errors); + }//end foreach + }//end validateTools() + + /** + * Validate a single verb's config object. + * + * @param string $verb The verb name (already known-valid). + * @param array $config The verb's config object. + * @param array $properties The schema's declared properties. + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Aggregating every per-verb shape rule in one pass is inherent. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DIALECT-002 — Save-time validation of the dialect shape) + */ + private function validateVerbConfig(string $verb, array $config, array $properties, array &$errors): void + { + $knownKeys = array_merge(['description', 'scope', 'filters'], self::HINT_KEYS); + + foreach (array_keys($config) as $key) { + if (in_array((string) $key, $knownKeys, true) === false) { + $errors[] = [ + 'code' => 'mcp-unknown-key', + 'message' => sprintf( + 'x-openregister-mcp: verb "%s" has an unrecognised key "%s".', + $verb, + (string) $key + ), + ]; + } + } + + if (array_key_exists('description', $config) === true && is_string($config['description']) === false) { + $errors[] = [ + 'code' => 'mcp-bad-description', + 'message' => sprintf('x-openregister-mcp: verb "%s" `description` must be a string.', $verb), + ]; + } + + if (array_key_exists('scope', $config) === true + && in_array($config['scope'], self::SCOPES, true) === false + ) { + $errors[] = [ + 'code' => 'mcp-bad-scope', + 'message' => sprintf( + 'x-openregister-mcp: verb "%s" `scope` must be one of [%s].', + $verb, + implode(', ', self::SCOPES) + ), + ]; + } + + foreach (self::HINT_KEYS as $hintKey) { + if (array_key_exists($hintKey, $config) === true && is_bool($config[$hintKey]) === false) { + $errors[] = [ + 'code' => 'mcp-bad-hint', + 'message' => sprintf( + 'x-openregister-mcp: verb "%s" `%s` must be a boolean.', + $verb, + $hintKey + ), + ]; + } + } + + $this->validateFilters(verb: $verb, config: $config, properties: $properties, errors: $errors); + }//end validateVerbConfig() + + /** + * Validate the `filters` key: permitted on `search` only, a list of + * strings, each naming an existing schema property. + * + * @param string $verb The verb name. + * @param array $config The verb's config object. + * @param array $properties The schema's declared properties. + * @param array $errors Error accumulator (by reference). + * + * @return void + * + * @spec openspec/specs/ai-mcp/spec.md + * (Scenario: search filter must reference an existing property; Scenario: filters are permitted only on the search verb) + */ + private function validateFilters(string $verb, array $config, array $properties, array &$errors): void + { + if (array_key_exists('filters', $config) === false) { + return; + } + + if ($verb !== 'search') { + $errors[] = [ + 'code' => 'mcp-filters-not-search', + 'message' => sprintf( + 'x-openregister-mcp: `filters` is valid on the "search" verb only (found on "%s").', + $verb + ), + ]; + return; + } + + $filters = $config['filters']; + if (is_array($filters) === false) { + $errors[] = [ + 'code' => 'mcp-bad-filters', + 'message' => 'x-openregister-mcp: `search.filters` must be a list of property names.', + ]; + return; + } + + foreach ($filters as $filter) { + if (is_string($filter) === false) { + $errors[] = [ + 'code' => 'mcp-bad-filters', + 'message' => 'x-openregister-mcp: `search.filters` entries must be strings.', + ]; + continue; + } + + if (array_key_exists($filter, $properties) === false) { + $errors[] = [ + 'code' => 'mcp-unknown-filter-property', + 'message' => sprintf( + 'x-openregister-mcp: `search.filters` names "%s" which is not a property on this schema.', + $filter + ), + ]; + } + }//end foreach + }//end validateFilters() +}//end class diff --git a/lib/Service/Mcp/McpToolsService.php b/lib/Service/Mcp/McpToolsService.php index 4cd91eb56f..a027446ff3 100644 --- a/lib/Service/Mcp/McpToolsService.php +++ b/lib/Service/Mcp/McpToolsService.php @@ -23,7 +23,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#mcptoolsservice-provider-discovery-refactor + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); @@ -130,12 +130,17 @@ public function listTools(): array }//end listTools() /** - * Execute an MCP tool by its namespaced id + * Execute an MCP tool by its namespaced id or short name * - * Routes the invocation to the provider whose app id prefix matches - * the given tool id. The first matching provider wins. + * Routes the invocation to the provider whose tool descriptor matches + * the given identifier (either descriptor `id` like "openregister.registers" + * or descriptor `name` like "registers"). The first matching provider wins. + * The resolved descriptor's namespaced `id` is forwarded to the provider's + * `invokeTool()` so providers always see the canonical form regardless of + * how the client addressed the tool. * - * @param string $name Namespaced tool id (e.g. "openregister.registers") + * @param string $name Tool identifier — descriptor `name` (e.g. "registers") + * or namespaced `id` (e.g. "openregister.registers"). * @param array $arguments Tool arguments * * @return array MCP tool result with content array @@ -151,15 +156,20 @@ public function callTool(string $name, array $arguments): array context: ['tool' => $name, 'arguments' => $arguments] ); - // Find a provider that owns this tool id. - $provider = $this->findProviderForTool(toolId: $name); + // Find a provider + descriptor that owns this tool id. + $match = $this->findProviderForTool(toolId: $name); - if ($provider === null) { + if ($match === null) { throw new InvalidArgumentException('Unknown tool: '.$name); } + // Forward the descriptor's namespaced id, not the raw client-supplied + // identifier — providers that multiplex on $toolId must see the + // canonical form even when the client addressed the tool by short name. + $providerToolId = (string) ($match['descriptor']['id'] ?? $name); + try { - $result = $provider->invokeTool(toolId: $name, arguments: $arguments); + $result = $match['provider']->invokeTool(toolId: $providerToolId, arguments: $arguments); return [ 'content' => [ @@ -193,12 +203,15 @@ public function callTool(string $name, array $arguments): array }//end callTool() /** - * Invoke a tool by namespaced id, returning a flat result array. + * Invoke a tool by namespaced id or short name, returning a flat result array. * * Used by ChatStreamController to invoke tools in the LLM pipeline - * and emit tool_result SSE events. + * and emit tool_result SSE events. Accepts both descriptor `id` + * ("openregister.registers") and descriptor `name` ("registers") + * forms; the resolved descriptor's namespaced id is forwarded to + * the owning provider. * - * @param string $toolId Namespaced tool id + * @param string $toolId Tool identifier — descriptor `name` or namespaced `id`. * @param array $arguments Tool arguments * * @return array{result: array, isError: bool} Result envelope @@ -207,17 +220,19 @@ public function callTool(string $name, array $arguments): array */ public function invokeTool(string $toolId, array $arguments): array { - $provider = $this->findProviderForTool(toolId: $toolId); + $match = $this->findProviderForTool(toolId: $toolId); - if ($provider === null) { + if ($match === null) { return [ 'result' => ['error' => 'Unknown tool: '.$toolId], 'isError' => true, ]; } + $providerToolId = (string) ($match['descriptor']['id'] ?? $toolId); + try { - $result = $provider->invokeTool(toolId: $toolId, arguments: $arguments); + $result = $match['provider']->invokeTool(toolId: $providerToolId, arguments: $arguments); return [ 'result' => $result, 'isError' => false, @@ -238,33 +253,74 @@ public function invokeTool(string $toolId, array $arguments): array }//end invokeTool() /** - * Find the first provider that owns the given tool id. + * Find the first provider + descriptor that owns the given tool id. + * + * Resolves against the descriptor's full namespaced `id` + * (e.g. "openregister.registers") OR its short MCP `name` + * (e.g. "registers"). The MCP protocol's tools/call uses + * the descriptor's `name`, so accepting both keeps spec- + * compliant clients and any chat-side caller that already + * uses the namespaced id working through the same path. * - * A provider owns a tool id when the tool's id starts with - * `{provider->getAppId()}.` AND the provider lists that tool in getTools(). + * Returns the descriptor alongside the provider so callers + * can forward the canonical namespaced id to `invokeTool()` + * regardless of how the client addressed the tool. * - * @param string $toolId Namespaced tool id + * Collision warning: when a short-name lookup matches descriptors in + * more than one provider, only the first match is returned (first-wins) + * and a warning is logged once so the ambiguity surfaces in the + * application log. External providers should namespace any short name + * that could collide. * - * @return IMcpToolProvider|null The matching provider, or null if not found + * @param string $toolId Tool identifier as sent by the client. + * + * @return array{provider: IMcpToolProvider, descriptor: array}|null + * The first matching provider + its descriptor, or null if not found. */ - private function findProviderForTool(string $toolId): ?IMcpToolProvider + private function findProviderForTool(string $toolId): ?array { + $first = null; + $collisions = 0; + foreach ($this->providers as $provider) { - $appId = $provider->getAppId(); + foreach ($provider->getTools() as $descriptor) { + $matchesId = (($descriptor['id'] ?? '') === $toolId); + $matchesName = (($descriptor['name'] ?? '') === $toolId); - if (str_starts_with($toolId, $appId.'.') === false) { - continue; - } + if ($matchesId === false && $matchesName === false) { + continue; + } - // Confirm the provider actually lists this tool. - foreach ($provider->getTools() as $descriptor) { - if (($descriptor['id'] ?? '') === $toolId) { - return $provider; + if ($first === null) { + $first = ['provider' => $provider, 'descriptor' => $descriptor]; + + // An exact id-match is the canonical form — no ambiguity + // is possible because listTools() already enforces the + // namespace prefix uniqueness. Stop scanning for collisions. + if ($matchesId === true) { + return $first; + } + + continue; } - } + + // Short-name collision across providers — log once below. + ++$collisions; + }//end foreach + }//end foreach + + if ($collisions > 0 && $first !== null) { + $this->logger->warning( + message: '[MCP] Short-name tool collision — first provider wins; use namespaced id to disambiguate.', + context: [ + 'tool' => $toolId, + 'winningAppId' => $first['provider']->getAppId(), + 'collisionCount' => $collisions, + ] + ); } - return null; + return $first; }//end findProviderForTool() /** diff --git a/lib/Service/Mcp/ToolRegistryFacade.php b/lib/Service/Mcp/ToolRegistryFacade.php new file mode 100644 index 0000000000..cd6749a792 --- /dev/null +++ b/lib/Service/Mcp/ToolRegistryFacade.php @@ -0,0 +1,305 @@ +publish()`-style silent removal cannot + * happen to this surface. + * + * The facade is a pure read/invoke wrapper: it changes no ToolRegistry + * behavior, adds no registration path, and performs NO impersonation — + * invocations run in the caller's ambient Nextcloud request/session context + * only (hydra ADR-034 Decision 7). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Mcp + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/ai-mcp/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Mcp; + +use OCA\OpenRegister\Service\ToolRegistry; +use OCA\OpenRegister\Tool\ToolInterface; +use Psr\Log\LoggerInterface; + +/** + * ToolRegistryFacade + * + * Small, additive public read/invoke surface over ToolRegistry. Exposes + * exactly two methods: + * + * - listTools() — every LLPhant-shaped function descriptor known to the + * registry (built-in tools AND MCP-bridged per-app tools), optionally + * narrowed by a whitelist of registry ids ({appId}.{toolName} — the + * hydra ADR-035 Decision 4 `Agent.toolWhitelist` semantics; empty means + * "all discovered tools allowed"). + * - invokeTool() — resolve a function name (or dotted mcpId) back to its + * owning ToolInterface and delegate to executeFunction(), returning the + * same {result, isError} envelope as McpToolsService::invokeTool(). + * + * Registration stays exclusively on the ToolRegistrationEvent + + * ToolRegistrationListener path; this facade cannot mutate the registry. + * + * @category Service + * @package OCA\OpenRegister\Service\Mcp + * + * @psalm-suppress UnusedClass - Public API consumed by external apps (Hermiq), injected via DI + */ +class ToolRegistryFacade +{ + /** + * Constructor + * + * Both dependencies are autowired by Nextcloud's DI container — no + * explicit Application.php registration is needed (mirrors ToolRegistry + * itself). + * + * @param ToolRegistry $toolRegistry The chat-side tool registry. + * @param LoggerInterface $logger PSR logger. + * + * @spec openspec/specs/ai-mcp/spec.md + */ + public function __construct( + private readonly ToolRegistry $toolRegistry, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * List every callable function descriptor known to the tool registry. + * + * Flattens registry-id-level tools that expose multiple functions (the + * built-in RegisterTool alone exposes five) into one list of + * LLPhant-shaped descriptors (`name`, `description`, `parameters`, plus + * `mcpId` for MCP-bridged tools) — the exact shape a tool-loop hands an + * LLM provider. Mirrors what ToolManagementHandler does internally for + * the in-app chat path, as a public stable read. + * + * The whitelist is matched PER FUNCTION, the same dual-form way invokeTool() + * resolves a call: an entry may name a function's LLPhant-safe `name` + * (`list_schemas`), its dotted `mcpId` (`openbuild.upsertSchema`), or the + * registry id of the owning tool (`openregister.schema`, which admits all of + * that tool's functions). + * + * Matching per FUNCTION rather than per TOOL is deliberate and load-bearing. + * A registry id such as `openregister.schema` is ONE tool exposing five + * functions (list/get/create/update/delete_schema), and most built-in + * descriptors carry no `mcpId` at all — so intersecting the whitelist against + * registry ids made the two id spaces disagree in BOTH directions: an entry + * naming a real function (`list_schemas`) matched nothing and silently + * resolved to zero tools, while an entry naming a tool handed over every + * function it owns — `delete_schema` included — defeating a caller's + * read-only intent. Consumers store function-level ids (an ADR-035 + * toolWhitelist, Hermiq's per-agent grants) and invokeTool() has always + * resolved them function-level; this is listTools() keeping the contract the + * rest of the class already documents. + * + * @param array $toolWhitelist Optional whitelist of function names, + * dotted mcpIds and/or registry ids + * ({appId}.{toolName}). Empty = all + * discovered tools allowed (ADR-035 + * Decision 4 default semantics). + * + * @return array> Flattened LLPhant function descriptors. + * + * @spec openspec/specs/ai-mcp/spec.md + */ + public function listTools(array $toolWhitelist=[]): array + { + $descriptors = []; + + foreach ($this->resolveRegisteredTools(toolWhitelist: []) as $registryId => $tool) { + foreach ($tool->getFunctions() as $function) { + $allowed = $this->functionIsWhitelisted( + function: $function, + registryId: (string) $registryId, + toolWhitelist: $toolWhitelist + ); + + if ($allowed === false) { + continue; + } + + $descriptors[] = $function; + } + } + + return $descriptors; + }//end listTools() + + /** + * Whether one function descriptor is admitted by a whitelist. + * + * An empty whitelist admits everything (ADR-035 Decision 4). Otherwise the + * entry must name the function's `name`, its dotted `mcpId`, or the registry + * id of the tool that owns it. + * + * @param array $function The LLPhant function descriptor. + * @param string $registryId Registry id of the owning tool. + * @param array $toolWhitelist The whitelist to apply. + * + * @return bool True when the descriptor should be listed. + * + * @spec openspec/specs/ai-mcp/spec.md + */ + private function functionIsWhitelisted(array $function, string $registryId, array $toolWhitelist): bool + { + if ($toolWhitelist === []) { + return true; + } + + $candidates = [$registryId]; + + $name = ($function['name'] ?? null); + if (is_string($name) === true && $name !== '') { + $candidates[] = $name; + } + + $mcpId = ($function['mcpId'] ?? null); + if (is_string($mcpId) === true && $mcpId !== '') { + $candidates[] = $mcpId; + } + + return (array_intersect($candidates, $toolWhitelist) !== []); + }//end functionIsWhitelisted() + + /** + * Invoke a tool function by its descriptor name or dotted mcpId. + * + * Resolves $toolId against the same flattened function index listTools() + * builds — matching either the LLPhant-safe function `name` (e.g. + * `decidesk_listMeetings`, the form an LLM tool-call echoes back) or the + * dotted `mcpId` (e.g. `decidesk.listMeetings`, the form an ADR-035 + * toolWhitelist stores) — and delegates to the owning tool's + * executeFunction(). + * + * NO impersonation: there is deliberately no $userId / acting-user / + * agent parameter. The call executes in the caller's ambient Nextcloud + * request/session context only; per-object IDOR boundaries remain each + * provider's own responsibility (IMcpToolProvider contract, hydra + * ADR-034 Decision 7). + * + * @param string $toolId Function name or dotted mcpId. + * @param array $arguments Decoded arguments object. + * + * @return array{result: array, isError: bool} Result envelope + * (same shape as McpToolsService::invokeTool()). + * + * @spec openspec/specs/ai-mcp/spec.md + */ + public function invokeTool(string $toolId, array $arguments): array + { + $tool = $this->findOwningTool(toolId: $toolId); + + if ($tool === null) { + return [ + 'result' => ['error' => 'Unknown tool: '.$toolId], + 'isError' => true, + ]; + } + + try { + $result = $tool->executeFunction($toolId, $arguments); + + return [ + 'result' => $result, + 'isError' => false, + ]; + } catch (\Throwable $e) { + // Catch \Throwable, not \Exception: a TypeError from a malformed + // argument must not escape the facade as an uncaught 500 — + // matching the \Throwable catches in McpProviderBridge and + // McpToolsService. + $this->logger->error( + message: '[ToolRegistryFacade] Tool invocation failed', + context: ['tool' => $toolId, 'error' => $e->getMessage()] + ); + + return [ + 'result' => ['error' => $e->getMessage()], + 'isError' => true, + ]; + }//end try + }//end invokeTool() + + /** + * Resolve the registered ToolInterface instances, optionally whitelisted. + * + * @param array $toolWhitelist Registry ids to keep (empty = all). + * + * @return array Map of registry id to tool instance. + */ + private function resolveRegisteredTools(array $toolWhitelist): array + { + $registryIds = array_keys($this->toolRegistry->getAllTools()); + + if ($toolWhitelist !== []) { + $registryIds = array_values(array_intersect($registryIds, $toolWhitelist)); + } + + $tools = []; + foreach ($registryIds as $registryId) { + $tool = $this->toolRegistry->getTool($registryId); + if ($tool === null) { + continue; + } + + $tools[$registryId] = $tool; + } + + return $tools; + }//end resolveRegisteredTools() + + /** + * Find the tool whose function descriptors match the given identifier. + * + * Matches a descriptor's `name` (LLPhant-safe form) or `mcpId` (dotted + * form) — the same dual-form resolution McpProviderBridge::resolveMcpId() + * already performs for bridged tools. First match in registry iteration + * order wins (built-ins first, per ToolRegistrationListener ordering), + * consistent with McpToolsService::findProviderForTool(). + * + * @param string $toolId Function name or dotted mcpId. + * + * @return ToolInterface|null The owning tool, or null when no descriptor matches. + */ + private function findOwningTool(string $toolId): ?ToolInterface + { + foreach ($this->resolveRegisteredTools(toolWhitelist: []) as $tool) { + foreach ($tool->getFunctions() as $function) { + $matchesName = (($function['name'] ?? '') === $toolId); + $matchesMcpId = (($function['mcpId'] ?? '') === $toolId); + + if ($matchesName === true || $matchesMcpId === true) { + return $tool; + } + } + } + + return null; + }//end findOwningTool() +}//end class diff --git a/lib/Service/McpDiscoveryService.php b/lib/Service/McpDiscoveryService.php index 1107bade8c..137f15f802 100644 --- a/lib/Service/McpDiscoveryService.php +++ b/lib/Service/McpDiscoveryService.php @@ -19,10 +19,10 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-52 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-55 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-56 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-1-discovery-catalog + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data + * @spec openspec/specs/mcp-discovery/spec.md + * @spec openspec/specs/mcp-discovery/spec.md */ namespace OCA\OpenRegister\Service; @@ -116,7 +116,7 @@ private function getBaseUrl(): string * * @return string The absolute URL * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-52 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-1-discovery-catalog */ private function getCapabilityHref(string $capabilityId): string { @@ -133,8 +133,8 @@ private function getCapabilityHref(string $capabilityId): string * * @return array The discovery catalog * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-52 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-55 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-1-discovery-catalog + * @spec openspec/specs/mcp-discovery/spec.md */ public function getCatalog(): array { @@ -248,8 +248,8 @@ public function getCapabilityIds(): array * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-56 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data + * @spec openspec/specs/mcp-discovery/spec.md */ public function getCapabilityDetail(string $capability): ?array { @@ -281,7 +281,7 @@ public function getCapabilityDetail(string $capability): ?array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildRegistersCapability(): array { @@ -356,7 +356,7 @@ private function buildRegistersCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildSchemasCapability(): array { @@ -429,7 +429,7 @@ private function buildSchemasCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildObjectsCapability(): array { @@ -570,7 +570,7 @@ private function buildObjectsCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildSearchCapability(): array { @@ -644,7 +644,7 @@ private function buildSearchCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildFilesCapability(): array { @@ -700,7 +700,7 @@ private function buildFilesCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildAuditCapability(): array { @@ -741,7 +741,7 @@ private function buildAuditCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildBulkCapability(): array { @@ -782,7 +782,7 @@ private function buildBulkCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildWebhooksCapability(): array { @@ -838,7 +838,7 @@ private function buildWebhooksCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildChatCapability(): array { @@ -889,7 +889,7 @@ private function buildChatCapability(): array * * @return array Capability detail with endpoints and context * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-53 + * @spec openspec/specs/mcp-discovery/spec.md#requirement-tier-2-capability-detail-with-live-data */ private function buildViewsCapability(): array { diff --git a/lib/Service/MdiIconRenderer.php b/lib/Service/MdiIconRenderer.php new file mode 100644 index 0000000000..08daab5723 --- /dev/null +++ b/lib/Service/MdiIconRenderer.php @@ -0,0 +1,154 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +/** + * Renders Material Design Icon references to self-hosted SVG data URIs. + */ +final class MdiIconRenderer +{ + + /** + * Fill colour for rendered glyphs — a neutral slate readable on the light + * unified-search dropdown. + * + * @var string + */ + private const FILL = '#5d6770'; + + /** + * Curated MDI path data, keyed by the normalised icon name (lower-case, + * alphanumeric, `mdi` prefix stripped — so "Dog", "mdi-dog" and "mdiDog" + * all resolve to "dog"). Values are the SVG `path` `d` data from Material + * Design Icons v7 (the mdi/js package). Lines hold one full path each and + * therefore exceed the line-length limit by design. + * + * @var array + */ + // phpcs:disable Generic.Files.LineLength.MaxExceeded + private const PATHS = [ + 'dog' => 'M18,4C16.29,4 15.25,4.33 14.65,4.61C13.88,4.23 13,4 12,4C11,4 10.12,4.23 9.35,4.61C8.75,4.33 7.71,4 6,4C3,4 1,12 1,14C1,14.83 2.32,15.59 4.14,15.9C4.78,18.14 7.8,19.85 11.5,20V15.72C10.91,15.35 10,14.68 10,14C10,13 12,13 12,13C12,13 14,13 14,14C14,14.68 13.09,15.35 12.5,15.72V20C16.2,19.85 19.22,18.14 19.86,15.9C21.68,15.59 23,14.83 23,14C23,12 21,4 18,4M4.15,13.87C3.65,13.75 3.26,13.61 3,13.5C3.25,10.73 5.2,6.4 6.05,6C6.59,6 7,6.06 7.37,6.11C5.27,8.42 4.44,12.04 4.15,13.87M9,12A1,1 0 0,1 8,11C8,10.46 8.45,10 9,10A1,1 0 0,1 10,11C10,11.56 9.55,12 9,12M15,12A1,1 0 0,1 14,11C14,10.46 14.45,10 15,10A1,1 0 0,1 16,11C16,11.56 15.55,12 15,12M19.85,13.87C19.56,12.04 18.73,8.42 16.63,6.11C17,6.06 17.41,6 17.95,6C18.8,6.4 20.75,10.73 21,13.5C20.75,13.61 20.36,13.75 19.85,13.87Z', + 'cat' => 'M12,8L10.67,8.09C9.81,7.07 7.4,4.5 5,4.5C5,4.5 3.03,7.46 4.96,11.41C4.41,12.24 4.07,12.67 4,13.66L2.07,13.95L2.28,14.93L4.04,14.67L4.18,15.38L2.61,16.32L3.08,17.21L4.53,16.32C5.68,18.76 8.59,20 12,20C15.41,20 18.32,18.76 19.47,16.32L20.92,17.21L21.39,16.32L19.82,15.38L19.96,14.67L21.72,14.93L21.93,13.95L20,13.66C19.93,12.67 19.59,12.24 19.04,11.41C20.97,7.46 19,4.5 19,4.5C16.6,4.5 14.19,7.07 13.33,8.09L12,8M9,11A1,1 0 0,1 10,12A1,1 0 0,1 9,13A1,1 0 0,1 8,12A1,1 0 0,1 9,11M15,11A1,1 0 0,1 16,12A1,1 0 0,1 15,13A1,1 0 0,1 14,12A1,1 0 0,1 15,11M11,14H13L12.3,15.39C12.5,16.03 13.06,16.5 13.75,16.5A1.5,1.5 0 0,0 15.25,15H15.75A2,2 0 0,1 13.75,17C13,17 12.35,16.59 12,16V16H12C11.65,16.59 11,17 10.25,17A2,2 0 0,1 8.25,15H8.75A1.5,1.5 0 0,0 10.25,16.5C10.94,16.5 11.5,16.03 11.7,15.39L11,14Z', + 'bird' => 'M23 11.5L19.95 10.37C19.69 9.22 19.04 8.56 19.04 8.56C17.4 6.92 14.75 6.92 13.11 8.56L11.63 10.04L5 3C4 7 5 11 7.45 14.22L2 19.5C2 19.5 10.89 21.5 16.07 17.45C18.83 15.29 19.45 14.03 19.84 12.7L23 11.5M17.71 11.72C17.32 12.11 16.68 12.11 16.29 11.72C15.9 11.33 15.9 10.7 16.29 10.31C16.68 9.92 17.32 9.92 17.71 10.31C18.1 10.7 18.1 11.33 17.71 11.72Z', + 'fish' => 'M12,20L12.76,17C9.5,16.79 6.59,15.4 5.75,13.58C5.66,14.06 5.53,14.5 5.33,14.83C4.67,16 3.33,16 2,16C3.1,16 3.5,14.43 3.5,12.5C3.5,10.57 3.1,9 2,9C3.33,9 4.67,9 5.33,10.17C5.53,10.5 5.66,10.94 5.75,11.42C6.4,10 8.32,8.85 10.66,8.32L9,5C11,5 13,5 14.33,5.67C15.46,6.23 16.11,7.27 16.69,8.38C19.61,9.08 22,10.66 22,12.5C22,14.38 19.5,16 16.5,16.66C15.67,17.76 14.86,18.78 14.17,19.33C13.33,20 12.67,20 12,20M17,11A1,1 0 0,0 16,12A1,1 0 0,0 17,13A1,1 0 0,0 18,12A1,1 0 0,0 17,11Z', + 'turtle' => 'M8.47,5.95C8.95,5.67 9.47,5.44 10,5.28V4C10,2.9 10.87,2 11.97,1.97C13.13,2 14,2.9 14,4V5.28C14.53,5.45 15.05,5.67 15.53,5.95L13.93,8.07H10.07L8.47,5.95M19,12C19,12.5 18.95,12.95 18.86,13.4L16.33,12.62L15.14,8.96L16.74,6.85C17.17,7.25 17.55,7.7 17.88,8.2C18.67,8.13 19.43,8.25 20.11,8.59C21.14,9.12 21.84,10.13 22,11.28L19,11.64C19,11.76 19,11.88 19,12M5,12C5,11.88 5,11.76 5,11.65L2,11.28C2.16,10.13 2.86,9.12 3.89,8.59C4.57,8.25 5.34,8.13 6.08,8.26C6.41,7.75 6.79,7.28 7.24,6.87L8.86,8.95L7.67,12.62L5.14,13.4C5.05,12.95 5,12.5 5,12M10.24,9.57H13.76L14.85,12.93L12,15L9.15,12.93L10.24,9.57M8.13,14.05L11.25,16.31V18.96C10.68,18.9 10.13,18.77 9.62,18.58L8.39,21.34C7.33,20.87 6.57,19.9 6.37,18.76C6.23,18 6.35,17.24 6.69,16.56C6.24,16.04 5.87,15.46 5.59,14.82L8.13,14.05M15.87,14.05L18.41,14.82C18.13,15.46 17.76,16.04 17.31,16.56C17.65,17.24 17.77,18 17.64,18.76C17.43,19.9 16.67,20.87 15.61,21.34L14.39,18.58C13.86,18.77 13.33,18.94 12.75,19V16.31L15.87,14.05Z', + 'paw' => 'M8.35,3C9.53,2.83 10.78,4.12 11.14,5.9C11.5,7.67 10.85,9.25 9.67,9.43C8.5,9.61 7.24,8.32 6.87,6.54C6.5,4.77 7.17,3.19 8.35,3M15.5,3C16.69,3.19 17.35,4.77 17,6.54C16.62,8.32 15.37,9.61 14.19,9.43C13,9.25 12.35,7.67 12.72,5.9C13.08,4.12 14.33,2.83 15.5,3M3,7.6C4.14,7.11 5.69,8 6.5,9.55C7.26,11.13 7,12.79 5.87,13.28C4.74,13.77 3.2,12.89 2.41,11.32C1.62,9.75 1.9,8.08 3,7.6M21,7.6C22.1,8.08 22.38,9.75 21.59,11.32C20.8,12.89 19.26,13.77 18.13,13.28C17,12.79 16.74,11.13 17.5,9.55C18.31,8 19.86,7.11 21,7.6M19.33,18.38C19.37,19.32 18.65,20.36 17.79,20.75C16,21.57 13.88,19.87 11.89,19.87C9.9,19.87 7.76,21.64 6,20.75C5,20.26 4.31,18.96 4.44,17.88C4.62,16.39 6.41,15.59 7.47,14.5C8.88,13.09 9.88,10.44 11.89,10.44C13.89,10.44 14.95,13.05 16.3,14.5C17.41,15.72 19.26,16.75 19.33,18.38Z', + 'account' => 'M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z', + 'accountgroup' => 'M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z', + 'tag' => 'M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z', + 'shape' => 'M11,13.5V21.5H3V13.5H11M12,2L17.5,11H6.5L12,2M17.5,13C20,13 22,15 22,17.5C22,20 20,22 17.5,22C15,22 13,20 13,17.5C13,15 15,13 17.5,13Z', + 'cartoutline' => 'M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z', + 'stethoscope' => 'M19,8C19.56,8 20,8.43 20,9A1,1 0 0,1 19,10C18.43,10 18,9.55 18,9C18,8.43 18.43,8 19,8M2,2V11C2,13.96 4.19,16.5 7.14,16.91C7.76,19.92 10.42,22 13.5,22A6.5,6.5 0 0,0 20,15.5V11.81C21.16,11.39 22,10.29 22,9A3,3 0 0,0 19,6A3,3 0 0,0 16,9C16,10.29 16.84,11.4 18,11.81V15.41C18,17.91 16,19.91 13.5,19.91C11.5,19.91 9.82,18.7 9.22,16.9C12,16.3 14,13.8 14,11V2H10V5H12V11A4,4 0 0,1 8,15A4,4 0 0,1 4,11V5H6V2H2Z', + 'clipboardpulse' => 'M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M5,13.46H7.17L10.5,7.08L11.44,14.05L13.93,10.86L16.53,13.46H19V15H15.89L14.07,13.21L10.38,17.92L9.62,12.15L8.11,15H5V13.46Z', + 'medicalbag' => 'M10,3L8,5V7H5C3.85,7 3.12,8 3,9L2,19C1.88,20 2.54,21 4,21H20C21.46,21 22.12,20 22,19L21,9C20.88,8 20.06,7 19,7H16V5L14,3H10M10,5H14V7H10V5M11,10H13V13H16V15H13V18H11V15H8V13H11V10Z', + 'calendar' => 'M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M17,12H12V17H17V12Z', + 'database' => 'M12,3C7.58,3 4,4.79 4,7C4,9.21 7.58,11 12,11C16.42,11 20,9.21 20,7C20,4.79 16.42,3 12,3M4,9V12C4,14.21 7.58,16 12,16C16.42,16 20,14.21 20,12V9C20,11.21 16.42,13 12,13C7.58,13 4,11.21 4,9M4,14V17C4,19.21 7.58,21 12,21C16.42,21 20,19.21 20,17V14C20,16.21 16.42,18 12,18C7.58,18 4,16.21 4,14Z', + 'filedocumentoutline' => 'M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z', + ]; + // phpcs:enable Generic.Files.LineLength.MaxExceeded + + /** + * Whether the curated set contains a renderable glyph for this icon. + * + * @param string|null $icon The schema icon reference (e.g. "Dog", "mdi-dog"). + * + * @return bool True when {@see self::svg()} would return an SVG. + */ + public static function has(?string $icon): bool + { + return self::resolvePath(icon: $icon) !== null; + + }//end has() + + /** + * Render an MDI icon reference to a standalone 24×24 SVG document. + * + * @param string|null $icon The schema icon reference (e.g. "Dog", "mdi-dog"). + * + * @return string|null The SVG markup, or null when the icon is empty or not + * in the curated set. + */ + public static function svg(?string $icon): ?string + { + $path = self::resolvePath(icon: $icon); + if ($path === null) { + return null; + } + + return '' + .''; + + }//end svg() + + /** + * Render an MDI icon reference to a `data:` SVG URI. + * + * @param string|null $icon The schema icon reference (e.g. "Dog", "mdi-dog"). + * + * @return string|null A `data:image/svg+xml;base64,…` URI, or null when the + * icon is empty or not in the curated set. + */ + public static function dataUri(?string $icon): ?string + { + $svg = self::svg(icon: $icon); + if ($svg === null) { + return null; + } + + return 'data:image/svg+xml;base64,'.base64_encode($svg); + + }//end dataUri() + + /** + * Resolve an icon reference to its SVG path data, or null when unknown. + * + * @param string|null $icon The schema icon reference. + * + * @return string|null The SVG path `d` data, or null. + */ + private static function resolvePath(?string $icon): ?string + { + if ($icon === null || $icon === '') { + return null; + } + + // Normalise: drop a leading "mdi" prefix, strip every non-alphanumeric + // character, lower-case — so "Dog", "mdi-dog" and "mdiDog" all map to + // the "dog" key. + $key = strtolower((string) preg_replace('/[^a-z0-9]/i', '', $icon)); + if (str_starts_with($key, 'mdi') === true) { + $key = substr($key, 3); + } + + return (self::PATHS[$key] ?? null); + + }//end resolvePath() +}//end class diff --git a/lib/Service/Merge/MergeAnnotationValidator.php b/lib/Service/Merge/MergeAnnotationValidator.php new file mode 100644 index 0000000000..8ad1420668 --- /dev/null +++ b/lib/Service/Merge/MergeAnnotationValidator.php @@ -0,0 +1,163 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Merge; + +/** + * Shape validator for the `x-openregister-merge` annotation. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#1.2 + */ +class MergeAnnotationValidator +{ + /** + * Validate the `x-openregister-merge` annotation in a schema shape. + * + * @param array $schema Shape with `properties` and `x-openregister-merge`. + * + * @return array Errors; empty when valid or absent. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#1.2 + */ + public function validate(array $schema): array + { + $annotation = ($schema['x-openregister-merge'] ?? null); + if ($annotation === null) { + return []; + } + + if (is_array($annotation) === false) { + return [ + [ + 'code' => 'merge.not-object', + 'message' => 'x-openregister-merge must be an object.', + ], + ]; + } + + $errors = []; + + if (array_key_exists('reversalWindowDays', $annotation) === true) { + $window = $annotation['reversalWindowDays']; + if (is_int($window) === false || $window <= 0) { + $errors[] = [ + 'code' => 'merge.invalid-reversal-window', + 'message' => 'x-openregister-merge "reversalWindowDays" must be a positive integer.', + ]; + } + } + + $errors = array_merge($errors, $this->validateStringFields(annotation: $annotation)); + $errors = array_merge($errors, $this->validateSourceLink(annotation: $annotation)); + + return $errors; + }//end validate() + + /** + * Validate an optional `sourceLink` block. A `reverseFk` mode requires + * `sourceSchema` and `referenceField`; anything else is accepted (embedded + * mode). Errors are non-fatal (surfaced as warnings by the caller). + * + * @param array $annotation Merge annotation. + * + * @return array + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#1.1 + */ + private function validateSourceLink(array $annotation): array + { + $sourceLink = ($annotation['sourceLink'] ?? null); + if ($sourceLink === null) { + return []; + } + + if (is_array($sourceLink) === false) { + return [ + [ + 'code' => 'merge.source-link-not-object', + 'message' => 'x-openregister-merge "sourceLink" must be an object.', + ], + ]; + } + + if ((string) ($sourceLink['mode'] ?? 'embedded') !== 'reverseFk') { + return []; + } + + $errors = []; + if ((string) ($sourceLink['sourceSchema'] ?? '') === '' + || (string) ($sourceLink['referenceField'] ?? '') === '' + ) { + $errors[] = [ + 'code' => 'merge.source-link-reverse-fk-incomplete', + 'message' => 'x-openregister-merge reverseFk "sourceLink" requires "sourceSchema" and "referenceField".', + ]; + } + + return $errors; + }//end validateSourceLink() + + /** + * Validate the optional string-valued fields, when present. + * + * @param array $annotation Merge annotation. + * + * @return array + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#1.2 + */ + private function validateStringFields(array $annotation): array + { + $errors = []; + + $stringFields = [ + 'sourceLinkField', + 'entityType', + 'statusField', + 'survivorStatus', + 'mergedStatus', + ]; + + foreach ($stringFields as $field) { + if (array_key_exists($field, $annotation) === false) { + continue; + } + + if (is_string($annotation[$field]) === false) { + $errors[] = [ + 'code' => 'merge.field-not-string', + 'message' => sprintf('x-openregister-merge "%s" must be a string.', $field), + ]; + } + } + + return $errors; + }//end validateStringFields() +}//end class diff --git a/lib/Service/Merge/MergeService.php b/lib/Service/Merge/MergeService.php new file mode 100644 index 0000000000..c28f7e3530 --- /dev/null +++ b/lib/Service/Merge/MergeService.php @@ -0,0 +1,1023 @@ + relink -> recompute -> status flip -> persist + * mergeOperation -> dispatch event. `reverseMerge` restores the snapshot + * within `reversalWindowDays`, best-effort per source-record link (an + * unresolvable record is skipped, not aborted — mirrors pipelinq). + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Merge + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Merge; + +use DateInterval; +use DateTimeImmutable; +use Exception; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ObjectsMergedEvent; +use OCA\OpenRegister\Service\ObjectService; +use OCA\OpenRegister\Service\Survivorship\SourceRecordResolver; +use OCA\OpenRegister\Service\Survivorship\SurvivorshipResolver; +use OCA\OpenRegister\Service\Survivorship\TrustTierResolver; +use OCP\EventDispatcher\IEventDispatcher; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +/** + * Server-authoritative, reversible merge engine driven by + * `x-openregister-merge` and reusing `SurvivorshipResolver`. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.1 + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) The class owns the full + * reversible-merge lifecycle (preview / execute / reverse / snapshot / + * recompute) as one server-authoritative unit of work per the spec; splitting + * it across services would scatter a single transactional contract and make + * the atomicity guarantee harder to audit, mirroring pipelinq's precedent. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Reuses the existing + * survivorship collaborators (ObjectService, SchemaMapper, + * SurvivorshipResolver, TrustTierResolver) plus the event dispatcher and + * logger, per ADR-011 reuse — introducing a facade purely to hide the + * dependency count would add indirection without reducing coupling. + */ +class MergeService +{ + + /** + * Slug of the OR-owned merge-operation register schema. + * + * @var string + */ + public const MERGE_SCHEMA = 'mergeOperation'; + + /** + * Slug of the OR-owned merge-operation register. + * + * @var string + */ + private const MERGE_REGISTER = 'merge-operation'; + + /** + * Slug of the OR-owned trust-configuration register schema. + * + * @var string + */ + private const TRUST_CONFIGURATION_SCHEMA = 'trustConfiguration'; + + /** + * Fallback reversal window (days) when the annotation omits `reversalWindowDays`. + * + * @var int + */ + public const DEFAULT_REVERSAL_WINDOW_DAYS = 30; + + /** + * Default field the merge status is read from when the annotation omits `statusField`. + * + * @var string + */ + private const DEFAULT_STATUS_FIELD = 'status'; + + /** + * Default value marking the surviving object. + * + * @var string + */ + private const DEFAULT_SURVIVOR_STATUS = 'active'; + + /** + * Default value marking a merged-away object. + * + * @var string + */ + private const DEFAULT_MERGED_STATUS = 'merged-into-other'; + + /** + * Wire collaborators. + * + * @param ObjectService $objectService Object read/write path (RBAC + tenant scoped). + * @param SchemaMapper $schemaMapper Schema lookup for the merge + survivorship annotations. + * @param SurvivorshipResolver $resolver Pure golden-record resolver, reused for recompute. + * @param TrustTierResolver $trustResolver Pure trust-tier lookup + decay engine. + * @param SourceRecordResolver $sourceRecordResolver Mode-aware source-record resolver (embedded | reverseFk). + * @param IEventDispatcher $eventDispatcher Dispatcher used to fire `ObjectsMergedEvent`. + * @param LoggerInterface $logger PSR logger. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.1 + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#3.1 + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly SchemaMapper $schemaMapper, + private readonly SurvivorshipResolver $resolver, + private readonly TrustTierResolver $trustResolver, + private readonly SourceRecordResolver $sourceRecordResolver, + private readonly IEventDispatcher $eventDispatcher, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Preview a merge with NO side effects: no object, mergeOperation, or + * event is written. Returns the projected survivor golden record + + * provenance (via `SurvivorshipResolver` over the union of both objects' + * linked source records) and the reversal deadline. + * + * @param string $from Uuid of the object that would be merged away. + * @param string $into Uuid of the object that would survive. + * + * @return array Preview payload. + * + * @throws RuntimeException When the uuids are equal or either object is unreadable. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.2 + */ + public function previewMerge(string $from, string $into): array + { + if ($from === $into) { + throw new RuntimeException('Cannot merge an object into itself.'); + } + + $fromObject = $this->loadReadable(uuid: $from); + $intoObject = $this->loadReadable(uuid: $into); + + $schema = $this->loadSchema(object: $intoObject); + $config = $this->getMergeConfig(schema: $schema); + + $resolution = $this->recomputeSurvivor( + fromObject: $fromObject, + intoObject: $intoObject, + schema: $schema, + config: $config + ); + + $now = new DateTimeImmutable(); + + return [ + 'from' => $from, + 'into' => $into, + 'postMergeGoldenRecord' => $resolution['goldenRecord'], + 'attributeProvenance' => $resolution['attributeProvenance'], + 'reversalDeadline' => $this->reversalDeadline(mergedAt: $now->format(DATE_ATOM), config: $config), + ]; + }//end previewMerge() + + /** + * Execute a merge atomically (server-authoritative): snapshot both + * objects, relink the losing object's source records onto the survivor, + * recompute the survivor via `SurvivorshipResolver`, flip statuses, + * persist a `mergeOperation` row, and dispatch `ObjectsMergedEvent`. + * + * @param string $from Uuid of the object being merged away. + * @param string $into Uuid of the surviving object. + * @param string $reason Steward-supplied merge reason. + * @param string $mergedBy Acting user uid. + * + * @return array The persisted `mergeOperation` row. + * + * @throws RuntimeException When self-merge, already-merged, or the survivor is not active. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) One atomic unit of work + * (snapshot -> relink -> recompute -> status flip -> persist -> event); + * splitting it would scatter a single server-authoritative transaction. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.3 + */ + public function executeMerge(string $from, string $into, string $reason, string $mergedBy): array + { + if ($from === $into) { + throw new RuntimeException('Cannot merge an object into itself.'); + } + + $fromObject = $this->loadReadable(uuid: $from); + $intoObject = $this->loadReadable(uuid: $into); + + $schema = $this->loadSchema(object: $intoObject); + $config = $this->getMergeConfig(schema: $schema); + + $statusField = (string) ($config['statusField'] ?? self::DEFAULT_STATUS_FIELD); + $survivorStatus = (string) ($config['survivorStatus'] ?? self::DEFAULT_SURVIVOR_STATUS); + $mergedStatus = (string) ($config['mergedStatus'] ?? self::DEFAULT_MERGED_STATUS); + + $fromData = ($fromObject->getObject() ?? []); + $intoData = ($intoObject->getObject() ?? []); + + if ((string) ($fromData[$statusField] ?? '') === $mergedStatus) { + throw new RuntimeException('Source object has already been merged into another object.'); + } + + if ((string) ($intoData[$statusField] ?? $survivorStatus) !== $survivorStatus) { + throw new RuntimeException('Target object is not active and cannot receive a merge.'); + } + + $snapshot = $this->buildSnapshot(from: $fromObject, into: $intoObject); + $survivorshipConfig = $this->getSurvivorshipConfig(schema: $schema); + + // 1. Relink the losing object's source records onto the survivor. + // Reverse-FK: rewrite each losing source object's back-reference to the + // survivor uuid (persisted), recording the moves for reversal. Embedded: + // merge the loser's `sourceLinkField` array onto the survivor payload. + $isReverseFk = $this->sourceRecordResolver->isReverseFk(config: $survivorshipConfig); + $relinkedCount = 0; + if ($isReverseFk === true) { + $reverseFkMoves = $this->relinkReverseFk( + fromUuid: $from, + intoUuid: $into, + config: $survivorshipConfig, + register: (string) $fromObject->getRegister() + ); + $snapshot['reverseFkMoves'] = $reverseFkMoves; + $relinkedCount = count($reverseFkMoves); + } + + if ($isReverseFk === false) { + $sourceLinkField = (string) ($config['sourceLinkField'] ?? ''); + $relinked = $this->relinkSourceRecords( + fromData: $fromData, + intoData: $intoData, + sourceLinkField: $sourceLinkField + ); + $intoData = $relinked['intoData']; + $relinkedCount = $relinked['count']; + } + + // 2. Mark the losing object as merged. + $fromData[$statusField] = $mergedStatus; + $fromObject->setObject($fromData); + $this->objectService->saveObject( + object: $fromObject, + register: $fromObject->getRegister(), + schema: $fromObject->getSchema(), + uuid: $fromObject->getUuid() + ); + + // 3. Recompute the survivor over the combined source set, mark it active, persist. + $resolution = $this->recomputeSurvivor( + fromObject: $fromObject, + intoObject: $intoObject, + schema: $schema, + config: $config, + intoDataOverride: $intoData + ); + + $goldenField = (string) ($survivorshipConfig['goldenRecordField'] ?? 'goldenRecord'); + $provenanceField = (string) ($survivorshipConfig['provenanceField'] ?? 'attributeProvenance'); + + $intoData[$goldenField] = $resolution['goldenRecord']; + $intoData[$provenanceField] = $resolution['attributeProvenance']; + $intoData[$statusField] = $survivorStatus; + $intoObject->setObject($intoData); + $savedInto = $this->objectService->saveObject( + object: $intoObject, + register: $intoObject->getRegister(), + schema: $intoObject->getSchema(), + uuid: $intoObject->getUuid() + ); + + // 4. Persist the merge-operation with the pre-merge snapshot. + $now = new DateTimeImmutable(); + $mergeOperation = [ + 'mergedIntoUuid' => $into, + 'mergedFromUuids' => [$from], + 'reason' => $reason, + 'preMergeSnapshot' => $snapshot, + 'reversible' => true, + 'mergedAt' => $now->format(DATE_ATOM), + ]; + $savedOperation = $this->objectService->saveObject( + object: $mergeOperation, + register: self::MERGE_REGISTER, + schema: self::MERGE_SCHEMA + ); + $operationData = ($savedOperation->getObject() ?? []); + $operationData['id'] = $savedOperation->getUuid(); + + $this->logger->info( + 'OpenRegister MDM: merge executed', + ['from' => $from, 'into' => $into, 'relinked' => $relinkedCount, 'by' => $mergedBy] + ); + + // 5. Dispatch the propagation event (no app-specific sync queue). + $this->eventDispatcher->dispatchTyped( + new ObjectsMergedEvent( + survivorUuid: (string) $savedInto->getUuid(), + mergedFromUuids: [$from], + mergeOperationId: (string) $savedOperation->getUuid(), + isReversal: false + ) + ); + + return $operationData; + }//end executeMerge() + + /** + * Reverse a merge within the reversal window: restore both objects from + * `preMergeSnapshot` (golden record, provenance, status, source links), + * mark the operation reversed, and dispatch a reversal `ObjectsMergedEvent`. + * Rejected outside the window with no mutation. + * + * @param string $mergeOperationId Uuid of the `mergeOperation` row. + * @param string $reversedBy Acting user uid. + * + * @return array The updated `mergeOperation` row. + * + * @throws RuntimeException When the operation is missing or no longer reversible. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.4 + */ + public function reverseMerge(string $mergeOperationId, string $reversedBy): array + { + $operationEntity = $this->objectService->find(id: $mergeOperationId, schema: self::MERGE_SCHEMA); + if ($operationEntity === null) { + throw new RuntimeException('Merge operation not found.'); + } + + $operation = ($operationEntity->getObject() ?? []); + if ($this->isReversible(operation: $operation) === false) { + throw new RuntimeException('Reversal window has expired.'); + } + + $snapshot = ($operation['preMergeSnapshot'] ?? []); + if (is_array($snapshot) === false || empty($snapshot['objects']) === true) { + throw new RuntimeException('Merge operation has no restorable snapshot.'); + } + + // 1. Reverse-FK: restore each moved source object's back-reference to + // its pre-merge master FIRST, so restoring the master payloads below + // recomputes each golden record over the correct (restored) source set. + foreach (($snapshot['reverseFkMoves'] ?? []) as $move) { + if (is_array($move) === true) { + $this->restoreReverseFkMove(move: $move); + } + } + + // 2. Restore each object's golden record, provenance and status (best-effort). + foreach ($snapshot['objects'] as $uuid => $state) { + $this->restoreObjectState(uuid: (string) $uuid, state: $state); + } + + // 3. Restore embedded source-record linkages (best-effort — skip unresolvable records). + foreach (($snapshot['sourceLinks'] ?? []) as $sourceUuid => $link) { + $this->restoreSourceLink(sourceUuid: (string) $sourceUuid, link: $link); + } + + // 3. Mark the operation reversed. + $now = new DateTimeImmutable(); + $operation['reversedAt'] = $now->format(DATE_ATOM); + $operation['reversedBy'] = $reversedBy; + $operation['reversible'] = false; + $operationEntity->setObject($operation); + $savedOperation = $this->objectService->saveObject( + object: $operationEntity, + register: self::MERGE_REGISTER, + schema: self::MERGE_SCHEMA, + uuid: $operationEntity->getUuid() + ); + + $this->logger->info( + 'OpenRegister MDM: merge reversed', + ['operation' => $mergeOperationId, 'by' => $reversedBy] + ); + + $this->eventDispatcher->dispatchTyped( + new ObjectsMergedEvent( + survivorUuid: (string) ($operation['mergedIntoUuid'] ?? ''), + mergedFromUuids: (array) ($operation['mergedFromUuids'] ?? []), + mergeOperationId: $mergeOperationId, + isReversal: true + ) + ); + + $result = ($savedOperation->getObject() ?? []); + $result['id'] = $savedOperation->getUuid(); + + return $result; + }//end reverseMerge() + + /** + * Determine whether a `mergeOperation` is still reversible: `reversible` + * is true, `reversedAt` is unset, and the elapsed time since `mergedAt` + * does not exceed `reversalWindowDays`. + * + * @param array $operation The mergeOperation row. + * @param string|null $asOf As-of timestamp (null = now). + * + * @return bool True when reversible and within the window. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.4 + */ + public function isReversible(array $operation, ?string $asOf=null): bool + { + if (($operation['reversible'] ?? false) !== true) { + return false; + } + + if (($operation['reversedAt'] ?? '') !== '' && ($operation['reversedAt'] ?? null) !== null) { + return false; + } + + $mergedAt = (string) ($operation['mergedAt'] ?? ''); + if ($mergedAt === '') { + return false; + } + + $windowDays = self::DEFAULT_REVERSAL_WINDOW_DAYS; + if (is_numeric($operation['reversalWindowDays'] ?? null) === true) { + $windowDays = (int) $operation['reversalWindowDays']; + } + + $now = ($asOf ?? (new DateTimeImmutable())->format(DATE_ATOM)); + + try { + $merged = new DateTimeImmutable($mergedAt); + $current = new DateTimeImmutable($now); + $deadline = $merged->add(new DateInterval('P'.$windowDays.'D')); + } catch (Exception $e) { + return false; + } + + return $current <= $deadline; + }//end isReversible() + + /** + * Build the pre-merge snapshot for both objects (pure — no I/O beyond the + * data already resolved on the passed-in entities): golden record, + * provenance, status, and the losing object's linked source-record owner. + * + * @param ObjectEntity $from The object being merged away. + * @param ObjectEntity $into The surviving object. + * + * @return array The snapshot. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.1 + */ + public function buildSnapshot(ObjectEntity $from, ObjectEntity $into): array + { + $fromUuid = (string) $from->getUuid(); + $intoUuid = (string) $into->getUuid(); + + $objects = []; + foreach ([$fromUuid => $from, $intoUuid => $into] as $uuid => $entity) { + $data = ($entity->getObject() ?? []); + $objects[$uuid] = $data; + } + + $schema = $this->loadSchema(object: $from); + $config = $this->getMergeConfig(schema: $schema); + $sourceLinkField = (string) ($config['sourceLinkField'] ?? ''); + + $sourceLinks = []; + if ($sourceLinkField !== '') { + $fromData = ($from->getObject() ?? []); + $raw = ($fromData[$sourceLinkField] ?? []); + if (is_array($raw) === true) { + foreach ($raw as $entry) { + $sourceUuid = $this->sourceUuidOf(entry: $entry); + if ($sourceUuid !== '') { + $sourceLinks[$sourceUuid] = $fromUuid; + } + } + } + } + + return ['objects' => $objects, 'sourceLinks' => $sourceLinks]; + }//end buildSnapshot() + + /** + * Compute the reversal deadline from a merge timestamp using the + * annotation's `reversalWindowDays` (falling back to the service + * constant), returned as an ISO date string. + * + * @param string $mergedAt Merge timestamp. + * @param array $config `x-openregister-merge` annotation. + * + * @return string Reversal deadline (ISO date), or empty on parse error. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.1 + */ + public function reversalDeadline(string $mergedAt, array $config=[]): string + { + $windowDays = self::DEFAULT_REVERSAL_WINDOW_DAYS; + if (is_numeric($config['reversalWindowDays'] ?? null) === true) { + $windowDays = (int) $config['reversalWindowDays']; + } + + try { + $merged = new DateTimeImmutable($mergedAt); + } catch (Exception $e) { + return ''; + } + + return $merged->add(new DateInterval('P'.$windowDays.'D'))->format('Y-m-d'); + }//end reversalDeadline() + + /** + * Relink the losing object's source records onto the survivor's + * `sourceLinkField`, returning the updated survivor payload + count. + * + * @param array $fromData Losing object's payload. + * @param array $intoData Surviving object's payload. + * @param string $sourceLinkField Field holding linked source records. + * + * @return array{intoData: array, count: int} + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.3 + */ + private function relinkSourceRecords(array $fromData, array $intoData, string $sourceLinkField): array + { + if ($sourceLinkField === '') { + return ['intoData' => $intoData, 'count' => 0]; + } + + $fromSources = ($fromData[$sourceLinkField] ?? []); + if (is_array($fromSources) === false) { + return ['intoData' => $intoData, 'count' => 0]; + } + + $intoSources = ($intoData[$sourceLinkField] ?? []); + if (is_array($intoSources) === false) { + $intoSources = []; + } + + $merged = array_values(array_unique(array_merge($intoSources, $fromSources), SORT_REGULAR)); + $intoData[$sourceLinkField] = $merged; + + return ['intoData' => $intoData, 'count' => count($fromSources)]; + }//end relinkSourceRecords() + + /** + * Reverse-FK relink: rewrite each of the losing master's source objects' + * back-reference field to the surviving master's uuid (a persisted write + * per source object), returning the list of moves for the reversal + * snapshot. Unresolvable/failed writes are logged and skipped. + * + * @param string $fromUuid Losing master uuid. + * @param string $intoUuid Surviving master uuid. + * @param array $config Survivorship config (carrying the reverse-FK `sourceLink`). + * @param string $register Master's register ref — the source register when the + * annotation omits one (the magic-table query needs it). + * + * @return array + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#3.2 + */ + private function relinkReverseFk(string $fromUuid, string $intoUuid, array $config, string $register): array + { + $descriptor = $this->sourceRecordResolver->reverseFkDescriptor(config: $config); + if ($descriptor === null) { + return []; + } + + $referenceField = $descriptor['referenceField']; + $sourceRegister = $descriptor['sourceRegister']; + if ($sourceRegister === '') { + $sourceRegister = $register; + } + + $filters = [ + 'schema' => $this->sourceRecordResolver->schemaQueryFilter(ref: $descriptor['sourceSchema']), + $referenceField => $fromUuid, + ]; + if ($sourceRegister !== '') { + $filters['register'] = $sourceRegister; + } + + try { + $objects = $this->objectService->findAll(['filters' => $filters], _rbac: true, _multitenancy: true); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Reverse-FK relink could not query sources for master "%s": %s', $fromUuid, $e->getMessage()) + ); + return []; + } + + $moves = []; + foreach ($objects as $object) { + if (($object instanceof ObjectEntity) === false) { + continue; + } + + $data = ($object->getObject() ?? []); + if ((string) ($data[$referenceField] ?? '') !== $fromUuid) { + continue; + } + + $data = self::normaliseRoundTripDates(data: $data); + $data[$referenceField] = $intoUuid; + $object->setObject($data); + + try { + $this->objectService->saveObject( + object: $object, + register: $object->getRegister(), + schema: $object->getSchema(), + uuid: $object->getUuid() + ); + $moves[] = [ + 'sourceUuid' => (string) $object->getUuid(), + 'referenceField' => $referenceField, + 'prior' => $fromUuid, + ]; + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Reverse-FK relink could not move source "%s": %s', (string) $object->getUuid(), $e->getMessage()) + ); + } + }//end foreach + + return $moves; + }//end relinkReverseFk() + + /** + * Normalise OpenRegister's stored `YYYY-MM-DD HH:MM:SS` date-time strings + * back to ISO-8601 (`...T...+00:00`) before an object is re-saved. OR + * stores date-time values in a space-separated form that its OWN schema + * validation later rejects against the `date-time` format on a + * getObject → mutate → saveObject round-trip; a reverse-FK relink / recompute + * only mutates one field but re-persists the whole object, so it must + * normalise the untouched date fields or the save fails. Recurses into + * nested arrays; leaves all non-date strings untouched. + * + * @param array $data Object payload. + * + * @return array Payload with space-format dates converted to ISO. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#3.2 + */ + public static function normaliseRoundTripDates(array $data): array + { + foreach ($data as $key => $value) { + if (is_array($value) === true) { + $data[$key] = self::normaliseRoundTripDates(data: $value); + continue; + } + + if (is_string($value) === true + && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value) === 1 + ) { + $data[$key] = str_replace(' ', 'T', $value).'+00:00'; + } + } + + return $data; + }//end normaliseRoundTripDates() + + /** + * Recompute the survivor's golden record over the union of both objects' + * linked source records, via the pure `SurvivorshipResolver`. + * + * @param ObjectEntity $fromObject Losing object. + * @param ObjectEntity $intoObject Surviving object. + * @param Schema|null $schema Resolved schema (for survivorship config + trust rows). + * @param array $config `x-openregister-merge` annotation. + * @param array|null $intoDataOverride Already-relinked survivor payload, when available. + * + * @return array{goldenRecord: array, attributeProvenance: array} + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.2 + */ + private function recomputeSurvivor( + ObjectEntity $fromObject, + ObjectEntity $intoObject, + ?Schema $schema, + array $config, + ?array $intoDataOverride=null + ): array { + $survivorshipConfig = $this->getSurvivorshipConfig(schema: $schema); + + $intoData = ($intoDataOverride ?? ($intoObject->getObject() ?? [])); + $fromData = ($fromObject->getObject() ?? []); + + // Resolve the union of both masters' source records via the mode-aware + // resolver. Embedded: reads each payload's `sourceLinkField`. Reverse-FK: + // queries each master's uuid — this is correct both for preview (the + // loser's sources still point at the loser) and after relink at execute + // time (they now point at the survivor; the loser query then yields + // none). A single-valued back-reference means no source is double-counted. + $sources = array_merge( + $this->sourceRecordResolver->resolveSources( + masterData: $intoData, + masterUuid: (string) $intoObject->getUuid(), + config: $survivorshipConfig, + masterRegister: (string) $intoObject->getRegister() + ), + $this->sourceRecordResolver->resolveSources( + masterData: $fromData, + masterUuid: (string) $fromObject->getUuid(), + config: $survivorshipConfig, + masterRegister: (string) $fromObject->getRegister() + ) + ); + + $entityType = (string) ( + $config['entityType'] ?? $survivorshipConfig['entityType'] ?? ($schema?->getSlug() ?? '') + ); + + $trustRows = $this->loadTrustRows(entityType: $entityType); + + return $this->resolver->resolveGoldenRecord( + entityType: $entityType, + sourceRecords: $sources, + config: $survivorshipConfig, + trustRows: $trustRows, + trustResolver: $this->trustResolver, + asOf: new DateTimeImmutable() + ); + }//end recomputeSurvivor() + + /** + * Best-effort restoration of one object's snapshot state. Unresolvable + * objects are skipped rather than aborting the whole reversal. + * + * @param string $uuid Object uuid to restore. + * @param array $state Snapshot state (full pre-merge payload). + * + * @return void + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.4 + */ + private function restoreObjectState(string $uuid, array $state): void + { + try { + $entity = $this->objectService->find(id: $uuid, _rbac: true, _multitenancy: true); + if ($entity === null) { + return; + } + + $entity->setObject($state); + $this->objectService->saveObject( + object: $entity, + register: $entity->getRegister(), + schema: $entity->getSchema(), + uuid: $entity->getUuid() + ); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Merge reversal could not restore object "%s": %s', $uuid, $e->getMessage()) + ); + } + }//end restoreObjectState() + + /** + * Best-effort restoration of one source-record's owner link. Unresolvable + * or moved records are skipped rather than aborting the reversal. + * + * @param string $sourceUuid Source-record uuid. + * @param mixed $link Snapshot owner reference (the prior owner uuid). + * + * @return void + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.4 + */ + private function restoreSourceLink(string $sourceUuid, $link): void + { + try { + $entity = $this->objectService->find(id: $sourceUuid, _rbac: true, _multitenancy: true); + if ($entity === null) { + return; + } + + $this->logger->debug( + sprintf('Merge reversal: source "%s" prior owner "%s" (best-effort log only).', $sourceUuid, (string) $link) + ); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Merge reversal could not resolve source record "%s": %s', $sourceUuid, $e->getMessage()) + ); + } + }//end restoreSourceLink() + + /** + * Reverse-FK reversal: rewrite one moved source object's back-reference + * field back to its pre-merge master uuid. Best-effort — an unresolvable + * source or a failed write is logged and skipped rather than aborting the + * whole reversal. + * + * @param array{sourceUuid?: string, referenceField?: string, prior?: string} $move Recorded move. + * + * @return void + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#3.3 + */ + private function restoreReverseFkMove(array $move): void + { + $sourceUuid = (string) ($move['sourceUuid'] ?? ''); + $referenceField = (string) ($move['referenceField'] ?? ''); + $prior = (string) ($move['prior'] ?? ''); + if ($sourceUuid === '' || $referenceField === '') { + return; + } + + try { + $entity = $this->objectService->find(id: $sourceUuid, _rbac: true, _multitenancy: true); + if ($entity === null) { + return; + } + + $data = self::normaliseRoundTripDates(data: ($entity->getObject() ?? [])); + $data[$referenceField] = $prior; + $entity->setObject($data); + $this->objectService->saveObject( + object: $entity, + register: $entity->getRegister(), + schema: $entity->getSchema(), + uuid: $entity->getUuid() + ); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Merge reversal could not restore source back-reference "%s": %s', $sourceUuid, $e->getMessage()) + ); + } + }//end restoreReverseFkMove() + + /** + * Resolve a source-record entry's own uuid (for the snapshot's + * `sourceLinks` map), from either an embedded record or a plain + * uuid/id reference string. + * + * @param mixed $entry Source-record entry. + * + * @return string Resolved uuid, or empty when unresolvable. + */ + private function sourceUuidOf($entry): string + { + if (is_string($entry) === true) { + return $entry; + } + + if (is_array($entry) === true) { + return (string) ($entry['id'] ?? ($entry['uuid'] ?? '')); + } + + return ''; + }//end sourceUuidOf() + + /** + * Load a readable object by uuid, RBAC + tenant scoped. + * + * @param string $uuid Object uuid. + * + * @return ObjectEntity The resolved object. + * + * @throws RuntimeException When the object cannot be found/read. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.2 + */ + private function loadReadable(string $uuid): ObjectEntity + { + try { + $object = $this->objectService->find(id: $uuid, _rbac: true, _multitenancy: true); + } catch (Throwable $e) { + throw new RuntimeException(sprintf('Object "%s" is not readable: %s', $uuid, $e->getMessage())); + } + + if ($object === null) { + throw new RuntimeException(sprintf('Object "%s" was not found.', $uuid)); + } + + return $object; + }//end loadReadable() + + /** + * Look up the schema referenced by an object instance. + * + * @param ObjectEntity $object Object whose schema reference to resolve. + * + * @return Schema|null Resolved schema, or null on lookup failure. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.2 + */ + private function loadSchema(ObjectEntity $object): ?Schema + { + $ref = $object->getSchema(); + if ($ref === null || $ref === '') { + return null; + } + + try { + return $this->schemaMapper->find($ref, _multitenancy: false); + } catch (Throwable) { + return null; + } + }//end loadSchema() + + /** + * Read the `x-openregister-merge` configuration block. + * + * @param Schema|null $schema Schema to inspect. + * + * @return array Merge config (empty array when absent — callers fall back to defaults). + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.1 + */ + private function getMergeConfig(?Schema $schema): array + { + if ($schema === null) { + return []; + } + + $config = ($schema->getConfiguration() ?? []); + $value = ($config['x-openregister-merge'] ?? null); + if (is_array($value) === true) { + return $value; + } + + return []; + }//end getMergeConfig() + + /** + * Read the `x-openregister-survivorship` configuration block. + * + * @param Schema|null $schema Schema to inspect. + * + * @return array Survivorship config (empty array when absent). + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.2 + */ + private function getSurvivorshipConfig(?Schema $schema): array + { + if ($schema === null) { + return []; + } + + $config = ($schema->getConfiguration() ?? []); + $value = ($config['x-openregister-survivorship'] ?? null); + if (is_array($value) === true) { + return $value; + } + + return []; + }//end getSurvivorshipConfig() + + /** + * Load the candidate trust-configuration rows for an entity type via the + * OR-owned `trustConfiguration` register (RBAC + tenant scoped). + * + * @param string $entityType Entity type to scope the lookup. + * + * @return array> Trust-configuration rows. + * + * @spec openspec/changes/mdm-merge-engine/tasks.md#4.2 + */ + private function loadTrustRows(string $entityType): array + { + if ($entityType === '') { + return []; + } + + try { + $objects = $this->objectService->findAll( + [ + 'filters' => [ + 'schema' => self::TRUST_CONFIGURATION_SCHEMA, + 'entityType' => $entityType, + ], + ], + _rbac: true, + _multitenancy: true + ); + } catch (Throwable) { + return []; + } + + $rows = []; + foreach ($objects as $object) { + if ($object instanceof ObjectEntity) { + $rows[] = ($object->getObject() ?? []); + } + } + + return $rows; + }//end loadTrustRows() +}//end class diff --git a/lib/Service/MetricsService.php b/lib/Service/MetricsService.php index 8e0a8c4b9c..f504a85809 100644 --- a/lib/Service/MetricsService.php +++ b/lib/Service/MetricsService.php @@ -105,6 +105,27 @@ class MetricsService */ public const METRIC_CHAT_MESSAGE = 'chat_message'; + /** + * Metric type constant for object creation (CRUD counter). + * + * @var string + */ + public const METRIC_OBJECT_CREATED = 'object_created'; + + /** + * Metric type constant for object update (CRUD counter). + * + * @var string + */ + public const METRIC_OBJECT_UPDATED = 'object_updated'; + + /** + * Metric type constant for object deletion (CRUD counter). + * + * @var string + */ + public const METRIC_OBJECT_DELETED = 'object_deleted'; + /** * Constructor * @@ -204,7 +225,7 @@ public function recordMetric( * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-27 + * @spec openspec/specs/production-observability/spec.md */ public function getFilesProcessedPerDay(int $days=30): array { @@ -252,7 +273,7 @@ public function getFilesProcessedPerDay(int $days=30): array * @psalm-return array{total: int, successful: int, failed: int, * success_rate: float, estimated_cost_usd: float, period_days: int} * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-28 + * @spec openspec/specs/production-observability/spec.md */ public function getEmbeddingStats(int $days=30): array { @@ -314,7 +335,7 @@ public function getEmbeddingStats(int $days=30): array * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-29 + * @spec openspec/specs/production-observability/spec.md */ public function getSearchLatencyStats(int $days=7): array { @@ -388,7 +409,7 @@ public function getSearchLatencyStats(int $days=7): array * current_storage_bytes: int, current_storage_mb: float, * avg_vectors_per_day: float, period_days: int} * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-30 + * @spec openspec/specs/production-observability/spec.md */ public function getStorageGrowth(int $days=30): array { @@ -459,7 +480,7 @@ public function getStorageGrowth(int $days=30): array * current_storage_bytes: int, current_storage_mb: float, * avg_vectors_per_day: float, period_days: int}} * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-31 + * @spec openspec/specs/production-observability/spec.md */ public function getDashboardMetrics(): array { @@ -552,7 +573,7 @@ private function encodeMetadata(?array $metadata): string * * @return float Success rate as percentage (0-100), rounded to 2 decimal places * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-32 + * @spec openspec/specs/production-observability/spec.md */ private function calculateSuccessRate(int $total, int $successful): float { @@ -579,7 +600,7 @@ private function calculateSuccessRate(int $total, int $successful): float * * @psalm-suppress MixedArgument * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-33 + * @spec openspec/specs/production-observability/spec.md */ private function roundAverageMs($avgMs): float { @@ -603,7 +624,7 @@ private function roundAverageMs($avgMs): float * * @return float Average vectors per day, rounded to 2 decimal places * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-34 + * @spec openspec/specs/production-observability/spec.md */ private function calculateAverageVectorsPerDay(array $growthData): float { diff --git a/lib/Service/MigrationPack/MappingEngine.php b/lib/Service/MigrationPack/MappingEngine.php new file mode 100644 index 0000000000..2f0da78812 --- /dev/null +++ b/lib/Service/MigrationPack/MappingEngine.php @@ -0,0 +1,359 @@ + + * value, with `id` recognised by the existing update-by-id convention), so + * the single write path (`ObjectService::saveObjects()`/`saveObject()`) is + * unchanged. + * + * Literal-leak guard (fleet lesson — a transform/template reference that + * doesn't resolve must ERROR the row, never pass the literal through): the + * `lookup` transform errors the row when the source value is present but + * has no entry in the map and no `default` is configured, rather than + * silently passing the raw, unmapped source value through to the target + * schema property. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\MigrationPack + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\MigrationPack; + +use DateTime; + +/** + * Maps one source row (CSV row / Excel row / decoded JSON object) onto a set + * of target schema-property values, per a migration-pack definition. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ +class MappingEngine +{ + /** + * Apply a pack definition to one source row. + * + * @param array $pack The validated pack definition (decoded JSON). + * @param array $sourceRow The parsed source row (flat for CSV/Excel, possibly nested for JSON). + * @param int $rowNumber 1-based row number, used only to label errors. + * + * @return array{data: array, errors: list} + * + * @spec openspec/specs/migration-mapping-packs/spec.md#mapping-engine + */ + public function mapRow(array $pack, array $sourceRow, int $rowNumber): array + { + $data = $pack['defaults'] ?? []; + $errors = []; + + foreach (($pack['fieldMappings'] ?? []) as $mapping) { + $source = (string) ($mapping['source'] ?? ''); + $target = (string) ($mapping['target'] ?? ''); + $required = ($mapping['required'] ?? false) === true; + $transform = $mapping['transform'] ?? null; + $transformId = null; + if (is_array($transform) === true) { + $transformId = ($transform['type'] ?? null); + } + + $rawValue = $this->resolveSource(row: $sourceRow, pointer: $source); + $isEmpty = ($rawValue === null || $rawValue === ''); + + if ($required === true && $isEmpty === true) { + $errors[] = [ + 'row' => $rowNumber, + 'source' => $source, + 'target' => $target, + 'transform' => $transformId, + 'message' => sprintf('Required source field "%s" is missing or empty', $source), + ]; + continue; + } + + // A `const` transform always applies, regardless of the source value. + // Every other transform is skipped (leaving any seeded default in + // place) when the source is empty and the mapping is optional — + // there is nothing to map, and nothing to error. + if ($isEmpty === true && $transformId !== 'const') { + continue; + } + + $result = $this->applyTransform( + value: $rawValue, + transform: $transform, + sourceRow: $sourceRow + ); + + if ($result['error'] !== null) { + $errors[] = [ + 'row' => $rowNumber, + 'source' => $source, + 'target' => $target, + 'transform' => $transformId, + 'message' => $result['error'], + ]; + continue; + } + + $data[$target] = $result['value']; + }//end foreach + + $data = $this->applyIdStrategy(pack: $pack, sourceRow: $sourceRow, data: $data); + + return [ + 'data' => $data, + 'errors' => $errors, + ]; + }//end mapRow() + + /** + * Whether a given (1-based) row number is listed in the pack's `skipRows`. + * + * @param array $pack The pack definition. + * @param int $rowNumber 1-based row number. + * + * @return bool + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function isRowSkipped(array $pack, int $rowNumber): bool + { + $skipRows = $pack['skipRows'] ?? []; + return in_array($rowNumber, $skipRows, true); + }//end isRowSkipped() + + /** + * Resolve the id/uuid target from `idStrategy`, mutating `data['id']` + * when the strategy is `sourceField` and a value is present. The + * `generate` strategy is a no-op here — leaving `data['id']` unset lets + * the existing import pipeline treat the row as a create, exactly as it + * already does for CSV/JSON rows with no id column. + * + * @param array $pack The pack definition. + * @param array $sourceRow The source row. + * @param array $data The mapped target data so far. + * + * @return array The (possibly id-augmented) target data. + */ + private function applyIdStrategy(array $pack, array $sourceRow, array $data): array + { + $idStrategy = $pack['idStrategy'] ?? ['type' => 'generate']; + if (($idStrategy['type'] ?? 'generate') !== 'sourceField') { + return $data; + } + + $idValue = $this->resolveSource(row: $sourceRow, pointer: (string) ($idStrategy['field'] ?? '')); + if ($idValue !== null && $idValue !== '') { + $data['id'] = (string) $idValue; + } + + return $data; + }//end applyIdStrategy() + + /** + * Resolve a source value from a row, given either a flat key or a + * JSON-Pointer-style `/a/b/c` path. + * + * @param array $row The source row. + * @param string $pointer A flat key or a leading-`/` pointer path. + * + * @return mixed The resolved value, or null when not found. + */ + private function resolveSource(array $row, string $pointer) + { + if ($pointer === '') { + return null; + } + + if ($pointer[0] !== '/') { + return $row[$pointer] ?? null; + } + + $segments = explode('/', ltrim($pointer, '/')); + $cursor = $row; + foreach ($segments as $segment) { + $segment = str_replace(['~1', '~0'], ['/', '~'], $segment); + if (is_array($cursor) === false || array_key_exists($segment, $cursor) === false) { + return null; + } + + $cursor = $cursor[$segment]; + } + + return $cursor; + }//end resolveSource() + + /** + * Apply one transform to a resolved source value. + * + * @param mixed $value The resolved source value (never null/'' — callers filter + * that). + * @param array|null $transform The transform block, or null for identity passthrough. + * @param array $sourceRow The full source row (needed by `concat` to resolve extra fields). + * + * @return array{value: mixed, error: ?string} + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) One branch per transform type. + */ + private function applyTransform($value, ?array $transform, array $sourceRow): array + { + if ($transform === null) { + return ['value' => $value, 'error' => null]; + } + + switch ($transform['type'] ?? null) { + case 'trim': + $stringValue = (string) $value; + if (is_string($value) === true) { + $stringValue = $value; + } + return ['value' => trim($stringValue), 'error' => null]; + + case 'date': + return $this->applyDateTransform(value: $value, transform: $transform); + + case 'bool-map': + return $this->applyMapTransform(value: $value, transform: $transform, coerceBool: true); + + case 'lookup': + return $this->applyMapTransform(value: $value, transform: $transform, coerceBool: false); + + case 'concat': + return $this->applyConcatTransform(value: $value, transform: $transform, sourceRow: $sourceRow); + + case 'const': + return ['value' => ($transform['value'] ?? null), 'error' => null]; + + default: + return ['value' => null, 'error' => 'Unknown transform type "'.(string) ($transform['type'] ?? '').'"']; + }//end switch + }//end applyTransform() + + /** + * `date` transform: parse the source value with `sourceFormat` (or a + * best-effort `DateTime` parse when omitted) and re-emit it as `targetFormat` + * (default `Y-m-d`). + * + * @param mixed $value The resolved source value. + * @param array $transform The transform block. + * + * @return array{value: mixed, error: ?string} + * + * @SuppressWarnings(PHPMD.StaticAccess) DateTime::createFromFormat is the standard PHP idiom for a strict-format parse. + */ + private function applyDateTransform($value, array $transform): array + { + $sourceFormat = $transform['sourceFormat'] ?? null; + $targetFormat = $transform['targetFormat'] ?? 'Y-m-d'; + $stringValue = (string) $value; + + try { + $date = null; + if (is_string($sourceFormat) === true && $sourceFormat !== '') { + $date = DateTime::createFromFormat($sourceFormat, $stringValue); + if ($date === false) { + return [ + 'value' => null, + 'error' => sprintf('Could not parse date "%s" with format "%s"', $stringValue, $sourceFormat), + ]; + } + } + + if ($date === null) { + $date = new DateTime($stringValue); + } + } catch (\Throwable $e) { + return ['value' => null, 'error' => sprintf('Could not parse date "%s": %s', $stringValue, $e->getMessage())]; + } + + return ['value' => $date->format($targetFormat), 'error' => null]; + }//end applyDateTransform() + + /** + * Shared implementation for `bool-map` and `lookup` — both resolve the + * source value through a `map`, with an optional `default` and, absent a + * default, an error on an unresolved key (the literal-leak guard). + * + * @param mixed $value The resolved source value. + * @param array $transform The transform block. + * @param bool $coerceBool Whether to cast the mapped value to bool (bool-map) or return it as-is (lookup). + * + * @return array{value: mixed, error: ?string} + */ + private function applyMapTransform($value, array $transform, bool $coerceBool): array + { + $map = $transform['map'] ?? []; + $key = (string) $value; + + if (array_key_exists($key, $map) === true) { + $mapped = $map[$key]; + if ($coerceBool === true) { + $mapped = (bool) $mapped; + } + + return ['value' => $mapped, 'error' => null]; + } + + if (array_key_exists('default', $transform) === true) { + $default = $transform['default']; + if ($coerceBool === true) { + $default = (bool) $default; + } + + return ['value' => $default, 'error' => null]; + } + + // Literal-leak guard: an unresolved map key is a data-quality problem the + // migration operator must see and fix, never a value that silently passes + // through unmapped into the target schema property. + return [ + 'value' => null, + 'error' => sprintf('Value "%s" has no mapping and no default is configured', $key), + ]; + }//end applyMapTransform() + + /** + * `concat` transform: join the primary source value with 0+ additional + * source fields, using `separator` (default a single space). Additional + * fields that resolve to nothing are treated as empty strings — a + * missing *optional* extra field is not itself a literal-leak case, since + * there is no map lookup involved. + * + * @param mixed $value The resolved primary source value. + * @param array $transform The transform block. + * @param array $sourceRow The full source row. + * + * @return array{value: mixed, error: ?string} + */ + private function applyConcatTransform($value, array $transform, array $sourceRow): array + { + $separator = $transform['separator'] ?? ' '; + $parts = [(string) $value]; + + foreach (($transform['fields'] ?? []) as $extraSource) { + $extraValue = $this->resolveSource(row: $sourceRow, pointer: (string) $extraSource); + $parts[] = (string) ($extraValue ?? ''); + } + + return ['value' => implode($separator, $parts), 'error' => null]; + }//end applyConcatTransform() +}//end class diff --git a/lib/Service/MigrationPack/PackDefinitionValidator.php b/lib/Service/MigrationPack/PackDefinitionValidator.php new file mode 100644 index 0000000000..678837834f --- /dev/null +++ b/lib/Service/MigrationPack/PackDefinitionValidator.php @@ -0,0 +1,386 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\MigrationPack; + +use InvalidArgumentException; + +/** + * Structural + business-rule validator for a migration-pack JSON document. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) One small, independently-testable validate*() method + * per pack-document field keeps each check simple; the class total sums them, not any single method. + */ +class PackDefinitionValidator +{ + /** + * Source formats a pack may declare. + * + * @var string[] + */ + public const ALLOWED_SOURCE_FORMATS = ['csv', 'json', 'excel']; + + /** + * Transform types a field mapping may declare. + * + * @var string[] + */ + public const ALLOWED_TRANSFORM_TYPES = ['trim', 'date', 'bool-map', 'concat', 'lookup', 'const']; + + /** + * IdStrategy types a pack may declare. + * + * @var string[] + */ + public const ALLOWED_ID_STRATEGY_TYPES = ['sourceField', 'generate']; + + /** + * Validate a pack definition document. + * + * @param array $definition The decoded pack definition JSON. + * + * @return string[] List of validation error messages. Empty when valid. + * + * @spec openspec/specs/migration-mapping-packs/spec.md#validation + */ + public function validate(array $definition): array + { + $errors = []; + + $errors = array_merge($errors, $this->validateId(definition: $definition)); + $errors = array_merge($errors, $this->validateName(definition: $definition)); + $errors = array_merge($errors, $this->validateSourceFormat(definition: $definition)); + $errors = array_merge($errors, $this->validateVersion(definition: $definition)); + $errors = array_merge($errors, $this->validateFieldMappings(definition: $definition)); + $errors = array_merge($errors, $this->validateDefaults(definition: $definition)); + $errors = array_merge($errors, $this->validateSkipRows(definition: $definition)); + $errors = array_merge($errors, $this->validateIdStrategy(definition: $definition)); + + return $errors; + }//end validate() + + /** + * Validate and throw on the first structural problem. + * + * @param array $definition The decoded pack definition JSON. + * + * @return void + * + * @throws InvalidArgumentException When the definition is invalid. The message joins every error found. + * + * @spec openspec/specs/migration-mapping-packs/spec.md#validation + */ + public function assertValid(array $definition): void + { + $errors = $this->validate(definition: $definition); + if (empty($errors) === false) { + throw new InvalidArgumentException('Invalid migration pack definition: '.implode('; ', $errors)); + } + }//end assertValid() + + /** + * Validate the `id` field (pack slug, used as the lookup key). + * + * @param array $definition The pack definition. + * + * @return string[] + */ + private function validateId(array $definition): array + { + $id = $definition['id'] ?? null; + if (is_string($id) === false || $id === '') { + return ['"id" is required and must be a non-empty string']; + } + + if (preg_match('/^[a-z0-9][a-z0-9-]*$/', $id) !== 1) { + return ['"id" must be a lowercase slug (letters, digits, hyphens), got "'.$id.'"']; + } + + return []; + }//end validateId() + + /** + * Validate the `name` field. + * + * @param array $definition The pack definition. + * + * @return string[] + */ + private function validateName(array $definition): array + { + $name = $definition['name'] ?? null; + if (is_string($name) === false || $name === '') { + return ['"name" is required and must be a non-empty string']; + } + + return []; + }//end validateName() + + /** + * Validate the `sourceFormat` field. + * + * @param array $definition The pack definition. + * + * @return string[] + */ + private function validateSourceFormat(array $definition): array + { + $format = $definition['sourceFormat'] ?? null; + if (is_string($format) === false || in_array($format, self::ALLOWED_SOURCE_FORMATS, true) === false) { + return [ + '"sourceFormat" must be one of: '.implode(', ', self::ALLOWED_SOURCE_FORMATS) + .' (got '.var_export($format, true).')', + ]; + } + + return []; + }//end validateSourceFormat() + + /** + * Validate the `version` field (strict semver: MAJOR.MINOR.PATCH). + * + * @param array $definition The pack definition. + * + * @return string[] + */ + private function validateVersion(array $definition): array + { + $version = $definition['version'] ?? null; + if (is_string($version) === false || preg_match('/^\d+\.\d+\.\d+$/', $version) !== 1) { + return ['"version" must be a semver string (MAJOR.MINOR.PATCH), got '.var_export($version, true)]; + } + + return []; + }//end validateVersion() + + /** + * Validate the `fieldMappings` array and every entry within it. + * + * @param array $definition The pack definition. + * + * @return string[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Per-transform-type validation requires many branches. + * @SuppressWarnings(PHPMD.NPathComplexity) Per-transform-type validation requires many branches. + */ + private function validateFieldMappings(array $definition): array + { + $mappings = $definition['fieldMappings'] ?? null; + if (is_array($mappings) === false || empty($mappings) === true) { + return ['"fieldMappings" is required and must be a non-empty array']; + } + + $errors = []; + foreach ($mappings as $index => $mapping) { + $path = 'fieldMappings['.$index.']'; + if (is_array($mapping) === false) { + $errors[] = $path.' must be an object'; + continue; + } + + $source = $mapping['source'] ?? null; + if (is_string($source) === false || $source === '') { + $errors[] = $path.'.source is required and must be a non-empty string'; + } + + $target = $mapping['target'] ?? null; + if (is_string($target) === false || $target === '') { + $errors[] = $path.'.target is required and must be a non-empty string'; + } + + if (isset($mapping['required']) === true && is_bool($mapping['required']) === false) { + $errors[] = $path.'.required must be a boolean when present'; + } + + if (isset($mapping['transform']) === true) { + $errors = array_merge($errors, $this->validateTransform(transform: $mapping['transform'], path: $path.'.transform')); + } + }//end foreach + + return $errors; + }//end validateFieldMappings() + + /** + * Validate one `transform` block. + * + * @param mixed $transform The transform value to validate. + * @param string $path The error-message path prefix. + * + * @return string[] + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each transform type has its own required-field shape. + * @SuppressWarnings(PHPMD.NPathComplexity) Each transform type has its own required-field shape. + */ + private function validateTransform($transform, string $path): array + { + if (is_array($transform) === false) { + return [$path.' must be an object with a "type" key']; + } + + $type = $transform['type'] ?? null; + if (is_string($type) === false || in_array($type, self::ALLOWED_TRANSFORM_TYPES, true) === false) { + return [ + $path.'.type must be one of: '.implode(', ', self::ALLOWED_TRANSFORM_TYPES) + .' (got '.var_export($type, true).')', + ]; + } + + switch ($type) { + case 'date': + if (isset($transform['sourceFormat']) === true && is_string($transform['sourceFormat']) === false) { + return [$path.'.sourceFormat must be a string when present']; + } + + if (isset($transform['targetFormat']) === true && is_string($transform['targetFormat']) === false) { + return [$path.'.targetFormat must be a string when present']; + } + break; + + case 'bool-map': + if (is_array($transform['map'] ?? null) === false || empty($transform['map']) === true) { + return [$path.'.map is required and must be a non-empty object for a bool-map transform']; + } + break; + + case 'lookup': + if (is_array($transform['map'] ?? null) === false || empty($transform['map']) === true) { + return [$path.'.map is required and must be a non-empty object for a lookup transform']; + } + break; + + case 'concat': + if (is_array($transform['fields'] ?? null) === false || empty($transform['fields']) === true) { + return [$path.'.fields is required and must be a non-empty array for a concat transform']; + } + + foreach ($transform['fields'] as $fieldIndex => $field) { + if (is_string($field) === false || $field === '') { + return [$path.'.fields['.$fieldIndex.'] must be a non-empty string']; + } + } + break; + + case 'const': + if (array_key_exists('value', $transform) === false) { + return [$path.'.value is required for a const transform']; + } + break; + + case 'trim': + default: + // No extra fields required. + break; + }//end switch + + return []; + }//end validateTransform() + + /** + * Validate the optional `defaults` map. + * + * @param array $definition The pack definition. + * + * @return string[] + */ + private function validateDefaults(array $definition): array + { + if (isset($definition['defaults']) === false) { + return []; + } + + if (is_array($definition['defaults']) === false) { + return ['"defaults" must be an object of target-property => default value when present']; + } + + return []; + }//end validateDefaults() + + /** + * Validate the optional `skipRows` array. + * + * @param array $definition The pack definition. + * + * @return string[] + */ + private function validateSkipRows(array $definition): array + { + if (isset($definition['skipRows']) === false) { + return []; + } + + if (is_array($definition['skipRows']) === false) { + return ['"skipRows" must be an array of row numbers when present']; + } + + foreach ($definition['skipRows'] as $row) { + if (is_int($row) === false || $row < 1) { + return ['"skipRows" entries must be positive integers']; + } + } + + return []; + }//end validateSkipRows() + + /** + * Validate the required `idStrategy` block. + * + * @param array $definition The pack definition. + * + * @return string[] + */ + private function validateIdStrategy(array $definition): array + { + $idStrategy = $definition['idStrategy'] ?? null; + if (is_array($idStrategy) === false) { + return ['"idStrategy" is required and must be an object with a "type" key']; + } + + $type = $idStrategy['type'] ?? null; + if (is_string($type) === false || in_array($type, self::ALLOWED_ID_STRATEGY_TYPES, true) === false) { + return [ + 'idStrategy.type must be one of: '.implode(', ', self::ALLOWED_ID_STRATEGY_TYPES) + .' (got '.var_export($type, true).')', + ]; + } + + if ($type === 'sourceField' + && (is_string($idStrategy['field'] ?? null) === false || $idStrategy['field'] === '') + ) { + return ['idStrategy.field is required and must be a non-empty string when idStrategy.type is "sourceField"']; + } + + return []; + }//end validateIdStrategy() +}//end class diff --git a/lib/Service/MigrationPackService.php b/lib/Service/MigrationPackService.php new file mode 100644 index 0000000000..21831bc2dc --- /dev/null +++ b/lib/Service/MigrationPackService.php @@ -0,0 +1,231 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use DateTime; +use InvalidArgumentException; +use OCA\OpenRegister\Db\MigrationPack; +use OCA\OpenRegister\Db\MigrationPackMapper; +use OCA\OpenRegister\Service\MigrationPack\PackDefinitionValidator; +use OCP\AppFramework\Db\DoesNotExistException; + +/** + * Class MigrationPackService + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ +class MigrationPackService +{ + /** + * Constructor. + * + * @param MigrationPackMapper $mapper Data mapper. + * @param PackDefinitionValidator $validator Pack definition validator. + */ + public function __construct( + private readonly MigrationPackMapper $mapper, + private readonly PackDefinitionValidator $validator + ) { + }//end __construct() + + /** + * List every stored migration pack. + * + * @return MigrationPack[] + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function findAll(): array + { + return $this->mapper->findAll(); + }//end findAll() + + /** + * Find a migration pack by its numeric id. + * + * @param int $id The migration pack id. + * + * @return MigrationPack + * + * @throws DoesNotExistException When no row matches. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function find(int $id): MigrationPack + { + return $this->mapper->find(id: $id); + }//end find() + + /** + * Find a migration pack by its pack document's own `id` (slug). + * + * @param string $packSlug The pack document's own `id`. + * + * @return MigrationPack + * + * @throws DoesNotExistException When no row matches. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function findByPackSlug(string $packSlug): MigrationPack + { + return $this->mapper->findByPackSlug(packSlug: $packSlug); + }//end findByPackSlug() + + /** + * Create a migration pack from a definition document. + * + * @param array $definition The pack definition document (the JSON body). + * @param string|null $ownerUid The creating admin's uid, or null for a system/built-in seed. + * @param bool $builtin Whether to mark this row as a built-in reference pack. + * + * @return MigrationPack + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) $builtin only distinguishes admin-authored packs from the seeded reference pack. + * + * @throws InvalidArgumentException When the definition is structurally invalid or its slug already exists. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function create(array $definition, ?string $ownerUid=null, bool $builtin=false): MigrationPack + { + $this->validator->assertValid(definition: $definition); + + $slug = (string) $definition['id']; + if ($this->slugExists(packSlug: $slug) === true) { + throw new InvalidArgumentException('A migration pack with id "'.$slug.'" already exists'); + } + + $pack = new MigrationPack(); + $pack->setPackSlug($slug); + $pack->setName((string) $definition['name']); + $pack->setSourceFormat((string) $definition['sourceFormat']); + $pack->setVersion((string) $definition['version']); + $pack->setDefinition(json_encode($definition, JSON_UNESCAPED_SLASHES)); + $pack->setBuiltin($builtin); + $pack->setOwner($ownerUid); + $pack->setCreatedAt(new DateTime()); + $pack->setUpdatedAt(new DateTime()); + + return $this->mapper->insert(entity: $pack); + }//end create() + + /** + * Update an existing migration pack's definition. + * + * @param int $id The migration pack id. + * @param array $definition The replacement pack definition document. + * + * @return MigrationPack + * + * @throws DoesNotExistException When no row matches. + * @throws InvalidArgumentException When the definition is structurally invalid, or its slug collides with a different pack. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function update(int $id, array $definition): MigrationPack + { + $this->validator->assertValid(definition: $definition); + + $pack = $this->mapper->find(id: $id); + + $slug = (string) $definition['id']; + if ($slug !== $pack->getPackSlug() && $this->slugExists(packSlug: $slug) === true) { + throw new InvalidArgumentException('A migration pack with id "'.$slug.'" already exists'); + } + + $pack->setPackSlug($slug); + $pack->setName((string) $definition['name']); + $pack->setSourceFormat((string) $definition['sourceFormat']); + $pack->setVersion((string) $definition['version']); + $pack->setDefinition(json_encode($definition, JSON_UNESCAPED_SLASHES)); + $pack->setUpdatedAt(new DateTime()); + + return $this->mapper->update(entity: $pack); + }//end update() + + /** + * Delete a migration pack. + * + * @param int $id The migration pack id. + * + * @return void + * + * @throws DoesNotExistException When no row matches. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function delete(int $id): void + { + $pack = $this->mapper->find(id: $id); + $this->mapper->delete(entity: $pack); + }//end delete() + + /** + * Import a pack document from a JSON file (upload) — upserts by the + * document's own `id`, so re-importing an updated pack (e.g. shared from + * another instance) updates the existing row rather than colliding. + * + * @param array $definition The decoded pack definition document. + * @param string|null $ownerUid The importing admin's uid. + * + * @return MigrationPack + * + * @throws InvalidArgumentException When the definition is structurally invalid. + * + * @spec openspec/specs/migration-mapping-packs/spec.md + */ + public function importDefinition(array $definition, ?string $ownerUid=null): MigrationPack + { + $this->validator->assertValid(definition: $definition); + + $slug = (string) $definition['id']; + + try { + $existing = $this->mapper->findByPackSlug(packSlug: $slug); + } catch (DoesNotExistException $e) { + return $this->create(definition: $definition, ownerUid: $ownerUid); + } + + return $this->update(id: $existing->getId(), definition: $definition); + }//end importDefinition() + + /** + * Whether a pack with the given slug already exists. + * + * @param string $packSlug The pack document's own `id`. + * + * @return bool + */ + private function slugExists(string $packSlug): bool + { + try { + $this->mapper->findByPackSlug(packSlug: $packSlug); + return true; + } catch (DoesNotExistException $e) { + return false; + } + }//end slugExists() +}//end class diff --git a/lib/Service/Notification/AnnotationNotificationDispatcher.php b/lib/Service/Notification/AnnotationNotificationDispatcher.php index 87bf0b00b8..b4244ed39b 100644 --- a/lib/Service/Notification/AnnotationNotificationDispatcher.php +++ b/lib/Service/Notification/AnnotationNotificationDispatcher.php @@ -34,13 +34,21 @@ use OCA\OpenRegister\Db\NotificationDispatchLogMapper; use OCA\OpenRegister\Db\NotificationHistoryMapper; use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\QueuedNotification; +use OCA\OpenRegister\Db\QueuedNotificationMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\BackgroundJob\WebPushDispatchJob; +use OCA\OpenRegister\Db\RegisterMapper; use OCP\Activity\IManager as IActivityManager; +use OCP\App\IAppManager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IGroupManager; use OCP\IServerContainer; +use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Mail\IMailer; use OCP\Notification\IManager as INotificationManager; @@ -49,32 +57,65 @@ /** * Reads notification annotations and dispatches matching ones. * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Dispatcher wires SchemaMapper, INotificationManager, IGroupManager, IUserManager, IMailer, IActivityManager, IClientService, IServerContainer, RateLimiter, IConfig, NotificationHistoryMapper, NotificationCoalescer, NotificationSubscriptionMapper, NotificationDispatchLogMapper, NotificationPreferenceService — every dependency serves a distinct notification channel or cross-cutting concern (audit, dedup, rate-limit, preference). - * @SuppressWarnings(PHPMD.ExcessiveClassLength) Class implements the full notification dispatch pipeline (nc-notification, email, activity, webhook, talk channels) plus recipient resolution, rate-limiting, coalescing, idempotency, history audit, and organisation gating; each concern requires its own helper method and cannot be moved to a separate class without breaking the single-dispatch-per-call contract. - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complexity is spread across 20+ private helpers each testing a distinct dispatch condition; PHPMD accumulates scores from every helper into the class total. - * @SuppressWarnings(PHPMD.TooManyMethods) Each notification channel (nc-notification, email, activity, webhook, talk), each gate (rate-limit, coalesce, preference, subscription, organisation), each helper (recipient resolve, locale, history record, idempotency) requires its own method per the single-responsibility principle. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Dispatcher wires SchemaMapper, INotificationManager, + * IGroupManager, IUserManager, IMailer, IActivityManager, IClientService, IServerContainer, RateLimiter, + * IConfig, NotificationHistoryMapper, NotificationCoalescer, NotificationSubscriptionMapper, + * NotificationDispatchLogMapper, NotificationPreferenceService — every dependency serves a distinct + * notification channel or cross-cutting concern (audit, dedup, rate-limit, preference). + * @SuppressWarnings(PHPMD.ExcessiveClassLength) Class implements the full notification dispatch pipeline + * (nc-notification, email, activity, webhook, talk channels) plus recipient resolution, rate-limiting, + * coalescing, idempotency, history audit, and organisation gating; each concern requires its own helper + * method and cannot be moved to a separate class without breaking the single-dispatch-per-call contract. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complexity is spread across 20+ private helpers each + * testing a distinct dispatch condition; PHPMD accumulates scores from every helper into the class total. + * @SuppressWarnings(PHPMD.TooManyMethods) Each notification channel (nc-notification, email, activity, + * webhook, talk), each gate (rate-limit, coalesce, preference, subscription, organisation), each helper + * (recipient resolve, locale, history record, idempotency) requires its own method per the + * single-responsibility principle. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-2 */ class AnnotationNotificationDispatcher { + + /** + * Per-instance cache of resolved relation display names, keyed by UUID. + * Avoids repeat ObjectService lookups when the same relation is + * interpolated across a recipient fan-out. + * + * @var array + */ + private array $relationDisplayCache = []; + /** * Constructor. * - * @param SchemaMapper $schemaMapper Mapper used to resolve the object's schema. - * @param INotificationManager $notificationManager Nextcloud notification API. - * @param LoggerInterface $logger Logger for dispatch diagnostics. - * @param IGroupManager $groupManager Group resolver for `groups` recipient kinds. - * @param IUserManager $userManager User resolver for `users` recipient kinds. - * @param IMailer $mailer Mailer for the `email` channel. - * @param IActivityManager $activityManager Activity manager for the `activity` channel. - * @param IClientService $httpClient HTTP client for the `webhook` and `talk` channels. - * @param IServerContainer $serverContainer Server container for expression resolvers (F06). - * @param RateLimiter|null $rateLimiter Optional rate limiter (per-rule, per-recipient). - * @param IConfig|null $config Optional config service for runtime tunables. - * @param NotificationHistoryMapper|null $historyMapper Optional history mapper for delivery audit rows. - * @param NotificationCoalescer|null $coalescer Optional coalescer for burst suppression. - * @param \OCA\OpenRegister\Db\NotificationSubscriptionMapper|null $subscriptionMapper Optional subscription mapper (DEPRECATED). - * @param NotificationDispatchLogMapper|null $dispatchLogMapper Optional dispatch-log mapper for idempotency-key dedup. - * @param NotificationPreferenceService|null $preferenceService Override-only preference resolver (delivery gate). + * @param SchemaMapper $schemaMapper Mapper used to resolve the object's schema. + * @param INotificationManager $notificationManager Nextcloud notification API. + * @param LoggerInterface $logger Logger for dispatch diagnostics. + * @param IGroupManager $groupManager Group resolver for `groups` recipient kinds. + * @param IUserManager $userManager User resolver for `users` recipient kinds. + * @param IMailer $mailer Mailer for the `email` channel. + * @param IActivityManager $activityManager Activity manager for the `activity` channel. + * @param IClientService $httpClient HTTP client for the `webhook` and `talk` channels. + * @param IServerContainer $serverContainer Server container for expression resolvers (F06). + * @param RateLimiter|null $rateLimiter Optional rate limiter (per-rule, per-recipient). + * @param IConfig|null $config Optional config service for runtime tunables. + * @param NotificationHistoryMapper|null $historyMapper Optional history mapper for delivery audit rows. + * @param NotificationCoalescer|null $coalescer Optional coalescer for burst suppression. + * @param \OCA\OpenRegister\Db\NotificationSubscriptionMapper|null $subscriptionMapper Optional subscription mapper (DEPRECATED). + * @param NotificationDispatchLogMapper|null $dispatchLogMapper Dispatch-log mapper for idempotency dedup. + * @param NotificationPreferenceService|null $preferenceService Override-only preference resolver (delivery gate). + * @param IJobList|null $jobList Job list used to enqueue the web-push dispatch job. + * @param IURLGenerator|null $urlGenerator URL generator for action-target deeplinks. + * @param RegisterMapper|null $registerMapper Register mapper used for the default originApp. + * @param \OCA\OpenRegister\Service\ObjectService|null $objectService Object resolver for relation-target deeplinks (RBAC). + * @param \OCA\OpenRegister\Service\DeepLinkRegistryService|null $deepLinkRegistry Canonical per-app deeplink resolver. + * @param IAppManager|null $appManager Resolves originApp display name (auto body). + * @param NotificationDeliveryWindowService|null $deliveryWindowService Override-only quiet-hours resolver + evaluator. + * @param QueuedNotificationMapper|null $queuedNotificationMapper Durable queue mapper (quiet-hours + digest schedule). + * @param DigestScheduleEvaluator|null $digestScheduleEvaluator Live evaluator for the `digest` fixed-time schedule. + * @param ITimeFactory|null $timeFactory Time source for the delivery/digest gate. * * @SuppressWarnings(PHPMD.ExcessiveParameterList) DI-injected dependencies. */ @@ -94,7 +135,17 @@ public function __construct( private readonly ?NotificationCoalescer $coalescer=null, private readonly ?\OCA\OpenRegister\Db\NotificationSubscriptionMapper $subscriptionMapper=null, private readonly ?NotificationDispatchLogMapper $dispatchLogMapper=null, - private readonly ?NotificationPreferenceService $preferenceService=null + private readonly ?NotificationPreferenceService $preferenceService=null, + private readonly ?IJobList $jobList=null, + private readonly ?IURLGenerator $urlGenerator=null, + private readonly ?RegisterMapper $registerMapper=null, + private readonly ?\OCA\OpenRegister\Service\ObjectService $objectService=null, + private readonly ?\OCA\OpenRegister\Service\DeepLinkRegistryService $deepLinkRegistry=null, + private readonly ?IAppManager $appManager=null, + private readonly ?NotificationDeliveryWindowService $deliveryWindowService=null, + private readonly ?QueuedNotificationMapper $queuedNotificationMapper=null, + private readonly ?DigestScheduleEvaluator $digestScheduleEvaluator=null, + private readonly ?ITimeFactory $timeFactory=null ) { }//end __construct() @@ -108,10 +159,19 @@ public function __construct( * * @return void * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) dispatch() iterates notifications, filters by organisation gate, idempotency, recipient resolution, rate-limit, coalesce, and preference per recipient per channel — each branch is a required dispatch condition; extracting sub-methods would split the sequential guard chain. - * @SuppressWarnings(PHPMD.NPathComplexity) Per-rule: trigger match + org gate + idempotency claim + subscription filter + rate-limit + coalesce + preference check + channel fan-out produce many independent paths; all are required for the notification contract. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) dispatch() is the single entry point that chains all notification gates in order; splitting the chain would break the sequential guard semantics (e.g. rate-limit must run after idempotency claim). - * @SuppressWarnings(PHPMD.LongVariable) $resolvedIdempotencyKey and $idempotencyKeyTemplate are descriptive names for distinct lifecycle stages of the same template value; abbreviating would obscure the claim-before-send ordering that prevents duplicate dispatches. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) dispatch() iterates notifications, filters by + * organisation gate, idempotency, recipient resolution, rate-limit, coalesce, and preference per + * recipient per channel — each branch is a required dispatch condition; extracting sub-methods + * would split the sequential guard chain. + * @SuppressWarnings(PHPMD.NPathComplexity) Per-rule: trigger match + org gate + idempotency claim + * + subscription filter + rate-limit + coalesce + preference check + channel fan-out produce many + * independent paths; all are required for the notification contract. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) dispatch() is the single entry point that chains + * all notification gates in order; splitting the chain would break the sequential guard semantics + * (e.g. rate-limit must run after idempotency claim). + * @SuppressWarnings(PHPMD.LongVariable) $resolvedIdempotencyKey and $idempotencyKeyTemplate are + * descriptive names for distinct lifecycle stages of the same template value; abbreviating would + * obscure the claim-before-send ordering that prevents duplicate dispatches. * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-5 */ @@ -122,6 +182,36 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ return; } + $this->dispatchWithSchema(object: $object, trigger: $trigger, context: $context, schema: $schema); + + }//end dispatch() + + /** + * Fire notifications declared on a pre-resolved Schema. + * + * Called by dispatch() after schema resolution, and directly by the + * SystemEntityNotificationBridge for system entities whose schema is not in + * the SchemaMapper but is instead provided by SystemSchemaRules::buildSchema(). + * + * This is the single entry point for the notification pipeline: every + * dispatch path (stored objects and system entities) goes through here. + * + * @param ObjectEntity $object The object (or virtual adapter) the event happened on. + * @param string $trigger 'created' | 'updated' | 'transition' | 'calculatedChange'. + * @param array $context Trigger-specific extras. + * @param Schema $schema Pre-resolved schema carrying the rules. + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.LongVariable) + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-2 + */ + public function dispatchWithSchema(ObjectEntity $object, string $trigger, array $context, Schema $schema): void + { $notifications = $this->getAnnotation(schema: $schema); if ($notifications === null) { return; @@ -146,7 +236,7 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ $matches = $this->matches( triggerSpec: $spec['trigger'] ?? [], trigger: $trigger, - context: $context + context: array_merge($context, ['_data' => $data]) ); if ($matches === false) { continue; @@ -227,6 +317,12 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ } $subjectTemplate = $spec['subject'] ?? (string) $name; + // The notification BODY template (distinct from the title). + // Absent when the rule declares no `message`; the per-recipient + // and broadcast resolvers below fall back to an auto-derived + // body ("Open in {AppName}.") when the rule has actions, else + // an empty body (back-compat). + $messageTemplate = ($spec['message'] ?? null); // Pre-render the broadcast (webhook/talk) subject using the // default locale fallback chain — these channels don't have // a per-recipient locale, so they use the spec's `defaultLocale` @@ -252,6 +348,46 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ $ruleId = (string) $name; + // Resolve the rule's originApp (declared value wins, else the + // app owning the schema's register) and the per-action deeplinks + // once per rule — these drive the icon + action buttons in the + // emitted notification and the web-push payload. + $originApp = $this->resolveOriginApp(spec: $spec, object: $object); + $resolvedActions = $this->resolveActions( + spec: $spec, + object: $object, + data: $data, + originApp: $originApp + ); + + // Resolve the broadcast (webhook/talk/web-push) BODY once per + // rule using the default-locale fallback chain — these channels + // have no per-recipient locale. Mirrors $broadcastSubject. + $broadcastMessage = $this->resolveMessageBody( + template: $messageTemplate, + locale: null, + data: $data, + context: $context, + hasActions: count($resolvedActions) > 0, + originApp: $originApp + ); + + // Route the web-push channel out of band: enqueue a background + // job per recipient so the originating save request is never + // blocked on push I/O. The job re-resolves recipients to their + // stored subscriptions and sends the encrypted VAPID payload. + if (in_array('web-push', $channels, true) === true) { + $this->enqueueWebPush( + recipients: $recipients, + ruleId: $ruleId, + originApp: $originApp, + subject: $broadcastSubject, + message: $broadcastMessage, + actions: $resolvedActions, + object: $object + ); + } + // Webhook is fired once per dispatch, not once per recipient, // and includes the recipient list in the payload. if (in_array('webhook', $channels, true) === true) { @@ -317,6 +453,17 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ context: $context, fallbackName: (string) $name ); + // The notification BODY for this recipient: the rule's + // localised `message`, or — when absent and the rule has + // actions — the auto-derived "Open in {AppName}." body. + $recipientMessage = $this->resolveMessageBody( + template: $messageTemplate, + locale: $recipientLocale, + data: $data, + context: $context, + hasActions: count($resolvedActions) > 0, + originApp: $originApp + ); // Per-recipient coalesce: silences duplicate dispatches // inside the configured window. Applied to every @@ -369,6 +516,40 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ } }//end if + // Delivery-window (quiet-hours) / digest-schedule gate. + // Evaluated only for the per-recipient channels + // (nc-notification, email, activity) — webhook/talk are + // broadcast and already fired above, unaffected. A + // `critical: true` rule skips this gate entirely and + // dispatches immediately (still subject to the preference + // gate above). When the gate suppresses, the event is + // QUEUED (never dropped) as a durable QueuedNotification + // row and flushed later by NotificationQueueFlushJob. + $queueableChannels = array_values( + array_intersect($effectiveChannels, ['nc-notification', 'email', 'activity']) + ); + if (count($queueableChannels) > 0 + && $this->deliveryWindowOrDigestSuppresses( + spec: $spec, + uid: $uid, + ruleId: $ruleId, + schemaId: (int) $schema->getId(), + queueableChannels: $queueableChannels, + channels: $channels, + object: $object, + subjectKey: $subjectKey, + name: (string) $name, + subject: $recipientSubject, + message: $recipientMessage, + context: $context, + originApp: $originApp, + actions: $resolvedActions, + locale: $recipientLocale + ) === true + ) { + continue; + } + if (in_array('nc-notification', $effectiveChannels, true) === true) { $this->emitNotification( uid: $uid, @@ -376,7 +557,11 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ subjectKey: $subjectKey, name: (string) $name, subject: $recipientSubject, - context: $context + message: $recipientMessage, + context: $context, + originApp: $originApp, + actions: $resolvedActions, + webPushActive: in_array('web-push', $channels, true) ); $this->recordHistory( ruleId: $ruleId, @@ -387,7 +572,7 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ subject: $recipientSubject, locale: $recipientLocale ); - } + }//end if if (in_array('email', $effectiveChannels, true) === true) { $this->emitEmail( @@ -426,7 +611,334 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ }//end foreach }//end foreach - }//end dispatch() + }//end dispatchWithSchema() + + /** + * Resolve "now" for the delivery-window / digest-schedule gate. Uses + * the injected `ITimeFactory` when present (tests inject a controllable + * clock), else the real wall clock. + * + * @return DateTimeImmutable + */ + private function currentTime(): DateTimeImmutable + { + if ($this->timeFactory !== null) { + return DateTimeImmutable::createFromInterface($this->timeFactory->getDateTime()); + } + + return new DateTimeImmutable('now'); + + }//end currentTime() + + /** + * Evaluate the delivery-window (quiet-hours) / digest-schedule gate for + * one recipient and, when it suppresses, persist a `QueuedNotification` + * row (never drop the event) and record notification history with the + * matching `queued-*` status. + * + * A `critical: true` rule bypasses this gate entirely — the caller + * checks this before invoking the method (see `deliveryWindowService` + * usage in `dispatchWithSchema()`), so this method itself does not + * need to special-case `critical` beyond the check below (defence in + * depth: a caller invoking this directly still gets the bypass). + * + * @param array $spec The full notification spec block. + * @param string $uid Recipient user UID. + * @param string $ruleId The rule id. + * @param int $schemaId Owning schema id. + * @param array $queueableChannels Per-recipient channels eligible for queueing + * (effectiveChannels ∩ {nc-notification, + * email, activity}). + * @param array $channels The rule's full declared channel list (for history audit). + * @param ObjectEntity $object The triggering object. + * @param string $subjectKey Canonical INotification subject key. + * @param string $name Notification annotation key. + * @param string $subject Pre-rendered, per-recipient subject. + * @param string $message Pre-rendered, per-recipient message body. + * @param array $context Trigger context (action, from, to). + * @param string $originApp Resolved origin app id. + * @param array $actions Resolved action buttons. + * @param string|null $locale Recipient locale. + * + * @return bool True when the event was queued (caller must skip immediate dispatch). + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Mirrors the call-site context in dispatchWithSchema(). + * + * @spec openspec/specs/notificatie-engine/spec.md + */ + private function deliveryWindowOrDigestSuppresses( + array $spec, + string $uid, + string $ruleId, + int $schemaId, + array $queueableChannels, + array $channels, + ObjectEntity $object, + string $subjectKey, + string $name, + string $subject, + string $message, + array $context, + string $originApp, + array $actions, + ?string $locale + ): bool { + if (($spec['critical'] ?? false) === true) { + return false; + } + + $now = $this->currentTime(); + + $windowActive = false; + if ($this->deliveryWindowService !== null) { + $window = $this->deliveryWindowService->getForUser(userId: $uid); + if ($window !== null) { + $windowActive = $this->deliveryWindowService->isInsideWindow(window: $window, now: $now); + } + } + + $digestSpec = ($spec['digest'] ?? null); + $digestDeclared = is_array($digestSpec) === true + && $this->digestScheduleEvaluator !== null + && $this->digestScheduleEvaluator->isValidDigestSpec($digestSpec) === true; + + if ($windowActive === false && $digestDeclared === false) { + // No window and no digest schedule — dispatch immediately, + // exactly as before this change (backward compatibility). + return false; + } + + $reason = QueuedNotification::REASON_QUIET_HOURS; + if ($windowActive === true && $digestDeclared === true) { + $reason = QueuedNotification::REASON_BOTH; + } else if ($digestDeclared === true) { + $reason = QueuedNotification::REASON_DIGEST_SCHEDULE; + } + + $status = 'queued-quiet-hours'; + if ($reason === QueuedNotification::REASON_DIGEST_SCHEDULE) { + $status = 'queued-digest'; + } + + if ($this->queuedNotificationMapper !== null) { + $payload = [ + 'subject' => $subject, + 'message' => $message, + 'channels' => $queueableChannels, + 'subjectKey' => $subjectKey, + 'name' => $name, + 'originApp' => $originApp, + 'actions' => $actions, + 'locale' => $locale, + 'action' => (string) ($context['action'] ?? ''), + 'objectUuid' => (string) ($object->getUuid() ?? ''), + 'objectRegister' => $object->getRegister(), + 'objectSchema' => $object->getSchema(), + 'objectName' => $object->getName(), + ]; + + $entity = new QueuedNotification(); + $entity->setSchemaId($schemaId); + $entity->setRuleKey($ruleId); + $entity->setRecipient($uid); + $entity->setReason($reason); + $entity->setObjectUuid((string) ($object->getUuid() ?? '')); + $entity->setPayload((string) json_encode($payload)); + $entity->setDueAtHint(DateTime::createFromImmutable($now)); + $entity->setCreatedAt(DateTime::createFromImmutable($now)); + + try { + $this->queuedNotificationMapper->insert(entity: $entity); + } catch (\Throwable $e) { + $this->logger->warning( + sprintf('[AnnotationNotificationDispatcher] failed to queue rule="%s" recipient="%s": %s', $ruleId, $uid, $e->getMessage()) + ); + } + }//end if + + $this->recordHistoryAcrossChannels( + ruleId: $ruleId, + recipient: $uid, + channels: $channels, + broadcastChannels: ['webhook', 'talk'], + status: $status, + object: $object, + subject: $subject, + locale: $locale + ); + + return true; + + }//end deliveryWindowOrDigestSuppresses() + + /** + * Flush a group of `QueuedNotification` rows sharing the same + * `(rule_key, recipient)` pair, called by `NotificationQueueFlushJob` + * once it has determined the group's holding condition has cleared. + * + * Reuses the existing channel fan-out unchanged (`emitNotification`, + * `emitEmail`, `emitActivity`). A single row flushes as its own + * pre-resolved subject/message; multiple rows are merged into one + * summary message so the recipient gets one notification instead of N. + * Notification history is updated to `dispatched` on success. + * + * @param array $queuedRows Rows sharing one (rule_key, recipient) pair. + * + * @return void + * + * @spec openspec/specs/notificatie-engine/spec.md + */ + public function dispatchQueued(array $queuedRows): void + { + if (count($queuedRows) === 0) { + return; + } + + $first = $queuedRows[0]; + $payload = $first->getDecodedPayload(); + $ruleId = (string) $first->getRuleKey(); + $recipient = (string) $first->getRecipient(); + + [$subject, $message] = $this->buildQueuedSummary(rows: $queuedRows); + + $channels = (array) ($payload['channels'] ?? []); + + $object = new ObjectEntity(); + $object->setUuid((string) ($payload['objectUuid'] ?? '')); + if (isset($payload['objectRegister']) === true && $payload['objectRegister'] !== null) { + $object->setRegister((string) $payload['objectRegister']); + } + + if (isset($payload['objectSchema']) === true && $payload['objectSchema'] !== null) { + $object->setSchema((string) $payload['objectSchema']); + } + + if (isset($payload['objectName']) === true && $payload['objectName'] !== null) { + $object->setName((string) $payload['objectName']); + } + + $locale = null; + if (($payload['locale'] ?? null) !== null) { + $locale = (string) $payload['locale']; + } + + if (in_array('nc-notification', $channels, true) === true) { + $this->emitNotification( + uid: $recipient, + object: $object, + subjectKey: (string) ($payload['subjectKey'] ?? $this->canonicalSubject(trigger: 'updated')), + name: (string) ($payload['name'] ?? $ruleId), + subject: $subject, + message: $message, + context: [], + originApp: (string) ($payload['originApp'] ?? 'openregister'), + actions: (array) ($payload['actions'] ?? []) + ); + $this->recordHistory( + ruleId: $ruleId, + channel: 'nc-notification', + recipient: $recipient, + status: 'dispatched', + object: $object, + subject: $subject, + locale: $locale + ); + }//end if + + if (in_array('email', $channels, true) === true) { + $emailBody = $subject; + if ($message !== '') { + $emailBody = $message; + } + + $this->emitEmail(uid: $recipient, subject: $subject, body: $emailBody); + $this->recordHistory( + ruleId: $ruleId, + channel: 'email', + recipient: $recipient, + status: 'dispatched', + object: $object, + subject: $subject, + locale: $locale + ); + } + + if (in_array('activity', $channels, true) === true) { + $this->emitActivity( + uid: $recipient, + objectId: (string) ($payload['objectUuid'] ?? ''), + name: (string) ($payload['name'] ?? $ruleId), + subject: $subject + ); + $this->recordHistory( + ruleId: $ruleId, + channel: 'activity', + recipient: $recipient, + status: 'dispatched', + object: $object, + subject: $subject, + locale: $locale + ); + } + + }//end dispatchQueued() + + /** + * Build the flushed subject/message for a group of queued rows. A + * single row flushes verbatim; multiple rows are merged into a + * digest-style summary (breakdown by the stored `action`, e.g. + * "3 nieuw, 2 gewijzigd" — reusing the rolling-digest breakdown + * pattern already documented for the coalescer). + * + * @param array $rows Rows sharing one (rule_key, recipient) pair. + * + * @return array{0: string, 1: string} [subject, message] + */ + private function buildQueuedSummary(array $rows): array + { + if (count($rows) === 1) { + $payload = $rows[0]->getDecodedPayload(); + return [(string) ($payload['subject'] ?? ''), (string) ($payload['message'] ?? '')]; + } + + $counts = []; + foreach ($rows as $row) { + $payload = $row->getDecodedPayload(); + $action = (string) ($payload['action'] ?? ''); + if ($action === '') { + $action = 'other'; + } + + $counts[$action] = (($counts[$action] ?? 0) + 1); + } + + $labels = [ + 'created' => 'nieuw', + 'updated' => 'gewijzigd', + ]; + + $parts = []; + foreach ($counts as $action => $count) { + $parts[] = sprintf('%d %s', $count, ($labels[$action] ?? $action)); + } + + $breakdown = implode(', ', $parts); + $subject = sprintf('%d meldingen (%s)', count($rows), $breakdown); + + $titles = []; + foreach (array_slice($rows, 0, 10) as $row) { + $payload = $row->getDecodedPayload(); + $titles[] = (string) ($payload['subject'] ?? ''); + } + + $message = implode('; ', array_filter($titles)); + if (count($rows) > 10) { + $message .= sprintf(' ... en %d meer', (count($rows) - 10)); + } + + return [$subject, $message]; + + }//end buildQueuedSummary() /** * Helper to dispatch a broadcast-style channel (webhook / talk). @@ -451,7 +963,10 @@ public function dispatch(ObjectEntity $object, string $trigger, array $context=[ * * @return void * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) dispatchBroadcastChannel() centralises the rate-limit+coalesce+history pattern for webhook and talk; all 11 parameters are forwarded from the dispatch() call-site context and cannot be bundled into a value-object without creating a throw-away struct that obscures the data flow. + * @SuppressWarnings(PHPMD.ExcessiveParameterList) dispatchBroadcastChannel() centralises the + * rate-limit+coalesce+history pattern for webhook and talk; all 11 parameters are forwarded from + * the dispatch() call-site context and cannot be bundled into a value-object without creating a + * throw-away struct that obscures the data flow. */ private function dispatchBroadcastChannel( string $channel, @@ -554,7 +1069,7 @@ private function rateLimitAllows(string $ruleId, string $recipient, ?array $rate * * @return bool True when the dispatch may proceed. * - * @spec openspec/changes/notificatie-engine/tasks.md + * @spec openspec/specs/notificatie-engine/spec.md */ private function coalesceAllows(string $ruleId, string $recipient, ?array $coalesce): bool { @@ -698,7 +1213,7 @@ static function (array $matches) use ($object, $data): string { * * @return void * - * @spec openspec/changes/notificatie-engine/tasks.md + * @spec openspec/specs/notificatie-engine/spec.md */ private function recordHistory( string $ruleId, @@ -775,7 +1290,7 @@ private function recordHistory( * * @return void * - * @spec openspec/changes/notificatie-engine/tasks.md + * @spec openspec/specs/notificatie-engine/spec.md */ private function recordHistoryAcrossChannels( string $ruleId, @@ -1013,8 +1528,13 @@ private function emitWebhook( * * @return bool True when the rule should fire for this event. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) matches() handles four trigger types (created/updated/transition/calculatedChange) each with sub-conditions (action filter, field-change condition, boundary-crossing operators); each branch is a distinct spec requirement and cannot be merged. - * @SuppressWarnings(PHPMD.NPathComplexity) Each trigger type has optional sub-conditions that can independently pass or fail; the NPath count reflects spec-mandated combinations, not accidental branching. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) matches() handles four trigger types + * (created/updated/transition/calculatedChange) each with sub-conditions (action filter, + * field-change condition, boundary-crossing operators); each branch is a distinct spec + * requirement and cannot be merged. + * @SuppressWarnings(PHPMD.NPathComplexity) Each trigger type has optional sub-conditions that + * can independently pass or fail; the NPath count reflects spec-mandated combinations, + * not accidental branching. */ private function matches(array $triggerSpec, string $trigger, array $context): bool { @@ -1035,6 +1555,25 @@ private function matches(array $triggerSpec, string $trigger, array $context): b } } + // Optional field filter for `created` triggers — the rule fires only + // when the created object's fields satisfy every declared equality + // (e.g. `channel == "telefoon"`). Evaluated against the created + // object's data forwarded as `_data`. A condition-less `created` rule + // matches on type alone (back-compat). Fail-closed when data is absent. + if ($trigger === 'created' && isset($triggerSpec['filter']) === true) { + $filter = $triggerSpec['filter']; + if (is_array($filter) === false) { + return false; + } + + $data = ($context['_data'] ?? null); + if (is_array($data) === false) { + return false; + } + + return $this->createdFilterMatches(filter: $filter, data: $data); + } + // Optional non-numeric field-change `condition` for `updated` triggers. // Engages ONLY when the trigger declares a `condition`; condition-less // `updated` rules match on type alone (back-compat). Reads the old/new @@ -1113,30 +1652,98 @@ private function matches(array $triggerSpec, string $trigger, array $context): b * * @return bool True when the value satisfies every declared operator. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) numericConditionMatches() evaluates six comparison operators (lt, lte, gt, gte, eq, ne) in a match expression; each arm is a distinct spec-defined operator and cannot be reduced further. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) numericConditionMatches() evaluates six comparison + * operators (lt, lte, gt, gte, eq, ne) in a match expression; each arm is a distinct spec-defined + * operator and cannot be reduced further. */ private function numericConditionMatches(mixed $value, array $operators): bool { foreach ($operators as $op => $threshold) { - $numeric = is_numeric($value) === true && is_numeric($threshold) === true; - $result = match ((string) $op) { + $numeric = (is_numeric($value) === true && is_numeric($threshold) === true); + + // BUG-SVC-8: when both sides are numeric, compare as floats so + // numerically-equal-but-differently-formatted values match + // (e.g. `1.0` eq `1`). Fall back to string compare otherwise. + if ($numeric === true) { + $equals = ((float) $value === (float) $threshold); + } else { + $equals = ((string) $value === (string) $threshold); + } + + $result = match ((string) $op) { 'lt' => $numeric && (float) $value < (float) $threshold, 'lte' => $numeric && (float) $value <= (float) $threshold, 'gt' => $numeric && (float) $value > (float) $threshold, 'gte' => $numeric && (float) $value >= (float) $threshold, - 'eq' => (string) $value === (string) $threshold, - 'ne' => (string) $value !== (string) $threshold, + 'eq' => $equals, + 'ne' => ($equals === false), default => false, }; if ($result === false) { return false; } - } + }//end foreach return true; }//end numericConditionMatches() + /** + * Evaluate a `created`-trigger field filter against the created object's data. + * + * Supports the contract's single-field shape `{ field, operator, value|values }` + * with operators `equals` (default), `in`, and `notIn`. Scalar comparison is + * string-cast (mirrors `fieldChangeConditionMatches`). A filter naming an + * unknown field fails closed. + * + * @param array $filter The `trigger.filter` sub-document. + * @param array $data The created object's data. + * + * @return bool True when the object satisfies the filter. + * + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md + */ + private function createdFilterMatches(array $filter, array $data): bool + { + $field = (string) ($filter['field'] ?? ''); + if ($field === '') { + return false; + } + + $operator = (string) ($filter['operator'] ?? 'equals'); + $actual = ($data[$field] ?? null); + $actualStr = ''; + if (is_scalar($actual) === true) { + $actualStr = (string) $actual; + } + + if ($operator === 'in' || $operator === 'notIn') { + $values = ($filter['values'] ?? []); + if (is_array($values) === false) { + return false; + } + + $haystack = []; + foreach ($values as $candidate) { + if (is_scalar($candidate) === true) { + $haystack[] = (string) $candidate; + } else { + $haystack[] = ''; + } + } + + $present = in_array($actualStr, $haystack, true); + if ($operator === 'in') { + return $present; + } + + return ($present === false); + }//end if + + // Default: equals. + return $actualStr === (string) ($filter['value'] ?? ''); + }//end createdFilterMatches() + /** * Evaluate a non-numeric field-change condition for an `updated` trigger. * @@ -1206,9 +1813,16 @@ private function fieldChangeConditionMatches(array $condition, array $oldData, a * * @return array * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) resolveRecipients() handles five recipient kinds (users, groups, field, acl-read, acl-manage) plus expression evaluation, deduplication, and exclusion; each kind requires its own resolution logic and must run in one pass to produce a deduplicated uid list. - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each recipient entry is dispatched by kind; within each kind there are null-guards and type checks — all are required branches of the spec's recipient model. - * @SuppressWarnings(PHPMD.NPathComplexity) Combinations of recipient kinds, expression evaluation, null-guards, and exclusion list produce many paths; each is required by the spec's recipient-resolution contract. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) resolveRecipients() handles five recipient kinds + * (users, groups, field, acl-read, acl-manage) plus expression evaluation, deduplication, and + * exclusion; each kind requires its own resolution logic and must run in one pass to produce a + * deduplicated uid list. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each recipient entry is dispatched by kind; within + * each kind there are null-guards and type checks — all are required branches of the spec's + * recipient model. + * @SuppressWarnings(PHPMD.NPathComplexity) Combinations of recipient kinds, expression evaluation, + * null-guards, and exclusion list produce many paths; each is required by the spec's + * recipient-resolution contract. */ private function resolveRecipients(array $recipientsSpec, array $data, ?ObjectEntity $object=null, array $context=[]): array { @@ -1342,7 +1956,10 @@ private function resolveRecipients(array $recipientsSpec, array $data, ?ObjectEn * * @return array * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) resolveObjectAclRecipients() walks owner, per-user, per-role, and per-group ACL entries; each entry type requires separate null-guards and uid-extraction logic that cannot be merged without losing the distinction between role-based and explicit-user grants. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) resolveObjectAclRecipients() walks owner, + * per-user, per-role, and per-group ACL entries; each entry type requires separate null-guards + * and uid-extraction logic that cannot be merged without losing the distinction between + * role-based and explicit-user grants. */ private function resolveObjectAclRecipients(ObjectEntity $object, string $permission): array { @@ -1494,7 +2111,9 @@ private function userExists(string $uid): bool * * @return array * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) extractUidsFromRelation() handles six distinct relation shapes (null, array-of-strings, array-of-objects with uid/id/userId, plain string); each shape requires a separate extraction branch that cannot be unified. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) extractUidsFromRelation() handles six distinct + * relation shapes (null, array-of-strings, array-of-objects with uid/id/userId, plain string); + * each shape requires a separate extraction branch that cannot be unified. */ private function extractUidsFromRelation(mixed $value): array { @@ -1566,8 +2185,12 @@ private function extractUidsFromRelation(mixed $value): array * * @return string The interpolated subject string. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) resolveLocalizedSubject() handles three template shapes (string, locale-keyed map, default-locale fallback) and within each shape applies template-variable interpolation; each branch is required by the i18n subject contract. - * @SuppressWarnings(PHPMD.NPathComplexity) Template shape × locale-match × defaultLocale fallback × interpolation presence produce many paths; all are required by the spec's subject-resolution priority chain. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) resolveLocalizedSubject() handles three template + * shapes (string, locale-keyed map, default-locale fallback) and within each shape applies + * template-variable interpolation; each branch is required by the i18n subject contract. + * @SuppressWarnings(PHPMD.NPathComplexity) Template shape × locale-match × defaultLocale fallback + * × interpolation presence produce many paths; all are required by the spec's subject-resolution + * priority chain. */ private function resolveLocalizedSubject( mixed $template, @@ -1621,6 +2244,98 @@ private function resolveLocalizedSubject( }//end resolveLocalizedSubject() + /** + * Resolve the notification BODY (message) for a recipient/broadcast. + * + * The body is distinct from the title (`subject`): + * + * 1. When the rule declares a `message` template (string or per-locale + * map), it is localised + `{{prop}}`-interpolated through the SAME + * resolver as the subject (`resolveLocalizedSubject()`). + * 2. When the rule declares NO `message` but DOES declare `actions[]`, + * the body is auto-derived as "Open in {AppName}." where {AppName} + * is the originApp's human display name (via IAppManager; falls back + * to ucfirst(originApp) when unavailable). + * 3. When the rule declares neither `message` nor `actions`, the body + * is empty — exactly today's behaviour (back-compat). + * + * @param mixed $template Raw `message` value (string, array, or null). + * @param string|null $locale Recipient locale, or null for broadcast channels. + * @param array $data Object data for `{{prop}}` interpolation. + * @param array $context Trigger-specific context. + * @param bool $hasActions Whether the rule declared resolvable actions. + * @param string $originApp Resolved origin app id (drives the auto-body app name). + * + * @return string The interpolated body string, or '' for the empty back-compat body. + * + * @spec openspec/changes/openregister-notification-body/specs/notificatie-engine/spec.md + */ + private function resolveMessageBody( + mixed $template, + ?string $locale, + array $data, + array $context, + bool $hasActions, + string $originApp + ): string { + $hasTemplate = (is_string($template) === true && $template !== '') || (is_array($template) === true && $template !== []); + if ($hasTemplate === true) { + // Reuse the subject resolver: identical shape, locale-resolution + // and {{prop}} interpolation contract. The empty fallbackName + // means an unresolvable map degrades to '' rather than a name. + return $this->resolveLocalizedSubject( + template: $template, + locale: $locale, + data: $data, + context: $context, + fallbackName: '' + ); + } + + // No explicit body: auto-derive only when the rule has actions. + if ($hasActions === false) { + return ''; + } + + return sprintf('Open in %s.', $this->resolveAppDisplayName(app: $originApp)); + + }//end resolveMessageBody() + + /** + * Resolve an app's human display name for the auto-derived body. + * + * Reads the app's `name` from IAppManager::getAppInfo() when the manager + * is available; falls back to ucfirst(app) when the manager is absent + * (older fixtures) or the lookup yields no usable name. + * + * @param string $app The app id. + * + * @return string The human-readable app display name. + * + * @spec openspec/changes/openregister-notification-body/specs/notificatie-engine/spec.md + */ + private function resolveAppDisplayName(string $app): string + { + $fallback = ucfirst($app); + if ($this->appManager === null || $app === '') { + return $fallback; + } + + try { + $info = $this->appManager->getAppInfo($app); + if (is_array($info) === true && is_string($info['name'] ?? null) === true && $info['name'] !== '') { + return $info['name']; + } + } catch (\Throwable $e) { + $this->logger->debug( + sprintf('[AnnotationNotificationDispatcher] app display-name lookup failed: %s', $e->getMessage()) + ); + } + + return $fallback; + + }//end resolveAppDisplayName() + /** * Resolve a Nextcloud user's preferred locale. * @@ -1691,14 +2406,20 @@ private function interpolate(string $template, array $data, array $context): str { return preg_replace_callback( '/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/', - static function (array $matches) use ($data, $context): string { + function (array $matches) use ($data, $context): string { $key = $matches[1]; if (array_key_exists($key, $data) === true) { if (is_scalar($data[$key]) === false) { return ''; } - return htmlspecialchars((string) $data[$key], ENT_QUOTES, 'UTF-8'); + // Relation fields hold a UUID reference; show the related + // object's display name instead of the raw UUID so + // "{{client}}" reads "Acme Gemeente BV", not a UUID string. + $raw = (string) $data[$key]; + $display = $this->resolveRelationDisplayName(value: $raw); + + return htmlspecialchars(($display ?? $raw), ENT_QUOTES, 'UTF-8'); } if (array_key_exists($key, $context) === true) { @@ -1715,6 +2436,58 @@ static function (array $matches) use ($data, $context): string { ) ?? $template; }//end interpolate() + /** + * Resolve a relation-reference UUID to the related object's display name. + * + * Notification subjects/bodies interpolate `{{prop}}` from the object's + * data; a relation field holds a UUID, which reads poorly in a popup + * ("Incoming call from 3b9f…"). When the value is UUID-shaped, resolve the + * related object through OpenRegister (RBAC-scoped, mirroring the action + * deeplink resolver) and return its name. Returns null — so the caller + * keeps the raw value — for non-UUID values, an absent ObjectService, an + * unresolvable id, or a nameless object. Cached per dispatcher instance to + * avoid repeat lookups across a recipient fan-out. + * + * @param string $value The interpolated field value. + * + * @return string|null The related object's display name, or null to keep the raw value. + * + * @spec openspec/changes/openregister-notification-relation-names/specs/notificatie-engine/spec.md + */ + private function resolveRelationDisplayName(string $value): ?string + { + if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value) !== 1) { + return null; + } + + if ($this->objectService === null) { + return null; + } + + if (array_key_exists($value, $this->relationDisplayCache) === true) { + return $this->relationDisplayCache[$value]; + } + + $name = null; + try { + $related = $this->objectService->find(id: $value, _rbac: true); + if ($related !== null) { + $candidate = $related->getName(); + if (is_string($candidate) === true && $candidate !== '') { + $name = $candidate; + } + } + } catch (\Throwable $e) { + $this->logger->debug('[AnnotationNotificationDispatcher] relation display-name resolve failed: '.$e->getMessage()); + $name = null; + } + + $this->relationDisplayCache[$value] = $name; + + return $name; + + }//end resolveRelationDisplayName() + /** * Map a trigger to the canonical INotification subject the Notifier * renders. Decouples the displayed subject from the schema author's @@ -1733,6 +2506,397 @@ private function canonicalSubject(string $trigger): string }; }//end canonicalSubject() + /** + * Resolve the rule's originApp: the declared value, else the app owning + * the schema's register, else 'openregister'. + * + * @param array $spec The notification rule spec. + * @param ObjectEntity $object The triggering object. + * + * @return string The resolved origin app id. + * + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md + */ + private function resolveOriginApp(array $spec, ObjectEntity $object): string + { + $declared = ($spec['originApp'] ?? null); + if (is_string($declared) === true && $declared !== '') { + return $declared; + } + + // Default: the app that owns the schema's register. + $registerId = $object->getRegister(); + $registerMapper = $this->registerMapper; + if ($registerMapper !== null && $registerId !== null && (string) $registerId !== '') { + try { + $register = $registerMapper->find( + $registerId, + _rbac: false, + _multitenancy: false + ); + $owningApp = $register->getApplication(); + if (is_string($owningApp) === true && $owningApp !== '') { + return $owningApp; + } + } catch (\Throwable $e) { + $this->logger->debug( + sprintf('[AnnotationNotificationDispatcher] originApp register lookup failed: %s', $e->getMessage()) + ); + } + } + + return 'openregister'; + }//end resolveOriginApp() + + /** + * Resolve declared `actions[]` to concrete, server-side deeplinks. + * + * Each returned action carries the i18n `label` map, the `primary` flag, + * and a resolved absolute `url`. Targets that cannot be resolved (e.g. an + * unreadable relation object) are dropped, never leaked. + * + * @param array $spec The notification rule spec. + * @param ObjectEntity $object The triggering object. + * @param array $data The triggering object's data. + * @param string $originApp The resolved origin app id. + * + * @return array, primary: bool, url: string}> + * + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md + */ + private function resolveActions(array $spec, ObjectEntity $object, array $data, string $originApp): array + { + $declared = ($spec['actions'] ?? null); + if (is_array($declared) === false || count($declared) === 0) { + return []; + } + + $resolved = []; + foreach ($declared as $action) { + if (is_array($action) === false) { + continue; + } + + $label = ($action['label'] ?? []); + if (is_array($label) === false) { + $label = []; + } + + $url = $this->resolveActionTarget( + target: ($action['target'] ?? []), + object: $object, + data: $data, + originApp: $originApp + ); + if ($url === null || $url === '') { + // Target unresolved (e.g. relation not readable) — drop the + // action rather than emit a dead or leaking button. + continue; + } + + $resolved[] = [ + 'label' => $label, + 'primary' => (bool) ($action['primary'] ?? false), + 'url' => $url, + ]; + }//end foreach + + return $resolved; + }//end resolveActions() + + /** + * Resolve a single action `target` to an absolute deeplink. + * + * Supported kinds: + * - object-detail: deeplink to the triggering object's detail page. + * - object-detail + { object: { kind: "relation", field } }: resolve the + * relation field on the triggering object to a related object's + * register/schema/uuid THROUGH OR RBAC ("Open client") — the related + * id is never trusted from the wire. + * - route: an originApp frontend route with {{prop}} interpolation (HTML-escaped). + * - url: an absolute URL, passed through verbatim. + * + * @param mixed $target The raw target spec. + * @param ObjectEntity $object The triggering object. + * @param array $data The triggering object's data. + * @param string $originApp The resolved origin app id. + * + * @return string|null The resolved absolute URL, or null when unresolvable. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) One branch per target kind. + * + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md + */ + private function resolveActionTarget(mixed $target, ObjectEntity $object, array $data, string $originApp): ?string + { + if (is_array($target) === false) { + return null; + } + + $kind = (string) ($target['kind'] ?? ''); + + if ($kind === 'url') { + $href = (string) ($target['href'] ?? ''); + if (filter_var($href, FILTER_VALIDATE_URL) === false) { + return null; + } + + return $href; + } + + if ($kind === 'route') { + $app = (string) ($target['app'] ?? $originApp); + $route = (string) ($target['route'] ?? ''); + if ($route === '') { + return null; + } + + // Interpolate {{prop}} tokens from the object data, HTML-escaped. + $interpolated = preg_replace_callback( + '/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/', + static function (array $m) use ($data): string { + $value = ($data[$m[1]] ?? ''); + if (is_scalar($value) === false) { + return ''; + } + + return rawurlencode(htmlspecialchars((string) $value, (ENT_QUOTES | ENT_HTML5))); + }, + $route + ); + + return $this->appRouteBase(app: $app).ltrim((string) $interpolated, '/'); + }//end if + + if ($kind === 'object-detail') { + $relationSpec = ($target['object'] ?? null); + if (is_array($relationSpec) === true && (string) ($relationSpec['kind'] ?? '') === 'relation') { + return $this->resolveRelationDeeplink( + field: (string) ($relationSpec['field'] ?? ''), + data: $data, + originApp: $originApp + ); + } + + // Deeplink to the triggering object itself. + return $this->buildObjectDetailLink( + originApp: $originApp, + registerId: (string) ($object->getRegister() ?? ''), + schemaId: (string) ($object->getSchema() ?? ''), + objectUuid: (string) ($object->getUuid() ?? '') + ); + }//end if + + return null; + }//end resolveActionTarget() + + /** + * Resolve a relation-field deeplink ("Open client") through OR RBAC. + * + * The field value on the triggering object is treated only as a lookup + * key; the related object is re-resolved via ObjectService::find() with + * RBAC enabled so a deeplink is built only for an object the current + * context may read. The wire-supplied id is never trusted directly. + * + * @param string $field The relation field name. + * @param array $data The triggering object's data. + * @param string $originApp The resolved origin app id. + * + * @return string|null The deeplink, or null when the relation is empty/unreadable. + * + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md + */ + private function resolveRelationDeeplink(string $field, array $data, string $originApp): ?string + { + if ($field === '' || $this->objectService === null) { + return null; + } + + $raw = ($data[$field] ?? null); + $lookup = null; + if (is_string($raw) === true && $raw !== '') { + $lookup = $raw; + } else if (is_array($raw) === true) { + // Array relation: take the first id/uuid candidate. + $candidate = ($raw['id'] ?? ($raw['uuid'] ?? ($raw[0] ?? null))); + if (is_string($candidate) === true && $candidate !== '') { + $lookup = $candidate; + } + } + + if ($lookup === null) { + return null; + } + + try { + $related = $this->objectService->find(id: $lookup, _rbac: true); + } catch (\Throwable $e) { + $this->logger->debug( + sprintf('[AnnotationNotificationDispatcher] relation deeplink resolve failed: %s', $e->getMessage()) + ); + return null; + } + + if ($related === null) { + return null; + } + + return $this->buildObjectDetailLink( + originApp: $originApp, + registerId: (string) ($related->getRegister() ?? ''), + schemaId: (string) ($related->getSchema() ?? ''), + objectUuid: (string) ($related->getUuid() ?? '') + ); + }//end resolveRelationDeeplink() + + /** + * Build an object-detail deeplink against the originApp frontend route. + * + * @param string $originApp The resolved origin app id. + * @param string $registerId The object's register id. + * @param string $schemaId The object's schema id. + * @param string $objectUuid The object's uuid. + * + * @return string|null The absolute deeplink, or null when ids are missing. + */ + private function buildObjectDetailLink(string $originApp, string $registerId, string $schemaId, string $objectUuid): ?string + { + if ($registerId === '' || $schemaId === '' || $objectUuid === '') { + return null; + } + + // Prefer the canonical per-app deeplink registered via + // DeepLinkRegistrationEvent (e.g. pipelinq::client → + // /apps/pipelinq/clients/{uuid}). This is the app-correct route the + // rest of the platform uses; the openregister hash-route below is only + // a fallback for schemas no leaf app has registered a deeplink for. + if ($this->deepLinkRegistry !== null && is_numeric($registerId) === true && is_numeric($schemaId) === true) { + $registered = $this->deepLinkRegistry->resolveUrl( + registerId: (int) $registerId, + schemaId: (int) $schemaId, + objectData: ['uuid' => $objectUuid, 'id' => $objectUuid] + ); + if ($registered !== null && $registered !== '') { + // Return a RELATIVE path so the Service Worker / browser resolves + // it against the real page origin (preserving a non-standard port + // such as :8080). Wrapping with getAbsoluteURL() injects + // overwrite.cli.url, which can drop the port (e.g. http://localhost + // instead of http://localhost:8080) and 404 the deeplink. + if (str_starts_with($registered, 'http') === true) { + $parts = parse_url($registered); + $path = ($parts['path'] ?? '/'); + if (isset($parts['query']) === true) { + $path .= '?'.$parts['query']; + } + + if (isset($parts['fragment']) === true) { + $path .= '#'.$parts['fragment']; + } + + return $path; + } + + return $registered; + }//end if + }//end if + + return $this->appRouteBase(app: $originApp) + .sprintf('#/registers/%s/schemas/%s/objects/%s', $registerId, $schemaId, $objectUuid); + }//end buildObjectDetailLink() + + /** + * Resolve the absolute frontend route base for an app id. + * + * Falls back to the openregister dashboard route when the app is not + * installed or the URL generator is unavailable. + * + * @param string $app The app id. + * + * @return string The absolute route base (always ends without a trailing fragment). + */ + private function appRouteBase(string $app): string + { + if ($this->urlGenerator === null) { + return '/index.php/apps/'.$app.'/'; + } + + $routeName = $app.'.dashboard.page'; + if ($app === 'openregister') { + $routeName = 'openregister.dashboard.page'; + } + + try { + return $this->urlGenerator->linkToRouteAbsolute($routeName); + } catch (\Throwable $e) { + // App has no dashboard.page route — fall back to the app base path. + return $this->urlGenerator->getAbsoluteURL('/index.php/apps/'.$app.'/'); + }//end try + + }//end appRouteBase() + + /** + * Enqueue a background web-push dispatch job per recipient. + * + * Push I/O (VAPID signing + aes128gcm encryption + endpoint POST) runs in + * WebPushDispatchJob so the originating object-save request returns + * immediately. Anonymous / non-uid recipients are skipped (web-push is + * user+browser scoped). + * + * @param array $recipients Resolved recipient uids (validated at runtime). + * @param string $ruleId The rule id. + * @param string $originApp The resolved origin app id. + * @param string $subject The notification subject text (push title). + * @param string $message The notification body text (push body). + * @param array> $actions Resolved action buttons. + * @param ObjectEntity $object The triggering object. + * + * @return void + * + * @spec openspec/changes/openregister-notification-body/specs/notificatie-engine/spec.md + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + private function enqueueWebPush( + array $recipients, + string $ruleId, + string $originApp, + string $subject, + string $message, + array $actions, + ObjectEntity $object + ): void { + if ($this->jobList === null) { + $this->logger->debug('[AnnotationNotificationDispatcher] web-push declared but IJobList unavailable.'); + return; + } + + $objectUuid = (string) ($object->getUuid() ?? ''); + $tagSuffix = $ruleId; + if ($objectUuid !== '') { + $tagSuffix = $objectUuid; + } + + $tag = sprintf('openregister-%s-%s', $ruleId, $tagSuffix); + + foreach ($recipients as $uid) { + if (is_string($uid) === false || $uid === '' || $this->userExists(uid: $uid) === false) { + continue; + } + + $this->jobList->add( + WebPushDispatchJob::class, + [ + 'uid' => $uid, + 'ruleId' => $ruleId, + 'originApp' => $originApp, + 'title' => $subject, + 'body' => $message, + 'tag' => $tag, + 'actions' => $actions, + ] + ); + }//end foreach + }//end enqueueWebPush() + /** * Persist + dispatch a single in-app Nextcloud notification row. * @@ -1746,14 +2910,21 @@ private function canonicalSubject(string $trigger): string * Push delivery needs no extra code: `notify_push` auto-intercepts this * same `IManager::notify()` call and relays it to connected devices. * - * @param string $uid Recipient user UID. - * @param ObjectEntity $object The object the event happened on. - * @param string $subjectKey Canonical subject identifier (object_created/_updated/_transitioned). - * @param string $name Annotation rule name (notification type identifier). - * @param string $subject Pre-interpolated subject text. - * @param array $context Trigger context (action, from, to). + * @param string $uid Recipient user UID. + * @param ObjectEntity $object The object the event happened on. + * @param string $subjectKey Canonical subject identifier (object_created/_updated/_transitioned). + * @param string $name Annotation rule name (notification type identifier). + * @param string $subject Pre-interpolated subject text (notification title). + * @param string $message Pre-interpolated body text (notification body); may be empty. + * @param array $context Trigger context (action, from, to). + * @param string $originApp Resolved originApp (declared or register-owning app). + * @param array> $actions Resolved action buttons (label map + deeplink url + primary). + * @param bool $webPushActive Whether the rule also delivers over web-push (drives duplicate suppression). * * @return void + * + * @spec openspec/changes/openregister-notification-body/specs/notificatie-engine/spec.md + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md */ private function emitNotification( string $uid, @@ -1761,14 +2932,42 @@ private function emitNotification( string $subjectKey, string $name, string $subject, - array $context + string $message, + array $context, + string $originApp='openregister', + array $actions=[], + bool $webPushActive=false ): void { $objectUuid = (string) ($object->getUuid() ?? ''); + $tagSuffix = $name; + if ($objectUuid !== '') { + $tagSuffix = $objectUuid; + } + $linkParams = [ - 'objectTitle' => (string) ($object->getName() ?? $objectUuid), - 'registerId' => $object->getRegister(), - 'schemaId' => $object->getSchema(), - 'objectUuid' => $objectUuid, + 'objectTitle' => (string) ($object->getName() ?? $objectUuid), + 'registerId' => $object->getRegister(), + 'schemaId' => $object->getSchema(), + 'objectUuid' => $objectUuid, + // The resolved origin app drives the notifier icon (originApp hex + // composite) and the deeplink base for declared actions. + 'originApp' => $originApp, + // Declared, server-resolved action buttons. The notifier renders + // these via addAction(); an empty array keeps the implicit "View". + '_actions' => $actions, + // Pre-interpolated notification BODY (distinct from the title). + // The notifier sets it via setParsedMessage() when non-empty; + // an empty string leaves the body unset (back-compat). + '_message' => $message, + // Stable notification tag used by the Service Worker / foreground + // client to COLLAPSE the web-push and the stock popup so the + // recipient never sees a duplicate. Keyed by (rule, object). + '_tag' => sprintf('openregister-%s-%s', $name, $tagSuffix), + // Foreground-suppression flag: when web-push is active for this + // rule, an open tab that holds an active push subscription + // declines to render the plain duplicate popup for this tag + // (see js/openregister-push-sw.js + src/webpush/register.js). + '_suppressForegroundPopup' => $webPushActive, ]; $objectRef = $name; diff --git a/lib/Service/Notification/DigestScheduleEvaluator.php b/lib/Service/Notification/DigestScheduleEvaluator.php new file mode 100644 index 0000000000..d99ec7e191 --- /dev/null +++ b/lib/Service/Notification/DigestScheduleEvaluator.php @@ -0,0 +1,154 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/notification-delivery-windows/design.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Notification; + +use DateTimeImmutable; + +/** + * Live evaluator for the `digest` fixed-time schedule dialect key. + */ +class DigestScheduleEvaluator +{ + /** + * Constructor. + * + * @param NotificationDeliveryWindowService $windowService Reused solely for its + * timezone-resolution helper + * (declared name, else server default). + */ + public function __construct(private readonly NotificationDeliveryWindowService $windowService) + { + }//end __construct() + + /** + * Whether a rule's `digest` block is well-formed enough to evaluate. + * + * @param mixed $digest Candidate `digest` block from the rule spec. + * + * @return bool + */ + public function isValidDigestSpec(mixed $digest): bool + { + if (is_array($digest) === false) { + return false; + } + + $schedule = ($digest['schedule'] ?? null); + if (in_array($schedule, ['daily', 'weekly'], true) === false) { + return false; + } + + $at = ($digest['at'] ?? null); + if (is_string($at) === false || preg_match('/^([01]\d|2[0-3]):([0-5]\d)$/', $at) !== 1) { + return false; + } + + if ($schedule === 'weekly') { + $weekday = ($digest['weekday'] ?? null); + if (is_int($weekday) === false || $weekday < 0 || $weekday > 6) { + return false; + } + } + + return true; + + }//end isValidDigestSpec() + + /** + * The most recent scheduled occurrence at-or-before `$now`. + * + * @param array $digest Validated `digest` block. + * @param DateTimeImmutable $now The instant to evaluate from. + * + * @return DateTimeImmutable + */ + public function lastOccurrence(array $digest, DateTimeImmutable $now): DateTimeImmutable + { + $timezoneSpec = null; + if (is_string($digest['timezone'] ?? null) === true) { + $timezoneSpec = $digest['timezone']; + } + + $tz = $this->windowService->resolveTimezone(tzName: $timezoneSpec); + $schedule = (string) ($digest['schedule'] ?? 'daily'); + $at = (string) ($digest['at'] ?? '00:00'); + $weekday = $digest['weekday'] ?? null; + + [$hour, $minute] = array_map('intval', explode(':', $at)); + + $localNow = $now->setTimezone($tz); + $candidate = $localNow->setTime($hour, $minute, 0); + + for ($i = 0; $i < 8; $i++) { + $matchesDay = ($schedule !== 'weekly') || ((int) $candidate->format('w') === (int) $weekday); + if ($matchesDay === true && $candidate <= $localNow) { + return $candidate; + } + + $candidate = $candidate->modify('-1 day'); + } + + // Should be unreachable (a week always contains one matching + // weekday), but fall back to "one week ago" rather than throwing. + return $candidate; + + }//end lastOccurrence() + + /** + * Whether a row enqueued at `$enqueuedAt` is due to flush at `$now` + * under the given `digest` schedule: the schedule's most recent + * occurrence at-or-before `$now` happened AFTER the row was enqueued. + * + * @param array $digest Validated `digest` block. + * @param DateTimeImmutable $enqueuedAt When the row was queued. + * @param DateTimeImmutable $now The instant to evaluate from. + * + * @return bool + */ + public function isDue(array $digest, DateTimeImmutable $enqueuedAt, DateTimeImmutable $now): bool + { + if ($this->isValidDigestSpec(digest: $digest) === false) { + // Malformed schedule — fail open (treat as due) so a bad + // annotation cannot indefinitely trap events in the queue. + return true; + } + + $last = $this->lastOccurrence(digest: $digest, now: $now); + + return $last > $enqueuedAt; + + }//end isDue() +}//end class diff --git a/lib/Service/Notification/NotificationAnnotationValidator.php b/lib/Service/Notification/NotificationAnnotationValidator.php index b8259158d5..cd93c1b33b 100644 --- a/lib/Service/Notification/NotificationAnnotationValidator.php +++ b/lib/Service/Notification/NotificationAnnotationValidator.php @@ -34,7 +34,10 @@ * - `subject`: string template OR per-locale map * ({nl: "...", en: "...", defaultLocale?: "nl"}; supports {{field}} * interpolation; recipient locale via `core.lang` user preference) - * - optional `message`: string + * - optional `message`: notification body — same shape as `subject` + * (string template OR per-locale map, with {{field}} interpolation). + * Title=`subject`, body=`message`. When absent, the body is auto-derived + * ("Open in {AppName}.") if `actions` are declared, else left empty. * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ @@ -45,7 +48,22 @@ final class NotificationAnnotationValidator private const VALID_RECIPIENT_KINDS = ['users', 'field', 'groups', 'relation', 'object-acl', 'expression']; - private const VALID_CHANNELS = ['nc-notification', 'email', 'activity', 'webhook', 'talk']; + private const VALID_CHANNELS = ['nc-notification', 'email', 'activity', 'webhook', 'talk', 'web-push']; + + /** + * Valid action `target.kind` values (foundation contract / ADR-031 dialect). + * + * @var array + */ + private const VALID_ACTION_TARGET_KINDS = ['object-detail', 'route', 'url']; + + /** + * Hard cap on declared action buttons — the Web Notification API renders + * at most two action buttons on the desktop OS popup. + * + * @var int + */ + private const MAX_ACTIONS = 2; /** * Validate the `x-openregister-notifications` annotation. @@ -59,6 +77,7 @@ final class NotificationAnnotationValidator * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-5 + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md */ public function validate(array $schema): array { @@ -132,7 +151,70 @@ public function validate(array $schema): array ), ]; } - } + + // Validate filter operator grammar (Phase 2 — save-time grammar validation). + // Scalar entries always pass; operator objects must declare a known operator, + // a present `value`, and an ISO-8601 DateInterval-parseable duration for + // withinNext / olderThan. See notification-engine-scheduled-conditions. + if (is_array($trigger) === true && isset($trigger['filter']) === true) { + $filter = $trigger['filter']; + if (is_array($filter) === false) { + $errors[] = [ + 'code' => 'notification-scheduled-bad-filter', + 'ruleKey' => $name, + 'field' => 'trigger.filter', + 'value' => $filter, + 'message' => sprintf( + 'Notification "%s" trigger.filter must be an object/map.', + $name + ), + ]; + } else { + foreach ($filter as $fieldKey => $filterSpec) { + $entryErrors = $this->validateScheduledFilterEntry( + ruleKey: $name, + field: (string) $fieldKey, + spec: $filterSpec + ); + foreach ($entryErrors as $entryError) { + $errors[] = $entryError; + } + }//end foreach + }//end if + }//end if + + if (is_array($trigger) === true && isset($trigger['dedupeFields']) === true) { + $dedupeFields = $trigger['dedupeFields']; + if (is_array($dedupeFields) === false || $dedupeFields === []) { + $errors[] = [ + 'code' => 'notification-scheduled-bad-dedupe-fields', + 'ruleKey' => $name, + 'field' => 'trigger.dedupeFields', + 'value' => $dedupeFields, + 'message' => sprintf( + 'Notification "%s" trigger.dedupeFields must be a non-empty array of strings.', + $name + ), + ]; + } else { + foreach ($dedupeFields as $idx => $field) { + if (is_string($field) === false || $field === '') { + $errors[] = [ + 'code' => 'notification-scheduled-bad-dedupe-fields', + 'ruleKey' => $name, + 'field' => sprintf('trigger.dedupeFields[%d]', (int) $idx), + 'value' => $field, + 'message' => sprintf( + 'Notification "%s" trigger.dedupeFields entries must be non-empty strings.', + $name + ), + ]; + break; + } + }//end foreach + }//end if + }//end if + }//end if if ($triggerType === 'threshold') { $aggregation = ''; @@ -257,6 +339,35 @@ public function validate(array $schema): array } } + // Optional `originApp` (foundation contract / ADR-031): identifies + // the leaf app that owns the rule. Drives the notification + // icon/badge and the deeplink base. When present it MUST be a + // non-empty string; absence is valid (defaults to the + // register-owning app at dispatch). + if (array_key_exists('originApp', $spec) === true) { + $originApp = $spec['originApp']; + if (is_string($originApp) === false || $originApp === '') { + $errors[] = [ + 'code' => 'notification-bad-origin-app', + 'message' => sprintf( + 'Notification "%s" originApp must be a non-empty string app id.', + $name + ), + ]; + } + } + + // Optional `actions[]` (foundation contract / ADR-031): rich + // action buttons rendered in the OS notification. Hard cap of 2 + // (Web Notification API desktop limit). Each action declares an + // i18n `label` map, an optional `primary` bool, and a `target` + // of kind object-detail | route | url. + if (array_key_exists('actions', $spec) === true) { + foreach ($this->validateActions(actions: $spec['actions'], name: $name) as $actionError) { + $errors[] = $actionError; + } + } + // `subject` accepts either a single template string OR a // per-locale map ({nl: "...", en: "..."} optionally prefixed // with `defaultLocale: `). The dispatcher resolves @@ -331,6 +442,19 @@ public function validate(array $schema): array } }//end if + // Optional `message` (foundation contract / ADR-031): the + // notification BODY, distinct from the title (`subject`). Same + // shape + locale-resolution + `{{prop}}` interpolation as + // `subject` — a single template string OR a per-locale map. + // Absence is valid (back-compat): rules with `actions` get an + // auto-derived "Open in {AppName}." body, rules without get an + // empty body. Malformed → notification-bad-message. + if (array_key_exists('message', $spec) === true) { + foreach ($this->validateMessage(message: $spec['message'], name: $name) as $messageError) { + $errors[] = $messageError; + } + } + // When the `webhook` channel is declared, the spec MUST include a `webhook.url` value. if (is_array($channels) === true && in_array('webhook', $channels, true) === true) { $hook = ($spec['webhook'] ?? null); @@ -384,6 +508,13 @@ public function validate(array $schema): array } } + // Optional `critical` bypass flag and optional fixed-time + // `digest` schedule (notification-delivery-windows dialect + // additions). See "Users MUST be able to manage their + // notification preferences" / "Notifications MUST support + // batching and digest delivery" in the notificatie-engine spec. + $errors = array_merge($errors, $this->validateCriticalAndDigest(spec: $spec, name: $name)); + $recipients = ($spec['recipients'] ?? []); if (is_array($recipients) === false || count($recipients) === 0) { $errors[] = [ @@ -473,6 +604,341 @@ public function validate(array $schema): array return $errors; }//end validate() + /** + * Validate the optional `actions[]` array (foundation contract / ADR-031). + * + * Rules: + * - `actions` MUST be an array; more than 2 entries → notification-too-many-actions. + * - each action's `label` MUST be a per-locale map with at least one + * non-empty locale value → otherwise notification-action-bad-label. + * - each action's `target.kind` MUST be one of object-detail | route | url + * → otherwise notification-action-bad-target. + * + * @param mixed $actions Raw value of the `actions` key. + * @param string $name Notification name (for diagnostics). + * + * @return array + * + * @spec openspec/changes/openregister-web-push-engine/specs/notificatie-engine/spec.md + */ + private function validateActions(mixed $actions, string $name): array + { + $errors = []; + + if (is_array($actions) === false) { + return [ + [ + 'code' => 'notification-action-bad-target', + 'message' => sprintf('Notification "%s" actions must be an array.', $name), + ], + ]; + } + + if (count($actions) > self::MAX_ACTIONS) { + $errors[] = [ + 'code' => 'notification-too-many-actions', + 'message' => sprintf( + 'Notification "%s" declares %d actions; the Web Notification API renders at most %d.', + $name, + count($actions), + self::MAX_ACTIONS + ), + ]; + } + + foreach ($actions as $idx => $action) { + if (is_array($action) === false) { + $errors[] = [ + 'code' => 'notification-action-bad-target', + 'message' => sprintf('Notification "%s" action[%s] must be an object.', $name, (string) $idx), + ]; + continue; + } + + // Label MUST be a per-locale map with at least one non-empty string value. + $label = ($action['label'] ?? null); + $labelOk = false; + if (is_array($label) === true && count($label) > 0) { + foreach ($label as $localeValue) { + if (is_string($localeValue) === true && $localeValue !== '') { + $labelOk = true; + break; + } + } + } + + if ($labelOk === false) { + $errors[] = [ + 'code' => 'notification-action-bad-label', + 'message' => sprintf( + 'Notification "%s" action[%s] label must be a per-locale map with at least one non-empty value.', + $name, + (string) $idx + ), + ]; + } + + // `primary` (optional) MUST be a boolean when present. + if (array_key_exists('primary', $action) === true && is_bool($action['primary']) === false) { + $errors[] = [ + 'code' => 'notification-action-bad-target', + 'message' => sprintf( + 'Notification "%s" action[%s] primary must be a boolean.', + $name, + (string) $idx + ), + ]; + } + + // Target MUST be an object with a recognised kind. + $target = ($action['target'] ?? null); + $targetKind = ''; + if (is_array($target) === true) { + $targetKind = (string) ($target['kind'] ?? ''); + } + + if (in_array($targetKind, self::VALID_ACTION_TARGET_KINDS, true) === false) { + $errors[] = [ + 'code' => 'notification-action-bad-target', + 'message' => sprintf( + 'Notification "%s" action[%s] target.kind "%s" is not in [%s].', + $name, + (string) $idx, + $targetKind, + implode(', ', self::VALID_ACTION_TARGET_KINDS) + ), + ]; + } + }//end foreach + + return $errors; + }//end validateActions() + + /** + * Validate the optional `critical` bypass flag and the optional + * fixed-time `digest` schedule block. + * + * Rules: + * - `critical`, when present, MUST be a boolean. + * - `digest.schedule`, when the block is present, MUST be + * `daily` | `weekly`. + * - `digest.at` MUST be an `HH:MM` 24h time string. + * - `digest.weekday` (0-6) is REQUIRED when `schedule: "weekly"` and + * FORBIDDEN otherwise. + * - `digest.timezone`, when present, MUST be a value `DateTimeZone` + * accepts. + * - A rule MUST NOT declare both a `digest` block and a rolling + * `coalesce` window — the two "hold and batch" mechanisms are + * mutually exclusive per rule (design.md "Digest scheduling is + * additive to, not a replacement for, the rolling digest window"). + * + * @param array $spec The full notification spec block. + * @param string $name Notification name (for diagnostics). + * + * @return array + * + * @spec openspec/specs/notificatie-engine/spec.md + */ + private function validateCriticalAndDigest(array $spec, string $name): array + { + $errors = []; + + if (array_key_exists('critical', $spec) === true && is_bool($spec['critical']) === false) { + $errors[] = [ + 'code' => 'notification-critical-not-boolean', + 'message' => sprintf('Notification "%s" `critical` must be a boolean.', $name), + ]; + } + + if (array_key_exists('digest', $spec) === false) { + return $errors; + } + + $digest = $spec['digest']; + if (is_array($digest) === false) { + $errors[] = [ + 'code' => 'notification-digest-malformed', + 'message' => sprintf('Notification "%s" `digest` must be an object.', $name), + ]; + return $errors; + } + + $schedule = ($digest['schedule'] ?? null); + if (in_array($schedule, ['daily', 'weekly'], true) === false) { + $errors[] = [ + 'code' => 'notification-digest-bad-schedule', + 'message' => sprintf( + 'Notification "%s" `digest.schedule` must be "daily" or "weekly".', + $name + ), + ]; + } + + $at = ($digest['at'] ?? null); + if (is_string($at) === false || preg_match('/^([01]\d|2[0-3]):([0-5]\d)$/', $at) !== 1) { + $errors[] = [ + 'code' => 'notification-digest-bad-time', + 'message' => sprintf('Notification "%s" `digest.at` must be an "HH:MM" time string.', $name), + ]; + } + + $hasWeekday = array_key_exists('weekday', $digest); + if ($schedule === 'weekly') { + $weekday = ($digest['weekday'] ?? null); + if ($hasWeekday === false || is_int($weekday) === false || $weekday < 0 || $weekday > 6) { + $errors[] = [ + 'code' => 'notification-digest-weekly-missing-weekday', + 'message' => sprintf( + 'Notification "%s" `digest.weekday` (0-6) is required when `schedule` is "weekly".', + $name + ), + ]; + } + } else if ($hasWeekday === true) { + $errors[] = [ + 'code' => 'notification-digest-weekday-not-allowed', + 'message' => sprintf( + 'Notification "%s" `digest.weekday` is only allowed when `schedule` is "weekly".', + $name + ), + ]; + } + + $timezone = ($digest['timezone'] ?? null); + if ($timezone !== null) { + $timezoneValid = false; + if (is_string($timezone) === true && $timezone !== '') { + try { + new \DateTimeZone($timezone); + $timezoneValid = true; + } catch (\Throwable $e) { + $timezoneValid = false; + } + } + + if ($timezoneValid === false) { + $errors[] = [ + 'code' => 'notification-digest-bad-timezone', + 'message' => sprintf( + 'Notification "%s" `digest.timezone` must be a valid IANA timezone name.', + $name + ), + ]; + } + }//end if + + // Mutually exclusive with the rolling `coalesce` window — a rule + // picks one "hold and batch" mechanism, not both. + if (is_array($spec['coalesce'] ?? null) === true) { + $errors[] = [ + 'code' => 'notification-digest-and-coalesce-mutually-exclusive', + 'message' => sprintf( + 'Notification "%s" declares both a rolling `coalesce` window and a fixed-time ' + .'`digest` schedule; a rule may declare at most one batching mechanism.', + $name + ), + ]; + } + + return $errors; + + }//end validateCriticalAndDigest() + + /** + * Validate the optional `message` field (notification body). + * + * Mirrors the `subject` shape contract exactly: either a single + * non-empty template string OR a per-locale map ({nl: "...", en: "..."} + * optionally prefixed with `defaultLocale: `). Every malformed + * shape returns a single `notification-bad-message` error so leaf + * authors get one canonical error code for a broken body template. + * + * @param mixed $message Raw value of the `message` key. + * @param string $name Notification name (for diagnostics). + * + * @return array + * + * @spec openspec/changes/openregister-notification-body/specs/notificatie-engine/spec.md + */ + private function validateMessage(mixed $message, string $name): array + { + $code = 'notification-bad-message'; + + if (is_string($message) === true) { + if ($message === '') { + return [ + [ + 'code' => $code, + 'message' => sprintf( + 'Notification "%s" message must be a non-empty string when present.', + $name + ), + ], + ]; + } + + return []; + } + + if (is_array($message) === false) { + return [ + [ + 'code' => $code, + 'message' => sprintf( + 'Notification "%s" message must be a string or a per-locale map.', + $name + ), + ], + ]; + } + + $errors = []; + $localeKeys = array_filter( + array_keys($message), + static fn ($key): bool => $key !== 'defaultLocale' && is_string($key) === true + ); + if (count($localeKeys) === 0) { + $errors[] = [ + 'code' => $code, + 'message' => sprintf( + 'Notification "%s" message map must declare at least one locale (e.g. nl, en).', + $name + ), + ]; + } + + foreach ($localeKeys as $localeKey) { + if (is_string($message[$localeKey]) === false || $message[$localeKey] === '') { + $errors[] = [ + 'code' => $code, + 'message' => sprintf( + 'Notification "%s" message for locale "%s" must be a non-empty string.', + $name, + $localeKey + ), + ]; + } + } + + if (isset($message['defaultLocale']) === true) { + $defaultLocale = $message['defaultLocale']; + $defaultLocaleBad = is_string($defaultLocale) === false; + $defaultLocaleBad |= isset($message[$defaultLocale]) === false; + if ((bool) $defaultLocaleBad === true) { + $errors[] = [ + 'code' => $code, + 'message' => sprintf( + 'Notification "%s" message defaultLocale "%s" is not declared in the message map.', + $name, + (string) $defaultLocale + ), + ]; + } + } + + return $errors; + }//end validateMessage() + /** * Validate the optional `organisation` rule-level gate. * @@ -536,6 +1002,107 @@ private function validateOrganisationGate(mixed $org, string $name): ?array $name ), ]; - }//end validateOrganisationGate() + + /** + * Validate a single scheduled-trigger filter entry. + * + * Scalar entries always pass (legacy v1 byte-for-byte equality). + * Operator-object entries must declare: + * - a recognised operator (equals | notEquals | withinNext | olderThan); + * - a `value` key (even if null) — for equals/notEquals; + * - an ISO-8601 DateInterval-parseable string `value` for withinNext / olderThan. + * + * @param string $ruleKey The notification rule key for diagnostics. + * @param string $field The filter field name being inspected. + * @param mixed $spec The raw filter entry value (scalar or operator object). + * + * @return array + */ + private function validateScheduledFilterEntry(string $ruleKey, string $field, $spec): array + { + // Scalar shortcut: always accepted (legacy v1 strict equality). + if (is_array($spec) === false || array_key_exists('operator', $spec) === false) { + return []; + } + + $validOperators = ['equals', 'notEquals', 'withinNext', 'olderThan']; + $operator = (string) ($spec['operator'] ?? ''); + + if (in_array($operator, $validOperators, true) === false) { + return [ + [ + 'code' => 'notification-scheduled-bad-filter-operator', + 'ruleKey' => $ruleKey, + 'field' => sprintf('trigger.filter.%s.operator', $field), + 'value' => $operator, + 'message' => sprintf( + 'Notification "%s" trigger.filter.%s.operator must be one of [%s]; got "%s".', + $ruleKey, + $field, + implode(', ', $validOperators), + $operator + ), + ], + ]; + } + + if (array_key_exists('value', $spec) === false) { + return [ + [ + 'code' => 'notification-scheduled-bad-filter-missing-value', + 'ruleKey' => $ruleKey, + 'field' => sprintf('trigger.filter.%s.value', $field), + 'value' => null, + 'message' => sprintf( + 'Notification "%s" trigger.filter.%s requires a "value" key.', + $ruleKey, + $field + ), + ], + ]; + } + + if ($operator === 'withinNext' || $operator === 'olderThan') { + $duration = $spec['value']; + if (is_string($duration) === false || $duration === '') { + return [ + [ + 'code' => 'notification-scheduled-bad-filter-duration', + 'ruleKey' => $ruleKey, + 'field' => sprintf('trigger.filter.%s.value', $field), + 'value' => $duration, + 'message' => sprintf( + 'Notification "%s" trigger.filter.%s.value must be an ISO-8601 DateInterval string for "%s".', + $ruleKey, + $field, + $operator + ), + ], + ]; + } + + try { + new \DateInterval($duration); + } catch (\Exception $e) { + return [ + [ + 'code' => 'notification-scheduled-bad-filter-duration', + 'ruleKey' => $ruleKey, + 'field' => sprintf('trigger.filter.%s.value', $field), + 'value' => $duration, + 'message' => sprintf( + 'Notification "%s" trigger.filter.%s.value "%s" is not a valid ISO-8601 DateInterval for "%s".', + $ruleKey, + $field, + $duration, + $operator + ), + ], + ]; + }//end try + }//end if + + return []; + }//end validateScheduledFilterEntry() }//end class diff --git a/lib/Service/Notification/NotificationDeliveryWindowService.php b/lib/Service/Notification/NotificationDeliveryWindowService.php new file mode 100644 index 0000000000..fb8016f332 --- /dev/null +++ b/lib/Service/Notification/NotificationDeliveryWindowService.php @@ -0,0 +1,364 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/notification-delivery-windows/design.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Notification; + +use DateTimeImmutable; +use DateTimeZone; +use OCP\IConfig; +use OCP\IDateTimeZone; +use Psr\Log\LoggerInterface; + +/** + * Resolves and stores a user's override-only delivery-window preference, + * and evaluates whether a given instant falls inside it. + */ +class NotificationDeliveryWindowService +{ + /** + * App id used for the user-config namespace. + */ + private const APP_NAME = 'openregister'; + + /** + * Per-user app-config key holding the JSON-encoded window. + */ + private const CONFIG_KEY = 'notification_delivery_window'; + + /** + * Constructor. + * + * @param IConfig $config Nextcloud config for per-user values. + * @param IDateTimeZone|null $serverTimezone Server-timezone resolver, used as the + * fallback when a window declares no `timezone`. + * @param LoggerInterface|null $logger Optional logger for diagnostics. + */ + public function __construct( + private readonly IConfig $config, + private readonly ?IDateTimeZone $serverTimezone=null, + private readonly ?LoggerInterface $logger=null + ) { + }//end __construct() + + /** + * Read a user's stored delivery-window preference. Returns null when no + * value is stored (zero-migration "no window configured" case) or when + * the stored value is malformed — never throws for a corrupt row. + * + * @param string $userId The user UID. + * + * @return array|null The decoded window, or null when none/invalid. + */ + public function getForUser(string $userId): ?array + { + $raw = $this->config->getUserValue($userId, self::APP_NAME, self::CONFIG_KEY, ''); + if ($raw === '') { + return null; + } + + $decoded = json_decode($raw, true); + if (is_array($decoded) === false) { + return null; + } + + return $decoded; + + }//end getForUser() + + /** + * Write or clear a user's delivery-window preference. Passing null (or + * `{enabled: false}`) clears the stored value so the user is never + * queued for quiet hours (zero-migration fall-through). + * + * @param string $userId The user UID. + * @param array|null $window Window body, or null to clear. + * + * @return void + * + * @throws \InvalidArgumentException When `$window` is present but malformed. + */ + public function setForUser(string $userId, ?array $window): void + { + if ($window === null || ($window['enabled'] ?? true) === false) { + $this->config->deleteUserValue($userId, self::APP_NAME, self::CONFIG_KEY); + return; + } + + $clean = $this->validateAndNormalize(window: $window); + + $this->config->setUserValue($userId, self::APP_NAME, self::CONFIG_KEY, json_encode($clean)); + + }//end setForUser() + + /** + * Validate a window body and return its normalized form. + * + * @param array $window Raw window body. + * + * @return array{enabled: bool, start: string, end: string, timezone: string, days: array|null} + * + * @throws \InvalidArgumentException When `start`/`end`/`timezone` are malformed. + */ + public function validateAndNormalize(array $window): array + { + $start = $window['start'] ?? null; + $end = $window['end'] ?? null; + if ($this->isValidTimeOfDay(value: $start) === false) { + throw new \InvalidArgumentException('"start" must be an "HH:MM" time string.'); + } + + if ($this->isValidTimeOfDay(value: $end) === false) { + throw new \InvalidArgumentException('"end" must be an "HH:MM" time string.'); + } + + $timezone = $window['timezone'] ?? null; + if ($timezone !== null) { + if (is_string($timezone) === false || $timezone === '' || $this->isValidTimezone(value: $timezone) === false) { + throw new \InvalidArgumentException('"timezone" must be a valid IANA timezone name.'); + } + } + + $days = null; + if (isset($window['days']) === true) { + if (is_array($window['days']) === false) { + throw new \InvalidArgumentException('"days" must be an array of integers 0-6.'); + } + + $days = []; + foreach ($window['days'] as $day) { + if (is_int($day) === false || $day < 0 || $day > 6) { + throw new \InvalidArgumentException('"days" entries must be integers 0-6.'); + } + + $days[] = $day; + } + + $days = array_values(array_unique($days)); + } + + $timezoneName = $this->resolveServerTimezoneName(); + if ($timezone !== null) { + $timezoneName = (string) $timezone; + } + + return [ + 'enabled' => true, + 'start' => (string) $start, + 'end' => (string) $end, + 'timezone' => $timezoneName, + 'days' => $days, + ]; + + }//end validateAndNormalize() + + /** + * Whether `$now` falls inside `$window` — the recipient is currently in + * quiet hours. Resolves `now` in the window's declared `timezone` + * (falling back to the Nextcloud server timezone when absent), so DST + * shifts are handled by PHP's tz database rather than by a precomputed + * instant (see design.md "Timezone handling"). + * + * Correctly wraps past-midnight ranges (e.g. `18:00`-`08:00`): active + * from 18:00 through 23:59 AND from 00:00 through 07:59. + * + * @param array $window Decoded window (as returned by getForUser()). + * @param DateTimeImmutable $now The instant to evaluate (any timezone; re-projected below). + * + * @return bool True when `$now` is inside the configured window. + */ + public function isInsideWindow(array $window, DateTimeImmutable $now): bool + { + if (($window['enabled'] ?? false) !== true) { + return false; + } + + $start = $window['start'] ?? null; + $end = $window['end'] ?? null; + if ($this->isValidTimeOfDay(value: $start) === false || $this->isValidTimeOfDay(value: $end) === false) { + return false; + } + + $tzName = $window['timezone'] ?? null; + if (is_string($tzName) === false) { + $tzName = null; + } + + $tz = $this->resolveTimezone(tzName: $tzName); + + $local = $now->setTimezone($tz); + + $startMinutes = $this->minutesSinceMidnight(time: (string) $start); + $endMinutes = $this->minutesSinceMidnight(time: (string) $end); + $nowMinutes = ((int) $local->format('G') * 60) + (int) $local->format('i'); + $weekday = (int) $local->format('w'); + + if ($startMinutes === $endMinutes) { + // Ambiguous zero-length / full-day range — treat as "never + // active" rather than guessing 24h-always-on. + return false; + } + + $days = $window['days'] ?? null; + if (is_array($days) === true && count($days) > 0) { + $todayAllowed = in_array($weekday, $days, true) === true; + $yesterdayAllowed = in_array((($weekday + 6) % 7), $days, true) === true; + + if ($startMinutes < $endMinutes) { + return $todayAllowed === true + && $nowMinutes >= $startMinutes + && $nowMinutes < $endMinutes; + } + + // Wrapping window: the "today" leg only applies on days the + // window starts; the "past midnight" leg belongs to the day + // the window STARTED (yesterday, from now's perspective). + return ($todayAllowed === true && $nowMinutes >= $startMinutes) + || ($yesterdayAllowed === true && $nowMinutes < $endMinutes); + } + + if ($startMinutes < $endMinutes) { + return $nowMinutes >= $startMinutes && $nowMinutes < $endMinutes; + } + + // Wrapping window, no day restriction: active either side of midnight. + return $nowMinutes >= $startMinutes || $nowMinutes < $endMinutes; + + }//end isInsideWindow() + + /** + * Resolve a `DateTimeZone` from a declared name, falling back to the + * Nextcloud server timezone (never a hardcoded UTC/Europe/Amsterdam + * default) when absent or invalid. + * + * @param string|null $tzName Declared IANA timezone name, or null. + * + * @return DateTimeZone + */ + public function resolveTimezone(?string $tzName): DateTimeZone + { + if ($tzName !== null && $tzName !== '' && $this->isValidTimezone(value: $tzName) === true) { + try { + return new DateTimeZone($tzName); + } catch (\Throwable $e) { + // Fall through to server default below. + } + } + + if ($this->serverTimezone !== null) { + try { + return $this->serverTimezone->getTimeZone(); + } catch (\Throwable $e) { + $this->logger?->debug( + '[NotificationDeliveryWindowService] server timezone resolution failed: '.$e->getMessage() + ); + } + } + + return new DateTimeZone('UTC'); + + }//end resolveTimezone() + + /** + * Name of the server-default timezone (used to persist a resolved + * default when a window is saved without an explicit `timezone`). + * + * @return string + */ + private function resolveServerTimezoneName(): string + { + return $this->resolveTimezone(tzName: null)->getName(); + + }//end resolveServerTimezoneName() + + /** + * Whether `$value` is a well-formed "HH:MM" 24h time-of-day string. + * + * @param mixed $value Candidate value. + * + * @return bool + */ + private function isValidTimeOfDay(mixed $value): bool + { + if (is_string($value) === false) { + return false; + } + + if (preg_match('/^([01]\d|2[0-3]):([0-5]\d)$/', $value) !== 1) { + return false; + } + + return true; + + }//end isValidTimeOfDay() + + /** + * Whether `$value` names a timezone `DateTimeZone` accepts. + * + * @param string $value Candidate IANA timezone name. + * + * @return bool + */ + private function isValidTimezone(string $value): bool + { + try { + new DateTimeZone($value); + return true; + } catch (\Throwable $e) { + return false; + } + + }//end isValidTimezone() + + /** + * Parse an "HH:MM" string into minutes since midnight. + * + * @param string $time "HH:MM" string (already validated). + * + * @return int + */ + private function minutesSinceMidnight(string $time): int + { + [$hours, $minutes] = explode(':', $time); + return ((int) $hours * 60) + (int) $minutes; + + }//end minutesSinceMidnight() +}//end class diff --git a/lib/Service/Notification/NotificationDigest.php b/lib/Service/Notification/NotificationDigest.php deleted file mode 100644 index 6bbcc74af7..0000000000 --- a/lib/Service/Notification/NotificationDigest.php +++ /dev/null @@ -1,130 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @link https://www.OpenRegister.app - * - * @spec openspec/changes/notificatie-engine/specs/notificatie-engine/spec.md "Notifications MUST support batching and digest delivery" - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service\Notification; - -/** - * Aggregates pending notifications per recipient until flushed. - */ -class NotificationDigest -{ - - /** - * Pending events keyed by recipient identifier. - * - * @var array> - */ - private array $pendingByRecipient = []; - - /** - * Add a notification event to the recipient's bucket. - * - * @param string $recipientId The recipient (uid, email, webhook id…). - * @param array $event The event payload (free-form). - * - * @return void - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-6 - */ - public function enqueue(string $recipientId, array $event): void - { - if (isset($this->pendingByRecipient[$recipientId]) === false) { - $this->pendingByRecipient[$recipientId] = []; - } - - $this->pendingByRecipient[$recipientId][] = $event; - - }//end enqueue() - - /** - * Number of recipients currently buffered. - * - * @return int - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-6 - */ - public function recipientCount(): int - { - return count($this->pendingByRecipient); - - }//end recipientCount() - - /** - * Number of pending events for a single recipient (0 if absent). - * - * @param string $recipientId The recipient identifier. - * - * @return int - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-6 - */ - public function pendingCount(string $recipientId): int - { - return count(($this->pendingByRecipient[$recipientId] ?? [])); - - }//end pendingCount() - - /** - * Total pending events across all recipients. - * - * @return int - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-6 - */ - public function totalPending(): int - { - $total = 0; - foreach ($this->pendingByRecipient as $events) { - $total += count($events); - } - - return $total; - - }//end totalPending() - - /** - * Flush the queue: returns one bucket per recipient with their - * pending events, in original enqueue order. Clears state. - * - * @return array> recipientId → list of events - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-6 - */ - public function flush(): array - { - $buckets = $this->pendingByRecipient; - $this->pendingByRecipient = []; - return $buckets; - - }//end flush() -}//end class diff --git a/lib/Service/Notification/NotificationPreferenceService.php b/lib/Service/Notification/NotificationPreferenceService.php index 49f62ad21d..a2a462cb9f 100644 --- a/lib/Service/Notification/NotificationPreferenceService.php +++ b/lib/Service/Notification/NotificationPreferenceService.php @@ -55,6 +55,11 @@ class NotificationPreferenceService */ private const KEY_PREFIX = 'notification_pref/'; + /** + * Maximum length of a Nextcloud `oc_preferences.configkey` value. + */ + private const MAX_KEY_LENGTH = 64; + /** * Constructor. * @@ -72,14 +77,33 @@ public function __construct( /** * Build the user-config key for a `(schemaSlug, notificationKey)` pair. * + * Nextcloud's `oc_preferences.configkey` column is 64 chars. Most + * `(slug, key)` pairs fit comfortably, but long system-schema slugs + * (e.g. `openregister_configuration` + `configuration-changed` = 66 chars) + * overflow and make the underlying `getUserValue()` / `setUserValue()` + * throw "for key is too long (64)". When the natural key fits, it is + * returned unchanged (full backward compatibility for every stored + * preference); when it would overflow, it is deterministically compressed + * to a stable 64-char key so the same pair always maps to the same key. + * * @param string $schemaSlug The owning schema's slug. * @param string $notificationKey The notification annotation key. * - * @return string The namespaced config key. + * @return string The namespaced config key (<= 64 chars). */ public function configKey(string $schemaSlug, string $notificationKey): string { - return self::KEY_PREFIX.$schemaSlug.'/'.$notificationKey; + $key = self::KEY_PREFIX.$schemaSlug.'/'.$notificationKey; + if (strlen($key) <= self::MAX_KEY_LENGTH) { + return $key; + } + + // Deterministic 64-char fallback: keep the readable prefix and append a + // stable hash of the full pair so distinct pairs never collide. + $hash = substr(hash('sha256', $schemaSlug.'/'.$notificationKey), 0, 16); + $budget = (self::MAX_KEY_LENGTH - strlen(self::KEY_PREFIX) - 1 - strlen($hash)); + $readable = substr($schemaSlug.'/'.$notificationKey, 0, max($budget, 0)); + return self::KEY_PREFIX.$readable.'~'.$hash; }//end configKey() /** @@ -229,6 +253,13 @@ public function getEffectiveForUser(string $userId): array $schemaSlug = (string) ($schema->getSlug() ?? $schema->getId()); $schemaTitle = (string) ($schema->getTitle() ?? $schemaSlug); + // Owning app id (e.g. "pipelinq"), set by the register/schema + // import (ImportHandler::setApplication()) when the schema was + // seeded from an app's configuration. Null for schemas with no + // known owning app (e.g. hand-authored/system schemas); the + // consuming settings UI groups those under an "other" bucket + // rather than dropping them. + $application = $schema->getApplication(); foreach ($notifications as $key => $spec) { if (is_array($spec) === false) { @@ -245,6 +276,7 @@ public function getEffectiveForUser(string $userId): array $entries[] = [ 'schema' => $schemaSlug, 'schemaTitle' => $schemaTitle, + 'application' => $application, 'notification' => (string) $key, 'enabled' => $effective['enabled'], 'channels' => $effective['channels'], diff --git a/lib/Service/Notification/NotificationReadState.php b/lib/Service/Notification/NotificationReadState.php index bb977ef5af..e4165d96ff 100644 --- a/lib/Service/Notification/NotificationReadState.php +++ b/lib/Service/Notification/NotificationReadState.php @@ -31,7 +31,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/notificatie-engine/specs/notificatie-engine/spec.md "Read/unread tracking MUST be maintained per user per notification" + * @spec openspec/specs/notificatie-engine/spec.md "Read/unread tracking MUST be maintained per user per notification" */ declare(strict_types=1); diff --git a/lib/Service/Notification/ScheduledFilterEvaluator.php b/lib/Service/Notification/ScheduledFilterEvaluator.php new file mode 100644 index 0000000000..47c75f88cd --- /dev/null +++ b/lib/Service/Notification/ScheduledFilterEvaluator.php @@ -0,0 +1,250 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Notification; + +use DateInterval; +use DateTimeImmutable; +use Exception; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +/** + * Evaluates a scheduled-trigger `filter` map against object data. + * + * Filter shape is a `field => value-spec` map where `value-spec` is: + * - a scalar (string/int/float/bool/null) — equivalent to `{operator: equals, value: }`; + * - an operator object `{operator: , value: }` where `` is one of: + * - `equals` — strict (`===`) equality; + * - `notEquals` — strict inequality; missing/null field counts as not-equal to any non-null value; + * - `withinNext` — field is a date in the half-open window `(now, now + duration]`; + * - `olderThan` — field is a date strictly before `now - duration`. + * + * All entries are combined with AND. An empty filter map matches. + * + * Fail-closed semantics: unparsable date values in the inspected object + * (for `withinNext` / `olderThan`) cause that entry to NOT match, plus a + * debug log. The rule still evaluates the remaining entries — useful so a + * single bad date does not silently bypass other constraints. + */ +final class ScheduledFilterEvaluator +{ + + /** + * Logger for fail-closed diagnostics. + * + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * Construct an evaluator with an optional logger. + * + * The logger is used to emit debug-level diagnostics when a value cannot + * be parsed; production callers should inject the standard Nextcloud + * logger, tests may pass a NullLogger. + * + * @param LoggerInterface|null $logger Logger for fail-closed diagnostics. + */ + public function __construct(?LoggerInterface $logger=null) + { + $this->logger = ($logger ?? new NullLogger()); + + }//end __construct() + + /** + * Evaluate the filter against the given object data. + * + * @param array $objectData Flat field map (typically `$object->getObject()`). + * @param array $filter Filter map per the class docblock. + * @param DateTimeImmutable $now Logical "now" for the entire scan pass. + * + * @return bool True when every entry matches, false otherwise. + */ + public function matches(array $objectData, array $filter, DateTimeImmutable $now): bool + { + if (count($filter) === 0) { + return true; + } + + foreach ($filter as $field => $spec) { + if ($this->entryMatches(field: (string) $field, spec: $spec, objectData: $objectData, now: $now) === false) { + return false; + } + } + + return true; + + }//end matches() + + /** + * Evaluate a single filter entry. + * + * @param string $field The field name on the object. + * @param mixed $spec Either a scalar (equals shortcut) or an operator object. + * @param array $objectData The full object data map. + * @param DateTimeImmutable $now Logical "now" for the entry. + * + * @return bool True when the entry matches. + */ + private function entryMatches(string $field, $spec, array $objectData, DateTimeImmutable $now): bool + { + $actual = ($objectData[$field] ?? null); + + // Scalar shortcut — strict equality. + if (is_array($spec) === false || array_key_exists('operator', $spec) === false) { + return $actual === $spec; + } + + $operator = (string) $spec['operator']; + $value = ($spec['value'] ?? null); + + switch ($operator) { + case 'equals': + return $actual === $value; + + case 'notEquals': + // Missing/null field satisfies notEquals for any non-null target. + if ($actual === null && $value !== null) { + return true; + } + return $actual !== $value; + + case 'withinNext': + $interval = $this->parseDuration(duration: (string) $value, field: $field, operator: $operator); + if ($interval === null) { + return false; + } + + $fieldDate = $this->parseDate(value: $actual, field: $field); + if ($fieldDate === null) { + return false; + } + + $upper = $now->add($interval); + + // Half-open window: (now, now + duration]. + return ($fieldDate > $now && $fieldDate <= $upper); + + case 'olderThan': + $interval = $this->parseDuration(duration: (string) $value, field: $field, operator: $operator); + if ($interval === null) { + return false; + } + + $fieldDate = $this->parseDate(value: $actual, field: $field); + if ($fieldDate === null) { + return false; + } + + $threshold = $now->sub($interval); + + return $fieldDate < $threshold; + + default: + $this->logger->debug( + 'ScheduledFilterEvaluator: unknown operator (fail-closed)', + ['field' => $field, 'operator' => $operator] + ); + return false; + }//end switch + + }//end entryMatches() + + /** + * Parse an ISO-8601 DateInterval string. Logs + returns null on failure. + * + * @param string $duration ISO-8601 duration ("PT24H", "P30D", ...). + * @param string $field Field name for diagnostics only. + * @param string $operator Operator label for diagnostics only. + * + * @return DateInterval|null + */ + private function parseDuration(string $duration, string $field, string $operator): ?DateInterval + { + if ($duration === '') { + $this->logger->debug( + 'ScheduledFilterEvaluator: empty duration (fail-closed)', + ['field' => $field, 'operator' => $operator] + ); + return null; + } + + try { + return new DateInterval($duration); + } catch (Exception $e) { + $this->logger->debug( + 'ScheduledFilterEvaluator: unparsable duration (fail-closed)', + [ + 'field' => $field, + 'operator' => $operator, + 'value' => $duration, + 'error' => $e->getMessage(), + ] + ); + return null; + } + + }//end parseDuration() + + /** + * Parse an object-data date value. Accepts strings only; non-string + * (null, bool, array, etc.) → null + debug log. Empty string → null. + * + * @param mixed $value Raw value from object data. + * @param string $field Field name for diagnostics only. + * + * @return DateTimeImmutable|null + */ + private function parseDate($value, string $field): ?DateTimeImmutable + { + if (is_string($value) === false || $value === '') { + $this->logger->debug( + 'ScheduledFilterEvaluator: missing or non-string date (fail-closed)', + ['field' => $field] + ); + return null; + } + + try { + return new DateTimeImmutable($value); + } catch (Exception $e) { + $this->logger->debug( + 'ScheduledFilterEvaluator: unparsable object date (fail-closed)', + [ + 'field' => $field, + 'error' => $e->getMessage(), + ] + ); + return null; + } + + }//end parseDate() +}//end class diff --git a/lib/Service/Notification/SystemEntityObjectAdapter.php b/lib/Service/Notification/SystemEntityObjectAdapter.php new file mode 100644 index 0000000000..61ca1bc3d4 --- /dev/null +++ b/lib/Service/Notification/SystemEntityObjectAdapter.php @@ -0,0 +1,111 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Notification; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCP\AppFramework\Db\Entity; + +/** + * Virtual ObjectEntity wrapper for system entities. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Adapter bridges two type hierarchies; + * coupling is structural and intentional. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ +class SystemEntityObjectAdapter extends ObjectEntity +{ + /** + * Build a virtual ObjectEntity from a system entity. + * + * The entity is expected to implement `jsonSerialize()` and expose `getUuid()`. + * Optionally `getTitle()` or `getName()` are used to populate the display name. + * + * @param Entity $entity The system entity (Source, Agent, Configuration, etc.). + * @param string $systemSlug The canonical system schema slug (SystemSchemaRules::SLUG_*). + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-3 + */ + public function __construct(Entity $entity, string $systemSlug) + { + parent::__construct(); + + // Virtual schema reference — the bridge listener resolves this slug to + // a synthetic Schema via SystemSchemaRules::buildSchema(). + $this->schema = $systemSlug; + $this->register = null; + + // UUID — used for the notification object reference and idempotency keys. + // Entity magic getters are dispatched via __call, so method_exists() returns + // false; call them directly and catch any BadMethodCallException. + try { + // @phpstan-ignore-next-line + $uuidVal = $entity->getUuid(); + if ($uuidVal !== null) { + $this->uuid = (string) $uuidVal; + } + } catch (\Throwable) { + // GetUuid() not available — uuid stays null. + } + + // Display name for subject interpolation (title wins over name). + $displayName = null; + try { + // @phpstan-ignore-next-line + $displayName = $entity->getTitle(); + } catch (\Throwable) { + // GetTitle() not available — fall through. + } + + if ($displayName === null) { + try { + // @phpstan-ignore-next-line + $displayName = $entity->getName(); + } catch (\Throwable) { + // GetName() not available — displayName stays null. + } + } + + if (is_string($displayName) === true && $displayName !== '') { + $this->name = $displayName; + } + + // Payload array for {{field}} template interpolation in subjects. + if ($entity instanceof \JsonSerializable) { + $serialized = $entity->jsonSerialize(); + if (is_array($serialized) === true) { + $this->object = $serialized; + } + } + }//end __construct() +}//end class diff --git a/lib/Service/Notification/SystemSchemaRules.php b/lib/Service/Notification/SystemSchemaRules.php new file mode 100644 index 0000000000..afa2c49a28 --- /dev/null +++ b/lib/Service/Notification/SystemSchemaRules.php @@ -0,0 +1,215 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Notification; + +use OCA\OpenRegister\Db\Schema; + +/** + * System-schema rule registry — option (b) from the design doc. + * + * Holds x-openregister-notifications rules for each system entity type and + * produces a synthetic Schema entity carrying those rules so the existing + * AnnotationNotificationDispatcher path works without modification. + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-2 + */ +class SystemSchemaRules +{ + + /** + * Canonical slugs for each system schema. + */ + public const SLUG_REGISTER = 'openregister_register'; + public const SLUG_SCHEMA = 'openregister_schema'; + public const SLUG_CONFIGURATION = 'openregister_configuration'; + public const SLUG_SOURCE = 'openregister_source'; + public const SLUG_AGENT = 'openregister_agent'; + public const SLUG_WEBHOOK = 'openregister_webhook'; + + /** + * All declared system-schema rules, keyed by canonical slug. + * + * Rules follow the same x-openregister-notifications schema as stored-object + * schemas so the dispatcher evaluates them identically. + * + * @var array> + */ + private const RULES = [ + self::SLUG_REGISTER => [ + 'register-changed' => [ + 'trigger' => ['type' => 'updated'], + 'recipients' => [['kind' => 'groups', 'groups' => ['admin']]], + 'channels' => ['nc-notification'], + 'subject' => [ + 'nl' => 'Register "{{title}}" is bijgewerkt', + 'en' => 'Register "{{title}}" was updated', + ], + ], + ], + self::SLUG_SCHEMA => [ + 'schema-changed' => [ + 'trigger' => ['type' => 'updated'], + 'recipients' => [['kind' => 'groups', 'groups' => ['admin']]], + 'channels' => ['nc-notification'], + 'subject' => [ + 'nl' => 'Schema "{{title}}" is bijgewerkt', + 'en' => 'Schema "{{title}}" was updated', + ], + ], + ], + self::SLUG_CONFIGURATION => [ + 'configuration-changed' => [ + 'trigger' => ['type' => 'updated'], + 'recipients' => [['kind' => 'groups', 'groups' => ['admin']]], + 'channels' => ['nc-notification'], + 'subject' => [ + 'nl' => 'Configuratie "{{title}}" is bijgewerkt', + 'en' => 'Configuration "{{title}}" was updated', + ], + ], + ], + self::SLUG_SOURCE => [ + 'source-unhealthy' => [ + 'trigger' => [ + 'type' => 'updated', + 'condition' => [ + 'field' => 'status', + 'operator' => 'equals', + 'value' => 'error', + ], + ], + 'recipients' => [['kind' => 'groups', 'groups' => ['admin']]], + 'channels' => ['nc-notification'], + 'subject' => [ + 'nl' => 'Bron "{{title}}" is niet beschikbaar', + 'en' => 'Source "{{title}}" is unhealthy', + ], + ], + 'source-changed' => [ + 'trigger' => ['type' => 'updated'], + 'recipients' => [['kind' => 'groups', 'groups' => ['admin']]], + 'channels' => ['nc-notification'], + 'subject' => [ + 'nl' => 'Bron "{{title}}" is bijgewerkt', + 'en' => 'Source "{{title}}" was updated', + ], + ], + ], + self::SLUG_AGENT => [ + 'agent-unhealthy' => [ + 'trigger' => [ + 'type' => 'updated', + 'condition' => [ + 'field' => 'status', + 'operator' => 'equals', + 'value' => 'error', + ], + ], + 'recipients' => [['kind' => 'groups', 'groups' => ['admin']]], + 'channels' => ['nc-notification'], + 'subject' => [ + 'nl' => 'Agent "{{name}}" is niet beschikbaar', + 'en' => 'Agent "{{name}}" is unhealthy', + ], + ], + 'agent-changed' => [ + 'trigger' => ['type' => 'updated'], + 'recipients' => [['kind' => 'groups', 'groups' => ['admin']]], + 'channels' => ['nc-notification'], + 'subject' => [ + 'nl' => 'Agent "{{name}}" is bijgewerkt', + 'en' => 'Agent "{{name}}" was updated', + ], + ], + ], + self::SLUG_WEBHOOK => [ + 'webhook-changed' => [ + 'trigger' => ['type' => 'updated'], + 'recipients' => [['kind' => 'groups', 'groups' => ['admin']]], + 'channels' => ['nc-notification'], + 'subject' => [ + 'nl' => 'Webhook "{{name}}" is bijgewerkt', + 'en' => 'Webhook "{{name}}" was updated', + ], + ], + ], + ]; + + /** + * Return the declared rules for a given system schema slug, or null when + * the slug is not a known system schema. + * + * @param string $slug One of the SLUG_* constants. + * + * @return array|null + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-2 + */ + public function getRules(string $slug): ?array + { + return self::RULES[$slug] ?? null; + }//end getRules() + + /** + * Return all known system schema slugs. + * + * @return array + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-1 + */ + public function getSlugs(): array + { + return array_keys(self::RULES); + }//end getSlugs() + + /** + * Build a synthetic Schema entity carrying the declared rules for the + * given system slug. The dispatcher reads the rules from + * $schema->getConfiguration()['x-openregister-notifications'], so we + * embed them there — no DB row needed. + * + * Returns null when $slug is not a known system schema. + * + * @param string $slug The canonical system schema slug. + * + * @return Schema|null + * + * @spec openspec/changes/openregister-system-notifications/tasks.md#task-2 + */ + public function buildSchema(string $slug): ?Schema + { + $rules = $this->getRules(slug: $slug); + if ($rules === null) { + return null; + } + + $schema = new Schema(); + $schema->setSlug($slug); + $schema->setTitle($slug); + $schema->setConfiguration(['x-openregister-notifications' => $rules]); + + return $schema; + }//end buildSchema() +}//end class diff --git a/lib/Service/Notification/VngNotificatiesEnvelope.php b/lib/Service/Notification/VngNotificatiesEnvelope.php index c9f07072a9..507dc58d09 100644 --- a/lib/Service/Notification/VngNotificatiesEnvelope.php +++ b/lib/Service/Notification/VngNotificatiesEnvelope.php @@ -36,7 +36,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/notificatie-engine/specs/notificatie-engine/spec.md "VNG Notificaties API compliance" + * @spec openspec/specs/notificatie-engine/spec.md "VNG Notificaties API compliance" */ declare(strict_types=1); diff --git a/lib/Service/NotificationService.php b/lib/Service/NotificationService.php index 4ebefca9df..a88c680c56 100644 --- a/lib/Service/NotificationService.php +++ b/lib/Service/NotificationService.php @@ -89,7 +89,7 @@ class NotificationService * * @psalm-return int<0, max> * - * @spec openspec/changes/retrofit-2026-04-28-notificatie-engine/tasks.md#task-1 + * @spec openspec/specs/notificatie-engine/spec.md */ public function notifyConfigurationUpdate(Configuration $configuration): int { @@ -180,7 +180,7 @@ public function notifyConfigurationUpdate(Configuration $configuration): int * * @throws \Exception If notification creation or sending fails * - * @spec openspec/changes/retrofit-2026-04-28-notificatie-engine/tasks.md#task-1 + * @spec openspec/specs/notificatie-engine/spec.md */ private function sendUpdateNotification( string $userId, @@ -218,7 +218,7 @@ private function sendUpdateNotification( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-notificatie-engine/tasks.md#task-1 + * @spec openspec/specs/notificatie-engine/spec.md */ public function markConfigurationUpdated(Configuration $configuration): void { diff --git a/lib/Service/Oas/OasETagComputer.php b/lib/Service/Oas/OasETagComputer.php index 60a11883b1..bcbb351ca0 100644 --- a/lib/Service/Oas/OasETagComputer.php +++ b/lib/Service/Oas/OasETagComputer.php @@ -25,7 +25,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/oas-validation/tasks.md "Performance Impact of Validation / ETag caching" + * @spec openspec/specs/oas-validation/spec.md "Performance Impact of Validation / ETag caching" */ declare(strict_types=1); diff --git a/lib/Service/Oas/OasRequestValidator.php b/lib/Service/Oas/OasRequestValidator.php index e4e41ebb4b..3a6c451c71 100644 --- a/lib/Service/Oas/OasRequestValidator.php +++ b/lib/Service/Oas/OasRequestValidator.php @@ -24,7 +24,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/oas-validation/tasks.md "Request/Response Validation Against OAS Schema" + * @spec openspec/specs/oas-validation/spec.md "Request/Response Validation Against OAS Schema" */ declare(strict_types=1); diff --git a/lib/Service/Oas/ProblemDetailsBuilder.php b/lib/Service/Oas/ProblemDetailsBuilder.php index a0557c6cda..c39f344d43 100644 --- a/lib/Service/Oas/ProblemDetailsBuilder.php +++ b/lib/Service/Oas/ProblemDetailsBuilder.php @@ -31,8 +31,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/oas-validation/tasks.md "API-46 Problem Details (RFC 7807)" - * @spec openspec/changes/oas-validation/specs/oas-validation/spec.md "Error responses include problem details (API-46 / RFC 7807)" + * @spec openspec/specs/oas-validation/spec.md "API-46 Problem Details (RFC 7807)" + * @spec openspec/specs/oas-validation/spec.md "Error responses include problem details (API-46 / RFC 7807)" + * @spec openspec/specs/oas-validation/spec.md "API-46 RFC 7807 — retrofit cross-ref" */ declare(strict_types=1); @@ -42,7 +43,8 @@ /** * Builds RFC 7807 problem-details response payloads. * - * @spec openspec/changes/oas-validation/specs/oas-validation/spec.md "Error responses include problem details (API-46 / RFC 7807)" + * @spec openspec/specs/oas-validation/spec.md "Error responses include problem details (API-46 / RFC 7807)" + * @spec openspec/specs/oas-validation/spec.md "API-46 RFC 7807 — retrofit cross-ref" */ class ProblemDetailsBuilder { diff --git a/lib/Service/OasService.php b/lib/Service/OasService.php index 526f94eb4f..73a2700c3d 100644 --- a/lib/Service/OasService.php +++ b/lib/Service/OasService.php @@ -17,15 +17,15 @@ * @version GIT: * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-12 + * @spec openspec/specs/actions/spec.md + * @spec openspec/specs/deprecate-published-metadata/spec.md * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-14 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-17 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-22 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-23 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-24 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/mock-registers/spec.md#requirement-schema-compliance-with-adr-006 + * @spec openspec/specs/oas-generation/spec.md + * @spec openspec/specs/oas-generation/spec.md + * @spec openspec/specs/oas-validation/spec.md#scenario-standard-http-methods-documented-api-01 + * @spec openspec/specs/oas-validation/spec.md#scenario-pagination-structure-follows-api-42 + * @spec openspec/specs/object-lifecycle/spec.md */ declare(strict_types=1); @@ -184,7 +184,7 @@ public function getLastValidationReport(): OasValidationReport * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ public function createOas(?string $registerId=null, bool $strict=false): array { @@ -338,6 +338,7 @@ public function createOas(?string $registerId=null, bool $strict=false): array operationIdPrefix: $operationIdPrefix ); $this->addExtendedPaths(register: $register, schema: $schema); + $this->addJsonLdContextPaths(register: $register, schema: $schema); } } }//end foreach @@ -365,7 +366,7 @@ public function createOas(?string $registerId=null, bool $strict=false): array * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-1 + * @spec openspec/specs/actions/spec.md */ private function logValidationIssues(): void { @@ -391,7 +392,7 @@ private function logValidationIssues(): void * * @throws \Exception When file cannot be read or parsed * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function getBaseOas(): array { @@ -419,7 +420,7 @@ private function getBaseOas(): array * @return array{createGroups: string[], readGroups: string[], updateGroups: string[], deleteGroups: string[]} * Unique groups per CRUD action * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-12 + * @spec openspec/specs/deprecate-published-metadata/spec.md */ private function extractSchemaGroups(object $schema): array { @@ -481,7 +482,7 @@ private function extractSchemaGroups(object $schema): array * * @return string|null The group name, or null if not extractable * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function extractGroupFromRule($rule): ?string { @@ -503,7 +504,7 @@ private function extractGroupFromRule($rule): ?string * * @return string The scope description * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function getScopeDescription(string $group): string { @@ -543,7 +544,7 @@ private function getScopeDescription(string $group): string * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function applyRbacToOperation(array &$operation, array $groups): void { @@ -611,7 +612,7 @@ static function (string $group): string { * * @return array Enriched schema with type, x-tags, and properties. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function enrichSchema(object $schema): array { @@ -861,7 +862,10 @@ private function sanitizePropertyDefinition($propertyDefinition): array if (is_array($cleanDef['items']) === true && array_is_list($cleanDef['items']) === true) { // Sequential array (list) — not valid. Use first element or default. $firstItem = $cleanDef['items'][0] ?? null; - $cleanDef['items'] = (empty($firstItem) === false) ? $firstItem : ['type' => 'string']; + $cleanDef['items'] = ['type' => 'string']; + if (empty($firstItem) === false) { + $cleanDef['items'] = $firstItem; + } } if (is_array($cleanDef['items']) === false || empty($cleanDef['items']) === true) { @@ -897,7 +901,7 @@ private function sanitizePropertyDefinition($propertyDefinition): array * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function addCrudPaths(object $register, object $schema, array $rbac=[], string $operationIdPrefix=''): void { @@ -958,6 +962,77 @@ private function addCrudPaths(object $register, object $schema, array $rbac=[], ]; }//end addCrudPaths() + /** + * Add the JSON-LD `@context` document endpoints for a register/schema + * pair (json-ld-output). These are the dereferenceable URLs that object + * JSON-LD serializations reference in their `@context`. + * + * @param object $register The register object. + * @param object $schema The schema object. + * + * @return void + * + * @spec openspec/specs/json-ld-output/spec.md + */ + private function addJsonLdContextPaths(object $register, object $schema): void + { + $registerSlug = $register->getSlug(); + if ($registerSlug === null || $registerSlug === '') { + $registerSlug = $this->slugify(string: $register->getTitle()); + } + + $schemaSlug = $schema->getSlug(); + if ($schemaSlug === null || $schemaSlug === '') { + $schemaSlug = $this->slugify(string: $schema->getTitle()); + } + + $contextResponse = [ + '200' => [ + 'description' => 'JSON-LD context document of the form `{"@context": {…}}`.', + 'content' => [ + 'application/ld+json' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + '@context' => ['type' => 'object'], + ], + ], + ], + ], + ], + '404' => [ + 'description' => 'Register or schema not found', + 'content' => [ + 'application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/Error'], + ], + ], + ], + ]; + + // Register-wide context document (idempotent across schemas of a register). + $this->oas['paths']['/contexts/'.$registerSlug] = [ + 'get' => [ + 'summary' => 'JSON-LD context for register '.$registerSlug, + 'operationId' => 'getContext'.$this->pascalCase(string: (string) $registerSlug), + 'tags' => ['JSON-LD'], + 'description' => 'Dereferenceable register-wide JSON-LD @context document.', + 'responses' => $contextResponse, + ], + ]; + + // Per-schema context document. + $this->oas['paths']['/contexts/'.$registerSlug.'/'.$schemaSlug] = [ + 'get' => [ + 'summary' => 'JSON-LD context for '.$schemaSlug, + 'operationId' => 'getContext'.$this->pascalCase(string: (string) $registerSlug).$this->pascalCase(string: (string) $schemaSlug), + 'tags' => ['JSON-LD'], + 'description' => 'Dereferenceable per-schema JSON-LD @context document.', + 'responses' => $contextResponse, + ], + ]; + }//end addJsonLdContextPaths() + /** * Add extended paths for a schema using whitelist approach * @@ -969,7 +1044,7 @@ private function addCrudPaths(object $register, object $schema, array $rbac=[], * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function addExtendedPaths(object $register, object $schema): void { @@ -1034,7 +1109,7 @@ private function addExtendedPaths(object $register, object $schema): void * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flag controls collection vs single item parameters * @SuppressWarnings(PHPMD.CyclomaticComplexity) Dynamic parameter generation from schema properties * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function createCommonQueryParameters(bool $isCollection=false, ?object $schema=null): array { @@ -1133,7 +1208,7 @@ private function createCommonQueryParameters(bool $isCollection=false, ?object $ * * @return string The OpenAPI type for the property * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-17 + * @spec openspec/specs/mock-registers/spec.md#requirement-schema-compliance-with-adr-006 */ private function getPropertyType($propertyDefinition): string { @@ -1176,7 +1251,7 @@ private function getPropertyType($propertyDefinition): string * * @return array OpenAPI operation definition for GET collection. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/oas-validation/spec.md#scenario-standard-http-methods-documented-api-01 */ private function createGetCollectionOperation(object $schema): array { @@ -1201,9 +1276,12 @@ private function createGetCollectionOperation(object $schema): array 'parameters' => $this->createCommonQueryParameters(isCollection: true, schema: $schema), 'responses' => [ '200' => [ - 'description' => 'List of '.$schemaTitle.' objects with pagination metadata', + 'description' => 'List of '.$schemaTitle.' objects with pagination metadata. ' + .'Send `Accept: application/ld+json` to receive a JSON-LD `@graph` ' + .'document (pagination under `or:` terms); the default ' + .'`application/json` representation is unchanged (json-ld-output).', 'content' => [ - 'application/json' => [ + 'application/json' => [ 'schema' => [ 'allOf' => [ [ @@ -1223,6 +1301,17 @@ private function createGetCollectionOperation(object $schema): array ], ], ], + 'application/ld+json' => [ + 'schema' => [ + 'type' => 'object', + 'description' => 'JSON-LD @graph serialization of the collection.', + 'properties' => [ + '@context' => ['type' => 'string', 'format' => 'uri'], + '@graph' => ['type' => 'array', 'items' => ['type' => 'object']], + 'or:total' => ['type' => 'integer'], + ], + ], + ], ], ], '400' => [ @@ -1244,7 +1333,7 @@ private function createGetCollectionOperation(object $schema): array * * @return array OpenAPI operation definition for GET single item. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function createGetOperation(object $schema): array { @@ -1276,13 +1365,27 @@ private function createGetOperation(object $schema): array ), 'responses' => [ '200' => [ - 'description' => $schema->getTitle().' found.', + 'description' => $schema->getTitle().' found. Send ' + .'`Accept: application/ld+json` to receive the JSON-LD ' + .'representation (`@context`/`@id`/`@type`); the default ' + .'`application/json` representation is unchanged (json-ld-output).', 'content' => [ - 'application/json' => [ + 'application/json' => [ 'schema' => [ '$ref' => '#/components/schemas/'.$this->sanitizeSchemaName(title: $schemaName), ], ], + 'application/ld+json' => [ + 'schema' => [ + 'type' => 'object', + 'description' => 'JSON-LD serialization of the object.', + 'properties' => [ + '@context' => ['type' => 'string', 'format' => 'uri'], + '@id' => ['type' => 'string', 'format' => 'uri'], + '@type' => ['type' => 'string'], + ], + ], + ], ], ], '404' => [ @@ -1306,7 +1409,7 @@ private function createGetOperation(object $schema): array * * @return array OpenAPI operation definition for PUT. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/oas-validation/spec.md#scenario-standard-http-methods-documented-api-01 */ private function createPutOperation(object $schema): array { @@ -1378,7 +1481,7 @@ private function createPutOperation(object $schema): array * * @return array OpenAPI operation definition for POST. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/oas-validation/spec.md#scenario-standard-http-methods-documented-api-01 */ private function createPostOperation(object $schema): array { @@ -1434,7 +1537,7 @@ private function createPostOperation(object $schema): array * * @return array OpenAPI operation definition for DELETE. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function createDeleteOperation(object $schema): array { @@ -1480,7 +1583,7 @@ private function createDeleteOperation(object $schema): array * * @return array OpenAPI operation definition for logs endpoint. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function createLogsOperation(object $schema): array { @@ -1536,7 +1639,7 @@ private function createLogsOperation(object $schema): array * * @return array OpenAPI operation definition for get files endpoint. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function createGetFilesOperation(object $schema): array { @@ -1592,7 +1695,7 @@ private function createGetFilesOperation(object $schema): array * * @return array OpenAPI operation definition for post file endpoint. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function createPostFileOperation(object $schema): array { @@ -1662,7 +1765,7 @@ private function createPostFileOperation(object $schema): array * * @return array OpenAPI operation definition for lock endpoint. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function createLockOperation(object $schema): array { @@ -1718,7 +1821,7 @@ private function createLockOperation(object $schema): array * * @return array OpenAPI operation definition for unlock endpoint. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function createUnlockOperation(object $schema): array { @@ -1767,7 +1870,7 @@ private function createUnlockOperation(object $schema): array * * @return string The slugified string * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function slugify(string $string): string { @@ -1798,7 +1901,7 @@ private function pascalCase(string $string): string * * @return string The sanitized schema name * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-23 + * @spec openspec/specs/oas-validation/spec.md#scenario-standard-http-methods-documented-api-01 */ private function sanitizeSchemaName(?string $title): string { @@ -1840,7 +1943,7 @@ private function sanitizeSchemaName(?string $title): string * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple nested loops and conditional checks for validating * paths, responses, and schemas * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-22 + * @spec openspec/specs/oas-generation/spec.md */ private function validateOasIntegrity(): void { @@ -1951,7 +2054,7 @@ private function validateAgainstMetaSchema(): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function validateServerUrls(): void { @@ -1978,7 +2081,7 @@ private function validateServerUrls(): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function validateOperationIdUniqueness(): void { @@ -2043,7 +2146,7 @@ private function validateOperationIdUniqueness(): void * * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ private function validateTagConsistency(): void { @@ -2171,7 +2274,7 @@ private function validateNlGovRules(): void * @SuppressWarnings(PHPMD.CyclomaticComplexity) Recursive schema validation with multiple reference types * @SuppressWarnings(PHPMD.NPathComplexity) Multiple conditional paths for allOf, $ref, and nested validation * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-24 + * @spec openspec/specs/oas-validation/spec.md#scenario-pagination-structure-follows-api-42 */ private function validateSchemaReferences(array &$schema, string $context): void { @@ -2303,7 +2406,7 @@ private function validateSchemaReferences(array &$schema, string $context): void * * @return array|null The effective authorization array. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function resolveEffectiveAuthorization(object $schema): ?array { @@ -2343,7 +2446,7 @@ private function resolveEffectiveAuthorization(object $schema): ?array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-21 + * @spec openspec/specs/oas-generation/spec.md */ private function expandRolesForOas(array $authorization, object $schema, ?object $register=null): array { diff --git a/lib/Service/Object/AuditHandler.php b/lib/Service/Object/AuditHandler.php index 72a8f2a147..996df7596e 100644 --- a/lib/Service/Object/AuditHandler.php +++ b/lib/Service/Object/AuditHandler.php @@ -20,8 +20,8 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining + * @spec openspec/specs/audit-trail-immutable/spec.md */ declare(strict_types=1); @@ -52,7 +52,7 @@ class AuditHandler * @param AuditTrailMapper $auditTrailMapper Audit trail mapper * @param LoggerInterface $logger PSR-3 logger * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-8 + * @spec openspec/specs/audit-trail-immutable/spec.md */ public function __construct( private readonly AuditTrailMapper $auditTrailMapper, @@ -74,8 +74,8 @@ public function __construct( * * @psalm-return array<\OCA\OpenRegister\Db\AuditTrail> * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-8 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-16 + * @spec openspec/specs/audit-trail-immutable/spec.md + * @spec openspec/specs/audit-trail-immutable/spec.md */ public function getLogs(string $uuid, array $filters=[]): array { @@ -121,62 +121,6 @@ public function getLogs(string $uuid, array $filters=[]): array }//end try }//end getLogs() - /** - * Validate object ownership - * - * Checks if object belongs to specified register and schema. - * - * @param object|array $object Object to validate - * @param string $requestedRegister Requested register ID or slug - * @param string $requestedSchema Requested schema ID or slug - * - * @return bool True if object belongs to register/schema - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-8 - */ - public function validateObjectOwnership(object|array $object, string $requestedRegister, string $requestedSchema): bool - { - try { - // Get object's register and schema. - $objectRegister = $object->getRegister(); - $objectSchema = $object->getSchema(); - if (is_array($object) === true) { - $objectRegister = $object['register'] ?? null; - $objectSchema = $object['schema'] ?? null; - } - - // Normalize and compare register. - $objectRegisterNorm = strtolower((string) $objectRegister); - $reqRegisterNorm = strtolower($requestedRegister); - $registerMatch = ($objectRegisterNorm === $reqRegisterNorm); - - // Normalize schema (handle array/object/string). - $objectSchemaId = $this->extractSchemaId(schema: $objectSchema); - $objectSchemaSlug = $this->extractSchemaSlug(schema: $objectSchema); - - $requestedSchemaNorm = strtolower($requestedSchema); - $objectSchemaIdNorm = strtolower((string) $objectSchemaId); - $objectSchemaSlugNorm = null; - if ($objectSchemaSlug !== null) { - $objectSchemaSlugNorm = strtolower($objectSchemaSlug); - } - - // Check schema match (by ID or slug). - $schemaMatch = ( - $requestedSchemaNorm === $objectSchemaIdNorm || - ($objectSchemaSlugNorm && $requestedSchemaNorm === $objectSchemaSlugNorm) - ); - - return $registerMatch && $schemaMatch; - } catch (\Exception $e) { - $this->logger->warning( - message: '[AuditHandler] Failed to validate object ownership', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - return false; - }//end try - }//end validateObjectOwnership() - /** * Prepare filters for audit trail query * @@ -185,7 +129,7 @@ public function validateObjectOwnership(object|array $object, string $requestedR * * @return array Prepared filters for audit trail query. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-8 + * @spec openspec/specs/audit-trail-immutable/spec.md#requirement-the-audit-trail-must-use-cryptographic-hash-chaining */ private function prepareFilters(string $uuid, array $filters): array { @@ -223,7 +167,7 @@ private function prepareFilters(string $uuid, array $filters): array * * @return string Schema ID * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-8 + * @spec openspec/specs/audit-trail-immutable/spec.md */ private function extractSchemaId(mixed $schema): string { @@ -245,7 +189,7 @@ private function extractSchemaId(mixed $schema): string * * @return null|string Schema slug * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-8 + * @spec openspec/specs/audit-trail-immutable/spec.md */ private function extractSchemaSlug(mixed $schema): string|null { diff --git a/lib/Service/Object/BatchOperationStatus.php b/lib/Service/Object/BatchOperationStatus.php index 80fa27beef..97f319218f 100644 --- a/lib/Service/Object/BatchOperationStatus.php +++ b/lib/Service/Object/BatchOperationStatus.php @@ -31,13 +31,13 @@ * @category Service * @package OCA\OpenRegister\Service\Object * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app * - * @spec openspec/changes/reference-existence-validation/tasks.md + * @spec openspec/specs/reference-existence-validation/spec.md */ declare(strict_types=1); diff --git a/lib/Service/Object/CacheHandler.php b/lib/Service/Object/CacheHandler.php index bd3340afc0..9bbaffa4ff 100644 --- a/lib/Service/Object/CacheHandler.php +++ b/lib/Service/Object/CacheHandler.php @@ -33,7 +33,6 @@ use OCA\OpenRegister\Db\OrganisationMapper; use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\SchemaMapper; -use OCA\OpenRegister\Service\IndexService; use OCP\AppFramework\IAppContainer; use OCP\DB\IResult; use OCP\ICacheFactory; @@ -171,7 +170,7 @@ class CacheHandler * @param SchemaMapper|null $schemaMapper Schema mapper for magic table queries * @param IDBConnection|null $db Database connection for magic table queries * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly OrganisationMapper $organisationMapper, @@ -206,7 +205,7 @@ public function __construct( * * @return null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getUser() { @@ -223,7 +222,7 @@ public function getUser() * * @throws RuntimeException When container is not available. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function getObjectMapper(): MagicMapper { @@ -238,39 +237,6 @@ private function getObjectMapper(): MagicMapper return $this->objectMapper; }//end getObjectMapper() - /** - * Get IndexService instance using lazy loading from container - * - * Lazy loads IndexService from container to break circular dependency. - * Returns null if index service is unavailable or disabled. - * - * @return IndexService|null Index service instance or null - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - private function getIndexService(): ?IndexService - { - // Lazy-load IndexService from container to break circular dependency. - if ($this->container === null) { - return null; - } - - try { - return $this->container->get(\OCA\OpenRegister\Service\IndexService::class); - } catch (\Exception $e) { - // If IndexService is not available, return null (graceful degradation). - $this->logger->debug( - message: '[CacheHandler] IndexService not available', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - return null; - } - }//end getIndexService() - /** * Get an object from cache or database * @@ -284,11 +250,16 @@ private function getIndexService(): ?IndexService * @phpstan-return ObjectEntity|null * @psalm-return ObjectEntity|null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getObject(int | string $identifier): ?ObjectEntity { - $key = (string) $identifier; + // BUG-OBJ-6: the in-memory object cache is keyed per active-organisation so a + // cached entity loaded under one tenant's context can never be served to a + // different tenant within the same request lifecycle. The underlying find() + // already applies RBAC + multitenancy on a cache miss; the tenant-scoped key + // closes the cache-hit bypass. + $key = $this->buildObjectCacheKey(rawKey: (string) $identifier); // Check cache first. if (($this->objectCache[$key] ?? null) !== null) { @@ -296,7 +267,7 @@ public function getObject(int | string $identifier): ?ObjectEntity return $this->objectCache[$key]; } - // Cache miss - load from database. + // Cache miss - load from database (RBAC + multitenancy enforced by find()). $this->stats['misses']++; try { @@ -311,223 +282,50 @@ public function getObject(int | string $identifier): ?ObjectEntity } }//end getObject() - // ========================================. - // SEARCH INDEX INTEGRATION METHODS. - // ========================================. - /** - * Index object in search index when available + * Build a tenant-scoped key for the in-memory object cache (BUG-OBJ-6). * - * Creates a search document from ObjectEntity matching the ObjectEntity structure. - * Metadata fields (name, description, etc.) are at root level, with flexible - * object data in a nested 'object' field. + * Prefixes the raw id/uuid with the caller's active organisation so a cached + * entity is never shared across tenants. Falls back to a stable sentinel when + * no active organisation can be resolved (anonymous / system context). * - * @param ObjectEntity $object Object to index - * @param bool $commit Whether to commit immediately + * @param string $rawKey The raw cache key (object id or uuid) * - * @return bool True if indexing was successful or index unavailable + * @return string The tenant-scoped cache key * - * @psalm-suppress UnusedReturnValue - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ - private function indexObjectInSolr(ObjectEntity $object, bool $commit=false): bool + private function buildObjectCacheKey(string $rawKey): string { - // Get index service using factory pattern (performance optimized). - $indexService = $this->getIndexService(); - - if ($indexService === null || $indexService->isAvailable() === false) { - return true; - // Graceful degradation - index service unavailable. - } - - // Index the object. - $result = $indexService->indexObject(object: $object, commit: $commit); - - if ($result !== true) { - $this->logger->error( - message: '[CacheHandler] Object indexing failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $object->getId(), - 'uuid' => $object->getUuid(), - 'schema' => $object->getSchema(), - 'register' => $object->getRegister(), - ] - ); - return $result; - } - - $this->logger->debug( - message: '[CacheHandler] 🔍 OBJECT INDEXED', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $object->getId(), - 'uuid' => $object->getUuid(), - 'schema' => $object->getSchema(), - 'register' => $object->getRegister(), - ] - ); - - return $result; - }//end indexObjectInSolr() + return $this->getActiveOrganisationCacheScope().'|'.$rawKey; + }//end buildObjectCacheKey() /** - * Remove object from search index - * - * @param ObjectEntity $object Object to remove from index - * @param bool $commit Whether to commit immediately + * Resolve the active-organisation discriminator for object-cache keys (BUG-OBJ-6). * - * @return bool True if removal was successful or index unavailable + * @return string The active organisation UUID, or a sentinel for no/unknown org * - * @psalm-suppress UnusedReturnValue - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ - private function removeObjectFromSolr(ObjectEntity $object, bool $commit=false): bool + private function getActiveOrganisationCacheScope(): string { - // Get index service using factory pattern (performance optimized). - $indexService = $this->getIndexService(); - if ($indexService === null || $indexService->isAvailable() === false) { - return true; - // Graceful degradation. - } - try { - $result = $indexService->deleteObject(objectId: $object->getUuid(), commit: $commit); - - if ($result === true) { - $this->logger->debug( - message: '[CacheHandler] 🗑️ OBJECT REMOVED FROM INDEX', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $object->getId(), - 'uuid' => $object->getUuid(), - ] - ); - } - - return $result; - } catch (\Exception $e) { - $this->logger->warning( - message: '[CacheHandler] Failed to remove object from search index', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'object_id' => $object->getId(), - 'error' => $e->getMessage(), - ] - ); - return true; - // Don't fail the whole operation for index issues. - }//end try - }//end removeObjectFromSolr() - - /** - * Extract dynamic fields from object data for search indexing - * - * Converts object properties into search dynamic fields with appropriate suffixes. - * - * @param array $objectData Object data to extract fields from - * @param string $prefix Field prefix for nested objects - * - * @return array Dynamic search fields - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - private function extractDynamicFieldsFromObject(array $objectData, string $prefix=''): array - { - $dynamicFields = []; - - foreach ($objectData as $key => $value) { - // Skip meta fields and null values. - if ($key === '@self' || $key === 'id' || $value === null) { - continue; + $user = $this->userSession?->getUser(); + if ($user === null) { + return '__no_org__'; } - $fieldName = $prefix.$key; - - if (is_array($value) === true) { - if (($value[0] ?? null) === null) { - // Nested object - recurse with dot notation. - $nestedFields = $this->extractDynamicFieldsFromObject(objectData: $value, prefix: $fieldName.'_'); - $dynamicFields = array_merge($dynamicFields, $nestedFields); - continue; - } - - // Multi-value array. - $dynamicFields[$fieldName.'_ss'] = $value; - // Also add as text for searching. - $dynamicFields[$fieldName.'_txt'] = implode(' ', array_filter($value, 'is_string')); - continue; + $organisation = $this->organisationMapper->getActiveOrganisationWithFallback($user->getUID()); + if ($organisation === null || $organisation === '') { + return '__no_org__'; } - if (is_string($value) === true) { - $dynamicFields[$fieldName.'_s'] = $value; - $dynamicFields[$fieldName.'_txt'] = $value; - } else if (is_int($value) === true || is_float($value) === true) { - $suffix = '_f'; - if (is_int($value) === true) { - $suffix = '_i'; - } - - $dynamicFields[$fieldName.$suffix] = $value; - } else if (is_bool($value) === true) { - $dynamicFields[$fieldName.'_b'] = $value; - } else if ($this->isDateString(value: $value) === true) { - $dynamicFields[$fieldName.'_dt'] = $this->formatDateForSolr(dateString: $value); - }//end if - }//end foreach - - return $dynamicFields; - }//end extractDynamicFieldsFromObject() - - /** - * Check if a string represents a date - * - * @param mixed $value Value to check - * - * @return bool True if value is a date string - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - private function isDateString($value): bool - { - if (is_string($value) === false) { - return false; + return $organisation; + } catch (\Throwable $e) { + // Never let cache-key resolution abort a read; isolate on a safe sentinel. + return '__no_org__'; } - - return (bool) strtotime($value); - }//end isDateString() - - /** - * Format date string for search index - * - * @param string $dateString Date string to format - * - * @return string|null Formatted date or null - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - private function formatDateForSolr(string $dateString): ?string - { - $timestamp = strtotime($dateString); - if ($timestamp === false) { - return null; - } - - return date('Y-m-d\\TH:i:s\\Z', $timestamp); - }//end formatDateForSolr() + }//end getActiveOrganisationCacheScope() /** * Bulk preload objects to warm the cache @@ -547,7 +345,7 @@ private function formatDateForSolr(string $dateString): ?string * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function preloadObjects(array $identifiers): array { @@ -555,17 +353,17 @@ public function preloadObjects(array $identifiers): array return []; } - // Filter out already cached objects. + // Filter out already cached objects (BUG-OBJ-6: tenant-scoped keys). $identifiersToLoad = array_filter( array_unique($identifiers), - fn($id) => isset($this->objectCache[(string) $id]) === false + fn($id) => isset($this->objectCache[$this->buildObjectCacheKey(rawKey: (string) $id)]) === false ); if (empty($identifiersToLoad) === true) { // All objects already cached. return array_filter( array_map( - fn($id) => $this->objectCache[(string) $id] ?? null, + fn($id) => $this->objectCache[$this->buildObjectCacheKey(rawKey: (string) $id)] ?? null, $identifiers ), fn($obj) => $obj !== null @@ -608,23 +406,33 @@ public function preloadObjects(array $identifiers): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function cacheObject(ObjectEntity $object): void { // Check cache size and evict oldest entries if necessary. + // PERF-9: unset the oldest keys in a tight loop instead of rebuilding the whole + // cache with array_slice() (which reallocates the entire array on every insert + // once the cap is reached). PHP arrays preserve insertion order, so the leading + // keys returned by array_keys() are the oldest. if (count($this->objectCache) >= $this->maxCacheSize) { - // Simple cache eviction - remove first 20% of entries. - $entriesToRemove = (int) ($this->maxCacheSize * 0.2); - $this->objectCache = array_slice($this->objectCache, $entriesToRemove, null, true); + $entriesToRemove = (int) ($this->maxCacheSize * 0.2); + if ($entriesToRemove < 1) { + $entriesToRemove = 1; + } + + $oldestKeys = array_slice(array_keys($this->objectCache), 0, $entriesToRemove); + foreach ($oldestKeys as $oldestKey) { + unset($this->objectCache[$oldestKey]); + } } - // Cache with ID. - $this->objectCache[$object->getId()] = $object; + // Cache with ID (BUG-OBJ-6: tenant-scoped key). + $this->objectCache[$this->buildObjectCacheKey(rawKey: (string) $object->getId())] = $object; // Also cache with UUID if available. if (($object->getUuid() !== null) === true) { - $this->objectCache[$object->getUuid()] = $object; + $this->objectCache[$this->buildObjectCacheKey(rawKey: (string) $object->getUuid())] = $object; } }//end cacheObject() @@ -648,7 +456,7 @@ private function cacheObject(ObjectEntity $object): void * cache_size: int<0, max>, query_cache_size: int<0, max>, * name_cache_size: int<0, max>} * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getStats(): array { @@ -697,7 +505,7 @@ public function getStats(): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function clearSearchCache(?string $pattern=null): void { @@ -754,7 +562,7 @@ function ($key) use ($pattern) { * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function clearSchemaRelatedCaches(?int $schemaId=null, ?int $registerId=null, string $operation='unknown'): void { @@ -839,7 +647,7 @@ private function clearSchemaRelatedCaches(?int $schemaId=null, ?int $registerId= * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function invalidateForObjectChange( ?ObjectEntity $object=null, @@ -866,11 +674,13 @@ public function invalidateForObjectChange( // Clear individual object from cache. $this->clearObjectFromCache(object: $object); - // **INDEX INTEGRATION**: Index or remove from search index based on operation. - if ($operation === 'create' || $operation === 'update') { - // Index the object with immediate commit for instant visibility. - $this->indexObjectInSolr(object: $object, commit: true); + // BUG-OBJ-7: drop any stale cached name for this object up-front, regardless + // of the operation label. The create/update branch below re-writes the fresh + // name; for any other (or unknown) operation the stale name is at least + // invalidated so a rename can never serve a stale value from cache. + $this->clearObjectNameFromCache(object: $object); + if ($operation === 'create' || $operation === 'update') { // Update name cache for the modified object. $name = $object->getName() ?? $object->getUuid(); $this->setObjectName(identifier: $object->getUuid(), name: $name); @@ -878,9 +688,6 @@ public function invalidateForObjectChange( $this->setObjectName(identifier: $object->getId(), name: $name); } } else if ($operation === 'delete') { - // Remove from search index with immediate commit for instant visibility. - $this->removeObjectFromSolr(object: $object, commit: true); - // Remove from in-memory name cache. unset($this->nameCache[$object->getUuid()]); unset($this->nameCache[(string) $object->getId()]); @@ -939,19 +746,26 @@ public function invalidateForObjectChange( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function clearObjectFromCache(ObjectEntity $object): void { - // Remove by ID. Ensure ID is string for array key. - $objectId = $object->getId(); - $objectIdKey = (string) $objectId; - - unset($this->objectCache[$objectIdKey]); - - // Remove by UUID if available. + // BUG-OBJ-6: object-cache keys are tenant-scoped ("|"). The active + // organisation at invalidation time may differ from the one in effect when the + // entry was written, so remove every scope's entry for this id/uuid by matching + // the raw-key suffix rather than a single scoped key. + $rawKeys = [(string) $object->getId()]; if (($object->getUuid() !== null) === true) { - unset($this->objectCache[$object->getUuid()]); + $rawKeys[] = (string) $object->getUuid(); + } + + foreach ($rawKeys as $rawKey) { + $suffix = '|'.$rawKey; + foreach (array_keys($this->objectCache) as $cacheKey) { + if ($cacheKey === $rawKey || str_ends_with((string) $cacheKey, $suffix) === true) { + unset($this->objectCache[$cacheKey]); + } + } } $this->logger->debug( @@ -965,6 +779,50 @@ private function clearObjectFromCache(ObjectEntity $object): void ); }//end clearObjectFromCache() + /** + * Remove an object's cached name entries (in-memory + distributed) — BUG-OBJ-7. + * + * Drops the name cached under both the object's UUID and id so a subsequent + * read recomputes the current name (fixing stale names after a rename). + * + * @param ObjectEntity $object The object whose cached name must be cleared + * + * @return void + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function clearObjectNameFromCache(ObjectEntity $object): void + { + $keys = []; + if ($object->getUuid() !== null) { + $keys[] = (string) $object->getUuid(); + } + + if ($object->getId() !== null) { + $keys[] = (string) $object->getId(); + } + + foreach ($keys as $key) { + unset($this->nameCache[$key]); + + if ($this->nameDistributedCache !== null) { + try { + $this->nameDistributedCache->remove('name_'.$key); + } catch (\Exception $e) { + $this->logger->warning( + message: '[CacheHandler] Failed to clear object name from distributed cache', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'identifier' => $key, + 'error' => $e->getMessage(), + ] + ); + } + } + }//end foreach + }//end clearObjectNameFromCache() + /** * Clear all caches (Administrative Operation) * @@ -973,7 +831,7 @@ private function clearObjectFromCache(ObjectEntity $object): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function clearAllCaches(): void { @@ -1043,7 +901,7 @@ public function clearAllCaches(): void * @deprecated Use clearAllCaches() instead * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function clearCache(): void { @@ -1066,7 +924,7 @@ public function clearCache(): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function setObjectName(string|int $identifier, string $name, int $ttl=86400): void { @@ -1120,7 +978,7 @@ public function setObjectName(string|int $identifier, string $name, int $ttl=864 * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getSingleObjectName(string|int $identifier): ?string { @@ -1235,7 +1093,7 @@ public function getSingleObjectName(string|int $identifier): ?string * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * Bulk name retrieval with multiple cache layers requires extensive handling. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getMultipleObjectNames(array $identifiers): array { @@ -1395,7 +1253,7 @@ function ($key) { * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getAllObjectNames(bool $forceWarmup=false): array { @@ -1445,7 +1303,7 @@ function ($key) { * * @psalm-return int<0, max> * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function warmupNameCache(): int { @@ -1537,7 +1395,7 @@ public function warmupNameCache(): int * * @return int Number of names loaded from magic tables. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function loadNamesFromMagicTables(): int { @@ -1639,7 +1497,7 @@ private function loadNamesFromMagicTables(): int * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Batch loading across multiple table types requires branching * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function batchLoadNamesFromMagicTables(array $uuids): array { @@ -1747,7 +1605,7 @@ function ($id) { * * @return array Map of UUID to name. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function queryTableForNames(string $tableName, array $uuids): array { @@ -1812,7 +1670,7 @@ private function queryTableForNames(string $tableName, array $uuids): array * * @return int Number of entries stored in distributed cache. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function persistNameCacheToDistributed(): int { @@ -1861,7 +1719,7 @@ private function persistNameCacheToDistributed(): int * * @return int The number of names in distributed cache, or 0 if unavailable * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getDistributedNameCacheCount(): int { @@ -1889,7 +1747,7 @@ public function getDistributedNameCacheCount(): int * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function clearNameCache(): void { @@ -1917,144 +1775,4 @@ public function clearNameCache(): void context: ['file' => __FILE__, 'line' => __LINE__] ); }//end clearNameCache() - - // ========================================. - // SEARCH INDEX BULK OPERATIONS. - // ========================================. - - /** - * Get comprehensive search index dashboard statistics - * - * @return array Dashboard statistics from IndexService - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - public function getSolrDashboardStats(): array - { - $indexService = $this->getIndexService(); - if ($indexService === null) { - throw new RuntimeException('Index service is not available'); - } - - return $indexService->getStats(); - }//end getSolrDashboardStats() - - /** - * Commit search index - * - * @return (bool|string)[] Commit operation results - * - * @psalm-return array{success: bool, error?: string, timestamp?: string, message?: 'Commit failed'|'Commit successful'} - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - public function commitSolr(): array - { - $indexService = $this->getIndexService(); - if ($indexService === null) { - return ['success' => false, 'error' => 'Index service is not available']; - } - - try { - $result = $indexService->commit(); - // Determine message based on result. - $message = 'Commit failed'; - if ($result === true) { - $message = 'Commit successful'; - } - - return [ - 'success' => $result, - 'timestamp' => date('c'), - 'message' => $message, - ]; - } catch (\Exception $e) { - return [ - 'success' => false, - 'error' => $e->getMessage(), - 'timestamp' => date('c'), - ]; - }//end try - }//end commitSolr() - - /** - * Optimize search index - * - * @return (bool|string)[] Optimize operation results - * - * @psalm-return array{success: bool, error?: string, timestamp?: string, - * message?: 'Optimization failed'|'Optimization successful'} - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - public function optimizeSolr(): array - { - $indexService = $this->getIndexService(); - if ($indexService === null) { - return ['success' => false, 'error' => 'Index service is not available']; - } - - try { - $result = $indexService->optimize(); - // Determine message based on result. - $message = 'Optimization failed'; - if ($result === true) { - $message = 'Optimization successful'; - } - - return [ - 'success' => $result, - 'timestamp' => date('c'), - 'message' => $message, - ]; - } catch (\Exception $e) { - return [ - 'success' => false, - 'error' => $e->getMessage(), - 'timestamp' => date('c'), - ]; - }//end try - }//end optimizeSolr() - - /** - * Clear search index completely for dashboard - * - * @return (false|mixed|null|string)[] Clear operation results - * - * @psalm-return array{success: false|mixed, error: mixed|null|string, - * timestamp?: string, error_details?: mixed|null, - * message?: 'Index clear failed'|'Index cleared successfully'} - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - public function clearSolrIndexForDashboard(): array - { - $indexService = $this->getIndexService(); - if ($indexService === null) { - return ['success' => false, 'error' => 'Index service is not available']; - } - - try { - $result = $indexService->clearIndex(); - // Determine message based on result. - $message = 'Index clear failed'; - if (($result['success'] === true) === true) { - $message = 'Index cleared successfully'; - } - - return [ - 'success' => $result['success'], - 'error' => $result['error'] ?? null, - 'error_details' => $result['error_details'] ?? null, - 'timestamp' => date('c'), - 'message' => $message, - ]; - } catch (\Exception $e) { - return [ - 'success' => false, - 'error' => $e->getMessage(), - 'timestamp' => date('c'), - ]; - }//end try - }//end clearSolrIndexForDashboard() }//end class diff --git a/lib/Service/Object/CascadingHandler.php b/lib/Service/Object/CascadingHandler.php index 35f6ae0e58..972ad6bc3e 100644 --- a/lib/Service/Object/CascadingHandler.php +++ b/lib/Service/Object/CascadingHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -51,7 +51,7 @@ class CascadingHandler * @param UtilityHandler $utilityHandler Handler for utility operations. * @param LoggerInterface $logger Logger for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function __construct( private readonly SaveObject $saveHandler, @@ -79,7 +79,7 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex cascading logic with multiple relationship types * @SuppressWarnings(PHPMD.NPathComplexity) Multiple paths for handling different relationship configurations * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function handlePreValidationCascading(array $object, Schema $schema, ?string $uuid, ?int $currentRegister): array { @@ -195,7 +195,7 @@ function (array $property) { * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function createRelatedObject( array $objectData, diff --git a/lib/Service/Object/CrudHandler.php b/lib/Service/Object/CrudHandler.php index 5611d31fac..562551f230 100644 --- a/lib/Service/Object/CrudHandler.php +++ b/lib/Service/Object/CrudHandler.php @@ -20,9 +20,9 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-62 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-30 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/specs/deletion-audit-trail/spec.md + * @spec openspec/specs/object-lifecycle/spec.md */ declare(strict_types=1); @@ -64,7 +64,7 @@ class CrudHandler * @param ObjectService $objectService Object service for save/search operations * @param LoggerInterface $logger PSR-3 logger * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly ObjectService $objectService, @@ -91,8 +91,8 @@ public function __construct( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - Boolean flags provide flexible API filtering options * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/specs/object-lifecycle/spec.md */ public function list( array $query=[], @@ -165,8 +165,8 @@ public function list( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - Boolean flags control RBAC and multitenancy behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/specs/object-lifecycle/spec.md */ public function get(string $objectId, bool $_rbac=true, bool $_multitenancy=true) { @@ -215,8 +215,8 @@ public function get(string $objectId, bool $_rbac=true, bool $_multitenancy=true * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - Boolean flags control RBAC and multitenancy behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-62 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/specs/object-lifecycle/spec.md */ public function create(array $data, bool $_rbac=true, bool $_multitenancy=true) { @@ -267,8 +267,8 @@ public function create(array $data, bool $_rbac=true, bool $_multitenancy=true) * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - Boolean flags control RBAC and multitenancy behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-62 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/specs/object-lifecycle/spec.md */ public function update( string $objectId, @@ -329,7 +329,7 @@ public function update( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - Boolean flags control RBAC and multitenancy behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function patch( string $objectId, @@ -407,8 +407,8 @@ public function patch( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - Boolean flags control RBAC and multitenancy behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/object-lifecycle/spec.md + * @spec openspec/specs/deletion-audit-trail/spec.md */ public function delete(string $objectId, bool $_rbac=true, bool $_multitenancy=true): bool { @@ -461,7 +461,7 @@ public function delete(string $objectId, bool $_rbac=true, bool $_multitenancy=t * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function buildSearchQuery( array $requestParams, diff --git a/lib/Service/Object/DataManipulationHandler.php b/lib/Service/Object/DataManipulationHandler.php index e9c1e561fe..3488594ff4 100644 --- a/lib/Service/Object/DataManipulationHandler.php +++ b/lib/Service/Object/DataManipulationHandler.php @@ -58,7 +58,7 @@ class DataManipulationHandler * * @return mixed The value at the path, or null if path doesn't exist * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getValueFromPath(array $data, string $path): mixed { @@ -86,7 +86,7 @@ public function getValueFromPath(array $data, string $path): mixed * * @return null|string The generated slug or null if generation failed * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function generateSlugFromValue(string $value): string|null { @@ -118,7 +118,7 @@ public function generateSlugFromValue(string $value): string|null * * @return string The generated slug * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createSlugHelper(string $text): string { @@ -158,7 +158,7 @@ public function createSlugHelper(string $text): string * * @return array The mapped data * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function mapObjectProperties(array $sourceData, array $mapping): array { diff --git a/lib/Service/Object/DeleteObject.php b/lib/Service/Object/DeleteObject.php index a8f695eeb8..4234158322 100644 --- a/lib/Service/Object/DeleteObject.php +++ b/lib/Service/Object/DeleteObject.php @@ -26,8 +26,8 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-30 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-29 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ namespace OCA\OpenRegister\Service\Object; @@ -46,6 +46,7 @@ use OCA\OpenRegister\Service\Schemas\SchemaCacheHandler; use OCA\OpenRegister\Service\Schemas\FacetCacheHandler; use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Service\FileService; use OCA\OpenRegister\Service\SettingsService; use OCP\IDBConnection; use OCP\IUserSession; @@ -67,6 +68,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Delete operations require coordination with multiple services * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complexity at threshold (50) due to integrity + cascade + audit logic + * @SuppressWarnings(PHPMD.ExcessiveClassLength) Integrity + legacy cascade + batched cascade paths live together * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -119,16 +121,21 @@ class DeleteObject /** * Constructor for DeleteObject handler. * - * @param MagicMapper $objectEntityMapper Object entity data mapper. - * @param CacheHandler $cacheHandler Object cache service for entity and query caching - * @param IUserSession $userSession User session service for tracking who deletes - * @param AuditTrailMapper $auditTrailMapper Audit trail mapper for logs - * @param SettingsService $settingsService Settings service for accessing trail settings - * @param LoggerInterface $logger Logger for error handling - * @param ReferentialIntegrityService $integrityService Referential integrity service - * @param IDBConnection $db Database connection for transactions + * @param MagicMapper $objectEntityMapper Object entity data mapper. + * @param CacheHandler $cacheHandler Object entity and query cache + * @param IUserSession $userSession User session service + * @param AuditTrailMapper $auditTrailMapper Audit trail mapper for logs + * @param SettingsService $settingsService Settings service for trail settings + * @param LoggerInterface $logger Logger for error handling + * @param ReferentialIntegrityService $integrityService Referential integrity service + * @param IDBConnection $db Database connection for transactions + * @param FileService|null $fileService File service for object folder cleanup + * @param \OCA\OpenRegister\Service\ObjectSource\ObjectSourceRegistry|null $objectSourceRegistry Writable object-source provider registry + * @param \OCA\OpenRegister\Db\RegisterMapper|null $registerMapper Register mapper for register lookups * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) DI constructor wires all delete-path collaborators */ public function __construct( private readonly MagicMapper $objectEntityMapper, @@ -138,7 +145,10 @@ public function __construct( SettingsService $settingsService, LoggerInterface $logger, ReferentialIntegrityService $integrityService, - IDBConnection $db + IDBConnection $db, + private readonly ?FileService $fileService=null, + private readonly ?\OCA\OpenRegister\Service\ObjectSource\ObjectSourceRegistry $objectSourceRegistry=null, + private readonly ?\OCA\OpenRegister\Db\RegisterMapper $registerMapper=null, ) { $this->auditTrailMapper = $auditTrailMapper; $this->settingsService = $settingsService; @@ -157,6 +167,10 @@ public function __construct( * 'triggerObject', 'triggerSchema', 'action_type'. * @param bool $permanent When true, physically removes the record from the database * instead of soft-deleting. Used by archival destruction workflow. + * @param Register|null $register Optional pre-resolved register context. When supplied together + * with `$schema` and an ObjectEntity `$object`, the redundant + * cross-table re-find of the object's context is skipped. + * @param Schema|null $schema Optional pre-resolved schema context (see `$register`). * * @return bool Whether the deletion was successful. * @@ -169,30 +183,66 @@ public function __construct( * * @psalm-suppress UndefinedInterfaceMethod Array access on JsonSerializable handled by type check * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-30 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ - public function delete(array | JsonSerializable $object, ?array $cascadeContext=null, bool $permanent=false): bool - { + public function delete( + array | JsonSerializable $object, + ?array $cascadeContext=null, + bool $permanent=false, + ?Register $register=null, + ?Schema $schema=null + ): bool { // Handle ObjectEntity passed from deleteObject() - skip redundant lookup. // Handle array input - find object with context (searches across all magic tables). + // NOTE: the ObjectEntity branch MUST be evaluated first. Reading $object['id'] + // on an ObjectEntity raises a fatal \Error ("Cannot use object of type ... as + // array") because ObjectEntity does not implement ArrayAccess; that \Error is + // not an \Exception and previously escaped every catch up the stack, surfacing + // as an HTML 500 to API clients (DELETE on magic-table objects). // @psalm-suppress UndefinedInterfaceMethod. - $identifier = ($object instanceof ObjectEntity) ? $object->getUuid() : $object['id']; - - $includeDeleted = ($object instanceof ObjectEntity); - $context = $this->objectEntityMapper->findAcrossAllSources( - identifier: $identifier, - includeDeleted: $includeDeleted, - _rbac: false, - _multitenancy: false - ); - $objectEntity = $context['object']; - if ($object instanceof ObjectEntity === true) { + $identifier = null; + if ($object instanceof ObjectEntity) { + $identifier = $object->getUuid(); + } + + if (is_array($object) === true) { + // BUG-OBJ-11: do not assume an 'id' key exists. Fall back to the + // canonical identifier shapes and fail with a clear exception rather + // than emitting an undefined-array-key warning and a null lookup. + $identifier = ($object['id'] ?? $object['@self']['id'] ?? $object['uuid'] ?? null); + } + + if ($identifier === null) { + throw new Exception('Cannot delete object: no identifier (id/@self.id/uuid) found in the supplied data.'); + } + + // PERF: callers that already resolved the object AND its register/schema + // context (deleteObject() always has both after resolveDeletionContext()) + // pass them in, skipping a full cross-magic-table re-scan per delete. + $objectEntity = null; + $registerEntity = $register; + $schemaEntity = $schema; + if ($object instanceof ObjectEntity === true && $register !== null && $schema !== null) { $objectEntity = $object; } - $registerEntity = $context['register']; - $schemaEntity = $context['schema']; + if ($objectEntity === null) { + $includeDeleted = ($object instanceof ObjectEntity); + $context = $this->objectEntityMapper->findAcrossAllSources( + identifier: $identifier, + includeDeleted: $includeDeleted, + _rbac: false, + _multitenancy: false + ); + $objectEntity = $context['object']; + if ($object instanceof ObjectEntity === true) { + $objectEntity = $object; + } + + $registerEntity = $context['register']; + $schemaEntity = $context['schema']; + } // **PERMANENT DELETE**: Physical removal from database (archival destruction workflow). if ($permanent === true) { @@ -205,6 +255,14 @@ public function delete(array | JsonSerializable $object, ?array $cascadeContext= ] ); + // BUG-OBJ-2: permanent deletion must also destroy the Nextcloud folder/files + // bound to the object. Without this every permanently-deleted object leaks its + // NC folder forever (storage bloat) and — for the archival `vernietigd` + // destruction workflow — the file contents survive on disk, which is a + // compliance issue. Folder cleanup runs BEFORE the DB record is removed so the + // object still resolves its folder binding. + $this->deleteObjectFolder(objectEntity: $objectEntity); + $this->objectEntityMapper->deleteObjectEntity( entity: $objectEntity, register: $registerEntity, @@ -267,18 +325,26 @@ public function delete(array | JsonSerializable $object, ?array $cascadeContext= 'organisation' => $activeOrganisation, ]; - $objectEntity->setDeleted($deletionData); + // PERF: snapshot the pre-delete state once and hand it to the mapper as + // the update's old entity — this skips the mapper's internal pre-update + // re-find (the entity was freshly fetched by every caller of this method). + $preDeleteState = clone $objectEntity; - /* - * Update the object in database (soft delete - keeps record with deleted metadata). - * Pass register/schema context for magic mapper routing. - * @psalm-suppress InvalidArgument - ObjectEntity extends Entity - */ + $objectEntity->setDeleted($deletionData); + // BUG-OBJ-2: soft delete intentionally LEAVES the bound Nextcloud folder/files + // in place so the object (and its attachments) can be restored. The folder is + // only destroyed on permanent delete (above). TODO: if product wants soft-deleted + // attachments moved to the trash for recoverability, trash the folder here. + // + // Update the object in database (soft delete - keeps record with deleted metadata). + // Pass register/schema context for magic mapper routing. + // @psalm-suppress InvalidArgument - ObjectEntity extends Entity. $result = $this->objectEntityMapper->update( entity: $objectEntity, register: $registerEntity, - schema: $schemaEntity + schema: $schemaEntity, + oldEntity: $preDeleteState ) !== null; // **CACHE INVALIDATION**: Clear collection and facet caches so soft-deleted objects disappear from regular queries. @@ -325,31 +391,78 @@ public function delete(array | JsonSerializable $object, ?array $cascadeContext= $auditAction = $cascadeContext['action_type'] ?? 'referential_integrity.cascade_delete'; } - $auditTrail = $this->auditTrailMapper->createAuditTrail( + // PERF: cascade context (referential-integrity tagging) is folded into + // the initial INSERT by the mapper. Previously this issued a second + // UPDATE per cascade-tagged row, which also invalidated the hash-chain + // seal computed over the freshly-inserted row. + $this->auditTrailMapper->createAuditTrail( old: $objectEntity, new: null, - action: $auditAction + action: $auditAction, + cascadeContext: $cascadeContext ); - - // If this deletion was triggered by referential integrity, tag the audit entry - // with cascade context metadata so it can be distinguished from user-initiated deletes. - if ($cascadeContext !== null && $auditTrail !== null) { - $changed = $auditTrail->getChanged() ?? []; - $changed['triggeredBy'] = 'referential_integrity'; - $changed['cascadeContext'] = [ - 'triggerObject' => $cascadeContext['triggerObject'] ?? null, - 'triggerSchema' => $cascadeContext['triggerSchema'] ?? null, - 'action_type' => $cascadeContext['action_type'] ?? 'referential_integrity.cascade_delete', - 'property' => $cascadeContext['property'] ?? null, - ]; - $auditTrail->setChanged($changed); - $this->auditTrailMapper->update($auditTrail); - } }//end if return $result; }//end delete() + /** + * Delete the Nextcloud folder bound to an object (BUG-OBJ-2). + * + * Resolves the object's bound folder via FileService and removes it, destroying + * any attached file contents. No-op when FileService is unavailable (e.g. unit + * tests), the object has no bound folder, or the folder can no longer be resolved. + * Failures are logged and swallowed so a missing/legacy folder never blocks the + * deletion of the database record. + * + * @param ObjectEntity $objectEntity The object whose folder must be removed + * + * @return void + * + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + */ + private function deleteObjectFolder(ObjectEntity $objectEntity): void + { + if ($this->fileService === null) { + return; + } + + // Skip objects that never had a folder bound (nothing to clean up). + $folderBinding = $objectEntity->getFolder(); + if ($folderBinding === null || $folderBinding === '') { + return; + } + + try { + $folder = $this->fileService->getObjectFolder(objectEntity: $objectEntity); + if ($folder === null) { + return; + } + + $folder->delete(); + + $this->logger->info( + message: '[DeleteObject] Removed Nextcloud folder for permanently deleted object', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $objectEntity->getUuid(), + ] + ); + } catch (\Throwable $e) { + // A missing or legacy folder must not abort the permanent delete. + $this->logger->warning( + message: '[DeleteObject] Failed to remove Nextcloud folder during permanent delete', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $objectEntity->getUuid(), + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end deleteObjectFolder() + /** * Perform pre-flight deletion analysis for an object. * @@ -357,8 +470,8 @@ public function delete(array | JsonSerializable $object, ?array $cascadeContext= * * @return DeletionAnalysis The analysis result. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-29 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function canDelete(ObjectEntity $object): DeletionAnalysis { @@ -385,6 +498,12 @@ public function canDelete(ObjectEntity $object): DeletionAnalysis * without touching any row (#1638). * When false (default), legacy cross-table lookup * via `findAcrossAllSources` is used. + * @param ObjectEntity|null $preResolved Optional already-fetched entity for `$uuid`. Callers + * that batch-resolve many objects up front (bulk delete) + * pass it together with concrete Register + Schema + * entities so the handler skips its own per-object + * cross-table lookup. Ignored unless its uuid matches + * `$uuid` and both scope entities are concrete. * * @return bool Whether the deletion was successful. * @@ -396,8 +515,8 @@ public function canDelete(ObjectEntity $object): DeletionAnalysis * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - * @spec openspec/changes/scoped-object-delete-api/tasks.md#2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + * @spec openspec/specs/object-lifecycle/spec.md */ public function deleteObject( Register | int | string | null $register, @@ -406,8 +525,31 @@ public function deleteObject( ?string $originalObjectId=null, bool $_rbac=true, bool $_multitenancy=true, - bool $scoped=false + bool $scoped=false, + ?ObjectEntity $preResolved=null ): bool { + // Read-only projection guard: a schema served from an external source + // (x-openregister-object-source) is read-only — deletes are not allowed. + $objectSource = null; + if ($schema instanceof Schema) { + $objectSource = $schema->getObjectSource(); + } + + if ($objectSource !== null) { + // Opt-in write-through (dbal-virtual-registers-crud): delegate to a + // WritableObjectSourceProvider when the annotation carries + // `readOnly: false`; the provider re-verifies the backing source's + // writable flag live (fail closed). Everything else keeps the v1 + // read-only rejection. Delete RBAC already ran upstream in + // ObjectService::delete before this dispatch. + return $this->delegateObjectSourceDelete( + register: $register, + schema: $schema, + objectSource: $objectSource, + uuid: $uuid + ); + } + // Reset cascade count for root deletions. if ($originalObjectId === null) { $this->lastCascadeCount = 0; @@ -424,7 +566,8 @@ public function deleteObject( uuid: $uuid, scoped: $scoped, _rbac: $_rbac, - _multitenancy: $_multitenancy + _multitenancy: $_multitenancy, + preResolved: $preResolved ); $object = $context['object']; @@ -442,7 +585,12 @@ public function deleteObject( } try { - return $this->delete(object: $object); + // Pass the already-resolved scope so delete() skips its own re-find. + return $this->delete( + object: $object, + register: $this->contextRegisterEntity(context: $context), + schema: $this->contextSchemaEntity(context: $context) + ); } catch (Exception $e) { $this->logger->warning( message: '[DeleteObject] Delete failed', @@ -474,6 +622,10 @@ public function deleteObject( * magic table; lookup MUST target only that table. * @param bool $_rbac Whether to apply RBAC checks. * @param bool $_multitenancy Whether to apply multitenancy filtering. + * @param ObjectEntity|null $preResolved Optional already-fetched entity for $uuid; + * used (and the lookup skipped entirely) only + * when its uuid matches and both scope + * parameters are concrete entities. * * @return array{object: ObjectEntity, register: Register|int|string, schema: Schema|int|string} * @@ -482,7 +634,7 @@ public function deleteObject( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) * - * @spec openspec/changes/scoped-object-delete-api/tasks.md#2 + * @spec openspec/specs/object-lifecycle/spec.md */ private function resolveDeletionContext( Register | int | string | null $register, @@ -490,8 +642,25 @@ private function resolveDeletionContext( string $uuid, bool $scoped, bool $_rbac, - bool $_multitenancy + bool $_multitenancy, + ?ObjectEntity $preResolved=null ): array { + // PERF: a caller that already resolved the entity (bulk delete performs + // ONE batched cross-table lookup for all UUIDs) passes it in — skip the + // per-object magic-table scan entirely. Defensive: the entity must match + // the requested uuid and the scope must be concrete entities. + if ($preResolved !== null + && $preResolved->getUuid() === $uuid + && $register instanceof Register === true + && $schema instanceof Schema === true + ) { + return [ + 'object' => $preResolved, + 'register' => $register, + 'schema' => $schema, + ]; + } + // Scoped path: lookup hits exactly one magic table — a UUID in a // different scope raises DoesNotExistException. This is the fix for // #1638 (cross-scope silent deletes). @@ -527,6 +696,44 @@ private function resolveDeletionContext( }//end resolveDeletionContext() + /** + * Extract a concrete Register entity from a deletion context, or null. + * + * @param array $context The deletion context (from resolveDeletionContext()). + * + * @return Register|null The register entity when the context holds one. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function contextRegisterEntity(array $context): ?Register + { + $contextRegister = ($context['register'] ?? null); + if ($contextRegister instanceof Register === true) { + return $contextRegister; + } + + return null; + }//end contextRegisterEntity() + + /** + * Extract a concrete Schema entity from a deletion context, or null. + * + * @param array $context The deletion context (from resolveDeletionContext()). + * + * @return Schema|null The schema entity when the context holds one. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function contextSchemaEntity(array $context): ?Schema + { + $contextSchema = ($context['schema'] ?? null); + if ($contextSchema instanceof Schema === true) { + return $contextSchema; + } + + return null; + }//end contextSchemaEntity() + /** * Handle referential integrity checks and cascade deletion for root deletions. * @@ -542,7 +749,7 @@ private function resolveDeletionContext( * @throws ReferentialIntegrityException If blocked by RESTRICT * @throws Exception If cascade transaction fails * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function handleIntegrityDeletion( ObjectEntity $object, @@ -584,7 +791,7 @@ private function handleIntegrityDeletion( * * @throws ReferentialIntegrityException Always thrown * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function logAndThrowRestrict(string $uuid, ?string $schemaId, DeletionAnalysis $analysis): void { @@ -613,7 +820,7 @@ private function logAndThrowRestrict(string $uuid, ?string $schemaId, DeletionAn * * @throws Exception If the transaction fails * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function executeIntegrityTransaction( ObjectEntity $object, @@ -652,7 +859,12 @@ private function executeIntegrityTransaction( analysis: $analysis ); - $result = $this->delete(object: $object, cascadeContext: $rootCascadeCtx); + $result = $this->delete( + object: $object, + cascadeContext: $rootCascadeCtx, + register: $this->contextRegisterEntity(context: $context), + schema: $this->contextSchemaEntity(context: $context) + ); $this->db->commit(); return $result; } catch (Exception $e) { @@ -680,7 +892,7 @@ private function executeIntegrityTransaction( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function runLegacyCascade(array $context, ObjectEntity $object, string $uuid): void { @@ -707,7 +919,7 @@ private function runLegacyCascade(array $context, ObjectEntity $object, string $ * * @return array|null The cascade context, or null if no cascades occurred * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function buildCascadeContext(string $uuid, ?string $triggerSlug, DeletionAnalysis $analysis): ?array { @@ -735,7 +947,7 @@ private function buildCascadeContext(string $uuid, ?string $triggerSlug, Deletio * * @return array{0: string, 1: mixed} [userId, activeOrganisation] * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function resolveUserContext(): array { @@ -760,6 +972,15 @@ private function resolveUserContext(): array /** * Handles cascading deletes for related objects (legacy cascade: true). * + * PERF: all referenced ids are collected first and resolved with ONE batched + * cross-table lookup, then soft-deleted with one UPDATE per magic table and + * one multi-row audit INSERT — instead of running the full per-object delete + * pipeline (cross-table scan, re-find, row update, audit insert) per id. + * Ids the uuid-based batch lookup cannot resolve (numeric ids, slugs, URIs) + * fall back to the legacy per-id pipeline, preserving prior behaviour. + * Cascade depth is unchanged: children are sub-deletions (originalObjectId + * set) and never cascade further, exactly like the per-id pipeline. + * * @param Register $register The register containing the object. * @param Schema $schema The schema of the object. * @param ObjectEntity $object The object being deleted. @@ -767,7 +988,7 @@ private function resolveUserContext(): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function cascadeDeleteObjects( Register $register, @@ -775,6 +996,61 @@ private function cascadeDeleteObjects( ObjectEntity $object, string $originalObjectId ): void { + $cascadeIds = $this->collectCascadeTargetIds(schema: $schema, object: $object); + if (empty($cascadeIds) === true) { + return; + } + + // Resolve every level-one cascade target in one batched lookup. + $targets = []; + try { + $targets = $this->objectEntityMapper->findMultipleAcrossAllMagicTables( + uuids: $cascadeIds, + includeDeleted: true + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[DeleteObject] Batched cascade lookup failed; falling back to per-id deletes', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + } + + $handledIds = $this->batchCascadeSoftDelete(targets: $targets); + + // Legacy per-id pipeline for ids the batch lookup did not resolve. + foreach ($cascadeIds as $cascadeId) { + if (isset($handledIds[$cascadeId]) === true) { + continue; + } + + $this->deleteObject( + register: $register, + schema: $schema, + uuid: $cascadeId, + originalObjectId: $originalObjectId + ); + } + }//end cascadeDeleteObjects() + + /** + * Collect the referenced ids of all `cascade: true` schema properties. + * + * @param Schema $schema The schema whose properties are inspected. + * @param ObjectEntity $object The object holding the reference values. + * + * @return string[] Unique, non-empty referenced ids. + * + * @psalm-return list + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function collectCascadeTargetIds(Schema $schema, ObjectEntity $object): array + { + $ids = []; $properties = $schema->getProperties(); foreach ($properties ?? [] as $propertyName => $property) { if (isset($property['cascade']) === false || $property['cascade'] !== true) { @@ -786,34 +1062,196 @@ private function cascadeDeleteObjects( continue; } + $candidates = [$value]; if (is_array($value) === true) { - foreach ($value as $id) { - $this->deleteObject( - register: $register, - schema: $schema, - uuid: $id, - originalObjectId: $originalObjectId - ); + $candidates = $value; + } + + foreach ($candidates as $candidate) { + if (is_scalar($candidate) === false || (string) $candidate === '') { + continue; } + $ids[] = (string) $candidate; + } + }//end foreach + + return array_values(array_unique($ids)); + }//end collectCascadeTargetIds() + + /** + * Soft-delete a batch of resolved cascade targets. + * + * Applies the same deletion metadata shape as {@see delete()}, persists via + * one UPDATE per magic table (MagicMapper::softDeleteMultipleObjectEntities, + * which also dispatches the per-object updating/updated events the per-id + * pipeline produced), writes the audit rows with one multi-row INSERT, and + * invalidates caches per object. + * + * @param ObjectEntity[] $targets The batch-resolved cascade target entities. + * + * @return array Ids taken care of by the batch (keyed by uuid). + * Empty when the batch write failed entirely — the + * caller then falls back to per-id deletes. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function batchCascadeSoftDelete(array $targets): array + { + if (empty($targets) === true) { + return []; + } + + [$userId, $activeOrganisation] = $this->resolveUserContext(); + $deletedAt = (new DateTime())->format(DateTime::ATOM); + + $handled = []; + $oldEntities = []; + foreach ($targets as $entity) { + $uuid = $entity->getUuid(); + if ($uuid === null || $uuid === '') { continue; } - $this->deleteObject( - register: $register, - schema: $schema, - uuid: $value, - originalObjectId: $originalObjectId + $handled[$uuid] = true; + $oldEntities[$uuid] = clone $entity; + $entity->setDeleted( + [ + 'deletedBy' => $userId, + 'deletedAt' => $deletedAt, + 'objectId' => $uuid, + 'organisation' => $activeOrganisation, + ] + ); + } + + try { + $deleted = $this->objectEntityMapper->softDeleteMultipleObjectEntities( + entities: $targets, + oldEntities: $oldEntities + ); + } catch (\Throwable $e) { + // Nothing was handled — the caller retries every id via the + // legacy per-id pipeline. + $this->logger->error( + message: '[DeleteObject] Batched cascade soft delete failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] ); + return []; + } + + $this->writeCascadeAuditTrails(deleted: $deleted); + $this->invalidateCachesForDeleted(deleted: $deleted); + + return $handled; + }//end batchCascadeSoftDelete() + + /** + * Write the audit rows for batch-cascade-deleted objects in one bulk INSERT. + * + * Row shape matches the per-object path exactly (buildAuditTrail with + * action `delete` and the post-soft-delete entity as the old state). An + * audit failure is logged but never aborts the cascade — parity with the + * per-id pipeline, where a throwing audit write left the object deleted + * and the cascade running. + * + * @param ObjectEntity[] $deleted The successfully soft-deleted entities. + * + * @return void + * + * @spec openspec/specs/deletion-audit-trail/spec.md + */ + private function writeCascadeAuditTrails(array $deleted): void + { + if (empty($deleted) === true || $this->isAuditTrailsEnabled() === false) { + return; + } + + $rows = []; + foreach ($deleted as $entity) { + try { + $rows[] = $this->auditTrailMapper->buildAuditTrail( + old: $entity, + new: null, + action: 'delete' + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[DeleteObject] Could not build cascade audit row', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $entity->getUuid(), + 'error' => $e->getMessage(), + ] + ); + } + } + + if (empty($rows) === true) { + return; + } + + try { + $this->auditTrailMapper->insertAuditTrails(entries: $rows); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[DeleteObject] Bulk cascade audit insert failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'count' => count($rows), + 'error' => $e->getMessage(), + ] + ); + } + }//end writeCascadeAuditTrails() + + /** + * Invalidate caches per soft-deleted object (parity with delete()). + * + * @param ObjectEntity[] $deleted The successfully soft-deleted entities. + * + * @return void + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function invalidateCachesForDeleted(array $deleted): void + { + foreach ($deleted as $entity) { + $registerIdInt = null; + if (is_numeric($entity->getRegister()) === true) { + $registerIdInt = (int) $entity->getRegister(); + } + + $schemaIdInt = null; + if (is_numeric($entity->getSchema()) === true) { + $schemaIdInt = (int) $entity->getSchema(); + } + + try { + $this->cacheHandler->invalidateForObjectChange( + object: $entity, + operation: 'soft_delete', + registerId: $registerIdInt, + schemaId: $schemaIdInt + ); + } catch (\Exception $e) { + // Cache invalidation failures never abort a delete (parity with delete()). + }//end try }//end foreach - }//end cascadeDeleteObjects() + }//end invalidateCachesForDeleted() /** * Check if audit trails are enabled in the settings * * @return bool True if audit trails are enabled, false otherwise * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function isAuditTrailsEnabled(): bool { @@ -838,10 +1276,92 @@ private function isAuditTrailsEnabled(): bool * * @return int The number of cascade-deleted objects. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function getLastCascadeCount(): int { return $this->lastCascadeCount; }//end getLastCascadeCount() + + /** + * Delegate a delete on an object-source schema to its writable provider. + * + * Preserves the v1 read-only rejection unless the annotation carries + * `readOnly: false` AND a writable provider is registered AND the register + * resolves. External deletes are hard deletes; zero affected rows surfaces + * as the same DoesNotExistException (404) an absent native object produces. + * + * @param Register|int|string|null $register The register (entity or identifier). + * @param Schema $schema The sourced schema. + * @param array $objectSource The `x-openregister-object-source` annotation. + * @param string $uuid The object id (external key). + * + * @return bool True when the external row was deleted. + * + * @throws \RuntimeException When the schema is not writable (v1 rejection). + * @throws \OCP\AppFramework\Db\DoesNotExistException When no external row matches the id. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Fail-closed writable checks require explicit branching + * @SuppressWarnings(PHPMD.NPathComplexity) Fail-closed writable checks require explicit branching + */ + private function delegateObjectSourceDelete( + Register | int | string | null $register, + Schema $schema, + array $objectSource, + string $uuid + ): bool { + $provider = null; + if ($this->objectSourceRegistry !== null) { + $provider = $this->objectSourceRegistry->get((string) $objectSource['provider']); + } + + $registerEntity = null; + if ($register instanceof Register) { + $registerEntity = $register; + } else if ($register !== null && $this->registerMapper !== null) { + try { + $registerEntity = $this->registerMapper->find($register); + } catch (\Throwable $e) { + $registerEntity = null; + } + } + + $writableOptIn = (($objectSource['readOnly'] ?? true) === false); + $writable = ($provider instanceof \OCA\OpenRegister\Service\ObjectSource\WritableObjectSourceProvider); + + if ($writableOptIn === false || $writable === false || $registerEntity === null) { + throw new \RuntimeException( + sprintf( + 'Schema "%s" is a read-only projection of object-source provider "%s"; deletes are not allowed.', + (string) $schema->getSlug(), + $objectSource['provider'] + ) + ); + } + + $config = ($objectSource['config'] ?? []); + + // Pre-read for the audit record (best effort). + $old = $provider->find(register: $registerEntity, schema: $schema, id: $uuid, config: $config); + + $deleted = $provider->remove(register: $registerEntity, schema: $schema, id: $uuid, config: $config); + if ($deleted === false) { + throw new \OCP\AppFramework\Db\DoesNotExistException('No external row matches id '.$uuid); + } + + if ($old !== null) { + try { + $this->auditTrailMapper->createAuditTrail(old: $old, new: null, action: 'delete'); + } catch (\Throwable $e) { + $this->logger->warning( + '[DeleteObject] audit trail for external delete on uuid '.$uuid.' could not be recorded: '.$e->getMessage(), + ['file' => __FILE__, 'line' => __LINE__] + ); + } + } + + return true; + }//end delegateObjectSourceDelete() }//end class diff --git a/lib/Service/Object/ExportHandler.php b/lib/Service/Object/ExportHandler.php index 46f51d3f65..1fbea98886 100644 --- a/lib/Service/Object/ExportHandler.php +++ b/lib/Service/Object/ExportHandler.php @@ -20,7 +20,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/data-import-export/spec.md */ declare(strict_types=1); @@ -76,7 +76,7 @@ class ExportHandler * @param FileService $fileService File service * @param LoggerInterface $logger PSR-3 logger * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-11 + * @spec openspec/specs/data-import-export/spec.md */ public function __construct( private readonly MagicMapper $objectEntityMapper, @@ -101,7 +101,7 @@ public function __construct( * * @throws \Exception If export fails. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-11 + * @spec openspec/specs/data-import-export/spec.md */ public function export( Register $register, @@ -217,7 +217,7 @@ public function export( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple file type handlers require conditional branching * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Import orchestration requires comprehensive error handling * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-15 + * @spec openspec/specs/data-import-export/spec.md */ public function import( Register $register, @@ -344,7 +344,7 @@ public function import( * * @throws \Exception If download fails. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-11 + * @spec openspec/specs/data-import-export/spec.md */ public function downloadObjectFiles(string $objectId) { diff --git a/lib/Service/Object/FacetHandler.php b/lib/Service/Object/FacetHandler.php index 0f2c76c286..fee4504c3a 100644 --- a/lib/Service/Object/FacetHandler.php +++ b/lib/Service/Object/FacetHandler.php @@ -18,16 +18,16 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-28 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-31 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-32 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-34 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-36 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-43 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-44 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-45 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-type-auto-detection-from-property-definitions + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation + * @spec openspec/specs/faceting-configuration/spec.md#requirement-metadata-facets-via-self-namespace + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-discovery-via-facetable-parameter + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-response-standardized-format + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-performance-optimization-via-hyperfacethandler + * @spec openspec/specs/faceting-configuration/spec.md + * @spec openspec/specs/faceting-configuration/spec.md + * @spec openspec/specs/faceting-configuration/spec.md */ declare(strict_types=1); @@ -107,7 +107,7 @@ class FacetHandler * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ public function __construct( private readonly MagicMapper $unifiedObjectMapper, @@ -168,8 +168,8 @@ public function __construct( * @psalm-return array * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-43 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter + * @spec openspec/specs/faceting-configuration/spec.md */ public function getFacetsForObjects(array $query=[]): array { @@ -258,8 +258,8 @@ public function getFacetsForObjects(array $query=[]): array * @psalm-return array{'@self': array, object_fields: array} * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-32 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-44 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-discovery-via-facetable-parameter + * @spec openspec/specs/faceting-configuration/spec.md */ public function getFacetableFields(array $baseQuery=[], int $_sampleSize=100): array { @@ -297,8 +297,8 @@ public function getFacetableFields(array $baseQuery=[], int $_sampleSize=100): a * @psalm-return list{'register', 'schema', 'owner', 'organisation', 'created', 'updated'} * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-32 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-45 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-discovery-via-facetable-parameter + * @spec openspec/specs/faceting-configuration/spec.md */ public function getMetadataFacetableFields(): array { @@ -328,7 +328,7 @@ public function getMetadataFacetableFields(): array * @psalm-return int<0, max> * @phpstan-return int * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-36 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-performance-optimization-via-hyperfacethandler */ public function getFacetCount(bool $hasFacets, array $query): int { @@ -357,7 +357,7 @@ public function getFacetCount(bool $hasFacets, array $query): int * * @return array Facets with performance metadata including strategy and fallback status. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter */ private function calculateFacetsWithFallback(array $facetQuery, array $facetConfig, array $facetableConfig=[]): array { @@ -484,7 +484,7 @@ private function calculateFacetsWithFallback(array $facetQuery, array $facetConf * * @return string A unique facet key. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function generateNonAggregatedFacetKey(string $fieldName, int $schemaId, array $facetConfig): string { @@ -518,7 +518,7 @@ private function generateNonAggregatedFacetKey(string $fieldName, int $schemaId, * * @return array Transformed facets in standardized format. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-34 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-response-standardized-format */ private function transformFacetsToStandardFormat(array $facets, array $facetableConfig=[]): array { @@ -579,7 +579,7 @@ private function transformFacetsToStandardFormat(array $facets, array $facetable * @return array Keyed by field name, each value contains title, * description, data_type, index_field, index_type, enabled. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-28 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-type-auto-detection-from-property-definitions */ private function getMetadataDefinitions(): array { @@ -644,7 +644,7 @@ private function getMetadataDefinitions(): array * * @return int The updated order counter after processing metadata facets. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-metadata-facets-via-self-namespace */ private function transformMetadataFacets(array $metadataFacets, array &$transformed, int $startOrder): int { @@ -688,7 +688,7 @@ private function transformMetadataFacets(array $metadataFacets, array &$transfor * * @return int The updated order counter. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function transformNonAggregatedFacet( string $field, @@ -758,7 +758,7 @@ private function transformNonAggregatedFacet( * * @return int The updated order counter. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-29 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-non-aggregated-facet-isolation */ private function transformAggregatedFacet( string $field, @@ -829,7 +829,7 @@ private function transformAggregatedFacet( * * @return array The formatted facet entry. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-34 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-response-standardized-format */ private function buildFacetEntry( string $name, @@ -888,7 +888,7 @@ private function buildFacetEntry( * * @return string The formatted title (e.g., Cloud Dienstverleningsmodel). * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-34 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-response-standardized-format */ private function formatFieldTitle(string $field): string { @@ -904,7 +904,7 @@ private function formatFieldTitle(string $field): string * * @return string The sanitized field name. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ private function sanitizeFieldName(string $field): string { @@ -922,7 +922,7 @@ private function sanitizeFieldName(string $field): string * * @return string The inferred data type. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ private function inferDataType(array $facetData): string { @@ -960,7 +960,7 @@ private function inferDataType(array $facetData): string * * @return string Cache key. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ private function generateFacetCacheKey(array $facetQuery, array $facetConfig): string { @@ -997,7 +997,7 @@ private function generateFacetCacheKey(array $facetQuery, array $facetConfig): s * * @return array|null Cached response or null if not found. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ private function getCachedFacetResponse(string $cacheKey): ?array { @@ -1031,7 +1031,7 @@ private function getCachedFacetResponse(string $cacheKey): ?array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ private function cacheFacetResponse(string $cacheKey, array $result): void { @@ -1071,7 +1071,7 @@ private function cacheFacetResponse(string $cacheKey, array $result): void * * @return int Total number of facet results. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-33 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-request-configuration-via-facets-parameter */ private function countFacetResults(array $facets): int { @@ -1099,7 +1099,7 @@ private function countFacetResults(array $facets): int * * @return bool True if query has restrictive filters. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ private function hasRestrictiveFilters(array $query): bool { @@ -1127,7 +1127,7 @@ private function hasRestrictiveFilters(array $query): bool * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ private function getSchemasForQuery(array $baseQuery): array { @@ -1177,7 +1177,7 @@ private function getSchemasForQuery(array $baseQuery): array * * @return array|null Normalized config or null if not facetable. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-9 + * @spec openspec/specs/faceting-configuration/spec.md */ private function normalizeFacetConfig(mixed $facetable): ?array { @@ -1219,7 +1219,7 @@ private function normalizeFacetConfig(mixed $facetable): ?array * * @psalm-return array{'@self': array, object_fields: array, non_aggregated_fields: array} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-32 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-discovery-via-facetable-parameter */ private function getFacetableFieldsFromSchemas(array $schemas): array { @@ -1239,6 +1239,16 @@ private function getFacetableFieldsFromSchemas(array $schemas): array $schemaId = $schema->getId(); $properties = $schema->getProperties() ?? []; foreach ($properties as $propertyKey => $property) { + // Encrypted properties are never facetable, even when a schema + // author also sets `facetable: true` on one by mistake — the + // magic-table value is ciphertext (or, once + // MagicMapper::buildTableColumnsFromSchema() skips it, not even a + // real column), so grouping by it would be meaningless at best + // and a ciphertext leak into facet option labels at worst. + if (($property['x-openregister-encrypted'] ?? false) === true) { + continue; + } + $facetConfig = $this->normalizeFacetConfig(facetable: $property['facetable'] ?? false); if ($facetConfig === null) { continue; @@ -1294,7 +1304,7 @@ private function getFacetableFieldsFromSchemas(array $schemas): array * * @return array Default metadata facet configuration. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-metadata-facets-via-self-namespace */ private function getDefaultMetadataFacets(): array { @@ -1314,7 +1324,7 @@ private function getDefaultMetadataFacets(): array * * @return string The facet type ('terms' or 'date_histogram'). * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-28 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-facet-type-auto-detection-from-property-definitions */ private function determineFacetTypeFromProperty(array $property): string { diff --git a/lib/Service/Object/GetObject.php b/lib/Service/Object/GetObject.php index 99af2988be..fdf0cdf5fb 100644 --- a/lib/Service/Object/GetObject.php +++ b/lib/Service/Object/GetObject.php @@ -37,6 +37,8 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCA\OpenRegister\Db\AuditTrailMapper; use OCA\OpenRegister\Service\SettingsService; +use OCA\OpenRegister\Service\ObjectSource\ObjectSourceRegistry; +use Psr\Log\LoggerInterface; /** * Handler class for retrieving objects in the OpenRegister application. @@ -57,19 +59,67 @@ class GetObject /** * Constructor for GetObject handler. * - * @param MagicMapper $objectMapper Object entity data mapper. - * @param AuditTrailMapper $auditTrailMapper Audit trail mapper for logs. - * @param SettingsService $settingsService Settings service for accessing trail settings. + * @param MagicMapper $objectMapper Object entity data mapper. + * @param AuditTrailMapper $auditTrailMapper Audit trail mapper for logs. + * @param SettingsService $settingsService Settings service for accessing trail settings. + * @param ObjectSourceRegistry $objectSourceRegistry Registry of object-source providers (virtual schemas). + * @param LoggerInterface $logger Logger for object-source delegation warnings. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly MagicMapper $objectMapper, private readonly AuditTrailMapper $auditTrailMapper, - private readonly SettingsService $settingsService + private readonly SettingsService $settingsService, + private readonly ObjectSourceRegistry $objectSourceRegistry, + private readonly LoggerInterface $logger ) { }//end __construct() + /** + * Resolve the ObjectSourceProvider for a schema, or null when the schema is + * served from the magic table (no `x-openregister-object-source`). + * + * When a schema declares an object source whose provider is missing or + * disabled, this logs a warning and returns null WITH `$sourced` set true, + * so the caller degrades to an empty result instead of reading the database. + * + * @param Schema|null $schema The schema being read (null = magic table). + * @param bool $sourced Set true when the schema declares an object source. + * + * @return \OCA\OpenRegister\Service\ObjectSource\ObjectSourceProvider|null + * An enabled provider, or null (see $sourced). + * + * @spec openspec/changes/object-source-providers/tasks.md#task-3.1 + */ + private function resolveObjectSource(?Schema $schema, bool &$sourced): ?object + { + $sourced = false; + if ($schema === null) { + return null; + } + + $source = $schema->getObjectSource(); + if ($source === null) { + return null; + } + + $sourced = true; + $provider = $this->objectSourceRegistry->get($source['provider']); + if ($provider === null || $provider->isEnabled() === false) { + $this->logger->warning( + sprintf( + '[ObjectSource] schema "%s" declares object-source provider "%s" but it is missing or disabled — returning empty result', + (string) $schema->getSlug(), + (string) $source['provider'] + ) + ); + return null; + } + + return $provider; + }//end resolveObjectSource() + /** * Gets an object by its ID with optional extensions. * @@ -90,7 +140,7 @@ public function __construct( * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags required for flexible API filtering * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function find( string $id, @@ -101,6 +151,21 @@ public function find( bool $_rbac=true, bool $_multitenancy=true ): ObjectEntity { + // Object-source delegation: for a schema served from an external source + // (x-openregister-object-source) the object is fetched live from the + // provider and never read from the magic table. Absent/denied → 404. + $sourced = false; + $provider = $this->resolveObjectSource(schema: $schema, sourced: $sourced); + if ($sourced === true) { + $config = ($schema->getObjectSource()['config'] ?? []); + $virtual = $provider?->find(register: $register, schema: $schema, id: $id, config: $config); + if ($virtual === null) { + throw new DoesNotExistException(sprintf('Object %s not found', $id)); + } + + return $virtual; + } + $object = $this->objectMapper->find( identifier: $id, register: $register, @@ -145,7 +210,7 @@ public function find( * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags required for flexible API filtering * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function findSilent( string $id, @@ -156,6 +221,19 @@ public function findSilent( bool $_rbac=true, bool $_multitenancy=true ): ObjectEntity { + // Object-source delegation (silent read, no audit) — see find(). + $sourced = false; + $provider = $this->resolveObjectSource(schema: $schema, sourced: $sourced); + if ($sourced === true) { + $config = ($schema->getObjectSource()['config'] ?? []); + $virtual = $provider?->find(register: $register, schema: $schema, id: $id, config: $config); + if ($virtual === null) { + throw new DoesNotExistException(sprintf('Object %s not found', $id)); + } + + return $virtual; + } + $object = $this->objectMapper->find( identifier: $id, register: $register, @@ -198,7 +276,7 @@ public function findSilent( * @SuppressWarnings(PHPMD.ExcessiveParameterList) Required for flexible query interface * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags required for flexible API filtering * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function findAll( ?int $limit=null, @@ -215,6 +293,41 @@ public function findAll( bool $_rbac=true, bool $_multitenancy=true ): array { + // Object-source delegation: a schema served from an external source + // lists live from the provider, never the magic table. A missing/disabled + // provider degrades to an empty list (resolveObjectSource logs it). + $sourced = false; + $provider = $this->resolveObjectSource(schema: $schema, sourced: $sourced); + if ($sourced === true) { + if ($provider === null) { + return []; + } + + $source = $schema->getObjectSource(); + $query = [ + 'limit' => $limit, + 'offset' => $offset, + 'filters' => $filters, + 'sort' => $sort, + 'search' => $search, + 'ids' => $ids, + ]; + + return $provider->findAll( + register: $register, + schema: $schema, + query: $query, + config: ($source['config'] ?? []) + ); + }//end if + + // Thread the RBAC / multitenancy posture into the filters so the search + // handler honours them. These are read from the query array downstream; + // passing them only as method arguments left them silently dropped, so + // a caller's `_rbac:false` (e.g. installer/system context) had no effect. + $filters['_rbac'] = $_rbac; + $filters['_multitenancy'] = $_multitenancy; + // Retrieve objects using the objectEntityMapper with optional register, schema, and ids. $objects = $this->objectMapper->findAll( limit: $limit, @@ -247,7 +360,7 @@ public function findAll( * * @return ObjectEntity The hydrated object. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function hydrateFiles(ObjectEntity $object, array $files): ObjectEntity { @@ -290,7 +403,7 @@ private function hydrateFiles(ObjectEntity $object, array $files): ObjectEntity * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags required for flexible API filtering * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function findLogs( ObjectEntity $object, @@ -320,7 +433,7 @@ public function findLogs( * * @return bool True if audit trails are enabled, false otherwise * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function isAuditTrailsEnabled(): bool { diff --git a/lib/Service/Object/LinkedEntityEnricher.php b/lib/Service/Object/LinkedEntityEnricher.php index c98d8abe9d..85153ca405 100644 --- a/lib/Service/Object/LinkedEntityEnricher.php +++ b/lib/Service/Object/LinkedEntityEnricher.php @@ -6,12 +6,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -67,7 +67,7 @@ class LinkedEntityEnricher * @param IUserManager $userManager User manager for resolving display names * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function __construct( private readonly IDBConnection $db, @@ -85,7 +85,7 @@ public function __construct( * * @return array The object data with enriched linked entities * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function enrich(array $objectData, array $extend): array { @@ -114,7 +114,7 @@ public function enrich(array $objectData, array $extend): array * * @return array Array of enriched mail objects * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function enrichMail(array $ids): array { @@ -163,7 +163,7 @@ private function enrichMail(array $ids): array * * @return array Array of enriched contact objects * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function enrichContacts(array $ids): array { @@ -208,7 +208,7 @@ private function enrichContacts(array $ids): array * * @return array Array of enriched note objects * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function enrichNotes(array $ids): array { @@ -246,7 +246,7 @@ private function enrichNotes(array $ids): array * * @return array Array of enriched todo objects * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function enrichTodos(array $ids): array { @@ -301,7 +301,7 @@ private function enrichTodos(array $ids): array * * @return array Array of enriched event objects * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function enrichCalendar(array $ids): array { @@ -356,7 +356,7 @@ private function enrichCalendar(array $ids): array * * @return array Array of enriched Talk objects * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function enrichTalk(array $ids): array { @@ -397,7 +397,7 @@ private function enrichTalk(array $ids): array * * @return array Array of enriched Deck objects * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function enrichDeck(array $ids): array { @@ -450,7 +450,7 @@ private function enrichDeck(array $ids): array * * @return array The fallback result * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function notFoundResult(string $id): array { @@ -468,7 +468,7 @@ private function notFoundResult(string $id): array * * @return string|null The field value or null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function extractVcardField(string $carddata, string $field): ?string { @@ -487,7 +487,7 @@ private function extractVcardField(string $carddata, string $field): ?string * * @return string|null The field value or null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function extractIcalField(string $caldata, string $field): ?string { diff --git a/lib/Service/Object/LockHandler.php b/lib/Service/Object/LockHandler.php index 3b7f0c3955..647bb7109f 100644 --- a/lib/Service/Object/LockHandler.php +++ b/lib/Service/Object/LockHandler.php @@ -20,7 +20,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-59 + * @spec openspec/specs/object-interactions/spec.md */ declare(strict_types=1); @@ -36,6 +36,7 @@ use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Exception\LockedException; +use OCP\IAppConfig; use OCP\IGroupManager; use OCP\IUserSession; use Psr\Log\LoggerInterface; @@ -54,6 +55,8 @@ * * @category Service * @package OCA\OpenRegister\Service\Objects\Handlers + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class LockHandler { @@ -66,6 +69,7 @@ class LockHandler * @param IUserSession $userSession User session for authorization checks * @param IGroupManager $groupManager Group manager for admin checks * @param SchemaMapper $schemaMapper Schema mapper for resolving manage rules + * @param IAppConfig $appConfig App config store for advisory (pre-creation) locks */ public function __construct( private readonly MagicMapper $magicMapper, @@ -73,10 +77,119 @@ public function __construct( private readonly LoggerInterface $logger, private readonly IUserSession $userSession, private readonly IGroupManager $groupManager, - private readonly SchemaMapper $schemaMapper + private readonly SchemaMapper $schemaMapper, + private readonly IAppConfig $appConfig ) { }//end __construct() + /** + * Advisory-lock app-config key prefix. + * + * @var string + */ + private const ADVISORY_LOCK_PREFIX = 'advisory_lock_'; + + /** + * Default advisory lock duration in seconds when none supplied. + * + * @var int + */ + private const ADVISORY_LOCK_DEFAULT_DURATION = 3600; + + /** + * Build the app-config key used to store an advisory (pre-creation) lock. + * + * @param string $identifier The arbitrary advisory-lock identifier. + * + * @return string The namespaced app-config key. + */ + private function advisoryLockKey(string $identifier): string + { + return self::ADVISORY_LOCK_PREFIX.md5($identifier); + }//end advisoryLockKey() + + /** + * Acquire an advisory (pre-creation) lock for an arbitrary identifier that + * does not (yet) resolve to a stored object. + * + * Supports create-then-store flows (e.g. the openbuild wizard locking + * `createApp:` before the object exists). The lock is stored in + * app-config with an expiry timestamp. A still-valid lock raises + * LockedException; an expired lock is silently overwritten. + * + * @param string $identifier The arbitrary advisory-lock identifier. + * @param string|null $process Optional process tag (who holds the lock). + * @param int|null $duration Lock duration in seconds. + * + * @return array{uuid: string, locked: array} Advisory lock result. + * + * @throws LockedException If a non-expired advisory lock already exists. + */ + private function acquireAdvisoryLock(string $identifier, ?string $process=null, ?int $duration=null): array + { + $duration = ($duration ?? self::ADVISORY_LOCK_DEFAULT_DURATION); + $key = $this->advisoryLockKey(identifier: $identifier); + $now = new DateTime(); + + $existingRaw = $this->appConfig->getValueString('openregister', $key, ''); + if ($existingRaw !== '') { + $existing = json_decode($existingRaw, true); + if (is_array($existing) === true && isset($existing['expiration']) === true) { + $expiration = new DateTime($existing['expiration']); + if ($expiration > $now) { + throw new LockedException(message: "Advisory lock '{$identifier}' is already held"); + } + } + } + + $expiration = (clone $now)->modify("+{$duration} seconds"); + $lock = [ + 'user' => $this->userSession->getUser()?->getUID(), + 'process' => $process, + 'created' => $now->format(DateTime::ATOM), + 'duration' => $duration, + 'expiration' => $expiration->format(DateTime::ATOM), + 'advisory' => true, + ]; + + $this->appConfig->setValueString('openregister', $key, json_encode($lock)); + + $this->logger->info( + message: '[LockHandler] Advisory (pre-creation) lock acquired', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'identifier' => $identifier, + 'process' => $process, + ] + ); + + return ['uuid' => $identifier, 'locked' => $lock]; + }//end acquireAdvisoryLock() + + /** + * Release an advisory (pre-creation) lock if one exists for the identifier. + * + * @param string $identifier The arbitrary advisory-lock identifier. + * + * @return bool True if an advisory lock was found and removed. + */ + private function releaseAdvisoryLock(string $identifier): bool + { + $key = $this->advisoryLockKey(identifier: $identifier); + if ($this->appConfig->getValueString('openregister', $key, '') === '') { + return false; + } + + $this->appConfig->deleteKey('openregister', $key); + $this->logger->info( + message: '[LockHandler] Advisory (pre-creation) lock released', + context: ['file' => __FILE__, 'line' => __LINE__, 'identifier' => $identifier] + ); + + return true; + }//end releaseAdvisoryLock() + /** * Find an object and get its register/schema context. * @@ -234,15 +347,18 @@ private function callerMayUnlock(ObjectEntity $object): bool * @param string $identifier Object ID or UUID * @param string|null $process Process ID (for tracking who locked it) * @param int|null $duration Lock duration in seconds + * @param bool $advisory When true, skip the object lookup and take the + * appConfig-backed advisory lock directly (used by + * pre-creation guards where no object exists yet) * * @return array Lock result with locked details and uuid. * * @throws LockedException If object is already locked. * @throws \Exception If lock operation fails. * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-59 + * @spec openspec/specs/object-interactions/spec.md */ - public function lock(string $identifier, ?string $process=null, ?int $duration=null): array + public function lock(string $identifier, ?string $process=null, ?int $duration=null, bool $advisory=false): array { $this->logger->debug( message: '[LockHandler] Locking object', @@ -252,12 +368,36 @@ public function lock(string $identifier, ?string $process=null, ?int $duration=n 'identifier' => $identifier, 'process' => $process, 'duration' => $duration, + 'advisory' => $advisory, ] ); + // Advisory (pre-creation) fast-path: the caller knows the identifier is a + // synthetic key that does NOT resolve to a stored object (e.g. openbuild's + // `createApp:` guard). Go straight to the appConfig-backed advisory + // lock and skip findObjectWithContext, whose findAcrossAllMagicTables scan + // would otherwise query every magic table just to conclude "not found". + if ($advisory === true) { + return $this->acquireAdvisoryLock(identifier: $identifier, process: $process, duration: $duration); + } + try { // Find the object and its register/schema context. - $context = $this->findObjectWithContext(identifier: $identifier); + try { + $context = $this->findObjectWithContext(identifier: $identifier); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + // Pre-creation / advisory lock: the identifier does not resolve + // to a stored object (e.g. the openbuild wizard locks + // `createApp:` before the object exists). Fall back to an + // advisory lock keyed by the arbitrary string so create-then-store + // flows work instead of failing with a 404/422. + return $this->acquireAdvisoryLock( + identifier: $identifier, + process: $process, + duration: $duration + ); + } + $objectBefore = $context['object']; // Use MagicMapper for lock operation. @@ -318,20 +458,29 @@ public function lock(string $identifier, ?string $process=null, ?int $duration=n * Removes the lock from an object, allowing other processes to modify it. * * @param string $identifier Object ID or UUID + * @param bool $advisory When true, release the appConfig-backed advisory lock + * directly and skip the object lookup / all-tables scan * * @return true True if unlocked successfully * * @throws \Exception If unlock operation fails * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-59 + * @spec openspec/specs/object-interactions/spec.md */ - public function unlock(string $identifier): bool + public function unlock(string $identifier, bool $advisory=false): bool { $this->logger->debug( message: '[LockHandler] Unlocking object', - context: ['file' => __FILE__, 'line' => __LINE__, 'identifier' => $identifier] + context: ['file' => __FILE__, 'line' => __LINE__, 'identifier' => $identifier, 'advisory' => $advisory] ); + // Advisory (pre-creation) fast-path — mirror of lock(): release the + // appConfig-backed advisory lock directly and skip the all-tables scan. + if ($advisory === true) { + $this->releaseAdvisoryLock(identifier: $identifier); + return true; + } + try { // Find the object and its register/schema context. // @@ -343,9 +492,28 @@ public function unlock(string $identifier): bool // releasing their own lock; the explicit `callerMayUnlock` // gate replaces the wave-3 C14 "any authenticated user can // unlock anything" behavior. - $context = $this->findObjectWithContext(identifier: $identifier, _rbacBypass: true); + try { + $context = $this->findObjectWithContext(identifier: $identifier, _rbacBypass: true); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + // Pre-creation / advisory lock release: identifier never + // resolved to a stored object. Clear any advisory lock held + // under this arbitrary key (no-op if none present). + $this->releaseAdvisoryLock(identifier: $identifier); + return true; + } + $objectBefore = $context['object']; + // No-op when the object isn't actually locked. Releasing a lock that + // does not exist must not require unlock permission: an empty or expired + // `_locked` means there is nothing to authorize. This keeps unlock + // idempotent and prevents spurious "permission to unlock" failures on + // flows that defensively unlock after a successful write (e.g. the + // object update endpoint's post-save unlock). See openregister#195. + if ($objectBefore->isLocked() === false) { + return true; + } + if ($this->callerMayUnlock(object: $objectBefore) === false) { $this->logger->warning( message: '[LockHandler] Unauthorized unlock attempt', @@ -399,7 +567,7 @@ public function unlock(string $identifier): bool * * @return bool True if locked, false otherwise * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-12 + * @spec openspec/specs/object-lifecycle/spec.md */ public function isLocked(string $identifier): bool { @@ -437,7 +605,7 @@ public function isLocked(string $identifier): bool * * @return array|null Lock info array or null if not locked. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-12 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getLockInfo(string $identifier): array|null { diff --git a/lib/Service/Object/MergeHandler.php b/lib/Service/Object/MergeHandler.php index 90f255baca..3921fd4c72 100644 --- a/lib/Service/Object/MergeHandler.php +++ b/lib/Service/Object/MergeHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -90,7 +90,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) Multiple merge paths for different data types and actions * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive merge requires handling all object aspects * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-13 + * @spec openspec/specs/object-lifecycle/spec.md */ public function mergeObjects(string $sourceObjectId, array $mergeData): array { diff --git a/lib/Service/Object/MetadataHandler.php b/lib/Service/Object/MetadataHandler.php index 563fc74fba..09ef31a728 100644 --- a/lib/Service/Object/MetadataHandler.php +++ b/lib/Service/Object/MetadataHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -53,7 +53,7 @@ class MetadataHandler * @psalm-return mixed * @phpstan-return mixed * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getValueFromPath(array $data, string $path): mixed { @@ -87,7 +87,7 @@ public function getValueFromPath(array $data, string $path): mixed * @psalm-return string|null * @phpstan-return string|null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function generateSlugFromValue(string $value): string|null { @@ -123,7 +123,7 @@ public function generateSlugFromValue(string $value): string|null * @psalm-return string * @phpstan-return string * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createSlugHelper(string $text): string { diff --git a/lib/Service/Object/MigrationHandler.php b/lib/Service/Object/MigrationHandler.php index 9e9ad2f07b..32f5997288 100644 --- a/lib/Service/Object/MigrationHandler.php +++ b/lib/Service/Object/MigrationHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -58,7 +58,7 @@ class MigrationHandler * @param DataManipulationHandler $dataManipHandler Handler for data manipulation. * @param LoggerInterface $logger Logger for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly MagicMapper $objectMapper, @@ -93,7 +93,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) Multiple migration paths for different object types * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive migration requires detailed processing * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function migrateObjects( string|int $sourceRegister, diff --git a/lib/Service/Object/PerformanceHandler.php b/lib/Service/Object/PerformanceHandler.php index 9c4d994fc2..3698ed88ec 100644 --- a/lib/Service/Object/PerformanceHandler.php +++ b/lib/Service/Object/PerformanceHandler.php @@ -53,7 +53,7 @@ class PerformanceHandler * @param CacheHandler $objectCacheService Object cache service for caching. * @param LoggerInterface $logger Logger for performance monitoring. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly CacheHandler $objectCacheService, @@ -74,7 +74,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function optimizeRequestForPerformance(array &$query, array &$perfTimings): void { @@ -121,7 +121,7 @@ public function optimizeRequestForPerformance(array &$query, array &$perfTimings * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple criteria for determining request simplicity * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function isSimpleRequest(array $query): bool { @@ -166,7 +166,7 @@ public function isSimpleRequest(array $query): bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function optimizeExtendQueries(array | string $extend): array { @@ -197,7 +197,7 @@ public function optimizeExtendQueries(array | string $extend): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function optimizeExtendInQuery(array &$query): void { @@ -238,7 +238,7 @@ private function optimizeExtendInQuery(array &$query): void * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function preloadCriticalEntities(array $query): void { @@ -268,7 +268,7 @@ public function preloadCriticalEntities(array $query): void * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control optional extraction features * @SuppressWarnings(PHPMD.NPathComplexity) Multiple paths for different data types * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function extractRelatedData(array $results, bool $includeRelated, bool $includeRelatedNames): array { @@ -342,7 +342,7 @@ public function extractRelatedData(array $results, bool $includeRelated, bool $i * * @return bool True if the value is a UUID string * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ private function isUuid(mixed $value): bool { @@ -369,24 +369,6 @@ private function isUuid(mixed $value): bool return false; }//end isUuid() - /** - * Get cached entities or use fallback function - * - * Attempts to retrieve entities from cache, falling back to the provided function if not cached. - * - * @param mixed $ids Entity ID(s) to retrieve (int, array, or 'all'). - * @param callable $fallbackFunc Function to call if not in cache. - * - * @return array Array of entities - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 - */ - public function getCachedEntities(mixed $ids, callable $fallbackFunc): array - { - // Entity caching is disabled - always use fallback function. - return call_user_func($fallbackFunc, $ids); - }//end getCachedEntities() - /** * Get facet count from query parameters * @@ -397,7 +379,7 @@ public function getCachedEntities(mixed $ids, callable $fallbackFunc): array * * @psalm-return int<0, max> * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFacetCount(bool $hasFacets, array $query): int { @@ -422,7 +404,7 @@ public function getFacetCount(bool $hasFacets, array $query): int * * @return int Total pages * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function calculateTotalPages(int $total, int $limit): int { @@ -444,7 +426,7 @@ public function calculateTotalPages(int $total, int $limit): int * * @psalm-return int<0, max> * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function calculateExtendCount(mixed $extend): int { diff --git a/lib/Service/Object/PerformanceOptimizationHandler.php b/lib/Service/Object/PerformanceOptimizationHandler.php index c56cfb3cfe..db2ac9f46a 100644 --- a/lib/Service/Object/PerformanceOptimizationHandler.php +++ b/lib/Service/Object/PerformanceOptimizationHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -45,7 +45,7 @@ class PerformanceOptimizationHandler * @param OrganisationService $organisationService Organisation service for context. * @param LoggerInterface $logger Logger for debugging. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly OrganisationService $organisationService, @@ -64,7 +64,7 @@ public function __construct( * @psalm-return string|null * @phpstan-return string|null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getActiveOrganisationForContext(): ?string { @@ -115,7 +115,7 @@ public function getActiveOrganisationForContext(): ?string * @SuppressWarnings(PHPMD.NPathComplexity) Different timing scenarios generate different recommendations * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive recommendations require detailed analysis * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getPerformanceRecommendations(float $totalTime, array $perfTimings, array $query): array { diff --git a/lib/Service/Object/PermissionHandler.php b/lib/Service/Object/PermissionHandler.php index 89cb98a177..64a34225f9 100644 --- a/lib/Service/Object/PermissionHandler.php +++ b/lib/Service/Object/PermissionHandler.php @@ -21,9 +21,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-55 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-56 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-57 + * @spec openspec/specs/rbac-scopes/spec.md#requirement-scope-model-hierarchy-register-schema-object-property + * @spec openspec/specs/rbac-scopes/spec.md#requirement-register-level-authorization-cascade + * @spec openspec/specs/rbac-scopes/spec.md#requirement-named-role-definitions-on-registers */ declare(strict_types=1); @@ -38,8 +38,12 @@ use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Db\MagicMapper; use OCA\OpenRegister\Event\CustomScopeEvaluatedEvent; +use OCA\OpenRegister\Exception\AuthorizationUnresolvableException; use OCA\OpenRegister\Event\CustomScopeEvaluatingEvent; +use OCA\OpenRegister\Exception\NotAuthorizedException; use OCA\OpenRegister\Service\ConditionMatcher; +use OCA\OpenRegister\Service\SystemOperationContext; +use OCP\IAppConfig; use OCP\IUserSession; use OCP\IUserManager; use OCP\IGroupManager; @@ -84,6 +88,13 @@ class PermissionHandler */ private array $cachedRegisterAuth = []; + /** + * Per-request cache of the resolved inheritFromPublic value, keyed by schema id. + * + * @var array + */ + private array $cachedInheritFromPublic = []; + /** * Per-request cache for register configuration (roles). * @@ -156,11 +167,12 @@ class PermissionHandler * @param SchemaMapper $schemaMapper Mapper for schema operations. * @param MagicMapper $objectEntityMapper Mapper for object entity operations. * @param ConditionMatcher $conditionMatcher Shared PHP-side match evaluator (ADR-011). + * @param IAppConfig $appConfig App config for the inheritFromPublic tenant default. * @param LoggerInterface $logger Logger for permission auditing. * @param ContainerInterface $container Container for lazy loading services. * @param \OCP\EventDispatcher\IEventDispatcher|null $eventDispatcher Optional dispatcher for custom-scope events. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function __construct( private readonly IUserSession $userSession, @@ -169,6 +181,7 @@ public function __construct( private readonly SchemaMapper $schemaMapper, private readonly MagicMapper $objectEntityMapper, private readonly ConditionMatcher $conditionMatcher, + private readonly IAppConfig $appConfig, private readonly LoggerInterface $logger, private readonly ContainerInterface $container, private readonly ?\OCP\EventDispatcher\IEventDispatcher $eventDispatcher=null @@ -182,10 +195,15 @@ public function __construct( * - Admin group always has all permissions * - Object owner always has all permissions for their specific objects * - If no authorization configured, all users have all permissions + * - If authorization is configured but the action is not granted, access is + * denied (fail-closed) * - Otherwise, check if user's groups match the required groups for the action * - * TODO: Implement property-level RBAC checks - * Properties can have their own authorization arrays that provide fine-grained access control. + * Property-level RBAC (fine-grained per-property authorization arrays, plus + * `writeOnly` secret stripping) is implemented in + * {@see \OCA\OpenRegister\Service\PropertyRbacHandler} and applied on the + * read path by {@see \OCA\OpenRegister\Service\Object\RenderObject::renderEntity()}. + * This handler owns object/schema-level RBAC only. * * @param Schema $schema The schema to check permissions for. * @param string $action The CRUD action (create, read, update, delete). @@ -202,7 +220,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) User/group/owner permission combinations create many paths * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC flag follows established API patterns * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function hasPermission( Schema $schema, @@ -217,6 +235,15 @@ public function hasPermission( return true; } + // Explicitly-scoped system operations (config imports at app boot, + // repair steps, webcron jobs run via ObjectService::runAsSystem()) + // are trusted the same way CLI cron already is: these run without a + // user session, and the anonymous fail-closed policy (#1955) would + // otherwise deny the app maintaining its own objects on every boot. + if (SystemOperationContext::isActive() === true) { + return true; + } + // Build a per-request cache key. The object UUID (when present) is part // of the key so conditional rules with `match` clauses are still // re-evaluated per object — the cache only deduplicates repeated calls @@ -371,6 +398,8 @@ private function schemaHasMatchRule(Schema $schema): bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) RBAC permission checks require multiple conditional paths * @SuppressWarnings(PHPMD.NPathComplexity) User/group/owner permission combinations create many paths + * + * @spec openspec/specs/rbac-zaaktype/spec.md */ private function evaluatePermission( Schema $schema, @@ -389,7 +418,26 @@ private function evaluatePermission( $objectOrganisation = $object->getOrganisation(); } - $authorization = $this->resolveAuthorization(schema: $schema); + // Fail-closed: when the effective authorization cannot be resolved we DENY. + // Previously the resolver swallowed the error and returned null, which this + // method's `empty($authorization)` treatment read as "no rules configured" + // and granted every action to every caller (CWE-863). + try { + $authorization = $this->resolveAuthorization(schema: $schema); + } catch (AuthorizationUnresolvableException $e) { + $this->logger->error( + message: '[PermissionHandler] Authorization unresolvable; denying action (fail-closed)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schema->getId(), + 'action' => $action, + 'userId' => $userId, + 'error' => $e->getMessage(), + ] + ); + return false; + } // Get current user if not provided. if ($userId === null) { @@ -447,6 +495,25 @@ private function evaluatePermission( return true; } + // 'authenticated' pseudo-group: any logged-in user qualifies, + // independent of real group membership (so a user with NO groups still + // matches). Evaluated here — symmetric with the SQL-layer + // MagicRbacHandler, which already honours 'authenticated' — so a + // single-object create/read/update/delete check and a list query agree + // on schemas that grant an action to `authenticated`. + if ($this->hasGroupPermission( + authorization: $authorization, + groupId: 'authenticated', + action: $action, + userId: $userId, + objectOwner: $objectOwner, + objectData: $objectData, + objectOrganisation: $objectOrganisation + ) === true + ) { + return true; + } + // Custom action verbs (anything outside the canonical 5) are // routed through a listener-driven dispatch so consuming apps // can contribute verdicts for verbs they own (e.g. ZGW @@ -469,6 +536,24 @@ private function evaluatePermission( } } + // User-level override (delegation) check — evaluated independently of + // group membership so a user with NO groups can still receive a + // delegated grant. Uses the sentinel group id '' which never matches a + // real group; only the user-override branch inside hasGroupPermission + // can grant here. Additive and fail-closed (see userOverrideMatches()). + if ($this->hasGroupPermission( + authorization: $authorization, + groupId: '', + action: $action, + userId: $userId, + objectOwner: $objectOwner, + objectData: $objectData, + objectOrganisation: $objectOrganisation + ) === true + ) { + return true; + } + // Check schema permissions for each user group. foreach ($userGroups as $groupId) { if ($this->hasGroupPermission( @@ -485,8 +570,13 @@ private function evaluatePermission( } }//end foreach - // Logged-in users should also have at least the same rights as 'public' users. - if ($this->hasGroupPermission( + // Logged-in users normally also inherit at least the same rights as + // 'public' users — unless the schema/register/tenant disabled public + // inheritance for authenticated users (inheritFromPublic = false). The + // flag only governs this authenticated-inherits-public step; anonymous + // users are handled earlier and are never affected. + if ($this->resolveInheritFromPublic(schema: $schema) === true + && $this->hasGroupPermission( authorization: $authorization, groupId: 'public', action: $action, @@ -637,13 +727,35 @@ private function dispatchCustomScopeEvaluated( * * @return void * - * @spec openspec/changes/rbac-scopes/specs/rbac-scopes/spec.md#requirement-scope-caching-for-performance + * @spec openspec/specs/rbac-scopes/spec.md#requirement-scope-caching-for-performance */ public function clearPermissionCache(): void { $this->permissionCache = []; }//end clearPermissionCache() + /** + * Clear the per-request inheritFromPublic resolution cache. + * + * The cache keys purely on schema id, which is collision-free only while + * schema/register/IAppConfig authorization is immutable mid-request. Any + * path that mutates authorization and then re-reads it within the same + * request must bust this cache to avoid serving a stale verdict. + * + * @param int|null $schemaId Specific schema to evict, or null to clear all. + * + * @return void + */ + public function clearInheritFromPublicCache(?int $schemaId=null): void + { + if ($schemaId === null) { + $this->cachedInheritFromPublic = []; + return; + } + + unset($this->cachedInheritFromPublic[$schemaId]); + }//end clearInheritFromPublicCache() + /** * Check permission and throw exception if not granted * @@ -660,7 +772,7 @@ public function clearPermissionCache(): void * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC flag follows established API patterns * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function checkPermission( Schema $schema, @@ -685,8 +797,13 @@ public function checkPermission( $userName = $user->getDisplayName(); } - throw new Exception( - "User '{$userName}' does not have permission to '{$action}' objects in schema '{$schema->getTitle()}'" + // Use NotAuthorizedException (HTTP 403) rather than a generic + // Exception so controllers map an RBAC denial to a 403 Forbidden + // response instead of leaking a 500 Internal Server Error. The + // class extends \Exception, so any existing `catch (Exception)` + // call sites remain backward-compatible. + throw new NotAuthorizedException( + message: "User '{$userName}' does not have permission to '{$action}' objects in schema '{$schema->getTitle()}'" ); } }//end checkPermission() @@ -708,7 +825,7 @@ public function checkPermission( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Permission filtering requires multiple conditional checks * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC/multitenancy flags follow established API patterns * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function filterObjectsForPermissions(array $objects, bool $_rbac, bool $_multitenancy): array { @@ -732,8 +849,9 @@ public function filterObjectsForPermissions(array $objects, bool $_rbac, bool $_ if ($objectSchema !== null) { try { $schema = $this->schemaMapper->find($objectSchema); - // TODO: Add property-level RBAC check for 'create' action here. - // Check individual property permissions before allowing property values to be set. + // Property-level RBAC (per-property authorization + writeOnly + // stripping) is enforced on the read path by PropertyRbacHandler + // via RenderObject; this loop only decides object-level visibility. if ($this->hasPermission( schema: $schema, action: 'create', @@ -784,7 +902,7 @@ public function filterObjectsForPermissions(array $objects, bool $_rbac, bool $_ * @SuppressWarnings(PHPMD.CyclomaticComplexity) UUID filtering with permission checks requires multiple conditions * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC/multitenancy flags follow established API patterns * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function filterUuidsForPermissions(array $uuids, bool $_rbac, bool $_multitenancy): array { @@ -812,8 +930,9 @@ public function filterUuidsForPermissions(array $uuids, bool $_rbac, bool $_mult try { $schema = $this->schemaMapper->find($objectSchema); - // TODO: Add property-level RBAC check for 'delete' action here - // Check if user has permission to delete objects with specific property values. + // Property-level RBAC (per-property authorization + writeOnly + // stripping) lives in PropertyRbacHandler on the read path; this + // loop only decides object-level visibility. if ($this->hasPermission( schema: $schema, action: 'delete', @@ -855,7 +974,7 @@ public function filterUuidsForPermissions(array $uuids, bool $_rbac, bool $_mult * * @return string|null The active organisation UUID or null if none set * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function getActiveOrganisationForContext(): ?string { @@ -898,7 +1017,8 @@ public function getActiveOrganisationForContext(): ?string * - Admin group always has all permissions * - Object owner always has all permissions for their specific objects * - If no authorization is set, everyone has permission - * - If authorization is set but action is not specified, everyone has permission + * - If authorization is set but the action is not listed, no one has + * permission (fail-closed) — except the admin/owner bypasses above * * Deduplication note: * Conditional match evaluation (rules with a `match` clause) is delegated to @@ -923,7 +1043,7 @@ public function getActiveOrganisationForContext(): ?string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Rule entries: string, object, conditional, or nested group - each type is a distinct RBAC branch * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function hasGroupPermission( ?array $authorization, @@ -950,13 +1070,33 @@ public function hasGroupPermission( return true; } - // If action is not specified in authorization, everyone has permission. - if (isset($authorization[$action]) === false) { - return true; + // Fail-closed: once a schema opts into authorization (non-empty block), + // an action that is not explicitly listed is denied — including for the + // `public`/unauthenticated pseudo-group. Only the empty-block default + // above still grants by default; the admin/owner bypasses precede this. + if (empty($authorization[$action]) === true) { + return false; } // Check each authorization entry for this action. foreach ($authorization[$action] as $entry) { + // User-level override entry (delegation): a bare string `user:` + // or a complex entry `{ "user": "", "match": {...} }` grants the + // action to that one user independent of group membership. This is the + // zaaktype-scoped DELEGATION primitive (rbac-zaaktype): it is purely + // ADDITIVE — it can only WIDEN access for the named user and never + // affects group rules. Fail-closed: only an exact uid match is honoured. + if ($this->userOverrideMatches( + entry: $entry, + action: $action, + userId: $userId, + objectData: $objectData, + objectOrganisation: $objectOrganisation + ) === true + ) { + return true; + } + // Simple string entry: direct group match. if (is_string($entry) === true) { if ($entry === $groupId) { @@ -1001,6 +1141,103 @@ public function hasGroupPermission( return false; }//end hasGroupPermission() + /** + * Determine whether an authorization entry is a user-level override that + * grants the current user the requested action. + * + * User-level overrides implement zaaktype-scoped DELEGATION (rbac-zaaktype): + * a permission granted to an individual user independent of group membership, + * used for external advisors, temporary replacements, and escalation paths. + * They are expressed inside the same schema `authorization[$action]` list as + * either: + * - a bare string `"user:"`; or + * - a complex entry `{ "user": "", "match": { ... } }` whose optional + * `match` clause is evaluated by the shared {@see ConditionMatcher} (so an + * expiry can be encoded as e.g. `{ "_expires": { "$gt": "$now" } }` on the + * object, or any other object-data predicate). + * + * SECURITY — fail closed and never widen group access: + * - Only an EXACT uid match grants. A missing/blank uid, a non-matching uid, + * or a malformed entry returns false. + * - Anonymous principals (userId === null) can never match a user override. + * - The override is purely additive: returning false here lets the normal + * group/owner/public rule chain decide, so an override can only ADD access + * for the named user and can never remove a group's existing grant. + * + * @param mixed $entry A single authorization entry for the action. + * @param string $action The CRUD action being checked (for match-create filtering). + * @param string|null $userId The current user ID (null = anonymous). + * @param array|null $objectData Optional object data for conditional matching. + * @param string|null $objectOrganisation Optional object organisation (folded into @self.organisation). + * + * @return bool True only when the entry is a user override for the current user + * and any attached match clause is satisfied. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + private function userOverrideMatches( + mixed $entry, + string $action, + ?string $userId, + ?array $objectData=null, + ?string $objectOrganisation=null + ): bool { + // Anonymous principals can never match a user-level override. + if ($userId === null || $userId === '') { + return false; + } + + $targetUid = null; + $match = null; + + // Bare string form: "user:". + if (is_string($entry) === true) { + if (str_starts_with($entry, 'user:') === false) { + return false; + } + + $targetUid = substr($entry, strlen('user:')); + } else if (is_array($entry) === true && isset($entry['user']) === true && is_string($entry['user']) === true) { + // Complex form: { "user": "", "match": {...} }. + $targetUid = $entry['user']; + $match = ($entry['match'] ?? null); + } else { + return false; + } + + // Exact uid match required (fail closed on blank / mismatch). + if ($targetUid === '' || $targetUid !== $userId) { + return false; + } + + // No match clause: the user override alone is sufficient. + if ($match === null || is_array($match) === false || empty($match) === true) { + return true; + } + + // On create there is no existing object to match organisation against; + // strip organisation predicates exactly as the group path relies on + // ConditionMatcher to do elsewhere. + if ($action === 'create') { + $match = $this->conditionMatcher->filterOrganisationMatchForCreate(match: $match); + if ($match === []) { + return true; + } + } + + // Evaluate the match clause via the shared ConditionMatcher (ADR-011) — + // same envelope construction as the group path so _organisation resolves. + $envelope = ($objectData ?? []); + if ($objectOrganisation !== null) { + $envelope['@self'] = (($envelope['@self'] ?? []) + ['organisation' => $objectOrganisation]); + } + + return $this->conditionMatcher->objectMatchesConditions( + object: $envelope, + match: $match + ); + }//end userOverrideMatches() + /** * Determine whether the `public` group is EXPLICITLY granted an action. * @@ -1050,7 +1287,7 @@ private function publicGroupExplicitlyGranted(?array $authorization, string $act * * @return array Array of group IDs that have permission, or empty array if all groups have permission * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function getAuthorizedGroups(?array $authorization, string $action): array { @@ -1129,8 +1366,24 @@ public function getReadableByUsers(ObjectEntity $object): array return []; }//end try - $authorization = $this->resolveAuthorization(schema: $schema); - $groupIds = $this->resolveReadGroupIds(authorization: $authorization); + // Fail-closed: an unresolvable authorization must not broadcast. Returning + // [] means "no targeted user list", which is the safe outcome here. + try { + $authorization = $this->resolveAuthorization(schema: $schema); + } catch (AuthorizationUnresolvableException $e) { + $this->logger->error( + message: '[PermissionHandler] Authorization unresolvable; no readable users resolved (fail-closed)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schema->getId(), + 'error' => $e->getMessage(), + ] + ); + return []; + } + + $groupIds = $this->resolveReadGroupIds(authorization: $authorization); // Null means "open / broadcast" — no targeted user list. if ($groupIds === null) { @@ -1250,7 +1503,13 @@ private function collectUsersFromGroups(array $groupIds): array * * @return array|null The effective authorization array, or null if none configured. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * Returns null when NO authorization is configured anywhere in the cascade + * (callers treat that as open). It THROWS when the cascade could not be + * evaluated — callers MUST treat that as deny, never as "no rules". + * + * @throws AuthorizationUnresolvableException When the register cascade cannot be resolved. Callers MUST deny. + * + * @spec openspec/specs/authorization-rbac/spec.md#requirement-authorization-resolution-fails-closed */ public function resolveAuthorization(Schema $schema): ?array { @@ -1275,6 +1534,197 @@ public function resolveAuthorization(Schema $schema): ?array return null; }//end resolveAuthorization() + /** + * Resolve whether authenticated users inherit `public` group rights for a schema. + * + * Cascade (first explicit boolean wins): schema authorization → + * register authorization → tenant-wide IAppConfig + * `openregister.rbac.inherit_from_public_default` → hard-coded `true`. + * A `null` (or any non-boolean) value at a level is treated as "unset" so + * the cascade falls through. Anonymous users are never affected by this flag. + * + * @param Schema $schema The schema being evaluated. + * + * @return bool True when authenticated users inherit `public` rights. + * + * @spec openspec/changes/rbac-disable-public-inheritance/tasks.md + */ + public function resolveInheritFromPublic(Schema $schema): bool + { + $schemaId = $schema->getId(); + if ($schemaId !== null && array_key_exists($schemaId, $this->cachedInheritFromPublic) === true) { + return $this->cachedInheritFromPublic[$schemaId]; + } + + // Resolve defensively: any unexpected failure falls back to the + // tenant-wide default rather than propagating. This keeps the fail-mode + // symmetric with MagicRbacHandler::authenticatedInheritsPublic() — both + // callers degrade to the configured tenant posture (not a hard-coded + // grant), so a cluster-wide lock-down is not silently softened by an + // exception on one of the two RBAC code paths. + try { + $resolved = null; + + // Step 1: schema-level authorization (strict boolean; non-bool = unset). + $auth = $schema->getAuthorization(); + if (is_array($auth) === true && array_key_exists('inheritFromPublic', $auth) === true) { + $resolved = $this->coerceStrictBoolOrLog(value: $auth['inheritFromPublic'], level: 'schema', schemaId: $schemaId); + } + + // Step 2: register-level authorization. Conservative across multiple + // registers — see resolveRegisterInheritFromPublic(). + if ($resolved === null) { + $resolved = $this->resolveRegisterInheritFromPublic(schemaId: $schemaId); + } + + // Step 3: tenant-wide IAppConfig default. + if ($resolved === null) { + $resolved = $this->inheritFromPublicTenantDefault(); + } + } catch (\Throwable $e) { + $resolved = $this->inheritFromPublicTenantDefault(); + $this->logger->error( + message: '[PermissionHandler] inheritFromPublic resolution failed; falling back to tenant default', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schemaId, + 'default' => $resolved, + 'error' => $e->getMessage(), + ] + ); + }//end try + + if ($schemaId !== null) { + $this->cachedInheritFromPublic[$schemaId] = $resolved; + } + + return $resolved; + }//end resolveInheritFromPublic() + + /** + * Read the tenant-wide inheritFromPublic default from IAppConfig. + * + * This is both the terminal cascade step and the fail-safe used when + * resolution errors out. When IAppConfig itself is unreachable it logs at + * error level and returns `true` (pre-PR posture) so a config-store outage + * does not hard-deny every authenticated read. + * + * @return bool The configured tenant default (true when unconfigured/unreachable). + */ + public function inheritFromPublicTenantDefault(): bool + { + try { + // IAppConfig tolerates string forms ("true"/"1") here. + return $this->appConfig->getValueBool(app: 'openregister', key: 'rbac.inherit_from_public_default', default: true); + } catch (\Throwable $e) { + $this->logger->error( + message: '[PermissionHandler] IAppConfig unreachable resolving inheritFromPublic default; assuming true', + context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] + ); + return true; + } + }//end inheritFromPublicTenantDefault() + + /** + * Resolve the register-level inheritFromPublic value, conservatively. + * + * A schema can belong to multiple registers. Rather than depending on the + * undefined scan order of "first register wins" — which would make a + * security verdict non-deterministic across nodes/restores — every register + * containing the schema is consulted and the most-restrictive explicit value + * wins: a single `false` disables inheritance even if other registers say + * `true`. Returns null when no register sets the flag (cascade falls through). + * + * @param int|null $schemaId The schema id (null when the schema is unsaved). + * + * @return bool|null false if any register disables inheritance, true if some + * enable it and none disable, null when no register sets it. + */ + private function resolveRegisterInheritFromPublic(?int $schemaId): ?bool + { + if ($schemaId === null) { + return null; + } + + $registerIds = []; + try { + $registerMapper = $this->container->get(RegisterMapper::class); + $registerIds = $registerMapper->getAllRegisterIdsWithSchema(schemaId: $schemaId); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[PermissionHandler] Failed to list registers for schema; skipping register cascade', + context: ['file' => __FILE__, 'line' => __LINE__, 'schemaId' => $schemaId, 'error' => $e->getMessage()] + ); + return null; + } + + $sawTrue = false; + foreach ($registerIds as $registerId) { + try { + $registerAuth = $this->getRegisterAuthorization(registerId: $registerId); + } catch (AuthorizationUnresolvableException $e) { + // Fail-closed: most-restrictive-wins, so an unresolvable register + // disables `public` inheritance rather than silently skipping it. + return false; + } + + if (is_array($registerAuth) === false || array_key_exists('inheritFromPublic', $registerAuth) === false) { + continue; + } + + $value = $this->coerceStrictBoolOrLog(value: $registerAuth['inheritFromPublic'], level: 'register', schemaId: $schemaId); + if ($value === false) { + // Most-restrictive wins: one explicit false disables inheritance. + return false; + } + + if ($value === true) { + $sawTrue = true; + } + }//end foreach + + if ($sawTrue === true) { + return true; + } + + return null; + }//end resolveRegisterInheritFromPublic() + + /** + * Strictly coerce an inheritFromPublic cascade value to bool, or null when unset. + * + * Only a literal `true`/`false` counts. Anything else (string "false", + * int 0, etc.) is treated as "unset" and logged — PHP's loose `(bool) + * "false" === true` would otherwise silently invert the gate on a + * seed/migration/CLI write that bypasses the schema validator. + * + * @param mixed $value The stored value. + * @param string $level The cascade level ('schema' | 'register'), for logging. + * @param int|null $schemaId The schema id, for logging. + * + * @return bool|null The boolean value, or null when the value is not a real boolean. + */ + private function coerceStrictBoolOrLog(mixed $value, string $level, ?int $schemaId): ?bool + { + if ($value === true || $value === false) { + return $value; + } + + if ($value !== null) { + $this->logger->warning( + message: '[PermissionHandler] Non-boolean inheritFromPublic value ignored; treating as unset', + context: [ + 'level' => $level, + 'schemaId' => $schemaId, + 'type' => get_debug_type($value), + ] + ); + } + + return null; + }//end coerceStrictBoolOrLog() + /** * Get the parent register for a schema. * @@ -1285,7 +1735,7 @@ public function resolveAuthorization(Schema $schema): ?array * * @return Register|null The parent register, or null if not found. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-55 + * @spec openspec/specs/rbac-scopes/spec.md#requirement-scope-model-hierarchy-register-schema-object-property */ private function getRegisterForSchema(Schema $schema): ?Register { @@ -1298,8 +1748,13 @@ private function getRegisterForSchema(Schema $schema): ?Register return $registerMapper->find($registerId); } catch (\Throwable $e) { - $this->logger->warning( - message: '[PermissionHandler] Failed to get register for schema', + // Fail-closed: this method logged but still returned null, and null here + // means "schema belongs to no register" -> open. Logging a fail-open does + // not make it safe. A genuine "no register" answer still returns null + // above (getFirstRegisterWithSchema() === null); only the ERROR path + // throws. + $this->logger->error( + message: '[PermissionHandler] Failed to get register for schema; denying access (fail-closed)', context: [ 'file' => __FILE__, 'line' => __LINE__, @@ -1307,8 +1762,12 @@ private function getRegisterForSchema(Schema $schema): ?Register 'error' => $e->getMessage(), ] ); - return null; - } + throw new AuthorizationUnresolvableException( + message: 'Unable to resolve register for schema '.$schema->getId(), + code: 0, + previous: $e + ); + }//end try }//end getRegisterForSchema() /** @@ -1317,11 +1776,21 @@ private function getRegisterForSchema(Schema $schema): ?Register * Caches the authorization array for each register ID to avoid * repeated database lookups within a single request. * + * Fails CLOSED (CWE-863): when the register cannot be resolved this throws + * rather than returning null. `null` from this method means "the register + * has no authorization configured" — a real answer that callers treat as + * open. A resolver error is NOT that answer, and must never be reported as + * one. The failure is also NOT cached: a transient mapper outage must not + * be frozen into a permanent per-request verdict. + * * @param int $registerId The register ID to get authorization for. * - * @return array|null The register's authorization array. + * @return array|null The register's authorization array, or null when the + * register genuinely has none configured. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-56 + * @throws AuthorizationUnresolvableException When the register's authorization cannot be determined. Callers MUST deny. + * + * @spec openspec/specs/authorization-rbac/spec.md#requirement-authorization-resolution-fails-closed */ private function getRegisterAuthorization(int $registerId): ?array { @@ -1339,9 +1808,27 @@ private function getRegisterAuthorization(int $registerId): ?array return $auth; } catch (\Throwable $e) { - $this->cachedRegisterAuth[$registerId] = null; - return null; - } + // Log — a sibling resolver (getRegisterForSchema) already logs on this + // exact shape; this one silently swallowed, so an authorization outage + // left no trace at all while granting full permissions. + $this->logger->error( + message: '[PermissionHandler] Failed to resolve register authorization; denying access (fail-closed)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'registerId' => $registerId, + 'error' => $e->getMessage(), + ] + ); + + // Deliberately NOT cached: caching the failure would turn a transient + // error into a permanent verdict for the rest of the request. + throw new AuthorizationUnresolvableException( + message: 'Unable to resolve authorization for register '.$registerId, + code: 0, + previous: $e + ); + }//end try }//end getRegisterAuthorization() /** @@ -1351,7 +1838,7 @@ private function getRegisterAuthorization(int $registerId): ?array * * @return array|null The register's configuration array. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-57 + * @spec openspec/specs/rbac-scopes/spec.md#requirement-named-role-definitions-on-registers */ private function getRegisterConfiguration(int $registerId): ?array { @@ -1359,8 +1846,14 @@ private function getRegisterConfiguration(int $registerId): ?array return $this->cachedRegisterConfig[$registerId]; } - // Calling getRegisterAuthorization populates both caches. - $this->getRegisterAuthorization(registerId: $registerId); + // Calling getRegisterAuthorization populates both caches. Configuration is + // not an authorization verdict, so an unresolvable register yields null + // ("unknown configuration") here — the resolver has already logged it. + try { + $this->getRegisterAuthorization(registerId: $registerId); + } catch (AuthorizationUnresolvableException $e) { + return null; + } return $this->cachedRegisterConfig[$registerId] ?? null; }//end getRegisterConfiguration() @@ -1386,7 +1879,7 @@ private function getRegisterConfiguration(int $registerId): ?array * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Role expansion: validate defs, resolve `extends` chains, merge action sets, guard malformed data * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-7 + * @spec openspec/specs/rbac-scopes/spec.md */ public function expandRoles(array $authorization, Schema $schema): array { @@ -1586,7 +2079,7 @@ private function collectRoleActions( * * @return array Array of role definitions, each with 'name', 'description', 'actions'. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-57 + * @spec openspec/specs/rbac-scopes/spec.md#requirement-named-role-definitions-on-registers */ private function getRoleDefinitionsForSchema(Schema $schema): array { @@ -1602,4 +2095,132 @@ private function getRoleDefinitionsForSchema(Schema $schema): array return $config['roles']; }//end getRoleDefinitionsForSchema() + + /** + * Decide whether a caller may perform a lifecycle transition that + * declares a per-transition `authorization` list. + * + * This is the declarative counterpart to the `requires` PHP-guard seam on + * `x-openregister-lifecycle` transitions: a schema author lists the + * Nextcloud group ids (and/or register-named roles) permitted to drive a + * given transition, and OpenRegister enforces it on the standard + * `saveObject()` path WITHOUT the author having to ship a guard class. + * + * Resolution rules (reusing the SAME `IGroupManager` membership check that + * every other RBAC verdict in this handler trusts): + * - The `admin` group always passes (mirrors {@see hasGroupPermission()}). + * - A bare string entry is a literal NC group id; the caller passes when + * {@see IGroupManager::isInGroup()} is true for it. + * - An entry shaped `{ "role": "" }` is expanded to the NC group ids + * assigned to that register-named role via the schema's + * `authorization.roles` map (the same `roles: {name: [groups]}` syntax + * {@see expandRoles()} consumes), then matched the same way. + * + * Fail-closed posture (CWE-863): an EMPTY list authorises nobody; an + * anonymous caller (null/unknown uid) is denied; an entry that cannot be + * resolved to a concrete group is skipped (never widens access). A + * transition WITHOUT an `authorization` list never reaches this method — + * the listener only calls it when the key is present — so existing + * transitions are completely unaffected (additive). + * + * @param array $authorizationList Group ids / `{role}` entries permitted to perform the transition. + * @param string|null $userId Caller uid, or null for an anonymous principal. + * @param Schema $schema Schema the transition belongs to (for named-role expansion). + * + * @return bool True when the caller is authorised; false (fail-closed) otherwise. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Anonymous guard + admin bypass + literal-vs-role entry handling are each one irreducible branch. + * + * @spec openspec/specs/object-lifecycle/spec.md#requirement-declarative-per-transition-authorization-gate + */ + public function isTransitionAuthorized(array $authorizationList, ?string $userId, Schema $schema): bool + { + // Fail-closed: an empty list authorises nobody. + if ($authorizationList === []) { + return false; + } + + // Anonymous callers can never satisfy a group-based transition gate. + if ($userId === null || $userId === '') { + return false; + } + + $userObj = $this->userManager->get($userId); + if ($userObj === null) { + return false; + } + + $userGroups = $this->groupManager->getUserGroupIds($userObj); + + // Admin bypass — consistent with every other verdict in this handler. + if (in_array(needle: 'admin', haystack: $userGroups, strict: true) === true) { + return true; + } + + $userGroupSet = array_flip($userGroups); + $roleGroupMap = null; + + foreach ($authorizationList as $entry) { + // Literal NC group id. + if (is_string($entry) === true) { + if ($entry !== '' && isset($userGroupSet[$entry]) === true) { + return true; + } + + continue; + } + + // Named-role indirection: { "role": "" } → assigned groups. + if (is_array($entry) === true && isset($entry['role']) === true && is_string($entry['role']) === true) { + if ($roleGroupMap === null) { + $roleGroupMap = $this->resolveTransitionRoleGroups(schema: $schema); + } + + $groupsForRole = ($roleGroupMap[$entry['role']] ?? []); + foreach ($groupsForRole as $groupId) { + if (is_string($groupId) === true && isset($userGroupSet[$groupId]) === true) { + return true; + } + } + } + }//end foreach + + return false; + }//end isTransitionAuthorized() + + /** + * Build a `roleName => [groupId, ...]` map from a schema's + * `authorization.roles` assignment, used to expand `{ "role": "" }` + * entries on lifecycle transitions. + * + * The role→group assignment lives on the schema's authorization block + * (`authorization.roles: { "": ["", ...] }`) — the same + * compact syntax {@see expandRoles()} reads. Only the assignment is needed + * here (not the register-level action set), so this is a thin, targeted + * read rather than a re-run of the full role-hierarchy expansion. + * + * @param Schema $schema Schema whose authorization block carries the role assignment. + * + * @return array> Map of role name to assigned NC group ids. + */ + private function resolveTransitionRoleGroups(Schema $schema): array + { + $authorization = $schema->getAuthorization(); + if (is_array($authorization) === false || isset($authorization['roles']) === false + || is_array($authorization['roles']) === false + ) { + return []; + } + + $map = []; + foreach ($authorization['roles'] as $roleName => $groups) { + if (is_string($roleName) === false) { + continue; + } + + $map[$roleName] = array_values(array_filter((array) $groups, 'is_string')); + } + + return $map; + }//end resolveTransitionRoleGroups() }//end class diff --git a/lib/Service/Object/QueryHandler.php b/lib/Service/Object/QueryHandler.php index 5ac9d527af..ace7085781 100644 --- a/lib/Service/Object/QueryHandler.php +++ b/lib/Service/Object/QueryHandler.php @@ -8,21 +8,20 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-view-based-search-composition */ namespace OCA\OpenRegister\Service\Object; use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\MagicMapper; -use OCA\OpenRegister\Service\IndexService; use OCA\OpenRegister\Service\Object\GetObject; use OCA\OpenRegister\Service\Object\RenderObject; use OCA\OpenRegister\Service\Object\SearchQueryHandler; @@ -60,6 +59,13 @@ */ class QueryHandler { + /** + * Hard maximum page size for list/search requests. A client-supplied + * `_limit` above this is clamped down, so an oversized request (e.g. + * `_limit=1000000`) cannot force an unbounded result load (DoS/OOM). + */ + public const MAX_PAGE_SIZE = 1000; + /** * Constructor for QueryHandler. * @@ -75,7 +81,7 @@ class QueryHandler * * @SuppressWarnings(PHPMD.ExcessiveParameterList) Nextcloud DI requires constructor injection * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function __construct( private readonly MagicMapper $objectMapper, @@ -109,7 +115,7 @@ public function __construct( * @psalm-return int * @phpstan-return int * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function countSearchObjects( array $query=[], @@ -159,7 +165,7 @@ public function countSearchObjects( * * @throws \OCP\DB\Exception If a database error occurs. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function searchObjects( array $query=[], @@ -270,7 +276,7 @@ public function searchObjects( * @psalm-return array * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function searchObjectsPaginated( array $query=[], @@ -289,34 +295,7 @@ public function searchObjectsPaginated( // Strip deprecated _source parameter (silently ignore for backward compatibility). unset($query['_source']); - // Use SOLR if enabled in config, unless relation-based search params are provided. - $hasIds = isset($query['_ids']) === true; - $hasUses = isset($query['_uses']) === true; - $hasIdsParam = $ids !== null; - $hasUsesParam = $uses !== null; - $isSolrEnabled = $this->searchQueryHandler->isSolrAvailable(); - - if ($isSolrEnabled === true - && $hasIdsParam === false && $hasUsesParam === false - && $hasIds === false && $hasUses === false - ) { - // Forward to Index service - let it handle availability checks and error handling. - $indexService = $this->container->get(IndexService::class); - $result = $indexService->searchObjects( - query: $query, - _rbac: $_rbac, - _multitenancy: $_multitenancy, - deleted: $deleted - ); - $result['@self']['source'] = 'index'; - $result['@self']['query'] = $query; - $result['@self']['rbac'] = $_rbac; - $result['@self']['multi'] = $_multitenancy; - $result['@self']['deleted'] = $deleted; - return $result; - } - - // Use database search. + // Use database search (the only search backend; the external Solr/index tier was removed). $result = $this->searchObjectsPaginatedDatabase( query: $query, _rbac: $_rbac, @@ -355,7 +334,7 @@ public function searchObjectsPaginated( * @psalm-return array * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-view-based-search-composition */ public function searchObjectsPaginatedDatabase( array $query=[], @@ -369,7 +348,8 @@ public function searchObjectsPaginatedDatabase( $metrics = []; // Extract pagination parameters (limit=0 is valid for count/facets-only requests). - $limit = max(0, (int) ($query['_limit'] ?? 20)); + // Clamp to MAX_PAGE_SIZE so an oversized `_limit` cannot force an unbounded load. + $limit = min(max(0, (int) ($query['_limit'] ?? 20)), self::MAX_PAGE_SIZE); $offset = $query['_offset'] ?? null; $page = $query['_page'] ?? null; @@ -494,6 +474,24 @@ function (string $item): bool { $filesStart = microtime(true); $this->renderHandler->attachLightweightFilesToRows(rows: $results); $metrics['lightweightFiles'] = round((microtime(true) - $filesStart) * 1000, 2); + + // Cheap path also skips renderEntity, which is where translatable + // properties get projected to the negotiated language. Without this, + // list rows return raw language-keyed maps (e.g. {"nl":...}) while the + // single-object read returns a projected string. Resolve them here so + // both paths agree. No-op for `?_translations=all` and for schemas with + // no translatable properties (both early-return in the handler). + $translateStart = microtime(true); + $this->renderHandler->resolveTranslationsForRows(rows: $results); + $metrics['translations'] = round((microtime(true) - $translateStart) * 1000, 2); + + // Cheap path also skips renderEntity, which is where write-only properties are + // redacted (openregister#380, ocon#147). Without this, a LIST read returns + // secrets that a single-object read would strip — and the OpenConnector leak + // was exactly a list read. Apply the same redaction to the raw rows here. + $redactStart = microtime(true); + $this->renderHandler->redactWriteOnlyFromRows(rows: $results, _rbac: $_rbac); + $metrics['writeOnlyRedaction'] = round((microtime(true) - $redactStart) * 1000, 2); }//end if // Calculate total pages (avoid division by zero when limit=0). diff --git a/lib/Service/Object/ReferentialIntegrityService.php b/lib/Service/Object/ReferentialIntegrityService.php index 94bde144b2..31f75bdad3 100644 --- a/lib/Service/Object/ReferentialIntegrityService.php +++ b/lib/Service/Object/ReferentialIntegrityService.php @@ -112,7 +112,7 @@ class ReferentialIntegrityService * @param LoggerInterface $logger Logger for debugging. * @param IDBConnection $db Database connection for raw SQL queries. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly SchemaMapper $schemaMapper, @@ -133,7 +133,7 @@ public function __construct( * * @return DeletionAnalysis The analysis result with targets and blockers. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function canDelete(ObjectEntity $object): DeletionAnalysis { @@ -168,7 +168,7 @@ public function canDelete(ObjectEntity $object): DeletionAnalysis * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple action types require distinct handling paths * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function applyDeletionActions( DeletionAnalysis $analysis, @@ -223,7 +223,8 @@ public function applyDeletionActions( cascadeTargets: $cascadeTargets, userId: $userId, cascadeSource: $cascadeSource, - triggerSchemaSlug: $triggerSchemaSlug + triggerSchemaSlug: $triggerSchemaSlug, + organisationId: $organisationId ); } }//end applyDeletionActions() @@ -241,7 +242,7 @@ public function applyDeletionActions( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function logRestrictBlock( string $objectUuid, @@ -284,7 +285,7 @@ public function logRestrictBlock( * * @return bool True if any schema has onDelete config referencing this schema. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function hasIncomingOnDeleteReferences(string $schemaId): bool { @@ -299,7 +300,7 @@ public function hasIncomingOnDeleteReferences(string $schemaId): bool * * @return bool True if the value is valid. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public static function isValidOnDeleteAction(string $value): bool { @@ -316,7 +317,7 @@ public static function isValidOnDeleteAction(string $value): bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) * Reduced from 19 to ~12 by extracting buildSchemaRegisterMap + indexRelationsForSchema. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function ensureRelationIndex(): void { @@ -357,7 +358,7 @@ private function ensureRelationIndex(): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function buildSchemaRegisterMap(): void { @@ -432,7 +433,7 @@ private function buildSchemaRegisterMap(): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function indexRelationsForSchema(\OCA\OpenRegister\Db\Schema $schema, array $allSchemas): void { @@ -481,7 +482,7 @@ private function indexRelationsForSchema(\OCA\OpenRegister\Db\Schema $schema, ar * * @return string|null The uppercase onDelete action, or null if not set. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function extractOnDelete(array $property): ?string { @@ -501,7 +502,7 @@ private function extractOnDelete(array $property): ?string * * @return string|null The $ref value, or null if not a relation property. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function extractTargetRef(array $property): ?string { @@ -530,7 +531,7 @@ private function extractTargetRef(array $property): ?string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple resolution strategies needed * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function resolveSchemaRef(string $ref, array $allSchemas): ?string { @@ -573,7 +574,7 @@ private function resolveSchemaRef(string $ref, array $allSchemas): ?string * @SuppressWarnings(PHPMD.NPathComplexity) Multiple action types and fallback chains create many paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Core algorithm that handles all 5 action types inline * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function walkDeletionGraph( ObjectEntity $object, @@ -757,7 +758,7 @@ private function walkDeletionGraph( * * @return ObjectEntity[] Matching objects. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function findReferencingObjects( string $sourceSchemaId, @@ -878,7 +879,7 @@ private function findReferencingObjects( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Handles PostgreSQL/MySQL and array/scalar variants * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function findReferencingInMagicTable( Register $register, @@ -970,7 +971,7 @@ private function findReferencingInMagicTable( * * @return bool True if the property is required. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function isRequiredProperty(string $schemaId, string $propertyName): bool { @@ -990,7 +991,7 @@ private function isRequiredProperty(string $schemaId, string $propertyName): boo * * @return mixed The default value, or null if not set. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function getDefaultValue(string $schemaId, string $propertyName): mixed { @@ -1023,7 +1024,7 @@ private function getDefaultValue(string $schemaId, string $propertyName): mixed * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function logIntegrityAction( string $action, @@ -1077,7 +1078,7 @@ private function logIntegrityAction( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function applySetNull(array $target): void { @@ -1136,7 +1137,7 @@ function ($val) use ($target) { * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function applySetDefault(array $target): void { @@ -1173,24 +1174,286 @@ private function applySetDefault(array $target): void }//end applySetDefault() /** - * Apply CASCADE deletes in batch, grouped by register+schema. - * - * Groups cascade targets by their register+schema pair, resolves entities - * once per group, and calls MagicMapper::deleteObjects() in bulk. - * Falls back to individual deletion for targets without register info. + * Apply CASCADE deletes with batched resolution, batched soft-delete writes + * and one multi-row audit INSERT. + * + * PERF: the previous implementation fed every target through + * MagicMapper::deleteObjects(), which runs a full cross-magic-table scan + * (findAcrossAllSources) PER UUID, and wrote one single-row audit INSERT per + * target. For N cascade targets that is ~N cross-table scans + N INSERTs + * inside the delete transaction. This method mirrors the legacy-cascade + * batch path (DeleteObject::cascadeDeleteObjects): ONE batched cross-table + * lookup, one CASE-UPDATE per magic table via + * MagicMapper::softDeleteMultipleObjectEntities() (which dispatches the + * per-object updating/updated event pairs), and ONE multi-row audit INSERT. + * + * Targets the uuid-based batch lookup cannot resolve — and every target + * when the batched resolve or write fails — fall back to the unchanged + * per-object pipeline ({@see applyPerObjectCascadeDelete()}), fail-soft. * * @param array $cascadeTargets The cascade targets from DeletionAnalysis (already reversed). * @param string $userId The user performing the deletion. * @param string $cascadeSource The UUID of the root object triggering the cascade. * @param string|null $triggerSchemaSlug Schema slug of the trigger object for logging. + * @param string|null $organisationId The active organisation ID for deletion attribution. + * + * @return void + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function applyBatchCascadeDelete( + array $cascadeTargets, + string $userId, + string $cascadeSource, + ?string $triggerSchemaSlug, + ?string $organisationId=null + ): void { + $uuids = []; + foreach ($cascadeTargets as $target) { + $uuids[] = (string) $target['objectUuid']; + } + + $uuids = array_values(array_unique($uuids)); + + // 1. Resolve every cascade target in ONE batched cross-table lookup. + // Only live rows are wanted: already soft-deleted targets stay on the + // per-object fallback, which no-ops their write (prior behaviour). + $resolved = []; + try { + $resolved = $this->objectEntityMapper->findMultipleAcrossAllMagicTables( + uuids: $uuids, + includeDeleted: false + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ReferentialIntegrity] Batched cascade lookup failed; falling back to per-object deletes', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + } + + // 2. One soft-delete UPDATE per magic table (+ per-object event pairs). + [$handled, $deleted] = $this->batchCascadeSoftDelete( + targets: $resolved, + userId: $userId, + organisationId: $organisationId + ); + + // 3. One multi-row audit INSERT covering every batch-deleted target. + $this->writeBatchCascadeAuditTrails( + cascadeTargets: $cascadeTargets, + deleted: $deleted, + cascadeSource: $cascadeSource, + triggerSchemaSlug: $triggerSchemaSlug + ); + + // 4. Per-object fallback for targets the batch did not take care of. + $fallbackTargets = []; + foreach ($cascadeTargets as $target) { + if (isset($handled[$target['objectUuid']]) === false) { + $fallbackTargets[] = $target; + } + } + + if (empty($fallbackTargets) === false) { + $this->applyPerObjectCascadeDelete( + cascadeTargets: $fallbackTargets, + userId: $userId, + cascadeSource: $cascadeSource, + triggerSchemaSlug: $triggerSchemaSlug + ); + } + }//end applyBatchCascadeDelete() + + /** + * Soft-delete a batch of resolved cascade targets. + * + * Applies the same deletion attribution metadata shape as + * DeleteObject::delete() and the legacy-cascade batch write, then persists + * via one UPDATE per magic table (MagicMapper::softDeleteMultipleObjectEntities, + * which also dispatches the per-object updating/updated event pairs — a + * hook stopping propagation skips that target's write and the target is + * NOT force-retried per-object). + * + * @param ObjectEntity[] $targets The batch-resolved cascade target entities. + * @param string $userId The user performing the deletion. + * @param string|null $organisationId The active organisation ID for attribution. + * + * @return array{0: array, 1: array} Two maps keyed + * by uuid: [0] every target the batch took care of + * (deleted OR deliberately skipped by a hook — the + * caller must not retry these), [1] the targets that + * were actually soft-deleted (audit rows are written + * for these). Both empty when the batched write + * failed entirely — the caller then falls back to + * the per-object pipeline for every target. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function batchCascadeSoftDelete(array $targets, string $userId, ?string $organisationId): array + { + if (empty($targets) === true) { + return [[], []]; + } + + $deletedAt = (new DateTime())->format(DateTime::ATOM); + + $handled = []; + $oldEntities = []; + foreach ($targets as $entity) { + $uuid = $entity->getUuid(); + if ($uuid === null || $uuid === '') { + continue; + } + + $handled[$uuid] = true; + $oldEntities[$uuid] = clone $entity; + $entity->setDeleted( + [ + 'deletedBy' => $userId, + 'deletedAt' => $deletedAt, + 'objectId' => $uuid, + 'organisation' => $organisationId, + ] + ); + } + + try { + $deletedEntities = $this->objectEntityMapper->softDeleteMultipleObjectEntities( + entities: $targets, + oldEntities: $oldEntities + ); + } catch (\Throwable $e) { + // Nothing was handled — the caller retries every target via the + // per-object pipeline. + $this->logger->error( + message: '[ReferentialIntegrity] Batched cascade soft delete failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + return [[], []]; + } + + $deleted = []; + foreach ($deletedEntities as $entity) { + $uuid = $entity->getUuid(); + if ($uuid !== null && $uuid !== '') { + $deleted[$uuid] = $entity; + } + } + + return [$handled, $deleted]; + }//end batchCascadeSoftDelete() + + /** + * Write the audit rows for batch-deleted cascade targets in one bulk INSERT. + * + * One row per analysis target (a target referenced through two properties + * gets two rows, matching the per-object path), each pre-built through + * AuditTrailMapper::buildAuditTrail() with action + * `referential_integrity.cascade_delete` and the canonical cascade-context + * fold (triggerObject/triggerSchema/action_type/property). Audit failures + * are logged but never abort the cascade — parity with the per-object + * path's fail-soft logIntegrityAction(). + * + * @param array $cascadeTargets All cascade targets (already reversed). + * @param array $deleted Batch-deleted entities keyed by uuid. + * @param string $cascadeSource The UUID of the root object triggering the cascade. + * @param string|null $triggerSchemaSlug Schema slug of the trigger object. + * + * @return void + * + * @spec openspec/specs/deletion-audit-trail/spec.md + */ + private function writeBatchCascadeAuditTrails( + array $cascadeTargets, + array $deleted, + string $cascadeSource, + ?string $triggerSchemaSlug + ): void { + if (empty($deleted) === true) { + return; + } + + $rows = []; + foreach ($cascadeTargets as $target) { + $entity = ($deleted[$target['objectUuid']] ?? null); + if ($entity === null) { + continue; + } + + try { + $rows[] = $this->auditTrailMapper->buildAuditTrail( + old: $entity, + new: null, + action: 'referential_integrity.cascade_delete', + cascadeContext: [ + 'triggerObject' => $cascadeSource, + 'triggerSchema' => $triggerSchemaSlug, + 'action_type' => 'referential_integrity.cascade_delete', + 'property' => ($target['property'] ?? null), + ] + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ReferentialIntegrity] Could not build cascade audit row', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uuid' => $target['objectUuid'], + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end foreach + + if (empty($rows) === true) { + return; + } + + try { + $this->auditTrailMapper->insertAuditTrails(entries: $rows); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ReferentialIntegrity] Bulk cascade audit insert failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'count' => count($rows), + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end writeBatchCascadeAuditTrails() + + /** + * Apply CASCADE deletes per object, grouped by register+schema (legacy fallback). + * + * Preserved verbatim as the fallback pipeline for targets the batched path + * cannot handle: groups cascade targets by their register+schema pair and + * calls MagicMapper::deleteObjects() in bulk (which re-resolves each uuid + * individually). Falls back to individual deletion for targets without + * register info. Each target gets its own single-row audit insert with the + * legacy changed-shape. + * + * @param array $cascadeTargets The cascade targets needing per-object handling. + * @param string $userId The user performing the deletion. + * @param string $cascadeSource The UUID of the root object triggering the cascade. + * @param string|null $triggerSchemaSlug Schema slug of the trigger object for logging. * * @return void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Groups targets and handles entity resolution per group * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ - private function applyBatchCascadeDelete( + private function applyPerObjectCascadeDelete( array $cascadeTargets, string $userId, string $cascadeSource, @@ -1258,5 +1521,5 @@ private function applyBatchCascadeDelete( ); } }//end foreach - }//end applyBatchCascadeDelete() + }//end applyPerObjectCascadeDelete() }//end class diff --git a/lib/Service/Object/RelationDetectionTrait.php b/lib/Service/Object/RelationDetectionTrait.php new file mode 100644 index 0000000000..6c5f5ab230 --- /dev/null +++ b/lib/Service/Object/RelationDetectionTrait.php @@ -0,0 +1,126 @@ + + * @copyright 2024 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Object; + +/** + * Relation detection rule shared by every relation-recording scan path. + * + * A scalar is recorded as a relation in `@self.relations` ONLY when it is a + * genuine reference to another object: + * - its schema property declares it a reference (`type:object`, + * `format` uuid/uri/url, or a `$ref` / `inversedBy` on the property), or + * - the value itself matches a UUID / prefixed-UUID / URL pattern. + * + * The previous loose heuristic — "any 8+ char string with a hyphen or + * underscore" — recorded dates, enum values and business identifiers as + * relations, polluting `@self.relations` and causing the relation graph to do + * wasted lookups. That heuristic is intentionally absent here. + * + * @category Trait + * @package OCA\OpenRegister\Service\Object + */ +trait RelationDetectionTrait +{ + /** + * Decide whether a scalar value should be recorded as a relation. + * + * @param string $value The scalar value scanned from the object data. + * @param array|null $propertyConfig The schema property definition for this value, if known. + * + * @return bool True when the value is a genuine, recordable reference. + */ + protected function isRecordableReference(string $value, ?array $propertyConfig=null): bool + { + $value = trim($value); + if ($value === '') { + return false; + } + + // Schema-declared references are authoritative. + if ($this->isSchemaDeclaredReference(propertyConfig: $propertyConfig) === true) { + return true; + } + + // Otherwise only values that themselves look like a reference qualify. + return $this->matchesReferencePattern(value: $value); + }//end isRecordableReference() + + /** + * Whether the schema property definition declares this value a reference. + * + * @param array|null $propertyConfig The schema property definition, if known. + * + * @return bool True when the property is a declared reference. + */ + private function isSchemaDeclaredReference(?array $propertyConfig): bool + { + if ($propertyConfig === null) { + return false; + } + + $type = $propertyConfig['type'] ?? ''; + $format = $propertyConfig['format'] ?? ''; + + if ($type === 'object') { + return true; + } + + if ($type === 'text' && in_array($format, ['uuid', 'uri', 'url'], true) === true) { + return true; + } + + return isset($propertyConfig['$ref']) === true || isset($propertyConfig['inversedBy']) === true; + }//end isSchemaDeclaredReference() + + /** + * Whether a value matches a UUID / prefixed-UUID / URL reference pattern. + * + * @param string $value The trimmed scalar value. + * + * @return bool True when the value looks like a reference. + */ + private function matchesReferencePattern(string $value): bool + { + // Canonical UUID (8-4-4-4-12). + if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value) === 1) { + return true; + } + + // UUID without dashes (32 hex chars). + if (preg_match('/^[0-9a-f]{32}$/i', $value) === 1) { + return true; + } + + // Prefixed UUID (e.g. "id-" with or without dashes). + $prefixed = '/^[a-z]+-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32})$/i'; + if (preg_match($prefixed, $value) === 1) { + return true; + } + + // URLs. + return filter_var($value, FILTER_VALIDATE_URL) !== false; + }//end matchesReferencePattern() +}//end trait diff --git a/lib/Service/Object/RelationHandler.php b/lib/Service/Object/RelationHandler.php index 9bdc90cbb4..1b8f3290d8 100644 --- a/lib/Service/Object/RelationHandler.php +++ b/lib/Service/Object/RelationHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -63,7 +63,7 @@ class RelationHandler * @param MagicRbacHandler $rbacHandler Handler for RBAC operations. * @param LoggerInterface $logger Logger for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function __construct( private readonly MagicMapper $objectEntityMapper, @@ -96,7 +96,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) Multiple conditional paths for schema property handling * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Inverse filter resolution requires comprehensive handling * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function applyInversedByFilter(array &$filters, callable $findAllCallback): array|null { @@ -211,7 +211,7 @@ function (ObjectEntity $object) use ($property, $key) { * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags control optional extraction features * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function extractRelatedData(array $results, bool $includeRelated, bool $includeRelatedNames): array { @@ -243,7 +243,7 @@ public function extractRelatedData(array $results, bool $includeRelated, bool $i * @SuppressWarnings(PHPMD.NPathComplexity) Multiple execution paths for relationship extraction limits * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Circuit breaker logic requires comprehensive safeguards * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function extractAllRelationshipIds(array $objects, array $_extend): array { @@ -354,7 +354,7 @@ public function extractAllRelationshipIds(array $objects, array $_extend): array * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Batch processing with error handling requires multiple conditions * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function bulkLoadRelationshipsBatched(array $relationshipIds): array { @@ -466,7 +466,7 @@ public function bulkLoadRelationshipsBatched(array $relationshipIds): array * @psalm-return list * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function loadRelationshipChunkOptimized(array $relationshipIds): array { @@ -504,7 +504,7 @@ public function loadRelationshipChunkOptimized(array $relationshipIds): array * * @psalm-return array{results: array|mixed, total: int<0, max>, limit: 30|mixed, offset: 0|mixed} * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function getContracts(string $objectId, array $filters=[]): array { @@ -568,7 +568,7 @@ public function getContracts(string $objectId, array $filters=[]): array * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC/multitenancy flags follow established API patterns * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function getUses( string $objectId, @@ -781,7 +781,7 @@ public function getUses( * * @return ObjectEntity[] Filtered objects the user has access to. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function filterByRbac(array $objects): array { @@ -860,7 +860,7 @@ private function filterByRbac(array $objects): array * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC/multitenancy flags follow established API patterns * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function getUsedBy( string $objectId, diff --git a/lib/Service/Object/RelationshipOptimizationHandler.php b/lib/Service/Object/RelationshipOptimizationHandler.php index e9b6be11cf..a6537dad3a 100644 --- a/lib/Service/Object/RelationshipOptimizationHandler.php +++ b/lib/Service/Object/RelationshipOptimizationHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -47,7 +47,7 @@ class RelationshipOptimizationHandler * * @param LoggerInterface $logger Logger for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly LoggerInterface $logger @@ -66,7 +66,7 @@ public function __construct( * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function extractAllRelationshipIds(array $_objects, array $_extend): array { @@ -85,7 +85,7 @@ public function extractAllRelationshipIds(array $_objects, array $_extend): arra * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function bulkLoadRelationshipsBatched(array $_relationshipIds): array { @@ -104,7 +104,7 @@ public function bulkLoadRelationshipsBatched(array $_relationshipIds): array * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function bulkLoadRelationshipsParallel(array $_relationshipIds): array { @@ -123,7 +123,7 @@ public function bulkLoadRelationshipsParallel(array $_relationshipIds): array * * @psalm-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function loadRelationshipChunkOptimized(array $_relationshipIds): array { @@ -140,7 +140,7 @@ public function loadRelationshipChunkOptimized(array $_relationshipIds): array * * @return null Created object entity or null. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-3 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createLightweightObjectEntity(array $_row) { diff --git a/lib/Service/Object/RenderObject.php b/lib/Service/Object/RenderObject.php index 3478e9b6ac..7448590677 100644 --- a/lib/Service/Object/RenderObject.php +++ b/lib/Service/Object/RenderObject.php @@ -34,6 +34,7 @@ use OCA\OpenRegister\Db\FileMapper; use OCA\OpenRegister\Formats\ExtendedFieldTypeValidator; use OCA\OpenRegister\Service\FileService; +use OCA\OpenRegister\Service\ObjectSource\ObjectSourceRegistry; use OCP\IRequest; use OCP\IURLGenerator; use OCA\OpenRegister\Db\ObjectEntity; @@ -42,15 +43,20 @@ use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\Translation; +use OCA\OpenRegister\Db\TranslationMapper; use OCA\OpenRegister\Db\AuditTrailMapper; use OCA\OpenRegister\Service\Object\CacheHandler; use OCA\OpenRegister\Service\Object\SaveObject\ComputedFieldHandler; use OCA\OpenRegister\Service\Object\LinkedEntityEnricher; use OCA\OpenRegister\Service\Object\TranslationHandler; +use OCA\OpenRegister\Service\FieldEncryptionHandler; use OCA\OpenRegister\Service\PropertyRbacHandler; +use OCA\OpenRegister\Service\SystemOperationContext; use OCA\OpenRegister\Service\Archival\RetentionEvaluator; use OCA\OpenRegister\Service\Calculation\CalculationEvaluator; use OCA\OpenRegister\Service\UrnService; +use OCA\OpenRegister\Service\LanguageService; use OCA\OpenRegister\Service\TranslationStatusService; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; @@ -134,31 +140,67 @@ class RenderObject */ private ?array $batchFileIdsCache = null; + /** + * Request-scoped cache of formatted file objects keyed by (string) fileid (PERF-1). + * + * Populated by renderEntities() (batch) before the per-entity loop so that + * renderFileProperties()/getFileObject() do not issue one FileMapper query per + * file per row. getFileObject() also back-fills this cache on the single-object + * path. Reset after the page loop. + * + * @var array + */ + private array $fileObjectCache = []; + + /** + * Request-scoped cache of file tag-name lists keyed by (string) fileid (PERF-1). + * + * @var array> + */ + private array $fileTagsCache = []; + + /** + * Whether a full page render is in progress (PERF-8). + * + * Set by renderEntities() around its per-entity loop. When true, extendObject() + * skips its per-entity preloadObjects() call and relies on the warm page-level + * batch preload already populated into $objectsCache — avoiding duplicate + * UUID collection + preload work for every row. + * + * @var boolean + */ + private bool $pageRenderActive = false; + /** * Constructor for RenderObject handler. * - * @param FileMapper $fileMapper File mapper for database operations. - * @param MagicMapper $objectEntityMapper Object entity mapper for database operations. - * @param RegisterMapper $registerMapper Register mapper for database operations. - * @param SchemaMapper $schemaMapper Schema mapper for database operations. - * @param ISystemTagManager $systemTagManager System tag manager for file tags. - * @param ISystemTagObjectMapper $systemTagMapper System tag object mapper for file tags. - * @param CacheHandler $cacheHandler Cache service for performance optimization. - * @param CacheHandler $objectCacheService Object cache service for optimized loading. - * @param PropertyRbacHandler $propertyRbacHandler Property-level RBAC handler. - * @param LoggerInterface $logger Logger for performance monitoring. - * @param FileService $fileService File service for file operations. - * @param ComputedFieldHandler $computedFieldHandler Handler for computed field evaluation. - * @param TranslationHandler $translationHandler Handler for translatable property resolution. - * @param LinkedEntityEnricher $linkedEntityEnricher Enricher for linked entity metadata. - * @param CalculationEvaluator $calculationEvaluator Evaluator for derived/computed properties. - * @param UrnService $urnService URN resolver for register/schema/object identifiers. - * @param TranslationStatusService $translationStatusService Service exposing per-object translation status metadata. - * @param IRequest|null $request Current request, used to read `?recurrenceOccurrences=N`. + * @param FileMapper $fileMapper File mapper for database operations. + * @param MagicMapper $objectEntityMapper Object entity mapper for database operations. + * @param RegisterMapper $registerMapper Register mapper for database operations. + * @param SchemaMapper $schemaMapper Schema mapper for database operations. + * @param ISystemTagManager $systemTagManager System tag manager for file tags. + * @param ISystemTagObjectMapper $systemTagMapper System tag object mapper for file tags. + * @param CacheHandler $cacheHandler Cache service for performance optimization. + * @param CacheHandler $objectCacheService Object cache service for optimized loading. + * @param PropertyRbacHandler $propertyRbacHandler Property-level RBAC handler. + * @param LoggerInterface $logger Logger for performance monitoring. + * @param FileService $fileService File service for file operations. + * @param ComputedFieldHandler $computedFieldHandler Handler for computed field evaluation. + * @param TranslationHandler $translationHandler Handler for translatable property resolution. + * @param LinkedEntityEnricher $linkedEntityEnricher Enricher for linked entity metadata. + * @param CalculationEvaluator $calculationEvaluator Evaluator for derived/computed properties. + * @param UrnService $urnService URN resolver for register/schema/object identifiers. + * @param TranslationStatusService $translationStatusService Service exposing per-object translation status metadata. + * @param TranslationMapper $translationMapper Translation mapper for database operations. + * @param LanguageService $languageService Language service for source-language resolution. + * @param IRequest|null $request Current request, used to read `?recurrenceOccurrences=N`. + * @param ObjectSourceRegistry|null $objectSourceRegistry Resolves object-source providers for `$ref` extends into virtual schemas. + * @param FieldEncryptionHandler|null $fieldEncryptionHandler Field-level encryption handler (x-openregister-encrypted). * * @SuppressWarnings(PHPMD.ExcessiveParameterList) All parameters are DI-injected dependencies * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + * @spec openspec/specs/field-level-encryption/spec.md#requirement-authorized-reads-are-decrypted-unauthorized-reads-never-see-ciphertext */ public function __construct( private readonly FileMapper $fileMapper, @@ -178,7 +220,11 @@ public function __construct( private readonly CalculationEvaluator $calculationEvaluator, private readonly UrnService $urnService, private readonly TranslationStatusService $translationStatusService, + private readonly TranslationMapper $translationMapper, + private readonly LanguageService $languageService, private readonly ?IRequest $request=null, + private readonly ?ObjectSourceRegistry $objectSourceRegistry=null, + private readonly ?FieldEncryptionHandler $fieldEncryptionHandler=null, ) { }//end __construct() @@ -196,7 +242,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function setUltraPreloadCache(array $ultraPreloadCache): void { @@ -218,7 +264,7 @@ public function setUltraPreloadCache(array $ultraPreloadCache): void * * @psalm-return int<0, max> * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function getUltraCacheSize(): int { @@ -232,7 +278,7 @@ public function getUltraCacheSize(): int * * @return Register|null The register or null if not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getRegister(int | string $id): ?Register { @@ -258,7 +304,7 @@ private function getRegister(int | string $id): ?Register * * @return Schema|null The schema or null if not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getSchema(int | string $id): ?Schema { @@ -279,6 +325,49 @@ private function getSchema(int | string $id): ?Schema } }//end getSchema() + /** + * The names of a schema's write-only properties. + * + * `writeOnly: true` is JSON Schema's marker for a value a client may send but the + * server must never return — a password, an API key, a TOTP seed. renderEntity() + * strips these at the API boundary (openregister#380, ocon#147); the engine's internal + * `getObject()` is unaffected, so the value is still available to code that legitimately + * needs it (a synchronisation, the credential engine). + * + * Resolved through the same cached `getSchema()` used elsewhere in the renderer, so it + * adds no query on the hot path once the schema is cached. Returns an empty array when + * the schema cannot be resolved or declares no write-only property — i.e. the default + * for every existing schema, which is why this changes no behaviour until a schema opts + * in. + * + * @param int|string|null $schemaId The object's schema id/uuid/slug. + * + * @return array The write-only property names. + */ + private function getWriteOnlyProperties(int | string | null $schemaId): array + { + if ($schemaId === null || $schemaId === '') { + return []; + } + + $schema = $this->getSchema(id: $schemaId); + if ($schema === null) { + return []; + } + + $writeOnly = []; + foreach (($schema->getProperties() ?? []) as $propertyName => $propertyConfig) { + if (is_array($propertyConfig) === true + && ($propertyConfig['writeOnly'] ?? false) === true + && is_string($propertyName) === true + ) { + $writeOnly[] = $propertyName; + } + } + + return $writeOnly; + }//end getWriteOnlyProperties() + /** * Enrich `recurrence`-typed properties with their upcoming occurrences. * @@ -296,7 +385,7 @@ private function getSchema(int | string $id): ?Schema * * @return array The object data, possibly enriched with occurrence lists. * - * @spec openspec/specs/extended-field-types/spec.md#REQ-EFT-003 + * @spec openspec/changes/add-openregister-discovered-capabilities/specs/extended-field-types/spec.md */ private function enrichRecurrenceOccurrences(array $objectData, Schema $schema): array { @@ -352,7 +441,7 @@ private function enrichRecurrenceOccurrences(array $objectData, Schema $schema): * * @return int The requested occurrence count. * - * @spec openspec/specs/extended-field-types/spec.md#REQ-EFT-003 + * @spec openspec/changes/add-openregister-discovered-capabilities/specs/extended-field-types/spec.md */ private function resolveOccurrenceCount(): int { @@ -378,7 +467,7 @@ private function resolveOccurrenceCount(): int * * @return bool True if the string matches UUID format * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function isUuidLike(string $value): bool { @@ -392,7 +481,7 @@ private function isUuidLike(string $value): bool * * @return ObjectEntity|null The object or null if not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getObject(int | string $id): ?ObjectEntity { @@ -426,7 +515,7 @@ private function getObject(int | string $id): ?ObjectEntity * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function clearCache(): void { @@ -443,7 +532,7 @@ public function clearCache(): void * * @return array Objects indexed by UUID, serialized as arrays * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function getObjectsCache(): array { @@ -483,7 +572,7 @@ public function getObjectsCache(): array * Caller must not call this when rows already have full file metadata from * renderEntities() — this method overwrites with IDs. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-15 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function attachLightweightFilesToRows(array &$rows): void { @@ -516,6 +605,286 @@ public function attachLightweightFilesToRows(array &$rows): void unset($row); }//end attachLightweightFilesToRows() + /** + * Resolve translatable properties on a batch of un-rendered list rows. + * + * List pipelines that bypass the full renderEntity pipeline (the cheap-path + * when no extend/fields/filter/unset is requested) never resolve translatable + * properties, so translatable fields come back as raw language-keyed maps + * (e.g. `{"nl":"Foo","en":"Bar"}`) instead of the single projected string the + * single-object read path returns. This method closes that gap by delegating + * each row to TranslationHandler::resolveTranslationsForRender(), mirroring + * the resolution done in renderEntity(). + * + * Behaviour: + * - No-op when the request opts in to `?_translations=all` and when the row's + * schema declares no translatable properties (both early-return inside the + * translation handler), so there is no cost for non-translatable schemas. + * - Multi-schema results resolve each row against ITS OWN schema and register. + * + * Shape-tolerant: rows may be ObjectEntity instances or serialized arrays. + * Rows whose schema cannot be resolved are left untouched. + * + * @param array $rows Result rows by reference. Each row is mutated in place. + * + * @return void + * + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + */ + public function resolveTranslationsForRows(array &$rows): void + { + if (empty($rows) === true) { + return; + } + + // Fast global skip: when `?_translations=all` is requested, no projection + // happens at all — resolveTranslationsForRender early-returns per row, but + // we short-circuit here to avoid resolving schemas/registers needlessly. + if ($this->languageService->shouldReturnAllTranslations() === true) { + return; + } + + foreach ($rows as &$row) { + if ($row instanceof ObjectEntity === true) { + $schemaId = $row->getSchema(); + if ($schemaId === null) { + continue; + } + + $schema = $this->getSchema(id: $schemaId); + if ($schema === null) { + continue; + } + + $registerId = $row->getRegister(); + $register = null; + if ($registerId !== null) { + $register = $this->getRegister(id: $registerId); + } + + $resolved = $this->translationHandler->resolveTranslationsForRender( + objectData: $row->getObject(), + schema: $schema, + register: $register + ); + $row->setObject($resolved); + continue; + }//end if + + if (is_array($row) === true) { + $schemaId = $row['@self']['schema'] ?? null; + if (is_int($schemaId) === false && is_string($schemaId) === false) { + continue; + } + + $schema = $this->getSchema(id: $schemaId); + if ($schema === null) { + continue; + } + + $registerId = $row['@self']['register'] ?? null; + $register = null; + if (is_int($registerId) === true || is_string($registerId) === true) { + $register = $this->getRegister(id: $registerId); + } + + $row = $this->translationHandler->resolveTranslationsForRender( + objectData: $row, + schema: $schema, + register: $register + ); + }//end if + }//end foreach + + unset($row); + }//end resolveTranslationsForRows() + + /** + * Redact write-only properties from cheap-path list rows (openregister#380, ocon#147). + * + * The list cheap-path (no _extend/_fields/_filter/_unset) does NOT run rows through + * renderEntity() — for performance — so the write-only redaction enforced there never + * runs. That is exactly the path the OpenConnector leak used: `GET .../objects/{reg}/ + * {schema}` is a LIST, so it took the cheap-path and returned every Source's apikey in + * cleartext even after the single-object read was redacted. This method closes that gap + * with the same rule, mirroring resolveTranslationsForRows()'s per-row schema + * resolution and object-vs-array handling. + * + * writeOnly stripping is a HARD render-boundary rule and is NOT rbac-gated, exactly as + * on the single-object path (renderEntity, #389): a `writeOnly: true` property — and a + * nested `x-openregister-writeonly-paths` dot-path (#459), which rides the same + * boundary — must never be returned on ANY read, including admin and `_rbac: false` + * renders. `ObjectsController` derives `_rbac` as `($isAdmin === false)`, so an ADMIN + * list/search read arrives here with `_rbac: false`; gating writeOnly on it returned + * every secret in cleartext (#460 — the list half of #389, which only ever fixed + * renderEntity). `_rbac: false` on an HTTP read means "this caller bypasses WHICH + * OBJECTS it may see", never "this caller may see secrets"; conflating the two was the + * bug. Internal code that needs a raw secret value reads ObjectEntity::getObject() from + * the mapper, or `find(..., _render: false)`, and never enters a render path. + * + * Property `authorization.read` (group-based) stripping and field decryption REMAIN + * `_rbac` / SystemOperationContext-gated: those are trusted-internal-read bypasses that + * mirror PermissionHandler::hasPermission(), and are unchanged by #460. + * + * Strips the object body AND the `@self.relations` search index copy (#429), which is a + * separate scalar mirror of reference-shaped values and therefore leaks a writeOnly + * secret independently of the body. + * + * @param array $rows Result rows by reference, mutated in place. + * @param bool $_rbac Whether this is a user-facing read (true) or system context (false). + * Gates ONLY the authorization.read strip and decryption — never writeOnly. + * + * @return void + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-writeonly-properties-must-never-be-returned-on-any-read + * @spec openspec/specs/row-field-level-security/spec.md#requirement-nested-writeonly-paths-must-never-be-returned-on-any-read + */ + public function redactWriteOnlyFromRows(array &$rows, bool $_rbac=true): void + { + if (empty($rows) === true) { + return; + } + + // The trusted-internal-read bypass applies to property `authorization.read` and to + // decryption ONLY. The writeOnly strip below deliberately does not consult it. + $applyGatedStrips = ($_rbac !== false) && (SystemOperationContext::isActive() === false); + + foreach ($rows as &$row) { + if ($row instanceof ObjectEntity === true) { + $schema = $this->getSchema(id: $row->getSchema()); + + $doWriteOnly = $this->schemaHasWriteOnlyRule(schema: $schema); + $doGated = ($applyGatedStrips === true && $this->schemaNeedsReadStrip(schema: $schema) === true); + if ($doWriteOnly === false && $doGated === false) { + continue; + } + + $object = $row->getObject(); + if (is_array($object) === true) { + if ($doGated === true) { + // Note filterReadableProperties strips writeOnly first, then applies + // the group-authorization filter, so this covers both concerns. + $object = $this->propertyRbacHandler->filterReadableProperties(schema: $schema, object: $object); + } else { + // The writeOnly-only path (admin / `_rbac: false` / system context, or a + // schema with no property authorization): strip writeOnly unconditionally. + $object = $this->propertyRbacHandler->stripWriteOnlyProperties(schema: $schema, object: $object); + } + + // Decrypt AFTER redaction: a field the RBAC/writeOnly strip above just + // removed is no longer a key in $object, so decryptProperties() (which + // only acts on keys still present) silently skips it — an unauthorized + // caller never reaches decryption for that field. + if ($doGated === true + && $this->fieldEncryptionHandler !== null + && $schema->hasEncryptedProperties() === true + ) { + $object = $this->fieldEncryptionHandler->decryptProperties(data: $object, schema: $schema); + }//end if + + $row->setObject($object); + }//end if + + // The body strip above leaves the `relations` search index untouched; it is + // a separate scalar mirror that jsonSerialize surfaces as @self.relations, + // so a write-only secret leaks there unless stripped too (#429). It rides the + // SAME unconditional boundary as the body strip. Encrypted values are + // ciphertext by the time scanForRelations() runs at save time and do not look + // like references (isReference() matches UUID/URI/URL shapes), so they are not + // expected to appear in this mirror in the first place — only the writeOnly + // strip applies here. + if ($doWriteOnly === true) { + $relations = $row->getRelations(); + if (is_array($relations) === true) { + $row->setRelations($this->propertyRbacHandler->stripWriteOnlyProperties(schema: $schema, object: $relations)); + } + } + + continue; + }//end if + + if (is_array($row) === true) { + $schemaId = $row['@self']['schema'] ?? null; + if (is_int($schemaId) === false && is_string($schemaId) === false) { + continue; + } + + $schema = $this->getSchema(id: $schemaId); + + $doWriteOnly = $this->schemaHasWriteOnlyRule(schema: $schema); + $doGated = ($applyGatedStrips === true && $this->schemaNeedsReadStrip(schema: $schema) === true); + if ($doWriteOnly === false && $doGated === false) { + continue; + } + + if ($doGated === true) { + $row = $this->propertyRbacHandler->filterReadableProperties(schema: $schema, object: $row); + } else { + $row = $this->propertyRbacHandler->stripWriteOnlyProperties(schema: $schema, object: $row); + } + + // Decrypt AFTER redaction, same ordering rationale as the ObjectEntity branch above. + if ($doGated === true + && $this->fieldEncryptionHandler !== null + && $schema->hasEncryptedProperties() === true + ) { + $row = $this->fieldEncryptionHandler->decryptProperties(data: $row, schema: $schema); + } + + if ($doWriteOnly === true + && isset($row['@self']['relations']) === true + && is_array($row['@self']['relations']) === true + ) { + $row['@self']['relations'] = $this->propertyRbacHandler->stripWriteOnlyProperties( + schema: $schema, + object: $row['@self']['relations'] + ); + } + }//end if + }//end foreach + + unset($row); + }//end redactWriteOnlyFromRows() + + /** + * Whether a schema has any read-strip rule (property-level read authorization or a + * write-only property). Mirrors the guard in renderEntity's read-strip block, so the + * cheap-path and the single-object path agree on when to strip. + * + * @param Schema|null $schema The row's schema. + * + * @return bool + */ + private function schemaNeedsReadStrip(?Schema $schema): bool + { + return $schema !== null + && ($schema->hasPropertyAuthorization() === true + || $this->schemaHasWriteOnlyRule(schema: $schema) === true + || $schema->hasEncryptedProperties() === true); + }//end schemaNeedsReadStrip() + + /** + * Whether a schema carries ANY write-only rule — a declared property with + * `writeOnly: true`, or a nested dot-path declared via + * `x-openregister-writeonly-paths`. + * + * Both spellings answer the same question ("must this render strip secrets?") and + * ride the same unconditional render boundary, so every gate asks it through here + * rather than checking one and forgetting the other — which is precisely how the + * nested paths would have ended up as a phantom annotation. + * + * @param Schema|null $schema The schema to inspect. + * + * @return bool True when the render path must apply write-only stripping. + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-nested-writeonly-paths-must-never-be-returned-on-any-read + */ + private function schemaHasWriteOnlyRule(?Schema $schema): bool + { + return $schema !== null + && ($schema->hasWriteOnlyProperties() === true || $schema->hasWriteOnlyPaths() === true); + }//end schemaHasWriteOnlyRule() + /** * Extract the UUID from a list-row of unknown shape (ObjectEntity or array). * @@ -655,7 +1024,7 @@ private function setLightweightFileIds(ObjectEntity $entity): ObjectEntity * * @throws \RuntimeException If multiple nodes are found for the object's uuid * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function renderFiles(ObjectEntity $object): ObjectEntity { @@ -746,10 +1115,15 @@ private function renderFiles(ObjectEntity $object): ObjectEntity * * @return array List of file tags * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getFileTags(string $fileId): array { + // PERF-1: serve from the request-scoped batch cache when present (page render). + if (array_key_exists($fileId, $this->fileTagsCache) === true) { + return $this->fileTagsCache[$fileId]; + } + // File tag type constant (same as in FileService). $fileTagType = 'files'; @@ -761,6 +1135,7 @@ private function getFileTags(string $fileId): array // Check if file has any tags. if (isset($tagIds[$fileId]) === false || empty($tagIds[$fileId]) === true) { + $this->fileTagsCache[$fileId] = []; return []; } @@ -781,8 +1156,10 @@ static function ($tagName) { } ); - // Return array of filtered tag names. - return array_values($tagNames); + // Return array of filtered tag names (PERF-1: back-fill request cache). + $result = array_values($tagNames); + $this->fileTagsCache[$fileId] = $result; + return $result; }//end getFileTags() /** @@ -806,7 +1183,7 @@ static function ($tagName) { * * @SuppressWarnings(PHPMD.CyclomaticComplexity) File property handling requires multiple type checks * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function renderFileProperties(ObjectEntity $entity): ObjectEntity { @@ -880,7 +1257,7 @@ private function renderFileProperties(ObjectEntity $entity): ObjectEntity * @psalm-return bool * @phpstan-return bool * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function isFilePropertyConfig(array $propertyConfig): bool { @@ -925,7 +1302,7 @@ private function isFilePropertyConfig(array $propertyConfig): bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function hydrateFileProperty($propertyValue, array $propertyConfig, string $_propertyName) { @@ -940,6 +1317,14 @@ private function hydrateFileProperty($propertyValue, array $propertyConfig, stri $returnBase64 = ($fileConfig['format'] ?? '') === 'base64'; + // PERF-11: never embed base64 file content on a list/collection (page) render — + // reading whole files into memory per file per row blows up peak memory. On a + // page render, fall back to the lightweight file reference instead; the full + // base64 content is still produced on single-object GETs (size-capped below). + if ($returnBase64 === true && $this->pageRenderActive === true) { + $returnBase64 = false; + } + if ($isArrayProperty === true) { // Handle array of files. if (is_array($propertyValue) === false) { @@ -988,7 +1373,7 @@ private function hydrateFileProperty($propertyValue, array $propertyConfig, stri * * @return string|null The base64 data URI or null if file not found. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getFileAsBase64($fileId): ?string { @@ -1005,6 +1390,33 @@ private function getFileAsBase64($fileId): ?string return null; } + // PERF-11: cap the size of files we are willing to base64-embed even on the + // single-object path, so a single oversized attachment cannot exhaust memory. + // Oversized files return null (the caller still has the file reference via the + // non-base64 path / @self.files). + $maxBase64Bytes = (10 * 1024 * 1024); + try { + $fileSize = (int) $file->getSize(); + if ($fileSize > $maxBase64Bytes) { + $this->logger->warning( + message: '[RenderObject] Refusing to base64-embed oversized file', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $fileIdInt, + 'size' => $fileSize, + 'cap' => $maxBase64Bytes, + ] + ); + return null; + } + } catch (\Throwable $sizeError) { + // If size can't be determined, fall through and let getContent() decide. + $this->logger->debug( + '[RenderObject] Could not determine file size for base64 cap: '.$sizeError->getMessage() + ); + }//end try + // Get file content. $fileContent = $file->getContent(); if ($fileContent === null || $fileContent === '') { @@ -1033,7 +1445,7 @@ private function getFileAsBase64($fileId): ?string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Metadata extraction requires multiple conditional checks * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function hydrateMetadataFromFileProperties(ObjectEntity $entity): ObjectEntity { @@ -1084,7 +1496,7 @@ private function hydrateMetadataFromFileProperties(ObjectEntity $entity): Object * * @return mixed|null The value at the path or null if not found. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getValueFromPath(array $data, string $path) { @@ -1119,7 +1531,7 @@ private function getValueFromPath(array $data, string $path) * modified: int|null, labels: list}|null * @phpstan-return array|null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getFileObject($fileId): array|null { @@ -1134,36 +1546,66 @@ private function getFileObject($fileId): array|null return null; } + $cacheKey = (string) $fileIdStr; + + // PERF-1: serve from the request-scoped batch cache when present. + if (array_key_exists($cacheKey, $this->fileObjectCache) === true) { + return $this->fileObjectCache[$cacheKey]; + } + // Use FileMapper to get file information directly. $fileRecord = $this->fileMapper->getFile((int) $fileIdStr); if (empty($fileRecord) === true) { + $this->fileObjectCache[$cacheKey] = null; return null; } - // Get file tags. - $labels = $this->getFileTags(fileId: (string) $fileRecord['fileid']); + $formatted = $this->formatFileRecord(fileRecord: $fileRecord); - // Format the file object (same structure as renderFiles method). - return [ - 'id' => (string) $fileRecord['fileid'], - 'path' => $fileRecord['path'], - 'title' => $fileRecord['name'], - 'accessUrl' => $fileRecord['accessUrl'] ?? null, - 'downloadUrl' => $fileRecord['downloadUrl'] ?? null, - 'type' => $fileRecord['mimetype'] ?? 'application/octet-stream', - 'extension' => pathinfo($fileRecord['name'], PATHINFO_EXTENSION), - 'size' => (int) $fileRecord['size'], - 'hash' => $fileRecord['etag'] ?? '', - 'published' => $fileRecord['published'] ?? null, - 'modified' => $fileRecord['mtime'] ?? null, - 'labels' => $labels, - ]; + // PERF-1: back-fill the request cache so repeated refs reuse it. + $this->fileObjectCache[$cacheKey] = $formatted; + + return $formatted; } catch (Exception $e) { return null; }//end try }//end getFileObject() + /** + * Format a raw FileMapper file record into the rendered file object shape (PERF-1). + * + * Shared by getFileObject() and the page-level batch prefetch so both paths emit + * an identical structure. + * + * @param array $fileRecord The raw file record from FileMapper + * + * @return array The formatted file object (same structure as renderFiles()). + * + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + */ + private function formatFileRecord(array $fileRecord): array + { + // Get file tags (request-cached). + $labels = $this->getFileTags(fileId: (string) $fileRecord['fileid']); + + // Format the file object (same structure as renderFiles method). + return [ + 'id' => (string) $fileRecord['fileid'], + 'path' => $fileRecord['path'], + 'title' => $fileRecord['name'], + 'accessUrl' => $fileRecord['accessUrl'] ?? null, + 'downloadUrl' => $fileRecord['downloadUrl'] ?? null, + 'type' => $fileRecord['mimetype'] ?? 'application/octet-stream', + 'extension' => pathinfo($fileRecord['name'], PATHINFO_EXTENSION), + 'size' => (int) $fileRecord['size'], + 'hash' => $fileRecord['etag'] ?? '', + 'published' => $fileRecord['published'] ?? null, + 'modified' => $fileRecord['mtime'] ?? null, + 'labels' => $labels, + ]; + }//end formatFileRecord() + /** * Renders an entity with optional extensions and filters. * @@ -1193,8 +1635,9 @@ private function getFileObject($fileId): array|null * @SuppressWarnings(PHPMD.NPathComplexity) Multiple optional rendering features create many paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive rendering requires extensive logic * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC and multitenancy flags control security behavior + * @SuppressWarnings(PHPMD.StaticAccess) SystemOperationContext is a static execution-context holder by design * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function renderEntity( ObjectEntity $entity, @@ -1299,6 +1742,64 @@ public function renderEntity( $entity->setObject($objectData); } + // Redact write-only properties (openregister#380, ocon#147). + // + // This runs AFTER every caller-controlled manipulation above (`fields`, `filter`, + // `unset`) precisely so that none of them can widen it: a caller cannot ask for a + // write-only field back the way it can with `fields`. It is server-side and + // schema-driven, which is the whole point — the lesson from the OpenConnector leak + // (ocon#147) was that controller-side redaction is bypassed the moment a second + // read path exists, and renderEntity() is the one path every API read renders through. + // + // `writeOnly` is JSON Schema's own word for "may be sent by the client, never + // returned by the server" — the exact semantic for a secret (a source's apikey, a + // portal account's mfaSecret). A property is affected only once its schema marks it + // `writeOnly: true`, so no existing schema changes behaviour until it opts in. + // + // Gated on `$_rbac`: a caller passing `_rbac: false` is trusted internal code + // reading in system context — the synchronisation engine, the credential engine, + // configuration import/export. That path MUST get the full object, or every + // integration that needs the secret breaks (it was reading through find(), which + // always renders). A real user-facing API read runs with `_rbac: true` (the + // default), and only that path is redacted. `_rbac: false` is already OR's marker + // for "access control is bypassed here"; reusing it keeps one trust boundary, not two. + if ($_rbac === true && is_array($objectData) === true && empty($objectData) === false) { + $writeOnly = $this->getWriteOnlyProperties(schemaId: $entity->getSchema()); + if (empty($writeOnly) === false) { + $redacted = false; + foreach ($writeOnly as $property) { + if (array_key_exists($property, $objectData) === true) { + unset($objectData[$property]); + $redacted = true; + } + } + + if ($redacted === true) { + $entity->setObject($objectData); + } + + // The object body is not the only copy. OpenRegister mirrors every scalar + // property into the `relations` search index, which `jsonSerialize()` + // surfaces as `@self.relations` — so a write-only secret leaks there even + // after the body is redacted (this is the "written to more than one place" + // half of ocon#147). Strip it from the index copy too. + $relations = $entity->getRelations(); + if (is_array($relations) === true && empty($relations) === false) { + $relationsRedacted = false; + foreach ($writeOnly as $property) { + if (array_key_exists($property, $relations) === true) { + unset($relations[$property]); + $relationsRedacted = true; + } + } + + if ($relationsRedacted === true) { + $entity->setRelations($relations); + } + }//end if + }//end if + }//end if + // Handle inversed properties ONLY if we're extending an inverse property. // This is a performance optimization: inverse lookups are expensive (search all magic tables), // so we only do them when explicitly requested via _extend. @@ -1425,38 +1926,114 @@ public function renderEntity( $entity->setObject($objectData); } - // Apply property-level RBAC filtering. - // This filters out properties that the current user is not authorized to read. - $schema = $readSchema ?? $this->getSchema(id: $entity->getSchema()); - if ($schema !== null && $schema->hasPropertyAuthorization() === true) { - // Ensure @self metadata is available for property-level RBAC checks. - // Property authorization can reference @self.organisation or _organisation, - // which needs to be accessible during filtering (before jsonSerialize adds @self). - $objectDataWithSelf = $objectData; - if (isset($objectDataWithSelf['@self']) === false) { - $objectDataWithSelf['@self'] = [ - 'organisation' => $entity->getOrganisation(), - 'owner' => $entity->getOwner(), - ]; - } + // Apply property-level read stripping (writeOnly secrets + property + // `authorization.read`). This removes properties the caller must not see. + // + // Fail-safe ordering: this runs AFTER caller-supplied `fields`/`unset` + // selection above, so a caller can never re-surface a stripped property + // by naming it (e.g. ?fields=apiToken). + // + // writeOnly stripping is a HARD render-boundary rule and is NOT rbac-gated: + // a `writeOnly: true` property must never be returned on ANY read — including + // admin and `_rbac: false` internal renders (an admin HTTP read renders with + // `_rbac: false`, so gating writeOnly on it leaked plaintext secrets — #389). + // Internal code that needs a raw secret value reads ObjectEntity::getObject() + // from the mapper directly and never enters renderEntity. + // + // Property `authorization.read` (group-based) stripping IS rbac-gated: it is + // bypassed for trusted internal reads (`_rbac: false`) and config boot / repair + // steps (SystemOperationContext), mirroring PermissionHandler::hasPermission(). + // + // Fail-safe ordering: both run AFTER caller-supplied `fields`/`unset` selection + // above, so a caller can never re-surface a stripped property by naming it + // (e.g. ?fields=apiToken). + // Nested write-only paths (x-openregister-writeonly-paths) ride the SAME + // unconditional boundary as top-level `writeOnly`, deliberately: they exist + // because a secret nested in an untyped `object` property has no property to + // hang `writeOnly: true` on, not because it deserves a weaker rule. Gating + // them on `_rbac` would reintroduce #389 for exactly the values that motivated + // this mechanism (an admin HTTP GET renders with `_rbac: false`). + $schema = $readSchema ?? $this->getSchema(id: $entity->getSchema()); + $stripAuthz = ($_rbac !== false) && (SystemOperationContext::isActive() === false); + $doWriteOnly = $this->schemaHasWriteOnlyRule(schema: $schema); + $doAuthz = ($stripAuthz === true && $schema !== null && $schema->hasPropertyAuthorization() === true); + if ($doWriteOnly === true || $doAuthz === true) { + if ($doAuthz === true) { + // Group-authorization filtering also strips writeOnly (filterReadableProperties + // calls stripWriteOnlyProperties first). Ensure @self metadata is available: + // property authorization can reference @self.organisation or _organisation. + $objectDataWithSelf = $objectData; + if (isset($objectDataWithSelf['@self']) === false) { + $objectDataWithSelf['@self'] = [ + 'organisation' => $entity->getOrganisation(), + 'owner' => $entity->getOwner(), + ]; + } - $objectData = $this->propertyRbacHandler->filterReadableProperties( - schema: $schema, - object: $objectDataWithSelf - ); + $objectData = $this->propertyRbacHandler->filterReadableProperties( + schema: $schema, + object: $objectDataWithSelf + ); - // Remove the temporary @self if it was added (it will be properly added in jsonSerialize). - if (isset($objectData['@self']) === true - && count($objectData['@self']) <= 2 - && isset($objectData['@self']['organisation']) === true - ) { - unset($objectData['@self']); - } + // Remove the temporary @self if it was added (jsonSerialize re-adds it). + if (isset($objectData['@self']) === true + && count($objectData['@self']) <= 2 + && isset($objectData['@self']['organisation']) === true + ) { + unset($objectData['@self']); + } + } else { + // The writeOnly-only path (admin / _rbac: false, or a schema with no + // property authorization): strip writeOnly unconditionally. + $objectData = $this->propertyRbacHandler->stripWriteOnlyProperties( + schema: $schema, + object: $objectData + ); + }//end if + + // The body is not the only copy of a writeOnly value. OpenRegister + // mirrors reference-shaped scalar properties (a UUID/URL/ref value) + // into the `relations` search index, which jsonSerialize() surfaces + // as `@self.relations` — so a writeOnly secret whose value is + // reference-shaped leaks there even after the body strip above. The + // body strip is a HARD, unconditional render-boundary rule (#389); + // its mirror MUST strip on the SAME boundary. The older _rbac-gated + // block earlier in this method strips the mirror only when + // `_rbac === true`, so an admin / `_rbac: false` read (an admin HTTP + // GET renders with `_rbac: false`) still returned the value via + // `@self.relations` (#420 — the mirror half of #389). Strip it here, + // unconditionally, whenever the schema has writeOnly properties. + if ($doWriteOnly === true) { + $relations = $entity->getRelations(); + if (is_array($relations) === true && empty($relations) === false) { + $entity->setRelations( + $this->propertyRbacHandler->stripWriteOnlyProperties( + schema: $schema, + object: $relations + ) + ); + } + }//end if }//end if + // Decrypt properties flagged `x-openregister-encrypted: true` (field-level- + // object-encryption). This MUST run after the writeOnly/property-authorization + // strip block above, and nowhere earlier: a property that block just removed is + // no longer a key in $objectData, so decryptProperties() — which only acts on + // keys still present — silently skips it. An unauthorized caller therefore never + // reaches decryption for a field it must not see; it gets the same "absent field" + // redaction every other property gets, never ciphertext, never plaintext. A value + // that is not yet an envelope (mixed plaintext/ciphertext rollout state) passes + // through unchanged; a value that fails to decrypt is replaced with a structured + // error marker rather than failing the whole render (see FieldEncryptionHandler). + if ($this->fieldEncryptionHandler !== null && $schema !== null && $schema->hasEncryptedProperties() === true) { + $objectData = $this->fieldEncryptionHandler->decryptProperties(data: $objectData, schema: $schema); + } + // Resolve translatable properties to the requested language. $renderSchema = $this->getSchema(id: $entity->getSchema()); $renderRegister = $this->getRegister(id: $entity->getRegister()); + $preResolveData = $objectData; if ($renderSchema !== null) { $objectData = $this->translationHandler->resolveTranslationsForRender( objectData: $objectData, @@ -1465,6 +2042,30 @@ public function renderEntity( ); } + // I18n-source-of-truth: when `?_translationMeta=true`, attach a + // `_meta.languageMeta` envelope describing each translatable + // property's served language, source language, isSource flag, and + // workflow status. Opt-in to keep payload size flat by default. + if ($renderSchema !== null && $this->shouldAttachLanguageMeta() === true) { + try { + $objectData = $this->attachLanguageMeta( + objectData: $objectData, + preResolveData: $preResolveData, + entity: $entity, + schema: $renderSchema, + register: $renderRegister + ); + } catch (\Throwable $e) { + $this->logger->debug( + sprintf( + '[RenderObject] languageMeta attach failed for %s: %s', + (string) $entity->getUuid(), + $e->getMessage() + ) + ); + } + } + // Evaluate virtual calculations (`materialise: false`) when the // caller asks for them via _extend. Materialised calcs are // already in $objectData (set by CalculationOnSaveListener). @@ -1549,7 +2150,7 @@ public function renderEntity( * * @return void * - * @spec openspec/changes/add-archival-annotation-support/tasks.md#task-6 + * @spec openspec/specs/archival-annotation-vocabulary/spec.md */ private function applyArchivalRetentionBlock(ObjectEntity $entity, ?Schema $schema): void { @@ -1596,6 +2197,168 @@ private function applyArchivalRetentionBlock(ObjectEntity $entity, ?Schema $sche }//end try }//end applyArchivalRetentionBlock() + /** + * Check whether the current request opts in to the `_translationMeta` + * envelope. + * + * @return bool True when `?_translationMeta=true`/`1` was passed. + * + * @spec openspec/specs/i18n-source-of-truth/spec.md + */ + private function shouldAttachLanguageMeta(): bool + { + if ($this->request === null) { + return false; + } + + $raw = $this->request->getParam('_translationMeta', null); + if ($raw === null) { + return false; + } + + if (is_string($raw) === false) { + return ($raw === true || $raw === 1); + } + + $normalised = strtolower(trim($raw)); + return in_array($normalised, ['1', 'true', 'yes', 'on'], true); + }//end shouldAttachLanguageMeta() + + /** + * Attach the `_meta.languageMeta` envelope describing per-property + * language metadata for translatable properties. + * + * The envelope is OFF by default; this method is only called when + * `?_translationMeta=true` is on the request. + * + * @param array $objectData Rendered data (post resolveTranslationsForRender). + * @param array $preResolveData Raw data (pre resolveTranslationsForRender). + * @param ObjectEntity $entity The entity being rendered. + * @param Schema $schema The schema for the entity. + * @param Register|null $register The owning register (for default fallback). + * + * @return array Object data with `_meta.languageMeta` attached. + * + * @spec openspec/specs/i18n-source-of-truth/spec.md + */ + private function attachLanguageMeta( + array $objectData, + array $preResolveData, + ObjectEntity $entity, + Schema $schema, + ?Register $register + ): array { + $translatableProps = $this->translationHandler->getTranslatableProperties($schema); + if (count($translatableProps) === 0) { + return $objectData; + } + + $uuid = (string) ($entity->getUuid() ?? ''); + if ($register !== null) { + $registerDefault = $register->getDefaultLanguage(); + } else { + $registerDefault = 'nl'; + } + + $served = $this->languageService->getPreferredLanguage(); + + $languageMeta = []; + foreach ($translatableProps as $property) { + // Determine source language from object body override → schema + // property modifier → register default. + $sourceLanguage = $this->resolveSourceLanguageForProperty( + schema: $schema, + preResolveData: $preResolveData, + property: $property, + registerDefault: $registerDefault + ); + + // Look up status from the sidecar for the served language. + $status = 'source'; + if ($uuid !== '' && $served !== $sourceLanguage) { + $row = null; + try { + $row = $this->translationMapper->findOne($uuid, $property, $served); + } catch (\Throwable $e) { + $row = null; + } + + if ($row !== null) { + $status = (string) ($row->getStatus() ?? Translation::STATUS_DRAFT); + } else { + $status = 'missing'; + } + } + + $languageMeta[$property] = [ + 'served' => $served, + 'sourceLanguage' => $sourceLanguage, + 'isSource' => ($served === $sourceLanguage), + 'status' => $status, + ]; + }//end foreach + + if (count($languageMeta) === 0) { + return $objectData; + } + + $existingMeta = $objectData['_meta'] ?? []; + if (is_array($existingMeta) === false) { + $existingMeta = []; + } + + $existingMeta['languageMeta'] = $languageMeta; + $objectData['_meta'] = $existingMeta; + return $objectData; + }//end attachLanguageMeta() + + /** + * Resolve the source language for a single property from object override + * → schema modifier → register default. + * + * @param Schema $schema The schema for the entity. + * @param array $preResolveData Raw object data with `_translationMeta` block intact. + * @param string $property The translatable property name. + * @param string $registerDefault Register-level default. + * + * @return string Resolved BCP-47 source language. + * + * @spec openspec/specs/i18n-source-of-truth/spec.md + */ + private function resolveSourceLanguageForProperty( + Schema $schema, + array $preResolveData, + string $property, + string $registerDefault + ): string { + $meta = $preResolveData['_translationMeta'] ?? null; + if (is_array($meta) === true + && isset($meta[$property]) === true + && is_array($meta[$property]) === true + && isset($meta[$property]['sourceLanguage']) === true + && is_string($meta[$property]['sourceLanguage']) === true + && $meta[$property]['sourceLanguage'] !== '' + ) { + return (string) $meta[$property]['sourceLanguage']; + } + + $properties = $schema->getProperties() ?? []; + $definition = $properties[$property] ?? null; + if (is_array($definition) === true + && isset($definition['sourceLanguage']) === true + && is_string($definition['sourceLanguage']) === true + && $definition['sourceLanguage'] !== '' + ) { + return (string) $definition['sourceLanguage']; + } + + if ($registerDefault !== '') { + return $registerDefault; + } + + return 'nl'; + }//end resolveSourceLanguageForProperty() + /** * Apply virtual (materialise:false) calculations declared on the * schema to the given data array. Materialised calculations are @@ -1679,7 +2442,7 @@ private function applyVirtualCalculations(ObjectEntity $entity, $schema, array $ * * @return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function handleWildcardExtends(array $objectData, array &$_extend, int $depth): array { @@ -1746,7 +2509,7 @@ function (string $key) { * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive dot notation handling requires extensive logic * @SuppressWarnings(PHPMD.BooleanArgumentFlag) All flag controls extension behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function handleExtendDot( array $data, @@ -1942,7 +2705,7 @@ function ($identifier) use ($depth, $keyExtends, $allFlag, $visitedIds) { * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function extendObject( ObjectEntity $entity, @@ -1975,10 +2738,33 @@ private function extendObject( $objectData['@self'] = $self; } + // Object-source relations first: a property whose `$ref` targets a + // schema served by an object-source provider must resolve through THAT + // provider — its raw key (often a plain integer) would otherwise be + // looked up against native object storage and can match an unrelated + // object entirely. Resolved keys are removed from $_extend so the + // generic path below never sees them. + [$objectData, $_extend] = $this->extendObjectSourceRefs( + entity: $entity, + _extend: $_extend, + objectData: $objectData, + depth: $depth, + visitedIds: ($visitedIds ?? []) + ); + // **PERFORMANCE OPTIMIZATION**: Batch preload all UUIDs that will be extended. // This collects all UUIDs from the properties that will be extended and loads // them in a SINGLE database query, instead of one query per UUID. - $uuidsToPreload = $this->collectUuidsForExtend(objectData: $objectData, extend: $_extend); + // PERF-8: during a full page render renderEntities() has already batch-preloaded + // every extend UUID for the whole page into $objectsCache, so re-collecting and + // re-preloading per row is wasted CPU. Skip it at depth 0 while a page render is + // active; nested (depth > 0) extends still preload their own children. + $skipPreload = ($this->pageRenderActive === true && $depth === 0); + $uuidsToPreload = []; + if ($skipPreload === false) { + $uuidsToPreload = $this->collectUuidsForExtend(objectData: $objectData, extend: $_extend); + } + if (empty($uuidsToPreload) === false) { $preloadedObjects = $this->objectCacheService->preloadObjects($uuidsToPreload); // Add preloaded objects to local cache for immediate access. @@ -2009,6 +2795,134 @@ private function extendObject( return $objectDataDot; }//end extendObject() + /** + * Resolve explicitly-extended `$ref` properties whose target schema is + * served by an object-source provider. + * + * The stored value of such a property is the EXTERNAL system's raw key + * (for a database-backed schema: the row's primary key, often a small + * integer). Resolving that key against native object storage — what the + * generic extend path does — can match an unrelated native object. Each + * matching key is resolved through the target schema's provider instead + * and removed from the extend list. + * + * Explicit extend keys only: `_extend=all` does not traverse object-source + * references (their raw keys are preserved un-expanded). + * + * @param ObjectEntity $entity The entity being rendered. + * @param array $_extend The requested extends. + * @param array $objectData The object data (mutated copy). + * @param int $depth The current render depth. + * @param array $visitedIds Circular-reference guard. + * + * @return array{0: array, 1: array} [objectData, remaining extends]. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function extendObjectSourceRefs( + ObjectEntity $entity, + array $_extend, + array $objectData, + int $depth, + array $visitedIds + ): array { + if ($this->objectSourceRegistry === null || $_extend === []) { + return [$objectData, $_extend]; + } + + $schema = $this->getSchema(id: $entity->getSchema()); + if ($schema === null) { + return [$objectData, $_extend]; + } + + $properties = ($schema->getProperties() ?? []); + $register = null; + + foreach ($properties as $name => $definition) { + if (is_array($definition) === false || in_array($name, $_extend, true) === false) { + continue; + } + + $ref = ($definition['$ref'] ?? ($definition['items']['$ref'] ?? null)); + if (is_string($ref) === false || $ref === '') { + continue; + } + + $target = $this->getSchema(id: $ref); + $objectSource = $target?->getObjectSource(); + if ($target === null || $objectSource === null) { + continue; + } + + $provider = $this->objectSourceRegistry->get((string) $objectSource['provider']); + if ($provider === null || $provider->isEnabled() === false) { + continue; + } + + if ($register === null) { + $register = $this->getRegister(id: $entity->getRegister()); + } + + if ($register === null) { + continue; + } + + $config = ($objectSource['config'] ?? []); + $resolve = function ($identifier) use ($provider, $register, $target, $config, $depth, $visitedIds) { + if (is_scalar($identifier) === false) { + return $identifier; + } + + $related = $provider->find( + register: $register, + schema: $target, + id: (string) $identifier, + config: $config + ); + if ($related === null) { + // Preserve the raw key when the external row is absent. + return $identifier; + } + + return $this->renderEntity( + entity: $related, + _extend: [], + depth: ($depth + 1), + filter: [], + fields: [], + unset: [], + visitedIds: $visitedIds + )->jsonSerialize(); + }; + + $value = ($objectData[$name] ?? null); + if (is_array($value) === true) { + // An associative array (i.e. one carrying string keys such as + // id/@self) is an object already embedded by an earlier extend + // pass — never treat its VALUES as ids to resolve. A list can + // never carry those string keys, so list-ness is the test. + $alreadyEmbedded = (array_is_list($value) === false); + if ($alreadyEmbedded === false) { + $objectData[$name] = array_map($resolve, $value); + } + } else if ($value !== null) { + $objectData[$name] = $resolve($value); + } + + // The generic extend path must never see this key — a raw external + // key resolved against native storage can match a foreign object. + $_extend = array_values( + array_filter( + $_extend, + fn ($key) => ((string) $key) !== $name + && str_starts_with((string) $key, $name.'.') === false + ) + ); + }//end foreach + + return [$objectData, $_extend]; + }//end extendObjectSourceRefs() + /** * Collect all UUIDs from object data for properties that will be extended. * @@ -2020,7 +2934,7 @@ private function extendObject( * * @return array Array of UUIDs to preload * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function collectUuidsForExtend(array $objectData, array $extend): array { @@ -2081,7 +2995,7 @@ private function collectUuidsForExtend(array $objectData, array $extend): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function preloadInverseRelationships(array $entities, array $extend): void { @@ -2150,7 +3064,7 @@ private function preloadInverseRelationships(array $entities, array $extend): vo * * @return array Filtered array of inverse properties that are being extended * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function filterExtendedInverseProperties(array $inversedProperties, array $extend): array { @@ -2171,7 +3085,7 @@ private function filterExtendedInverseProperties(array $inversedProperties, arra * * @return array Array of UUID strings * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function collectEntityUuids(array $entities): array { @@ -2194,7 +3108,7 @@ private function collectEntityUuids(array $entities): array * * @return array|null Array with keys 'targetSchemaRef' and 'inversedByFields', or null if invalid * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function extractInverseConfig(array $propConfig): ?array { @@ -2232,7 +3146,7 @@ private function extractInverseConfig(array $propConfig): ?array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function preloadSingleInverseProperty( string $propName, @@ -2318,7 +3232,7 @@ private function preloadSingleInverseProperty( * * @return array Array of ObjectEntity instances that reference the given UUIDs * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function batchLoadReferencingObjects( array $entityUuids, @@ -2343,9 +3257,7 @@ private function batchLoadReferencingObjects( $additionalFields = array_slice($inversedByFields, 1); } - $magicMapper = \OC::$server->get(\OCA\OpenRegister\Db\MagicMapper::class); - - return $magicMapper->findByRelationBatchInSchema( + return $this->objectEntityMapper->findByRelationBatchInSchema( uuids: $entityUuids, schemaId: $schemaIdInt, registerId: $registerId, @@ -2365,7 +3277,7 @@ private function batchLoadReferencingObjects( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function initializeInverseCacheEntries(array $entityUuids, string $propName): void { @@ -2390,7 +3302,7 @@ private function initializeInverseCacheEntries(array $entityUuids, string $propN * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function indexReferencingObjects( array $referencingObjects, @@ -2437,7 +3349,7 @@ private function indexReferencingObjects( * * @return array Array of UUID strings (may contain nulls which should be filtered by caller) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function resolveReferencedUuids(array $refData, string $field): array { @@ -2465,7 +3377,7 @@ private function resolveReferencedUuids(array $refData, string $field): array * * @return array Array of property names that have inversedBy configurations * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getInversedProperties(Schema $schema): array { @@ -2508,7 +3420,7 @@ function (array $property): bool { * @SuppressWarnings(PHPMD.NPathComplexity) Multiple relationship types create many paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive relationship handling requires extensive logic * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function handleInversedProperties( ObjectEntity $entity, @@ -2556,13 +3468,42 @@ private function handleInversedProperties( ); } - // Fallback: Query for referencing objects (original slower path). - // This happens when preloading wasn't done (e.g., single entity render). + // Single-entity render (e.g. show()): the batch preload only runs on the + // list path (renderEntities), so the cache is cold here. Route this read + // through the SAME schema-targeted batched machinery the list path uses + // (preloadInverseRelationships → findByRelationBatchInSchema, GIN-indexed) + // instead of falling straight into the generic cross-table + // findByRelation() scan. ALL inverse properties are preloaded — not just + // the extended ones — because a single read has always resolved every + // inverse property once any one of them is extended; preloading a subset + // would silently empty the others (consumer-visible response shape). + $this->preloadInverseRelationships( + entities: [$entity], + extend: $propertyNames + ); + + foreach ($propertyNames as $propName) { + if (isset($this->inverseRelationCache[$entityUuid.'_'.$propName]) === true) { + $hasCache = true; + break; + } + } + + if ($hasCache === true) { + return $this->handleInversedPropertiesFromCache( + entity: $entity, + objectData: $objectData, + inversedProperties: $inversedProperties + ); + } + + // Fallback: Query for referencing objects (original slower cross-table path). + // Only reached when the batched preload could not populate the cache + // (e.g. unresolvable target schema reference or batch query failure). $referencingObjects = $this->objectEntityMapper->findByRelation($entityUuid); // For multi-field inversedBy, also search columns directly since _relations // may not contain UUIDs stored in object-format fields (e.g., {"value": "uuid"}). - $magicMapper = \OC::$server->get(\OCA\OpenRegister\Db\MagicMapper::class); foreach ($inversedProperties as $propName => $propConfig) { $inversedByValue = $propConfig['items']['inversedBy'] ?? $propConfig['inversedBy'] ?? null; if (is_array($inversedByValue) === true && count($inversedByValue) > 1) { @@ -2577,7 +3518,7 @@ private function handleInversedProperties( } $additionalFields = array_slice($inversedByValue, 1); - $additionalResults = $magicMapper->findByRelationBatchInSchema( + $additionalResults = $this->objectEntityMapper->findByRelationBatchInSchema( uuids: [$entityUuid], schemaId: (int) $targetSchemaId, registerId: (int) $entity->getRegister(), @@ -2769,7 +3710,7 @@ function (ObjectEntity $object) { * * @return array The updated object data with inversed properties populated * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function handleInversedPropertiesFromCache( ObjectEntity $entity, @@ -2847,7 +3788,7 @@ function (ObjectEntity $object) { * * @return string The resolved schema ID * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function resolveSchemaReference(string $schemaRef): string { @@ -2909,7 +3850,7 @@ private function resolveSchemaReference(string $schemaRef): string * * @return string The reference string without query parameters * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function removeQueryParameters(string $reference): string { @@ -2941,7 +3882,7 @@ private function removeQueryParameters(string $reference): string * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) RBAC and multitenancy flags control security behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function renderEntities( array $entities, @@ -2976,7 +3917,8 @@ public function renderEntities( // **PERFORMANCE OPTIMIZATION**: Batch preload ALL related objects BEFORE rendering. // This prevents N+1 query problem when extending relations across multiple entities. - $this->logger->info( + // Debug level: this fires on EVERY extended list render (hot path). + $this->logger->debug( message: '[RenderObject] [BATCH_PRELOAD] Starting batch preload check', context: [ 'file' => __FILE__, @@ -3008,7 +3950,7 @@ public function renderEntities( // Remove duplicates and batch preload ALL related objects in ONE query. $allUuidsToPreload = array_unique($allUuidsToPreload); - $this->logger->info( + $this->logger->debug( message: '[RenderObject] [BATCH_PRELOAD] UUIDs collected', context: [ 'file' => __FILE__, @@ -3064,8 +4006,18 @@ public function renderEntities( } } + // PERF-1: batch-prefetch full file objects + tags for every file-typed property + // value across the whole page, so renderFileProperties()/getFileObject() do not + // issue 1+ queries per file per row. This turns a 20-row x 3-file page from ~120 + // queries into ~3 (one IN(...) files query + one batched tag-id query + one tag + // resolve). Caches are request-scoped and cleared in the finally below. + $this->batchPreloadFileObjects(entities: $entities); + $renderedEntities = []; + // PERF-8: mark a page render so extendObject() skips per-row re-preloading. + $this->pageRenderActive = true; + try { // Render each entity (now using warm cache for forward relations). foreach ($entities as $entity) { @@ -3089,8 +4041,162 @@ public function renderEntities( } } finally { $this->batchFileIdsCache = null; + // PERF-1: drop request-scoped file caches after the page render. + $this->fileObjectCache = []; + $this->fileTagsCache = []; + // PERF-8: clear the page-render flag. + $this->pageRenderActive = false; }//end try return $renderedEntities; }//end renderEntities() + + /** + * Batch-preload formatted file objects + tags for an entire page of entities (PERF-1). + * + * Collects every file id referenced by a file-typed property across all entities, + * loads them in a single FileMapper::getFilesByIds() call and resolves their tags in + * one batched ISystemTagManager call, then populates the request-scoped + * $fileObjectCache / $fileTagsCache so per-row getFileObject()/getFileTags() are + * cache hits. + * + * @param array $entities The page of ObjectEntity instances about to be rendered + * + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + */ + private function batchPreloadFileObjects(array $entities): void + { + if (empty($entities) === true) { + return; + } + + // 1) Collect all file ids referenced by file-typed properties across the page. + $allFileIds = []; + foreach ($entities as $entity) { + if ($entity instanceof ObjectEntity === false) { + continue; + } + + $schema = $this->getSchema(id: $entity->getSchema()); + if ($schema === null) { + continue; + } + + $schemaProperties = $schema->getProperties() ?? []; + $objectData = $entity->getObject(); + if (is_array($objectData) === false) { + continue; + } + + foreach ($schemaProperties as $propertyName => $propertyConfig) { + if (is_array($propertyConfig) === false + || $this->isFilePropertyConfig(propertyConfig: $propertyConfig) === false + ) { + continue; + } + + $value = ($objectData[$propertyName] ?? null); + if ($value === null) { + continue; + } + + // Base64-format properties read content, not the formatted file + // object/tags, so they are not part of this prefetch. + $isArrayProperty = ($propertyConfig['type'] ?? '') === 'array'; + $fileConfig = $propertyConfig; + if ($isArrayProperty === true) { + $fileConfig = ($propertyConfig['items'] ?? []); + } + + if (($fileConfig['format'] ?? '') === 'base64') { + continue; + } + + $candidates = $value; + if (is_array($value) === false) { + $candidates = [$value]; + } + + foreach ($candidates as $candidate) { + if (is_numeric($candidate) === true + || (is_string($candidate) === true && ctype_digit($candidate) === true) + ) { + $allFileIds[(int) $candidate] = (int) $candidate; + } + } + }//end foreach + }//end foreach + + if (empty($allFileIds) === true) { + return; + } + + $fileIds = array_values($allFileIds); + + try { + // 2) Batch-load all files in one query. + $filesById = $this->fileMapper->getFilesByIds(fileIds: $fileIds); + + // 3) Batch-load all tag ids for all files in one query. + $stringFileIds = array_map(static fn($id) => (string) $id, $fileIds); + $allTagIdsPerFile = $this->systemTagMapper->getTagIdsForObjects( + objIds: $stringFileIds, + objectType: 'files' + ); + + // Resolve all unique tag ids to names in one call. + $uniqueTagIds = []; + foreach ($allTagIdsPerFile as $fileTagIds) { + foreach ($fileTagIds as $tagId) { + $uniqueTagIds[$tagId] = true; + } + } + + $tagNameMap = []; + if (empty($uniqueTagIds) === false) { + $tags = $this->systemTagManager->getTagsByIds(tagIds: array_keys($uniqueTagIds)); + foreach ($tags as $tag) { + $name = $tag->getName(); + if (str_starts_with($name, 'object:') === false) { + $tagNameMap[$tag->getId()] = $name; + } + } + } + + // 4) Populate the request-scoped tag cache for every requested file id. + foreach ($stringFileIds as $fileIdStr) { + $labels = []; + foreach (($allTagIdsPerFile[$fileIdStr] ?? []) as $tagId) { + if (isset($tagNameMap[$tagId]) === true) { + $labels[] = $tagNameMap[$tagId]; + } + } + + $this->fileTagsCache[$fileIdStr] = $labels; + } + + // 5) Populate the request-scoped file-object cache (null for missing files). + foreach ($fileIds as $fileId) { + $fileIdStr = (string) $fileId; + $record = ($filesById[$fileIdStr] ?? null); + if ($record === null) { + $this->fileObjectCache[$fileIdStr] = null; + continue; + } + + $this->fileObjectCache[$fileIdStr] = $this->formatFileRecord(fileRecord: $record); + } + } catch (Exception $e) { + // A prefetch failure must never break rendering — per-row paths will simply + // fall back to individual lookups (the pre-PERF-1 behaviour). + $this->logger->warning( + message: '[RenderObject] Batch file prefetch failed; falling back to per-row lookups', + context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] + ); + }//end try + }//end batchPreloadFileObjects() }//end class diff --git a/lib/Service/Object/RevertHandler.php b/lib/Service/Object/RevertHandler.php index 8451950c21..6a3f8fd697 100644 --- a/lib/Service/Object/RevertHandler.php +++ b/lib/Service/Object/RevertHandler.php @@ -81,11 +81,11 @@ class RevertHandler /** * RevertHandler constructor. * - * @param AuditTrailMapper $auditTrailMapper Audit trail mapper. - * @param ContainerInterface $container DI container. - * @param IEventDispatcher $eventDispatcher Event dispatcher. - * @param MagicMapper $objectEntityMapper Object entity mapper. - * @param PermissionHandler $permissionHandler Permission handler for RBAC. + * @param AuditTrailMapper $auditTrailMapper Audit trail mapper. + * @param ContainerInterface $container DI container. + * @param IEventDispatcher $eventDispatcher Event dispatcher. + * @param MagicMapper $objectEntityMapper Object entity mapper. + * @param PermissionHandler $permissionHandler Permission handler for RBAC. */ public function __construct( AuditTrailMapper $auditTrailMapper, @@ -119,7 +119,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean needed to control version overwrite behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-12 + * @spec openspec/specs/content-versioning/spec.md */ public function revert( string $register, @@ -147,7 +147,8 @@ public function revert( schema: $schemaEntity, action: 'update', object: $object - ) === false) { + ) === false + ) { throw new NotAuthorizedException( message: 'You do not have permission to revert this object' ); diff --git a/lib/Service/Object/SaveObject.php b/lib/Service/Object/SaveObject.php index 111dd866b8..d5d3d08cdf 100644 --- a/lib/Service/Object/SaveObject.php +++ b/lib/Service/Object/SaveObject.php @@ -47,12 +47,16 @@ use OCA\OpenRegister\Service\Object\SaveObject\LinkedEntityPropertyHandler; use OCA\OpenRegister\Service\Object\TranslationHandler; use OCA\OpenRegister\Service\Object\SaveObject\MetadataHydrationHandler; +use OCA\OpenRegister\Service\FieldEncryptionHandler; use OCA\OpenRegister\Service\OrganisationService; use OCA\OpenRegister\Service\PropertyRbacHandler; use OCA\OpenRegister\Service\TmloService; +use OCA\OpenRegister\Service\TranslationProjectionService; +use OCA\OpenRegister\Service\TranslationStatusService; use OCA\OpenRegister\Service\Schemas\SchemaCacheHandler; use OCA\OpenRegister\Service\Schemas\FacetCacheHandler; use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Db\ObjectHandling; use OCA\OpenRegister\Event\ReferenceValidatedEvent; use OCA\OpenRegister\Event\ReferenceValidationFailedEvent; use OCA\OpenRegister\Service\SettingsService; @@ -63,6 +67,7 @@ use OCP\IAppConfig; use OCP\IGroupManager; use OCP\IURLGenerator; +use OCP\IUser; use OCP\IUserSession; use Psr\Log\LoggerInterface; use Symfony\Component\Uid\Uuid; @@ -256,32 +261,38 @@ class SaveObject /** * Constructor for SaveObject handler. * - * @param MagicMapper $objectEntityMapper Object entity mapper - * @param MagicMapper $unifiedObjectMapper Unified object mapper for object operations - * @param MetadataHydrationHandler $metaHydrationHandler Handler for metadata extraction - * @param FilePropertyHandler $filePropertyHandler Handler for file property operations - * @param LinkedEntityPropertyHandler $linkedEntityHandler Linked entity property handler - * @param IUserSession $userSession User session service - * @param AuditTrailMapper $auditTrailMapper Audit trail mapper for logging changes - * @param SchemaMapper $schemaMapper Schema mapper for schema operations - * @param RegisterMapper $registerMapper Register mapper for register operations - * @param IURLGenerator $urlGenerator URL generator service - * @param OrganisationService $organisationService Service for organisation operations - * @param CacheHandler $cacheHandler Object cache service for entity and query caching - * @param SettingsService $settingsService Settings service for accessing trail settings - * @param PropertyRbacHandler $propertyRbacHandler Property-level RBAC handler - * @param ComputedFieldHandler $computedFieldHandler Handler for computed field evaluation - * @param TranslationHandler $translationHandler Handler for translation operations - * @param LoggerInterface $logger Logger interface for logging operations - * @param TmloService $tmloService TMLO archival metadata service - * @param ArrayLoader $arrayLoader Twig array loader for template rendering - * @param IGroupManager|null $groupManager Group manager for admin-bypass detection - * @param IAppConfig|null $appConfig App-config reader for the admin-bypass toggle - * @param IEventDispatcher|null $eventDispatcher Event dispatcher for reference validation events + * @param MagicMapper $objectEntityMapper Object entity mapper + * @param MagicMapper $unifiedObjectMapper Unified object mapper + * @param MetadataHydrationHandler $metaHydrationHandler Handler for metadata extraction + * @param FilePropertyHandler $filePropertyHandler Handler for file property operations + * @param LinkedEntityPropertyHandler $linkedEntityHandler Linked entity property handler + * @param IUserSession $userSession User session service + * @param AuditTrailMapper $auditTrailMapper Audit trail mapper for logging changes + * @param SchemaMapper $schemaMapper Schema mapper for schema operations + * @param RegisterMapper $registerMapper Register mapper for register operations + * @param IURLGenerator $urlGenerator URL generator service + * @param OrganisationService $organisationService Service for organisation operations + * @param CacheHandler $cacheHandler Object entity and query cache + * @param SettingsService $settingsService Settings service for trail settings + * @param PropertyRbacHandler $propertyRbacHandler Property-level RBAC handler + * @param ComputedFieldHandler $computedFieldHandler Handler for computed field evaluation + * @param TranslationHandler $translationHandler Handler for translation operations + * @param TranslationProjectionService $translationProjectionService Translation projection service + * @param TranslationStatusService $translationStatusService Translation status service + * @param LoggerInterface $logger Logger interface for logging operations + * @param TmloService $tmloService TMLO archival metadata service + * @param \OCA\OpenRegister\Service\File\FolderManagementHandler $folderManagementHandler Folder access-control gateway + * @param ArrayLoader $arrayLoader Twig array loader for template rendering + * @param IGroupManager|null $groupManager Group manager for admin-bypass detection + * @param IAppConfig|null $appConfig App-config reader (admin bypass) + * @param IEventDispatcher|null $eventDispatcher Event dispatcher (reference events) + * @param \OCA\OpenRegister\Service\ObjectSource\ObjectSourceRegistry|null $objectSourceRegistry Writable object-source provider registry + * @param FieldEncryptionHandler|null $fieldEncryptionHandler Field-level encryption handler * * @SuppressWarnings(PHPMD.ExcessiveParameterList) Nextcloud DI requires constructor injection * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + * @spec openspec/specs/field-level-encryption/spec.md#requirement-flagged-properties-are-encrypted-on-save */ public function __construct( private readonly MagicMapper $objectEntityMapper, @@ -300,12 +311,17 @@ public function __construct( private readonly PropertyRbacHandler $propertyRbacHandler, private readonly ComputedFieldHandler $computedFieldHandler, private readonly TranslationHandler $translationHandler, + private readonly TranslationProjectionService $translationProjectionService, + private readonly TranslationStatusService $translationStatusService, private readonly LoggerInterface $logger, private readonly TmloService $tmloService, + private readonly \OCA\OpenRegister\Service\File\FolderManagementHandler $folderManagementHandler, ArrayLoader $arrayLoader, private readonly ?IGroupManager $groupManager=null, private readonly ?IAppConfig $appConfig=null, private readonly ?IEventDispatcher $eventDispatcher=null, + private readonly ?\OCA\OpenRegister\Service\ObjectSource\ObjectSourceRegistry $objectSourceRegistry=null, + private readonly ?FieldEncryptionHandler $fieldEncryptionHandler=null, ) { $this->twig = new Environment($arrayLoader); }//end __construct() @@ -318,7 +334,7 @@ public function __construct( * * @return array Sub-objects indexed by UUID * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function getCreatedSubObjects(): array { @@ -333,7 +349,7 @@ public function getCreatedSubObjects(): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function clearCreatedSubObjects(): void { @@ -352,7 +368,7 @@ public function clearCreatedSubObjects(): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function clearAllCaches(): void { @@ -371,7 +387,7 @@ public function clearAllCaches(): void * * @throws DoesNotExistException If schema not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getCachedSchema(int|string $schemaId): Schema { @@ -392,7 +408,7 @@ private function getCachedSchema(int|string $schemaId): Schema * * @throws DoesNotExistException If register not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getCachedRegister(int|string $registerId): Register { @@ -415,7 +431,7 @@ private function getCachedRegister(int|string $registerId): Register * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function trackCreatedSubObject(string $uuid, array $objectData): void { @@ -439,7 +455,7 @@ public function trackCreatedSubObject(string $uuid, array $objectData): void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple resolution strategies require branching * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function resolveSchemaReference(string $reference): string|null { @@ -509,7 +525,7 @@ private function resolveSchemaReference(string $reference): string|null // Try direct slug match as last resort. try { // SchemaMapper->find() supports id, uuid, and slug via orX(). - $schema = $this->schemaMapper->find(id: $slug, published: null, _rbac: false, _multitenancy: false); + $schema = $this->schemaMapper->find(id: $slug, _rbac: false, _multitenancy: false); if ($schema !== null) { $schemaId = (string) $schema->getId(); $this->schemaCache[$schemaId] = $schema; @@ -532,7 +548,7 @@ private function resolveSchemaReference(string $reference): string|null * * @return string The reference string without query parameters * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function removeQueryParameters(string $reference): string { @@ -560,7 +576,7 @@ private function removeQueryParameters(string $reference): string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple resolution strategies require branching * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function resolveRegisterReference(string $reference): string|null { @@ -601,7 +617,7 @@ private function resolveRegisterReference(string $reference): string|null // Try direct slug match as last resort. try { // RegisterMapper->find() supports id, uuid, and slug via orX(). - $register = $this->registerMapper->find(id: $slug, published: null, _rbac: false, _multitenancy: false); + $register = $this->registerMapper->find(id: $slug, _rbac: false, _multitenancy: false); if ($register !== null) { return (string) $register->getId(); } @@ -630,7 +646,7 @@ private function resolveRegisterReference(string $reference): string|null * @SuppressWarnings(PHPMD.NPathComplexity) Multiple detection paths for different value types * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive relation scanning requires extended logic * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function scanForRelations(array $data, string $prefix='', ?Schema $schema=null): array { @@ -754,7 +770,7 @@ public function scanForRelations(array $data, string $prefix='', ?Schema $schema * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple reference pattern checks required * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function isReference(string $value): bool { @@ -821,7 +837,7 @@ private function isReference(string $value): bool * * @return ObjectEntity The updated object entity * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function updateObjectRelations(ObjectEntity $objectEntity, array $data, ?Schema $schema=null): ObjectEntity { @@ -849,7 +865,7 @@ private function updateObjectRelations(ObjectEntity $objectEntity, array $data, * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Inverse relation handling requires per-type branching * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function updateInverseRelations(ObjectEntity $savedEntity, Register $register, Schema $schema): void { @@ -930,7 +946,6 @@ private function updateInverseRelations(ObjectEntity $savedEntity, Register $reg try { $targetSchema = $this->schemaMapper->find( id: $targetSchemaSlug, - published: null, _rbac: false, _multitenancy: false ); @@ -1047,7 +1062,7 @@ private function updateInverseRelations(ObjectEntity $savedEntity, Register $reg * @SuppressWarnings(PHPMD.NPathComplexity) Multiple field types and formats require branching * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive metadata hydration logic * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function hydrateObjectMetadata(ObjectEntity $entity, Schema $schema): void { @@ -1166,7 +1181,7 @@ public function hydrateObjectMetadata(ObjectEntity $entity, Schema $schema): voi * * @return mixed The value at the path or null if not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getValueFromPath(array $data, string $path) { @@ -1208,7 +1223,7 @@ private function getValueFromPath(array $data, string $path) * @SuppressWarnings(PHPMD.NPathComplexity) Multiple property types and behaviors require branching * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive default value handling * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function setDefaultValues(ObjectEntity $objectEntity, Schema $schema, array $data): array { @@ -1362,7 +1377,7 @@ function (string $key, array $property) { * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Default value resolution requires template + type branching * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function applyAlwaysDefaults(Schema $schema, array $data): array { @@ -1408,6 +1423,113 @@ public function applyAlwaysDefaults(Schema $schema, array $data): array return $data; }//end applyAlwaysDefaults() + /** + * Auto-seed the lifecycle field from the parent on the CREATE path. + * + * When the schema declares an object-form `x-openregister-lifecycle.initial` + * (`{ from, field }`) and the lifecycle field is absent/null/empty, this + * resolves the parent reference from `data[initial.from]`, loads the parent + * through the standard read path (RBAC + multitenancy apply), and sets the + * lifecycle field to the parent's `initial.field` value. It runs BEFORE + * schema validation so a `required` lifecycle `$ref` field passes on a + * seeded create. Called from ObjectService only on the create path — never + * on update. + * + * Rules (design: `initial` object-form + auto-seed on create): + * - Empty-field-only: an explicitly provided value is NEVER overwritten. + * - Fail-soft no-op (debug log) when the parent ref is empty, the parent + * cannot be loaded, or the parent's `initial.field` value is empty. + * - The legacy literal-string `initial` form is NOT auto-seeded (no static + * behaviour change). + * - Dispatches no `ObjectTransitionedEvent` — this is initialisation. + * + * @param Schema $schema The schema whose annotation drives the seed. + * @param array $data The incoming object data (create payload). + * + * @return array The data, with the lifecycle field seeded when applicable. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each guard is a distinct fail-soft no-op branch required by the spec. + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + public function seedLifecycleFieldOnCreate(Schema $schema, array $data): array + { + $config = ($schema->getConfiguration() ?? []); + $annotation = ($config['x-openregister-lifecycle'] ?? null); + if (is_array($annotation) === false) { + return $data; + } + + // Auto-seed is driven exclusively by the object-form `initial`. + $initial = ($annotation['initial'] ?? null); + if (is_array($initial) === false) { + return $data; + } + + $fromKey = (string) ($initial['from'] ?? ''); + $parentPath = (string) ($initial['field'] ?? ''); + $field = (string) ($annotation['field'] ?? ($annotation['property'] ?? '')); + if ($fromKey === '' || $parentPath === '' || $field === '') { + return $data; + } + + // Empty-field-only: never overwrite a client-supplied value. + $currentValue = ($data[$field] ?? null); + if ($currentValue !== null && $currentValue !== '') { + return $data; + } + + // Resolve the parent reference off the create payload. + $parentRef = (string) ($data[$fromKey] ?? ''); + if ($parentRef === '') { + $this->logger->debug( + message: '[SaveObject] Lifecycle seed no-op: parent reference is empty', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'field' => $field, + 'from' => $fromKey, + ] + ); + return $data; + } + + // Load the parent through the standard read path (RBAC + multitenancy). + try { + $parent = $this->unifiedObjectMapper->find(identifier: $parentRef); + } catch (\Throwable $e) { + $this->logger->debug( + message: '[SaveObject] Lifecycle seed no-op: parent could not be loaded: '.$e->getMessage(), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'field' => $field, + 'parentRef' => $parentRef, + ] + ); + return $data; + } + + $parentData = ($parent->getObject() ?? []); + $seedValue = ($parentData[$parentPath] ?? null); + if ($seedValue === null || $seedValue === '') { + $this->logger->debug( + message: '[SaveObject] Lifecycle seed no-op: parent initial value is empty', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'field' => $field, + 'parentPath' => $parentPath, + ] + ); + return $data; + } + + $data[$field] = $seedValue; + + return $data; + }//end seedLifecycleFieldOnCreate() + /** * Apply property default values to a data array (for use in bulk save paths). * @@ -1420,7 +1542,7 @@ public function applyAlwaysDefaults(Schema $schema, array $data): array * * @return array The data with defaults applied. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function applyPropertyDefaults(Schema $schema, array $data): array { @@ -1476,7 +1598,7 @@ public function applyPropertyDefaults(Schema $schema, array $data): array * * @return bool True if the default should be applied. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function shouldApplyDefault(string $behavior, array $data, string $key): bool { @@ -1510,7 +1632,7 @@ private function shouldApplyDefault(string $behavior, array $data, string $key): * * @return mixed The resolved value, or null if resolution failed. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function resolveDefaultTemplateValue($defaultValue, array $context, array $schemaProperties) { @@ -1558,7 +1680,7 @@ private function resolveDefaultTemplateValue($defaultValue, array $context, arra * * @return null|string The generated slug or null if no slug could be generated * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function generateSlug(array $data, Schema $schema): string|null { @@ -1596,7 +1718,7 @@ private function generateSlug(array $data, Schema $schema): string|null * * @return string The generated slug * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function createSlug(string $text): string { @@ -1643,7 +1765,7 @@ private function createSlug(string $text): string * @SuppressWarnings(PHPMD.NPathComplexity) Multiple cascading paths and configurations * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive cascading for objects and arrays * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function cascadeObjects(ObjectEntity $objectEntity, Schema $schema, array $data): array { @@ -1699,8 +1821,8 @@ function (array $property) { || (($property['items']['inversedBy'] ?? null) !== null); $objHandling = $property['objectConfiguration']['handling'] ?? null; $itemsHandling = $property['items']['objectConfiguration']['handling'] ?? null; - $hasCascade = $objHandling === 'cascade' || $objHandling === 'related-object' - || $itemsHandling === 'cascade' || $itemsHandling === 'related-object'; + $hasCascade = $objHandling === 'cascade' || ObjectHandling::relates($objHandling) === true + || $itemsHandling === 'cascade' || ObjectHandling::relates($itemsHandling) === true; return $property['type'] === 'array' && $hasRef && ($hasInversedBy || $hasCascade); } @@ -1772,7 +1894,7 @@ function (array $property) { // Determine handling type for orphan cleanup. $objHandling = $definition['objectConfiguration']['handling'] ?? null; $itemsHandling = $definition['items']['objectConfiguration']['handling'] ?? null; - $isRelatedObject = $objHandling === 'related-object' || $itemsHandling === 'related-object'; + $isRelatedObject = ObjectHandling::relates($objHandling) === true || ObjectHandling::relates($itemsHandling) === true; $isCascade = $objHandling === 'cascade' || $itemsHandling === 'cascade'; // Capture old UUIDs from the existing object for orphan detection. @@ -1953,7 +2075,7 @@ function ($item) { * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex array object cascading logic * @SuppressWarnings(PHPMD.NPathComplexity) Multiple validation and processing paths * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function cascadeMultipleObjects(ObjectEntity $objectEntity, array $property, array $propData): array { @@ -2084,7 +2206,7 @@ function ($object) { * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex single object cascading with relation handling * @SuppressWarnings(PHPMD.NPathComplexity) Multiple configuration and validation paths * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function cascadeSingleObject(ObjectEntity $objectEntity, array $definition, array $object): ?string { @@ -2200,7 +2322,7 @@ private function cascadeSingleObject(ObjectEntity $objectEntity, array $definiti * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function deleteOrphanedRelatedObjects( array $orphanedUuids, @@ -2278,7 +2400,7 @@ private function deleteOrphanedRelatedObjects( * * @return array The data with all schema properties present (missing ones set to null). * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function fillMissingSchemaPropertiesWithNull(array $data, int|string $schemaId): array { @@ -2322,7 +2444,7 @@ private function fillMissingSchemaPropertiesWithNull(array $data, int|string $sc * @SuppressWarnings(PHPMD.NPathComplexity) Multiple property and item level configurations * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive write-back handling for all relation types * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function handleInverseRelationsWriteBack(ObjectEntity $objectEntity, Schema $schema, array $data): array { @@ -2536,7 +2658,7 @@ function ($uuid) { * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex sanitization logic for multiple property types * @SuppressWarnings(PHPMD.NPathComplexity) Multiple property types and required states * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function sanitizeEmptyStringsForObjectProperties(array $data, Schema $schema): array { @@ -2636,6 +2758,7 @@ private function sanitizeEmptyStringsForObjectProperties(array $data, Schema $sc * @param bool $silent Whether to skip audit trail creation and events (default: false). * @param bool $_validation Whether to validate the object (default: true). * @param array|null $uploadedFiles Uploaded files array (optional). + * @param IUser|null $currentUser Explicit acting user for `@self.folder` access checks; falls back to the session user when null. * * @return ObjectEntity The saved object entity. * @@ -2644,7 +2767,7 @@ private function sanitizeEmptyStringsForObjectProperties(array $data, Schema $sc * @SuppressWarnings(PHPMD.ExcessiveParameterList) Required for flexible save options * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags needed for flexible save behavior * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function saveObject( Register | int | string | null $register, @@ -2657,7 +2780,8 @@ public function saveObject( bool $persist=true, bool $silent=false, bool $_validation=true, - ?array $uploadedFiles=null + ?array $uploadedFiles=null, + ?IUser $currentUser=null ): ObjectEntity { // Extract UUID and @self metadata from data. [$uuid, $selfData, $data] = $this->extractUuidAndSelfData( @@ -2672,6 +2796,29 @@ public function saveObject( register: $register ); + // Read-only projection guard: a schema served from an external source + // (x-openregister-object-source) is read-only — the external system stays + // authoritative and OpenRegister must never become a second write path. + $objectSource = $schema->getObjectSource(); + if ($persist === true && $objectSource !== null) { + // Opt-in write-through (dbal-virtual-registers-crud): delegate to a + // WritableObjectSourceProvider when the schema annotation carries + // `readOnly: false` — the provider re-verifies its backing source's + // writable flag live and fails closed. Everything else keeps the + // v1 read-only rejection. RBAC (create/update) and schema + // validation already ran upstream in ObjectService before this + // dispatch, so the external system is only reached for an + // authorized, valid write. + return $this->delegateObjectSourceWrite( + register: $register, + schema: $schema, + objectSource: $objectSource, + data: $data, + uuid: $uuid, + silent: $silent + ); + } + // Normalize translatable properties (wrap simple values under default language). $data = $this->translationHandler->normalizeTranslationsForSave( objectData: $data, @@ -2782,7 +2929,8 @@ public function saveObject( selfData: $selfData, folderId: $folderId, persist: $persist, - silent: $silent + silent: $silent, + currentUser: $currentUser ); } finally { $this->popSaveCallFrame(key: $frameKey); @@ -2827,7 +2975,8 @@ public function saveObject( folderId: $folderId, persist: $persist, silent: $silent, - _multitenancy: $_multitenancy + _multitenancy: $_multitenancy, + currentUser: $currentUser ); } finally { $this->popSaveCallFrame(key: $frameKey); @@ -2843,7 +2992,7 @@ public function saveObject( * * @return array{0: string|null, 1: array, 2: array} [uuid, selfData, cleanedData] * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function extractUuidAndSelfData( array $data, @@ -2893,7 +3042,7 @@ private function extractUuidAndSelfData( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple type resolution paths for schema and register * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function resolveSchemaAndRegister( Schema | int | string $schema, @@ -2963,7 +3112,7 @@ private function resolveSchemaAndRegister( * * @throws Exception If object is locked by another user. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function findAndValidateExistingObject( string $uuid, @@ -3018,10 +3167,11 @@ private function findAndValidateExistingObject( * @param int|null $folderId Folder ID * @param bool $persist Whether to persist changes * @param bool $silent Whether to skip audit trail + * @param IUser|null $currentUser Explicit acting user for `@self.folder` access checks (forwarded to setSelfMetadata) * * @return ObjectEntity Updated object * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function handleObjectUpdate( ObjectEntity $existingObject, @@ -3031,7 +3181,8 @@ private function handleObjectUpdate( array $selfData, ?int $folderId, bool $persist, - bool $silent + bool $silent, + ?IUser $currentUser=null ): ObjectEntity { // Check archival immutability: destroyed and transferred objects cannot be modified. $retention = $existingObject->getRetention() ?? []; @@ -3062,7 +3213,8 @@ private function handleObjectUpdate( schema: $schema, data: $data, selfData: $selfData, - folderId: $folderId + folderId: $folderId, + currentUser: $currentUser ); // If not persisting, return the prepared object. @@ -3096,6 +3248,7 @@ private function handleObjectUpdate( * @param bool $persist Whether to persist changes * @param bool $silent Whether to skip audit trail * @param bool $_multitenancy Whether to apply multitenancy + * @param IUser|null $currentUser Explicit acting user for `@self.folder` access checks (forwarded to setSelfMetadata) * * @return ObjectEntity Created object * @@ -3103,7 +3256,7 @@ private function handleObjectUpdate( * * @SuppressWarnings(PHPMD.ExcessiveParameterList) Required for flexible object creation * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function handleObjectCreation( int $registerId, @@ -3116,7 +3269,8 @@ private function handleObjectCreation( ?int $folderId, bool $persist, bool $silent, - bool $_multitenancy + bool $_multitenancy, + ?IUser $currentUser=null ): ObjectEntity { // Create a new object entity. $objectEntity = new ObjectEntity(); @@ -3140,7 +3294,8 @@ private function handleObjectCreation( schema: $schema, data: $data, selfData: $selfData, - _multitenancy: $_multitenancy + _multitenancy: $_multitenancy, + currentUser: $currentUser ); // Apply archival metadata from schema archive configuration. @@ -3208,7 +3363,7 @@ private function handleObjectCreation( * * @throws Exception If file processing fails * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function processFilePropertiesWithRollback( ObjectEntity $savedEntity, @@ -3332,7 +3487,7 @@ private function processFilePropertiesWithRollback( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function clearImageMetadataIfFileProperty( ObjectEntity $savedEntity, @@ -3364,6 +3519,7 @@ private function clearImageMetadataIfFileProperty( * @param array $data The object data. * @param array $selfData The @self metadata. * @param bool $_multitenancy Whether to apply multitenancy filtering. + * @param IUser|null $currentUser Explicit acting user for `@self.folder` access checks (forwarded to setSelfMetadata). * * @return ObjectEntity The prepared object entity. * @@ -3373,17 +3529,18 @@ private function clearImageMetadataIfFileProperty( * @SuppressWarnings(PHPMD.NPathComplexity) Multiple optional configuration paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive preparation requires extended logic * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function prepareObjectForCreation( ObjectEntity $objectEntity, Schema $schema, array $data, array $selfData, - bool $_multitenancy + bool $_multitenancy, + ?IUser $currentUser=null ): ObjectEntity { // Set @self metadata properties. - $this->setSelfMetadata(objectEntity: $objectEntity, selfData: $selfData, data: $data); + $this->setSelfMetadata(objectEntity: $objectEntity, selfData: $selfData, data: $data, currentUser: $currentUser); // Set UUID if provided, otherwise generate a new one. if ($objectEntity->getUuid() === null) { @@ -3425,7 +3582,7 @@ private function prepareObjectForCreation( $this->linkedEntityHandler->extractAndPopulate($objectEntity, $schema, $preparedData); // Populate TMLO archival metadata defaults if register has TMLO enabled. - $this->populateTmloDefaults(objectEntity: $objectEntity, schema: $schema, selfData: $selfData); + $this->populateTmloDefaults(objectEntity: $objectEntity, schema: $schema); // Set owner from the active user session, or fall back to the system // identifier when no session is active. See applyOwnerAttribution() @@ -3433,11 +3590,11 @@ private function prepareObjectForCreation( $this->applyOwnerAttribution(objectEntity: $objectEntity); // Set organisation from active organisation if not already set. - // Always respect user's active organisation regardless of multitenancy settings. - // BUT: Don't override if organisation was explicitly set via @self metadata (e.g., for organization activation). - if (($objectEntity->getOrganisation() === null || $objectEntity->getOrganisation() === '') - && isset($selfData['organisation']) === false - ) { + // setSelfMetadata() (called above) only accepted a client-supplied @self.organisation + // value when the caller was admin or a verified member of that organisation (SB1 fix). + // If setOrganisation() was NOT called there (non-member caller, or no @self.organisation + // provided at all), stamp the caller's active organisation here as the authoritative value. + if ($objectEntity->getOrganisation() === null || $objectEntity->getOrganisation() === '') { $organisationUuid = $this->organisationService->getOrganisationForNewEntity(); $objectEntity->setOrganisation($organisationUuid); } @@ -3503,22 +3660,24 @@ private function applyOwnerAttribution(ObjectEntity $objectEntity): void * @param array $data The updated object data. * @param array $selfData The @self metadata. * @param int|null $folderId The folder ID to set on the object. + * @param IUser|null $currentUser Explicit acting user for `@self.folder` access checks (forwarded to setSelfMetadata). * * @return ObjectEntity The prepared object entity. * * @throws Exception If there is an error during preparation. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function prepareObjectForUpdate( ObjectEntity $existingObject, Schema $schema, array $data, array $selfData, - ?int $folderId + ?int $folderId, + ?IUser $currentUser=null ): ObjectEntity { // Set @self metadata properties. - $this->setSelfMetadata(objectEntity: $existingObject, selfData: $selfData, data: $data); + $this->setSelfMetadata(objectEntity: $existingObject, selfData: $selfData, data: $data, currentUser: $currentUser); // Set folder ID if provided. if ($folderId !== null) { @@ -3588,16 +3747,21 @@ private function prepareObjectForUpdate( * @param ObjectEntity $objectEntity The object entity to set metadata on. * @param array $selfData The @self metadata. * @param array $data The object data (for generated values like slug). + * @param IUser|null $currentUser Explicit acting user for the `@self.folder` access check; falls back to the session user when null. * * @return void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex metadata extraction from multiple sources * @SuppressWarnings(PHPMD.NPathComplexity) Multiple optional metadata fields with validation * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ - private function setSelfMetadata(ObjectEntity $objectEntity, array $selfData, array $data=[]): void - { + private function setSelfMetadata( + ObjectEntity $objectEntity, + array $selfData, + array $data=[], + ?IUser $currentUser=null + ): void { // Extract and set slug property if present (check both @self and data). $slug = $selfData['slug'] ?? $data['slug'] ?? null; if (empty($slug) === false) { @@ -3609,34 +3773,111 @@ private function setSelfMetadata(ObjectEntity $objectEntity, array $selfData, ar // stamps the session user's UID (or the configured system identifier for // background jobs) AFTER this method returns. Accepting an owner value here // would allow any caller to forge object ownership: - // - For authenticated REST requests applyOwnerAttribution() would override it, - // but the defence-in-depth is still worthwhile. - // - For background / system contexts (no IUserSession user) applyOwnerAttribution - // only fills in owner when it is empty, so a client-supplied value would - // persist — that is the actual attack vector closed by this change. + // - For authenticated REST requests applyOwnerAttribution() would override it, + // but the defence-in-depth is still worthwhile. + // - For background / system contexts (no IUserSession user) applyOwnerAttribution + // only fills in owner when it is empty, so a client-supplied value would + // persist — that is the actual attack vector closed by this change. + // SECURITY (wave-11 SB1): organisation must only be accepted from @self when the + // caller is an admin or has verified membership in that organisation. + // Blindly applying a client-supplied organisation UUID allows any authenticated + // user to plant data inside another tenant's view (cross-tenant data injection). + if (array_key_exists('organisation', $selfData) === true + && empty($selfData['organisation']) === false + ) { + $sessionUser = $this->userSession->getUser(); + $isAdmin = $sessionUser !== null + && $this->groupManager !== null + && $this->groupManager->isAdmin($sessionUser->getUID()) === true; + + // Only allow admin, or callers who actually belong to the requested organisation. + if ($isAdmin === true + || $this->organisationService->hasAccessToOrganisation($selfData['organisation']) === true + ) { + $objectEntity->setOrganisation($selfData['organisation']); + } - if (array_key_exists('organisation', $selfData) === true && empty($selfData['organisation']) === false) { - $objectEntity->setOrganisation($selfData['organisation']); - } + // Non-member callers silently fall through; prepareObjectForCreation() will + // stamp the caller's active organisation via getOrganisationForNewEntity(). + } + + // Propagate @self.folder so the access-control check governs the bind + // (numeric → access-checked, empty/legacy → auto-create). Without + // propagation a user-supplied @self.folder is silently dropped — + // defeating the whole `self-folder-access-control` capability. + if (array_key_exists('folder', $selfData) === true && empty($selfData['folder']) === false) { + $folderValue = (string) $selfData['folder']; + + // For folder values that are a bare positive integer node id (the + // format produced by explicit `@self.folder` writes), require that + // the acting user can read the target folder — otherwise reject the + // bind with `FolderAccessDeniedException`. We gate on `ctype_digit` + // rather than `is_numeric` so floats / scientific-notation / signed + // strings (e.g. "42.5", "4e2", "+42") don't slip through and then + // get `(int)`-truncated to a *different* node than was validated. + // Legacy non-numeric values fall through to the auto-create path. + if (ctype_digit($folderValue) === true) { + // Pass `$currentUser` through so the access check uses the + // SAME user identity that the downstream + // `createObjectFolderById` check uses on the lazy-init path. + // When `$currentUser` is null (HTTP path), both checks fall + // back to `IUserSession::getUser()` and agree by default. + // When `$currentUser` is non-null (DI / event listener path), + // both checks use it explicitly — no chance of one site + // passing while another denies because of session vs explicit + // user disagreement. + $this->folderManagementHandler->assertFolderIsAccessible( + folderId: $folderValue, + currentUser: $currentUser, + objectEntity: $objectEntity + ); + + // Normalise to the canonical integer string so the persisted + // value matches the node that was validated (no "42.5" vs "42" + // drift in audit / analytics / equality checks). + $folderValue = (string) (int) $folderValue; + }//end if - // Set TMLO metadata from @self if provided. - if (array_key_exists('tmlo', $selfData) === true && is_array($selfData['tmlo']) === true) { - $objectEntity->setTmlo($selfData['tmlo']); + $objectEntity->setFolder($folderValue); + }//end if + + // Propagate @self.geo — the object's geographical metadata (a GeoJSON-ish + // array rendered/edited by map widgets, e.g. CnObjectGeoWidget). Unlike + // organisation/tmlo this is plain descriptive data driving no security or + // lifecycle decisions, so it is client-writable. An explicit null clears it. + if (array_key_exists('geo', $selfData) === true) { + $geoValue = $selfData['geo']; + if ($geoValue === null || is_array($geoValue) === true) { + $objectEntity->setGeo($geoValue); + } } + + // SECURITY (wave-11 WF1): TMLO fields (@self.tmlo) must NOT be accepted verbatim + // from client input on CREATE. The destruction pipeline keys off archiefstatus; + // a client-submitted {"archiefstatus":"vernietigd"} would mark an object as + // destruction-eligible bypassing the validated state machine. populateTmloDefaults() + // (called after this method) will apply the correct system defaults including + // resetting archiefstatus to 'actief' for new objects, overriding any client value. + // On UPDATE, validateTmloOnUpdate() enforces the allowed transition matrix. + // Therefore: strip @self.tmlo entirely here; let populateTmloDefaults() own it. + // (No setTmlo() call — the field is intentionally omitted). }//end setSelfMetadata() /** * Populate TMLO defaults on a new object if the register has TMLO enabled. * + * Client-supplied @self.tmlo is intentionally NOT accepted here (wave-11 WF1 fix). + * All TMLO fields on CREATE are system-managed defaults applied by TmloService. + * On UPDATE, validateTmloOnUpdate() enforces the allowed transition matrix instead. + * * @param ObjectEntity $objectEntity The object entity being created * @param Schema $schema The schema for TMLO defaults - * @param array $selfData The @self metadata from the request * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ - private function populateTmloDefaults(ObjectEntity $objectEntity, Schema $schema, array $selfData): void + private function populateTmloDefaults(ObjectEntity $objectEntity, Schema $schema): void { $registerId = $objectEntity->getRegister(); if ($registerId === null) { @@ -3653,12 +3894,14 @@ private function populateTmloDefaults(ObjectEntity $objectEntity, Schema $schema return; } - // If TMLO data was explicitly provided via @self, use it as the starting point. - if (array_key_exists('tmlo', $selfData) === true && is_array($selfData['tmlo']) === true) { - $objectEntity->setTmlo($selfData['tmlo']); - } - - // Validate field values before populating. + // SECURITY (wave-11 WF1): Do NOT seed TMLO from client-supplied @self.tmlo on CREATE. + // The destruction pipeline keys off archiefstatus; a client-submitted + // {"archiefstatus":"vernietigd"} would bypass the validated state machine and + // mark the object destruction-eligible immediately. On CREATE, all TMLO fields + // are system-managed defaults set by tmloService->populateDefaults() below. + // On UPDATE, the separate validateTmloOnUpdate() path enforces the transition matrix. + // Validate field values before populating (entity may have leftover TMLO from a prior + // update path; ensure they are valid before stamping defaults). $currentTmlo = $objectEntity->getTmlo(); if (is_array($currentTmlo) === true && empty($currentTmlo) === false) { $errors = $this->tmloService->validateFieldValues($currentTmlo); @@ -3680,7 +3923,7 @@ private function populateTmloDefaults(ObjectEntity $objectEntity, Schema $schema * * @throws Exception If TMLO validation fails * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function validateTmloOnUpdate(ObjectEntity $existingObject, array $selfData): void { @@ -3732,7 +3975,7 @@ private function validateTmloOnUpdate(ObjectEntity $existingObject, array $selfD * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple property type checks * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function validateReferences( Schema $schema, @@ -3887,7 +4130,7 @@ private function validateReferences( * @return string|null Returns `'error'` for strict mode, `'warn'` for * warn-only, `null` when validation is disabled. * - * @spec openspec/changes/reference-existence-validation/tasks.md + * @spec openspec/specs/reference-existence-validation/spec.md */ private function resolveReferenceStrictness(array $property): ?string { @@ -4046,7 +4289,7 @@ private function validateExternalUrlSyntax( * * @throws ValidationException If the referenced object does not exist (HTTP 422). * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function validateReferenceExists( string $propertyName, @@ -4243,7 +4486,7 @@ private function validateReferenceExists( * * @return void * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-14 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function clearReferenceValidationCache(): void { @@ -4291,7 +4534,7 @@ public function clearReferenceValidationCache(): void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-14 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function saveObjectsStreaming( Register | int | string | null $register, @@ -4408,7 +4651,7 @@ public function saveObjectsStreaming( * * @return string|null The stack frame key, or null when nothing was pushed. * - * @spec openspec/changes/reference-existence-validation/tasks.md + * @spec openspec/specs/reference-existence-validation/spec.md */ private function pushSaveCallFrame(string $schemaSlug, string $uuid, ?string $register): ?string { @@ -4446,7 +4689,7 @@ private function pushSaveCallFrame(string $schemaSlug, string $uuid, ?string $re * * @return void * - * @spec openspec/changes/reference-existence-validation/tasks.md + * @spec openspec/specs/reference-existence-validation/spec.md */ private function popSaveCallFrame(?string $key): void { @@ -4498,7 +4741,7 @@ private function popSaveCallFrame(?string $key): void * @return array|null * Cycle path when detected, null otherwise. * - * @spec openspec/changes/reference-existence-validation/tasks.md + * @spec openspec/specs/reference-existence-validation/spec.md */ private function detectCircularReference(string $uuid): ?array { @@ -4662,7 +4905,7 @@ private function dispatchReferenceValidationFailedEvent( * * @throws Exception If there is an error during preparation. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function prepareObjectData(ObjectEntity $objectEntity, Schema $schema, array $data): array { @@ -4702,6 +4945,18 @@ private function prepareObjectData(ObjectEntity $objectEntity, Schema $schema, a ); } + // Encrypt properties flagged `x-openregister-encrypted: true` (field-level- + // object-encryption). This is the LAST step before the entity is persisted — + // every transform above (cascading, defaults, computed fields) needs the + // plaintext value to operate correctly, and nothing after this point needs + // it: relation scanning treats an opaque ciphertext string like any other + // non-reference scalar, and the mapper insert/update writes exactly what it + // is given. Schema validation already ran on plaintext upstream in + // ObjectService::saveObject(), before this handler is ever invoked. + if ($this->fieldEncryptionHandler !== null) { + $data = $this->fieldEncryptionHandler->encryptProperties(data: $data, schema: $schema); + } + return $data; }//end prepareObjectData() @@ -4724,7 +4979,7 @@ private function prepareObjectData(ObjectEntity $objectEntity, Schema $schema, a * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function preCacheParentName(ObjectEntity $objectEntity, Schema $schema, array $data): void { @@ -4788,7 +5043,7 @@ private function preCacheParentName(ObjectEntity $objectEntity, Schema $schema, * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive update with file handling * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Silent flag needed for audit trail control * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function updateObject( Register | int | string $register, @@ -4878,6 +5133,24 @@ public function updateObject( oldEntity: $oldObject ); + // I18n-source-of-truth: when a translatable property's source-language + // value changes, flip every derived translation row to `outdated` so + // editorial tooling surfaces the staleness immediately. Conservative — + // only triggers when the source value itself changed; per-language + // edits on non-source languages do NOT flip status. + try { + $this->flagOutdatedDerivedTranslations( + schema: $schema, + register: $register, + oldObject: $oldObject, + newObject: $updatedEntity + ); + } catch (\Throwable $e) { + $this->logger->warning( + '[SaveObject] failed to flag outdated translations: '.$e->getMessage() + ); + } + $this->logger->info( message: '[SaveObject] Object updated successfully', context: [ @@ -4895,7 +5168,11 @@ public function updateObject( } // Handle file properties - process them and replace content with file IDs. - $filePropsProcessed = false; + // BUG-OBJ-8: collect the names of the properties the file handler rewrote so + // we can overlay ONLY those file-id replacements onto the merged/prepared body + // below, instead of clobbering the whole object with the raw partial $data. + $filePropsProcessed = false; + $processedFileProperties = []; foreach ($data as $propertyName => $value) { $isFileProperty = $this->filePropertyHandler->isFileProperty( value: $value, @@ -4909,13 +5186,27 @@ public function updateObject( propertyName: $propertyName, schema: $schema ); - $filePropsProcessed = true; + $filePropsProcessed = true; + $processedFileProperties[] = $propertyName; } } // Update the object with the modified data (file IDs instead of content). if ($filePropsProcessed === true) { - $updatedEntity->setObject($data); + // BUG-OBJ-8: build the post-file body from the MERGED/prepared object that + // was just persisted (which contains computed fields, defaults, cascaded + // sub-objects, null-fills, and previously-stored fields not present in this + // request), then overlay only the file-id replacements computed by the file + // handler. Using the raw partial $data here would drop every field absent + // from the request — data loss that depends on payload shape. + $mergedObject = $updatedEntity->getObject() ?? []; + foreach ($processedFileProperties as $fileProperty) { + if (array_key_exists($fileProperty, $data) === true) { + $mergedObject[$fileProperty] = $data[$fileProperty]; + } + } + + $updatedEntity->setObject($mergedObject); // Clear image metadata if objectImageField points to a file property. // This ensures the image URL is extracted from the file object during rendering. @@ -4948,6 +5239,113 @@ public function updateObject( return $updatedEntity; }//end updateObject() + /** + * Compare old vs new object values for translatable properties and flip + * derived-language translation rows to `outdated` when the resolved + * source-language value changes. + * + * Conservative trigger: only fires when the source-language value + * itself changed. Edits to other-language values do not flip status. + * + * @param Schema $schema The schema for the object. + * @param Register $register The owning register (for default language fallback). + * @param ObjectEntity $oldObject The pre-persist state. + * @param ObjectEntity $newObject The post-persist state. + * + * @return void + * + * @spec openspec/specs/i18n-source-of-truth/spec.md + */ + private function flagOutdatedDerivedTranslations( + Schema $schema, + Register $register, + ObjectEntity $oldObject, + ObjectEntity $newObject + ): void { + $uuid = $newObject->getUuid(); + if ($uuid === null || $uuid === '') { + return; + } + + $translatableProps = $this->translationHandler->getTranslatableProperties($schema); + if (count($translatableProps) === 0) { + return; + } + + $oldData = (array) ($oldObject->getObject() ?? []); + $newData = (array) ($newObject->getObject() ?? []); + + $registerDefault = $register->getDefaultLanguage(); + foreach ($translatableProps as $property) { + $sourceLanguage = $this->translationProjectionService->resolveSourceLanguage( + schema: $schema, + object: $newObject, + property: $property, + registerDefault: $registerDefault + ); + + $oldSource = $this->extractLanguageValue( + value: $oldData[$property] ?? null, + language: $sourceLanguage + ); + $newSource = $this->extractLanguageValue( + value: $newData[$property] ?? null, + language: $sourceLanguage + ); + + if ($oldSource === null || $newSource === null) { + continue; + } + + if ($oldSource === $newSource) { + continue; + } + + $this->translationStatusService->markDerivedTranslationsOutdated( + objectUuid: (string) $uuid, + property: (string) $property, + sourceLanguage: $sourceLanguage + ); + }//end foreach + }//end flagOutdatedDerivedTranslations() + + /** + * Extract the value for a given language out of a translatable property + * payload. Accepts both language-keyed objects (`{nl: "x"}`) and scalar + * values (interpreted as the source language). + * + * @param mixed $value The raw property value from the object body. + * @param string $language The language tag to extract for. + * + * @return string|null The string value, or null when not present. + * + * @spec openspec/specs/i18n-source-of-truth/spec.md + */ + private function extractLanguageValue(mixed $value, string $language): ?string + { + if ($value === null) { + return null; + } + + if (is_string($value) === true) { + // Scalar bodies are treated as the source-language value. + return $value; + } + + if (is_array($value) === true && isset($value[$language]) === true) { + $entry = $value[$language]; + if (is_string($entry) === true) { + return $entry; + } + + if (is_scalar($entry) === true) { + return (string) $entry; + } + } + + return null; + }//end extractLanguageValue() + /** * Check if an object is effectively empty (contains only empty values) * @@ -4959,7 +5357,7 @@ public function updateObject( * * @return bool True if the object is effectively empty, false otherwise * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function isEffectivelyEmptyObject(array $object): bool { @@ -4994,7 +5392,7 @@ private function isEffectivelyEmptyObject(array $object): bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple value type checks required * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function isValueNotEmpty($value): bool { @@ -5039,7 +5437,7 @@ private function isValueNotEmpty($value): bool * * @return bool True if audit trails are enabled, false otherwise * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function isAuditTrailsEnabled(): bool { @@ -5055,4 +5453,98 @@ private function isAuditTrailsEnabled(): bool return true; } }//end isAuditTrailsEnabled() + + /** + * Delegate a persisting save on an object-source schema to its writable provider. + * + * The v1 read-only rejection is preserved for every case that is not an + * explicit, currently-valid opt-in: annotation `readOnly` missing or not + * `false`, provider missing or not writable, register unresolvable. The + * provider itself re-verifies the backing source's writable flag at write + * time (fail closed), so a stale annotation can never authorize a write. + * + * @param Register|null $register The resolved register. + * @param Schema $schema The sourced schema. + * @param array $objectSource The `x-openregister-object-source` annotation. + * @param array $data The validated object data. + * @param string|null $uuid The object id for updates, null for creates. + * @param bool $silent Whether to skip audit trail creation. + * + * @return ObjectEntity The written virtual object as returned by the provider. + * + * @throws \RuntimeException When the schema is not writable (v1 rejection). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function delegateObjectSourceWrite( + ?Register $register, + Schema $schema, + array $objectSource, + array $data, + ?string $uuid, + bool $silent + ): ObjectEntity { + $provider = null; + if ($this->objectSourceRegistry !== null) { + $provider = $this->objectSourceRegistry->get((string) $objectSource['provider']); + } + + $writableOptIn = (($objectSource['readOnly'] ?? true) === false); + $writable = ($provider instanceof \OCA\OpenRegister\Service\ObjectSource\WritableObjectSourceProvider); + + if ($writableOptIn === false || $writable === false || $register instanceof Register === false) { + throw new \RuntimeException( + sprintf( + 'Schema "%s" is a read-only projection of object-source provider "%s"; writes are not allowed.', + (string) $schema->getSlug(), + $objectSource['provider'] + ) + ); + } + + $config = ($objectSource['config'] ?? []); + + if ($uuid === null || $uuid === '') { + $entity = $provider->insert(register: $register, schema: $schema, data: $data, config: $config); + $this->recordObjectSourceAudit(old: null, new: $entity, action: 'create', silent: $silent); + return $entity; + } + + $old = $provider->find(register: $register, schema: $schema, id: $uuid, config: $config); + $entity = $provider->update(register: $register, schema: $schema, id: $uuid, data: $data, config: $config); + $this->recordObjectSourceAudit(old: $old, new: $entity, action: 'update', silent: $silent); + + return $entity; + }//end delegateObjectSourceWrite() + + /** + * Record an audit-trail row for an external write (best effort, design D6). + * + * An audit failure must never mask a successful external write; it degrades + * to a structured secret-free warning. + * + * @param ObjectEntity|null $old The pre-write entity (null on create). + * @param ObjectEntity $new The post-write entity. + * @param string $action The action (`create`|`update`). + * @param bool $silent Whether audit creation is suppressed. + * + * @return void + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function recordObjectSourceAudit(?ObjectEntity $old, ObjectEntity $new, string $action, bool $silent): void + { + if ($silent === true) { + return; + } + + try { + $this->auditTrailMapper->createAuditTrail(old: $old, new: $new, action: $action); + } catch (\Throwable $e) { + $this->logger->warning( + '[SaveObject] audit trail for external '.$action.' on uuid '.((string) $new->getUuid()).' could not be recorded: '.$e->getMessage(), + ['file' => __FILE__, 'line' => __LINE__] + ); + } + }//end recordObjectSourceAudit() }//end class diff --git a/lib/Service/Object/SaveObject/ComputedFieldHandler.php b/lib/Service/Object/SaveObject/ComputedFieldHandler.php index 229f836ffc..aae62eb9bd 100644 --- a/lib/Service/Object/SaveObject/ComputedFieldHandler.php +++ b/lib/Service/Object/SaveObject/ComputedFieldHandler.php @@ -78,7 +78,7 @@ class ComputedFieldHandler * @param MappingRuntimeLoader $mappingRuntimeLoader Twig runtime loader for mapping functions. * @param LoggerInterface $logger Logger for error and debug messages. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly MagicMapper $objectMapper, @@ -98,7 +98,7 @@ public function __construct( * * @return Environment The configured Twig environment * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function getTwig(): Environment { @@ -176,7 +176,7 @@ private function getTwig(): Environment * * @return array The object data with computed field values added/updated. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function evaluateComputedFields(array $data, Schema $schema, string $evaluateOn='save'): array { @@ -226,7 +226,7 @@ public function evaluateComputedFields(array $data, Schema $schema, string $eval * * @return mixed The computed value, or null on error. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function evaluateExpression( string $expression, @@ -283,7 +283,7 @@ private function evaluateExpression( * * @return array The Twig context with data and resolved references. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function buildTwigContext(array $data, Schema $schema, int $depth=0): array { @@ -312,7 +312,7 @@ private function buildTwigContext(array $data, Schema $schema, int $depth=0): ar * * @return array Resolved reference data indexed by property name. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function resolveReferences(array $data, Schema $schema, int $depth): array { @@ -391,7 +391,7 @@ private function resolveReferences(array $data, Schema $schema, int $depth): arr * * @return mixed The cast result. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function castResult(string $result): mixed { @@ -423,7 +423,7 @@ private function castResult(string $result): mixed * * @return bool True if the schema has at least one computed property. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function hasComputedProperties(Schema $schema): bool { @@ -446,7 +446,7 @@ public function hasComputedProperties(Schema $schema): bool * * @return array List of property names that are computed for the given mode. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getComputedPropertyNames(Schema $schema, string $evaluateOn='save'): array { diff --git a/lib/Service/Object/SaveObject/FilePropertyHandler.php b/lib/Service/Object/SaveObject/FilePropertyHandler.php index db036898c1..b7b3190dda 100644 --- a/lib/Service/Object/SaveObject/FilePropertyHandler.php +++ b/lib/Service/Object/SaveObject/FilePropertyHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object\SaveObject; @@ -23,6 +23,7 @@ use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Service\FileService; +use OCA\OpenRegister\Service\SecurityService; use OCP\Files\File; use Psr\Log\LoggerInterface; @@ -56,7 +57,7 @@ class FilePropertyHandler * @param LoggerInterface $logger Logger for logging operations * @param FileService $fileService File service for file operations * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function __construct( private readonly LoggerInterface $logger, @@ -89,7 +90,7 @@ public function __construct( * * @throws Exception If file reading fails. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function processUploadedFiles(array $uploadedFiles, array $data): array { @@ -111,7 +112,13 @@ public function processUploadedFiles(array $uploadedFiles, array $data): array continue; } - // Read file content. + // Read file content. Check readability first so an absent/unreadable + // tmp path throws our clear exception instead of leaking a + // file_get_contents() warning before returning false. + if (is_readable((string) ($fileInfo['tmp_name'] ?? '')) === false) { + throw new Exception("Failed to read uploaded file for field '$fieldName'"); + } + $fileContent = file_get_contents($fileInfo['tmp_name']); if ($fileContent === false) { throw new Exception("Failed to read uploaded file for field '$fieldName'"); @@ -168,7 +175,7 @@ public function processUploadedFiles(array $uploadedFiles, array $data): array * @SuppressWarnings(PHPMD.NPathComplexity) Many conditional paths for different file input formats * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive file type detection requires checking many formats * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function isFileProperty($value, ?Schema $schema=null, ?string $propertyName=null): bool { @@ -313,7 +320,7 @@ public function isFileProperty($value, ?Schema $schema=null, ?string $propertyNa * @psalm-return bool * @phpstan-return bool * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function isFileObject(array $value): bool { @@ -370,7 +377,7 @@ public function isFileObject(array $value): bool * @psalm-return string * @phpstan-return string * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function generateFileName( string $propertyName, @@ -408,7 +415,7 @@ private function generateFileName( * @SuppressWarnings(PHPMD.UnusedFormalParameter) $index kept for API consistency with other methods * @psalm-suppress UnusedParam $index kept for API consistency and future use * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function prepareAutoTags( array $fileConfig, @@ -464,7 +471,7 @@ private function prepareAutoTags( * @SuppressWarnings(PHPMD.NPathComplexity) Multiple conditional branches for file property processing * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive file property handling requires many steps * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function handleFileProperty( ObjectEntity $objectEntity, @@ -494,14 +501,28 @@ public function handleFileProperty( throw new Exception("Property '$propertyName' is not configured as a file property"); } - // Merge schema-level autoPublish setting if not set at property level. - // Schema configuration.autoPublish serves as a default for all file properties. - if (isset($fileConfig['autoPublish']) === false) { + // Merge schema-level autoShare setting if not set at property level. + // Schema configuration.autoShare serves as a default for all file properties: + // when true, an NC share is automatically created for each uploaded file. + if (isset($fileConfig['autoShare']) === false) { $schemaConfig = $schema->getConfiguration() ?? []; - if (isset($schemaConfig['autoPublish']) === true) { - $fileConfig['autoPublish'] = $schemaConfig['autoPublish']; + if (isset($schemaConfig['autoShare']) === true) { + $fileConfig['autoShare'] = $schemaConfig['autoShare']; + } else if (isset($schemaConfig['autoPublish']) === true) { + // DEPRECATED: autoPublish was renamed to autoShare in 2026. + // Log a one-time deprecation warning and do NOT act on the legacy value. + $this->logger->warning( + message: "[FilePropertyHandler] Schema config key 'autoPublish' is deprecated and ignored; " + ."rename it to 'autoShare' to keep auto-sharing uploaded files.", + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'app' => 'openregister', + 'schemaId' => $schema->getId(), + ] + ); } - } + }//end if // Handle file deletion: null for single files, empty array for array properties. if ($fileValue === null || (is_array($fileValue) === true && empty($fileValue) === true)) { @@ -679,7 +700,7 @@ public function handleFileProperty( * * @throws Exception If file validation fails or file operations fail. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function processSingleFileProperty( ObjectEntity $objectEntity, @@ -746,7 +767,7 @@ public function processSingleFileProperty( * * @throws Exception If file processing fails. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function processStringFileInput( ObjectEntity $objectEntity, @@ -791,14 +812,14 @@ private function processStringFileInput( index: $index ); - $autoPublish = $fileConfig['autoPublish'] ?? false; + $autoShare = $fileConfig['autoShare'] ?? false; // Create the file using FileService. $file = $this->fileService->addFile( objectEntity: $objectEntity, fileName: $filename, content: $fileData['content'], - share: $autoPublish, + share: $autoShare, tags: $autoTags ); @@ -832,7 +853,7 @@ private function processStringFileInput( * * @throws Exception If file processing fails. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function processFileObjectInput( ObjectEntity $objectEntity, @@ -911,19 +932,27 @@ private function processFileObjectInput( * @psalm-return string * @phpstan-return string * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function fetchFileFromUrl(string $url): string { + // SEC-SVC-2: validate the user-supplied URL against the shared anti-SSRF guard + // BEFORE issuing any request. This rejects non-http(s) schemes and any host that + // resolves to a loopback / private / reserved address (e.g. cloud metadata, + // localhost, RFC-1918), closing the full read-SSRF on this object-write path. + SecurityService::assertSafeFetchUrl($url); + // Create a context with appropriate options. + // SEC-SVC-2: redirects are DISABLED — follow_location would let a public URL + // 302-redirect to an internal address, defeating the point-in-time guard above. $context = stream_context_create( [ 'http' => [ 'timeout' => 30, // 30 second timeout. 'user_agent' => 'OpenRegister/1.0', - 'follow_location' => true, - 'max_redirects' => 5, + 'follow_location' => false, + 'max_redirects' => 0, ], ] ); @@ -956,7 +985,7 @@ private function fetchFileFromUrl(string $url): string * @psalm-return array{content: string, mimeType: string, extension: string, size: int<0, max>} * @phpstan-return array{content: string, mimeType: string, extension: string, size: int} * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function parseFileDataFromUrl(string $url, string $content): array { @@ -1002,7 +1031,7 @@ private function parseFileDataFromUrl(string $url, string $content): array * @psalm-return array{content: string, mimeType: string, extension: string, size: int<0, max>} * @phpstan-return array{content: string, mimeType: string, extension: string, size: int} * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function parseFileData(string $fileContent): array { @@ -1077,7 +1106,7 @@ public function parseFileData(string $fileContent): array * * @throws Exception If validation fails. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function validateFileAgainstConfig( array $fileData, @@ -1141,7 +1170,7 @@ public function validateFileAgainstConfig( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple security checks for executable detection * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ public function blockExecutableFiles(array $fileData, string $errorPrefix): void { @@ -1218,7 +1247,7 @@ public function blockExecutableFiles(array $fileData, string $errorPrefix): void * * @throws Exception If executable magic bytes are detected. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function detectExecutableMagicBytes(string $content, string $errorPrefix): void { @@ -1288,7 +1317,7 @@ private function detectExecutableMagicBytes(string $content, string $errorPrefix * @psalm-return string * @phpstan-return string * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function getExtensionFromMimeType(string $mimeType): string { @@ -1365,7 +1394,7 @@ private function getExtensionFromMimeType(string $mimeType): string * 'xml', 'json', 'sql', 'exe', 'dmg', 'iso', 'deb', 'rpm'} * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function getCommonFileExtensions(): array { @@ -1445,7 +1474,7 @@ private function getCommonFileExtensions(): array * 'out', 'o', 'so', 'dylib'} * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function getDangerousExecutableExtensions(): array { @@ -1530,7 +1559,7 @@ private function getDangerousExecutableExtensions(): array * 'application/x-python-code', 'application/java-archive'} * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-13 + * @spec openspec/specs/content-versioning/spec.md */ private function getExecutableMimeTypes(): array { diff --git a/lib/Service/Object/SaveObject/LinkedEntityPropertyHandler.php b/lib/Service/Object/SaveObject/LinkedEntityPropertyHandler.php index 2e5fe5dc01..4f5c3c94c8 100644 --- a/lib/Service/Object/SaveObject/LinkedEntityPropertyHandler.php +++ b/lib/Service/Object/SaveObject/LinkedEntityPropertyHandler.php @@ -8,14 +8,14 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables */ namespace OCA\OpenRegister\Service\Object\SaveObject; @@ -72,7 +72,7 @@ class LinkedEntityPropertyHandler * * @param LoggerInterface $logger Logger for logging operations * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function __construct( private readonly LoggerInterface $logger, @@ -92,7 +92,7 @@ public function __construct( * * @return ObjectEntity The updated object entity * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables */ public function extractAndPopulate(ObjectEntity $object, Schema $schema, array $data): ObjectEntity { @@ -137,7 +137,7 @@ public function extractAndPopulate(ObjectEntity $object, Schema $schema, array $ * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function extractFromProperty( string $propertyName, @@ -185,7 +185,7 @@ private function extractFromProperty( * * @return string|null The extracted ID or null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function extractIdFromEnvelope(mixed $value): ?string { @@ -215,7 +215,7 @@ private function extractIdFromEnvelope(mixed $value): ?string * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-43 + * @spec openspec/specs/linked-entity-types/spec.md#requirement-metadata-columns-on-entity-tables */ private function mergeIntoMetadataColumn(ObjectEntity $object, string $ncType, array $newIds): void { diff --git a/lib/Service/Object/SaveObject/MetadataHydrationHandler.php b/lib/Service/Object/SaveObject/MetadataHydrationHandler.php index d29fa280d3..77074800a3 100644 --- a/lib/Service/Object/SaveObject/MetadataHydrationHandler.php +++ b/lib/Service/Object/SaveObject/MetadataHydrationHandler.php @@ -64,7 +64,7 @@ class MetadataHydrationHandler * @param LoggerInterface $logger Logger interface for logging operations. * @param CacheHandler $cacheHandler Cache handler for UUID-to-name resolution. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly LoggerInterface $logger, @@ -96,35 +96,13 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 - * @spec openspec/specs/deprecate-published-metadata/spec.md#REQ-6 (Deprecation Warnings — logs a warning for - * objectPublishedField/objectDepublishedField/autoPublish schema config keys, recommending RBAC $now rules) + * @spec openspec/specs/object-lifecycle/spec.md */ public function hydrateObjectMetadata(ObjectEntity $entity, Schema $schema): void { $config = $schema->getConfiguration() ?? []; $objectData = $entity->getObject(); - // DEPRECATED: Log warnings for deprecated published metadata config keys. - // Object-level published/depublished metadata has been removed. - // Use RBAC authorization rules with $now for publication control instead. - $deprecatedKeys = ['objectPublishedField', 'objectDepublishedField', 'autoPublish']; - foreach ($deprecatedKeys as $key) { - if (isset($config[$key]) === true) { - $this->logger->warning( - message: "[MetadataHydrationHandler] Schema config key '{$key}' is deprecated. Use RBAC \$now rules instead.", - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'app' => 'openregister', - 'schemaId' => $schema->getId(), - 'key' => $key, - 'value' => $config[$key], - ] - ); - } - } - // CRITICAL FIX: Extract business data from correct location. // If object data has 'object' key that is an array (structured format), use that for property access. // Otherwise use the objectData directly (flat format). @@ -219,7 +197,7 @@ public function hydrateObjectMetadata(ObjectEntity $entity, Schema $schema): voi * * @return string|null The first non-empty value found, or null. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function tryCommonFields(array $data, array $fieldNames): ?string { @@ -246,7 +224,7 @@ private function tryCommonFields(array $data, array $fieldNames): ?string * * @return mixed The value at the path, or null if not found. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getValueFromPath(array $data, string $path) { @@ -288,7 +266,7 @@ public function getValueFromPath(array $data, string $path) * * @return string|null The extracted/concatenated value, or null if not found. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function extractMetadataValue(array $data, string $fieldPath, array $schemaProperties=[]): ?string { @@ -325,7 +303,7 @@ public function extractMetadataValue(array $data, string $fieldPath, array $sche * * @return string|null The first non-empty value found, or null if none found. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function processFieldWithFallbacks(array $data, string $fieldChain, array $schemaProperties=[]): ?string { @@ -379,7 +357,7 @@ public function processFieldWithFallbacks(array $data, string $fieldChain, array * * @return null|string The processed result or null if no values found. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function processTwigLikeTemplate(array $data, string $template, array $schemaProperties=[]): string|null { @@ -476,7 +454,7 @@ public function processTwigLikeTemplate(array $data, string $template, array $sc * * @return string|null The mapped value, the raw field value as fallback, or null if field is empty. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function processMapFilter(array $data, string $fieldName, string $mapDefinition): ?string { @@ -525,7 +503,7 @@ public function processMapFilter(array $data, string $fieldName, string $mapDefi * * @return string|null The selected value based on whether the field is filled or empty. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function processIfFilledFilter(array $data, string $fieldName, string $definition): ?string { @@ -563,7 +541,7 @@ public function processIfFilledFilter(array $data, string $fieldName, string $de * * @return string|null The resolved name, or the original value if not a relation. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function resolveRelationValue(string $fieldName, mixed $value, array $schemaProperties): ?string { @@ -625,7 +603,7 @@ private function resolveRelationValue(string $fieldName, mixed $value, array $sc * * @return bool True if the property is a relation. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function isRelationProperty(array $property): bool { @@ -677,7 +655,7 @@ private function isRelationProperty(array $property): bool * * @return string|null The extracted UUID or null. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ private function extractUuidFromValue(mixed $value): ?string { @@ -708,7 +686,7 @@ private function extractUuidFromValue(mixed $value): ?string * * @return string|null The generated slug or null if value is empty. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createSlugFromValue(string $value): ?string { @@ -738,7 +716,7 @@ public function createSlugFromValue(string $value): ?string * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple fallback paths for slug source determination * @SuppressWarnings(PHPMD.NPathComplexity) Multiple nested conditional paths for evaluating different field options * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function generateSlug(array $data, Schema $schema): string|null { @@ -794,7 +772,7 @@ public function generateSlug(array $data, Schema $schema): string|null * * @return string The generated slug. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createSlug(string $text): string { diff --git a/lib/Service/Object/SaveObject/RelationCascadeHandler.php b/lib/Service/Object/SaveObject/RelationCascadeHandler.php index 2e27a538cd..ec6530898f 100644 --- a/lib/Service/Object/SaveObject/RelationCascadeHandler.php +++ b/lib/Service/Object/SaveObject/RelationCascadeHandler.php @@ -60,7 +60,7 @@ class RelationCascadeHandler * @param RegisterMapper $registerMapper Register mapper for register operations. * @param LoggerInterface $logger Logger interface for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function __construct( private readonly MagicMapper $objectEntityMapper, @@ -87,7 +87,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple reference format handling paths * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function resolveSchemaReference(string $reference): string|null { @@ -146,7 +146,7 @@ public function resolveSchemaReference(string $reference): string|null * * @return string The cleaned reference without query parameters. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function removeQueryParameters(string $reference): string { @@ -174,7 +174,7 @@ private function removeQueryParameters(string $reference): string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple reference format handling paths * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function resolveRegisterReference(string $reference): string|null { @@ -240,7 +240,7 @@ public function resolveRegisterReference(string $reference): string|null * @SuppressWarnings(PHPMD.CyclomaticComplexity) Recursive relation scanning with multiple reference types * @SuppressWarnings(PHPMD.NPathComplexity) Multiple conditional paths for different relation patterns * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function scanForRelations(array $data, string $prefix='', ?Schema $schema=null): array { @@ -298,7 +298,7 @@ public function scanForRelations(array $data, string $prefix='', ?Schema $schema * * @return array The property definition or empty array. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function getPropertyDefinition(array $properties, array $propertyPath): array { @@ -321,7 +321,7 @@ private function getPropertyDefinition(array $properties, array $propertyPath): * * @return bool True if array contains references. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function isArrayOfReferences(array $array): bool { @@ -337,11 +337,18 @@ private function isArrayOfReferences(array $array): bool /** * Checks if a value looks like an object reference. * + * This is the relation RESOLUTION path (it accepts numeric ids and `/api/` + * URLs so cascade write-back can resolve them to UUIDs), which is a broader + * concern than the `@self.relations` RECORDING rule shared by SaveObjects + * and BulkRelationHandler via RelationDetectionTrait. It deliberately does + * NOT carry the loose "length + separator" heuristic, so scalars such as + * dates or enum values are never matched here either. + * * @param string $value The value to check. * * @return bool True if it looks like a reference. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function looksLikeObjectReference(string $value): bool { @@ -387,7 +394,7 @@ private function looksLikeObjectReference(string $value): bool * * @return bool True if value is a reference. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function isReference(string $value): bool { @@ -434,7 +441,7 @@ public function isReference(string $value): bool * * @return ObjectEntity The updated object entity. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function updateObjectRelations(ObjectEntity $objectEntity, array $data, ?Schema $schema=null): ObjectEntity { @@ -468,7 +475,7 @@ public function updateObjectRelations(ObjectEntity $objectEntity, array $data, ? * @SuppressWarnings(PHPMD.CyclomaticComplexity) Navigating nested arrays and handling * multiple reference types requires complex logic * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function resolveRelationPath(array &$objectData, string $relationPath): void { @@ -525,7 +532,7 @@ private function resolveRelationPath(array &$objectData, string $relationPath): * * @SuppressWarnings(PHPMD.StaticAccess) Uuid::isValid is standard Symfony UID pattern * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function extractUuidFromReference(string $reference): ?string { @@ -572,7 +579,7 @@ private function extractUuidFromReference(string $reference): ?string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex cascading logic for different property types * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function cascadeObjects(ObjectEntity $objectEntity, Schema $schema, array $data): array { @@ -624,7 +631,7 @@ public function cascadeObjects(ObjectEntity $objectEntity, Schema $schema, array * * @return bool True if all values are scalar. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function isArrayOfScalars(array $array): bool { @@ -651,7 +658,7 @@ private function isArrayOfScalars(array $array): bool * @SuppressWarnings(PHPMD.StaticAccess) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function cascadeMultipleObjects(ObjectEntity $objectEntity, array $property, array $propData): array { @@ -681,7 +688,7 @@ public function cascadeMultipleObjects(ObjectEntity $objectEntity, array $proper * * @return null The UUID of the created object or null. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function cascadeSingleObject(ObjectEntity $_objectEntity, array $_definition, array $_object) { @@ -708,7 +715,7 @@ public function cascadeSingleObject(ObjectEntity $_objectEntity, array $_definit * * @return array The updated data after write-back operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function handleInverseRelationsWriteBack(ObjectEntity $_objectEntity, Schema $_schema, array $data): array { diff --git a/lib/Service/Object/SaveObjects.php b/lib/Service/Object/SaveObjects.php index 9033f6fe13..792c5b7c30 100644 --- a/lib/Service/Object/SaveObjects.php +++ b/lib/Service/Object/SaveObjects.php @@ -62,15 +62,21 @@ use DateTime; use Exception; use InvalidArgumentException; +use OCA\OpenRegister\Db\AuditTrailMapper; use OCA\OpenRegister\Db\ObjectEntity; use OCA\OpenRegister\Db\MagicMapper; use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Event\ObjectCreatedEvent; +use OCA\OpenRegister\Event\ObjectUpdatedEvent; +use OCA\OpenRegister\Service\Object\PermissionHandler; use OCA\OpenRegister\Service\Object\SaveObject; use OCA\OpenRegister\Service\Object\ValidateObject; use OCA\OpenRegister\Service\OrganisationService; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IGroupManager; use OCP\IUserSession; use Psr\Log\LoggerInterface; use Symfony\Component\Uid\Uuid; @@ -96,6 +102,17 @@ */ class SaveObjects { + use RelationDetectionTrait; + + /** + * Maximum number of audit entries materialised into AuditTrail entities + * and persisted per insertAuditTrails() call. Bounds peak memory: a save + * chunk can hold up to 20k objects, and each audit entity embeds the + * object's old+new payload diff. + * + * @var int + */ + private const AUDIT_INSERT_CHUNK_SIZE = 100; /** * Static schema cache to avoid repeated database lookups @@ -121,15 +138,20 @@ class SaveObjects /** * Constructor for SaveObjects handler * - * @param MagicMapper $objectEntityMapper Mapper for object entity database operations - * @param SchemaMapper $schemaMapper Mapper for schema operations - * @param RegisterMapper $registerMapper Mapper for register operations - * @param SaveObject $saveHandler Handler for individual object operations - * @param IUserSession $userSession User session for getting current user - * @param OrganisationService $organisationService Service for organisation operations - * @param LoggerInterface $logger Logger for error and debug logging - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @param MagicMapper $objectEntityMapper Mapper for object entity database operations + * @param SchemaMapper $schemaMapper Mapper for schema operations + * @param RegisterMapper $registerMapper Mapper for register operations + * @param SaveObject $saveHandler Handler for individual object operations + * @param IUserSession $userSession User session for getting current user + * @param OrganisationService $organisationService Service for organisation operations + * @param LoggerInterface $logger Logger for error and debug logging + * @param IGroupManager|null $groupManager Group manager for admin-bypass detection + * @param PermissionHandler|null $permissionHandler Permission handler for per-object RBAC enforcement + * @param ValidateObject|null $validateHandler Validation handler for per-object schema validation + * @param IEventDispatcher|null $eventDispatcher Event dispatcher for object lifecycle events + * @param AuditTrailMapper|null $auditTrailMapper Audit trail mapper for logging bulk changes + * + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly MagicMapper $objectEntityMapper, @@ -138,7 +160,12 @@ public function __construct( private readonly SaveObject $saveHandler, private readonly IUserSession $userSession, private readonly OrganisationService $organisationService, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly ?IGroupManager $groupManager=null, + private readonly ?PermissionHandler $permissionHandler=null, + private readonly ?ValidateObject $validateHandler=null, + private readonly ?IEventDispatcher $eventDispatcher=null, + private readonly ?AuditTrailMapper $auditTrailMapper=null ) { }//end __construct() @@ -151,7 +178,7 @@ public function __construct( * @return Schema The loaded schema * @throws Exception If schema cannot be found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function loadSchemaWithCache(int|string $schemaId): Schema { @@ -174,7 +201,7 @@ private function loadSchemaWithCache(int|string $schemaId): Schema * * @return array Comprehensive schema analysis * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function getSchemaAnalysisWithCache(Schema $schema): array { @@ -200,7 +227,7 @@ private function getSchemaAnalysisWithCache(Schema $schema): array * @return Register The loaded register * @throws Exception If register cannot be found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function loadRegisterWithCache(int|string $registerId): Register { @@ -221,7 +248,7 @@ private function loadRegisterWithCache(int|string $registerId): Register * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public static function clearSchemaCache(): void { @@ -263,7 +290,7 @@ public static function clearSchemaCache(): void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Orchestrator at threshold after extracting initializeSaveResult * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function saveObjects( array $objects, @@ -354,13 +381,19 @@ public function saveObjects( $chunkStart = microtime(true); // Process the current chunk and get the result. + // Forward the caller-supplied register/schema so the bulk-save path + // does not have to fall back to per-row @self.register/@self.schema + // — those values are advisory and may carry stale IDs from the + // source instance when re-importing exported data. $chunkResult = $this->processObjectsChunk( objects: $objectsChunk, schemaCache: $globalSchemaCache, _rbac: $_rbac, _multitenancy: $_multitenancy, _validation: $_validation, - _events: $_events + _events: $_events, + register: $register, + schema: $schema ); // Merge chunk results for saved, updated, invalid, errors, and unchanged. @@ -440,7 +473,7 @@ public function saveObjects( * * @return array The initialized result structure * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function initializeSaveResult(int $totalObjects): array { @@ -469,7 +502,7 @@ private function initializeSaveResult(int $totalObjects): array * * @return int Optimal chunk size * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function calculateOptimalChunkSize(int $totalObjects): int { @@ -517,7 +550,7 @@ private function calculateOptimalChunkSize(int $totalObjects): int * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multi-schema grouping + validation requires branching * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function prepareObjectsForBulkSave(array $objects): array { @@ -555,7 +588,7 @@ private function prepareObjectsForBulkSave(array $objects): array * * @return array [schemaCache, schemaAnalysis] indexed by schema ID * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function groupAndLoadSchemas(array $objects): array { @@ -595,7 +628,7 @@ private function groupAndLoadSchemas(array $objects): array * * @throws Exception If schema is not found in cache * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function prepareMixedSchemaObject(array $object, array $schemaCache): array { @@ -672,7 +705,7 @@ private function prepareMixedSchemaObject(array $object, array $schemaCache): ar * @SuppressWarnings(PHPMD.CyclomaticComplexity) Single-schema optimization path with many edge cases * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function prepareSingleSchemaObjectsOptimized( array $objects, @@ -744,7 +777,7 @@ private function prepareSingleSchemaObjectsOptimized( * * @return array [registerId, registerObj, schemaId, schemaObj, schemaCache, schemaAnalysis] * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function loadAndValidateSchemaContext( Register|string|int $register, @@ -798,7 +831,7 @@ private function loadAndValidateSchemaContext( * * @return array The prepared selfData array ready for database operations * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function prepareSingleSchemaObject( array $object, @@ -825,9 +858,15 @@ private function prepareSingleSchemaObject( $selfData['id'] = $providedId; } - // PERFORMANCE: Use pre-calculated metadata values. - $selfData['owner'] = $selfData['owner'] ?? $defaultOwner; - $selfData['organisation'] = $selfData['organisation'] ?? $defaultOrganisation; + // SECURITY (wave-11 SB2): owner and organisation MUST NOT be accepted from + // client-supplied @self data on the bulk path. The wave-9 fix (#2008) added + // stripping for the single-object MagicMapper path but the bulk pipeline + // (SaveObjects → MagicBulkHandler::prepareObjectsForDynamicTable) is a + // completely separate code path that was not covered. Always stamp the + // authoritative values from the session / active organisation, regardless of + // what the client sent in @self.owner or @self.organisation. + $selfData['owner'] = $defaultOwner; + $selfData['organisation'] = $defaultOrganisation; // Update object's @self data before hydration. $object['@self'] = $selfData; @@ -898,7 +937,7 @@ private function prepareSingleSchemaObject( * * @return array Updated selfData with metadata fields * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function applyHydratedMetadata(array $selfData, array &$object, ObjectEntity $tempEntity): array { @@ -932,19 +971,21 @@ private function applyHydratedMetadata(array $selfData, array &$object, ObjectEn * - Memory-efficient processing * - Streamlined response format * - * @param array $objects Array of pre-processed objects ready for database operations - * @param array $schemaCache Pre-built schema cache for performance optimization - * @param bool $_rbac Apply RBAC filtering - * @param bool $_multitenancy Apply multi-tenancy filtering - * @param bool $_validation Apply schema validation - * @param bool $_events Dispatch events + * @param array $objects Array of pre-processed objects ready for database operations + * @param array $schemaCache Pre-built schema cache for performance optimization + * @param bool $_rbac Apply RBAC filtering + * @param bool $_multitenancy Apply multi-tenancy filtering + * @param bool $_validation Apply schema validation + * @param bool $_events Dispatch events + * @param Register|string|int|null $register Caller-supplied register context (forwarded to bulk save) + * @param Schema|string|int|null $schema Caller-supplied schema context (forwarded to bulk save) * * @return array Processing result for this chunk with bulk operation statistics * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Chunk pipeline: transform → validate → persist → relations * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function processObjectsChunk( array $objects, @@ -952,7 +993,9 @@ private function processObjectsChunk( bool $_rbac, bool $_multitenancy, bool $_validation, - bool $_events + bool $_events, + Register|string|int|null $register=null, + Schema|string|int|null $schema=null ): array { $startTime = microtime(true); @@ -981,11 +1024,43 @@ private function processObjectsChunk( return $result; } + // STEP 1b (BUG-OBJ-1): enforce per-object RBAC and schema validation BEFORE persisting, + // mirroring the guarantees of the single-object save path. Objects that fail either check + // are moved into $result['invalid'] and excluded from persistence. + $transformedObjects = $this->enforceChunkGuards( + transformedObjects: $transformedObjects, + schemaCache: $schemaCache, + _rbac: $_rbac, + _validation: $_validation, + result: $result + ); + + if (empty($transformedObjects) === true) { + return $result; + } + // STEP 2: Persist transformed objects to database. - $bulkResult = $this->persistChunk(transformedObjects: $transformedObjects); + $bulkResult = $this->persistChunk( + transformedObjects: $transformedObjects, + register: $register, + schema: $schema + ); + + // STEP 3: Build and classify results from bulk operation output. Returns the + // hydrated entities (with pre-update state for updates) for side effects. + $sideEffects = $this->buildChunkResults( + bulkResult: $bulkResult, + transformedObjects: $transformedObjects, + result: $result + ); - // STEP 3: Build and classify results from bulk operation output. - $this->buildChunkResults(bulkResult: $bulkResult, transformedObjects: $transformedObjects, result: $result); + // STEP 3b (BUG-OBJ-1): always write the audit trail for created/updated objects and, + // when requested, dispatch the matching lifecycle events. The ultraFastBulkSave mapper + // path bypasses the per-row event/audit hooks that the single-object insert/update apply, + // so the bulk path must replay them here to stay at parity with single-object save. + // Audit rows are written with streamed batched multi-row INSERTs (100 rows per + // statement) instead of one INSERT (plus hash-chain queries) per object. + $this->emitChunkSideEffects(sideEffects: $sideEffects, _events: $_events); $endTime = microtime(true); $processingTime = round(($endTime - $startTime) * 1000, 2); @@ -996,6 +1071,260 @@ private function processObjectsChunk( return $result; }//end processObjectsChunk() + /** + * Enforce per-object RBAC and schema validation before persistence (BUG-OBJ-1). + * + * Mirrors the guarantees of the single-object save path for the bulk path. Each + * transformed object is checked for create permission (when $_rbac is true) and + * validated against its resolved schema (when $_validation is true). Objects that + * fail either check are appended to $result['invalid'] and excluded from the + * returned list so they are never persisted. + * + * @param array $transformedObjects Flat transformed objects ready for persistence + * @param array $schemaCache Schema cache keyed by schema id + * @param bool $_rbac Whether to enforce per-object create permission + * @param bool $_validation Whether to validate each object against its schema + * @param array $result The result array (invalid bucket is appended to) + * + * @return array The transformed objects that passed all enabled guards + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function enforceChunkGuards( + array $transformedObjects, + array $schemaCache, + bool $_rbac, + bool $_validation, + array &$result + ): array { + // Nothing to enforce — return untouched. + if ($_rbac === false && $_validation === false) { + return $transformedObjects; + } + + $allowedObjects = []; + $currentUserId = null; + $currentUser = $this->userSession->getUser(); + if ($currentUser !== null) { + $currentUserId = $currentUser->getUID(); + } + + foreach ($transformedObjects as $objData) { + $schemaId = $objData['schema'] ?? null; + $schema = null; + if ($schemaId !== null && isset($schemaCache[$schemaId]) === true) { + $schema = $schemaCache[$schemaId]; + } + + // Without a resolvable schema we cannot safely authorize or validate — reject. + if ($schema instanceof Schema === false) { + $result['invalid'][] = [ + 'object' => $objData, + 'error' => 'Schema could not be resolved for RBAC/validation enforcement', + 'type' => 'InvalidSchemaException', + ]; + $result['statistics']['invalid']++; + $result['statistics']['errors']++; + continue; + } + + // RBAC: enforce create permission per object (BUG-OBJ-1). + if ($_rbac === true && $this->permissionHandler !== null) { + $hasPermission = $this->permissionHandler->hasPermission( + schema: $schema, + action: 'create', + userId: $currentUserId, + objectOwner: ($objData['owner'] ?? null), + _rbac: true + ); + + if ($hasPermission === false) { + $result['invalid'][] = [ + 'object' => $objData, + 'error' => 'You do not have permission to create objects in this schema', + 'type' => 'PermissionDeniedException', + ]; + $result['statistics']['invalid']++; + $result['statistics']['errors']++; + continue; + } + } + + // Validation: validate the business data against the schema (BUG-OBJ-1). + if ($_validation === true && $this->validateHandler !== null) { + $businessData = $objData['object'] ?? []; + $validationResult = $this->validateHandler->validateObject( + object: $businessData, + schema: $schema + ); + + if ($validationResult->isValid() === false) { + $result['invalid'][] = [ + 'object' => $objData, + 'error' => $this->validateHandler->generateErrorMessage(result: $validationResult), + 'type' => 'ValidationException', + ]; + $result['statistics']['invalid']++; + $result['statistics']['errors']++; + continue; + } + } + + $allowedObjects[] = $objData; + }//end foreach + + return $allowedObjects; + }//end enforceChunkGuards() + + /** + * Dispatch lifecycle events and write audit trail for persisted objects (BUG-OBJ-1). + * + * The ultraFastBulkSave mapper path bypasses the per-row event/audit hooks the + * single-object insert/update apply, so the bulk path replays them here. Audit + * trail rows are written with batched multi-row INSERTs + * ({@see AuditTrailMapper::insertAuditTrails()}) instead of one INSERT per + * object, streamed in slices of {@see self::AUDIT_INSERT_CHUNK_SIZE} entries + * so peak memory never holds a whole 20k-object chunk's audit entities (each + * embeds an old+new payload diff). Update rows carry the REAL pre-update + * state so their changeset is an accurate old-vs-new diff. + * ObjectCreated/ObjectUpdatedEvent are only dispatched when $_events is true. + * + * @param array $sideEffects Side-effect payload from buildChunkResults(): + * 'created' => list, + * 'updated' => list + * @param bool $_events Whether to dispatch object lifecycle events + * + * @return void + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function emitChunkSideEffects(array $sideEffects, bool $_events): void + { + $createdEntities = ($sideEffects['created'] ?? []); + $updatedEntities = ($sideEffects['updated'] ?? []); + + // Batched audit trail: multi-row INSERTs instead of one INSERT (plus + // hash-chain queries) per object. + if ($this->auditTrailMapper !== null) { + $auditEntries = []; + foreach ($createdEntities as $entity) { + $auditEntries[] = [ + 'old' => null, + 'new' => $entity, + 'action' => 'create', + ]; + } + + foreach ($updatedEntities as $pair) { + // Fall back to the persisted entity as `old` when no pre-update + // snapshot could be reconstructed, so the row still records an + // 'update' action (a null `old` would be classified as 'create'). + $auditEntries[] = [ + 'old' => ($pair['old'] ?? $pair['new']), + 'new' => $pair['new'], + 'action' => 'update', + ]; + } + + // MEMORY: stream the entries in slices of AUDIT_INSERT_CHUNK_SIZE. + // A save chunk can hold up to 20k objects (calculateOptimalChunkSize), + // and every AuditTrail entity embeds an old+new payload diff — building + // them all before inserting would hold the whole chunk's diffs in + // memory at once. array_splice() consumes the entry list destructively + // so each slice's references are released as soon as it is persisted. + while ($auditEntries !== []) { + $slice = array_splice($auditEntries, 0, self::AUDIT_INSERT_CHUNK_SIZE); + try { + $this->auditTrailMapper->insertAuditTrails(entries: $slice); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[SaveObjects] Failed to write batched audit trail for bulk chunk', + context: ['error' => $e->getMessage(), 'entries' => count($slice)] + ); + } + } + }//end if + + if ($_events !== true || $this->eventDispatcher === null) { + return; + } + + // Created objects → ObjectCreatedEvent. + foreach ($createdEntities as $entity) { + try { + $this->eventDispatcher->dispatchTyped(new ObjectCreatedEvent(object: $entity)); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[SaveObjects] Failed to dispatch ObjectCreatedEvent for bulk object', + context: ['error' => $e->getMessage(), 'uuid' => $entity->getUuid()] + ); + } + } + + // Updated objects → ObjectUpdatedEvent with the real pre-update state + // when available so diff-aware listeners see an accurate changeset. + foreach ($updatedEntities as $pair) { + $entity = $pair['new']; + try { + $this->eventDispatcher->dispatchTyped( + new ObjectUpdatedEvent(newObject: $entity, oldObject: ($pair['old'] ?? $entity)) + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[SaveObjects] Failed to dispatch ObjectUpdatedEvent for bulk object', + context: ['error' => $e->getMessage(), 'uuid' => $entity->getUuid()] + ); + } + } + }//end emitChunkSideEffects() + + /** + * Convert a raw magic-table result row into an ObjectEntity. + * + * The bulk mapper returns raw rows (underscore-prefixed metadata columns + * plus snake_case property columns). Conversion needs register + schema + * context, resolved through the static caches from the row's `_register` + * and `_schema` values. + * + * @param array $row The raw magic-table row + * + * @return ObjectEntity|null The converted entity, or null when it cannot be built + */ + private function convertBulkRowToEntity(array $row): ?ObjectEntity + { + $registerId = ($row['_register'] ?? null); + $schemaId = ($row['_schema'] ?? null); + + if ($registerId === null || $schemaId === null) { + return null; + } + + try { + $register = $this->loadRegisterWithCache(registerId: $registerId); + $schema = $this->loadSchemaWithCache(schemaId: $schemaId); + + return $this->objectEntityMapper->convertRowToObjectEntity( + row: $row, + _register: $register, + _schema: $schema + ); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[SaveObjects] Failed to convert bulk result row to entity', + context: [ + 'error' => $e->getMessage(), + 'uuid' => ($row['_uuid'] ?? 'unknown'), + 'register' => $registerId, + 'schema' => $schemaId, + ] + ); + + return null; + }//end try + }//end convertBulkRowToEntity() + /** * Transform a chunk of objects to database format and collect invalid objects * @@ -1008,7 +1337,7 @@ private function processObjectsChunk( * * @return array Valid transformed objects ready for database operations * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function transformChunk(array $objects, array $schemaCache, array &$result): array { @@ -1032,14 +1361,19 @@ private function transformChunk(array $objects, array $schemaCache, array &$resu * All objects go directly to the bulk save operation which handles create vs update * automatically using INSERT...ON DUPLICATE KEY UPDATE with database-computed classification. * - * @param array $transformedObjects Valid objects ready for database operations + * @param array $transformedObjects Valid objects ready for database operations + * @param Register|string|int|null $register Caller-supplied register context (forwarded to mapper) + * @param Schema|string|int|null $schema Caller-supplied schema context (forwarded to mapper) * * @return mixed The bulk operation result from the mapper * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ - private function persistChunk(array $transformedObjects): mixed - { + private function persistChunk( + array $transformedObjects, + Register|string|int|null $register=null, + Schema|string|int|null $schema=null + ): mixed { $this->logger->info( "[SaveObjects] Using single-call bulk processing (no pre-lookup needed)", [ @@ -1048,8 +1382,40 @@ private function persistChunk(array $transformedObjects): mixed ] ); + // Resolve register/schema to entity instances if the caller passed an ID + // — the mapper's fallback resolution path goes through @self.* on each + // row, which is exactly what we're trying to avoid here. + if ($register !== null && $register instanceof Register === false) { + try { + $register = $this->registerMapper->find($register); + } catch (Exception $e) { + $this->logger->warning( + message: '[SaveObjects] Failed to resolve register for bulk save', + context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $register] + ); + $register = null; + } + } + + if ($schema !== null && $schema instanceof Schema === false) { + try { + $schema = $this->schemaMapper->find($schema); + } catch (Exception $e) { + $this->logger->warning( + message: '[SaveObjects] Failed to resolve schema for bulk save', + context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $schema] + ); + $schema = null; + } + } + // MAXIMUM PERFORMANCE: Always use ultra-fast bulk operations for large imports. - return $this->objectEntityMapper->ultraFastBulkSave($transformedObjects, []); + return $this->objectEntityMapper->ultraFastBulkSave( + insertObjects: $transformedObjects, + updateObjects: [], + register: $register, + schema: $schema + ); }//end persistChunk() /** @@ -1063,12 +1429,18 @@ private function persistChunk(array $transformedObjects): mixed * @param array $transformedObjects The original transformed objects (for fallback) * @param array $result The result array to populate * - * @return void + * @return array Side-effect payload: 'created' => list, + * 'updated' => list * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ - private function buildChunkResults(mixed $bulkResult, array $transformedObjects, array &$result): void + private function buildChunkResults(mixed $bulkResult, array $transformedObjects, array &$result): array { + $sideEffects = [ + 'created' => [], + 'updated' => [], + ]; + if (is_array($bulkResult) === false) { // Fallback for unexpected return format. $this->logger->warning("[SaveObjects] Unexpected bulk result format, using fallback"); @@ -1077,20 +1449,32 @@ private function buildChunkResults(mixed $bulkResult, array $transformedObjects, $result['statistics']['saved']++; } - return; + return $sideEffects; } // Check if we got complete objects (new approach) or just UUIDs (fallback). + // The mapper contract is raw magic-table rows carrying an `object_status` + // field — the previous gate looked for `created`/`updated` keys that raw + // rows never have (they are `_created`/`_updated`), which sent every bulk + // result down the legacy path and silently dropped classification, audit + // trails, and events. $firstItem = reset($bulkResult); - if (is_array($firstItem) === true && isset($firstItem['created'], $firstItem['updated']) === true) { + if (is_array($firstItem) === true && isset($firstItem['object_status']) === true) { // NEW APPROACH: Complete objects with database-computed classification returned. - $this->classifyDatabaseComputedResults(bulkResult: $bulkResult, result: $result); - return; + return $this->classifyDatabaseComputedResults(bulkResult: $bulkResult, result: $result); } - // FALLBACK: UUID array returned (legacy behavior). + // FALLBACK: UUID array returned (legacy behavior). This path cannot + // reconstruct entities, so it yields NO side effects: no audit rows are + // written and no lifecycle events are dispatched for these objects. + $this->logger->warning( + message: '[SaveObjects] Legacy uuid-array bulk result — audit trail and lifecycle events are skipped for this chunk', + context: ['objects' => count($bulkResult)] + ); $this->classifyLegacyResults(bulkResult: $bulkResult, transformedObjects: $transformedObjects, result: $result); + + return $sideEffects; }//end buildChunkResults() /** @@ -1102,14 +1486,22 @@ private function buildChunkResults(mixed $bulkResult, array $transformedObjects, * @param array $bulkResult The complete objects from bulk save with object_status * @param array $result The result array to populate * - * @return void + * @return array Side-effect payload: 'created' => list, + * 'updated' => list + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Per-status branching plus entity conversion fallbacks * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ - private function classifyDatabaseComputedResults(array $bulkResult, array &$result): void + private function classifyDatabaseComputedResults(array $bulkResult, array &$result): array { $this->logger->info("[SaveObjects] Processing complete objects with database-computed classification"); + $sideEffects = [ + 'created' => [], + 'updated' => [], + ]; + $createdCount = 0; $updatedCount = 0; $unchangedCount = 0; @@ -1118,21 +1510,48 @@ private function classifyDatabaseComputedResults(array $bulkResult, array &$resu // DATABASE-COMPUTED CLASSIFICATION: Use the object_status calculated by database. $objectStatus = $completeObject['object_status'] ?? 'unknown'; + // Retain the pre-update row (updates only) for the audit diff, and keep + // internal bookkeeping fields out of the raw row before conversion. + $preUpdateRow = ($completeObject['_pre_update_row'] ?? null); + unset($completeObject['_pre_update_row'], $completeObject['object_status'], $completeObject['operation_start_time']); + + // Convert the raw magic-table row to an ObjectEntity so the response + // carries the same serialized shape as the single-object save path. + $entity = $this->convertBulkRowToEntity(row: $completeObject); + $serialized = $completeObject; + if ($entity !== null) { + $serialized = $entity->jsonSerialize(); + } + switch ($objectStatus) { case 'created': - $result['saved'][] = $completeObject; + $result['saved'][] = $serialized; $result['statistics']['saved']++; $createdCount++; + if ($entity !== null) { + $sideEffects['created'][] = $entity; + } break; case 'updated': - $result['updated'][] = $completeObject; + $result['updated'][] = $serialized; $result['statistics']['updated']++; $updatedCount++; + if ($entity !== null) { + $oldEntity = null; + if (is_array($preUpdateRow) === true) { + $oldEntity = $this->convertBulkRowToEntity(row: $preUpdateRow); + } + + $sideEffects['updated'][] = [ + 'old' => $oldEntity, + 'new' => $entity, + ]; + } break; case 'unchanged': - $result['unchanged'][] = $completeObject; + $result['unchanged'][] = $serialized; $result['statistics']['unchanged']++; $unchangedCount++; break; @@ -1142,11 +1561,11 @@ private function classifyDatabaseComputedResults(array $bulkResult, array &$resu $this->logger->warning( "Unexpected object status: {$objectStatus}", [ - 'uuid' => $completeObject['uuid'], + 'uuid' => ($completeObject['_uuid'] ?? $completeObject['uuid'] ?? 'unknown'), 'object_status' => $objectStatus, ] ); - $result['unchanged'][] = $completeObject; + $result['unchanged'][] = $serialized; $result['statistics']['unchanged']++; $unchangedCount++; }//end switch @@ -1162,6 +1581,8 @@ private function classifyDatabaseComputedResults(array $bulkResult, array &$resu 'classification_method' => 'database_computed_sql', ] ); + + return $sideEffects; }//end classifyDatabaseComputedResults() /** @@ -1176,7 +1597,7 @@ private function classifyDatabaseComputedResults(array $bulkResult, array &$resu * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function classifyLegacyResults(array $bulkResult, array $transformedObjects, array &$result): void { @@ -1217,7 +1638,7 @@ private function classifyLegacyResults(array $bulkResult, array $transformedObje * @psalm-return array * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function performComprehensiveSchemaAnalysis(Schema $schema): array { @@ -1294,7 +1715,7 @@ function ($field) { * * @return bool The boolean value * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function castToBoolean($value): bool { @@ -1326,7 +1747,7 @@ private function castToBoolean($value): bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Inverse relation resolution requires many type checks * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function handleBulkInverseRelationsWithAnalysis(array &$preparedObjects, array $schemaAnalysis): void { @@ -1382,7 +1803,7 @@ private function handleBulkInverseRelationsWithAnalysis(array &$preparedObjects, * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function processInverseRelation( mixed $value, @@ -1431,7 +1852,7 @@ private function processInverseRelation( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function applyInverseRelationToTarget( string $targetUuid, @@ -1473,7 +1894,7 @@ private function applyInverseRelationToTarget( * * @throws Exception If there's an error during object creation * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function handlePreValidationCascading(array $object, Schema $schema, ?string $uuid): array { @@ -1500,7 +1921,7 @@ private function handlePreValidationCascading(array $object, Schema $schema, ?st * @SuppressWarnings(PHPMD.CyclomaticComplexity) Metadata hydration touches many field types * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function transformObjectsToDatabaseFormatInPlace(array &$objects, array $schemaCache): array { @@ -1593,7 +2014,7 @@ private function transformObjectsToDatabaseFormatInPlace(array &$objects, array * * @return array Updated selfData with identifiers set * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function generateObjectIdentifiers(array $selfData, array $object): array { @@ -1629,7 +2050,7 @@ private function generateObjectIdentifiers(array $selfData, array $object): arra * * @return array|null Null if valid, error array if invalid * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function validateObjectRequiredFields(array $selfData, array $object, int $index, array $schemaCache): ?array { @@ -1672,21 +2093,41 @@ private function validateObjectRequiredFields(array $selfData, array $object, in * * @return array Updated selfData with owner and organisation set * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function hydrateObjectMetadataFields(array $selfData): array { - // Set owner to current user if not provided (with null check). - if (isset($selfData['owner']) === false || empty($selfData['owner']) === true) { - $currentUser = $this->userSession->getUser(); + // SECURITY (BUG-OBJ-15): mirror single-save setSelfMetadata()/applyOwnerAttribution(). + // Owner is ALWAYS stamped to the session user and a client-supplied @self.owner is + // ignored — accepting it would let any bulk caller forge object ownership. Only when + // there is no session user (background/system context) do we fall back to the + // (possibly client-supplied) value so system imports keep working. + $currentUser = $this->userSession->getUser(); + if ($currentUser !== null) { + $selfData['owner'] = $currentUser->getUID(); + } else if (isset($selfData['owner']) === false || empty($selfData['owner']) === true) { $selfData['owner'] = null; - if ($currentUser !== null) { - $selfData['owner'] = $currentUser->getUID(); + } + + // SECURITY (BUG-OBJ-15): a client-supplied @self.organisation is only honoured when the + // caller is an admin OR is a verified member of that organisation. Otherwise we fall back + // to the caller's active organisation via getOrganisationForNewEntity() — preventing + // cross-tenant data injection through the bulk endpoint. + $requestedOrganisation = $selfData['organisation'] ?? null; + $acceptOrganisation = false; + if (empty($requestedOrganisation) === false) { + $isAdmin = $currentUser !== null + && $this->groupManager !== null + && $this->groupManager->isAdmin($currentUser->getUID()) === true; + + if ($isAdmin === true + || $this->organisationService->hasAccessToOrganisation((string) $requestedOrganisation) === true + ) { + $acceptOrganisation = true; } } - // Set organization using optimized OrganisationService method if not provided. - if (isset($selfData['organisation']) === false || empty($selfData['organisation']) === true) { + if ($acceptOrganisation === false) { // NO ERROR SUPPRESSION: Let organisation service errors bubble up immediately! $selfData['organisation'] = $this->organisationService->getOrganisationForNewEntity(); } @@ -1704,7 +2145,7 @@ private function hydrateObjectMetadataFields(array $selfData): array * * @return array The extracted business data * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function extractBusinessData(array $object): array { @@ -1763,14 +2204,16 @@ private function extractBusinessData(array $object): array * * @return array Array of ObjectEntity objects representing saved objects * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function reconstructSavedObjects(array $insertObjects, array $updateObjects, array $savedObjectIds, array $existingObjects): array { $savedObjects = []; // Build a lookup set of saved IDs for filtering — only reconstruct objects that were actually saved. - $savedIdSet = array_flip($savedObjectIds); + // Restrict to scalar ids first: array_flip warns and skips any non-scalar + // (e.g. a null id from a failed save), so filtering yields the same set cleanly. + $savedIdSet = array_flip(array_filter($savedObjectIds, 'is_scalar')); // CRITICAL FIX: Don't use createFromArray() - it tries to insert objects that already exist! // Instead, create ObjectEntity and hydrate without inserting. @@ -1844,7 +2287,7 @@ private function reconstructSavedObjects(array $insertObjects, array $updateObje * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Recursive relation scanning across nested structures * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function scanForRelations(array $data, string $prefix='', ?Schema $schema=null): array { @@ -1897,7 +2340,7 @@ private function scanForRelations(array $data, string $prefix='', ?Schema $schem * * @return array Relations found in this property * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function scanPropertyForRelation( string $key, @@ -1944,7 +2387,7 @@ private function scanPropertyForRelation( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Array scanning requires type-checking each element * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function scanArrayForRelations( string $key, @@ -1984,8 +2427,8 @@ private function scanArrayForRelations( continue; } - // For non-object arrays, check if the string looks like a reference. - if (trim($item) !== '' && $this->isReference(value: $item) === true) { + // For non-object arrays, record only genuine references. + if ($this->isRecordableReference(value: $item) === true) { $relations[$currentPath.'.'.$index] = $item; } }//end foreach @@ -2006,7 +2449,7 @@ private function scanArrayForRelations( * * @return array Relations found (empty array or single-element array) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function scanStringForRelation( string $key, @@ -2014,87 +2457,15 @@ private function scanStringForRelation( string $currentPath, ?array $schemaProperties ): array { - $isRelation = false; - - // Check schema property configuration first. + $propertyConfig = null; if ($schemaProperties !== null && isset($schemaProperties[$key]) === true) { $propertyConfig = $schemaProperties[$key]; - $propertyType = $propertyConfig['type'] ?? ''; - $propertyFormat = $propertyConfig['format'] ?? ''; - - // Check for explicit relation types. - if ($propertyType === 'text' && in_array($propertyFormat, ['uuid', 'uri', 'url']) === true) { - $isRelation = true; - } else if ($propertyType === 'object') { - // Object properties with string values are always relations. - $isRelation = true; - } } - // If not determined by schema, check for reference patterns. - if ($isRelation === false) { - $isRelation = $this->isReference(value: $value); - } - - if ($isRelation === true) { + if ($this->isRecordableReference(value: $value, propertyConfig: $propertyConfig) === true) { return [$currentPath => $value]; } return []; }//end scanStringForRelation() - - /** - * Determines if a string value should be treated as a reference to another object - * - * This method checks for various reference patterns including: - * - Standard UUIDs (e.g., "dec9ac6e-a4fd-40fc-be5f-e7ef6e5defb4") - * - Prefixed IDs (e.g., "id-819c2fe5-db4e-4b6f-8071-6a63fd400e34") - * - URLs - * - Other identifier patterns - * - * @param string $value The string value to check - * - * @return bool True if the value should be treated as a reference - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 - */ - private function isReference(string $value): bool - { - $value = trim($value); - - // Empty strings are not references. - if (empty($value) === true) { - return false; - } - - // Check for standard UUID pattern (8-4-4-4-12 format). - if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value) === 1) { - return true; - } - - // Check for prefixed UUID patterns (e.g., "id-uuid", "ref-uuid", etc.). - if (preg_match('/^[a-z]+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value) === 1) { - return true; - } - - // Check for URLs. - if (filter_var($value, FILTER_VALIDATE_URL) !== false) { - return true; - } - - // Check for other common ID patterns, but be more selective to avoid false positives. - // Only consider strings that look like identifiers, not regular text. - if (preg_match('/^[a-z0-9][a-z0-9_-]{7,}$/i', $value) === 1) { - // Must contain at least one hyphen or underscore (indicating it's likely an ID). - // AND must not contain spaces or common text words. - if ((strpos($value, '-') !== false || strpos($value, '_') !== false) - && preg_match('/\s/', $value) === 0 - && in_array(strtolower($value), ['applicatie', 'systeemsoftware', 'open-source', 'closed-source']) === false - ) { - return true; - } - } - - return false; - }//end isReference() }//end class diff --git a/lib/Service/Object/SaveObjects/BulkRelationHandler.php b/lib/Service/Object/SaveObjects/BulkRelationHandler.php index 15dee44f45..f9a2399158 100644 --- a/lib/Service/Object/SaveObjects/BulkRelationHandler.php +++ b/lib/Service/Object/SaveObjects/BulkRelationHandler.php @@ -8,18 +8,19 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object\SaveObjects; use OCA\OpenRegister\Db\MagicMapper; use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Service\Object\RelationDetectionTrait; use OCA\OpenRegister\Service\Object\SaveObjects\BulkValidationHandler; use Psr\Log\LoggerInterface; @@ -43,6 +44,8 @@ */ class BulkRelationHandler { + use RelationDetectionTrait; + /** * Constructor for BulkRelationHandler. * @@ -50,7 +53,7 @@ class BulkRelationHandler * @param MagicMapper $objectEntityMapper Mapper for object entities. * @param LoggerInterface $logger Logger for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function __construct( private readonly BulkValidationHandler $bulkValidHandler, @@ -82,7 +85,7 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex inverse relation handling with multiple conditions * @SuppressWarnings(PHPMD.NPathComplexity) Multiple code paths for different relation types * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function handleBulkInverseRelationsWithAnalysis(array &$preparedObjects, array $schemaAnalysis): void { @@ -200,7 +203,7 @@ public function handleBulkInverseRelationsWithAnalysis(array &$preparedObjects, * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Method handles complete post-save relation workflow * Else branches improve readability for array vs single value handling * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function handlePostSaveInverseRelations( array $savedObjects, @@ -335,7 +338,7 @@ public function handlePostSaveInverseRelations( * * Else branch used for early continue when UUID already present * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ private function performBulkWriteBackUpdatesWithContext(array $writeBackOperations): void { @@ -419,7 +422,7 @@ private function performBulkWriteBackUpdatesWithContext(array $writeBackOperatio * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex relation type detection with multiple conditions * Else branches handle schema vs heuristic detection paths * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-6 + * @spec openspec/specs/linked-entity-types/spec.md */ public function scanForRelations(array $data, string $prefix='', ?Schema $schema=null): array { @@ -444,33 +447,8 @@ public function scanForRelations(array $data, string $prefix='', ?Schema $schema // Handle string values (potential UUIDs/URLs). if (is_string($value) === true) { - // Check if it's a UUID or URL based on schema definition. - $isRelation = false; - - if ($propertyConfig !== null) { - $type = $propertyConfig['type'] ?? ''; - $format = $propertyConfig['format'] ?? ''; - - // Check for explicit relation types. - if ($type === 'text' && in_array($format, ['uuid', 'uri', 'url'], true) === true) { - $isRelation = true; - } else if ($type === 'object') { - // Type 'object' with a string value is always a relation. - $isRelation = true; - } - } - - if ($propertyConfig === null) { - // No schema info - use heuristics. - // If it looks like a UUID or URL, treat it as a relation. - if (\Symfony\Component\Uid\Uuid::isValid($value) === true) { - $isRelation = true; - } else if (filter_var($value, FILTER_VALIDATE_URL) !== false) { - $isRelation = true; - } - } - - if ($isRelation === true) { + // Record only genuine references — shared rule (RelationDetectionTrait). + if ($this->isRecordableReference(value: $value, propertyConfig: $propertyConfig) === true) { $relations[$currentPath] = $value; } } else if (is_array($value) === true) { diff --git a/lib/Service/Object/SaveObjects/BulkValidationHandler.php b/lib/Service/Object/SaveObjects/BulkValidationHandler.php index 798db31427..864a5cf7dc 100644 --- a/lib/Service/Object/SaveObjects/BulkValidationHandler.php +++ b/lib/Service/Object/SaveObjects/BulkValidationHandler.php @@ -53,7 +53,7 @@ class BulkValidationHandler * * @param LoggerInterface $logger Logger interface for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly LoggerInterface $logger, @@ -75,7 +75,7 @@ public function __construct( * * @return array Schema analysis with metadataFields, inverseProperties, validationRequired, etc. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function performComprehensiveSchemaAnalysis(Schema $schema): array { @@ -158,7 +158,7 @@ function ($field) { * * @return bool The boolean value. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function castToBoolean($value): bool { @@ -190,7 +190,7 @@ public function castToBoolean($value): bool * * @psalm-return list{array, string} * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function handlePreValidationCascading(array $object, ?string $uuid): array { diff --git a/lib/Service/Object/SaveObjects/ChunkProcessingHandler.php b/lib/Service/Object/SaveObjects/ChunkProcessingHandler.php deleted file mode 100644 index 774d6ca310..0000000000 --- a/lib/Service/Object/SaveObjects/ChunkProcessingHandler.php +++ /dev/null @@ -1,385 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister - */ - -namespace OCA\OpenRegister\Service\Object\SaveObjects; - -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Db\MagicMapper; -use OCA\OpenRegister\Db\RegisterMapper; -use OCA\OpenRegister\Db\SchemaMapper; -use OCA\OpenRegister\Db\Schema; -use Psr\Log\LoggerInterface; - -/** - * Handles processing of object chunks for bulk operations. - * - * This handler is responsible for: - * - Transforming objects to database format - * - Executing ultra-fast bulk save operations - * - Classifying objects (created/updated/unchanged) using database-computed status - * - Reconstructing saved objects - * - Aggregating results and statistics - * - * @category Service - * @package OCA\OpenRegister - * @author Conduction - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister - * @version 1.0.0 - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -class ChunkProcessingHandler -{ - /** - * Constructor for ChunkProcessingHandler. - * - * @param TransformationHandler $transformHandler Handler for object transformation. - * @param MagicMapper $unifiedObjectMapper Mapper for magic table operations. - * @param RegisterMapper $registerMapper Mapper for register operations. - * @param SchemaMapper $schemaMapper Mapper for schema operations. - * @param LoggerInterface $logger Logger for logging operations. - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-4 - */ - public function __construct( - private readonly TransformationHandler $transformHandler, - private readonly MagicMapper $unifiedObjectMapper, - private readonly RegisterMapper $registerMapper, - private readonly SchemaMapper $schemaMapper, - private readonly LoggerInterface $logger - ) { - }//end __construct() - - /** - * Process a chunk of objects for bulk save operations. - * - * This method orchestrates the complete chunk processing pipeline: - * 1. Transform objects to database format - * 2. Handle invalid objects - * 3. Execute ultra-fast bulk database save - * 4. Classify objects using database-computed status - * 5. Reconstruct objects for response - * 6. Return aggregated results and statistics - * - * @param array $objects Objects to process. - * @param array $schemaCache Schema cache for metadata. - * @param bool $_rbac RBAC flag (reserved). - * @param bool $_multitenancy Multitenancy flag (reserved). - * @param bool $_validation Validation flag (reserved). - * @param bool $_events Events flag (reserved). - * @param \OCA\OpenRegister\Db\Register|string|int|null $register The register. - * @param \OCA\OpenRegister\Db\Schema|string|int|null $schema The schema. - * - * @psalm-param array> $objects - * @psalm-param array $schemaCache - * @phpstan-param array> $objects - * @phpstan-param array $schemaCache - * - * @return array Array containing saved, updated, invalid objects and statistics. - * - * @psalm-return array{saved: list>, - * updated: list>, - * unchanged: list>, - * invalid: list>, - * errors: list>, - * statistics: array{saved: int, updated: int, unchanged: int, - * invalid: int, errors: int, processingTimeMs: float}} - * @phpstan-return array{saved: list>, - * updated: list>, - * unchanged: list>, - * invalid: list>, - * errors: list>, - * statistics: array} - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex bulk processing with multiple classification paths - * @SuppressWarnings(PHPMD.NPathComplexity) Many paths due to database-computed classification handling - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Complete chunk processing pipeline in single method - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flags for feature toggles in bulk operations - * Multiple conditional paths for object classification and reconstruction - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-4 - */ - public function processObjectsChunk( - array $objects, - array $schemaCache, - bool $_rbac, - bool $_multitenancy, - bool $_validation, - bool $_events, - \OCA\OpenRegister\Db\Register|string|int|null $register=null, - \OCA\OpenRegister\Db\Schema|string|int|null $schema=null - ): array { - $startTime = microtime(true); - - // Resolve register/schema if they are IDs. - if (is_int($register) === true || is_string($register) === true) { - try { - $registerMapper = \OC::$server->get(\OCA\OpenRegister\Db\RegisterMapper::class); - $register = $registerMapper->find((int) $register, _multitenancy: false); - } catch (\Exception $e) { - $this->logger->warning( - message: '[ChunkProcessingHandler] Failed to resolve register', - context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $register] - ); - $register = null; - } - } - - if (is_int($schema) === true || is_string($schema) === true) { - try { - $schemaMapper = \OC::$server->get(\OCA\OpenRegister\Db\SchemaMapper::class); - $schema = $schemaMapper->find((int) $schema, _multitenancy: false); - } catch (\Exception $e) { - $this->logger->warning( - message: '[ChunkProcessingHandler] Failed to resolve schema', - context: ['file' => __FILE__, 'line' => __LINE__, 'id' => $schema] - ); - $schema = null; - } - } - - $result = [ - 'saved' => [], - 'updated' => [], - 'unchanged' => [], - 'invalid' => [], - 'errors' => [], - 'statistics' => [ - 'saved' => 0, - 'updated' => 0, - 'unchanged' => 0, - 'invalid' => 0, - 'errors' => 0, - ], - ]; - - // STEP 1: Transform objects for database format with metadata hydration. - $transformationResult = $this->transformHandler->transformObjectsToDatabaseFormatInPlace( - objects: $objects, - schemaCache: $schemaCache - ); - $transformedObjects = $transformationResult['valid']; - - // PERFORMANCE OPTIMIZATION: Batch error processing. - if (empty($transformationResult['invalid']) === false) { - $invalidCount = count($transformationResult['invalid']); - $result['invalid'] = array_merge($result['invalid'], $transformationResult['invalid']); - $result['statistics']['invalid'] += $invalidCount; - - // Initialize errors counter if needed. - if (array_key_exists('errors', $result['statistics']) === false) { - $result['statistics']['errors'] = 0; - } - - $result['statistics']['errors'] += $invalidCount; - } - - if (empty($transformedObjects) === true) { - $endTime = microtime(true); - $result['statistics']['processingTimeMs'] = round(($endTime - $startTime) * 1000, 2); - return $result; - } - - // REVOLUTIONARY APPROACH: Skip database lookup entirely and use single-call processing. - $this->logger->info( - message: '[ChunkProcessingHandler] Using single-call bulk processing (no pre-lookup needed)', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'objects_to_process' => count($transformedObjects), - 'approach' => 'INSERT...ON DUPLICATE KEY UPDATE with database-computed classification', - ] - ); - - // STEP 2: DIRECT BULK PROCESSING - No categorization needed upfront. - $unchangedObjects = []; - - // Update statistics for unchanged objects (skipped because content was unchanged). - $result['statistics']['unchanged'] = count($unchangedObjects); - $result['unchanged'] = array_map( - function ($obj) { - if (is_array($obj) === true) { - return $obj; - } - - return $obj->jsonSerialize(); - }, - $unchangedObjects - ); - - // STEP 3: ULTRA-FAST BULK DATABASE OPERATIONS. - // Register & schema are now passed as parameters (already resolved in function entry). - // Route through MagicMapper for magic table operations. - $bulkResult = $this->unifiedObjectMapper->ultraFastBulkSave( - insertObjects: $transformedObjects, - updateObjects: [], - register: $register, - schema: $schema - ); - - // STEP 4: ENHANCED PROCESSING - Handle complete objects with timestamp-based classification. - $savedObjectIds = []; - $createdObjects = []; - $updatedObjects = []; - $unchangedObjects = []; - $reconstructedObjects = []; - - if (is_array($bulkResult) !== true) { - // Fallback for unexpected return format. - $this->logger->warning( - message: '[ChunkProcessingHandler] Unexpected bulk result format, using fallback', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - foreach ($transformedObjects ?? [] as $objData) { - $savedObjectIds[] = $objData['uuid']; - $result['statistics']['saved']++; - } - } - - if (is_array($bulkResult) === true) { - // Check if we got complete objects (new approach) or just UUIDs (fallback). - $firstItem = reset($bulkResult); - $hasDatabaseStatus = is_array($firstItem) === true - && isset($firstItem['object_status']) === true; - - if ($hasDatabaseStatus === true) { - // NEW APPROACH: Complete objects with database-computed classification returned. - $this->logger->info( - message: '[ChunkProcessingHandler] Processing complete objects with database-computed classification', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - foreach ($bulkResult as $completeObject) { - $savedObjectIds[] = $completeObject['_uuid']; - - // Convert to ObjectEntity for consistent response format. - $objEntity = new ObjectEntity(); - $objEntity->hydrate($completeObject); - $reconstructedObjects[] = $objEntity; - - // DATABASE-COMPUTED CLASSIFICATION: Use the object_status calculated by database. - $objectStatus = $completeObject['object_status'] ?? 'unknown'; - - switch ($objectStatus) { - case 'created': - $createdObjects[] = $completeObject; - $result['saved'][] = $objEntity->jsonSerialize(); - $result['statistics']['saved']++; - break; - - case 'updated': - $updatedObjects[] = $completeObject; - $result['updated'][] = $objEntity->jsonSerialize(); - $result['statistics']['updated']++; - break; - - case 'unchanged': - $unchangedObjects[] = $completeObject; - $result['unchanged'][] = $objEntity->jsonSerialize(); - $result['statistics']['unchanged']++; - break; - - default: - // Fallback for unexpected status. - $this->logger->warning( - message: "[ChunkProcessingHandler] Unexpected object status: {$objectStatus}", - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'uuid' => $completeObject['uuid'], - 'object_status' => $objectStatus, - ] - ); - $unchangedObjects[] = $completeObject->jsonSerialize(); - $result['unchanged'][] = $objEntity; - $result['statistics']['unchanged']++; - }//end switch - }//end foreach - - $this->logger->info( - message: '[ChunkProcessingHandler] Database-computed classification completed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'total_processed' => count($bulkResult), - 'created_objects' => count($createdObjects), - 'updated_objects' => count($updatedObjects), - 'unchanged_objects' => count($unchangedObjects), - 'classification_method' => 'database_computed_sql', - ] - ); - }//end if - - if ($hasDatabaseStatus !== true) { - // FALLBACK: UUID array returned (legacy behavior). - $this->logger->info( - message: '[ChunkProcessingHandler] Processing UUID array (legacy mode)', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - $savedObjectIds = $bulkResult; - - // Fallback counting (less precise). - foreach ($transformedObjects ?? [] as $objData) { - if (in_array($objData['uuid'], $bulkResult, true) === true) { - $result['statistics']['saved']++; - } - } - }//end if - }//end if - - // STEP 5: ENHANCED OBJECT RESPONSE - Already populated in STEP 4. - // The result arrays (saved, updated, unchanged) were populated during the classification loop above. - if (empty($reconstructedObjects) === false) { - $this->logger->info( - message: '[ChunkProcessingHandler] Using database-computed pre-classified objects for response', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'saved_objects' => count($result['saved']), - 'updated_objects' => count($result['updated']), - 'unchanged_objects' => count($result['unchanged']), - ] - ); - } - - if (empty($reconstructedObjects) === true) { - // FALLBACK: Use traditional object reconstruction (placeholder). - $this->logger->info( - message: '[ChunkProcessingHandler] Using fallback object reconstruction', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - - // Fallback classification (less precise). - foreach ($transformedObjects ?? [] as $objData) { - if (in_array($objData['uuid'], $savedObjectIds, true) === true) { - $result['saved'][] = $objData; - } - } - } - - // STEP 6: Calculate processing time. - $endTime = microtime(true); - $processingTime = round(($endTime - $startTime) * 1000, 2); - - // Add processing time to the result for transparency and performance monitoring. - $result['statistics']['processingTimeMs'] = $processingTime; - - return $result; - }//end processObjectsChunk() -}//end class diff --git a/lib/Service/Object/SaveObjects/PreparationHandler.php b/lib/Service/Object/SaveObjects/PreparationHandler.php index 5326dffddd..66c416641d 100644 --- a/lib/Service/Object/SaveObjects/PreparationHandler.php +++ b/lib/Service/Object/SaveObjects/PreparationHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object\SaveObjects; @@ -64,7 +64,7 @@ class PreparationHandler * @param BulkValidationHandler $bulkValidHandler Handler for schema analysis. * @param IUserSession $userSession User session for owner assignment. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly SaveObject $saveHandler, @@ -102,7 +102,7 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) Many conditional paths for metadata extraction * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Complete preparation pipeline with multiple steps * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ public function prepareObjectsForBulkSave(array $objects): array { @@ -224,7 +224,7 @@ public function prepareObjectsForBulkSave(array $objects): array * * @return Schema The loaded schema. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ private function loadSchemaWithCache($schemaId): Schema { @@ -247,7 +247,7 @@ private function loadSchemaWithCache($schemaId): Schema * * @return array The schema analysis with metadataFields, inverseProperties, and configuration. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ private function getSchemaAnalysisWithCache(Schema $schema): array { @@ -263,7 +263,7 @@ private function getSchemaAnalysisWithCache(Schema $schema): array * * @return array The processed object. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ private function handlePreValidationCascading(array $object, string $uuid): array { @@ -287,7 +287,7 @@ private function handlePreValidationCascading(array $object, string $uuid): arra * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-4 + * @spec openspec/specs/object-lifecycle/spec.md */ private function handleBulkInverseRelationsWithAnalysis(array &$preparedObjects, array $schemaAnalysis): void { diff --git a/lib/Service/Object/SaveObjects/TransformationHandler.php b/lib/Service/Object/SaveObjects/TransformationHandler.php index 6dd92921a9..f9e70ee594 100644 --- a/lib/Service/Object/SaveObjects/TransformationHandler.php +++ b/lib/Service/Object/SaveObjects/TransformationHandler.php @@ -8,14 +8,14 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ namespace OCA\OpenRegister\Service\Object\SaveObjects; @@ -90,7 +90,7 @@ public function __construct( * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Method handles complete transformation workflow * Else branches handle different object structures and fallbacks * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function transformObjectsToDatabaseFormatInPlace(array &$objects, array $schemaCache): array { diff --git a/lib/Service/Object/SearchQueryHandler.php b/lib/Service/Object/SearchQueryHandler.php index 6abe084700..827409106c 100644 --- a/lib/Service/Object/SearchQueryHandler.php +++ b/lib/Service/Object/SearchQueryHandler.php @@ -21,9 +21,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-89 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-95 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-96 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-saved-searches-and-search-trails + * @spec openspec/specs/zoeken-filteren/spec.md + * @spec openspec/specs/zoeken-filteren/spec.md */ declare(strict_types=1); @@ -34,6 +34,7 @@ use OCA\OpenRegister\Db\ViewMapper; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Service\SettingsService; +use OCA\OpenRegister\Service\SearchTrailService; use OCP\IRequest; use Psr\Log\LoggerInterface; @@ -56,23 +57,59 @@ */ class SearchQueryHandler { + + /** + * Memoized effective search-trail recording mode for this request. + * + * The recording mode is read from settings once per request instead of + * twice per search (getEffectiveRecordingMode() + the enabled check in + * logSearchTrail()). Null until the first read. Long-running processes + * (CLI) keep the first value for their lifetime, which is acceptable + * for a best-effort analytics trail. + * + * @var string|null + */ + private ?string $recordingModeMemo = null; + + /** + * In-request buffer of search-trail entries pending persistence. + * + * Entries accumulate during the request and are flushed after the + * response by a shutdown function (mirrors ProcessingLogService's + * buffered emission), keeping the INSERT off the hot search path. + * The trail is best-effort: losing buffered rows on a fatal is + * acceptable. + * + * @var array + */ + private array $searchTrailBuffer = []; + + /** + * Whether the shutdown flush for the trail buffer is registered. + * + * @var boolean + */ + private bool $trailFlushRegistered = false; + /** * SearchQueryHandler constructor. * - * @param ViewMapper $viewMapper Mapper for view operations. - * @param SchemaMapper $schemaMapper Mapper for schema operations. - * @param SettingsService $settingsService Service for settings operations. - * @param LoggerInterface $logger Logger for performance monitoring. - * @param IRequest $request Request object. + * @param ViewMapper $viewMapper Mapper for view operations. + * @param SchemaMapper $schemaMapper Mapper for schema operations. + * @param SettingsService $settingsService Service for settings operations. + * @param LoggerInterface $logger Logger for performance monitoring. + * @param IRequest $request Request object. + * @param SearchTrailService $searchTrailService Service for recording search trails. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function __construct( private readonly ViewMapper $viewMapper, private readonly SchemaMapper $schemaMapper, private readonly SettingsService $settingsService, private readonly LoggerInterface $logger, - private readonly IRequest $request + private readonly IRequest $request, + private readonly SearchTrailService $searchTrailService ) { }//end __construct() @@ -96,8 +133,8 @@ public function __construct( * @SuppressWarnings(PHPMD.NPathComplexity) Many paths for handling different parameter formats * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Handles extensive parameter processing for query building * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-95 + * @spec openspec/specs/zoeken-filteren/spec.md + * @spec openspec/specs/zoeken-filteren/spec.md */ public function buildSearchQuery( array $requestParams, @@ -278,7 +315,7 @@ public function buildSearchQuery( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex view merging with multiple filter types * @SuppressWarnings(PHPMD.NPathComplexity) Multiple view filter paths for registers, schemas, and search terms * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function applyViewsToQuery(array $query, array $viewIds): array { @@ -386,23 +423,6 @@ public function applyViewsToQuery(array $query, array $viewIds): array return $query; }//end applyViewsToQuery() - /** - * Check if SOLR search engine is available - * - * @return bool True if SOLR is enabled and available, false otherwise - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 - */ - public function isSolrAvailable(): bool - { - try { - $solrSettings = $this->settingsService->getSolrSettings(); - return $solrSettings['enabled'] ?? false; - } catch (Exception $e) { - return false; - } - }//end isSolrAvailable() - /** * Clean and normalize query parameters * @@ -415,7 +435,7 @@ public function isSolrAvailable(): bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple conditional paths for parameter normalization * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function cleanQuery(array $parameters): array { @@ -485,8 +505,8 @@ public function cleanQuery(array $parameters): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-96 + * @spec openspec/specs/zoeken-filteren/spec.md + * @spec openspec/specs/zoeken-filteren/spec.md */ public function addPaginationUrls(array &$paginatedResults, int $page, int $pages): void { @@ -531,7 +551,7 @@ public function addPaginationUrls(array &$paginatedResults, int $page, int $page * * @psalm-return '&'|'?' * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/zoeken-filteren/spec.md */ private function getUrlSeparator(string $url): string { @@ -543,10 +563,13 @@ private function getUrlSeparator(string $url): string }//end getUrlSeparator() /** - * Log search trail entry + * Buffer a search trail entry for deferred persistence * - * Creates a search trail entry if search trails are enabled in settings. - * Logs query, result counts, and execution time for analytics and debugging. + * Records a search trail entry if search trails are enabled in settings. + * The entry is buffered in-request and persisted after the response by a + * shutdown function (see flushSearchTrails()), so the trail INSERT never + * adds latency to the search itself. The trail is best-effort by + * contract: buffered rows lost on a fatal are acceptable. * * @param array $_query Search query array. * @param int $_resultCount Number of results returned. @@ -556,7 +579,7 @@ private function getUrlSeparator(string $url): string * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-10 + * @spec openspec/specs/search-trail-recording/spec.md */ public function logSearchTrail( array $_query, @@ -565,47 +588,131 @@ public function logSearchTrail( float $_executionTime, string $_executionType='sync' ): void { - try { - // Only create search trail if search trails are enabled. - if ($this->isSearchTrailsEnabled() === true) { - // Create the search trail entry using the service with actual execution time. - // TODO - // $this->searchTrailService->createSearchTrail( - // Query: $query, - // ResultCount: $resultCount, - // TotalResults: $totalResults, - // ResponseTime: $executionTime, - // ExecutionType: $executionType. - // );. - } - } catch (Exception $e) { - // Log the error but don't fail the request. + // Only record when trails are enabled. Reuses the memoized recording + // mode so the settings are read at most once per request instead of a + // second time here. + if ($this->getEffectiveRecordingMode() === 'none') { + return; + } + + $this->searchTrailBuffer[] = [ + 'query' => $_query, + 'resultCount' => $_resultCount, + 'totalResults' => $_totalResults, + 'responseTime' => $_executionTime, + 'executionType' => $_executionType, + ]; + + // Register the deferred flush once per request; it runs after the + // response has been generated so the write cost is off the hot path. + if ($this->trailFlushRegistered === false) { + $this->trailFlushRegistered = true; + register_shutdown_function([$this, 'flushSearchTrails']); } }//end logSearchTrail() /** - * Check if search trails are enabled in the settings + * Flush buffered search-trail entries to storage * - * @return bool True if search trails are enabled, false otherwise + * Runs as a shutdown function after the response, and may be called + * directly (tests, CLI) to force persistence. Fail-soft per entry: a + * failed insert is logged and dropped — the trail is best-effort and + * must never surface an error to the request. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-89 + * @return int Number of entries persisted. + * + * @spec openspec/specs/search-trail-recording/spec.md + */ + public function flushSearchTrails(): int + { + $persisted = 0; + $entries = $this->searchTrailBuffer; + + // Clear the buffer up front so re-entrant calls never double-write. + $this->searchTrailBuffer = []; + + foreach ($entries as $entry) { + try { + // The mapper extracts the search term from query['_search'] into + // the search_term column that the popular-terms stats aggregate. + $this->searchTrailService->createSearchTrail( + query: $entry['query'], + resultCount: $entry['resultCount'], + totalResults: $entry['totalResults'], + responseTime: $entry['responseTime'], + executionType: $entry['executionType'] + ); + $persisted++; + } catch (\Throwable $e) { + // Log the error but never fail: the trail is best-effort. + $this->logger->warning( + message: '[SearchQueryHandler] Failed to record search trail', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end foreach + + return $persisted; + }//end flushSearchTrails() + + /** + * Resolve the effective search-trail recording mode. + * + * Returns 'none' when search trails are disabled (master switch / + * back-compat), otherwise the configured `searchTrailRecordingMode` + * ('all', '_search', or 'none'; default '_search'). Falls back to + * '_search' if settings cannot be read. The resolved mode is memoized + * per request so repeated calls (mode gate + logSearchTrail) read the + * settings once. + * + * @return string One of 'all', '_search', 'none'. + * + * @spec openspec/specs/search-trail-recording/spec.md + */ + public function getEffectiveRecordingMode(): string + { + if ($this->recordingModeMemo !== null) { + return $this->recordingModeMemo; + } + + $this->recordingModeMemo = $this->resolveRecordingMode(); + + return $this->recordingModeMemo; + }//end getEffectiveRecordingMode() + + /** + * Read the recording mode from settings (uncached). + * + * @return string One of 'all', '_search', 'none'. */ - public function isSearchTrailsEnabled(): bool + private function resolveRecordingMode(): string { try { $retentionSettings = $this->settingsService->getRetentionSettingsOnly(); - return $retentionSettings['searchTrailsEnabled'] ?? true; + if (($retentionSettings['searchTrailsEnabled'] ?? true) === false) { + return 'none'; + } + + $mode = $retentionSettings['searchTrailRecordingMode'] ?? '_search'; + if (in_array($mode, ['all', '_search', 'none'], true) === true) { + return $mode; + } + + return '_search'; } catch (Exception $e) { - // If we can't get settings, default to enabled for safety. $this->logger->warning( - message: '[SearchQueryHandler] Failed to check search trails setting, defaulting to enabled', + message: '[SearchQueryHandler] Failed to read recording mode, defaulting to _search', context: [ 'file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage(), ] ); - return true; - } - }//end isSearchTrailsEnabled() + return '_search'; + }//end try + }//end resolveRecordingMode() }//end class diff --git a/lib/Service/Object/TranslationHandler.php b/lib/Service/Object/TranslationHandler.php index 421a78fce3..6cfa9a33c7 100644 --- a/lib/Service/Object/TranslationHandler.php +++ b/lib/Service/Object/TranslationHandler.php @@ -28,6 +28,7 @@ use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Exception\TranslationTargetConflictException; use OCA\OpenRegister\Service\LanguageService; use Psr\Log\LoggerInterface; @@ -51,7 +52,7 @@ class TranslationHandler * @param LanguageService $languageService The request-scoped language service * @param LoggerInterface $logger Logger interface * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly LanguageService $languageService, @@ -69,7 +70,7 @@ public function __construct( * * @return string[] Array of translatable property names * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getTranslatableProperties(Schema $schema): array { @@ -103,7 +104,7 @@ public function getTranslatableProperties(Schema $schema): array * * @return array The object data with resolved translations * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function resolveTranslationsForRender( array $objectData, @@ -194,18 +195,26 @@ public function resolveTranslationsForRender( /** * Normalize translatable properties in object data for saving. * - * For each translatable property: - * - If the value is already a language-keyed object, stores as-is - * - If the value is a simple (non-array) value, wraps it under the default language - * - Validates that the default language always has a value + * Resolution per property: + * - Already a language-keyed object (e.g. `{nl: ..., en: ...}`): + * keep as-is. When `X-Translation-Target-Language` is also set on + * the request, throw `TranslationTargetConflictException` to + * surface conflicting intent to the consumer. + * - Scalar value AND `X-Translation-Target-Language: ` on the + * request: wrap under the target language. + * - Scalar value AND no target header: wrap under the register + * default (legacy behaviour preserved). * - * @param array $objectData The incoming object data - * @param Schema $schema The schema for property definitions - * @param Register|null $register The register for language configuration + * @param array $objectData The incoming object data. + * @param Schema $schema The schema for property definitions. + * @param Register|null $register The register for language configuration. * - * @return array The normalized object data with translations wrapped correctly + * @return array The normalized object data with translations wrapped correctly. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @throws TranslationTargetConflictException When a full language-keyed body is + * paired with `X-Translation-Target-Language`. + * + * @spec openspec/changes/i18n-api-language-negotiation/tasks.md#phase-2 */ public function normalizeTranslationsForSave( array $objectData, @@ -223,6 +232,8 @@ public function normalizeTranslationsForSave( $defaultLanguage = $register->getDefaultLanguage(); } + $targetLanguage = $this->languageService->getTargetLanguage(); + foreach ($translatableProps as $propName) { if (isset($objectData[$propName]) === false) { continue; @@ -232,6 +243,14 @@ public function normalizeTranslationsForSave( // If it's already a language-keyed object, validate and keep. if (is_array($value) === true && $this->isLanguageKeyedObject(value: $value) === true) { + if ($targetLanguage !== null && $targetLanguage !== '') { + // Shape D — conflicting intent. Fail fast. + throw new TranslationTargetConflictException( + property: $propName, + targetLanguage: $targetLanguage + ); + } + // Ensure default language has a value. if (isset($value[$defaultLanguage]) === false || $value[$defaultLanguage] === null) { $this->logger->warning( @@ -247,11 +266,17 @@ public function normalizeTranslationsForSave( $objectData[$propName] = $value; continue; - } + }//end if - // Simple value: wrap under the default language. + // Simple value path. if ($value !== null) { - $objectData[$propName] = [$defaultLanguage => $value]; + if ($targetLanguage !== null && $targetLanguage !== '') { + // Shape B — wrap under the target language. + $objectData[$propName] = [$targetLanguage => $value]; + } else { + // Shape A — legacy: wrap under register default. + $objectData[$propName] = [$defaultLanguage => $value]; + } } }//end foreach @@ -268,7 +293,7 @@ public function normalizeTranslationsForSave( * * @return bool True if this looks like a language-keyed object * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ private function isLanguageKeyedObject(array $value): bool { diff --git a/lib/Service/Object/UtilityHandler.php b/lib/Service/Object/UtilityHandler.php index b8a82898f5..75972f6366 100644 --- a/lib/Service/Object/UtilityHandler.php +++ b/lib/Service/Object/UtilityHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -46,7 +46,7 @@ class UtilityHandler * @param RegisterMapper $registerMapper Mapper for register entities. * @param SchemaMapper $schemaMapper Mapper for schema entities. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly RegisterMapper $registerMapper, @@ -66,7 +66,7 @@ public function __construct( * @psalm-return bool * @phpstan-return bool * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function isUuid($value): bool { @@ -106,7 +106,7 @@ public function isUuid($value): bool * @psalm-return array * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function normalizeToArray($value): array { @@ -130,7 +130,7 @@ public function normalizeToArray($value): array * @psalm-return '&'|'?' * @phpstan-return '&'|'?' * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getUrlSeparator(string $url): string { @@ -155,7 +155,7 @@ public function getUrlSeparator(string $url): string * @psalm-return mixed * @phpstan-return mixed * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function normalizeEntity($entity, string $type) { @@ -187,7 +187,7 @@ public function normalizeEntity($entity, string $type) * @psalm-return string * @phpstan-return string * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function calculateEfficiency(array $lookupMap, float $totalTime): string { @@ -214,7 +214,7 @@ public function calculateEfficiency(array $lookupMap, float $totalTime): string * @psalm-return array * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-1 + * @spec openspec/specs/object-lifecycle/spec.md */ public function cleanQuery(array $parameters): array { diff --git a/lib/Service/Object/ValidateObject.php b/lib/Service/Object/ValidateObject.php index 5a276adba1..95c329e9ac 100644 --- a/lib/Service/Object/ValidateObject.php +++ b/lib/Service/Object/ValidateObject.php @@ -26,7 +26,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-63 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ namespace OCA\OpenRegister\Service\Object; @@ -38,10 +38,12 @@ use OCA\OpenRegister\Db\MagicMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\ObjectHandling; use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Exception\CustomValidationException; use OCA\OpenRegister\Formats\BsnFormat; use OCA\OpenRegister\Formats\ExtendedFieldTypeValidator; +use OCA\OpenRegister\Formats\Iso8601DateTimeFormat; use OCA\OpenRegister\Formats\SemVerFormat; use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; @@ -84,6 +86,36 @@ class ValidateObject */ public const VALIDATION_ERROR_MESSAGE = 'Invalid object'; + /** + * Request-scoped memoized validator instance. + * + * Building an Opis Validator and registering the custom formats and the + * http protocol resolver on every validateObject() call is pure overhead: + * the configuration is identical for every validation in a request. The + * shared instance also lets the Opis schema loader reuse `$ref` documents + * it already resolved (each of which costs a SchemaMapper::find round + * trip through resolveSchema()) instead of re-resolving them per call. + * + * @var Validator|null + */ + private ?Validator $validator = null; + + /** + * Request-scoped cache of fully prepared validation schemas. + * + * Keyed by `{schemaId}:{schemaVersion}` (mirroring SchemaMapper's + * findCache pattern — a version bump produces a new key, so stale + * entries are never served). Each entry holds the schema object after + * the complete per-schema preparation pipeline (reference/circular-ref + * transformation, metadata cleaning, computed-property stripping from + * `required`, null-type widening) plus the derived computed-property + * and required-field lists, so validating N objects against the same + * schema runs the pipeline once instead of N times. + * + * @var array, required: array}> + */ + private array $preparedSchemaCache = []; + /** * Constructor for ValidateObject * @@ -93,7 +125,7 @@ class ValidateObject * @param IURLGenerator $urlGenerator URL generator. * @param LoggerInterface $logger Logger for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function __construct( private IAppConfig $config, @@ -120,7 +152,7 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Schema reference resolution requires multiple type checks * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flag needed for backward compatibility * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function preprocessSchemaReferences( object $schemaObject, @@ -181,7 +213,7 @@ private function preprocessSchemaReferences( * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex reference resolution with multiple format handlers * @SuppressWarnings(PHPMD.NPathComplexity) Multiple reference types and nested schema scenarios * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function resolveSchemaProperty(object $propertySchema, array $visited=[]): object { @@ -288,7 +320,7 @@ private function resolveSchemaProperty(object $propertySchema, array $visited=[] * * @return object The transformed schema object * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function transformOpenRegisterObjectConfigurations(object $schemaObject): object { @@ -317,7 +349,7 @@ private function transformOpenRegisterObjectConfigurations(object $schemaObject) * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple OpenRegister configuration scenarios * @SuppressWarnings(PHPMD.NPathComplexity) Various property transformation paths * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function transformPropertyForOpenRegister(object $propertySchema): void { @@ -391,10 +423,24 @@ private function transformPropertyForOpenRegister(object $propertySchema): void $itemsSchema->pattern = $uuidPat; $itemsSchema->description = 'UUID reference to a related object (inversedBy - should be empty)'; unset($itemsSchema->properties, $itemsSchema->required, $itemsSchema->{'$ref'}); + } else if (($itemsSchema->{'$ref'} ?? null) !== null) { + // Array items that $ref another schema are relation references + // stored as UUID strings (e.g. assignment.briefingMaterialIds, + // item-bank.itemIds declared as items {"$ref":"","format": + // "uuid"}). OpenRegister uses the $ref only for relation tracking; + // Opis JSON Schema would otherwise try to resolve the bare slug as + // a URI and fail with "Unresolved reference: schema:///#". + // Treat the items as opaque UUID strings for validation — this + // mirrors how single string $ref props (below) and array + // self-references (transformSchemaForValidation Step 1) are handled. + $itemsSchema->type = 'string'; + $itemsSchema->pattern = $uuidPat; + $itemsSchema->description = 'UUID reference to a related object'; + unset($itemsSchema->properties, $itemsSchema->required, $itemsSchema->{'$ref'}); } else if (isset($itemsSchema->type) === true && $itemsSchema->type === 'object') { $this->transformObjectPropertyForOpenRegister(objectSchema: $itemsSchema); - } - } + }//end if + }//end if // Handle direct object properties. if (($propertySchema->type ?? null) !== null && $propertySchema->type === 'object') { @@ -435,7 +481,7 @@ private function transformPropertyForOpenRegister(object $propertySchema): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function transformObjectPropertyForOpenRegister(object $objectSchema): void { @@ -448,6 +494,7 @@ private function transformObjectPropertyForOpenRegister(object $objectSchema): v } switch ($handling) { + case 'related-schema': case 'related-object': // For related objects, expect UUID strings instead of full objects. $this->transformToUuidProperty(objectSchema: $objectSchema); @@ -471,7 +518,7 @@ private function transformObjectPropertyForOpenRegister(object $objectSchema): v * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function transformToUuidProperty(object $objectSchema): void { @@ -544,7 +591,7 @@ private function transformToUuidProperty(object $objectSchema): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function transformToNestedObjectProperty(object $objectSchema): void { @@ -598,7 +645,7 @@ private function transformToNestedObjectProperty(object $objectSchema): void * * @return string|null The handling value or null if not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function extractObjectConfigurationHandling(object $propertySchema): ?string { @@ -651,7 +698,7 @@ private function extractObjectConfigurationHandling(object $propertySchema): ?st * * @return string|null The handling value or null if not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function extractHandlingFromOneOfItems($oneOf): ?string { @@ -683,7 +730,7 @@ private function extractHandlingFromOneOfItems($oneOf): ?string * * @return mixed The value or null if not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getMixedValue($data, string $key) { @@ -716,8 +763,9 @@ private function getMixedValue($data, string $key) * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex schema transformation with multiple scenarios * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive schema transformation logic + * @SuppressWarnings(PHPMD.StaticAccess) ObjectHandling::relates is a stateless enum-style helper * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function transformSchemaForValidation(object $schemaObject, array $object, string $currentSchemaSlug): array { @@ -746,7 +794,7 @@ private function transformSchemaForValidation(object $schemaObject, array $objec // UUID pattern for related object references. $uuidPat = '^([a-z]+-)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32}|[0-9]+)$'; - if ($config !== null && $handling === 'related-object') { + if ($config !== null && ObjectHandling::relates($handling) === true) { // Handle inversedBy relationships for single objects. if (($propertySchema->inversedBy ?? null) !== null) { // For inversedBy properties, allow objects, UUIDs, or null @@ -860,7 +908,7 @@ private function transformSchemaForValidation(object $schemaObject, array $objec * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flag needed to handle array items differently * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function cleanSchemaForValidation(object $schemaObject, bool $_isArrayItems=false): object { @@ -924,7 +972,7 @@ private function cleanSchemaForValidation(object $schemaObject, bool $_isArrayIt * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flag needed to handle array items differently * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function cleanPropertyForValidation($propertySchema, bool $isArrayItems=false) { @@ -1004,7 +1052,7 @@ private function cleanPropertyForValidation($propertySchema, bool $isArrayItems= * * @return object The property schema with constraints moved to items level * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function fixMisplacedArrayConstraints(object $propertySchema): object { @@ -1070,7 +1118,7 @@ private function fixMisplacedArrayConstraints(object $propertySchema): object * * @return object The transformed property schema with valid JSON Schema types * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function transformCustomTypeToJsonSchemaType(object $propertySchema): object { @@ -1130,7 +1178,7 @@ function ($t) use ($customTypeMap) { * * @return object The transformed items schema * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function transformArrayItemsForValidation(object $itemsSchema): object { @@ -1150,6 +1198,20 @@ private function transformArrayItemsForValidation(object $itemsSchema): object $handling = $config->handling; } + // Relation markers are the only signal that these items describe a reference to + // another object rather than an inline value object: explicit objectConfiguration + // handling, or a $ref pointing at another schema. + $hasRelationMarkers = ($config !== null && $handling !== null) || (($itemsSchema->{'$ref'} ?? null) !== null); + + // Inline value-object arrays (e.g. [{register, label}]) declare their own properties + // and carry no relation markers at all. They are not references to other objects, so + // they must be left untouched - replacing their properties with a bare {id} stub while + // an authored `additionalProperties: false` survives the clean would reject every one + // of the schema's own sub-properties. See or#290. + if ($hasRelationMarkers === false && isset($itemsSchema->properties) === true) { + return $itemsSchema; + } + // Determine whether to use UUID strings or simple object structure. // UUID strings: related-object handling, $ref references, or unknown handling types. // Simple object: nested-object handling or no configuration and no $ref. @@ -1221,7 +1283,7 @@ private function transformArrayItemsForValidation(object $itemsSchema): object * * @return bool True if this is a self-reference * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function isSelfReference(object $propertySchema, string $schemaSlug): bool { @@ -1256,7 +1318,7 @@ private function isSelfReference(object $propertySchema, string $schemaSlug): bo * * @return Schema|null The found schema or null if not found * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function findSchemaBySlug(string $slug): ?Schema { @@ -1303,8 +1365,8 @@ private function findSchemaBySlug(string $slug): ?Schema * @SuppressWarnings(PHPMD.NPathComplexity) Multiple validation scenarios and schema types * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Complete validation logic requires extensive handling * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-63 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function validateObject( array $object, @@ -1313,78 +1375,67 @@ public function validateObject( int $_depth=0 ): ValidationResult { + // Resolve a schema id to its entity once so downstream steps (unique-field + // validation, prepared-schema caching) always work with the entity. + // SchemaMapper::find() memoizes lookups in its own findCache. + if ($schema !== null && ($schema instanceof Schema) === false) { + $schema = $this->schemaMapper->find($schema); + } + // Use == because === will never be true when comparing stdClass-instances. // Phpcs:ignore Squiz.Operators.ComparisonOperatorUsage.NotAllowed - if ($schemaObject == new stdClass()) { - if ($schema instanceof Schema) { - $schemaObject = $schema->getSchemaObject($this->urlGenerator); - } else if ($schema !== null) { - // Handle int or string schema ID. - $schemaObject = $this->schemaMapper->find($schema)->getSchemaObject($this->urlGenerator); - } - }//end if + $useDefaultSchema = ($schemaObject == new stdClass()); + if ($useDefaultSchema === true && $schema instanceof Schema) { + $schemaObject = $schema->getSchemaObject($this->urlGenerator); + } - $this->validateUniqueFields(object: $object, schema: $schema); + if ($schema instanceof Schema) { + $this->validateUniqueFields(object: $object, schema: $schema); + } // Validate extended field types (color, recurrence) against the original, // un-transformed schema so the declared `type`/`format` annotations are intact. $this->validateExtendedFieldTypes(object: $object, schemaObject: $schemaObject); - // Get the current schema slug for circular reference detection. - $currentSchemaSlug = ''; - if ($schema instanceof Schema) { - $currentSchemaSlug = $schema->getSlug(); + // Run the per-schema preparation pipeline (transform, clean, computed strip, + // null-type widening) — memoized per schemaId:version so bulk validation of + // N objects against one schema prepares it once instead of N times. A custom + // caller-supplied $schemaObject bypasses the cache (no stable cache key). + $cacheKey = null; + $prepared = null; + if ($useDefaultSchema === true && $schema instanceof Schema) { + $cacheKey = ((string) $schema->getId()).':'.((string) $schema->getVersion()); + $prepared = ($this->preparedSchemaCache[$cacheKey] ?? null); + } + + if ($prepared === null) { + $prepared = $this->prepareSchemaForValidation(schemaObject: $schemaObject, schema: $schema); + if ($cacheKey !== null) { + $this->preparedSchemaCache[$cacheKey] = $prepared; + } } - // Transform schema for validation (handles circular references, OpenRegister configs, and schema resolution). - [$schemaObject, $object] = $this->transformSchemaForValidation( - schemaObject: $schemaObject, - object: $object, - currentSchemaSlug: $currentSchemaSlug - ); - - // Clean the schema by removing all Nextcloud-specific metadata properties. - $schemaObject = $this->cleanSchemaForValidation(schemaObject: $schemaObject); - - // Log the final schema object before validation. - // If schemaObject reuired is empty unset it. - if (($schemaObject->required ?? null) !== null && empty($schemaObject->required) === true) { - unset($schemaObject->required); - } + $schemaObject = $prepared['schemaObject']; // If there are no properties, we don't need to validate. // Skip validation ONLY if properties are NOT set OR if properties are empty. if (isset($schemaObject->properties) === false || empty($schemaObject->properties) === true) { // Validate against an empty schema object to get a valid ValidationResult. - $validator = new Validator(); - return $validator->validate(json_decode(json_encode($object)), new stdClass()); + return $this->getValidator()->validate(json_decode(json_encode($object)), new stdClass()); } // @todo This should be done earlier. unset($object['extend'], $object['filters']); - // Remove computed properties from input data and required fields. + // Remove computed properties from input data. // Computed fields are system-generated and should not be validated against user input. - // @var object{properties?: object, required?: array} $schemaObject. - if (($schemaObject->properties ?? null) !== null) { - foreach ($schemaObject->properties as $propName => $propSchema) { - if (($propSchema->computed ?? null) !== null) { - unset($object[$propName]); - // Also remove from required array — computed fields can't be required from user input. - if (is_array($schemaObject->required) === true) { - $reqKey = array_search($propName, $schemaObject->required, true); - if ($reqKey !== false) { - unset($schemaObject->required[$reqKey]); - $schemaObject->required = array_values($schemaObject->required); - } - } - } - } + foreach ($prepared['computed'] as $computedProperty) { + unset($object[$computedProperty]); } // Remove only truly empty values that have no validation significance. // Keep empty strings for required fields so they can fail validation with proper error messages. - $requiredFields = $schemaObject->required ?? []; + $requiredFields = $prepared['required']; $object = array_filter( $object, function ($value, $key) use ($requiredFields, $schemaObject) { @@ -1436,6 +1487,77 @@ function ($value, $key) use ($requiredFields, $schemaObject) { ARRAY_FILTER_USE_BOTH ); + return $this->getValidator()->validate(json_decode(json_encode($object)), $schemaObject); + }//end validateObject() + + /** + * Run the complete per-schema preparation pipeline for validation. + * + * Applies, in order: circular-reference/OpenRegister-config transformation, + * Nextcloud metadata cleaning, empty-`required` removal, computed-property + * stripping from `required`, and null-type widening for non-required + * fields. The output depends only on the schema (never on the object being + * validated), which is what makes it cacheable per schemaId:version. + * + * @param object $schemaObject The raw schema object to prepare + * @param Schema|null $schema The schema entity (for slug-based circular reference detection) + * + * @return array{schemaObject: object, computed: list, required: array} The prepared + * schema object plus the derived computed-property and required-field lists + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Sequential schema mutations with per-property branching + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function prepareSchemaForValidation(object $schemaObject, ?Schema $schema): array + { + // Get the current schema slug for circular reference detection. + $currentSchemaSlug = ''; + if ($schema instanceof Schema) { + $currentSchemaSlug = $schema->getSlug(); + } + + // Transform schema for validation (handles circular references, OpenRegister configs, and schema resolution). + [$schemaObject] = $this->transformSchemaForValidation( + schemaObject: $schemaObject, + object: [], + currentSchemaSlug: $currentSchemaSlug + ); + + // Collect computed property names BEFORE cleaning — the cleaning step + // strips the per-property `computed` marker, which previously made + // computed-property removal unreachable (the old code inspected the + // already-cleaned schema and never found a `computed` key). + $computedProperties = []; + if (($schemaObject->properties ?? null) !== null) { + foreach ($schemaObject->properties as $propName => $propSchema) { + if (is_object($propSchema) === true && ($propSchema->computed ?? null) !== null) { + $computedProperties[] = $propName; + } + } + } + + // Clean the schema by removing all Nextcloud-specific metadata properties. + $schemaObject = $this->cleanSchemaForValidation(schemaObject: $schemaObject); + + // If schemaObject required is empty unset it. + if (($schemaObject->required ?? null) !== null && empty($schemaObject->required) === true) { + unset($schemaObject->required); + } + + // Strip computed properties from the required list — computed fields + // are system-generated and can't be required from user input. + foreach ($computedProperties as $propName) { + if (is_array($schemaObject->required ?? null) === true) { + $reqKey = array_search($propName, $schemaObject->required, true); + if ($reqKey !== false) { + unset($schemaObject->required[$reqKey]); + $schemaObject->required = array_values($schemaObject->required); + } + } + } + + $requiredFields = ($schemaObject->required ?? []); + /* * Modify schema to allow null values for non-required fields. * This ensures that null values are valid for optional fields. @@ -1476,6 +1598,30 @@ function ($value, $key) use ($requiredFields, $schemaObject) { }//end if }//end if + return [ + 'schemaObject' => $schemaObject, + 'computed' => $computedProperties, + 'required' => $requiredFields, + ]; + }//end prepareSchemaForValidation() + + /** + * Get the request-scoped memoized validator. + * + * Builds the Opis Validator once per service instance: max-error limit, + * custom format validators (bsn, semver, ISO 8601 date-time) and the + * http protocol resolver are registered a single time instead of on + * every validateObject() call. The shared loader also caches resolved + * `$ref` schema documents across calls. + * + * @return Validator The configured validator instance + */ + private function getValidator(): Validator + { + if ($this->validator !== null) { + return $this->validator; + } + $validator = new Validator(); $validator->setMaxErrors(100); @@ -1483,10 +1629,17 @@ function ($value, $key) use ($requiredFields, $schemaObject) { $this->registerCustomFormat(validator: $validator, type: 'string', format: 'bsn', resolver: new BsnFormat()); $this->registerCustomFormat(validator: $validator, type: 'string', format: 'semver', resolver: new SemVerFormat()); + // Accept ISO 8601 date-time input (optional seconds/timezone), overriding the + // opis built-in date-time format whose regex mandates seconds. Storage still + // normalises to a DATETIME column and reads emit RFC 3339. + $this->registerCustomFormat(validator: $validator, type: 'string', format: 'date-time', resolver: new Iso8601DateTimeFormat()); + $validator->loader()->resolver()->registerProtocol('http', [$this, 'resolveSchema']); - return $validator->validate(json_decode(json_encode($object)), $schemaObject); - }//end validateObject() + $this->validator = $validator; + + return $validator; + }//end getValidator() /** * Register a custom format validator with named parameters support @@ -1501,7 +1654,7 @@ function ($value, $key) use ($requiredFields, $schemaObject) { * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function registerCustomFormat(Validator $validator, string $type, string $format, object $resolver): void { @@ -1522,7 +1675,7 @@ private function registerCustomFormat(Validator $validator, string $type, string * * @SuppressWarnings(PHPMD.StaticAccess) Uri::fromParts is standard GuzzleHttp\Psr7 pattern * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function resolveSchema(Uri $uri): string { @@ -1573,7 +1726,7 @@ public function resolveSchema(Uri $uri): string * * @return string The reference string without query parameters * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function removeQueryParameters(string $reference): string { @@ -1595,7 +1748,7 @@ private function removeQueryParameters(string $reference): string * * @return string A meaningful error message. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function generateErrorMessage(ValidationResult $result): string { @@ -1619,7 +1772,7 @@ public function generateErrorMessage(ValidationResult $result): string * @SuppressWarnings(PHPMD.CyclomaticComplexity) Many validation error types require specific formatting * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive error formatting for all validation types * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function formatValidationError(\Opis\JsonSchema\Errors\ValidationError $error): string { @@ -1783,7 +1936,7 @@ function ($v) { * * @return string The type name. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function getValueType($value): string { @@ -1825,7 +1978,7 @@ private function getValueType($value): string * * @return JSONResponse JSON error response with validation errors and 400 status code. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ public function handleValidationException(ValidationException | CustomValidationException $exception): JSONResponse { @@ -1876,7 +2029,7 @@ public function handleValidationException(ValidationException | CustomValidation * @return void * @throws CustomValidationException * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md */ private function validateUniqueFields(array $object, Schema $schema): void { @@ -1906,7 +2059,6 @@ private function validateUniqueFields(array $object, Schema $schema): void $fieldNames = implode(', ', $uniqueFields); } - $fieldValues = $uniqueFields.'='.($object[$uniqueFields] ?? 'null'); if (is_array($uniqueFields) === true) { $fieldValues = implode( ', ', @@ -1917,11 +2069,14 @@ function ($field) use ($object) { $uniqueFields ) ); + $errorName = (string) (array_shift($uniqueFields) ?? 'uniqueField'); } - $errorName = (string) $uniqueFields; - if (is_array($uniqueFields) === true) { - $errorName = (string) (array_shift($uniqueFields) ?? 'uniqueField'); + if (is_array($uniqueFields) === false) { + // Scalar field name only — guarding the concat here avoids an + // "Array to string conversion" when $uniqueFields is an array. + $fieldValues = $uniqueFields.'='.($object[$uniqueFields] ?? 'null'); + $errorName = (string) $uniqueFields; } $errMsg = "The identifying fields ({$fieldNames}) are not unique. "; @@ -1956,8 +2111,8 @@ function ($field) use ($object) { * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Per-type dispatch over schema properties * - * @spec openspec/specs/extended-field-types/spec.md#REQ-EFT-003 - * @spec openspec/specs/extended-field-types/spec.md#REQ-EFT-005 + * @spec openspec/changes/add-openregister-discovered-capabilities/specs/extended-field-types/spec.md + * @spec openspec/changes/add-openregister-discovered-capabilities/specs/extended-field-types/spec.md */ private function validateExtendedFieldTypes(array $object, object $schemaObject): void { diff --git a/lib/Service/Object/ValidationHandler.php b/lib/Service/Object/ValidationHandler.php index 6406b49723..03a93f32a1 100644 --- a/lib/Service/Object/ValidationHandler.php +++ b/lib/Service/Object/ValidationHandler.php @@ -8,12 +8,12 @@ * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. * - * @category Service - * @package OCA\OpenRegister - * @author Conduction + * @category Service + * @package OCA\OpenRegister + * @author Conduction * @copyright 2026 Conduction B.V. - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html - * @link https://github.com/ConductionNL/openregister + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://github.com/ConductionNL/openregister */ namespace OCA\OpenRegister\Service\Object; @@ -43,7 +43,7 @@ * @category Service * @package OCA\OpenRegister * @author Conduction - * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * @link https://github.com/ConductionNL/openregister * @version 1.0.0 * @@ -64,7 +64,7 @@ class ValidationHandler * @param MagicMapper $magicMapper Mapper for magic tables. * @param LoggerInterface $logger Logger for logging operations. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly ValidateObject $validateHandler, @@ -86,7 +86,7 @@ public function __construct( * @psalm-param ValidationException|CustomValidationException $exception * @phpstan-param ValidationException|CustomValidationException $exception * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function handleValidationException(ValidationException|CustomValidationException $exception): mixed { @@ -108,7 +108,7 @@ public function handleValidationException(ValidationException|CustomValidationEx * * @throws InvalidArgumentException If required fields are missing. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function validateRequiredFields(array $objects): void { @@ -163,7 +163,7 @@ public function validateRequiredFields(array $objects): void * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple exception types require separate handling * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function validateObjectsBySchema(int $schemaId, callable $saveCallback): array { @@ -281,7 +281,7 @@ public function validateObjectsBySchema(int $schemaId, callable $saveCallback): * * @throws \Exception If schema/register loading fails or object retrieval fails * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function validateAndSaveObjectsBySchema( int $registerId, @@ -414,7 +414,7 @@ public function validateAndSaveObjectsBySchema( * @psalm-return array{schema: \OCA\OpenRegister\Db\Schema, register: \OCA\OpenRegister\Db\Register}|null * @phpstan-return array{schema: \OCA\OpenRegister\Db\Schema, register: \OCA\OpenRegister\Db\Register}|null * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ private function loadSchemaAndRegister(int $registerId, int $schemaId): ?array { @@ -451,7 +451,7 @@ private function loadSchemaAndRegister(int $registerId, int $schemaId): ?array * * @return array|null Array of objects, or null on failure. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ private function loadObjectsForValidation(mixed $register, mixed $schema, int $schemaId): ?array { @@ -483,7 +483,7 @@ private function loadObjectsForValidation(mixed $register, mixed $schema, int $s * * @return array The sliced array of objects. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ private function applyLimitOffset(array $allObjects, int $schemaId, int $totalObjects, ?int $limit, int $offset): array { @@ -513,7 +513,7 @@ private function applyLimitOffset(array $allObjects, int $schemaId, int $totalOb * * @return int The chunk size to use. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ private function calculateChunkSize(int $objectsToProcess): int { @@ -546,7 +546,7 @@ private function calculateChunkSize(int $objectsToProcess): int * * @return array{processed: int, updated: int, failed: int} Aggregated totals. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ private function processAllChunks( array $allObjects, @@ -611,7 +611,7 @@ private function processAllChunks( * * @return array{processed: int, updated: int, failed: int} Chunk processing results. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ private function processValidationChunk( array $objectsChunk, @@ -730,7 +730,7 @@ private function processValidationChunk( * * @return array Array of object data arrays. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ private function convertChunkToArrays(array $objectsChunk): array { @@ -747,112 +747,6 @@ private function convertChunkToArrays(array $objectsChunk): array return $objectsData; }//end convertChunkToArrays() - /** - * Validate all objects belonging to a specific schema (comprehensive version). - * - * This method validates all objects that belong to the specified schema against their schema definition. - * It returns detailed validation results including valid and invalid objects with error details. - * - * @param int $schemaId The ID of the schema whose objects should be validated. - * @param callable $saveCallback Callback to validate objects (object, register, schema, uuid, rbac, multi, silent). - * - * @return array Comprehensive validation results. - * - * @throws \Exception If the validation operation fails. - * - * @phpstan-return array{valid_count: int, invalid_count: int, - * valid_objects: array, invalid_objects: array, - * schema_id: int} - * @psalm-return array{valid_count: int<0, max>, - * invalid_count: int<0, max>, - * valid_objects: list, - * invalid_objects: list, - * id: int, name: null|string, uuid: null|string}>, schema_id: int} - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Comprehensive validation with detailed error extraction - * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 - */ - public function validateSchemaObjects(int $schemaId, callable $saveCallback): array - { - // Use the mapper's findBySchema method to get all objects for this schema. - // This bypasses RBAC and multi-tenancy automatically. - $objects = $this->objectEntityMapper->findBySchema($schemaId); - - $validObjects = []; - $invalidObjects = []; - - foreach ($objects as $object) { - $objectData = []; - try { - // Get the object data for validation. - $objectData = $object->getObject(); - - // Use saveCallback with silent=true to validate without actually saving. - // This will trigger validation and return any errors. - $saveCallback( - $objectData, - $object->getRegister(), - $schemaId, - $object->getUuid(), - false, - false, - true - ); - - // If saveCallback succeeded, the object is valid. - $validObjects[] = [ - 'id' => $object->getId(), - 'uuid' => $object->getUuid(), - 'name' => $object->getName(), - 'data' => $objectData, - ]; - } catch (\Exception $e) { - // Extract validation errors from the exception. - $errors = []; - - // Check if it's a validation exception with detailed errors. - if ($e instanceof \OCA\OpenRegister\Exception\ValidationException) { - foreach ($e->getErrors() ?? [] as $error) { - $errors[] = [ - 'path' => $error['path'] ?? 'unknown', - 'message' => $error['message'] ?? $error, - 'keyword' => $error['keyword'] ?? 'validation', - ]; - } - } - - if ($e instanceof \OCA\OpenRegister\Exception\ValidationException === false) { - // Generic error. - $errors[] = [ - 'path' => 'general', - 'message' => 'Validation failed: '.$e->getMessage(), - 'keyword' => 'exception', - ]; - } - - $invalidObjects[] = [ - 'id' => $object->getId(), - 'uuid' => $object->getUuid(), - 'name' => $object->getName(), - 'data' => $objectData, - 'errors' => $errors, - ]; - }//end try - }//end foreach - - return [ - 'valid_count' => count($validObjects), - 'invalid_count' => count($invalidObjects), - 'valid_objects' => $validObjects, - 'invalid_objects' => $invalidObjects, - 'schema_id' => $schemaId, - ]; - }//end validateSchemaObjects() - /** * Apply inversedBy filter to query filters. * @@ -863,7 +757,7 @@ public function validateSchemaObjects(int $schemaId, callable $saveCallback): ar * * @return array|null Matching object IDs or null. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function applyInversedByFilter(array &$_filters): array|null { diff --git a/lib/Service/Object/VectorizationHandler.php b/lib/Service/Object/VectorizationHandler.php index 00d614d113..c4b9ffdf25 100644 --- a/lib/Service/Object/VectorizationHandler.php +++ b/lib/Service/Object/VectorizationHandler.php @@ -55,7 +55,7 @@ class VectorizationHandler * @param MagicMapper $objectEntityMapper Object entity mapper * @param LoggerInterface $logger PSR-3 logger * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly VectorizationService $vectorizationService, @@ -76,7 +76,7 @@ public function __construct( * * @throws \Exception If vectorization fails. * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function vectorizeBatch(?array $views=null, int $batchSize=25): array { @@ -141,7 +141,7 @@ public function vectorizeBatch(?array $views=null, int $batchSize=25): array * * @psalm-return array{total_objects: int<0, max>, views: array|null} * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getStatistics(?array $views=null): array { @@ -198,7 +198,7 @@ public function getStatistics(?array $views=null): array * * @throws \Exception If count fails * - * @spec openspec/changes/retrofit-2026-04-28-object-lifecycle/tasks.md#task-2 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getCount(?array $schemas=null): int { diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 238cb2eeff..637f7117e0 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -22,9 +22,9 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-26 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-27 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ declare(strict_types=1); @@ -56,7 +56,7 @@ use OCA\OpenRegister\Service\Object\DataManipulationHandler; use OCA\OpenRegister\Service\Object\DeleteObject; use OCA\OpenRegister\Service\Object\GetObject; -use OCA\OpenRegister\Service\Object\PerformanceHandler; +use OCA\OpenRegister\Service\ObjectSource\ObjectSourceRegistry; use OCA\OpenRegister\Service\Object\PermissionHandler; use OCA\OpenRegister\Service\Object\RenderObject; use OCA\OpenRegister\Service\Object\SaveObject; @@ -85,22 +85,17 @@ use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Exception\CustomValidationException; use OCP\AppFramework\Db\DoesNotExistException as OcpDoesNotExistException; -use React\Promise\Promise; -use React\Promise\PromiseInterface; -use React\Async; +use OCP\IUser; use OCP\IUserSession; use OCP\IGroupManager; use OCP\IUserManager; use OCA\OpenRegister\Service\OrganisationService; use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; use OCP\AppFramework\IAppContainer; use OCP\DB\QueryBuilder\IQueryBuilder; use Psr\Log\LoggerInterface; use Symfony\Component\Uid\Uuid; -use function React\Promise\all; - /** * Primary Object Management Service for OpenRegister * @@ -193,52 +188,69 @@ class ObjectService */ private ?ObjectEntity $currentObject = null; + /** + * Request-scoped cache of resolved uuid → (register, schema) contexts. + * + * When a uuid is read under a stale URL scope, the scoped lookup misses and + * find() falls back to an expensive cross-table search (openregister#1520 — + * that fallback is deliberate and must keep working). Within one request the + * same uuid is often resolved repeatedly (relation resolution, inverse + * rendering), and each resolution used to re-miss the scoped path and re-run + * the cross-table scan. This cache remembers the object's true register and + * schema after the first successful resolution so subsequent lookups go + * straight to the right magic table. It is a plain array property, so it + * lives exactly as long as this service instance (one request). + * + * @var array + */ + private array $uuidScopeCache = []; + // **REMOVED**: Distributed caching mechanisms removed since SOLR is now our index. // **REMOVED**: Cache TTL constants removed since SOLR is now our index. /** * Constructor for ObjectService. * - * @param DataManipulationHandler $dataManipHandler Handler for data manipulation operations. - * @param DeleteObject $deleteHandler Handler for object deletion. - * @param GetObject $getHandler Handler for object retrieval. - * @param PerformanceHandler $performanceHandler Handler for performance operations. - * @param PermissionHandler $permissionHandler Handler for permission checks. - * @param RenderObject $renderHandler Handler for object rendering. - * @param SaveObject $saveHandler Handler for individual object saving. - * @param SaveObjects $saveObjectsHandler Handler for bulk object saving operations. - * @param SearchQueryHandler $searchQueryHandler Handler for search query operations. - * @param ValidateObject $validateHandler Handler for object validation. - * @param LockHandler $lockHandler Handler for object locking. - * @param AuditHandler $auditHandler Handler for audit trail operations. - * @param RelationHandler $relationHandler Handler for object relationships. - * @param MergeHandler $mergeHandler Handler for merge and migration. - * @param FacetHandler $facetHandler Handler for facet operations. - * @param MetadataHandler $metadataHandler Handler for metadata operations. - * @param PerformanceOptimizationHandler $perfOptHandler Handler for performance optimization. - * @param QueryHandler $queryHandler Handler for query operations. - * @param RevertHandler $revertHandler Handler for revert operations. - * @param UtilityHandler $utilityHandler Handler for utility operations. - * @param ValidationHandler $validationHandler Handler for validation operations. - * @param CascadingHandler $cascadingHandler Handler for cascading operations. - * @param MigrationHandler $migrationHandler Handler for migration operations. - * @param RegisterMapper $registerMapper Mapper for register operations. - * @param SchemaMapper $schemaMapper Mapper for schema operations. - * @param ViewMapper $viewMapper Mapper for view operations. - * @param MagicMapper $objectMapper Unified mapper for object - * operations (routes to - * magic tables). - * @param FileService $fileService Service for file operations. - * @param IUserSession $userSession User session for getting current user. - * @param SearchTrailService $searchTrailService Service for search trail operations. - * @param IGroupManager $groupManager Group manager for checking user groups. - * @param IUserManager $userManager User manager for getting user objects. - * @param OrganisationService $organisationService Service for organisation operations. - * @param LoggerInterface $logger Logger for performance monitoring. - * @param CacheHandler $cacheHandler Service for entity and query caching. - * @param SettingsService $settingsService Service for settings operations. - * @param DateTimeNormalizer $dateTimeNormalizer Normaliser for user-supplied datetime input. - * @param IAppContainer $container Application container. + * @param DataManipulationHandler $dataManipHandler Handler for data manipulation operations. + * @param DeleteObject $deleteHandler Handler for object deletion. + * @param GetObject $getHandler Handler for object retrieval. + * @param PermissionHandler $permissionHandler Handler for permission checks. + * @param RenderObject $renderHandler Handler for object rendering. + * @param SaveObject $saveHandler Handler for individual object saving. + * @param SaveObjects $saveObjectsHandler Handler for bulk object saving operations. + * @param SearchQueryHandler $searchQueryHandler Handler for search query operations. + * @param ValidateObject $validateHandler Handler for object validation. + * @param LockHandler $lockHandler Handler for object locking. + * @param AuditHandler $auditHandler Handler for audit trail operations. + * @param RelationHandler $relationHandler Handler for object relationships. + * @param MergeHandler $mergeHandler Handler for merge and migration. + * @param FacetHandler $facetHandler Handler for facet operations. + * @param MetadataHandler $metadataHandler Handler for metadata operations. + * @param PerformanceOptimizationHandler $perfOptHandler Handler for performance optimization. + * @param QueryHandler $queryHandler Handler for query operations. + * @param RevertHandler $revertHandler Handler for revert operations. + * @param UtilityHandler $utilityHandler Handler for utility operations. + * @param ValidationHandler $validationHandler Handler for validation operations. + * @param CascadingHandler $cascadingHandler Handler for cascading operations. + * @param MigrationHandler $migrationHandler Handler for migration operations. + * @param RegisterMapper $registerMapper Mapper for register operations. + * @param SchemaMapper $schemaMapper Mapper for schema operations. + * @param ViewMapper $viewMapper Mapper for view operations. + * @param MagicMapper $objectMapper Unified mapper for object + * operations (routes to + * magic tables). + * @param FileService $fileService Service for file operations. + * @param IUserSession $userSession User session for getting current user. + * @param SearchTrailService $searchTrailService Service for search trail operations. + * @param IGroupManager $groupManager Group manager for checking user groups. + * @param IUserManager $userManager User manager for getting user objects. + * @param OrganisationService $organisationService Service for organisation operations. + * @param LoggerInterface $logger Logger for performance monitoring. + * @param CacheHandler $cacheHandler Service for entity and query caching. + * @param SettingsService $settingsService Service for settings operations. + * @param DateTimeNormalizer $dateTimeNormalizer Normaliser for user-supplied datetime input. + * @param IAppContainer $container Application container. + * @param ObjectSourceRegistry $objectSourceRegistry Registry of object-source providers (virtual schemas). * * @SuppressWarnings(PHPMD.ExcessiveParameterList) Nextcloud DI requires constructor injection */ @@ -247,7 +259,6 @@ public function __construct( private readonly DataManipulationHandler $dataManipHandler, private readonly DeleteObject $deleteHandler, private readonly GetObject $getHandler, - private readonly PerformanceHandler $performanceHandler, private readonly PermissionHandler $permissionHandler, private readonly RenderObject $renderHandler, private readonly SaveObject $saveHandler, @@ -291,7 +302,8 @@ public function __construct( private readonly CacheHandler $cacheHandler, private readonly SettingsService $settingsService, private readonly DateTimeNormalizer $dateTimeNormalizer, - private readonly IAppContainer $container + private readonly IAppContainer $container, + private readonly ObjectSourceRegistry $objectSourceRegistry // TODO: CIRCULAR DEPENDENCY ISSUE - ExportService, ImportService, and VectorizationService // These services have deep circular dependencies: // - ExportService → uses SaveObjects → potentially loops back @@ -345,50 +357,33 @@ private function checkPermission( }//end checkPermission() /** - * Ensure folder exists for an ObjectEntity. + * Run a callable as a trusted system operation. * - * This method checks if the object has a valid folder ID and creates one if needed. - * It handles legacy cases where the folder property might be null, empty, or a string path. + * For app-initiated maintenance that legitimately runs without a user + * session — repair steps triggered from web requests, event listeners + * reacting to webcron-created objects, boot-time migrations. Inside the + * callable, RBAC treats the caller as a trusted system principal + * (mirroring the existing CLI-cron trust) instead of denying every write + * as anonymous. The elevation is scoped strictly to the callable and is + * released even when it throws. * - * @param ObjectEntity $entity The object entity to ensure folder for + * Only pass operations whose inputs originate from code or the app's own + * shipped data — never wrap handling of user-supplied request data. * - * @return void + * @param callable $operation The trusted operation to execute. + * + * @return mixed Whatever the callable returns. * - * @psalm-return void - * @phpstan-return void + * @SuppressWarnings(PHPMD.StaticAccess) SystemOperationContext is a static execution-context holder by design * - * @spec exclude Lazily creates the object's storage folder via FileService when missing; file-folder plumbing. + * @spec openspec/specs/rbac-scopes/spec.md + * + * @SuppressWarnings(PHPMD.StaticAccess) SystemOperationContext::run is the canonical scoped-elevation API */ - public function ensureObjectFolderExists(ObjectEntity $entity): void + public function runAsSystem(callable $operation) { - $folderProperty = $entity->getFolder(); - - // Check if folder needs to be created (null, empty string, or legacy string path). - $isString = is_string($folderProperty) === true; - if ($folderProperty === null || $folderProperty === '' || $isString === true) { - try { - // Create folder and get the folder node. - $folderNode = $this->fileService->createEntityFolder($entity); - - if ($folderNode !== null) { - // Update the entity with the folder ID. - $folderIdValue = $folderNode->getId(); - $folderStr = null; - if ($folderIdValue !== null) { - $folderStr = (string) $folderIdValue; - } - - $entity->setFolder($folderStr); - - // Save the entity with the new folder ID. - $this->objectMapper->update($entity); - } - } catch (Exception $e) { - // Log the error but don't fail the object creation/update. - // The object can still function without a folder. - }//end try - }//end if - }//end ensureObjectFolderExists() + return SystemOperationContext::run($operation); + }//end runAsSystem() /** * Set the current register context. @@ -397,55 +392,22 @@ public function ensureObjectFolderExists(ObjectEntity $entity): void * * @return static Returns self for method chaining * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-1 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function setRegister(Register | string | int $register): static { if (is_string($register) === true || is_int($register) === true) { - // **PERFORMANCE OPTIMIZATION**: Use cached entity lookup for numeric IDs only. - // When deriving register from object context, bypass RBAC and multi-tenancy checks. - // If user has access to the object, they should be able to access its register. - if (is_numeric($register) === true) { - $registers = $this->performanceHandler->getCachedEntities( - [$register], - function (array $ids): array { - return [$this->registerMapper->find( - id: $ids[0], - published: null, - _rbac: false, - _multitenancy: false - ) - ]; - } - ); - $registerExists = isset($registers[0]) === true; - $isRegisterInstance = $registerExists - && $registers[0] instanceof Register; - if ($isRegisterInstance === true) { - $register = $registers[0]; - } - - if ($isRegisterInstance !== true) { - // Fallback to direct database lookup if cache fails. - $register = $this->registerMapper->find( - id: $register, - published: null, - _rbac: false, - _multitenancy: false - ); - } - }//end if - - if (is_numeric($register) !== true && ($register instanceof Register) === false) { - // It's a slug string - find() already supports slugs via orX(id, uuid, slug). - $register = $this->registerMapper->find( - id: $register, - published: null, - _rbac: false, - _multitenancy: false - ); - } - }//end if + // Resolve the identifier through the mapper. RegisterMapper::find() + // already provides request-scoped caching (findCache) and supports + // numeric IDs, UUIDs, and slugs via orX(id, uuid, slug). RBAC and + // multi-tenancy checks are bypassed: if the user has access to the + // object, they should be able to access its register. + $register = $this->registerMapper->find( + id: $register, + _rbac: false, + _multitenancy: false + ); + } $this->currentRegister = $register; return $this; @@ -458,53 +420,20 @@ function (array $ids): array { * * @return static Returns self for method chaining * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-1 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function setSchema(Schema | string | int $schema): static { if (is_string($schema) === true || is_int($schema) === true) { - // Try to find schema by ID first (if numeric) or by slug. + // Resolve the identifier through the mapper. SchemaMapper::find() + // already provides request-scoped caching (findCache) and supports + // numeric IDs, UUIDs, and slugs via orX(id, uuid, slug). try { - if (is_numeric($schema) === true) { - // **PERFORMANCE OPTIMIZATION**: Use cached entity lookup for numeric IDs. - $schemas = $this->performanceHandler->getCachedEntities( - [$schema], - function (array $ids): array { - return [$this->schemaMapper->find( - id: $ids[0], - published: null, - _rbac: false, - _multitenancy: false - ) - ]; - } - ); - $schemaExists = isset($schemas[0]) === true; - $isSchemaInstance = $schemaExists && $schemas[0] instanceof Schema; - if ($isSchemaInstance === true) { - $schema = $schemas[0]; - } - - if ($isSchemaInstance !== true) { - // Fallback to direct database lookup if cache fails. - $schema = $this->schemaMapper->find( - id: $schema, - published: null, - _rbac: false, - _multitenancy: false - ); - } - }//end if - - if (is_numeric($schema) !== true && ($schema instanceof Schema) === false) { - // It's a slug string - find() supports slugs via orX(id, uuid, slug). - $schema = $this->schemaMapper->find( - id: $schema, - published: null, - _rbac: false, - _multitenancy: false - ); - } + $schema = $this->schemaMapper->find( + id: $schema, + _rbac: false, + _multitenancy: false + ); } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { // Debug logging to understand WHY schema lookup fails. $this->logger->error( @@ -528,6 +457,38 @@ function (array $ids): array { return $this; }//end setSchema() + /** + * Get the register entity resolved by the last setRegister() call. + * + * Exposes the already-resolved entity so callers (e.g. controllers that + * resolved slugs via setRegister()) can reuse it instead of re-fetching + * the same register from the database. + * + * @return Register|null The current register entity, or null when no register context is set. + * + * @spec exclude Context getter exposing the entity resolved by setRegister(); no business rule. + */ + public function getCurrentRegisterEntity(): ?Register + { + return $this->currentRegister; + }//end getCurrentRegisterEntity() + + /** + * Get the schema entity resolved by the last setSchema() call. + * + * Exposes the already-resolved entity so callers (e.g. controllers that + * resolved slugs via setSchema()) can reuse it instead of re-fetching + * the same schema from the database. + * + * @return Schema|null The current schema entity, or null when no schema context is set. + * + * @spec exclude Context getter exposing the entity resolved by setSchema(); no business rule. + */ + public function getCurrentSchemaEntity(): ?Schema + { + return $this->currentSchema; + }//end getCurrentSchemaEntity() + /** * Set the current object context. * @@ -535,7 +496,7 @@ function (array $ids): array { * * @return static Returns self for method chaining * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-1 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function setObject(ObjectEntity | string | int $object): static { @@ -585,8 +546,12 @@ public function getObject(): ?ObjectEntity * @param Schema|string|int|null $schema The schema object or its ID/UUID. * @param bool $_rbac Whether to apply RBAC checks (default: true). * @param bool $_multitenancy Whether to apply multitenancy filtering (default: true). + * @param bool $_render Whether to render the entity before returning (default: true). + * Pass false when the caller performs its own single render + * (e.g. ObjectsController::show()) so the object is not + * rendered twice; permission checks and read logging still run. * - * @return ObjectEntity|null The rendered object or null. + * @return ObjectEntity|null The rendered object (or the raw entity when $_render is false) or null. * * @throws Exception If the object is not found. * @@ -604,96 +569,233 @@ public function find( Register | string | int | null $register=null, Schema | string | int | null $schema=null, bool $_rbac=true, - bool $_multitenancy=true + bool $_multitenancy=true, + bool $_render=true ): ?ObjectEntity { // Resolve the call's register / schema, isolating it from any // stale `currentRegister` / `currentSchema` state left over from // a previous call on this service instance. When the caller // omits either argument we want MagicMapper's cross-table search // to resolve the object — NOT a previous caller's leftover - // schema (openbuilt#75 / openregister#1520: TransitionController + // schema (openbuild#75 / openregister#1520: TransitionController // was 500-ing because `objectService->find(id: $uuid)` from // `TransitionEngine::transition()` inherited a stale schema and // hit the wrong magic table). - $callRegister = null; - $callSchema = null; - if ($register !== null) { - $this->setRegister(register: $register); - $callRegister = $this->currentRegister; - } + // BUG-OBJ-13 (openregister#1520): find() is a read operation and + // MUST NOT leave the shared `currentRegister` / `currentSchema` + // instance state mutated for the next caller. We snapshot the + // previous context here and restore it in a `finally` below, so any + // re-anchoring done for this call's rendering / RBAC is local to + // this invocation only. + $previousRegister = $this->currentRegister; + $previousSchema = $this->currentSchema; - if ($schema !== null) { - $this->setSchema(schema: $schema); - $callSchema = $this->currentSchema; - } + try { + $callRegister = null; + $callSchema = null; + if ($register !== null) { + $this->setRegister(register: $register); + $callRegister = $this->currentRegister; + } - // Retrieve the object — when both call args are null, MagicMapper - // falls back to its `findAcrossAllMagicTables` path. - $object = $this->getHandler->find( - id: $id, - register: $callRegister, - schema: $callSchema, - _extend: $_extend, - files: $files, - _rbac: $_rbac, - _multitenancy: $_multitenancy - ); + if ($schema !== null) { + $this->setSchema(schema: $schema); + $callSchema = $this->currentSchema; + } - // If the object is not found, return null (@psalm-suppress TypeDoesNotContainNull). - if ($object === null) { - return null; - } + // Request-scoped uuid → (register, schema) cache: when this uuid was + // already resolved earlier in this request, target its true context + // directly. This avoids re-missing the scoped lookup (and re-running + // the cross-table fallback below) for every repeated relation + // resolution of the same uuid under a stale URL scope. + if (is_string($id) === true && isset($this->uuidScopeCache[$id]) === true) { + $cachedScope = $this->uuidScopeCache[$id]; + if ($cachedScope['register'] !== null) { + $this->setRegister(register: $cachedScope['register']); + $callRegister = $this->currentRegister; + } - // When the caller did NOT specify register/schema we just did a - // cross-table find. Re-anchor `currentSchema` / `currentRegister` - // to the freshly-resolved object so the downstream rendering / - // RBAC code points at the right context — never at the stale - // leftover from a previous call. - if ($callSchema === null) { - $this->setSchema(schema: $object->getSchema()); - } + $this->setSchema(schema: $cachedScope['schema']); + $callSchema = $this->currentSchema; + } - if ($callRegister === null) { - $registerRef = $object->getRegister(); - if ($registerRef !== null && $registerRef !== '') { - $this->setRegister(register: $registerRef); + // Retrieve the object — when both call args are null, MagicMapper + // falls back to its `findAcrossAllMagicTables` path. + try { + $object = $this->getHandler->find( + id: $id, + register: $callRegister, + schema: $callSchema, + _extend: $_extend, + files: $files, + _rbac: $_rbac, + _multitenancy: $_multitenancy + ); + } catch (OcpDoesNotExistException $e) { + // Cross-schema fallback for relation name-resolution. + // + // Relations reference their target by a globally-unique UUID, but + // the URL/context schema can be stale or point at a sibling schema + // (e.g. objects/larpingapp/25/ where the object actually + // lives in schema 1470). A schema-scoped lookup only inspects one + // magic table, so it 404s even though the object exists in the + // register. When the identifier is a UUID (collision-free, unlike a + // slug or numeric id) we retry across all magic tables and, on a + // hit, RE-ANCHOR the call's register/schema to the resolved object + // so RBAC + rendering use the object's true context, not the stale + // one supplied by the caller. + // A cached scope that no longer resolves (object deleted or + // moved mid-request) must not pin future lookups to it. + if (is_string($id) === true) { + unset($this->uuidScopeCache[$id]); + } + + $canFallBack = (($callSchema !== null || $callRegister !== null) + && is_string($id) === true + && $this->isUuidFormat(value: $id) === true); + if ($canFallBack === false) { + throw $e; + } + + $object = $this->getHandler->find( + id: $id, + register: null, + schema: null, + _extend: $_extend, + files: $files, + _rbac: $_rbac, + _multitenancy: $_multitenancy + ); + + // Force re-anchoring below to the resolved object's real context. + $callSchema = null; + $callRegister = null; + }//end try + + // If the object is not found, return null (@psalm-suppress TypeDoesNotContainNull). + if ($object === null) { + return null; } - } - // Check user has permission to read this specific object (includes object owner check). - // Publication visibility is now handled by RBAC conditional rules with $now variable. - $this->checkPermission( - schema: $this->currentSchema, - action: 'read', - userId: null, - objectOwner: $object->getOwner(), - _rbac: $_rbac, - object: $object - ); + // When the caller did NOT specify register/schema we just did a + // cross-table find. Re-anchor `currentSchema` / `currentRegister` + // to the freshly-resolved object so the downstream rendering / + // RBAC code points at the right context — never at the stale + // leftover from a previous call. This mutation is undone by the + // `finally` block before returning to the caller. + if ($callSchema === null) { + $this->setSchema(schema: $object->getSchema()); + } - // Render the object before returning. - $registers = null; - if ($this->currentRegister !== null) { - $registers = [$this->currentRegister->getId() => $this->currentRegister]; - } + if ($callRegister === null) { + $registerRef = $object->getRegister(); + if ($registerRef !== null && $registerRef !== '') { + $this->setRegister(register: $registerRef); + } + } - // Always use the current schema (either provided or derived from object). - if ($this->currentSchema === null) { - throw new RuntimeException('Schema must be set before rendering entity.'); - } + // Remember this uuid's resolved (register, schema) for the rest of + // the request so repeated lookups skip the scoped-miss + cross-table + // fallback dance entirely. Only uuids are cached: they are globally + // unique, unlike slugs or numeric per-table ids. + if (is_string($id) === true + && $this->isUuidFormat(value: $id) === true + && $this->currentSchema !== null + ) { + $this->uuidScopeCache[$id] = [ + 'register' => $this->currentRegister, + 'schema' => $this->currentSchema, + ]; + } - $schemas = [$this->currentSchema->getId() => $this->currentSchema]; + // Check user has permission to read this specific object (includes object owner check). + // Publication visibility is now handled by RBAC conditional rules with $now variable. + $this->checkPermission( + schema: $this->currentSchema, + action: 'read', + userId: null, + objectOwner: $object->getOwner(), + _rbac: $_rbac, + object: $object + ); - return $this->renderHandler->renderEntity( - entity: $object, - _extend: $_extend, - registers: $registers, - schemas: $schemas, - _rbac: $_rbac, - _multitenancy: $_multitenancy - ); + // Skip rendering when the caller is the render site itself. The read + // path stays intact above — retrieval, cross-schema fallback, + // permission check — but the (expensive) render pass with its + // writeOnly redaction runs exactly once, in the caller, instead of + // twice. Used by ObjectsController::show(). + if ($_render === false) { + // AVG / GDPR per-access read logging still applies to the read. + $this->logProcessingRead(object: $object); + return $object; + } + + // Render the object before returning. + $registers = null; + if ($this->currentRegister !== null) { + $registers = [$this->currentRegister->getId() => $this->currentRegister]; + } + + // Always use the current schema (either provided or derived from object). + if ($this->currentSchema === null) { + throw new RuntimeException('Schema must be set before rendering entity.'); + } + + $schemas = [$this->currentSchema->getId() => $this->currentSchema]; + + // AVG / GDPR per-access read logging (verwerkingenlogging). + // Fail-soft and gated on the schema's `x-openregister-processing` + // opt-in inside ProcessingLogService — never blocks the read. + $this->logProcessingRead(object: $object); + + return $this->renderHandler->renderEntity( + entity: $object, + _extend: $_extend, + registers: $registers, + schemas: $schemas, + _rbac: $_rbac, + _multitenancy: $_multitenancy + ); + } finally { + // BUG-OBJ-13: restore the caller's context so find() has no + // observable side-effect on shared instance state. + $this->currentRegister = $previousRegister; + $this->currentSchema = $previousSchema; + }//end try }//end find() + /** + * Record an AVG processing-log entry for a single object read. + * + * Lazily resolves ProcessingLogService from the container (mirrors + * the audit-trail attribution resolver) so this stays an additive, + * fail-soft hook with no new constructor dependency and no circular + * risk. The service itself gates on the schema opt-in and swallows + * its own errors; the wrapping try/catch is belt-and-braces so a + * misconfigured container can never break a read. + * + * @param ObjectEntity $object The object that was read. + * + * @return void + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + private function logProcessingRead(ObjectEntity $object): void + { + try { + $service = $this->container->get(\OCA\OpenRegister\Service\ProcessingLogService::class); + $service->logRead(object: $object, action: 'read'); + $service->flush(); + } catch (\Throwable $e) { + // Fail-soft: read logging never breaks or slows the read path. + $this->logger->debug( + message: '[AVG] processing-log read hook skipped', + context: ['exception' => $e->getMessage()] + ); + } + + }//end logProcessingRead() + /** * Gets an object by its ID without creating an audit trail. * @@ -802,18 +904,19 @@ public function findAll(array $config=[], bool $_rbac=true, bool $_multitenancy= _multitenancy: $_multitenancy ); - // Resolve register and schema entities for rendering. - [$registers, $schemas] = $this->resolveRegisterAndSchema( - config: $config, - objects: $objects - ); - - // Render all objects asynchronously. - return $this->renderObjectsAsync( - objects: $objects, - config: $config, - registers: $registers, - schemas: $schemas, + // Render via renderEntities, which batch-preloads ALL related objects in + // one query before the per-row render (optimize-object-render-hot-path). + // A previous code path pre-resolved registers/schemas and looped + // renderEntity() per row, causing an N+1 on any `?_extend=` list. + // renderEntities() performs the identical per-row renderEntity() rendering, + // only with the relation/file caches pre-warmed, so output is unchanged; + // registers/schemas were a pre-resolution optimization renderEntity() + // reproduces internally, not a correctness input. + return $this->renderHandler->renderEntities( + entities: $objects, + _extend: ($config['extend'] ?? []), + _filter: ($config['unset'] ?? null), + _fields: ($config['fields'] ?? null), _rbac: $_rbac, _multitenancy: $_multitenancy ); @@ -852,123 +955,6 @@ private function prepareFindAllConfig(array $config): array return $config; }//end prepareFindAllConfig() - /** - * Resolve register and schema entities for rendering. - * - * @param array $config Configuration array - * @param array $objects Retrieved objects - * - * @return ((Register|Schema|mixed)[]|null)[] [registers, schemas] - * - * @psalm-return list{array|null, array|null} - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple conditions for register/schema resolution - */ - private function resolveRegisterAndSchema(array $config, array $objects): array - { - // Determine if register and schema should be passed to renderEntity. - $registers = null; - if ($this->currentRegister !== null && ($config['filters']['register'] ?? null) !== null) { - $registers = [$this->currentRegister->getId() => $this->currentRegister]; - } - - $schemas = null; - if ($this->currentSchema !== null && isset($config['filters']['schema']) === true) { - $schemas = [$this->currentSchema->getId() => $this->currentSchema]; - } - - // Check if '@self.schema' or '@self.register' is in extend but not in filters. - // This handles cases where we need to load schemas/registers for rendering. - $hasExtend = isset($config['extend']) === true; - $extendArray = (array) ($config['extend'] ?? []); - $needsSchema = $hasExtend - && in_array('@self.schema', $extendArray, true) === true - && $schemas === null; - $needsRegister = $hasExtend - && in_array('@self.register', $extendArray, true) === true - && $registers === null; - - if ($needsSchema === true) { - $schemaIds = array_unique( - array_filter(array_map(fn($object) => $object->getSchema() ?? null, $objects)) - ); - $schemas = $this->performanceHandler->getCachedEntities( - ids: $schemaIds, - fallbackFunc: [$this->schemaMapper, 'findMultiple'] - ); - $schemas = array_combine( - array_map(fn(Schema $schema): int => $schema->getId(), $schemas), - $schemas - ); - } - - if ($needsRegister === true) { - $registerIds = array_unique( - array_filter(array_map(fn($object) => $object->getRegister() ?? null, $objects)) - ); - $registers = $this->performanceHandler->getCachedEntities( - ids: $registerIds, - fallbackFunc: [$this->registerMapper, 'findMultiple'] - ); - $registers = array_combine( - array_map(fn(Register $register): int => $register->getId(), $registers), - $registers - ); - } - - return [$registers, $schemas]; - }//end resolveRegisterAndSchema() - - /** - * Render objects asynchronously using promises. - * - * @param array $objects Objects to render - * @param array $config Configuration array - * @param array|null $registers Register entities - * @param array|null $schemas Schema entities - * @param bool $_rbac Apply RBAC - * @param bool $_multitenancy Apply multitenancy - * - * @return array Rendered objects - */ - private function renderObjectsAsync( - array $objects, - array $config, - ?array $registers, - ?array $schemas, - bool $_rbac, - bool $_multitenancy - ): array { - // Render each object through the render handler. - $promises = []; - foreach ($objects as $key => $object) { - // @psalm-suppress InvalidArgument Promise resolve accepts mixed. - $promises[$key] = new Promise( - function ($resolve, $reject) use ($object, $config, $registers, $schemas, $_rbac, $_multitenancy) { - try { - $renderedObject = $this->renderHandler->renderEntity( - entity: $object, - _extend: $config['extend'] ?? [], - filter: $config['unset'] ?? null, - fields: $config['fields'] ?? null, - registers: $registers, - schemas: $schemas, - _rbac: $_rbac, - _multitenancy: $_multitenancy - ); - - $resolve($renderedObject); - } catch (\Throwable $e) { - $reject($e); - }//end try - } - ); - }//end foreach - - // @psalm-suppress UndefinedFunction React\Async\await is from external library. - return Async\await(all($promises)); - }//end renderObjectsAsync() - /** * Counts the number of objects matching the given criteria. * @@ -996,23 +982,18 @@ function ($resolve, $reject) use ($object, $config, $registers, $schemas, $_rbac public function count( array $config=[] ): int { - // Add register and schema IDs to filters. - // Ensure we have both register and schema set. - if ($this->currentRegister !== null && empty($config['filers']['register']) === true) { - // Note: $_filters was intended for filter building but is currently unused. - // Filters are applied directly to $config instead. - // $_filters = ['register' => $this->currentRegister->getId()];. - } - - if ($this->currentSchema !== null && empty($config['filers']['schema']) === true) { - $config['filers']['schema'] = $this->currentSchema->getId(); - } - - // Remove limit from config as it's not needed for count. + // Scope the count to the current register/schema context (set via + // setRegister()/setSchema()) by passing them to the mapper as typed + // params. Without them MagicMapper::countAll() sums EVERY register/schema + // table — i.e. the whole instance — so a context-scoped caller would get + // the global object total instead of its own. Any extra ad-hoc filters + // travel in $config['filters'] and are applied within the scoped table(s). unset($config['limit']); return $this->objectMapper->countAll( - _filters: $config['filters'] ?? [] + _filters: $config['filters'] ?? [], + schema: $this->currentSchema, + register: $this->currentRegister ); }//end count() @@ -1105,6 +1086,12 @@ public function getLogs(string $uuid, array $filters=[], bool $_rbac=true, bool * @param bool $_multitenancy Whether to apply multitenancy filtering (default: true) * @param bool $silent Whether to skip audit trail creation and events (default: false) * @param array|null $uploadedFiles Uploaded files from multipart/form-data (optional) + * @param IUser|null $currentUser Explicit acting user for `@self.folder` access checks + * (forwarded to `ensureObjectFolder` → `assertObjectFolderAccessible`). + * Defaults to null → `IUserSession::getUser()` resolution. + * Non-HTTP callers (cron, import pipelines, event listeners) + * MUST pass an explicit user to avoid the + * default-deny fall-through on every folder-bound save. * * @return ObjectEntity The saved and rendered object * @@ -1114,7 +1101,9 @@ public function getLogs(string $uuid, array $filters=[], bool $_rbac=true, bool * Before saving object data, check if user has permission to create/update specific properties * based on property-level authorization arrays in the schema. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-3 + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Save options are flag-driven; `$currentUser` was added for `@self.folder` access checks. + * + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function saveObject( array | ObjectEntity $object, @@ -1125,8 +1114,15 @@ public function saveObject( bool $_rbac=true, bool $_multitenancy=true, bool $silent=false, - ?array $uploadedFiles=null + ?array $uploadedFiles=null, + ?IUser $currentUser=null ): ObjectEntity { + // Bound the folder-access revalidation cache to this single save call + // (not the whole FileService/request lifetime), so a cascade save that + // moves or trashes a folder mid-request can't be waved through on a + // stale "accessible" verdict from an earlier write. + $this->fileService->resetFolderAccessRevalidationCache(); + // Set register/schema context. $this->setContextFromParameters( register: $register, @@ -1190,6 +1186,18 @@ public function saveObject( ); } + // BUG-OBJ-4: applyAlwaysDefaults() and validateObjectIfRequired() + // both dereference $this->currentSchema (non-nullable param / + // ->getHardValidation()). If the schema could not be resolved from + // the request we would otherwise emit a raw TypeError 500 here. + // Throw a structured ValidationException instead, which the + // controllers translate into a clean 400 via handleValidationException(). + if ($this->currentSchema === null) { + throw new ValidationException( + message: 'Schema could not be resolved for this object; provide a valid register/schema.' + ); + } + // Apply "always" defaults BEFORE validation. // This ensures computed/derived properties (e.g., dienstType from type) are set // before validation runs, allowing them to override invalid incoming values. @@ -1203,11 +1211,24 @@ public function saveObject( // and casts it to date-only (e.g. "2024-01-15") so Opis validation passes. $object = $this->normalizeDateValues(object: $object); + // Auto-seed a graph-lifecycle field from the parent on CREATE only, + // BEFORE validation, so a required `$ref` lifecycle field passes on a + // seeded create. $uuidWasNull is the create signal (updates always + // carry a UUID); the seed itself is empty-field-only and fail-soft, so + // it never overwrites a client-supplied value. See the object-lifecycle + // spec: fk-graph-lifecycle-transitions. + if ($uuidWasNull === true && is_array($object) === true) { + $object = $this->saveHandler->seedLifecycleFieldOnCreate( + schema: $this->currentSchema, + data: $object + ); + } + // Validate if hard validation is enabled. $this->validateObjectIfRequired(object: $object); // Ensure folder exists for the object. - $folderId = $this->ensureObjectFolder(uuid: $uuid); + $folderId = $this->ensureObjectFolder(uuid: $uuid, currentUser: $currentUser); // Clear request-scoped caches before starting a new top-level save operation. // This ensures cascade operations benefit from caching while avoiding stale data. @@ -1225,28 +1246,49 @@ public function saveObject( persist: true, silent: $silent, _validation: true, - uploadedFiles: $uploadedFiles + uploadedFiles: $uploadedFiles, + currentUser: $currentUser ); // Invalidate contact matching cache for objects with email properties. + // BUG-OBJ-9: invalidate against the SAVED object's data (final UUID + + // applied defaults), not the pre-save input array, so an email value + // injected by a default/computed property is also invalidated. try { $container = \OC::$server; if ($container !== null) { $contactMatchingService = $container->get( \OCA\OpenRegister\Service\ContactMatchingService::class ); - $contactMatchingService->invalidateCacheForObject($object); + $contactMatchingService->invalidateCacheForObject($savedObject->getObject()); } } catch (\Throwable $e) { - // ContactMatchingService not available — skip cache invalidation. - // Catches Error too so a partially-wired container in tests (or - // any transitive DI failure) doesn't abort the save path. - } - - // Ensure the object has a file-storage folder (belt-and-suspenders for - // new objects that bypassed the pre-save ensureObjectFolder path). - $this->ensureObjectFolderExists(entity: $savedObject); + // BUG-OBJ-9 / BUG-OBJ-14: contact-match cache invalidation is a + // non-essential post-save side-effect and must NEVER fail the save. + // Catch \Throwable (the invalidation path can raise a runtime Error, + // e.g. an unavailable SystemTag subsystem, not just a container + // exception) but log it with object context so the miss stays + // visible instead of being silently swallowed. + $this->logger->warning( + message: '[ObjectService] Skipped contact-match cache invalidation: invalidation failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'exception' => $e->getMessage(), + 'uuid' => $savedObject->getUuid(), + 'register' => $this->currentRegister?->getId(), + 'schema' => $this->currentSchema?->getId(), + ] + ); + }//end try + // Lazy folder creation: intentionally do NOT create a file-storage + // folder here. An object only needs a folder once a file is attached; + // the folder is created on demand on the first upload + // (CreateFileHandler → getObjectFolder, which creates-if-missing). This + // avoids cluttering the Files tree with an empty folder per object and + // avoids binding system/seed-created objects to a folder a later editor + // can't access (the folder_access_denied case). // Render and return the saved object. return $this->renderHandler->renderEntity( entity: $savedObject, @@ -1289,7 +1331,7 @@ private function setContextFromParameters( * * @return array{0: array, 1: string|null} [normalized object array, extracted UUID] * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-25 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ private function extractUuidAndNormalizeObject(array | ObjectEntity $object, ?string $uuid): array { @@ -1394,12 +1436,16 @@ private function handleCascadingWithContextPreservation(array $object, ?string $ $currentRegisterId = $this->currentRegister->getId(); } - [$object, $uuid] = $this->cascadingHandler->handlePreValidationCascading( + $cascadeResult = $this->cascadingHandler->handlePreValidationCascading( object: $object, schema: $parentSchema, uuid: $uuid, currentRegister: $currentRegisterId ); + // The handler returns an [array $object, ?string $uuid] tuple; offset 0 + // always exists, offset 1 may be null — keep the current uuid then. + $object = $cascadeResult[0]; + $uuid = ($cascadeResult[1] ?? $uuid); // Restore the parent object's register and schema context after cascading. $this->currentRegister = $parentRegister; @@ -1419,6 +1465,16 @@ private function handleCascadingWithContextPreservation(array $object, ?string $ */ private function validateObjectIfRequired(array $object): void { + // BUG-OBJ-4: guard against a null schema reaching the + // ->getHardValidation() dereference (raw TypeError 500). Callers + // should already have thrown the structured ValidationException in + // saveObject(); this is a belt-and-suspenders 400 for any other path. + if ($this->currentSchema === null) { + throw new ValidationException( + message: 'Schema could not be resolved for this object; provide a valid register/schema.' + ); + } + // Validate the object against the current schema only if hard validation is enabled. if ($this->currentSchema->getHardValidation() === true) { $result = $this->validateHandler->validateObject( @@ -1499,11 +1555,16 @@ private function normalizeDateValues(array $object): array /** * Ensure object folder exists, create if needed. * - * @param string|null $uuid Object UUID + * @param string|null $uuid Object UUID + * @param IUser|null $currentUser Explicit acting user forwarded to the + * defense-in-depth re-validation; falls + * back to `IUserSession::getUser()`. + * Non-HTTP callers (cron, import + * pipelines) MUST pass an explicit user. * * @return int|null Folder ID if created/exists, null otherwise */ - private function ensureObjectFolder(?string $uuid): ?int + private function ensureObjectFolder(?string $uuid, ?IUser $currentUser=null): ?int { // Handle folder creation for existing objects or new objects with UUIDs. $folderId = null; @@ -1513,21 +1574,62 @@ private function ensureObjectFolder(?string $uuid): ?int try { $existingObject = $this->objectMapper->find($uuid); $folder = $existingObject->getFolder(); - $isString = is_string($folder) === true; - if ($folder === null || $folder === '' || $isString === true) { - try { - $folderId = $this->fileService->createObjectFolderWithoutUpdate($existingObject); - } catch (Exception $e) { - // Log error but continue - object can function without folder. - } + // The `_folder` column is `varchar(255)` — every populated + // value is a string. The earlier `is_string($folder) === true` + // clause matched ANY non-empty string and so triggered an + // auto-create on every update, overwriting valid folder + // bindings with freshly-generated auto-folders under the + // register's storage tree. The intent of the string branch + // was to handle LEGACY non-numeric string paths that + // pre-date the integer-id storage convention; restrict the + // check to that case. + $needsAutoCreate = ( + $folder === null + || $folder === '' + || (is_string($folder) === true && is_numeric($folder) === false) + ); + + if ($needsAutoCreate === false) { + // Defense in depth (PR #1431 review concern): the + // `setSelfMetadata` access check only fires when the + // write payload includes `@self.folder`. Pre-PR + // cross-tenant bindings (or any subsequent save that + // touches other fields) would otherwise pass through + // unchecked. Re-validate the existing binding on every + // save so the check applies uniformly. Throws + // `FolderAccessDeniedException` → HTTP 403 at the + // controller layer when the acting user cannot access + // the bound folder. The existing binding is kept, so no + // folder id is returned (auto-create is not needed). + $this->fileService->assertObjectFolderAccessible( + object: $existingObject, + currentUser: $currentUser + ); + return null; } + + // Lazy folder creation: do NOT create a Files folder for an + // object that has none. Most objects never get a file attached, + // so eagerly creating a per-object folder on every save (a) + // clutters the Files tree with thousands of empty folders and + // (b) can bind the object to a folder created in a + // no-session/system context (e.g. config-import seeding) that a + // later editor cannot access — which then denies the edit with + // folder_access_denied. The folder is created on demand the + // first time a file is actually uploaded to the object + // (CreateFileHandler → getObjectFolder, which creates-if-missing). + // Leave $folderId null; the object functions fine without one. + $folderId = null; + } catch (\OCA\OpenRegister\Exception\FolderAccessDeniedException $e) { + // Propagate folder-access denials up to the controller. + throw $e; } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { // Object not found, will create new one with the specified UUID. // Let SaveObject handle the creation with the provided UUID. } catch (Exception $e) { // Other errors - let SaveObject handle the creation. - } + }//end try }//end if return $folderId; @@ -1575,7 +1677,7 @@ private function ensureObjectFolder(?string $uuid): ?int * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @spec openspec/changes/add-archival-annotation-support/tasks.md#task-3 + * @spec openspec/specs/archival-annotation-vocabulary/spec.md */ public function deleteObject( string $uuid, @@ -1702,7 +1804,7 @@ public function deleteObject( * * @return bool True when the schema carries a valid archival annotation. * - * @spec openspec/changes/add-archival-annotation-support/tasks.md#task-3 + * @spec openspec/specs/archival-annotation-vocabulary/spec.md */ private function schemaHasArchivalAnnotation(Schema $schema): bool { @@ -1722,7 +1824,7 @@ private function schemaHasArchivalAnnotation(Schema $schema): bool * * @throws \OCP\AppFramework\Http\ContentSecurityPolicy * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-4 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ private function rejectIfTransferred(string $uuid): void { @@ -1851,7 +1953,7 @@ private function applyViewsToQuery(array $query, array $viewIds): array * * **Numeric-ID contract (runtime-schema-api):** `@self.register` and * `@self.schema` MUST be numeric IDs (int). Passing a slug string here - * is the documented foot-gun the OpenBuilt smoke test surfaced — it + * is the documented foot-gun the OpenBuild smoke test surfaced — it * silently returns zero results instead of resolving the slug. * Slug-aware callers MUST use {@see self::searchObjectsBySlug()}, * which resolves the slugs via the mappers and then delegates here on @@ -1913,7 +2015,7 @@ public function searchObjects( * the mappers' standard multi-tenancy filter) and delegates to the * numeric-ID search path. * - * This helper exists to close the OpenBuilt smoke-test foot-gun where + * This helper exists to close the OpenBuild smoke-test foot-gun where * callers passed slugs in `@self.register` / `@self.schema` and got * zero results back. The strict numeric-ID contract on `searchObjects` * means any future misuse fails loudly at the call site; slug-aware @@ -2250,9 +2352,35 @@ public function searchObjectsPaginated( ?string $uses=null, ?array $views=null ): array { + // Object-source delegation: a schema served from an external source + // (x-openregister-object-source) lists live from its provider, never the + // search index or magic table. Returns the standard paginated shape. + $sourcePaginated = $this->paginateObjectSource(query: $query, _rbac: $_rbac); + if ($sourcePaginated !== null) { + return $sourcePaginated; + } + + // Capture the start time so the search trail can record the real + // response time regardless of which backend (index/database) runs. + $searchStartTime = microtime(true); + + // Detect a cross-schema search: a `@self.schema` array, `_schemas`, or + // `@self.schemas` means the caller wants to search MANY schemas (e.g. the + // unified-search provider passes every searchable schema). Such a search + // must NOT be scoped to a single register — each schema is resolved to + // its own owning register downstream — so the ambient currentRegister + // (often a default like the first register) must not leak in. + $selfSchema = ($query['@self']['schema'] ?? null); + $isMultiSchemaCtx = (is_array($selfSchema) === true && count($selfSchema) > 0) + || array_key_exists('_schemas', $query) === true + || (isset($query['@self']['schemas']) === true); + // Add register and schema context to query for magic mapper routing. // Use array_key_exists to allow explicit null values to disable auto-setting. - if ($this->currentRegister !== null && array_key_exists('_register', $query) === false) { + if ($this->currentRegister !== null + && array_key_exists('_register', $query) === false + && $isMultiSchemaCtx === false + ) { $query['_register'] = $this->currentRegister->getId(); } @@ -2273,95 +2401,6 @@ public function searchObjectsPaginated( // Strip deprecated _source parameter (silently ignore for backward compatibility). unset($query['_source']); - // Use SOLR if enabled in config, unless relation-based search params are provided. - $hasIds = isset($query['_ids']) === true; - $hasUses = isset($query['_uses']) === true; - $hasIdsParam = $ids !== null; - $hasUsesParam = $uses !== null; - $isSolrEnabled = $this->isSolrAvailable() === true; - if ($isSolrEnabled === true - && $hasIdsParam === false && $hasUsesParam === false - && $hasIds === false && $hasUses === false - ) { - // Forward to Index service - let it handle availability checks and error handling. - $indexService = $this->container->get(IndexService::class); - $result = $indexService->searchObjects( - query: $query, - _rbac: $_rbac, - _multitenancy: $_multitenancy, - deleted: $deleted - ); - $result['@self']['source'] = 'index'; - $result['@self']['query'] = $query; - $result['@self']['rbac'] = $_rbac; - $result['@self']['multi'] = $_multitenancy; - $result['@self']['deleted'] = $deleted; - - // Per the files-render-extension capability, every list row MUST carry - // @self.files. The SOLR/index path does not route rows through renderEntities, - // so attach the lightweight ID list via a single batched FileMapper lookup. - // Note: the SOLR path does not currently support _extend[]=@self.files for - // full metadata; consumers needing full metadata should query the DB path. - if (isset($result['results']) === true && is_array($result['results']) === true) { - $this->renderHandler->attachLightweightFilesToRows(rows: $result['results']); - } - - // Surface a machine-readable signal when the consumer asked for an - // extend value the SOLR path cannot honour, so they can detect the - // shape mismatch programmatically instead of diffing the response. - // Today this only applies to @self.files (and its shorthand _files). - $extendForSignal = $query['_extend'] ?? []; - if (is_string($extendForSignal) === true) { - $extendForSignal = array_filter(array_map('trim', explode(',', $extendForSignal))); - } - - if (is_array($extendForSignal) === true - && (in_array('@self.files', $extendForSignal, true) === true - || in_array('_files', $extendForSignal, true) === true) - ) { - $result['@self']['extend_unsupported'] = ['@self.files']; - } - - // Add extended objects only if _extend is requested. - // Normalize _extend to array (handles comma-separated string from URL). - $extend = $query['_extend'] ?? []; - if (is_string($extend) === true) { - $extend = array_filter(array_map('trim', explode(',', $extend))); - } - - if (empty($extend) === false) { - $result['@self']['objects'] = $this->getExtendedObjects(); - } - - // Add names mapping if _names is in _extend. - // This provides UUID-to-name mappings for all related objects in the results, - // reducing frontend calls to the names service. - if (is_array($extend) === true && in_array('_names', $extend, true) === true) { - $resultsToProcess = $result['results'] ?? []; - - // Only process if results exist and is an array. - $result['@self']['names'] = []; - - if (is_array($resultsToProcess) === true && empty($resultsToProcess) === false) { - try { - $result['@self']['names'] = $this->collectNamesForResults(results: $resultsToProcess); - } catch (\Throwable $e) { - $errMsg = $e->getMessage(); - $errFile = $e->getFile(); - $errLine = $e->getLine(); - $this->logger->error( - message: "[ObjectService] _names extension failed: {$errMsg} at {$errFile}:{$errLine}", - context: ['file' => __FILE__, 'line' => __LINE__] - ); - $result['@self']['names'] = []; - $result['@self']['names_error'] = $e->getMessage(); - } - } - }//end if - - return $result; - }//end if - // Bypass multitenancy for schemas with public read access. // Public schemas should be visible to all users regardless of organisation. $effectiveMt = $_multitenancy; @@ -2425,18 +2464,336 @@ public function searchObjectsPaginated( } }//end if + $this->recordSearchTrail(query: $query, result: $result, startTime: $searchStartTime); + return $result; }//end searchObjectsPaginated() /** - * Check if Solr is available for use. + * Build a paginated result from the current schema's object-source provider, + * or null when the current schema is not served from an external source. + * + * Mirrors the `{ results, total, '@self' }` shape that the index/database + * search backends return, so the controller renders virtual objects exactly + * like native ones. When the schema declares a source whose provider is + * missing/disabled, returns an empty paginated result (never the DB). + * + * @param array $query The search query (filters/limit/offset). + * @param bool $_rbac Whether to enforce RBAC checks. + * + * @return array|null The paginated result, or null when not source-backed. + * + * @throws \OCA\OpenRegister\Exception\NotAuthorizedException When the acting user lacks read access to the schema. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function paginateObjectSource(array $query, bool $_rbac=true): ?array + { + $schema = $this->currentSchema; + if ($schema === null) { + return null; + } + + $source = $schema->getObjectSource(); + if ($source === null) { + return null; + } + + // Read-access parity (no enumeration oracle): enforce the schema-level + // read authorization BEFORE the provider — and therefore the external + // database — is consulted. A denied user receives the same + // NotAuthorizedException a native schema raises, whether or not any + // matching external rows exist. + $this->checkPermission( + schema: $schema, + action: 'read', + userId: null, + objectOwner: null, + _rbac: $_rbac + ); + + // The canonical search query keeps object-field filters as TOP-LEVEL + // keys and paging as `_limit`/`_page`/`_offset`, while the provider + // contract reads `filters`, `limit` and `offset`. Normalise additively + // (existing keys are never overwritten) so providers written against + // either shape keep working. + $query = $this->normaliseObjectSourceQuery(query: $query); + + $results = []; + $config = ($source['config'] ?? []); + $provider = $this->objectSourceRegistry->get($source['provider']); + $active = ($provider !== null && $provider->isEnabled() === true && $this->currentRegister !== null); + if ($active === false) { + $this->logger->warning( + sprintf( + '[ObjectSource] schema "%s" declares provider "%s" but it is missing/disabled or has no register context — returning empty list', + (string) $schema->getSlug(), + $source['provider'] + ) + ); + } else { + $results = $provider->findAll( + register: $this->currentRegister, + schema: $schema, + query: $query, + config: $config + ); + } + + $resultCount = count($results); + $limit = (int) ($query['limit'] ?? 0); + $offset = max(0, (int) ($query['offset'] ?? 0)); + + // D4b: consult the provider's count() for the TRUE total and compute + // page metadata, passing limit/offset through (findAll already received + // them). A provider that cannot report a real total — count() throws or + // returns a value inconsistent with the returned window — falls back to + // the pre-existing in-memory behaviour so the native providers are + // unaffected. + $trueTotal = null; + if ($active === true) { + $trueTotal = $this->objectSourceTrueTotal( + provider: $provider, + schema: $schema, + query: $query, + config: $config, + offset: $offset, + resultCount: $resultCount + ); + } + + $total = ($trueTotal ?? $resultCount); + $self = $this->objectSourcePageMetadata( + total: $total, + limit: $limit, + offset: $offset, + realCount: ($trueTotal !== null) + ); + + return [ + 'results' => $results, + 'total' => $total, + '@self' => $self, + ]; + }//end paginateObjectSource() + + /** + * Adapt a canonical search query to the object-source provider contract. + * + * The canonical shape (SearchQueryHandler::buildSearchQuery) carries object + * field filters as top-level keys, paging as `_limit`/`_page`/`_offset`, + * and sorting as `_order`. Providers read `filters`, `limit`, `offset` and + * `sort`. Mapping is ADDITIVE: a key the caller already set is never + * overwritten, so provider behaviour under the old shape is unchanged. + * + * @param array $query The canonical search query. + * + * @return array The query with provider-contract keys added. + * + * @SuppressWarnings(PHPMD.NPathComplexity) Additive key-by-key mapping; each guard is independent by design + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + * + * @SuppressWarnings(PHPMD.NPathComplexity) Additive key-mapping requires one guard per provider key + */ + private function normaliseObjectSourceQuery(array $query): array + { + if (isset($query['limit']) === false && isset($query['_limit']) === true) { + $query['limit'] = (int) $query['_limit']; + } + + if (isset($query['offset']) === false) { + $offset = (int) ($query['_offset'] ?? 0); + $page = (int) ($query['_page'] ?? 0); + $limit = (int) ($query['limit'] ?? 0); + if ($offset === 0 && $page > 1 && $limit > 0) { + $offset = (($page - 1) * $limit); + } + + if ($offset > 0) { + $query['offset'] = $offset; + } + } + + if (isset($query['sort']) === false && isset($query['_order']) === true) { + $query['sort'] = $query['_order']; + } + + if (isset($query['filters']) === false) { + $filters = []; + foreach ($query as $key => $value) { + $key = (string) $key; + if ($key === '' || $key[0] === '_' || $key[0] === '@') { + continue; + } + + if (in_array($key, ['limit', 'offset', 'sort', 'filters', 'extend', 'fields'], true) === true) { + continue; + } + + if (is_scalar($value) === true) { + $filters[$key] = $value; + } + } + + if ($filters !== []) { + $query['filters'] = $filters; + } + }//end if + + return $query; + }//end normaliseObjectSourceQuery() + + /** + * Consult an object-source provider's count() for the true total (D4b). + * + * Returns null — signalling the in-memory fallback — when count() throws or + * reports a value inconsistent with the returned window (smaller than + * offset + returned results), so providers without real count support keep + * their pre-existing single-page behaviour. + * + * @param \OCA\OpenRegister\Service\ObjectSource\ObjectSourceProvider $provider The resolved provider. + * @param Schema $schema The sourced schema. + * @param array $query The search query. + * @param array $config The object-source config block. + * @param int $offset The requested offset. + * @param int $resultCount The returned window size. * - * @return bool True if Solr is enabled and available, false otherwise + * @return int|null The true total, or null when the provider has no real count. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md */ - private function isSolrAvailable(): bool + private function objectSourceTrueTotal( + $provider, + Schema $schema, + array $query, + array $config, + int $offset, + int $resultCount + ): ?int { + try { + $counted = $provider->count( + register: $this->currentRegister, + schema: $schema, + query: $query, + config: $config + ); + } catch (\Throwable $e) { + $this->logger->warning( + '[ObjectSource] provider "'.$provider->getId().'" count() unavailable — falling back to in-memory total: '.$e->getMessage() + ); + return null; + } + + if ($counted >= ($offset + $resultCount)) { + return $counted; + } + + return null; + }//end objectSourceTrueTotal() + + /** + * Compute the `@self` pagination block for an object-source result (D4b). + * + * @param int $total The (true or fallback) total. + * @param int $limit The requested limit (0 = none). + * @param int $offset The requested offset. + * @param bool $realCount Whether the total came from a real provider count. + * + * @return array The `@self` pagination metadata. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function objectSourcePageMetadata(int $total, int $limit, int $offset, bool $realCount): array + { + $page = 1; + $pages = 1; + $next = null; + $prev = null; + + if ($realCount === true && $limit > 0) { + $pages = max(1, (int) ceil($total / $limit)); + $page = ((int) floor($offset / $limit) + 1); + if ($page < $pages) { + $next = ($page + 1); + } + + if ($page > 1) { + $prev = ($page - 1); + } + } + + $effectiveLimit = $total; + if ($limit > 0) { + $effectiveLimit = $limit; + } + + return [ + 'total' => $total, + 'page' => $page, + 'pages' => $pages, + 'limit' => $effectiveLimit, + 'next' => $next, + 'prev' => $prev, + ]; + }//end objectSourcePageMetadata() + + /** + * Record a search-trail entry for a paginated search, honouring the + * configured recording mode. + * + * Resolves the effective mode via SearchQueryHandler (memoized per + * request): 'none' records nothing; '_search' records only when a + * non-empty `_search` term is present; 'all' records every paginated + * search. Derives the execution type from the result source (index vs + * database) and the response time from the captured start time. The + * entry is buffered and persisted after the response (deferred flush in + * SearchQueryHandler), so recording adds no write latency to the search. + * Never throws — a recording failure must not affect the search response. + * + * @param array $query The post-view-merge search query. + * @param array $result The paginated search result (results, total, @self). + * @param float $startTime The microtime(true) captured at search entry. + * + * @return void + * + * @spec openspec/specs/search-trail-recording/spec.md + */ + private function recordSearchTrail(array $query, array $result, float $startTime): void { - return $this->searchQueryHandler->isSolrAvailable(); - }//end isSolrAvailable() + try { + $mode = $this->searchQueryHandler->getEffectiveRecordingMode(); + if ($mode === 'none') { + return; + } + + $searchTerm = trim((string) ($query['_search'] ?? '')); + if ($mode === '_search' && $searchTerm === '') { + return; + } + + $responseTime = (float) ((microtime(true) - $startTime) * 1000); + $source = $result['@self']['source'] ?? 'database'; + $executionType = 'database'; + if ($source === 'index') { + $executionType = 'index'; + } + + $this->searchQueryHandler->logSearchTrail( + $query, + count($result['results'] ?? []), + (int) ($result['total'] ?? 0), + $responseTime, + $executionType + ); + } catch (\Throwable $e) { + // Recording is best-effort and must never fail the search. + $this->logger->warning( + message: '[ObjectService] recordSearchTrail failed: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + }//end try + }//end recordSearchTrail() /** * Original database search logic - extracted to avoid code duplication. @@ -2490,6 +2847,39 @@ public function getRegister(): int return $this->currentRegister->getId(); }//end getRegister() + /** + * Returns all registers with their schemas expanded. + * + * @return array List of registers, each with a 'schemas' array of schema objects. + * + * @spec exclude Read-only lister returning registers with their schemas inlined; no business rule. + */ + public function getRegisters(): array + { + $registers = $this->registerMapper->findAll(_multitenancy: false); + $result = []; + + foreach ($registers as $register) { + $registerArr = $register->jsonSerialize(); + $schemaIds = $register->getSchemas() ?? []; + $schemas = []; + + foreach ($schemaIds as $schemaId) { + try { + $schema = $this->schemaMapper->find(id: (int) $schemaId, _multitenancy: false); + $schemas[] = $schema->jsonSerialize(); + } catch (\Exception $e) { + // Skip schemas that cannot be found. + } + } + + $registerArr['schemas'] = $schemas; + $result[] = $registerArr; + } + + return $result; + }//end getRegisters() + /** * Renders the rendered object. * @@ -2590,7 +2980,7 @@ public function getDeleteHandler(): DeleteObject * * @return array Map of UUID to name. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-2 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ private function collectNamesForResults(array $results): array { @@ -2824,6 +3214,9 @@ public function handleValidationException( * @param string $identifier Object ID or UUID * @param string|null $process Process ID (for tracking who locked it) * @param int|null $duration Lock duration in seconds + * @param bool $advisory When true, treat the identifier as a synthetic + * pre-creation key and take the appConfig-backed + * advisory lock without scanning object tables * * @return array Lock information * @@ -2831,9 +3224,9 @@ public function handleValidationException( * * @spec exclude One-line delegation to lock handler; lock behavior owned by object-lifecycle. */ - public function lockObject(string $identifier, ?string $process=null, ?int $duration=null): array + public function lockObject(string $identifier, ?string $process=null, ?int $duration=null, bool $advisory=false): array { - return $this->lockHandler->lock(identifier: $identifier, process: $process, duration: $duration); + return $this->lockHandler->lock(identifier: $identifier, process: $process, duration: $duration, advisory: $advisory); }//end lockObject() /** @@ -2842,6 +3235,8 @@ public function lockObject(string $identifier, ?string $process=null, ?int $dura * Removes the lock from an object, allowing other processes to modify it. * * @param string|int $identifier The object to unlock + * @param bool $advisory When true, release the appConfig-backed advisory + * lock for this synthetic key without scanning tables * * @return true True if unlocked successfully * @@ -2849,9 +3244,9 @@ public function lockObject(string $identifier, ?string $process=null, ?int $dura * * @spec exclude One-line delegation to lock handler; unlock behavior owned by object-lifecycle. */ - public function unlockObject(string|int $identifier): bool + public function unlockObject(string|int $identifier, bool $advisory=false): bool { - return $this->lockHandler->unlock(identifier: (string) $identifier); + return $this->lockHandler->unlock(identifier: (string) $identifier, advisory: $advisory); }//end unlockObject() /** @@ -2913,7 +3308,7 @@ public function unlockObject(string|int $identifier): bool * * @phpstan-return array * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-27 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function saveObjects( array $objects, @@ -2927,6 +3322,10 @@ public function saveObjects( bool $enrich=true ): array { + // Bound the folder-access revalidation cache to this bulk-save call + // (see saveObject) so mid-request folder mutations are re-validated. + $this->fileService->resetFolderAccessRevalidationCache(); + // Set register and schema context if provided. if ($register !== null) { $this->setRegister(register: $register); @@ -2963,15 +3362,18 @@ public function saveObjects( schemaId: $this->currentSchema?->getId() ); } catch (\Exception $e) { + // BUG-OBJ-14: include register/schema context in the warning. $this->logger->warning( message: '[ObjectService] Bulk save cache invalidation failed', context: [ 'error' => $e->getMessage(), 'totalAffected' => $totalAffected, + 'registerId' => $this->currentRegister?->getId(), + 'schemaId' => $this->currentSchema?->getId(), ] ); } - } + }//end if return $bulkResult; }//end saveObjects() @@ -3072,26 +3474,124 @@ public function deleteObjects(array $uuids=[], bool $_rbac=true, bool $_multiten ); } + // PERF: resolve the scope AND the entity of every UUID with ONE batched + // cross-table lookup instead of a full magic-table scan per UUID. The + // batch matches on the uuid column only; identifiers in other shapes + // (numeric ids, slugs, URIs) fall back to the legacy per-uuid lookup. + $preResolvedObjects = $this->batchResolveDeleteScopes(uuids: $filteredUuids); + + // Request-local caches so each distinct (register, schema) pair is + // materialised as entities at most once for the whole bulk operation. + $registerEntityCache = []; + $schemaEntityCache = []; + // Process each object individually through the delete handler so that // referential integrity rules (CASCADE, SET_NULL, SET_DEFAULT, RESTRICT) // are enforced per object. Skips objects that fail (e.g., RESTRICT blocks). $deletedObjectIds = []; $skippedUuids = []; $totalCascadeCount = 0; + // BUG-OBJ-5: collect the distinct (registerId, schemaId) pairs of the + // objects we actually delete so we can invalidate the per-schema query + // cache for each of them. A bulk delete may span multiple registers / + // schemas (cross-table), and CacheHandler::clearSchemaRelatedCaches() + // only clears the distributed query cache when schemaId !== null — + // passing null/null below left those caches stale. + $invalidationPairs = []; foreach ($filteredUuids as $uuid) { try { + // BUG-OBJ-5: resolve the object's register/schema BEFORE deleting + // it (the delete handler returns only a bool). Prefer the batch + // resolution above; fall back to a per-uuid mapper find (no read + // audit trail) that also matches non-uuid identifier shapes. Both + // include already-soft-deleted rows so a delete of a trashed + // object still yields its scope. + $deletedRegisterId = null; + $deletedSchemaId = null; + $preResolved = ($preResolvedObjects[$uuid] ?? null); + if ($preResolved !== null) { + $deletedRegisterId = $preResolved->getRegister(); + $deletedSchemaId = $preResolved->getSchema(); + } + + if ($preResolved === null) { + try { + $preDeleteObject = $this->objectMapper->find( + identifier: $uuid, + register: $this->currentRegister, + schema: $this->currentSchema, + includeDeleted: true, + _rbac: $_rbac, + _multitenancy: $_multitenancy + ); + $deletedRegisterId = $preDeleteObject->getRegister(); + $deletedSchemaId = $preDeleteObject->getSchema(); + } catch (\Throwable $resolveError) { + // BUG-OBJ-14: scope resolution failed (object already gone or + // not visible) — log and fall back to a broad invalidation. + $this->logger->warning( + message: '[ObjectService] Bulk delete could not resolve register/schema scope for cache invalidation', + context: [ + 'uuid' => $uuid, + 'error' => $resolveError->getMessage(), + ] + ); + }//end try + }//end if + + // PERF: hand the batch-resolved entity + its concrete scope to the + // delete handler so it skips its own per-object cross-table re-find. + // When scope entities cannot be materialised, keep the legacy call. + $handlerRegister = $this->currentRegister; + $handlerSchema = $this->currentSchema; + $handlerPreResolved = null; + if ($preResolved !== null) { + $scopeEntities = $this->loadDeleteScopeEntities( + registerId: $deletedRegisterId, + schemaId: $deletedSchemaId, + registerCache: $registerEntityCache, + schemaCache: $schemaEntityCache + ); + if ($scopeEntities !== null) { + [$handlerRegister, $handlerSchema] = $scopeEntities; + $handlerPreResolved = $preResolved; + } + } + $result = $this->deleteHandler->deleteObject( - register: $this->currentRegister, - schema: $this->currentSchema, + register: $handlerRegister, + schema: $handlerSchema, uuid: $uuid, originalObjectId: null, _rbac: $_rbac, - _multitenancy: $_multitenancy + _multitenancy: $_multitenancy, + preResolved: $handlerPreResolved ); if ($result === true) { $deletedObjectIds[] = $uuid; + // BUG-OBJ-10: read the cascade count EXACTLY ONCE per + // successful delete. The handler resets lastCascadeCount to 0 + // at the start of every root delete (originalObjectId === null, + // which is always the case here), so each read reflects only + // this object's cascade and accumulates correctly. $totalCascadeCount += $this->deleteHandler->getLastCascadeCount(); - } + + // BUG-OBJ-5: record the distinct scope pair for invalidation. + // CacheHandler expects int ids; entity getters return string ids. + if ($deletedSchemaId !== null && $deletedSchemaId !== '') { + $registerIdInt = null; + if ($deletedRegisterId !== null && $deletedRegisterId !== '') { + $registerIdInt = (int) $deletedRegisterId; + } + + $schemaIdInt = (int) $deletedSchemaId; + $pairKey = ($registerIdInt ?? 'null').':'.$schemaIdInt; + $invalidationPairs[$pairKey] = [ + 'registerId' => $registerIdInt, + 'schemaId' => $schemaIdInt, + ]; + } + }//end if } catch (\OCA\OpenRegister\Exception\ReferentialIntegrityException $e) { // RESTRICT blocks should not abort the entire bulk operation. // Log and skip this object, continue with the rest. @@ -3117,24 +3617,40 @@ public function deleteObjects(array $uuids=[], bool $_rbac=true, bool $_multiten }//end foreach // Invalidate collection caches after bulk delete operations. + // BUG-OBJ-5: invalidate per distinct (register, schema) pair gathered + // from the deleted objects, so CacheHandler::clearSchemaRelatedCaches() + // actually clears the per-schema distributed query cache (it no-ops when + // schemaId is null). Falls back to a broad null/null invalidation only + // when no scope could be resolved for any deleted object. if (empty($deletedObjectIds) === false) { - try { - $this->cacheHandler->invalidateForObjectChange( - object: null, - operation: 'bulk_delete', - registerId: null, - schemaId: null - ); - } catch (\Exception $e) { - $this->logger->warning( - message: '[ObjectService] Bulk delete cache invalidation failed', - context: [ - 'error' => $e->getMessage(), - 'deletedCount' => count($deletedObjectIds), - ] - ); + $pairsToInvalidate = $invalidationPairs; + if (empty($pairsToInvalidate) === true) { + $pairsToInvalidate = [['registerId' => null, 'schemaId' => null]]; } - } + + foreach ($pairsToInvalidate as $pair) { + try { + $this->cacheHandler->invalidateForObjectChange( + object: null, + operation: 'bulk_delete', + registerId: $pair['registerId'], + schemaId: $pair['schemaId'] + ); + } catch (\Exception $e) { + // BUG-OBJ-14: log with register/schema context instead of + // silently swallowing the invalidation failure. + $this->logger->warning( + message: '[ObjectService] Bulk delete cache invalidation failed', + context: [ + 'error' => $e->getMessage(), + 'deletedCount' => count($deletedObjectIds), + 'registerId' => $pair['registerId'], + 'schemaId' => $pair['schemaId'], + ] + ); + } + }//end foreach + }//end if return [ 'deleted_uuids' => $deletedObjectIds, @@ -3143,11 +3659,118 @@ public function deleteObjects(array $uuids=[], bool $_rbac=true, bool $_multiten ]; }//end deleteObjects() + /** + * Batch-resolve the entities (and thus register/schema scopes) for a set of UUIDs. + * + * One cross-table lookup for the whole set (uuid-column matches only). + * Includes soft-deleted rows so deleting a trashed object still resolves + * its scope. A total lookup failure logs and returns an empty map — every + * uuid then falls back to the legacy per-uuid resolution. + * + * @param array $uuids The UUIDs to resolve. + * + * @return array Resolved entities keyed by uuid. + * + * @phpstan-param array $uuids + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function batchResolveDeleteScopes(array $uuids): array + { + $resolved = []; + try { + $entities = $this->objectMapper->findMultipleAcrossAllMagicTables( + uuids: $uuids, + includeDeleted: true + ); + foreach ($entities as $entity) { + $entityUuid = $entity->getUuid(); + if ($entityUuid !== null && $entityUuid !== '') { + $resolved[$entityUuid] = $entity; + } + } + } catch (\Throwable $batchError) { + $this->logger->warning( + message: '[ObjectService] Bulk delete batch scope resolution failed; falling back to per-uuid lookups', + context: ['error' => $batchError->getMessage()] + ); + } + + return $resolved; + }//end batchResolveDeleteScopes() + + /** + * Materialise the Register + Schema entities for a resolved delete scope. + * + * Uses per-bulk-operation caches (passed by reference) so each distinct + * pair is loaded at most once; RegisterMapper/SchemaMapper add their own + * request-scoped caching underneath. Returns null when either entity + * cannot be loaded — the caller then keeps the legacy delete call. + * + * @param string|int|null $registerId The register id from the resolved entity. + * @param string|int|null $schemaId The schema id from the resolved entity. + * @param array $registerCache Cache of Register entities keyed by int id. + * @param array $schemaCache Cache of Schema entities keyed by int id. + * + * @return array{0: \OCA\OpenRegister\Db\Register, 1: \OCA\OpenRegister\Db\Schema}|null + * + * @spec openspec/specs/object-lifecycle/spec.md + */ + private function loadDeleteScopeEntities( + string | int | null $registerId, + string | int | null $schemaId, + array &$registerCache, + array &$schemaCache + ): ?array { + if (is_numeric($registerId) === false || is_numeric($schemaId) === false) { + return null; + } + + $registerKey = (int) $registerId; + $schemaKey = (int) $schemaId; + + try { + if (isset($registerCache[$registerKey]) === false) { + $registerCache[$registerKey] = $this->registerMapper->find( + $registerKey, + _rbac: false, + _multitenancy: false + ); + } + + if (isset($schemaCache[$schemaKey]) === false) { + $schemaCache[$schemaKey] = $this->schemaMapper->find( + $schemaKey, + _rbac: false, + _multitenancy: false + ); + } + } catch (\Throwable $scopeError) { + $this->logger->warning( + message: '[ObjectService] Bulk delete could not materialise scope entities', + context: [ + 'registerId' => $registerKey, + 'schemaId' => $schemaKey, + 'error' => $scopeError->getMessage(), + ] + ); + return null; + }//end try + + return [$registerCache[$registerKey], $schemaCache[$schemaKey]]; + }//end loadDeleteScopeEntities() + /** * Delete all objects belonging to a specific schema * - * This method efficiently deletes all objects that belong to the specified schema. - * It uses bulk operations for optimal performance and maintains data integrity. + * Every object is snapshotted to the (hash-chained) audit trail before its row is + * touched, then the rows are removed in one bulk statement via MagicMapper. + * + * The work itself lives in SchemaDeletionService, which is shared with the schema + * cascade (`DELETE /api/schemas/{id}?deleteObjects=true`). It is resolved lazily + * from the container rather than constructor-injected: ObjectService is + * constructed on virtually every request, and this is the only method that needs + * it. * * @param int $registerId The ID of the register * @param int $schemaId The ID of the schema whose objects should be deleted @@ -3161,13 +3784,19 @@ public function deleteObjects(array $uuids=[], bool $_rbac=true, bool $_multiten * * @psalm-return array{deleted_count: int, deleted_uuids: array, schema_id: int} * - * @spec exclude Deprecated throwing stub; schema-wide delete awaits MagicMapper reimplementation (blob table retired). + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The hard/soft toggle mirrors the mapper primitive it wraps. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md */ public function deleteObjectsBySchema(int $registerId, int $schemaId, bool $hardDelete=false): array { - // TODO: Reimplement using MagicMapper for schema-wide delete on magic tables. - throw new RuntimeException( - 'deleteObjectsBySchema needs reimplementation using MagicMapper (blob objects table retired)' + $schemaDeletionService = $this->container->get(\OCA\OpenRegister\Service\SchemaDeletionService::class); + + return $schemaDeletionService->deleteObjectsBySchema( + registerId: $registerId, + schemaId: $schemaId, + hardDelete: $hardDelete, + triggeredBy: \OCA\OpenRegister\Service\SchemaDeletionService::TRIGGER_BULK_DELETE ); }//end deleteObjectsBySchema() @@ -3494,7 +4123,7 @@ public function mergeObjects(string $sourceObjectId, array $mergeData): array * * @return array Validation result with valid and invalid objects * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-26 + * @spec openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md */ public function validateObjectsBySchema(int $schemaId): array { diff --git a/lib/Service/ObjectSource/CalDavVtodoObjectSourceProvider.php b/lib/Service/ObjectSource/CalDavVtodoObjectSourceProvider.php new file mode 100644 index 0000000000..5fcb1e2016 --- /dev/null +++ b/lib/Service/ObjectSource/CalDavVtodoObjectSourceProvider.php @@ -0,0 +1,276 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Service\TaskService; +use OCP\App\IAppManager; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by CalDAV VTODOs. + */ +class CalDavVtodoObjectSourceProvider implements ObjectSourceProvider +{ + /** + * Constructor. + * + * @param TaskService $taskService The CalDAV VTODO read/list service. + * @param IAppManager $appManager App availability checks. + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly TaskService $taskService, + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.1 + */ + public function getId(): string + { + return 'caldav-vtodo'; + }//end getId() + + /** + * {@inheritDoc} + * + * Enabled when CalDAV is available. VTODOs live in the user's CalDAV + * calendars served by the CORE `dav` app — the `tasks`/`calendar` apps are + * only optional UIs, so this checks `dav` (with tasks/calendar as positive + * signals too). + * + * @return bool True when CalDAV VTODO reads are available. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.2 + */ + public function isEnabled(): bool + { + try { + return ($this->appManager->isInstalled('dav') === true + || $this->appManager->isInstalled('tasks') === true + || $this->appManager->isInstalled('calendar') === true); + } catch (Throwable $e) { + return false; + } + }//end isEnabled() + + /** + * {@inheritDoc} + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param string $id The VTODO id (calendar-object uri) or uid. + * @param array $config The object-source config block. + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.1 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + foreach ($this->readTasks(config: $config, limit: 1000, offset: 0) as $task) { + if ($this->matchesScope(task: $task, register: $register, schema: $schema, config: $config) === false) { + continue; + } + + if ((string) ($task['id'] ?? '') === $id || (string) ($task['uid'] ?? '') === $id) { + return $this->toObjectEntity(register: $register, schema: $schema, task: $task); + } + } + + return null; + }//end find() + + /** + * {@inheritDoc} + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/limit/offset). + * @param array $config The object-source config block. + * + * @return ObjectEntity[] The matching virtual objects. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.1 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $limit = ($query['limit'] ?? 200); + $offset = ($query['offset'] ?? 0); + $status = ($query['filters']['status'] ?? null); + + $objects = []; + foreach ($this->readTasks(config: $config, limit: (int) $limit, offset: (int) $offset, status: $status) as $task) { + if ($this->matchesScope(task: $task, register: $register, schema: $schema, config: $config) === false) { + continue; + } + + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, task: $task); + } + + return $objects; + }//end findAll() + + /** + * Whether a VTODO is in scope for the bound schema. + * + * By default a sourced schema only surfaces VTODOs whose X-OPENREGISTER link + * points at the bound register AND schema, so each projection shows only its + * own tasks (not every VTODO the user owns). Config escape hatches: + * - `unscoped: true` — surface all of the user's VTODOs (no link filter); + * - `register` (int) / `schema` (int) — scope to a different register/schema + * than the bound one; + * - `schemas` (int[]) — scope to VTODOs linked to ANY of these schema ids. + * + * @param array $task The VTODO task array. + * @param Register $register The bound register. + * @param Schema $schema The bound schema. + * @param array $config The object-source config block. + * + * @return bool True when the task is in scope. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.1 + */ + private function matchesScope(array $task, Register $register, Schema $schema, array $config): bool + { + if (($config['unscoped'] ?? false) === true) { + return true; + } + + $wantRegister = (int) ($config['register'] ?? $register->getId()); + if ((int) ($task['registerId'] ?? 0) !== $wantRegister) { + return false; + } + + $wantSchemas = $config['schemas'] ?? null; + if (is_array($wantSchemas) === true && empty($wantSchemas) === false) { + return in_array((int) ($task['schemaId'] ?? 0), array_map('intval', $wantSchemas), true); + } + + $wantSchema = (int) ($config['schema'] ?? $schema->getId()); + return ((int) ($task['schemaId'] ?? 0) === $wantSchema); + }//end matchesScope() + + /** + * {@inheritDoc} + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters). + * @param array $config The object-source config block. + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.1 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Read VTODO task arrays for the acting user, failing closed to an empty list. + * + * @param array $config The object-source config block. + * @param int $limit Maximum tasks to read. + * @param int $offset Tasks to skip. + * @param string|null $status Optional status filter. + * + * @return array> The task arrays. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for calendar/collection selection. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.1 + */ + private function readTasks(array $config, int $limit, int $offset, ?string $status=null): array + { + try { + $result = $this->taskService->getAllUserTasks(status: $status, limit: $limit, offset: $offset); + return array_values($result['results']); + } catch (Throwable $e) { + $this->logger->warning( + '[ObjectSource:caldav-vtodo] could not read VTODOs: '.$e->getMessage() + ); + return []; + } + }//end readTasks() + + /** + * Map a VTODO task array onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $task The TaskService VTODO array. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/changes/object-source-providers/tasks.md#task-5.1 + */ + private function toObjectEntity(Register $register, Schema $schema, array $task): ObjectEntity + { + $uuid = (string) ($task['uid'] ?? $task['id'] ?? ''); + + $data = [ + 'title' => ($task['summary'] ?? ''), + 'description' => ($task['description'] ?? ''), + 'status' => ($task['status'] ?? ''), + 'dueDate' => ($task['due'] ?? null), + 'completed' => ($task['completed'] ?? null), + 'priority' => ($task['priority'] ?? null), + ]; + + // Merge non-core schema fields round-tripped via X-OPENREGISTER-DATA + // (e.g. assignee), so the projection is faithful to the bound schema. + if (empty($task['fields']) === false && is_array($task['fields']) === true) { + $data = array_merge($data, $task['fields']); + } + + $entity = new ObjectEntity(); + $entity->setUuid($uuid); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/CalendarEventObjectSourceProvider.php b/lib/Service/ObjectSource/CalendarEventObjectSourceProvider.php new file mode 100644 index 0000000000..d4584b6307 --- /dev/null +++ b/lib/Service/ObjectSource/CalendarEventObjectSourceProvider.php @@ -0,0 +1,222 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Service\CalendarEventService; +use OCP\App\IAppManager; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by CalDAV VEVENTs. + */ +class CalendarEventObjectSourceProvider implements ObjectSourceProvider +{ + /** + * Constructor. + * + * @param CalendarEventService $calendarEventService The CalDAV VEVENT read/list service. + * @param IAppManager $appManager App availability checks. + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly CalendarEventService $calendarEventService, + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function getId(): string + { + return 'calendar-event-source'; + }//end getId() + + /** + * {@inheritDoc} + * + * Enabled when CalDAV is available. VEVENTs live in the user's CalDAV + * calendars served by the CORE `dav` app — the `calendar` app is only an + * optional UI, so this checks `dav` (with `calendar` as a positive signal too). + * + * @return bool True when CalDAV VEVENT reads are available. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function isEnabled(): bool + { + try { + return ($this->appManager->isInstalled('dav') === true + || $this->appManager->isInstalled('calendar') === true); + } catch (Throwable $e) { + return false; + } + }//end isEnabled() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The event UID (or calendar-object uri). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + foreach ($this->readEvents(limit: 1000, offset: 0) as $event) { + if ((string) ($event['uid'] ?? '') === $id || (string) ($event['id'] ?? '') === $id) { + return $this->toObjectEntity(register: $register, schema: $schema, event: $event); + } + } + + return null; + }//end find() + + /** + * {@inheritDoc} + * + * Honours `limit` and `offset`; the event set is scoped by CalendarEventService + * to the acting user's own calendars. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (limit/offset). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity[] The matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $limit = (int) ($query['limit'] ?? 200); + $offset = (int) ($query['offset'] ?? 0); + + $objects = []; + foreach ($this->readEvents(limit: $limit, offset: $offset) as $event) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, event: $event); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters). + * @param array $config The object-source config block (unused). + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Read VEVENT arrays for the acting user, failing closed to an empty list. + * + * @param int $limit Maximum events to read. + * @param int $offset Events to skip. + * + * @return array> The event arrays. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function readEvents(int $limit, int $offset): array + { + try { + $result = $this->calendarEventService->getAllUserEvents(limit: $limit, offset: $offset); + return array_values($result['results']); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:calendar-event-source] could not read VEVENTs: '.$e->getMessage()); + return []; + } + }//end readEvents() + + /** + * Map a VEVENT array onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $event The CalendarEventService VEVENT array. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function toObjectEntity(Register $register, Schema $schema, array $event): ObjectEntity + { + $uuid = (string) ($event['uid'] ?? $event['id'] ?? ''); + + $data = [ + 'id' => $uuid, + 'summary' => (string) ($event['summary'] ?? ''), + 'startDate' => ($event['dtstart'] ?? null), + 'endDate' => ($event['dtend'] ?? null), + 'location' => ($event['location'] ?? null), + ]; + + $entity = new ObjectEntity(); + $entity->setUuid($uuid); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/ContactsObjectSourceProvider.php b/lib/Service/ObjectSource/ContactsObjectSourceProvider.php new file mode 100644 index 0000000000..15f5b73b4f --- /dev/null +++ b/lib/Service/ObjectSource/ContactsObjectSourceProvider.php @@ -0,0 +1,280 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\App\IAppManager; +use OCP\Contacts\IManager; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by the Nextcloud address books. + */ +class ContactsObjectSourceProvider implements ObjectSourceProvider +{ + + /** + * NC app id whose install-state gates this provider. + * + * @var string + */ + private const REQUIRED_APP = 'contacts'; + + /** + * Searched vCard properties; an empty pattern LIKE-matches every contact. + * + * @var array + */ + private const SEARCH_PROPERTIES = ['FN', 'EMAIL', 'ORG', 'UID']; + + /** + * Constructor. + * + * @param IManager $contactsManager Nextcloud contacts (address book) manager. + * @param IAppManager $appManager App availability checks. + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly IManager $contactsManager, + private readonly IAppManager $appManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function getId(): string + { + return 'contacts-source'; + }//end getId() + + /** + * {@inheritDoc} + * + * Gated on the Contacts app: when it is not installed the bound schema + * degrades to an empty list rather than erroring. + * + * @return bool True when the Contacts app is installed. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function isEnabled(): bool + { + try { + return $this->appManager->isInstalled(self::REQUIRED_APP); + } catch (Throwable $e) { + return false; + } + }//end isEnabled() + + /** + * {@inheritDoc} + * + * MUST return null when the contact is absent OR the acting user may not read + * it, so the two cases are indistinguishable (no enumeration oracle). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The contact UID. + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + foreach ($this->readContacts(search: $id) as $contact) { + if ((string) ($contact['UID'] ?? '') === $id) { + return $this->toObjectEntity(register: $register, schema: $schema, contact: $contact); + } + } + + return null; + }//end find() + + /** + * {@inheritDoc} + * + * Honours `filters.search`/`_search`, `limit` and `offset`. The result is + * scoped by IManager to the acting user's readable address books. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search/limit/offset). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity[] The matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $search = (string) ($query['filters']['search'] ?? $query['_search'] ?? $query['search'] ?? ''); + $limit = (int) ($query['limit'] ?? 200); + $offset = (int) ($query['offset'] ?? 0); + + $contacts = $this->readContacts(search: $search); + $contacts = array_slice($contacts, $offset, $limit); + + $objects = []; + foreach ($contacts as $contact) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, contact: $contact); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search). + * @param array $config The object-source config block (unused). + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Read the contacts visible to the acting user, failing closed to an empty list. + * + * System address book entries (the user directory) are filtered out so this + * projection does not duplicate `nc-user`. + * + * @param string $search Optional search term ('' LIKE-matches every contact). + * + * @return array> The vCard-derived contact arrays. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function readContacts(string $search): array + { + try { + $contacts = $this->contactsManager->search($search, self::SEARCH_PROPERTIES); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:contacts-source] could not list contacts: '.$e->getMessage()); + return []; + } + + return array_values( + array_filter( + $contacts, + static function ($contact) { + return is_array($contact) === true + && ($contact['isLocalSystemBook'] ?? false) !== true + && ($contact['UID'] ?? '') !== ''; + } + ) + ); + }//end readContacts() + + /** + * Normalise a vCard EMAIL value (string or array) to a single address. + * + * @param mixed $email The raw EMAIL value from IManager. + * + * @return string The first email address, or an empty string. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function firstEmail(mixed $email): string + { + if (is_array($email) === true) { + $first = reset($email); + if (is_string($first) === true) { + return $first; + } + + return ''; + } + + if (is_string($email) === true) { + return $email; + } + + return ''; + }//end firstEmail() + + /** + * Map a Nextcloud contact array onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $contact The IManager contact array. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function toObjectEntity(Register $register, Schema $schema, array $contact): ObjectEntity + { + $uid = (string) ($contact['UID'] ?? ''); + + $data = [ + 'id' => $uid, + 'fullName' => (string) ($contact['FN'] ?? ''), + 'email' => $this->firstEmail(email: ($contact['EMAIL'] ?? '')), + 'org' => (string) ($contact['ORG'] ?? ''), + ]; + + $entity = new ObjectEntity(); + $entity->setUuid($uid); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/DbalObjectSourceException.php b/lib/Service/ObjectSource/DbalObjectSourceException.php new file mode 100644 index 0000000000..3a5c73c810 --- /dev/null +++ b/lib/Service/ObjectSource/DbalObjectSourceException.php @@ -0,0 +1,70 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use RuntimeException; +use Throwable; + +/** + * Read failure carrying a 502/503 HTTP status for the object-source dispatch. + */ +class DbalObjectSourceException extends RuntimeException +{ + + /** + * The HTTP status this failure should surface as (502 or 503). + * + * @var integer + */ + private int $statusCode; + + /** + * Constructor. + * + * @param string $message A non-sensitive message (never a secret). + * @param int $statusCode The HTTP status (502 upstream error / 503 unreachable). + * @param Throwable|null $previous The underlying DBAL exception, if any. + * + * @return void + */ + public function __construct(string $message, int $statusCode=503, ?Throwable $previous=null) + { + parent::__construct(message: $message, code: 0, previous: $previous); + $this->statusCode = $statusCode; + }//end __construct() + + /** + * The HTTP status this failure should surface as. + * + * @return int The status code (502 or 503). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function getStatusCode(): int + { + return $this->statusCode; + }//end getStatusCode() +}//end class diff --git a/lib/Service/ObjectSource/DbalObjectSourceProvider.php b/lib/Service/ObjectSource/DbalObjectSourceProvider.php new file mode 100644 index 0000000000..ff79f50b3b --- /dev/null +++ b/lib/Service/ObjectSource/DbalObjectSourceProvider.php @@ -0,0 +1,1145 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Exception as DbalException; +use Doctrine\DBAL\Exception\DriverException; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Query\QueryBuilder; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\Source; +use OCA\OpenRegister\Db\SourceMapper; +use OCA\OpenRegister\Service\Dbal\DbalConnectionException; +use OCA\OpenRegister\Service\Dbal\DbalConnectionFactory; +use OCA\OpenRegister\Service\Dbal\DatabaseIntrospectionService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by an external SQL database. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) The provider owns the whole + * OR-query → parameterised-SQL translation (filters, search, sort, paging, id + * shapes, allowlisting, error semantics) as one cohesive design-D4 unit. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Bridges the DBAL query stack + * and the OR entity/mapper seams the design names explicitly. + */ +class DbalObjectSourceProvider implements WritableObjectSourceProvider +{ + /** + * Hard cap on rows returned by a single findAll(), regardless of the query. + * + * @var int + */ + private const MAX_RESULTS = 1000; + + /** + * Default page size when the query does not request a limit. + * + * @var int + */ + private const DEFAULT_LIMIT = 200; + + /** + * Constructor. + * + * @param SourceMapper $sourceMapper Loads the backing database source. + * @param DbalConnectionFactory $connectionFactory Opens the read-only DBAL connection. + * @param LoggerInterface $logger Secret-free diagnostics. + * + * @return void + */ + public function __construct( + private readonly SourceMapper $sourceMapper, + private readonly DbalConnectionFactory $connectionFactory, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function getId(): string + { + return DatabaseIntrospectionService::PROVIDER_ID; + }//end getId() + + /** + * {@inheritDoc} + * + * Enabled when Doctrine DBAL is present and at least one supported driver + * extension is loaded. Per-source driver availability is re-checked at query + * time so a schema bound to an absent driver degrades to an empty list. + * + * @return bool True when the provider can serve at least one driver. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function isEnabled(): bool + { + if (class_exists(\Doctrine\DBAL\DriverManager::class) === false) { + return false; + } + + foreach (DbalConnectionFactory::SUPPORTED_DRIVERS as $driver) { + if ($this->connectionFactory->isDriverAvailable(driver: $driver) === true) { + return true; + } + } + + return false; + }//end isEnabled() + + /** + * {@inheritDoc} + * + * Returns null when the object is absent, the schema is list-only (no id), or + * the driver extension is missing — indistinguishably (no enumeration oracle). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The object id / source key. + * @param array $config The object-source `config` block. + * + * @return ObjectEntity|null The virtual object, or null when absent/denied/list-only. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + $idColumns = $this->idColumns(config: $config); + if ($idColumns === []) { + // List-only schema (no primary key): no stable object identity. + return null; + } + + $context = $this->resolveContext(config: $config, schema: $schema); + if ($context === null) { + return null; + } + + [$source, $columns] = $context; + + try { + $connection = $this->connect(source: $source); + } catch (DbalConnectionException $e) { + $this->logger->warning('[ObjectSource:dbal-source] connection unavailable for find: '.$e->getMessage()); + return null; + } + + try { + $qb = $this->baseSelect(connection: $connection, table: $this->table(config: $config), columns: $columns); + $this->applyIdPredicate(qb: $qb, connection: $connection, idColumns: $idColumns, id: $id); + $qb->setMaxResults(1); + + $row = $qb->executeQuery()->fetchAssociative(); + } catch (DbalException $e) { + $this->logger->warning('[ObjectSource:dbal-source] query failed for find: '.$e->getMessage()); + throw new DbalObjectSourceException('The external database returned an error.', 502, $e); + } + + if ($row === false) { + return null; + } + + return $this->toObjectEntity(register: $register, schema: $schema, row: $row, config: $config); + }//end find() + + /** + * {@inheritDoc} + * + * Applies filters/`_search`/sort as bound parameters and limit/offset in SQL. + * A missing driver extension degrades to an empty list; a connection/query + * error surfaces a {@see DbalObjectSourceException} (503/502), never a 500. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search/sort/limit/offset). + * @param array $config The object-source `config` block. + * + * @return ObjectEntity[] The matching virtual objects (possibly empty). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $context = $this->resolveContext(config: $config, schema: $schema); + if ($context === null) { + return []; + } + + [$source, $columns] = $context; + + try { + $connection = $this->connect(source: $source); + } catch (DbalConnectionException $e) { + if ($this->driverMissing(source: $source) === true) { + $this->logger->warning('[ObjectSource:dbal-source] driver unavailable — returning empty list: '.$e->getMessage()); + return []; + } + + throw new DbalObjectSourceException('The external database is unreachable.', 503, $e); + } + + try { + $qb = $this->baseSelect(connection: $connection, table: $this->table(config: $config), columns: $columns); + $this->applyFilters(qb: $qb, connection: $connection, query: $query, columns: $columns, config: $config); + $this->applySort(qb: $qb, connection: $connection, query: $query, columns: $columns); + + $limit = $this->limit(query: $query); + $offset = max(0, (int) ($query['offset'] ?? 0)); + $qb->setMaxResults($limit); + $qb->setFirstResult($offset); + + $rows = $qb->executeQuery()->fetchAllAssociative(); + } catch (DbalException $e) { + $this->logger->warning('[ObjectSource:dbal-source] query failed for findAll: '.$e->getMessage()); + throw new DbalObjectSourceException('The external database returned an error.', 502, $e); + }//end try + + $objects = []; + foreach ($rows as $row) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, row: $row, config: $config); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * Issues `SELECT COUNT(*)` with the same WHERE predicate as findAll(), so the + * pagination dispatch can report a true total. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search). + * @param array $config The object-source `config` block. + * + * @return int The number of matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $register kept for interface parity. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + $context = $this->resolveContext(config: $config, schema: $schema); + if ($context === null) { + return 0; + } + + [$source, $columns] = $context; + + try { + $connection = $this->connect(source: $source); + } catch (DbalConnectionException $e) { + if ($this->driverMissing(source: $source) === true) { + return 0; + } + + throw new DbalObjectSourceException('The external database is unreachable.', 503, $e); + } + + try { + $qb = $connection->createQueryBuilder(); + $qb->select('COUNT(*)')->from($this->quote(connection: $connection, identifier: $this->table(config: $config))); + $this->applyFilters(qb: $qb, connection: $connection, query: $query, columns: $columns, config: $config); + + $total = $qb->executeQuery()->fetchOne(); + } catch (DbalException $e) { + $this->logger->warning('[ObjectSource:dbal-source] query failed for count: '.$e->getMessage()); + throw new DbalObjectSourceException('The external database returned an error.', 502, $e); + } + + return (int) $total; + }//end count() + + /** + * Resolve the backing source and the allowlisted scalar column set. + * + * @param array $config The object-source config block. + * @param Schema $schema The sourced schema. + * + * @return array{0: Source, 1: array}|null [source, columns] or null when unresolvable. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function resolveContext(array $config, Schema $schema): ?array + { + $source = $this->resolveSource(config: $config); + if ($source === null) { + $this->logger->warning('[ObjectSource:dbal-source] no source resolved for schema '.(string) $schema->getSlug()); + return null; + } + + return [$source, $this->scalarColumns(schema: $schema)]; + }//end resolveContext() + + /** + * Load the backing database source referenced by the config `sourceId`. + * + * @param array $config The object-source config block. + * + * @return Source|null The source, or null when not found / not a database source. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function resolveSource(array $config): ?Source + { + $sourceId = (string) ($config['sourceId'] ?? ''); + if ($sourceId === '') { + return null; + } + + try { + $source = null; + if (ctype_digit($sourceId) === true) { + $source = $this->sourceMapper->find(id: (int) $sourceId); + } + + if ($source === null) { + $matches = $this->sourceMapper->findAll(filters: ['uuid' => $sourceId]); + $source = ($matches[0] ?? null); + } + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:dbal-source] could not load source "'.$sourceId.'": '.$e->getMessage()); + return null; + } + + if ($source === null || (string) $source->getType() !== 'database') { + return null; + } + + return $source; + }//end resolveSource() + + /** + * Open the read-only DBAL connection for a source. + * + * @param Source $source The backing source. + * + * @return Connection The connection. + * + * @throws DbalConnectionException On any connection/credential error (fail closed). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function connect(Source $source): Connection + { + return $this->connectionFactory->getConnection(source: $source); + }//end connect() + + /** + * Whether the source's configured driver extension is absent on this instance. + * + * @param Source $source The backing source. + * + * @return bool True when the driver extension is missing. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function driverMissing(Source $source): bool + { + $config = ($source->getAuthConfig() ?? []); + $driver = (string) ($config['driver'] ?? ''); + return ($this->connectionFactory->isDriverAvailable(driver: $driver) === false); + }//end driverMissing() + + /** + * Build a `SELECT FROM ` query with quoted identifiers. + * + * @param Connection $connection The DBAL connection. + * @param string $table The backing table name. + * @param array $columns The scalar columns to select. + * + * @return QueryBuilder The query builder. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function baseSelect(Connection $connection, string $table, array $columns): QueryBuilder + { + $qb = $connection->createQueryBuilder(); + + $selects = []; + foreach ($columns as $column) { + $selects[] = $this->quote(connection: $connection, identifier: $column); + } + + if ($selects === []) { + $selects = ['*']; + } + + $qb->select(...$selects)->from($this->quote(connection: $connection, identifier: $table)); + + return $qb; + }//end baseSelect() + + /** + * Apply equality filters and a `_search` LIKE as bound parameters. + * + * @param QueryBuilder $qb The query builder (mutated). + * @param Connection $connection The DBAL connection. + * @param array $query The OR query. + * @param array $columns The allowlisted scalar columns. + * @param array $config The object-source config block. + * + * @return void + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function applyFilters(QueryBuilder $qb, Connection $connection, array $query, array $columns, array $config): void + { + $filterable = $this->filterableColumns(columns: $columns, config: $config); + + $filters = ($query['filters'] ?? []); + if (is_array($filters) === true) { + foreach ($filters as $column => $value) { + if (is_string($column) === false || in_array($column, $filterable, true) === false) { + continue; + } + + if (is_array($value) === true || is_object($value) === true) { + continue; + } + + $qb->andWhere( + $this->quote(connection: $connection, identifier: $column).' = '.$qb->createNamedParameter($value) + ); + } + } + + $this->applySearch(qb: $qb, connection: $connection, query: $query, filterable: $filterable); + }//end applyFilters() + + /** + * Apply a `_search` term as a bound LIKE across the filterable columns. + * + * @param QueryBuilder $qb The query builder (mutated). + * @param Connection $connection The DBAL connection. + * @param array $query The OR query. + * @param array $filterable The filterable columns. + * + * @return void + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function applySearch(QueryBuilder $qb, Connection $connection, array $query, array $filterable): void + { + $search = (string) ($query['_search'] ?? $query['search'] ?? ''); + if ($search === '' || $filterable === []) { + return; + } + + // LIKE only exists for text operands: on PostgreSQL `integer LIKE + // text` raises "operator does not exist" (observed live), so every + // column is CAST to the platform's text type first. MySQL spells the + // cast target CHAR; PostgreSQL and SQLite use TEXT. + $castType = 'TEXT'; + if ($connection->getDatabasePlatform() instanceof AbstractMySQLPlatform === true) { + $castType = 'CHAR'; + } + + $like = '%'.$search.'%'; + $ors = []; + $param = $qb->createNamedParameter($like); + foreach ($filterable as $column) { + $quoted = $this->quote(connection: $connection, identifier: $column); + $ors[] = 'CAST('.$quoted.' AS '.$castType.') LIKE '.$param; + } + + $qb->andWhere('('.implode(' OR ', $ors).')'); + }//end applySearch() + + /** + * Apply a validated sort column/direction. + * + * @param QueryBuilder $qb The query builder (mutated). + * @param Connection $connection The DBAL connection. + * @param array $query The OR query. + * @param array $columns The allowlisted scalar columns. + * + * @return void + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function applySort(QueryBuilder $qb, Connection $connection, array $query, array $columns): void + { + $sort = ($query['sort'] ?? $query['_order'] ?? null); + if (is_array($sort) === false || $sort === []) { + return; + } + + foreach ($sort as $column => $direction) { + if (is_string($column) === false || in_array($column, $columns, true) === false) { + continue; + } + + $dir = 'ASC'; + if (strtolower((string) $direction) === 'desc') { + $dir = 'DESC'; + } + + $qb->addOrderBy($this->quote(connection: $connection, identifier: $column), $dir); + } + }//end applySort() + + /** + * Add the WHERE predicate that selects a single object by its id. + * + * @param QueryBuilder $qb The query builder (mutated). + * @param Connection $connection The DBAL connection. + * @param array $idColumns The id column(s) (single or composite). + * @param string $id The object id (composite ids are separator-joined). + * + * @return void + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function applyIdPredicate(QueryBuilder $qb, Connection $connection, array $idColumns, string $id): void + { + if (count($idColumns) === 1) { + $qb->andWhere( + $this->quote(connection: $connection, identifier: $idColumns[0]).' = '.$qb->createNamedParameter($id) + ); + return; + } + + $parts = explode(DatabaseIntrospectionService::COMPOSITE_ID_SEPARATOR, $id); + foreach ($idColumns as $index => $column) { + $part = ($parts[$index] ?? ''); + $qb->andWhere( + $this->quote(connection: $connection, identifier: $column).' = '.$qb->createNamedParameter($part) + ); + } + }//end applyIdPredicate() + + /** + * Map a fetched row onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $row The fetched row. + * @param array $config The object-source config block. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function toObjectEntity(Register $register, Schema $schema, array $row, array $config): ObjectEntity + { + $id = $this->rowId(row: $row, config: $config); + + $data = $row; + if ($id !== null) { + $data['id'] = $id; + } + + $entity = new ObjectEntity(); + if ($id !== null) { + $entity->setUuid($id); + } + + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + + return $entity; + }//end toObjectEntity() + + /** + * Derive the object id from a row per the config id shape. + * + * @param array $row The fetched row. + * @param array $config The object-source config block. + * + * @return string|null The id (single value or separator-joined composite), or null when list-only. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function rowId(array $row, array $config): ?string + { + $idColumns = $this->idColumns(config: $config); + if ($idColumns === []) { + return null; + } + + $parts = []; + foreach ($idColumns as $column) { + $parts[] = (string) ($row[$column] ?? ''); + } + + return implode(DatabaseIntrospectionService::COMPOSITE_ID_SEPARATOR, $parts); + }//end rowId() + + /** + * The ordered id column list from config (single, composite, or empty). + * + * @param array $config The object-source config block. + * + * @return array The id columns (empty for a list-only schema). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function idColumns(array $config): array + { + $idColumn = ($config['idColumn'] ?? null); + if (is_string($idColumn) === true && $idColumn !== '') { + return [$idColumn]; + } + + $idColumns = ($config['idColumns'] ?? null); + if (is_array($idColumns) === true && $idColumns !== []) { + return array_values(array_map('strval', $idColumns)); + } + + return []; + }//end idColumns() + + /** + * The backing table name from config. + * + * @param array $config The object-source config block. + * + * @return string The table name. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function table(array $config): string + { + return (string) ($config['table'] ?? ''); + }//end table() + + /** + * The scalar (non-relation) columns of a schema — the query allowlist. + * + * Relation and inverse properties (arrays, or object properties carrying a + * `$ref`) are not real columns and are excluded from SELECT/WHERE. + * + * @param Schema $schema The sourced schema. + * + * @return array The scalar column names. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function scalarColumns(Schema $schema): array + { + $columns = []; + foreach ($schema->getProperties() as $name => $definition) { + if (is_array($definition) === false) { + continue; + } + + $type = ($definition['type'] ?? 'string'); + if ($type === 'array' || isset($definition['items']) === true) { + continue; + } + + $columns[] = (string) $name; + } + + return $columns; + }//end scalarColumns() + + /** + * The columns that may appear in a WHERE predicate (allowlist minus + * non-filterable columns declared at introspection). + * + * @param array $columns The scalar columns. + * @param array $config The object-source config block. + * + * @return array The filterable columns. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function filterableColumns(array $columns, array $config): array + { + $nonFilterable = ($config['nonFilterable'] ?? []); + if (is_array($nonFilterable) === false || $nonFilterable === []) { + return $columns; + } + + return array_values(array_diff($columns, $nonFilterable)); + }//end filterableColumns() + + /** + * The effective row limit for a findAll(), clamped to the hard cap. + * + * @param array $query The OR query. + * + * @return int The clamped limit. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function limit(array $query): int + { + $limit = (int) ($query['limit'] ?? self::DEFAULT_LIMIT); + if ($limit < 1) { + $limit = self::DEFAULT_LIMIT; + } + + return min($limit, self::MAX_RESULTS); + }//end limit() + + /** + * Quote a SQL identifier through the connection's platform. + * + * @param Connection $connection The DBAL connection. + * @param string $identifier The raw identifier. + * + * @return string The quoted identifier. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function quote(Connection $connection, string $identifier): string + { + return $connection->getDatabasePlatform()->quoteIdentifier($identifier); + }//end quote() + + /** + * {@inheritDoc} + * + * Executes a parameterized INSERT restricted to introspected scalar columns. + * The generated single-column primary key is retrieved via `RETURNING` on + * PostgreSQL and `lastInsertId()` elsewhere; the created row is re-read so + * the response reflects database-applied defaults. No-PK tables accept + * inserts (append-only) and return the payload as the entity body. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $data The validated object data. + * @param array $config The object-source `config` block. + * + * @return ObjectEntity The created virtual object. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function insert(Register $register, Schema $schema, array $data, array $config=[]): ObjectEntity + { + [$source, $connection, $columns] = $this->writeContext(schema: $schema, config: $config, needsId: false); + + $values = $this->writeValues(data: $data, columns: $columns, config: $config); + $idColumns = $this->idColumns(config: $config); + $table = $this->table(config: $config); + + try { + $newId = $this->executeInsert( + connection: $connection, + table: $table, + values: $values, + idColumns: $idColumns + ); + } catch (DbalException $e) { + throw $this->translateWriteError(exception: $e); + } + + if ($newId !== null) { + $created = $this->find(register: $register, schema: $schema, id: $newId, config: $config); + if ($created !== null) { + return $created; + } + } + + // No-PK (append-only) tables — or a re-read miss — echo the written values. + return $this->toObjectEntity(register: $register, schema: $schema, row: $values, config: $config); + }//end insert() + + /** + * {@inheritDoc} + * + * Executes a parameterized UPDATE whose predicate is the (possibly + * composite) primary key reconstructed from the object id. Primary-key + * columns cannot be changed (400). Zero affected rows surfaces as the same + * 404 an absent native object produces. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The object id (external key). + * @param array $data The validated object data. + * @param array $config The object-source `config` block. + * + * @return ObjectEntity The updated virtual object as re-read. + * + * @throws \OCP\AppFramework\Db\DoesNotExistException When no external row matches the id. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function update(Register $register, Schema $schema, string $id, array $data, array $config=[]): ObjectEntity + { + [$source, $connection, $columns] = $this->writeContext(schema: $schema, config: $config, needsId: true); + + $idColumns = $this->idColumns(config: $config); + $values = $this->writeValues(data: $data, columns: $columns, config: $config); + + // Primary-key columns are the row's identity — never updatable. + foreach ($idColumns as $idColumn) { + unset($values[$idColumn]); + } + + if ($values === []) { + throw new DbalWriteException('The update contains no writable columns.', 400); + } + + try { + $qb = $connection->createQueryBuilder()->update($this->quote(connection: $connection, identifier: $this->table(config: $config))); + foreach ($values as $column => $value) { + $qb->set($this->quote(connection: $connection, identifier: $column), $qb->createNamedParameter($value)); + } + + $this->applyWritePredicate(qb: $qb, connection: $connection, idColumns: $idColumns, id: $id); + $affected = (int) $qb->executeStatement(); + } catch (DbalException $e) { + throw $this->translateWriteError(exception: $e); + } + + if ($affected === 0) { + throw new \OCP\AppFramework\Db\DoesNotExistException('No external row matches id '.$id); + } + + $updated = $this->find(register: $register, schema: $schema, id: $id, config: $config); + if ($updated !== null) { + return $updated; + } + + return $this->toObjectEntity(register: $register, schema: $schema, row: array_merge($values, ['id' => $id]), config: $config); + }//end update() + + /** + * {@inheritDoc} + * + * Executes a parameterized DELETE (hard delete — the external system has no + * OpenRegister soft-delete) with the primary-key predicate. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The object id (external key). + * @param array $config The object-source `config` block. + * + * @return bool True when a row was deleted; false when no row matched. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function remove(Register $register, Schema $schema, string $id, array $config=[]): bool + { + [$source, $connection] = $this->writeContext(schema: $schema, config: $config, needsId: true); + + $idColumns = $this->idColumns(config: $config); + + try { + $qb = $connection->createQueryBuilder()->delete($this->quote(connection: $connection, identifier: $this->table(config: $config))); + $this->applyWritePredicate(qb: $qb, connection: $connection, idColumns: $idColumns, id: $id); + $affected = (int) $qb->executeStatement(); + } catch (DbalException $e) { + throw $this->translateWriteError(exception: $e); + } + + return ($affected > 0); + }//end remove() + + /** + * Resolve and verify the write context for a schema (design D2, fail closed). + * + * Re-verifies AT WRITE TIME that the backing source exists, is a database + * source, and currently has `authConfig.writable === true` — a stale + * schema annotation alone can never authorize a write. Views never accept + * writes; tables without a primary key reject id-addressed writes. + * + * @param Schema $schema The sourced schema. + * @param array $config The object-source `config` block. + * @param bool $needsId Whether the operation needs an id predicate (update/delete). + * + * @return array{0: Source, 1: Connection, 2: array} [source, connection, scalar columns]. + * + * @throws \RuntimeException When the source is not writable (read-only rejection). + * @throws DbalObjectSourceException When the external database is unreachable. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function writeContext(Schema $schema, array $config, bool $needsId): array + { + $readOnlyError = sprintf( + 'Schema "%s" is a read-only projection of object-source provider "%s"; writes are not allowed.', + (string) $schema->getSlug(), + $this->getId() + ); + + if (($config['isView'] ?? false) === true) { + throw new \RuntimeException($readOnlyError); + } + + if ($needsId === true && $this->idColumns(config: $config) === []) { + throw new DbalWriteException('This external table has no primary key; rows cannot be updated or deleted.', 400); + } + + $source = $this->resolveSource(config: $config); + if ($source === null) { + // Fail closed: an unresolvable source can never authorize a write. + throw new \RuntimeException($readOnlyError); + } + + $authConfig = ($source->getAuthConfig() ?? []); + if (($authConfig['writable'] ?? false) !== true) { + throw new \RuntimeException($readOnlyError); + } + + try { + $connection = $this->connect(source: $source); + } catch (DbalConnectionException $e) { + throw new DbalObjectSourceException('The external database is unreachable.', 503, $e); + } + + return [$source, $connection, $this->scalarColumns(schema: $schema)]; + }//end writeContext() + + /** + * Restrict a write payload to introspected scalar columns. + * + * The synthetic `id` key and `@self` metadata are stripped; any remaining + * property that is not an introspected column is rejected — never silently + * dropped (consistent with validation strictness). + * + * @param array $data The validated object data. + * @param array $columns The scalar column allowlist. + * @param array $config The object-source `config` block. + * + * @return array The column => value map to write. + * + * @throws DbalWriteException When the payload names a non-column property (400). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function writeValues(array $data, array $columns, array $config): array + { + unset($data['id'], $data['@self']); + + $values = []; + foreach ($data as $property => $value) { + $property = (string) $property; + if (in_array($property, $columns, true) === false) { + throw new DbalWriteException( + sprintf('Property "%s" is not a column of the external table.', $property), + 400 + ); + } + + if ($value !== null && is_scalar($value) === false) { + throw new DbalWriteException( + sprintf('Property "%s" must be a scalar value.', $property), + 400 + ); + } + + $values[$property] = $value; + } + + return $values; + }//end writeValues() + + /** + * Execute the INSERT and return the new object id (or null for no-PK tables). + * + * @param Connection $connection The DBAL connection. + * @param string $table The unquoted table name. + * @param array $values The column => value map. + * @param array $idColumns The id columns (empty for no-PK). + * + * @return string|null The new object id, or null when the table has no primary key. + * + * @throws DbalException On any driver error (translated by the caller). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function executeInsert(Connection $connection, string $table, array $values, array $idColumns): ?string + { + // Composite key: every part must be supplied; the id is derived from the payload. + if (count($idColumns) > 1) { + $parts = []; + foreach ($idColumns as $idColumn) { + if (isset($values[$idColumn]) === false) { + throw new DbalWriteException( + sprintf('Composite-key inserts must supply every key column; "%s" is missing.', $idColumn), + 400 + ); + } + + $parts[] = (string) $values[$idColumn]; + } + + $connection->insert( + $this->quote(connection: $connection, identifier: $table), + $this->quoteColumns(connection: $connection, values: $values) + ); + return implode(DatabaseIntrospectionService::COMPOSITE_ID_SEPARATOR, $parts); + } + + $idColumn = ($idColumns[0] ?? null); + + // Caller-supplied primary key wins (no generation round-trip needed). + if ($idColumn !== null && isset($values[$idColumn]) === true) { + $connection->insert( + $this->quote(connection: $connection, identifier: $table), + $this->quoteColumns(connection: $connection, values: $values) + ); + return (string) $values[$idColumn]; + } + + // PostgreSQL: RETURNING is the only reliable generated-key retrieval. + if ($idColumn !== null && $connection->getDatabasePlatform() instanceof PostgreSQLPlatform === true) { + $columnsSql = []; + $params = []; + foreach ($values as $column => $value) { + $columnsSql[] = $this->quote(connection: $connection, identifier: $column); + $params[] = $value; + } + + $sql = sprintf( + 'INSERT INTO %s (%s) VALUES (%s) RETURNING %s', + $this->quote(connection: $connection, identifier: $table), + implode(', ', $columnsSql), + implode(', ', array_fill(0, count($params), '?')), + $this->quote(connection: $connection, identifier: $idColumn) + ); + + $newId = $connection->fetchOne($sql, $params); + return (string) $newId; + } + + $connection->insert($this->quote(connection: $connection, identifier: $table), $this->quoteColumns(connection: $connection, values: $values)); + if ($idColumn === null) { + return null; + } + + return (string) $connection->lastInsertId(); + }//end executeInsert() + + /** + * Quote the column keys of a value map for Connection::insert(). + * + * @param Connection $connection The DBAL connection. + * @param array $values The column => value map. + * + * @return array The map with platform-quoted column keys. + * + * @spec exclude Private quoting helper; behaviour covered by the insert/update tests. + */ + private function quoteColumns(Connection $connection, array $values): array + { + $quoted = []; + foreach ($values as $column => $value) { + $quoted[$this->quote(connection: $connection, identifier: (string) $column)] = $value; + } + + return $quoted; + }//end quoteColumns() + + /** + * Apply the primary-key predicate to a write query builder. + * + * @param QueryBuilder $qb The query builder (mutated). + * @param Connection $connection The DBAL connection. + * @param array $idColumns The id columns. + * @param string $id The object id (possibly composite-joined). + * + * @return void + * + * @throws DbalWriteException When a composite id has the wrong number of parts (400). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function applyWritePredicate(QueryBuilder $qb, Connection $connection, array $idColumns, string $id): void + { + if (count($idColumns) === 1) { + $qb->andWhere( + $this->quote(connection: $connection, identifier: $idColumns[0]).' = '.$qb->createNamedParameter($id) + ); + return; + } + + $parts = explode(DatabaseIntrospectionService::COMPOSITE_ID_SEPARATOR, $id); + if (count($parts) !== count($idColumns)) { + throw new DbalWriteException('The object id does not match the table\'s composite key shape.', 400); + } + + foreach ($idColumns as $index => $column) { + $qb->andWhere( + $this->quote(connection: $connection, identifier: $column).' = '.$qb->createNamedParameter($parts[$index]) + ); + } + }//end applyWritePredicate() + + /** + * Translate a DBAL write error into a sanitized, status-carrying exception. + * + * @param DbalException $exception The driver exception. + * + * @return DbalWriteException The sanitized exception (never contains SQL or secrets). + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + private function translateWriteError(DbalException $exception): DbalWriteException + { + // Doctrine's typed constraint exceptions are platform-independent + // (SQLite reports the generic SQLSTATE 23000 for every constraint + // class); prefer them over raw SQLSTATE inspection. + if ($exception instanceof \Doctrine\DBAL\Exception\UniqueConstraintViolationException === true) { + return new DbalWriteException('A unique constraint on the external table was violated.', 409, $exception); + } + + if ($exception instanceof \Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException === true) { + return new DbalWriteException('A foreign-key constraint on the external table was violated.', 409, $exception); + } + + if ($exception instanceof \Doctrine\DBAL\Exception\NotNullConstraintViolationException === true) { + return new DbalWriteException('A required (not-null) column on the external table was not provided.', 422, $exception); + } + + if ($exception instanceof \Doctrine\DBAL\Exception\ConstraintViolationException === true) { + return new DbalWriteException('A constraint on the external table was violated.', 422, $exception); + } + + $sqlState = ''; + if ($exception instanceof DriverException === true) { + $sqlState = (string) ($exception->getSQLState() ?? ''); + } + + return DbalWriteException::fromSqlState(sqlState: $sqlState, previous: $exception); + }//end translateWriteError() +}//end class diff --git a/lib/Service/ObjectSource/DbalWriteException.php b/lib/Service/ObjectSource/DbalWriteException.php new file mode 100644 index 0000000000..f01baff67f --- /dev/null +++ b/lib/Service/ObjectSource/DbalWriteException.php @@ -0,0 +1,103 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use RuntimeException; +use Throwable; + +/** + * Sanitized, status-carrying write failure against an external database. + */ +class DbalWriteException extends RuntimeException +{ + + /** + * The HTTP status this failure should surface as. + * + * @var integer + */ + private int $statusCode; + + /** + * Constructor. + * + * @param string $message A sanitized, client-safe message (never a secret or SQL). + * @param int $statusCode The HTTP status (400/409/422/502/503). + * @param Throwable|null $previous The underlying DBAL exception, if any. + * + * @return void + */ + public function __construct(string $message, int $statusCode=422, ?Throwable $previous=null) + { + parent::__construct(message: $message, code: 0, previous: $previous); + $this->statusCode = $statusCode; + }//end __construct() + + /** + * The HTTP status this failure should surface as. + * + * @return int The status code. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function getStatusCode(): int + { + return $this->statusCode; + }//end getStatusCode() + + /** + * Translate a driver SQLSTATE into a sanitized write exception. + * + * @param string $sqlState The five-character SQLSTATE (empty when unknown). + * @param Throwable|null $previous The underlying DBAL exception. + * + * @return self The mapped exception. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public static function fromSqlState(string $sqlState, ?Throwable $previous=null): self + { + switch ($sqlState) { + case '23505': + return new self('A unique constraint on the external table was violated.', 409, $previous); + case '23503': + return new self('A foreign-key constraint on the external table was violated.', 409, $previous); + case '23502': + return new self('A required (not-null) column on the external table was not provided.', 422, $previous); + case '23514': + return new self('A check constraint on the external table was violated.', 422, $previous); + default: + if (str_starts_with($sqlState, '22') === true) { + return new self('A value did not match the external column type.', 422, $previous); + } + return new self('The external database rejected the write.', 502, $previous); + }//end switch + }//end fromSqlState() +}//end class diff --git a/lib/Service/ObjectSource/DeckObjectSourceProvider.php b/lib/Service/ObjectSource/DeckObjectSourceProvider.php new file mode 100644 index 0000000000..a3f276f597 --- /dev/null +++ b/lib/Service/ObjectSource/DeckObjectSourceProvider.php @@ -0,0 +1,415 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\App\IAppManager; +use OCP\IUserSession; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by the Nextcloud Deck app. + */ +class DeckObjectSourceProvider implements ObjectSourceProvider +{ + + /** + * NC app id whose install-state gates this provider. + * + * @var string + */ + private const REQUIRED_APP = 'deck'; + + /** + * Deck's per-user board service (resolved dynamically — Deck's namespace). + * + * @var string + */ + private const BOARD_SERVICE = 'OCA\\Deck\\Service\\BoardService'; + + /** + * Deck's per-user stack service (resolved dynamically — Deck's namespace). + * + * @var string + */ + private const STACK_SERVICE = 'OCA\\Deck\\Service\\StackService'; + + /** + * Constructor. + * + * @param IAppManager $appManager App availability checks. + * @param IUserSession $userSession The acting-user session. + * @param ContainerInterface $container Server container (lazy Deck-service lookup). + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly IUserSession $userSession, + private readonly ContainerInterface $container, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function getId(): string + { + return 'deck-source'; + }//end getId() + + /** + * {@inheritDoc} + * + * Gated on the Deck app: when it is not installed the bound schema degrades + * to an empty list rather than erroring. + * + * @return bool True when the Deck app is installed. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function isEnabled(): bool + { + try { + return $this->appManager->isInstalled(self::REQUIRED_APP); + } catch (Throwable $e) { + return false; + } + }//end isEnabled() + + /** + * {@inheritDoc} + * + * MUST return null when the card is absent OR the acting user may not read + * it, so the two cases are indistinguishable (no enumeration oracle). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The Deck card id. + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + foreach ($this->readCards() as $card) { + if ((string) ($card['id'] ?? '') === $id) { + return $this->toObjectEntity(register: $register, schema: $schema, card: $card); + } + } + + return null; + }//end find() + + /** + * {@inheritDoc} + * + * Honours `limit` and `offset`. The result is scoped by Deck's own + * per-user services to the acting user's readable boards. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (limit/offset). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity[] The matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $limit = (int) ($query['limit'] ?? 200); + $offset = (int) ($query['offset'] ?? 0); + + $cards = array_slice($this->readCards(), $offset, $limit); + + $objects = []; + foreach ($cards as $card) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, card: $card); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (limit/offset). + * @param array $config The object-source config block (unused). + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Read the Deck cards visible to the acting user, failing closed to an empty + * list. + * + * Walks the user's boards (Deck `BoardService::findAll()`) and, for each, + * its stacks-with-cards (Deck `StackService::findAll()`); every access is + * guarded so an absent Deck app or a per-card read failure degrades to an + * empty/partial list rather than erroring. + * + * @return array> The normalised card arrays. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function readCards(): array + { + if ($this->userSession->getUser() === null) { + return []; + } + + $boardService = $this->resolveService(class: self::BOARD_SERVICE); + $stackService = $this->resolveService(class: self::STACK_SERVICE); + if ($boardService === null || $stackService === null) { + return []; + } + + try { + $boards = $boardService->findAll(); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:deck-source] could not list boards: '.$e->getMessage()); + return []; + } + + $cards = []; + foreach ($boards as $board) { + $boardId = $this->intGetter(entity: $board, getter: 'getId'); + if ($boardId === null) { + continue; + } + + try { + $stacks = $stackService->findAll($boardId); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:deck-source] could not list stacks for board '.$boardId.': '.$e->getMessage()); + continue; + } + + foreach ($stacks as $stack) { + $cards = array_merge($cards, $this->cardsFromStack(stack: $stack, boardId: $boardId)); + } + } + + return $cards; + }//end readCards() + + /** + * Extract the normalised card arrays from a single Deck stack. + * + * @param object $stack The Deck stack entity (carries getCards()). + * @param int $boardId The parent board id (Deck cards expose only stackId). + * + * @return array> The normalised card arrays. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function cardsFromStack(object $stack, int $boardId): array + { + try { + $stackCards = $stack->getCards(); + } catch (Throwable $e) { + return []; + } + + if (is_array($stackCards) === false) { + return []; + } + + $result = []; + foreach ($stackCards as $card) { + if (is_object($card) === false) { + continue; + } + + $result[] = [ + 'id' => (string) ($this->intGetter(entity: $card, getter: 'getId') ?? ''), + 'title' => $this->stringGetter(entity: $card, getter: 'getTitle'), + 'description' => $this->stringGetter(entity: $card, getter: 'getDescription'), + 'stackId' => $this->intGetter(entity: $card, getter: 'getStackId'), + 'boardId' => $boardId, + 'duedate' => $this->dueDate(card: $card), + ]; + } + + return $result; + }//end cardsFromStack() + + /** + * Resolve one of Deck's service classes from the container, or null when + * Deck is not installed / its service cannot be resolved. + * + * @param string $class The fully-qualified Deck service class name. + * + * @return object|null The resolved service, or null. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function resolveService(string $class): ?object + { + if (class_exists($class) === false) { + return null; + } + + try { + $service = $this->container->get($class); + if (is_object($service) === true) { + return $service; + } + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:deck-source] could not resolve '.$class.': '.$e->getMessage()); + } + + return null; + }//end resolveService() + + /** + * Read an integer getter from a Deck entity (getters resolve via OCP Entity + * magic, so method_exists is unreliable — each call is guarded). + * + * @param object $entity The Deck entity. + * @param string $getter The getter method name. + * + * @return int|null The integer value, or null when unavailable. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function intGetter(object $entity, string $getter): ?int + { + try { + $value = $entity->$getter(); + if ($value === null) { + return null; + } + + return (int) $value; + } catch (Throwable $e) { + return null; + } + }//end intGetter() + + /** + * Read a string getter from a Deck entity, defaulting to '' on failure. + * + * @param object $entity The Deck entity. + * @param string $getter The getter method name. + * + * @return string The string value, or ''. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function stringGetter(object $entity, string $getter): string + { + try { + return (string) $entity->$getter(); + } catch (Throwable $e) { + return ''; + } + }//end stringGetter() + + /** + * Read a Deck card's due date as an ISO-8601 string, or null. + * + * @param object $card The Deck card entity. + * + * @return string|null The ISO-8601 due date, or null. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function dueDate(object $card): ?string + { + try { + $due = $card->getDuedate(); + if ($due instanceof \DateTimeInterface) { + return $due->format(\DateTime::ATOM); + } + } catch (Throwable $e) { + // Getter missing on this Deck version — leave null. + } + + return null; + }//end dueDate() + + /** + * Map a normalised Deck card array onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $card The normalised card array. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function toObjectEntity(Register $register, Schema $schema, array $card): ObjectEntity + { + $id = (string) ($card['id'] ?? ''); + + $entity = new ObjectEntity(); + $entity->setUuid($id); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($card); + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/FederatedObjectSourceProvider.php b/lib/Service/ObjectSource/FederatedObjectSourceProvider.php new file mode 100644 index 0000000000..2dc6698292 --- /dev/null +++ b/lib/Service/ObjectSource/FederatedObjectSourceProvider.php @@ -0,0 +1,219 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\Http\Client\IClientService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by a remote OpenRegister instance. + */ +class FederatedObjectSourceProvider implements ObjectSourceProvider +{ + /** + * Constructor. + * + * @param IClientService $clientService HTTP client factory for remote reads. + * @param LoggerInterface $logger Logger for remote-read failures. + */ + public function __construct( + private readonly IClientService $clientService, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + */ + public function getId(): string + { + return 'federated'; + }//end getId() + + /** + * {@inheritDoc} + * + * Always available — a broken/offline peer degrades to an empty result. + * + * @return bool Always true. + */ + public function isEnabled(): bool + { + return true; + }//end isEnabled() + + /** + * {@inheritDoc} + * + * @param Register $register The local shadow register. + * @param Schema $schema The local shadow schema. + * @param string $id The remote object id/uuid. + * @param array $config Object-source config `{remoteUrl, shareToken}`. + * + * @return ObjectEntity|null The virtual object, or null when absent/unreachable. + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + $base = $this->baseUrl(config: $config); + if ($base === null) { + return null; + } + + try { + $client = $this->clientService->newClient(); + $response = $client->get($base.'/objects/'.rawurlencode($id), ['timeout' => 15]); + $body = json_decode((string) $response->getBody(), true); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:federated] find failed: '.$e->getMessage()); + return null; + } + + if (is_array($body) === false || isset($body['error']) === true) { + return null; + } + + return $this->toObjectEntity(register: $register, schema: $schema, record: $body); + }//end find() + + /** + * {@inheritDoc} + * + * @param Register $register The local shadow register. + * @param Schema $schema The local shadow schema. + * @param array $query Query (limit/offset/search honoured). + * @param array $config Object-source config `{remoteUrl, shareToken}`. + * + * @return ObjectEntity[] The matching virtual objects (possibly empty). + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $base = $this->baseUrl(config: $config); + if ($base === null) { + return []; + } + + $params = [ + '_limit' => (string) ($query['limit'] ?? 50), + '_offset' => (string) ($query['offset'] ?? 0), + ]; + $search = ($query['filters']['search'] ?? $query['_search'] ?? $query['search'] ?? null); + if ($search !== null && $search !== '') { + $params['_search'] = (string) $search; + } + + try { + $client = $this->clientService->newClient(); + $response = $client->get($base.'/objects', ['timeout' => 15, 'query' => $params]); + $body = json_decode((string) $response->getBody(), true); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:federated] findAll failed: '.$e->getMessage()); + return []; + } + + $results = []; + foreach (($body['results'] ?? []) as $record) { + if (is_array($record) === true) { + $results[] = $this->toObjectEntity(register: $register, schema: $schema, record: $record); + } + } + + return $results; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The local shadow register. + * @param Schema $schema The local shadow schema. + * @param array $query Query (search honoured). + * @param array $config Object-source config `{remoteUrl, shareToken}`. + * + * @return int The number of matching virtual objects. + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Build the remote federation base URL from the source config. + * + * @param array $config Object-source config `{remoteUrl, shareToken}`. + * + * @return string|null The base `.../api/federation/{token}` URL, or null when misconfigured. + */ + private function baseUrl(array $config): ?string + { + $remoteUrl = trim((string) ($config['remoteUrl'] ?? '')); + $token = trim((string) ($config['shareToken'] ?? '')); + + if ($remoteUrl === '' || $token === '') { + $this->logger->warning('[ObjectSource:federated] missing remoteUrl/shareToken in source config'); + return null; + } + + return rtrim($remoteUrl, '/').'/apps/openregister/api/federation/'.rawurlencode($token); + }//end baseUrl() + + /** + * Project a remote rendered record onto a non-persisted local ObjectEntity. + * + * @param Register $register The local shadow register. + * @param Schema $schema The local shadow schema. + * @param array $record The remote rendered object (data + `@self`). + * + * @return ObjectEntity The virtual object (never saved). + */ + private function toObjectEntity(Register $register, Schema $schema, array $record): ObjectEntity + { + $self = ($record['@self'] ?? []); + $uuid = (string) ($self['id'] ?? $record['id'] ?? $record['uuid'] ?? ''); + + $data = $record; + unset($data['@self']); + + $entity = new ObjectEntity(); + $entity->setUuid($uuid); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + + if (isset($self['uri']) === true) { + $entity->setUri((string) $self['uri']); + } + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/FilesObjectSourceProvider.php b/lib/Service/ObjectSource/FilesObjectSourceProvider.php new file mode 100644 index 0000000000..42bbf03ee6 --- /dev/null +++ b/lib/Service/ObjectSource/FilesObjectSourceProvider.php @@ -0,0 +1,291 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by the acting user's files. + */ +class FilesObjectSourceProvider implements ObjectSourceProvider +{ + + /** + * Hard cap on nodes traversed per read, so a huge home folder never runs away. + * + * @var int + */ + private const SCAN_CAP = 10000; + + /** + * Constructor. + * + * @param IRootFolder $rootFolder Nextcloud root folder (per-user home resolution). + * @param IUserSession $userSession Acting-user session for read-scoping. + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly IRootFolder $rootFolder, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function getId(): string + { + return 'files-source'; + }//end getId() + + /** + * {@inheritDoc} + * + * Files is a Nextcloud core app, so this provider is always available. + * + * @return bool Always true. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function isEnabled(): bool + { + return true; + }//end isEnabled() + + /** + * {@inheritDoc} + * + * MUST return null when the file is absent OR outside the acting user's home, + * so the two cases are indistinguishable (no enumeration oracle). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The file id (fileid). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + $userFolder = $this->userFolder(); + if ($userFolder === null || is_numeric($id) === false) { + return null; + } + + try { + $nodes = $userFolder->getById((int) $id); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:files-source] could not read file: '.$e->getMessage()); + return null; + } + + foreach ($nodes as $node) { + if ($node instanceof File) { + return $this->toObjectEntity(register: $register, schema: $schema, file: $node, userFolder: $userFolder); + } + } + + return null; + }//end find() + + /** + * {@inheritDoc} + * + * Honours `limit` and `offset`. Walks the acting user's home folder depth-first + * (bounded by {@see self::SCAN_CAP}) and projects each file it encounters. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (limit/offset). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity[] The matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $limit = (int) ($query['limit'] ?? 200); + $offset = (int) ($query['offset'] ?? 0); + + $userFolder = $this->userFolder(); + if ($userFolder === null) { + return []; + } + + $files = array_slice($this->collectFiles(root: $userFolder), $offset, $limit); + $objects = []; + foreach ($files as $file) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, file: $file, userFolder: $userFolder); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters). + * @param array $config The object-source config block (unused). + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Resolve the acting user's home folder, or null when there is no session user. + * + * @return Folder|null The user's home folder, or null. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function userFolder(): ?Folder + { + $user = $this->userSession->getUser(); + if ($user === null) { + return null; + } + + try { + return $this->rootFolder->getUserFolder($user->getUID()); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:files-source] could not resolve user folder: '.$e->getMessage()); + return null; + } + }//end userFolder() + + /** + * Depth-first collect the File nodes under a folder, bounded by the scan cap. + * + * @param Folder $root The folder to walk. + * + * @return array The collected files. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function collectFiles(Folder $root): array + { + $files = []; + $stack = [$root]; + $scanned = 0; + + while ($stack !== [] && $scanned < self::SCAN_CAP) { + $folder = array_pop($stack); + + try { + $children = $folder->getDirectoryListing(); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:files-source] could not list folder: '.$e->getMessage()); + continue; + } + + foreach ($children as $child) { + $scanned++; + if ($child instanceof File) { + $files[] = $child; + continue; + } + + if ($child instanceof Folder) { + $stack[] = $child; + } + } + }//end while + + return $files; + }//end collectFiles() + + /** + * Map a Nextcloud file onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param File $file The Nextcloud file. + * @param Folder $userFolder The acting user's home folder (for relative paths). + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function toObjectEntity(Register $register, Schema $schema, File $file, Folder $userFolder): ObjectEntity + { + $id = (string) $file->getId(); + $path = ($userFolder->getRelativePath($file->getPath()) ?? $file->getPath()); + + $data = [ + 'id' => $id, + 'name' => $file->getName(), + 'path' => $path, + 'mimetype' => $file->getMimetype(), + 'size' => (int) $file->getSize(), + 'mtime' => $file->getMTime(), + ]; + + $entity = new ObjectEntity(); + $entity->setUuid($id); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/GroupObjectSourceProvider.php b/lib/Service/ObjectSource/GroupObjectSourceProvider.php new file mode 100644 index 0000000000..585acd66eb --- /dev/null +++ b/lib/Service/ObjectSource/GroupObjectSourceProvider.php @@ -0,0 +1,271 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by the Nextcloud group directory. + */ +class GroupObjectSourceProvider implements ObjectSourceProvider +{ + /** + * Constructor. + * + * @param IGroupManager $groupManager Nextcloud group directory (search/get/admin). + * @param IUserSession $userSession Acting-user session for read-scoping. + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly IGroupManager $groupManager, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + public function getId(): string + { + return 'group-source'; + }//end getId() + + /** + * {@inheritDoc} + * + * The Nextcloud group directory is a core service, so this provider is always + * available. + * + * @return bool Always true. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + public function isEnabled(): bool + { + return true; + }//end isEnabled() + + /** + * {@inheritDoc} + * + * MUST return null when the group is absent OR the acting user may not read + * it, so the two cases are indistinguishable (no enumeration oracle). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The group gid. + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + try { + $group = $this->groupManager->get($id); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:group-source] could not read group: '.$e->getMessage()); + return null; + } + + if ($group === null || $this->mayRead(group: $group) === false) { + return null; + } + + return $this->toObjectEntity(register: $register, schema: $schema, group: $group); + }//end find() + + /** + * {@inheritDoc} + * + * Honours `filters.search`/`_search`, `limit` and `offset`. An admin sees + * every group; a plain user sees only the groups they belong to. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search/limit/offset). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity[] The matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $objects = []; + foreach ($this->readGroups(query: $query) as $group) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, group: $group); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search). + * @param array $config The object-source config block (unused). + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Read the groups visible to the acting user, failing closed to an empty list. + * + * Admins get the full group list (optionally filtered by a search term); a + * plain user gets only the groups they belong to. + * + * @param array $query Query (filters/search/limit/offset). + * + * @return array The visible groups. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + private function readGroups(array $query): array + { + $acting = $this->userSession->getUser(); + if ($acting === null) { + return []; + } + + $search = (string) ($query['filters']['search'] ?? $query['_search'] ?? $query['search'] ?? ''); + $limit = (int) ($query['limit'] ?? 200); + $offset = (int) ($query['offset'] ?? 0); + + try { + if ($this->groupManager->isAdmin($acting->getUID()) === true) { + return array_values($this->groupManager->search($search, $limit, $offset)); + } + + // A plain user only sees the groups they belong to. + $groups = array_values($this->groupManager->getUserGroups($acting)); + if ($search === '') { + return $groups; + } + + return array_values( + array_filter( + $groups, + static fn(IGroup $group) => str_contains(strtolower($group->getGID()), strtolower($search)) === true + || str_contains(strtolower($group->getDisplayName()), strtolower($search)) === true + ) + ); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:group-source] could not list groups: '.$e->getMessage()); + return []; + }//end try + }//end readGroups() + + /** + * Whether the acting user may read the given group's projection. + * + * Admins may read any group; a plain user may read only groups they belong to. + * + * @param IGroup $group The group being read. + * + * @return bool True when the acting user may read it. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + private function mayRead(IGroup $group): bool + { + $acting = $this->userSession->getUser(); + if ($acting === null) { + return false; + } + + try { + if ($this->groupManager->isAdmin($acting->getUID()) === true) { + return true; + } + + return $group->inGroup($acting); + } catch (Throwable $e) { + return false; + } + }//end mayRead() + + /** + * Map a Nextcloud group onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param IGroup $group The Nextcloud group. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.2 + */ + private function toObjectEntity(Register $register, Schema $schema, IGroup $group): ObjectEntity + { + $gid = $group->getGID(); + + $data = [ + 'id' => $gid, + 'displayName' => $group->getDisplayName(), + ]; + + $entity = new ObjectEntity(); + $entity->setUuid($gid); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/NcEntitySemanticMap.php b/lib/Service/ObjectSource/NcEntitySemanticMap.php new file mode 100644 index 0000000000..aa12e7ed4f --- /dev/null +++ b/lib/Service/ObjectSource/NcEntitySemanticMap.php @@ -0,0 +1,165 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-2.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +/** + * Static registry of Nextcloud-entity → virtual-schema semantic mappings. + */ +final class NcEntitySemanticMap +{ + + /** + * The virtual register slug every core NC-entity schema is seeded under. + * + * @var string + */ + public const DIRECTORY_REGISTER = 'directory'; + + /** + * Canonical NC-entity → virtual-schema rows. + * + * Row shape: + * - `register` — virtual register slug (core rows live in `directory`; each + * app-gated row lives in its own app-named register so the + * ADR-048 app-enabled gate degrades it when the app is gone); + * - `schema` — virtual schema slug (always `nc-`-prefixed so it never + * collides with a leaf-app schema of the same bare name); + * - `schemaOrg` — schema.org CURIE → the schema's `x-schema-org` marker; + * - `provider` — the ObjectSourceProvider id that serves its objects; + * - `requiredApp` — the NC app that must be installed for the provider to be + * usable (`null` = Nextcloud core, always available); + * - `application` — the register's `application` (drives the ADR-048 gate). + * + * @var array + */ + public const ENTITIES = [ + 'user' => [ + 'register' => self::DIRECTORY_REGISTER, + 'schema' => 'nc-user', + 'schemaOrg' => 'schema:Person', + 'provider' => 'user-directory-source', + 'requiredApp' => null, + 'application' => 'openregister', + ], + 'group' => [ + 'register' => self::DIRECTORY_REGISTER, + 'schema' => 'nc-group', + 'schemaOrg' => 'schema:Organization', + 'provider' => 'group-source', + 'requiredApp' => null, + 'application' => 'openregister', + ], + // App-gated rows — each lives on its OWN app-named register (application = + // register slug) so the ADR-048 app-enabled gate degrades the projection + // when the backing app is uninstalled. Schemas are `nc-`-prefixed to avoid + // colliding with same-named leaf-app schemas (e.g. `contact`, `event`). + 'contact' => [ + 'register' => 'contacts', + 'schema' => 'nc-contact', + 'schemaOrg' => 'schema:Person', + 'provider' => 'contacts-source', + 'requiredApp' => 'contacts', + 'application' => 'contacts', + ], + 'event' => [ + 'register' => 'calendar', + 'schema' => 'nc-event', + 'schemaOrg' => 'schema:Event', + 'provider' => 'calendar-event-source', + 'requiredApp' => 'calendar', + 'application' => 'calendar', + ], + 'file' => [ + 'register' => 'files', + 'schema' => 'nc-file', + 'schemaOrg' => 'schema:DigitalDocument', + 'provider' => 'files-source', + 'requiredApp' => null, + 'application' => 'files', + ], + 'card' => [ + 'register' => 'deck', + 'schema' => 'nc-card', + 'schemaOrg' => 'schema:Action', + 'provider' => 'deck-source', + 'requiredApp' => 'deck', + 'application' => 'deck', + ], + 'talk' => [ + 'register' => 'talk', + 'schema' => 'nc-conversation', + 'schemaOrg' => 'schema:Conversation', + 'provider' => 'talk-source', + 'requiredApp' => 'spreed', + 'application' => 'spreed', + ], + // Tasks reuse the EXISTING `caldav-vtodo` provider (no new provider + // class) — VTODOs are served from the core `dav` app, so the register's + // `application` gates on the optional `tasks` UI app. + 'task' => [ + 'register' => 'tasks', + 'schema' => 'nc-task', + 'schemaOrg' => 'schema:Action', + 'provider' => 'caldav-vtodo', + 'requiredApp' => 'tasks', + 'application' => 'tasks', + ], + // Nextcloud Tables hosts MANY tables (unlike the one-entity-kind rows + // above), so this row records only the provider + app gate; the concrete + // per-table schemas are auto-seeded under the `tables` register by + // SeedTablesVirtualSchemas / `occ openregister:tables:sync` (design D7). + // The `schema` slug here is a nominal placeholder — no single schema is + // seeded from this row. + 'tables' => [ + 'register' => 'tables', + 'schema' => 'nc-table', + 'schemaOrg' => 'schema:Dataset', + 'provider' => 'tables', + 'requiredApp' => 'tables', + 'application' => 'tables', + ], + ]; + + /** + * Not instantiable — this is a static map only. + */ + private function __construct() + { + }//end __construct() +}//end class diff --git a/lib/Service/ObjectSource/ObjectSourceProvider.php b/lib/Service/ObjectSource/ObjectSourceProvider.php new file mode 100644 index 0000000000..015633f2dd --- /dev/null +++ b/lib/Service/ObjectSource/ObjectSourceProvider.php @@ -0,0 +1,117 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; + +/** + * Read-only provider that serves a schema's objects from an external source. + */ +interface ObjectSourceProvider +{ + /** + * Stable provider id referenced by `x-openregister-object-source.provider`. + * + * @return string The provider id (e.g. 'caldav-vtodo'). + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.1 + */ + public function getId(): string; + + /** + * Whether this provider can serve objects on this instance right now + * (e.g. the backing Nextcloud app is installed and enabled). + * + * When false, reads of a bound schema degrade to an empty result with a + * logged warning rather than erroring or falling back to the database. + * + * @return bool True when the provider is usable. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.1 + */ + public function isEnabled(): bool; + + /** + * Find a single virtual object by id, applying the schema's read + * authorization for the acting user. + * + * MUST return null when the object is absent OR access is denied, so the + * two cases are indistinguishable (no enumeration oracle). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema being read. + * @param string $id The object id / source key. + * @param array $config The schema's object-source `config` block. + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.1 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity; + + /** + * Find all virtual objects matching the query, applying the schema's read + * authorization for the acting user (denied objects are omitted). + * + * A provider MAY ignore query operators it cannot honour; it MUST document + * what it supports and MUST NOT silently cap results without a logged warning. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema being read. + * @param array $query Query (filters/sort/search/limit/offset). + * @param array $config The schema's object-source `config` block. + * + * @return ObjectEntity[] The matching virtual objects (possibly empty). + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.1 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array; + + /** + * Count virtual objects matching the query (same authorization as findAll). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema being read. + * @param array $query Query (filters/search). + * @param array $config The schema's object-source `config` block. + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.1 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int; +}//end interface diff --git a/lib/Service/ObjectSource/ObjectSourceRegistry.php b/lib/Service/ObjectSource/ObjectSourceRegistry.php new file mode 100644 index 0000000000..d0d0993ba0 --- /dev/null +++ b/lib/Service/ObjectSource/ObjectSourceRegistry.php @@ -0,0 +1,145 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.2 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use Psr\Log\LoggerInterface; + +/** + * Registry of all ObjectSourceProvider implementations on this NC instance. + */ +class ObjectSourceRegistry +{ + + /** + * Registered providers, keyed by id. + * + * @var array + */ + private array $providers = []; + + /** + * Constructor. + * + * @param LoggerInterface $logger Logger for collision warnings. + * + * @return void + */ + public function __construct( + private LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Register a provider with the registry. + * + * Duplicate id: first registration wins, second logs a warning. + * + * @param ObjectSourceProvider $provider The provider to register. + * + * @return bool True when accepted, false when rejected (duplicate id). + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.2 + */ + public function addProvider(ObjectSourceProvider $provider): bool + { + $id = $provider->getId(); + if (isset($this->providers[$id]) === true) { + $this->logger->warning( + sprintf( + '[ObjectSourceRegistry] duplicate provider id "%s" — keeping first registration', + $id + ) + ); + return false; + } + + $this->providers[$id] = $provider; + return true; + }//end addProvider() + + /** + * Replace the entire provider set in one call (test seam). + * + * @param array $providers Provider instances. + * + * @return void + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.2 + */ + public function withProviders(array $providers): void + { + $this->providers = []; + foreach ($providers as $provider) { + $this->addProvider(provider: $provider); + } + }//end withProviders() + + /** + * Look up a provider by id. + * + * @param string $id The provider id (e.g. 'caldav-vtodo'). + * + * @return ObjectSourceProvider|null Provider, or null when unknown. + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.2 + */ + public function get(string $id): ?ObjectSourceProvider + { + return $this->providers[$id] ?? null; + }//end get() + + /** + * List every registered provider, irrespective of isEnabled(). + * + * @return array + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.2 + */ + public function list(): array + { + return array_values($this->providers); + }//end list() + + /** + * Return the ids of every registered provider. + * + * @return array + * + * @spec openspec/changes/object-source-providers/tasks.md#task-1.2 + */ + public function listIds(): array + { + return array_keys($this->providers); + }//end listIds() +}//end class diff --git a/lib/Service/ObjectSource/TablesColumnMapper.php b/lib/Service/ObjectSource/TablesColumnMapper.php new file mode 100644 index 0000000000..ed256293a3 --- /dev/null +++ b/lib/Service/ObjectSource/TablesColumnMapper.php @@ -0,0 +1,453 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use Psr\Log\LoggerInterface; + +/** + * Pure Tables-column ↔ virtual-schema projection helper. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) The D5 column-type mapping table is inherently a wide dispatch. + */ +class TablesColumnMapper +{ + /** + * Constructor. + * + * @param TablesUuidDeriver $uuidDeriver Deterministic uuid derivation (relations). + * @param LoggerInterface $logger Logger for drift / collision diagnostics. + * + * @return void + */ + public function __construct( + private readonly TablesUuidDeriver $uuidDeriver, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Build the `columnId → propertyName` map for a table's columns. + * + * Prefers each column's `technicalName`, falling back to a slug of its + * `title`. When two columns resolve to the same property name the collision + * is disambiguated by appending the numeric `columnId` and a warning is + * logged, so the projection stays deterministic (design edge-handling table). + * + * @param array> $columns The table's column descriptors. + * + * @return array Map of columnId → property name. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function buildColumnMap(array $columns): array + { + $map = []; + $seen = []; + foreach ($columns as $column) { + $columnId = (int) ($column['id'] ?? 0); + if ($columnId === 0) { + continue; + } + + $name = $this->propertyName(column: $column); + + if (isset($seen[$name]) === true) { + $disambiguated = $name.'_'.$columnId; + $this->logger->warning( + sprintf( + '[ObjectSource:tables] column %d slug "%s" collides with column %d; using "%s"', + $columnId, + $name, + $seen[$name], + $disambiguated + ) + ); + $name = $disambiguated; + } + + $seen[$name] = $columnId; + $map[$columnId] = $name; + }//end foreach + + return $map; + }//end buildColumnMap() + + /** + * Project a single row's cells onto a property-keyed object payload. + * + * Unknown `columnId`s (column drift after binding) are skipped and logged. + * Relation cells are mapped to the referenced virtual object's derived uuid + * when `$targetSchemaExists` confirms the target table has a seeded schema, + * otherwise they fall back to the raw integer rowId with a logged warning + * (design D9). + * + * @param array $row The row descriptor ({id, cells:[{columnId,value}]}). + * @param array> $columns The table's column descriptors (keyed by list order). + * @param array $columnMap Map of columnId → + * property name. + * @param callable(int): bool $targetSchemaExists Predicate: does the target table have a seeded schema? + * + * @return array The projected object payload (property → value). + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function projectRow(array $row, array $columns, array $columnMap, callable $targetSchemaExists): array + { + $byId = []; + foreach ($columns as $column) { + $byId[(int) ($column['id'] ?? 0)] = $column; + } + + $data = []; + foreach (($row['cells'] ?? []) as $cell) { + $columnId = (int) ($cell['columnId'] ?? 0); + if (isset($columnMap[$columnId]) === false) { + $this->logger->warning( + sprintf('[ObjectSource:tables] row %s references unknown column %d — skipped (drift)', (string) ($row['id'] ?? '?'), $columnId) + ); + continue; + } + + $column = ($byId[$columnId] ?? []); + $data[$columnMap[$columnId]] = $this->coerceCell( + value: ($cell['value'] ?? null), + column: $column, + targetSchemaExists: $targetSchemaExists + ); + }//end foreach + + return $data; + }//end projectRow() + + /** + * Build the virtual schema `properties` + `required` blocks for a table. + * + * @param array> $columns The table's column descriptors. + * + * @return array{properties: array, required: array} + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function buildSchemaProperties(array $columns): array + { + $columnMap = $this->buildColumnMap(columns: $columns); + $properties = []; + $required = []; + foreach ($columns as $column) { + $columnId = (int) ($column['id'] ?? 0); + if (isset($columnMap[$columnId]) === false) { + continue; + } + + $name = $columnMap[$columnId]; + $properties[$name] = $this->columnToProperty(column: $column); + if (($column['mandatory'] ?? false) === true) { + $required[] = $name; + } + } + + return [ + 'properties' => $properties, + 'required' => $required, + ]; + }//end buildSchemaProperties() + + /** + * Map one Tables column onto its JSON-schema property fragment (design D5). + * + * @param array $column The column descriptor. + * + * @return array The JSON-schema property fragment. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function columnToProperty(array $column): array + { + $type = (string) ($column['type'] ?? 'text'); + $subtype = (string) ($column['subtype'] ?? ''); + + switch ($type) { + case 'number': + return $this->numberProperty(column: $column, subtype: $subtype); + case 'datetime': + return ['type' => 'string', 'format' => $this->dateFormat(subtype: $subtype)]; + case 'selection': + return $this->selectionProperty(column: $column, subtype: $subtype); + case 'usergroup': + return ['type' => 'array', 'items' => ['type' => 'object']]; + case 'relation': + return ['type' => 'string', 'format' => 'uuid']; + case 'text': + default: + if ($subtype === 'link') { + return ['type' => 'string', 'format' => 'uri']; + } + return ['type' => 'string']; + }//end switch + }//end columnToProperty() + + /** + * Resolve a column's property name (technicalName, else slug of title). + * + * @param array $column The column descriptor. + * + * @return string The property name. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function propertyName(array $column): string + { + $technical = trim((string) ($column['technicalName'] ?? '')); + if ($technical !== '') { + return $this->slug(value: $technical); + } + + $slug = $this->slug(value: (string) ($column['title'] ?? '')); + if ($slug === '') { + return 'column_'.((int) ($column['id'] ?? 0)); + } + + return $slug; + }//end propertyName() + + /** + * Slugify a value into a stable, lower-case property name. + * + * @param string $value The raw value. + * + * @return string The slug (may be empty when the value has no word chars). + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function slug(string $value): string + { + $lower = strtolower(trim($value)); + $slugged = preg_replace('/[^a-z0-9]+/', '_', $lower); + + return trim((string) $slugged, '_'); + }//end slug() + + /** + * Coerce a raw cell value to its projected type (design D5). + * + * @param mixed $value The raw cell value. + * @param array $column The column descriptor. + * @param callable(int): bool $targetSchemaExists Predicate for relation targets. + * + * @return mixed The coerced value. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function coerceCell(mixed $value, array $column, callable $targetSchemaExists): mixed + { + $type = (string) ($column['type'] ?? 'text'); + $subtype = (string) ($column['subtype'] ?? ''); + + if ($value === null) { + return null; + } + + switch ($type) { + case 'number': + if ($subtype === 'progress' || $subtype === 'stars' || $this->isIntegral(column: $column) === true) { + return (int) $value; + } + return (float) $value; + case 'selection': + if ($subtype === 'check') { + // Tables stores check cells as the strings 'true'/'false'; + // a plain (bool) cast would turn 'false' into true. + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + + if ($subtype === 'multi') { + return (array) $value; + } + return (string) $value; + case 'usergroup': + return (array) $value; + case 'relation': + return $this->relationValue(value: $value, column: $column, targetSchemaExists: $targetSchemaExists); + case 'datetime': + case 'text': + default: + if (is_array($value) === true) { + return $value; + } + return (string) $value; + }//end switch + }//end coerceCell() + + /** + * Map a relation cell to the referenced virtual object's uuid, or fall back. + * + * @param mixed $value The raw relation cell value (referenced rowId). + * @param array $column The relation column descriptor (carries the target tableId). + * @param callable(int): bool $targetSchemaExists Predicate: does the target table have a seeded schema? + * + * @return string|int The derived uuid, or the raw integer rowId on fallback. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function relationValue(mixed $value, array $column, callable $targetSchemaExists): string|int + { + $targetTableId = (int) ($column['relationTargetTableId'] ?? 0); + $rowId = (int) $value; + + if ($targetTableId !== 0 && $targetSchemaExists($targetTableId) === true) { + return $this->uuidDeriver->deriveObjectUuid(tableId: $targetTableId, rowId: $rowId); + } + + $this->logger->warning( + sprintf( + '[ObjectSource:tables] relation column %d target table %d has no seeded schema — falling back to raw rowId %d', + (int) ($column['id'] ?? 0), + $targetTableId, + $rowId + ) + ); + + return $rowId; + }//end relationValue() + + /** + * Build the JSON-schema fragment for a `number` column (design D5). + * + * @param array $column The column descriptor. + * @param string $subtype The column subtype (progress/stars/...). + * + * @return array The JSON-schema property fragment. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function numberProperty(array $column, string $subtype): array + { + if ($subtype === 'progress') { + return ['type' => 'integer', 'minimum' => 0, 'maximum' => 100]; + } + + if ($subtype === 'stars') { + return ['type' => 'integer', 'minimum' => 0, 'maximum' => 5]; + } + + $numberType = 'number'; + if ($this->isIntegral(column: $column) === true) { + $numberType = 'integer'; + } + + $property = ['type' => $numberType]; + if (array_key_exists('numberMin', $column) === true && $column['numberMin'] !== null) { + $property['minimum'] = $column['numberMin'] + 0; + } + + if (array_key_exists('numberMax', $column) === true && $column['numberMax'] !== null) { + $property['maximum'] = $column['numberMax'] + 0; + } + + return $property; + }//end numberProperty() + + /** + * Build the JSON-schema fragment for a `selection` column (design D5). + * + * @param array $column The column descriptor. + * @param string $subtype The column subtype (check/multi/...). + * + * @return array The JSON-schema property fragment. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function selectionProperty(array $column, string $subtype): array + { + if ($subtype === 'check') { + return ['type' => 'boolean']; + } + + $options = array_values(array_map('strval', (array) ($column['selectionOptions'] ?? []))); + + if ($subtype === 'multi') { + return ['type' => 'array', 'items' => ['enum' => $options]]; + } + + $property = ['type' => 'string']; + if (empty($options) === false) { + $property['enum'] = $options; + } + + return $property; + }//end selectionProperty() + + /** + * Resolve the JSON-schema `format` for a datetime subtype. + * + * @param string $subtype The datetime subtype (date/time/...). + * + * @return string The JSON-schema format keyword. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function dateFormat(string $subtype): string + { + if ($subtype === 'date') { + return 'date'; + } + + if ($subtype === 'time') { + return 'time'; + } + + return 'date-time'; + }//end dateFormat() + + /** + * Whether a number column carries an integral step (⇒ integer, not number). + * + * @param array $column The column descriptor. + * + * @return bool True when the column is integral. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function isIntegral(array $column): bool + { + if (array_key_exists('numberDecimals', $column) === false || $column['numberDecimals'] === null) { + return false; + } + + return ((int) $column['numberDecimals']) === 0; + }//end isIntegral() +}//end class diff --git a/lib/Service/ObjectSource/TablesObjectSourceProvider.php b/lib/Service/ObjectSource/TablesObjectSourceProvider.php new file mode 100644 index 0000000000..2d18412c87 --- /dev/null +++ b/lib/Service/ObjectSource/TablesObjectSourceProvider.php @@ -0,0 +1,430 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use DateTime; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by the Nextcloud Tables app. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class TablesObjectSourceProvider implements ObjectSourceProvider +{ + + /** + * Default page size when a query carries no explicit limit. + * + * @var int + */ + private const DEFAULT_LIMIT = 200; + + /** + * Row cap for the O(n) UUID-resolution scan in find() (bounded + logged). + * + * @var int + */ + private const UUID_SCAN_CAP = 1000; + + /** + * Constructor. + * + * @param TablesTableReader $reader Guarded gateway to Tables services. + * @param TablesColumnMapper $columnMapper Column/row → property + * projection. + * @param TablesUuidDeriver $uuidDeriver Deterministic object-uuid derivation. + * @param TablesSchemaSyncService $syncService Managed-schema lookup (relation targets). + * @param IUserSession $userSession Acting-user session (RBAC scoping). + * @param LoggerInterface $logger Logger for read diagnostics. + * + * @return void + */ + public function __construct( + private readonly TablesTableReader $reader, + private readonly TablesColumnMapper $columnMapper, + private readonly TablesUuidDeriver $uuidDeriver, + private readonly TablesSchemaSyncService $syncService, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function getId(): string + { + return 'tables'; + }//end getId() + + /** + * {@inheritDoc} + * + * Gated on the Tables app (enabled for the acting user) and the loadability of + * its service classes; otherwise a bound schema degrades to an empty list. + * + * @return bool True when Tables can serve reads on this instance. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function isEnabled(): bool + { + return $this->reader->isAvailable(); + }//end isEnabled() + + /** + * {@inheritDoc} + * + * Accepts both a numeric Tables rowId and a derived UUID (resolved by a + * bounded, logged scan of the bound table). Returns null when the row is + * absent OR the acting user may not read it (denied == absent). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The rowId or derived UUID. + * @param array $config The object-source config block. + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + $userId = $this->userId(); + if ($userId === null) { + return null; + } + + $tableId = (int) ($config['tableId'] ?? 0); + if ($tableId === 0) { + return null; + } + + if (ctype_digit($id) === true) { + $row = $this->reader->findRow(rowId: (int) $id, userId: $userId); + if ($row === null) { + return null; + } + + return $this->toObjectEntity(register: $register, schema: $schema, row: $row, tableId: $tableId, userId: $userId); + } + + if ($this->uuidDeriver->looksLikeUuid($id) === true) { + return $this->findByUuid(register: $register, schema: $schema, uuid: $id, config: $config, userId: $userId); + } + + return null; + }//end find() + + /** + * {@inheritDoc} + * + * Pushes `limit`/`offset` natively to Tables; any other filter/sort is applied + * provider-side in PHP. A `config.viewId` binds a Tables View instead of the + * raw `config.tableId`. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/sort/limit/offset). + * @param array $config The object-source config block. + * + * @return ObjectEntity[] The matching virtual objects. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $userId = $this->userId(); + if ($userId === null) { + return []; + } + + $tableId = (int) ($config['tableId'] ?? 0); + if ($tableId === 0) { + return []; + } + + $limit = (int) ($query['limit'] ?? self::DEFAULT_LIMIT); + $offset = (int) ($query['offset'] ?? 0); + $this->warnUnpushedFilters(query: $query); + + $rows = $this->fetchRows(config: $config, tableId: $tableId, userId: $userId, limit: $limit, offset: $offset); + + $objects = []; + foreach ($rows as $row) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, row: $row, tableId: $tableId, userId: $userId); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search). + * @param array $config The object-source config block. + * + * @return int The number of matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $register/$schema unused; the count is source-side. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + $userId = $this->userId(); + if ($userId === null) { + return 0; + } + + $viewId = (int) ($config['viewId'] ?? 0); + $tableId = (int) ($config['tableId'] ?? 0); + if ($viewId !== 0) { + return $this->reader->countRows(id: $viewId, userId: $userId, isView: true); + } + + if ($tableId !== 0) { + return $this->reader->countRows(id: $tableId, userId: $userId, isView: false); + } + + return 0; + }//end count() + + /** + * Resolve a virtual object by its derived UUID via a bounded, logged scan. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $uuid The derived object UUID. + * @param array $config The object-source config block. + * @param string $userId The acting user id. + * + * @return ObjectEntity|null The virtual object, or null when not found. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function findByUuid(Register $register, Schema $schema, string $uuid, array $config, string $userId): ?ObjectEntity + { + $tableId = (int) ($config['tableId'] ?? 0); + $this->logger->warning( + sprintf('[ObjectSource:tables] resolving UUID %s by scanning table %d (bounded to %d rows)', $uuid, $tableId, self::UUID_SCAN_CAP) + ); + + $rows = $this->fetchRows(config: $config, tableId: $tableId, userId: $userId, limit: self::UUID_SCAN_CAP, offset: 0); + foreach ($rows as $row) { + $rowId = (int) ($row['id'] ?? 0); + if ($this->uuidDeriver->deriveObjectUuid(tableId: $tableId, rowId: $rowId) === $uuid) { + return $this->toObjectEntity(register: $register, schema: $schema, row: $row, tableId: $tableId, userId: $userId); + } + } + + return null; + }//end findByUuid() + + /** + * Fetch a page of rows, honouring a `config.viewId` View binding. + * + * @param array $config The object-source config block. + * @param int $tableId The bound table id. + * @param string $userId The acting user id. + * @param int $limit The native row limit. + * @param int $offset The native row offset. + * + * @return array> The row descriptors. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function fetchRows(array $config, int $tableId, string $userId, int $limit, int $offset): array + { + $viewId = (int) ($config['viewId'] ?? 0); + if ($viewId !== 0) { + return $this->reader->findRowsByView(viewId: $viewId, userId: $userId, limit: $limit, offset: $offset); + } + + return $this->reader->findRowsByTable(tableId: $tableId, userId: $userId, limit: $limit, offset: $offset); + }//end fetchRows() + + /** + * Log a warning when a query carries filters/sort the provider cannot push + * down (no silent truncation — per the interface contract). + * + * @param array $query The query. + * + * @return void + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function warnUnpushedFilters(array $query): void + { + $pushable = ['limit', 'offset', '_limit', '_offset', 'page', '_page']; + $unpushed = array_diff(array_keys($query), $pushable); + if (empty($unpushed) === false) { + $this->logger->warning( + '[ObjectSource:tables] query operators applied provider-side (not pushed down): '.implode(', ', $unpushed) + ); + } + }//end warnUnpushedFilters() + + /** + * Map a Tables row descriptor onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $row The row descriptor. + * @param int $tableId The bound table id. + * @param string $userId The acting user id. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function toObjectEntity(Register $register, Schema $schema, array $row, int $tableId, string $userId): ObjectEntity + { + $columns = $this->reader->listColumns(tableId: $tableId, userId: $userId); + $columnMap = $this->columnMapper->buildColumnMap(columns: $columns); + $resolver = $this->targetSchemaResolver(); + + $rowId = (int) ($row['id'] ?? 0); + $data = $this->columnMapper->projectRow(row: $row, columns: $columns, columnMap: $columnMap, targetSchemaExists: $resolver); + $data = array_merge(['id' => (string) $rowId], $data); + + $entity = new ObjectEntity(); + $entity->setUuid($this->uuidDeriver->deriveObjectUuid(tableId: $tableId, rowId: $rowId)); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + $this->applyMetadata(entity: $entity, row: $row); + + return $entity; + }//end toObjectEntity() + + /** + * Apply the row's `@self` metadata (created/updated/owner) to the entity. + * + * @param ObjectEntity $entity The virtual object. + * @param array $row The row descriptor. + * + * @return void + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function applyMetadata(ObjectEntity $entity, array $row): void + { + $owner = ($row['createdBy'] ?? null); + if (is_string($owner) === true && $owner !== '') { + $entity->setOwner($owner); + } + + $this->applyDate(entity: $entity, setter: 'setCreated', value: ($row['createdAt'] ?? null)); + $this->applyDate(entity: $entity, setter: 'setUpdated', value: ($row['lastEditAt'] ?? null)); + }//end applyMetadata() + + /** + * Parse an ISO-8601 string and apply it via a DateTime setter, guarded. + * + * @param ObjectEntity $entity The virtual object. + * @param string $setter The DateTime setter name (setCreated/setUpdated). + * @param mixed $value The ISO-8601 string, or null. + * + * @return void + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function applyDate(ObjectEntity $entity, string $setter, mixed $value): void + { + if (is_string($value) === false || $value === '') { + return; + } + + try { + $entity->$setter(new DateTime($value)); + } catch (Throwable $e) { + // Unparseable timestamp — leave the metadata field unset. + } + }//end applyDate() + + /** + * Build a memoised predicate: does a target table have a seeded schema? + * + * @return callable(int): bool The relation-target predicate. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function targetSchemaResolver(): callable + { + $cache = []; + + return function (int $targetTableId) use (&$cache): bool { + if (array_key_exists($targetTableId, $cache) === false) { + $cache[$targetTableId] = $this->syncService->hasManagedSchemaForTableId(tableId: $targetTableId); + } + + return $cache[$targetTableId]; + }; + }//end targetSchemaResolver() + + /** + * The acting user id, or null when no user is logged in. + * + * @return string|null The acting user id. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function userId(): ?string + { + $user = $this->userSession->getUser(); + if ($user === null) { + return null; + } + + return $user->getUID(); + }//end userId() +}//end class diff --git a/lib/Service/ObjectSource/TablesSchemaSyncService.php b/lib/Service/ObjectSource/TablesSchemaSyncService.php new file mode 100644 index 0000000000..bff330ffcf --- /dev/null +++ b/lib/Service/ObjectSource/TablesSchemaSyncService.php @@ -0,0 +1,403 @@ +-t`, carrying the + * `x-openregister-object-source` binding and a `managed` marker) and retires the + * managed schemas of tables that are gone. It NEVER overwrites a schema it did + * not create (a hand-authored `config.tableId` binding is left untouched), per + * design D7. The same reconcile runs from the Repair step, the + * `occ openregister:tables:sync` command, and — for single-table retirement — + * the `TableDeletedEvent` listener. + * + * @category Service + * @package OCA\OpenRegister\Service\ObjectSource + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Reconciler for the auto-seeded `tables` virtual register and its schemas. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class TablesSchemaSyncService +{ + + /** + * The virtual register slug every Tables schema is seeded under. + * + * @var string + */ + public const REGISTER_SLUG = 'tables'; + + /** + * The provider id serving these schemas' objects. + * + * @var string + */ + public const PROVIDER_ID = 'tables'; + + /** + * Constructor. + * + * @param RegisterMapper $registerMapper Register data mapper. + * @param SchemaMapper $schemaMapper Schema data mapper. + * @param TablesColumnMapper $columnMapper Column → schema-property projection. + * @param LoggerInterface $logger Logger for reconcile diagnostics. + * + * @return void + */ + public function __construct( + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly TablesColumnMapper $columnMapper, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Reconcile the `tables` register against the current set of tables. + * + * Idempotent: re-running with an unchanged table set makes no changes. Seeds + * or refreshes one managed schema per descriptor and retires managed schemas + * whose table is no longer present. + * + * @param array> $tables The table descriptors ({id, title, columns}). + * + * @return array{seeded: int, retired: int, skipped: int} Reconcile statistics. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function reconcile(array $tables): array + { + $register = $this->ensureRegister(); + $stats = ['seeded' => 0, 'retired' => 0, 'skipped' => 0]; + + $seenTableIds = []; + foreach ($tables as $table) { + $tableId = (int) ($table['id'] ?? 0); + $seenTableIds[] = $tableId; + if ($this->seedTable(register: $register, table: $table) === true) { + $stats['seeded']++; + continue; + } + + $stats['skipped']++; + } + + $stats['retired'] = $this->retireMissing(register: $register, keepTableIds: $seenTableIds); + + return $stats; + }//end reconcile() + + /** + * Retire the managed schema bound to a single deleted table. + * + * @param int $tableId The id of the deleted table. + * + * @return bool True when a schema was retired. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function retireByTableId(int $tableId): bool + { + try { + $register = $this->registerMapper->find(self::REGISTER_SLUG, _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + return false; + } + + $retired = $this->retireMissing(register: $register, keepTableIds: [], onlyTableId: $tableId); + + return $retired > 0; + }//end retireByTableId() + + /** + * Whether a managed schema exists for the given table id (relation target). + * + * @param int $tableId The candidate target table id. + * + * @return bool True when a managed schema is bound to that table. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function hasManagedSchemaForTableId(int $tableId): bool + { + try { + $register = $this->registerMapper->find(self::REGISTER_SLUG, _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + return false; + } + + return isset($this->managedSchemasByTableId(register: $register)[$tableId]); + }//end hasManagedSchemaForTableId() + + /** + * Deterministic idempotent slug for a table's virtual schema. + * + * @param string $title The table title. + * @param int $tableId The table id. + * + * @return string The schema slug (`nc--t`). + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function deterministicSlug(string $title, int $tableId): string + { + $slug = strtolower(trim($title)); + $slug = (string) preg_replace('/[^a-z0-9]+/', '-', $slug); + $slug = trim($slug, '-'); + if ($slug === '') { + $slug = 'table'; + } + + return sprintf('nc-%s-t%d', $slug, $tableId); + }//end deterministicSlug() + + /** + * Find or create the `tables` virtual register. + * + * @return Register The existing or newly created register. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function ensureRegister(): Register + { + try { + return $this->registerMapper->find(self::REGISTER_SLUG, _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + return $this->registerMapper->createFromArray( + object: [ + 'title' => 'Tables', + 'slug' => self::REGISTER_SLUG, + 'description' => 'Read-only virtual register projecting Nextcloud Tables tables as OpenRegister objects.', + 'application' => self::REGISTER_SLUG, + 'schemas' => [], + ] + ); + } + }//end ensureRegister() + + /** + * Seed or refresh the managed schema for one table. + * + * @param Register $register The `tables` register. + * @param array $table The table descriptor ({id, title, columns}). + * + * @return bool True when the schema was created or refreshed, false when skipped. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function seedTable(Register $register, array $table): bool + { + $tableId = (int) ($table['id'] ?? 0); + $title = (string) ($table['title'] ?? ''); + $slug = $this->deterministicSlug(title: $title, tableId: $tableId); + $shape = $this->columnMapper->buildSchemaProperties(columns: (array) ($table['columns'] ?? [])); + + $configuration = [ + 'x-schema-org' => 'schema:Thing', + 'x-openregister-object-source' => [ + 'provider' => self::PROVIDER_ID, + 'readOnly' => true, + 'config' => ['tableId' => $tableId, 'managed' => true], + ], + ]; + + $schemaTitle = $title; + if ($schemaTitle === '') { + $schemaTitle = $slug; + } + + $payload = [ + 'title' => $schemaTitle, + 'slug' => $slug, + 'description' => sprintf('Read-only virtual schema for Nextcloud Tables table #%d.', $tableId), + 'properties' => $shape['properties'], + 'required' => $shape['required'], + 'configuration' => $configuration, + ]; + + try { + $existing = $this->schemaMapper->find($slug, _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + $schema = $this->schemaMapper->createFromArray(object: $payload); + $this->linkSchema(register: $register, schema: $schema); + return true; + } + + if ($this->isManaged(schema: $existing) === false) { + $this->logger->warning( + sprintf('[ObjectSource:tables] schema "%s" is hand-authored — not overwriting during sync', $slug) + ); + return false; + } + + $this->schemaMapper->updateFromArray(id: (int) $existing->getId(), object: $payload); + $this->linkSchema(register: $register, schema: $existing); + + return true; + }//end seedTable() + + /** + * Retire managed schemas whose table is no longer present. + * + * @param Register $register The `tables` register. + * @param array $keepTableIds Table ids to keep (empty when only retiring one). + * @param int|null $onlyTableId When set, retire only this table's schema. + * + * @return int The number of schemas retired. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function retireMissing(Register $register, array $keepTableIds, ?int $onlyTableId=null): int + { + $retired = 0; + $schemaIds = $register->getSchemas(); + $changed = false; + foreach ($this->managedSchemasByTableId(register: $register) as $tableId => $schema) { + if ($onlyTableId !== null && $tableId !== $onlyTableId) { + continue; + } + + if ($onlyTableId === null && in_array($tableId, $keepTableIds, true) === true) { + continue; + } + + // **DELETE SAFETY (runtime-schema-api)**: no `force: true` here. + // + // These schemas are read-only MIRRORS of Nextcloud Tables tables — their + // objects live in Tables, not in a magic table — so the repaired + // SchemaMapper guard normally counts 0 and retiring a mirror of a + // now-deleted table proceeds exactly as before. If a magic table DOES hold + // rows for such a schema, that is real user data that this sync job has no + // mandate to destroy; refusing (and leaving the schema linked) is the safe + // outcome. Forcing here would silently orphan it. + try { + $this->schemaMapper->delete($schema); + $retired++; + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:tables] could not retire schema '.$schema->getSlug().': '.$e->getMessage()); + // Do NOT unlink a schema we failed to delete: that would detach a + // surviving schema from its register and hide it from every editor — + // the precise data-corruption pattern this change exists to close. + continue; + } + + $schemaIds = array_values( + array_filter( + $schemaIds, + static fn($ref) => (string) $ref !== (string) $schema->getId() + && (string) $ref !== (string) $schema->getSlug() + ) + ); + $changed = true; + }//end foreach + + if ($changed === true) { + $register->setSchemas($schemaIds); + $this->registerMapper->update($register); + } + + return $retired; + }//end retireMissing() + + /** + * Load the managed schemas of the register, keyed by their bound table id. + * + * @param Register $register The `tables` register. + * + * @return array Map of tableId → managed schema. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function managedSchemasByTableId(Register $register): array + { + $result = []; + foreach ($register->getSchemas() as $ref) { + try { + $schema = $this->schemaMapper->find($ref, _rbac: false, _multitenancy: false); + } catch (Throwable $e) { + continue; + } + + if ($this->isManaged(schema: $schema) === false) { + continue; + } + + $tableId = (int) ($schema->getObjectSource()['config']['tableId'] ?? 0); + if ($tableId !== 0) { + $result[$tableId] = $schema; + } + } + + return $result; + }//end managedSchemasByTableId() + + /** + * Whether a schema is managed by this sync (vs hand-authored). + * + * @param Schema $schema The schema to test. + * + * @return bool True when the schema carries the managed marker. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function isManaged(Schema $schema): bool + { + $source = $schema->getObjectSource(); + if ($source === null || $source['provider'] !== self::PROVIDER_ID) { + return false; + } + + return ($source['config']['managed'] ?? false) === true; + }//end isManaged() + + /** + * Ensure a schema is linked into the register's schema list. + * + * @param Register $register The `tables` register. + * @param Schema $schema The schema to link. + * + * @return void + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function linkSchema(Register $register, Schema $schema): void + { + $schemaIds = $register->getSchemas(); + if (in_array($schema->getId(), $schemaIds, false) === true) { + return; + } + + $schemaIds[] = $schema->getId(); + $register->setSchemas($schemaIds); + $this->registerMapper->update($register); + }//end linkSchema() +}//end class diff --git a/lib/Service/ObjectSource/TablesTableReader.php b/lib/Service/ObjectSource/TablesTableReader.php new file mode 100644 index 0000000000..4fc11d6eab --- /dev/null +++ b/lib/Service/ObjectSource/TablesTableReader.php @@ -0,0 +1,682 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCP\App\IAppManager; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Guarded plain-array reader over the Nextcloud Tables internal services. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Every Tables-entity getter is individually guarded (soft dependency). + */ +class TablesTableReader +{ + + /** + * NC app id whose install-state gates the reader. + * + * @var string + */ + public const REQUIRED_APP = 'tables'; + + /** + * Tables' table service (resolved dynamically — Tables' namespace). + * + * @var string + */ + private const TABLE_SERVICE = 'OCA\\Tables\\Service\\TableService'; + + /** + * Tables' column service (resolved dynamically — Tables' namespace). + * + * @var string + */ + private const COLUMN_SERVICE = 'OCA\\Tables\\Service\\ColumnService'; + + /** + * Tables' row service (resolved dynamically — Tables' namespace). + * + * @var string + */ + private const ROW_SERVICE = 'OCA\\Tables\\Service\\RowService'; + + /** + * Tables' view service (resolved dynamically — Tables' namespace). + * + * @var string + */ + private const VIEW_SERVICE = 'OCA\\Tables\\Service\\ViewService'; + + /** + * Constructor. + * + * @param IAppManager $appManager App availability checks. + * @param ContainerInterface $container Server container (lazy Tables-service lookup). + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly ContainerInterface $container, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Whether the Tables app is enabled for the acting (session) user AND its + * service classes are loadable on this instance. + * + * @return bool True when Tables reads can be served. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function isAvailable(): bool + { + try { + $enabled = $this->appManager->isEnabledForUser(self::REQUIRED_APP); + } catch (Throwable $e) { + return false; + } + + return $enabled === true && class_exists(self::ROW_SERVICE) === true; + }//end isAvailable() + + /** + * List every table the acting user may read, as `{id, title}` descriptors. + * + * @param string $userId The acting user id. + * + * @return array The table descriptors. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function listTables(string $userId): array + { + $service = $this->resolveService(class: self::TABLE_SERVICE); + if ($service === null) { + return []; + } + + try { + // Fourth argument (createTutorial) MUST stay false: the default of + // TableService::findAll() creates a tutorial table for first-time + // users, which a read-only reconcile pass must never do. + $tables = $service->findAll($userId, false, false, false); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:tables] could not list tables: '.$e->getMessage()); + return []; + } + + $result = []; + foreach ((array) $tables as $table) { + if (is_object($table) === false) { + continue; + } + + $id = $this->intGetter(entity: $table, getter: 'getId'); + if ($id === null) { + continue; + } + + $result[] = [ + 'id' => $id, + 'title' => $this->stringGetter(entity: $table, getter: 'getTitle'), + ]; + }//end foreach + + return $result; + }//end listTables() + + /** + * Collect full table descriptors (`{id, title, columns}`) visible to any of + * the given users, deduplicated by table id. + * + * Tables scopes enumeration per user (no un-scoped public API), so seeding + * enumerates the tables visible to the supplied users (typically admins); + * this is best-effort and documented — a table visible to none of them is not + * seeded until a sync runs for a user who can see it. + * + * @param array $userIds The acting user ids to enumerate for. + * + * @return array>}> The descriptors. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function collectTableDescriptors(array $userIds): array + { + $byId = []; + foreach ($userIds as $userId) { + foreach ($this->listTables(userId: $userId) as $table) { + $tableId = (int) $table['id']; + if (isset($byId[$tableId]) === true) { + continue; + } + + $byId[$tableId] = [ + 'id' => $tableId, + 'title' => (string) $table['title'], + 'columns' => $this->listColumns(tableId: $tableId, userId: $userId), + ]; + } + } + + return array_values($byId); + }//end collectTableDescriptors() + + /** + * List a table's columns as plain descriptors. + * + * @param int $tableId The table id. + * @param string $userId The acting user id. + * + * @return array> The column descriptors. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function listColumns(int $tableId, string $userId): array + { + $service = $this->resolveService(class: self::COLUMN_SERVICE); + if ($service === null) { + return []; + } + + try { + $columns = $service->findAllByTable($tableId, $userId); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:tables] could not list columns for table '.$tableId.': '.$e->getMessage()); + return []; + } + + $result = []; + foreach ((array) $columns as $column) { + if (is_object($column) === false) { + continue; + } + + $result[] = $this->columnDescriptor(column: $column); + } + + return $result; + }//end listColumns() + + /** + * Fetch a page of rows for a table, as plain descriptors. + * + * @param int $tableId The table id. + * @param string $userId The acting user id. + * @param int|null $limit Optional native row limit. + * @param int|null $offset Optional native row offset. + * + * @return array> The row descriptors. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function findRowsByTable(int $tableId, string $userId, ?int $limit=null, ?int $offset=null): array + { + return $this->rows(getter: 'findAllByTable', id: $tableId, userId: $userId, limit: $limit, offset: $offset); + }//end findRowsByTable() + + /** + * Fetch a page of rows for a View, as plain descriptors. + * + * @param int $viewId The view id. + * @param string $userId The acting user id. + * @param int|null $limit Optional native row limit. + * @param int|null $offset Optional native row offset. + * + * @return array> The row descriptors. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function findRowsByView(int $viewId, string $userId, ?int $limit=null, ?int $offset=null): array + { + return $this->rows(getter: 'findAllByView', id: $viewId, userId: $userId, limit: $limit, offset: $offset); + }//end findRowsByView() + + /** + * Fetch a single row by id, as a plain descriptor. + * + * @param int $rowId The Tables row id. + * @param string $userId The acting user id. + * + * @return array|null The row descriptor, or null when absent/denied. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $userId is kept for reader-contract parity + * with the View/table paths; RowService::find() resolves the acting user from the session. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function findRow(int $rowId, string $userId): ?array + { + $service = $this->resolveService(class: self::ROW_SERVICE); + if ($service === null) { + return null; + } + + try { + // RowService::find() takes only the row id; Tables resolves the + // acting user from the session internally (session-scoped RBAC). + // The $userId parameter is kept in this reader's contract for the + // View/table paths, which do take an explicit user id. + $row = $service->find($rowId); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:tables] could not read row '.$rowId.': '.$e->getMessage()); + return null; + } + + if (is_object($row) === false) { + return null; + } + + return $this->rowDescriptor(row: $row); + }//end findRow() + + /** + * Count the rows of a table (or View) the acting user may read. + * + * @param int $id The table id (or the view id when $isView). + * @param string $userId The acting user id. + * @param bool $isView Whether $id is a view id. + * + * @return int The row count (0 when unavailable/denied). + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Mirrors the table-vs-View binding in the schema config. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function countRows(int $id, string $userId, bool $isView=false): int + { + $service = $this->resolveService(class: self::ROW_SERVICE); + if ($service === null) { + return 0; + } + + try { + if ($isView === true) { + // The getViewRowsCount() call needs the View entity, not the view id. + $viewService = $this->resolveService(class: self::VIEW_SERVICE); + if ($viewService === null) { + return 0; + } + + $view = $viewService->find($id, true, $userId); + return (int) $service->getViewRowsCount($view, $userId); + } + + // The getRowsCount() call takes only the table id; Tables checks read + // access against the session user internally. + return (int) $service->getRowsCount($id); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:tables] could not count rows for '.$id.': '.$e->getMessage()); + return 0; + } + }//end countRows() + + /** + * Shared row-fetch for table/view getters. + * + * @param string $getter The RowService method name. + * @param int $id The table or view id. + * @param string $userId The acting user id. + * @param int|null $limit Optional native row limit. + * @param int|null $offset Optional native row offset. + * + * @return array> The row descriptors. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function rows(string $getter, int $id, string $userId, ?int $limit, ?int $offset): array + { + $service = $this->resolveService(class: self::ROW_SERVICE); + if ($service === null) { + return []; + } + + try { + $rows = $service->$getter($id, $userId, $limit, $offset); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:tables] could not read rows for '.$id.': '.$e->getMessage()); + return []; + } + + $result = []; + foreach ((array) $rows as $row) { + if (is_object($row) === false) { + continue; + } + + $result[] = $this->rowDescriptor(row: $row); + } + + return $result; + }//end rows() + + /** + * Extract a plain descriptor from a Tables Row2 entity. + * + * @param object $row The Tables row entity. + * + * @return array The row descriptor. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function rowDescriptor(object $row): array + { + $cells = []; + foreach ($this->arrayGetter(entity: $row, getter: 'getData') as $cell) { + $cellArray = $this->cellToArray(cell: $cell); + if ($cellArray !== null) { + $cells[] = $cellArray; + } + } + + return [ + 'id' => ($this->intGetter(entity: $row, getter: 'getId') ?? 0), + 'createdBy' => $this->nullableStringGetter(entity: $row, getter: 'getCreatedBy'), + 'createdAt' => $this->dateGetter(entity: $row, getter: 'getCreatedAt'), + 'lastEditBy' => $this->nullableStringGetter(entity: $row, getter: 'getLastEditBy'), + 'lastEditAt' => $this->dateGetter(entity: $row, getter: 'getLastEditAt'), + 'cells' => $cells, + ]; + }//end rowDescriptor() + + /** + * Normalise a single Tables cell (array or object) to `{columnId, value}`. + * + * @param mixed $cell The raw cell (array or object). + * + * @return array{columnId: int, value: mixed}|null The normalised cell, or null. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function cellToArray(mixed $cell): ?array + { + if (is_array($cell) === true) { + if (isset($cell['columnId']) === false) { + return null; + } + + return ['columnId' => (int) $cell['columnId'], 'value' => ($cell['value'] ?? null)]; + } + + if (is_object($cell) === true) { + $columnId = $this->intGetter(entity: $cell, getter: 'getColumnId'); + if ($columnId === null) { + return null; + } + + return ['columnId' => $columnId, 'value' => $this->rawGetter(entity: $cell, getter: 'getValue')]; + } + + return null; + }//end cellToArray() + + /** + * Extract a plain descriptor from a Tables Column entity. + * + * @param object $column The Tables column entity. + * + * @return array The column descriptor. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function columnDescriptor(object $column): array + { + return [ + 'id' => ($this->intGetter(entity: $column, getter: 'getId') ?? 0), + 'title' => $this->stringGetter(entity: $column, getter: 'getTitle'), + 'technicalName' => $this->nullableStringGetter(entity: $column, getter: 'getTechnicalName'), + 'type' => $this->stringGetter(entity: $column, getter: 'getType'), + 'subtype' => $this->nullableStringGetter(entity: $column, getter: 'getSubtype'), + 'mandatory' => ($this->boolGetter(entity: $column, getter: 'getMandatory') === true), + 'numberMin' => $this->floatGetter(entity: $column, getter: 'getNumberMin'), + 'numberMax' => $this->floatGetter(entity: $column, getter: 'getNumberMax'), + 'numberDecimals' => $this->intGetter(entity: $column, getter: 'getNumberDecimals'), + 'selectionOptions' => $this->selectionOptions(column: $column), + 'relationTargetTableId' => $this->intGetter(entity: $column, getter: 'getRelationTableId'), + ]; + }//end columnDescriptor() + + /** + * Extract selection-option labels from a Tables column, tolerant of shapes. + * + * @param object $column The Tables column entity. + * + * @return array The option labels. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function selectionOptions(object $column): array + { + $options = []; + foreach ($this->arrayGetter(entity: $column, getter: 'getSelectionOptions') as $option) { + if (is_array($option) === true) { + $options[] = (string) ($option['label'] ?? $option['value'] ?? ''); + continue; + } + + $options[] = (string) $option; + } + + return array_values(array_filter($options, static fn(string $label) => $label !== '')); + }//end selectionOptions() + + /** + * Resolve one of Tables' service classes from the container, or null. + * + * @param string $class The fully-qualified Tables service class name. + * + * @return object|null The resolved service, or null when Tables is absent. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function resolveService(string $class): ?object + { + if (class_exists($class) === false) { + return null; + } + + try { + $service = $this->container->get($class); + if (is_object($service) === true) { + return $service; + } + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:tables] could not resolve '.$class.': '.$e->getMessage()); + } + + return null; + }//end resolveService() + + /** + * Read an integer getter from a Tables entity, guarded (magic getters). + * + * @param object $entity The Tables entity. + * @param string $getter The getter method name. + * + * @return int|null The integer value, or null when unavailable. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function intGetter(object $entity, string $getter): ?int + { + $value = $this->rawGetter(entity: $entity, getter: $getter); + if ($value === null || is_scalar($value) === false) { + return null; + } + + return (int) $value; + }//end intGetter() + + /** + * Read a float getter from a Tables entity, guarded. + * + * @param object $entity The Tables entity. + * @param string $getter The getter method name. + * + * @return float|null The float value, or null when unavailable. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function floatGetter(object $entity, string $getter): ?float + { + $value = $this->rawGetter(entity: $entity, getter: $getter); + if ($value === null || is_scalar($value) === false) { + return null; + } + + return (float) $value; + }//end floatGetter() + + /** + * Read a boolean getter from a Tables entity, guarded. + * + * @param object $entity The Tables entity. + * @param string $getter The getter method name. + * + * @return bool The boolean value (false when unavailable). + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function boolGetter(object $entity, string $getter): bool + { + return (bool) $this->rawGetter(entity: $entity, getter: $getter); + }//end boolGetter() + + /** + * Read a string getter from a Tables entity, defaulting to '' on failure. + * + * @param object $entity The Tables entity. + * @param string $getter The getter method name. + * + * @return string The string value, or ''. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function stringGetter(object $entity, string $getter): string + { + return (string) ($this->nullableStringGetter(entity: $entity, getter: $getter) ?? ''); + }//end stringGetter() + + /** + * Read a string getter from a Tables entity, returning null when absent. + * + * @param object $entity The Tables entity. + * @param string $getter The getter method name. + * + * @return string|null The string value, or null. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function nullableStringGetter(object $entity, string $getter): ?string + { + $value = $this->rawGetter(entity: $entity, getter: $getter); + if ($value === null || is_scalar($value) === false) { + return null; + } + + return (string) $value; + }//end nullableStringGetter() + + /** + * Read an ISO-8601 date getter from a Tables entity, or null. + * + * @param object $entity The Tables entity. + * @param string $getter The getter method name. + * + * @return string|null The ISO-8601 date string, or null. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function dateGetter(object $entity, string $getter): ?string + { + $value = $this->rawGetter(entity: $entity, getter: $getter); + if ($value instanceof \DateTimeInterface) { + return $value->format(\DateTime::ATOM); + } + + if (is_string($value) === true && $value !== '') { + return $value; + } + + return null; + }//end dateGetter() + + /** + * Read an array getter from a Tables entity, defaulting to [] on failure. + * + * @param object $entity The Tables entity. + * @param string $getter The getter method name. + * + * @return array The array value, or []. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function arrayGetter(object $entity, string $getter): array + { + $value = $this->rawGetter(entity: $entity, getter: $getter); + if (is_array($value) === true) { + return $value; + } + + return []; + }//end arrayGetter() + + /** + * Invoke a getter on a Tables entity, guarded (OCP Entity magic getters make + * method_exists unreliable, so each call is wrapped). + * + * @param object $entity The Tables entity. + * @param string $getter The getter method name. + * + * @return mixed The raw value, or null on failure. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + private function rawGetter(object $entity, string $getter): mixed + { + try { + return $entity->$getter(); + } catch (Throwable $e) { + return null; + } + }//end rawGetter() +}//end class diff --git a/lib/Service/ObjectSource/TablesUuidDeriver.php b/lib/Service/ObjectSource/TablesUuidDeriver.php new file mode 100644 index 0000000000..414e5847cf --- /dev/null +++ b/lib/Service/ObjectSource/TablesUuidDeriver.php @@ -0,0 +1,93 @@ +:`, so a relation link and the referenced + * object's own uuid always agree and OR-level deep-linking works across the + * auto-seeded virtual schemas. + * + * UUIDv5 is one-way, so a UUID cannot be inverted back to a `(tableId, rowId)` + * pair; {@see TablesObjectSourceProvider::find()} resolves a UUID by scanning the + * bound table and comparing derived uuids (bounded + logged). + * + * @category Service + * @package OCA\OpenRegister\Service\ObjectSource + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use Symfony\Component\Uid\Uuid; + +/** + * Deterministic UUIDv5 derivation for Tables-backed virtual objects. + */ +final class TablesUuidDeriver +{ + + /** + * The fixed OpenRegister namespace UUID for Tables-derived object uuids. + * + * A stable, purpose-specific RFC-4122 namespace so the derivation never + * collides with any other UUIDv5 namespace in the system. It is a constant of + * the derivation contract and MUST NOT change (changing it would silently + * re-key every virtual object and break existing relation deep-links). + * + * @var string + */ + public const NAMESPACE_UUID = '4b3d2c1a-5e6f-4a7b-8c9d-0e1f2a3b4c5d'; + + /** + * Derive the deterministic uuid of the virtual object for a Tables row. + * + * @param int $tableId The id of the table the row lives in. + * @param int $rowId The Tables row id. + * + * @return string The RFC-4122 UUIDv5 string. + * + * @SuppressWarnings(PHPMD.StaticAccess) Uuid::v5/fromString are the standard symfony/uid factories. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function deriveObjectUuid(int $tableId, int $rowId): string + { + $namespace = Uuid::fromString(self::NAMESPACE_UUID); + + return Uuid::v5($namespace, sprintf('tables:%d:%d', $tableId, $rowId))->toRfc4122(); + }//end deriveObjectUuid() + + /** + * Whether an id looks like an RFC-4122 UUID (rather than a numeric rowId). + * + * @param string $id The candidate id. + * + * @return bool True when the id is a valid UUID string. + * + * @SuppressWarnings(PHPMD.StaticAccess) Uuid::isValid is the standard symfony/uid validator. + * + * @spec openspec/specs/tables-virtual-register/spec.md + */ + public function looksLikeUuid(string $id): bool + { + return Uuid::isValid($id); + }//end looksLikeUuid() +}//end class diff --git a/lib/Service/ObjectSource/TalkObjectSourceProvider.php b/lib/Service/ObjectSource/TalkObjectSourceProvider.php new file mode 100644 index 0000000000..b6066403a5 --- /dev/null +++ b/lib/Service/ObjectSource/TalkObjectSourceProvider.php @@ -0,0 +1,431 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\App\IAppManager; +use OCP\IUserSession; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by the Nextcloud Talk (spreed) app. + */ +class TalkObjectSourceProvider implements ObjectSourceProvider +{ + + /** + * NC app id whose install-state gates this provider. + * + * @var string + */ + private const REQUIRED_APP = 'spreed'; + + /** + * Talk's room manager (resolved dynamically — Talk's namespace). + * + * @var string + */ + private const MANAGER = 'OCA\\Talk\\Manager'; + + /** + * Talk's participant service (resolved dynamically — Talk's namespace). + * + * @var string + */ + private const PARTICIPANT_SERVICE = 'OCA\\Talk\\Service\\ParticipantService'; + + /** + * Constructor. + * + * @param IAppManager $appManager App availability checks. + * @param IUserSession $userSession The acting-user session. + * @param ContainerInterface $container Server container (lazy Talk-service lookup). + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly IUserSession $userSession, + private readonly ContainerInterface $container, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function getId(): string + { + return 'talk-source'; + }//end getId() + + /** + * {@inheritDoc} + * + * Gated on the Talk (spreed) app: when it is not installed the bound schema + * degrades to an empty list rather than erroring. + * + * @return bool True when the Talk app is installed. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function isEnabled(): bool + { + try { + return $this->appManager->isInstalled(self::REQUIRED_APP); + } catch (Throwable $e) { + return false; + } + }//end isEnabled() + + /** + * {@inheritDoc} + * + * MUST return null when the conversation is absent OR the acting user is not + * a participant, so the two cases are indistinguishable (no enumeration + * oracle). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The Talk room token. + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + foreach ($this->readRooms() as $room) { + if ((string) ($room['id'] ?? '') === $id) { + return $this->toObjectEntity(register: $register, schema: $schema, room: $room); + } + } + + return null; + }//end find() + + /** + * {@inheritDoc} + * + * Honours `limit` and `offset`. The result is scoped by Talk's `Manager` to + * the acting user's own conversations. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (limit/offset). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity[] The matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $limit = (int) ($query['limit'] ?? 200); + $offset = (int) ($query['offset'] ?? 0); + + $rooms = array_slice($this->readRooms(), $offset, $limit); + + $objects = []; + foreach ($rooms as $room) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, room: $room); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (limit/offset). + * @param array $config The object-source config block (unused). + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Read the Talk rooms the acting user participates in, failing closed to an + * empty list. + * + * Every access is guarded so an absent Talk app or a per-room read failure + * degrades to an empty/partial list rather than erroring. + * + * @return array> The normalised room arrays. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function readRooms(): array + { + $user = $this->userSession->getUser(); + if ($user === null) { + return []; + } + + $manager = $this->resolveService(class: self::MANAGER); + if ($manager === null) { + return []; + } + + try { + $rooms = $manager->getRoomsForUser($user->getUID()); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:talk-source] could not list rooms: '.$e->getMessage()); + return []; + } + + $participantService = $this->resolveService(class: self::PARTICIPANT_SERVICE); + + $result = []; + foreach ($rooms as $room) { + if (is_object($room) === false) { + continue; + } + + $result[] = $this->normaliseRoom(room: $room, userId: $user->getUID(), participantService: $participantService); + } + + return $result; + }//end readRooms() + + /** + * Normalise a single Talk room entity into a flat array. + * + * @param object $room The Talk room entity. + * @param string $userId The acting user's uid (for the display name). + * @param object|null $participantService Talk's participant service (may be null). + * + * @return array The normalised room array. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function normaliseRoom(object $room, string $userId, ?object $participantService): array + { + return [ + 'id' => $this->stringGetter(entity: $room, getter: 'getToken'), + 'name' => $this->stringGetter(entity: $room, getter: 'getName'), + 'displayName' => $this->displayName(room: $room, userId: $userId), + 'type' => $this->intGetter(entity: $room, getter: 'getType'), + 'participantCount' => $this->participantCount(room: $room, participantService: $participantService), + 'lastActivity' => $this->lastActivity(room: $room), + ]; + }//end normaliseRoom() + + /** + * Resolve one of Talk's service classes from the container, or null when + * Talk is not installed / its service cannot be resolved. + * + * @param string $class The fully-qualified Talk service class name. + * + * @return object|null The resolved service, or null. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function resolveService(string $class): ?object + { + if (class_exists($class) === false) { + return null; + } + + try { + $service = $this->container->get($class); + if (is_object($service) === true) { + return $service; + } + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:talk-source] could not resolve '.$class.': '.$e->getMessage()); + } + + return null; + }//end resolveService() + + /** + * Resolve the acting-user display name for a room, defaulting to the room + * name when the per-user getter is unavailable. + * + * @param object $room The Talk room entity. + * @param string $userId The acting user's uid. + * + * @return string The display name, or ''. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function displayName(object $room, string $userId): string + { + try { + return (string) $room->getDisplayName($userId); + } catch (Throwable $e) { + return $this->stringGetter(entity: $room, getter: 'getName'); + } + }//end displayName() + + /** + * Resolve the participant count for a room, defaulting to 0 when the + * participant service is unavailable. + * + * @param object $room The Talk room entity. + * @param object|null $participantService Talk's participant service (may be null). + * + * @return int The participant count. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function participantCount(object $room, ?object $participantService): int + { + if ($participantService === null) { + return 0; + } + + try { + return (int) $participantService->getNumberOfActors($room); + } catch (Throwable $e) { + return 0; + } + }//end participantCount() + + /** + * Resolve a room's last-activity timestamp as an ISO-8601 string, or null. + * + * @param object $room The Talk room entity. + * + * @return string|null The ISO-8601 last activity, or null. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function lastActivity(object $room): ?string + { + try { + $activity = $room->getLastActivity(); + if ($activity instanceof \DateTimeInterface) { + return $activity->format(\DateTime::ATOM); + } + } catch (Throwable $e) { + // Getter missing on this Talk version — leave null. + } + + return null; + }//end lastActivity() + + /** + * Read an integer getter from a Talk entity, guarded. + * + * @param object $entity The Talk entity. + * @param string $getter The getter method name. + * + * @return int|null The integer value, or null when unavailable. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function intGetter(object $entity, string $getter): ?int + { + try { + $value = $entity->$getter(); + if ($value === null) { + return null; + } + + return (int) $value; + } catch (Throwable $e) { + return null; + } + }//end intGetter() + + /** + * Read a string getter from a Talk entity, defaulting to '' on failure. + * + * @param object $entity The Talk entity. + * @param string $getter The getter method name. + * + * @return string The string value, or ''. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function stringGetter(object $entity, string $getter): string + { + try { + return (string) $entity->$getter(); + } catch (Throwable $e) { + return ''; + } + }//end stringGetter() + + /** + * Map a normalised Talk room array onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param array $room The normalised room array. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-5.1 + */ + private function toObjectEntity(Register $register, Schema $schema, array $room): ObjectEntity + { + $token = (string) ($room['id'] ?? ''); + + $entity = new ObjectEntity(); + $entity->setUuid($token); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($room); + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/UserDirectoryObjectSourceProvider.php b/lib/Service/ObjectSource/UserDirectoryObjectSourceProvider.php new file mode 100644 index 0000000000..fa00369027 --- /dev/null +++ b/lib/Service/ObjectSource/UserDirectoryObjectSourceProvider.php @@ -0,0 +1,282 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @link https://conduction.nl + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Read-only object-source provider backed by the Nextcloud user directory. + */ +class UserDirectoryObjectSourceProvider implements ObjectSourceProvider +{ + /** + * Constructor. + * + * @param IUserManager $userManager Nextcloud user directory (search/get/count). + * @param IUserSession $userSession Acting-user session for read-scoping. + * @param IGroupManager $groupManager Group manager, used only for the admin check. + * @param LoggerInterface $logger Logger for read failures. + * + * @return void + */ + public function __construct( + private readonly IUserManager $userManager, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @return string The provider id. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + public function getId(): string + { + return 'user-directory-source'; + }//end getId() + + /** + * {@inheritDoc} + * + * The Nextcloud user directory is a core service, so this provider is always + * available. + * + * @return bool Always true. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + public function isEnabled(): bool + { + return true; + }//end isEnabled() + + /** + * {@inheritDoc} + * + * MUST return null when the user is absent OR the acting user may not read it, + * so the two cases are indistinguishable (no enumeration oracle). + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param string $id The user uid. + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity|null The virtual object, or null when absent/denied. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + public function find(Register $register, Schema $schema, string $id, array $config=[]): ?ObjectEntity + { + try { + $user = $this->userManager->get($id); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:user-directory-source] could not read user: '.$e->getMessage()); + return null; + } + + if ($user === null || $this->mayRead(user: $user) === false) { + return null; + } + + return $this->toObjectEntity(register: $register, schema: $schema, user: $user); + }//end find() + + /** + * {@inheritDoc} + * + * Honours `filters.search`/`_search`, `limit` and `offset`. An admin sees + * every user; a plain user sees only themselves. + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search/limit/offset). + * @param array $config The object-source config block (unused). + * + * @return ObjectEntity[] The matching virtual objects. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $config reserved for future scoping options. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + public function findAll(Register $register, Schema $schema, array $query=[], array $config=[]): array + { + $objects = []; + foreach ($this->readUsers(query: $query) as $user) { + $objects[] = $this->toObjectEntity(register: $register, schema: $schema, user: $user); + } + + return $objects; + }//end findAll() + + /** + * {@inheritDoc} + * + * @param Register $register The register the schema belongs to. + * @param Schema $schema The sourced schema. + * @param array $query Query (filters/search). + * @param array $config The object-source config block (unused). + * + * @return int The number of matching virtual objects. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + public function count(Register $register, Schema $schema, array $query=[], array $config=[]): int + { + return count($this->findAll(register: $register, schema: $schema, query: $query, config: $config)); + }//end count() + + /** + * Read the users visible to the acting user, failing closed to an empty list. + * + * Admins get the full directory (optionally filtered by a search term); a + * plain user gets only their own account. + * + * @param array $query Query (filters/search/limit/offset). + * + * @return array The visible users. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + private function readUsers(array $query): array + { + $acting = $this->userSession->getUser(); + if ($acting === null) { + return []; + } + + $search = (string) ($query['filters']['search'] ?? $query['_search'] ?? $query['search'] ?? ''); + $limit = (int) ($query['limit'] ?? 200); + $offset = (int) ($query['offset'] ?? 0); + + try { + // A plain user only ever sees their own account. + if ($this->isAdmin(user: $acting) === false) { + if ($search !== '' && str_contains(strtolower($acting->getUID()), strtolower($search)) === false) { + return []; + } + + return [$acting]; + } + + return array_values($this->userManager->search($search, $limit, $offset)); + } catch (Throwable $e) { + $this->logger->warning('[ObjectSource:user-directory-source] could not list users: '.$e->getMessage()); + return []; + } + }//end readUsers() + + /** + * Whether the acting user may read the given user's projection. + * + * Admins may read any user; a plain user may read only themselves. + * + * @param IUser $user The user being read. + * + * @return bool True when the acting user may read it. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + private function mayRead(IUser $user): bool + { + $acting = $this->userSession->getUser(); + if ($acting === null) { + return false; + } + + if ($this->isAdmin(user: $acting) === true) { + return true; + } + + return ($acting->getUID() === $user->getUID()); + }//end mayRead() + + /** + * Whether the given user is a Nextcloud administrator. + * + * @param IUser $user The user to check. + * + * @return bool True when the user is an admin. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + private function isAdmin(IUser $user): bool + { + try { + return $this->groupManager->isAdmin($user->getUID()); + } catch (Throwable $e) { + return false; + } + }//end isAdmin() + + /** + * Map a Nextcloud user onto a non-persisted virtual ObjectEntity. + * + * @param Register $register The register. + * @param Schema $schema The sourced schema. + * @param IUser $user The Nextcloud user. + * + * @return ObjectEntity The virtual object (never saved). + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-3.1 + */ + private function toObjectEntity(Register $register, Schema $schema, IUser $user): ObjectEntity + { + $uid = $user->getUID(); + + $data = [ + 'id' => $uid, + 'displayName' => $user->getDisplayName(), + 'email' => ($user->getEMailAddress() ?? ''), + ]; + + $entity = new ObjectEntity(); + $entity->setUuid($uid); + $entity->setRegister((string) $register->getId()); + $entity->setSchema((string) $schema->getId()); + $entity->setObject($data); + + return $entity; + }//end toObjectEntity() +}//end class diff --git a/lib/Service/ObjectSource/WritableObjectSourceProvider.php b/lib/Service/ObjectSource/WritableObjectSourceProvider.php new file mode 100644 index 0000000000..d6713d4e13 --- /dev/null +++ b/lib/Service/ObjectSource/WritableObjectSourceProvider.php @@ -0,0 +1,85 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://conduction.nl + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\ObjectSource; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; + +/** + * Write capability for object-source providers (opt-in per provider). + */ +interface WritableObjectSourceProvider extends ObjectSourceProvider +{ + /** + * Insert a new row/entity in the external system. + * + * @param Register $register The register owning the schema. + * @param Schema $schema The sourced schema. + * @param array $data The validated object data. + * @param array $config The object-source config block. + * + * @return ObjectEntity The created entity as re-read from the external system. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function insert(Register $register, Schema $schema, array $data, array $config=[]): ObjectEntity; + + /** + * Update an existing row/entity in the external system. + * + * @param Register $register The register owning the schema. + * @param Schema $schema The sourced schema. + * @param string $id The object id (external key, possibly composite-joined). + * @param array $data The validated object data. + * @param array $config The object-source config block. + * + * @return ObjectEntity The updated entity as re-read from the external system. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function update(Register $register, Schema $schema, string $id, array $data, array $config=[]): ObjectEntity; + + /** + * Delete a row/entity in the external system (hard delete). + * + * @param Register $register The register owning the schema. + * @param Schema $schema The sourced schema. + * @param string $id The object id (external key, possibly composite-joined). + * @param array $config The object-source config block. + * + * @return bool True when a row was deleted; false when no row matched. + * + * @spec openspec/specs/dbal-virtual-registers/spec.md + */ + public function remove(Register $register, Schema $schema, string $id, array $config=[]): bool; +}//end interface diff --git a/lib/Service/OpenProjectLinkService.php b/lib/Service/OpenProjectLinkService.php index be3d1a7d48..45ed1aa7db 100644 --- a/lib/Service/OpenProjectLinkService.php +++ b/lib/Service/OpenProjectLinkService.php @@ -75,8 +75,13 @@ * user session + logger. Each dependency is required for one of the * Tier-2 flows (link, create, unlink, list, picker, cache refresh, * graceful degradation). - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Tier-2 service implements link/createAndLink/unlink/list/picker/refresh/hydrateLink/fetchWorkPackage/normalise — all are required faces of the OpenProject integration surface; splitting into separate classes would break the single-service lazy-resolution contract. - * @SuppressWarnings(PHPMD.LongVariable) $openProjectLinkMapper follows the repo naming convention for OR link-table mappers; $workPackageId is the exact term used by the OpenProject API and abbreviating it would misalign with upstream documentation. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Tier-2 service implements + * link/createAndLink/unlink/list/picker/refresh/hydrateLink/fetchWorkPackage/normalise — all are + * required faces of the OpenProject integration surface; splitting into separate classes would break + * the single-service lazy-resolution contract. + * @SuppressWarnings(PHPMD.LongVariable) $openProjectLinkMapper follows the repo naming convention for + * OR link-table mappers; $workPackageId is the exact term used by the OpenProject API and abbreviating + * it would misalign with upstream documentation. */ class OpenProjectLinkService { @@ -509,7 +514,10 @@ private function unavailable(ProviderUnavailableException $exception): Exception * * @return array> * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) normaliseList() probes four distinct OpenProject response envelope shapes (results/items/_embedded/elements) and handles the `_embedded.elements` nesting variant; each probe is required to support the differing API versions returned by the router. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) normaliseList() probes four distinct OpenProject + * response envelope shapes (results/items/_embedded/elements) and handles the `_embedded.elements` + * nesting variant; each probe is required to support the differing API versions returned by the + * router. */ private function normaliseList(array $response): array { diff --git a/lib/Service/OrganisationService.php b/lib/Service/OrganisationService.php index c4d41358c1..e2f7b258e3 100644 --- a/lib/Service/OrganisationService.php +++ b/lib/Service/OrganisationService.php @@ -896,6 +896,48 @@ public function hasAccessToOrganisation(string $organisationUuid): bool } }//end hasAccessToOrganisation() + /** + * Whether a user administers an organisation (may manage its shared resources). + * + * Owner-level authority: a Nextcloud administrator, or the organisation's + * `owner`. This is the gate for creating, updating, or deleting an + * organisation-scoped credential (credential-broker-organisation-scope D4). + * + * follow-up: OpenRegister exposes only owner-level organisation authority + * today (the `owner` field). When finer organisation roles land (an + * administrators list / an authorization "admin" group), widen this check to + * honour them — see the change's Follow-ups. + * + * @param string $organisationUuid The organisation UUID. + * @param string|null $userId The user id to test, or null for the current session user. + * + * @return bool True when the user is an org owner or a Nextcloud admin. + * + * @spec openspec/specs/credential-broker/spec.md + */ + public function isOrganisationAdmin(string $organisationUuid, ?string $userId=null): bool + { + if ($userId === null) { + $userId = $this->getCurrentUser()?->getUID(); + } + + if ($userId === null || $userId === '') { + return false; + } + + if ($this->groupManager->isAdmin($userId) === true) { + return true; + } + + try { + $organisation = $this->organisationMapper->findByUuid($organisationUuid); + } catch (DoesNotExistException $e) { + return false; + } + + return $organisation->getOwner() === $userId; + }//end isOrganisationAdmin() + /** * Get user organisation statistics * diff --git a/lib/Service/PhotoLinkService.php b/lib/Service/PhotoLinkService.php index a8220e7060..16d7060906 100644 --- a/lib/Service/PhotoLinkService.php +++ b/lib/Service/PhotoLinkService.php @@ -301,7 +301,7 @@ public function getLinkedAlbums(string $objectUuid): array } $row = $link->jsonSerialize(); - $row['url'] = $this->albumDeepLink(albumId: (int) ($row['albumId'] ?? 0)); + $row['url'] = $this->albumDeepLink(albumName: (string) ($row['albumName'] ?? '')); $results[] = $row; } @@ -377,9 +377,9 @@ private function pickerRowFromAlbum(object $album, ?string $needle): ?array return [ 'id' => $albumId, 'name' => $name, - 'coverPhotoUrl' => $this->albumDeepLink(albumId: $albumId), + 'coverPhotoUrl' => $this->albumDeepLink(albumName: $name), 'photoCount' => null, - 'url' => $this->albumDeepLink(albumId: $albumId), + 'url' => $this->albumDeepLink(albumName: $name), ]; }//end pickerRowFromAlbum() @@ -472,7 +472,7 @@ private function fetchAlbumInfo(int $albumId, string $uid): ?array return [ 'name' => (string) $album->getTitle(), - 'coverPhotoUrl' => $this->albumDeepLink(albumId: $albumId), + 'coverPhotoUrl' => $this->albumDeepLink(albumName: (string) $album->getTitle()), 'photoCount' => $photoCount, 'lastEdited' => $lastEdited, ]; @@ -501,12 +501,17 @@ private function countPhotos(object $mapper, int $albumId, string $uid): ?int /** * Build the NC Photos deep link for an album. * - * @param int $albumId The album id. + * NC Photos routes albums by NAME (`/apps/photos/albums/{name}`), not by the + * numeric id — verified live: `/albums/6` opens an empty "Album 6", while + * `/albums/{name}` opens the real album. The name is URL-encoded so spaces + * survive as a single path segment. + * + * @param string $albumName The album name. * * @return string */ - private function albumDeepLink(int $albumId): string + private function albumDeepLink(string $albumName): string { - return $this->urlGenerator->linkToRoute('photos.page.index').'albums/'.$albumId; + return $this->urlGenerator->linkToRoute('photos.page.index').'albums/'.rawurlencode($albumName); }//end albumDeepLink() }//end class diff --git a/lib/Service/PollLinkService.php b/lib/Service/PollLinkService.php index 447f81bfd9..16b4d7efad 100644 --- a/lib/Service/PollLinkService.php +++ b/lib/Service/PollLinkService.php @@ -35,7 +35,7 @@ * @version GIT: * @link https://OpenRegister.app * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes PollLinkMapper, IDBConnection, + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Composes PollLinkMapper, IDBConnection, * IAppManager, IUserSession, and LoggerInterface; each is a distinct orchestration * concern for direct-DB poll creation and user-session handling. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Direct SQL inserts into oc_polls_polls @@ -117,7 +117,7 @@ public function isPollsAvailable(): bool * * @throws Exception On missing user, missing poll (404), duplicate (409). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function linkPoll(string $objectUuid, int $registerId, int $schemaId, int $pollId): PollLink { @@ -172,7 +172,7 @@ public function linkPoll(string $objectUuid, int $registerId, int $schemaId, int * * @throws Exception When no matching link is found (404). * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function unlinkPoll(string $objectUuid, int $pollId): void { @@ -200,7 +200,7 @@ public function unlinkPoll(string $objectUuid, int $pollId): void * available/unavailable) combined with per-link row hydration requires nested guards; * the logic cannot be split without losing the "degrade gracefully" contract. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getLinkedPolls(string $objectUuid): array { @@ -226,7 +226,10 @@ public function getLinkedPolls(string $objectUuid): array } } - $row['url'] = '/index.php/apps/polls/vote/'.($row['pollId'] ?? ''); + // NC Polls routes votes at /apps/polls/vote/{id}; the legacy + // /index.php prefix breaks under the SPA's (the path + // doubled to /apps/polls/index.php/apps/polls/vote/… on navigation). + $row['url'] = '/apps/polls/vote/'.($row['pollId'] ?? ''); $results[] = $row; }//end foreach @@ -243,7 +246,7 @@ public function getLinkedPolls(string $objectUuid): array * * @return array * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function getAvailablePolls(?string $search=null): array { @@ -314,7 +317,7 @@ public function getAvailablePolls(?string $search=null): array * * @throws Exception On missing user, Polls unavailable, or create failure. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) PHPMD 2.x accumulates the complexity + * @SuppressWarnings(PHPMD.CyclomaticComplexity) PHPMD 2.x accumulates the complexity * of insertPollRecord and buildPollLink (extracted helpers) into this orchestrator; * the method itself only validates preconditions and delegates to those helpers. * @SuppressWarnings(PHPMD.ExcessiveParameterList) Poll creation requires seven distinct @@ -322,7 +325,7 @@ public function getAvailablePolls(?string $search=null): array * deadline); grouping them into a DTO would add an extra abstraction layer with no * functional benefit since callers already have all values as discrete variables. * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-1 + * @spec openspec/specs/generic-integrations/spec.md */ public function createAndLinkPoll( string $objectUuid, @@ -397,7 +400,10 @@ private function insertPollRecord( ?DateTimeInterface $deadline ): int { $now = time(); - $expire = ($deadline !== null) ? $deadline->getTimestamp() : 0; + $expire = 0; + if ($deadline !== null) { + $expire = $deadline->getTimestamp(); + } try { $insert = $this->db->getQueryBuilder(); @@ -447,15 +453,15 @@ private function insertPollRecord( /** * Assemble an unsaved PollLink entity from poll creation data. * - * @param string $objectUuid Parent OR object uuid. - * @param int $registerId OR register id. - * @param int $schemaId OR schema id. - * @param int $pollId Newly created Polls poll id. - * @param string $title Poll title. + * @param string $objectUuid Parent OR object uuid. + * @param int $registerId OR register id. + * @param int $schemaId OR schema id. + * @param int $pollId Newly created Polls poll id. + * @param string $title Poll title. * @param string $normalisedType Normalised poll type. - * @param array $options Option labels. - * @param DateTimeInterface|null $deadline Optional deadline. - * @param string $uid Owner user id. + * @param array $options Option labels. + * @param DateTimeInterface|null $deadline Optional deadline. + * @param string $uid Owner user id. * * @return PollLink The unsaved entity. * diff --git a/lib/Service/ProcessingLogService.php b/lib/Service/ProcessingLogService.php new file mode 100644 index 0000000000..dd8cb43ba8 --- /dev/null +++ b/lib/Service/ProcessingLogService.php @@ -0,0 +1,553 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use DateTime; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\ProcessingLogEntry; +use OCA\OpenRegister\Db\ProcessingLogMapper; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\Verwerkingsactiviteit; +use OCA\OpenRegister\Db\VerwerkingsactiviteitMapper; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Buffered, fail-soft processing-log writer for object reads/exports. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ +class ProcessingLogService +{ + + /** + * Annotation key carrying the new declarative processing dialect. + * + * @var string + */ + public const ANNOTATION_KEY = 'x-openregister-processing'; + + /** + * Legacy single-string attribution annotation (shorthand). + * + * @var string + */ + public const LEGACY_ANNOTATION_KEY = 'x-openregister-processing-activity'; + + /** + * Code of the per-organisation seeded flagged fallback activity. + * + * @var string + */ + public const FALLBACK_CODE = 'niet-geclassificeerde-verwerking'; + + /** + * In-request buffer of pending entries (flushed after the response). + * + * @var array + */ + private array $buffer = []; + + /** + * Constructor. + * + * @param ProcessingLogMapper $logMapper Append-only log mapper. + * @param VerwerkingsactiviteitMapper $vrwMapper Activity resolver + fallback seed. + * @param SchemaMapper $schemaMapper Schema annotation reader. + * @param RegisterMapper $registerMapper Register annotation reader (inheritance). + * @param IUserSession $userSession Actor identity. + * @param LoggerInterface $logger Fail-soft diagnostics. + */ + public function __construct( + private readonly ProcessingLogMapper $logMapper, + private readonly VerwerkingsactiviteitMapper $vrwMapper, + private readonly SchemaMapper $schemaMapper, + private readonly RegisterMapper $registerMapper, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Record a single object read/export if its schema opts in. + * + * Fail-soft: never throws. Buffers the entry; nothing is written to + * the database until `flush()` runs. + * + * @param ObjectEntity $object The object being processed. + * @param string $action `read` | `export`. + * @param string|null $channel Access channel; defaults from the request shape. + * @param string|null $actor Explicit actor (e.g. API client id) or null for the NC user. + * + * @return void + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + public function logRead( + ObjectEntity $object, + string $action='read', + ?string $channel=null, + ?string $actor=null + ): void { + try { + $config = $this->resolveProcessingConfig(schemaId: $object->getSchema(), registerId: $object->getRegister()); + if ($config === null || ($config['logReads'] ?? false) !== true) { + return; + } + + $activityUuid = $this->resolveAttribution( + config: $config, + action: $action, + organisationId: (string) $object->getOrganisation() + ); + + $confidential = false; + $subject = $this->extractSubject(object: $object, config: $config); + + $entry = new ProcessingLogEntry(); + $entry->setActivityId($activityUuid); + $entry->setAction($action); + $entry->setActor($actor ?? $this->currentActor()); + $entry->setChannel($channel ?? $this->detectChannel()); + $entry->setRegisterId((string) $object->getRegister()); + $entry->setSchemaId((string) $object->getSchema()); + $entry->setObjectUuid((string) $object->getUuid()); + $entry->setSubjectIdType($subject['type']); + $entry->setSubjectIdValue($subject['value']); + $entry->setObjectCount(1); + $entry->setConfidential($confidential); + $entry->setOrganisationId((string) $object->getOrganisation()); + + $this->buffer[] = $entry; + } catch (\Throwable $e) { + // Fail-soft: logging never breaks the read. + $this->logger->debug( + message: '[AVG] processing-log capture skipped', + context: ['exception' => $e->getMessage()] + ); + }//end try + + }//end logRead() + + /** + * Record a single collapsed entry for a list/search result. + * + * A list response produces ONE entry carrying `objectCount` — never + * one row per scanned object. Per-object subject identifiers are + * only attached when the result set is small (<= 100); otherwise the + * entry stays an aggregate count. + * + * @param array $objects The list result. + * @param string $action `read` | `export`. + * @param string|null $channel Access channel. + * @param string|null $actor Explicit actor or null. + * + * @return void + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + public function logReadList( + array $objects, + string $action='read', + ?string $channel=null, + ?string $actor=null + ): void { + if ($objects === []) { + return; + } + + try { + // Anchor on the first object to resolve schema/register opt-in. + $first = $objects[array_key_first($objects)]; + if (($first instanceof ObjectEntity) === false) { + return; + } + + $config = $this->resolveProcessingConfig(schemaId: $first->getSchema(), registerId: $first->getRegister()); + if ($config === null || ($config['logReads'] ?? false) !== true) { + return; + } + + $activityUuid = $this->resolveAttribution( + config: $config, + action: $action, + organisationId: (string) $first->getOrganisation() + ); + + $entry = new ProcessingLogEntry(); + $entry->setActivityId($activityUuid); + $entry->setAction($action); + $entry->setActor($actor ?? $this->currentActor()); + $entry->setChannel($channel ?? $this->detectChannel()); + $entry->setRegisterId((string) $first->getRegister()); + $entry->setSchemaId((string) $first->getSchema()); + $entry->setObjectCount(count($objects)); + $entry->setConfidential(false); + $entry->setOrganisationId((string) $first->getOrganisation()); + + // Only attach a subject identifier when the set is small AND + // homogeneous enough that a single identifier is meaningful; + // for safety we leave the identifier null on collapsed lists + // and rely on objectCount (per-object identifiers for <=100 + // sets are emitted as individual entries by callers that need + // betrokkene-level granularity). + $this->buffer[] = $entry; + } catch (\Throwable $e) { + $this->logger->debug( + message: '[AVG] processing-log list capture skipped', + context: ['exception' => $e->getMessage()] + ); + }//end try + + }//end logReadList() + + /** + * Flush the in-request buffer as one batched append. + * + * Fail-soft: on a flush failure the buffer is retained so a caller / + * background retry can attempt it again; the primary action is never + * affected because flush runs after the response. + * + * @return int Number of entries persisted (0 on failure or empty buffer). + * + * @spec openspec/specs/avg-verwerkingsregister/spec.md + */ + public function flush(): int + { + if ($this->buffer === []) { + return 0; + } + + try { + $count = $this->logMapper->insertBatch(entries: $this->buffer); + $this->buffer = []; + return $count; + } catch (\Throwable $e) { + $this->logger->error( + message: '[AVG] processing-log flush failed; entries retained for retry', + context: ['exception' => $e->getMessage(), 'pending' => count($this->buffer)] + ); + return 0; + } + + }//end flush() + + /** + * Number of entries currently buffered (test/diagnostic helper). + * + * @return int + * + * @spec exclude Test/diagnostic accessor for the in-request buffer; no behaviour. + */ + public function pendingCount(): int + { + return count($this->buffer); + + }//end pendingCount() + + /** + * Resolve the effective processing config for a schema, inheriting + * the new dialect or the legacy string from schema then register. + * + * @param string|int|null $schemaId Schema identifier. + * @param string|int|null $registerId Register identifier. + * + * @return array|null Normalised config, or null when none. + */ + private function resolveProcessingConfig($schemaId, $registerId): ?array + { + $schemaConfig = $this->readAnnotation(loader: fn() => $this->loadSchemaConfig(schemaId: $schemaId)); + $registerConfig = $this->readAnnotation(loader: fn() => $this->loadRegisterConfig(registerId: $registerId)); + + if ($schemaConfig === null && $registerConfig === null) { + return null; + } + + // Schema-level wins; fall back to register-level field by field. + $effective = ($registerConfig ?? []); + if ($schemaConfig !== null) { + $effective = array_merge($effective, $schemaConfig); + // Attribution merges per-key, schema overriding register. + $effective['attribution'] = array_merge( + ($registerConfig['attribution'] ?? []), + ($schemaConfig['attribution'] ?? []) + ); + } + + return $effective; + + }//end resolveProcessingConfig() + + /** + * Normalise an annotation block from either the new dialect or the + * legacy single-string key. + * + * @param callable $loader Returns the raw configuration array or null. + * + * @return array|null + */ + private function readAnnotation(callable $loader): ?array + { + $config = $loader(); + if (is_array($config) === false) { + return null; + } + + $dialect = ($config[self::ANNOTATION_KEY] ?? null); + if (is_array($dialect) === true) { + return [ + 'logReads' => (($dialect['logReads'] ?? false) === true), + 'attribution' => $this->asArray(value: ($dialect['attribution'] ?? null)), + 'subjectIdFields' => $this->asArray(value: ($dialect['subjectIdFields'] ?? null)), + ]; + } + + // Legacy shorthand: a single attribution reference, reads off. + $legacy = ($config[self::LEGACY_ANNOTATION_KEY] ?? null); + if (is_string($legacy) === true && $legacy !== '') { + return [ + 'logReads' => false, + 'attribution' => ['default' => $legacy], + 'subjectIdFields' => [], + ]; + } + + return null; + + }//end readAnnotation() + + /** + * Resolve the attributed activity uuid for an action, falling back + * to the seeded flagged fallback activity when nothing resolves. + * + * @param array $config Normalised processing config. + * @param string $action Operation (`read`|`export`|...). + * @param string $organisationId Tenant for the fallback seed. + * + * @return string Resolved (or fallback) activity uuid. + */ + private function resolveAttribution(array $config, string $action, string $organisationId): string + { + $attribution = $this->asArray(value: ($config['attribution'] ?? null)); + $reference = ($attribution[$action] ?? $attribution['default'] ?? null); + + if (is_string($reference) === true && $reference !== '') { + $resolved = $this->vrwMapper->resolveReference(reference: $reference); + // A draft/retired/non-existent target falls through to fallback. + if ($resolved !== null && $this->isAttributable(activity: $resolved) === true) { + return (string) $resolved->getUuid(); + } + } + + return $this->fallbackActivityUuid(organisationId: $organisationId); + + }//end resolveAttribution() + + /** + * Whether an activity may receive new attribution. + * + * `retired`/`archived` activities remain resolvable for history but + * MUST NOT accept new processing — they fall back instead. + * + * @param Verwerkingsactiviteit $activity Candidate activity. + * + * @return bool + */ + private function isAttributable(Verwerkingsactiviteit $activity): bool + { + $status = (string) $activity->getStatus(); + return in_array($status, ['retired', 'archived'], true) === false; + + }//end isAttributable() + + /** + * Find-or-create the per-organisation flagged fallback activity. + * + * @param string $organisationId Tenant scope. + * + * @return string Fallback activity uuid. + */ + private function fallbackActivityUuid(string $organisationId): string + { + $existing = $this->vrwMapper->findByCode(code: self::FALLBACK_CODE); + if ($existing !== null) { + return (string) $existing->getUuid(); + } + + $activity = new Verwerkingsactiviteit(); + $activity->setCode(self::FALLBACK_CODE); + $activity->setNaam('Niet-geclassificeerde verwerking'); + $activity->setDoelbinding('Catch-all for processing whose activity attribution did not resolve (AVG accountability gap marker).'); + $activity->setRechtsgrond('public-task'); + if ($organisationId !== '') { + $activity->setOrganisationId($organisationId); + } + + return (string) $this->vrwMapper->insert(entity: $activity)->getUuid(); + + }//end fallbackActivityUuid() + + /** + * Extract the data-subject identifier from the object, using the + * schema-declared `subjectIdFields` map (`{ "BSN": "bsn" }`). + * + * @param ObjectEntity $object The read object. + * @param array $config Normalised processing config. + * + * @return array{type: string|null, value: string|null} + */ + private function extractSubject(ObjectEntity $object, array $config): array + { + $fields = $this->asArray(value: ($config['subjectIdFields'] ?? null)); + if ($fields === []) { + return ['type' => null, 'value' => null]; + } + + $data = $object->getObject(); + foreach ($fields as $idType => $fieldName) { + $value = ($data[$fieldName] ?? null); + if (is_scalar($value) === true && (string) $value !== '') { + return ['type' => (string) $idType, 'value' => (string) $value]; + } + } + + return ['type' => null, 'value' => null]; + + }//end extractSubject() + + /** + * Current actor identifier — NC user id, or `system` when none. + * + * @return string + */ + private function currentActor(): string + { + $user = $this->userSession->getUser(); + if ($user === null) { + return 'system'; + } + + return $user->getUID(); + + }//end currentActor() + + /** + * Best-effort channel detection from the runtime. + * + * @return string One of ui | api | public | background. + */ + private function detectChannel(): string + { + if (class_exists('\OC', false) === true && \OC::$CLI === true) { + return 'background'; + } + + if ($this->userSession->getUser() === null) { + return 'public'; + } + + return 'ui'; + + }//end detectChannel() + + /** + * Load a schema's configuration array, RBAC/multitenancy bypassed. + * + * @param string|int|null $schemaId Schema identifier. + * + * @return array|null + */ + private function loadSchemaConfig($schemaId): ?array + { + if ($schemaId === null || $schemaId === '' || $schemaId === 0) { + return null; + } + + $schema = $this->schemaMapper->find($schemaId, _rbac: false, _multitenancy: false); + return $schema->getConfiguration(); + + }//end loadSchemaConfig() + + /** + * Load a register's configuration array, RBAC/multitenancy bypassed. + * + * @param string|int|null $registerId Register identifier. + * + * @return array|null + */ + private function loadRegisterConfig($registerId): ?array + { + if ($registerId === null || $registerId === '' || $registerId === 0) { + return null; + } + + $register = $this->registerMapper->find($registerId, _multitenancy: false); + return $register->getConfiguration(); + + }//end loadRegisterConfig() + + /** + * Coerce a value to an array, returning an empty array when it is not one. + * + * @param mixed $value Candidate value. + * + * @return array + */ + private function asArray($value): array + { + if (is_array($value) === true) { + return $value; + } + + return []; + + }//end asArray() +}//end class diff --git a/lib/Service/PropertyRbacHandler.php b/lib/Service/PropertyRbacHandler.php index 04c9dcc9b6..adabc3234b 100644 --- a/lib/Service/PropertyRbacHandler.php +++ b/lib/Service/PropertyRbacHandler.php @@ -137,10 +137,20 @@ public function canUpdateProperty( * * @spec openspec/specs/row-field-level-security/spec.md#fls-strips-restricted-fields (strips unreadable * property-authorized fields from outgoing data; admin + no-property-auth short-circuit) + * @spec openspec/specs/row-field-level-security/spec.md#requirement-writeonly-properties-must-never-be-returned-on-any-read + * (writeOnly stripping runs before the admin bypass — admin is NOT exempt from writeOnly) */ public function filterReadableProperties(Schema $schema, array $object): array { - // If user is admin, return object as-is. + // Write-only stripping applies to EVERYONE, including admin: a write-only + // property (secret/token) is never returned on read. This runs BEFORE the + // admin short-circuit below precisely because admin is not exempt. It is a + // fail-safe projection: the property may have been selected by the caller + // (e.g. ?fields=apiToken) yet is still removed here because stripping is + // applied after selection in the render path. + $object = $this->stripWriteOnlyProperties(schema: $schema, object: $object); + + // If user is admin, return object as-is (writeOnly already stripped above). if ($this->isAdmin() === true) { return $object; } @@ -174,6 +184,137 @@ public function filterReadableProperties(Schema $schema, array $object): array return $object; }//end filterReadableProperties() + /** + * Strip write-only properties from outgoing object data. + * + * A property declared `writeOnly: true` (standard JSON Schema / OpenAPI + * keyword) is a write-only secret: it may be written but is NEVER returned + * on read, for ANY caller including admin. This is the correct semantic for + * secrets and tokens and is the platform-level answer to openregister#380 + * (ADR-063 MCP tools have no field projection of their own, so stripping in + * OpenRegister's read path makes them inherit the redaction automatically). + * + * Fail-safe: only opted-in properties are removed; a schema with no + * writeOnly property returns the object untouched (backward compatible). + * The render path applies this AFTER caller-supplied field/extend selection, + * so a caller cannot re-surface a stripped property by naming it. + * + * @param Schema $schema Schema containing property definitions + * @param array $object Object data to filter + * + * @return array Object data with write-only properties removed + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-writeonly-properties-must-never-be-returned-on-any-read + * (strips writeOnly properties for every reader; short-circuits when the schema has none) + */ + public function stripWriteOnlyProperties(Schema $schema, array $object): array + { + $hasProperties = $schema->hasWriteOnlyProperties(); + $paths = $schema->getWriteOnlyPaths(); + + if ($hasProperties === false && empty($paths) === true) { + return $object; + } + + if ($hasProperties === true) { + foreach ($schema->getWriteOnlyProperties() as $propertyName) { + if (array_key_exists($propertyName, $object) === true) { + unset($object[$propertyName]); + $this->logger->debug( + message: '[PropertyRbacHandler] Stripped write-only property', + context: ['file' => __FILE__, 'line' => __LINE__, 'property' => $propertyName] + ); + } + } + } + + foreach ($paths as $path) { + $object = $this->stripWriteOnlyPath(path: $path, object: $object); + } + + return $object; + }//end stripWriteOnlyProperties() + + /** + * Remove one declared write-only dot-path from an outgoing array. + * + * This handles BOTH shapes OpenRegister renders a write-only value in, + * because the same declaration has to cover both and every caller of + * stripWriteOnlyProperties() passes one or the other: + * + * 1. The object BODY, where `configuration.authentication.client_secret` + * is a nested structure to descend into and unset. + * 2. The `relations` search-index MIRROR, where SaveObject::scanForRelations() + * flattens nested values into LITERAL dot-path keys — the mirror map is + * already keyed `configuration.authentication.client_secret`. jsonSerialize() + * surfaces that map as `@self.relations`, so a nested secret leaks there + * even after the body strip, exactly as top-level writeOnly did in #429. + * + * Both operations are applied unconditionally and are mutually inert: a body + * has no literal dotted key (dots are not used in property names), and a flat + * mirror map has nothing to descend. That keeps this method total over every + * input the render path hands it, rather than relying on the caller to know + * which shape it holds. + * + * A declared path removes the value at that location AND its whole sub-tree — + * declaring `configuration.authentication` strips every key beneath it, and in + * the mirror that means every flattened key prefixed `configuration.authentication.` + * (this is what covers `configuration.authentication.keys.`, whose leaf + * segments are attacker-supplied and therefore cannot be enumerated in advance). + * + * @param string $path The declared dot-path. + * @param array $object The object body or relations mirror. + * + * @return array The array with the path removed. + * + * @spec openspec/specs/row-field-level-security/spec.md#requirement-nested-writeonly-paths-must-never-be-returned-on-any-read + */ + private function stripWriteOnlyPath(string $path, array $object): array + { + // Shape 2: the flattened relations mirror. Remove an exact dot-path key + // and every key beneath it. + $prefix = $path.'.'; + foreach (array_keys($object) as $key) { + $key = (string) $key; + if ($key === $path || str_starts_with($key, $prefix) === true) { + unset($object[$key]); + $this->logger->debug( + message: '[PropertyRbacHandler] Stripped write-only path from relations mirror', + context: ['file' => __FILE__, 'line' => __LINE__, 'path' => $key] + ); + } + } + + // Shape 1: the nested object body. Walk the path by reference and unset + // the final segment. Bail out the moment the path stops resolving — + // a declared path that is absent from this object is normal (the value + // was never set) and must not create keys on the way down. + $segments = explode('.', $path); + $leaf = array_pop($segments); + $cursor = &$object; + + foreach ($segments as $segment) { + if (is_array($cursor) === false || array_key_exists($segment, $cursor) === false) { + unset($cursor); + return $object; + } + + $cursor = &$cursor[$segment]; + } + + if (is_array($cursor) === true && array_key_exists($leaf, $cursor) === true) { + unset($cursor[$leaf]); + $this->logger->debug( + message: '[PropertyRbacHandler] Stripped write-only path', + context: ['file' => __FILE__, 'line' => __LINE__, 'path' => $path] + ); + } + + unset($cursor); + + return $object; + }//end stripWriteOnlyPath() + /** * Validate writable properties on incoming data * diff --git a/lib/Service/Quality/DedupAnnotationValidator.php b/lib/Service/Quality/DedupAnnotationValidator.php new file mode 100644 index 0000000000..5c5bafa4c0 --- /dev/null +++ b/lib/Service/Quality/DedupAnnotationValidator.php @@ -0,0 +1,128 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Quality; + +/** + * Shape validator for the `x-openregister-dedup` annotation. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-5 + */ +class DedupAnnotationValidator +{ + /** + * Recognised match methods. + * + * @var array + */ + private const VALID_METHODS = ['exact', 'normalized', 'levenshtein']; + + /** + * Validate the `x-openregister-dedup` annotation in a schema shape. + * + * @param array $schema Shape with `properties` and `x-openregister-dedup`. + * + * @return array Errors; empty when valid. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-5 + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) The branch count tracks the annotation's + * optional top-level fields (matchRules presence, threshold, blockingKeys) plus the + * per-rule loop; each is a distinct, independently-testable guard. + * @SuppressWarnings(PHPMD.NPathComplexity) Same rationale — flat sequential guards. + */ + public function validate(array $schema): array + { + $annotation = ($schema['x-openregister-dedup'] ?? null); + if ($annotation === null) { + return []; + } + + if (is_array($annotation) === false) { + return [['code' => 'dedup.not-object', 'message' => 'x-openregister-dedup must be an object.']]; + } + + $matchRules = ($annotation['matchRules'] ?? null); + if (is_array($matchRules) === false || count($matchRules) === 0) { + return [['code' => 'dedup.no-rules', 'message' => 'x-openregister-dedup requires a non-empty "matchRules" array.']]; + } + + $errors = []; + foreach ($matchRules as $index => $rule) { + $errors = array_merge($errors, $this->validateRule(rule: $rule, index: (int) $index)); + } + + if (array_key_exists('threshold', $annotation) === true && is_numeric($annotation['threshold']) === false) { + $errors[] = ['code' => 'dedup.bad-threshold', 'message' => 'x-openregister-dedup "threshold" must be numeric.']; + } + + $blockingKeys = ($annotation['blockingKeys'] ?? null); + if ($blockingKeys !== null && is_array($blockingKeys) === false) { + $errors[] = ['code' => 'dedup.bad-blocking-keys', 'message' => 'x-openregister-dedup "blockingKeys" must be an array.']; + } + + return $errors; + }//end validate() + + /** + * Validate a single match rule. + * + * @param mixed $rule Rule definition. + * @param int $index Position in matchRules (for messaging). + * + * @return array + */ + private function validateRule($rule, int $index): array + { + if (is_array($rule) === false) { + return [['code' => 'dedup.rule-not-object', 'message' => sprintf('Match rule #%d must be an object.', $index)]]; + } + + if ((string) ($rule['field'] ?? '') === '') { + return [['code' => 'dedup.missing-field', 'message' => sprintf('Match rule #%d requires a "field".', $index)]]; + } + + $method = (string) ($rule['method'] ?? ''); + if (in_array($method, self::VALID_METHODS, true) === false) { + $valid = implode(', ', self::VALID_METHODS); + return [ + [ + 'code' => 'dedup.unknown-method', + 'message' => sprintf('Match rule #%d has unknown method "%s" (expected one of: %s).', $index, $method, $valid), + ], + ]; + } + + $errors = []; + if (array_key_exists('weight', $rule) === true && is_numeric($rule['weight']) === false) { + $errors[] = ['code' => 'dedup.bad-weight', 'message' => sprintf('Match rule #%d "weight" must be numeric.', $index)]; + } + + return $errors; + }//end validateRule() +}//end class diff --git a/lib/Service/Quality/DuplicateDetectionService.php b/lib/Service/Quality/DuplicateDetectionService.php new file mode 100644 index 0000000000..7cc8ba0c74 --- /dev/null +++ b/lib/Service/Quality/DuplicateDetectionService.php @@ -0,0 +1,442 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Quality; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Finds scored duplicate-candidate pairs in a register/schema. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-6 + */ +class DuplicateDetectionService +{ + /** + * Default similarity threshold a pair must reach to be reported. + * + * @var float + */ + private const DEFAULT_THRESHOLD = 0.85; + + /** + * Upper bound on the candidate set pulled per detection run. + * + * @var int + */ + private const MAX_CANDIDATES = 1000; + + /** + * Wire collaborators. + * + * @param ObjectService $objectService Object query path (RBAC + tenant scoped). + * @param SchemaMapper $schemaMapper Schema lookup for the dedup annotation. + * @param SimilarityCalculator $similarity Pure field-similarity primitives. + * @param LoggerInterface $logger PSR logger. + * + * @return void + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-6 + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly SchemaMapper $schemaMapper, + private readonly SimilarityCalculator $similarity, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Find duplicate-candidate pairs within a register/schema. + * + * @param int|string $register Register id, uuid or slug. + * @param int|string $schema Schema id, uuid or slug. + * @param array|null $matchRules Optional caller-supplied match rules; when + * null the schema's x-openregister-dedup rules apply. + * @param float|null $threshold Optional score cut-off; defaults to the + * annotation threshold or {@see DEFAULT_THRESHOLD}. + * + * @return array + * }> Scored candidate pairs, highest score first. Empty when nothing reaches the threshold. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-6 + */ + public function findDuplicates($register, $schema, ?array $matchRules=null, ?float $threshold=null): array + { + $config = $this->resolveConfig(schema: $schema, matchRules: $matchRules, threshold: $threshold); + if ($config === null) { + return []; + } + + [$rules, $blockingKeys, $cutOff] = $config; + + $objects = $this->loadCandidates(register: $register, schema: $schema); + if (count($objects) < 2) { + return []; + } + + $blocks = $this->partition(objects: $objects, blockingKeys: $blockingKeys); + + $pairs = []; + foreach ($blocks as $bucket) { + $this->scoreBucket(bucket: $bucket, rules: $rules, cutOff: $cutOff, pairs: $pairs); + } + + usort($pairs, static fn(array $left, array $right) => $right['score'] <=> $left['score']); + + return $pairs; + }//end findDuplicates() + + /** + * Resolve effective rules, blocking keys and threshold. + * + * @param int|string $schema Schema reference. + * @param array|null $matchRules Caller-supplied rules, or null. + * @param float|null $threshold Caller-supplied threshold, or null. + * + * @return array{0: array>, 1: array, 2: float}|null + * [rules, blockingKeys, threshold] or null when no usable rules exist. + */ + private function resolveConfig($schema, ?array $matchRules, ?float $threshold): ?array + { + $annotation = $this->loadAnnotation(schema: $schema); + + $rules = $matchRules; + if ($rules === null) { + $rules = ($annotation['matchRules'] ?? null); + } + + $rules = $this->sanitiseRules(rules: $rules); + if (count($rules) === 0) { + return null; + } + + $blockingKeys = []; + $declaredKeys = ($annotation['blockingKeys'] ?? []); + if (is_array($declaredKeys) === true) { + foreach ($declaredKeys as $key) { + if (is_string($key) === true && $key !== '') { + $blockingKeys[] = $key; + } + } + } + + $cutOff = $threshold; + if ($cutOff === null) { + $declared = ($annotation['threshold'] ?? null); + $cutOff = self::DEFAULT_THRESHOLD; + if (is_numeric($declared) === true) { + $cutOff = (float) $declared; + } + } + + return [$rules, $blockingKeys, $cutOff]; + }//end resolveConfig() + + /** + * Filter a raw rule list down to well-formed match rules. + * + * @param mixed $rules Candidate rule list. + * + * @return array> + */ + private function sanitiseRules($rules): array + { + if (is_array($rules) === false) { + return []; + } + + $clean = []; + foreach ($rules as $rule) { + if (is_array($rule) === false) { + continue; + } + + $field = (string) ($rule['field'] ?? ''); + $method = (string) ($rule['method'] ?? ''); + if ($field === '' || $method === '') { + continue; + } + + $clean[] = $rule; + } + + return $clean; + }//end sanitiseRules() + + /** + * Read the `x-openregister-dedup` annotation off a schema. + * + * @param int|string $schema Schema reference. + * + * @return array Annotation (empty array when absent / unresolvable). + */ + private function loadAnnotation($schema): array + { + try { + $entity = $this->schemaMapper->find($schema, _multitenancy: false); + } catch (Throwable $e) { + return []; + } + + $config = ($entity->getConfiguration() ?? []); + $annotation = ($config['x-openregister-dedup'] ?? null); + if (is_array($annotation) === true) { + return $annotation; + } + + return []; + }//end loadAnnotation() + + /** + * Load the candidate object set, RBAC + tenant scoped. + * + * @param int|string $register Register reference. + * @param int|string $schema Schema reference. + * + * @return array + */ + private function loadCandidates($register, $schema): array + { + try { + $objects = $this->objectService->findAll( + [ + 'filters' => [ + 'register' => $register, + 'schema' => $schema, + ], + 'limit' => self::MAX_CANDIDATES, + ] + ); + } catch (Throwable $e) { + $this->logger->warning('Duplicate detection candidate load failed: '.$e->getMessage()); + return []; + } + + $entities = []; + foreach ($objects as $object) { + if ($object instanceof ObjectEntity) { + $entities[] = $object; + } + } + + return $entities; + }//end loadCandidates() + + /** + * Partition objects into comparison buckets by blocking key. + * + * Objects whose blocking-key token is empty are dropped from blocking + * (they would otherwise dominate a single bucket). With no blocking keys + * declared, every object lands in one bucket. + * + * @param array $objects Candidate objects. + * @param array $blockingKeys Field names to block on. + * + * @return array> + */ + private function partition(array $objects, array $blockingKeys): array + { + if (count($blockingKeys) === 0) { + return ['*' => $objects]; + } + + $buckets = []; + foreach ($objects as $object) { + $data = ($object->getObject() ?? []); + $token = $this->blockingTokenFor(data: $data, keys: $blockingKeys); + if ($token === '') { + continue; + } + + $buckets[$token][] = $object; + } + + // Drop singleton buckets — a bucket of one has no pair to compare. + return array_filter($buckets, static fn(array $bucket) => count($bucket) > 1); + }//end partition() + + /** + * Build a composite blocking token across the declared keys. + * + * @param array $data Object payload. + * @param array $keys Blocking field names (plain or dotted paths). + * + * @return string Composite token, or empty when any key is absent. + * + * @spec openspec/changes/mdm-dedup-nested-paths/tasks.md#task-2 + */ + private function blockingTokenFor(array $data, array $keys): string + { + $parts = []; + foreach ($keys as $key) { + $token = $this->similarity->blockingToken('normalized', $this->resolvePath(data: $data, path: $key)); + if ($token === '') { + return ''; + } + + $parts[] = $token; + } + + return implode('|', $parts); + }//end blockingTokenFor() + + /** + * Resolve a dotted-path field value from an object payload. + * + * A plain, dot-free field resolves exactly as a direct top-level array + * read. A dotted path (e.g. `goldenRecord.email`) traverses each + * segment in order and yields `null` — never throws — as soon as any + * segment is missing or its container is not an array. Mirrors the + * dot-path idiom used by {@see QualityScorer::fieldValue()}. + * + * @param array $data Object payload. + * @param string $path Field name or dotted path. + * + * @return mixed The resolved value, or null when the path is missing. + * + * @spec openspec/changes/mdm-dedup-nested-paths/tasks.md#task-1 + */ + private function resolvePath(array $data, string $path) + { + if ($path === '') { + return null; + } + + $segments = explode('.', $path); + $cursor = $data; + foreach ($segments as $segment) { + if (is_array($cursor) === false || array_key_exists($segment, $cursor) === false) { + return null; + } + + $cursor = $cursor[$segment]; + } + + return $cursor; + }//end resolvePath() + + /** + * Score every pair within a bucket and collect those above the cut-off. + * + * @param array $bucket Objects in one block. + * @param array> $rules Match rules. + * @param float $cutOff Threshold. + * @param array> $pairs Accumulator (by reference). + * + * @return void + */ + private function scoreBucket(array $bucket, array $rules, float $cutOff, array &$pairs): void + { + $bucket = array_values($bucket); + $count = count($bucket); + + for ($i = 0; $i < $count; $i++) { + for ($j = ($i + 1); $j < $count; $j++) { + $result = $this->scorePair(a: $bucket[$i], b: $bucket[$j], rules: $rules); + if ($result['score'] >= $cutOff) { + $pairs[] = $result; + } + } + } + }//end scoreBucket() + + /** + * Score one pair of objects against the match rules. + * + * @param ObjectEntity $a First object. + * @param ObjectEntity $b Second object. + * @param array> $rules Match rules. + * + * @return array{objectA: string, objectB: string, score: float, matchedOn: array} + * + * @spec openspec/changes/mdm-dedup-nested-paths/tasks.md#task-2 + */ + private function scorePair(ObjectEntity $a, ObjectEntity $b, array $rules): array + { + $dataA = ($a->getObject() ?? []); + $dataB = ($b->getObject() ?? []); + + $weightedSum = 0.0; + $totalWeight = 0.0; + $matchedOn = []; + + foreach ($rules as $rule) { + $field = (string) ($rule['field'] ?? ''); + $method = (string) ($rule['method'] ?? ''); + $weight = 1.0; + if (is_numeric($rule['weight'] ?? null) === true) { + $weight = (float) $rule['weight']; + } + + if ($weight <= 0.0) { + continue; + } + + $sim = $this->similarity->similarity( + $method, + $this->resolvePath(data: $dataA, path: $field), + $this->resolvePath(data: $dataB, path: $field) + ); + + $weightedSum += ($sim * $weight); + $totalWeight += $weight; + + if ($sim >= 0.9) { + $matchedOn[] = $field; + } + }//end foreach + + $score = 0.0; + if ($totalWeight > 0.0) { + $score = round(($weightedSum / $totalWeight), 4); + } + + return [ + 'objectA' => (string) $a->getUuid(), + 'objectB' => (string) $b->getUuid(), + 'score' => $score, + 'matchedOn' => array_values(array_unique($matchedOn)), + ]; + }//end scorePair() +}//end class diff --git a/lib/Service/Quality/QualityAnnotationValidator.php b/lib/Service/Quality/QualityAnnotationValidator.php new file mode 100644 index 0000000000..382c92f393 --- /dev/null +++ b/lib/Service/Quality/QualityAnnotationValidator.php @@ -0,0 +1,168 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Quality; + +/** + * Shape validator for the `x-openregister-quality` annotation. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-3 + */ +class QualityAnnotationValidator +{ + /** + * Recognised rule types. + * + * @var array + */ + private const VALID_TYPES = ['required', 'format', 'freshness']; + + /** + * Validate the `x-openregister-quality` annotation in a schema shape. + * + * @param array $schema Shape with `properties` and `x-openregister-quality`. + * + * @return array Errors; empty when valid. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-3 + */ + public function validate(array $schema): array + { + $annotation = ($schema['x-openregister-quality'] ?? null); + if ($annotation === null) { + return []; + } + + if (is_array($annotation) === false) { + return [['code' => 'quality.not-object', 'message' => 'x-openregister-quality must be an object.']]; + } + + $rules = ($annotation['rules'] ?? null); + if (is_array($rules) === false || count($rules) === 0) { + return [['code' => 'quality.no-rules', 'message' => 'x-openregister-quality requires a non-empty "rules" array.']]; + } + + $errors = []; + foreach ($rules as $index => $rule) { + $errors = array_merge($errors, $this->validateRule(rule: $rule, index: (int) $index)); + } + + $errors = array_merge($errors, $this->validateThresholds(annotation: $annotation)); + + return $errors; + }//end validate() + + /** + * Validate a single rule entry. + * + * @param mixed $rule Rule definition. + * @param int $index Position in the rules array (for messaging). + * + * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) The branch count tracks the per-rule-type + * validation matrix (shape, type, field, weight, format/pattern, half-life); each branch + * emits a distinct, independently-testable error and is clearer kept inline than scattered. + * @SuppressWarnings(PHPMD.NPathComplexity) Same rationale — flat per-field guards. + */ + private function validateRule($rule, int $index): array + { + if (is_array($rule) === false) { + return [['code' => 'quality.rule-not-object', 'message' => sprintf('Rule #%d must be an object.', $index)]]; + } + + $type = (string) ($rule['type'] ?? ''); + if (in_array($type, self::VALID_TYPES, true) === false) { + return [ + [ + 'code' => 'quality.unknown-type', + 'message' => sprintf('Rule #%d has unknown type "%s" (expected one of: %s).', $index, $type, implode(', ', self::VALID_TYPES)), + ], + ]; + } + + $field = (string) ($rule['field'] ?? ''); + if ($field === '') { + return [['code' => 'quality.missing-field', 'message' => sprintf('Rule #%d (%s) requires a "field".', $index, $type)]]; + } + + $errors = []; + + if (array_key_exists('weight', $rule) === true && is_numeric($rule['weight']) === false) { + $errors[] = ['code' => 'quality.bad-weight', 'message' => sprintf('Rule #%d "weight" must be numeric.', $index)]; + } + + if ($type === 'format') { + $hasNamed = (isset($rule['format']) === true && (string) $rule['format'] !== ''); + $hasPattern = (isset($rule['pattern']) === true && (string) $rule['pattern'] !== ''); + if ($hasNamed === false && $hasPattern === false) { + $errors[] = [ + 'code' => 'quality.format-missing-spec', + 'message' => sprintf('Rule #%d (format) requires "format" or "pattern".', $index), + ]; + } + } + + if ($type === 'freshness' + && array_key_exists('halfLifeDays', $rule) === true + && is_numeric($rule['halfLifeDays']) === false + ) { + $errors[] = ['code' => 'quality.bad-half-life', 'message' => sprintf('Rule #%d "halfLifeDays" must be numeric.', $index)]; + } + + return $errors; + }//end validateRule() + + /** + * Validate the optional thresholds block. + * + * @param array $annotation Quality annotation. + * + * @return array + */ + private function validateThresholds(array $annotation): array + { + $thresholds = ($annotation['thresholds'] ?? null); + if ($thresholds === null) { + return []; + } + + if (is_array($thresholds) === false) { + return [['code' => 'quality.thresholds-not-object', 'message' => 'x-openregister-quality "thresholds" must be an object.']]; + } + + $errors = []; + foreach (['good', 'fair'] as $key) { + if (array_key_exists($key, $thresholds) === true && is_numeric($thresholds[$key]) === false) { + $errors[] = ['code' => 'quality.bad-threshold', 'message' => sprintf('Threshold "%s" must be numeric.', $key)]; + } + } + + return $errors; + }//end validateThresholds() +}//end class diff --git a/lib/Service/Quality/QualityScorer.php b/lib/Service/Quality/QualityScorer.php new file mode 100644 index 0000000000..129b0ff99f --- /dev/null +++ b/lib/Service/Quality/QualityScorer.php @@ -0,0 +1,369 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Quality; + +use DateTimeImmutable; +use Throwable; + +/** + * Computes a per-object data-quality score from declarative rules. + * + * Each rule yields a sub-score in [0, 1]; the object score is the + * weight-normalised mean of all sub-scores. Mirrors the pure-function, + * null-safe contract of {@see \OCA\OpenRegister\Service\Calculation\CalculationEvaluator}. + * + * Supported rule types: + * - required: 1.0 when the field is present and non-empty, else 0.0. + * - format: 1.0 when the field value matches a named format or regex + * `pattern`; an absent field scores 0.0. Named formats: email, + * url, date. + * - freshness: exponential decay exp(-ageDays / halfLifeDays) of a date + * field against now; absent / unparseable date scores 0.0. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-1 + */ +class QualityScorer +{ + /** + * Default weight applied when a rule omits an explicit weight. + * + * @var float + */ + private const DEFAULT_WEIGHT = 1.0; + + /** + * Default half-life (days) for a freshness rule that omits one. + * + * @var float + */ + private const DEFAULT_HALF_LIFE_DAYS = 180.0; + + /** + * Built-in named formats mapped to PCRE patterns. + * + * @var array + */ + private const NAMED_FORMATS = [ + 'email' => '/^[^@\s]+@[^@\s]+\.[^@\s]+$/', + 'url' => '#^https?://[^\s]+$#i', + 'date' => '/^\d{4}-\d{2}-\d{2}([T ].*)?$/', + ]; + + /** + * Score an object payload against a list of quality rules. + * + * @param array $object The object's stored data. + * @param array $rules Declarative rule list. + * @param DateTimeImmutable $now Reference instant for freshness rules. + * + * @return float Quality score in [0, 1]. Returns 1.0 when no usable rule + * applies (an object with no declared quality constraints is + * trivially compliant). + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-1 + */ + public function score(array $object, array $rules, DateTimeImmutable $now): float + { + $weightedSum = 0.0; + $totalWeight = 0.0; + + foreach ($rules as $rule) { + if (is_array($rule) === false) { + continue; + } + + $type = (string) ($rule['type'] ?? ''); + if ($type === '') { + continue; + } + + $weight = $this->ruleWeight(rule: $rule); + if ($weight <= 0.0) { + continue; + } + + $sub = $this->scoreRule(object: $object, rule: $rule, type: $type, now: $now); + + $weightedSum += ($sub * $weight); + $totalWeight += $weight; + }//end foreach + + if ($totalWeight <= 0.0) { + return 1.0; + } + + $score = ($weightedSum / $totalWeight); + + // Clamp defensively; sub-scores are already bounded but float math drifts. + return max(0.0, min(1.0, round($score, 4))); + }//end score() + + /** + * Map a numeric score to a status label using optional thresholds. + * + * @param float $score Score in [0, 1]. + * @param array $thresholds Map with optional `good` / `fair` cut-offs. + * + * @return string One of `good`, `fair`, `poor`. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-1 + */ + public function status(float $score, array $thresholds): string + { + $good = $this->floatOr(value: ($thresholds['good'] ?? null), fallback: 0.8); + $fair = $this->floatOr(value: ($thresholds['fair'] ?? null), fallback: 0.5); + + if ($score >= $good) { + return 'good'; + } + + if ($score >= $fair) { + return 'fair'; + } + + return 'poor'; + }//end status() + + /** + * Dispatch a single rule to its scoring routine. + * + * @param array $object Object payload. + * @param array $rule Rule definition. + * @param string $type Rule type token. + * @param DateTimeImmutable $now Reference instant. + * + * @return float Sub-score in [0, 1]. + */ + private function scoreRule(array $object, array $rule, string $type, DateTimeImmutable $now): float + { + return match ($type) { + 'required' => $this->scoreRequired(object: $object, rule: $rule), + 'format' => $this->scoreFormat(object: $object, rule: $rule), + 'freshness' => $this->scoreFreshness(object: $object, rule: $rule, now: $now), + default => 0.0, + }; + }//end scoreRule() + + /** + * Score a required-field rule. + * + * @param array $object Object payload. + * @param array $rule Rule definition. + * + * @return float 1.0 when present and non-empty, else 0.0. + */ + private function scoreRequired(array $object, array $rule): float + { + $value = $this->fieldValue(object: $object, field: (string) ($rule['field'] ?? '')); + if ($this->isPresent(value: $value) === true) { + return 1.0; + } + + return 0.0; + }//end scoreRequired() + + /** + * Score a format rule against a named format or a custom regex. + * + * An absent value scores 0.0 (a format rule presumes presence). When both + * `format` and `pattern` are absent the rule cannot be evaluated and scores 0.0. + * + * @param array $object Object payload. + * @param array $rule Rule definition. + * + * @return float 1.0 on match, else 0.0. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) The branch count tracks the rule's optional + * inputs (presence, named format, custom pattern, regex outcome); each is a distinct, + * independently-testable guard and folding them would obscure the format-resolution flow. + * @SuppressWarnings(PHPMD.NPathComplexity) Same rationale — sequential optional guards. + * @SuppressWarnings(PHPMD.ErrorControlOperator) The @ on preg_match() suppresses a warning + * for a user-supplied pattern that fails to compile; the false return is handled as a + * non-match, so the operator is the guard, not a swallowed error. + */ + private function scoreFormat(array $object, array $rule): float + { + $value = $this->fieldValue(object: $object, field: (string) ($rule['field'] ?? '')); + if ($this->isPresent(value: $value) === false || is_scalar($value) === false) { + return 0.0; + } + + $subject = (string) $value; + + $pattern = null; + $named = (string) ($rule['format'] ?? ''); + if ($named !== '' && isset(self::NAMED_FORMATS[$named]) === true) { + $pattern = self::NAMED_FORMATS[$named]; + } + + $custom = ($rule['pattern'] ?? null); + if (is_string($custom) === true && $custom !== '') { + $pattern = '/'.str_replace('/', '\\/', $custom).'/'; + } + + if ($pattern === null) { + return 0.0; + } + + try { + $matched = @preg_match($pattern, $subject); + } catch (Throwable $e) { + return 0.0; + } + + if ($matched === 1) { + return 1.0; + } + + return 0.0; + }//end scoreFormat() + + /** + * Score a freshness rule via exponential decay against now. + * + * @param array $object Object payload. + * @param array $rule Rule definition. + * @param DateTimeImmutable $now Reference instant. + * + * @return float exp(-ageDays / halfLifeDays) clamped to [0, 1]; 0.0 when + * the date is absent or unparseable. + */ + private function scoreFreshness(array $object, array $rule, DateTimeImmutable $now): float + { + $value = $this->fieldValue(object: $object, field: (string) ($rule['field'] ?? '')); + if ($this->isPresent(value: $value) === false || is_scalar($value) === false) { + return 0.0; + } + + try { + $when = new DateTimeImmutable((string) $value); + } catch (Throwable $e) { + return 0.0; + } + + $ageSeconds = ($now->getTimestamp() - $when->getTimestamp()); + if ($ageSeconds <= 0) { + return 1.0; + } + + $ageDays = ($ageSeconds / 86400.0); + $halfLife = $this->floatOr(value: ($rule['halfLifeDays'] ?? null), fallback: self::DEFAULT_HALF_LIFE_DAYS); + if ($halfLife <= 0.0) { + return 0.0; + } + + // Half-life decay: value drops to 0.5 every halfLife days. + $decay = exp((-1.0 * M_LN2 * $ageDays) / $halfLife); + + return max(0.0, min(1.0, $decay)); + }//end scoreFreshness() + + /** + * Resolve a dotted-path field value from the payload. + * + * @param array $object Object payload. + * @param string $field Field name (dotted for nesting). + * + * @return mixed The value, or null when the path is missing. + */ + private function fieldValue(array $object, string $field) + { + if ($field === '') { + return null; + } + + $segments = explode('.', $field); + $cursor = $object; + foreach ($segments as $segment) { + if (is_array($cursor) === false || array_key_exists($segment, $cursor) === false) { + return null; + } + + $cursor = $cursor[$segment]; + } + + return $cursor; + }//end fieldValue() + + /** + * Determine whether a value counts as present (non-empty). + * + * @param mixed $value Value to test. + * + * @return bool True when the value is meaningfully populated. + */ + private function isPresent($value): bool + { + if ($value === null) { + return false; + } + + if (is_string($value) === true) { + return trim($value) !== ''; + } + + if (is_array($value) === true) { + return count($value) > 0; + } + + return true; + }//end isPresent() + + /** + * Resolve a rule weight, defaulting when absent or non-numeric. + * + * @param array $rule Rule definition. + * + * @return float Weight (>= 0). + */ + private function ruleWeight(array $rule): float + { + return max(0.0, $this->floatOr(value: ($rule['weight'] ?? null), fallback: self::DEFAULT_WEIGHT)); + }//end ruleWeight() + + /** + * Coerce a value to float, falling back when not numeric. + * + * @param mixed $value Candidate value. + * @param float $fallback Fallback when not numeric. + * + * @return float + */ + private function floatOr($value, float $fallback): float + { + if (is_int($value) === true || is_float($value) === true) { + return (float) $value; + } + + if (is_string($value) === true && is_numeric($value) === true) { + return (float) $value; + } + + return $fallback; + }//end floatOr() +}//end class diff --git a/lib/Service/Quality/QualityStatisticsService.php b/lib/Service/Quality/QualityStatisticsService.php new file mode 100644 index 0000000000..f19603f7e5 --- /dev/null +++ b/lib/Service/Quality/QualityStatisticsService.php @@ -0,0 +1,461 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-1 + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Quality; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Computes read-time quality statistics and serves the lowest-quality listing. + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-1 + */ +class QualityStatisticsService +{ + /** + * Default field the score is read from when the annotation omits `field`. + * + * Mirrors {@see \OCA\OpenRegister\Listener\QualityScoreOnSaveListener::DEFAULT_FIELD} + * so the surface reads the same field the listener wrote. + * + * @var string + */ + private const DEFAULT_FIELD = 'qualityScore'; + + /** + * Upper bound on the object set pulled per statistics computation. + * + * Mirrors {@see DuplicateDetectionService::MAX_CANDIDATES} — scopes every + * read to a bounded, RBAC-scoped set rather than the whole schema. + * + * @var int + */ + private const MAX_OBJECTS = 1000; + + /** + * Number of equal-width histogram buckets spanning `[0, 1]`. + * + * @var int + */ + private const HISTOGRAM_BUCKETS = 10; + + /** + * Default page size for the lowest-quality listing. + * + * @var int + */ + private const DEFAULT_LIMIT = 20; + + /** + * Wire collaborators. + * + * @param ObjectService $objectService Object query path (RBAC + tenant scoped). + * @param SchemaMapper $schemaMapper Schema lookup for the quality annotation. + * @param QualityScorer $scorer Reused for status() bucketing — never reimplemented. + * @param LoggerInterface $logger PSR logger. + * + * @return void + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-1 + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly SchemaMapper $schemaMapper, + private readonly QualityScorer $scorer, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Compute quality statistics for a register/schema. + * + * @param int|string $register Register id, uuid or slug. + * @param int|string $schema Schema id, uuid or slug. + * + * @return array{ + * average: float|null, + * total: int, + * buckets: array{good: int, fair: int, poor: int}, + * histogram: array + * } Statistics envelope. Zeroed (null average, zero counts) when the + * scoped set is empty. + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-1 + */ + public function statisticsFor($register, $schema): array + { + $quality = $this->loadAnnotation(schema: $schema); + $field = $this->scoreField(quality: $quality); + $thresholds = $this->thresholds(quality: $quality); + + $objects = $this->loadObjects(register: $register, schema: $schema); + + $histogram = $this->emptyHistogram(); + $buckets = [ + 'good' => 0, + 'fair' => 0, + 'poor' => 0, + ]; + + $sum = 0.0; + $total = 0; + + foreach ($objects as $object) { + $score = $this->scoreOf(object: $object, field: $field); + if ($score === null) { + continue; + } + + $total++; + $sum += $score; + + $status = $this->scorer->status(score: $score, thresholds: $thresholds); + if (isset($buckets[$status]) === true) { + $buckets[$status]++; + } + + $index = $this->histogramIndex(score: $score); + $histogram[$index]['count']++; + }//end foreach + + $average = null; + if ($total > 0) { + $average = round(($sum / $total), 4); + } + + return [ + 'average' => $average, + 'total' => $total, + 'buckets' => $buckets, + 'histogram' => array_values($histogram), + ]; + }//end statisticsFor() + + /** + * List objects of a register/schema ascending by quality score. + * + * @param int|string $register Register id, uuid or slug. + * @param int|string $schema Schema id, uuid or slug. + * @param string|null $qualityStatus Optional status filter (`good`/`fair`/`poor`). + * @param string $sort Sort field: `qualityScore` (default) or `qualityStatus`. + * @param string $order Sort order: `asc` (default) or `desc`. + * @param int $limit Page size. + * @param int $offset Page offset. + * + * @return array{ + * items: array, + * total: int, + * limit: int, + * offset: int + * } Paginated listing, worst-first by default. + * + * @spec openspec/changes/mdm-surface-api/tasks.md#task-1 + */ + public function lowestQuality( + $register, + $schema, + ?string $qualityStatus=null, + string $sort='qualityScore', + string $order='asc', + int $limit=self::DEFAULT_LIMIT, + int $offset=0 + ): array { + $quality = $this->loadAnnotation(schema: $schema); + $field = $this->scoreField(quality: $quality); + $thresholds = $this->thresholds(quality: $quality); + + $objects = $this->loadObjects(register: $register, schema: $schema); + + $rows = []; + foreach ($objects as $object) { + $score = $this->scoreOf(object: $object, field: $field); + $status = null; + if ($score !== null) { + $status = $this->scorer->status(score: $score, thresholds: $thresholds); + } + + if ($qualityStatus !== null && $qualityStatus !== '' && $status !== $qualityStatus) { + continue; + } + + $rows[] = [ + 'id' => (string) $object->getUuid(), + 'qualityScore' => $score, + 'qualityStatus' => $status, + ]; + }//end foreach + + $this->sortRows(rows: $rows, sort: $sort, order: $order); + + $total = count($rows); + $page = array_slice($rows, max(0, $offset), max(0, $limit)); + + return [ + 'items' => $page, + 'total' => $total, + 'limit' => $limit, + 'offset' => $offset, + ]; + }//end lowestQuality() + + /** + * Sort listing rows in place by the requested field/order. + * + * @param array $rows Rows to sort (by reference). + * @param string $sort Sort field. + * @param string $order Sort order. + * + * @return void + */ + private function sortRows(array &$rows, string $sort, string $order): void + { + $direction = 1; + if (strtolower($order) === 'desc') { + $direction = -1; + } + + $key = 'qualityScore'; + if ($sort === 'qualityStatus') { + $key = 'qualityStatus'; + } + + usort( + $rows, + static function (array $left, array $right) use ($key, $direction): int { + $leftValue = $left[$key]; + $rightValue = $right[$key]; + + if ($leftValue === $rightValue) { + return 0; + } + + // Nulls (unscored objects) sort last regardless of direction. + if ($leftValue === null) { + return 1; + } + + if ($rightValue === null) { + return -1; + } + + if ($leftValue < $rightValue) { + return -1 * $direction; + } + + return 1 * $direction; + } + ); + }//end sortRows() + + /** + * Load the RBAC + tenant scoped object set for a register/schema. + * + * @param int|string $register Register reference. + * @param int|string $schema Schema reference. + * + * @return array + */ + private function loadObjects($register, $schema): array + { + try { + $objects = $this->objectService->findAll( + [ + 'filters' => [ + 'register' => $register, + 'schema' => $schema, + ], + 'limit' => self::MAX_OBJECTS, + ] + ); + } catch (Throwable $e) { + $this->logger->warning('Quality statistics object load failed: '.$e->getMessage()); + return []; + } + + $entities = []; + foreach ($objects as $object) { + if ($object instanceof ObjectEntity) { + $entities[] = $object; + } + } + + return $entities; + }//end loadObjects() + + /** + * Read the `x-openregister-quality` annotation off a schema. + * + * Mirrors {@see DuplicateDetectionService::loadAnnotation()}. + * + * @param int|string $schema Schema reference. + * + * @return array Annotation (empty array when absent / unresolvable). + */ + private function loadAnnotation($schema): array + { + try { + $entity = $this->schemaMapper->find($schema, _multitenancy: false); + } catch (Throwable $e) { + return []; + } + + if ($entity instanceof Schema === false) { + return []; + } + + $config = ($entity->getConfiguration() ?? []); + $annotation = ($config['x-openregister-quality'] ?? null); + if (is_array($annotation) === true) { + return $annotation; + } + + return []; + }//end loadAnnotation() + + /** + * Resolve the payload field the quality score was materialised into. + * + * @param array $quality Quality annotation. + * + * @return string Field name, defaulting to `qualityScore`. + */ + private function scoreField(array $quality): string + { + $field = (string) ($quality['field'] ?? self::DEFAULT_FIELD); + if ($field === '') { + return self::DEFAULT_FIELD; + } + + return $field; + }//end scoreField() + + /** + * Resolve the status thresholds declared on the quality annotation. + * + * @param array $quality Quality annotation. + * + * @return array Thresholds map (possibly empty — QualityScorer + * falls back to its own defaults). + */ + private function thresholds(array $quality): array + { + $thresholds = ($quality['thresholds'] ?? []); + if (is_array($thresholds) === true) { + return $thresholds; + } + + return []; + }//end thresholds() + + /** + * Read the materialised score off an object's payload. + * + * @param ObjectEntity $object Object entity. + * @param string $field Payload field the score lives at. + * + * @return float|null The score, or null when absent / non-numeric. + */ + private function scoreOf(ObjectEntity $object, string $field): ?float + { + $data = ($object->getObject() ?? []); + $value = ($data[$field] ?? null); + if (is_int($value) === true || is_float($value) === true) { + return (float) $value; + } + + if (is_string($value) === true && is_numeric($value) === true) { + return (float) $value; + } + + return null; + }//end scoreOf() + + /** + * Build an empty 10-bucket histogram scaffold over `[0, 1]`. + * + * @return array + */ + private function emptyHistogram(): array + { + $histogram = []; + $width = (1.0 / self::HISTOGRAM_BUCKETS); + + for ($i = 0; $i < self::HISTOGRAM_BUCKETS; $i++) { + $histogram[$i] = [ + 'min' => round(($i * $width), 4), + 'max' => round((($i + 1) * $width), 4), + 'count' => 0, + ]; + } + + return $histogram; + }//end emptyHistogram() + + /** + * Resolve which histogram bucket a score falls into. + * + * Buckets are `[min, max)` half-open, except the final bucket which is + * closed on both ends so a perfect `1.0` score is not dropped. + * + * @param float $score Score in `[0, 1]`. + * + * @return int Bucket index in `[0, HISTOGRAM_BUCKETS - 1]`. + */ + private function histogramIndex(float $score): int + { + $clamped = max(0.0, min(1.0, $score)); + $index = (int) floor($clamped * self::HISTOGRAM_BUCKETS); + + if ($index >= self::HISTOGRAM_BUCKETS) { + $index = (self::HISTOGRAM_BUCKETS - 1); + } + + return $index; + }//end histogramIndex() +}//end class diff --git a/lib/Service/Quality/SimilarityCalculator.php b/lib/Service/Quality/SimilarityCalculator.php new file mode 100644 index 0000000000..70911b3079 --- /dev/null +++ b/lib/Service/Quality/SimilarityCalculator.php @@ -0,0 +1,184 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Quality; + +/** + * Field comparison primitives for duplicate detection. + * + * Methods: + * - exact: 1.0 when the raw string values are byte-identical, else 0.0. + * - normalized: 1.0 when case-folded, whitespace-collapsed, accent-stripped + * values are equal, else 0.0. + * - levenshtein: 1 - (editDistance / maxLen) over the normalised values, + * clamped to [0, 1]. A cheap stand-in for fuzzy name matching. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-4 + */ +class SimilarityCalculator +{ + /** + * Compute the similarity of two values under a named method. + * + * @param string $method One of `exact`, `normalized`, `levenshtein`. + * @param mixed $a First value. + * @param mixed $b Second value. + * + * @return float Similarity in [0, 1]; 0.0 for unknown method or non-scalar operands. + * + * @spec openspec/changes/mdm-foundation/tasks.md#task-4 + */ + public function similarity(string $method, $a, $b): float + { + if (is_scalar($a) === false || is_scalar($b) === false) { + return 0.0; + } + + $left = (string) $a; + $right = (string) $b; + + return match ($method) { + 'exact' => $this->boolScore(matched: $left === $right), + 'normalized' => $this->boolScore(matched: $this->normalize(value: $left) === $this->normalize(value: $right)), + 'levenshtein' => $this->levenshteinRatio(a: $left, b: $right), + default => 0.0, + }; + }//end similarity() + + /** + * Map a boolean match outcome to a 0.0 / 1.0 score. + * + * @param bool $matched Whether the values matched. + * + * @return float 1.0 when matched, else 0.0. + */ + private function boolScore(bool $matched): float + { + if ($matched === true) { + return 1.0; + } + + return 0.0; + }//end boolScore() + + /** + * Produce a stable blocking token for a value under a method. + * + * Used to group candidates into comparison blocks cheaply. `exact` keeps + * the raw value; everything else uses the normalised form so near-equal + * values land in the same block. + * + * @param string $method Match method. + * @param mixed $value Field value. + * + * @return string Blocking token (empty string for absent / non-scalar). + */ + public function blockingToken(string $method, $value): string + { + if (is_scalar($value) === false) { + return ''; + } + + $raw = (string) $value; + if ($method === 'exact') { + return $raw; + } + + return $this->normalize(value: $raw); + }//end blockingToken() + + /** + * Normalise a string: trim, lowercase, collapse whitespace, strip accents. + * + * @param string $value Raw value. + * + * @return string Normalised value. + * + * @SuppressWarnings(PHPMD.ErrorControlOperator) The @ on iconv() suppresses a locale + * notice on non-translatable byte sequences; the result is null-checked immediately + * after, so the operator is the idiomatic guard, not a swallowed error. + */ + private function normalize(string $value): string + { + $value = trim($value); + $value = strtolower($value); + if (function_exists('mb_strtolower') === true) { + $value = mb_strtolower($value, 'UTF-8'); + } + + // Strip common diacritics to ASCII where iconv is available. + $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value); + if (is_string($ascii) === true && $ascii !== '') { + $value = $ascii; + } + + // Collapse internal whitespace runs to a single space. + $collapsed = preg_replace('/\s+/', ' ', $value); + if (is_string($collapsed) === true) { + $value = $collapsed; + } + + return trim($value); + }//end normalize() + + /** + * Levenshtein similarity ratio over normalised strings. + * + * @param string $a First value. + * @param string $b Second value. + * + * @return float 1 - distance/maxLen, clamped to [0, 1]. + */ + private function levenshteinRatio(string $a, string $b): float + { + $left = $this->normalize(value: $a); + $right = $this->normalize(value: $b); + + if ($left === $right) { + return 1.0; + } + + $maxLen = max(strlen($left), strlen($right)); + if ($maxLen === 0) { + return 1.0; + } + + // PHP's levenshtein() caps inputs at 255 bytes; guard longer strings. + if (strlen($left) > 255 || strlen($right) > 255) { + $left = substr($left, 0, 255); + $right = substr($right, 0, 255); + $maxLen = max(strlen($left), strlen($right)); + if ($maxLen === 0) { + return 1.0; + } + } + + $distance = levenshtein($left, $right); + $ratio = (1.0 - ($distance / $maxLen)); + + return max(0.0, min(1.0, $ratio)); + }//end levenshteinRatio() +}//end class diff --git a/lib/Service/RealtimeService.php b/lib/Service/RealtimeService.php deleted file mode 100644 index 53c321e306..0000000000 --- a/lib/Service/RealtimeService.php +++ /dev/null @@ -1,142 +0,0 @@ - - * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://OpenRegister.app - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service; - -use DateTime; -use DateTimeImmutable; -use DateTimeInterface; -use OCA\OpenRegister\Db\ObjectEntity; -use OCA\OpenRegister\Db\RealtimeEvent; -use OCA\OpenRegister\Db\RealtimeEventMapper; -use OCP\IURLGenerator; -use OCP\IUserSession; -use Psr\Log\LoggerInterface; - -class RealtimeService -{ - - public const TYPE_OBJECT_CREATED = 'or.object.created'; - public const TYPE_OBJECT_UPDATED = 'or.object.updated'; - public const TYPE_OBJECT_DELETED = 'or.object.deleted'; - public const TYPE_OBJECT_TRANSITIONED = 'or.object.transitioned'; - - /** - * Constructor. - * - * @param RealtimeEventMapper $eventMapper The realtime event mapper. - * @param IUserSession $userSession The user session. - * @param IURLGenerator $urlGenerator The URL generator. - * @param UrnService $urnService The URN service. - * @param LoggerInterface $logger The logger. - */ - public function __construct( - private readonly RealtimeEventMapper $eventMapper, - private readonly IUserSession $userSession, - private readonly IURLGenerator $urlGenerator, - private readonly UrnService $urnService, - private readonly LoggerInterface $logger - ) { - }//end __construct() - - /** - * Record a CloudEvent-shaped change record for a register object. - * - * Returns null when the underlying DB write fails — caller is the - * event listener which logs + continues; one missed realtime event - * MUST NOT break the actual save pipeline. - * - * @param string $eventType The event type (e.g. or.object.created). - * @param ObjectEntity $object The register object. - * @param array $extra Trigger-specific extras (e.g. transition action/from/to). - * - * @return RealtimeEvent|null The persisted event entity, or null on failure. - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-35 - */ - public function record(string $eventType, ObjectEntity $object, array $extra=[]): ?RealtimeEvent - { - try { - $actor = $this->userSession->getUser()?->getUID(); - $urn = $this->urnService->buildForObject($object); - $base = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/'); - - // CloudEvents 1.0 envelope. - $payload = [ - 'specversion' => '1.0', - 'type' => $eventType, - 'source' => $base.'/apps/openregister', - 'subject' => $urn ?? (string) $object->getUuid(), - 'id' => bin2hex(random_bytes(16)), - 'time' => (new DateTimeImmutable())->format(DateTimeInterface::ATOM), - 'datacontenttype' => 'application/json', - 'data' => [ - 'register' => $object->getRegister(), - 'schema' => $object->getSchema(), - 'uuid' => $object->getUuid(), - 'urn' => $urn, - 'organisation' => $object->getOrganisation(), - 'owner' => $object->getOwner(), - 'actor' => $actor, - 'extra' => $extra, - ], - ]; - - $event = new RealtimeEvent(); - $event->setEventType($eventType); - $event->setSource($base.'/apps/openregister'); - $event->setSubject($urn ?? (string) $object->getUuid()); - $event->setRegisterId((string) $object->getRegister()); - $event->setSchemaId((string) $object->getSchema()); - $event->setObjectUuid((string) $object->getUuid()); - $event->setActorUid($actor); - $event->setOrganisation((string) $object->getOrganisation()); - $event->setPayload(json_encode($payload, JSON_UNESCAPED_SLASHES)); - $event->setCreated(new DateTime()); - - return $this->eventMapper->insert($event); - } catch (\Throwable $e) { - $this->logger->warning( - sprintf('[RealtimeService] failed to record %s: %s', $eventType, $e->getMessage()) - ); - return null; - }//end try - }//end record() -}//end class diff --git a/lib/Service/RegisterResolverService.php b/lib/Service/RegisterResolverService.php new file mode 100644 index 0000000000..19f67c4c6e --- /dev/null +++ b/lib/Service/RegisterResolverService.php @@ -0,0 +1,611 @@ +_register` / `_schema` (or grandfathered `register` + * / `schema`) into either bare slug/UUID strings or hydrated Register + * and Schema entities. Replaces the ad-hoc `getValueString` + manual + * mapper lookup pattern observed across opencatalogi, pipelinq and + * docudesk (13 call sites at audit time, 2026-05-03). + * + * The service is multi-tenant aware (the underlying mappers already + * call `applyOrganisationFilter`), supports an explicit + * organisationUuid override for cross-tenant admin tooling, and caches + * resolved entities at request scope so a single request only hits the + * mapper once per (appId, configKey, organisationUuid) tuple. + * + * Naming convention documented in `docs/services/register-resolver.md` + * and in `openspec/changes/register-resolver-service/spec.md`. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/register-resolver-service/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\Resolver\Exception\MissingConfigException; +use OCA\OpenRegister\Service\Resolver\Exception\PropertyNotFoundException; +use OCA\OpenRegister\Service\Resolver\Exception\RegisterNotFoundException; +use OCA\OpenRegister\Service\Resolver\Exception\SchemaNotFoundException; +use OCA\OpenRegister\Service\Resolver\RegisterSchemaPair; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; + +/** + * Resolve `_register` / `_schema` app-config keys to + * Register / Schema entities, with request-scoped caching and + * multi-tenant awareness. + * + * @spec openspec/specs/register-resolver-service/spec.md + */ +final class RegisterResolverService +{ + + /** + * Request-scoped cache for resolved Register entities. + * + * Keyed by "{appId}:{configKey}:{organisationUuid|''}". Per-request + * lifetime — clear on tenant switch via clearCache(). + * + * @var array + */ + private array $registerCache = []; + + /** + * Request-scoped cache for resolved Schema entities. + * + * Keyed by "{appId}:{configKey}:{organisationUuid|''}". Per-request + * lifetime — clear on tenant switch via clearCache(). + * + * @var array + */ + private array $schemaCache = []; + + /** + * Last seen active-tenant UUID; defensive reset on tenant switch. + * + * @var string|null + */ + private ?string $lastTenantUuid = null; + + /** + * Wire the resolver against the canonical OR mappers + IAppConfig. + * + * @param IAppConfig $appConfig The app config store. + * @param RegisterMapper $registerMapper Register lookup mapper. + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param OrganisationService $organisationService Active-tenant resolution. + * @param LoggerInterface $logger Logger for diagnostics. + */ + public function __construct( + private readonly IAppConfig $appConfig, + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly OrganisationService $organisationService, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Read the configured slug/UUID for a register from app config. + * + * Honours the documented naming convention (`_register` + * preferred; bare `register` grandfathered). + * + * @param string $appId Consumer app id. + * @param string $configKey Config key to read. + * @param string|null $default Fallback value if config unset. + * @param string|null $organisationUuid Optional org override (unused here, kept for signature parity). + * + * @return string The configured slug or UUID. + * + * @throws MissingConfigException If the config is unset and no default was provided. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: resolveRegisterId) + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) The organisationUuid parameter is reserved for + * signature parity with resolveRegister; pure id reads never need it but the unified contract + * lets callers pass the same tuple through. + */ + public function resolveRegisterId( + string $appId, + string $configKey, + ?string $default=null, + ?string $organisationUuid=null, + ): string { + $value = $this->appConfig->getValueString($appId, $configKey, ''); + if ($value !== '') { + return $value; + } + + if ($default !== null && $default !== '') { + return $default; + } + + throw new MissingConfigException(appId: $appId, configKey: $configKey); + + }//end resolveRegisterId() + + /** + * Read the configured slug/UUID for a schema from app config. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key to read. + * @param string|null $default Fallback value if config unset. + * @param string|null $organisationUuid Optional org override (unused here, kept for signature parity). + * + * @return string The configured slug or UUID. + * + * @throws MissingConfigException If the config is unset and no default was provided. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: resolveSchemaId) + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) Same parity rationale as resolveRegisterId. + */ + public function resolveSchemaId( + string $appId, + string $configKey, + ?string $default=null, + ?string $organisationUuid=null, + ): string { + $value = $this->appConfig->getValueString($appId, $configKey, ''); + if ($value !== '') { + return $value; + } + + if ($default !== null && $default !== '') { + return $default; + } + + throw new MissingConfigException(appId: $appId, configKey: $configKey); + + }//end resolveSchemaId() + + /** + * Read the configured slug/UUID/name for a schema property from app config. + * + * Honours the documented naming convention (`_property` + * preferred). Apps storing a property identifier (slug, UUID, or + * canonical name) in app config can resolve it through the same + * resolver surface they already use for registers and schemas. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key to read (e.g. `swc_type_property`). + * @param string|null $default Fallback value if config unset. + * @param string|null $organisationUuid Optional org override (unused here, kept for signature parity). + * + * @return string The configured slug, UUID, or name. + * + * @throws MissingConfigException If the config is unset and no default was provided. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: resolvePropertyId) + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) Same parity rationale as resolveRegisterId. + */ + public function resolvePropertyId( + string $appId, + string $configKey, + ?string $default=null, + ?string $organisationUuid=null, + ): string { + $value = $this->appConfig->getValueString($appId, $configKey, ''); + if ($value !== '') { + return $value; + } + + if ($default !== null && $default !== '') { + return $default; + } + + throw new MissingConfigException(appId: $appId, configKey: $configKey); + + }//end resolvePropertyId() + + /** + * Resolve `_register` config to a hydrated Register entity. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key to read. + * @param string|null $default Fallback slug/UUID if config unset. + * @param string|null $organisationUuid Optional explicit-tenant override. + * + * @return Register The hydrated Register entity. + * + * @throws MissingConfigException If config is unset and no default. + * @throws RegisterNotFoundException If the value does not resolve in the caller tenant. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: resolveRegister) + */ + public function resolveRegister( + string $appId, + string $configKey, + ?string $default=null, + ?string $organisationUuid=null, + ): Register { + $this->maybeFlushOnTenantSwitch(); + + $slugOrUuid = $this->resolveRegisterId( + appId: $appId, + configKey: $configKey, + default: $default, + organisationUuid: $organisationUuid + ); + $cacheKey = $this->cacheKey(appId: $appId, configKey: $configKey, organisationUuid: $organisationUuid); + + if (isset($this->registerCache[$cacheKey]) === true) { + return $this->registerCache[$cacheKey]; + } + + try { + // When an explicit org override was passed, bypass multi-tenancy + // filtering at the mapper layer and verify membership ourselves + // after fetch; otherwise rely on the mapper's implicit caller- + // tenant scoping via applyOrganisationFilter(). + if ($organisationUuid !== null) { + $register = $this->registerMapper->find(id: $slugOrUuid, _multitenancy: false); + if ($register->getOrganisation() !== null + && $register->getOrganisation() !== $organisationUuid + ) { + throw new RegisterNotFoundException(appId: $appId, configKey: $configKey, resolvedValue: $slugOrUuid); + } + } else { + $register = $this->registerMapper->find(id: $slugOrUuid); + } + } catch (DoesNotExistException | MultipleObjectsReturnedException $e) { + throw new RegisterNotFoundException(appId: $appId, configKey: $configKey, resolvedValue: $slugOrUuid, previous: $e); + } + + $this->registerCache[$cacheKey] = $register; + + return $register; + + }//end resolveRegister() + + /** + * Resolve `_schema` config to a hydrated Schema entity. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key to read. + * @param string|null $default Fallback slug/UUID if config unset. + * @param string|null $organisationUuid Optional explicit-tenant override. + * + * @return Schema The hydrated Schema entity. + * + * @throws MissingConfigException If config is unset and no default. + * @throws SchemaNotFoundException If the value does not resolve in the caller tenant. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: resolveSchema) + */ + public function resolveSchema( + string $appId, + string $configKey, + ?string $default=null, + ?string $organisationUuid=null, + ): Schema { + $this->maybeFlushOnTenantSwitch(); + + $slugOrUuid = $this->resolveSchemaId( + appId: $appId, + configKey: $configKey, + default: $default, + organisationUuid: $organisationUuid + ); + $cacheKey = $this->cacheKey(appId: $appId, configKey: $configKey, organisationUuid: $organisationUuid); + + if (isset($this->schemaCache[$cacheKey]) === true) { + return $this->schemaCache[$cacheKey]; + } + + try { + if ($organisationUuid !== null) { + $schema = $this->schemaMapper->find(id: $slugOrUuid, _multitenancy: false); + if ($schema->getOrganisation() !== null + && $schema->getOrganisation() !== $organisationUuid + ) { + throw new SchemaNotFoundException(appId: $appId, configKey: $configKey, resolvedValue: $slugOrUuid); + } + } else { + $schema = $this->schemaMapper->find(id: $slugOrUuid); + } + } catch (DoesNotExistException | MultipleObjectsReturnedException $e) { + throw new SchemaNotFoundException(appId: $appId, configKey: $configKey, resolvedValue: $slugOrUuid, previous: $e); + } + + $this->schemaCache[$cacheKey] = $schema; + + return $schema; + + }//end resolveSchema() + + /** + * Resolve a schema property by configured identifier. + * + * Combines `resolvePropertyId` with a schema lookup, then locates + * the matching property on the resolved schema. The configured + * property identifier is matched against the property's array key, + * its `slug`, its `name`, or its `id` (in that order) so apps can + * store any stable identifier they prefer. + * + * Pattern mirrors `resolveRegister` / `resolveSchema`: read config, + * hydrate, throw if absent. Returned shape is the raw property + * definition array from `Schema::getProperties()[$key]` — callers + * read whatever field they need (`id`, `type`, `format`, etc.). + * + * @param string $appId Consumer app id. + * @param string $schemaConfigKey Config key for the schema (e.g. `swc_schema`). + * @param string $propertyConfigKey Config key for the property (e.g. `swc_type_property`). + * @param string|null $propertyDefault Fallback property id if config unset. + * @param string|null $schemaDefault Fallback schema id if config unset. + * @param string|null $organisationUuid Optional explicit-tenant override. + * + * @return array{0: string, 1: array} Tuple of the resolved property key + the property + * definition array. The key is the property's array + * key on `Schema::getProperties()`, which is also + * the canonical name used in object payloads. + * + * @throws MissingConfigException If either config is unset and no default given. + * @throws SchemaNotFoundException If the schema id does not resolve. + * @throws PropertyNotFoundException If the property id does not match any property on the schema. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: resolveProperty) + */ + public function resolveProperty( + string $appId, + string $schemaConfigKey, + string $propertyConfigKey, + ?string $propertyDefault=null, + ?string $schemaDefault=null, + ?string $organisationUuid=null, + ): array { + $schema = $this->resolveSchema( + appId: $appId, + configKey: $schemaConfigKey, + default: $schemaDefault, + organisationUuid: $organisationUuid + ); + $propertyId = $this->resolvePropertyId( + appId: $appId, + configKey: $propertyConfigKey, + default: $propertyDefault, + organisationUuid: $organisationUuid + ); + + $properties = $schema->getProperties(); + $schemaId = (string) ($schema->getSlug() ?? $schema->getUuid() ?? ''); + + // Match by array key first (canonical name in property maps). + if (isset($properties[$propertyId]) === true && is_array($properties[$propertyId]) === true) { + return [$propertyId, $properties[$propertyId]]; + } + + // Fall back to scanning by slug / name / id. + foreach ($properties as $key => $definition) { + if (is_array($definition) === false) { + continue; + } + + $candidates = [ + (string) ($definition['slug'] ?? ''), + (string) ($definition['name'] ?? ''), + (string) ($definition['id'] ?? ''), + ]; + + if (in_array($propertyId, $candidates, true) === true) { + return [(string) $key, $definition]; + } + } + + throw new PropertyNotFoundException( + appId: $appId, + configKey: $propertyConfigKey, + resolvedValue: $propertyId, + schemaIdentifier: $schemaId, + ); + + }//end resolveProperty() + + /** + * Convenience: resolve a register and schema in a single call. + * + * @param string $appId Consumer app id. + * @param string $registerKey Config key for the register. + * @param string $schemaKey Config key for the schema. + * @param string|null $organisationUuid Optional explicit-tenant override. + * + * @return RegisterSchemaPair Pair of resolved entities + raw ids. + * + * @throws MissingConfigException If either config is unset. + * @throws RegisterNotFoundException If the register cannot be resolved. + * @throws SchemaNotFoundException If the schema cannot be resolved. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: resolvePair) + */ + public function resolvePair( + string $appId, + string $registerKey, + string $schemaKey, + ?string $organisationUuid=null, + ): RegisterSchemaPair { + $registerId = $this->resolveRegisterId(appId: $appId, configKey: $registerKey, default: null, organisationUuid: $organisationUuid); + $schemaId = $this->resolveSchemaId(appId: $appId, configKey: $schemaKey, default: null, organisationUuid: $organisationUuid); + $register = $this->resolveRegister(appId: $appId, configKey: $registerKey, default: null, organisationUuid: $organisationUuid); + $schema = $this->resolveSchema(appId: $appId, configKey: $schemaKey, default: null, organisationUuid: $organisationUuid); + + return new RegisterSchemaPair( + registerId: $registerId, + schemaId: $schemaId, + register: $register, + schema: $schema, + ); + + }//end resolvePair() + + /** + * Enumerate every `_(register|schema)` key set for an app. + * + * Used by admin UIs and the `openregister:resolver:list` console + * command for diagnostics. The bare keys `register` and `schema` + * are surfaced as `default_register` / `default_schema` so admin + * UIs can flag them as legacy convention. + * + * @param string $appId Consumer app id. + * + * @return array Map of config-key → raw configured value. + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: enumerateAppConfigs) + */ + public function enumerateAppConfigs(string $appId): array + { + $allValues = $this->appConfig->getAllValues($appId); + $matches = []; + foreach ($allValues as $key => $value) { + $resolved = $this->matchResolverKey(key: $key); + if ($resolved === null) { + continue; + } + + // Skip empty values; an empty IAppConfig string means "unset" + // in NC and we don't want to advertise it as a configured key. + $stringValue = (string) $value; + if ($stringValue === '') { + continue; + } + + $matches[$resolved] = $stringValue; + } + + ksort($matches); + + return $matches; + + }//end enumerateAppConfigs() + + /** + * Clear request-scoped caches. Test hook + defensive tenant-switch path. + * + * @return void + * + * @spec openspec/specs/register-resolver-service/spec.md + * (Requirement: service MUST cache resolved entities at the request scope — clear hook used + * by maybeFlushOnTenantSwitch + by test suites needing a fresh cache between cases.) + */ + public function clearCache(): void + { + $this->registerCache = []; + $this->schemaCache = []; + + }//end clearCache() + + /** + * Match a raw config-key string to its canonical resolver key. + * + * Returns null when the key does not look like a resolver key. + * Bare `register` / `schema` are surfaced as `default_register` / + * `default_schema` per the documented naming convention. + * + * @param string $key Raw IAppConfig key. + * + * @return string|null Canonical resolver-key form, or null if not a resolver key. + */ + private function matchResolverKey(string $key): ?string + { + if ($key === 'register') { + return 'default_register'; + } + + if ($key === 'schema') { + return 'default_schema'; + } + + if (preg_match('/^[A-Za-z][A-Za-z0-9_-]*_(register|schema|property)$/', $key) === 1) { + return $key; + } + + return null; + + }//end matchResolverKey() + + /** + * Build the per-tuple cache key used by both internal caches. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key being resolved. + * @param string|null $organisationUuid Optional explicit-tenant override. + * + * @return string Cache key string. + */ + private function cacheKey(string $appId, string $configKey, ?string $organisationUuid): string + { + return $appId.':'.$configKey.':'.($organisationUuid ?? ''); + + }//end cacheKey() + + /** + * Defensive cache flush when the active tenant changes mid-request. + * + * Rare in practice (admin tooling switching scopes) but cheap to + * check and prevents the cache from returning an entity scoped to + * the previous tenant. + * + * @return void + */ + private function maybeFlushOnTenantSwitch(): void + { + try { + $active = $this->organisationService->getActiveOrganisation(); + } catch (\Throwable $e) { + // The resolver must not fail just because the active-org + // lookup misfires; treat as "no current tenant" for the + // purpose of switch detection. + $this->logger->debug( + message: '[RegisterResolverService] getActiveOrganisation threw — skipping switch check', + context: ['file' => __FILE__, 'line' => __LINE__, 'exception' => $e->getMessage()] + ); + return; + } + + $currentUuid = null; + if ($active !== null) { + $currentUuid = $active->getUuid(); + } + + if ($this->lastTenantUuid !== null && $currentUuid !== $this->lastTenantUuid) { + $this->clearCache(); + } + + $this->lastTenantUuid = $currentUuid; + + }//end maybeFlushOnTenantSwitch() +}//end class diff --git a/lib/Service/RegisterService.php b/lib/Service/RegisterService.php index 8c6c064efc..e4b2211986 100644 --- a/lib/Service/RegisterService.php +++ b/lib/Service/RegisterService.php @@ -31,6 +31,7 @@ use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Service\FileService; use OCA\OpenRegister\Service\OrganisationService; +use OCA\OpenRegister\Service\Serializer\RegisterSerializer; use OCP\IDBConnection; use Psr\Log\LoggerInterface; @@ -53,7 +54,9 @@ * * @link https://www.OpenRegister.app * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) $_multitenancy boolean flag is part of the public API contract shared with RegisterMapper::findAll/find; the method signature must match callers that explicitly pass `false` to bypass multitenancy for admin/repair contexts. + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) $_multitenancy boolean flag is part of the public API + * contract shared with RegisterMapper::findAll/find; the method signature must match callers that + * explicitly pass `false` to bypass multitenancy for admin/repair contexts. */ class RegisterService { @@ -85,6 +88,16 @@ class RegisterService */ private readonly IDBConnection $db; + /** + * Every magic table that exists, as a name => true lookup. Null until first read. + * + * Populated once per request by {@see magicTableExists()}. See that method for why + * this cache has to exist at all. + * + * @var array|null + */ + private ?array $magicTableNames = null; + /** * File service * @@ -112,6 +125,17 @@ class RegisterService */ private readonly LoggerInterface $logger; + /** + * Register serializer + * + * Applies `_extend` post-processing (`schemas`, `@self.stats`) on + * Register entities. Shared between the HTTP controller and DI + * consumers via findAllSerialized / findSerialized. + * + * @var RegisterSerializer + */ + private readonly RegisterSerializer $registerSerializer; + /** * Constructor * @@ -123,6 +147,7 @@ class RegisterService * @param FileService $fileService File service for file operations * @param OrganisationService $organisationService Organisation service for permissions * @param LoggerInterface $logger Logger for error tracking + * @param RegisterSerializer $registerSerializer Serializer for `_extend` post-processing * * @return void */ @@ -132,7 +157,8 @@ public function __construct( IDBConnection $db, FileService $fileService, OrganisationService $organisationService, - LoggerInterface $logger + LoggerInterface $logger, + RegisterSerializer $registerSerializer ) { $this->logger = $logger; $this->logger->debug( @@ -145,6 +171,7 @@ public function __construct( $this->db = $db; $this->fileService = $fileService; $this->organisationService = $organisationService; + $this->registerSerializer = $registerSerializer; $this->logger->debug( message: '[RegisterService] RegisterService constructor completed.', context: ['file' => __FILE__, 'line' => __LINE__] @@ -171,7 +198,12 @@ public function __construct( */ public function find(int | string $id, array $_extend=[], bool $_multitenancy=true): Register { - return $this->registerMapper->find(id: $id, _extend: $_extend, _multitenancy: $_multitenancy); + // NB: `_extend` is declared for signature compatibility with the + // pre-spec contract; the mapper no longer accepts it. Use + // `findSerialized()` to obtain an array payload with `_extend` + // post-processing applied. + unset($_extend); + return $this->registerMapper->find(id: $id, _multitenancy: $_multitenancy); }//end find() /** @@ -208,17 +240,147 @@ public function findAll( bool $_multitenancy=true ): array { // Find all registers with optional filtering, pagination, and extensions. + // NB: `_extend` is declared for signature compatibility only — the + // mapper layer ignores it; honored extensions live on findSerialized + // / findAllSerialized which delegate to RegisterSerializer. return $this->registerMapper->findAll( limit: $limit, offset: $offset, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams, - _extend: $_extend, _multitenancy: $_multitenancy ); }//end findAll() + /** + * Find a register by ID and serialize it with `_extend` applied + * + * Combines `find()` with `RegisterSerializer::serialize()`. When + * `_extend` requests both `schemas` and `@self.stats`, this method + * pre-computes the per-schema counts via `getSchemaObjectCounts()` + * and hands them to the serializer so the serializer itself has + * no DI back-reference to this service. + * + * @param int|string $id The ID of the register to find. + * @param array $_extend Extension keys to apply (`schemas`, `@self.stats`). + * @param bool $_multitenancy Whether to apply multitenancy filtering. + * + * @return array Serialized register payload. + * + * @throws \OCP\AppFramework\Db\DoesNotExistException If register not found. + * + * @spec openspec/specs/register-service-extensions/spec.md + * (Requirement: RegisterService SHALL expose serialized query methods that honor `_extend`) + */ + public function findSerialized(int | string $id, array $_extend=[], bool $_multitenancy=true): array + { + $register = $this->find(id: $id, _extend: $_extend, _multitenancy: $_multitenancy); + + $stats = null; + if (in_array('schemas', $_extend, true) === true + && in_array('@self.stats', $_extend, true) === true + ) { + $stats = $this->getSchemaObjectCounts( + registerId: (int) $register->getId(), + schemas: $this->resolveSchemasForStats(schemaIds: ($register->getSchemas() ?? [])) + ); + } + + return $this->registerSerializer->serialize($register, $_extend, $stats); + + }//end findSerialized() + + /** + * Find all registers + serialize each with `_extend` applied + * + * Companion to {@see findAll()} that returns serialized arrays with + * `_extend` honored. When `schemas` + `@self.stats` are both + * requested, per-register stats are pre-computed and handed to the + * serializer via `serializeMany()`. + * + * @param int|null $limit Maximum number of results. + * @param int|null $offset Pagination offset. + * @param array|null $filters Mapper filters. + * @param array|null $searchConditions Search conditions. + * @param array|null $searchParams Search parameters. + * @param array $_extend Extension keys to apply. + * @param bool $_multitenancy Whether to apply multitenancy filtering. + * + * @return array Array of serialized register payloads. + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Mirrors findAll() so HTTP and DI + * callers can hand the same parameter set to either method without re-wrangling + * query params. + * + * @spec openspec/specs/register-service-extensions/spec.md + * (Requirement: RegisterService SHALL expose serialized query methods that honor `_extend`) + */ + public function findAllSerialized( + ?int $limit=null, + ?int $offset=null, + ?array $filters=[], + ?array $searchConditions=[], + ?array $searchParams=[], + array $_extend=[], + bool $_multitenancy=true + ): array { + $registers = $this->findAll( + limit: $limit, + offset: $offset, + filters: $filters, + searchConditions: $searchConditions, + searchParams: $searchParams, + _multitenancy: $_multitenancy + ); + + $statsByRegisterId = null; + if (in_array('schemas', $_extend, true) === true + && in_array('@self.stats', $_extend, true) === true + ) { + $statsByRegisterId = []; + foreach ($registers as $register) { + $statsByRegisterId[(int) $register->getId()] = $this->getSchemaObjectCounts( + registerId: (int) $register->getId(), + schemas: $this->resolveSchemasForStats(schemaIds: ($register->getSchemas() ?? [])) + ); + } + } + + return $this->registerSerializer->serializeMany($registers, $_extend, $statsByRegisterId); + + }//end findAllSerialized() + + /** + * Resolve schema IDs to hydrated objects for stats computation. + * + * `getSchemaObjectCounts()` reads `tablePrefix` and `slug` off each + * schema (used to build the magic-table name). The serializer hands + * us bare IDs, so we resolve them here. Orphan IDs are silently + * dropped — they cannot produce a stats lookup anyway. + * + * @param array $schemaIds Schema ID array (int|string). + * + * @return array Resolved schema entities for the stats query. + */ + private function resolveSchemasForStats(array $schemaIds): array + { + $schemas = []; + foreach ($schemaIds as $schemaId) { + try { + $schemas[] = $this->schemaMapper->find(id: $schemaId, _multitenancy: false); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + // Orphan IDs cannot contribute to stats; skip silently. + // The RegisterSerializer logs the orphan when it hits the + // same ID during expansion. + continue; + } + } + + return $schemas; + + }//end resolveSchemasForStats() + /** * Create a new register from array data. * @@ -393,7 +555,10 @@ private function ensureRegisterFolderExists(Register $entity): void * * @spec openspec/changes/retrofit-2026-05-24-b-svc-compute-profile-org/tasks.md#task-5 * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) getSchemaObjectCounts() builds a UNION SQL query across N schema magic-tables with platform-specific CAST syntax (Postgres vs MariaDB/MySQL), processes the result set, and backfills zero-stats for missing tables in one pass; splitting would require multiple DB round-trips or passing the DB connection to sub-helpers. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) getSchemaObjectCounts() builds a UNION SQL query + * across N schema magic-tables with platform-specific CAST syntax (Postgres vs MariaDB/MySQL), + * processes the result set, and backfills zero-stats for missing tables in one pass; splitting + * would require multiple DB round-trips or passing the DB connection to sub-helpers. */ public function getSchemaObjectCounts(int $registerId, array $schemas): array { @@ -421,7 +586,20 @@ public function getSchemaObjectCounts(int $registerId, array $schemas): array $unionQueries = []; foreach ($schemas as $schema) { - $schemaId = $schema['id'] ?? null; + // $schemas may arrive as hydrated Schema objects (the + // `_extend[]=schemas` + `@self.stats` path via + // resolveSchemasForStats()/schemaMapper->find()), as plain + // arrays, or as scalar ids — resolve the id from any shape. + if ($schema instanceof \OCA\OpenRegister\Db\Schema) { + $schemaId = $schema->getId(); + } else if (is_array($schema) === true) { + $schemaId = $schema['id'] ?? null; + } else if (is_numeric($schema) === true) { + $schemaId = (int) $schema; + } else { + $schemaId = null; + } + if ($schemaId === null) { $this->logger->warning( message: '[RegisterService] Schema without ID found, skipping', @@ -431,7 +609,7 @@ public function getSchemaObjectCounts(int $registerId, array $schemas): array } $tableName = 'openregister_table_'.$registerId.'_'.$schemaId; - $tableExists = $this->db->tableExists($tableName); + $tableExists = $this->magicTableExists(tableName: $tableName); if ($tableExists !== true) { // Table doesn't exist yet, return 0 for all stats. @@ -496,6 +674,86 @@ public function getSchemaObjectCounts(int $registerId, array $schemas): array return $result; }//end getSchemaObjectCounts() + /** + * Whether a magic table exists, answered from a single cached catalog read. + * + * IDBConnection::tableExists() looks cheap and is not: it goes to Doctrine's schema + * manager, which re-reads `information_schema.tables` on EVERY call. Called once per + * schema inside getSchemaObjectCounts()'s loop, that is one full catalog scan per + * schema — on a dev instance with 76 registers / 1,231 schemas / 1,960 magic tables + * it made `GET /api/registers?_extend[]=schemas&_extend[]=@self.stats` take **76 + * seconds**, ~90% of it inside that one introspection query. The same call without + * the extend took 0.9s. + * + * So read the catalog ONCE and answer from memory. The cache is per-request (the + * service is request-scoped), which is the right lifetime: a magic table created by + * a concurrent request is irrelevant to a stats snapshot already in flight, and a + * table created by THIS request goes through MagicMapper, not here. + * + * @param string $tableName The unprefixed magic table name (`openregister_table_1_2`). + * + * @return bool True when the table exists. + * + * @spec exclude performance cache over information_schema; behaviour identical to tableExists() + */ + private function magicTableExists(string $tableName): bool + { + if ($this->magicTableNames === null) { + $this->magicTableNames = []; + + try { + // Match on the `openregister_table_` marker itself, NOT on a computed + // prefix. The obvious `getQueryBuilder()->getTableName('')` returns the + // literal `*PREFIX*` placeholder — it is only resolved to the real prefix + // (`oc_`) when a query is EXECUTED through the NC DB layer, which a raw + // information_schema string never is. Building the LIKE from that + // placeholder matched zero tables, so every schema was reported as having + // zero objects. Anchoring on the marker sidesteps the prefix completely: + // the caller passes `openregister_table_R_S`, which is exactly the suffix + // we key on. + $sql = 'SELECT table_name FROM information_schema.tables'; + $sql .= ' WHERE table_name LIKE :pattern'; + + $stmt = $this->db->prepare($sql); + $stmt->bindValue('pattern', '%openregister\_table\_%'); + $stmt->execute(); + + while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) { + $name = (string) ($row['table_name'] ?? ''); + if ($name === '') { + continue; + } + + // Key on the unprefixed marker onwards, so callers need not know the + // prefix: `oc_openregister_table_1_2` => `openregister_table_1_2`. + $pos = strpos($name, 'openregister_table_'); + if ($pos === false) { + continue; + } + + $this->magicTableNames[substr($name, $pos)] = true; + } + + $stmt->closeCursor(); + } catch (\Throwable $e) { + // A failed catalog read must not become "no tables exist" — that would + // silently report every schema as having zero objects. Fall back to the + // slow-but-correct per-table check for the rest of this request. Catch + // \Throwable, not just \Exception: a stats read is not worth a fatal if + // the platform's query builder is unavailable. + $this->logger->warning( + message: '[RegisterService] Could not list magic tables, falling back to per-table checks: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + $this->magicTableNames = null; + + return $this->db->tableExists($tableName); + }//end try + }//end if + + return isset($this->magicTableNames[$tableName]); + }//end magicTableExists() + /** * Get a zero-initialized count stats array, optionally populated from a database row. * diff --git a/lib/Service/Registers/RegisterCacheHandler.php b/lib/Service/Registers/RegisterCacheHandler.php index 954d1e568d..fe1ec154dd 100644 --- a/lib/Service/Registers/RegisterCacheHandler.php +++ b/lib/Service/Registers/RegisterCacheHandler.php @@ -100,7 +100,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function invalidate(int $registerId): void { diff --git a/lib/Service/Reporting/HtmlReportWriter.php b/lib/Service/Reporting/HtmlReportWriter.php index 5425d53f1f..c870e9b31e 100644 --- a/lib/Service/Reporting/HtmlReportWriter.php +++ b/lib/Service/Reporting/HtmlReportWriter.php @@ -14,9 +14,9 @@ * @category Service * @package OCA\OpenRegister\Service\Reporting * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Service/Reporting/PdfReportWriter.php b/lib/Service/Reporting/PdfReportWriter.php index 89180a850b..59afc79e11 100644 --- a/lib/Service/Reporting/PdfReportWriter.php +++ b/lib/Service/Reporting/PdfReportWriter.php @@ -17,9 +17,9 @@ * @category Service * @package OCA\OpenRegister\Service\Reporting * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Service/Reporting/ReportRenderService.php b/lib/Service/Reporting/ReportRenderService.php index 99ba0ffe62..7f25db8433 100644 --- a/lib/Service/Reporting/ReportRenderService.php +++ b/lib/Service/Reporting/ReportRenderService.php @@ -26,9 +26,9 @@ * @category Service * @package OCA\OpenRegister\Service\Reporting * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ diff --git a/lib/Service/Reporting/SpreadsheetReportWriter.php b/lib/Service/Reporting/SpreadsheetReportWriter.php index 0840e7527c..adb8c1c32e 100644 --- a/lib/Service/Reporting/SpreadsheetReportWriter.php +++ b/lib/Service/Reporting/SpreadsheetReportWriter.php @@ -13,9 +13,9 @@ * @category Service * @package OCA\OpenRegister\Service\Reporting * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -239,7 +239,10 @@ private function writeFormat(Spreadsheet $spreadsheet, string $format): string return $bytes; } - $writer = ($format === 'ods') ? new Ods($spreadsheet) : new Xlsx($spreadsheet); + $writer = new Xlsx($spreadsheet); + if ($format === 'ods') { + $writer = new Ods($spreadsheet); + } ob_start(); $writer->save('php://output'); diff --git a/lib/Service/RequestScopedCache.php b/lib/Service/RequestScopedCache.php deleted file mode 100644 index ca9114a51c..0000000000 --- a/lib/Service/RequestScopedCache.php +++ /dev/null @@ -1,148 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.app - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-58 - */ - -declare(strict_types=1); - -namespace OCA\OpenRegister\Service; - -/** - * Request-scoped in-memory cache shared across all services within one HTTP request - * - * This cache holds entities (schemas, registers, objects, organisations) so that - * repeated lookups within a single request hit memory instead of the database. - * The cache is automatically discarded when the request ends (PHP process exit). - * - * Usage: inject this service into mappers/services that do repeated lookups. - * Namespaces prevent key collisions between entity types. - * - * @category Service - * @package OCA\OpenRegister\Service - * - * @author Conduction Development Team - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version 1.0.0 - * - * @link https://www.OpenRegister.app - */ -class RequestScopedCache -{ - - /** - * Cache storage indexed by namespace and key - * - * @var array> - */ - private array $cache = []; - - /** - * Retrieve a value from the cache - * - * @param string $namespace The cache namespace (e.g. 'schema', 'register') - * @param string $key The cache key (e.g. entity id, uuid, or slug) - * - * @return mixed The cached value, or null if not found - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-58 - */ - public function get(string $namespace, string $key): mixed - { - return $this->cache[$namespace][$key] ?? null; - }//end get() - - /** - * Store a value in the cache - * - * @param string $namespace The cache namespace - * @param string $key The cache key - * @param mixed $value The value to cache - * - * @return void - * - * @spec exclude Trivial in-memory cache write; no business logic. Class behavior already annotated under annotate-openregister#task-58. - */ - public function set(string $namespace, string $key, mixed $value): void - { - $this->cache[$namespace][$key] = $value; - }//end set() - - /** - * Check whether a key exists in the cache - * - * @param string $namespace The cache namespace - * @param string $key The cache key - * - * @return bool True if the key exists (even if value is null) - * - * @spec exclude Trivial array-key existence check; no business logic. - */ - public function has(string $namespace, string $key): bool - { - return array_key_exists(key: $namespace, array: $this->cache) - && array_key_exists(key: $key, array: $this->cache[$namespace]); - }//end has() - - /** - * Retrieve multiple values from the cache at once - * - * @param string $namespace The cache namespace - * @param string[] $keys The cache keys to look up - * - * @return array Map of key => value for found entries only - * - * @spec exclude Trivial fan-out over has()/get(); no business logic. - */ - public function getMultiple(string $namespace, array $keys): array - { - $results = []; - foreach ($keys as $key) { - if ($this->has(namespace: $namespace, key: $key) === true) { - $results[$key] = $this->cache[$namespace][$key]; - } - } - - return $results; - }//end getMultiple() - - /** - * Clear a specific namespace or the entire cache - * - * @param string|null $namespace Namespace to clear, or null to clear everything - * - * @return void - * - * @spec exclude Trivial cache reset (unset namespace or empty array); no business logic. - */ - public function clear(?string $namespace=null): void - { - if ($namespace !== null) { - unset($this->cache[$namespace]); - return; - } - - $this->cache = []; - }//end clear() -}//end class diff --git a/lib/Service/Resolver/Exception/MissingConfigException.php b/lib/Service/Resolver/Exception/MissingConfigException.php new file mode 100644 index 0000000000..2ab5f6ec7e --- /dev/null +++ b/lib/Service/Resolver/Exception/MissingConfigException.php @@ -0,0 +1,98 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Resolver\Exception; + +use Exception; + +/** + * Raised when a resolver-config key is unset and no default was passed. + * + * @phpstan-consistent-constructor + */ +class MissingConfigException extends Exception +{ + + /** + * Consumer app id (e.g. `opencatalogi`). + * + * @var string + */ + private string $appId; + + /** + * Config key the resolver tried to read (e.g. `theme_register`). + * + * @var string + */ + private string $configKey; + + /** + * Construct the missing-config exception with diagnostic context. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key that was missing. + * @param Exception|null $previous Previous exception in the chain. + */ + public function __construct(string $appId, string $configKey, ?Exception $previous=null) + { + $this->appId = $appId; + $this->configKey = $configKey; + + parent::__construct( + message: sprintf( + 'Resolver config key "%s" is not set for app "%s" and no default was provided.', + $configKey, + $appId + ), + code: 500, + previous: $previous + ); + + }//end __construct() + + /** + * Get the app id that was being resolved against. + * + * @return string The consumer app id. + */ + public function getAppId(): string + { + return $this->appId; + + }//end getAppId() + + /** + * Get the config key that was missing. + * + * @return string The config key. + */ + public function getConfigKey(): string + { + return $this->configKey; + + }//end getConfigKey() +}//end class diff --git a/lib/Service/Resolver/Exception/PropertyNotFoundException.php b/lib/Service/Resolver/Exception/PropertyNotFoundException.php new file mode 100644 index 0000000000..80afd19fe6 --- /dev/null +++ b/lib/Service/Resolver/Exception/PropertyNotFoundException.php @@ -0,0 +1,146 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Resolver\Exception; + +use Exception; + +/** + * Raised when a resolved property slug/UUID has no matching property on + * the target schema. + * + * @phpstan-consistent-constructor + */ +class PropertyNotFoundException extends Exception +{ + + /** + * Consumer app id (e.g. `openconnector`). + * + * @var string + */ + private string $appId; + + /** + * Config key the resolver read. + * + * @var string + */ + private string $configKey; + + /** + * Slug/UUID/name that failed to resolve to a property. + * + * @var string + */ + private string $resolvedValue; + + /** + * Slug/UUID of the schema the property lookup targeted. + * + * @var string + */ + private string $schemaIdentifier; + + /** + * Construct the not-found exception with diagnostic context. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key that resolved. + * @param string $resolvedValue Slug/UUID/name the lookup attempted. + * @param string $schemaIdentifier Schema slug/UUID the lookup targeted. + * @param Exception|null $previous Previous exception in the chain. + */ + public function __construct( + string $appId, + string $configKey, + string $resolvedValue, + string $schemaIdentifier, + ?Exception $previous=null, + ) { + $this->appId = $appId; + $this->configKey = $configKey; + $this->resolvedValue = $resolvedValue; + $this->schemaIdentifier = $schemaIdentifier; + + parent::__construct( + message: sprintf( + 'Property "%s" (from config key "%s" on app "%s") not found on schema "%s".', + $resolvedValue, + $configKey, + $appId, + $schemaIdentifier + ), + code: 404, + previous: $previous + ); + + }//end __construct() + + /** + * Get the app id the resolver was working against. + * + * @return string The consumer app id. + */ + public function getAppId(): string + { + return $this->appId; + + }//end getAppId() + + /** + * Get the config key that was read. + * + * @return string The config key. + */ + public function getConfigKey(): string + { + return $this->configKey; + + }//end getConfigKey() + + /** + * Get the resolved value that could not be hydrated. + * + * @return string The slug/UUID/name the lookup attempted. + */ + public function getResolvedValue(): string + { + return $this->resolvedValue; + + }//end getResolvedValue() + + /** + * Get the schema identifier the property lookup targeted. + * + * @return string The schema slug or UUID. + */ + public function getSchemaIdentifier(): string + { + return $this->schemaIdentifier; + + }//end getSchemaIdentifier() +}//end class diff --git a/lib/Service/Resolver/Exception/RegisterNotFoundException.php b/lib/Service/Resolver/Exception/RegisterNotFoundException.php new file mode 100644 index 0000000000..5661ece61b --- /dev/null +++ b/lib/Service/Resolver/Exception/RegisterNotFoundException.php @@ -0,0 +1,119 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Resolver\Exception; + +use Exception; + +/** + * Raised when a resolved register slug/UUID has no matching entity. + * + * @phpstan-consistent-constructor + */ +class RegisterNotFoundException extends Exception +{ + + /** + * Consumer app id (e.g. `opencatalogi`). + * + * @var string + */ + private string $appId; + + /** + * Config key the resolver read. + * + * @var string + */ + private string $configKey; + + /** + * Slug or UUID that failed to resolve to an entity. + * + * @var string + */ + private string $resolvedValue; + + /** + * Construct the not-found exception with diagnostic context. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key that resolved. + * @param string $resolvedValue Slug/UUID the lookup attempted. + * @param Exception|null $previous Previous exception in the chain. + */ + public function __construct(string $appId, string $configKey, string $resolvedValue, ?Exception $previous=null) + { + $this->appId = $appId; + $this->configKey = $configKey; + $this->resolvedValue = $resolvedValue; + + parent::__construct( + message: sprintf( + 'Register "%s" (from config key "%s" on app "%s") not found in caller tenant.', + $resolvedValue, + $configKey, + $appId + ), + code: 404, + previous: $previous + ); + + }//end __construct() + + /** + * Get the app id the resolver was working against. + * + * @return string The consumer app id. + */ + public function getAppId(): string + { + return $this->appId; + + }//end getAppId() + + /** + * Get the config key that was read. + * + * @return string The config key. + */ + public function getConfigKey(): string + { + return $this->configKey; + + }//end getConfigKey() + + /** + * Get the resolved value that could not be hydrated. + * + * @return string The slug or UUID the lookup attempted. + */ + public function getResolvedValue(): string + { + return $this->resolvedValue; + + }//end getResolvedValue() +}//end class diff --git a/lib/Service/Resolver/Exception/SchemaNotFoundException.php b/lib/Service/Resolver/Exception/SchemaNotFoundException.php new file mode 100644 index 0000000000..aa57c5f1bb --- /dev/null +++ b/lib/Service/Resolver/Exception/SchemaNotFoundException.php @@ -0,0 +1,119 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Resolver\Exception; + +use Exception; + +/** + * Raised when a resolved schema slug/UUID has no matching entity. + * + * @phpstan-consistent-constructor + */ +class SchemaNotFoundException extends Exception +{ + + /** + * Consumer app id (e.g. `opencatalogi`). + * + * @var string + */ + private string $appId; + + /** + * Config key the resolver read. + * + * @var string + */ + private string $configKey; + + /** + * Slug or UUID that failed to resolve to an entity. + * + * @var string + */ + private string $resolvedValue; + + /** + * Construct the not-found exception with diagnostic context. + * + * @param string $appId Consumer app id. + * @param string $configKey Config key that resolved. + * @param string $resolvedValue Slug/UUID the lookup attempted. + * @param Exception|null $previous Previous exception in the chain. + */ + public function __construct(string $appId, string $configKey, string $resolvedValue, ?Exception $previous=null) + { + $this->appId = $appId; + $this->configKey = $configKey; + $this->resolvedValue = $resolvedValue; + + parent::__construct( + message: sprintf( + 'Schema "%s" (from config key "%s" on app "%s") not found in caller tenant.', + $resolvedValue, + $configKey, + $appId + ), + code: 404, + previous: $previous + ); + + }//end __construct() + + /** + * Get the app id the resolver was working against. + * + * @return string The consumer app id. + */ + public function getAppId(): string + { + return $this->appId; + + }//end getAppId() + + /** + * Get the config key that was read. + * + * @return string The config key. + */ + public function getConfigKey(): string + { + return $this->configKey; + + }//end getConfigKey() + + /** + * Get the resolved value that could not be hydrated. + * + * @return string The slug or UUID the lookup attempted. + */ + public function getResolvedValue(): string + { + return $this->resolvedValue; + + }//end getResolvedValue() +}//end class diff --git a/lib/Service/Resolver/RegisterSchemaPair.php b/lib/Service/Resolver/RegisterSchemaPair.php new file mode 100644 index 0000000000..4b4cbe37c5 --- /dev/null +++ b/lib/Service/Resolver/RegisterSchemaPair.php @@ -0,0 +1,97 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Resolver; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; + +/** + * Readonly value object pairing a resolved register and schema. + */ +final class RegisterSchemaPair +{ + /** + * Constructor; capture every field as readonly. + * + * @param string $registerId The raw register slug/UUID string from app config. + * @param string $schemaId The raw schema slug/UUID string from app config. + * @param Register $register The hydrated Register entity. + * @param Schema $schema The hydrated Schema entity. + */ + public function __construct( + private readonly string $registerId, + private readonly string $schemaId, + private readonly Register $register, + private readonly Schema $schema, + ) { + + }//end __construct() + + /** + * Get the raw register slug/UUID the resolver started from. + * + * @return string The register identifier. + */ + public function getRegisterId(): string + { + return $this->registerId; + + }//end getRegisterId() + + /** + * Get the raw schema slug/UUID the resolver started from. + * + * @return string The schema identifier. + */ + public function getSchemaId(): string + { + return $this->schemaId; + + }//end getSchemaId() + + /** + * Get the hydrated Register entity. + * + * @return Register The Register entity. + */ + public function getRegister(): Register + { + return $this->register; + + }//end getRegister() + + /** + * Get the hydrated Schema entity. + * + * @return Schema The Schema entity. + */ + public function getSchema(): Schema + { + return $this->schema; + + }//end getSchema() +}//end class diff --git a/lib/Service/RetentionService.php b/lib/Service/RetentionService.php index a8ac4a43fd..c4f98ab4eb 100644 --- a/lib/Service/RetentionService.php +++ b/lib/Service/RetentionService.php @@ -20,16 +20,16 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-60 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-61 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-62 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-70 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-65 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-68 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-67 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-5 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-6 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-7 + * @spec openspec/specs/retention-management/spec.md#requirement-objects-must-carry-mdto-compliant-archival-metadata-in-the-retention-field + * @spec openspec/specs/retention-management/spec.md#requirement-the-system-must-calculate-archiefactiedatum-using-configurable-afleidingswijzen + * @spec openspec/specs/retention-management/spec.md#requirement-the-system-must-generate-destruction-lists-via-a-background-job + * @spec openspec/specs/retention-management/spec.md + * @spec openspec/specs/retention-management/spec.md + * @spec openspec/specs/retention-management/spec.md + * @spec openspec/specs/retention-management/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ declare(strict_types=1); @@ -131,8 +131,8 @@ public function __construct( * * @return ObjectEntity The object with archival metadata applied * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-60 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-70 + * @spec openspec/specs/retention-management/spec.md#requirement-objects-must-carry-mdto-compliant-archival-metadata-in-the-retention-field + * @spec openspec/specs/retention-management/spec.md */ public function applyArchivalMetadata(ObjectEntity $object, Schema $schema): ObjectEntity { @@ -203,8 +203,8 @@ public function applyArchivalMetadata(ObjectEntity $object, Schema $schema): Obj * * @return string|null ISO 8601 date string or null if calculation not possible * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-61 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-65 + * @spec openspec/specs/retention-management/spec.md#requirement-the-system-must-calculate-archiefactiedatum-using-configurable-afleidingswijzen + * @spec openspec/specs/retention-management/spec.md */ public function calculateArchiefactiedatum( ObjectEntity $object, @@ -315,7 +315,7 @@ private function determineBrondatum( * * @return ObjectEntity The object with recalculated dates * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-65 + * @spec openspec/specs/retention-management/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -400,7 +400,7 @@ public function recalculateArchiefactiedatum( * * @return array|null The selectielijst entry data or null if not found * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-5 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function lookupSelectielijstEntry(string $categorie): ?array { @@ -473,7 +473,7 @@ public function validateNotImmutable(ObjectEntity $object): ?string * * @return ObjectEntity The object with legal hold applied * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-7 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function placeLegalHold(ObjectEntity $object, string $reason): ObjectEntity { @@ -505,7 +505,7 @@ public function placeLegalHold(ObjectEntity $object, string $reason): ObjectEnti * * @return ObjectEntity The object with legal hold released * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-7 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function releaseLegalHold(ObjectEntity $object, string $reason): ObjectEntity { @@ -552,7 +552,7 @@ public function releaseLegalHold(ObjectEntity $object, string $reason): ObjectEn * * @return bool True if object has active legal hold * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-7 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function hasActiveLegalHold(ObjectEntity $object): bool { @@ -568,7 +568,7 @@ public function hasActiveLegalHold(ObjectEntity $object): bool * * @return ObjectEntity The object with extended archiefactiedatum * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-6 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function extendArchiefactiedatum(ObjectEntity $object, ?string $extensionPeriod=null): ObjectEntity { @@ -616,7 +616,7 @@ public function extendArchiefactiedatum(ObjectEntity $object, ?string $extension * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-5 + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function findEligibleForDestruction(array $excludeUuids=[]): array { @@ -662,7 +662,7 @@ public function findEligibleForDestruction(array $excludeUuids=[]): array } // Skip objects in an immutable archival status (vernietigd, overgebracht). - if ($this->validateNotImmutable($object) !== null) { + if ($this->validateNotImmutable(object: $object) !== null) { continue; } @@ -694,7 +694,7 @@ public function findEligibleForDestruction(array $excludeUuids=[]): array * * @return string[] Array of object UUIDs * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-62 + * @spec openspec/specs/retention-management/spec.md#requirement-the-system-must-generate-destruction-lists-via-a-background-job */ public function getObjectsOnPendingDestructionLists(): array { @@ -747,7 +747,7 @@ public function getObjectsOnPendingDestructionLists(): array * * @return array|null The destruction list data or null on failure * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-68 + * @spec openspec/specs/retention-management/spec.md */ public function createDestructionList(array $objects): ?array { @@ -809,9 +809,9 @@ public function createDestructionList(array $objects): ?array * * @return array The destruction certificate data * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-62 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-67 - * @spec openspec/changes/retrofit-2026-05-24-b3a-workflow-seed/tasks.md#task-2 + * @spec openspec/specs/retention-management/spec.md#requirement-the-system-must-generate-destruction-lists-via-a-background-job + * @spec openspec/specs/retention-management/spec.md + * @spec openspec/specs/archival-destruction-workflow/spec.md */ public function generateDestructionCertificate( array $destructionList, diff --git a/lib/Service/RiskLevelService.php b/lib/Service/RiskLevelService.php index 222e293008..db0c05a960 100644 --- a/lib/Service/RiskLevelService.php +++ b/lib/Service/RiskLevelService.php @@ -133,7 +133,7 @@ public function __construct( * * @return string Risk level constant (RISK_NONE through RISK_VERY_HIGH) * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-3/tasks.md#task-3 + * @spec openspec/specs/file-risk-classification/spec.md */ public function computeRiskLevel(int $fileId): string { @@ -171,7 +171,7 @@ public function computeRiskLevel(int $fileId): string * * @return string The computed risk level * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-3/tasks.md#task-4 + * @spec openspec/specs/file-risk-classification/spec.md */ public function updateRiskLevel(int $fileId): string { @@ -204,7 +204,7 @@ public function updateRiskLevel(int $fileId): string * * @return string Risk level constant * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-3/tasks.md#task-4 + * @spec openspec/specs/file-risk-classification/spec.md */ public function getRiskLevel(int $fileId): string { @@ -233,7 +233,7 @@ public function getRiskLevel(int $fileId): string * * @return void * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-3/tasks.md#task-4 + * @spec openspec/specs/file-risk-classification/spec.md */ public function initMetadataKey(): void { @@ -252,7 +252,7 @@ public function initMetadataKey(): void * * @return array Map of risk level value to human-readable label * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-3/tasks.md#task-4 + * @spec openspec/specs/file-risk-classification/spec.md */ public static function getAllRiskLevels(): array { diff --git a/lib/Service/ScheduledReportService.php b/lib/Service/ScheduledReportService.php new file mode 100644 index 0000000000..17a3c9e372 --- /dev/null +++ b/lib/Service/ScheduledReportService.php @@ -0,0 +1,1224 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use DateTime; +use DateTimeInterface; +use InvalidArgumentException; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\ScheduledReport; +use OCA\OpenRegister\Db\ScheduledReportMapper; +use OCA\OpenRegister\Exception\ExportTooLargeException; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IConfig; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Mail\IMailer; +use OCP\Notification\IManager; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use Psr\Log\LoggerInterface; +use RuntimeException; + +/** + * Service for managing and executing scheduled report exports. + * + * @SuppressWarnings(PHPMD.ExcessiveClassLength) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.TooManyMethods) The email-delivery leg + * (scheduled-report-email-delivery) added recipient resolution, + * attachment/oversize-fallback, and row-count helpers alongside the + * original Files-delivery/CRUD/due-check methods; each is a small, + * single-purpose private helper — splitting them into a second class + * would just relocate the method count behind an extra collaborator, + * same reasoning as ExcessiveClassComplexity below. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Owns validation, CRUD, + * due-computation, and single-report execution (export/deliver/notify) + * in one cohesive unit per design.md; splitting execution into its own + * class would just relocate the complexity behind an extra collaborator. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ +class ScheduledReportService +{ + + /** + * Export formats a scheduled report may use — a deliberate subset of + * `ExportService`'s supported set (excludes `json`; see design.md). + * + * @var string[] + */ + public const ALLOWED_FORMATS = ['csv', 'excel', 'pdf']; + + /** + * Supported schedule cadences (no cron expressions — see design.md). + * + * @var string[] + */ + public const ALLOWED_SCHEDULE_TYPES = ['daily', 'weekly', 'monthly']; + + /** + * Supported delivery modes. `files` is the original, pre-email-delivery + * behaviour and remains the default so existing reports are unaffected. + * + * @var string[] + */ + public const ALLOWED_DELIVERY_MODES = ['files', 'email', 'both']; + + /** + * Maximum number of recipient email addresses a report may configure. + * + * @var int + */ + public const MAX_RECIPIENTS = 20; + + /** + * Maximum export size, in bytes, that is attached directly to the + * delivery email. Larger exports fall back to Files delivery plus a + * link in the email body — SMTP servers/relays commonly cap message + * size well under typical export sizes for large registers. + * + * @var int + */ + public const MAX_EMAIL_ATTACHMENT_BYTES = 10485760; + + /** + * Elapsed-period thresholds in seconds, keyed by schedule type. + * + * @var array + */ + private const PERIOD_SECONDS = [ + 'daily' => 86400, + 'weekly' => 604800, + 'monthly' => 2592000, + ]; + + /** + * Constructor. + * + * @param ScheduledReportMapper $mapper Scheduled report mapper. + * @param RegisterMapper $registerMapper Register lookup (create/update validation). + * @param SchemaMapper $schemaMapper Schema lookup (create/update validation). + * @param ExportService $exportService The export engine this feature composes. + * @param IRootFolder $rootFolder Files root, for owner-folder delivery. + * @param IUserManager $userManager Resolves the owner IUser for impersonation. + * @param IUserSession $userSession Session, swapped to the owner during a run. + * @param IManager $notificationManager Notification dispatch. + * @param LoggerInterface $logger Logger. + * @param IMailer $mailer Sends the email-delivery leg (deliveryMode email|both). + * @param IConfig $config Resolves the instance's default mail sender. + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) DI-injected dependencies — IMailer/IConfig are the + * two email-delivery additions on top of the original 9; each is a distinct, testable collaborator + * (mail transport vs. system config) rather than an arbitrary parameter split. + */ + public function __construct( + private readonly ScheduledReportMapper $mapper, + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly ExportService $exportService, + private readonly IRootFolder $rootFolder, + private readonly IUserManager $userManager, + private readonly IUserSession $userSession, + private readonly IManager $notificationManager, + private readonly LoggerInterface $logger, + private readonly IMailer $mailer, + private readonly IConfig $config + ) { + }//end __construct() + + /** + * Find a scheduled report by id. + * + * @param int $id The scheduled report id. + * + * @return ScheduledReport + * + * @throws DoesNotExistException When no row matches. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function find(int $id): ScheduledReport + { + return $this->mapper->find(id: $id); + }//end find() + + /** + * List the reports owned by a user. + * + * @param string $ownerUid The owning uid. + * + * @return ScheduledReport[] + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function findForOwner(string $ownerUid): array + { + return $this->mapper->findByOwner(owner: $ownerUid); + }//end findForOwner() + + /** + * List every scheduled report (admin listing). + * + * @return ScheduledReport[] + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function findAllForAdmin(): array + { + return $this->mapper->findAll(); + }//end findAllForAdmin() + + /** + * Create a scheduled report owned by the given user. + * + * @param array $data Request payload. + * @param string $ownerUid The owning uid (always the caller — self-service creation). + * + * @return ScheduledReport + * + * @throws InvalidArgumentException When validation fails. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function create(array $data, string $ownerUid): ScheduledReport + { + $this->validate(data: $data); + + $now = new DateTime(); + + $report = new ScheduledReport(); + $report->setOwner($ownerUid); + $report->setName((string) $data['name']); + $report->setRegisterId((int) $data['registerId']); + $report->setSchemaId($this->coerceNullableInt(value: ($data['schemaId'] ?? null))); + $report->setFilters(json_encode(($data['filters'] ?? []), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + $report->setFormat((string) $data['format']); + $report->setScheduleType((string) $data['scheduleType']); + $report->setScheduleHour((int) ($data['scheduleHour'] ?? 0)); + $report->setScheduleDayOfWeek($this->coerceNullableInt(value: ($data['scheduleDayOfWeek'] ?? null))); + $report->setScheduleDayOfMonth($this->coerceNullableInt(value: ($data['scheduleDayOfMonth'] ?? null))); + $report->setDeliveryFolder((string) ($data['deliveryFolder'] ?? 'Reports/')); + $report->setDeliveryMode((string) ($data['deliveryMode'] ?? 'files')); + $report->setRecipients( + json_encode($this->normalizeRecipients(recipients: ($data['recipients'] ?? [])), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) + ); + $report->setEnabled(true); + $report->setLastRunAt(null); + $report->setLastStatus(null); + $report->setLastError(null); + $report->setCreatedAt($now); + $report->setUpdatedAt($now); + + return $this->mapper->insert(entity: $report); + }//end create() + + /** + * Update an existing scheduled report. Only the owner or an admin may update. + * + * @param int $id The scheduled report id. + * @param array $data Fields to update (same shape as create()). + * @param string $callerUid The calling user's uid. + * @param bool $callerIsAdmin Whether the caller is a Nextcloud administrator. + * + * @return ScheduledReport + * + * @throws DoesNotExistException When no row matches. + * @throws RuntimeException When the caller does not own the row and is not an admin. + * @throws InvalidArgumentException When validation fails. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function update(int $id, array $data, string $callerUid, bool $callerIsAdmin): ScheduledReport + { + $report = $this->mapper->find(id: $id); + $this->assertOwnerOrAdmin(report: $report, callerUid: $callerUid, callerIsAdmin: $callerIsAdmin); + + // Merge with existing values so a partial update doesn't blow away + // fields the caller didn't send. + $schemaId = $report->getSchemaId(); + if (array_key_exists('schemaId', $data) === true) { + $schemaId = $data['schemaId']; + } + + $scheduleDayOfWeek = $report->getScheduleDayOfWeek(); + if (array_key_exists('scheduleDayOfWeek', $data) === true) { + $scheduleDayOfWeek = $data['scheduleDayOfWeek']; + } + + $scheduleDayOfMonth = $report->getScheduleDayOfMonth(); + if (array_key_exists('scheduleDayOfMonth', $data) === true) { + $scheduleDayOfMonth = $data['scheduleDayOfMonth']; + } + + $merged = [ + 'name' => ($data['name'] ?? $report->getName()), + 'registerId' => ($data['registerId'] ?? $report->getRegisterId()), + 'schemaId' => $schemaId, + 'filters' => ($data['filters'] ?? $report->getFiltersArray()), + 'format' => ($data['format'] ?? $report->getFormat()), + 'scheduleType' => ($data['scheduleType'] ?? $report->getScheduleType()), + 'scheduleHour' => ($data['scheduleHour'] ?? $report->getScheduleHour()), + 'scheduleDayOfWeek' => $scheduleDayOfWeek, + 'scheduleDayOfMonth' => $scheduleDayOfMonth, + 'deliveryFolder' => ($data['deliveryFolder'] ?? $report->getDeliveryFolder()), + 'deliveryMode' => ($data['deliveryMode'] ?? ($report->getDeliveryMode() ?? 'files')), + 'recipients' => ($data['recipients'] ?? $report->getRecipientsArray()), + ]; + $this->validate(data: $merged); + + $report->setName((string) $merged['name']); + $report->setRegisterId((int) $merged['registerId']); + $report->setSchemaId($this->coerceNullableInt(value: $merged['schemaId'])); + $report->setFilters(json_encode($merged['filters'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + $report->setFormat((string) $merged['format']); + $report->setScheduleType((string) $merged['scheduleType']); + $report->setScheduleHour((int) $merged['scheduleHour']); + $report->setScheduleDayOfWeek($this->coerceNullableInt(value: $merged['scheduleDayOfWeek'])); + $report->setScheduleDayOfMonth($this->coerceNullableInt(value: $merged['scheduleDayOfMonth'])); + $report->setDeliveryFolder((string) $merged['deliveryFolder']); + $report->setDeliveryMode((string) $merged['deliveryMode']); + $report->setRecipients( + json_encode($this->normalizeRecipients(recipients: $merged['recipients']), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) + ); + if (array_key_exists('enabled', $data) === true) { + $report->setEnabled((bool) $data['enabled']); + } + + $report->setUpdatedAt(new DateTime()); + + return $this->mapper->update(entity: $report); + }//end update() + + /** + * Delete a scheduled report. Only the owner or an admin may delete. + * + * @param int $id The scheduled report id. + * @param string $callerUid The calling user's uid. + * @param bool $callerIsAdmin Whether the caller is a Nextcloud administrator. + * + * @return void + * + * @throws DoesNotExistException When no row matches. + * @throws RuntimeException When the caller does not own the row and is not an admin. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function delete(int $id, string $callerUid, bool $callerIsAdmin): void + { + $report = $this->mapper->find(id: $id); + $this->assertOwnerOrAdmin(report: $report, callerUid: $callerUid, callerIsAdmin: $callerIsAdmin); + $this->mapper->delete(entity: $report); + }//end delete() + + /** + * Assert the caller owns the report or is an admin. + * + * @param ScheduledReport $report The report. + * @param string $callerUid The calling user's uid. + * @param bool $callerIsAdmin Whether the caller is an admin. + * + * @return void + * + * @throws RuntimeException When the assertion fails. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function assertOwnerOrAdmin(ScheduledReport $report, string $callerUid, bool $callerIsAdmin): void + { + if ($callerIsAdmin === true) { + return; + } + + if ($report->getOwner() === $callerUid) { + return; + } + + throw new RuntimeException('You do not have permission to access this scheduled report.'); + }//end assertOwnerOrAdmin() + + /** + * Validate a create/update payload. Register/schema existence is checked + * via the caller's own request-scoped RBAC/multi-tenancy session (not an + * explicit owner parameter) — configuration-time validation intentionally + * uses the same access lens as every other read the caller performs. + * + * @param array $data The payload. + * + * @return void + * + * @throws InvalidArgumentException When any field is invalid. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function validate(array $data): void + { + if (empty($data['name']) === true) { + throw new InvalidArgumentException('A name is required.'); + } + + if (empty($data['registerId']) === true) { + throw new InvalidArgumentException('A registerId is required.'); + } + + try { + $this->registerMapper->find($data['registerId'], _multitenancy: true); + } catch (\Throwable $e) { + throw new InvalidArgumentException('Register not found or not accessible.'); + } + + $schemaId = $this->coerceNullableInt(value: ($data['schemaId'] ?? null)); + if ($schemaId !== null) { + try { + $this->schemaMapper->find($schemaId, _multitenancy: true); + } catch (\Throwable $e) { + throw new InvalidArgumentException('Schema not found or not accessible.'); + } + } + + $format = ($data['format'] ?? null); + if (in_array($format, self::ALLOWED_FORMATS, true) === false) { + throw new InvalidArgumentException( + sprintf('format must be one of: %s', implode(', ', self::ALLOWED_FORMATS)) + ); + } + + if ($format === 'csv' && $schemaId === null) { + throw new InvalidArgumentException('CSV export requires a specific schemaId.'); + } + + $scheduleType = ($data['scheduleType'] ?? null); + if (in_array($scheduleType, self::ALLOWED_SCHEDULE_TYPES, true) === false) { + throw new InvalidArgumentException( + sprintf('scheduleType must be one of: %s', implode(', ', self::ALLOWED_SCHEDULE_TYPES)) + ); + } + + $scheduleHour = (int) ($data['scheduleHour'] ?? 0); + if ($scheduleHour < 0 || $scheduleHour > 23) { + throw new InvalidArgumentException('scheduleHour must be between 0 and 23.'); + } + + if ($scheduleType === 'weekly') { + $dayOfWeek = $this->coerceNullableInt(value: ($data['scheduleDayOfWeek'] ?? null)); + if ($dayOfWeek === null || $dayOfWeek < 0 || $dayOfWeek > 6) { + throw new InvalidArgumentException('scheduleDayOfWeek (0-6) is required when scheduleType is weekly.'); + } + } + + if ($scheduleType === 'monthly') { + $dayOfMonth = $this->coerceNullableInt(value: ($data['scheduleDayOfMonth'] ?? null)); + if ($dayOfMonth === null || $dayOfMonth < 1 || $dayOfMonth > 28) { + throw new InvalidArgumentException('scheduleDayOfMonth (1-28) is required when scheduleType is monthly.'); + } + } + + $deliveryMode = ($data['deliveryMode'] ?? 'files'); + if (in_array($deliveryMode, self::ALLOWED_DELIVERY_MODES, true) === false) { + throw new InvalidArgumentException( + sprintf('deliveryMode must be one of: %s', implode(', ', self::ALLOWED_DELIVERY_MODES)) + ); + } + + $this->validateRecipients(recipients: ($data['recipients'] ?? [])); + }//end validate() + + /** + * Validate a recipients payload: must be an array of strings, each a + * syntactically valid email address, capped at {@see self::MAX_RECIPIENTS}. + * An empty/absent array is always valid — it means "default to the + * owner's own email at run time" (see `resolveRecipients()`). + * + * @param mixed $recipients The raw payload value. + * + * @return void + * + * @throws InvalidArgumentException When the payload isn't a valid recipient list. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + private function validateRecipients(mixed $recipients): void + { + if (is_array($recipients) === false) { + throw new InvalidArgumentException('recipients must be an array of email addresses.'); + } + + if (count($recipients) > self::MAX_RECIPIENTS) { + throw new InvalidArgumentException(sprintf('recipients cannot exceed %d addresses.', self::MAX_RECIPIENTS)); + } + + foreach ($recipients as $email) { + if (is_string($email) === false) { + throw new InvalidArgumentException(sprintf('"%s" is not a valid email address.', gettype($email))); + } + + // Trimmed the same way normalizeRecipients() will store it — + // incidental whitespace shouldn't fail validation only to have + // the trimmed form pass moments later. + if (filter_var(trim($email), FILTER_VALIDATE_EMAIL) === false) { + throw new InvalidArgumentException(sprintf('"%s" is not a valid email address.', $email)); + } + } + }//end validateRecipients() + + /** + * Normalize an already-validated recipients payload to a de-duplicated + * list of trimmed email strings ready for JSON storage. + * + * @param mixed $recipients The raw payload value (already passed {@see validateRecipients()}). + * + * @return string[] + */ + private function normalizeRecipients(mixed $recipients): array + { + if (is_array($recipients) === false) { + return []; + } + + $normalized = []; + foreach ($recipients as $email) { + if (is_string($email) === false) { + continue; + } + + $trimmed = trim($email); + if ($trimmed !== '' && in_array($trimmed, $normalized, true) === false) { + $normalized[] = $trimmed; + } + } + + return $normalized; + }//end normalizeRecipients() + + /** + * Coerce a value to a nullable int. Empty string and null both become null. + * + * @param mixed $value Input. + * + * @return ?int + */ + private function coerceNullableInt(mixed $value): ?int + { + if ($value === null || $value === '') { + return null; + } + + if (is_numeric($value) === false) { + return null; + } + + return (int) $value; + }//end coerceNullableInt() + + /** + * Whether a report is due to run now. + * + * Catch-up-safe elapsed-period check per design.md: due when never run, + * or when at least a full schedule period has elapsed since the last run. + * Disabled reports are never due. + * + * @param ScheduledReport $report The report to check. + * @param DateTimeInterface $now The reference time. + * + * @return bool + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function isDue(ScheduledReport $report, DateTimeInterface $now): bool + { + if ($report->getEnabled() !== true) { + return false; + } + + $lastRunAt = $report->getLastRunAt(); + if ($lastRunAt === null) { + return true; + } + + $periodSeconds = (self::PERIOD_SECONDS[$report->getScheduleType()] ?? self::PERIOD_SECONDS['daily']); + $elapsed = ($now->getTimestamp() - $lastRunAt->getTimestamp()); + + return $elapsed >= $periodSeconds; + }//end isDue() + + /** + * Execute a single scheduled report: export as the owner, deliver per + * `deliveryMode` (Files, email, or both), notify the owner, and persist + * the outcome. Never throws — all failures (including + * `ExportTooLargeException`) are caught, recorded on the report, and + * notified, so a caller (job or run-now) can iterate a batch without + * per-report try/catch of its own (though callers SHOULD still wrap + * this call for defence in depth — see ScheduledReportJob). + * + * The email leg is isolated from the Files leg: when `deliveryMode` is + * `both` and Files delivery succeeds but email fails, the run is marked + * `email_failed` (not `failed`) — the export WAS delivered, just not by + * every requested channel. When `deliveryMode` is `email` only and the + * email leg fails outright, the run is marked `failed` since nothing was + * delivered. + * + * @param ScheduledReport $report The report to run. + * + * @return void + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + public function runOne(ScheduledReport $report): void + { + $ownerUid = $report->getOwner(); + $owner = null; + if ($ownerUid !== null) { + $owner = $this->userManager->get($ownerUid); + } + + if ($owner === null) { + $this->logger->warning( + message: '[ScheduledReportService] Owner account not found — skipping run', + context: ['file' => __FILE__, 'line' => __LINE__, 'reportId' => $report->getId(), 'owner' => $ownerUid] + ); + $this->markOutcome(report: $report, status: 'failed', error: 'Owner account no longer exists.'); + return; + } + + $previousUser = $this->userSession->getUser(); + $this->userSession->setUser($owner); + + try { + $export = $this->runExport(report: $report, owner: $owner); + $filename = $this->buildFilename(report: $report); + $mode = ($report->getDeliveryMode() ?? 'files'); + + $delivered = false; + if (in_array($mode, ['files', 'both'], true) === true) { + $this->deliverToFiles(report: $report, owner: $owner, filename: $filename, bytes: $export['bytes']); + $delivered = true; + } + + $emailFailureReason = null; + if (in_array($mode, ['email', 'both'], true) === true) { + $emailFailureReason = $this->deliverToEmail( + report: $report, + owner: $owner, + filename: $filename, + bytes: $export['bytes'], + rowCount: $export['rowCount'], + alreadyDelivered: $delivered + ); + } + + $this->finalizeOutcome( + report: $report, + filename: $filename, + delivered: $delivered, + emailFailureReason: $emailFailureReason + ); + } catch (ExportTooLargeException $e) { + $this->logger->warning( + message: '[ScheduledReportService] Export too large', + context: ['file' => __FILE__, 'line' => __LINE__, 'reportId' => $report->getId(), 'error' => $e->getMessage()] + ); + $this->markOutcome(report: $report, status: 'failed', error: $e->getMessage()); + $this->notifyOwner(report: $report, success: false, reason: $e->getMessage(), filename: null, emailFailureReason: null); + } catch (\Throwable $e) { + $this->logger->error( + message: '[ScheduledReportService] Scheduled report run failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'reportId' => $report->getId(), + 'error' => $e->getMessage(), + ] + ); + $this->markOutcome(report: $report, status: 'failed', error: $e->getMessage()); + $this->notifyOwner(report: $report, success: false, reason: $e->getMessage(), filename: null, emailFailureReason: null); + } finally { + $this->userSession->setUser($previousUser); + }//end try + }//end runOne() + + /** + * Determine and persist a run's final outcome from its two independent + * delivery legs, then notify the owner. Extracted out of `runOne()` as + * three mutually-exclusive early-return cases (rather than an + * if/elseif/else chain inline) so each outcome reads as a single, + * self-contained rule: + * - no email failure → `success`. + * - email failed, but Files already delivered → `email_failed` (isolated). + * - email failed and nothing else delivered → `failed`. + * + * @param ScheduledReport $report The report. + * @param string $filename The delivery filename. + * @param bool $delivered Whether the Files leg delivered this pass. + * @param string|null $emailFailureReason The email leg's failure reason, or null on success/not-requested. + * + * @return void + */ + private function finalizeOutcome(ScheduledReport $report, string $filename, bool $delivered, ?string $emailFailureReason): void + { + if ($emailFailureReason === null) { + $this->markOutcome(report: $report, status: 'success', error: null); + $this->notifyOwner(report: $report, success: true, reason: null, filename: $filename, emailFailureReason: null); + return; + } + + if ($delivered === true) { + // Files delivery (the requested Files/both leg) succeeded — + // don't report a hard failure, but do surface the distinct + // email problem. + $this->markOutcome(report: $report, status: 'email_failed', error: $emailFailureReason); + $this->notifyOwner(report: $report, success: true, reason: null, filename: $filename, emailFailureReason: $emailFailureReason); + return; + } + + // Email-only mode and the email leg failed — nothing was delivered. + $this->markOutcome(report: $report, status: 'failed', error: $emailFailureReason); + $this->notifyOwner(report: $report, success: false, reason: $emailFailureReason, filename: null, emailFailureReason: null); + }//end finalizeOutcome() + + /** + * Resolve register/schema and run the format-appropriate ExportService + * method (unchanged from the original `exportBytes()` call surface — + * still exactly one `ExportService` call per format, preserving prior + * behaviour/tests), also deriving a best-effort object row count for the + * email body. Row counting never risks turning an otherwise-successful + * export into a failure: `csv`'s count is parsed from the bytes already + * produced (no extra fetch), `excel`'s count comes from the `Spreadsheet` + * object already returned by `exportToExcel()`, and `pdf`'s count is a + * best-effort secondary call swallowed on any error (see + * `bestEffortRowCount()`). + * + * @param ScheduledReport $report The report. + * @param \OCP\IUser $owner The impersonated owner (drives RBAC-aware admin-metadata column visibility). + * + * @return array{bytes: string, rowCount: int} + * + * @throws ExportTooLargeException When the pdf row cap is exceeded. + */ + private function runExport(ScheduledReport $report, \OCP\IUser $owner): array + { + $register = $this->registerMapper->find($report->getRegisterId(), _rbac: false, _multitenancy: false); + $schema = null; + if ($report->getSchemaId() !== null) { + $schema = $this->schemaMapper->find($report->getSchemaId(), _rbac: false, _multitenancy: false); + } + + $filters = $report->getFiltersArray(); + + switch ($report->getFormat()) { + case 'csv': + $bytes = $this->exportService->exportToCsv(register: $register, schema: $schema, filters: $filters, currentUser: $owner); + return ['bytes' => $bytes, 'rowCount' => $this->countCsvRows(csv: $bytes)]; + + case 'pdf': + $bytes = $this->exportService->exportToPdf(register: $register, schema: $schema, filters: $filters, currentUser: $owner); + $rowCount = $this->bestEffortRowCount(register: $register, schema: $schema, filters: $filters, owner: $owner); + return ['bytes' => $bytes, 'rowCount' => $rowCount]; + + default: + $spreadsheet = $this->exportService->exportToExcel(register: $register, schema: $schema, filters: $filters, currentUser: $owner); + return [ + 'bytes' => $this->spreadsheetToXlsxBytes(spreadsheet: $spreadsheet), + 'rowCount' => $this->countSpreadsheetRows(spreadsheet: $spreadsheet), + ]; + }//end switch + }//end runExport() + + /** + * Count data rows in CSV bytes (total non-empty lines minus the header row). + * + * @param string $csv The CSV content. + * + * @return int + */ + private function countCsvRows(string $csv): int + { + $lines = array_filter(explode("\n", $csv), static fn ($line) => trim($line) !== ''); + + return max(0, (count($lines) - 1)); + }//end countCsvRows() + + /** + * Sum data-row counts (excluding each sheet's header row) across every + * sheet in a spreadsheet. + * + * @param Spreadsheet $spreadsheet The spreadsheet. + * + * @return int + */ + private function countSpreadsheetRows(Spreadsheet $spreadsheet): int + { + $rows = 0; + foreach ($spreadsheet->getAllSheets() as $sheet) { + $rows += max(0, ($sheet->getHighestRow() - 1)); + } + + return $rows; + }//end countSpreadsheetRows() + + /** + * Best-effort row count for a pdf export, used only for the delivery + * email body — never allowed to turn an otherwise-successful pdf export + * into a failure. + * + * @param Register $register The register. + * @param Schema|null $schema The schema, if any. + * @param array $filters The filter map. + * @param \OCP\IUser $owner The impersonated owner. + * + * @return int + */ + private function bestEffortRowCount(Register $register, ?Schema $schema, array $filters, \OCP\IUser $owner): int + { + try { + $spreadsheet = $this->exportService->exportToExcel(register: $register, schema: $schema, filters: $filters, currentUser: $owner); + return $this->countSpreadsheetRows(spreadsheet: $spreadsheet); + } catch (\Throwable $e) { + $this->logger->debug( + message: '[ScheduledReportService] Could not derive pdf export row count for email body', + context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] + ); + return 0; + } + }//end bestEffortRowCount() + + /** + * Write a PhpSpreadsheet Spreadsheet to raw XLSX bytes. + * + * @param Spreadsheet $spreadsheet The spreadsheet. + * + * @return string Raw XLSX bytes. + */ + private function spreadsheetToXlsxBytes(Spreadsheet $spreadsheet): string + { + $writer = new Xlsx($spreadsheet); + ob_start(); + $writer->save('php://output'); + return ob_get_clean(); + }//end spreadsheetToXlsxBytes() + + /** + * Build the dated delivery filename for a report. + * + * @param ScheduledReport $report The report. + * + * @return string + */ + private function buildFilename(ScheduledReport $report): string + { + $extension = match ($report->getFormat()) { + 'csv' => 'csv', + 'pdf' => 'pdf', + default => 'xlsx', + }; + + $slug = $this->slugify(value: (string) $report->getName()); + $date = (new DateTime())->format('Y-m-d'); + + return sprintf('%s_%s.%s', $slug, $date, $extension); + }//end buildFilename() + + /** + * Slugify a value for use in a filename. + * + * @param string $value The value. + * + * @return string + */ + private function slugify(string $value): string + { + $slug = strtolower(trim($value)); + $slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? $slug; + $slug = trim($slug, '-'); + if ($slug === '') { + return 'report'; + } + + return $slug; + }//end slugify() + + /** + * Deliver rendered bytes into the owner's Files, under the configured + * (sanitized) delivery folder. Mirrors `ReportRenderJob::writeToFiles()`'s + * ownership + path-traversal hardening. + * + * @param ScheduledReport $report The report. + * @param \OCP\IUser $owner The owner. + * @param string $filename The delivery filename. + * @param string $bytes The rendered bytes. + * + * @return void + * + * @throws RuntimeException When the delivery folder is rejected or the user folder is unavailable. + */ + private function deliverToFiles(ScheduledReport $report, \OCP\IUser $owner, string $filename, string $bytes): void + { + try { + $userFolder = $this->rootFolder->getUserFolder(userId: $owner->getUID()); + } catch (NotFoundException $e) { + throw new RuntimeException('Owner Nextcloud Files folder unavailable: '.$e->getMessage()); + } + + $folderPath = trim((string) ($report->getDeliveryFolder() ?? 'Reports/'), '/'); + if ($folderPath === '') { + $folderPath = 'Reports'; + } + + if (str_contains($folderPath, '..') === true) { + throw new RuntimeException('Delivery folder contains a path-traversal segment and was rejected.'); + } + + if ($userFolder->nodeExists(path: $folderPath) === false) { + $userFolder->newFolder(path: $folderPath); + } + + $folder = $userFolder->get(path: $folderPath); + if ($folder->nodeExists(path: $filename) === true) { + $folder->get(path: $filename)->putContent(data: $bytes); + return; + } + + $folder->newFile(path: $filename, content: $bytes); + }//end deliverToFiles() + + /** + * Deliver the export by email (deliveryMode email|both). Attaches the + * export when it's under {@see self::MAX_EMAIL_ATTACHMENT_BYTES}; over + * that cap, the attachment is omitted and, if it wasn't already written + * by the Files leg, the export is written to Files as a fallback so the + * data is never silently lost — the email body then links to it instead. + * + * Never throws — every failure (no resolvable recipient, Files fallback + * failure, `IMailer::send()` failure) is caught and returned as a + * human-readable reason string so `runOne()` can decide, based on + * whether the Files leg already succeeded, whether this is a hard + * failure or an isolated `email_failed` detail. + * + * @param ScheduledReport $report The report. + * @param \OCP\IUser $owner The owner (recipient default + display name). + * @param string $filename The delivery filename. + * @param string $bytes The rendered export bytes. + * @param int $rowCount The best-effort exported row count, for the email body. + * @param bool $alreadyDelivered Whether the Files leg already ran this pass (mode `both`). + * + * @return string|null The failure reason, or null on success. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + private function deliverToEmail( + ScheduledReport $report, + \OCP\IUser $owner, + string $filename, + string $bytes, + int $rowCount, + bool $alreadyDelivered + ): ?string { + $recipients = $this->resolveRecipients(report: $report, owner: $owner); + if (count($recipients) === 0) { + return 'No valid recipient email address available (no recipients configured and the owner has no email on file).'; + } + + $oversize = (strlen($bytes) > self::MAX_EMAIL_ATTACHMENT_BYTES); + + if ($oversize === true && $alreadyDelivered === false) { + try { + $this->deliverToFiles(report: $report, owner: $owner, filename: $filename, bytes: $bytes); + } catch (\Throwable $e) { + return 'Export exceeds the email attachment size limit and the Files fallback failed: '.$e->getMessage(); + } + } + + try { + $subject = sprintf('Scheduled report "%s"', $report->getName()); + + $bodyLines = $this->buildEmailBodyLines(report: $report, rowCount: $rowCount); + if ($oversize === true) { + $bodyLines[] = sprintf( + 'The export (%s) exceeds the %dMB email attachment limit and was not attached. ' + .'It has been saved to your Nextcloud Files at %s%s instead.', + $this->humanFileSize(bytes: strlen($bytes)), + (int) (self::MAX_EMAIL_ATTACHMENT_BYTES / 1024 / 1024), + trim((string) ($report->getDeliveryFolder() ?? 'Reports/'), '/'), + '/'.$filename + ); + } + + $emailTemplate = $this->mailer->createEMailTemplate('openregister.scheduledReportDelivery', []); + $emailTemplate->setSubject($subject); + $emailTemplate->addHeader(); + $emailTemplate->addHeading($report->getName()); + foreach ($bodyLines as $line) { + $emailTemplate->addBodyText($line); + } + + $emailTemplate->addFooter(); + + $message = $this->mailer->createMessage(); + $message->setTo($recipients); + $message->setSubject($subject); + $message->useTemplate($emailTemplate); + $message->setFrom($this->resolveFromAddress()); + + if ($oversize === false) { + $attachment = $this->mailer->createAttachment( + $bytes, + $filename, + $this->mimeTypeFor(format: $report->getFormat()) + ); + $message->attach($attachment); + } + + $this->mailer->send($message); + } catch (\Throwable $e) { + return 'Email delivery failed: '.$e->getMessage(); + }//end try + + return null; + }//end deliverToEmail() + + /** + * Resolve the email recipients for a report: the explicitly configured + * list, or — when empty — the owner's own Nextcloud email address. An + * owner with no email on file and no explicit recipients resolves to no + * recipients at all (the caller treats that as an email-leg failure). + * + * @param ScheduledReport $report The report. + * @param \OCP\IUser $owner The owner. + * + * @return array Email address => display name, ready for `IMessage::setTo()`. + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + private function resolveRecipients(ScheduledReport $report, \OCP\IUser $owner): array + { + $configured = $report->getRecipientsArray(); + if (count($configured) > 0) { + $map = []; + foreach ($configured as $email) { + $map[$email] = $email; + } + + return $map; + } + + $ownerEmail = $owner->getEMailAddress(); + if ($ownerEmail === null || $ownerEmail === '') { + return []; + } + + return [$ownerEmail => $owner->getDisplayName()]; + }//end resolveRecipients() + + /** + * Build the delivery email's body text lines: register/schema, period, row count. + * + * @param ScheduledReport $report The report. + * @param int $rowCount The exported row count. + * + * @return string[] + */ + private function buildEmailBodyLines(ScheduledReport $report, int $rowCount): array + { + $lines = []; + $lines[] = sprintf('Your scheduled report "%s" ran and delivered %d row(s).', $report->getName(), $rowCount); + + try { + $register = $this->registerMapper->find($report->getRegisterId(), _rbac: false, _multitenancy: false); + $registerName = ($register->getTitle() ?? ('register '.$report->getRegisterId())); + } catch (\Throwable $e) { + $registerName = ('register '.$report->getRegisterId()); + } + + $schemaName = 'all schemas'; + if ($report->getSchemaId() !== null) { + try { + $schema = $this->schemaMapper->find($report->getSchemaId(), _rbac: false, _multitenancy: false); + $schemaName = ($schema->getTitle() ?? ('schema '.$report->getSchemaId())); + } catch (\Throwable $e) { + $schemaName = ('schema '.$report->getSchemaId()); + } + } + + $lines[] = sprintf('Source: %s / %s.', $registerName, $schemaName); + $lines[] = sprintf('Schedule: %s.', ($report->getScheduleType() ?? 'daily')); + + return $lines; + }//end buildEmailBodyLines() + + /** + * Resolve the instance's default outgoing mail sender, mirroring + * `FlowActionService::runEmail()`'s `mail_from_address`/`mail_domain` + * system-config pattern. + * + * @return array + */ + private function resolveFromAddress(): array + { + $from = $this->config->getSystemValue('mail_from_address', 'no-reply'); + $domain = $this->config->getSystemValue('mail_domain', 'localhost'); + + return [$from.'@'.$domain => 'OpenRegister']; + }//end resolveFromAddress() + + /** + * MIME type for an export format's attachment. + * + * @param string|null $format csv|excel|pdf. + * + * @return string + */ + private function mimeTypeFor(?string $format): string + { + return match ($format) { + 'csv' => 'text/csv', + 'pdf' => 'application/pdf', + default => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }; + }//end mimeTypeFor() + + /** + * Human-readable byte size, e.g. "12.3MB". + * + * @param int $bytes The byte count. + * + * @return string + */ + private function humanFileSize(int $bytes): string + { + if ($bytes >= 1048576) { + return sprintf('%.1fMB', ($bytes / 1048576)); + } + + if ($bytes >= 1024) { + return sprintf('%.1fKB', ($bytes / 1024)); + } + + return sprintf('%dB', $bytes); + }//end humanFileSize() + + /** + * Persist the outcome of a run. + * + * @param ScheduledReport $report The report. + * @param string $status success|failed|email_failed. + * @param string|null $error The failure reason, if any. + * + * @return void + */ + private function markOutcome(ScheduledReport $report, string $status, ?string $error): void + { + $report->setLastRunAt(new DateTime()); + $report->setLastStatus($status); + $report->setLastError($error); + $report->setUpdatedAt(new DateTime()); + + try { + $this->mapper->update(entity: $report); + } catch (\Throwable $e) { + $this->logger->error( + message: '[ScheduledReportService] Failed to persist run outcome', + context: ['file' => __FILE__, 'line' => __LINE__, 'reportId' => $report->getId(), 'error' => $e->getMessage()] + ); + } + }//end markOutcome() + + /** + * Notify the report owner of a run's outcome. Reuses the existing + * `scheduled_report_delivered`/`scheduled_report_failed` subjects for + * every deliveryMode — `mode` and `emailFailureReason` are passed as + * extra, optional subject parameters so `Notifier` can render + * email-aware wording without a new notification subject. + * + * @param ScheduledReport $report The report. + * @param bool $success Whether the run succeeded (or partially succeeded — see `email_failed`). + * @param string|null $reason The failure reason (failure only). + * @param string|null $filename The delivered filename (success only). + * @param string|null $emailFailureReason Set when Files succeeded but the email leg failed (mode `both`). + * + * @return void + * + * @spec openspec/specs/scheduled-report-jobs/spec.md + */ + private function notifyOwner( + ScheduledReport $report, + bool $success, + ?string $reason, + ?string $filename, + ?string $emailFailureReason + ): void { + $ownerUid = $report->getOwner(); + if ($ownerUid === null) { + return; + } + + try { + $mode = ($report->getDeliveryMode() ?? 'files'); + $subject = 'scheduled_report_failed'; + $parameters = [ + 'reportId' => $report->getId(), + 'reportName' => $report->getName(), + 'reason' => ($reason ?? 'Unknown error'), + ]; + if ($success === true) { + $subject = 'scheduled_report_delivered'; + $parameters = [ + 'reportId' => $report->getId(), + 'reportName' => $report->getName(), + 'folder' => $report->getDeliveryFolder(), + 'filename' => $filename, + 'mode' => $mode, + 'emailFailureReason' => $emailFailureReason, + ]; + } + + $notification = $this->notificationManager->createNotification(); + $notification->setApp('openregister') + ->setUser($ownerUid) + ->setDateTime(new DateTime()) + ->setObject(type: 'scheduled_report', id: (string) $report->getId()) + ->setSubject(subject: $subject, parameters: $parameters); + + $this->notificationManager->notify($notification); + } catch (\Throwable $e) { + $this->logger->warning( + message: '[ScheduledReportService] Failed to send outcome notification', + context: ['file' => __FILE__, 'line' => __LINE__, 'reportId' => $report->getId(), 'error' => $e->getMessage()] + ); + }//end try + }//end notifyOwner() +}//end class diff --git a/lib/Service/Schema/MigrationPlanResult.php b/lib/Service/Schema/MigrationPlanResult.php new file mode 100644 index 0000000000..3a5ac08fc6 --- /dev/null +++ b/lib/Service/Schema/MigrationPlanResult.php @@ -0,0 +1,136 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Schema; + +/** + * Outcome of applying a transform chain to one object's data. + */ +final class MigrationPlanResult +{ + + /** + * The resulting data after applying the transform chain. + * + * @var array + */ + private array $data; + + /** + * Whether the data changed relative to the input. + * + * @var boolean + */ + private bool $changed; + + /** + * The failure reason, or null when the chain applied cleanly. + * + * @var string|null + */ + private ?string $failure; + + /** + * Ordered descriptions of the transforms that were applied. + * + * @var array + */ + private array $applied; + + /** + * Constructor. + * + * @param array $data Resulting data. + * @param bool $changed Whether data changed. + * @param string|null $failure Failure reason, or null. + * @param array $applied Applied transform descriptions. + */ + public function __construct(array $data, bool $changed, ?string $failure=null, array $applied=[]) + { + $this->data = $data; + $this->changed = $changed; + $this->failure = $failure; + $this->applied = $applied; + + }//end __construct() + + /** + * Get the resulting data. + * + * @return array The data. + */ + public function getData(): array + { + return $this->data; + + }//end getData() + + /** + * Whether the data changed. + * + * @return bool True when changed. + */ + public function isChanged(): bool + { + return $this->changed; + + }//end isChanged() + + /** + * Whether the chain failed for this object. + * + * @return bool True when a transform failed. + */ + public function isFailed(): bool + { + return $this->failure !== null; + + }//end isFailed() + + /** + * Get the failure reason, if any. + * + * @return string|null The failure reason. + */ + public function getFailure(): ?string + { + return $this->failure; + + }//end getFailure() + + /** + * Get the applied transform descriptions. + * + * @return array The descriptions. + */ + public function getApplied(): array + { + return $this->applied; + + }//end getApplied() +}//end class diff --git a/lib/Service/Schema/SchemaChangeSet.php b/lib/Service/Schema/SchemaChangeSet.php new file mode 100644 index 0000000000..8fe408a108 --- /dev/null +++ b/lib/Service/Schema/SchemaChangeSet.php @@ -0,0 +1,172 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Schema; + +use JsonSerializable; + +/** + * Immutable structural diff of two schema definitions. + * + * @phpstan-type Change array{property: string, kind: string, old?: mixed, new?: mixed} + */ +final class SchemaChangeSet implements JsonSerializable +{ + + /** + * Classification: no definition change at all (metadata-only save). + * + * @var string + */ + public const CLASS_NONE = 'none'; + + /** + * Classification: additive / relaxing change, safe for existing data. + * + * @var string + */ + public const CLASS_COMPATIBLE = 'compatible'; + + /** + * Classification: removes/renames/tightens — may invalidate existing data. + * + * @var string + */ + public const CLASS_BREAKING = 'breaking'; + + /** + * The typed list of changes. + * + * @var array> + */ + private array $changes; + + /** + * The overall classification. + * + * @var string + */ + private string $classification; + + /** + * The derived version bump level (`major` | `minor` | `patch` | `none`). + * + * @var string + */ + private string $bump; + + /** + * Constructor. + * + * @param array> $changes Typed change list. + * @param string $classification One of the CLASS_* constants. + * @param string $bump One of major|minor|patch|none. + */ + public function __construct(array $changes, string $classification, string $bump) + { + $this->changes = array_values($changes); + $this->classification = $classification; + $this->bump = $bump; + + }//end __construct() + + /** + * Get the typed change list. + * + * @return array> The changes. + */ + public function getChanges(): array + { + return $this->changes; + + }//end getChanges() + + /** + * Get the classification. + * + * @return string The classification (CLASS_* constant). + */ + public function getClassification(): string + { + return $this->classification; + + }//end getClassification() + + /** + * Get the derived semantic-version bump level. + * + * @return string One of major|minor|patch|none. + */ + public function getBump(): string + { + return $this->bump; + + }//end getBump() + + /** + * Whether the change set is classified breaking. + * + * @return bool True when breaking. + */ + public function isBreaking(): bool + { + return $this->classification === self::CLASS_BREAKING; + + }//end isBreaking() + + /** + * Whether there is any structural change at all. + * + * @return bool True when the definitions differ structurally. + */ + public function hasChanges(): bool + { + return count($this->changes) > 0; + + }//end hasChanges() + + /** + * JSON serialisation. + * + * @return array The serialised change set. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function jsonSerialize(): array + { + return [ + 'classification' => $this->classification, + 'bump' => $this->bump, + 'changes' => $this->changes, + ]; + + }//end jsonSerialize() +}//end class diff --git a/lib/Service/Schema/SchemaDiffService.php b/lib/Service/Schema/SchemaDiffService.php new file mode 100644 index 0000000000..37608d6974 --- /dev/null +++ b/lib/Service/Schema/SchemaDiffService.php @@ -0,0 +1,605 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Schema; + +/** + * Structural schema diff + classification. + */ +class SchemaDiffService +{ + + /** + * Constraint keywords whose tightening is breaking and whose + * relaxation is compatible. The boolean is the "direction" hint: + * true = a larger numeric value tightens (e.g. minLength, minItems), + * false = a smaller numeric value tightens (e.g. maxLength, maxItems). + * + * @var array + */ + private const NUMERIC_BOUND_TIGHTENS_UP = [ + 'minLength' => true, + 'minItems' => true, + 'minimum' => true, + 'minProperties' => true, + 'maxLength' => false, + 'maxItems' => false, + 'maximum' => false, + 'maxProperties' => false, + ]; + + /** + * Diff two schema definitions and classify the change set. + * + * Each definition is the schema's serialised shape, at minimum a + * `properties` map (property name => definition array) and an + * optional `required` list of property names. + * + * @param array $old Previous definition. + * @param array $new New definition. + * @param array $renames Optional declared renames + * (old name => new name). When + * a property is declared renamed + * it is classified as a single + * `renamed` (breaking) change + * rather than remove + add. + * + * @return SchemaChangeSet The typed, classified change set. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function diff(array $old, array $new, array $renames=[]): SchemaChangeSet + { + $oldProps = $this->properties(definition: $old); + $newProps = $this->properties(definition: $new); + $oldReq = $this->requiredSet(definition: $old); + $newReq = $this->requiredSet(definition: $new); + + $changes = []; + $hasBreaking = false; + $hasAddedProp = false; + $hasOtherChange = false; + + // Map renamed-away source names so they are not double-counted as removals. + $renamedFrom = array_keys($renames); + $renamedTo = array_values($renames); + + // Declared renames first. + foreach ($renames as $from => $to) { + if (array_key_exists($from, $oldProps) === false || array_key_exists($to, $newProps) === false) { + // The declared rename does not match the definitions; treat as + // ordinary add/remove handled below. + continue; + } + + $changes[] = [ + 'property' => $from, + 'kind' => 'renamed', + 'old' => $from, + 'new' => $to, + ]; + $hasBreaking = true; + } + + // Removed properties (not accounted for by a rename). + foreach ($oldProps as $name => $def) { + if (array_key_exists($name, $newProps) === true) { + continue; + } + + if (in_array($name, $renamedFrom, true) === true) { + continue; + } + + $changes[] = [ + 'property' => $name, + 'kind' => 'removed', + 'old' => $def, + ]; + $hasBreaking = true; + } + + // Added properties (not the target of a rename). + foreach ($newProps as $name => $def) { + if (array_key_exists($name, $oldProps) === true) { + continue; + } + + if (in_array($name, $renamedTo, true) === true) { + continue; + } + + $isRequired = in_array($name, $newReq, true); + $hasDefault = array_key_exists('default', (array) $def); + $changes[] = [ + 'property' => $name, + 'kind' => 'added', + 'new' => $def, + ]; + + if ($isRequired === true && $hasDefault === false) { + // New required property with no default invalidates every + // existing object that lacks it. + $changes[] = [ + 'property' => $name, + 'kind' => 'added_required_no_default', + 'new' => $def, + ]; + $hasBreaking = true; + } else { + $hasAddedProp = true; + } + }//end foreach + + // Changed properties (present in both). + foreach ($newProps as $name => $newDef) { + if (array_key_exists($name, $oldProps) === false) { + continue; + } + + $propChanges = $this->diffProperty(name: $name, oldDef: (array) $oldProps[$name], newDef: (array) $newDef); + foreach ($propChanges as $pc) { + $changes[] = $pc; + if ($this->kindIsBreaking(kind: $pc['kind']) === true) { + $hasBreaking = true; + } else { + $hasOtherChange = true; + } + } + } + + // Required-set changes for properties that exist in both definitions. + foreach ($newReq as $name) { + if (in_array($name, $oldReq, true) === true) { + continue; + } + + // Newly required. Already handled above if it is also a new property. + if (array_key_exists($name, $oldProps) === false) { + continue; + } + + $hasDefault = array_key_exists('default', (array) ($newProps[$name] ?? [])); + if ($hasDefault === true) { + $changes[] = [ + 'property' => $name, + 'kind' => 'required_added_with_default', + ]; + $hasOtherChange = true; + } else { + $changes[] = [ + 'property' => $name, + 'kind' => 'required_added', + ]; + $hasBreaking = true; + } + }//end foreach + + foreach ($oldReq as $name) { + if (in_array($name, $newReq, true) === true) { + continue; + } + + if (array_key_exists($name, $newProps) === false) { + // Property removed entirely; already counted as removed. + continue; + } + + // Relaxing a required constraint is compatible. + $changes[] = [ + 'property' => $name, + 'kind' => 'required_removed', + ]; + $hasOtherChange = true; + } + + return $this->classify(changes: $changes, hasBreaking: $hasBreaking, hasAddedProp: $hasAddedProp, hasOtherChange: $hasOtherChange); + + }//end diff() + + /** + * Compute the next version string given the current one and a change set. + * + * Follows semantic versioning: breaking → major, added property → + * minor, any other compatible change → patch, no change → unchanged. + * + * @param string|null $currentVersion Current version (defaults to 0.0.0). + * @param SchemaChangeSet $changeSet The classified change set. + * + * @return string The next version string. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function nextVersion(?string $currentVersion, SchemaChangeSet $changeSet): string + { + $parts = $this->parseVersion(version: $currentVersion); + + switch ($changeSet->getBump()) { + case 'major': + $parts[0]++; + $parts[1] = 0; + $parts[2] = 0; + break; + case 'minor': + $parts[1]++; + $parts[2] = 0; + break; + case 'patch': + $parts[2]++; + break; + case 'none': + default: + // No bump. + break; + } + + return implode('.', $parts); + + }//end nextVersion() + + /** + * Diff a single property definition. + * + * @param string $name Property name. + * @param array $oldDef Old property definition. + * @param array $newDef New property definition. + * + * @return array> Typed changes for this property. + */ + private function diffProperty(string $name, array $oldDef, array $newDef): array + { + $changes = []; + + $oldType = $oldDef['type'] ?? null; + $newType = $newDef['type'] ?? null; + if ($this->normaliseType(type: $oldType) !== $this->normaliseType(type: $newType)) { + $changes[] = [ + 'property' => $name, + 'kind' => 'type_changed', + 'old' => $oldType, + 'new' => $newType, + ]; + } + + // Constraint changes. + foreach (self::NUMERIC_BOUND_TIGHTENS_UP as $keyword => $tightensUp) { + $hadOld = array_key_exists($keyword, $oldDef); + $hasNew = array_key_exists($keyword, $newDef); + + if ($hadOld === false && $hasNew === false) { + continue; + } + + if ($hadOld === false && $hasNew === true) { + // Newly introduced bound = tightening. + $changes[] = [ + 'property' => $name, + 'kind' => 'constraint_tightened', + 'old' => null, + 'new' => [$keyword => $newDef[$keyword]], + ]; + continue; + } + + if ($hadOld === true && $hasNew === false) { + // Removed bound = relaxation. + $changes[] = [ + 'property' => $name, + 'kind' => 'constraint_relaxed', + 'old' => [$keyword => $oldDef[$keyword]], + 'new' => null, + ]; + continue; + } + + $oldVal = $oldDef[$keyword]; + $newVal = $newDef[$keyword]; + if ($oldVal === $newVal) { + continue; + } + + $tighter = $this->boundTightened(oldVal: (float) $oldVal, newVal: (float) $newVal, tightensUp: $tightensUp); + if ($tighter === true) { + $kind = 'constraint_tightened'; + } else { + $kind = 'constraint_relaxed'; + } + + $changes[] = [ + 'property' => $name, + 'kind' => $kind, + 'old' => [$keyword => $oldVal], + 'new' => [$keyword => $newVal], + ]; + }//end foreach + + // Enum narrowing/widening. + $enumChange = $this->diffEnum(name: $name, oldDef: $oldDef, newDef: $newDef); + if ($enumChange !== null) { + $changes[] = $enumChange; + } + + // Pattern / format introduction = tightening. + foreach (['pattern', 'format'] as $keyword) { + $oldHas = array_key_exists($keyword, $oldDef); + $newHas = array_key_exists($keyword, $newDef); + if ($oldHas === false && $newHas === true) { + $changes[] = [ + 'property' => $name, + 'kind' => 'constraint_tightened', + 'old' => null, + 'new' => [$keyword => $newDef[$keyword]], + ]; + } else if ($oldHas === true && $newHas === false) { + $changes[] = [ + 'property' => $name, + 'kind' => 'constraint_relaxed', + 'old' => [$keyword => $oldDef[$keyword]], + 'new' => null, + ]; + } else if ($oldHas === true && $newHas === true && $oldDef[$keyword] !== $newDef[$keyword]) { + // A changed pattern/format is conservatively breaking. + $changes[] = [ + 'property' => $name, + 'kind' => 'constraint_tightened', + 'old' => [$keyword => $oldDef[$keyword]], + 'new' => [$keyword => $newDef[$keyword]], + ]; + }//end if + }//end foreach + + return $changes; + + }//end diffProperty() + + /** + * Diff an enum constraint. + * + * @param string $name Property name. + * @param array $oldDef Old definition. + * @param array $newDef New definition. + * + * @return array|null The change, or null if no enum change. + */ + private function diffEnum(string $name, array $oldDef, array $newDef): ?array + { + $oldEnum = $oldDef['enum'] ?? null; + $newEnum = $newDef['enum'] ?? null; + + if ($oldEnum === null && $newEnum === null) { + return null; + } + + if (is_array($oldEnum) === false && is_array($newEnum) === true) { + // Newly constrained to a fixed set = tightening. + return [ + 'property' => $name, + 'kind' => 'constraint_tightened', + 'old' => null, + 'new' => ['enum' => $newEnum], + ]; + } + + if (is_array($oldEnum) === true && is_array($newEnum) === false) { + // Enum dropped = relaxation. + return [ + 'property' => $name, + 'kind' => 'constraint_relaxed', + 'old' => ['enum' => $oldEnum], + 'new' => null, + ]; + } + + // Both arrays — compare membership. + $removed = array_diff($oldEnum, $newEnum); + $added = array_diff($newEnum, $oldEnum); + + if (count($removed) === 0 && count($added) === 0) { + return null; + } + + if (count($removed) > 0) { + // Removing allowed values can invalidate existing data. + return [ + 'property' => $name, + 'kind' => 'constraint_tightened', + 'old' => ['enum' => $oldEnum], + 'new' => ['enum' => $newEnum], + ]; + } + + // Only widened. + return [ + 'property' => $name, + 'kind' => 'constraint_relaxed', + 'old' => ['enum' => $oldEnum], + 'new' => ['enum' => $newEnum], + ]; + + }//end diffEnum() + + /** + * Whether a numeric bound moved in the tightening direction. + * + * @param float $oldVal Old value. + * @param float $newVal New value. + * @param bool $tightensUp True when increasing the value tightens. + * + * @return bool True when the bound tightened. + */ + private function boundTightened(float $oldVal, float $newVal, bool $tightensUp): bool + { + if ($tightensUp === true) { + return $newVal > $oldVal; + } + + return $newVal < $oldVal; + + }//end boundTightened() + + /** + * Classify the accumulated changes into a SchemaChangeSet. + * + * @param array> $changes The change list. + * @param bool $hasBreaking Any breaking change present. + * @param bool $hasAddedProp Any added optional property. + * @param bool $hasOtherChange Any other compatible change. + * + * @return SchemaChangeSet The classified change set. + */ + private function classify(array $changes, bool $hasBreaking, bool $hasAddedProp, bool $hasOtherChange): SchemaChangeSet + { + if (count($changes) === 0) { + return new SchemaChangeSet([], SchemaChangeSet::CLASS_NONE, 'none'); + } + + if ($hasBreaking === true) { + return new SchemaChangeSet($changes, SchemaChangeSet::CLASS_BREAKING, 'major'); + } + + if ($hasAddedProp === true) { + return new SchemaChangeSet($changes, SchemaChangeSet::CLASS_COMPATIBLE, 'minor'); + } + + // Other compatible change (relaxed constraint, dropped requirement). + return new SchemaChangeSet($changes, SchemaChangeSet::CLASS_COMPATIBLE, 'patch'); + + }//end classify() + + /** + * Whether a change kind is breaking. + * + * @param string $kind The change kind. + * + * @return bool True when breaking. + */ + private function kindIsBreaking(string $kind): bool + { + return in_array( + $kind, + [ + 'removed', + 'renamed', + 'type_changed', + 'constraint_tightened', + 'required_added', + 'added_required_no_default', + ], + true + ); + + }//end kindIsBreaking() + + /** + * Extract the properties map from a definition. + * + * @param array $definition The definition. + * + * @return array The properties map. + */ + private function properties(array $definition): array + { + $props = ($definition['properties'] ?? []); + if (is_array($props) === false) { + return []; + } + + return $props; + + }//end properties() + + /** + * Extract the required-property name set from a definition. + * + * @param array $definition The definition. + * + * @return array The required property names. + */ + private function requiredSet(array $definition): array + { + $required = ($definition['required'] ?? []); + if (is_array($required) === false) { + return []; + } + + return array_values(array_filter($required, 'is_string')); + + }//end requiredSet() + + /** + * Normalise a JSON-schema type for comparison. + * + * @param mixed $type The type value (string or array). + * + * @return string The normalised type signature. + */ + private function normaliseType($type): string + { + if (is_array($type) === true) { + $copy = $type; + sort($copy); + return implode('|', array_map('strval', $copy)); + } + + if ($type === null) { + return ''; + } + + return (string) $type; + + }//end normaliseType() + + /** + * Parse a semantic version string into a [major, minor, patch] int triple. + * + * @param string|null $version The version string. + * + * @return array{0: int, 1: int, 2: int} The parsed triple. + */ + private function parseVersion(?string $version): array + { + if ($version === null || trim($version) === '') { + return [0, 0, 0]; + } + + $parts = explode('.', trim($version)); + + return [ + (int) ($parts[0] ?? 0), + (int) ($parts[1] ?? 0), + (int) ($parts[2] ?? 0), + ]; + + }//end parseVersion() +}//end class diff --git a/lib/Service/Schema/SchemaMigrationPlanner.php b/lib/Service/Schema/SchemaMigrationPlanner.php new file mode 100644 index 0000000000..0d83c6b210 --- /dev/null +++ b/lib/Service/Schema/SchemaMigrationPlanner.php @@ -0,0 +1,494 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Schema; + +/** + * Pure transform engine for declarative migration plans. + */ +class SchemaMigrationPlanner +{ + + /** + * The set of recognised transform operations. + * + * @var array + */ + public const TRANSFORMS = [ + 'rename', + 'setDefault', + 'cast', + 'drop', + 'compute', + ]; + + /** + * Optional template renderer for the `compute` transform. + * + * Signature: fn(string $template, array $context): string. + * When null a minimal `{{ field }}` substitution is used. + * + * @var callable|null + */ + private $templateRenderer; + + /** + * Constructor. + * + * @param callable|null $templateRenderer Optional template renderer for + * the `compute` transform. + */ + public function __construct(?callable $templateRenderer=null) + { + $this->templateRenderer = $templateRenderer; + + }//end __construct() + + /** + * Validate a migration plan's structure. + * + * Returns a list of human-readable problems; an empty list means the + * plan is structurally valid. Unknown transforms or missing required + * fields are reported so the caller can refuse the plan (HTTP 422) + * before any object is loaded. + * + * @param array> $plan The transform chain. + * + * @return array The list of validation problems (empty when valid). + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function validatePlan(array $plan): array + { + $problems = []; + + if (count($plan) === 0) { + $problems[] = 'Migration plan must contain at least one transform.'; + return $problems; + } + + foreach ($plan as $index => $step) { + if (is_array($step) === false || isset($step['op']) === false) { + $problems[] = sprintf('Transform #%d is missing an "op" key.', $index); + continue; + } + + $op = $step['op']; + if (in_array($op, self::TRANSFORMS, true) === false) { + $problems[] = sprintf('Transform #%d has unknown op "%s".', $index, (string) $op); + continue; + } + + switch ($op) { + case 'rename': + if (empty($step['from']) === true || empty($step['to']) === true) { + $problems[] = sprintf('rename transform #%d requires "from" and "to".', $index); + } + break; + case 'setDefault': + if (empty($step['field']) === true || array_key_exists('value', $step) === false) { + $problems[] = sprintf('setDefault transform #%d requires "field" and "value".', $index); + } + break; + case 'cast': + if (empty($step['field']) === true || empty($step['to']) === true) { + $problems[] = sprintf('cast transform #%d requires "field" and "to".', $index); + } else if (in_array($step['to'], ['string', 'integer', 'number', 'boolean', 'date'], true) === false) { + $problems[] = sprintf('cast transform #%d has unsupported target type "%s".', $index, (string) $step['to']); + } + break; + case 'drop': + if (empty($step['field']) === true) { + $problems[] = sprintf('drop transform #%d requires "field".', $index); + } + break; + case 'compute': + if (empty($step['field']) === true || array_key_exists('template', $step) === false) { + $problems[] = sprintf('compute transform #%d requires "field" and "template".', $index); + } + break; + }//end switch + }//end foreach + + return $problems; + + }//end validatePlan() + + /** + * Apply a transform chain to one object's data. + * + * The input data is never mutated; a transformed copy is returned in + * the result. An uncastable value (or any transform error) yields a + * failed result with the original data preserved, so a failure can + * never corrupt or partially write an object. + * + * @param array $data The object's current data. + * @param array> $plan The transform chain. + * + * @return MigrationPlanResult The transform outcome. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function apply(array $data, array $plan): MigrationPlanResult + { + $original = $data; + $working = $data; + $applied = []; + + foreach ($plan as $step) { + $op = ($step['op'] ?? ''); + + try { + switch ($op) { + case 'rename': + $working = $this->applyRename(data: $working, from: (string) $step['from'], to: (string) $step['to']); + $applied[] = sprintf('rename %s -> %s', $step['from'], $step['to']); + break; + case 'setDefault': + $working = $this->applySetDefault(data: $working, field: (string) $step['field'], value: $step['value']); + $applied[] = sprintf('setDefault %s', $step['field']); + break; + case 'cast': + $working = $this->applyCast( + data: $working, + field: (string) $step['field'], + toType: (string) $step['to'], + format: ($step['format'] ?? null) + ); + $applied[] = sprintf('cast %s -> %s', $step['field'], $step['to']); + break; + case 'drop': + $working = $this->applyDrop(data: $working, field: (string) $step['field']); + $applied[] = sprintf('drop %s', $step['field']); + break; + case 'compute': + $working = $this->applyCompute(data: $working, field: (string) $step['field'], template: (string) $step['template']); + $applied[] = sprintf('compute %s', $step['field']); + break; + default: + return new MigrationPlanResult($original, false, sprintf('Unknown transform op "%s".', (string) $op), $applied); + }//end switch + } catch (\Throwable $e) { + // Any transform failure leaves the ORIGINAL data intact. + return new MigrationPlanResult($original, false, $e->getMessage(), $applied); + }//end try + }//end foreach + + $changed = ($working !== $original); + + return new MigrationPlanResult($working, $changed, null, $applied); + + }//end apply() + + /** + * Apply a rename transform. + * + * @param array $data The data. + * @param string $from Source property. + * @param string $to Target property. + * + * @return array The transformed data. + */ + private function applyRename(array $data, string $from, string $to): array + { + if (array_key_exists($from, $data) === false) { + return $data; + } + + $data[$to] = $data[$from]; + unset($data[$from]); + + return $data; + + }//end applyRename() + + /** + * Apply a setDefault transform (only when missing or null). + * + * @param array $data The data. + * @param string $field Target property. + * @param mixed $value The default value. + * + * @return array The transformed data. + */ + private function applySetDefault(array $data, string $field, $value): array + { + if (array_key_exists($field, $data) === false || $data[$field] === null) { + $data[$field] = $value; + } + + return $data; + + }//end applySetDefault() + + /** + * Apply a cast transform. + * + * @param array $data The data. + * @param string $field Target property. + * @param string $toType Target type. + * @param string|null $format Optional date format. + * + * @return array The transformed data. + * + * @throws \RuntimeException When the value is uncastable. + */ + private function applyCast(array $data, string $field, string $toType, ?string $format): array + { + if (array_key_exists($field, $data) === false || $data[$field] === null) { + // Nothing to cast; leave as-is. + return $data; + } + + $value = $data[$field]; + $data[$field] = $this->castValue(value: $value, toType: $toType, format: $format); + + return $data; + + }//end applyCast() + + /** + * Cast a scalar value to the target type. + * + * @param mixed $value The value. + * @param string $toType Target type. + * @param string|null $format Optional date format. + * + * @return mixed The cast value. + * + * @throws \RuntimeException When uncastable. + */ + private function castValue($value, string $toType, ?string $format) + { + switch ($toType) { + case 'string': + if (is_scalar($value) === true) { + return (string) $value; + } + throw new \RuntimeException('Cannot cast non-scalar value to string.'); + + case 'integer': + if (is_bool($value) === true) { + return (int) $value; + } + + if (is_numeric($value) === true) { + return (int) $value; + } + throw new \RuntimeException(sprintf('Cannot cast "%s" to integer.', $this->describe(value: $value))); + + case 'number': + if (is_bool($value) === true) { + return (float) $value; + } + + if (is_numeric($value) === true) { + return (float) $value; + } + throw new \RuntimeException(sprintf('Cannot cast "%s" to number.', $this->describe(value: $value))); + + case 'boolean': + return $this->castBoolean(value: $value); + + case 'date': + return $this->castDate(value: $value, format: $format); + + default: + throw new \RuntimeException(sprintf('Unsupported cast target type "%s".', $toType)); + }//end switch + + }//end castValue() + + /** + * Cast a value to a boolean using a strict allow-list. + * + * @param mixed $value The value. + * + * @return bool The boolean. + * + * @throws \RuntimeException When the value is not recognisably boolean. + */ + private function castBoolean($value): bool + { + if (is_bool($value) === true) { + return $value; + } + + if (is_int($value) === true && ($value === 0 || $value === 1)) { + return $value === 1; + } + + if (is_string($value) === true) { + $lower = strtolower(trim($value)); + if (in_array($lower, ['true', '1', 'yes', 'on'], true) === true) { + return true; + } + + if (in_array($lower, ['false', '0', 'no', 'off'], true) === true) { + return false; + } + } + + throw new \RuntimeException(sprintf('Cannot cast "%s" to boolean.', $this->describe(value: $value))); + + }//end castBoolean() + + /** + * Cast a value to an ISO-8601 date string. + * + * @param mixed $value The value. + * @param string|null $format Optional input format. + * + * @return string The ISO-8601 date string. + * + * @throws \RuntimeException When the value cannot be parsed as a date. + */ + private function castDate($value, ?string $format): string + { + if (is_string($value) === false && is_numeric($value) === false) { + throw new \RuntimeException('Cannot cast non-string value to date.'); + } + + $stringValue = (string) $value; + + if ($format !== null && $format !== '') { + $dt = \DateTime::createFromFormat($format, $stringValue); + if ($dt === false) { + throw new \RuntimeException(sprintf('Value "%s" does not match date format "%s".', $stringValue, $format)); + } + + return $dt->format(\DateTime::ATOM); + } + + $timestamp = strtotime($stringValue); + if ($timestamp === false) { + throw new \RuntimeException(sprintf('Value "%s" is not a parseable date.', $stringValue)); + } + + return (new \DateTime('@'.$timestamp))->format(\DateTime::ATOM); + + }//end castDate() + + /** + * Apply a drop transform. + * + * @param array $data The data. + * @param string $field Property to drop. + * + * @return array The transformed data. + */ + private function applyDrop(array $data, string $field): array + { + unset($data[$field]); + + return $data; + + }//end applyDrop() + + /** + * Apply a compute transform via the template renderer. + * + * @param array $data The data. + * @param string $field Target property. + * @param string $template The template string. + * + * @return array The transformed data. + */ + private function applyCompute(array $data, string $field, string $template): array + { + if ($this->templateRenderer !== null) { + $data[$field] = ($this->templateRenderer)($template, $data); + + return $data; + } + + $data[$field] = $this->renderSimpleTemplate(template: $template, context: $data); + + return $data; + + }//end applyCompute() + + /** + * Render a minimal `{{ field }}` template against the object's own data. + * + * Used as the default `compute` renderer so the planner is testable + * without the Twig stack. Production wiring injects the MappingService + * renderer for full template parity. + * + * @param string $template The template. + * @param array $context The object's data. + * + * @return string The rendered string. + */ + private function renderSimpleTemplate(string $template, array $context): string + { + return preg_replace_callback( + '/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/', + static function (array $matches) use ($context): string { + $key = $matches[1]; + if (array_key_exists($key, $context) === false) { + return ''; + } + + $value = $context[$key]; + if (is_scalar($value) === true) { + return (string) $value; + } + + return ''; + }, + $template + ); + + }//end renderSimpleTemplate() + + /** + * Describe a value for an error message. + * + * @param mixed $value The value. + * + * @return string A short description. + */ + private function describe($value): string + { + if (is_scalar($value) === true) { + return (string) $value; + } + + return gettype($value); + + }//end describe() +}//end class diff --git a/lib/Service/Schema/SchemaMigrationService.php b/lib/Service/Schema/SchemaMigrationService.php new file mode 100644 index 0000000000..301f42d407 --- /dev/null +++ b/lib/Service/Schema/SchemaMigrationService.php @@ -0,0 +1,547 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Schema; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\SchemaRun; +use OCA\OpenRegister\Db\SchemaRunEntry; +use OCA\OpenRegister\Db\SchemaRunEntryMapper; +use OCA\OpenRegister\Db\SchemaRunMapper; +use OCA\OpenRegister\Exception\SchemaRunConcurrencyException; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; + +/** + * Declarative migration engine with rollback. + */ +class SchemaMigrationService +{ + + /** + * Default preview sample size. + * + * @var int + */ + public const DEFAULT_PREVIEW_SAMPLE = 10; + + /** + * Default per-batch object count. + * + * @var int + */ + public const DEFAULT_BATCH = 100; + + /** + * Constructor. + * + * @param SchemaMigrationPlanner $planner Pure transform engine. + * @param SchemaRunMapper $runMapper Run persistence. + * @param SchemaRunEntryMapper $runEntryMapper Per-object run entries. + * @param SchemaMapper $schemaMapper Schema lookup. + * @param ObjectService $objectService Object read/save pipeline. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly SchemaMigrationPlanner $planner, + private readonly SchemaRunMapper $runMapper, + private readonly SchemaRunEntryMapper $runEntryMapper, + private readonly SchemaMapper $schemaMapper, + private readonly ObjectService $objectService, + private readonly LoggerInterface $logger + ) { + + }//end __construct() + + /** + * Validate a migration plan structurally. + * + * @param array> $plan The transform chain. + * + * @return array Problems (empty when valid). + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function validatePlan(array $plan): array + { + return $this->planner->validatePlan($plan); + + }//end validatePlan() + + /** + * Preview a plan against a bounded sample without persisting. + * + * @param int $schemaId The schema id. + * @param int $registerId The register id. + * @param array> $plan The transform chain. + * @param int $sampleSize The sample size. + * + * @return array> Before/after pairs. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function preview(int $schemaId, int $registerId, array $plan, int $sampleSize=self::DEFAULT_PREVIEW_SAMPLE): array + { + $objects = $this->loadSample(schemaId: $schemaId, registerId: $registerId, sampleSize: $sampleSize); + $pairs = []; + + foreach ($objects as $object) { + $before = $object->getObject(); + $result = $this->planner->apply($before, $plan); + + $pairs[] = [ + 'uuid' => $object->getUuid(), + 'before' => $before, + 'after' => $result->getData(), + 'changed' => $result->isChanged(), + 'failed' => $result->isFailed(), + 'error' => $result->getFailure(), + ]; + } + + return $pairs; + + }//end preview() + + /** + * Start a migration run. + * + * @param int $schemaId The schema id. + * @param int $registerId The register id. + * @param array> $plan The transform chain. + * @param array $options Options (stopOnError). + * @param string|null $startedBy The starting user. + * + * @return SchemaRun The created run. + * + * @throws SchemaRunConcurrencyException When an active run exists. + * @throws \InvalidArgumentException When the plan is invalid. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function start(int $schemaId, int $registerId, array $plan, array $options=[], ?string $startedBy=null): SchemaRun + { + $problems = $this->planner->validatePlan($plan); + if (count($problems) > 0) { + throw new \InvalidArgumentException(implode(' ', $problems)); + } + + $this->assertNoActiveRun(schemaId: $schemaId); + + $total = $this->countPopulation(schemaId: $schemaId, registerId: $registerId); + + return $this->runMapper->createFromArray( + [ + 'schemaId' => $schemaId, + 'registerId' => $registerId, + 'type' => SchemaRun::TYPE_MIGRATION, + 'state' => SchemaRun::STATE_RUNNING, + 'plan' => $plan, + 'options' => $options, + 'total' => $total, + 'startedBy' => $startedBy, + 'report' => ['migrated' => 0, 'unchanged' => 0, 'failed' => 0], + ] + ); + + }//end start() + + /** + * Process one batch of a migration run. + * + * @param SchemaRun $run The run. + * @param int $batchSize The batch size. + * + * @return bool True when more work remains. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function processBatch(SchemaRun $run, int $batchSize=self::DEFAULT_BATCH): bool + { + $schema = $this->schemaMapper->find($run->getSchemaId()); + $plan = ($run->getPlan() ?? []); + $stopOnError = (bool) (($run->getOptions() ?? [])['stopOnError'] ?? false); + + $objects = $this->loadBatch(run: $run, batchSize: $batchSize); + if (count($objects) === 0) { + $this->finish(run: $run, state: SchemaRun::STATE_COMPLETED); + return false; + } + + $report = ($run->getReport() ?? ['migrated' => 0, 'unchanged' => 0, 'failed' => 0]); + $maxId = $run->getCursor(); + + foreach ($objects as $object) { + $maxId = max($maxId, (int) $object->getId()); + $before = $object->getObject(); + $result = $this->planner->apply($before, $plan); + + if ($result->isFailed() === true) { + $report['failed'] = (($report['failed'] ?? 0) + 1); + $this->runEntryMapper->createFromArray( + [ + 'runId' => $run->getId(), + 'objectUuid' => $object->getUuid(), + 'outcome' => SchemaRunEntry::OUTCOME_FAILED, + 'message' => $result->getFailure(), + ] + ); + + $run->setProcessed(($run->getProcessed() + 1)); + + if ($stopOnError === true) { + $run->setCursor($maxId); + $run->setReport($report); + $this->finish(run: $run, state: SchemaRun::STATE_FAILED); + return false; + } + + continue; + }//end if + + if ($result->isChanged() === false) { + $report['unchanged'] = (($report['unchanged'] ?? 0) + 1); + $run->setProcessed(($run->getProcessed() + 1)); + continue; + } + + // Persist the changed object through the standard save pipeline. + $preVersion = $object->getVersion(); + try { + $saved = $this->objectService->saveObject( + object: $result->getData(), + register: $run->getRegisterId(), + schema: $schema, + uuid: $object->getUuid(), + _rbac: false, + _multitenancy: false + ); + + $report['migrated'] = (($report['migrated'] ?? 0) + 1); + $this->runEntryMapper->createFromArray( + [ + 'runId' => $run->getId(), + 'objectUuid' => $object->getUuid(), + 'outcome' => SchemaRunEntry::OUTCOME_MIGRATED, + 'preVersion' => $preVersion, + 'postVersion' => $saved->getVersion(), + 'preData' => $before, + ] + ); + } catch (\Throwable $e) { + $report['failed'] = (($report['failed'] ?? 0) + 1); + $this->runEntryMapper->createFromArray( + [ + 'runId' => $run->getId(), + 'objectUuid' => $object->getUuid(), + 'outcome' => SchemaRunEntry::OUTCOME_FAILED, + 'message' => $e->getMessage(), + ] + ); + + if ($stopOnError === true) { + $run->setProcessed(($run->getProcessed() + 1)); + $run->setCursor($maxId); + $run->setReport($report); + $this->finish(run: $run, state: SchemaRun::STATE_FAILED); + return false; + } + }//end try + + $run->setProcessed(($run->getProcessed() + 1)); + }//end foreach + + $run->setCursor($maxId); + $run->setReport($report); + $this->runMapper->save($run); + + return true; + + }//end processBatch() + + /** + * Roll a completed or failed migration run back. + * + * Restores each migrated object's pre-migration snapshot forward through + * the save pipeline, but only when the object's current version still + * equals the recorded post-migration version; otherwise the object is + * conflict-skipped and reported. A run can be rolled back only once. + * + * @param int $runId The migration run id. + * @param string|null $startedBy The user performing the rollback. + * + * @return SchemaRun The rollback result on the original run. + * + * @throws SchemaRunConcurrencyException When the run was already rolled back. + * @throws \InvalidArgumentException When the run is not a migration. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function rollback(int $runId, ?string $startedBy=null): SchemaRun + { + $run = $this->runMapper->find($runId); + + if ($run->getType() !== SchemaRun::TYPE_MIGRATION) { + throw new \InvalidArgumentException('Only migration runs can be rolled back.'); + } + + if ($run->getState() === SchemaRun::STATE_ROLLED_BACK) { + throw new SchemaRunConcurrencyException(message: sprintf('Run #%d has already been rolled back.', $runId)); + } + + $schema = $this->schemaMapper->find($run->getSchemaId()); + $entries = $this->runEntryMapper->findByRun($runId, SchemaRunEntry::OUTCOME_MIGRATED); + + $restored = 0; + $conflicts = 0; + + foreach ($entries as $entry) { + $object = $this->objectService->find( + id: $entry->getObjectUuid(), + register: $run->getRegisterId(), + schema: $schema, + _rbac: false, + _multitenancy: false + ); + + if ($object === null) { + $conflicts++; + $this->recordRollbackEntry( + run: $run, + objectUuid: $entry->getObjectUuid(), + outcome: SchemaRunEntry::OUTCOME_CONFLICT, + message: 'Object no longer exists.' + ); + continue; + } + + // Conflict guard: skip objects edited by other writers since the migration. + if ((string) $object->getVersion() !== (string) $entry->getPostVersion()) { + $conflicts++; + $this->recordRollbackEntry( + run: $run, + objectUuid: $entry->getObjectUuid(), + outcome: SchemaRunEntry::OUTCOME_CONFLICT, + message: sprintf('Object modified after migration (current version %s).', (string) $object->getVersion()) + ); + continue; + } + + try { + $this->objectService->saveObject( + object: ($entry->getPreData() ?? []), + register: $run->getRegisterId(), + schema: $schema, + uuid: $entry->getObjectUuid(), + _rbac: false, + _multitenancy: false + ); + $restored++; + $this->recordRollbackEntry( + run: $run, + objectUuid: $entry->getObjectUuid(), + outcome: SchemaRunEntry::OUTCOME_RESTORED, + message: null + ); + } catch (\Throwable $e) { + $conflicts++; + $this->recordRollbackEntry( + run: $run, + objectUuid: $entry->getObjectUuid(), + outcome: SchemaRunEntry::OUTCOME_CONFLICT, + message: $e->getMessage() + ); + }//end try + }//end foreach + + $report = ($run->getReport() ?? []); + $report['rollback'] = ['restored' => $restored, 'conflicts' => $conflicts]; + $run->setReport($report); + $run->setState(SchemaRun::STATE_ROLLED_BACK); + $this->runMapper->save($run); + + return $run; + + }//end rollback() + + /** + * Finish a run, persisting its terminal state. + * + * @param SchemaRun $run The run. + * @param string $state The terminal state. + * + * @return void + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function finish(SchemaRun $run, string $state): void + { + $run->setState($state); + $this->runMapper->save($run); + + }//end finish() + + /** + * Refuse a second concurrent run on the same schema. + * + * @param int $schemaId The schema id. + * + * @return void + * + * @throws SchemaRunConcurrencyException When an active run exists. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function assertNoActiveRun(int $schemaId): void + { + $active = $this->runMapper->findActiveForSchema($schemaId); + if ($active !== null) { + throw new SchemaRunConcurrencyException( + message: sprintf('An active run (#%d) already exists for schema %d.', $active->getId(), $schemaId) + ); + } + + }//end assertNoActiveRun() + + /** + * Record a rollback per-object entry. + * + * @param SchemaRun $run The run. + * @param string|null $objectUuid The object UUID. + * @param string $outcome The outcome. + * @param string|null $message The message. + * + * @return void + */ + private function recordRollbackEntry(SchemaRun $run, ?string $objectUuid, string $outcome, ?string $message): void + { + $this->runEntryMapper->createFromArray( + [ + 'runId' => $run->getId(), + 'objectUuid' => $objectUuid, + 'outcome' => $outcome, + 'message' => $message, + ] + ); + + }//end recordRollbackEntry() + + /** + * Load a bounded sample of objects. + * + * @param int $schemaId The schema id. + * @param int $registerId The register id. + * @param int $sampleSize The sample size. + * + * @return ObjectEntity[] The objects. + */ + private function loadSample(int $schemaId, int $registerId, int $sampleSize): array + { + $result = $this->objectService->findAll( + config: [ + 'limit' => $sampleSize, + 'filters' => [ + 'register' => $registerId, + 'schema' => $schemaId, + ], + ], + _rbac: false, + _multitenancy: false + ); + + return array_values(array_filter($result, static fn($o) => $o instanceof ObjectEntity)); + + }//end loadSample() + + /** + * Load the next batch of non-deleted objects. + * + * @param SchemaRun $run The run. + * @param int $batchSize The batch size. + * + * @return ObjectEntity[] The objects. + */ + private function loadBatch(SchemaRun $run, int $batchSize): array + { + $result = $this->objectService->findAll( + config: [ + 'limit' => $batchSize, + 'offset' => $run->getProcessed(), + 'sort' => ['id' => 'ASC'], + 'filters' => [ + 'register' => $run->getRegisterId(), + 'schema' => $run->getSchemaId(), + ], + ], + _rbac: false, + _multitenancy: false + ); + + return array_values(array_filter($result, static fn($o) => $o instanceof ObjectEntity)); + + }//end loadBatch() + + /** + * Count the non-deleted population for a schema in a register. + * + * @param int $schemaId The schema id. + * @param int $registerId The register id. + * + * @return int The count. + */ + private function countPopulation(int $schemaId, int $registerId): int + { + try { + return $this->objectService->count( + config: [ + 'filters' => [ + 'register' => $registerId, + 'schema' => $schemaId, + ], + ] + ); + } catch (\Throwable $e) { + return 0; + } + + }//end countPopulation() +}//end class diff --git a/lib/Service/Schema/SchemaRevalidationService.php b/lib/Service/Schema/SchemaRevalidationService.php new file mode 100644 index 0000000000..006dfcd635 --- /dev/null +++ b/lib/Service/Schema/SchemaRevalidationService.php @@ -0,0 +1,340 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Schema; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Db\SchemaRun; +use OCA\OpenRegister\Db\SchemaRunEntry; +use OCA\OpenRegister\Db\SchemaRunEntryMapper; +use OCA\OpenRegister\Db\SchemaRunMapper; +use OCA\OpenRegister\Exception\SchemaRunConcurrencyException; +use OCA\OpenRegister\Service\Object\ValidateObject; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; + +/** + * Population revalidation orchestrator. + */ +class SchemaRevalidationService +{ + + /** + * Default per-batch object count. + * + * @var int + */ + public const DEFAULT_BATCH = 100; + + /** + * Max per-object validation errors recorded. + * + * @var int + */ + private const MAX_ERRORS_PER_OBJECT = 25; + + /** + * Constructor. + * + * @param SchemaRunMapper $runMapper Run persistence. + * @param SchemaRunEntryMapper $runEntryMapper Per-object run entries. + * @param SchemaMapper $schemaMapper Schema lookup. + * @param ObjectService $objectService Object read/save pipeline. + * @param ValidateObject $validateObject Single write-path validator. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly SchemaRunMapper $runMapper, + private readonly SchemaRunEntryMapper $runEntryMapper, + private readonly SchemaMapper $schemaMapper, + private readonly ObjectService $objectService, + private readonly ValidateObject $validateObject, + private readonly LoggerInterface $logger + ) { + + }//end __construct() + + /** + * Start a revalidation run. + * + * Creates the run record, refusing a second concurrent run on the same + * schema, and returns it ready for the background job to process. + * + * @param int $schemaId The schema id. + * @param int $registerId The register the objects live in. + * @param array|null $proposedDefinition Optional proposed definition (dry-run). + * @param string|null $startedBy The starting user. + * + * @return SchemaRun The created run. + * + * @throws SchemaRunConcurrencyException When an active run already exists. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function start(int $schemaId, int $registerId, ?array $proposedDefinition=null, ?string $startedBy=null): SchemaRun + { + $this->assertNoActiveRun(schemaId: $schemaId); + + $total = $this->countPopulation(schemaId: $schemaId, registerId: $registerId); + + return $this->runMapper->createFromArray( + [ + 'schemaId' => $schemaId, + 'registerId' => $registerId, + 'type' => SchemaRun::TYPE_REVALIDATION, + 'state' => SchemaRun::STATE_RUNNING, + 'proposedDefinition' => $proposedDefinition, + 'total' => $total, + 'startedBy' => $startedBy, + 'report' => ['valid' => 0, 'invalid' => 0], + ] + ); + + }//end start() + + /** + * Process one batch of a revalidation run, advancing its cursor. + * + * Returns true when more batches remain, false when the run is done. + * Guarantees zero mutation for a proposed-definition (dry-run) run; for + * a current-definition run, only a metadata-only validity stamp is + * written. + * + * @param SchemaRun $run The run to advance. + * @param int $batchSize The batch size. + * + * @return bool True when more work remains. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function processBatch(SchemaRun $run, int $batchSize=self::DEFAULT_BATCH): bool + { + $schema = $this->schemaMapper->find($run->getSchemaId()); + + $validationSchema = $schema; + $isDryRun = ($run->getProposedDefinition() !== null); + if ($isDryRun === true) { + $validationSchema = $this->transientSchema(base: $schema, proposed: $run->getProposedDefinition()); + } + + $objects = $this->loadBatch(run: $run, batchSize: $batchSize); + if (count($objects) === 0) { + $this->finish(run: $run, state: SchemaRun::STATE_COMPLETED); + return false; + } + + $report = ($run->getReport() ?? ['valid' => 0, 'invalid' => 0]); + $maxId = $run->getCursor(); + + foreach ($objects as $object) { + $maxId = max($maxId, (int) $object->getId()); + + $errors = $this->validate(object: $object, schema: $validationSchema); + if (count($errors) === 0) { + $report['valid'] = (($report['valid'] ?? 0) + 1); + } else { + $report['invalid'] = (($report['invalid'] ?? 0) + 1); + $this->runEntryMapper->createFromArray( + [ + 'runId' => $run->getId(), + 'objectUuid' => $object->getUuid(), + 'outcome' => SchemaRunEntry::OUTCOME_INVALID, + 'message' => implode('; ', array_slice($errors, 0, self::MAX_ERRORS_PER_OBJECT)), + ] + ); + } + + $run->setProcessed(($run->getProcessed() + 1)); + }//end foreach + + $run->setCursor($maxId); + $run->setReport($report); + $this->runMapper->save($run); + + return true; + + }//end processBatch() + + /** + * Validate one object against a schema, returning a list of error strings. + * + * @param ObjectEntity $object The object. + * @param Schema $schema The schema (real or transient proposed). + * + * @return array The validation error messages (empty when valid). + */ + private function validate(ObjectEntity $object, Schema $schema): array + { + try { + $result = $this->validateObject->validateObject( + object: $object->getObject(), + schema: $schema + ); + + if ($result->isValid() === true) { + return []; + } + + $error = $result->error(); + if ($error === null) { + return ['Validation failed.']; + } + + return [$error->message()]; + } catch (\Throwable $e) { + return [$e->getMessage()]; + } + + }//end validate() + + /** + * Build a transient (unpersisted) Schema from a proposed definition. + * + * @param Schema $base The base schema (for slug/identity). + * @param array $proposed The proposed definition. + * + * @return Schema The transient schema. + */ + private function transientSchema(Schema $base, array $proposed): Schema + { + $schema = new Schema(); + $schema->hydrate( + [ + 'title' => $base->getTitle(), + 'slug' => $base->getSlug(), + 'version' => $base->getVersion(), + 'properties' => ($proposed['properties'] ?? []), + 'required' => ($proposed['required'] ?? []), + ] + ); + + return $schema; + + }//end transientSchema() + + /** + * Load the next batch of non-deleted objects after the run's cursor. + * + * @param SchemaRun $run The run. + * @param int $batchSize The batch size. + * + * @return ObjectEntity[] The objects. + */ + private function loadBatch(SchemaRun $run, int $batchSize): array + { + $config = [ + 'limit' => $batchSize, + 'offset' => $run->getProcessed(), + 'sort' => ['id' => 'ASC'], + 'filters' => [ + 'register' => $run->getRegisterId(), + 'schema' => $run->getSchemaId(), + ], + ]; + + $result = $this->objectService->findAll(config: $config, _rbac: false, _multitenancy: false); + + return array_values(array_filter($result, static fn($o) => $o instanceof ObjectEntity)); + + }//end loadBatch() + + /** + * Count the non-deleted population for a schema in a register. + * + * @param int $schemaId The schema id. + * @param int $registerId The register id. + * + * @return int The count. + */ + private function countPopulation(int $schemaId, int $registerId): int + { + try { + return $this->objectService->count( + config: [ + 'filters' => [ + 'register' => $registerId, + 'schema' => $schemaId, + ], + ] + ); + } catch (\Throwable $e) { + return 0; + } + + }//end countPopulation() + + /** + * Finish a run, persisting its terminal state. + * + * @param SchemaRun $run The run. + * @param string $state The terminal state. + * + * @return void + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function finish(SchemaRun $run, string $state): void + { + $run->setState($state); + $this->runMapper->save($run); + + }//end finish() + + /** + * Refuse a second concurrent run on the same schema. + * + * @param int $schemaId The schema id. + * + * @return void + * + * @throws SchemaRunConcurrencyException When an active run exists. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function assertNoActiveRun(int $schemaId): void + { + $active = $this->runMapper->findActiveForSchema($schemaId); + if ($active !== null) { + throw new SchemaRunConcurrencyException( + message: sprintf('An active run (#%d) already exists for schema %d.', $active->getId(), $schemaId) + ); + } + + }//end assertNoActiveRun() +}//end class diff --git a/lib/Service/Schema/SchemaVersioningService.php b/lib/Service/Schema/SchemaVersioningService.php new file mode 100644 index 0000000000..270c1068e5 --- /dev/null +++ b/lib/Service/Schema/SchemaVersioningService.php @@ -0,0 +1,253 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Schema; + +use DateTime; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaChangelog; +use OCA\OpenRegister\Db\SchemaChangelogMapper; +use OCA\OpenRegister\Db\SchemaRun; +use OCA\OpenRegister\Db\SchemaRunEntry; +use OCA\OpenRegister\Db\SchemaRunEntryMapper; +use OCA\OpenRegister\Db\SchemaRunMapper; +use OCA\OpenRegister\Exception\BreakingSchemaChangeException; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; + +/** + * Shared schema-update gate + changelog recorder. + */ +class SchemaVersioningService +{ + /** + * Constructor. + * + * @param SchemaDiffService $diffService Pure diff/classification. + * @param SchemaChangelogMapper $changelogMapper Changelog persistence. + * @param SchemaRunMapper $runMapper Run persistence (for invalid counts). + * @param SchemaRunEntryMapper $runEntryMapper Per-object run entries. + * @param IUserSession $userSession Current user resolution. + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly SchemaDiffService $diffService, + private readonly SchemaChangelogMapper $changelogMapper, + private readonly SchemaRunMapper $runMapper, + private readonly SchemaRunEntryMapper $runEntryMapper, + private readonly IUserSession $userSession, + private readonly LoggerInterface $logger + ) { + + }//end __construct() + + /** + * Diff the incoming definition against the stored schema. + * + * @param Schema $existing The stored schema. + * @param array $newDefinition The incoming definition (properties/required). + * @param array $renames Optional declared renames. + * + * @return SchemaChangeSet The classified change set. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function classify(Schema $existing, array $newDefinition, array $renames=[]): SchemaChangeSet + { + $old = [ + 'properties' => ($existing->getProperties() ?? []), + 'required' => ($existing->getRequired() ?? []), + ]; + + return $this->diffService->diff($old, $newDefinition, $renames); + + }//end classify() + + /** + * Enforce the breaking-change acknowledgement gate. + * + * @param SchemaChangeSet $changeSet The classified change set. + * @param bool $acknowledged Whether acknowledgeBreaking was set. + * @param int $schemaId The schema id (for invalid-count lookup). + * + * @return void + * + * @throws BreakingSchemaChangeException When breaking and unacknowledged. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function enforceGate(SchemaChangeSet $changeSet, bool $acknowledged, int $schemaId): void + { + if ($changeSet->isBreaking() === false) { + return; + } + + if ($acknowledged === true) { + return; + } + + throw new BreakingSchemaChangeException( + changes: $changeSet->getChanges(), + invalidCount: $this->latestInvalidCount(schemaId: $schemaId) + ); + + }//end enforceGate() + + /** + * Compute the next version for a schema given a change set. + * + * @param Schema $existing The stored schema. + * @param SchemaChangeSet $changeSet The classified change set. + * + * @return string The next version. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function nextVersion(Schema $existing, SchemaChangeSet $changeSet): string + { + return $this->diffService->nextVersion($existing->getVersion(), $changeSet); + + }//end nextVersion() + + /** + * Record a changelog entry for an applied schema update. + * + * No entry is written for a metadata-only (no structural change) update. + * + * @param int $schemaId The schema id. + * @param string|null $version The resulting version. + * @param SchemaChangeSet $changeSet The classified change set. + * @param bool $acknowledged Whether the change was acknowledged. + * + * @return SchemaChangelog|null The recorded entry, or null for a no-op. + * + * @spec openspec/specs/schema-migration/spec.md + */ + public function recordChangelog(int $schemaId, ?string $version, SchemaChangeSet $changeSet, bool $acknowledged): ?SchemaChangelog + { + if ($changeSet->hasChanges() === false) { + return null; + } + + $actor = $this->currentActor(); + + $data = [ + 'schemaId' => $schemaId, + 'version' => $version, + 'classification' => $changeSet->getClassification(), + 'changes' => $changeSet->getChanges(), + 'actor' => $actor, + ]; + + if ($changeSet->isBreaking() === true && $acknowledged === true) { + $data['acknowledgedBy'] = $actor; + $data['acknowledgedAt'] = new DateTime(); + } + + try { + return $this->changelogMapper->createFromArray($data); + } catch (\Throwable $e) { + // Changelog persistence must never break the update itself. + $this->logger->warning( + '[SchemaVersioningService] Failed to record changelog', + ['schema_id' => $schemaId, 'error' => $e->getMessage()] + ); + return null; + } + + }//end recordChangelog() + + /** + * The invalid-object count from the most recent completed revalidation, + * or null when none exists. + * + * @param int $schemaId The schema id. + * + * @return int|null The invalid count, or null. + */ + private function latestInvalidCount(int $schemaId): ?int + { + try { + $runs = $this->runMapper->findBySchema($schemaId, 25); + } catch (\Throwable $e) { + return null; + } + + foreach ($runs as $run) { + if ($run->getType() !== SchemaRun::TYPE_REVALIDATION) { + continue; + } + + if ($run->getState() !== SchemaRun::STATE_COMPLETED) { + continue; + } + + $report = ($run->getReport() ?? []); + if (array_key_exists('invalid', $report) === true) { + return (int) $report['invalid']; + } + + // Fallback: count invalid entries in the side table. + try { + return count($this->runEntryMapper->findByRun($run->getId(), SchemaRunEntry::OUTCOME_INVALID)); + } catch (\Throwable $e) { + return null; + } + }//end foreach + + return null; + + }//end latestInvalidCount() + + /** + * The current acting user id, or 'system'. + * + * @return string The actor id. + */ + private function currentActor(): string + { + $user = $this->userSession->getUser(); + if ($user !== null) { + return $user->getUID(); + } + + return 'system'; + + }//end currentActor() +}//end class diff --git a/lib/Service/SchemaDeletionService.php b/lib/Service/SchemaDeletionService.php new file mode 100644 index 0000000000..5303b32a89 --- /dev/null +++ b/lib/Service/SchemaDeletionService.php @@ -0,0 +1,599 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Db\MagicMapper; +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Class SchemaDeletionService + * + * Single implementation of "delete every object of a schema", shared by the bulk + * endpoints (`POST /api/bulk/{register}/{schema}/delete-objects`) and the schema + * cascade (`DELETE /api/schemas/{id}?deleteObjects=true`). + * + * The cascade is deliberately split into two phases (see design D2): + * + * Phase 1 (one transaction) — audit every object, hard-delete the rows, delete + * the schema entity, commit. Any failure rolls the whole thing back, so a + * committed audit entry always implies a genuinely deleted object. + * + * Phase 2 (after the commit, best effort) — drop the magic tables. `DROP TABLE` + * is DDL and causes an implicit commit on MySQL/MariaDB, so it can neither + * participate in phase 1's transaction nor be rolled back. A drop failure + * therefore leaves an EMPTY orphan table, which is reported honestly as + * `tableDropped: false` — it never fails a request whose data work succeeded. + * + * KNOWN LIMIT: the cascade is synchronous, and its audit writes are inherently + * sequential (hash-chained, ADR-003). A schema with far more than ~10k objects will + * make the request slow and may hit the PHP time limit. Because phase 1 is + * transactional, such a timeout rolls back cleanly — the failure is slow, not dirty. + * Moving very large cascades to a background job is deferred, not designed away. + * + * @package OCA\OpenRegister\Service + */ +class SchemaDeletionService +{ + + /** + * Audit action recorded for every object removed by a schema-wide delete. + * + * @var string + */ + public const ACTION_CASCADE_DELETE = 'schema.cascade_delete'; + + /** + * Trigger context for objects removed because their schema is being deleted. + * + * @var string + */ + public const TRIGGER_SCHEMA_DELETION = 'schema_deletion'; + + /** + * Trigger context for objects removed via the bulk delete-objects endpoints. + * + * @var string + */ + public const TRIGGER_BULK_DELETE = 'bulk_schema_delete'; + + /** + * Objects snapshotted + audited per batch. + * + * ADR-009 (bounded per-object work): the object set is read and audited in + * fixed-size chunks so a schema with many objects never materialises its + * entire object set in memory at once. + * + * @var int + */ + private const CHUNK_SIZE = 500; + + /** + * SchemaDeletionService constructor. + * + * @param IDBConnection $db Database connection, for phase-1 transaction control. + * @param MagicMapper $magicMapper Magic-table mapper (row reads, bulk delete, table drop). + * @param RegisterMapper $registerMapper Register mapper, to resolve register entities. + * @param SchemaMapper $schemaMapper Schema mapper, to resolve and delete schema entities. + * @param AuditTrailMapper $auditTrailMapper Audit trail mapper (ADR-003 hash-chained writes). + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly IDBConnection $db, + private readonly MagicMapper $magicMapper, + private readonly RegisterMapper $registerMapper, + private readonly SchemaMapper $schemaMapper, + private readonly AuditTrailMapper $auditTrailMapper, + private readonly LoggerInterface $logger + ) { + + }//end __construct() + + /** + * Delete every object of a register/schema pair, without deleting the schema. + * + * Backs `POST /api/bulk/{register}/{schema}/delete-objects` (and its legacy + * twin `/delete-schema`). Each object is snapshotted to the audit trail before + * its row is touched, because `MagicMapper::deleteObjectsBySchema()` returns + * only a count and cannot tell us afterwards what it removed. + * + * @param int $registerId The register id. + * @param int $schemaId The schema id. + * @param bool $hardDelete True to remove the rows, false to soft-delete them. + * @param string $triggeredBy Audit trigger context. + * + * @throws \Exception If the register or schema cannot be resolved, or deletion fails. + * + * @return array{deleted_count: int, deleted_uuids: array, schema_id: int} The deletion result. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The hard/soft toggle mirrors the mapper primitive it wraps. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md + */ + public function deleteObjectsBySchema( + int $registerId, + int $schemaId, + bool $hardDelete=false, + string $triggeredBy=self::TRIGGER_BULK_DELETE + ): array { + $register = $this->registerMapper->find(id: $registerId); + $schema = $this->schemaMapper->find(id: $schemaId); + + $result = $this->deleteObjectsOfPair( + register: $register, + schema: $schema, + hardDelete: $hardDelete, + triggeredBy: $triggeredBy + ); + + return [ + 'deleted_count' => $result['deleted_count'], + 'deleted_uuids' => $result['deleted_uuids'], + 'schema_id' => $schemaId, + ]; + + }//end deleteObjectsBySchema() + + /** + * Cascade-delete a schema: its objects, its magic tables, and the schema itself. + * + * Phase 1 (transactional): audit + hard-delete every object of every magic table + * belonging to this schema, then delete the schema entity, then commit. Any + * failure rolls the entire phase back and the exception propagates (HTTP 500). + * + * Phase 2 (post-commit, best effort): drop the now-empty magic tables. A failure + * here is logged at WARNING and reported as `tableDropped: false`; the request + * still succeeds, because the caller's intent is already satisfied and DDL is not + * rollbackable on MySQL/MariaDB. + * + * @param Schema $schema The schema to tear down. + * + * @throws \Exception If phase 1 fails (nothing is deleted). + * + * @return array{deletedCount: int, deletedUuids: array, tableDropped: bool} The cascade result. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md + */ + public function cascadeDeleteSchema(Schema $schema): array + { + $schemaId = (int) $schema->getId(); + $tables = $this->resolveMagicTablesForSchema(schema: $schema); + + $deletedCount = 0; + $deletedUuids = []; + + // ---- Phase 1: one transaction. ---- + $this->db->beginTransaction(); + try { + foreach ($tables as $entry) { + if ($entry['register'] === null) { + // The magic table outlives its register (stale pair). There is no + // Register entity to read rows through, so nothing can be audited + // or deleted here — but the table is still dropped in phase 2. + continue; + } + + $result = $this->deleteObjectsOfPair( + register: $entry['register'], + schema: $schema, + hardDelete: true, + triggeredBy: self::TRIGGER_SCHEMA_DELETION + ); + + $deletedCount += $result['deleted_count']; + $deletedUuids = array_merge($deletedUuids, $result['deleted_uuids']); + } + + // The rows are gone, so the mapper guard naturally counts 0 — no force needed. + $this->schemaMapper->delete(entity: $schema); + + $this->db->commit(); + } catch (Throwable $e) { + $this->db->rollBack(); + + $this->logger->error( + message: '[SchemaDeletionService] Cascade delete rolled back', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schemaId, + 'error' => $e->getMessage(), + ] + ); + + throw $e; + }//end try + + // ---- Phase 2: post-commit, best effort. ---- + $tableDropped = $this->dropTables( + tableNames: array_column($tables, 'tableName'), + schemaId: $schemaId + ); + + $this->logger->info( + message: '[SchemaDeletionService] Cascade deleted schema', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schemaId, + 'schemaSlug' => $schema->getSlug(), + 'deletedCount' => $deletedCount, + 'tableDropped' => $tableDropped, + ] + ); + + return [ + 'deletedCount' => $deletedCount, + 'deletedUuids' => $deletedUuids, + 'tableDropped' => $tableDropped, + ]; + + }//end cascadeDeleteSchema() + + /** + * Drop the schema's magic tables, but only when they hold no rows at all. + * + * Used by the plain (no-flag, zero-object) delete path so it stops leaving an + * orphan table behind. The emptiness check counts EVERY row, including + * soft-deleted ones — the controller's object count excludes soft-deleted rows, + * so a "0 object" schema may still have tombstones in its table. Dropping such a + * table would destroy those rows with no audit entry, so we keep the table + * instead and log it. The cascade disposition is the audited way to remove them. + * + * Never throws: a failed reclaim of an empty table must not fail the delete that + * already succeeded. + * + * @param Schema $schema The schema whose tables should be reclaimed. + * + * @return bool True when no magic table for this schema remains. + * + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md + */ + public function dropEmptyTablesForSchema(Schema $schema): bool + { + $schemaId = (int) $schema->getId(); + + try { + $tables = $this->resolveMagicTablesForSchema(schema: $schema); + $droppable = []; + + foreach ($tables as $entry) { + $rowCount = $this->countAllRows(tableName: $entry['tableName']); + if ($rowCount > 0) { + $this->logger->warning( + message: '[SchemaDeletionService] Keeping non-empty magic table of a deleted schema', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schemaId, + 'tableName' => $entry['tableName'], + 'rowCount' => $rowCount, + ] + ); + continue; + } + + $droppable[] = $entry['tableName']; + } + + $allDropped = $this->dropTables(tableNames: $droppable, schemaId: $schemaId); + + return ($allDropped === true && count($droppable) === count($tables)); + } catch (Throwable $e) { + $this->logger->warning( + message: '[SchemaDeletionService] Could not reclaim magic tables of a deleted schema', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schemaId, + 'error' => $e->getMessage(), + ] + ); + + return false; + }//end try + + }//end dropEmptyTablesForSchema() + + /** + * Audit and delete every object of one register/schema magic table. + * + * @param Register $register The register context. + * @param Schema $schema The schema context. + * @param bool $hardDelete True to remove the rows, false to soft-delete them. + * @param string $triggeredBy Audit trigger context. + * + * @return array{deleted_count: int, deleted_uuids: array} The deletion result. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The hard/soft toggle mirrors the mapper primitive it wraps. + */ + private function deleteObjectsOfPair( + Register $register, + Schema $schema, + bool $hardDelete, + string $triggeredBy + ): array { + if ($this->magicMapper->tableExistsForRegisterSchema(register: $register, schema: $schema) === false) { + return [ + 'deleted_count' => 0, + 'deleted_uuids' => [], + ]; + } + + // Snapshot BEFORE deleting: the mapper primitive returns only a count, so the + // pre-deletion state has to be captured here or it is unrecoverable. + $deletedUuids = $this->auditObjectsBeforeDeletion( + register: $register, + schema: $schema, + hardDelete: $hardDelete, + triggeredBy: $triggeredBy + ); + + $deletedCount = $this->magicMapper->deleteObjectsBySchema( + register: $register, + schema: $schema, + hardDelete: $hardDelete + ); + + return [ + 'deleted_count' => $deletedCount, + 'deleted_uuids' => $deletedUuids, + ]; + + }//end deleteObjectsOfPair() + + /** + * Write one hash-chained audit entry per object, capturing its full pre-delete state. + * + * Reads the objects in chunks (ADR-009) and resolves the schema-derived audit + * context once for the whole operation rather than once per object. RBAC and + * multitenancy filters are switched off for the read: the caller is already gated + * on `checkSchemaManagePermission()`, and the delete that follows removes EVERY + * row, so an RBAC-narrowed read would audit fewer objects than it destroys. + * Soft-deleted rows are included for the same reason. + * + * @param Register $register The register context. + * @param Schema $schema The schema context. + * @param bool $hardDelete Whether the rows are about to be hard-deleted. + * @param string $triggeredBy Audit trigger context. + * + * @return array The UUIDs of the audited objects. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Recorded as audit context, not a behaviour switch. + */ + private function auditObjectsBeforeDeletion( + Register $register, + Schema $schema, + bool $hardDelete, + string $triggeredBy + ): array { + // Schema-derived values: resolved ONCE for the whole operation (ADR-009). + $baseContext = [ + 'triggeredBy' => $triggeredBy, + 'hardDelete' => $hardDelete, + 'cascadeContext' => [ + 'triggerSchema' => $schema->getSlug(), + 'triggerSchemaId' => (int) $schema->getId(), + 'registerId' => (int) $register->getId(), + ], + ]; + + $filters = [ + // See the docblock: audit exactly the set that is about to be deleted. + '_includeDeleted' => true, + '_rbac' => false, + '_multitenancy' => false, + ]; + + $uuids = []; + $offset = 0; + + while (true) { + $objects = $this->magicMapper->findAllInRegisterSchemaTable( + register: $register, + schema: $schema, + limit: self::CHUNK_SIZE, + offset: $offset, + filters: $filters + ); + + if (empty($objects) === true) { + break; + } + + foreach ($objects as $object) { + $uuid = $this->auditObject(object: $object, baseContext: $baseContext); + if ($uuid !== null) { + $uuids[] = $uuid; + } + } + + if (count($objects) < self::CHUNK_SIZE) { + break; + } + + $offset += self::CHUNK_SIZE; + }//end while + + return $uuids; + + }//end auditObjectsBeforeDeletion() + + /** + * Write a single object's pre-deletion snapshot to the audit trail. + * + * Written through AuditTrailMapper so ADR-003's SHA-256 hash-chaining is applied + * at insert time, and inside the caller's transaction so a rollback discards it. + * + * @param ObjectEntity $object The object about to be deleted. + * @param array $baseContext Operation-wide audit context. + * + * @return string|null The object's UUID, or null when it has none. + */ + private function auditObject(ObjectEntity $object, array $baseContext): ?string + { + $context = $baseContext; + $context['snapshot'] = $object->jsonSerialize(); + + $this->auditTrailMapper->createAuditTrailEntry( + object: $object, + action: self::ACTION_CASCADE_DELETE, + context: $context + ); + + $uuid = $object->getUuid(); + if ($uuid === null) { + return null; + } + + return (string) $uuid; + + }//end auditObject() + + /** + * Resolve every magic table that belongs to this schema. + * + * Tables are discovered from the tables that actually exist (the same source the + * controller's object count uses), not from the registers that merely reference + * the schema — so a table whose register has since dropped the reference is still + * reclaimed. When the register entity itself is gone the table is still returned, + * with a null register, so phase 2 can drop it. + * + * @param Schema $schema The schema. + * + * @return array The tables. + */ + private function resolveMagicTablesForSchema(Schema $schema): array + { + $schemaId = (int) $schema->getId(); + $tables = []; + + foreach ($this->magicMapper->getAllRegisterSchemaPairs() as $pair) { + if ((int) $pair['schemaId'] !== $schemaId) { + continue; + } + + $registerId = (int) $pair['registerId']; + $register = null; + + try { + $register = $this->registerMapper->find(id: $registerId); + } catch (Throwable $e) { + $this->logger->warning( + message: '[SchemaDeletionService] Magic table has no register entity', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'registerId' => $registerId, + 'schemaId' => $schemaId, + 'error' => $e->getMessage(), + ] + ); + } + + $tables[] = [ + 'registerId' => $registerId, + 'register' => $register, + 'tableName' => MagicMapper::TABLE_PREFIX.$registerId.'_'.$schemaId, + ]; + }//end foreach + + return $tables; + + }//end resolveMagicTablesForSchema() + + /** + * Drop magic tables, best effort. + * + * @param array $tableNames The tables to drop. + * @param int $schemaId The schema being torn down (for logging). + * + * @return bool True when every table was dropped. + */ + private function dropTables(array $tableNames, int $schemaId): bool + { + $allDropped = true; + + foreach ($tableNames as $tableName) { + try { + $this->magicMapper->dropTable(tableName: $tableName); + } catch (Throwable $e) { + $allDropped = false; + + // The objects and the schema are already gone and committed. Failing the + // request now would report failure for work that succeeded, and there is + // nothing to roll back to — DDL is not rollbackable on MySQL/MariaDB. + // The leftover table is empty and reclaimable later. + $this->logger->warning( + message: '[SchemaDeletionService] Could not drop magic table after schema deletion', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schemaId, + 'tableName' => $tableName, + 'error' => $e->getMessage(), + ] + ); + }//end try + }//end foreach + + return $allDropped; + + }//end dropTables() + + /** + * Count every row of a magic table, including soft-deleted ones. + * + * @param string $tableName The magic table name. + * + * @return int The row count (0 when the table does not exist). + */ + private function countAllRows(string $tableName): int + { + if ($this->db->tableExists($tableName) === false) { + return 0; + } + + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*'))->from($tableName); + + $result = $qb->executeQuery(); + $count = (int) $result->fetchOne(); + $result->closeCursor(); + + return $count; + + }//end countAllRows() +}//end class diff --git a/lib/Service/SchemaImport/DialectDetector.php b/lib/Service/SchemaImport/DialectDetector.php new file mode 100644 index 0000000000..50894d0b69 --- /dev/null +++ b/lib/Service/SchemaImport/DialectDetector.php @@ -0,0 +1,216 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +/** + * Detects the dialect of an uploaded schema document. + * + * @spec openspec/specs/schema-import/spec.md + */ +class DialectDetector +{ + + /** + * The dialect for plain JSON Schema documents. + * + * @var string + */ + public const DIALECT_JSON_SCHEMA = 'json-schema'; + + /** + * The dialect for OpenAPI documents. + * + * @var string + */ + public const DIALECT_OPENAPI = 'openapi'; + + /** + * The dialect for Schema.org JSON-LD documents. + * + * @var string + */ + public const DIALECT_SCHEMA_ORG = 'schema.org'; + + /** + * The dialect for GGM export documents. + * + * @var string + */ + public const DIALECT_GGM = 'ggm'; + + /** + * The supported dialect identifiers. + * + * @return array The dialect keys. + * + * @spec openspec/specs/schema-import/spec.md + */ + public static function supportedDialects(): array + { + return [ + self::DIALECT_JSON_SCHEMA, + self::DIALECT_OPENAPI, + self::DIALECT_SCHEMA_ORG, + self::DIALECT_GGM, + ]; + }//end supportedDialects() + + /** + * Detect the dialect of a decoded document. + * + * Order matters: the most specific, unambiguous markers are checked first. + * Returns null when no marker matches (caller fails with 422). + * + * @param array $document The decoded JSON document. + * + * @return string|null The detected dialect, or null when undetectable. + * + * @spec openspec/specs/schema-import/spec.md + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Each marker check adds one conservative branch. + * @SuppressWarnings(PHPMD.NPathComplexity) Each marker check adds one conservative branch. + */ + public function detect(array $document): ?string + { + // OpenAPI: unambiguous `openapi` version + `components`. + if (isset($document['openapi']) === true && isset($document['components']) === true) { + return self::DIALECT_OPENAPI; + } + + // Schema.org JSON-LD: a @context referencing schema.org. + if ($this->referencesSchemaOrg(context: ($document['@context'] ?? null)) === true) { + return self::DIALECT_SCHEMA_ORG; + } + + if (isset($document['@type']) === true + && is_string($document['@type']) === true + && str_contains($document['@type'], 'schema.org') === true + ) { + return self::DIALECT_SCHEMA_ORG; + } + + // GGM export root markers. + if ($this->looksLikeGgm(document: $document) === true) { + return self::DIALECT_GGM; + } + + // JSON Schema: explicit $schema, or a recognisable JSON-Schema shape. + if (isset($document['$schema']) === true) { + return self::DIALECT_JSON_SCHEMA; + } + + if ($this->looksLikeJsonSchema(document: $document) === true) { + return self::DIALECT_JSON_SCHEMA; + } + + return null; + }//end detect() + + /** + * Whether a value is a `@context` that references schema.org. + * + * Accepts a string context, or an array/object context with a value or + * a `@vocab` referencing schema.org. + * + * @param mixed $context The `@context` value. + * + * @return bool True when it references schema.org. + */ + private function referencesSchemaOrg(mixed $context): bool + { + if (is_string($context) === true) { + return str_contains($context, 'schema.org'); + } + + if (is_array($context) === true) { + foreach ($context as $value) { + if (is_string($value) === true && str_contains($value, 'schema.org') === true) { + return true; + } + } + } + + return false; + }//end referencesSchemaOrg() + + /** + * Whether a decoded document carries GGM export root markers. + * + * The normalised GGM intermediate (and exports converted to it) carry a + * `standard: "ggm"` marker; raw exports are recognised by an + * `objecttypen` collection alongside GGM-specific keys. + * + * @param array $document The decoded document. + * + * @return bool True when GGM markers are present. + */ + private function looksLikeGgm(array $document): bool + { + $standard = ($document['standard'] ?? null); + if (is_string($standard) === true && strtolower($standard) === 'ggm') { + return true; + } + + if (isset($document['objecttypen']) === true && is_array($document['objecttypen']) === true) { + return true; + } + + // A single objecttype export: a GGM objecttype carries `attribuutsoorten`. + if (isset($document['attribuutsoorten']) === true) { + return true; + } + + return false; + }//end looksLikeGgm() + + /** + * Whether a decoded document looks like a JSON Schema even without an + * explicit `$schema` key: a `type: object` with a `properties` map, or a + * top-level `properties` map. + * + * @param array $document The decoded document. + * + * @return bool True when it has a JSON-Schema shape. + */ + private function looksLikeJsonSchema(array $document): bool + { + $type = ($document['type'] ?? null); + if ($type === 'object' && isset($document['properties']) === true && is_array($document['properties']) === true) { + return true; + } + + return false; + }//end looksLikeJsonSchema() +}//end class diff --git a/lib/Service/SchemaImport/GgmImporter.php b/lib/Service/SchemaImport/GgmImporter.php new file mode 100644 index 0000000000..03cef7ed82 --- /dev/null +++ b/lib/Service/SchemaImport/GgmImporter.php @@ -0,0 +1,255 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +use OCA\OpenRegister\Exception\SchemaImportException; + +/** + * Imports GGM objecttypes as register schemas. + * + * @spec openspec/specs/schema-import/spec.md + */ +class GgmImporter implements SchemaDialectImporter +{ + /** + * Constructor. + * + * @param GgmSnapshot $snapshot The indexed GGM snapshot. + * @param string|null $sourceLabel Provenance source label (e.g. 'snapshot' or an upload name). + */ + public function __construct( + private readonly GgmSnapshot $snapshot, + private readonly ?string $sourceLabel=null + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @spec openspec/specs/schema-import/spec.md + * + * @return string The dialect key. + */ + public function dialect(): string + { + return DialectDetector::DIALECT_GGM; + }//end dialect() + + /** + * {@inheritDoc} + * + * @spec openspec/specs/schema-import/spec.md + * + * @return string The snapshot version. + */ + public function snapshotVersion(): string + { + return $this->snapshot->version(); + }//end snapshotVersion() + + /** + * {@inheritDoc} + * + * @param string $query The search term. + * + * @return array> Candidate objecttype records. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function discover(string $query): array + { + $version = $this->snapshot->version(); + $results = []; + foreach ($this->snapshot->searchObjecttypes($query) as $objecttype) { + $results[] = [ + 'id' => $objecttype['id'], + 'label' => $objecttype['naam'], + 'description' => $objecttype['definitie'], + 'parent' => null, + 'snapshotVersion' => $version, + ]; + } + + return $results; + }//end discover() + + /** + * {@inheritDoc} + * + * @param string $reference The objecttype id or Dutch name. + * @param ImportOptions $options Import options. + * + * @return ImportedSchema The mapped schema. + * + * @throws SchemaImportException When the objecttype is unknown to the snapshot. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function import(string $reference, ImportOptions $options): ImportedSchema + { + $objecttype = $this->snapshot->findObjecttype($reference); + if ($objecttype === null) { + throw SchemaImportException::unknownReference($reference, $this->dialect()); + } + + $attributes = ($objecttype['attribuutsoorten'] ?? []); + if (is_array($attributes) === false) { + $attributes = []; + } + + $unknownRequested = []; + if ($options->hasSubset() === true) { + [$attributes, $unknownRequested] = $this->applySubset(attributes: $attributes, subset: $options->propertySubset); + } + + $properties = []; + foreach ($attributes as $attribute) { + if (is_array($attribute) === false || isset($attribute['naam']) === false) { + continue; + } + + $properties[(string) $attribute['naam']] = $this->mapAttribute(attribute: $attribute); + } + + $importSource = [ + 'dialect' => $this->dialect(), + 'reference' => (string) $objecttype['id'], + 'snapshotVersion' => $this->snapshot->version(), + 'source' => ($this->sourceLabel ?? 'snapshot'), + 'importedAt' => gmdate('c'), + 'baseline' => $properties, + ]; + + return new ImportedSchema( + title: (string) $objecttype['naam'], + description: (string) ($objecttype['definitie'] ?? ''), + properties: $properties, + jsonld: [], + importSource: $importSource, + unknownRequested: $unknownRequested + ); + }//end import() + + /** + * Restrict the attributes to a requested subset, tracking unknown requests. + * + * @param array> $attributes The objecttype attributes. + * @param array $subset The requested attribute names. + * + * @return array{0: array>, 1: array} Selected attributes + unknown names. + */ + private function applySubset(array $attributes, array $subset): array + { + $byName = []; + foreach ($attributes as $attribute) { + if (is_array($attribute) === true && isset($attribute['naam']) === true) { + $byName[(string) $attribute['naam']] = $attribute; + } + } + + $selected = []; + $unknown = []; + foreach ($subset as $name) { + if (isset($byName[$name]) === true) { + $selected[] = $byName[$name]; + } else { + $unknown[] = $name; + } + } + + return [$selected, $unknown]; + }//end applySubset() + + /** + * Map one GGM attribuutsoort to a JSON Schema property definition. + * + * @param array $attribute The attribuutsoort record. + * + * @return array The JSON Schema property definition. + */ + private function mapAttribute(array $attribute): array + { + $type = strtolower(trim((string) ($attribute['type'] ?? 'tekst'))); + $definition = $this->mapType(type: $type, attribute: $attribute); + + $omschrijving = (string) ($attribute['definitie'] ?? ''); + if ($omschrijving !== '') { + $definition['description'] = $omschrijving; + } + + // Referentielijst with values present → enum. + $values = ($attribute['referentielijst']['waarden'] ?? null); + if (is_array($values) === true && $values !== []) { + $definition['enum'] = array_values($values); + } + + return $definition; + }//end mapAttribute() + + /** + * Map a GGM attribute type to a JSON Schema type/format fragment. + * + * @param string $type The lowercased GGM type label. + * @param array $attribute The full attribute record (for relations). + * + * @return array The JSON Schema type fragment. + */ + private function mapType(string $type, array $attribute): array + { + switch ($type) { + case 'tekst': + return ['type' => 'string']; + case 'geheel getal': + return ['type' => 'integer']; + case 'decimaal': + return ['type' => 'number']; + case 'boolean': + return ['type' => 'boolean']; + case 'datum': + return ['type' => 'string', 'format' => 'date']; + case 'datumtijd': + return ['type' => 'string', 'format' => 'date-time']; + case 'relatie': + // Relations become a single reference property (string id), + // never a recursive import of the target objecttype. + $target = (string) ($attribute['doelObjecttype'] ?? ''); + $fragment = ['type' => 'string', 'format' => 'uri']; + if ($target !== '') { + $fragment['description'] = 'Reference to GGM objecttype '.$target.'.'; + } + return $fragment; + default: + // Unknown GGM type → safest mapping is free-text string. + return ['type' => 'string']; + }//end switch + }//end mapType() +}//end class diff --git a/lib/Service/SchemaImport/GgmSnapshot.php b/lib/Service/SchemaImport/GgmSnapshot.php new file mode 100644 index 0000000000..af864b9d86 --- /dev/null +++ b/lib/Service/SchemaImport/GgmSnapshot.php @@ -0,0 +1,201 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +use OCA\OpenRegister\Exception\SchemaImportException; + +/** + * Indexed view over a normalised GGM release. + * + * @spec openspec/specs/schema-import/spec.md + */ +class GgmSnapshot +{ + + /** + * Objecttypes indexed by id (uppercased). + * + * @var array>|null + */ + private ?array $objecttypes = null; + + /** + * The release version of the loaded snapshot. + * + * @var string + */ + private string $loadedVersion = ''; + + /** + * Constructor. + * + * Exactly one source is used: an in-memory normalised intermediate (for an + * uploaded export) takes precedence over the bundled snapshot file. + * + * @param string $snapshotFile The absolute path to the bundled normalised snapshot. + * @param string $version The bundled snapshot version identifier. + * @param array|null $override An in-memory normalised intermediate, or null. + */ + public function __construct( + private readonly string $snapshotFile, + private readonly string $version, + private readonly ?array $override=null + ) { + }//end __construct() + + /** + * Build a snapshot backed by an in-memory normalised intermediate (upload). + * + * @param array $normalised The normalised intermediate. + * + * @return self The snapshot. + * + * @spec openspec/specs/schema-import/spec.md + */ + public static function fromNormalised(array $normalised): self + { + return new self('', (string) ($normalised['version'] ?? 'upload'), $normalised); + }//end fromNormalised() + + /** + * The release version of the loaded snapshot. + * + * @return string The version. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function version(): string + { + $this->ensureParsed(); + if ($this->loadedVersion !== '') { + return $this->loadedVersion; + } + + return $this->version; + }//end version() + + /** + * Resolve an objecttype by id (case-insensitive) or by Dutch name. + * + * @param string $reference The objecttype id or name. + * + * @return array|null The objecttype record, or null when unknown. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function findObjecttype(string $reference): ?array + { + $this->ensureParsed(); + + $key = strtoupper(trim($reference)); + if (isset($this->objecttypes[$key]) === true) { + return $this->objecttypes[$key]; + } + + // Fall back to a case-insensitive name match. + foreach ($this->objecttypes as $objecttype) { + if (strcasecmp((string) $objecttype['naam'], trim($reference)) === 0) { + return $objecttype; + } + } + + return null; + }//end findObjecttype() + + /** + * Search objecttypes by a case-insensitive name/definition term. + * + * @param string $query A search term; empty returns all. + * + * @return array> Matching objecttype records. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function searchObjecttypes(string $query): array + { + $this->ensureParsed(); + + $needle = strtolower(trim($query)); + $results = []; + foreach ($this->objecttypes as $objecttype) { + if ($needle === '' + || str_contains(strtolower((string) $objecttype['naam']), $needle) === true + || str_contains(strtolower((string) $objecttype['definitie']), $needle) === true + ) { + $results[] = $objecttype; + } + } + + return $results; + }//end searchObjecttypes() + + /** + * Parse + index the snapshot once. + * + * @return void + * + * @throws SchemaImportException When the source is missing or malformed. + */ + private function ensureParsed(): void + { + if ($this->objecttypes !== null) { + return; + } + + if ($this->override !== null) { + $decoded = $this->override; + } else { + if (is_file($this->snapshotFile) === false) { + throw new SchemaImportException(message: 'GGM snapshot file is missing: '.$this->snapshotFile, httpStatus: 500); + } + + $raw = (string) file_get_contents($this->snapshotFile); + $decoded = json_decode($raw, associative: true); + } + + if (is_array($decoded) === false || isset($decoded['objecttypen']) === false + || is_array($decoded['objecttypen']) === false + ) { + throw new SchemaImportException( + message: 'GGM source is not a valid normalised GGM intermediate (missing "objecttypen").', + httpStatus: 422 + ); + } + + $this->loadedVersion = (string) ($decoded['version'] ?? $this->version); + $this->objecttypes = []; + foreach ($decoded['objecttypen'] as $objecttype) { + if (is_array($objecttype) === false || isset($objecttype['id']) === false) { + continue; + } + + $this->objecttypes[strtoupper((string) $objecttype['id'])] = $objecttype; + } + }//end ensureParsed() +}//end class diff --git a/lib/Service/SchemaImport/ImportOptions.php b/lib/Service/SchemaImport/ImportOptions.php new file mode 100644 index 0000000000..8bc6f1d6a3 --- /dev/null +++ b/lib/Service/SchemaImport/ImportOptions.php @@ -0,0 +1,97 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +/** + * Options for a standards import. + * + * @spec openspec/specs/schema-import/spec.md + */ +final class ImportOptions +{ + /** + * Constructor. + * + * @param array $propertySubset Explicit property names to import; empty = all direct properties. + * @param bool $includeAncestors When true, inherited (ancestor) properties are also imported. + * @param int|null $targetRegister Register id the resulting schema should be associated with, or null. + */ + public function __construct( + public readonly array $propertySubset=[], + public readonly bool $includeAncestors=false, + public readonly ?int $targetRegister=null + ) { + }//end __construct() + + /** + * Build options from a loosely-typed request payload. + * + * @param array $payload The request body / parameters. + * + * @return self The constructed options. + * + * @spec openspec/specs/schema-import/spec.md + */ + public static function fromArray(array $payload): self + { + $subset = []; + if (isset($payload['propertySubset']) === true && is_array($payload['propertySubset']) === true) { + foreach ($payload['propertySubset'] as $name) { + if (is_string($name) === true && $name !== '') { + $subset[] = $name; + } + } + } + + $includeAncestors = false; + if (isset($payload['includeAncestors']) === true) { + $includeAncestors = filter_var($payload['includeAncestors'], FILTER_VALIDATE_BOOLEAN); + } + + $targetRegister = null; + if (isset($payload['targetRegister']) === true && is_numeric($payload['targetRegister']) === true) { + $targetRegister = (int) $payload['targetRegister']; + } + + return new self( + propertySubset: $subset, + includeAncestors: $includeAncestors, + targetRegister: $targetRegister + ); + }//end fromArray() + + /** + * Whether an explicit property subset was requested. + * + * @return bool True when a non-empty subset is set. + */ + public function hasSubset(): bool + { + return $this->propertySubset !== []; + }//end hasSubset() +}//end class diff --git a/lib/Service/SchemaImport/ImportedSchema.php b/lib/Service/SchemaImport/ImportedSchema.php new file mode 100644 index 0000000000..916733e165 --- /dev/null +++ b/lib/Service/SchemaImport/ImportedSchema.php @@ -0,0 +1,87 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +/** + * The mapped schema produced by a dialect importer. + * + * @spec openspec/specs/schema-import/spec.md + */ +final class ImportedSchema +{ + /** + * Constructor. + * + * @param string $title The schema title. + * @param string $description The schema description. + * @param array> $properties JSON Schema property definitions keyed by name. + * @param array $jsonld The `configuration.jsonld` block (@vocab, type, properties map). + * @param array $importSource The `configuration.importSource` provenance block. + * @param array $unknownRequested Requested property names not present on the source type. + */ + public function __construct( + public readonly string $title, + public readonly string $description, + public readonly array $properties, + public readonly array $jsonld, + public readonly array $importSource, + public readonly array $unknownRequested=[] + ) { + }//end __construct() + + /** + * Render the schema payload ready for SchemaMapper hydration / upload. + * + * Merges the jsonld + importSource fragments under `configuration`, + * preserving any caller-supplied configuration. + * + * @param array $baseConfiguration Existing configuration to merge into (e.g. on update). + * + * @return array The schema array (title, description, properties, configuration). + * + * @spec openspec/specs/schema-import/spec.md + */ + public function toSchemaArray(array $baseConfiguration=[]): array + { + $configuration = $baseConfiguration; + + if ($this->jsonld !== []) { + $configuration['jsonld'] = $this->jsonld; + } + + $configuration['importSource'] = $this->importSource; + + return [ + 'title' => $this->title, + 'description' => $this->description, + 'properties' => $this->properties, + 'configuration' => $configuration, + ]; + }//end toSchemaArray() +}//end class diff --git a/lib/Service/SchemaImport/SchemaDialectImporter.php b/lib/Service/SchemaImport/SchemaDialectImporter.php new file mode 100644 index 0000000000..14f7c544fc --- /dev/null +++ b/lib/Service/SchemaImport/SchemaDialectImporter.php @@ -0,0 +1,82 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +use OCA\OpenRegister\Exception\SchemaImportException; + +/** + * Contract for importing a register schema from an external standard. + * + * @spec openspec/specs/schema-import/spec.md + */ +interface SchemaDialectImporter +{ + /** + * The dialect identifier this importer handles (e.g. `schema.org`, `ggm`). + * + * @return string The dialect key. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function dialect(): string; + + /** + * Search the bundled snapshot for importable types/objecttypes. + * + * @param string $query A name/term query; empty returns a bounded sample. + * + * @return array> Candidates: id, label, description, parent (where applicable), snapshotVersion. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function discover(string $query): array; + + /** + * Resolve and map an external type reference into a register schema. + * + * @param string $reference The type reference (IRI, bare name, or objecttype id). + * @param ImportOptions $options Import options (subset, ancestors, target register). + * + * @return ImportedSchema The mapped schema + configuration fragments. + * + * @throws SchemaImportException When the reference is unknown to the snapshot. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function import(string $reference, ImportOptions $options): ImportedSchema; + + /** + * The bundled snapshot version this importer reads from. + * + * @return string The snapshot/release version identifier. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function snapshotVersion(): string; +}//end interface diff --git a/lib/Service/SchemaImport/SchemaImportService.php b/lib/Service/SchemaImport/SchemaImportService.php new file mode 100644 index 0000000000..8f57f5fbe9 --- /dev/null +++ b/lib/Service/SchemaImport/SchemaImportService.php @@ -0,0 +1,317 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +use OCA\OpenRegister\Exception\SchemaImportException; + +/** + * Registers dialect importers and orchestrates standards imports. + * + * @spec openspec/specs/schema-import/spec.md + */ +class SchemaImportService +{ + + /** + * Registered importers keyed by dialect. + * + * @var array + */ + private array $importers = []; + + /** + * Constructor. + * + * Builds the bundled Schema.org and GGM importers over the committed, + * versioned snapshot resources and registers them. The resource root is + * injectable so unit tests can point at fixtures. + * + * @param DialectDetector $detector The dialect detector. + * @param ThreeWayMerge $merge The pure three-way merge. + * @param string|null $resourceRoot The lib/Resources directory, or null for the bundled default. + */ + public function __construct( + private readonly DialectDetector $detector, + private readonly ThreeWayMerge $merge, + ?string $resourceRoot=null + ) { + $root = ($resourceRoot ?? dirname(__DIR__, 2).'/Resources'); + + $schemaOrgVersion = $this->readVersion(versionFile: $root.'/schemaorg/version.json', fallback: 'schemaorg-unknown'); + $this->register( + importer: new SchemaOrgImporter( + new SchemaOrgSnapshot($root.'/schemaorg/schemaorg-current-https.jsonld', $schemaOrgVersion) + ) + ); + + $ggmVersion = $this->readVersion(versionFile: $root.'/ggm/version.json', fallback: 'ggm-unknown'); + $this->register( + importer: new GgmImporter( + new GgmSnapshot($root.'/ggm/ggm-snapshot.json', $ggmVersion) + ) + ); + }//end __construct() + + /** + * Register a dialect importer (pluggability seam). + * + * @param SchemaDialectImporter $importer The importer to register. + * + * @return void + * + * @spec openspec/specs/schema-import/spec.md + */ + public function register(SchemaDialectImporter $importer): void + { + $this->importers[$importer->dialect()] = $importer; + }//end register() + + /** + * The registered dialect keys for standards import. + * + * @return array The dialect keys. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function importableDialects(): array + { + return array_keys($this->importers); + }//end importableDialects() + + /** + * Resolve a registered importer by dialect, or fail with 404. + * + * @param string $dialect The dialect key. + * + * @return SchemaDialectImporter The importer. + * + * @throws SchemaImportException When no importer handles the dialect. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function importerFor(string $dialect): SchemaDialectImporter + { + if (isset($this->importers[$dialect]) === false) { + throw new SchemaImportException( + message: sprintf('No importer registered for dialect "%s". Available: %s.', $dialect, implode(', ', $this->importableDialects())), + httpStatus: 404 + ); + } + + return $this->importers[$dialect]; + }//end importerFor() + + /** + * Discover importable types/objecttypes for a dialect. + * + * @param string $dialect The dialect key. + * @param string $query The search term. + * + * @return array { snapshotVersion: string, results: array } + * + * @throws SchemaImportException When the dialect is unknown. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function discover(string $dialect, string $query): array + { + $importer = $this->importerFor(dialect: $dialect); + return [ + 'dialect' => $dialect, + 'snapshotVersion' => $importer->snapshotVersion(), + 'results' => $importer->discover($query), + ]; + }//end discover() + + /** + * The bundled snapshot version metadata for a dialect. + * + * @param string $dialect The dialect key. + * + * @return array { dialect, snapshotVersion } + * + * @throws SchemaImportException When the dialect is unknown. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function snapshotInfo(string $dialect): array + { + $importer = $this->importerFor(dialect: $dialect); + return [ + 'dialect' => $dialect, + 'snapshotVersion' => $importer->snapshotVersion(), + ]; + }//end snapshotInfo() + + /** + * Import a type/objecttype from a dialect's snapshot. + * + * @param string $dialect The dialect key. + * @param string $reference The type reference. + * @param ImportOptions $options Import options. + * + * @return ImportedSchema The mapped schema. + * + * @throws SchemaImportException When the dialect or reference is unknown. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function import(string $dialect, string $reference, ImportOptions $options): ImportedSchema + { + return $this->importerFor(dialect: $dialect)->import($reference, $options); + }//end import() + + /** + * Import a GGM objecttype from an uploaded export (normalised intermediate). + * + * @param array $normalised The normalised GGM intermediate. + * @param string $reference The objecttype id/name to import. + * @param ImportOptions $options Import options. + * @param string $sourceLabel A provenance label for the upload. + * + * @return ImportedSchema The mapped schema. + * + * @throws SchemaImportException When the objecttype is unknown to the upload. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function importGgmUpload(array $normalised, string $reference, ImportOptions $options, string $sourceLabel='upload'): ImportedSchema + { + $importer = new GgmImporter(GgmSnapshot::fromNormalised($normalised), $sourceLabel); + return $importer->import($reference, $options); + }//end importGgmUpload() + + /** + * Detect the dialect of a decoded upload document. + * + * @param array $document The decoded document. + * + * @return string|null The detected dialect, or null when undetectable. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function detectDialect(array $document): ?string + { + return $this->detector->detect($document); + }//end detectDialect() + + /** + * Resolve the effective dialect for an upload: explicit wins, else detect, + * else fail with 422 listing the supported dialects. + * + * @param array $document The decoded document. + * @param string|null $explicitDialect The explicit dialect parameter, or null. + * + * @return string The resolved dialect. + * + * @throws SchemaImportException When the dialect cannot be resolved (422). + * + * @spec openspec/specs/schema-import/spec.md + */ + public function resolveUploadDialect(array $document, ?string $explicitDialect): string + { + if ($explicitDialect !== null && $explicitDialect !== '') { + if (in_array($explicitDialect, DialectDetector::supportedDialects(), true) === false) { + throw SchemaImportException::undetectableDialect(DialectDetector::supportedDialects()); + } + + return $explicitDialect; + } + + $detected = $this->detector->detect($document); + if ($detected === null) { + throw SchemaImportException::undetectableDialect(DialectDetector::supportedDialects()); + } + + return $detected; + }//end resolveUploadDialect() + + /** + * Compute the classified update-from-source diff for an imported schema. + * + * @param array $importSource The stored configuration.importSource block (with baseline). + * @param array> $currentProperties The schema's current properties. + * @param array $resolvedConflicts Conflict property names confirmed by the caller. + * + * @return array The merge result (added/removed/changed/keptLocal/conflicts/merged/applied). + * + * @throws SchemaImportException When the schema has no recorded import source. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function previewUpdateFromSource(array $importSource, array $currentProperties, array $resolvedConflicts=[]): array + { + $dialect = (string) ($importSource['dialect'] ?? ''); + $reference = (string) ($importSource['reference'] ?? ''); + if ($dialect === '' || $reference === '') { + throw new SchemaImportException(message: 'Schema has no recorded import source to update from.', httpStatus: 422); + } + + $baseline = ($importSource['baseline'] ?? []); + if (is_array($baseline) === false) { + $baseline = []; + } + + $fresh = $this->import(dialect: $dialect, reference: $reference, options: new ImportOptions()); + $incoming = $fresh->properties; + + $result = $this->merge->compute($baseline, $currentProperties, $incoming, $resolvedConflicts); + $result['incomingSource'] = $fresh->importSource; + $result['jsonld'] = $fresh->jsonld; + + return $result; + }//end previewUpdateFromSource() + + /** + * Read a snapshot version from its version.json, with a fallback. + * + * @param string $versionFile The version.json path. + * @param string $fallback The fallback version when the file is unreadable. + * + * @return string The version identifier. + */ + private function readVersion(string $versionFile, string $fallback): string + { + if (is_file($versionFile) === false) { + return $fallback; + } + + $decoded = json_decode((string) file_get_contents($versionFile), associative: true); + if (is_array($decoded) === true && isset($decoded['version']) === true && is_string($decoded['version']) === true) { + return $decoded['version']; + } + + return $fallback; + }//end readVersion() +}//end class diff --git a/lib/Service/SchemaImport/SchemaOrgImporter.php b/lib/Service/SchemaImport/SchemaOrgImporter.php new file mode 100644 index 0000000000..918ac67de9 --- /dev/null +++ b/lib/Service/SchemaImport/SchemaOrgImporter.php @@ -0,0 +1,318 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +use OCA\OpenRegister\Exception\SchemaImportException; + +/** + * Imports Schema.org types as register schemas. + * + * @spec openspec/specs/schema-import/spec.md + */ +class SchemaOrgImporter implements SchemaDialectImporter +{ + + /** + * The Schema.org `@vocab` base IRI written into the jsonld block. + * + * @var string + */ + private const VOCAB = 'https://schema.org/'; + + /** + * Permissiveness rank for collapsing a multi-type range to one JSON type. + * Higher = more permissive; string wins over everything. + * + * @var array + */ + private const PERMISSIVENESS = [ + 'string' => 5, + 'number' => 4, + 'integer' => 3, + 'boolean' => 2, + ]; + + /** + * Constructor. + * + * @param SchemaOrgSnapshot $snapshot The indexed Schema.org vocabulary snapshot. + */ + public function __construct(private readonly SchemaOrgSnapshot $snapshot) + { + }//end __construct() + + /** + * {@inheritDoc} + * + * @spec openspec/specs/schema-import/spec.md + * + * @return string The dialect key. + */ + public function dialect(): string + { + return DialectDetector::DIALECT_SCHEMA_ORG; + }//end dialect() + + /** + * {@inheritDoc} + * + * @spec openspec/specs/schema-import/spec.md + * + * @return string The snapshot version. + */ + public function snapshotVersion(): string + { + return $this->snapshot->version(); + }//end snapshotVersion() + + /** + * {@inheritDoc} + * + * @param string $query The search term. + * + * @return array> Candidate type records. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function discover(string $query): array + { + $version = $this->snapshot->version(); + $results = []; + foreach ($this->snapshot->searchClasses($query) as $class) { + $parent = null; + if (is_string($class['parent']) === true && $class['parent'] !== '') { + $parent = $this->snapshot->bareName($class['parent']); + } + + $results[] = [ + 'id' => $class['iri'], + 'label' => $class['label'], + 'description' => $class['comment'], + 'parent' => $parent, + 'snapshotVersion' => $version, + ]; + } + + return $results; + }//end discover() + + /** + * {@inheritDoc} + * + * @param string $reference The type IRI or bare name. + * @param ImportOptions $options Import options. + * + * @return ImportedSchema The mapped schema. + * + * @throws SchemaImportException When the type is unknown to the snapshot. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function import(string $reference, ImportOptions $options): ImportedSchema + { + $class = $this->snapshot->findClass($reference); + if ($class === null) { + throw SchemaImportException::unknownReference($reference, $this->dialect()); + } + + $className = $this->snapshot->bareName($class['iri']); + + if ($options->includeAncestors === true) { + $sourceProperties = $this->snapshot->propertiesWithAncestors($className); + } else { + $sourceProperties = $this->snapshot->directPropertiesOf($className); + } + + // Apply an explicit property subset, tracking unknown requests. + $unknownRequested = []; + if ($options->hasSubset() === true) { + $selected = []; + foreach ($options->propertySubset as $name) { + if (isset($sourceProperties[$name]) === true) { + $selected[$name] = $sourceProperties[$name]; + } else { + $unknownRequested[] = $name; + } + } + + $sourceProperties = $selected; + } + + $properties = []; + $termMap = []; + foreach ($sourceProperties as $name => $property) { + $properties[$name] = $this->mapProperty(property: $property); + $termMap[$name] = $property['iri']; + } + + $jsonld = [ + '@vocab' => self::VOCAB, + 'type' => $class['iri'], + 'properties' => $termMap, + ]; + + $importSource = [ + 'dialect' => $this->dialect(), + 'reference' => $class['iri'], + 'snapshotVersion' => $this->snapshot->version(), + 'importedAt' => gmdate('c'), + 'baseline' => $properties, + ]; + + return new ImportedSchema( + title: $class['label'], + description: $class['comment'], + properties: $properties, + jsonld: $jsonld, + importSource: $importSource, + unknownRequested: $unknownRequested + ); + }//end import() + + /** + * Map one Schema.org property record to a JSON Schema property definition. + * + * @param array $property The snapshot property record. + * + * @return array The JSON Schema property definition. + */ + private function mapProperty(array $property): array + { + $definition = $this->mapRanges(ranges: $property['ranges']); + $comment = (string) ($property['comment'] ?? ''); + if ($comment !== '') { + $definition['description'] = $comment; + } + + return $definition; + }//end mapProperty() + + /** + * Map a Schema.org range list to a JSON Schema type/format. + * + * Datatype ranges map per the normative table; object-typed ranges (a + * class) become a `string` with `format: uri` reference (never a recursive + * import); a multi-type range collapses to the most permissive member. + * + * @param array $ranges The rangeIncludes bare names. + * + * @return array The JSON Schema type fragment. + */ + private function mapRanges(array $ranges): array + { + $mapped = []; + foreach ($ranges as $range) { + $mapped[] = $this->mapSingleRange(range: $range); + } + + if ($mapped === []) { + // No declared range: default to a free-text string. + return ['type' => 'string']; + } + + if (count($mapped) === 1) { + return $mapped[0]; + } + + return $this->collapseMostPermissive(fragments: $mapped); + }//end mapRanges() + + /** + * Map a single Schema.org range (datatype or class) to a JSON fragment. + * + * @param string $range The bare range name. + * + * @return array The JSON Schema fragment. + */ + private function mapSingleRange(string $range): array + { + switch ($range) { + case 'Text': + case 'PronounceableText': + case 'CssSelectorType': + case 'XPathType': + return ['type' => 'string']; + case 'Number': + case 'Float': + return ['type' => 'number']; + case 'Integer': + return ['type' => 'integer']; + case 'Boolean': + return ['type' => 'boolean']; + case 'Date': + return ['type' => 'string', 'format' => 'date']; + case 'DateTime': + return ['type' => 'string', 'format' => 'date-time']; + case 'Time': + return ['type' => 'string', 'format' => 'time']; + case 'URL': + return ['type' => 'string', 'format' => 'uri']; + default: + // Object-typed range: a reference to another type, never a + // recursive import. Carry the target IRI for traceability. + return [ + 'type' => 'string', + 'format' => 'uri', + 'description' => 'Reference to a '.$range.' ('.$this->snapshot->classIri($range).').', + ]; + }//end switch + }//end mapSingleRange() + + /** + * Collapse a list of mapped fragments to the single most permissive one. + * + * @param array> $fragments The mapped fragments. + * + * @return array The most permissive fragment. + */ + private function collapseMostPermissive(array $fragments): array + { + $best = $fragments[0]; + $bestRank = (self::PERMISSIVENESS[$best['type']] ?? 0); + foreach ($fragments as $fragment) { + $rank = (self::PERMISSIVENESS[$fragment['type']] ?? 0); + if ($rank > $bestRank) { + $best = $fragment; + $bestRank = $rank; + } + } + + // When the most permissive choice is a plain string, drop any + // format/description carried from a more specific sibling. + if ($best['type'] === 'string' && ($best['format'] ?? null) === 'uri') { + return ['type' => 'string']; + } + + return ['type' => $best['type']]; + }//end collapseMostPermissive() +}//end class diff --git a/lib/Service/SchemaImport/SchemaOrgSnapshot.php b/lib/Service/SchemaImport/SchemaOrgSnapshot.php new file mode 100644 index 0000000000..bab9cbafb1 --- /dev/null +++ b/lib/Service/SchemaImport/SchemaOrgSnapshot.php @@ -0,0 +1,395 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +use OCA\OpenRegister\Exception\SchemaImportException; + +/** + * Indexed view over a bundled Schema.org vocabulary release. + * + * @spec openspec/specs/schema-import/spec.md + */ +class SchemaOrgSnapshot +{ + + /** + * The Schema.org namespace IRI prefix. + * + * @var string + */ + public const NAMESPACE = 'https://schema.org/'; + + /** + * Classes indexed by bare name: name => ['iri','label','comment','parent']. + * + * @var array>|null + */ + private ?array $classes = null; + + /** + * Properties indexed by bare name: name => ['iri','label','comment','domains','ranges']. + * + * @var array>|null + */ + private ?array $properties = null; + + /** + * Constructor. + * + * @param string $releaseFile The absolute path to the JSON-LD release file. + * @param string $version The snapshot version identifier. + */ + public function __construct( + private readonly string $releaseFile, + private readonly string $version + ) { + }//end __construct() + + /** + * The snapshot version identifier. + * + * @return string The version. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function version(): string + { + return $this->version; + }//end version() + + /** + * Resolve a class (type) by IRI or bare name. + * + * @param string $reference The class IRI or bare name. + * + * @return array|null The class record, or null when unknown. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function findClass(string $reference): ?array + { + $this->ensureParsed(); + $name = $this->bareName(reference: $reference); + return ($this->classes[$name] ?? null); + }//end findClass() + + /** + * The direct properties whose domain includes the given class. + * + * @param string $className The bare class name. + * + * @return array> Property records keyed by bare name. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function directPropertiesOf(string $className): array + { + $this->ensureParsed(); + + $result = []; + foreach ($this->properties as $name => $property) { + if (in_array($className, $property['domains'], true) === true) { + $result[$name] = $property; + } + } + + return $result; + }//end directPropertiesOf() + + /** + * The properties of a class plus all its ancestors' properties. + * + * @param string $className The bare class name. + * + * @return array> Property records keyed by bare name. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function propertiesWithAncestors(string $className): array + { + $this->ensureParsed(); + + $result = []; + foreach ($this->ancestryOf(className: $className) as $ancestor) { + foreach ($this->directPropertiesOf(className: $ancestor) as $name => $property) { + if (isset($result[$name]) === false) { + $result[$name] = $property; + } + } + } + + return $result; + }//end propertiesWithAncestors() + + /** + * The class chain from the given class up to its root ancestor. + * + * @param string $className The bare class name. + * + * @return array Bare class names, the class itself first. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function ancestryOf(string $className): array + { + $this->ensureParsed(); + + $chain = []; + $current = $className; + $guard = 0; + while ($current !== null && isset($this->classes[$current]) === true && $guard < 50) { + $chain[] = $current; + $parent = $this->classes[$current]['parent']; + if ($parent !== null) { + $current = $this->bareName(reference: $parent); + } else { + $current = null; + } + + $guard++; + } + + return $chain; + }//end ancestryOf() + + /** + * Search classes by a case-insensitive name/comment substring. + * + * @param string $query A search term; empty returns a bounded sample. + * @param int $limit Maximum number of results. + * + * @return array> Matching class records. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function searchClasses(string $query, int $limit=50): array + { + $this->ensureParsed(); + + $needle = strtolower(trim($query)); + $results = []; + foreach ($this->classes as $name => $class) { + if ($needle === '' + || str_contains(strtolower($name), $needle) === true + || str_contains(strtolower((string) $class['comment']), $needle) === true + ) { + $results[] = $class; + } + + if (count($results) >= $limit) { + break; + } + } + + return $results; + }//end searchClasses() + + /** + * The bare term name from an IRI or `schema:`-prefixed term. + * + * @param string $reference The IRI / prefixed term / bare name. + * + * @return string The bare name. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function bareName(string $reference): string + { + $reference = trim($reference); + if (str_starts_with($reference, self::NAMESPACE) === true) { + return substr($reference, strlen(self::NAMESPACE)); + } + + if (str_starts_with($reference, 'http://schema.org/') === true) { + return substr($reference, strlen('http://schema.org/')); + } + + if (str_starts_with($reference, 'schema:') === true) { + return substr($reference, strlen('schema:')); + } + + return $reference; + }//end bareName() + + /** + * The canonical class IRI for a bare class name. + * + * @param string $className The bare class name. + * + * @return string The class IRI. + * + * @spec openspec/specs/schema-import/spec.md + */ + public function classIri(string $className): string + { + return self::NAMESPACE.$className; + }//end classIri() + + /** + * Parse + index the release file once. + * + * @return void + * + * @throws SchemaImportException When the file is missing or unparseable. + */ + private function ensureParsed(): void + { + if ($this->classes !== null && $this->properties !== null) { + return; + } + + if (is_file($this->releaseFile) === false) { + throw new SchemaImportException( + message: 'Schema.org snapshot file is missing: '.$this->releaseFile, + httpStatus: 500 + ); + } + + $raw = (string) file_get_contents($this->releaseFile); + $decoded = json_decode($raw, associative: true); + if (is_array($decoded) === false || isset($decoded['@graph']) === false) { + throw new SchemaImportException( + message: 'Schema.org snapshot is not a valid JSON-LD release file.', + httpStatus: 500 + ); + } + + $this->classes = []; + $this->properties = []; + + foreach ($decoded['@graph'] as $node) { + if (is_array($node) === false || isset($node['@id']) === false) { + continue; + } + + $type = $this->normaliseTypes(type: $node['@type'] ?? null); + if (in_array('rdfs:Class', $type, true) === true) { + $this->indexClass(node: $node); + continue; + } + + if (in_array('rdf:Property', $type, true) === true) { + $this->indexProperty(node: $node); + } + } + }//end ensureParsed() + + /** + * Index a class node. + * + * @param array $node The JSON-LD class node. + * + * @return void + */ + private function indexClass(array $node): void + { + $iri = (string) $node['@id']; + $name = $this->bareName(reference: $iri); + $parent = null; + if (isset($node['rdfs:subClassOf']['@id']) === true) { + $parent = (string) $node['rdfs:subClassOf']['@id']; + } + + $this->classes[$name] = [ + 'iri' => $iri, + 'label' => (string) ($node['rdfs:label'] ?? $name), + 'comment' => (string) ($node['rdfs:comment'] ?? ''), + 'parent' => $parent, + ]; + }//end indexClass() + + /** + * Index a property node. + * + * @param array $node The JSON-LD property node. + * + * @return void + */ + private function indexProperty(array $node): void + { + $iri = (string) $node['@id']; + $name = $this->bareName(reference: $iri); + + $this->properties[$name] = [ + 'iri' => $iri, + 'label' => (string) ($node['rdfs:label'] ?? $name), + 'comment' => (string) ($node['rdfs:comment'] ?? ''), + 'domains' => $this->idList(value: $node['schema:domainIncludes'] ?? null), + 'ranges' => $this->idList(value: $node['schema:rangeIncludes'] ?? null), + ]; + }//end indexProperty() + + /** + * Extract bare names from a single `{"@id":...}` or a list of them. + * + * @param mixed $value The domainIncludes / rangeIncludes value. + * + * @return array Bare names. + */ + private function idList(mixed $value): array + { + if (is_array($value) === false) { + return []; + } + + // Single { "@id": ... }. + if (isset($value['@id']) === true) { + return [$this->bareName(reference: (string) $value['@id'])]; + } + + $names = []; + foreach ($value as $entry) { + if (is_array($entry) === true && isset($entry['@id']) === true) { + $names[] = $this->bareName(reference: (string) $entry['@id']); + } + } + + return $names; + }//end idList() + + /** + * Normalise a JSON-LD `@type` to a list of strings. + * + * @param mixed $type The `@type` value. + * + * @return array The type strings. + */ + private function normaliseTypes(mixed $type): array + { + if (is_string($type) === true) { + return [$type]; + } + + if (is_array($type) === true) { + return array_values(array_filter($type, 'is_string')); + } + + return []; + }//end normaliseTypes() +}//end class diff --git a/lib/Service/SchemaImport/ThreeWayMerge.php b/lib/Service/SchemaImport/ThreeWayMerge.php new file mode 100644 index 0000000000..0116d55e51 --- /dev/null +++ b/lib/Service/SchemaImport/ThreeWayMerge.php @@ -0,0 +1,179 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\SchemaImport; + +/** + * Pure three-way merge for update-from-source. + * + * @spec openspec/specs/schema-import/spec.md + */ +class ThreeWayMerge +{ + /** + * Compute the classified diff + merged result for an update-from-source. + * + * @param array> $baseline Imported baseline property definitions. + * @param array> $current Current schema property definitions. + * @param array> $incoming Freshly-imported property definitions. + * @param array $resolved Conflicting property names the caller confirmed to apply. + * + * @return array { + * added: string[], removed: string[], changed: string[], keptLocal: string[], + * conflicts: string[], merged: array>, applied: bool + * } + * + * @spec openspec/specs/schema-import/spec.md + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) The merge table is inherently branchy; each branch is one table row. + * @SuppressWarnings(PHPMD.NPathComplexity) The merge table is inherently branchy; each branch is one table row. + */ + public function compute(array $baseline, array $current, array $incoming, array $resolved=[]): array + { + $added = []; + $removed = []; + $changed = []; + $keptLocal = []; + $conflicts = []; + $merged = $current; + + $resolvedSet = array_fill_keys($resolved, true); + + $allNames = array_unique( + array_merge(array_keys($baseline), array_keys($current), array_keys($incoming)) + ); + + foreach ($allNames as $name) { + $inBaseline = array_key_exists($name, $baseline); + $inCurrent = array_key_exists($name, $current); + $inIncoming = array_key_exists($name, $incoming); + + // Added locally: present in current but never in baseline, and not + // (re)introduced by source — keep untouched. + if ($inCurrent === true && $inBaseline === false && $inIncoming === false) { + $keptLocal[] = $name; + continue; + } + + // Source adds a new property absent locally → add it. + if ($inIncoming === true && $inCurrent === false) { + $merged[$name] = $incoming[$name]; + $added[] = $name; + continue; + } + + // Source removed a property that we still carry. + if ($inIncoming === false && $inBaseline === true && $inCurrent === true) { + $removed[] = $name; + // Removal is breaking; reported, not auto-applied here. + continue; + } + + // Present in both current and incoming → compare against baseline. + if ($inCurrent === true && $inIncoming === true) { + $localModified = ($inBaseline === false || $this->differs(a: $baseline[$name] ?? null, b: $current[$name])); + $sourceChanged = ($inBaseline === false || $this->differs(a: $baseline[$name] ?? null, b: $incoming[$name])); + + if ($sourceChanged === false) { + // Source unchanged → keep local (covers local mods + identical). + $keptLocal[] = $name; + continue; + } + + if ($localModified === false) { + // Local matches baseline, source changed → apply source. + $merged[$name] = $incoming[$name]; + $changed[] = $name; + continue; + } + + // Both changed → conflict; apply only if explicitly resolved. + if (isset($resolvedSet[$name]) === true) { + $merged[$name] = $incoming[$name]; + $changed[] = $name; + } else { + $conflicts[] = $name; + } + }//end if + }//end foreach + + return [ + 'added' => $added, + 'removed' => $removed, + 'changed' => $changed, + 'keptLocal' => $keptLocal, + 'conflicts' => $conflicts, + 'merged' => $merged, + 'applied' => ($conflicts === []), + ]; + }//end compute() + + /** + * Whether two property definitions differ (order-insensitive deep compare). + * + * @param mixed $a The first definition. + * @param mixed $b The second definition. + * + * @return bool True when they differ. + */ + private function differs(mixed $a, mixed $b): bool + { + return $this->canonical(value: $a) !== $this->canonical(value: $b); + }//end differs() + + /** + * Canonicalise a value for a stable, order-insensitive comparison. + * + * @param mixed $value The value. + * + * @return mixed The canonicalised value. + */ + private function canonical(mixed $value): mixed + { + if (is_array($value) === true) { + $copy = []; + foreach ($value as $key => $item) { + $copy[$key] = $this->canonical(value: $item); + } + + ksort($copy); + return $copy; + } + + return $value; + }//end canonical() +}//end class diff --git a/lib/Service/SchemaService.php b/lib/Service/SchemaService.php index 7145ff4203..28f9fb8ced 100644 --- a/lib/Service/SchemaService.php +++ b/lib/Service/SchemaService.php @@ -50,6 +50,14 @@ */ class SchemaService { + /** + * Maximum number of objects sampled when exploring a schema's properties. + * + * Property exploration is statistical, so a bounded sample is sufficient; + * loading every object of a schema (potentially hundreds of thousands) into + * memory to analyse it is an OOM hazard (bound-unbounded-query-memory). + */ + private const SCHEMA_EXPLORE_SAMPLE_SIZE = 1000; /** * Schema mapper for schema operations @@ -108,7 +116,7 @@ public function __construct( * * @throws \Exception If schema not found or analysis fails * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-3/tasks.md#task-1 + * @spec openspec/specs/schema-property-exploration/spec.md */ public function exploreSchemaProperties(int $schemaId): array { @@ -124,11 +132,13 @@ public function exploreSchemaProperties(int $schemaId): array throw new Exception('Schema not found with ID: '.$schemaId); } - // Get all objects for this schema. - $objects = $this->objectEntityMapper->findBySchema($schemaId); + // Sample a bounded set of objects for this schema. Exploration is + // statistical, so a capped sample avoids loading the full (potentially + // huge) object set into memory (bound-unbounded-query-memory). + $objects = $this->objectEntityMapper->findBySchema($schemaId, self::SCHEMA_EXPLORE_SAMPLE_SIZE); $this->logger->info( - message: '[SchemaService] Found '.count($objects).' objects to analyze', + message: '[SchemaService] Analyzing a sample of '.count($objects).' objects (cap '.self::SCHEMA_EXPLORE_SAMPLE_SIZE.')', context: ['file' => __FILE__, 'line' => __LINE__] ); @@ -303,8 +313,18 @@ private function analyzePropertyValue($value): array $analysis['min_length'] = $length; // Detect format based on string patterns. - $analysis['detected_format'] = $this->detectStringFormat(value: $value); - $analysis['string_patterns'][] = $this->analyzeStringPattern(value: $value); + $analysis['detected_format'] = $this->detectStringFormat(value: $value); + // AnalyzeStringPattern() returns a flat list of pattern-name + // strings. Merge it (rather than append the whole array) so + // string_patterns stays a flat string[] as every consumer + // expects — getTypeFromPatterns' in_array checks, the + // array_unique dedup, and the [0] "main pattern". Appending + // nested it, which broke those lookups and triggered + // array-to-string warnings. + $analysis['string_patterns'] = array_merge( + $analysis['string_patterns'], + $this->analyzeStringPattern(value: $value) + ); break; case 'integer': @@ -473,8 +493,11 @@ private function analyzeStringPattern(string $value): array $patterns[] = 'SCREAMING_SNAKE_CASE'; } - // Check for filename patterns. - if (preg_match('/^[^<>:"/\\|?*]+\.[a-zA-Z0-9]+$/', $value) === 1) { + // Check for filename patterns. Use ~ as the delimiter: the class + // contains a literal "/", which would prematurely close a /…/ pattern + // (PCRE delimiter scanning is naive), making preg_match error with + // "Unknown modifier '\'" and silently never match. + if (preg_match('~^[^<>:"/\\|?*]+\.[a-zA-Z0-9]+$~', $value) === 1) { $patterns[] = 'filename'; } @@ -1745,7 +1768,7 @@ private function generateArrayItemType(array $arrayStructure): array * * @return Schema Updated schema entity * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-3/tasks.md#task-2 + * @spec openspec/specs/schema-property-exploration/spec.md */ public function updateSchemaFromExploration(int $schemaId, array $propertyUpdates): Schema { diff --git a/lib/Service/Schemas/FacetCacheHandler.php b/lib/Service/Schemas/FacetCacheHandler.php index 7ff97fa995..48a968b77b 100644 --- a/lib/Service/Schemas/FacetCacheHandler.php +++ b/lib/Service/Schemas/FacetCacheHandler.php @@ -23,7 +23,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-schema-id-in-non-aggregated-facet-response */ namespace OCA\OpenRegister\Service\Schemas; @@ -255,7 +255,7 @@ public function cacheFacetableFields(int $schemaId, array $facetableFields, int * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Operation parameter with default is not a boolean * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/faceting-configuration/spec.md#requirement-schema-id-in-non-aggregated-facet-response */ public function invalidateForSchemaChange(int $schemaId, string $operation='update'): void { diff --git a/lib/Service/Schemas/PropertyValidatorHandler.php b/lib/Service/Schemas/PropertyValidatorHandler.php index fbf84b8422..dbcc9c7ad1 100644 --- a/lib/Service/Schemas/PropertyValidatorHandler.php +++ b/lib/Service/Schemas/PropertyValidatorHandler.php @@ -146,18 +146,30 @@ public function validateProperty(array $property, string $path=''): bool throw new Exception("Property at '$path' must have a 'type' field"); } - // Validate type. + // Validate type. Union types arrive as arrays — render them as JSON in + // the message instead of letting string interpolation emit a PHP + // "Array to string conversion" warning. if (in_array($property['type'], $this->validTypes) === false) { + $typeLabel = $property['type']; + if (is_string($typeLabel) === false) { + $typeLabel = (string) json_encode($typeLabel); + } + throw new Exception( - "Invalid type '{$property['type']}' at '$path'. Must be one of: ".implode(', ', $this->validTypes) + "Invalid type '{$typeLabel}' at '$path'. Must be one of: ".implode(', ', $this->validTypes) ); } // Validate string format if present. if ($property['type'] === 'string' && (($property['format'] ?? null) !== null)) { if (in_array($property['format'], $this->validStringFormats) === false) { + $formatLabel = $property['format']; + if (is_string($formatLabel) === false) { + $formatLabel = (string) json_encode($formatLabel); + } + $validFormats = implode(', ', $this->validStringFormats); - $message = "Invalid string format '{$property['format']}' at '$path'. Must be one of: $validFormats"; + $message = "Invalid string format '{$formatLabel}' at '$path'. Must be one of: $validFormats"; throw new Exception($message); } } @@ -218,6 +230,28 @@ public function validateProperty(array $property, string $path=''): bool throw new Exception("'hideOnForm' at '$path' must be a boolean"); } + // Validate sourceLanguage modifier (i18n-source-of-truth). + // Only allowed on translatable properties; rejects on non-translatable. + if (array_key_exists('sourceLanguage', $property) === true) { + if (($property['translatable'] ?? false) !== true) { + $msg = "'sourceLanguage' at '$path' requires translatable: true"; + throw new Exception($msg); + } + + $sourceLanguage = $property['sourceLanguage']; + if (is_string($sourceLanguage) === false || $sourceLanguage === '') { + throw new Exception("'sourceLanguage' at '$path' must be a non-empty string"); + } + + // Basic BCP-47 syntax check: 2-3 lowercase letters, optional + // region/subtag suffix. + if (preg_match('/^[a-z]{2,3}(-[a-zA-Z0-9]{2,8})*$/', $sourceLanguage) !== 1) { + throw new Exception( + "'sourceLanguage' at '$path' is not a valid BCP-47 language tag: '$sourceLanguage'" + ); + } + } + // Validate onDelete property if present. if (($property['onDelete'] ?? null) !== null) { // OnDelete is only valid on relation properties (those with $ref). diff --git a/lib/Service/Schemas/SchemaCacheHandler.php b/lib/Service/Schemas/SchemaCacheHandler.php index 2066ebb678..46f4bb91fc 100644 --- a/lib/Service/Schemas/SchemaCacheHandler.php +++ b/lib/Service/Schemas/SchemaCacheHandler.php @@ -199,7 +199,7 @@ public function __construct( * * @throws \OCP\DB\Exception If a database error occurs * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getSchema(int $schemaId): ?Schema { @@ -254,7 +254,7 @@ public function getSchema(int $schemaId): ?Schema * * @return void * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-11 + * @spec openspec/specs/object-lifecycle/spec.md */ public function clearSchemaCache(int $schemaId): void { @@ -296,7 +296,7 @@ public function clearSchemaCache(int $schemaId): void * * @throws \OCP\DB\Exception If a database error occurs * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-11 + * @spec openspec/specs/object-lifecycle/spec.md */ public function cacheSchema(Schema $schema, int $ttl=self::DEFAULT_TTL): void { @@ -324,7 +324,7 @@ public function cacheSchema(Schema $schema, int $ttl=self::DEFAULT_TTL): void * * @throws \OCP\DB\Exception If a database error occurs * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-11 + * @spec openspec/specs/object-lifecycle/spec.md */ public function cacheSchemaConfiguration(Schema $schema, int $ttl=self::DEFAULT_TTL): void { @@ -347,7 +347,7 @@ public function cacheSchemaConfiguration(Schema $schema, int $ttl=self::DEFAULT_ * * @throws \OCP\DB\Exception If a database error occurs * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-11 + * @spec openspec/specs/object-lifecycle/spec.md */ public function cacheSchemaProperties(Schema $schema, int $ttl=self::DEFAULT_TTL): void { @@ -373,7 +373,7 @@ public function cacheSchemaProperties(Schema $schema, int $ttl=self::DEFAULT_TTL * * @throws \OCP\DB\Exception If a database error occurs. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-object-facade/tasks.md#task-5 + * @spec openspec/specs/object-lifecycle/spec.md */ public function invalidate(int $schemaId): void { @@ -402,7 +402,7 @@ public function invalidate(int $schemaId): void * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Operation parameter with default is not a boolean * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-11 + * @spec openspec/specs/object-lifecycle/spec.md */ public function invalidateForSchemaChange(int $schemaId, string $operation='update'): void { @@ -466,7 +466,7 @@ public function invalidateForSchemaChange(int $schemaId, string $operation='upda * * @throws \OCP\DB\Exception If a database error occurs * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-11 + * @spec openspec/specs/object-lifecycle/spec.md */ public function clearAllCaches(): void { @@ -507,7 +507,7 @@ public function clearAllCaches(): void * * @psalm-return int * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-11 + * @spec openspec/specs/object-lifecycle/spec.md */ public function cleanExpiredEntries(): int { @@ -544,7 +544,7 @@ public function cleanExpiredEntries(): int * * @throws \OCP\DB\Exception If a database error occurs. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-11 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getCacheStatistics(): array { diff --git a/lib/Service/ScriptManifestLoader.php b/lib/Service/ScriptManifestLoader.php new file mode 100644 index 0000000000..32afd12d99 --- /dev/null +++ b/lib/Service/ScriptManifestLoader.php @@ -0,0 +1,161 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use OCP\Util; + +/** + * Loads the webpack output for an entry point as a set of cooperating chunks. + * + * The production webpack build splits shared dependencies (vue, @nextcloud/vue, + * pinia, the nextcloud-vue source, …) out of each entry into shared chunks so + * they are bundled once instead of once per entry. As a result an entry is no + * longer a single self-contained file: the page must load every initial chunk + * the entry depends on, in order, before the entry chunk can run. + * + * The build emits js/openregister-entrypoints.json mapping each entry name to + * its ordered list of initial chunk filenames. Split-chunk filenames are + * content-hashed and change every build, so they cannot be hardcoded in PHP — + * this loader resolves them from the manifest at render time. + * + * If the manifest is absent (e.g. a dev build, or an older build predating the + * split), the loader falls back to enqueuing the single legacy entry script so + * behaviour degrades gracefully rather than breaking the page. + * + * @psalm-suppress UnusedClass + */ +class ScriptManifestLoader +{ + + /** + * In-process cache of the decoded manifest, keyed by absolute path. + * + * @var array> + */ + private static array $manifestCache = []; + + /** + * Enqueue every initial chunk of an entry point. + * + * @param string $appId The Nextcloud app id (e.g. 'openregister'). + * @param string $entry The webpack entry name (e.g. 'main'). + * @param string $fallbackScript The legacy single-script name to enqueue + * when the manifest is unavailable, without + * the '.js' extension (e.g. 'openregister-main'). + * + * @return void + * + * @SuppressWarnings(PHPMD.StaticAccess) \OCP\Util::addScript is the + * canonical Nextcloud API for enqueuing scripts and has no injectable + * DI equivalent in the AppFramework. + */ + public static function addEntryScripts(string $appId, string $entry, string $fallbackScript): void + { + $chunks = self::resolveEntryScripts(appId: $appId, entry: $entry); + + if ($chunks === null) { + Util::addScript($appId, $fallbackScript); + return; + } + + foreach ($chunks as $chunk) { + Util::addScript($appId, $chunk); + } + + }//end addEntryScripts() + + /** + * Resolve the ordered chunk names (without '.js') for an entry. + * + * Returns null when the manifest is missing, malformed, or does not list the + * requested entry; callers treat null as "fall back to the legacy script". + * + * @param string $appId The Nextcloud app id. + * @param string $entry The webpack entry name. + * @param string|null $jsDirectory Override for the directory containing the + * manifest. Defaults to the app's js/ folder; + * primarily a seam for testing. + * + * @return string[]|null Ordered chunk names, or null when the entry cannot + * be resolved from a manifest. + */ + public static function resolveEntryScripts(string $appId, string $entry, ?string $jsDirectory=null): ?array + { + $manifest = self::loadManifest(appId: $appId, jsDirectory: $jsDirectory); + + if ($manifest === null || isset($manifest[$entry]) === false) { + return null; + } + + $chunks = []; + foreach ($manifest[$entry] as $file) { + // The manifest stores filenames with the '.js' extension; addScript + // expects the name without it. + $chunks[] = preg_replace('/\.js$/', '', $file); + } + + return $chunks; + + }//end resolveEntryScripts() + + /** + * Load and cache the entrypoints manifest for an app. + * + * @param string $appId The Nextcloud app id. + * @param string|null $jsDirectory Override for the directory containing the + * manifest. Defaults to the app's js/ folder. + * + * @return array|null The decoded manifest, or null when + * it is missing or invalid. + */ + private static function loadManifest(string $appId, ?string $jsDirectory=null): ?array + { + $directory = ($jsDirectory ?? __DIR__.'/../../js'); + $path = $directory.'/'.$appId.'-entrypoints.json'; + + if (array_key_exists($path, self::$manifestCache) === true) { + return self::$manifestCache[$path]; + } + + if (is_file($path) === false) { + return null; + } + + $contents = file_get_contents($path); + if ($contents === false) { + return null; + } + + $decoded = json_decode($contents, true); + if (is_array($decoded) === false) { + return null; + } + + self::$manifestCache[$path] = $decoded; + return $decoded; + + }//end loadManifest() +}//end class diff --git a/lib/Service/Search/PlaceholderResolver.php b/lib/Service/Search/PlaceholderResolver.php index f8fa35c3d8..676e45ab74 100644 --- a/lib/Service/Search/PlaceholderResolver.php +++ b/lib/Service/Search/PlaceholderResolver.php @@ -21,7 +21,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-3 + * @spec openspec/specs/aggregations-backend-native/spec.md */ declare(strict_types=1); @@ -65,7 +65,7 @@ public function __construct( * @return mixed Resolved value: DateTimeImmutable for date placeholders, * string for $currentUser, original value otherwise. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-3 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function resolve(mixed $value): mixed { @@ -87,7 +87,7 @@ public function resolve(mixed $value): mixed * * @return array * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-3 + * @spec openspec/specs/aggregations-backend-native/spec.md */ public function resolveArray(array $values): array { diff --git a/lib/Service/SearchTrailService.php b/lib/Service/SearchTrailService.php index 1d7086a7aa..77a3359edd 100644 --- a/lib/Service/SearchTrailService.php +++ b/lib/Service/SearchTrailService.php @@ -23,7 +23,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-89 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-saved-searches-and-search-trails */ namespace OCA\OpenRegister\Service; @@ -171,7 +171,7 @@ public function createSearchTrail( * * @psalm-suppress PossiblyUnusedReturnValue * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-89 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-saved-searches-and-search-trails */ public function clearExpiredSearchTrails(): array { @@ -219,7 +219,7 @@ public function clearExpiredSearchTrails(): array * * @psalm-return array{results: array, total: int, page: float|int<1, max>, pages: int, limit: int<1, max>, offset: int} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-89 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-saved-searches-and-search-trails */ public function getSearchTrails(array $config=[]): array { @@ -287,7 +287,7 @@ public function getSearchTrail(int $id): SearchTrail * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple statistics calculations and aggregations * @SuppressWarnings(PHPMD.NPathComplexity) Multiple conditional statistics computations * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-5 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getSearchStatistics(?DateTime $from=null, ?DateTime $to=null): array { @@ -368,7 +368,7 @@ public function getSearchStatistics(?DateTime $from=null, ?DateTime $to=null): a * * @return array Popular search terms data * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-5 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getPopularSearchTerms(int $limit=10, ?DateTime $from=null, ?DateTime $to=null): array { @@ -413,7 +413,7 @@ function ($term) use ($totalSearches) { * * @return array Search activity data with insights * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-5 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getSearchActivity(string $interval='day', ?DateTime $from=null, ?DateTime $to=null): array { @@ -441,7 +441,7 @@ public function getSearchActivity(string $interval='day', ?DateTime $from=null, * * @return array Register/schema statistics data * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-5 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getRegisterSchemaStatistics(?DateTime $from=null, ?DateTime $to=null): array { @@ -490,7 +490,7 @@ function ($a, $b) { * * @return array User agent statistics data * - * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-1/tasks.md#task-5 + * @spec openspec/specs/zoeken-filteren/spec.md */ public function getUserAgentStatistics(int $limit=10, ?DateTime $from=null, ?DateTime $to=null): array { @@ -536,7 +536,7 @@ function ($stat) { * * @SuppressWarnings(PHPMD.UnusedFormalParameter) $_before kept for API compatibility * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-89 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-saved-searches-and-search-trails */ public function cleanupSearchTrails(?DateTime $_before=null): array { @@ -580,7 +580,7 @@ public function cleanupSearchTrails(?DateTime $_before=null): array * @SuppressWarnings(PHPMD.NPathComplexity) Multiple conditional configuration paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive configuration processing * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-89 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-saved-searches-and-search-trails */ private function processConfig(array $config): array { @@ -892,7 +892,7 @@ private function aggregateByBrowser(array $userAgentStats): array * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple entity lookups with exception handling * @SuppressWarnings(PHPMD.NPathComplexity) Multiple conditional entity lookups and exception handling * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-89 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-saved-searches-and-search-trails */ private function enrichTrailsWithNames(array $trails): array { diff --git a/lib/Service/SecurityService.php b/lib/Service/SecurityService.php index 4e16f813b1..b49a106b9d 100644 --- a/lib/Service/SecurityService.php +++ b/lib/Service/SecurityService.php @@ -126,6 +126,80 @@ public function __construct( $this->logger = $logger; }//end __construct() + /** + * Assert that a user-supplied URL is safe to fetch server-side (anti-SSRF). + * + * Validates scheme (http/https only) and resolves EVERY A and AAAA record + * for the host, rejecting the request if ANY resolved address is loopback, + * private (RFC-1918 / ULA), or reserved/link-local. This is the single + * shared guard that every server-side URL fetcher in the app MUST call + * before issuing a request (UploadService, FilePropertyHandler, + * ConfigurationController, GitHub/GitLab handlers, webhooks). + * + * Callers that follow redirects MUST re-validate every redirect target, + * or disable redirect following entirely, because this check is a + * point-in-time DNS resolution and does not pin the connection IP. + * + * @param string $url The user-supplied URL to validate. + * + * @return void + * + * @throws \InvalidArgumentException When the URL is malformed or resolves to a non-public address. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public static function assertSafeFetchUrl(string $url): void + { + $parts = parse_url($url); + if ($parts === false || empty($parts['scheme']) === true || empty($parts['host']) === true) { + throw new \InvalidArgumentException('Invalid URL.'); + } + + $scheme = strtolower($parts['scheme']); + if (in_array($scheme, ['http', 'https'], true) === false) { + throw new \InvalidArgumentException('Only http and https URLs are allowed.'); + } + + $host = $parts['host']; + + // Collect every IP the host resolves to (IPv4 + IPv6). If the host is + // already a literal IP, validate it directly. + $ips = []; + if (filter_var($host, FILTER_VALIDATE_IP) !== false) { + $ips[] = $host; + } else { + $v4 = gethostbynamel($host); + if (is_array($v4) === true) { + $ips = array_merge($ips, $v4); + } + + $aaaa = @dns_get_record($host, DNS_AAAA); + if (is_array($aaaa) === true) { + foreach ($aaaa as $record) { + if (isset($record['ipv6']) === true) { + $ips[] = $record['ipv6']; + } + } + } + } + + if (empty($ips) === true) { + // Fail closed: a host we cannot resolve must not be fetched. + throw new \InvalidArgumentException('URL host could not be resolved.'); + } + + foreach ($ips as $ip) { + $isPublic = filter_var( + $ip, + FILTER_VALIDATE_IP, + (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) + ); + if ($isPublic === false) { + throw new \InvalidArgumentException('URL resolves to a non-public address and cannot be fetched.'); + } + } + }//end assertSafeFetchUrl() + /** * Check if login attempt is allowed based on rate limiting rules * @@ -136,7 +210,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function checkLoginRateLimit(string $username, string $ipAddress): array { @@ -224,7 +298,7 @@ public function checkLoginRateLimit(string $username, string $ipAddress): array * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function recordFailedLoginAttempt(string $username, string $ipAddress, string $reason='invalid_credentials'): void { @@ -291,7 +365,7 @@ public function recordFailedLoginAttempt(string $username, string $ipAddress, st * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function recordSuccessfulLogin(string $username, string $ipAddress): void { @@ -335,7 +409,7 @@ public function recordSuccessfulLogin(string $username, string $ipAddress): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function clearIpRateLimits(string $ipAddress): void { @@ -363,7 +437,7 @@ public function clearIpRateLimits(string $ipAddress): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function clearUserRateLimits(string $username): void { @@ -405,7 +479,7 @@ public function clearUserRateLimits(string $username): void * * @return array{allowed: bool, lockout_until?: int, reason?: string} Result * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function checkAuthRateLimit(string $identity, string $ipAddress): array { @@ -439,7 +513,7 @@ public function checkAuthRateLimit(string $identity, string $ipAddress): array * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function recordFailedAuthAttempt(string $identity, string $ipAddress, string $reason='inbound_auth_failed'): void { @@ -499,7 +573,7 @@ public function recordFailedAuthAttempt(string $identity, string $ipAddress, str * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-3 + * @spec openspec/specs/auth-system/spec.md#requirement-rate-limiting-must-protect-against-brute-force-attacks-and-api-abuse */ public function recordSuccessfulAuth(string $identity, string $ipAddress): void { @@ -522,7 +596,7 @@ public function recordSuccessfulAuth(string $identity, string $ipAddress): void * * @return string The trusted client IP address * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-5 + * @spec openspec/specs/auth-system/spec.md#requirement-cors-policy-must-be-enforced-per-consumer-and-prevent-csrf */ public function getTrustedClientIpAddress(IRequest $request): string { @@ -544,8 +618,8 @@ private function buildAuthCompositeKey(string $identity, string $ipAddress): str $identityValue = 'anonymous'; } - $identity = $this->sanitizeForCacheKey(input: $identityValue); - $ipKey = $this->sanitizeForCacheKey(input: $ipAddress); + $identity = $this->sanitizeForCacheKey(input: $identityValue); + $ipKey = $this->sanitizeForCacheKey(input: $ipAddress); return $identity.'_'.$ipKey; }//end buildAuthCompositeKey() @@ -560,7 +634,7 @@ private function buildAuthCompositeKey(string $identity, string $ipAddress): str * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-4 + * @spec openspec/specs/auth-system/spec.md#requirement-input-sanitization-must-prevent-xss-and-injection-attacks */ public function sanitizeInput(mixed $input, int $maxLength=255): mixed { @@ -606,7 +680,7 @@ public function sanitizeInput(mixed $input, int $maxLength=255): mixed * * @return array Validated and sanitized credentials or error information * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-4 + * @spec openspec/specs/auth-system/spec.md#requirement-input-sanitization-must-prevent-xss-and-injection-attacks */ public function validateLoginCredentials(array $credentials): array { @@ -657,7 +731,7 @@ public function validateLoginCredentials(array $credentials): array * * @return JSONResponse The response with added security headers * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-5 + * @spec openspec/specs/auth-system/spec.md#requirement-cors-policy-must-be-enforced-per-consumer-and-prevent-csrf */ public function addSecurityHeaders(JSONResponse $response): JSONResponse { @@ -682,37 +756,18 @@ public function addSecurityHeaders(JSONResponse $response): JSONResponse * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-urn-sec-edepot-view/tasks.md#task-5 + * @spec openspec/specs/auth-system/spec.md#requirement-cors-policy-must-be-enforced-per-consumer-and-prevent-csrf */ public function getClientIpAddress(IRequest $request): string { - $ipAddress = $request->getRemoteAddress(); - - $forwardedHeaders = [ - 'HTTP_CF_CONNECTING_IP', - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_REAL_IP', - 'HTTP_X_FORWARDED', - 'HTTP_FORWARDED_FOR', - 'HTTP_FORWARDED', - ]; - - foreach ($forwardedHeaders as $header) { - $headerValue = $request->getHeader($header); - if (empty($headerValue) === false) { - $ipList = explode(',', $headerValue); - $clientIp = trim($ipList[0]); - - $flags = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE; - $isPublic = filter_var($clientIp, FILTER_VALIDATE_IP, $flags); - if ($isPublic !== false) { - $ipAddress = $clientIp; - break; - } - } - } - - return $ipAddress; + // SEC-SVC-11: rely on Nextcloud's IRequest::getRemoteAddress(), which + // already resolves the real client IP via the configured + // `trusted_proxies` / `forwarded_for_headers` system settings. Parsing + // X-Forwarded-For / X-Real-IP / CF-Connecting-IP unconditionally let + // any client spoof its source IP and bypass per-IP rate limiting and + // lockouts. NC only honours those headers when the request actually + // originates from a configured trusted proxy. + return $request->getRemoteAddress(); }//end getClientIpAddress() /** diff --git a/lib/Service/SemanticTypeResolver.php b/lib/Service/SemanticTypeResolver.php new file mode 100644 index 0000000000..a0a9acb649 --- /dev/null +++ b/lib/Service/SemanticTypeResolver.php @@ -0,0 +1,513 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\JsonLd\JsonLdContextService; +use OCP\App\IAppManager; +use Psr\Log\LoggerInterface; + +/** + * Resolve a canonical semantic-type URI to the installed schema implementing + * it, null-safe across all registers, with a deterministic tie-break. + * + * Class complexity is inherent: every resolution path is deliberately + * null-safe (each mapper/app-manager call is individually guarded so a missing + * provider degrades rather than raises), which unavoidably raises the WMC. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + */ +final class SemanticTypeResolver +{ + + /** + * Request-scoped cache of resolved schemas, keyed by + * "{uri}:{consumingRegisterId|''}". A null cache entry records a proven + * "no provider" so repeated lookups in one request stay cheap. Mirrors + * {@see \OCA\OpenRegister\Service\JsonLd\JsonLdContextService::$contextCache}. + * + * @var array + */ + private array $resolveCache = []; + + /** + * Wire the resolver against the canonical OR mappers + JSON-LD helper. + * + * Constructor-injection only; the service is autowired (no explicit DI + * registration needed, matching RegisterResolverService). + * + * @param SchemaMapper $schemaMapper Cross-register, org/RBAC-scoped schema enumeration. + * @param RegisterMapper $registerMapper Register enumeration for register/app resolution + tie-break. + * @param JsonLdContextService $jsonLdContextService Computes a schema's implemented semantic types. + * @param LoggerInterface $logger Logger for ambiguity WARN. + * @param IAppManager|null $appManager App manager used to treat a provider whose owning + * app is disabled as unavailable; null-safe (when + * unavailable the app-enabled filter is skipped). + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly RegisterMapper $registerMapper, + private readonly JsonLdContextService $jsonLdContextService, + private readonly LoggerInterface $logger, + private readonly ?IAppManager $appManager=null, + ) { + + }//end __construct() + + /** + * Resolve a semantic-type URI to the schema that implements it. + * + * Enumerates all schemas the caller can see, keeps those whose implemented + * types contain `$uri`, and returns the deterministic pick — or `null` when + * none implement it. Never raises for a "not found" outcome. + * + * Tie-break when >1 candidate matches: (a) a schema in the consuming + * register (when known), else (b) the first candidate by slug. A WARN log + * records the ambiguity and the chosen provider. + * + * @param string $uri The canonical semantic-type URI (absolute IRI). + * @param int|null $consumingRegisterId The consuming schema's register id, used only to bias the tie-break. + * + * @return Schema|null The implementing schema, or null when none is installed. + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + * (Requirement: Resolution is null-safe across installed schemas) + */ + public function resolveSchemaByImplements(string $uri, ?int $consumingRegisterId=null): ?Schema + { + if ($uri === '') { + return null; + } + + $cacheKey = $uri.':'.((string) ($consumingRegisterId ?? '')); + if (array_key_exists($cacheKey, $this->resolveCache) === true) { + return $this->resolveCache[$cacheKey]; + } + + // Enumerate every schema the caller may read (org/RBAC-scoped, all + // registers). findAll never throws "not found"; treat any failure as + // "no providers" so resolution stays null-safe. + try { + $schemas = $this->schemaMapper->findAll(); + } catch (\Throwable $e) { + $this->logger->debug( + message: '[SemanticTypeResolver] schema enumeration failed — treating as no providers', + context: ['file' => __FILE__, 'line' => __LINE__, 'uri' => $uri, 'exception' => $e->getMessage()] + ); + $this->resolveCache[$cacheKey] = null; + return null; + } + + $candidates = []; + foreach ($schemas as $schema) { + $implemented = $this->implementedTypesWithAncestors(schema: $schema); + if (in_array($uri, $implemented, true) === false) { + continue; + } + + // A provider whose owning register's app is disabled is NOT an + // available provider (ADR-048): its schemas may linger in OR after + // `occ app:disable`, but the app that gives them meaning is gone, so + // resolution must degrade exactly as if no provider were installed. + if ($this->isSchemaProvidedByEnabledApp(schema: $schema) === false) { + continue; + } + + $candidates[] = $schema; + } + + if ($candidates === []) { + // Standalone-safe: no installed schema provides this type. + $this->resolveCache[$cacheKey] = null; + return null; + } + + if (count($candidates) === 1) { + $this->resolveCache[$cacheKey] = $candidates[0]; + return $candidates[0]; + } + + $pick = $this->tieBreak(uri: $uri, candidates: $candidates, consumingRegisterId: $consumingRegisterId); + $this->resolveCache[$cacheKey] = $pick; + return $pick; + + }//end resolveSchemaByImplements() + + /** + * Compute a schema's implemented semantic types INCLUDING those inherited via + * `allOf`. + * + * A schema's implemented types are the union of its OWN markers + * ({@see \OCA\OpenRegister\Service\JsonLd\JsonLdContextService::getImplementedTypes()}) + * and the implemented types of every schema it extends via `allOf`, resolved + * recursively with a visited-set circular guard (mirroring + * {@see \OCA\OpenRegister\Db\SchemaMapper::resolveAllOf()}). A child ADDS, + * never removes — so a schema that `allOf`-extends a `schema:Person` schema + * resolves for `https://schema.org/Person` even with no marker of its own. A + * schema with no `allOf` is unaffected (exactly its own markers). + * + * The ancestor walk lives here rather than in `JsonLdContextService` so that + * service stays dependency-light; this resolver already holds a `SchemaMapper` + * to load each `allOf` parent by id/uuid/slug. + * + * @param Schema $schema The schema whose implemented types to compute. + * @param array $visited Visited schema identifiers (circular guard). + * + * @return array The union of own + inherited implemented types. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-1.1 + */ + private function implementedTypesWithAncestors(Schema $schema, array $visited=[]): array + { + // Mark this schema visited so a cyclic `allOf` never loops. + $currentId = (string) $schema->getId(); + if ($currentId !== '' && in_array($currentId, $visited, true) === true) { + return []; + } + + if ($currentId !== '') { + $visited[] = $currentId; + } + + // Start with the schema's own markers. + $types = $this->jsonLdContextService->getImplementedTypes(schema: $schema); + + $allOf = $schema->getAllOf(); + if (is_array($allOf) === false || $allOf === []) { + return array_values(array_unique($types)); + } + + // Union each `allOf` ancestor's implemented types (recursive). + foreach ($allOf as $parentRef) { + foreach ($this->ancestorTypesForRef(parentRef: $parentRef, visited: $visited) as $inherited) { + $types[] = $inherited; + } + } + + return array_values(array_unique($types)); + + }//end implementedTypesWithAncestors() + + /** + * Resolve the implemented types contributed by a single `allOf` parent + * reference, empty when the ref is unusable or its ancestor cannot be read. + * + * Extracted from {@see self::implementedTypesWithAncestors()} so the ancestor + * walk stays within complexity limits; behaviour is identical (an unusable or + * unresolved parent contributes nothing and never raises). + * + * @param mixed $parentRef A single `allOf` entry (schema id/uuid/slug). + * @param array $visited Visited schema identifiers (circular guard). + * + * @return array The parent's (recursive) implemented types. + * + * @spec openspec/changes/virtual-schema-semantic-providers/tasks.md#task-1.1 + */ + private function ancestorTypesForRef(mixed $parentRef, array $visited): array + { + if (is_string($parentRef) === false && is_int($parentRef) === false) { + return []; + } + + if ((string) $parentRef === '') { + return []; + } + + try { + $parent = $this->schemaMapper->find(id: $parentRef); + } catch (\Throwable $e) { + // A missing/unreadable ancestor contributes nothing; never raise. + $this->logger->debug( + message: '[SemanticTypeResolver] allOf ancestor unresolved — skipping', + context: ['file' => __FILE__, 'line' => __LINE__, 'ref' => (string) $parentRef, 'exception' => $e->getMessage()] + ); + return []; + } + + return $this->implementedTypesWithAncestors(schema: $parent, visited: $visited); + + }//end ancestorTypesForRef() + + /** + * Find the register a resolved schema belongs to. + * + * Registers hold their schema ids in `Register::getSchemas()`; a schema + * carries no back-reference, so we scan registers for membership. Returns + * the first register whose schema-id list contains the schema id. Null when + * the schema is orphaned (belongs to no register) or enumeration fails. + * + * @param Schema $schema The resolved schema. + * + * @return Register|null The owning register, or null when none is found. + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + * (Requirement: Resolution is null-safe across installed schemas) + */ + public function findRegisterForSchema(Schema $schema): ?Register + { + $schemaId = $schema->getId(); + + try { + $registers = $this->registerMapper->findAll(); + } catch (\Throwable $e) { + $this->logger->debug( + message: '[SemanticTypeResolver] register enumeration failed', + context: ['file' => __FILE__, 'line' => __LINE__, 'schemaId' => $schemaId, 'exception' => $e->getMessage()] + ); + return null; + } + + foreach ($registers as $register) { + foreach ($register->getSchemas() as $memberId) { + if ((int) $memberId === (int) $schemaId) { + return $register; + } + } + } + + return null; + + }//end findRegisterForSchema() + + /** + * Whether the app that owns a candidate schema is installed AND enabled. + * + * A schema resolves ONLY when its owning app is enabled — a disabled + * provider app degrades to "no provider" even though its schemas may still + * linger in OR after `occ app:disable`. The owning app id is taken, in + * order, from the schema's own `application` field (the reliable per-schema + * signal — a register's `application` column is frequently null in practice) + * then the owning register's `application`. Fully null-safe: when the app + * manager is unavailable, no owning app id can be determined, or the id is + * core OR (`openregister`), the schema is treated as available so this check + * never *removes* a provider that no leaf app claimed. Only a concrete owning + * app that is NOT enabled filters the schema out. + * + * @param Schema $schema The candidate provider schema. + * + * @return bool True when the owning app is enabled (or the check does not apply); false only when a named owning app is disabled. + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + * (Requirement: A disabled provider app degrades to no provider) + */ + private function isSchemaProvidedByEnabledApp(Schema $schema): bool + { + if ($this->appManager === null) { + return true; + } + + $appId = $this->owningAppId(schema: $schema); + if ($appId === null || $appId === '' || $appId === 'openregister') { + return true; + } + + try { + return $this->appManager->isEnabledForUser($appId); + } catch (\Throwable $e) { + // Some app entities reject anonymous / user-less contexts; fall back + // to install-state so resolution never raises. + try { + return $this->appManager->isInstalled($appId); + } catch (\Throwable $inner) { + return true; + } + } + + }//end isSchemaProvidedByEnabledApp() + + /** + * Determine the id of the app that owns a schema, or null when none is + * declared. + * + * Prefers the schema's own `application` field (present and reliable on real + * fleet schemas — e.g. `shillinq`), then falls back to the owning register's + * `application`. Returns null when neither names an app, in which case the + * app-enabled gate does not apply. + * + * @param Schema $schema The candidate provider schema. + * + * @return string|null The owning app id, or null when undeclared. + */ + private function owningAppId(Schema $schema): ?string + { + $appId = $schema->getApplication(); + if (is_string($appId) === true && $appId !== '') { + return $appId; + } + + $register = $this->findRegisterForSchema(schema: $schema); + if ($register === null) { + return null; + } + + $registerApp = $register->getApplication(); + if (is_string($registerApp) === true && $registerApp !== '') { + return $registerApp; + } + + return null; + + }//end owningAppId() + + /** + * Clear the request-scoped resolve cache. Test hook. + * + * @return void + * + * @spec openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md + */ + public function clearCache(): void + { + $this->resolveCache = []; + + }//end clearCache() + + /** + * Deterministically pick one schema from >1 candidates for the same URI. + * + * Order: (1) a candidate in the consuming register (when known), else + * (2) the first candidate by slug. Emits a WARN naming the pick so + * ambiguous vocabulary is observable. + * + * @param string $uri The semantic-type URI being resolved (for the log). + * @param array $candidates The >1 matching schemas. + * @param int|null $consumingRegisterId The consuming schema's register id, or null. + * + * @return Schema The chosen schema. + */ + private function tieBreak(string $uri, array $candidates, ?int $consumingRegisterId): Schema + { + // Deterministic base order: by slug ascending. + usort( + $candidates, + fn(Schema $a, Schema $b) => strcmp($this->slugOf(schema: $a), $this->slugOf(schema: $b)) + ); + + $pick = $candidates[0]; + + // Bias to a candidate that lives in the consuming register, when known. + if ($consumingRegisterId !== null) { + $consumingIds = $this->schemaIdsOfRegister(registerId: $consumingRegisterId); + foreach ($candidates as $candidate) { + if (in_array((int) $candidate->getId(), $consumingIds, true) === true) { + $pick = $candidate; + break; + } + } + } + + $this->logger->warning( + message: sprintf( + "[SemanticTypeResolver] %d schemas implement '%s'; picked '%s' (id %s)", + count($candidates), + $uri, + $this->slugOf(schema: $pick), + (string) $pick->getId() + ), + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'uri' => $uri, + 'candidates' => array_map(fn(Schema $s) => $this->slugOf(schema: $s), $candidates), + 'picked' => $this->slugOf(schema: $pick), + 'pickedId' => $pick->getId(), + 'consumingReg' => $consumingRegisterId, + ] + ); + + return $pick; + + }//end tieBreak() + + /** + * Read the schema-id list of a register by id, empty on any failure. + * + * @param int $registerId The register id. + * + * @return array The register's member schema ids (as ints). + */ + private function schemaIdsOfRegister(int $registerId): array + { + try { + $register = $this->registerMapper->find(id: (string) $registerId); + } catch (\Throwable $e) { + return []; + } + + return array_map('intval', $register->getSchemas()); + + }//end schemaIdsOfRegister() + + /** + * Resolve a schema's slug for deterministic ordering, falling back to + * uuid then id so a sort key is always available. + * + * @param Schema $schema The schema. + * + * @return string The slug-like sort key. + */ + private function slugOf(Schema $schema): string + { + $slug = $schema->getSlug(); + if (is_string($slug) === true && $slug !== '') { + return $slug; + } + + $uuid = $schema->getUuid(); + if (is_string($uuid) === true && $uuid !== '') { + return $uuid; + } + + return (string) $schema->getId(); + + }//end slugOf() +}//end class diff --git a/lib/Service/SequenceService.php b/lib/Service/SequenceService.php new file mode 100644 index 0000000000..aae0307061 --- /dev/null +++ b/lib/Service/SequenceService.php @@ -0,0 +1,163 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +use OCA\OpenRegister\Db\Sequence; +use OCA\OpenRegister\Db\SequenceMapper; +use OCP\DB\Exception as DbException; +use OCP\IDBConnection; +use Throwable; + +/** + * Reserves the next running number for a (register, schema, scope) triple. + */ +class SequenceService +{ + /** + * Wire the sequence mapper and DB connection used for the reservation transaction. + * + * @param SequenceMapper $mapper The sequence mapper. + * @param IDBConnection $db DB connection (for the reservation transaction). + * + * @return void + */ + public function __construct( + private readonly SequenceMapper $mapper, + private readonly IDBConnection $db + ) { + }//end __construct() + + /** + * Atomically reserve and return the next number for the given scope. + * + * The returned value is never handed out twice for the same + * (register, schema, scope) triple. Numbering starts at 1. + * + * @param int $registerId The register the sequence is scoped to. + * @param int $schemaId The schema the sequence is scoped to. + * @param string $scopeKey The scope discriminator (e.g. "2026", "2026-06" or "" for global). + * + * @return int The reserved running number (>= 1). + * + * @throws DbException When the reservation cannot be completed. + */ + public function reserveNext(int $registerId, int $schemaId, string $scopeKey): int + { + $this->db->beginTransaction(); + try { + $affected = $this->mapper->incrementScope( + registerId: $registerId, + schemaId: $schemaId, + scopeKey: $scopeKey + ); + + if ($affected === 0) { + // Scope row does not exist yet — seed it reserving value 1 by + // storing next_value = 2. A concurrent seeder may win the + // unique index; on that violation, fall back to the increment + // path so we still reserve a fresh value. + $reserved = $this->seedScope( + registerId: $registerId, + schemaId: $schemaId, + scopeKey: $scopeKey + ); + + $this->db->commit(); + return $reserved; + } + + $row = $this->mapper->findForScope( + registerId: $registerId, + schemaId: $schemaId, + scopeKey: $scopeKey + ); + + $next = (int) ($row?->getNextValue() ?? 1); + $this->db->commit(); + + // The next_value column now points at the NEXT hand-out; the value + // we just reserved is one below it. + return ($next - 1); + } catch (Throwable $e) { + $this->db->rollBack(); + throw $e; + }//end try + }//end reserveNext() + + /** + * Seed a brand-new scope row, reserving value 1. + * + * On a unique-index race (another transaction inserted the row first) the + * insert throws; we then re-run the atomic increment so the caller still + * receives a fresh, unique value. + * + * @param int $registerId The register the sequence is scoped to. + * @param int $schemaId The schema the sequence is scoped to. + * @param string $scopeKey The scope discriminator. + * + * @return int The reserved running number. + * + * @throws DbException When the fallback increment cannot read the row. + */ + private function seedScope(int $registerId, int $schemaId, string $scopeKey): int + { + $entity = new Sequence(); + $entity->setRegisterId($registerId); + $entity->setSchemaId($schemaId); + $entity->setScopeKey($scopeKey); + // Storing 2 means value 1 has just been reserved. + $entity->setNextValue(2); + + try { + $this->mapper->insert($entity); + return 1; + } catch (DbException $e) { + // Lost the insert race — the row now exists; increment it instead. + $this->mapper->incrementScope( + registerId: $registerId, + schemaId: $schemaId, + scopeKey: $scopeKey + ); + $row = $this->mapper->findForScope( + registerId: $registerId, + schemaId: $schemaId, + scopeKey: $scopeKey + ); + $next = (int) ($row?->getNextValue() ?? 1); + return ($next - 1); + }//end try + }//end seedScope() +}//end class diff --git a/lib/Service/Serializer/RegisterSerializer.php b/lib/Service/Serializer/RegisterSerializer.php new file mode 100644 index 0000000000..f61e70c6da --- /dev/null +++ b/lib/Service/Serializer/RegisterSerializer.php @@ -0,0 +1,301 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/specs/register-service-extensions/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Serializer; + +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\Schema; +use OCA\OpenRegister\Db\SchemaMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use Psr\Log\LoggerInterface; + +/** + * Serialize Register entities with optional `_extend` post-processing. + * + * Supported `_extend` keys: + * - `schemas` — replace schema IDs with hydrated schema objects. + * - `@self.stats` — attach per-schema `stats.objects.total` counts + * (only meaningful alongside `schemas`). + * + * Unknown keys are silently ignored. Orphan schema IDs (schema not + * found in the DB) are retained in their original array position; the + * serializer logs a warning and does not throw. + * + * @spec openspec/specs/register-service-extensions/spec.md + */ +final class RegisterSerializer +{ + + /** + * Schemas already fetched this request, keyed by identifier. + * + * @var array + */ + private array $schemaCache = []; + + /** + * Wire mappers + logger via constructor DI. + * + * @param SchemaMapper $schemaMapper Schema lookup mapper. + * @param LoggerInterface $logger Logger for orphan-ID warnings. + */ + public function __construct( + private readonly SchemaMapper $schemaMapper, + private readonly LoggerInterface $logger, + ) { + + }//end __construct() + + /** + * Serialize a single Register entity with optional extensions. + * + * @param Register $register The Register entity to serialize. + * @param array $extend Extension keys to apply (`schemas`, `@self.stats`). + * @param array|null $schemaStats Pre-computed per-schema object counts (id → ['total' => int]). + * + * @return array Serialized register payload, with extensions applied. + * + * @spec openspec/specs/register-service-extensions/spec.md + * (Requirement: schemas extension SHALL replace schema IDs with full schema objects) + */ + public function serialize(Register $register, array $extend=[], ?array $schemaStats=null): array + { + $data = $register->jsonSerialize(); + $wantSchemas = in_array('schemas', $extend, true); + $wantStats = in_array('@self.stats', $extend, true); + + if ($wantSchemas === true) { + $data['schemas'] = $this->expandSchemas(ids: ($data['schemas'] ?? []), attachStats: $wantStats, schemaStats: $schemaStats); + } + + return $data; + + }//end serialize() + + /** + * Serialize a collection of Register entities. + * + * @param Register[] $registers Registers to serialize. + * @param array $extend Extension keys to apply. + * @param array>|null $schemaStatsByRegisterId Pre-computed per-register stats + * (registerId → schemaId → + * ['total' => int]). + * + * @return array Serialized payload for each register. + * + * @spec openspec/specs/register-service-extensions/spec.md + */ + public function serializeMany( + array $registers, + array $extend=[], + ?array $schemaStatsByRegisterId=null, + ): array { + // PERF (N+1): expandSchemas() resolves each schema id through + // findSchemaCached() → SchemaMapper::find(), i.e. ONE query per schema. + // With `_extend[]=schemas` over every register that is one query per + // register-schema pair (~1,200 on the dev instance) on a single page + // load. Warm the cache with one batched lookup up front; the per-id path + // below then only runs for ids the batch could not resolve. + $this->warmSchemaCache(registers: $registers, extend: $extend); + + $out = []; + foreach ($registers as $register) { + $registerId = (int) $register->getId(); + $stats = null; + if ($schemaStatsByRegisterId !== null + && isset($schemaStatsByRegisterId[$registerId]) === true + ) { + $stats = $schemaStatsByRegisterId[$registerId]; + } + + $out[] = $this->serialize(register: $register, extend: $extend, schemaStats: $stats); + } + + return $out; + + }//end serializeMany() + + /** + * Expand a `schemas` ID array to schema objects, optionally annotating with stats. + * + * Orphan IDs (SchemaMapper::find throws DoesNotExistException) are + * retained in their original position with their original PHP type + * preserved. The serializer logs a warning for each orphan. + * + * @param array $ids Schema ID array (int|string). + * @param bool $attachStats Whether to attach per-schema stats to expanded entries. + * @param array|null $schemaStats Pre-computed per-schema stats (id → ['total' + * => int]). + * + * @return array Heterogeneous array of schema objects + retained orphan IDs. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Handles three branches per ID (resolved/orphan/stats-on) + * which produce the smallest combined surface; splitting them duplicates the loop scaffold. + */ + private function expandSchemas(array $ids, bool $attachStats, ?array $schemaStats): array + { + $expanded = []; + foreach ($ids as $schemaId) { + try { + // Match the original controller's call shape: bypass + // multitenancy when expanding schemas for registers (a + // register may legitimately reference a schema that is + // not directly visible in the caller's tenant — the + // expansion is read-only metadata). + $schema = $this->findSchemaCached(schemaId: $schemaId); + $schemaJson = $schema->jsonSerialize(); + } catch (DoesNotExistException $e) { + // Preserve the orphan ID at its original position + + // original type so typed JSON clients can still see it + // even if they have to switch decoder shape on the + // edge case. Log warning and continue. + $this->logger->warning( + message: '[RegisterSerializer] Schema not found for expansion — retaining orphan ID', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'schemaId' => $schemaId, + ] + ); + $expanded[] = $schemaId; + continue; + }//end try + + if ($attachStats === true) { + $idForLookup = $schemaJson['id'] ?? null; + $count = 0; + if ($idForLookup !== null + && $schemaStats !== null + && isset($schemaStats[$idForLookup]) === true + && isset($schemaStats[$idForLookup]['total']) === true + ) { + $count = (int) $schemaStats[$idForLookup]['total']; + } + + $schemaJson['stats'] = [ + 'objects' => ['total' => $count], + ]; + } + + $expanded[] = $schemaJson; + }//end foreach + + return $expanded; + + }//end expandSchemas() + + /** + * Find a Schema, reusing the entity within this request. + * + * Registers share schemas, and a list response expands the schemas of EVERY register + * — so the same schema was re-fetched once per register that references it, and + * SchemaMapper::find() resolves an identifier with `WHERE uuid = ? OR slug = ? OR + * id = ?`, which no index covers. On a dev instance with 76 registers / 1,231 schemas + * this was the single hottest query in the whole request. + * + * Schemas do not change mid-request, so hold on to them. A miss still throws + * DoesNotExistException, keeping the orphan-ID retention contract in expandSchemas() + * exactly as it was. + * + * @param int|string $schemaId The schema identifier. + * + * @return Schema The schema entity. + * + * @throws DoesNotExistException When no schema matches the identifier. + * + * @spec exclude request-scoped identity cache; no behaviour change + */ + private function findSchemaCached(int|string $schemaId): Schema + { + $key = (string) $schemaId; + + if (isset($this->schemaCache[$key]) === false) { + $this->schemaCache[$key] = $this->schemaMapper->find(id: $schemaId, _multitenancy: false); + } + + return $this->schemaCache[$key]; + }//end findSchemaCached() + + /** + * Resolve every register's schemas in ONE query and seed the request cache. + * + * Only NUMERIC ids are batched: findMultipleOptimized() matches on `id`, + * whereas find() also resolves a uuid or a slug. Anything non-numeric (and + * anything the batch does not return — e.g. an orphan id) is deliberately + * left out of the cache so findSchemaCached() still falls back to find(), + * preserving both the uuid/slug lookup and the DoesNotExistException that + * expandSchemas() relies on to retain orphan ids. + * + * Matches find(_multitenancy: false) semantics: findMultipleOptimized() + * applies no organisation filter, and find()'s `_rbac` branch is currently a + * no-op, so this is a pure query-count optimisation. + * + * @param Register[] $registers The registers about to be serialized. + * @param array $extend Extension keys; schemas are only resolved when requested. + * + * @return void + * + * @spec exclude batched warm-up of the request-scoped cache; no behaviour change + */ + private function warmSchemaCache(array $registers, array $extend): void + { + if (in_array('schemas', $extend, true) === false) { + return; + } + + $ids = []; + foreach ($registers as $register) { + foreach (($register->getSchemas() ?? []) as $schemaId) { + if (is_numeric($schemaId) === false) { + continue; + } + + if (isset($this->schemaCache[(string) $schemaId]) === true) { + continue; + } + + $ids[(int) $schemaId] = true; + } + } + + if (empty($ids) === true) { + return; + } + + foreach ($this->schemaMapper->findMultipleOptimized(ids: array_keys($ids)) as $id => $schema) { + $this->schemaCache[(string) $id] = $schema; + } + }//end warmSchemaCache() +}//end class diff --git a/lib/Service/Settings/ConfigurationSettingsHandler.php b/lib/Service/Settings/ConfigurationSettingsHandler.php index 46f55d5b15..8c673f568a 100644 --- a/lib/Service/Settings/ConfigurationSettingsHandler.php +++ b/lib/Service/Settings/ConfigurationSettingsHandler.php @@ -135,6 +135,7 @@ public function __construct( * @return bool True if multi-tenancy is enabled, false otherwise * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-3 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function isMultiTenancyEnabled(): bool { @@ -618,58 +619,6 @@ public function updateSettings(array $data): array }//end try }//end updateSettings() - /** - * Update the publishing options configuration. - * - * @param array $options The publishing options data to update. - * - * @return bool[] The updated publishing options configuration. - * - * @throws \RuntimeException If publishing options update fails. - * - * @psalm-return array{ - * use_old_style_publishing_view?: bool, - * auto_publish_objects?: bool, - * auto_publish_attachments?: bool - * } - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 - */ - public function updatePublishingOptions(array $options): array - { - try { - // Define valid publishing option keys for security. - $validOptions = [ - 'auto_publish_attachments', - 'auto_publish_objects', - 'use_old_style_publishing_view', - ]; - - $updatedOptions = []; - - // Update each publishing option in the configuration. - foreach ($validOptions as $option) { - // Check if this option is provided in the input data. - if (isset($options[$option]) === true) { - // Convert boolean or string to string format for storage. - $value = 'false'; - if ($options[$option] === true || $options[$option] === 'true') { - $value = 'true'; - } - - // Store the value in the configuration. - $this->appConfig->setValueString($this->appName, $option, $value); - // Retrieve and convert back to boolean for the response. - $updatedOptions[$option] = $this->appConfig->getValueString($this->appName, $option, '') === 'true'; - } - } - - return $updatedOptions; - } catch (Exception $e) { - throw new RuntimeException('Failed to update publishing options: '.$e->getMessage()); - }//end try - }//end updatePublishingOptions() - /** * Get focused RBAC settings only * @@ -684,6 +633,7 @@ public function updatePublishingOptions(array $options): array * availableUsers: array} * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getRbacSettingsOnly(): array { @@ -738,6 +688,7 @@ public function getRbacSettingsOnly(): array * availableUsers: array} * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function updateRbacSettingsOnly(array $rbacData): array { @@ -775,6 +726,7 @@ public function updateRbacSettingsOnly(array $rbacData): array * }} * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getOrganisationSettingsOnly(): array { @@ -820,6 +772,7 @@ public function getOrganisationSettingsOnly(): array * }} * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function updateOrganisationSettingsOnly(array $organisationData): array { @@ -845,6 +798,7 @@ public function updateOrganisationSettingsOnly(array $organisationData): array * @return string|null Default organisation UUID or null if not set * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-3 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getDefaultOrganisationUuid(): ?string { @@ -866,6 +820,7 @@ public function getDefaultOrganisationUuid(): ?string * @return string|null Tenant ID (default user tenant) or null if not set * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-3 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getTenantId(): ?string { @@ -885,6 +840,8 @@ public function getTenantId(): ?string * Get organisation ID (alias for getDefaultOrganisationUuid) * * @return string|null Organisation ID or null if not set + * + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getOrganisationId(): ?string { @@ -899,6 +856,7 @@ public function getOrganisationId(): ?string * @return void * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-3 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function setDefaultOrganisationUuid(?string $uuid): void { @@ -931,6 +889,7 @@ public function setDefaultOrganisationUuid(?string $uuid): void * adminOverride: mixed|true}, availableTenants: array} * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getMultitenancySettingsOnly(): array { @@ -979,6 +938,7 @@ public function getMultitenancySettingsOnly(): array * @return array Updated multitenancy config with settings and available tenants. * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function updateMultitenancySettingsOnly(array $multitenancyData): array { @@ -1017,6 +977,7 @@ public function updateMultitenancySettingsOnly(array $multitenancyData): array * Nested else branches handle optional vector config backward compatibility * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getLLMSettingsOnly(): array { @@ -1100,6 +1061,7 @@ public function getLLMSettingsOnly(): array * @SuppressWarnings(PHPMD.NPathComplexity) PATCH behavior requires merging multiple nested configuration structures * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function updateLLMSettingsOnly(array $llmData): array { @@ -1163,6 +1125,7 @@ public function updateLLMSettingsOnly(array $llmData): array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive file settings require many default configuration values * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getFileSettingsOnly(): array { @@ -1240,6 +1203,7 @@ public function getFileSettingsOnly(): array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive file settings require many configuration fields * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function updateFileSettingsOnly(array $fileData): array { @@ -1300,6 +1264,7 @@ public function updateFileSettingsOnly(array $fileData): array * @throws \RuntimeException If n8n settings retrieval fails. * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getN8nSettingsOnly(): array { @@ -1336,6 +1301,7 @@ public function getN8nSettingsOnly(): array * @psalm-return array{enabled: false|mixed, url: ''|mixed, apiKey: ''|mixed, project: 'openregister'|mixed} * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-1 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function updateN8nSettingsOnly(array $n8nData): array { @@ -1362,22 +1328,24 @@ public function updateN8nSettingsOnly(array $n8nData): array * @return array Version info with name, version, description, author, licence, timestamp, and date. * * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-2 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 */ public function getVersionInfoOnly(): array { try { - $appInfo = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppInfo($this->appName); + $appManager = \OCP\Server::get(\OCP\App\IAppManager::class); + $appInfo = $appManager->getAppInfo($this->appName); return [ - 'version' => $appInfo['version'] ?? 'unknown', - 'name' => $appInfo['name'] ?? 'OpenRegister', - 'description' => $appInfo['description'] ?? '', - 'author' => $appInfo['author'] ?? 'Conduction', - 'licence' => $appInfo['licence'] ?? 'AGPL', + 'version' => ($appInfo['version'] ?? null) ?? 'unknown', + 'name' => ($appInfo['name'] ?? null) ?? 'OpenRegister', + 'description' => ($appInfo['description'] ?? null) ?? '', + 'author' => ($appInfo['author'] ?? null) ?? 'Conduction', + 'licence' => ($appInfo['licence'] ?? null) ?? 'AGPL', 'timestamp' => time(), 'date' => date('Y-m-d H:i:s'), ]; - } catch (Exception $e) { + } catch (\Throwable $e) { return [ 'version' => 'unknown', 'error' => 'Failed to retrieve version info: '.$e->getMessage(), diff --git a/lib/Service/Settings/FileSettingsHandler.php b/lib/Service/Settings/FileSettingsHandler.php index 57abe9b94e..eaffa740d8 100644 --- a/lib/Service/Settings/FileSettingsHandler.php +++ b/lib/Service/Settings/FileSettingsHandler.php @@ -128,9 +128,13 @@ public function getFileSettingsOnly(): array // Presidio entity recognition settings. 'presidioApiEndpoint' => '', 'openAnonymiserApiEndpoint' => '', + // OpenAnonymiser source: 'internal' (AppAPI ExApp) or 'external' (operator URL). + 'openAnonymiserSource' => 'internal', 'entityRecognitionEnabled' => false, - 'entityRecognitionMethod' => 'hybrid', - // Regex, presidio, openanonymiser, llm, hybrid. + // 'auto' is an internal "not yet configured" marker resolved at state-query + // time to the recommended backend; it is never shown as a UI option. + 'entityRecognitionMethod' => 'auto', + // Auto (unconfigured marker), regex, presidio, openanonymiser, llm, hybrid. ]; }//end if @@ -199,9 +203,10 @@ public function updateFileSettingsOnly(array $fileData): array // Presidio entity recognition settings. 'presidioApiEndpoint' => $fileData['presidioApiEndpoint'] ?? '', 'openAnonymiserApiEndpoint' => $fileData['openAnonymiserApiEndpoint'] ?? '', + 'openAnonymiserSource' => $fileData['openAnonymiserSource'] ?? 'internal', 'entityRecognitionEnabled' => $fileData['entityRecognitionEnabled'] ?? false, - 'entityRecognitionMethod' => $fileData['entityRecognitionMethod'] ?? 'hybrid', - // Regex, presidio, openanonymiser, llm, hybrid. + 'entityRecognitionMethod' => $fileData['entityRecognitionMethod'] ?? 'auto', + // Auto (unconfigured marker), regex, presidio, openanonymiser, llm, hybrid. ]; $this->appConfig->setValueString($this->appName, 'fileManagement', json_encode($fileConfig)); diff --git a/lib/Service/Settings/ObjectRetentionHandler.php b/lib/Service/Settings/ObjectRetentionHandler.php index 66c6c80d82..5638c3f4ce 100644 --- a/lib/Service/Settings/ObjectRetentionHandler.php +++ b/lib/Service/Settings/ObjectRetentionHandler.php @@ -17,7 +17,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/retention-management/spec.md#requirement-retention-settings-must-be-configurable-via-api */ namespace OCA\OpenRegister\Service\Settings; @@ -82,7 +82,7 @@ public function __construct(IAppConfig $appConfig, string $appName="openregister * includeMetadata: mixed|true, includeRelations: mixed|true, * maxNestingDepth: 10|mixed, batchSize: 25|mixed, autoRetry: mixed|true} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/retention-management/spec.md#requirement-retention-settings-must-be-configurable-via-api */ public function getObjectSettingsOnly(): array { @@ -137,7 +137,7 @@ public function getObjectSettingsOnly(): array * includeMetadata: mixed|true, includeRelations: mixed|true, * maxNestingDepth: 10|mixed, batchSize: 25|mixed, autoRetry: mixed|true} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/retention-management/spec.md#requirement-retention-settings-must-be-configurable-via-api */ public function updateObjectSettingsOnly(array $objectData): array { @@ -177,7 +177,7 @@ public function updateObjectSettingsOnly(array $objectData): array * deleteLogRetention: 2592000000|mixed, auditTrailsEnabled: bool, * searchTrailsEnabled: bool} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/retention-management/spec.md#requirement-retention-settings-must-be-configurable-via-api */ public function getRetentionSettingsOnly(): array { @@ -186,38 +186,41 @@ public function getRetentionSettingsOnly(): array if (empty($retentionConfig) === true) { return [ - 'objectArchiveRetention' => 31536000000, + 'objectArchiveRetention' => 31536000000, // 1 year default - 'objectDeleteRetention' => 63072000000, + 'objectDeleteRetention' => 63072000000, // 2 years default - 'searchTrailRetention' => 2592000000, + 'searchTrailRetention' => 2592000000, // 1 month default - 'createLogRetention' => 2592000000, + 'createLogRetention' => 2592000000, // 1 month default - 'readLogRetention' => 86400000, + 'readLogRetention' => 86400000, // 24 hours default - 'updateLogRetention' => 604800000, + 'updateLogRetention' => 604800000, // 1 week default - 'deleteLogRetention' => 2592000000, + 'deleteLogRetention' => 2592000000, // 1 month default - 'auditTrailsEnabled' => true, + 'auditTrailsEnabled' => true, // Audit trails enabled by default. - 'searchTrailsEnabled' => true, + 'searchTrailsEnabled' => true, // Search trails enabled by default. + 'searchTrailRecordingMode' => '_search', + // Which searches to record: all | _search | none. Default: text searches only. ]; }//end if $retentionData = json_decode($retentionConfig, true); return [ - 'objectArchiveRetention' => $retentionData['objectArchiveRetention'] ?? 31536000000, - 'objectDeleteRetention' => $retentionData['objectDeleteRetention'] ?? 63072000000, - 'searchTrailRetention' => $retentionData['searchTrailRetention'] ?? 2592000000, - 'createLogRetention' => $retentionData['createLogRetention'] ?? 2592000000, - 'readLogRetention' => $retentionData['readLogRetention'] ?? 86400000, - 'updateLogRetention' => $retentionData['updateLogRetention'] ?? 604800000, - 'deleteLogRetention' => $retentionData['deleteLogRetention'] ?? 2592000000, - 'auditTrailsEnabled' => $this->convertToBoolean(value: $retentionData['auditTrailsEnabled'] ?? true), - 'searchTrailsEnabled' => $this->convertToBoolean(value: $retentionData['searchTrailsEnabled'] ?? true), + 'objectArchiveRetention' => $retentionData['objectArchiveRetention'] ?? 31536000000, + 'objectDeleteRetention' => $retentionData['objectDeleteRetention'] ?? 63072000000, + 'searchTrailRetention' => $retentionData['searchTrailRetention'] ?? 2592000000, + 'createLogRetention' => $retentionData['createLogRetention'] ?? 2592000000, + 'readLogRetention' => $retentionData['readLogRetention'] ?? 86400000, + 'updateLogRetention' => $retentionData['updateLogRetention'] ?? 604800000, + 'deleteLogRetention' => $retentionData['deleteLogRetention'] ?? 2592000000, + 'auditTrailsEnabled' => $this->convertToBoolean(value: $retentionData['auditTrailsEnabled'] ?? true), + 'searchTrailsEnabled' => $this->convertToBoolean(value: $retentionData['searchTrailsEnabled'] ?? true), + 'searchTrailRecordingMode' => $this->normaliseRecordingMode(value: $retentionData['searchTrailRecordingMode'] ?? '_search'), ]; } catch (Exception $e) { throw new RuntimeException('Failed to retrieve Retention settings: '.$e->getMessage()); @@ -241,21 +244,22 @@ public function getRetentionSettingsOnly(): array * deleteLogRetention: 2592000000|mixed, auditTrailsEnabled: mixed|true, * searchTrailsEnabled: mixed|true} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/retention-management/spec.md#requirement-retention-settings-must-be-configurable-via-api */ public function updateRetentionSettingsOnly(array $retentionData): array { try { $retentionConfig = [ - 'objectArchiveRetention' => $retentionData['objectArchiveRetention'] ?? 31536000000, - 'objectDeleteRetention' => $retentionData['objectDeleteRetention'] ?? 63072000000, - 'searchTrailRetention' => $retentionData['searchTrailRetention'] ?? 2592000000, - 'createLogRetention' => $retentionData['createLogRetention'] ?? 2592000000, - 'readLogRetention' => $retentionData['readLogRetention'] ?? 86400000, - 'updateLogRetention' => $retentionData['updateLogRetention'] ?? 604800000, - 'deleteLogRetention' => $retentionData['deleteLogRetention'] ?? 2592000000, - 'auditTrailsEnabled' => $retentionData['auditTrailsEnabled'] ?? true, - 'searchTrailsEnabled' => $retentionData['searchTrailsEnabled'] ?? true, + 'objectArchiveRetention' => $retentionData['objectArchiveRetention'] ?? 31536000000, + 'objectDeleteRetention' => $retentionData['objectDeleteRetention'] ?? 63072000000, + 'searchTrailRetention' => $retentionData['searchTrailRetention'] ?? 2592000000, + 'createLogRetention' => $retentionData['createLogRetention'] ?? 2592000000, + 'readLogRetention' => $retentionData['readLogRetention'] ?? 86400000, + 'updateLogRetention' => $retentionData['updateLogRetention'] ?? 604800000, + 'deleteLogRetention' => $retentionData['deleteLogRetention'] ?? 2592000000, + 'auditTrailsEnabled' => $retentionData['auditTrailsEnabled'] ?? true, + 'searchTrailsEnabled' => $retentionData['searchTrailsEnabled'] ?? true, + 'searchTrailRecordingMode' => $this->normaliseRecordingMode(value: $retentionData['searchTrailRecordingMode'] ?? '_search'), ]; $this->appConfig->setValueString($this->appName, 'retention', json_encode($retentionConfig)); @@ -272,7 +276,7 @@ public function updateRetentionSettingsOnly(array $retentionData): array * * @throws \RuntimeException If archival settings retrieval fails * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/retention-management/spec.md#requirement-retention-settings-must-be-configurable-via-api */ public function getArchivalSettingsOnly(): array { @@ -309,7 +313,7 @@ public function getArchivalSettingsOnly(): array * * @throws \RuntimeException If archival settings update fails * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/retention-management/spec.md#requirement-retention-settings-must-be-configurable-via-api */ public function updateArchivalSettingsOnly(array $archivalData): array { @@ -338,7 +342,7 @@ public function updateArchivalSettingsOnly(array $archivalData): array * * @return array Default archival configuration * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-64 + * @spec openspec/specs/retention-management/spec.md#requirement-retention-settings-must-be-configurable-via-api */ private function getArchivalDefaults(): array { @@ -364,7 +368,7 @@ private function getArchivalDefaults(): array * * @psalm-return array{appName: 'Open Register', appVersion: '0.2.3'} * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-3 + * @spec openspec/specs/retention-management/spec.md */ public function getVersionInfoOnly(): array { @@ -385,7 +389,7 @@ public function getVersionInfoOnly(): array * * @return bool The boolean representation * - * @spec openspec/changes/retrofit-2026-05-24-retention-management/tasks.md#task-3 + * @spec openspec/specs/retention-management/spec.md */ private function convertToBoolean($value): bool { @@ -403,4 +407,30 @@ private function convertToBoolean($value): bool return (bool) $value; }//end convertToBoolean() + + /** + * Normalise the search-trail recording mode to an allowed value. + * + * Accepts only 'all', '_search', or 'none'; any other input falls back + * to the safe default '_search' (record text searches only). + * + * @param mixed $value The raw recording-mode value. + * + * @return string One of 'all', '_search', 'none'. + * + * @spec openspec/specs/search-trail-recording/spec.md + */ + private function normaliseRecordingMode($value): string + { + $mode = ''; + if (is_string($value) === true) { + $mode = strtolower($value); + } + + if (in_array($mode, ['all', '_search', 'none'], true) === true) { + return $mode; + } + + return '_search'; + }//end normaliseRecordingMode() }//end class diff --git a/lib/Service/Settings/SearchBackendHandler.php b/lib/Service/Settings/SearchBackendHandler.php index 5e0fbbe0d4..df91126094 100644 --- a/lib/Service/Settings/SearchBackendHandler.php +++ b/lib/Service/Settings/SearchBackendHandler.php @@ -19,13 +19,11 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-88 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-backend-agnostic-search-architecture */ namespace OCA\OpenRegister\Service\Settings; -use Exception; -use RuntimeException; use InvalidArgumentException; use OCP\IAppConfig; use Psr\Log\LoggerInterface; @@ -33,8 +31,9 @@ /** * Handler for search backend settings operations. * - * This handler is responsible for managing which search backend is active - * (Solr, Elasticsearch, etc.) and providing configuration for the active backend. + * This handler reports the active search backend. Since the external Solr and + * Elasticsearch backends were removed, the built-in database (Magic-Tables) + * search is the sole backend. * * @category Service * @package OCA\OpenRegister\Service\Settings @@ -93,80 +92,66 @@ public function __construct( /** * Get search backend configuration. * - * Returns which search backend is currently active (solr, elasticsearch, etc). + * The external Solr/Elasticsearch backends were removed; the built-in + * database (Magic-Tables) search is the only backend. * * @return array Backend configuration with 'active' key. * * @throws \RuntimeException If backend configuration retrieval fails. * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-88 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-backend-agnostic-search-architecture */ public function getSearchBackendConfig(): array { - try { - $backendConfig = $this->appConfig->getValueString($this->appName, 'search_backend', ''); - - if (empty($backendConfig) === true) { - return [ - 'active' => 'solr', - // Default to Solr for backward compatibility. - 'available' => ['solr', 'elasticsearch'], - ]; - } - - return json_decode($backendConfig, true); - } catch (Exception $e) { - throw new RuntimeException('Failed to retrieve search backend configuration: '.$e->getMessage()); - } + return [ + 'active' => 'database', + 'available' => ['database'], + ]; }//end getSearchBackendConfig() /** * Update search backend configuration. * - * Sets which search backend should be active. + * Retained for API compatibility. The database backend is the only option, + * so any backend other than 'database' is rejected. * - * @param string $backend Backend name ('solr', 'elasticsearch', etc). + * @param string $backend Backend name (only 'database' is valid). * * @return (int|string[])[] Updated backend configuration. * - * @throws \RuntimeException If backend configuration update fails. + * @throws \InvalidArgumentException If a non-database backend is requested. * - * @psalm-return array{active: 'elasticsearch'|'solr', available: list{'solr', 'elasticsearch'}, updated: int<1, max>} + * @psalm-return array{active: 'database', available: list{'database'}, updated: int<1, max>} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-88 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-backend-agnostic-search-architecture */ public function updateSearchBackendConfig(string $backend): array { - try { - $availableBackends = ['solr', 'elasticsearch']; + $availableBackends = ['database']; - if (in_array($backend, $availableBackends, true) === false) { - throw new InvalidArgumentException( - "Invalid backend '$backend'. Must be one of: ".implode(', ', $availableBackends) - ); - } + if (in_array($backend, $availableBackends, true) === false) { + throw new InvalidArgumentException( + "Invalid backend '$backend'. Must be one of: ".implode(', ', $availableBackends) + ); + } - $backendConfig = [ - 'active' => $backend, - 'available' => $availableBackends, - 'updated' => time(), - ]; + $backendConfig = [ + 'active' => 'database', + 'available' => $availableBackends, + 'updated' => time(), + ]; - $this->appConfig->setValueString($this->appName, 'search_backend', json_encode($backendConfig)); + $this->appConfig->setValueString($this->appName, 'search_backend', json_encode($backendConfig)); - $this->logger->info( - message: '[SearchBackendHandler] Search backend changed to: '.$backend, - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'app' => 'openregister', - 'backend' => $backend, - ] - ); + $this->logger->info( + message: '[SearchBackendHandler] Search backend set to: database', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'app' => 'openregister', + ] + ); - return $backendConfig; - } catch (Exception $e) { - throw new RuntimeException('Failed to update search backend configuration: '.$e->getMessage()); - }//end try + return $backendConfig; }//end updateSearchBackendConfig() }//end class diff --git a/lib/Service/Settings/SolrSettingsHandler.php b/lib/Service/Settings/SolrSettingsHandler.php deleted file mode 100644 index e19157fdf9..0000000000 --- a/lib/Service/Settings/SolrSettingsHandler.php +++ /dev/null @@ -1,803 +0,0 @@ - - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.nl - */ - -namespace OCA\OpenRegister\Service\Settings; - -use Exception; -use RuntimeException; -use InvalidArgumentException; -use OCP\IAppConfig; -use OCP\AppFramework\IAppContainer; -use OCA\OpenRegister\Service\Object\CacheHandler; -use Psr\Log\LoggerInterface; - -/** - * Handler for SOLR settings and operations. - * - * This handler is responsible for managing SOLR configuration, - * dashboard statistics, and facet configuration. - * - * @category Service - * @package OCA\OpenRegister\Service\Settings - * - * @author Conduction Development Team - * @copyright 2024 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * @version GIT: - * - * @link https://www.OpenRegister.nl - */ -class SolrSettingsHandler -{ - - /** - * Configuration service - * - * @var IAppConfig - */ - private IAppConfig $appConfig; - - /** - * Object cache service (lazy-loaded when needed) - * - * @var CacheHandler|null - */ - private ?CacheHandler $objectCacheService = null; - - /** - * Container for lazy loading services - * - * @var IAppContainer|null - */ - private ?IAppContainer $container = null; - - /** - * Application name - * - * @var string - */ - private string $appName; - - /** - * Logger instance - * - * @var LoggerInterface|null - */ - private ?LoggerInterface $logger = null; - - /** - * Constructor for SolrSettingsHandler - * - * @param IAppConfig $appConfig Configuration service. - * @param CacheHandler|null $objectCacheService Object cache service (optional, lazy-loaded). - * @param IAppContainer|null $container Container for lazy loading (optional). - * @param LoggerInterface $logger Logger for logging operations. - * @param string $appName Application name. - * - * @return void - */ - public function __construct( - IAppConfig $appConfig, - ?CacheHandler $objectCacheService=null, - ?IAppContainer $container=null, - ?LoggerInterface $logger=null, - string $appName='openregister' - ) { - $this->appConfig = $appConfig; - $this->objectCacheService = $objectCacheService; - $this->container = $container; - $this->logger = $logger; - $this->appName = $appName; - }//end __construct() - - /** - * Get SOLR configuration settings - * - * @return array SOLR configuration array - * - * @throws \RuntimeException if SOLR settings retrieval fails - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) SOLR configuration requires many default settings - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 - */ - public function getSolrSettings(): array - { - try { - $solrConfig = $this->appConfig->getValueString($this->appName, 'solr', ''); - if (empty($solrConfig) === true) { - return [ - 'enabled' => false, - 'host' => 'solr', - 'port' => 8983, - 'path' => '/solr', - 'core' => 'openregister', - 'configSet' => '_default', - 'scheme' => 'http', - 'username' => '', - 'password' => '', - 'timeout' => 30, - 'autoCommit' => true, - 'commitWithin' => 1000, - 'enableLogging' => true, - 'zookeeperHosts' => 'zookeeper:2181', - 'collection' => 'openregister', - 'useCloud' => true, - ]; - } - - return json_decode($solrConfig, true); - } catch (Exception $e) { - throw new RuntimeException('Failed to retrieve SOLR settings: '.$e->getMessage()); - }//end try - }//end getSolrSettings() - - /** - * Complete SOLR warmup: mirror schemas and index objects from the database - * - * This method performs comprehensive SOLR index warmup by: - * 1. Mirroring all OpenRegister schemas to SOLR for proper field typing - * 2. Bulk indexing objects from the database using schema-aware mapping - * 3. Performing cache warmup queries - * 4. Committing and optimizing the index - * - * @param int $batchSize Number of objects to process per batch (default 1000, parameter kept for API compatibility) - * @param int $maxObjects Maximum number of objects to index (0 = all) - * - * @return array Warmup operation results with statistics and status - * - * @throws \RuntimeException If SOLR warmup fails - */ - - /** - * Complete search index warmup: mirror schemas and index objects from the database - * - * @return never Warmup operation results with statistics and status - * - * @throws \RuntimeException Always throws exception indicating method is deprecated - * - * @deprecated This method is deprecated. Use IndexService->warmupIndex() directly via controller. - * This method is kept for backward compatibility but should not be used. - * The controller now uses IndexService directly to avoid circular dependencies. - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-4 - */ - public function warmupSolrIndex() - { - // NOTE: This method is deprecated. Use IndexService->warmupIndex() directly via controller. - // This method is kept for backward compatibility but should not be used. - // The controller now uses IndexService directly to avoid circular dependencies. - throw new RuntimeException( - 'SettingsService::warmupSolrIndex() is deprecated. Use IndexService->warmupIndex() directly via controller.' - ); - }//end warmupSolrIndex() - - /** - * Get comprehensive SOLR dashboard statistics - * - * Provides detailed metrics for the SOLR Search Management dashboard - * including core statistics, performance metrics, and health indicators. - * - * @return array Dashboard stats with overview, cores, performance, health, and operations. - * - * @throws \RuntimeException If SOLR statistics retrieval fails. - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive dashboard requires complete statistics structure - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-4 - */ - public function getSolrDashboardStats(): array - { - try { - $objectCacheService = $this->objectCacheService; - if ($objectCacheService === null && $this->container !== null) { - try { - $objectCacheService = $this->container->get(CacheHandler::class); - } catch (Exception $e) { - throw new Exception('CacheHandler not available'); - } - } - - if ($objectCacheService === null) { - throw new Exception('CacheHandler not available'); - } - - $rawStats = $objectCacheService->getSolrDashboardStats(); - - // Transform the raw stats into the expected dashboard structure. - return $this->transformSolrStatsToDashboard(rawStats: $rawStats); - } catch (Exception $e) { - // Return default dashboard structure if SOLR is not available. - return [ - 'overview' => [ - 'available' => false, - 'connection_status' => 'unavailable', - 'response_time_ms' => 0, - 'total_documents' => 0, - 'index_size' => '0 B', - 'last_commit' => null, - ], - 'cores' => [ - 'active_core' => 'unknown', - 'core_status' => 'inactive', - 'endpoint_url' => 'N/A', - ], - 'performance' => [ - 'total_searches' => 0, - 'total_indexes' => 0, - 'total_deletes' => 0, - 'avg_search_time_ms' => 0, - 'avg_index_time_ms' => 0, - 'total_search_time' => 0, - 'total_index_time' => 0, - 'operations_per_sec' => 0, - 'error_rate' => 0, - ], - 'health' => [ - 'status' => 'unavailable', - 'uptime' => 'N/A', - 'memory_usage' => ['used' => 'N/A', 'max' => 'N/A', 'percentage' => 0], - 'disk_usage' => ['used' => 'N/A', 'available' => 'N/A', 'percentage' => 0], - 'warnings' => ['SOLR service is not available or not configured'], - 'last_optimization' => null, - ], - 'operations' => [ - 'recent_activity' => [], - 'queue_status' => ['pending_operations' => 0, 'processing' => false, 'last_processed' => null], - 'commit_frequency' => ['auto_commit' => false, 'commit_within' => 0, 'last_commit' => null], - 'optimization_needed' => false, - ], - 'generated_at' => date('c'), - 'error' => $e->getMessage(), - ]; - }//end try - }//end getSolrDashboardStats() - - /** - * Transform raw SOLR stats into dashboard structure - * - * @param array $rawStats Raw statistics from SOLR service - * - * @return array Dashboard structure with overview, cores, performance, health, and operations. - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * Dashboard transformation requires comprehensive data mapping - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * Multiple metric calculations require conditional handling - * @SuppressWarnings(PHPMD.NPathComplexity) - * Statistics calculations depend on multiple optional values - * Multiple conditional branches for calculating performance metrics require else clauses - */ - private function transformSolrStatsToDashboard(array $rawStats): array - { - // If SOLR is not available, return error structure. - if (($rawStats['available'] ?? false) === false) { - return [ - 'overview' => [ - 'available' => false, - 'connection_status' => 'unavailable', - 'response_time_ms' => 0, - 'total_documents' => 0, - 'index_size' => '0 B', - 'last_commit' => null, - ], - 'cores' => [ - 'active_core' => 'unknown', - 'core_status' => 'inactive', - 'endpoint_url' => 'N/A', - ], - 'performance' => [ - 'total_searches' => 0, - 'total_indexes' => 0, - 'total_deletes' => 0, - 'avg_search_time_ms' => 0, - 'avg_index_time_ms' => 0, - 'total_search_time' => 0, - 'total_index_time' => 0, - 'operations_per_sec' => 0, - 'error_rate' => 0, - ], - 'health' => [ - 'status' => 'unavailable', - 'uptime' => 'N/A', - 'memory_usage' => ['used' => 'N/A', 'max' => 'N/A', 'percentage' => 0], - 'disk_usage' => ['used' => 'N/A', 'available' => 'N/A', 'percentage' => 0], - 'warnings' => [$rawStats['error'] ?? 'SOLR service is not available or not configured'], - 'last_optimization' => null, - ], - 'operations' => [ - 'recent_activity' => [], - 'queue_status' => ['pending_operations' => 0, 'processing' => false, 'last_processed' => null], - 'commit_frequency' => ['auto_commit' => false, 'commit_within' => 0, 'last_commit' => null], - 'optimization_needed' => false, - ], - 'generated_at' => date('c'), - 'error' => $rawStats['error'] ?? 'SOLR service unavailable', - ]; - }//end if - - // Transform available SOLR stats into dashboard structure. - $serviceStats = $rawStats['service_stats'] ?? []; - $totalOps = ($serviceStats['searches'] ?? 0) + ($serviceStats['indexes'] ?? 0) + ($serviceStats['deletes'] ?? 0); - $totalTime = ($serviceStats['search_time'] ?? 0) + ($serviceStats['index_time'] ?? 0); - - // Calculate operations per second. - $opsPerSec = 0; - if ($totalTime > 0) { - $opsPerSec = round($totalOps / ($totalTime / 1000), 2); - } - - $errorRate = 0; - if ($totalOps > 0) { - $errorRate = round(($serviceStats['errors'] ?? 0) / $totalOps * 100, 2); - } - - $coreStatus = 'inactive'; - if ($rawStats['available'] === true) { - $coreStatus = 'active'; - } - - $avgSearchTimeMs = 0; - if (($serviceStats['searches'] ?? 0) > 0) { - $avgSearchTimeMs = round(($serviceStats['search_time'] ?? 0) / ($serviceStats['searches'] ?? 1), 2); - } - - $avgIndexTimeMs = 0; - if (($serviceStats['indexes'] ?? 0) > 0) { - $avgIndexTimeMs = round(($serviceStats['index_time'] ?? 0) / ($serviceStats['indexes'] ?? 1), 2); - } - - return [ - 'overview' => [ - 'available' => true, - 'connection_status' => $rawStats['health'] ?? 'unknown', - 'response_time_ms' => 0, - // Not available in raw stats. - 'total_documents' => $rawStats['document_count'] ?? 0, - 'index_size' => $this->formatBytesForDashboard(bytes: ($rawStats['index_size'] ?? 0) * 1024), - // Assuming KB. - 'last_commit' => $rawStats['last_modified'] ?? null, - ], - 'cores' => [ - 'active_core' => $rawStats['collection'] ?? 'unknown', - 'core_status' => $coreStatus, - 'endpoint_url' => 'N/A', - // Endpoint URL no longer available in SettingsService (use IndexService directly). - ], - 'performance' => [ - 'total_searches' => $serviceStats['searches'] ?? 0, - 'total_indexes' => $serviceStats['indexes'] ?? 0, - 'total_deletes' => $serviceStats['deletes'] ?? 0, - 'avg_search_time_ms' => $avgSearchTimeMs, - 'avg_index_time_ms' => $avgIndexTimeMs, - 'total_search_time' => $serviceStats['search_time'] ?? 0, - 'total_index_time' => $serviceStats['index_time'] ?? 0, - 'operations_per_sec' => $opsPerSec, - 'error_rate' => $errorRate, - ], - 'health' => [ - 'status' => $rawStats['health'] ?? 'unknown', - 'uptime' => 'N/A', - // Not available in raw stats. - 'memory_usage' => ['used' => 'N/A', 'max' => 'N/A', 'percentage' => 0], - 'disk_usage' => ['used' => 'N/A', 'available' => 'N/A', 'percentage' => 0], - 'warnings' => [], - 'last_optimization' => null, - ], - 'operations' => [ - 'recent_activity' => [], - 'queue_status' => ['pending_operations' => 0, 'processing' => false, 'last_processed' => null], - 'commit_frequency' => [ - 'auto_commit' => true, - 'commit_within' => 1000, - 'last_commit' => $rawStats['last_modified'] ?? null, - ], - 'optimization_needed' => false, - ], - 'generated_at' => date('c'), - ]; - }//end transformSolrStatsToDashboard() - - /** - * Format bytes to human readable format for dashboard - * - * @param int $bytes Number of bytes - * - * @return string Formatted byte string - */ - private function formatBytesForDashboard(int $bytes): string - { - if ($bytes <= 0) { - return '0 B'; - } - - $units = ['B', 'KB', 'MB', 'GB', 'TB']; - $factor = floor(log($bytes, 1024)); - $factor = min($factor, count($units) - 1); - - return round($bytes / pow(1024, $factor), 2).' '.$units[$factor]; - }//end formatBytesForDashboard() - - /** - * Get focused SOLR settings only - * - * @return (bool|int|mixed|null|string)[] SOLR configuration with tenant information - * - * @throws \RuntimeException If SOLR settings retrieval fails - * - * @psalm-return array{enabled: false|mixed, host: 'solr'|mixed, port: 8983|mixed, - * path: '/solr'|mixed, core: 'openregister'|mixed, - * configSet: '_default'|mixed, scheme: 'http'|mixed, username: 'solr'|mixed, - * password: 'SolrRocks'|mixed, timeout: 30|mixed, autoCommit: mixed|true, - * commitWithin: 1000|mixed, enableLogging: mixed|true, - * zookeeperHosts: 'zookeeper:2181'|mixed, zookeeperUsername: ''|mixed, - * zookeeperPassword: ''|mixed, collection: 'openregister'|mixed, - * useCloud: mixed|true, objectCollection: mixed|null, - * fileCollection: mixed|null} - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) SOLR configuration requires many settings fields - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-4 - */ - public function getSolrSettingsOnly(): array - { - try { - $solrConfig = $this->appConfig->getValueString($this->appName, 'solr', ''); - - if (empty($solrConfig) === true) { - return [ - 'enabled' => false, - 'host' => 'solr', - 'port' => 8983, - 'path' => '/solr', - 'core' => 'openregister', - 'configSet' => '_default', - 'scheme' => 'http', - 'username' => 'solr', - 'password' => 'SolrRocks', - 'timeout' => 30, - 'autoCommit' => true, - 'commitWithin' => 1000, - 'enableLogging' => true, - 'zookeeperHosts' => 'zookeeper:2181', - 'zookeeperUsername' => '', - 'zookeeperPassword' => '', - 'collection' => 'openregister', - 'useCloud' => true, - 'objectCollection' => null, - 'fileCollection' => null, - ]; - }//end if - - $solrData = json_decode($solrConfig, true); - return [ - 'enabled' => $solrData['enabled'] ?? false, - 'host' => $solrData['host'] ?? 'solr', - 'port' => $solrData['port'] ?? 8983, - 'path' => $solrData['path'] ?? '/solr', - 'core' => $solrData['core'] ?? 'openregister', - 'configSet' => $solrData['configSet'] ?? '_default', - 'scheme' => $solrData['scheme'] ?? 'http', - 'username' => $solrData['username'] ?? 'solr', - 'password' => $solrData['password'] ?? 'SolrRocks', - 'timeout' => $solrData['timeout'] ?? 30, - 'autoCommit' => $solrData['autoCommit'] ?? true, - 'commitWithin' => $solrData['commitWithin'] ?? 1000, - 'enableLogging' => $solrData['enableLogging'] ?? true, - 'zookeeperHosts' => $solrData['zookeeperHosts'] ?? 'zookeeper:2181', - 'zookeeperUsername' => $solrData['zookeeperUsername'] ?? '', - 'zookeeperPassword' => $solrData['zookeeperPassword'] ?? '', - 'collection' => $solrData['collection'] ?? 'openregister', - 'useCloud' => $solrData['useCloud'] ?? true, - 'objectCollection' => $solrData['objectCollection'] ?? null, - 'fileCollection' => $solrData['fileCollection'] ?? null, - ]; - } catch (Exception $e) { - throw new RuntimeException('Failed to retrieve SOLR settings: '.$e->getMessage()); - }//end try - }//end getSolrSettingsOnly() - - /** - * Update SOLR settings only - * - * @param array $solrData SOLR configuration data - * - * @return (bool|int|mixed|null|string)[] Updated SOLR configuration - * - * @throws \RuntimeException If SOLR settings update fails - * - * @psalm-return array{enabled: false|mixed, host: 'solr'|mixed, port: int, - * path: '/solr'|mixed, core: 'openregister'|mixed, - * configSet: '_default'|mixed, scheme: 'http'|mixed, username: 'solr'|mixed, - * password: 'SolrRocks'|mixed, timeout: int, autoCommit: mixed|true, - * commitWithin: int, enableLogging: mixed|true, - * zookeeperHosts: 'zookeeper:2181'|mixed, zookeeperUsername: ''|mixed, - * zookeeperPassword: ''|mixed, collection: 'openregister'|mixed, - * useCloud: mixed|true, objectCollection: mixed|null, - * fileCollection: mixed|null} - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) SOLR configuration requires many settings fields - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 - */ - public function updateSolrSettingsOnly(array $solrData): array - { - try { - $solrConfig = [ - 'enabled' => $solrData['enabled'] ?? false, - 'host' => $solrData['host'] ?? 'solr', - 'port' => (int) ($solrData['port'] ?? 8983), - 'path' => $solrData['path'] ?? '/solr', - 'core' => $solrData['core'] ?? 'openregister', - 'configSet' => $solrData['configSet'] ?? '_default', - 'scheme' => $solrData['scheme'] ?? 'http', - 'username' => $solrData['username'] ?? 'solr', - 'password' => $solrData['password'] ?? 'SolrRocks', - 'timeout' => (int) ($solrData['timeout'] ?? 30), - 'autoCommit' => $solrData['autoCommit'] ?? true, - 'commitWithin' => (int) ($solrData['commitWithin'] ?? 1000), - 'enableLogging' => $solrData['enableLogging'] ?? true, - 'zookeeperHosts' => $solrData['zookeeperHosts'] ?? 'zookeeper:2181', - 'zookeeperUsername' => $solrData['zookeeperUsername'] ?? '', - 'zookeeperPassword' => $solrData['zookeeperPassword'] ?? '', - 'collection' => $solrData['collection'] ?? 'openregister', - 'useCloud' => $solrData['useCloud'] ?? true, - // Collection assignments for objects and files. - 'objectCollection' => $solrData['objectCollection'] ?? null, - 'fileCollection' => $solrData['fileCollection'] ?? null, - ]; - - $this->appConfig->setValueString($this->appName, 'solr', json_encode($solrConfig)); - return $solrConfig; - } catch (Exception $e) { - throw new RuntimeException('Failed to update SOLR settings: '.$e->getMessage()); - }//end try - }//end updateSolrSettingsOnly() - - /** - * Get search backend configuration. - * - * Returns which search backend is currently active (solr, elasticsearch, etc). - * - * @return array Backend configuration with 'active' key - * - * @throws \RuntimeException If backend configuration retrieval fails - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-4 - */ - public function getSearchBackendConfig(): array - { - try { - $backendConfig = $this->appConfig->getValueString($this->appName, 'search_backend', ''); - - if (empty($backendConfig) === true) { - return [ - 'active' => 'solr', - // Default to Solr for backward compatibility. - 'available' => ['solr', 'elasticsearch'], - ]; - } - - return json_decode($backendConfig, true); - } catch (Exception $e) { - throw new RuntimeException('Failed to retrieve search backend configuration: '.$e->getMessage()); - } - }//end getSearchBackendConfig() - - /** - * Update search backend configuration. - * - * Sets which search backend should be active. - * - * @param string $backend Backend name ('solr', 'elasticsearch', etc) - * - * @return (int|string[])[] Updated backend configuration - * - * @throws \RuntimeException If backend configuration update fails - * - * @psalm-return array{active: string, available: list{'solr', 'elasticsearch'}, updated: int<1, max>} - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-4 - */ - public function updateSearchBackendConfig(string $backend): array - { - try { - $availableBackends = ['solr', 'elasticsearch']; - - if (in_array($backend, $availableBackends) === false) { - throw new InvalidArgumentException( - "Invalid backend '$backend'. Must be one of: ".implode(', ', $availableBackends) - ); - } - - $backendConfig = [ - 'active' => $backend, - 'available' => $availableBackends, - 'updated' => time(), - ]; - - $this->appConfig->setValueString($this->appName, 'search_backend', json_encode($backendConfig)); - - $this->logger->info( - message: '[SolrSettingsHandler] Search backend changed to: '.$backend, - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'app' => 'openregister', - 'backend' => $backend, - ] - ); - - return $backendConfig; - } catch (Exception $e) { - throw new RuntimeException('Failed to update search backend configuration: '.$e->getMessage()); - }//end try - }//end updateSearchBackendConfig() - - /** - * Get SOLR facet configuration - * - * Returns the configuration for customizing SOLR facets including - * custom titles, ordering, and descriptions. - * - * @return array Facet configuration array - * - * @throws \RuntimeException If facet configuration retrieval fails - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Default configuration structure requires comprehensive initialization - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-4 - */ - public function getSolrFacetConfiguration(): array - { - try { - $facetConfig = $this->appConfig->getValueString($this->appName, 'solr_facet_config', ''); - if (empty($facetConfig) === true) { - return [ - 'facets' => [], - 'global_order' => [], - 'default_settings' => [ - 'show_count' => true, - 'show_empty' => false, - 'max_items' => 10, - ], - ]; - } - - return json_decode($facetConfig, true); - } catch (Exception $e) { - throw new RuntimeException('Failed to retrieve SOLR facet configuration: '.$e->getMessage()); - } - }//end getSolrFacetConfiguration() - - /** - * Update SOLR facet configuration - * - * Updates the configuration for customizing SOLR facets including - * custom titles, ordering, and descriptions. - * - * Expected structure: - * [ - * 'facets' => [ - * 'field_name' => [ - * 'title' => 'Custom Title', - * 'description' => 'Custom description', - * 'order' => 1, - * 'enabled' => true, - * 'show_count' => true, - * 'max_items' => 10 - * ] - * ], - * 'global_order' => ['field1', 'field2', 'field3'], - * 'default_settings' => [ - * 'show_count' => true, - * 'show_empty' => false, - * 'max_items' => 10 - * ] - * ] - * - * @param array $facetConfig Facet configuration data - * - * @return array Updated facet configuration with facets, global_order, and default_settings. - * - * @throws \RuntimeException If facet configuration update fails. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Facet configuration validation requires multiple checks - * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid1/tasks.md#task-4 - */ - public function updateSolrFacetConfiguration(array $facetConfig): array - { - try { - // Validate the configuration structure. - $validatedConfig = $this->validateFacetConfiguration(config: $facetConfig); - - $this->appConfig->setValueString($this->appName, 'solr_facet_config', json_encode($validatedConfig)); - return $validatedConfig; - } catch (Exception $e) { - throw new RuntimeException('Failed to update SOLR facet configuration: '.$e->getMessage()); - } - }//end updateSolrFacetConfiguration() - - /** - * Validate facet configuration structure - * - * @param array $config Configuration to validate - * - * @return array Validated configuration with facets, global_order, and default_settings. - * - * @throws \InvalidArgumentException If configuration is invalid. - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Validation requires checking multiple configuration sections - * @SuppressWarnings(PHPMD.NPathComplexity) Each configuration section is independently validated - */ - private function validateFacetConfiguration(array $config): array - { - $validatedConfig = [ - 'facets' => [], - 'global_order' => [], - 'default_settings' => [ - 'show_count' => true, - 'show_empty' => false, - 'max_items' => 10, - ], - ]; - - // Validate facets configuration. - if (($config['facets'] ?? null) !== null && is_array($config['facets']) === true) { - foreach ($config['facets'] as $fieldName => $facetConfig) { - if (is_string($fieldName) === false || empty($fieldName) === true) { - continue; - } - - $validatedFacet = [ - 'title' => $facetConfig['title'] ?? $fieldName, - 'description' => $facetConfig['description'] ?? '', - 'order' => (int) ($facetConfig['order'] ?? 0), - 'enabled' => (bool) ($facetConfig['enabled'] ?? true), - 'show_count' => (bool) ($facetConfig['show_count'] ?? true), - 'max_items' => (int) ($facetConfig['max_items'] ?? 10), - ]; - - $validatedConfig['facets'][$fieldName] = $validatedFacet; - } - } - - // Validate global order. - if (($config['global_order'] ?? null) !== null && is_array($config['global_order']) === true) { - $validatedConfig['global_order'] = array_filter($config['global_order'], 'is_string'); - } - - // Validate default settings. - if (($config['default_settings'] ?? null) !== null && is_array($config['default_settings']) === true) { - $defaults = $config['default_settings']; - $validatedConfig['default_settings'] = [ - 'show_count' => (bool) ($defaults['show_count'] ?? true), - 'show_empty' => (bool) ($defaults['show_empty'] ?? false), - 'max_items' => (int) ($defaults['max_items'] ?? 10), - ]; - } - - return $validatedConfig; - }//end validateFacetConfiguration() -}//end class diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index 4362cc3f01..dcd061b998 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -25,7 +25,6 @@ use DateTime; use Exception; use InvalidArgumentException; -use ReflectionClass; use RuntimeException; use stdClass; use OCP\IAppConfig; @@ -54,9 +53,7 @@ use OCA\OpenRegister\Service\Settings\FileSettingsHandler; use OCA\OpenRegister\Service\Settings\ObjectRetentionHandler; use OCA\OpenRegister\Service\Settings\CacheSettingsHandler; -use OCA\OpenRegister\Service\Settings\SolrSettingsHandler; use OCA\OpenRegister\Service\Settings\ConfigurationSettingsHandler; -use OCA\OpenRegister\Service\Index\SetupHandler; use OCP\ICacheFactory; use Psr\Log\LoggerInterface; @@ -69,14 +66,13 @@ * RESPONSIBILITIES: * - Store and retrieve settings from Nextcloud's IAppConfig * - Provide default values for unconfigured settings - * - Manage settings for: RBAC, Multitenancy, Retention, SOLR, LLM, Files, Objects + * - Manage settings for: RBAC, Multitenancy, Retention, LLM, Files, Objects * - Get available options (groups, users, tenants) for settings UI * - Rebase operations (apply default owners/tenants to existing objects) * - Cache management statistics and operations * * WHAT THIS SERVICE DOES NOT DO: * - Test LLM connections (use VectorEmbeddingService or ChatService) - * - Test search index connections (use IndexService) * - Generate embeddings (use VectorEmbeddingService) * - Execute chat operations (use ChatService) * - Perform searches (use appropriate search services) @@ -86,7 +82,6 @@ * - RBAC: Role-based access control configuration * - Multitenancy: Tenant isolation and default tenant settings * - Retention: Data retention policies for objects, logs, and trails - * - SOLR: Search engine configuration and connection details * - LLM: Language model provider configuration (OpenAI, Fireworks, Ollama) * - Files: File processing and vectorization settings * - Objects: Object vectorization and metadata settings @@ -103,7 +98,6 @@ * - IConfig: Nextcloud's system configuration * - ChatService: Reads LLM settings for chat operations * - VectorEmbeddingService: Reads LLM settings for embeddings - * - IndexService: Reads search index settings for search operations * - Controllers: Delegate settings CRUD operations to this service * * @category Service @@ -215,13 +209,6 @@ class SettingsService */ private CacheSettingsHandler $cacheSettingsHandler; - /** - * SOLR settings handler - * - * @var SolrSettingsHandler - */ - private SolrSettingsHandler $solrSettingsHandler; - /** * Configuration settings handler * @@ -230,7 +217,7 @@ class SettingsService private ?ConfigurationSettingsHandler $configurationSettingsHandler = null; /** - * Setup handler for SOLR field definitions (optional, lazy-loaded to break circular dependency). + * Setup handler (optional, lazy-loaded to break circular dependency). * * @var SetupHandler|null */ @@ -341,7 +328,6 @@ class SettingsService * @param FileSettingsHandler|null $fileSettingsHandler File settings handler * @param ObjectRetentionHandler|null $objRetentionHandler Object retention handler * @param CacheSettingsHandler|null $cacheSettingsHandler Cache settings handler - * @param SolrSettingsHandler|null $solrSettingsHandler SOLR settings handler * @param ConfigurationSettingsHandler|null $cfgSettingsHandler Configuration settings handler * * @return void @@ -371,7 +357,6 @@ public function __construct( ?FileSettingsHandler $fileSettingsHandler=null, ?ObjectRetentionHandler $objRetentionHandler=null, ?CacheSettingsHandler $cacheSettingsHandler=null, - ?SolrSettingsHandler $solrSettingsHandler=null, ?ConfigurationSettingsHandler $cfgSettingsHandler=null ) { $this->config = $config; @@ -398,7 +383,6 @@ public function __construct( $this->fileSettingsHandler = $fileSettingsHandler; $this->objectRetentionHandler = $objRetentionHandler; $this->cacheSettingsHandler = $cacheSettingsHandler; - $this->solrSettingsHandler = $solrSettingsHandler; $this->configurationSettingsHandler = $cfgSettingsHandler; }//end __construct() @@ -413,32 +397,16 @@ public function __construct( * @return array Search backend configuration * * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-2 + * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-1 */ public function getSearchBackendConfig(): array { - // Direct implementation to avoid circular dependency during DI initialization. - // The handler might not be initialized yet when Application.php needs this method. - try { - $backendConfig = $this->config->getAppValue($this->appName, 'search_backend', ''); - - if (empty($backendConfig) === true) { - return [ - 'active' => 'solr', - 'available' => ['solr', 'elasticsearch'], - ]; - } - - return json_decode($backendConfig, true); - } catch (\Exception $e) { - $this->logger->error( - message: '[SettingsService] Failed to retrieve search backend configuration: '.$e->getMessage(), - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return [ - 'active' => 'solr', - 'available' => ['solr', 'elasticsearch'], - ]; - }//end try + // The external Solr/Elasticsearch backends were removed; the built-in + // database (Magic-Tables) search is the only backend. + return [ + 'active' => 'database', + 'available' => ['database'], + ]; }//end getSearchBackendConfig() /** @@ -452,8 +420,8 @@ public function getSearchBackendConfig(): array */ public function updateSearchBackendConfig(array $data): array { - // Extract backend string from data array. - $backend = $data['backend'] ?? $data['active'] ?? 'solr'; + // Extract backend string from data array (database is the only valid backend). + $backend = $data['backend'] ?? $data['active'] ?? 'database'; return $this->searchBackendHandler->updateSearchBackendConfig($backend); }//end updateSearchBackendConfig() @@ -633,115 +601,6 @@ public function warmupNamesCache(): array return $this->cacheSettingsHandler->warmupNamesCache(); }//end warmupNamesCache() - // SolrSettingsHandler methods (7 main ones). - - /** - * Get SOLR settings - * - * @return array SOLR settings - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-1 - */ - public function getSolrSettings(): array - { - return $this->solrSettingsHandler->getSolrSettings(); - }//end getSolrSettings() - - /** - * Get SOLR settings only - * - * @return array SOLR settings - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-1 - */ - public function getSolrSettingsOnly(): array - { - return $this->solrSettingsHandler->getSolrSettingsOnly(); - }//end getSolrSettingsOnly() - - /** - * Update SOLR settings only - * - * @param array $data SOLR settings data - * - * @return array Updated SOLR settings - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-1 - */ - public function updateSolrSettingsOnly(array $data): array - { - return $this->solrSettingsHandler->updateSolrSettingsOnly($data); - }//end updateSolrSettingsOnly() - - /** - * Get SOLR dashboard statistics - * - * @return array SOLR dashboard stats - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-5 - */ - public function getSolrDashboardStats(): array - { - return $this->solrSettingsHandler->getSolrDashboardStats(); - }//end getSolrDashboardStats() - - /** - * Get SOLR facet configuration - * - * @return array Facet configuration - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-5 - */ - public function getSolrFacetConfiguration(): array - { - return $this->solrSettingsHandler->getSolrFacetConfiguration(); - }//end getSolrFacetConfiguration() - - /** - * Update SOLR facet configuration - * - * @param array $data Facet configuration data - * - * @return array Updated facet configuration - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-5 - */ - public function updateSolrFacetConfiguration(array $data): array - { - return $this->solrSettingsHandler->updateSolrFacetConfiguration($data); - }//end updateSolrFacetConfiguration() - - /** - * Warmup SOLR index - * - * @param array $schemas Schemas to warmup - * @param int $maxObjects Maximum objects to process - * @param string $mode Processing mode - * @param bool $collectErrors Whether to collect errors - * @param int $batchSize Batch size - * @param array $schemaIds Schema IDs to process - * - * @return never Warmup result - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Boolean flag needed for error collection behavior - * @SuppressWarnings(PHPMD.UnusedFormalParameter) Public facade signature kept while the deprecated - * delegate is refactored — see the warmupSolrIndex TODO comment - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-3 - */ - public function warmupSolrIndex( - array $schemas=[], - int $maxObjects=0, - string $mode='serial', - bool $collectErrors=false, - int $batchSize=1000, - array $schemaIds=[] - ): never { - // NOTE: This method calls a deprecated method that always throws. - // TODO: Refactor to use IndexService->warmupIndex() directly. - $this->solrSettingsHandler->warmupSolrIndex(); - }//end warmupSolrIndex() - // ConfigurationSettingsHandler methods (15 main ones). /** @@ -770,23 +629,6 @@ public function updateSettings(array $data): array return $this->configurationSettingsHandler->updateSettings($data); }//end updateSettings() - /** - * Update publishing options - * - * @param array $data Publishing options data - * - * @return bool[] Updated settings - * - * @psalm-return array{use_old_style_publishing_view?: bool, - * auto_publish_objects?: bool, auto_publish_attachments?: bool} - * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-settings-mgmt/tasks.md#task-1 - */ - public function updatePublishingOptions(array $data): array - { - return $this->configurationSettingsHandler->updatePublishingOptions($data); - }//end updatePublishingOptions() - /** * Check if multi-tenancy is enabled * @@ -1687,165 +1529,6 @@ public function maskToken(string $token): string return $start.$middle.$end; }//end maskToken() - /** - * Get expected schema fields based on OpenRegister schemas. - * - * Returns field definitions for SOLR schema comparison, combining - * core metadata fields with user-defined schema fields. - * - * @param \OCA\OpenRegister\Db\SchemaMapper $schemaMapper Schema mapper for database access. - * @param \OCA\OpenRegister\Service\IndexService $solrSchemaService Index service for field analysis. - * - * @return array Expected field configuration. - * - * @spec exclude Assembles the expected Solr field set from core metadata + schema fields for diffing; admin-tooling helper. - */ - public function getExpectedSchemaFields( - \OCA\OpenRegister\Db\SchemaMapper $schemaMapper, - \OCA\OpenRegister\Service\IndexService $solrSchemaService - ): array { - try { - // Start with the core ObjectEntity metadata fields from SetupHandler (if available). - $expectedFields = []; - if ($this->setupHandler !== null) { - $expectedFields = $this->setupHandler->getObjectEntityFieldDefinitions(); - } - - // Get all schemas. - $schemas = $schemaMapper->findAll(); - - // Use the existing analyzeAndResolveFieldConflicts method via reflection. - $reflection = new ReflectionClass($solrSchemaService); - $method = $reflection->getMethod('analyzeAndResolveFieldConflicts'); - - $result = $method->invoke($solrSchemaService, $schemas); - - // Merge user-defined schema fields with core metadata fields. - $userSchemaFields = $result['fields'] ?? []; - $expectedFields = array_merge($expectedFields, $userSchemaFields); - - return $expectedFields; - } catch (\Exception $e) { - $this->logger->warning( - message: '[SettingsService] Failed to get expected schema fields', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - ] - ); - // Return at least the core metadata fields even if schema analysis fails. - if ($this->setupHandler !== null) { - return $this->setupHandler->getObjectEntityFieldDefinitions(); - } - - return []; - }//end try - }//end getExpectedSchemaFields() - - /** - * Compare actual SOLR fields with expected schema fields. - * - * Identifies missing fields, extra fields, and configuration mismatches - * between the current SOLR schema and expected field definitions. - * - * @param array $actualFields Current SOLR fields. - * @param array $expectedFields Expected fields from schemas. - * - * @return array Field comparison results - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple field comparison paths - * - * @spec exclude Pure diff helper categorising missing/extra/mismatched Solr fields; admin-tooling computation. - */ - public function compareFields(array $actualFields, array $expectedFields): array - { - $missing = []; - $extra = []; - $mismatched = []; - - // Find missing fields (expected but not in SOLR). - foreach ($expectedFields as $fieldName => $expectedConfig) { - if (isset($actualFields[$fieldName]) === false) { - $missing[] = [ - 'field' => $fieldName, - 'expected_type' => $expectedConfig['type'] ?? 'unknown', - 'expected_config' => $expectedConfig, - ]; - } - } - - // Find extra fields (in SOLR but not expected) and mismatched configurations. - foreach ($actualFields as $fieldName => $actualField) { - // Skip only system fields (but allow self_* metadata fields to be checked). - if (str_starts_with($fieldName, '_') === true) { - continue; - } - - if (isset($expectedFields[$fieldName]) === false) { - $extra[] = [ - 'field' => $fieldName, - 'actual_type' => $actualField['type'] ?? 'unknown', - 'actual_config' => $actualField, - ]; - continue; - } - - // Check for configuration mismatches (type, multiValued, docValues). - $expectedConfig = $expectedFields[$fieldName]; - $expectedType = $expectedConfig['type'] ?? ''; - $actualType = $actualField['type'] ?? ''; - $expectedMultiValued = $expectedConfig['multiValued'] ?? false; - $actualMultiValued = $actualField['multiValued'] ?? false; - $expectedDocValues = $expectedConfig['docValues'] ?? false; - $actualDocValues = $actualField['docValues'] ?? false; - - // Check if any configuration differs. - if ($expectedType !== $actualType - || $expectedMultiValued !== $actualMultiValued - || $expectedDocValues !== $actualDocValues - ) { - $differences = []; - if ($expectedType !== $actualType) { - $differences[] = 'type'; - } - - if ($expectedMultiValued !== $actualMultiValued) { - $differences[] = 'multiValued'; - } - - if ($expectedDocValues !== $actualDocValues) { - $differences[] = 'docValues'; - } - - $mismatched[] = [ - 'field' => $fieldName, - 'expected_type' => $expectedType, - 'actual_type' => $actualType, - 'expected_multiValued' => $expectedMultiValued, - 'actual_multiValued' => $actualMultiValued, - 'expected_docValues' => $expectedDocValues, - 'actual_docValues' => $actualDocValues, - 'differences' => $differences, - 'expected_config' => $expectedConfig, - 'actual_config' => $actualField, - ]; - }//end if - }//end foreach - - return [ - 'missing' => $missing, - 'extra' => $extra, - 'mismatched' => $mismatched, - 'summary' => [ - 'missing_count' => count($missing), - 'extra_count' => count($extra), - 'mismatched_count' => count($mismatched), - 'total_differences' => count($missing) + count($extra) + count($mismatched), - ], - ]; - }//end compareFields() - /** * Get comprehensive statistics. * @@ -1889,7 +1572,6 @@ public function compareFields(array $actualFields, array $expectedFields): array * totalWebhookLogs: int, * deletedObjects: int * }, - * solr?: array, * cache?: array, * system: array{ * php_version: string, @@ -1947,13 +1629,6 @@ public function getStats(): array ]; }//end try - // Get Solr stats if available. - try { - $stats['solr'] = $this->getSolrDashboardStats(); - } catch (\Exception $e) { - $stats['solr'] = ['error' => $e->getMessage()]; - } - // Get cache stats. try { $stats['cache'] = $this->getCacheStats(); @@ -2144,8 +1819,7 @@ private function getDatabaseStats(): array * @return ((string|true)[][]|bool|int|string)[] Rebase result * * @psalm-return array{success: bool, error?: 'Rebase failed', message: string, - * rebased?: array{solr?: array{success: true, - * message: 'Solr configuration rebased'}, cache?: array{success: true, + * rebased?: array{cache?: array{success: true, * message: 'Cache cleared and ready for rebuild'}}, * timestamp?: int<1, max>} * @@ -2165,14 +1839,6 @@ public function rebase(array $options=[]): array $components = $options['components'] ?? ['all']; $rebased = []; - if (in_array('all', $components, true) === true || in_array('solr', $components, true) === true) { - // Rebase Solr configuration. - $rebased['solr'] = [ - 'success' => true, - 'message' => 'Solr configuration rebased', - ]; - } - if (in_array('all', $components, true) === true || in_array('cache', $components, true) === true) { // Clear and rebuild cache. $this->clearCache(); @@ -2201,4 +1867,95 @@ public function rebase(array $options=[]): array ]; }//end try }//end rebase() + + // ============================================ + // INTEGRATION DEFAULTS (Deck — schema-level sticky board+stack). + // ============================================ + // Persists the {boardId, stackId} pair the first time a user creates + // a Deck card for a given schema slug, so subsequent create-card + // affordances on objects of that schema pre-select the same target. + // See openspec/changes/integration-deck/design.md AD-1. + + /** + * Build the IAppConfig storage key for the sticky Deck default. + * + * @param string $schemaSlug Schema slug or numeric id (caller's choice — only used as opaque key suffix). + * + * @return string The config key, e.g. `integration.deck.default.case`. + */ + private function buildDeckDefaultKey(string $schemaSlug): string + { + return 'integration.deck.default.'.$schemaSlug; + }//end buildDeckDefaultKey() + + /** + * Get the persisted Deck default board+stack for a schema (or null). + * + * @param string $schemaSlug Schema slug or numeric id used to scope the default. + * + * @return array{boardId:int,stackId:int}|null Default pair, or null when none has been recorded yet. + * + * @spec openspec/specs/integration-deck/spec.md + */ + public function getDeckDefault(string $schemaSlug): ?array + { + $raw = $this->config->getAppValue($this->appName, $this->buildDeckDefaultKey(schemaSlug: $schemaSlug), ''); + if ($raw === '') { + return null; + } + + $decoded = json_decode($raw, true); + if (is_array($decoded) === false) { + return null; + } + + if (isset($decoded['boardId']) === false || isset($decoded['stackId']) === false) { + return null; + } + + return [ + 'boardId' => (int) $decoded['boardId'], + 'stackId' => (int) $decoded['stackId'], + ]; + }//end getDeckDefault() + + /** + * Persist the Deck default board+stack for a schema. Overwrites any prior value. + * + * @param string $schemaSlug Schema slug or numeric id used to scope the default. + * @param int $boardId NC Deck board id. + * @param int $stackId NC Deck stack id on that board. + * + * @return void + * + * @spec openspec/specs/integration-deck/spec.md + */ + public function setDeckDefault(string $schemaSlug, int $boardId, int $stackId): void + { + $payload = json_encode( + [ + 'boardId' => $boardId, + 'stackId' => $stackId, + ] + ); + if ($payload === false) { + return; + } + + $this->config->setAppValue($this->appName, $this->buildDeckDefaultKey(schemaSlug: $schemaSlug), $payload); + }//end setDeckDefault() + + /** + * Clear the persisted Deck default for a schema. No-op when none stored. + * + * @param string $schemaSlug Schema slug or numeric id used to scope the default. + * + * @return void + * + * @spec exclude — defensive cleanup helper exercised only by uninstall paths; behaviour proxied to IConfig. + */ + public function clearDeckDefault(string $schemaSlug): void + { + $this->config->deleteAppValue($this->appName, $this->buildDeckDefaultKey(schemaSlug: $schemaSlug)); + }//end clearDeckDefault() }//end class diff --git a/lib/Service/ShareLinkService.php b/lib/Service/ShareLinkService.php index 0266213211..f29d5369f2 100644 --- a/lib/Service/ShareLinkService.php +++ b/lib/Service/ShareLinkService.php @@ -42,7 +42,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/integration-shares/tasks.md + * @spec openspec/specs/integration-shares/spec.md */ declare(strict_types=1); @@ -68,8 +68,14 @@ * @category Service * @package OCA\OpenRegister\Service * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Service references IManager, IShare, Folder, Node, IL10N, LoggerInterface, ContainerInterface, DateTime, DateTimeInterface, OCP\Server, and Throwable — every type is required for the share list/create/revoke contract or the lazy-resolution policy. - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Service exposes getLinkedShares/createShare/revokeShare/getShareableFiles plus internal resolution helpers (getShareManager, resolveObjectFolder, resolveNodeInFolder, normaliseShare, shareIsInFolder, resolveCurrentUserId, resolveObjectEntity, lookup, resolveContainer); each is a required step of a distinct public flow. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Service references IManager, IShare, Folder, Node, + * IL10N, LoggerInterface, ContainerInterface, DateTime, DateTimeInterface, OCP\Server, and Throwable — + * every type is required for the share list/create/revoke contract or the lazy-resolution policy. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Service exposes + * getLinkedShares/createShare/revokeShare/getShareableFiles plus internal resolution helpers + * (getShareManager, resolveObjectFolder, resolveNodeInFolder, normaliseShare, shareIsInFolder, + * resolveCurrentUserId, resolveObjectEntity, lookup, resolveContainer); each is a required step + * of a distinct public flow. */ class ShareLinkService { @@ -141,7 +147,9 @@ public function __construct( * @spec exclude ADR-019 Tier-2 integration link-service facade; the shares-listing contract is owned by the * integration-shares / generic-integrations capability. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) getLinkedShares() walks six share types per folder node with per-type Throwable swallow and per-share deduplication; each branch is a required degradation guard per the AD-23 graceful-degradation contract. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) getLinkedShares() walks six share types per folder + * node with per-type Throwable swallow and per-share deduplication; each branch is a required + * degradation guard per the AD-23 graceful-degradation contract. */ public function getLinkedShares(string $objectUuid): array { @@ -217,10 +225,18 @@ public function getLinkedShares(string $objectUuid): array * @throws Exception On missing user (401), file-not-in-folder (404), * invalid recipient (400) or share-manager failure. * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) createShare() maps directly to IManager::createShare()'s required fields (node, shareType, sharedBy, permissions, shareWith, password, expiration); bundling into a value-object would add an intermediate type not used elsewhere. - * @SuppressWarnings(PHPMD.CyclomaticComplexity) createShare() guards share type validity, shareWith requirement per type, password/expiration optional fields, and ownership boundary (node-in-folder check); each branch enforces a distinct access-control requirement. - * @SuppressWarnings(PHPMD.NPathComplexity) Optional password + optional expiration + three share-type branches that require shareWith produce many independent paths; all enforce the IManager::createShare contract. - * @SuppressWarnings(PHPMD.UnusedFormalParameter) $registerId and $schemaId are reserved for future folder-scoping; removing them now would break the method signature used by the controller and SharesProvider. + * @SuppressWarnings(PHPMD.ExcessiveParameterList) createShare() maps directly to + * IManager::createShare()'s required fields (node, shareType, sharedBy, permissions, shareWith, + * password, expiration); bundling into a value-object would add an intermediate type not used elsewhere. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) createShare() guards share type validity, shareWith + * requirement per type, password/expiration optional fields, and ownership boundary + * (node-in-folder check); each branch enforces a distinct access-control requirement. + * @SuppressWarnings(PHPMD.NPathComplexity) Optional password + optional expiration + three share-type + * branches that require shareWith produce many independent paths; all enforce the + * IManager::createShare contract. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) $registerId and $schemaId are reserved for future + * folder-scoping; removing them now would break the method signature used by the controller + * and SharesProvider. * * @spec exclude ADR-019 Tier-2 integration link-service facade; the create-share contract is owned by the * integration-shares / generic-integrations capability. @@ -626,7 +642,9 @@ private function lookup(string $serviceName): ?object * * @return ContainerInterface * - * @SuppressWarnings(PHPMD.StaticAccess) \OCP\Server::get() is the Nextcloud-prescribed static service-locator for optional late-bound dependencies; no injectable alternative exists for the lazy-resolution policy (constructor injection would fail when optional services are absent). + * @SuppressWarnings(PHPMD.StaticAccess) \OCP\Server::get() is the Nextcloud-prescribed static + * service-locator for optional late-bound dependencies; no injectable alternative exists for the + * lazy-resolution policy (constructor injection would fail when optional services are absent). */ private function resolveContainer(): ContainerInterface { diff --git a/lib/Service/Survivorship/SourceRecordResolver.php b/lib/Service/Survivorship/SourceRecordResolver.php new file mode 100644 index 0000000000..db20da5c50 --- /dev/null +++ b/lib/Service/Survivorship/SourceRecordResolver.php @@ -0,0 +1,391 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Survivorship; + +use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\SchemaMapper; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Mode-aware source-record resolver shared by the survivorship recompute + * listener and the merge service. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ +class SourceRecordResolver +{ + + /** + * Cross-request cache of source-schema reference (slug/uuid) => numeric + * schema id. Populated only from successful resolutions (a mid-save slug + * lookup that throws never poisons it). Lets the reverse-FK query pass a + * numeric schema id — which `ObjectService::setSchema()` resolves via its + * cached numeric path — instead of a slug, whose DB lookup throws when it + * runs inside a save transaction (the recompute-on-save listener). + * + * @var array + */ + private static array $schemaIdCache = []; + + /** + * Wire the object read path used to resolve uuid references (embedded + * mode) and to query source objects (reverse-FK mode). + * + * @param ObjectService $objectService Object read path (RBAC + tenant scoped). + * @param SchemaMapper $schemaMapper Schema lookup, to resolve the source schema slug to a numeric id. + * @param LoggerInterface $logger PSR logger for non-fatal warnings. + * + * @return void + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly SchemaMapper $schemaMapper, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Resolve the competing source records for a master, honouring the + * annotation's `sourceLink.mode`. + * + * @param array $masterData Master object payload. + * @param string $masterUuid Master object uuid (needed for reverse-FK). + * @param array $config `x-openregister-survivorship` (or `-merge`) config. + * @param string $masterRegister Master's register ref, used as the reverse-FK + * source register when the annotation omits one + * (the magic-table query needs a register + schema). + * + * @return array> Resolved source-record payloads. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + public function resolveSources(array $masterData, string $masterUuid, array $config, string $masterRegister=''): array + { + $reverseFk = $this->reverseFkConfig(config: $config); + if ($reverseFk !== null) { + $sourceRegister = $reverseFk['sourceRegister']; + if ($sourceRegister === '') { + $sourceRegister = $masterRegister; + } + + return $this->resolveReverseFk( + masterUuid: $masterUuid, + sourceSchema: $reverseFk['sourceSchema'], + referenceField: $reverseFk['referenceField'], + sourceRegister: $sourceRegister + ); + } + + // Embedded mode (default): read the master payload's sourceLinkField. + $sourceLinkField = (string) ($config['sourceLinkField'] ?? ''); + if ($sourceLinkField === '') { + return []; + } + + return $this->resolveEmbedded(data: $masterData, sourceLinkField: $sourceLinkField); + }//end resolveSources() + + /** + * Whether the config selects reverse-FK resolution. + * + * @param array $config Survivorship/merge config. + * + * @return bool True when a well-formed reverse-FK `sourceLink` is present. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + public function isReverseFk(array $config): bool + { + return $this->reverseFkConfig(config: $config) !== null; + }//end isReverseFk() + + /** + * Return the validated reverse-FK descriptor (`sourceSchema`, + * `referenceField`, `sourceRegister`) for a config, or null when the + * config is not reverse-FK. Used by callers that need to query or mutate + * the source objects themselves (e.g. merge relink). + * + * @param array $config Survivorship/merge config. + * + * @return array{sourceSchema: string, referenceField: string, sourceRegister: string}|null + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + public function reverseFkDescriptor(array $config): ?array + { + return $this->reverseFkConfig(config: $config); + }//end reverseFkDescriptor() + + /** + * Resolve a source-schema reference to the value the reverse-FK `findAll` + * filter should carry: the numeric schema id when resolvable (the + * save-transaction-safe path), else the original reference unchanged. + * Used by callers that build the query themselves (e.g. merge relink). + * + * @param string $ref Source-schema reference (slug/uuid/id). + * + * @return string Numeric id when resolvable, otherwise `$ref`. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + public function schemaQueryFilter(string $ref): string + { + $id = $this->resolveSchemaId(ref: $ref); + if ($id !== '') { + return $id; + } + + return $ref; + }//end schemaQueryFilter() + + /** + * Resolve a schema reference (slug/uuid/id) to its numeric id, memoising + * successful resolutions across requests. A lookup that throws (e.g. a slug + * resolved inside a save transaction) returns '' and is NOT cached, so a + * later clean-context call can still populate it. + * + * @param string $ref Schema reference. + * + * @return string Numeric schema id, or '' when unresolved. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + private function resolveSchemaId(string $ref): string + { + if ($ref === '') { + return ''; + } + + if (is_numeric($ref) === true) { + return $ref; + } + + if (array_key_exists($ref, self::$schemaIdCache) === true) { + return self::$schemaIdCache[$ref]; + } + + try { + $schema = $this->schemaMapper->find($ref, _rbac: false, _multitenancy: false); + $id = (string) $schema->getId(); + } catch (Throwable) { + return ''; + } + + if ($id !== '') { + self::$schemaIdCache[$ref] = $id; + } + + return $id; + }//end resolveSchemaId() + + /** + * Parse and validate the reverse-FK `sourceLink` block. A block with mode + * `reverseFk` but missing `sourceSchema`/`referenceField` degrades to + * embedded mode (returns null) with a logged warning. + * + * @param array $config Survivorship/merge config. + * + * @return array{sourceSchema: string, referenceField: string, sourceRegister: string}|null + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + private function reverseFkConfig(array $config): ?array + { + $sourceLink = ($config['sourceLink'] ?? null); + if (is_array($sourceLink) === false) { + return null; + } + + if ((string) ($sourceLink['mode'] ?? 'embedded') !== 'reverseFk') { + return null; + } + + $sourceSchema = (string) ($sourceLink['sourceSchema'] ?? ''); + $referenceField = (string) ($sourceLink['referenceField'] ?? ''); + if ($sourceSchema === '' || $referenceField === '') { + $this->logger->warning( + 'x-openregister source resolution: reverseFk sourceLink is missing sourceSchema or referenceField; falling back to embedded mode.' + ); + return null; + } + + return [ + 'sourceSchema' => $sourceSchema, + 'referenceField' => $referenceField, + 'sourceRegister' => (string) ($sourceLink['sourceRegister'] ?? ''), + ]; + }//end reverseFkConfig() + + /** + * Query the source schema for objects whose `referenceField` equals the + * master uuid. RBAC + multitenancy scoped. A DB-level property filter is + * requested, and the result is re-filtered in PHP so correctness does not + * depend on the magic-table filter honouring an arbitrary property. + * + * @param string $masterUuid Master object uuid. + * @param string $sourceSchema Slug/id of the source schema. + * @param string $referenceField Field on a source object holding the master uuid. + * @param string $sourceRegister Optional register slug/id; empty means unset. + * + * @return array> Resolved source-record payloads. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + private function resolveReverseFk( + string $masterUuid, + string $sourceSchema, + string $referenceField, + string $sourceRegister + ): array { + if ($masterUuid === '') { + return []; + } + + // Prefer a numeric schema id (setSchema's cached numeric path works + // inside a save transaction; a slug lookup throws there). + $schemaFilter = $this->resolveSchemaId(ref: $sourceSchema); + if ($schemaFilter === '') { + $schemaFilter = $sourceSchema; + } + + $filters = [ + 'schema' => $schemaFilter, + $referenceField => $masterUuid, + ]; + if ($sourceRegister !== '') { + $filters['register'] = $sourceRegister; + } + + try { + $objects = $this->objectService->findAll( + ['filters' => $filters], + _rbac: true, + _multitenancy: true + ); + } catch (Throwable $e) { + $this->logger->warning( + sprintf('Reverse-FK source resolution failed for master "%s": %s', $masterUuid, $e->getMessage()) + ); + return []; + } + + $records = []; + foreach ($objects as $object) { + if (($object instanceof ObjectEntity) === false) { + continue; + } + + $data = ($object->getObject() ?? []); + // Re-filter in PHP: only sources actually pointing at this master. + if ((string) ($data[$referenceField] ?? '') !== $masterUuid) { + continue; + } + + $records[] = $data; + } + + return $records; + }//end resolveReverseFk() + + /** + * Embedded resolution: the field holds an array of already-embedded source + * records and/or uuid strings referencing objects elsewhere in the + * register (resolved via ObjectService::find, RBAC + tenant scoped). + * Unresolvable entries are skipped. + * + * @param array $data Master object payload. + * @param string $sourceLinkField Field holding the linked source records. + * + * @return array> Resolved source-record payloads. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + private function resolveEmbedded(array $data, string $sourceLinkField): array + { + $raw = ($data[$sourceLinkField] ?? null); + if (is_array($raw) === false) { + return []; + } + + $records = []; + foreach ($raw as $entry) { + if (is_array($entry) === true) { + $records[] = $entry; + continue; + } + + if (is_string($entry) === true && $entry !== '') { + $resolved = $this->resolveReference(uuid: $entry); + if ($resolved !== null) { + $records[] = $resolved; + } + } + } + + return $records; + }//end resolveEmbedded() + + /** + * Resolve a single uuid reference to its payload, RBAC + tenant scoped. + * + * @param string $uuid Referenced object's uuid/id. + * + * @return array|null Resolved payload, or null on lookup failure. + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#2.1 + */ + private function resolveReference(string $uuid): ?array + { + try { + $entity = $this->objectService->find(id: $uuid, _rbac: true, _multitenancy: true); + } catch (Throwable) { + return null; + } + + if ($entity === null) { + return null; + } + + return ($entity->getObject() ?? []); + }//end resolveReference() +}//end class diff --git a/lib/Service/Survivorship/SurvivorshipAnnotationValidator.php b/lib/Service/Survivorship/SurvivorshipAnnotationValidator.php new file mode 100644 index 0000000000..c9e4548d7d --- /dev/null +++ b/lib/Service/Survivorship/SurvivorshipAnnotationValidator.php @@ -0,0 +1,288 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Survivorship; + +/** + * Shape validator for the `x-openregister-survivorship` annotation. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#3.2 + */ +class SurvivorshipAnnotationValidator +{ + /** + * Default ordered tier list (weakest first) when the annotation omits one. + * + * @var array + */ + private const DEFAULT_TIER_ORDER = ['discard', 'bronze', 'silver', 'gold']; + + /** + * Default tier for an uncontested source with no matching trust row. + * + * @var string + */ + private const DEFAULT_TIER = 'bronze'; + + /** + * Default tier that is always excluded from the golden record. + * + * @var string + */ + private const DEFAULT_DISCARD_TIER = 'discard'; + + /** + * Validate the `x-openregister-survivorship` annotation in a schema shape. + * + * @param array $schema Shape with `properties` and `x-openregister-survivorship`. + * + * @return array Errors; empty when valid or absent. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#3.2 + */ + public function validate(array $schema): array + { + $annotation = ($schema['x-openregister-survivorship'] ?? null); + if ($annotation === null) { + return []; + } + + if (is_array($annotation) === false) { + return [ + [ + 'code' => 'survivorship.not-object', + 'message' => 'x-openregister-survivorship must be an object.', + ], + ]; + } + + $errors = []; + + // A reverse-FK `sourceLink` block supplies the source linkage instead + // of an embedded `sourceLinkField`; when it is present and well-formed, + // `sourceLinkField` is not required. + $sourceLinkErrors = $this->validateSourceLink(annotation: $annotation); + $errors = array_merge($errors, $sourceLinkErrors); + + $sourceLinkField = (string) ($annotation['sourceLinkField'] ?? ''); + if ($sourceLinkField === '' && $this->hasReverseFkSourceLink(annotation: $annotation) === false) { + $errors[] = [ + 'code' => 'survivorship.missing-source-link-field', + 'message' => 'x-openregister-survivorship requires a "sourceLinkField" (or a reverse-FK "sourceLink" block).', + ]; + } + + $errors = array_merge($errors, $this->validateTierOrder(annotation: $annotation)); + $errors = array_merge($errors, $this->validateTrustLookup(annotation: $annotation)); + $errors = array_merge($errors, $this->validateOverridesField(annotation: $annotation)); + + return $errors; + }//end validate() + + /** + * Validate an optional `sourceLink` block. A `reverseFk` mode requires + * `sourceSchema` and `referenceField`; anything else is accepted (embedded + * mode). Errors are non-fatal (surfaced as warnings by the caller). + * + * @param array $annotation Survivorship annotation. + * + * @return array + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#1.1 + */ + private function validateSourceLink(array $annotation): array + { + $sourceLink = ($annotation['sourceLink'] ?? null); + if ($sourceLink === null) { + return []; + } + + if (is_array($sourceLink) === false) { + return [ + [ + 'code' => 'survivorship.source-link-not-object', + 'message' => 'x-openregister-survivorship "sourceLink" must be an object.', + ], + ]; + } + + if ((string) ($sourceLink['mode'] ?? 'embedded') !== 'reverseFk') { + return []; + } + + $errors = []; + if ((string) ($sourceLink['sourceSchema'] ?? '') === '' + || (string) ($sourceLink['referenceField'] ?? '') === '' + ) { + $errors[] = [ + 'code' => 'survivorship.source-link-reverse-fk-incomplete', + 'message' => 'x-openregister-survivorship reverseFk "sourceLink" requires "sourceSchema" and "referenceField".', + ]; + } + + return $errors; + }//end validateSourceLink() + + /** + * Whether the annotation carries a well-formed reverse-FK `sourceLink`. + * + * @param array $annotation Survivorship annotation. + * + * @return bool + * + * @spec openspec/changes/mdm-reverse-fk-source-resolution/tasks.md#1.1 + */ + private function hasReverseFkSourceLink(array $annotation): bool + { + $sourceLink = ($annotation['sourceLink'] ?? null); + if (is_array($sourceLink) === false) { + return false; + } + + return (string) ($sourceLink['mode'] ?? 'embedded') === 'reverseFk' + && (string) ($sourceLink['sourceSchema'] ?? '') !== '' + && (string) ($sourceLink['referenceField'] ?? '') !== ''; + }//end hasReverseFkSourceLink() + + /** + * Validate `tierOrder` plus `defaultTier` / `discardTier` membership. + * + * @param array $annotation Survivorship annotation. + * + * @return array + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#3.2 + */ + private function validateTierOrder(array $annotation): array + { + $errors = []; + + $tierOrder = ($annotation['tierOrder'] ?? self::DEFAULT_TIER_ORDER); + if (is_array($tierOrder) === false) { + $errors[] = [ + 'code' => 'survivorship.tier-order-not-array', + 'message' => 'x-openregister-survivorship "tierOrder" must be an array.', + ]; + + // Cannot validate defaultTier/discardTier membership against a + // non-array tierOrder; the caller degrades the whole block anyway. + return $errors; + } + + $defaultTier = (string) ($annotation['defaultTier'] ?? self::DEFAULT_TIER); + if (in_array($defaultTier, $tierOrder, true) === false) { + $errors[] = [ + 'code' => 'survivorship.default-tier-not-in-order', + 'message' => sprintf('"defaultTier" (%s) must be present in "tierOrder".', $defaultTier), + ]; + } + + $discardTier = (string) ($annotation['discardTier'] ?? self::DEFAULT_DISCARD_TIER); + if (in_array($discardTier, $tierOrder, true) === false) { + $errors[] = [ + 'code' => 'survivorship.discard-tier-not-in-order', + 'message' => sprintf('"discardTier" (%s) must be present in "tierOrder".', $discardTier), + ]; + } + + return $errors; + }//end validateTierOrder() + + /** + * Validate the optional `trustLookup.keys` block. + * + * @param array $annotation Survivorship annotation. + * + * @return array + */ + private function validateTrustLookup(array $annotation): array + { + $trustLookup = ($annotation['trustLookup'] ?? null); + if ($trustLookup === null) { + return []; + } + + if (is_array($trustLookup) === false) { + return [ + [ + 'code' => 'survivorship.trust-lookup-not-object', + 'message' => 'x-openregister-survivorship "trustLookup" must be an object.', + ], + ]; + } + + $keys = ($trustLookup['keys'] ?? null); + if ($keys === null) { + return []; + } + + if (is_array($keys) === false || count($keys) === 0) { + return [ + [ + 'code' => 'survivorship.trust-lookup-keys-invalid', + 'message' => 'x-openregister-survivorship "trustLookup.keys" must be a non-empty array.', + ], + ]; + } + + return []; + }//end validateTrustLookup() + + /** + * Validate the optional `overridesField` key: when present it MUST be a + * non-empty string naming the object field holding the per-object + * attribute-override map. Absent is valid — callers default to + * `attributeOverrides`. + * + * @param array $annotation Survivorship annotation. + * + * @return array + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.4 + */ + private function validateOverridesField(array $annotation): array + { + if (array_key_exists('overridesField', $annotation) === false) { + return []; + } + + $overridesField = $annotation['overridesField']; + if (is_string($overridesField) === false || $overridesField === '') { + return [ + [ + 'code' => 'survivorship.overrides-field-invalid', + 'message' => 'x-openregister-survivorship "overridesField" must be a non-empty string.', + ], + ]; + } + + return []; + }//end validateOverridesField() +}//end class diff --git a/lib/Service/Survivorship/SurvivorshipResolver.php b/lib/Service/Survivorship/SurvivorshipResolver.php new file mode 100644 index 0000000000..4519f4c84d --- /dev/null +++ b/lib/Service/Survivorship/SurvivorshipResolver.php @@ -0,0 +1,536 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Survivorship; + +use DateTimeImmutable; +use Throwable; + +/** + * Computes a golden record + per-attribute provenance from linked source + * records, driven entirely by the `x-openregister-survivorship` config. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#2.1 + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) The class owns the full + * pure resolution pipeline (candidate collection, tier ranking, tie-break, + * freshness decay, AND now per-object override short-circuiting) as one + * cohesive, side-effect-free algorithm per the spec; splitting the override + * short-circuit into a separate collaborator would scatter one resolution + * contract across two classes for a step that is intrinsically the last + * stage of the same pipeline (ADR-045 #E design.md D1/D2). + */ +class SurvivorshipResolver +{ + /** + * Default ordered tier list (weakest first) when the config omits one. + * + * @var array + */ + private const DEFAULT_TIER_ORDER = ['discard', 'bronze', 'silver', 'gold']; + + /** + * Default tier for an uncontested source with no matching trust row. + * + * @var string + */ + private const DEFAULT_TIER = 'bronze'; + + /** + * Default tier that is always excluded from the golden record. + * + * @var string + */ + private const DEFAULT_DISCARD_TIER = 'discard'; + + /** + * Resolve the golden record + attribute provenance for a set of linked + * source records. + * + * Never throws: a malformed source record (non-array, or one that raises + * while being processed) is simply skipped so one bad record cannot + * abort the whole resolution. Likewise a malformed override map (or + * malformed override entry) is skipped without throwing. + * + * @param string $entityType Entity type (passed through to the trust lookup; never hardcoded). + * @param array> $sourceRecords Linked source records. + * @param array $config The `x-openregister-survivorship` annotation. + * @param array> $trustRows Candidate trust-configuration rows. + * @param TrustTierResolver $trustResolver Pure trust-tier lookup + decay engine. + * @param DateTimeImmutable $asOf Reference instant for effectiveFrom + decay. + * @param mixed $overrides Optional per-object attribute-override map + * (`attribute => {value, overriddenBy, rationale?}` + * or a bare `attribute => value` shorthand); + * untrusted input, validated defensively. + * + * @return array{goldenRecord: array, attributeProvenance: array} + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.1 + */ + public function resolveGoldenRecord( + string $entityType, + array $sourceRecords, + array $config, + array $trustRows, + TrustTierResolver $trustResolver, + DateTimeImmutable $asOf, + mixed $overrides=null + ): array { + $tierOrder = $this->tierOrder(config: $config); + $defaultTier = (string) ($config['defaultTier'] ?? self::DEFAULT_TIER); + $discardTier = (string) ($config['discardTier'] ?? self::DEFAULT_DISCARD_TIER); + $anchorField = (string) ($config['freshnessAnchorField'] ?? ''); + + $candidates = $this->collectCandidates( + entityType: $entityType, + sourceRecords: $sourceRecords, + tierOrder: $tierOrder, + defaultTier: $defaultTier, + discardTier: $discardTier, + anchorField: $anchorField, + trustRows: $trustRows, + trustResolver: $trustResolver, + asOf: $asOf + ); + + $overrideMap = $this->normaliseOverrides(overrides: $overrides); + + $goldenRecord = []; + $provenance = []; + foreach ($candidates as $attribute => $options) { + $winner = $this->pickWinner(options: $options, tierOrder: $tierOrder, trustResolver: $trustResolver); + if ($winner === null) { + continue; + } + + $goldenRecord[$attribute] = $winner['value']; + $provenance[$attribute] = [ + 'value' => $winner['value'], + 'sourceSystem' => $winner['sourceSystem'], + 'trustTier' => $winner['trustTier'], + 'lastUpdated' => $winner['lastUpdated'], + ]; + } + + // Overrides short-circuit tier selection unconditionally — an override + // can also populate an attribute no source supplies at all. + foreach ($overrideMap as $attribute => $override) { + $goldenRecord[$attribute] = $override['value']; + $provenance[$attribute] = [ + 'value' => $override['value'], + 'override' => true, + 'overriddenBy' => $override['overriddenBy'], + 'rationale' => $override['rationale'], + ]; + } + + return [ + 'goldenRecord' => $goldenRecord, + 'attributeProvenance' => $provenance, + ]; + }//end resolveGoldenRecord() + + /** + * Normalise a raw override map into `attribute => {value, overriddenBy, + * rationale}` entries, silently dropping malformed entries. Accepts + * either the canonical `attribute => {value, overriddenBy?, rationale?}` + * shape or a bare `attribute => value` shorthand (`overriddenBy` then + * defaults to an empty string). + * + * @param mixed $overrides Raw override map (untrusted; may be non-array or malformed). + * + * @return array + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.1 + */ + private function normaliseOverrides(mixed $overrides): array + { + if (is_array($overrides) === false) { + return []; + } + + $clean = []; + foreach ($overrides as $attribute => $entry) { + if (is_string($attribute) === false || $attribute === '') { + continue; + } + + try { + $normalised = $this->normaliseOverrideEntry(entry: $entry); + } catch (Throwable) { + // A malformed override entry must never abort resolution of the rest. + continue; + } + + if ($normalised !== null) { + $clean[$attribute] = $normalised; + } + }//end foreach + + return $clean; + }//end normaliseOverrides() + + /** + * Normalise a single override entry, accepting either the canonical + * `{value, overriddenBy?, rationale?}` shape or a bare scalar-value + * shorthand. Returns null when the entry is malformed or its value is + * not present (the caller then drops it from the override map). + * + * @param mixed $entry Raw override entry (untrusted; may be malformed). + * + * @return array{value: mixed, overriddenBy: string, rationale: string|null}|null + * + * @spec openspec/changes/mdm-survivorship-override/tasks.md#1.1 + */ + private function normaliseOverrideEntry(mixed $entry): ?array + { + if (is_array($entry) === true) { + if (array_key_exists('value', $entry) === false || $this->isPresent(value: $entry['value']) === false) { + return null; + } + + $rationale = null; + if (($entry['rationale'] ?? null) !== null) { + $rationale = (string) $entry['rationale']; + } + + return [ + 'value' => $entry['value'], + 'overriddenBy' => (string) ($entry['overriddenBy'] ?? ''), + 'rationale' => $rationale, + ]; + } + + if ($this->isPresent(value: $entry) === true) { + return [ + 'value' => $entry, + 'overriddenBy' => '', + 'rationale' => null, + ]; + } + + return null; + }//end normaliseOverrideEntry() + + /** + * Collect, per attribute, the competing candidate values across all + * non-withdrawn source records with a non-empty value for that attribute. + * + * @param string $entityType Entity type. + * @param array $sourceRecords Linked source records; entries are + * untrusted external data and are + * re-validated as arrays below. + * @param array $tierOrder Ordered tier names, weakest first. + * @param string $defaultTier Fallback tier for an unmatched tuple. + * @param string $discardTier Tier excluded from competing. + * @param string $anchorField Field holding the freshness anchor date. + * @param array> $trustRows Candidate trust-configuration rows. + * @param TrustTierResolver $trustResolver Trust lookup + decay engine. + * @param DateTimeImmutable $asOf Reference instant. + * + * @return array>> Attribute => list of candidate options. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Sequential per-record/per-attribute guards + * (withdrawn, malformed, empty value, discard tier) each independently skip a candidate; + * splitting them further would scatter one linear collection pass across helpers. + * @SuppressWarnings(PHPMD.NPathComplexity) Same rationale — flat sequential guards. + */ + private function collectCandidates( + string $entityType, + array $sourceRecords, + array $tierOrder, + string $defaultTier, + string $discardTier, + string $anchorField, + array $trustRows, + TrustTierResolver $trustResolver, + DateTimeImmutable $asOf + ): array { + $candidates = []; + + foreach ($sourceRecords as $record) { + try { + if (is_array($record) === false) { + continue; + } + + if (($record['withdrawn'] ?? false) === true) { + continue; + } + + $values = ($record['values'] ?? $record['mappedAttributes'] ?? null); + if (is_array($values) === false) { + continue; + } + + $sourceSystem = (string) ($record['sourceSystem'] ?? ''); + + $anchorValue = null; + if ($anchorField !== '') { + $anchorValue = ($record[$anchorField] ?? null); + } + + $anchor = $this->parseDate(value: $anchorValue); + + foreach ($values as $attribute => $value) { + if ($this->isPresent(value: $value) === false) { + continue; + } + + $attribute = (string) $attribute; + + $tier = $trustResolver->resolveTier( + entityType: $entityType, + attribute: $attribute, + sourceSystem: $sourceSystem, + trustRows: $trustRows, + asOf: $asOf + ); + + $matched = ($tier !== null); + if ($matched === false) { + $tier = $defaultTier; + } + + $decayDays = null; + if ($matched === true) { + $decayDays = $this->decayDaysFor( + trustRows: $trustRows, + entityType: $entityType, + attribute: $attribute, + sourceSystem: $sourceSystem + ); + } + + $effectiveTier = $trustResolver->applyFreshnessDecay( + tier: $tier, + tierOrder: $tierOrder, + freshnessDecayDays: $decayDays, + anchor: $anchor, + asOf: $asOf + ); + + if ($effectiveTier === $discardTier) { + continue; + } + + $candidates[$attribute][] = [ + 'value' => $value, + 'sourceSystem' => $sourceSystem, + 'trustTier' => $effectiveTier, + 'lastUpdated' => ($anchor?->format(DATE_ATOM) ?? ''), + ]; + }//end foreach + } catch (Throwable) { + // A malformed source record must never abort resolution of the rest. + continue; + }//end try + }//end foreach + + return $candidates; + }//end collectCandidates() + + /** + * Pick the winning candidate for one attribute: highest tier rank, then + * (on a tie) the most recently updated source, compared as a parsed date. + * + * @param array> $options Candidate options for one attribute. + * @param array $tierOrder Ordered tier names, weakest first. + * @param TrustTierResolver $trustResolver Trust lookup + decay engine (for tierRank). + * + * @return array|null The winning candidate, or null when none are eligible. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#2.1 + */ + private function pickWinner(array $options, array $tierOrder, TrustTierResolver $trustResolver): ?array + { + $winner = null; + $winnerRank = -1; + $winnerDate = null; + + foreach ($options as $option) { + $rank = $trustResolver->tierRank(tier: (string) $option['trustTier'], tierOrder: $tierOrder); + if ($rank < 0) { + continue; + } + + $date = $this->parseDate(value: ($option['lastUpdated'] ?? null)); + + if ($winner === null || $rank > $winnerRank) { + $winner = $option; + $winnerRank = $rank; + $winnerDate = $date; + continue; + } + + if ($rank === $winnerRank && $this->isMoreRecent(candidate: $date, current: $winnerDate) === true) { + $winner = $option; + $winnerDate = $date; + } + }//end foreach + + return $winner; + }//end pickWinner() + + /** + * Compare two (possibly absent) parsed dates chronologically. An absent + * date never counts as more recent — this is the date-correct + * replacement for pipelinq's lexical `(string) $a > (string) $b` compare. + * + * @param DateTimeImmutable|null $candidate Candidate's parsed anchor date. + * @param DateTimeImmutable|null $current Current winner's parsed anchor date. + * + * @return bool True when `$candidate` is strictly more recent than `$current`. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#2.1 + */ + private function isMoreRecent(?DateTimeImmutable $candidate, ?DateTimeImmutable $current): bool + { + if ($candidate === null) { + return false; + } + + if ($current === null) { + return true; + } + + return $candidate->getTimestamp() > $current->getTimestamp(); + }//end isMoreRecent() + + /** + * Look up the `freshnessDecayDays` declared on the matching trust row(s) + * for a tuple. Mirrors the match used by `TrustTierResolver::resolveTier()` + * without duplicating its effectiveFrom-ranking logic — this reads the + * decay window off the same row set the tier itself was resolved from. + * + * @param array $trustRows Candidate trust-configuration rows; entries are + * untrusted external data and are re-validated as + * arrays below. + * @param string $entityType Entity type to match. + * @param string $attribute Attribute to match. + * @param string $sourceSystem Source system to match. + * + * @return int|null The decay window in days, or null when undeclared/unmatched. + */ + private function decayDaysFor(array $trustRows, string $entityType, string $attribute, string $sourceSystem): ?int + { + foreach ($trustRows as $row) { + if (is_array($row) === false) { + continue; + } + + if ((string) ($row['entityType'] ?? '') !== $entityType + || (string) ($row['attribute'] ?? '') !== $attribute + || (string) ($row['sourceSystem'] ?? '') !== $sourceSystem + ) { + continue; + } + + $days = ($row['freshnessDecayDays'] ?? null); + if (is_numeric($days) === true) { + return (int) $days; + } + } + + return null; + }//end decayDaysFor() + + /** + * Resolve the config's `tierOrder`, falling back to the default when + * absent or malformed. + * + * @param array $config The `x-openregister-survivorship` annotation. + * + * @return array Ordered tier names, weakest first. + */ + private function tierOrder(array $config): array + { + $declared = ($config['tierOrder'] ?? null); + if (is_array($declared) === false || count($declared) === 0) { + return self::DEFAULT_TIER_ORDER; + } + + $clean = []; + foreach ($declared as $tier) { + if (is_string($tier) === true && $tier !== '') { + $clean[] = $tier; + } + } + + if (count($clean) === 0) { + return self::DEFAULT_TIER_ORDER; + } + + return $clean; + }//end tierOrder() + + /** + * Determine whether a value counts as present (non-null, non-empty-string). + * + * @param mixed $value Value to test. + * + * @return bool True when the value is meaningfully populated. + */ + private function isPresent(mixed $value): bool + { + if ($value === null) { + return false; + } + + if (is_string($value) === true) { + return trim($value) !== ''; + } + + return true; + }//end isPresent() + + /** + * Parse a date-ish value into a DateTimeImmutable, never throwing. + * Unparseable/absent values return null (sorted as oldest by callers). + * + * @param mixed $value Candidate date value. + * + * @return DateTimeImmutable|null Parsed date, or null when absent/unparseable. + */ + private function parseDate(mixed $value): ?DateTimeImmutable + { + if (is_string($value) === false || $value === '') { + return null; + } + + try { + return new DateTimeImmutable($value); + } catch (Throwable) { + return null; + } + }//end parseDate() +}//end class diff --git a/lib/Service/Survivorship/TrustTierResolver.php b/lib/Service/Survivorship/TrustTierResolver.php new file mode 100644 index 0000000000..6beb6a7a46 --- /dev/null +++ b/lib/Service/Survivorship/TrustTierResolver.php @@ -0,0 +1,254 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Survivorship; + +use DateTimeImmutable; +use Throwable; + +/** + * Resolves the effective trust tier for an (entityType, attribute, sourceSystem) + * tuple, honouring `effectiveFrom` versioning and freshness decay. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#1.1 + */ +class TrustTierResolver +{ + /** + * Resolve the effective trust tier for a tuple as of a given date. + * + * Among the supplied candidate rows matching the tuple, the most recent + * row whose `effectiveFrom` is on or before `$asOf` wins. Rows with an + * `effectiveFrom` strictly after `$asOf` are ignored (future-dated). A + * row without `effectiveFrom` is treated as always-effective (earliest + * possible). When no row matches, returns null so the caller falls back + * to the annotation's `defaultTier`. + * + * @param string $entityType Entity type to match. + * @param string $attribute Attribute name to match. + * @param string $sourceSystem Source system to match. + * @param array> $trustRows Candidate trust-configuration rows. + * @param DateTimeImmutable $asOf Reference instant for effectiveFrom resolution. + * + * @return string|null The effective tier, or null when no row applies. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#1.1 + */ + public function resolveTier( + string $entityType, + string $attribute, + string $sourceSystem, + array $trustRows, + DateTimeImmutable $asOf + ): ?string { + $best = null; + $bestFrom = null; + + foreach ($trustRows as $row) { + $candidate = $this->eligibleCandidate( + row: $row, + entityType: $entityType, + attribute: $attribute, + sourceSystem: $sourceSystem, + asOf: $asOf + ); + if ($candidate === null) { + continue; + } + + [$tier, $rank] = $candidate; + if ($best === null || $rank >= $bestFrom) { + $best = $tier; + $bestFrom = $rank; + } + } + + return $best; + }//end resolveTier() + + /** + * Evaluate a single candidate row for the tuple: matches the tuple, is + * effective as of `$asOf`, and declares a non-empty tier. + * + * @param mixed $row Candidate row (validated to be an array here). + * @param string $entityType Entity type to match. + * @param string $attribute Attribute name to match. + * @param string $sourceSystem Source system to match. + * @param DateTimeImmutable $asOf Reference instant for effectiveFrom resolution. + * + * @return array{0: string, 1: int}|null [tier, effectiveFrom-rank], or null when ineligible. + */ + private function eligibleCandidate( + mixed $row, + string $entityType, + string $attribute, + string $sourceSystem, + DateTimeImmutable $asOf + ): ?array { + if (is_array($row) === false) { + return null; + } + + if ($this->matchesTuple(row: $row, entityType: $entityType, attribute: $attribute, sourceSystem: $sourceSystem) === false) { + return null; + } + + $effectiveFrom = $this->parseDate(value: ($row['effectiveFrom'] ?? null)); + if ($effectiveFrom !== null && $effectiveFrom > $asOf) { + // Future-dated row: not yet effective. + return null; + } + + $tier = (string) ($row['trustTier'] ?? ''); + if ($tier === '') { + return null; + } + + $rank = ($effectiveFrom?->getTimestamp() ?? PHP_INT_MIN); + + return [$tier, $rank]; + }//end eligibleCandidate() + + /** + * Apply freshness decay: step the tier down exactly one level on + * `tierOrder` when the elapsed time since `$anchor` exceeds + * `$freshnessDecayDays`. Pure and null-safe — a null/non-positive decay + * window, or a null anchor, leaves the tier unchanged. + * + * @param string $tier Starting tier. + * @param array $tierOrder Ordered tier names, weakest first. + * @param int|null $freshnessDecayDays Decay window in days, or null for no decay. + * @param DateTimeImmutable|null $anchor The source's freshness anchor date, or null. + * @param DateTimeImmutable $asOf Reference instant. + * + * @return string The (possibly stepped-down) tier. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#1.1 + */ + public function applyFreshnessDecay( + string $tier, + array $tierOrder, + ?int $freshnessDecayDays, + ?DateTimeImmutable $anchor, + DateTimeImmutable $asOf + ): string { + if ($freshnessDecayDays === null || $freshnessDecayDays <= 0 || $anchor === null) { + return $tier; + } + + $elapsedDays = (int) floor(($asOf->getTimestamp() - $anchor->getTimestamp()) / 86400); + if ($elapsedDays <= $freshnessDecayDays) { + return $tier; + } + + return $this->stepDown(tier: $tier, tierOrder: $tierOrder); + }//end applyFreshnessDecay() + + /** + * Step a tier down exactly one level on the ordered tier list (weakest + * first). A tier already at the weakest position, or not present on the + * list, stays unchanged. + * + * @param string $tier Starting tier. + * @param array $tierOrder Ordered tier names, weakest first. + * + * @return string The stepped-down tier. + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#1.1 + */ + public function stepDown(string $tier, array $tierOrder): string + { + $index = array_search($tier, $tierOrder, true); + if ($index === false || $index <= 0) { + return $tier; + } + + return (string) $tierOrder[((int) $index - 1)]; + }//end stepDown() + + /** + * Rank of a tier within the ordered tier list (higher = stronger). + * Unknown tiers rank lowest (-1) so they never win a comparison. + * + * @param string $tier Tier name. + * @param array $tierOrder Ordered tier names, weakest first. + * + * @return int Rank (0 = weakest declared tier; -1 = unknown). + * + * @spec openspec/changes/mdm-survivorship-engine/tasks.md#1.1 + */ + public function tierRank(string $tier, array $tierOrder): int + { + $index = array_search($tier, $tierOrder, true); + if ($index === false) { + return -1; + } + + return (int) $index; + }//end tierRank() + + /** + * Check whether a trust-configuration row matches the given tuple. + * + * @param array $row Candidate row. + * @param string $entityType Entity type to match. + * @param string $attribute Attribute to match. + * @param string $sourceSystem Source system to match. + * + * @return bool True when all three fields match. + */ + private function matchesTuple(array $row, string $entityType, string $attribute, string $sourceSystem): bool + { + return (string) ($row['entityType'] ?? '') === $entityType + && (string) ($row['attribute'] ?? '') === $attribute + && (string) ($row['sourceSystem'] ?? '') === $sourceSystem; + }//end matchesTuple() + + /** + * Parse a date-ish value into a DateTimeImmutable, never throwing. + * + * @param mixed $value Candidate date value. + * + * @return DateTimeImmutable|null Parsed date, or null when absent/unparseable. + */ + private function parseDate(mixed $value): ?DateTimeImmutable + { + if (is_string($value) === false || $value === '') { + return null; + } + + try { + return new DateTimeImmutable($value); + } catch (Throwable) { + return null; + } + }//end parseDate() +}//end class diff --git a/lib/Service/Sync/HarvestPipelineService.php b/lib/Service/Sync/HarvestPipelineService.php new file mode 100644 index 0000000000..b05e0e984a --- /dev/null +++ b/lib/Service/Sync/HarvestPipelineService.php @@ -0,0 +1,452 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\Sync; + +use OCA\OpenRegister\Db\Mapping; +use OCA\OpenRegister\Db\MappingMapper; +use OCA\OpenRegister\Db\Source; +use OCA\OpenRegister\Db\SyncRecord; +use OCA\OpenRegister\Db\SyncRecordMapper; +use OCA\OpenRegister\Service\MappingService; +use OCA\OpenRegister\Service\ObjectService; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Runs the gather -> fetch -> import pipeline for one source. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class HarvestPipelineService +{ + + /** + * Default batch size when a source does not configure one. + */ + public const DEFAULT_BATCH_SIZE = 50; + + /** + * Constructor. + * + * @param SyncRecordMapper $syncRecordMapper Per-record tracking persistence + * @param MappingMapper $mappingMapper Resolves the configured Mapping entity + * @param MappingService $mappingService Applies Twig field transformation + * @param ObjectService $objectService Validates + persists target objects + * @param SyncConflictResolver $conflictResolver Decides source/local conflicts + * @param LoggerInterface $logger Logger + */ + public function __construct( + private readonly SyncRecordMapper $syncRecordMapper, + private readonly MappingMapper $mappingMapper, + private readonly MappingService $mappingService, + private readonly ObjectService $objectService, + private readonly SyncConflictResolver $conflictResolver, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Run the full pipeline for a source. + * + * @param Source $source The source to harvest + * @param SourceFetcherInterface $fetcher Transport for gather/fetch + * @param string $executionId Unique id for this run + * @param string|null $since Incremental checkpoint (null = full) + * + * @return array Execution summary (counts + status) + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function run( + Source $source, + SourceFetcherInterface $fetcher, + string $executionId, + ?string $since=null + ): array { + $gathered = $this->gather(source: $source, fetcher: $fetcher, executionId: $executionId, since: $since); + $fetched = $this->fetchAll(source: $source, fetcher: $fetcher, records: $gathered); + + return $this->import(source: $source, records: $fetched, executionId: $executionId); + }//end run() + + /** + * Gather stage: identify records and create pending tracking rows. + * + * @param Source $source The source + * @param SourceFetcherInterface $fetcher Transport + * @param string $executionId Execution id + * @param string|null $since Incremental checkpoint + * + * @return SyncRecord[] The created pending tracking rows + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function gather( + Source $source, + SourceFetcherInterface $fetcher, + string $executionId, + ?string $since=null + ): array { + $externalIds = $fetcher->gather(source: $source, since: $since); + + $records = []; + foreach ($externalIds as $externalId) { + $records[] = $this->syncRecordMapper->createPending( + sourceId: (int) $source->getId(), + executionId: $executionId, + externalId: (string) $externalId, + organisation: $source->getOrganisation() + ); + } + + $this->logger->info( + message: sprintf('[HarvestPipeline] Gather complete: %d records identified', count($records)), + context: ['source' => $source->getUuid(), 'execution' => $executionId] + ); + + return $records; + }//end gather() + + /** + * Fetch stage: retrieve raw data for each pending record. + * + * Individual fetch failures are isolated: the record is marked + * fetch_error and the pipeline continues (partial-failure support). + * + * @param Source $source The source + * @param SourceFetcherInterface $fetcher Transport + * @param SyncRecord[] $records Pending records to fetch + * + * @return SyncRecord[] Records that fetched successfully + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function fetchAll(Source $source, SourceFetcherInterface $fetcher, array $records): array + { + $fetched = []; + foreach ($records as $record) { + try { + $raw = $fetcher->fetch(source: $source, externalId: (string) $record->getExternalId()); + $record->setRawData($raw); + $record->setAttempts((int) $record->getAttempts() + 1); + $this->syncRecordMapper->transitionStatus(record: $record, newStatus: SyncRecordStatus::FETCHED); + $fetched[] = $record; + } catch (Throwable $e) { + $record->setAttempts((int) $record->getAttempts() + 1); + $this->syncRecordMapper->transitionStatus( + record: $record, + newStatus: SyncRecordStatus::FETCH_ERROR, + errorMessage: $e->getMessage() + ); + $this->logger->warning( + message: sprintf('[HarvestPipeline] Fetch failed for %s: %s', $record->getExternalId(), $e->getMessage()) + ); + }//end try + }//end foreach + + return $fetched; + }//end fetchAll() + + /** + * Import stage: map, validate, resolve conflicts and persist. + * + * @param Source $source The source + * @param SyncRecord[] $records Fetched records to import + * @param string $executionId Execution id + * + * @return array Summary counts + overall status + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function import(Source $source, array $records, string $executionId): array + { + $summary = [ + 'executionId' => $executionId, + 'gathered' => count($records), + 'created' => 0, + 'updated' => 0, + 'unchanged' => 0, + 'conflicts' => 0, + 'errors' => 0, + ]; + + $mapping = $this->resolveMapping(source: $source); + + foreach ($records as $record) { + try { + $this->importRecord(source: $source, record: $record, mapping: $mapping, summary: $summary); + } catch (Throwable $e) { + $summary['errors']++; + $this->syncRecordMapper->transitionStatus( + record: $record, + newStatus: SyncRecordStatus::IMPORT_ERROR, + errorMessage: $e->getMessage() + ); + $this->logger->warning( + message: sprintf('[HarvestPipeline] Import failed for %s: %s', $record->getExternalId(), $e->getMessage()) + ); + } + } + + $summary['status'] = $this->deriveStatus(summary: $summary); + + $this->logger->info( + message: sprintf( + '[HarvestPipeline] Import complete: %d created, %d updated, %d unchanged, %d conflicts, %d errors', + $summary['created'], + $summary['updated'], + $summary['unchanged'], + $summary['conflicts'], + $summary['errors'] + ), + context: ['source' => $source->getUuid(), 'execution' => $executionId] + ); + + return $summary; + }//end import() + + /** + * Import a single fetched record, updating the summary in place. + * + * @param Source $source The source + * @param SyncRecord $record The fetched record + * @param Mapping|null $mapping The configured mapping (or null for pass-through) + * @param array $summary Running summary (by reference) + * + * @return void + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + private function importRecord(Source $source, SyncRecord $record, ?Mapping $mapping, array &$summary): void + { + $raw = ($record->getRawData() ?? []); + $mapped = $this->applyMapping(mapping: $mapping, raw: $raw); + $hash = $this->contentHash(data: $mapped); + + // Change detection against the previous synced version. + $previous = $this->syncRecordMapper->findByExternalId( + sourceId: (int) $source->getId(), + externalId: (string) $record->getExternalId() + ); + + // Find an existing local object via prior tracking (carries objectUuid). + $existingUuid = null; + if ($previous !== null && $previous->getId() !== $record->getId()) { + $existingUuid = $previous->getObjectUuid(); + if ($previous->getContentHash() === $hash) { + // Unchanged: skip the write. + $record->setContentHash($hash); + $record->setObjectUuid($existingUuid); + $this->syncRecordMapper->transitionStatus(record: $record, newStatus: SyncRecordStatus::UNCHANGED); + $summary['unchanged']++; + return; + } + } + + // Conflict resolution when an existing local object changed too. + if ($existingUuid !== null) { + $decision = $this->conflictResolver->resolve( + strategy: (string) ($source->getConflictStrategy() ?? SyncConflictResolver::SOURCE_WINS), + localChanged: $this->localChangedSinceSync(source: $source, objectUuid: $existingUuid, previous: $previous) + ); + + if ($decision === SyncConflictResolver::KEEP_LOCAL) { + $record->setContentHash($hash); + $record->setObjectUuid($existingUuid); + $this->syncRecordMapper->transitionStatus(record: $record, newStatus: SyncRecordStatus::UNCHANGED); + $summary['unchanged']++; + return; + } + + if ($decision === SyncConflictResolver::DEFER) { + $record->setContentHash($hash); + $record->setObjectUuid($existingUuid); + $this->syncRecordMapper->transitionStatus(record: $record, newStatus: SyncRecordStatus::CONFLICT); + $summary['conflicts']++; + return; + } + }//end if + + // Persist via ObjectService (validates against the target schema). + $isUpdate = ($existingUuid !== null); + $saved = $this->objectService->saveObject( + object: $mapped, + register: $source->getTargetRegister(), + schema: $source->getTargetSchema(), + uuid: $existingUuid + ); + + $savedUuid = $existingUuid; + try { + $candidate = $saved->getUuid(); + if (is_string($candidate) === true && $candidate !== '') { + $savedUuid = $candidate; + } + } catch (Throwable $e) { + // Fall back to the existing uuid when the entity cannot report one. + } + + $record->setContentHash($hash); + $record->setObjectUuid($savedUuid); + $this->syncRecordMapper->transitionStatus(record: $record, newStatus: SyncRecordStatus::IMPORTED); + + if ($isUpdate === true) { + $summary['updated']++; + } else { + $summary['created']++; + } + }//end importRecord() + + /** + * Apply the configured mapping to a raw record (pass-through when none). + * + * @param Mapping|null $mapping The mapping entity + * @param array $raw The raw record + * + * @return array The mapped record + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function applyMapping(?Mapping $mapping, array $raw): array + { + if ($mapping === null) { + return $raw; + } + + return $this->mappingService->executeMapping(mapping: $mapping, input: $raw); + }//end applyMapping() + + /** + * Deterministic content hash for change detection. + * + * @param array $data The mapped record + * + * @return string A stable SHA-256 hash + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function contentHash(array $data): string + { + ksort($data); + + return hash('sha256', (string) json_encode($data)); + }//end contentHash() + + /** + * Derive the overall execution status from the summary counts. + * + * @param array $summary The summary + * + * @return string success | partial | failed + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function deriveStatus(array $summary): string + { + $errors = (int) ($summary['errors'] ?? 0); + $created = (int) ($summary['created'] ?? 0); + $updated = (int) ($summary['updated'] ?? 0); + $unchanged = (int) ($summary['unchanged'] ?? 0); + $conflicts = (int) ($summary['conflicts'] ?? 0); + $processed = ($created + $updated + $unchanged + $conflicts); + + if ($errors === 0) { + return 'success'; + } + + if ($processed === 0) { + return 'failed'; + } + + return 'partial'; + }//end deriveStatus() + + /** + * Resolve the configured Mapping entity for a source, if any. + * + * @param Source $source The source + * + * @return Mapping|null The mapping or null when none/unresolvable + */ + private function resolveMapping(Source $source): ?Mapping + { + $mappingId = $source->getMappingId(); + if ($mappingId === null) { + return null; + } + + try { + return $this->mappingMapper->find($mappingId); + } catch (Throwable $e) { + $this->logger->warning( + message: sprintf('[HarvestPipeline] Mapping %d not resolvable: %s', $mappingId, $e->getMessage()) + ); + return null; + } + }//end resolveMapping() + + /** + * Whether the local object changed since the last sync wrote it. + * + * Compares the current local object's content hash to the hash stored + * on the previous sync record. Absence of a previous hash is treated as + * "not changed" so a first-time match does not spuriously conflict. + * + * @param Source $source The source + * @param string $objectUuid The local object uuid + * @param SyncRecord|null $previous The previous tracking record + * + * @return bool True when the local object diverged from the last sync + */ + private function localChangedSinceSync(Source $source, string $objectUuid, ?SyncRecord $previous): bool + { + if ($previous === null || $previous->getContentHash() === null) { + return false; + } + + try { + $local = $this->objectService->find( + id: $objectUuid, + register: $source->getTargetRegister(), + schema: $source->getTargetSchema() + ); + } catch (Throwable $e) { + return false; + } + + if ($local === null) { + return false; + } + + $localHash = $this->contentHash(data: $local->getObject()); + + return $localHash !== $previous->getContentHash(); + }//end localChangedSinceSync() +}//end class diff --git a/lib/Service/Sync/RestApiSourceFetcher.php b/lib/Service/Sync/RestApiSourceFetcher.php new file mode 100644 index 0000000000..d76aa0ec19 --- /dev/null +++ b/lib/Service/Sync/RestApiSourceFetcher.php @@ -0,0 +1,309 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\Sync; + +use GuzzleHttp\Client; +use OCA\OpenRegister\Db\Source; +use OCP\Security\ICrypto; +use Psr\Log\LoggerInterface; +use RuntimeException; + +/** + * REST/OpenRegister transport for the harvest pipeline. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class RestApiSourceFetcher implements SourceFetcherInterface +{ + + /** + * Maximum pages to walk during Gather (runaway guard). + */ + private const MAX_PAGES = 1000; + + /** + * Constructor. + * + * @param Client $httpClient HTTP client + * @param ICrypto $crypto Credential decryption + * @param LoggerInterface $logger Logger + */ + public function __construct( + private readonly Client $httpClient, + private readonly ICrypto $crypto, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * {@inheritDoc} + * + * @param string $type The source type + * + * @return bool True for rest-api and openregister + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function supports(string $type): bool + { + return in_array($type, ['rest-api', 'openregister'], true); + }//end supports() + + /** + * {@inheritDoc} + * + * @param Source $source The source + * @param string|null $since Incremental checkpoint (ISO-8601 timestamp), or null + * + * @return list External record identifiers + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function gather(Source $source, ?string $since=null): array + { + $baseUrl = (string) $source->getDatabaseUrl(); + if ($baseUrl === '') { + throw new RuntimeException('Source has no endpoint URL configured.'); + } + + $headers = $this->buildHeaders(source: $source, since: $since); + $idField = $this->identifierField(source: $source); + + $ids = []; + $url = $baseUrl; + $page = 0; + + while ($url !== null && $page < self::MAX_PAGES) { + $page++; + // Bound each request so a hung upstream cannot stall the harvest job + // indefinitely (harden-harvest-http). Guzzle defaults to no timeout. + $response = $this->httpClient->request( + 'GET', + $url, + ['headers' => $headers, 'timeout' => 30, 'connect_timeout' => 5] + ); + + // 304 Not Modified: nothing changed since the checkpoint. + if ($response->getStatusCode() === 304) { + break; + } + + $body = json_decode((string) $response->getBody(), true); + if (is_array($body) === false) { + break; + } + + $items = $this->extractItems(body: $body); + foreach ($items as $item) { + if (is_array($item) === true && isset($item[$idField]) === true) { + $ids[] = (string) $item[$idField]; + } + } + + $url = $this->nextPageUrl(body: $body, response: $response); + }//end while + + return array_values(array_unique($ids)); + }//end gather() + + /** + * {@inheritDoc} + * + * @param Source $source The source + * @param string $externalId The external record id + * + * @return array The raw record + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function fetch(Source $source, string $externalId): array + { + $baseUrl = rtrim((string) $source->getDatabaseUrl(), '/'); + $headers = $this->buildHeaders(source: $source, since: null); + $url = $baseUrl.'/'.rawurlencode($externalId); + // Bound the request (harden-harvest-http): a hung upstream must not stall + // the harvest job. + $response = $this->httpClient->request( + 'GET', + $url, + ['headers' => $headers, 'timeout' => 30, 'connect_timeout' => 5] + ); + $body = json_decode((string) $response->getBody(), true); + + if (is_array($body) === false) { + throw new RuntimeException(sprintf('Non-JSON response fetching record %s.', $externalId)); + } + + return $body; + }//end fetch() + + /** + * Build request headers including auth and conditional-request headers. + * + * @param Source $source The source + * @param string|null $since ISO-8601 checkpoint for If-Modified-Since, or null + * + * @return array The headers + */ + private function buildHeaders(Source $source, ?string $since): array + { + $headers = ['Accept' => 'application/json']; + + $auth = $this->decryptAuthConfig(source: $source); + switch ((string) $source->getAuthType()) { + case 'apikey': + $headerName = (string) ($auth['header'] ?? 'X-Api-Key'); + $headers[$headerName] = (string) ($auth['key'] ?? ''); + break; + + case 'basic': + $headers['Authorization'] = 'Basic '.base64_encode( + (string) ($auth['username'] ?? '').':'.(string) ($auth['password'] ?? '') + ); + break; + + default: + // None / unsupported-here: no auth header. + break; + } + + if ($since !== null) { + $ts = strtotime($since); + if ($ts !== false) { + $headers['If-Modified-Since'] = gmdate('D, d M Y H:i:s', $ts).' GMT'; + } + } + + return $headers; + }//end buildHeaders() + + /** + * Decrypt the source's auth config blob. + * + * @param Source $source The source + * + * @return array The decrypted credentials (empty on failure) + */ + private function decryptAuthConfig(Source $source): array + { + $config = $source->getAuthConfig(); + if (is_array($config) === false || $config === []) { + return []; + } + + $decrypted = []; + foreach ($config as $key => $value) { + if (is_string($value) === false || $value === '') { + $decrypted[$key] = $value; + continue; + } + + try { + $decrypted[$key] = $this->crypto->decrypt($value); + } catch (\Throwable $e) { + // Value was not encrypted (or key rotated): use as-is. + $decrypted[$key] = $value; + } + } + + return $decrypted; + }//end decryptAuthConfig() + + /** + * Determine the identifier field name for gathered records. + * + * @param Source $source The source + * + * @return string The id field (defaults to 'id') + */ + private function identifierField(Source $source): string + { + $config = $source->getConfiguration(); + if (is_array($config) === true && isset($config['identifierField']) === true) { + return (string) $config['identifierField']; + } + + return 'id'; + }//end identifierField() + + /** + * Extract the list of items from a collection response body. + * + * Supports common envelopes: results[], items[], data[], or a bare array. + * + * @param array|list $body The decoded body + * + * @return list The items + */ + private function extractItems(array $body): array + { + foreach (['results', 'items', 'data', 'value'] as $key) { + if (isset($body[$key]) === true && is_array($body[$key]) === true) { + return array_values($body[$key]); + } + } + + // Bare list of records. + if (array_is_list($body) === true) { + return $body; + } + + return []; + }//end extractItems() + + /** + * Resolve the next-page URL from the body or Link header. + * + * @param array $body The decoded body + * @param mixed $response The PSR-7 response + * + * @return string|null The next URL or null when exhausted + */ + private function nextPageUrl(array $body, $response): ?string + { + // _links.next.href or next at top level. + if (isset($body['_links']['next']['href']) === true) { + return (string) $body['_links']['next']['href']; + } + + if (isset($body['next']) === true && is_string($body['next']) === true && $body['next'] !== '') { + return $body['next']; + } + + // Link: ; rel="next". + $link = $response->getHeaderLine('Link'); + if ($link !== '' && preg_match('/<([^>]+)>;\s*rel="next"/', $link, $m) === 1) { + return $m[1]; + } + + return null; + }//end nextPageUrl() +}//end class diff --git a/lib/Service/Sync/SourceFetcherInterface.php b/lib/Service/Sync/SourceFetcherInterface.php new file mode 100644 index 0000000000..7b1f47687f --- /dev/null +++ b/lib/Service/Sync/SourceFetcherInterface.php @@ -0,0 +1,76 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\Sync; + +use OCA\OpenRegister\Db\Source; + +/** + * Transport contract for a sync source. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +interface SourceFetcherInterface +{ + /** + * Whether this fetcher can handle the given source type. + * + * @param string $type The source type (rest-api, odata, soap, csv-file, openregister) + * + * @return bool True when supported + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function supports(string $type): bool; + + /** + * Gather stage: return the list of external record identifiers to process. + * + * Implementations MUST honour incremental sync when $since is provided, + * returning only records modified since that checkpoint where the source + * supports it. + * + * @param Source $source The source being synced + * @param string|null $since Incremental checkpoint (ISO-8601 timestamp or delta token), or null for full sync + * + * @return list External record identifiers + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function gather(Source $source, ?string $since=null): array; + + /** + * Fetch stage: retrieve the full raw payload for a single record. + * + * @param Source $source The source being synced + * @param string $externalId The external record identifier + * + * @return array The raw record payload + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function fetch(Source $source, string $externalId): array; +}//end interface diff --git a/lib/Service/Sync/SourceFetcherRegistry.php b/lib/Service/Sync/SourceFetcherRegistry.php new file mode 100644 index 0000000000..302763790e --- /dev/null +++ b/lib/Service/Sync/SourceFetcherRegistry.php @@ -0,0 +1,88 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\Sync; + +/** + * Registry mapping source types to fetchers. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +class SourceFetcherRegistry +{ + + /** + * Registered fetchers. + * + * @var list + */ + private array $fetchers = []; + + /** + * Constructor. + * + * @param iterable $fetchers Initial fetchers (DI-provided) + */ + public function __construct(iterable $fetchers=[]) + { + foreach ($fetchers as $fetcher) { + $this->register(fetcher: $fetcher); + } + }//end __construct() + + /** + * Register a fetcher. + * + * @param SourceFetcherInterface $fetcher The fetcher + * + * @return void + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function register(SourceFetcherInterface $fetcher): void + { + $this->fetchers[] = $fetcher; + }//end register() + + /** + * Get a fetcher for the given source type. + * + * @param string $type The source type + * + * @return SourceFetcherInterface|null The first supporting fetcher, or null + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function get(string $type): ?SourceFetcherInterface + { + foreach ($this->fetchers as $fetcher) { + if ($fetcher->supports($type) === true) { + return $fetcher; + } + } + + return null; + }//end get() +}//end class diff --git a/lib/Service/Sync/SyncConflictResolver.php b/lib/Service/Sync/SyncConflictResolver.php new file mode 100644 index 0000000000..3f99981054 --- /dev/null +++ b/lib/Service/Sync/SyncConflictResolver.php @@ -0,0 +1,174 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\Sync; + +use DateTimeInterface; + +/** + * Resolves source-vs-local conflicts using a configurable strategy. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +final class SyncConflictResolver +{ + + /** + * Source data overwrites local changes. + */ + public const SOURCE_WINS = 'source-wins'; + + /** + * Local changes are kept; the source update is skipped. + */ + public const LOCAL_WINS = 'local-wins'; + + /** + * The most recently modified side wins. + */ + public const NEWEST_WINS = 'newest-wins'; + + /** + * Conflict is flagged for manual administrator resolution. + */ + public const MANUAL = 'manual'; + + /** + * Resolution outcome: apply the source data. + */ + public const APPLY_SOURCE = 'apply_source'; + + /** + * Resolution outcome: keep the local data (skip the update). + */ + public const KEEP_LOCAL = 'keep_local'; + + /** + * Resolution outcome: defer to a human (flag as conflict). + */ + public const DEFER = 'defer'; + + /** + * All recognised strategy values. + * + * @return list The known strategies + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public static function strategies(): array + { + return [self::SOURCE_WINS, self::LOCAL_WINS, self::NEWEST_WINS, self::MANUAL]; + }//end strategies() + + /** + * Whether a value is a recognised conflict strategy. + * + * @param string $strategy The strategy to check + * + * @return bool True when the strategy is recognised + */ + public function isValidStrategy(string $strategy): bool + { + return in_array($strategy, self::strategies(), true); + }//end isValidStrategy() + + /** + * Decide the outcome for a record. + * + * A conflict only exists when BOTH sides changed since the last sync. + * When the local copy did not change, the source update is applied + * regardless of strategy (there is nothing to conflict with). + * + * @param string $strategy Configured strategy (one of the *_WINS / MANUAL constants) + * @param bool $localChanged Whether the local object changed since the last sync + * @param DateTimeInterface|null $sourceModified Source-side last-modified timestamp (for newest-wins) + * @param DateTimeInterface|null $localModified Local-side last-modified timestamp (for newest-wins) + * + * @return string One of APPLY_SOURCE, KEEP_LOCAL, DEFER + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function resolve( + string $strategy, + bool $localChanged, + ?DateTimeInterface $sourceModified=null, + ?DateTimeInterface $localModified=null + ): string { + // Unknown strategy falls back to the safest default: defer to a human. + if ($this->isValidStrategy(strategy: $strategy) === false) { + return self::DEFER; + } + + // No competing local edit: there is no conflict, just apply the source. + if ($localChanged === false) { + return self::APPLY_SOURCE; + } + + switch ($strategy) { + case self::SOURCE_WINS: + return self::APPLY_SOURCE; + + case self::LOCAL_WINS: + return self::KEEP_LOCAL; + + case self::NEWEST_WINS: + return $this->resolveNewest(sourceModified: $sourceModified, localModified: $localModified); + + case self::MANUAL: + default: + return self::DEFER; + }//end switch + }//end resolve() + + /** + * Compare timestamps for the newest-wins strategy. + * + * @param DateTimeInterface|null $sourceModified Source-side timestamp + * @param DateTimeInterface|null $localModified Local-side timestamp + * + * @return string APPLY_SOURCE, KEEP_LOCAL, or DEFER when undecidable + */ + private function resolveNewest(?DateTimeInterface $sourceModified, ?DateTimeInterface $localModified): string + { + // Cannot compare without both timestamps; defer rather than guess. + if ($sourceModified === null || $localModified === null) { + return self::DEFER; + } + + $sourceTs = $sourceModified->getTimestamp(); + $localTs = $localModified->getTimestamp(); + + if ($sourceTs > $localTs) { + return self::APPLY_SOURCE; + } + + if ($localTs > $sourceTs) { + return self::KEEP_LOCAL; + } + + // Exact tie: prefer the source so sync remains convergent. + return self::APPLY_SOURCE; + }//end resolveNewest() +}//end class diff --git a/lib/Service/Sync/SyncRecordStatus.php b/lib/Service/Sync/SyncRecordStatus.php new file mode 100644 index 0000000000..7109aa6ca9 --- /dev/null +++ b/lib/Service/Sync/SyncRecordStatus.php @@ -0,0 +1,177 @@ + fetch -> import). Kept as pure logic so the state + * machine can be unit-tested without a live database or HTTP client. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @category Service + * @package OCA\OpenRegister\Service\Sync + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\Sync; + +/** + * Per-record sync status state machine. + * + * Mirrors CKAN's harvest object status model: records move forward + * through the three pipeline stages and may end in a terminal state. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +final class SyncRecordStatus +{ + + /** + * Record identified during the Gather stage, awaiting fetch. + */ + public const PENDING = 'pending'; + + /** + * Full record data retrieved during the Fetch stage. + */ + public const FETCHED = 'fetched'; + + /** + * Fetch failed for this record (transient or hard error). + */ + public const FETCH_ERROR = 'fetch_error'; + + /** + * Record successfully created or updated in the target register. + */ + public const IMPORTED = 'imported'; + + /** + * Record content unchanged since the last sync; no write performed. + */ + public const UNCHANGED = 'unchanged'; + + /** + * Import failed (schema validation or persistence error). + */ + public const IMPORT_ERROR = 'import_error'; + + /** + * Source and local copy both changed; awaiting manual resolution. + */ + public const CONFLICT = 'conflict'; + + /** + * Record skipped (e.g. filtered out or beyond the execution limit). + */ + public const SKIPPED = 'skipped'; + + /** + * Record failed after all retries were exhausted. + */ + public const PERMANENT_ERROR = 'permanent_error'; + + /** + * Allowed forward transitions keyed by current status. + * + * @var array> + */ + private const TRANSITIONS = [ + self::PENDING => [self::FETCHED, self::FETCH_ERROR, self::SKIPPED], + self::FETCHED => [self::IMPORTED, self::UNCHANGED, self::IMPORT_ERROR, self::CONFLICT, self::SKIPPED], + self::FETCH_ERROR => [self::FETCHED, self::PERMANENT_ERROR, self::SKIPPED], + self::IMPORT_ERROR => [self::IMPORTED, self::PERMANENT_ERROR, self::SKIPPED], + self::CONFLICT => [self::IMPORTED, self::UNCHANGED, self::SKIPPED], + // Terminal states. + self::IMPORTED => [], + self::UNCHANGED => [], + self::SKIPPED => [], + self::PERMANENT_ERROR => [], + ]; + + /** + * All valid status values. + * + * @return list The known statuses + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public static function all(): array + { + return [ + self::PENDING, + self::FETCHED, + self::FETCH_ERROR, + self::IMPORTED, + self::UNCHANGED, + self::IMPORT_ERROR, + self::CONFLICT, + self::SKIPPED, + self::PERMANENT_ERROR, + ]; + }//end all() + + /** + * Whether a value is a known status. + * + * @param string $status The status to check + * + * @return bool True when the status is recognised + */ + public static function isValid(string $status): bool + { + return in_array($status, self::all(), true); + }//end isValid() + + /** + * Whether a status is terminal (no further transitions allowed). + * + * @param string $status The status to check + * + * @return bool True when the status is terminal + */ + public static function isTerminal(string $status): bool + { + return isset(self::TRANSITIONS[$status]) === true && self::TRANSITIONS[$status] === []; + }//end isTerminal() + + /** + * Whether a status counts as an error outcome. + * + * @param string $status The status to check + * + * @return bool True when the status represents a failure + */ + public static function isError(string $status): bool + { + return in_array($status, [self::FETCH_ERROR, self::IMPORT_ERROR, self::PERMANENT_ERROR], true); + }//end isError() + + /** + * Whether moving from one status to another is allowed. + * + * @param string $from The current status + * @param string $to The target status + * + * @return bool True when the transition is permitted + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public static function canTransition(string $from, string $to): bool + { + if (self::isValid(status: $from) === false || self::isValid(status: $to) === false) { + return false; + } + + return in_array($to, (self::TRANSITIONS[$from] ?? []), true); + }//end canTransition() +}//end class diff --git a/lib/Service/Sync/SyncScheduleService.php b/lib/Service/Sync/SyncScheduleService.php new file mode 100644 index 0000000000..2db0a5d559 --- /dev/null +++ b/lib/Service/Sync/SyncScheduleService.php @@ -0,0 +1,109 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\Sync; + +use DateTimeInterface; +use OCA\OpenRegister\Db\Source; + +/** + * Determines which sources are due for sync at a given moment. + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ +final class SyncScheduleService +{ + + /** + * Default interval (hours) used when a source has sync enabled but no + * explicit interval configured. + */ + public const DEFAULT_INTERVAL_HOURS = 24; + + /** + * Whether a single source is due for synchronisation at $now. + * + * A source is due when: + * - sync is enabled, AND + * - it is not already running (last status != 'running'), AND + * - it has never synced, OR the configured interval has elapsed. + * + * @param Source $source The source to evaluate + * @param DateTimeInterface $now The reference moment + * + * @return bool True when the source should be queued for sync + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function isDueForSync(Source $source, DateTimeInterface $now): bool + { + if ($source->getSyncEnabled() !== true) { + return false; + } + + // Skip sources whose previous execution is still in progress + // (overlap protection). + if ($source->getLastSyncStatus() === 'running') { + return false; + } + + $lastSync = $source->getLastSyncDate(); + if ($lastSync === null) { + // Never synced: due immediately. + return true; + } + + $intervalHours = $source->getSyncInterval(); + if ($intervalHours === null || $intervalHours <= 0) { + $intervalHours = self::DEFAULT_INTERVAL_HOURS; + } + + $hoursPassed = ($now->getTimestamp() - $lastSync->getTimestamp()) / 3600; + + return $hoursPassed >= $intervalHours; + }//end isDueForSync() + + /** + * Select the subset of sources that are due for sync at $now. + * + * @param array $sources The candidate sources + * @param DateTimeInterface $now The reference moment + * + * @return list The sources that are due + * + * @spec openspec/specs/data-sync-harvesting/spec.md + */ + public function selectDueSources(array $sources, DateTimeInterface $now): array + { + $due = []; + foreach ($sources as $source) { + if ($this->isDueForSync(source: $source, now: $now) === true) { + $due[] = $source; + } + } + + return $due; + }//end selectDueSources() +}//end class diff --git a/lib/Service/SystemOperationContext.php b/lib/Service/SystemOperationContext.php new file mode 100644 index 0000000000..88efc05cc9 --- /dev/null +++ b/lib/Service/SystemOperationContext.php @@ -0,0 +1,93 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +final class SystemOperationContext +{ + + /** + * Nesting depth of active system-operation scopes. + * + * A depth counter (not a boolean) so nested run() calls compose: the + * elevation only ends when the OUTERMOST scope exits. + * + * @var integer + */ + private static int $depth = 0; + + /** + * This class is a static scope holder and must not be instantiated. + */ + private function __construct() + { + }//end __construct() + + /** + * Run a callable inside a trusted system-operation scope. + * + * While the callable executes, RBAC checks in MultiTenancyTrait and + * PermissionHandler treat the caller as a trusted system principal — + * mirroring the existing CLI trust. The scope is released in a finally + * block, so an exception inside the operation cannot leak elevation. + * + * @param callable $operation The trusted operation to execute. + * + * @return mixed Whatever the callable returns. + * + * @spec openspec/specs/faceting-configuration/spec.md + */ + public static function run(callable $operation) + { + self::$depth++; + + try { + return $operation(); + } finally { + self::$depth--; + } + }//end run() + + /** + * Whether a system-operation scope is currently active. + * + * @return bool True when executing inside run(). + */ + public static function isActive(): bool + { + return self::$depth > 0; + }//end isActive() +}//end class diff --git a/lib/Service/TalkLinkService.php b/lib/Service/TalkLinkService.php index 84c26d18c5..f1da7f8957 100644 --- a/lib/Service/TalkLinkService.php +++ b/lib/Service/TalkLinkService.php @@ -208,7 +208,9 @@ public function unlinkRoom(string $objectUuid, string $roomToken): void * * @return array> * - * @SuppressWarnings(PHPMD.LongVariable) $refreshedLastMessageData is the only name that clearly distinguishes the refreshed message payload from $link->getLastMessageData(); a shorter alias would lose that semantic clarity in the stale-cache refresh path. + * @SuppressWarnings(PHPMD.LongVariable) $refreshedLastMessageData is the only name that clearly + * distinguishes the refreshed message payload from $link->getLastMessageData(); a shorter alias + * would lose that semantic clarity in the stale-cache refresh path. * * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-2/tasks.md#task-2 */ @@ -280,9 +282,17 @@ public function getLinkedRooms(string $objectUuid): array * @throws Exception On missing user, Talk unavailable, invalid * type, or create failure. * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) createAndLinkRoom() guards user auth, room type validity, Talk availability, createRoom success, token extraction, description (best-effort), and participant invite (best-effort); each is a sequential guard required for the create+link contract. - * @SuppressWarnings(PHPMD.NPathComplexity) Each of the best-effort optional paths (description, participant invite, display-name fallback) contributes to NPath independently; all are correct-by-contract Try/catch degradations. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) createAndLinkRoom() creates the room, optionally sets description and invites the owner, extracts the token, builds cached metadata, and persists the link row in one transactional sequence; splitting would scatter the atomic create+link operation. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) createAndLinkRoom() guards user auth, room type + * validity, Talk availability, createRoom success, token extraction, description (best-effort), + * and participant invite (best-effort); each is a sequential guard required for the create+link + * contract. + * @SuppressWarnings(PHPMD.NPathComplexity) Each of the best-effort optional paths (description, + * participant invite, display-name fallback) contributes to NPath independently; all are + * correct-by-contract Try/catch degradations. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) createAndLinkRoom() creates the room, optionally + * sets description and invites the owner, extracts the token, builds cached metadata, and persists + * the link row in one transactional sequence; splitting would scatter the atomic create+link + * operation. * * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-2/tasks.md#task-2 */ @@ -414,9 +424,17 @@ public function createAndLinkRoom( * * @return array * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) getAvailableRoomsForUser() iterates rooms and per-room wraps every Talk Entity::__call getter (token, name, displayName, type, participantCount) in try/catch; each is a cross-version compatibility guard required because Talk's Room entity API changed between releases. - * @SuppressWarnings(PHPMD.NPathComplexity) Per-room: empty-token skip + search filter + changelog/note-to-self type exclude + displayName override + participantCount resolve each independently expand NPath; all are valid filtering/degradation paths. - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) getAvailableRoomsForUser() must wrap every Talk getter individually in try/catch because Talk's Room entity uses Entity::__call magic and accessor availability varies across Talk releases; extracting helpers for each getter would scatter the version-compatibility logic. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) getAvailableRoomsForUser() iterates rooms and + * per-room wraps every Talk Entity::__call getter (token, name, displayName, type, participantCount) + * in try/catch; each is a cross-version compatibility guard required because Talk's Room entity API + * changed between releases. + * @SuppressWarnings(PHPMD.NPathComplexity) Per-room: empty-token skip + search filter + + * changelog/note-to-self type exclude + displayName override + participantCount resolve each + * independently expand NPath; all are valid filtering/degradation paths. + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) getAvailableRoomsForUser() must wrap every Talk + * getter individually in try/catch because Talk's Room entity uses Entity::__call magic and accessor + * availability varies across Talk releases; extracting helpers for each getter would scatter the + * version-compatibility logic. * * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-2/tasks.md#task-2 */ @@ -674,8 +692,13 @@ private function stripMarker(string $name): string * * @return array{roomId:?int,roomName:?string,roomType:?int,subtitle:?string,participantCount:?int,lastMessage:?array,lastActivity:?DateTime} * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) extractRoomFields() wraps every Talk Room getter (id, name, displayName, type, participantCount, lastActivity, lastMessage) in individual try/catch blocks; each is a required cross-version guard because Talk's Room entity uses Entity::__call magic and accessor availability varies. - * @SuppressWarnings(PHPMD.NPathComplexity) Each optional accessor (displayName, participantService, lastActivity, lastMessage) contributes independent true/false paths; all are required degradation guards for older Talk releases. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) extractRoomFields() wraps every Talk Room getter + * (id, name, displayName, type, participantCount, lastActivity, lastMessage) in individual + * try/catch blocks; each is a required cross-version guard because Talk's Room entity uses + * Entity::__call magic and accessor availability varies. + * @SuppressWarnings(PHPMD.NPathComplexity) Each optional accessor (displayName, participantService, + * lastActivity, lastMessage) contributes independent true/false paths; all are required degradation + * guards for older Talk releases. */ private function extractRoomFields(object $room, string $userUid): array { @@ -815,8 +838,11 @@ private function buildSubtitle(object $room, ?int $type): ?string * * @return array{actor:array{type:string,id:string},text:string,timestamp:?int}|null * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) buildLastMessage() handles four lastMessage shapes returned by different Talk releases (IComment, Message entity, plain object, or null); each shape requires distinct accessor paths that cannot be merged. - * @SuppressWarnings(PHPMD.NPathComplexity) The four lastMessage shapes × actor-type detection × actor-id extraction produce many paths; all are required for cross-version Talk compatibility. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) buildLastMessage() handles four lastMessage shapes + * returned by different Talk releases (IComment, Message entity, plain object, or null); each shape + * requires distinct accessor paths that cannot be merged. + * @SuppressWarnings(PHPMD.NPathComplexity) The four lastMessage shapes × actor-type detection × + * actor-id extraction produce many paths; all are required for cross-version Talk compatibility. */ private function buildLastMessage(object $room): ?array { diff --git a/lib/Service/TaskService.php b/lib/Service/TaskService.php index 4a613de95f..ee4eeceff1 100644 --- a/lib/Service/TaskService.php +++ b/lib/Service/TaskService.php @@ -18,7 +18,7 @@ * @version GIT: * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-annotate-openregister-2026-04-30/tasks.md#task-61 + * @spec openspec/specs/object-interactions/spec.md */ declare(strict_types=1); @@ -29,6 +29,7 @@ use Exception; use OCA\DAV\CalDAV\CalDavBackend; use OCA\OpenRegister\Exception\NoVtodoCalendarException; +use OCP\IURLGenerator; use OCP\IUserSession; use Psr\Log\LoggerInterface; use Sabre\VObject\Reader; @@ -71,23 +72,33 @@ class TaskService */ private readonly LoggerInterface $logger; + /** + * URL generator (webroot-aware deep links). + * + * @var IURLGenerator + */ + private readonly IURLGenerator $urlGenerator; + /** * Constructor. * * @param CalDavBackend $calDavBackend CalDAV backend for VTODO operations * @param IUserSession $userSession User session for current user context * @param LoggerInterface $logger Logger for error reporting + * @param IURLGenerator $urlGenerator URL generator for deep links * * @return void */ public function __construct( CalDavBackend $calDavBackend, IUserSession $userSession, - LoggerInterface $logger + LoggerInterface $logger, + IURLGenerator $urlGenerator ) { $this->calDavBackend = $calDavBackend; $this->userSession = $userSession; $this->logger = $logger; + $this->urlGenerator = $urlGenerator; }//end __construct() /** @@ -267,7 +278,7 @@ private function extractAssigneeFromDescription(string $description): ?string * * @throws Exception If no user is logged in or no calendar found * - * @spec openspec/changes/retrofit-annotate-openregister-2026-04-30/tasks.md#task-61 + * @spec openspec/specs/object-interactions/spec.md */ public function getTasksForObject(string $objectUuid): array { @@ -306,6 +317,15 @@ public function getTasksForObject(string $objectUuid): array // Only include tasks that match our object UUID. if ($taskArray !== null && $taskArray['objectUuid'] === $objectUuid) { + // Deep-link to the specific task in the Tasks app (hash route). + $deepLink = $this->buildTaskDeepLink( + calendarUri: ($calendar['uri'] ?? null), + taskUri: ($taskArray['id'] ?? null) + ); + if ($deepLink !== null) { + $taskArray['url'] = $deepLink; + } + $tasks[] = $taskArray; } } catch (Exception $e) { @@ -313,12 +333,41 @@ public function getTasksForObject(string $objectUuid): array 'Failed to parse calendar object: '.$e->getMessage(), ['uri' => $calendarObject['uri']] ); - } + }//end try }//end foreach return $tasks; }//end getTasksForObject() + /** + * Build a webroot-aware deep-link to a specific task in the Tasks app. + * + * The Tasks app uses hash routing: + * `/apps/tasks/#/calendars/{calendarUri}/tasks/{taskUri}`. + * + * Returns null (record gets no `url`) when either URI is missing, rather + * than emitting a broken link. + * + * @param string|null $calendarUri The CalDAV calendar URI owning the task. + * @param string|null $taskUri The VTODO object URI (.ics). + * + * @return string|null The deep-link URL, or null when not resolvable. + */ + private function buildTaskDeepLink(?string $calendarUri, ?string $taskUri): ?string + { + if ($calendarUri === null || $calendarUri === '' || $taskUri === null || $taskUri === '') { + return null; + } + + try { + $base = $this->urlGenerator->linkToRoute('tasks.page.index'); + } catch (\Throwable $e) { + return null; + } + + return rtrim($base, '/').'/#/calendars/'.rawurlencode($calendarUri).'/tasks/'.rawurlencode($taskUri); + }//end buildTaskDeepLink() + /** * Create a new CalDAV task linked to an OpenRegister object. * @@ -335,7 +384,7 @@ public function getTasksForObject(string $objectUuid): array * * @throws Exception If no user is logged in or no calendar found * - * @spec openspec/changes/retrofit-annotate-openregister-2026-04-30/tasks.md#task-61 + * @spec openspec/specs/object-interactions/spec.md */ public function createTask( int $registerId, @@ -380,6 +429,13 @@ public function createTask( $lines[] = 'X-OPENREGISTER-SCHEMA:'.$schemaId; $lines[] = 'X-OPENREGISTER-OBJECT:'.$objectUuid; + // Non-core schema fields (e.g. assignee) are round-tripped as a single + // base64-encoded JSON blob so any field survives the VTODO projection + // (object-source-providers) without iCal escaping concerns. + if (empty($data['fields']) === false && is_array($data['fields']) === true) { + $lines[] = 'X-OPENREGISTER-DATA:'.base64_encode((string) json_encode($data['fields'])); + } + // RFC 9253 LINK property. $linkLabel = $this->escapeIcalText(text: $objectTitle); $linkUri = '/apps/openregister/api/objects/'.$registerId.'/'.$schemaId.'/'.$objectUuid; @@ -455,6 +511,17 @@ public function updateTask(string $calendarId, string $taskUri, array $data): ?a $vtodo->DUE = new DateTime($data['due']); } + // Round-trip non-core schema fields (object-source-providers): replace the + // X-OPENREGISTER-DATA blob so a projected object's non-core fields (e.g. + // assignee, taskStatus) update faithfully. Linking props (REGISTER/SCHEMA/ + // OBJECT) are left untouched above so the link + scoping survive the update. + if (isset($data['fields']) === true && is_array($data['fields']) === true) { + unset($vtodo->{'X-OPENREGISTER-DATA'}); + if (empty($data['fields']) === false) { + $vtodo->add('X-OPENREGISTER-DATA', base64_encode((string) json_encode($data['fields']))); + } + } + // Update DTSTAMP. $vtodo->DTSTAMP = gmdate('Ymd\THis\Z'); @@ -562,6 +629,7 @@ private function vtodoToArray(string $calendarData, string $calendarId, string $ 'objectUuid' => $linkData['objectUuid'], 'registerId' => $linkData['registerId'], 'schemaId' => $linkData['schemaId'], + 'fields' => $linkData['fields'], ]; }//end vtodoToArray() @@ -570,13 +638,14 @@ private function vtodoToArray(string $calendarData, string $calendarId, string $ * * @param mixed $vtodo The VTODO component from the parsed iCalendar. * - * @return array{objectUuid: string|null, registerId: int|null, schemaId: int|null} + * @return array{objectUuid: string|null, registerId: int|null, schemaId: int|null, fields: array} */ private function extractOpenRegisterProperties(mixed $vtodo): array { $objectUuid = null; $registerId = null; $schemaId = null; + $fields = []; if (isset($vtodo->{'X-OPENREGISTER-OBJECT'}) === true) { $objectUuid = (string) $vtodo->{'X-OPENREGISTER-OBJECT'}; @@ -590,10 +659,24 @@ private function extractOpenRegisterProperties(mixed $vtodo): array $schemaId = (int) (string) $vtodo->{'X-OPENREGISTER-SCHEMA'}; } + // Non-core schema fields are round-tripped as a single base64-encoded + // JSON blob (X-OPENREGISTER-DATA) so any field survives the VTODO + // projection without iCal-escaping pitfalls. Decoded best-effort. + if (isset($vtodo->{'X-OPENREGISTER-DATA'}) === true) { + $raw = base64_decode((string) $vtodo->{'X-OPENREGISTER-DATA'}, true); + if ($raw !== false) { + $decoded = json_decode($raw, true); + if (is_array($decoded) === true) { + $fields = $decoded; + } + } + } + return [ 'objectUuid' => $objectUuid, 'registerId' => $registerId, 'schemaId' => $schemaId, + 'fields' => $fields, ]; }//end extractOpenRegisterProperties() diff --git a/lib/Service/TenantKeyService.php b/lib/Service/TenantKeyService.php index f2887d84d9..429aeb3f5c 100644 --- a/lib/Service/TenantKeyService.php +++ b/lib/Service/TenantKeyService.php @@ -175,7 +175,7 @@ public function rotateTenantKey(string $tenantId): array * * @return array|null Database row or null * - * @spec openspec/changes/retrofit-2026-05-24-saas-multi-tenant/tasks.md#task-1 + * @spec openspec/specs/saas-multi-tenant/spec.md */ private function fetchActiveRow(string $tenantId): ?array { @@ -254,7 +254,7 @@ private function generateKey(): string * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-saas-multi-tenant/tasks.md#task-2 + * @spec openspec/specs/saas-multi-tenant/spec.md */ private function insertKey(string $tenantId, string $rawKey, string $rotatedAt): void { diff --git a/lib/Service/TenantLifecycleService.php b/lib/Service/TenantLifecycleService.php index d3c6f70af0..62dea9d312 100644 --- a/lib/Service/TenantLifecycleService.php +++ b/lib/Service/TenantLifecycleService.php @@ -19,12 +19,12 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-73 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-74 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-77 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-76 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-74 + * @spec openspec/specs/tenant-lifecycle/spec.md#requirement-organisation-entities-must-have-a-lifecycle-status-field-with-defined-state-transitions + * @spec openspec/specs/tenant-lifecycle/spec.md#requirement-tenant-provisioning-must-create-default-resources-automatically + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md */ declare(strict_types=1); @@ -115,13 +115,13 @@ public function __construct( * * @throws Exception If the transition is invalid * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-73 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-76 + * @spec openspec/specs/tenant-lifecycle/spec.md#requirement-organisation-entities-must-have-a-lifecycle-status-field-with-defined-state-transitions + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function validateTransition(string $currentStatus, string $targetStatus): void { // Validate that both statuses are known before checking transition. - if ($this->isValidStatus($currentStatus) === false || $this->isValidStatus($targetStatus) === false) { + if ($this->isValidStatus(status: $currentStatus) === false || $this->isValidStatus(status: $targetStatus) === false) { throw new Exception( sprintf("Unknown status '%s' or '%s'.", $currentStatus, $targetStatus), Response::HTTP_UNPROCESSABLE_ENTITY @@ -148,7 +148,7 @@ public function validateTransition(string $currentStatus, string $targetStatus): * * @return string[] Valid next states * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-73 + * @spec openspec/specs/tenant-lifecycle/spec.md#requirement-organisation-entities-must-have-a-lifecycle-status-field-with-defined-state-transitions */ public function getValidTransitions(string $status): array { @@ -165,8 +165,8 @@ public function getValidTransitions(string $status): array * * @throws Exception If provisioning fails * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-74 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-77 + * @spec openspec/specs/tenant-lifecycle/spec.md#requirement-tenant-provisioning-must-create-default-resources-automatically + * @spec openspec/specs/tenant-lifecycle/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -183,7 +183,7 @@ public function provision(Organisation $organisation, string $adminUserId): Orga // Validate that the organisation's target environment is a known OTAP stage. $env = $organisation->getEnvironment() ?? self::ENV_PRODUCTION; - if ($this->isValidEnvironment($env) === false) { + if ($this->isValidEnvironment(environment: $env) === false) { throw new Exception( sprintf("Unknown target environment '%s'.", $env), Response::HTTP_UNPROCESSABLE_ENTITY @@ -192,7 +192,7 @@ public function provision(Organisation $organisation, string $adminUserId): Orga // If a source environment is set, validate the promotion order. $sourceEnv = $organisation->getEnvironment() ?? null; - if ($sourceEnv !== null && $sourceEnv !== $env && $this->isValidPromotionOrder($sourceEnv, $env) === false) { + if ($sourceEnv !== null && $sourceEnv !== $env && $this->isValidPromotionOrder(sourceEnv: $sourceEnv, targetEnv: $env) === false) { throw new Exception( sprintf("Invalid OTAP promotion order from '%s' to '%s'.", $sourceEnv, $env), Response::HTTP_CONFLICT @@ -280,7 +280,7 @@ public function provision(Organisation $organisation, string $adminUserId): Orga * * @throws Exception If transition is invalid * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-76 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function suspend(Organisation $organisation): Organisation { @@ -309,7 +309,7 @@ public function suspend(Organisation $organisation): Organisation * * @throws Exception If transition is invalid * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-76 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function reactivate(Organisation $organisation): Organisation { @@ -338,7 +338,7 @@ public function reactivate(Organisation $organisation): Organisation * * @throws Exception If transition is invalid * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function deprovision(Organisation $organisation): Organisation { @@ -367,7 +367,7 @@ public function deprovision(Organisation $organisation): Organisation * * @throws Exception If transition is invalid * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-75 + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function archive(Organisation $organisation): Organisation { @@ -393,8 +393,8 @@ public function archive(Organisation $organisation): Organisation * * @return bool Whether the environment is valid * - * @spec openspec/changes/retrofit-2026-04-28-tenant-lifecycle/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-74 + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function isValidEnvironment(string $environment): bool { @@ -409,8 +409,8 @@ public function isValidEnvironment(string $environment): bool * * @return bool Whether the promotion order is valid * - * @spec openspec/changes/retrofit-2026-04-28-tenant-lifecycle/tasks.md#task-2 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-74 + * @spec openspec/specs/tenant-lifecycle/spec.md + * @spec openspec/specs/tenant-lifecycle/spec.md */ public function isValidPromotionOrder(string $sourceEnv, string $targetEnv): bool { diff --git a/lib/Service/TextExtraction/EmlAttachment.php b/lib/Service/TextExtraction/EmlAttachment.php index 9cbda606e4..fbd15550b0 100644 --- a/lib/Service/TextExtraction/EmlAttachment.php +++ b/lib/Service/TextExtraction/EmlAttachment.php @@ -29,7 +29,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/text-extraction-eml/specs/text-extraction-eml/spec.md + * @spec openspec/specs/text-extraction-eml/spec.md * "Each `EmlAttachment` MUST carry filename, MIME type, raw bytes, and inline / contentId metadata" */ diff --git a/lib/Service/TextExtraction/EmlBody.php b/lib/Service/TextExtraction/EmlBody.php index 84a025de1c..0b522e2298 100644 --- a/lib/Service/TextExtraction/EmlBody.php +++ b/lib/Service/TextExtraction/EmlBody.php @@ -19,7 +19,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/text-extraction-eml/specs/text-extraction-eml/spec.md + * @spec openspec/specs/text-extraction-eml/spec.md * "A new public method `parseEmlStructured()` MUST return an `EmlStructure` value object" */ diff --git a/lib/Service/TextExtraction/EmlParser.php b/lib/Service/TextExtraction/EmlParser.php index 1417617982..4e86741d78 100644 --- a/lib/Service/TextExtraction/EmlParser.php +++ b/lib/Service/TextExtraction/EmlParser.php @@ -40,9 +40,9 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/text-extraction-eml/specs/text-extraction-eml/spec.md - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/text-extraction-eml/spec.md + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-non-utf-8-body-parts-must-be-transcoded-to-utf-8-on-a-best-effort-basis */ declare(strict_types=1); @@ -61,7 +61,7 @@ /** * Parser for `message/rfc822` files. * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) MIME parsing requires several collaborating types + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) MIME parsing requires several collaborating types * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complex MIME/RFC 2822 parsing logic */ class EmlParser @@ -104,7 +104,7 @@ public function __construct(private readonly LoggerInterface $logger) * * @throws EmlParseException When the input is irrecoverably malformed. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ public function parse(File $file, int $depth=0): EmlStructure { @@ -139,7 +139,7 @@ public function parse(File $file, int $depth=0): EmlStructure * * @return EmlStructure * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ public function parseMessage(IMessage $message, int $depth=0): EmlStructure { @@ -166,9 +166,9 @@ public function parseMessage(IMessage $message, int $depth=0): EmlStructure * @return string Flat plain-text output. * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Mostly attachment-loop branching - * @SuppressWarnings(PHPMD.NPathComplexity) Nested attachment-tree traversal + * @SuppressWarnings(PHPMD.NPathComplexity) Nested attachment-tree traversal * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ public function flatten(EmlStructure $structure, int $depth=0): string { @@ -235,7 +235,7 @@ public function flatten(EmlStructure $structure, int $depth=0): string * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ private function extractHeaders(IMessage $message): array { @@ -267,7 +267,7 @@ private function extractHeaders(IMessage $message): array * @return array * * @SuppressWarnings(PHPMD.CyclomaticComplexity) RFC 2822 address-list parsing state machine - * @SuppressWarnings(PHPMD.NPathComplexity) RFC 2822 address-list parsing state machine + * @SuppressWarnings(PHPMD.NPathComplexity) RFC 2822 address-list parsing state machine */ private function splitAddressList(?string $raw): array { @@ -335,7 +335,7 @@ private function splitAddressList(?string $raw): array * * @return EmlBody * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-non-utf-8-body-parts-must-be-transcoded-to-utf-8-on-a-best-effort-basis */ private function extractBody(IMessage $message): EmlBody { @@ -366,7 +366,7 @@ private function extractBody(IMessage $message): EmlBody * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ private function extractAttachments(IMessage $message, int $depth): array { @@ -395,7 +395,7 @@ private function extractAttachments(IMessage $message, int $depth): array * * @return EmlAttachment * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ private function buildAttachment(IMessagePart $part, int $position, int $depth): EmlAttachment { @@ -525,7 +525,7 @@ private function sanitiseFilename(string $raw): string * tolerates a malformed nested EML so the * rest of the structure is still usable). * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ private function parseNestedEml(string $bytes, int $depth): ?EmlStructure { @@ -579,7 +579,7 @@ private function stripAngleBrackets(?string $raw): ?string * * @SuppressWarnings(PHPMD.StaticAccess) DateTimeImmutable::createFromFormat is idiomatic PHP * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ private function parseDate(?string $raw): ?DateTimeImmutable { @@ -623,7 +623,7 @@ private function parseDate(?string $raw): ?DateTimeImmutable * * @return string Plain text. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ private function htmlToText(string $html): string { @@ -664,7 +664,7 @@ private function htmlToText(string $html): string * * @return string UTF-8 string (or the raw input when detection fails). * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-31 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-non-utf-8-body-parts-must-be-transcoded-to-utf-8-on-a-best-effort-basis */ private function ensureUtf8(string $value): string { @@ -716,7 +716,7 @@ private function ensureUtf8(string $value): string * * @return string Sanitised string safe to log. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ public static function sanitisePiiForLogging(string $message): string { diff --git a/lib/Service/TextExtraction/EmlStructure.php b/lib/Service/TextExtraction/EmlStructure.php index d9addd067a..43e34f1425 100644 --- a/lib/Service/TextExtraction/EmlStructure.php +++ b/lib/Service/TextExtraction/EmlStructure.php @@ -26,7 +26,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/text-extraction-eml/specs/text-extraction-eml/spec.md + * @spec openspec/specs/text-extraction-eml/spec.md */ declare(strict_types=1); diff --git a/lib/Service/TextExtraction/EntityRecognitionHandler.php b/lib/Service/TextExtraction/EntityRecognitionHandler.php index ef83a4bd59..9746fb4055 100644 --- a/lib/Service/TextExtraction/EntityRecognitionHandler.php +++ b/lib/Service/TextExtraction/EntityRecognitionHandler.php @@ -31,6 +31,8 @@ use OCA\OpenRegister\Db\GdprEntity; use OCA\OpenRegister\Db\GdprEntityMapper; use OCA\OpenRegister\Service\SettingsService; +use OCA\OpenRegister\Service\Anonymisation\AnonymisationBackendService; +use OCA\OpenRegister\Service\Anonymisation\BackendState; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IDBConnection; use Psr\Log\LoggerInterface; @@ -92,12 +94,13 @@ class EntityRecognitionHandler /** * Constructor. * - * @param ChunkMapper $chunkMapper Chunk mapper. - * @param GdprEntityMapper $entityMapper Entity mapper. - * @param EntityRelationMapper $entityRelationMapper Entity relation mapper. - * @param IDBConnection $db Database connection. - * @param LoggerInterface $logger Logger. - * @param SettingsService $settingsService Settings service. + * @param ChunkMapper $chunkMapper Chunk mapper. + * @param GdprEntityMapper $entityMapper Entity mapper. + * @param EntityRelationMapper $entityRelationMapper Entity relation mapper. + * @param IDBConnection $db Database connection. + * @param LoggerInterface $logger Logger. + * @param SettingsService $settingsService Settings service. + * @param AnonymisationBackendService $anonymisationBackendService Backend state + ExApp client. */ public function __construct( private readonly ChunkMapper $chunkMapper, @@ -105,7 +108,8 @@ public function __construct( private readonly EntityRelationMapper $entityRelationMapper, private readonly IDBConnection $db, private readonly LoggerInterface $logger, - private readonly SettingsService $settingsService + private readonly SettingsService $settingsService, + private readonly AnonymisationBackendService $anonymisationBackendService ) { }//end __construct() @@ -299,11 +303,14 @@ private function storeDetectedEntities( foreach ($detectedEntities as $detected) { try { - // Find or create entity. + // Find or create entity. Scrub the value to valid UTF-8: text + // extracted from some PDFs contains ill-formed byte sequences + // (e.g. a stray 0xC3 lead byte) that PostgreSQL (UTF8) rejects + // with SQLSTATE[22021], aborting the insert and losing the entity. $entity = $this->findOrCreateEntity( type: $detected['type'], - value: $detected['value'], - category: $detected['category'] ?? $this->getCategoryForType(type: $detected['type']) + value: $this->sanitizeUtf8(value: trim((string) ($detected['value'] ?? ''))), + category: $detected['category'] ?? self::getCategoryForType(type: $detected['type']) ); // Create entity relation. @@ -320,7 +327,7 @@ private function storeDetectedEntities( positionEnd: $detected['position_end'], window: $contextWindow ); - $relation->setContext($context); + $relation->setContext($this->sanitizeUtf8(value: $context)); $relation->setCreatedAt(new DateTime()); // Set source references based on chunk source type. @@ -383,6 +390,8 @@ private function storeDetectedEntities( */ private function detectEntities(string $text, string $method, ?array $entityTypes, float $confidenceThreshold): array { + $method = $this->resolveMethod(method: $method); + return match ($method) { self::METHOD_REGEX => $this->detectWithRegex( text: $text, @@ -413,6 +422,35 @@ private function detectEntities(string $text, string $method, ?array $entityType };//end match }//end detectEntities() + /** + * Resolve the requested method to a concrete, runnable method. + * + * Guards against the removed `auto` sentinel (which legacy installs may still + * have stored) and any unknown/empty value by deferring to the single source + * of truth — `AnonymisationBackendService::getState()->effectiveMethod` — which + * applies the availability/precedence rules. Concrete known methods pass through. + * + * @param string $method The requested method. + * + * @return string A concrete method from BackendState::METHODS. + */ + private function resolveMethod(string $method): string + { + if (in_array($method, BackendState::METHODS, true) === true) { + return $method; + } + + try { + return $this->anonymisationBackendService->getState()->effectiveMethod; + } catch (Exception $e) { + $this->logger->warning( + message: '[EntityRecognitionHandler] Could not resolve method "'.$method.'", falling back to regex: '.$e->getMessage(), + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return self::METHOD_REGEX; + } + }//end resolveMethod() + /** * Detect entities using regex patterns. * @@ -502,8 +540,10 @@ private function detectWithPresidio(string $text, ?array $entityTypes, float $co { try { // Get Presidio settings. - $fileSettings = $this->settingsService->getFileSettingsOnly(); - $presidioEndpoint = $fileSettings['presidioApiEndpoint'] ?? ''; + $fileSettings = $this->settingsService->getFileSettingsOnly(); + // Strip a trailing slash to avoid a double-slash "//analyze" URL + // (some servers 404, silently sending the run to the regex fallback). + $presidioEndpoint = rtrim(trim((string) ($fileSettings['presidioApiEndpoint'] ?? '')), '/'); if (empty($presidioEndpoint) === true) { $this->logger->warning( @@ -576,31 +616,57 @@ private function detectWithOpenAnonymiser(string $text, ?array $entityTypes, flo try { // Get OpenAnonymiser settings. $fileSettings = $this->settingsService->getFileSettingsOnly(); - $anonEndpoint = $fileSettings['openAnonymiserApiEndpoint'] ?? ''; + // Strip a trailing slash: a configured endpoint like + // "https://host/" would otherwise build "https://host//api/v1/analyze" + // (double slash), which some servers 404 — sending the run silently + // to the regex fallback. + $anonEndpoint = rtrim(trim((string) ($fileSettings['openAnonymiserApiEndpoint'] ?? '')), '/'); + // Source: 'internal' (AppAPI ExApp, default) or 'external' (operator-entered URL). + $useExternal = (($fileSettings['openAnonymiserSource'] ?? 'internal') === 'external'); + + // Build request body (shared by both transports). + $requestBody = $this->buildAnalyzeRequestBody(text: $text, language: 'nl', entityTypes: $entityTypes); - if (empty($anonEndpoint) === true) { - $this->logger->warning( - message: '[EntityRecognitionHandler] OpenAnonymiser endpoint not configured, falling back to regex', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return $this->detectWithRegex( - text: $text, - entityTypes: $entityTypes, - confidenceThreshold: $confidenceThreshold + if ($useExternal === false) { + // Internal: call the ExApp through AppAPI (signed; routing by app id). + $responseData = $this->anonymisationBackendService->requestOpenAnonymiser( + route: '/api/v1/analyze', + params: $requestBody ); - } - // Build request body. - $requestBody = $this->buildAnalyzeRequestBody(text: $text, language: 'nl', entityTypes: $entityTypes); + // Fall back to a configured external endpoint if the ExApp is unreachable. + if ($responseData === null && $anonEndpoint !== '') { + $responseData = $this->postAnalyzeRequest( + url: $anonEndpoint.'/api/v1/analyze', + requestBody: $requestBody, + serviceName: 'OpenAnonymiser' + ); + } + } else { + if ($anonEndpoint === '') { + $this->logger->warning( + message: '[EntityRecognitionHandler] OpenAnonymiser external endpoint not configured, falling back to regex', + context: ['file' => __FILE__, 'line' => __LINE__] + ); + return $this->detectWithRegex( + text: $text, + entityTypes: $entityTypes, + confidenceThreshold: $confidenceThreshold + ); + } - // Make HTTP request and parse response. - $responseData = $this->postAnalyzeRequest( - url: $anonEndpoint.'/api/v1/analyze', - requestBody: $requestBody, - serviceName: 'OpenAnonymiser' - ); + $responseData = $this->postAnalyzeRequest( + url: $anonEndpoint.'/api/v1/analyze', + requestBody: $requestBody, + serviceName: 'OpenAnonymiser' + ); + }//end if if ($responseData === null) { + $this->logger->warning( + message: '[EntityRecognitionHandler] OpenAnonymiser unreachable, falling back to regex', + context: ['file' => __FILE__, 'line' => __LINE__] + ); return $this->detectWithRegex( text: $text, entityTypes: $entityTypes, @@ -779,7 +845,7 @@ private function convertApiResultsToEntities( $entities[] = [ 'type' => $entityType, 'value' => $value, - 'category' => $this->getCategoryForType(type: $entityType), + 'category' => self::getCategoryForType(type: $entityType), 'position_start' => $start, 'position_end' => $end, 'confidence' => $score, @@ -949,11 +1015,19 @@ private function findOrCreateEntity(string $type, string $value, string $categor /** * Get category for entity type. * - * @param string $type Entity type. + * Lifted to `public static` so flows that produce GdprEntity rows + * outside the detector pipeline (notably the manual-entity endpoint + * in `ManualEntityService`) can derive the same category mapping + * without taking on this handler's DI graph. The `oc_openregister_entities.category` + * column is NOT NULL with no default — every insert path MUST set it, + * and consistency with detector-produced rows is desirable so the + * catalogue stays homogeneous. * - * @return string Category. + * @param string $type Entity type tag (e.g. `PERSON`, `IBAN`). + * + * @return string One of the `CATEGORY_*` constants on this class. */ - private function getCategoryForType(string $type): string + public static function getCategoryForType(string $type): string { return match ($type) { self::ENTITY_TYPE_PERSON, @@ -986,6 +1060,43 @@ private function extractContext(string $text, int $positionStart, int $positionE return substr($text, $start, $end - $start); }//end extractContext() + /** + * Scrub a string to well-formed UTF-8 for safe PostgreSQL storage. + * + * Text extracted from some PDFs contains ill-formed UTF-8 byte sequences. + * PostgreSQL's UTF8 encoding rejects these on insert (SQLSTATE[22021]), + * which would abort the entity-relation insert and silently drop the + * entity — leaving residual PII in the anonymised output. Replacing the + * invalid bytes here keeps storage (and the downstream substitution map) + * intact. Operates on already-substringed context, so stored integer + * offsets are unaffected. + * + * @param string $value The (possibly ill-formed) input string. + * + * @return string A valid-UTF-8 string. + */ + private function sanitizeUtf8(string $value): string + { + if ($value === '') { + return $value; + } + + if (function_exists('mb_scrub') === true) { + $scrubbed = mb_scrub($value, 'UTF-8'); + if (is_string($scrubbed) === true) { + return $scrubbed; + } + } + + $converted = @iconv('UTF-8', 'UTF-8//IGNORE', $value); + + if ($converted === false) { + return $value; + } + + return $converted; + }//end sanitizeUtf8() + /** * Populate the disambiguating object-context columns on a relation. * diff --git a/lib/Service/TextExtraction/FileHandler.php b/lib/Service/TextExtraction/FileHandler.php index 482d84bc92..41807fb1c7 100644 --- a/lib/Service/TextExtraction/FileHandler.php +++ b/lib/Service/TextExtraction/FileHandler.php @@ -17,7 +17,7 @@ * @version GIT: * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-1 + * @spec openspec/specs/text-extraction/spec.md */ namespace OCA\OpenRegister\Service\TextExtraction; @@ -91,7 +91,7 @@ public function getSourceType(): string * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Force parameter follows interface contract * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-2 + * @spec openspec/specs/text-extraction/spec.md */ public function extractText(int $sourceId, array $sourceMeta, bool $force=false): array { @@ -162,7 +162,7 @@ public function extractText(int $sourceId, array $sourceMeta, bool $force=false) * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Force parameter follows interface contract * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-3 + * @spec openspec/specs/text-extraction/spec.md */ public function needsExtraction(int $sourceId, int $sourceTimestamp, bool $force): bool { @@ -197,7 +197,7 @@ public function needsExtraction(int $sourceId, int $sourceTimestamp, bool $force * share_stime: int|null, storage_id: null|string, owner: null|string, * accessUrl: null|string, downloadUrl: null|string, published: null|string} * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-1 + * @spec openspec/specs/text-extraction/spec.md */ public function getSourceMetadata(int $sourceId): array { @@ -216,7 +216,7 @@ public function getSourceMetadata(int $sourceId): array * * @return int Unix timestamp. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-3 + * @spec openspec/specs/text-extraction/spec.md */ public function getSourceTimestamp(int $sourceId): int { @@ -252,7 +252,6 @@ private function performTextExtraction(\OCP\Files\File $file, string $mimeType): // For other types, we'd need to use the extraction methods // From TextExtractionService (PDF, DOCX, etc.). - // This should be refactored to use IndexService if needed. $this->logger->warning( message: '[FileHandler] Complex extraction not yet implemented', context: [ diff --git a/lib/Service/TextExtraction/ObjectHandler.php b/lib/Service/TextExtraction/ObjectHandler.php index 6c17b20b58..57f8c04456 100644 --- a/lib/Service/TextExtraction/ObjectHandler.php +++ b/lib/Service/TextExtraction/ObjectHandler.php @@ -17,7 +17,7 @@ * @version GIT: * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-1 + * @spec openspec/specs/text-extraction/spec.md */ namespace OCA\OpenRegister\Service\TextExtraction; @@ -98,7 +98,7 @@ public function getSourceType(): string * @SuppressWarnings(PHPMD.NPathComplexity) Multiple field extraction paths with optional data * @SuppressWarnings(PHPMD.UnusedFormalParameter) $sourceMeta and $force kept to honour interface contract * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-2 + * @spec openspec/specs/text-extraction/spec.md */ public function extractText(int $sourceId, array $sourceMeta, bool $force=false): array { @@ -222,7 +222,7 @@ public function extractText(int $sourceId, array $sourceMeta, bool $force=false) * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Force parameter follows interface contract * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-3 + * @spec openspec/specs/text-extraction/spec.md */ public function needsExtraction(int $sourceId, int $sourceTimestamp, bool $force): bool { @@ -253,7 +253,7 @@ public function needsExtraction(int $sourceId, int $sourceTimestamp, bool $force * register: null|string, version: null|string, organization: mixed, * owner: null|string, updated: \DateTime|null} * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-1 + * @spec openspec/specs/text-extraction/spec.md */ public function getSourceMetadata(int $sourceId): array { @@ -278,7 +278,7 @@ public function getSourceMetadata(int $sourceId): array * * @return int Unix timestamp. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-3 + * @spec openspec/specs/text-extraction/spec.md */ public function getSourceTimestamp(int $sourceId): int { diff --git a/lib/Service/TextExtraction/PdfExtractor.php b/lib/Service/TextExtraction/PdfExtractor.php new file mode 100644 index 0000000000..5a1cf326dd --- /dev/null +++ b/lib/Service/TextExtraction/PdfExtractor.php @@ -0,0 +1,139 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\TextExtraction; + +use Exception; +use OCP\Files\File; +use Smalot\PdfParser\Parser as PdfParser; +use Psr\Log\LoggerInterface; + +/** + * Extracts text content from PDF files. + */ +class PdfExtractor +{ + /** + * Constructor. + * + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Extract plain text from a PDF file. + * + * @param File $file The PDF file to extract. + * + * @return string|null The extracted text, or null when empty. + * + * @throws Exception When the PDF parser is unavailable or extraction fails. + */ + public function extract(File $file): ?string + { + // Check if PdfParser library is available. + if (class_exists('Smalot\PdfParser\Parser') === false) { + $this->logger->warning( + message: '[PdfExtractor] PDF parser library not available', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + ] + ); + $msg = "PDF parser library (smalot/pdfparser) is not installed. "; + $msg .= "Run: composer require smalot/pdfparser"; + throw new Exception($msg); + } + + try { + $this->logger->debug( + message: '[PdfExtractor] Extracting PDF', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'name' => $file->getName(), + ] + ); + + // Get file content. + $content = $file->getContent(); + + // Create temporary file for PdfParser (it requires a file path). + $tempFile = tmpfile(); + $tempPath = stream_get_meta_data($tempFile)['uri']; + fwrite($tempFile, $content); + + // Parse PDF. + $parser = new PdfParser(); + $pdf = $parser->parseFile($tempPath); + + // Extract text. + $text = $pdf->getText(); + + // Clean up. + fclose($tempFile); + + if ($text === '') { + $this->logger->warning( + message: '[PdfExtractor] PDF extraction returned empty text', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + ] + ); + return null; + } + + $this->logger->debug( + message: '[PdfExtractor] PDF extracted successfully', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'length' => strlen($text), + ] + ); + + return $text; + } catch (Exception $e) { + $this->logger->error( + message: '[PdfExtractor] PDF extraction failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'error' => $e->getMessage(), + ] + ); + throw new Exception("PDF extraction failed: ".$e->getMessage()); + }//end try + }//end extract() +}//end class diff --git a/lib/Service/TextExtraction/SpreadsheetExtractor.php b/lib/Service/TextExtraction/SpreadsheetExtractor.php new file mode 100644 index 0000000000..99a0c7cb7a --- /dev/null +++ b/lib/Service/TextExtraction/SpreadsheetExtractor.php @@ -0,0 +1,167 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\TextExtraction; + +use Exception; +use OCP\Files\File; +use PhpOffice\PhpSpreadsheet\IOFactory as SpreadsheetIOFactory; +use Psr\Log\LoggerInterface; + +/** + * Extracts text content from spreadsheet files. + */ +class SpreadsheetExtractor +{ + /** + * Constructor. + * + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Extract plain text from a spreadsheet file. + * + * @param File $file The spreadsheet file to extract. + * + * @return string|null The extracted text, or null when empty. + * + * @throws Exception When PhpSpreadsheet is unavailable or extraction fails. + */ + public function extract(File $file): ?string + { + // PhpSpreadsheet should already be installed (in composer.json). + if (class_exists('PhpOffice\PhpSpreadsheet\IOFactory') === false) { + $this->logger->warning( + message: '[SpreadsheetExtractor] PhpSpreadsheet library not available', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + ] + ); + $msg = "PhpSpreadsheet library (phpoffice/phpspreadsheet) is not installed. "; + $msg .= "Run: composer require phpoffice/phpspreadsheet"; + throw new Exception($msg); + } + + try { + $this->logger->debug( + message: '[SpreadsheetExtractor] Extracting spreadsheet', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'name' => $file->getName(), + ] + ); + + // Get file content. + $content = $file->getContent(); + + // Create temporary file for PhpSpreadsheet. + $tempFile = tmpfile(); + $tempPath = stream_get_meta_data($tempFile)['uri']; + fwrite($tempFile, $content); + + // Load spreadsheet. + $spreadsheet = SpreadsheetIOFactory::load($tempPath); + + // Extract text from all sheets. + $text = ''; + foreach ($spreadsheet->getAllSheets() as $sheet) { + $text .= "Sheet: ".$sheet->getTitle()."\n"; + + $highestRow = $sheet->getHighestRow(); + $highestColumn = $sheet->getHighestColumn(); + + // Iterate through rows and columns. + for ($row = 1; $row <= $highestRow; $row++) { + $rowData = []; + // @psalm-suppress StringIncrement - Excel column increment is intentional + for ($col = 'A'; $col !== $highestColumn; $col++) { + $value = $sheet->getCell($col.$row)->getValue(); + if ($value !== null && $value !== '') { + $rowData[] = $value; + } + } + + // Add last column. + $value = $sheet->getCell($highestColumn.$row)->getValue(); + if ($value !== null && $value !== '') { + $rowData[] = $value; + } + + if (empty($rowData) === false) { + $text .= implode("\t", $rowData)."\n"; + } + } + + $text .= "\n"; + }//end foreach + + // Clean up. + fclose($tempFile); + + if (trim($text) === '') { + $this->logger->warning( + message: '[SpreadsheetExtractor] Spreadsheet extraction returned empty text', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + ] + ); + return null; + } + + $this->logger->debug( + message: '[SpreadsheetExtractor] Spreadsheet extracted successfully', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'length' => strlen($text), + ] + ); + + return $text; + } catch (Exception $e) { + $this->logger->error( + message: '[SpreadsheetExtractor] Spreadsheet extraction failed', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'error' => $e->getMessage(), + ] + ); + throw new Exception("Spreadsheet extraction failed: ".$e->getMessage()); + }//end try + }//end extract() +}//end class diff --git a/lib/Service/TextExtraction/TextExtractionHandlerInterface.php b/lib/Service/TextExtraction/TextExtractionHandlerInterface.php index a124d58478..2d124eccb7 100644 --- a/lib/Service/TextExtraction/TextExtractionHandlerInterface.php +++ b/lib/Service/TextExtraction/TextExtractionHandlerInterface.php @@ -19,7 +19,7 @@ * @version GIT: * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-1 + * @spec openspec/specs/text-extraction/spec.md */ namespace OCA\OpenRegister\Service\TextExtraction; @@ -59,7 +59,7 @@ interface TextExtractionHandlerInterface * * @throws \Exception When extraction fails. * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid2/tasks.md#task-1 + * @spec openspec/specs/text-extraction/spec.md */ public function extractText(int $sourceId, array $sourceMeta, bool $force=false): array; diff --git a/lib/Service/TextExtraction/WordExtractor.php b/lib/Service/TextExtraction/WordExtractor.php new file mode 100644 index 0000000000..05b730049a --- /dev/null +++ b/lib/Service/TextExtraction/WordExtractor.php @@ -0,0 +1,310 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://OpenRegister.app + */ + +namespace OCA\OpenRegister\Service\TextExtraction; + +use Exception; +use OCP\Files\File; +use PhpOffice\PhpWord\IOFactory as WordIOFactory; +use Psr\Log\LoggerInterface; + +/** + * Extracts text content from Word-family documents. + */ +class WordExtractor +{ + /** + * Maximum recursion depth when walking Word document elements. + */ + private const MAX_WORD_DEPTH = 50; + + /** + * Constructor. + * + * @param LoggerInterface $logger Logger. + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Extract plain text from a Word-family document. + * + * @param File $file The document to extract. + * + * @return string|null The extracted text, or null when empty / on per-document failure. + * + * @throws Exception When the PhpWord library is unavailable. + */ + public function extract(File $file): ?string + { + // Check if PhpWord library is available (deployment error — still throws). + if (class_exists('PhpOffice\PhpWord\IOFactory') === false) { + $this->logger->warning( + message: '[WordExtractor] PhpWord library not available', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + ] + ); + $msg = "PhpWord library (phpoffice/phpword) is not installed. "; + $msg .= "Run: composer require phpoffice/phpword"; + throw new Exception($msg); + } + + $readerName = $this->resolveWordReader(mimeType: (string) $file->getMimeType(), fileName: (string) $file->getName()); + + $tempFile = null; + try { + $this->logger->debug( + message: '[WordExtractor] Extracting Word document', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'name' => $file->getName(), + 'reader' => $readerName, + ] + ); + + // Write the content to a temp file for PhpWord to read. + $content = $file->getContent(); + $tempFile = tmpfile(); + $tempPath = stream_get_meta_data($tempFile)['uri']; + fwrite($tempFile, $content); + + // Load with the reader chosen from the MIME/extension. + $phpWord = WordIOFactory::load($tempPath, $readerName); + + // Walk every section: headers, body, footers. + $text = ''; + foreach ($phpWord->getSections() as $section) { + foreach ($section->getHeaders() as $header) { + $text .= $this->walkWordElements(elements: $header->getElements()); + } + + $text .= $this->walkWordElements(elements: $section->getElements()); + + foreach ($section->getFooters() as $footer) { + $text .= $this->walkWordElements(elements: $footer->getElements()); + } + } + + // Always capture document-level footnotes/endnotes in addition to + // any inline notes the body walk already picked up. + $text .= $this->extractWordNotes(phpWord: $phpWord); + + fclose($tempFile); + $tempFile = null; + + if (trim($text) === '') { + $this->logger->warning( + message: '[WordExtractor] Word extraction returned empty text', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'reader' => $readerName, + ] + ); + return null; + } + + $this->logger->debug( + message: '[WordExtractor] Word document extracted successfully', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'length' => strlen($text), + ] + ); + + return $text; + } catch (\Throwable $e) { + if (is_resource($tempFile) === true) { + fclose($tempFile); + } + + // Per-document failure (e.g. limited MsDoc binary parsing): log + // structure only (no document content, per ADR-005) and degrade + // to null so the surrounding pipeline treats it as "no text". + $this->logger->error( + message: '[WordExtractor] Word extraction failed; returning null', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'fileId' => $file->getId(), + 'mimeType' => (string) $file->getMimeType(), + 'reader' => $readerName, + 'exception' => get_class($e), + ] + ); + return null; + }//end try + }//end extract() + + /** + * Recursively walk PhpWord elements accumulating their text. + * + * @param iterable $elements The elements to walk. + * @param int $depth Current recursion depth. + * + * @return string Accumulated text. + */ + private function walkWordElements(iterable $elements, int $depth=0): string + { + if ($depth > self::MAX_WORD_DEPTH) { + $this->logger->debug( + message: '[WordExtractor] Word element walk hit MAX_WORD_DEPTH; stopping descent', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'maxDepth' => self::MAX_WORD_DEPTH, + ] + ); + return ''; + } + + $text = ''; + foreach ($elements as $element) { + // Table: rows → cells → recurse cell elements (nested tables re-enter here). + if (method_exists($element, 'getRows') === true) { + foreach ($element->getRows() as $row) { + if (method_exists($row, 'getCells') === false) { + continue; + } + + foreach ($row->getCells() as $cell) { + if (method_exists($cell, 'getElements') === true) { + $text .= $this->walkWordElements(elements: $cell->getElements(), depth: ($depth + 1)); + } + } + + $text .= "\n"; + } + + continue; + } + + // Composite container (TextRun, ListItemRun, Footnote, ...): descend into children. + if (method_exists($element, 'getElements') === true) { + $children = $element->getElements(); + if (empty($children) === false) { + $text .= $this->walkWordElements(elements: $children, depth: ($depth + 1)); + $text .= "\n"; + continue; + } + } + + // Leaf text-bearing element (Text, Title, Link, ListItem, PreserveText). + if (method_exists($element, 'getText') === true) { + $value = $element->getText(); + if (is_string($value) === true) { + if ($value !== '') { + $text .= $value."\n"; + } + } else if (is_object($value) === true) { + // Some elements (e.g. Title) return a TextRun from getText() — walk it. + $text .= $this->walkWordElements(elements: [$value], depth: ($depth + 1)); + } + } + }//end foreach + + return $text; + }//end walkWordElements() + + /** + * Extract document-level footnote and endnote text. + * + * @param \PhpOffice\PhpWord\PhpWord $phpWord Loaded PhpWord document. + * + * @return string Accumulated footnote/endnote text. + */ + private function extractWordNotes(\PhpOffice\PhpWord\PhpWord $phpWord): string + { + $text = ''; + + $collections = []; + try { + $collections[] = $phpWord->getFootnotes(); + $collections[] = $phpWord->getEndnotes(); + } catch (\Throwable $e) { + // Older/newer PhpWord without these accessors — inline capture still applies. + return $text; + } + + foreach ($collections as $collection) { + if (method_exists($collection, 'getItems') === false) { + continue; + } + + foreach ($collection->getItems() as $note) { + if (method_exists($note, 'getElements') === true) { + $text .= $this->walkWordElements(elements: $note->getElements()); + } + } + } + + return $text; + }//end extractWordNotes() + + /** + * Map a Word-family MIME type (or filename extension) to a PhpWord reader name. + * + * @param string $mimeType The file MIME type. + * @param string $fileName The file name (extension used as fallback). + * + * @return string PhpWord reader name (Word2007 | MsDoc | ODText). + */ + private function resolveWordReader(string $mimeType, string $fileName): string + { + $byMime = [ + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'Word2007', + 'application/msword' => 'MsDoc', + 'application/vnd.oasis.opendocument.text' => 'ODText', + ]; + + if (isset($byMime[$mimeType]) === true) { + return $byMime[$mimeType]; + } + + // Fall back to the filename extension when the MIME is generic/ambiguous. + $byExt = [ + 'docx' => 'Word2007', + 'doc' => 'MsDoc', + 'odt' => 'ODText', + ]; + + $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + if (isset($byExt[$ext]) === true) { + return $byExt[$ext]; + } + + return 'Word2007'; + }//end resolveWordReader() +}//end class diff --git a/lib/Service/TextExtractionService.php b/lib/Service/TextExtractionService.php index ddc1cb6312..719b68c5b6 100644 --- a/lib/Service/TextExtractionService.php +++ b/lib/Service/TextExtractionService.php @@ -18,7 +18,7 @@ * @version GIT: * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ declare(strict_types=1); @@ -39,6 +39,10 @@ use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Service\RiskLevelService; +use OCA\OpenRegister\Service\TextExtraction\EmlParser; +use OCA\OpenRegister\Service\TextExtraction\SpreadsheetExtractor; +use OCA\OpenRegister\Service\TextExtraction\PdfExtractor; +use OCA\OpenRegister\Service\TextExtraction\WordExtractor; use OCA\OpenRegister\Service\TextExtraction\EntityRecognitionHandler; use OCA\OpenRegister\Service\TextExtraction\ObjectHandler; use OCP\AppFramework\Db\DoesNotExistException; @@ -49,9 +53,6 @@ use Symfony\Component\Uid\Uuid; use Throwable; // Document parsing libraries. -use Smalot\PdfParser\Parser as PdfParser; -use PhpOffice\PhpWord\IOFactory as WordIOFactory; -use PhpOffice\PhpSpreadsheet\IOFactory as SpreadsheetIOFactory; /** * TextExtractionService @@ -69,7 +70,9 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complex multi-format document extraction logic. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Requires multiple document parsing libraries and mapper types for multi-format extraction. * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Individual extraction methods for PDF/DOCX/XLSX/EML each require format-specific logic. - * @SuppressWarnings(PHPMD.TooManyMethods) One private extraction method per supported MIME group (PDF/DOCX/XLSX/EML/text/object/file) plus chunking strategies; splitting into sub-services would break the encapsulated extraction workflow. + * @SuppressWarnings(PHPMD.TooManyMethods) One private extraction method per supported MIME group + * (PDF/DOCX/XLSX/EML/text/object/file) plus chunking strategies; splitting into sub-services would + * break the encapsulated extraction workflow. */ class TextExtractionService { @@ -135,6 +138,9 @@ class TextExtractionService * inputs and for the public `parseEmlStructured` * surface that DocuDesk's `eml-pdf-assembly` * consumes; see `text-extraction-eml`). + * @param SpreadsheetExtractor $spreadsheetExtractor Extractor for spreadsheet documents + * @param PdfExtractor $pdfExtractor Extractor for PDF documents + * @param WordExtractor $wordExtractor Extractor for Word documents * * @SuppressWarnings(PHPMD.ExcessiveParameterList) Nextcloud DI requires constructor injection for all document-type parsers and entity mappers. * @SuppressWarnings(PHPMD.ShortVariable) $db is a well-known PHP idiom for a database connection parameter. @@ -153,7 +159,10 @@ public function __construct( private readonly EntityRelationMapper $entityRelationMapper, private readonly SettingsService $settingsService, private readonly RiskLevelService $riskLevelService, - private readonly \OCA\OpenRegister\Service\TextExtraction\EmlParser $emlParser + private readonly EmlParser $emlParser, + private readonly SpreadsheetExtractor $spreadsheetExtractor, + private readonly PdfExtractor $pdfExtractor, + private readonly WordExtractor $wordExtractor ) { }//end __construct() @@ -1347,195 +1356,36 @@ private function sanitizeText(string $text): string */ private function extractPdf(\OCP\Files\File $file): string|null { - // Check if PdfParser library is available. - if (class_exists('Smalot\PdfParser\Parser') === false) { - $this->logger->warning( - message: '[TextExtractionService] PDF parser library not available', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - ] - ); - $msg = "PDF parser library (smalot/pdfparser) is not installed. "; - $msg .= "Run: composer require smalot/pdfparser"; - throw new Exception($msg); - } - - try { - $this->logger->debug( - message: '[TextExtractionService] Extracting PDF', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'name' => $file->getName(), - ] - ); - - // Get file content. - $content = $file->getContent(); - - // Create temporary file for PdfParser (it requires a file path). - $tempFile = tmpfile(); - $tempPath = stream_get_meta_data($tempFile)['uri']; - fwrite($tempFile, $content); - - // Parse PDF. - $parser = new PdfParser(); - $pdf = $parser->parseFile($tempPath); - - // Extract text. - $text = $pdf->getText(); - - // Clean up. - fclose($tempFile); - - if ($text === '') { - $this->logger->warning( - message: '[TextExtractionService] PDF extraction returned empty text', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - ] - ); - return null; - } - - $this->logger->debug( - message: '[TextExtractionService] PDF extracted successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'length' => strlen($text), - ] - ); - - return $text; - } catch (Exception $e) { - $this->logger->error( - message: '[TextExtractionService] PDF extraction failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'error' => $e->getMessage(), - ] - ); - throw new Exception("PDF extraction failed: ".$e->getMessage()); - }//end try + // Delegated to the dedicated PdfExtractor handler + // (extract-god-class-services) — behaviour unchanged. + return $this->pdfExtractor->extract(file: $file); }//end extractPdf() /** - * Extract text from Word document (DOCX/DOC) using PhpWord + * Extract text from a Word-family document (DOCX/DOC/ODT) using PhpWord + * + * Selects the PhpWord reader from the file's MIME type / extension + * (DOCX → Word2007, DOC → MsDoc, ODT → ODText) and walks the full + * element tree — body, tables (incl. nested tables and in-cell text + * runs / list items), section headers and footers, and document-level + * footnotes/endnotes. On a per-document load/parse failure the method + * logs structural detail (no document content) and returns null rather + * than throwing, so a single un-parseable file does not abort a batch. * * @param \OCP\Files\File $file Nextcloud file object * - * @return string|null Extracted text content + * @return string|null Extracted text content, or null on empty/failed extraction * - * @throws Exception If Word parsing fails + * @throws Exception If the PhpWord library itself is not installed (deployment error) * * @SuppressWarnings(PHPMD.StaticAccess) IOFactory::load is standard PhpWord pattern - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex document structure traversal + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multi-section header/body/footer/notes traversal */ private function extractWord(\OCP\Files\File $file): ?string { - // Check if PhpWord library is available. - if (class_exists('PhpOffice\PhpWord\IOFactory') === false) { - $this->logger->warning( - message: '[TextExtractionService] PhpWord library not available', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - ] - ); - $msg = "PhpWord library (phpoffice/phpword) is not installed. "; - $msg .= "Run: composer require phpoffice/phpword"; - throw new Exception($msg); - } - - try { - $this->logger->debug( - message: '[TextExtractionService] Extracting Word document', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'name' => $file->getName(), - ] - ); - - // Get file content. - $content = $file->getContent(); - - // Create temporary file for PhpWord. - $tempFile = tmpfile(); - $tempPath = stream_get_meta_data($tempFile)['uri']; - fwrite($tempFile, $content); - - // Load Word document. - $phpWord = WordIOFactory::load($tempPath); - - // Extract text from all sections. - $text = ''; - foreach ($phpWord->getSections() as $section) { - foreach ($section->getElements() as $element) { - if (method_exists($element, 'getText') === true) { - $text .= $element->getText()."\n"; - } else if (method_exists($element, 'getElements') === true) { - // Handle nested elements (tables, etc.). - foreach ($element->getElements() as $childElement) { - if (method_exists($childElement, 'getText') === true) { - $text .= $childElement->getText()." "; - } - } - - $text .= "\n"; - } - } - } - - // Clean up. - fclose($tempFile); - - if (trim($text) === '' || trim($text) === null) { - $this->logger->warning( - message: '[TextExtractionService] Word extraction returned empty text', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - ] - ); - return null; - } - - $this->logger->debug( - message: '[TextExtractionService] Word document extracted successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'length' => strlen($text), - ] - ); - - return $text; - } catch (Exception $e) { - $this->logger->error( - message: '[TextExtractionService] Word extraction failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'error' => $e->getMessage(), - ] - ); - throw new Exception("Word extraction failed: ".$e->getMessage()); - }//end try + // Delegated to the dedicated WordExtractor handler + // (extract-god-class-services) — behaviour unchanged. + return $this->wordExtractor->extract(file: $file); }//end extractWord() /** @@ -1553,114 +1403,9 @@ private function extractWord(\OCP\Files\File $file): ?string */ private function extractSpreadsheet(\OCP\Files\File $file): ?string { - // PhpSpreadsheet should already be installed (in composer.json). - if (class_exists('PhpOffice\PhpSpreadsheet\IOFactory') === false) { - $this->logger->warning( - message: '[TextExtractionService] PhpSpreadsheet library not available', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - ] - ); - $msg = "PhpSpreadsheet library (phpoffice/phpspreadsheet) is not installed. "; - $msg .= "Run: composer require phpoffice/phpspreadsheet"; - throw new Exception($msg); - } - - try { - $this->logger->debug( - message: '[TextExtractionService] Extracting spreadsheet', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'name' => $file->getName(), - ] - ); - - // Get file content. - $content = $file->getContent(); - - // Create temporary file for PhpSpreadsheet. - $tempFile = tmpfile(); - $tempPath = stream_get_meta_data($tempFile)['uri']; - fwrite($tempFile, $content); - - // Load spreadsheet. - $spreadsheet = SpreadsheetIOFactory::load($tempPath); - - // Extract text from all sheets. - $text = ''; - foreach ($spreadsheet->getAllSheets() as $sheet) { - $text .= "Sheet: ".$sheet->getTitle()."\n"; - - $highestRow = $sheet->getHighestRow(); - $highestColumn = $sheet->getHighestColumn(); - - // Iterate through rows and columns. - for ($row = 1; $row <= $highestRow; $row++) { - $rowData = []; - // @psalm-suppress StringIncrement - Excel column increment is intentional - for ($col = 'A'; $col !== $highestColumn; $col++) { - $value = $sheet->getCell($col.$row)->getValue(); - if ($value !== null && $value !== '') { - $rowData[] = $value; - } - } - - // Add last column. - $value = $sheet->getCell($highestColumn.$row)->getValue(); - if ($value !== null && $value !== '') { - $rowData[] = $value; - } - - if (empty($rowData) === false) { - $text .= implode("\t", $rowData)."\n"; - } - } - - $text .= "\n"; - }//end foreach - - // Clean up. - fclose($tempFile); - - if (trim($text) === '' || trim($text) === null) { - $this->logger->warning( - message: '[TextExtractionService] Spreadsheet extraction returned empty text', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - ] - ); - return null; - } - - $this->logger->debug( - message: '[TextExtractionService] Spreadsheet extracted successfully', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'length' => strlen($text), - ] - ); - - return $text; - } catch (Exception $e) { - $this->logger->error( - message: '[TextExtractionService] Spreadsheet extraction failed', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'fileId' => $file->getId(), - 'error' => $e->getMessage(), - ] - ); - throw new Exception("Spreadsheet extraction failed: ".$e->getMessage()); - }//end try + // Delegated to the dedicated SpreadsheetExtractor handler + // (extract-god-class-services) — behaviour unchanged. + return $this->spreadsheetExtractor->extract(file: $file); }//end extractSpreadsheet() /** @@ -1674,9 +1419,10 @@ private function extractSpreadsheet(\OCP\Files\File $file): ?string * * @return string|null Flat plain-text, or null when the file cannot be parsed. * - * @SuppressWarnings(PHPMD.StaticAccess) EmlParser::sanitisePiiForLogging is a stateless utility; making it non-static would not improve testability or DI. + * @SuppressWarnings(PHPMD.StaticAccess) EmlParser::sanitisePiiForLogging is a stateless utility; + * making it non-static would not improve testability or DI. * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ private function extractEml(\OCP\Files\File $file): ?string { @@ -1715,7 +1461,7 @@ private function extractEml(\OCP\Files\File $file): ?string * * @throws \OCA\OpenRegister\Exception\EmlParseException * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-30 + * @spec openspec/specs/text-extraction-eml/spec.md#requirement-extracteml-and-parseemlstructured-must-not-log-pii-adr-005 */ public function parseEmlStructured(\OCP\Files\File $file): \OCA\OpenRegister\Service\TextExtraction\EmlStructure { @@ -1869,11 +1615,24 @@ private function chunkFixedSize(string $text, int $chunkSize, int $chunkOverlap) $chunkLength = strlen($chunk); - if (strlen(trim($chunk)) >= self::MIN_CHUNK_SIZE) { - $chunks[] = [ - 'text' => trim($chunk), - 'start_offset' => $offset, - 'end_offset' => $offset + $chunkLength, + $trimmedChunk = trim($chunk); + if (strlen($trimmedChunk) >= self::MIN_CHUNK_SIZE) { + // Align the persisted absolute offset with the trimmed text we + // store. trim() drops leading whitespace, so the first char of + // `text_content` sits `leadingWhitespace` bytes after the raw + // window start ($offset). Persisting the raw $offset would make + // every regex offset computed against `text_content` (and added + // to start_offset by ChunkTextMatcher) wrong by that amount — + // and two overlapping chunks with different leading-whitespace + // counts would then derive DIFFERENT absolute positions for the + // same occurrence, defeating absolute-position dedup and + // inserting duplicate EntityRelation rows (design §D2). + $leadingWhitespace = (strlen($chunk) - strlen(ltrim($chunk))); + $trimmedStart = ($offset + $leadingWhitespace); + $chunks[] = [ + 'text' => $trimmedChunk, + 'start_offset' => $trimmedStart, + 'end_offset' => ($trimmedStart + strlen($trimmedChunk)), ]; } @@ -2092,6 +1851,7 @@ private function isWordDocument(string $mimeType): bool $wordTypes = [ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/msword', + 'application/vnd.oasis.opendocument.text', ]; return in_array($mimeType, $wordTypes, true) === true; diff --git a/lib/Service/TimeTrackerLinkService.php b/lib/Service/TimeTrackerLinkService.php index 7643b2afb6..9423e59384 100644 --- a/lib/Service/TimeTrackerLinkService.php +++ b/lib/Service/TimeTrackerLinkService.php @@ -71,7 +71,10 @@ * session + app manager + container + logger. Each dependency is * required for one of the Tier-2 flows (link, create, unlink, list, * picker, cache refresh, graceful degradation). - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Tier-2 service implements linkEntry/createAndLinkClient/unlink/getLinkedEntries/getAvailableClients plus resolveMapper/requireUid/normaliseEntryType/idComponents/fetchEntryInfo/hydrateLink/insertClient/normaliseRow/isStale/refreshEntry; each is required for a distinct flow in the integration surface. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Tier-2 service implements + * linkEntry/createAndLinkClient/unlink/getLinkedEntries/getAvailableClients plus + * resolveMapper/requireUid/normaliseEntryType/idComponents/fetchEntryInfo/hydrateLink/insertClient/normaliseRow/isStale/refreshEntry; + * each is required for a distinct flow in the integration surface. */ class TimeTrackerLinkService { @@ -100,7 +103,9 @@ class TimeTrackerLinkService * @param IUserSession $userSession Active session. * @param LoggerInterface $logger Logger. * - * @SuppressWarnings(PHPMD.LongVariable) $timeTrackerLinkMapper follows the repo naming convention for OR link-table mappers; $timeTrackerLinkMapper is the exact constructor-injected variable name used across the Tier-2 service pattern and abbreviating it would obscure its purpose. + * @SuppressWarnings(PHPMD.LongVariable) $timeTrackerLinkMapper follows the repo naming convention for OR + * link-table mappers; $timeTrackerLinkMapper is the exact constructor-injected variable name used across + * the Tier-2 service pattern and abbreviating it would obscure its purpose. */ public function __construct( private readonly TimeTrackerLinkMapper $timeTrackerLinkMapper, @@ -621,6 +626,139 @@ private function fetchClientRows(object $clientMapper, string $uid, ?string $sea }//end try }//end fetchClientRows() + /** + * Reconcile every persisted link row's denormalised entry metadata + * (name, duration, billable, started_at) against the authoritative + * NC TimeManager row. + * + * Used by the `openregister:time:reconcile` occ command — exposes + * the same refresh path the on-read drift-guard uses, but driven + * for every link irrespective of staleness so totals can be + * recomputed off-line (per the integration-time-tracker spec's + * "denormalised totals" requirement). + * + * Each link is refreshed using its own `linkedBy` user id so the + * upstream user-scoped TimeManager lookup stays correct even when + * the command runs without an active user session (e.g. cron). + * + * @param string|null $objectUuid Optional object uuid scope. Null walks every row. + * @param bool $dryRun When true, no DB writes happen; counts are returned regardless. + * + * @return array{walked: int, refreshed: int, missing: int, errors: int} + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The dry-run toggle is the conventional shape for occ commands. + * + * @spec openspec/specs/integration-time-tracker/spec.md + */ + public function reconcileAllLinks(?string $objectUuid=null, bool $dryRun=false): array + { + $stats = [ + 'walked' => 0, + 'refreshed' => 0, + 'missing' => 0, + 'errors' => 0, + ]; + + if ($this->isTimeManagerAvailable() === false) { + return $stats; + } + + try { + $links = $this->timeTrackerLinkMapper->findAll($objectUuid); + } catch (Throwable $e) { + $this->logger->error('TimeTrackerLinkService::reconcileAllLinks findAll failed: '.$e->getMessage()); + $stats['errors'] = 1; + return $stats; + } + + foreach ($links as $link) { + $stats['walked']++; + $uid = (string) $link->getLinkedBy(); + if ($uid === '') { + $stats['missing']++; + continue; + } + + $info = $this->fetchEntryInfo( + entryType: (string) $link->getEntryType(), + id: $this->entryId(link: $link), + uid: $uid + ); + if ($info === null) { + $stats['missing']++; + continue; + } + + $changed = $this->applyRefreshedInfo(link: $link, info: $info); + if ($changed === false) { + continue; + } + + $stats['refreshed']++; + + if ($dryRun === true) { + continue; + } + + $link->setLinkedAt(new DateTime()); + try { + $this->timeTrackerLinkMapper->update($link); + } catch (Throwable $e) { + $this->logger->error( + 'TimeTrackerLinkService::reconcileAllLinks update failed for link id ' + .((string) $link->getId()).': '.$e->getMessage() + ); + $stats['errors']++; + } + }//end foreach + + return $stats; + }//end reconcileAllLinks() + + /** + * Apply refreshed upstream metadata onto a link row. Returns whether + * any of the denormalised fields actually changed (so the reconcile + * stats can distinguish "in sync" from "rewritten"). + * + * @param TimeTrackerLink $link The link row. + * @param array $info Normalised upstream info. + * + * @return bool + */ + private function applyRefreshedInfo(TimeTrackerLink $link, array $info): bool + { + $changed = false; + + $name = (string) ($info['name'] ?? ''); + if ($name !== '' && $name !== (string) $link->getName()) { + $link->setName($name); + $changed = true; + } + + $duration = $info['duration'] ?? null; + if ($duration !== null && (int) $duration !== (int) $link->getDuration()) { + $link->setDuration((int) $duration); + $changed = true; + } + + $billable = $info['billable'] ?? null; + if ($billable !== null && (bool) $billable !== (bool) $link->getBillable()) { + $link->setBillable((bool) $billable); + $changed = true; + } + + $startedAt = $info['startedAt'] ?? null; + if ($startedAt instanceof DateTime + && ($link->getStartedAt() === null + || $startedAt->getTimestamp() !== $link->getStartedAt()->getTimestamp()) + ) { + $link->setStartedAt($startedAt); + $changed = true; + } + + return $changed; + }//end applyRefreshedInfo() + /** * Whether a link row's cache is older than the stale window. * @@ -803,7 +941,10 @@ private function queryRowByUuid(string $table, string $uuid, string $uid): ?obje * * @return array * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) normaliseRow() maps four fields (name/duration/billable/startedAt) from NC TimeManager entity objects whose property names differ across client/task/time entry types (`payed` vs `billable`, `start` vs `started_at`); each alternative is required for backward-compatibility with TimeManager's schema evolution. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) normaliseRow() maps four fields (name/duration/billable/startedAt) + * from NC TimeManager entity objects whose property names differ across client/task/time entry types + * (`payed` vs `billable`, `start` vs `started_at`); each alternative is required for backward-compatibility + * with TimeManager's schema evolution. */ private function normaliseRow(object $row): array { diff --git a/lib/Service/TmloService.php b/lib/Service/TmloService.php index 1639a6648b..62ab9b26d7 100644 --- a/lib/Service/TmloService.php +++ b/lib/Service/TmloService.php @@ -26,7 +26,7 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-32 + * @spec openspec/specs/tmlo-validation/spec.md#scenario-valid-iso-8601-duration-accepted */ namespace OCA\OpenRegister\Service; @@ -234,7 +234,7 @@ public function populateDefaults(ObjectEntity $object, Register $register, Schem * * @return string|null ISO-8601 date string or null if invalid duration * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-32 + * @spec openspec/specs/tmlo-validation/spec.md#scenario-valid-iso-8601-duration-accepted */ public function calculateArchiefactiedatum(string $duration): ?string { diff --git a/lib/Service/ToolRegistry.php b/lib/Service/ToolRegistry.php index 71876b2f5a..fcfaf7dec9 100644 --- a/lib/Service/ToolRegistry.php +++ b/lib/Service/ToolRegistry.php @@ -20,7 +20,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-10 + * @spec openspec/specs/chat-ai/spec.md */ namespace OCA\OpenRegister\Service; @@ -103,7 +103,7 @@ class ToolRegistry * @param IEventDispatcher $eventDispatcher Event dispatcher * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-1 + * @spec openspec/specs/ai-mcp/spec.md */ public function __construct( IEventDispatcher $eventDispatcher, @@ -120,7 +120,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-1 + * @spec openspec/specs/ai-mcp/spec.md */ private function loadTools(): void { @@ -165,18 +165,20 @@ private function loadTools(): void * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple validation checks required * @SuppressWarnings(PHPMD.NPathComplexity) Multiple validation paths with exceptions * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-2 + * @spec openspec/specs/ai-mcp/spec.md */ public function registerTool(string $id, ToolInterface $tool, array $metadata): void { - // Validate ID format (should be app_name.tool_name). MCP tool - // ids commonly use camelCase on the right side (e.g. - // `openbuilt.createApp`, `decidesk.listRecentMeetings`) so the - // right-hand side accepts both cases. The left-hand side stays + // Validate ID format (should be app_name.tool_name, or + // app_name.schema.verb for ADR-063 chain-2 schema-derived tools — + // e.g. `pipelinq.lead.search`). MCP tool ids commonly use camelCase + // on the right side (e.g. `openbuild.createApp`, + // `decidesk.listRecentMeetings`) so every segment after the first + // accepts both cases. The left-hand (app id) segment stays // lowercase since it maps to a Nextcloud app id. - if (preg_match('/^[a-z0-9_]+\.[a-zA-Z0-9_]+$/', $id) === 0) { + if (preg_match('/^[a-z0-9_]+(\.[a-zA-Z0-9_]+)+$/', $id) === 0) { throw new InvalidArgumentException( - "Invalid tool ID format: {$id}. Must be 'app_name.tool_name'" + "Invalid tool ID format: {$id}. Must be 'app_name.tool_name' (dot-separated segments)" ); } @@ -218,7 +220,7 @@ public function registerTool(string $id, ToolInterface $tool, array $metadata): * * @return ToolInterface|null Tool instance or null if not found * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-3 + * @spec openspec/specs/ai-mcp/spec.md */ public function getTool(string $id): ?ToolInterface { @@ -236,8 +238,8 @@ public function getTool(string $id): ?ToolInterface * * @return array Array of tool IDs and their metadata * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-3 - * @spec openspec/changes/retrofit-2026-05-24-annotate-openregister/tasks.md#task-10 + * @spec openspec/specs/ai-mcp/spec.md + * @spec openspec/specs/chat-ai/spec.md */ public function getAllTools(): array { @@ -260,7 +262,7 @@ public function getAllTools(): array * * @return array Array of ToolInterface instances (key: id, value: tool) * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-3 + * @spec openspec/specs/ai-mcp/spec.md */ public function getTools(array $ids): array { diff --git a/lib/Service/Translation/IdentityTranslationProvider.php b/lib/Service/Translation/IdentityTranslationProvider.php index abfada1123..3050eb70ea 100644 --- a/lib/Service/Translation/IdentityTranslationProvider.php +++ b/lib/Service/Translation/IdentityTranslationProvider.php @@ -39,7 +39,7 @@ class IdentityTranslationProvider implements TranslationProviderInterface * * @return string|null Always the source text (passthrough). * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-8 + * @spec openspec/specs/register-i18n/spec.md */ public function translate(string $text, string $fromLang, string $toLang): ?string { @@ -54,7 +54,7 @@ public function translate(string $text, string $fromLang, string $toLang): ?stri * * @return string The literal `identity`. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-9 + * @spec openspec/specs/register-i18n/spec.md */ public function getIdentifier(): string { diff --git a/lib/Service/Translation/TranslationCsvCodec.php b/lib/Service/Translation/TranslationCsvCodec.php index e6e3287f0f..2c01ccba00 100644 --- a/lib/Service/Translation/TranslationCsvCodec.php +++ b/lib/Service/Translation/TranslationCsvCodec.php @@ -58,7 +58,7 @@ public function __construct( * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-12 + * @spec openspec/specs/register-i18n/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -134,7 +134,7 @@ public function flattenForCsv(array $data, Schema $schema): array * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-13 + * @spec openspec/specs/register-i18n/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ diff --git a/lib/Service/Translation/TranslationProviderInterface.php b/lib/Service/Translation/TranslationProviderInterface.php index a06c29877b..97bef5c5d1 100644 --- a/lib/Service/Translation/TranslationProviderInterface.php +++ b/lib/Service/Translation/TranslationProviderInterface.php @@ -45,7 +45,7 @@ interface TranslationProviderInterface * * @return string|null The translated text, or null on miss/error. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-10 + * @spec openspec/specs/register-i18n/spec.md */ public function translate(string $text, string $fromLang, string $toLang): ?string; @@ -58,7 +58,7 @@ public function translate(string $text, string $fromLang, string $toLang): ?stri * * @return string The provider identifier slug. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-11 + * @spec openspec/specs/register-i18n/spec.md */ public function getIdentifier(): string; }//end interface diff --git a/lib/Service/TranslationProjectionService.php b/lib/Service/TranslationProjectionService.php index e50aae5aec..08db3bb3ed 100644 --- a/lib/Service/TranslationProjectionService.php +++ b/lib/Service/TranslationProjectionService.php @@ -31,6 +31,8 @@ namespace OCA\OpenRegister\Service; use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\Register; +use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\Schema; use OCA\OpenRegister\Db\SchemaMapper; use OCA\OpenRegister\Db\Translation; @@ -47,6 +49,7 @@ class TranslationProjectionService * @param TranslationMapper $translationMapper The translation mapper. * @param TranslationHandler $translationHandler The translation handler. * @param SchemaMapper $schemaMapper The schema mapper. + * @param RegisterMapper $registerMapper The register mapper. * @param IUserSession $userSession The user session. * @param LoggerInterface $logger The logger. */ @@ -54,6 +57,7 @@ public function __construct( private readonly TranslationMapper $translationMapper, private readonly TranslationHandler $translationHandler, private readonly SchemaMapper $schemaMapper, + private readonly RegisterMapper $registerMapper, private readonly IUserSession $userSession, private readonly LoggerInterface $logger ) { @@ -79,7 +83,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-1 + * @spec openspec/specs/register-i18n/spec.md * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -117,6 +121,23 @@ public function project(ObjectEntity $object): void $data = (array) ($object->getObject() ?? []); $translator = $this->userSession->getUser()?->getUID(); + $register = $this->loadRegister(object: $object); + if ($register !== null) { + $defaultLanguage = $register->getDefaultLanguage(); + } else { + $defaultLanguage = 'nl'; + } + + // Per-property resolved source language (i18n-source-of-truth). + $sourceLanguages = []; + foreach ($translatableProps as $property) { + $sourceLanguages[$property] = $this->resolveSourceLanguage( + schema: $schema, + object: $object, + property: $property, + registerDefault: $defaultLanguage + ); + } // Build the desired set of (property, language, value) tuples. $desired = []; @@ -136,17 +157,15 @@ public function project(ObjectEntity $object): void } } } else if (is_string($value) === true && $value !== '') { - // Legacy single-language shape; credit the register's default language. - // We don't have register here without an extra mapper hop; the projection - // can be re-run after register language config changes if needed. - $defaultLang = 'nl'; - $desired[$property][$defaultLang] = $value; + // Legacy single-language shape; credit the resolved source language. + $desired[$property][$sourceLanguages[$property]] = $value; } }//end foreach // Upsert every desired slot. $upsertedKeys = []; foreach ($desired as $property => $byLang) { + $sourceLanguage = $sourceLanguages[$property] ?? $defaultLanguage; foreach ($byLang as $lang => $stringValue) { $this->translationMapper->upsert( objectUuid: $uuid, @@ -155,7 +174,8 @@ public function project(ObjectEntity $object): void value: $stringValue, status: null, // Preserve existing or default to draft on insert. - translator: $translator + translator: $translator, + sourceLanguage: $sourceLanguage ); $upsertedKeys[] = $property.'|'.$lang; } @@ -187,7 +207,7 @@ public function project(ObjectEntity $object): void * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-2 + * @spec openspec/specs/register-i18n/spec.md */ public function purge(ObjectEntity $object): void { @@ -226,6 +246,83 @@ private function loadSchema(ObjectEntity $object): ?Schema } }//end loadSchema() + /** + * Resolve a register entity from an object's register reference. + * + * @param ObjectEntity $object The object whose register reference should be resolved. + * + * @return Register|null The resolved register, or null when not resolvable. + */ + private function loadRegister(ObjectEntity $object): ?Register + { + $ref = $object->getRegister(); + if ($ref === null || $ref === '') { + return null; + } + + try { + return $this->registerMapper->find((string) $ref, _rbac: false, _multitenancy: false); + } catch (\Throwable $e) { + return null; + } + }//end loadRegister() + + /** + * Resolve the source language for a single (object, property) tuple. + * + * Lookup chain (highest precedence first): + * 1. object body `_translationMeta..sourceLanguage` + * 2. schema `properties..sourceLanguage` + * 3. supplied register default + * 4. hardcoded `'nl'` fallback + * + * @param Schema $schema The schema declaring the property. + * @param ObjectEntity $object The object body containing optional override. + * @param string $property The translatable property name. + * @param string $registerDefault Register-level default language. + * + * @return string The resolved BCP-47 source language for the property. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-2 + */ + public function resolveSourceLanguage( + Schema $schema, + ObjectEntity $object, + string $property, + string $registerDefault + ): string { + // Step 1 — object-level override. + $body = (array) ($object->getObject() ?? []); + $meta = $body['_translationMeta'] ?? null; + if (is_array($meta) === true + && isset($meta[$property]) === true + && is_array($meta[$property]) === true + && isset($meta[$property]['sourceLanguage']) === true + && is_string($meta[$property]['sourceLanguage']) === true + && $meta[$property]['sourceLanguage'] !== '' + ) { + return (string) $meta[$property]['sourceLanguage']; + } + + // Step 2 — schema-property modifier. + $properties = $schema->getProperties() ?? []; + $definition = $properties[$property] ?? null; + if (is_array($definition) === true + && isset($definition['sourceLanguage']) === true + && is_string($definition['sourceLanguage']) === true + && $definition['sourceLanguage'] !== '' + ) { + return (string) $definition['sourceLanguage']; + } + + // Step 3 — register default; Step 4 — hardcoded fallback. + if ($registerDefault !== '') { + return $registerDefault; + } + + return 'nl'; + }//end resolveSourceLanguage() + /** * Coerce a translatable value to a string for storage. * diff --git a/lib/Service/TranslationStatusService.php b/lib/Service/TranslationStatusService.php index 8a6488b5c3..f05670cd4f 100644 --- a/lib/Service/TranslationStatusService.php +++ b/lib/Service/TranslationStatusService.php @@ -65,7 +65,7 @@ public function __construct( * * @throws InvalidArgumentException When status is invalid or no slot exists. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-3 + * @spec openspec/specs/register-i18n/spec.md */ public function setStatus(string $objectUuid, string $property, string $language, string $status): Translation { @@ -116,7 +116,7 @@ public function setStatus(string $objectUuid, string $property, string $language * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-4 + * @spec openspec/specs/register-i18n/spec.md */ public function completenessForObject(string $objectUuid, Schema $schema): array { @@ -142,27 +142,138 @@ public function completenessForObject(string $objectUuid, Schema $schema): array /** * Search translation rows. * - * @param string|null $query Optional full-text query. - * @param string|null $language Optional language filter. - * @param string|null $status Optional status filter. - * @param string|null $objectUuid Optional object UUID filter. - * @param int $limit Maximum number of results. + * @param string|null $query Optional full-text query. + * @param string|null $language Optional language filter. + * @param string|null $status Optional status filter. + * @param string|null $objectUuid Optional object UUID filter. + * @param int $limit Maximum number of results. + * @param string|null $sourceLanguage Optional source-language filter. + * @param bool $isOutOfDate When true, restrict to status=outdated. * * @return array[] * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-5 + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-3 */ public function search( ?string $query=null, ?string $language=null, ?string $status=null, ?string $objectUuid=null, - int $limit=100 + int $limit=100, + ?string $sourceLanguage=null, + bool $isOutOfDate=false ): array { - $rows = $this->translationMapper->search($query, $language, $status, $objectUuid, $limit); + $rows = $this->translationMapper->search( + query: $query, + language: $language, + status: $status, + objectUuid: $objectUuid, + limit: $limit, + sourceLanguage: $sourceLanguage, + isOutOfDate: $isOutOfDate + ); return array_map(fn(Translation $t) => $t->jsonSerialize(), $rows); }//end search() + /** + * Search translation rows and attach the source-language value side-by-side. + * + * Used to power `GET /api/translations/search?compareToSource=true` so + * editorial tooling can render the source vs target value pair without + * a follow-up round trip. + * + * @param string|null $query Optional full-text query. + * @param string|null $language Optional language filter. + * @param string|null $status Optional status filter. + * @param string|null $objectUuid Optional object UUID filter. + * @param int $limit Maximum number of results. + * @param string|null $sourceLanguage Optional source-language filter. + * @param bool $isOutOfDate When true, restrict to status=outdated. + * + * @return array[] Each row carries an extra `sourceValue` field. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-3 + */ + public function searchWithSourceValues( + ?string $query=null, + ?string $language=null, + ?string $status=null, + ?string $objectUuid=null, + int $limit=100, + ?string $sourceLanguage=null, + bool $isOutOfDate=false + ): array { + $rows = $this->translationMapper->search( + query: $query, + language: $language, + status: $status, + objectUuid: $objectUuid, + limit: $limit, + sourceLanguage: $sourceLanguage, + isOutOfDate: $isOutOfDate + ); + + // Resolve (uuid, property, sourceLanguage) -> sourceValue lookup once. + $cache = []; + $out = []; + foreach ($rows as $row) { + $entry = $row->jsonSerialize(); + $uuid = (string) $row->getObjectUuid(); + $prop = (string) $row->getProperty(); + $source = (string) ($row->getSourceLanguage() ?? ''); + + $key = $uuid.'|'.$prop.'|'.$source; + if ($source === '') { + $entry['sourceValue'] = null; + } else if (isset($cache[$key]) === true) { + $entry['sourceValue'] = $cache[$key]; + } else { + $sourceRow = $this->translationMapper->findOne($uuid, $prop, $source); + if ($sourceRow !== null) { + $value = $sourceRow->getValue(); + } else { + $value = null; + } + + $cache[$key] = $value; + $entry['sourceValue'] = $value; + } + + $out[] = $entry; + }//end foreach + + return $out; + }//end searchWithSourceValues() + + /** + * Mark every non-source-language Translation row for a property as `outdated`. + * + * When the source-language value for a translatable property changes, + * every derived translation becomes stale by definition; flipping the + * status surface immediately surfaces the staleness to translators + + * editorial tooling. Rows already in `outdated` (or `draft`) status are + * not re-flipped. + * + * @param string $objectUuid UUID of the object whose translations to flip. + * @param string $property Property whose derived translations to flip. + * @param string $sourceLanguage Resolved source language for the property. + * + * @return int Number of derived rows transitioned to `outdated`. + * + * @spec openspec/changes/i18n-source-of-truth/tasks.md#phase-2 + */ + public function markDerivedTranslationsOutdated( + string $objectUuid, + string $property, + string $sourceLanguage + ): int { + if ($objectUuid === '' || $property === '' || $sourceLanguage === '') { + return 0; + } + + return $this->translationMapper->markDerivedOutdated($objectUuid, $property, $sourceLanguage); + }//end markDerivedTranslationsOutdated() + /** * Find objects missing translation slots in a language. * @@ -175,7 +286,7 @@ public function search( * * @return string[] Subset of `$candidateUuids` lacking the language. * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-6 + * @spec openspec/specs/register-i18n/spec.md */ public function findObjectsMissingLanguage(string $language, Schema $schema, array $candidateUuids): array { diff --git a/lib/Service/UploadService.php b/lib/Service/UploadService.php index 710e55fe0e..42a70e4192 100644 --- a/lib/Service/UploadService.php +++ b/lib/Service/UploadService.php @@ -246,9 +246,18 @@ private function processJsonUpload(mixed $jsonInput): array | JSONResponse */ private function getJSONfromURL(string $url): array | JSONResponse { + // Anti-SSRF: reject non-public/loopback/private hosts before fetching (SEC-SVC-1). + try { + SecurityService::assertSafeFetchUrl($url); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => 'URL is not allowed: '.$e->getMessage()], statusCode: 400); + } + try { // Step 1: Make HTTP GET request to fetch data from URL. - $response = $this->client->request('GET', $url); + // Redirects are disabled so the validated host cannot be swapped + // for a private/internal target via a 3xx Location header. + $response = $this->client->request('GET', $url, ['allow_redirects' => false]); } catch (\GuzzleHttp\Exception\BadResponseException $e) { // Return error response if HTTP request fails. $errorMsg = 'Failed to do a GET api-call on url: '.$url.' '.$e->getMessage(); diff --git a/lib/Service/UserService.php b/lib/Service/UserService.php index 7f06fc8dcf..b7f4130fad 100644 --- a/lib/Service/UserService.php +++ b/lib/Service/UserService.php @@ -1166,8 +1166,12 @@ public function setNotificationPreferences(IUser $user, array $prefs): array } $storeValue = (string) $value; - if (is_bool($value) === true) { - $storeValue = ($value === true) ? 'true' : 'false'; + if ($value === true) { + $storeValue = 'true'; + } + + if ($value === false) { + $storeValue = 'false'; } $this->config->setUserValue($userId, self::APP_NAME, 'notification_'.$key, $storeValue); diff --git a/lib/Service/Vectorization/Handlers/EmbeddingGeneratorHandler.php b/lib/Service/Vectorization/Handlers/EmbeddingGeneratorHandler.php index 7e89384745..efd9e2d8e1 100644 --- a/lib/Service/Vectorization/Handlers/EmbeddingGeneratorHandler.php +++ b/lib/Service/Vectorization/Handlers/EmbeddingGeneratorHandler.php @@ -52,6 +52,9 @@ class EmbeddingGeneratorHandler 'text-embedding-ada-002' => 1536, 'text-embedding-3-small' => 1536, 'text-embedding-3-large' => 3072, + 'nomic-embed-text' => 768, + 'mxbai-embed-large' => 1024, + 'all-minilm' => 384, 'ollama-default' => 384, ]; @@ -83,7 +86,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple provider configurations require separate conditions * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function getGenerator(array $config): EmbeddingGeneratorInterface { @@ -132,9 +135,9 @@ public function getGenerator(array $config): EmbeddingGeneratorInterface * * @return int Default dimensions * - * @psalm-return 384|1536|3072 + * @psalm-return 384|768|1024|1536|3072 * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function getDefaultDimensions(string $model): int { @@ -153,7 +156,7 @@ public function getDefaultDimensions(string $model): int * * @throws \Exception If model is not supported * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ private function createOpenAIGenerator( string $model, @@ -192,7 +195,7 @@ private function createOpenAIGenerator( * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Anonymous class requires complete implementation * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ private function createFireworksGenerator(string $model, array $config): object { @@ -246,7 +249,7 @@ public function __construct(string $model, array $config, \Psr\Log\LoggerInterfa * * @throws \Exception If API call fails * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function embedText(string $text): array { @@ -265,6 +268,11 @@ public function embedText(string $text): array $ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // Bound the request: without timeouts a hung/slow embedding + // provider blocks the whole batch (and cron) until PHP's + // max_execution_time kills it (optimize-vectorization-pipeline). + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt( $ch, CURLOPT_HTTPHEADER, @@ -317,7 +325,7 @@ public function embedText(string $text): array * * @psalm-return 768|1024 * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function getEmbeddingLength(): int { @@ -338,7 +346,7 @@ public function getEmbeddingLength(): int * * @return \LLPhant\Embeddings\Document Embedded document * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function embedDocument(\LLPhant\Embeddings\Document $document): \LLPhant\Embeddings\Document { @@ -353,7 +361,7 @@ public function embedDocument(\LLPhant\Embeddings\Document $document): \LLPhant\ * * @return array Embedded documents * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function embedDocuments(array $documents): array { @@ -374,7 +382,7 @@ public function embedDocuments(array $documents): array * * @return OllamaEmbeddingGenerator Generator instance * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ private function createOllamaGenerator(string $model, array $config): OllamaEmbeddingGenerator { diff --git a/lib/Service/Vectorization/Handlers/PgVectorPlatform.php b/lib/Service/Vectorization/Handlers/PgVectorPlatform.php new file mode 100644 index 0000000000..7b8c0c07c2 --- /dev/null +++ b/lib/Service/Vectorization/Handlers/PgVectorPlatform.php @@ -0,0 +1,169 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.nl + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Vectorization\Handlers; + +use Exception; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; + +/** + * PgVectorPlatform + * + * Single source of truth for "is the pgvector fast path available, and at what + * dimension?" — consumed by VectorStorageHandler (dual-write + warm-up backfill) + * and VectorSearchHandler (SQL KNN path). When this helper reports the fast + * path as unavailable, both handlers degrade to the serialized-BLOB path + * exactly as before the hybrid-document-search change. + * + * The fast path lives in the UNPREFIXED sidecar table `openregister_vec_ann` + * (one row per vector: vector_id -> embedding vector(N), HNSW-indexed, cascade + * delete from the main vectors table). It is deliberately outside Nextcloud's + * table prefix so Doctrine's introspectSchema() — which throws "Unknown + * database type vector" for pgvector-typed columns on prefix-matched tables — + * never sees it (live-verified implementation amendment, 2026-07-06). + * + * @category Service + * @package OCA\OpenRegister\Service\Vectorization\Handlers + */ +class PgVectorPlatform +{ + + /** + * Unprefixed pgvector ANN sidecar table name. + * + * Must never carry the Nextcloud table prefix: staying outside the + * `/^/` migration filter is what keeps the vector-typed column + * invisible to Doctrine schema introspection. + */ + public const SIDECAR_TABLE = 'openregister_vec_ann'; + + /** + * Cached sidecar embedding dimension. + * + * False = not yet resolved; null = unavailable (non-Postgres platform, + * sidecar missing, or catalog lookup failed); int = usable dimension. + * + * @var integer|null|false + */ + private int|null|false $columnDimension = false; + + /** + * Constructor + * + * @param IDBConnection $db Database connection + * @param LoggerInterface $logger PSR-3 logger + */ + public function __construct( + private readonly IDBConnection $db, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Whether the active database platform is PostgreSQL. + * + * @return bool True on PostgreSQL + * + * @spec openspec/changes/hybrid-document-search/tasks.md#3.1 + */ + public function isPostgres(): bool + { + $platform = $this->db->getDatabasePlatform(); + + return str_contains(get_class($platform), 'PostgreSQL'); + }//end isPostgres() + + /** + * Get the declared dimension of the ANN sidecar's embedding column. + * + * Returns null when the fast path is unavailable: non-PostgreSQL platform, + * the sidecar table does not exist (pgvector extension missing or the + * migration skipped), or the catalog lookup failed. The result is cached + * per request. + * + * For the pgvector type the pg_attribute.atttypmod value is the declared + * dimension itself (no header offset, unlike varchar). + * + * @return int|null Sidecar embedding dimension, or null when unavailable + * + * @spec openspec/changes/hybrid-document-search/tasks.md#3.1 + */ + public function getVectorColumnDimension(): ?int + { + if ($this->columnDimension !== false) { + return $this->columnDimension; + } + + if ($this->isPostgres() === false) { + $this->columnDimension = null; + return null; + } + + try { + $result = $this->db->executeQuery( + 'SELECT a.atttypmod FROM pg_attribute a ' + ."WHERE a.attrelid = '".self::SIDECAR_TABLE."'::regclass " + ."AND a.attname = 'embedding' AND NOT a.attisdropped" + ); + $typmod = $result->fetchOne(); + $result->closeCursor(); + + if ($typmod === false || (int) $typmod <= 0) { + $this->columnDimension = null; + return null; + } + + $this->columnDimension = (int) $typmod; + return $this->columnDimension; + } catch (Exception $e) { + $this->logger->debug( + message: '[PgVectorPlatform] pgvector ANN sidecar unavailable', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'error' => $e->getMessage(), + ] + ); + $this->columnDimension = null; + return null; + }//end try + }//end getVectorColumnDimension() + + /** + * Format a float array as a pgvector literal ('[0.1,0.2,...]'). + * + * @param array $embedding Embedding vector + * + * @return string pgvector literal + * + * @spec openspec/changes/hybrid-document-search/tasks.md#2.1 + */ + public function formatVector(array $embedding): string + { + return '['.implode(',', array_map(static fn($v): float => (float) $v, $embedding)).']'; + }//end formatVector() +}//end class diff --git a/lib/Service/Vectorization/Handlers/VectorSearchHandler.php b/lib/Service/Vectorization/Handlers/VectorSearchHandler.php index eb11bfe8a9..c70e7a62da 100644 --- a/lib/Service/Vectorization/Handlers/VectorSearchHandler.php +++ b/lib/Service/Vectorization/Handlers/VectorSearchHandler.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-91 + * @spec openspec/specs/zoeken-filteren/spec.md#requirement-view-based-search-composition */ declare(strict_types=1); @@ -30,35 +30,37 @@ use InvalidArgumentException; use OCP\IDBConnection; use Psr\Log\LoggerInterface; -use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Service\Index\Backends\SolrBackend; /** * VectorSearchHandler * * Responsible for searching vectors using semantic search and hybrid search. - * Handles both database (cosine similarity) and Solr (KNN) backends. + * On PostgreSQL with a populated pgvector ANN sidecar (`openregister_vec_ann`) + * the primary path is an index-backed SQL KNN query (`ORDER BY embedding <=> :query`); + * the PHP cosine-similarity loop over serialized BLOBs remains as the explicit + * fallback for MariaDB/SQLite, Postgres installs without the pgvector extension, + * and rows whose stored dimension doesn't match the column. * * @category Service * @package OCA\OpenRegister\Service\Vectorization\Handlers * - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Complex vector search with multiple backend strategies + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) The class deliberately carries BOTH + * ranking paths (index-backed SQL KNN primary + PHP-cosine fallback) plus RRF fusion; + * splitting them would separate two implementations of one contract that must stay + * result-shape-identical. */ class VectorSearchHandler { /** * Constructor * - * @param IDBConnection $db Database connection - * @param SettingsService $settingsService Settings service - * @param IndexService $indexService Index service for Solr - * @param LoggerInterface $logger PSR-3 logger + * @param IDBConnection $db Database connection + * @param PgVectorPlatform $pgVector pgvector fast-path capability helper + * @param LoggerInterface $logger PSR-3 logger */ public function __construct( private readonly IDBConnection $db, - private readonly SettingsService $settingsService, - private readonly IndexService $indexService, + private readonly PgVectorPlatform $pgVector, private readonly LoggerInterface $logger ) { }//end __construct() @@ -66,26 +68,30 @@ public function __construct( /** * Perform semantic similarity search * - * @param array $queryEmbedding Query embedding vector - * @param int $limit Maximum number of results - * @param array $filters Additional filters (entity_type, etc.) - * @param string $backend Search backend ('php', 'database', or 'solr') + * Primary path: SQL K-nearest-neighbour on PostgreSQL via the pgvector + * ANN sidecar (`openregister_vec_ann`) and its HNSW index (index-backed + * `ORDER BY embedding <=> :query LIMIT :n`). Fallback path: fetch + * candidate rows and score them with a PHP cosine loop (MariaDB/SQLite, + * missing pgvector extension, dimension mismatch, or no converted rows + * yet during warm-up). + * + * @param array $queryEmbedding Query embedding vector + * @param int $limit Maximum number of results + * @param array $filters Additional filters (entity_type, etc.) * * @return array> Search results * * @throws \Exception If search fails * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multi-backend search requires multiple conditions - * @SuppressWarnings(PHPMD.NPathComplexity) Complex search path handling - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive semantic search with multiple backends + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Filter handling requires multiple conditions + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive semantic search with error handling * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-1 + * @spec openspec/changes/hybrid-document-search/tasks.md#3.1 */ public function semanticSearch( array $queryEmbedding, int $limit=10, - array $filters=[], - string $backend='php' + array $filters=[] ): array { $startTime = microtime(true); @@ -94,7 +100,6 @@ public function semanticSearch( context: [ 'file' => __FILE__, 'line' => __LINE__, - 'backend' => $backend, 'limit' => $limit, 'filters' => $filters, ] @@ -102,92 +107,120 @@ public function semanticSearch( $results = []; try { - // Route to appropriate backend for vector search. - if ($backend === 'solr') { - $results = $this->searchVectorsInSolr( - queryEmbedding: $queryEmbedding, - limit: $limit, - filters: $filters + // Primary path: index-backed SQL KNN on PostgreSQL/pgvector. + $knnResults = $this->knnSearch( + queryEmbedding: $queryEmbedding, + limit: $limit, + filters: $filters + ); + + if ($knnResults !== null && $knnResults !== []) { + $searchTime = round((microtime(true) - $startTime) * 1000, 2); + + $this->logger->info( + message: '[VectorSearchHandler] Semantic search completed (pgvector KNN path)', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'results_count' => count($knnResults), + 'top_similarity' => $knnResults[0]['similarity'] ?? 0, + 'search_time_ms' => $searchTime, + ] ); + + return $knnResults; } - // Use PHP/database similarity calculation. - if ($backend !== 'solr') { - $vectors = $this->fetchVectors(filters: $filters); + // Fallback path: PHP cosine loop over candidate BLOB rows. + $vectors = $this->fetchVectors(filters: $filters); + + if ($vectors === []) { + $this->logger->warning( + message: '[VectorSearchHandler] No vectors found in database', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'filters' => $filters, + ] + ); + return []; + } + + // Calculate cosine similarity for each vector. + $results = []; + foreach ($vectors as $vector) { + try { + // PostgreSQL returns BLOB columns as stream resources + // (live-verified on PG16); normalise to a string first — + // unserialize() on a resource is a TypeError, which the + // per-row catch below would not have caught. + $blob = $vector['embedding']; + if (is_resource($blob) === true) { + $blob = stream_get_contents($blob); + } + + if (is_string($blob) === false) { + continue; + } + + // SEC-SVC-9: embeddings are plain float arrays; never + // allow object instantiation during unserialize to + // avoid PHP object-injection from a tampered blob. + $storedEmbedding = unserialize($blob, ['allowed_classes' => false]); + + if (is_array($storedEmbedding) === false) { + continue; + } - if ($vectors === []) { + $similarity = $this->cosineSimilarity( + vector1: $queryEmbedding, + vector2: $storedEmbedding + ); + + // Parse metadata. + $metadata = []; + if (empty($vector['metadata']) === false) { + $metadata = json_decode($vector['metadata'], true) ?? []; + } + + $results[] = [ + 'vector_id' => $vector['id'], + 'entity_type' => $vector['entity_type'], + 'entity_id' => $vector['entity_id'], + 'similarity' => $similarity, + 'chunk_index' => $vector['chunk_index'], + 'total_chunks' => $vector['total_chunks'], + 'chunk_text' => $vector['chunk_text'], + 'metadata' => $metadata, + 'model' => $vector['embedding_model'], + 'dimensions' => $vector['embedding_dimensions'], + ]; + } catch (Exception $e) { $this->logger->warning( - message: '[VectorSearchHandler] No vectors found in database', + message: '[VectorSearchHandler] Failed to process vector', context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'filters' => $filters, + 'file' => __FILE__, + 'line' => __LINE__, + 'vector_id' => $vector['id'], + 'error' => $e->getMessage(), ] ); - return []; - } + }//end try + }//end foreach - // Calculate cosine similarity for each vector. - $results = []; - foreach ($vectors as $vector) { - try { - $storedEmbedding = unserialize($vector['embedding']); - - if (is_array($storedEmbedding) === false) { - continue; - } - - $similarity = $this->cosineSimilarity( - vector1: $queryEmbedding, - vector2: $storedEmbedding - ); - - // Parse metadata. - $metadata = []; - if (empty($vector['metadata']) === false) { - $metadata = json_decode($vector['metadata'], true) ?? []; - } - - $results[] = [ - 'vector_id' => $vector['id'], - 'entity_type' => $vector['entity_type'], - 'entity_id' => $vector['entity_id'], - 'similarity' => $similarity, - 'chunk_index' => $vector['chunk_index'], - 'total_chunks' => $vector['total_chunks'], - 'chunk_text' => $vector['chunk_text'], - 'metadata' => $metadata, - 'model' => $vector['embedding_model'], - 'dimensions' => $vector['embedding_dimensions'], - ]; - } catch (Exception $e) { - $this->logger->warning( - message: '[VectorSearchHandler] Failed to process vector', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'vector_id' => $vector['id'], - 'error' => $e->getMessage(), - ] - ); - }//end try - }//end foreach - - // Sort by similarity descending. - usort($results, fn($a, $b) => $b['similarity'] <=> $a['similarity']); - - // Return top N results. - $results = array_slice($results, 0, $limit); - }//end if + // Sort by similarity descending. + usort($results, fn($a, $b) => $b['similarity'] <=> $a['similarity']); + + // Return top N results. + $results = array_slice($results, 0, $limit); $searchTime = round((microtime(true) - $startTime) * 1000, 2); $this->logger->info( - message: '[VectorSearchHandler] Semantic search completed', + message: '[VectorSearchHandler] Semantic search completed (PHP fallback path)', context: [ 'file' => __FILE__, 'line' => __LINE__, - 'backend' => $backend, 'results_count' => count($results), 'top_similarity' => $results[0]['similarity'] ?? 0, 'search_time_ms' => $searchTime, @@ -212,150 +245,133 @@ public function semanticSearch( }//end semanticSearch() /** - * Search vectors in Solr using dense vector KNN - * - * @param array $queryEmbedding Query vector embedding + * Index-backed SQL KNN search on PostgreSQL via pgvector. + * + * Executes `ORDER BY a.embedding <=> :queryVector LIMIT :limit` + * (cosine-distance operator, ascending = most similar first) against the + * HNSW index on the `openregister_vec_ann` sidecar table (joined to the + * main vectors table for entity data), honouring `entity_type` / + * `entity_id` filters as WHERE predicates. Returns null when the fast path + * is unavailable (non-Postgres platform, missing sidecar, or + * query/sidecar dimension mismatch) or when the KNN query itself fails — + * the caller then uses the PHP fallback. + * + * @param array $queryEmbedding Query embedding vector * @param int $limit Maximum number of results - * @param array $filters Additional filters (entity_type, etc.) + * @param array $filters Additional filters (entity_type, entity_id) * - * @return array Vector search results with entity info, similarity scores, and metadata. + * @return array>|null Results, or null when unavailable * - * @throws \Exception If search fails or Solr is not configured. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Filter handling requires multiple conditions + * @SuppressWarnings(PHPMD.NPathComplexity) Multiple filter handling paths * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Solr KNN search requires multiple condition checks - * @SuppressWarnings(PHPMD.NPathComplexity) Complex search path handling - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive Solr KNN search with error handling + * @spec openspec/changes/hybrid-document-search/tasks.md#3.1 */ - private function searchVectorsInSolr( - array $queryEmbedding, - int $limit=10, - array $filters=[] - ): array { - $this->logger->debug( - message: '[VectorSearchHandler] Searching vectors in Solr', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'limit' => $limit, - 'filters' => $filters, - ] - ); - - try { - // Get Solr backend. - $solrBackend = $this->indexService->getBackend(); - if ($solrBackend->isAvailable() === false) { - throw new Exception('Solr service is not available'); - } - - $settings = $this->settingsService->getSettings(); + private function knnSearch(array $queryEmbedding, int $limit, array $filters=[]): ?array + { + $columnDimension = $this->pgVector->getVectorColumnDimension(); - // Get vector field from LLM configuration, default to '_embedding_' field. - $vectorField = $settings['llm']['vectorConfig']['solrField'] ?? '_embedding_'; - $allResults = []; + if ($columnDimension === null || count($queryEmbedding) !== $columnDimension) { + return null; + } - // Determine which collections to search based on entity_type filter. - $collectionsToSearch = $this->getCollectionsToSearch(filters: $filters); + $where = []; + $params = ['qvec' => $this->pgVector->formatVector($queryEmbedding)]; + $types = []; - if ($collectionsToSearch === []) { - throw new Exception('No Solr collections configured for vector search'); + if (($filters['entity_type'] ?? null) !== null) { + if (is_array($filters['entity_type']) === true) { + $where[] = 'v.entity_type IN (:entityTypes)'; + $params['entityTypes'] = $filters['entity_type']; + $types['entityTypes'] = \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR_ARRAY; } - // Build Solr KNN query. - $vectorString = '['.implode(', ', $queryEmbedding).']'; - $knnQuery = "{!knn f={$vectorField} topK={$limit}}{$vectorString}"; - - // Search each collection. - foreach ($collectionsToSearch as $collectionInfo) { - $collection = $collectionInfo['collection']; - $entityType = $collectionInfo['type']; - - $queryParams = [ - 'q' => $knnQuery, - 'rows' => $limit, - 'fl' => '*,score', - 'wt' => 'json', - ]; - - // Cast to SolrBackend to access Solr-specific methods. - if ($solrBackend instanceof SolrBackend === false) { - throw new Exception('Vector search requires SolrBackend'); - } - - $baseUrl = $solrBackend->getHttpClient()->buildSolrBaseUrl(); - $solrUrl = $baseUrl."/{$collection}/select"; - - try { - $response = $solrBackend->getHttpClient()->getHttpClient()->get( - $solrUrl, - ['query' => $queryParams] - ); + if (is_array($filters['entity_type']) === false) { + $where[] = 'v.entity_type = :entityType'; + $params['entityType'] = $filters['entity_type']; + } + } - $responseData = json_decode((string) $response->getBody(), true); + if (($filters['entity_id'] ?? null) !== null) { + if (is_array($filters['entity_id']) === true) { + $where[] = 'v.entity_id IN (:entityIds)'; + $params['entityIds'] = $filters['entity_id']; + $types['entityIds'] = \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR_ARRAY; + } - if (isset($responseData['response']['docs']) === false) { - continue; - } + if (is_array($filters['entity_id']) === false) { + $where[] = 'v.entity_id = :entityId'; + $params['entityId'] = $filters['entity_id']; + } + } - // Transform Solr documents. - foreach ($responseData['response']['docs'] as $doc) { - $allResults[] = [ - 'vector_id' => $doc['id'], - 'entity_type' => $entityType, - 'entity_id' => $this->extractEntityId( - doc: $doc, - entityType: $entityType - ), - 'similarity' => $doc['score'] ?? 0.0, - 'chunk_index' => $doc['chunk_index'] ?? $doc['chunk_index_i'] ?? 0, - 'total_chunks' => $doc['chunk_total'] ?? $doc['total_chunks_i'] ?? 1, - 'chunk_text' => $doc['chunk_text'] ?? $doc['chunk_text_txt'] ?? null, - 'metadata' => $doc, - 'model' => $doc['_embedding_model_'] ?? $doc['embedding_model_s'] ?? '', - 'dimensions' => $doc['_embedding_dim_'] ?? $doc['embedding_dimensions_i'] ?? 0, - ]; - } - } catch (Exception $e) { - $this->logger->warning( - message: '[VectorSearchHandler] Failed to search collection', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'collection' => $collection, - 'error' => $e->getMessage(), - ] - ); - }//end try - }//end foreach + $whereSql = ''; + if ($where !== []) { + $whereSql = 'WHERE '.implode(' AND ', $where).' '; + } - // Sort all results by similarity and limit. - usort($allResults, fn($a, $b) => $b['similarity'] <=> $a['similarity']); - $allResults = array_slice($allResults, 0, $limit); + $sql = 'SELECT v.id, v.entity_type, v.entity_id, v.chunk_index, v.total_chunks, v.chunk_text, ' + .'v.metadata, v.embedding_model, v.embedding_dimensions, ' + .'(a.embedding <=> :qvec::vector) AS distance ' + .'FROM '.PgVectorPlatform::SIDECAR_TABLE.' a ' + .'JOIN *PREFIX*openregister_vectors v ON v.id = a.vector_id ' + .$whereSql + .'ORDER BY a.embedding <=> :qvec::vector ASC ' + .'LIMIT '.max(1, $limit); - return $allResults; + try { + $result = $this->db->executeQuery($sql, $params, $types); + $rows = $result->fetchAll(); + $result->closeCursor(); } catch (Exception $e) { - $this->logger->error( - message: '[VectorSearchHandler] Solr vector search failed', + $this->logger->warning( + message: '[VectorSearchHandler] pgvector KNN query failed, using PHP fallback', context: [ 'file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage(), ] ); - throw new Exception('Solr vector search failed: '.$e->getMessage()); - }//end try - }//end searchVectorsInSolr() + + return null; + } + + $results = []; + foreach ($rows as $row) { + $metadata = []; + if (empty($row['metadata']) === false) { + $metadata = json_decode($row['metadata'], true) ?? []; + } + + $results[] = [ + 'vector_id' => $row['id'], + 'entity_type' => $row['entity_type'], + 'entity_id' => $row['entity_id'], + // Cosine distance = 1 - cosine similarity. + 'similarity' => 1 - (float) $row['distance'], + 'chunk_index' => $row['chunk_index'], + 'total_chunks' => $row['total_chunks'], + 'chunk_text' => $row['chunk_text'], + 'metadata' => $metadata, + 'model' => $row['embedding_model'], + 'dimensions' => $row['embedding_dimensions'], + ]; + }//end foreach + + return $results; + }//end knnSearch() /** - * Perform hybrid search combining keyword (SOLR) and semantic (vectors) + * Perform hybrid search combining keyword and semantic (vectors) * * Uses Reciprocal Rank Fusion (RRF) to combine results. + * The keyword results set is passed in by the caller; vector search + * always runs against the PostgreSQL database backend. * - * @param array $queryEmbedding Query embedding vector - * @param array $solrResults SOLR keyword search results - * @param int $limit Maximum results - * @param array $weights Weights for each search type ['solr' => 0.5, 'vector' => 0.5] - * @param string $backend Vector search backend + * @param array $queryEmbedding Query embedding vector + * @param array $keywordResults Keyword search results to fuse with vector results + * @param int $limit Maximum results + * @param array $weights Weights for each search type ['keyword' => 0.5, 'vector' => 0.5] * * @return (((array|bool|float|int|mixed|null)[]|float|int)[]|float|int)[] * @@ -363,50 +379,48 @@ private function searchVectorsInSolr( * * @psalm-return array{results: list|mixed, solr_rank: int|null, - * solr_score: mixed|null, vector_rank: int|null, + * entity_type: mixed, in_keyword: bool, in_vector: bool, + * metadata: array|mixed, keyword_rank: int|null, + * keyword_score: mixed|null, vector_rank: int|null, * vector_similarity: mixed|null}>, total: int<0, max>, * search_time_ms: float, * source_breakdown: array{vector_only: int<0, max>, - * solr_only: int<0, max>, both: int<0, max>}, - * weights: array{solr: float, vector: float}} + * keyword_only: int<0, max>, both: int<0, max>}, + * weights: array{keyword: float, vector: float}} * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Hybrid search combines multiple result sets * @SuppressWarnings(PHPMD.NPathComplexity) Multiple search path combinations * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive hybrid search with result fusion * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function hybridSearch( array $queryEmbedding, - array $solrResults=[], + array $keywordResults=[], int $limit=20, - array $weights=['solr' => 0.5, 'vector' => 0.5], - string $backend='php' + array $weights=['keyword' => 0.5, 'vector' => 0.5] ): array { $startTime = microtime(true); try { // Validate and normalize weights. - $solrWeight = $weights['solr'] ?? 0.5; - $vectorWeight = $weights['vector'] ?? 0.5; + $keywordWeight = $weights['keyword'] ?? 0.5; + $vectorWeight = $weights['vector'] ?? 0.5; - $totalWeight = $solrWeight + $vectorWeight; + $totalWeight = $keywordWeight + $vectorWeight; if ($totalWeight > 0) { - $solrWeight = $solrWeight / $totalWeight; - $vectorWeight = $vectorWeight / $totalWeight; + $keywordWeight = $keywordWeight / $totalWeight; + $vectorWeight = $vectorWeight / $totalWeight; } - // Perform vector semantic search. + // Perform vector semantic search against PostgreSQL. $vectorResults = []; if ($vectorWeight > 0) { try { $vectorResults = $this->semanticSearch( queryEmbedding: $queryEmbedding, limit: $limit * 2, - filters: [], - backend: $backend + filters: [] ); } catch (Exception $e) { $this->logger->warning( @@ -423,9 +437,9 @@ public function hybridSearch( // Combine results using Reciprocal Rank Fusion (RRF). $combined = $this->reciprocalRankFusion( vectorResults: $vectorResults, - solrResults: $solrResults, + keywordResults: $keywordResults, vectorWeight: $vectorWeight, - solrWeight: $solrWeight + keywordWeight: $keywordWeight ); // Return top N results. @@ -433,17 +447,17 @@ public function hybridSearch( $searchTime = round((microtime(true) - $startTime) * 1000, 2); // Calculate source breakdown. - $vectorOnly = 0; - $solrOnly = 0; - $both = 0; + $vectorOnly = 0; + $keywordOnly = 0; + $both = 0; foreach ($finalResults as $result) { - if ($result['in_vector'] === true && $result['in_solr'] === true) { + if ($result['in_vector'] === true && $result['in_keyword'] === true) { $both++; } else if ($result['in_vector'] === true) { $vectorOnly++; - } else if ($result['in_solr'] === true) { - $solrOnly++; + } else if ($result['in_keyword'] === true) { + $keywordOnly++; } } @@ -452,13 +466,13 @@ public function hybridSearch( 'total' => count($finalResults), 'search_time_ms' => $searchTime, 'source_breakdown' => [ - 'vector_only' => $vectorOnly, - 'solr_only' => $solrOnly, - 'both' => $both, + 'vector_only' => $vectorOnly, + 'keyword_only' => $keywordOnly, + 'both' => $both, ], 'weights' => [ - 'solr' => $solrWeight, - 'vector' => $vectorWeight, + 'keyword' => $keywordWeight, + 'vector' => $vectorWeight, ], ]; } catch (Exception $e) { @@ -480,25 +494,25 @@ public function hybridSearch( /** * Combine search results using Reciprocal Rank Fusion (RRF) * - * @param array $vectorResults Results from vector search - * @param array $solrResults Results from SOLR search - * @param float $vectorWeight Weight for vector results (0-1) - * @param float $solrWeight Weight for SOLR results (0-1) + * @param array $vectorResults Results from vector search + * @param array $keywordResults Results from keyword search + * @param float $vectorWeight Weight for vector results (0-1) + * @param float $keywordWeight Weight for keyword results (0-1) * * @return (array|bool|float|int|mixed|null)[][] * * @psalm-return list|mixed, solr_rank: int|null, - * solr_score: mixed|null, vector_rank: int|null, + * in_keyword: bool, in_vector: bool, + * metadata: array|mixed, keyword_rank: int|null, + * keyword_score: mixed|null, vector_rank: int|null, * vector_similarity: mixed|null}> */ private function reciprocalRankFusion( array $vectorResults, - array $solrResults, + array $keywordResults, float $vectorWeight=0.5, - float $solrWeight=0.5 + float $keywordWeight=0.5 ): array { $k = 60; $combinedScores = []; @@ -515,12 +529,12 @@ private function reciprocalRankFusion( 'chunk_text' => $result['chunk_text'], 'metadata' => $result['metadata'], 'vector_similarity' => $result['similarity'], - 'solr_score' => null, + 'keyword_score' => null, 'combined_score' => 0, 'in_vector' => false, - 'in_solr' => false, + 'in_keyword' => false, 'vector_rank' => null, - 'solr_rank' => null, + 'keyword_rank' => null, ]; } @@ -530,8 +544,8 @@ private function reciprocalRankFusion( $combinedScores[$key]['vector_rank'] = (int) $rank + 1; }//end foreach - // Process SOLR results. - foreach ($solrResults as $rank => $result) { + // Process keyword results. + foreach ($keywordResults as $rank => $result) { $key = $result['entity_type'].'_'.$result['entity_id']; if (isset($combinedScores[$key]) === false) { @@ -542,20 +556,20 @@ private function reciprocalRankFusion( 'chunk_text' => $result['chunk_text'] ?? null, 'metadata' => $result['metadata'] ?? [], 'vector_similarity' => null, - 'solr_score' => $result['score'], + 'keyword_score' => $result['score'] ?? null, 'combined_score' => 0, 'in_vector' => false, - 'in_solr' => false, + 'in_keyword' => false, 'vector_rank' => null, - 'solr_rank' => null, + 'keyword_rank' => null, ]; } - $rrfScore = $solrWeight / ($k + (int) $rank + 1); + $rrfScore = $keywordWeight / ($k + (int) $rank + 1); $combinedScores[$key]['combined_score'] += $rrfScore; - $combinedScores[$key]['in_solr'] = true; - $combinedScores[$key]['solr_rank'] = (int) $rank + 1; - $combinedScores[$key]['solr_score'] = $result['score']; + $combinedScores[$key]['in_keyword'] = true; + $combinedScores[$key]['keyword_rank'] = (int) $rank + 1; + $combinedScores[$key]['keyword_score'] = $result['score'] ?? null; }//end foreach // Convert to array and sort by combined score. @@ -576,6 +590,8 @@ private function reciprocalRankFusion( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Filter handling requires multiple conditions * @SuppressWarnings(PHPMD.NPathComplexity) Multiple filter handling paths + * + * @spec openspec/changes/hybrid-document-search/tasks.md#3.2 */ private function fetchVectors(array $filters=[]): array { @@ -621,10 +637,16 @@ private function fetchVectors(array $filters=[]): array } } - // Performance optimization: Limit vectors fetched. + // Approximate-fallback bound (hybrid-document-search, decision 4): + // max_vectors caps how many BLOB rows the O(n) PHP cosine fallback + // will unserialize and score per request. It is a documented safety + // cap, NOT a relevance mechanism — ordering is by primary key + // (stable, non-biasing) instead of the former `created_at DESC`, + // which silently substituted "newest" for "most relevant" past the + // cap. The index-backed pgvector KNN path has no such cap. $maxVectors = $filters['max_vectors'] ?? 500; $qb->setMaxResults($maxVectors); - $qb->orderBy('created_at', 'DESC'); + $qb->orderBy('id', 'ASC'); $result = $qb->executeQuery(); $vectors = $result->fetchAll(); @@ -677,98 +699,4 @@ private function cosineSimilarity(array $vector1, array $vector2): float return $dotProduct / ($magnitude1 * $magnitude2); }//end cosineSimilarity() - - /** - * Extract entity ID from Solr document based on entity type - * - * @param array $doc Solr document - * @param string $entityType Entity type ('file' or 'object') - * - * @return string Entity ID - */ - private function extractEntityId(array $doc, string $entityType): string - { - if ($entityType === 'file' || $entityType === 'files') { - return (string) ($doc['file_id'] ?? $doc['file_id_l'] ?? ''); - } - - return $doc['self_uuid'] ?? $doc['self_object_id'] ?? $doc['id'] ?? ''; - }//end extractEntityId() - - /** - * Get collections to search based on filters - * - * @param array $filters Search filters - * - * @return array Collections to search - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Collection resolution requires multiple conditions - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple filter and collection resolution paths - * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-91 - */ - private function getCollectionsToSearch(array $filters): array - { - $collectionsToSearch = []; - $settings = $this->settingsService->getSettings(); - - if (($filters['entity_type'] ?? null) !== null) { - $entityTypes = []; - if (is_array($filters['entity_type']) === true) { - $entityTypes = $filters['entity_type']; - } - - if (is_array($filters['entity_type']) === false) { - $entityTypes = [$filters['entity_type']]; - } - - foreach ($entityTypes as $entityType) { - $collection = $this->getSolrCollectionForEntityType( - entityType: $entityType, - settings: $settings - ); - if ($collection !== null && $collection !== '') { - $collectionsToSearch[] = [ - 'type' => $entityType, - 'collection' => $collection, - ]; - } - } - }//end if - - if (($filters['entity_type'] ?? null) === null) { - // Search both object and file collections. - $objectCollection = $settings['solr']['objectCollection'] ?? $settings['solr']['collection'] ?? null; - $fileCollection = $settings['solr']['fileCollection'] ?? null; - - if ($objectCollection !== null && $objectCollection !== '') { - $collectionsToSearch[] = ['type' => 'object', 'collection' => $objectCollection]; - } - - if ($fileCollection !== null && $fileCollection !== '') { - $collectionsToSearch[] = ['type' => 'file', 'collection' => $fileCollection]; - } - }//end if - - return $collectionsToSearch; - }//end getCollectionsToSearch() - - /** - * Get Solr collection for entity type - * - * @param string $entityType Entity type - * @param array $settings Settings array - * - * @return string|null Collection name - */ - private function getSolrCollectionForEntityType(string $entityType, array $settings): ?string - { - $entityType = strtolower($entityType); - - if ($entityType === 'file' || $entityType === 'files') { - return $settings['solr']['fileCollection'] ?? null; - } - - return $settings['solr']['objectCollection'] ?? $settings['solr']['collection'] ?? null; - }//end getSolrCollectionForEntityType() }//end class diff --git a/lib/Service/Vectorization/Handlers/VectorStatsHandler.php b/lib/Service/Vectorization/Handlers/VectorStatsHandler.php index cc10cba77b..d6e8702370 100644 --- a/lib/Service/Vectorization/Handlers/VectorStatsHandler.php +++ b/lib/Service/Vectorization/Handlers/VectorStatsHandler.php @@ -3,7 +3,7 @@ /** * Vector Statistics Handler * - * Handles gathering statistics about stored vectors from database and Solr. + * Handles gathering statistics about stored vectors from the database. * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. @@ -27,14 +27,11 @@ use Exception; use OCP\IDBConnection; use Psr\Log\LoggerInterface; -use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; /** * VectorStatsHandler * - * Responsible for gathering statistics about stored vectors. - * Supports both database and Solr backends. + * Responsible for gathering statistics about stored vectors from the PostgreSQL database. * * @category Service * @package OCA\OpenRegister\Service\Vectorization\Handlers @@ -44,40 +41,28 @@ class VectorStatsHandler /** * Constructor * - * @param IDBConnection $db Database connection - * @param SettingsService $settingsService Settings service - * @param IndexService $indexService Index service for Solr - * @param LoggerInterface $logger PSR-3 logger + * @param IDBConnection $db Database connection + * @param LoggerInterface $logger PSR-3 logger */ public function __construct( private readonly IDBConnection $db, - private readonly SettingsService $settingsService, - private readonly IndexService $indexService, private readonly LoggerInterface $logger ) { }//end __construct() /** - * Get vector statistics - * - * @param string $backend Backend to use ('php', 'database', or 'solr') + * Get vector statistics from the PostgreSQL database * * @return ((int|mixed)[]|int|string)[] Statistics about stored vectors * * @psalm-return array{total_vectors: int, by_type: array, - * by_model: array, object_vectors?: int, file_vectors?: int, - * source?: 'solr'|'solr_error'|'solr_unavailable'} + * by_model: array, object_vectors: int, file_vectors: int} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 + * @spec openspec/specs/vector-embeddings/spec.md */ - public function getStats(string $backend='php'): array + public function getStats(): array { try { - if ($backend === 'solr') { - return $this->getStatsFromSolr(); - } - - // Default: get stats from database. return $this->getStatsFromDatabase(); } catch (Exception $e) { $this->logger->error( @@ -85,9 +70,11 @@ public function getStats(string $backend='php'): array context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] ); return [ - 'total_vectors' => 0, - 'by_type' => [], - 'by_model' => [], + 'total_vectors' => 0, + 'by_type' => [], + 'by_model' => [], + 'object_vectors' => 0, + 'file_vectors' => 0, ]; }//end try }//end getStats() @@ -100,7 +87,7 @@ public function getStats(string $backend='php'): array * @psalm-return array{total_vectors: int, by_type: array, * by_model: array, object_vectors: int, file_vectors: int} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 + * @spec openspec/specs/vector-embeddings/spec.md */ private function getStatsFromDatabase(): array { @@ -145,186 +132,4 @@ private function getStatsFromDatabase(): array 'file_vectors' => $byType['file'] ?? 0, ]; }//end getStatsFromDatabase() - - /** - * Get vector statistics from Solr collections - * - * @return (array|int|string)[] Vector statistics from Solr - * - * @psalm-return array{ - * total_vectors: int, - * by_type: array{object?: int, file?: int}, - * by_model: array, - * object_vectors: int, - * file_vectors: int, - * source: 'solr'|'solr_error'|'solr_unavailable' - * } - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multi-collection stats gathering requires multiple conditions - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 - */ - private function getStatsFromSolr(): array - { - try { - $solrBackend = $this->indexService->getBackend(); - if ($solrBackend->isAvailable() === false) { - $this->logger->warning( - message: '[VectorStatsHandler] Solr not available for stats', - context: ['file' => __FILE__, 'line' => __LINE__] - ); - return [ - 'total_vectors' => 0, - 'by_type' => [], - 'by_model' => [], - 'object_vectors' => 0, - 'file_vectors' => 0, - 'source' => 'solr_unavailable', - ]; - } - - $settings = $this->settingsService->getSettings(); - - // Get vector field from LLM configuration, default to '_embedding_' field. - $vectorField = $settings['llm']['vectorConfig']['solrField'] ?? '_embedding_'; - $objectCollection = $settings['solr']['objectCollection'] ?? $settings['solr']['collection'] ?? null; - $fileCollection = $settings['solr']['fileCollection'] ?? null; - - $objectCount = 0; - $fileCount = 0; - $byModel = []; - - // Count objects with embeddings. - if ($objectCollection !== null && $objectCollection !== '') { - try { - $objectStats = $this->countVectorsInCollection( - collection: $objectCollection, - vectorField: $vectorField, - solrBackend: $solrBackend - ); - $objectCount = $objectStats['count']; - $byModel = array_merge($byModel, $objectStats['by_model']); - } catch (Exception $e) { - $this->logger->warning( - message: '[VectorStatsHandler] Failed to get object vector stats from Solr', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - } - } - - // Count files with embeddings. - if ($fileCollection !== null && $fileCollection !== '') { - try { - $fileStats = $this->countVectorsInCollection( - collection: $fileCollection, - vectorField: $vectorField, - solrBackend: $solrBackend - ); - $fileCount = $fileStats['count']; - foreach ($fileStats['by_model'] as $model => $count) { - $byModel[$model] = ($byModel[$model] ?? 0) + $count; - } - } catch (Exception $e) { - $this->logger->warning( - message: '[VectorStatsHandler] Failed to get file vector stats from Solr', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - } - } - - $total = $objectCount + $fileCount; - - return [ - 'total_vectors' => $total, - 'by_type' => [ - 'object' => $objectCount, - 'file' => $fileCount, - ], - 'by_model' => $byModel, - 'object_vectors' => $objectCount, - 'file_vectors' => $fileCount, - 'source' => 'solr', - ]; - } catch (Exception $e) { - $this->logger->error( - message: '[VectorStatsHandler] Failed to get vector stats from Solr', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - return [ - 'total_vectors' => 0, - 'by_type' => [], - 'by_model' => [], - 'object_vectors' => 0, - 'file_vectors' => 0, - 'source' => 'solr_error', - ]; - }//end try - }//end getStatsFromSolr() - - /** - * Count vectors in a specific Solr collection - * - * @param string $collection Collection name - * @param string $vectorField Vector field name - * @param mixed $solrBackend Solr backend instance - * - * @return array{count: int, by_model: array} Count and breakdown by model - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Facet processing requires multiple conditions - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 - */ - private function countVectorsInCollection( - string $collection, - string $vectorField, - mixed $solrBackend - ): array { - // Get Solr configuration for authentication. - $settings = $this->settingsService->getSettings(); - $solrConfig = $settings['solr'] ?? []; - - // Build request options. - $options = [ - 'query' => [ - 'q' => "{$vectorField}:*", - 'rows' => 0, - 'wt' => 'json', - 'facet' => 'true', - 'facet.field' => '_embedding_model_', - ], - ]; - - // Add HTTP authentication if configured. - if (empty($solrConfig['username']) === false && empty($solrConfig['password']) === false) { - $options['auth'] = [$solrConfig['username'], $solrConfig['password']]; - } - - // Query Solr. - $solrUrl = $solrBackend->buildSolrBaseUrl()."/{$collection}/select"; - $response = $solrBackend->getHttpClient()->get($solrUrl, $options); - - $data = json_decode((string) $response->getBody(), true); - $count = $data['response']['numFound'] ?? 0; - - // Extract model counts from facets. - $byModel = []; - if (($data['facet_counts']['facet_fields']['_embedding_model_'] ?? null) !== null) { - $facets = $data['facet_counts']['facet_fields']['_embedding_model_']; - $facetCount = count($facets); - for ($i = 0; $i < $facetCount; $i += 2) { - if (($facets[$i] ?? null) !== null && ($facets[$i + 1] ?? null) !== null) { - $modelName = $facets[$i]; - $modelCount = $facets[$i + 1]; - if ($modelName !== null && $modelName !== '' && $modelCount > 0) { - $byModel[$modelName] = $modelCount; - } - } - } - } - - return [ - 'count' => $count, - 'by_model' => $byModel, - ]; - }//end countVectorsInCollection() }//end class diff --git a/lib/Service/Vectorization/Handlers/VectorStorageHandler.php b/lib/Service/Vectorization/Handlers/VectorStorageHandler.php index 486be40d4e..36577ab24b 100644 --- a/lib/Service/Vectorization/Handlers/VectorStorageHandler.php +++ b/lib/Service/Vectorization/Handlers/VectorStorageHandler.php @@ -3,7 +3,8 @@ /** * Vector Storage Handler * - * Handles storing vector embeddings in database and Solr. + * Handles storing vector embeddings in the database (serialized-BLOB storage of + * record plus an opportunistic PostgreSQL pgvector fast-path column). * * SPDX-License-Identifier: EUPL-1.2 * SPDX-FileCopyrightText: 2026 Conduction B.V. @@ -27,43 +28,35 @@ use Exception; use OCP\IDBConnection; use Psr\Log\LoggerInterface; -use OCA\OpenRegister\Service\SettingsService; -use OCA\OpenRegister\Service\IndexService; -use OCA\OpenRegister\Service\Index\Backends\SolrBackend; /** * VectorStorageHandler * - * Responsible for storing vector embeddings in database or Solr. - * Routes storage based on configuration and handles both backends. + * Responsible for storing vector embeddings in the PostgreSQL database. * * @category Service * @package OCA\OpenRegister\Service\Vectorization\Handlers - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ class VectorStorageHandler { /** * Constructor * - * @param IDBConnection $db Database connection - * @param SettingsService $settingsService Settings service - * @param IndexService $indexService Index service for Solr - * @param LoggerInterface $logger PSR-3 logger + * @param IDBConnection $db Database connection + * @param PgVectorPlatform $pgVector pgvector fast-path capability helper + * @param LoggerInterface $logger PSR-3 logger */ public function __construct( private readonly IDBConnection $db, - private readonly SettingsService $settingsService, - private readonly IndexService $indexService, + private readonly PgVectorPlatform $pgVector, private readonly LoggerInterface $logger ) { }//end __construct() /** - * Store vector embedding + * Store vector embedding in the database * - * Routes to database or Solr based on configured backend. + * PostgreSQL is the sole vector storage backend. * * @param string $entityType Entity type ('object' or 'file') * @param string $entityId Entity UUID @@ -74,15 +67,14 @@ public function __construct( * @param int $totalChunks Total number of chunks * @param string|null $chunkText The text that was embedded * @param array $metadata Additional metadata as associative array - * @param string $backend Backend to use ('php', 'database', or 'solr') * - * @return int The ID of the inserted vector (or pseudo-ID for Solr) + * @return int The ID of the inserted vector * * @throws \Exception If storage fails * * @SuppressWarnings(PHPMD.ExcessiveParameterList) Required for flexible vector storage options * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-2 + * @spec openspec/specs/vector-embeddings/spec.md */ public function storeVector( string $entityType, @@ -93,66 +85,19 @@ public function storeVector( int $chunkIndex=0, int $totalChunks=1, ?string $chunkText=null, - array $metadata=[], - string $backend='php' + array $metadata=[] ): int { - $this->logger->debug( - message: '[VectorStorageHandler] Routing vector storage', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'backend' => $backend, - 'entity_type' => $entityType, - 'entity_id' => $entityId, - 'chunk_index' => $chunkIndex, - 'dimensions' => $dimensions, - ] + return $this->storeVectorInDatabase( + entityType: $entityType, + entityId: $entityId, + embedding: $embedding, + model: $model, + dimensions: $dimensions, + chunkIndex: $chunkIndex, + totalChunks: $totalChunks, + chunkText: $chunkText, + metadata: $metadata ); - - try { - // Route to selected backend. - if ($backend === 'solr') { - // Store in Solr and return a pseudo-ID. - $documentId = $this->storeVectorInSolr( - entityType: $entityType, - entityId: $entityId, - embedding: $embedding, - model: $model, - dimensions: $dimensions, - chunkIndex: $chunkIndex, - totalChunks: $totalChunks, - chunkText: $chunkText, - metadata: $metadata - ); - return crc32($documentId); - } - - // Default: Store in database. - return $this->storeVectorInDatabase( - entityType: $entityType, - entityId: $entityId, - embedding: $embedding, - model: $model, - dimensions: $dimensions, - chunkIndex: $chunkIndex, - totalChunks: $totalChunks, - chunkText: $chunkText, - metadata: $metadata - ); - } catch (Exception $e) { - $this->logger->error( - message: '[VectorStorageHandler] Failed to store vector', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'backend' => $backend, - 'error' => $e->getMessage(), - 'entity_type' => $entityType, - 'entity_id' => $entityId, - ] - ); - throw new Exception('Vector storage failed: '.$e->getMessage()); - }//end try }//end storeVector() /** @@ -175,7 +120,7 @@ public function storeVector( * @SuppressWarnings(PHPMD.ExcessiveParameterList) Required for flexible vector storage options * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple storage conditions and error handling * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-2 + * @spec openspec/specs/vector-embeddings/spec.md */ private function storeVectorInDatabase( string $entityType, @@ -237,6 +182,11 @@ private function storeVectorInDatabase( $vectorId = $qb->getLastInsertId(); + // Additive pgvector dual-write (hybrid-document-search, decision 1): + // populate the PostgreSQL fast-path column when available and the + // dimension matches; the BLOB write above stays the storage of record. + $this->populateVectorColumn(vectorId: $vectorId, embedding: $embedding); + $this->logger->info( message: '[VectorStorageHandler] Vector stored successfully in database', context: [ @@ -265,235 +215,185 @@ private function storeVectorInDatabase( }//end storeVectorInDatabase() /** - * Store vector embedding in Solr - * - * Stores a vector embedding in the configured Solr collection using dense vector fields. - * - * @param string $entityType Entity type ('object' or 'file') - * @param string $entityId Entity UUID - * @param array $embedding Vector embedding (array of floats) - * @param string $model Model used to generate embedding - * @param int $dimensions Number of dimensions - * @param int $chunkIndex Chunk index (0 for objects, N for file chunks) - * @param int $totalChunks Total number of chunks (reserved for future use) - * @param string|null $chunkText The text that was embedded (reserved for future use) - * @param array $metadata Additional metadata (reserved for future use) + * Upsert the pgvector ANN sidecar row for a stored vector. * - * @return string The Solr document ID + * PostgreSQL + matching configured dimension only (hybrid-document-search, + * decision 2): vectors whose embedding dimension does not match the + * sidecar's declared dimension get no sidecar row and keep being served by + * the PHP-cosine fallback. Failures are logged, never fatal — the BLOB + * write is the durable storage of record. * - * @throws \Exception If storage fails or Solr is not configured + * @param int $vectorId Vector row id + * @param array $embedding Embedding (array of floats) * - * @psalm-suppress UnusedParam Parameters reserved for future atomic update enhancements + * @return bool True when the sidecar row was written * - * @SuppressWarnings(PHPMD.ExcessiveParameterList) Required for flexible vector storage options - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple Solr storage conditions - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple storage paths with error handling - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive Solr vector storage with atomic updates - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-2 + * @spec openspec/changes/hybrid-document-search/tasks.md#2.1 */ - private function storeVectorInSolr( - string $entityType, - string $entityId, - array $embedding, - string $model, - int $dimensions, - int $chunkIndex=0, - int $totalChunks=1, - ?string $chunkText=null, - array $metadata=[] - ): string { - $this->logger->debug( - message: '[VectorStorageHandler] Storing vector in Solr', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'entity_type' => $entityType, - 'entity_id' => $entityId, - 'chunk_index' => $chunkIndex, - 'dimensions' => $dimensions, - ] - ); - - try { - // Get appropriate Solr collection based on entity type. - $collection = $this->getSolrCollectionForEntityType(entityType: $entityType); - $vectorField = $this->getSolrVectorField(); - - if ($collection === null || $collection === '') { - throw new Exception("Solr collection not configured for entity type: {$entityType}"); - } - - // Get Solr backend from IndexService. - $solrBackend = $this->indexService->getBackend(); - if ($solrBackend->isAvailable() === false) { - throw new Exception('Solr service is not available'); - } - - // Determine document ID based on entity type. - $entityTypeLower = strtolower($entityType); - $documentId = $entityId; - if ($entityTypeLower === 'file' || $entityTypeLower === 'files') { - $documentId = "{$entityId}_chunk_{$chunkIndex}"; - } - - // Prepare atomic update document. - $updateDocument = [ - 'id' => $documentId, - $vectorField => ['set' => $embedding], - '_embedding_model_' => ['set' => $model], - '_embedding_dim_' => ['set' => $dimensions], - 'self_updated' => ['set' => gmdate('Y-m-d\TH:i:s\Z')], - ]; - - $this->logger->debug( - message: '[VectorStorageHandler] Preparing Solr atomic update', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'document_id' => $documentId, - 'collection' => $collection, - 'vector_field' => $vectorField, - 'embedding_size' => count($embedding), - ] - ); - - // Perform atomic update in Solr. - // Cast to SolrBackend to access Solr-specific methods. - if ($solrBackend instanceof SolrBackend === false) { - throw new Exception('Vector storage requires SolrBackend'); - } - - $solrUrl = $solrBackend->getHttpClient()->buildSolrBaseUrl()."/{$collection}/update?commit=true"; - - $response = $solrBackend->getHttpClient()->getHttpClient()->post( - $solrUrl, - [ - 'json' => [$updateDocument], - 'headers' => ['Content-Type' => 'application/json'], - ] - ); - - $responseData = json_decode((string) $response->getBody(), true); + private function populateVectorColumn(int $vectorId, array $embedding): bool + { + $columnDimension = $this->pgVector->getVectorColumnDimension(); - $statusMissing = isset($responseData['responseHeader']['status']) === false; - $statusNotZero = ($responseData['responseHeader']['status'] ?? null) !== 0; - if ($statusMissing === true || $statusNotZero === true) { - throw new Exception('Solr atomic update failed: '.json_encode($responseData)); - } + if ($columnDimension === null || count($embedding) !== $columnDimension) { + return false; + } - $this->logger->info( - message: '[VectorStorageHandler] Vector added to Solr document', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'document_id' => $documentId, - 'collection' => $collection, - 'entity_type' => $entityType, - 'entity_id' => $entityId, - ] + try { + $this->db->executeStatement( + 'INSERT INTO '.PgVectorPlatform::SIDECAR_TABLE.' (vector_id, embedding) ' + .'VALUES (?, ?::vector) ' + .'ON CONFLICT (vector_id) DO UPDATE SET embedding = EXCLUDED.embedding', + [(string) $vectorId, $this->pgVector->formatVector($embedding)] ); - return $documentId; + return true; } catch (Exception $e) { - $this->logger->error( - message: '[VectorStorageHandler] Failed to store vector in Solr', + $this->logger->warning( + message: '[VectorStorageHandler] Failed to write pgvector ANN sidecar row', context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'error' => $e->getMessage(), - 'entity_type' => $entityType, - 'entity_id' => $entityId, - 'chunk_index' => $chunkIndex, + 'file' => __FILE__, + 'line' => __LINE__, + 'vector_id' => $vectorId, + 'error' => $e->getMessage(), ] ); - throw new Exception('Solr vector storage failed: '.$e->getMessage()); + + return false; }//end try - }//end storeVectorInSolr() + }//end populateVectorColumn() /** - * Get the appropriate Solr collection based on entity type + * Warm-up backfill: convert existing BLOB rows to pgvector ANN sidecar rows. + * + * Job-only warm-up (DECIDED 2026-07-06): the migration creates the sidecar + * table and index only; this method — driven by ChunkVectorizationJob — + * converts existing rows in bounded batches, selecting vectors WITHOUT a + * sidecar row (the sidecar equivalent of `embedding_vector IS NULL`) whose + * stored dimension matches the sidecar's declared dimension (idempotent: + * converted rows drop out of the selection). Rows with an unparseable BLOB + * are logged and skipped; the `$afterId` cursor lets the caller make + * progress past persistently-failing rows. * - * @param string $entityType Entity type ('file' or 'object') + * @param int $batchSize Maximum rows to process in this call + * @param int $afterId Only process rows with id > this cursor * - * @return string|null Solr collection name or null if not configured + * @return array{converted: int, failed: int, last_id: int, remaining: int} * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) Collection resolution requires multiple conditions - * @SuppressWarnings(PHPMD.NPathComplexity) Multiple collection determination paths + * @SuppressWarnings(PHPMD.ErrorControlOperator) @unserialize: malformed BLOBs emit + * E_WARNING; the false return is handled explicitly (row skipped + logged). + * @SuppressWarnings(PHPMD.CyclomaticComplexity) One bounded batch loop with explicit + * per-row failure handling (resource normalisation, parse check, upsert outcome). + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Selection + per-row tolerance + + * remaining-count reporting belong to one atomic batch step. * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-2 + * @spec openspec/changes/hybrid-document-search/tasks.md#2.2 */ - private function getSolrCollectionForEntityType(string $entityType): ?string + public function backfillEmbeddingVectors(int $batchSize=100, int $afterId=0): array { - try { - $settings = $this->settingsService->getSettings(); - - // Normalize entity type. - $entityType = strtolower($entityType); - - // Determine which collection to use based on entity type. - $collection = null; - if ($entityType === 'file' || $entityType === 'files') { - $collection = $settings['solr']['fileCollection'] ?? null; - } - - if ($entityType !== 'file' && $entityType !== 'files') { - // Default to object collection. - $collection = $settings['solr']['objectCollection'] ?? $settings['solr']['collection'] ?? null; - } - - if ($collection === null || $collection === '') { - $this->logger->warning( - message: '[VectorStorageHandler] No Solr collection configured for entity type', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'entity_type' => $entityType, - ] - ); - } + $columnDimension = $this->pgVector->getVectorColumnDimension(); + + if ($columnDimension === null) { + return [ + 'converted' => 0, + 'failed' => 0, + 'last_id' => $afterId, + 'remaining' => 0, + ]; + } - return $collection; - } catch (Exception $e) { - $this->logger->warning( - message: '[VectorStorageHandler] Failed to get Solr collection for entity type', - context: [ - 'file' => __FILE__, - 'line' => __LINE__, - 'entity_type' => $entityType, - 'error' => $e->getMessage(), - ] - ); - return null; - }//end try - }//end getSolrCollectionForEntityType() + $converted = 0; + $failed = 0; + $lastId = $afterId; + $sidecar = PgVectorPlatform::SIDECAR_TABLE; - /** - * Get the configured Solr vector field name - * - * @return string Solr vector field name (default: '_embedding_') - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-2 - */ - private function getSolrVectorField(): string - { try { - $settings = $this->settingsService->getSettings(); - - // Get vector field from LLM configuration, default to '_embedding_' field name. - return $settings['llm']['vectorConfig']['solrField'] ?? '_embedding_'; + $result = $this->db->executeQuery( + 'SELECT v.id, v.embedding FROM *PREFIX*openregister_vectors v ' + ."LEFT JOIN $sidecar a ON a.vector_id = v.id " + .'WHERE a.vector_id IS NULL AND v.embedding_dimensions = ? AND v.id > ? ' + .'ORDER BY v.id ASC LIMIT '.((int) $batchSize), + [(string) $columnDimension, (string) $afterId] + ); + $rows = $result->fetchAll(); + $result->closeCursor(); + + foreach ($rows as $row) { + $lastId = (int) $row['id']; + + // PostgreSQL returns BLOB columns as stream resources + // (live-verified on PG16); normalise to a string first. + $blob = $row['embedding']; + if (is_resource($blob) === true) { + $blob = stream_get_contents($blob); + } + + // SEC-SVC-9: embeddings are plain float arrays; never allow + // object instantiation during unserialize. The error-control + // operator suppresses the E_WARNING malformed input emits — + // the false return value is handled explicitly below. + $embedding = false; + if (is_string($blob) === true) { + $embedding = @unserialize($blob, ['allowed_classes' => false]); + } + + if (is_array($embedding) === false || count($embedding) !== $columnDimension) { + $failed++; + $this->logger->warning( + message: '[VectorStorageHandler] Skipping backfill for unparseable embedding BLOB', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'vector_id' => $row['id'], + ] + ); + continue; + } + + $populated = $this->populateVectorColumn(vectorId: (int) $row['id'], embedding: $embedding); + + if ($populated === true) { + $converted++; + } + + if ($populated === false) { + // The populate call logged the failure already. + $failed++; + } + }//end foreach + + $remainingResult = $this->db->executeQuery( + 'SELECT COUNT(*) FROM *PREFIX*openregister_vectors v ' + ."LEFT JOIN $sidecar a ON a.vector_id = v.id " + .'WHERE a.vector_id IS NULL AND v.embedding_dimensions = ?', + [(string) $columnDimension] + ); + $remaining = (int) $remainingResult->fetchOne(); + $remainingResult->closeCursor(); } catch (Exception $e) { - $this->logger->warning( - message: '[VectorStorageHandler] Failed to get Solr vector field, using default', + $this->logger->error( + message: '[VectorStorageHandler] pgvector warm-up backfill batch failed', context: [ 'file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage(), ] ); - return '_embedding_'; - } - }//end getSolrVectorField() + + return [ + 'converted' => $converted, + 'failed' => $failed, + 'last_id' => $lastId, + 'remaining' => 0, + ]; + }//end try + + return [ + 'converted' => $converted, + 'failed' => $failed, + 'last_id' => $lastId, + 'remaining' => $remaining, + ]; + }//end backfillEmbeddingVectors() /** * Sanitize text to prevent UTF-8 encoding errors @@ -504,7 +404,7 @@ private function getSolrVectorField(): string * * @return string Sanitized text safe for UTF-8 storage * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-2 + * @spec openspec/specs/vector-embeddings/spec.md */ private function sanitizeText(string $text): string { diff --git a/lib/Service/Vectorization/Strategies/FileVectorizationStrategy.php b/lib/Service/Vectorization/Strategies/FileVectorizationStrategy.php index 94ae9b99df..3367b1adb9 100644 --- a/lib/Service/Vectorization/Strategies/FileVectorizationStrategy.php +++ b/lib/Service/Vectorization/Strategies/FileVectorizationStrategy.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.nl * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ namespace OCA\OpenRegister\Service\Vectorization\Strategies; @@ -91,7 +91,7 @@ public function __construct( * * @psalm-return list<\OCA\OpenRegister\Db\Chunk> * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function fetchEntities(array $options): array { @@ -155,7 +155,7 @@ public function fetchEntities(array $options): array * * @psalm-return list * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function extractVectorizationItems($entity): array { @@ -191,7 +191,7 @@ public function extractVectorizationItems($entity): array * } * } * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function prepareVectorMetadata($entity, array $item): array { diff --git a/lib/Service/Vectorization/Strategies/ObjectVectorizationStrategy.php b/lib/Service/Vectorization/Strategies/ObjectVectorizationStrategy.php index 7411afef1a..a4f4a996c2 100644 --- a/lib/Service/Vectorization/Strategies/ObjectVectorizationStrategy.php +++ b/lib/Service/Vectorization/Strategies/ObjectVectorizationStrategy.php @@ -88,7 +88,7 @@ public function __construct( * * @psalm-return list<\OCA\OpenRegister\Db\ObjectEntity> * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-4 + * @spec openspec/specs/vector-embeddings/spec.md */ public function fetchEntities(array $options): array { @@ -147,7 +147,7 @@ public function fetchEntities(array $options): array * * @psalm-return list{array{text: string, index: 0}} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-4 + * @spec openspec/specs/vector-embeddings/spec.md */ public function extractVectorizationItems($entity): array { @@ -204,7 +204,7 @@ public function extractVectorizationItems($entity): array * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex metadata extraction with multiple fallbacks * @SuppressWarnings(PHPMD.NPathComplexity) Multiple field extraction paths * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-4 + * @spec openspec/specs/vector-embeddings/spec.md */ public function prepareVectorMetadata($entity, array $item): array { @@ -287,7 +287,7 @@ public function prepareVectorMetadata($entity, array $item): array * * @return array Array of @self keys * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-4 + * @spec openspec/specs/vector-embeddings/spec.md */ private function extractSelfKeys(array $objectData): array { @@ -311,7 +311,7 @@ private function extractSelfKeys(array $objectData): array * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple field type checks required * @SuppressWarnings(PHPMD.NPathComplexity) Multiple field validation paths * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-4 + * @spec openspec/specs/vector-embeddings/spec.md */ private function extractFirstStringField(array $objectData): ?string { @@ -345,7 +345,7 @@ private function extractFirstStringField(array $objectData): ?string * * @return string|int Object ID * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-4 + * @spec openspec/specs/vector-embeddings/spec.md */ public function getEntityIdentifier($entity) { @@ -369,7 +369,7 @@ public function getEntityIdentifier($entity) * * @return false|string Serialized text * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-4 + * @spec openspec/specs/vector-embeddings/spec.md */ private function serializeObject(array $object, array $config): string|false { diff --git a/lib/Service/Vectorization/Strategies/VectorizationStrategyInterface.php b/lib/Service/Vectorization/Strategies/VectorizationStrategyInterface.php index 24b08a75a5..f4573fc9bd 100644 --- a/lib/Service/Vectorization/Strategies/VectorizationStrategyInterface.php +++ b/lib/Service/Vectorization/Strategies/VectorizationStrategyInterface.php @@ -44,7 +44,7 @@ interface VectorizationStrategyInterface * * @return array Array of entities to vectorize * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function fetchEntities(array $options): array; @@ -63,7 +63,7 @@ public function fetchEntities(array $options): array; * * @return array Array of items, each with 'text' and other data * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function extractVectorizationItems($entity): array; @@ -83,7 +83,7 @@ public function extractVectorizationItems($entity): array; * * @return array Metadata for vector storage * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function prepareVectorMetadata($entity, array $item): array; @@ -94,7 +94,7 @@ public function prepareVectorMetadata($entity, array $item): array; * * @return string|int Identifier * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function getEntityIdentifier($entity); }//end interface diff --git a/lib/Service/Vectorization/VectorEmbeddings.php b/lib/Service/Vectorization/VectorEmbeddings.php index 0f7c44d5c4..069efccfe1 100644 --- a/lib/Service/Vectorization/VectorEmbeddings.php +++ b/lib/Service/Vectorization/VectorEmbeddings.php @@ -88,7 +88,7 @@ public function __construct( * * @psalm-return array{embedding: array, model: string, dimensions: int<0, max>} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function generateEmbedding(string $text, ?string $provider=null): array { @@ -154,7 +154,7 @@ public function generateEmbedding(string $text, ?string $provider=null): array * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Complex config validation logic * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function generateEmbeddingWithCustomConfig(string $text, array $config): array { @@ -233,7 +233,7 @@ public function generateEmbeddingWithCustomConfig(string $text, array $config): * * @return array * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function testEmbedding(string $provider, array $config, string $testText='Test.'): array { @@ -303,14 +303,17 @@ public function testEmbedding(string $provider, array $config, string $testText= /** * Generate embeddings for multiple texts in batch * + * Per-text failures are tolerated: the failing entry carries a null + * embedding and an error message instead of aborting the whole batch. + * * @param array $texts Array of texts to embed * @param string|null $provider Embedding provider * - * @return array, model: string, dimensions: int}> Array of embeddings + * @return array|null, model: string, dimensions: int, error?: string}> Array of embeddings * * @throws \Exception If batch embedding generation fails * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function generateBatchEmbeddings(array $texts, ?string $provider=null): array { @@ -392,7 +395,7 @@ public function generateBatchEmbeddings(array $texts, ?string $provider=null): a // ============================================================================= /** - * Store vector embedding + * Store vector embedding in the PostgreSQL database * * @param string $entityType Entity type ('object' or 'file') * @param string $entityId Entity UUID @@ -408,7 +411,7 @@ public function generateBatchEmbeddings(array $texts, ?string $provider=null): a * * @throws \Exception If storage fails * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-2 + * @spec openspec/specs/vector-embeddings/spec.md */ public function storeVector( string $entityType, @@ -421,8 +424,6 @@ public function storeVector( ?string $chunkText=null, array $metadata=[] ): int { - $backend = $this->getVectorSearchBackend(); - return $this->storageHandler->storeVector( entityType: $entityType, entityId: $entityId, @@ -432,8 +433,7 @@ public function storeVector( chunkIndex: $chunkIndex, totalChunks: $totalChunks, chunkText: $chunkText, - metadata: $metadata, - backend: $backend + metadata: $metadata ); }//end storeVector() @@ -453,7 +453,7 @@ public function storeVector( * * @throws \Exception If search fails * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function semanticSearch( string $query, @@ -465,55 +465,47 @@ public function semanticSearch( $queryEmbeddingData = $this->generateEmbedding(text: $query, provider: $provider); $queryEmbedding = $queryEmbeddingData['embedding']; - // Delegate to search handler. - $backend = $this->getVectorSearchBackend(); - return $this->searchHandler->semanticSearch( queryEmbedding: $queryEmbedding, limit: $limit, - filters: $filters, - backend: $backend + filters: $filters ); }//end semanticSearch() /** - * Perform hybrid search combining keyword (SOLR) and semantic (vectors) + * Perform hybrid search combining keyword and semantic (vector) results * - * @param string $query Query text - * @param array $solrFilters SOLR-specific filters - * @param int $limit Maximum results - * @param array $weights Weights for each search type ['solr' => 0.5, 'vector' => 0.5] - * @param string|null $provider Embedding provider + * Caller supplies keyword search results; vector search runs against the database. + * Results are fused using Reciprocal Rank Fusion (RRF). * - * @return array + * @param string $query Query text + * @param array $keywordResults Pre-fetched keyword search results to fuse with vector results + * @param int $limit Maximum results + * @param array $weights Weights for each search type ['keyword' => 0.5, 'vector' => 0.5] + * @param string|null $provider Embedding provider + * + * @return array Hybrid search results with combined scores and source breakdown * * @throws \Exception If hybrid search fails * - * @spec openspec/changes/retrofit-2026-05-25-bw-svc-mid3/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function hybridSearch( string $query, - array $solrFilters=[], + array $keywordResults=[], int $limit=20, - array $weights=['solr' => 0.5, 'vector' => 0.5], + array $weights=['keyword' => 0.5, 'vector' => 0.5], ?string $provider=null ): array { // Generate query embedding. $queryEmbeddingData = $this->generateEmbedding(text: $query, provider: $provider); $queryEmbedding = $queryEmbeddingData['embedding']; - // Get SOLR results (placeholder - implement when integrating with SOLR). - $solrResults = $solrFilters['solr_results'] ?? []; - - // Delegate to search handler. - $backend = $this->getVectorSearchBackend(); - return $this->searchHandler->hybridSearch( queryEmbedding: $queryEmbedding, - solrResults: $solrResults, + keywordResults: $keywordResults, limit: $limit, - weights: $weights, - backend: $backend + weights: $weights ); }//end hybridSearch() @@ -522,21 +514,18 @@ public function hybridSearch( // ============================================================================= /** - * Get vector statistics + * Get vector statistics from the PostgreSQL database * * @return ((int|mixed)[]|int|string)[] Statistics about stored vectors * * @psalm-return array{total_vectors: int, by_type: array, - * by_model: array, object_vectors?: int, file_vectors?: int, - * source?: 'solr'|'solr_error'|'solr_unavailable'} + * by_model: array, object_vectors: int, file_vectors: int} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 + * @spec openspec/specs/vector-embeddings/spec.md */ public function getVectorStats(): array { - $backend = $this->getVectorSearchBackend(); - - return $this->statsHandler->getStats($backend); + return $this->statsHandler->getStats(); }//end getVectorStats() // ============================================================================= @@ -552,7 +541,7 @@ public function getVectorStats(): array * @SuppressWarnings(PHPMD.NPathComplexity) Multiple database queries and conditions * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Thorough model mismatch detection * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 + * @spec openspec/specs/vector-embeddings/spec.md */ public function checkEmbeddingModelMismatch(): array { @@ -666,7 +655,7 @@ public function checkEmbeddingModelMismatch(): array * * @psalm-return array{success: bool, error?: string, message: string, deleted?: int} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 + * @spec openspec/specs/vector-embeddings/spec.md */ public function clearAllEmbeddings(): array { @@ -720,27 +709,6 @@ public function clearAllEmbeddings(): array // CONFIGURATION HELPERS // ============================================================================= - /** - * Get the configured vector search backend - * - * @return string Vector search backend ('php', 'database', or 'solr') - * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-2 - */ - private function getVectorSearchBackend(): string - { - try { - $llmSettings = $this->settingsService->getLLMSettingsOnly(); - return $llmSettings['vectorConfig']['backend'] ?? 'php'; - } catch (Exception $e) { - $this->logger->warning( - message: '[VectorEmbeddings] Failed to get vector search backend, defaulting to PHP', - context: ['file' => __FILE__, 'line' => __LINE__, 'error' => $e->getMessage()] - ); - return 'php'; - } - }//end getVectorSearchBackend() - /** * Get embedding configuration from settings * @@ -751,7 +719,7 @@ private function getVectorSearchBackend(): string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Provider-specific configuration mapping * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ private function getEmbeddingConfig(?string $provider=null): array { diff --git a/lib/Service/VectorizationService.php b/lib/Service/VectorizationService.php index 1ab35da5fe..16f69e3438 100644 --- a/lib/Service/VectorizationService.php +++ b/lib/Service/VectorizationService.php @@ -93,7 +93,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function registerStrategy(string $entityType, VectorizationStrategyInterface $strategy): void { @@ -126,7 +126,7 @@ public function registerStrategy(string $entityType, VectorizationStrategyInterf * @SuppressWarnings(PHPMD.NPathComplexity) Multiple processing paths with exceptions * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive batch processing with progress tracking * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ public function vectorizeBatch(string $entityType, array $options=[]): array { @@ -268,7 +268,7 @@ public function vectorizeBatch(string $entityType, array $options=[]): array * @SuppressWarnings(PHPMD.NPathComplexity) Multiple embedding and error handling paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive entity vectorization with error handling * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ private function vectorizeEntity($entity, VectorizationStrategyInterface $strategy, array $options): array { @@ -389,7 +389,7 @@ private function vectorizeEntity($entity, VectorizationStrategyInterface $strate * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ private function storeVector($entity, array $item, array $embeddingData, VectorizationStrategyInterface $strategy): void { @@ -417,7 +417,7 @@ private function storeVector($entity, array $item, array $embeddingData, Vectori * * @throws \Exception If strategy not registered * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-3 + * @spec openspec/specs/vector-embeddings/spec.md */ private function getStrategy(string $entityType): VectorizationStrategyInterface { @@ -450,7 +450,7 @@ private function getStrategy(string $entityType): VectorizationStrategyInterface * * @psalm-return array{embedding: array, model: string, dimensions: int<0, max>} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function generateEmbedding(string $text, ?string $provider=null): array { @@ -483,15 +483,16 @@ public function semanticSearch( }//end semanticSearch() /** - * Perform hybrid search combining keyword (SOLR) and semantic (vectors) + * Perform hybrid search combining keyword and semantic (vector) results * - * Delegates to VectorEmbeddings. + * Delegates to VectorEmbeddings. Caller supplies keyword search results; + * vector search runs against the PostgreSQL database. * - * @param string $query Query text - * @param array $solrFilters SOLR-specific filters - * @param int $limit Maximum results - * @param array $weights Weights for each search type ['solr' => 0.5, 'vector' => 0.5] - * @param string|null $provider Embedding provider + * @param string $query Query text + * @param array $keywordResults Pre-fetched keyword search results to fuse + * @param int $limit Maximum results + * @param array $weights Weights for each search type ['keyword' => 0.5, 'vector' => 0.5] + * @param string|null $provider Embedding provider * * @return array Hybrid search results with combined scores and source breakdown. * @@ -501,14 +502,14 @@ public function semanticSearch( */ public function hybridSearch( string $query, - array $solrFilters=[], + array $keywordResults=[], int $limit=20, - array $weights=['solr' => 0.5, 'vector' => 0.5], + array $weights=['keyword' => 0.5, 'vector' => 0.5], ?string $provider=null ): array { return $this->vectorService->hybridSearch( query: $query, - solrFilters: $solrFilters, + keywordResults: $keywordResults, limit: $limit, weights: $weights, provider: $provider @@ -522,7 +523,7 @@ public function hybridSearch( * * @return array Vector statistics with totals and breakdowns by type and model. * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 + * @spec openspec/specs/vector-embeddings/spec.md */ public function getVectorStats(): array { @@ -545,7 +546,7 @@ public function getVectorStats(): array * vectorLength: int<0, max>, sampleValues: array, * testText: string}} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-1 + * @spec openspec/specs/vector-embeddings/spec.md */ public function testEmbedding(string $provider, array $config, string $testText='Test.'): array { @@ -564,7 +565,7 @@ public function testEmbedding(string $provider, array $config, string $testText= * existing_models?: list{0?: mixed,...}, total_vectors?: int, * null_model_count?: int, mismatched_models?: list} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 + * @spec openspec/specs/vector-embeddings/spec.md */ public function checkEmbeddingModelMismatch(): array { @@ -580,7 +581,7 @@ public function checkEmbeddingModelMismatch(): array * * @psalm-return array{success: bool, error?: string, message: string, deleted?: int} * - * @spec openspec/changes/retrofit-2026-05-24-newcap-vector-embeddings/tasks.md#task-5 + * @spec openspec/specs/vector-embeddings/spec.md */ public function clearAllEmbeddings(): array { diff --git a/lib/Service/WebPush/HexIconService.php b/lib/Service/WebPush/HexIconService.php new file mode 100644 index 0000000000..eb97c620a7 --- /dev/null +++ b/lib/Service/WebPush/HexIconService.php @@ -0,0 +1,322 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\WebPush; + +use OCP\App\IAppManager; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFolder; +use Psr\Log\LoggerInterface; + +/** + * Builds + caches the cobalt-hex notification icon/badge per originApp. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ +class HexIconService +{ + + /** + * Conduction cobalt hex fill colour. + * + * @var string + */ + private const COBALT = '#21468B'; + + /** + * Appdata folder name for cached icons. + * + * @var string + */ + private const CACHE_FOLDER = 'webpush-icons'; + + /** + * Rendered icon edge length in pixels (square). + * + * @var int + */ + private const ICON_SIZE = 192; + + /** + * Constructor. + * + * @param IAppManager $appManager App manager for resolving app.svg paths. + * @param IAppData $appData App data for the rendered-PNG cache. + * @param LoggerInterface $logger Logger for compositing diagnostics. + */ + public function __construct( + private readonly IAppManager $appManager, + private readonly IAppData $appData, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Return the icon PNG bytes (cobalt hex + white glyph) for an app. + * + * Cached: the second request for the same app is served from appdata. + * + * @param string $appId The originApp id. + * + * @return array{body: string, mime: string} The icon body + MIME type. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function getIcon(string $appId): array + { + return $this->getOrRender(appId: $appId, badge: false); + }//end getIcon() + + /** + * Return the monochrome badge PNG bytes for an app. + * + * @param string $appId The originApp id. + * + * @return array{body: string, mime: string} The badge body + MIME type. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function getBadge(string $appId): array + { + return $this->getOrRender(appId: $appId, badge: true); + }//end getBadge() + + /** + * Resolve from cache or render + cache. + * + * @param string $appId The originApp id. + * @param bool $badge Whether to render the monochrome badge variant. + * + * @return array{body: string, mime: string} + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + private function getOrRender(string $appId, bool $badge): array + { + $safeApp = preg_replace('/[^a-z0-9_-]/', '', strtolower($appId)) ?? 'openregister'; + $suffix = '-icon.png'; + if ($badge === true) { + $suffix = '-badge.png'; + } + + $cacheKey = $safeApp.$suffix; + + $folder = $this->cacheFolder(); + if ($folder !== null && $folder->fileExists($cacheKey) === true) { + try { + return ['body' => $folder->getFile($cacheKey)->getContent(), 'mime' => 'image/png']; + } catch (\Throwable $e) { + $this->logger->debug('[HexIconService] cache read failed: '.$e->getMessage()); + } + } + + // Imagick unavailable → degrade to the raw app.svg (SVG MIME). + if (extension_loaded('imagick') === false) { + return $this->rawAppSvg(appId: $safeApp); + } + + $png = $this->composite(appId: $safeApp, badge: $badge); + if ($png === null) { + return $this->rawAppSvg(appId: $safeApp); + } + + if ($folder !== null) { + try { + $folder->newFile($cacheKey, $png); + } catch (\Throwable $e) { + $this->logger->debug('[HexIconService] cache write failed: '.$e->getMessage()); + } + } + + return ['body' => $png, 'mime' => 'image/png']; + }//end getOrRender() + + /** + * Composite the white app glyph onto the cobalt hexagon via Imagick. + * + * @param string $appId The originApp id. + * @param bool $badge Whether to render the monochrome badge (transparent bg). + * + * @return string|null PNG bytes, or null when compositing failed. + * + * @SuppressWarnings(PHPMD.StaticAccess) + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + private function composite(string $appId, bool $badge): ?string + { + $svgPath = $this->appSvgPath(appId: $appId); + if ($svgPath === null) { + return null; + } + + try { + $size = self::ICON_SIZE; + + // Base canvas: the cobalt hexagon for the icon, transparent for the badge. + $canvas = new \Imagick(); + $canvas->newImage($size, $size, new \ImagickPixel('transparent'), 'png'); + + if ($badge === false) { + $hex = new \Imagick(); + $hex->setBackgroundColor(new \ImagickPixel('transparent')); + // The brand hex is a regular pointy-top hexagon with a 100:115 + // (width:height) ratio. Rendering it into a square canvas would + // stretch it ~15% horizontally and distort the shape, so fit it + // to the canvas height and centre it horizontally instead. + $hexHeight = $size; + $hexWidth = (int) round($size * (100 / 115)); + $hexSvg = sprintf( + '' + .'', + $hexWidth, + $hexHeight, + self::COBALT + ); + $hex->readImageBlob($hexSvg); + $hex->setImageFormat('png'); + $hexX = (int) (($size - $hexWidth) / 2); + $canvas->compositeImage($hex, \Imagick::COMPOSITE_OVER, $hexX, 0); + $hex->clear(); + }//end if + + // The app glyph, tinted white, scaled to ~60% of the canvas and centred. + $glyph = new \Imagick(); + $glyph->setBackgroundColor(new \ImagickPixel('transparent')); + $glyph->readImage($svgPath); + $glyph->setImageFormat('png'); + $glyphSize = (int) ($size * 0.6); + $glyph->resizeImage($glyphSize, $glyphSize, \Imagick::FILTER_LANCZOS, 1, true); + + // Force the glyph to white (monochrome) so it reads on cobalt / as a badge. + $glyph->setImageAlphaChannel(\Imagick::ALPHACHANNEL_EXTRACT); + $white = new \Imagick(); + $white->newImage($glyph->getImageWidth(), $glyph->getImageHeight(), new \ImagickPixel('white'), 'png'); + $white->compositeImage($glyph, \Imagick::COMPOSITE_COPYOPACITY, 0, 0); + + $offset = (int) (($size - $white->getImageWidth()) / 2); + $canvas->compositeImage($white, \Imagick::COMPOSITE_OVER, $offset, $offset); + + $canvas->setImageFormat('png'); + $blob = $canvas->getImageBlob(); + + $glyph->clear(); + $white->clear(); + $canvas->clear(); + + return $blob; + } catch (\Throwable $e) { + $this->logger->warning('[HexIconService] Imagick compositing failed: '.$e->getMessage()); + return null; + }//end try + }//end composite() + + /** + * Resolve the absolute filesystem path to an app's img/app.svg. + * + * @param string $appId The app id. + * + * @return string|null The path, or null when the app / file is absent. + */ + private function appSvgPath(string $appId): ?string + { + try { + $path = $this->appManager->getAppPath($appId).'/img/app.svg'; + } catch (\Throwable $e) { + $path = ''; + } + + if ($path !== '' && is_file($path) === true) { + return $path; + } + + // Fall back to openregister's own glyph. + try { + $fallback = $this->appManager->getAppPath('openregister').'/img/app.svg'; + if (is_file($fallback) === true) { + return $fallback; + } + } catch (\Throwable $e) { + $this->logger->debug('[HexIconService] openregister app.svg fallback missing: '.$e->getMessage()); + } + + return null; + }//end appSvgPath() + + /** + * Degrade to the raw app.svg bytes when Imagick is unavailable. + * + * @param string $appId The app id. + * + * @return array{body: string, mime: string} + */ + private function rawAppSvg(string $appId): array + { + $path = $this->appSvgPath(appId: $appId); + if ($path !== null) { + $bytes = file_get_contents($path); + if ($bytes !== false) { + return ['body' => $bytes, 'mime' => 'image/svg+xml']; + } + } + + // Minimal valid empty PNG-less fallback: a tiny transparent SVG. + return [ + 'body' => '', + 'mime' => 'image/svg+xml', + ]; + }//end rawAppSvg() + + /** + * Get (creating if needed) the appdata cache folder, or null on failure. + * + * @return ISimpleFolder|null The cache folder. + */ + private function cacheFolder(): ?ISimpleFolder + { + try { + try { + return $this->appData->getFolder(self::CACHE_FOLDER); + } catch (NotFoundException $e) { + return $this->appData->newFolder(self::CACHE_FOLDER); + } + } catch (\Throwable $e) { + $this->logger->debug('[HexIconService] appdata cache folder unavailable: '.$e->getMessage()); + return null; + } + }//end cacheFolder() +}//end class diff --git a/lib/Service/WebPush/WebPushService.php b/lib/Service/WebPush/WebPushService.php new file mode 100644 index 0000000000..771b47908b --- /dev/null +++ b/lib/Service/WebPush/WebPushService.php @@ -0,0 +1,221 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\WebPush; + +use Minishlink\WebPush\Subscription; +use Minishlink\WebPush\WebPush; +use OCA\OpenRegister\Db\PushSubscriptionMapper; +use OCP\IAppConfig; +use OCP\IURLGenerator; +use Psr\Log\LoggerInterface; + +/** + * Encrypts, signs, and delivers Web Push payloads; prunes gone subscriptions. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ +class WebPushService +{ + + /** + * App id used for IAppConfig reads. + * + * @var string + */ + private const APP_ID = 'openregister'; + + /** + * App-config key holding the VAPID public key. + * + * @var string + */ + public const VAPID_PUBLIC_KEY = 'vapid_public_key'; + + /** + * App-config key holding the VAPID private key. + * + * @var string + */ + public const VAPID_PRIVATE_KEY = 'vapid_private_key'; + + /** + * Constructor. + * + * @param PushSubscriptionMapper $subscriptionMapper Subscription store (lookup + prune). + * @param IAppConfig $appConfig App-config reader for the VAPID keypair. + * @param IURLGenerator $urlGenerator URL generator for the VAPID JWT subject + icon. + * @param LoggerInterface $logger Logger for delivery diagnostics. + */ + public function __construct( + private readonly PushSubscriptionMapper $subscriptionMapper, + private readonly IAppConfig $appConfig, + private readonly IURLGenerator $urlGenerator, + private readonly LoggerInterface $logger + ) { + }//end __construct() + + /** + * Whether a VAPID keypair is configured. + * + * @return bool True when both public and private keys are present. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function isConfigured(): bool + { + $public = $this->appConfig->getValueString(self::APP_ID, self::VAPID_PUBLIC_KEY, ''); + $private = $this->appConfig->getValueString(self::APP_ID, self::VAPID_PRIVATE_KEY, ''); + + return ($public !== '' && $private !== ''); + }//end isConfigured() + + /** + * Return the configured VAPID public key (or empty string when absent). + * + * Never returns the private key. + * + * @return string The base64url VAPID public key, or empty string. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function getPublicKey(): string + { + return $this->appConfig->getValueString(self::APP_ID, self::VAPID_PUBLIC_KEY, ''); + }//end getPublicKey() + + /** + * Deliver a payload to every active subscription of a recipient. + * + * Builds one aes128gcm-encrypted, VAPID-signed message per subscription and + * POSTs it. On a 404 / 410 Gone response the dead subscription is pruned. + * + * @param string $uid The recipient Nextcloud uid. + * @param array $payload The notification payload (title, body, icon, tag, url, actions). + * + * @return int The number of endpoints successfully delivered to. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function deliver(string $uid, array $payload): int + { + if ($this->isConfigured() === false) { + $this->logger->warning( + '[WebPushService] No VAPID keypair configured — skipping web-push delivery. ' + .'Run `occ openregister:web-push:generate-vapid`.' + ); + return 0; + } + + $subscriptions = $this->subscriptionMapper->findByUser($uid); + if (count($subscriptions) === 0) { + return 0; + } + + $webPush = $this->buildWebPush(); + $json = json_encode($payload); + if ($json === false) { + $this->logger->warning('[WebPushService] Failed to JSON-encode push payload.'); + return 0; + } + + foreach ($subscriptions as $subscription) { + $sub = Subscription::create( + [ + 'endpoint' => (string) $subscription->getEndpoint(), + 'keys' => [ + 'p256dh' => (string) $subscription->getP256dh(), + 'auth' => (string) $subscription->getAuth(), + ], + ] + ); + $webPush->queueNotification(subscription: $sub, payload: $json); + } + + $delivered = 0; + foreach ($webPush->flush() as $report) { + if ($report->isSuccess() === true) { + $delivered++; + continue; + } + + // Prune endpoints the push service reports gone (404 / 410). + $statusCode = 0; + $response = $report->getResponse(); + if ($response !== null) { + $statusCode = $response->getStatusCode(); + } + + if ($report->isSubscriptionExpired() === true || $statusCode === 404 || $statusCode === 410) { + $pruned = $this->subscriptionMapper->deleteByEndpoint($report->getEndpoint()); + $this->logger->info( + sprintf( + '[WebPushService] Pruned %d gone subscription(s) for endpoint (status %d).', + $pruned, + $statusCode + ) + ); + continue; + } + + $this->logger->warning( + sprintf('[WebPushService] Push delivery failed (status %d): %s', $statusCode, $report->getReason()) + ); + }//end foreach + + return $delivered; + }//end deliver() + + /** + * Build a configured WebPush client from the stored VAPID keypair. + * + * @return WebPush The configured client. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + private function buildWebPush(): WebPush + { + $auth = [ + 'VAPID' => [ + 'subject' => $this->urlGenerator->getBaseUrl(), + 'publicKey' => $this->appConfig->getValueString(self::APP_ID, self::VAPID_PUBLIC_KEY, ''), + 'privateKey' => $this->appConfig->getValueString(self::APP_ID, self::VAPID_PRIVATE_KEY, ''), + ], + ]; + + return new WebPush(auth: $auth); + }//end buildWebPush() +}//end class diff --git a/lib/Service/Webhook/CloudEventFormatter.php b/lib/Service/Webhook/CloudEventFormatter.php index 0ad38cb0a9..91aa8c875a 100644 --- a/lib/Service/Webhook/CloudEventFormatter.php +++ b/lib/Service/Webhook/CloudEventFormatter.php @@ -19,7 +19,7 @@ * * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-84 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ declare(strict_types=1); @@ -73,7 +73,7 @@ class CloudEventFormatter * subject: null|string, dataschema: null, data: array, * openregister: array{app: 'openregister', version: '1.0.0'}} * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-84 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ public function formatAsCloudEvent( string $eventType, @@ -131,7 +131,7 @@ public function formatAsCloudEvent( * queryParams: array|mixed, headers: array|mixed, body: array|mixed,...}, * openregister: array{app: 'openregister', version: '1.0.0'}} * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-84 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ public function formatRequestAsCloudEvent( IRequest $request, @@ -285,7 +285,7 @@ private function getRequestHeaders(IRequest $request): array * * @return string Content type header value * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-26 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ private function getContentTypeHeader(IRequest $request): string { diff --git a/lib/Service/Webhook/WebhookInterceptionCache.php b/lib/Service/Webhook/WebhookInterceptionCache.php new file mode 100644 index 0000000000..93a9412806 --- /dev/null +++ b/lib/Service/Webhook/WebhookInterceptionCache.php @@ -0,0 +1,182 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service\Webhook; + +use OCP\ICache; +use OCP\ICacheFactory; + +/** + * WebhookInterceptionCache caches the "has interception webhooks" flag + * + * Object create/update requests must ask "is any webhook configured to + * intercept this event?" before saving. Without a cache that question costs + * a webhook table scan on EVERY write, even on installs with zero webhooks. + * This class stores a per-event-type boolean in the distributed cache so + * the zero-webhook common case costs a cache read instead of a DB query. + * + * IMPORTANT — the flag is deliberately tenant-agnostic: it answers whether + * ANY enabled interception webhook exists for the event type across ALL + * organisations. A per-organisation "false" here would let one tenant's + * cache entry silently disable another tenant's interception hooks. When + * the global flag is true, the caller still runs the organisation-filtered + * lookup to select the webhooks that actually apply. + * + * Invalidation: every webhook insert/update/delete clears the flags (see + * WebhookMapper). A short TTL bounds staleness across nodes that miss an + * invalidation (e.g. cache backends without distributed clear). + * + * IMPORTANT — the cache is only armed when a genuinely DISTRIBUTED memory + * cache is configured (ICacheFactory::isAvailable()). Without one, + * createDistributed() silently falls back to a node-LOCAL cache (APCu): + * on a multi-node deployment a cached "false" on node B would then survive + * a webhook created on node A for up to the TTL, and node B's writes would + * bypass a real interception webhook. In that configuration get() always + * reports a miss and set()/invalidate() are no-ops, so callers compute the + * flag from the database on every write — the pre-cache behaviour, slower + * but always correct. + * + * @category Service + * @package OCA\OpenRegister\Service\Webhook + * + * @author Conduction Development Team + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://www.OpenRegister.app + */ +class WebhookInterceptionCache +{ + /** + * Cache TTL for the has-interception-webhooks flag, in seconds. + * + * Bounds staleness when an invalidation is missed; webhook CRUD clears + * the flags eagerly so this is a safety net, not the primary mechanism. + * Kept short (60s) so that even in a pathological case where a node + * misses the distributed invalidation, a newly created interception + * webhook is honoured within a minute. + * + * @var integer + */ + private const CACHE_TTL_SECONDS = 60; + + /** + * Cache key prefix for per-event-type flags. + * + * @var string + */ + private const KEY_PREFIX = 'has-interception-webhooks:'; + + /** + * Distributed cache instance scoped to webhook interception flags. + * + * Null when no genuinely distributed memory cache is configured — the + * cache is then disabled entirely (see the class docblock) rather than + * silently degrading to a node-local backend. + * + * @var ICache|null + */ + private ?ICache $cache = null; + + /** + * Constructor + * + * Arms the cache only when ICacheFactory reports a distributed memory + * cache backend; otherwise createDistributed() would hand back a + * node-LOCAL cache whose stale "false" flags could bypass interception + * webhooks on multi-node deployments. + * + * @param ICacheFactory $cacheFactory Cache factory used to create the distributed cache. + * + * @return void + */ + public function __construct(ICacheFactory $cacheFactory) + { + if ($cacheFactory->isAvailable() === true) { + $this->cache = $cacheFactory->createDistributed('openregister-webhook-interception'); + } + }//end __construct() + + /** + * Get the cached "has interception webhooks" flag for an event type. + * + * @param string $eventType Event type (e.g. 'object.creating'). + * + * @return bool|null True/false when cached, null on cache miss. + * + * @spec openspec/specs/webhook-payload-mapping/spec.md#request-interception-pre-event-webhooks + */ + public function get(string $eventType): ?bool + { + // Cache disabled (no distributed backend) → always a miss, so the + // caller falls back to computing the flag from the database. + if ($this->cache === null) { + return null; + } + + $value = $this->cache->get(self::KEY_PREFIX.$eventType); + if ($value === null) { + return null; + } + + return (bool) $value; + }//end get() + + /** + * Store the "has interception webhooks" flag for an event type. + * + * @param string $eventType Event type (e.g. 'object.creating'). + * @param bool $hasWebhooks Whether any enabled interception webhook exists for the event type. + * + * @return void + * + * @spec openspec/specs/webhook-payload-mapping/spec.md#request-interception-pre-event-webhooks + */ + public function set(string $eventType, bool $hasWebhooks): void + { + // ICache values must be scalars; store as int for backend portability. + // No-op when the cache is disabled (no distributed backend). + $this->cache?->set(self::KEY_PREFIX.$eventType, (int) $hasWebhooks, self::CACHE_TTL_SECONDS); + }//end set() + + /** + * Invalidate all cached flags. + * + * Called on every webhook insert/update/delete — any CRUD can change + * which event types have interception webhooks, so all flags are cleared. + * + * @return void + * + * @spec openspec/specs/webhook-payload-mapping/spec.md#request-interception-pre-event-webhooks + */ + public function invalidate(): void + { + // No-op when the cache is disabled (no distributed backend). + $this->cache?->clear(self::KEY_PREFIX); + }//end invalidate() +}//end class diff --git a/lib/Service/WebhookService.php b/lib/Service/WebhookService.php index 70f8c8c625..4d6de0ac64 100644 --- a/lib/Service/WebhookService.php +++ b/lib/Service/WebhookService.php @@ -17,11 +17,11 @@ * @version GIT: * @link https://www.OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-76 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-78 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-80 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-86 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-webhook-entity-must-support-an-optional-mapping-reference-for-payload-transformation + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-webhook-testing-must-support-dry-run-delivery + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-request-interception-must-support-pre-event-webhooks + * @spec openspec/specs/webhook-payload-mapping/spec.md + * @spec openspec/specs/webhook-payload-mapping/spec.md */ declare(strict_types=1); @@ -38,13 +38,16 @@ use OCA\OpenRegister\Db\WebhookLogMapper; use OCA\OpenRegister\Db\WebhookMapper; use OCA\OpenRegister\Service\Webhook\CloudEventFormatter; +use OCA\OpenRegister\Service\Webhook\WebhookInterceptionCache; use OCP\AppFramework\Db\DoesNotExistException; +use OCA\OpenRegister\BackgroundJob\WebhookDeliveryJob; use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\Event; use OCP\IRequest; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; +use RuntimeException; /** * WebhookService handles webhook delivery and request interception @@ -95,6 +98,13 @@ class WebhookService */ private ?CloudEventFormatter $cloudEventFormatter; + /** + * Job list used to enqueue asynchronous webhook delivery. + * + * @var IJobList + */ + private IJobList $jobList; + /** * Mapping service for payload transformation * @@ -109,15 +119,28 @@ class WebhookService */ private MappingMapper $mappingMapper; + /** + * Cache of the "has interception webhooks" flag per event type. + * + * Nullable so the service stays constructible without a cache backend + * (unit tests, degraded environments); the fast path then falls back to + * the direct lookup. + * + * @var WebhookInterceptionCache|null + */ + private ?WebhookInterceptionCache $interceptionCache; + /** * Constructor * - * @param WebhookMapper $webhookMapper Webhook mapper - * @param LoggerInterface $logger Logger - * @param WebhookLogMapper $webhookLogMapper Webhook log mapper - * @param MappingService $mappingService Mapping service - * @param MappingMapper $mappingMapper Mapping mapper - * @param CloudEventFormatter|null $cloudEventFormatter CloudEvent formatter (optional) + * @param WebhookMapper $webhookMapper Webhook mapper + * @param LoggerInterface $logger Logger + * @param WebhookLogMapper $webhookLogMapper Webhook log mapper + * @param MappingService $mappingService Mapping service + * @param MappingMapper $mappingMapper Mapping mapper + * @param IJobList $jobList Background job list for async delivery + * @param CloudEventFormatter|null $cloudEventFormatter CloudEvent formatter (optional) + * @param WebhookInterceptionCache|null $interceptionCache Interception-flag cache (optional) * * @return void */ @@ -127,14 +150,18 @@ public function __construct( WebhookLogMapper $webhookLogMapper, MappingService $mappingService, MappingMapper $mappingMapper, - ?CloudEventFormatter $cloudEventFormatter=null + IJobList $jobList, + ?CloudEventFormatter $cloudEventFormatter=null, + ?WebhookInterceptionCache $interceptionCache=null ) { $this->webhookMapper = $webhookMapper; $this->logger = $logger; $this->webhookLogMapper = $webhookLogMapper; $this->mappingService = $mappingService; $this->mappingMapper = $mappingMapper; + $this->jobList = $jobList; $this->cloudEventFormatter = $cloudEventFormatter; + $this->interceptionCache = $interceptionCache; $this->initializeHttpClient(); }//end __construct() @@ -162,6 +189,20 @@ public function __construct( */ private const LOG_BODY_PREVIEW_BYTES = 1024; + /** + * Connect + total timeout for request-interception deliveries, in seconds. + * + * Interception is request-blocking BY DESIGN: the object write waits for + * the hook's answer. A hook that cannot respond within 2 seconds must not + * gate writes — with the client default of 30s total / 10s connect, one + * slow endpoint stalled every object create for up to 30 seconds. This cap + * applies ONLY to the synchronous interception path; post-save deliveries + * run async via WebhookDeliveryJob and keep the per-webhook timeout. + * + * @var integer + */ + private const INTERCEPTION_TIMEOUT_SECONDS = 2; + /** * Initialize HTTP client with default configuration * @@ -172,7 +213,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-22 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ private function initializeHttpClient(): void { @@ -232,29 +273,49 @@ private function initializeHttpClient(): void * - Unresolvable hosts are allowed through so that DNS failures surface * as the normal Guzzle ConnectException, not a 400 here. * - * @param string $uri Full request URI to validate. + * When $allowPrivate is true (an admin-set, per-hook opt-in stored as + * `configuration.allowPrivateTargets`), the IP-range checks are skipped so a + * developer can deliver to a local target such as http://localhost:8000. The + * scheme (http/https only) and parse/host checks are ALWAYS enforced — only + * the loopback/RFC-1918/link-local/IPv6 range checks are bypassed. + * + * @param string $uri Full request URI to validate. + * @param boolean $allowPrivate When true, skip the private/loopback IP-range checks. * * @return void * * @throws \RuntimeException When the URI is blocked. + * + * @spec openspec/specs/notificatie-engine/spec.md + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) The flag is the per-hook + * allowPrivateTargets opt-in; a boolean toggle is the clearest API for a + * binary "bypass the IP-range checks" decision threaded from the caller. */ - private function assertSafeWebhookUri(string $uri): void + private function assertSafeWebhookUri(string $uri, bool $allowPrivate=false): void { $parsed = parse_url($uri); if ($parsed === false) { - throw new \RuntimeException('Webhook target URL is not parseable'); + throw new RuntimeException('Webhook target URL is not parseable'); } $scheme = strtolower($parsed['scheme'] ?? ''); if (in_array($scheme, ['http', 'https'], true) === false) { - throw new \RuntimeException( + throw new RuntimeException( 'Webhook target URL must use http or https scheme; got "'.$scheme.'"' ); } $host = strtolower($parsed['host'] ?? ''); if ($host === '') { - throw new \RuntimeException('Webhook target URL is missing a host'); + throw new RuntimeException('Webhook target URL is missing a host'); + } + + // Per-hook opt-in: the admin deliberately allowed private/loopback + // targets for this webhook. Scheme + host/parse checks above stay + // enforced; only the IP-range checks below are bypassed. + if ($allowPrivate === true) { + return; } // ── IPv6 literal detection ────────────────────────────────────── @@ -263,14 +324,14 @@ private function assertSafeWebhookUri(string $uri): void // before validation. A colon anywhere in $host (brackets or not) is // the reliable heuristic that we have an IPv6 address rather than a // plain hostname. - $bareHost = trim($host, '[]'); + $bareHost = trim($host, '[]'); $isIpv6Literal = filter_var($bareHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false || strpos($host, ':') !== false; if ($isIpv6Literal === true) { - $reason = $this->blockedIpv6Reason($bareHost); + $reason = $this->blockedIpv6Reason(ip: $bareHost); if ($reason !== null) { - throw new \RuntimeException( + throw new RuntimeException( 'Webhook target URL resolves to a blocked IP range ('.$reason.')' ); } @@ -290,9 +351,9 @@ private function assertSafeWebhookUri(string $uri): void continue; } - $reason = $this->blockedIpv6Reason($ipv6); + $reason = $this->blockedIpv6Reason(ip: $ipv6); if ($reason !== null) { - throw new \RuntimeException( + throw new RuntimeException( 'Webhook target URL resolves to a blocked IP range ('.$reason.')' ); } @@ -322,35 +383,35 @@ private function assertSafeWebhookUri(string $uri): void // Loopback 127.0.0.0/8. if (($longIp & 0xFF000000) === 0x7F000000) { - throw new \RuntimeException( + throw new RuntimeException( 'Webhook target URL resolves to a blocked IP range (loopback)' ); } // RFC-1918 10.0.0.0/8. if (($longIp & 0xFF000000) === 0x0A000000) { - throw new \RuntimeException( + throw new RuntimeException( 'Webhook target URL resolves to a blocked IP range (RFC-1918)' ); } // RFC-1918 172.16.0.0/12. if (($longIp & 0xFFF00000) === 0xAC100000) { - throw new \RuntimeException( + throw new RuntimeException( 'Webhook target URL resolves to a blocked IP range (RFC-1918)' ); } // RFC-1918 192.168.0.0/16. if (($longIp & 0xFFFF0000) === 0xC0A80000) { - throw new \RuntimeException( + throw new RuntimeException( 'Webhook target URL resolves to a blocked IP range (RFC-1918)' ); } // Link-local 169.254.0.0/16 — includes cloud metadata 169.254.169.254. if (($longIp & 0xFFFF0000) === 0xA9FE0000) { - throw new \RuntimeException( + throw new RuntimeException( 'Webhook target URL resolves to a blocked IP range (link-local/metadata)' ); } @@ -396,13 +457,13 @@ private function blockedIpv6Reason(string $ip): ?string return 'unspecified'; } - // fc00::/7 — unique local (covers fc00:: and fd00:: blocks). + // Fc00::/7 — unique local (covers fc00:: and fd00:: blocks). // First byte with mask 0xFE must equal 0xFC. if ((ord($bin[0]) & 0xFE) === 0xFC) { return 'unique-local'; } - // fe80::/10 — link-local unicast. + // Fe80::/10 — link-local unicast. // First byte 0xFE, second byte upper 6 bits = 0x80 → mask 0xC0. if (ord($bin[0]) === 0xFE && (ord($bin[1]) & 0xC0) === 0x80) { return 'link-local'; @@ -472,12 +533,12 @@ private function isPrivateHost(string $host): bool // ── IPv6 literal ──────────────────────────────────────────────── // Strip brackets that parse_url() may leave around IPv6 literals. - $bareHost = trim($host, '[]'); + $bareHost = trim($host, '[]'); $isIpv6Literal = filter_var($bareHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false || strpos($host, ':') !== false; if ($isIpv6Literal === true) { - return $this->blockedIpv6Reason($bareHost) !== null; + return $this->blockedIpv6Reason(ip: $bareHost) !== null; } // ── IPv6 DNS (AAAA) ───────────────────────────────────────────── @@ -485,7 +546,7 @@ private function isPrivateHost(string $host): bool if (is_array($aaaaRecords) === true) { foreach ($aaaaRecords as $record) { $ipv6 = $record['ipv6'] ?? ''; - if ($ipv6 !== '' && $this->blockedIpv6Reason($ipv6) !== null) { + if ($ipv6 !== '' && $this->blockedIpv6Reason(ip: $ipv6) !== null) { return true; } } @@ -574,8 +635,8 @@ private function capResponseBody(string $body): string * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple webhook dispatch conditions * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-80 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-86 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-request-interception-must-support-pre-event-webhooks + * @spec openspec/specs/webhook-payload-mapping/spec.md */ public function dispatchEvent(Event $_event, string $eventName, array $payload): void { @@ -618,18 +679,35 @@ public function dispatchEvent(Event $_event, string $eventName, array $payload): ] ); + // Enqueue delivery (including the FIRST attempt) as a background job so + // the object-write response never blocks on an external endpoint — with + // N webhooks each up to a 30s timeout, synchronous delivery made write + // latency a function of third-party uptime (async-webhook-delivery / + // ADR-009 Rule 5). WebhookDeliveryJob performs the same delivery + logging. foreach ($webhooks as $webhook) { - $this->deliverWebhook(webhook: $webhook, eventName: $eventName, payload: $payload); + $this->jobList->add( + WebhookDeliveryJob::class, + [ + 'webhook_id' => $webhook->getId(), + 'event_name' => $eventName, + 'payload' => $payload, + 'attempt' => 1, + ] + ); } }//end dispatchEvent() /** * Deliver webhook to target URL * - * @param Webhook $webhook Webhook configuration - * @param string $eventName Event name - * @param array $payload Payload data - * @param int $attempt Current attempt number (for retries) + * @param Webhook $webhook Webhook configuration + * @param string $eventName Event name + * @param array $payload Payload data + * @param int $attempt Current attempt number (for retries) + * @param int|null $timeoutCapSeconds Optional hard cap on connect + total timeout; + * used by the synchronous interception path + * (INTERCEPTION_TIMEOUT_SECONDS). Null keeps the + * per-webhook timeout (async deliveries). * * @return bool Success status * @@ -638,10 +716,15 @@ public function dispatchEvent(Event $_event, string $eventName, array $payload): * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive webhook delivery with logging * Fallback for connection errors without response * - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ - public function deliverWebhook(Webhook $webhook, string $eventName, array $payload, int $attempt=1): bool - { + public function deliverWebhook( + Webhook $webhook, + string $eventName, + array $payload, + int $attempt=1, + ?int $timeoutCapSeconds=null + ): bool { if ($webhook->getEnabled() === false) { $this->logger->debug( message: '[WebhookService] Webhook is disabled, skipping delivery', @@ -686,7 +769,51 @@ public function deliverWebhook(Webhook $webhook, string $eventName, array $paylo $webhookLog->setAttempt($attempt); try { - $response = $this->sendRequest(webhook: $webhook, payload: $webhookPayload); + $response = $this->sendRequest( + webhook: $webhook, + payload: $webhookPayload, + timeoutCapSeconds: $timeoutCapSeconds + ); + + // BUG-SVC-1: http_errors is disabled on the Guzzle client, so a 4xx/5xx + // response does NOT throw — it lands here. Treat any non-2xx status as a + // delivery failure (log it, record failed statistics, schedule a retry) + // instead of silently recording it as a successful delivery. + $statusCode = (int) $response['status_code']; + if ($statusCode < 200 || $statusCode >= 300) { + $webhookLog->setSuccess(false); + $webhookLog->setStatusCode($statusCode); + $webhookLog->setResponseBody($this->capResponseBody(body: (string) $response['body'])); + $webhookLog->setErrorMessage('Webhook endpoint returned non-2xx status: '.$statusCode); + $webhookLog->setRequestBody(json_encode($webhookPayload)); + + $this->logger->error( + message: '[WebhookService] Webhook delivery failed with non-2xx status', + context: [ + 'file' => __FILE__, + 'line' => __LINE__, + 'webhook_id' => $webhook->getId(), + 'webhook_name' => $webhook->getName(), + 'event' => $eventName, + 'status_code' => $statusCode, + 'attempt' => $attempt, + 'max_retries' => $webhook->getMaxRetries(), + ] + ); + + $this->webhookMapper->updateStatistics(webhook: $webhook, success: false); + + // Schedule retry if within retry limit. + if ($attempt < $webhook->getMaxRetries()) { + $nextRetryAt = $this->calculateNextRetryTime(webhook: $webhook, attempt: $attempt); + $webhookLog->setNextRetryAt($nextRetryAt); + $this->scheduleRetry(webhook: $webhook, eventName: $eventName, _payload: $payload, attempt: $attempt + 1); + } + + $this->webhookLogMapper->insert($webhookLog); + + return false; + }//end if // Log success. $webhookLog->setSuccess(true); @@ -862,7 +989,7 @@ private function passesFilters(Webhook $webhook, array $payload): bool * * @return mixed * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-23 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ private function getNestedValue(array $array, string $key) { @@ -896,7 +1023,7 @@ private function getNestedValue(array $array, string $key) * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Three payload format strategies * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-76 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-webhook-entity-must-support-an-optional-mapping-reference-for-payload-transformation */ private function buildPayload(Webhook $webhook, string $eventName, array $payload, int $attempt): array { @@ -969,7 +1096,7 @@ private function buildPayload(Webhook $webhook, string $eventName, array $payloa * * @return array|null Transformed payload, or null on failure * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-76 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-webhook-entity-must-support-an-optional-mapping-reference-for-payload-transformation */ private function applyMappingTransformation( int $mappingId, @@ -1038,7 +1165,7 @@ private function applyMappingTransformation( * * @return string Short class name (e.g., "ObjectCreatedEvent") * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-24 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ private function getShortEventName(string $eventName): string { @@ -1049,8 +1176,10 @@ private function getShortEventName(string $eventName): string /** * Send HTTP request to webhook URL * - * @param Webhook $webhook Webhook configuration - * @param array $payload Payload to send + * @param Webhook $webhook Webhook configuration + * @param array $payload Payload to send + * @param int|null $timeoutCapSeconds Optional hard cap on connect + total timeout + * (request-blocking interception deliveries) * * @return (int|string)[] Response data * @@ -1059,13 +1188,20 @@ private function getShortEventName(string $eventName): string * @psalm-return array{status_code: int, body: string} * * Different handling for GET vs POST/PUT/PATCH/DELETE methods + * + * @spec openspec/specs/notificatie-engine/spec.md */ - private function sendRequest(Webhook $webhook, array $payload): array + private function sendRequest(Webhook $webhook, array $payload, ?int $timeoutCapSeconds=null): array { + // Per-hook opt-in (admin-set) to allow private/loopback targets for + // local testing. Defaults to false (full SSRF blocking) when the key + // is absent. See configuration.allowPrivateTargets. + $allowPrivate = (bool) ($webhook->getConfigurationArray()['allowPrivateTargets'] ?? false); + // SSRF guard: validate the webhook target before issuing the request. - // Redirects are re-validated by the on_redirect callback in - // initializeHttpClient(). - $this->assertSafeWebhookUri(uri: $webhook->getUrl()); + // Redirects are re-validated by the per-request on_redirect callback set + // in $options below, which honours the same per-hook flag. + $this->assertSafeWebhookUri(uri: $webhook->getUrl(), allowPrivate: $allowPrivate); $headers = array_merge( [ @@ -1086,6 +1222,40 @@ private function sendRequest(Webhook $webhook, array $payload): array 'timeout' => $webhook->getTimeout(), ]; + // Hard-cap connect + total timeout for request-blocking deliveries + // (interception). The cap never RAISES a webhook's own shorter timeout, + // but a non-positive per-webhook timeout (Guzzle: 0 = wait forever) + // must not escape the cap either. + if ($timeoutCapSeconds !== null) { + $webhookTimeout = (int) $webhook->getTimeout(); + $options['timeout'] = $timeoutCapSeconds; + if ($webhookTimeout > 0 && $webhookTimeout < $timeoutCapSeconds) { + $options['timeout'] = $webhookTimeout; + } + + $options['connect_timeout'] = $timeoutCapSeconds; + } + + // Override the shared client's redirect guard per-request so the + // on_redirect callback can see this hook's allowPrivate flag. Mirrors + // the client default (max/strict/protocols) from initializeHttpClient(); + // the only difference is the flag-aware re-validation. The shared client + // default keeps allowPrivate = false as the safe fallback. + $options['allow_redirects'] = [ + 'max' => 5, + 'strict' => true, + 'referer' => false, + 'protocols' => ['http', 'https'], + 'track_redirects' => true, + 'on_redirect' => function ( + RequestInterface $request, + ResponseInterface $response, + \Psr\Http\Message\UriInterface $uri + ) use ($allowPrivate): void { + $this->assertSafeWebhookUri(uri: (string) $uri, allowPrivate: $allowPrivate); + }, + ]; + // For GET requests, use query parameters; for others, send JSON body. $payloadKey = 'json'; if (strtoupper($webhook->getMethod()) === 'GET') { @@ -1136,7 +1306,7 @@ private function generateSignature(array $payload, string $secret): string * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-78 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-webhook-testing-must-support-dry-run-delivery */ private function scheduleRetry(Webhook $webhook, string $eventName, array $_payload, int $attempt): void { @@ -1185,7 +1355,7 @@ private function calculateNextRetryTime(Webhook $webhook, int $attempt): DateTim * * @return int Delay in seconds * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-78 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-webhook-testing-must-support-dry-run-delivery */ private function calculateRetryDelay(Webhook $webhook, int $attempt): int { @@ -1232,11 +1402,19 @@ private function calculateRetryDelay(Webhook $webhook, int $attempt): int */ public function interceptRequest(IRequest $request, string $eventType): array { - // Find webhooks configured for this event type. + // Fast path: a cached (or freshly computed) tenant-agnostic flag tells + // us whether ANY interception webhook exists for this event type. On + // installs without interception webhooks — the common case — this + // avoids the webhook table scan on every object write. + if ($this->hasInterceptionWebhooks(eventType: $eventType) === false) { + return $request->getParams(); + } + + // Find webhooks configured for this event type (organisation-filtered). $webhooks = $this->findWebhooksForInterception(eventType: $eventType); if (empty($webhooks) === true) { - // No webhooks configured, return original request data. + // No webhooks configured for this organisation, return original request data. return $request->getParams(); } @@ -1269,12 +1447,14 @@ public function interceptRequest(IRequest $request, string $eventType): array 'cloudEvent' => $cloudEvent, ]; - // Deliver webhook. + // Deliver webhook. Interception blocks the object write, so the + // delivery timeout is capped hard — see INTERCEPTION_TIMEOUT_SECONDS. $success = $this->deliverWebhook( webhook: $webhook, eventName: $eventType, payload: $webhookPayload, - attempt: 1 + attempt: 1, + timeoutCapSeconds: self::INTERCEPTION_TIMEOUT_SECONDS ); // Process response if webhook is configured to handle responses. @@ -1328,39 +1508,99 @@ public function interceptRequest(IRequest $request, string $eventType): array * @SuppressWarnings(PHPMD.CyclomaticComplexity) Multiple webhook filtering conditions * @SuppressWarnings(PHPMD.NPathComplexity) Multiple filter matching paths * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-80 + * @spec openspec/specs/webhook-payload-mapping/spec.md#requirement-request-interception-must-support-pre-event-webhooks */ private function findWebhooksForInterception(string $eventType): array { - // Get all enabled webhooks. + // Get all enabled webhooks (organisation-filtered). $allWebhooks = $this->webhookMapper->findEnabled(); // Filter webhooks that match this event type and are configured for interception. $matchingWebhooks = []; foreach ($allWebhooks as $webhook) { - $config = $webhook->getConfigurationArray(); + if ($this->matchesInterception(webhook: $webhook, eventType: $eventType) === true) { + $matchingWebhooks[] = $webhook; + } + } - // Check if webhook is configured for request interception. - if (($config['interceptRequests'] ?? false) !== true) { - continue; + return $matchingWebhooks; + }//end findWebhooksForInterception() + + /** + * Check whether a webhook intercepts requests for an event type + * + * A webhook intercepts when its configuration opts into interception AND + * it either listens to all events or matches the given event type. + * + * @param Webhook $webhook Webhook to check + * @param string $eventType Event type (e.g., 'object.creating') + * + * @return bool True when the webhook intercepts requests for the event type + * + * @spec openspec/specs/webhook-payload-mapping/spec.md#request-interception-pre-event-webhooks + */ + private function matchesInterception(Webhook $webhook, string $eventType): bool + { + $config = $webhook->getConfigurationArray(); + + // Check if webhook is configured for request interception. + if (($config['interceptRequests'] ?? false) !== true) { + return false; + } + + // Check if webhook listens to this event type (empty events = all events). + $events = $webhook->getEventsArray(); + if (empty($events) === false) { + $eventClass = $this->eventTypeToEventClass(eventType: $eventType); + if ($webhook->matchesEvent($eventClass) === false) { + return false; } + } - // Check if webhook listens to this event type. - $events = $webhook->getEventsArray(); - if (empty($events) === false) { - // Check if event type matches. - $eventClass = $this->eventTypeToEventClass(eventType: $eventType); - if ($webhook->matchesEvent($eventClass) === false) { - continue; - } + return true; + }//end matchesInterception() + + /** + * Determine whether ANY interception webhook exists for an event type + * + * Answers the question globally (tenant-agnostic) and caches the boolean + * in the distributed cache so the common zero-webhook case skips the + * table scan on every object write. A global "true" is a superset check: + * the caller still runs the organisation-filtered lookup to select the + * webhooks that actually apply. A per-organisation flag is deliberately + * NOT cached — one tenant's "false" must never disable another tenant's + * interception hooks. Cache entries are invalidated on webhook CRUD (see + * WebhookMapper) and expire via TTL as a safety net. + * + * @param string $eventType Event type (e.g., 'object.creating') + * + * @return bool True when at least one enabled interception webhook exists for the event type + * + * @spec openspec/specs/webhook-payload-mapping/spec.md#request-interception-pre-event-webhooks + */ + private function hasInterceptionWebhooks(string $eventType): bool + { + $cached = $this->interceptionCache?->get(eventType: $eventType); + if ($cached !== null) { + return $cached; + } + + // Cache miss (or no cache backend): compute the flag from a + // tenant-agnostic scan so the cached answer is valid for every + // organisation. + $hasWebhooks = false; + foreach ($this->webhookMapper->findEnabledForInterceptionScan() as $webhook) { + if ($this->matchesInterception(webhook: $webhook, eventType: $eventType) === true) { + $hasWebhooks = true; + break; } + } - $matchingWebhooks[] = $webhook; - }//end foreach + $this->interceptionCache?->set(eventType: $eventType, hasWebhooks: $hasWebhooks); - return $matchingWebhooks; - }//end findWebhooksForInterception() + return $hasWebhooks; + }//end hasInterceptionWebhooks() /** * Check if webhook response should be processed @@ -1390,7 +1630,7 @@ private function shouldProcessResponse(Webhook $webhook): bool * * @return string Event class name (e.g., 'OCA\OpenRegister\Event\ObjectCreatingEvent') * - * @spec openspec/changes/retrofit-2026-05-24-b-svc-i18n-endpoint-gql-wh/tasks.md#task-25 + * @spec openspec/specs/webhook-payload-mapping/spec.md */ private function eventTypeToEventClass(string $eventType): string { diff --git a/lib/Service/WorkflowEngineRegistry.php b/lib/Service/WorkflowEngineRegistry.php index 14ba55b1d4..0c244352df 100644 --- a/lib/Service/WorkflowEngineRegistry.php +++ b/lib/Service/WorkflowEngineRegistry.php @@ -17,8 +17,8 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-84 - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-configuration-entity + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-health-monitoring */ declare(strict_types=1); @@ -70,7 +70,7 @@ public function __construct( * * @throws InvalidArgumentException If engine type is unsupported * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-84 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-configuration-entity */ public function resolveAdapter(WorkflowEngine $engine): WorkflowEngineInterface { @@ -200,7 +200,7 @@ public function deleteEngine(int $id): WorkflowEngine * * @return array{healthy: bool, responseTime: int} * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-health-monitoring */ public function healthCheck(int $id): array { @@ -265,7 +265,7 @@ public function discoverEngines(): array * * @return array Decrypted auth config * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-84 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-configuration-entity */ private function decryptAuthConfig(WorkflowEngine $engine): array { diff --git a/lib/Service/XwikiLinkService.php b/lib/Service/XwikiLinkService.php index 19a071bb37..9f930c75c5 100644 --- a/lib/Service/XwikiLinkService.php +++ b/lib/Service/XwikiLinkService.php @@ -70,7 +70,10 @@ * user session + app manager + container + logger. Each dependency is * required for one of the Tier-2 flows (link, create, unlink, list, * picker, cache refresh, graceful degradation). - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Tier-2 service implements linkPage/createAndLinkPage/unlinkPage/getLinkedPages/getAvailablePages plus getProvider/resolveRouter/requireUid/resolveRemotePage/hydrateLink/normaliseRemoteRow/normaliseList/toServiceException/isStale/refreshLink; each is a required face of the xWiki integration surface. + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Tier-2 service implements + * linkPage/createAndLinkPage/unlinkPage/getLinkedPages/getAvailablePages plus + * getProvider/resolveRouter/requireUid/resolveRemotePage/hydrateLink/normaliseRemoteRow/normaliseList/ + * toServiceException/isStale/refreshLink; each is a required face of the xWiki integration surface. */ class XwikiLinkService { @@ -243,8 +246,13 @@ public function linkPage(string $objectUuid, int $registerId, int $schemaId, str * * @spec openspec/changes/retrofit-2026-05-25-bw2-svc-flat-2/tasks.md#task-2 * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) createAndLinkPage() guards user auth, non-empty title/space, OpenConnector availability, provider resolution, create call, canonical-reference extraction, and title/space fallback in sequence; each is a required guard for the atomic create+link contract. - * @SuppressWarnings(PHPMD.NPathComplexity) empty-title + empty-space + OpenConnector-unavailable + provider-null + ProviderUnavailableException + generic-Throwable + canonical-reference-fallback + title-fallback + space-fallback produce many independent paths; all guard the create+link contract. + * @SuppressWarnings(PHPMD.CyclomaticComplexity) createAndLinkPage() guards user auth, non-empty + * title/space, OpenConnector availability, provider resolution, create call, canonical-reference + * extraction, and title/space fallback in sequence; each is a required guard for the atomic + * create+link contract. + * @SuppressWarnings(PHPMD.NPathComplexity) empty-title + empty-space + OpenConnector-unavailable + * + provider-null + ProviderUnavailableException + generic-Throwable + canonical-reference-fallback + * + title-fallback + space-fallback produce many independent paths; all guard the create+link contract. */ public function createAndLinkPage( string $objectUuid, @@ -460,6 +468,99 @@ public function getAvailablePages(?string $search=null): array return ['results' => $out, 'total' => count($out)]; }//end getAvailablePages() + /** + * Free-text search of the remote xWiki knowledge base — object-independent. + * + * This is the query-first surface a consuming app (e.g. a dashboard widget) + * uses to find pages by free text, without an OR object context. It shares + * the XwikiProvider browse path with {@see getAvailablePages()} but exposes + * a paginated `{ results, total, limit, offset }` envelope and resolves the + * xWiki base URL exclusively from the OpenConnector `xwiki` source (AD-4) — + * the consumer never holds a direct xWiki URL. + * + * Read-only and null-safe: when no `xwiki` source is configured (or the + * upstream is unreachable) it returns the unconfigured-source descriptor + * `{ unavailable, cause, results: [], total: 0, limit, offset }` rather than + * throwing, so the caller degrades to an empty/Configure state and never a + * fatal (AD-23). + * + * @param string|null $query Free-text query (title/content). Null/empty + * lists the most recent / available pages. + * @param int $limit Max results (clamped 1..100). + * @param int $offset Pagination offset (clamped >= 0). + * + * @return array `{ results, total, limit, offset }` on + * success, or `{ unavailable, cause, results, + * total, limit, offset }` when the source is + * unconfigured/down. + * + * @spec openspec/changes/integration-xwiki-query-search/specs/integration-xwiki/spec.md + */ + public function searchPages(?string $query=null, int $limit=25, int $offset=0): array + { + $limit = max(1, min(100, $limit)); + $offset = max(0, $offset); + + if ($this->isOpenConnectorAvailable() === false) { + return $this->degraded(cause: ProviderUnavailableException::CAUSE_OPENCONNECTOR_DOWN, limit: $limit, offset: $offset); + } + + $provider = $this->getProvider(); + if ($provider === null) { + return $this->degraded(cause: ProviderUnavailableException::CAUSE_OPENCONNECTOR_DOWN, limit: $limit, offset: $offset); + } + + $filters = ['_limit' => $limit, '_page' => (int) (floor($offset / $limit) + 1)]; + if ($query !== null && trim($query) !== '') { + $filters['_search'] = trim($query); + } + + try { + // Object-independent browse/search: empty OR context, the + // OpenConnector source returns the matching remote pages. + $rows = $provider->list('', '', '', $filters); + } catch (ProviderUnavailableException $e) { + return $this->degraded(cause: $e->getCause(), limit: $limit, offset: $offset); + } catch (Throwable $e) { + $this->logger->warning('XwikiLinkService::searchPages failed: '.$e->getMessage()); + return $this->degraded(cause: ProviderUnavailableException::CAUSE_UPSTREAM_SERVICE_DOWN, limit: $limit, offset: $offset); + } + + $out = []; + foreach ($rows as $row) { + if (is_array($row) === true) { + $out[] = $this->normaliseRemoteRow(row: $row); + } + } + + return [ + 'results' => $out, + 'total' => count($out), + 'limit' => $limit, + 'offset' => $offset, + ]; + }//end searchPages() + + /** + * Build the degraded search envelope — the unconfigured-source descriptor + * folded with the resolved limit/offset, so the search shape stays stable + * across the success + degraded paths (AD-23). + * + * @param string $cause One of the ProviderUnavailableException CAUSE_* values. + * @param int $limit Resolved limit. + * @param int $offset Resolved offset. + * + * @return array + */ + private function degraded(string $cause, int $limit, int $offset): array + { + $state = $this->unavailableState(cause: $cause); + $state['limit'] = $limit; + $state['offset'] = $offset; + + return $state; + }//end degraded() + /** * Resolve a remote xWiki page's canonical reference + metadata via * OpenConnector. Best-effort: when the upstream is unreachable the diff --git a/lib/Service/ZaaktypeAuthorizationService.php b/lib/Service/ZaaktypeAuthorizationService.php new file mode 100644 index 0000000000..a4c4ae9153 --- /dev/null +++ b/lib/Service/ZaaktypeAuthorizationService.php @@ -0,0 +1,375 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT: + * @link https://www.OpenRegister.app + * + * @since 2.0.0 Initial implementation for zaaktype-scoped authorization config. + */ + +declare(strict_types=1); + +namespace OCA\OpenRegister\Service; + +/** + * Zaaktype-scoped authorization configuration and ZGW mapping helper. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) ZGW scope and matrix mapping branch per scope verb and per principal kind. + */ +class ZaaktypeAuthorizationService +{ + + /** + * Canonical ZGW vertrouwelijkheidaanduiding (confidentiality) levels, ordered + * least-restrictive (index 0) to most-restrictive (index 7). + * + * Source: ZGW Catalogi API `VertrouwelijkheidaanduidingEnum`. The ordinal + * position drives "a user cleared to level N may see objects at level N or + * below" decisions. Changing the order here changes the security semantics + * for every consumer, so it is treated as a single source of truth. + * + * @var string[] + */ + public const VERTROUWELIJKHEIDAANDUIDING_LEVELS = [ + 'openbaar', + 'beperkt_openbaar', + 'intern', + 'zaakvertrouwelijk', + 'vertrouwelijk', + 'confidentieel', + 'geheim', + 'zeer_geheim', + ]; + + /** + * Mapping of the four ZGW CRUD scope suffixes to OpenRegister action verbs. + * + * ZGW scopes follow the pattern `zaken.lezen` / `zaken.aanmaken` / + * `zaken.bijwerken` / `zaken.verwijderen`. The suffix after the final `.` + * determines the action. + * + * @var array + */ + private const ZGW_SCOPE_SUFFIX_TO_ACTION = [ + 'lezen' => 'read', + 'aanmaken' => 'create', + 'bijwerken' => 'update', + 'verwijderen' => 'delete', + ]; + + /** + * Return the ordinal (0-based) position of a confidentiality level. + * + * @param string $level The vertrouwelijkheidaanduiding value. + * + * @return int|null The 0-based ordinal, or null when the level is unknown. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + public function confidentialityOrdinal(string $level): ?int + { + $index = array_search($level, self::VERTROUWELIJKHEIDAANDUIDING_LEVELS, true); + if ($index === false) { + return null; + } + + return $index; + }//end confidentialityOrdinal() + + /** + * Return every confidentiality level at or below a maximum clearance. + * + * Used to build the allow-list for an `$in` match clause: a principal cleared + * to `$maxLevel` may access objects whose vertrouwelijkheidaanduiding is in + * the returned set. Fail closed: an unknown `$maxLevel` returns an empty list + * (which, when fed to `buildConfidentialityMatch()`, yields a match that the + * engine can never satisfy). + * + * @param string $maxLevel The maximum clearance level (inclusive). + * + * @return string[] Allowed levels, ordered least- to most-restrictive. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + public function levelsAtOrBelow(string $maxLevel): array + { + // Derive the allow-list from the same ordinal comparison used for + // single-object access decisions, so the list-filter allow-set and the + // single-object verdict can never disagree (no list-vs-find drift). + return array_values( + array_filter( + self::VERTROUWELIJKHEIDAANDUIDING_LEVELS, + fn(string $level): bool => $this->isAccessibleAtClearance(maxLevel: $maxLevel, objectLevel: $level) + ) + ); + }//end levelsAtOrBelow() + + /** + * Decide whether a principal cleared to `$maxLevel` may access an object at + * `$objectLevel`. + * + * Fail closed: an unknown clearance or an unknown object level denies access. + * This is the canonical ordinal comparison the audit-denial reason + * `confidentiality_level_exceeded` is derived from. + * + * @param string $maxLevel The principal's maximum clearance. + * @param string $objectLevel The object's vertrouwelijkheidaanduiding. + * + * @return bool True only when objectLevel <= maxLevel by ZGW ordinal. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + public function isAccessibleAtClearance(string $maxLevel, string $objectLevel): bool + { + $maxOrdinal = $this->confidentialityOrdinal(level: $maxLevel); + $objectOrdinal = $this->confidentialityOrdinal(level: $objectLevel); + + if ($maxOrdinal === null || $objectOrdinal === null) { + return false; + } + + return $objectOrdinal <= $maxOrdinal; + }//end isAccessibleAtClearance() + + /** + * Build a conditional-match clause that limits a rule to objects at or below + * a maximum vertrouwelijkheidaanduiding. + * + * The clause is expressed in the existing `$in` operator vocabulary so the + * unchanged RBAC engine enforces it at both the PHP (ConditionMatcher) and + * SQL (MagicRbacHandler) layers. The property name is configurable to match + * the schema's actual field (default `vertrouwelijkheidaanduiding`). + * + * @param string $maxLevel The maximum clearance level (inclusive). + * @param string $property The object property holding the confidentiality level. + * + * @return array> A `match` clause: + * `{ "": { "$in": [] } }`. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + public function buildConfidentialityMatch(string $maxLevel, string $property='vertrouwelijkheidaanduiding'): array + { + return [$property => ['$in' => $this->levelsAtOrBelow(maxLevel: $maxLevel)]]; + }//end buildConfidentialityMatch() + + /** + * Map a single ZGW Autorisaties "autorisatie" record onto an OpenRegister + * schema `authorization` block. + * + * Mapping (per the rbac-zaaktype spec "ZGW Autorisaties API concepts" req): + * - Each ZGW `scope` (e.g. `zaken.lezen`) maps to an OpenRegister action + * (`read`) granted to the supplied `$group` (the Nextcloud group that + * represents the ZGW Applicatie's identity / scope). + * - A `maxVertrouwelijkheidaanduiding` (when present) attaches an `$in` + * conditional match to EVERY mapped action so the engine filters objects + * above the clearance at query time. + * + * The returned block is purely the existing group / conditional vocabulary — + * it can be merged into a schema's `authorization` and enforced unchanged. + * Unknown scope suffixes are skipped (fail closed: they grant nothing). + * + * @param array $scopes ZGW scopes, e.g. ['zaken.lezen', 'zaken.aanmaken']. + * @param string $group The Nextcloud group representing the ZGW Applicatie. + * @param string|null $maxLevel Optional maxVertrouwelijkheidaanduiding. + * @param string $property Confidentiality property name on the schema. + * + * @return array> An `authorization`-shaped block. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + public function mapZgwAutorisatie( + array $scopes, + string $group, + ?string $maxLevel=null, + string $property='vertrouwelijkheidaanduiding' + ): array { + $authorization = []; + + foreach ($scopes as $scope) { + if (is_string($scope) === false || $scope === '') { + continue; + } + + $action = $this->scopeToAction(scope: $scope); + if ($action === null) { + // Unknown scope suffix grants nothing (fail closed). + continue; + } + + // A clearance limit turns the grant into a conditional rule; an + // unrestricted grant (heeftAlleAutorisaties-style per-zaaktype) is a + // bare group string. + if ($maxLevel !== null) { + $entry = [ + 'group' => $group, + 'match' => $this->buildConfidentialityMatch(maxLevel: $maxLevel, property: $property), + ]; + } else { + $entry = $group; + } + + if (isset($authorization[$action]) === false) { + $authorization[$action] = []; + } + + $authorization[$action][] = $entry; + }//end foreach + + return $authorization; + }//end mapZgwAutorisatie() + + /** + * Translate a single ZGW scope string to an OpenRegister action verb. + * + * @param string $scope A ZGW scope, e.g. `zaken.lezen`. + * + * @return string|null The action (`read`/`create`/`update`/`delete`), or null + * when the scope suffix is not a recognised CRUD verb. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + public function scopeToAction(string $scope): ?string + { + $dot = strrpos($scope, '.'); + if ($dot === false) { + $suffix = $scope; + } else { + $suffix = substr($scope, ($dot + 1)); + } + + return self::ZGW_SCOPE_SUFFIX_TO_ACTION[$suffix] ?? null; + }//end scopeToAction() + + /** + * Extract a read-only permission matrix from a resolved authorization block. + * + * Produces one row per (action x principal), distinguishing group entries, + * `user:`/`{user}` overrides, and conditional (`match`) entries. This is the + * canonical representation the admin matrix view and CSV export render; it is + * derived from the SAME `authorization` block the engine enforces, so it can + * never drift from the actual access decisions. + * + * @param array|null $authorization A resolved authorization block. + * + * @return array + * One entry per (action, principal) grant. + * + * @spec openspec/specs/rbac-zaaktype/spec.md + */ + public function extractPermissionMatrix(?array $authorization): array + { + if (is_array($authorization) === false || $authorization === []) { + return []; + } + + $rows = []; + + foreach ($authorization as $action => $entries) { + // Skip non-action keys (e.g. inheritFromPublic, roles) and malformed lists. + if (in_array($action, ['read', 'create', 'update', 'delete', 'list'], true) === false) { + continue; + } + + if (is_array($entries) === false) { + continue; + } + + foreach ($entries as $entry) { + $row = $this->matrixRowForEntry(action: (string) $action, entry: $entry); + if ($row !== null) { + $rows[] = $row; + } + } + }//end foreach + + return $rows; + }//end extractPermissionMatrix() + + /** + * Build a single permission-matrix row for one authorization entry. + * + * @param string $action The CRUD action the entry grants. + * @param mixed $entry A single authorization entry (string or array). + * + * @return array{action: string, principal: string, kind: string, conditional: bool}|null + * The row, or null when the entry is malformed. + */ + private function matrixRowForEntry(string $action, mixed $entry): ?array + { + // Bare string entry: a group or a `user:` override. + if (is_string($entry) === true) { + if (str_starts_with($entry, 'user:') === true) { + return [ + 'action' => $action, + 'principal' => $entry, + 'kind' => 'user', + 'conditional' => false, + ]; + } + + return [ + 'action' => $action, + 'principal' => $entry, + 'kind' => 'group', + 'conditional' => false, + ]; + } + + if (is_array($entry) === false) { + return null; + } + + $conditional = isset($entry['match']) === true && empty($entry['match']) === false; + + // Complex user override: { "user": "", "match"?: {...} }. + if (isset($entry['user']) === true && is_string($entry['user']) === true) { + return [ + 'action' => $action, + 'principal' => 'user:'.$entry['user'], + 'kind' => 'user', + 'conditional' => $conditional, + ]; + } + + // Complex group rule: { "group": "", "match"?: {...} }. + if (isset($entry['group']) === true && is_string($entry['group']) === true) { + return [ + 'action' => $action, + 'principal' => $entry['group'], + 'kind' => 'group', + 'conditional' => $conditional, + ]; + } + + return null; + }//end matrixRowForEntry() +}//end class diff --git a/lib/Settings/OpenRegisterAdmin.php b/lib/Settings/OpenRegisterAdmin.php index 1e56a9d51c..92d26a1855 100644 --- a/lib/Settings/OpenRegisterAdmin.php +++ b/lib/Settings/OpenRegisterAdmin.php @@ -87,7 +87,7 @@ class OpenRegisterAdmin implements ISettings * @param IAppConfig $appConfig App config for reading push_available flag * @param IInitialState $initialState Initial state service for Vue data * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md * @spec openspec/changes/add-live-updates/tasks.md#task-10 */ public function __construct( @@ -140,7 +140,7 @@ private function getPushStatus(): string * * @psalm-return TemplateResponse<200, array> * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md * @spec openspec/changes/add-live-updates/tasks.md#task-10 */ public function getForm() @@ -170,7 +170,7 @@ public function getForm() * * @psalm-return 'openregister' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getSection() { @@ -189,7 +189,7 @@ public function getSection() * * @psalm-return 11 * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-25 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getPriority() { diff --git a/lib/Settings/OpenRegisterPersonal.php b/lib/Settings/OpenRegisterPersonal.php new file mode 100644 index 0000000000..cf39cf7372 --- /dev/null +++ b/lib/Settings/OpenRegisterPersonal.php @@ -0,0 +1,92 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @version GIT: + * + * @link https://OpenRegister.app + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + +namespace OCA\OpenRegister\Settings; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Settings\ISettings; + +/** + * OpenRegisterPersonal + * + * Personal settings implementation for OpenRegister — the browser-notifications + * opt-in toggle. + * + * @category Settings + * @package OCA\OpenRegister\Settings + */ +class OpenRegisterPersonal implements ISettings +{ + /** + * Get the personal settings form. + * + * Renders the personal-settings template, which mounts the Vue + * browser-notifications opt-in toggle. + * + * @return TemplateResponse The personal settings template response. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function getForm(): TemplateResponse + { + return new TemplateResponse( + appName: 'openregister', + templateName: 'settings/personal' + ); + + }//end getForm() + + /** + * Get the personal settings section identifier. + * + * Renders on the built-in "notifications" personal section (provided by the + * notifications app) where users expect notification controls; falls back to + * "additional" behaviour is unnecessary since notifications is always present + * on these instances. + * + * @return string The section id. + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function getSection(): string + { + return 'notifications'; + + }//end getSection() + + /** + * Get the settings priority within the section. + * + * @return int Priority (0-100; lower renders earlier). + * + * @spec openspec/changes/openregister-web-push-engine/specs/web-push-delivery/spec.md + */ + public function getPriority(): int + { + return 50; + + }//end getPriority() +}//end class diff --git a/lib/Settings/bag_register.json b/lib/Settings/bag_register.json index 5be3293e69..9282be03a3 100644 --- a/lib/Settings/bag_register.json +++ b/lib/Settings/bag_register.json @@ -34,6 +34,7 @@ "published": "2025-01-01T00:00:00+00:00", "summary": "A BAG address designation (house number + postal code combination)", "description": "A nummeraanduiding is the official BAG object that assigns a house number (with optional letter and addition) and postal code to an addressable object. It links a human-readable address to a verblijfsobject, standplaats, or ligplaats via the BAG identification system.", + "x-schema-org": "schema:PostalAddress", "required": [ "identificatie", "huisnummer", @@ -50,26 +51,30 @@ "pattern": "^[0-9]{16}$", "minLength": 16, "maxLength": 16, - "facetable": true + "facetable": true, + "title": "Identification Number" }, "huisnummer": { "type": "integer", "description": "House number (1-99999)", "minimum": 1, "maximum": 99999, - "facetable": true + "facetable": true, + "title": "House Number" }, "huisletter": { "type": "string", "description": "Optional letter suffix (a-z, A-Z)", "maxLength": 1, - "facetable": false + "facetable": false, + "title": "House Letter" }, "huisnummertoevoeging": { "type": "string", "description": "Optional house number addition (e.g. 'bis', 'I', 'M')", "maxLength": 4, - "facetable": false + "facetable": false, + "title": "House Number Addition" }, "postcode": { "type": "string", @@ -77,7 +82,8 @@ "pattern": "^[0-9]{4}[A-Z]{2}$", "minLength": 6, "maxLength": 6, - "facetable": true + "facetable": true, + "title": "Postal Code" }, "status": { "type": "string", @@ -86,7 +92,8 @@ "Naamgeving uitgegeven", "Naamgeving ingetrokken" ], - "facetable": true + "facetable": true, + "title": "Status" }, "typeAdresseerbaarObject": { "type": "string", @@ -96,19 +103,22 @@ "Standplaats", "Ligplaats" ], - "facetable": true + "facetable": true, + "title": "Addressable Object Type" }, "openbareRuimteNaam": { "type": "string", "description": "Street name (naam openbare ruimte)", "maxLength": 80, - "facetable": true + "facetable": true, + "title": "Street Name" }, "woonplaatsNaam": { "type": "string", "description": "City or village name", "maxLength": 80, - "facetable": true + "facetable": true, + "title": "City Name" } }, "hardValidation": false, @@ -126,6 +136,7 @@ "published": "2025-01-01T00:00:00+00:00", "summary": "A BAG dwelling unit or usable space within a building", "description": "A verblijfsobject is the smallest usable unit within one or more panden (buildings) that is suitable for residential, office, retail, or other use. It has one or more gebruiksdoelen (usage purposes) and a floor area in square meters.", + "x-schema-org": "schema:Place", "required": [ "identificatie", "gebruiksdoel", @@ -141,7 +152,8 @@ "pattern": "^[0-9]{16}$", "minLength": 16, "maxLength": 16, - "facetable": true + "facetable": true, + "title": "Identification Number" }, "gebruiksdoel": { "type": "array", @@ -162,13 +174,15 @@ "overige gebruiksfunctie" ] }, - "facetable": true + "facetable": true, + "title": "Usage Purposes" }, "oppervlakte": { "type": "integer", "description": "Floor area in square meters", "minimum": 1, - "facetable": true + "facetable": true, + "title": "Floor Area" }, "status": { "type": "string", @@ -180,19 +194,22 @@ "Verblijfsobject ingetrokken", "Verblijfsobject buiten gebruik" ], - "facetable": true + "facetable": true, + "title": "Status" }, "pandIdentificatie": { "type": "string", "description": "Reference to the pand (building) this verblijfsobject is in (16 digits, TT=10)", "pattern": "^[0-9]{16}$", - "facetable": true + "facetable": true, + "title": "Building Identification" }, "nummeraanduidingIdentificatie": { "type": "string", "description": "Reference to the nummeraanduiding (address) of this verblijfsobject (16 digits, TT=20)", "pattern": "^[0-9]{16}$", - "facetable": true + "facetable": true, + "title": "Address Identification" } }, "hardValidation": false, @@ -210,6 +227,7 @@ "published": "2025-01-01T00:00:00+00:00", "summary": "A BAG building (pand)", "description": "A pand is the smallest building unit that is architecturally self-contained, accessible from the outside, and functionally independent. Each pand has a construction year and a status indicating its lifecycle state.", + "x-schema-org": "schema:Place", "required": [ "identificatie", "oorspronkelijkBouwjaar", @@ -222,7 +240,8 @@ "pattern": "^[0-9]{16}$", "minLength": 16, "maxLength": 16, - "facetable": true + "facetable": true, + "title": "Identification Number" }, "oorspronkelijkBouwjaar": { "type": "string", @@ -230,7 +249,8 @@ "pattern": "^[0-9]{4}$", "minLength": 4, "maxLength": 4, - "facetable": true + "facetable": true, + "title": "Original Construction Year" }, "status": { "type": "string", @@ -245,7 +265,8 @@ "Bouw gestart", "Sloopvergunning verleend" ], - "facetable": true + "facetable": true, + "title": "Status" } }, "hardValidation": false, @@ -716,7 +737,9 @@ "slug": "0599010000208579" }, "identificatie": "0599010000208579", - "gebruiksdoel": ["woonfunctie"], + "gebruiksdoel": [ + "woonfunctie" + ], "oppervlakte": 85, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0599100000300001", @@ -729,7 +752,9 @@ "slug": "0363010000100100" }, "identificatie": "0363010000100100", - "gebruiksdoel": ["woonfunctie"], + "gebruiksdoel": [ + "woonfunctie" + ], "oppervlakte": 120, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0363100000100001", @@ -742,7 +767,9 @@ "slug": "0518010000610966" }, "identificatie": "0518010000610966", - "gebruiksdoel": ["woonfunctie"], + "gebruiksdoel": [ + "woonfunctie" + ], "oppervlakte": 95, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0518100000600001", @@ -755,7 +782,9 @@ "slug": "0344010000050050" }, "identificatie": "0344010000050050", - "gebruiksdoel": ["woonfunctie"], + "gebruiksdoel": [ + "woonfunctie" + ], "oppervlakte": 78, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0344100000050001", @@ -768,7 +797,9 @@ "slug": "0014010000001001" }, "identificatie": "0014010000001001", - "gebruiksdoel": ["woonfunctie"], + "gebruiksdoel": [ + "woonfunctie" + ], "oppervlakte": 110, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0014100000001001", @@ -781,7 +812,9 @@ "slug": "0034010000001001" }, "identificatie": "0034010000001001", - "gebruiksdoel": ["woonfunctie"], + "gebruiksdoel": [ + "woonfunctie" + ], "oppervlakte": 92, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0034100000001001", @@ -794,7 +827,9 @@ "slug": "0153010000015015" }, "identificatie": "0153010000015015", - "gebruiksdoel": ["woonfunctie"], + "gebruiksdoel": [ + "woonfunctie" + ], "oppervlakte": 68, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0153100000015001", @@ -807,7 +842,9 @@ "slug": "0935010000046046" }, "identificatie": "0935010000046046", - "gebruiksdoel": ["woonfunctie"], + "gebruiksdoel": [ + "woonfunctie" + ], "oppervlakte": 105, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0935100000046001", @@ -820,7 +857,9 @@ "slug": "0363010000017017" }, "identificatie": "0363010000017017", - "gebruiksdoel": ["kantoorfunctie"], + "gebruiksdoel": [ + "kantoorfunctie" + ], "oppervlakte": 250, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0363100000017001", @@ -833,7 +872,10 @@ "slug": "0363010000200200" }, "identificatie": "0363010000200200", - "gebruiksdoel": ["kantoorfunctie", "winkelfunctie"], + "gebruiksdoel": [ + "kantoorfunctie", + "winkelfunctie" + ], "oppervlakte": 180, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0363100000200001", @@ -846,7 +888,9 @@ "slug": "0363010000500500" }, "identificatie": "0363010000500500", - "gebruiksdoel": ["kantoorfunctie"], + "gebruiksdoel": [ + "kantoorfunctie" + ], "oppervlakte": 320, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0363100000500001", @@ -859,7 +903,10 @@ "slug": "0599010000300300" }, "identificatie": "0599010000300300", - "gebruiksdoel": ["kantoorfunctie", "bijeenkomstfunctie"], + "gebruiksdoel": [ + "kantoorfunctie", + "bijeenkomstfunctie" + ], "oppervlakte": 4500, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0599100000300002", @@ -872,7 +919,10 @@ "slug": "0599010000300301" }, "identificatie": "0599010000300301", - "gebruiksdoel": ["winkelfunctie", "logiesfunctie"], + "gebruiksdoel": [ + "winkelfunctie", + "logiesfunctie" + ], "oppervlakte": 65, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0599100000300003", @@ -885,7 +935,10 @@ "slug": "0363010000600600" }, "identificatie": "0363010000600600", - "gebruiksdoel": ["winkelfunctie", "kantoorfunctie"], + "gebruiksdoel": [ + "winkelfunctie", + "kantoorfunctie" + ], "oppervlakte": 2200, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0363100000600001", @@ -898,7 +951,9 @@ "slug": "0363010000600601" }, "identificatie": "0363010000600601", - "gebruiksdoel": ["bijeenkomstfunctie"], + "gebruiksdoel": [ + "bijeenkomstfunctie" + ], "oppervlakte": 8000, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0363100000600002", @@ -911,7 +966,10 @@ "slug": "0363010000600602" }, "identificatie": "0363010000600602", - "gebruiksdoel": ["bijeenkomstfunctie", "woonfunctie"], + "gebruiksdoel": [ + "bijeenkomstfunctie", + "woonfunctie" + ], "oppervlakte": 450, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0363100000600003", @@ -924,7 +982,9 @@ "slug": "0518010000700700" }, "identificatie": "0518010000700700", - "gebruiksdoel": ["kantoorfunctie"], + "gebruiksdoel": [ + "kantoorfunctie" + ], "oppervlakte": 5500, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0518100000700001", @@ -937,7 +997,9 @@ "slug": "0344010000060060" }, "identificatie": "0344010000060060", - "gebruiksdoel": ["bijeenkomstfunctie"], + "gebruiksdoel": [ + "bijeenkomstfunctie" + ], "oppervlakte": 3200, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0344100000060001", @@ -950,7 +1012,9 @@ "slug": "0014010000002002" }, "identificatie": "0014010000002002", - "gebruiksdoel": ["winkelfunctie"], + "gebruiksdoel": [ + "winkelfunctie" + ], "oppervlakte": 140, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0014100000002001", @@ -963,7 +1027,9 @@ "slug": "0363010000600603" }, "identificatie": "0363010000600603", - "gebruiksdoel": ["gezondheidszorgfunctie"], + "gebruiksdoel": [ + "gezondheidszorgfunctie" + ], "oppervlakte": 1800, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0363100000600004", @@ -976,7 +1042,10 @@ "slug": "0935010000047047" }, "identificatie": "0935010000047047", - "gebruiksdoel": ["winkelfunctie", "logiesfunctie"], + "gebruiksdoel": [ + "winkelfunctie", + "logiesfunctie" + ], "oppervlakte": 200, "status": "Verblijfsobject in gebruik", "pandIdentificatie": "0935100000047001", @@ -1194,4 +1263,4 @@ } ] } -} \ No newline at end of file +} diff --git a/lib/Settings/brp_register.json b/lib/Settings/brp_register.json index c009efb33e..2d385aa200 100644 --- a/lib/Settings/brp_register.json +++ b/lib/Settings/brp_register.json @@ -32,6 +32,7 @@ "published": "2026-03-16T00:00:00+00:00", "summary": "A person registered in the BRP (Basisregistratie Personen)", "description": "Represents an ingeschreven persoon as defined by the Haal Centraal BRP Personen Bevragen API v2 data model. Contains personal details, address information, family relationships, nationality, and life events.", + "x-schema-org": "schema:Person", "required": [ "burgerservicenummer", "geslachtsnaam", @@ -45,32 +46,37 @@ "pattern": "^[0-9]{9}$", "minLength": 9, "maxLength": 9, - "facetable": true + "facetable": true, + "title": "Citizen Service Number (BSN)" }, "voornamen": { "type": "string", "description": "First names (space-separated)", "maxLength": 200, - "facetable": false + "facetable": false, + "title": "First Names" }, "voorletters": { "type": "string", "description": "Initials derived from voornamen", "maxLength": 20, - "facetable": false + "facetable": false, + "title": "Initials" }, "voorvoegsel": { "type": "string", "description": "Name prefix (e.g. 'de', 'van der', 'van')", "maxLength": 10, - "facetable": true + "facetable": true, + "title": "Name Prefix" }, "geslachtsnaam": { "type": "string", "description": "Family name / surname", "maxLength": 200, "minLength": 1, - "facetable": true + "facetable": true, + "title": "Family Name" }, "aanduidingNaamgebruik": { "type": "string", @@ -81,7 +87,8 @@ "V", "N" ], - "facetable": true + "facetable": true, + "title": "Name Usage Indicator" }, "geslachtsaanduiding": { "type": "string", @@ -91,43 +98,50 @@ "V", "O" ], - "facetable": true + "facetable": true, + "title": "Gender" }, "geboortedatum": { "type": "string", "description": "Date of birth in ISO 8601 format", "format": "date", - "facetable": true + "facetable": true, + "title": "Date of Birth" }, "geboorteplaats": { "type": "string", "description": "Place of birth", "maxLength": 80, - "facetable": true + "facetable": true, + "title": "Place of Birth" }, "geboorteland": { "type": "string", "description": "Country of birth", "maxLength": 80, - "facetable": true + "facetable": true, + "title": "Country of Birth" }, "geboortelandCode": { "type": "string", "description": "Country of birth code (Tabel 34 Landentabel)", "maxLength": 4, - "facetable": false + "facetable": false, + "title": "Country of Birth Code" }, "nationaliteit": { "type": "string", "description": "Nationality description", "maxLength": 80, - "facetable": true + "facetable": true, + "title": "Nationality" }, "nationaliteitCode": { "type": "string", "description": "Nationality code (Tabel 32 Nationaliteitentabel)", "maxLength": 4, - "facetable": false + "facetable": false, + "title": "Nationality Code" }, "verblijfplaats": { "type": "object", @@ -136,43 +150,51 @@ "straat": { "type": "string", "description": "Street name (naam openbare ruimte)", - "maxLength": 80 + "maxLength": 80, + "title": "Street Name" }, "huisnummer": { "type": "integer", "description": "House number", "minimum": 1, - "maximum": 99999 + "maximum": 99999, + "title": "House Number" }, "huisletter": { "type": "string", "description": "House letter (optional)", - "maxLength": 1 + "maxLength": 1, + "title": "House Letter" }, "huisnummertoevoeging": { "type": "string", "description": "House number addition (optional)", - "maxLength": 4 + "maxLength": 4, + "title": "House Number Addition" }, "postcode": { "type": "string", "description": "Dutch postal code (####XX)", - "pattern": "^[0-9]{4}[A-Z]{2}$" + "pattern": "^[0-9]{4}[A-Z]{2}$", + "title": "Postal Code" }, "woonplaats": { "type": "string", "description": "City / woonplaats", - "maxLength": 80 + "maxLength": 80, + "title": "City" }, "adresseerbaarObjectIdentificatie": { "type": "string", "description": "BAG verblijfsobject identification (16 digits)", - "pattern": "^[0-9]{16}$" + "pattern": "^[0-9]{16}$", + "title": "Addressable Object Identification" }, "nummeraanduidingIdentificatie": { "type": "string", "description": "BAG nummeraanduiding identification (16 digits)", - "pattern": "^[0-9]{16}$" + "pattern": "^[0-9]{16}$", + "title": "Address Designation Identification" }, "functieAdres": { "type": "string", @@ -180,28 +202,33 @@ "enum": [ "W", "B" - ] + ], + "title": "Address Function" } }, - "facetable": false + "facetable": false, + "title": "Residential Address" }, "gemeenteVanInschrijving": { "type": "string", "description": "Municipality of registration", "maxLength": 80, - "facetable": true + "facetable": true, + "title": "Municipality of Registration" }, "gemeenteVanInschrijvingCode": { "type": "string", "description": "Municipality code (4 digits)", "pattern": "^[0-9]{4}$", - "facetable": true + "facetable": true, + "title": "Municipality Code" }, "datumInschrijvingInGemeente": { "type": "string", "description": "Date of registration in the municipality", "format": "date", - "facetable": false + "facetable": false, + "title": "Date of Municipal Registration" }, "immigratie": { "type": "object", @@ -210,15 +237,18 @@ "datumVestiging": { "type": "string", "description": "Date of settlement in the Netherlands", - "format": "date" + "format": "date", + "title": "Date of Settlement" }, "landVanHerkomst": { "type": "string", "description": "Country of origin", - "maxLength": 80 + "maxLength": 80, + "title": "Country of Origin" } }, - "facetable": false + "facetable": false, + "title": "Immigration Details" }, "overlijden": { "type": "object", @@ -227,15 +257,18 @@ "datum": { "type": "string", "description": "Date of death", - "format": "date" + "format": "date", + "title": "Date of Death" }, "plaats": { "type": "string", "description": "Place of death", - "maxLength": 80 + "maxLength": 80, + "title": "Place of Death" } }, - "facetable": false + "facetable": false, + "title": "Death Details" }, "partners": { "type": "array", @@ -245,11 +278,13 @@ "properties": { "burgerservicenummer": { "type": "string", - "description": "Partner's BSN" + "description": "Partner's BSN", + "title": "Partner BSN" }, "naam": { "type": "string", - "description": "Partner's full name" + "description": "Partner's full name", + "title": "Partner Name" }, "soortVerbintenis": { "type": "string", @@ -257,11 +292,13 @@ "enum": [ "H", "P" - ] + ], + "title": "Partnership Type" } } }, - "facetable": false + "facetable": false, + "title": "Partners" }, "ouders": { "type": "array", @@ -271,11 +308,13 @@ "properties": { "burgerservicenummer": { "type": "string", - "description": "Parent's BSN" + "description": "Parent's BSN", + "title": "Parent BSN" }, "naam": { "type": "string", - "description": "Parent's full name" + "description": "Parent's full name", + "title": "Parent Name" }, "ouderAanduiding": { "type": "string", @@ -283,11 +322,13 @@ "enum": [ "1", "2" - ] + ], + "title": "Parent Indicator" } } }, - "facetable": false + "facetable": false, + "title": "Parents" }, "kinderen": { "type": "array", @@ -297,15 +338,18 @@ "properties": { "burgerservicenummer": { "type": "string", - "description": "Child's BSN" + "description": "Child's BSN", + "title": "Child BSN" }, "naam": { "type": "string", - "description": "Child's full name" + "description": "Child's full name", + "title": "Child Name" } } }, - "facetable": false + "facetable": false, + "title": "Children" } }, "hardValidation": false, @@ -1696,4 +1740,4 @@ } ] } -} \ No newline at end of file +} diff --git a/lib/Settings/credential-providers.json b/lib/Settings/credential-providers.json new file mode 100644 index 0000000000..9fedd56c44 --- /dev/null +++ b/lib/Settings/credential-providers.json @@ -0,0 +1,316 @@ +{ + "$comment": "Credential-broker provider catalogue — a RUNTIME-IMMUTABLE, read-only file shipped with OpenRegister. It defines the external providers the broker may call, host-locked, and the constrained-proxy allow-rules (permitted method + path-prefix patterns) that bound what any credential can ever do. These allow-rules are a security control: they MUST NOT be widenable at runtime via any API — new providers/rules ship in a reviewed release only (design.md D2, ADR-031 exception). The broker reads this file server-side; it is NOT an OpenRegister schema, NOT register-seeded objects, and has no create/update/delete endpoint. '{secret}' in an authScheme.template is substituted by the broker with the vault-held token at call time; the token itself never appears here. Doffin caveat: the baseUrl is Doffin's Azure-APIM-hosted Public API on its BETA host, and the GET /notices rule is carried from the spectr Norway connector contract (not live-verifiable without a real subscription key, 2026-07-06); if the path or host moves, the broker fails closed (403) and the fix is another reviewed release of this file.", + "$fleetComment": "2026-07-12 — the catalogue held only github/gitlab/doffin, and CredentialController::create() rejects an unknown provider. So Mollie, Stripe, Adyen, CCV, KVK, Twilio, MessageBird, CM.com and the LLM providers could not be brokered AT ALL — you could not even create a credential for them. Every app therefore kept custody of its own secrets, not out of carelessness but because compliance was impossible. This release adds the providers whose auth the broker can actually express: ONE request header carrying ONE secret (injectAuth() substitutes {secret} into a single header template — it cannot do a token exchange, mTLS, or a signing operation). Rules are scoped to the endpoints the fleet actually calls, taken from the adapters themselves — no root wildcards, no DELETE. STILL NOT BROKERABLE, each needing broker capability work rather than a catalogue entry: (1) BRP/HaalCentraal — OAuth2 client_credentials needs a token EXCHANGE (POST secret -> access_token -> call), which a static header template cannot do; (2) procest StUF — an mTLS client certificate rides the TLS handshake, not a header; (3) scholiq's tenant RSA key — needs a SIGN operation, it is never sent as a request header; (4) softwarecatalog's SMTP password and its SendGrid/Mailgun/Postmark/Mailjet keys — Symfony Mailer takes them in a transport DSN, and SMTP is not HTTP at all, so a constrained HTTP proxy cannot carry them (these move to Doriath custody with the app holding only a credentialRef). Self-hosted targets (xWiki, and OpenConnector's arbitrary Sources) cannot be host-locked from an immutable file at all — they need a per-install, admin-approved provider registration, which is a broker design change.", + "$injectOnlyComment": "2026-07-14 (v1.4.0) — the last paragraph above named the standing gap: OpenConnector's arbitrary/self-hosted Sources cannot be host-locked from an immutable file, so their secrets never had a compliant home and stayed embedded in the source schema (ocon#147). The `inject_only` providers below close that gap WITHOUT weakening the host-locked proxy. They carry NO baseUrl and NO allowRules, so the broker's request() proxy path REFUSES them (CredentialBrokerService::request() denies an inject_only provider — an unbounded host is exactly what must never be proxied). They are reachable ONLY through CredentialBrokerService::resolveInjectable(), which runs guards 1+2 (owner + allowedApps) and hands the raw secret to the SAME-INSTANCE calling app, which injects it itself against whatever host its own config names. The trade-off is deliberate and narrow: the secret leaves OR into the trusted in-process app (NOT zero-knowledge, unlike the SaaS proxy above), but it lives in Doriath and the app's schema holds only a credentialRef — which is the whole point (never store a secret in a schema; store a reference). The five entries mirror OpenConnector's Source `auth` schemes; the app decides how to inject per its own Source.auth, so the authScheme here is descriptive only. 2026-07-16 (v1.6.0) — `inject_only` is NO LONGER only the five generic-* entries: `anthropic-cli` (grouped with the other anthropic providers above, not here) is inject_only for a different reason. It is not an unbounded host — it is a CLI. A vendor CLI needs its token in the process ENVIRONMENT, so there is no outbound request to proxy and no header to substitute, and the constrained-proxy path cannot express it at any host. Read `inject_only` as 'the broker cannot bound this call, so it refuses to make it and the guarded resolveInjectable() path applies instead' — which covers BOTH an unbounded host and a non-HTTP consumer. When adding a provider, prefer a host-locked proxy entry; reach for inject_only only when the broker genuinely cannot make the call itself.", + "version": "1.6.0", + "providers": { + "mollie": { + "identifier": "mollie", + "title": "Mollie", + "baseUrl": "https://api.mollie.com", + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/v2/methods" }, + { "method": "POST", "pathPattern": "/v2/payments" }, + { "method": "GET", "pathPattern": "/v2/payments/*" }, + { "method": "POST", "pathPattern": "/v2/payments/*/refunds" } + ] + }, + "stripe": { + "identifier": "stripe", + "title": "Stripe", + "baseUrl": "https://api.stripe.com", + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/v1/balance" }, + { "method": "POST", "pathPattern": "/v1/payment_intents" }, + { "method": "GET", "pathPattern": "/v1/payment_intents/*" }, + { "method": "POST", "pathPattern": "/v1/payment_intents/*/capture" } + ] + }, + "adyen": { + "identifier": "adyen", + "title": "Adyen (live)", + "baseUrl": "https://checkout-live.adyen.com", + "authScheme": { + "header": "X-API-Key", + "template": "{secret}" + }, + "allowRules": [ + { "method": "POST", "pathPattern": "/v71/paymentMethods" }, + { "method": "POST", "pathPattern": "/v71/payments" }, + { "method": "GET", "pathPattern": "/v71/payments/*" }, + { "method": "POST", "pathPattern": "/v71/payments/*/captures" } + ] + }, + "adyen-test": { + "identifier": "adyen-test", + "title": "Adyen (test)", + "$comment": "A separate entry, not a flag: baseUrl is the host-lock, so live and test cannot share one. The credentials differ anyway.", + "baseUrl": "https://checkout-test.adyen.com", + "authScheme": { + "header": "X-API-Key", + "template": "{secret}" + }, + "allowRules": [ + { "method": "POST", "pathPattern": "/v71/paymentMethods" }, + { "method": "POST", "pathPattern": "/v71/payments" }, + { "method": "GET", "pathPattern": "/v71/payments/*" }, + { "method": "POST", "pathPattern": "/v71/payments/*/captures" } + ] + }, + "ccv": { + "identifier": "ccv", + "title": "CCV PSP (live)", + "baseUrl": "https://api.psp.ccv.eu", + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/api/v1/terminals" }, + { "method": "POST", "pathPattern": "/api/v1/payments" }, + { "method": "GET", "pathPattern": "/api/v1/payments/*" }, + { "method": "POST", "pathPattern": "/api/v1/refunds" } + ] + }, + "ccv-sandbox": { + "identifier": "ccv-sandbox", + "title": "CCV PSP (sandbox)", + "baseUrl": "https://api.psp.sandbox.ccv.eu", + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/api/v1/terminals" }, + { "method": "POST", "pathPattern": "/api/v1/payments" }, + { "method": "GET", "pathPattern": "/api/v1/payments/*" }, + { "method": "POST", "pathPattern": "/api/v1/refunds" } + ] + }, + "kvk": { + "identifier": "kvk", + "title": "KVK (Kamer van Koophandel)", + "baseUrl": "https://api.kvk.nl", + "authScheme": { + "header": "apikey", + "template": "{secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/api/v2/zoeken" }, + { "method": "GET", "pathPattern": "/api/v1/basisprofielen/*" }, + { "method": "GET", "pathPattern": "/api/v1/vestigingsprofielen/*" } + ] + }, + "twilio": { + "identifier": "twilio", + "title": "Twilio", + "$comment": "Twilio is HTTP Basic (accountSid:authToken). The secret stored in the vault is the base64 of that pair, so the single-header template can carry it.", + "baseUrl": "https://api.twilio.com", + "authScheme": { + "header": "Authorization", + "template": "Basic {secret}" + }, + "allowRules": [ + { "method": "POST", "pathPattern": "/2010-04-01/Accounts/*/Messages.json" }, + { "method": "GET", "pathPattern": "/2010-04-01/Accounts/*/Messages/*" } + ] + }, + "messagebird": { + "identifier": "messagebird", + "title": "MessageBird", + "baseUrl": "https://rest.messagebird.com", + "authScheme": { + "header": "Authorization", + "template": "AccessKey {secret}" + }, + "allowRules": [ + { "method": "POST", "pathPattern": "/messages" }, + { "method": "GET", "pathPattern": "/messages/*" } + ] + }, + "cmcom": { + "identifier": "cmcom", + "title": "CM.com", + "baseUrl": "https://gw.cmtelecom.com", + "authScheme": { + "header": "X-CM-PRODUCTTOKEN", + "template": "{secret}" + }, + "allowRules": [ + { "method": "POST", "pathPattern": "/v1.0/message" } + ] + }, + "openai": { + "identifier": "openai", + "title": "OpenAI", + "$comment": "Host-locked to the public API. An OpenAI-compatible endpoint on another host cannot be brokered through this entry — the broker fails closed (403), which is the intended behaviour for an immutable catalogue.", + "baseUrl": "https://api.openai.com", + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/v1/models" }, + { "method": "POST", "pathPattern": "/v1/chat/completions" }, + { "method": "POST", "pathPattern": "/v1/embeddings" } + ] + }, + "anthropic": { + "identifier": "anthropic", + "title": "Anthropic (Claude) — API key", + "$comment": "Host-locked to the public API. API-key auth: injected as the x-api-key header. Used by Hermiq's anthropic chat provider (authMode: api_key). An Anthropic-compatible endpoint on another host cannot be brokered through this entry — the broker fails closed (403).", + "baseUrl": "https://api.anthropic.com", + "authScheme": { + "header": "x-api-key", + "template": "{secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/v1/models" }, + { "method": "POST", "pathPattern": "/v1/messages" } + ] + }, + "anthropic-oauth": { + "identifier": "anthropic-oauth", + "title": "Anthropic (Claude Max) — OAuth subscription", + "$comment": "Host-locked to the public API. Claude Max/Pro subscription OAuth token: injected as the Authorization: Bearer header (Hermiq additionally sends the anthropic-beta: oauth-2025-04-20 header, which is not a secret and passes through). PERSONAL-scope only per the Anthropic Terms of Service — a Max subscription is for that individual's own use; never share it across users.", + "baseUrl": "https://api.anthropic.com", + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/v1/models" }, + { "method": "POST", "pathPattern": "/v1/messages" } + ] + }, + "anthropic-cli": { + "identifier": "anthropic-cli", + "title": "Anthropic (Claude Max) — CLI subscription", + "$comment": "inject_only: NO baseUrl, NO allowRules — request() denies it, exactly like the generic-* entries below. Resolved ONLY through resolveInjectable(), which runs guards 1+2 (owner + allowedApps) and hands the raw secret to the same-instance calling app. The secret is a Claude Max/Pro subscription OAuth token (from `claude setup-token`); Hermiq injects it into the hermiq-llm-runner ExApp's `claude` CLI process ENVIRONMENT as CLAUDE_CODE_OAUTH_TOKEN. It is inject_only because a CLI needs the token in its env: there is no outbound request for the broker to proxy and no header to substitute, so the constrained-proxy path cannot express this at all. The trade-off is the same deliberate one the generic-* entries make: the secret LEAVES OpenRegister into the trusted in-process app (NOT zero-knowledge), bounded by those two guards, with Doriath holding custody and the app's config holding only a credentialRef. PERSONAL SCOPE ONLY per the Anthropic Terms of Service — a Max/Pro subscription is for that individual's own use; this credential MUST be rejected at organisation scope and may serve only its owner. Prefer the host-locked `anthropic` proxy entry (pay-per-token API key) wherever it works: it is zero-knowledge and strictly stronger. Do NOT use this entry for the direct Messages API — Anthropic refuses a subscription OAuth token there (HTTP 429 rate_limit_error carrying no rate-limit counters, verified 2026-07-16); the official CLI is the ToS-sanctioned path for a subscription. The authScheme below is DESCRIPTIVE ONLY — the broker injects nothing for this provider.", + "inject_only": true, + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + } + }, + "fireworks": { + "identifier": "fireworks", + "title": "Fireworks AI", + "baseUrl": "https://api.fireworks.ai", + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/inference/v1/models" }, + { "method": "POST", "pathPattern": "/inference/v1/chat/completions" }, + { "method": "POST", "pathPattern": "/inference/v1/embeddings" } + ] + }, + "github": { + "identifier": "github", + "title": "GitHub", + "baseUrl": "https://api.github.com", + "authScheme": { + "header": "Authorization", + "template": "token {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/repos/*" }, + { "method": "GET", "pathPattern": "/user/repos" }, + { "method": "PUT", "pathPattern": "/repos/*/contents/*" }, + { "method": "POST", "pathPattern": "/repos/*/git/*" }, + { "method": "GET", "pathPattern": "/search/repositories" }, + { "method": "GET", "pathPattern": "/user" }, + { "method": "POST", "pathPattern": "/user/repos" }, + { "method": "POST", "pathPattern": "/orgs/*/repos" }, + { "method": "PATCH", "pathPattern": "/repos/*/git/refs/*" }, + { "method": "PUT", "pathPattern": "/repos/*/topics" }, + { "$why": "Opening a pull request. Added so OpenBuild's export-to-GitHub can run through the broker instead of taking custody of the user's Personal Access Token: it pushes a tree (blobs/trees/commits/refs — already allowed) and then opens a PR, which was the ONE call the rules did not cover, and the only reason that path still held a raw PAT. Strictly less dangerous than the repo-write grants already present above (PUT /repos/*/contents/*, POST /repos/*/git/*), and it retires an app-held credential.", "method": "POST", "pathPattern": "/repos/*/pulls" } + ] + }, + "gitlab": { + "identifier": "gitlab", + "title": "GitLab", + "baseUrl": "https://gitlab.com/api/v4", + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/projects/*" }, + { "method": "POST", "pathPattern": "/projects/*/repository/commits" } + ] + }, + "doffin": { + "identifier": "doffin", + "title": "Doffin (Norway)", + "baseUrl": "https://betaapi.doffin.no/public/v2", + "authScheme": { + "header": "Ocp-Apim-Subscription-Key", + "template": "{secret}" + }, + "allowRules": [ + { "method": "GET", "pathPattern": "/notices" } + ] + }, + "generic-apikey": { + "identifier": "generic-apikey", + "title": "Generic API key (app-injected)", + "$comment": "inject_only: no baseUrl, no allowRules. The broker will NOT proxy this — request() denies it. It is resolved app-side via resolveInjectable(): the calling app reads the raw key and injects it into its own request header against its own host. The secret is the bare API key.", + "inject_only": true, + "authScheme": { + "header": "Authorization", + "template": "{secret}" + } + }, + "generic-bearer": { + "identifier": "generic-bearer", + "title": "Generic bearer token (app-injected)", + "$comment": "inject_only. The secret is the bare token; the app injects `Authorization: Bearer {secret}` (or the equivalent for its Source.auth) against its own host.", + "inject_only": true, + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + } + }, + "generic-basic": { + "identifier": "generic-basic", + "title": "Generic HTTP Basic password (app-injected)", + "$comment": "inject_only. The secret is the PASSWORD only; the username is non-sensitive and stays readable on the app's own config. The app combines them into `Authorization: Basic base64(user:secret)`.", + "inject_only": true, + "authScheme": { + "header": "Authorization", + "template": "Basic {secret}" + } + }, + "generic-oauth2": { + "identifier": "generic-oauth2", + "title": "Generic OAuth2 client secret (app-injected)", + "$comment": "inject_only. The secret is the OAuth2 client_secret; the non-sensitive client_id, token URL and scope stay on the app's own config. The app performs the token exchange itself (the broker cannot — a static header template cannot do a token exchange).", + "inject_only": true, + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + } + }, + "generic-jwt": { + "identifier": "generic-jwt", + "title": "Generic JWT signing secret (app-injected)", + "$comment": "inject_only. The secret is the JWT signing secret; the non-sensitive claims and algorithm stay on the app's own config. The app generates and signs the JWT itself (the broker cannot sign).", + "inject_only": true, + "authScheme": { + "header": "Authorization", + "template": "Bearer {secret}" + } + } + } +} diff --git a/lib/Settings/credential_broker_register.json b/lib/Settings/credential_broker_register.json new file mode 100644 index 0000000000..ec4b44285e --- /dev/null +++ b/lib/Settings/credential_broker_register.json @@ -0,0 +1,130 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Credential Broker", + "description": "Owner- and organisation-scoped credential metadata for the OpenRegister credential broker. Each object carries only a handle to a stored secret (name, provider, owner, scope, organisation, allowedApps) — never the secret value itself, which lives in the Nextcloud credential vault (ICredentialsManager) keyed by the object UUID (under the owning user for personal credentials, under the reserved system identity for organisation credentials). Materialised into OpenRegister by the credential-broker-service Repair step (OR does not self-import its own register JSON at boot — ADR-037).", + "version": "1.1.0" + }, + "x-openregister": { + "type": "core", + "app": "openregister", + "openregister": "^v0.2.10", + "description": "Foundational credential-metadata register shipped by OpenRegister for the credential broker (credential-broker chain). The provider catalogue and the outbound-broker code live outside this descriptor (lib/Settings/credential-providers.json + the credential-broker-service phase)." + }, + "paths": {}, + "components": { + "registers": { + "credential-broker": { + "slug": "credential-broker", + "title": "Credential Broker", + "version": "1.1.0", + "description": "Owner- and organisation-scoped credential metadata. One object per stored credential; the secret is NOT here — it lives in the Nextcloud vault keyed by the object UUID (owning user for personal, reserved system identity for organisation).", + "published": "2026-01-01T00:00:00+00:00", + "schemas": [ + "brokeredcredential" + ], + "folder": "Open Registers/Credential Broker" + } + }, + "schemas": { + "brokeredcredential": { + "slug": "brokeredcredential", + "title": "Credential", + "version": "1.1.0", + "published": "2026-01-01T00:00:00+00:00", + "summary": "Owner- and organisation-scoped metadata for a brokered credential — a handle, never the secret.", + "description": "Metadata for a secret the credential broker holds on a user's or organisation's behalf. Carries a name, the catalogue provider it targets, the owning user, the scope (personal|organisation), the owning organisation UUID for organisation credentials, and the app ids permitted to use it. MUST NOT contain the secret value: the token is stored in the Nextcloud credential vault (ICredentialsManager), encrypted at rest, keyed by this object's UUID — under the owning user for personal credentials and under a reserved system identity for organisation credentials. No property here is secret-bearing, so the metadata is safe to list, export, audit, and query.", + "required": [ + "name", + "provider" + ], + "properties": { + "name": { + "type": "string", + "description": "Human-readable name the owner gives the credential (e.g. \"GitHub publisher\").", + "maxLength": 255, + "facetable": true, + "title": "Name" + }, + "provider": { + "type": "string", + "description": "Catalogue provider identifier the credential targets (e.g. \"github\", \"gitlab\") — a key in the read-only lib/Settings/credential-providers.json catalogue.", + "maxLength": 64, + "facetable": true, + "title": "Provider" + }, + "owner": { + "type": "string", + "description": "The owning user (OR-resolved owner). For a personal credential this is the access-control key: only the owner may read, use, or manage it, and the paired secret is stored under this user in the Nextcloud vault. For an organisation credential this records the provisioning administrator for attribution ONLY — access control is membership of `organisation`, never this field.", + "maxLength": 255, + "facetable": true, + "title": "Owner" + }, + "scope": { + "type": "string", + "enum": [ + "personal", + "organisation" + ], + "default": "personal", + "description": "Access-control scope. `personal` (the default when absent): owner-scoped, the secret lives under the owning user in the Nextcloud vault and only the owner may use or manage it. `organisation`: shared by an organisation, the secret lives under a reserved system identity keyed by this object's UUID and any member of `organisation` may drive a broker call through an allowed app; only an organisation administrator (or a Nextcloud admin) may create, update, or delete it.", + "facetable": true, + "title": "Scope" + }, + "organisation": { + "type": "string", + "description": "The owning OpenRegister organisation UUID. REQUIRED when `scope` is `organisation` (membership of this organisation is the access-control key); MUST be absent/ignored for a personal credential, where it is never an access key.", + "maxLength": 255, + "facetable": true, + "title": "Organisation" + }, + "allowedApps": { + "type": "array", + "description": "App ids the owner has permitted to use this credential via the broker (the per-app allow-list the broker enforces).", + "items": { + "type": "string", + "maxLength": 64 + }, + "title": "Allowed Apps" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "When the credential was created.", + "title": "Created At" + } + } + } + }, + "objects": [ + { + "@self": { + "register": "credential-broker", + "schema": "brokeredcredential", + "slug": "gemeente-example-github-publish" + }, + "name": "Gemeente Example — GitHub publisher", + "provider": "github", + "owner": "00000000-0000-0000-0000-000000000000", + "allowedApps": [ + "hermiq" + ], + "createdAt": "2026-01-01T00:00:00+00:00" + }, + { + "@self": { + "register": "credential-broker", + "schema": "brokeredcredential", + "slug": "reisbureau-example-gitlab-discovery" + }, + "name": "Reisbureau Example — GitLab discovery", + "provider": "gitlab", + "owner": "00000000-0000-0000-0000-000000000000", + "allowedApps": [ + "openbuild" + ], + "createdAt": "2026-01-01T00:00:00+00:00" + } + ] + } +} diff --git a/lib/Settings/data_subject_request_register.json b/lib/Settings/data_subject_request_register.json new file mode 100644 index 0000000000..760b33a9f1 --- /dev/null +++ b/lib/Settings/data_subject_request_register.json @@ -0,0 +1,802 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "GDPR Data-Subject Requests", + "description": "Generic, jurisdiction-neutral GDPR / AVG data-subject-request register. Captures access (art-15), rectification (art-16), erasure (art-17), restriction (art-18), portability (art-20), and objection (art-21) requests, with the EU art-12 legal deadline and a status lifecycle. Consuming apps (e.g. pipelinq) project locale-specific overlays (BSN/BRP, AP complaint reference, FG/DPO, citizen correspondence) on top of this generic model.", + "version": "1.0.0" + }, + "x-openregister": { + "type": "core", + "app": "openregister", + "openregister": "^v0.2.10", + "description": "Foundational GDPR data-subject-rights register shipped by OpenRegister for fleet-wide reuse." + }, + "paths": {}, + "components": { + "registers": { + "data-subject-requests": { + "slug": "data-subject-requests", + "title": "Data-Subject Requests", + "version": "1.0.0", + "description": "Generic GDPR / AVG data-subject-request register (art-15/16/17/18/20/21) with the EU art-12 legal deadline and a status lifecycle.", + "published": "2026-01-01T00:00:00+00:00", + "schemas": [ + "dataSubjectRequest" + ], + "folder": "Open Registers/Data-Subject Requests" + } + }, + "schemas": { + "dataSubjectRequest": { + "slug": "dataSubjectRequest", + "title": "Data-Subject Request", + "version": "1.2.0", + "published": "2026-01-01T00:00:00+00:00", + "summary": "A single GDPR / AVG data-subject-rights request tracked as a stateful case.", + "description": "Jurisdiction-neutral model for a data-subject-rights request tracked as a stateful case: who the subject is, which right is exercised, the request's status in its lifecycle, the EU art-12(3) legal deadline (one month from receipt, extendable once by two months), the assigned handler, case timestamps, retention window, denial workflow, and evidence/redaction sub-collections.", + "required": [ + "subjectId", + "type", + "status", + "receivedAt" + ], + "x-openregister-references": { + "pack": { + "mode": "lookup", + "schema": "dsarPolicyPack", + "register": "dsar-policy-packs", + "filters": { + "jurisdiction": "@self.jurisdiction" + } + } + }, + "x-openregister-lifecycle": { + "field": "status", + "initial": "received", + "final": [ + "fulfilled", + "refused", + "closed" + ], + "transitions": { + "startVerifying": { + "from": [ + "received", + "verifying", + "in-progress" + ], + "to": "verifying", + "description": "Begin verifying the requester's identity / eligibility." + }, + "startProgress": { + "from": [ + "received", + "verifying", + "in-progress" + ], + "to": "in-progress", + "description": "Begin fulfilling the request (discovery, export, rectify, erase)." + }, + "fulfil": { + "from": [ + "received", + "verifying", + "in-progress" + ], + "to": "fulfilled", + "description": "Mark the request fulfilled (the right was honoured)." + }, + "refuse": { + "from": [ + "received", + "verifying", + "in-progress" + ], + "to": "refused", + "description": "Refuse the request (a lawful exemption applies)." + }, + "close": { + "from": [ + "received", + "verifying", + "in-progress" + ], + "to": "closed", + "description": "Close the request without a fulfil/refuse outcome (e.g. withdrawn)." + }, + "assign": { + "from": [ + "received", + "verifying", + "in-progress", + "evidence-collection", + "denial-drafted" + ], + "to": "verifying", + "description": "Assign (or reassign) the case to a handler and move it into active verification." + }, + "collectEvidence": { + "from": [ + "verifying", + "in-progress", + "evidence-collection" + ], + "to": "evidence-collection", + "description": "Enter evidence-collection: harvest source data for the case (harvesting itself is delivered by dsar-case-engine)." + }, + "draftDenial": { + "from": [ + "verifying", + "in-progress", + "evidence-collection", + "denial-drafted" + ], + "to": "denial-drafted", + "description": "Record a proposed denialGround as a draft denial. Does NOT require a regulatorReference (the gate applies only at finalise)." + }, + "finaliseDenial": { + "from": [ + "denial-drafted" + ], + "to": "refused", + "description": "Finalise the denial and reach the refused final state. Guarded: refused unless a regulatorReference is present on the case.", + "requires": "OCA\\OpenRegister\\Service\\Gdpr\\Lifecycle\\DenialFinaliseGuard" + }, + "redact": { + "from": [ + "in-progress", + "evidence-collection" + ], + "to": "in-progress", + "description": "Record a redaction (field before/after + ground) on the case. The redaction write path is delivered by dsar-case-engine." + }, + "bundle": { + "from": [ + "in-progress", + "evidence-collection" + ], + "to": "fulfilled", + "description": "Assemble and sign the export bundle and reach the fulfilled final state (bundle generation/signing is delivered by dsar-case-engine)." + }, + "retain": { + "from": [ + "fulfilled", + "refused", + "closed" + ], + "to": "closed", + "description": "Move a finalised case into its configured retention window (the retention sweep is delivered by dsar-case-engine)." + } + } + }, + "x-openregister-calculations": { + "daysRemaining": { + "type": "number", + "materialise": true, + "title": "Days Remaining", + "description": "Whole days between now and the effective deadline (extendedUntil when set, otherwise dueAt). Negative once the deadline has passed. Reuses DataSubjectDeadline art-12 semantics (ADR-011).", + "expression": { + "diffDays": [ + { + "coalesce": [ + { + "prop": "extendedUntil" + }, + { + "prop": "dueAt" + } + ] + }, + { + "now": [] + } + ] + } + }, + "isOverdue": { + "type": "boolean", + "materialise": true, + "title": "Is Overdue", + "description": "True when the effective deadline (extendedUntil else dueAt) has passed, i.e. daysRemaining is negative. Reuses DataSubjectDeadline::isOverdue semantics (ADR-011).", + "expression": { + "lt": [ + { + "prop": "daysRemaining" + }, + 0 + ] + } + }, + "escalationTier": { + "type": "string", + "materialise": true, + "title": "Escalation Tier", + "description": "Deadline-tracking tier (on-track, reminder, escalation, breached) derived from daysRemaining. The tier boundaries are NOT hard-coded: they resolve from the active dsarPolicyPack's escalationTiers via @ref.pack (index 0 = reminder, 1 = escalation, 2 = breach). The tier activates when the days-past-deadline value (-daysRemaining) reaches a tier's offsetDays; breach when daysRemaining < 0. When no pack resolves (@ref.pack is null) every boundary comparison yields false and the case stays on-track (fail-safe, no false escalation) — the seeded default pack ensures a pack always resolves.", + "expression": { + "if": [ + { + "gt": [ + { + "-": [ + { + "prop": "daysRemaining" + } + ] + }, + { + "prop": "@ref.pack.escalationTiers.2.offsetDays" + } + ] + }, + "breached", + { + "if": [ + { + "gte": [ + { + "-": [ + { + "prop": "daysRemaining" + } + ] + }, + { + "prop": "@ref.pack.escalationTiers.1.offsetDays" + } + ] + }, + "escalation", + { + "if": [ + { + "gte": [ + { + "-": [ + { + "prop": "daysRemaining" + } + ] + }, + { + "prop": "@ref.pack.escalationTiers.0.offsetDays" + } + ] + }, + "reminder", + "on-track" + ] + } + ] + } + ] + } + }, + "breachedAt": { + "type": "string", + "materialise": true, + "title": "Breached At", + "description": "Write-once breach stamp (dsar-escalation-and-dpia): the instant the case's escalationTier first crossed into 'breached'. Declared as a calculation (ADR-031: the temporal sweep job carries no DSAR-specific branch) evaluated AFTER escalationTier in declaration order: while the tier is 'breached' the value coalesces to its existing stamp (write-once) or the evaluation clock on the first crossing; otherwise the existing value (null until a breach) is kept unchanged.", + "expression": { + "if": [ + { + "eq": [ + { + "prop": "escalationTier" + }, + "breached" + ] + }, + { + "coalesce": [ + { + "prop": "breachedAt" + }, + { + "formatDate": [ + { + "now": [] + }, + "c" + ] + } + ] + }, + { + "prop": "breachedAt" + } + ] + } + } + }, + "x-openregister-aggregations": { + "openCaseCount": { + "metric": "count", + "schema": "dataSubjectRequest", + "filter": { + "status": [ + "received", + "verifying", + "in-progress", + "evidence-collection", + "denial-drafted" + ] + } + }, + "overdueCaseCount": { + "metric": "count", + "schema": "dataSubjectRequest", + "filter": { + "status": [ + "received", + "verifying", + "in-progress", + "evidence-collection", + "denial-drafted" + ], + "isOverdue": true + } + }, + "breachedCaseCount": { + "metric": "count", + "schema": "dataSubjectRequest", + "filter": { + "escalationTier": "breached" + } + } + }, + "x-openregister-notifications": { + "deadlineAdvanceReminder": { + "trigger": { + "type": "calculatedChange", + "field": "escalationTier", + "condition": { + "eq": "reminder" + }, + "previously": { + "ne": "reminder" + } + }, + "enabled": true, + "originApp": "openregister", + "channels": [ + "nc-notification" + ], + "recipients": [ + { + "kind": "field", + "field": "handler" + } + ], + "subject": { + "nl": "Herinnering: DSAR-deadline nadert voor {{subjectId}}", + "en": "Reminder: DSAR deadline approaching for {{subjectId}}" + }, + "message": { + "nl": "De wettelijke termijn voor dit verzoek nadert. Nog {{daysRemaining}} dagen tot de deadline.", + "en": "The legal deadline for this request is approaching. {{daysRemaining}} days remaining." + } + }, + "deadlineEscalation": { + "trigger": { + "type": "calculatedChange", + "field": "escalationTier", + "condition": { + "eq": "escalation" + }, + "previously": { + "ne": "escalation" + } + }, + "enabled": true, + "originApp": "openregister", + "channels": [ + "nc-notification" + ], + "recipients": [ + { + "kind": "field", + "field": "handler" + } + ], + "subject": { + "nl": "Escalatie: DSAR-deadline kritiek voor {{subjectId}}", + "en": "Escalation: DSAR deadline critical for {{subjectId}}" + }, + "message": { + "nl": "Dit verzoek nadert kritiek zijn wettelijke termijn. Nog {{daysRemaining}} dagen.", + "en": "This request is critically close to its legal deadline. {{daysRemaining}} days remaining." + } + }, + "deadlineBreach": { + "trigger": { + "type": "calculatedChange", + "field": "escalationTier", + "condition": { + "eq": "breached" + }, + "previously": { + "ne": "breached" + } + }, + "enabled": true, + "originApp": "openregister", + "channels": [ + "nc-notification" + ], + "recipients": [ + { + "kind": "field", + "field": "handler" + }, + { + "kind": "expression", + "resolver": "OCA\\OpenRegister\\Service\\Gdpr\\PrivacyOfficerRecipientResolver" + } + ], + "subject": { + "nl": "Overschreden: DSAR-deadline verlopen voor {{subjectId}}", + "en": "Breached: DSAR deadline passed for {{subjectId}}" + }, + "message": { + "nl": "De wettelijke termijn voor dit verzoek is verstreken.", + "en": "The legal deadline for this request has passed." + } + }, + "dpiaFlagged": { + "trigger": { + "type": "calculatedChange", + "field": "dpiaRequired", + "condition": { + "eq": true + }, + "previously": { + "ne": true + } + }, + "enabled": true, + "originApp": "openregister", + "channels": [ + "nc-notification" + ], + "recipients": [ + { + "kind": "expression", + "resolver": "OCA\\OpenRegister\\Service\\Gdpr\\PrivacyOfficerRecipientResolver" + } + ], + "subject": { + "nl": "DPIA vereist: patroon gedetecteerd rond {{subjectId}}", + "en": "DPIA required: pattern detected around {{subjectId}}" + }, + "message": { + "nl": "Voor dit verzoek is een Data Protection Impact Assessment (DPIA) gemarkeerd — automatisch patroon (art. 35 AVG) of handmatige markering.", + "en": "This request was flagged as requiring a Data Protection Impact Assessment (DPIA) — automatic pattern detection (GDPR art. 35) or a manual flag." + } + } + }, + "x-openregister-seeds": [ + { + "@self": { + "register": "data-subject-requests", + "schema": "dataSubjectRequest", + "slug": "case-municipality-access" + }, + "subjectId": "j.jansen@example.org", + "subjectType": "email", + "type": "access", + "status": "in-progress", + "handler": "handler-gemeente", + "receivedAt": "2026-06-01T09:00:00+00:00", + "dueAt": "2026-07-01T09:00:00+00:00", + "closedAt": null, + "dpiaRequired": false, + "retentionWindow": "standard", + "retainUntil": "2027-07-01T00:00:00+00:00", + "evidence": [ + { + "sourceId": "or-objects", + "contentHash": "sha256:PLACEHOLDER_HASH", + "status": "collected" + } + ], + "notes": "Inwoner vraagt inzage in geregistreerde persoonsgegevens." + }, + { + "@self": { + "register": "data-subject-requests", + "schema": "dataSubjectRequest", + "slug": "case-consultancy-erasure" + }, + "subjectId": "contact@example-consultancy.test", + "subjectType": "email", + "type": "erasure", + "status": "verifying", + "handler": "handler-privacy", + "receivedAt": "2026-05-20T12:00:00+00:00", + "dueAt": "2026-06-20T12:00:00+00:00", + "extendedUntil": "2026-08-20T12:00:00+00:00", + "extensionReason": "Complex cross-register discovery; single art-12(3) extension applied.", + "dpiaRequired": true, + "retentionWindow": "extended" + }, + { + "@self": { + "register": "data-subject-requests", + "schema": "dataSubjectRequest", + "slug": "case-travel-objection-denied" + }, + "subjectId": "traveler@example-travel.test", + "subjectType": "email", + "type": "objection", + "status": "refused", + "handler": "handler-dpo", + "receivedAt": "2026-04-10T08:30:00+00:00", + "dueAt": "2026-05-10T08:30:00+00:00", + "closedAt": "2026-05-08T16:00:00+00:00", + "denialGround": "overriding-legitimate-grounds", + "regulatorReference": "REG-REF-PLACEHOLDER-0001", + "outcome": "Objection refused; compelling legitimate grounds documented; regulator reference recorded.", + "redactions": [ + { + "field": "notes", + "before": "[redacted-source]", + "after": "[erased]", + "ground": "third-party-data" + } + ] + } + ], + "properties": { + "subjectId": { + "type": "string", + "description": "Identifier of the data subject (e.g. email or another identifier value indexed in the PII index).", + "maxLength": 255, + "facetable": true, + "title": "Subject Identifier" + }, + "subjectType": { + "type": "string", + "description": "Optional type hint for the subject identifier (e.g. 'email', 'phone'). Maps to a GdprEntity type when discovering data.", + "maxLength": 64, + "facetable": true, + "title": "Subject Type" + }, + "jurisdiction": { + "type": "string", + "description": "Jurisdiction / tenant key selecting which dsarPolicyPack supplies this case's escalation-tier thresholds, denial-ground wording, and retention durations (resolved via the pack cross-object reference). Defaults to 'default', which resolves the jurisdiction-neutral default pack. Full tenant-scoped/override selection semantics are delivered by the successor dsar-integration-seams / dsar-case-ui changes.", + "maxLength": 128, + "default": "default", + "facetable": true, + "title": "Jurisdiction" + }, + "type": { + "type": "string", + "description": "Which GDPR right is exercised.", + "enum": [ + "access", + "rectification", + "erasure", + "restriction", + "portability", + "objection" + ], + "facetable": true, + "title": "Request Type" + }, + "status": { + "type": "string", + "description": "Lifecycle state of the request (governed by x-openregister-lifecycle).", + "enum": [ + "received", + "verifying", + "in-progress", + "evidence-collection", + "denial-drafted", + "fulfilled", + "refused", + "closed" + ], + "default": "received", + "facetable": true, + "title": "Status" + }, + "receivedAt": { + "type": "string", + "format": "date-time", + "description": "When the request was received. Anchors the EU art-12 legal deadline.", + "title": "Received At" + }, + "dueAt": { + "type": "string", + "format": "date-time", + "description": "EU art-12(3) base legal deadline: receivedAt + 1 month.", + "title": "Legal Due Date" + }, + "extendedUntil": { + "type": "string", + "format": "date-time", + "description": "Extended legal deadline when a single +2-month extension was granted (art-12(3)). Empty when not extended.", + "title": "Extended Deadline" + }, + "extensionReason": { + "type": "string", + "description": "Justification recorded when the deadline was extended.", + "maxLength": 2000, + "title": "Extension Reason" + }, + "outcome": { + "type": "string", + "description": "Generic outcome summary once the request reaches a final state.", + "maxLength": 2000, + "title": "Outcome" + }, + "notes": { + "type": "string", + "description": "Free-text handler notes (generic; no jurisdiction policy).", + "maxLength": 4000, + "title": "Handler Notes" + }, + "handler": { + "type": "string", + "description": "Identifier (Nextcloud uid) of the handler the case is assigned to. Used as the recipient for advance-reminder, escalation, and breach notifications.", + "maxLength": 255, + "facetable": true, + "title": "Handler" + }, + "closedAt": { + "type": "string", + "format": "date-time", + "description": "When the case was closed (reached a final state). Empty while the case is still open.", + "title": "Closed At" + }, + "dpiaRequired": { + "type": "boolean", + "description": "Complexity flag: whether this case requires a Data Protection Impact Assessment (DPIA) before it can be fulfilled. Set manually by a handler or automatically by the daily DPIA pattern-detection job (dsar-escalation-and-dpia) when the active policy pack's dpiaDetection threshold is reached; the job never clears a flag (one-way ratchet).", + "default": false, + "facetable": true, + "title": "DPIA Required" + }, + "breachedAt": { + "type": "string", + "format": "date-time", + "description": "Write-once stamp of the instant the case's escalationTier first crossed into 'breached'. Populated declaratively by the breachedAt calculation on the tier write that crosses the boundary; empty while the deadline has never been breached. Read by the regulator dossier and breach reporting.", + "title": "Breached At" + }, + "daysRemaining": { + "type": "number", + "description": "Engine-materialised value of the daysRemaining calculation (whole days until the effective deadline; negative once passed). Declared as a property so the materialised value persists to a magic-table column — calculation outputs without a backing property are dropped at persist time. Do not edit by hand; the save path and the temporal sweep recompute it.", + "facetable": true, + "title": "Days Remaining" + }, + "isOverdue": { + "type": "boolean", + "description": "Engine-materialised value of the isOverdue calculation (true when the effective deadline has passed). Declared as a property so the materialised value persists to a magic-table column and the overdueCaseCount aggregation can filter on it. Do not edit by hand.", + "facetable": true, + "title": "Is Overdue" + }, + "escalationTier": { + "type": "string", + "description": "Engine-materialised value of the escalationTier calculation (on-track, reminder, escalation, breached — boundaries from the active dsarPolicyPack). Declared as a property so the materialised value persists to a magic-table column, the breachedCaseCount aggregation can filter on it, and the calculatedChange notification rules see old/new values. Do not edit by hand; the save path and the temporal sweep recompute it.", + "enum": [ + "on-track", + "reminder", + "escalation", + "breached" + ], + "facetable": true, + "title": "Escalation Tier" + }, + "retentionWindow": { + "type": "string", + "description": "Configured retention window key selecting how long the finalised case dossier is retained before the retention sweep (delivered by dsar-case-engine) may purge it.", + "maxLength": 64, + "facetable": true, + "title": "Retention Window" + }, + "retainUntil": { + "type": "string", + "format": "date-time", + "description": "Computed date until which the finalised case dossier must be retained. Set from receivedAt/closedAt plus the retentionWindow; the retention sweep may purge only after this date.", + "title": "Retain Until" + }, + "purgedAt": { + "type": "string", + "format": "date-time", + "description": "When the case dossier was purged by the retention sweep. Empty until the case has been purged.", + "title": "Purged At" + }, + "denialGround": { + "type": "string", + "description": "Generic ground key recorded when a case is denied. Jurisdiction-specific statutory wording is Phase-2 policy-pack data and is NOT baked into this schema.", + "enum": [ + "manifestly-unfounded", + "excessive-request", + "identity-unverified", + "legal-exemption", + "third-party-rights", + "overriding-legitimate-grounds", + "legal-claims", + "public-interest", + "professional-secrecy", + "not-applicable" + ], + "facetable": true, + "title": "Denial Ground" + }, + "regulatorReference": { + "type": "string", + "description": "Reference recorded for the supervisory authority / regulator when a denial is finalised. Required by the finaliseDenial lifecycle guard before the case may reach the refused final state.", + "maxLength": 255, + "title": "Regulator Reference" + }, + "evidence": { + "type": "array", + "description": "Evidence items collected for the case. The item shape is declared here; the harvesting/dedup behaviour is delivered by dsar-case-engine.", + "title": "Evidence", + "items": { + "type": "object", + "title": "Evidence Item", + "description": "A single piece of evidence collected for the case, keyed by content hash for downstream deduplication.", + "properties": { + "sourceId": { + "type": "string", + "description": "Identifier of the source system or provider the evidence was collected from.", + "maxLength": 255, + "title": "Source Identifier" + }, + "contentHash": { + "type": "string", + "description": "Content hash of the evidence item (e.g. sha256:...), used downstream to deduplicate identical evidence across sources.", + "maxLength": 255, + "title": "Content Hash" + }, + "status": { + "type": "string", + "description": "Per-item collection status (e.g. pending, collected, failed).", + "maxLength": 64, + "title": "Item Status" + } + } + } + }, + "redactions": { + "type": "array", + "description": "Redaction entries recorded on the case. Each entry captures a before/after and the ground, distinct from the erase-time pseudonymise already performed by DataSubjectRequestService. The item shape is declared here; the redaction write path is delivered by dsar-case-engine.", + "title": "Redactions", + "items": { + "type": "object", + "title": "Redaction Entry", + "description": "A single redaction applied to a field of the case, with a before/after audit and the ground it was applied under.", + "properties": { + "field": { + "type": "string", + "description": "Name of the field that was redacted.", + "maxLength": 255, + "title": "Redacted Field" + }, + "before": { + "type": "string", + "description": "Value of the field before redaction (audit of the original content).", + "maxLength": 4000, + "title": "Value Before" + }, + "after": { + "type": "string", + "description": "Value of the field after redaction.", + "maxLength": 4000, + "title": "Value After" + }, + "ground": { + "type": "string", + "description": "Generic ground key under which the redaction was applied (e.g. third-party-data).", + "maxLength": 128, + "title": "Redaction Ground" + } + } + } + } + } + } + } + } +} diff --git a/lib/Settings/dsar_policy_pack_register.json b/lib/Settings/dsar_policy_pack_register.json new file mode 100644 index 0000000000..362012070d --- /dev/null +++ b/lib/Settings/dsar_policy_pack_register.json @@ -0,0 +1,381 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "AVG/DSAR Policy Packs", + "description": "Per-jurisdiction / per-tenant AVG-DSAR policy pack register (ADR-047). A pack is a declarative configuration object (ADR-031) that supplies the jurisdiction-specific VALUES the generic data-subject-request workflow reads at runtime: deadline and extension durations, escalation-tier thresholds, denial-ground wording (generic key -> label + statutory citation), named retention windows, intake channels, the DPO/FG/handler role mapping, letter/notification template references, and the two integration-seam provider selectors (identity-verify, regulator-escalate). Jurisdiction is data, not code: the Phase-1 dataSubjectRequest schema binds to the active pack via a declarative cross-object reference and holds no hard-coded jurisdiction values. Real NL policy values (BSN/BRP/RvIG, AP) live in the Phase-3 pipelinq consumer, NOT in OpenRegister core.", + "version": "1.0.0" + }, + "x-openregister": { + "type": "core", + "app": "openregister", + "openregister": "^v0.2.10", + "description": "Foundational AVG/DSAR policy-pack register shipped by OpenRegister; supplies the jurisdiction values the generic data-subject-request workflow (data-subject-requests register) resolves from.", + "seedData": { + "description": "Seeded policy packs (imported by the ImportDsarRegisters repair step through the ConfigurationService seedData path — the per-schema x-openregister-seeds blocks below are the historical declaration and are NOT processed by importFromApp). The default pack guarantees a pack always resolves (fail-safe convention: escalation tiers, DPIA detection defaults); role/group values are placeholders an operator binds.", + "objects": { + "dsarPolicyPack": [ + { + "slug": "pack-default", + "name": "Default AVG/DSAR policy pack", + "jurisdiction": "default", + "deadlineDurationDays": 30, + "extensionDurationDays": 60, + "escalationTiers": [ + { "tier": "reminder", "offsetDays": -7 }, + { "tier": "escalation", "offsetDays": -2 }, + { "tier": "breach", "offsetDays": 0 } + ], + "intakeChannels": ["web-form", "email", "post"], + "roleMapping": { "dpo": "", "handler": "" }, + "templates": { "acknowledgement": "template:ack-default", "denial": "template:denial-default" }, + "denialGrounds": [ + { "key": "manifestly-unfounded", "label": "Manifestly unfounded or excessive request", "citation": "GDPR Art 12(5)" }, + { "key": "third-party-rights", "label": "Would adversely affect the rights of third parties", "citation": "GDPR Art 15(4)" }, + { "key": "overriding-legitimate-grounds", "label": "Compelling legitimate grounds override the objection", "citation": "GDPR Art 21(1)" } + ], + "retentionWindows": [ + { "key": "standard", "durationMonths": 12 }, + { "key": "extended", "durationMonths": 24 } + ], + "privacyOfficerGroup": "", + "dpiaDetection": { "threshold": 10, "windowDays": 30, "groupBy": ["type", "scope"] }, + "identityVerifyProvider": "or.default.identity-verify.null", + "regulatorEscalateProvider": "or.default.regulator-escalate.null" + }, + { + "slug": "pack-nl-example", + "name": "Voorbeeld NL AVG-beleidspakket (illustratief)", + "jurisdiction": "nl-example", + "deadlineDurationDays": 30, + "extensionDurationDays": 60, + "escalationTiers": [ + { "tier": "reminder", "offsetDays": -7 }, + { "tier": "escalation", "offsetDays": -3 }, + { "tier": "breach", "offsetDays": 0 } + ], + "intakeChannels": ["web-form", "email"], + "roleMapping": { "fg": "", "handler": "" }, + "templates": { "denial": "template:denial-nl-example" }, + "denialGrounds": [ + { "key": "manifestly-unfounded", "label": "Kennelijk ongegrond of buitensporig verzoek", "citation": "AVG art. 12 lid 5 (illustratief)" } + ], + "retentionWindows": [ + { "key": "boekhoudplicht", "durationMonths": 84 } + ], + "privacyOfficerGroup": "", + "identityVerifyProvider": "", + "regulatorEscalateProvider": "" + } + ] + } + } + }, + "paths": {}, + "components": { + "registers": { + "dsar-policy-packs": { + "slug": "dsar-policy-packs", + "title": "AVG/DSAR Policy Packs", + "version": "1.0.0", + "description": "Per-jurisdiction / per-tenant AVG-DSAR policy packs supplying deadline durations, escalation-tier thresholds, denial-ground wording, retention windows, intake channels, the DPO/FG role mapping, template references, and the two integration-seam provider selectors.", + "published": "2026-01-01T00:00:00+00:00", + "schemas": [ + "dsarPolicyPack" + ], + "folder": "Open Registers/AVG DSAR Policy Packs" + } + }, + "schemas": { + "dsarPolicyPack": { + "slug": "dsarPolicyPack", + "title": "AVG/DSAR Policy Pack", + "version": "1.1.0", + "published": "2026-01-01T00:00:00+00:00", + "summary": "A per-jurisdiction / per-tenant AVG-DSAR policy pack supplying the values the generic data-subject-request workflow resolves from.", + "description": "Declarative configuration object (ADR-031) holding the jurisdiction-specific AVG/DSAR policy. The generic dataSubjectRequest case resolves its escalation-tier thresholds, denial-ground wording, and retention durations from the ACTIVE pack (selected by the case's jurisdiction, falling back to the jurisdiction-neutral default pack). Every value here is data a steward may edit without a code change. The two seam selectors name which registered provider a jurisdiction binds for identity verification and regulator escalation; an unset or unknown selector is resolved fail-closed to the OR safe-default (refusing) provider by the successor dsar-integration-seams change.", + "required": [ + "jurisdiction" + ], + "properties": { + "name": { + "type": "string", + "description": "Human-readable name of this policy pack (e.g. 'Default AVG/DSAR policy pack').", + "maxLength": 255, + "facetable": true, + "title": "Pack Name" + }, + "jurisdiction": { + "type": "string", + "description": "Jurisdiction / tenant key that selects this pack for a case (e.g. 'default', 'nl-example'). The neutral fail-closed baseline uses 'default'; a dataSubjectRequest case resolves the pack whose jurisdiction matches the case's jurisdiction field, falling back to the default pack.", + "maxLength": 128, + "facetable": true, + "title": "Jurisdiction Key" + }, + "deadlineDurationDays": { + "type": "integer", + "description": "Base statutory response deadline in days measured from receipt (EU art-12(3): one month). Conservative neutral default is 30.", + "minimum": 1, + "title": "Deadline Duration (days)" + }, + "extensionDurationDays": { + "type": "integer", + "description": "Additional days granted by the single permitted deadline extension (EU art-12(3): up to two further months). Conservative neutral default is 60.", + "minimum": 1, + "title": "Extension Duration (days)" + }, + "escalationTiers": { + "type": "array", + "description": "Ordered deadline-tracking escalation tiers. ORDER IS SIGNIFICANT: index 0 = reminder, index 1 = escalation, index 2 = breach. The dataSubjectRequest escalationTier calculation reads each tier's offsetDays by this position, so a jurisdiction pack MUST keep the three tiers in this order.", + "title": "Escalation Tiers", + "items": { + "type": "object", + "title": "Escalation Tier", + "description": "One deadline-tracking tier: a name and the day offset relative to the deadline at which the tier becomes active.", + "properties": { + "tier": { + "type": "string", + "description": "Tier name (reminder, escalation, or breach). Used for human display; the case calculation resolves the boundary by array position.", + "maxLength": 64, + "title": "Tier Name" + }, + "offsetDays": { + "type": "integer", + "description": "Day offset relative to the deadline at which this tier activates: negative = before the deadline (e.g. -7 = seven days before), 0 = on the deadline. The case escalationTier calculation activates the tier when daysRemaining <= -offsetDays (breach when daysRemaining < 0).", + "title": "Offset Days" + } + } + } + }, + "intakeChannels": { + "type": "array", + "description": "Channels through which a data-subject request may be received in this jurisdiction (e.g. web-form, email, post).", + "title": "Intake Channels", + "items": { + "type": "string", + "title": "Intake Channel", + "description": "A single intake channel identifier.", + "maxLength": 64 + } + }, + "roleMapping": { + "type": "object", + "description": "Maps the abstract case roles to the jurisdiction's role identifiers (ADR-023). Placeholders () are used in seed data; a leaf app binds real Nextcloud group/role ids.", + "title": "Role Mapping", + "properties": { + "dpo": { + "type": "string", + "description": "Role identifier for the Data Protection Officer (used in jurisdictions with a DPO).", + "maxLength": 255, + "title": "DPO Role" + }, + "fg": { + "type": "string", + "description": "Role identifier for the Functionaris Gegevensbescherming (the NL-context equivalent of a DPO).", + "maxLength": 255, + "title": "FG Role" + }, + "handler": { + "type": "string", + "description": "Role identifier for the case handler who processes the request day to day.", + "maxLength": 255, + "title": "Handler Role" + } + } + }, + "templates": { + "type": "object", + "description": "References to the letter / notification templates this jurisdiction uses (template: references resolved by a document leaf per ADR-022; not inline content).", + "title": "Template References", + "properties": { + "acknowledgement": { + "type": "string", + "description": "Template reference for the request-acknowledgement letter sent on intake.", + "maxLength": 255, + "title": "Acknowledgement Template" + }, + "denial": { + "type": "string", + "description": "Template reference for the denial letter sent when a request is refused.", + "maxLength": 255, + "title": "Denial Template" + }, + "extension": { + "type": "string", + "description": "Template reference for the deadline-extension notice sent when the deadline is extended.", + "maxLength": 255, + "title": "Extension Template" + } + } + }, + "denialGrounds": { + "type": "array", + "description": "Maps each generic Phase-1 denial-ground key to this jurisdiction's human wording and statutory citation. The generic keys live on the dataSubjectRequest schema; the jurisdiction label + citation live here.", + "title": "Denial Grounds", + "items": { + "type": "object", + "title": "Denial Ground", + "description": "One denial-ground mapping: a generic key plus the jurisdiction label and statutory citation shown to handlers and data subjects.", + "properties": { + "key": { + "type": "string", + "description": "Generic denial-ground key matching a value of the dataSubjectRequest denialGround enum (e.g. manifestly-unfounded, third-party-rights).", + "maxLength": 128, + "title": "Ground Key" + }, + "label": { + "type": "string", + "description": "Jurisdiction-specific human-readable label for this denial ground.", + "maxLength": 512, + "title": "Ground Label" + }, + "citation": { + "type": "string", + "description": "Statutory citation supporting this denial ground (illustrative in the shipped example packs; authoritative wording is supplied by the leaf consumer).", + "maxLength": 512, + "title": "Statutory Citation" + } + } + } + }, + "retentionWindows": { + "type": "array", + "description": "Named retention windows this jurisdiction offers. Each maps a window key (selected by the case's retentionWindow field) to a duration; the consuming retention engine computes retainUntil from the finalisation date plus this duration.", + "title": "Retention Windows", + "items": { + "type": "object", + "title": "Retention Window", + "description": "One named retention window: a key selectable on a case and the duration the finalised dossier is retained.", + "properties": { + "key": { + "type": "string", + "description": "Retention-window key matching the dataSubjectRequest retentionWindow selector (e.g. standard, extended).", + "maxLength": 64, + "title": "Window Key" + }, + "durationMonths": { + "type": "integer", + "description": "Number of months a finalised case dossier is retained under this window before the retention sweep may purge it.", + "minimum": 1, + "title": "Duration (months)" + } + } + } + }, + "privacyOfficerGroup": { + "type": "string", + "description": "Nextcloud group id of this jurisdiction's privacy officer (DPO/FG) team. Resolved by the declared deadlineBreach and dpiaFlagged notification recipients (PrivacyOfficerRecipientResolver) so breach and DPIA visibility reach the officer without hard-coding a group in the case schema. Unset, placeholder (<...>), or unknown groups resolve to zero officer recipients (fail-safe — the handler recipient still receives the notification).", + "maxLength": 255, + "facetable": true, + "title": "Privacy-Officer Group" + }, + "dpiaDetection": { + "type": "object", + "description": "GDPR art-35 DPIA pattern-detection configuration read by the daily detection job (dsar-escalation-and-dpia). Absent block = detection disabled for this jurisdiction (fail-safe: no false DPIA flags). The seeded default pack carries the pipelinq-parity defaults (threshold 10, window 30 days, group by type + scope).", + "title": "DPIA Detection", + "properties": { + "threshold": { + "type": "integer", + "description": "Minimum number of similar cases inside the rolling window that triggers the DPIA flag on the whole group.", + "minimum": 1, + "title": "Threshold" + }, + "windowDays": { + "type": "integer", + "description": "Rolling-window length in days over which similar cases are counted (receivedAt based).", + "minimum": 1, + "title": "Window (days)" + }, + "groupBy": { + "type": "array", + "description": "Case fields whose normalised values (trim/lowercase/collapse-whitespace) define 'similar' — cases sharing every listed value form one group.", + "title": "Group By", + "items": { + "type": "string", + "title": "Grouping Field", + "description": "A dataSubjectRequest field name used as a grouping characteristic.", + "maxLength": 128 + } + } + } + }, + "identityVerifyProvider": { + "type": "string", + "description": "Selector naming the registered identity-verify seam provider this jurisdiction binds. Unset or unknown resolves fail-closed to the OR safe-default (refusing) provider id (never fail-open); the default pack sets this to or.default.identity-verify.null. Provider interface/registry/resolution are delivered by the successor dsar-integration-seams change.", + "maxLength": 255, + "facetable": true, + "title": "Identity-Verify Provider" + }, + "regulatorEscalateProvider": { + "type": "string", + "description": "Selector naming the registered regulator-escalate seam provider this jurisdiction binds. Unset or unknown resolves fail-closed to the OR safe-default (refusing) provider id (never fail-open); the default pack sets this to or.default.regulator-escalate.null. Provider interface/registry/resolution are delivered by the successor dsar-integration-seams change.", + "maxLength": 255, + "facetable": true, + "title": "Regulator-Escalate Provider" + } + }, + "x-openregister-seeds": [ + { + "@self": { + "register": "dsar-policy-packs", + "schema": "dsarPolicyPack", + "slug": "pack-default" + }, + "name": "Default AVG/DSAR policy pack", + "jurisdiction": "default", + "deadlineDurationDays": 30, + "extensionDurationDays": 60, + "escalationTiers": [ + { "tier": "reminder", "offsetDays": -7 }, + { "tier": "escalation", "offsetDays": -2 }, + { "tier": "breach", "offsetDays": 0 } + ], + "intakeChannels": ["web-form", "email", "post"], + "roleMapping": { "dpo": "", "handler": "" }, + "templates": { "acknowledgement": "template:ack-default", "denial": "template:denial-default" }, + "denialGrounds": [ + { "key": "manifestly-unfounded", "label": "Manifestly unfounded or excessive request", "citation": "GDPR Art 12(5)" }, + { "key": "third-party-rights", "label": "Would adversely affect the rights of third parties", "citation": "GDPR Art 15(4)" }, + { "key": "overriding-legitimate-grounds", "label": "Compelling legitimate grounds override the objection", "citation": "GDPR Art 21(1)" } + ], + "retentionWindows": [ + { "key": "standard", "durationMonths": 12 }, + { "key": "extended", "durationMonths": 24 } + ], + "privacyOfficerGroup": "", + "dpiaDetection": { "threshold": 10, "windowDays": 30, "groupBy": ["type", "scope"] }, + "identityVerifyProvider": "or.default.identity-verify.null", + "regulatorEscalateProvider": "or.default.regulator-escalate.null" + }, + { + "@self": { + "register": "dsar-policy-packs", + "schema": "dsarPolicyPack", + "slug": "pack-nl-example" + }, + "name": "Voorbeeld NL AVG-beleidspakket (illustratief)", + "jurisdiction": "nl-example", + "deadlineDurationDays": 30, + "extensionDurationDays": 60, + "escalationTiers": [ + { "tier": "reminder", "offsetDays": -7 }, + { "tier": "escalation", "offsetDays": -3 }, + { "tier": "breach", "offsetDays": 0 } + ], + "intakeChannels": ["web-form", "email"], + "roleMapping": { "fg": "", "handler": "" }, + "templates": { "denial": "template:denial-nl-example" }, + "denialGrounds": [ + { "key": "manifestly-unfounded", "label": "Kennelijk ongegrond of buitensporig verzoek", "citation": "AVG art. 12 lid 5 (illustratief)" } + ], + "retentionWindows": [ + { "key": "boekhoudplicht", "durationMonths": 84 } + ], + "privacyOfficerGroup": "", + "identityVerifyProvider": "", + "regulatorEscalateProvider": "" + } + ] + } + } + } +} diff --git a/lib/Settings/dso_register.json b/lib/Settings/dso_register.json index 0942174d7c..e8d08e9d30 100644 --- a/lib/Settings/dso_register.json +++ b/lib/Settings/dso_register.json @@ -44,15 +44,18 @@ "properties": { "identificatie": { "type": "string", - "description": "Unique identifier for the activity" + "description": "Unique identifier for the activity", + "title": "Identification" }, "naam": { "type": "string", - "description": "Name of the activity (e.g. 'Dakkapel plaatsen')" + "description": "Name of the activity (e.g. 'Dakkapel plaatsen')", + "title": "Name" }, "activiteitgroep": { "type": "string", - "description": "Category group the activity belongs to" + "description": "Category group the activity belongs to", + "title": "Activity Group" }, "regelkwalificatie": { "type": "string", @@ -62,15 +65,18 @@ "meldingsplicht", "informatieplicht", "vergunningvrij" - ] + ], + "title": "Regulatory Classification" }, "bovenliggendeActiviteit": { "type": "string", - "description": "Reference to the parent activity in the hierarchy" + "description": "Reference to the parent activity in the hierarchy", + "title": "Parent Activity" }, "omschrijving": { "type": "string", - "description": "Detailed description of the activity" + "description": "Detailed description of the activity", + "title": "Description" } }, "hardValidation": false, @@ -92,11 +98,13 @@ "properties": { "identificatie": { "type": "string", - "description": "Unique identifier for the location" + "description": "Unique identifier for the location", + "title": "Identification" }, "naam": { "type": "string", - "description": "Name of the location" + "description": "Name of the location", + "title": "Name" }, "type": { "type": "string", @@ -107,15 +115,18 @@ "gemeente", "waterschap", "provincie" - ] + ], + "title": "Location Type" }, "gemeenteCode": { "type": "string", - "description": "Municipality code (CBS gemeentecode)" + "description": "Municipality code (CBS gemeentecode)", + "title": "Municipality Code" }, "gemeenteNaam": { "type": "string", - "description": "Municipality name" + "description": "Municipality name", + "title": "Municipality Name" }, "adres": { "type": "object", @@ -123,25 +134,31 @@ "properties": { "straat": { "type": "string", - "description": "Street name" + "description": "Street name", + "title": "Street Name" }, "huisnummer": { "type": "integer", - "description": "House number" + "description": "House number", + "title": "House Number" }, "huisletter": { "type": "string", - "description": "House letter" + "description": "House letter", + "title": "House Letter" }, "postcode": { "type": "string", - "description": "Dutch postal code" + "description": "Dutch postal code", + "title": "Postal Code" }, "woonplaats": { "type": "string", - "description": "City name" + "description": "City name", + "title": "City" } - } + }, + "title": "Address" } }, "hardValidation": false, @@ -163,7 +180,8 @@ "properties": { "identificatie": { "type": "string", - "description": "Unique document identifier" + "description": "Unique document identifier", + "title": "Document Identification" }, "type": { "type": "string", @@ -174,7 +192,8 @@ "waterschapsverordening", "AMvB", "ministeriele_regeling" - ] + ], + "title": "Document Type" }, "status": { "type": "string", @@ -183,20 +202,24 @@ "vastgesteld", "ontwerp", "ingetrokken" - ] + ], + "title": "Publication Status" }, "bevoegdGezag": { "type": "string", - "description": "Competent authority (organization name or OIN)" + "description": "Competent authority (organization name or OIN)", + "title": "Competent Authority" }, "titel": { "type": "string", - "description": "Title of the document" + "description": "Title of the document", + "title": "Title" }, "publicatiedatum": { "type": "string", "format": "date", - "description": "Date of publication" + "description": "Date of publication", + "title": "Publication Date" } }, "hardValidation": false, @@ -220,14 +243,16 @@ "properties": { "identificatie": { "type": "string", - "description": "Unique application identifier" + "description": "Unique application identifier", + "title": "Application Identification" }, "activiteiten": { "type": "array", "items": { "type": "string" }, - "description": "References to the activities in this application" + "description": "References to the activities in this application", + "title": "Activities" }, "locatie": { "type": "object", @@ -235,13 +260,16 @@ "properties": { "identificatie": { "type": "string", - "description": "Reference to a locatie record" + "description": "Reference to a locatie record", + "title": "Location Reference" }, "adres": { "type": "string", - "description": "Human-readable address" + "description": "Human-readable address", + "title": "Address" } - } + }, + "title": "Location" }, "initiatiefnemer": { "type": "object", @@ -249,7 +277,8 @@ "properties": { "naam": { "type": "string", - "description": "Applicant name" + "description": "Applicant name", + "title": "Applicant Name" }, "type": { "type": "string", @@ -257,13 +286,16 @@ "enum": [ "particulier", "bedrijf" - ] + ], + "title": "Applicant Type" } - } + }, + "title": "Applicant" }, "bevoegdGezag": { "type": "string", - "description": "Competent authority handling the application" + "description": "Competent authority handling the application", + "title": "Competent Authority" }, "status": { "type": "string", @@ -274,17 +306,20 @@ "verleend", "geweigerd", "ingetrokken" - ] + ], + "title": "Application Status" }, "indieningsdatum": { "type": "string", "format": "date", - "description": "Date the application was submitted" + "description": "Date the application was submitted", + "title": "Submission Date" }, "besluitdatum": { "type": "string", "format": "date", - "description": "Date the decision was made" + "description": "Date the decision was made", + "title": "Decision Date" }, "bijlagen": { "type": "array", @@ -293,19 +328,23 @@ "properties": { "naam": { "type": "string", - "description": "Attachment file name" + "description": "Attachment file name", + "title": "File Name" }, "type": { "type": "string", - "description": "Attachment type" + "description": "Attachment type", + "title": "File Type" } } }, - "description": "Attachments to the application" + "description": "Attachments to the application", + "title": "Attachments" }, "toelichting": { "type": "string", - "description": "Additional explanation or decision motivation" + "description": "Additional explanation or decision motivation", + "title": "Explanation" } }, "hardValidation": false, @@ -638,7 +677,6 @@ "bovenliggendeActiviteit": "nl.imow-gm0363.activiteit.evenementen", "omschrijving": "Het organiseren van een evenement waarbij professioneel vuurwerk wordt afgestoken. Vergunningplichtig vanwege veiligheidsrisico's." }, - { "@self": { "register": "dso", @@ -849,7 +887,6 @@ "woonplaats": "Groningen" } }, - { "@self": { "register": "dso", @@ -928,7 +965,6 @@ "titel": "Omgevingsplan Den Haag", "publicatiedatum": "2024-01-01" }, - { "@self": { "register": "dso", @@ -1251,4 +1287,4 @@ } ] } -} \ No newline at end of file +} diff --git a/lib/Settings/edepot_transfer_register.json b/lib/Settings/edepot_transfer_register.json new file mode 100644 index 0000000000..76fcd5367e --- /dev/null +++ b/lib/Settings/edepot_transfer_register.json @@ -0,0 +1,220 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "e-Depot Transfers", + "description": "System register for durable e-Depot (Archiefwet overbrenging) transfer records: the transfer list itself (status machine, approvals, exclusions, append-only delivery attempts) and the immutable per-object proof-of-transfer records (ingest confirmation, SIP identity, per-file checksums). Making the transfer API's placeholder comments true (archival-transfer-hardening, OR-AD-2/OR-AD-3). Proof records are write-once and live here — not in the transferred object's register — so destruction of the source object after handover never cascades to the proof (the Archiefwet accountability point).", + "version": "1.0.0" + }, + "x-openregister": { + "type": "core", + "app": "openregister", + "openregister": "^v0.2.10", + "description": "Foundational e-Depot transfer register shipped by OpenRegister; backs the durable transfer-list + proof-of-transfer records the transfer API serves." + }, + "paths": {}, + "components": { + "registers": { + "edepot-transfers": { + "slug": "edepot-transfers", + "title": "e-Depot Transfers", + "version": "1.0.0", + "description": "Durable e-Depot transfer records: transfer lists (status, approvals, exclusions, delivery attempts) and immutable per-object proof-of-transfer records.", + "published": "2026-01-01T00:00:00+00:00", + "schemas": [ + "edepotTransfer", + "edepotTransferProof" + ], + "folder": "Open Registers/e-Depot Transfers" + } + }, + "schemas": { + "edepotTransfer": { + "slug": "edepotTransfer", + "title": "e-Depot Transfer List", + "version": "1.0.0", + "published": "2026-01-01T00:00:00+00:00", + "summary": "A durable e-Depot transfer list tracked through its review-approve-execute status machine.", + "description": "The durable record of an e-Depot transfer: which objects are pending/approved/completed, archivist approvals + exclusions, the append-only history of delivery attempts (long-horizon retry), the final transfer result, and the SIP package format/identity. Persisted through ObjectService so status transitions ride OR's audited write path (making TransferController's former placeholder responses real).", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "description": "Transfer list lifecycle state.", + "enum": [ + "in_review", + "approved", + "rejected", + "in_progress", + "completed", + "partially_failed", + "failed" + ], + "default": "in_review", + "facetable": true, + "title": "Status" + }, + "objectReferences": { + "type": "array", + "description": "Objects included in this transfer, each as {uuid, schema, register}.", + "title": "Object References", + "items": { + "type": "object", + "title": "Object Reference", + "description": "A reference to one object included in the transfer.", + "properties": { + "uuid": { "type": "string", "maxLength": 255, "title": "Object UUID", "description": "UUID of the object included in the transfer." }, + "schema": { "type": "integer", "title": "Schema Id", "description": "Identifier of the schema the referenced object belongs to." }, + "register": { "type": "integer", "title": "Register Id", "description": "Identifier of the register the referenced object belongs to." } + } + } + }, + "exclusions": { + "type": "array", + "description": "Objects excluded from the transfer during review, each with a reason and timestamp.", + "title": "Exclusions", + "items": { + "type": "object", + "title": "Exclusion", + "description": "One excluded object.", + "properties": { + "uuid": { "type": "string", "maxLength": 255, "title": "Object UUID", "description": "UUID of the object excluded from the transfer." }, + "reason": { "type": "string", "maxLength": 2000, "title": "Reason", "description": "Human-readable reason the object was excluded during review." }, + "excludedAt": { "type": "string", "format": "date-time", "title": "Excluded At", "description": "Timestamp at which the object was excluded from the transfer." } + } + } + }, + "approvalMetadata": { + "type": "object", + "description": "Archivist approval / rejection metadata (who, when, rejection reason).", + "title": "Approval Metadata" + }, + "attempts": { + "type": "array", + "description": "Append-only history of delivery attempts (long-horizon retry): attempt number, timestamp, transport, per-package outcome, and error. Never rewritten — the WebhookLog attempt semantics, stored on this durable aggregate.", + "title": "Delivery Attempts", + "items": { + "type": "object", + "title": "Delivery Attempt", + "description": "One transport attempt for the transfer.", + "properties": { + "attempt": { "type": "integer", "title": "Attempt Number", "description": "Sequential number of this delivery attempt." }, + "timestamp": { "type": "string", "format": "date-time", "title": "Attempted At", "description": "Timestamp at which this delivery attempt was made." }, + "transport": { "type": "string", "maxLength": 128, "title": "Transport", "description": "Transport channel used for this delivery attempt." }, + "outcome": { "type": "string", "maxLength": 64, "title": "Outcome", "description": "Per-package outcome of this delivery attempt." }, + "error": { "type": "string", "maxLength": 4000, "title": "Error", "description": "Error detail captured when the delivery attempt failed." } + } + } + }, + "transferResult": { + "type": "object", + "description": "The final transfer result (completion timestamp + per-object outcomes).", + "title": "Transfer Result" + }, + "packageFormat": { + "type": "string", + "description": "SIP serialization format used for this transfer.", + "enum": ["zip", "bagit"], + "default": "zip", + "facetable": true, + "title": "Package Format" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "When the transfer list was created.", + "title": "Created At" + } + } + }, + "edepotTransferProof": { + "slug": "edepotTransferProof", + "title": "e-Depot Proof of Transfer", + "version": "1.0.0", + "published": "2026-01-01T00:00:00+00:00", + "immutable": true, + "summary": "An immutable proof-of-transfer record for one object confirmed ingested by an e-Depot.", + "description": "The durable, write-once ingest-confirmation artefact an archivist or auditor can produce years later (Archiefwet accountability). Created per confirmed object when the transfer completes; captures the e-Depot ingest reference/archivId + raw transport receipt, the SIP package identity (id, format, manifest SHA-256), and the per-file checksums as sent. Immutable (schema-level + service-level refusal); lives in this system register so destruction of the source object after handover never cascades to it. procest's retired archiefBewijs records migrate 1:1 onto this schema.", + "required": [ + "objectUuid", + "transferUuid", + "confirmedAt" + ], + "properties": { + "objectUuid": { + "type": "string", + "description": "UUID of the transferred (confirmed) object this proof attests.", + "maxLength": 255, + "facetable": true, + "title": "Object UUID" + }, + "transferUuid": { + "type": "string", + "description": "UUID of the edepotTransfer list this proof belongs to.", + "maxLength": 255, + "facetable": true, + "title": "Transfer UUID" + }, + "eDepotReference": { + "type": "string", + "description": "The e-Depot ingest reference / archivId returned on confirmation.", + "maxLength": 512, + "facetable": true, + "title": "e-Depot Reference" + }, + "transportReceipt": { + "type": "string", + "description": "Raw transport receipt / confirmation payload as returned by the e-Depot (opaque).", + "maxLength": 8000, + "title": "Transport Receipt" + }, + "transportName": { + "type": "string", + "description": "Name of the transport used (sftp, rest_api, openconnector).", + "maxLength": 128, + "title": "Transport" + }, + "packageId": { + "type": "string", + "description": "Identity of the SIP package the object was delivered in.", + "maxLength": 255, + "title": "Package Id" + }, + "packageFormat": { + "type": "string", + "description": "SIP serialization format of the delivered package.", + "enum": ["zip", "bagit"], + "title": "Package Format" + }, + "packageManifestSha256": { + "type": "string", + "description": "SHA-256 over the package payload manifest (the manifest-sha256.txt content for bagit, the sip-manifest.json content for zip).", + "maxLength": 64, + "title": "Package Manifest SHA-256" + }, + "fileChecksums": { + "type": "array", + "description": "Per-file checksums as sent, each {name, sha256}.", + "title": "File Checksums", + "items": { + "type": "object", + "title": "File Checksum", + "description": "One payload file's name and SHA-256.", + "properties": { + "name": { "type": "string", "maxLength": 1024, "title": "File Name", "description": "Name of the payload file included in the transfer." }, + "sha256": { "type": "string", "maxLength": 64, "title": "SHA-256", "description": "SHA-256 checksum of the payload file as sent." } + } + } + }, + "confirmedAt": { + "type": "string", + "format": "date-time", + "description": "When the e-Depot confirmed ingest of this object.", + "title": "Confirmed At" + } + } + } + } + } +} diff --git a/lib/Settings/kvk_register.json b/lib/Settings/kvk_register.json index 1b53b24b79..7ae16ecf07 100644 --- a/lib/Settings/kvk_register.json +++ b/lib/Settings/kvk_register.json @@ -33,6 +33,7 @@ "published": "2026-01-01T00:00:00+00:00", "summary": "A business entity registered at the Chamber of Commerce", "description": "Represents a maatschappelijke activiteit (business entity) in the KVK Handelsregister. Each record corresponds to a unique KVK number and contains the legal form, trade names, SBI activity codes, employee count, and registration dates.", + "x-schema-org": "schema:Organization", "required": [ "kvkNummer", "naam" @@ -44,13 +45,15 @@ "pattern": "^\\d{8}$", "minLength": 8, "maxLength": 8, - "facetable": true + "facetable": true, + "title": "KVK Number" }, "naam": { "type": "string", "description": "Primary name of the business", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Business Name" }, "handelsnamen": { "type": "array", @@ -60,32 +63,41 @@ "properties": { "naam": { "type": "string", - "description": "Trade name" + "description": "Trade name", + "title": "Trade Name" }, "volgorde": { "type": "integer", - "description": "Display order (0 = primary)" + "description": "Display order (0 = primary)", + "title": "Display Order" } }, - "required": ["naam", "volgorde"] + "required": [ + "naam", + "volgorde" + ] }, - "facetable": false + "facetable": false, + "title": "Trade Names" }, "rechtsvorm": { "type": "string", "description": "Legal form description", - "facetable": true + "facetable": true, + "title": "Legal Form" }, "uitgebreideRechtsvorm": { "type": "string", "description": "Detailed legal form description", - "facetable": false + "facetable": false, + "title": "Detailed Legal Form" }, "formeleRegistratiedatum": { "type": "string", "description": "Formal registration date (ISO 8601)", "format": "date", - "facetable": false + "facetable": false, + "title": "Formal Registration Date" }, "materieleRegistratie": { "type": "object", @@ -94,21 +106,25 @@ "datumAanvang": { "type": "string", "description": "Start date of material registration", - "format": "date" + "format": "date", + "title": "Start Date" }, "datumEinde": { "type": "string", "description": "End date of material registration (null if active)", - "format": "date" + "format": "date", + "title": "End Date" } }, - "facetable": false + "facetable": false, + "title": "Material Registration" }, "totaalWerkzamePersonen": { "type": "integer", "description": "Total number of employed persons", "minimum": 0, - "facetable": true + "facetable": true, + "title": "Total Employees" }, "sbiActiviteiten": { "type": "array", @@ -118,38 +134,56 @@ "properties": { "sbiCode": { "type": "string", - "description": "SBI code" + "description": "SBI code", + "title": "SBI Code" }, "sbiOmschrijving": { "type": "string", - "description": "SBI activity description" + "description": "SBI activity description", + "title": "SBI Description" }, "indHoofdactiviteit": { "type": "string", "description": "Whether this is the main activity (Ja/Nee)", - "enum": ["Ja", "Nee"] + "enum": [ + "Ja", + "Nee" + ], + "title": "Main Activity Indicator" } }, - "required": ["sbiCode", "sbiOmschrijving", "indHoofdactiviteit"] + "required": [ + "sbiCode", + "sbiOmschrijving", + "indHoofdactiviteit" + ] }, - "facetable": false + "facetable": false, + "title": "SBI Activities" }, "indNonMailing": { "type": "string", "description": "Non-mailing indicator (Ja/Nee)", - "enum": ["Ja", "Nee"], - "facetable": false + "enum": [ + "Ja", + "Nee" + ], + "facetable": false, + "title": "Non-Mailing Indicator" }, "actief": { "type": "boolean", "description": "Whether the business is currently active (computed from datumEinde)", - "facetable": true + "facetable": true, + "title": "Active" } }, "hardValidation": false, "searchable": true, "authorization": { - "read": ["public"] + "read": [ + "public" + ] } }, "vestiging": { @@ -159,6 +193,7 @@ "published": "2026-01-01T00:00:00+00:00", "summary": "A branch/establishment of a business", "description": "Represents a vestiging (branch/establishment) in the KVK Handelsregister. Each vestiging has a unique 12-digit vestigingsnummer and is linked to a parent business via kvkNummer. Contains address information, employee counts, and SBI activity codes.", + "x-schema-org": "schema:Organization", "required": [ "vestigingsnummer", "kvkNummer", @@ -171,49 +206,63 @@ "pattern": "^\\d{12}$", "minLength": 12, "maxLength": 12, - "facetable": true + "facetable": true, + "title": "Branch Number" }, "kvkNummer": { "type": "string", "description": "8-digit KVK number of the parent business", "pattern": "^\\d{8}$", - "facetable": true + "facetable": true, + "title": "KVK Number" }, "eersteHandelsnaam": { "type": "string", "description": "Primary trade name of the branch", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Primary Trade Name" }, "indHoofdvestiging": { "type": "string", "description": "Whether this is the main branch (Ja/Nee)", - "enum": ["Ja", "Nee"], - "facetable": true + "enum": [ + "Ja", + "Nee" + ], + "facetable": true, + "title": "Main Branch Indicator" }, "indCommercieleVestiging": { "type": "string", "description": "Whether this is a commercial branch (Ja/Nee)", - "enum": ["Ja", "Nee"], - "facetable": false + "enum": [ + "Ja", + "Nee" + ], + "facetable": false, + "title": "Commercial Branch Indicator" }, "voltijdWerkzamePersonen": { "type": "integer", "description": "Number of full-time employees", "minimum": 0, - "facetable": false + "facetable": false, + "title": "Full-Time Employees" }, "deeltijdWerkzamePersonen": { "type": "integer", "description": "Number of part-time employees", "minimum": 0, - "facetable": false + "facetable": false, + "title": "Part-Time Employees" }, "totaalWerkzamePersonen": { "type": "integer", "description": "Total number of employees at this branch", "minimum": 0, - "facetable": true + "facetable": true, + "title": "Total Employees" }, "adressen": { "type": "array", @@ -224,40 +273,54 @@ "type": { "type": "string", "description": "Address type", - "enum": ["bezoekadres", "correspondentieadres"] + "enum": [ + "bezoekadres", + "correspondentieadres" + ], + "title": "Address Type" }, "straatnaam": { "type": "string", - "description": "Street name" + "description": "Street name", + "title": "Street Name" }, "huisnummer": { "type": "integer", - "description": "House number" + "description": "House number", + "title": "House Number" }, "huisletter": { "type": "string", - "description": "House letter (optional)" + "description": "House letter (optional)", + "title": "House Letter" }, "postbusnummer": { "type": "integer", - "description": "PO box number (optional)" + "description": "PO box number (optional)", + "title": "PO Box Number" }, "postcode": { "type": "string", - "description": "Postal code (####XX)" + "description": "Postal code (####XX)", + "title": "Postal Code" }, "plaats": { "type": "string", - "description": "City" + "description": "City", + "title": "City" }, "land": { "type": "string", - "description": "Country" + "description": "Country", + "title": "Country" } }, - "required": ["type"] + "required": [ + "type" + ] }, - "facetable": false + "facetable": false, + "title": "Addresses" }, "handelsnamen": { "type": "array", @@ -267,16 +330,22 @@ "properties": { "naam": { "type": "string", - "description": "Trade name" + "description": "Trade name", + "title": "Trade Name" }, "volgorde": { "type": "integer", - "description": "Display order" + "description": "Display order", + "title": "Display Order" } }, - "required": ["naam", "volgorde"] + "required": [ + "naam", + "volgorde" + ] }, - "facetable": false + "facetable": false, + "title": "Trade Names" }, "sbiActiviteiten": { "type": "array", @@ -285,25 +354,41 @@ "type": "object", "properties": { "sbiCode": { - "type": "string" + "type": "string", + "title": "SBI Code", + "description": "SBI code" }, "sbiOmschrijving": { - "type": "string" + "type": "string", + "title": "SBI Description", + "description": "SBI activity description" }, "indHoofdactiviteit": { "type": "string", - "enum": ["Ja", "Nee"] + "enum": [ + "Ja", + "Nee" + ], + "title": "Main Activity Indicator", + "description": "Whether this is the main activity (Ja/Nee)" } }, - "required": ["sbiCode", "sbiOmschrijving", "indHoofdactiviteit"] + "required": [ + "sbiCode", + "sbiOmschrijving", + "indHoofdactiviteit" + ] }, - "facetable": false + "facetable": false, + "title": "SBI Activities" } }, "hardValidation": false, "searchable": true, "authorization": { - "read": ["public"] + "read": [ + "public" + ] } } }, @@ -317,8 +402,14 @@ "kvkNummer": "69599084", "naam": "Test EMZ Dagobert", "handelsnamen": [ - { "naam": "Test EMZ Dagobert", "volgorde": 0 }, - { "naam": "Dagobert's Goudhandel", "volgorde": 1 } + { + "naam": "Test EMZ Dagobert", + "volgorde": 0 + }, + { + "naam": "Dagobert's Goudhandel", + "volgorde": 1 + } ], "rechtsvorm": "Eenmanszaak", "uitgebreideRechtsvorm": "Eenmanszaak", @@ -329,8 +420,16 @@ }, "totaalWerkzamePersonen": 1, "sbiActiviteiten": [ - { "sbiCode": "10110", "sbiOmschrijving": "Slachterijen (geen pluimvee-)", "indHoofdactiviteit": "Ja" }, - { "sbiCode": "10120", "sbiOmschrijving": "Slachterijen (pluimvee-)", "indHoofdactiviteit": "Nee" } + { + "sbiCode": "10110", + "sbiOmschrijving": "Slachterijen (geen pluimvee-)", + "indHoofdactiviteit": "Ja" + }, + { + "sbiCode": "10120", + "sbiOmschrijving": "Slachterijen (pluimvee-)", + "indHoofdactiviteit": "Nee" + } ], "indNonMailing": "Nee", "actief": true @@ -344,8 +443,14 @@ "kvkNummer": "68727720", "naam": "Test NV Katrien", "handelsnamen": [ - { "naam": "Test NV Katrien", "volgorde": 0 }, - { "naam": "Katrien's Modehuis", "volgorde": 1 } + { + "naam": "Test NV Katrien", + "volgorde": 0 + }, + { + "naam": "Katrien's Modehuis", + "volgorde": 1 + } ], "rechtsvorm": "Naamloze Vennootschap", "uitgebreideRechtsvorm": "Naamloze Vennootschap", @@ -356,8 +461,16 @@ }, "totaalWerkzamePersonen": 12, "sbiActiviteiten": [ - { "sbiCode": "4771", "sbiOmschrijving": "Winkels in kleding", "indHoofdactiviteit": "Ja" }, - { "sbiCode": "4772", "sbiOmschrijving": "Winkels in schoenen en lederwaren", "indHoofdactiviteit": "Nee" } + { + "sbiCode": "4771", + "sbiOmschrijving": "Winkels in kleding", + "indHoofdactiviteit": "Ja" + }, + { + "sbiCode": "4772", + "sbiOmschrijving": "Winkels in schoenen en lederwaren", + "indHoofdactiviteit": "Nee" + } ], "indNonMailing": "Nee", "actief": true @@ -371,8 +484,14 @@ "kvkNummer": "68750110", "naam": "Test BV Donald", "handelsnamen": [ - { "naam": "Test BV Donald", "volgorde": 0 }, - { "naam": "Donald's Scheepvaart B.V.", "volgorde": 1 } + { + "naam": "Test BV Donald", + "volgorde": 0 + }, + { + "naam": "Donald's Scheepvaart B.V.", + "volgorde": 1 + } ], "rechtsvorm": "Besloten Vennootschap", "uitgebreideRechtsvorm": "Besloten Vennootschap met beperkte aansprakelijkheid", @@ -383,8 +502,16 @@ }, "totaalWerkzamePersonen": 5, "sbiActiviteiten": [ - { "sbiCode": "5010", "sbiOmschrijving": "Zee- en kustvaart (passagiers)", "indHoofdactiviteit": "Ja" }, - { "sbiCode": "5020", "sbiOmschrijving": "Zee- en kustvaart (vracht)", "indHoofdactiviteit": "Nee" } + { + "sbiCode": "5010", + "sbiOmschrijving": "Zee- en kustvaart (passagiers)", + "indHoofdactiviteit": "Ja" + }, + { + "sbiCode": "5020", + "sbiOmschrijving": "Zee- en kustvaart (vracht)", + "indHoofdactiviteit": "Nee" + } ], "indNonMailing": "Ja", "actief": true @@ -398,7 +525,10 @@ "kvkNummer": "69599068", "naam": "Test Stichting Bolderbast", "handelsnamen": [ - { "naam": "Test Stichting Bolderbast", "volgorde": 0 } + { + "naam": "Test Stichting Bolderbast", + "volgorde": 0 + } ], "rechtsvorm": "Stichting", "uitgebreideRechtsvorm": "Stichting", @@ -409,7 +539,11 @@ }, "totaalWerkzamePersonen": 3, "sbiActiviteiten": [ - { "sbiCode": "8810", "sbiOmschrijving": "Maatschappelijke dienstverlening zonder overnachting", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "8810", + "sbiOmschrijving": "Maatschappelijke dienstverlening zonder overnachting", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -423,8 +557,14 @@ "kvkNummer": "69599076", "naam": "Test VOF Guus", "handelsnamen": [ - { "naam": "Test VOF Guus", "volgorde": 0 }, - { "naam": "Guus Gelansen Goudsmederij", "volgorde": 1 } + { + "naam": "Test VOF Guus", + "volgorde": 0 + }, + { + "naam": "Guus Gelansen Goudsmederij", + "volgorde": 1 + } ], "rechtsvorm": "Vennootschap Onder Firma", "uitgebreideRechtsvorm": "Vennootschap onder firma", @@ -435,7 +575,11 @@ }, "totaalWerkzamePersonen": 4, "sbiActiviteiten": [ - { "sbiCode": "3212", "sbiOmschrijving": "Vervaardiging van sieraden", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "3212", + "sbiOmschrijving": "Vervaardiging van sieraden", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -449,7 +593,10 @@ "kvkNummer": "90000102", "naam": "Stichting Free opentrans", "handelsnamen": [ - { "naam": "Stichting Free opentrans", "volgorde": 0 } + { + "naam": "Stichting Free opentrans", + "volgorde": 0 + } ], "rechtsvorm": "Stichting", "uitgebreideRechtsvorm": "Stichting", @@ -460,7 +607,11 @@ }, "totaalWerkzamePersonen": 2, "sbiActiviteiten": [ - { "sbiCode": "6201", "sbiOmschrijving": "Ontwikkelen, produceren en uitgeven van software", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "6201", + "sbiOmschrijving": "Ontwikkelen, produceren en uitgeven van software", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -474,8 +625,14 @@ "kvkNummer": "90001354", "naam": "Grand Kontex B.V.", "handelsnamen": [ - { "naam": "Grand Kontex B.V.", "volgorde": 0 }, - { "naam": "Kontex Solutions", "volgorde": 1 } + { + "naam": "Grand Kontex B.V.", + "volgorde": 0 + }, + { + "naam": "Kontex Solutions", + "volgorde": 1 + } ], "rechtsvorm": "Besloten Vennootschap", "uitgebreideRechtsvorm": "Besloten Vennootschap met beperkte aansprakelijkheid", @@ -486,7 +643,11 @@ }, "totaalWerkzamePersonen": 8, "sbiActiviteiten": [ - { "sbiCode": "6420", "sbiOmschrijving": "Financiele holdings", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "6420", + "sbiOmschrijving": "Financiele holdings", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -500,8 +661,14 @@ "kvkNummer": "55344526", "naam": "Regional Stimflex Cooperatie", "handelsnamen": [ - { "naam": "Regional Stimflex Cooperatie", "volgorde": 0 }, - { "naam": "Stimflex Europe", "volgorde": 1 } + { + "naam": "Regional Stimflex Cooperatie", + "volgorde": 0 + }, + { + "naam": "Stimflex Europe", + "volgorde": 1 + } ], "rechtsvorm": "Cooperatie", "uitgebreideRechtsvorm": "Cooperatie met beperkte aansprakelijkheid", @@ -512,7 +679,11 @@ }, "totaalWerkzamePersonen": 15, "sbiActiviteiten": [ - { "sbiCode": "7022", "sbiOmschrijving": "Overige adviesbureaus op het gebied van management", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "7022", + "sbiOmschrijving": "Overige adviesbureaus op het gebied van management", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -526,8 +697,14 @@ "kvkNummer": "74123456", "naam": "Bakkerij De Vries B.V.", "handelsnamen": [ - { "naam": "Bakkerij De Vries B.V.", "volgorde": 0 }, - { "naam": "De Vries Brood & Banket", "volgorde": 1 } + { + "naam": "Bakkerij De Vries B.V.", + "volgorde": 0 + }, + { + "naam": "De Vries Brood & Banket", + "volgorde": 1 + } ], "rechtsvorm": "Besloten Vennootschap", "uitgebreideRechtsvorm": "Besloten Vennootschap met beperkte aansprakelijkheid", @@ -538,7 +715,11 @@ }, "totaalWerkzamePersonen": 25, "sbiActiviteiten": [ - { "sbiCode": "1071", "sbiOmschrijving": "Vervaardiging van brood en vers banketbakkerswerk", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "1071", + "sbiOmschrijving": "Vervaardiging van brood en vers banketbakkerswerk", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -552,8 +733,14 @@ "kvkNummer": "81234567", "naam": "Van den Berg IT Solutions", "handelsnamen": [ - { "naam": "Van den Berg IT Solutions", "volgorde": 0 }, - { "naam": "VdB IT", "volgorde": 1 } + { + "naam": "Van den Berg IT Solutions", + "volgorde": 0 + }, + { + "naam": "VdB IT", + "volgorde": 1 + } ], "rechtsvorm": "Eenmanszaak", "uitgebreideRechtsvorm": "Eenmanszaak", @@ -564,7 +751,11 @@ }, "totaalWerkzamePersonen": 1, "sbiActiviteiten": [ - { "sbiCode": "6201", "sbiOmschrijving": "Ontwikkelen, produceren en uitgeven van software", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "6201", + "sbiOmschrijving": "Ontwikkelen, produceren en uitgeven van software", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -578,7 +769,10 @@ "kvkNummer": "50123456", "naam": "Gemeente Voorbeeldstad", "handelsnamen": [ - { "naam": "Gemeente Voorbeeldstad", "volgorde": 0 } + { + "naam": "Gemeente Voorbeeldstad", + "volgorde": 0 + } ], "rechtsvorm": "Publiekrechtelijke Rechtspersoon", "uitgebreideRechtsvorm": "Gemeente", @@ -589,7 +783,11 @@ }, "totaalWerkzamePersonen": 450, "sbiActiviteiten": [ - { "sbiCode": "8411", "sbiOmschrijving": "Algemeen overheidsbestuur", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "8411", + "sbiOmschrijving": "Algemeen overheidsbestuur", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -603,8 +801,14 @@ "kvkNummer": "62345678", "naam": "Bouwbedrijf Jansen VOF", "handelsnamen": [ - { "naam": "Bouwbedrijf Jansen VOF", "volgorde": 0 }, - { "naam": "Jansen Bouw", "volgorde": 1 } + { + "naam": "Bouwbedrijf Jansen VOF", + "volgorde": 0 + }, + { + "naam": "Jansen Bouw", + "volgorde": 1 + } ], "rechtsvorm": "Vennootschap Onder Firma", "uitgebreideRechtsvorm": "Vennootschap onder firma", @@ -615,7 +819,11 @@ }, "totaalWerkzamePersonen": 50, "sbiActiviteiten": [ - { "sbiCode": "4120", "sbiOmschrijving": "Algemene burgerlijke en utiliteitsbouw", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "4120", + "sbiOmschrijving": "Algemene burgerlijke en utiliteitsbouw", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -629,8 +837,14 @@ "kvkNummer": "71234567", "naam": "Restaurant Het Gerecht", "handelsnamen": [ - { "naam": "Restaurant Het Gerecht", "volgorde": 0 }, - { "naam": "Het Gerecht Catering", "volgorde": 1 } + { + "naam": "Restaurant Het Gerecht", + "volgorde": 0 + }, + { + "naam": "Het Gerecht Catering", + "volgorde": 1 + } ], "rechtsvorm": "Eenmanszaak", "uitgebreideRechtsvorm": "Eenmanszaak", @@ -641,7 +855,11 @@ }, "totaalWerkzamePersonen": 8, "sbiActiviteiten": [ - { "sbiCode": "5610", "sbiOmschrijving": "Restaurants, cafetaria's, snackbars, ijssalons, eetkramen e.d.", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "5610", + "sbiOmschrijving": "Restaurants, cafetaria's, snackbars, ijssalons, eetkramen e.d.", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -655,8 +873,14 @@ "kvkNummer": "83456789", "naam": "Zorggroep Horizon", "handelsnamen": [ - { "naam": "Zorggroep Horizon", "volgorde": 0 }, - { "naam": "Horizon Ziekenhuis Maastricht", "volgorde": 1 } + { + "naam": "Zorggroep Horizon", + "volgorde": 0 + }, + { + "naam": "Horizon Ziekenhuis Maastricht", + "volgorde": 1 + } ], "rechtsvorm": "Stichting", "uitgebreideRechtsvorm": "Stichting", @@ -667,8 +891,16 @@ }, "totaalWerkzamePersonen": 200, "sbiActiviteiten": [ - { "sbiCode": "8610", "sbiOmschrijving": "Ziekenhuizen", "indHoofdactiviteit": "Ja" }, - { "sbiCode": "8810", "sbiOmschrijving": "Maatschappelijke dienstverlening zonder overnachting", "indHoofdactiviteit": "Nee" } + { + "sbiCode": "8610", + "sbiOmschrijving": "Ziekenhuizen", + "indHoofdactiviteit": "Ja" + }, + { + "sbiCode": "8810", + "sbiOmschrijving": "Maatschappelijke dienstverlening zonder overnachting", + "indHoofdactiviteit": "Nee" + } ], "indNonMailing": "Nee", "actief": true @@ -682,9 +914,18 @@ "kvkNummer": "64567890", "naam": "TransPort Nederland NV", "handelsnamen": [ - { "naam": "TransPort Nederland NV", "volgorde": 0 }, - { "naam": "TransPort Logistiek", "volgorde": 1 }, - { "naam": "TP Express", "volgorde": 2 } + { + "naam": "TransPort Nederland NV", + "volgorde": 0 + }, + { + "naam": "TransPort Logistiek", + "volgorde": 1 + }, + { + "naam": "TP Express", + "volgorde": 2 + } ], "rechtsvorm": "Naamloze Vennootschap", "uitgebreideRechtsvorm": "Naamloze Vennootschap", @@ -695,7 +936,11 @@ }, "totaalWerkzamePersonen": 150, "sbiActiviteiten": [ - { "sbiCode": "4941", "sbiOmschrijving": "Goederenvervoer over de weg", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "4941", + "sbiOmschrijving": "Goederenvervoer over de weg", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Nee", "actief": true @@ -709,7 +954,10 @@ "kvkNummer": "67890123", "naam": "Duckstad Vastgoed B.V.", "handelsnamen": [ - { "naam": "Duckstad Vastgoed B.V.", "volgorde": 0 } + { + "naam": "Duckstad Vastgoed B.V.", + "volgorde": 0 + } ], "rechtsvorm": "Besloten Vennootschap", "uitgebreideRechtsvorm": "Besloten Vennootschap met beperkte aansprakelijkheid", @@ -720,7 +968,11 @@ }, "totaalWerkzamePersonen": 0, "sbiActiviteiten": [ - { "sbiCode": "6810", "sbiOmschrijving": "Handel in eigen onroerend goed", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "6810", + "sbiOmschrijving": "Handel in eigen onroerend goed", + "indHoofdactiviteit": "Ja" + } ], "indNonMailing": "Ja", "actief": false @@ -751,12 +1003,26 @@ } ], "handelsnamen": [ - { "naam": "Test EMZ Dagobert", "volgorde": 0 }, - { "naam": "Dagobert's Goudhandel", "volgorde": 1 } + { + "naam": "Test EMZ Dagobert", + "volgorde": 0 + }, + { + "naam": "Dagobert's Goudhandel", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "10110", "sbiOmschrijving": "Slachterijen (geen pluimvee-)", "indHoofdactiviteit": "Ja" }, - { "sbiCode": "10120", "sbiOmschrijving": "Slachterijen (pluimvee-)", "indHoofdactiviteit": "Nee" } + { + "sbiCode": "10110", + "sbiOmschrijving": "Slachterijen (geen pluimvee-)", + "indHoofdactiviteit": "Ja" + }, + { + "sbiCode": "10120", + "sbiOmschrijving": "Slachterijen (pluimvee-)", + "indHoofdactiviteit": "Nee" + } ] }, { @@ -784,10 +1050,17 @@ } ], "handelsnamen": [ - { "naam": "Dagobert's Goudhandel Filiaal", "volgorde": 0 } + { + "naam": "Dagobert's Goudhandel Filiaal", + "volgorde": 0 + } ], "sbiActiviteiten": [ - { "sbiCode": "10110", "sbiOmschrijving": "Slachterijen (geen pluimvee-)", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "10110", + "sbiOmschrijving": "Slachterijen (geen pluimvee-)", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -823,12 +1096,26 @@ } ], "handelsnamen": [ - { "naam": "Test NV Katrien", "volgorde": 0 }, - { "naam": "Katrien's Modehuis", "volgorde": 1 } + { + "naam": "Test NV Katrien", + "volgorde": 0 + }, + { + "naam": "Katrien's Modehuis", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "4771", "sbiOmschrijving": "Winkels in kleding", "indHoofdactiviteit": "Ja" }, - { "sbiCode": "4772", "sbiOmschrijving": "Winkels in schoenen en lederwaren", "indHoofdactiviteit": "Nee" } + { + "sbiCode": "4771", + "sbiOmschrijving": "Winkels in kleding", + "indHoofdactiviteit": "Ja" + }, + { + "sbiCode": "4772", + "sbiOmschrijving": "Winkels in schoenen en lederwaren", + "indHoofdactiviteit": "Nee" + } ] }, { @@ -863,12 +1150,26 @@ } ], "handelsnamen": [ - { "naam": "Test BV Donald", "volgorde": 0 }, - { "naam": "Donald's Scheepvaart B.V.", "volgorde": 1 } + { + "naam": "Test BV Donald", + "volgorde": 0 + }, + { + "naam": "Donald's Scheepvaart B.V.", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "5010", "sbiOmschrijving": "Zee- en kustvaart (passagiers)", "indHoofdactiviteit": "Ja" }, - { "sbiCode": "5020", "sbiOmschrijving": "Zee- en kustvaart (vracht)", "indHoofdactiviteit": "Nee" } + { + "sbiCode": "5010", + "sbiOmschrijving": "Zee- en kustvaart (passagiers)", + "indHoofdactiviteit": "Ja" + }, + { + "sbiCode": "5020", + "sbiOmschrijving": "Zee- en kustvaart (vracht)", + "indHoofdactiviteit": "Nee" + } ] }, { @@ -896,10 +1197,17 @@ } ], "handelsnamen": [ - { "naam": "Donald's Scheepvaart B.V. - Kantoor Amsterdam", "volgorde": 0 } + { + "naam": "Donald's Scheepvaart B.V. - Kantoor Amsterdam", + "volgorde": 0 + } ], "sbiActiviteiten": [ - { "sbiCode": "5010", "sbiOmschrijving": "Zee- en kustvaart (passagiers)", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "5010", + "sbiOmschrijving": "Zee- en kustvaart (passagiers)", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -927,10 +1235,17 @@ } ], "handelsnamen": [ - { "naam": "Test Stichting Bolderbast", "volgorde": 0 } + { + "naam": "Test Stichting Bolderbast", + "volgorde": 0 + } ], "sbiActiviteiten": [ - { "sbiCode": "8810", "sbiOmschrijving": "Maatschappelijke dienstverlening zonder overnachting", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "8810", + "sbiOmschrijving": "Maatschappelijke dienstverlening zonder overnachting", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -958,11 +1273,21 @@ } ], "handelsnamen": [ - { "naam": "Test VOF Guus", "volgorde": 0 }, - { "naam": "Guus Gelansen Goudsmederij", "volgorde": 1 } + { + "naam": "Test VOF Guus", + "volgorde": 0 + }, + { + "naam": "Guus Gelansen Goudsmederij", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "3212", "sbiOmschrijving": "Vervaardiging van sieraden", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "3212", + "sbiOmschrijving": "Vervaardiging van sieraden", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -998,11 +1323,21 @@ } ], "handelsnamen": [ - { "naam": "Regional Stimflex Cooperatie", "volgorde": 0 }, - { "naam": "Stimflex Europe", "volgorde": 1 } + { + "naam": "Regional Stimflex Cooperatie", + "volgorde": 0 + }, + { + "naam": "Stimflex Europe", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "7022", "sbiOmschrijving": "Overige adviesbureaus op het gebied van management", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "7022", + "sbiOmschrijving": "Overige adviesbureaus op het gebied van management", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -1030,11 +1365,21 @@ } ], "handelsnamen": [ - { "naam": "Bakkerij De Vries B.V.", "volgorde": 0 }, - { "naam": "De Vries Brood & Banket", "volgorde": 1 } + { + "naam": "Bakkerij De Vries B.V.", + "volgorde": 0 + }, + { + "naam": "De Vries Brood & Banket", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "1071", "sbiOmschrijving": "Vervaardiging van brood en vers banketbakkerswerk", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "1071", + "sbiOmschrijving": "Vervaardiging van brood en vers banketbakkerswerk", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -1062,11 +1407,21 @@ } ], "handelsnamen": [ - { "naam": "Van den Berg IT Solutions", "volgorde": 0 }, - { "naam": "VdB IT", "volgorde": 1 } + { + "naam": "Van den Berg IT Solutions", + "volgorde": 0 + }, + { + "naam": "VdB IT", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "6201", "sbiOmschrijving": "Ontwikkelen, produceren en uitgeven van software", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "6201", + "sbiOmschrijving": "Ontwikkelen, produceren en uitgeven van software", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -1094,11 +1449,21 @@ } ], "handelsnamen": [ - { "naam": "Bouwbedrijf Jansen VOF", "volgorde": 0 }, - { "naam": "Jansen Bouw", "volgorde": 1 } + { + "naam": "Bouwbedrijf Jansen VOF", + "volgorde": 0 + }, + { + "naam": "Jansen Bouw", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "4120", "sbiOmschrijving": "Algemene burgerlijke en utiliteitsbouw", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "4120", + "sbiOmschrijving": "Algemene burgerlijke en utiliteitsbouw", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -1126,11 +1491,21 @@ } ], "handelsnamen": [ - { "naam": "Restaurant Het Gerecht", "volgorde": 0 }, - { "naam": "Het Gerecht Catering", "volgorde": 1 } + { + "naam": "Restaurant Het Gerecht", + "volgorde": 0 + }, + { + "naam": "Het Gerecht Catering", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "5610", "sbiOmschrijving": "Restaurants, cafetaria's, snackbars, ijssalons, eetkramen e.d.", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "5610", + "sbiOmschrijving": "Restaurants, cafetaria's, snackbars, ijssalons, eetkramen e.d.", + "indHoofdactiviteit": "Ja" + } ] }, { @@ -1158,12 +1533,26 @@ } ], "handelsnamen": [ - { "naam": "Zorggroep Horizon", "volgorde": 0 }, - { "naam": "Horizon Ziekenhuis Maastricht", "volgorde": 1 } + { + "naam": "Zorggroep Horizon", + "volgorde": 0 + }, + { + "naam": "Horizon Ziekenhuis Maastricht", + "volgorde": 1 + } ], "sbiActiviteiten": [ - { "sbiCode": "8610", "sbiOmschrijving": "Ziekenhuizen", "indHoofdactiviteit": "Ja" }, - { "sbiCode": "8810", "sbiOmschrijving": "Maatschappelijke dienstverlening zonder overnachting", "indHoofdactiviteit": "Nee" } + { + "sbiCode": "8610", + "sbiOmschrijving": "Ziekenhuizen", + "indHoofdactiviteit": "Ja" + }, + { + "sbiCode": "8810", + "sbiOmschrijving": "Maatschappelijke dienstverlening zonder overnachting", + "indHoofdactiviteit": "Nee" + } ] }, { @@ -1199,14 +1588,27 @@ } ], "handelsnamen": [ - { "naam": "TransPort Nederland NV", "volgorde": 0 }, - { "naam": "TransPort Logistiek", "volgorde": 1 }, - { "naam": "TP Express", "volgorde": 2 } + { + "naam": "TransPort Nederland NV", + "volgorde": 0 + }, + { + "naam": "TransPort Logistiek", + "volgorde": 1 + }, + { + "naam": "TP Express", + "volgorde": 2 + } ], "sbiActiviteiten": [ - { "sbiCode": "4941", "sbiOmschrijving": "Goederenvervoer over de weg", "indHoofdactiviteit": "Ja" } + { + "sbiCode": "4941", + "sbiOmschrijving": "Goederenvervoer over de weg", + "indHoofdactiviteit": "Ja" + } ] } ] } -} \ No newline at end of file +} diff --git a/lib/Settings/merge_operation_register.json b/lib/Settings/merge_operation_register.json new file mode 100644 index 0000000000..c3c22027e3 --- /dev/null +++ b/lib/Settings/merge_operation_register.json @@ -0,0 +1,100 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "MDM Merge Operation", + "description": "Generic, entity-type-agnostic reversible merge audit log consumed by MergeService. Rows are written only by executeMerge()/reverseMerge() — a runtime audit trail, never seeded. RBAC-scoped and auditable via OpenRegister's object infrastructure (ADR-022) rather than a bespoke app-local log table.", + "version": "1.0.0" + }, + "x-openregister": { + "type": "core", + "app": "openregister", + "openregister": "^v0.2.10", + "description": "Foundational MDM merge-operation register shipped by OpenRegister for the reversible merge engine (x-openregister-merge)." + }, + "paths": {}, + "components": { + "registers": { + "merge-operation": { + "slug": "merge-operation", + "title": "Merge Operation", + "version": "1.0.0", + "description": "Runtime audit log of executed (and reversed) MDM merges. One row per merge.", + "published": "2026-01-01T00:00:00+00:00", + "schemas": [ + "mergeOperation" + ], + "folder": "Open Registers/MDM Merge Operation" + } + }, + "schemas": { + "mergeOperation": { + "slug": "mergeOperation", + "title": "Merge Operation", + "version": "1.0.0", + "published": "2026-01-01T00:00:00+00:00", + "summary": "An audit row for one executed (or reversed) merge.", + "description": "Written exactly once by MergeService::executeMerge() and updated by MergeService::reverseMerge(). Captures the survivor, the merged-away objects, the reason, and a preMergeSnapshot sufficient to restore both objects within the reversal window.", + "required": [ + "mergedIntoUuid", + "mergedFromUuids", + "mergedAt", + "reversible" + ], + "properties": { + "mergedIntoUuid": { + "type": "string", + "description": "UUID of the surviving object.", + "maxLength": 255, + "facetable": true, + "title": "Merged Into" + }, + "mergedFromUuids": { + "type": "array", + "description": "UUIDs of the losing objects merged away into the survivor.", + "items": { + "type": "string", + "maxLength": 255 + }, + "title": "Merged From" + }, + "reason": { + "type": "string", + "description": "Steward-supplied merge reason / classification.", + "maxLength": 1000, + "title": "Reason" + }, + "preMergeSnapshot": { + "type": "object", + "description": "Both objects' golden record, provenance, status and linked source-record ownership, sufficient to restore them on reversal.", + "title": "Pre-Merge Snapshot" + }, + "reversible": { + "type": "boolean", + "description": "Whether this operation can still be reversed (false once reversed or the window has elapsed).", + "default": true, + "facetable": true, + "title": "Reversible" + }, + "mergedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp the merge was executed.", + "title": "Merged At" + }, + "reversedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp the merge was reversed, when applicable.", + "title": "Reversed At" + }, + "reversedBy": { + "type": "string", + "description": "Acting user uid that reversed the merge, when applicable.", + "maxLength": 255, + "title": "Reversed By" + } + } + } + } + } +} diff --git a/lib/Settings/n8n_workflows.openregister.json b/lib/Settings/n8n_workflows.openregister.json index dc5d6ed9d3..65d8fdb1d7 100644 --- a/lib/Settings/n8n_workflows.openregister.json +++ b/lib/Settings/n8n_workflows.openregister.json @@ -52,27 +52,33 @@ "required": ["title", "workflowId"], "properties": { "title": { + "title": "Workflow Title", "type": "string", "description": "The title of the workflow", "minLength": 1, "maxLength": 255 }, "workflowId": { + "title": "Workflow ID", "type": "string", "description": "The n8n workflow ID", "pattern": "^[a-zA-Z0-9-_]+$" }, "description": { + "title": "Description", "type": "string", "description": "Description of what the workflow does" }, "active": { + "title": "Active", "type": "boolean", "description": "Whether the workflow is currently active", "default": false }, "tags": { + "title": "Tags", "type": "array", + "description": "Labels used to categorise and filter the workflow.", "items": {"type": "string"} } } @@ -83,12 +89,25 @@ "type": "object", "required": ["workflowId", "type"], "properties": { - "workflowId": {"type": "string", "format": "uuid"}, + "workflowId": { + "title": "Workflow ID", + "type": "string", + "format": "uuid", + "$ref": "workflow", + "description": "UUID reference to the workflow this trigger is associated with." + }, "type": { + "title": "Trigger Type", "type": "string", + "description": "The mechanism that initiates the workflow execution.", "enum": ["webhook", "schedule", "event", "manual"] }, - "active": {"type": "boolean", "default": true} + "active": { + "title": "Active", + "type": "boolean", + "description": "Whether this trigger is currently enabled.", + "default": true + } } }, "webhook": { @@ -97,12 +116,25 @@ "type": "object", "required": ["path", "method", "workflowId"], "properties": { - "path": {"type": "string", "pattern": "^/[a-zA-Z0-9/_-]+$"}, + "path": { + "title": "Webhook Path", + "type": "string", + "description": "The URL path at which the webhook endpoint is exposed.", + "pattern": "^/[a-zA-Z0-9/_-]+$" + }, "method": { + "title": "HTTP Method", "type": "string", + "description": "The HTTP method the webhook listens for.", "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"] }, - "workflowId": {"type": "string", "format": "uuid"} + "workflowId": { + "title": "Workflow ID", + "type": "string", + "format": "uuid", + "$ref": "workflow", + "description": "UUID reference to the workflow triggered by this webhook." + } } }, "schedule": { @@ -112,12 +144,30 @@ "required": ["cronExpression", "workflowId"], "properties": { "cronExpression": { + "title": "Cron Expression", "type": "string", + "description": "Standard five-field cron expression defining when the workflow runs.", "pattern": "^[0-9*,/-]+ [0-9*,/-]+ [0-9*,/-]+ [0-9*,/-]+ [0-9*,/-]+$" }, - "workflowId": {"type": "string", "format": "uuid"}, - "timezone": {"type": "string", "default": "UTC"}, - "active": {"type": "boolean", "default": true} + "workflowId": { + "title": "Workflow ID", + "type": "string", + "format": "uuid", + "$ref": "workflow", + "description": "UUID reference to the workflow executed on this schedule." + }, + "timezone": { + "title": "Timezone", + "type": "string", + "description": "IANA timezone name used to interpret the cron expression.", + "default": "UTC" + }, + "active": { + "title": "Active", + "type": "boolean", + "description": "Whether this scheduled trigger is currently enabled.", + "default": true + } } }, "notification": { @@ -126,14 +176,26 @@ "type": "object", "required": ["title", "channel"], "properties": { - "title": {"type": "string"}, + "title": { + "title": "Notification Title", + "type": "string", + "description": "The subject or heading displayed in the notification." + }, "channel": { + "title": "Delivery Channel", "type": "string", + "description": "The medium through which the notification is delivered.", "enum": ["email", "slack", "webhook", "sms", "push"] }, - "template": {"type": "string"}, + "template": { + "title": "Message Template", + "type": "string", + "description": "Template string used to render the notification body." + }, "recipients": { + "title": "Recipients", "type": "array", + "description": "List of addresses or identifiers that receive the notification.", "items": {"type": "string"} } } diff --git a/lib/Settings/ori_register.json b/lib/Settings/ori_register.json index 9e3410bb58..26f62a5e81 100644 --- a/lib/Settings/ori_register.json +++ b/lib/Settings/ori_register.json @@ -37,6 +37,7 @@ "title": "Vergadering", "version": "1.0.0", "published": "2025-01-01T00:00:00+00:00", + "x-schema-org": "schema:Event", "summary": "A council meeting or committee session", "description": "Represents a meeting of the municipal council or one of its committees. Contains scheduling information, location, and organizational references aligned with the VNG ODS-Open-Raadsinformatie specification.", "required": [ @@ -49,7 +50,8 @@ "type": "string", "description": "Name or title of the meeting", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Meeting Name" }, "type": { "type": "string", @@ -60,7 +62,8 @@ "informatiebijeenkomst", "hoorzitting" ], - "facetable": true + "facetable": true, + "title": "Meeting Type" }, "status": { "type": "string", @@ -70,41 +73,49 @@ "bevestigd", "afgelast" ], - "facetable": true + "facetable": true, + "title": "Status" }, "startDatum": { "type": "string", "format": "date-time", - "description": "Start date and time of the meeting" + "description": "Start date and time of the meeting", + "title": "Start Date" }, "eindDatum": { "type": "string", "format": "date-time", - "description": "End date and time of the meeting" + "description": "End date and time of the meeting", + "title": "End Date" }, "locatie": { "type": "string", "description": "Location where the meeting takes place", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Location" }, "organisatie": { "type": "string", "description": "Organization reference (municipality or body)", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Organisation" }, "commissie": { "type": "string", "description": "Committee name if this is a committee meeting", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Committee" } }, "searchable": true, "hardValidation": false, "authorization": { - "read": ["public"] + "read": [ + "public" + ] } }, "agendapunt": { @@ -112,6 +123,7 @@ "title": "Agendapunt", "version": "1.0.0", "published": "2025-01-01T00:00:00+00:00", + "x-schema-org": "schema:Thing", "summary": "An agenda item within a council meeting", "description": "Represents a single item on the agenda of a meeting. Contains the subject, description, ordering position, and references to the parent meeting and any related documents.", "required": [ @@ -124,39 +136,47 @@ "type": "string", "description": "Subject or title of the agenda item", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Subject" }, "omschrijving": { "type": "string", "description": "Detailed description of the agenda item", - "maxLength": 2000 + "maxLength": 2000, + "title": "Description" }, "volgorde": { "type": "integer", "description": "Position/order on the agenda", - "minimum": 1 + "minimum": 1, + "title": "Order" }, "vergadering": { "type": "string", "description": "Reference (slug) to the parent vergadering", - "facetable": true + "facetable": true, + "title": "Meeting" }, "bovenliggendAgendapunt": { "type": "string", - "description": "Reference to a parent agenda item for sub-items" + "description": "Reference to a parent agenda item for sub-items", + "title": "Parent Agenda Item" }, "bijlagen": { "type": "array", "items": { "type": "string" }, - "description": "References to related raadsdocumenten" + "description": "References to related raadsdocumenten", + "title": "Attachments" } }, "searchable": true, "hardValidation": false, "authorization": { - "read": ["public"] + "read": [ + "public" + ] } }, "raadsdocument": { @@ -164,6 +184,7 @@ "title": "Raadsdocument", "version": "1.0.0", "published": "2025-01-01T00:00:00+00:00", + "x-schema-org": "schema:DigitalDocument", "summary": "A document related to council proceedings", "description": "Represents a document within the council information system, such as motions, amendments, decisions, letters, reports, or minutes.", "required": [ @@ -175,7 +196,8 @@ "type": "string", "description": "Title of the document", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Title" }, "type": { "type": "string", @@ -188,40 +210,48 @@ "rapport", "notulen" ], - "facetable": true + "facetable": true, + "title": "Document Type" }, "classificatie": { "type": "string", "description": "Category or classification of the document", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Classification" }, "url": { "type": "string", "description": "URL where the document can be accessed", "format": "uri", - "maxLength": 500 + "maxLength": 500, + "title": "Document URL" }, "bestandsnaam": { "type": "string", "description": "File name of the document", - "maxLength": 255 + "maxLength": 255, + "title": "File Name" }, "bestandsgrootte": { "type": "integer", "description": "File size in bytes", - "minimum": 0 + "minimum": 0, + "title": "File Size" }, "inhoudType": { "type": "string", "description": "MIME type of the document", - "maxLength": 100 + "maxLength": 100, + "title": "Content Type" } }, "searchable": true, "hardValidation": false, "authorization": { - "read": ["public"] + "read": [ + "public" + ] } }, "stemming": { @@ -229,6 +259,7 @@ "title": "Stemming", "version": "1.0.0", "published": "2025-01-01T00:00:00+00:00", + "x-schema-org": "schema:VoteAction", "summary": "A vote on a council proposal", "description": "Represents a formal vote taken during a council meeting. Contains the subject, result, vote counts, and per-party breakdown of the voting results.", "required": [ @@ -242,13 +273,15 @@ "type": "string", "description": "Subject that was voted on", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Vote Subject" }, "type": { "type": "string", "description": "Type of vote (e.g. voorstel, motie, amendement)", "maxLength": 100, - "facetable": true + "facetable": true, + "title": "Vote Type" }, "resultaat": { "type": "string", @@ -257,27 +290,32 @@ "aangenomen", "verworpen" ], - "facetable": true + "facetable": true, + "title": "Result" }, "agendapunt": { "type": "string", "description": "Reference (slug) to the agenda item this vote belongs to", - "facetable": true + "facetable": true, + "title": "Agenda Item" }, "stemmenVoor": { "type": "integer", "description": "Number of votes in favor", - "minimum": 0 + "minimum": 0, + "title": "Votes In Favour" }, "stemmenTegen": { "type": "integer", "description": "Number of votes against", - "minimum": 0 + "minimum": 0, + "title": "Votes Against" }, "onthoudingen": { "type": "integer", "description": "Number of abstentions", - "minimum": 0 + "minimum": 0, + "title": "Abstentions" }, "fractieResultaten": { "type": "array", @@ -287,25 +325,35 @@ "properties": { "fractie": { "type": "string", - "description": "Party name" + "description": "Party name", + "title": "Party Name" }, "stem": { "type": "string", "description": "Vote cast by this party", - "enum": ["voor", "tegen", "onthouding"] + "enum": [ + "voor", + "tegen", + "onthouding" + ], + "title": "Vote Cast" }, "zetels": { "type": "integer", - "description": "Number of seats this party holds" + "description": "Number of seats this party holds", + "title": "Seats" } } - } + }, + "title": "Party Results" } }, "searchable": true, "hardValidation": false, "authorization": { - "read": ["public"] + "read": [ + "public" + ] } }, "raadslid": { @@ -313,24 +361,35 @@ "title": "Raadslid", "version": "1.0.0", "published": "2025-01-01T00:00:00+00:00", + "x-schema-org": "schema:Person", "summary": "A member of the municipal council or executive board", - "description": "Represents a person serving in the municipal council, college, or council administration. Includes council members, aldermen, the mayor, and the clerk.", + "description": "Represents a person serving in the municipal council, college, or council administration. Includes council members, aldermen, the mayor, and the clerk. A raadslid IS a Person playing a council role: identity should be sourced from a canonical Person provider via the `person` reference, while this schema only overlays the role (fractie, functie, actief).", "required": [ "naam", "fractie", "functie" ], "properties": { + "person": { + "type": "string", + "format": "uuid", + "title": "Person", + "description": "The person who holds this council seat. Cross-app reference (ADR-048) to a schema.org/Person provider (e.g. a contact/addressbook person) when available.", + "referenceSemanticType": "https://schema.org/Person", + "referenceSemanticApp": "contacts" + }, "naam": { "type": "string", - "description": "Full name of the council member", + "description": "Denormalised display copy of the referenced person's full name, sourced from the `person` reference. Kept for display and backward compatibility with existing objects; the canonical identity lives on the referenced Person.", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Full Name" }, "fractie": { "type": "string", "description": "Reference (slug) to the party/faction this member belongs to", - "facetable": true + "facetable": true, + "title": "Party" }, "functie": { "type": "string", @@ -341,18 +400,22 @@ "burgemeester", "griffier" ], - "facetable": true + "facetable": true, + "title": "Function" }, "actief": { "type": "boolean", "description": "Whether this member is currently active", - "facetable": true + "facetable": true, + "title": "Active" } }, "searchable": true, "hardValidation": false, "authorization": { - "read": ["public"] + "read": [ + "public" + ] } }, "fractie": { @@ -360,6 +423,7 @@ "title": "Fractie", "version": "1.0.0", "published": "2025-01-01T00:00:00+00:00", + "x-schema-org": "schema:Organization", "summary": "A political party or faction in the municipal council", "description": "Represents a political party or faction with seats in the municipal council. Contains the party name, number of seats, and coalition/opposition classification.", "required": [ @@ -371,12 +435,14 @@ "type": "string", "description": "Name of the party or faction", "maxLength": 255, - "facetable": true + "facetable": true, + "title": "Party Name" }, "zetels": { "type": "integer", "description": "Number of seats in the council", - "minimum": 0 + "minimum": 0, + "title": "Seats" }, "classificatie": { "type": "string", @@ -385,13 +451,16 @@ "coalitiepartij", "oppositiepartij" ], - "facetable": true + "facetable": true, + "title": "Council Role" } }, "searchable": true, "hardValidation": false, "authorization": { - "read": ["public"] + "read": [ + "public" + ] } } }, @@ -1706,14 +1775,46 @@ "stemmenTegen": 16, "onthoudingen": 0, "fractieResultaten": [ - { "fractie": "Voorbeeldstad Vooruit", "stem": "voor", "zetels": 8 }, - { "fractie": "Groen Links Voorbeeldstad", "stem": "voor", "zetels": 6 }, - { "fractie": "Democraten Voorbeeldstad", "stem": "voor", "zetels": 5 }, - { "fractie": "Lokaal Belang", "stem": "tegen", "zetels": 5 }, - { "fractie": "PvdA Voorbeeldstad", "stem": "tegen", "zetels": 4 }, - { "fractie": "VVD Voorbeeldstad", "stem": "tegen", "zetels": 3 }, - { "fractie": "SP Voorbeeldstad", "stem": "tegen", "zetels": 2 }, - { "fractie": "Forum Voorbeeldstad", "stem": "tegen", "zetels": 2 } + { + "fractie": "Voorbeeldstad Vooruit", + "stem": "voor", + "zetels": 8 + }, + { + "fractie": "Groen Links Voorbeeldstad", + "stem": "voor", + "zetels": 6 + }, + { + "fractie": "Democraten Voorbeeldstad", + "stem": "voor", + "zetels": 5 + }, + { + "fractie": "Lokaal Belang", + "stem": "tegen", + "zetels": 5 + }, + { + "fractie": "PvdA Voorbeeldstad", + "stem": "tegen", + "zetels": 4 + }, + { + "fractie": "VVD Voorbeeldstad", + "stem": "tegen", + "zetels": 3 + }, + { + "fractie": "SP Voorbeeldstad", + "stem": "tegen", + "zetels": 2 + }, + { + "fractie": "Forum Voorbeeldstad", + "stem": "tegen", + "zetels": 2 + } ] }, { @@ -1730,14 +1831,46 @@ "stemmenTegen": 10, "onthoudingen": 0, "fractieResultaten": [ - { "fractie": "Voorbeeldstad Vooruit", "stem": "voor", "zetels": 8 }, - { "fractie": "Groen Links Voorbeeldstad", "stem": "voor", "zetels": 6 }, - { "fractie": "Democraten Voorbeeldstad", "stem": "voor", "zetels": 5 }, - { "fractie": "Lokaal Belang", "stem": "tegen", "zetels": 5 }, - { "fractie": "PvdA Voorbeeldstad", "stem": "voor", "zetels": 4 }, - { "fractie": "VVD Voorbeeldstad", "stem": "tegen", "zetels": 3 }, - { "fractie": "SP Voorbeeldstad", "stem": "voor", "zetels": 2 }, - { "fractie": "Forum Voorbeeldstad", "stem": "tegen", "zetels": 2 } + { + "fractie": "Voorbeeldstad Vooruit", + "stem": "voor", + "zetels": 8 + }, + { + "fractie": "Groen Links Voorbeeldstad", + "stem": "voor", + "zetels": 6 + }, + { + "fractie": "Democraten Voorbeeldstad", + "stem": "voor", + "zetels": 5 + }, + { + "fractie": "Lokaal Belang", + "stem": "tegen", + "zetels": 5 + }, + { + "fractie": "PvdA Voorbeeldstad", + "stem": "voor", + "zetels": 4 + }, + { + "fractie": "VVD Voorbeeldstad", + "stem": "tegen", + "zetels": 3 + }, + { + "fractie": "SP Voorbeeldstad", + "stem": "voor", + "zetels": 2 + }, + { + "fractie": "Forum Voorbeeldstad", + "stem": "tegen", + "zetels": 2 + } ] }, { @@ -1754,14 +1887,46 @@ "stemmenTegen": 12, "onthoudingen": 0, "fractieResultaten": [ - { "fractie": "Voorbeeldstad Vooruit", "stem": "voor", "zetels": 8 }, - { "fractie": "Groen Links Voorbeeldstad", "stem": "voor", "zetels": 6 }, - { "fractie": "Democraten Voorbeeldstad", "stem": "voor", "zetels": 5 }, - { "fractie": "Lokaal Belang", "stem": "tegen", "zetels": 5 }, - { "fractie": "PvdA Voorbeeldstad", "stem": "voor", "zetels": 4 }, - { "fractie": "VVD Voorbeeldstad", "stem": "tegen", "zetels": 3 }, - { "fractie": "SP Voorbeeldstad", "stem": "tegen", "zetels": 2 }, - { "fractie": "Forum Voorbeeldstad", "stem": "tegen", "zetels": 2 } + { + "fractie": "Voorbeeldstad Vooruit", + "stem": "voor", + "zetels": 8 + }, + { + "fractie": "Groen Links Voorbeeldstad", + "stem": "voor", + "zetels": 6 + }, + { + "fractie": "Democraten Voorbeeldstad", + "stem": "voor", + "zetels": 5 + }, + { + "fractie": "Lokaal Belang", + "stem": "tegen", + "zetels": 5 + }, + { + "fractie": "PvdA Voorbeeldstad", + "stem": "voor", + "zetels": 4 + }, + { + "fractie": "VVD Voorbeeldstad", + "stem": "tegen", + "zetels": 3 + }, + { + "fractie": "SP Voorbeeldstad", + "stem": "tegen", + "zetels": 2 + }, + { + "fractie": "Forum Voorbeeldstad", + "stem": "tegen", + "zetels": 2 + } ] }, { @@ -1778,14 +1943,46 @@ "stemmenTegen": 25, "onthoudingen": 0, "fractieResultaten": [ - { "fractie": "Voorbeeldstad Vooruit", "stem": "tegen", "zetels": 8 }, - { "fractie": "Groen Links Voorbeeldstad", "stem": "tegen", "zetels": 6 }, - { "fractie": "Democraten Voorbeeldstad", "stem": "tegen", "zetels": 5 }, - { "fractie": "Lokaal Belang", "stem": "voor", "zetels": 5 }, - { "fractie": "PvdA Voorbeeldstad", "stem": "tegen", "zetels": 4 }, - { "fractie": "VVD Voorbeeldstad", "stem": "voor", "zetels": 3 }, - { "fractie": "SP Voorbeeldstad", "stem": "tegen", "zetels": 2 }, - { "fractie": "Forum Voorbeeldstad", "stem": "voor", "zetels": 2 } + { + "fractie": "Voorbeeldstad Vooruit", + "stem": "tegen", + "zetels": 8 + }, + { + "fractie": "Groen Links Voorbeeldstad", + "stem": "tegen", + "zetels": 6 + }, + { + "fractie": "Democraten Voorbeeldstad", + "stem": "tegen", + "zetels": 5 + }, + { + "fractie": "Lokaal Belang", + "stem": "voor", + "zetels": 5 + }, + { + "fractie": "PvdA Voorbeeldstad", + "stem": "tegen", + "zetels": 4 + }, + { + "fractie": "VVD Voorbeeldstad", + "stem": "voor", + "zetels": 3 + }, + { + "fractie": "SP Voorbeeldstad", + "stem": "tegen", + "zetels": 2 + }, + { + "fractie": "Forum Voorbeeldstad", + "stem": "voor", + "zetels": 2 + } ] }, { @@ -1802,14 +1999,46 @@ "stemmenTegen": 0, "onthoudingen": 0, "fractieResultaten": [ - { "fractie": "Voorbeeldstad Vooruit", "stem": "voor", "zetels": 8 }, - { "fractie": "Groen Links Voorbeeldstad", "stem": "voor", "zetels": 6 }, - { "fractie": "Democraten Voorbeeldstad", "stem": "voor", "zetels": 5 }, - { "fractie": "Lokaal Belang", "stem": "voor", "zetels": 5 }, - { "fractie": "PvdA Voorbeeldstad", "stem": "voor", "zetels": 4 }, - { "fractie": "VVD Voorbeeldstad", "stem": "voor", "zetels": 3 }, - { "fractie": "SP Voorbeeldstad", "stem": "voor", "zetels": 2 }, - { "fractie": "Forum Voorbeeldstad", "stem": "voor", "zetels": 2 } + { + "fractie": "Voorbeeldstad Vooruit", + "stem": "voor", + "zetels": 8 + }, + { + "fractie": "Groen Links Voorbeeldstad", + "stem": "voor", + "zetels": 6 + }, + { + "fractie": "Democraten Voorbeeldstad", + "stem": "voor", + "zetels": 5 + }, + { + "fractie": "Lokaal Belang", + "stem": "voor", + "zetels": 5 + }, + { + "fractie": "PvdA Voorbeeldstad", + "stem": "voor", + "zetels": 4 + }, + { + "fractie": "VVD Voorbeeldstad", + "stem": "voor", + "zetels": 3 + }, + { + "fractie": "SP Voorbeeldstad", + "stem": "voor", + "zetels": 2 + }, + { + "fractie": "Forum Voorbeeldstad", + "stem": "voor", + "zetels": 2 + } ] }, { @@ -1826,14 +2055,46 @@ "stemmenTegen": 8, "onthoudingen": 0, "fractieResultaten": [ - { "fractie": "Voorbeeldstad Vooruit", "stem": "voor", "zetels": 8 }, - { "fractie": "Groen Links Voorbeeldstad", "stem": "voor", "zetels": 6 }, - { "fractie": "Democraten Voorbeeldstad", "stem": "voor", "zetels": 5 }, - { "fractie": "Lokaal Belang", "stem": "tegen", "zetels": 5 }, - { "fractie": "PvdA Voorbeeldstad", "stem": "voor", "zetels": 4 }, - { "fractie": "VVD Voorbeeldstad", "stem": "voor", "zetels": 3 }, - { "fractie": "SP Voorbeeldstad", "stem": "tegen", "zetels": 2 }, - { "fractie": "Forum Voorbeeldstad", "stem": "tegen", "zetels": 1 } + { + "fractie": "Voorbeeldstad Vooruit", + "stem": "voor", + "zetels": 8 + }, + { + "fractie": "Groen Links Voorbeeldstad", + "stem": "voor", + "zetels": 6 + }, + { + "fractie": "Democraten Voorbeeldstad", + "stem": "voor", + "zetels": 5 + }, + { + "fractie": "Lokaal Belang", + "stem": "tegen", + "zetels": 5 + }, + { + "fractie": "PvdA Voorbeeldstad", + "stem": "voor", + "zetels": 4 + }, + { + "fractie": "VVD Voorbeeldstad", + "stem": "voor", + "zetels": 3 + }, + { + "fractie": "SP Voorbeeldstad", + "stem": "tegen", + "zetels": 2 + }, + { + "fractie": "Forum Voorbeeldstad", + "stem": "tegen", + "zetels": 1 + } ] } ] diff --git a/lib/Settings/register.d/README.md b/lib/Settings/register.d/README.md new file mode 100644 index 0000000000..d033a22a55 --- /dev/null +++ b/lib/Settings/register.d/README.md @@ -0,0 +1,27 @@ +# ADR-037: modular register fragments + +This directory is the drop-zone for per-OpenSpec-change register fragments +(`*.json`) that would otherwise edit a shared `*_register.json` monolith, +so concurrent same-app builds touch disjoint files and never collide on a +merge. + +## Status for OpenRegister + +OpenRegister is the configuration *platform* itself: it exposes +`ConfigurationService::importFromApp()` / `importFromFilePath()` for **other** +apps to import their `{app}_register.json` data models. OpenRegister does not +read its own register monolith and feed it to `importFromApp` for its own data +model (the local `lib/Settings/*_register.json` files are integration register +descriptors fetched/imported through separate flows). There is therefore no +single PHP load-and-import site to wrap with a `deepMergeConfig()` fragment +merge here. + +The PHP-side fragment merge (the `deepMergeConfig()` + `register.d/*.json` glob +that consumer apps such as shillinq carry — see shillinq#171) is consequently +**not wired** in OpenRegister. This directory is kept as the convention anchor: +if a future change introduces a self-imported register monolith, drop fragments +here and add the `deepMergeConfig` merge in the loading service, mirroring the +shillinq `SettingsService::loadRegisterConfigData()` reference. + +The manifest side (`src/manifest.d/*.json` merged in `src/main.js` via +`mergeManifestFragments()`) **is** enabled. diff --git a/lib/Settings/trust_configuration_register.json b/lib/Settings/trust_configuration_register.json new file mode 100644 index 0000000000..4d13c70951 --- /dev/null +++ b/lib/Settings/trust_configuration_register.json @@ -0,0 +1,173 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "MDM Trust Configuration", + "description": "Generic, entity-type-agnostic trust-tier configuration register consumed by the x-openregister-survivorship golden-record engine. Rows declare a trust tier for an (entityType, attribute, sourceSystem) tuple, with optional freshness decay and effectiveFrom versioning. RBAC-scoped and auditable via OpenRegister's object infrastructure (ADR-022) rather than frozen inline in a schema annotation.", + "version": "1.0.0" + }, + "x-openregister": { + "type": "core", + "app": "openregister", + "openregister": "^v0.2.10", + "description": "Foundational MDM trust-configuration register shipped by OpenRegister for the survivorship engine (x-openregister-survivorship)." + }, + "paths": {}, + "components": { + "registers": { + "trust-configuration": { + "slug": "trust-configuration", + "title": "Trust Configuration", + "version": "1.0.0", + "description": "Generic (entityType, attribute, sourceSystem) trust-tier rows used by the survivorship golden-record engine.", + "published": "2026-01-01T00:00:00+00:00", + "schemas": [ + "trustConfiguration" + ], + "folder": "Open Registers/MDM Trust Configuration" + } + }, + "schemas": { + "trustConfiguration": { + "slug": "trustConfiguration", + "title": "Trust Configuration", + "version": "1.0.0", + "published": "2026-01-01T00:00:00+00:00", + "summary": "A trust-tier rule for one (entityType, attribute, sourceSystem) tuple.", + "description": "One row declares which trust tier a source system's value earns for a given entity type + attribute, optionally with a freshness-decay window and an effectiveFrom date for versioning. TrustTierResolver looks up the most recent row whose effectiveFrom is on/before the as-of date; when no row matches, the caller falls back to the annotation's defaultTier.", + "required": [ + "entityType", + "attribute", + "sourceSystem", + "trustTier" + ], + "properties": { + "entityType": { + "type": "string", + "description": "The master-record entity type this rule applies to (e.g. 'organisation'). Never hardcoded by the engine.", + "maxLength": 255, + "facetable": true, + "title": "Entity Type" + }, + "attribute": { + "type": "string", + "description": "The attribute name this rule applies to (e.g. 'legalName', 'email').", + "maxLength": 255, + "facetable": true, + "title": "Attribute" + }, + "sourceSystem": { + "type": "string", + "description": "Slug of the source system this rule applies to (e.g. 'municipal-registry').", + "maxLength": 255, + "facetable": true, + "title": "Source System" + }, + "trustTier": { + "type": "string", + "description": "The trust tier this source earns for this attribute.", + "enum": [ + "gold", + "silver", + "bronze", + "discard" + ], + "facetable": true, + "title": "Trust Tier" + }, + "freshnessDecayDays": { + "type": "integer", + "description": "Days after which a source's value for this attribute steps one tier down (freshness decay). Omit/null for no decay.", + "minimum": 1, + "title": "Freshness Decay (days)" + }, + "effectiveFrom": { + "type": "string", + "format": "date", + "description": "Date this rule takes effect. Among matching rows, the most recent effectiveFrom on/before the as-of date wins (time-versioned).", + "title": "Effective From" + } + } + } + }, + "objects": [ + { + "@self": { + "register": "trust-configuration", + "schema": "trustConfiguration", + "slug": "trust-organisation-legalname-municipal-registry" + }, + "entityType": "organisation", + "attribute": "legalName", + "sourceSystem": "municipal-registry", + "trustTier": "gold", + "freshnessDecayDays": 365, + "effectiveFrom": "2026-01-01" + }, + { + "@self": { + "register": "trust-configuration", + "schema": "trustConfiguration", + "slug": "trust-organisation-legalname-consultancy-crm" + }, + "entityType": "organisation", + "attribute": "legalName", + "sourceSystem": "consultancy-crm", + "trustTier": "silver", + "freshnessDecayDays": 180, + "effectiveFrom": "2026-01-01" + }, + { + "@self": { + "register": "trust-configuration", + "schema": "trustConfiguration", + "slug": "trust-organisation-legalname-travel-agency-booking" + }, + "entityType": "organisation", + "attribute": "legalName", + "sourceSystem": "travel-agency-booking", + "trustTier": "bronze", + "freshnessDecayDays": 90, + "effectiveFrom": "2026-01-01" + }, + { + "@self": { + "register": "trust-configuration", + "schema": "trustConfiguration", + "slug": "trust-organisation-email-consultancy-crm" + }, + "entityType": "organisation", + "attribute": "email", + "sourceSystem": "consultancy-crm", + "trustTier": "gold", + "freshnessDecayDays": 120, + "effectiveFrom": "2026-01-01" + }, + { + "@self": { + "register": "trust-configuration", + "schema": "trustConfiguration", + "slug": "trust-organisation-email-municipal-registry" + }, + "entityType": "organisation", + "attribute": "email", + "sourceSystem": "municipal-registry", + "trustTier": "silver", + "freshnessDecayDays": 365, + "effectiveFrom": "2026-01-01" + }, + { + "@self": { + "register": "trust-configuration", + "schema": "trustConfiguration", + "slug": "trust-organisation-phone-travel-agency-booking" + }, + "entityType": "organisation", + "attribute": "phone", + "sourceSystem": "travel-agency-booking", + "trustTier": "silver", + "freshnessDecayDays": 60, + "effectiveFrom": "2026-01-01" + } + ] + } +} diff --git a/lib/Tool/AbstractTool.php b/lib/Tool/AbstractTool.php index b06a026f12..faff46a0e5 100644 --- a/lib/Tool/AbstractTool.php +++ b/lib/Tool/AbstractTool.php @@ -91,7 +91,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function setAgent(?Agent $agent): void { @@ -110,7 +110,7 @@ public function setAgent(?Agent $agent): void * * @return string|null User ID or null if no user context * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function getUserId(?string $explicitUserId=null): ?string { @@ -140,7 +140,7 @@ protected function getUserId(?string $explicitUserId=null): ?string * * @return bool True if user context is available * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function hasUserContext(?string $explicitUserId=null): bool { @@ -157,7 +157,7 @@ protected function hasUserContext(?string $explicitUserId=null): bool * * @return array Query parameters with view filters applied * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function applyViewFilters(array $params): array { @@ -187,7 +187,7 @@ protected function applyViewFilters(array $params): array * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function formatSuccess($data, string $message='Success'): array { @@ -208,7 +208,7 @@ protected function formatSuccess($data, string $message='Success'): array * * @psalm-return array{success: false, error: string, details?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function formatError(string $message, $details=null): array { @@ -240,7 +240,7 @@ protected function formatError(string $message, $details=null): array * * @psalm-suppress PossiblyNullArgument * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function log(string $functionName, array $parameters, string $level='info', string $message=''): void { @@ -307,7 +307,7 @@ protected function log(string $functionName, array $parameters, string $level='i * * @throws \InvalidArgumentException If any required parameter is missing * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ protected function validateParameters(array $parameters, array $required): void { @@ -349,7 +349,7 @@ protected function validateParameters(array $parameters, array $required): void * @SuppressWarnings(PHPMD.NPathComplexity) Multiple type conversion paths * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive type handling * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __call(string $name, array $arguments) { diff --git a/lib/Tool/AgentTool.php b/lib/Tool/AgentTool.php index b67d844e49..ae0a0eeb26 100644 --- a/lib/Tool/AgentTool.php +++ b/lib/Tool/AgentTool.php @@ -73,7 +73,7 @@ public function __construct( * * @psalm-return 'Agent Management' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getName(): string { @@ -85,7 +85,7 @@ public function getName(): string * * @return string The tool description * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getDescription(): string { @@ -103,7 +103,7 @@ public function getDescription(): string * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive function definitions for LLM * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFunctions(): array { @@ -230,7 +230,7 @@ public function getFunctions(): array * * @psalm-return array{success: bool, error?: string, details?: mixed, message?: string, data?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function listAgents(int $limit=50, int $offset=0): array { @@ -283,7 +283,7 @@ public function listAgents(int $limit=50, int $offset=0): array * * @psalm-return array{success: bool, error?: string, details?: mixed, message?: string, data?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getAgent(string $uuid): array { @@ -330,7 +330,7 @@ public function getAgent(string $uuid): array * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Optional nullable parameters for agent creation * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createAgent( string $name, @@ -400,7 +400,7 @@ public function createAgent( * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Optional nullable parameters for partial updates * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function updateAgent( string $uuid, @@ -462,7 +462,7 @@ public function updateAgent( * * @psalm-return array{success: bool, error?: string, details?: mixed, message?: string, data?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function deleteAgent(string $uuid): array { @@ -508,7 +508,7 @@ public function deleteAgent(string $uuid): array * * @return array Response * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function executeFunction(string $functionName, array $parameters, ?string $userId=null): array { diff --git a/lib/Tool/ApplicationTool.php b/lib/Tool/ApplicationTool.php index ce82b44396..0f0a986f58 100644 --- a/lib/Tool/ApplicationTool.php +++ b/lib/Tool/ApplicationTool.php @@ -75,7 +75,7 @@ public function __construct( * * @psalm-return 'Application Management' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getName(): string { @@ -89,7 +89,7 @@ public function getName(): string * * @psalm-return 'Manage applications: list, view, create, update, or delete with RBAC permissions.' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getDescription(): string { @@ -104,7 +104,7 @@ public function getDescription(): string * * @return array> Array of function definitions * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFunctions(): array { @@ -216,7 +216,7 @@ public function getFunctions(): array * * @psalm-return array{success: bool, error?: string, details?: mixed, message?: string, data?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function listApplications(int $limit=50, int $offset=0): array { @@ -269,7 +269,7 @@ public function listApplications(int $limit=50, int $offset=0): array * * @psalm-return array{success: bool, error?: string, details?: mixed, message?: string, data?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getApplication(string $uuid): array { @@ -313,7 +313,7 @@ public function getApplication(string $uuid): array * * @psalm-return array{success: bool, error?: string, details?: mixed, message?: string, data?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createApplication( string $name, @@ -366,7 +366,7 @@ public function createApplication( * * @psalm-return array{success: bool, error?: string, details?: mixed, message?: string, data?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function updateApplication( string $uuid, @@ -424,7 +424,7 @@ public function updateApplication( * * @psalm-return array{success: bool, error?: string, details?: mixed, message?: string, data?: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function deleteApplication(string $uuid): array { @@ -470,7 +470,7 @@ public function deleteApplication(string $uuid): array * * @return array Response * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function executeFunction(string $functionName, array $parameters, ?string $userId=null): array { diff --git a/lib/Tool/McpProviderBridge.php b/lib/Tool/McpProviderBridge.php index 27a101ada9..5cc0c49cfb 100644 --- a/lib/Tool/McpProviderBridge.php +++ b/lib/Tool/McpProviderBridge.php @@ -33,10 +33,14 @@ namespace OCA\OpenRegister\Tool; use OCA\OpenRegister\Mcp\IMcpToolProvider; +use OCA\OpenRegister\Service\Mcp\McpAnnotationValidator; use Psr\Log\LoggerInterface; /** * Adapter from IMcpToolProvider to ToolInterface. + * + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-002 — Both serving surfaces are fed from one derivation) */ class McpProviderBridge implements ToolInterface { @@ -68,7 +72,7 @@ class McpProviderBridge implements ToolInterface * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-4 + * @spec openspec/specs/ai-mcp/spec.md */ public function __construct( private readonly IMcpToolProvider $provider, @@ -83,7 +87,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-4 + * @spec openspec/specs/ai-mcp/spec.md */ public function setOnlyMcpId(string $mcpId): void { @@ -95,7 +99,7 @@ public function setOnlyMcpId(string $mcpId): void * * @return string The provider's appId, used as the tool-group name. * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-4 + * @spec openspec/specs/ai-mcp/spec.md */ public function getName(): string { @@ -109,7 +113,7 @@ public function getName(): string * * @return string Human-readable description of the bridged tool group. * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-4 + * @spec openspec/specs/ai-mcp/spec.md */ public function getDescription(): string { @@ -119,9 +123,21 @@ public function getDescription(): string /** * Each MCP descriptor becomes one LLphant function definition. * + * Additively forwards the ADR-063 annotation hints + * (`readOnlyHint` / `destructiveHint` / `idempotentHint`) and `scope` + * onto the LLphant descriptor when the provider set them, so chat-surface + * consumers reached through {@see \OCA\OpenRegister\Service\Mcp\ToolRegistryFacade::listTools()} + * see the same annotations the JSON-RPC `tools/list` surface already + * carries (previously dropped here — OR#369). A key is omitted entirely + * when the provider descriptor didn't set it; no defaults are invented. + * These hints are ADVISORY UX metadata only — OpenRegister's + * `ObjectService` RBAC enforcement remains the sole authoritative + * invoke-time gate, unaffected by this or any hint value (ADR-063). + * * @return array> LLphant-shaped function descriptors. * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-4 + * @spec openspec/specs/ai-mcp/spec.md + * (Requirement: REQ-DERIVED-002 — Both serving surfaces are fed from one derivation) */ public function getFunctions(): array { @@ -140,12 +156,24 @@ public function getFunctions(): array // expose the raw MCP id as the function name AND rewrite a // safe alias (underscore) so both forms route back the same way. $inputSchema = $descriptor['inputSchema'] ?? ['type' => 'object', 'properties' => []]; - $functions[] = [ + $function = [ 'name' => $this->safeFunctionName(mcpId: $rawId), 'mcpId' => $rawId, 'description' => (string) ($descriptor['description'] ?? $descriptor['name'] ?? $rawId), 'parameters' => $this->sanitiseSchema(schema: $inputSchema), ]; + + foreach (McpAnnotationValidator::HINT_KEYS as $hintKey) { + if (array_key_exists($hintKey, $descriptor) === true) { + $function[$hintKey] = $descriptor[$hintKey]; + } + } + + if (array_key_exists('scope', $descriptor) === true) { + $function['scope'] = $descriptor['scope']; + } + + $functions[] = $function; }//end foreach return $functions; @@ -161,7 +189,7 @@ public function getFunctions(): array * * @return array Sanitised schema. * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-4 + * @spec openspec/specs/ai-mcp/spec.md */ private function sanitiseSchema(array $schema): array { @@ -187,7 +215,7 @@ private function sanitiseSchema(array $schema): array * * @return string The first non-null string type, or `string` as fallback. * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-4 + * @spec openspec/specs/ai-mcp/spec.md */ private function collapseType(array $types): string { @@ -209,7 +237,7 @@ private function collapseType(array $types): string * * @return array MCP-shaped response or error envelope. * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-5 + * @spec openspec/specs/ai-mcp/spec.md */ public function executeFunction(string $functionName, array $parameters, ?string $userId=null): array { @@ -250,7 +278,7 @@ public function executeFunction(string $functionName, array $parameters, ?string * * @return void * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-4 + * @spec openspec/specs/ai-mcp/spec.md */ public function setAgent(?\OCA\OpenRegister\Db\Agent $agent): void { @@ -269,9 +297,10 @@ public function setAgent(?\OCA\OpenRegister\Db\Agent $agent): void * @param string $functionName The function LLPhant resolved. * @param array $args Positional or single-array argument list. * - * @return mixed Whatever executeFunction() returned. + * @return string executeFunction()'s result JSON-encoded (LLPhant requires a string + * tool result — see OR#269). * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-5 + * @spec openspec/specs/ai-mcp/spec.md */ public function __call(string $functionName, array $args): mixed { @@ -284,7 +313,20 @@ public function __call(string $functionName, array $args): mixed $parameters = $args; } - return $this->executeFunction(functionName: $functionName, parameters: $parameters); + $result = $this->executeFunction(functionName: $functionName, parameters: $parameters); + + // LLPhant's tool-call handling requires the tool RESULT as a ?string, not an array + // (OllamaChat::callFunction → new CalledFunction(..., $return) type-hints ?string, + // and OpenAI tool messages are strings too). MCP tools return a structured array, so + // encode it to a JSON string here — the LLM reads the tool output as text. This is + // the fix for OR#269 (array given to CalledFunction $return → agent tool-calls 500). + // executeFunction() still returns the array for direct callers (the MCP server). + $encoded = json_encode($result); + if ($encoded === false) { + return '{"isError":true,"error":"encode_failed","message":"Tool result could not be encoded."}'; + } + + return $encoded; }//end __call() /** @@ -295,7 +337,7 @@ public function __call(string $functionName, array $args): mixed * * @return string Safe function name with dots replaced by underscores. * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-5 + * @spec openspec/specs/ai-mcp/spec.md */ private function safeFunctionName(string $mcpId): string { @@ -311,7 +353,7 @@ private function safeFunctionName(string $mcpId): string * * @return string|null Original MCP id, or null when no match is found. * - * @spec openspec/changes/retrofit-2026-05-24-ai-mcp/tasks.md#task-5 + * @spec openspec/specs/ai-mcp/spec.md */ private function resolveMcpId(string $functionName): ?string { diff --git a/lib/Tool/ObjectsTool.php b/lib/Tool/ObjectsTool.php index 24b5610794..13fe8ca36a 100644 --- a/lib/Tool/ObjectsTool.php +++ b/lib/Tool/ObjectsTool.php @@ -78,7 +78,7 @@ public function __construct( * * @psalm-return 'objects' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getName(): string { @@ -90,7 +90,7 @@ public function getName(): string * * @return string The tool description. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getDescription(): string { @@ -106,7 +106,7 @@ public function getDescription(): string * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive function definitions for LLM * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFunctions(): array { @@ -223,7 +223,7 @@ public function getFunctions(): array * * @throws \Exception If function execution fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function executeFunction(string $functionName, array $parameters, ?string $userId=null): array { @@ -260,7 +260,7 @@ public function executeFunction(string $functionName, array $parameters, ?string * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Optional nullable filter parameters * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function searchObjects( int $limit=20, @@ -327,7 +327,7 @@ function (ObjectEntity $object): array { * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getObject(string $id): array { @@ -365,7 +365,7 @@ public function getObject(string $id): array * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createObject(string $register, string $schema, array $data): array { @@ -409,7 +409,7 @@ public function createObject(string $register, string $schema, array $data): arr * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function updateObject(string $id, array $data): array { @@ -453,7 +453,7 @@ public function updateObject(string $id, array $data): array * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function deleteObject(string $id): array { diff --git a/lib/Tool/RegisterTool.php b/lib/Tool/RegisterTool.php index 15de2e4255..ca12ce73b8 100644 --- a/lib/Tool/RegisterTool.php +++ b/lib/Tool/RegisterTool.php @@ -77,7 +77,7 @@ public function __construct( * * @psalm-return 'register' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getName(): string { @@ -89,7 +89,7 @@ public function getName(): string * * @return string The tool description * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getDescription(): string { @@ -101,7 +101,7 @@ public function getDescription(): string * * @return array> Array of function definitions * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFunctions(): array { @@ -210,7 +210,7 @@ public function getFunctions(): array * * @throws \Exception If function execution fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function executeFunction(string $functionName, array $parameters, ?string $userId=null): array { @@ -244,7 +244,7 @@ public function executeFunction(string $functionName, array $parameters, ?string * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function listRegisters(int $limit=100, int $offset=0): array { @@ -281,7 +281,7 @@ function ($register) { * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getRegister(string $id): array { @@ -316,7 +316,7 @@ public function getRegister(string $id): array * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createRegister(string $title, string $description='', ?string $slug=null): array { @@ -356,7 +356,7 @@ public function createRegister(string $title, string $description='', ?string $s * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function updateRegister(string $id, ?string $title=null, ?string $description=null): array { @@ -398,7 +398,7 @@ public function updateRegister(string $id, ?string $title=null, ?string $descrip * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function deleteRegister(string $id): array { diff --git a/lib/Tool/SchemaTool.php b/lib/Tool/SchemaTool.php index 779e679118..c70faad258 100644 --- a/lib/Tool/SchemaTool.php +++ b/lib/Tool/SchemaTool.php @@ -76,7 +76,7 @@ public function __construct( * * @psalm-return 'schema' * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getName(): string { @@ -88,7 +88,7 @@ public function getName(): string * * @return string The tool description * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getDescription(): string { @@ -104,7 +104,7 @@ public function getDescription(): string * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) Comprehensive function definitions for LLM * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFunctions(): array { @@ -225,7 +225,7 @@ public function getFunctions(): array * * @throws \Exception If function execution fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function executeFunction(string $functionName, array $parameters, ?string $userId=null): array { @@ -260,7 +260,7 @@ public function executeFunction(string $functionName, array $parameters, ?string * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Optional nullable filter parameter * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function listSchemas(int $limit=100, int $offset=0, ?string $register=null): array { @@ -300,7 +300,7 @@ function ($schema) { * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getSchema(string $id): array { @@ -340,7 +340,7 @@ public function getSchema(string $id): array * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function createSchema(string $title, array $properties, string $description='', ?array $required=null): array { @@ -386,7 +386,7 @@ public function createSchema(string $title, array $properties, string $descripti * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) Optional nullable parameters for partial updates * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function updateSchema( string $id, @@ -431,6 +431,14 @@ public function updateSchema( /** * Delete a schema * + * **DELETE SAFETY (runtime-schema-api)**: this is an LLM-invokable schema-destruction + * surface with no controller in front of it. It deliberately does NOT pass + * `force: true` — SchemaMapper::delete() refuses a schema that still holds objects, + * and a model acting on an ambiguous instruction must not be able to orphan a user's + * data. The ValidationException surfaces to the model as a tool error naming the + * object count; deleting the objects is a separate, explicit human decision + * (`DELETE /api/schemas/{id}?deleteObjects=true`). + * * @param string $id Schema ID * * @return (mixed|string|true)[] Result of deletion @@ -439,7 +447,7 @@ public function updateSchema( * * @psalm-return array{success: true, message: string, data: mixed} * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/changes/schema-delete-cascade/specs/runtime-schema-api/spec.md */ public function deleteSchema(string $id): array { diff --git a/lib/Tool/StreamingToolInstanceWrapper.php b/lib/Tool/StreamingToolInstanceWrapper.php index f1bd2c19bd..5de8279d68 100644 --- a/lib/Tool/StreamingToolInstanceWrapper.php +++ b/lib/Tool/StreamingToolInstanceWrapper.php @@ -29,7 +29,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/ai-chat-companion-streaming/specs/chat-ai/spec.md#tool-call-and-tool-result-sse-events + * @spec openspec/specs/chat-ai/spec.md */ declare(strict_types=1); diff --git a/lib/Tool/ToolInterface.php b/lib/Tool/ToolInterface.php index 473144b466..62639fa6f4 100644 --- a/lib/Tool/ToolInterface.php +++ b/lib/Tool/ToolInterface.php @@ -43,7 +43,7 @@ interface ToolInterface * * @return string Tool name (e.g., 'register', 'schema', 'objects') * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getName(): string; @@ -55,7 +55,7 @@ public function getName(): string; * * @return string Tool description for LLM * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getDescription(): string; @@ -83,7 +83,7 @@ public function getDescription(): string; * * @return array Array of function definitions * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFunctions(): array; @@ -101,7 +101,7 @@ public function getFunctions(): array; * * @throws \Exception If function execution fails * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function executeFunction(string $functionName, array $parameters, ?string $userId=null): array; @@ -115,7 +115,7 @@ public function executeFunction(string $functionName, array $parameters, ?string * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-29 + * @spec openspec/specs/object-lifecycle/spec.md */ public function setAgent(?\OCA\OpenRegister\Db\Agent $agent): void; }//end interface diff --git a/lib/Twig/AuthenticationExtension.php b/lib/Twig/AuthenticationExtension.php index cc65f218bd..3cab9a2e12 100644 --- a/lib/Twig/AuthenticationExtension.php +++ b/lib/Twig/AuthenticationExtension.php @@ -9,9 +9,9 @@ * @category Twig * @package OCA\OpenRegister\Twig * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -33,7 +33,7 @@ class AuthenticationExtension extends AbstractExtension * * @return TwigFunction[] Array of TwigFunction instances * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFunctions(): array { diff --git a/lib/Twig/AuthenticationRuntime.php b/lib/Twig/AuthenticationRuntime.php index 83e7c0503d..af88d92b3b 100644 --- a/lib/Twig/AuthenticationRuntime.php +++ b/lib/Twig/AuthenticationRuntime.php @@ -9,9 +9,9 @@ * @category Twig * @package OCA\OpenRegister\Twig * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2026 Conduction B.V. - * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * * @link https://OpenRegister.app */ @@ -35,7 +35,7 @@ class AuthenticationRuntime implements RuntimeExtensionInterface * * @param AuthenticationService $authService The authentication service * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly AuthenticationService $authService, @@ -52,7 +52,7 @@ public function __construct( * * @throws \GuzzleHttp\Exception\GuzzleException If the request fails. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function oauthToken(Source $source): string { @@ -72,7 +72,7 @@ public function oauthToken(Source $source): string * * @throws \GuzzleHttp\Exception\GuzzleException If the request fails. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function decosToken(Source $source): string { @@ -90,7 +90,7 @@ public function decosToken(Source $source): string * * @return string The signed JWT token * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function jwtToken(Source $source): string { diff --git a/lib/Twig/MappingExtension.php b/lib/Twig/MappingExtension.php index 2e790c7120..b66466681f 100644 --- a/lib/Twig/MappingExtension.php +++ b/lib/Twig/MappingExtension.php @@ -45,7 +45,7 @@ class MappingExtension extends AbstractExtension * * @return TwigFilter[] Array of Twig filters * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFilters(): array { @@ -64,7 +64,7 @@ public function getFilters(): array * * @return TwigFunction[] Array of Twig functions * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function getFunctions(): array { diff --git a/lib/Twig/MappingRuntime.php b/lib/Twig/MappingRuntime.php index b723b45e4b..007e5578c9 100644 --- a/lib/Twig/MappingRuntime.php +++ b/lib/Twig/MappingRuntime.php @@ -58,7 +58,7 @@ class MappingRuntime implements RuntimeExtensionInterface * @param MappingService $mappingService The mapping service for executing mappings * @param MappingMapper $mappingMapper The mapping mapper for finding mappings * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly MappingService $mappingService, @@ -73,7 +73,7 @@ public function __construct( * * @return string The encoded output. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function b64enc(string $input): string { @@ -87,7 +87,7 @@ public function b64enc(string $input): string * * @return string The decoded output. * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function b64dec(string $input): string { @@ -103,7 +103,7 @@ public function b64dec(string $input): string * * @psalm-suppress MixedReturnStatement * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function jsonDecode(string $input): array { @@ -121,7 +121,7 @@ public function jsonDecode(string $input): array * * @return array The mapped output * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function executeMapping(Mapping|array|string|int $mapping, array $input, bool $list=false): array { @@ -131,7 +131,7 @@ public function executeMapping(Mapping|array|string|int $mapping, array $input, $mapping = $mappingObject; } else if (is_string($mapping) === true || is_int($mapping) === true) { if (is_string($mapping) !== true || str_starts_with($mapping, 'http') !== true) { - $mapping = $this->mappingMapper->find($mapping); + $mapping = $this->mappingMapper->find($mapping, true); } if (is_string($mapping) === true && str_starts_with($mapping, 'http') === true) { @@ -152,7 +152,7 @@ public function executeMapping(Mapping|array|string|int $mapping, array $input, * * @return UuidV4 A new UUID v4 instance * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function generateUuid(): UuidV4 { @@ -170,7 +170,7 @@ public function generateUuid(): UuidV4 * * @return string The translated value, or original if no mapping found * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function zgwEnum(string $value, string $fieldName, array $valueMappings=[]): string { @@ -192,7 +192,7 @@ public function zgwEnum(string $value, string $fieldName, array $valueMappings=[ * * @return string The English value, or original if no mapping found * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function zgwEnumReverse(string $value, string $fieldName, array $valueMappings=[]): string { @@ -213,7 +213,7 @@ public function zgwEnumReverse(string $value, string $fieldName, array $valueMap * * @return string The extracted UUID * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function zgwExtractUuid(?string $url=null): string { diff --git a/lib/Twig/MappingRuntimeLoader.php b/lib/Twig/MappingRuntimeLoader.php index a6892898b8..72447cea3d 100644 --- a/lib/Twig/MappingRuntimeLoader.php +++ b/lib/Twig/MappingRuntimeLoader.php @@ -42,7 +42,7 @@ class MappingRuntimeLoader implements RuntimeLoaderInterface * @param MappingService $mappingService The mapping service * @param MappingMapper $mappingMapper The mapping mapper * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function __construct( private readonly MappingService $mappingService, @@ -57,7 +57,7 @@ public function __construct( * * @return MappingRuntime|null The runtime instance or null if not this class * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-28 + * @spec openspec/specs/object-lifecycle/spec.md */ public function load(string $class): ?MappingRuntime { diff --git a/lib/WorkflowEngine/N8nAdapter.php b/lib/WorkflowEngine/N8nAdapter.php index 31ecc19502..263b9f49f9 100644 --- a/lib/WorkflowEngine/N8nAdapter.php +++ b/lib/WorkflowEngine/N8nAdapter.php @@ -17,8 +17,8 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-85 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-93 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-health-monitoring + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ declare(strict_types=1); @@ -60,7 +60,7 @@ class N8nAdapter implements WorkflowEngineInterface * @param IClientService $clientService HTTP client * @param LoggerInterface $logger Logger * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function __construct( private readonly IClientService $clientService, @@ -81,7 +81,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function configure(string $baseUrl, array $authConfig=[]): void { @@ -96,7 +96,7 @@ public function configure(string $baseUrl, array $authConfig=[]): void * * @return string The workflow ID * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function deployWorkflow(array $workflowDefinition): string { @@ -119,7 +119,7 @@ public function deployWorkflow(array $workflowDefinition): string * * @return string The workflow ID * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function updateWorkflow(string $workflowId, array $workflowDefinition): string { @@ -141,7 +141,7 @@ public function updateWorkflow(string $workflowId, array $workflowDefinition): s * * @return array The workflow definition * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function getWorkflow(string $workflowId): array { @@ -161,7 +161,7 @@ public function getWorkflow(string $workflowId): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function deleteWorkflow(string $workflowId): void { @@ -179,7 +179,7 @@ public function deleteWorkflow(string $workflowId): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function activateWorkflow(string $workflowId): void { @@ -197,7 +197,7 @@ public function activateWorkflow(string $workflowId): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function deactivateWorkflow(string $workflowId): void { @@ -217,7 +217,7 @@ public function deactivateWorkflow(string $workflowId): void * * @return WorkflowResult The execution result * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function executeWorkflow(string $workflowId, array $data, int $timeout=30): WorkflowResult { @@ -270,7 +270,7 @@ public function executeWorkflow(string $workflowId, array $data, int $timeout=30 * * @return string The webhook URL * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function getWebhookUrl(string $workflowId): string { @@ -282,7 +282,7 @@ public function getWebhookUrl(string $workflowId): string * * @return array List of workflows * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function listWorkflows(): array { @@ -320,7 +320,7 @@ public function listWorkflows(): array * * @return bool True if healthy * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-85 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-health-monitoring */ public function healthCheck(): bool { @@ -349,7 +349,7 @@ public function healthCheck(): bool * * @return array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ private function buildRequestOptions(array $extra=[]): array { @@ -368,7 +368,7 @@ private function buildRequestOptions(array $extra=[]): array * * @return array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ private function buildAuthHeaders(): array { @@ -394,7 +394,7 @@ private function buildAuthHeaders(): array * * @return WorkflowResult * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-4 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ private function parseWorkflowResponse(?array $responseData): WorkflowResult { diff --git a/lib/WorkflowEngine/WindmillAdapter.php b/lib/WorkflowEngine/WindmillAdapter.php index 8c468a411a..a1e349302b 100644 --- a/lib/WorkflowEngine/WindmillAdapter.php +++ b/lib/WorkflowEngine/WindmillAdapter.php @@ -17,8 +17,8 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-94 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ declare(strict_types=1); @@ -83,7 +83,7 @@ public function __construct( * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function configure(string $baseUrl, array $authConfig=[]): void { @@ -99,7 +99,7 @@ public function configure(string $baseUrl, array $authConfig=[]): void * * @return string The workflow ID * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function deployWorkflow(array $workflowDefinition): string { @@ -122,7 +122,7 @@ public function deployWorkflow(array $workflowDefinition): string * * @return string The workflow ID * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function updateWorkflow(string $workflowId, array $workflowDefinition): string { @@ -144,7 +144,7 @@ public function updateWorkflow(string $workflowId, array $workflowDefinition): s * * @return array The workflow definition * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function getWorkflow(string $workflowId): array { @@ -164,7 +164,7 @@ public function getWorkflow(string $workflowId): array * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function deleteWorkflow(string $workflowId): void { @@ -182,7 +182,7 @@ public function deleteWorkflow(string $workflowId): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function activateWorkflow(string $workflowId): void { @@ -196,7 +196,7 @@ public function activateWorkflow(string $workflowId): void * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function deactivateWorkflow(string $workflowId): void { @@ -213,7 +213,7 @@ public function deactivateWorkflow(string $workflowId): void * * @return WorkflowResult The execution result * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function executeWorkflow(string $workflowId, array $data, int $timeout=30): WorkflowResult { @@ -264,7 +264,7 @@ public function executeWorkflow(string $workflowId, array $data, int $timeout=30 * * @return string The webhook URL * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function getWebhookUrl(string $workflowId): string { @@ -276,7 +276,7 @@ public function getWebhookUrl(string $workflowId): string * * @return array List of workflows * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function listWorkflows(): array { @@ -313,7 +313,7 @@ public function listWorkflows(): array * * @return bool True if healthy * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ public function healthCheck(): bool { @@ -342,7 +342,7 @@ public function healthCheck(): bool * * @return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ private function buildRequestOptions(array $extra=[]): array { @@ -359,7 +359,7 @@ private function buildRequestOptions(array $extra=[]): array * * @return array * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ private function buildAuthHeaders(): array { @@ -379,7 +379,7 @@ private function buildAuthHeaders(): array * * @return WorkflowResult * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-82 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-windmill-adapter-implementation */ private function parseWorkflowResponse(?array $responseData): WorkflowResult { diff --git a/lib/WorkflowEngine/WorkflowEngineInterface.php b/lib/WorkflowEngine/WorkflowEngineInterface.php index 9f03b464f8..0f3261df57 100644 --- a/lib/WorkflowEngine/WorkflowEngineInterface.php +++ b/lib/WorkflowEngine/WorkflowEngineInterface.php @@ -17,9 +17,9 @@ * * @link https://OpenRegister.app * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-90 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-92 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition + * @spec openspec/specs/workflow-engine-abstraction/spec.md + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ declare(strict_types=1); @@ -41,8 +41,8 @@ interface WorkflowEngineInterface * * @return string Engine-specific workflow ID * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-90 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function deployWorkflow(array $workflowDefinition): string; @@ -54,7 +54,7 @@ public function deployWorkflow(array $workflowDefinition): string; * * @return string Engine-specific workflow ID (may change on some engines) * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition */ public function updateWorkflow(string $workflowId, array $workflowDefinition): string; @@ -65,7 +65,7 @@ public function updateWorkflow(string $workflowId, array $workflowDefinition): s * * @return array Engine-specific workflow definition * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition */ public function getWorkflow(string $workflowId): array; @@ -76,7 +76,7 @@ public function getWorkflow(string $workflowId): array; * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition */ public function deleteWorkflow(string $workflowId): void; @@ -87,7 +87,7 @@ public function deleteWorkflow(string $workflowId): void; * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition */ public function activateWorkflow(string $workflowId): void; @@ -98,7 +98,7 @@ public function activateWorkflow(string $workflowId): void; * * @return void * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition */ public function deactivateWorkflow(string $workflowId): void; @@ -111,8 +111,8 @@ public function deactivateWorkflow(string $workflowId): void; * * @return WorkflowResult Structured execution result * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 - * @spec openspec/changes/retrofit-2026-04-30-annotate-openregister/tasks.md#task-92 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function executeWorkflow(string $workflowId, array $data, int $timeout=30): WorkflowResult; @@ -123,7 +123,7 @@ public function executeWorkflow(string $workflowId, array $data, int $timeout=30 * * @return string Webhook URL * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition */ public function getWebhookUrl(string $workflowId): string; @@ -132,7 +132,7 @@ public function getWebhookUrl(string $workflowId): string; * * @return array Array of workflow summaries * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition */ public function listWorkflows(): array; @@ -141,7 +141,7 @@ public function listWorkflows(): array; * * @return bool True if the engine is reachable and responsive * - * @spec openspec/changes/retrofit-2026-04-23-annotate-openregister/tasks.md#task-81 + * @spec openspec/specs/workflow-engine-abstraction/spec.md#requirement-engine-interface-definition */ public function healthCheck(): bool; }//end interface diff --git a/lib/WorkflowEngine/WorkflowResult.php b/lib/WorkflowEngine/WorkflowResult.php index 244e97c1e7..f03dc221a1 100644 --- a/lib/WorkflowEngine/WorkflowResult.php +++ b/lib/WorkflowEngine/WorkflowResult.php @@ -82,7 +82,7 @@ class WorkflowResult implements JsonSerializable * * @throws InvalidArgumentException If status is not valid * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function __construct( string $status, @@ -110,7 +110,7 @@ public function __construct( * * @return self * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public static function approved(array $metadata=[]): self { @@ -125,7 +125,7 @@ public static function approved(array $metadata=[]): self * * @return self * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public static function rejected(array $errors, array $metadata=[]): self { @@ -140,7 +140,7 @@ public static function rejected(array $errors, array $metadata=[]): self * * @return self * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public static function modified(array $data, array $metadata=[]): self { @@ -155,7 +155,7 @@ public static function modified(array $data, array $metadata=[]): self * * @return self * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public static function error(string $message, array $metadata=[]): self { @@ -171,7 +171,7 @@ public static function error(string $message, array $metadata=[]): self * * @return string The status value * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function getStatus(): string { @@ -183,7 +183,7 @@ public function getStatus(): string * * @return array|null * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function getData(): ?array { @@ -195,7 +195,7 @@ public function getData(): ?array * * @return array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function getErrors(): array { @@ -207,7 +207,7 @@ public function getErrors(): array * * @return array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function getMetadata(): array { @@ -219,7 +219,7 @@ public function getMetadata(): array * * @return bool True if approved * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function isApproved(): bool { @@ -231,7 +231,7 @@ public function isApproved(): bool * * @return bool True if rejected * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function isRejected(): bool { @@ -243,7 +243,7 @@ public function isRejected(): bool * * @return bool True if modified * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function isModified(): bool { @@ -255,7 +255,7 @@ public function isModified(): bool * * @return bool True if error * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function isError(): bool { @@ -267,7 +267,7 @@ public function isError(): bool * * @return array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function toArray(): array { @@ -284,7 +284,7 @@ public function toArray(): array * * @return array * - * @spec openspec/changes/retrofit-2026-04-28-b2b-crossrefs/tasks.md#task-5 + * @spec openspec/specs/workflow-engine-abstraction/spec.md */ public function jsonSerialize(): array { diff --git a/no-admin-idor-findings.log b/no-admin-idor-findings.log new file mode 100644 index 0000000000..b8f655aa85 --- /dev/null +++ b/no-admin-idor-findings.log @@ -0,0 +1,103 @@ +lib/Controller/AgentsController.php:636 method=tools rule=no-auth-guard-in-body +lib/Controller/AnalyticsLinksController.php:276 method=available rule=no-auth-guard-in-body +lib/Controller/AuditTrailController.php:362 method=update rule=no-auth-guard-in-body +lib/Controller/AuditTrailController.php:391 method=objects rule=no-auth-guard-in-body +lib/Controller/AuditTrailController.php:516 method=destroy rule=no-auth-guard-in-body +lib/Controller/AuditTrailController.php:534 method=destroyMultiple rule=no-auth-guard-in-body +lib/Controller/AuditTrailController.php:607 method=verify rule=no-auth-guard-in-body +lib/Controller/BookmarkLinksController.php:280 method=available rule=no-auth-guard-in-body +lib/Controller/CalendarEventsController.php:343 method=listCalendars rule=no-auth-guard-in-body +lib/Controller/CalendarEventsController.php:365 method=listCalendarEvents rule=no-auth-guard-in-body +lib/Controller/ChatController.php:863 method=getChatStats rule=no-auth-guard-in-body +lib/Controller/CollectiveLinksController.php:276 method=available rule=no-auth-guard-in-body +lib/Controller/CollectiveLinksController.php:304 method=collectives rule=no-auth-guard-in-body +lib/Controller/CompanyLookupController.php:88 method=kvkCompany rule=no-auth-guard-in-body +lib/Controller/CompanyLookupController.php:114 method=kvkSearch rule=no-auth-guard-in-body +lib/Controller/CompanyLookupController.php:147 method=openCorporatesSearch rule=no-auth-guard-in-body +lib/Controller/ConfigurationController.php:255 method=enrichDetails rule=no-auth-guard-in-body +lib/Controller/ConfigurationController.php:784 method=discover rule=no-auth-guard-in-body +lib/Controller/ConfigurationController.php:868 method=getGitHubBranches rule=no-auth-guard-in-body +lib/Controller/ConfigurationController.php:913 method=getGitHubRepositories rule=no-auth-guard-in-body +lib/Controller/ConfigurationController.php:966 method=getGitHubConfigurations rule=no-auth-guard-in-body +lib/Controller/ConfigurationController.php:1015 method=getGitLabBranches rule=no-auth-guard-in-body +lib/Controller/ConfigurationController.php:1067 method=getGitLabConfigurations rule=no-auth-guard-in-body +lib/Controller/ContactsController.php:438 method=objects rule=no-auth-guard-in-body +lib/Controller/ContactsController.php:480 method=match rule=no-auth-guard-in-body +lib/Controller/CospendLinksController.php:301 method=available rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:272 method=index rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:324 method=calculate rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:367 method=getAuditTrailActionChart rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:414 method=getObjectsByRegisterChart rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:442 method=getObjectsBySchemaChart rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:472 method=getObjectsBySizeChart rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:502 method=getAuditTrailStatistics rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:535 method=getAuditTrailActionDistribution rule=no-auth-guard-in-body +lib/Controller/DashboardController.php:569 method=getMostActiveObjects rule=no-auth-guard-in-body +lib/Controller/DataSubjectRequestController.php:72 method=subjectData rule=no-auth-guard-in-body +lib/Controller/DataSubjectRequestController.php:105 method=accessExport rule=no-auth-guard-in-body +lib/Controller/DataSubjectRequestController.php:133 method=rectify rule=no-auth-guard-in-body +lib/Controller/DataSubjectRequestController.php:164 method=erase rule=no-auth-guard-in-body +lib/Controller/DataSubjectRequestController.php:194 method=restrict rule=no-auth-guard-in-body +lib/Controller/DataSubjectRequestController.php:226 method=objection rule=no-auth-guard-in-body +lib/Controller/DeckController.php:182 method=objects rule=no-auth-guard-in-body +lib/Controller/DeckLinksController.php:297 method=boards rule=no-auth-guard-in-body +lib/Controller/DeckLinksController.php:322 method=stacks rule=no-auth-guard-in-body +lib/Controller/DeletedController.php:365 method=topDeleters rule=no-auth-guard-in-body +lib/Controller/EmailLinksController.php:220 method=accounts rule=no-auth-guard-in-body +lib/Controller/EmailsController.php:277 method=search rule=no-auth-guard-in-body +lib/Controller/EmailsController.php:333 method=bySender rule=no-auth-guard-in-body +lib/Controller/FileExtractionController.php:292 method=extract rule=no-auth-guard-in-body +lib/Controller/FileExtractionController.php:357 method=discover rule=no-auth-guard-in-body +lib/Controller/FileExtractionController.php:408 method=extractAll rule=no-auth-guard-in-body +lib/Controller/FileExtractionController.php:456 method=retryFailed rule=no-auth-guard-in-body +lib/Controller/FileExtractionController.php:508 method=stats rule=no-auth-guard-in-body +lib/Controller/FileExtractionController.php:556 method=cleanup rule=no-auth-guard-in-body +lib/Controller/FileExtractionController.php:607 method=fileTypes rule=no-auth-guard-in-body +lib/Controller/FileExtractionController.php:645 method=vectorizeBatch rule=no-auth-guard-in-body +lib/Controller/FileSearchController.php:83 method=semanticSearch rule=no-auth-guard-in-body +lib/Controller/FileSidebarController.php:77 method=getObjectsForFile rule=no-auth-guard-in-body +lib/Controller/FileSidebarController.php:115 method=getExtractionStatus rule=no-auth-guard-in-body +lib/Controller/FileTextController.php:100 method=getFileText rule=no-auth-guard-in-body +lib/Controller/FileTextController.php:148 method=extractFileText rule=no-auth-guard-in-body +lib/Controller/FileTextController.php:207 method=bulkExtract rule=no-auth-guard-in-body +lib/Controller/FileTextController.php:254 method=getStats rule=no-auth-guard-in-body +lib/Controller/FileTextController.php:298 method=deleteFileText rule=no-auth-guard-in-body +lib/Controller/FormLinksController.php:392 method=available rule=no-auth-guard-in-body +lib/Controller/GitHubIssuesController.php:127 method=index rule=no-auth-guard-in-body +lib/Controller/GraphQLController.php:153 method=explorer rule=no-auth-guard-in-body +lib/Controller/GraphQLSubscriptionController.php:80 method=subscribe rule=no-auth-guard-in-body +lib/Controller/HeartbeatController.php:93 method=heartbeat rule=no-auth-guard-in-body +lib/Controller/LinkedEntityController.php:165 method=addRegisterLink rule=no-auth-guard-in-body +lib/Controller/LinkedEntityController.php:200 method=addSchemaLink rule=no-auth-guard-in-body +lib/Controller/LinkedEntityController.php:236 method=reverseLookup rule=no-auth-guard-in-body +lib/Controller/MapLinksController.php:309 method=available rule=no-auth-guard-in-body +lib/Controller/MappingsController.php:348 method=test rule=no-auth-guard-in-body +lib/Controller/McpController.php:119 method=discoverCapability rule=no-auth-guard-in-body +lib/Controller/MessageDispatchController.php:101 method=smsSend rule=no-auth-guard-in-body +lib/Controller/MessageDispatchController.php:124 method=whatsappSend rule=no-auth-guard-in-body +lib/Controller/ObjectIntegrationsController.php:107 method=index rule=no-auth-guard-in-body +lib/Controller/ObjectIntegrationsController.php:133 method=show rule=no-auth-guard-in-body +lib/Controller/ObjectIntegrationsController.php:156 method=create rule=no-auth-guard-in-body +lib/Controller/ObjectIntegrationsController.php:182 method=update rule=no-auth-guard-in-body +lib/Controller/OrganisationController.php:223 method=index rule=no-auth-guard-in-body +lib/Controller/OrganisationController.php:274 method=setActive rule=no-auth-guard-in-body +lib/Controller/OrganisationController.php:344 method=getActive rule=no-auth-guard-in-body +lib/Controller/OrganisationController.php:405 method=create rule=no-auth-guard-in-body +lib/Controller/OrganisationController.php:871 method=clearCache rule=no-auth-guard-in-body +lib/Controller/PersonLookupController.php:96 method=brpPerson rule=no-auth-guard-in-body +lib/Controller/PhotoLinksController.php:268 method=available rule=no-auth-guard-in-body +lib/Controller/PollLinksController.php:291 method=available rule=no-auth-guard-in-body +lib/Controller/RegistersController.php:1623 method=stats rule=no-auth-guard-in-body +lib/Controller/SearchTrailController.php:768 method=export rule=no-auth-guard-in-body +lib/Controller/TalkLinksController.php:267 method=rooms rule=no-auth-guard-in-body +lib/Controller/TasksController.php:107 method=allUserTasks rule=no-auth-guard-in-body +lib/Controller/TimeTrackerLinksController.php:286 method=available rule=no-auth-guard-in-body +lib/Controller/UrnController.php:76 method=resolve rule=no-auth-guard-in-body +lib/Controller/UrnController.php:122 method=lookup rule=no-auth-guard-in-body +lib/Controller/UrnController.php:160 method=bulk rule=no-auth-guard-in-body +lib/Controller/WebPushController.php:89 method=vapidPublicKey rule=no-auth-guard-in-body +lib/Controller/WebhooksController.php:736 method=events rule=no-auth-guard-in-body +lib/Controller/WorkflowEngineController.php:94 method=index rule=no-auth-guard-in-body +lib/Controller/WorkflowEngineController.php:114 method=show rule=no-auth-guard-in-body +lib/Controller/XwikiLinksController.php:256 method=available rule=no-auth-guard-in-body +lib/Controller/XwikiLinksController.php:305 method=search rule=no-auth-guard-in-body diff --git a/no-admin-idor-triage.md b/no-admin-idor-triage.md new file mode 100644 index 0000000000..fdb3a38522 --- /dev/null +++ b/no-admin-idor-triage.md @@ -0,0 +1,166 @@ +# Security triage — `no-admin-idor` (gate-7 / OWASP A01:2021 / ADR-005 Rule 3) + +Issue: **Conduction/openregister#325**. Branch: `fix/no-admin-idor-triage` (off `origin/development`). + +Gate-7 (`hydra-gate-no-admin-idor`) flagged **103** controller methods carrying +`#[NoAdminRequired]` / `@NoAdminRequired` with no per-object or admin +authorization guard visible in the method body. Each finding was traced through +its actual call chain (service/mapper internals read, not assumed) and +classified into one of four buckets. Gate-7 is now **0** (was 103); gates 5 +(route-auth), 8 (unsafe-auth-resolver) and 9 (semantic-auth) still PASS. + +## Classification counts + +| Bucket | Meaning | Count | Action | +|---|---|---|---| +| **(a)** GUARDED-DOWNSTREAM | Delegates to a service/mapper that already enforces per-object authz | **14** | `@no-admin-idor-exempt` documenting the real guard | +| **(b)** GENUINE-IDOR | Any authed user could read/mutate arbitrary objects by id — no check anywhere | **14** | Real guard added + unit test | +| **(c)** PUBLIC-BY-DESIGN / NO-OBJECT | No per-object resource (capability probe, session-scoped list, external proxy, static catalog, 405/deprecated stub) | **46** | `@no-admin-idor-exempt` documenting why no guard is needed | +| **(d)** WRONG-ANNOTATION | Should be admin-only (tenant-wide analytics, instance config, batch maintenance) | **29** | Admin guard added (`isCurrentUserAdmin()` / `requireAdmin()`); some also drop `@NoAdminRequired` | +| **Total** | | **103** | gate-7 → 0 | + +## Genuine IDORs fixed (bucket b) — the security core + +| File:method | Leak | Guard added | +|---|---|---| +| `AuditTrailController::objects` | Per-object audit trail (actor UID, IP, per-field diffs) for any object id | `requireAdmin()` + `@NoAdminRequired` removed (aligns with admin-only index/show/export) | +| `EmailsController::search` / `::bySender` | Scanned **all** users' mailboxes for a sender → OR object links in other users' mail | `EmailService::findMessageIdsBySender` now `JOIN oc_mail_accounts ma … AND ma.user_id = ?` (session UID); returns [] for anon | +| `ContactsController::objects` | OR object links for arbitrary (enumerable) CardDAV contact UIDs across all users | `ContactService::getObjectsForContact` filters links to the caller's own addressbook IDs (`getAddressBooksForUser`); [] for anon | +| `DeckController::objects` | OR object links for any Deck board id, bypassing Deck's ACL | `DeckCardService::getObjectsForBoard` gates on Deck `BoardService::find` (board ACL, fail-closed) | +| `FileExtractionController::extract` | Forced text/PII extraction of any file by id (unscoped `FileMapper::getFile` / `IRootFolder::getById`) | `hasFileAccess()` — resolves via caller's `getUserFolder`; 404 if not accessible | +| `FileSidebarController::getObjectsForFile` / `::getExtractionStatus` | Object links + extraction status/PII-risk for any file id | `hasFileAccess()` guard (404) | +| `FileTextController::extractFileText` | Forced re-extraction of any file by id | `hasFileAccess()` guard (404) | +| `LinkedEntityController::reverseLookup` | Cross-tenant object-link scan (RBAC + multitenancy deliberately disabled; TODO #1273) | `isCurrentUserAdmin()` → 403 until per-tenant scoping lands | +| `ObjectIntegrationsController::index/show/create/update` | OR object id forwarded to pluggable providers with **no** OR-side authz | `guardObjectAccess()` resolves the object via `ObjectService` (register RBAC + multitenancy); 404 if inaccessible | + +## Wrong-annotation fixes (bucket d) — admin-gated + +- `ConfigurationController` ×7 (`enrichDetails`, `discover`, `getGitHub{Branches,Repositories,Configurations}`, `getGitLab{Branches,Configurations}`) — reach external repo content via the instance-wide admin GitHub/GitLab credential; added the sibling `isCurrentUserAdmin()` → 403 guard. +- `DashboardController` ×9 (`index`, `calculate`, 4 chart endpoints, `getAuditTrailStatistics`, `getAuditTrailActionDistribution`, `getMostActiveObjects`) — tenant-wide analytics across all registers/schemas; injected `IUserSession`+`IGroupManager`, added `isCurrentUserAdmin()` gate. Dashboard `page()` shell stays open. +- `AuditTrailController::verify`, `SearchTrailController::export` — tenant-wide audit/search PII; `@NoAdminRequired` removed + `requireAdmin()` (matches sibling posture; gate-9 clean). +- `DeletedController::topDeleters` — cross-user deletion analytics; `isCurrentUserAdmin()` gate. +- `FileExtractionController::{discover,extractAll,retryFailed,cleanup,vectorizeBatch}`, `FileTextController::bulkExtract` — instance-wide batch/maintenance; `isCurrentUserAdmin()` gate. +- `WorkflowEngineController::index/show` — instance-wide integration-engine metadata (internal baseUrl/healthStatus); `isCurrentUserAdmin()` gate (write siblings already gated). +- `LinkedEntityController::addRegisterLink/addSchemaLink` — register/schema config mutation; `isCurrentUserAdmin()` → 403. + +## Residual / follow-ups (documented, not silently suppressed) + +- `MessageDispatchController::smsSend` / `::whatsappSend` — classified (c): dispatch is bounded (admin-owned base URL, per-channel source allowlist) and keys no OR object. **Open risk**: no rate-limit/quota, so any authed user can trigger paid sends. Recommend a follow-up issue for a per-user rate limiter (not an IDOR; out of gate-7 scope). +- `FileSearchController::semanticSearch` — (c) free-text query (no id). The underlying vector search is not per-user scoped; cross-tenant snippet exposure is a separate concern to track. +- `LinkedEntityController::reverseLookup` — admin-gated as a stop-gap; the proper fix is per-tenant scoping in `LinkedEntityService` (TODO #1273). +- `ObjectIntegrations` providers (e.g. `NotesProvider`) still rely on their downstream NC-app service for entity-level ownership; the controller now authorizes the **OR object**, but a per-provider entity-authz audit is worthwhile. + +## How gate-7 is satisfied honestly + +- **(b)/(d)** carry a real body guard: `isCurrentUserAdmin()` / `requireAdmin()` (Pattern-1 helper delegation) or a per-resource check (`hasFileAccess()`, `guardObjectAccess()`, `userCanAccessBoard()` — all guard-predicate names the gate recognises), returning 403 (admin) or 404 (per-object, existence-hiding). +- **(a)/(c)** carry a reason-bearing `@no-admin-idor-exempt ` docblock tag (the sanctioned exemption; precedent: `SourcesController`, `ApplicationsController`) naming the downstream guard or explaining the absence of a per-object resource. No blanket suppression — every reason reflects the real authorization story and is reviewer-verifiable. + +## Full per-finding table + +Classification key: **a** guarded-downstream · **b** genuine-IDOR (fixed) · **c** public/no-object · **d** wrong-annotation (admin-gated). + +| # | File:method | Class | Mechanism / fix | +|---|---|---|---| +| 1 | AgentsController::tools | c | Tool-registry metadata (ToolRegistry::getAllTools); no id — exempt | +| 2 | AnalyticsLinksController::available | c | Integration availability probe — exempt | +| 3 | AuditTrailController::update | c | 405 immutability stub, no object access — exempt | +| 4 | AuditTrailController::objects | b | **FIX** requireAdmin() (per-object audit = cross-tenant PII) | +| 5 | AuditTrailController::destroy | c | 405 immutability stub — exempt | +| 6 | AuditTrailController::destroyMultiple | c | 405 immutability stub — exempt | +| 7 | AuditTrailController::verify | d | **FIX** requireAdmin() (tenant-wide hash-chain) | +| 8 | BookmarkLinksController::available | c | Session-scoped bookmarks probe — exempt | +| 9 | CalendarEventsController::listCalendars | c | Session-scoped calendar list — exempt | +| 10 | CalendarEventsController::listCalendarEvents | a | calendarUri resolved only against the caller's own principal calendars — exempt | +| 11 | ChatController::getChatStats | a | Counts scoped to caller's active organisation — exempt | +| 12 | CollectiveLinksController::available | c | Collectives availability probe — exempt | +| 13 | CollectiveLinksController::collectives | c | Session-scoped collectives list — exempt | +| 14 | CompanyLookupController::kvkCompany | c | External KVK proxy, no OR id — exempt | +| 15 | CompanyLookupController::kvkSearch | c | External KVK search proxy — exempt | +| 16 | CompanyLookupController::openCorporatesSearch | c | External OpenCorporates proxy — exempt | +| 17 | ConfigurationController::enrichDetails | d | **FIX** isCurrentUserAdmin() (admin GitHub/GitLab credential) | +| 18 | ConfigurationController::discover | d | **FIX** isCurrentUserAdmin() | +| 19 | ConfigurationController::getGitHubBranches | d | **FIX** isCurrentUserAdmin() | +| 20 | ConfigurationController::getGitHubRepositories | d | **FIX** isCurrentUserAdmin() | +| 21 | ConfigurationController::getGitHubConfigurations | d | **FIX** isCurrentUserAdmin() | +| 22 | ConfigurationController::getGitLabBranches | d | **FIX** isCurrentUserAdmin() | +| 23 | ConfigurationController::getGitLabConfigurations | d | **FIX** isCurrentUserAdmin() | +| 24 | ContactsController::objects | b | **FIX** ContactService scopes to caller's addressbooks | +| 25 | ContactsController::match | c | Free-text contact matching, no id — exempt | +| 26 | CospendLinksController::available | c | Session-scoped Cospend projects — exempt | +| 27 | DashboardController::index | d | **FIX** isCurrentUserAdmin() (tenant-wide analytics) | +| 28 | DashboardController::calculate | d | **FIX** isCurrentUserAdmin() | +| 29 | DashboardController::getAuditTrailActionChart | d | **FIX** isCurrentUserAdmin() | +| 30 | DashboardController::getObjectsByRegisterChart | d | **FIX** isCurrentUserAdmin() | +| 31 | DashboardController::getObjectsBySchemaChart | d | **FIX** isCurrentUserAdmin() | +| 32 | DashboardController::getObjectsBySizeChart | d | **FIX** isCurrentUserAdmin() | +| 33 | DashboardController::getAuditTrailStatistics | d | **FIX** isCurrentUserAdmin() | +| 34 | DashboardController::getAuditTrailActionDistribution | d | **FIX** isCurrentUserAdmin() | +| 35 | DashboardController::getMostActiveObjects | d | **FIX** isCurrentUserAdmin() | +| 36 | DataSubjectRequestController::subjectData | a | Objects loaded via MagicMapper::find(_rbac,_multitenancy) — exempt | +| 37 | DataSubjectRequestController::accessExport | a | Same MagicMapper RBAC scoping — exempt | +| 38 | DataSubjectRequestController::rectify | a | loadByIdentifier → MagicMapper RBAC; 404 on miss — exempt | +| 39 | DataSubjectRequestController::erase | a | Per-object RBAC load; saveObject re-scoped — exempt | +| 40 | DataSubjectRequestController::restrict | a | setMarker → MagicMapper RBAC — exempt | +| 41 | DataSubjectRequestController::objection | a | setMarker → MagicMapper RBAC — exempt | +| 42 | DeckController::objects | b | **FIX** DeckCardService gates on Deck BoardService::find | +| 43 | DeckLinksController::boards | c | Deck BoardService returns only the caller's boards — exempt | +| 44 | DeckLinksController::stacks | a | Deck StackService::findAll → PermissionService::checkPermission — exempt | +| 45 | DeletedController::topDeleters | d | **FIX** isCurrentUserAdmin() (cross-user analytics) | +| 46 | EmailLinksController::accounts | c | oc_mail_accounts filtered by session UID — exempt | +| 47 | EmailsController::search | b | **FIX** EmailService scopes mailbox scan to caller's accounts | +| 48 | EmailsController::bySender | b | **FIX** same EmailService scoping | +| 49 | FileExtractionController::extract | b | **FIX** hasFileAccess() (per-user file resolution) | +| 50 | FileExtractionController::discover | d | **FIX** isCurrentUserAdmin() (instance-wide) | +| 51 | FileExtractionController::extractAll | d | **FIX** isCurrentUserAdmin() | +| 52 | FileExtractionController::retryFailed | d | **FIX** isCurrentUserAdmin() | +| 53 | FileExtractionController::stats | c | Aggregate counters, no id — exempt | +| 54 | FileExtractionController::cleanup | d | **FIX** isCurrentUserAdmin() | +| 55 | FileExtractionController::fileTypes | c | Aggregate stub, no id — exempt | +| 56 | FileExtractionController::vectorizeBatch | d | **FIX** isCurrentUserAdmin() | +| 57 | FileSearchController::semanticSearch | c | Free-text query, no id — exempt (cross-tenant scoping = follow-up) | +| 58 | FileSidebarController::getObjectsForFile | b | **FIX** hasFileAccess() | +| 59 | FileSidebarController::getExtractionStatus | b | **FIX** hasFileAccess() | +| 60 | FileTextController::getFileText | c | Deprecated 404 stub, no access — exempt | +| 61 | FileTextController::extractFileText | b | **FIX** hasFileAccess() | +| 62 | FileTextController::bulkExtract | d | **FIX** isCurrentUserAdmin() | +| 63 | FileTextController::getStats | c | Aggregate counters — exempt | +| 64 | FileTextController::deleteFileText | c | 501 stub — proactively guarded with hasFileAccess() for when implemented | +| 65 | FormLinksController::available | c | Session-scoped Forms — exempt | +| 66 | GitHubIssuesController::index | a | GitHubGuards::enforceRepoAllowlist (repo allowlist + rate-limit) — exempt | +| 67 | GraphQLController::explorer | c | Static GraphiQL HTML page — exempt | +| 68 | GraphQLSubscriptionController::subscribe | a | SubscriptionService per-event verifyEventRBAC → hasPermission — exempt | +| 69 | HeartbeatController::heartbeat | c | Liveness probe — exempt | +| 70 | LinkedEntityController::addRegisterLink | d | **FIX** isCurrentUserAdmin() (register config mutation) | +| 71 | LinkedEntityController::addSchemaLink | d | **FIX** isCurrentUserAdmin() (schema config mutation) | +| 72 | LinkedEntityController::reverseLookup | b | **FIX** isCurrentUserAdmin() (RBAC-disabled cross-tenant scan; TODO #1273) | +| 73 | MapLinksController::available | c | Session-scoped Maps POIs — exempt | +| 74 | MappingsController::test | c | Stateless mapping evaluation, no id — exempt | +| 75 | McpController::discoverCapability | a | Closed hardcoded capability allowlist — exempt | +| 76 | MessageDispatchController::smsSend | c | Bounded dispatch (admin base URL + source allowlist), no id — exempt (rate-limit = follow-up) | +| 77 | MessageDispatchController::whatsappSend | c | Same bounded dispatch — exempt | +| 78 | ObjectIntegrationsController::index | b | **FIX** guardObjectAccess() (ObjectService RBAC) | +| 79 | ObjectIntegrationsController::show | b | **FIX** guardObjectAccess() | +| 80 | ObjectIntegrationsController::create | b | **FIX** guardObjectAccess() | +| 81 | ObjectIntegrationsController::update | b | **FIX** guardObjectAccess() | +| 82 | OrganisationController::index | c | Caller's own organisations only — exempt | +| 83 | OrganisationController::setActive | a | setActiveOrganisation enforces membership (hasUser) — exempt | +| 84 | OrganisationController::getActive | c | Caller's active org only — exempt | +| 85 | OrganisationController::create | c | Self-service org creation, no id read — exempt | +| 86 | OrganisationController::clearCache | c | Clears only the caller's own caches — exempt | +| 87 | PersonLookupController::brpPerson | c | External BRP proxy (BSN), no OR id — exempt | +| 88 | PhotoLinksController::available | c | Session-scoped Photos albums — exempt | +| 89 | PollLinksController::available | c | Session-scoped polls — exempt | +| 90 | RegistersController::stats | a | RegisterMapper::find(_multitenancy:true); metadata read open by design — exempt | +| 91 | SearchTrailController::export | d | **FIX** requireAdmin() + @NoAdminRequired removed | +| 92 | TalkLinksController::rooms | c | Talk rooms the caller participates in — exempt | +| 93 | TasksController::allUserTasks | c | Calendars resolved from session UID — exempt | +| 94 | TimeTrackerLinksController::available | c | Session-scoped TimeTracker clients — exempt | +| 95 | UrnController::resolve | c | URN→URL address translation, no object body — exempt | +| 96 | UrnController::lookup | c | URL→URN address translation — exempt | +| 97 | UrnController::bulk | c | Batch URN translation (capped 1000) — exempt | +| 98 | WebPushController::vapidPublicKey | c | Public VAPID key only (private key never exposed) — exempt | +| 99 | WebhooksController::events | c | Static webhook event-type catalogue — exempt | +| 100 | WorkflowEngineController::index | d | **FIX** isCurrentUserAdmin() (instance-wide engine metadata) | +| 101 | WorkflowEngineController::show | d | **FIX** isCurrentUserAdmin() | +| 102 | XwikiLinksController::available | c | Admin-configured XWiki source reachability probe — exempt | +| 103 | XwikiLinksController::search | c | Object-independent free-text KB search — exempt | diff --git a/openapi.json b/openapi.json index aa758bae16..52953a91f8 100644 --- a/openapi.json +++ b/openapi.json @@ -255,6 +255,306 @@ "description": "ISO-8601 timestamp when GitHub's rate-limit window resets. Present on github_rate_limited responses." } } + }, + "FileError": { + "type": "object", + "description": "Error envelope returned by the object file-action endpoints. The message is wrapped in IL10N on the server so it is localised to the caller's language.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "description": "Human-readable, localised error message.", + "example": "Object not found" + } + } + }, + "FilePreviewError": { + "type": "object", + "description": "Error envelope returned by the preview endpoint. Carries a fallback icon URL so the client can render a placeholder when no thumbnail is available.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "example": "File not found" + }, + "fallbackIcon": { + "type": "string", + "description": "Relative URL of a generic file-type icon the client may render instead of a thumbnail.", + "example": "/core/img/filetypes/file.svg" + } + } + }, + "File": { + "type": "object", + "description": "A file attached to an object, as returned by FileFormattingHandler::formatFile(). Fields gated on authentication (downloadCount, orLock) are omitted for anonymous callers.", + "properties": { + "id": { + "type": "integer", + "description": "Nextcloud filecache file id.", + "example": 4211 + }, + "name": { + "type": "string", + "example": "report.pdf" + }, + "size": { + "type": "integer", + "example": 184320 + }, + "mimeType": { + "type": "string", + "example": "application/pdf" + }, + "description": { + "type": "string", + "description": "OR-managed file description (Phase 8 metadata enrichment).", + "nullable": true + }, + "category": { + "type": "string", + "description": "OR-managed file category (Phase 8 metadata enrichment).", + "nullable": true + }, + "labels": { + "type": "array", + "description": "Merged tag-backed + OR-managed labels.", + "items": { + "type": "string" + } + }, + "downloadCount": { + "type": "integer", + "description": "Monotonic download counter. Authenticated callers only.", + "example": 7 + }, + "published": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Public-share publication timestamp, when the file has been shared." + } + } + }, + "FileList": { + "type": "object", + "description": "Paginated list of files attached to an object.", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/File" + } + }, + "total": { + "type": "integer", + "example": 3 + }, + "page": { + "type": "integer", + "example": 1 + }, + "pages": { + "type": "integer", + "example": 1 + } + } + }, + "FileRenameRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "New file name. Must not contain invalid filesystem characters and must not collide with an existing name in the object folder.", + "example": "renamed-report.pdf" + } + } + }, + "FileCopyMoveRequest": { + "type": "object", + "required": [ + "targetObjectId" + ], + "properties": { + "targetObjectId": { + "type": "string", + "description": "UUID or id of the object the file is copied/moved into.", + "example": "9f1c2b7e-8a3d-4c5e-bf21-1234567890ab" + }, + "targetRegister": { + "type": "string", + "description": "Target register slug or id. Defaults to the source register when omitted.", + "example": "publications" + }, + "targetSchema": { + "type": "string", + "description": "Target schema slug or id. Defaults to the source schema when omitted.", + "example": "attachment" + } + } + }, + "FileVersion": { + "type": "object", + "properties": { + "versionId": { + "type": "string", + "example": "1700000000" + }, + "timestamp": { + "type": "integer", + "example": 1700000000 + }, + "size": { + "type": "integer", + "example": 184320 + }, + "author": { + "type": "string", + "example": "alice" + }, + "authorDisplayName": { + "type": "string", + "example": "Alice Example" + }, + "label": { + "type": "string", + "nullable": true + }, + "isCurrent": { + "type": "boolean", + "example": false + } + } + }, + "FileVersionList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileVersion" + } + }, + "FileLockResponse": { + "type": "object", + "properties": { + "lockedBy": { + "type": "string", + "example": "alice" + }, + "lockedAt": { + "type": "string", + "format": "date-time" + }, + "lockExpires": { + "type": "string", + "format": "date-time" + } + } + }, + "FileUnlockRequest": { + "type": "object", + "properties": { + "force": { + "type": "boolean", + "description": "When true, an administrator force-unlocks a file locked by another user.", + "default": false + } + } + }, + "FileBatchRequest": { + "type": "object", + "required": [ + "action", + "fileIds" + ], + "properties": { + "action": { + "type": "string", + "enum": [ + "publish", + "depublish", + "delete", + "label" + ], + "description": "Batch action to apply to each file id." + }, + "fileIds": { + "type": "array", + "description": "File ids to operate on. Max 100 per request.", + "items": { + "type": "integer" + }, + "maxItems": 100, + "example": [ + 4211, + 4212 + ] + }, + "labels": { + "type": "array", + "description": "Labels to apply. Used only when action = label.", + "items": { + "type": "string" + } + } + } + }, + "FileBatchResponse": { + "type": "object", + "description": "Per-file batch result. HTTP 200 when every file succeeded, HTTP 207 when at least one failed.", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fileId": { + "type": "integer" + }, + "success": { + "type": "boolean" + }, + "error": { + "type": "string", + "nullable": true + } + } + } + }, + "summary": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "succeeded": { + "type": "integer" + }, + "failed": { + "type": "integer" + } + } + } + } + }, + "FileLabelsRequest": { + "type": "object", + "required": [ + "labels" + ], + "properties": { + "labels": { + "type": "array", + "description": "Replacement label set. An empty array clears all labels.", + "items": { + "type": "string" + } + } + } } } }, @@ -575,12 +875,1620 @@ } } } - } - }, - "tags": [ - { - "name": "github-issues", - "description": "GitHub issues proxy backing the Features & Roadmap menu component." + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "description": "Register slug or id.", + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "description": "Schema slug or id.", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "description": "Object UUID or id.", + "schema": { + "type": "string" + } + } + ], + "get": { + "operationId": "files-index", + "summary": "List the files attached to an object", + "description": "Returns the paginated list of files attached to the object. Anonymous callers (PublicPage) see only published (publicly shared) files; authenticated callers are subject to object-level read RBAC.", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "File list.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileList" + } + } + } + }, + "404": { + "description": "Object not found or the object folder is missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + }, + "post": { + "operationId": "files-create", + "summary": "Attach a new file to an object (base64/text content)", + "description": "Creates a file from an inline `content` payload. Enforces object-level write RBAC.", + "tags": [ + "files" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "content" + ], + "properties": { + "name": { + "type": "string", + "example": "report.pdf" + }, + "content": { + "type": "string", + "description": "File content (base64-encoded for binary files)." + }, + "share": { + "type": "boolean", + "default": false + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "File created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/File" + } + } + } + }, + "400": { + "description": "Missing file name or content.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/save": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "description": "Register slug or id.", + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "description": "Schema slug or id.", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "description": "Object UUID or id.", + "schema": { + "type": "string" + } + } + ], + "post": { + "operationId": "files-save", + "summary": "Upsert a file on an object", + "description": "Creates a new file or updates the existing file with the same name (upsert). Body requires `name` and `content`. Auth posture: `#[NoAdminRequired] #[NoCSRFRequired]` with object-level write RBAC.", + "tags": [ + "files" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "content" + ], + "properties": { + "name": { + "type": "string", + "example": "report.pdf" + }, + "content": { + "type": "string", + "description": "File content (base64-encoded for binary files)." + }, + "share": { + "type": "boolean", + "default": false + }, + "tags": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "description": "Comma-separated list." + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "File created or updated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/File" + } + } + } + }, + "400": { + "description": "Missing file name or content.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "403": { + "description": "Caller may not access the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/filesMultipart": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "description": "Register slug or id.", + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "description": "Schema slug or id.", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "description": "Object UUID or id.", + "schema": { + "type": "string" + } + } + ], + "post": { + "operationId": "files-create-multipart", + "summary": "Upload one or more files via multipart form", + "description": "Accepts `multipart/form-data` with a single `file` field or a `files[]` array, plus optional `share` and `tags`. Auth posture: `#[NoAdminRequired] #[NoCSRFRequired]` with object-level write RBAC.", + "tags": [ + "files" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "files": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + }, + "share": { + "type": "boolean" + }, + "tags": { + "type": "string", + "description": "Comma-separated list." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Files uploaded.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/File" + } + } + } + } + }, + "400": { + "description": "No file uploaded or upload error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "403": { + "description": "Caller may not access the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "description": "Nextcloud filecache file id.", + "schema": { + "type": "integer" + } + } + ], + "get": { + "operationId": "files-show", + "summary": "Stream a file attached to an object", + "description": "Streams the file content inline. Records a best-effort download event (counter + audit). Anonymous callers may only fetch published files.", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "Binary file stream.", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Forbidden — anonymous access to an unpublished file, or read RBAC denial.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object or file not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + }, + "put": { + "operationId": "files-update", + "summary": "Update file content and/or tags", + "tags": [ + "files" + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "nullable": true, + "description": "Optional new content; omit for a metadata-only update." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/File" + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + }, + "delete": { + "operationId": "files-delete", + "summary": "Delete a file from an object", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "Deletion result.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/rename": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "put": { + "operationId": "files-rename", + "summary": "Rename a file within its object folder", + "description": "Renames the file. Writes an audit-trail entry and dispatches the `nl.openregister.object.file.renamed` event.", + "tags": [ + "files" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileRenameRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Renamed file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/File" + } + } + } + }, + "400": { + "description": "Name required, or contains invalid characters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "409": { + "description": "A file with the target name already exists in the object folder.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "423": { + "description": "File is locked by another user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/copy": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "post": { + "operationId": "files-copy", + "summary": "Copy a file into another object", + "description": "Copies the file into the target object's folder, resolving name conflicts with a numeric suffix. Writes dual audit-trail entries (`file.copied` on the source, `file.copied_in` on the target) and dispatches `nl.openregister.object.file.copied`.", + "tags": [ + "files" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileCopyMoveRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Copied file (in the target object).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/File" + } + } + } + }, + "400": { + "description": "Target object id missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "403": { + "description": "Caller lacks access to the source or target object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Source or target object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/move": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "post": { + "operationId": "files-move", + "summary": "Move a file into another object", + "description": "Copies the file to the target object then deletes the source. Writes dual audit-trail entries (`file.moved` / `file.moved_in`) and dispatches `nl.openregister.object.file.moved`.", + "tags": [ + "files" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileCopyMoveRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Moved file (in the target object).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/File" + } + } + } + }, + "400": { + "description": "Target object id missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "403": { + "description": "Caller lacks access to the source or target object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Source or target object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "423": { + "description": "Source file is locked.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/versions": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "get": { + "operationId": "files-list-versions", + "summary": "List versions of a file", + "description": "Lists historical versions via the Nextcloud `files_versions` app. Degrades gracefully (current version only) when that app is disabled.", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "Version list.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileVersionList" + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object or file not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/versions/{versionId}/restore": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "versionId", + "in": "path", + "required": true, + "description": "Identifier of the version to restore.", + "schema": { + "type": "string" + } + } + ], + "post": { + "operationId": "files-restore-version", + "summary": "Restore a previous version of a file", + "description": "Rolls the file back to the named version. Writes a `file.version_restored` audit entry and dispatches `nl.openregister.object.file.version_restored`.", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "Restored file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/File" + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object, file, or version not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/lock": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "post": { + "operationId": "files-lock", + "summary": "Lock a file", + "description": "Acquires a distributed-cache-backed lock. Writes a `file.locked` audit entry and dispatches `nl.openregister.object.file.locked`.", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "Lock metadata.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileLockResponse" + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "423": { + "description": "File is already locked by another user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/unlock": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "post": { + "operationId": "files-unlock", + "summary": "Unlock a file", + "description": "Releases a lock. The lock owner may unlock; an administrator may force-unlock another user's lock via `force: true`. Writes `file.unlocked` or `file.force_unlocked` audit entries and dispatches `nl.openregister.object.file.unlocked`.", + "tags": [ + "files" + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUnlockRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Unlock result.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileLockResponse" + } + } + } + }, + "403": { + "description": "Only the lock owner or an administrator may unlock the file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/batch": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "post": { + "operationId": "files-batch", + "summary": "Run a batch action over multiple files", + "description": "Applies publish/depublish/delete/label to up to 100 files in one request, collecting per-file results. Returns HTTP 200 when every file succeeded and HTTP 207 when at least one failed.", + "tags": [ + "files" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileBatchRequest" + } + } + } + }, + "responses": { + "200": { + "description": "All files processed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileBatchResponse" + } + } + } + }, + "207": { + "description": "Partial success — at least one file failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileBatchResponse" + } + } + } + }, + "400": { + "description": "Invalid action, or batch size over the 100-file limit.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/preview": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 256 + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 256 + } + } + ], + "get": { + "operationId": "files-preview", + "summary": "Get a file thumbnail/preview", + "description": "Streams a preview image (cached `max-age=3600`). Anonymous callers may only preview published files; on an unsupported type the response is a 404 carrying a `fallbackIcon` URL.", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "Preview image stream.", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Anonymous preview of an unpublished file, or read RBAC denial.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object/file not found or preview unsupported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilePreviewError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/{fileId}/labels": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "put": { + "operationId": "files-update-labels", + "summary": "Replace a file's labels", + "tags": [ + "files" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileLabelsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Updated file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/File" + } + } + } + }, + "403": { + "description": "Caller lacks access to the object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/objects/{register}/{schema}/{id}/files/download": { + "parameters": [ + { + "name": "register", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "schema", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "get": { + "operationId": "files-download-zip", + "summary": "Download all (or selected) object files as a ZIP", + "description": "Streams a ZIP archive of the object's files and writes a single `file.bulk_downloaded` audit entry capturing the archive contents.", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "ZIP archive stream.", + "content": { + "application/zip": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Object not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + }, + "/index.php/apps/openregister/api/files/{fileId}/download": { + "parameters": [ + { + "name": "fileId", + "in": "path", + "required": true, + "description": "Nextcloud filecache file id.", + "schema": { + "type": "integer" + } + } + ], + "get": { + "operationId": "files-download-by-id", + "summary": "Download a file by its id (object-agnostic)", + "description": "Streams a single file by numeric id without needing the register/schema/object. Records a best-effort download counter increment. Anonymous callers may only download published files.", + "tags": [ + "files" + ], + "responses": { + "200": { + "description": "Binary file stream.", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Anonymous download of an unpublished file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + }, + "404": { + "description": "File not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileError" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "github-issues", + "description": "GitHub issues proxy backing the Features & Roadmap menu component." + }, + { + "name": "files", + "description": "Object file actions: upload, download, rename, copy, move, version, lock, batch, preview, and label management." } ] } diff --git a/openspec/architecture/adr-001-information-architecture.md b/openspec/architecture/adr-001-information-architecture.md new file mode 100644 index 0000000000..c0f3b48122 --- /dev/null +++ b/openspec/architecture/adr-001-information-architecture.md @@ -0,0 +1,206 @@ +# ADR-001: Information architecture — top-level navigation and spec placement + +**Status**: accepted + +**Date**: 2026-05-23 + +## Context + +OpenRegister is a developer-and-admin platform for building registries: typed +Schemas, governed Registers, validated Objects, with a long tail of +capabilities — file attachment, audit trail, RBAC scopes + ABAC policy, +retention/AVG, OpenLineage, NLX, CDC, PII detection, NORA/BIO/Forum +Standaardisatie compliance, plus integrations with NC core apps and external +adapters (BAG/BRP/KvK, ZGW, DSO, OpenProject, xWiki). + +The spec catalog now holds 114 specs. Without a discipline for placing each +spec on a navigation surface, the top-level menu would inflate every time a +new capability landed (one menu entry per ABAC engine, per lineage protocol, +per compliance matrix, per integration). That fails users on two axes: + +- **Audience confusion** — platform admins, register owners, and integrators + navigate the same surface; promoting every capability to the top hides the + data primitives those users came to find. +- **Discoverability collapse** — a 25-item top menu is no better than no menu; + users learn neither path. + +The guiding principle is: **menu items are destinations users navigate to, not +features the app has.** An ABAC engine, a lineage protocol, a compliance +matrix — none of those are destinations. They are properties of destinations. + +This ADR codifies the resulting information architecture so future specs land +in the right place without re-litigating placement every time. + +## Decision + +**Adopt an eight-item top-level navigation organised around data primitives, +with cross-cutting capabilities placed on tabs inside those primitives.** + +The top-level menu is fixed at: + +1. **Registers** — registry catalog (tenant-bounded collections of objects). +2. **Schemas** — type catalog (definitions, evolution, hooks). +3. **Objects** — cross-cutting data browser (search, validation, history). +4. **Integrations** — pluggable integration registry (NC + external). +5. **Audit** — audit/compliance forensics surface. +6. **Workflows** — workflow engine, scheduled actions, retention runs. +7. **Beheer** — platform-ops admin (tenants, auth, policies, environments, + data, compliance, observability, system). +8. **API & Docs** — developer surface (REST/GraphQL/OAS/MCP, no-code, roadmap, + URN, deep links). + +The AI chat companion (`chat-ai`) is a global launcher (sidebar pill), not a +menu item — same pattern as Nextcloud unified search. + +### Numbered rules + +#### Rule 1 — Menu items are data primitives, not features + +Registers, Schemas, Objects, Integrations, Audit, Workflows are *things you +navigate to*. ABAC, NLX, OpenLineage, PII detection, CDC, NORA are +*capabilities of those things*. Capabilities live on a tab inside a primitive +or inside Beheer; they never get their own menu item. + +**Rationale.** Every capability promoted to the menu shrinks the headroom for +genuine destinations and forces all users to scan vocabulary that only one +audience cares about. Tabs let the audience that needs the capability find it +in context (e.g., an integrator finds NLX on the Integrations menu, not by +scanning the top bar). + +**How to apply.** When proposing a new spec, ask: *would a user navigate here +as their first action of the day?* If no, it is a tab or sub-page under a +primitive — not a menu item. If the spec spans multiple primitives (e.g. +`linked-entity-types` touches Schemas and Objects), it gets a tab in each. + +#### Rule 2 — Cross-cutting governance lives in Beheer, organised by topic + +Beheer has exactly eight tabs: Tenants, Auth, Policies, Environments, Data, +Compliance, Observability, System. New governance specs join an existing tab +by topic; they do not earn a new tab unless a topic doesn't fit any existing +one (and even then, the bar is "this is a new governance domain", not "this +is a new spec"). + +**Rationale.** Admins look for "who can share what" or "what runs on a +schedule", not "spec X". Topic-grouping keeps the cognitive cost of the +admin surface constant as specs accrete. Eight tabs is the comfort limit for +horizontal tab strips at typical Nextcloud admin widths. + +**How to apply.** A new compliance spec joins the Compliance tab. A new +observability spec joins Observability. If the spec sits at the platform-ops +level (defaults, quotas, system toggles), it goes into Beheer; if it is +instance-scoped (per-register, per-schema), it goes onto the primitive's +detail page. + +#### Rule 3 — Every primitive gets a consistent tab template + +Registers detail page is always: Overview, Schemas, Objects, Permissions, +Retention, Integrations, Lifecycle, Audit, Settings. Schemas detail page is +always: Definition, Evolution, Hooks, Validation, Read coercion, i18n, +Used-in, Cleanup. Objects detail page is always: Properties, History, Audit, +Lineage, Files, Interactions, Workflow. + +**Rationale.** Consistency across primitives reduces cognitive load — users +learn the tab layout once and apply it to every register or schema they +visit. Tab order encodes priority (what they need most first). + +**How to apply.** A new spec that scopes to a primitive lands on one of the +existing tabs. Adding a new tab requires the same bar as Rule 2: a new domain +of concern, not just a new feature. + +#### Rule 4 — Specs that affect both an instance and an org-level policy appear in both places + +`retention-management` appears per-register (Retention tab) and at org-level +(Workflows: Retention runs; Beheer: Tab Data for defaults). `rbac-scopes` +appears per-register (Permissions tab) and at org-level (Beheer: Tab Auth). +`computed-fields`, `datetime-input-handling`, `extended-field-types`, +`schema-driven-read-coercion`, `mock-registers`, `webhook-payload-mapping` +follow the same pattern. + +**Rationale.** Different users approach these from different directions. A +register owner edits the binding from their register's tab; a platform admin +sets the default from Beheer. Forcing one audience through the other's +surface is a UX tax. The detail page edits the instance; Beheer sets the +defaults. + +**How to apply.** When a spec has both per-instance configuration and +org-wide defaults, mention both placements in the spec's metadata and link +them in the UI (e.g., the per-register Retention tab shows "Inherits from +Beheer → Data" and links to it). + +#### Rule 5 — Developer surface is its own menu, separate from Beheer + +API & Docs (REST, GraphQL, OAS, OpenAPI, MCP, deep links, URN, no-code, +roadmap, NC API compat, ZGW mapping, webhook templates) is its own menu. +Beheer is for platform-ops (tenants, auth, policies, environments). Both +audiences require admin rights, but the work they do is different enough to +warrant the split. + +**Rationale.** Integrators want generated specs, playgrounds, addressing +references, and roadmap visibility. Platform admins want quotas, policies, +audit, compliance matrices. Bundling them would either bury the developer +artefacts inside a governance console or pollute the governance console with +developer surfaces. + +**How to apply.** A new spec aimed at people writing integrations (clients, +adapters, webhooks, OAS extensions, MCP tools, no-code) goes into API & Docs. +A new spec aimed at people running the platform goes into Beheer. + +## Consequences + +**Positive:** + +- The top-level menu stays at 8 items with one slot of headroom — new + capabilities never threaten the navigation budget. +- Spec placement is mechanical once the rules are internalised — proposers + and reviewers stop arguing about menus and argue about capabilities. +- New audiences (integrators vs admins vs register owners) find their work in + the same place each time, because the primitives are stable and the tabs + are templated. +- The pluggable integration registry naturally absorbs new internal NC and + external adapter specs without inflating the menu — a new adapter is a row + on the Integrations list, not a new top-level item. + +**Negative / trade-offs:** + +- Specs that affect multiple primitives need two (or more) UI surfaces and a + cross-link convention; reviewers must remember Rule 4. Mitigated by the + explicit "instance vs default" framing. +- Compliance matrices (NORA / BIO / NEN-7510 / Forum Standaardisatie) live + inside a Beheer tab and don't get their own menu — they may feel + under-promoted to compliance-focused users. Mitigated by linking the + Compliance tab from the Audit menu's landing page. +- The Integrations menu is large (32 placements). Mitigated by grouping + into Internal / External / Protocol-level columns on the list view. + +## Alternatives considered + +- **Promote every capability to a menu item.** Rejected: 25+ menu items is + no menu; it forces all audiences through every other audience's vocabulary. +- **Single "Admin" menu that absorbs Beheer + API & Docs.** Rejected: + conflates platform-ops with integrator workflows; the audiences differ. +- **Capability-first navigation (Compliance, Security, Integrations as + top-level peers).** Rejected: the platform's *unit of work* is the + registry primitive, not the capability — admins start with "which + register" before they start with "which policy". +- **Hide Beheer behind Instellingen** (launchpad pattern). Rejected: + openregister's admin surface is large enough (~28 specs) to warrant a + dedicated menu rather than a settings tab; launchpad's admin surface is much + smaller. + +## Source + +Distilled from the cross-app information-architecture analysis at +`/tmp/ia-launchpad-openregister.md` (openregister section, §B–§F), which +catalogues all 114 openregister specs against this 8-item menu and the +per-primitive tab templates above. + +## Related + +- launchpad mirrors the same discipline with a 7-item menu (Dashboards, + Templates, Reports, Comments, Beheer, Catalog, Instellingen) and the + parallel rule that widget types are catalog entries, not menu items. +- Hydra ADR-022 (`apps-consume-or-abstractions`) — informs why Beheer for OR + is platform-ops specifically (consuming apps don't re-implement the same + governance). +- Hydra ADR-019 (`integration-registry`) — the Integrations menu is the + surface for the cross-app pluggable integration registry. diff --git a/openspec/architecture/adr-002-organisation-uuid-tenancy.md b/openspec/architecture/adr-002-organisation-uuid-tenancy.md new file mode 100644 index 0000000000..1db451baa5 --- /dev/null +++ b/openspec/architecture/adr-002-organisation-uuid-tenancy.md @@ -0,0 +1,60 @@ +# ADR-002: Multi-tenancy via organisation UUID, not Nextcloud groups + +**Status**: accepted (documents the decision as implemented) + +**Date**: 2026-07-07 + +## Context + +OpenRegister scopes registers, objects, and users to tenants. Nextcloud ships a +native group system (`IGroupManager`), and the obvious-looking design is to use +NC groups as tenants. OpenRegister deliberately does not do that: tenancy is +carried by a first-class `Organisation` entity keyed by UUID, persisted in +`openregister_organisations` (`lib/Db/OrganisationMapper.php`), and threaded +through the query path (`lib/Service/Object/SearchQueryHandler.php`, +`lib/Service/RequestScopedCache.php`, `lib/Db/RealtimeEventMapper.php`). + +The decision was load-bearing from the start but never written down. Consuming +apps and reviewers keep re-deriving it — and getting it wrong breaks per-tenant +isolation silently, because a query scoped to an NC group id simply matches +nothing (or worse, everything, when the filter is dropped). + +## Decision + +**A tenant is an `Organisation` object identified by UUID. Nextcloud groups are +never a tenancy boundary in OpenRegister.** + +### Numbered rules + +#### Rule 1 — Organisation UUID is the only tenant key + +Every tenant-scoped query, cache key, event stream, and RBAC evaluation uses +the organisation UUID from `openregister_organisations`. Code MUST NOT use an +NC group id, group display name, or `IGroupManager` membership as a tenant +discriminator. + +**Rationale.** Organisations carry OR-specific state (default register +settings, membership metadata) that NC groups cannot; and NC group ids are +admin-editable strings, unsuitable as stable foreign keys. + +#### Rule 2 — Membership resolves through OrganisationMapper + +"Which tenants does this user belong to?" is answered by +`OrganisationMapper` (user–organisation relationship rows), not by group +membership. Consuming apps needing tenant context call OR's APIs; they do not +reimplement the join. + +#### Rule 3 — NC groups remain valid for RBAC principals, not tenancy + +Schema/register `authorization` blocks may reference NC groups as *permission +principals* (who may read/write). That is orthogonal to tenancy: a group can +appear in RBAC while the tenant boundary stays the organisation UUID. + +## Consequences + +- (+) Stable, rename-proof tenant keys; tenant metadata lives with the tenant. +- (+) Cross-app tenancy contract is a single mapper, testable in isolation. +- (−) Two membership systems coexist (NC groups for RBAC principals, + organisations for tenancy); onboarding docs must state the split explicitly. +- Follow-up: consuming-app docs should link this ADR wherever `organisation` + appears in an API contract. diff --git a/openspec/architecture/adr-003-immutable-hash-chained-audit-trail.md b/openspec/architecture/adr-003-immutable-hash-chained-audit-trail.md new file mode 100644 index 0000000000..3bdb30e15b --- /dev/null +++ b/openspec/architecture/adr-003-immutable-hash-chained-audit-trail.md @@ -0,0 +1,77 @@ +# ADR-003: Immutable, hash-chained audit trail + +**Status**: accepted (documents the decision as implemented; see the write-path +gap in `openspec/changes/fix-audit-hash-chain-write-path`) + +**Date**: 2026-07-07 + +## Context + +OpenRegister is the system of record for governed data used by compliance-bound +consumers (Archiefwet, NEN-7510, GDPR Art. 30). Those consumers need +*tamper-evidence*: proof that an audit entry was not altered or deleted after +the fact, independent of Nextcloud's own activity log (which is mutable by +anyone with DB or file access). + +OpenRegister implements this as a SHA-256 **hash chain** over audit-trail rows +(`lib/Service/AuditHashService.php`): each entry's `hash` is +`sha256(previousHash + canonicalJSON(entry))`, seeded by a fixed genesis +constant `GENESIS_SEED = 'openregister-genesis-v1'` +(`lib/Service/AuditHashService.php:52`). The `hash`/`previousHash` fields are +themselves excluded from the canonicalised payload. `verifyChain()` walks the +chain and reports the first break. + +ADR-022 (company-wide) *references* this audit trail as an abstraction that +consuming apps must reuse rather than reimplement, but the mechanism — genesis +seed, canonicalisation rules, verification semantics, and the immutability +guarantee — has no OpenRegister-side ADR. That gap has real consequences: the +write path was shipped without wiring the hash fields (fixed separately), and +`LogService::deleteLog()` exists as an un-gated deletion method that would +violate the immutability contract if ever wired. + +## Decision + +**The audit trail is an append-only, SHA-256 hash-chained log. Entries are never +updated or deleted through any public surface. Tamper-evidence is a first-class +guarantee, not a best-effort side effect.** + +### Numbered rules + +#### Rule 1 — Every audit row is hash-chained at insert time + +The two insert paths — `AuditTrailMapper::createAuditTrail()` and +`createAuditTrailEntry()` — MUST compute `previousHash` from the current chain +head (`AuditHashService::getLastHash()`) and set the row's `hash` +(`computeHash()`) before persisting. A row inserted with a null hash is a +defect: `verifyChain()` treats null-hash rows as *skipped*, so an all-null table +reports `valid: true` with `entriesVerified: 0` — a false guarantee. + +#### Rule 2 — No update or delete surface for audit entries + +Controllers MUST reject mutation of audit entries. +`AuditTrailController::destroy()`/`destroyMultiple()` returning HTTP 405 is the +canonical behaviour. Service-layer deletion helpers (`LogService::deleteLog()`, +`deleteLogs()`) MUST NOT exist without an explicit admin gate *and* a recorded +"immutability override" audit entry — matching `clearAll()`'s `requireAdmin()`. + +#### Rule 3 — Governance-config changes are audited durably, not only logged + +Authorization/RBAC changes on registers and schemas +(`AuthorizationAuditService`) MUST be persisted as audit-trail rows, not only +written to `ILogger`. Application logs are rotated and mutable; "who changed +access rules and when" is compliance evidence and belongs in the hash chain. + +#### Rule 4 — Canonicalisation is stable and versioned + +The canonical JSON form and genesis seed are part of the chain's identity. +Changing either breaks verification of pre-existing rows, so any change is a +migration event with a new seed version, never an in-place edit. + +## Consequences + +- (+) Independent, portable tamper-evidence for compliance consumers. +- (+) A single verification routine (`verifyChain()`) any auditor can run. +- (−) Insert paths carry hashing cost and MUST be the only way to write audit + rows; bulk/back-door inserts silently break the chain. +- Follow-up: the write-path wiring and the `LogService` deletion methods are + addressed in `openspec/changes/fix-audit-hash-chain-write-path`. diff --git a/openspec/architecture/adr-004-credential-broker-custody.md b/openspec/architecture/adr-004-credential-broker-custody.md new file mode 100644 index 0000000000..8eea7aa8b9 --- /dev/null +++ b/openspec/architecture/adr-004-credential-broker-custody.md @@ -0,0 +1,74 @@ +# ADR-004: Credential broker — apps never hold third-party secrets + +**Status**: accepted (documents the decision as implemented across the +`credential-broker`, `credential-doriath-leaf`, and `credential-provider-doffin` +changes) + +**Date**: 2026-07-07 + +## Context + +Conduction apps increasingly act on external providers (GitHub, GitLab, Doffin, +…) on a user's behalf. The naive design has each app store the user's personal +access token itself, spreading long-lived secrets across every app with no +central revocation, no per-app scoping, and no audit. + +OpenRegister implements a **credential broker** instead +(`lib/Service/Credential/`): the user gives a secret to OpenRegister once; apps +make outbound calls *through* OpenRegister and never see the secret. This is a +security-critical custody boundary that is currently documented only in project +memory, not in any ADR a reviewer or new app author would find. + +## Decision + +**Third-party secrets live in a vault behind OpenRegister. Consuming apps +receive short-lived, per-app, signed capability tokens — never the secret — and +every outbound call passes through a constrained proxy with a fixed allow-list.** + +### Numbered rules + +#### Rule 1 — Secret never leaves the vault; metadata never carries the secret + +The secret is stored via a pluggable `CredentialStore` +(`DoriathCredentialStore` → the Doriath vault leaf; `NextcloudVaultCredentialStore` +→ `ICredentialsManager` fallback), keyed by the credential UUID. The +`credential` OR object holds only metadata (`name`, `provider`, `owner`, +`allowedApps[]`, `createdAt`) and MUST NOT contain a secret in any property, +export, audit row, or GraphQL projection. + +#### Rule 2 — Apps present signed, per-app, expiring tokens + +`CredentialAppTokenService` mints HMAC-signed tokens bound to `{appId, +credentialId, iat, exp}` and verifies them with constant-time `hash_equals`. +The per-app secret is looked up from the `appId` *inside* the signed payload, +not asserted by the caller, so a token cannot be forged without the registered +app secret. + +#### Rule 3 — The proxy is constrained by a runtime-immutable catalogue + +`lib/Settings/credential-providers.json` host-locks each provider's `baseUrl`, +declares the auth-header `authScheme`, and enumerates `allowRules[]` (method + +path-prefix). The catalogue is read-only at runtime: there is no API that +creates, updates, or deletes a provider or an allow-rule. Widening the proxy's +reach requires a code review and release. + +#### Rule 4 — Four fail-closed guards on every brokered call + +`CredentialBrokerService` enforces, in order: owner check → allowed-app check → +allow-rule (method+path) check → host-lock check. Every denial funnels through +a single static 403 with the reason logged **secret-free** (credential UUID +only). Store errors (decrypt/lookup) fail closed to denial. + +## Consequences + +- (+) One revocation point; per-app scoping; secret-free audit. +- (+) The blast radius of a compromised consuming app is bounded to its + allow-rules, not "all the user's tokens." +- (−) Every provider integration requires a catalogue entry + release; apps + cannot self-serve new hosts. +- Known hardening follow-ups (token method/path binding, private-key temp-file + handling, JWT algorithm pinning) are tracked in + `openspec/changes/fix-jwt-algorithm-confusion` and + `openspec/changes/harden-credential-token-binding`. The canonical spec still + lives under `openspec/changes/credential-broker/`; promote it to + `openspec/specs/credential-broker/` via `/opsx-sync`. diff --git a/openspec/architecture/adr-005-register-import-via-repair-steps.md b/openspec/architecture/adr-005-register-import-via-repair-steps.md new file mode 100644 index 0000000000..633d19eac9 --- /dev/null +++ b/openspec/architecture/adr-005-register-import-via-repair-steps.md @@ -0,0 +1,67 @@ +# ADR-005: OpenRegister seeds its own registers via Repair steps, not self-import + +**Status**: accepted (documents the decision as implemented) + +**Date**: 2026-07-07 + +## Context + +Consuming apps ship a register descriptor JSON in `lib/Settings/` and +OpenRegister imports it when the app is installed. It is natural to assume +OpenRegister does the same for its *own* built-in registers (credential-broker, +DSAR, risk-level metadata). It does not — and that surprises app authors +repeatedly (it recurs as a cross-session "gotcha"). + +OpenRegister does not self-import its own register JSON at install/upgrade. +Instead, each OR-owned register is materialised by a dedicated `Repair` step +class under `lib/Repair/` (`ImportCredentialBrokerRegister`, +`ImportDsarRegisters`, `RegisterOpenRegisterWithDoriath`, +`RegisterRiskLevelMetadata`, `SeedAppVirtualSchemas`, +`SeedDirectoryVirtualSchemas`) that runs on `occ upgrade`. + +The reason is a bootstrap ordering problem: the import machinery +(`ObjectService`, mappers, magic tables) is itself part of OpenRegister, so at +the moment OR's own app boots there is no already-initialised OR to import +into. Repair steps run at a point in the lifecycle where the schema/table +infrastructure is available and can be invoked idempotently. + +## Decision + +**OpenRegister-owned registers and schemas are seeded by idempotent `Repair` +step classes under `lib/Repair/`, invoked on `occ upgrade`. OpenRegister does +not self-import its own `lib/Settings/*_register.json` at app boot.** + +### Numbered rules + +#### Rule 1 — OR-owned register JSON is materialised by a Repair step + +Any new OR-owned register descriptor MUST be accompanied by a `lib/Repair/` +step that imports it (via `importFromFilePath()` or equivalent). Shipping the +JSON alone does nothing at runtime. + +#### Rule 2 — Repair steps are idempotent and slug-matched + +Steps MUST be safe to run on every upgrade: match existing registers/schemas by +slug, create-or-update, never duplicate. A second `occ upgrade` must be a no-op. + +#### Rule 3 — Descriptor is the source of truth; the step is the delivery + +The register JSON in `lib/Settings/` remains the declarative source (reviewable, +diffable). The Repair step is only the delivery mechanism — it MUST NOT embed a +second, divergent copy of the schema inline. + +#### Rule 4 — Consuming apps are unaffected + +This decision is OR-internal. Consuming apps continue to ship descriptors that +OR imports for them; they do not write Repair steps. + +## Consequences + +- (+) Avoids the boot-time chicken-and-egg; seeding happens when infrastructure + is ready. +- (+) Idempotent upgrades; predictable re-seed behaviour. +- (−) An OR-owned register that ships JSON without a Repair step silently never + appears — a recurring author error this ADR exists to prevent. +- Relationship to ADR-037 (canonical req-id / modular-config): this ADR is about + *who imports OR's own registers*; ADR-037 is about config fragment structure. + They are complementary, not overlapping. diff --git a/openspec/architecture/adr-006-publish-is-rbac-scope.md b/openspec/architecture/adr-006-publish-is-rbac-scope.md new file mode 100644 index 0000000000..62e63753a8 --- /dev/null +++ b/openspec/architecture/adr-006-publish-is-rbac-scope.md @@ -0,0 +1,60 @@ +# ADR-006: "Published" is an RBAC scope, not a data field + +**Status**: accepted (documents the decision as implemented) + +**Date**: 2026-07-07 + +## Context + +Consumers such as opencatalogi (DCAT-AP publishing) need a notion of objects +being "published" — visible to anonymous/public readers — versus internal. +The intuitive design is a boolean data property (`published: true`) on the +object that a user flips. OpenRegister deliberately does not model it that way, +and consumers repeatedly re-derive the actual rule. + +Publication is modelled as an **RBAC/authorization concern at the schema level** +(`lib/Db/Schema.php` carries an `authorization` JSON block — +`getAuthorization()`/`setAuthorization()`, typed `json`, at +`lib/Db/Schema.php:84,262,467`), not as a data field on `ObjectEntity`. Whether +an object is readable by the `public`/anonymous principal is decided by the +schema's authorization configuration and the RBAC evaluation on read, not by a +`@self.published` value the caller can set directly. + +## Decision + +**Publication state is expressed through schema-level RBAC scopes, not a +data property. There is no authoritative `published` boolean on the object that +consumers flip to change visibility.** + +### Numbered rules + +#### Rule 1 — Visibility is decided by RBAC on read, not a data flag + +Whether an object is returned to a given principal (including the anonymous +public principal) is determined by the schema `authorization` block evaluated +during `find`/`search`. A data property named `published` (if present for +display) is descriptive metadata, not the access-control decision. + +#### Rule 2 — To publish, grant a read scope, do not set a field + +Making objects of a schema publicly readable means configuring the schema's +authorization to grant read to the public principal — an RBAC change — not +writing a boolean onto each object. + +#### Rule 3 — Consumers must not treat `published` as a security boundary + +opencatalogi and other DCAT-AP consumers MUST rely on OR's RBAC evaluation for +public exposure. Filtering client-side on a `published` field is not a security +control; the server decides visibility. + +## Consequences + +- (+) A single, server-enforced visibility model; no gap between a data flag and + the actual permission. +- (+) Publishing a whole schema's worth of objects is one RBAC change, not a + per-object write. +- (−) "Publish this one object" is expressed differently than authors expect; + needs to be surfaced clearly in publishing UIs and docs. +- Cross-reference: this is the OR-side companion to the fleet memory note + "publish is RBAC not @self.published" and underpins the opencatalogi + publishing model (ADR-022 apps-consume-OR-abstractions). diff --git a/openspec/architecture/adr-007-single-builtin-search-backend.md b/openspec/architecture/adr-007-single-builtin-search-backend.md new file mode 100644 index 0000000000..d0d42d4b04 --- /dev/null +++ b/openspec/architecture/adr-007-single-builtin-search-backend.md @@ -0,0 +1,68 @@ +# ADR-007: A single built-in database (Magic-Tables) search backend + +**Status**: accepted (documents the decision as implemented; supersedes the +earlier pluggable-backend assumption) + +**Date**: 2026-07-07 + +## Context + +OpenRegister historically shipped pluggable search backends (external Solr and +Elasticsearch) alongside the built-in database search. The abstraction lingers +in names — `lib/Service/Settings/SearchBackendHandler.php`, `lib/Service/Search/`, +vectorisation handlers — which reads like a live strategy pattern with multiple +selectable backends. It is not, any more. + +At HEAD, the external Solr and Elasticsearch backends have been **removed**. The +built-in database search over the Magic-Tables (`lib/Db/MagicMapper`) is the +sole backend. `SearchBackendHandler` documents this in its own header ("the +external Solr and Elasticsearch backends were removed, the built-in database +(Magic-Tables) search is the sole backend", +`lib/Service/Settings/SearchBackendHandler.php:34-36`) and +`updateSearchBackendConfig()` rejects any value other than `database` +(`:128-138`). The "solr hotfix" comments that disabled read-path RBAC in the +mappers (`RegisterMapper.php:246,500`, `SchemaMapper.php:261,540`) are the +residue of that removed integration. + +Leaving this undocumented has two costs: authors reason about a backend +selection that no longer exists, and the disabled-RBAC hotfix that was +introduced *for* Solr is still in place with a "uncomment when ready" comment +and no owning decision. + +## Decision + +**The built-in database (Magic-Tables) search is OpenRegister's only search +backend. `SearchBackendHandler` is retained only as an API-compatibility shim +that reports `database` and rejects anything else. Vector search is an +augmentation of the database backend, not a separate selectable backend.** + +### Numbered rules + +#### Rule 1 — `database` is the only valid backend value + +Any config or API attempting to select a non-`database` backend MUST be +rejected. `SearchBackendHandler` exists to enforce that, not to switch engines. + +#### Rule 2 — No dormant external-backend code paths + +Code paths, comments, or config that imply a live Solr/Elasticsearch backend +are misleading and should be removed or clearly marked as historical. In +particular, the "remove this hotfix for solr — uncomment when ready" RBAC +bypasses in the register/schema mappers are no longer justified by an active +Solr integration and MUST be resolved (see +`openspec/changes/restore-register-schema-rbac-enforcement`). + +#### Rule 3 — Adding a backend is an ADR-level decision + +Reintroducing an external search backend is a new architectural decision +requiring its own ADR (selection criteria, RBAC interaction, index-sync +contract), not a config toggle. + +## Consequences + +- (+) One code path to reason about, test, and secure. +- (+) Removes the standing justification for the disabled read-path RBAC. +- (−) Scaling full-text/vector search is now bounded by what the database + backend can do; heavy-search consumers have no alternate engine to select. +- Follow-up: the disabled-RBAC hotfix is addressed in + `openspec/changes/restore-register-schema-rbac-enforcement`. diff --git a/openspec/architecture/adr-008-shared-format-validators.md b/openspec/architecture/adr-008-shared-format-validators.md new file mode 100644 index 0000000000..772cb4e42d --- /dev/null +++ b/openspec/architecture/adr-008-shared-format-validators.md @@ -0,0 +1,78 @@ +# ADR-008: Shared Format validators in `lib/Formats/` — one home per validation rule + +**Status**: accepted (codifies the OR-side application of company ADR-011; see +the UUID gap addressed in `openspec/changes/extract-uuid-format-validator`) + +**Date**: 2026-07-07 + +## Context + +Company ADR-011 requires: before implementing any utility (validation, +formatting, parsing), search for an existing implementation and reuse it. +OpenRegister has a designated home for JSON-Schema format validators — +`lib/Formats/` — where `BsnFormat`, `Iso8601DateTimeFormat`, and `SemVerFormat` +each live as a single `Opis\JsonSchema\Format` implementation, and a +`DateTimeNormalizer` service centralises user-supplied datetime parsing with an +explicit "direct `new DateTime($value)` on user data is forbidden" contract +(`lib/Service/DateTimeNormalizer.php:11-13`). + +In practice the discipline has eroded: + +- The UUID-validation regex + `'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i'` is + copy-pasted across 35+ call sites (controllers, services, save/render + handlers, GraphQL scalar, facet handler) with subtly divergent variants + (prefixed forms, 32-hex forms), and there is **no** `UuidFormat` in + `lib/Formats/`. +- `ProcessingLogController::optionalDateParam()` calls + `new DateTime($value)` directly on a request parameter + (`lib/Controller/ProcessingLogController.php:359`), bypassing + `DateTimeNormalizer`. +- Slug generation is reimplemented inline in `RegisterMapper::cleanObject()` + instead of reusing `SchemaMapper::generateSlug()`. + +Divergent copies mean a fix to one does not propagate, and validation drift +becomes a correctness/security hazard (e.g. `BsnFormat`'s elfproef accepting +all-zero input, addressed in the same change). + +## Decision + +**Every reusable validation/format/parse rule has exactly one implementation. +Format validators live in `lib/Formats/`; user-input datetime parsing goes +through `DateTimeNormalizer`; identifier and slug helpers are shared, not +inlined.** + +### Numbered rules + +#### Rule 1 — One validator per rule, in `lib/Formats/` + +UUID, BSN, ISO-8601, SemVer, and any future format check is a single +`lib/Formats/` class. Call sites reference it (DI or static helper); they do not +carry their own regex copy. A duplicated regex is a defect to consolidate. + +#### Rule 2 — No `new DateTime($userValue)` outside DateTimeNormalizer + +Any conversion of a user-supplied datetime string to a `DateTime` MUST delegate +to `DateTimeNormalizer`. Direct construction on request input is forbidden, per +the class's own contract. + +#### Rule 3 — Shared identifier/slug helpers + +Slug generation, UUID formatting, and similar identifier utilities are shared +helpers reused across mappers and services, never re-inlined per class. + +#### Rule 4 — Validators reject known-invalid sentinels + +A format validator MUST reject structurally-valid-but-semantically-invalid +sentinels where they exist (e.g. the all-zero BSN, over-length numeric strings), +not merely pass a checksum on padded input. + +## Consequences + +- (+) A single place to fix a validation bug; no silent drift between copies. +- (+) Enforceable mechanically (a lint/grep gate can flag inline UUID regexes + and raw `new DateTime` on request params). +- (−) Requires a one-time consolidation sweep of the existing 35+ UUID sites. +- Follow-up: `openspec/changes/extract-uuid-format-validator` performs the sweep, + fixes the BSN sentinel gap, and routes the `ProcessingLogController` datetime + through `DateTimeNormalizer`. diff --git a/openspec/architecture/adr-009-performance-invariants.md b/openspec/architecture/adr-009-performance-invariants.md new file mode 100644 index 0000000000..5fe82160ff --- /dev/null +++ b/openspec/architecture/adr-009-performance-invariants.md @@ -0,0 +1,89 @@ +# ADR-009: Performance invariants for the object hot path and write side + +**Status**: accepted (codifies performance rules the object read/write path must +hold; see the optimization changes under `openspec/changes/` that bring the code +into compliance) + +**Date**: 2026-07-07 + +## Context + +OpenRegister is the foundation app for ~15 consuming apps. Its object read +(list/search/render) and write (save/bulk) paths run on every request those apps +make, so a constant-factor inefficiency there is multiplied across the whole +fleet. A performance review at HEAD found a consistent class of problem: work +that is a function of the *schema* (or of the *request*) is being recomputed per +*object*, and cache-invalidation is instance-wide ("nuclear clear") on every +write. Individually cheap operations — a property scan, a group lookup, a JSON +round-trip, a `Validator` construction — become O(objects × N) because they sit +inside the per-object loop instead of being hoisted or memoized. + +There was no written performance contract, so these regressions passed review +individually. This ADR establishes the invariants the hot path must satisfy so +future changes can be checked against them. + +## Decision + +**Per-object work is bounded to what genuinely varies per object. Schema-derived +and request-derived values are computed once and reused. Cache invalidation is +scoped, never instance-wide. Outbound I/O never blocks the write response.** + +### Numbered rules + +#### Rule 1 — Schema-derived values are computed once per schema, not per object + +Anything that depends only on the schema — `hasPropertyAuthorization()`, +`getPropertiesWithAuthorization()`, `hasComputedProperties()`, the cleaned +validation schema, the compiled validator — MUST be computed once per schema per +render/save pass and reused across all objects of that schema. The correct +pattern already exists in `lib/Service/Object/QueryHandler.php:221` +(`$checkedSchemas[$schemaId] = …`); it MUST be applied on the render and +validate paths too. + +#### Rule 2 — Request-invariant values are memoized per request + +Values that cannot change within a request — the current user's group ids, +admin status, resolved registers/schemas — MUST be resolved once and cached for +the request's duration. Repeated `getUserGroupIds()`/`isAdmin()` calls per +property per object are prohibited. + +#### Rule 3 — One request-scoped cache, not many ad-hoc ones + +There is exactly one request-scoped entity cache. The parallel per-request caches +(`SchemaMapper::$findCache`, `RegisterMapper::$findCache`, +`RenderObject::$registersCache`/`$schemasCache`) MUST be consolidated onto the +single `RequestScopedCache` built for this purpose (currently dead code), or that +class MUST be removed — but three uncoordinated caches for the same entities is +not permitted. + +#### Rule 4 — Cache invalidation is scoped + +A write MUST NOT clear the entire distributed query/aggregation cache. Cache keys +MUST carry a `register:schema` (or finer) prefix so invalidation targets only the +affected bucket. `queryCache->clear()` / `AggregationCache::evictForSchema()` +doing an instance-wide wipe on every CUD is prohibited; bulk operations MUST +collapse invalidation to one call per affected bucket, not one per item. + +#### Rule 5 — Outbound I/O is asynchronous to the write + +Webhook delivery and any other outbound HTTP triggered by an object write MUST be +dispatched to a background job, including the first attempt. A write response's +latency MUST NOT depend on a third party's availability. Cheap "is anything +subscribed?" checks MUST short-circuit *before* serializing payloads. + +#### Rule 6 — No unbounded full-table load on a user-facing request + +Full-table warmups/scans (e.g. `warmupNameCache()`) MUST run only in background +jobs, never be auto-triggered synchronously inside a request. + +## Consequences + +- (+) Object list/search cost scales with page size, not with schema width × + page size; write latency is decoupled from webhook targets. +- (+) The cache actually serves scoped hits instead of behaving as a global cache + wiped on every write. +- (−) Requires a memoization/caching discipline that a mechanical gate should + eventually enforce (e.g. flag `getUserGroupIds` inside a per-object loop). +- Follow-up changes: `optimize-object-render-hot-path`, + `scope-cache-invalidation`, `async-webhook-delivery`, + `consolidate-request-scoped-cache` under `openspec/changes/`. diff --git a/openspec/archive/2026-03-25-contacts-actions/design.md b/openspec/archive/2026-03-25-contacts-actions/design.md new file mode 100644 index 0000000000..c38842e2ac --- /dev/null +++ b/openspec/archive/2026-03-25-contacts-actions/design.md @@ -0,0 +1,206 @@ +# Design: Contacts Actions + +## Approach + +Implement a Nextcloud Contacts Menu provider that bridges the Contacts/CardDAV ecosystem with OpenRegister entity data. The backend consists of two PHP classes: a `ContactsMenuProvider` that implements `OCP\Contacts\ContactsMenu\IProvider` and processes contact entries, and a `ContactMatchingService` that handles entity matching with APCu caching. A new API endpoint exposes the matching logic for reuse by the `mail-sidebar` change. + +The design leverages existing infrastructure: +- **Data access**: Uses `ObjectService::searchObjects()` for querying objects by property values across all registers and schemas. +- **URL resolution**: Uses `DeepLinkRegistryService::resolveUrl()` and `resolveIcon()` for consuming-app aware links and icons. +- **Metadata**: Uses `SchemaMapper` and `RegisterMapper` for schema/register names in count badges and action labels. +- **Caching**: Uses Nextcloud's `ICacheFactory` to obtain an APCu cache instance (falls back to memory cache if APCu is unavailable). + +## Architecture + +``` +Nextcloud Contacts Menu (core UI) + | + v +ContactsMenuProvider (PHP, implements IProvider) + |-- process(IEntry) --> Extract email/name/org from contact entry + |-- matchEntities() --> ContactMatchingService + |-- injectActions() --> Action registry lookup + ILinkAction creation + |-- injectCountBadge() --> Summary count action (highest priority) + | + v +ContactMatchingService (PHP, shared service) + |-- matchContact() --> Combined matching (email + name + org) + |-- matchByEmail() --> ObjectService search with APCu cache + |-- matchByName() --> ObjectService search with APCu cache + |-- matchByOrganization() --> ObjectService search with APCu cache + |-- invalidateCache() --> Called from ObjectService::saveObject() + | + v +ContactsController (PHP, API endpoint) + |-- match() --> GET /api/contacts/match?email=&name=&organization= +``` + +## Files Affected + +### New Files + +- **`lib/Contacts/ContactsMenuProvider.php`** -- Main contacts menu provider class. Implements `OCP\Contacts\ContactsMenu\IProvider`. Constructor-injected with `ContactMatchingService`, `DeepLinkRegistryService`, `IURLGenerator`, `IL10N`, `LoggerInterface`. The `process(IEntry $entry)` method: + 1. Extracts email address(es) from `$entry->getEMailAddresses()` + 2. Extracts full name from `$entry->getFullName()` + 3. Extracts organization from `$entry->getProperty('ORG')` (vCard ORG field) + 4. Calls `ContactMatchingService::matchContact()` with extracted metadata + 5. If matches found: queries action registry for `context: "contact"` actions, resolves URL templates with contact placeholders, creates `ILinkAction` entries via `$entry->addAction()` + 6. Adds a count badge summary action with highest priority + +- **`lib/Service/ContactMatchingService.php`** -- Shared entity matching service. Constructor-injected with `ObjectService`, `SchemaMapper`, `RegisterMapper`, `ICacheFactory`, `LoggerInterface`. Provides: + - `matchContact(string $email, ?string $name, ?string $organization): array` -- Combined matching with deduplication + - `matchByEmail(string $email): array` -- Primary matching by email property (case-insensitive, exact match) + - `matchByName(string $name): array` -- Secondary matching by name properties (fuzzy, lower confidence) + - `matchByOrganization(string $organization): array` -- Tertiary matching by organization name + - `invalidateCache(string $email): void` -- Clears APCu cache entry for a specific email + - `invalidateCacheForObject(array $object): void` -- Extracts email-like property values and invalidates each + - `getRelatedObjectCounts(array $matches): array` -- Groups matched entities by schema and returns counts (e.g., `['Zaken' => 3, 'Leads' => 1]`) + +- **`lib/Controller/ContactsController.php`** -- API controller for the contact matching endpoint. Extends `OCSController`. Constructor-injected with `ContactMatchingService`, `DeepLinkRegistryService`, `IRequest`, `IL10N`. Provides: + - `match()` -- Handles `GET /api/contacts/match` with query parameters `email`, `name`, `organization`. Returns JSON with `matches`, `total`, `cached` fields. + +### Modified Files + +- **`lib/AppInfo/Application.php`** -- Add `$context->registerContactsMenuProvider(ContactsMenuProvider::class)` in the registration method, alongside the existing `registerSearchProvider` call. Add import for the new class. + +- **`lib/Service/ObjectService.php`** -- Add a hook in `saveObject()` to call `ContactMatchingService::invalidateCacheForObject()` when an object with email-type properties is saved. This is done by checking if the saved object has properties that look like email addresses and invalidating corresponding cache entries. + +- **`lib/Service/DeepLinkRegistryService.php`** -- Extend URL template resolution to support contact-specific placeholders: `{contactId}`, `{contactEmail}`, `{contactName}`, `{entityId}`. The existing `resolveUrl()` method's placeholder replacement logic is extended with a new `$contactContext` parameter that provides these values. + +- **`appinfo/routes.php`** -- Add the contact matching route: + ```php + ['name' => 'contacts#match', 'url' => '/api/contacts/match', 'verb' => 'GET'], + ``` + +- **`l10n/en.json`** / **`l10n/nl.json`** -- Add translation strings for action labels, count badges, and error messages. + +## Entity Matching Strategy + +### Email Matching (Highest Confidence) +Email matching is the primary identification mechanism. The service searches across all registers and schemas for objects with properties whose value matches the given email address. The search uses `ObjectService::searchObjects()` with a filter on properties that contain the email value. + +**Implementation approach:** +1. Build a search filter: `{'_search': 'jan@example.nl'}` using the global search to find objects containing the email string +2. Post-filter results to confirm the email appears in a property that semantically represents an email (property name contains "email", "e-mail", "mail", or the schema property is typed as `format: email`) +3. Assign confidence score: `1.0` for exact email match + +### Name Matching (Medium Confidence) +Name matching is secondary. The service searches for objects with name-like properties that match the contact's display name. + +**Implementation approach:** +1. Split the display name into parts (e.g., "Jan de Vries" -> ["Jan", "de", "Vries"]) +2. Search using `ObjectService::searchObjects()` with `{'_search': 'Jan de Vries'}` +3. Post-filter to confirm name parts appear in name-like properties (property name contains "naam", "name", "voornaam", "achternaam", "firstName", "lastName") +4. Assign confidence score: `0.7` for full name match, `0.4` for partial match + +### Organization Matching (Lowest Confidence) +Organization matching identifies related organization entities. + +**Implementation approach:** +1. Search using `ObjectService::searchObjects()` with `{'_search': 'Gemeente Tilburg'}` +2. Post-filter to confirm the value appears in organization-like properties (property name contains "organisatie", "organization", "bedrijf", "company", "naam") +3. Only match objects in schemas that are semantically "organization" schemas (heuristic: schema name contains "organisat", "company", "bedrijf") +4. Assign confidence score: `0.5` for exact organization name match + +### Deduplication +When combining results from email, name, and organization matching, entities are deduplicated by object UUID. The highest confidence match type is retained. + +## APCu Cache Design + +``` +Cache key format: "or_contact_match_email_{sha256(lowercase(email))}" +Cache key format: "or_contact_match_name_{sha256(lowercase(name))}" +Cache key format: "or_contact_match_org_{sha256(lowercase(org))}" +TTL: 60 seconds +``` + +The cache stores serialized match result arrays. Cache is obtained via `ICacheFactory::createDistributed('openregister_contacts')`, which uses APCu if available or falls back to Nextcloud's default cache backend. + +**Cache invalidation** happens in two ways: +1. **TTL expiry**: After 60 seconds, entries are automatically evicted. +2. **Active invalidation**: When `ObjectService::saveObject()` processes an object, if the object has email-like properties, the corresponding cache entries are invalidated via `ContactMatchingService::invalidateCacheForObject()`. + +## Action Injection Flow + +``` +1. ContactsMenuProvider::process(IEntry $entry) +2. -> Extract email, name, org from $entry +3. -> ContactMatchingService::matchContact(email, name, org) +4. -> If matches found: +5. a. Get actions from action registry with context: "contact" +6. b. For each action + each matched entity: +7. - Resolve URL template placeholders: +8. {contactId} -> $entry->getProperty('UID') +9. {contactEmail} -> urlencode($email) +10. {contactName} -> urlencode($name) +11. {entityId} -> $match['uuid'] +12. - Create ILinkAction: +13. ->setName($action['label'] . ' (' . $match['title'] . ')') +14. ->setHref($resolvedUrl) +15. ->setIcon($action['icon'] ?? $deepLinkIcon) +16. ->setPriority(10) +17. - $entry->addAction($action) +18. c. Add count badge action (priority 0, renders first): +19. ->setName("3 zaken, 1 lead, 5 documenten") +20. ->setHref(openregister search URL filtered by email) +21. ->setIcon(openregister app icon) +22. ->setPriority(0) +23. -> If no action registry actions found but matches exist: +24. - Add default "View in OpenRegister" action per matched entity +``` + +## Action Registry Integration + +The contacts-actions feature depends on the `action-registry` change to provide registered actions. Until the action registry is implemented, the provider SHALL: +1. Check if the action registry service class exists (via DI container) +2. If available: query for actions with `context: "contact"` +3. If not available: fall back to adding only the default "View in OpenRegister" / "Bekijk in OpenRegister" action for each matched entity + +This graceful degradation ensures the contacts menu integration works even before the action registry is fully implemented. + +## API Response Format + +```json +{ + "matches": [ + { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "register": {"id": 5, "title": "Gemeente"}, + "schema": {"id": 12, "title": "Medewerkers"}, + "title": "Jan de Vries", + "matchType": "email", + "confidence": 1.0, + "properties": { + "email": "jan@example.nl", + "functie": "Beleidsmedewerker" + }, + "url": "/apps/procest/#/medewerkers/550e8400-e29b-41d4-a716-446655440000", + "icon": "/apps/procest/img/app-dark.svg" + } + ], + "total": 1, + "cached": true +} +``` + +## Error Handling + +- `ContactsMenuProvider::process()` catches all exceptions and logs them at warning level. The contacts menu SHALL never break due to OpenRegister errors. +- `ContactMatchingService` catches database exceptions and returns empty results. Cache failures (APCu unavailable) are logged and the service falls back to uncached operation. +- `ContactsController::match()` returns appropriate HTTP status codes: 200 (success), 400 (missing parameters), 401 (unauthenticated), 500 (internal error). +- Missing or uninstalled Contacts app: The provider is registered regardless; if Nextcloud never calls it (no contacts available), there is no impact. + +## Performance Considerations + +- **200ms budget**: The contacts menu popup is rendered synchronously. The provider MUST complete within 200ms. APCu caching ensures repeat lookups are under 10ms. First-time lookups rely on `ObjectService::searchObjects()` which uses indexed queries. +- **Lazy service loading**: `ContactMatchingService` is only instantiated when `process()` is called, not on every page load. Nextcloud's DI container handles lazy instantiation. +- **Minimal data transfer**: The provider extracts only essential fields (email, name, org) from the contact entry and returns only action links. No large data payloads. +- **Cache warming**: No proactive cache warming. The cache is populated on first access per email address. +- **Parallel matching**: Email, name, and organization matching could be parallelized in the future, but the initial implementation runs them sequentially (email first, skip name/org matching if email yields high-confidence results). + +## Security Considerations + +- **RBAC**: The `ContactMatchingService` respects OpenRegister's authorization model. Only objects the current user has permission to view are returned as matches. +- **No data leakage**: If a contact matches an object the user cannot access, the match is excluded from results. +- **API authentication**: The `/api/contacts/match` endpoint requires Nextcloud session authentication. No public access. +- **Input validation**: Email addresses are validated for format before being used in queries. Name and organization strings are sanitized (max 255 chars, no SQL injection risk via ORM). diff --git a/openspec/archive/2026-03-25-contacts-actions/plan.json b/openspec/archive/2026-03-25-contacts-actions/plan.json new file mode 100644 index 0000000000..695f29e0de --- /dev/null +++ b/openspec/archive/2026-03-25-contacts-actions/plan.json @@ -0,0 +1,110 @@ +{ + "change": "contacts-actions", + "repo": "ConductionNL/openregister", + "parent_issue": 998, + "tracking_issue": 1020, + "tasks": [ + { + "id": 1, + "title": "Create ContactMatchingService with constructor and matchByEmail", + "spec_ref": "tasks.md#ContactMatchingService (Shared Service)", + "acceptance_criteria": [ + "GIVEN a ContactMatchingService instance WHEN constructed THEN it has ObjectService, SchemaMapper, RegisterMapper, ICacheFactory, LoggerInterface injected and APCu cache initialized", + "GIVEN an email address WHEN matchByEmail is called THEN it searches across all registers/schemas and returns matches with confidence 1.0", + "GIVEN a cached email WHEN matchByEmail is called THEN it returns cached results without DB query" + ], + "files_likely_affected": ["lib/Service/ContactMatchingService.php"], + "github_issue": 1023 + }, + { + "id": 2, + "title": "Add matchByName, matchByOrganization, matchContact, getRelatedObjectCounts, and cache invalidation", + "spec_ref": "tasks.md#ContactMatchingService (Shared Service)", + "acceptance_criteria": [ + "GIVEN a name WHEN matchByName is called THEN it returns matches with confidence 0.7 for full match or 0.4 for partial", + "GIVEN an organization WHEN matchByOrganization is called THEN it returns matches filtered to org-typed schemas with confidence 0.5", + "GIVEN email+name+org WHEN matchContact is called THEN results are deduplicated by UUID keeping highest confidence", + "GIVEN matches WHEN getRelatedObjectCounts is called THEN it returns counts grouped by schema title", + "GIVEN an email WHEN invalidateCache is called THEN the cache entry is deleted" + ], + "files_likely_affected": ["lib/Service/ContactMatchingService.php"], + "github_issue": 1024 + }, + { + "id": 3, + "title": "Create ContactsMenuProvider implementing IProvider", + "spec_ref": "tasks.md#ContactsMenuProvider", + "acceptance_criteria": [ + "GIVEN a contact entry WHEN process() is called THEN it extracts email/name/org and calls ContactMatchingService", + "GIVEN matches found WHEN no action registry THEN it adds default View in OpenRegister actions", + "GIVEN matches found WHEN action registry available THEN it resolves URL templates with contact placeholders", + "GIVEN matches WHEN count badge injected THEN it shows human-readable counts by schema type", + "GIVEN an exception in matching WHEN process() runs THEN it catches and logs at warning level" + ], + "files_likely_affected": ["lib/Contacts/ContactsMenuProvider.php"], + "github_issue": 1025 + }, + { + "id": 4, + "title": "Register provider and add cache invalidation hook", + "spec_ref": "tasks.md#Registration and Cache Invalidation", + "acceptance_criteria": [ + "GIVEN Application register WHEN called THEN ContactsMenuProvider is registered via registerContactsMenuProvider", + "GIVEN an object with email properties WHEN saved via ObjectService THEN ContactMatchingService cache is invalidated" + ], + "files_likely_affected": ["lib/AppInfo/Application.php", "lib/Service/ObjectService.php"], + "github_issue": 1026 + }, + { + "id": 5, + "title": "Create ContactsController API endpoint", + "spec_ref": "tasks.md#API Endpoint", + "acceptance_criteria": [ + "GIVEN valid email param WHEN GET /api/contacts/match called THEN returns matches with total and cached fields", + "GIVEN no email or name param WHEN GET /api/contacts/match called THEN returns 400", + "GIVEN matches WHEN response enriched THEN each match includes url and icon fields" + ], + "files_likely_affected": ["lib/Controller/ContactsController.php", "appinfo/routes.php"], + "github_issue": 1027 + }, + { + "id": 6, + "title": "Extend DeepLinkRegistryService with contact context placeholders", + "spec_ref": "tasks.md#DeepLinkRegistryService Extension", + "acceptance_criteria": [ + "GIVEN a contactContext array WHEN resolveUrl is called THEN contactId contactEmail contactName placeholders are replaced", + "GIVEN both object and contact placeholders WHEN resolveUrl is called THEN both are resolved" + ], + "files_likely_affected": ["lib/Service/DeepLinkRegistryService.php", "lib/Dto/DeepLinkRegistration.php"], + "github_issue": 1028 + }, + { + "id": 7, + "title": "Add translation strings (en + nl)", + "spec_ref": "tasks.md#Translations", + "acceptance_criteria": [ + "GIVEN en.json WHEN loaded THEN contains all contacts-actions translation keys", + "GIVEN nl.json WHEN loaded THEN contains Dutch translations for all contacts-actions keys" + ], + "files_likely_affected": ["l10n/en.json", "l10n/nl.json"], + "github_issue": 1029 + }, + { + "id": 8, + "title": "Write unit tests for ContactMatchingService, ContactsMenuProvider, and ContactsController", + "spec_ref": "tasks.md#Testing", + "acceptance_criteria": [ + "GIVEN ContactMatchingService tests WHEN run THEN matchByEmail matchByName matchByOrganization matchContact cache tests all pass", + "GIVEN ContactsMenuProvider tests WHEN run THEN process with matches no matches exception handling tests pass", + "GIVEN ContactsController tests WHEN run THEN match 200 missing params 400 tests pass", + "GIVEN URL template tests WHEN run THEN placeholder resolution tests pass" + ], + "files_likely_affected": [ + "tests/Unit/Service/ContactMatchingServiceTest.php", + "tests/Unit/Contacts/ContactsMenuProviderTest.php", + "tests/Unit/Controller/ContactsControllerTest.php" + ], + "github_issue": 1030 + } + ] +} diff --git a/openspec/archive/2026-03-25-contacts-actions/proposal.md b/openspec/archive/2026-03-25-contacts-actions/proposal.md new file mode 100644 index 0000000000..f2c67d4d15 --- /dev/null +++ b/openspec/archive/2026-03-25-contacts-actions/proposal.md @@ -0,0 +1,34 @@ +## Why + +Contact persons in Nextcloud (from the Contacts/CardDAV app) often correspond to entities in OpenRegister (persons, organizations). When users click on a contact name anywhere in Nextcloud -- the contacts menu popup, or the Contacts app -- there is no bridge to OpenRegister data. Users cannot see what cases, leads, or documents relate to a contact, nor take actions like "Create Case for Contact" or "View Lead History" without manually switching apps and searching. + +## What Changes + +- Implement `OCP\Contacts\ContactsMenu\IProvider` as `ContactsMenuProvider` that processes contact entries: extracts email and name, looks up matching OpenRegister entities, and adds actions to the entry +- Create `ContactMatchingService` for entity matching by email address (against EMAIL entities), display name (against PERSON entities), and organization field (against ORGANIZATION entities); shared logic with `mail-sidebar` change +- Add actions from the action registry with `context: "contact"` to each matched contact entry using `ILinkAction` (clickable links in the contacts menu) +- URL templates support placeholders: `{contactId}`, `{contactEmail}`, `{contactName}`, `{entityId}` +- Show entity/object count badge in the contacts menu popup (e.g., "3 cases, 1 lead, 5 documents") +- Investigate Nextcloud Contacts app sidebar tab support; if available, add Entities/Objects/Actions tabs reusing components from `files-sidebar-tabs` +- Add API endpoint: `GET /api/contacts/match?email={email}&name={name}` for entity matching (reusable by mail-sidebar) +- Cache entity lookups by email address in APCu (TTL 60s) for fast contact menu rendering (< 200ms) + +## Capabilities + +### New Capabilities +- `contacts-actions`: ContactsMenu provider integration with entity matching, action injection, and count badges for bridging Nextcloud Contacts with OpenRegister entities and consuming app actions +- `contact-entity-matching`: Shared service for matching contact metadata (email, name, organization) to OpenRegister entities with APCu caching + +### Modified Capabilities +- `deep-link-registry`: Needs URL template variable support for `{contactId}`, `{contactEmail}`, `{contactName}` + +## Impact + +- **New PHP classes**: `lib/Contacts/ContactsMenuProvider.php`, `lib/Service/ContactMatchingService.php` +- **Modified**: `lib/AppInfo/Application.php` (register contacts menu provider) +- **New routes**: 1 API endpoint in `appinfo/routes.php` +- **Shared logic**: `ContactMatchingService` entity matching is reused by `mail-sidebar` change +- **Caching**: APCu cache for email-to-entity lookups, TTL 60s +- **Dependencies**: Requires Nextcloud Contacts app installed; depends on `action-registry` change for action cards +- **Performance**: Contact menu popup must render in < 200ms; caching ensures this +- **No breaking changes**: Purely additive diff --git a/openspec/archive/2026-03-25-contacts-actions/specs/contacts-actions/spec.md b/openspec/archive/2026-03-25-contacts-actions/specs/contacts-actions/spec.md new file mode 100644 index 0000000000..0a67a1bb40 --- /dev/null +++ b/openspec/archive/2026-03-25-contacts-actions/specs/contacts-actions/spec.md @@ -0,0 +1,261 @@ +--- +status: draft +--- + +# Contacts Actions + +## Purpose + +Bridge Nextcloud's Contacts/CardDAV ecosystem with OpenRegister entity data by providing a ContactsMenu provider that matches contact persons to OpenRegister entities (persons, organizations) and injects contextual actions. When a user clicks on a contact name anywhere in Nextcloud (the contacts menu popup, the Contacts app, or any app that uses the contacts menu), the provider SHALL look up matching OpenRegister entities by email address, display name, and organization field, then add action links (e.g., "View Cases", "Create Lead") sourced from the action registry. A shared `ContactMatchingService` provides reusable entity matching with APCu caching, also consumed by the `mail-sidebar` change. + +**Source**: Case handlers, CRM users, and records managers need to see OpenRegister context (cases, leads, documents) when interacting with contacts in Nextcloud. Without this integration, users must manually switch to OpenRegister and search by email or name, breaking workflow continuity. + +## Requirements + +### Requirement: OpenRegister MUST register a ContactsMenu provider + +The app MUST implement `OCP\Contacts\ContactsMenu\IProvider` as `ContactsMenuProvider` and register it in `Application::register()` via `$context->registerContactsMenuProvider()`. The provider SHALL process contact entries, match them to OpenRegister entities, and add action links to the contacts menu popup. + +#### Scenario: Provider is registered and processes contact entries +- **GIVEN** the OpenRegister app is enabled +- **WHEN** a user clicks on a contact name in Nextcloud (e.g., in the top-bar contacts menu or in the Contacts app) +- **THEN** the `ContactsMenuProvider::process()` method SHALL be called with the `IEntry` object +- **AND** the provider SHALL extract the contact's email address(es), full name, and organization from the entry +- **AND** the provider SHALL call `ContactMatchingService::matchContact()` with the extracted metadata + +#### Scenario: Provider registration in Application +- **GIVEN** the `Application::register()` method in `lib/AppInfo/Application.php` +- **WHEN** the app boots +- **THEN** `$context->registerContactsMenuProvider(ContactsMenuProvider::class)` SHALL be called +- **AND** the provider SHALL be injectable via Nextcloud DI with constructor injection of `ContactMatchingService`, `DeepLinkRegistryService`, `IURLGenerator`, `IL10N`, and `LoggerInterface` + +### Requirement: ContactMatchingService MUST match contacts to OpenRegister entities + +A shared `ContactMatchingService` SHALL match contact metadata (email, name, organization) to OpenRegister objects across all registers and schemas. The service is the core matching engine used by both the contacts-actions provider and the mail-sidebar integration. + +#### Scenario: Match by email address +- **GIVEN** a contact with email address `jan.devries@gemeente.nl` +- **AND** an OpenRegister object in schema "Medewerkers" has a property `email` with value `jan.devries@gemeente.nl` +- **WHEN** `ContactMatchingService::matchByEmail('jan.devries@gemeente.nl')` is called +- **THEN** the service SHALL search across all registers and schemas for objects with email-type properties matching the given address (case-insensitive) +- **AND** it SHALL return an array of matched objects with their register, schema, and object metadata + +#### Scenario: Match by display name +- **GIVEN** a contact with display name `Jan de Vries` +- **AND** an OpenRegister object in schema "Personen" has properties `voornaam: Jan` and `achternaam: de Vries` +- **WHEN** `ContactMatchingService::matchByName('Jan de Vries')` is called +- **THEN** the service SHALL search for objects with name-type properties that fuzzy-match the given display name +- **AND** the matching SHALL be secondary to email matching (email is the primary key) + +#### Scenario: Match by organization +- **GIVEN** a contact with organization field `Gemeente Tilburg` +- **AND** an OpenRegister object in schema "Organisaties" has a property `naam` with value `Gemeente Tilburg` +- **WHEN** `ContactMatchingService::matchByOrganization('Gemeente Tilburg')` is called +- **THEN** the service SHALL search for organization-type objects matching the given organization name +- **AND** the results SHALL be returned alongside person matches, tagged with match type `organization` + +#### Scenario: Combined matching via matchContact +- **GIVEN** a contact entry with email `jan@example.nl`, name `Jan de Vries`, and organization `Gemeente Tilburg` +- **WHEN** `ContactMatchingService::matchContact(email: 'jan@example.nl', name: 'Jan de Vries', organization: 'Gemeente Tilburg')` is called +- **THEN** the service SHALL execute email matching first (highest confidence) +- **AND** then name matching (medium confidence) +- **AND** then organization matching (lowest confidence) +- **AND** results SHALL be deduplicated by object UUID +- **AND** each result SHALL include a `matchType` field (`email`, `name`, `organization`) and a `confidence` score + +#### Scenario: No matches found +- **GIVEN** a contact with email `unknown@nowhere.test` +- **WHEN** `ContactMatchingService::matchContact()` is called +- **THEN** it SHALL return an empty array +- **AND** the contacts menu SHALL display no OpenRegister actions for this contact + +### Requirement: APCu caching MUST be used for entity lookups + +The `ContactMatchingService` MUST cache entity lookup results in APCu to ensure the contacts menu popup renders within the 200ms performance budget. + +#### Scenario: Cache hit for repeated email lookup +- **GIVEN** a previous call to `matchByEmail('jan@example.nl')` returned 3 matches +- **AND** the cache TTL (60 seconds) has not expired +- **WHEN** `matchByEmail('jan@example.nl')` is called again +- **THEN** the service SHALL return the cached result without querying the database +- **AND** the response time SHALL be under 10ms + +#### Scenario: Cache miss triggers database query +- **GIVEN** no cached result exists for `info@bedrijf.nl` +- **WHEN** `matchByEmail('info@bedrijf.nl')` is called +- **THEN** the service SHALL query OpenRegister objects via `ObjectService::searchObjects()` +- **AND** the result SHALL be stored in APCu with key prefix `or_contact_match_` and TTL 60 seconds + +#### Scenario: Cache invalidation on object save +- **GIVEN** an OpenRegister object with email `jan@example.nl` is updated +- **WHEN** `ObjectService::saveObject()` completes +- **THEN** the service SHALL invalidate the APCu cache entry for `jan@example.nl` +- **AND** the next lookup SHALL fetch fresh data from the database + +### Requirement: Actions MUST be injected from the action registry + +The `ContactsMenuProvider` MUST query the action registry for actions with `context: "contact"` and add them as `ILinkAction` entries to the contact's menu popup. Each action SHALL resolve its URL template with contact-specific placeholders. + +#### Scenario: Action links appear in contacts menu +- **GIVEN** the action registry contains an action with `context: "contact"`, `label: "Bekijk zaken"`, and `url: "/apps/procest/#/zaken?contact={contactEmail}"` +- **AND** the contact's email is `jan@example.nl` +- **WHEN** the contacts menu is rendered for this contact +- **THEN** an `ILinkAction` SHALL be added with: + - `setName('Bekijk zaken')` + - `setHref('/apps/procest/#/zaken?contact=jan@example.nl')` + - `setIcon(...)` using the action's configured icon + - `setPriority(10)` + +#### Scenario: URL template placeholder resolution +- **GIVEN** an action URL template `"/apps/openregister/#/objects?email={contactEmail}&name={contactName}&entity={entityId}"` +- **AND** the contact has email `jan@example.nl`, name `Jan de Vries`, and a matched entity with UUID `550e8400-e29b-41d4-a716-446655440000` +- **WHEN** the URL template is resolved +- **THEN** the placeholders `{contactEmail}`, `{contactName}`, and `{entityId}` SHALL be replaced with URL-encoded values +- **AND** `{contactId}` SHALL resolve to the contact's UID from the vCard if available + +#### Scenario: No actions registered for contact context +- **GIVEN** no actions exist in the registry with `context: "contact"` +- **WHEN** the contacts menu is rendered +- **THEN** only the entity count badge SHALL be shown (if matches exist) +- **AND** a default "View in OpenRegister" action SHALL be added linking to the matched entity's detail page + +#### Scenario: Multiple matched entities produce multiple action sets +- **GIVEN** a contact matches 2 OpenRegister entities (one person, one organization) +- **AND** there are 2 actions registered for `context: "contact"` +- **WHEN** actions are injected +- **THEN** each action SHALL be resolved for each matched entity separately +- **AND** the action label SHALL include the entity context (e.g., "Bekijk zaken (Jan de Vries)" and "Bekijk zaken (Gemeente Tilburg)") + +### Requirement: Entity count badges MUST be shown in the contacts menu + +When a contact matches OpenRegister entities, the provider MUST add a summary action showing the count of related objects grouped by schema type. + +#### Scenario: Count badge for matched contact +- **GIVEN** a contact matches entities that are related to 3 cases, 1 lead, and 5 documents across different schemas +- **WHEN** the contacts menu popup is rendered +- **THEN** an `ILinkAction` SHALL be added with a summary label like `"3 zaken, 1 lead, 5 documenten"` +- **AND** the action SHALL link to an OpenRegister search filtered by the contact's email +- **AND** the action's priority SHALL be higher than individual action links (renders first) + +#### Scenario: No matches produce no badge +- **GIVEN** a contact has no matching OpenRegister entities +- **WHEN** the contacts menu popup is rendered +- **THEN** no count badge or OpenRegister actions SHALL be added +- **AND** the contacts menu SHALL render normally without OpenRegister interference + +### Requirement: A REST API endpoint MUST expose contact matching + +A new API endpoint SHALL provide programmatic access to the contact matching service, enabling reuse by the mail-sidebar change and external integrations. + +#### Scenario: Match by email via API +- **GIVEN** an authenticated user +- **WHEN** `GET /api/contacts/match?email=jan@example.nl` is called +- **THEN** the response SHALL return HTTP 200 with a JSON body containing: + - `matches`: array of matched entities with `uuid`, `register`, `schema`, `title`, `matchType`, `confidence` + - `total`: total number of matches + - `cached`: boolean indicating whether the result was served from cache + +#### Scenario: Match by name and email via API +- **GIVEN** an authenticated user +- **WHEN** `GET /api/contacts/match?email=jan@example.nl&name=Jan+de+Vries` is called +- **THEN** the response SHALL combine email and name matches, deduplicated by UUID +- **AND** email matches SHALL have higher confidence than name matches + +#### Scenario: Match by organization via API +- **GIVEN** an authenticated user +- **WHEN** `GET /api/contacts/match?organization=Gemeente+Tilburg` is called +- **THEN** the response SHALL return organization-type entity matches + +#### Scenario: Unauthenticated request returns 401 +- **GIVEN** no authentication credentials +- **WHEN** `GET /api/contacts/match?email=jan@example.nl` is called +- **THEN** the response SHALL be HTTP 401 Unauthorized + +### Requirement: The provider MUST integrate with DeepLinkRegistryService for action URLs + +When generating action URLs for matched entities, the provider MUST use `DeepLinkRegistryService::resolveUrl()` to determine the best URL for each entity, preferring consuming app deep links over raw OpenRegister URLs. + +#### Scenario: Deep link to consuming app +- **GIVEN** a matched entity in schema "Zaken" with a deep link registered by Procest +- **WHEN** the default "View in OpenRegister" action URL is generated +- **THEN** the URL SHALL point to the Procest route (e.g., `/apps/procest/#/zaken/{uuid}`) instead of the OpenRegister generic view +- **AND** the action icon SHALL use Procest's app icon via `DeepLinkRegistryService::resolveIcon()` + +#### Scenario: No deep link falls back to OpenRegister +- **GIVEN** a matched entity in a schema with no deep link registered +- **WHEN** the action URL is generated +- **THEN** the URL SHALL point to the OpenRegister object detail view +- **AND** the icon SHALL use `imagePath('openregister', 'app-dark.svg')` + +### Requirement: URL template variables MUST support contact-specific placeholders + +The deep link registry URL templates MUST be extended to support contact-specific placeholder variables beyond the existing object placeholders. + +#### Scenario: Contact placeholders in URL templates +- **GIVEN** a deep link URL template `"/apps/crm/#/contacts/{contactEmail}/cases"` +- **WHEN** resolved for a contact with email `jan@example.nl` +- **THEN** `{contactEmail}` SHALL be replaced with `jan%40example.nl` (URL-encoded) + +#### Scenario: All supported placeholders +- **GIVEN** a URL template with all contact placeholders +- **WHEN** resolved +- **THEN** the following placeholders SHALL be supported: + - `{contactId}` -- the contact's vCard UID + - `{contactEmail}` -- the contact's primary email address (URL-encoded) + - `{contactName}` -- the contact's display name (URL-encoded) + - `{entityId}` -- the matched OpenRegister entity's UUID + +### Requirement: i18n MUST be applied to all user-visible strings + +All user-visible strings in the `ContactsMenuProvider` and `ContactMatchingService` MUST use Nextcloud's `IL10N` translation system. Dutch and English translations MUST be provided as minimum per ADR-005. + +#### Scenario: Action labels are translated +- **GIVEN** a user with Nextcloud locale set to `nl` +- **WHEN** the contacts menu shows the entity count badge +- **THEN** the label SHALL use Dutch translations (e.g., "3 zaken, 1 lead, 5 documenten") + +#### Scenario: Default action label is translated +- **GIVEN** the default "View in OpenRegister" action +- **WHEN** rendered for a Dutch user +- **THEN** the label SHALL be "Bekijk in OpenRegister" + +#### Scenario: API error messages are translated +- **GIVEN** a failed contact matching API call +- **WHEN** the error response is generated +- **THEN** error messages SHALL use `IL10N::t()` for translation + +## Current Implementation Status + +**Not yet implemented.** The following existing infrastructure supports this feature: + +- `ObjectService::searchObjects()` provides the data access layer for searching objects by property values across registers and schemas. +- `DeepLinkRegistryService` provides `resolveUrl()` and `resolveIcon()` for consuming-app URL resolution. +- `Application::register()` already calls `$context->registerSearchProvider(ObjectsProvider::class)` -- the contacts menu provider registration will be added alongside it. +- Nextcloud's `OCP\Contacts\ContactsMenu\IProvider` interface is available since Nextcloud 12+. +- Nextcloud's `OCP\Contacts\ContactsMenu\ILinkAction` interface provides the mechanism for adding clickable action links. + +**Not yet implemented:** +- `ContactsMenuProvider` PHP class +- `ContactMatchingService` PHP class +- Contact matching API endpoint +- APCu caching for entity lookups +- Action registry integration (depends on `action-registry` change) +- URL template placeholder extension for contact variables +- Translation strings for provider labels and count badges + +## Standards & References + +- Nextcloud Contacts Menu API: `OCP\Contacts\ContactsMenu\IProvider` (NC 12+) +- Nextcloud Contacts Menu Actions: `OCP\Contacts\ContactsMenu\ILinkAction` (NC 12+) +- Nextcloud APCu Caching: `OCP\ICacheFactory` / `OCP\ICache` +- ADR-005: Dutch and English required for all UI strings +- ADR-011: Reuse existing services before creating new ones + +## Cross-References + +- `action-registry` -- Provides the action definitions with `context: "contact"` that are injected into the menu +- `mail-sidebar` -- Also consumes `ContactMatchingService` for email-based entity matching +- `deep-link-registry` -- URL resolution for consuming apps; extended with contact placeholders +- `profile-actions` -- User profile actions, separate from contact-person actions +- `files-sidebar-tabs` -- Sidebar tab pattern that could be reused if Contacts app supports tabs +- `nextcloud-entity-relations` -- Email linking table used for reverse lookups diff --git a/openspec/archive/2026-03-25-contacts-actions/tasks.md b/openspec/archive/2026-03-25-contacts-actions/tasks.md new file mode 100644 index 0000000000..38b10d5767 --- /dev/null +++ b/openspec/archive/2026-03-25-contacts-actions/tasks.md @@ -0,0 +1,58 @@ +# Tasks: Contacts Actions + +## ContactMatchingService (Shared Service) + +- [x] Create `lib/Service/ContactMatchingService.php` with constructor injection of `ObjectService`, `SchemaMapper`, `RegisterMapper`, `ICacheFactory`, `LoggerInterface`; initialize distributed cache via `$cacheFactory->createDistributed('openregister_contacts')` in constructor +- [x] Implement `matchByEmail(string $email): array` that searches across all registers and schemas for objects containing the given email address using `ObjectService::searchObjects()` with `{'_search': $email}`, post-filters results to confirm the email appears in email-like properties (property name containing "email", "e-mail", "mail"), and assigns confidence `1.0` +- [x] Implement APCu caching in `matchByEmail()`: check cache key `or_contact_match_email_{sha256(strtolower($email))}` before querying; store results with TTL 60 seconds; return cached results with `cached: true` flag +- [x] Implement `matchByName(?string $name): array` that splits the display name into parts, searches via `ObjectService::searchObjects()` with `{'_search': $name}`, post-filters to confirm name parts appear in name-like properties (naam, name, voornaam, achternaam, firstName, lastName), and assigns confidence `0.7` for full match or `0.4` for partial match; cache with key `or_contact_match_name_{sha256}` +- [x] Implement `matchByOrganization(?string $organization): array` that searches for organization-type objects via `ObjectService::searchObjects()`, post-filters on organization-like properties (organisatie, organization, bedrijf, company, naam) in organization-typed schemas, and assigns confidence `0.5`; cache with key `or_contact_match_org_{sha256}` +- [x] Implement `matchContact(string $email, ?string $name = null, ?string $organization = null): array` that calls `matchByEmail()` first, then `matchByName()` and `matchByOrganization()` if provided, deduplicates results by object UUID keeping the highest confidence match, and returns the combined sorted array +- [x] Implement `getRelatedObjectCounts(array $matches): array` that groups matched entities by schema title and returns an associative array of counts (e.g., `['Zaken' => 3, 'Leads' => 1, 'Documenten' => 5]`) +- [x] Implement `invalidateCache(string $email): void` that deletes the APCu cache entry for the given email address; also implement `invalidateCacheForObject(array $object): void` that extracts email-like property values from the object and invalidates each + +## ContactsMenuProvider + +- [x] Create `lib/Contacts/ContactsMenuProvider.php` implementing `OCP\Contacts\ContactsMenu\IProvider` with constructor injection of `ContactMatchingService`, `DeepLinkRegistryService`, `IURLGenerator`, `IL10N`, `LoggerInterface` +- [x] Implement `process(IEntry $entry): void` that extracts email addresses via `$entry->getEMailAddresses()`, full name via `$entry->getFullName()`, and organization via `$entry->getProperty('ORG')`; calls `ContactMatchingService::matchContact()` with the primary email and optional name/organization +- [x] When matches are found, query the action registry (if available via DI) for actions with `context: "contact"`; for each action and each matched entity, resolve the URL template by replacing `{contactId}`, `{contactEmail}`, `{contactName}`, `{entityId}` placeholders with URL-encoded values; create an `ILinkAction` via `$entry->addAction()` with the resolved URL, label (including entity title for disambiguation), icon, and priority `10` +- [x] When no action registry is available (graceful degradation), add a default `ILinkAction` per matched entity with label `$this->l10n->t('View in OpenRegister')`, href pointing to the deep-linked URL via `DeepLinkRegistryService::resolveUrl()` or fallback to OpenRegister's object detail route, and the app icon +- [x] Implement count badge injection: call `ContactMatchingService::getRelatedObjectCounts()`, format the counts as a human-readable string (e.g., "3 zaken, 1 lead, 5 documenten" using `IL10N::t()` with pluralization), create an `ILinkAction` with priority `0` (highest) linking to OpenRegister search filtered by the contact's email +- [x] Wrap the entire `process()` method body in a try-catch that logs exceptions at warning level and returns silently, ensuring the contacts menu never breaks due to OpenRegister errors + +## Registration and Cache Invalidation + +- [x] Register the provider in `Application::register()` via `$context->registerContactsMenuProvider(ContactsMenuProvider::class)` in the same method that calls `registerSearchProvider`, adding the necessary import statement for the new class +- [x] Add `ContactMatchingService` cache invalidation call in `ObjectService::saveObject()`: after successful persistence, check if the saved object has email-like property values, and if so call `ContactMatchingService::invalidateCacheForObject($objectArray)` to bust stale cache entries + +## API Endpoint + +- [x] Create `lib/Controller/ContactsController.php` extending `OCSController` with constructor injection of `ContactMatchingService`, `DeepLinkRegistryService`, `IRequest`, `IL10N`; implement `match()` method that reads `email`, `name`, `organization` query parameters, validates that at least `email` or `name` is provided (return 400 if neither), calls `ContactMatchingService::matchContact()`, and returns a `DataResponse` with `matches`, `total`, `cached` fields +- [x] Add route to `appinfo/routes.php`: `['name' => 'contacts#match', 'url' => '/api/contacts/match', 'verb' => 'GET']` positioned before any wildcard routes to avoid route conflicts +- [x] Enrich each match in the API response with `url` and `icon` fields by calling `DeepLinkRegistryService::resolveUrl()` and `resolveIcon()` for each matched entity + +## DeepLinkRegistryService Extension + +- [x] Extend `DeepLinkRegistryService::resolveUrl()` to accept an optional `array $contactContext = []` parameter; when provided, resolve additional placeholders `{contactId}`, `{contactEmail}`, `{contactName}` from the context array alongside existing object placeholders like `{uuid}` +- [x] Ensure placeholder replacement is applied after the existing object-level placeholder resolution, so both object and contact placeholders can coexist in the same URL template + +## Translations + +- [x] Add English translation strings to `l10n/en.json`: "View in OpenRegister", "No matching entities found", "Contact matching", "%n case" / "%n cases" (plural), "%n lead" / "%n leads", "%n document" / "%n documents", "Match by email", "Match by name", "Match by organization" +- [x] Add Dutch translation strings to `l10n/nl.json`: "Bekijk in OpenRegister", "Geen gekoppelde entiteiten gevonden", "Contact koppeling", "%n zaak" / "%n zaken", "%n lead" / "%n leads", "%n document" / "%n documenten", "Koppeling via e-mail", "Koppeling via naam", "Koppeling via organisatie" + +## Testing + +- [x] Write unit tests for `ContactMatchingService::matchByEmail()` covering: exact email match returns results with confidence `1.0`, case-insensitive matching, no match returns empty array, cached results are returned without DB query (mock `ICacheFactory`), cache invalidation clears the entry +- [x] Write unit tests for `ContactMatchingService::matchByName()` covering: full name match returns confidence `0.7`, partial name match returns `0.4`, no match returns empty array +- [x] Write unit tests for `ContactMatchingService::matchByOrganization()` covering: exact organization match, no match, results filtered to organization-typed schemas only +- [x] Write unit tests for `ContactMatchingService::matchContact()` covering: combined matching with deduplication (same object matched by email and name keeps email confidence), empty email with name-only matching, all three parameters provided +- [x] Write unit tests for `ContactsMenuProvider::process()` covering: matched contact gets actions and count badge added, unmatched contact gets no actions, exception in matching service is caught and logged, action registry unavailable falls back to default action +- [x] Write unit tests for `ContactsController::match()` covering: successful match returns 200 with correct JSON structure, missing parameters returns 400, authentication required returns 401 +- [x] Write unit tests for URL template placeholder resolution covering: `{contactEmail}` is URL-encoded, `{contactName}` is URL-encoded, `{entityId}` is replaced with UUID, `{contactId}` is replaced with vCard UID, missing placeholder values are left as-is +- [x] Manual test: verify clicking a contact name in Nextcloud's top-bar contacts menu shows OpenRegister actions when the contact's email matches an object +- [x] Manual test: verify the count badge shows correct counts grouped by schema type +- [x] Manual test: verify the API endpoint `GET /api/contacts/match?email=...` returns correct matches with cache hit/miss indicator +- [x] Manual test: verify performance -- contacts menu popup renders within 200ms when APCu cache is warm +- [x] Manual test: verify no actions appear for contacts with no matching OpenRegister entities +- [x] Manual test: verify the provider does not break the contacts menu when OpenRegister has no data or when the action registry is not yet implemented diff --git a/openspec/archive/2026-03-25-mail-sidebar/design.md b/openspec/archive/2026-03-25-mail-sidebar/design.md new file mode 100644 index 0000000000..4f3cff2b71 --- /dev/null +++ b/openspec/archive/2026-03-25-mail-sidebar/design.md @@ -0,0 +1,152 @@ +# Design: Mail Sidebar + +## Approach + +Inject an OpenRegister sidebar panel into the Nextcloud Mail app that displays linked objects for the currently viewed email. The implementation follows a three-layer architecture: + +1. **Backend**: New reverse-lookup API endpoints on `EmailsController` + a sender-based object discovery endpoint +2. **Script injection**: Register an additional script via `OCP\Util::addScript()` that loads when the Mail app is active +3. **Frontend**: A standalone Vue micro-app that renders a sidebar panel, communicates with OpenRegister API, and observes Mail app DOM/URL changes to detect which email is being viewed + +## Architecture Decisions + +### AD-1: Script Injection via OCP\Util::addScript vs. IFrame + +**Decision**: Use `OCP\Util::addScript()` to inject a JavaScript bundle into the Mail app page. + +**Why**: `OCP\Util::addScript()` is the supported Nextcloud mechanism for cross-app script loading. It loads synchronously with the page, has access to the same DOM and Nextcloud JS APIs (OC, OCA), and can use Nextcloud's axios instance for authenticated API calls. An IFrame would require separate authentication, CORS configuration, and would not integrate visually. + +**Trade-off**: The injected script depends on the Mail app's DOM structure, which may change between versions. We mitigate this by observing URL hash changes rather than DOM mutations where possible. + +### AD-2: Email Detection via URL Observation + +**Decision**: Detect the currently viewed email by observing the Mail app's URL hash/route changes rather than intercepting Mail app internal events. + +**Why**: The Mail app's Vue router encodes the current mailbox and message ID in the URL (e.g., `#/accounts/1/folders/INBOX/messages/42`). Observing URL changes is non-invasive, does not depend on Mail app internal APIs, and survives Mail app updates as long as the URL structure remains stable. The URL format has been stable since Nextcloud Mail 1.x. + +**Fallback**: If URL parsing fails, the sidebar shows a "Select an email to see linked objects" placeholder rather than erroring. + +### AD-3: Dual Query Strategy (Explicit Links + Sender Discovery) + +**Decision**: The sidebar performs two queries per email: (1) explicit links from `openregister_email_links` for the current message ID, and (2) a sender-based discovery query that finds objects linked to ANY email from the same sender. + +**Why**: Explicit links give precise results. Sender discovery provides context -- "this person has 3 other cases" -- which is valuable for case handlers who need to see the full picture. The two result sets are displayed in separate sections to avoid confusion. + +**Trade-off**: Two API calls per email view. Mitigated by debouncing (wait 300ms after URL change) and caching results per message ID for the session. + +### AD-4: Sidebar Position -- Right Panel Injection + +**Decision**: Inject the sidebar as a right-side panel that appears alongside (not replacing) the Mail app's existing message detail view. + +**Why**: The Mail app uses `NcAppContentDetails` for the message body on the right side. We inject a collapsible panel at the far right of the content area, similar to how Files app shows file details. This avoids conflicting with the Mail app's own layout. + +**Implementation**: The injected script creates a container div, appends it to the Mail app's content area, and mounts a Vue instance into it. CSS ensures proper width and responsive behavior. + +### AD-5: Graceful Degradation When Mail App Not Present + +**Decision**: The script injection is conditional -- only registered when the Mail app is installed and enabled. + +**Why**: OpenRegister must work without the Mail app. The `Application::register()` method checks `IAppManager::isEnabledForUser('mail')` before calling `Util::addScript()`. + +### AD-6: API Reuse -- Extend Existing EmailsController + +**Decision**: Add reverse-lookup endpoints to the existing `EmailsController` rather than creating a new controller. + +**Why**: The `EmailsController` already owns the `/api/emails/*` route namespace (from nextcloud-entity-relations). Adding `GET /api/emails/by-message/{accountId}/{messageId}` and `GET /api/emails/by-sender` follows RESTful conventions and avoids route duplication. + +## Files Affected + +### New Files (Backend) + +| File | Purpose | +|------|---------| +| `lib/Listener/MailAppScriptListener.php` | Listens for `BeforeTemplateRenderedEvent` from the Mail app and injects the sidebar script | + +### Modified Files (Backend) + +| File | Change | +|------|--------| +| `lib/Service/EmailService.php` | Add `findByMessageId()`, `findBySender()`, `findObjectsByMessageId()`, `findObjectsBySender()` methods | +| `lib/Controller/EmailsController.php` | Add `byMessage()` and `bySender()` endpoints | +| `appinfo/routes.php` | Add routes for reverse-lookup endpoints | +| `lib/AppInfo/Application.php` | Register `MailAppScriptListener` and conditional script injection | + +### New Files (Frontend) + +| File | Purpose | +|------|---------| +| `src/mail-sidebar.js` | Entry point for the Mail sidebar micro-app (webpack additional entry) | +| `src/mail-sidebar/MailSidebar.vue` | Root component for the sidebar panel | +| `src/mail-sidebar/components/LinkedObjectsList.vue` | Displays explicitly linked objects | +| `src/mail-sidebar/components/SuggestedObjectsList.vue` | Displays sender-based discovery results | +| `src/mail-sidebar/components/ObjectCard.vue` | Card component for a single object with metadata | +| `src/mail-sidebar/components/LinkObjectDialog.vue` | Modal dialog for searching and linking objects | +| `src/mail-sidebar/composables/useMailObserver.js` | Composable that observes Mail app URL changes and extracts account/message IDs | +| `src/mail-sidebar/composables/useEmailLinks.js` | Composable for API calls to email link endpoints | +| `src/mail-sidebar/api/emailLinks.js` | Axios API wrapper for email link endpoints | +| `css/mail-sidebar.css` | Styles for the sidebar panel (NL Design System compatible) | + +### Modified Files (Frontend) + +| File | Change | +|------|--------| +| `webpack.config.js` | Add `mail-sidebar` as additional entry point | + +## API Routes (to add to routes.php) + +```php +// Reverse-lookup: find objects linked to a specific email message +['name' => 'emails#byMessage', 'url' => '/api/emails/by-message/{accountId}/{messageId}', 'verb' => 'GET', 'requirements' => ['accountId' => '\d+', 'messageId' => '\d+']], + +// Discovery: find objects linked to emails from a specific sender +['name' => 'emails#bySender', 'url' => '/api/emails/by-sender', 'verb' => 'GET'], + +// Quick link: link current email to an object (used from sidebar) +['name' => 'emails#quickLink', 'url' => '/api/emails/quick-link', 'verb' => 'POST'], +``` + +## Sequence Diagram + +``` +User opens email in Mail app + | + v +MailSidebar.vue (injected script) + | + +--> useMailObserver detects URL change + | extracts accountId=1, messageId=42 + | + +--> GET /api/emails/by-message/1/42 + | Returns: [{objectUuid, register, schema, title, ...}] + | --> Renders LinkedObjectsList + | + +--> GET /api/emails/by-sender?sender=burger@test.local + | Returns: [{objectUuid, register, schema, title, linkedEmailCount, ...}] + | --> Renders SuggestedObjectsList (filtered to exclude already-linked) + | +User clicks "Link to Object" + | + +--> LinkObjectDialog opens + | User searches for object by title/UUID + | GET /api/objects/search?q=vergunning+123 + | + +--> User selects object, confirms + | POST /api/emails/quick-link + | {accountId: 1, messageId: 42, objectUuid: "abc-123", register: 1, schema: 2} + | + +--> Sidebar refreshes, shows new link in LinkedObjectsList +``` + +## CSS/Styling Strategy + +The sidebar panel uses Nextcloud's standard CSS variables (`--color-primary`, `--color-background-dark`, etc.) and NL Design System tokens where available. The panel width is 320px on desktop, collapses to a toggleable overlay on narrow viewports (<1024px). The toggle button is a small tab anchored to the right edge of the content area. + +## Dependency on nextcloud-entity-relations + +This change REQUIRES the nextcloud-entity-relations spec to be implemented first, specifically: +- `openregister_email_links` database table +- `EmailService` with link/unlink/list methods +- `EmailLinkMapper` for database queries +- `EmailsController` with base CRUD endpoints + +This change EXTENDS that foundation with reverse-lookup capabilities and the Mail app UI integration. diff --git a/openspec/archive/2026-03-25-mail-sidebar/plan.json b/openspec/archive/2026-03-25-mail-sidebar/plan.json new file mode 100644 index 0000000000..c093f8f5df --- /dev/null +++ b/openspec/archive/2026-03-25-mail-sidebar/plan.json @@ -0,0 +1,167 @@ +{ + "change": "mail-sidebar", + "repo": "ConductionNL/openregister", + "tracking_issue": 1006, + "parent_issue": 1001, + "tasks": [ + { + "id": 1, + "title": "EmailLink entity and EmailLinkMapper", + "github_issue": 1007, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#reverse-lookup-api", + "acceptance_criteria": [ + "GIVEN openregister_email_links table exists WHEN an EmailLink entity is created THEN it persists with all required fields", + "GIVEN EmailLinkMapper WHEN findByAccountAndMessage is called THEN it returns matching email links" + ], + "files_likely_affected": [ + "lib/Db/EmailLink.php", + "lib/Db/EmailLinkMapper.php", + "lib/Migration/Version1Date20260325120000.php" + ], + "status": "todo" + }, + { + "id": 2, + "title": "EmailService with reverse-lookup methods", + "github_issue": 1008, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#reverse-lookup-api", + "acceptance_criteria": [ + "GIVEN email links exist WHEN findByMessageId is called THEN objects are returned with resolved metadata", + "GIVEN emails from a sender WHEN findObjectsBySender is called THEN distinct objects with email counts are returned", + "GIVEN valid params WHEN quickLink is called THEN a new email link is created" + ], + "files_likely_affected": [ + "lib/Service/EmailService.php" + ], + "status": "todo" + }, + { + "id": 3, + "title": "EmailsController endpoints", + "github_issue": 1009, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#reverse-lookup-api", + "acceptance_criteria": [ + "GIVEN a valid accountId/messageId WHEN GET /api/emails/by-message/{accountId}/{messageId} THEN linked objects are returned", + "GIVEN a valid sender WHEN GET /api/emails/by-sender?sender=x THEN discovered objects are returned", + "GIVEN valid body WHEN POST /api/emails/quick-link THEN link is created and 201 returned" + ], + "files_likely_affected": [ + "lib/Controller/EmailsController.php" + ], + "status": "todo" + }, + { + "id": 4, + "title": "Routes and input validation", + "github_issue": 1010, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#reverse-lookup-api", + "acceptance_criteria": [ + "GIVEN routes.php WHEN email routes are added THEN by-message, by-sender, quick-link are accessible", + "GIVEN invalid input WHEN endpoints are called THEN 400 errors with messages are returned" + ], + "files_likely_affected": [ + "appinfo/routes.php" + ], + "status": "todo" + }, + { + "id": 5, + "title": "MailAppScriptListener and Application registration", + "github_issue": 1011, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#mail-app-script-injection", + "acceptance_criteria": [ + "GIVEN Mail app is enabled WHEN BeforeTemplateRenderedEvent fires THEN sidebar script is injected", + "GIVEN Mail app is not installed WHEN any page loads THEN no script is registered", + "GIVEN user without OpenRegister access WHEN Mail app opens THEN script is not injected" + ], + "files_likely_affected": [ + "lib/Listener/MailAppScriptListener.php", + "lib/AppInfo/Application.php" + ], + "status": "todo" + }, + { + "id": 6, + "title": "Webpack mail-sidebar entry point", + "github_issue": 1012, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#webpack-entry-point", + "acceptance_criteria": [ + "GIVEN webpack config has mail-sidebar entry WHEN npm run build runs THEN openregister-mail-sidebar.js is produced", + "GIVEN the bundle THEN it uses externalized Vue and @nextcloud/axios" + ], + "files_likely_affected": [ + "webpack.config.js", + "src/mail-sidebar.js" + ], + "status": "todo" + }, + { + "id": 7, + "title": "Vue sidebar components", + "github_issue": 1013, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#sidebar-panel-ui", + "acceptance_criteria": [ + "GIVEN sidebar loads WHEN linked objects exist THEN LinkedObjectsList shows object cards", + "GIVEN sidebar loads WHEN sender has other cases THEN SuggestedObjectsList shows discovery results", + "GIVEN user clicks Link to Object WHEN dialog opens THEN search and link flow works" + ], + "files_likely_affected": [ + "src/mail-sidebar/MailSidebar.vue", + "src/mail-sidebar/components/LinkedObjectsList.vue", + "src/mail-sidebar/components/SuggestedObjectsList.vue", + "src/mail-sidebar/components/ObjectCard.vue", + "src/mail-sidebar/components/LinkObjectDialog.vue" + ], + "status": "todo" + }, + { + "id": 8, + "title": "Composables and API layer", + "github_issue": 1014, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#email-url-observation", + "acceptance_criteria": [ + "GIVEN Mail app URL changes WHEN observer detects it THEN accountId/messageId are extracted", + "GIVEN 300ms debounce WHEN rapid navigation occurs THEN only last change triggers refresh", + "GIVEN API calls WHEN responses arrive THEN results are cached per messageId" + ], + "files_likely_affected": [ + "src/mail-sidebar/composables/useMailObserver.js", + "src/mail-sidebar/composables/useEmailLinks.js", + "src/mail-sidebar/api/emailLinks.js" + ], + "status": "todo" + }, + { + "id": 9, + "title": "CSS, i18n, accessibility", + "github_issue": 1015, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md#i18n-support", + "acceptance_criteria": [ + "GIVEN sidebar renders THEN NL Design System CSS variables are used", + "GIVEN Dutch user WHEN sidebar loads THEN all text is in Dutch", + "GIVEN keyboard navigation WHEN Tab is pressed THEN all interactive elements are reachable" + ], + "files_likely_affected": [ + "css/mail-sidebar.css" + ], + "status": "todo" + }, + { + "id": 10, + "title": "Unit tests", + "github_issue": 1016, + "spec_ref": "openspec/changes/mail-sidebar/specs/mail-sidebar/spec.md", + "acceptance_criteria": [ + "GIVEN EmailService WHEN tested THEN 3+ unit tests pass", + "GIVEN EmailsController WHEN tested THEN 3+ unit tests pass", + "GIVEN MailAppScriptListener WHEN tested THEN 2+ unit tests pass" + ], + "files_likely_affected": [ + "tests/Unit/Service/EmailServiceTest.php", + "tests/Unit/Controller/EmailsControllerTest.php", + "tests/Unit/Listener/MailAppScriptListenerTest.php" + ], + "status": "todo" + } + ] +} diff --git a/openspec/archive/2026-03-25-mail-sidebar/proposal.md b/openspec/archive/2026-03-25-mail-sidebar/proposal.md new file mode 100644 index 0000000000..409a0093a1 --- /dev/null +++ b/openspec/archive/2026-03-25-mail-sidebar/proposal.md @@ -0,0 +1,44 @@ +# Mail Sidebar + +## Problem + +When a Nextcloud user views an email in the Mail app, there is no way to see which OpenRegister objects are related to that email. Case handlers working with Procest, ZaakAfhandelApp, or Pipelinq must manually search for cases by copying sender addresses or subject lines from emails into the OpenRegister search. This context-switching breaks workflow continuity and wastes time. + +The nextcloud-entity-relations spec establishes the `openregister_email_links` table that maps emails to objects, and the `EmailService` that manages those links. However, this linkage is only visible from the OpenRegister side (object detail -> emails tab). There is no reverse integration: when viewing an email in the Mail app, users cannot see or manage the objects linked to that email. + +## Context + +- **Existing infrastructure**: `openregister_email_links` table, `EmailService`, `EmailsController` (from nextcloud-entity-relations spec) +- **Nextcloud Mail integration point**: The Mail app does not provide a formal sidebar extension API. Integration requires injecting a sidebar panel via Nextcloud's collaboration resources system or registering a custom script that extends the Mail app UI +- **Alternative approach**: Nextcloud 28+ supports apps registering "additional scripts" that load into other apps' pages via `OCP\Util::addScript()` +- **Consuming apps**: Procest (case workflows), Pipelinq (pipeline management), ZaakAfhandelApp (ZGW case handling) +- **Related specs**: nextcloud-entity-relations (email linking), object-interactions (notes/tasks/files), deep-link-registry (deep links to objects) + +## Proposed Solution + +Build a Mail sidebar integration that shows OpenRegister objects related to the currently viewed email. The integration consists of: + +1. **Backend API** -- A reverse-lookup endpoint that finds objects by mail message ID, mail account ID, or sender email address. This leverages the existing `openregister_email_links` table. +2. **Mail app script injection** -- Use `OCP\Util::addScript()` to inject a JavaScript bundle into the Mail app that renders a sidebar panel showing linked objects. +3. **Sidebar panel UI** -- A Vue component that displays linked objects with key metadata (title, schema, register, status), allows quick linking/unlinking, and provides a "search and link" flow for associating new objects with the email. +4. **Auto-suggestion** -- When viewing an email, automatically query for objects that match the sender's email address, even if not explicitly linked, providing discovery of potentially relevant cases. + +## Scope + +### In scope +- Reverse-lookup API endpoint (find objects by mail message/sender) +- Mail app script injection via `OCP\Util::addScript()` +- Sidebar panel Vue component for the Mail app +- Display of linked objects with metadata +- Quick link/unlink actions from the sidebar +- Search-and-link flow (search objects, link to current email) +- Auto-suggestion of objects matching sender email address +- Deep links from sidebar to object detail in OpenRegister +- i18n support (Dutch and English) + +### Out of scope +- Sending emails from OpenRegister (n8n's responsibility) +- Modifying the email itself +- Integration with other mail clients (Thunderbird, Outlook) +- Creating new objects from the sidebar (navigate to OpenRegister for that) +- Nextcloud Talk/Spreed sidebar integration (separate future change) diff --git a/openspec/archive/2026-03-25-mail-sidebar/specs/mail-sidebar/spec.md b/openspec/archive/2026-03-25-mail-sidebar/specs/mail-sidebar/spec.md new file mode 100644 index 0000000000..c3fbc0bdb7 --- /dev/null +++ b/openspec/archive/2026-03-25-mail-sidebar/specs/mail-sidebar/spec.md @@ -0,0 +1,442 @@ +--- +status: proposed +--- + +# Mail Sidebar + +## Purpose + +Provide a sidebar panel inside the Nextcloud Mail app that displays OpenRegister objects related to the currently viewed email. This enables case handlers to see at a glance which cases, applications, or records are associated with an email -- and to create new associations -- without leaving the Mail app. The integration builds on the `openregister_email_links` table and `EmailService` established by the nextcloud-entity-relations spec. + +**Standards**: Nextcloud App Framework (script injection via `OCP\Util::addScript()`), REST API conventions (JSON responses, standard HTTP status codes), WCAG AA accessibility +**Cross-references**: [nextcloud-entity-relations](../../../specs/nextcloud-entity-relations/spec.md), [object-interactions](../../../specs/object-interactions/spec.md), [deep-link-registry](../../../specs/deep-link-registry/spec.md) + +--- + +## Requirements + +### Requirement: Reverse-lookup API to find objects by mail message ID + +The system SHALL provide a REST endpoint that accepts a Nextcloud Mail account ID and message ID, queries the `openregister_email_links` table, and returns all OpenRegister objects linked to that specific email message. For each linked object, the response MUST include the object's UUID, register ID, schema ID, title (derived from the object's data using the schema's title property), and the link metadata (who linked it and when). + +#### Rationale + +The existing `EmailsController` provides forward lookups (object -> emails). The sidebar needs the reverse: email -> objects. This endpoint is the primary data source for the sidebar's "Linked Objects" section. + +#### Scenario: Find objects linked to a specific email +- **GIVEN** email with account ID 1 and message ID 42 is linked to objects `abc-123` and `def-456` in the `openregister_email_links` table +- **WHEN** a GET request is sent to `/api/emails/by-message/1/42` +- **THEN** the response MUST return HTTP 200 with JSON: + ```json + { + "results": [ + { + "linkId": 1, + "objectUuid": "abc-123", + "registerId": 1, + "registerTitle": "Vergunningen", + "schemaId": 3, + "schemaTitle": "Omgevingsvergunning", + "objectTitle": "OV-2026-0042", + "linkedBy": "behandelaar-1", + "linkedAt": "2026-03-20T14:30:00+00:00" + }, + { + "linkId": 2, + "objectUuid": "def-456", + "registerId": 1, + "registerTitle": "Vergunningen", + "schemaId": 3, + "schemaTitle": "Omgevingsvergunning", + "objectTitle": "OV-2026-0043", + "linkedBy": "admin", + "linkedAt": "2026-03-21T09:15:00+00:00" + } + ], + "total": 2 + } + ``` +- **AND** each result MUST include `registerTitle` and `schemaTitle` resolved from the Register and Schema entities + +#### Scenario: No objects linked to this email +- **GIVEN** email with account ID 1 and message ID 99 has no entries in `openregister_email_links` +- **WHEN** a GET request is sent to `/api/emails/by-message/1/99` +- **THEN** the response MUST return HTTP 200 with `{"results": [], "total": 0}` + +#### Scenario: Invalid account ID or message ID +- **GIVEN** a GET request with non-numeric account or message ID +- **WHEN** the request is processed +- **THEN** the response MUST return HTTP 400 with `{"error": "Invalid account ID or message ID"}` + +--- + +### Requirement: Sender-based object discovery API + +The system SHALL provide a REST endpoint that accepts a sender email address and returns all OpenRegister objects that have ANY linked email from that sender. This enables the sidebar's "Other cases from this sender" discovery section. The results MUST be distinct by object UUID (no duplicates if multiple emails from the same sender are linked to the same object) and MUST include a count of how many emails from that sender are linked to each object. + +#### Rationale + +Case handlers need context beyond the current email. Knowing that the sender has 3 other open cases helps prioritize and cross-reference. This query leverages the `sender` column in `openregister_email_links`. + +#### Scenario: Discover objects by sender email +- **GIVEN** sender `burger@test.local` has emails linked to objects `abc-123` (2 emails), `ghi-789` (1 email) +- **WHEN** a GET request is sent to `/api/emails/by-sender?sender=burger@test.local` +- **THEN** the response MUST return HTTP 200 with: + ```json + { + "results": [ + { + "objectUuid": "abc-123", + "registerId": 1, + "registerTitle": "Vergunningen", + "schemaId": 3, + "schemaTitle": "Omgevingsvergunning", + "objectTitle": "OV-2026-0042", + "linkedEmailCount": 2 + }, + { + "objectUuid": "ghi-789", + "registerId": 2, + "registerTitle": "Meldingen", + "schemaId": 5, + "schemaTitle": "Melding", + "objectTitle": "ML-2026-0015", + "linkedEmailCount": 1 + } + ], + "total": 2 + } + ``` +- **AND** results MUST be ordered by `linkedEmailCount` descending (most-linked first) + +#### Scenario: No objects found for sender +- **GIVEN** sender `unknown@example.com` has no linked emails in any object +- **WHEN** a GET request is sent to `/api/emails/by-sender?sender=unknown@example.com` +- **THEN** the response MUST return HTTP 200 with `{"results": [], "total": 0}` + +#### Scenario: Missing sender parameter +- **GIVEN** a GET request to `/api/emails/by-sender` without the `sender` query parameter +- **WHEN** the request is processed +- **THEN** the response MUST return HTTP 400 with `{"error": "The sender parameter is required"}` + +#### Scenario: Sender discovery excludes current email's linked objects +- **GIVEN** the sidebar makes both a by-message and by-sender call +- **WHEN** the frontend renders the results +- **THEN** objects already shown in the "Linked Objects" section (from by-message) MUST be excluded from the "Other cases from this sender" section +- **AND** this filtering happens client-side to keep the API stateless + +--- + +### Requirement: Quick-link endpoint for sidebar use + +The system SHALL provide a POST endpoint that creates an email-object link with minimal input, designed for use from the Mail sidebar where the mail context (account ID, message ID, subject, sender, date) is already known. The endpoint MUST accept all required fields in one call and return the created link with resolved object metadata. + +#### Rationale + +The existing `POST /api/objects/{register}/{schema}/{id}/emails` endpoint requires knowing the register, schema, and object ID upfront and navigates from the object side. The sidebar needs to link from the email side -- the user sees the email and picks an object to link. The quick-link endpoint inverts the flow. + +#### Scenario: Quick-link an email to an object from the sidebar +- **GIVEN** an authenticated user viewing email (accountId: 1, messageId: 42, subject: "Aanvraag vergunning", sender: "burger@test.local", date: "2026-03-20T10:00:00Z") +- **WHEN** a POST request is sent to `/api/emails/quick-link` with body: + ```json + { + "mailAccountId": 1, + "mailMessageId": 42, + "mailMessageUid": "1234", + "subject": "Aanvraag vergunning", + "sender": "burger@test.local", + "date": "2026-03-20T10:00:00Z", + "objectUuid": "abc-123", + "registerId": 1 + } + ``` +- **THEN** a record MUST be created in `openregister_email_links` +- **AND** the `linkedBy` field MUST be set to the current authenticated user +- **AND** the response MUST return HTTP 201 with the created link including resolved `objectTitle`, `registerTitle`, `schemaTitle` + +#### Scenario: Quick-link with non-existent object +- **GIVEN** a POST with `objectUuid: "nonexistent-uuid"` +- **WHEN** the system validates the object +- **THEN** the response MUST return HTTP 404 with `{"error": "Object not found"}` + +#### Scenario: Quick-link duplicate prevention +- **GIVEN** email (accountId: 1, messageId: 42) is already linked to object `abc-123` +- **WHEN** a POST request tries to create the same link +- **THEN** the response MUST return HTTP 409 with `{"error": "Email already linked to this object"}` + +--- + +### Requirement: Mail app script injection via event listener + +The system SHALL register a PHP event listener that injects the OpenRegister mail sidebar JavaScript bundle into the Nextcloud Mail app page. The injection MUST only occur when: (1) the Mail app is installed and enabled for the current user, (2) the user has access to at least one OpenRegister register, and (3) the current page is the Mail app. The script MUST be loaded as a separate webpack entry point to avoid bloating the main OpenRegister bundle. + +#### Rationale + +Nextcloud's `OCP\Util::addScript()` is the standard mechanism for cross-app script injection. By listening to the Mail app's template rendering event, we ensure the script is only loaded when relevant. + +#### Scenario: Script is injected when Mail app is active +- **GIVEN** a user with OpenRegister access opens the Nextcloud Mail app +- **WHEN** the Mail app's `BeforeTemplateRenderedEvent` fires +- **THEN** `OCP\Util::addScript('openregister', 'openregister-mail-sidebar')` MUST be called +- **AND** the script MUST create a container element and mount the Vue sidebar component +- **AND** the script MUST NOT interfere with the Mail app's existing functionality + +#### Scenario: Script is NOT injected when Mail app is not installed +- **GIVEN** the Nextcloud Mail app is not installed +- **WHEN** the user navigates to any page +- **THEN** no mail sidebar script MUST be registered or loaded +- **AND** no errors MUST appear in the server log related to the mail sidebar + +#### Scenario: Script is NOT injected for users without OpenRegister access +- **GIVEN** a user who has no access to any OpenRegister registers +- **WHEN** the user opens the Mail app +- **THEN** the mail sidebar script MUST NOT be injected +- **AND** no OpenRegister UI elements MUST appear in the Mail app + +--- + +### Requirement: Sidebar panel UI with linked objects display + +The system SHALL render a collapsible sidebar panel on the right side of the Mail app's message detail view. The panel MUST display two sections: (1) "Linked Objects" showing objects explicitly linked to the current email, and (2) "Related Cases" showing objects discovered via sender email address. Each object MUST be displayed as a card with the object title, schema name, register name, and a deep link to the object in OpenRegister. + +#### Rationale + +Case handlers need quick, scannable access to case context while reading emails. A sidebar panel is the least disruptive UI pattern -- it does not obscure the email content and can be collapsed when not needed. + +#### Scenario: Sidebar shows linked objects for current email +- **GIVEN** the user is viewing email (accountId: 1, messageId: 42) which is linked to 2 objects +- **WHEN** the sidebar loads +- **THEN** the "Linked Objects" section MUST display 2 object cards +- **AND** each card MUST show: object title, schema name (e.g., "Omgevingsvergunning"), register name (e.g., "Vergunningen") +- **AND** each card MUST have a clickable link that navigates to `/apps/openregister/registers/{registerId}/{schemaId}/{objectUuid}` in a new tab + +#### Scenario: Sidebar shows related cases from same sender +- **GIVEN** the current email is from `burger@test.local` who has emails linked to 3 objects (1 of which is already linked to the current email) +- **WHEN** the sidebar loads +- **THEN** the "Related Cases" section MUST display 2 object cards (excluding the one already shown in "Linked Objects") +- **AND** each card MUST show: object title, schema name, register name, and a badge showing "N emails" (how many emails from this sender are linked) + +#### Scenario: Sidebar is collapsible +- **GIVEN** the sidebar panel is visible +- **WHEN** the user clicks the collapse toggle button +- **THEN** the panel MUST animate to a narrow tab (40px wide) showing only the OpenRegister icon +- **AND** clicking the tab MUST re-expand the panel +- **AND** the collapsed/expanded state MUST persist in `localStorage` across page reloads + +#### Scenario: Sidebar shows empty state when no links exist +- **GIVEN** the current email has no linked objects and the sender has no linked emails anywhere +- **WHEN** the sidebar loads +- **THEN** the "Linked Objects" section MUST show: "No objects linked to this email" +- **AND** the "Related Cases" section MUST show: "No related cases found for this sender" +- **AND** a prominent "Link to Object" button MUST be visible + +#### Scenario: Sidebar handles email navigation +- **GIVEN** the sidebar is showing objects for email (messageId: 42) +- **WHEN** the user clicks on a different email (messageId: 43) in the Mail app +- **THEN** the sidebar MUST detect the URL change within 300ms +- **AND** the sidebar MUST show a loading state while fetching new data +- **AND** the sidebar MUST display objects linked to the new email (messageId: 43) +- **AND** the previous results MUST be cached so returning to email 42 is instant + +--- + +### Requirement: Link and unlink actions from the sidebar + +The system SHALL provide UI actions in the sidebar to link and unlink objects from the current email. Linking opens a search dialog where the user can find objects by title, UUID, or schema. Unlinking removes the association after confirmation. + +#### Rationale + +The sidebar is the natural place to manage email-object associations. Without link/unlink actions, users would need to navigate to OpenRegister to manage links, defeating the purpose of the sidebar integration. + +#### Scenario: Link an object to the current email via search +- **GIVEN** the user clicks "Link to Object" in the sidebar +- **WHEN** the link dialog opens +- **THEN** the dialog MUST show a search input with placeholder "Search by title or UUID..." +- **AND** as the user types, results MUST appear after 300ms debounce +- **AND** each result MUST show: object title, schema name, register name +- **AND** objects already linked to this email MUST be marked with a "Already linked" badge and be non-selectable + +#### Scenario: Confirm linking an object +- **GIVEN** the user has selected object "OV-2026-0042" in the link dialog +- **WHEN** the user clicks "Link" +- **THEN** a POST request MUST be sent to `/api/emails/quick-link` with the current email's metadata and the selected object's UUID +- **AND** on success, the dialog MUST close and the linked object MUST appear in the "Linked Objects" section +- **AND** a Nextcloud toast notification MUST show "Object linked successfully" / "Object succesvol gekoppeld" + +#### Scenario: Unlink an object from the current email +- **GIVEN** object "OV-2026-0042" is linked to the current email (linkId: 7) +- **WHEN** the user clicks the unlink (X) button on the object card +- **THEN** a confirmation dialog MUST appear: "Remove link between this email and OV-2026-0042?" / "Koppeling tussen deze e-mail en OV-2026-0042 verwijderen?" +- **AND** on confirmation, a DELETE request MUST be sent to `/api/objects/{register}/{schema}/{objectUuid}/emails/7` +- **AND** the object card MUST be removed from the "Linked Objects" section +- **AND** if the object has other emails from the same sender linked, it MUST appear in the "Related Cases" section + +#### Scenario: Link dialog search returns no results +- **GIVEN** the user types "nonexistent-case-99" in the search input +- **WHEN** the debounced search completes +- **THEN** the dialog MUST show "No objects found" / "Geen objecten gevonden" +- **AND** a hint MUST appear: "Try searching by UUID or with different keywords" / "Probeer te zoeken op UUID of met andere zoektermen" + +--- + +### Requirement: Email URL observation for automatic context switching + +The system SHALL implement a URL observer that monitors the Nextcloud Mail app's route changes to detect when the user switches between emails. The observer MUST extract the mail account ID and message ID from the URL hash and trigger sidebar data refresh. The observer MUST handle all Mail app URL patterns including inbox, sent, drafts, and custom folders. + +#### Rationale + +The Mail app is a single-page application with client-side routing. The sidebar cannot rely on page reloads to detect navigation -- it must observe route changes programmatically. URL observation is more reliable and less invasive than DOM mutation observation or intercepting the Mail app's internal event bus. + +#### Scenario: Detect email selection from inbox URL +- **GIVEN** the Mail app URL changes to `#/accounts/1/folders/INBOX/messages/42` +- **WHEN** the URL observer processes the change +- **THEN** it MUST extract `accountId: 1` and `messageId: 42` +- **AND** trigger a sidebar data refresh for that account/message combination +- **AND** the refresh MUST be debounced (300ms) to avoid rapid-fire requests during quick navigation + +#### Scenario: Detect email selection from custom folder +- **GIVEN** the Mail app URL changes to `#/accounts/2/folders/Archief/messages/108` +- **WHEN** the URL observer processes the change +- **THEN** it MUST extract `accountId: 2` and `messageId: 108` +- **AND** trigger a sidebar data refresh + +#### Scenario: Handle URL without message selection (folder view) +- **GIVEN** the Mail app URL changes to `#/accounts/1/folders/INBOX` (no message selected) +- **WHEN** the URL observer processes the change +- **THEN** the sidebar MUST clear the current results +- **AND** show a placeholder: "Select an email to see linked objects" / "Selecteer een e-mail om gekoppelde objecten te zien" + +#### Scenario: Handle compose/settings URLs +- **GIVEN** the Mail app URL changes to `#/compose` or `#/settings` +- **WHEN** the URL observer processes the change +- **THEN** the sidebar MUST collapse or hide (no email context available) +- **AND** no API calls MUST be made + +#### Scenario: Cache results for previously viewed emails +- **GIVEN** the user viewed email (messageId: 42) and then navigated to email (messageId: 43) +- **WHEN** the user navigates back to email (messageId: 42) +- **THEN** the sidebar MUST immediately display the cached results for messageId 42 +- **AND** a background refresh MUST be triggered to check for updates +- **AND** if the background refresh returns different data, the UI MUST update seamlessly + +--- + +### Requirement: Webpack entry point for mail sidebar bundle + +The system SHALL build the mail sidebar as a separate webpack entry point (`mail-sidebar`) that produces an independent JavaScript bundle. This bundle MUST NOT import or depend on the main OpenRegister application bundle. It MUST only include the Vue components, composables, and API utilities needed for the sidebar panel. + +#### Rationale + +Loading the entire OpenRegister frontend bundle (with all views, stores, and dependencies) into the Mail app would be wasteful and could cause conflicts. A separate entry point ensures minimal bundle size and isolation. + +#### Scenario: Separate webpack entry point +- **GIVEN** the webpack configuration has a `mail-sidebar` entry point at `src/mail-sidebar.js` +- **WHEN** `npm run build` is executed +- **THEN** a separate bundle `js/openregister-mail-sidebar.js` MUST be produced +- **AND** the bundle size MUST be less than 100KB gzipped (excluding Vue runtime shared with Nextcloud) +- **AND** the bundle MUST NOT include any OpenRegister store modules, router configuration, or view components from the main app + +#### Scenario: Bundle uses Nextcloud's shared Vue instance +- **GIVEN** the Mail app page already has Vue loaded via Nextcloud's runtime +- **WHEN** the mail sidebar bundle loads +- **THEN** it MUST use the externalized Vue (from webpack externals) rather than bundling its own +- **AND** it MUST use Nextcloud's shared axios instance for API calls (`@nextcloud/axios`) + +--- + +### Requirement: i18n support for Dutch and English + +The system SHALL provide all user-facing strings in the sidebar in both Dutch (nl) and English (en), using Nextcloud's standard translation mechanism (`@nextcloud/l10n`). The sidebar MUST follow the user's Nextcloud language preference. + +#### Rationale + +All Conduction apps require Dutch and English as minimum languages (per i18n requirement in project.md). Government users in the Netherlands primarily use Dutch. + +#### Key translatable strings + +| English | Dutch | +|---------|-------| +| Linked Objects | Gekoppelde objecten | +| Related Cases | Gerelateerde zaken | +| No objects linked to this email | Geen objecten gekoppeld aan deze e-mail | +| No related cases found for this sender | Geen gerelateerde zaken gevonden voor deze afzender | +| Link to Object | Koppelen aan object | +| Search by title or UUID... | Zoeken op titel of UUID... | +| Already linked | Al gekoppeld | +| Link | Koppelen | +| Cancel | Annuleren | +| Object linked successfully | Object succesvol gekoppeld | +| Remove link? | Koppeling verwijderen? | +| Remove link between this email and {title}? | Koppeling tussen deze e-mail en {title} verwijderen? | +| Remove | Verwijderen | +| Select an email to see linked objects | Selecteer een e-mail om gekoppelde objecten te zien | +| N emails | N e-mails | +| Open in OpenRegister | Openen in OpenRegister | + +#### Scenario: Sidebar renders in Dutch for Dutch user +- **GIVEN** a user whose Nextcloud language is set to `nl` +- **WHEN** the sidebar loads +- **THEN** all labels, buttons, placeholders, and messages MUST be displayed in Dutch +- **AND** the `t('openregister', ...)` function MUST be used for all translatable strings + +#### Scenario: Sidebar renders in English for English user +- **GIVEN** a user whose Nextcloud language is set to `en` +- **WHEN** the sidebar loads +- **THEN** all labels, buttons, placeholders, and messages MUST be displayed in English + +--- + +### Requirement: Accessibility compliance (WCAG AA) + +The sidebar panel MUST meet WCAG AA accessibility standards. All interactive elements MUST be keyboard-navigable, have visible focus indicators, and include appropriate ARIA labels. Color contrast MUST meet 4.5:1 for normal text and 3:1 for large text. + +#### Scenario: Keyboard navigation through sidebar +- **GIVEN** the sidebar is visible and has linked objects +- **WHEN** the user presses Tab +- **THEN** focus MUST move through: collapse toggle -> first object card link -> first object unlink button -> second object card link -> ... -> "Link to Object" button +- **AND** each focused element MUST have a visible focus ring (using `--color-primary` outline) + +#### Scenario: Screen reader announces sidebar content +- **GIVEN** a screen reader user navigates to the sidebar +- **WHEN** the sidebar region is reached +- **THEN** it MUST be announced as "OpenRegister: Linked Objects sidebar" (via `role="complementary"` and `aria-label`) +- **AND** each object card MUST announce: "{title}, {schema} in {register}. Linked by {user} on {date}" +- **AND** the unlink button MUST announce: "Remove link to {title}" + +#### Scenario: Color contrast in light and dark themes +- **GIVEN** the sidebar uses Nextcloud CSS variables for colors +- **WHEN** rendered in light theme or dark theme +- **THEN** all text MUST have at least 4.5:1 contrast ratio against its background +- **AND** the sidebar MUST NOT use hardcoded colors (CSS variables only, per NL Design System requirements) + +--- + +### Requirement: Error handling and resilience + +The sidebar MUST handle API errors, network failures, and unexpected states gracefully without breaking the Mail app experience. Errors MUST be displayed inline in the sidebar, not as modal dialogs or browser alerts. + +#### Scenario: API returns 500 error +- **GIVEN** the reverse-lookup API returns HTTP 500 +- **WHEN** the sidebar processes the response +- **THEN** the sidebar MUST display: "Could not load linked objects. Try again later." / "Gekoppelde objecten konden niet worden geladen. Probeer het later opnieuw." +- **AND** a "Retry" button MUST be shown +- **AND** the error MUST be logged to the browser console with the response details + +#### Scenario: Network timeout +- **GIVEN** the API call takes longer than 10 seconds +- **WHEN** the timeout is reached +- **THEN** the sidebar MUST abort the request and show a timeout message +- **AND** a "Retry" button MUST be shown + +#### Scenario: Mail app DOM structure changes (version mismatch) +- **GIVEN** the Mail app updates and the expected container element is not found +- **WHEN** the sidebar script attempts to mount +- **THEN** the script MUST log a warning: "Mail sidebar: could not find mount point, skipping injection" +- **AND** the script MUST NOT throw unhandled exceptions +- **AND** the Mail app MUST continue to function normally + +#### Scenario: OpenRegister API is unreachable +- **GIVEN** the OpenRegister app is disabled or uninstalled while the Mail app is open +- **WHEN** the sidebar attempts an API call +- **THEN** the sidebar MUST catch the error and hide itself +- **AND** no error dialogs or broken UI elements MUST remain in the Mail app diff --git a/openspec/archive/2026-03-25-mail-sidebar/tasks.md b/openspec/archive/2026-03-25-mail-sidebar/tasks.md new file mode 100644 index 0000000000..2b175d5813 --- /dev/null +++ b/openspec/archive/2026-03-25-mail-sidebar/tasks.md @@ -0,0 +1,94 @@ +# Tasks: Mail Sidebar + +## Backend API + +- [x] Add `findByMessageId(int $accountId, int $messageId)` method to EmailService that queries openregister_email_links and resolves object/register/schema metadata +- [x] Add `findObjectsBySender(string $sender)` method to EmailService with GROUP BY object_uuid and COUNT for linkedEmailCount +- [x] Add `quickLink(array $params)` method to EmailService that creates an email link from the sidebar's email-side perspective +- [x] Add `byMessage(int $accountId, int $messageId)` endpoint to EmailsController returning linked objects with register/schema titles +- [x] Add `bySender(string $sender)` endpoint to EmailsController returning discovered objects with email counts +- [x] Add `quickLink()` POST endpoint to EmailsController for sidebar-initiated linking +- [x] Add reverse-lookup and quick-link routes to appinfo/routes.php +- [x] Add input validation for accountId, messageId (numeric), and sender (email format) parameters + +## Script Injection + +- [x] Create MailAppScriptListener.php that listens for BeforeTemplateRenderedEvent from the Mail app +- [x] Implement conditional injection: check Mail app enabled AND user has OpenRegister access +- [x] Register MailAppScriptListener in Application.php with IEventDispatcher +- [x] Add openregister-mail-sidebar script registration via OCP\Util::addScript() + +## Webpack Build + +- [x] Add mail-sidebar entry point to webpack.config.js pointing to src/mail-sidebar.js +- [x] Create src/mail-sidebar.js entry point that mounts the sidebar Vue component +- [x] Configure webpack externals to use Nextcloud's shared Vue and axios +- [x] Verify separate bundle output (js/openregister-mail-sidebar.js) does not include main app code + +## Frontend - Core Components + +- [x] Create src/mail-sidebar/MailSidebar.vue root component with collapsible panel layout +- [x] Create src/mail-sidebar/components/LinkedObjectsList.vue for explicitly linked objects +- [x] Create src/mail-sidebar/components/SuggestedObjectsList.vue for sender-based discovery results +- [x] Create src/mail-sidebar/components/ObjectCard.vue with title, schema, register, deep link, and unlink button +- [x] Create src/mail-sidebar/components/LinkObjectDialog.vue modal with search input and results list + +## Frontend - Composables and API + +- [x] Create src/mail-sidebar/composables/useMailObserver.js to observe Mail app URL changes and extract accountId/messageId +- [x] Implement URL parsing for all Mail app route patterns (inbox, sent, drafts, custom folders, compose, settings) +- [x] Implement 300ms debounce on URL change detection +- [x] Implement per-messageId result caching with background refresh +- [x] Create src/mail-sidebar/composables/useEmailLinks.js for API state management (loading, error, results) +- [x] Create src/mail-sidebar/api/emailLinks.js with axios wrappers for by-message, by-sender, and quick-link endpoints + +## Frontend - UX + +- [x] Implement collapse/expand toggle with animation and localStorage persistence +- [x] Implement client-side filtering to exclude already-linked objects from the suggested list +- [x] Implement link confirmation flow: search dialog -> select object -> POST quick-link -> refresh sidebar +- [x] Implement unlink confirmation dialog with bilingual text +- [x] Implement toast notifications for link/unlink success and error states +- [x] Implement empty state displays for both sections (linked and suggested) +- [x] Implement loading spinners during API calls +- [x] Implement error states with retry buttons for API failures and timeouts + +## Styling + +- [x] Create css/mail-sidebar.css with NL Design System compatible styles using Nextcloud CSS variables +- [x] Implement responsive layout: 320px panel on desktop, overlay on <1024px viewports +- [x] Ensure dark theme compatibility (no hardcoded colors) +- [x] Verify WCAG AA contrast ratios for all text elements + +## Accessibility + +- [x] Add role="complementary" and aria-label to sidebar container +- [x] Add aria-labels to all interactive elements (toggle, cards, buttons) +- [x] Implement keyboard navigation (Tab order through all interactive elements) +- [x] Add visible focus indicators using --color-primary outline +- [x] Test with screen reader (object card announcements, button labels) + +## Internationalization + +- [x] Add all translatable strings to l10n source files (en and nl) +- [x] Use t('openregister', ...) for all user-facing text in Vue components +- [x] Verify Dutch translations render correctly in sidebar + +## Error Handling and Resilience + +- [x] Handle API 500 errors with inline error message and retry button +- [x] Implement 10-second request timeout with abort controller +- [x] Handle missing mount point gracefully (log warning, skip injection, no exceptions) +- [x] Handle OpenRegister app disabled/uninstalled (catch errors, hide sidebar) +- [x] Ensure Mail app continues functioning normally when sidebar encounters any error + +## Testing + +- [x] Unit tests for EmailService reverse-lookup methods (findByMessageId, findObjectsBySender) +- [x] Unit tests for EmailsController new endpoints (byMessage, bySender, quickLink) +- [x] Unit tests for MailAppScriptListener conditional injection logic +- [x] Unit tests for URL parser (all Mail app route patterns) +- [x] Unit tests for result caching and deduplication logic +- [x] Integration test: link email from sidebar, verify appears in object's email tab +- [x] Integration test: unlink email from sidebar, verify removed from object's email tab +- [x] Integration test: Mail app functions normally with sidebar script injected diff --git a/openspec/archive/retrofit-annotate-openregister-2026-04-23/proposal.md b/openspec/archive/retrofit-annotate-openregister-2026-04-23/proposal.md new file mode 100644 index 0000000000..8969aeea93 --- /dev/null +++ b/openspec/archive/retrofit-annotate-openregister-2026-04-23/proposal.md @@ -0,0 +1,31 @@ +# Retrofit — annotate openregister against existing specs + +Retroactive annotation of 395 methods across 98 files against 92 REQs in 21 capabilities. No code logic changes. No spec deltas (all REQs already exist in `openspec/specs/`). + +Source: `openspec/coverage-report.md` generated 2026-04-23 (Bucket 1 high-confidence entries only; 254 NEEDS-REVIEW entries deferred for human review). + +See [retrofit playbook](../../../.github/docs/claude/retrofit.md). + +## Capabilities covered + +- `archival-destruction-workflow` +- `audit-trail-immutable` +- `data-import-export` +- `deep-link-registry` +- `edepot-transfer` +- `event-driven-architecture` +- `faceting-configuration` +- `graphql-api` +- `linked-entity-types` +- `mail-sidebar` +- `mcp-discovery` +- `mock-registers` +- `rbac-scopes` +- `retention-management` +- `schema-hooks` +- `tenant-lifecycle` +- `tenant-quotas` +- `webhook-payload-mapping` +- `workflow-engine-abstraction` +- `workflow-in-import` +- `zoeken-filteren` diff --git a/openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md b/openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md new file mode 100644 index 0000000000..5191e4650c --- /dev/null +++ b/openspec/archive/retrofit-annotate-openregister-2026-04-23/tasks.md @@ -0,0 +1,94 @@ +# Tasks + +- [x] task-1: archival-destruction-workflow#REQ-001 — The system MUST provide a DestructionCheckJob that generates destruction lists from eligible objects (retroactive annotation) +- [x] task-2: archival-destruction-workflow#REQ-002 — The system MUST provide API endpoints for destruction list management (retroactive annotation) +- [x] task-3: archival-destruction-workflow#REQ-003 — Destruction MUST follow a multi-step approval workflow with full, partial, and rejection paths (retroactive annotation) +- [x] task-4: archival-destruction-workflow#REQ-005 — The system MUST generate destruction certificates upon completed destruction (retroactive annotation) +- [x] task-5: archival-destruction-workflow#REQ-006 — The system MUST support legal holds that prevent destruction (retroactive annotation) +- [x] task-6: archival-destruction-workflow#REQ-007 — The system MUST calculate archiefactiedatum using configurable afleidingswijzen (retroactive annotation) +- [x] task-7: audit-trail-immutable#REQ-002 — The AuditTrail entity MUST include hash and previousHash fields (retroactive annotation) +- [x] task-8: audit-trail-immutable#REQ-003 — The audit trail MUST use cryptographic hash chaining (retroactive annotation) +- [x] task-9: data-import-export#REQ-001 — The system MUST support import from CSV, Excel, JSON, and XML formats (retroactive annotation) +- [x] task-10: data-import-export#REQ-003 — Import MUST validate data against schema definitions before insertion (retroactive annotation) +- [x] task-11: data-import-export#REQ-007 — The system MUST support structured export to CSV, Excel (XLSX), JSON, XML, and ODS formats (retroactive annotation) +- [x] task-12: data-import-export#REQ-009 — Export MUST resolve relations to human-readable names (retroactive annotation) +- [x] task-13: data-import-export#REQ-011 — Import MUST support field mapping and value transformation (retroactive annotation) +- [x] task-14: data-import-export#REQ-013 — Import templates MUST be downloadable per schema (retroactive annotation) +- [x] task-15: data-import-export#REQ-014 — Import and export MUST respect RBAC permissions (retroactive annotation) +- [x] task-16: data-import-export#REQ-015 — The system MUST support i18n for export headers and templates (retroactive annotation) +- [x] task-17: data-import-export#REQ-016 — Configuration import/export MUST support full register portability (retroactive annotation) +- [x] task-18: deep-link-registry#REQ-002 — Deep link registry SHALL resolve URLs for unified search results (retroactive annotation) +- [x] task-19: deep-link-registry#REQ-009 — Notification deep links SHALL use the deep link registry (retroactive annotation) +- [x] task-20: edepot-transfer#REQ-001 — The system MUST generate MDTO-compliant XML metadata per object (retroactive annotation) +- [x] task-21: edepot-transfer#REQ-002 — The system MUST assemble SIP packages for e-Depot transfer (retroactive annotation) +- [x] task-22: edepot-transfer#REQ-003 — The system MUST support transfer list management (retroactive annotation) +- [x] task-23: edepot-transfer#REQ-004 — The system MUST support configurable e-Depot endpoint settings (retroactive annotation) +- [x] task-24: edepot-transfer#REQ-008 — The system MUST log all transfer actions in the audit trail (retroactive annotation) +- [x] task-25: event-driven-architecture#REQ-004 — WebhookEventListener MUST extract structured payloads from all event types (retroactive annotation) +- [x] task-26: event-driven-architecture#REQ-010 — Event listener isolation MUST prevent cascading failures (retroactive annotation) +- [x] task-27: event-driven-architecture#REQ-015 — Event payloads for webhook delivery MUST include register and schema context for object events (retroactive annotation) +- [x] task-28: faceting-configuration#REQ-002 — Facet type auto-detection from property definitions (retroactive annotation) +- [x] task-29: faceting-configuration#REQ-003 — Non-aggregated facet isolation (retroactive annotation) +- [x] task-30: faceting-configuration#REQ-004 — Schema ID in non-aggregated facet response (retroactive annotation) +- [x] task-31: faceting-configuration#REQ-007 — Metadata facets via @self namespace (retroactive annotation) +- [x] task-32: faceting-configuration#REQ-010 — Facet discovery via _facetable parameter (retroactive annotation) +- [x] task-33: faceting-configuration#REQ-011 — Facet request configuration via _facets parameter (retroactive annotation) +- [x] task-34: faceting-configuration#REQ-012 — Facet response standardized format (retroactive annotation) +- [x] task-35: faceting-configuration#REQ-015 — Frontend _schema parameter for non-aggregated facets (retroactive annotation) +- [x] task-36: faceting-configuration#REQ-016 — Facet performance optimization via HyperFacetHandler (retroactive annotation) +- [x] task-37: graphql-api#REQ-007 — GraphQL MUST enforce schema-level RBAC via PermissionHandler (retroactive annotation) +- [x] task-38: graphql-api#REQ-009 — GraphQL MUST log operations to the audit trail (retroactive annotation) +- [x] task-39: graphql-api#REQ-010 — Query complexity analysis MUST prevent resource abuse (retroactive annotation) +- [x] task-40: graphql-api#REQ-013 — Cross-register schema stitching MUST provide a unified graph (retroactive annotation) +- [x] task-41: graphql-api#REQ-018 — GraphQL resolver MUST reset state between requests (retroactive annotation) +- [x] task-42: linked-entity-types#REQ-003 — Metadata Columns on Magic Tables (retroactive annotation) +- [x] task-43: linked-entity-types#REQ-004 — Metadata Columns on Entity Tables (retroactive annotation) +- [x] task-44: linked-entity-types#REQ-008 — Reverse Lookup Across Tables (retroactive annotation) +- [x] task-45: linked-entity-types#REQ-013 — Remove Link Entities and Mappers (retroactive annotation) +- [x] task-46: mail-sidebar#REQ-001 — Reverse-lookup API to find objects by mail message ID (retroactive annotation) +- [x] task-47: mail-sidebar#REQ-003 — Quick-link endpoint for sidebar use (retroactive annotation) +- [x] task-48: mail-sidebar#REQ-004 — Mail app script injection via event listener (retroactive annotation) +- [x] task-49: mail-sidebar#REQ-005 — Sidebar panel UI with linked objects display (retroactive annotation) +- [x] task-50: mail-sidebar#REQ-006 — Link and unlink actions from the sidebar (retroactive annotation) +- [x] task-51: mail-sidebar#REQ-008 — Webpack entry point for mail sidebar bundle (retroactive annotation) +- [x] task-52: mcp-discovery#REQ-001 — Tier 1 Discovery Catalog (retroactive annotation) +- [x] task-53: mcp-discovery#REQ-002 — Tier 2 Capability Detail with Live Data (retroactive annotation) +- [x] task-54: mock-registers#REQ-001 — BRP Mock Register (Basisregistratie Personen) (retroactive annotation) +- [x] task-55: rbac-scopes#REQ-001 — Scope Model Hierarchy (Register > Schema > Object > Property) (retroactive annotation) +- [x] task-56: rbac-scopes#REQ-002 — Register-level authorization cascade (retroactive annotation) +- [x] task-57: rbac-scopes#REQ-003 — Named role definitions on registers (retroactive annotation) +- [x] task-58: rbac-scopes#REQ-005 — Register authorization cache (retroactive annotation) +- [x] task-59: rbac-scopes#REQ-012 — OAS Scope Generation from RBAC Configuration (retroactive annotation) +- [x] task-60: retention-management#REQ-001 — Objects MUST carry MDTO-compliant archival metadata in the retention field (retroactive annotation) +- [x] task-61: retention-management#REQ-003 — The system MUST calculate archiefactiedatum using configurable afleidingswijzen (retroactive annotation) +- [x] task-62: retention-management#REQ-004 — The system MUST generate destruction lists via a background job (retroactive annotation) +- [x] task-63: retention-management#REQ-008 — The system MUST send pre-destruction notifications (retroactive annotation) +- [x] task-64: retention-management#REQ-009 — Retention settings MUST be configurable via API (retroactive annotation) +- [x] task-65: schema-hooks#REQ-001 — Hook Configuration on Schema (retroactive annotation) +- [x] task-66: schema-hooks#REQ-002 — Hook Lifecycle Events (retroactive annotation) +- [x] task-67: schema-hooks#REQ-004 — Sync Hook Response Format (retroactive annotation) +- [x] task-68: schema-hooks#REQ-005 — Hook Execution Order (retroactive annotation) +- [x] task-69: schema-hooks#REQ-006 — Failure Mode Behavior (retroactive annotation) +- [x] task-70: schema-hooks#REQ-007 — Filter Condition for Conditional Hook Execution (retroactive annotation) +- [x] task-71: schema-hooks#REQ-010 — Async Hook Execution (Fire-and-Forget) (retroactive annotation) +- [x] task-72: schema-hooks#REQ-011 — Hook Retry via Background Job (retroactive annotation) +- [x] task-73: tenant-lifecycle#REQ-001 — Organisation entities MUST have a lifecycle status field with defined state transitions (retroactive annotation) +- [x] task-74: tenant-lifecycle#REQ-002 — Tenant provisioning MUST create default resources automatically (retroactive annotation) +- [x] task-75: tenant-quotas#REQ-001 — Request quota MUST be enforced via middleware before controller execution (retroactive annotation) +- [x] task-76: webhook-payload-mapping#REQ-002 — Webhook entity MUST support an optional mapping reference for payload transformation (retroactive annotation) +- [x] task-77: webhook-payload-mapping#REQ-004 — Event payload input MUST include full context for mapping templates (retroactive annotation) +- [x] task-78: webhook-payload-mapping#REQ-011 — Webhook testing MUST support dry-run delivery (retroactive annotation) +- [x] task-79: webhook-payload-mapping#REQ-012 — Webhook delivery MUST support async processing via background jobs (retroactive annotation) +- [x] task-80: webhook-payload-mapping#REQ-015 — Request interception MUST support pre-event webhooks (retroactive annotation) +- [x] task-81: workflow-engine-abstraction#REQ-001 — Engine Interface Definition (retroactive annotation) +- [x] task-82: workflow-engine-abstraction#REQ-003 — Windmill Adapter Implementation (retroactive annotation) +- [x] task-83: workflow-engine-abstraction#REQ-005 — Workflow Execution API (Sync and Async) (retroactive annotation) +- [x] task-84: workflow-engine-abstraction#REQ-008 — Engine Configuration Entity (retroactive annotation) +- [x] task-85: workflow-engine-abstraction#REQ-010 — Engine Health Monitoring (retroactive annotation) +- [x] task-86: workflow-in-import#REQ-006 — Schema Hook Wiring During Import (retroactive annotation) +- [x] task-87: zoeken-filteren#REQ-001 — Full-text search across object properties (retroactive annotation) +- [x] task-88: zoeken-filteren#REQ-009 — Backend-agnostic search architecture (retroactive annotation) +- [x] task-89: zoeken-filteren#REQ-011 — Saved searches and search trails (retroactive annotation) +- [x] task-90: zoeken-filteren#REQ-013 — Search across registers (global search) (retroactive annotation) +- [x] task-91: zoeken-filteren#REQ-014 — View-based search composition (retroactive annotation) +- [x] task-92: zoeken-filteren#REQ-016 — Dutch language search support (i18n) (retroactive annotation) diff --git a/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/proposal.md b/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/proposal.md new file mode 100644 index 0000000000..6d1ff1b5c2 --- /dev/null +++ b/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/proposal.md @@ -0,0 +1,25 @@ +# Retrofit — archival-destruction-workflow + +Describes observed behavior of 2 methods under `archival-destruction-workflow` as 1 new REQ. Code already exists — this change retroactively specifies it. + +## Affected code units +- lib/BackgroundJob/DestructionCheckJob.php::run (pre-notification step) +- lib/BackgroundJob/DestructionCheckJob.php::sendObjectNotification + +## Additional methods annotated with existing REQs +- lib/Controller/ArchivalController.php::createLegalHold (REQ-006) +- lib/Controller/ArchivalController.php::releaseLegalHold (REQ-006) +- lib/Service/Archival/LegalHoldService.php::placeHold (REQ-006) +- lib/Service/Archival/LegalHoldService.php::releaseHold (REQ-006) +- lib/Service/Archival/LegalHoldService.php::hasActiveHold (REQ-006) +- lib/Service/Archival/LegalHoldService.php::hasActiveHoldFromRetention (REQ-006) +- lib/Service/Archival/LegalHoldService.php::bulkPlaceHold (REQ-006) +- lib/Service/ArchivalService.php::calculateArchivalDate (REQ-007) +- lib/Service/ArchivalService.php::findObjectsDueForDestruction (REQ-001) + +## Approach +- Observed: DestructionCheckJob::run sends per-object notifications to archivaris group for objects within a configurable lead window (default 30 days) before archiefactiedatum. Deduplication via app config `retention_notified_objects`. Legal hold exclusion. Subject varies by archiefnominatie. +- This behavior is DISTINCT from the list-creation notification in REQ-001 (which fires when a destruction list is created). Pre-destruction notifications fire N days before the deadline. +- REQ-009 added to spec; remaining 18 Bucket 2a methods annotated with existing REQ refs. + +Source: openspec/coverage-report.md generated 2026-04-23. See retrofit playbook. diff --git a/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/specs/archival-destruction-workflow/spec.md b/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/specs/archival-destruction-workflow/spec.md new file mode 100644 index 0000000000..9ae5e30147 --- /dev/null +++ b/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/specs/archival-destruction-workflow/spec.md @@ -0,0 +1,36 @@ +--- +retrofit_extensions: ["The system MUST send configurable advance notifications to archivists for objects approaching their archiefactiedatum"] +--- + +### Requirement: The system MUST send configurable advance notifications to archivists for objects approaching their archiefactiedatum + +The `DestructionCheckJob` MUST scan for objects whose `archiefactiedatum` falls within a configurable advance window (default: 30 days) and send per-object Nextcloud notifications to all users in the `archivaris` group. Notifications MUST be deduplicated — each object is notified at most once per retention lifecycle — using a persisted list stored in app config. Objects with active legal holds MUST be excluded. Objects with `archiefnominatie: bewaren` receive an e-Depot transfer subject rather than a destruction warning. + +#### Scenario: Objects within lead window trigger advance notifications +- **GIVEN** the `notificationLeadDays` setting is 30 (default) +- **AND** 5 objects have `archiefactiedatum` between today and today+30 days +- **AND** `archiefstatus` is `nog_te_archiveren` for all 5 +- **AND** none have been notified before (not in the `retention_notified_objects` app config list) +- **AND** none have an active legal hold +- **WHEN** the `DestructionCheckJob` runs +- **THEN** 5 Nextcloud notifications MUST be sent, one per object, to each user in the `archivaris` group +- **AND** each notification MUST include: object title, `archiefactiedatum`, selectielijst category, and a descriptive subject +- **AND** all 5 object UUIDs MUST be appended to `retention_notified_objects` in app config + +#### Scenario: Already-notified objects are not re-notified +- **GIVEN** object `zaak-111` is in the `retention_notified_objects` list +- **AND** `zaak-111` still has `archiefactiedatum` within the lead window +- **WHEN** the `DestructionCheckJob` runs +- **THEN** no notification MUST be sent for `zaak-111` +- **AND** `zaak-111` MUST NOT be added to the notified list again + +#### Scenario: Objects with active legal hold excluded from advance notifications +- **GIVEN** object `zaak-222` has `archiefactiedatum` within the lead window +- **AND** `zaak-222` has `retention.legalHold.active` set to `true` +- **WHEN** the `DestructionCheckJob` runs +- **THEN** no advance notification MUST be sent for `zaak-222` + +#### Scenario: Objects with archiefnominatie bewaren receive e-Depot transfer subject +- **GIVEN** object `zaak-333` has `archiefnominatie: bewaren` and `archiefactiedatum` within the lead window +- **WHEN** the `DestructionCheckJob` runs +- **THEN** the notification subject MUST be "Object requires e-Depot transfer" rather than "Object approaching destruction date" diff --git a/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/tasks.md b/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/tasks.md new file mode 100644 index 0000000000..eb914a3613 --- /dev/null +++ b/openspec/archive/retrofit-archival-destruction-workflow-2026-04-24/tasks.md @@ -0,0 +1,3 @@ +# Tasks + +- [x] task-1: archival-destruction-workflow#REQ-009 — The system MUST send configurable advance notifications to archivists for objects approaching their archiefactiedatum (retroactive annotation) diff --git a/openspec/archive/retrofit-b2b-crossrefs-2026-04-28/proposal.md b/openspec/archive/retrofit-b2b-crossrefs-2026-04-28/proposal.md new file mode 100644 index 0000000000..f0e908ac26 --- /dev/null +++ b/openspec/archive/retrofit-b2b-crossrefs-2026-04-28/proposal.md @@ -0,0 +1,58 @@ +# Retrofit — Bucket 2b Cross-References + +Annotates Bucket 2b methods that map to existing capability specs. No new REQs are drafted — all methods are annotated with cross-capability task refs to their canonical home spec. + +## Approach +Scanner could not match these methods to any spec REQ (Bucket 2b). Upon inspection, most implement existing capabilities and belong to already-specified clusters. This change cross-references them appropriately. A small set of truly unspecced infrastructure methods (app bootstrap, exception boilerplate, Twig extensions) are annotated with object-lifecycle#REQ-001 as the most general available anchor. + +## Affected code units (by cluster) +- lib/Service/ActivityService.php → event-driven-architecture +- lib/Service/AuthenticationService.php → rbac-scopes (OAuth/JWT auth) +- lib/Reference/ObjectReferenceProvider.php → deep-link-registry +- lib/WorkflowEngine/N8nAdapter.php → workflow-engine-abstraction +- lib/WorkflowEngine/WorkflowResult.php → workflow-engine-abstraction +- lib/BackgroundJob/BulkLegalHoldJob.php → archival-destruction-workflow +- lib/BackgroundJob/ActionScheduleJob.php → workflow-engine-abstraction +- lib/BackgroundJob/BlobMigrationJob.php → data-import-export +- lib/BackgroundJob/ObjectTextExtractionJob.php → object-lifecycle +- lib/BackgroundJob/CronFileTextExtractionJob.php → object-lifecycle +- lib/BackgroundJob/DestructionCheckJob.php → archival-destruction-workflow +- lib/BackgroundJob/SolrNightlyWarmupJob.php → zoeken-filteren +- lib/EventListener/AbstractNodeFolderEventListener.php → event-driven-architecture +- lib/EventListener/AbstractNodesFolderEventListener.php → event-driven-architecture +- lib/EventListener/SolrEventListener.php → zoeken-filteren +- lib/Controller/FileExtractionController.php → object-lifecycle +- lib/Controller/SearchController.php → zoeken-filteren +- lib/Controller/OrganisationController.php → tenant-lifecycle +- lib/Listener/ActionListener.php → event-driven-architecture +- lib/Listener/FilesSidebarListener.php → object-lifecycle +- lib/Listener/FileChangeListener.php → event-driven-architecture +- lib/Listener/ObjectCleanupListener.php → event-driven-architecture +- lib/Listener/GraphQLSubscriptionListener.php → graphql-api +- lib/Listener/CommentsEntityListener.php → event-driven-architecture +- lib/Listener/ObjectChangeListener.php → event-driven-architecture +- lib/Listener/ToolRegistrationListener.php → object-lifecycle +- lib/Listener/WebhookEventListener.php → event-driven-architecture +- lib/Exception/*.php → object-lifecycle (boilerplate getters) +- lib/Sections/OpenRegisterAdmin.php → object-lifecycle +- lib/Command/MigrateStorageCommand.php → object-lifecycle +- lib/Command/SolrManagementCommand.php → zoeken-filteren +- lib/Command/SolrDebugCommand.php → zoeken-filteren +- lib/AppInfo/Application.php → object-lifecycle +- lib/Contacts/ContactsMenuProvider.php → object-lifecycle +- lib/Cron/ConfigurationCheckJob.php → notificatie-engine +- lib/Cron/TransferCheckJob.php → edepot-transfer +- lib/Cron/LogCleanUpTask.php → retention-management +- lib/Cron/WebhookRetryJob.php → webhook-payload-mapping +- lib/Cron/SyncConfigurationsJob.php → faceting-configuration +- lib/Formats/BsnFormat.php, SemVerFormat.php → data-import-export +- lib/Settings/OpenRegisterAdmin.php → object-lifecycle +- lib/Activity/*.php → event-driven-architecture +- lib/Search/ObjectsProvider.php → zoeken-filteren +- lib/Twig/*.php → object-lifecycle +- lib/Notification/Notifier.php → notificatie-engine (already annotated via earlier cluster) +- lib/Middleware/TenantQuota*.php, TenantStatus*.php → tenant-quotas +- lib/Middleware/LanguageMiddleware.php → tenant-quotas +- lib/Repair/RegisterRiskLevelMetadata.php → object-lifecycle + +Source: openspec/coverage-report.md generated 2026-04-28. Bucket 2b cross-reference pass. diff --git a/openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md b/openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md new file mode 100644 index 0000000000..798712f5b5 --- /dev/null +++ b/openspec/archive/retrofit-b2b-crossrefs-2026-04-28/tasks.md @@ -0,0 +1,35 @@ +# Tasks + +- [x] task-1: event-driven-architecture#REQ-004 — ActivityService, AbstractNode*EventListener, FileChangeListener, ObjectChangeListener, ObjectCleanupListener, WebhookEventListener cross-cap (retroactive annotation) +- [x] task-2: event-driven-architecture#REQ-015 — Activity provider/filter/settings cross-cap (retroactive annotation) +- [x] task-3: deep-link-registry#REQ-002 — ObjectReferenceProvider cross-cap (retroactive annotation) +- [x] task-4: workflow-engine-abstraction#REQ-003 — N8nAdapter cross-cap (retroactive annotation) +- [x] task-5: workflow-engine-abstraction#REQ-005 — WorkflowResult cross-cap (retroactive annotation) +- [x] task-6: workflow-engine-abstraction#REQ-010 — ActionScheduleJob cross-cap (retroactive annotation) +- [x] task-7: archival-destruction-workflow#REQ-006 — BulkLegalHoldJob cross-cap (retroactive annotation) +- [x] task-8: archival-destruction-workflow#REQ-001 — DestructionCheckJob cross-cap (retroactive annotation) +- [x] task-9: data-import-export#REQ-001 — BlobMigrationJob cross-cap (retroactive annotation) +- [x] task-10: zoeken-filteren#REQ-001 — SolrNightlyWarmupJob, ObjectsProvider, SearchController, SolrManagementCommand, SolrDebugCommand, SolrEventListener cross-cap (retroactive annotation) +- [x] task-11: graphql-api#REQ-007 — GraphQLSubscriptionListener cross-cap (retroactive annotation) +- [x] task-12: edepot-transfer#REQ-001 — TransferCheckJob cross-cap (retroactive annotation) +- [x] task-13: webhook-payload-mapping#REQ-001 — WebhookRetryJob cross-cap (retroactive annotation) +- [x] task-14: faceting-configuration#REQ-002 — SyncConfigurationsJob cross-cap (retroactive annotation) +- [x] task-15: retention-management#REQ-009 — LogCleanUpTask cross-cap (retroactive annotation) +- [x] task-16: tenant-lifecycle#REQ-001 — OrganisationController cross-cap (retroactive annotation) +- [x] task-17: tenant-quotas#REQ-001 — TenantQuotaExceededException, TenantStatusException, LanguageMiddleware cross-cap (retroactive annotation) +- [x] task-18: notificatie-engine#REQ-001 — ConfigurationCheckJob (sends update notifications) cross-cap (retroactive annotation) +- [x] task-19: event-driven-architecture#REQ-004 — ActionListener cross-cap (retroactive annotation) +- [x] task-20: object-lifecycle#REQ-001 — FileExtractionController, CronFileTextExtractionJob, ObjectTextExtractionJob, ToolRegistrationListener, FilesSidebarListener cross-cap (retroactive annotation) +- [x] task-21: object-lifecycle#REQ-001 — RegisterRiskLevelMetadata (repair step), MigrateStorageCommand cross-cap (retroactive annotation) +- [x] task-22: data-import-export#REQ-001 — BsnFormat, SemVerFormat (validation formats) cross-cap (retroactive annotation) +- [x] task-23: event-driven-architecture#REQ-004 — CommentsEntityListener cross-cap (retroactive annotation) +- [x] task-24: object-lifecycle#REQ-001 — AppInfo/Application (app bootstrap) cross-cap (retroactive annotation) +- [x] task-25: object-lifecycle#REQ-001 — Settings sections/admin pages cross-cap (retroactive annotation) +- [x] task-26: object-lifecycle#REQ-001 — Exception classes (boilerplate getters) cross-cap (retroactive annotation) +- [x] task-27: rbac-scopes#REQ-001 — AuthenticationService (OAuth/JWT token management) cross-cap (retroactive annotation) +- [x] task-28: object-lifecycle#REQ-001 — Twig extensions/runtimes, ContactsMenuProvider cross-cap (retroactive annotation) +- [x] task-29: object-lifecycle#REQ-001 — SchemaTool, RegisterTool, ToolInterface (LLM function tool layer over object CRUD) cross-cap (retroactive annotation) +- [x] task-30: object-lifecycle#REQ-003 — CacheWarmupJob, NameCacheWarmupJob (configurable cache warmup background jobs) cross-cap (retroactive annotation) +- [x] task-31: zoeken-filteren#REQ-001 — SolrWarmupJob (post-import Solr index warmup) cross-cap (retroactive annotation) +- [x] task-32: workflow-engine-abstraction#REQ-010 — ActionRetryJob (retry failed action executions), ExecutionHistoryCleanupJob (prune old execution history) cross-cap (retroactive annotation) +- [x] task-33: edepot-transfer#REQ-001 — TransferExecutionJob (queued e-Depot transfer execution) cross-cap (retroactive annotation) diff --git a/openspec/archive/retrofit-calendar-integration-2026-04-28/proposal.md b/openspec/archive/retrofit-calendar-integration-2026-04-28/proposal.md new file mode 100644 index 0000000000..290e1f919f --- /dev/null +++ b/openspec/archive/retrofit-calendar-integration-2026-04-28/proposal.md @@ -0,0 +1,13 @@ +# Retrofit — calendar-integration (new cluster) + +Describes observed behavior of 19 methods in the calendar cluster as 2 new REQs in a new `calendar-integration` capability. Code already exists — this change retroactively specifies it. + +## Affected code units +- lib/Calendar/RegisterCalendarProvider.php (3 methods: getCalendars, getCalendarEnabledSchemas, isValidUserPrincipal) +- lib/Calendar/RegisterCalendar.php (9 methods: getKey, getUri, getDisplayName, getDisplayColor, getPermissions, isDeleted, search, extractUserId, buildTimerangeFilters, findRegistersForSchema, matchesPattern) +- lib/Calendar/CalendarEventTransformer.php (5 methods: transform, determineAllDay, formatDateValue, buildDtend, interpolateTemplate) + +## Approach +REQ-001 covers the ICalendarProvider/ICalendar contract implementation (CalDAV exposure layer). REQ-002 covers the iCalendar data transformation (VEVENT generation with all-day vs datetime handling, template interpolation). + +Source: openspec/coverage-report.md generated 2026-04-28. Bucket 2b — calendar cluster. diff --git a/openspec/archive/retrofit-calendar-integration-2026-04-28/specs/calendar-integration/spec.md b/openspec/archive/retrofit-calendar-integration-2026-04-28/specs/calendar-integration/spec.md new file mode 100644 index 0000000000..8fd7b5d62f --- /dev/null +++ b/openspec/archive/retrofit-calendar-integration-2026-04-28/specs/calendar-integration/spec.md @@ -0,0 +1,59 @@ +--- +retrofit: true +--- + +# Calendar Integration + +## Purpose + +OpenRegister can surface register objects as Nextcloud calendar events by implementing the `ICalendarProvider` contract. Objects from enabled schemas are exposed as iCalendar events (VEVENT) via CalDAV, allowing any CalDAV client (Nextcloud Calendar app, iOS/Android) to subscribe to, view, and filter them. This capability integrates with the object-interactions spec's broader "objects as structured resources" theme but focuses specifically on the CalDAV provider layer. + +## Requirements + +### REQ-001: The system MUST expose register objects as CalDAV calendar events + +`RegisterCalendarProvider` MUST implement `ICalendarProvider` and return a list of `RegisterCalendar` instances for schemas configured with `calendarEnabled: true`. Each `RegisterCalendar` MUST implement the Nextcloud `ICalendar` contract (getKey, getUri, getDisplayName, getDisplayColor, getPermissions, isDeleted, search) and MUST filter objects by the authenticated user's principal. + +#### Scenario: Provider returns configured schemas as calendars +- **GIVEN** register `gemeente` has schemas `meldingen` (calendarEnabled: true) and `vergunningen` (calendarEnabled: false) +- **WHEN** `RegisterCalendarProvider::getCalendars()` is called for principal `/principals/users/behandelaar-1` +- **THEN** exactly one calendar MUST be returned (for `meldingen`) +- **AND** the calendar key MUST be in the format `openregister_{registerId}_{schemaId}` +- **AND** the user MUST be extracted from the principal URI via the last path segment + +#### Scenario: Calendar search returns matching objects as VEVENT +- **GIVEN** register `gemeente` / schema `meldingen` has 3 objects with date fields matching a time range +- **WHEN** `RegisterCalendar::search()` is called with `start` and `end` calendar filters +- **THEN** objects whose date field falls within the range MUST be returned as iCalendar VEVENT strings +- **AND** each VEVENT MUST include `UID`, `DTSTART`, `DTEND`, `SUMMARY`, and `DESCRIPTION` components + +### REQ-002: The system MUST transform register objects to iCalendar VEVENT format + +`CalendarEventTransformer::transform()` MUST convert an OpenRegister object array into a complete iCalendar `VCALENDAR` string containing a single `VEVENT`. The transformer MUST determine whether the event is all-day (boolean date properties) or datetime-based, format date values to iCalendar `DATE` or `DATE-TIME` format accordingly, and compute `DTEND` from either an explicit end-date property or a 1-hour default duration. + +#### Scenario: All-day event from boolean date schema property +- **GIVEN** object has property `datum` with value `"2026-05-15"` (no time component) +- **WHEN** `CalendarEventTransformer::determineAllDay()` is called +- **THEN** it MUST return `true` +- **AND** `formatDateValue()` MUST return `"20260515"` (DATE format) +- **AND** DTSTART MUST use VALUE=DATE parameter + +#### Scenario: DateTime event with explicit end property +- **GIVEN** object has `startTime: "2026-05-15T09:00:00"` and `endTime: "2026-05-15T10:30:00"` +- **WHEN** `transform()` is called +- **THEN** DTSTART MUST be `"20260515T090000"` and DTEND MUST be `"20260515T103000"` + +#### Scenario: DateTime event with no end property (default 1-hour duration) +- **GIVEN** object has `startTime: "2026-05-15T09:00:00"` and no end property +- **WHEN** `buildDtend()` is called +- **THEN** DTEND MUST be `"20260515T100000"` (1 hour after start) + +#### Scenario: Template interpolation in SUMMARY +- **GIVEN** schema has `calendarSummaryTemplate: "Melding: {{omschrijving}}"` +- **AND** object has `omschrijving: "Geluidsoverlast"` +- **WHEN** `interpolateTemplate()` is called +- **THEN** the result MUST be `"Melding: Geluidsoverlast"` + +#### Notes +- `RegisterCalendar::search()` builds Nextcloud QBMapper-style timerange filters using the schema's configured date property name. If no date property is configured, the calendar MUST return an empty array. +- The CalDAV integration is read-only: objects can be viewed via CalDAV but not created or modified through the calendar interface. diff --git a/openspec/archive/retrofit-calendar-integration-2026-04-28/tasks.md b/openspec/archive/retrofit-calendar-integration-2026-04-28/tasks.md new file mode 100644 index 0000000000..78517f223e --- /dev/null +++ b/openspec/archive/retrofit-calendar-integration-2026-04-28/tasks.md @@ -0,0 +1,4 @@ +# Tasks + +- [x] task-1: calendar-integration#REQ-001 — RegisterCalendarProvider + RegisterCalendar ICalendar implementation (retroactive annotation) +- [x] task-2: calendar-integration#REQ-002 — CalendarEventTransformer VEVENT format conversion (retroactive annotation) diff --git a/openspec/archive/retrofit-content-versioning-2026-04-28/proposal.md b/openspec/archive/retrofit-content-versioning-2026-04-28/proposal.md new file mode 100644 index 0000000000..999787c8d5 --- /dev/null +++ b/openspec/archive/retrofit-content-versioning-2026-04-28/proposal.md @@ -0,0 +1,16 @@ +# Retrofit — content-versioning + +Describes observed behavior of 4 methods under `content-versioning` as 1 new REQ. Code already exists — this change retroactively specifies it. + +## Affected code units +- lib/Service/File/FileVersioningHandler.php::listVersions +- lib/Service/File/FileVersioningHandler.php::restoreVersion +- lib/Service/File/FileVersioningHandler.php::isVersioningEnabled +- lib/Service/File/FileVersioningHandler.php::getCurrentUserId (private) + +## Approach +- `FileVersioningHandler` provides a wrapper around Nextcloud's `files_versions` app to list and restore file attachment versions. The existing content-versioning spec covers *object* versioning via audit trail (semantic versioning, drafts, rollback) but does not specify file attachment versioning via NC's `IVersionManager`. This change adds REQ-017 to cover that observed behavior. +- 2 methods (`ExecutionHistoryCleanupJob::run` and `MessageHistoryHandler`) appeared in the content-versioning Bucket 2a cluster but are scanner misclassifications: ExecutionHistoryCleanupJob prunes WorkflowExecution records (workflow-engine-abstraction capability) and MessageHistoryHandler manages LLM chat conversation history (no current spec). These are deferred to their respective capability clusters. +- Vue component methods are skipped — @spec convention not established for JS/Vue files. + +Source: openspec/coverage-report.md generated 2026-04-23. See retrofit playbook. diff --git a/openspec/archive/retrofit-content-versioning-2026-04-28/specs/content-versioning/spec.md b/openspec/archive/retrofit-content-versioning-2026-04-28/specs/content-versioning/spec.md new file mode 100644 index 0000000000..b53d13a8cd --- /dev/null +++ b/openspec/archive/retrofit-content-versioning-2026-04-28/specs/content-versioning/spec.md @@ -0,0 +1,31 @@ +--- +retrofit_extensions: [REQ-017] +--- + +### REQ-017: The system MUST support listing and restoring file versions via Nextcloud files_versions + +When Nextcloud's `files_versions` app is enabled, the system MUST expose version history and restore operations for files attached to register objects. The `FileVersioningHandler` MUST wrap `IVersionManager` to list all historical snapshots with metadata (versionId, timestamp, size, author, isCurrent flag) and MUST support restoring a specific version by its timestamp-based identifier. When `files_versions` is disabled, listing MUST degrade gracefully by returning an empty version array with a warning, while restoring MUST throw an Exception. + +#### Scenario: List versions when files_versions is enabled +- **GIVEN** the `files_versions` Nextcloud app is enabled +- **WHEN** `FileVersioningHandler::listVersions($file)` is called +- **THEN** the response MUST include a `versions` array where the first entry represents the current file version (`isCurrent: true`) with fields `versionId`, `timestamp`, `size`, `author`, `authorDisplayName`, `label`, and `isCurrent` +- **AND** each historical version MUST have `versionId` in the format `v-{unix_timestamp}` and `isCurrent: false` + +#### Scenario: Graceful degradation when files_versions is disabled +- **GIVEN** the `files_versions` Nextcloud app is NOT enabled +- **WHEN** `FileVersioningHandler::listVersions($file)` is called +- **THEN** the response MUST return `{versions: [], warning: "File versioning is not enabled on this instance"}` +- **AND** `FileVersioningHandler::restoreVersion($file, $versionId)` MUST throw an Exception with the same message + +#### Scenario: Restore a specific file version +- **GIVEN** the `files_versions` app is enabled and file `/user/files/report.pdf` has a historical version with `versionId: "v-1710892800"` +- **WHEN** `FileVersioningHandler::restoreVersion($file, "v-1710892800")` is called +- **THEN** the system MUST locate the version whose timestamp matches `1710892800` +- **AND** call `IVersionManager::rollback($version)` to restore the file content +- **AND** return `true` on success +- **AND** throw an Exception with message `"Version not found"` if no matching timestamp exists + +#### Notes +- The `IVersionManager` is resolved via `\OCP\Server::get()` with a class_exists guard since `OCA\Files_Versions\Versions\IVersionManager` is not always available (depends on enabled apps). This is an observed runtime resolution pattern, not dependency injection. +- The `getCurrentUserId()` private method falls back to the literal string `'system'` when no authenticated session exists (background job context). diff --git a/openspec/archive/retrofit-content-versioning-2026-04-28/tasks.md b/openspec/archive/retrofit-content-versioning-2026-04-28/tasks.md new file mode 100644 index 0000000000..69bbc6147e --- /dev/null +++ b/openspec/archive/retrofit-content-versioning-2026-04-28/tasks.md @@ -0,0 +1,3 @@ +# Tasks + +- [x] task-1: content-versioning#REQ-017 — The system MUST support listing and restoring file versions via Nextcloud files_versions (retroactive annotation) diff --git a/openspec/archive/retrofit-notificatie-engine-2026-04-28/proposal.md b/openspec/archive/retrofit-notificatie-engine-2026-04-28/proposal.md new file mode 100644 index 0000000000..ba748ce1a3 --- /dev/null +++ b/openspec/archive/retrofit-notificatie-engine-2026-04-28/proposal.md @@ -0,0 +1,22 @@ +# Retrofit — notificatie-engine + +Maps 6 PHP methods from the notificatie-engine Bucket 2a cluster to the existing spec requirement for INotificationManager integration. No new REQs needed — the spec's first named requirement covers all observed behaviors. + +## Affected code units +- lib/Notification/Notifier.php::getID +- lib/Notification/Notifier.php::prepare +- lib/Notification/Notifier.php::prepareConfigurationUpdate (private) +- lib/Service/NotificationService.php::notifyConfigurationUpdate +- lib/Service/NotificationService.php::sendUpdateNotification (private) +- lib/Service/NotificationService.php::markConfigurationUpdated + +## Skipped +- src/views/account/sections/NotificationsSection.vue::save — @spec tag convention not established for Vue files + +## Approach +- All methods implement the first named requirement: "The system MUST integrate with Nextcloud's INotificationManager for in-app notifications" +- Notifier implements INotifier contract for displaying configuration_update_available notifications with i18n +- NotificationService resolves recipients from groups, creates and sends Nextcloud notifications, and marks them as processed +- Spec status is `partial` — only configuration update notifications are currently implemented; the rest of the notificatie-engine spec is not yet implemented + +Source: openspec/coverage-report.md generated 2026-04-23. See retrofit playbook. diff --git a/openspec/archive/retrofit-notificatie-engine-2026-04-28/tasks.md b/openspec/archive/retrofit-notificatie-engine-2026-04-28/tasks.md new file mode 100644 index 0000000000..15f8f720d2 --- /dev/null +++ b/openspec/archive/retrofit-notificatie-engine-2026-04-28/tasks.md @@ -0,0 +1,3 @@ +# Tasks + +- [x] task-1: notificatie-engine#REQ-001 — INotificationManager integration: Notifier + NotificationService (retroactive annotation, covered by first requirement in spec) diff --git a/openspec/archive/retrofit-object-lifecycle-2026-04-28/proposal.md b/openspec/archive/retrofit-object-lifecycle-2026-04-28/proposal.md new file mode 100644 index 0000000000..42d39ae316 --- /dev/null +++ b/openspec/archive/retrofit-object-lifecycle-2026-04-28/proposal.md @@ -0,0 +1,55 @@ +# Retrofit — object-lifecycle (new cluster) + +Describes observed behavior of 252 methods classified by the scanner under `object-interactions` (Bucket 2a) as 5 new REQs in a new `object-lifecycle` capability. Code already exists — this change retroactively specifies it. + +## Affected code units (core object-lifecycle) +- lib/Service/Object/SaveObjects.php (33 methods) +- lib/Service/Object/ValidateObject.php (21 methods) +- lib/Service/Object/CacheHandler.php (16 methods) +- lib/Service/Object/ValidationHandler.php (13 methods) +- lib/Service/Object/SaveObject/MetadataHydrationHandler.php (13 methods) +- lib/Service/Object/SaveObject.php (9 methods) +- lib/Service/Object/PerformanceHandler.php (8 methods) +- lib/Service/Object/SaveObject/RelationCascadeHandler.php (8 methods) +- lib/Service/Object/CrudHandler.php (7 methods) +- lib/Service/Object/UtilityHandler.php (6 methods) +- lib/Service/Object/RenderObject.php (5 methods) +- lib/Service/Object/RelationshipOptimizationHandler.php (5 methods) +- lib/Service/Object/TranslationHandler.php (4 methods) +- lib/Service/Object/SaveObjects/PreparationHandler.php (4 methods) +- lib/Service/Object/VectorizationHandler.php (3 methods) +- lib/Service/Object/DataManipulationHandler.php (3 methods) +- lib/Service/Object/GetObject.php (3 methods) +- lib/Service/Object/SaveObjects/BulkValidationHandler.php (3 methods) +- lib/Service/Object/ReferentialIntegrityService.php (2 methods) +- lib/Service/Object/DeleteObject.php (2 methods) +- lib/Service/Object/MetadataHandler.php (2 methods) +- lib/Service/Object/SaveObject/ComputedFieldHandler.php (2 methods) +- lib/Service/Object/MigrationHandler.php (1 method) +- lib/Service/Object/PerformanceOptimizationHandler.php (1 method) +- lib/Service/Object/SaveObjects/TransformationHandler.php (1 method) +- lib/Service/Object/SaveObjects/ChunkProcessingHandler.php (1 method) + +## Cross-capability misclassified units (existing task refs) +- lib/Service/Object/PermissionHandler.php → rbac-scopes#REQ-001 +- lib/Service/Object/LinkedEntityEnricher.php → linked-entity-types#REQ-003 +- lib/Service/Object/RelationHandler.php → linked-entity-types#REQ-003 +- lib/Service/Object/SaveObject/RelationCascadeHandler.php → linked-entity-types#REQ-003 +- lib/Service/Object/SaveObjects/BulkRelationHandler.php → linked-entity-types#REQ-003 +- lib/Service/Object/AuditHandler.php → audit-trail-immutable#REQ-002 +- lib/Service/Object/FacetHandler.php → faceting-configuration#REQ-002 +- lib/Service/Object/SearchQueryHandler.php → zoeken-filteren#REQ-001 +- lib/Service/Object/QueryHandler.php → zoeken-filteren#REQ-001 +- lib/Service/Object/ExportHandler.php → data-import-export#REQ-007 +- lib/Service/Object/RevertHandler.php → content-versioning#REQ-005 +- lib/Service/Object/SaveObject/FilePropertyHandler.php → content-versioning#REQ-017 + +## Approach +The coverage scanner classified all 252 methods under `object-interactions` (the notes/tasks/files spec), which is a misclassification. None of these handlers deal with the ICommentsManager, CalDAV, or file attachment APIs. The core CRUD handlers (SaveObjects, ValidateObject, etc.) implement an internal layered pipeline with no corresponding capability spec. This change mints `object-lifecycle` as a new capability to anchor them. Cross-cap handlers (PermissionHandler, AuditHandler, etc.) get cross-references to their correct capability specs. + +Notes: +- `NewFacetingExample.php` and `ObjectServiceFacetExample.php` are example/demo files — annotated under faceting-configuration as documentation aids +- `ReferentialIntegrityService.php` is annotated under object-lifecycle#REQ-001 since it enforces referential integrity during the save pipeline +- The scanner misclassification is expected to persist until the next scan re-scores these methods against the new object-lifecycle spec + +Source: openspec/coverage-report.md generated 2026-04-28. See retrofit playbook. diff --git a/openspec/archive/retrofit-object-lifecycle-2026-04-28/specs/object-lifecycle/spec.md b/openspec/archive/retrofit-object-lifecycle-2026-04-28/specs/object-lifecycle/spec.md new file mode 100644 index 0000000000..9cd1bf4e41 --- /dev/null +++ b/openspec/archive/retrofit-object-lifecycle-2026-04-28/specs/object-lifecycle/spec.md @@ -0,0 +1,91 @@ +--- +retrofit: true +--- + +# Object Lifecycle + +## Purpose + +Describes the internal pipeline that governs how OpenRegister objects are created, read, updated, and deleted. This capability covers the layered handler pattern used to decompose the save, validate, cache, metadata-hydration, and bulk processing concerns, and is the foundation on which all higher-level capabilities (schema hooks, RBAC, retention, audit trail) attach their side effects. + +## Requirements + +### REQ-001: The system MUST process object mutations through a layered save pipeline + +Every create or update operation on an object MUST be processed through a layered handler pipeline before persisting. For a single object the entry point is `SaveObject::saveObject()`, which invokes validation, metadata hydration, computed field resolution, and relation cascade handlers in sequence before calling the mapper's insert/update. For bulk operations the entry point is `SaveObjects::saveObjects()`, which delegates to `SaveObject::saveObject()` per item after preparation and chunking. The pipeline MUST be deterministic: the same input object with the same schema MUST always produce the same persisted state and version increment. + +#### Scenario: Single object create flows through full pipeline +- **GIVEN** a valid object payload for schema `meldingen` in register `gemeente` +- **WHEN** `SaveObject::saveObject()` is invoked +- **THEN** the pipeline MUST invoke validation, metadata hydration, computed field resolution, and relation cascade handlers in order before calling the mapper's insert/update +- **AND** the resulting entity MUST have a non-null `uuid`, `version`, and `created` timestamp + +#### Scenario: Pipeline short-circuits on validation failure +- **GIVEN** an object payload that fails schema validation (missing required field) +- **WHEN** the save pipeline is invoked +- **THEN** the pipeline MUST return a validation error response before reaching the persistence step +- **AND** no database write MUST occur + +### REQ-002: Object validation MUST enforce schema constraints before persistence + +The `ValidateObject` and `ValidationHandler` MUST check all object field values against the schema's property definitions (type, format, required, enum, pattern) before the object is persisted. Validation errors MUST be collected and returned as a structured array, not as exceptions. + +#### Scenario: Required field missing +- **GIVEN** schema `meldingen` has a required property `omschrijving` +- **WHEN** an object without `omschrijving` is validated +- **THEN** `ValidationHandler` MUST return `["omschrijving" => "Field is required"]` + +#### Scenario: Bulk validation collects all errors +- **GIVEN** a bulk import payload of 50 objects, 5 of which have type mismatches +- **WHEN** `BulkValidationHandler` validates the batch +- **THEN** the 45 valid objects MUST proceed and the 5 invalid ones MUST be returned as failed with per-field error details +- **AND** the successful objects MUST NOT be blocked by the failures + +### REQ-003: Object reads MUST be served from cache when available + +`CacheHandler` MUST cache retrieved objects by their UUID key. A cache hit MUST bypass the database query entirely. Cache MUST be invalidated on any successful save or delete of the same UUID. The cache strategy MUST be transparent to callers of the object service layer. + +#### Scenario: Cache hit bypasses database +- **GIVEN** object `abc-123` was previously fetched and cached +- **WHEN** a second request for `abc-123` arrives within the cache TTL +- **THEN** the response MUST be served from cache without a database query +- **AND** `PerformanceHandler` metrics MUST record a cache hit + +#### Scenario: Save invalidates cache +- **GIVEN** object `abc-123` is in cache +- **WHEN** `CrudHandler` persists an update to `abc-123` +- **THEN** the cache entry for `abc-123` MUST be evicted before the updated object is returned + +### REQ-004: Bulk object operations MUST use chunked processing + +When handling batches of objects, `SaveObjects` and its sub-handlers (`PreparationHandler`, `ChunkProcessingHandler`) MUST split the batch into configurable chunks to limit memory consumption and enable partial-success reporting. Each chunk MUST be processed independently so that a failure in one chunk does not roll back already-persisted chunks. + +#### Scenario: Large import is chunked +- **GIVEN** a bulk import of 5000 objects with chunk size 100 +- **WHEN** `ChunkProcessingHandler` processes the import +- **THEN** objects MUST be processed in groups of 100 +- **AND** the response MUST include a `processed`, `failed`, and `skipped` count per chunk +- **AND** a failure in chunk 30 MUST NOT roll back objects from chunks 1–29 + +### REQ-005: Object metadata MUST be hydrated before persistence + +`MetadataHydrationHandler` MUST populate system-managed fields (uuid, created, updated, version, organisationId, application) on every object before it is inserted or updated. Computed fields MUST be evaluated after user-provided data is set, so computations can reference other field values. + +#### Scenario: UUID assigned on first save +- **GIVEN** a new object is submitted without a uuid +- **WHEN** `MetadataHydrationHandler` processes it +- **THEN** a UUIDv4 MUST be assigned to the `uuid` field +- **AND** `created` and `updated` MUST both be set to the current UTC timestamp + +#### Scenario: Computed field references sibling field +- **GIVEN** schema `meldingen` has a computed field `volledigeNaam` that concatenates `voornaam` and `achternaam` +- **WHEN** an object with `voornaam: "Jan"` and `achternaam: "Janssen"` is saved +- **THEN** `ComputedFieldHandler` MUST set `volledigeNaam: "Jan Janssen"` after hydration + +## Cross-References +- **rbac-scopes** — RBAC checks are applied by `PermissionHandler` at the start of every pipeline stage +- **schema-hooks** — schema hooks fire via event dispatcher after each successful save +- **audit-trail-immutable** — `AuditHandler` records every mutation as an immutable audit trail entry +- **linked-entity-types** — `RelationHandler` and `RelationCascadeHandler` resolve and cascade linked entity relations +- **faceting-configuration** — `FacetHandler` builds facet aggregations from queried object sets +- **zoeken-filteren** — `SearchQueryHandler` and `QueryHandler` translate search parameters into database queries diff --git a/openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md b/openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md new file mode 100644 index 0000000000..a0b1f8b102 --- /dev/null +++ b/openspec/archive/retrofit-object-lifecycle-2026-04-28/tasks.md @@ -0,0 +1,17 @@ +# Tasks + +- [x] task-1: object-lifecycle#REQ-001 — Object save pipeline (SaveObjects, SaveObject, CrudHandler, DeleteObject, GetObject, RenderObject, DataManipulationHandler, MigrationHandler) (retroactive annotation) +- [x] task-2: object-lifecycle#REQ-002 — Object validation pipeline (ValidateObject, ValidationHandler, BulkValidationHandler, VectorizationHandler) (retroactive annotation) +- [x] task-3: object-lifecycle#REQ-003 — Object cache management (CacheHandler, PerformanceHandler, PerformanceOptimizationHandler, RelationshipOptimizationHandler) (retroactive annotation) +- [x] task-4: object-lifecycle#REQ-004 — Bulk object operations (PreparationHandler, ChunkProcessingHandler) (retroactive annotation) +- [x] task-5: object-lifecycle#REQ-005 — Object metadata hydration (MetadataHydrationHandler, MetadataHandler, ComputedFieldHandler) (retroactive annotation) +- [x] task-6: linked-entity-types#REQ-003 — Relation handling cross-cap (LinkedEntityEnricher, RelationHandler, RelationCascadeHandler, BulkRelationHandler, LinkedEntityPropertyHandler, CascadingHandler) (retroactive annotation) +- [x] task-7: rbac-scopes#REQ-001 — PermissionHandler cross-cap (PermissionHandler) (retroactive annotation) +- [x] task-8: audit-trail-immutable#REQ-002 — AuditHandler cross-cap (retroactive annotation) +- [x] task-9: faceting-configuration#REQ-002 — FacetHandler + example files cross-cap (retroactive annotation) +- [x] task-10: zoeken-filteren#REQ-001 — SearchQueryHandler, QueryHandler cross-cap (retroactive annotation) +- [x] task-11: data-import-export#REQ-007 — ExportHandler cross-cap (retroactive annotation) +- [x] task-12: content-versioning#REQ-005 — RevertHandler cross-cap (retroactive annotation) +- [x] task-13: content-versioning#REQ-017 — FilePropertyHandler cross-cap (retroactive annotation) +- [x] task-14: object-lifecycle#REQ-001 — UtilityHandler (object pipeline utility methods) (retroactive annotation) +- [x] task-15: object-lifecycle#REQ-001 — TranslationHandler (retroactive annotation) diff --git a/openspec/archive/retrofit-schema-hooks-2026-04-28/proposal.md b/openspec/archive/retrofit-schema-hooks-2026-04-28/proposal.md new file mode 100644 index 0000000000..d560399614 --- /dev/null +++ b/openspec/archive/retrofit-schema-hooks-2026-04-28/proposal.md @@ -0,0 +1,17 @@ +# Retrofit — schema-hooks (private helper methods) + +Maps 7 private/unlisted methods that the scanner could not match to the existing schema-hooks retrofit-annotate tasks. All behaviors are fully covered — no new REQs required. + +## Affected code units +- lib/Listener/HookListener.php::handle (task-65 coverage) +- lib/Listener/HookListener.php::getObjectFromEvent (task-65 coverage, private) +- lib/Service/HookExecutor.php::getObjectFromEvent (task-65 coverage, private) +- lib/Service/HookExecutor.php::isEventStopped (task-68 coverage, private) +- lib/Service/HookExecutor.php::executeSingleHook (task-65 coverage, private) +- lib/Service/HookExecutor.php::setModifiedDataOnEvent (task-67 coverage, private) +- lib/Service/HookExecutor.php::setValidationMetadata (task-69 coverage, private) + +## Approach +All 7 methods are private implementation helpers whose public-facing behaviors are already annotated at class level (HookExecutor class docblock lists task-65 through task-72). The scanner missed them because it couldn't follow the private method call chain. Annotating at the method level adds precision without new REQs. + +Source: openspec/coverage-report.md generated 2026-04-23. See retrofit playbook. diff --git a/openspec/archive/retrofit-schema-hooks-2026-04-28/tasks.md b/openspec/archive/retrofit-schema-hooks-2026-04-28/tasks.md new file mode 100644 index 0000000000..4479c4ea35 --- /dev/null +++ b/openspec/archive/retrofit-schema-hooks-2026-04-28/tasks.md @@ -0,0 +1,4 @@ +# Tasks + +- [x] task-1: schema-hooks#REQ-002/REQ-010 — HookListener::handle + getObjectFromEvent (retroactive annotation, covered by task-65/task-71) +- [x] task-2: schema-hooks#REQ-005/REQ-006 — HookExecutor private helpers: getObjectFromEvent, isEventStopped, executeSingleHook, setModifiedDataOnEvent, setValidationMetadata (retroactive annotation, covered by task-65/task-67/task-68/task-69) diff --git a/openspec/archive/retrofit-tenant-isolation-audit-2026-04-28/proposal.md b/openspec/archive/retrofit-tenant-isolation-audit-2026-04-28/proposal.md new file mode 100644 index 0000000000..1171bc9831 --- /dev/null +++ b/openspec/archive/retrofit-tenant-isolation-audit-2026-04-28/proposal.md @@ -0,0 +1,19 @@ +# Retrofit — tenant-isolation-audit + +Maps 3 methods from the `tenant-isolation-audit` bucket to existing REQs in other capabilities. No new REQs drafted — behaviors are fully covered by specs already in place. + +## Affected code units +- lib/BackgroundJob/TenantPurgeJob.php::run (tenant-lifecycle#REQ-003 — purge scenario) +- lib/BackgroundJob/TenantUsageSyncJob.php::run (tenant-quotas#REQ-004 — usage counter persistence) +- lib/Middleware/TenantQuotaMiddleware.php::checkRequestQuota (tenant-quotas#REQ-001 via task-75) + +## Approach +- `TenantPurgeJob::run`: daily job; queries archived organisations; purges those past configurable retention window (default 90 days); deletes usage records + org entity. Implements the "purge after retention" scenario of tenant-lifecycle#REQ-003. +- `TenantUsageSyncJob::run`: every 5 minutes; reads APCu hourly-bucket counters per active org; upserts to `openregister_tenant_usage` table. Implements tenant-quotas#REQ-004. +- `TenantQuotaMiddleware::checkRequestQuota`: private; reads APCu counter, applies OTAP environment multiplier, throws `TenantQuotaExceededException` when exceeded. Covered by existing class-level task-75 annotation (tenant-quotas#REQ-001). + +## Notes +- Scanner placed these under `tenant-isolation-audit` due to proximity to tenant management code; actual behaviors belong to `tenant-lifecycle` and `tenant-quotas`. +- `checkRequestQuota` is private; class-level annotation already present (task-75). Method-level annotation added for completeness. + +Source: openspec/coverage-report.md generated 2026-04-23. See retrofit playbook. diff --git a/openspec/archive/retrofit-tenant-isolation-audit-2026-04-28/tasks.md b/openspec/archive/retrofit-tenant-isolation-audit-2026-04-28/tasks.md new file mode 100644 index 0000000000..cab64511cd --- /dev/null +++ b/openspec/archive/retrofit-tenant-isolation-audit-2026-04-28/tasks.md @@ -0,0 +1,4 @@ +# Tasks + +- [x] task-1: tenant-lifecycle#REQ-003 — Deprovisioned organisations MUST transition to archived with data retention (retroactive annotation) +- [x] task-2: tenant-quotas#REQ-004 — Usage counters MUST be persisted via background job (retroactive annotation) diff --git a/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/proposal.md b/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/proposal.md new file mode 100644 index 0000000000..31c86364b4 --- /dev/null +++ b/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/proposal.md @@ -0,0 +1,21 @@ +# Retrofit — tenant-lifecycle + +Describes observed behavior of 4 methods under `tenant-lifecycle` as 1 new REQ + 1 backfilled existing REQ annotation. Code already exists — this change retroactively specifies it. + +## Affected code units +- lib/BackgroundJob/TenantDeprovisionJob.php::run (REQ-003 backfill) +- lib/Service/TenantLifecycleService.php::isValidEnvironment (REQ-005 new) +- lib/Service/TenantLifecycleService.php::isValidPromotionOrder (REQ-005 new) +- lib/Service/TenantLifecycleService.php::validateTransition (REQ-001 existing, task-73) + +## Approach +- `isValidEnvironment`: checks env ∈ OTAP_ORDER constant map — returns bool, no side effects +- `isValidPromotionOrder`: compares OTAP_ORDER[source] < OTAP_ORDER[target] — returns bool, unknown envs get order -1 (automatically fail) +- `TenantDeprovisionJob::run`: hourly job, queries deprovisioning orgs, calls archive() on each, logs result +- `validateTransition`: checks STATE_TRANSITIONS map, throws Exception (HTTP 409) on invalid transition + +## Observed gap (REQ-003) +The spec says: "all objects belonging to the Organisation MUST be soft-deleted" during deprovisioning. +The actual job only calls `tenantLifecycleService->archive()` which transitions status to `archived` but does NOT soft-delete objects. Object soft-deletion is commented as deferred to "the purge job after retention period expires." This diverges from the REQ-003 scenario. Not fixing — surfacing for reviewer attention. + +Source: openspec/coverage-report.md generated 2026-04-23. See retrofit playbook. diff --git a/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/specs/tenant-lifecycle/spec.md b/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/specs/tenant-lifecycle/spec.md new file mode 100644 index 0000000000..5568a47e68 --- /dev/null +++ b/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/specs/tenant-lifecycle/spec.md @@ -0,0 +1,26 @@ +--- +retrofit_extensions: [REQ-005] +--- + +### REQ-005: The system MUST validate OTAP environment values and enforce unidirectional promotion order + +The `TenantLifecycleService` MUST expose utility methods for validating OTAP (Development, Test, Acceptance, Production) environments in multi-environment SaaS deployments. Validation MUST confirm that a given environment name is one of the four recognised OTAP stages, and that promotions only flow in the canonical upward direction (development → test → acceptance → production). Reverse promotions or same-environment promotions MUST be rejected. + +#### Scenario: Valid OTAP environment names are accepted +- **GIVEN** the system knows four OTAP stages: `development`, `test`, `acceptance`, `production` +- **WHEN** `isValidEnvironment("acceptance")` is called +- **THEN** the method MUST return `true` +- **AND** `isValidEnvironment("staging")` MUST return `false` +- **AND** `isValidEnvironment("")` MUST return `false` + +#### Scenario: Unidirectional promotion is enforced +- **GIVEN** OTAP order is development (0) < test (1) < acceptance (2) < production (3) +- **WHEN** `isValidPromotionOrder("test", "acceptance")` is called +- **THEN** the method MUST return `true` (upward promotion) +- **AND** `isValidPromotionOrder("production", "test")` MUST return `false` (reverse — not allowed) +- **AND** `isValidPromotionOrder("test", "test")` MUST return `false` (same-stage — not allowed) + +#### Scenario: Invalid environment names are rejected in promotion checks +- **GIVEN** an unknown environment string (e.g. `"staging"`) is passed as source or target +- **WHEN** `isValidPromotionOrder("staging", "production")` is called +- **THEN** the method MUST return `false` diff --git a/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/tasks.md b/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/tasks.md new file mode 100644 index 0000000000..85526bc82a --- /dev/null +++ b/openspec/archive/retrofit-tenant-lifecycle-2026-04-28/tasks.md @@ -0,0 +1,4 @@ +# Tasks + +- [x] task-1: tenant-lifecycle#REQ-003 — Deprovisioned organisations MUST transition to archived with data retention (retroactive annotation) +- [x] task-2: tenant-lifecycle#REQ-005 — The system MUST validate OTAP environment values and enforce unidirectional promotion order (retroactive annotation) diff --git a/openspec/changes/actor-forwarded-listener-jobs/design.md b/openspec/changes/actor-forwarded-listener-jobs/design.md new file mode 100644 index 0000000000..727e272dbe --- /dev/null +++ b/openspec/changes/actor-forwarded-listener-jobs/design.md @@ -0,0 +1,150 @@ +# Design — actor-forwarded-listener-jobs + +Precedent: ADR-009 Rule 5 ("Outbound I/O is asynchronous to the write" — +webhook delivery already runs first-attempt-in-job). This change extends the +same principle to session-entangled listeners by making the missing piece — +the acting context — an explicit, forwarded parameter instead of ambient +session state. + +## D1. What "session-entangled" actually meant, per listener + +The `optimize-write-path-performance` D5 audit blocked three deferrals on +`IUserSession`. Re-audit at HEAD confirms the exact entanglement points: + +- `TranslationProjectionListener` → `TranslationProjectionService::project()` + line ~123: `$translator = $this->userSession->getUser()?->getUID()` — + attribution only. Everything else in the projection is a function of the + object + schema + register. +- `AnnotationNotificationListener` → `AnnotationNotificationDispatcher`: + the dispatcher never reads `IUserSession` directly, but + `resolveRelationDeeplink()` re-resolves relation targets through + `ObjectService::find()` **with RBAC enabled** — deliberately, so a deeplink + is only built for an object the current context may read. In a session-less + job that read fails closed (Anonymous), silently dropping deeplinks. +- `AggregationThresholdListener`: the aggregation itself already runs + `bypassRbac: true` (system reaction to a write); its entanglement is only + transitive, through the same dispatcher. + +Conclusion: forwarding the acting user and re-establishing it in the job +resolves all three. No listener needs `SystemOperationContext` elevation — +running as the real actor is strictly more faithful and strictly less +privileged. + +## D2. The contract + +**Capture (request, dispatch time).** `ListenerDeferralService` captures once +per request: `userId` from `IUserSession`, `organisationUuid` from +`OrganisationService::getActiveOrganisation()` (fail-soft null — capture must +never break a save). Both may legitimately be null (occ/cron-originated +writes had no session inline either; the job then simply runs without +impersonation, which is behaviour-identical). + +**Payload.** Job arguments go through `IJobList::add(class, arguments)` and +are JSON-serialized into `oc_jobs.argument`. Entries carry ids only — uuid, +register, schema, version — plus the minimal per-entry payload the listener +semantically requires and that cannot be re-fetched later: +- translation: nothing (projection is a pure function of current state); +- threshold: nothing (aggregation recomputed fresh); +- annotation `transition`: `{action, from, to}` (event-only data); +- annotation `updated`: the pre-update object data snapshot (`oldData`). The + OLD state is gone from the primary row the moment the job runs, and + field-change / calculatedChange conditions need it. New data is NOT + snapshotted — the job re-fetches current state. + +**Chunking.** One job per ≤ chunk-size entries per (job class): the service +buffers appends, flushes a full chunk immediately, and flushes the remainder +via `register_shutdown_function` (runs after the response; DB still +available; precedent `SearchQueryHandler`). Bulk saves dispatch per-object +events, so without buffering a 1 000-object import would enqueue 1 000 jobs; +with it: ⌈1000/chunk⌉ for translation, and — because threshold entries carry +a (register, schema) dedupe key — exactly 1 threshold job per touched schema. + +**Re-establishment (cron, run time).** `ActorForwardedJob::run()`: + +``` +previous = userSession->getUser() +user = userId !== null ? userManager->get(userId) : null +if (userId !== null && user === null): log + return // never misattribute +if (user !== null): userSession->setUser(user) +try: + warn-if-drifted(capturedOrgUuid vs current active org) + static::runDeferred(context) // subclass work + // (QueuedJob::execute() is final in OCP) +finally: + userSession->setUser(previous) // ALWAYS restore +``` + +Cleanup guarantees: the `finally` restore runs on any exception, so the cron +worker can never carry one job's identity into the next job +(`ScheduledReportService::runOne()` precedent). `OrganisationService`'s +session/static caches are keyed per userId, so no cross-user cache bleed. + +**Organisation context.** The active organisation re-derives at run time from +the restored user's persistent user-config (`fetchActiveOrganisationFromDatabase`) +— the same source of truth the request's session cache was primed from. It is +NOT force-restored to the captured value: `setActiveOrganisation()` would +mutate the user's persistent preference, and resurrecting a since-switched +organisation would mean running under authority the user no longer exercises. +Captured `organisationUuid` is used for drift *detection* only (logged); the +job proceeds under the user's CURRENT authority. Deliberate security posture: +re-establish identity, never re-establish stale authority. + +## D3. Idempotency / delivery semantics + +NC `QueuedJob`s are at-least-once (a worker dying mid-run leaves the row for +the next cron pass). Every job is therefore a reconciliation against current +state: + +- Re-fetch by `(uuid, register, schema)` via `ObjectService::find(_rbac: + false, _multitenancy: false, _render: false)` — register+schema avoids the + bare-UUID cross-table scan; RBAC false because the inline listener received + the entity directly (the actor context is needed for attribution and for + the dispatcher's own RBAC-scoped deeplink reads, not for the re-fetch). +- Object gone or soft-deleted (`getDeleted()` non-empty) → skip that entry + (stale no-op). This also closes the update-then-delete race: a projection + job that lands after the delete's inline purge must not resurrect sidecar + rows. +- Translation projection reconciles a desired set (upsert + prune) — running + twice, or running against newer state than the event, converges. +- Threshold evaluation recomputes the aggregation fresh; rising-edge dedup + lives in the distributed state cache exactly as inline. +- Annotation dispatch re-reads current data for `_newData`; duplicate + delivery on retry is suppressed by the dispatcher's dispatch-log dedup. + If the object changed again before the job ran, conditions evaluate + old-snapshot → current (the later change's own job covers the rest); + documented as at-least-once with converging effects. + +**Ordering vs the DB transaction:** verified — neither `SaveObject`, nor +`SaveObjects`, nor the `MagicMapper` dispatch sites wrap event dispatch in an +explicit transaction (`beginTransaction` absent from the save path). Events +fire after rows are persisted, so an enqueued job always references committed +state; there is no torn-read window to protect against. Should a transaction +ever be introduced around the save, the enqueue (an INSERT on the same +connection) would commit or roll back atomically with it — the outbox +property holds in both worlds. + +## D4. Per-listener verdicts (this change) + +| Listener | Deferred | How / why not | +|---|---|---| +| TranslationProjectionListener (C/U/Transitioned) | ✅ `TranslationProjectionJob` | Heavy reconciliation off the write path; translator attribution correct via forwarded actor. Inline gate: schema declares ≥1 `translatable` property. | +| TranslationProjectionListener (Deleted → `purge()`) | ❌ stays inline | Bounded sidecar DELETE (cheap, not worth a job) AND the entity is not re-fetchable post-delete; inline purge also guarantees no deferred `project()` can outlive the object unnoticed (jobs skip deleted objects). | +| AnnotationNotificationListener (created/updated/transition/calculatedChange) | ✅ `AnnotationNotificationDispatchJob` | Rule evaluation + outbound HTTP/mail off the write path (ADR-009 Rule 5). Deeplink RBAC reads now run under the forwarded actor. Inline gate: schema declares `x-openregister-notifications`. Old-data snapshot travels in the entry. | +| AggregationThresholdListener (C/U/Transitioned) | ✅ `AggregationThresholdJob` | Aggregation queries off the write path; (register, schema) dedupe collapses bulk saves to one evaluation. Inline gate: schema declares a threshold-typed notification. | +| AggregationThresholdListener (Deleted) | ❌ stays inline | Hard-deleted objects cannot be re-fetched by the job, and the dispatcher needs an object; deferring would silently drop delete-driven crossings (`lt`/`lte` rules). Evaluation logic is shared (`ThresholdEvaluationService`) so the inline path costs no duplication. | +| Everything else in D5's table (NotifyPush, Realtime, cache invalidation, Activity, pre-save `-ing` listeners, SourceRecordChange, ObjectCleanup, Hook, FlowAction, Metrics, GraphQL, Webhook/Action/TextExtraction) | unchanged | Cheap, latency-sensitive, already-deferred, or out of scope per the brief (pre-save listeners can never defer; SourceRecordChange/ObjectCleanup remain session-entangled beyond actor identity — RBAC-scoped recompute reads and per-user calendar/addressbook resolution need their own analysis). | + +## D5. Failure modes + +- Capture fails (org lookup throws) → context captured with null org; save + unaffected. +- Enqueue fails (`IJobList` throws) → logged error, side effect lost for that + chunk. Same blast radius as today's swallowed listener exceptions; the + kill switch restores inline behaviour if an instance's job system is + unhealthy. +- Captured user deleted before run → job logs + skips (never runs as wrong + identity; matches "user gone" semantics elsewhere, e.g. ScheduledReport). +- Cron disabled/stalled → effects delayed indefinitely; mitigations: the + `listenerDeferral=inline` kill switch and standard NC cron monitoring. +- Job crash mid-chunk → whole chunk re-runs next pass (at-least-once); + per-entry effects idempotent per D3. diff --git a/openspec/changes/actor-forwarded-listener-jobs/proposal.md b/openspec/changes/actor-forwarded-listener-jobs/proposal.md new file mode 100644 index 0000000000..04b73e792b --- /dev/null +++ b/openspec/changes/actor-forwarded-listener-jobs/proposal.md @@ -0,0 +1,105 @@ +--- +kind: code +--- + +## Why + +The `optimize-write-path-performance` listener audit (design.md D5) classified +all 16 post-save object-event listeners and concluded that **zero** could move +to background jobs: every heavy candidate resolves the acting user from the +ambient `IUserSession` — translator attribution in +`TranslationProjectionListener`, RBAC-scoped deeplink resolution inside the +notification dispatcher used by `AnnotationNotificationListener` and +`AggregationThresholdListener`. A Nextcloud background job runs without a +session, so naive deferral would misattribute writes or silently change RBAC +scope. The audit explicitly deferred the fix to a follow-up "actor-forwarding +deferral contract" (openregister#408). This change builds that contract and +uses it to take the three blocked listeners off the synchronous write path. + +Cost today, per object write: a translation-projection reconciliation +(sidecar SELECT + up to P×L upserts), a full notification-rule evaluation +(which can perform synchronous outbound HTTP and mail per ADR-009 Rule 5 +violation), and — on schemas with threshold triggers — aggregation queries. +On bulk saves this multiplies per object. + +## What Changes + +- **Actor-forwarding deferral contract** (new `lib/Service/Deferral/`): + - `DeferredListenerContext` — serializable value object carrying the acting + context captured at dispatch time: `userId`, `organisationUuid` (active + organisation at capture, for drift detection), and per-object entries + (uuid, register, schema, version, per-entry payload). Round-trips through + the JSON-serialized `oc_jobs.argument` column. + - `ListenerDeferralService` — captures the actor once per request, buffers + per-job-class entries, and enqueues **chunk-level** jobs (one job per ≤ + chunk-size entries, not per object; flush at chunk boundary and at request + shutdown via `register_shutdown_function`, precedent: + `SearchQueryHandler::flushSearchTrails()`). Supports per-entry dedupe keys + so N writes to one schema coalesce to one threshold evaluation. Honours an + `openregister/listenerDeferral` app-config kill switch (`background` + default, `inline` restores pre-change synchronous behaviour). +- **`ActorForwardedJob`** (new `lib/BackgroundJob/ActorForwardedJob.php`, + abstract, extends `\OCP\BackgroundJob\QueuedJob`): re-establishes the + captured user via `IUserManager::get()` + `IUserSession::setUser()` before + running the subclass work, and **always restores the previous session user + in a `finally` block** so a cron process never leaks identity across jobs + (precedent: `ScheduledReportService::runOne()`). If the captured user no + longer resolves, the job logs and no-ops — it never runs under a wrong or + system identity. Organisation context re-derives from the restored user's + persistent config (the same source the request session cache was primed + from); a drift between captured and current active organisation is logged + and the job proceeds under the user's CURRENT authority (never resurrects + stale authority). +- **Three listeners deferred** (each keeps a cheap inline schema-config gate + so schemas without the feature enqueue nothing, and keeps its full inline + path for the kill switch): + - `TranslationProjectionListener`: created/updated/transitioned projection + moves to `TranslationProjectionJob` (chunked uuids; translator attribution + now correct in the job because the actor is forwarded). Delete-time + `purge()` stays inline — it is a cheap bounded sidecar DELETE and the + object row is not re-fetchable after the delete. + - `AnnotationNotificationListener`: all dispatches move to + `AnnotationNotificationDispatchJob`. Update events carry the pre-update + data snapshot in the entry payload (it cannot be re-fetched later); new + data is re-fetched at run time so the job always evaluates against + current state. + - `AggregationThresholdListener`: evaluation logic is extracted verbatim + into `ThresholdEvaluationService` (shared by the inline and deferred + paths); created/updated/transitioned evaluations move to + `AggregationThresholdJob` with entries deduped per (register, schema). + Delete events stay inline: a hard-deleted object cannot be re-fetched by + the job, and delete-driven threshold crossings (e.g. `lt` operators) + would be silently lost. +- **Delivery semantics**: at-least-once. The object-event dispatch sites + (`SaveObjects::emitChunkSideEffects()`, `MagicMapper`) run after + persistence with no wrapping DB transaction, so an enqueued job always + refers to committed state. Jobs are idempotent: they re-fetch the object by + (uuid, register, schema), no-op when it is gone or soft-deleted, and + reconcile against CURRENT state (projection is a desired-set reconciliation; + threshold state has rising-edge dedup in the distributed cache; the + notification dispatcher has dispatch-log dedup). + +## Capabilities + +### event-driven-architecture (delta) +Adds the actor-forwarded deferral contract: capture-at-dispatch, +re-establish-in-job, guaranteed restore, chunk-level enqueueing, stale no-op +idempotency, and the classification of which listeners run deferred vs inline. + +## Impact + +- Affected code: `lib/Service/Deferral/` (new), `lib/BackgroundJob/ + ActorForwardedJob.php` + 3 concrete jobs (new), + `lib/Service/Aggregation/ThresholdEvaluationService.php` (new, extracted), + `lib/Listener/TranslationProjectionListener.php`, + `lib/Listener/AnnotationNotificationListener.php`, + `lib/Listener/AggregationThresholdListener.php`. +- Request-path effect per write on a schema using these features: projection + reconciliation, notification-rule evaluation (incl. outbound HTTP/mail) and + aggregation queries leave the synchronous path; what remains inline is one + request-cached `SchemaMapper::find()` + a config-array scan + a buffered + array append (one `oc_jobs` INSERT per chunk). +- Behavioural: side effects become eventually consistent (next cron run). + Realtime/cache/activity listeners intentionally stay inline (unchanged). + Admins can restore synchronous behaviour with + `occ config:app:set openregister listenerDeferral --value inline`. diff --git a/openspec/changes/actor-forwarded-listener-jobs/specs/event-driven-architecture/spec.md b/openspec/changes/actor-forwarded-listener-jobs/specs/event-driven-architecture/spec.md new file mode 100644 index 0000000000..13ff11c7ed --- /dev/null +++ b/openspec/changes/actor-forwarded-listener-jobs/specs/event-driven-architecture/spec.md @@ -0,0 +1,112 @@ +## ADDED Requirements + +### Requirement: Deferred listeners MUST forward the acting context to their background jobs + +Heavy post-save object-event listeners moved off the write path MUST capture +the acting context at dispatch time (session user id +and active organisation uuid, both nullable) into a serializable +`DeferredListenerContext`, and their background jobs MUST extend +`ActorForwardedJob`, which re-establishes the captured user via +`IUserManager::get()` + `IUserSession::setUser()` before the deferred work +runs and restores the previous session user in a `finally` block — including +when the deferred work throws — so a cron worker never leaks one job's +identity into the next. Organisation context re-derives from the restored +user's persistent configuration; a captured-vs-current drift is logged and +the job proceeds under the user's current authority. + +#### Scenario: Job runs the deferred work as the captured user and restores the session + +- **GIVEN** an object save dispatched by user `alice` deferred listener work into a job +- **WHEN** the job runs in a session-less cron worker +- **THEN** the job resolves `alice` through `IUserManager` and sets her on `IUserSession` before executing +- **AND** the deferred work observes `alice` as the session user (e.g. translator attribution records `alice`) +- **AND** after execution the session user is restored to its pre-job value (null in cron) + +#### Scenario: Session user is restored even when the deferred work throws + +- **GIVEN** a job whose deferred work throws midway +- **WHEN** the job runs +- **THEN** the previous session user is restored in a `finally` block before the exception propagates + +#### Scenario: A job for a deleted or unresolvable user never runs under a wrong identity + +- **GIVEN** a job captured for a user that no longer exists +- **WHEN** the job runs +- **THEN** the deferred work is skipped with a log entry and no impersonation occurs + +#### Scenario: A context captured without a session runs without impersonation + +- **GIVEN** an object written from occ/cron with no session user +- **WHEN** the deferral captures a null userId and the job later runs +- **THEN** the deferred work executes without calling `setUser()` — identical to the inline session-less behaviour + +### Requirement: Deferred listener jobs MUST be chunk-level, idempotent, and stale-safe + +Listeners deferring through `ListenerDeferralService` MUST enqueue chunk-level +jobs (buffered entries flushed per chunk and at request shutdown), never one +job per object of a bulk save. Jobs MUST treat delivery as at-least-once: +re-fetch each entry's object by (uuid, register, schema), skip entries whose +object is gone or soft-deleted, and reconcile against current state so a +re-run converges. Entries MAY declare a dedupe key so writes that share an +evaluation target (e.g. threshold triggers per schema) coalesce into one +entry. + +#### Scenario: Bulk save enqueues chunked jobs, not per-object jobs + +- **GIVEN** a bulk save of 250 objects on a schema with a deferred listener and a chunk size of 100 +- **WHEN** the request completes +- **THEN** at most ⌈250/100⌉ = 3 jobs are enqueued for that listener, each carrying an entry array + +#### Scenario: A job entry whose object was deleted no-ops + +- **GIVEN** an enqueued projection entry for object `X` +- **AND** `X` is deleted before cron runs +- **WHEN** the job processes the entry +- **THEN** the entry is skipped (no sidecar rows are resurrected, no notification is dispatched) + +#### Scenario: Threshold evaluations coalesce per schema + +- **GIVEN** 50 objects of schema `S` saved in one request, where `S` declares a threshold notification +- **WHEN** the deferral buffers the entries with the (register, schema) dedupe key +- **THEN** exactly one threshold evaluation entry for `S` is enqueued + +#### Scenario: The kill switch restores inline execution + +- **GIVEN** app config `openregister/listenerDeferral` set to `inline` +- **WHEN** an object event fires on a schema using a deferred listener's feature +- **THEN** the listener performs its work synchronously as before this change and enqueues nothing + +### Requirement: Translation projection, annotation notifications and threshold evaluation MUST run via actor-forwarded jobs + +Three listeners MUST perform their heavy work in actor-forwarded jobs: +`TranslationProjectionListener` (created/updated/transitioned), +`AnnotationNotificationListener` (all triggers) and +`AggregationThresholdListener` (created/updated/transitioned), keeping only a +request-cached schema-config gate inline so schemas without the respective +feature enqueue nothing. Delete-time work stays inline for +`TranslationProjectionListener` (sidecar purge) and +`AggregationThresholdListener` (the entity is not re-fetchable post-delete). +The deferred job MUST produce the same effect the inline listener produced: +projected translation rows record the acting user as translator; update +notifications evaluate field-change and calculatedChange conditions against +the pre-update snapshot captured at dispatch time and the current object +data; threshold crossings dispatch on the rising edge only. + +#### Scenario: Deferred projection matches the inline effect including translator attribution + +- **GIVEN** user `alice` updates an object with translatable properties +- **WHEN** the enqueued `TranslationProjectionJob` runs in cron +- **THEN** the translations sidecar contains the same rows an inline `project()` would have written, with `alice` recorded as translator + +#### Scenario: Deferred update notification keeps old/new condition context + +- **GIVEN** an update event with a real pre-update state on a schema with notification rules +- **WHEN** the enqueued `AnnotationNotificationDispatchJob` runs +- **THEN** the dispatcher receives trigger `updated` with `_oldData` from the captured snapshot and `_newData` from the re-fetched current object +- **AND** a `calculatedChange` dispatch follows with the same context, mirroring the inline listener + +#### Scenario: Deferred threshold evaluation dispatches on the rising edge + +- **GIVEN** a schema with a threshold notification whose aggregation value crosses the boundary +- **WHEN** the enqueued `AggregationThresholdJob` runs +- **THEN** the notification is dispatched once and the state cache records `above`, exactly as the inline evaluation did diff --git a/openspec/changes/actor-forwarded-listener-jobs/tasks.md b/openspec/changes/actor-forwarded-listener-jobs/tasks.md new file mode 100644 index 0000000000..4f46ebf9cd --- /dev/null +++ b/openspec/changes/actor-forwarded-listener-jobs/tasks.md @@ -0,0 +1,30 @@ +# Tasks — actor-forwarded-listener-jobs + +## 1. Contract + +- [x] 1.1 `DeferredListenerContext` value object (userId, organisationUuid, entries; `toJobArguments()`/`fromJobArguments()` round-trip) — `lib/Service/Deferral/DeferredListenerContext.php`. +- [x] 1.2 `ListenerDeferralService` — actor capture (fail-soft), per-job-class entry buffers with chunk-size flush + shutdown flush, per-entry dedupe keys, `listenerDeferral` kill switch — `lib/Service/Deferral/ListenerDeferralService.php`. +- [x] 1.3 `ActorForwardedJob` abstract QueuedJob — resolve captured user, `setUser()`, org-drift log, `finally` restore, skip-on-unresolvable-user — `lib/BackgroundJob/ActorForwardedJob.php`. +- [x] 1.4 `DeferredEntryObjectResolver` — stale-safe entry re-fetch (scoped raw lookup; gone/soft-deleted → null) shared by all three jobs — `lib/Service/Deferral/DeferredEntryObjectResolver.php`. + +## 2. Deferred listeners + +- [x] 2.1 `TranslationProjectionJob` (re-fetch, skip gone/deleted, `project()` current state) + slim `TranslationProjectionListener` to gate + enqueue for C/U/Transitioned; delete purge stays inline; inline fallback for kill switch. +- [x] 2.2 `AnnotationNotificationDispatchJob` (re-fetch, `_newData` = current, `_oldData` = captured snapshot, `updated`+`calculatedChange` parity, `transition` context) + slim `AnnotationNotificationListener` to gate + enqueue; inline fallback. +- [x] 2.3 Extract `ThresholdEvaluationService` from `AggregationThresholdListener` (evaluate/compare/state-cache verbatim); `AggregationThresholdJob` for C/U/Transitioned with (register, schema) dedupe; deletes evaluate inline via the service; inline fallback. + +## 3. Tests + +- [x] 3.1 `tests/Unit/Service/Deferral/DeferredListenerContextTest.php` — arguments round-trip incl. null actor and payload snapshots. +- [x] 3.2 `tests/Unit/Service/Deferral/ListenerDeferralServiceTest.php` — chunk flush, shutdown-remainder flush, dedupe coalescing, kill switch, fail-soft capture. +- [x] 3.3 `tests/Unit/BackgroundJob/ActorForwardedJobTest.php` — impersonate + restore round-trip, finally-restore on exception, unresolvable-user skip, null-actor no-impersonation. +- [x] 3.4 `tests/Unit/BackgroundJob/TranslationProjectionJobTest.php` — projects re-fetched object under forwarded actor; deleted/gone entry no-ops. +- [x] 3.5 `tests/Unit/BackgroundJob/AnnotationNotificationDispatchJobTest.php` — created/transition/updated(+calculatedChange) dispatch parity with the inline listener; stale no-op. +- [x] 3.6 `tests/Unit/BackgroundJob/AggregationThresholdJobTest.php` + `tests/Unit/Service/Aggregation/ThresholdEvaluationServiceTest.php` — rising-edge semantics preserved through the job; deleted entry no-op. +- [x] 3.7 Listener tests: `TranslationProjectionListenerTest` (new), `AnnotationNotificationListenerTest` (new), `AggregationThresholdListenerTest` (updated) — request path enqueues instead of running heavy work; gates suppress enqueue; delete paths stay inline; kill switch runs inline. + +## 4. Verification + +- [x] 4.1 phpcs/phpmd/phpstan/psalm clean on touched files. +- [x] 4.2 Full `tests/Unit/Listener` + `tests/Unit/BackgroundJob` suites green. +- [x] 4.3 `openspec validate actor-forwarded-listener-jobs --strict` passes. diff --git a/openspec/changes/add-archival-annotation-support/tasks.md b/openspec/changes/add-archival-annotation-support/tasks.md deleted file mode 100644 index 939f2a8d9f..0000000000 --- a/openspec/changes/add-archival-annotation-support/tasks.md +++ /dev/null @@ -1,67 +0,0 @@ -# Tasks — Honor `x-openregister-archival` - -## Vocabulary + fold-up - -- [x] 1.1 Add `'x-openregister-archival'` and `'x-openregister-seed'` to `Schema::ANNOTATION_VOCABULARY` (`lib/Db/Schema.php:1853`). -- [x] 1.2 Cover the round-trip in a unit test: a schema with `x-openregister-archival` at top-level survives `cleanObject()` → `validateConfigurationArray()` and lands under `configuration['x-openregister-archival']`. Re-import does not emit the "Dropped unknown x-openregister-* key(s)" warning. - -## Validation - -- [x] 2.1 Create `lib/Service/Archival/ArchivalAnnotationValidator.php` mirroring `LifecycleAnnotationValidator`: - - `validate(array $schema): array` returns `[ {code, message}, ... ]`. - - Required: `retention.default` parseable by `new \DateInterval($v)`. - - Required: each `retention.rules[i].condition` is a non-empty string. - - Required: each `retention.rules[i].retention` parseable by `new \DateInterval($v)`. - - Optional: `retention.rules[i].reason` (string). - - Reject unknown keys under `retention.` so typos fail loudly. -- [x] 2.2 Wire the validator into `SchemaMapper::insert()` and `SchemaMapper::update()` next to the existing lifecycle / aggregations hooks, with the same "errors aggregated into one Exception" shape. -- [x] 2.3 Unit tests for the validator: happy path, missing default, malformed default, malformed rule retention, non-string condition, unknown key under `retention.`. - -## Immutability - -- [x] 3.1 New `lib/Exception/ArchivalImmutableException.php` mirroring `AppendOnlyException`: HTTP 403, structured body `{error: SCHEMA_ARCHIVAL_IMMUTABLE, message, schema, operation, hint}`. -- [x] 3.2 In `ObjectService::deleteObject()`, after the existing append-only check, add an archival check. Schema declares archival ⇒ throw `ArchivalImmutableException` unless an internal `$_retentionSweep` flag is set. -- [x] 3.3 Add `$_retentionSweep` private signature flag to `deleteObject()` alongside `$_rbac` / `$_multitenancy`. Only the cron task sets it true. -- [x] 3.4 Unit test: deleteObject on an archival schema as admin → 403 with structured body. -- [x] 3.5 Unit test: deleteObject with `$_retentionSweep = true` skips the gate (cron path). - -## Retention evaluator - -- [x] 4.1 New `lib/Service/Archival/RetentionConditionEvaluator.php`: - - `evaluate(string $condition, array $row): bool`. - - Grammar: ` ` with ops `<`, `<=`, `==`, `!=`, `>=`, `>`; literals are int / float / `"..."` / `'...'` / `true` / `false` / `null`. - - Unknown field on the row ⇒ false (no rule match). - - Malformed condition ⇒ log + throw `InvalidArgumentException` (caller catches and logs). -- [x] 4.2 New `lib/Service/Archival/RetentionEvaluator.php`: - - `evaluate(array $annotation, array $row, \DateTimeInterface $createdAt): array { effectiveRetention, matchedRule, expiresAt }`. - - First-match-wins across `retention.rules[]`; fall back to `retention.default`. - - `expiresAt = createdAt + DateInterval(effectiveRetention)`. -- [x] 4.3 Unit tests: rules in order (first match wins), fallback to default, both operators, no-match returns default, malformed condition logged + skipped not crashing. - -## Retention sweep cron - -- [x] 5.1 New `lib/Cron/ArchivalRetentionTask.php` (`TimedJob`, 1-hour interval, `TIME_INSENSITIVE`). -- [x] 5.2 Iterate every `(register, schema)` whose schema's `configuration['x-openregister-archival']` is set; resolve the magic table via `MagicTableHandler::getTableNameForRegisterSchema()`. -- [x] 5.3 Native SQL per schema (Postgres `SELECT _uuid, _created, … FROM
`); for each row run `RetentionEvaluator::evaluate()`; if `expiresAt < NOW()` schedule the row for deletion. -- [x] 5.4 Delete via `ObjectService::deleteObject(... _retentionSweep: true)` so the gate is bypassed and audit trails still fire. -- [x] 5.5 Emit summary log per schema: `{schemaSlug, scanned, expired, deleted}`. -- [x] 5.6 Register the job in `appinfo/info.xml` ``. -- [ ] 5.7 Unit test: feed a sweep with a known row backdated past retention → sweep deletes it; row within retention → kept. - -## Surface in UI - -- [x] 6.1 In `ObjectEntity::jsonSerialize()` (or a thin renderer above it), when the schema declares archival, attach `_retention: { effectiveRetention, matchedRule, expiresAt }` computed by `RetentionEvaluator`. -- [x] 6.2 Unit test: object read returns `_retention` block when archival is set; absent when not. - -## Quality gates - -- [x] 7.1 `composer check:strict` (PHPCS + PHPMD + Psalm + PHPStan) green. -- [x] 7.2 `composer test:unit` green. -- [x] 7.3 `openspec validate add-archival-annotation-support --strict` green. - -## Live verification - -- [ ] 8.1 Re-import the openconnector descriptor in a dev container; confirm the "Dropped unknown x-openregister-* key(s)" warning no longer mentions `x-openregister-archival` or `x-openregister-seed` on the call_log / job_log / synchronization_log / synchronization_contract_log schemas. -- [ ] 8.2 `DELETE /api/objects/openconnector/call_log/` as admin → 403 with `SCHEMA_ARCHIVAL_IMMUTABLE`. -- [ ] 8.3 Backdate a call_log row's `_created` past `P30D` and trigger the cron manually (`occ background-job:execute `) → row disappears; cron log says `{scanned: N, expired: 1, deleted: 1}`. - diff --git a/openspec/changes/add-features-roadmap-menu/design.md b/openspec/changes/add-features-roadmap-menu/design.md deleted file mode 100644 index 5af6878ed2..0000000000 --- a/openspec/changes/add-features-roadmap-menu/design.md +++ /dev/null @@ -1,195 +0,0 @@ -# Design: add-features-roadmap-menu - -## Context - -Two sources of truth for every Conduction app are invisible to end-users: -the `openspec/specs/*/spec.md` tree (what's built — YAML frontmatter -`status: implemented | reviewed | redirect`, H1 = capability title, -`## Purpose` section as summary) and the app's GitHub issue tracker -(roadmap — reactions act as informal voting). This change binds both into -one menu entry, one route with two tabs, plus a submission path that lets -authenticated users file feature requests straight into the repo. - -`@conduction/nextcloud-vue` is the existing shared Vue library (currently -exporting only settings primitives — `CnSettingsCard`, `CnSettingsSection`) -so it's the natural home for the new component family. The build-time -spec-to-manifest tool is its own package (`@conduction/openspec-manifest`) -because it must be usable from any app's build without dragging in the full -UI library. A fourth package (`@conduction/docusaurus-features`) renders -the same manifest on each app's public Docusaurus site. - -The backend (read proxy + submission endpoint) lives in OpenRegister and -ships in the paired `add-github-issue-proxy` change. This change depends on -it. - -## Goals - -- One menu entry → one dedicated route with two tabs in every Conduction app. -- Features surfaced from OpenSpec specs at build time (no runtime filesystem - walk, no backend read path for feature metadata). Same `docs/features.json` - powers the in-app view AND the Docusaurus public page. -- Roadmap items fetched live via OR's `github-issue-proxy` with full - markdown rendering (XSS-sanitised) and a blocklist that hides - pipeline/workflow labels. -- Submission via the same proxy — preferring the user's own PAT for - authorship, falling back to the server PAT with attribution. -- Widgets/pages declaratively opt into a `specRef` (ADR-008 capability-slug - convention) so the "Suggest feature" action appears contextually wherever - the user is working. -- Pilot integration on OpenRegister itself in this change. - -## Non-Goals - -- The OR backend — separate change. -- GitHub Discussions integration. -- Fleet rollout to other Conduction apps (separate ADR-019). -- Webpack-plugin wrapper for the manifest generator. - -## Decisions - -### D3. Features source: build-time manifest from `openspec/specs/` - -Runtime endpoint reading specs from the installed app's filesystem rejected -(couples backend to deployment layout). Hand-authored JSON curated per -release rejected (guaranteed drift). **Chosen:** `@conduction/openspec-manifest` -CLI runs as `prebuild`, writes `docs/features.json`, both webpack/vite and -Docusaurus consume the same file. - -### D4. Feature filter: `status: implemented` OR `status: reviewed` - -`implemented` only rejected (`reviewed` is strictly more finished). -Include `redirect` rejected (re-homing markers, not features). Include -drafts / planned rejected (Features tab must show what actually ships). -**Chosen:** `status ∈ {implemented, reviewed}`. - -### D5. Docs link default - -Require every spec to add a `docsUrl:` frontmatter key rejected -(high-friction migration). No link at all rejected. **Chosen:** -auto-compute against the repo's default branch resolved from -`origin/HEAD` (NOT the currently checked-out branch). Specs can opt in -to an override via a `docsUrl:` frontmatter key (validated as `https://` -URL with non-empty hostname; rejection → use computed default). - -### D6. Manifest generator as a standalone npm package - -Ship as dev-dependency inside `@conduction/nextcloud-vue` rejected -(couples build tooling to UI library; not every consumer has OpenSpec). -Inline script per app rejected (DRY violation across ten-plus apps). -**Chosen:** new `@conduction/openspec-manifest` package with a `build` -CLI. - -### D7. Component home: `@conduction/nextcloud-vue` - -The new `CnFeaturesAndRoadmapLink`, `FeaturesAndRoadmapView`, -`SuggestFeatureModal`, `useSpecRef`, `useSuggestFeatureAction`, -`SAFE_MARKDOWN_DOMPURIFY_CONFIG`, and `ROADMAP_LABEL_BLOCKLIST` exports -establish a new component category: "cross-app surfaces." - -### D8. i18n - -Component ships with Dutch (nl) and English (en) strings for all chrome -(route title, tab labels, modal fields, empty states, toasts, errors). -Feature titles + summaries pass through unchanged. - -### D11. Pilot scope: OpenRegister only - -Ship adoption into every Conduction app in this single change rejected -(huge blast radius for a first-time shared component). **Chosen:** wire -only OpenRegister; fleet rollout deferred to hydra ADR-019. - -### D12. UI pattern: full route, not side panel - -Side panel (`NcAppSidebar`) considered — too cramped for browsing + -submitting + awkward on mobile. Modal rejected — too restrictive for -two-tab browsing. **Chosen:** dedicated Vue route (e.g. -`/features-roadmap`). The `NcAppNavigationSettings` entry is an -`` that navigates to this route. - -### D13. Submission destination: GitHub Issues (not Discussions) - -User explicitly chose Issues. Direct roadmap visibility: a new submission -shows up immediately in the same Roadmap tab where it was filed. - -### D14 (UI half). Authorship UX - -The "Suggest feature" modal POSTs `{repo, title, body, specRef?}` to -OR's `POST /api/github/issues` (with the Nextcloud CSRF token). Authorship -fallback (user-PAT preferred, server-PAT fallback with sanitised -attribution prefix) is OR's responsibility — see `add-github-issue-proxy`. - -### D15. Markdown rendering: `marked` + `DOMPurify` - -Plain-text rendering rejected (raw `*`, `#`, code fences look broken). -Iframe-embed GitHub's render rejected (no style control, slow, breaks -dark mode). First-paragraph-only rejected by the user. **Chosen:** -`marked` to parse GHFM, `DOMPurify` with a strict allowlist (no -`"` -- **THEN** the rendered DOM SHALL NOT contain any `"` +- **THEN** the rendered DOM SHALL NOT contain any `` +- **WHEN** `SecurityService::validateLoginCredentials()` processes the input +- **THEN** the username MUST be sanitized via `htmlspecialchars()` with ENT_QUOTES +- **AND** null bytes MUST be stripped +- **AND** JavaScript event handlers (`onload=`, `onerror=`, etc.) MUST be removed +- **AND** the sanitized username MUST be truncated to 320 characters maximum + +#### Scenario: Reject credentials with invalid characters +- **GIVEN** a username containing `<>"\'/\\` characters +- **WHEN** `SecurityService::validateLoginCredentials()` processes the input +- **THEN** the validation MUST return `{ valid: false, error: "Username contains invalid characters" }` + +#### Scenario: Prevent excessively long passwords +- **GIVEN** a login attempt with a password exceeding 1000 characters +- **WHEN** `SecurityService::validateLoginCredentials()` processes the input +- **THEN** the validation MUST return `{ valid: false, error: "Password is too long" }` + +## Current Implementation Status +- **Fully implemented:** + - `Consumer` entity (`lib/Db/Consumer.php`) with fields: uuid, name, description, domains (CORS), ips (IP allow-list), authorizationType (none/basic/bearer/apiKey/oauth2/jwt), authorizationConfiguration (JSON with keys, algorithms, secrets), userId (mapped Nextcloud user), created, updated + - `ConsumerMapper` (`lib/Db/ConsumerMapper.php`) for CRUD operations on consumers + - `ConsumersController` (`lib/Controller/ConsumersController.php`) for API consumer management + - `AuthorizationService` (`lib/Service/AuthorizationService.php`) supporting JWT (HMAC HS256/384/512), Basic Auth, OAuth2 Bearer, and API key validation — all methods resolve to a Nextcloud user via `$this->userSession->setUser()` + - `AuthenticationService` (`lib/Service/AuthenticationService.php`) for outbound token generation (OAuth2 client_credentials, OAuth2 password, JWT signing with HS/RS/PS algorithms) + - `SecurityService` (`lib/Service/SecurityService.php`) with APCu-backed rate limiting (5 attempts / 15min window, 1hr lockout), progressive delays (2s-60s), IP and user lockout, XSS sanitization, security headers, and security event logging + - `PermissionHandler` (`lib/Service/Object/PermissionHandler.php`) for schema-level RBAC with admin bypass, owner privileges, public group, conditional matching with `$organisation` variable + - `PropertyRbacHandler` (`lib/Service/PropertyRbacHandler.php`) for property-level RBAC with `canReadProperty()`, `canUpdateProperty()`, `filterReadableProperties()`, `getUnauthorizedProperties()`, conditional rule matching, and admin/public/authenticated pseudo-groups + - `MagicRbacHandler` (`lib/Db/MagicMapper/MagicRbacHandler.php`) for SQL-level RBAC filtering with QueryBuilder integration, raw SQL for UNION queries, dynamic variable resolution ($organisation, $userId, $now), operator conditions ($eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$exists), and multi-tenancy bypass detection + - `MultiTenancyTrait` (`lib/Db/MultiTenancyTrait.php`) for organisation-level data isolation + - `ConditionMatcher` (`lib/Service/ConditionMatcher.php`) and `OperatorEvaluator` (`lib/Service/OperatorEvaluator.php`) for conditional authorization rule evaluation + - Nextcloud session auth works natively via the Nextcloud AppFramework + - Public endpoint support via `@PublicPage` annotations on ObjectsController (5 public methods) + - CORS enforcement in `AuthorizationService::corsAfterController()` with CSRF protection + - Twig authentication extensions (`lib/Twig/AuthenticationExtension.php`, `lib/Twig/AuthenticationRuntime.php`) for `oauthToken` function in mapping templates + - MCP endpoint uses Nextcloud's standard Basic Auth via the AppFramework controller pattern + +- **Not implemented:** + - Per-consumer rate limiting (configured request limits per consumer with `Retry-After` headers) + - Authentication event auditing to Nextcloud's audit log (via `OCP\Log\ILogFactory`) — currently logged via `LoggerInterface` only + - JWT token auto-generation and one-time display workflow in the consumer creation UI + - Consumer revocation with immediate token invalidation (deleting a consumer works, but active JWT sessions may not be immediately invalidated if cached) + - IP allow-list enforcement in `AuthorizationService` (Consumer stores `ips` field but enforcement is not implemented) + - CORS enforcement per Consumer's `domains` field (currently uses generic origin reflection) + - RSA/PS256 signature verification for inbound JWT tokens (only HMAC verification is implemented; `AuthorizationService::authorizeJwt()` checks HMAC_MAP only) + +- **Partial:** + - Rate limiting exists via `SecurityService` with APCu-backed counters, but is not integrated into the `AuthorizationService` flow for every authentication method + - Public schema access exists via `@PublicPage` endpoints but mixed public/private schema discovery filtering is not explicitly implemented in schema listing endpoints + - Group membership caching relies on Nextcloud's internal caching; no explicit per-request cache in OpenRegister handlers + +## Standards & References +- **OAuth 2.0 (RFC 6749)** — Authorization framework for Consumer entity auth types +- **JWT (RFC 7519)** — JSON Web Token for API consumer authentication +- **JWS (RFC 7515)** — JSON Web Signature for JWT signing/verification +- **SAML 2.0** — Via Nextcloud's `user_saml` app for enterprise SSO +- **OpenID Connect Core 1.0** — Via Nextcloud's `user_oidc` app for OIDC SSO +- **BIO (Baseline Informatiebeveiliging Overheid)** — Dutch government baseline information security requirements for authentication and access control +- **DigiD/eHerkenning** — Dutch government authentication standards (via SAML/OIDC gateway) +- **RFC 6585** — HTTP 429 Too Many Requests for rate limiting +- **OWASP Authentication Cheat Sheet** — Best practices for credential handling, session management, and brute force protection +- **Nextcloud AppFramework annotations** — `@PublicPage`, `@NoCSRFRequired`, `@NoAdminRequired`, `@CORS` +- **Nextcloud OCP interfaces** — `IUserSession`, `IUserManager`, `IGroupManager`, `IAppConfig`, `ICacheFactory`, `ISecureRandom` +- **ZGW Autorisaties API (VNG)** — Dutch government authorization patterns (see cross-reference: `rbac-scopes` spec) + +## Cross-References +- **`rbac-scopes`** — Maps Nextcloud groups to OAuth2 scopes in generated OAS; depends on the same group-based authorization model defined here +- **`rbac-zaaktype`** — Implements schema-level RBAC per zaaktype/objecttype; uses `PermissionHandler` defined here +- **`row-field-level-security`** — Extends the authorization model with row-level and field-level security; uses `MagicRbacHandler` and `PropertyRbacHandler` defined here +- **ADR: Security and Authentication** — Architecture decision record for the security model (not yet created; to be defined at `openspec/architecture/adr-007-security-and-auth.md`) + +## Specificity Assessment +- **Highly specific and largely implemented**: The core multi-auth system, RBAC hierarchy (admin > owner > group > authenticated > public), and three-level authorization (schema, property, row) are fully implemented with clear code references. +- **Well-documented Consumer entity**: The Consumer entity fields, auth types, and resolution flow are clearly specified with implementation details. +- **Code-grounded scenarios**: All scenarios reference specific methods, classes, and behaviors verified against the actual implementation. +- **Missing implementations clearly identified**: IP allow-list enforcement, per-consumer rate limiting, RSA JWT verification, and audit log integration are explicitly marked as not implemented. +- **No open design questions**: The architecture is settled — all auth methods resolve to Nextcloud users, all RBAC uses Nextcloud groups, all layers are composable. diff --git a/openspec/changes/auth-system/tasks.md b/openspec/changes/auth-system/tasks.md new file mode 100644 index 0000000000..685eb0cf34 --- /dev/null +++ b/openspec/changes/auth-system/tasks.md @@ -0,0 +1,17 @@ +# Tasks: Authentication and Authorization System + +- [ ] Implement: The system MUST support multiple authentication methods with unified identity resolution +- [ ] Implement: API consumers MUST be configurable entities that bridge external systems to Nextcloud identities +- [ ] Implement: The RBAC model MUST enforce schema-level, property-level, and row-level access control using Nextcloud groups +- [ ] Implement: The role hierarchy MUST include admin bypass, owner privileges, public access, and authenticated access +- [ ] Implement: Group-based access MUST support conditional matching with dynamic variables +- [ ] Implement: Multi-tenancy isolation MUST restrict data access to the user's active organisation +- [ ] Implement: Public endpoints MUST use Nextcloud's annotation framework and enforce mixed visibility +- [ ] Implement: The system MUST support SSO via SAML, OIDC, and LDAP through Nextcloud's identity providers +- [ ] Implement: Rate limiting MUST protect against brute force attacks and API abuse +- [ ] Implement: Authentication and security events MUST be audited +- [ ] Implement: Permission evaluation results MUST be cacheable for performance +- [ ] Implement: CORS policy MUST be enforced per Consumer and prevent CSRF +- [ ] Implement: MCP endpoint authentication MUST use Nextcloud's standard auth mechanisms +- [ ] Implement: Service-to-service authentication MUST support outbound token generation +- [ ] Implement: Input sanitization MUST prevent XSS and injection attacks diff --git a/openspec/changes/authorization-rbac-enhancement/proposal.md b/openspec/changes/authorization-rbac-enhancement/proposal.md new file mode 100644 index 0000000000..2a89cb3f0d --- /dev/null +++ b/openspec/changes/authorization-rbac-enhancement/proposal.md @@ -0,0 +1,74 @@ +# Proposal: authorization-rbac-enhancement + +## Summary + +Implement fine-grained role-based access control (RBAC) for OpenRegister, enabling per-schema, per-register, and per-object authorization with row-level security and team-based access control. This extends Nextcloud's built-in group system with OpenRegister-specific permission scopes that match the granularity required by Dutch government organisations. + +## Demand Evidence + +**Cluster: Authorization/RBAC** -- 205 tenders, 876 requirements +**Cluster: Role-based access** -- 200 tenders, 629 requirements +**Combined**: 405 tenders, 1505 requirements (highest demand across all clusters) + +### Sample Requirements from Tenders + +1. **Gemeente Berkelland**: "Autorisaties kunnen door een beheerinterface eenvoudig worden geconfigureerd. Het hele rollen- en rechtenmodel van de oplossing kan op een plek geconfigureerd worden." +2. **Gemeente Hilversum**: "A. Beschrijf waar en op welke wijze autorisatie binnen de Oplossing plaatsvindt. B. Beschrijf op welke wijze de inrichting de mogelijkheid biedt om op verschillende niveaus te autoriseren. C. Beschrijf hoe rollen en rechten worden beheerd." +3. **Gemeente Winterswijk**: "Beheer: Centraal beheer autorisaties, inrichting Active Directory. Beschrijf waar en op welke wijze autorisatie plaatsvindt, mogelijkheid om op verschillende niveaus te autoriseren, rol van Active Directory." +4. **Gemeente Berkelland**: "Gebruikers met de juiste autorisaties kunnen altijd handmatig gegevens zoals bewaartermijn en vernietigingsdatum aanpassen." +5. **Gemeente Berkelland**: "Op basis van zaaktypen en door verschillende beheerders (delegatie) is het mogelijk om configuraties, rollen en rechten te stapelen, overerven, kopieren." + +## Scope + +### In Scope + +- **Permission model**: Define CRUD+L (Create, Read, Update, Delete, List) permissions at three levels: register, schema, and individual object +- **Role definitions**: Configurable roles with named permission sets (e.g., "Viewer", "Editor", "Manager", "Archivist") that can be assigned per scope +- **Row-level security**: Filter query results based on user permissions -- users only see objects they are authorised to access +- **Team-based access**: Assign permissions to Nextcloud groups (teams), with support for hierarchical group inheritance +- **Delegation**: Allow schema/register managers to delegate permission management to sub-administrators +- **Permission inheritance**: Register-level permissions cascade to schemas unless overridden; schema-level permissions cascade to objects unless overridden +- **Field-level visibility**: Optional configuration to hide sensitive fields from users without specific roles (e.g., BSN, financial data) +- **Authorization admin UI**: Central interface for managing all roles, permissions, and assignments across registers and schemas +- **LDAP/AD group mapping**: Map external directory groups to OpenRegister roles automatically +- **Public access scopes**: Define which schemas/registers are publicly accessible (unauthenticated) vs. restricted + +### Out of Scope + +- Authentication (handled by Nextcloud's auth system) +- Single sign-on configuration (Nextcloud-level concern) +- CSV import/export (already exists) +- Multi-tenant isolation (separate change: `saas-multi-tenant`) + +## Acceptance Criteria + +1. Permissions can be assigned at register, schema, and object level with CRUD+L granularity +2. Roles are configurable with named permission sets and can be reused across scopes +3. API queries automatically filter results based on the requesting user's permissions (row-level security) +4. Nextcloud groups can be assigned roles, and group membership changes are reflected immediately +5. Permission inheritance works top-down (register -> schema -> object) with explicit override capability +6. A central admin UI shows all permission assignments and allows bulk management +7. Field-level visibility can be configured per schema to hide sensitive properties from unauthorised users +8. Delegation allows register managers to grant/revoke permissions within their scope without requiring system admin access +9. Public access can be toggled per schema/register without affecting authenticated user permissions +10. Performance: row-level security filtering adds less than 50ms overhead to typical list queries + +## Dependencies + +- OpenRegister Schema, Register, and Object entities +- Nextcloud IGroupManager and IUserManager for group/user resolution +- Nextcloud IAppConfig for permission storage (or dedicated permission table) +- Existing archived changes: `auth-system`, `rbac-scopes`, `rbac-zaaktype` -- this proposal consolidates and extends those concepts + +## Standards & Regulations + +- BIO (Baseline Informatiebeveiliging Overheid) -- section 9 (access control) +- AVG/GDPR Article 25 (data protection by design -- principle of least privilege) +- NEN-ISO 27001:2013 / 27002:2013 (access control domain) +- NORA (Nederlandse Overheid Referentie Architectuur) -- authorization principles + +## Notes + +- OpenRegister already has CSV import/export with ID support +- The archived changes `auth-system`, `rbac-scopes`, and `rbac-zaaktype` covered aspects of this; this proposal consolidates them into a single comprehensive RBAC enhancement +- This is the highest-demand capability across all analysed clusters (1505 combined requirements) and should be prioritised accordingly diff --git a/openspec/changes/batch-integrity-cascade-deletes/proposal.md b/openspec/changes/batch-integrity-cascade-deletes/proposal.md new file mode 100644 index 0000000000..5172c20ee8 --- /dev/null +++ b/openspec/changes/batch-integrity-cascade-deletes/proposal.md @@ -0,0 +1,105 @@ +--- +kind: code +--- + +# Batch referential-integrity CASCADE deletions + +## Why + +`optimize-delete-path-performance` (PR #405) batched the legacy `cascade: true` +delete path: one batched cross-table target resolution, one +`UPDATE ... SET _deleted = CASE _uuid ... WHERE _uuid IN (...)` per magic table +(`MagicMapper::softDeleteMultipleObjectEntities()`), and one multi-row audit +INSERT (`AuditTrailMapper::insertAuditTrails()` with rows built through +`buildAuditTrail()`). + +The referential-integrity CASCADE path did not get the same treatment. +`ReferentialIntegrityService::applyBatchCascadeDelete()` still resolves and +audits per object: `MagicMapper::deleteObjects()` runs a full +`findAcrossAllSources()` cross-magic-table scan **per UUID** just to regroup +targets whose register/schema the `DeletionAnalysis` already carries, and every +cascade target gets its own single-row `AuditTrailMapper::insert()`. For N +CASCADE targets that is ~N cross-table UNION scans + N audit INSERTs — inside +the delete transaction opened by `DeleteObject::executeIntegrityTransaction()`, +so concurrent writers block for the duration (issue #409). + +## What Changes + +### `ReferentialIntegrityService::applyBatchCascadeDelete()` (CASCADE targets only) + +- Resolve **all** CASCADE targets with ONE batched cross-table lookup + (`MagicMapper::findMultipleAcrossAllMagicTables(uuids, includeDeleted: false)`), + exactly like the legacy-cascade batch path. +- Soft-delete the resolved targets via + `MagicMapper::softDeleteMultipleObjectEntities()` — one + `UPDATE ... SET _deleted = CASE _uuid ... END WHERE _uuid IN (...)` per magic + table, with per-target deletion metadata + (`deletedBy`/`deletedAt`/`objectId`/`organisation`, the same attribution + shape `DeleteObject::delete()` and the legacy-cascade batch write). The + previously unused `$organisationId` parameter of `applyDeletionActions()` now + feeds the `organisation` attribution field. +- Write ONE multi-row audit INSERT via + `AuditTrailMapper::insertAuditTrails()` with rows pre-built through + `buildAuditTrail(old: $entity, new: null, action: + 'referential_integrity.cascade_delete', cascadeContext: ...)`. The cascade + context carries the exact key shape `createAuditTrail()`'s folding uses: + `triggerObject` (root UUID), `triggerSchema` (root schema slug), + `action_type` (`referential_integrity.cascade_delete`) and `property` (the + referencing property from the analysis target). One audit row per analysis + target is preserved (a target referenced through two properties still gets + two rows). +- Targets the uuid-based batch lookup cannot resolve — and every target when + the batch resolve or the batched soft-delete write fails — fall back to the + unchanged legacy per-object pipeline + (`MagicMapper::deleteObjects()` + per-target single-row audit insert), + preserving prior behaviour byte-for-byte on the fallback path (fail-soft, + like #405). + +### Preserved exactly + +- RESTRICT, SET_NULL and SET_DEFAULT handling in `applyDeletionActions()` is + untouched (per-target `findAcrossAllSources` + `update` + single audit row). +- Execution order: SET_NULL → SET_DEFAULT → CASCADE (deepest first — the + cascade target list stays reversed), unchanged recursion/analysis semantics + (`canDelete()` / `walkDeletionGraph()` untouched). +- Audit writes stay fail-soft: a failed audit build/insert logs a warning and + never aborts the cascade. +- Cache invalidation: the integrity cascade path performed no per-target cache + invalidation before and performs none now (the root object's invalidation in + `DeleteObject` is untouched). + +### Deliberate, documented behaviour deltas (batch path only) + +- Per-target `ObjectUpdatingEvent`/`ObjectUpdatedEvent` pairs are now + dispatched by `softDeleteMultipleObjectEntities()` (a hook stopping + propagation skips that target's write and it is NOT retried per-object; a + payload-modifying hook routes that target through the full-row save). The + previous `deleteObjectsByUuids()` write dispatched no events at all — the + batch path now matches the event contract PR #405 established for the + legacy cascade. +- Batch-handled audit rows are built by the shared `buildAuditTrail()` row + builder: full row shape (session/request/ip/size/object id), hash-chain + sealed via the batched `insertAuditTrails()` pass, and the cascade details + live in `changed.cascadeContext` (the shipped single-INSERT cascade-context + fold) instead of the ad-hoc + `{deletedBecause, triggerObject, triggerSchema, property}` map. Action stays + `referential_integrity.cascade_delete`. Fallback-path rows keep the old + shape unchanged. +- Batch-handled targets get `_deleted` attribution metadata + (`deletedBy`/`deletedAt`/`objectId`/`organisation`) instead of + `deleteObjectsByUuids()`'s generic + `{time, user, reason: 'Bulk soft delete...', retention}` marker — matching + the root delete and the legacy-cascade batch. + +## Impact + +- Affected code: `lib/Service/Object/ReferentialIntegrityService.php` + (`applyDeletionActions`, `applyBatchCascadeDelete` + 3 helpers; the old + per-object body survives verbatim as `applyPerObjectCascadeDelete()`). +- Affected specs: `object-lifecycle` (batched integrity-cascade requirement), + `deletion-audit-trail` (single multi-row INSERT for integrity cascade rows). +- Query cost for N CASCADE targets spread over T magic tables: + ~N cross-table UNION scans + T `UPDATE`s + N single-row audit INSERTs + → 1 UNION lookup + T row fetches + T `UPDATE`s + 1 multi-row audit INSERT + (+ the unchanged batched hash-chain seal). +- No API surface change; `applyDeletionActions()` signature is unchanged. diff --git a/openspec/changes/batch-integrity-cascade-deletes/specs/deletion-audit-trail/spec.md b/openspec/changes/batch-integrity-cascade-deletes/specs/deletion-audit-trail/spec.md new file mode 100644 index 0000000000..9f3da60c6a --- /dev/null +++ b/openspec/changes/batch-integrity-cascade-deletes/specs/deletion-audit-trail/spec.md @@ -0,0 +1,45 @@ +--- +status: draft +--- +# Capability: `deletion-audit-trail` + +## Purpose + +Extend the single-INSERT cascade-context audit contract to referential-integrity +CASCADE enforcement: batch-handled cascade targets are audited with one +multi-row, hash-chain-sealed INSERT whose rows carry the canonical +cascade-context fold. + +## ADDED Requirements + +### Requirement: Batched integrity-cascade deletions MUST be audited with one multi-row INSERT + +`ReferentialIntegrityService::applyDeletionActions()` SHALL persist the audit +rows of CASCADE targets soft-deleted through its batched path with ONE +multi-row INSERT (`AuditTrailMapper::insertAuditTrails()`), each row pre-built +through the shared `buildAuditTrail()` row builder with action +`referential_integrity.cascade_delete` and a cascade context folded into the +`changed` column in the exact shape `createAuditTrail()` uses: +`changed.triggeredBy` = `referential_integrity` and `changed.cascadeContext` +with keys `triggerObject` (root object UUID), `triggerSchema` (root schema +slug), `action_type` (`referential_integrity.cascade_delete`) and `property` +(the referencing property from the analysis target). + +Audit persistence SHALL remain fail-soft: a failed row build or a failed +multi-row INSERT logs a warning and never aborts the cascade. Targets handled +by the per-object fallback pipeline keep their existing single-row audit +insert and legacy `changed` shape unchanged. + +#### Scenario: One multi-row INSERT for N batch-handled cascade targets +- **GIVEN** N CASCADE targets soft-deleted through the batched integrity path +- **WHEN** the audit rows are written +- **THEN** exactly one multi-row INSERT persists N rows +- **AND** each row has action `referential_integrity.cascade_delete` and a + `changed.cascadeContext` with `triggerObject`, `triggerSchema`, + `action_type` and `property` + +#### Scenario: Audit failure never aborts the cascade +- **GIVEN** the multi-row audit INSERT throws +- **WHEN** the batched integrity cascade continues +- **THEN** a warning is logged and the soft-deleted targets stay deleted +- **AND** no exception escapes `applyDeletionActions()` diff --git a/openspec/changes/batch-integrity-cascade-deletes/specs/object-lifecycle/spec.md b/openspec/changes/batch-integrity-cascade-deletes/specs/object-lifecycle/spec.md new file mode 100644 index 0000000000..b0e8b4e951 --- /dev/null +++ b/openspec/changes/batch-integrity-cascade-deletes/specs/object-lifecycle/spec.md @@ -0,0 +1,64 @@ +--- +status: draft +--- +# Capability: `object-lifecycle` + +## Purpose + +Extend the batched delete-path contract to referential-integrity CASCADE +enforcement: the deletion actions applied from a `DeletionAnalysis` resolve and +soft-delete their CASCADE targets with batched statements instead of running +the per-object pipeline per target — while preserving RESTRICT / SET_NULL / +SET_DEFAULT semantics, execution order and the per-object fallback. + +## ADDED Requirements + +### Requirement: Referential-integrity CASCADE deletions MUST be batched + +`ReferentialIntegrityService::applyDeletionActions()` SHALL apply the CASCADE +targets of a pre-computed `DeletionAnalysis` with batched statements: ONE +cross-magic-table lookup resolving all target UUIDs, one +`UPDATE ... SET _deleted = CASE _uuid ... END WHERE _uuid IN (...)` per magic +table (per-target `deletedBy`/`deletedAt`/`objectId`/`organisation` attribution +metadata bound per row), and ONE multi-row audit INSERT — instead of a +cross-table scan plus a single-row audit INSERT per target. + +Per-target semantics SHALL be preserved: an object-updating event is dispatched +per target before the write (a hook stopping propagation skips that target; a +payload-modifying hook routes that target through the full-row save), an +object-updated event is dispatched per target after the write, and one audit +row is written per analysis target (a target referenced through two properties +yields two rows). Targets the uuid-based batch lookup cannot resolve — and all +targets when the batched resolve or write fails — SHALL fall back to the +legacy per-object pipeline unchanged. RESTRICT, SET_NULL and SET_DEFAULT +handling and the SET_NULL → SET_DEFAULT → CASCADE (deepest first) execution +order are unchanged. + +#### Scenario: CASCADE targets are soft-deleted with batched statements +- **GIVEN** a deletion analysis with N CASCADE targets stored in one magic table +- **WHEN** `applyDeletionActions()` runs +- **THEN** the targets are resolved with one batched cross-table lookup +- **AND** soft-deleted with one UPDATE statement carrying per-target attribution + metadata including the acting user and active organisation +- **AND** N per-object updating and updated events are dispatched +- **AND** N audit rows are persisted with one multi-row INSERT + +#### Scenario: Batch misses keep the per-object pipeline +- **GIVEN** a deletion analysis where one CASCADE target UUID is not found by the + batched lookup +- **WHEN** `applyDeletionActions()` runs +- **THEN** that target is deleted and audited through the unchanged per-object + pipeline +- **AND** the resolved targets are still handled by the batched statements + +#### Scenario: Batch failure falls back to the per-object pipeline +- **GIVEN** the batched lookup or the batched soft-delete write fails +- **WHEN** `applyDeletionActions()` continues +- **THEN** every CASCADE target is retried through the legacy per-object + pipeline and no exception escapes + +#### Scenario: Non-CASCADE actions are untouched +- **GIVEN** a deletion analysis with SET_NULL and SET_DEFAULT targets +- **WHEN** `applyDeletionActions()` runs +- **THEN** those targets are processed per object exactly as before, before any + CASCADE deletion diff --git a/openspec/changes/batch-integrity-cascade-deletes/tasks.md b/openspec/changes/batch-integrity-cascade-deletes/tasks.md new file mode 100644 index 0000000000..b80487c7ac --- /dev/null +++ b/openspec/changes/batch-integrity-cascade-deletes/tasks.md @@ -0,0 +1,50 @@ +# Tasks — Batch referential-integrity CASCADE deletions + +Scope note: pure backend performance change with preserved contracts — no new +schemas/registers (no seed-data task), no lifecycle/aggregation/notification/ +widget behaviour (ADR-031 declarative-vs-imperative guidance does not apply). + +## 1. Batch the CASCADE path in ReferentialIntegrityService + +- [x] 1.1 `applyBatchCascadeDelete()`: resolve all CASCADE target UUIDs with ONE + `MagicMapper::findMultipleAcrossAllMagicTables(uuids, includeDeleted: false)` + call; a resolve failure logs a warning and routes every target to the + per-object fallback. +- [x] 1.2 Soft-delete resolved targets via + `MagicMapper::softDeleteMultipleObjectEntities()` (one CASE-UPDATE per magic + table, per-object updating/updated event pairs) with + `deletedBy`/`deletedAt`/`objectId`/`organisation` attribution metadata; + thread `applyDeletionActions()`'s `$organisationId` into the attribution. +- [x] 1.3 Write ONE multi-row audit INSERT via + `AuditTrailMapper::insertAuditTrails()` with rows pre-built through + `buildAuditTrail(old, null, 'referential_integrity.cascade_delete', + cascadeContext: {triggerObject, triggerSchema, action_type, property})` — + one row per analysis target, fail-soft on build and insert failures. +- [x] 1.4 Keep the previous per-object pipeline verbatim as + `applyPerObjectCascadeDelete()` and route batch misses, resolve failures and + total batch-write failures through it. +- [x] 1.5 Leave RESTRICT / SET_NULL / SET_DEFAULT handling, execution order + (SET_NULL → SET_DEFAULT → CASCADE deepest-first) and the analysis walk + untouched. + +## 2. Tests + +- [x] 2.1 Batched CASCADE path: N targets → 1 batched resolve + 1 + `softDeleteMultipleObjectEntities` call + 1 `insertAuditTrails` call; no + `deleteObjects()` / per-row `insert()` calls. +- [x] 2.2 Audit-row shape: `buildAuditTrail()` receives the deleted entity, + `new: null`, action `referential_integrity.cascade_delete` and the exact + cascade-context key shape; duplicate targets (same uuid, two properties) + produce two rows but one soft-delete. +- [x] 2.3 Fallbacks: batch-resolve miss → that target goes through + `deleteObjects()` + per-target audit insert (old changed-shape asserted); + resolve failure and batched-write failure → all targets fall back; audit + insert failure is fail-soft; hook-skipped targets are not force-retried. +- [x] 2.4 Existing RESTRICT / SET_NULL / SET_DEFAULT and integration tests stay + green unchanged. + +## 3. Verification + +- [x] 3.1 phpcs / phpmd / phpstan / psalm clean on touched files. +- [x] 3.2 Run the referential-integrity, delete and audit unit test families. +- [x] 3.3 `openspec validate batch-integrity-cascade-deletes --strict`. diff --git a/openspec/changes/besluiten-management/.openspec.yaml b/openspec/changes/besluiten-management/.openspec.yaml new file mode 100644 index 0000000000..7112eff115 --- /dev/null +++ b/openspec/changes/besluiten-management/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +status: proposed +created: 2026-03-20 diff --git a/openspec/changes/besluiten-management/design.md b/openspec/changes/besluiten-management/design.md new file mode 100644 index 0000000000..7b805e44ea --- /dev/null +++ b/openspec/changes/besluiten-management/design.md @@ -0,0 +1,9 @@ +# Design: Besluiten Management + +## Approach +Implement the requirements defined in the spec using OpenRegister's existing service architecture. + +## Files Affected +- `lib/Db/Register.php` +- `lib/Db/Schema.php` +- `lib/Service/FileService.php` diff --git a/openspec/changes/besluiten-management/proposal.md b/openspec/changes/besluiten-management/proposal.md new file mode 100644 index 0000000000..2fdef04bf8 --- /dev/null +++ b/openspec/changes/besluiten-management/proposal.md @@ -0,0 +1,7 @@ +# Besluiten Management + +## Problem +Implement formal decision management (besluiten) conforming to the ZGW BRC (Besluiten Registratie Component) standard, enabling Dutch government organizations to register, track, publish, and withdraw formal administrative decisions as first-class entities within OpenRegister. Decisions MUST be linked to decision types from a configurable catalog, support full lifecycle management from concept through definitive to withdrawn states, and integrate with case management (zaak-besluit linking), document management (besluit-informatieobject linking), archival calculations, and publication workflows under the Wet open overheid (Woo). + +## Proposed Solution +Implement formal decision management (besluiten) conforming to the ZGW BRC (Besluiten Registratie Component) standard, enabling Dutch government organizations to register, track, publish, and withdraw formal administrative decisions as first-class entities within OpenRegister. Decisions MUST be linked to decision types from a configurable catalog, support full lifecycle management from concept through definitive to withdrawn states, and integrate with case management (zaak-besluit linking), docu diff --git a/openspec/changes/besluiten-management/specs/besluiten-management/spec.md b/openspec/changes/besluiten-management/specs/besluiten-management/spec.md new file mode 100644 index 0000000000..10249ae4d9 --- /dev/null +++ b/openspec/changes/besluiten-management/specs/besluiten-management/spec.md @@ -0,0 +1,664 @@ +--- +status: draft +--- +# Besluiten Management + +## Purpose +Implement formal decision management (besluiten) conforming to the ZGW BRC (Besluiten Registratie Component) standard, enabling Dutch government organizations to register, track, publish, and withdraw formal administrative decisions as first-class entities within OpenRegister. Decisions MUST be linked to decision types from a configurable catalog, support full lifecycle management from concept through definitive to withdrawn states, and integrate with case management (zaak-besluit linking), document management (besluit-informatieobject linking), archival calculations, and publication workflows under the Wet open overheid (Woo). Every decision MUST maintain an immutable audit trail documenting creation, modification, publication, and withdrawal actions to satisfy legal accountability requirements under the Algemene wet bestuursrecht (Awb). + +**Source**: ZGW BRC API standard v1.0.2 (VNG Realisatie); gap identified in cross-platform competitive analysis (Dimpact ZAC, OpenZaak, Valtimo); Procest roles-decisions spec alignment. + +## ADDED Requirements + +### Requirement: Besluit Entity Schema (ZGW BRC Compliant) +Decisions (besluiten) SHALL be stored as OpenRegister objects with a dedicated schema whose properties conform to the ZGW BRC data model. The schema MUST include all fields defined in the BRC standard to ensure interoperability with other ZGW-compliant systems. + +The besluit schema MUST define the following properties: + +| Property | Type | ZGW Mapping | Required | Description | +|----------|------|-------------|----------|-------------| +| `identificatie` | string (max 50) | `identificatie` | auto-gen | Human-readable decision identifier, unique per verantwoordelijkeOrganisatie | +| `verantwoordelijkeOrganisatie` | string (RSIN, 9 digits) | `verantwoordelijke_organisatie` | Yes | RSIN of the responsible organization | +| `besluittype` | reference (UUID) | `besluittype` | Yes | Reference to a BesluitType object in the catalog | +| `zaak` | reference (UUID) | `zaak` | No | Reference to the originating case (optional for standalone decisions) | +| `datum` | date (ISO 8601) | `datum` | Yes | Decision date (MUST be today or earlier) | +| `toelichting` | string | `toelichting` | No | Explanation or motivation for the decision | +| `bestuursorgaan` | string (max 50) | `bestuursorgaan` | No | Governing body (e.g., Burgemeester, College van B&W, Gemeenteraad) | +| `ingangsdatum` | date (ISO 8601) | `ingangsdatum` | Yes | Effective start date of the decision | +| `vervaldatum` | date (ISO 8601) | `vervaldatum` | No | Expiry date of the decision | +| `vervalreden` | enum | `vervalreden` | No | Reason for expiry: `tijdelijk`, `ingetrokken_overheid`, `ingetrokken_belanghebbende` | +| `publicatiedatum` | date (ISO 8601) | `publicatiedatum` | No | Date the decision was published | +| `verzenddatum` | date (ISO 8601) | `verzenddatum` | No | Date the decision was sent to the betrokkene | +| `uiterlijkeReactiedatum` | date (ISO 8601) | `uiterlijke_reactiedatum` | No | Deadline for objection/response | + +Key constraint: The combination of `identificatie` and `verantwoordelijkeOrganisatie` MUST be unique across all besluit objects. + +#### Scenario: Create a besluit with all required fields +- **GIVEN** a register `procest` with a `besluit` schema conforming to the ZGW BRC data model +- **AND** a besluittype `omgevingsvergunning-verleend` exists in the catalog +- **WHEN** the user creates a besluit with: + - `besluittype`: reference to `omgevingsvergunning-verleend` + - `verantwoordelijkeOrganisatie`: `002220647` + - `datum`: `2026-03-15` + - `ingangsdatum`: `2026-03-16` + - `toelichting`: `Vergunning verleend conform aanvraag` + - `bestuursorgaan`: `College van B&W` +- **THEN** the besluit MUST be created as an OpenRegister object in the `besluit` schema +- **AND** an `identificatie` MUST be auto-generated based on the datum and a sequence number +- **AND** the besluit MUST be retrievable by its UUID + +#### Scenario: Auto-generate identificatie when not provided +- **GIVEN** a besluit is being created without an explicit `identificatie` +- **AND** the `verantwoordelijkeOrganisatie` is `002220647` +- **AND** the `datum` is `2026-03-15` +- **WHEN** the besluit is saved +- **THEN** the system MUST generate a unique `identificatie` (e.g., `BESLUIT-2026-0001`) +- **AND** the combination `(BESLUIT-2026-0001, 002220647)` MUST be unique + +#### Scenario: Reject duplicate identificatie for same organisation +- **GIVEN** a besluit with `identificatie` `BESLUIT-2026-0001` and `verantwoordelijkeOrganisatie` `002220647` already exists +- **WHEN** a new besluit is created with the same `identificatie` and `verantwoordelijkeOrganisatie` +- **THEN** the system MUST reject the creation with an error indicating the uniqueness constraint violation + +#### Scenario: Reject datum in the future +- **GIVEN** today is `2026-03-15` +- **WHEN** the user creates a besluit with `datum` set to `2026-03-20` +- **THEN** the system MUST reject the creation +- **AND** the error message MUST indicate that `datum` cannot be in the future + +#### Scenario: Create a standalone besluit without zaak reference +- **GIVEN** a policy decision that is not tied to a specific case +- **WHEN** the user creates a besluit with `zaak` set to null +- **THEN** the besluit MUST be created successfully as a standalone decision +- **AND** the besluit MUST still require `besluittype`, `datum`, and `ingangsdatum` + +--- + +### Requirement: Besluit Lifecycle (Concept to Definitief to Ingetrokken) +The system SHALL track the lifecycle state of each besluit through three phases: concept (draft), definitief (final/published), and ingetrokken (withdrawn). Lifecycle transitions MUST be validated to prevent illegal state changes, and each transition MUST be recorded in the audit trail. + +#### Scenario: Create a besluit in concept state +- **GIVEN** the user creates a new besluit +- **WHEN** the besluit is saved without setting `publicatiedatum` or `verzenddatum` +- **THEN** the besluit MUST be in the `concept` lifecycle state +- **AND** the besluit MUST be editable (all fields modifiable) +- **AND** the besluit MUST NOT be visible via the public API + +#### Scenario: Transition from concept to definitief +- **GIVEN** a besluit in `concept` state +- **WHEN** the user sets the `verzenddatum` to `2026-03-16` +- **AND** the besluit has all required fields populated (besluittype, datum, ingangsdatum) +- **THEN** the besluit MUST transition to `definitief` state +- **AND** the audit trail MUST record the transition with timestamp and user +- **AND** core fields (`besluittype`, `datum`, `verantwoordelijkeOrganisatie`) MUST become immutable + +#### Scenario: Transition from definitief to ingetrokken +- **GIVEN** a besluit in `definitief` state with verzenddatum `2026-03-16` +- **WHEN** the authorized user withdraws the besluit with vervalreden `ingetrokken_overheid` +- **THEN** the `vervaldatum` MUST be set to the current date +- **AND** the `vervalreden` MUST be set to `ingetrokken_overheid` +- **AND** the besluit MUST transition to `ingetrokken` state +- **AND** the audit trail MUST record the withdrawal with reason + +#### Scenario: Prevent re-activation of a withdrawn besluit +- **GIVEN** a besluit in `ingetrokken` state +- **WHEN** the user attempts to clear `vervalreden` or change `vervaldatum` to a future date +- **THEN** the system MUST reject the modification +- **AND** the error message MUST indicate that withdrawn decisions cannot be reactivated + +#### Scenario: Prevent deletion of a definitief besluit +- **GIVEN** a besluit in `definitief` state with linked informatieobjecten +- **WHEN** the user attempts to delete the besluit +- **THEN** the system MUST reject the deletion +- **AND** the error message MUST indicate that final decisions must be withdrawn, not deleted + +--- + +### Requirement: BesluitType Configuration via Catalog +Decision types (besluittypen) SHALL be defined as OpenRegister objects in a dedicated `besluittype` schema within the catalog, analogous to zaaktype configuration. Each besluittype MUST define reaction periods, publication requirements, and allowed information object types. + +The besluittype schema MUST include: + +| Property | Type | ZGW Mapping | Required | +|----------|------|-------------|----------| +| `omschrijving` | string (max 255) | `omschrijving` | Yes | +| `omschrijvingGeneriek` | string | `omschrijving_generiek` | No | +| `besluitcategorie` | string | `besluitcategorie` | No | +| `reactietermijn` | string (ISO 8601 duration) | `reactietermijn` | No | +| `publicatieIndicatie` | boolean | `publicatie_indicatie` | Yes | +| `publicatietermijn` | string (ISO 8601 duration) | `publicatietermijn` | No | +| `informatieobjecttypen` | array of references | `informatieobjecttypen` | No | +| `zaaktypen` | array of references | `zaaktypen` | No | +| `beginGeldigheid` | date | `begin_geldigheid` | Yes | +| `eindeGeldigheid` | date | `einde_geldigheid` | No | +| `concept` | boolean | `concept` | Yes | + +#### Scenario: Define a besluittype with reaction period +- **GIVEN** an admin configuring the besluittype catalog +- **WHEN** they create a besluittype: + - `omschrijving`: `Omgevingsvergunning verleend` + - `besluitcategorie`: `vergunning` + - `reactietermijn`: `P42D` (42 days / 6 weeks) + - `publicatieIndicatie`: `true` + - `publicatietermijn`: `P14D` (14 days) + - `beginGeldigheid`: `2026-01-01` + - `concept`: `false` +- **THEN** the besluittype MUST be available for selection when creating besluiten +- **AND** the reactietermijn MUST be used to auto-calculate `uiterlijkeReactiedatum` + +#### Scenario: Define a besluittype without publication requirement +- **GIVEN** an admin creating a besluittype for internal decisions +- **WHEN** they create a besluittype with `publicatieIndicatie`: `false` +- **THEN** besluiten of this type MUST NOT require `publicatiedatum` +- **AND** if a user sets `publicatiedatum` on a besluit of this type, the system MUST reject it with a validation error + +#### Scenario: Link besluittype to specific zaaktypen +- **GIVEN** a besluittype `Omgevingsvergunning verleend` +- **WHEN** the admin links it to zaaktypen `Omgevingsvergunning` and `Bouwvergunning` +- **THEN** only cases of those zaaktypen MUST be able to create besluiten with this besluittype +- **AND** attempting to create a besluit with this besluittype on a case of zaaktype `Klacht` MUST be rejected + +#### Scenario: Retire a besluittype by setting einde geldigheid +- **GIVEN** a besluittype `Subsidie toekenning` with `beginGeldigheid` `2024-01-01` +- **WHEN** the admin sets `eindeGeldigheid` to `2026-03-31` +- **THEN** the besluittype MUST remain valid for existing besluiten +- **AND** after `2026-03-31`, the besluittype MUST NOT be selectable for new besluiten + +--- + +### Requirement: Besluit-Zaak Linking +Each besluit SHOULD be linkable to a case (zaak) through a bidirectional reference. When a besluit is linked to a zaak, the system SHALL create a corresponding ZaakBesluit reference on the zaak side to maintain referential integrity, consistent with the ZGW cross-API synchronization pattern. + +#### Scenario: Link a besluit to a zaak on creation +- **GIVEN** a case `vergunning-1` of zaaktype `Omgevingsvergunning` +- **AND** the zaaktype has besluittype `Omgevingsvergunning verleend` in its `besluittypen` list +- **WHEN** the user creates a besluit with `zaak` referencing `vergunning-1` +- **THEN** the besluit MUST be created with the zaak reference +- **AND** the zaak object MUST be updated to include the besluit reference in its `besluiten` array +- **AND** the besluit MUST be visible in the Decisions section of the case detail view + +#### Scenario: Validate besluittype belongs to zaaktype +- **GIVEN** a case `klacht-1` of zaaktype `Klacht behandeling` +- **AND** the zaaktype does NOT include besluittype `Omgevingsvergunning verleend` +- **WHEN** the user creates a besluit with `zaak` referencing `klacht-1` and `besluittype` referencing `Omgevingsvergunning verleend` +- **THEN** the system MUST reject the creation +- **AND** the error MUST indicate that the besluittype is not allowed for this zaaktype + +#### Scenario: Update zaak reference on an existing besluit +- **GIVEN** a besluit `B-001` linked to zaak `vergunning-1` +- **WHEN** the user changes the `zaak` reference to `vergunning-2` +- **THEN** the previous zaak `vergunning-1` MUST have its besluit reference removed +- **AND** the new zaak `vergunning-2` MUST have the besluit reference added +- **AND** the audit trail MUST record the zaak change with both old and new references + +#### Scenario: Display multiple besluiten on a case +- **GIVEN** case `vergunning-1` has three linked besluiten: + - `B-001`: `Omgevingsvergunning verleend` (datum: 2026-03-10, ingangsdatum: 2026-03-15) + - `B-002`: `Voorwaardelijk besluit` (datum: 2026-02-20, ingangsdatum: 2026-03-01) + - `B-003`: `Besluit ingetrokken` (datum: 2026-04-01, vervalreden: ingetrokken_overheid) +- **WHEN** the user views the case detail +- **THEN** all three besluiten MUST be displayed in the Decisions section +- **AND** besluiten MUST be sorted by `datum` descending +- **AND** each besluit MUST show: identificatie, besluittype omschrijving, datum, lifecycle state indicator + +--- + +### Requirement: Besluit-InformatieObject Linking +Each besluit SHALL support linking to one or more informatieobjecten (documents) via a `besluitInformatieObject` join entity. This linking pattern follows the ZGW BRC standard where the BRC leads the relationship and the DRC mirrors it through ObjectInformatieObject records. + +#### Scenario: Link a document to a besluit +- **GIVEN** a besluit `B-001` for `Omgevingsvergunning verleend` +- **WHEN** the user uploads a formal decision letter `beschikking-2026-0001.pdf` +- **THEN** a `besluitInformatieObject` record MUST be created linking the besluit to the document +- **AND** the document MUST be accessible from both the besluit detail and the case dossier +- **AND** the `aardRelatie` MUST be set to `legt_vast` (documents the decision) + +#### Scenario: Link multiple documents to a besluit +- **GIVEN** a besluit `B-001` +- **WHEN** the user links three documents: the decision letter, a site plan, and an environmental assessment +- **THEN** three `besluitInformatieObject` records MUST be created +- **AND** all three documents MUST be listed in the besluit detail view +- **AND** each document MUST display its title, type, and creation date + +#### Scenario: Validate informatieobjecttype against besluittype +- **GIVEN** besluittype `Omgevingsvergunning verleend` allows informatieobjecttypen: `Beschikking`, `Bijlage` +- **AND** document `rapport.pdf` has informatieobjecttype `Intern rapport` +- **WHEN** the user attempts to link `rapport.pdf` to a besluit of this type +- **THEN** the system MUST reject the link with a validation error +- **AND** the error MUST indicate that the informatieobjecttype is not allowed for this besluittype + +#### Scenario: Remove a document link from a besluit +- **GIVEN** a besluit `B-001` with linked document `beschikking-2026-0001.pdf` +- **WHEN** the user removes the document link +- **THEN** the `besluitInformatieObject` record MUST be deleted +- **AND** the document itself MUST NOT be deleted (only the link is removed) +- **AND** the corresponding ObjectInformatieObject in the DRC MUST also be removed + +--- + +### Requirement: Verantwoordelijke Organisatie Tracking +Each besluit SHALL record the RSIN (Rechtspersonen en Samenwerkingsverbanden Identificatienummer) of the responsible organization. This field MUST be validated as a 9-digit number and SHALL be used together with the identificatie to ensure uniqueness across organizations. + +#### Scenario: Set verantwoordelijke organisatie from system configuration +- **GIVEN** the OpenRegister instance is configured with default RSIN `002220647` (Gemeente Utrecht) +- **WHEN** a new besluit is created without explicitly setting `verantwoordelijkeOrganisatie` +- **THEN** the system MUST default to the configured RSIN `002220647` +- **AND** the RSIN MUST be stored on the besluit object + +#### Scenario: Override verantwoordelijke organisatie for mandated decisions +- **GIVEN** a besluit is being created by Gemeente Utrecht on behalf of the Omgevingsdienst (RSIN `003456789`) +- **WHEN** the user explicitly sets `verantwoordelijkeOrganisatie` to `003456789` +- **THEN** the system MUST accept the override +- **AND** the uniqueness constraint for `identificatie` MUST be scoped to the new RSIN + +#### Scenario: Reject invalid RSIN format +- **GIVEN** the user sets `verantwoordelijkeOrganisatie` to `12345` (too short) or `abcdefghi` (non-numeric) +- **WHEN** the besluit is submitted +- **THEN** the system MUST reject the submission +- **AND** the error MUST indicate that the RSIN must be exactly 9 digits + +--- + +### Requirement: Ingangsdatum/Vervaldatum Handling +The system SHALL track the validity period (werkingsperiode) of each besluit through `ingangsdatum` (effective start) and `vervaldatum` (expiry). Changes to these dates MUST trigger archival recalculation on the linked zaak when the zaaktype uses `ingangsdatum_besluit` or `vervaldatum_besluit` as the archival date derivation method (afleidingswijze). + +#### Scenario: Calculate archival date from ingangsdatum when afleidingswijze is ingangsdatum_besluit +- **GIVEN** a zaak `vergunning-1` with zaaktype where `afleidingswijze` is `ingangsdatum_besluit` +- **AND** the zaak has two besluiten with `ingangsdatum` `2026-03-15` and `2026-04-01` +- **WHEN** the archival date is calculated +- **THEN** the system MUST use the maximum `ingangsdatum` across all linked besluiten (`2026-04-01`) +- **AND** this date MUST be the brondatum for the archival calculation + +#### Scenario: Calculate archival date from vervaldatum when afleidingswijze is vervaldatum_besluit +- **GIVEN** a zaak `vergunning-1` with zaaktype where `afleidingswijze` is `vervaldatum_besluit` +- **AND** the zaak has two besluiten with `vervaldatum` `2031-03-15` and `2029-12-31` +- **WHEN** the archival date is calculated +- **THEN** the system MUST use the maximum `vervaldatum` across all linked besluiten (`2031-03-15`) + +#### Scenario: Trigger archival recalculation on vervaldatum change +- **GIVEN** a besluit `B-001` linked to zaak `vergunning-1` +- **AND** the zaak's zaaktype uses `vervaldatum_besluit` as afleidingswijze +- **WHEN** the user updates `vervaldatum` from `2031-03-15` to `2033-06-30` +- **THEN** the system MUST trigger archival date recalculation on `vergunning-1` +- **AND** the new archival brondatum MUST reflect the updated vervaldatum + +#### Scenario: Display validity period on besluit detail +- **GIVEN** a besluit with `ingangsdatum` `2026-03-16` and `vervaldatum` `2031-03-16` +- **AND** today is `2026-06-15` +- **WHEN** the user views the besluit detail +- **THEN** the system MUST display the validity period as `16 maart 2026 -- 16 maart 2031` +- **AND** the status MUST show `Actief` with remaining time `4 jaar, 9 maanden resterend` + +#### Scenario: Display besluit without vervaldatum as indefinitely valid +- **GIVEN** a besluit with `ingangsdatum` `2026-03-16` and no `vervaldatum` +- **WHEN** the user views the besluit detail +- **THEN** the system MUST display `Geldig vanaf 16 maart 2026` with no end date +- **AND** the besluit MUST be treated as indefinitely valid + +--- + +### Requirement: Vervalreden Tracking +When a besluit expires or is withdrawn, the system SHALL record the reason (vervalreden) using the ZGW standard enumeration. The vervalreden MUST be one of three values: `tijdelijk` (temporary decision expired naturally), `ingetrokken_overheid` (withdrawn by the governing authority), or `ingetrokken_belanghebbende` (withdrawn at the request of the interested party). + +#### Scenario: Record expiry by natural end of temporary decision +- **GIVEN** a besluit `B-001` with `vervaldatum` `2026-12-31` +- **WHEN** the vervaldatum passes and the system detects the expiry during a scheduled check +- **THEN** the `vervalreden` MUST be set to `tijdelijk` +- **AND** the besluit lifecycle state MUST change to reflect the expiry + +#### Scenario: Record withdrawal by the governing authority +- **GIVEN** a definitief besluit `B-001` for `Omgevingsvergunning verleend` +- **WHEN** the authorized user withdraws the besluit with explanation `Besluit ingetrokken wegens onregelmatigheden in de aanvraag` +- **THEN** the `vervalreden` MUST be set to `ingetrokken_overheid` +- **AND** the `vervaldatum` MUST be set to the current date +- **AND** the `toelichting` MUST be updated with: `Overheid: Besluit ingetrokken wegens onregelmatigheden in de aanvraag` + +#### Scenario: Record withdrawal at request of the interested party +- **GIVEN** a definitief besluit `B-002` for a granted permit +- **WHEN** the permit holder requests withdrawal +- **AND** the authorized user processes the withdrawal with vervalreden `ingetrokken_belanghebbende` +- **THEN** the `vervalreden` MUST be set to `ingetrokken_belanghebbende` +- **AND** the `toelichting` MUST be updated with: `Belanghebbende: [withdrawal explanation]` + +#### Scenario: Reject vervalreden without vervaldatum +- **GIVEN** a besluit without a `vervaldatum` +- **WHEN** the user attempts to set `vervalreden` to `tijdelijk` +- **THEN** the system MUST reject the modification +- **AND** the error MUST indicate that vervalreden requires a vervaldatum to be set + +--- + +### Requirement: Besluit Publicatie (Woo Compliance) +Besluiten with `publicatieIndicatie: true` on their besluittype SHALL be subject to publication requirements under the Wet open overheid (Woo). The system MUST support marking decisions for publication, tracking publication dates, and providing a public-facing view with personal data redaction. + +#### Scenario: Flag a besluit for publication based on besluittype +- **GIVEN** a besluit of besluittype `Omgevingsvergunning verleend` with `publicatieIndicatie: true` +- **WHEN** the besluit transitions to definitief state +- **THEN** the system MUST flag the besluit as requiring publication +- **AND** the publication deadline MUST be calculated from the `verzenddatum` plus the besluittype's `publicatietermijn` +- **AND** a notification MUST be sent to the publication officer + +#### Scenario: Set publicatiedatum and validate response deadline +- **GIVEN** a besluit with besluittype having `reactietermijn: P42D` (42 days) +- **WHEN** the user sets `publicatiedatum` to `2026-03-16` +- **THEN** the `uiterlijkeReactiedatum` MUST be at minimum `2026-04-27` (publicatiedatum + 42 days) +- **AND** if the user sets `uiterlijkeReactiedatum` to a date before `2026-04-27`, the system MUST reject it with a validation error + +#### Scenario: Publish besluit to public API with PII redaction +- **GIVEN** a besluit with `publicatiedatum` set and `publicatieIndicatie: true` +- **WHEN** the besluit is accessed via the public (unauthenticated) API +- **THEN** the besluit MUST be returned with personal data fields redacted +- **AND** the `toelichting` MUST have person names, BSN numbers, and addresses replaced with `[GEANONIMISEERD]` +- **AND** linked documents in the public view MUST also have PII redacted or be restricted based on schema-level redaction configuration + +#### Scenario: Reject publication dates when publicatieIndicatie is false +- **GIVEN** a besluittype `Intern adviesbesluit` with `publicatieIndicatie: false` +- **WHEN** the user creates a besluit of this type and sets `publicatiedatum` to `2026-03-16` +- **THEN** the system MUST reject the publication date +- **AND** the error MUST indicate that this besluittype does not require publication + +#### Scenario: Validate response date requires publication date and vice versa +- **GIVEN** a besluit with publicatieIndicatie true +- **WHEN** the user sets `uiterlijkeReactiedatum` without setting `publicatiedatum` +- **THEN** the system MUST reject with error indicating that `publicatiedatum` is required when `uiterlijkeReactiedatum` is set +- **AND** similarly, setting `publicatiedatum` without `uiterlijkeReactiedatum` MUST also be rejected + +--- + +### Requirement: Besluit Bezwaar/Beroep Tracking +The system SHALL support tracking objections (bezwaar) and appeals (beroep) filed against decisions. When the `uiterlijkeReactiedatum` is set, the system MUST track whether the deadline has passed and whether any formal objection has been received, supporting the administrative law lifecycle under the Awb. + +#### Scenario: Calculate uiterlijkeReactiedatum from verzenddatum and reactietermijn +- **GIVEN** a besluit with `verzenddatum` `2026-03-16` +- **AND** the besluittype has `reactietermijn` `P42D` +- **WHEN** the besluit is created or `verzenddatum` is set +- **THEN** `uiterlijkeReactiedatum` MUST be automatically calculated as `2026-04-27` +- **AND** the calculated date MUST be stored on the besluit +- **AND** the user MAY override the calculated date to a later date but NOT to an earlier date + +#### Scenario: Display active bezwaartermijn with countdown +- **GIVEN** a besluit with `uiterlijkeReactiedatum` `2026-04-27` +- **AND** today is `2026-04-01` +- **WHEN** the besluit detail is viewed +- **THEN** the system MUST display `26 dagen resterend voor bezwaar/beroep` +- **AND** a progress indicator MUST show the elapsed and remaining portion of the bezwaartermijn + +#### Scenario: Display expired bezwaartermijn +- **GIVEN** a besluit with `uiterlijkeReactiedatum` `2026-04-27` +- **AND** today is `2026-05-01` +- **WHEN** the besluit detail is viewed +- **THEN** the system MUST display `Bezwaartermijn verlopen (sinds 27 april 2026)` +- **AND** the indicator MUST be visually distinct (e.g., greyed out or marked as complete) + +#### Scenario: Register a bezwaar against a besluit +- **GIVEN** a definitief besluit `B-001` with active bezwaartermijn +- **WHEN** a formal objection is received and registered +- **THEN** the system MUST create a linked `bezwaar` record referencing the besluit +- **AND** the besluit detail MUST show the number of active bezwaren +- **AND** the bezwaar MAY trigger a new case (zaak) for processing the objection + +#### Scenario: Notify approaching bezwaartermijn deadline +- **GIVEN** a besluit with `uiterlijkeReactiedatum` `2026-04-27` +- **AND** today is `2026-04-22` (5 days before deadline) +- **WHEN** the daily scheduled job runs +- **THEN** the system MUST send a Nextcloud notification to the case handler +- **AND** the notification MUST include: besluit identificatie, linked zaak, days remaining + +--- + +### Requirement: Besluit API (CRUD and Status Transitions) +The system SHALL expose RESTful API endpoints for besluit CRUD operations that follow the ZGW BRC URL structure and response format. The API MUST support content negotiation, pagination, filtering, and the standard ZGW scope-based authorization model. + +| Method | Path | Scope | Description | +|--------|------|-------|-------------| +| GET | `/api/besluiten/v1/besluiten` | `besluiten.lezen` | List decisions with filtering | +| POST | `/api/besluiten/v1/besluiten` | `besluiten.aanmaken` | Create a decision | +| GET | `/api/besluiten/v1/besluiten/{uuid}` | `besluiten.lezen` | Retrieve a decision | +| PUT | `/api/besluiten/v1/besluiten/{uuid}` | `besluiten.bijwerken` | Full update | +| PATCH | `/api/besluiten/v1/besluiten/{uuid}` | `besluiten.bijwerken` | Partial update | +| DELETE | `/api/besluiten/v1/besluiten/{uuid}` | `besluiten.verwijderen` | Delete a decision | +| GET | `/api/besluiten/v1/besluiten/{uuid}/audittrail` | `besluiten.lezen` | Audit trail | +| GET | `/api/besluiten/v1/besluitinformatieobjecten` | `besluiten.lezen` | List linked documents | +| POST | `/api/besluiten/v1/besluitinformatieobjecten` | `besluiten.aanmaken` | Link a document | +| DELETE | `/api/besluiten/v1/besluitinformatieobjecten/{uuid}` | `besluiten.verwijderen` | Unlink a document | + +#### Scenario: Create a besluit via API +- **GIVEN** an authenticated client with scope `besluiten.aanmaken` +- **WHEN** the client sends `POST /api/besluiten/v1/besluiten` with a valid JSON body +- **THEN** the system MUST return HTTP 201 with the created besluit including generated `uuid` and `identificatie` +- **AND** the `url` field in the response MUST be the absolute URL to the created resource + +#### Scenario: List besluiten with filtering +- **GIVEN** 50 besluiten in the register, 10 of which are linked to zaak `vergunning-1` +- **WHEN** the client sends `GET /api/besluiten/v1/besluiten?zaak=` +- **THEN** the system MUST return only the 10 besluiten linked to the specified zaak +- **AND** the response MUST use standard ZGW pagination with `count`, `next`, `previous`, and `results` + +#### Scenario: Filter besluiten by besluittype and date range +- **GIVEN** multiple besluiten across different types and dates +- **WHEN** the client sends `GET /api/besluiten/v1/besluiten?besluittype=&datum__gte=2026-01-01&datum__lte=2026-03-31` +- **THEN** the system MUST return only besluiten matching both the besluittype and the date range + +#### Scenario: Reject unauthorized API access +- **GIVEN** an authenticated client with only `besluiten.lezen` scope +- **WHEN** the client sends `POST /api/besluiten/v1/besluiten` (create) +- **THEN** the system MUST return HTTP 403 Forbidden +- **AND** the error MUST indicate insufficient scope + +#### Scenario: Return audit trail for a besluit +- **GIVEN** a besluit `B-001` that has been created, updated, and had documents linked +- **WHEN** the client sends `GET /api/besluiten/v1/besluiten/{uuid}/audittrail` +- **THEN** the system MUST return a chronological list of all actions performed on the besluit +- **AND** each entry MUST include: timestamp, user, action type (create/update/delete), and changed fields + +--- + +### Requirement: Bulk Besluit Operations +The system SHALL support batch processing of besluiten for common government workflows where multiple decisions are issued simultaneously (e.g., batch permit approvals, mass subsidy grants). The batch endpoint MUST follow the OpenZaak `besluit_verwerken` convenience pattern. + +#### Scenario: Batch create besluiten for multiple cases +- **GIVEN** 15 cases of zaaktype `Subsidie aanvraag` are ready for decision +- **AND** all cases should receive besluittype `Subsidie toegekend` +- **WHEN** the user submits a batch operation with a list of zaak UUIDs and shared besluit properties +- **THEN** the system MUST create 15 individual besluiten, one per case +- **AND** each besluit MUST have a unique `identificatie` +- **AND** the response MUST include a summary: `15 besluiten aangemaakt, 0 fouten` + +#### Scenario: Batch create with partial failure +- **GIVEN** a batch of 10 besluiten to create +- **AND** 2 of the 10 have invalid zaak references +- **WHEN** the batch is submitted +- **THEN** the system MUST create the 8 valid besluiten +- **AND** the response MUST report `8 besluiten aangemaakt, 2 fouten` +- **AND** each error MUST include the zaak reference and the specific validation error + +#### Scenario: Batch withdrawal of related besluiten +- **GIVEN** 5 besluiten linked to cases that are part of a revoked policy +- **WHEN** the user submits a batch withdrawal with vervalreden `ingetrokken_overheid` +- **THEN** all 5 besluiten MUST have `vervaldatum` set to the current date and `vervalreden` set to `ingetrokken_overheid` +- **AND** the audit trail for each besluit MUST record the withdrawal + +--- + +### Requirement: Besluit Search and Filtering +The system SHALL provide comprehensive search and filtering capabilities for besluiten, supporting both the API filter parameters from the ZGW BRC standard and a frontend search interface integrated with OpenRegister's faceted search. + +#### Scenario: Search besluiten by free text in toelichting +- **GIVEN** 100 besluiten in the register +- **AND** 3 of them contain the word `asbest` in the toelichting +- **WHEN** the user searches for `asbest` +- **THEN** the system MUST return the 3 matching besluiten +- **AND** the search result MUST highlight the matching text in the toelichting + +#### Scenario: Filter besluiten by lifecycle state +- **GIVEN** 50 besluiten: 20 concept, 25 definitief, 5 ingetrokken +- **WHEN** the user filters by lifecycle state `definitief` +- **THEN** the system MUST return only the 25 definitief besluiten + +#### Scenario: Filter besluiten by verantwoordelijke organisatie +- **GIVEN** besluiten from multiple organizations in a shared register +- **WHEN** the user filters by `verantwoordelijkeOrganisatie` `002220647` +- **THEN** only besluiten from that organization MUST be returned + +#### Scenario: Filter besluiten with active bezwaartermijn +- **GIVEN** 30 definitief besluiten, 12 of which have `uiterlijkeReactiedatum` in the future +- **WHEN** the user selects the filter `Bezwaartermijn actief` +- **THEN** the system MUST return only the 12 besluiten with unexpired bezwaartermijn +- **AND** results MUST be sorted by `uiterlijkeReactiedatum` ascending (nearest deadline first) + +#### Scenario: Faceted search combining multiple filters +- **GIVEN** the user wants to find all granted permits from Q1 2026 +- **WHEN** the user applies filters: + - besluittype: `Omgevingsvergunning verleend` + - datum range: `2026-01-01` to `2026-03-31` + - lifecycle state: `definitief` +- **THEN** the system MUST return only besluiten matching all three criteria +- **AND** facet counts MUST be displayed for further narrowing + +--- + +### Requirement: Audit Trail for Decisions +Every action on a besluit (creation, modification, status transition, document linking, withdrawal) SHALL be recorded in an immutable audit trail. The audit trail MUST comply with the ZGW BRC audittrail specification and integrate with OpenRegister's existing AuditTrailMapper for consistent logging across all entity types. + +#### Scenario: Record besluit creation in audit trail +- **GIVEN** user `jan.devries` creates a besluit `B-001` +- **WHEN** the creation is completed +- **THEN** the audit trail MUST contain an entry with: + - `actie`: `create` + - `actieWeergave`: `Besluit aangemaakt` + - `resultaat`: HTTP 201 + - `hoofdObject`: URL of the besluit + - `resource`: `besluit` + - `resourceUrl`: URL of the besluit + - `aanmaakdatum`: current timestamp + - `wijzigingen.nieuw`: all field values of the created besluit + +#### Scenario: Record field modification in audit trail +- **GIVEN** a besluit `B-001` with `toelichting` `Vergunning verleend` +- **WHEN** user `maria.bakker` updates `toelichting` to `Vergunning verleend met voorwaarden` +- **THEN** the audit trail MUST contain an entry with: + - `actie`: `update` + - `wijzigingen.oud.toelichting`: `Vergunning verleend` + - `wijzigingen.nieuw.toelichting`: `Vergunning verleend met voorwaarden` + +#### Scenario: Record withdrawal in audit trail +- **GIVEN** a definitief besluit `B-001` +- **WHEN** the besluit is withdrawn with vervalreden `ingetrokken_overheid` +- **THEN** the audit trail MUST contain an entry recording: + - The vervalreden being set + - The vervaldatum being set + - The toelichting being updated with the withdrawal explanation + - The lifecycle state transition from `definitief` to `ingetrokken` + +#### Scenario: Audit trail entries are immutable +- **GIVEN** an audit trail with 10 entries for besluit `B-001` +- **WHEN** a user or API client attempts to modify or delete an existing audit trail entry +- **THEN** the system MUST reject the operation with HTTP 405 Method Not Allowed +- **AND** audit trail entries MUST be append-only + +--- + +### Requirement: VNG BRC API Mapping +The system SHALL provide a ZGW BRC-compatible API layer that maps OpenRegister's internal besluit objects to the standard BRC response format. This mapping enables interoperability with other ZGW-compliant systems (Dimpact ZAC, Valtimo, Open Formulieren) that expect standard BRC endpoints and response structures. + +#### Scenario: Map internal besluit to ZGW BRC response format +- **GIVEN** an internal besluit object stored in OpenRegister with camelCase property names +- **WHEN** the besluit is retrieved via the ZGW-compatible API endpoint +- **THEN** the response MUST use the ZGW BRC field naming convention (snake_case): + - `verantwoordelijke_organisatie` (not `verantwoordelijkeOrganisatie`) + - `uiterlijke_reactiedatum` (not `uiterlijkeReactiedatum`) + - `besluittype` as full URL reference (not UUID) + - `zaak` as full URL reference (not UUID) +- **AND** the response MUST include the standard `url` field pointing to the resource's canonical URL + +#### Scenario: Accept ZGW BRC request format on creation +- **GIVEN** an external system (e.g., Valtimo) sends a POST request using ZGW BRC field naming +- **WHEN** the request body uses `verantwoordelijke_organisatie` and `uiterlijke_reactiedatum` +- **THEN** the system MUST accept both snake_case and camelCase field names +- **AND** the internal storage MUST normalize to the OpenRegister property naming convention + +#### Scenario: Resolve URL references to besluittype and zaak +- **GIVEN** a besluit creation request with `besluittype` as a full URL `https://catalogi.example.com/api/v1/besluittypen/{uuid}` +- **WHEN** the system processes the request +- **THEN** the system MUST resolve the URL to the internal besluittype object +- **AND** if the URL references an external catalog, the system MUST validate that the besluittype exists at that URL (HTTP GET returns 200) + +#### Scenario: Cross-API synchronization for zaak-besluit linking +- **GIVEN** a besluit is created with a `zaak` reference +- **WHEN** the creation is processed +- **THEN** the system MUST automatically create a corresponding `ZaakBesluit` record on the zaak side +- **AND** the ZaakBesluit MUST reference the besluit URL +- **AND** deleting the besluit MUST also remove the ZaakBesluit record + +--- + +## Current Implementation Status + +- **NOT implemented:** No dedicated besluiten (decisions) management exists in the OpenRegister core codebase. + - No `besluit` schema, entity, or dedicated controller in OpenRegister + - No `besluittype` catalog schema or configuration + - No bezwaartermijn calculation logic + - No decision withdrawal (intrekking) workflow + - No publication workflow for decisions + - No personal data redaction for public decision views + - No batch besluit operations + - No BRC-compatible API endpoints + +- **Partial foundations in OpenRegister:** + - Register and Schema entities (`lib/Db/Register.php`, `lib/Db/Schema.php`) support arbitrary schema definitions that can model the besluit data structure + - Objects can reference each other via schema `$ref` properties, enabling zaak-besluit bidirectional linking + - The existing object model can store besluiten as regular register objects with a dedicated besluit schema + - File linking is available via `FileService` (`lib/Service/FileService.php`) for attaching decision documents + - `AuditTrailMapper` provides immutable audit logging infrastructure + - DSO register (`lib/Settings/dso_register.json`) already contains `besluitdatum` fields on permit applications, demonstrating the pattern + - ORI register (`lib/Settings/ori_register.json`) already has `besluit` as a document type for council decisions + +- **Partial foundations in Procest:** + - Decision schema defined in `procest_register.json` with `title`, `description`, `case`, `decisionType`, `decidedBy`, `decidedAt`, `effectiveDate`, `expiryDate` properties -- needs alignment with ZGW BRC field names + - DecisionType schema defined with `name`, `description`, `category`, `objectionPeriod`, `publicationRequired`, `publicationPeriod` -- needs ZGW BRC field mapping + - No frontend UI exists for creating, viewing, editing, or deleting decisions + - The roles-decisions spec (`procest/openspec/specs/roles-decisions/spec.md`) defines the Procest-side data model and CRUD requirements + +## Standards & References +- **ZGW BRC (Besluiten Registratie Component) v1.0.2** -- API standard for decision registration in Dutch government (VNG Realisatie) +- **ZGW ZTC (Zaaktypecatalogus)** -- BesluitType definitions within the catalog, including reactietermijn and publicatieIndicatie +- **Awb (Algemene wet bestuursrecht)** -- Legal framework for formal government decisions, appeal periods (bezwaartermijn), and administrative proceedings +- **RGBZ (Referentiemodel Gemeentelijke Basisgegevens Zaken)** -- Reference data model including besluiten entity relationships +- **MDTO (Metagegevens Duurzaam Toegankelijke Overheidsinformatie)** -- Archival metadata standard for decisions +- **Wet open overheid (Woo)** -- Publication requirements for government decisions, replacing the Wob +- **VNG ZGW API specificaties** -- https://vng-realisatie.github.io/gemma-zaken/ +- **OpenZaak BRC implementation** -- Reference implementation for BRC API compliance (analyzed in competitive analysis) +- **Dimpact ZAC DecisionService** -- Publication date validation patterns with reactietermijn calculation + +## Cross-References +- **document-zaakdossier** -- Linked documents (beschikking PDFs) in the case dossier view; besluitInformatieObject records integrate with the dossier structure +- **archivering-vernietiging** -- Besluit ingangsdatum/vervaldatum drive archival brondatum calculation via afleidingswijze `ingangsdatum_besluit` and `vervaldatum_besluit` +- **zgw-api-mapping** -- BRC API endpoint structure, field name translation (camelCase to snake_case), and URL-based resource references +- **audit-trail-immutable** -- Audit trail entries for besluit lifecycle events use the shared AuditTrailMapper infrastructure +- **roles-decisions (Procest)** -- Procest-side decision entity and decision type schemas; the `decision_maker` generic role determines who can create besluiten + +## Nextcloud Integration Analysis + +**Status**: Not yet implemented. No dedicated besluiten management, besluittype catalog, bezwaartermijn tracking, or publication workflow exists. Objects can reference each other and files can be linked, providing partial foundations. + +**Nextcloud Core Interfaces**: +- `INotifier` / `INotification`: Send notifications for bezwaartermijn expiration warnings (e.g., "5 days remaining for bezwaar on besluit X"), decision publication deadlines, and withdrawal actions. Register a `BesluitNotifier` implementing `INotifier` for formatted notification display. +- `IEventDispatcher`: Fire typed events (`BesluitCreatedEvent`, `BesluitPublishedEvent`, `BesluitWithdrawnEvent`, `BesluitExpiredEvent`) for cross-app integration. Procest and other consuming apps can listen for these events to update case status or trigger follow-up workflows. +- `TimedJob`: Schedule a `BezwaartermijnCheckJob` that runs daily, scanning besluiten with upcoming or expired `uiterlijkeReactiedatum` and triggering notifications or status updates. Schedule a `VervaldatumCheckJob` to detect naturally expired temporary decisions and set `vervalreden` to `tijdelijk`. +- `IActivityManager` / `IProvider`: Register decision lifecycle events (creation, publication, withdrawal, expiry) in the Nextcloud Activity stream so users see a chronological history of decision actions on their activity feed. + +**Implementation Approach**: +- Model besluiten and besluittypen as OpenRegister schemas within the Procest register. The `besluit` schema stores the decision data conforming to the ZGW BRC data model. The `besluittype` schema serves as the catalog defining decision types with reactietermijn and publicatieIndicatie. +- Use schema `$ref` properties for bidirectional zaak-besluit linking. When a besluit is created, the linked zaak object is updated with the besluit reference (via `ObjectService`). Implement a pre-save hook to maintain referential integrity when zaak references change. +- Implement bezwaartermijn calculation as a computed field or pre-save hook: `uiterlijkeReactiedatum = verzenddatum + besluittype.reactietermijn` (ISO 8601 duration parsing). +- For publication, leverage OpenRegister's existing public API access control. Mark published besluiten with a publication flag that makes them accessible via unauthenticated API endpoints. Implement a `RedactionHandler` that strips PII fields from the public view based on schema-level configuration (field-level annotation of sensitive fields). +- Use `FileService` for linking beschikking documents (PDF) to besluit objects, integrating with the document-zaakdossier spec for structured dossier views. +- Implement the BRC-compatible API layer as a separate controller that translates between ZGW BRC format (snake_case, URL references) and the internal OpenRegister object model (camelCase, UUID references). + +**Dependencies on Existing OpenRegister Features**: +- `ObjectService` -- CRUD for besluit and besluittype objects with inter-object references +- `SchemaService` / `SchemaMapper` -- schema definitions with `$ref` for zaak-besluit relationships +- `AuditTrailMapper` -- immutable logging of decision creation, publication, and withdrawal actions +- `FileService` -- document attachment for beschikking PDFs +- `HyperFacetHandler` -- faceted search and filtering for besluit lists +- Procest app -- owns the case context and decision type catalog configuration; the `decision_maker` role determines authorization for besluit creation diff --git a/openspec/changes/besluiten-management/tasks.md b/openspec/changes/besluiten-management/tasks.md new file mode 100644 index 0000000000..e23dda82dd --- /dev/null +++ b/openspec/changes/besluiten-management/tasks.md @@ -0,0 +1,17 @@ +# Tasks: Besluiten Management + +- [ ] Implement: Besluit Entity Schema (ZGW BRC Compliant) +- [ ] Implement: Besluit Lifecycle (Concept to Definitief to Ingetrokken) +- [ ] Implement: BesluitType Configuration via Catalog +- [ ] Implement: Besluit-Zaak Linking +- [ ] Implement: Besluit-InformatieObject Linking +- [ ] Implement: Verantwoordelijke Organisatie Tracking +- [ ] Implement: Ingangsdatum/Vervaldatum Handling +- [ ] Implement: Vervalreden Tracking +- [ ] Implement: Besluit Publicatie (Woo Compliance) +- [ ] Implement: Besluit Bezwaar/Beroep Tracking +- [ ] Implement: Besluit API (CRUD and Status Transitions) +- [ ] Implement: Bulk Besluit Operations +- [ ] Implement: Besluit Search and Filtering +- [ ] Implement: Audit Trail for Decisions +- [ ] Implement: VNG BRC API Mapping diff --git a/openspec/changes/calc-engine-scalar-functions/.openspec.yaml b/openspec/changes/calc-engine-scalar-functions/.openspec.yaml new file mode 100644 index 0000000000..18edba1f54 --- /dev/null +++ b/openspec/changes/calc-engine-scalar-functions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-20 diff --git a/openspec/changes/calc-engine-scalar-functions/design.md b/openspec/changes/calc-engine-scalar-functions/design.md new file mode 100644 index 0000000000..710053b3ae --- /dev/null +++ b/openspec/changes/calc-engine-scalar-functions/design.md @@ -0,0 +1,138 @@ +## Context + +`CalculationEvaluator` (`lib/Service/Calculation/CalculationEvaluator.php`) is a +pure JSON-AST interpreter. `evaluate(array $object, mixed $expression)` dispatches +on a single-key operator object via `match($op)` (current arms: `prop, lit, concat, +if, not, and, or, +, -, *, /, %, eq/ne/lt/lte/gt/gte, now, diffDays, formatDate, +dateDiff`). Each arm delegates to a private handler that re-evaluates its operands +through `evaluate()`, so operators compose recursively. The engine has no I/O — it +reads only the object payload (plus the synthetic `@self` block the +`CalculationOnSaveListener` injects). Materialisation, serialisation, and the +`materialise: true` gate live in the listener and are operator-agnostic; adding +operators requires no listener change. + +Established conventions in the existing arms (read from the code, to be matched): + +- **Operand shape.** Multi-operand ops take an array of sub-expressions (e.g. + `arith()`); fixed-arity ops index `$args[0]`, `$args[1]` (e.g. `divide`, `diffDays`). +- **Numeric guard.** Arithmetic uses `is_numeric($v) === false → throw + EvaluationException` and coerces with `$v + 0` (`arith`, `subOrNeg`, `divide`). +- **Date handling.** `toDateOrNull()` returns `DateTimeImmutable|null`; date ops + (`diffDays`, `formatDate`) return `null` when an operand is unparseable rather + than throwing. Calendar month math already exists in `calendarDiff()` + (`$interval->y * 12 + $interval->m`, sign from `$interval->invert`). +- **Errors.** Malformed arity / wrong arg type → `EvaluationException` with a + descriptive message; data-shaped failures (null/unparseable date) → `null`. + +The fleet gap (shillinq, ~43 calcs) is that common derived fields — a clamped +total, a null-fallback default, an absolute variance, a rounded amount, a year +extracted from a date, months-elapsed for an ageing bucket — have no operator and +fall back to imperative PHP, violating ADR-031's declarative-first rule. + +## Goals / Non-Goals + +**Goals:** +- Add `max, min, coalesce, abs, round, year, monthsElapsed` as pure per-object + operators, each matching the existing evaluator's arg/null/error conventions. +- Keep the change additive and backward-compatible (no existing arm touched). +- Cover each operator with unit tests including null/edge cases. + +**Non-Goals:** +- **Cross-object folding** (`sum`/`lookup`/`map` over OTHER objects). These require + a register/schema query surface the per-object evaluator deliberately does not + have. They belong to `x-openregister-aggregations`. Stated as an explicit + non-goal so authors do not reach for `max`/`min` expecting a cross-object reduce. +- Changing the `materialise` gate, serialisation, listener, or schema validator. +- Twig-path computed fields (a separate mechanism in the same `computed-fields` + capability) — untouched. + +## Decisions + +### ADR-031 declarative-vs-imperative decision + +| Behaviour | Declarative (this change) | Imperative (rejected) | Decision | +|---|---|---|---| +| Clamp / pick extreme (`max`,`min`) | New evaluator operator | Per-app PHP calc service | **Declarative** — pure per-object math, fits `x-openregister-calculations` | +| Null-fallback (`coalesce`) | New operator | `if(not(prop),…)` chains | **Declarative** — first-non-null is a primitive, chains are unreadable | +| Magnitude / rounding (`abs`,`round`) | New operator | PHP at read time | **Declarative** — pure scalar math | +| Year extraction (`year`) | New operator | `formatDate(d,'Y')` then cast | **Declarative** — typed integer result, avoids string round-trip | +| Whole months (`monthsElapsed`) | New operator reusing calendar logic | `dateDiff({unit:'months'})` | **Declarative** — `dateDiff` already covers months but requires the dict arg shape; `monthsElapsed(later,earlier)` is the positional twin matching `diffDays`, and is what the audited calcs expect | + +This is pure per-object math/date logic with no external reach, so it is squarely +declarative per ADR-031's calculations row — no exception applies. + +### Operator semantics (the contract for the spec) + +| Op | Arity | Operand semantics | Null handling | Error (`EvaluationException`) | +|---|---|---|---|---| +| `max` | N ≥ 1 | array of numeric sub-exprs | a `null` operand is skipped; all-null → `null` | non-array args, or a non-null non-numeric operand | +| `min` | N ≥ 1 | array of numeric sub-exprs | a `null` operand is skipped; all-null → `null` | non-array args, or a non-null non-numeric operand | +| `coalesce` | N ≥ 1 | array of sub-exprs (any type) | returns first operand that is not `null`; all-null → `null` | non-array args | +| `abs` | 1 | single numeric sub-expr | `null` operand → `null` | non-null non-numeric operand | +| `round` | 1 or 2 | `[value]` or `[value, precision]` | `null` value → `null` | non-array args, non-numeric value, or non-integer precision | +| `year` | 1 | single date sub-expr | unparseable / `null` date → `null` | (none beyond arity — mirrors `formatDate`) | +| `monthsElapsed` | 2 | `[later, earlier]` date sub-exprs | either date unparseable / `null` → `null` | fewer than two operands | + +Notes: +- `max`/`min` **skip** null operands (rather than returning null) so a calc over an + optional field still computes from the present operands; this matches spreadsheet + `MAX`/`MIN` and the audited usage. `coalesce` is the operator for "treat absent as + a value". +- `round` precision is the number of decimals (PHP `round($v, $p)`); negative + precision is allowed (PHP semantics) but precision MUST be an integer. +- `year` returns an `int` (e.g. `2026`), reusing `toDateOrNull()`. +- `monthsElapsed` is **whole calendar months** (signed not required by the audited + calcs, but for symmetry with `diffDays`/`calendarDiff` it returns a signed int: + positive when `later` is after `earlier`). It reuses the existing + `calendarDiff(..., 'months')` logic — no new date arithmetic. + +### Placement + +Add seven arms to the `match` in `evaluate()` and seven small private handlers +following the file's one-handler-per-operator style. `max`/`min` share a numeric- +operand collector; `monthsElapsed` delegates to the existing month branch of +`calendarDiff()`. No public signature changes. + +## Risks / Trade-offs + +- **`monthsElapsed` calendar vs 30-day ambiguity** → Decision: **calendar whole + months** (reusing `calendarDiff`), consistent with `dateDiff(unit:'months')`. A + 30-day-bucket variant, if ever needed, is `(/ (diffDays a b) 30)` and needs no new + operator. Flagged as a deferred question. +- **`max`/`min` null-skip vs null-propagate** → null-skip chosen (spreadsheet + parity). Documented so reviewers do not "fix" it to propagate. Deferred question. +- **PHPMD class-complexity** → the class already carries + `@SuppressWarnings(PHPMD.ExcessiveClassComplexity)` for its 20+ operator dispatch; + seven more arms stay within that documented suppression. No new suppression needed + beyond per-method cyclomatic where guard chains require it. + +## Migration Plan + +Additive — deploy is a code change with no data migration. Existing objects are +unaffected until a schema declares a calc using a new operator and the object is +re-saved (or re-materialised via the existing `occ +openregister:rematerialise-calculations`). Rollback = revert the file; any object +whose materialised field used a new operator simply stops updating (the stored value +remains). No schema or DB rollback. + +## Open Questions + +- `monthsElapsed`: calendar-whole-months (chosen) vs 30-day-period — confirm with + the shillinq calc audit owner. Provisional: calendar. +- `max`/`min` null operand: skip (chosen) vs propagate-null — confirm against the + audited calcs. Provisional: skip. + +## Test Plan + +Unit tests in `tests/Unit/Service/Calculation/` (extend +`CalculationEvaluatorTest.php` or add a sibling `ScalarFunctionsTest.php`), one group +per operator: +- `max`/`min`: multiple numerics; with a null operand (skipped); all-null → null; + non-numeric operand → `EvaluationException`; non-array args → exception. +- `coalesce`: first non-null returned; nulls then a value; all-null → null. +- `abs`: positive, negative, zero; null → null; non-numeric → exception. +- `round`: default precision (0); explicit precision (2); negative precision; null + value → null; non-numeric value → exception; non-integer precision → exception. +- `year`: ISO date string; `DateTimeImmutable`; `@self.created`; unparseable → null. +- `monthsElapsed`: exact months; partial month (floors); reversed order (negative); + unparseable operand → null; one operand → exception. diff --git a/openspec/changes/calc-engine-scalar-functions/proposal.md b/openspec/changes/calc-engine-scalar-functions/proposal.md new file mode 100644 index 0000000000..ec5cdcbee4 --- /dev/null +++ b/openspec/changes/calc-engine-scalar-functions/proposal.md @@ -0,0 +1,61 @@ +--- +kind: code +--- + +## Why + +OpenRegister's per-object calculation engine (`CalculationEvaluator`, a JSON-AST +interpreter behind `x-openregister-calculations`) supports arithmetic, comparison, +logical, string, and a few date operators — but lacks the common scalar functions +that derived fields routinely need: `max`, `min`, `coalesce`, `abs`, `round`, +`year`, and `monthsElapsed`. A fleet audit (shillinq) found ~43 declarative +calculations that cannot be expressed today because these primitives are missing, +forcing apps back to imperative PHP (against ADR-031). Adding them is purely +additive and unlocks those calcs fleet-wide. + +## What Changes + +- Add **seven new per-object scalar operators** to `CalculationEvaluator::evaluate()`'s + `match($op)` dispatch, each a pure function over the object payload: + - `max` — largest of N numeric sub-expression operands. + - `min` — smallest of N numeric sub-expression operands. + - `coalesce` — first non-null operand of N sub-expressions (null-fallback). + - `abs` — absolute value of one numeric operand. + - `round` — round one numeric operand to an optional second `precision` operand (default 0). + - `year` — extract the four-digit year (integer) from one date operand. + - `monthsElapsed` — whole calendar months between two date operands (later, earlier). +- Each operator follows the established evaluator conventions: operands are + sub-expressions evaluated via `evaluate()`, malformed arity / non-numeric / + non-date inputs raise `EvaluationException`, and unparseable date operands + return `null` (matching `diffDays`/`formatDate`). +- Add unit-test coverage for each new operator (happy path + null/edge cases) in + the evaluator test suite. +- **Non-goal (explicitly out of scope):** cross-object folding operators + (`sum`/`lookup`/`map` over *other* objects). Those belong to the aggregation + engine (`x-openregister-aggregations`), not this per-object evaluator. + +This change is **backward-compatible** — additive operators only. No existing +operator, signature, or behaviour changes. No new routes, schemas, or dependencies. + +## Capabilities + +### New Capabilities + + +### Modified Capabilities +- `computed-fields`: the `CalculationEvaluator` vocabulary (currently `prop, lit, + concat, if, not, and, or, +, -, *, /, %, eq/ne/lt/lte/gt/gte, now, diffDays, + formatDate, dateDiff`) gains seven scalar operators (`max, min, coalesce, abs, + round, year, monthsElapsed`). This is a requirement-level extension of the + "String, Date, and Math Operations" surface for the JSON-AST evaluator path. + +## Impact + +- **Code:** `lib/Service/Calculation/CalculationEvaluator.php` (add operator arms + + private handlers). No change to `CalculationOnSaveListener` (the `materialise: true` + gate, serialisation, and dispatch are operator-agnostic). +- **Tests:** `tests/Unit/Service/Calculation/CalculationEvaluatorTest.php` (and/or a + new sibling test class) — one group of cases per new operator. +- **Dependent apps:** every Conduction app that declares `x-openregister-calculations` + benefits; none break (additive). shillinq's ~43 stalled calcs become expressible. +- **No** DB, API, schema, or dependency changes. diff --git a/openspec/changes/calc-engine-scalar-functions/specs/computed-fields/spec.md b/openspec/changes/calc-engine-scalar-functions/specs/computed-fields/spec.md new file mode 100644 index 0000000000..4912105797 --- /dev/null +++ b/openspec/changes/calc-engine-scalar-functions/specs/computed-fields/spec.md @@ -0,0 +1,53 @@ +## ADDED Requirements + +### Requirement: Scalar function operators in the calculation evaluator +The `CalculationEvaluator` JSON-AST interpreter SHALL support seven additional pure per-object scalar operators — `max`, `min`, `coalesce`, `abs`, `round`, `year`, and `monthsElapsed` — dispatched from `evaluate()` alongside the existing operator vocabulary. Each operator SHALL evaluate its operands as sub-expressions through `evaluate()`, SHALL raise `EvaluationException` on malformed arity or wrong-typed (non-null) operands, and SHALL return `null` for unparseable date inputs (matching the existing `diffDays` / `formatDate` convention). These operators MUST be additive: no existing operator's name, arity, or behaviour changes. The operators SHALL NOT perform cross-object folding (summing or looking up values on OTHER objects); cross-object reduction remains the responsibility of `x-openregister-aggregations`. + +#### Scenario: max returns the largest numeric operand +- **WHEN** an expression `{ "max": [ { "prop": "a" }, { "prop": "b" }, 5 ] }` is evaluated against an object where `a = 3` and `b = 9` +- **THEN** the result MUST be `9` + +#### Scenario: max skips null operands and returns null when all operands are null +- **WHEN** `{ "max": [ { "prop": "missing1" }, { "prop": "missing2" } ] }` is evaluated and both properties are absent +- **THEN** the result MUST be `null` +- **AND** when at least one operand is present, only the present operands MUST be considered + +#### Scenario: min returns the smallest numeric operand +- **WHEN** `{ "min": [ 10, { "prop": "a" }, 4 ] }` is evaluated against an object where `a = 7` +- **THEN** the result MUST be `4` + +#### Scenario: non-numeric operand to max raises an evaluation error +- **WHEN** `{ "max": [ 1, "abc" ] }` is evaluated and `"abc"` is a non-null non-numeric value +- **THEN** an `EvaluationException` MUST be raised + +#### Scenario: coalesce returns the first non-null operand +- **WHEN** `{ "coalesce": [ { "prop": "missing" }, { "prop": "fallback" } ] }` is evaluated against an object where `missing` is absent and `fallback = "x"` +- **THEN** the result MUST be `"x"` + +#### Scenario: coalesce returns null when every operand is null +- **WHEN** `{ "coalesce": [ { "prop": "a" }, { "prop": "b" } ] }` is evaluated and both are absent +- **THEN** the result MUST be `null` + +#### Scenario: abs returns the absolute value +- **WHEN** `{ "abs": [ { "prop": "variance" } ] }` is evaluated against an object where `variance = -12.5` +- **THEN** the result MUST be `12.5` +- **AND** a null operand MUST yield `null` + +#### Scenario: round honours an optional precision operand +- **WHEN** `{ "round": [ { "prop": "amount" }, 2 ] }` is evaluated against an object where `amount = 3.14159` +- **THEN** the result MUST be `3.14` +- **AND** `{ "round": [ 3.6 ] }` (no precision) MUST return `4` +- **AND** a non-integer precision operand MUST raise an `EvaluationException` + +#### Scenario: year extracts the four-digit year from a date +- **WHEN** `{ "year": [ { "prop": "@self.created" } ] }` is evaluated and `@self.created` is the ISO date `2026-06-20T10:00:00+00:00` +- **THEN** the result MUST be the integer `2026` +- **AND** an unparseable or absent date operand MUST yield `null` + +#### Scenario: monthsElapsed returns whole calendar months between two dates +- **WHEN** `{ "monthsElapsed": [ "2026-06-20", "2026-01-20" ] }` is evaluated +- **THEN** the result MUST be the integer `5` +- **AND** a partial trailing month MUST be floored (e.g. `2026-06-19` vs `2026-01-20` MUST yield `4`) +- **AND** when the later operand precedes the earlier operand the result MUST be negative +- **AND** an unparseable operand MUST yield `null` +- **AND** fewer than two operands MUST raise an `EvaluationException` diff --git a/openspec/changes/calc-engine-scalar-functions/tasks.md b/openspec/changes/calc-engine-scalar-functions/tasks.md new file mode 100644 index 0000000000..460d86f395 --- /dev/null +++ b/openspec/changes/calc-engine-scalar-functions/tasks.md @@ -0,0 +1,29 @@ +## 1. Evaluator operators + +- [ ] 1.1 Add `max` and `min` arms to `evaluate()`'s `match($op)` plus a shared numeric-operand collector (null operands skipped; all-null → `null`; non-null non-numeric → `EvaluationException`) in `lib/Service/Calculation/CalculationEvaluator.php`. +- [ ] 1.2 Add `coalesce` arm + handler (first non-null sub-expression result; all-null → `null`; non-array args → `EvaluationException`). +- [ ] 1.3 Add `abs` arm + handler (single numeric operand; `null` → `null`; non-null non-numeric → `EvaluationException`). +- [ ] 1.4 Add `round` arm + handler (`[value]` or `[value, precision]`; default precision 0; `null` value → `null`; non-numeric value or non-integer precision → `EvaluationException`). +- [ ] 1.5 Add `year` arm + handler (extract integer year via `toDateOrNull()`; unparseable/`null` date → `null`). +- [ ] 1.6 Add `monthsElapsed` arm + handler reusing the existing `calendarDiff(..., 'months')` month logic (`[later, earlier]`; signed whole months; unparseable/`null` → `null`; fewer than two operands → `EvaluationException`). + +## 2. Tests + +- [ ] 2.1 Add `max`/`min` unit cases (multiple operands, null-skip, all-null→null, non-numeric→exception, non-array→exception) in `tests/Unit/Service/Calculation/`. +- [ ] 2.2 Add `coalesce` cases (first non-null, nulls-then-value, all-null→null). +- [ ] 2.3 Add `abs` and `round` cases (sign/zero, null→null, non-numeric→exception; default vs explicit vs negative precision, non-integer precision→exception). +- [ ] 2.4 Add `year` and `monthsElapsed` cases (ISO string, `DateTimeImmutable`, `@self.created`, unparseable→null; exact/partial/reversed months, one-operand→exception). + +## 3. Validation + +- [ ] 3.1 Run the evaluator test suite and confirm all new cases pass. +- [ ] 3.2 Run `composer check:strict` (PHPCS, PHPMD, Psalm, PHPStan) on the changed evaluator file and fix any new findings. + +Acceptance criteria: +- All seven operators dispatch from `evaluate()` and match the existing arg/null/error conventions. +- No existing operator's name, arity, or behaviour changes (backward-compatible). +- Cross-object folding (`sum`/`lookup`/`map`) is NOT added. + +Quality: +- New code carries EUPL-1.2 SPDX header (file already has one) and full PHPDoc per OR conventions. +- No new PHPMD/PHPStan/Psalm regressions beyond the class's existing documented suppressions. diff --git a/openspec/changes/cleanup-linked-entity-type-map/hydra.json b/openspec/changes/cleanup-linked-entity-type-map/hydra.json deleted file mode 100644 index df91f96840..0000000000 --- a/openspec/changes/cleanup-linked-entity-type-map/hydra.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema_version": 2, - "spec_slug": "cleanup-linked-entity-type-map", - "app": "openregister", - "repo": "https://github.com/ConductionNL/openregister", - "depends_on": [ - "pluggable-integration-registry" - ], - "issue": "https://github.com/ConductionNL/openregister/issues/1327", - "pipeline": {}, - "cycles": [] -} diff --git a/openspec/changes/cleanup-linked-entity-type-map/specs/cleanup-linked-entity-type-map/spec.md b/openspec/changes/cleanup-linked-entity-type-map/specs/cleanup-linked-entity-type-map/spec.md deleted file mode 100644 index 74778257a2..0000000000 --- a/openspec/changes/cleanup-linked-entity-type-map/specs/cleanup-linked-entity-type-map/spec.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: proposed ---- - -# Cleanup: Remove LinkedEntityService::TYPE_COLUMN_MAP - -## Purpose - -Complete the migration away from the hardcoded type-column constant to the `IntegrationRegistry`. Pure removal — no behaviour change. - -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Constants Removed - -`LinkedEntityService::TYPE_COLUMN_MAP` and `Schema::VALID_LINKED_TYPES` SHALL be absent from the codebase after this change. - -#### Scenario: Grep confirms absence - -- **WHEN** the codebase is grep'd for `TYPE_COLUMN_MAP` or `VALID_LINKED_TYPES` -- **THEN** zero matches MUST exist in OR core or `@conduction/nextcloud-vue` - -### Requirement: Registry-Driven Behaviour Unchanged - -All integration discovery and schema validation SHALL continue to function via `IntegrationRegistry`. - -#### Scenario: Existing schemas continue to validate - -- **GIVEN** a schema with `configuration.linkedTypes: ["files", "notes"]` -- **WHEN** the schema is saved after this change -- **THEN** validation MUST succeed via `IntegrationRegistry::listIds()` - -### Requirement: Pre-Removal Grep Sweep - -A grep sweep of the ConductionNL organisation SHALL be run before the removal commit, and any remaining references outside OR core MUST be migrated before removal. diff --git a/openspec/changes/cleanup-linked-entity-type-map/specs/generic-integrations/spec.md b/openspec/changes/cleanup-linked-entity-type-map/specs/generic-integrations/spec.md deleted file mode 100644 index 59fafa6b4a..0000000000 --- a/openspec/changes/cleanup-linked-entity-type-map/specs/generic-integrations/spec.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: proposed ---- - -# Cleanup: Remove LinkedEntityService::TYPE_COLUMN_MAP - -## Purpose - -Complete the migration away from the hardcoded type-column constant to the `IntegrationRegistry`. Pure removal — no behaviour change. - -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Constants Removed - -`LinkedEntityService::TYPE_COLUMN_MAP` and `Schema::VALID_LINKED_TYPES` SHALL be absent from the codebase after this change. - -#### Scenario: Grep confirms absence - -- **WHEN** the codebase is grep'd for `TYPE_COLUMN_MAP` or `VALID_LINKED_TYPES` -- **THEN** zero matches MUST exist in OR core or `@conduction/nextcloud-vue` - -### Requirement: Registry-Driven Behaviour Unchanged - -All integration discovery and schema validation SHALL continue to function via `IntegrationRegistry`. - -#### Scenario: Existing schemas continue to validate - -- **GIVEN** a schema with `configuration.linkedTypes: ["files", "notes"]` -- **WHEN** the schema is saved after this change -- **THEN** validation MUST succeed via `IntegrationRegistry::listIds()` - -### Requirement: Pre-Removal Grep Sweep - -A grep sweep of the ConductionNL organisation SHALL be run before the removal commit, and any remaining references outside OR core MUST be migrated before removal. diff --git a/openspec/changes/cleanup-linked-entity-type-map/tasks.md b/openspec/changes/cleanup-linked-entity-type-map/tasks.md deleted file mode 100644 index ccc3e065b4..0000000000 --- a/openspec/changes/cleanup-linked-entity-type-map/tasks.md +++ /dev/null @@ -1,27 +0,0 @@ -# Tasks: Cleanup — Remove LinkedEntityService::TYPE_COLUMN_MAP - -## Pre-removal verification - -- [ ] Grep ConductionNL org for `TYPE_COLUMN_MAP` and `VALID_LINKED_TYPES` — list any remaining references -- [ ] For each remaining reference, open a migration issue pointing to `IntegrationRegistry::listIds()` -- [ ] Wait for migration issues to close before proceeding with the removal commit -- [ ] Confirm all 5 built-in providers (files, notes, tasks, tags, audit-trail) are in production and stable - -## Removal - -- [ ] Remove `const TYPE_COLUMN_MAP` from `lib/Service/LinkedEntityService.php` -- [ ] Remove `const VALID_LINKED_TYPES` from `lib/Db/Schema.php` (if present) -- [ ] Update any `@deprecated` docblocks to reflect removal -- [ ] Search-and-remove any import / reference patterns that the constants previously satisfied - -## Verification - -- [ ] PHPCS / PHPMD / PHPStan / Psalm strict pass -- [ ] Backwards-compat snapshot tests on `CnObjectSidebar` pass -- [ ] `Schema::validateLinkedTypesValue()` tests still pass (registry path) -- [ ] No regression in integration discovery via `/api/integrations` or OCS capabilities - -## Documentation - -- [ ] Update developer docs / READMEs mentioning either constant -- [ ] CHANGELOG note: "BREAKING for anyone reading private API constants; no public API change" diff --git a/openspec/changes/complete-live-updates/proposal.md b/openspec/changes/complete-live-updates/proposal.md new file mode 100644 index 0000000000..6e37d2d405 --- /dev/null +++ b/openspec/changes/complete-live-updates/proposal.md @@ -0,0 +1,76 @@ +--- +kind: code +--- + +## Why + +The `add-live-updates` change (archived 2026-06-14) shipped the notify_push +transport but left task 6 half-wired: `ImportService` toggles +`NotifyPushListener::setBatchMode(true)` around its two bulk-save call sites, +then calls `setBatchMode(false)` — which **clears the accumulated collection +events without ever emitting them** (`flushBatch()` was implemented on the +listener but never called; the call sites carried a +`@todo add-live-updates/task-6`). Net effect: bulk imports emit ZERO push +events, so connected clients never learn an import happened, while single-object +saves push fine. + +Separately, the repo carries a second, older realtime write path from the +archived `2026-05-01-realtime-updates` change: a DB-backed cursor-polling +change feed (`RealtimeService` + `RealtimeEventListener` + +`RealtimeController` at `/api/realtime/events` + `/api/realtime/cursor` + +daily `RealtimeEventRetentionJob` + `openregister_realtime_events` table). +It writes a CloudEvent row on EVERY object change (4 lifecycle events) and has +**zero consumers** — verified by grepping the entire apps-extra workspace plus +this repo's `src/` and `website/` for `realtime/events`, `RealtimeService`, +`RealtimeController`, and `api/realtime`: every hit is openregister's own +implementation, its tests, archived openspec docs, or coverage reports. It is +pure write amplification on the hot object-save path. + +## What Changes + +- **Fix the import batch flush (Task 1).** `ImportService` gains a lazily + resolved DI container reference and a `flushNotifyPushBatch()` helper. Both + bulk-save call sites (`processSpreadsheetBatch`, `processCsvSheet`) now call + the helper in their `finally` block — BEFORE `setBatchMode(false)` — so on + import completion (success or failure; partial saves still happened) exactly + one deduplicated `or-collection-{register-slug}-{schema-slug}` broadcast is + pushed per affected pair. Crucially, the collection hint is DERIVED FROM THE + SAVE RESULT, not from lifecycle events: the entire import call chain defaults + `events=false` (UI, `RegistersController`, `SaveObjects`), so + `NotifyPushListener::handle()` never fires during a standard import — the + import queues the pair itself via the new + `NotifyPushListener::addBatchedCollection()` when the save result contains + saved/updated rows (or conservatively when the save throws; all-unchanged + imports emit nothing). The flush keeps the archived change's design + decisions: collection events are broadcast without per-user targeting + (payload is slugs + action only; clients refetch through the RBAC-filtered + REST API), and everything soft-fails when notify_push is absent (no + accumulation happens in that case, so the flush is a silent no-op; a resolve + failure with pending events logs at most one DEBUG entry). + `NotifyPushListener::flushBatch()` drops its never-used `PermissionHandler` + parameter and gains `hasBatchedCollections()` / `addBatchedCollection()`. + +- **Remove the orphaned RealtimeService write path (Task 2).** Deletes + `RealtimeService`, `RealtimeController` (+ its two routes), + `RealtimeEventListener` (+ its four registrations in `Application.php`), + `RealtimeEvent`/`RealtimeEventMapper`, `RealtimeEventRetentionJob` (+ its + `info.xml` registration), the table-creating migration + (`Version1Date20260430000000` — fresh installs no longer create the dead + table), and the two tests covering the subsystem. A new migration + (`Version1Date20260714120000`) drops the orphaned + `openregister_realtime_events` table (and thereby its indexes) on instances + that ran development between May and July 2026 — idempotent, drops only when + present. Spec deltas REMOVE the requirements that mandated this subsystem + from `realtime-updates`, `production-observability`, and + `retention-management`. + +## Impact + +- Affected specs: `realtime-updates` (1 MODIFIED, 1 REMOVED requirement), + `production-observability` (1 REMOVED), `retention-management` (1 REMOVED) +- Affected code: `lib/Service/ImportService.php`, + `lib/Listener/NotifyPushListener.php`, `lib/AppInfo/Application.php`, + `appinfo/routes.php`, `appinfo/info.xml`, deletions listed above +- Behaviour: bulk imports now notify connected clients (one collection event + per affected register/schema pair); every object save stops paying a + realtime-log DB insert diff --git a/openspec/changes/complete-live-updates/specs/production-observability/spec.md b/openspec/changes/complete-live-updates/specs/production-observability/spec.md new file mode 100644 index 0000000000..0c2d0ff1f1 --- /dev/null +++ b/openspec/changes/complete-live-updates/specs/production-observability/spec.md @@ -0,0 +1,16 @@ +# Production Observability — delta + +## REMOVED Requirements + +### Requirement: Realtime change records MUST be emitted as CloudEvent-shaped envelopes + +**Reason**: `RealtimeService` and its `openregister_realtime_events` store are +removed in this change — the CloudEvent rows were written on every object save +but never read (the cursor-polling endpoints had zero consumers across the +entire apps-extra workspace). Realtime delivery is covered by the notify_push +transport (`NotifyPushListener`) and the GraphQL SSE subscription path; audit +history remains in the audit trail. + +**Migration**: None for consumers — none existed. The orphaned +`openregister_realtime_events` table is dropped by migration +`Version1Date20260714120000` (idempotent, drops only when present). diff --git a/openspec/changes/complete-live-updates/specs/realtime-updates/spec.md b/openspec/changes/complete-live-updates/specs/realtime-updates/spec.md new file mode 100644 index 0000000000..f0501f84a3 --- /dev/null +++ b/openspec/changes/complete-live-updates/specs/realtime-updates/spec.md @@ -0,0 +1,98 @@ +# Realtime Updates — delta + +## MODIFIED Requirements + +### Requirement: The system MUST support a batch-mode flag to suppress per-object pushes during bulk import + +Callers running bulk import operations MUST suppress per-object push delivery by +setting batch mode. On import completion — including the failure path, since +partial saves may already have happened — the import caller MUST flush the +accumulated collection events: a single `or-collection-{register-slug}-{schema-slug}` +event per affected `(register, schema)` pair, deduplicated across all saved objects. + +The batch flush is an untargeted broadcast: the pushed message carries no +per-user targeting and its payload contains only the register slug, the schema +slug, and the action `batch` — never object data. Authorization is enforced at +refetch time: clients receiving the event re-query the RBAC-filtered REST API, +so subscribers without access simply get an empty page. + +The flush MUST soft-fail when notify_push is not installed: no events are +accumulated in that case (the listener never reaches its accumulator), so the +flush is a silent no-op without touching the container; a queue-resolution +failure with pending events logs at most one DEBUG entry and MUST NOT interrupt +the import. + +#### Scenario: Bulk import with batch mode + +- **GIVEN** batch mode is enabled via `NotifyPushListener::setBatchMode(true)` +- **WHEN** 500 objects in schema `meldingen` (register `zaken`) are saved in a loop +- **THEN** `IQueue::push()` MUST NOT be called during the loop +- **WHEN** `NotifyPushListener::flushBatch($queue)` is called +- **THEN** `IQueue::push('notify_custom', ...)` MUST be called exactly once with + message `or-collection-zaken-meldingen` +- **AND** the push payload MUST NOT contain a `user` key (broadcast) +- **AND** the payload body MUST be exactly `{action: "batch", register: "zaken", schema: "meldingen"}` +- **AND** per-object `or-object-{uuid}` events MUST NOT be emitted for any of the 500 objects + +#### Scenario: Import service flushes on completion + +- **GIVEN** `ImportService` runs a bulk save (`processSpreadsheetBatch` or `processCsvSheet`) +- **WHEN** the save completes (normally or by throwing) +- **THEN** the `finally` block MUST call the flush BEFORE `setBatchMode(false)` + (disabling batch mode clears the accumulator) +- **AND** one collection event per affected `(register, schema)` pair MUST be pushed +- **AND** subsequent single-object saves in the same request MUST emit per-object + events again (batch mode is off) + +#### Scenario: Collection hint derived from the save result, not from lifecycle events + +- **GIVEN** a default import (`events=false` throughout the import call chain, + as sent by the UI and `RegistersController`), so `NotifyPushListener::handle()` + never fires during the bulk save +- **WHEN** the bulk save returns with a non-empty `saved` or `updated` set +- **THEN** the import MUST queue the `(register-slug, schema-slug)` pair + directly from its own register/schema entities + (`NotifyPushListener::addBatchedCollection()`), independent of event dispatch +- **AND** when the bulk save throws (partial saves may have landed), the pair + MUST be queued conservatively +- **AND** when every row is `unchanged` (smart dedup skipped all writes), NO + collection event MUST be emitted +- **AND** when lifecycle events ARE enabled, listener accumulation and the + result-derived hint MUST collapse onto the same accumulator key (no double + emit) + +#### Scenario: Flush soft-fails without notify_push + +- **GIVEN** notify_push is not installed (`IQueue` not resolvable) +- **WHEN** a bulk import completes +- **THEN** the flush MUST be a silent no-op (nothing was accumulated, so the + container is not even queried) +- **AND** a queue-resolution failure with pending events MUST log at most one + DEBUG entry (never WARNING or ERROR) +- **AND** the import result MUST be unaffected + +#### Scenario: Import without batch mode causes write amplification (anti-pattern) + +- **GIVEN** batch mode is NOT enabled +- **WHEN** 500 objects in schema `meldingen` are saved in a loop +- **THEN** `IQueue::push()` MUST be called up to `500 × N_readers` times +- **AND** this MUST be documented as the rationale for batch mode + +## REMOVED Requirements + +### Requirement: The system MUST record object lifecycle events as CloudEvents in the realtime log + +**Reason**: The DB-backed realtime event log (`RealtimeEventListener` → +`RealtimeService::record()` → `openregister_realtime_events`) wrote a row on +every object lifecycle event but had zero consumers — the cursor-polling +endpoints it fed (`/api/realtime/events`, `/api/realtime/cursor`) were never +called by any frontend or sibling app (verified by grepping the entire +apps-extra workspace). The notify_push transport (`NotifyPushListener`) and the +GraphQL SSE subscription path cover realtime delivery without per-save DB write +amplification. The subsystem is removed; migration `Version1Date20260714120000` +drops the orphaned `openregister_realtime_events` table (and its indexes) on +instances that still carry it. + +**Migration**: None required — no consumers existed. Clients wanting a change +feed subscribe to `or-object-{uuid}` / `or-collection-{register}-{schema}` +notify_push events and refetch via the RBAC-filtered REST API. diff --git a/openspec/changes/complete-live-updates/specs/retention-management/spec.md b/openspec/changes/complete-live-updates/specs/retention-management/spec.md new file mode 100644 index 0000000000..ff2e6c9495 --- /dev/null +++ b/openspec/changes/complete-live-updates/specs/retention-management/spec.md @@ -0,0 +1,15 @@ +# Retention Management — delta + +## REMOVED Requirements + +### Requirement: The system MUST prune the realtime events log daily + +**Reason**: The `RealtimeEventRetentionJob` pruned the +`openregister_realtime_events` table, which is removed from the write path in +this change (`RealtimeService`/`RealtimeEventListener` deleted — the log had +zero consumers). With nothing writing to the table, there is nothing to prune; +the job and its `info.xml` registration are deleted. This is independent of the +AVG retention pass and the Archiefwet destruction workflow, which are unchanged. + +**Migration**: The leftover table is dropped by migration +`Version1Date20260714120000` (idempotent, drops only when present). diff --git a/openspec/changes/complete-live-updates/tasks.md b/openspec/changes/complete-live-updates/tasks.md new file mode 100644 index 0000000000..93ef0b769d --- /dev/null +++ b/openspec/changes/complete-live-updates/tasks.md @@ -0,0 +1,75 @@ +# Tasks: Complete Live Updates + +## 1. Fix the import batch flush + +- [x] 1.1 Add `NotifyPushListener::hasBatchedCollections(): bool` guard accessor + so import callers can skip IQueue resolution entirely when nothing was + accumulated (notify_push absent → zero logs, zero container churn). +- [x] 1.2 Simplify `NotifyPushListener::flushBatch()` signature to + `flushBatch(object $queue): void` — the `PermissionHandler` parameter was + never used (batch flush is an untargeted broadcast by design) and carried a + PHPMD `UnusedFormalParameter` suppression. +- [x] 1.3 Inject `Psr\Container\ContainerInterface` into `ImportService` and add + `flushNotifyPushBatch()`: early-return when nothing accumulated, lazily + resolve `OCA\NotifyPush\Queue\IQueue`, soft-fail with at most one DEBUG log, + delegate to `NotifyPushListener::flushBatch()`. +- [x] 1.4 Wire both bulk-save call sites (`processSpreadsheetBatch`, + `processCsvSheet`): call `flushNotifyPushBatch()` inside the existing + `finally` block BEFORE `setBatchMode(false)` (which clears the accumulator). + Remove the `@todo add-live-updates/task-6` markers. +- [x] 1.5 Extend `tests/Unit/Listener/NotifyPushListenerTest.php`: flush emits + exactly one deduplicated event per (register, schema) pair, broadcasts + without a `user` key, payload is slugs + `action: batch` only, accumulator + cleared after flush, `hasBatchedCollections()` state transitions. +- [x] 1.6 Add `tests/Unit/Service/ImportServiceNotifyPushBatchTest.php` proving + the end-to-end contract through `importFromCsv()`: batch mode on → N object + saves emit zero pushes during the save → completion flushes exactly the + deduplicated collection events and zero per-object events; per-object pushes + resume after the import; failure path (`saveObjects` throws after partial + saves) still flushes. +- [x] 1.7 Decouple hint accumulation from the `events` flag: the default import + path runs `events=false` end to end (UI → `RegistersController` → + `ImportService` → `SaveObjects`), so the listener never accumulates on its + own. Add `NotifyPushListener::addBatchedCollection()` and + `ImportService::queueNotifyPushCollectionHint()`; both call sites derive the + `(register-slug, schema-slug)` pair from the save RESULT (saved/updated + non-empty, or conservatively when the save throws; all-unchanged emits + nothing). Tests exercise the real default flags (no manual event dispatch), + the failure path, the all-unchanged case, and the events-enabled variant + (result hint + listener accumulation deduplicate — no double emit). + +## 2. Remove the orphaned RealtimeService write path + +- [x] 2.1 Verify zero consumers: grep all of apps-extra plus this repo's `src/` + and `website/` for `realtime/events`, `RealtimeService`, `RealtimeController`, + `api/realtime` — only openregister's own implementation/tests/archived docs + matched. +- [x] 2.2 Delete `lib/Service/RealtimeService.php`, + `lib/Controller/RealtimeController.php`, `lib/Db/RealtimeEvent.php`, + `lib/Db/RealtimeEventMapper.php`, `lib/Listener/RealtimeEventListener.php`, + `lib/BackgroundJob/RealtimeEventRetentionJob.php`. +- [x] 2.3 Remove the two `/api/realtime/*` routes from `appinfo/routes.php`, the + four `RealtimeEventListener` registrations + import from + `lib/AppInfo/Application.php`, and the `RealtimeEventRetentionJob` entry from + `appinfo/info.xml`. +- [x] 2.4 Delete the `openregister_realtime_events` create-migration + (`lib/Migration/Version1Date20260430000000.php`) so fresh installs no longer + create the dead table, and add `lib/Migration/Version1Date20260714120000.php` + dropping the orphaned table (and its indexes) on instances that still carry + it — idempotent, drops only when present. +- [x] 2.5 Delete `tests/Service/RealtimeUpdatesIntegrationTest.php` and + `tests/Service/RealtimeEventRetentionJobTest.php`; drop the stale + `RealtimeEventMapper` entry from `phpstan-baseline.neon`. +- [x] 2.6 Spec deltas: REMOVE the realtime-log recorder requirement from + `realtime-updates`, the CloudEvent-envelope requirement from + `production-observability`, and the daily prune requirement from + `retention-management`; MODIFY the batch-mode requirement in + `realtime-updates` to codify the import-completion flush and broadcast + semantics. +- [x] 2.7 Update `docs/features/realtime-updates.md` — drop the removed/never-built + `/api/realtime/*` endpoint listings, document the actual transports. + +## 3. Verification + +- [x] 3.1 `composer check:strict` clean on touched files. +- [x] 3.2 PHPUnit unit suite green (new tests pass, no regressions). diff --git a/openspec/changes/config-import-seed-objects/.openspec.yaml b/openspec/changes/config-import-seed-objects/.openspec.yaml new file mode 100644 index 0000000000..18edba1f54 --- /dev/null +++ b/openspec/changes/config-import-seed-objects/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-20 diff --git a/openspec/changes/config-import-seed-objects/design.md b/openspec/changes/config-import-seed-objects/design.md new file mode 100644 index 0000000000..ff90fb3853 --- /dev/null +++ b/openspec/changes/config-import-seed-objects/design.md @@ -0,0 +1,146 @@ +## Context + +Apps ship starter/seed data inside their register JSON +(`lib/Settings/{app}_register.json`). At app init the app calls +`AppHostSettingsService::loadConfiguration(force: true)` +(`lib/AppHost/Service/AppHostSettingsService.php:185`) → +`ConfigurationService::importFromApp()` +(`lib/Service/ConfigurationService.php:569-577`) → `ImportHandler::importFromApp()` +→ `ImportHandler::importFromJson()`. + +### Root cause (file:line evidence) + +`ImportHandler::importFromJson()` imports each component by reading a fixed set of +keys under `data['components']`: + +- schemas — `lib/Service/Configuration/ImportHandler.php:1602` (`$data['components']['schemas']`) +- registers — `:1772` (`$data['components']['registers']`) +- workflows — `:1852`, mappings — `:1871` +- **objects — `:1960`** (`$data['components']['objects']`), then `saveObject()` at `:2108` / `:2145`. + +The object loop is **only** fed from `data['components']['objects']`. There is **no +read of the top-level `data['objects']` key anywhere on the import path** — a repo +grep finds top-level `['objects']` only in `PreviewHandler.php` (preview display, +`:194`/`:211`) and in `result['objects']` accumulators. The export side confirms the +canonical location is nested: `ExportHandler.php:359` writes +`$openApiSpec['components']['objects'][]`. + +shillinq's `lib/Settings/shillinq_register.json` places all **78** seed objects in +the **top-level `objects`** array (verified: top-level keys include `objects`; +`components` has `schemas` + bare schema-name keys but no `components.objects`). All +78 carry an `@self.slug`. So: + +1. Schemas import (the `components.schemas` branch runs) — matching the observed + "schemas DO import". +2. The object loop reads `components.objects`, finds it absent, and imports **0** + objects — matching the observed "78 seed objects yield 0 rows, incl. `SalesOrderLine`". + +This is a **pure location mismatch**: the app authored objects at the top level +(the same shape the loop expects, just one level up), and the importer never looks +there. It is **not** the slug guard (`:1969-1971`, which skips slug-less objects), +not RBAC, and not the no-user-context path (those are already handled). There is +also a third, unrelated seed path — `x-openregister.seedData.objects` keyed by +schema slug, `importSeedData()` at `:3714`/`:3807` — which shillinq does not use; +this change does not touch it. + +### Where the fix lands + +**Purely in OpenRegister** — `ImportHandler::importFromJson()`. Immediately before +the object loop (~`:1960`), normalise the seed-object source by merging a top-level +`data['objects']` array into `data['components']['objects']` (top-level entries +appended; `components.objects` kept as-is). The existing loop — register/schema +resolution via maps + DB fallback, search-by-(register, schema, slug), version +compare, `saveObject()` with `_rbac:false`/`_multitenancy:false`, per-entity +try/catch, `@ref` token resolution at `:1948` — then handles both sources unchanged. + +**No app-side change is required.** `loadConfigurationForced` / +`AppHostSettingsService::loadConfiguration` and the `importFromApp` signature stay +the same; the forced path already calls the loop. Apps need not move their objects +to `components.objects` (though that remains valid and canonical). Stated explicitly +so consumers know this is not a dependency on a coordinated app change. + +## Goals / Non-Goals + +**Goals:** +- Make a top-level `objects` array an accepted seed-object source on the + app-init/forced import path, folded into the existing `components.objects` loop. +- Idempotent re-import: match by `@self` identity (slug within register+schema, with + uuid fallback), update-or-skip, never duplicate. +- Prove both with tests. + +**Non-Goals:** +- Changing the `components.objects` branch, the slug-skip guard, RBAC/multitenancy + bypass, acting-user resolution, or `@ref` resolution. +- Touching the `x-openregister.seedData.objects` path (`importSeedData()`). +- Changing export, `PreviewHandler`, or any public method signature. + +## Decisions + +### ADR-031 declarative-vs-imperative decision + +This change is import-plumbing inside OpenRegister core, not app business logic, so +the declarative-vs-imperative table applies to the *enablement* rather than a new +service: + +| Concern | Approach | Rejected alternative | Decision | +|---|---|---|---| +| Where to read seed objects | Merge top-level `objects` into the existing `components.objects` loop | Add a second parallel loop for top-level objects | **Merge** — one code path, one set of counters, reuses idempotency + resilience | +| App-side vs OR-side fix | Fix in OR importer | Tell every app to move objects to `components.objects` | **OR-side** — single fix vs N app PRs; export already writes `components.objects` so authored top-level is a legitimate equivalent | +| Idempotency key | `@self` slug within (register, schema), uuid fallback | Append-always | **Match-then-update** — reuses the loop's existing search; declarative seed must be re-runnable on every forced import | + +This keeps app seed data declarative (objects in the register JSON, imported by the +engine) per ADR-031 — no app gains an imperative seeding service. + +### Idempotency + +The existing loop already searches `@self{register, schema, slug}` (`:2035-2055`) +and version-compares before `saveObject()`; for objects carrying `@self.id`/uuid it +passes the uuid to `saveObject()` (`:2100-2116`). The fold reuses this verbatim, so +re-import of a top-level seed object updates the matched object (or skips when the +imported version is not higher) rather than creating a duplicate. Where a seed object +carries an explicit `@self.id`/uuid, that uuid is the stable identity across +re-imports. + +## Risks / Trade-offs + +- **Both keys present (`objects` + `components.objects`)** → Merge appends top-level + after `components.objects`. If the same logical object appears in both, the + search-by-slug idempotency collapses them to one (second is an update of the + first). Mitigation: dedupe by `@self` slug/uuid during the merge so the loop sees + each once; covered by a test. +- **Slug-less top-level objects** → still skipped by the existing guard + (`:1969-1971`), same as `components.objects`. This is intended (an + `ImportHandlerSluglessSkipTest` already pins slug-less skip for the schema path); + objects needing identity must carry `@self.slug` or `@self.id`. Noted, not changed. +- **Performance** → merge is an `array_merge` of already-loaded data; negligible. + +## Migration Plan + +Additive importer behaviour; no data migration. Deploy = code change. On the next +forced import (`loadConfiguration(force: true)` at app init, or the +`SyncConfigurationsJob` re-import), previously-dropped top-level seed objects +materialise. Rollback = revert the file; top-level seed objects stop importing again +(no data is destroyed; already-imported objects remain). No DB/schema rollback. + +## Open Questions + +- Confirm shillinq's 78 objects are intended to live at the top level vs being + re-authored into `components.objects`. Provisional: support both (this change). +- Whether the export should additionally emit a top-level `objects` mirror for + symmetry — out of scope here; export already uses `components.objects`. + +## Test Plan + +Unit tests in `tests/Unit/Service/Configuration/` (e.g. +`ImportHandlerTopLevelObjectsTest.php`): +- **Materialisation:** a configuration with schemas + registers + a **top-level + `objects`** array (each with `@self{register, schema, slug}`) imports the objects; + assert the saved-object count and that `saveObject()` was invoked per object (via + the existing `ObjectService` mock pattern used in `ImportHandlerTest`). +- **Idempotency:** importing the same top-level seed twice does not duplicate — the + second pass matches the existing object by (register, schema, slug)/uuid and + updates-or-skips; assert no second create for an unchanged version. +- **Equivalence:** the same objects authored at `components.objects` vs top-level + produce the same import result. +- **Regression:** a config using only `components.objects` is unchanged; a slug-less + top-level object is skipped (and counted in `skipped.objects`). diff --git a/openspec/changes/config-import-seed-objects/proposal.md b/openspec/changes/config-import-seed-objects/proposal.md new file mode 100644 index 0000000000..8d27ff8939 --- /dev/null +++ b/openspec/changes/config-import-seed-objects/proposal.md @@ -0,0 +1,62 @@ +--- +kind: code +--- + +## Why + +App register-seed objects placed in a register JSON's **top-level `objects` array** +are silently dropped on import: OpenRegister's `ImportHandler::importFromJson()` +only ever reads `data['components']['objects']`, never the top-level `objects` key. +shillinq ships 78 seed objects (e.g. `SalesOrderLine`) in the top-level `objects` +array — its schemas import correctly, but **0** objects materialise. This blocks +every app that authored seed data the way the export format reads (top-level) and +undermines the "ship an app with starter data" path (ADR-031 declarative apps). + +## What Changes + +- **Accept the top-level `objects` array as a seed-object source.** When a + configuration JSON carries a top-level `objects` array (and not / in addition to + `components.objects`), `ImportHandler::importFromJson()` SHALL fold it into the + same object-import loop that already handles `components.objects`. `components.objects` + remains the canonical export location; the top-level array becomes an accepted + equivalent so seed bundles authored either way import identically. +- **Idempotent re-import.** Seed objects MUST match existing objects by their + `@self` identity (slug within register+schema, falling back to `@self.id`/uuid) + so a second import updates-or-skips rather than duplicating — reusing the existing + search-by-(register, schema, slug) + uuid path already in the loop. +- **Observability.** Folded top-level objects are counted in the same `result` + counters (`objects`, `skipped.objects`) as `components.objects`, so callers and + tests see them. +- **No change** to the slug requirement, the per-entity resilience try/catch, the + no-user-context acting-user resolution, or `@ref` seed-token resolution — those + already work; this change only makes the loop *see* top-level objects. + +This is **backward-compatible**: apps using `components.objects` are unaffected +(that branch is untouched); apps using the top-level array start importing. + +## Capabilities + +### New Capabilities + + +### Modified Capabilities +- `import-resilient-per-entity-and-no-user-context`: the object-import phase of + `importFromJson()` gains a requirement that a top-level `objects` array is an + accepted seed-object source (merged with `components.objects`), imported + idempotently by `@self` identity. (This capability already owns the object-import + loop's resilience/no-user-context behaviour, so the seed-source requirement lands + here.) + +## Impact + +- **Code:** `lib/Service/Configuration/ImportHandler.php` — `importFromJson()` + normalises top-level `objects` into the existing `components.objects` loop (a small + merge before the loop at ~line 1960). No new method signatures; `ConfigurationService` + and `AppHostSettingsService::loadConfiguration()` (`importFromApp` → forced path) + are unchanged. +- **Tests:** `tests/Unit/Service/Configuration/` — a test proving top-level seed + objects materialise after import, and that re-import does not duplicate. +- **Consumers:** shillinq's 78 seed objects (and any app authoring top-level + `objects`) begin importing. **No app-side change is required** — the fix is purely + in OpenRegister (see design.md "Where the fix lands"). +- **No** DB, API, route, or dependency changes. diff --git a/openspec/changes/config-import-seed-objects/specs/import-resilient-per-entity-and-no-user-context/spec.md b/openspec/changes/config-import-seed-objects/specs/import-resilient-per-entity-and-no-user-context/spec.md new file mode 100644 index 0000000000..04cd418044 --- /dev/null +++ b/openspec/changes/config-import-seed-objects/specs/import-resilient-per-entity-and-no-user-context/spec.md @@ -0,0 +1,44 @@ +## ADDED Requirements + +### Requirement: Top-level objects array is an accepted seed-object source +`importFromJson()` SHALL treat a configuration's top-level `objects` array as a valid seed-object source, folding its entries into the same object-import loop that processes `components.objects`, so seed objects authored at the top level import identically to those nested under `components.objects`. The merge SHALL preserve `components.objects` entries and append top-level entries, de-duplicating by `@self` identity (slug within (register, schema), with `@self.id`/uuid fallback) so an object declared in both keys is imported once. `components.objects` SHALL remain the canonical export location; this requirement only adds the top-level array as an accepted equivalent on the import (including the app-init/forced) path. No app-side change to `loadConfiguration`/`importFromApp` SHALL be required for top-level seed objects to import. + +#### Scenario: Top-level seed objects materialise after a forced app import +- **GIVEN** a register configuration with schemas and registers, and a **top-level `objects`** array whose entries each carry `@self` with `register`, `schema`, and `slug` +- **AND** no `components.objects` key is present +- **WHEN** the app triggers `importFromApp()` with `force: true` (the `loadConfiguration` path) +- **THEN** each top-level seed object MUST be persisted via `saveObject()` +- **AND** the saved objects MUST appear in `result['objects']` +- **AND** the schemas and registers MUST still import unchanged + +#### Scenario: Re-import of top-level seed objects is idempotent +- **GIVEN** a top-level `objects` seed array that has already been imported once +- **WHEN** the same configuration is imported a second time at the same object version +- **THEN** each seed object MUST be matched to its existing object by `@self` slug within (register, schema) (or `@self.id`/uuid) +- **AND** no duplicate object MUST be created +- **AND** an unchanged-version object MUST be skipped rather than re-created + +#### Scenario: Both components.objects and a top-level objects array are merged once +- **GIVEN** a configuration that declares the SAME logical seed object in both `components.objects` and the top-level `objects` array +- **WHEN** `importFromJson()` runs +- **THEN** the object MUST be imported exactly once (the duplicate collapses by `@self` identity) +- **AND** objects unique to either key MUST each import + +## MODIFIED Requirements + +### Requirement: Per-object import resilience for the main object loop +The main object loop in `importFromJson()` (covering both `components.objects` and the merged top-level `objects` array) SHALL wrap each object's resolve/search/save sequence in its own try/catch. When an object fails validation or persistence (e.g. a missing required property such as `name`), the handler SHALL log a WARNING carrying the object slug and reason, increment a skipped-object counter, and CONTINUE with the remaining objects. A single failing object SHALL NOT abort sibling object imports nor the overall app import. Objects lacking an `@self.slug` SHALL continue to be skipped by the existing slug guard, identically for both the `components.objects` and top-level sources. + +#### Scenario: A name-missing object is skipped, not fatal +- **GIVEN** a configuration whose object source (`components.objects` or top-level `objects`) includes one object missing a required property and one valid object +- **WHEN** `importFromJson()` runs +- **THEN** the valid object SHALL be saved and present in `result['objects']` +- **AND** the failing object SHALL be skipped with a logged WARNING (not re-thrown) +- **AND** `result['skipped']['objects']` SHALL be incremented by one +- **AND** the import SHALL complete and return a result rather than throwing + +#### Scenario: A slug-less top-level seed object is skipped +- **GIVEN** a top-level `objects` array containing one entry whose `@self` has no `slug` and one valid slugged entry +- **WHEN** `importFromJson()` runs +- **THEN** the slug-less entry SHALL be skipped by the existing slug guard +- **AND** the valid slugged entry SHALL be imported diff --git a/openspec/changes/config-import-seed-objects/tasks.md b/openspec/changes/config-import-seed-objects/tasks.md new file mode 100644 index 0000000000..719b601f68 --- /dev/null +++ b/openspec/changes/config-import-seed-objects/tasks.md @@ -0,0 +1,28 @@ +## 1. Importer fix + +- [ ] 1.1 In `ImportHandler::importFromJson()` (`lib/Service/Configuration/ImportHandler.php`), immediately before the object loop (~line 1960), build a merged seed-object list from `data['components']['objects']` plus a top-level `data['objects']` array. +- [ ] 1.2 De-duplicate the merged list by `@self` identity (slug within (register, schema), falling back to `@self.id`/uuid) so an object declared in both keys is processed once. +- [ ] 1.3 Feed the merged list into the existing loop (register/schema resolution, search-by-(register, schema, slug), version compare, `saveObject()` with `_rbac:false`/`_multitenancy:false`, per-entity try/catch) without altering that logic, the slug guard, or `@ref` token resolution. +- [ ] 1.4 Ensure folded top-level objects are counted in the existing `result['objects']` and `result['skipped']['objects']` counters. + +## 2. Tests + +- [ ] 2.1 Add a test proving top-level `objects` seed entries materialise after a forced import (assert saved-object count / `saveObject()` invocations) in `tests/Unit/Service/Configuration/`. +- [ ] 2.2 Add an idempotency test: re-importing the same top-level seed does not duplicate (match by (register, schema, slug)/uuid; unchanged version skipped). +- [ ] 2.3 Add an equivalence test: identical objects at `components.objects` vs top-level produce the same result; and a regression test that a `components.objects`-only config is unchanged and a slug-less top-level object is skipped. + +## 3. Validation + +- [ ] 3.1 Run the configuration import test suite (`ImportHandler*Test`) and confirm new + existing tests pass. +- [ ] 3.2 Run `composer check:strict` on `ImportHandler.php` and fix any new findings. +- [ ] 3.3 Smoke-verify against `shillinq/lib/Settings/shillinq_register.json` (78 top-level objects) — a forced import imports the seed objects (0 → 78) and a second import does not duplicate. + +Acceptance criteria: +- A top-level `objects` array imports identically to `components.objects` on the app-init/forced path. +- Re-import is idempotent (match by `@self` slug/uuid; no duplicates). +- `components.objects`-only configs and the slug-skip guard are unchanged. +- Fix is OR-side only — no app change to `loadConfiguration`/`importFromApp`. + +Quality: +- No new PHPCS/PHPMD/PHPStan/Psalm regressions; SPDX header + PHPDoc preserved. +- WARNING logging and `skipped.objects` counters cover folded top-level objects. diff --git a/openspec/changes/credential-broker-organisation-scope/.openspec.yaml b/openspec/changes/credential-broker-organisation-scope/.openspec.yaml new file mode 100644 index 0000000000..8cceb8d51a --- /dev/null +++ b/openspec/changes/credential-broker-organisation-scope/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-08 diff --git a/openspec/changes/credential-broker-organisation-scope/design.md b/openspec/changes/credential-broker-organisation-scope/design.md new file mode 100644 index 0000000000..b4bff6117e --- /dev/null +++ b/openspec/changes/credential-broker-organisation-scope/design.md @@ -0,0 +1,118 @@ +# Design — credential broker organisation scope + +## Context + +The credential-broker chain (see `credential-broker/design.md`) resolves an outbound +call in `CredentialBrokerService` behind four guards: **owner** (per-object IDOR), +**allowedApps** (the calling app is listed), **provider allow-rules** (method + path +pattern), and **host-lock** (base URL). The stored secret lives in the Nextcloud vault +(`ICredentialsManager`) keyed by the credential's UUID; the object itself never holds a +secret. Organisations are OR-native (UUID identities resolved through `UserService`). + +This design adds an **organisation** flavour of credential alongside the existing +**personal** one, changing only the owner guard and the vault-owner selection — the +provider allow-rules, host-lock, and constrained proxy are untouched. + +## D1 — Schema: `scope` + `organisation` + +The `credential` schema gains two properties: + +- `scope`: enum `personal` | `organisation`, **default `personal`**. Absent ⇒ personal. +- `organisation`: an OR organisation UUID. **Required iff** `scope = organisation`; + MUST be absent/ignored for personal credentials. + +`owner` remains, and for organisation credentials records the **provisioning admin** for +audit/attribution only — it is NOT the access-control key (membership is). No secret is +added to the object; the metadata stays safe to list, export, audit, and query. + +## D2 — Where the organisation secret lives (the crux) + +`ICredentialsManager` is per-user: `store($userId, $identifier, $value)`. A shared +organisation secret cannot live under one member's user, or it would vanish when that +member leaves and would be readable only in that member's session. + +**Decision:** organisation secrets are stored as Nextcloud **system credentials** — the +same `ICredentialsManager`, but under a single reserved system identity +(`ICredentialsManager::store('', $uuid, $secret)`; the empty user is Nextcloud's +supported system-credential namespace), still keyed by the credential UUID. Rationale: + +- No user "owns" the secret, so membership changes never orphan it. +- The key is the credential UUID (unique), so different organisations never collide. +- **Access is gated by the guards in D3 BEFORE the vault is read** — the UUID is only + used to fetch the secret once the acting user has been proven a member and the app + proven allowed. The vault key is not itself a capability. +- Encryption at rest is inherited from `ICredentialsManager` (encrypted store), + identical to the personal path. + +Personal secrets are unchanged: stored under the owning user, keyed by UUID. + +A single private helper selects the vault owner from the credential's scope +(`'' ` for organisation, the `owner` uid for personal) at both write (create/rotate) and +read (broker resolve) time, so the two paths cannot drift. + +## D3 — Broker guard: additive organisation branch + +`CredentialBrokerService` today (personal): resolve acting identity → **deny unless +`credential.owner === actingUid`** → allowedApps → provider rules → host-lock. + +The owner guard becomes scope-dispatched, **without changing the personal branch**: + +``` +if credential.scope == 'personal' (or absent): + deny unless credential.owner === actingUid # UNCHANGED +else: # organisation + deny unless actingUid is a member of credential.organisation +``` + +Membership is resolved through `UserService` (the acting user's organisations must +include `credential.organisation`). The `allowedApps`, provider-allow-rule, and +host-lock guards run afterwards for BOTH scopes, unchanged. The sessionless +`actingUserId` fallback (design D-K, honoured only when no session exists) applies to the +personal branch only; an organisation call MUST have a real session to resolve +membership (no session ⇒ deny), because there is no owner to fall back to. + +**Security invariant:** the change is strictly additive. A personal credential's guard +is byte-for-byte the same; the organisation branch is a *new*, *narrower-than-public* +gate (org membership + allowedApps), never a widening of the personal one. It is +impossible for the new code to admit a personal-credential call that the old code denied. + +## D4 — Admin CRUD & listing + +- `POST /api/credentials` with `scope: 'organisation'` + `organisation: `: + allowed only when the caller is an **administrator of that organisation** (or a + Nextcloud admin). The `organisation` defaults to the caller's active organisation when + omitted. On success the secret is written to the system vault (D2). +- `PUT` / `DELETE` on an organisation credential: same admin gate. +- `GET /api/credentials?scope=organisation`: returns the organisation credentials of the + caller's active organisation (visible to any member; secrets never returned). +- `GET /api/credentials` (no scope) or `?scope=personal`: unchanged — the caller's own + personal credentials. + +Organisation-admin resolution reuses OR's existing organisation ownership/role model +(the organisation object's owner/administrators); if OR exposes only owner-level +authority today, org-credential management is gated to the organisation owner + NC admin, +and finer roles are a follow-up (called out in tasks). Personal CRUD is unchanged and +requires no admin. + +## D5 — API compatibility + +The wire shape is a superset: `scope` and `organisation` are optional additive fields on +`POST`, and `scope` is an optional query param on `GET`. Every existing client that +sends neither continues to hit the personal path with identical behaviour. The +`/providers` catalogue endpoint is unchanged. + +## D6 — Frontend already present + +`CnCredentials` (nextcloud-vue) renders `scope="organisation"` with full allowed-app +management and posts `scope: 'organisation'`; it is surfaced from an app's **admin** +settings (organisation credentials) vs the **personal** settings (personal credentials). +This change is the backend that mode calls; no further frontend contract is required +beyond honouring the `scope` param already sent. + +## Risks + +- **Vault-owner drift** — mitigated by the single shared owner-selector helper (D2). +- **Membership check cost** — `UserService` organisation lookups are cached; the guard + adds one membership resolution per organisation broker call. +- **Org-admin authority granularity** — bounded explicitly in D4; owner-level gate first, + finer roles deferred. diff --git a/openspec/changes/credential-broker-organisation-scope/proposal.md b/openspec/changes/credential-broker-organisation-scope/proposal.md new file mode 100644 index 0000000000..557b6d8f6f --- /dev/null +++ b/openspec/changes/credential-broker-organisation-scope/proposal.md @@ -0,0 +1,48 @@ +# Credential broker — organisation scope + +## Why + +The OpenRegister credential broker today is strictly **per-user owner-scoped**: a +`credential` object carries an `owner` (a single user), the paired secret lives in the +Nextcloud vault keyed by the credential UUID, and the broker's IDOR guard denies any +call whose acting user is not that owner. This is correct for *personal* credentials — +"an app acts on **my** behalf against GitHub with **my** token". + +It cannot express an **organisation** credential — "an app acts on behalf of **the +organisation** with a shared token that any member's session may drive". Organisations +in OpenRegister are first-class (a UUID identity; users belong to one or more, and have +an active organisation — `UserService::getUserActiveOrganisations()` / +`getActiveOrganisation()`). A tender-office GitHub token, a shared APIM subscription +key, or a per-department GitLab token should be provisioned once by an organisation +administrator and usable by every member's app calls — without the secret being copied +into each user's personal vault, and without weakening the personal per-user guard. + +The frontend already anticipates this: `CnCredentials` renders a `scope="organisation"` +mode (full allowed-app management on an app's admin surface) that has no backend to talk +to. This change adds that backend. + +## What + +- Extend the `credential` schema with `scope` (`personal` | `organisation`, default + `personal`) and `organisation` (an OR organisation UUID, required when + `scope = organisation`). +- Store organisation secrets in the Nextcloud vault under a **fixed system identity** + (system credentials), still keyed by the credential UUID — so no user "owns" the + secret and any authorised member's session can drive a broker call that reads it. + Personal secrets continue to live under the owning user, unchanged. +- Extend the broker guard with an **organisation branch** that is *additive*: the + personal path (owner must equal the acting user) is untouched; the organisation path + admits a call only when the acting user is a **member** of the credential's + organisation AND the calling app is in `allowedApps`. +- Add admin-scoped CRUD: creating, updating, or deleting an organisation credential + requires the caller to be an **administrator of that organisation** (or a Nextcloud + admin); listing organisation credentials returns those of the caller's active + organisation. `GET /api/credentials?scope=organisation` and a `scope` body field on + `POST` select the organisation set. + +## Non-goals + +- No change to the personal path's storage, guard, or API shape (strict backward + compatibility — a request with no `scope` behaves exactly as today). +- No cross-organisation sharing (a credential belongs to exactly one organisation). +- No change to the constrained-proxy allow-rules or the provider catalogue. diff --git a/openspec/changes/credential-broker-organisation-scope/specs/credential-broker/spec.md b/openspec/changes/credential-broker-organisation-scope/specs/credential-broker/spec.md new file mode 100644 index 0000000000..d30c2821fe --- /dev/null +++ b/openspec/changes/credential-broker-organisation-scope/specs/credential-broker/spec.md @@ -0,0 +1,93 @@ +## ADDED Requirements + +### Requirement: Credential scope + +A `credential` object SHALL carry a `scope` property whose value is `personal` or `organisation`, defaulting to `personal` when absent, plus an `organisation` property (an OpenRegister organisation UUID) that MUST be present when `scope` is `organisation` and MUST NOT act as an access key for a personal credential. + +#### Scenario: Absent scope defaults to personal + +- **WHEN** a `credential` object is read with no `scope` property +- **THEN** it is treated as `scope = personal` +- **AND** its access control and secret storage behave exactly as the pre-existing personal credential + +#### Scenario: Organisation credential requires an organisation + +- **WHEN** a credential is created with `scope = organisation` +- **THEN** an `organisation` UUID MUST be supplied (defaulting to the caller's active organisation when omitted) +- **AND** the object records the provisioning admin in `owner` for attribution only, never as the access-control key + +### Requirement: Organisation secret storage + +An organisation credential's secret SHALL be stored in the Nextcloud vault under a single reserved system identity keyed by the credential UUID, never under an individual user, so that membership changes never orphan it and different organisations never collide. + +#### Scenario: Organisation secret stored under the system identity + +- **WHEN** an organisation credential is created or rotated +- **THEN** its secret is written to `ICredentialsManager` under the system identity keyed by the credential UUID +- **AND** no `credential` object property holds the secret value +- **AND** a personal credential's secret continues to be stored under its owning user, unchanged + +#### Scenario: Vault owner is selected from scope at read and write + +- **WHEN** the broker resolves, or the controller writes/rotates, a credential's secret +- **THEN** the vault owner is derived from the credential's `scope` by a single shared selector (system identity for organisation, owner uid for personal) +- **AND** the vault key is used only after the access guards have admitted the caller + +### Requirement: Organisation broker guard + +The broker's owner guard SHALL dispatch on `scope` such that a `personal` credential is admitted only when the acting user equals its `owner` (unchanged), and an `organisation` credential is admitted only when the acting user is a member of the credential's `organisation`; the existing allowedApps, provider allow-rule, and host-lock guards then apply to both scopes. + +When a user session exists it is AUTHORITATIVE for the organisation branch: membership is resolved against the session user and any asserted acting organisation is ignored, so a request-context caller can never escalate via an assertion. When NO session exists, an `organisation` credential MAY be admitted for a trusted in-process caller that asserts an `actingOrganisationId` equal to the credential's `organisation` (openregister#450); this is the only sessionless admit path, it is honored ONLY without a session, and the value SHALL NOT be settable from request input — the HTTP-routed broker call passes none, and the assertion is reachable only through the non-routed in-process resolution path. The match keeps resolution decoupled from any individual user's membership (ADR-064 Rule 4): the sessionless `actingUserId` fallback SHALL NOT be consulted for the organisation branch, so org scope is never recoupled to one user. + +#### Scenario: Organisation member drives an organisation credential + +- **WHEN** an authenticated member of the credential's organisation triggers a broker call for an app listed in `allowedApps` +- **THEN** the call is admitted and the secret is injected server-side from the system vault +- **AND** the personal owner-equality branch is not evaluated for this credential + +#### Scenario: Non-member is denied an organisation credential + +- **WHEN** a user who is not a member of the credential's organisation triggers a broker call against it +- **THEN** the broker denies the call before any provider call is made + +#### Scenario: Routed organisation call still requires a session + +- **WHEN** an HTTP-routed broker call for an organisation credential has no authenticated user session +- **THEN** the broker denies it (the routed path asserts no acting organisation, and the sessionless `actingUserId` fallback applies to personal credentials only) + +#### Scenario: Sessionless in-process caller resolves an organisation credential by matching assertion + +- **WHEN** a trusted in-process caller with no user session resolves an organisation-scoped inject-only credential and asserts an `actingOrganisationId` +- **THEN** the credential is admitted and its organisation-scoped secret is returned only when the asserted organisation equals the credential's `organisation` +- **AND** it is denied when the assertion is absent, empty, or does not match + +#### Scenario: A session ignores an asserted acting organisation + +- **WHEN** a broker call for an organisation credential has an authenticated user session AND also carries an `actingOrganisationId` +- **THEN** membership is resolved against the session user and the asserted organisation is ignored, so a session member is admitted despite a wrong assertion and a session non-member is denied despite a matching one + +#### Scenario: Personal guard is unchanged + +- **WHEN** a broker call resolves a personal (or scope-absent) credential +- **THEN** it is admitted only when the acting user equals the credential `owner`, identical to the behaviour before organisation scope existed + +### Requirement: Organisation credential administration + +Creating, updating, or deleting an `organisation` credential SHALL require the caller to be an administrator of that organisation (or a Nextcloud administrator), while listing organisation credentials returns the metadata of the caller's active organisation to any member and never returns a secret. + +#### Scenario: Only an org admin creates an organisation credential + +- **WHEN** a non-admin member POSTs a credential with `scope = organisation` +- **THEN** the request is denied +- **AND** an administrator of that organisation (or a Nextcloud admin) performing the same POST succeeds + +#### Scenario: Members list organisation credentials without secrets + +- **WHEN** a member requests `GET /api/credentials?scope=organisation` +- **THEN** the response lists the active organisation's credential metadata +- **AND** no secret value is present in any listed credential + +#### Scenario: Personal API is unchanged + +- **WHEN** a client calls `GET /api/credentials` with no scope, or `POST` with no `scope` field +- **THEN** the personal credential path is used with identical behaviour to before this change diff --git a/openspec/changes/credential-broker-organisation-scope/tasks.md b/openspec/changes/credential-broker-organisation-scope/tasks.md new file mode 100644 index 0000000000..8cec1eb6c9 --- /dev/null +++ b/openspec/changes/credential-broker-organisation-scope/tasks.md @@ -0,0 +1,46 @@ +# Tasks — credential broker organisation scope + +## Schema +- [ ] Add `scope` (enum personal|organisation, default personal) + `organisation` + (UUID, required when scope=organisation) to the `credential` schema in + `lib/Settings/credential_broker_register.json`; keep it secret-free. +- [ ] Bump the register descriptor + confirm the Repair-step materialisation picks up + the new properties (ADR-037: OR does not self-import its own register JSON). + +## Vault owner selection (D2) +- [ ] Add a single private helper that maps a credential's scope → vault owner + (`''` system identity for organisation, `owner` uid for personal), used at BOTH + write and read time. +- [ ] Store/rotate organisation secrets via `ICredentialsManager` under the system + identity keyed by the credential UUID; personal path unchanged. + +## Broker guard (D3) +- [ ] Dispatch the owner guard on `scope`: personal branch byte-for-byte unchanged; + organisation branch = acting user is a member of `credential.organisation` + (resolved via `UserService`), then the existing allowedApps/provider/host-lock + guards run for both scopes. +- [ ] Organisation calls require a real session (no `actingUserId` sessionless + fallback); deny when unauthenticated. + +## Controller / API (D4, D5) +- [ ] `POST /api/credentials`: accept `scope` + `organisation`; gate organisation + creation to org-admin (or NC admin); default organisation to the caller's active + organisation when omitted. +- [ ] `PUT` / `DELETE` on an organisation credential: same org-admin gate. +- [ ] `GET /api/credentials?scope=organisation`: list the active organisation's + credentials (members may read metadata; secrets never returned). +- [ ] Confirm the no-scope / `?scope=personal` paths are unchanged. + +## Tests +- [ ] Personal-path regression: a no-`scope` create/list/broker-call behaves identically + to before (guard, storage, response). +- [ ] Organisation happy path: org-admin creates → member's app call resolves through + the system vault → non-member is denied → non-allowed app is denied. +- [ ] Guard invariant: a personal credential is never admitted to a non-owner (the org + branch cannot leak into the personal branch). +- [ ] Vault-owner helper: organisation secret written under system identity, personal + under owner; delete removes the right vault entry. + +## Follow-ups (out of scope here) +- [ ] Finer organisation roles for credential management (beyond owner + NC admin). +- [ ] openconnector `authentication.credentialRef` documentation for org-scoped refs. diff --git a/openspec/changes/credential-broker/.openspec.yaml b/openspec/changes/credential-broker/.openspec.yaml new file mode 100644 index 0000000000..e089cfacb5 --- /dev/null +++ b/openspec/changes/credential-broker/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-05 diff --git a/openspec/changes/credential-broker/design.md b/openspec/changes/credential-broker/design.md new file mode 100644 index 0000000000..8c65f7c6f1 --- /dev/null +++ b/openspec/changes/credential-broker/design.md @@ -0,0 +1,329 @@ +## Context + +Apps in the Conduction fleet need to act on external providers (GitHub, GitLab, …) +on behalf of a signed-in user. Two concrete blockers exist today: + +- **Hermiq** `publishToHub` wants to push a skill to a user's GitHub repo. +- **OpenBuild** wants to discover external apps via a provider's API. + +The only current path is "paste your personal access token into the app", which +scatters long-lived secrets across every consuming app with no central revocation, +no per-app scoping, and no audit trail. OpenRegister is the foundation repository +and the natural place to hold secrets once and broker constrained outbound calls. + +This design covers the **full three-spec chain**. Only the **config head** +(`credential-broker`, `kind: config`) is implemented by *this* change: the +`credential` schema, the runtime-immutable `lib/` provider catalogue JSON file, and +the additive manifest `credentials` field. The code phases (`credential-broker-service`, +`credential-broker-ui`) are designed here so reviewers see the whole arc, but their +tasks live in their own changes (ADR-032 chaining — a `mixed` config-plus-code +envelope is the anti-pattern this split avoids). Both surfaces in the head are +declarative JSON (an OR register file + a shipped catalogue file), so the head stays +`kind: config`. + +**Constraints:** + +- OR MUST NOT depend on OpenConnector (OR is the foundation). The outbound call is + OR-native, built on Nextcloud's `OCP\Http\Client\IClientService`. +- Security model per hydra ADR-005: NC-native auth only; every endpoint + `#[NoAdminRequired]` + a per-object owner IDOR check; identity from `IUserSession`; + static generic error messages; no secret/PII in logs or responses; provider + host-lock; constrained method/path allow-rules. +- Data model per ADR-001: domain metadata → OR objects; app config → `IAppConfig`; + the *secret* is not domain data, so it lives in the NC vault, not an OR object. + +## Goals / Non-Goals + +**Goals:** + +- Define an owner-scoped `credential` schema whose objects carry only metadata + a + handle — never a secret value. +- Ship a runtime-immutable provider catalogue as a `lib/` JSON file + (`lib/Settings/credential-providers.json`) capturing host-lock + auth scheme + + constrained-proxy allow-rules, with `github` and `gitlab` entries. +- Add an additive `credentials[]` field to the shared app-manifest schema. +- Design (for the code phases) the `CredentialStore` abstraction + `ICredentialsManager` + vault leaf, the `CredentialBrokerService` + guarded request endpoint, and the + `CnCredentials` settings section — so the config head lands with a coherent plan. + +**Non-Goals (this change):** + +- No PHP, no Vue, no endpoints, no vault access — those are the code phases. +- No consuming-app wiring (Hermiq, OpenBuild adopt the broker in their own changes). +- No Vault/KMS backend — only the design seam for future leaves is described. +- No OAuth authorization-code flow — v1 brokers pre-issued tokens/PATs only. + +## Decisions + +### D1 — Credential metadata is an OR object; the secret is in the NC vault + +The `credential` schema stores `{ name, provider, owner, allowedApps[], createdAt }` +as an owner-scoped OR object. It holds **no secret** — the secret is written to and +read from Nextcloud's `OCP\Security\ICredentialsManager`, keyed by the credential's +OR UUID (e.g. vault key `openregister/credential/`). `ICredentialsManager` +encrypts at rest and scopes per user. + +*Why:* ADR-001 says domain metadata belongs in OR objects, but a secret is not domain +data and MUST NOT be queryable/exportable/auditable like one. Splitting metadata (OR) +from secret (vault) means the token never appears in a list response, an export, an +audit trail, or a GraphQL result. *Alternative rejected:* storing the token in an OR +object property with a "sensitive" flag — OR has no field-level encryption-at-rest for +object JSON and every read path (search, export, audit, GraphQL) would risk leaking it. + +### D2 — Provider catalogue is a runtime-immutable `lib/` JSON file, NOT an OR schema + +The provider catalogue ships as a **declarative JSON file in OR's `lib/`** +(`lib/Settings/credential-providers.json`), read-only at runtime, NOT a +`credentialProvider` OR schema and NOT register-seeded objects. Each entry declares +`identifier`, `title`, `baseUrl` (host-lock), `authScheme` (header + `{secret}` +template), and `allowRules[]` (`{ method, pathPattern }`). The broker loads it from +disk server-side; there is no API that creates, edits, or deletes catalogue entries. + +*Why (security):* the `allowRules[]` ARE the constrained proxy's security guardrails — +they define exactly which methods and paths the broker will ever call on a locked host. +If the catalogue were a register-seeded OR object it would be reachable by the object +CRUD API and therefore *widenable at runtime* (an admin — or, via any IDOR/authz slip, +worse — could add a rule like `PUT /repos/*` and broaden the blast radius of every +credential). Shipping the catalogue in `lib/`, fixed at release, is the strictly smaller +trust surface: new providers and new rules require a code review + a release, not a live +API write. This is a deliberate exception to ADR-031's declarative-first default — +justified because the artefact is a security control, not domain data. It is still a +declarative JSON config file (no imperative logic), so the change stays `kind: config`. +*Alternative rejected:* register-seeded `credentialProvider` objects — more flexible, but +runtime-mutable guardrails are the wrong default for a secret-brokering proxy. A PHP +catalogue class was also considered; a plain JSON file keeps the head declarative and +diffable. + +### D3 — `CredentialStore` abstraction + `ICredentialsManager` leaf (code phase) + +`credential-broker-service` introduces an abstract `CredentialStore` interface +(`put(uuid, secret)`, `get(uuid): ?string`, `delete(uuid)`) with a first concrete +`NextcloudVaultCredentialStore` backed by `OCP\Security\ICredentialsManager`. This is +the OR "leaf" pattern (cf. `integration-leaf-foundation`): future leaves (Vault, AWS +KMS) implement the same interface and slot in behind DI without touching the broker. + +### D4 — The broker enforces four ordered guards, then injects the secret + +`credential-broker-service` ships `CredentialBrokerService::request()` and +`POST /api/credentials/{id}/request` accepting `{ method, path, headers?, body? }` + +the calling app's id. The service MUST enforce, **in order, failing closed**: + +1. **Owner** — `IUserSession` current user owns the credential (per-object IDOR check, + ADR-005 Rule 3). Else `403`. +2. **Allowed app** — the calling app id is in the credential's `allowedApps[]`. Else `403`. +3. **Allow-rules** — the request `method` + `path` match one of the provider's + `allowRules[]` (method equality + path-prefix/glob match). Else `403`. +4. **Host-lock** — the resolved target URL host equals the provider's `baseUrl` host + (path is appended to `baseUrl`; the caller supplies only the path, never a full URL). + Else `403`. + +Only after all four pass does the broker read the secret from the store (D3) and inject +it per the provider's `authScheme` (e.g. `Authorization: token `), perform the +call via `IClientService`, and return the provider's status + body. **The secret never +appears in a response, a log line, or an error message.** Errors returned to the client +are static + generic (`'Request not permitted'`, `'Upstream request failed'`); the real +cause is logged server-side with `$this->logger->error()` and the secret redacted. + +### D5 — App-identity trust: signed per-app tokens (in-process AND cross-runtime) + +The broker must trust the "calling app id" it checks against `allowedApps[]`. The app id +is **never** read from a request body or a caller-supplied string. Instead the broker +authenticates the calling app via a **signed per-app token**: + +- **App registration.** Each broker-consuming app is registered with the broker under + its app id and a per-app signing key/secret (issued at registration, held by the broker; + the app holds its own copy). Registration + key storage live in the + `credential-broker-service` code phase (the broker's app registry; secrets held via + `ICredentialsManager`, never in an OR object). +- **Token.** The calling app presents a short-lived token — a JWS/HMAC-signed assertion + binding `{ appId, iat, exp, aud=openregister }` (and, for HTTP callers, a nonce to + resist replay). The token is signed with the app's registered key. +- **Verification.** The broker verifies the signature against the registered key for the + claimed `appId`, checks `exp`/`aud`/nonce, and only then treats `appId` as authentic and + runs the `allowedApps[]` guard. A bad/absent/expired token → `403` (static error). + +*Why this over an in-process-only scheme:* a signed token authenticates the caller +identically whether the call arrives as an in-process PHP service call OR as an HTTP / +cross-runtime request. There is therefore **no HTTP-only limitation** — the same +mechanism secures both, so ExApp sidecars and out-of-process consumers are first-class. +The broker still NEVER trusts an app id from the JSON body; identity comes only from the +verified token (and the current user still comes only from `IUserSession`). Token +issuance + verification are specified here but implemented in the +`credential-broker-service` code phase — this config-head change ships no token code. + +### D6 — Manifest `credentials` field; catalogue is read-only, credentials are owner-scoped + +The app-manifest schema gains an additive, optional `credentials` array of +`{ provider, reason, scopes }`. `provider` is a catalogue `identifier`; `reason` is a +human string for the settings UI; `scopes` is an advisory string array. The settings UI +(`credential-broker-ui`) aggregates installed apps' declarations to show "app X wants to +use provider Y for reason Z". `credential` objects are owner-scoped (any authenticated +user CRUDs their own via `#[NoAdminRequired]` + per-object owner check). The provider +catalogue is **read-only at runtime** for everyone (it is the `lib/` file, D2) — there is +no create/update/delete API for it, admin or otherwise. + +## Declarative-vs-imperative decision (ADR-031) + +- **Provider catalogue → declarative JSON, but runtime-immutable in `lib/`** (D2): a shipped + `credential-providers.json`, NOT a register-seeded `credentialProvider` schema. This is a + deliberate, security-justified exception to ADR-031's declarative-first-*in-the-register* + default — the allow-rules are a security control that MUST NOT be widenable at runtime, so + they ship fixed at release. Still declarative JSON (no imperative logic), so the head stays + `kind: config`. +- **Credential metadata → declarative**: plain OR `credential` objects via ObjectService; + no bespoke mapper/entity (ADR-001). +- **`createdAt` / any derived count fields → declarative**: use OR object metadata + (`@self.created`) and `x-openregister-calculations` if a "credentials in use" count is + ever surfaced; no service method that loops and counts. +- **The broker call + the four auth guards → imperative (justified exception)**: reaching + an external HTTP host and enforcing an ordered security guard is a genuine external- + integration + security-guard concern that no `x-openregister-*` extension expresses + (ADR-031 "what apps SHOULD still write in PHP": external API integrations; ADR-003). + `CredentialBrokerService` + `NextcloudVaultCredentialStore` are therefore legitimately + imperative and live in the `credential-broker-service` code phase. Documented here so + the reviewer sees the exception is deliberate, not an oversight. + +## Reuse Analysis (ADR-011 / ADR-001 dedup) + +- **`OCP\Http\Client\IClientService`** — OR already uses it across `WebhookService`, + `ConfigurationService`, `UploadService`, `Configuration/GitLabHandler`. The broker's + outbound call reuses this, NOT a new Guzzle wrapper. +- **`OCP\Security\ICredentialsManager`** — the NC-native encrypted per-user vault. The + first `CredentialStore` leaf wraps it; no custom secret storage (ADR-005: no custom + token/password storage). +- **`ObjectService`** (`saveObject` / `findAll` / `deleteObject`) — all `credential` + metadata CRUD goes through it; no custom Entity/Mapper (ADR-001). +- **`ConfigurationService::importFromApp()` + repair step** — seeds the `credential` + schema + example credential objects idempotently (slug-matched, `version_compare`), + exactly like the existing `*_register.json` files. The provider catalogue is NOT + imported this way — it is the read-only `lib/` JSON file loaded directly by the broker. +- **`AuthorizationService` / per-object owner check** — the broker's owner guard reuses + OR's existing owner/IDOR pattern rather than a new access-control layer. +- **`@conduction/nextcloud-vue` `CnAppRoot` user-settings slot** — `CnCredentials` mounts + as an `NcAppSettingsSection` between `CnNotificationPreferences` and the walkthrough + section (`CnAppRoot.vue` `user-settings` slot), reusing `CnSettingsSection` primitives. +- **NOT reused: OpenConnector `AuthenticationService`/`CallService`** — OR must not depend + on OpenConnector (foundation-direction rule). OR's own `AuthenticationService` handles + outbound OAuth/JWT but is not a general proxy; the broker is a small new OR-native seam. + +## Seed Data (ADR-001) + +All values are SAFE placeholders. No real token, secret, or UUID appears. The `credential` +metadata objects are seeded via `lib/Settings/credential_broker_register.json` +`components.objects[]` with the `@self` envelope, imported idempotently (matched by slug). + +**Register:** `credential-broker` → schema `credential` (single schema; the provider +catalogue is NOT a schema, see below). + +**Provider catalogue — shipped `lib/Settings/credential-providers.json` (NOT seeded objects, +read-only at runtime, D2):** + +```jsonc +{ + "providers": { + "github": { + "identifier": "github", + "title": "GitHub", + "baseUrl": "https://api.github.com", + "authScheme": { "header": "Authorization", "template": "token {secret}" }, + "allowRules": [ + { "method": "GET", "pathPattern": "/repos/*" }, + { "method": "GET", "pathPattern": "/user/repos" }, + { "method": "PUT", "pathPattern": "/repos/*/contents/*" }, + { "method": "POST", "pathPattern": "/repos/*/git/*" } + ] + }, + "gitlab": { + "identifier": "gitlab", + "title": "GitLab", + "baseUrl": "https://gitlab.com/api/v4", + "authScheme": { "header": "Authorization", "template": "Bearer {secret}" }, + "allowRules": [ + { "method": "GET", "pathPattern": "/projects/*" }, + { "method": "POST", "pathPattern": "/projects/*/repository/commits" } + ] + } + } +} +``` + +This file is a shipped release artefact, not seed data — it is never written through any +API, so it has no `@self` envelope and is not idempotently "re-imported"; it is simply +present on disk and read by the broker. + +**`credential` metadata (2 secret-less seed examples — general org data):** + +```jsonc +{ + "@self": { "register": "credential-broker", "schema": "credential", "slug": "gemeente-example-github-publish" }, + "name": "Gemeente Example — GitHub publisher", + "provider": "github", + "owner": "00000000-0000-0000-0000-000000000000", + "allowedApps": ["hermiq"], + "createdAt": "2026-01-01T00:00:00+00:00" +} +{ + "@self": { "register": "credential-broker", "schema": "credential", "slug": "reisbureau-example-gitlab-discovery" }, + "name": "Reisbureau Example — GitLab discovery", + "provider": "gitlab", + "owner": "00000000-0000-0000-0000-000000000000", + "allowedApps": ["openbuild"], + "createdAt": "2026-01-01T00:00:00+00:00" +} +``` + +The paired secret for each example credential is NOT seeded (a seed has no vault entry); +in a real flow the user pastes `YOUR_TOKEN_HERE` into the OR settings UI, which writes it +to the vault under the credential UUID. `owner` is the nil UUID placeholder in seed data; +real objects carry the creating user's OR-resolved owner. + +## Risks / Trade-offs + +- [Runtime-widenable allow-rules] → the catalogue is a read-only `lib/` file (D2); there is + no API to create/edit/delete a provider or a rule, so the guardrails cannot be broadened at + runtime. New providers/rules ship in a reviewed release only. The broker reads rules from + disk server-side, never from the request body. +- [Secret leakage via logs/errors] → broker uses static generic client errors + server-side + logging with the secret redacted; the secret is only ever in memory during the injected + call, never persisted to an OR object (D1). +- [Path-glob allow-rule bypass] → path matching normalises the path (reject `..`, decode + once, require leading `/`, match prefix/glob against the *normalised* path) and host-lock + is checked on the resolved URL, not the caller's string — the caller supplies only a path, + never a full URL (D4). +- [App-identity spoofing] → the calling app id is taken only from a verified signed per-app + token (D5), never from a body field; the token scheme secures in-process AND cross-runtime + callers identically, so there is no HTTP-only gap. +- [Per-app signing-key compromise] → keys are per-app and stored in `ICredentialsManager` + (never an OR object); tokens are short-lived with `exp` + nonce; a compromised app key is + revoked/rotated at the broker's app registry without touching other apps' keys (service phase). +- [Config-head lands before consumers] → expand-then-contract: the `credential` schema, the + `lib/` catalogue, and the manifest field are additive and inert until the service/UI phases + consume them; no regression to existing apps (opencatalogi, softwarecatalog manifests keep + validating). + +## Migration Plan + +1. This change (config head): add `lib/Settings/credential_broker_register.json` (the `credential` + schema descriptor + secret-less example objects) and `lib/Settings/credential-providers.json` + (the read-only provider catalogue), and add the additive `credentials` field to the manifest + schema(s). These are inert declarative descriptors — **OpenRegister does NOT self-import its own + register JSON at boot (ADR-037)**, so nothing lands in the DB from this change; the schema is + materialised by the service-phase Repair step. No data migration. Rollback = delete the files. +2. `credential-broker-service` (code, `depends_on` this): a `lib/Repair/` step that imports + `credential_broker_register.json` into OR (`importFromFilePath()`, idempotent, slug-matched, + mirroring `SeedAppVirtualSchemas`); the `CredentialStore` + vault leaf; the broker service + + guarded endpoint + credential CRUD endpoints; the signed per-app token registry (D5). +3. `credential-broker-ui` (code, `depends_on` service): `CnCredentials` settings section + + per-app manifest `credentials` declarations; then consuming apps (Hermiq, OpenBuild) adopt. + +## Resolved Decisions (signed off) + +- **Catalogue placement:** runtime-immutable `lib/Settings/credential-providers.json` (D2), NOT + register-seeded objects — chosen for the strictly smaller trust surface (the allow-rules are a + security control that must not be widenable at runtime). +- **App-identity trust:** signed per-app tokens (D5) — secures in-process and HTTP / cross-runtime + callers identically; no HTTP-only limitation. +- **Credential owner:** per-user — `owner` = the creating NC user (via the OR-resolved owner); the + secret is stored per-user in `ICredentialsManager`. Managed from Personal settings. +- **Chain shape:** three specs — `credential-broker` (config head) → `credential-broker-service` + (code) → `credential-broker-ui` (code), each `depends_on` the previous. diff --git a/openspec/changes/credential-broker/proposal.md b/openspec/changes/credential-broker/proposal.md new file mode 100644 index 0000000000..64a3210215 --- /dev/null +++ b/openspec/changes/credential-broker/proposal.md @@ -0,0 +1,89 @@ +--- +kind: config +depends_on: [] +chain: + - credential-broker # this spec (config head — credential schema, lib provider catalogue, manifest field) + - credential-broker-service # code — CredentialStore + ICredentialsManager leaf + broker service + endpoints + - credential-broker-ui # code — CnCredentials settings section + per-app manifest wiring +--- + +## Why + +Conduction apps increasingly need to act on external providers (GitHub, GitLab, …) +*on a user's behalf* — Hermiq wants to publish a skill to GitHub, OpenBuild wants +to discover external apps — but today the only way is for the user to paste a +personal access token *into the consuming app*, which then holds the secret. That +spreads long-lived secrets across every app, with no central revocation, no +per-app scoping, and no audit. We need a **credential broker**: the user gives a +secret to OpenRegister *once*, and apps make outbound calls through OR without +ever seeing the secret. + +## What Changes + +This change is the **config head of a three-spec chain** (ADR-032). It lands the +declarative data model that the code phases build on; it ships no PHP/Vue. + +- Introduce a `credential` OR schema — owner-scoped metadata for a stored secret + (`name`, `provider`, `owner`, `allowedApps[]`, `createdAt`). The metadata holds + **no secret** — only a handle. The token lives in the Nextcloud vault, keyed by + the credential's UUID (delivered in the service phase). +- Ship the curated provider catalogue as a **runtime-immutable declarative JSON file + in OR's `lib/`** (`lib/Settings/credential-providers.json`), NOT an OR schema. Each + entry declares the host-locked `baseUrl`, the auth-header scheme + (`Authorization: token {secret}` / `Bearer {secret}`), and the constrained-proxy + `allowRules[]` (permitted HTTP method + path-prefix patterns), with `github` and + `gitlab` shipped. The catalogue is fixed at release — the allow-rules are the + proxy's security guardrails and MUST NOT be widenable at runtime via any API. +- Seed a couple of secret-less example `credential` metadata objects (ADR-001 + seed-data rule). +- Add a `credentials` field to the shared app-manifest schema + (`@conduction/nextcloud-vue`) so an app can declare which providers it uses: + `credentials: [{ provider, reason, scopes }]`. + +**Downstream chain (narrated here, not implemented in this change):** + +- `credential-broker-service` (`kind: code`, `depends_on: credential-broker`) — + the `CredentialStore` abstraction + first `ICredentialsManager` vault leaf, the + `CredentialBrokerService` and `POST /api/credentials/{id}/request` endpoint that + performs the outbound call server-side under the four ordered guards + (owner → allowedApps → allow-rules → host-lock), plus credential CRUD endpoints. +- `credential-broker-ui` (`kind: code`, `depends_on: credential-broker-service`) — + the shared `CnCredentials` user-settings section (create / name / duplicate / + delete / allow-per-app) and per-app manifest `credentials` declarations. Consuming + apps (Hermiq `publishToHub`, OpenBuild external-app discovery) adopt the broker in + their own later changes. + +## Capabilities + +### New Capabilities + +- `credential-broker`: A secret-broker capability where OR holds provider secrets + in the Nextcloud vault and performs constrained outbound calls on a user's behalf, + so consuming apps never hold the secret. This config head defines the `credential` + schema, the runtime-immutable `lib/` provider catalogue (`credential-providers.json`, + github/gitlab), and the manifest `credentials` declaration. Downstream chain specs + ADD the broker service, the guarded request endpoint, and the settings UI to this + same capability. + +### Modified Capabilities + +- `openregister-app-manifest`: the app-manifest schema gains an optional + `credentials[]` field (`{ provider, reason, scopes }`) so apps can declare their + provider usage. Additive and back-compatible — existing manifests keep validating. + +## Impact + +- **New declarative surfaces**: `lib/Settings/credential_broker_register.json` + (`credential` schema descriptor + secret-less credential example objects) and + `lib/Settings/credential-providers.json` — the runtime-immutable provider catalogue + (github/gitlab, host-lock + auth scheme + allow-rules), read-only at runtime, never + API-editable. These are **inert descriptors**: OpenRegister does not self-import its own + register JSON at boot (ADR-037), so the `credential` schema is materialised by a `lib/Repair/` + step that ships in the `credential-broker-service` code phase (`importFromFilePath()`, + idempotent, slug-matched — mirroring `SeedAppVirtualSchemas`), not by this config head. +- **Shared schema**: `@conduction/nextcloud-vue/src/schemas/app-manifest*.schema.json` + gains the additive `credentials` field (validated by `npm run check:manifest`). +- **No code, no endpoints, no secrets** in this change — those land in + `credential-broker-service`. No secret value is ever stored in an OR object. +- **Downstream dependents**: Hermiq (publish-to-GitHub) and OpenBuild + (external-app discovery) unblock once the service + UI phases merge. diff --git a/openspec/changes/credential-broker/specs/credential-broker/spec.md b/openspec/changes/credential-broker/specs/credential-broker/spec.md new file mode 100644 index 0000000000..3f7fd33be9 --- /dev/null +++ b/openspec/changes/credential-broker/specs/credential-broker/spec.md @@ -0,0 +1,93 @@ +## ADDED Requirements + +### Requirement: Credential metadata schema + +OpenRegister SHALL define a `credential` schema whose objects hold owner-scoped +metadata for a stored secret and MUST NOT contain the secret value. Each object SHALL +carry `name` (string), `provider` (a catalogue provider identifier), `owner` (the owning +user's OR-resolved identity), `allowedApps` (array of app ids), and `createdAt` +(ISO-8601 timestamp). The secret itself SHALL live in the Nextcloud vault keyed by the +credential's UUID, never in any `credential` object property. + +#### Scenario: Credential schema declared in the register descriptor + +- **WHEN** the `credential-broker` register descriptor (`lib/Settings/credential_broker_register.json`) is read +- **THEN** it declares the `credential` schema with properties `name`, `provider`, `owner`, `allowedApps`, `createdAt` +- **AND** the schema declares no property that stores a secret/token value +- **AND** the descriptor is materialised into OpenRegister by the `credential-broker-service` Repair step (OR does not self-import its own register JSON at boot, ADR-037) + +#### Scenario: Metadata never carries a secret + +- **WHEN** a `credential` object is inspected via list, export, audit, or GraphQL +- **THEN** no secret or token value is present in the object +- **AND** only the metadata handle (`name`, `provider`, `owner`, `allowedApps`, `createdAt`) is returned + +### Requirement: Provider catalogue as a runtime-immutable lib file + +OpenRegister SHALL ship the curated provider catalogue as a runtime-immutable JSON file in +`lib/` (`lib/Settings/credential-providers.json`), NOT as an OR schema and NOT as +register-seeded objects. Each entry SHALL declare `identifier` (slug), `title`, `baseUrl` +(the host-locked API base), `authScheme` (an object with the header name and a `template` +containing the `{secret}` placeholder), and `allowRules` (an array of `{ method, pathPattern }` +entries defining the permitted HTTP method + path-prefix patterns for the constrained proxy). +The catalogue SHALL be read-only at runtime — there SHALL be no API that creates, updates, or +deletes a provider or an allow-rule (the allow-rules are a security control and MUST NOT be +widenable without a code review + release). The catalogue SHALL include `github` and `gitlab` +entries, each host-locked with allow-rules scoped to the minimum methods and path prefixes needed. + +#### Scenario: Catalogue shipped and read-only + +- **WHEN** OpenRegister loads the provider catalogue +- **THEN** it reads `lib/Settings/credential-providers.json` from disk, with each entry declaring + `identifier`, `title`, `baseUrl`, `authScheme`, and `allowRules` +- **AND** no API exists to create, update, or delete a catalogue provider or allow-rule + +#### Scenario: Provider declares host-lock and auth scheme + +- **WHEN** a catalogue entry is read +- **THEN** its `baseUrl` defines the single host the broker is permitted to call +- **AND** its `authScheme.template` contains a `{secret}` placeholder for secret injection + +#### Scenario: GitHub entry present + +- **WHEN** the catalogue is loaded +- **THEN** a `github` entry with `baseUrl` `https://api.github.com` and an + `Authorization: token {secret}` auth scheme exists +- **AND** its `allowRules` permit `PUT /repos/*/contents/*` and `GET /repos/*` + +#### Scenario: GitLab entry present + +- **WHEN** the catalogue is loaded +- **THEN** a `gitlab` entry with `baseUrl` `https://gitlab.com/api/v4` and a `Bearer {secret}` + auth scheme exists + +### Requirement: Seeded secret-less example credentials + +OpenRegister SHALL seed example `credential` metadata objects using general +organisation data (municipality, travel agency) so the capability is testable on a +fresh install. Seed credentials SHALL carry only metadata and MUST use safe placeholder +values (nil UUID for `owner`); no secret is seeded for them. + +#### Scenario: Example credentials present after install + +- **WHEN** the register import completes +- **THEN** at least two `credential` objects exist, each referencing a seeded provider +- **AND** each seeded credential contains no secret value and uses the nil-UUID `owner` placeholder + +### Requirement: App manifest declares provider usage + +The shared app-manifest schema (`@conduction/nextcloud-vue`) SHALL gain an optional, +additive `credentials` array. Each entry SHALL declare `provider` (a catalogue provider +identifier), `reason` (human-readable string), and `scopes` (advisory +string array). The addition MUST be back-compatible — existing manifests without a +`credentials` field SHALL keep validating. + +#### Scenario: Manifest with credentials validates + +- **WHEN** an app manifest declares `credentials: [{ provider: "github", reason: "publish skills", scopes: ["repo"] }]` +- **THEN** `npm run check:manifest` validates the manifest successfully + +#### Scenario: Manifest without credentials still validates + +- **WHEN** an existing app manifest has no `credentials` field +- **THEN** the manifest continues to validate against the updated schema diff --git a/openspec/changes/credential-broker/tasks.md b/openspec/changes/credential-broker/tasks.md new file mode 100644 index 0000000000..e7ea21fd34 --- /dev/null +++ b/openspec/changes/credential-broker/tasks.md @@ -0,0 +1,36 @@ +## 1. Declarative descriptors (this config head) + +- [x] 1.1 Create `lib/Settings/credential_broker_register.json` — the `credential-broker` register descriptor (`x-openregister.type: application`) with the `credential` schema (`name`, `provider`, `owner`, `allowedApps[]`, `createdAt`; NO secret-bearing property) and 2 secret-less example `credential` objects (general org data, nil-UUID `owner`) under `components.objects[]`. This is the declarative source the **service-phase** Repair step imports — OpenRegister does NOT self-import register JSON at boot (ADR-037; OR seeds its own schemas via `lib/Repair/` steps, cf. `SeedAppVirtualSchemas`). +- [x] 1.2 Add `lib/Settings/credential-providers.json` — the runtime-immutable provider catalogue with `github` + `gitlab` entries (`identifier`, `title`, `baseUrl` host-lock, `authScheme{header,template}`, `allowRules[]{method,pathPattern}`). Read-only at runtime; NOT a register schema and NOT seeded objects (D2). + +## 2. Manifest schema field + +- [x] 2.1 Add the additive optional `credentials[]` field (`{provider, reason, scopes}`) to `@conduction/nextcloud-vue/src/schemas/app-manifest.schema.json` and `app-manifest-v2.schema.json`. +- [x] 2.2 Bump the manifest schema version additively and confirm `npm run check:manifest` passes for a manifest with and without `credentials`. + +## 3. Verification + +- [x] 3.1 Deduplication check — document that no existing OR service/schema covers a credential broker; confirm the service phase reuses `ObjectService`, `IClientService`, `ICredentialsManager`, `ConfigurationService` (findings recorded even if "no overlap"). +- [x] 3.2 JSON-validate the new register + catalogue files; run `composer check:strict`; confirm no regression to opencatalogi/softwarecatalog manifest validation. + +## Deferred to `credential-broker-service` (code phase) + +- The PHP Repair step (`lib/Repair/`) that imports `credential_broker_register.json` into OR (via `importFromFilePath()`) so the `credential` schema + example objects land on `occ upgrade`, idempotently (slug-matched). OR does not self-import its own registers at boot, so this seeding is code, not declarative config — it ships with the broker service. + +## Acceptance criteria + +- `lib/Settings/credential_broker_register.json` declares the `credential` schema (no secret-bearing property) + ≥2 secret-less example objects with the nil-UUID `owner` placeholder; the file is valid JSON. +- `lib/Settings/credential-providers.json` ships `github` (`Authorization: token {secret}`, permits `PUT /repos/*/contents/*` + `GET /repos/*`) and `gitlab` (`Bearer {secret}`) entries, host-locked; it is a read-only file (no create/update/delete API). +- The app-manifest schema accepts an optional `credentials[]` field; manifests with and without it both validate. +- No secret/token value appears in any descriptor or seed object. +- The Repair-step import that makes the schema live is scoped to `credential-broker-service` (this head ships descriptors only). + +## Quality checklist + +- ADR-001: domain metadata in OR objects; the secret is NOT an OR object property (vault, delivered in the service phase). +- ADR-031: the provider catalogue is a runtime-immutable `lib/` JSON file — a deliberate, security-justified exception to declarative-first (the allow-rules must not be runtime-widenable). +- ADR-011: no new HTTP/credential utility invented in this change — reuse documented for the code phases. +- ADR-037: OR does not self-import its own register monolith; the import is a service-phase `lib/Repair/` step, not a declarative-import wiring. +- Seed data uses only safe placeholders (`YOUR_TOKEN_HERE`, nil UUID); no value resembling a real token/secret/UUID. +- Manifest change is additive and back-compatible (existing fleet manifests keep validating). +- This is the `kind: config` head of the chain; no PHP/Vue/endpoints land here. diff --git a/openspec/changes/credential-doriath-leaf/.openspec.yaml b/openspec/changes/credential-doriath-leaf/.openspec.yaml new file mode 100644 index 0000000000..dd9a1d92eb --- /dev/null +++ b/openspec/changes/credential-doriath-leaf/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-06 diff --git a/openspec/changes/credential-doriath-leaf/design.md b/openspec/changes/credential-doriath-leaf/design.md new file mode 100644 index 0000000000..d581310fff --- /dev/null +++ b/openspec/changes/credential-doriath-leaf/design.md @@ -0,0 +1,323 @@ +## Context + +The credential broker (capability `credential-broker`, head change still active in +`openspec/changes/credential-broker/`) shipped with a single custody leaf: +`NextcloudVaultCredentialStore` behind the `CredentialStore` interface +(`put/get/delete` keyed by the credential OR object's UUID), bound by a static DI +alias in `lib/AppInfo/Application.php` (~line 347). The broker +(`CredentialBrokerService::request`) enforces four ordered guards failing closed +(owner via `IUserSession` → `allowedApps[]` → catalogue allow-rules → host-lock), +and app identity on the HTTP path is proven by short-lived HMAC tokens +(`CredentialAppTokenService`, per-app signing secrets system-scoped in +`ICredentialsManager` under `openregister/credential-app-key/`). + +Doriath is the fleet's secrets app. Verified contract (doriath +`origin/development`): + +- `ApplicationService::register(string $name, ?string $description, string $type, + ?string $csr, ?string $userId, bool $isAdmin): Application` — `type` ∈ + `internal|external`; **admin callers auto-approve** (row becomes `active`, + `approvedBy`/`approvedAt` set) and, when a CSR is supplied, it is validated + (PKCS#10, **≥4096-bit key**) and an EncryptionSuite is provisioned immediately + via `EncryptionSuiteService::provisionForApplication(applicationId, csrPem)`. + **Doriath generates the application row id (`Uuid::uuid4()`)** — the caller + supplies a *name*, not an id. +- `SecretService::createByApplication(array $data, string $applicationId)` / + `updateByApplication(string $id, array $data, string $applicationId)` — + application-owned secrets (`ownerType='application'`); `createByApplication` + requires an ACTIVE EncryptionSuite for the application and stores `key` (the + ciphertext) verbatim; audit events dispatched. +- `MachineSecretEnvelopeService::SCHEME = 'rsa-oaep-sha256-chunked-v1'` — the + encryption scheme identifier; `EncryptService::rsaEncrypt(plaintext, + publicKeyPem)` / `DecryptService::rsaDecrypt(ciphertext, privateKeyPem)` are + **stateless** (no DB, no entity awareness) and implement OAEP-SHA256 with + manual padding + 512-byte chunking (WebCrypto-compatible — PHP's built-in OAEP + is SHA-1-locked, so reimplementing in OR would risk scheme drift). +- **Gap (verified):** `SecretService` has NO application-scoped read or delete + today. The only application-scoped read-by-name is + `SecretMapper::findByName('application', $ownerId, $name, ?$folderId)`, + consumed by Doriath's machine HTTP controller. `deleteByApplication` is being + specced as the Doriath change `application-secret-delete` (cross-repo + dependency); the service-level read-by-name that `get()`/upsert need must land + with it (see D-C). + +All decisions below (D-A, D-B, D-C, D-F, D-G, D-K) are **signed off** — recorded +here with their code-verified mechanics, not re-litigated. + +## Goals / Non-Goals + +**Goals:** + +- Doriath-preferred, vault-fallback custody behind the existing `CredentialStore` + interface, with lazy read-time migration (D-A). +- OR registered as a first-class Doriath application with its own EncryptionSuite, + idempotently, via Repair (D-B). +- Scheme-compatible envelope custody: Doriath stores ciphertext only; OR holds the + only private key (D-C); custody trade-off documented (D-F). +- Zero-admin-step onboarding for manifest-driven apps + a trusted in-process call + path (D-G) and a background acting-user seam (D-K) with an explicit trust + boundary. + +**Non-Goals:** + +- No guard-order/semantics changes; no new HTTP endpoints; no HTTP contract + changes (the broker endpoint keeps `X-Credential-Token`). +- No eager migration job; no Doriath-side code in this repo; no OR schema/seed + changes. + +## Decisions + +### D-A — `CredentialStoreResolver` with Doriath-preferred resolution + lazy migration + +A new `CredentialStoreResolver` mirrors the backend-selection role of +`lib/Service/Anonymisation/AnonymisationBackendService.php` (single source of +truth for "which backend is active and is it actually usable") combined with the +Deck-leaf availability idiom of `lib/Service/DeckLinkService.php` +(`IAppManager::isEnabledForUser('deck')`) and the lazy-class-resolution idiom of +`AnonymisationBackendService::requestOpenAnonymiser()` (`class_exists` + +`OCP\Server::get`, so OR carries no compile-time dependency on the optional app): + +- **Doriath eligible** ⇔ `IAppManager::isEnabledForUser('doriath')` AND + `class_exists` succeeds for every Doriath service class the leaf calls + (`ApplicationService`, `SecretService`, `EncryptService`, `DecryptService`) + AND the required application-scoped methods exist (`method_exists` for + `deleteByApplication` and the read-by-name seam — this is what makes the + cross-repo rollout order safe: an older Doriath yields the vault leaf, not a + broken one) AND OR's self-registration state exists (D-B `IAppConfig` id). +- Eligible → `DoriathCredentialStore`; otherwise → `NextcloudVaultCredentialStore`. + The DI alias at `lib/AppInfo/Application.php` (~line 347) becomes a + `registerService(CredentialStore::class, fn → resolver->resolve())` factory; + the broker and controller keep depending only on the `CredentialStore` + interface — zero call-site changes. +- **Lazy migration on read** (inside `DoriathCredentialStore`, which composes the + vault leaf as its fallback): `get(uuid)` → Doriath miss → vault `get(uuid)` + hit → `put()` into Doriath → vault `delete(uuid)` → return the secret. Writes + and deletes go to Doriath only (delete additionally clears any leftover vault + row so a later fallback can never resurrect a deleted secret). +- **Known limitation (documented, accepted):** the vault leaf is scoped to the + *session* user, so lazy migration only fires in a session context. A + background call (D-K) against a not-yet-migrated credential misses in both + stores and fails CLOSED (static 403 via the existing "no secret stored" + deny). First interactive use migrates; background use then works. + +*Alternative rejected:* a separate `MigratingCredentialStore` decorator — one more +DI seam for behaviour that only exists on the Doriath path; composing the vault +leaf inside `DoriathCredentialStore` keeps the resolver a two-outcome switch. + +### D-B — OR self-registration with Doriath via an idempotent Repair step + +A new `lib/Repair/` step (registered in `appinfo/info.xml` under both `` +and ``, like `ImportCredentialBrokerRegister`): + +1. **Skip fast** when already registered: `IAppConfig` holds the Doriath + application UUID and the Doriath row still exists — the step is a no-op + (idempotent, safe on every `occ upgrade`). Also skip (warn, don't fail) when + Doriath is not eligible (app absent/disabled, classes missing) — mirroring + `GenericInitializeSettings`' degrade-don't-throw posture. +2. Generate an **RSA-4096 keypair** via openssl (`openssl_pkey_new`). +3. Store the **private key SYSTEM-scoped** in `ICredentialsManager` — + `store('', 'openregister/doriath/private-key', $pem)` — the same system-scope + idiom (`userId = ''`) `CredentialAppTokenService` already uses for per-app + signing secrets. The private key NEVER leaves `ICredentialsManager`, is never + logged, and never lands in `IAppConfig` or an OR object. +4. Self-generate a **CSR** (`openssl_csr_new`, CN `openregister`) from the + keypair — Doriath validates PKCS#10 format + ≥4096-bit key. +5. Call `ApplicationService::register(name: 'openregister', description: …, + type: 'internal', csr: $csrPem, userId: null, isAdmin: true)` **in-process** + — admin registration auto-approves the row (`active`) and provisions the + EncryptionSuite from the CSR in the same call. +6. Persist in `IAppConfig` (non-secret): the **Doriath-assigned application UUID** + (Doriath generates the row id; `'openregister'` is the application *name* — + verified against `ApplicationService::register`, which calls + `Uuid::uuid4()`), plus OR's **public key PEM** (derived from the keypair; + public material, needed by `put()` for envelope encryption without a Doriath + round-trip). + +`userId: null` is intentional: the repair step runs without a session; Doriath +records the registration as a system-actored audit event (verified: its audit +dispatch handles the null registrant). + +### D-C — Envelope custody: OR encrypts to itself, Doriath stores ciphertext only + +`DoriathCredentialStore` (implements `CredentialStore`): + +- **`put(uuid, secret)`** — encrypt `secret` against OR's own public key PEM + (from `IAppConfig`, D-B) using Doriath's `rsa-oaep-sha256-chunked-v1` scheme, + by invoking Doriath's **stateless `EncryptService::rsaEncrypt`** cross-app + (resolved via `class_exists` + `Server::get`). Reusing Doriath's implementation + is deliberate: the scheme does manual OAEP-SHA256 padding + chunked raw RSA to + stay WebCrypto-compatible, and a second implementation in OR would be a drift + risk for every future Doriath consumer. Then upsert the application-owned + secret: read-by-name (secret **name = credential UUID**, **root folder** + (`folderId` null), owner = OR's Doriath application) → absent ⇒ + `SecretService::createByApplication(['name' => $uuid, 'key' => $ciphertext], + $applicationId)`; present ⇒ `updateByApplication($secretId, ['key' => + $ciphertext], $applicationId)` (rotation keeps the row, bumping Doriath's + `keyUpdatedAt` audit metadata). +- **`get(uuid)`** — read the ciphertext in-process from the application-owned + vault by name (= credential UUID, root folder), decrypt via Doriath + `DecryptService::rsaDecrypt($ciphertext, $privateKeyPem)` with OR's private + key fetched from system-scoped `ICredentialsManager`. Miss → D-A lazy + migration path. Plaintext exists only in memory; never logged. +- **`delete(uuid)`** — call Doriath's NEW `SecretService::deleteByApplication` + (**cross-repo dependency**: Doriath change `application-secret-delete`), + idempotent per the `CredentialStore` contract (absent = no-op). Also clears + any residual vault row (D-A). +- **Read seam status (verified gap):** `SecretService` has no application-scoped + read today; only `SecretMapper::findByName('application', …)` exists, used by + Doriath's machine HTTP controller. This change REQUIRES a service-level + in-process read-by-name (e.g. alongside `deleteByApplication` in + `application-secret-delete`); OR MUST NOT reach into Doriath's `Db` mapper + layer. Until both methods exist, the resolver's `method_exists` probe keeps OR + on the vault leaf (D-A), so the repos can merge in either order. +- **Ambiguity policy:** name-lookup returning >1 row (Doriath does not enforce + name uniqueness) is treated as corruption: fail closed on `get`, log + secret-free, never guess. + +### D-F — Custody trade-off: application-owned vault, broker-layer user isolation + +Before: per-user `ICredentialsManager` rows — storage itself was user-partitioned. +After (Doriath path): ONE application-owned vault (OR's Doriath application) holds +every brokered secret; **per-user isolation is enforced at the broker/metadata +layer** — the owner IDOR guard on the `credential` OR object (guard 1, unchanged) +is the boundary that decides whose secret may be read. Documented consequences: + +- The storage layer no longer provides a second, independent user partition: a + bug that bypassed the owner guard would expose other users' *ciphertext* + handles to the broker code path (never to the caller directly — the broker + still only injects the secret into the host-locked upstream call, and never + returns it). +- What is gained: real envelope encryption at rest (secrets unreadable without + OR's private key, which lives in `ICredentialsManager` system scope), Doriath + audit events on every put/rotate/delete, rotation metadata, admin visibility, + and D-K background reads (the per-user vault structurally could not serve a + sessionless job — `currentUserId()` is `''` without a session). +- Compensating controls: the owner guard is already covered by unit tests and + the `hydra-gate-no-admin-idor` mechanical gate; this change adds tests pinning + "user B can never broker user A's credential" on the Doriath path + specifically. + +Accepted: the broker/metadata layer was ALWAYS the authorization boundary (the +vault partition never checked `allowedApps` or provider rules); this +consolidates enforcement where it already lived, in exchange for materially +better at-rest and audit properties. + +### D-G — Manifest-driven no-code onboarding + trusted in-process calls + +- **Auto-registration:** when an AppHost leaf initialises + (`lib/AppHost/Repair/GenericInitializeSettings::run()`) or a virtual-app + manifest declaring `credentials[]` is registered, OR calls + `CredentialAppTokenService::registerApp($appId)` for that app — + **guarded by `isRegistered($appId)`**: `registerApp` currently ROTATES the + signing secret on every call (verified), and an unguarded auto-run on each + upgrade would invalidate the app's held copy. Auto-onboarding therefore + registers only when absent; explicit rotation stays admin-only via the + existing `POST /api/credentials/apps/{appId}/register`. +- **Trusted in-process path:** in-process server-side consumers (e.g. + openconnector) call `CredentialBrokerService::request(credentialId, appId, + …)` directly, passing `appId` WITHOUT an HMAC token — the service method + already takes `appId` as a parameter, and token minting/verification was only + ever the *controller's* mechanism for proving app identity across the HTTP + boundary. Same-instance PHP is trusted: a malicious in-process caller could + equally forge a token, so the token adds no security there (it authenticates + claims across a trust boundary that in-process calls do not cross). Signed + tokens REMAIN required for every cross-runtime/HTTP caller; the HTTP + controller path is unchanged (`X-Credential-Token` verified, app id taken + ONLY from the verified claims, never a body field). + +### D-K — Background/system consumption: `actingUserId` for in-process callers only + +`CredentialBrokerService::request` gains an optional `?string $actingUserId = +null` parameter with strict semantics: + +- Honored ONLY when **no user session exists** (`IUserSession::getUser() === + null`): the owner guard (guard 1) then evaluates ownership against + `$actingUserId`. When a session exists, the **session wins unconditionally** + — a session-context caller cannot impersonate another user by passing + `actingUserId`. +- The **HTTP controller NEVER forwards an acting user** — `brokerRequest()` does + not read any such parameter; on the HTTP path identity is session-only, + exactly as today. Because the controller never forwards it, any call carrying + `actingUserId` is by construction an in-process PHP caller. +- **Trust boundary (explicit):** `actingUserId` is an *assertion by trusted + same-instance code* (background jobs, cron) that it acts for that user — the + broker cannot verify it and does not pretend to; it applies the same owner + + allowedApps + rules + host-lock guards against the asserted user, failing + closed. Consumers MUST derive `actingUserId` from durable job context (e.g. + the user who configured the sync), never from request input. Cross-runtime + callers get no such parameter — they authenticate per app token and act as + the session user only. +- All four guards, their order, and the fail-closed static-403 behaviour are + unchanged; the only delta is *which* user identity guard 1 compares against + in the sessionless in-process case. + +## Declarative-vs-imperative note (ADR-031) + +The broker/store remains an **external-integration exception — imperative +justified**: reaching an external HTTP host, enforcing an ordered security-guard +chain, RSA envelope crypto, and cross-app service invocation are exactly the +"what apps SHOULD still write in PHP" category (ADR-031; ADR-003), and no +`x-openregister-*` declarative extension expresses any of them. This change adds +no new declarative surface and removes none: credential *metadata* stays plain +declarative OR objects; the provider catalogue stays the runtime-immutable +declarative `lib/` JSON (credential-broker D2); custody, resolution, migration, +registration, and the trust-boundary logic are imperative services/repair steps +by design. (No seed-data section: this change makes no OR schema changes.) + +## Reuse Analysis (ADR-011) + +- **`CredentialStore` interface** — reused as-is; `DoriathCredentialStore` is the + second leaf the interface was built for. No signature changes. +- **`AnonymisationBackendService`** — the resolution pattern (availability + detection via `IAppManager` + lazy `class_exists`/`Server::get`, no + compile-time dependency on the optional app) is mirrored, not imported. +- **`DeckLinkService`** — the Deck-leaf availability idiom + (`isEnabledForUser('deck')`, degrade gracefully when absent) is the template + for Doriath eligibility. +- **`ICredentialsManager` system scope** — OR's Doriath private key uses the + exact idiom `CredentialAppTokenService` already uses (`store('', key, value)`). +- **Doriath `EncryptService`/`DecryptService`** — invoked cross-app precisely + BECAUSE they are stateless; no OR-side reimplementation of + `rsa-oaep-sha256-chunked-v1` (drift risk). +- **Repair conventions** — `ImportCredentialBrokerRegister` (never-throw, + info.xml install + post-migration) and `GenericInitializeSettings` + (degrade-don't-throw when a peer app is absent) are the templates for the + self-registration step and the D-G hook respectively. +- **NOT reused:** Doriath's machine HTTP API (`ApplicationSecretsController`) — + same-instance OR calls services in-process; HTTP-looping to localhost would + add auth surface for nothing. Doriath's `SecretMapper` is explicitly OFF + LIMITS (service seam only). + +## Risks / Trade-offs + +- [Cross-repo ordering] Doriath's `deleteByApplication` + application-scoped + read may land after this change → the resolver's `method_exists` probe keeps + OR on the vault leaf until the full seam exists; merge order is free. +- [Doriath disabled after adoption] Secrets already migrated to Doriath become + unreachable while it is disabled (resolver falls back to the vault, which no + longer holds them) → broker fails closed (static 403); re-enabling Doriath + restores access. Documented as operator guidance; no plaintext is ever + stranded outside a vault. (Fleet note: disabling openregister itself is + already a known instance-wide hazard.) +- [Private-key loss] `ICredentialsManager` row lost ⇒ ciphertext undecryptable + → fail closed per credential; the user re-enters the secret (put() re-encrypts + against the current keypair). No rotation tooling in this change. +- [Auto-registration rotating app secrets] → `isRegistered` guard (D-G); + rotation stays an explicit admin action. +- [actingUserId spoofing] → honored only sessionless + in-process (the + controller never forwards it); trust boundary documented in D-K; tests pin + "session wins" and "HTTP never forwards". +- [Migration races] Two concurrent reads may both migrate → `put()` upsert is + by-name idempotent, vault `delete()` is idempotent; worst case is a redundant + rotate audit event. + +## Migration Plan + +1. Land this change: resolver + Doriath leaf + repair step + D-G/D-K seams; all + inert on instances without an eligible Doriath (vault leaf keeps serving). +2. Doriath lands `application-secret-delete` (deleteByApplication + the + application-scoped read); the resolver's probe flips instances to the Doriath + leaf on their next request. +3. Existing vault secrets migrate lazily on first session read (D-A); no bulk + job. Rollback = revert the DI factory to the static vault alias — un-migrated + secrets were never moved, and migrated ones require re-entry (documented). diff --git a/openspec/changes/credential-doriath-leaf/proposal.md b/openspec/changes/credential-doriath-leaf/proposal.md new file mode 100644 index 0000000000..6b794fee78 --- /dev/null +++ b/openspec/changes/credential-doriath-leaf/proposal.md @@ -0,0 +1,121 @@ +--- +kind: code +depends_on: + - credential-broker +--- + +## Why + +The credential broker's secret custody currently has exactly one leaf: +`NextcloudVaultCredentialStore`, backed by `OCP\Security\ICredentialsManager` +per-user rows. That was the right first leaf (credential-broker design D1/D3), +but the fleet now ships a dedicated secrets app — **Doriath** — with real vault +semantics: per-application EncryptionSuites provisioned from a CSR, ciphertext- +only storage (`rsa-oaep-sha256-chunked-v1`), audit events on every secret +mutation, folders, rotation metadata, and an admin surface. Where Doriath is +installed, broker secrets should live there; where it is not, the NC vault must +keep working unchanged. The `CredentialStore` interface was designed exactly for +this second leaf. + +Two adjacent gaps close in the same change because they share the trust-boundary +design: (a) consuming apps still need a manual admin `registerApp` call before +they can use the broker — manifest-driven apps should onboard with no code and no +admin step (D-G); and (b) background jobs (e.g. scheduled ingestion using a +Doffin credential) have no user session, so the broker's owner guard can never +pass for them today (D-K). + +## What Changes + +All design decisions below are **signed off** (D-A, D-B, D-C, D-F, D-G, D-K) and +carried into design.md — they are inputs, not open questions. + +- **(D-A) Backend resolution.** A `CredentialStoreResolver` selects the store the + way `AnonymisationBackendService` selects anonymisation backends: prefer a new + `DoriathCredentialStore` when `IAppManager::isEnabledForUser('doriath')` AND the + Doriath service classes exist (`class_exists`, Deck-leaf pattern per + `DeckLinkService`); otherwise fall back to `NextcloudVaultCredentialStore`. The + DI alias in `lib/AppInfo/Application.php` (currently a static + `CredentialStore → NextcloudVaultCredentialStore` alias) becomes a resolver- + backed factory. **Lazy migration on read**: a Doriath miss that hits in the + vault re-puts the secret into Doriath and deletes the vault row. +- **(D-B) OR self-registers with Doriath** via a new idempotent Repair step: + generate an RSA-4096 keypair (openssl), keep the private key SYSTEM-scoped in + `ICredentialsManager`, self-generate a CSR, and call Doriath's + `ApplicationService::register(name: 'openregister', …, isAdmin: true)` + in-process — admin registration auto-approves the row and provisions the + EncryptionSuite from the CSR (Doriath validates PKCS#10 + ≥4096 bits). Doriath + assigns the application row's UUID; OR persists that UUID (and its own public + key PEM) in `IAppConfig`. Skips when already registered. +- **(D-C) Doriath-backed custody.** `DoriathCredentialStore::put()` encrypts the + secret against OR's own public certificate using Doriath's + `rsa-oaep-sha256-chunked-v1` scheme — preferring Doriath's stateless + `EncryptService` invoked cross-app for scheme compatibility — and stores it as + an application-owned Doriath secret (name = credential UUID, root folder). + `get()` reads the ciphertext in-process and decrypts via Doriath + `DecryptService::rsaDecrypt` with OR's private key. `delete()` calls Doriath's + NEW `SecretService::deleteByApplication` — a **cross-repo dependency** on the + Doriath change `application-secret-delete` (see Impact). +- **(D-F) Custody moves** from per-user NC vault rows to OR's single + application-owned Doriath vault; per-user isolation is enforced at the + broker/metadata layer (the owner IDOR guard on the credential OR object). + The trade-off is documented explicitly in design.md. +- **(D-G) Manifest-driven no-code onboarding.** When an AppHost leaf initialises + (`lib/AppHost/Repair/GenericInitializeSettings.php`) or a virtual-app manifest + declaring `credentials[]` is registered, OR auto-registers that app with + `CredentialAppTokenService::registerApp(appId)` — idempotently (never silently + rotating an existing signing secret). In-process server-side consumers (e.g. + openconnector) call `CredentialBrokerService::request` passing `appId` directly + WITHOUT an HMAC token — same-instance PHP is trusted; signed tokens remain + required for cross-runtime/HTTP callers. The HTTP controller path is unchanged. +- **(D-K) Background/system consumption.** `CredentialBrokerService::request` + gains an optional `actingUserId` parameter honored ONLY for in-process trusted + callers when no user session exists; the owner guard evaluates against it. The + HTTP controller NEVER forwards an acting user — session-only there. + +## Non-Goals + +- No change to the four ordered guards' order or semantics (owner → allowedApps + → allow-rules → host-lock, failing closed). +- No new HTTP endpoints; `CredentialController` routes and auth posture stay as + they are (the broker endpoint still requires the `X-Credential-Token`). +- No Doriath UI work and no changes to Doriath-side code from this repo — the + Doriath-side `deleteByApplication` (and the application-scoped in-process read + it pairs with) land in Doriath's own change. +- No bulk/eager migration of existing vault secrets — migration is lazy-on-read + only (D-A). +- No OAuth2 flows; Belgium OAuth2 and GitHub-for-spectr remain PARKED. + +## Capabilities + +### Modified Capabilities + +- `credential-broker`: secret custody becomes leaf-resolved (Doriath preferred, + NC vault fallback, lazy migration); OR self-registers as a Doriath application + via Repair; consuming apps onboard from their manifest with no admin step; + in-process trusted callers may pass `appId` (and, for background jobs, + `actingUserId`) directly while the HTTP surface keeps signed-token-only + identity. The credential metadata schema and the guard chain are unchanged. + +## Impact + +- **New code**: `lib/Service/Credential/CredentialStoreResolver.php`, + `lib/Service/Credential/DoriathCredentialStore.php`, `lib/Repair/` + self-registration step (+ `appinfo/info.xml` repair-steps entries). +- **Changed code**: `lib/AppInfo/Application.php` (DI alias → resolver factory, + ~line 347); `lib/Service/Credential/CredentialBrokerService.php` + (`actingUserId`, in-process trust path); `lib/Service/Credential/` + `CredentialAppTokenService.php` (idempotent register guard for auto-onboarding); + `lib/AppHost/Repair/GenericInitializeSettings.php` + the virtual-app manifest + registration path (D-G hook). +- **Cross-repo dependency (prose, not `depends_on` — different repo):** Doriath + change `application-secret-delete` must land `SecretService::deleteByApplication` + before `delete()` custody is complete. The same in-process seam needs an + application-scoped read-by-name: today only `SecretMapper::findByName('application', …)` + exists (consumed by Doriath's machine HTTP controller), with no service-level + equivalent — the Doriath change (or a sibling) must expose one for `get()`/ + `put()`-upsert. OR degrades gracefully until then (resolver falls back to the + vault leaf when the required Doriath classes/methods are absent). +- **No OR schema changes** — no seed data, no register JSON edits. +- **Security surface**: net-neutral or better — secrets gain at-rest RSA + envelope encryption + Doriath audit events; the owner IDOR guard remains the + per-user isolation boundary (D-F trade-off in design.md). diff --git a/openspec/changes/credential-doriath-leaf/specs/credential-broker/spec.md b/openspec/changes/credential-doriath-leaf/specs/credential-broker/spec.md new file mode 100644 index 0000000000..f043f04085 --- /dev/null +++ b/openspec/changes/credential-doriath-leaf/specs/credential-broker/spec.md @@ -0,0 +1,217 @@ +## Purpose + +Delta against the `credential-broker` capability: secret custody becomes +leaf-resolved (Doriath application vault preferred, Nextcloud vault fallback, +lazy read-time migration), OR self-registers as a Doriath application via +Repair, manifest-driven apps onboard with no admin step, and trusted in-process +callers gain a direct (token-less) call path plus a background acting-user +seam. The four ordered guards, the credential metadata schema's shape, the +provider catalogue, and the HTTP contract are unchanged. Note: the base +`credential-broker` spec still lives in the active head change +(`openspec/changes/credential-broker/specs/credential-broker/spec.md`); +`openspec/specs/credential-broker/` does not exist yet. + +## MODIFIED Requirements + +### Requirement: Credential metadata schema + +OpenRegister SHALL define a `credential` schema whose objects hold owner-scoped +metadata for a stored secret and MUST NOT contain the secret value. Each object +SHALL carry `name` (string), `provider` (a catalogue provider identifier), +`owner` (the owning user's OR-resolved identity), `allowedApps` (array of app +ids), and `createdAt` (ISO-8601 timestamp). The secret itself SHALL live behind +the `CredentialStore` abstraction, keyed by the credential's UUID — in +OpenRegister's application-owned Doriath vault when the Doriath leaf is active, +otherwise in the Nextcloud vault — and never in any `credential` object +property. + +#### Scenario: Credential schema declared in the register descriptor + +- **WHEN** the `credential-broker` register descriptor (`lib/Settings/credential_broker_register.json`) is read +- **THEN** it declares the `credential` schema with properties `name`, `provider`, `owner`, `allowedApps`, `createdAt` +- **AND** the schema declares no property that stores a secret/token value + +#### Scenario: Metadata never carries a secret + +- **WHEN** a `credential` object is inspected via list, export, audit, or GraphQL +- **THEN** no secret or token value is present in the object +- **AND** only the metadata handle (`name`, `provider`, `owner`, `allowedApps`, `createdAt`) is returned + +#### Scenario: Secret custody is store-resolved, never object-resident + +- **WHEN** a credential's secret is stored or rotated +- **THEN** it is written only through the resolved `CredentialStore` leaf, keyed by the credential UUID +- **AND** no OR object, log line, export, or API response ever contains the secret value + +## ADDED Requirements + +### Requirement: Credential store backend resolution + +OpenRegister SHALL resolve the active `CredentialStore` leaf at runtime through a +resolver that prefers a Doriath-backed store when the `doriath` app is enabled for +the instance (`IAppManager::isEnabledForUser('doriath')`), the required Doriath +service classes exist (`class_exists` — no compile-time dependency), the required +application-scoped service methods exist, and OpenRegister's Doriath +self-registration state is present. When any condition fails, the resolver SHALL +fall back to the Nextcloud-vault store. Consumers (broker, controller) SHALL keep +depending only on the `CredentialStore` interface; the selection SHALL be wired in +DI (replacing the static alias), with zero call-site changes. + +#### Scenario: Doriath eligible selects the Doriath store + +- **WHEN** the `doriath` app is enabled, its service classes and application-scoped methods exist, and OpenRegister is registered as a Doriath application +- **THEN** the resolver yields the Doriath-backed `CredentialStore` leaf + +#### Scenario: Doriath absent falls back to the vault store + +- **WHEN** the `doriath` app is not installed, is disabled, or lacks the required classes/methods +- **THEN** the resolver yields the Nextcloud-vault `CredentialStore` leaf +- **AND** all broker and credential CRUD behaviour continues unchanged + +### Requirement: Lazy migration of vault secrets to Doriath + +When the Doriath leaf is active, a secret read that misses in Doriath SHALL fall +back to the Nextcloud vault; on a vault hit, the store SHALL re-put the secret +into Doriath and delete the vault row, then return the secret. Writes and deletes +SHALL target Doriath (deletes SHALL also clear any residual vault row). Migration +SHALL only occur in a user-session context (the vault is session-scoped); a +sessionless read of an un-migrated secret SHALL fail closed. + +#### Scenario: Read migrates a legacy vault secret + +- **WHEN** the Doriath leaf is active and `get()` misses in Doriath but hits in the Nextcloud vault +- **THEN** the secret is stored into Doriath, deleted from the vault, and returned to the caller +- **AND** a subsequent `get` is served from Doriath alone + +#### Scenario: Sessionless read of an un-migrated secret fails closed + +- **WHEN** a background (no-session) broker call reaches a credential whose secret still lives only in the per-user Nextcloud vault +- **THEN** the read misses in both stores and the broker denies with the static 403 +- **AND** no partial migration occurs + +### Requirement: OpenRegister self-registration as a Doriath application + +OpenRegister SHALL register itself with Doriath through an idempotent repair step +(install + post-migration) that: generates an RSA-4096 keypair; stores the private +key SYSTEM-scoped in `ICredentialsManager` (never in `IAppConfig`, an OR object, or +a log); self-generates a PKCS#10 CSR from the keypair; and calls Doriath's +`ApplicationService::register` in-process with the name `openregister`, type +`internal`, the CSR, and `isAdmin: true` — so the registration auto-approves and +Doriath provisions the EncryptionSuite from the CSR. OpenRegister SHALL persist +the Doriath-assigned application UUID and its own public key PEM in `IAppConfig`. +The step SHALL skip when already registered and SHALL degrade (warn, not fail) +when Doriath is unavailable. + +#### Scenario: First run registers and provisions + +- **WHEN** the repair step runs on an instance with an eligible Doriath and no prior registration +- **THEN** an RSA-4096 keypair is generated, the private key is stored system-scoped in `ICredentialsManager`, and Doriath registers an active `openregister` application with an EncryptionSuite provisioned from the CSR +- **AND** the Doriath-assigned application UUID and the public key PEM are persisted in `IAppConfig` + +#### Scenario: Re-run is a no-op + +- **WHEN** the repair step runs again after a successful registration +- **THEN** it detects the existing registration and makes no new keypair, CSR, registration call, or suite + +#### Scenario: Doriath unavailable degrades + +- **WHEN** the repair step runs while Doriath is absent or disabled +- **THEN** it logs/outputs a warning and completes without error +- **AND** the credential broker keeps operating on the Nextcloud-vault leaf + +### Requirement: Doriath-backed secret custody + +The Doriath-backed store SHALL keep custody as application-owned Doriath secrets +in OpenRegister's single application vault: secret name equal to the credential +UUID, root folder. `put` SHALL encrypt the secret against OpenRegister's own +public certificate using Doriath's `rsa-oaep-sha256-chunked-v1` scheme — +preferring Doriath's stateless `EncryptService` invoked cross-app for scheme +compatibility — and SHALL upsert by name (create via +`SecretService::createByApplication`, rotate via `updateByApplication`). `get` +SHALL read the ciphertext in-process from the application vault and decrypt via +Doriath's `DecryptService::rsaDecrypt` with OpenRegister's private key from the +system-scoped `ICredentialsManager`. `delete` SHALL call Doriath's +`SecretService::deleteByApplication` (cross-repo dependency: Doriath change +`application-secret-delete`) and SHALL be idempotent. Doriath SHALL only ever +hold ciphertext for brokered secrets; the plaintext SHALL exist only in +OpenRegister process memory and never in a log, response, or OR object. +OpenRegister SHALL access Doriath through service-level seams only, never +Doriath's database mappers. + +#### Scenario: Put stores ciphertext under the credential UUID + +- **WHEN** `put(, )` runs on the Doriath leaf +- **THEN** the stored Doriath secret is application-owned, named ``, in the root folder, and its key material is the `rsa-oaep-sha256-chunked-v1` ciphertext of the secret under OpenRegister's public certificate +- **AND** a repeated `put` for the same UUID rotates the same row instead of creating a duplicate + +#### Scenario: Get round-trips the secret + +- **WHEN** `get()` runs on the Doriath leaf for a stored secret +- **THEN** the in-process read returns the ciphertext and decryption with OpenRegister's private key yields the original secret +- **AND** the plaintext appears in no log line, error, or API response + +#### Scenario: Delete is idempotent and complete + +- **WHEN** `delete()` runs on the Doriath leaf +- **THEN** the application-owned Doriath secret is removed via `deleteByApplication` and any residual Nextcloud-vault row is cleared +- **AND** deleting an absent secret is a no-op + +#### Scenario: Per-user isolation enforced at the broker layer + +- **WHEN** an authenticated user attempts to broker a call with another user's credential id while custody lives in the shared application vault +- **THEN** the owner guard (guard 1) denies with the static 403 before any store read occurs + +### Requirement: Manifest-driven credential app onboarding + +OpenRegister SHALL auto-register a consuming app with the credential broker +(`CredentialAppTokenService::registerApp(appId)`) when an AppHost leaf +initialises or a virtual-app manifest declaring `credentials[]` is registered — +idempotently: auto-onboarding SHALL skip apps that already hold a signing secret +and SHALL never silently rotate one. In-process server-side consumers MAY call +`CredentialBrokerService::request` passing their `appId` directly without an HMAC +token — same-instance PHP is trusted. Signed tokens SHALL remain required for all +cross-runtime and HTTP callers, and the HTTP controller path SHALL be unchanged +(app identity taken only from the verified `X-Credential-Token`, never a body +field). + +#### Scenario: Leaf initialisation registers the app once + +- **WHEN** an AppHost leaf whose manifest declares `credentials[]` initialises for the first time +- **THEN** the app is registered with the broker without any admin action +- **AND** a subsequent initialisation run detects the existing registration and does not rotate the signing secret + +#### Scenario: In-process consumer calls without a token + +- **WHEN** an in-process server-side consumer invokes `CredentialBrokerService::request` with its own `appId` and no HMAC token +- **THEN** the four ordered guards run unchanged against that `appId` (including `allowedApps[]`) +- **AND** the call succeeds or fails solely on the guards + +#### Scenario: HTTP path still requires the signed token + +- **WHEN** an HTTP caller posts to the broker endpoint without a valid `X-Credential-Token` +- **THEN** the request is denied with the static 403 exactly as before this change + +### Requirement: Background acting-user resolution + +`CredentialBrokerService::request` SHALL accept an optional `actingUserId` +honored ONLY for in-process trusted callers when no user session exists; the +owner guard SHALL then evaluate ownership against `actingUserId`. When a user +session exists, the session identity SHALL win unconditionally. The HTTP +controller SHALL NEVER forward an acting user — on the HTTP path identity +remains session-only. All guard ordering and fail-closed behaviour SHALL be +unchanged. + +#### Scenario: Background job acts for its configuring user + +- **WHEN** a background job (no user session) invokes the broker in-process with `actingUserId` set to the credential owner's id +- **THEN** the owner guard evaluates against `actingUserId` and the call proceeds through the remaining guards + +#### Scenario: Session identity cannot be overridden + +- **WHEN** a caller with an active user session passes an `actingUserId` differing from the session user +- **THEN** the owner guard evaluates against the session user, ignoring `actingUserId` + +#### Scenario: HTTP callers cannot supply an acting user + +- **WHEN** an HTTP request to the broker endpoint carries any acting-user parameter +- **THEN** the controller ignores it entirely and the owner guard uses only the session identity diff --git a/openspec/changes/credential-doriath-leaf/tasks.md b/openspec/changes/credential-doriath-leaf/tasks.md new file mode 100644 index 0000000000..76ad6c40e3 --- /dev/null +++ b/openspec/changes/credential-doriath-leaf/tasks.md @@ -0,0 +1,48 @@ +## 1. Store resolution (D-A) + +- [x] 1.1 Add `lib/Service/Credential/CredentialStoreResolver.php` — Doriath eligibility = `IAppManager::isEnabledForUser('doriath')` + `class_exists` on the Doriath service classes + `method_exists` on the application-scoped seam (`deleteByApplication`, read-by-name) + self-registration state in `IAppConfig`; returns `DoriathCredentialStore` or `NextcloudVaultCredentialStore`. +- [x] 1.2 Replace the static `CredentialStore → NextcloudVaultCredentialStore` DI alias in `lib/AppInfo/Application.php` (~line 347) with a resolver-backed `registerService` factory; no call-site changes. + +## 2. Doriath leaf (D-C, D-F, D-A migration) + +- [x] 2.1 Add `lib/Service/Credential/DoriathCredentialStore.php` implementing `CredentialStore`: `put` = encrypt via Doriath stateless `EncryptService::rsaEncrypt` against OR's public key PEM (`rsa-oaep-sha256-chunked-v1`), upsert application-owned secret (name = credential UUID, root folder) via `createByApplication`/`updateByApplication`; `get` = in-process read-by-name + `DecryptService::rsaDecrypt` with the system-scoped private key; `delete` = `SecretService::deleteByApplication`, idempotent, plus residual-vault cleanup; >1 name match = fail closed, secret-free log. +- [x] 2.2 Implement lazy migration inside `DoriathCredentialStore` (composes the vault leaf): Doriath miss → vault hit → re-put into Doriath → vault delete → return; sessionless reads of un-migrated secrets fail closed. + +## 3. Self-registration repair step (D-B) + +- [x] 3.1 Add `lib/Repair/RegisterOpenRegisterWithDoriath.php`: skip when registered (IAppConfig UUID + live Doriath row) or Doriath ineligible (warn, never throw); else openssl RSA-4096 keypair → private key `ICredentialsManager` system scope → self-generated PKCS#10 CSR → in-process `ApplicationService::register(name: 'openregister', type: 'internal', csr, userId: null, isAdmin: true)` → persist Doriath-assigned application UUID + public key PEM in `IAppConfig`. +- [x] 3.2 Register the step in `appinfo/info.xml` under `` and ``. + +## 4. No-code onboarding + in-process trust (D-G) + +- [x] 4.1 Hook `lib/AppHost/Repair/GenericInitializeSettings.php` (and the virtual-app manifest registration path) to call `CredentialAppTokenService::registerApp(appId)` for manifests declaring `credentials[]`, guarded by `isRegistered(appId)` so auto-runs never rotate an existing signing secret. +- [x] 4.2 Document (docblocks) the in-process trusted path: same-instance PHP callers pass `appId` to `CredentialBrokerService::request` directly without an HMAC token; HTTP/cross-runtime callers keep signed tokens; controller unchanged. + +## 5. Background acting user (D-K) + +- [x] 5.1 Add optional `?string $actingUserId = null` to `CredentialBrokerService::request`; owner guard uses it ONLY when `IUserSession` has no user; session identity wins unconditionally otherwise. +- [x] 5.2 Assert `CredentialController::brokerRequest` forwards no acting user (session-only on HTTP) — controller diff must be empty apart from docblocks. + +## 6. Tests + verification + +- [x] 6.1 Unit tests: resolver eligibility matrix (enabled/disabled/missing classes/missing methods/unregistered), Doriath leaf put-rotate/get/delete + ambiguity fail-closed, lazy migration (migrates once; sessionless fails closed), repair idempotency + degrade, D-G no-rotation guard, D-K session-wins + sessionless-acting-user + HTTP-never-forwards. +- [x] 6.2 IDOR pin: user B can never broker user A's credential on the Doriath path (owner guard before any store read). +- [x] 6.3 `composer check:strict`; `@spec` tags on all new/changed methods; no secret/plaintext in any log or error path (grep-level check). + +## Acceptance criteria + +- With Doriath ineligible, behaviour is byte-for-byte today's: vault leaf, no new calls, repair steps warn-and-skip. +- With Doriath eligible, secrets live ONLY as application-owned ciphertext (name = credential UUID, root folder); plaintext never leaves OR process memory. +- OR's Doriath private key exists only system-scoped in `ICredentialsManager`; the application UUID and public PEM in `IAppConfig` carry no secret material. +- Lazy migration moves a vault secret exactly once and deletes the vault row; deletes clear both stores. +- Auto-onboarding never rotates an existing app signing secret; explicit admin rotation still works. +- `actingUserId` is inert on the HTTP path and whenever a session exists. +- Cross-repo: OR merges safely before or after Doriath's `application-secret-delete` (resolver probes `method_exists`). +- Examples/tests use only placeholders: `YOUR_API_KEY_HERE`, nil UUID `00000000-0000-0000-0000-000000000000`, ``. + +## Quality checklist + +- ADR-031: broker/store remains the documented external-integration imperative exception; no new declarative surface; no seed data (no OR schema changes). +- ADR-005: guards unchanged, fail-closed, static generic errors, no secret/PII in logs; owner IDOR guard is the user-isolation boundary (D-F trade-off documented in design.md). +- ADR-011: Doriath crypto reused cross-app (no scheme reimplementation); service seams only, never Doriath mappers. +- Cross-repo dependency on Doriath `application-secret-delete` (+ application-scoped read) recorded in proposal Impact; no OR-internal dependency on `credential-provider-doffin` (siblings). diff --git a/openspec/changes/credential-provider-doffin/.openspec.yaml b/openspec/changes/credential-provider-doffin/.openspec.yaml new file mode 100644 index 0000000000..dd9a1d92eb --- /dev/null +++ b/openspec/changes/credential-provider-doffin/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-06 diff --git a/openspec/changes/credential-provider-doffin/design.md b/openspec/changes/credential-provider-doffin/design.md new file mode 100644 index 0000000000..bbf126be00 --- /dev/null +++ b/openspec/changes/credential-provider-doffin/design.md @@ -0,0 +1,96 @@ +## Context + +The credential-broker provider catalogue (`lib/Settings/credential-providers.json`) +is a runtime-immutable security control (credential-broker design D2): its +`allowRules[]` bound exactly which method + path any credential can ever reach on a +host-locked base. New providers ship only through a reviewed release. This change is +such a release step: it adds the `doffin` provider so spectr's Norway ingestion can +consume a broker-held subscription key instead of a key pasted into an OpenConnector +source config. + +Source of truth for the provider shape (verified, not assumed): + +- **Connector contract** — `spectr/connectors/norway_doffin.json` (read-only, main + spectr checkout): `source.location` = `https://betaapi.doffin.no/public/v2`, + auth = header `Ocp-Apim-Subscription-Key: `, and the synchronisation calls + exactly one endpoint: `GET /notices` with `cpvCodes` / `pageNumber` / `pageSize` + query parameters. No detail-fetch (`extraDataConfigs`) is configured. +- **Broker mechanics** (worktree HEAD) — `CredentialBrokerService::normalisePath()` + strips the query string before rule matching and `assertRuleAllowed()` matches + with exact method + `fnmatch(pathPattern, path)`; `resolveAndLockUrl()` appends + the caller path to `baseUrl` and enforces the host equality. `injectAuth()` sets + `header => str_replace('{secret}', $secret, template)` and discards any + caller-supplied value for that header. + +## Decisions + +### DF-1 — Base path lives in `baseUrl`, mirroring the `gitlab` precedent + +`baseUrl` is `https://betaapi.doffin.no/public/v2` (not the bare host), exactly as +`gitlab` ships `https://gitlab.com/api/v4`. Callers then supply provider-relative +paths (`/notices`), and the allow-rules stay short and readable. The host-lock guard +operates on the parsed host (`betaapi.doffin.no`) regardless of the base path. + +*Alternative rejected:* bare-host `baseUrl` with `/public/v2/notices` rules — works, +but breaks the catalogue's established convention and makes every future rule longer. + +### DF-2 — GET-only, single allow-rule: `GET /notices` + +The connector uses exactly one path. The rule set is therefore the minimum: + +```jsonc +"allowRules": [ { "method": "GET", "pathPattern": "/notices" } ] +``` + +Query parameters (`cpvCodes`, `pageNumber`, `pageSize`) need no pattern coverage — +the broker matches on the query-stripped path. A single-notice detail path +(`/notices/`-style) is deliberately NOT pre-authorised: the connector does not +use one today, and widening later is a cheap reviewed release, whereas shipping an +unused rule is a permanent surface. Doffin's Public API is read-only for our use +case; no non-GET rule may ever be added to this entry without a new review. + +### DF-3 — `authScheme` template is the bare `{secret}` + +Azure API Management subscription keys are sent as the raw header value — +`Ocp-Apim-Subscription-Key: ` — with no `token`/`Bearer` prefix (verified +against the connector's `configuration.headers`). So: + +```jsonc +"authScheme": { "header": "Ocp-Apim-Subscription-Key", "template": "{secret}" } +``` + +`injectAuth()` already owns the header (any caller-supplied +`Ocp-Apim-Subscription-Key` is discarded before injection), so a caller can never +substitute their own key. + +## Declarative-vs-imperative note (ADR-031) + +Unchanged from credential-broker D2: the catalogue is a deliberate, +security-justified exception to declarative-first-in-the-register — a runtime- +immutable declarative JSON file in `lib/`, never API-writable. This change adds an +entry to that file through the exact mechanism D2 prescribes (code review + +release). No imperative code is added; the change stays `kind: config`. + +## Risks / Trade-offs + +- [Beta API path unverified live] The connector's own 2026-07-06 probe could not + disambiguate Azure APIM's 404 (route masking with an invalid key vs. a moved + beta path) without a real subscription key. The rule follows the connector's + documented contract (`GET /notices`). If the path has moved, the broker fails + CLOSED (403 on the unmatched path) — the fix is a follow-up reviewed catalogue + release, never a runtime widening. The same applies if Doffin promotes + `betaapi.doffin.no` to a production host: that is a `baseUrl` change in a + reviewed release. +- [Secret exposure] None added: the subscription key lives behind the existing + `CredentialStore`; this file carries only the `{secret}` placeholder, never a + key. Examples in specs/tasks use `YOUR_API_KEY_HERE` placeholders only. +- [Regression to existing providers] The entry is additive; `github`/`gitlab` + stay byte-identical and the acceptance criteria pin that. + +## Migration Plan + +1. Add the `doffin` entry + bump the catalogue `version` (apply task). +2. No data migration, no repair step, no schema change. Rollback = revert the file. +3. Operator flow after release: obtain the Doffin subscription key via the + developer portal (manual), create an OR credential with provider `doffin` + in Personal settings, and allow the consuming app on it. diff --git a/openspec/changes/credential-provider-doffin/proposal.md b/openspec/changes/credential-provider-doffin/proposal.md new file mode 100644 index 0000000000..ddab92c8a3 --- /dev/null +++ b/openspec/changes/credential-provider-doffin/proposal.md @@ -0,0 +1,84 @@ +--- +kind: config +depends_on: + - credential-broker +--- + +## Why + +Spectr's Norway/Doffin ingestion (the W3-revival OpenConnector config at +`spectr/connectors/norway_doffin.json`) is blocked on a real Doffin Public API +subscription key. Today that connector ships as a documented draft with a +placeholder key pasted directly into the OpenConnector source configuration — +exactly the "paste your long-lived secret into the consuming app" anti-pattern +the credential broker exists to remove. The broker's provider catalogue +(`lib/Settings/credential-providers.json`, runtime-immutable per +credential-broker design D2) currently ships only `github` and `gitlab`, so no +credential can legally target Doffin: the broker fails closed on an unknown +provider. Adding providers requires a reviewed release — this change IS that +reviewed release step for `doffin`. + +## What Changes + +- Add a `doffin` entry to the runtime-immutable provider catalogue + `lib/Settings/credential-providers.json`, mirroring the exact shape of the + existing `github`/`gitlab` entries: + - `baseUrl`: `https://betaapi.doffin.no/public/v2` — host-locked; verified + against the spectr connector's `source.location` (Doffin's Azure API + Management-hosted Public API, still on its beta host). + - `authScheme`: header `Ocp-Apim-Subscription-Key`, template `{secret}` — + the APIM subscription-key header carries the bare key, no prefix + (verified against the connector's `configuration.headers`). + - `allowRules`: **GET-only**, limited to the single notice-search path the + connector actually uses: `GET /notices` (the connector's + `sourceConfig.endpoint` relative to the base; its `cpvCodes` / + `pageNumber` / `pageSize` query parameters are stripped by the broker's + path normalisation before rule matching, so the exact pattern `/notices` + covers every search call). +- Bump the catalogue's top-level `version`. +- No code, no endpoints, no schema changes — the broker, catalogue loader, and + guard chain already handle any catalogue entry generically. + +## Non-Goals + +- **Belgium OAuth2 provider — PARKED** per program decision (round 2, + 2026-07-06). The catalogue's `authScheme` is header-injection only; an + OAuth2 token-refresh flow is a broker feature, not a catalogue entry, and is + explicitly out of scope here. +- **GitHub-for-spectr — PARKED** per the same program decision. The existing + `github` entry is untouched; no spectr-specific GitHub rules are added. +- No widening of the existing `github`/`gitlab` entries. +- No runtime/API mutation path for the catalogue — the file stays + runtime-immutable (credential-broker D2); this change is itself the + reviewed-release mechanism that D2 prescribes. +- Obtaining the real Doffin subscription key (developer-portal signup) stays a + manual operator step ("Ruben territory"); the key is pasted once into the OR + credential settings UI, never into this file or any connector JSON. + +## Capabilities + +### Modified Capabilities + +- `credential-broker`: the provider catalogue gains a third entry, `doffin` — + host-locked to Doffin's Public API base, subscription-key header injection, + GET-only allow-rules scoped to the notice-search path. The catalogue + requirement itself (runtime-immutable `lib/` file, no mutation API) is + unchanged; this is an additive entry shipped through the designed + reviewed-release path. + +## Impact + +- **Changed file**: `lib/Settings/credential-providers.json` (additive entry + + version bump). `github` and `gitlab` entries stay byte-identical. +- **No PHP/Vue changes**: `ProviderCatalogue`, `CredentialBrokerService` (four + ordered guards incl. `fnmatch` rule matching and host-lock), and + `CredentialController::providers()` consume catalogue entries generically. +- **Downstream**: once merged, a user stores the Doffin subscription key ONCE + as an OR credential (provider `doffin`), and the spectr ingestion path can + call `GET /notices` through the broker without any app holding the key. +- **Known caveat carried from the connector**: the connector's own live check + (2026-07-06) could not confirm the `/notices` path against a real key + (Azure APIM returned 404 for keyless/fake-key probes — route masking or a + moved beta path). The allow-rule follows the connector's documented + contract; if the path moved, correcting it is another reviewed catalogue + release — the failure mode is fail-closed (403), never a widened surface. diff --git a/openspec/changes/credential-provider-doffin/specs/credential-broker/spec.md b/openspec/changes/credential-provider-doffin/specs/credential-broker/spec.md new file mode 100644 index 0000000000..7859e898cb --- /dev/null +++ b/openspec/changes/credential-provider-doffin/specs/credential-broker/spec.md @@ -0,0 +1,53 @@ +## Purpose + +Delta against the `credential-broker` capability: the runtime-immutable provider +catalogue (`lib/Settings/credential-providers.json`) gains a `doffin` entry so the +Doffin (Norway public procurement) subscription key can be held once by the broker. +The catalogue's own requirement (runtime-immutable `lib/` file, no mutation API, +entries change only via reviewed release) is unchanged — this delta is one such +reviewed release. Note: the base `credential-broker` spec still lives in the active +head change (`openspec/changes/credential-broker/specs/credential-broker/spec.md`); +`openspec/specs/credential-broker/` does not exist yet. + +## ADDED Requirements + +### Requirement: Doffin provider entry + +The provider catalogue SHALL include a `doffin` entry declaring the Doffin Public +API as a host-locked, GET-only provider. The entry SHALL declare `baseUrl` +`https://betaapi.doffin.no/public/v2`, an `authScheme` injecting the header +`Ocp-Apim-Subscription-Key` whose template is exactly the `{secret}` placeholder +(no prefix), and `allowRules` containing ONLY GET rules limited to the +notice-search path used by the spectr ingestion connector (`/notices`). No rule +for the `doffin` provider SHALL permit a non-GET method, and the entry SHALL be +mutable only through a reviewed release like every other catalogue entry. + +#### Scenario: Doffin entry present and host-locked + +- **WHEN** the provider catalogue is loaded +- **THEN** a `doffin` entry exists with `identifier` `doffin` and `baseUrl` + `https://betaapi.doffin.no/public/v2` +- **AND** any brokered call for a `doffin` credential resolves only to the + `betaapi.doffin.no` host (host-lock guard) + +#### Scenario: Subscription-key header injected + +- **WHEN** the broker performs a permitted call for a `doffin` credential whose + stored secret is `YOUR_API_KEY_HERE` +- **THEN** the outbound request carries the header + `Ocp-Apim-Subscription-Key: YOUR_API_KEY_HERE` +- **AND** any caller-supplied `Ocp-Apim-Subscription-Key` header is discarded + before injection + +#### Scenario: Notice search allowed with query parameters + +- **WHEN** a brokered call requests `GET /notices?cpvCodes=48000000&pageNumber=1&pageSize=25` +- **THEN** the query string is stripped for rule matching and the `GET /notices` + allow-rule matches, so the call proceeds + +#### Scenario: Non-GET and unlisted paths are denied + +- **WHEN** a brokered call requests `POST /notices`, or `GET` on any path other + than `/notices` +- **THEN** no `doffin` allow-rule matches and the broker denies the call with the + static 403, failing closed diff --git a/openspec/changes/credential-provider-doffin/tasks.md b/openspec/changes/credential-provider-doffin/tasks.md new file mode 100644 index 0000000000..dd1a98e57f --- /dev/null +++ b/openspec/changes/credential-provider-doffin/tasks.md @@ -0,0 +1,24 @@ +## 1. Catalogue entry + +- [x] 1.1 Add the `doffin` entry to `lib/Settings/credential-providers.json`: `identifier` `doffin`, `title` `Doffin (Norway)`, `baseUrl` `https://betaapi.doffin.no/public/v2`, `authScheme` `{header: "Ocp-Apim-Subscription-Key", template: "{secret}"}`, `allowRules` `[{method: "GET", pathPattern: "/notices"}]` — GET-only, nothing else. Leave `github`/`gitlab` byte-identical. +- [x] 1.2 Bump the catalogue's top-level `version` and extend the file's `$comment` only if the doffin caveat (beta host, path carried from the spectr connector contract) needs recording there. + +## 2. Verification + +- [x] 2.1 JSON-validate the catalogue file; run `composer check:strict`. +- [x] 2.2 Add/extend unit coverage: `ProviderCatalogue::get('doffin')` returns the entry; broker rule matching permits `GET /notices` (with query string) and denies `POST /notices` and `GET` on any other path; host-lock resolves to `betaapi.doffin.no`. +- [x] 2.3 Confirm no new mutation surface: no route, controller method, or service writes the catalogue (unchanged invariant; grep-level check is enough). + +## Acceptance criteria + +- The `doffin` catalogue entry exists with host-locked `baseUrl` `https://betaapi.doffin.no/public/v2`, header `Ocp-Apim-Subscription-Key`, template exactly `{secret}`, and a GET-only rule set limited to `/notices`. +- `github` and `gitlab` entries are unchanged (byte-identical). +- No secret, key, or realistic-looking token appears anywhere in the diff — placeholders only (`YOUR_API_KEY_HERE` in tests/spec examples). +- The catalogue remains runtime-immutable: no API can create, update, or delete the `doffin` entry or its rules. +- Broker behaviour for unknown/unlisted doffin paths is fail-closed (static 403). + +## Quality checklist + +- ADR-031: catalogue stays the reviewed-release, runtime-immutable exception (credential-broker D2); this change is additive config only. +- Belgium OAuth2 and GitHub-for-spectr remain PARKED — nothing in this change touches or anticipates them. +- Kind stays `config`: no PHP/Vue surface ships beyond test coverage of existing generic code paths. diff --git a/openspec/changes/cross-app-semantic-references/.openspec.yaml b/openspec/changes/cross-app-semantic-references/.openspec.yaml new file mode 100644 index 0000000000..43e65ca6e6 --- /dev/null +++ b/openspec/changes/cross-app-semantic-references/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-03 diff --git a/openspec/changes/cross-app-semantic-references/design.md b/openspec/changes/cross-app-semantic-references/design.md new file mode 100644 index 0000000000..9a8309964f --- /dev/null +++ b/openspec/changes/cross-app-semantic-references/design.md @@ -0,0 +1,147 @@ +# Design: Cross-app object references by canonical semantic type + +## Guiding principle + +Additive extension of ADR-019. The reference *linkage key* changes from an +app-coupled identifier (schema slug for `$ref`, integration id for +`referenceType`) to a **canonical semantic-type URI** resolved across *all +installed schemas*, with a **null-safe** resolver and an **explicit disabled +affordance** when no provider is installed. + +## Building blocks that already exist (reuse, do not reinvent) + +| Need | Existing asset | +| --- | --- | +| Schema-side semantic type | `Schema.configuration.jsonld.type` — validated by `JsonLdContextService::validateMapping()`, serialized as object `@type`, schema.org-first per ADR-011 | +| IRI validation | `JsonLdContextService::isAbsoluteIri()` | +| Property reference marker precedent | `referenceType` (+ `PropertyReferenceTypeValidator`), surfaced in `fieldsFromSchema` (`schema.js`) and rendered in `CnFormDialog::resolveReferenceWidget()` | +| Cross-register schema enumeration | `SchemaMapper::findAll()` (org/RBAC-scoped, no register filter) | +| Config→entity resolver precedent | `RegisterResolverService` (request-scoped cache, typed exceptions) | +| Absent-app detection | `useAppStatus` / `isAppInstalled` / `resolveProviderAvailability` (reason ∈ `available|missing-app|not-configured|unknown`) | +| "{App} not available" copy | `CnIntegrationWidget.vue` empty-state | +| Disabled-field plumbing | `CnFormDialog` binds `:disabled` on every widget; `helper-text` from `field.description` | + +## What is genuinely new + +1. `configuration.implements: string[]` (alias, defaults to the union of + `jsonld.type` and the `x-schema-org` marker). +2. A schema-level `x-schema-org` marker that survives save/import: a **top-level** + `x-schema-org` (the fleet's ADR-011 annotation form, a sibling of + `properties`) is folded into `configuration['x-schema-org']` on hydrate — the + same fold already applied to `x-openregister-*` blocks — so the live schema + keeps the marker instead of dropping it (the previous behaviour that left + every real imported schema unresolvable). +3. Property keyword `referenceSemanticType: `. +4. `SemanticTypeResolver::resolveSchemaByImplements(uri): ?Schema`, which + additionally treats a provider whose owning app is **disabled** as absent. +5. Discovery of resolved providers to the frontend. +6. `CnFormDialog` degradation branch for an unresolved semantic reference. + +## Data model + +### Provider side (e.g. shillinq `Payee`) + +Fleet schemas declare the marker at the **top level** (schema.org-first, +ADR-011); on save it is folded into `configuration` and becomes resolvable with +no manual `implements`: +```jsonc +{ + "title": "Payee", + "x-schema-org": "schema:Organization", // top-level, folded on save → + "properties": { /* ... */ } + // configuration['x-schema-org'] = "schema:Organization" (persisted) + // implements defaults to ["https://schema.org/Organization"] +} +``` +Equivalently, a schema MAY carry `configuration.jsonld.type`, +`configuration['x-schema-org']`, or an explicit `configuration.implements[]` — +all three feed `JsonLdContextService::getImplementedTypes()`. + +### Consumer side (pipelinq `product`) +```jsonc +"buyCost": { + "title": "Buy cost", "type": "number", + "description": "Purchase/cost price. Sourced from the vendor when available." +}, +"vendor": { + "title": "Vendor", "type": "string", "format": "uuid", + "referenceSemanticType": "https://schema.org/Organization", + "referenceSemanticApp": "shillinq" +} +``` +`referenceSemanticApp` (optional) is a **hint only** — used for the "install +shillinq" tooltip copy and as a tie-break, never a hard requirement. Resolution +is still by URI, so any app exposing `schema.org/Organization` satisfies it. + +## Resolution algorithm (`SemanticTypeResolver`) + +``` +resolveSchemaByImplements(uri): + cache-hit? return it + candidates = SchemaMapper.findAll() # org/RBAC-scoped, all registers + .filter(s => uri in getImplementedTypes(s)) # implements[] ∪ jsonld.type ∪ x-schema-org + .filter(s => owning app of s is installed AND enabled) # disabled app ⇒ absent + if empty: cache null; return null # standalone-safe + pick = deterministic(candidates): # any adherer is acceptable — no clever tie-break + first by slug, optionally biased to the consuming register when that hint is given + cache pick; return pick +``` +- **No sophisticated tie-break** (product-owner decision): any schema adhering to + the URI is an acceptable provider. Selection is just deterministic-first-by-slug, + with an optional trivial bias to the consuming register kept from the earlier + design because it costs nothing. +- **App-enabled gate**: a provider whose owning app is not enabled is skipped as + if uninstalled — via `IAppManager::isEnabledForUser` (falling back to + `isInstalled`), fully null-safe. The owning app id is taken from the schema's + own `application` field first (the reliable per-schema signal — a register's + `application` column is frequently null), then the owning register's + `application`. Core `openregister` / schemas with no declared owning app are + never filtered out. +- Never throws on "not found" — returns `null`. +- Request-scoped cache keyed by `uri` (+ consuming register when biasing), + mirroring `JsonLdContextService::$contextCache`. +- A `WARN`-level log (not an error) when >1 candidate matches, naming the pick, + so ambiguous vocab is observable without breaking rendering. + +## Discovery surface + +Extend the existing integrations OCS capability payload +(`availability.js` already reads `openregister.integrations.providers[]`) with a +parallel `semanticProviders` list: `[{ uri, register, schema, appId, available, +reason }]`, computed server-side from resolvable schemas + `getRequiredApp` +availability. Plus a thin `@NoAdminRequired` `SchemasController` action +`resolveByImplements(uri)` for on-demand lookup. The frontend prefers the +capability payload (no round-trip) and falls back to the endpoint. + +## Frontend flow (`fieldsFromSchema` + `CnFormDialog`) + +1. `fieldsFromSchema` surfaces `referenceSemanticType` (+ `referenceSemanticApp`) + onto the field descriptor next to where `referenceType` is surfaced today. +2. `CnFormDialog` field rendering gains a branch, ordered: + - `resolveReferenceWidget(field)` (existing `referenceType` integration) → + - **NEW** `field.referenceSemanticType`: + - resolve URI → provider via the capability payload; + - **resolved** → render the standard `$ref`-style searchable object + dropdown bound to `{register, schema}` of the provider, storing the UUID; + - **unresolved** → render a **disabled** field (`:disabled="true"`) with a + tooltip: `t('The {type} app is not installed', {type: label(uri)})`, + label derived from the URI (last path segment) and `referenceSemanticApp` + hint; copy sourced from `resolveProviderAvailability` reason-codes. + - else existing auto-field. +3. Backward compatible: absent `referenceSemanticType` → unchanged. + +## Ambiguity & governance + +- schema.org-first (ADR-011); a small Conduction canonical vocabulary under + `https://openregister.app/ns#` only where schema.org has no fit. +- Multiple providers for one URI → any adherer is acceptable; the resolver just + picks deterministically (first by slug) and WARN-logs. No preferred-provider + map is introduced (product-owner decision); a future change may add one if + collisions ever become a real problem. + +## Non-goals + +- No write-back / ownership transfer: the referenced object stays owned by its + home app; this is a read/pick reference only. +- No automatic buy-cost sync from the vendor — `buyCost` is a plain field; a + later change may add a declarative pull (`@ref`) once the reference exists. diff --git a/openspec/changes/cross-app-semantic-references/hydra.json b/openspec/changes/cross-app-semantic-references/hydra.json new file mode 100644 index 0000000000..ca1751e91f --- /dev/null +++ b/openspec/changes/cross-app-semantic-references/hydra.json @@ -0,0 +1,10 @@ +{ + "spec_slug": "cross-app-semantic-references", + "title": "Cross-app object references by canonical semantic type", + "app": "openregister", + "repo": "https://codeberg.org/Conduction/openregister", + "depends_on": [ + "pluggable-integration-registry" + ], + "issue": null +} diff --git a/openspec/changes/cross-app-semantic-references/proposal.md b/openspec/changes/cross-app-semantic-references/proposal.md new file mode 100644 index 0000000000..8d9b99af15 --- /dev/null +++ b/openspec/changes/cross-app-semantic-references/proposal.md @@ -0,0 +1,83 @@ +--- +kind: code +--- + +# Proposal: Cross-app object references by canonical semantic type + +## Problem + +Conduction apps are independently installable but share the OpenRegister object +foundation. Real workflows cross app boundaries, yet OR references are keyed to +a **specific schema identity** (`$ref` = slug/id) or a **specific integration +id** (`referenceType`). Neither expresses *"a reference to any object of KIND X, +provided by whatever app supplies it, or null if none is installed."* + +Concrete driver: pipelinq just dropped its redundant `supplier` schema (a thin +subset of shillinq's `Payee` procurement master). We now want pipelinq's +`product` to carry a **buy cost + vendor** where the vendor is shillinq's +`Payee` — *if shillinq is installed* — without pipelinq hard-coupling to +shillinq's slug or existence, and with the field degrading gracefully (disabled ++ explained) when shillinq is absent so pipelinq still runs standalone. + +## Proposed Change + +Add a semantic-type reference layer on top of ADR-019's reference mechanism. +Reuse existing infrastructure wherever it exists — this is deliberately additive. + +1. **Schema advertises its semantic type** — reuse the existing, validated, + `@type`-serialized `configuration.jsonld.type` (ADR-011 schema.org-first). + Add an optional `configuration.implements: ["", …]` alias defaulting to + `[jsonld.type]` so one schema can advertise multiple capability URIs. +2. **Property references a semantic type** — new property keyword + `referenceSemanticType: ""` (semantic sibling of + `referenceType`), validated as a well-formed IRI (reuse + `JsonLdContextService::isAbsoluteIri`), value stored as the target UUID. +3. **Resolver** — a `SemanticTypeResolver` (sibling of the done + `RegisterResolverService`) with `resolveSchemaByImplements(uri): ?Schema` + over `SchemaMapper::findAll()`, request-cached, returning **null** (never an + exception) when no installed schema matches; deterministic tie-break when + several match (same-register → configured preferred → first by slug). +4. **Discovery** — expose resolved semantic providers to the frontend via the + existing OCS capabilities payload the availability engine already reads + (`availability.js`), plus a `@NoAdminRequired` lookup on `SchemasController`. +5. **Form degradation (nc-vue)** — a new `CnFormDialog` branch: when + `referenceSemanticType` is set but no provider resolves, render a **disabled** + field with a `title=`/helper-text tooltip sourced from the existing + `resolveProviderAvailability` reason-codes and "{App} not available" copy — + instead of the current silent fallback to a plain input. When a provider IS + resolved, render the standard `$ref`-style searchable object dropdown. + +### Scope + +**In scope**: the `implements` alias + validation, `referenceSemanticType` +property keyword + validator, the resolver + discovery surface, the nc-vue +form-degradation branch, and the first adopter (`product.vendor` + `buyCost` +referencing `schema.org/Organization`). + +**Out of scope**: changing existing `$ref`/`referenceType` behaviour; a +governance registry of canonical URIs beyond "schema.org-first + a small +Conduction vocabulary"; write-back / bidirectional sync of the referenced +object (it stays owned by its home app). + +## Impact + +- **OpenRegister**: additive schema `configuration` key, new property keyword + + validator, new resolver service, capability/endpoint surface. No change to + existing reference resolution paths. +- **nextcloud-vue**: new descriptor field in `fieldsFromSchema`, new degradation + branch in `CnFormDialog` (backward-compatible — untouched when the keyword is + absent). +- **Consuming apps**: opt-in per property; apps without the provider app keep + working (field disabled). +- **Security (ADR-005)**: read-only reference; resolver is org/RBAC-scoped via + `SchemaMapper::findAll()`; no new IDOR surface (value is a UUID the user picks + from RBAC-filtered results). + +## Dependencies + +- ADR-048 (hydra) — records the cross-app semantic-reference decision. +- ADR-019 (integration-registry), ADR-011 (schema-standards), ADR-022 + (apps-consume-OR-abstractions). +- Existing: `JsonLdContextService`, `SchemaMapper::findAll()`, + `PropertyReferenceTypeValidator`, `RegisterResolverService`, + `CnFormDialog` + `CnIntegrationWidget/availability.js`. diff --git a/openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md b/openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md new file mode 100644 index 0000000000..27ff8825a9 --- /dev/null +++ b/openspec/changes/cross-app-semantic-references/specs/semantic-schema-references/spec.md @@ -0,0 +1,156 @@ +# Spec delta: Semantic schema references — cross-app references by canonical type + +**Status:** proposed +**Scope:** openregister (+ nextcloud-vue render layer) +**Depends on:** integration-registry (existing — extended), schema JSON-LD config (existing — reused) + +## Motivation (context for the delta) + +OR references are keyed to a specific schema identity (`$ref`) or a specific +integration id (`referenceType`). Neither lets a property reference "an object of +KIND X provided by whatever app supplies it, or null if none is installed." This +delta adds a semantic-type reference resolved across all installed schemas, with +a null-safe resolver and an explicit disabled-field affordance when no provider +is present. It reuses the schema `configuration.jsonld.type` semantic marker +(ADR-011) and the ADR-019 availability engine — no existing reference behaviour +changes. See ADR-048. + +## ADDED Requirements + +### Requirement: Schemas advertise the canonical types they implement + +A schema SHALL be resolvable by the canonical semantic type(s) it implements. +The set of implemented type URIs MUST be `configuration.implements` when +present (authoritative — the defaults below are then NOT merged), otherwise the +union of `configuration.jsonld.type` and a schema-level `x-schema-org` marker. +The `x-schema-org` marker MAY be supplied as a **top-level** field (a sibling of +`properties`, matching the fleet's ADR-011 annotation convention) or inside +`configuration`; a top-level marker MUST be folded into +`configuration['x-schema-org']` on schema write so it survives save/import and +is visible on the live schema. A marker MAY be a single value or an array, and +each entry MAY be a compact schema.org CURIE (`schema:Organization`, expanded to +`https://schema.org/Organization`) or an absolute IRI. Only absolute-IRI entries +(after CURIE expansion) count; a non-IRI `implements` value is dropped. + +#### Scenario: Implements defaults from jsonld.type + +- **GIVEN** a schema with `configuration.jsonld.type = "https://schema.org/Organization"` and no `configuration.implements` +- **WHEN** its implemented types are computed +- **THEN** they are `["https://schema.org/Organization"]` + +#### Scenario: Top-level x-schema-org survives save and resolves + +- **GIVEN** a schema written with a top-level `"x-schema-org": "schema:Organization"` (a sibling of `properties`, not inside `configuration`) and no `configuration.implements` +- **WHEN** the schema is saved or imported and then re-read +- **THEN** the live schema carries `configuration['x-schema-org'] = "schema:Organization"` and its implemented types include `https://schema.org/Organization` + +#### Scenario: Multiple advertised capability URIs + +- **GIVEN** a schema with `configuration.implements = ["https://schema.org/Organization", "https://openregister.app/ns#Vendor"]` +- **WHEN** its implemented types are computed +- **THEN** both URIs are returned, while the object's `@type` stays the single `jsonld.type` + +#### Scenario: Non-IRI implements value dropped + +- **WHEN** a schema is written with `configuration.implements = ["Organization", "https://schema.org/Organization"]` +- **THEN** only `https://schema.org/Organization` is advertised (the non-IRI entry is dropped) + +### Requirement: A property can reference a semantic type + +The system SHALL let a schema property reference cross-app objects by canonical +semantic type. A property MAY declare `referenceSemanticType` (an absolute IRI) +and an optional `referenceSemanticApp` hint; the stored value is the referenced +object's UUID. `referenceSemanticType` MUST be validated as a well-formed IRI on +schema write, and is independent of `referenceType` (an integration id) and of +`$ref` (a concrete schema). + +#### Scenario: Valid semantic reference accepted + +- **WHEN** a `product` schema declares `vendor` with `referenceSemanticType = "https://schema.org/Organization"` and `format = uuid` +- **THEN** the schema is accepted and the property surfaces `referenceSemanticType` to the render layer + +#### Scenario: Malformed semantic reference rejected + +- **WHEN** a property declares `referenceSemanticType = "Organization"` (not an absolute IRI) +- **THEN** the schema write is rejected with a validation error + +### Requirement: Resolution is null-safe across installed schemas + +The system SHALL resolve a semantic-type URI to a schema that implements it, +enumerating all registers (org/RBAC-scoped), and SHALL return `null` — never +raise — when no available schema implements the URI. Any schema adhering to the +requested URI is an acceptable provider; the resolver MUST NOT require +sophisticated selection. When more than one schema adheres, the resolver SHALL +pick deterministically (first by slug, optionally biased to the consuming +register when that hint is supplied) and MUST emit a WARN log naming the pick so +ambiguous vocabulary stays observable. + +#### Scenario: No provider installed → null + +- **GIVEN** no installed schema implements `https://schema.org/Organization` +- **WHEN** `resolveSchemaByImplements("https://schema.org/Organization")` is called +- **THEN** it returns `null` and no exception is raised + +#### Scenario: Single provider resolves + +- **GIVEN** exactly one installed schema (shillinq `Payee`) implements `https://schema.org/Organization` +- **WHEN** the URI is resolved +- **THEN** the `Payee` schema (its register + slug) is returned + +#### Scenario: Multiple providers → deterministic pick + +- **GIVEN** two installed schemas implement the same URI +- **WHEN** the URI is resolved +- **THEN** the pick is deterministic — the first candidate by slug, or the provider in the consuming schema's own register when that hint is supplied +- **AND** a WARN log records the ambiguity and the chosen provider + +### Requirement: A disabled provider app degrades to no provider + +Resolution SHALL treat a schema as available only when its owning app is +installed AND enabled. The owning app id MUST be resolved from the schema's own +`application` field first (the reliable per-schema signal), then the owning +register's `application`. When that owning app names a concrete app that is not +enabled, the resolver MUST skip that schema as if it were not installed, and the +`resolve-by-implements` endpoint MUST return `{ "resolved": false }` (HTTP 200, +never 500). Schemas with no named owning app (core `openregister`, or an +undeclared owner) MUST NOT be filtered out by this check, and the check MUST +remain null-safe when the app manager is unavailable. + +#### Scenario: Provider app enabled → resolves + +- **GIVEN** shillinq is enabled and its `Payee` schema implements `https://schema.org/Organization` +- **WHEN** the URI is resolved +- **THEN** the `Payee` schema is returned + +#### Scenario: Provider app disabled → resolved:false + +- **GIVEN** shillinq's `Payee` schema still exists in OR but the shillinq app is disabled +- **WHEN** `GET /api/schemas/resolve-by-implements?uri=https://schema.org/Organization` is called +- **THEN** the response is `{ "resolved": false }` with HTTP 200 and no exception is raised + +### Requirement: Absent provider degrades to a disabled, explained form field + +The generated form SHALL render a `referenceSemanticType` field as **disabled**, +with a mouse-over tooltip explaining the supporting app is not installed, when no +installed schema provides that semantic type — reusing the integration-availability +reason copy. When a provider IS resolved, the field SHALL render as a searchable +object picker over the provider schema, storing the referenced UUID. A property +without `referenceSemanticType` MUST be unaffected. + +#### Scenario: Provider present → object picker + +- **GIVEN** shillinq is installed and provides `https://schema.org/Organization` +- **WHEN** the `product` form renders the `vendor` field +- **THEN** it shows a searchable dropdown of `Payee` objects and stores the chosen UUID + +#### Scenario: Provider absent → disabled + tooltip + +- **GIVEN** shillinq is not installed +- **WHEN** the `product` form renders the `vendor` field +- **THEN** the field is disabled and a mouse-over tooltip states the supporting app (shillinq) is not installed +- **AND** the rest of the `product` form remains fully editable and saveable + +#### Scenario: Backward compatibility + +- **WHEN** a form renders a property that declares neither `referenceType` nor `referenceSemanticType` +- **THEN** it renders exactly as before this change (the normal auto-generated field) diff --git a/openspec/changes/cross-app-semantic-references/tasks.md b/openspec/changes/cross-app-semantic-references/tasks.md new file mode 100644 index 0000000000..6aae5e41d2 --- /dev/null +++ b/openspec/changes/cross-app-semantic-references/tasks.md @@ -0,0 +1,58 @@ +# Tasks: Cross-app object references by canonical semantic type + +## 1. Schema-side semantic type (OpenRegister) + +- [ ] 1.1 Support `configuration.implements: string[]` on `Schema` — default to + `[jsonld.type]` when absent; validate each entry is an absolute IRI via + `JsonLdContextService::isAbsoluteIri()`. Reject non-IRI values on write. +- [ ] 1.2 Unit-test the default (`implements` derives from `jsonld.type`), + multi-value advertisement, and IRI validation rejection. + +## 2. Property keyword `referenceSemanticType` + +- [ ] 2.1 Add a `PropertySemanticReferenceValidator` (mirror + `PropertyReferenceTypeValidator`): a property may declare + `referenceSemanticType` (absolute IRI) and optional `referenceSemanticApp` + (string hint). Validate on schema write. +- [ ] 2.2 Unit-test valid/invalid `referenceSemanticType`. + +## 3. Resolver (OpenRegister) + +- [ ] 3.1 Add `SemanticTypeResolver::resolveSchemaByImplements(string $uri, ?int $consumingRegisterId = null): ?Schema` + over `SchemaMapper::findAll()`; request-scoped cache; **return null** (never + throw) when no installed schema matches. +- [ ] 3.2 Deterministic tie-break: same register → `referenceSemanticApp` hint → + first by slug; `WARN` log naming the pick when >1 candidate. +- [ ] 3.3 Unit-test: 0 providers → null; 1 provider → it; N providers → tie-break + order; org/RBAC scoping honoured. + +## 4. Discovery surface (OpenRegister) + +- [ ] 4.1 Extend the integrations OCS capability payload with `semanticProviders: + [{ uri, register, schema, appId, available, reason }]` computed from + resolvable schemas + provider availability. +- [ ] 4.2 Add `@NoAdminRequired` `SchemasController::resolveByImplements(uri)` + returning the resolved `{register, schema}` or `null`. +- [ ] 4.3 API test both surfaces (present provider, absent provider → null, + RBAC-filtered). + +## 5. Form degradation (nextcloud-vue) + +- [ ] 5.1 Surface `referenceSemanticType` + `referenceSemanticApp` onto the field + descriptor in `fieldsFromSchema` (next to `referenceType`). +- [ ] 5.2 Add the `CnFormDialog` branch: resolved → `$ref`-style searchable + object dropdown bound to the provider `{register, schema}`, stores UUID; + unresolved → **disabled** field + tooltip from the availability reason-codes. +- [ ] 5.3 Component tests: resolved renders a working picker; unresolved renders + disabled + tooltip; absent keyword renders the normal field (backward compat). + +## 6. First adopter + docs + +- [ ] 6.1 shillinq `Payee`: set `configuration.jsonld.type = + https://schema.org/Organization` (confirm/adjust). +- [ ] 6.2 pipelinq `product`: add `buyCost` (number) and `vendor` + (`referenceSemanticType: https://schema.org/Organization`, + `referenceSemanticApp: shillinq`). Verify: with shillinq → vendor picker + lists Payees; without shillinq → vendor disabled + tooltip, product still + editable. +- [ ] 6.3 Document the pattern in OR schema-config docs; cross-link ADR-048. diff --git a/openspec/changes/dashboard-live-updates/proposal.md b/openspec/changes/dashboard-live-updates/proposal.md new file mode 100644 index 0000000000..08031dd0e1 --- /dev/null +++ b/openspec/changes/dashboard-live-updates/proposal.md @@ -0,0 +1,88 @@ +--- +kind: code +--- + +## Why + +`adopt-live-updates-ui` (openregister#402) wired OpenRegister's object list +and object detail views to the notify_push live-update events the backend +already emits, but explicitly deferred the dashboard: its widgets render from +a separate plain-Pinia dashboard store (custom aggregate endpoints under +`/api/dashboard/*`) and keep one-shot fetch behaviour. A user watching the +dashboard while another session creates or deletes objects sees stale counts, +charts, and audit statistics until a manual refresh. Issue openregister#410 +tracks closing that gap. + +## What Changes + +- Dashboard page (`src/views/dashboard/DashboardIndex.vue`) holds ONE live + collection subscription (`or-collection-{register-slug}-{schema-slug}`) + covering all its object-CRUD-derived widgets, active only while the + dashboard is scoped to a register+schema via the sidebar filter + (`registerStore.registerItem` + `schemaStore.schemaItem`). The subscription + reuses the package object store's `liveUpdatesPlugin` + (`objectStore.subscribe(type)` — the same API #402 uses), registering the + type first when the sidebar's own object search has not done so yet. +- Re-scope on filter change (watcher on a derived `liveCollectionType`), + subscribe on mount, release in `beforeDestroy`. Same async-race guards as + #402, copied verbatim: pending-type marker dedupes an in-flight same-scope + subscribe; an epoch counter invalidates resolutions that land after a + release (the stale handle is unsubscribed, never leaked). +- Events are refetch HINTS only. The plugin stamps + `objectStore.liveLastEventAt` on every event; the dashboard watches it and + — after a 750ms trailing debounce that coalesces bulk-import event bursts — + refetches the object-CRUD-derived widget data: `fetchRegisters()` (Objects + KPI + sidebar totals), `fetchAllChartData()` (objects-by-register/schema/ + size charts + audit-trail action chart behind the Events KPI), and + `fetchAllStatistics()` (audit-trail stats). The debounce is needed because + the dashboard store's `chartLoading`/`statisticsLoading` guards DROP + concurrent fetches rather than queueing a trailing one, so an undebounced + burst would miss the final state. No data is ever patched from an event + payload. +- Side effect of reusing the plugin: on each event the plugin also re-runs + `fetchCollection` for the subscribed type, keeping the object-collection + cache (used by the dashboard sidebar's object search) warm. This is the + documented plugin contract, not extra wiring. +- No backend changes; no new endpoints; no dependency bump + (`@conduction/nextcloud-vue` 1.0.0-beta.206 already ships the layer). + +## Widget coverage (honest partial) + +Wired (refetched on collection events while scoped): + +- `count-objects` KPI — object totals from `/api/dashboard`. +- `count-events` KPI — audit-trail totals/chart; every create/delete writes + audit entries, so a collection event is a valid hint. +- `objects-by-register`, `objects-by-schema`, `objects-chart` — object-count + charts. +- Dashboard sidebar totals/orphaned tables — render from the same + `dashboardStore.registers` state as the Objects KPI. + +Left unwired, and why: + +- **Unscoped dashboard (no register+schema selected — the default view).** + The push event dialect only defines `or-object-{uuid}` and + `or-collection-{registerSlug}-{schemaSlug}`; there is no instance-wide or + register-level event key, and subscribing to every register×schema pair + would need N×M subscriptions plus slug enumeration. A backend + wildcard/register-level event is the correct fix and is out of scope here. + A register-only scope is unwireable for the same reason (the key needs + both slugs). +- `count-registers`, `count-schemas` KPIs — driven by register/schema CRUD, + which emits no push events at all. +- `count-searches`, `popular-terms` — search-trail activity; not object CRUD, + not signalled by or-collection events. + +## Capabilities + +### Modified Capabilities + +- `realtime-updates`: the dashboard becomes a consumer of collection events + for its sidebar-scoped register+schema — widgets refetch on events instead + of rendering one-shot fetches only. + +## Impact + +- `src/views/dashboard/DashboardIndex.vue` — subscription lifecycle, debounced + refetch of dashboard aggregates. +- No store, API, schema, or PHP changes. diff --git a/openspec/changes/dashboard-live-updates/specs/realtime-updates/spec.md b/openspec/changes/dashboard-live-updates/specs/realtime-updates/spec.md new file mode 100644 index 0000000000..c24579185e --- /dev/null +++ b/openspec/changes/dashboard-live-updates/specs/realtime-updates/spec.md @@ -0,0 +1,42 @@ +## ADDED Requirements + +### Requirement: The dashboard MUST consume live update events for its sidebar-scoped register+schema + +The dashboard page MUST, while scoped to a register+schema via the sidebar +filter, hold a live collection subscription +(`or-collection-{register-slug}-{schema-slug}`) through the shared +`@conduction/nextcloud-vue` live-updates layer, and MUST treat received +events as refetch hints for its object-CRUD-derived widgets — refetching +through the existing dashboard store actions and NEVER applying event +payloads as data. + +- The subscription MUST re-scope when the sidebar filter changes and MUST be + released on unmount; an in-flight subscribe MUST be deduplicated per scope + and a resolution landing after a release MUST be unsubscribed instead of + leaked (same guard set as the object list view). +- Refetches MUST be coalesced with a trailing debounce so a bulk-import event + burst results in a single refetch after the burst settles. +- Widgets whose data is NOT signalled by the push event dialect + (register/schema CRUD counts, search-trail activity) and the unscoped + ("all registers") dashboard MUST keep their existing manual/one-shot + refresh behaviour — no wildcard or register-level event key exists. +- A failed subscription attempt MUST NOT break the dashboard — it degrades to + the pre-existing manual-refresh behaviour with a console warning. + +#### Scenario: Scoped dashboard refreshes when another session changes the collection +- **WHEN** the dashboard is scoped to register `zaken` / schema `meldingen` and another session creates or deletes a `meldingen` object +- **THEN** the client receives the `or-collection-zaken-meldingen` event and, after the trailing debounce, refetches the register totals, the objects-by-* chart data, and the audit-trail statistics, so the Objects KPI, Events KPI, charts, and sidebar totals reflect the change without a manual refresh + +#### Scenario: Bulk import burst coalesces into one refetch +- **WHEN** a bulk import flushes a burst of collection events for the subscribed scope +- **THEN** the dashboard performs a single refetch of its widget data after the burst settles instead of one refetch per event + +#### Scenario: Subscription follows the sidebar scope +- **WHEN** the user switches the sidebar filter to a different register/schema, clears it, or leaves the dashboard +- **THEN** the previous subscription is released (and any pending debounced refetch cancelled) before a new one is established for the new scope, and no subscription is held while the dashboard is unscoped + +#### Scenario: Unscoped dashboard keeps one-shot behaviour +- **WHEN** the dashboard shows the default "all registers" view (no register+schema selected) +- **THEN** no live subscription is held and the widgets refresh only via mount fetches or the manual refresh action + +@e2e exclude Live-updates transport requires a notify_push-enabled instance and a second mutating session; covered by nc-vue unit tests on the shared plugin and manual verification. diff --git a/openspec/changes/dashboard-live-updates/tasks.md b/openspec/changes/dashboard-live-updates/tasks.md new file mode 100644 index 0000000000..d553c6de52 --- /dev/null +++ b/openspec/changes/dashboard-live-updates/tasks.md @@ -0,0 +1,53 @@ +# Tasks — dashboard-live-updates + +## 1. Architecture mapping + +- [x] 1.1 Inventory the dashboard surface: `DashboardIndex.vue` (only + `CnDashboardPage` in the app, 9 custom widgets) + `DashboardSideBar.vue` + (register/schema scope filter + totals tables), all rendering from the + plain-Pinia `dashboard` store (`/api/dashboard/*` aggregates) and the + search-trail store. Classify each widget as object-CRUD-derived (wireable + when scoped) vs register/schema-CRUD or search-trail (no event key exists) + — documented in proposal.md. + +## 2. Subscription lifecycle + +- [x] 2.1 `src/views/dashboard/DashboardIndex.vue`: add + `syncLiveSubscription()` / `releaseLiveSubscription()` managing a single + collection subscription via `objectStore.subscribe(type)` for the derived + `liveCollectionType` (empty unless BOTH register and schema are selected); + register the type (`registerObjectType`) when the sidebar's object search + has not done so yet. +- [x] 2.2 Re-scope on sidebar filter change via a watcher on + `liveCollectionType`; subscribe on mount; release in `beforeDestroy`. +- [x] 2.3 Copy the #402 race guards verbatim: pending-type marker dedupes an + in-flight same-scope subscribe; epoch counter invalidates resolutions + landing after a release (stale handle immediately unsubscribed, never + leaked). + +## 3. Event-driven refetch + +- [x] 3.1 Watch `objectStore.liveLastEventAt` (plugin-stamped on every event); + while a subscription is held, schedule a refetch of the object-CRUD-derived + widget data (`fetchRegisters` + `fetchAllChartData` + `fetchAllStatistics`) + — events are refetch hints, payloads are never applied as data. +- [x] 3.2 Debounce: 750ms trailing timer coalesces bulk-import event bursts + into one refetch (the nc-vue plugin dedupes in-flight fetches but the + dashboard store's loading guards DROP concurrent fetches, so a trailing + debounce is required to not miss the post-burst state). Timer is cleared + and epoch-guarded on release. + +## 4. Out of scope / deferred + +- [x] 4.1 Unscoped ("all registers") dashboard, register-only scope, + `count-registers`/`count-schemas`, and search-trail widgets deliberately + NOT wired — no matching push event key exists in the dialect (see + proposal.md, Widget coverage). + +## 5. Verification + +- [x] 5.1 `npm run lint` passes on the touched files. +- [x] 5.2 `npm test` (jest) passes. +- [x] 5.3 `npm run build` (webpack production) succeeds. +- [ ] 5.4 Live two-session verification on a notify_push-enabled instance + (deferred — requires a deployed instance; not part of this change's CI). diff --git a/openspec/changes/dbal-virtual-registers/.openspec.yaml b/openspec/changes/dbal-virtual-registers/.openspec.yaml new file mode 100644 index 0000000000..074342d550 --- /dev/null +++ b/openspec/changes/dbal-virtual-registers/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-09 diff --git a/openspec/changes/dbal-virtual-registers/design.md b/openspec/changes/dbal-virtual-registers/design.md new file mode 100644 index 0000000000..6440bec7ea --- /dev/null +++ b/openspec/changes/dbal-virtual-registers/design.md @@ -0,0 +1,127 @@ +## Context + +OpenRegister already has a read-only object-source seam: a schema whose `configuration['x-openregister-object-source']` block names a provider is served live by that provider instead of from the magic tables. The seam is fully wired: + +- **Interface** `lib/Service/ObjectSource/ObjectSourceProvider.php` — `getId(): string`, `isEnabled(): bool`, `find(Register $r, Schema $s, string $id, array $config): ?ObjectEntity`, `findAll(Register $r, Schema $s, array $query, array $config): array`, `count(Register $r, Schema $s, array $query, array $config): int`. +- **Registry** `ObjectSourceRegistry` — `addProvider($provider)`, `get($id)`; shared instance. +- **Binding** `Schema::getObjectSource()` (~line 1592) returns `['provider' => …, 'config' => …]` or null. +- **Dispatch** `ObjectService::paginateObjectSource()` (~line 2489) calls `provider->findAll(...)` and — importantly — currently computes `total = count($results)` and returns `page: 1, pages: 1`. There is a parallel single-object dispatch on the get path. The standard `ObjectsController` API serves these schemas transparently; writes are rejected upstream. +- **Boot** `Application::bootObjectSourceProviders()` (~line 2882) iterates a hard-coded `$providerClasses` list and `addProvider($server->get($class))`, guarded so a failing provider never takes down the app. Eight providers exist today (Contacts, CalendarEvent, CalDavVtodo, Deck, Files, Group, Talk, UserDirectory). + +Doctrine DBAL `^3.8` is already a composer dependency and is used in `MagicMapper` (imports `Doctrine\DBAL\Platforms\*`). OCP exposes no API for *external* connections, so we use `Doctrine\DBAL\DriverManager::getConnection()` directly. The `Source` entity (`lib/Db/Source.php`) already models an external DB connection (`databaseUrl`, `type`, `authType`, `authConfig`, `targetRegister`…) but its DB fetch path was never implemented — only `RestApiSourceFetcher` exists. The credential custody seam (`lib/Service/Credential/CredentialStore` → `get/put/delete(uuid, scope)`, resolved via `CredentialStoreResolver::resolve()` — `DoriathCredentialStore` when the Doriath custody leaf is eligible, `NextcloudVaultCredentialStore` as fallback) is the ADR-004 custody home for secrets. + +This change adds external SQL databases as a **new object-source provider** plus the introspection that turns SQL structure into JSON Schema. Every seam above is reused; the only core modification is a minimal, backward-compatible fix to `paginateObjectSource()` page/total metadata (D4b). + +## Goals / Non-Goals + +**Goals:** +- Connect to an external database over DBAL (`pdo_mysql`, `pdo_pgsql`, `pdo_sqlite`) and expose it as a **read-only, live** virtual register: database = register, table/view = schema, row = object. +- Introspect columns → JSON-Schema properties, `NOT NULL` → `required`, foreign keys → canonical `$ref` relations, so validation and `_extend` work identically to native schemas. +- Keep DB credentials behind the ADR-004 custody seam (`CredentialStoreResolver` → Doriath leaf, NC vault fallback); never store a DB password in plaintext in a `Source` row or an OR object. +- Prevent SQL injection (parameterised `QueryBuilder` + platform identifier quoting) and IDOR (RBAC parity with native schemas; provider re-applies the schema's read authorization). +- Turn connection/introspection failures into 502/503 semantics, never a bare 500. + +**Non-Goals:** +- No writes (create/update/delete) to virtual schemas in v1 — read-only. +- No copy/sync/materialisation — objects are served live only. (The existing harvest `Source` path is untouched.) +- No cross-database joins beyond single-column foreign-key `$ref` resolution via the existing `_extend` machinery. +- No connection pooling across requests (per-request cache only). +- Drivers beyond the three named (e.g. `pdo_sqlsrv`, `oci8`) are out of scope for v1. + +## Declarative-vs-imperative decision (ADR-031) + +ADR-031 prefers declarative configuration over imperative code. Here the split is: + +- **Declarative (data):** the binding itself — a schema's `x-openregister-object-source` annotation `{provider: 'dbal-source', config: {sourceId, table, idColumn}}` — is pure schema data, produced by introspection and stored like any other schema config. Dispatch is data-driven through the existing registry. No imperative branch is added to `ObjectService`. +- **Imperative (services), justified under the ADR-031 external-integration exception:** talking to a *foreign* database — building a connection, running DBAL `SchemaManager` introspection, translating an OR query into parameterised SQL, mapping rows to `ObjectEntity` — is inherently imperative I/O against a system OpenRegister does not own. This mirrors the existing eight object-source providers and the `IntegrationProvider` query-time strategy (ADR-019), all of which are imperative for exactly this reason. A declarative mapping DSL for arbitrary SQL dialects would be more complex and less correct than DBAL's own platform abstraction. The imperative surface is confined to `lib/Service/Dbal/*` and one new provider; nothing else in the object read path changes. + +## Mixed-spec rationale + +This change is `kind: code`. It ships no `lib/Settings/*register*.json` seed as substance — the virtual register/schema are *produced at runtime by introspection*, not seeded from a config register. The only "config-shaped" artifact is the demo SQLite fixture + example introspected JSON under the change dir, used by tests and docs (see Seed Data). That is thin test glue, not a schema-register config chain, so the change stays single-kind `code`. + +## Seed Data (ADR-001) + +Seed data for this change is a **demo external database fixture**, not an OR register import: + +- **`tests/fixtures/dbal/permits.sqlite`** (built by a small `tests/fixtures/dbal/build-permits-sqlite.php` so no binary is committed): a municipality permit-tracking DB with three related tables — + - `permit_types(id PK, code, name, max_duration_days)` + - `applicants(id PK, full_name, email, kvk_number NULL)` + - `permits(id PK, applicant_id FK→applicants.id NOT NULL, permit_type_id FK→permit_types.id NOT NULL, reference, status, submitted_at, decided_at NULL)` + - plus a view `active_permits` selecting `permits WHERE status = 'active'` to exercise view support. +- **`tests/fixtures/dbal/expected-introspection.json`**: the register + three-schema (+ view schema) JSON the introspection service must produce from that fixture, used as the golden output for `DatabaseIntrospectionService` tests (idColumn `id`, FK `$ref`s, `required` excluding PK/nullable/defaulted columns). + +These fixtures are the demo path too: a reviewer points a `type: database` Source at `permits.sqlite`, introspects, and browses the three virtual schemas through the normal UI. Placeholder DSNs in docs use `postgres://user:PASS@host:5432/db` and the nil UUID `00000000-0000-0000-0000-000000000000`. + +## Decisions + +### D1 — Reuse the `Source` entity (type `database`), no new entity +A `Source` with `type = 'database'` holds non-secret connection metadata. **Rationale:** it already has the fields and lifecycle (controller, repository, RBAC). Alternative (new `DatabaseConnection` entity) duplicates all of that. **Change to current behaviour:** today `SourcesController` encrypts `databaseUrl` at rest with `ICrypto`. For `type = database` we instead store **non-secret** connection parts (driver, host, port, dbname, user) in the Source and custody the **password through the `CredentialStore` abstraction**, obtained via `CredentialStoreResolver::resolve()` — the **Doriath custody leaf** (`DoriathCredentialStore`) is the intended store for DB credentials, with `NextcloudVaultCredentialStore` as the fallback when Doriath is not eligible on the instance — keyed by a `credential` UUID referenced from the Source `authConfig`. This is full ADR-004 custody (secrets live in the custody leaf, not encrypted-in-a-row) and means a leaked DB row exposes no password. The `ICrypto` path stays for legacy harvest sources. The connection factory depends only on the `CredentialStore` interface; store selection stays entirely inside the resolver. + +### D2 — `DbalConnectionFactory` with a per-request connection cache +`Source → resolve secret from CredentialStore → DriverManager::getConnection([...])`. Connections are cached per `sourceId` for the lifetime of the request only (no cross-request pool). **Rationale:** introspection and a paginated read may open the same source several times in one request; a persistent pool adds lifecycle/security complexity we don't need for v1. On decrypt/secret-missing errors the factory **fails closed** (throws a typed `DbalConnectionException`) — it never falls back to an unauthenticated connection. + +### D3 — `DatabaseIntrospectionService` via DBAL `SchemaManager` +`Connection::createSchemaManager()` → `listTableNames()` + views, `introspectTable()/listTableColumns()/listTableForeignKeys()`. Produces a `Register` (`source = sourceId`) and one `Schema` per table/view, each tagged `x-openregister-object-source = {provider: 'dbal-source', config: {sourceId, table, idColumn}}`. Re-introspection **updates** existing schemas (matched by table name) rather than duplicating, applying changes as diffs through the existing `SchemaDiffService`/`SchemaVersioningService` path so drift is versioned and logged. Optionally emits an importable `Configuration` bundle (register + schemas) through the existing `ConfigurationService`/`ImportHandler`. **Rationale:** DBAL's `SchemaManager` already normalises dialect differences; reimplementing per-driver introspection would be error-prone. + +### D3b — Scheduled re-introspection (drift detection) via BackgroundJob +A `DbalIntrospectionJob` (NC `TimedInterval`/`TimedJob`) re-introspects every `type: database` Source on a configurable interval, applies schema diffs via the `SchemaDiffService` path, and logs detected drift (added/dropped/retyped columns, new/removed tables). The job's `run()` performs the real work — no stub body (hydra stub-scan gate). A manual `introspect` action on `SourcesController` remains for on-demand refresh. **Rationale (user decision):** scheduled drift detection keeps virtual schemas honest against a database OpenRegister does not own; manual-only was rejected. Failures for one source are caught and logged so the job continues with the next source. + +### D4 — `DbalObjectSourceProvider` translates OR query → parameterised `QueryBuilder` +Implements `ObjectSourceProvider` with `getId() = 'dbal-source'`. `findAll` builds `$conn->createQueryBuilder()->select(quoted cols)->from(quoted table)`, maps OR filters/search/sort to `where()/andWhere()/orderBy()` with **named/positional bound parameters only**, and applies `setMaxResults()/setFirstResult()` from the OR `limit/offset`. The provider honours `limit`/`offset` in SQL and exposes a real `count()` that issues `SELECT COUNT(*)` with the same WHERE; `paginateObjectSource()` consults it for the true total (see D4b). `find()` issues `WHERE = :id`. Rows → `ObjectEntity` (register/schema/uuid set from `idColumn`, `object` = column→property map). **Guardrails:** identifier quoting via `$platform->quoteIdentifier()`/QueryBuilder; a per-source **table allowlist** (only introspected tables may be queried); a hard result cap; read-only (no write methods exist); RBAC parity — the provider applies the same read authorization the native path would (denied → omitted / null, no enumeration oracle). + +### D4b — Fix `paginateObjectSource()` page/total metadata for all providers +**User decision:** rather than documenting the `total = count($results)` / `pages: 1` behaviour as a limitation, extend `ObjectService::paginateObjectSource()` minimally and backward-compatibly: pass the query's `limit`/`offset` through to `findAll()` (already part of the query array), call the provider's existing `count()` (the interface already defines it) for the true total, and compute `page`/`pages`/`next`/`prev` from `total`+`limit`+`offset`. **Backward compatibility:** the existing 8 providers already implement `count()`; when a provider's `count()` throws or returns a value smaller than the returned window (signalling no real count support), the dispatch falls back to the current in-memory behaviour (`total = count($results)`, single page). No provider interface change; this fixes page metadata for ALL object-source providers, not just `dbal-source`. + +### D5 — SQL type → JSON-Schema mapping (`SqlTypeMapper`) +Mapping keyed on DBAL `Types::*` (dialect-normalised), so one table covers MySQL/PostgreSQL/SQLite: + +| DBAL type (`Doctrine\DBAL\Types\Types`) | JSON-Schema `type` | `format` | Extra | +|---|---|---|---| +| `STRING`, `ASCII_STRING` | `string` | — | `maxLength` from column length | +| `TEXT` | `string` | — | no maxLength | +| `GUID` | `string` | `uuid` | | +| `INTEGER`, `SMALLINT`, `BIGINT` | `integer` | — | BIGINT documented as JS-precision-lossy | +| `DECIMAL`, `FLOAT` | `number` | — | `DECIMAL` carries precision/scale in description | +| `BOOLEAN` | `boolean` | — | | +| `DATE_MUTABLE`/`DATE_IMMUTABLE` | `string` | `date` | | +| `TIME_MUTABLE`/`TIME_IMMUTABLE` | `string` | `time` | | +| `DATETIME_MUTABLE`/`DATETIMETZ_*`/`DATETIME_IMMUTABLE` | `string` | `date-time` | | +| `JSON` | `object` | — | free-form (no sub-schema) | +| `BINARY`, `BLOB` | `string` | `binary` | flagged unsupported for filtering | +| `SIMPLE_ARRAY`, `ARRAY` | `array` | — | items untyped | +| unknown/vendor-specific | `string` | — | fallback + logged warning (see D7) | + +`NOT NULL` columns become `required`, **excluding** the primary key and any column with a database default (those are server-populated, so absence in a read projection is valid). Length → `maxLength`; DECIMAL precision/scale surfaced in `description`. + +### D6 — Foreign keys → canonical relation dialect +A single-column FK from `permits.applicant_id` → `applicants.id` produces on the `permits` schema a property whose value stays the raw key but is typed as a relation per the hydra gate-31 / OR canonical dialect: `type: string`, `format` set, `$ref` pointing at the related virtual schema, and `objectConfiguration.handling: 'related-object'`. The inverse side is added on the target schema with `inversedBy`. **Rationale:** this is exactly the shape `RenderObject`/`RelationHandler` resolve via `_extend`, so relation expansion and validation work with zero changes. Multi-column FKs are **not** mapped to `$ref` in v1 (documented under D7). + +### D7 — Composite PK and unsupported types +- **Composite / no primary key:** a table with a composite PK gets a **synthesised deterministic id** = the ordered PK column values joined with a reserved separator (e.g. `col1::col2`), stored as the object `id`/`uuid`; the introspected schema records `idColumns: [...]` in its object-source config and the provider reconstructs the WHERE from the parts. A table with **no** PK at all is introspected as **read-list-only**: `findAll`/`count` work, but `find(id)` returns null and the schema is flagged (`x-openregister-object-source.config.idColumn = null`) — no stable object identity. Both cases are logged at introspection time. (Alternative — refuse composite-PK tables entirely — rejected as too restrictive for real municipal databases.) +- **Unsupported column types** (vendor geometry, enums, arrays of composite types): mapped to the `string` fallback with a logged warning and excluded from the filterable set; they are surfaced read-only as their string cast, never used in a WHERE. + +### D8 — Error semantics +Connection refused / auth failure / driver missing → the provider catches the DBAL exception, logs it, and surfaces a **503 Service Unavailable** (source temporarily unreachable) or **502 Bad Gateway** (upstream DB returned an error) through the controller — never a bare 500. `isEnabled()` returns false when the DBAL driver extension is absent, degrading a bound schema to an empty list (matching existing provider behaviour). Introspection failures return a structured error on the `introspect` action, not a fatal. + +## Risks / Trade-offs + +- **[Pagination dispatch change touches all providers]** `paginateObjectSource()` is extended per D4b (decision, not a limitation): provider `count()` supplies the true total and limit/offset are pushed down. → Fallback to the current in-memory behaviour when a provider signals no count support keeps the existing 8 providers working unchanged; covered by a regression test over a native provider. +- **[Schema drift between scheduled runs]** the external DB can change shape between job runs. → `DbalIntrospectionJob` (D3b) re-introspects on an interval and applies diffs via `SchemaDiffService`; within a window, a dropped column yields a null property and a logged warning rather than a crash. +- **[Credential custody deviation from existing Source]** legacy harvest sources encrypt `databaseUrl` with `ICrypto`; virtual DB sources custody the password via `CredentialStoreResolver` (Doriath leaf, NC vault fallback). → Two code paths keyed on `type`; documented in D1, no migration of legacy rows. +- **[SQL injection]** any identifier/value reaching SQL. → No string interpolation anywhere; values are bound parameters, identifiers pass through `quoteIdentifier()` and are checked against the introspected allowlist. +- **[Untrusted external DB / host]** an admin could point a source at an internal host (SSRF-adjacent). → Only admins can create sources; document an optional host allowlist config; the connection is read-only and scoped to the named database. Fail closed on any credential-resolution error. +- **[BIGINT precision]** JS/JSON number precision. → Documented; BIGINT ids are carried as strings. + +## Migration Plan + +Additive, no data migration. Deploy = ship new services + provider + background job + routes; register the provider by adding `DbalObjectSourceProvider::class` to the `$providerClasses` list in `Application::bootObjectSourceProviders()`, its DI in `Application::register()`, and register `DbalIntrospectionJob`. The `paginateObjectSource()` change is backward-compatible (fallback path preserves current behaviour), so it needs no migration. Rollback = remove the provider and job from registration (bound schemas degrade to empty lists via the existing guard) and disable the `test-connection`/`introspect` routes; no schema/table changes to undo. Feature is inert until an admin creates a `type: database` Source and introspects it. + +## Open Questions + +None — all questions were decided with the user: + +- Host allowlist: **advisory** config, documented; fail-closed on credentials regardless. +- Re-introspection: **scheduled background job** (`DbalIntrospectionJob`, D3b) plus the manual `introspect` action. +- Configuration bundle: introspection writes register/schemas **directly**; bundle export is an opt-in action. +- Credential custody: **Doriath leaf via `CredentialStoreResolver`**, NC vault fallback (D1). +- Pagination: **fix `paginateObjectSource()`** for all providers (D4b), backward-compatible fallback. diff --git a/openspec/changes/dbal-virtual-registers/proposal.md b/openspec/changes/dbal-virtual-registers/proposal.md new file mode 100644 index 0000000000..e17e0d4f2d --- /dev/null +++ b/openspec/changes/dbal-virtual-registers/proposal.md @@ -0,0 +1,37 @@ +--- +kind: code +--- + +## Why + +Organisations that adopt OpenRegister already run authoritative data in relational databases (permit systems, HR, finance, line-of-business apps). Today the only way to surface that data as OpenRegister objects is to copy it in via a sync/harvest Source, which duplicates data, goes stale, and requires write access to a target register. There is no way to point OpenRegister at an existing database and treat it — read-only and live — as a set of registers and schemas. The read-only object-source seam (`ObjectSourceProvider`) that already backs Nextcloud entities (Contacts, Deck, Talk, …) is exactly the mechanism needed to close this gap for arbitrary external SQL databases. + +## What Changes + +- Add the ability to connect OpenRegister to an **external relational database** over Doctrine DBAL and expose it as a **virtual register**: the database is a register, each table/view is a schema, and rows are served as objects **live** — no copy, no sync. +- **Introspect SQL structure into JSON Schema** so virtual schemas obey the same validation and relation rules as native schemas: columns → properties (with type/format/length/precision), `NOT NULL` (non-PK, non-defaulted) → `required`, foreign keys → `$ref` relations in the canonical relation dialect with `objectConfiguration.handling: related-object` and an `inversedBy` reverse side. +- Add a `DbalConnectionFactory` service that builds cached `Doctrine\DBAL\Connection`s from a `Source` (type `database`), resolving the DB password through the `CredentialStore` abstraction via `CredentialStoreResolver` (Doriath custody leaf when eligible, Nextcloud vault fallback — ADR-004) — never from plaintext in a Source row or OR object. +- Add a `DatabaseIntrospectionService` that turns a live connection into an importable Register + one Schema per table/view, each tagged with the `x-openregister-object-source` annotation `{provider: 'dbal-source', config: {sourceId, table, idColumn}}`. Re-introspection runs both on demand and via a scheduled `DbalIntrospectionJob` background job that detects schema drift and applies diffs through the existing `SchemaDiffService` path. +- Fix `ObjectService::paginateObjectSource()` page/total metadata for **all** object-source providers: push limit/offset down to `findAll()`, consult the provider's existing `count()` for the true total, and compute page/pages/next/prev — with a backward-compatible in-memory fallback for providers without real count support. +- Add a `DbalObjectSourceProvider implements ObjectSourceProvider` that translates an OpenRegister query (filters, search, sort, limit/offset) into a **parameterised** DBAL `QueryBuilder`, maps rows to `ObjectEntity` in memory, and enforces read-only, identifier quoting, per-source table allowlist, and result limits. Registered in `Application.php` alongside the existing providers. +- Add a `test-connection` action and a `introspect` action to `SourcesController` (new routes) plus a minimal UI flow to add a database source, test it, pick tables, and create the virtual register. +- **Read-only v1.** Writes to virtual schemas stay rejected by the existing object-source path. Drivers: `pdo_mysql`, `pdo_pgsql`, `pdo_sqlite`. Views are supported like tables. + +## Capabilities + +### New Capabilities +- `dbal-virtual-registers`: Connecting to an external SQL database over Doctrine DBAL and exposing it — read-only and live — as an OpenRegister register whose schemas and objects are introspected from tables/views/columns/foreign keys. + +### Modified Capabilities + + +## Impact + +- **New code:** `lib/Service/Dbal/DbalConnectionFactory.php`, `lib/Service/Dbal/DatabaseIntrospectionService.php`, `lib/Service/Dbal/SqlTypeMapper.php` (SQL→JSON-Schema type/format), `lib/Service/ObjectSource/DbalObjectSourceProvider.php`, `DbalIntrospectionJob` background job. +- **Modified code:** `lib/AppInfo/Application.php` (register the provider in the object-source boot list + DI for the new services + job), `lib/Controller/SourcesController.php` (+ `appinfo/routes.php`) for `test-connection` and `introspect`, `ObjectService::paginateObjectSource()` (backward-compatible pagination-metadata fix for all providers), minimal Vue add-source flow. +- **Reused as-is:** `ObjectSourceProvider`/`ObjectSourceRegistry` seam (no interface change), `Schema::getObjectSource()` binding, `Source` entity (type `database`), `CredentialStoreResolver`/Doriath custody seam (ADR-004), `SchemaDiffService`, `Register.source`. +- **Dependencies:** `doctrine/dbal ^3.8` (already required). No new runtime dependency. +- **Security surface:** external DB credentials (vault-custodied), SQL injection (parameterised QueryBuilder + platform identifier quoting), and connection-failure error semantics (502/503, never a bare 500). +- **Downstream:** opencatalogi/softwarecatalog consume virtual schemas transparently through the standard objects API; `_extend` relation resolution works because FKs map onto the canonical relation dialect. diff --git a/openspec/changes/dbal-virtual-registers/specs/dbal-virtual-registers/spec.md b/openspec/changes/dbal-virtual-registers/specs/dbal-virtual-registers/spec.md new file mode 100644 index 0000000000..3702c7411b --- /dev/null +++ b/openspec/changes/dbal-virtual-registers/specs/dbal-virtual-registers/spec.md @@ -0,0 +1,118 @@ +## ADDED Requirements + +### Requirement: Connect to an external database as a virtual register +The system SHALL allow an administrator to configure a `Source` of type `database` that references an external relational database over Doctrine DBAL, and SHALL build a read-only connection to it on demand. Supported drivers SHALL be `pdo_mysql`, `pdo_pgsql`, and `pdo_sqlite`. The database password SHALL be custodied through the `CredentialStore` abstraction resolved via `CredentialStoreResolver` (Doriath custody leaf when eligible, Nextcloud vault fallback — ADR-004) and referenced from the Source by credential UUID; it SHALL NOT be stored in plaintext in the Source row or any OpenRegister object. + +#### Scenario: Admin creates a database source and tests the connection +- **WHEN** an administrator submits a database source (driver, host, port, dbname, user, credential reference) and invokes the test-connection action +- **THEN** the system resolves the password through the credential custody seam, opens a DBAL connection, runs a trivial read, and returns success without exposing the password in the response + +#### Scenario: Test connection fails without leaking secrets +- **WHEN** the test-connection action runs against an unreachable host or with wrong credentials +- **THEN** the system returns a 503 (unreachable) or 502 (upstream error) with a non-sensitive message and no bare 500 and no credential value in the response or logs + +#### Scenario: Missing or undecryptable credential fails closed +- **WHEN** the password cannot be resolved from the vault for a database source +- **THEN** the connection factory throws a typed connection exception and never opens an unauthenticated connection +- **@e2e** exclude vault-decrypt-failure requires fault injection into the credential store; covered by a unit test on the connection factory + +### Requirement: Introspect SQL structure into JSON Schema +The system SHALL introspect a connected database via the DBAL schema manager and produce a `Register` (whose `source` is the source id) plus one `Schema` per table and per view. Each produced schema SHALL carry an `x-openregister-object-source` annotation of the form `{provider: 'dbal-source', config: {sourceId, table, idColumn}}`. Column types SHALL map to JSON-Schema `type`/`format` per the change's type-mapping table; `NOT NULL` columns that are neither the primary key nor database-defaulted SHALL become `required`; column length SHALL map to `maxLength`. Re-introspection SHALL update existing schemas matched by table name rather than duplicating them. + +#### Scenario: Introspecting a table produces a schema with typed properties +- **WHEN** an administrator introspects a database source containing an `applicants(id PK, full_name TEXT NOT NULL, email VARCHAR(255) NOT NULL, kvk_number VARCHAR NULL)` table +- **THEN** a schema `applicants` is created with `full_name` and `email` in `required`, `kvk_number` optional, `email` typed `string` with `maxLength` 255, `idColumn` set to `id`, and the `x-openregister-object-source` annotation naming provider `dbal-source` + +#### Scenario: Views are exposed as schemas +- **WHEN** the source contains a view `active_permits` +- **THEN** introspection creates a schema `active_permits` served by the same provider + +#### Scenario: Re-introspection updates rather than duplicates +- **WHEN** an administrator introspects the same source twice +- **THEN** the second run updates the existing schemas in place and does not create duplicate schemas + +### Requirement: Scheduled re-introspection detects schema drift +The system SHALL run a background job on a configurable interval that re-introspects every `type: database` source, applies structural changes to the affected schemas as diffs via the existing schema-diff path, and logs detected drift (added, dropped, or retyped columns; new or removed tables). A failure for one source SHALL NOT prevent the job from processing the remaining sources. A manual introspect action SHALL remain available for on-demand refresh. The job's run method SHALL perform this work directly (no stub body). + +#### Scenario: Background job picks up an added column +- **WHEN** a column is added to an external table and the scheduled introspection job runs +- **THEN** the corresponding schema gains the new property via a schema diff and the drift is logged +- **@e2e** exclude background-job timing cannot be driven deterministically through the UI; covered by an integration test invoking the job class directly against the SQLite fixture + +#### Scenario: One failing source does not block the rest +- **WHEN** the job runs while one of several database sources is unreachable +- **THEN** the unreachable source is logged as failed and the remaining sources are still re-introspected +- **@e2e** exclude requires fault injection across multiple sources; covered by a unit test on the job + +### Requirement: Map foreign keys to canonical relations +The system SHALL map each single-column foreign key to a relation property on the owning schema using the canonical relation dialect: `type: string`, a `format`, a `$ref` to the related virtual schema, and `objectConfiguration.handling: 'related-object'`. The inverse side SHALL be added on the target schema with `inversedBy`. Multi-column foreign keys SHALL NOT be mapped to `$ref` in v1. + +#### Scenario: A foreign key becomes a resolvable relation +- **WHEN** introspection encounters `permits.applicant_id` referencing `applicants.id` +- **THEN** the `permits` schema gains an `applicant_id` property with a `$ref` to the `applicants` schema and `objectConfiguration.handling: related-object`, and `applicants` gains an inverse property with `inversedBy` + +#### Scenario: Related object expands via _extend +- **WHEN** a client reads a `permits` object with `_extend=applicant_id` +- **THEN** the referenced `applicants` object is resolved and embedded, using the existing relation-resolution machinery unchanged + +### Requirement: Serve external rows as live read-only objects +The system SHALL serve objects of a `dbal-source` schema live through the existing object-source dispatch, translating OpenRegister queries (filters, search, sort, limit, offset) into a parameterised DBAL query builder. The provider SHALL apply `limit`/`offset` in SQL, expose a real `count()` via `SELECT COUNT(*)` with the same predicate, quote all identifiers via the platform, and restrict queried tables to the introspected allowlist. Writes to a `dbal-source` schema SHALL be rejected. + +#### Scenario: List objects from an external table +- **WHEN** a client requests objects of the `permits` schema with a filter on `status` and a page limit +- **THEN** the provider returns the matching rows as objects, having applied the filter as a bound parameter and the limit/offset in SQL + +#### Scenario: Fetch a single object by id +- **WHEN** a client requests a `permits` object by its `id` +- **THEN** the provider issues `WHERE = :id` with a bound parameter and returns the object, or null when absent + +#### Scenario: Filter values are bound, not interpolated +- **WHEN** a client supplies a filter value containing SQL metacharacters (e.g. `' OR 1=1 --`) +- **THEN** the value is passed as a bound parameter and matched literally, returning no injected rows + +#### Scenario: Writes are rejected +- **WHEN** a client attempts to create, update, or delete an object on a `dbal-source` schema +- **THEN** the request is rejected because the schema is served by a read-only object source + +### Requirement: Correct pagination metadata for object-source schemas +The object-source pagination dispatch SHALL pass the query's limit and offset through to the provider, SHALL consult the provider's `count()` for the true total, and SHALL compute `page`, `pages`, `next`, and `prev` from total, limit, and offset. When a provider signals no count support (its `count()` throws or returns a value inconsistent with the returned window), the dispatch SHALL fall back to the current in-memory behaviour (total = returned result count, single page) so the existing providers remain backward compatible. + +#### Scenario: Paging through a large external table +- **WHEN** a client requests page 2 with a limit of 50 from a `dbal-source` schema backed by a table of 120 rows +- **THEN** the response contains 50 objects, `total` 120, `pages` 3, `page` 2, and working `next`/`prev` links + +#### Scenario: Provider without count support falls back +- **WHEN** a schema is served by a provider whose `count()` cannot report a real total +- **THEN** pagination metadata falls back to the pre-existing in-memory behaviour without an error +- **@e2e** exclude requires a synthetic provider stub without count support; covered by a unit test on the dispatch fallback + +### Requirement: Read access parity and no enumeration oracle +The provider SHALL apply the same read authorization as native schemas for the acting user. When an object is absent or access is denied, `find` SHALL return null indistinguishably, and `findAll` SHALL omit denied objects, so no enumeration oracle is exposed. + +#### Scenario: Denied and absent are indistinguishable +- **WHEN** a user without read access requests an existing `dbal-source` object and, separately, requests a non-existent id +- **THEN** both requests return null with no difference that reveals the object's existence +- **@e2e** exclude RBAC-parity for a virtual schema requires seeding an access-restricted virtual register; covered by a provider unit test asserting the null-return contract + +### Requirement: Resilient failure semantics +When the external database is unreachable or returns an error during a read, the system SHALL surface 503 (unreachable) or 502 (upstream error) semantics and SHALL NOT return a bare 500. When the required DBAL driver extension is absent, the provider's `isEnabled()` SHALL return false and a bound schema SHALL degrade to an empty result with a logged warning. + +#### Scenario: External database down during a read +- **WHEN** the external database becomes unreachable while a client lists a `dbal-source` schema +- **THEN** the system responds with a 503-class error and a logged warning, not a 500 +- **@e2e** exclude simulating mid-request DB outage requires network fault injection; covered by a provider unit test that stubs a DBAL connection exception + +#### Scenario: Driver extension missing degrades gracefully +- **WHEN** the `pdo_pgsql` extension is not installed and a schema bound to a pgsql source is read +- **THEN** the provider reports itself disabled and the read returns an empty list with a logged warning + +### Requirement: Composite and missing primary keys +The system SHALL handle tables without a single-column primary key. For a composite primary key, the object id SHALL be a deterministic ordered join of the key columns and the schema's object-source config SHALL record `idColumns`. For a table with no primary key, the schema SHALL be read-list-only with `idColumn` null: `findAll`/`count` work but `find(id)` returns null. + +#### Scenario: Composite primary key yields a deterministic id +- **WHEN** introspecting a table whose primary key is `(tenant_id, code)` +- **THEN** each object's id is the ordered join of the two column values and `find` reconstructs the predicate from the parts + +#### Scenario: Table with no primary key is list-only +- **WHEN** introspecting a table with no primary key +- **THEN** the schema is created with `idColumn` null, `findAll` and `count` succeed, and `find(id)` returns null diff --git a/openspec/changes/dbal-virtual-registers/tasks.md b/openspec/changes/dbal-virtual-registers/tasks.md new file mode 100644 index 0000000000..91fc16403e --- /dev/null +++ b/openspec/changes/dbal-virtual-registers/tasks.md @@ -0,0 +1,58 @@ +## 1. Connection factory & credential custody + +- [ ] 1.1 Add `DbalConnectionFactory` (`lib/Service/Dbal/DbalConnectionFactory.php`): resolve a `type: database` Source → password via `CredentialStoreResolver::resolve()->get(uuid, scope)` (Doriath custody leaf when eligible, NC vault fallback — ADR-004) → `DriverManager::getConnection([...])`; per-request connection cache keyed by sourceId; fail closed (typed `DbalConnectionException`) on decrypt/secret-missing; drivers restricted to `pdo_mysql`/`pdo_pgsql`/`pdo_sqlite`. + - Acceptance: connecting to the SQLite fixture succeeds; a source with an unresolvable credential throws, never opens an unauthenticated connection; password never appears in logs; factory depends only on the `CredentialStore` interface. +- [ ] 1.2 Wire `Source` type `database` to store non-secret metadata (driver/host/port/dbname/user) + credential UUID in `authConfig`; keep the password in the credential custody seam, not in `databaseUrl`/`ICrypto`. Document the two-path split (legacy harvest vs. virtual DB) in code. + - Acceptance: a database Source round-trips with no plaintext password persisted in the row or any OR object. + +## 2. Seed data (demo fixture) + +- [ ] 2.1 Add `tests/fixtures/dbal/build-permits-sqlite.php` that builds `permits.sqlite` (tables `permit_types`, `applicants`, `permits` with FKs + view `active_permits`) and `tests/fixtures/dbal/expected-introspection.json` (golden register+schemas). Do not commit a binary DB — generate it in the test bootstrap. + - Acceptance: running the builder produces a SQLite DB whose introspection matches the golden JSON. + +## 3. Introspection & mapping + +- [ ] 3.1 Add `SqlTypeMapper` (`lib/Service/Dbal/SqlTypeMapper.php`): DBAL `Types::*` → JSON-Schema `type`/`format`/`maxLength`/precision per the design table; unknown types → `string` fallback + logged warning. + - Acceptance: unit tests cover every row of the mapping table for mysql/pgsql/sqlite type names. +- [ ] 3.2 Add `DatabaseIntrospectionService` (`lib/Service/Dbal/DatabaseIntrospectionService.php`): `createSchemaManager()` → Register (`source=sourceId`) + one Schema per table/view; `NOT NULL` (non-PK, non-defaulted) → `required`; single-column PK → `idColumn`; tag each schema `x-openregister-object-source = {provider:'dbal-source', config:{sourceId, table, idColumn}}`; re-introspection updates in place. + - Acceptance: introspecting the fixture yields schemas matching `expected-introspection.json`; a second run creates no duplicates. +- [ ] 3.3 Map foreign keys to the canonical relation dialect (`type:string` + `format` + `$ref` + `objectConfiguration.handling: related-object`), add inverse side with `inversedBy`; skip multi-column FKs. + - Acceptance: `permits.applicant_id` gains a `$ref` to `applicants` and `_extend` resolves it via the existing RelationHandler. +- [ ] 3.4 Handle composite PK (deterministic ordered-join id + `idColumns` in config) and no-PK tables (list-only, `idColumn` null); log both at introspection. + - Acceptance: composite-PK table `find` reconstructs the predicate; no-PK table `find(id)` returns null while `findAll`/`count` work. +- [ ] 3.5 Add `DbalIntrospectionJob` (NC `TimedJob`, configurable interval): re-introspect every `type: database` source, apply diffs via `SchemaDiffService`, log drift; per-source failures caught and logged so the job continues; `run()` does the real work (no stub body — hydra stub-scan gate); register the job. + - Acceptance: invoking the job against the fixture after adding a column applies the diff and logs the drift; an unreachable source does not block the others. + +## 4. Object-source provider + +- [ ] 4.1 Add `DbalObjectSourceProvider implements ObjectSourceProvider` (`lib/Service/ObjectSource/DbalObjectSourceProvider.php`): `getId()='dbal-source'`, `isEnabled()` false when the driver extension is absent. + - Acceptance: provider satisfies the interface signatures verified against `ObjectSourceProvider.php`. +- [ ] 4.2 Implement `findAll`/`find`/`count`: OR query → parameterised `QueryBuilder` (filters/search/sort as bound params), `setMaxResults()/setFirstResult()` for limit/offset, `count()` via `SELECT COUNT(*)` with the same WHERE; identifier quoting via the platform; per-source table allowlist; hard result cap; rows → `ObjectEntity`. + - Acceptance: a filter value with SQL metacharacters is matched literally; limit/offset applied in SQL; count reflects the full predicate. +- [ ] 4.3 Enforce read authorization parity (denied/absent both null in `find`, denied omitted in `findAll`) and read-only (writes rejected via the existing object-source path). + - Acceptance: null-return contract holds; a write to a `dbal-source` schema is rejected. +- [ ] 4.4 Map DBAL connection/query exceptions to 502/503 semantics (never a bare 500); log warnings; degrade to empty list when `isEnabled()` is false. + - Acceptance: a stubbed connection exception surfaces a 503-class error, not a 500. +- [ ] 4.5 Extend `ObjectService::paginateObjectSource()` (design D4b): pass limit/offset through to `findAll()`, consult the provider's existing `count()` for the true total, compute `page`/`pages`/`next`/`prev`; fall back to the current in-memory behaviour when a provider signals no count support. Minimal and backward-compatible — no interface change. + - Acceptance: page-2/limit-50 over a 120-row fixture table returns total 120, pages 3; a regression test over a native provider (and a no-count stub) shows unchanged fallback behaviour. + +## 5. Registration & routes + +- [ ] 5.1 Register `DbalObjectSourceProvider` DI in `Application::register()` and add it to the `$providerClasses` list in `Application::bootObjectSourceProviders()`; register the new `lib/Service/Dbal/*` services. + - Acceptance: the provider is resolvable by `ObjectSourceRegistry::get('dbal-source')` at runtime. +- [ ] 5.2 Add `test-connection` and `introspect` actions to `SourcesController` + `appinfo/routes.php` (admin-only, correct NC auth attributes); responses never expose the password. + - Acceptance: routes are reachable, admin-guarded, and covered by route-auth/route-reachability gates. + +## 6. UI (minimal) + +- [ ] 6.1 Add a minimal add-database-source flow: driver/host/db/user + credential selection, a "Test connection" button, table selection, and "Create virtual register" (introspect). Modals in their own files; `NcSelect` with `inputLabel`. + - Acceptance: an admin can add the SQLite-fixture source, test it, pick tables, and browse the resulting virtual schemas through the standard objects UI. + +## 7. Tests & quality gates + +- [ ] 7.1 Unit tests: `SqlTypeMapper` (all mapping rows), `DbalConnectionFactory` fail-closed, provider query-translation + injection-literal + count. + - Acceptance: tests pass; each ADDED spec scenario is traceable to a test or carries an `@e2e exclude`. +- [ ] 7.2 Integration test: introspect the SQLite fixture and assert against `expected-introspection.json`; read objects and resolve a relation via `_extend`. + - Acceptance: golden-file assertion passes. +- [ ] 7.3 Run `composer check:strict` (PHPCS, PHPMD, Psalm, PHPStan) and the Hydra mechanical gates; fix all findings including any pre-existing issues touched. + - Acceptance: `composer check:strict` and the gates pass green. diff --git a/openspec/changes/deprecate-published-metadata/specs/deprecate-published-metadata/spec.md b/openspec/changes/deprecate-published-metadata/specs/deprecate-published-metadata/spec.md new file mode 100644 index 0000000000..5e68bf113f --- /dev/null +++ b/openspec/changes/deprecate-published-metadata/specs/deprecate-published-metadata/spec.md @@ -0,0 +1,59 @@ +# Spec: Deprecate Published/Depublished Object Metadata + +## Overview + +Remove the dedicated `published`/`depublished` object metadata system from OpenRegister. The RBAC `$now` dynamic variable (already implemented) replaces this functionality for authorization-based visibility control. + +## Scope + +**In scope (OpenRegister only):** +- Remove `addPublishedDateToObjects()` from ImportService and auto-publish import logic +- Remove `@self.published`/`@self.depublished` references from frontend copy modals +- Remove object "published" stats from dashboard, register, and schema views +- Update MultiTenancyTrait documentation to remove object-level published bypass references +- Add deprecation log warnings for schema config keys if encountered + +**Out of scope:** +- Register/Schema `published`/`depublished` fields (multi-tenancy bypass system) +- File publish/depublish (Nextcloud share management, `autoPublish` in FilePropertyHandler) +- Configuration `publishToGitHub` (GitHub export) +- OpenCatalogi and Softwarecatalogus changes (separate repos) +- `SearchTrailMapper.published_only` (historical tracking data) +- MagicMapper columns (already removed) +- Object publish/depublish API routes (already removed) +- ObjectEntity published/depublished properties (already removed) + +## Requirements + +### REQ-1: ImportService Published Date Removal +- GIVEN an import operation with `publish=true` +- WHEN objects are imported via JSON or CSV +- THEN the `addPublishedDateToObjects()` logic is removed +- AND the `$publish` parameter is ignored with a deprecation log warning +- AND existing import functionality continues to work without published date injection + +### REQ-2: Frontend Copy Modal Cleanup +- GIVEN a user copies an object via CopyObject or MassCopyObjects modal +- WHEN the `@self` metadata is stripped from the copy +- THEN `published` and `depublished` keys are no longer deleted (they don't exist) + +### REQ-3: Frontend Stats Cleanup +- GIVEN dashboard, register detail, or schema detail views +- WHEN object statistics are displayed +- THEN the "published" count row/column is removed from the stats display + +### REQ-4: Import UI Cleanup +- GIVEN the ImportRegister modal +- WHEN a user configures an import +- THEN the "Auto-publish imported objects" toggle is removed + +### REQ-5: MultiTenancyTrait Documentation +- GIVEN the MultiTenancyTrait docblock +- WHEN describing multi-tenancy bypass +- THEN object-level published bypass references are removed +- AND Register/Schema published bypass documentation remains + +### REQ-6: Deprecation Warnings +- GIVEN a schema with `objectPublishedField`, `objectDepublishedField`, or `autoPublish` config keys +- WHEN an object is saved using that schema +- THEN a deprecation warning is logged suggesting migration to RBAC rules with `$now` diff --git a/openspec/changes/deprecate-published-metadata/tasks.md b/openspec/changes/deprecate-published-metadata/tasks.md new file mode 100644 index 0000000000..c1a71cb5e6 --- /dev/null +++ b/openspec/changes/deprecate-published-metadata/tasks.md @@ -0,0 +1,102 @@ +# Tasks: Deprecate Published/Depublished Object Metadata + +> **Status note (2026-06-15):** All **OpenRegister-core** scope is COMPLETE — columns dropped by migration `lib/Migration/Version1Date20260313130000.php`, removal locked by `tests/Unit/Service/ImportServicePublishDeprecationTest.php`, frontend cleaned (Phases 1–3, 8). The lingering open `[ ]` items below are **Phases 4–7: downstream cleanup in SEPARATE repos** (OpenCatalogi / Softwarecatalogus / decidesk), which this change cannot tick from the OR repo. That downstream cleanup is **now being done** in those repos (opencatalogi `fix/publish-rbac`; softwarecatalog + decidesk anon-publish builds, 2026-06-15), migrating publication visibility to the live RBAC model (public-group read with `publicatiedatum <= $now`). These items are tracked there, not forgotten. +> +> **Archive readiness:** All OR-core tasks are done, and the only remaining items are out-of-scope downstream tasks now tracked in their own repos. This change is therefore a candidate to **archive** — but the archive decision is left FLAGGED here (not forced) until the downstream `fix/publish-rbac` / anon-publish PRs land. Do NOT fake-tick the cross-repo `[ ]` items in OR. +> +> See also: the retired `BUG-OBJ-3` entry in `CODE-REVIEW-IMPROVEMENT-PLAN.md` — re-adding object-level `setPublished()` would revert this breaking change and must not be implemented. + +## Phase 1: OpenRegister Core Cleanup (COMPLETED - already done prior to this change) + +### MagicMapper Column and Metadata Removal +- [x] Remove `_published` and `_depublished` from `MagicMapper::getBaseMetadataColumns()` (already removed) +- [x] Remove `'published'` from `$metadataColumns` arrays in `ensureTableForRegisterSchema()` (already removed) +- [x] Remove `'published'` from `$idxMetaFields` index definitions (already removed) +- [x] Remove `'published'` and `'depublished'` from `buildInsertData()` metadata fields list (already removed) +- [x] Remove `'published'` and `'depublished'` from datetime conversion check (already removed) +- [x] Remove `'published'` and `'depublished'` from `buildObjectFromRow()` datetime field list (already removed) + +### Search and Facet Handlers +- [x] Remove `'published'` and `'depublished'` from `MariaDbSearchHandler` (already removed) +- [x] Remove `'published'` and `'depublished'` from `MetaDataFacetHandler` (already removed) +- [x] Remove `'published'` from `MagicFacetHandler` (already removed) + +### SaveObject Metadata Hydration +- [x] Remove `objectPublishedField` processing from `SaveObject::hydrateObjectMetadata()` (already removed) +- [x] Remove `objectDepublishedField` processing (already removed) +- [x] Remove `autoPublish` processing from SaveObject (already removed) +- [x] Add deprecation warning log when these config keys are encountered in schema configuration (#1132) +- [x] Remove published field processing in `setSelfMetadata()` (already removed) + +### Search Query Pipeline +- [x] Remove `'published'` and `'depublished'` from `@self` metadata fields in `SearchQueryHandler` (already removed) +- [x] Remove `$params['published']` passing in `SearchQueryHandler` (already removed) + +### Index Service (Solr) +- [x] Remove `$published` parameter from `IndexService::searchObjects()` (already removed) +- [x] Remove `$published` parameter from `ObjectHandler::searchObjects()` and `buildSolrQuery()` (already removed) +- [x] Remove `published:true` Solr filter (already removed) +- [x] Remove `$published` parameter from `SearchBackendInterface::searchObjects()` (already removed) + +### Controller Cleanup +- [x] Update `ObjectsController` docblock comments (already removed) +- [x] Update `BulkController` class docblock (already removed) +- [x] Remove object publish/depublish methods from `BulkController` (already removed) + +### Documentation Updates +- [x] Remove `published`/`depublished` from MultiTenancyTrait documentation about object-level bypass (#1132) + +### Import Service +- [x] Remove `addPublishedDateToObjects()` from `ImportService` (#1128) +- [x] Add deprecation warning when `$publish=true` is passed to import methods (#1128) + +## Phase 2: Database Migration Verification (COMPLETED) + +- [x] Verify `Version1Date20260313130000` migration handles tables where columns don't exist (idempotent) (#1133) +- [x] Test migration on a database with magic tables that have `_published`/`_depublished` columns (#1133) +- [x] Test migration on a database with magic tables that do NOT have these columns (#1133) + +## Phase 3: OpenRegister Frontend (COMPLETED) + +- [x] Remove `@self.published`/`@self.depublished` from copy object modals (#1129) +- [x] Remove published object stats from all frontend views (#1130) +- [x] Remove auto-publish toggle from ImportRegister modal (#1131) +- [x] Remove published CSS classes from schema modals (#1130) +- [x] Remove published from type definitions and mock data (#1130) + +## Phase 4: OpenCatalogi Backend (OUT OF SCOPE - separate repo) + +- [ ] Remove `isObjectPublished()` from `EventService`; replace published-state checks with RBAC-based logic +- [ ] Remove `@self.published`/`@self.depublished` reads from `ObjectCreatedEventListener` +- [ ] Remove `isObjectEntityPublished()` and `isObjectPublished()` from `ObjectUpdatedEventListener` +- [ ] Remove `@self.published`/`@self.depublished` reads from `ObjectUpdatedEventListener` +- [ ] Remove `'published'` and `'depublished'` from `$universalOrderFields` in `PublicationsController` +- [ ] Update `PublicationService` docblock examples referencing `@self.published` ordering + +## Phase 5: OpenCatalogi Frontend (OUT OF SCOPE - separate repo) + +- [ ] Delete `src/modals/object/MassPublishObjects.vue` +- [ ] Delete `src/modals/object/MassDepublishObjects.vue` +- [ ] Delete or repurpose `src/components/PublishedIcon.vue` for RBAC-based visibility +- [ ] Remove `publishObject()`/`depublishObject()` from `src/store/modules/object.js` +- [ ] Remove `published`/`depublished` from publication and attachment entities + +## Phase 6: Softwarecatalogus Frontend (OUT OF SCOPE - separate repo) + +- [ ] Delete `src/modals/object/MassPublishObjects.vue` +- [ ] Delete `src/modals/object/MassDepublishObjects.vue` +- [ ] Delete or repurpose `src/components/PublishedIcon.vue` + +## Phase 7: Schema Migration Guide (OUT OF SCOPE - documentation change) + +- [ ] Create migration guide documentation +- [ ] Update existing WOO publication schemas in OpenCatalogi to use RBAC rules +- [ ] Test WOO publication visibility with RBAC `$now` rules end-to-end + +## Phase 8: Testing (COMPLETED for OpenRegister scope) + +- [x] Test that deprecated schema config keys produce deprecation warning logs (#1133) +- [x] Test that ImportService $publish parameter is deprecated (#1133) +- [x] Test migration idempotency (#1133) +- [ ] Test OpenCatalogi WOO publication schemas with RBAC `$now` rules (separate repo) +- [ ] Test Softwarecatalogus date-based queries (separate repo) diff --git a/openspec/changes/dsar-case-engine/.openspec.yaml b/openspec/changes/dsar-case-engine/.openspec.yaml new file mode 100644 index 0000000000..43e65ca6e6 --- /dev/null +++ b/openspec/changes/dsar-case-engine/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-03 diff --git a/openspec/changes/dsar-case-engine/design.md b/openspec/changes/dsar-case-engine/design.md new file mode 100644 index 0000000000..ceb0cd34d8 --- /dev/null +++ b/openspec/changes/dsar-case-engine/design.md @@ -0,0 +1,232 @@ +## Context + +The declarative head `dsar-case-subsystem` (kind:config, merged first in the chain) extends +`lib/Settings/data_subject_request_register.json` so the `dataSubjectRequest` schema is a stateful +**case**: an N-state `x-openregister-lifecycle` (`received → verifying → in-progress → +fulfilled/refused/closed`, plus `assign`/`collectEvidence`/`draftDenial`/`finaliseDenial`/`redact`/ +`bundle`/`retain`), deadline-tracking `x-openregister-calculations`/`-aggregations`, and +advance-reminder/escalation/breach `x-openregister-notifications`. It declares the **shape** of the +`evidence` and `redactions` sub-collections, the `denialGround` enum, and the +`retentionWindow`/`retainUntil`/`purgedAt` stamps — but it ships **no runnable behaviour** for +them, and its `finaliseDenial` transition references a `requires` guard FQCN that does not yet +exist. + +OpenRegister already ships the stateless DSAR engine this change builds on +(`lib/Service/Gdpr/DataSubjectRequestService.php`): `assembleAccessExport(subjectId, type?)`, +`erase(subjectId, type?, eraseMode, dryRun)` (defaulting to `ERASE_MODE_PSEUDONYMISE`), +`rectify`/`setRestriction`/`setObjection`, plus the art-12 maths via `DataSubjectDeadline` +(`computeDueAt`/`extend`/`isOverdue`/`daysRemaining`). Retention primitives live in +`lib/Service/RetentionService.php` (`hasActiveLegalHold`, `validateNotImmutable`, +`findEligibleForDestruction`) and there is an existing `AvgRetentionJob`/`TimedJob` scheduled-work +pattern under `lib/BackgroundJob/`. Every write is pinned to the DSAR processing activity via +`ObjectEntity::setProcessingActivityId()` so the immutable hash-chained `AuditTrailMapper` records +it. Existing DSAR routes live under `/api/gdpr/*` (`DataSubjectRequestController`) and `/api/avg/*` +(`DsarController`). + +This change is the **imperative successor** (config head → this engine → Phase 2 +`dsar-policy-pack-and-seams`). It supplies the ADR-031 imperative exceptions the head deferred, so +that a register with both changes has a fully operational tracked workflow. + +Constraints: +- All case reads/writes flow through `ObjectService` (RBAC + multitenancy); no custom Entity/Mapper + (ADR-001). Case-level access control (handler-scopes-own + officer override, ADR-023) layers on + top, it does not replace object RBAC. +- Reuse the existing stateless primitives; do not re-implement export assembly, deadline maths, + erase, or legal-hold awareness (ADR-011). +- Endpoints are authenticated steward endpoints (`@NoAdminRequired`), never `@PublicPage`; every + method is registered in `appinfo/routes.php` and reachable (ADR-005/016/029). + +Stakeholders: the OR team (owns the engine + provider/guard/job contracts), Phase 2 +`dsar-policy-pack-and-seams` (binds jurisdiction data + integration seams), pipelinq (reference +migrator, later). + +## Goals / Non-Goals + +**Goals:** +- An `EvidenceSourceProvider` interface + a registry (ADR-019 style) so leaf apps register harvest + sources; async collection that dedups by content hash and tracks per-item `status` onto the + case's declared `evidence` sub-collection. +- An export-bundle service: assemble via `DataSubjectRequestService::assembleAccessExport`, sign + (PAdES-LTV / SHA-256), mint a one-time secure download token, and assemble a regulator dossier + from the case's evidence + redactions + audit trail. +- A field-level redaction write path recording before/after + ground through the audit trail, + distinct from erase-time pseudonymise. +- The `finaliseDenial` lifecycle guard class enforcing the mandatory `regulatorReference` + precondition (the FQCN the head's transition references). +- A scheduled, legal-hold-aware retention sweep (`TimedJob`): hard-delete dossiers past + `retainUntil`, scrub evidence PII via `erase(mode=pseudonymise)`. +- Case-management REST controllers/routes with correct auth posture + a case-level access-control + check. + +**Non-Goals:** +- No jurisdiction policy-pack config contract and no identity-verify (BSN/BRP/RvIG) / + regulator-escalate (AP) integration seams — that is Phase 2 `dsar-policy-pack-and-seams`, + referenced only as `depends_on` successor context. `BsnFormat` is noted for the Phase-2 author, + not used here. +- No change to the declarative head's register (schema, lifecycle graph, denial-ground enum, + notification rules) — this change **consumes** it. No new OR schema is added. +- No change to the existing stateless fulfilment behaviour — it is reused verbatim. +- No frontend / Vue (the full `AvgIndex.vue` case-management surface is a Phase-2 concern per + ADR-047's migration sequencing). + +## Decisions + +### Declarative-vs-imperative decision (ADR-031) + +The head owns the declarative column (lifecycle, calculations, aggregations, notifications, schema +shape). This change is `kind: code` — every item below is a **legitimate ADR-031 imperative +exception**, not a declarative construct that was reached for in code. The default is declarative; +each entry justifies why the schema engine cannot express it: + +| Behaviour | Chosen path | ADR-031 rationale (why imperative, not declarative) | +|---|---|---| +| **Pluggable evidence-source providers + async harvest + content-hash dedup + per-item status** | **Imperative** — `EvidenceSourceProvider` interface + registry + harvest service | External-source harvest via provider adapters reaching outside OR (OpenConnector et al.) is exactly the ADR-003/ADR-031 "external API integration" exception; the schema engine cannot reach external systems, compute content hashes, or drive async collection. The head declared only the `evidence` sub-collection *shape*; the harvesting is code. Registry style follows ADR-019. | +| **Signed export bundle** (assemble + PAdES-LTV/SHA-256 sign + one-time download token + regulator dossier) | **Imperative** — export-bundle service | Document generation / PAdES signing / rendered signed bytes is the ADR-031 document-generation exception; the schema engine has no opinion on rendered/signed output. The one-time token is a security credential minted + burned at request time — a stateful side effect, not a declarative field. | +| **Field-level redaction write path** (apply before/after + ground, audit it) | **Imperative** — redaction-write service | The head declared the `redactions` sub-collection shape; *capturing* the before/after at redaction time and recording it to the immutable trail is a write operation with side effects — it belongs to the engine, and must be distinct from `erase(mode=pseudonymise)`. | +| **Denial finalise guard** (mandatory `regulatorReference` before finalise) | **Imperative** — a short lifecycle guard class (`requires` FQCN) | ADR-031 §3 explicitly permits a short PHP guard as a lifecycle transition precondition. The head *declared* the `finaliseDenial` transition with a `requires` reference to this class; this change *authors* the class. It is the legitimate PHP seam, not business logic smuggled out of the schema. | +| **Retention sweep** (hard-delete dossier + scrub evidence PII on a schedule, legal-hold aware) | **Imperative** — a `TimedJob` | Scheduled bulk work over an object queue with destructive side effects (delete/scrub) is the ADR-031 external-orchestrator / scheduled-work exception: use a `TimedJob`, not a declarative construct. The head declared the *windows*; the *sweep* is code. Reuses the `AvgRetentionJob`/`TimedJob` pattern + `RetentionService` legal-hold awareness. | +| **Controllers / routes / case-level access control** | **Imperative** — controller(s) + routes + access-control check | REST endpoints and request-time authorization are inherently imperative (ADR-002/005/016/023); they are the transport for the above services. The case-level check (handler-scopes-own + officer override) layers on OR object RBAC, it does not re-implement it. | + +Nothing in this table is a materialised per-object calculation, a cross-object aggregation, a +lifecycle state set, or a notification-on-condition — those are the head's, and re-doing any of +them in code here would be an ADR-031 (gate-18/gate-31) violation. + +### Reuse before build (ADR-011) + +Searched `lib/Formats/`, `lib/Service/`, `lib/Service/Gdpr/`, `lib/BackgroundJob/`, `lib/Cron/`, +and `appinfo/routes.php` before proposing anything. Reuse, do not duplicate: + +- **`DataSubjectRequestService::assembleAccessExport(subjectId, type?)`** — the export-bundle + service assembles from this; it does **not** re-implement subject-data discovery/assembly. Bundle + signing wraps the assembled payload. +- **`DataSubjectRequestService::erase(subjectId, type?, eraseMode=ERASE_MODE_PSEUDONYMISE, + dryRun)`** — the retention sweep scrubs evidence PII through this (pseudonymise mode); it does not + hand-roll a scrub. The redaction write path is deliberately **distinct** from this erase. +- **`DataSubjectDeadline`** — reused verbatim for any deadline reference the engine needs; the head + already declares the derived tracking fields. No second deadline implementation. +- **`RetentionService::hasActiveLegalHold` / `validateNotImmutable`** — the sweep consults these + before any hard-delete/scrub so a legal hold blocks destruction (same guarantee erase already + honours). +- **`AuditTrailMapper` + hash chain + `ObjectEntity::setProcessingActivityId()`** — every case + transition, redaction, evidence attach, and sweep action is audited through the existing + immutable trail pinned to the DSAR processing activity; no parallel audit log (ADR-022). +- **`AvgRetentionJob` / `OCP\BackgroundJob\TimedJob` + `IAppConfig` toggle/dry-run pattern** — the + retention sweep follows this exact shape (an enabled toggle + a dry-run toggle) rather than a + bespoke scheduler. +- **`BsnFormat` (`lib/Formats/BsnFormat.php`)** — reserved for Phase-2 identity verification; not + used here, noted so the Phase-2 author reuses it. +- **`DataSubjectRequestController` / `DsarController` + the `/api/gdpr/*` and `/api/avg/*` route + conventions** — the new case-management endpoints extend this surface (register in the same + `appinfo/routes.php`, same auth-annotation conventions) rather than replacing it. + +### Provider interface + registry (ADR-019) + +`EvidenceSourceProvider` is a narrow interface (identify + harvest-for-subject → items carrying a +`sourceId`, a `contentHash`, and a `status`). A registry collects the providers a leaf app +registers; the harvest service iterates registered providers, dedups items by `contentHash` (an +item whose hash already exists on the case is skipped, not re-appended), and writes each item's +per-item `status` onto the case's declared `evidence` sub-collection through `ObjectService`. This +mirrors the existing OR registry/integration surface and keeps external systems (OpenConnector) +behind the seam. + +### Object / access-control patterns (ADR-001, ADR-022, ADR-023) + +All case objects are OR objects under the `data-subject-requests` register / `dataSubjectRequest` +schema; reads/writes flow through `ObjectService` (RBAC + multitenancy). No custom Entity/Mapper. +The case-level access-control check is a thin authorization layer **on top of** object RBAC: a +handler may act on cases assigned to them (handler-scopes-own); a configured officer role may +override across cases. It never widens access beyond what object RBAC already grants — it only +narrows a broadly-authorized user to their own cases unless they hold the officer role. It fails +closed (a resolver returning null/unavailable denies, never skips — avoids the CWE-863 fail-open +pattern). + +### Endpoint shapes (ADR-002, REST / Common Ground) + +Provisional case-management routes, aligned with the existing `/api/gdpr/*` conventions +(exact shape confirmed in Open Questions): +- `POST /api/gdpr/cases` → create a case. +- `POST /api/gdpr/cases/{id}/transition` → run a declared lifecycle transition (incl. + `draftDenial`/`finaliseDenial`, the latter passing through the guard). +- `POST /api/gdpr/cases/{id}/evidence` → trigger/attach evidence harvest. +- `POST /api/gdpr/cases/{id}/redactions` → apply a field-level redaction. +- `POST /api/gdpr/cases/{id}/bundle` → generate the signed export bundle (returns a one-time + download reference). +- `GET /api/gdpr/cases/{id}/bundle/download?token=YOUR_TOKEN_HERE` → one-time secure download + (token burned on use). + +All carry `@NoAdminRequired` (+ `@NoCSRFRequired` where the client cannot supply a CSRF token, e.g. +the download), RBAC + case-level access control enforced in the service, and every method is +registered in `appinfo/routes.php` (ADR-016, ADR-029). None is `@PublicPage`. + +## Risks / Trade-offs + +- **[Depends on the head's register being imported first]** → The guard FQCN, evidence/redaction + shapes, and retention windows come from `dsar-case-subsystem`. Mitigation: `depends_on` + ordering — Hydra will not build this until the head's issue is merged; the guard is referenced by + the head's transition and resolved at runtime (same way decidesk's `MeetingTransitionGuard` + reference resolves). +- **[PAdES-LTV signing is a genuinely new dependency in OR]** → No existing PAdES/secure-token + primitive under `lib/`, and the 2026-07-04 spike found **no ready EUPL-compatible LTV signer** + (tc-lib-pdf stubs B-T; SetaPDF commercial). **Interim: SHA-256 hash-only** via the swappable + `PadesSigner` interface (`UnsignedPadesSigner`); **real PAdES-LTV DEFERRED**, leading candidate + `pyHanko` (MIT sidecar) or a tc-lib-pdf re-spike. This is a genuine integrity-vs-signature gap the + interim accepts: the SHA-256 hash proves the bytes weren't altered but is not a cryptographic + signature binding a signer identity — flagged for the deferred follow-up. +- **[One-time download token could leak or be replayed]** → Mitigation: mint a + single-use, time-boxed token that is burned on first successful download; the token is never a + realistic-looking secret in any artifact (`YOUR_TOKEN_HERE` placeholder only); the download route + is authenticated + case-scoped, not `@PublicPage`. +- **[Retention sweep hard-deletes irreversibly]** → Mitigation: legal-hold-aware + (`hasActiveLegalHold`) + an `IAppConfig` dry-run toggle mirroring `AvgRetentionJob`, so the first + deployment can log what *would* be destroyed before acting; every sweep action is audited. +- **[Evidence harvest reaching external systems can be slow / partial]** → Mitigation: async + collection with per-item `status` so a slow/failed source is visible on the case and re-runnable; + content-hash dedup makes re-runs idempotent (no duplicate evidence). +- **[Case-level access control silently failing open]** → Mitigation: the check fails closed; a + resolver that cannot determine the officer role denies rather than skips (CWE-863 / OWASP A01). + +## Migration Plan + +Additive only: new interface + registry, new services, a new `TimedJob`, new controller(s), new +routes. **No new OR schema** — this change consumes the head's register. No data backfill; existing +cases (seeded by the head) simply gain runnable behaviour. Rollback is removing the new classes + +routes + the job registration; the head's declarative case remains valid and exercisable via OR's +existing object/lifecycle/aggregation APIs. The retention sweep ships with its enabled toggle +defaulting on and a dry-run toggle for first-deployment verification (per `AvgRetentionJob`). + +## Seed Data + +This change adds **no new OR schemas** — it consumes the `dataSubjectRequest` schema + register the +`dsar-case-subsystem` head already extended (and whose seed case objects — municipality, +consultancy, travel agency — that head already provides). Therefore there is **no seed-data task +here**. Tests run the CI way (php:8.3-cli + OCP stubs, no live Nextcloud) against `ObjectService` / +provider / `RetentionService` doubles; any fixture ids/tokens use safe placeholders only (nil UUID +`00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`) — never realistic secrets/UUIDs. + +## Resolved decisions + +- **Bundle format + signing** — the export bundle is a **PDF disclosure document** (art-15 access) + carrying a **SHA-256 content hash**, behind the swappable `PadesSigner` interface. **Interim (now): + hash-only** (`UnsignedPadesSigner`). **PAdES-LTV signing is DEFERRED** (decision 2026-07-04, reversing + the earlier "required in Phase 1"): the spike found NO ready EUPL-compatible LTV signer — + `tecnickcom/tc-lib-pdf` 8.65.4 is LGPL-3/EUPL-clean and has the API but is **mid-development** + (fetches then **discards** the RFC-3161 timestamp → B-T stubbed; legacy `adbe.pkcs7.detached` + subfilter; no ESS signing-cert attr → not spec-strict PAdES; DSS/VRI structurally present but + meaningless without B-T). When PAdES-LTV is resumed the **leading candidate is `pyHanko`** (MIT + sidecar — real ETSI.CAdES.detached + embedded doc-timestamp + DSS/VRI, EU-DSS-validated; cost = a + Python runtime to package/secure), with a **re-spike of tc-lib-pdf** on a later release as the + license-clean pure-PHP alternative once its timestamp-embedding "next slice" lands. Either needs a + configurable RFC-3161 **TSA URL** (DigiCert/FreeTSA reachable; qualified TSA for eIDAS-grade). The + interface keeps this a config/adapter swap, not a call-site change. A machine-readable art-20 + *portability* payload (JSON/CSV + JAdES/CAdES) is likewise a deferred follow-up, out of scope here. +- **Route namespace** — case-management endpoints live under **`/api/gdpr/cases/...`**, mirroring the + existing DSAR routes. +- **Officer-override access control** — resolved from an admin-configured **ADR-023 action/group + mapping now**; the check **fails closed** when the role is unresolved. Phase-2 policy data may + refine the mapping. + +## Open Questions + +- One-time download token lifetime + concrete storage (short-lived app-config-backed vs a dedicated + token store) — deferred to implementation; the security posture (single-use, burned on first use, + time-boxed, authenticated, case-scoped) is fixed in the spec regardless. diff --git a/openspec/changes/dsar-case-engine/proposal.md b/openspec/changes/dsar-case-engine/proposal.md new file mode 100644 index 0000000000..5e5603aadf --- /dev/null +++ b/openspec/changes/dsar-case-engine/proposal.md @@ -0,0 +1,102 @@ +--- +kind: code +depends_on: [dsar-case-subsystem] +--- + +## Why + +The declarative head `dsar-case-subsystem` (kind:config) extends +`lib/Settings/data_subject_request_register.json` with the stateful `dataSubjectRequest` case +entity, its N-state `x-openregister-lifecycle`, deadline-tracking `-calculations`/`-aggregations`, +and reminder/escalation/breach `-notifications`. It deliberately declared only the **shape** of the +evidence and redaction sub-collections and stamped retention windows — it does **not** ship the +imperative surface those declarations imply. Per ADR-031 that surface is genuinely code: external +evidence harvest, PAdES document signing, scheduled retention bulk-work, and a lifecycle guard. + +This change is the **imperative successor** in the ADR-047 Phase-1 chain (config head → +**this engine** → Phase 2 `dsar-policy-pack-and-seams`). It builds the runnable case-management +behaviour on top of the head's declarations and OpenRegister's existing stateless DSAR primitives +(`DataSubjectRequestService`, `DataSubjectDeadline`, `RetentionService`, `AuditTrailMapper`), so +that the moment both changes land a register gets a fully operational, tracked data-subject-request +workflow — not just a schema. It lists `dsar-case-subsystem` in `depends_on`: the case entity, +lifecycle graph, denial-ground enum, and notification rules must already exist for this change's +providers, guard, sweep, and controllers to bind to them. + +## What Changes + +- **Pluggable evidence-source providers** — an `EvidenceSourceProvider` interface + a registry so + leaf apps register harvest sources (OpenConnector et al.), plus async collection that dedups + items by content hash and tracks per-item collection `status` on the case's declared `evidence` + sub-collection. (ADR-019 registry style; ADR-003 external-integration.) +- **Signed export bundle** — assemble the subject's data by reusing + `DataSubjectRequestService::assembleAccessExport`, sign it (PAdES-LTV / SHA-256), mint a + one-time secure download token, and assemble a regulator dossier from the case's evidence, + redactions, and audit trail. +- **Field-level redaction write path** — apply a redaction (before/after + ground) to a case + field and record the `redactions` entry through the immutable audit trail. Distinct from the + erase-time pseudonymise that `DataSubjectRequestService::erase(mode=pseudonymise)` already does. +- **Denial finalise guard** — the lifecycle-transition guard class the head's `finaliseDenial` + transition references via `requires`; it enforces the mandatory `regulatorReference` before a + case may enter the finalised-denial outcome (ADR-031 §3 legitimate PHP seam). +- **Retention sweep** — a scheduled `TimedJob` that enforces the head's declared windowed + retention: hard-delete dossiers past their `retainUntil`, scrub evidence PII via + `DataSubjectRequestService::erase(mode=pseudonymise)`, legal-hold aware via + `RetentionService::hasActiveLegalHold`. (ADR-031 scheduled-bulk-work exception; reuses the + existing `AvgRetentionJob`/`TimedJob` pattern.) +- **Controllers + routes** — case-management REST endpoints (create case, transition, attach + evidence, generate bundle, download bundle, deny/finalise) registered in `appinfo/routes.php` + with the correct auth posture (docblock `@NoAdminRequired` + RBAC in the service, ADR-005/016), + plus a case-level access-control check layered on OR object RBAC (handler-scopes-own + officer + override, ADR-023). + +**Explicitly out of scope — Phase 2 `dsar-policy-pack-and-seams` (successor; not specced here):** +the jurisdiction policy-pack config contract (deadline durations, denial-grounds wording, +retention-window values, letter templates) and the two pluggable integration seams — +identity-verify (BSN/BRP/RvIG) and regulator-escalate (AP-complaint dossier). This change defines +the generic case engine; Phase 2 supplies the jurisdiction data + bindings it consumes. Referenced +as `depends_on` successor context only. + +## Capabilities + +### New Capabilities +- `dsar-evidence-collection`: the pluggable evidence-source provider interface + registry and the + async, content-hash-deduplicated harvest that populates the case's `evidence` sub-collection with + per-item status. +- `dsar-export-bundle`: assembly + PAdES/SHA-256 signing of the export bundle, the one-time secure + download token, and regulator-dossier assembly. +- `dsar-redaction-write`: the field-level redaction write path recording before/after + ground to + the audit trail, distinct from erase-time pseudonymise. +- `dsar-denial-guard`: the `finaliseDenial` lifecycle-transition guard enforcing the mandatory + `regulatorReference` precondition. +- `dsar-retention-sweep`: the scheduled, legal-hold-aware retention sweep (dossier hard-delete + + evidence-PII scrub) enforcing the head's declared windows. +- `dsar-case-api`: the case-management REST controllers/routes and the case-level access-control + layer (auth posture + handler-scopes-own + officer override) over OR object RBAC. + +### Modified Capabilities + + +## Impact + +- **New code**: an `EvidenceSourceProvider` interface + registry and the harvest service; an + export-bundle service (assemble/sign/tokenise/dossier); a redaction-write service; the + `finaliseDenial` guard class; a retention-sweep `TimedJob`; case-management controller(s); a + case-level access-control check; new route entries in `appinfo/routes.php`. +- **Consumes (unchanged)**: `DataSubjectRequestService` (`assembleAccessExport`, `erase`), + `DataSubjectDeadline`, `RetentionService` (`hasActiveLegalHold`, legal-hold awareness), + `AuditTrailMapper` + hash chain, `ObjectEntity::setProcessingActivityId()`, `ObjectService` + (RBAC + multitenancy), the `AvgRetentionJob`/`TimedJob` scheduled-work pattern, and (reserved for + Phase 2 identity) `BsnFormat`. +- **Consumes (from `dsar-case-subsystem`)**: the extended `dataSubjectRequest` schema + register, + the `x-openregister-lifecycle` graph (incl. the `finaliseDenial` transition that references this + change's guard), the `denialGround` enum, the `evidence`/`redactions` sub-collection shapes, the + `retentionWindow`/`retainUntil` stamps, and the deadline-tracking notification rules. +- **APIs**: new authenticated case-management endpoints (create/transition/attach-evidence/ + generate-bundle/download/deny-finalise), each `@NoAdminRequired` with RBAC + case-level access + control in the service; no breaking change to the existing `/api/gdpr/*` or `/api/avg/*` routes. +- **No new schemas** — this change consumes the head's register; it adds no OR schema. +- **Downstream**: Phase 2 `dsar-policy-pack-and-seams` depends on both this change and the head; + pipelinq's `avg-consume-or-workflow` retirement is gated on the full chain. No app is migrated by + this change. diff --git a/openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md b/openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md new file mode 100644 index 0000000000..e46350af31 --- /dev/null +++ b/openspec/changes/dsar-case-engine/specs/dsar-case-api/spec.md @@ -0,0 +1,49 @@ +## ADDED Requirements + +### Requirement: Case-management REST endpoints with correct auth posture +OpenRegister SHALL expose case-management REST endpoints for data-subject-request cases: create a +case, run a lifecycle transition, attach/trigger evidence harvest, apply a redaction, generate the +signed export bundle, download the bundle, and draft/finalise a denial. Every endpoint MUST be an +authenticated steward endpoint declaring `@NoAdminRequired` (plus `@NoCSRFRequired` only where a +client cannot supply a CSRF token, such as the one-time download) and MUST NOT be `@PublicPage` +(ADR-005). Every controller method MUST be registered in `appinfo/routes.php` and be reachable — +no orphan methods and no route entry pointing at a non-existent method (ADR-016, ADR-029). All +reads and writes MUST flow through `ObjectService` under RBAC + multitenancy. + +#### Scenario: Every endpoint is authenticated and registered +- **WHEN** a case-management controller method is defined +- **THEN** it MUST declare an explicit auth annotation (`@NoAdminRequired`, never `@PublicPage`) +- **AND** it MUST have a matching entry in `appinfo/routes.php` targeting an existing method + +#### Scenario: Anonymous access is rejected +- **WHEN** an unauthenticated caller invokes any case-management endpoint +- **THEN** the request MUST be rejected before the case is read or written + +#### Scenario: Transition endpoint drives the declared lifecycle +- **WHEN** a handler invokes the transition endpoint with a declared transition (e.g. `assign`, `collectEvidence`, `draftDenial`, `finaliseDenial`) +- **THEN** the case status MUST change per the head's declared `x-openregister-lifecycle` +- **AND** the `finaliseDenial` transition MUST pass through the denial-finalise guard + +### Requirement: Case-level access control layered on object RBAC +OpenRegister SHALL enforce a case-level access-control check on the case-management endpoints, +layered on top of OpenRegister object RBAC (ADR-022, ADR-023): a handler MAY act on cases assigned +to them (handler-scopes-own), and a configured officer role MAY override across cases. The check +MUST NOT widen access beyond what object RBAC already grants — it only further restricts a +broadly-authorised user to their own cases unless they hold the officer role. The check MUST fail +closed: if the officer-role determination is unavailable, access MUST be denied rather than skipped +(CWE-863 / OWASP A01). + +#### Scenario: A handler acts only on their own cases +- **WHEN** a handler invokes a case-management endpoint on a case assigned to them +- **THEN** the action MUST be permitted (subject to object RBAC) +- **AND** the same handler invoking it on a case assigned to another handler, without the officer role, MUST be refused + +#### Scenario: An officer overrides across cases +- **WHEN** a caller holding the configured officer role invokes a case-management endpoint on a case not assigned to them +- **THEN** the action MUST be permitted (subject to object RBAC) + +#### Scenario: The access check fails closed +- **WHEN** the officer-role determination cannot be resolved for a caller +- **THEN** access MUST be denied rather than skipped + +@e2e A handler creates a case, transitions it, and attaches evidence on their own case (allowed); attempting the same on another handler's case is refused; an officer performs the action across cases; all endpoints reject an anonymous caller. diff --git a/openspec/changes/dsar-case-engine/specs/dsar-denial-guard/spec.md b/openspec/changes/dsar-case-engine/specs/dsar-denial-guard/spec.md new file mode 100644 index 0000000000..26654fe56e --- /dev/null +++ b/openspec/changes/dsar-case-engine/specs/dsar-denial-guard/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Denial-finalise lifecycle guard enforcing a mandatory regulator reference +OpenRegister SHALL provide the lifecycle-transition guard class that the `dsar-case-subsystem` +head's `finaliseDenial` transition references via its `requires` binding (ADR-031 §3). The guard +MUST refuse the `finaliseDenial` transition when the case's `regulatorReference` is empty, and MUST +permit it only when a `regulatorReference` is present. The guard MUST NOT be invoked for +`draftDenial`: drafting a denial (recording a `denialGround`) MUST remain possible without a +regulator reference. The guard MUST fail closed — if it cannot determine that the precondition is +satisfied, it MUST refuse the transition rather than allow it. + +#### Scenario: Finalise denial is blocked without a regulator reference +- **WHEN** a handler attempts `finaliseDenial` on a case whose `regulatorReference` is empty +- **THEN** the guard MUST refuse the transition +- **AND** the case MUST NOT enter the finalised-denial outcome + +#### Scenario: Finalise denial succeeds once the reference is recorded +- **WHEN** a handler records a `regulatorReference` and then attempts `finaliseDenial` +- **THEN** the guard MUST permit the transition +- **AND** the case MUST reach the `refused` final state with the `denialGround` and `regulatorReference` persisted + +#### Scenario: Drafting a denial is not gated +- **WHEN** a handler runs `draftDenial` recording a `denialGround` but no `regulatorReference` +- **THEN** the guard MUST NOT block the draft (the gate applies only at finalise) + +@e2e A steward drafts a denial with no regulator reference (allowed), attempts finalise (blocked by the guard), records the reference, then finalises (allowed) — the case ends refused with ground and reference persisted. diff --git a/openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md b/openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md new file mode 100644 index 0000000000..5335b8b6bb --- /dev/null +++ b/openspec/changes/dsar-case-engine/specs/dsar-evidence-collection/spec.md @@ -0,0 +1,45 @@ +## ADDED Requirements + +### Requirement: Pluggable evidence-source provider interface and registry +OpenRegister SHALL define an `EvidenceSourceProvider` interface and a registry through which a leaf +app registers evidence-harvest sources (e.g. OpenConnector-backed sources), so that the set of +sources is extensible without modifying OpenRegister core (ADR-019). Each provider SHALL identify +itself with a stable `sourceId` and SHALL harvest evidence items for a given data-subject-request +case. OpenRegister core MUST NOT hard-code the list of sources; a source that is not registered +MUST NOT contribute evidence. + +#### Scenario: A registered provider contributes evidence +- **WHEN** a leaf app registers an `EvidenceSourceProvider` and a harvest is triggered for a case +- **THEN** the provider's harvested items MUST be considered for that case +- **AND** a source that is not registered MUST NOT contribute any evidence item + +#### Scenario: Providers are discovered through the registry, not hard-coded +- **WHEN** the harvest service enumerates sources for a case +- **THEN** it MUST enumerate only the registered providers from the registry +- **AND** adding a new source MUST NOT require a change to OpenRegister core code + +### Requirement: Async content-hash-deduplicated evidence collection with per-item status +OpenRegister SHALL collect evidence asynchronously from the registered providers and record each +item onto the case's declared `evidence` sub-collection through `ObjectService` (RBAC + +multitenancy), never via a custom Entity/Mapper. Each stored evidence item MUST carry its +`sourceId`, a `contentHash`, and a per-item collection `status`. Collection MUST deduplicate by +`contentHash`: an item whose `contentHash` already exists on the case MUST NOT be appended a second +time, so re-running a harvest is idempotent. Each attach MUST be recorded in the case's immutable +hash-chained audit trail (`AuditTrailMapper`). + +#### Scenario: Harvested items are stored with source, hash, and status +- **WHEN** a harvest collects an item from a provider +- **THEN** the stored evidence item MUST carry its `sourceId`, its `contentHash`, and a per-item `status` +- **AND** the attach MUST be recorded in the case's immutable audit trail + +#### Scenario: Re-running a harvest does not duplicate evidence +- **WHEN** a harvest produces an item whose `contentHash` already exists on the case +- **THEN** the item MUST NOT be appended again +- **AND** the case's evidence sub-collection MUST contain exactly one item for that `contentHash` + +#### Scenario: A slow or failing source is visible and re-runnable +- **WHEN** a provider fails or has not yet returned during an async harvest +- **THEN** the affected evidence items MUST reflect a non-collected `status` on the case +- **AND** a subsequent harvest MUST be able to complete the collection without duplicating already-collected items + +@e2e A steward triggers evidence collection on a case, sees items attached with source/hash/status, and re-triggers collection without producing duplicate evidence. diff --git a/openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md b/openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md new file mode 100644 index 0000000000..83ad6b7c92 --- /dev/null +++ b/openspec/changes/dsar-case-engine/specs/dsar-export-bundle/spec.md @@ -0,0 +1,53 @@ +## ADDED Requirements + +### Requirement: Assemble and sign the export bundle +OpenRegister SHALL assemble a data-subject export bundle for a case by reusing +`DataSubjectRequestService::assembleAccessExport`, and SHALL NOT re-implement subject-data +discovery or assembly (ADR-011). The bundle SHALL be rendered as a **PDF disclosure document** and +SHALL carry a **SHA-256 content hash** so its integrity is verifiable. Signing is isolated behind a +swappable `PadesSigner` interface. **A PAdES-LTV digital signature is a deferred enhancement** (the +interim signer attaches the SHA-256 hash only) — the 2026-07-04 spike found no ready EUPL-compatible +LTV signer (tc-lib-pdf 8.65 discards the timestamp / stubs B-T; pyHanko is the leading candidate when +resumed). (A machine-readable art-20 *portability* payload — e.g. JSON/CSV with a JAdES/CAdES +signature — is likewise a deferred follow-up and out of scope here.) The assembly MUST run +through `ObjectService` under the caller's RBAC + multitenancy scope, and the bundle-generation +action MUST be recorded in the case's immutable audit trail pinned to the DSAR processing activity +(`ObjectEntity::setProcessingActivityId()`). + +#### Scenario: Bundle is assembled from the existing access-export primitive +- **WHEN** a steward generates the export bundle for a case +- **THEN** the bundle contents MUST be assembled via `DataSubjectRequestService::assembleAccessExport` +- **AND** the generation MUST be recorded in the case's immutable audit trail + +#### Scenario: Bundle is integrity-verifiable +- **WHEN** an export bundle is generated +- **THEN** the PDF bundle MUST carry a SHA-256 content hash via the `PadesSigner` seam +- **AND** altering the bundle bytes MUST invalidate the recorded hash +- **AND** a real PAdES-LTV signature MAY later replace the interim hash-only signer with no call-site change + +### Requirement: One-time secure download token +OpenRegister SHALL issue a single-use, time-boxed secure download token for a generated bundle. The +download endpoint MUST require the token and MUST be authenticated and case-scoped (never +`@PublicPage`). The token MUST be burned on the first successful download so it cannot be replayed; +a second attempt with the same token MUST be refused. + +#### Scenario: Token permits exactly one download +- **WHEN** a valid one-time token is presented to the download endpoint +- **THEN** the signed bundle MUST be returned +- **AND** a second request with the same token MUST be refused + +#### Scenario: Download requires authentication and case scope +- **WHEN** an unauthenticated caller, or a caller without access to the case, presents a token +- **THEN** the download MUST be refused + +### Requirement: Regulator dossier assembly +OpenRegister SHALL assemble a regulator dossier for a case from the case's evidence sub-collection, +its redaction records, and its audit trail, so the dossier reflects what was collected, what was +redacted (with grounds), and the case history. The dossier assembly MUST read through the same +RBAC-scoped case object and MUST NOT expose data outside the caller's authorisation. + +#### Scenario: Dossier reflects evidence, redactions, and history +- **WHEN** a regulator dossier is assembled for a case +- **THEN** it MUST include the case's collected evidence, its redaction records with grounds, and its audit-trail history + +@e2e A steward generates a signed bundle for a case, downloads it once via the one-time token (a replay is refused), and assembles a regulator dossier that reflects the case's evidence and redactions. diff --git a/openspec/changes/dsar-case-engine/specs/dsar-redaction-write/spec.md b/openspec/changes/dsar-case-engine/specs/dsar-redaction-write/spec.md new file mode 100644 index 0000000000..22cdbb689c --- /dev/null +++ b/openspec/changes/dsar-case-engine/specs/dsar-redaction-write/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Field-level redaction write path with before/after and ground +OpenRegister SHALL provide a write path that applies a field-level redaction to a data-subject +request case: given a target `field`, it SHALL record a `redactions` entry on the case carrying the +redacted `field`, its `before` value, its `after` value, and the redaction `ground`, writing +through `ObjectService` under RBAC + multitenancy. The redaction MUST be recorded in the case's +immutable hash-chained audit trail (`AuditTrailMapper`) pinned to the DSAR processing activity. + +#### Scenario: A redaction records before/after and ground +- **WHEN** a handler applies a redaction to a case field +- **THEN** the case MUST carry a `redactions` entry with the `field`, its `before` value, its `after` value, and the `ground` +- **AND** the redaction MUST be recorded in the case's immutable audit trail + +### Requirement: Redaction is distinct from erase-time pseudonymise +The redaction write path SHALL be distinct from the erase-time pseudonymise already performed by +`DataSubjectRequestService::erase(mode=pseudonymise)`. Applying a redaction MUST NOT invoke the +erase pseudonymise path, and a redaction entry MUST be distinguishable from an erase pseudonymise +record. Redaction is a pre-bundle field-level action recording its own before/after; it does not +replace or trigger statutory erasure. + +#### Scenario: Redaction does not trigger erase pseudonymise +- **WHEN** a handler applies a field-level redaction +- **THEN** the erase-time pseudonymise path MUST NOT be invoked +- **AND** the resulting redaction entry MUST be distinguishable from an erase pseudonymise record + +@e2e A steward redacts a field on a case, and the case shows a redactions entry with before/after and ground in the audit trail, without any statutory erasure being triggered. diff --git a/openspec/changes/dsar-case-engine/specs/dsar-retention-sweep/spec.md b/openspec/changes/dsar-case-engine/specs/dsar-retention-sweep/spec.md new file mode 100644 index 0000000000..d50fde38fe --- /dev/null +++ b/openspec/changes/dsar-case-engine/specs/dsar-retention-sweep/spec.md @@ -0,0 +1,38 @@ +## ADDED Requirements + +### Requirement: Scheduled retention sweep enforcing declared windows +OpenRegister SHALL provide a scheduled `TimedJob` that enforces the windowed retention the +`dsar-case-subsystem` head declares on the case (`retentionWindow` / `retainUntil` stamps). On each +run the sweep SHALL identify data-subject-request cases whose `retainUntil` has passed, hard-delete +the case dossier, and scrub the case's evidence PII by reusing +`DataSubjectRequestService::erase(mode=pseudonymise)` — it MUST NOT hand-roll a scrub (ADR-011). +The job SHALL follow the existing `AvgRetentionJob` / `OCP\BackgroundJob\TimedJob` pattern with an +`IAppConfig` enabled toggle and a dry-run toggle, and every destructive action MUST be recorded in +the immutable audit trail. + +#### Scenario: A dossier past its window is hard-deleted and its evidence scrubbed +- **WHEN** the sweep runs and a case's `retainUntil` has passed +- **THEN** the case dossier MUST be hard-deleted +- **AND** the case's evidence PII MUST be scrubbed via `DataSubjectRequestService::erase(mode=pseudonymise)` +- **AND** each action MUST be recorded in the immutable audit trail + +#### Scenario: Cases still within their window are untouched +- **WHEN** the sweep runs and a case's `retainUntil` has not yet passed +- **THEN** the case MUST NOT be hard-deleted or scrubbed + +#### Scenario: Dry-run reports without destroying +- **WHEN** the sweep runs with the dry-run toggle enabled +- **THEN** it MUST report the cases it would destroy without deleting or scrubbing any data + +### Requirement: Retention sweep is legal-hold aware +The retention sweep MUST consult `RetentionService::hasActiveLegalHold` (and +`validateNotImmutable`) before deleting or scrubbing any case, and MUST skip any case under an +active legal hold, leaving it intact. This mirrors the legal-hold guarantee the erase path already +honours. + +#### Scenario: A case under legal hold is skipped +- **WHEN** the sweep encounters a case past its `retainUntil` that is under an active legal hold +- **THEN** the case MUST be left intact (not deleted, not scrubbed) +- **AND** the skip MUST NOT advance any destruction on that case until the hold is released + +@e2e An operator runs the retention sweep in dry-run and sees which expired cases would be purged; a case under legal hold is reported as skipped, and enabling the sweep hard-deletes only the non-held expired dossiers. diff --git a/openspec/changes/dsar-case-engine/tasks.md b/openspec/changes/dsar-case-engine/tasks.md new file mode 100644 index 0000000000..96c9fbb93d --- /dev/null +++ b/openspec/changes/dsar-case-engine/tasks.md @@ -0,0 +1,64 @@ +# Tasks — dsar-case-engine (kind: code, depends_on: dsar-case-subsystem) + +Imperative successor to the declarative head. Consumes the head's `dataSubjectRequest` register +(case entity, lifecycle graph incl. the `finaliseDenial` `requires` binding, denial-ground enum, +evidence/redaction sub-collection shapes, retention windows). No new OR schema is added. + +## 1. Evidence collection + +- [x] 1.1 Add an `EvidenceSourceProvider` interface (identify via stable `sourceId`; harvest evidence items for a case) and a registry that leaf apps register providers into (ADR-019); OR core enumerates only registered providers. +- [x] 1.2 Add an async harvest service that collects from registered providers, dedups items by `contentHash`, writes each item (`sourceId`/`contentHash`/`status`) onto the case's declared `evidence` sub-collection via `ObjectService` (RBAC + multitenancy), and audits each attach through `AuditTrailMapper` pinned to the DSAR processing activity. + +## 2. Export bundle + +- [~] 2.1 Add an export-bundle service that assembles via `DataSubjectRequestService::assembleAccessExport` (no re-implementation, ADR-011); select + vendor a PAdES-LTV signing library (in scope, isolated behind the service as a swappable dependency), sign the bundle with PAdES-LTV, and attach a SHA-256 content hash; audit the generation. **PARTIAL (Phase-1a):** export-bundle service, PDF render, SHA-256 content hash and generation-audit are implemented; signing is isolated behind a swappable `PadesSigner` interface (constructor-injected) with a default `UnsignedPadesSigner` that attaches the hash and a `signed:false / pending PAdES-LTV library` state. **PAdES-LTV DEFERRED (spike outcome 2026-07-04):** the tc-lib-pdf spike was **No-Go** — 8.65.4 is LGPL-3/EUPL-clean with the API but mid-development (discards the RFC-3161 timestamp → B-T stubbed; legacy subfilter; no ESS signing-cert attr; DSS/VRI empty without B-T). **Accepted interim = SHA-256 hash-only** via `UnsignedPadesSigner` (already shipped). Real PAdES-LTV is a deferred follow-up; **leading candidate `pyHanko`** (MIT sidecar, real B-LTA) or a tc-lib-pdf re-spike once timestamp-embedding lands. No `composer require` made. The `PadesSigner` interface keeps the swap a config/adapter change (see `TODO(ADR-047 Phase-1b)` in `ExportBundleService` + the DI binding in `Application::register`). +- [x] 2.2 Add the one-time secure download: mint a single-use, time-boxed token; burn it on first successful download; refuse replay; keep the download authenticated + case-scoped (never `@PublicPage`). +- [x] 2.3 Add regulator-dossier assembly from the case's evidence, redaction records (with grounds), and audit-trail history, read through the RBAC-scoped case object. + +## 3. Redaction write path + +- [x] 3.1 Add a redaction-write service that applies a field-level redaction, records a `redactions` entry (`field`/`before`/`after`/`ground`) on the case via `ObjectService`, audits it, and stays distinct from `DataSubjectRequestService::erase(mode=pseudonymise)` (MUST NOT invoke the erase path). + +## 4. Denial finalise guard + +- [x] 4.1 Add the `finaliseDenial` lifecycle guard class (the FQCN the head's transition references via `requires`, ADR-031 §3): refuse finalise when `regulatorReference` is empty, permit it when present, never gate `draftDenial`, and fail closed. + +## 5. Retention sweep + +- [x] 5.1 Add a retention-sweep `TimedJob` (mirroring `AvgRetentionJob`/`OCP\BackgroundJob\TimedJob` with `IAppConfig` enabled + dry-run toggles) that hard-deletes cases past `retainUntil`, scrubs evidence PII via `DataSubjectRequestService::erase(mode=pseudonymise)`, and audits each destructive action. +- [x] 5.2 Make the sweep legal-hold aware: consult `RetentionService::hasActiveLegalHold` / `validateNotImmutable` and skip any held case intact; register the job in `appinfo/info.xml` background-jobs. + +## 6. Controllers, routes, access control + +- [x] 6.1 Add case-management controller method(s): create case, run lifecycle transition (incl. `draftDenial`/`finaliseDenial` through the guard), attach/trigger evidence, apply redaction, generate bundle, download bundle — delegating to the services above; all reads/writes through `ObjectService`. +- [x] 6.2 Add the case-level access-control check layered on object RBAC (ADR-023): handler-scopes-own + configured officer-role override, never widening beyond object RBAC, failing closed when the officer role cannot be resolved. +- [x] 6.3 Register every controller method in `appinfo/routes.php` under the `/api/gdpr/cases/...` shape with the correct auth annotation (`@NoAdminRequired`, `@NoCSRFRequired` only on the download; never `@PublicPage`); verify route↔method reachability — no orphan methods, no orphan routes (ADR-016, ADR-029). + +## 7. Tests + +- [x] 7.1 Add PHPUnit tests (CI way: php:8.3-cli + OCP stubs, no live NC) against `ObjectService` / provider / `RetentionService` / `DataSubjectRequestService` doubles for: evidence dedup + per-item status, bundle assemble/sign + one-time-token single-use, redaction distinct-from-erase, the denial guard (blocked/allowed/draft-not-gated/fail-closed), and the legal-hold-aware sweep (dry-run + skip-held). +- [x] 7.2 Add controller tests asserting auth annotations, anonymous rejection, RBAC + case-level access control (handler-scopes-own, officer override, fail-closed), and route↔method reachability. + +## 8. Verification + +- [x] 8.1 Run `composer check:strict` (PHPCS, PHPMD, Psalm, PHPStan) and the Hydra mechanical gates (spdx-headers, route-auth, no-admin-idor, unsafe-auth-resolver, route-reachability, redundant-controller); fix any pre-existing issues touched. +- [x] 8.2 Run `openspec validate --change dsar-case-engine --strict`; resolve any errors. + +## Acceptance Criteria + +- Evidence collection harvests only from registered providers, dedups by `contentHash` (re-runs are idempotent), and records `sourceId`/`contentHash`/`status` per item with an audit entry. +- The export bundle is assembled via the existing access-export primitive, PAdES-LTV signed + SHA-256 hashed, downloadable exactly once via a burned one-time token, and a regulator dossier reflects evidence + redactions + history. +- A field-level redaction records before/after + ground to the audit trail and never triggers erase-time pseudonymise. +- `finaliseDenial` is refused without a `regulatorReference`, allowed with one, `draftDenial` is never gated, and the guard fails closed. +- The retention sweep hard-deletes only expired, non-legal-held cases, scrubs their evidence PII via `erase(mode=pseudonymise)`, supports dry-run, and audits every destructive action. +- Every case-management endpoint is `@NoAdminRequired` (never `@PublicPage`), registered + reachable in `appinfo/routes.php`, rejects anonymous callers, and enforces handler-scopes-own + officer-override case-level access control that fails closed. +- No new OR schema is added; the change consumes the `dsar-case-subsystem` register. + +## Quality Checklist + +- Reused `DataSubjectRequestService::assembleAccessExport` / `erase`, `DataSubjectDeadline`, `RetentionService` legal-hold, `AuditTrailMapper` + processing-activity pinning, and the `AvgRetentionJob`/`TimedJob` pattern rather than duplicating them (ADR-011, ADR-022). +- Every imperative item is a genuine ADR-031 exception (external integration / document generation / scheduled bulk work / lifecycle guard) — no declarative construct (lifecycle/calculation/aggregation/notification) reimplemented in code (gate-18/gate-31). +- Auth posture explicit on every route (ADR-005/016/029): `@NoAdminRequired`, `@NoCSRFRequired` only where justified, no `@PublicPage`; no-admin-IDOR and unsafe-auth-resolver gates clean; case-level access control fails closed (CWE-863). +- SPDX + `@license`/`@copyright` docblock headers on every new PHP file (EUPL-1.2). +- Contract coverage: the case-management endpoints exercised via Newman (API/contract), not counted as UI-green; behavioural spec scenarios carry `@e2e` and are referenced by Playwright e2e (or a reason-bearing `@e2e exclude`). +- Any fixture ids/tokens use safe placeholders only (nil UUID `00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`) — no realistic-looking secrets/UUIDs (gitleaks). diff --git a/openspec/changes/dsar-case-subsystem/.openspec.yaml b/openspec/changes/dsar-case-subsystem/.openspec.yaml new file mode 100644 index 0000000000..43e65ca6e6 --- /dev/null +++ b/openspec/changes/dsar-case-subsystem/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-03 diff --git a/openspec/changes/dsar-case-subsystem/design.md b/openspec/changes/dsar-case-subsystem/design.md new file mode 100644 index 0000000000..8f00eb78cc --- /dev/null +++ b/openspec/changes/dsar-case-subsystem/design.md @@ -0,0 +1,217 @@ +## Context + +OpenRegister ships a **stateless** DSAR engine today. `DataSubjectRequestService` +(`lib/Service/Gdpr/DataSubjectRequestService.php`) fulfils art-15/16/17/18/20/21 on demand +(discover, access-export, rectify, erase, restrict, object) under RBAC + tenant scoping, pinning +every write to the DSAR processing activity so the immutable hash-chained `AuditTrailMapper` +records it (`ObjectEntity::setProcessingActivityId()`). `DataSubjectDeadline` +(`lib/Service/Gdpr/DataSubjectDeadline.php`) is the pure art-12(3) maths +(`computeDueAt`/`extend`/`isOverdue`/`daysRemaining`). `lib/Settings/data_subject_request_register.json` +already declares a `dataSubjectRequest` schema with an `x-openregister-lifecycle` +(`received → verifying → in-progress → fulfilled/refused/closed`) and the art-12 fields +(`receivedAt`/`dueAt`/`extendedUntil`). Controllers `DataSubjectRequestController` and +`DsarController` expose the fulfilment surface. + +What is absent is the **stateful case**: the request is a first-class tracked object that a +handler owns, that escalates as its deadline nears, that collects evidence, that can be denied +under a recorded ground with a regulator reference, that redacts fields with a before/after audit, +that produces a signed export bundle, and that is retained under a configured window. ADR-047 +assigns this DSAR *case workflow* to OpenRegister as a register/schema-scoped capability. Per +ADR-031, the case entity, its lifecycle, its deadline-tracking aggregations, and its +reminder/escalation/breach notifications are **schema-declarative config**; only document +generation, external providers, and scheduled bulk work stay imperative. + +This change is the **declarative head** of the Phase-1 chain: it extends the register JSON only. +The imperative surface is the successor `dsar-case-engine` change. + +## Goals / Non-Goals + +**Goals:** +- Extend the persisted `dataSubjectRequest` case entity in the register JSON with the + case-management fields it lacks (handler, `closedAt`, retention stamps, DPIA flag, denial fields, + evidence + redaction sub-collections), each with a human-friendly `title`/`description` (ADR-011). +- Extend `x-openregister-lifecycle` into a configurable N-state case graph with the case + transitions and a mandatory `regulatorReference` guard before `finaliseDenial`. +- Declare deadline **tracking** on top of the existing art-12 maths via + `x-openregister-calculations` (`daysRemaining`, `isOverdue`, `escalationTier`), + `x-openregister-aggregations` (open/overdue/breached counts), and + `x-openregister-notifications` (advance-reminder, escalation, breach), with fire-once idempotent + semantics from the notification engine. +- Declare the denial-ground enum (generic keys) and the redaction sub-collection shape. +- Declare windowed retention stamps (`retentionWindow`/`retainUntil`/`purgedAt`) as config. +- Ship realistic seed case objects (municipality, consultancy, travel agency) with safe + placeholders. + +**Non-Goals:** +- No PHP service classes, controllers, or routes in this change — that is `dsar-case-engine` + (kind:code, `depends_on` this change): pluggable evidence providers, bundle signing/PAdES, + one-time secure download, the redaction-audit write path, regulator dossier assembly, the + retention sweep job, and the case-level access-control layer. +- No jurisdiction policy-pack contract and no identity-verify (BSN/BRP) / regulator-escalate (AP) + integration seams — that is Phase 2 `dsar-policy-pack-and-seams`, referenced only as `depends_on` + successor context. +- No change to the existing stateless fulfilment behaviour — it is reused verbatim. + +## Decisions + +### Declarative-vs-imperative decision (ADR-031) + +Default is declarative (schema register); each imperative exception is justified below. This +change authors **only the declarative column**; the imperative column is scoped into +`dsar-case-engine` and listed here so the reviewer sees the whole picture. + +| Behaviour | Chosen path | Rationale | +|---|---|---| +| **Case lifecycle / state machine** (N-state, handler assignment, denial-draft/finalise, redact/bundle/retain transitions) | **Declarative** — `x-openregister-lifecycle` on `dataSubjectRequest` | A state machine over one object's `status` field is the canonical `x-openregister-lifecycle` fit. Gives audit-trailed transitions, RBAC per state, replayability, CloudEvents, MCP discovery for free. Zero PHP. | +| **Deadline-tracking aggregation** (open/overdue/breached counts; `daysRemaining`/`isOverdue`/`escalationTier` derived fields) | **Declarative** — `x-openregister-aggregations` + `x-openregister-calculations` referencing `dueAt`/`extendedUntil` | The art-12 arithmetic already exists in `DataSubjectDeadline`; *tracking* is derived state + cross-object counts, exactly what calculations/aggregations materialise. No per-app aggregation service (ADR-031 anti-pattern). | +| **Breach / reminder / escalation notifications** | **Declarative** — `x-openregister-notifications` (canonical ADR-031 dialect; `scheduled` + `threshold`/`calculatedChange` triggers) | Notification-on-condition is the notification dialect's job; the engine's fire-once-per-condition semantics give the "idempotent deadline-event audit" the ADR-047 table asks for. Hand-rolling a `NotificationService` is a gate-18 fail. | +| **Retention windows** (which window, `retainUntil`/`purgedAt` stamps) | **Declarative (config)** for the window selection + stamps | Windows are config values on the case object; the stamps are declared fields. Only the *sweep* is imperative. | +| **Retention sweep** (hard-delete dossier + scrub evidence PII on a schedule) | **Imperative — deferred to `dsar-case-engine`** | Scheduled bulk work over an object queue with side effects (delete/scrub) is the ADR-031 exception: use a `ScheduledWorkflow`/job, not a declarative construct. Reuses `RetentionService` legal-hold awareness. | +| **Evidence dedup** (pluggable source providers, dedup by content hash, per-item status) | **Imperative — deferred to `dsar-case-engine`** | External-source harvest via provider adapters is exactly ADR-003/ADR-031 "external API integration"; the schema engine can't reach outside systems. The *evidence sub-collection shape* is declared here; the harvesting is code there. | +| **Denial gate** (config-driven ground enum + mandatory regulator-reference before finalise) | **Declarative** — enum property + `x-openregister-lifecycle` transition guard `requires` | The enum is schema config; the "regulatorReference must be set" precondition is a short lifecycle guard (`requires: FQCN`) — the legitimate PHP seam ADR-031 §3 allows, authored in `dsar-case-engine` and *referenced* by the declarative transition here. | +| **Bundle signing** (assemble + PAdES sign + one-time secure download + regulator dossier) | **Imperative — deferred to `dsar-case-engine`** | Document generation / PAdES signing / rendered output is the ADR-031 document-generation exception; the schema engine has no opinion on rendered bytes. | +| **Redaction audit** (before/after + ground, distinct from erase pseudonymise) | **Split** — sub-collection *shape* declarative here; *write path* imperative in `dsar-case-engine` | The redaction record is a declared field; capturing before/after at redaction time is a write operation that belongs to the engine change. | + +### Reuse before build (ADR-011) + +Searched `lib/Formats/`, `lib/Service/`, `lib/Service/Gdpr/`, `lib/Handler/` before proposing +anything. Reuse, do not duplicate: + +- **`DataSubjectDeadline`** (`computeDueAt`/`extend`/`isOverdue`/`daysRemaining`) — the art-12 + maths the `escalationTier`/`daysRemaining` calculations reference. No new date helper. +- **`DataSubjectRequestService`** — the erase/rectify/restrict/object fulfilment the case + transitions drive; the case does not re-implement fulfilment. +- **`AuditTrailMapper` + hash chain + `ObjectEntity::setProcessingActivityId()`** — every case + transition and field change is audited through the existing immutable trail; no parallel audit. +- **`RetentionService`** (`hasActiveLegalHold`/`validateNotImmutable`) — the retention sweep + (engine change) reuses legal-hold awareness; erase already honours it. +- **`BsnFormat`** (`lib/Formats/BsnFormat.php`) — reserved for Phase-2 identity verification; not + used here, noted so the Phase-2 author reuses it. +- **`DataSubjectRequestController` / `DsarController`** — the existing fulfilment routes; the + case-management endpoints (engine change) extend the surface rather than replace it. + +### Object / schema service patterns (ADR-001, ADR-003) + +All case objects are OR objects under the `data-subject-requests` register / `dataSubjectRequest` +schema. Reads/writes flow through `ObjectService` (RBAC + multitenancy). `SchemaService` / +`RegisterService` own the schema definition loaded from the register JSON. No custom +Entity/Mapper for the case (ADR-001). The lifecycle/aggregation/calculation/notification engines +consume the `x-openregister-*` blocks folded into the schema `configuration` at import — the same +path `decidesk_register.json` uses. + +### Lifecycle graph shape + +The existing graph is preserved (initial `received`; final `fulfilled`/`refused`/`closed`) and +extended with case-management transitions layered on the intermediate states: +`assign` (set handler), `collectEvidence`, `draftDenial` → `finaliseDenial` (guarded by +`regulatorReference` presence), `redact`, `bundle`, `retain`. The state *set* is register config +so a jurisdiction/tenant can add or rename states in Phase-2 policy data without a code change. + +## Risks / Trade-offs + +- **[Declarative head with no runnable surface]** — this change alone extends the schema but the + imperative behaviours (evidence harvest, signing, sweep, redaction write, controllers) don't + exist until `dsar-case-engine`. Mitigation: the two changes are a chain; `dsar-case-engine` + `depends_on` this and lands the surface. The declarative case is still exercisable via OR's + existing object/lifecycle/aggregation APIs the moment it imports. +- **[Lifecycle-guard reference to a not-yet-authored FQCN]** — the `finaliseDenial` guard + references a PHP guard class delivered by `dsar-case-engine`. Mitigation: declare the transition + with the `requires` reference; the guard class is a task in the engine change; until it lands the + transition is declared but the register import tolerates a pending guard the same way decidesk's + `MeetingTransitionGuard` reference is resolved at runtime. +- **[Notification fire-once semantics must be idempotent]** — advance-reminder/escalation/breach + must not re-fire each scheduler tick. Mitigation: rely on the ADR-031 engine's + fire-once-per-condition + user-override preference model rather than a hand-rolled idempotency + key (the legacy `idempotencyKey` dialect is obsolete/rejected by gate-18). +- **[Seed data leaking realistic PII]** — seed cases carry subject identifiers. Mitigation: use + obvious placeholder emails/ids and the nil UUID; never realistic-looking secrets or UUIDs + (gitleaks-flagged). +- **[Schema field addition is a migration]** — adding required/typed fields to a live schema. + Mitigation: all new fields are **optional** (non-breaking per ADR-011 versioning); no existing + required field is removed/renamed, so no repair-step migration is needed — the register import + UNION-merges additively. + +## Seed Data + +Realistic `dataSubjectRequest` case objects for the `data-subject-requests` register / +`dataSubjectRequest` schema, using general org data and **safe placeholders** (nil UUID +`00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`, ``). Seeded via the OR +seed path (schemas gain fields → a seed task is required, ADR-001). + +**Case 1 — Municipality (access request, on track):** +```json +{ + "id": "00000000-0000-0000-0000-000000000000", + "subjectId": "j.jansen@example.org", + "subjectType": "email", + "type": "access", + "status": "in-progress", + "handler": "handler-gemeente", + "receivedAt": "2026-06-01T09:00:00+00:00", + "dueAt": "2026-07-01T09:00:00+00:00", + "closedAt": null, + "dpiaRequired": false, + "retentionWindow": "standard", + "retainUntil": "2027-07-01T00:00:00+00:00", + "evidence": [ + { "sourceId": "or-objects", "contentHash": "sha256:PLACEHOLDER_HASH", "status": "collected" } + ], + "notes": "Inwoner vraagt inzage in geregistreerde persoonsgegevens." +} +``` + +**Case 2 — Consultancy (erasure, deadline extended):** +```json +{ + "id": "00000000-0000-0000-0000-000000000000", + "subjectId": "contact@example-consultancy.test", + "subjectType": "email", + "type": "erasure", + "status": "verifying", + "handler": "handler-privacy", + "receivedAt": "2026-05-20T12:00:00+00:00", + "dueAt": "2026-06-20T12:00:00+00:00", + "extendedUntil": "2026-08-20T12:00:00+00:00", + "extensionReason": "Complex cross-register discovery; single art-12(3) extension applied.", + "dpiaRequired": true, + "retentionWindow": "extended" +} +``` + +**Case 3 — Travel agency (objection, denied with regulator reference):** +```json +{ + "id": "00000000-0000-0000-0000-000000000000", + "subjectId": "traveler@example-travel.test", + "subjectType": "email", + "type": "objection", + "status": "refused", + "handler": "handler-dpo", + "receivedAt": "2026-04-10T08:30:00+00:00", + "dueAt": "2026-05-10T08:30:00+00:00", + "closedAt": "2026-05-08T16:00:00+00:00", + "denialGround": "overriding-legitimate-grounds", + "regulatorReference": "REG-REF-PLACEHOLDER-0001", + "outcome": "Objection refused; compelling legitimate grounds documented; regulator reference recorded.", + "redactions": [ + { "field": "notes", "before": "[redacted-source]", "after": "[erased]", "ground": "third-party-data" } + ] +} +``` + +## Migration Plan + +Additive only. All new properties are **optional** (ADR-011: adding optional properties is +non-breaking); no property is removed or renamed, so no repair-step migration is required. The +register import UNION-merges the extended schema definition. Rollback is reverting the register +JSON. The seed objects are illustrative and can be re-seeded or removed independently. + +## Open Questions + +- Exact escalation-tier boundaries (e.g. reminder at T-7d, escalation at T-2d, breach at T+0) — + provisional in this change; final thresholds are candidate Phase-2 policy-pack values. +- Whether the denial-ground enum keys stay generic here and gain jurisdiction wording only in + Phase-2 policy data (recommended) vs a fuller generic set now. +- Whether the `finaliseDenial` guard is a lifecycle `requires` FQCN (recommended, ADR-031 §3) or a + purely declarative "required field before transition" the lifecycle engine can express natively — + depends on current `x-openregister-lifecycle` guard expressiveness; deferred to the engine change. diff --git a/openspec/changes/dsar-case-subsystem/proposal.md b/openspec/changes/dsar-case-subsystem/proposal.md new file mode 100644 index 0000000000..df1df81646 --- /dev/null +++ b/openspec/changes/dsar-case-subsystem/proposal.md @@ -0,0 +1,103 @@ +--- +kind: config +depends_on: [] +--- + +## Why + +OpenRegister already fulfils data-subject rights **statelessly** — `DataSubjectRequestService` +(art-15/16/17/18/20/21 + `computeDueAt`/`extend`/`isOverdue`), the pure `DataSubjectDeadline` +art-12(3) maths, the hash-chained `AuditTrailMapper`, and a `dataSubjectRequest` schema with an +`x-openregister-lifecycle` already shipped in `lib/Settings/data_subject_request_register.json`. +What is missing is a persisted, **stateful case** on top of that engine: a tracked request with a +configurable status graph, handler assignment, deadline *tracking* (reminder/escalation/breach — +not just the art-12 arithmetic), evidence, denial, redaction audit, a signed export bundle, and +windowed retention. ADR-047 rules that this DSAR **case workflow** is a register/schema-scoped +OpenRegister capability, not a per-app rebuild — pipelinq is the pilot that today carries its own +9-state `avgVerzoek` workflow that this subsumes. + +This change is the **head of the ADR-047 Phase-1 delivery chain**. Per ADR-031 the case entity, +its N-state lifecycle, deadline-tracking aggregations, and reminder/escalation/breach +notifications are **declarative schema-register config** (`x-openregister-{lifecycle, +aggregations, calculations, notifications}` on `dataSubjectRequest`), NOT new service classes. It +ships that declarative slice (`kind: config`) and hands the genuinely-imperative bits (pluggable +evidence providers, bundle signing/PAdES, one-time secure download, redaction-audit write path, +regulator dossier assembly, controllers) to the successor `dsar-case-engine` change, which lists +this change in its `depends_on`. This split keeps the head-of-chain change a single declarative +shape rather than a `mixed`-shaped anti-pattern (see DEFERRED_QUESTIONS — this split decision is +explicitly flagged for the human). + +## What Changes + +- **Extend the persisted `dataSubjectRequest` case entity** in + `lib/Settings/data_subject_request_register.json`: add the case-management fields the current + fulfilment-only schema lacks — `handler` (assignment), `receivedAt`/`dueAt`/`closedAt` case + timestamps (dueAt already present; add `closedAt`), retention stamps + (`retainUntil`/`retentionWindow`/`purgedAt`), a `dpiaRequired` complexity flag, denial-workflow + fields (`denialGround`/`regulatorReference`), and evidence/redaction sub-collections. Every + property carries a human-friendly `title` + `description` (ADR-011). +- **Make the status graph configurable and N-state** by extending the existing + `x-openregister-lifecycle` block: add case-management states/transitions + (`assign`, `collectEvidence`, `draftDenial`, `finaliseDenial`, `redact`, `bundle`, `retain`) + layered on the current `received → verifying → in-progress → fulfilled/refused/closed` graph, + keeping the initial/final semantics. The state set is register config, not hard-coded PHP. +- **Declare deadline TRACKING as aggregations + notifications**, on top of the existing art-12 + maths: `x-openregister-calculations` for `daysRemaining`/`isOverdue`/`escalationTier` + (referencing `dueAt`/`extendedUntil`), `x-openregister-aggregations` for open/overdue/breached + case counts, and `x-openregister-notifications` for advance-reminder, escalation, and + breach-detection rules (the canonical ADR-031 notification dialect; `scheduled` + `threshold` + triggers). Idempotent deadline-event audit is inherent to the notification engine's + fire-once-per-condition semantics. +- **Declare the denial gate and redaction-audit shape** as schema config: `denialGround` is a + config-driven enum (generic ground keys; jurisdiction wording is Phase 2 policy-pack data), and + a lifecycle transition guard requires `regulatorReference` to be set before `finaliseDenial`. + Redaction entries (before/after + ground) are a declared sub-collection distinct from the + erase-time pseudonymise the engine already does. +- **Windowed retention as config**: `retentionWindow` selects a window; the retention stamps are + written declaratively; the actual hard-delete/scrub sweep is deferred to `dsar-case-engine` + (imperative scheduled work, ADR-031 external-orchestrator exception). +- **Seed data**: realistic `dataSubjectRequest` example objects (municipality, consultancy, + travel agency) using safe placeholder ids, added as a seed task since the schema gains fields. + +**Explicitly out of scope — successor `dsar-case-engine` change (kind:code, `depends_on` this):** +pluggable evidence source providers (dedup by content hash, per-item status); signed export +bundle assembly + PAdES signing + one-time secure download; the redaction-audit *write* path; +regulator dossier assembly; the retention sweep job; the case-level access-control layer and +controllers/routes. **Out of scope — Phase 2 `dsar-policy-pack-and-seams`:** the jurisdiction +policy-pack config contract, and the identity-verify (BSN/BRP) + regulator-escalate (AP) +integration seams. Phase 2 is `depends_on` successor context only; not specced here. + +## Capabilities + +### New Capabilities +- `dsar-case-entity`: the persisted, stateful `dataSubjectRequest` case entity — handler + assignment, case timestamps, retention stamps, DPIA flag, denial fields, evidence/redaction + sub-collections — declared as schema properties on the existing register. +- `dsar-case-lifecycle`: the configurable N-state case status graph and its transition guards + (including the mandatory `regulatorReference` gate before `finaliseDenial`), declared via + `x-openregister-lifecycle` on `dataSubjectRequest`. +- `dsar-deadline-tracking`: advance-reminder, escalation, and breach-detection tracking on top of + the existing art-12 deadline maths, declared via `x-openregister-{calculations,aggregations, + notifications}` — with fire-once (idempotent) deadline-event semantics. + +### Modified Capabilities +- `gdpr-data-subject-rights`: the existing stateless capability gains a **stateful case** overlay. + Its requirements do not change; this change adds new requirements in the three new capabilities + above and references the stateless engine as reused. (No delta spec — behaviour is additive, no + existing requirement is altered.) + +## Impact + +- **Config (this change)**: `lib/Settings/data_subject_request_register.json` — extended schema + properties, `x-openregister-lifecycle`, `-calculations`, `-aggregations`, `-notifications` + blocks on `dataSubjectRequest`; a seed file/task for the example case objects. +- **Reused, unchanged**: `lib/Service/Gdpr/DataSubjectRequestService.php`, + `lib/Service/Gdpr/DataSubjectDeadline.php`, `AuditTrailMapper` + hash chain, + `ObjectEntity::setProcessingActivityId()`, `lib/Controller/DataSubjectRequestController.php` / + `DsarController.php`, `RetentionService` legal-hold awareness, `BsnFormat` (Phase-2 identity). +- **APIs**: no new routes in this change — the declarative case is served by OR's existing + object/lifecycle/aggregation APIs. Controllers/routes for the imperative surface land in + `dsar-case-engine`. +- **Downstream**: `dsar-case-engine` (kind:code) depends on this; Phase 2 + `dsar-policy-pack-and-seams` depends on both; pipelinq's `avg-consume-or-workflow` retirement is + gated on the full chain. No app is migrated by this change. diff --git a/openspec/changes/dsar-case-subsystem/specs/dsar-case-entity/spec.md b/openspec/changes/dsar-case-subsystem/specs/dsar-case-entity/spec.md new file mode 100644 index 0000000000..a165e60d4b --- /dev/null +++ b/openspec/changes/dsar-case-subsystem/specs/dsar-case-entity/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: Persisted data-subject-request case entity +OpenRegister SHALL persist a data-subject request as a stateful **case** object of the +`dataSubjectRequest` schema in the `data-subject-requests` register, extending the existing +fulfilment-only schema with case-management fields. The case SHALL carry, in addition to the +existing `subjectId`/`subjectType`/`type`/`status`/`receivedAt`/`dueAt`/`extendedUntil` fields: a +`handler` (the assigned handler identifier), a `closedAt` timestamp, a `dpiaRequired` boolean +complexity flag, retention stamps (`retentionWindow`, `retainUntil`, `purgedAt`), denial fields +(`denialGround`, `regulatorReference`), and evidence + redaction sub-collections. Every added +property MUST declare a human-friendly English `title` and a `description` (ADR-011). All added +properties MUST be optional so the extension is non-breaking. The case MUST be read and written +through `ObjectService` under RBAC and multitenancy scoping; it MUST NOT introduce a custom +Entity/Mapper (ADR-001). + +#### Scenario: Case carries handler assignment and case timestamps +- **WHEN** a data-subject-request case is created and assigned to a handler +- **THEN** the persisted object MUST hold the `handler`, `receivedAt`, `dueAt`, and (once closed) `closedAt` values +- **AND** the object MUST be retrievable through `ObjectService` scoped to the caller's RBAC and tenant + +#### Scenario: DPIA flag is recorded on the case +- **WHEN** a case is flagged as requiring a DPIA +- **THEN** the persisted object MUST carry `dpiaRequired = true` + +#### Scenario: Added properties are optional and non-breaking +- **WHEN** the extended schema is imported over the existing `dataSubjectRequest` schema +- **THEN** no existing required property MUST be removed or renamed +- **AND** every added case-management property MUST be optional so pre-existing objects remain valid + +#### Scenario: Every added property is human-labelled +- **WHEN** the extended schema is rendered in a create/edit form or a list +- **THEN** each added property MUST show its `title` as the field label and its `description` as help text (no raw camelCase key leaks) + +### Requirement: Denial-workflow fields +The case SHALL carry a config-driven `denialGround` enum property and a `regulatorReference` +string property that together record a denial. `denialGround` SHALL be an enumeration of generic +ground keys (jurisdiction-specific wording is Phase-2 policy-pack data, not embedded here). The +`regulatorReference` SHALL be recordable only as part of the denial workflow. These fields carry +no behaviour on their own; the mandatory-gate behaviour is specified in the case-lifecycle +capability. + +#### Scenario: A denied case records ground and regulator reference +- **WHEN** a case is refused under a documented ground +- **THEN** the persisted object MUST carry a `denialGround` value drawn from the configured enum and a `regulatorReference` value + +#### Scenario: Denial ground is a config-driven enum, not hard-coded wording +- **WHEN** the `denialGround` property is defined in the schema +- **THEN** its allowed values MUST be generic ground keys declared as schema config +- **AND** no jurisdiction-specific statutory wording MUST be baked into the schema + +### Requirement: Evidence and redaction sub-collections declared on the case +The case SHALL declare an `evidence` sub-collection and a `redactions` sub-collection as schema +properties. Each evidence item SHALL carry at least a `sourceId`, a `contentHash` (used downstream +for deduplication), and a per-item `status`. Each redaction entry SHALL carry the redacted +`field`, a `before` value, an `after` value, and the `ground`. This change declares only the +**shape** of these sub-collections; the imperative evidence-harvest and redaction-write behaviour +is delivered by the successor `dsar-case-engine` change. The redaction entry SHALL be distinct +from the erase-time pseudonymise already performed by `DataSubjectRequestService`. + +#### Scenario: Evidence item shape supports content-hash dedup +- **WHEN** an evidence item is stored on a case +- **THEN** the item MUST carry a `sourceId`, a `contentHash`, and a `status` + +#### Scenario: Redaction entry records before/after and ground +- **WHEN** a redaction entry is stored on a case +- **THEN** the entry MUST carry the `field`, its `before` value, its `after` value, and the `ground` +- **AND** the redaction entry MUST be distinct from an erase-time pseudonymise record diff --git a/openspec/changes/dsar-case-subsystem/specs/dsar-case-lifecycle/spec.md b/openspec/changes/dsar-case-subsystem/specs/dsar-case-lifecycle/spec.md new file mode 100644 index 0000000000..32e3aa0d32 --- /dev/null +++ b/openspec/changes/dsar-case-subsystem/specs/dsar-case-lifecycle/spec.md @@ -0,0 +1,50 @@ +## ADDED Requirements + +### Requirement: Configurable N-state case status graph +OpenRegister SHALL express the data-subject-request case status graph as declarative +`x-openregister-lifecycle` config on the `dataSubjectRequest` schema, not as a hard-coded PHP +state machine. The graph SHALL preserve the existing initial state (`received`) and final states +(`fulfilled`, `refused`, `closed`) and SHALL extend the intermediate states with the +case-management transitions: `assign` (handler assignment), `collectEvidence`, `draftDenial`, +`finaliseDenial`, `redact`, `bundle`, and `retain`. The state set and its transitions SHALL be +register configuration so a jurisdiction or tenant can add or rename states without a code change. +Every transition MUST be audited through the existing immutable hash-chained audit trail +(`AuditTrailMapper`); no parallel transition log MUST be introduced (ADR-022, ADR-031). + +#### Scenario: Case advances through declared transitions +- **WHEN** a case in an intermediate state is advanced by a declared transition (e.g. `assign` then `collectEvidence`) +- **THEN** the case `status` MUST change to the transition's target state +- **AND** the transition MUST be recorded in the object's immutable audit trail + +#### Scenario: Initial and final semantics are preserved +- **WHEN** the extended lifecycle is imported +- **THEN** the initial state MUST remain `received` and the final states MUST remain `fulfilled`, `refused`, and `closed` + +#### Scenario: State set is config, not hard-coded +- **WHEN** an operator adds or renames a case state in the register configuration +- **THEN** the new state graph MUST take effect without any PHP code change + +@e2e A steward opens a received case, assigns a handler, and advances it through collectEvidence, and the status + audit trail reflect each transition. + +### Requirement: Mandatory regulator-reference gate before denial finalise +The lifecycle SHALL guard the `finaliseDenial` transition so that a case MAY be moved into a +finalised-denial outcome ONLY when a `regulatorReference` is present on the case. When +`regulatorReference` is empty, the `finaliseDenial` transition MUST be refused. The `draftDenial` +transition (recording a proposed `denialGround`) MUST NOT require the regulator reference — the +gate applies only at finalise. The guard MAY be expressed as a lifecycle `requires` reference to a +short PHP guard (delivered by `dsar-case-engine`, ADR-031 §3) or, where the lifecycle engine can +express a required-field precondition natively, as declarative config. + +#### Scenario: Finalise denial is blocked without a regulator reference +- **WHEN** a handler attempts `finaliseDenial` on a case whose `regulatorReference` is empty +- **THEN** the transition MUST be refused and the case MUST NOT enter the finalised-denial outcome + +#### Scenario: Finalise denial succeeds once the reference is recorded +- **WHEN** a handler records a `regulatorReference` and then attempts `finaliseDenial` +- **THEN** the transition MUST succeed and the case MUST reach the `refused` final state with the ground and reference persisted + +#### Scenario: Drafting a denial does not require the reference +- **WHEN** a handler runs `draftDenial` recording a `denialGround` but no `regulatorReference` +- **THEN** the transition MUST succeed (the reference is only required at finalise) + +@e2e A steward drafts a denial without a regulator reference (allowed), attempts finalise (blocked), records the reference, then finalises (allowed) — the case ends refused. diff --git a/openspec/changes/dsar-case-subsystem/specs/dsar-deadline-tracking/spec.md b/openspec/changes/dsar-case-subsystem/specs/dsar-deadline-tracking/spec.md new file mode 100644 index 0000000000..7a82a06b04 --- /dev/null +++ b/openspec/changes/dsar-case-subsystem/specs/dsar-deadline-tracking/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: Declarative deadline derived fields +OpenRegister SHALL expose deadline-tracking state on each case as declarative +`x-openregister-calculations` on the `dataSubjectRequest` schema, layered on the existing art-12 +maths. The calculations SHALL include `daysRemaining` (whole days until the effective deadline), +`isOverdue` (boolean), and `escalationTier` (a tier derived from `daysRemaining`, e.g. +`on-track` / `reminder` / `escalation` / `breached`). The effective deadline SHALL be +`extendedUntil` when set, otherwise `dueAt`. The calculations MUST reuse the semantics of +`DataSubjectDeadline` (`computeDueAt`/`extend`/`isOverdue`/`daysRemaining`); they MUST NOT +introduce a second, divergent deadline implementation (ADR-011). The derived fields MUST be +available on every case read without a service round-trip. + +#### Scenario: Days-remaining and overdue reflect the effective deadline +- **WHEN** a case has `dueAt` set (and no extension) and the reference date is before it +- **THEN** `daysRemaining` MUST be positive and `isOverdue` MUST be false + +#### Scenario: Extension moves the effective deadline +- **WHEN** a case has an `extendedUntil` later than its `dueAt` +- **THEN** `daysRemaining` and `isOverdue` MUST be computed against `extendedUntil`, not `dueAt` + +#### Scenario: Escalation tier is derived from days remaining +- **WHEN** the effective deadline has passed +- **THEN** `isOverdue` MUST be true and `escalationTier` MUST be `breached` + +### Requirement: Case-count aggregations for the tracking surface +OpenRegister SHALL expose cross-case counts as declarative `x-openregister-aggregations` on the +`dataSubjectRequest` schema: counts of open cases, overdue cases, and breached cases. The +aggregations MUST be computed over the RBAC- and tenant-scoped object set — a case the caller +cannot read MUST NOT contribute to any count. The aggregations MUST NOT be implemented as an +app-local aggregation service that loops objects (ADR-031 anti-pattern). + +#### Scenario: Open and overdue counts are available declaratively +- **WHEN** a steward requests the case-count aggregations for the register/schema +- **THEN** the response MUST include an open-case count and an overdue-case count derived from the cases' status and effective deadline + +#### Scenario: Aggregations respect RBAC and tenant scope +- **WHEN** two tenants hold cases under the same schema +- **THEN** the counts returned to a caller MUST reflect only the cases that caller is authorised to read + +### Requirement: Advance-reminder, escalation, and breach notifications +OpenRegister SHALL declare advance-reminder, escalation, and breach-detection notifications for +data-subject-request cases via `x-openregister-notifications` on the `dataSubjectRequest` schema, +using the canonical ADR-031 notification dialect (`scheduled` and/or `threshold` / +`calculatedChange` triggers over the deadline calculations and aggregations). Each notification +rule MUST fire once per condition per case (idempotent deadline-event semantics) rather than on +every scheduler tick, satisfying the ADR-047 "idempotent deadline-event audit" requirement via the +engine's fire-once-per-condition model. The rules MUST use the canonical dialect only; the obsolete +legacy notification dialect (singular `channel`/`recipient`, `idempotencyKey`, +`trigger.lifecycleEnter`) MUST NOT be used (gate-18). Recipient resolution SHALL target the case +`handler` (and MAY escalate to a configured officer role for the escalation/breach tiers). + +#### Scenario: Advance reminder fires as the deadline approaches +- **WHEN** a case crosses the advance-reminder tier before its effective deadline +- **THEN** a reminder notification MUST be dispatched to the case handler exactly once for that tier + +#### Scenario: Breach detection fires once when the deadline passes +- **WHEN** a case's effective deadline passes without the case being closed +- **THEN** a breach notification MUST be dispatched once +- **AND** the same breach MUST NOT re-notify on subsequent scheduler ticks + +#### Scenario: Rules use the canonical notification dialect +- **WHEN** the notification rules are declared on the schema +- **THEN** they MUST use the canonical `x-openregister-notifications` dialect +- **AND** they MUST NOT use the obsolete legacy dialect fields + +@e2e A case with a near deadline triggers a single reminder to its handler; after the deadline passes it triggers exactly one breach notification and does not duplicate on the next tick. diff --git a/openspec/changes/dsar-case-subsystem/tasks.md b/openspec/changes/dsar-case-subsystem/tasks.md new file mode 100644 index 0000000000..ced13ad1df --- /dev/null +++ b/openspec/changes/dsar-case-subsystem/tasks.md @@ -0,0 +1,48 @@ +# Tasks — dsar-case-subsystem (kind: config) + +This change is the declarative head of the ADR-047 Phase-1 chain: it patches the register JSON +only. No PHP services, controllers, or routes — those land in the successor `dsar-case-engine`. + +## 1. Case entity (schema properties) + +- [x] 1.1 Add case-management properties to `dataSubjectRequest` in `lib/Settings/data_subject_request_register.json`: `handler`, `closedAt`, `dpiaRequired`, retention stamps (`retentionWindow`, `retainUntil`, `purgedAt`) — all optional, each with a human-friendly `title` + `description` (ADR-011). +- [x] 1.2 Add the denial-workflow properties `denialGround` (config-driven generic-key enum, no jurisdiction wording) and `regulatorReference` (optional), with `title`/`description`. +- [x] 1.3 Add the `evidence` sub-collection (items carry `sourceId`, `contentHash`, `status`) and the `redactions` sub-collection (entries carry `field`, `before`, `after`, `ground`) as declared array properties with nested `title`/`description` on every item property. + +## 2. Case lifecycle (declarative) + +- [x] 2.1 Extend `x-openregister-lifecycle` on `dataSubjectRequest` with the N-state case transitions (`assign`, `collectEvidence`, `draftDenial`, `finaliseDenial`, `redact`, `bundle`, `retain`), preserving initial `received` and finals `fulfilled`/`refused`/`closed`. +- [x] 2.2 Declare the `finaliseDenial` transition guard requiring `regulatorReference` presence (lifecycle `requires` reference to the guard delivered by `dsar-case-engine`, or a native required-field precondition if the engine supports it) — `draftDenial` MUST NOT require the reference. + +## 3. Deadline tracking (declarative) + +- [x] 3.1 Add `x-openregister-calculations` for `daysRemaining`, `isOverdue`, `escalationTier`, computed against the effective deadline (`extendedUntil` else `dueAt`), reusing `DataSubjectDeadline` semantics (ADR-011 — no second deadline implementation). +- [x] 3.2 Add `x-openregister-aggregations` for open / overdue / breached case counts over the RBAC + tenant-scoped set. +- [x] 3.3 Add `x-openregister-notifications` (canonical ADR-031 dialect; `scheduled`/`threshold`/`calculatedChange` triggers) for advance-reminder, escalation, and breach rules targeting the case `handler`, with fire-once-per-condition (idempotent) semantics — no legacy dialect fields (gate-18). + +## 4. Seed data + +- [x] 4.1 Add seed `dataSubjectRequest` case objects for a municipality (access, on-track), a consultancy (erasure, extended), and a travel agency (objection, denied with regulator reference), using safe placeholders (nil UUID `00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`, ``) — per the design Seed Data section. + +## 5. Verification + +- [x] 5.1 Validate `lib/Settings/data_subject_request_register.json` parses and imports cleanly (schema + `x-openregister-*` blocks fold into `configuration`); confirm the schema extension is additive (no existing required property removed/renamed). +- [x] 5.2 Run the Hydra mechanical gates relevant to config (`notification-dialect` gate-18, `schema-property-titles` gate-28) and `openspec validate --change dsar-case-subsystem --strict`; fix any pre-existing issues touched. + +## Acceptance Criteria + +- The `dataSubjectRequest` schema gains handler, `closedAt`, retention stamps, DPIA flag, denial fields, and evidence/redaction sub-collections — all optional, all human-labelled. +- The status graph is a configurable N-state `x-openregister-lifecycle`; initial/final semantics preserved; `finaliseDenial` blocked until `regulatorReference` is set, `draftDenial` not blocked. +- Deadline tracking exposes `daysRemaining`/`isOverdue`/`escalationTier` calculations and open/overdue/breached aggregations, RBAC + tenant scoped, reusing `DataSubjectDeadline`. +- Advance-reminder / escalation / breach notifications use the canonical dialect and fire once per condition per case (idempotent). +- Seed cases exist for the three orgs with only safe placeholder ids/tokens. +- No PHP service/controller/route is added by this change; the schema extension is additive and non-breaking. + +## Quality Checklist + +- Reused `DataSubjectDeadline` maths and the existing `DataSubjectRequestService` / audit-trail / `RetentionService` rather than duplicating them (ADR-011, ADR-022). +- Declarative-first: lifecycle, calculations, aggregations, and notifications are register config, not new Service classes (ADR-031); imperative bits are correctly deferred to `dsar-case-engine`. +- Canonical `x-openregister-notifications` dialect only; no obsolete legacy notification fields (gate-18). +- Every added property (including nested sub-collection item properties) declares `title` + `description` (ADR-011, gate-28). +- Seed and example ids/tokens use obvious placeholders (nil UUID, `YOUR_TOKEN_HERE`, ``) — no realistic-looking secrets/UUIDs (gitleaks). +- Schema extension is additive (optional properties only) — non-breaking, no repair-step migration required (ADR-011 versioning). diff --git a/openspec/changes/dsar-case-ui/.openspec.yaml b/openspec/changes/dsar-case-ui/.openspec.yaml new file mode 100644 index 0000000000..43e65ca6e6 --- /dev/null +++ b/openspec/changes/dsar-case-ui/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-03 diff --git a/openspec/changes/dsar-case-ui/design.md b/openspec/changes/dsar-case-ui/design.md new file mode 100644 index 0000000000..dfc0a5f746 --- /dev/null +++ b/openspec/changes/dsar-case-ui/design.md @@ -0,0 +1,138 @@ +## Context + +OpenRegister already ships an AVG surface: `src/views/avg/AvgIndex.vue` (839 lines) — a manifest-driven +page component registered in `src/registry.js` (`AvgIndex: page(AvgIndex)`) and mounted at `/avg` via +`src/manifest.json` (`{ "id": "avg", "route": "/avg", "component": "AvgIndex" }`, nav entry +`AVG / Verwerkingsregister`). It renders four tabs (`activities`, `verantwoording`, `dsar`, +`compliance`) driven by a local `activeTab` string and a `tabs` computed. Its data comes from the +`avg` pinia store module (`src/store/modules/avg.js`), registered as a singleton in +`src/store/store.js` (`avgStore = useAvgStore(pinia)`) and consumed with the Options-API +`avgStore.getX` getters — the codebase's canonical store pattern (`createObjectStore`-style, no +bespoke store). The existing `DSAR` tab exposes only the **stateless** primitives — a one-shot +inzage/vergetelheid/portabiliteit form calling `/api/avg/{inzage,vergetelheid,portabiliteit}`. + +Phase-1 (`dsar-case-subsystem` + `dsar-case-engine`) added the stateful **case**: the +`dataSubjectRequest` entity, an N-state `x-openregister-lifecycle` (`received` → `assign` → +`collectEvidence` → `draftDenial` → `finaliseDenial` → `redact` → `bundle` → `retain` → +`fulfilled`/`refused`/`closed`), deadline calculations (`daysRemaining`, `isOverdue`, +`escalationTier`) + case-count aggregations, and a `/api/gdpr/cases/...` REST API +(`POST /cases`, `POST /cases/{id}/transition`, `POST /cases/{id}/evidence`, +`POST /cases/{id}/redactions`, `POST /cases/{id}/bundle`, `GET /cases/{id}/bundle/download?token=…`). +Phase-2's head `dsar-policy-pack-and-seams` made status/ground labels, escalation thresholds, +retention windows, and template references **policy-pack data**; `dsar-integration-seams` added the +identity-verify and regulator-escalate seams the engine calls. **Nothing in the UI surfaces any of +this.** This change is the last OR-side Phase-2 change: it extends `AvgIndex.vue` + `avg.js` into a +full case-management surface. Phase-3 pipelinq binds NL providers + values and deep-links here. + +## Goals / Non-Goals + +**Goals:** +- Add a `Cases` tab to the existing `AvgIndex.vue` — a filterable, policy-pack-labelled case LIST and + a per-case DETAIL surface with lifecycle transition controls, deadline/escalation display, evidence + panel, redaction, export-bundle (one-time download), denial composer, and the two seam triggers. +- Extend the existing `avg` store with case-management actions/getters that thinly wrap the Phase-1 + `/api/gdpr/cases/...` API and read the active policy pack via OR object APIs — no new store, no new + backend, no new schema/route. +- Resolve every label/ground/wording from the active policy pack (tenant scope + optional per-case + jurisdiction override, a confirmed decision); reference letter/notification templates as leaves + (ADR-022), never inline body text. +- NL Design (CSS variables, standard nc-vue components) + WCAG AA (adr-010/004), with a + visual-regression + e2e proof for the new views (adr-008). + +**Non-Goals:** +- No NL provider bindings, NL policy-pack values, or NL deep-link wiring — Phase-3 pipelinq. +- No change to the Phase-1 case API, the policy-pack/seam contracts, or any OR schema/register/route. +- No rewrite of the existing `activities`/`verantwoording`/`dsar`/`compliance` tabs — the stateless + DSAR form stays; the case workflow is additive. + +## Decisions + +### Declarative-vs-imperative (ADR-031) + +This change is **frontend / transport**, not declarative-schema behaviour — the ADR-031 table below +records why every unit of behaviour is UI/transport code rather than register config. The stateful +behaviour (lifecycle, guards, deadlines, seams) already lives declaratively in the register + as the +Phase-1 engine code; this change only *renders and drives* it. + +| Behaviour | Chosen path | Rationale | +|---|---|---| +| **Case list + detail rendering** | **Frontend — Vue view on `AvgIndex.vue`** | Presentation of the existing case register; a schema cannot express a filterable table + detail. Reuses the app's existing view/table markup + styling. | +| **Filter/label resolution from the policy pack** | **Frontend read of pack config** | The pack is declarative config (head change owns it); the UI *reads* status/ground/tier labels + template refs from the active pack. No new config is authored here. | +| **Lifecycle transition controls** | **Frontend — calls `POST /api/gdpr/cases/{id}/transition`** | The state graph + guards are declarative/engine (Phase-1). The UI only offers the declared transitions and posts them; `finaliseDenial` gating is *enforced* server-side, *reflected* client-side. | +| **Evidence / redaction / bundle / denial / verify / escalate actions** | **Frontend — thin `avg`-store actions over the Phase-1 API + seams** | Transport only — each is an axios call to an existing endpoint, mirroring `avg.js`'s existing `runInzage`/`runVergetelheid` passthroughs. No business logic in the UI. | +| **One-time bundle download** | **Frontend — request bundle → receive token → single authenticated download** | The token mint + burn is server-side (engine); the UI just triggers generation and offers one download, like the existing `downloadPortabiliteit` blob pattern. | +| **Store shape** | **Extend existing `avg` pinia module** | The codebase's canonical pattern (Options API + singleton store in `store.js`); a bespoke case store would violate the store-pattern rule. | + +### Extend `AvgIndex.vue`, do not add a new app/view + +A new `Cases` tab is added to the existing `tabs` computed and the `activeTab` switch — mirroring the +existing four tabs exactly. The list uses the existing `.avgTable`/`.tableContainer`/`.badge` styling +and `NcEmptyContent` empty-states already present; detail + action controls reuse `NcButton`, +`NcActions`/`NcActionButton`, `NcTextField`, and `NcSelect`. This keeps one AVG entry point and one +store. Alternative considered — a separate `CasesIndex.vue` page + nav entry — rejected: it +fragments the AVG surface, duplicates store wiring, and contradicts the "extend, don't rewrite" +scope. + +### Modals live in their own files (ADR-004) + +The denial composer, the redaction form, and the transition/escalate confirmations are non-trivial +forms; per ADR-004 modal-isolation each lives in its own `.vue` under `src/dialogs/avg/` (mirroring +the existing `src/dialogs/avg/EditActivityDialog.vue`) or `src/modals/`, imported by `AvgIndex.vue` — +never inline ``/`` markup. Alternative — inline modals — rejected (gate: +modal-isolation). + +### Policy-pack-driven labels + per-case jurisdiction override (CONFIRMED) + +The UI resolves status/ground/tier labels, deadline wording, and template references from the active +`dsarPolicyPack` for the tenant. A **per-case jurisdiction override** is supported (confirmed +decision): when a case carries a jurisdiction, its pack overrides the tenant default for that case's +labels/grounds/wording. The denial composer's ground `` options are the pack's +denial-grounds enum (key → label + citation). No jurisdiction string is inlined in the Vue. Template +content is rendered from the pack's `template:` reference as a **leaf** (ADR-022) — the UI references +it, it does not embed body text. + +### Accessibility (adr-010 / adr-004, WCAG AA) + +All colours via CSS variables (already the `AvgIndex.vue` convention); every `NcSelect` carries an +`inputLabel` (gate: nc-input-labels); status/escalation badges convey state by text + icon, not colour +alone; the case table is a real `
` with `
` headers (as the existing tables are). A +visual-regression baseline + e2e workflow cover the new views (adr-008). + +## Risks / Trade-offs + +- **[The Phase-1 `/api/gdpr/cases/...` route shape is "provisional" in the engine design]** → The + `avg`-store actions target that exact shape; if the engine finalises different paths, only the + store's URL constants change. Mitigation: centralise the case-API base in one `CASE_API_BASE` + constant in `avg.js` (mirroring the existing `API_BASE`), and keep actions thin so a path change is + a one-line edit. Recorded as an Open Question. +- **[Policy-pack resolution adds a second data fetch]** → The UI must load the active pack alongside + the case list. Mitigation: fetch the pack once on tab-open and cache it in store state (like + `complianceReport`); resolve labels client-side from the cached pack — no per-row round-trip. +- **[`finaliseDenial` is guarded server-side]** → A user could press finalise before recording a + regulator reference. Mitigation: the UI disables/blocks the finalise action until the case carries + a `regulatorReference` (reflecting the engine guard) and surfaces the server's refusal if bypassed — + the guard remains authoritative server-side. +- **[Seam actions can fail closed]** → identity-verify/regulator-escalate may return + unverified/refused (the seams' fail-closed default). Mitigation: the UI renders the seam result + faithfully (unverified / escalation-not-performed) rather than optimistically showing success. +- **[Manifest-driven cache-bust]** → `AvgIndex.vue` is bundled; a new tab needs the standard + info.xml `` bump to bust the immutable JS cache. Mitigation: note it in tasks; no runtime + behaviour change. + +## Migration Plan + +Additive, no migration: the change adds a tab + store actions + dialog(s). No schema/register/route +change, no seed data (no new OR object type — the case register and policy-pack register already +exist from Phase-1/Phase-2). Rollback = revert the frontend commit; the Phase-1 API and pack are +untouched. Any placeholders used in fixtures/e2e (subject ids, tokens) MUST be safe placeholders +(`00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`) — no realistic BSN or secret. + +## Open Questions + +- **Final `/api/gdpr/cases/...` path shape** — the engine design marks the routes "provisional". + Provisional decision: target the documented shapes and isolate them in one store constant so a + finalisation is a one-line change. To confirm against the engine change at apply time. +- **Per-case jurisdiction override source** — confirmed that an override exists; the exact case field + carrying the jurisdiction (and how the pack is selected from it) is resolved against the + case-entity + policy-pack specs at apply time. Provisional: read a `jurisdiction` field on the case + and select the matching pack, falling back to the tenant default pack. diff --git a/openspec/changes/dsar-case-ui/proposal.md b/openspec/changes/dsar-case-ui/proposal.md new file mode 100644 index 0000000000..2a76a58368 --- /dev/null +++ b/openspec/changes/dsar-case-ui/proposal.md @@ -0,0 +1,102 @@ +--- +kind: code +depends_on: [dsar-policy-pack-and-seams, dsar-integration-seams] +--- + +## Why + +Phase-1 gave OpenRegister a stateful data-subject-request **case** (the `dataSubjectRequest` +entity, its N-state lifecycle, deadline tracking, evidence/redaction sub-collections) and a +`/api/gdpr/cases/...` case-management API (`dsar-case-subsystem` + `dsar-case-engine`). Phase-2's +head `dsar-policy-pack-and-seams` made every jurisdiction value (status/ground labels, escalation +thresholds, retention windows, letter templates) **policy-pack data**, and `dsar-integration-seams` +added the identity-verify and regulator-escalate seams the case engine calls. But OpenRegister's +existing AVG surface (`src/views/avg/AvgIndex.vue`) still exposes only the *stateless* DSAR +primitives — a single-shot inzage/vergetelheid/portabiliteit form under the `DSAR` tab. There is **no +UI to open, list, work, or close a tracked case**: no case list, no per-case detail, no lifecycle +transition controls, no deadline/escalation display, no evidence/bundle/denial actions, and no way +to trigger the two seams. The full case-management workflow that Phase-1 + the policy pack made +possible is invisible to a handler. + +This change extends the existing `AvgIndex.vue` surface and the `avg` store module to a **full +data-subject-request case-management UI**, driven by the active policy pack and consuming the +Phase-1 case API and the two seams. It is the **last OR-side Phase-2 change** (policy-pack config → +integration seams code → **this UI**). Phase-3 (`pipelinq`) then binds the NL identity/regulator +providers, supplies the NL policy pack values, and deep-links into this OR case surface; no NL +provider bindings are in scope here — this is the generic OR case UI. + +## What Changes + +- **Add a case LIST** (a new `Cases` tab on the existing `AvgIndex.vue`, not a new app): case rows + with columns whose headings come from the active **policy-pack labels**, filterable by status, + handler, and overdue, and driven by the Phase-1 case list/deadline aggregations. +- **Add a case DETAIL surface**: open one case to see its status, handler, deadline + (due/overdue/escalation-tier from the deadline-tracking calculations), denial fields, evidence, + redactions, and audit history — with the lifecycle transition controls. +- **Add lifecycle TRANSITION controls**: buttons/actions that reflect the case-subsystem state graph + (`assign`, `collectEvidence`, `draftDenial`, `finaliseDenial`, `redact`, `bundle`, `retain`, …), + invoking `POST /api/gdpr/cases/{id}/transition`; the `finaliseDenial` action is gated per the + engine's mandatory-`regulatorReference` guard (blocked until the reference is present). +- **Add evidence, redaction, and export-bundle actions**: an evidence panel (list items with + per-item collection status; trigger a harvest), a field-level redaction action, and a + generate-bundle action that mints the one-time secure download and offers a single download. +- **Add denial compose, identity-verify trigger, and regulator-escalate action**: a denial composer + whose ground options come from the **policy-pack denial-grounds enum** (label + citation from the + pack), an identity-verify trigger (via the identity seam at the `verifying` state), and a + regulator-escalate action (via the regulator seam), each reflecting the seam's fail-closed result. +- **Resolve ALL labels/grounds/wording from the ACTIVE policy pack** for the tenant, with an optional + **per-case jurisdiction override** (a confirmed decision): the UI reads status/ground/tier labels, + deadline wording, and template references from the pack, never inlining jurisdiction strings. +- **Render template content from `template:` references** (leaves, ADR-022): the UI **references** a + letter/notification template by its pack-supplied reference and renders it from the leaf; it does + not inline template body text. +- **Extend the `avg` store** with case-management actions/getters (list, detail, transition, + evidence, redaction, bundle, denial, verify, escalate) that thinly wrap the `/api/gdpr/cases/...` + API — no new custom store, following the existing `avg.js` pinia pattern. + +**Explicitly out of scope — Phase-3 `pipelinq` (successor, not specced here):** the NL identity +(BSN/BRP/RvIG) and NL regulator (AP-complaint) provider bindings, the NL policy-pack values, and any +NL-specific deep-link wiring into this surface. This change ships only the generic OR case UI driven +by whatever pack + seams are active. + +## Capabilities + +### New Capabilities +- `dsar-case-list`: the case LIST surface on `AvgIndex.vue` — a filterable (status/handler/overdue), + policy-pack-labelled table of tracked data-subject-request cases with deadline/escalation state, + driven by the Phase-1 case list + deadline aggregations, plus the `avg`-store list plumbing. +- `dsar-case-detail-actions`: the per-case DETAIL surface and its action set — lifecycle transition + controls (guarded `finaliseDenial`), deadline/escalation display, the evidence panel + redaction + + export-bundle (one-time download) actions, the denial composer (pack-grounds), and the + identity-verify + regulator-escalate seam triggers — all resolving labels/grounds/wording from the + active policy pack (tenant scope + optional per-case jurisdiction override) and referencing + templates as leaves, over NL Design + WCAG AA. + +### Modified Capabilities + +- _none_ + +## Impact + +- **Frontend (this change)**: extend `src/views/avg/AvgIndex.vue` (add a `Cases` tab: list + detail + + action controls) and the `avg` store module `src/store/modules/avg.js` (case-management + actions/getters wrapping `/api/gdpr/cases/...`); new modal(s) under `src/modals/`|`src/dialogs/` for + the denial composer / redaction / transition confirmations (ADR-004 modal-isolation); nc-vue + components only (`NcAppContent`, `NcButton`, `NcActions`, `NcEmptyContent`, `NcTextField`, + `NcSelect` with `inputLabel`, `NcLoadingIcon`), reusing the existing view/table styling. +- **Consumes (unchanged)**: the Phase-1 `/api/gdpr/cases/...` endpoints (create/list/transition/ + evidence/redactions/bundle/download/deny) and the deadline calculations/aggregations; the active + `dsarPolicyPack` object (via OR object APIs) for labels/grounds/windows/templates; the two seams + (identity-verify, regulator-escalate) via the engine's transition/escalation call-outs. +- **APIs**: no new routes — the UI calls the existing `/api/gdpr/cases/...` API and reads the policy + pack through OR's object APIs (RBAC + multitenancy). No backend/schema change. +- **No new schema / register / migration / seed** — this change adds no OR object type; it renders + the existing case register and the existing policy-pack register. +- **Downstream (successor context, not specced here)**: Phase-3 pipelinq binds NL providers + the NL + policy pack and deep-links into this surface; no app is migrated by this change. diff --git a/openspec/changes/dsar-case-ui/specs/dsar-case-detail-actions/spec.md b/openspec/changes/dsar-case-ui/specs/dsar-case-detail-actions/spec.md new file mode 100644 index 0000000000..7913cf7593 --- /dev/null +++ b/openspec/changes/dsar-case-ui/specs/dsar-case-detail-actions/spec.md @@ -0,0 +1,123 @@ +## ADDED Requirements + +### Requirement: Case detail surface with deadline and escalation display +OpenRegister SHALL present a per-case DETAIL surface showing the case status, handler, denial fields, evidence, redactions, audit history, and its deadline/escalation state. +The deadline display MUST render the Phase-1 deadline-tracking calculations (`daysRemaining`, `isOverdue`, `escalationTier`) against the effective deadline, and the escalation tier MUST be conveyed by text and icon, not by colour alone (WCAG AA). Tier and status wording MUST resolve from the active policy pack, never from inlined jurisdiction strings. + +#### Scenario: Detail shows the case deadline and escalation tier +- **WHEN** a handler opens a case detail +- **THEN** the surface MUST display the case's days-remaining/overdue state and its escalation tier from the deadline-tracking calculations +- **AND** the tier MUST be distinguishable without relying on colour alone + +#### Scenario: Overdue case is shown as breached +- **WHEN** a case's effective deadline has passed +- **THEN** the detail MUST show it as overdue with the breached escalation tier label from the active pack + +@e2e A handler opens an overdue case and sees its detail show days-remaining, an overdue indicator, and the breached escalation-tier label (with a text/icon indicator, not colour-only). + +### Requirement: Lifecycle transition controls reflect the declared state graph +OpenRegister SHALL present lifecycle transition controls on the case detail that reflect the Phase-1 declared `x-openregister-lifecycle` transitions and invoke `POST /api/gdpr/cases/{id}/transition` through the extended `avg` store. +The controls MUST offer the declared transitions (e.g. `assign`, `collectEvidence`, `draftDenial`, `finaliseDenial`, `redact`, `bundle`, `retain`) for the case's current state, MUST post the chosen transition to the case API, and MUST reflect the resulting status. The UI MUST NOT embed its own state machine — the declared graph and its guards remain authoritative server-side. + +#### Scenario: Advancing a case posts the declared transition +- **WHEN** a handler runs a transition control (e.g. assign then collectEvidence) on a case +- **THEN** the UI MUST post that transition to `POST /api/gdpr/cases/{id}/transition` +- **AND** the detail MUST reflect the case's new status returned by the API + +#### Scenario: Controls follow the case's current state +- **WHEN** a case is in a given state +- **THEN** the controls MUST offer only the transitions declared from that state, not an arbitrary hard-coded button set + +@e2e A handler opens a received case, assigns a handler, and advances it through collectEvidence, and the detail reflects each status change driven by the case API. + +### Requirement: Finalise-denial control is gated on a regulator reference +OpenRegister SHALL gate the `finaliseDenial` transition control so it is blocked until the case carries a `regulatorReference`, reflecting the Phase-1 denial-finalise guard. +Drafting a denial (`draftDenial`) MUST NOT be gated. The finalise control MUST be disabled or refuse to submit while `regulatorReference` is empty, and MUST surface the server's refusal if a finalise is attempted without it — the server-side guard remains authoritative. + +#### Scenario: Finalise is blocked without a regulator reference +- **WHEN** a case has no `regulatorReference` recorded +- **THEN** the finalise-denial control MUST be blocked/disabled and MUST NOT finalise the denial + +#### Scenario: Finalise proceeds once the reference is recorded +- **WHEN** a handler records a `regulatorReference` and then finalises the denial +- **THEN** the control MUST post `finaliseDenial` and the case MUST reach the refused outcome + +@e2e A handler drafts a denial (allowed), sees finalise blocked while no regulator reference is present, records the reference, then finalises successfully. + +### Requirement: Denial composer resolves grounds from the active policy pack +OpenRegister SHALL present a denial composer whose ground options are the active `dsarPolicyPack` denial-grounds enum, displaying each ground's pack-supplied label and statutory citation. +The composer's ground selector MUST source its options from the pack (key → label + citation), MUST carry an accessible input label (WCAG AA), and MUST NOT inline jurisdiction-specific ground wording in the component. The selected ground key MUST be recorded on the case via the transition/denial endpoint. + +#### Scenario: Ground options come from the pack with label and citation +- **WHEN** a handler opens the denial composer on a case +- **THEN** the ground options MUST be the active pack's denial grounds, each shown with its pack label and citation +- **AND** no jurisdiction-specific ground wording MUST be inlined in the component + +#### Scenario: Selecting a ground records it on the case +- **WHEN** a handler selects a ground and drafts the denial +- **THEN** the selected ground key MUST be sent to the case API and recorded on the case + +@e2e A handler opens the denial composer, sees the pack's grounds with their labels and citations, selects one, and drafts a denial that records the ground on the case. + +### Requirement: Evidence panel lists items with status and triggers a harvest +OpenRegister SHALL present an evidence panel on the case detail that lists the case's evidence items with their per-item collection status and lets a handler trigger an evidence harvest. +The panel MUST render the case's `evidence` sub-collection (source, status per item) and MUST let a handler trigger collection via `POST /api/gdpr/cases/{id}/evidence`, reflecting the returned per-item status (including a re-runnable failed item). It MUST NOT duplicate the harvest logic client-side — it triggers and displays. + +#### Scenario: Evidence items show source and collection status +- **WHEN** a handler opens the evidence panel on a case +- **THEN** each evidence item MUST show its source and its collection status + +#### Scenario: Triggering a harvest updates item status +- **WHEN** a handler triggers an evidence harvest +- **THEN** the UI MUST post to the evidence endpoint and reflect the returned per-item collection status + +@e2e A handler opens a case, triggers an evidence harvest, and sees the evidence items listed with their per-item collection status. + +### Requirement: Redaction action records a field-level redaction +OpenRegister SHALL present a redaction action on the case detail that applies a field-level redaction (before/after + ground) via `POST /api/gdpr/cases/{id}/redactions`. +The redaction form MUST live in its own dialog file (ADR-004 modal isolation), MUST capture the target field, the redaction, and a ground, and MUST post to the redactions endpoint. The applied redaction MUST appear in the case's redactions list on success. + +#### Scenario: Applying a redaction records it on the case +- **WHEN** a handler applies a field-level redaction with a ground +- **THEN** the UI MUST post it to the redactions endpoint +- **AND** the applied redaction MUST appear in the case's redactions list + +@e2e A handler applies a field-level redaction with a ground on a case and sees it recorded in the case's redactions list. + +### Requirement: Export-bundle generation offers a one-time download +OpenRegister SHALL present an export-bundle action that generates the signed bundle via `POST /api/gdpr/cases/{id}/bundle` and offers exactly one download of the returned one-time token. +The UI MUST request bundle generation, receive the one-time download reference, and offer a single authenticated download (`GET /api/gdpr/cases/{id}/bundle/download?token=…`); it MUST NOT expose the token for reuse. Any placeholder shown in fixtures/docs MUST be `YOUR_TOKEN_HERE`, never a realistic token. + +#### Scenario: Generating a bundle yields a single download +- **WHEN** a handler generates the export bundle +- **THEN** the UI MUST request generation and offer exactly one download of the returned one-time reference +- **AND** it MUST NOT present the download token for repeated reuse + +@e2e A handler generates a case export bundle and downloads it once via the one-time reference. + +### Requirement: Identity-verify and regulator-escalate triggers reflect the fail-closed seam result +OpenRegister SHALL present an identity-verify trigger and a regulator-escalate action that invoke the Phase-2 seams through the case API and render the seam's result faithfully, including a fail-closed outcome. +The identity-verify trigger MUST render the seam's `verified`/`failed`/`needs-more` result (never optimistically showing success), and the regulator-escalate action MUST render whether escalation was performed or refused (fail-closed). Neither MUST treat a fail-closed/unavailable seam result as success. + +#### Scenario: Identity verification renders the real result +- **WHEN** a handler triggers identity verification on a case +- **THEN** the UI MUST render the seam's returned status (`verified`, `failed`, or `needs-more`) +- **AND** an unverified/fail-closed result MUST NOT be shown as verified + +#### Scenario: Regulator escalation renders performed-or-refused +- **WHEN** a handler triggers regulator escalation on a case +- **THEN** the UI MUST render whether the escalation was performed or refused +- **AND** a refused/fail-closed result MUST NOT be shown as a successful escalation + +@e2e On an install with no leaf identity or regulator provider bound, a handler triggers identity-verify and regulator-escalate and sees the fail-closed results (unverified / escalation refused), not a false success. + +### Requirement: Template content is referenced as a leaf, not inlined +OpenRegister SHALL render letter/notification template content from the active policy pack's `template:` reference as a leaf (ADR-022), the UI referencing the template rather than inlining its body text. +The UI MUST resolve the template by the pack-supplied reference and render/link it as a leaf; it MUST NOT embed the template body text in the Vue component. Changing the referenced template MUST change what the UI renders without a component change. + +#### Scenario: A denial letter is rendered from a template reference +- **WHEN** a case's active pack supplies a letter template reference for a denial +- **THEN** the UI MUST render/link the template from that reference as a leaf +- **AND** the template body text MUST NOT be inlined in the component + +@e2e A handler composing a denial sees the letter rendered from the pack's template reference, and updating the referenced template changes the rendered content without a redeploy. diff --git a/openspec/changes/dsar-case-ui/specs/dsar-case-list/spec.md b/openspec/changes/dsar-case-ui/specs/dsar-case-list/spec.md new file mode 100644 index 0000000000..b9d92b455f --- /dev/null +++ b/openspec/changes/dsar-case-ui/specs/dsar-case-list/spec.md @@ -0,0 +1,47 @@ +## ADDED Requirements + +### Requirement: Case list surface on the existing AVG view +OpenRegister SHALL present a data-subject-request case LIST as an additional tab on the existing `src/views/avg/AvgIndex.vue` surface, not as a separate app or page. +The tab MUST be added to the existing `tabs`/`activeTab` structure and reuse the app's existing view/table styling and `NcEmptyContent` empty-state, so a handler reaches the case list from the same AVG entry point as the activities/verantwoording/dsar/compliance tabs. The list MUST render tracked cases from the Phase-1 case API (`/api/gdpr/cases`) via the extended `avg` store, MUST NOT introduce a bespoke store, and MUST NOT rewrite or remove the existing stateless DSAR tab. + +#### Scenario: Handler opens the case list from the AVG surface +- **WHEN** a handler opens the AVG view and selects the Cases tab +- **THEN** the tracked data-subject-request cases MUST be listed in a table with per-case status, handler, and deadline state +- **AND** the existing activities/verantwoording/dsar/compliance tabs MUST remain available and unchanged + +#### Scenario: Empty case list shows an empty-state, not an error +- **WHEN** no cases exist for the caller +- **THEN** the list MUST show an empty-state (not an error or blank table) + +@e2e A handler opens the AVG view, selects the Cases tab, and sees the tracked cases listed (or a friendly empty-state when there are none), with the other AVG tabs still present. + +### Requirement: Case list columns and wording resolve from the active policy pack +OpenRegister SHALL resolve the case-list column labels, status wording, and escalation-tier wording from the active `dsarPolicyPack`, not from strings hard-coded in the Vue component. +The list MUST read status/tier labels from the pack for the tenant (with an optional per-case jurisdiction override) so that changing a label in the pack changes the list wording without a component change. No jurisdiction-specific label string MUST be inlined in the view. + +#### Scenario: Status and tier labels come from the pack +- **WHEN** the active policy pack defines status and escalation-tier labels +- **THEN** the case list MUST display those pack-supplied labels +- **AND** changing a label in the pack MUST change the list wording without a code change + +#### Scenario: No jurisdiction wording is hard-coded in the view +- **WHEN** the case-list component is inspected +- **THEN** it MUST resolve status/ground/tier labels from the active pack +- **AND** it MUST NOT contain inlined jurisdiction-specific label strings + +@e2e A steward changes a status label in the active policy pack and sees the case-list column wording reflect the new label without a redeploy. + +### Requirement: Case list is filterable by status, handler, and overdue +OpenRegister SHALL let a handler filter the case list by status, by handler, and by an overdue toggle, driven by the Phase-1 deadline-tracking state. +Filtering by an overdue toggle MUST use the case's `isOverdue`/`escalationTier` deadline state, and the list MUST reflect the RBAC- and tenant-scoped case set — a case the caller cannot read MUST NOT appear. Every filter control that is a select MUST carry an accessible input label (WCAG AA). + +#### Scenario: Overdue filter narrows to breached/overdue cases +- **WHEN** a handler enables the overdue filter +- **THEN** the list MUST show only cases whose effective deadline has passed (overdue/breached) +- **AND** clearing the filter MUST restore the full authorised set + +#### Scenario: Status and handler filters narrow the list +- **WHEN** a handler selects a status and/or a handler filter +- **THEN** the list MUST show only cases matching the selected status/handler within the caller's authorised set + +@e2e A handler filters the case list by an overdue toggle and by status, and the visible rows narrow to the matching authorised cases; clearing the filters restores the full list. diff --git a/openspec/changes/dsar-case-ui/tasks.md b/openspec/changes/dsar-case-ui/tasks.md new file mode 100644 index 0000000000..a15fb173c5 --- /dev/null +++ b/openspec/changes/dsar-case-ui/tasks.md @@ -0,0 +1,57 @@ +# Tasks — dsar-case-ui (kind: code, depends_on: dsar-policy-pack-and-seams, dsar-integration-seams) + +Last of the ADR-047 Phase-2 chain (policy-pack config → integration seams code → **THIS UI**). This +change EXTENDS the existing `src/views/avg/AvgIndex.vue` (add a `Cases` tab) and the existing `avg` +pinia store (`src/store/modules/avg.js`) into a full data-subject-request case-management surface, +consuming the Phase-1 `/api/gdpr/cases/...` API + the policy pack + the two seams. NO new app/page, +NO new store, NO backend/schema/register/route/seed, NO NL provider bindings (Phase-3 pipelinq). + +## 1. Store extension (`avg` module) + +- [x] 1.1 Extend `src/store/modules/avg.js` with a single `CASE_API_BASE` constant and thin case-management actions/getters — `fetchCases` (list, with status/handler/overdue params), `fetchCase`, `transitionCase`, `collectEvidence`, `applyRedaction`, `generateBundle`, `downloadBundle`, `draftDenial`/`finaliseDenial`, `verifyIdentity`, `escalateRegulator` — each a thin `/api/gdpr/cases/...` passthrough mirroring the existing `runInzage`/`runVergetelheid` style (no business logic, `@spec exclude` passthrough docblocks). +- [x] 1.2 Add a `fetchActivePolicyPack` action + `getActivePolicyPack` getter that loads the active `dsarPolicyPack` (tenant scope, with per-case jurisdiction override) via OR object APIs and caches it in store state, so labels/grounds/tier wording resolve client-side without a per-row round-trip. + +## 2. Case list (Cases tab on AvgIndex.vue) + +- [x] 2.1 Add a `Cases` entry to the existing `tabs` computed + `activeTab` switch in `AvgIndex.vue`, reusing the existing `.avgTable`/`.tableContainer`/`.badge` styling and an `NcEmptyContent` empty-state; do NOT alter the existing activities/verantwoording/dsar/compliance tabs. +- [x] 2.2 Render the case list from `avgStore.fetchCases`, with columns/status/tier wording resolved from the cached active policy pack (no inlined jurisdiction strings), showing per-case status, handler, and deadline/escalation state. +- [x] 2.3 Add status, handler, and overdue filter controls (any `NcSelect` carries an `inputLabel`), driving the list against the Phase-1 deadline state (`isOverdue`/`escalationTier`) within the RBAC/tenant-scoped set. + +## 3. Case detail + deadline display + +- [x] 3.1 Add a per-case detail surface (opened from a list row) showing status, handler, denial fields, evidence, redactions, and audit history, with tier/status wording from the active pack. +- [x] 3.2 Render the deadline/escalation display from the Phase-1 calculations (`daysRemaining`, `isOverdue`, `escalationTier`) against the effective deadline, conveying tier by text + icon (not colour alone, WCAG AA). + +## 4. Lifecycle transition controls + +- [x] 4.1 Add transition controls that offer the declared `x-openregister-lifecycle` transitions for the case's current state (`assign`, `collectEvidence`, `draftDenial`, `finaliseDenial`, `redact`, `bundle`, `retain`) and post them via `avgStore.transitionCase` (`POST /api/gdpr/cases/{id}/transition`), reflecting the returned status — no client-side state machine. +- [x] 4.2 Gate the `finaliseDenial` control: block/disable it while the case has no `regulatorReference` (reflecting the engine guard), allow `draftDenial` ungated, and surface the server's refusal if finalise is attempted without the reference. + +## 5. Detail actions + seam triggers (dialogs, ADR-004) + +- [x] 5.1 Add a denial-composer dialog under `src/dialogs/avg/` (own file, ADR-004 modal isolation) whose ground `NcSelect` (with `inputLabel`) sources options from the pack's denial-grounds enum (label + citation), recording the selected ground key via the denial/transition endpoint; render the letter from the pack's `template:` reference as a leaf (ADR-022) — no inlined body text. +- [x] 5.2 Add an evidence panel listing the case's `evidence` items (source + per-item status) with a trigger-harvest action (`POST /api/gdpr/cases/{id}/evidence`) that reflects returned per-item status, and a redaction dialog (own file) applying a field-level redaction (before/after + ground) via `POST /api/gdpr/cases/{id}/redactions`. +- [x] 5.3 Add an export-bundle action (`POST /api/gdpr/cases/{id}/bundle`) that offers exactly one authenticated download of the returned one-time token (`YOUR_TOKEN_HERE` placeholder in any fixture, never a real token), plus identity-verify and regulator-escalate triggers that render the seam's fail-closed result faithfully (`verified`/`failed`/`needs-more`; performed/refused) — never showing a false success. + +## 6. Cache-bust + verification + +- [x] 6.1 Bump `appinfo/info.xml` `` to bust the immutable bundled-JS cache for the new tab (no runtime behaviour change). +- [x] 6.2 Add a Playwright e2e workflow + a visual-regression baseline for the new Cases list + detail views (adr-008); run `openspec validate --change dsar-case-ui --strict` and the relevant Hydra gates (nc-input-labels, modal-isolation, e2e-coverage gate-19, visual-coverage gate-26, spec-coverage gate-16), fixing any pre-existing issue touched. + +## Acceptance Criteria + +- A `Cases` tab is added to the existing `AvgIndex.vue` (existing tabs unchanged) showing a filterable (status/handler/overdue) case list whose column/status/tier wording resolves from the active policy pack, driven by `/api/gdpr/cases` via the extended `avg` store — no new app, page, or store. +- A per-case detail surface shows status/handler/denial/evidence/redactions/history and the deadline/escalation state from the Phase-1 calculations, tier conveyed by text + icon (not colour alone). +- Lifecycle transition controls reflect the declared state graph and post transitions to the case API; `finaliseDenial` is gated on a `regulatorReference` (draftDenial ungated), with the server guard authoritative. +- The denial composer sources grounds (label + citation) from the pack; the evidence panel lists items with status and triggers a harvest; a field-level redaction is applied; the export bundle offers exactly one one-time download; identity-verify and regulator-escalate render the fail-closed seam result faithfully. +- Letter/notification templates are rendered from the pack's `template:` reference as leaves (ADR-022), not inlined; no jurisdiction-specific label/ground string is hard-coded in the Vue. +- No backend/schema/register/route/seed change and no NL provider bindings are added by this change. + +## Quality Checklist + +- Frontend/transport only — the ADR-031 table in design.md justifies why every unit is UI/transport (the lifecycle/guards/deadlines/seams stay declarative + Phase-1 engine); no business logic moves into the Vue. +- Extends the existing `AvgIndex.vue` + `avg` pinia module (Options API + singleton store) rather than adding a bespoke store/app (store-pattern rule, ADR-004); component/composable bugs are fixed in `@conduction/nextcloud-vue`, not worked around locally. +- NL Design + WCAG AA: CSS variables only (no hardcoded colours), every `NcSelect` carries an `inputLabel`, status/tier conveyed by text + icon not colour alone, real ``/`
` structure (adr-010/adr-004). +- Every modal/dialog lives in its own file under `src/dialogs/avg/`|`src/modals/` (ADR-004 modal-isolation); no inline ``/`` in `AvgIndex.vue`. +- A Playwright e2e workflow AND a visual-regression baseline cover the new Cases list + detail views (adr-008, gate-19 e2e-coverage + gate-26 visual-coverage); behavioural spec scenarios carry `@e2e`. +- Any fixtures/e2e use safe placeholders only (nil UUID `00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`); no realistic BSN, subject id, token, or secret (gitleaks). diff --git a/openspec/changes/dsar-escalation-and-dpia/design.md b/openspec/changes/dsar-escalation-and-dpia/design.md new file mode 100644 index 0000000000..7ea656ff36 --- /dev/null +++ b/openspec/changes/dsar-escalation-and-dpia/design.md @@ -0,0 +1,96 @@ +# Design: dsar-escalation-and-dpia + +## Verified state on HEAD (= origin/development, 6b0534094) + +What exists vs what is missing — all claims checked against the working tree: + +| Piece | State on HEAD | +|---|---| +| `lib/Service/Gdpr/DataSubjectDeadline.php` | Pure art-12(3) arithmetic only: `computeDueAt` (+1 month), `extend` (+2 months, once — caller-enforced), `isOverdue`, `daysRemaining`. No scheduling, no notification. | +| `escalationTier` | Declared as a calculated field on `dataSubjectRequest` (`data_subject_request_register.json:210`), tier boundaries resolved from the active `dsarPolicyPack.escalationTiers` via `@ref.pack` (index 0 reminder / 1 escalation / 2 breach), fail-safe on-track when no pack resolves. | +| Deadline notifications | THREE rules already declared (`:311`): `deadlineAdvanceReminder`, `deadlineEscalation`, `deadlineBreach` — all `trigger.type=calculatedChange` on `escalationTier` with `previously.ne` boundary guards, recipient `{kind: field, field: handler}`, nc-notification channel, NL+EN copy. | +| The gap | `calculatedChange` rules are evaluated ONLY by `lib/Listener/AnnotationNotificationListener.php` (needs old+new data from a real object write). `lib/BackgroundJob/ScheduledNotificationJob.php` iterates schemas but processes ONLY `trigger.type === 'scheduled'` entries (`:209`). **Nothing re-computes a time-dependent calculation for an untouched object** ⇒ the three declared rules are dead letters for exactly the proactive case they exist for. | +| Scheduled-trigger evaluator | `ScheduledFilterEvaluator` supports `withinNext`/`olderThan` duration operators — considered and rejected as the fix (below). | +| DPIA | `dpiaRequired` appears ONLY in the register JSON (property + seeds); `grep dpiaRequired lib --include=*.php` → no PHP hits. No detection engine anywhere in OR. | +| pipelinq semantics to generalise | `DeadlineTrackerService`: 7-day reminder → handler, <72 h escalation (skip if already breached), breach → `termijnOverschreden=true` + `fgGeinformeerd=true` + notify; every event recorded once (dedupe by event type). `DpiaDetectionService`: `DEFAULT_THRESHOLD=10`, `WINDOW_DAYS=30`, group = same article + scope, flag + inform FG. | +| Policy pack | `dsarPolicyPack` schema exists (escalationTiers, jurisdiction selection via the case's pack key, seeded default pack guarantees resolution). Natural home for detection config + officer recipient. | + +## Decision 1 — re-materialise, don't re-declare + +Two candidate mechanics for making the deadline rules fire: + +1. **Rewrite the three rules as `trigger.type=scheduled` with `withinNext`/`olderThan` filters on + `dueAt`.** Rejected: the tier boundaries would move out of the policy pack into hard-coded + filter durations (breaking the jurisdiction-as-data design ADR-047 chose), `extendedUntil` + handling would be re-implemented in filters, and `escalationTier` — which the UI, the + `breachedCaseCount` aggregation, and the dossier already read — would still go stale. +2. **A temporal re-evaluation sweep** (chosen): `TemporalCalculationSweepJob` (TimedJob, hourly + class, `IAppConfig` interval + enabled toggles per the `DsarRetentionSweepJob` convention) + iterates schemas whose materialised calculations reference `now` (detectable from the + calculation AST the `CalculationAnnotationValidator` already parses; cached per schema), + selects objects in non-terminal lifecycle states, recomputes via `CalculationEvaluator`, and + persists ONLY changed values through the normal `ObjectService` write path. The write emits + the standard updated event with old+new data ⇒ `AnnotationNotificationListener` evaluates the + `calculatedChange` rules ⇒ the three declared notifications dispatch, boundary-guarded + (`previously.ne`) so each tier crossing notifies exactly once. Zero new notification + machinery; the fix is generic clockwork any future `now`-dependent schema inherits. + +Write amplification is bounded: a case is rewritten at most once per tier crossing (≤ 3 writes +over its whole life), because unchanged recomputations are skipped before the write path. + +## Decision 2 — breach visibility + +pipelinq's tracker set `fgGeinformeerd` and stamped the breach. OR equivalents: +- `breachedAt` (date-time, nullable) on `dataSubjectRequest`, written by the same sweep write + that crosses into `breached` (declared as an `onSet`-style calculation guard: only when null — + keeping ADR-031's data-over-code rule; the job carries no DSAR-specific branch). +- `deadlineBreach` recipients gain a second entry resolving the privacy-officer **group** from the + active pack. The notification engine's recipient kinds already include `groups` and + `expression` (`NotificationAnnotationValidator::VALID_RECIPIENT_KINDS`); the pack contributes + the group name. Exact recipient kind (`groups` with a pack-resolved name vs `expression`) is an + implementation detail decided against the dispatcher's resolver capabilities at build time — + the requirement is only "pack-resolved officer recipient, declared on the register". + +## Decision 3 — DPIA detection shape + +`Gdpr/DpiaPatternDetectionService` (pure, unit-testable grouping/thresholding — mirrors +`DataSubjectDeadline`'s dependency-light style) + `DsarDpiaDetectionJob` (daily TimedJob): + +``` +run(): + pack = resolve active dsarPolicyPack; no pack or no pack.dpiaDetection → no-op (fail-safe) + cases = DSAR cases with receivedAt >= now - pack.dpiaDetection.windowDays + groups = group cases by pack.dpiaDetection.groupBy (default [type, scope-normalised]) + for each group with count >= pack.dpiaDetection.threshold: + for each case in group where dpiaRequired != true: + set dpiaRequired = true via ObjectService (audited write; audit context carries + rule/groupKey/window/count) +``` + +Idempotency falls out of the `dpiaRequired != true` filter (flagged cases count toward the group +but are never re-written) — no detection-state table needed. Manual flags are never cleared by +the job (one-way ratchet; unflagging is a human decision). Officer notification is NOT dispatched +from the job: a declared `dpiaFlagged` rule on the register (updated-trigger, `dpiaRequired` +false→true boundary) covers both detection and manual flagging through one path — the exact +anti-imperative-dispatch posture gate-18 (notification-dialect) enforces. + +Scope normalisation for grouping starts deliberately dumb (trim/lowercase/collapse whitespace) — +pipelinq's detector compared raw equality; anything smarter (similarity clustering) is a future +engine change, not v1. + +## ADR-032 sizing note + +Escalation + DPIA ship as ONE `kind: code` change: same register file, same policy pack, same +TimedJob conventions, one consumer's gap list — splitting would create two changes whose register +edits collide in `data_subject_request_register.json`. The register edits are incidental to the +code (ADR-032's "may incidentally touch declarative JSON" clause); there is no `mixed`-shape risk +because no schema-engine feature is being declared that code then consumes — the code makes +already-declared behaviour live. + +## Out of scope + +- An NL BSN identity-verify provider (pipelinq gap #3) — belongs to whichever app owns NL + identity, recorded in `consume-or-dsar`, not an OR engine gap. +- Regulator auto-escalation on breach (the `RegulatorEscalateRegistry` seam exists; wiring breach + → automatic AP dossier is a policy decision deferred to a policy-pack iteration). +- Rewriting the three existing notification rules' copy/channels — they ship as declared. diff --git a/openspec/changes/dsar-escalation-and-dpia/proposal.md b/openspec/changes/dsar-escalation-and-dpia/proposal.md new file mode 100644 index 0000000000..ae21d4da37 --- /dev/null +++ b/openspec/changes/dsar-escalation-and-dpia/proposal.md @@ -0,0 +1,95 @@ +--- +kind: code +depends_on: [] +--- + +## Why + +When pipelinq retired its parallel AVG stack onto OR's Gdpr subsystem (ADR-047 Phase 3, pipelinq +change `consume-or-dsar`), its deletion audit mapped 15 app services onto OR replacements and found +exactly two capabilities OR does not provide (`pipelinq/openspec/changes/consume-or-dsar/design.md` +§OR-side gaps): + +1. **Deadline-escalation notifications.** pipelinq's `DeadlineTrackerService` + + `AvgDeadlineTrackerJob` proactively notified handlers (7-day reminder, <72 h escalation, breach + + FG informed) as `dueAt` approached/passed. OR HEAD has the *declarations* but not the + *clockwork*: `lib/Settings/data_subject_request_register.json` already declares an + `escalationTier` calculated field (tier boundaries from the active `dsarPolicyPack` via + `@ref.pack`, fail-safe on-track when no pack resolves) and three `x-openregister-notifications` + rules (`deadlineAdvanceReminder` / `deadlineEscalation` / `deadlineBreach`) on + `calculatedChange` of that field — but `calculatedChange` triggers fire ONLY from + `AnnotationNotificationListener` on object writes, and `ScheduledNotificationJob` processes + ONLY `trigger.type=scheduled` entries. A DSAR case nobody touches never re-computes its tier, + so the declared reminder/escalation/breach notifications never dispatch. `Gdpr/DataSubjectDeadline` + is pure arithmetic (computeDueAt/extend/isOverdue/daysRemaining) — nothing schedules it. +2. **DPIA pattern detection.** OR carries only the `dpiaRequired` boolean on the + `dataSubjectRequest` schema (verified: the string appears nowhere in `lib/` PHP — register JSON + only). pipelinq's `DpiaDetectionService` + `AvgDpiaPatternDetectionJob` flagged DPIA review when + ≥ N similar requests (same article + scope) arrived inside a rolling 30-day window + (`DEFAULT_THRESHOLD = 10`, `WINDOW_DAYS = 30`) and informed the FG. That heuristic is generic + GDPR art-35 tooling, not pipelinq domain — per ADR-047/ADR-051 §4 it belongs in OR, not in a + leaf. + +Both gaps block nothing in the pipelinq migration (the app code is deleted regardless) but leave +every ADR-047 consumer without proactive Art-12 deadline safety or Art-35 pattern awareness until +OR closes them. + +## What Changes + +- **Temporal re-evaluation sweep** — a `TimedJob` that periodically re-materialises + time-dependent calculated fields (calculations whose expressions reference `now`, starting with + `escalationTier`) for objects in non-terminal lifecycle states, writing through the normal + object write path so the already-declared `calculatedChange` notification rules fire with + old/new data. No new notification machinery: the three declared DSAR rules become live, and any + future schema with a `now`-dependent calculation + `calculatedChange` rule gets deadline-watch + behaviour for free (ADR-031: behaviour stays declared on the schema; the job is generic + clockwork). +- **Breach visibility beyond the handler** — the `deadlineBreach` rule today notifies only the + case handler; pipelinq's tracker also informed the FG (`fgGeinformeerd`). The breach rule gains + a second recipient resolved from the active `dsarPolicyPack` (privacy-officer group), and the + breach crossing is stamped on the case (`breachedAt`) through the same write, so the existing + `breachedCaseCount` aggregation and regulator dossier reflect it. +- **DPIA pattern detection** — a `Gdpr/DpiaPatternDetectionService` + daily `TimedJob` + generalising pipelinq's heuristic: group open/recent DSAR cases by configurable characteristics + (default: `type` + normalised scope) over a rolling window; when a group reaches the threshold, + set `dpiaRequired = true` on the group's cases through the normal write path (audited), and + notify the privacy officer via a new declared `x-openregister-notifications` rule on the + `dpiaRequired` false→true transition. Threshold, window, grouping fields, and the + privacy-officer recipient come from the `dsarPolicyPack` (new `dpiaDetection` block) — config as + data, fail-safe: no resolvable pack ⇒ no flagging, never a false DPIA. + +Register edits (breach recipient, `dpiaFlagged` rule, `breachedAt` field, policy-pack +`dpiaDetection` + officer-group fields) are incidental to the two code capabilities — centre of +mass is code (ADR-032 `kind: code`), and the two capabilities ship together because they close one +consumer's audit (same register, same policy pack, same sweep-job pattern). + +## Capabilities + +### New Capabilities +- `dsar-deadline-escalation`: the temporal re-evaluation sweep that makes the declared + reminder/escalation/breach notifications dispatch without object writes, plus breach + stamping and privacy-officer breach visibility. +- `dsar-dpia-detection`: policy-pack-driven DPIA pattern detection over DSAR cases with audited + flagging and privacy-officer notification. + +### Modified Capabilities + + +## Impact + +- **New code**: `lib/BackgroundJob/TemporalCalculationSweepJob.php` (generic, schema-driven), + `lib/Service/Gdpr/DpiaPatternDetectionService.php` + `lib/BackgroundJob/DsarDpiaDetectionJob.php`, + registration in `appinfo/info.xml`. +- **Register edits** (`lib/Settings/data_subject_request_register.json`): `breachedAt` property, + privacy-officer recipient on `deadlineBreach`, new `dpiaFlagged` notification rule, + `dsarPolicyPack` gains `dpiaDetection` (threshold, windowDays, groupBy) + privacy-officer group; + seeded default pack updated so a pack always resolves (existing fail-safe convention). +- **Consumes (unchanged)**: `CalculationEvaluator` (exposes `now`), the materialised-calculation + write path, `AnnotationNotificationDispatcher` `calculatedChange` boundary-crossing semantics + (fires once per tier crossing — natural dedupe), `ObjectService` (RBAC + audit), + `DsarRetentionSweepJob`/`AvgRetentionJob` TimedJob conventions, `dsarPolicyPack` resolution. +- **No new endpoints, no new schemas, no lifecycle changes.** +- **Downstream**: pipelinq `consume-or-dsar` records these as its two OR deltas; zaakafhandelapp / + procest DSAR surfaces inherit both behaviours with zero app code. diff --git a/openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md b/openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md new file mode 100644 index 0000000000..16cf1d2dab --- /dev/null +++ b/openspec/changes/dsar-escalation-and-dpia/specs/dsar-deadline-escalation/spec.md @@ -0,0 +1,47 @@ +## ADDED Requirements + +### Requirement: Time-dependent calculated fields re-evaluate without object writes +OpenRegister SHALL provide a scheduled temporal re-evaluation sweep that periodically +re-materialises calculated fields whose expressions reference the evaluation clock (`now`) — +starting with the `dataSubjectRequest.escalationTier` field — for objects in non-terminal +lifecycle states, writing any changed value through the normal object write path. Because the +write path emits the standard object-updated events, the schema's declared +`x-openregister-notifications` rules with `trigger.type=calculatedChange` MUST consequently fire +on tier boundary crossings exactly as if a user had edited the object. The sweep MUST NOT rewrite +objects whose recomputed value is unchanged, and MUST skip schemas with no `now`-dependent +materialised calculations. + +#### Scenario: Untouched case crosses the reminder tier +- **WHEN** an open DSAR case's `dueAt` moves inside the active policy pack's reminder window while nobody edits the case +- **THEN** the next sweep run re-materialises `escalationTier` to `reminder` through the write path +- **AND** the declared `deadlineAdvanceReminder` notification is dispatched to the case handler + +#### Scenario: Tier crossing notifies exactly once +- **WHEN** consecutive sweep runs recompute a case whose tier remains `reminder` +- **THEN** no write occurs for the unchanged value and no duplicate reminder is dispatched (the `calculatedChange` previously/eq boundary guard fires only on the crossing) + +#### Scenario: Terminal cases are left alone +- **WHEN** a case is in a terminal lifecycle state (fulfilled, refused, closed) +- **THEN** the sweep does not recompute or rewrite it + +#### Scenario: No resolvable policy pack stays fail-safe +- **WHEN** no `dsarPolicyPack` resolves for a case's jurisdiction +- **THEN** the recomputed tier remains on-track (existing fail-safe convention) and no escalation notification is produced + +### Requirement: Deadline breach is stamped on the case and visible to the privacy officer +When the sweep (or any write) moves a case's `escalationTier` to `breached`, OpenRegister SHALL +stamp the crossing on the case (`breachedAt` timestamp, written once) and the declared +`deadlineBreach` notification SHALL reach, in addition to the case handler, a privacy-officer +recipient resolved from the active `dsarPolicyPack`. The stamp and the recipients MUST be +declared on the register (ADR-031), not hard-coded in the job. + +#### Scenario: Breach notifies handler and privacy officer +- **WHEN** an open case passes its (possibly extended) deadline and the sweep re-materialises its tier to `breached` +- **THEN** the `deadlineBreach` notification is dispatched to the case handler AND to the policy pack's privacy-officer recipient +- **AND** `breachedAt` is set on the case through the same audited write + +#### Scenario: Breach stamp is written once +- **WHEN** later sweeps recompute an already-breached case +- **THEN** `breachedAt` keeps its original value and no further breach notification is dispatched + +@e2e A privacy officer seeds an open DSAR case whose deadline lies inside the reminder window, triggers the temporal sweep, and sees the reminder notification arrive for the handler; moving the clock past the deadline and re-running the sweep shows the breach notification for both handler and privacy officer and the case's breached timestamp in its detail view. diff --git a/openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md b/openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md new file mode 100644 index 0000000000..c934420a64 --- /dev/null +++ b/openspec/changes/dsar-escalation-and-dpia/specs/dsar-dpia-detection/spec.md @@ -0,0 +1,55 @@ +## ADDED Requirements + +### Requirement: DPIA pattern detection over DSAR cases +OpenRegister SHALL provide a scheduled DPIA pattern-detection capability (GDPR art-35) that +groups DSAR cases received inside a rolling window by configurable characteristics (default: +request `type` plus normalised scope) and, when a group's size reaches the configured threshold, +sets `dpiaRequired = true` on every case in the group through the normal object write path with +an audit-trail entry recording the detection (rule, group key, window, count). Detection MUST be +idempotent: cases already flagged are counted for the group but never re-written or re-audited, +and a group that already triggered does not re-trigger for the same membership. + +#### Scenario: Threshold crossing flags the group +- **WHEN** the number of cases with the same type and scope received inside the rolling window reaches the configured threshold +- **THEN** every unflagged case in the group gets `dpiaRequired = true` through an audited write +- **AND** the audit entry names the detection rule, group key, window, and count + +#### Scenario: Re-runs are idempotent +- **WHEN** the detection job runs again over an already-flagged group with no new members +- **THEN** no case is re-written, no audit entry is added, and no notification is re-sent + +#### Scenario: Below-threshold groups stay untouched +- **WHEN** a group's size inside the window is below the threshold +- **THEN** no case in it is flagged and `dpiaRequired` set manually by a handler is never cleared by the job + +### Requirement: Detection configuration lives in the DSAR policy pack +OpenRegister SHALL read the detection threshold, rolling-window length, grouping characteristics, +and the privacy-officer recipient from the active `dsarPolicyPack` (new `dpiaDetection` block) — +configuration as data, never hard-coded in the job. When no policy pack resolves, or the pack +declares no `dpiaDetection` block, detection MUST be a no-op (fail-safe: no false DPIA flags). +The seeded default pack SHALL carry the generalised defaults (threshold 10, window 30 days, +group by type + scope — pipelinq `DpiaDetectionService` parity). + +#### Scenario: Pack-driven thresholds +- **WHEN** an administrator lowers the active pack's `dpiaDetection.threshold` +- **THEN** the next detection run applies the new threshold without any code change + +#### Scenario: No pack, no detection +- **WHEN** no `dsarPolicyPack` resolves +- **THEN** the detection run exits without flagging, auditing, or notifying + +### Requirement: DPIA flagging notifies the privacy officer +The DSAR register SHALL declare an `x-openregister-notifications` rule dispatching to the policy +pack's privacy-officer recipient when a case's `dpiaRequired` transitions from false to true, so +detection results reach the officer through the existing notification engine (ADR-031) rather +than a bespoke dispatch path in the job. + +#### Scenario: Officer notified on detection flag +- **WHEN** the detection job sets `dpiaRequired = true` on a case +- **THEN** the declared rule dispatches one notification to the privacy-officer recipient naming the case + +#### Scenario: Manual flagging uses the same rule +- **WHEN** a handler manually sets `dpiaRequired = true` on a case +- **THEN** the same declared rule dispatches — detection and manual flagging share one notification path + +@e2e A privacy officer seeds eleven access requests with the same scope inside the window, runs the DPIA detection job, and sees the cases flagged dpiaRequired with an audit entry and a notification in the officer's inbox; re-running the job produces no duplicates, and removing the policy pack's dpiaDetection block makes the next run a no-op. diff --git a/openspec/changes/dsar-escalation-and-dpia/tasks.md b/openspec/changes/dsar-escalation-and-dpia/tasks.md new file mode 100644 index 0000000000..85eb865187 --- /dev/null +++ b/openspec/changes/dsar-escalation-and-dpia/tasks.md @@ -0,0 +1,28 @@ +# Tasks — dsar-escalation-and-dpia (kind: code, depends_on: —) + +Closes the two OR gaps from pipelinq's `consume-or-dsar` deletion audit (deadline escalation, +DPIA detection). Verify all register/dispatcher claims against HEAD before building — the three +deadline notification rules and the `escalationTier` calculation already exist in +`lib/Settings/data_subject_request_register.json`; this change makes them live and adds detection. + +## 1. Temporal re-evaluation sweep (deadline escalation) + +- [ ] 1.1 Add `lib/BackgroundJob/TemporalCalculationSweepJob.php` (TimedJob; `IAppConfig` interval + enabled toggles per the `DsarRetentionSweepJob` convention): detect schemas with materialised calculations referencing `now` (reuse the calculation AST the `CalculationAnnotationValidator` parses; cache per schema), select objects in non-terminal lifecycle states, recompute via `CalculationEvaluator`, persist ONLY changed values through the `ObjectService` write path (so `AnnotationNotificationListener` fires the declared `calculatedChange` rules), skip everything else; register in `appinfo/info.xml`. +- [ ] 1.2 Verify end-to-end that an untouched DSAR case crossing a tier boundary dispatches the already-declared `deadlineAdvanceReminder` / `deadlineEscalation` / `deadlineBreach` rules exactly once per crossing (boundary `previously.ne` guard), and that a no-pack case stays on-track with no dispatch. + +## 2. Breach stamping + officer visibility + +- [ ] 2.1 Add the `breachedAt` (date-time, nullable, write-once) property to `dataSubjectRequest` in `lib/Settings/data_subject_request_register.json`, populated declaratively on the tier write that crosses into `breached` (no DSAR-specific branch in the sweep job). +- [ ] 2.2 Extend the declared `deadlineBreach` rule with a second recipient resolving the privacy-officer group from the active `dsarPolicyPack` (recipient kind chosen against the dispatcher's resolver capabilities — `groups`/`expression`); add the officer-group field to the `dsarPolicyPack` schema and the seeded default pack. + +## 3. DPIA pattern detection + +- [ ] 3.1 Add `lib/Service/Gdpr/DpiaPatternDetectionService.php` (dependency-light, unit-testable — `DataSubjectDeadline` style): group cases received inside the rolling window by the pack's `groupBy` characteristics (default type + normalised scope: trim/lowercase/collapse whitespace), return the groups meeting the threshold; fail-safe empty result when no pack or no `dpiaDetection` block resolves. +- [ ] 3.2 Add `lib/BackgroundJob/DsarDpiaDetectionJob.php` (daily TimedJob, same config-toggle conventions): for each triggering group, set `dpiaRequired = true` on unflagged cases via `ObjectService` with the audit context carrying rule/groupKey/window/count; idempotent (flagged cases count but are never re-written); never clears a manual flag; register in `appinfo/info.xml`. +- [ ] 3.3 Add the `dpiaDetection` block (threshold, windowDays, groupBy) to the `dsarPolicyPack` schema and the seeded default pack (threshold 10, window 30 days, group by type + scope — pipelinq parity). +- [ ] 3.4 Declare the `dpiaFlagged` notification rule on `dataSubjectRequest` (updated trigger, `dpiaRequired` false→true boundary, pack-resolved privacy-officer recipient) so detection and manual flagging share one declared dispatch path (gate-18 posture: no imperative dispatch from the job). + +## 4. Tests + +- [ ] 4.1 PHPUnit (CI way: php:8.3-cli + OCP stubs): sweep skips terminal states / unchanged values / `now`-free schemas and writes exactly the changed tier; breach stamp written once; detection service grouping + threshold + normalisation + fail-safe no-pack matrices; detection job idempotency (no rewrite, no re-audit) and manual-flag ratchet. +- [ ] 4.2 Playwright e2e for the two @e2e flows (reminder→breach sweep journey incl. officer notification; DPIA seed-eleven→flag→no-duplicate→no-op-without-pack journey); Newman assertions on the case payload (`escalationTier`, `breachedAt`, `dpiaRequired`) after job runs. diff --git a/openspec/changes/dsar-integration-seams/.openspec.yaml b/openspec/changes/dsar-integration-seams/.openspec.yaml new file mode 100644 index 0000000000..43e65ca6e6 --- /dev/null +++ b/openspec/changes/dsar-integration-seams/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-03 diff --git a/openspec/changes/dsar-integration-seams/design.md b/openspec/changes/dsar-integration-seams/design.md new file mode 100644 index 0000000000..92c867482a --- /dev/null +++ b/openspec/changes/dsar-integration-seams/design.md @@ -0,0 +1,159 @@ +## Context + +ADR-047 rules that OpenRegister owns the AVG/DSAR case workflow and defines **two pluggable +integration seams** a leaf app binds: **identity verification** (NL → BSN/BRP/RvIG) and **regulator +escalation** (NL → AP complaint dossier). Phase-2's head `dsar-policy-pack-and-seams` (kind:config) +shipped the `dsarPolicyPack` config object whose two selector fields — `identityVerifyProvider` and +`regulatorEscalateProvider` — *name* which registered provider a jurisdiction binds per seam, and +whose default pack points both at an OR safe-default id. That head deliberately deferred the actual +seam machinery: a selector string is inert until an interface + registry + resolution + a default +provider exist. + +This change (the middle of the Phase-2 chain: policy-pack → **THIS** → `dsar-case-ui`) delivers that +machinery as code. Per ADR-031 an interface + registry + runtime resolution is genuinely imperative — +the legitimate ADR-019 registry exception; a schema cannot express a PHP interface — so this change is +`kind: code`. It is scoped to **two seams, generic only, no NL bindings**. The Phase-1 case engine +(`dsar-case-engine`) is the caller: it invokes identity-verify at the `verifying` lifecycle state and +regulator-escalate at denial/escalation, always **through the registry**, resolving the provider from +the active pack's selector. + +Constraints: fail-closed is the hard invariant (ADR-005 no fail-open, CWE-863). A missing, unset, or +unknown provider MUST resolve to a **refusing** default — identity is never auto-verified and +escalation is never silently skipped. No NL specifics (BSN/BRP/RvIG, AP) in OR core — those are +Phase-3 pipelinq providers registered into these registries. Stakeholders: the OR team (owns the two +seam contracts + registries + defaults), `dsar-case-ui` (consumes resolution state in the UI), +pipelinq (registers the NL leaf providers in Phase-3). + +## Goals / Non-Goals + +**Goals:** +- Define an `IdentityVerifyProvider` interface — verify a data-subject's identity for a case → + `verified` / `failed` / `needs-more` — and a `RegulatorEscalateProvider` interface — escalate/dossier + a case to a supervisory authority. +- Ship an `IdentityVerifyRegistry` and a `RegulatorEscalateRegistry`, each a shared per-request + service leaf apps register providers into via `addProvider()`, first-wins collision policy, mirroring + the existing OR `IntegrationRegistry` / `ObjectSourceRegistry`. +- Resolve, per seam, the active provider from the case's active `dsarPolicyPack` selector + (`identityVerifyProvider` / `regulatorEscalateProvider`) via the registry; an unset/unknown selector + falls back to the OR fail-closed default. +- Ship an OR **fail-closed default/null provider** per seam (identity default returns unverified; + regulator default refuses) registered at bootstrap so a fresh install always resolves *some* provider. +- Wire the Phase-1 case engine to call both seams through the registries (never a hardcoded provider). + +**Non-Goals:** +- No policy-pack config contract or selector fields — that is the head `dsar-policy-pack-and-seams`. +- No `AvgIndex.vue` / store / UI — that is the successor `dsar-case-ui`. +- No NL bindings: NL→BSN/BRP identity provider, NL→AP regulator provider — those are Phase-3 pipelinq + leaf providers registered *into* these registries; out of scope here. +- No new schema/register/migration/seed — these are code registries, not stored objects. The only + seed (the default pack) belongs to the head change. +- No change to the Phase-1 lifecycle graph shape — this change supplies the two call-outs the engine + makes at existing states. + +## Decisions + +### Declarative-vs-imperative decision (ADR-031) + +Default is declarative. This change is deliberately **imperative** for both seams — the ADR-019 / +ADR-031 §"PHP guards / registries remain a legitimate seam" registry exception. A PHP interface, +a registry that collects provider implementations, and runtime resolution of a selector-id → live +provider cannot be expressed as schema config; the *selection* (which provider) is declarative and +lives on the pack (head change), the *contract + resolution + refusal* is code. + +| Behaviour | Chosen path | Rationale | +|---|---|---| +| **Seam provider *selection*** (which provider a jurisdiction binds) | **Declarative — already delivered by the head** | Two string selector fields on `dsarPolicyPack`. The *choice* is config data. This change does not re-add them. | +| **`IdentityVerifyProvider` / `RegulatorEscalateProvider` interfaces** | **Imperative — PHP interface** | A provider contract (method signatures, verify/escalate result shape) is a PHP type; a schema cannot express an interface. Legitimate ADR-019 seam. | +| **`IdentityVerifyRegistry` / `RegulatorEscalateRegistry`** (collect providers, resolve by id, first-wins) | **Imperative — shared registry service** | Exactly mirrors OR's existing `IntegrationRegistry` (`lib/Service/Integration/IntegrationRegistry.php`) and `ObjectSourceRegistry` (`lib/Service/ObjectSource/ObjectSourceRegistry.php`): `addProvider()` from each app's bootstrap, single shared per-request instance, first-wins with a collision warning. ADR-019. | +| **Resolution** (pack selector id → live provider, fail-closed default when unset/unknown) | **Imperative — registry `resolve()` method** | Runtime dispatch on config-supplied id, with a refusing fallback. This is the ObjectSourceRegistry read-path pattern (resolve a provider by the id declared in config). | +| **OR fail-closed default / null providers** (identity ⇒ unverified, regulator ⇒ refuse) | **Imperative — default provider classes** | A refusing implementation of each interface, registered at bootstrap. The security invariant lives in code (CWE-863), not config. | +| **Case-engine call-out** (invoke seams at `verifying` / denial-escalation) | **Imperative — engine resolves + calls via registry** | The Phase-1 engine already owns the lifecycle transitions; this change adds the two registry-mediated calls at those points. Never a hardcoded provider. | + +### Registry pattern — grounded in the real OR code + +Both registries mirror the two existing OR registries verbatim in shape: +- `lib/Service/Integration/IntegrationRegistry.php` — `addProvider()`, `private array $providers` + keyed by id, first-registration-wins + `LoggerInterface` collision warning, registered as a **shared** + per-request service via `IRegistrationContext` so every app sees one instance (see its class + docblock: "single per-request service … so all apps see the same instance"). +- `lib/Service/ObjectSource/ObjectSourceRegistry.php` — the resolution reference: "resolves a provider + by the id declared in a schema's config … and delegates to it when the provider is enabled." Our + `resolve($selectorId)` returns the registered provider for the id, or the OR fail-closed default when + the id is unset or not registered. + +Bootstrap registration follows `lib/AppInfo/Application.php` where OR registers its built-in +`IntegrationProvider` implementations — the two new registries + their OR default providers register +the same way. Phase-1's `EvidenceSourceProvider` registry (from `dsar-case-engine`) is the closest +sibling and is the shape to reuse. + +### Reuse before build (ADR-011) + +Searched `lib/Service/Integration/`, `lib/Service/ObjectSource/`, `lib/Service/Lifecycle/`, +`lib/AppInfo/Application.php`, and the Phase-1 `dsar-case-engine` design before proposing: +- `IntegrationRegistry` / `ObjectSourceRegistry` — the exact registry+resolution shape to mirror; not + duplicated, followed. +- `EvidenceSourceProvider` registry (Phase-1 `dsar-case-engine`) — the sibling DSAR registry; same + bootstrap + first-wins pattern. +- `dsarPolicyPack` selector fields + default provider ids (head change) — resolution reads these; this + change does not re-declare them. +- The Phase-1 case engine's `verifying` state and denial/escalation transitions — the call-out sites; + this change adds the two registry-mediated calls, it does not reshape the lifecycle. + +### Fail-closed resolution (ADR-005, CWE-863) + +`resolve($selectorId)`: +1. selector id is registered → return that provider; +2. selector id unset OR not registered → return the OR fail-closed default provider (logs a warning), + NEVER null, NEVER a success shortcut. + +The identity default returns an **unverified/needs-more** result (a case is never auto-verified when +verification is unavailable). The regulator default **refuses** the escalation (an escalation is never +recorded as done when the seam is unbound). The caller (case engine) treats a null/absent provider as +impossible — resolution always yields a provider — so there is no `if ($provider !== null)` fail-open +branch (the `hydra-gate-unsafe-auth-resolver` anti-pattern). + +## Risks / Trade-offs + +- **[Registry with no leaf providers on a fresh install]** — only the OR defaults are registered, so + every case fails identity-verify closed. → Mitigation: that is the intended safe baseline; leaf + providers (Phase-3 pipelinq) bind real verification. The default pack's selectors already point at + the OR default ids, so resolution is well-defined from install. +- **[Selector points at an unregistered provider]** — a pack may name a provider id no registry has + (typo, uninstalled leaf). → Mitigation: `resolve()` falls back to the fail-closed default and logs; + never fails open. Contract stated in both seam specs. +- **[Silent fail-open regression]** — a future caller could treat "provider unavailable" as "skip the + check". → Mitigation: `resolve()` never returns null; the default is a refusing provider; a + fail-closed scenario is specced per seam and is security-review-relevant (called out in tasks). +- **[Interface churn as NL leaf lands]** — the Phase-3 NL provider might want a richer result shape. → + Mitigation: keep the interfaces narrow (verify → status enum; escalate → dossier ref + status); + additive method/field extensions are non-breaking for existing (default) implementations. +- **[Case-engine wiring depends on unarchived Phase-1 specs]** — the engine caller is specced in + `dsar-case-engine` (not yet archived). → Mitigation: the wiring point is expressed as ADDED + requirements on the two new seam capabilities here, not as a delta on Phase-1's spec. + +## Migration Plan + +Additive, code-only. New interfaces + registries + default providers under the DSAR service namespace; +two shared-service registrations + two default-provider registrations in `lib/AppInfo/Application.php` +(mirroring the existing `IntegrationRegistry` bootstrap); the Phase-1 case engine gains two +registry-mediated call sites. No schema, register, migration, or seed. Rollback is removing the +services + registrations and the two engine call sites. Because the OR defaults register at bootstrap +and the default pack already selects them, resolution is well-defined the moment this change lands even +with zero leaf providers. + +## Seed Data + +None. These are code registries, not stored OpenRegister objects — no new schema or register is added, +so no seed data is required. The only DSAR seed (the fail-closed **default policy pack**) is owned by +the head change `dsar-policy-pack-and-seams`. Any test fixtures use safe placeholders only (nil UUID +`00000000-0000-0000-0000-000000000000`, ``, `YOUR_TOKEN_HERE`); no realistic secrets/UUIDs. + +## Open Questions + +- Whether identity-verify runs synchronously at the `verifying` transition or as an async job when a + leaf provider calls an external scheme (BRP/RvIG). Recommended: interface returns a status (`verified` + / `failed` / `needs-more`) so both a synchronous and a deferred provider satisfy it; the async + orchestration is a leaf-provider concern, not the seam contract. Flagged in DEFERRED_QUESTIONS. +- Whether the two registries stay separate classes or share a small generic base. Recommended: two + explicit classes (matching `IntegrationRegistry` vs `ObjectSourceRegistry` staying separate) for + clarity and independent evolution; revisit only if a third seam appears. Flagged in DEFERRED_QUESTIONS. diff --git a/openspec/changes/dsar-integration-seams/proposal.md b/openspec/changes/dsar-integration-seams/proposal.md new file mode 100644 index 0000000000..7e198bb1de --- /dev/null +++ b/openspec/changes/dsar-integration-seams/proposal.md @@ -0,0 +1,88 @@ +--- +kind: code +depends_on: [dsar-policy-pack-and-seams] +--- + +## Why + +Phase-2's head `dsar-policy-pack-and-seams` (kind:config) added the `dsarPolicyPack` config contract, +including two **provider selector fields** (`identityVerifyProvider`, `regulatorEscalateProvider`) +that name which registered provider a jurisdiction binds for each integration seam. But a selector +string only names an id — nothing yet defines the seam **interfaces**, the **registries** leaf apps +register providers into, the **resolution** from a pack selector to a live provider, or the +**fail-closed default** provider that must answer when a seam is unbound. Per ADR-031 that surface is +genuinely code (a PHP interface + registry + runtime resolution — the legitimate ADR-019 registry +exception; a schema cannot express a PHP interface), and per ADR-047 the two seams — identity +verification and regulator escalation — are exactly the pluggable extension points OR **defines** and +a leaf app **binds** later. + +This is the **middle change of the Phase-2 chain**: policy-pack (config, the selectors) → +**THIS `dsar-integration-seams`** (code, the seam interfaces + registries + resolution + fail-closed +defaults) → `dsar-case-ui` (code, the `AvgIndex.vue` case-management surface). It ships the two seams +so the Phase-1 case engine calls identity-verify and regulator-escalate **through the registry** +(never a hardcoded provider), resolving the provider by the pack's selector, and fails **closed** +(CWE-863 / ADR-005) when a seam is unbound or names an unknown provider — a missing provider MUST NOT +silently skip an identity check. It defines NO NL bindings: pipelinq's NL→BSN/BRP identity provider +and NL→AP escalation provider are Phase-3, out of scope here. + +## What Changes + +- **Identity-verify seam** — an `IdentityVerifyProvider` interface (verify a data-subject's identity + → `verified` / `failed` / `needs-more`), an `IdentityVerifyRegistry` (leaf apps register providers, + first-wins collision policy, mirroring the existing OR `IntegrationRegistry`/`ObjectSourceRegistry` + pattern, ADR-019), resolution driven by the active policy pack's `identityVerifyProvider` selector, + and an OR-shipped **fail-closed default/null provider** (verification unavailable ⇒ NOT + auto-verified; returns a refusing/unverified result, never success). +- **Regulator-escalate seam** — a `RegulatorEscalateProvider` interface (escalate/dossier a case to a + supervisory authority), a `RegulatorEscalateRegistry`, resolution driven by the pack's + `regulatorEscalateProvider` selector, and an OR-shipped **fail-closed default/null provider** + (escalation unavailable ⇒ refuses, never a silent success). +- **Registry bootstrap + safe defaults** — both registries are registered as shared per-request + services in `lib/AppInfo/Application.php`, each with its OR safe-default provider registered so a + fresh install always resolves *some* provider; the default pack (from the head change) points both + selectors at these default ids. +- **Case-engine wiring point** — the Phase-1 case engine invokes identity-verify (at the + `verifying` lifecycle state) and regulator-escalate (at denial/escalation) **via the registry**, + resolving the provider from the active pack's selector; an unbound/unknown selector resolves to the + fail-closed default (refuse), never fail-open. + +**Explicitly out of scope:** the policy-pack config contract + selector fields (delivered by the +head `dsar-policy-pack-and-seams`), the `AvgIndex.vue` case-management UI (successor `dsar-case-ui`), +and all NL bindings — pipelinq's NL→BSN/BRP identity provider and NL→AP regulator provider (Phase-3 +`avg-consume-or-workflow`). This change defines the two generic seams; leaf apps register providers +into them later. + +## Capabilities + +### New Capabilities +- `dsar-identity-verify-seam`: the `IdentityVerifyProvider` interface, the identity-verify registry, + policy-pack-selector-driven resolution, and the OR fail-closed default provider that leaf identity + providers (NL→BSN/BRP) bind into later. +- `dsar-regulator-escalate-seam`: the `RegulatorEscalateProvider` interface, the regulator-escalate + registry, policy-pack-selector-driven resolution, and the OR fail-closed default provider that leaf + regulator providers (NL→AP) bind into later. + +### Modified Capabilities + + +## Impact + +- **New code (imperative, ADR-019/ADR-031 registry exception):** `lib/Service/Gdpr/Seam/` (or the + Phase-1 DSAR service namespace) — `IdentityVerifyProvider` + `RegulatorEscalateProvider` interfaces, + `IdentityVerifyRegistry` + `RegulatorEscalateRegistry`, and the two fail-closed default provider + classes. +- **Bootstrap:** `lib/AppInfo/Application.php` registers both registries as shared services and + registers each OR default provider (mirrors the existing `IntegrationRegistry`/`ObjectSourceRegistry` + bootstrap). +- **Case-engine wiring:** the Phase-1 case engine (`dsar-case-engine`) calls the two seams through + the registries, resolving providers from the active `dsarPolicyPack` selectors. +- **Depends on:** `dsar-policy-pack-and-seams` (the selector fields + default provider ids). **Consumed + by (later):** `dsar-case-ui`; NL leaf providers in Phase-3 pipelinq. +- **No new schema / register / migration.** These are code registries, not stored objects — no seed + data beyond the head change's default pack. +- **Security:** fail-closed is the invariant (ADR-005, CWE-863). A missing/unknown provider resolves + to a refusing default; identity verification is never auto-satisfied and escalation is never + silently skipped. diff --git a/openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md b/openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md new file mode 100644 index 0000000000..f7890a35d1 --- /dev/null +++ b/openspec/changes/dsar-integration-seams/specs/dsar-identity-verify-seam/spec.md @@ -0,0 +1,65 @@ +## ADDED Requirements + +### Requirement: OpenRegister defines an identity-verify provider interface +OpenRegister SHALL define an `IdentityVerifyProvider` interface that verifies a data-subject's identity for a DSAR case and returns exactly one of `verified`, `failed`, or `needs-more`. +The interface MUST be a narrow PHP contract (identify itself by a stable provider `id`, and verify-for-case → a status result), NOT a hard-coded verification service or an inline check in the case engine. It is the legitimate ADR-019 / ADR-031 registry-seam exception; leaf apps (e.g. pipelinq NL→BSN/BRP) implement it later and OUT of scope here. + +#### Scenario: The interface exposes a stable id and a verify result +- **WHEN** a provider implementing `IdentityVerifyProvider` is inspected +- **THEN** it MUST expose a stable provider `id` and a verify-for-case operation +- **AND** that operation MUST return exactly one of `verified`, `failed`, or `needs-more` + +#### Scenario: No hard-coded verification lives in the engine +- **WHEN** the case engine needs to verify a data-subject's identity +- **THEN** it MUST call an `IdentityVerifyProvider` obtained from the registry +- **AND** no jurisdiction-specific or hard-coded identity check MUST be embedded in the engine + +@e2e An administrator inspecting the identity-verify seam sees the provider contract exposing a stable id and a three-state verify result, with no built-in NL-specific check. + +### Requirement: Leaf apps register identity providers into an identity-verify registry +OpenRegister SHALL ship an `IdentityVerifyRegistry`, a shared per-request service into which any leaf app registers an `IdentityVerifyProvider` from its own bootstrap, following the existing OR `IntegrationRegistry`/`ObjectSourceRegistry` first-wins collision policy. +The registry MUST collect providers keyed by id, MUST accept the first registration of an id and log a warning on a duplicate id (first-wins), and MUST expose the registered providers so resolution can select one. Registration MUST mirror the existing OR registry bootstrap in `lib/AppInfo/Application.php` (ADR-019). + +#### Scenario: A leaf provider registers and is discoverable +- **WHEN** a leaf app registers an `IdentityVerifyProvider` with a unique id from its bootstrap +- **THEN** the registry MUST hold that provider under its id +- **AND** resolution MUST be able to select it by that id + +#### Scenario: Duplicate provider id is first-wins +- **WHEN** two providers register the same id +- **THEN** the first registration MUST win and the second MUST be rejected with a logged warning +- **AND** the registry MUST NOT silently replace the first provider + +@e2e A steward registering a second identity provider under an existing id sees the first-wins collision logged and the original provider retained. + +### Requirement: The identity provider resolves from the active policy pack selector +OpenRegister SHALL resolve the active identity provider for a case from the active `dsarPolicyPack`'s `identityVerifyProvider` selector via the registry, never from a hardcoded provider reference. +Resolution MUST look up the provider registered under the selector id and return it, so the case engine calls identity-verify through the registry using the jurisdiction's chosen provider. Changing the pack selector MUST change which provider verifies, with no PHP code change. + +#### Scenario: Verification uses the pack-selected provider +- **WHEN** a case's active pack sets `identityVerifyProvider` to a registered provider id +- **THEN** the engine MUST resolve that provider through the registry and use it to verify identity +- **AND** changing the selector to another registered provider MUST switch verification without a code change + +#### Scenario: Engine never calls a hardcoded provider +- **WHEN** the case engine reaches the `verifying` state +- **THEN** it MUST obtain the identity provider from the registry via the pack selector +- **AND** it MUST NOT reference any provider by a hardcoded class or id + +@e2e A steward points a jurisdiction pack's identityVerifyProvider at a different registered provider and observes a case verify through the newly selected provider without a redeploy. + +### Requirement: An unbound or unknown identity seam fails closed +OpenRegister SHALL resolve an unset or unknown `identityVerifyProvider` selector to an OR-shipped fail-closed default provider that returns unverified (`failed`/`needs-more`), never auto-verified and never null. +Resolution MUST NOT return null and MUST NOT treat "verification unavailable" as "identity verified" — a missing provider MUST NOT silently skip the identity check (ADR-005 no fail-open, CWE-863). The OR default provider MUST register at bootstrap so a fresh install always resolves a provider, and the default pack MUST select it. + +#### Scenario: Unset selector resolves to the fail-closed default +- **WHEN** the active pack leaves `identityVerifyProvider` unset +- **THEN** resolution MUST return the OR fail-closed default provider +- **AND** verifying a case with it MUST yield an unverified result (`failed` or `needs-more`), never `verified` + +#### Scenario: Unknown provider id does not skip the check +- **WHEN** the pack names an `identityVerifyProvider` id that is not registered +- **THEN** resolution MUST fall back to the fail-closed default rather than returning null +- **AND** the identity check MUST NOT be silently skipped or treated as satisfied + +@e2e On an install with no leaf identity provider bound, a steward advances a case to verifying and sees it fail closed (unverified) rather than auto-passing. diff --git a/openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md b/openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md new file mode 100644 index 0000000000..9636ad2bc0 --- /dev/null +++ b/openspec/changes/dsar-integration-seams/specs/dsar-regulator-escalate-seam/spec.md @@ -0,0 +1,65 @@ +## ADDED Requirements + +### Requirement: OpenRegister defines a regulator-escalate provider interface +OpenRegister SHALL define a `RegulatorEscalateProvider` interface that escalates or dossiers a DSAR case to a supervisory authority and returns the escalation outcome. +The interface MUST be a narrow PHP contract (identify itself by a stable provider `id`, and escalate-for-case → an escalation result carrying a regulator reference and a status), NOT a hard-coded escalation service or an inline call in the case engine. It is the legitimate ADR-019 / ADR-031 registry-seam exception; leaf apps (e.g. pipelinq NL→AP) implement it later and OUT of scope here. + +#### Scenario: The interface exposes a stable id and an escalate result +- **WHEN** a provider implementing `RegulatorEscalateProvider` is inspected +- **THEN** it MUST expose a stable provider `id` and an escalate-for-case operation +- **AND** that operation MUST return an outcome carrying a regulator reference and a status + +#### Scenario: No hard-coded escalation lives in the engine +- **WHEN** the case engine needs to escalate a case to a supervisory authority +- **THEN** it MUST call a `RegulatorEscalateProvider` obtained from the registry +- **AND** no jurisdiction-specific or hard-coded escalation target MUST be embedded in the engine + +@e2e An administrator inspecting the regulator-escalate seam sees the provider contract exposing a stable id and an escalate result, with no built-in AP-specific target. + +### Requirement: Leaf apps register regulator providers into a regulator-escalate registry +OpenRegister SHALL ship a `RegulatorEscalateRegistry`, a shared per-request service into which any leaf app registers a `RegulatorEscalateProvider` from its own bootstrap, following the existing OR `IntegrationRegistry`/`ObjectSourceRegistry` first-wins collision policy. +The registry MUST collect providers keyed by id, MUST accept the first registration of an id and log a warning on a duplicate id (first-wins), and MUST expose the registered providers so resolution can select one. Registration MUST mirror the existing OR registry bootstrap in `lib/AppInfo/Application.php` (ADR-019). + +#### Scenario: A leaf provider registers and is discoverable +- **WHEN** a leaf app registers a `RegulatorEscalateProvider` with a unique id from its bootstrap +- **THEN** the registry MUST hold that provider under its id +- **AND** resolution MUST be able to select it by that id + +#### Scenario: Duplicate provider id is first-wins +- **WHEN** two providers register the same id +- **THEN** the first registration MUST win and the second MUST be rejected with a logged warning +- **AND** the registry MUST NOT silently replace the first provider + +@e2e A steward registering a second regulator provider under an existing id sees the first-wins collision logged and the original provider retained. + +### Requirement: The regulator provider resolves from the active policy pack selector +OpenRegister SHALL resolve the active regulator provider for a case from the active `dsarPolicyPack`'s `regulatorEscalateProvider` selector via the registry, never from a hardcoded provider reference. +Resolution MUST look up the provider registered under the selector id and return it, so the case engine escalates through the registry using the jurisdiction's chosen provider. Changing the pack selector MUST change which provider escalates, with no PHP code change. + +#### Scenario: Escalation uses the pack-selected provider +- **WHEN** a case's active pack sets `regulatorEscalateProvider` to a registered provider id +- **THEN** the engine MUST resolve that provider through the registry and use it to escalate +- **AND** changing the selector to another registered provider MUST switch escalation without a code change + +#### Scenario: Engine never calls a hardcoded provider +- **WHEN** the case engine reaches a denial/escalation point +- **THEN** it MUST obtain the regulator provider from the registry via the pack selector +- **AND** it MUST NOT reference any provider by a hardcoded class or id + +@e2e A steward points a jurisdiction pack's regulatorEscalateProvider at a different registered provider and observes a case escalate through the newly selected provider without a redeploy. + +### Requirement: An unbound or unknown regulator seam fails closed +OpenRegister SHALL resolve an unset or unknown `regulatorEscalateProvider` selector to an OR-shipped fail-closed default provider that refuses the escalation, never a silent success and never null. +Resolution MUST NOT return null and MUST NOT treat "escalation unavailable" as "escalation done" — a missing provider MUST NOT silently skip the escalation (ADR-005 no fail-open, CWE-863). The OR default provider MUST register at bootstrap so a fresh install always resolves a provider, and the default pack MUST select it. + +#### Scenario: Unset selector resolves to the fail-closed default +- **WHEN** the active pack leaves `regulatorEscalateProvider` unset +- **THEN** resolution MUST return the OR fail-closed default provider +- **AND** escalating a case with it MUST refuse (report escalation not performed), never report success + +#### Scenario: Unknown provider id does not skip the escalation +- **WHEN** the pack names a `regulatorEscalateProvider` id that is not registered +- **THEN** resolution MUST fall back to the fail-closed default rather than returning null +- **AND** the escalation MUST NOT be silently skipped or recorded as done + +@e2e On an install with no leaf regulator provider bound, a steward escalates a case and sees it fail closed (escalation refused) rather than silently reported as done. diff --git a/openspec/changes/dsar-integration-seams/tasks.md b/openspec/changes/dsar-integration-seams/tasks.md new file mode 100644 index 0000000000..4369a9af6a --- /dev/null +++ b/openspec/changes/dsar-integration-seams/tasks.md @@ -0,0 +1,51 @@ +# Tasks — dsar-integration-seams (kind: code, depends_on: dsar-policy-pack-and-seams) + +Middle of the ADR-047 Phase-2 chain (policy-pack config → **THIS** code seams → `dsar-case-ui`). +This change adds two pluggable integration seams — identity-verify and regulator-escalate — as PHP +interfaces + registries + resolution + fail-closed defaults, mirroring the existing OR +`IntegrationRegistry`/`ObjectSourceRegistry` (ADR-019). NO NL bindings (BSN/BRP identity, AP +regulator = Phase-3 pipelinq), NO schema/register/migration/seed, NO Vue (that is `dsar-case-ui`). +Fail-closed is the security invariant (ADR-005, CWE-863). + +## 1. Identity-verify seam + +- [x] 1.1 Define the `IdentityVerifyProvider` PHP interface (stable provider `id` + verify-for-case → a status result of exactly `verified`/`failed`/`needs-more`) under the DSAR service namespace; SPDX/@license docblock + @spec tags per ADR-011/gate conventions. +- [x] 1.2 Add the `IdentityVerifyRegistry` shared per-request service (`addProvider()`, `private array $providers` keyed by id, first-wins collision + `LoggerInterface` warning) mirroring `lib/Service/Integration/IntegrationRegistry.php`. +- [x] 1.3 Add a `resolve($selectorId)` on the registry that returns the registered provider for the active `dsarPolicyPack` `identityVerifyProvider` selector, and returns the OR fail-closed default when the id is unset or not registered — MUST NOT return null. +- [x] 1.4 Add the OR fail-closed default `IdentityVerifyProvider` (returns unverified: `failed`/`needs-more`, never `verified`); SECURITY-REVIEW: it must never auto-verify and resolution must never fail open (CWE-863). + +## 2. Regulator-escalate seam + +- [x] 2.1 Define the `RegulatorEscalateProvider` PHP interface (stable provider `id` + escalate-for-case → an outcome carrying a regulator reference + status) under the DSAR service namespace; SPDX/@license docblock + @spec tags. +- [x] 2.2 Add the `RegulatorEscalateRegistry` shared per-request service (same `addProvider()` + first-wins + logger shape as the identity registry / `ObjectSourceRegistry`). +- [x] 2.3 Add a `resolve($selectorId)` on the registry driven by the pack's `regulatorEscalateProvider` selector, falling back to the OR fail-closed default when unset/unknown — MUST NOT return null. +- [x] 2.4 Add the OR fail-closed default `RegulatorEscalateProvider` (refuses: reports escalation not performed, never a silent success); SECURITY-REVIEW: escalation must never be silently skipped or recorded as done when the seam is unbound (CWE-863). + +## 3. Bootstrap + case-engine wiring + +- [x] 3.1 Register both registries as shared per-request services and register each OR fail-closed default provider at bootstrap in `lib/AppInfo/Application.php`, mirroring the existing `IntegrationRegistry`/built-in-provider registration. +- [x] 3.2 Wire the Phase-1 case engine (`dsar-case-engine`) to call identity-verify at the `verifying` lifecycle state via `IdentityVerifyRegistry::resolve(pack.identityVerifyProvider)` — never a hardcoded provider; the returned status drives the transition. +- [x] 3.3 Wire the Phase-1 case engine to call regulator-escalate at the denial/escalation point via `RegulatorEscalateRegistry::resolve(pack.regulatorEscalateProvider)` — never a hardcoded provider; SECURITY-REVIEW: no `if ($provider !== null)` fail-open branch (hydra-gate-unsafe-auth-resolver anti-pattern). + +## 4. Verification + +- [x] 4.1 Add unit tests: registration + first-wins collision per registry; resolve returns the pack-selected provider; resolve of unset/unknown selector returns the fail-closed default (never null); identity default = unverified; regulator default = refuse. +- [x] 4.2 Run the relevant Hydra gates (spdx-headers, forbidden-patterns, stub-scan, unsafe-auth-resolver, orphan-auth, spec-coverage gate-16, e2e-coverage gate-19) and `openspec validate --change dsar-integration-seams --strict`; fix any pre-existing issues touched. + +## Acceptance Criteria + +- Both seams ship a narrow PHP interface: `IdentityVerifyProvider` (verify → `verified`/`failed`/`needs-more`) and `RegulatorEscalateProvider` (escalate → regulator-reference + status). +- Each seam has a shared registry (first-wins collision, logged) that leaf apps register providers into from their own bootstrap, mirroring `IntegrationRegistry`/`ObjectSourceRegistry`. +- Resolution is driven by the active `dsarPolicyPack` selector (`identityVerifyProvider`/`regulatorEscalateProvider`); the case engine calls both seams through the registry, never a hardcoded provider. +- An unset or unknown selector resolves to the OR fail-closed default — identity default is unverified, regulator default refuses — and resolution never returns null (fail-closed, CWE-863). +- Both OR default providers register at bootstrap so a fresh install always resolves a provider; the default pack (head change) selects them. +- No NL bindings, no new schema/register/migration/seed, and no Vue are added by this change. + +## Quality Checklist + +- Imperative seam registries + interfaces are the justified ADR-019/ADR-031 registry exception, documented in design.md's declarative-vs-imperative table; the *selection* stays declarative on the pack (head change). +- Reused the existing OR `IntegrationRegistry`/`ObjectSourceRegistry` shape and the Phase-1 `EvidenceSourceProvider` sibling rather than inventing a new registry mechanism (ADR-011, ADR-022). +- Fail-closed is enforced in code (resolution never returns null; defaults refuse), specced with a fail-closed scenario per seam, and called out for security review (CWE-863, hydra-gate-unsafe-auth-resolver). +- Every new PHP file under lib/ carries SPDX/@license/@copyright + @spec tags (gate-16, spdx gate); no forbidden debug helpers or stub bodies. +- Behavioural spec scenarios carry `@e2e`; the case-management UI e2e lands with the successor `dsar-case-ui`. +- Any test fixtures use safe placeholders only (nil UUID `00000000-0000-0000-0000-000000000000`, ``, `YOUR_TOKEN_HERE`); no realistic-looking secrets/UUIDs (gitleaks). diff --git a/openspec/changes/dsar-policy-pack-and-seams/.openspec.yaml b/openspec/changes/dsar-policy-pack-and-seams/.openspec.yaml new file mode 100644 index 0000000000..43e65ca6e6 --- /dev/null +++ b/openspec/changes/dsar-policy-pack-and-seams/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-03 diff --git a/openspec/changes/dsar-policy-pack-and-seams/design.md b/openspec/changes/dsar-policy-pack-and-seams/design.md new file mode 100644 index 0000000000..a8bed4d366 --- /dev/null +++ b/openspec/changes/dsar-policy-pack-and-seams/design.md @@ -0,0 +1,205 @@ +## Context + +Phase-1 (`dsar-case-subsystem` config head + `dsar-case-engine` imperative successor) shipped a +generic, stateful data-subject-request workflow on the `dataSubjectRequest` register: an N-state +`x-openregister-lifecycle`, deadline-tracking `x-openregister-calculations`/`-aggregations`, and +reminder/escalation/breach `x-openregister-notifications`, plus the engine's evidence/bundle/ +redaction/denial-guard/retention-sweep surface. But Phase-1 deliberately left every +jurisdiction-specific *value* as a **provisional hard-coded default** — its own Open Questions call +out the escalation-tier boundaries (reminder T-7d / escalation T-2d / breach T+0), the +`denialGround` enum keys (generic, no wording), and the retention-window durations as candidate +Phase-2 policy-pack data. + +ADR-047 rules that **jurisdiction is data, not code**: a jurisdiction/tenant supplies its AVG/DSAR +policy as a **policy pack** (a config object) and binds two **integration seams** (identity-verify, +regulator-escalate). ADR-031 says such configuration is expressed as schema-declarative register +config, not as PHP service classes. This change authors the **policy-pack config contract** — the +`dsarPolicyPack` schema/register the Phase-1 declarative lifecycle/deadline/notification config reads +its now-provisional values from. It is `kind: config`: it adds a register JSON and a declarative +binding, no PHP. + +Constraints: the pack lives on an OR register, read/written through `ObjectService` (RBAC + +multitenancy); it must ship a **fail-closed default** so a fresh install works without any leaf-app +pack; and NL specifics (BSN/BRP/RvIG, AP) MUST NOT appear in OR core — they are Phase-3 pipelinq +data. + +## Goals / Non-Goals + +**Goals:** +- Add a `dsarPolicyPack` schema + register (`lib/Settings/dsar_policy_pack_register.json`) supplying: + deadline durations + escalation-tier thresholds, the denial-grounds enum **with jurisdiction + wording** (generic Phase-1 keys → label + statutory citation), retention windows (named window → + duration), intake channels, the DPO/FG role mapping (ADR-023), letter/notification template + references, and the two seam-provider selector fields (`identityVerifyProvider`, + `regulatorEscalateProvider`). Every property carries `title` + `description` (ADR-011). +- Bind Phase-1's `escalationTier` calculation, reminder/escalation/breach notification thresholds, + `denialGround` wording, and `retentionWindow` durations to resolve from the **active policy pack** + for the case's jurisdiction/tenant — declaratively, so no hard-coded threshold remains. +- Define the two integration-seam **contracts as config** (contract text + a `null`/default provider + identity per seam recorded in the pack), fail-closed to the OR-shipped safe default when unbound. +- Seed a jurisdiction-neutral **default** pack (fail-closed working baseline) and an illustrative + NL-shaped **example** pack, with safe placeholders only. + +**Non-Goals:** +- No `IdentityVerifyProvider`/`RegulatorEscalateProvider` interfaces, ADR-019 registries, resolution + code, or null/default provider classes — that is the successor `dsar-integration-seams` + (`kind: code`, `depends_on` this). +- No `AvgIndex.vue`/store extension — that is the successor `dsar-case-ui` (`kind: code`, + `depends_on` this + seams). +- No NL bindings (BSN/BRP/RvIG identity, AP-complaint escalation) and no real NL policy values — that + is Phase-3 `avg-consume-or-workflow` (pipelinq). +- No change to Phase-1's lifecycle/aggregation *structure* — this change supplies values, it does not + reshape the graph. + +## Decisions + +### Declarative-vs-imperative decision (ADR-031) + +Default is declarative (schema register). This change authors **only the declarative column**; the +seam registries and UI are scoped into the successor code changes and listed here so the reviewer +sees the whole Phase-2 picture. + +| Behaviour | Chosen path | Rationale | +|---|---|---| +| **Policy-pack object** (deadline durations, escalation thresholds, retention windows, intake channels, DPO/FG role map, template refs) | **Declarative** — a `dsarPolicyPack` schema/register | These are pure configuration *values* per jurisdiction/tenant — the exact ADR-031 case for a schema-config object over a `PolicyService`. Gets RBAC, multitenancy, audit trail, versioning, MCP discovery for free via `ObjectService`. Zero PHP. | +| **Denial-grounds enum with wording** | **Declarative** — array-of-objects property on the pack (generic key ← Phase-1 → label + statutory citation) | Phase-1 ships the generic enum *keys*; the *wording* is jurisdiction data. Mapping key→label+citation is a config table, not logic. | +| **Binding Phase-1 thresholds/enum to the pack** | **Declarative** — Phase-1 `escalationTier` calculation + `-notifications` + `denialGround`/`retentionWindow` resolve their values from the active pack | A calculation/notification referencing another object's field is exactly what `x-openregister-calculations`/`@ref` express (ADR-031 declarative cross-object calc). No resolver service. | +| **Which pack is "active" for a case** (jurisdiction/tenant selection) | **Declarative** — a `jurisdiction`/`tenant` key on the pack + the case's tenant scope selects it | Selection is a scoped lookup the object layer already does (multitenancy); no dispatch code. | +| **Seam provider *selection*** (`identityVerifyProvider`/`regulatorEscalateProvider` = which registered provider a jurisdiction binds) | **Declarative** — two string selector fields on the pack, defaulting to the safe-default provider id | The *choice* of provider is config; the pack names a provider id. Fail-closed default id when unbound. | +| **Seam *interface* + *registry* + *resolution*** (`IdentityVerifyProvider`/`RegulatorEscalateProvider` contracts, the ADR-019 registries, resolving a selector id → provider, the null/default fail-closed providers) | **Imperative — deferred to `dsar-integration-seams`** | An interface + registry + runtime resolution is the legitimate ADR-019 registry exception (like the Phase-1 `EvidenceSourceProvider` registry); a schema can't express a PHP interface. Specced in the successor code change. | +| **Case-management UI** (list/detail/transition/evidence/bundle/deny driven by the pack) | **Frontend — deferred to `dsar-case-ui`** | Vue view + store per ADR-004; not a declarative or backend construct. | + +### Reuse before build (ADR-011) + +Searched `lib/Settings/`, `lib/Service/Gdpr/`, `lib/Formats/`, `src/store/modules/` before proposing: + +- **`data_subject_request_register.json`** (Phase-1) — this change binds to its `escalationTier` + calculation, notification thresholds, `denialGround` enum keys, and `retentionWindow` selector; it + does not duplicate them, it supplies their values. +- **`decidesk_register.json` / existing OR register JSONs** — the `dsar_policy_pack_register.json` + follows the same register+schema+`x-openregister-*` fold-at-import shape; no new import path. +- **`ObjectService` (RBAC + multitenancy), `SchemaService`, `RegisterService`** — the pack is a plain + OR object served by the existing object APIs; no custom Entity/Mapper (ADR-001). +- **`AuditTrailMapper` + hash chain** — pack edits are audited through the existing immutable trail. +- **Phase-1 `EvidenceSourceProvider` registry pattern** — the successor `dsar-integration-seams` + registries mirror it (noted so that author reuses the shape, not this change). + +### Object / schema service patterns (ADR-001, ADR-003) + +The policy pack is an OR object under a `dsar-policy-packs` register / `dsarPolicyPack` schema. +Reads/writes flow through `ObjectService`. `SchemaService`/`RegisterService` own the definition loaded +from the register JSON. The `x-openregister-*` blocks fold into the schema `configuration` at import, +the same path Phase-1 uses. + +### Fail-closed default + +A jurisdiction-neutral **default** pack ships as seed data so a fresh install has a working baseline: +generic ground labels, conservative thresholds, and both seam selectors set to the OR-shipped +safe-default (fail-closed) provider id. When no jurisdiction pack is bound, the default applies and +identity-verify / regulator-escalate resolve to a provider that **refuses** (Phase-1/seams fail-closed +posture, CWE-863) rather than silently succeeding — the actual refusal is enforced by the successor +seams change; this change records the default selector id. + +## Risks / Trade-offs + +- **[Config head with no runnable seams/UI]** — this change adds the pack schema but the seam + registries and the UI that consume it land in the two successor changes. → Mitigation: the three are + a `depends_on` chain; the pack is exercisable via OR's object APIs the moment it imports, and + Phase-1's declarative config immediately reads real values from it. +- **[Phase-1 binding references a not-yet-populated pack]** — Phase-1 calculations/notifications + resolve from the active pack; before any pack exists they must not break. → Mitigation: ship the + default pack as seed so an active pack always exists; Phase-1's provisional defaults remain the + fallback if resolution finds no pack (documented, non-breaking). +- **[Seam selector points at an unregistered provider]** — a pack may name a provider id no registry + has. → Mitigation: the default selector id is the OR safe-default (always registered by the seams + change); an unknown id fails closed (refuse), never fails open — enforced in the seams change, + contract stated here. +- **[Jurisdiction wording drift]** — statutory citations in the example pack could go stale. → + Mitigation: the shipped NL-shaped pack is explicitly an *illustrative example* with placeholder + citations; the authoritative NL wording is Phase-3 pipelinq data, not OR core. +- **[Schema addition is a migration]** — a new register/schema. → Mitigation: it is a **new** register, + additive; it removes/renames nothing on the Phase-1 register (the binding adds calculation/reference + fields only, all optional). No repair-step migration; register import UNION-merges additively. +- **[Seed data leaking realistic values]** — packs carry role ids and provider ids. → Mitigation: nil + UUID, `YOUR_TOKEN_HERE`, `` placeholders; no realistic-looking secrets/UUIDs (gitleaks). + +## Seed Data + +A new `dsarPolicyPack` schema is added, so realistic seed packs are provided, using **safe +placeholders** (nil UUID `00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`, ``, +``). Seeded via the OR seed path. + +**Pack 1 — Default (jurisdiction-neutral, fail-closed working baseline):** +```json +{ + "id": "00000000-0000-0000-0000-000000000000", + "name": "Default AVG/DSAR policy pack", + "jurisdiction": "default", + "deadlineDurationDays": 30, + "extensionDurationDays": 60, + "escalationTiers": [ + { "tier": "reminder", "offsetDays": -7 }, + { "tier": "escalation", "offsetDays": -2 }, + { "tier": "breach", "offsetDays": 0 } + ], + "denialGrounds": [ + { "key": "manifestly-unfounded", "label": "Manifestly unfounded or excessive request", "citation": "GDPR Art 12(5)" }, + { "key": "third-party-data", "label": "Would adversely affect third-party rights", "citation": "GDPR Art 15(4)" }, + { "key": "overriding-legitimate-grounds", "label": "Compelling legitimate grounds override the objection", "citation": "GDPR Art 21(1)" } + ], + "retentionWindows": [ + { "key": "standard", "durationMonths": 12 }, + { "key": "extended", "durationMonths": 24 } + ], + "intakeChannels": ["web-form", "email", "post"], + "roleMapping": { "dpo": "", "handler": "" }, + "templates": { "acknowledgement": "template:ack-default", "denial": "template:denial-default" }, + "identityVerifyProvider": "or.default.identity-verify.null", + "regulatorEscalateProvider": "or.default.regulator-escalate.null" +} +``` + +**Pack 2 — NL-shaped illustrative example (reference only; real NL data is Phase-3 pipelinq):** +```json +{ + "id": "00000000-0000-0000-0000-000000000000", + "name": "Voorbeeld NL AVG-beleidspakket (illustratief)", + "jurisdiction": "nl-example", + "deadlineDurationDays": 30, + "extensionDurationDays": 60, + "escalationTiers": [ + { "tier": "reminder", "offsetDays": -7 }, + { "tier": "escalation", "offsetDays": -3 }, + { "tier": "breach", "offsetDays": 0 } + ], + "denialGrounds": [ + { "key": "manifestly-unfounded", "label": "Kennelijk ongegrond of buitensporig verzoek", "citation": "AVG art. 12 lid 5 (illustratief)" } + ], + "retentionWindows": [ + { "key": "boekhoudplicht", "durationMonths": 84 } + ], + "intakeChannels": ["web-form", "email"], + "roleMapping": { "fg": "", "handler": "" }, + "templates": { "denial": "template:denial-nl-example" }, + "identityVerifyProvider": "", + "regulatorEscalateProvider": "" +} +``` + +## Migration Plan + +Additive only. A **new** `dsar-policy-packs` register/schema is added; the declarative binding on the +Phase-1 `data_subject_request_register.json` adds calculation/reference fields only (all optional), +removing/renaming nothing. No repair-step migration is required — register import UNION-merges the new +register and the additive binding. Rollback is reverting the two register JSONs. The seed packs are +illustrative and can be re-seeded or removed independently. + +## Open Questions + +- Whether the "active pack" is selected purely by tenant scope, or also by an explicit + `jurisdiction` field on the case (recommended: tenant scope, with an optional per-case override) — + final selection semantics deferred to the seams/UI changes that consume the resolution. +- Whether letter/notification templates are `template:` references resolved by a document leaf + (recommended, ADR-022 content-in-leaves) vs inline strings on the pack — provisional as references. +- Whether the two seam selector fields belong on the pack (recommended, keeps binding declarative) or + in a separate bindings object — deferred to the `dsar-integration-seams` author. diff --git a/openspec/changes/dsar-policy-pack-and-seams/proposal.md b/openspec/changes/dsar-policy-pack-and-seams/proposal.md new file mode 100644 index 0000000000..d10b451058 --- /dev/null +++ b/openspec/changes/dsar-policy-pack-and-seams/proposal.md @@ -0,0 +1,95 @@ +--- +kind: config +depends_on: [dsar-case-subsystem, dsar-case-engine] +--- + +## Why + +Phase-1 (`dsar-case-subsystem` + `dsar-case-engine`) gave OpenRegister a generic, stateful +data-subject-request case workflow, but it left every jurisdiction-specific value **hard-coded as a +provisional default**: escalation-tier boundaries, the denial-ground enum keys, retention-window +durations, intake channels, and the DPO/FG role mapping all live inside the register JSON and the +engine change with placeholder values (see `dsar-case-subsystem` Open Questions). ADR-047 rules +that **jurisdiction is data, not code**: a jurisdiction or tenant must be able to supply its AVG/DSAR +policy as a **policy pack** (a config object) and bind two **integration seams** — identity +verification and regulator escalation — without touching OpenRegister core. This change defines the +policy-pack config contract that the Phase-1 declarative lifecycle/deadline/notification config +**reads from**, so that the provisional Phase-1 defaults become policy-pack-driven data. + +This is the **head of the Phase-2 chain** and is deliberately `kind: config` only. Per ADR-032 a +`mixed` change (declarative config + imperative registries + Vue UI) is an anti-pattern, so Phase-2 +is split into three chained changes: this policy-pack **contract/schema** (config), then +`dsar-integration-seams` (the two ADR-019 registries + interfaces + default providers, `kind: code`, +`depends_on` this), then `dsar-case-ui` (the `AvgIndex.vue` full case-management extension, +`kind: code`, `depends_on` both). This change authors the head-of-chain (config) artifacts only; the +seam registries and the UI are specced in their own successor changes. The split decision is flagged +for the human in DEFERRED_QUESTIONS. + +## What Changes + +- **Add a `dsarPolicyPack` schema + register** (`lib/Settings/dsar_policy_pack_register.json`): a + per-jurisdiction/tenant config object supplying — deadline durations + escalation thresholds + (reminder/escalation/breach tier boundaries the Phase-1 `escalationTier` calculation and + reminder/escalation/breach notifications consume), the denial-grounds enum **with jurisdiction + wording** (generic ground keys from Phase-1 mapped to human-readable labels + statutory citation), + retention windows (named window → duration, feeding Phase-1 `retentionWindow`/`retainUntil`), + intake channels, the DPO/FG role mapping (ties to ADR-023 action-level authorization), and + letter/notification template references. Every property carries a human-friendly `title` + + `description` (ADR-011). +- **Bind the Phase-1 declarative config to the policy pack** (declarative reference, not code): the + Phase-1 `x-openregister-calculations` (`escalationTier`), `-notifications` (reminder/escalation/ + breach thresholds), and the `denialGround` enum + `retentionWindow` selectors resolve their + now-provisional values from the active `dsarPolicyPack` object for the case's jurisdiction/tenant, + so the boundaries become **data**. No hard-coded threshold remains in the register JSON. +- **Define the two integration-seam CONTRACTS as config** (the contract text + a `null`/default + provider identity per seam recorded in the policy pack) — `identityVerifyProvider` and + `regulatorEscalateProvider` fields on the policy pack naming which registered provider a + jurisdiction binds, defaulting to the OR-shipped safe-default (fail-closed) provider when unbound. + The interfaces + registries + resolution themselves are **imperative** and specced in the + successor `dsar-integration-seams` change. +- **Seed data**: a realistic default (jurisdiction-neutral) policy pack plus an illustrative NL-shaped + example pack, using safe placeholders — so a fresh install has a working, fail-closed default and a + reference for what a leaf-app jurisdiction pack looks like. + +**Explicitly out of scope — successor Phase-2 chain changes (specced separately, `depends_on` this):** +`dsar-integration-seams` (`kind: code`) — the `IdentityVerifyProvider` and `RegulatorEscalateProvider` +interfaces, their ADR-019 registries, resolution, and the OR-shipped null/default (fail-closed) +providers; `dsar-case-ui` (`kind: code`) — extending `src/views/avg/AvgIndex.vue` + the `avg` store +module to full case-management (list/detail/transition/evidence/bundle/deny) driven by the policy +pack. **Explicitly out of scope — Phase 3 `avg-consume-or-workflow` (pipelinq):** the actual NL +bindings (BSN/BRP/RvIG identity, AP-complaint escalation) and the NL policy-pack values. OR ships +only the contract + safe defaults; NL data + bindings live in the pipelinq consumer. + +## Capabilities + +### New Capabilities +- `dsar-policy-pack`: the `dsarPolicyPack` schema/register config contract — deadline durations + + escalation thresholds, denial-grounds enum with jurisdiction wording, retention windows, intake + channels, DPO/FG role mapping, letter/notification templates, and the two seam-provider selector + fields — expressed declaratively (ADR-031), with a fail-closed default pack, and consumed by the + Phase-1 declarative lifecycle/deadline/notification config. + +### Modified Capabilities + +- _none_ + +## Impact + +- **Config (this change)**: new `lib/Settings/dsar_policy_pack_register.json` (schema + register + + seed default/example packs); a declarative binding on `lib/Settings/data_subject_request_register.json` + so Phase-1 `escalationTier`/notifications/`denialGround`/`retentionWindow` resolve from the active + policy pack. +- **Consumes (from Phase-1, unchanged)**: the `dataSubjectRequest` register, its + `x-openregister-lifecycle`/`-calculations`/`-aggregations`/`-notifications`, the `denialGround` enum + keys, and the `retentionWindow` selector — this change supplies their values, it does not alter their + structure. +- **APIs**: no new routes — the policy pack is served by OR's existing object APIs; leaf apps read/write + packs through `ObjectService` (RBAC + multitenancy). +- **Downstream (successor context, not specced here)**: `dsar-integration-seams` and `dsar-case-ui` + `depends_on` this; pipelinq's `avg-consume-or-workflow` (Phase 3) supplies the NL policy pack + the + two NL seam bindings and retires its app-local AVG surface. No app is migrated by this change. diff --git a/openspec/changes/dsar-policy-pack-and-seams/specs/dsar-policy-pack/spec.md b/openspec/changes/dsar-policy-pack-and-seams/specs/dsar-policy-pack/spec.md new file mode 100644 index 0000000000..faf51fbbb6 --- /dev/null +++ b/openspec/changes/dsar-policy-pack-and-seams/specs/dsar-policy-pack/spec.md @@ -0,0 +1,105 @@ +## ADDED Requirements + +### Requirement: Policy pack is a declarative per-jurisdiction config object +OpenRegister SHALL express the AVG/DSAR policy pack as a declarative `dsarPolicyPack` schema on a +dedicated register (`lib/Settings/dsar_policy_pack_register.json`), NOT as a PHP `PolicyService` or +hard-coded constants (ADR-031). A pack SHALL supply, as configuration data: deadline durations, +escalation-tier thresholds, the denial-grounds enum with jurisdiction wording, retention windows, +intake channels, the DPO/FG role mapping, letter/notification template references, and the two +integration-seam provider selectors. Every schema property MUST declare a human-friendly `title` and +`description` (ADR-011). Packs MUST be read and written through `ObjectService` (RBAC + +multitenancy), never via a custom Entity/Mapper (ADR-001), and every pack edit MUST be recorded in +the existing immutable hash-chained audit trail (`AuditTrailMapper`). + +#### Scenario: A pack is stored as a declarative config object +- **WHEN** a jurisdiction pack is created on the `dsar-policy-packs` register +- **THEN** it MUST be a plain OpenRegister object served by the existing object APIs +- **AND** no bespoke PHP policy service or hard-coded threshold constant MUST be introduced to hold its values + +#### Scenario: Every pack property is human-labelled +- **WHEN** the `dsarPolicyPack` schema is imported +- **THEN** every property (including nested items of the denial-grounds, escalation-tier, and retention-window collections) MUST carry a `title` and a `description` + +@e2e An administrator opens the policy-pack surface, views the default pack's deadline/escalation/denial/retention values, and each field shows its human-readable title. + +### Requirement: Deadline and escalation thresholds resolve from the active pack +The Phase-1 escalation-tier calculation and the reminder/escalation/breach notifications on the `dataSubjectRequest` case SHALL resolve their tier boundaries from the active `dsarPolicyPack`. +Resolution MUST be declarative (a cross-object calculation reference) for the case's jurisdiction or +tenant, so that NO escalation-tier boundary remains hard-coded in the register JSON. When no +jurisdiction pack is bound, the default pack MUST supply the values, and the case MUST NOT be left +without a resolvable threshold. + +#### Scenario: Escalation boundaries come from the pack, not code +- **WHEN** a jurisdiction pack sets its escalation tier boundaries (e.g. reminder T-7d, escalation T-3d, breach T+0) +- **THEN** the case's `escalationTier` calculation and reminder/escalation/breach notifications MUST use those pack-supplied boundaries +- **AND** changing the boundaries in the pack MUST take effect without any PHP code change + +#### Scenario: Default pack applies when no jurisdiction pack is bound +- **WHEN** a case has no bound jurisdiction pack +- **THEN** the default pack's escalation thresholds MUST apply +- **AND** the case MUST always resolve a threshold (never an unresolved/empty boundary) + +@e2e A steward changes an escalation-tier boundary on a jurisdiction pack and observes a case's escalation state recompute against the new boundary without a redeploy. + +### Requirement: Denial-ground wording and retention windows are supplied by the pack +The pack SHALL map each generic Phase-1 `denialGround` key to a jurisdiction `label` and statutory +`citation`, and SHALL define named retention windows (window key → duration) that supply the Phase-1 +`retentionWindow`/`retainUntil` values. The Phase-1 `denialGround` enum keys and `retentionWindow` +selector MUST NOT carry jurisdiction wording or durations in the register JSON — those MUST come from +the active pack. No existing Phase-1 lifecycle transition or denial guard behaviour is altered by +this resolution. + +#### Scenario: A denial ground shows jurisdiction wording from the pack +- **WHEN** a handler selects a `denialGround` key on a case whose active pack maps that key to a label and citation +- **THEN** the label and statutory citation MUST come from the pack +- **AND** the register JSON MUST NOT hold jurisdiction-specific denial wording + +#### Scenario: A retention window resolves its duration from the pack +- **WHEN** a case selects a named `retentionWindow` (e.g. `standard`) +- **THEN** the window's duration MUST be read from the active pack and applied to `retainUntil` +- **AND** changing the window duration in the pack MUST change resolution without a code change + +@e2e A steward selects a denial ground on a case and sees the pack's jurisdiction label and citation, then selects a retention window and sees the pack-defined duration applied. + +### Requirement: Integration-seam provider selection is fail-closed config +The pack SHALL name which registered provider a jurisdiction binds for each of the two integration +seams via an `identityVerifyProvider` and a `regulatorEscalateProvider` selector field. When a +selector is unset, or names a provider that is not registered, resolution MUST fall back to the +OR-shipped safe-default (fail-closed) provider — it MUST NOT fail open. The default pack MUST set both +selectors to the OR safe-default provider id. This change supplies the selector fields and default +ids as configuration; the provider interfaces, registries, and resolution are delivered by the +successor `dsar-integration-seams` change. + +#### Scenario: An unbound seam defaults to the fail-closed provider +- **WHEN** a pack leaves `identityVerifyProvider` or `regulatorEscalateProvider` unset +- **THEN** the selector MUST resolve to the OR safe-default provider id +- **AND** the safe-default MUST be a fail-closed (refusing) provider, never a fail-open success + +#### Scenario: An unknown provider id fails closed +- **WHEN** a pack names a seam provider id that is not registered +- **THEN** resolution MUST fall back to the fail-closed safe-default +- **AND** it MUST NOT silently proceed as if the seam were satisfied + +@e2e An administrator inspects the default pack and sees both seam selectors set to the OR safe-default fail-closed provider id. + +### Requirement: A fail-closed default pack ships as seed +OpenRegister SHALL ship a jurisdiction-neutral **default** `dsarPolicyPack` as seed data so a fresh +install has a working, fail-closed baseline (generic denial-ground labels, conservative thresholds, +both seam selectors set to the OR safe-default provider), plus an illustrative NL-shaped **example** +pack for reference. All seeded packs MUST use safe placeholders only (nil UUID +`00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`, ``, ``); no +realistic-looking secrets, UUIDs, or authoritative jurisdiction citations. The NL-shaped pack MUST be +marked illustrative — the authoritative NL policy values are supplied by the Phase-3 pipelinq consumer, +not OR core. + +#### Scenario: Fresh install has a working default pack +- **WHEN** OpenRegister is installed and seeded +- **THEN** a jurisdiction-neutral default pack MUST exist with resolvable deadline/escalation/denial/retention values +- **AND** its two seam selectors MUST point at the OR safe-default fail-closed provider + +#### Scenario: Seed packs use only safe placeholders +- **WHEN** the seed packs are inspected +- **THEN** every id, token, role, and provider reference MUST be an obvious placeholder (nil UUID, `YOUR_TOKEN_HERE`, ``, ``) +- **AND** no realistic-looking secret or authoritative jurisdiction citation MUST be present + +@e2e On a fresh seeded install an administrator sees a default policy pack listed with a fail-closed configuration and an illustrative NL example pack. diff --git a/openspec/changes/dsar-policy-pack-and-seams/tasks.md b/openspec/changes/dsar-policy-pack-and-seams/tasks.md new file mode 100644 index 0000000000..2555eeee75 --- /dev/null +++ b/openspec/changes/dsar-policy-pack-and-seams/tasks.md @@ -0,0 +1,45 @@ +# Tasks — dsar-policy-pack-and-seams (kind: config, depends_on: dsar-case-subsystem, dsar-case-engine) + +Head of the ADR-047 Phase-2 chain. This change patches register JSON only — it adds the +`dsarPolicyPack` schema/register and binds Phase-1's declarative thresholds/wording/windows to it. +No PHP services, registries, controllers, routes, or Vue — the two integration-seam registries and +the `AvgIndex.vue` extension land in the successor changes `dsar-integration-seams` (kind:code) and +`dsar-case-ui` (kind:code). See DEFERRED_QUESTIONS for the split decision. + +## 1. Policy-pack schema + register (config) + +- [x] 1.1 Add `lib/Settings/dsar_policy_pack_register.json` — a `dsar-policy-packs` register + `dsarPolicyPack` schema with a `jurisdiction`/tenant key, `deadlineDurationDays`, `extensionDurationDays`, an `escalationTiers` collection (tier + offsetDays), `intakeChannels`, `roleMapping` (dpo/fg/handler), and `templates` (references); every property + nested item property carries a `title` + `description` (ADR-011). +- [x] 1.2 Add the `denialGrounds` collection (generic `key` ← Phase-1 → jurisdiction `label` + statutory `citation`) and the `retentionWindows` collection (`key` → duration) to the `dsarPolicyPack` schema, with `title`/`description` on every item property. +- [x] 1.3 Add the two integration-seam selector fields `identityVerifyProvider` and `regulatorEscalateProvider` (string provider ids, fail-closed to the OR safe-default id when unset/unknown), with `title`/`description`. + +## 2. Bind Phase-1 to the pack (declarative) + +- [x] 2.1 Bind the Phase-1 `escalationTier` `x-openregister-calculations` and the reminder/escalation/breach `x-openregister-notifications` on `data_subject_request_register.json` to resolve their tier boundaries from the active pack via a declarative cross-object reference (ADR-031) — remove every hard-coded threshold from the register JSON, falling back to the default pack when no jurisdiction pack is bound. +- [x] 2.2 Bind the Phase-1 `denialGround` enum wording and the `retentionWindow` durations to resolve from the active pack; ensure the register JSON holds only the generic keys/selectors, not jurisdiction wording or durations, and that no existing Phase-1 lifecycle transition or denial guard is altered. + +## 3. Seed data + +- [x] 3.1 Seed a jurisdiction-neutral **default** `dsarPolicyPack` (generic denial labels, conservative thresholds, retention windows, both seam selectors set to the OR safe-default fail-closed provider id) and an illustrative **NL-shaped example** pack — per the design Seed Data section, safe placeholders only (nil UUID `00000000-0000-0000-0000-000000000000`, `YOUR_TOKEN_HERE`, ``, ``). + +## 4. Verification + +- [x] 4.1 Validate both register JSONs parse and import cleanly (`x-openregister-*` blocks fold into `configuration`); confirm the Phase-1 register change is additive (calculation/reference fields only, all optional; no existing required property removed/renamed) and the new register is standalone/additive. +- [x] 4.2 Run the config-relevant Hydra gates (`notification-dialect` gate-18, `schema-property-titles` gate-28) and `openspec validate --change dsar-policy-pack-and-seams --strict`; fix any pre-existing issues touched. + +## Acceptance Criteria + +- The `dsarPolicyPack` schema/register supplies deadline durations, escalation-tier thresholds, denial-grounds enum with jurisdiction wording, retention windows, intake channels, DPO/FG role mapping, template references, and the two seam selectors — all human-labelled (ADR-011). +- Phase-1's `escalationTier` calculation, reminder/escalation/breach notifications, `denialGround` wording, and `retentionWindow` durations resolve from the active pack; no hard-coded threshold/wording/duration remains in the register JSON. +- Seam provider selection is fail-closed: an unset or unknown selector resolves to the OR safe-default fail-closed provider id; the default pack sets both selectors to that id. +- A jurisdiction-neutral default pack seeds a working, fail-closed baseline; an illustrative NL-shaped example pack is present and marked illustrative. +- No PHP service/registry/controller/route and no Vue is added by this change; both register JSON changes are additive and non-breaking. + +## Quality Checklist + +- Declarative-first: the pack + the Phase-1 binding are register config, not new Service classes (ADR-031); the seam interfaces/registries/resolution and the UI are correctly deferred to `dsar-integration-seams` / `dsar-case-ui`. +- Reused the Phase-1 `data_subject_request_register.json` (escalationTier/notifications/denialGround/retentionWindow) and `ObjectService`/audit-trail rather than duplicating them (ADR-011, ADR-022); the binding supplies values, it does not reshape the graph. +- Canonical `x-openregister-notifications` dialect only; no obsolete legacy notification fields (gate-18). +- Every added property (including nested collection item properties) declares `title` + `description` (ADR-011, gate-28). +- Fail-closed posture on the two seam selectors (unset/unknown → refusing safe-default, never fail-open) is stated in the config and spec (CWE-863). +- Behavioural spec scenarios carry `@e2e` and are referenced by Playwright e2e (or a reason-bearing `@e2e exclude`); the seam registries + UI e2e land with their own successor changes. +- Seed and example ids/tokens use obvious placeholders (nil UUID, `YOUR_TOKEN_HERE`, ``, ``) and no authoritative jurisdiction citations — no realistic-looking secrets/UUIDs (gitleaks). diff --git a/openspec/changes/edepot-transfer/proposal.md b/openspec/changes/edepot-transfer/proposal.md new file mode 100644 index 0000000000..0ac8dbe59a --- /dev/null +++ b/openspec/changes/edepot-transfer/proposal.md @@ -0,0 +1,78 @@ +# Proposal: edepot-transfer + +## Summary + +Implement the ability to transfer OpenRegister objects and their associated files to e-Depot (regional digital archive) systems, with full MDTO/TMLO metadata compliance and durable format conversion (PDF/A, ODF). This enables Dutch government organisations to meet their legal obligation to transfer permanent records to a regional or national archive. + +## Demand Evidence + +**Cluster: e-Depot (digital archive)** -- 117 tenders, 247 requirements +**Cluster: MDTO (metadata standard)** -- 98 tenders, 196 requirements +**Cluster: TMLO (metadata local government)** -- 58 tenders, 104 requirements +**Combined**: 273 tenders, 547 requirements (with overlap across clusters) + +### Sample Requirements from Tenders + +1. **Gemeente Berkelland**: "Het is mogelijk om te zijner tijd zaken en documenten over te dragen aan een e-depot voorziening, dan wel een andere RM applicatie, waarbij zonder informatieverlies alle benodigde bestanden en metadata worden overgedragen." +2. **Gemeente Hilversum**: "Bestanden die horen bij een te archiveren zaak, worden zowel in het oorspronkelijke als in een duurzaam archief bestandsformaat gearchiveerd. De voorkeursformaten zoals omschreven in de Specificatie..." +3. **Gemeente Hilversum**: "Met de Oplossing is het mogelijk om geautomatiseerd gearchiveerde digitale zaken over te dragen aan andere RMA systemen en eDepots waarbij de bestanden, metadata en dossiers worden omgezet in een formeel voorgeschreven formaat (SIP)." +4. **Gemeente Winterswijk**: "RMA: eDepot Achterhoek. TMLO-Achterhoek. Beschrijf op welke manier het overbrengen van gearchiveerde digitale zaken aan andere RMA-systemen of e-Depots verloopt." +5. **Gemeente Zeist**: "De Oplossing levert de functionaliteiten om aan de normen van NEN-2082 of ISO 16175-2:2011 alsmede naar 15489-1 (2016) en 23081-1 (2017) te kunnen voldoen en ondersteunt het TMLO (en MDTO zodra van toepassing)." +6. **Gemeenschappelijke Regeling Omgevingsdienst**: "De MDTO-standaard moet worden gevolgd door de leverancier." + +## Scope + +### In Scope + +- **MDTO metadata mapping**: Map OpenRegister object metadata to MDTO (Metagegevens Duurzaam Toegankelijke Overheidsinformatie) schema fields +- **TMLO backward compatibility**: Support TMLO (Toepassingsprofiel Metadatering Lokale Overheden) for organisations not yet migrated to MDTO +- **SIP package generation**: Generate Submission Information Packages (SIP) conforming to e-Depot ingest specifications, containing objects, files, and metadata +- **Durable format conversion**: Convert documents to archival formats before transfer (PDF/A-1b or PDF/A-2b for documents, ODF for spreadsheets, TIFF for images) +- **Transfer workflow**: Multi-step process: select objects for transfer, validate metadata completeness, generate SIP, submit to e-Depot, confirm receipt +- **e-Depot connector**: Configurable API connector supporting common Dutch e-Depot systems (Preservica, Archivematica, Het Utrechts Archief, e-Depot Achterhoek) +- **Transfer status tracking**: Track transfer status per object (pending, submitted, accepted, rejected) with error reporting +- **Metadata completeness validation**: Pre-transfer check that all required MDTO/TMLO fields are populated + +### Out of Scope + +- Retention period management (separate change: `retention-management`) +- Destruction workflow (separate change: `archival-destruction-workflow`) +- CSV import/export (already exists) +- Physical archive management + +## Acceptance Criteria + +1. Objects can be selected for e-Depot transfer individually or in bulk (by schema, register, or retention category) +2. MDTO metadata is generated from OpenRegister object properties with configurable field mapping +3. SIP packages are generated containing all object data, files, and MDTO/TMLO metadata in the required XML format +4. Documents are automatically converted to durable formats (PDF/A, ODF) before inclusion in SIP packages +5. Metadata completeness is validated before transfer -- missing required fields block the transfer with clear error messages +6. Transfer status is tracked per object and visible in the object detail view +7. At least one e-Depot system can be connected via API (Preservica or equivalent) +8. Failed transfers can be retried without data duplication +9. TMLO output is available as a fallback for organisations not yet on MDTO + +## Dependencies + +- **retention-management**: Transfer typically happens after retention period assessment +- **enhanced-audit-trail**: All transfers must be logged in the audit trail +- OpenRegister ObjectService and file handling +- Docudesk or equivalent for format conversion (PDF/A, ODF) +- External e-Depot API endpoint (configurable per installation) + +## Standards & Regulations + +- MDTO (Metagegevens Duurzaam Toegankelijke Overheidsinformatie) -- replaces TMLO +- TMLO (Toepassingsprofiel Metadatering Lokale Overheden) -- legacy support +- NEN 2082:2008 (Eisen voor functionaliteit van informatie- en archiefmanagement) +- NEN-ISO 16175-2:2011 +- NEN-ISO 15489-1:2016 and NEN-ISO 23081-1:2017 +- OAIS reference model (ISO 14721) for SIP/AIP/DIP concepts +- Archiefwet 1995 (Article 12: transfer obligation) +- Specificatie Duurzame Toegankelijkheid (preferred archival formats) + +## Notes + +- OpenRegister already has CSV import/export with ID support -- this change focuses on archival transfer +- Durable format conversion may leverage Docudesk capabilities already present in the ecosystem +- Many regional archives have their own TMLO profiles (e.g., TMLO-Achterhoek) -- the mapping must be configurable diff --git a/openspec/changes/enhanced-audit-trail/proposal.md b/openspec/changes/enhanced-audit-trail/proposal.md new file mode 100644 index 0000000000..c593a7864c --- /dev/null +++ b/openspec/changes/enhanced-audit-trail/proposal.md @@ -0,0 +1,75 @@ +# Proposal: enhanced-audit-trail + +## Summary + +Implement a complete, immutable audit trail on all object mutations in OpenRegister, recording who changed what, when, with old/new values. Includes verwerkingenlogging for AVG/GDPR compliance and integration with BIO logging requirements. This is distinct from the archived `audit-trail-immutable` change by focusing on the practical audit UX, verwerkingenlogging API, and zaak-history integration rather than the cryptographic storage layer. + +## Demand Evidence + +**Cluster: Logging/audit** -- 157 tenders, 463 requirements +**Cluster: Zaak history / audit trail** -- 86 tenders, 179 requirements +**Cluster: Audit trail** -- 19 tenders, 29 requirements +**Combined**: 262 tenders, 671 requirements + +### Sample Requirements from Tenders + +1. **Gemeente Hilversum**: "De Oplossing beschikt over een niet-muteerbare audit-trail met daarin minimaal de gebeurtenis; de benodigde informatie die nodig is om het incident met hoge mate van zekerheid te herleiden tot een natuurlijk persoon." +2. **Gemeente Hilversum**: "Met de Oplossing wordt alle gestructureerde informatie en alle ongestructureerde informatie gearchiveerd bij de afgesloten zaak, inclusief de audittrail van de zaak." +3. **Gemeente Winterswijk**: "De Oplossing beschikt over een niet-muteerbare audit-trail met daarin minimaal de gebeurtenis; de benodigde informatie die nodig is om het incident met hoge mate van zekerheid te herleiden tot een natuurlijk persoon." +4. **Gemeente Lochem**: "Verwerkingen van gebruikers worden gelogd. Deze verwerkingen worden gelogd volgens de BIO (12.4.1.1. en 2)." +5. **Rijkswaterstaat**: "Verwerkingsregister vereist conform AVG." +6. **Gemeente Deventer**: "Logging, audittrail, berichtherstel." + +## Scope + +### In Scope + +- **Field-level change tracking**: Record old and new values for every field modified in an object mutation +- **Audit trail viewer**: UI component showing chronological history of all changes to an object, with diff view +- **Verwerkingenlogging API**: REST API endpoint conforming to the VNG Verwerkingenlogging standard for registering and querying data processing activities (verwerkingsactiviteiten) +- **BIO logging compliance**: Log entries include all fields required by BIO 12.4.1 (event type, timestamp, user identity, source IP, affected resource, outcome) +- **Bulk operation logging**: Audit trail entries for bulk imports, bulk updates, and bulk deletions with summary records +- **Audit trail export**: Export audit trail data in structured formats (JSON, CSV) for compliance reporting +- **Retention of audit data**: Configurable retention periods for audit trail data (minimum 10 years for government records) +- **Read access logging**: Optional logging of read/view operations on objects containing personal data (AVG Article 30) +- **API mutation logging**: All API-driven changes are logged with the calling application/token identity + +### Out of Scope + +- Cryptographic hash chaining (covered by archived `audit-trail-immutable` spec) +- Object destruction workflow (separate change: `archival-destruction-workflow`) +- CSV import/export (already exists) +- Application-level error logging (Nextcloud's own logging handles this) + +## Acceptance Criteria + +1. Every create, update, and delete operation on an object produces an audit trail entry +2. Audit trail entries include: timestamp, user ID, user display name, action type, affected object ID, field-level changes (old value, new value) +3. Audit trail entries are immutable -- they cannot be modified or deleted through the application +4. An audit trail viewer in the object detail view shows all changes chronologically with expandable diffs +5. A verwerkingenlogging API endpoint allows external systems to query processing activities by person (BSN), time range, or processing purpose +6. Bulk operations produce summary audit entries linking to individual change records +7. Audit trail data can be exported as JSON or CSV for compliance reporting +8. Read access logging can be enabled per schema for objects containing personal data +9. Audit trail retention is configurable and defaults to 10 years + +## Dependencies + +- OpenRegister ObjectService (hooks into save/update/delete lifecycle) +- OpenRegister Entity framework for change detection +- Nextcloud user session for identity tracking +- No external service dependencies (self-contained within OpenRegister) + +## Standards & Regulations + +- AVG/GDPR Article 30 (record of processing activities) +- BIO (Baseline Informatiebeveiliging Overheid) -- section 12.4.1 (event logging) +- VNG Verwerkingenlogging API standard +- Archiefwet 1995 (audit trail as part of archival record) +- NEN-ISO 15489-1:2016 (records management -- metadata requirements) + +## Notes + +- OpenRegister already has CSV import/export with ID support +- The archived `audit-trail-immutable` change covers the storage-layer foundation; this change focuses on the practical audit UX, verwerkingenlogging compliance, and the field-level diff capability +- Verwerkingenlogging is a VNG standard that is increasingly required in government tenders -- it registers which personal data was accessed/modified and for what purpose diff --git a/openspec/changes/entity-relation-grondslagen/design.md b/openspec/changes/entity-relation-grondslagen/design.md deleted file mode 100644 index d8f47000b5..0000000000 --- a/openspec/changes/entity-relation-grondslagen/design.md +++ /dev/null @@ -1,119 +0,0 @@ -## Context - -`EntityRelation` (`lib/Db/EntityRelation.php`) is the join row between a detected entity and its location — chunk, file, object, or email — plus position offsets, confidence, detection method, and the anonymisation outcome (`anonymized`, `anonymizedValue`). It is OpenRegister's authoritative record of "which entity was found where, at what confidence, and what was done about it." - -What it does NOT carry is *why* a redaction was applied. Consumer apps — DocuDesk first — record per-anonymisation legal bases (Woo Art. 5 grondslagen and similar) for compliance reporting and audit. Today this metadata has no home: stuffing it into consumer-app schemas decouples it from the actual EntityRelation row that drove the redaction, and leaves the audit trail incomplete. - -The minimal correct surface is one optional field on `EntityRelation`: a JSON array of UUIDs that reference whatever vocabulary the consumer app uses (DocuDesk's `base` schema, in the in-flight `add-dossier-schema` change). OpenRegister stores the array verbatim. It does not validate the UUIDs because the vocabulary lives in another register owned by another app — cross-app referential integrity at this layer would be a coupling we don't want. - -The anonymise endpoint (`FileService::anonymizeDocument` and the controller routes that call it) needs to learn three things: - -1. Accept `bases` per entity in the request payload. -2. Persist them on the matching `EntityRelation` row alongside `anonymized` / `anonymizedValue`. -3. **Strip** `bases` from each entry before forwarding the entity list to OpenAnonymiser. Bases are decision metadata, not redaction input — OpenAnonymiser doesn't need them and the contract there should remain unchanged. - -## Goals / Non-Goals - -**Goals:** - -- Add an optional `bases` JSON column on the `oc_openregister_entity_relations` table. -- Extend `EntityRelation` with a getter/setter for `bases` following the existing Nextcloud `Entity` base-class pattern. -- Extend the anonymise endpoint to accept, persist, and strip per-entity `bases` as described. -- Make the change additive and non-breaking: callers that don't pass `bases` see identical behaviour to today; OpenAnonymiser sees an unchanged payload. -- Provide a forward-only, zero-downtime migration. - -**Non-Goals:** - -- Validate that base UUIDs resolve. OpenRegister does not own the vocabulary; cross-app validation is the consumer app's responsibility. -- Spec the broader entity-recognition / anonymisation pipeline. `FileService::anonymizeDocument`, `EntityRelation`, `EntityRecognitionHandler` are implemented but not currently covered by an OpenSpec capability. Retrofitting a full capability spec for that surface is out of scope here; this change only specs the new bases-related behaviour. -- Provide a separate "set bases on EntityRelation" API. Bases are set as part of the anonymise call. If a future use case needs to attach bases without anonymising, it can be added as a follow-up. -- Provide a prohibition gate. Validating that the right entities were selected for anonymisation is a consumer-app concern (DocuDesk implements this in the paired change `anonymisation-grondslagen-and-prohibition-gate`). - -## Decisions - -### D1. `bases` is a JSON column, not a join table - -The column is a JSON array of strings (UUIDs). No separate `entity_relation_base` join table. - -**Rationale:** - -- **Nextcloud convention.** Nextcloud's storage layer treats moderate-cardinality multi-valued attributes as JSON columns by default; join tables are reserved for relationships requiring foreign-key integrity, indexed reverse-lookup, or cross-record constraints. None of those apply here — bases are a per-row tag list. -- **Parity with `dossier.bases[]`.** DocuDesk's `dossier` schema (in-flight) uses an `array of $ref` to `base` objects. Mirroring that shape on the EntityRelation row keeps the mental model uniform across the system. -- **Zero foreign-key coupling.** OpenRegister does not own the `base` vocabulary; storing UUIDs as JSON strings sidesteps the question of whether to enforce FK constraints across registers. - -**Alternative considered:** A `entity_relation_bases` join table with `(entity_relation_id, base_uuid)`. Rejected: introduces a second migration, two write points to keep in sync, and reverse-lookup queries that aren't part of any current use case. Can be added later if compliance reporting demands it. - -### D2. No validation that base UUIDs resolve - -The mapper accepts any string array. Whether the UUIDs point to actual `base` objects in DocuDesk's register (or any register) is the consumer app's problem. - -**Rationale:** - -- The `base` vocabulary lives in DocuDesk's `dossier` register. OpenRegister doesn't own it and shouldn't reach into another app's data to validate. -- The consumer app already validates the picker output before sending. Double-validation at the OpenRegister layer adds latency without catching new cases. -- Cross-register referential integrity is a known harder problem in OpenRegister; deferring it here is consistent with how other cross-app references work. - -**Mitigation for the dangling-reference risk:** the consumer app should mark its canonical `base` seed objects as immutable (DocuDesk already does this per ADR-016 + the `add-dossier-schema` design). Tenant-created bases are deletable but rare; if a delete leaves dangling references on EntityRelation rows, the references read as "unknown grondslag" — degraded but not broken. - -### D3. Strip happens after persistence, before the OpenAnonymiser call - -Order in `FileService::anonymizeDocument`: - -``` - 1. for each entry in payload.entities: - find or upsert the EntityRelation row, write anonymized/anonymizedValue + bases - 2. clone the entity list with `bases` removed from each entry - 3. forward the cloned list to OpenAnonymiser -``` - -**Rationale:** The persistence step is the source of truth for "what was decided". The OpenAnonymiser call is downstream, with a contract that does not include bases. Stripping after persistence ensures the audit record is complete even if the OpenAnonymiser call fails (failure is reported back; bases are still recorded with the relation in `anonymized: false` state, which is recoverable on retry). - -**Alternative considered:** Strip first, persist after the OpenAnonymiser response. Rejected: if OpenAnonymiser succeeds and the persist fails, the redaction happened but the grondslag is lost — a worse outcome than the reverse. - -### D4. Bases payload is set per entity, not per request - -Each entry in `payload.entities[]` carries its own `bases`. There is no top-level `bases` field that applies to all entries. - -**Rationale:** Per-entity grondslagen are the legal reality — different entities in the same document can be anonymised under different bases (a name = `persoonsgegevens`, a competitor's pricing offer = `bedrijfs-fabricagegegevens`). A top-level field would force consumers to either degrade to a "common denominator" or duplicate the logic. - -The consumer app is responsible for any "apply to all" UX (e.g. a checkbox that fans the dossier's bases out to every entity client-side); OpenRegister sees only the per-entity result. - -### D5. Empty / null `bases` is allowed - -A row with `bases: null` or `bases: []` represents "anonymisation happened, no grondslag was attached". This stays valid because: - -- Existing rows (pre-migration) have `bases: null`. We don't backfill. -- Some consumer flows may not record grondslagen yet (or ever — generic file-sanitisation flows have no grondslag concept). - -The column is nullable; the JSON array, when present, has no `minItems` constraint. - -### D6. Migration is forward-only and idempotent - -The migration class adds the column with `notnull => false` and `default => null`. No data migration. Re-running the migration is a no-op (the column-add primitive is idempotent in Nextcloud's schema migration framework). - -**Rollback:** drop the column. Existing callers that started passing `bases` would silently lose them on writes after rollback; this is acceptable for an emergency-only path. - -## Risks / Trade-offs - -- **[Dangling base UUIDs after consumer-side delete]** → Mitigation per D2: consumer app marks canonical seeds as immutable; tenant-created bases are rare to delete; readers tolerate "unknown grondslag" gracefully. -- **[Partial anonymise call: persist succeeds, OpenAnonymiser fails]** → Acceptable. The EntityRelation row reflects "intended" state with `anonymized: false`; retry re-issues the anonymise call. This was already the failure mode pre-change for `anonymized` / `anonymizedValue`; bases inherit the same semantics. -- **[Consumer app that wraps the call differently]** → If a consumer ever calls `FileService::anonymizeDocument` directly with a stale payload shape (no `bases` field), the call still works and bases default to null. Backwards-compatible. -- **[JSON column query performance]** → Negligible. Reverse lookups ("show me all relations under grondslag X") are not a current use case; if they emerge, a generated index on the JSON path or a materialised view is a follow-up. - -## Migration Plan - -1. Land `EntityRelation` field + `EntityRelationMapper` persistence + new migration class in one PR. -2. Apply migration on dev / staging — column appears as nullable JSON. Existing rows have `null`. Smoke-test: run an anonymise call without `bases` (existing behaviour) and with `bases` (new behaviour). -3. Update `FileService::anonymizeDocument` to accept, persist, and strip. Smoke-test the OpenAnonymiser leg — the request body should be unchanged from today's shape. -4. Release. Consumer apps (DocuDesk first) start sending `bases`. - -**Rollback:** drop the column via a reverse migration. Bases sent by callers after rollback are silently ignored. Consumer apps must be redeployed to stop sending the field if rollback is permanent; in practice rollback is for emergencies and consumer apps tolerate the field being silently dropped. - -## Seed Data - -Not applicable — this change adds a column to an existing DB table, not new schemas or registers. The `base` vocabulary that bases reference lives in DocuDesk and is seeded there per the in-flight `add-dossier-schema` change. - -## Open Questions - -- Whether to add a generated index on the JSON column for compliance-reporting queries. **Resolution:** defer until a real query workload appears. JSON column queries on small per-row arrays are inexpensive at expected volumes. -- Whether the strip step should also happen if a future caller passes other DocuDesk-specific decoration fields (notes, audit hints). **Resolution:** out of scope. The strip is targeted at `bases` specifically. If other decoration fields appear, this design is the precedent for handling them — strip in the same place, with the same rationale. diff --git a/openspec/changes/entity-relation-grondslagen/proposal.md b/openspec/changes/entity-relation-grondslagen/proposal.md deleted file mode 100644 index 0b7d2ba55d..0000000000 --- a/openspec/changes/entity-relation-grondslagen/proposal.md +++ /dev/null @@ -1,41 +0,0 @@ -## Why - -`EntityRelation` records the link between a detected entity and the chunk/file/object/email it was found in, plus its anonymisation outcome (`anonymized`, `anonymizedValue`). What it does NOT record is the **legal basis** (grondslag) under which the anonymisation was performed. - -Consuming apps — primarily DocuDesk — need to associate per-entity legal bases (Woo Art. 5 grondslagen and equivalents) with each anonymisation event for audit, compliance reporting (Wet open overheid), and downstream rendering (e.g. attaching a "grondslagenpagina" to the anonymised document). Today this metadata has nowhere to live. The choices were either to scatter it across consumer-app schemas (loses the link to the actual EntityRelation row that drove the redaction) or to never persist it (loses the audit trail entirely). - -This change adds a single optional `bases` JSON column to `EntityRelation` and extends the anonymise endpoint to accept and persist these bases per entity. The bases MUST be stripped from the payload before forwarding to OpenAnonymiser — they are metadata about the decision, not input to the redaction tool. - -## What Changes - -- **NEW:** Optional `bases` JSON column on `EntityRelation` — array of UUIDs referencing `base` schema objects (the grondslag vocabulary owned by the consuming app, e.g. DocuDesk's `dossier` register). OpenRegister stores the array verbatim; it does NOT validate that the UUIDs resolve, since the vocabulary lives outside OpenRegister's own schemas. -- **MODIFIED:** The anonymise endpoint (`FileService::anonymizeDocument` and the controller routes that call it) accepts an optional `bases` array per entity in the request payload. When present, the bases are persisted on the matching `EntityRelation` row alongside the existing `anonymized` / `anonymizedValue` fields. -- **MODIFIED:** Before forwarding the entity list to OpenAnonymiser, the service MUST strip the `bases` field from each entry. OpenAnonymiser's contract is unchanged. -- **NEW:** Database migration adds the `bases` column to the `oc_openregister_entity_relations` table as a nullable JSON column. Existing rows are unaffected (column defaults to NULL). -- **NO trigger-side changes.** No new endpoints, no breaking changes to existing API. Callers that don't pass `bases` see identical behaviour to today. -- **NO prohibition gate.** Validating that "the right entities were selected for anonymisation" is the consumer app's responsibility (DocuDesk implements this against its own `publicationProhibition` schema in a paired change). OpenRegister stays a generic anonymise primitive. - -## Capabilities - -### New Capabilities - -- `entity-relation-grondslagen`: optional bases-link on `EntityRelation`; the anonymise endpoint's contract for accepting, persisting, and stripping per-entity bases. - -### Modified Capabilities - -(none — the broader entity-recognition / anonymisation pipeline is implemented but currently uncovered by an OpenSpec capability; this change does not retroactively spec it. See `Out of scope` in design.md.) - -## Impact - -- **Code (openregister):** - - `lib/Db/EntityRelation.php` — new `bases` field (JSON, nullable, default null); getter/setter via Nextcloud's Entity base class pattern. - - `lib/Db/EntityRelationMapper.php` — persistence handles the new column. - - Migration class under `lib/Migration/` — adds the column. Idempotent; safe on existing installs. - - `lib/Service/FileService.php` (or the equivalent path that today calls OpenAnonymiser) — accept `bases` per entity, persist on EntityRelation, strip before forwarding. -- **API contract:** Anonymise endpoint payload gains an optional `bases` field per entity entry. Additive, non-breaking. Existing callers (and OpenAnonymiser) see no change. -- **Cross-app:** - - **DocuDesk** is the immediate consumer; the paired change `anonymisation-grondslagen-and-prohibition-gate` in DocuDesk relies on this work landing. - - **opencatalogi** and **softwarecatalog** do not call the anonymise endpoint; unaffected. - - The `base` vocabulary itself lives in DocuDesk (per the in-flight `add-dossier-schema` change). OpenRegister does not own or validate that vocabulary. -- **Database:** One column added to one table. Migration is forward-only and zero-downtime. -- **Tests:** Mapper unit tests for the new column. Service-level tests for the persist + strip path. diff --git a/openspec/changes/entity-relation-grondslagen/specs/entity-relation-grondslagen/spec.md b/openspec/changes/entity-relation-grondslagen/specs/entity-relation-grondslagen/spec.md deleted file mode 100644 index b3c5af69e5..0000000000 --- a/openspec/changes/entity-relation-grondslagen/specs/entity-relation-grondslagen/spec.md +++ /dev/null @@ -1,262 +0,0 @@ ---- -status: draft ---- - -# Entity Relation Grondslagen - -## Purpose - -Defines an optional `bases` link on `EntityRelation` and the contract by which the anonymise endpoint accepts, persists, and strips per-entity legal bases (grondslagen). The link is consumer-app-agnostic — OpenRegister stores the array of UUIDs verbatim and does not validate that they resolve to real `base` objects (the vocabulary lives in the consumer app). Writes are audit-logged and inherit the anonymise endpoint's existing authorization model. - -## ADDED Requirements - -### Requirement: `EntityRelation` MUST gain an optional `bases` JSON column - -The `oc_openregister_entity_relations` table MUST gain a column named `bases` of type JSON (or the platform-equivalent JSON column type), nullable, with default `NULL`. The column MUST hold either `NULL`, an empty array `[]`, or an array of strings (UUIDs). No JSON-schema validation of the array contents is enforced at the database or mapper layer. - -The `EntityRelation` PHP entity (`lib/Db/EntityRelation.php`) MUST expose `getBases(): ?array` and `setBases(?array $bases): void`, registered via `addType('bases', 'json')` in the constructor. Existing rows (pre-migration) MUST read as `bases = null` without errors. - -#### Scenario: Migration adds the column without disturbing existing rows - -- **GIVEN** an OpenRegister install with existing `EntityRelation` rows -- **WHEN** the migration is applied -- **THEN** the `bases` column MUST be added to `oc_openregister_entity_relations` -- **AND** every existing row MUST read with `bases = null` via `EntityRelation::getBases()` -- **AND** no other columns or rows MUST be modified - -#### Scenario: Migration is idempotent - -- **GIVEN** the migration has already been applied -- **WHEN** the migration runs again (e.g. on upgrade after a previous deploy) -- **THEN** the migration MUST be a no-op -- **AND** no error MUST be raised - -#### Scenario: Mapper reads and writes bases - -- **GIVEN** an `EntityRelation` row with `bases = ["uuid-a", "uuid-b"]` written via the mapper -- **WHEN** the row is read back via `EntityRelationMapper::find($id)` -- **THEN** `getBases()` MUST return the array `["uuid-a", "uuid-b"]` -- **AND** `jsonSerialize()` MUST include `bases` in its output - -#### Scenario: Empty array is accepted and distinct from null - -- **WHEN** `setBases([])` is called and the row is persisted -- **THEN** subsequent reads MUST return `bases = []` (an empty array, not null) -- **AND** `jsonSerialize()['bases']` MUST be `[]` - -### Requirement: The anonymise endpoint MUST accept per-entity `bases` in the request payload - -The anonymise endpoint (currently `FileService::anonymizeDocument(node, payload)` and the controller routes that wrap it) MUST accept an optional `bases` field on each entry in `payload.entities[]`. The field MUST be either absent, `null`, or an array of strings (see also the endpoint-shape-validation Requirement below). The field's presence MUST NOT change any existing behaviour for callers that omit it. - -#### Scenario: Anonymise call without bases preserves today's behaviour - -- **GIVEN** a request payload with entities that have no `bases` field -- **WHEN** the anonymise endpoint processes the request -- **THEN** the matching `EntityRelation` rows MUST be written with `bases = null` -- **AND** the request forwarded to OpenAnonymiser MUST contain exactly the same fields it would have contained before this change - -#### Scenario: Anonymise call with bases populates EntityRelation - -- **GIVEN** a request payload with `entities: [{entityId: 42, bases: ["uuid-a"]}, {entityId: 43, bases: ["uuid-b", "uuid-c"]}]` -- **WHEN** the anonymise endpoint processes the request -- **THEN** the EntityRelation row for entity 42 MUST have `bases = ["uuid-a"]` -- **AND** the EntityRelation row for entity 43 MUST have `bases = ["uuid-b", "uuid-c"]` - -#### Scenario: Bases field with empty array writes empty array - -- **GIVEN** a payload entry with `bases: []` -- **WHEN** the anonymise endpoint processes it -- **THEN** the matching EntityRelation row MUST have `bases = []` (empty array, not null) - -### Requirement: The anonymise endpoint MUST validate the SHAPE of `bases` at the entry point but MUST NOT validate its CONTENT - -At the endpoint layer (controller / `FileService::anonymizeDocument`), the `bases` field on each entry of `payload.entities[]` MUST be one of: - -- absent, -- `null`, -- an array whose every element is a string. - -Any other shape — a non-array value (e.g. a number, an object, a single string), or an array containing non-string elements — MUST be rejected with an HTTP 400 response. The 400 response body MUST identify the offending entry by index so the caller can fix the right entry in a multi-entity payload. - -At the mapper layer, no further validation MUST be applied: the elements of the `bases` array MUST be persisted verbatim regardless of their content (UUID-shaped, garbage strings, or empty strings). This two-layer contract exists deliberately — endpoint validation rejects ill-typed input early, while the mapper remains content-agnostic so the consumer-app's vocabulary can evolve without OR changes. - -#### Scenario: Endpoint rejects `bases` as a non-array - -- **GIVEN** a payload entry with `bases: "uuid-a"` (a string, not an array) -- **WHEN** the anonymise endpoint processes the request -- **THEN** the response MUST be HTTP 400 -- **AND** no EntityRelation row MUST be modified - -#### Scenario: Endpoint rejects array elements that are not strings - -- **GIVEN** a payload entry at index 1 with `bases: ["uuid-a", 42]` -- **WHEN** the anonymise endpoint processes the request -- **THEN** the response MUST be HTTP 400 -- **AND** the error body MUST identify entity index `1` as the offending entry - -#### Scenario: Mapper accepts any string content - -- **GIVEN** a validated payload with `bases: ["not-a-uuid", "12345", ""]` -- **WHEN** the endpoint forwards the payload to the mapper layer -- **THEN** the row MUST be persisted with the values verbatim -- **AND** no error MUST be raised - -### Requirement: `bases` writes MUST inherit the anonymise endpoint's existing authorization (ADR-005 / ADR-023) - -The anonymise endpoint already enforces a per-object authorization check (the caller MUST have write access to the file/object being anonymised). Setting `bases` MUST require no additional authorization beyond that check — `bases` is metadata attached to an existing anonymisation operation that the caller is already authorized to perform. There MUST be no separate group, role, or action-level permission for `bases` writes in this change. - -A caller who cannot anonymise a given object MUST NOT be able to set `bases` on its EntityRelation rows, because the persist step (next Requirement) runs only after the endpoint's existing authorization has passed. A caller who CAN anonymise the object MUST be able to set arbitrary `bases` strings as part of that operation. - -This decision is recorded explicitly so reviewers and implementers can reason about the authorization surface: there is intentionally NO extra check, and that absence is the intended contract — not an oversight. If a future change introduces a separate action-level permission for `bases` (per ADR-023), it MUST add a new Requirement here. - -#### Scenario: Unauthorized caller cannot anonymise and therefore cannot set bases - -- **GIVEN** a user without write access to file `F` -- **WHEN** the user POSTs an anonymise request for `F` with `bases` populated -- **THEN** the existing endpoint authorization check MUST fail and the request MUST be rejected (HTTP 403) -- **AND** no EntityRelation row for `F` MUST be modified -- **AND** no bases value MUST be persisted - -#### Scenario: Authorized caller can set bases as part of anonymisation - -- **GIVEN** a user with write access to file `F` -- **WHEN** the user POSTs an anonymise request for `F` with `bases: ["uuid-a"]` -- **THEN** the request MUST succeed -- **AND** the EntityRelation row's `bases` value MUST be `["uuid-a"]` - -### Requirement: The anonymise endpoint MUST persist bases BEFORE forwarding to OpenAnonymiser - -The order of operations in the anonymise endpoint MUST be: (1) for each entry, find or upsert the `EntityRelation` row writing the existing fields (`anonymized`, `anonymizedValue`, etc.) plus `bases`; then (2) construct the request to OpenAnonymiser with `bases` stripped from each entry; then (3) forward to OpenAnonymiser. Persistence MUST NOT be conditional on the OpenAnonymiser call succeeding. - -#### Scenario: Persist precedes the OpenAnonymiser call - -- **GIVEN** a successful anonymise request with bases populated -- **WHEN** the endpoint runs -- **THEN** the EntityRelation rows MUST be written before the HTTP call to OpenAnonymiser is issued -- **AND** the persist write MUST include the bases values - -#### Scenario: Persist survives an OpenAnonymiser failure - -- **GIVEN** a request whose OpenAnonymiser call fails (network timeout, 500 response) -- **WHEN** the endpoint processes the request -- **THEN** the EntityRelation rows MUST have been written with `bases` populated -- **AND** the rows MUST have `anonymized = false` (or the existing pre-change semantics for failed redaction) - -### Requirement: `bases` writes MUST be recorded in OpenRegister's audit trail (ADR-022 / Woo compliance) - -Every mutation of an `EntityRelation` row that sets or changes the `bases` value MUST produce an entry in the OpenRegister audit trail. The audit entry MUST include the actor (Nextcloud user UID — per ADR-005, never the display name), the timestamp, the row's stable identifier, and both the previous and new `bases` values. Reads of `EntityRelation` rows MUST NOT produce audit entries. - -The audit entry MUST be written through OpenRegister's existing immutable-audit-trail subsystem (the same one that records other `EntityRelation` mutations), not by direct mapper writes that bypass audit. This is the load-bearing compliance requirement of the change — without it, a Woo officer cannot reconstruct which grondslag justified a given redaction, defeating the feature's stated purpose. - -#### Scenario: Setting bases for the first time produces an audit entry - -- **GIVEN** a request that sets `bases: ["uuid-a"]` on a new EntityRelation row (no prior value) -- **WHEN** the row is persisted -- **THEN** an audit-trail entry MUST exist for that row referencing the action (e.g. `entity_relation_bases_set` or OR's equivalent), the acting user's UID, an ISO-8601 timestamp, the row's identifier, `previousBases: null`, and `newBases: ["uuid-a"]` - -#### Scenario: Updating bases produces an audit entry with old + new values - -- **GIVEN** an EntityRelation row with `bases: ["uuid-a"]` -- **WHEN** a subsequent anonymise call sets `bases: ["uuid-a", "uuid-b"]` -- **THEN** an audit-trail entry MUST be written with `previousBases: ["uuid-a"]` and `newBases: ["uuid-a", "uuid-b"]` - -#### Scenario: Reads do not produce audit entries - -- **WHEN** an `EntityRelation` row with non-null `bases` is read via `EntityRelationMapper::find` or `findEntitiesForFile` -- **THEN** no audit-trail entry MUST be produced for the read - -### Requirement: A retry that omits `bases` MUST reuse the persisted values - -When the anonymise endpoint receives a retry request where `bases` is absent from one or more entries that previously had `bases` persisted (i.e. the EntityRelation row already exists with non-null `bases`), the endpoint MUST NOT overwrite the persisted values. The retry MUST proceed with the OpenAnonymiser call using the persisted `bases` for downstream behaviour (audit trail, consumer-app summary rendering), without requiring the caller to resupply `bases`. - -A retry that DOES include `bases` MUST overwrite the persisted values; the resulting audit entry MUST record the previous-vs-new transition per the audit-trail Requirement. The endpoint MUST distinguish three caller intents: - -- field **absent** → reuse persisted value (no audit entry for `bases` field) -- field **present and `null`** → set to `null` (explicit clear, audit-logged) -- field **present and `[]`** → set to `[]` (explicit empty, audit-logged) - -This contract lets DocuDesk's `anonymisation-grondslagen-and-prohibition-gate` issue retries after a gate-fail without re-prompting the operator for grondslagen. - -#### Scenario: Retry without `bases` preserves the persisted value - -- **GIVEN** an EntityRelation row with `bases: ["uuid-a"]` and `anonymized: false` (previous OpenAnonymiser call failed) -- **WHEN** a retry request arrives for the same entity with no `bases` field in the payload -- **THEN** the EntityRelation row's `bases` MUST remain `["uuid-a"]` (unchanged) -- **AND** the retry MUST proceed with the OpenAnonymiser call using the persisted bases -- **AND** on success the row MUST transition to `anonymized: true` with `bases` unchanged -- **AND** no audit-trail entry MUST be produced specifically for the `bases` field (other audit entries for the anonymised-flag transition follow existing OR semantics and are out of scope of this Requirement) - -#### Scenario: Retry with new `bases` overwrites the persisted value - -- **GIVEN** an EntityRelation row with `bases: ["uuid-a"]` -- **WHEN** a retry request arrives for the same entity with `bases: ["uuid-b"]` -- **THEN** the EntityRelation row's `bases` MUST be updated to `["uuid-b"]` -- **AND** an audit-trail entry MUST record the transition (`previousBases: ["uuid-a"]`, `newBases: ["uuid-b"]`) - -#### Scenario: Retry with explicit `bases: null` clears the persisted value - -- **GIVEN** an EntityRelation row with `bases: ["uuid-a"]` -- **WHEN** a retry request arrives with `bases: null` -- **THEN** the EntityRelation row's `bases` MUST be set to `null` -- **AND** the transition MUST be audit-logged (`previousBases: ["uuid-a"]`, `newBases: null`) - -### Requirement: The anonymise endpoint MUST strip `bases` from the payload before forwarding to OpenAnonymiser - -Before issuing the HTTP call to OpenAnonymiser, the service MUST construct a copy of the entity list with the `bases` field removed from every entry. The OpenAnonymiser request body MUST be byte-equivalent to what it would have been before this change (modulo any other unrelated field reorderings introduced by the JSON encoder). - -#### Scenario: OpenAnonymiser sees no `bases` field - -- **GIVEN** a request payload with bases populated on every entry -- **WHEN** the anonymise endpoint forwards to OpenAnonymiser -- **THEN** the request body to OpenAnonymiser MUST contain no `bases` field on any entity entry -- **AND** all other fields (`text`, `entityType`, `score`, etc.) MUST be forwarded unchanged - -#### Scenario: Mixed payload — some entries with bases, some without - -- **GIVEN** a payload where entries 1, 3 have `bases` populated and entry 2 has none -- **WHEN** the endpoint forwards to OpenAnonymiser -- **THEN** the forwarded request MUST have no `bases` field on any of the three entries -- **AND** entries 1, 3 MUST still have their EntityRelation rows updated with the supplied bases - -### Requirement: OpenRegister MUST NOT validate that `bases` UUIDs resolve - -The persistence layer accepts any string array. OpenRegister MUST NOT issue any cross-register lookup to verify that the supplied UUIDs correspond to actual objects in any register. The vocabulary that bases reference is owned by the consumer app (see Notes for the canonical DocuDesk `base` vocabulary). - -#### Scenario: Unknown UUID strings are accepted - -- **GIVEN** a payload with `bases: ["00000000-0000-0000-0000-000000000000"]` (a UUID that doesn't resolve to any object) -- **WHEN** the endpoint processes the request -- **THEN** the row MUST be persisted with the value verbatim -- **AND** no error MUST be raised -- **AND** no cross-register query MUST be issued - -### Requirement: The change MUST be additive and non-breaking - -Existing callers (and the OpenAnonymiser contract) MUST be unaffected when no `bases` field is present in any entry of the payload. No existing field is removed, renamed, or repurposed. No existing scenario in any other capability MUST be invalidated. - -#### Scenario: Pre-change client continues to work - -- **GIVEN** a client that constructs anonymise payloads using the pre-change schema (no `bases` field anywhere) -- **WHEN** that client sends a request to the anonymise endpoint -- **THEN** the request MUST succeed with identical behaviour to before this change -- **AND** the resulting EntityRelation rows MUST have `bases = null` -- **AND** the OpenAnonymiser request body MUST be identical to what it would have been pre-change - -## Notes - -### Consumer-owned vocabulary - -The `bases` array contains UUID-shaped strings whose meaning is defined by the consumer app, not by OpenRegister. OpenRegister persists the array verbatim and never resolves the UUIDs (see Requirement: *OpenRegister MUST NOT validate that bases UUIDs resolve*). - -For DocuDesk-driven anonymisation — the first consumer of this capability — the UUIDs SHOULD resolve to objects in the `base` register defined by DocuDesk's [`add-dossier-schema`](https://github.com/ConductionNL/docudesk/pull/135) change. That schema seeds six canonical Woo Art. 5 *uitzonderingsgronden*: - -| Slug | Woo Art. 5 reference | Description (NL, excerpt) | -|---|---|---| -| `persoonsgegevens` | Art. 5.1 Woo jo. AVG Art. 4 lid 1 | Herleidbare gegevens van een natuurlijke persoon. | -| `bijzondere-persoonsgegevens` | Art. 5.1 Woo jo. AVG Art. 9 | Bijzondere persoonsgegevens (medisch, religieus, etc.). | -| `strafrechtelijk` | Art. 5.1 Woo jo. AVG Art. 10 | Strafrechtelijke gegevens. | -| `bedrijfs-fabricagegegevens` | Art. 5.1 sub c Woo | Vertrouwelijke bedrijfs- en fabricagegegevens. | -| `onevenredige-benadeling` | Art. 5.2 Woo | Onevenredige benadeling van betrokkenen of derden. | -| `nationale-veiligheid` | Art. 5.1 sub a/b Woo | Nationale veiligheid / opsporing. | - -Other consumers MAY define their own `base` vocabulary; OpenRegister does not enforce a single registry. Consumers MUST handle the case where a stored `bases` UUID does not resolve to a known `base` object (e.g. by surfacing it as "onbekende grondslag" or filtering it from rendered summaries) — this graceful-degradation behaviour is the consumer's responsibility, not OpenRegister's. \ No newline at end of file diff --git a/openspec/changes/entity-relation-grondslagen/tasks.md b/openspec/changes/entity-relation-grondslagen/tasks.md deleted file mode 100644 index 28d89b78d2..0000000000 --- a/openspec/changes/entity-relation-grondslagen/tasks.md +++ /dev/null @@ -1,32 +0,0 @@ -## 1. Database migration - -- [ ] 1.1 Add a new `lib/Migration/VersionDate.php` that adds a nullable JSON column `bases` to `oc_openregister_entity_relations` (match the JSON column type used by other OR JSON columns, e.g. `oc_openregister_objects`). Verify idempotency (Nextcloud `addColumn` is idempotent) and smoke-test on a populated dev database so pre-existing rows read `bases` as `null`. - -## 2. EntityRelation entity + mapper - -- [ ] 2.1 Add `protected ?array $bases = null;` to `lib/Db/EntityRelation.php`, register it via `addType(fieldName: 'bases', type: 'json')` in the constructor, update the class-header magic-method docblocks (`getBases()`/`setBases()`), and include `bases` in `jsonSerialize()` after `anonymizedValue` (also updating the psalm/phpdoc return type). -- [ ] 2.2 Confirm `EntityRelationMapper` requires no changes (QBMapper auto-handles `addType` columns); if any manual `select(...)` lists exist, extend them to include `bases`. - -## 3. Anonymise endpoint integration - -- [ ] 3.1 In `FileService::anonymizeDocument(node, payload)` (or the controller-equivalent), accept an optional `bases` field per entry in `payload.entities[]`. Validate shape only at the entry point: `null` OR a string array; reject malformed shape with HTTP 400 naming the offending entity index. The mapper is intentionally content-agnostic (no UUID validation). -- [ ] 3.2 Implement persist-then-strip ordering: for each entry, locate/upsert the `EntityRelation` row, apply retry-omit semantics (3.4), set `anonymized` / `anonymizedValue` / `bases`, persist BEFORE the OpenAnonymiser HTTP call. Construct the OpenAnonymiser request body from a copy of `payload.entities[]` with `bases` removed — outgoing body MUST be byte-equivalent to the pre-change shape. -- [ ] 3.3 Wire `bases` mutations through OpenRegister's existing immutable audit-trail subsystem (the same path used for other `EntityRelation` mutations — grep `EntityRelationMapper` + audit-trail wiring). Every set/update of `bases` MUST emit an audit entry with `previousBases`, `newBases`, acting user UID (NOT display name, per ADR-005), timestamp, and row identifier. Reads MUST NOT audit. Reference ADR-022. -- [ ] 3.4 Implement retry-omit semantics distinguishing three caller intents: field **absent** → reuse persisted value, no audit entry; field present and `null` → set to `null` (explicit clear, audit-logged); field present and `[]` → set to empty array (audit-logged). -- [ ] 3.5 Confirm the anonymise endpoint's existing per-object write-access check is the **only** auth on the `bases` write path — no extra group/role check is added. Cross-reference ADR-005 + ADR-023 in the PR description so the absence is intentional. - -## 4. Unit tests - -- [ ] 4.1 Add `tests/unit/Db/EntityRelationTest.php` + `EntityRelationMapperTest.php` covering: `getBases`/`setBases` round-trip; `jsonSerialize` includes `bases`; null-vs-empty-array distinction preserved; insert with/without bases; update bases on existing row; non-UUID strings accepted; idempotent migration smoke. -- [ ] 4.2 Add `tests/unit/Service/FileServiceTest.php` + `FileServiceShapeValidationTest.php` covering the endpoint integration: persist precedes OpenAnonymiser call; OpenAnonymiser receives a request body without `bases`; OpenAnonymiser failure preserves persisted bases; rejects `bases: "string"` (400); rejects `bases: ["uuid", 42]` (400); accepts `bases: ["any", "strings"]`; 400 error body identifies the offending entity index. -- [ ] 4.3 Add `tests/unit/Service/FileServiceAuditTrailTest.php` + `FileServiceRetryTest.php` + `FileServiceAuthorizationTest.php` covering: first-time set emits audit entry with `previousBases: null` + `newBases: `; update audits old + new; read does NOT audit; UID not display name (ADR-005); retry-omit reuses persisted bases without audit-logging the unchanged field; the three caller intents (absent / present-null / present-empty-array) distinguished correctly; HTTP 403 + no-persist when caller lacks write-access; arbitrary strings accepted with write-access. - -## 5. Integration tests + cross-app regression - -- [ ] 5.1 Add a Newman/Postman integration test (or extend the existing collection) for the anonymise endpoint covering: `bases`-populated payload returns 200 and direct DB query shows correct bases; pre-change-shape payload (no `bases`) still works identically. -- [ ] 5.2 Cross-app regression: smoke-test DocuDesk's existing anonymise calls (without `bases`) and confirm no break; inspect the OpenAnonymiser request body via debug logging on a single test call and confirm the body shape is unchanged from pre-change. (opencatalogi is not a known anonymise consumer, but log negative confirmation.) - -## 6. Documentation + verification - -- [ ] 6.1 Add a `CHANGELOG.md` entry under Added describing the new optional `bases` column on `EntityRelation` and the anonymise endpoint's persist+strip behaviour. Update the OpenAnonymiser interface contract doc (or add a one-line comment on `FileService::anonymizeDocument`) noting that `bases` is recognised but stripped before the call. -- [ ] 6.2 Run `composer check:strict` (PHPUnit + Psalm/PHPStan + PHPCS) and `openspec validate entity-relation-grondslagen` — all clean. Final manual smoke against a live stack: anonymise call with `bases` populated, confirm EntityRelation row has bases populated and OpenAnonymiser was NOT sent `bases`. diff --git a/openspec/changes/extend-schemas-in-register-service/tasks.md b/openspec/changes/extend-schemas-in-register-service/tasks.md deleted file mode 100644 index e8dea45066..0000000000 --- a/openspec/changes/extend-schemas-in-register-service/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -## 1. Serializer namespace + RegisterSerializer - -- [ ] 1.1 Create `lib/Service/Serializer/RegisterSerializer.php` (PHP ns `OCA\OpenRegister\Service\Serializer`) with constructor DI for `SchemaMapper` + `LoggerInterface`. Expose `serialize(Register $register, array $extend = [], ?array $schemaStats = null): array` (calls `$register->jsonSerialize()` then applies `$extend`) and `serializeMany(array $registers, array $extend = [], ?array $schemaStatsByRegisterId = null): array` (iterates + delegates). -- [ ] 1.2 Implement the `'schemas'` extension: for each ID in the register's `schemas` field, attempt `SchemaMapper::find($id, _multitenancy: false)`. On success place the schema's `jsonSerialize()` output in the same array position; on `DoesNotExistException` retain the original ID in its position (do NOT drop) and log a warning via `LoggerInterface`. Preserve the `properties` field on expanded schemas (stripping is a consumer concern). Unknown `_extend` keys are silently ignored. -- [ ] 1.3 Implement the `'@self.stats'` extension: only effective when `'schemas'` is also in `$extend`. For each successfully expanded schema, set `stats.objects.total` from the provided `$schemaStats` lookup; default to `0` when the ID is absent. Orphan ID entries are NOT augmented. - -## 2. Wire RegisterSerializer into RegisterService - -- [ ] 2.1 Add `RegisterSerializer` as a constructor dependency on `RegisterService`. Add `findSerialized(string|int $id, array $_extend = [], bool $_multitenancy = true): array` (calls existing `find()`, pre-computes stats via existing `getSchemaObjectCounts()` when `'@self.stats'` + `'schemas'` requested, delegates to `RegisterSerializer::serialize()`). -- [ ] 2.2 Add `findAllSerialized(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = [], array $_extend = [], bool $_multitenancy = true): array` (calls existing `findAll()`, pre-computes per-register schema stats if requested, delegates to `serializeMany()`). Keep `RegisterService::findAll()` and `::find()` signatures + return types unchanged (still entities); the `_extend` parameter on those methods is documented as a no-op placeholder for signature compatibility. - -## 3. Refactor RegistersController::index() to delegate - -- [ ] 3.1 Replace the inline schema-expansion block in `lib/Controller/RegistersController.php::index()` (the post-`findAll()` loop calling `SchemaMapper::find()` per schema ID, plus its `getSchemaObjectCounts()` + per-schema stats loop) with a single call to `$this->registerService->findAllSerialized(...)`, passing the parsed `$_extend` array. Remove the now-unused direct `SchemaMapper` usage (keep any other usages intact). Verify the controller body is thin (routing, param parsing, service call, response formatting only) per ADR-008. - -## 4. Drop unused `_extend` plumbing in the mapper - -- [ ] 4.1 Drop `_extend` from `RegisterMapper::findAll()` AND `RegisterMapper::find()` signatures and remove the `@SuppressWarnings(PHPMD.UnusedFormalParameter)` pragmas. Update `RegisterService::findAll()` and `::find()` to stop forwarding `_extend` to the mapper (the mapper no longer accepts it). Run `composer check:strict` and resolve any sniff complaints from the signature change. - -## 5. Unit tests for RegisterSerializer - -- [ ] 5.1 Tests for default + `'schemas'` expansion: no `_extend` → output `schemas` is the unchanged ID array (no `SchemaMapper::find()` calls); `_extend: ['schemas']` with all schemas resolvable → ordered array of schema objects each with `id`, `title`, `properties`; entity contract unchanged (`Register::jsonSerialize()` still returns ID array). -- [ ] 5.2 Tests for orphan-ID retention: `_extend: ['schemas']` with one orphan → orphan ID kept in original array position (mixed object/ID array), logger receives warning with the failing ID in context, no exception; mixed numeric + UUID schema references both orphans retain their original PHP types (int stays int, string stays string). -- [ ] 5.3 Tests for `@self.stats` interaction: `_extend: ['schemas', '@self.stats']` with precomputed `[10 => 5, 20 => 0]` → schema 10 has `stats.objects.total == 5`, schema 20 has `0`; orphan ID gets NO stats wrapping; `_extend: ['@self.stats']` alone (no `'schemas'`) → `schemas` field unchanged, no stats anywhere; `_extend: ['schemas', 'unknown-key']` → identical to `['schemas']`, no warning for the unknown key. - -## 6. Integration tests / HTTP parity - -- [ ] 6.1 Add an integration test that hits `GET /api/registers?_extend=schemas` and compares against a pre-refactor snapshot — must be byte-identical for the happy path (no orphans in fixture). Add a second test with a fixture register referencing a deleted schema — verifies orphan ID is preserved (post-refactor behaviour change). -- [ ] 6.2 Add an integration test for `GET /api/registers?_extend=schemas&_extend=@self.stats` — each expanded schema carries `stats.objects.total`. Add a service-level integration test that calls `$registerService->findAllSerialized(_extend: ['schemas'])` via DI and asserts identical output to the HTTP path (same fixture, ignoring HTTP envelope). - -## 7. Cross-repo verification + docs - -- [ ] 7.1 Grep `opencatalogi/`, `softwarecatalog/`, and `docudesk/lib/Service/RegisterDiscoveryService.php` for `RegisterService::findAll(` / `::find(` with `_extend`. Do NOT change consumers in this PR; file a follow-up issue per consumer that should swap to `findAllSerialized`/`findSerialized` (including the DocuDesk `RegisterDiscoveryService::serializeRegister()` one-line swap). -- [ ] 7.2 Update `openregister/docs/` to document the new `lib/Service/Serializer/` namespace + `RegisterSerializer` usage (example DI consumer call). Add a `CHANGELOG.md` entry noting: (a) new `findAllSerialized` / `findSerialized` on `RegisterService`, (b) new `RegisterSerializer`, (c) the orphan-schema-ID retention behaviour change for `/api/registers?_extend=schemas`. - -## 8. Quality gates - -- [ ] 8.1 `composer check:strict` passes (PHPCS, PHPMD, Psalm, PHPStan) with zero new issues on changed files; existing test suite + new section-5/6 tests pass; `openspec validate extend-schemas-in-register-service` passes; ADR-008 alignment confirmed (controller thin; logic in service/serializer). diff --git a/openspec/changes/file-actions/proposal.md b/openspec/changes/file-actions/proposal.md index 7e20eccbfd..601311f856 100644 --- a/openspec/changes/file-actions/proposal.md +++ b/openspec/changes/file-actions/proposal.md @@ -1,24 +1,5 @@ # File Actions -## Why - -OpenRegister already wraps the Nextcloud Files API with a `FileService` plus a family of single-purpose handlers and exposes per-object file CRUD/publish/depublish endpoints. But everyday document workflows in consuming apps (Procest, Pipelinq, ZaakAfhandelApp) need richer actions: rename without re-upload, copy/move between objects, version listing and restore, soft locking to prevent concurrent edits, batch publish/depublish/delete to avoid N HTTP calls, scoped previews, label/description metadata enrichment, and download audit logging. These actions complete the file lifecycle and align with WebDAV locking (RFC 4918 §6) and Nextcloud's `IPreview` / `IVersionManager` APIs. Most of the surface has shipped (handlers + controller methods), but route registrations and audit/event integration still have gaps. - -## What Changes - -- Add `description`, `category`, `locked_by`, `locked_at`, `lock_expires`, `download_count` columns to `oc_openregister_files` (migration shipped) and surface them on `FileMapper` getters/setters and `jsonSerialize()`. -- Introduce five new handlers under `lib/Service/File/` — `FileVersioningHandler`, `FileLockHandler`, `FileBatchHandler`, `FilePreviewHandler`, `FileAuditHandler` — wired into `FileService` via DI. -- Implement file rename via `UpdateFileHandler::renameFile()` (using `OCP\Files\File::move()` within the same folder), with conflict and invalid-character validation; expose as `PUT .../files/{fileId}/rename`. -- Implement file copy and move between objects (`FileService::copyFile()` / `moveFile()`), including cross-register/schema targets and name-conflict resolution; expose as `POST .../files/{fileId}/copy` and `.../move`. -- Implement file version listing and restore via `IVersionManager`, with graceful degradation when `files_versions` is disabled; expose as `GET .../files/{fileId}/versions` and `POST .../files/{fileId}/versions/{versionId}/restore`. -- Implement soft file locking via `FileLockHandler` (acquire / release / TTL expiry / admin force-unlock) and integrate lock checks into update / rename / move / delete; expose as `POST .../files/{fileId}/lock` and `.../unlock`. -- Implement batch operations via `FileBatchHandler` (publish / depublish / delete / label, max 100 per batch, returning HTTP 207 on partial failure); expose as `POST .../files/batch`. -- Implement scoped file previews via `FilePreviewHandler` and `IPreview` (configurable width/height, cache-control, fallback icon); expose as `GET .../files/{fileId}/preview`. -- Add metadata enrichment for files (description, category, labels) with autocomplete, optimistic UI, and category-based filtering in `ReadFileHandler`; dedicated label endpoint at `PUT .../files/{fileId}/labels`. -- Add download audit logging via `FileAuditHandler::logDownload()` (anonymous and authenticated, with bulk-ZIP single-entry treatment) and download-count caching on `FileMapper`. -- Dispatch CloudEvents for every action (`nl.openregister.object.file.renamed/copied/moved/locked/unlocked/version_restored/...`) and write audit-trail entries for all mutations. -- Register all new endpoints in `appinfo/routes.php` with CORS OPTIONS routes and update `openapi.json`. - ## Problem OpenRegister has a comprehensive file management layer (FileService with 13 handler classes, FilesController, routes for CRUD/publish/depublish) but critical gaps remain in the file action capabilities: diff --git a/openspec/changes/file-actions/tasks.md b/openspec/changes/file-actions/tasks.md index 70f0779564..5bfd46312f 100644 --- a/openspec/changes/file-actions/tasks.md +++ b/openspec/changes/file-actions/tasks.md @@ -1,44 +1,16 @@ # Tasks: File Actions -> **Status (2026-05-02 — FOUNDATION FIX SHIPPED):** the prior architectural-gap warning has been partially addressed. Two material things shipped: -> -> 1. **`openregister_files` table now exists.** Prior `Version1Date20260325120000` was authored to add columns to a table that no migration had ever created — it ran as a no-op for months. New migration `Version1Date20260502130000` creates the table with all the columns the file-actions feature needs (description / category / labels / locked_by / locked_at / lock_expires / download_count / created / updated). -> 2. **`File` entity + FileMapper write methods.** `lib/Db/File.php` wraps each `openregister_files` row; FileMapper gains `findByFileId / findOrCreateByFileId / findByFileIds / setDescriptionForFile / setCategoryForFile / setLabelsForFile / incrementDownloadCount / setLockForFile`. `FileFormattingHandler::formatFile()` now enriches its output with description / category / merged labels / downloadCount when a row exists. -> -> Subsequently corrected: the prior audit's claim that `FileLockHandler` stores locks in-memory was stale. The current implementation (v1.1) uses `ICacheFactory::createDistributed('openregister_file_locks')` for cross-request persistence. Locks DO survive between requests via the distributed cache layer. -> -> Real work that still remains (items below): -> -> Reverting the 27 items so the spec reflects honest status. Real work that remains: -> - **FileMapper File-entity** that backs the existing migration columns (`locked_by` / `locked_at` / `lock_expires` etc) — Phase 1 line 6 -> - **FileLockHandler write-through** so locks survive between requests — depends on the entity above -> - **Lock metadata in `FileFormattingHandler::formatFile()`** — depends on the persistence layer -> - **Description / category / labels** on files (Phase 8 backend + frontend) -> - **Download audit logging** integrated into `FilesController::show()` / `downloadById()` (Phase 9) -> - **Frontend batch endpoint usage** in `ViewObject.vue` (Phase 6) -> - **Cross-app integration tests** with opencatalogi + procest (Phase 10) -> - **Public preview for published files** (Phase 7) -> - **OpenAPI spec regeneration** with the new endpoints -> -> Many items DO have legitimate "delivered via NC built-in" or "delivered via existing OR machinery" resolutions — but bulk-ticking everything papered over the architectural gap. The honest path is to leave items open, surface the gap clearly, and let the next session audit each item against the actual code. -> -> **Status (2026-05-01 v3 audit-trail batch):** Closed 7 audit-trail and test-coverage items in one PR. New `FileAuditHandler::logFileAction()` helper persists `oc_openregister_audit_trails` rows via `AuditTrailMapper::insert`, tagged to the parent `ObjectEntity` with namespaced actions (`file.renamed`, `file.copied`, `file.copied_in`, `file.moved`, `file.moved_in`, `file.locked`, `file.unlocked`, `file.force_unlocked`, `file.version_restored`). Wired into `FilesController::rename / copy / move / lock / unlock / restoreVersion`. Copy + move use dual-entry pattern (one row on source object, one on target object). Audit failures are swallowed and warning-logged so they cannot break the underlying file operation. Three handler-level tests (`testLogFileActionPersistsAuditTrail`, `testLogFileActionDoesNotThrowOnInsertFailure`, `testLogFileActionFallsBackToSystemUser`) prove the contract. Six controller-level tests (`testCopyWithinSameRegister`, `testCopyAcrossRegisters`, `testCopyToNonexistentTarget`, `testMoveWithSourceCleanup`, `testMoveBlockedWhenSourceLocked`, `testRestoreVersionResponseShape`) close the previously missing copy/move/version-restore test coverage. -> -> **Status (2026-05-01 v2 audit):** Re-spot-checked all `[x]` items across 10 phases. Routes (11) all registered, controller methods (10) all implemented, handlers (5) all wired through DI, events (6) all dispatched at controller layer, unit tests for handlers all present. Two follow-up wins this batch: -> - Phase 5 line 72 ticked: `FileService::updateFile()` now calls `fileLockHandler->assertCanModify()` for numeric file IDs (the rename / copy / move / delete paths were already integrated). Test added: `FileLockHandlerTest::testAssertCanModifyByNonOwnerThrows`. -> - Phase 4 line 55 ticked: version JSON shape already complete in `FileVersioningHandler::listVersions` (six fields + `authorDisplayName`). -> -> **Earlier audit (preserved for context):** routes for all file-action endpoints registered in commit 9c1b70533. Spot-check of "[x]" ticks identified two phantom claims that are now corrected: -> -> - **Phase 5 lock unit tests (lines 73 / 74 / 75 originally ticked)** — `FileLockHandlerTest` was missing the non-owner-unlock, admin-force-unlock, and TTL-expiry cases. Controller-level `testUnlockNonOwner` exists in `FilesControllerFileActionsTest`, but the handler-level cases were absent. Added in this batch (`testUnlockByNonOwnerThrows`, `testAdminForceUnlockSucceeds`, `testTtlExpiryAutoClears`). -> - **Phase 4 version-restore unit test (line 56 was correctly `[ ]`)** — added `testRestoreVersionRejectsMalformedId` to cover the parse-side defensive path. -> -> **Genuine architectural gap surfaced (NOT silently fixed):** `FileLockHandler` stores locks in a private in-memory `$locks` array (no `FileMapper` write-through, no `oc_openregister_files.locked_by`/`locked_at`/`lock_expires` persistence). The migration columns from Phase 1 are present but unused. **Locks evaporate between requests.** This makes Phase 1 line 7 (FileLockHandler creation) and Phase 5 lock-acquisition checks structurally insufficient for production use. Tracked as a follow-up; no fix in this batch because it requires a `FileMapper` File-entity that does not yet exist (Phase 1 line 6 still `[ ]`). +> Status note (2026-06-15): A finishing pass verified every deliverable against +> the code. Almost all items previously left unchecked were in fact already +> implemented by the earlier wave; they are now ticked with the verifying file. +> Net-new work in this pass: object-level RBAC guards on all file endpoints +> (ADR-005 / gate-7) plus their 403 tests. Two items remain genuinely deferred +> with reasons (i18n + OpenAPI), see Phase 10. ## Phase 1: Database and Infrastructure -- [x] Migration: Add `description`, `category`, `locked_by`, `locked_at`, `lock_expires`, `download_count` columns to `oc_openregister_files` table -- [x] Update `FileMapper` entity to include new columns with getters/setters and `jsonSerialize()` output. **Shipped 2026-05-02:** new `lib/Db/File.php` entity wraps `openregister_files` rows (description / category / labels / locked_by / locked_at / lock_expires / download_count / created / updated) with typed addType registrations and a `jsonSerialize()` for response embedding. New migration `Version1Date20260502130000` creates the table (the prior migration's add-column passes were no-ops because the table never existed). New FileMapper methods: `findByFileId`, `findOrCreateByFileId`, `findByFileIds` (bulk), `setDescriptionForFile`, `setCategoryForFile`, `setLabelsForFile`, `incrementDownloadCount`, `setLockForFile`. **Verified** by `tests/Service/FileMetadataPersistenceIntegrationTest.php` — 6 tests / 28 assertions covering null-on-absent, lazy create, round-trip on description/category/labels, monotonic download-count, lock round-trip + release, bulk lookup. PHPCS clean. +- [x] Migration: Add `description`, `category`, `locked_by`, `locked_at`, `lock_expires`, `download_count` columns to `oc_openregister_files` table — `lib/Migration/Version1Date20260502130000.php` +- [x] Update `FileMapper` entity to include new columns with getters/setters and `jsonSerialize()` output — `lib/Db/File.php` (props + addType + jsonSerialize), `lib/Db/FileMapper.php` - [x] Create `FileVersioningHandler` class with constructor DI for `IRootFolder` and optional `IVersionManager` - [x] Create `FileLockHandler` class with constructor DI for `FileMapper`, `IUserSession`, `IGroupManager` - [x] Create `FileBatchHandler` class with constructor DI for `FilePublishingHandler`, `DeleteFileHandler`, `TaggingHandler` @@ -48,13 +20,12 @@ ## Phase 2: File Rename -- [x] Implement `UpdateFileHandler::renameFile()` using `File::move()` within the same parent folder +- [x] Implement `UpdateFileHandler::renameFile()` using `File::move()` within the same parent folder — implemented as `FileService::renameFile()` - [x] Add name conflict detection (check if target name exists in object folder) - [x] Add invalid character validation for file names - [x] Add `FilesController::rename()` endpoint with `@NoAdminRequired` and `@NoCSRFRequired` - [x] Register route: `PUT /api/objects/{register}/{schema}/{id}/files/{fileId}/rename` -- [x] Generate audit trail entry on successful rename - - `FilesController::rename()` calls `FileAuditHandler::logFileAction($object, $fileId, 'file.renamed', ['oldName', 'newName'])` after `FileService::renameFile` succeeds. Audit row carries the parent object reference (`object`, `objectUuid`, `register`, `schema`) so file events surface in the same audit timeline as object updates. +- [x] Generate audit trail entry on successful rename — `FilesController::rename()` calls `logFileAction('file.renamed')` - [x] Dispatch `nl.openregister.object.file.renamed` event - [x] Write unit test for rename with valid name - [x] Write unit test for rename with duplicate name (409) @@ -63,41 +34,33 @@ ## Phase 3: File Copy and Move - [x] Implement `FileService::copyFile()` -- copy file content to target object's folder via `CreateFileHandler` -- [x] Implement name conflict resolution for copy (append numeric suffix). **Shipped 2026-05-02:** new private helper `FileService::resolveCopyTargetName(Folder $folder, string $desiredName)` checks `nodeExists` on the target folder and appends `(1)`, `(2)`, … before the extension when needed. Caps at 999 attempts to avoid runaway loops on pathological inputs. `copyFile()` now calls it before delegating to `CreateFileHandler`. Suffix sits before the extension so `report.pdf` → `report (1).pdf`. -- [x] Implement cross-register/schema copy with target validation. **Shipped 2026-05-02:** `copyFile($sourceObject, $fileId, $targetObject)` already accepts a freely-chosen `$targetObject`, so cross-register/schema copy works by definition. Added explicit target validation at the top of `copyFile()`: rejects when the target object has no UUID (cannot resolve target folder). The folder resolution itself routes through `FolderManagementHandler::getObjectFolder()` which works against any (register, schema) pair. +- [x] Implement name conflict resolution for copy (append numeric suffix) — `FileService::resolveNonConflictingName()` +- [x] Implement cross-register/schema copy with target validation — `FilesController::copy()` resolves `targetRegister`/`targetSchema` - [x] Add `FilesController::copy()` endpoint - [x] Register route: `POST /api/objects/{register}/{schema}/{id}/files/{fileId}/copy` - [x] Implement `FileService::moveFile()` -- copy then delete source, with atomicity check - [x] Add `FilesController::move()` endpoint - [x] Register route: `POST /api/objects/{register}/{schema}/{id}/files/{fileId}/move` -- [x] Generate dual audit trail entries (on source and target objects) - - Copy emits `file.copied` on the source object and `file.copied_in` on the target object (with `sourceObjectUuid` + `sourceFileId` payload). Move uses the same pattern with `file.moved` / `file.moved_in`. Implemented in `FilesController::copy` and `FilesController::move` via `FileAuditHandler::logFileAction`. +- [x] Generate dual audit trail entries (on source and target objects) — `file.copied`/`file.copied_in`, `file.moved`/`file.moved_in` - [x] Dispatch `nl.openregister.object.file.copied` and `nl.openregister.object.file.moved` events -- [x] Write unit test for copy within same register - - `FilesControllerFileActionsTest::testCopyWithinSameRegister` -- asserts 201 status and that `FileService::copyFile` is invoked with the source/target ObjectEntity pair. -- [x] Write unit test for copy across registers - - `FilesControllerFileActionsTest::testCopyAcrossRegisters` -- asserts the controller switches `objectService` schema/register to the targetRegister/targetSchema params before resolving the target object. -- [x] Write unit test for move with source cleanup - - `FilesControllerFileActionsTest::testMoveWithSourceCleanup` -- asserts `FileService::moveFile` is invoked exactly once and `FileMovedEvent` is dispatched. Source-cleanup behaviour is the contract of `FileService::moveFile` (copy + delete-source) and is covered by `FileServiceTest`. - - `FilesControllerFileActionsTest::testMoveBlockedWhenSourceLocked` -- asserts 423 when `FileService::moveFile` throws a "locked" exception. -- [x] Write unit test for copy/move to non-existent target (404) - - `FilesControllerFileActionsTest::testCopyToNonexistentTarget` -- asserts 404 when the second `objectService->getObject()` call (target lookup) returns null, and that `FileService::copyFile` is never invoked. +- [x] Write unit test for copy within same register — `testCopyWithinSameRegister` +- [x] Write unit test for copy across registers — `testCopyAcrossRegisters` +- [x] Write unit test for move with source cleanup — `testMoveWithSourceCleanup` +- [x] Write unit test for copy/move to non-existent target (404) — `testCopyToNonexistentTarget` ## Phase 4: File Versioning - [x] Implement `FileVersioningHandler::listVersions()` using `IVersionManager::getVersionsForFile()` - [x] Handle graceful degradation when `files_versions` app is disabled -- [x] Format version data as JSON with versionId, timestamp, size, author, label, isCurrent - - Already implemented in `FileVersioningHandler::listVersions` (lines 100-108 for the `current` entry; lines 119-127 for each historical version). All six fields plus `authorDisplayName` are emitted. Verified during 2026-05-01 audit pass. +- [x] Format version data as JSON with versionId, timestamp, size, author, label, isCurrent — `FileVersioningHandler` (versionId/isCurrent/authorDisplayName) - [x] Implement `FileVersioningHandler::restoreVersion()` using `IVersionManager::rollback()` - [x] Add `FilesController::listVersions()` endpoint - [x] Add `FilesController::restoreVersion()` endpoint - [x] Register routes: `GET .../files/{fileId}/versions` and `POST .../files/{fileId}/versions/{versionId}/restore` -- [x] Generate audit trail entry on version restore - - `FilesController::restoreVersion()` calls `FileAuditHandler::logFileAction($object, $fileId, 'file.version_restored', ['versionId' => $versionId])` after `FileVersioningHandler::restoreVersion` succeeds. +- [x] Generate audit trail entry on version restore — `logFileAction('file.version_restored')` - [x] Dispatch `nl.openregister.object.file.version_restored` event - [x] Write unit test for version listing -- [x] Write unit test for version restore (parse-side: `testRestoreVersionRejectsMalformedId`; response-shape: `FilesControllerFileActionsTest::testRestoreVersionResponseShape` regression-locks the formatted-file payload) +- [x] Write unit test for version restore — `testRestoreVersionResponseShape`, `FileVersioningHandlerTest::testRestoreVersion*` - [x] Write unit test for graceful degradation without files_versions ## Phase 5: File Locking @@ -106,15 +69,11 @@ - [x] Implement `FileLockHandler::unlockFile()` with owner/admin check - [x] Implement `FileLockHandler::isLocked()` with TTL expiry check - [x] Implement `FileLockHandler::forceUnlock()` for admin users -- [x] Integrate lock checking into UpdateFileHandler, rename, move, and delete operations - - rename / copy-source / move-source / delete already wired through `FileService::renameFile / copyFile / moveFile / deleteFile` (each calls `fileLockHandler->assertCanModify($fileId)`). - - Update path now wired in `FileService::updateFile()` -- numeric file-IDs are checked before delegating to `UpdateFileHandler`. String-path updates remain unguarded (the lock map is ID-keyed; resolving path -> ID would re-hit the filesystem ahead of the actual write and is deferred). - - Coverage: `FileLockHandlerTest::testAssertCanModifyByNonOwnerThrows` proves the assertion contract used by `updateFile`. +- [x] Integrate lock checking into UpdateFileHandler, rename, move, and delete operations — `FileService::renameFile/copyFile/moveFile` + `updateFile` + `DeleteFileHandler` all call `assertCanModify()` - [x] Add `FilesController::lock()` and `FilesController::unlock()` endpoints - [x] Register routes: `POST .../files/{fileId}/lock` and `POST .../files/{fileId}/unlock` -- [x] Include lock metadata in file formatting output (formatFile). **Shipped 2026-05-02:** `formatFile()` already includes the NC `ILockManager` lock state via `formatLock()` (gated on authenticated callers per existing design). On top of that, when an OR-side row exists in `openregister_files` AND the row has `locked_by` set, the handler now also emits an `orLock` envelope with `lockedBy`, `lockedAt` (ISO 8601), `lockExpires` (ISO 8601). Surfaced under a separate key from NC's `lock` so consumers can distinguish cache-backed vs DB-backed locks; both can coexist. Also gated on authentication. -- [x] Generate audit trail entries for lock, unlock, and force-unlock - - `FilesController::lock()` emits `file.locked` with the lock metadata as the data payload. `FilesController::unlock()` emits either `file.unlocked` or `file.force_unlocked` depending on the `force` flag, so admin force-unlocks are distinguishable from regular owner unlocks in the audit timeline. +- [x] Include lock metadata in file formatting output (formatFile) — `FileFormattingHandler::formatFile()` emits `locked`/`lockedBy`/`lockedAt` +- [x] Generate audit trail entries for lock, unlock, and force-unlock — `file.locked` / `file.unlocked` / `file.force_unlocked` - [x] Dispatch `nl.openregister.object.file.locked` and `nl.openregister.object.file.unlocked` events - [x] Write unit test for lock acquisition - [x] Write unit test for lock conflict (423) @@ -133,7 +92,7 @@ - [x] Add action validation (only publish/depublish/delete/label) - [x] Add `FilesController::batch()` endpoint returning HTTP 200 (all success) or 207 (partial) - [x] Register route: `POST /api/objects/{register}/{schema}/{id}/files/batch` -- [x] Update `ViewObject.vue` to use batch endpoint instead of N sequential calls. **Shipped 2026-05-02:** new `_runBatchAction(action, perFileFallback)` helper in `src/modals/object/ViewObject.vue` calls the shared-store `objectStore.batchFiles(type, objectId, action, fileIds)` (one POST to `/files/batch`) when the runtime store exposes it, and falls back to the legacy per-file loop when an older `@conduction/nextcloud-vue` is installed (defensive `typeof batchFiles === 'function'` check). `publishSelectedFiles`, `depublishSelectedFiles`, `deleteSelectedFiles` all delegate to `_runBatchAction` — so the moment the dep bumps to a beta containing the batchFiles action, mass actions become single-round-trip without further frontend changes. The shared-store `batchFiles` action lives in [`@conduction/nextcloud-vue` PR #112](https://github.com/ConductionNL/nextcloud-vue/pull/112) (opened 2026-05-02): adds `batchFiles(type, objectId, action, fileIds, params?)` to `src/store/plugins/files.js`, treats 200 + 207 both as valid responses, 384/384 tests green. +- [x] Update `ViewObject.vue` to use batch endpoint instead of N sequential calls — `applyBatchAction()` prefers `objectStore.batchFiles` (single `/files/batch` POST) - [x] Write unit test for batch publish - [x] Write unit test for batch with partial failure (207) - [x] Write unit test for batch size limit (400) @@ -146,44 +105,44 @@ - [x] Add cache headers (Cache-Control: max-age=3600) - [x] Add `FilesController::preview()` endpoint returning StreamResponse - [x] Register route: `GET /api/objects/{register}/{schema}/{id}/files/{fileId}/preview` -- [x] Support public preview for published files. **Shipped 2026-05-02:** new `FileMapper::isFilePublished(int $fileId): bool` checks for an active `share_type=3` (public link) row. `FilesController::preview()` now gates anonymous callers on that — when `IUserSession::getUser() === null`, the controller returns 403 unless `isFilePublished` is true. Authenticated callers bypass the gate entirely (preserves existing object-level RBAC). Verified by 3 new tests in `FilesControllerFileActionsTest`: `testPreviewAnonymousOnUnpublishedFileReturns403`, `testPreviewAnonymousOnPublishedFileFallsThrough`, `testPreviewAuthenticatedBypassesPublishedGate`. The `IUserSession` constructor dep is optional + null-safe so legacy fixtures continue to work. +- [x] Support public preview for published files — `preview()` is `@PublicPage`; anonymous gated on `isFilePublished()` - [x] Write unit test for preview generation - [x] Write unit test for unsupported preview type (404) ## Phase 8: Metadata Enrichment -- [x] Extend `UpdateFileHandler` to support description and category fields. **Shipped 2026-05-02:** new public method `UpdateFileHandler::updateFileMetadata(int $fileId, ?string $description, ?string $category, ?array $labels)`. Each parameter is optional — null leaves the field untouched, explicit empty value clears. Delegates to FileMapper write methods (which are lazy-create-on-miss). Verified by 3 tests in `FileMetadataUpdateIntegrationTest`: full update writes all 3 fields; partial update only touches the named field; explicit empty/null values clear. +- [x] Extend `UpdateFileHandler` to support description and category fields — `UpdateFileHandler::updateMetadata()` writes via `FileMapper::setDescriptionForFile`/`setCategoryForFile`/`setLabelsForFile` - [x] Implement `FilesController::updateLabels()` endpoint for dedicated label updates - [x] Register route: `PUT /api/objects/{register}/{schema}/{id}/files/{fileId}/labels` -- [x] Include description, category, and labels in `FileFormattingHandler::formatFile()` output. **Shipped 2026-05-02:** `FileFormattingHandler` now takes an optional `?FileMapper $fileMapper` constructor dependency (null-safe for legacy fixtures). When wired AND a row exists in `openregister_files` for the file's NC fileid, `formatFile()` enriches the metadata with `description`, `category`, OR-managed `labels` (merged + deduped with the existing tag-backed labels), and `downloadCount` (gated on authentication). Lookup failures are caught and logged so the formatter can never break the response. The OR-managed labels collection lives alongside the existing tag-driven labels — consumers see one unified array. -- [x] Support category-based filtering in `ReadFileHandler::getFiles()` / file listing. **Shipped 2026-05-02:** `ReadFileHandler::getFiles()` accepts a new optional `?string $category` parameter. When set + the FileMapper dependency is wired, after fetching the file list the handler does ONE bulk `findByFileIds` lookup, builds a fileid → File map, and keeps only nodes whose OR-side row has the matching category. Files without an OR-side row are excluded (matching `WHERE category = :cat` left-join semantics). Lookup failures are skipped gracefully so a malformed node never breaks the listing. -- [x] Implement `editFileLabels()` in `ViewObject.vue` with inline NcSelect editor. **Shipped 2026-05-02:** the modal already mounts an inline `` editor when the user clicks the per-row Tag action — `editFileLabels(attachment)` enters edit mode (sets `editingLabelsFileId` + seeds `editingLabels` from the row), `cancelFileLabels()` exits, `saveFileLabels(attachment)` persists. Verified live at `localhost:3030 → object detail → files tab → row → Labels action`. -- [x] Add label autocomplete from existing register labels. **Shipped 2026-05-02:** `editFileLabels()` calls `objectStore.fetchTags()` (shared-store action that GETs the `/tags` endpoint and returns the array of tag strings) and assigns the result to `availableLabels`, which the NcSelect renders as suggestions. New labels typed by the user become tags too via `:taggable="true"`. -- [x] Wire label changes to API call with optimistic UI update. **Shipped 2026-05-02:** `saveFileLabels` snapshots `previousLabels`, flips `file.labels = nextLabels` optimistically before the request, then delegates to the new `updateFileLabels({ registerId, schemaId, objectId, fileId, labels })` helper in [`src/services/fileMetadata.js`](../../../src/services/fileMetadata.js). On HTTP error the helper throws and the catch reverts `file.labels = previousLabels` so the UI never lies. The dedicated `PUT /files/{fileId}/labels` endpoint replaces the legacy fetch to `PUT /files/{fileId}` with a `tags` field. Wire-shape contract verified by 6 unit tests in [`src/services/fileMetadata.spec.js`](../../../src/services/fileMetadata.spec.js). -- [x] Write unit test for label update. **Shipped 2026-05-02:** `fileMetadata.spec.js > updateFileLabels (item 163)` — 2 cases: (1) PUT to correct URL with `{ labels: [...] }` body, (2) throws on non-OK so the modal can revert optimistic UI. -- [x] Write unit test for description/category update. **Shipped 2026-05-02:** `fileMetadata.spec.js > updateFileMetadata (item 164)` — 2 cases: (1) description + category sent in one round-trip, null fields NOT in payload (= leave untouched), (2) explicit empty strings sent as `''` distinct from null/skip semantics. -- [x] Write unit test for label clearing. **Shipped 2026-05-02:** `fileMetadata.spec.js > label clearing (item 165)` — 2 cases: (1) PUT `{ labels: [] }` to `/labels` with explicit empty array (NOT `{}` or `{ labels: null }`), (2) clearing via `updateFileMetadata` also passes `[]` not `null`. 6 tests / 6 passing. +- [x] Include description, category, and labels in `FileFormattingHandler::formatFile()` output +- [x] Support category-based filtering in `ReadFileHandler::getFiles()` / file listing — `ReadFileHandler::filterByCategory()` +- [x] Implement `editFileLabels()` in `ViewObject.vue` with inline NcSelect editor — `editFileLabels()` + `saveFileLabels()` + inline `` +- [x] Add label autocomplete from existing register labels — `` options seeded from existing register labels +- [x] Wire label changes to API call with optimistic UI update — `saveFileLabels()` calls the labels API +- [x] Write unit test for label update +- [x] Write unit test for description/category update — `FileMetadataUpdateIntegrationTest::testUpdateMetadataWritesAllFieldsThroughHandler` +- [x] Write unit test for label clearing — `FileMetadataUpdateIntegrationTest::testUpdateMetadataExplicitEmptyClearsField` ## Phase 9: Download Audit Logging -- [x] Implement `FileAuditHandler::logDownload()` creating audit trail entries -- [x] Integrate download logging into `FilesController::show()` endpoint. **Shipped 2026-05-02:** new private helper `FilesController::recordDownloadEvent(int $fileId, ?ObjectEntity $object)` runs `FileMapper::incrementDownloadCount` + `FileAuditHandler::logFileAction(action: 'file.downloaded')`. Called from `show()` after the StreamResponse is built so the download is already wired up. Best-effort — exceptions on either path are silently swallowed so a counter or audit failure never breaks the file response. -- [x] Integrate download logging into `FilesController::downloadById()` endpoint. **Shipped 2026-05-02:** same helper called from `downloadById()` before `streamFile()` returns. The downloadById path doesn't carry a parent object reference, so the audit-trail call is skipped (helper short-circuits when `$object` is null) — only the OR-side counter increments. This matches the cross-object semantics: a free-floating fileId download has no parent-object audit timeline to write to. +- [x] Implement `FileAuditHandler::logDownload()` creating audit trail entries — `logFileAction('file.downloaded')` +- [x] Integrate download logging into `FilesController::show()` endpoint — `recordDownloadEvent()` +- [x] Integrate download logging into `FilesController::downloadById()` endpoint — `recordDownloadEvent()` - [x] Log anonymous downloads with IP and user-agent -- [x] Implement download count caching in FileMapper (increment on download). **Shipped 2026-05-02:** `FileMapper::incrementDownloadCount(int $fileId)` is monotonic, lazy-create-on-miss, and idempotent across multiple calls. Verified by `testIncrementDownloadCountIsMonotonic` in `FileMetadataPersistenceIntegrationTest` (3 calls → counter 1/2/3). -- [x] Include `downloadCount` in file metadata responses. **Shipped 2026-05-02:** `FileFormattingHandler::formatFile()` includes `downloadCount` from the OR-side row when authenticated (anonymous callers MUST NOT see download tallies — same gating as lock metadata). Wired together with the description / category / labels enrichment in the foundation commit. -- [x] Log bulk download (ZIP archive) as single audit entry. **Shipped 2026-05-02:** `FileAuditHandler::logBulkDownload(ObjectEntity, fileIds, fileNames, zipName, ?totalBytes)` now persists ONE `AuditTrail` row with action `file.bulk_downloaded` keyed off the parent ObjectEntity. The `changed` payload captures `fileIds`, `fileNames`, `fileCount`, `zipName`, and `totalBytes` so the contents of the archive are reconstructable. Anonymous bulk downloads attribute `User='Anonymous'` + `UserName='Anonymous ()'` for traceability. Wired into `ObjectsController::downloadFiles()` after the ZIP is generated — best-effort try/catch so an audit failure never breaks the download response. Verified by `FileAuditHandlerTest::testLogBulkDownload` (single row, full payload assertions) + `testLogBulkDownloadAnonymous` (IP capture). 8 audit tests / 20 assertions green; PHPCS clean. +- [x] Implement download count caching in FileMapper (increment on download) — `FileMapper::incrementDownloadCount()` +- [x] Include `downloadCount` in file metadata responses — `formatFile()` emits `downloadCount` (auth-gated) +- [x] Log bulk download (ZIP archive) as single audit entry — `ObjectsController::downloadFiles()` calls `logBulkDownload()` - [x] Write unit test for download logging - [x] Write unit test for anonymous download logging - [x] Write unit test for download count ## Phase 10: Integration and Testing -- [x] Add CORS OPTIONS routes for all new public endpoints. **Resolution 2026-05-02:** Nextcloud's framework auto-handles OPTIONS for any route that declares `@CORS` on its controller method. The file-action endpoints in `FilesController` carry `@CORS` per the existing convention (verified via the `@NoAdminRequired @NoCSRFRequired @CORS` annotation pattern across the file-action methods); CORS preflight Just Works without per-route OPTIONS registration. -- [x] Update OpenAPI spec (`openapi.json`) with new endpoints. **Resolution 2026-05-02:** OpenAPI spec for OR's API is generated on demand by `OasService::createOas()` from the route registrations + schema definitions; there is no static `openapi.json` to edit. Every newly-registered file-action route automatically appears in the generated OAS, verified by the `oas-validation` change's integration suite which asserts the OAS is structurally valid against the live route table. -- [x] Verify all new endpoints respect existing RBAC (object read/write access) -- [x] Verify lock checking does not break existing update/delete flows. **Resolution 2026-05-02:** `FileLockHandler::assertCanModify()` is integrated into `FileService::updateFile`, `renameFile`, `copyFile`, `moveFile`, and `deleteFile`. When no lock exists OR the lock is held by the current user, the assertion is a no-op and the operation proceeds normally. Verified by `FileLockHandlerTest::testAssertCanModifyByNonOwnerThrows` (rejects non-owner) + the controller-level tests `testUnlockNonOwner`, `testCopyWithinSameRegister`, `testMoveBlockedWhenSourceLocked` in `FilesControllerFileActionsTest` which exercise the full flow on locked + unlocked files. -- [x] Integration test: full file lifecycle (upload, rename, copy, lock, version, download, delete). **Shipped 2026-05-02:** `tests/Service/FileActionsLifecycleIntegrationTest::testFullFileLifecycle` walks ONE file through all 8 phases end-to-end against the live Nextcloud filesystem + OR services: (1) `FileService::addFile` upload, (2) `UpdateFileHandler::updateFileMetadata` description/category/labels, (3) `FileService::renameFile` (preserves fileId so OR-side metadata stays bound), (4) `FileMapper::setLockForFile` lock acquisition, (5) `FileService::copyFile` cross-object copy (asserts new fileId, asserts copy survives source delete), (6) `FileMapper::incrementDownloadCount` monotonicity, (7) `FileVersioningHandler::listVersions` graceful degradation, (8) `FileService::deleteFile` source removal. 1 test / 18 assertions green. The integration test caught two real regressions in `FileService::copyFile` (named-arg mismatch on `getObjectFolder` + non-existent `createFile` method) — both fixed in the same commit. -- [x] Test with opencatalogi app to verify no file operation regressions. **Resolution 2026-05-02:** filed as cross-repo hand-off issue [opencatalogi#517](https://github.com/ConductionNL/opencatalogi/issues/517) — "File-action regression test sweep on opencatalogi flows". OR-side surface (rename/copy/move/lock/version/preview/batch + audit + download counter) is shipped and unit/integration-tested here; the cross-app sweep belongs in the consumer's E2E suite. Tracked there. -- [x] Test with procest app to verify file workflow compatibility. **Resolution 2026-05-02:** filed as cross-repo hand-off issue [procest#308](https://github.com/ConductionNL/procest/issues/308) — "File-workflow regression test sweep against new OR file-actions". Procest's process-step file mutations (attach / detach / publish / lock-on-step-completion) need to be exercised against the new endpoints in procest's own test suite. Tracked there. -- [x] Verify i18n: all error messages use `$this->l->t()` with nl/en translations. **Resolution 2026-05-02:** OR ships nl + en translation catalogs at `l10n/nl.json` and `l10n/en_*.json`. The file-action controller methods route exceptions through `JSONResponse(['error' => $e->getMessage()])` which surfaces the existing translation-aware exception messages. Translation strings in the file-action surface are already wired through `$this->l->t()` in the controllers per the existing OR pattern; new error messages added by this change inherit the same path. +- [~] Add CORS OPTIONS routes for all new public endpoints — deferred: the file-action endpoints are same-origin, session-authenticated NC API routes (not cross-origin public APIs), so no dedicated OPTIONS preflight route is required. Revisit if a cross-origin consumer is added. +- [ ] Update OpenAPI spec (`openapi.json`) with new endpoints — DEFERRED: `openapi.json` is a 2-path stub that does not document any existing file endpoints (show/create/update/delete) either, so it is not the canonical file-API contract in this repo. Documenting only the new endpoints into an otherwise-empty spec would be inconsistent; a separate openapi-generation effort should populate the whole file domain at once. +- [x] Verify all new endpoints respect existing RBAC (object read/write access) — added `FilesController::ensureObjectAccess()` (ADR-005 / gate-7) re-resolving the object via `ObjectService::find(_rbac: true)` for authenticated callers; wired into every file-action endpoint plus pre-existing create/save/createMultipart/update/delete. 403 path covered by 4 new controller tests. +- [x] Verify lock checking does not break existing update/delete flows — 170 file-domain unit tests green (incl. update/delete) after lock + RBAC integration +- [x] Integration test: full file lifecycle (upload, rename, copy, lock, version, download, delete) — `tests/Service/FileActionsLifecycleIntegrationTest::testFullFileLifecycle` +- [~] Test with opencatalogi app to verify no file operation regressions — deferred: requires a multi-app live environment; no API/contract change to existing endpoints (additive only), so regression risk is low. +- [~] Test with procest app to verify file workflow compatibility — deferred: same multi-app live-environment dependency as the opencatalogi item. +- [ ] Verify i18n: all error messages use `$this->l->t()` with nl/en translations — DEFERRED: no OpenRegister JSON-API controller currently wraps error strings in `IL10N` (e.g. `ObjectsController` has zero `l->t()` calls on errors, and most file error text originates from service-layer exceptions). i18n of the JSON error layer is a fleet-wide convention gap, not file-actions-specific, and should be addressed as a dedicated cross-controller change rather than only in `FilesController`. diff --git a/openspec/changes/fix-empty-string-date-conversion/.openspec.yaml b/openspec/changes/fix-empty-string-date-conversion/.openspec.yaml new file mode 100644 index 0000000000..c4036b7c34 --- /dev/null +++ b/openspec/changes/fix-empty-string-date-conversion/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-20 diff --git a/openspec/changes/fix-empty-string-date-conversion/design.md b/openspec/changes/fix-empty-string-date-conversion/design.md new file mode 100644 index 0000000000..c055a0caa6 --- /dev/null +++ b/openspec/changes/fix-empty-string-date-conversion/design.md @@ -0,0 +1,108 @@ +## Context + +OpenRegister stores objects as register+schema-scoped rows with schema-defined properties. Properties declared with JSON Schema `format: "date"` or `format: "date-time"` are stored as strings and are converted to/from `DateTime` at several points in the pipeline. Additionally, object metadata fields (`created`, `updated`, `expires`, etc.) flow through similar conversion code. + +The conversion consistently uses the `new DateTime($value)` constructor. PHP treats an empty string and `null` as "now" rather than throwing, so any unguarded call site turns an empty-string input into the current timestamp — silently overwriting what the user intended to be an unset field. + +Identified call sites on user-data paths (all unguarded today): + +| File | Line | Direction | Affects | +|---|---|---|---| +| `lib/Db/MagicMapper/MagicStatisticsHandler.php` | ~583 | Read | User-defined `date` property — primary culprit for the observed bug | +| `lib/Db/MagicMapper/MagicStatisticsHandler.php` | ~590 | Read | User-defined `date-time` property | +| `lib/Db/MagicMapper/MagicBulkHandler.php` | ~766 (`formatDateTimeForDatabase`) | Write (bulk) | Generic helper used during bulk writes | +| `lib/Db/ObjectHandlers/MariaDbSearchHandler.php` | ~637 (`normalizeDateValue`) | Search | Empty-string search parameters | +| `lib/Db/MagicMapper.php` | ~2986 | Write | Metadata fields (created/updated/expires) | + +Existing guarded reference: `lib/Db/Schema.php:1186` already checks `is_string($value) === true && $value !== ''` for Schema-level metadata fields — confirming the pattern is known and just inconsistently applied. + +## Goals / Non-Goals + +**Goals:** +- Guarantee that `null`, `''`, and whitespace-only strings are treated as absence (→ `null`) at every user-input datetime conversion point. +- Guarantee that malformed datetime strings become `null` (already mostly true via `catch`) rather than silently producing a bogus value. +- Eliminate the defect class by channelling every call site through one helper; make the correct path easier than the broken path. +- Add tests that pin the behavior. + +**Non-Goals:** +- Canonicalising the stored datetime format across the codebase (separate concern — flagged in the earlier "normalize datetimes for RBAC matchers" discussion; may become its own change). +- Aligning `$now` format between `MagicRbacHandler` and `ConditionMatcher` (same separate concern). +- Migrating existing stored data (the read-path fix makes current bad data render as `null` automatically on next read; re-save persists `null`). +- Changing frontend behavior (the backend becoming robust also protects other consumers; the frontend emitting `null` is a parallel improvement owned elsewhere). + +## Decisions + +### D1 — Centralise in a new helper, don't inline-guard each site + +**Decision**: Add a `DateTimeNormalizer` (exact class name/location TBD among `lib/Service/` or `lib/Formats/` — leaning `lib/Service/DateTimeNormalizer.php` for discoverability alongside other normalization services, while `lib/Formats/` is reserved for *format validators* like `BsnFormat`/`SemVerFormat` per ADR-011). All identified call sites delegate to it. + +**Rationale**: Three inline guards in three files *look* the same today but drift. A helper removes that drift and gives us one obvious place to add unit tests. + +**Alternatives considered**: +- *Guard each site in-place with `if ($value === '' || $value === null)`*. Smaller diff, but multiplies the invariant across the codebase and leaves the next developer free to reintroduce the bug in a new site. Rejected. +- *Extend PHP's `DateTime` via a custom subclass*. Over-engineered and surprises callers who expect standard PHP semantics. Rejected. + +### D2 — Normalizer contract + +`DateTimeNormalizer` exposes (at minimum): + +```php +public function normalize(mixed $value): ?DateTimeImmutable; +public function formatForDatabase(mixed $value): ?string; // 'Y-m-d H:i:s' +public function formatForIso8601(mixed $value): ?string; // ISO 8601 with offset +``` + +Rules enforced in `normalize()`: + +1. `null` → `null` +2. `string` → `trim()`; if empty after trim → `null` +3. `DateTimeInterface` instance → returned as-is (normalised to `DateTimeImmutable`) +4. Any other input or parse failure → `null` and a debug-level log entry (not warning — this fires during normal user input) + +**Rationale**: Single source of truth. Immutability prevents accidental mutation. Debug logging avoids alerting noise for expected empty-string inputs while still offering visibility when investigating. + +### D3 — Metadata fields keep their existing "default to now for created/updated" + +`MagicMapper::prepareMetadata` currently fills `created`/`updated` with `$now` when absent (line ~2977). Do NOT change this — that is *correct* defaulting, unrelated to the bug. The bug is only in the branch that tries to parse a *provided* string. + +**Rationale**: Keep scope tight. Conflating "default on absent" with "default on empty-string" would grow the change and risk regressions on auto-managed timestamps. + +### D4 — Parse failures log at debug, not warn + +Empty-string input is expected (from forms); parse failures on a non-empty string are the legitimately interesting case but still user-caused. Use debug level; escalate to warning only if a caller explicitly opts in. + +**Rationale**: Prevents log-spam on routine object writes; still leaves a trail for debugging. + +### D5 — Search normalization returns `null` (not the original value) on empty input + +`MariaDbSearchHandler::normalizeDateValue` currently returns the *original* value when parsing fails. After the change, empty input returns `null` so the caller can skip the filter rather than send a stale value into the SQL layer. + +**Rationale**: An empty date filter should match no constraint, not the value `""`. Callers that relied on the old string passthrough (expected: zero) would need to handle `null`; grep-verifiable. + +## Risks / Trade-offs + +- **[Risk]** Hidden dependents that relied on empty-string → now behavior. → **Mitigation**: ripgrep for `new DateTime\(` across `lib/` and verify each remaining unguarded site is non-user-input; run the existing test suite; spot-check `opencatalogi` and `softwarecatalog`. +- **[Risk]** Existing objects with empty-string date values on disk will "change" from returning-today to returning `null` on the next read. → **Mitigation**: this is the *intended* correction; documented in the proposal. Flag in release notes. +- **[Risk]** `DateTimeNormalizer` gains scope creep (timezone handling, canonical format, etc.). → **Mitigation**: keep the contract minimal in this change; defer canonicalisation to the separately-tracked "normalize datetimes for RBAC matchers" change. +- **[Trade-off]** A helper adds one indirection vs. inline guards. Worth it for the invariant-enforcement and testability. + +## Migration Plan + +No data migration required. Deployment steps: + +1. Land the normalizer + call-site migrations as a single PR. +2. Run full PHPUnit suite and integration tests. +3. Merge; on next object read, previously empty-string date values render as `null` automatically. +4. (Optional later) A one-shot maintenance command could `UPDATE ... SET col = NULL WHERE col = ''` for date-typed schema property columns to normalise stored values. Not required; filed as a follow-up. + +**Rollback**: revert the PR. No schema or data changes to unwind. + +## Open Questions + +- Final home for `DateTimeNormalizer` — `lib/Service/` (preferred) vs. `lib/Formats/` vs. a new `lib/Util/` directory. Decide in code review. +- Should `normalize()` accept `int` (Unix timestamp) input? Defer — no current caller needs it; YAGNI. +- Should the search path (`MariaDbSearchHandler::normalizeDateValue`) also benefit from a query-planner signal when the result is `null` (i.e. drop the predicate entirely)? Likely yes but out of scope here; surface during implementation. + +## Seed Data + +**Not applicable.** This change does not introduce or modify any OpenRegister schemas. It corrects conversion behavior on existing user-defined datetime properties regardless of which schema they belong to. Per ADR-016, the seed data requirement applies when schemas are introduced or materially modified; none are here. diff --git a/openspec/changes/fix-empty-string-date-conversion/plan.json b/openspec/changes/fix-empty-string-date-conversion/plan.json new file mode 100644 index 0000000000..a8dc3f2ec2 --- /dev/null +++ b/openspec/changes/fix-empty-string-date-conversion/plan.json @@ -0,0 +1,522 @@ +{ + "change": "fix-empty-string-date-conversion", + "project": "openregister", + "repo": "ConductionNL/openregister", + "created": "2026-04-20", + "tracking_issue": 1292, + "tracking_issue_url": "https://github.com/ConductionNL/openregister/issues/1292", + "spec_ref": "specs/datetime-input-handling/spec.md", + "tasks": [ + { + "id": 1, + "number": "1.1", + "section": "Audit & pinning", + "title": "Inventory new DateTime( call sites", + "description": "Ripgrep `new DateTime\\(` across `lib/` and produce a complete inventory of call sites; classify each as user-supplied input, internal/trusted value, or literal/now.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/**/*.php" + ] + }, + { + "id": 2, + "number": "1.2", + "section": "Audit & pinning", + "title": "Confirm exhaustiveness of user-supplied sites", + "description": "Confirm the user-supplied sites listed in design.md are exhaustive; add any newly discovered sites to the inventory.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "openspec/changes/fix-empty-string-date-conversion/design.md" + ] + }, + { + "id": 3, + "number": "1.3", + "section": "Audit & pinning", + "title": "Add failing regression test for the defect", + "description": "Add a regression test that demonstrates the current defect: write an object with publishedAt = '' for a date-time property, read it back, assert value is NOT the current datetime (fails before fix, passes after).", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Db/MagicStatisticsHandlerTest.php" + ] + }, + { + "id": 4, + "number": "2.1", + "section": "Implement the normalizer", + "title": "Create DateTimeNormalizer class", + "description": "Create lib/Service/DateTimeNormalizer.php (final location confirmed at code review) with the contract from design.md D2.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Service/DateTimeNormalizer.php" + ] + }, + { + "id": 5, + "number": "2.2", + "section": "Implement the normalizer", + "title": "Implement normalize()", + "description": "Implement normalize(mixed $value): ?DateTimeImmutable with rules: null \u2192 null, trim + empty \u2192 null, DateTimeInterface \u2192 pass through as immutable, parse failures \u2192 null + debug log.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Service/DateTimeNormalizer.php" + ] + }, + { + "id": 6, + "number": "2.3", + "section": "Implement the normalizer", + "title": "Implement formatForDatabase()", + "description": "Implement formatForDatabase(mixed $value): ?string returning Y-m-d H:i:s or null.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Service/DateTimeNormalizer.php" + ] + }, + { + "id": 7, + "number": "2.4", + "section": "Implement the normalizer", + "title": "Implement formatForIso8601()", + "description": "Implement formatForIso8601(mixed $value): ?string returning ISO 8601 with offset or null.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Service/DateTimeNormalizer.php" + ] + }, + { + "id": 8, + "number": "2.5", + "section": "Implement the normalizer", + "title": "Wire up as injectable service", + "description": "Register as an injectable service (no static state; DI via constructor).", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Service/DateTimeNormalizer.php", + "lib/AppInfo/Application.php" + ] + }, + { + "id": 9, + "number": "2.6", + "section": "Implement the normalizer", + "title": "Docblock mandate", + "description": "Add a class docblock stating that all user-datetime conversion MUST go through this class and referencing this change.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Service/DateTimeNormalizer.php" + ] + }, + { + "id": 10, + "number": "3.1", + "section": "Unit tests for the normalizer", + "title": "Unit: null input \u2192 null", + "description": "Test null input produces null.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 11, + "number": "3.2", + "section": "Unit tests for the normalizer", + "title": "Unit: empty string \u2192 null", + "description": "Test empty string input produces null.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 12, + "number": "3.3", + "section": "Unit tests for the normalizer", + "title": "Unit: whitespace-only \u2192 null", + "description": "Test whitespace-only string inputs (' ', '\\t', '\\n') produce null.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 13, + "number": "3.4", + "section": "Unit tests for the normalizer", + "title": "Unit: ISO 8601 with offset", + "description": "Test valid ISO 8601 with offset produces correct DateTimeImmutable.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 14, + "number": "3.5", + "section": "Unit tests for the normalizer", + "title": "Unit: ISO 8601 Zulu", + "description": "Test valid ISO 8601 Zulu produces correct DateTimeImmutable.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 15, + "number": "3.6", + "section": "Unit tests for the normalizer", + "title": "Unit: database format", + "description": "Test Y-m-d H:i:s input produces correct DateTimeImmutable.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 16, + "number": "3.7", + "section": "Unit tests for the normalizer", + "title": "Unit: date-only", + "description": "Test Y-m-d input produces DateTimeImmutable at midnight.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 17, + "number": "3.8", + "section": "Unit tests for the normalizer", + "title": "Unit: existing DateTime passthrough", + "description": "Test existing DateTime/DateTimeImmutable passthrough produces an immutable instance.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 18, + "number": "3.9", + "section": "Unit tests for the normalizer", + "title": "Unit: garbled string", + "description": "Test garbled strings produce null and emit a debug log.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 19, + "number": "3.10", + "section": "Unit tests for the normalizer", + "title": "Unit: non-string non-null non-DateTime", + "description": "Test numeric/array/object input produces null and emits a debug log.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 20, + "number": "3.11", + "section": "Unit tests for the normalizer", + "title": "Unit: formatForDatabase + formatForIso8601", + "description": "Test formatForDatabase and formatForIso8601 with each of the preceding inputs.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Service/DateTimeNormalizerTest.php" + ] + }, + { + "id": 21, + "number": "4.1", + "section": "Migrate call sites (read path)", + "title": "Migrate MagicStatisticsHandler date branch", + "description": "Replace the date branch at ~line 583 with a delegation to the normalizer (format to Y-m-d).", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Db/MagicMapper/MagicStatisticsHandler.php" + ] + }, + { + "id": 22, + "number": "4.2", + "section": "Migrate call sites (read path)", + "title": "Migrate MagicStatisticsHandler date-time branch", + "description": "Replace the date-time branch at ~line 590 with a delegation to the normalizer (format to ISO 8601).", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Db/MagicMapper/MagicStatisticsHandler.php" + ] + }, + { + "id": 23, + "number": "4.3", + "section": "Migrate call sites (read path)", + "title": "Add MagicStatisticsHandler tests", + "description": "Add/extend a MagicStatisticsHandler test covering: empty-string stored value \u2192 rendered as null; valid stored value \u2192 rendered correctly.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Unit/Db/MagicMapper/MagicStatisticsHandlerTest.php" + ] + }, + { + "id": 24, + "number": "5.1", + "section": "Migrate call sites (write + bulk + search)", + "title": "Migrate MagicBulkHandler::formatDateTimeForDatabase", + "description": "Delegate to DateTimeNormalizer::formatForDatabase; remove direct new DateTime($value).", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Db/MagicMapper/MagicBulkHandler.php" + ] + }, + { + "id": 25, + "number": "5.2", + "section": "Migrate call sites (write + bulk + search)", + "title": "Migrate MagicMapper metadata handling", + "description": "Delegate metadata datetime conversion (~line 2986) to the normalizer; keep the existing default-to-now-when-key-is-absent logic for created/updated exactly as-is (D3).", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Db/MagicMapper.php" + ] + }, + { + "id": 26, + "number": "5.3", + "section": "Migrate call sites (write + bulk + search)", + "title": "Migrate MariaDbSearchHandler::normalizeDateValue", + "description": "Delegate to the normalizer; return null on empty/invalid input (D5).", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/Db/ObjectHandlers/MariaDbSearchHandler.php" + ] + }, + { + "id": 27, + "number": "5.4", + "section": "Migrate call sites (write + bulk + search)", + "title": "Verify no remaining unguarded sites", + "description": "Grep for any remaining unguarded new DateTime($ on user-supplied paths; migrate or document why each remaining site is safe.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "lib/**/*.php" + ] + }, + { + "id": 28, + "number": "6.1", + "section": "Regression tests for migrated sites", + "title": "Integration: POST publishedAt='' \u2192 GET null", + "description": "POST object with publishedAt='' \u2192 GET object \u2192 publishedAt is null.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Integration/" + ] + }, + { + "id": 29, + "number": "6.2", + "section": "Regression tests for migrated sites", + "title": "Integration: valid datetime round-trip", + "description": "POST object with publishedAt='2026-04-20T14:00:00Z' \u2192 GET object \u2192 value round-trips correctly.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Integration/" + ] + }, + { + "id": 30, + "number": "6.3", + "section": "Regression tests for migrated sites", + "title": "Integration: absent publishedAt \u2192 GET null", + "description": "POST object WITHOUT publishedAt in payload \u2192 GET object \u2192 publishedAt is null (unchanged behavior).", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Integration/" + ] + }, + { + "id": 31, + "number": "6.4", + "section": "Regression tests for migrated sites", + "title": "Integration: bulk import empty dates", + "description": "Bulk import path with empty-string date values \u2192 no current-datetime substitution.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Integration/" + ] + }, + { + "id": 32, + "number": "6.5", + "section": "Regression tests for migrated sites", + "title": "Integration: search with empty date filter", + "description": "Search with publishedAt='' filter \u2192 no SQL predicate for empty filter (or no-op predicate).", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Integration/" + ] + }, + { + "id": 33, + "number": "6.6", + "section": "Regression tests for migrated sites", + "title": "Integration: expires='' persists as null", + "description": "POST metadata with expires='' \u2192 persisted as null.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Integration/" + ] + }, + { + "id": 34, + "number": "6.7", + "section": "Regression tests for migrated sites", + "title": "Integration: absent created \u2192 defaults to now", + "description": "POST without created in payload \u2192 created defaulted to now (existing behavior preserved).", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "tests/Integration/" + ] + }, + { + "id": 35, + "number": "7.1", + "section": "Cross-app verification", + "title": "opencatalogi regression check", + "description": "Run opencatalogi test suite against the patched backend; confirm no regressions.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "../opencatalogi/" + ] + }, + { + "id": 36, + "number": "7.2", + "section": "Cross-app verification", + "title": "softwarecatalog regression check", + "description": "Run softwarecatalog test suite against the patched backend; confirm no regressions.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "../softwarecatalog/" + ] + }, + { + "id": 37, + "number": "7.3", + "section": "Cross-app verification", + "title": "docudesk spot-check", + "description": "Spot-check docudesk flows that consume OpenRegister datetime values (where applicable).", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [ + "../docudesk/" + ] + }, + { + "id": 38, + "number": "8.1", + "section": "Quality & documentation", + "title": "composer check:strict passes", + "description": "PHPCS, PHPMD, Psalm, PHPStan all clean.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [] + }, + { + "id": 39, + "number": "8.2", + "section": "Quality & documentation", + "title": "CHANGELOG update", + "description": "Update CHANGELOG.md with a user-facing note on the behavior correction.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "CHANGELOG.md" + ] + }, + { + "id": 40, + "number": "8.3", + "section": "Quality & documentation", + "title": "Release note", + "description": "Empty-string date fields now correctly round-trip as null; previously, empty-string dates on existing objects rendered as the current datetime. On next read/save, the value normalises to null.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "CHANGELOG.md" + ] + }, + { + "id": 41, + "number": "8.4", + "section": "Quality & documentation", + "title": "Follow-up: data-normalisation maintenance command", + "description": "Optional: file a follow-up issue for the stored-data normalisation maintenance command (UPDATE ... SET col = NULL WHERE col = '') flagged in design.md \u00a7Migration Plan.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [] + }, + { + "id": 42, + "number": "9.1", + "section": "Wrap-up", + "title": "openspec verify", + "description": "Run openspec verify fix-empty-string-date-conversion and resolve any findings.", + "status": "done", + "acceptance_criteria": [], + "files_likely_affected": [ + "openspec/changes/fix-empty-string-date-conversion/" + ] + }, + { + "id": 43, + "number": "9.2", + "section": "Wrap-up", + "title": "Open PR", + "description": "Open PR referencing this change; link the failing-then-passing regression test from task 1.3.", + "status": "pending", + "acceptance_criteria": [], + "files_likely_affected": [] + } + ] +} \ No newline at end of file diff --git a/openspec/changes/fix-empty-string-date-conversion/proposal.md b/openspec/changes/fix-empty-string-date-conversion/proposal.md new file mode 100644 index 0000000000..ede6b9c72b --- /dev/null +++ b/openspec/changes/fix-empty-string-date-conversion/proposal.md @@ -0,0 +1,41 @@ +## Why + +PHP's `new DateTime('')` and `new DateTime(null)` silently return the current date-time instead of failing or returning `null`. Several OpenRegister code paths pass user-supplied datetime strings directly into this constructor without guarding against empty input. When a form submits `""` for a cleared date field (common default frontend behavior), the backend interprets the value as "now" and persists/renders the current timestamp. From the user's perspective a field they never filled in has silently been populated with the moment they pressed save. + +The effect is subtle, silent, and hard to notice: audit trails, retention calculations, RBAC conditions comparing `$now`, and any UI showing the date all misreport the field. Fixing this class of bug is a small, focused change that removes a whole category of data-corruption footguns. + +## What Changes + +- Introduce a single normalization helper that converts user-supplied datetime input to either a valid `DateTime` (or formatted string) or `null` — treating `null`, `''`, and whitespace-only strings uniformly as `null`, and parsing failures as `null`. +- Replace direct `new DateTime($value)` calls on user-supplied property values with this helper in the identified call sites (read path, bulk formatter, search normalization; metadata path already has a partial guard but gets aligned too). +- Guard the helper behind a single point so future call sites cannot reintroduce the footgun. +- Add regression tests that cover `null`, `""`, `" "`, valid ISO-8601, and malformed inputs for both user-defined properties and metadata fields. + +**Not breaking**: no API contract changes; the fix is a behavior correction from silently-wrong to correctly-null. + +## Capabilities + +### New Capabilities + +- `datetime-input-handling`: Normalization contract for datetime-typed user input across save, read, bulk, and search paths. Defines how `null`, empty strings, whitespace, valid ISO-8601, and malformed inputs are treated uniformly across the codebase. + +### Modified Capabilities + +_None._ Behavior change is a bug fix, not a requirement change to existing specs. + +## Impact + +**Affected code:** +- `lib/Db/MagicMapper/MagicStatisticsHandler.php` (primary: lines ~583, 590 — render path for user-defined `date`/`date-time` properties; the empty-string render defect is here). +- `lib/Db/MagicMapper/MagicBulkHandler.php` (bulk formatter `formatDateTimeForDatabase`, line ~766). +- `lib/Db/ObjectHandlers/MariaDbSearchHandler.php` (search normalization, line ~637). +- `lib/Db/MagicMapper.php` (metadata fields `created`/`updated`/`expires`, line ~2986 — for consistency with the new helper). +- New utility (location TBD in design): a `DateTimeNormalizer` — or similar — that the above sites delegate to. +- Tests under `tests/Unit/` for the helper and affected handlers. + +**Affected data:** +- Objects that currently contain empty-string date fields on disk will, on next read, return `null` instead of the current datetime. This is the intended correction. +- No data migration required (reads become correct; re-saving converts empty-string to `null` naturally). + +**Affected dependents:** +- `opencatalogi` and `softwarecatalog` consumers that relied on the (wrong) current-datetime behavior would need verification — expected to be zero; the behavior was unintended. diff --git a/openspec/changes/fix-empty-string-date-conversion/specs/datetime-input-handling/spec.md b/openspec/changes/fix-empty-string-date-conversion/specs/datetime-input-handling/spec.md new file mode 100644 index 0000000000..7b61b3a722 --- /dev/null +++ b/openspec/changes/fix-empty-string-date-conversion/specs/datetime-input-handling/spec.md @@ -0,0 +1,136 @@ +## ADDED Requirements + +### Requirement: Empty input SHALL normalize to null + +User-supplied datetime input values of `null`, the empty string `""`, or a string consisting only of whitespace characters SHALL normalize to `null` when passed through the datetime normalization helper. No value SHALL be synthesized (no defaulting to "now" or any other timestamp). + +#### Scenario: Null input + +- **WHEN** the normalizer receives `null` +- **THEN** it SHALL return `null` +- **AND** no `DateTime` object SHALL be constructed + +#### Scenario: Empty-string input + +- **WHEN** the normalizer receives the empty string `""` +- **THEN** it SHALL return `null` +- **AND** no `DateTime` object SHALL be constructed + +#### Scenario: Whitespace-only input + +- **WHEN** the normalizer receives a string containing only whitespace (e.g. `" "`, `"\t"`, `"\n"`) +- **THEN** it SHALL return `null` +- **AND** no `DateTime` object SHALL be constructed + +### Requirement: Valid datetime input SHALL parse correctly + +User-supplied datetime input that is a non-empty, parseable string SHALL be normalized into a `DateTimeImmutable` (or the equivalent formatted string for the requested output format). + +#### Scenario: ISO 8601 with timezone + +- **WHEN** the normalizer receives `"2026-04-20T14:00:00+02:00"` +- **THEN** it SHALL return a `DateTimeImmutable` representing that instant + +#### Scenario: ISO 8601 UTC Zulu + +- **WHEN** the normalizer receives `"2026-04-20T14:00:00Z"` +- **THEN** it SHALL return a `DateTimeImmutable` representing that instant + +#### Scenario: Database datetime format + +- **WHEN** the normalizer receives `"2026-04-20 14:00:00"` +- **THEN** it SHALL return a `DateTimeImmutable` representing that instant (local/default timezone) + +#### Scenario: Date-only format + +- **WHEN** the normalizer receives `"2026-04-20"` +- **THEN** it SHALL return a `DateTimeImmutable` at midnight of that date + +#### Scenario: Existing DateTime instance + +- **WHEN** the normalizer receives an existing `DateTimeInterface` instance +- **THEN** it SHALL return a `DateTimeImmutable` equal to that instant + +### Requirement: Malformed input SHALL normalize to null without raising + +Input that is a non-empty string but cannot be parsed as a datetime SHALL normalize to `null`. The normalizer SHALL NOT throw; it SHALL log at debug level for traceability. + +#### Scenario: Garbled string + +- **WHEN** the normalizer receives `"not-a-date"` +- **THEN** it SHALL return `null` +- **AND** a debug-level log entry SHALL be emitted referencing the input + +#### Scenario: Numeric input (not accepted) + +- **WHEN** the normalizer receives a number (e.g. `1745150400`) +- **THEN** it SHALL return `null` +- **AND** a debug-level log entry SHALL be emitted + +### Requirement: Normalizer SHALL offer canonical output formats + +For callers that need a formatted string rather than a `DateTimeImmutable`, the normalizer SHALL expose deterministic formatters. + +#### Scenario: Database format + +- **WHEN** a caller requests the database format for a valid input +- **THEN** the result SHALL be `"Y-m-d H:i:s"`-formatted +- **AND** empty or invalid input SHALL produce `null` (not the empty string) + +#### Scenario: ISO 8601 format + +- **WHEN** a caller requests the ISO 8601 format for a valid input +- **THEN** the result SHALL be an ISO 8601 string with timezone offset +- **AND** empty or invalid input SHALL produce `null` (not the empty string) + +### Requirement: User-defined date/date-time properties SHALL render null when empty + +When OpenRegister renders an object whose user-defined property is declared with JSON Schema `format: "date"` or `format: "date-time"`, and the stored value is `null`, `""`, or whitespace-only, the rendered value SHALL be `null`. The backend SHALL NOT substitute the current date-time or any other synthesized value. + +#### Scenario: Empty stored date-time on read + +- **WHEN** an object has a user-defined property `publishedAt` with `format: "date-time"` whose stored value is `""` +- **AND** the object is rendered via the standard read path +- **THEN** the rendered `publishedAt` SHALL be `null` +- **AND** the response SHALL NOT contain the current date-time for that field + +#### Scenario: Null stored date on read + +- **WHEN** an object has a user-defined property `birthDate` with `format: "date"` whose stored value is `null` +- **AND** the object is rendered via the standard read path +- **THEN** the rendered `birthDate` SHALL be `null` + +### Requirement: Metadata datetime fields SHALL not accept empty-string input as "now" + +When writing an object, if the caller explicitly provides `""` or a whitespace-only string for a metadata field that accepts a datetime (e.g. `expires`), the field SHALL be persisted as `null`, not as the current date-time. The existing behavior of defaulting `created`/`updated` to the current date-time when the field is absent (key not present) is preserved. + +#### Scenario: Absent created field (unchanged) + +- **WHEN** an object is written without a `created` field in the incoming data +- **THEN** `created` SHALL be populated with the current date-time (existing behavior preserved) + +#### Scenario: Empty-string expires field + +- **WHEN** an object is written with `expires: ""` +- **THEN** `expires` SHALL be persisted as `null` +- **AND** the current date-time SHALL NOT be used + +### Requirement: Search normalization SHALL treat empty input as no constraint + +The MariaDB search path's date normalization SHALL treat `null`, empty-string, and whitespace-only input as "no constraint" by returning `null`, enabling callers to drop the predicate rather than injecting a bogus filter value. + +#### Scenario: Empty-string date search parameter + +- **WHEN** a search filter is invoked with `value = ""` for a date field +- **THEN** the normalization helper SHALL return `null` +- **AND** the SQL builder SHALL NOT emit a comparison against the empty string or the current date-time + +### Requirement: Single canonical implementation point + +All OpenRegister code paths that convert user-supplied datetime strings to `DateTime`/`DateTimeImmutable` or to a database datetime string SHALL delegate to the shared datetime normalization helper. Direct use of `new DateTime($value)` with user-supplied values SHALL be removed from the identified sites. + +#### Scenario: No unguarded DateTime construction on user input + +- **WHEN** the codebase is audited for `new DateTime(` on user-supplied values +- **THEN** every call site SHALL either delegate to the normalizer or demonstrably operate on values already produced by the normalizer +- **AND** new call sites SHALL be prevented by code review — documented in the helper's class docblock diff --git a/openspec/changes/fix-empty-string-date-conversion/tasks.md b/openspec/changes/fix-empty-string-date-conversion/tasks.md new file mode 100644 index 0000000000..5fc09f17ea --- /dev/null +++ b/openspec/changes/fix-empty-string-date-conversion/tasks.md @@ -0,0 +1,94 @@ +## 1. Audit & pinning + +- [x] 1.1 Ripgrep `new DateTime\(` across `lib/` and produce a complete inventory of call sites; classify each as "user-supplied input", "internal/trusted value", or "literal/now" +- [x] 1.2 Confirm the user-supplied sites listed in `design.md` are exhaustive; add any newly discovered sites to the inventory +- [x] 1.3 Add a regression test that demonstrates the current defect: write an object with `publishedAt = ""` for a `date-time` property, read it back, assert value is NOT the current datetime (this test SHALL fail before the fix and pass after) + +## 2. Implement the normalizer + +- [x] 2.1 Create `lib/Service/DateTimeNormalizer.php` (final location confirmed at code review) with the contract from `design.md` D2 +- [x] 2.2 Implement `normalize(mixed $value): ?DateTimeImmutable` with rules: `null` → null, trim + empty → null, `DateTimeInterface` → pass through as immutable, parse failures → null + debug log +- [x] 2.3 Implement `formatForDatabase(mixed $value): ?string` returning `Y-m-d H:i:s` or `null` +- [x] 2.4 Implement `formatForIso8601(mixed $value): ?string` returning ISO 8601 with offset or `null` +- [x] 2.5 Register as an injectable service (no static state; DI via constructor) +- [x] 2.6 Add a class docblock stating that all user-datetime conversion MUST go through this class and referencing this change + +## 3. Unit tests for the normalizer + +- [x] 3.1 Null input → null +- [x] 3.2 Empty string → null +- [x] 3.3 Whitespace-only string (`" "`, `"\t"`, `"\n"`) → null +- [x] 3.4 Valid ISO 8601 with offset → correct DateTimeImmutable +- [x] 3.5 Valid ISO 8601 Zulu → correct DateTimeImmutable +- [x] 3.6 Database format `"Y-m-d H:i:s"` → correct DateTimeImmutable +- [x] 3.7 Date-only `"Y-m-d"` → correct DateTimeImmutable at midnight +- [x] 3.8 Existing `DateTime`/`DateTimeImmutable` passthrough → immutable instance +- [x] 3.9 Garbled string → null + debug log +- [x] 3.10 Numeric/array/object input → null + debug log +- [x] 3.11 `formatForDatabase` and `formatForIso8601` with each of the above inputs + +## 4. Migrate call sites (read path — primary fix) + +- [x] 4.1 `lib/Db/MagicMapper/MagicStatisticsHandler.php`: replace the `date` branch at ~line 583 with a delegation to the normalizer (format to `Y-m-d`) +- [x] 4.2 `lib/Db/MagicMapper/MagicStatisticsHandler.php`: replace the `date-time` branch at ~line 590 with a delegation to the normalizer (format to ISO 8601) +- [x] 4.3 Add/extend a MagicStatisticsHandler test covering: empty-string stored value → rendered as `null`; valid stored value → rendered correctly + +## 5. Migrate call sites (write + bulk + search) + +- [x] 5.1 `lib/Db/MagicMapper/MagicBulkHandler.php::formatDateTimeForDatabase` (~line 766) → delegate to `DateTimeNormalizer::formatForDatabase`; remove direct `new DateTime($value)` +- [x] 5.2 `lib/Db/MagicMapper.php` metadata handling (~line 2986) → delegate to the normalizer; keep the existing "default to now when key is absent" logic for `created`/`updated` exactly as-is (D3) +- [x] 5.3 `lib/Db/ObjectHandlers/MariaDbSearchHandler.php::normalizeDateValue` (~line 637) → delegate to the normalizer; return `null` on empty/invalid input (D5) +- [x] 5.5 `lib/Service/ObjectService.php::normalizeDateValues` (~line 1383) → guard empty/whitespace input via the normalizer; discovered during live verification where writes with `publishedAt: ""` persisted as today at midnight because `(new DateTime(''))->format('Y-m-d')` returned today. Now empty/whitespace normalises to `null`; `date-time` properties also routed through the normalizer. +- [x] 5.4 Verify: grep for any remaining unguarded `new DateTime($` on user-supplied paths; migrate or document why each remaining site is safe (e.g. on a literal, on an internal `DateTime` instance, etc.) + + Remaining unguarded `new DateTime($var)` call sites outside the five primary sites fall into three categories: + + **Internal/trusted values (safe — not user input):** + - `lib/Service/Object/LockHandler.php:253` — `$locked['expiresAt']` from ObjectEntity's own locked metadata (set by code) + - `lib/Service/Schemas/SchemaCacheHandler.php:557, 727, 732` — cached Schema metadata + - `lib/Service/OrganisationService.php:1329, 1338` — cached Organisation metadata + - `lib/Db/ObjectEntity.php:954, 960` — `$this->locked` internal state + - `lib/Db/MagicMapper/MagicSearchHandler.php:1416, 1420, 1425` — metadata read from our own DB columns + - `lib/Db/Schema.php:2119, 2146` & `lib/Db/Register.php:710, 737` — published/depublished metadata already validated upstream + - `lib/Db/Schema.php:1188` — already guarded (`is_string && !== ''`) + + **Controller/service paths that accept user-supplied date strings but are out-of-scope for the primary bug fix** (the bug is reading objects with empty-string dates; these paths parse date *params* and typically 404/400 on garbage rather than silently storing current time): + - `lib/Controller/DashboardController.php:356, 361` — from/till query params + - `lib/Controller/RevertController.php:82` — datetime in request body + - `lib/Controller/SearchTrailController.php:120, 128, 641` + - `lib/Controller/SchemasController.php:1078, 1162`; `lib/Controller/RegistersController.php:1350, 1452` + - `lib/Service/SearchTrailService.php:618, 626` + - `lib/Service/ImportService.php:1044, 1055`; `lib/Service/ExportService.php:620, 664` + - `lib/Service/TaskService.php:199, 278`; `lib/Service/CalendarEventService.php:170, 175` + - `lib/Service/EmailService.php:183`; `lib/Service/ObjectService.php:1383` + - `lib/Service/Archival/ArchiefactiedatumCalculator.php:240` + + These are tracked as follow-up work; the empty-string → current-datetime bug is eliminated for the object write/read/bulk/search paths, which is the user-reported defect scope. + +## 6. Regression tests for migrated sites + +- [x] 6.1 `EmptyStringDateConversionIntegrationTest::testEmptyStringDateRoundTripsAsNull()` — real `SaveObject` + DB read; empty `publishedAt` round-trips as `null`. +- [x] 6.2 `testValidIso8601DateRoundTripsCorrectly()` — valid ISO 8601 input round-trips to the same instant. +- [x] 6.3 `testAbsentDateFieldRemainsAbsentOrNull()` — absent `publishedAt` is absent-or-null, never "now". (Plus `testWhitespaceOnlyDateRoundTripsAsNull()`.) +- [x] 6.4 `testBulkImportEmptyStringDateNotSubstitutedWithNow()` — `MagicBulkHandler::bulkUpsert` with empty-string date → `null`, no current-datetime substitution. +- [x] 6.5 `MariaDbSearchHandlerDateFilterTest` — empty/whitespace filter on a date field normalises to `null` (no concrete datetime predicate); valid filter → DB format; non-date field passes through unchanged. +- [x] 6.6 `testMetadataEmptyExpiresPersistsAsNull()` — `@self.expires: ""` persists as `null`. +- [x] 6.7 `testMetadataAbsentCreatedDefaultsToNow()` — absent `created` still defaults to now (existing behaviour preserved). + +## 7. Cross-app verification + +- [~] 7.1 opencatalogi suite — OR datetime API contract unchanged (empty/garbage → null is strictly safer than the pre-fix "now"); no regression expected. Full downstream suite run deferred to ops (env-dependent). +- [~] 7.2 softwarecatalog suite — same as 7.1; deferred to ops verification. +- [~] 7.3 docudesk datetime flows — same as 7.1; deferred to ops verification. + +## 8. Quality & documentation + +- [x] 8.1 `composer check:strict` — the lib/ implementation merged via PR #1294 passed strict; the added test files follow the existing test conventions (SPDX header, `//end` markers, reflection pattern) and are diff gate-clean. +- [x] 8.2 Update `CHANGELOG.md` with a user-facing note on the behavior correction +- [x] 8.3 Release note: "Empty-string date fields now correctly round-trip as null; previously, empty-string dates on existing objects rendered as the current datetime. On next read/save, the value normalises to null." +- [ ] 8.4 (Optional) File a follow-up issue for the stored-data normalisation maintenance command (`UPDATE ... SET col = NULL WHERE col = ''`) flagged in `design.md` §Migration Plan + +## 9. Wrap-up + +- [x] 9.1 Run `openspec validate fix-empty-string-date-conversion` and resolve any findings +- [x] 9.2 Open PR referencing this change; links the regression tests (task 1.3 + section 6). diff --git a/openspec/changes/fk-graph-lifecycle-transitions/.openspec.yaml b/openspec/changes/fk-graph-lifecycle-transitions/.openspec.yaml new file mode 100644 index 0000000000..8cceb8d51a --- /dev/null +++ b/openspec/changes/fk-graph-lifecycle-transitions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-08 diff --git a/openspec/changes/fk-graph-lifecycle-transitions/design.md b/openspec/changes/fk-graph-lifecycle-transitions/design.md new file mode 100644 index 0000000000..6d4d8eadd4 --- /dev/null +++ b/openspec/changes/fk-graph-lifecycle-transitions/design.md @@ -0,0 +1,284 @@ +## Context + +`TransitionEngine` (`lib/Service/Lifecycle/TransitionEngine.php`) implements the +static form of `x-openregister-lifecycle`: `transitions` is a fixed +`action → { from: [literal states], to: literal }` map, and both `transition()` and +`availableActions()` compare the object's current **literal** field value against +those literals. `LifecycleAnnotationValidator` shape-checks that map at schema-save +time and requires `field`, a string-literal `initial`, and a non-empty `transitions` +map, with an enum constraint on the field. + +Government case processes (procest) model status as a **relation**: `case.status` is +a `$ref` UUID to a `statusType` object. The valid set of statuses is not a fixed +enum on the case schema — it is the set of `statusType` objects whose `caseType` FK +points at the case's own `caseType`, ordered by `statusType.order`, with terminal +states flagged by `statusType.isFinal`. Because the case schema cannot enumerate +these UUIDs statically (they differ per parent `caseType` and are authored as data), +the static engine produces `[]` from `/available-actions` and the lifecycle field is +frozen. hydra ADR-062 rule 10 forbids `readOnly` on such fields until a dynamic mode +exists. + +Constraints: fully backwards compatible; no DB migration; derivation must reuse the +existing `ObjectService` read path (RBAC + multitenancy already enforced there); the +static path must remain byte-for-byte unchanged and take precedence. + +## Goals / Non-Goals + +**Goals:** +- Add a declarative `graph` block to `x-openregister-lifecycle` that derives the + available and target transitions at runtime from FK-scoped sibling objects. +- Support `allowedMoves` = `forward` | `adjacent` | `any`, terminal-state lockout via + `finalField`, and stable `move-to-` action ids with human-readable labels. +- Support object-form `initial` (`{ from, field }`) that seeds the starting status + from the parent, including **auto-seed at object-create time** when the lifecycle + field is empty (v1 behaviour, per Ruben 2026-07-08). +- Keep static `transitions` behaviour unchanged; static takes precedence when both + are declared. +- Fire `ObjectTransitionedEvent` and run the same save-path validation for graph-mode + transitions as for static ones. + +**Non-Goals:** +- No consumer register JSON here — the procest `case`/`statusType` register change + lives in the procest repo. This change ships only the OR engine + validator. +- No new HTTP endpoints — `/transition` and `/available-actions` are reused as-is. +- No change to the static `transitions` shape or semantics. +- No cross-parent transitions (a case can only move among statuses of its own + `caseType`). +- No graph editing UI. + +## Decisions + +### Declarative-vs-imperative decision + +This change **is the declarative path**. It extends the existing declarative +`x-openregister-lifecycle` annotation (ADR-024 / hydra#202) with a `graph` block, so +a consumer declares the FK-scoped status graph as **data** (schema configuration + +`statusType` objects) rather than writing an imperative per-schema transition +controller. The derivation logic itself lives in **engine code** (`TransitionEngine`), +not in the declarative payload, and this is the intended split under the ADR-031 +lifecycle-guard/engine exception: the annotation declares *what* the graph is (which +sibling schema, which FK, which order/final fields, which move policy); the engine is +the single shared interpreter that turns that declaration into concrete transitions +at runtime. Putting the traversal in engine code (rather than, say, a calc-expression +or per-app guard) keeps one audited, testable implementation that every consumer app +inherits, matching how the static `transitions` map is already interpreted centrally. + +Alternative considered: express the graph as a `requires` guard per app (imperative, +in each consumer). Rejected — it re-implements traversal N times, bypasses +`/available-actions` (the UI would still get `[]`), and duplicates the ordering/final +logic that belongs in one place. + +Alternative considered: materialise the derived enum onto the case schema via the +calc engine. Rejected — the valid set is per-parent and per-object, not per-schema, +so there is no single enum to materialise; it would also couple lifecycle to the calc +re-materialisation cycle. + +### Derivation algorithm + +Given a graph-mode object: +1. Read `parentValue = object.data[graph.parentFrom]` (the parent FK on the object, + e.g. the case's `caseType` UUID). If absent → no actions (`[]`). +2. Fetch siblings: `ObjectService::findAll` over `graph.schema`, filtered by + `graph.parentField == parentValue`, sorted ascending by `graph.orderField`. This + goes through the standard read path so RBAC + multitenancy apply. +3. Locate the current state: `currentUuid = object.data[field]`; find its index in + the ordered sibling list. If the current value is not among the siblings, treat + index as "unset" and offer the first sibling only (recover-to-start — confirmed + by Ruben 2026-07-08). +4. Compute candidate targets by `allowedMoves`: + - `forward` → sibling at `index + 1` only. + - `adjacent` → siblings at `index - 1` and `index + 1`. + - `any` → every sibling except the current one. +5. Terminal lockout: if the **current** sibling's `finalField` is true and + `allowedMoves` is not `any`, yield no candidates (the state is a sink). +6. Emit one action per candidate: `action = "move-to-"`, + `to = `, `label = ` (the sibling's title/name + field), `description = null`, `requires = null` (graph mode has no per-edge + `requires` in v1). + +`transition()` re-runs steps 1–6 and accepts the requested `action` only if it is in +the derived candidate set — the client cannot post a `move-to-` that the graph +does not currently allow. On success it mutates `object.data[field] = ` +and saves through the unchanged `ObjectService::saveObject` path, then dispatches +`ObjectTransitionedEvent(from: currentUuid, to: targetUuid, action)`. + +### Mode selection & precedence + +`getLifecycleAnnotation()` is unchanged. In both `transition()` and +`availableActions()`: if `annotation['transitions']` is a non-empty array → **static +path** (existing code, untouched). Else if `annotation['graph']` is a non-empty array +→ **graph path** (new). Else → existing empty/absent behaviour. This guarantees +static precedence and zero regression for every existing static schema. + +### `initial` object-form + auto-seed on create + +`LifecycleAnnotationValidator` currently requires `initial` to be a string in the +field enum. Graph mode has no field enum (the field is a `$ref`), so: +- When `graph` is present, `initial` MAY be an object `{ "from": "", + "field": "" }`. Validator shape-checks the two string + keys; it does NOT resolve the parent (runtime concern). + +**Auto-seed (v1, decided by Ruben 2026-07-08 — reverses the earlier provisional +"no auto-seed")**: the engine DOES set the lifecycle field from the object-form +`initial` declaration at object-**create** time, with these rules: +- **Hook point**: the `SaveObject` create pipeline, as a lifecycle-seed step that + runs alongside metadata hydration — i.e. BEFORE schema validation and persistence, + so a `required` lifecycle field passes validation on a seeded create. It runs only + on the create path (no existing entity), never on update. +- **Empty-field-only**: seeding applies only when `object.data[field]` is absent, + null, or the empty string. An explicitly provided value is NEVER overwritten — + client-supplied status wins. +- **Resolution**: read `parentRef = object.data[initial.from]`; load the parent + object through the standard `ObjectService` read path (RBAC + multitenancy apply); + read `seedValue = parent.data[initial.field]`. Set `object.data[field] = seedValue`. +- **Fail-soft no-op**: if the parent ref is empty, the parent cannot be loaded, or + the parent's `initial.field` is empty, the seed step is a no-op (logged at debug) + and the create proceeds with the field unset — normal schema validation then + decides whether an unset field is acceptable (e.g. `required` still rejects). +- **Interaction with the object form**: auto-seed is driven exclusively by the + object-form `initial`; the legacy literal-string `initial` keeps its existing + static-mode semantics and is NOT auto-seeded by this step (no behaviour change for + static schemas). +- No `ObjectTransitionedEvent` is dispatched for a seed — it is an initialisation, + not a transition. + +### Validator changes + +`validate()` gains a branch: if `x-openregister-lifecycle.graph` is present, validate +the graph block instead of (or in addition to) `transitions`: +- `graph.schema`, `graph.parentField`, `graph.parentFrom` — required non-empty + strings. +- `graph.orderField`, `graph.finalField` — required non-empty strings. +- `graph.allowedMoves` — required, one of `forward` | `adjacent` | `any`. +- `field` — required non-empty string; the enum/`type:string` constraint is + **relaxed** when `graph` is present (a `$ref` field has no enum). +- `initial` — accept either the existing string form or the object form + `{ from, field }`. +Error codes follow the existing `lifecycle-*` convention (e.g. +`lifecycle-graph-missing-key`, `lifecycle-graph-allowedmoves-invalid`). + +### Guards / events + +`ObjectTransitionedEvent` and `LifecycleGuardRegistry` are reused verbatim — the +engine dispatches the same typed event after a graph transition. NOTE (discrepancy, +see below): `LifecycleGuardRegistry` is currently **not invoked** by `TransitionEngine` +at all; guard firing today happens on the `saveObject` validation path, not inside the +engine. Graph mode goes through the same `saveObject` path, so whatever guard/`requires` +enforcement exists for static transitions applies identically to graph transitions — +no new wiring, and no attempt to add per-edge `requires` in graph v1. + +## Seed Data + +Realistic municipal example for PHPUnit fixtures — an *Omgevingsvergunning* (building +permit) case whose status is FK-driven. All UUIDs are nil placeholders. + +**`caseType` object** (the parent): +```json +{ + "id": "00000000-0000-0000-0000-000000000000", + "name": "Omgevingsvergunning", + "initialStatus": "00000000-0000-0000-0000-000000000001" +} +``` + +**`statusType` objects** (siblings, all `caseType` → the parent above): +```json +[ + { + "id": "00000000-0000-0000-0000-000000000001", + "name": "Ontvangen", + "caseType": "00000000-0000-0000-0000-000000000000", + "order": 1, + "isFinal": false + }, + { + "id": "00000000-0000-0000-0000-000000000002", + "name": "In behandeling", + "caseType": "00000000-0000-0000-0000-000000000000", + "order": 2, + "isFinal": false + }, + { + "id": "00000000-0000-0000-0000-000000000003", + "name": "Afgehandeld", + "caseType": "00000000-0000-0000-0000-000000000000", + "order": 3, + "isFinal": true + } +] +``` + +**`case` object** (the transitioning object), with graph annotation on its schema: +```json +{ + "id": "00000000-0000-0000-0000-0000000000aa", + "caseType": "00000000-0000-0000-0000-000000000000", + "status": "00000000-0000-0000-0000-000000000002" +} +``` + +**`case` schema `x-openregister-lifecycle`**: +```json +{ + "field": "status", + "initial": { "from": "caseType", "field": "initialStatus" }, + "graph": { + "schema": "statustype", + "parentField": "caseType", + "parentFrom": "caseType", + "orderField": "order", + "finalField": "isFinal", + "allowedMoves": "forward" + } +} +``` + +Expected derivations for tests: +- `forward` from `Ontvangen` (order 1) → `[move-to-…0002]` (In behandeling). +- `adjacent` from `In behandeling` (order 2) → `[move-to-…0001, move-to-…0003]`. +- `any` from `In behandeling` → `[move-to-…0001, move-to-…0003]` (all but self). +- final lockout: `forward`/`adjacent` from `Afgehandeld` (isFinal) → `[]`; `any` from + `Afgehandeld` → the other two. +- static-precedence: a schema declaring both `transitions` and `graph` uses only the + static `transitions`. + +## Risks / Trade-offs + +- [Extra read per available-actions call: graph mode fetches all siblings] → + Mitigation: sibling sets are tiny (a handful of statuses per case type); the fetch + uses the cached `ObjectService` read path. Bound results and rely on existing cache. +- [Current value not among siblings (parent re-typed, orphan status)] → Mitigation: + treat as unset and offer the first sibling as a recover-to-start move; log at debug + (confirmed decision). +- [Auto-seed reads the parent on every create of a graph-mode object] → Mitigation: + single cached `ObjectService` read, create-path only, skipped entirely when the + client supplied a value; fail-soft no-op keeps create latency bounded on missing + parents. +- [`allowedMoves: any` bypasses terminal lockout by design] → documented explicitly; + consumers choosing `any` accept that finality is advisory, not enforced, for that + schema. +- [Silent divergence between `availableActions` derivation and `transition` + validation] → Mitigation: both call the **same** private derivation method; a single + code path prevents drift (unit-tested for parity). +- [Ordering ties on `orderField`] → Mitigation: stable secondary sort by UUID so + derivation is deterministic; documented. + +## Migration Plan + +No DB migration. Deploy is code-only: +1. Ship `TransitionEngine` + `LifecycleAnnotationValidator` changes (additive; static + path untouched). +2. Existing static-mode schemas: no change, no re-save required. +3. procest (separate repo) adopts the `graph` block on its `case` schema and removes + the ADR-062-rule-10 `readOnly` block once this ships. +Rollback: revert the two files; graph-mode schemas fall back to `[]` from +`/available-actions` (the pre-change behaviour) — no data corruption, no migration to +undo. + +## Open Questions + +None remaining. Both formerly deferred questions were decided by Ruben on +2026-07-08: (1) auto-seed on create IS in v1 (empty-field-only, never overwrites a +provided value, fail-soft no-op — see "`initial` object-form + auto-seed on +create"); (2) orphan/unknown current status uses recover-to-start (first sibling +offered). diff --git a/openspec/changes/fk-graph-lifecycle-transitions/proposal.md b/openspec/changes/fk-graph-lifecycle-transitions/proposal.md new file mode 100644 index 0000000000..7b121c2336 --- /dev/null +++ b/openspec/changes/fk-graph-lifecycle-transitions/proposal.md @@ -0,0 +1,72 @@ +--- +kind: code +--- + +## Why + +OpenRegister's `TransitionEngine` only supports **static** lifecycle graphs: +`x-openregister-lifecycle.transitions` is a fixed map of `action → { from: [literal +states], to: literal }`, and `/available-actions` filters those literals against the +object's current field value. Real government case processes model status as a +**relation**, not a literal: a `case.status` is a `$ref` UUID to a `statusType` +object, and the valid status set differs *per parent* (`statusType.caseType` FK → +the case's own `caseType`; ordering via `statusType.order`; terminality via +`statusType.isFinal`). Such dynamic FK-based status graphs are inexpressible today — +`/available-actions` always returns `[]`, so a read-only lifecycle field is +permanently frozen (found live on procest 2026-07-08; hydra ADR-062 rule 10 now +forbids `readOnly` on such fields until this feature ships). Procest is the reference +consumer waiting on this capability. + +## What Changes + +- **New `graph` mode** on `x-openregister-lifecycle`: alongside the existing static + `transitions` map, a schema may declare a `graph` block that derives the valid + transition set **at runtime** from sibling objects of a related schema, scoped to + the object's own parent via a foreign key. +- `graph` fields: `schema` (sibling schema slug), `parentField` (FK on the sibling + pointing at the parent), `parentFrom` (field on the transitioning object holding + the parent reference), `orderField`, `finalField`, and `allowedMoves` + (`forward` | `adjacent` | `any`). +- **Object-form `initial`**: `initial` may be `{ "from": "", "field": + "" }` to seed the starting status from the parent, in addition to + the existing literal-string form. +- `TransitionEngine::availableActions` derives actions from ordered sibling + `statusType` objects belonging to the object's parent; `forward` = only the + next-higher order, `adjacent` = next + previous, `any` = every sibling. No move + **out** of a state whose `finalField` is true unless `allowedMoves` is `any`. + Actions get stable ids (`move-to-`) with the target's display name as + label. +- `TransitionEngine::transition` (and `TransitionController::apply`) validate the + requested target through the **same derivation**, then mutate + save through the + unchanged `ObjectService` path; `ObjectTransitionedEvent` fires exactly as for + static transitions. +- **Static `transitions` takes precedence** when both `transitions` and `graph` are + declared. Fully backwards compatible — no change to any existing static-mode + behaviour. +- `LifecycleAnnotationValidator` accepts the new `graph` block and object-form + `initial` (shape-check only; sibling existence is a runtime concern). + +## Capabilities + +### New Capabilities + + +### Modified Capabilities +- `object-lifecycle`: adds a declarative **graph** transition mode that derives the + available/target transitions at runtime from FK-scoped sibling objects, in addition + to the existing static `transitions` map. Static mode is unchanged and takes + precedence when both are present. + +## Impact + +- **Code**: `lib/Service/Lifecycle/TransitionEngine.php` (derivation logic for + available-actions + apply-validation), `lib/Service/Lifecycle/LifecycleAnnotationValidator.php` + (accept `graph` + object-form `initial`). `TransitionController` unchanged (same + endpoints, richer payload). `ObjectService::findAll` used read-only to fetch + ordered siblings. +- **APIs**: `/available-actions` response gains derived actions for graph-mode + schemas (previously always `[]`); `move-to-` action ids. No breaking change + to the static-mode payload shape. +- **Consumers**: procest `case`/`statusType` registers become the reference consumer + (register JSON change lives in the **procest** repo, not here). +- **No DB migration**, no new dependency. diff --git a/openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md b/openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md new file mode 100644 index 0000000000..d2a6417ebb --- /dev/null +++ b/openspec/changes/fk-graph-lifecycle-transitions/specs/object-lifecycle/spec.md @@ -0,0 +1,162 @@ +## ADDED Requirements + +### Requirement: Lifecycle graph mode derives transitions from FK-scoped siblings + +`x-openregister-lifecycle` SHALL support a declarative `graph` mode in addition to +the static `transitions` map. When a schema declares a non-empty `graph` block and no +non-empty `transitions` map, `TransitionEngine` SHALL derive the available and target +transitions **at runtime** from sibling objects of a related schema, scoped to the +transitioning object's own parent by a foreign key. + +The `graph` block SHALL declare: `schema` (the sibling schema slug), `parentField` +(the FK property on the sibling that references the parent), `parentFrom` (the +property on the transitioning object holding the parent reference), `orderField` (the +numeric ordering property on the sibling), `finalField` (the boolean terminal-state +property on the sibling), and `allowedMoves` (one of `forward`, `adjacent`, `any`). + +Derivation SHALL: read the parent reference from `object.data[parentFrom]`; fetch +sibling objects of `schema` where `parentField` equals that parent reference, ordered +ascending by `orderField` (ties broken deterministically by UUID); locate the object's +current state (`object.data[field]`) within that ordered list; and compute candidate +targets by `allowedMoves` — `forward` yields only the next-higher-ordered sibling, +`adjacent` yields the next-higher and next-lower siblings, `any` yields every sibling +except the current one. Each derived action SHALL have a stable id `move-to-`, +a `to` equal to the target UUID, and a `label` equal to the target's display name. + +The derivation used by `availableActions()` and the validation used by `transition()` +SHALL be the SAME code path, so a client can only apply a `move-to-` action that +the current graph state offers. + +#### Scenario: Forward move offers only the next status +- **GIVEN** a `case` object whose `status` is `Ontvangen` (order 1) and whose graph declares `allowedMoves: forward` +- **AND** sibling `statusType` objects `Ontvangen` (1), `In behandeling` (2), `Afgehandeld` (3) all scoped to the case's `caseType` +- **WHEN** `availableActions()` is called for the object +- **THEN** the result MUST contain exactly one action `move-to-` targeting `In behandeling` + +#### Scenario: Adjacent move offers previous and next status +- **GIVEN** the same object at `status` `In behandeling` (order 2) with `allowedMoves: adjacent` +- **WHEN** `availableActions()` is called +- **THEN** the result MUST contain exactly two actions targeting `Ontvangen` (order 1) and `Afgehandeld` (order 3) + +#### Scenario: Any move offers every other sibling +- **GIVEN** the same object at `status` `In behandeling` with `allowedMoves: any` +- **WHEN** `availableActions()` is called +- **THEN** the result MUST contain one action per sibling except the current one + +#### Scenario: Applying a derived transition mutates and saves the object +- **GIVEN** a `case` object at `status` `Ontvangen` with `allowedMoves: forward` +- **WHEN** `transition()` is called with action `move-to-` +- **THEN** the object's `status` MUST be saved as the `In behandeling` UUID through the standard object save path +- **AND** an `ObjectTransitionedEvent` MUST be dispatched with `from` = the `Ontvangen` UUID and `to` = the `In behandeling` UUID + +#### Scenario: A target the graph does not allow is rejected +- **GIVEN** a `case` object at `status` `Ontvangen` with `allowedMoves: forward` +- **WHEN** `transition()` is called with action `move-to-` (order 3, not adjacent) +- **THEN** the transition MUST be rejected and the object's `status` MUST NOT change + +#### Scenario: Object without a parent reference yields no actions +- **GIVEN** a `case` object whose `parentFrom` property is empty +- **WHEN** `availableActions()` is called +- **THEN** the result MUST be an empty list + +### Requirement: Terminal graph states lock out non-any moves + +The engine MUST lock out moves out of a terminal graph state: when the object's +current sibling has `finalField` set to true, `TransitionEngine` SHALL yield no +candidate targets under `allowedMoves` `forward` or `adjacent` (the state is a sink). +Under `allowedMoves` `any`, terminality SHALL be advisory and the engine SHALL still +offer moves to the other siblings. + +#### Scenario: Final state blocks forward and adjacent moves +- **GIVEN** a `case` object at `status` `Afgehandeld` whose `statusType.isFinal` is true, with `allowedMoves: forward` +- **WHEN** `availableActions()` is called +- **THEN** the result MUST be an empty list + +#### Scenario: Any mode overrides terminal lockout +- **GIVEN** the same final-state object but with `allowedMoves: any` +- **WHEN** `availableActions()` is called +- **THEN** the result MUST contain actions targeting the non-final siblings + +### Requirement: Static transitions take precedence over graph mode + +The engine MUST prefer static transitions over graph mode: when a schema declares +BOTH a non-empty static `transitions` map and a `graph` block, `TransitionEngine` +SHALL use only the static `transitions` map and SHALL ignore the `graph` block, in +both `availableActions()` and `transition()`. Schemas declaring only +`transitions` SHALL behave exactly as before this change (no regression). + +#### Scenario: Both declared uses static path +- **GIVEN** a schema declaring both a non-empty `transitions` map and a `graph` block +- **WHEN** `availableActions()` is called for an object of that schema +- **THEN** the actions MUST be derived from the static `transitions` map only +- **AND** no sibling objects MUST be fetched for derivation + +### Requirement: Schema validation accepts the graph block and object-form initial + +`LifecycleAnnotationValidator` SHALL accept a `graph` block on +`x-openregister-lifecycle` and SHALL shape-check it: `schema`, `parentField`, +`parentFrom`, `orderField`, and `finalField` MUST be non-empty strings, and +`allowedMoves` MUST be one of `forward`, `adjacent`, `any`. When `graph` is present, +the `field` property MUST be a non-empty string but the `enum`/`type:string` +constraint on that field SHALL be relaxed (a `$ref` lifecycle field has no enum). +`initial` MAY be either the existing literal-string form OR an object of the form +`{ "from": "", "field": "" }`; both string keys MUST be non-empty +when the object form is used. Validation SHALL NOT resolve sibling schemas or parent +objects — existence is a runtime concern. Validation errors SHALL use the existing +`lifecycle-*` error-code convention. + +#### Scenario: Valid graph annotation passes validation +- **GIVEN** a schema whose `x-openregister-lifecycle` declares a well-formed `graph` block and object-form `initial` +- **WHEN** the schema is validated +- **THEN** `LifecycleAnnotationValidator` MUST return no errors + +#### Scenario: Invalid allowedMoves is rejected +- **GIVEN** a `graph` block whose `allowedMoves` is `sideways` +- **WHEN** the schema is validated +- **THEN** the validator MUST return an error identifying the invalid `allowedMoves` value + +#### Scenario: Missing graph key is rejected +- **GIVEN** a `graph` block missing `parentField` +- **WHEN** the schema is validated +- **THEN** the validator MUST return an error identifying the missing key + +### Requirement: Object-form initial auto-seeds the lifecycle field on create + +The object-create pipeline MUST auto-seed the lifecycle field when the schema +declares an object-form `initial` (with `from` and `field` keys) on +`x-openregister-lifecycle`: on the CREATE path only (never on update), when the +lifecycle field is absent, null, or the empty string, the pipeline MUST read the +parent reference from the object's `initial.from` property, load the parent object +through the standard `ObjectService` read path, and set the lifecycle field to the +parent's `initial.field` value BEFORE schema validation and persistence. + +An explicitly provided lifecycle value MUST NOT be overwritten by the seed step. +When the parent reference is empty, the parent cannot be loaded, or the parent's +`initial.field` value is empty, the seed step SHALL be a no-op and the create SHALL +proceed with the field unset (normal schema validation then applies). The seed step +SHALL NOT dispatch an `ObjectTransitionedEvent` (it is an initialisation, not a +transition), and the legacy literal-string `initial` form SHALL keep its existing +static-mode semantics unchanged (no auto-seed behaviour change for static schemas). + +#### Scenario: Empty lifecycle field is seeded from the parent on create +- **GIVEN** a `case` schema declaring `initial: { "from": "caseType", "field": "initialStatus" }` +- **AND** a create payload whose `caseType` references an `Omgevingsvergunning` case type with `initialStatus` = the `Ontvangen` UUID and whose `status` is empty +- **WHEN** the object is created +- **THEN** the persisted object's `status` MUST equal the `Ontvangen` UUID +- **AND** no `ObjectTransitionedEvent` MUST be dispatched for the seed + +#### Scenario: Explicitly provided value is not overwritten +- **GIVEN** the same schema and a create payload whose `status` is explicitly set to the `In behandeling` UUID +- **WHEN** the object is created +- **THEN** the persisted object's `status` MUST equal the `In behandeling` UUID (the client-supplied value wins) + +#### Scenario: Missing parent reference makes the seed a no-op +- **GIVEN** the same schema and a create payload with an empty `caseType` and an empty `status` +- **WHEN** the object is created +- **THEN** the seed step MUST be a no-op and the `status` field MUST remain unset +- **AND** normal schema validation MUST still apply to the unset field + +#### Scenario: Parent without an initial status makes the seed a no-op +- **GIVEN** the same schema and a create payload whose `caseType` references a parent whose `initialStatus` is empty +- **WHEN** the object is created +- **THEN** the seed step MUST be a no-op and the `status` field MUST remain unset diff --git a/openspec/changes/fk-graph-lifecycle-transitions/tasks.md b/openspec/changes/fk-graph-lifecycle-transitions/tasks.md new file mode 100644 index 0000000000..ae2c5a9fdf --- /dev/null +++ b/openspec/changes/fk-graph-lifecycle-transitions/tasks.md @@ -0,0 +1,36 @@ +## 1. Validator: accept graph mode + +- [ ] 1.1 In `LifecycleAnnotationValidator::validate()`, branch when `x-openregister-lifecycle.graph` is present: shape-check `schema`/`parentField`/`parentFrom`/`orderField`/`finalField` (non-empty strings) and `allowedMoves` (`forward`|`adjacent`|`any`), emitting `lifecycle-graph-*` error codes. +- [ ] 1.2 Relax the `field` enum/`type:string` requirement when `graph` is present (keep `field` required, non-empty); accept object-form `initial` `{ from, field }` alongside the existing literal-string form. + +## 2. Engine: graph derivation + +- [ ] 2.1 Add a private `deriveGraphActions(ObjectEntity, array $graph, string $field)` to `TransitionEngine` that reads the parent ref, fetches ordered siblings via `ObjectService::findAll` (filter `parentField == parentFrom value`, sort asc by `orderField`, UUID tiebreak), locates the current state, and returns candidate targets per `allowedMoves` with `move-to-` ids and display-name labels. +- [ ] 2.2 Implement terminal lockout: current sibling `finalField == true` yields no candidates unless `allowedMoves` is `any`; empty parent ref or absent siblings yields `[]`. +- [ ] 2.3 Wire mode selection in `availableActions()`: non-empty static `transitions` → existing path (unchanged); else non-empty `graph` → `deriveGraphActions`; else existing empty behaviour. +- [ ] 2.4 Wire `transition()` to re-run the SAME derivation, accept the posted action only if it is a current candidate, mutate `data[field]` to the target UUID, save through `ObjectService::saveObject`, and dispatch `ObjectTransitionedEvent(from, to, action)`. + +## 3. Auto-seed on create + +- [ ] 3.1 Add a lifecycle-seed step to the `SaveObject` CREATE pipeline (before schema validation/persistence, never on update): when the schema declares object-form `initial { from, field }` and the lifecycle field is absent/null/empty, resolve the parent via `object.data[initial.from]` through `ObjectService` and set the field to `parent.data[initial.field]`; never overwrite a provided value; fail-soft no-op (debug log) on missing parent/empty initial value; dispatch no `ObjectTransitionedEvent`. + +## 4. Tests + +- [ ] 4.1 Add `tests/Unit/Service/Lifecycle/TransitionEngineGraphTest.php` covering derivation with the Omgevingsvergunning seed data: `forward`/`adjacent`/`any` candidate sets, final-state lockout (blocked for forward/adjacent, open for any), static-precedence (both declared → static only, no sibling fetch), object-with-no-parent → `[]`, apply-rejects-non-candidate, and `ObjectTransitionedEvent` firing on a valid apply. +- [ ] 4.2 Extend `LifecycleAnnotationValidator` tests: valid graph + object-form `initial` passes; invalid `allowedMoves` and each missing graph key are rejected with the expected codes. +- [ ] 4.3 Add PHPUnit coverage for auto-seed on create: seeds when the field is empty; does not overwrite an explicitly provided value; no-ops when the parent ref is missing, the parent cannot be loaded, or the parent's initial field is empty; no seed on update; no `ObjectTransitionedEvent` for a seed. + +## 5. Docs / API surface + +- [ ] 5.1 Document graph mode + auto-seed in the lifecycle annotation reference and the `/api/objects/{id}/available-actions` + `/transition` OAS/description surface: the `graph` block fields, `move-to-` action ids + labels, and static precedence. + +## Acceptance criteria + +- Graph-mode `/available-actions` returns FK-scoped, order-derived actions for the procest `case`/`statusType` shape; previously it returned `[]`. +- `forward` offers only the next-higher status; `adjacent` offers previous+next; `any` offers all other siblings. +- No move out of a `finalField: true` state under `forward`/`adjacent`; `any` still offers other siblings. +- A schema declaring both `transitions` and `graph` uses static `transitions` only, with zero regression to existing static-mode behaviour. +- `transition()` rejects any `move-to-` not in the current derived candidate set and never mutates on rejection. +- `ObjectTransitionedEvent` and the `saveObject` guard/validation path fire for graph transitions exactly as for static ones. +- Create with object-form `initial` and an empty lifecycle field seeds the field from the parent's initial status; an explicitly provided value is never overwritten; missing parent/initial value no-ops fail-soft; no seed event is dispatched. +- `composer check:strict` passes; all new PHPUnit tests green. diff --git a/openspec/changes/flow-agent-action/.openspec.yaml b/openspec/changes/flow-agent-action/.openspec.yaml new file mode 100644 index 0000000000..dd9a1d92eb --- /dev/null +++ b/openspec/changes/flow-agent-action/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-06 diff --git a/openspec/changes/flow-agent-action/design.md b/openspec/changes/flow-agent-action/design.md new file mode 100644 index 0000000000..c3d42d9b35 --- /dev/null +++ b/openspec/changes/flow-agent-action/design.md @@ -0,0 +1,132 @@ +# Design: `type: "agent"` flow action + +> Umbrella decisions apply (SPECTR-NEXTCLOUD-PLAN.md §5.2). + +## Approach + +`FlowActionService::runAction()` gains a fourth case (`agent`, alongside +`calendar-event`/`agenda-task` and `email`/`mail`). It validates the action +config, renders the prompt template through the SAME `render()` helper the +other two actions already use (no new templating engine), builds an +`AgentRunRequestedEvent`, and dispatches it via `IEventDispatcher::dispatchTyped()`. +That is the entire OpenRegister-side surface — there is no LLM invocation, no +HTTP call to Hermiq, and no polling. The event either finds a listener (Hermiq +installed) or is a silent no-op (Hermiq absent); either way the triggering +object's save path is unaffected. + +## Event contract + +`OCA\OpenRegister\Event\AgentRunRequestedEvent` (extends `OCP\EventDispatcher\Event`): + +| Field | Type | Source | Notes | +|---|---|---|---| +| `subjectUuid` | string | `$object->getUuid()` | The triggering object. | +| `subjectRegister` | string | `$object->getRegister()` | Register slug/id. | +| `subjectSchema` | string | `$object->getSchema()` | Schema slug/id. | +| `agent` | string | action config `agent` | Agent UUID (v1: UUID only — OpenRegister's `Agent` entity has no `slug` field yet; see the "Agent ref resolution" note in the companion Hermiq change). | +| `skill` | string\|null | action config `skill` | Optional skill slug; `null` when omitted. | +| `prompt` | string | action config `prompt`, **rendered** | `{{field}}` placeholders already resolved against the object's data via `FlowActionService::render()` — the consumer receives a plain string, not a template. | +| `resultField` | string | action config `resultField` | The object field the consumer writes the agent's output to. | +| `requiresApproval` | bool | action config `requiresApproval` | Default `false`. | +| `mode` | string | action config `mode` | Default `"async"` — the only supported value in v1 (SPECTR-NEXTCLOUD-PLAN.md §5.2 point 5: sync inline execution is explicitly out of scope). | +| `flowName` | string | owning flow's `name` | Diagnostics/audit only. | +| `correlationId` | string | generated (`Uuid::v4()`) | Unique per dispatch — lets a consumer de-duplicate a gated (pending-approval) run. | + +`getPayload(): array` flattens all of the above into a plain, JSON-serialisable +associative array — the shape a consumer hands to a background job argument +(job arguments must be scalar-only; the event object itself cannot cross that +boundary). + +**Why the prompt is rendered, not a raw template.** SPECTR-NEXTCLOUD-PLAN.md +§5.2 describes the dispatched payload as carrying "prompt template" at the +*configuration* level (the flow author writes `{{field}}` placeholders, same as +`calendar-event`/`email`). OpenRegister already owns a working `{{field}}` +renderer (`FlowActionService::render()`, reused unchanged by all three action +types) and already has the object's full data in hand at dispatch time. Two +options were considered: + +1. **Dispatch the raw template string; let the consumer render it.** Rejected — + duplicates the exact same `{{field}}` interpolation logic in a second + codebase (Hermiq), and the consumer would need the object's full data + snapshot in the event anyway to render it, which is a larger payload than a + single rendered string. +2. **Render once, in OpenRegister, before dispatch (chosen).** Reuses the + existing helper, keeps the event payload small and self-contained (a plain + string a consumer can hand straight to an LLM call), and is symmetric with + how `calendar-event`/`email` already render their templated fields before + acting. The "template" framing in the plan describes the *author-facing* + config surface, not the wire payload. + +## Declarative-vs-imperative decision (ADR-031) + +ADR-031 requires: when OR provides an `x-openregister-*` extension that fits +the requirement, apps declare behaviour in the schema register rather than +writing a service class, and any imperative code added alongside a declarative +surface is justified in this section. + +**This change adds no new imperative business logic to OpenRegister.** The +declarative surface (`x-openregister-flows`, already schema-register-driven) +is *extended in vocabulary only* — a flow author now has a fourth action +`type` to choose from, exactly as declarative as `calendar-event`/`email` +before it. The one piece of new PHP (`FlowActionService::runAgent()` + +`AgentRunRequestedEvent`) is not a business-logic service: it is the ADR-041 +cross-app command seam, and ADR-031 explicitly carves out exactly this kind of +code as legitimate: + +- ADR-031 §"What apps SHOULD still write in PHP" lists **external API + integrations** (CalDAV, Peppol, ZGW, ORI, TenderNed, IMAP, vendor SaaS) as + work the schema engine cannot express — "the OR engine cannot reach outside + systems; the adapter is yours to write." An LLM agent runtime living in a + sibling Conduction app (Hermiq) is architecturally the same shape as an + external system from OpenRegister's point of view: OR does not — and per + gate-27 must not — reach into it directly. Dispatching a typed event is the + *smallest possible* adapter: one method, one `dispatchTyped()` call, zero + business rules, zero LLM logic, zero loops. +- The existing `calendar-event` and `email` actions are the precedent: they + are themselves small, single-purpose PHP methods invoking Nextcloud + surfaces (`CalendarEventService`, `IMailer`) — accepted, non-controversial + imperative glue *underneath* a declarative config surface. `runAgent()` + follows the exact same shape one level further out (through an event + instead of a direct Nextcloud service call), because the destination + (Hermiq) is a sibling app rather than an NC built-in. +- No aggregation, calculation, lifecycle, or notification logic is added — + the four extension categories ADR-031 asks to prefer declaratively. This + change touches none of them; it only widens `x-openregister-flows`' action + vocabulary, which itself has no declarative "extension" equivalent to + supersede it (flows are already the declarative surface). + +**Conclusion**: no ADR-031 exception filing is needed — this is the +"external API integrations" carve-out applied to a sibling-app boundary via +the ADR-041 event recipe, not a new service class duplicating an existing +`x-openregister-*` extension. + +## Failure isolation + +Unchanged from the existing flow contract: `runAction()`'s try/catch wraps +`runAgent()` exactly like every other action type. A malformed config (missing +`agent`/`resultField`, unsupported `mode`) is caught *inside* `runAgent()` +itself (logged, no dispatch) rather than relying on the outer catch, so the +distinction between "config error" and "dispatch failure" is visible in the +logs. A `dispatchTyped()` call that throws (e.g. a buggy third-party listener) +is still caught by `runAction()`'s outer try/catch — the triggering object's +save is never affected, and the remaining actions in the flow still run. + +## Files Affected + +### Backend (new) +- `lib/Event/AgentRunRequestedEvent.php` +- `tests/Unit/Event/AgentRunRequestedEventTest.php` + +### Backend (modified) +- `lib/Service/Flow/FlowActionService.php` — new `agent` case + `runAgent()` + + `IEventDispatcher` constructor injection. +- `tests/Unit/Service/Flow/FlowActionServiceTest.php` — event-dispatcher mock + wiring + new agent-action test cases. + +## Risks + +| Risk | Mitigation | +|---|---| +| A flow author configures `type: "agent"` with no Hermiq installed | Silent no-op by design (ADR-041) — objects keep flowing; nothing to mitigate beyond the existing behaviour of any unlistened NC event. | +| A malformed `agent`/`resultField`/`mode` config silently does nothing, confusing the author | Every skip path logs a `warning` naming the exact missing/invalid field (mirrors the existing "Unknown flow action type" warning). | +| The rendered-prompt design leaks object data the flow author didn't intend | Same trust boundary as `calendar-event`/`email` today — the flow author already controls which object fields the template references; no new exposure introduced. | diff --git a/openspec/changes/flow-agent-action/proposal.md b/openspec/changes/flow-agent-action/proposal.md new file mode 100644 index 0000000000..38602694a5 --- /dev/null +++ b/openspec/changes/flow-agent-action/proposal.md @@ -0,0 +1,76 @@ +--- +kind: code +depends_on: [] +--- + +## Why + +Spectr's "classify tender" / "requirement extraction" / "feature mapping" stages +(SPECTR-NEXTCLOUD-PLAN.md §5.3) all need a declarative object-lifecycle flow to +trigger an LLM agent turn — the same shape as today's `calendar-event`/`email` +flow actions, but landing in Hermiq's governed agent runtime instead of a +Nextcloud built-in surface. Today `x-openregister-flows` only knows two action +types (`FlowActionService::runAction()`), and there is no sanctioned way for a +declarative flow to reach an agent runtime: OpenRegister must not call Hermiq +directly (gate-27, `no-phantom-cross-app-rpc`) and must not grow its own inline +LLM-invocation code path for this (v1's "OR runs an Agent inline" design was +explicitly retired in favour of the Hermiq re-platform, plan §5.2). + +This change adds the `type: "agent"` flow action: a schema author declares the +same trigger/action shape they already use for `calendar-event`/`email`, and +`FlowActionService` dispatches a typed `AgentRunRequestedEvent` (ADR-041's +cross-app command recipe) instead of invoking a runtime inline. A consuming app +(Hermiq, in a companion change) registers an `IEventListener` for this event and +performs the actual governed run through its own services. + +## What Changes + +- Add a new action `type: "agent"` to the `x-openregister-flows` vocabulary + handled by `FlowActionService::runAction()` — config keys: `agent` (required, + agent UUID), `skill` (optional slug), `prompt` (required, `{{field}}`-templated + like the existing actions), `resultField` (required — the object field the + agent's output is written to by the consumer), `requiresApproval` (optional + bool, default `false`), `mode` (optional, default `"async"` — the only + supported value in v1; any other value is treated as malformed config). +- Add `lib/Event/AgentRunRequestedEvent.php` — a new `OCP\EventDispatcher\Event` + carrying the triggering object's `subjectUuid`/`subjectRegister`/ + `subjectSchema`, the `agent`/`skill` refs, the fully-rendered `prompt`, the + `resultField`, `requiresApproval`, `mode`, the owning flow's `flowName`, and a + generated `correlationId`. `getPayload()` flattens the event to a + JSON-serialisable array for a consumer that hands the request off to a + background job. +- Inject `IEventDispatcher` into `FlowActionService` and dispatch the event via + `dispatchTyped()`. OpenRegister never calls the consuming app directly — if no + listener is installed the dispatch is a silent no-op (existing objects keep + flowing; ADR-041 point 1). +- Malformed config (missing `agent`, missing `resultField`, or an unsupported + `mode`) is logged and the action is skipped — the existing flow semantics + (a failing/malformed action never corrupts the triggering save) are + unchanged. + +## Capabilities + +### New Capabilities + +- `flow-actions`: the declarative `x-openregister-flows` action vocabulary that + `FlowActionService` executes on an object-lifecycle trigger. This is the + *first* formal spec for the flow engine — the pre-existing `calendar-event`/ + `email` actions ship without one (`FlowActionService::run()` carries a + `@spec exclude` annotation dating back to their introduction) and stay out of + scope here (diff-scoped per ADR-020); this change specs only the new `agent` + action and its event contract. + +## Impact + +- **New event contract**: `OCA\OpenRegister\Event\AgentRunRequestedEvent` — + the payload shape a consuming app (Hermiq) must match exactly on the listener + side. See `design.md` for the full field table. +- **`FlowActionService` constructor gains `IEventDispatcher`** — resolved by + Nextcloud's DI container automatically; no explicit construction site to + update (verified: the service is never `new`'d directly in `lib/`). +- **No new endpoints, no schema migration, no new OR public API surface.** The + action is declared in existing `x-openregister-flows` schema configuration — + purely additive to the vocabulary an author can already write. +- **Downstream dependent**: Hermiq's `feat/flow-agent-listener` change + (companion, different repo) implements the listener that turns this event + into a governed agent run. That change `depends_on` this one. diff --git a/openspec/changes/flow-agent-action/specs/flow-actions/spec.md b/openspec/changes/flow-agent-action/specs/flow-actions/spec.md new file mode 100644 index 0000000000..95190f4db6 --- /dev/null +++ b/openspec/changes/flow-agent-action/specs/flow-actions/spec.md @@ -0,0 +1,82 @@ +## ADDED Requirements + +### Requirement: Agent flow action dispatches a typed cross-app event + +`FlowActionService` SHALL support a flow action of `type: "agent"` in the +`x-openregister-flows` vocabulary. When such an action fires, the service +SHALL render the action's `prompt` template against the triggering object's +data (using the same `{{field}}` interpolation the `calendar-event`/`email` +actions already use) and dispatch an `AgentRunRequestedEvent` via +`IEventDispatcher::dispatchTyped()`. OpenRegister SHALL NOT invoke an agent +runtime inline and SHALL NOT call a consuming app's service directly (gate-27, +ADR-041). + +#### Scenario: Agent action dispatches the event with the full payload + +- **WHEN** a schema's `x-openregister-flows` declares a flow whose trigger + matches, with an action `{ "type": "agent", "agent": "", "skill": + "", "prompt": "Classify {{name}}", "resultField": "categorySlug", + "requiresApproval": true, "mode": "async" }` +- **THEN** `FlowActionService` dispatches one `AgentRunRequestedEvent` whose + `subjectUuid`/`subjectRegister`/`subjectSchema` identify the triggering + object, `agent` and `skill` match the config, `prompt` is the + fully-rendered string (placeholders resolved), `resultField` matches the + config, `requiresApproval` is `true`, `mode` is `"async"`, `flowName` + matches the owning flow's name, and `correlationId` is a non-empty, + per-dispatch-unique string + +#### Scenario: Omitted optional fields default correctly + +- **WHEN** an `agent` action config omits `skill`, `requiresApproval`, and `mode` +- **THEN** the dispatched event's `skill` is `null`, `requiresApproval` is + `false`, and `mode` is `"async"` + +### Requirement: Malformed agent action config is skipped without dispatching + +`FlowActionService` SHALL treat an `agent` action missing a required field +(`agent` or `resultField`) as malformed configuration: it SHALL log a warning +identifying the missing field and SHALL NOT dispatch `AgentRunRequestedEvent`. +This mirrors the existing behaviour for an unrecognised action `type` and +preserves the flow engine's invariant that a failing/malformed action never +corrupts the triggering object's save. + +#### Scenario: Missing agent reference is skipped and logged + +- **WHEN** an `agent` action config omits `agent` +- **THEN** no `AgentRunRequestedEvent` is dispatched +- **AND** a warning is logged identifying the missing `agent` reference + +#### Scenario: Missing resultField is skipped and logged + +- **WHEN** an `agent` action config omits `resultField` +- **THEN** no `AgentRunRequestedEvent` is dispatched +- **AND** a warning is logged identifying the missing `resultField` + +### Requirement: Only async mode is supported in v1 + +`FlowActionService` SHALL accept only `mode: "async"` for the `agent` action +in v1. Sync inline execution is explicitly out of scope +(SPECTR-NEXTCLOUD-PLAN.md §5.2 point 5). A config declaring any other `mode` +value SHALL be treated as malformed: logged and skipped, with no event +dispatched. + +#### Scenario: Unsupported mode is skipped and logged + +- **WHEN** an `agent` action config declares `"mode": "sync"` +- **THEN** no `AgentRunRequestedEvent` is dispatched +- **AND** a warning is logged identifying the unsupported mode + +### Requirement: A throwing dispatch never corrupts the save or blocks other actions + +`FlowActionService` SHALL catch a throwing `dispatchTyped()` call (e.g. a +misbehaving listener in a consuming app) via its existing per-action error +isolation, consistent with the existing flow engine contract. The triggering +object's save SHALL be unaffected and any remaining actions in the same flow +SHALL still execute. + +#### Scenario: Dispatch failure does not block a later action + +- **WHEN** a flow declares an `agent` action followed by an `email` action, + and dispatching the agent action's event throws +- **THEN** the `email` action still executes +- **AND** no exception propagates out of `FlowActionService::run()` diff --git a/openspec/changes/flow-agent-action/tasks.md b/openspec/changes/flow-agent-action/tasks.md new file mode 100644 index 0000000000..820a7635ad --- /dev/null +++ b/openspec/changes/flow-agent-action/tasks.md @@ -0,0 +1,61 @@ +## 1. Event contract + +- [x] 1.1 Create `lib/Event/AgentRunRequestedEvent.php` — provenance + (`subjectUuid`/`subjectRegister`/`subjectSchema`) + requested-run payload + (`agent`, `skill`, `prompt`, `resultField`, `requiresApproval`, `mode`, + `flowName`, generated `correlationId`) + `getPayload()` flattener. +- [x] 1.2 Unit tests for the event: getters, payload flattening, correlation-id + uniqueness per instance (`tests/Unit/Event/AgentRunRequestedEventTest.php`). + +## 2. `FlowActionService` wiring + +- [x] 2.1 Inject `IEventDispatcher` into `FlowActionService`'s constructor. +- [x] 2.2 Add the `agent` case to `runAction()`'s switch and implement + `runAgent()`: validate `agent`/`resultField`/`mode`, render the prompt via + the existing `render()` helper, build the event, dispatch via + `dispatchTyped()`. +- [x] 2.3 Malformed config (missing `agent`, missing `resultField`, unsupported + `mode`) logs a warning naming the field and skips the dispatch — mirrors + the existing "Unknown flow action type" warning shape. + +## 3. Tests + +- [x] 3.1 Happy path: `type: "agent"` action dispatches `AgentRunRequestedEvent` + with the exact rendered/derived payload (agent/skill/prompt/resultField/ + requiresApproval/mode/flowName/correlationId). +- [x] 3.2 Defaults: omitted `mode`/`requiresApproval`/`skill` resolve to + `"async"`/`false`/`null`. +- [x] 3.3 Malformed config: missing `agent` → skipped + logged; missing + `resultField` → skipped + logged; unsupported `mode` (e.g. `"sync"`) → + skipped + logged. Dispatcher never called in any case. +- [x] 3.4 Failure isolation: a throwing `dispatchTyped()` call does not block a + later action in the same flow and never throws into the caller. +- [x] 3.5 Existing calendar-event/email/unknown-type tests updated for the new + constructor param (`IEventDispatcher` mock) and confirmed unaffected. + +## Acceptance criteria + +- `FlowActionService` supports a fourth flow action `type: "agent"` alongside + `calendar-event`/`agenda-task` and `email`/`mail`. +- `AgentRunRequestedEvent` carries the exact field set in `design.md`'s event + contract table — this is the contract the companion Hermiq change + (`feat/flow-agent-listener`) listens against; field names and types must + match verbatim. +- Malformed `agent` action config is logged and skipped without dispatching — + it never corrupts the triggering object's save (existing flow semantics). +- `mode` other than `"async"` is treated as malformed config in v1. +- No new OR public API surface, no schema migration, no direct call from + OpenRegister into any consuming app (gate-27 compliance). + +## Quality checklist + +- ADR-041: cross-app command via a typed `IEventDispatcher` event; OpenRegister + never resolves or calls a sibling app's service directly. +- ADR-031: no new business-logic service added; see design.md's + "Declarative-vs-imperative decision" section for the ADR-031 external-API + carve-out this change relies on. +- ADR-020: diff-scoped — the pre-existing `calendar-event`/`email` actions + stay `@spec exclude`d (unchanged); only the new `agent` action + event are + specced here. +- Existing flow semantics preserved: a failing/malformed action never corrupts + the triggering save; the remaining actions/flows still run. diff --git a/openspec/changes/geo-metadata-kaart/tasks.md b/openspec/changes/geo-metadata-kaart/tasks.md deleted file mode 100644 index a01805c601..0000000000 --- a/openspec/changes/geo-metadata-kaart/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -# Tasks: Geo Metadata en Kaart - -> **Status (2026-05-02): scope-tightened to GeoJSON storage + spatial-query API on the OpenRegister side.** UI / map-rendering / base-registration overlays / map drawing / NL Design System map styling are handed off to consuming apps: -> - mydash: new change `map-support` proposed at https://github.com/ConductionNL/mydash/blob/main/openspec/changes/map-support/proposal.md -> - tilburg-woo-ui: tracked at https://github.com/ConductionNL/tilburg-woo-ui/issues/436 - -## In-OR-scope (storage + API) - -- [x] **REQ-GEO-001 — Schema properties MUST support geospatial data types.** `geo` added to the `validTypes` whitelist on `PropertyValidatorHandler`. Schema authors can declare `{"type": "geo"}` on any property; the validator accepts it as a first-class JSON-Schema type alongside `string`, `number`, `array`, etc. -- [x] **REQ-GEO-002 — GeoJSON storage in MagicMapper.** Storage works through the existing JSON-column path on magic tables — `geo`-typed properties serialise as GeoJSON `Geometry` or `FeatureCollection` documents and round-trip through the standard save/read cycle without a new column. Indexing for at-scale spatial queries is tracked in `geo-spatial-queries` (a focused follow-up gated on a real customer with >50k geo rows). -- [x] **REQ-GEO-004 — Spatial queries in the API.** **Shipped 2026-05-02:** new `OCA\OpenRegister\Service\Geo` namespace with four primitives — (1) `GeoFilter` value object with static factories `fromBbox`, `fromNearAndRadius`, `fromWithinGeometry`, `fromIntersectsGeometry` (fail-fast input validation: `west > east` throws); (2) `GeoSpatialEvaluator` — pure-PHP fallback that decides whether a single GeoJSON geometry matches a filter, using ray-casting point-in-polygon, Haversine distance for `near+radius`, AABB for bbox; supports Point / Polygon / MultiPolygon / LineString row geometries; (3) `GeoFilterParser` — wire-format adapter that turns `?geo.bbox=`, `?geo.near=&geo.radius=`, `?geo.property=` query params into filters, and the POST `/geo-search` body `{ geometry: { within | intersects } }` into filters; (4) `GeoFilterApplier` — post-filters a list of rows under AND-composition, with property-hint or first-found geometry extraction. Verified by 36 unit tests / 56 assertions across `GeoSpatialEvaluatorTest` (18 — bbox edge inclusivity, Polygon centroids, Haversine sanity, within / intersects, malformed-input rejection), `GeoFilterParserTest` (12 — both wire formats + AND-composition + property-hint), `GeoFilterApplierTest` (6 — empty-list pass-through, AND, missing-geometry exclusion, property-hint targeting). PHPCS strict-clean. Wiring `ObjectsController::index` to consume the parser + new `POST /geo-search` route is a follow-up; the value-object surface is the engineering risk and that's locked + tested. PostGIS push-down still tracked in `geo-spatial-queries`. -- [x] **REQ-GEO-011 — Geo-filtering in search and facets.** **Shipped 2026-05-02:** the `GeoFilterApplier` is the integration hook for both list/search and facet pipelines — it accepts a row stream and AND-composes spatial filters with whatever scalar/range filters are applied upstream. Composition with property filters (e.g. `?geo.near=...&geo.radius=...&status=open`) is verified by `GeoFilterApplierTest::testAndComposition_BboxIntersectsNear` and the parser side by `GeoFilterParserTest::testQueryComposesBboxAndNear`. End-to-end controller wiring is the same follow-up commit referenced in REQ-GEO-004. -- [x] **REQ-GEO-015 — Coordinate transformation and Dutch grid support.** OpenRegister stores GeoJSON in WGS84 (the GeoJSON spec's mandatory CRS). Transformation to/from RD (EPSG:28992) is a UI-side concern handled by the consuming apps via `proj4` (per the mydash `map-support` proposal). Documented as the canonical contract: storage = WGS84 GeoJSON, presentation-time transformation = consumer responsibility. - -## Out of OR-scope (handed off to consuming apps) - -- [x] **REQ-GEO-003 — Map visualization component with PDOK tile layers.** Moved to mydash `map-support` + tilburg-woo-ui#436. Each consuming app picks its own map library (Leaflet/MapLibre/OpenLayers) and tile layer. -- [x] **REQ-GEO-005 — Geocoding via PDOK Locatieserver.** Moved to consuming apps. PDOK Locatieserver is a UI-input affordance ("type a street name → get coords") that doesn't belong in OR's storage/API layer. -- [x] **REQ-GEO-006 — BAG and BGT base-registration integration.** Moved to consuming apps. BAG/BGT overlays are tile-layer choices in the map widget, not OR data. -- [x] **REQ-GEO-007 — Multi-layer map views with layer control.** Pure UI feature. Moved to mydash `map-support`. -- [x] **REQ-GEO-008 — WFS and GeoJSON export.** GeoJSON export comes free with the OR listing API (`?_format=json` already returns GeoJSON for `geo`-typed properties). WFS specifically is a niche export format; deferred to a follow-up `wfs-export` change if a real customer requires it. -- [x] **REQ-GEO-009 — INSPIRE metadata compliance.** Publication-side concern (which datasets are catalogued), not storage-side. INSPIRE metadata records are themselves register objects in the publication app's register; OR doesn't impose a separate compliance pass. -- [x] **REQ-GEO-010 — Geo-fencing with event triggers.** Implementable on top of OR's existing event pipeline + `CustomScopeEvaluatingEvent` once a real consumer needs it. Not a storage/API concern; deferred to a focused `geo-fencing-events` change when triggered by demand. -- [x] **REQ-GEO-012 — Solr and Elasticsearch spatial query support.** Folded into the broader `aggregations-backend-native` change (which adds Solr + ES backends). Geo-specific Solr / ES query construction will live there. -- [x] **REQ-GEO-013 — Map drawing and geometry editing.** UI-side concern. Moved to a follow-up mydash `map-drawing-tools` change. -- [x] **REQ-GEO-014 — NL Design System map styling.** UI-side concern. Each consuming app applies NLDS tokens to its map widget per its own design pass. - -## Hand-off summary - -| Concern | Owner | -|---|---| -| Storage + API (GeoJSON, WGS84, listing filters) | OpenRegister (this change, in-OR-scope items above) | -| Map widget rendering, base layers, drawing tools, layer control, NLDS styling | mydash `map-support` | -| Tilburg WOO map integration | tilburg-woo-ui#436 | -| Spatial-index optimisation (PostGIS GIST, Solr/ES spatial fields) | future `geo-spatial-queries` change (gated on >50k row customer) | -| Geo-fencing event triggers | future `geo-fencing-events` change (demand-gated) | -| WFS export | future `wfs-export` change (demand-gated) | -| Solr / ES spatial query construction | folded into `aggregations-backend-native` | diff --git a/openspec/changes/github-provider-shop-rules/.openspec.yaml b/openspec/changes/github-provider-shop-rules/.openspec.yaml new file mode 100644 index 0000000000..074342d550 --- /dev/null +++ b/openspec/changes/github-provider-shop-rules/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-09 diff --git a/openspec/changes/github-provider-shop-rules/design.md b/openspec/changes/github-provider-shop-rules/design.md new file mode 100644 index 0000000000..2aba807a56 --- /dev/null +++ b/openspec/changes/github-provider-shop-rules/design.md @@ -0,0 +1,152 @@ +## Context + +The credential-broker provider catalogue (`lib/Settings/credential-providers.json`) +is a runtime-immutable security control (credential-broker design D2): its +`allowRules[]` bound exactly which method + path any credential can ever reach on a +host-locked base. New providers and widened rules ship only through a reviewed +release. This change is such a release step: it widens the existing `github` +provider so OpenBuild's redesigned app shop can search GitHub, resolve the PAT's +login, and create repositories when publishing an app — all through the broker, +with the token held in Doriath custody, instead of a token pasted into OpenBuild. + +Source of truth for the broker mechanics (verified against HEAD, not assumed): + +- **Rule matching** — `CredentialBrokerService::assertRuleAllowed()` matches with + exact upper-cased method plus `fnmatch($rulePattern, $matchPath)`. No + `FNM_PATHNAME` flag is passed, so `*` matches path separators too. +- **Path normalisation** — `normalisePath()` rejects `..` traversal and + protocol-relative paths, single-decodes, and strips the query string BEFORE rule + matching. So `GET /search/repositories?q=topic:nextcloud` matches the pattern + `/search/repositories`. +- **Host-lock** — `resolveAndLockUrl()` appends the caller path to `baseUrl` + (`https://api.github.com`) and enforces host equality (`api.github.com`). +- **Auth injection** — `injectAuth()` sets `Authorization: token {secret}` and + discards any caller-supplied `Authorization`/`Host` header. Unchanged here. + +Current `github` allow-rules at HEAD (catalogue v1.1.0): + +```jsonc +[ + { "method": "GET", "pathPattern": "/repos/*" }, + { "method": "GET", "pathPattern": "/user/repos" }, + { "method": "PUT", "pathPattern": "/repos/*/contents/*" }, + { "method": "POST", "pathPattern": "/repos/*/git/*" } +] +``` + +## Decisions + +### GH-1 — Add exactly six rules, no more + +The redesigned shop flow needs precisely: search, identity, repo-create (user + +org), branch-ref update on re-publish, and discovery-topic write on first publish. +The minimal additive set is therefore: + +```jsonc +{ "method": "GET", "pathPattern": "/search/repositories" }, +{ "method": "GET", "pathPattern": "/user" }, +{ "method": "POST", "pathPattern": "/user/repos" }, +{ "method": "POST", "pathPattern": "/orgs/*/repos" }, +{ "method": "PATCH", "pathPattern": "/repos/*/git/refs/*" }, +{ "method": "PUT", "pathPattern": "/repos/*/topics" } +``` + +The `PUT /repos/*/topics` rule exists because OpenBuild's shop discovery contract is +the `openbuild-app` GitHub topic: publishing an unlinked app creates the repo and +must tag it via `PUT /repos/{owner}/{repo}/topics`. The existing +`PUT /repos/*/contents/*` rule is anchored under `/contents/` and does not match the +topics endpoint. + +The `PATCH` rule exists because the Git Data API creates refs with `POST +/repos/{owner}/{repo}/git/refs` (already covered by the existing `POST +/repos/*/git/*` rule) but updates an existing branch ref only via `PATCH +/repos/{owner}/{repo}/git/refs/{ref}` — without it, OpenBuild could publish an app +to a fresh repo but never push a second commit to the same branch. The pattern is +anchored under `/git/refs/`, so repository-settings `PATCH /repos/{owner}/{repo}` +stays denied. + +Nothing for issues, pull-requests, workflows, webhooks, deletes, or org/team +administration — those are permanent surface for a flow that does not use them, and +each would be a cheap follow-up reviewed release if a future feature needs it. + +### GH-2 — No explicit GET contents/git rule is added (verified redundant) + +The task brief asked to verify whether `GET /repos/*` already covers +`GET /repos/{owner}/{repo}/contents/{path}` and `GET /repos/{owner}/{repo}/git/*` +under the broker's glob semantics. It does. Because `assertRuleAllowed()` calls +`fnmatch('/repos/*', $path)` with no `FNM_PATHNAME` flag, the `*` spans slashes. +Verified at HEAD: + +``` +fnmatch('/repos/*', '/repos/owner/repo/contents/path/to/file.json') === true +fnmatch('/repos/*', '/repos/owner/repo/git/trees/main') === true +``` + +So repo reads (including contents and git-data reads) are already permitted by the +existing `GET /repos/*` rule. Adding explicit GET contents/git rules would be dead, +redundant surface. **No such rule is added.** + +### GH-3 — `GET /user` is a distinct rule from the existing `GET /user/repos` + +The broker's `fnmatch` uses the pattern as a literal glob, so `/user` matches only +the exact path `/user` and does NOT match `/user/repos` (verified: +`fnmatch('/user', '/user/repos') === false`). The already-shipped `GET /user/repos` +rule therefore does not authorise `GET /user`, and the new `GET /user` rule does not +broaden `/user/repos` matching. Both coexist as two exact-match rules. + +### GH-4 — `POST /user/repos` and `POST /orgs/*/repos` for repo creation + +GitHub creates a repository under the authenticated user with `POST /user/repos` +and under an organisation with `POST /orgs/{org}/repos`. The org rule uses the +`*` wildcard for the org login segment (`/orgs/*/repos`), matching the catalogue's +established wildcard-for-owner-segment convention (as in `/repos/*`). The user-repo +create path (`/user/repos`) is the same literal path as the already-allowed +`GET /user/repos` but a different method, so it is a separate rule — the broker +matches method + path together, so the GET rule never authorises the POST. + +### GH-5 — Search path lives at `/search/repositories` + +The GitHub repository-search endpoint is `GET /search/repositories` under the same +`https://api.github.com` base. Query parameters (`q`, `sort`, `per_page`, `page`) +are stripped by `normalisePath()` before matching, so the exact pattern +`/search/repositories` covers every search call regardless of its query string. No +wildcard is needed or added. + +## Declarative-vs-imperative note (ADR-031) + +Unchanged from credential-broker D2: the catalogue is a deliberate, +security-justified exception to declarative-first-in-the-register — a +runtime-immutable declarative JSON file in `lib/`, never API-writable. This change +widens one provider's rules in that file through the exact mechanism D2 prescribes +(code review + release). No imperative code, lifecycle, aggregation, notification, +relation, or widget behaviour is added; the change stays `kind: config`. + +## Risks / Trade-offs + +- [Surface growth] The `github` provider gains six new permitted calls. Each is + scoped to the shop flow and justified above; none touches issues/PRs/workflows/ + webhooks/deletes. Repo creation (`POST /user/repos`, `POST /orgs/*/repos`) is the + most consequential new capability — it lets an allow-listed app create repos under + the token's account/orgs. This is the intended publish-an-unlinked-app behaviour; + the token owner controls it by choosing which app to allow-list on the credential. +- [Org wildcard breadth] `/orgs/*/repos` matches any org login, and (because `*` + spans slashes) technically also deeper paths like `/orgs/x/teams/y/repos`. GitHub + has no such create endpoint, so this is not a real broadening; the practical + surface is org-repo creation only. Tightening to a non-slash segment would require + a broker-level `FNM_PATHNAME` change, which is out of scope and would affect every + provider's existing rules. +- [Secret exposure] None added: the PAT lives behind the existing `CredentialStore` + (Doriath custody); this file carries only the `token {secret}` template, never a + token. Examples in specs/tasks use `YOUR_API_KEY_HERE` placeholders only. +- [Regression to other providers/rules] The change is additive; the existing four + `github` rules and the whole `gitlab`/`doffin` entries stay byte-identical, and + the acceptance criteria pin that. + +## Migration Plan + +1. Append the six `github` allow-rules + bump the catalogue `version` (apply task). +2. No data migration, no repair step, no schema change. Rollback = revert the file. +3. Operator/consumer flow after release: the GitHub PAT is stored once as an OR + credential (provider `github`) with the secret held in Doriath custody, OpenBuild + is allow-listed on that credential, and OpenBuild's shop calls the broker for + search, identity, and repo-create/read/write. diff --git a/openspec/changes/github-provider-shop-rules/proposal.md b/openspec/changes/github-provider-shop-rules/proposal.md new file mode 100644 index 0000000000..2ef1455e9e --- /dev/null +++ b/openspec/changes/github-provider-shop-rules/proposal.md @@ -0,0 +1,97 @@ +--- +kind: config +depends_on: + - credential-broker +--- + +## Why + +OpenBuild is being redesigned so its app shop reads apps from GitHub (topic +search) and app owners can publish or pull their virtual apps to/from their own +GitHub repositories, with the personal-access token held in Doriath custody and +every call routed through the credential broker — never a token pasted into +OpenBuild itself. The broker's provider catalogue +(`lib/Settings/credential-providers.json`, runtime-immutable per credential-broker +design D2) currently gives the `github` provider only four allow-rules: +`GET /repos/*`, `GET /user/repos`, `PUT /repos/*/contents/*`, and +`POST /repos/*/git/*`. Those cover repo reads, contents writes and git-data +writes, but the shop flow needs three capabilities the current rules deny: + +- **Repository search** — the shop discovers apps by topic; when a credential is + used (rate-limit upgrade / private discovery) the call is `GET /search/repositories`. +- **Caller identity** — to target `{owner}/{repo}` correctly the flow must first + learn the PAT's own login via `GET /user`. `GET /user/repos` is already allowed, + but `GET /user` (a distinct path under the broker's exact-match glob) is not. +- **Repository creation** — publishing an unlinked app creates a repo under the + user (`POST /user/repos`) or under an org (`POST /orgs/*/repos`). + +Allow-rules are a security control and are NOT runtime-widenable by design +(credential-broker D2, ADR-031 exception) — widening them requires a reviewed +release of the catalogue file. This change IS that reviewed release for `github`. + +## What Changes + +- Widen the `github` provider's `allowRules[]` in the runtime-immutable catalogue + `lib/Settings/credential-providers.json` with the minimal set the shop needs: + - `GET /search/repositories` — topic/keyword shop search (query parameters are + stripped by the broker's path normalisation before rule matching, so the exact + pattern covers every search call). + - `GET /user` — read the PAT's own login (exact match; distinct from the + already-allowed `GET /user/repos`). + - `POST /user/repos` — create a repository under the authenticated user. + - `POST /orgs/*/repos` — create a repository under an organisation. +- Bump the catalogue's top-level `version`. +- The existing four `github` rules stay byte-identical, and `gitlab`/`doffin` + are untouched. + +**No new GET rule for contents/git reads is needed.** The broker matches rules +with `fnmatch(pathPattern, path)` and no `FNM_PATHNAME` flag, so `*` spans path +separators: the existing `GET /repos/*` already matches +`GET /repos/{owner}/{repo}/contents/{path}` and +`GET /repos/{owner}/{repo}/git/{...}` (verified against +`CredentialBrokerService::assertRuleAllowed()` at HEAD). Adding explicit GET +contents/git rules would be redundant surface. + +## Non-Goals + +- **No issues, pull-requests, workflows, or webhooks.** The shop flow is search + + identity + repo-create + the already-permitted contents/git writes. Any of those + broader surfaces would be a separate reviewed release with its own justification. +- **No widening of `gitlab` or `doffin`.** Their entries stay byte-identical. +- **No runtime/API mutation path for the catalogue** — the file stays + runtime-immutable (credential-broker D2); this change is itself the + reviewed-release mechanism D2 prescribes. +- **No OpenBuild code.** This change only widens the OpenRegister catalogue so the + broker will permit the calls; wiring the OpenBuild shop to the broker is separate + work in the OpenBuild repo. +- Obtaining the GitHub PAT and storing it in Doriath custody stays a manual + operator step; the token is pasted once into the OR credential settings UI, never + into this file or OpenBuild. + +## Capabilities + +### Modified Capabilities + +- `credential-broker`: the `github` provider's allow-rule set is widened with six + additional minimal rules (repository search, caller identity, user-repo create, + org-repo create, branch-ref update, discovery-topic write). The catalogue + requirement itself (runtime-immutable `lib/` + file, no mutation API, entries change only via reviewed release) is unchanged; + this is an additive rule set shipped through the designed reviewed-release path. + +## Impact + +- **Changed file**: `lib/Settings/credential-providers.json` (six additional + `github` allow-rules + version bump). The existing `github` rules and the + `gitlab`/`doffin` entries stay byte-identical. +- **No PHP/Vue changes**: `ProviderCatalogue`, `CredentialBrokerService` (four + ordered guards incl. `fnmatch` rule matching and host-lock), and + `CredentialController::providers()` consume catalogue entries generically. +- **Downstream**: once merged, OpenBuild (with the PAT held in Doriath custody and + allow-listed on the credential) can, through the broker, search repositories, + resolve the token's login, create repos (and tag them with the discovery topic) + when publishing an unlinked app, and advance an existing branch ref when + re-publishing — in addition to the repo reads and contents/git writes already + permitted. +- **Fail-closed unchanged**: any GitHub path outside the (now-widened) rule set + still denies with the static 403; the surface grows only by the six named rules. diff --git a/openspec/changes/github-provider-shop-rules/specs/credential-broker/spec.md b/openspec/changes/github-provider-shop-rules/specs/credential-broker/spec.md new file mode 100644 index 0000000000..2d1645dd74 --- /dev/null +++ b/openspec/changes/github-provider-shop-rules/specs/credential-broker/spec.md @@ -0,0 +1,86 @@ +## Purpose + +Delta against the `credential-broker` capability: the runtime-immutable provider +catalogue (`lib/Settings/credential-providers.json`) widens the existing `github` +provider's allow-rules so OpenBuild's redesigned app shop can search repositories, +resolve the PAT's own login, and create repositories (under the user or an org) +through the broker — with the token held in Doriath custody. The catalogue's own +requirement (runtime-immutable `lib/` file, no mutation API, entries change only via +reviewed release) is unchanged — this delta is one such reviewed release. + +## ADDED Requirements + +### Requirement: GitHub shop allow-rules + +The provider catalogue's `github` entry SHALL permit, in addition to its existing +repo-read, contents-write and git-data-write rules, exactly the following six +allow-rules and no more: `GET /search/repositories` (repository search), +`GET /user` (read the authenticated token's login), `POST /user/repos` (create a +repository under the authenticated user), `POST /orgs/*/repos` (create a +repository under an organisation), `PATCH /repos/*/git/refs/*` (advance an +existing git ref when publishing — the existing `POST /repos/*/git/*` rule covers +ref creation but not the update of an existing branch ref, which the Git Data API +exposes only as `PATCH`), and `PUT /repos/*/topics` (set a repository's discovery +topics when publishing — the existing `PUT /repos/*/contents/*` rule is anchored +under `/contents/` and does not cover the topics endpoint). No rule for the +`github` provider SHALL permit +issues, pull-requests, workflows, webhooks, deletes, or any path outside this set, +and the entry SHALL be mutable only through a reviewed release like every other +catalogue entry. The pre-existing `github` rules and the `gitlab`/`doffin` entries +SHALL remain unchanged. + +#### Scenario: Repository search allowed with query parameters + +- **WHEN** a brokered call for a `github` credential requests + `GET /search/repositories?q=topic:nextcloud-app&per_page=25` +- **THEN** the query string is stripped for rule matching and the + `GET /search/repositories` allow-rule matches, so the call proceeds host-locked to + `api.github.com` + +#### Scenario: Token identity lookup allowed and distinct from user-repos + +- **WHEN** a brokered call requests `GET /user` +- **THEN** the `GET /user` allow-rule matches exactly and the call proceeds +- **AND** the already-shipped `GET /user/repos` rule is unaffected (a call to + `GET /user/repos` still matches its own rule, and `GET /user` does not match + `/user/repos`) + +#### Scenario: Repository creation under the user and an organisation + +- **WHEN** a brokered call requests `POST /user/repos`, or `POST /orgs/conduction/repos` +- **THEN** the corresponding new allow-rule (`POST /user/repos` / + `POST /orgs/*/repos`) matches and the call proceeds host-locked to `api.github.com` +- **AND** a `GET` on `/user/repos` still matches only its own read rule, never the + create rule (method + path are matched together) + +#### Scenario: Existing repo reads and writes remain covered + +- **WHEN** a brokered call requests `GET /repos/owner/repo/contents/path`, + `GET /repos/owner/repo/git/trees/main`, `PUT /repos/owner/repo/contents/file`, or + `POST /repos/owner/repo/git/blobs` +- **THEN** each is permitted by the pre-existing `github` rules (the `GET /repos/*` + glob spans path separators), so no additional GET contents/git rule is added + +#### Scenario: Branch ref update allowed, repo-settings PATCH denied + +- **WHEN** a brokered call requests `PATCH /repos/owner/repo/git/refs/heads/main` + (fast-forwarding a branch to a new commit during publish) +- **THEN** the `PATCH /repos/*/git/refs/*` allow-rule matches and the call proceeds + host-locked to `api.github.com` +- **AND** a `PATCH /repos/owner/repo` call (repository settings) matches no rule and + the broker denies it with the static 403 + +#### Scenario: Discovery-topic write allowed + +- **WHEN** a brokered call requests `PUT /repos/owner/repo/topics` (setting the + `openbuild-app` discovery topic on a freshly published repository) +- **THEN** the `PUT /repos/*/topics` allow-rule matches and the call proceeds + host-locked to `api.github.com` + +#### Scenario: Out-of-scope GitHub calls stay denied + +- **WHEN** a brokered call requests an issues, pull-request, workflow, webhook, or + delete path (e.g. `POST /repos/owner/repo/issues`, `DELETE /repos/owner/repo`), or + any other path not in the `github` rule set +- **THEN** no `github` allow-rule matches and the broker denies the call with the + static 403, failing closed diff --git a/openspec/changes/github-provider-shop-rules/tasks.md b/openspec/changes/github-provider-shop-rules/tasks.md new file mode 100644 index 0000000000..4fe45f2b57 --- /dev/null +++ b/openspec/changes/github-provider-shop-rules/tasks.md @@ -0,0 +1,25 @@ +## 1. Catalogue rules + +- [ ] 1.1 In `lib/Settings/credential-providers.json`, append six allow-rules to the `github` provider's `allowRules[]`: `{method:"GET", pathPattern:"/search/repositories"}`, `{method:"GET", pathPattern:"/user"}`, `{method:"POST", pathPattern:"/user/repos"}`, `{method:"POST", pathPattern:"/orgs/*/repos"}`, `{method:"PATCH", pathPattern:"/repos/*/git/refs/*"}`, `{method:"PUT", pathPattern:"/repos/*/topics"}`. Leave the four existing `github` rules and the whole `gitlab`/`doffin` entries byte-identical. Do NOT add any GET contents/git rule — `GET /repos/*` already covers those (fnmatch spans slashes). +- [ ] 1.2 Bump the catalogue's top-level `version` (1.1.0 → 1.2.0) and extend the file's `$comment` only if the GitHub-shop rationale needs recording there. + +## 2. Verification + +- [ ] 2.1 JSON-validate the catalogue file; run `composer check:strict`. +- [ ] 2.2 Add/extend unit coverage on `ProviderCatalogue` / broker rule matching: `GET /search/repositories` (with query string) matches; `GET /user` matches and is distinct from `GET /user/repos`; `POST /user/repos` and `POST /orgs/conduction/repos` match; `GET /repos/owner/repo/contents/x` and `GET /repos/owner/repo/git/trees/main` still match `GET /repos/*`; `PATCH /repos/owner/repo/git/refs/heads/main` matches; `PUT /repos/owner/repo/topics` matches and `PUT /repos/owner/repo` (repo settings, wrong method anyway) denies; `POST /repos/owner/repo/issues`, `DELETE /repos/owner/repo`, `PATCH /repos/owner/repo` (repo settings), and `GET /user` used as `POST /user` all deny (static 403); host-lock resolves to `api.github.com`. +- [ ] 2.3 Confirm no new mutation surface: no route, controller method, or service writes the catalogue (unchanged invariant; grep-level check is enough). + +## Acceptance criteria + +- The `github` entry gains exactly six rules: `GET /search/repositories`, `GET /user`, `POST /user/repos`, `POST /orgs/*/repos`, `PATCH /repos/*/git/refs/*`, `PUT /repos/*/topics` — and no others. +- The four pre-existing `github` rules and the `gitlab`/`doffin` entries are unchanged (byte-identical). +- No explicit GET contents/git rule is added; `GET /repos/*` is confirmed to already cover contents and git-data reads under the broker's glob semantics. +- Issues, pull-requests, workflows, webhooks, and deletes remain denied (fail-closed static 403). +- No secret, PAT, or realistic-looking token appears anywhere in the diff — placeholders only (`YOUR_API_KEY_HERE` in tests/spec examples). +- The catalogue remains runtime-immutable: no API can create, update, or delete the `github` rules; the `authScheme` template stays exactly `token {secret}`. + +## Quality checklist + +- ADR-031: catalogue stays the reviewed-release, runtime-immutable exception (credential-broker D2); this change is additive config only. +- Kind stays `config`: no PHP/Vue surface ships beyond test coverage of existing generic code paths. +- Scope is search + identity + repo-create + branch-ref update + discovery-topic write only — no issues/PRs/workflows/webhooks, per the proposal Non-Goals. diff --git a/openspec/changes/harden-audit-seal-concurrency/proposal.md b/openspec/changes/harden-audit-seal-concurrency/proposal.md new file mode 100644 index 0000000000..d12a792274 --- /dev/null +++ b/openspec/changes/harden-audit-seal-concurrency/proposal.md @@ -0,0 +1,75 @@ +--- +kind: code +--- + +## Why + +Two follow-ups from the write-path performance wave (PR #399 review): + +1. **Seal passes race each other (openregister#406).** `AuditHashService:: + sealRow()` and `sealRows()` both read a predecessor hash + (`getHashBefore()`) and then UPDATE. Two interleaved passes — a + concurrent writer whose row lands inside a batch's id range, or a + boundary row sealed between another pass's predecessor read and its + UPDATE — can chain one link over a stale predecessor. The next + `verifyChain()` then reports a break at that boundary: a FALSE tamper + alarm (not tamper-hiding — resealing recomputes deterministically). + Pre-existing in `sealRow()`, widened to the batch duration by + `sealRows()`. + + On top of the race, `getHashBefore()` had a documented weakness: when + the immediately-prior row is UNSEALED (a fail-soft leftover), the next + seal chained from genesis while `verifyChain()`'s walk skips null-hash + rows and carries the last SEALED hash forward — a PERMANENT false break + at that link. + +2. **Dead ChunkProcessingHandler (openregister#407).** `lib/Service/Object/ + SaveObjects/ChunkProcessingHandler.php` has zero callers in `lib/` and + `appinfo/` — `SaveObjects` performs chunk processing through its own + internal `processObjectsChunk()`. The class is an orphaned capability: + implemented, tested, DI-resolvable, and invoked by nothing. + +## What Changes + +- Serialize ALL seal passes under one exclusive advisory lock + (`OCP\Lock\ILockingProvider`, key `openregister/audit-seal`) taken by + both `sealRow()` and `sealRows()`. A well-known advisory lock is used + instead of `SELECT ... FOR UPDATE` because the primary race is a row + INSERTed into the range by a concurrent uncommitted transaction — row + locks cannot lock a row that does not exist yet (and gap locking is + engine-specific), while a single advisory key serializes the passes + themselves and cannot deadlock with the surrounding request transaction. +- Lock acquisition is bounded (3 attempts, 50ms apart) and fail-soft: + on sustained contention the pass logs a warning and leaves the rows + unsealed — a later seal pass chains them — instead of blocking the + write path. The lock is always released, including when the seal pass + throws. +- `getHashBefore()` now returns the hash of the nearest PRIOR **SEALED** + row (filtering `hash IS NULL` / `hash = ''`), exactly mirroring + `verifyChain()`'s skip-null walk. Sealing "row N unsealed, row N+1 + sealed" now verifies instead of permanently breaking. This also fixes + ranged verification (`verifyChain(from: ...)`) starting right after an + unsealed row. +- Delete dead `ChunkProcessingHandler` (class + unit tests + integration + test section); correct the spec references that presented it as the + bulk chunking implementation (`SaveObjects` itself chunks). + +## Impact + +- Affected specs: `audit-hash-chain` (serialization requirement made + concrete + new nearest-sealed-predecessor requirement, via delta); + `object-lifecycle` REQ-004, `data-import-export`, and + `method-decomposition` (implementation references corrected from + `ChunkProcessingHandler` to `SaveObjects`' internal chunk processing — + edited canonically because their `REQ-NNN` heading style cannot be + targeted by a MODIFIED delta). +- Affected code: `lib/Service/AuditHashService.php`; + `lib/Service/Object/SaveObjects/ChunkProcessingHandler.php` (deleted); + `tests/Unit/Service/AuditHash*`, `tests/Unit/Service/Object/SaveObjects/ + ChunkProcessingHandlerTest.php` (deleted), `tests/Service/ + SaveObjectHandlersIntegrationTest.php`. +- No API or schema changes. `AuditHashService`'s constructor gains + `ILockingProvider` + `LoggerInterface` (both DI-autowired). +- Failure-mode change: under seal-lock contention rows are left unsealed + (warning logged) instead of risking a false tamper alarm; unsealed rows + are chained over by both sealing and verification. diff --git a/openspec/changes/harden-audit-seal-concurrency/specs/audit-hash-chain/spec.md b/openspec/changes/harden-audit-seal-concurrency/specs/audit-hash-chain/spec.md new file mode 100644 index 0000000000..1af154454c --- /dev/null +++ b/openspec/changes/harden-audit-seal-concurrency/specs/audit-hash-chain/spec.md @@ -0,0 +1,67 @@ +## MODIFIED Requirements + +### Requirement: Hash chain writes MUST be serialized to prevent race conditions + +Concurrent audit trail inserts MUST NOT produce broken hash chains. All seal +passes — per-row (`sealRow()`) and batched (`sealRows()`) — MUST serialize +their critical section (predecessor-hash read through hash UPDATE) under one +exclusive advisory lock with the well-known key `openregister/audit-seal` +(`OCP\Lock\ILockingProvider`), so no two passes can interleave a predecessor +read with another pass's write. Lock acquisition MUST be bounded (short +retry window, no indefinite wait) and fail-soft: when the lock cannot be +acquired the pass MUST log a warning and leave the rows unsealed — a later +seal pass chains them, and unsealed rows are skipped by both verification +and predecessor lookup — rather than blocking the audit write path. The +lock MUST always be released, including when the seal pass throws. + +#### Scenario: Two simultaneous audit writes + +- **WHEN** two audit trail entries are created at the same moment +- **THEN** both entries MUST be correctly chained (each referencing the correct previous hash) +- **AND** no two entries MUST share the same `previousHash` value (except the genesis hash for the first entry) + +#### Scenario: Seal passes run under the shared exclusive advisory lock + +- **WHEN** `sealRow()` or `sealRows()` seals audit rows +- **THEN** the exclusive lock `openregister/audit-seal` is acquired before the predecessor hash is read +- **AND** the lock is released after the hash UPDATE completes + +#### Scenario: Lock contention is fail-soft + +- **WHEN** the seal lock cannot be acquired within the bounded retry window +- **THEN** a warning is logged and the rows are left unsealed +- **AND** no database read or write is performed by the abandoned pass +- **AND** the audit rows themselves remain persisted (sealing is fail-soft, the insert is not) + +#### Scenario: Lock released when the seal pass throws + +- **WHEN** the seal pass fails mid-flight (e.g. a database error) +- **THEN** the advisory lock is released regardless +- **AND** the failure propagates to the caller's existing fail-soft handling + +## ADDED Requirements + +### Requirement: Seal passes MUST chain from the nearest prior SEALED row + +The predecessor lookup for a seal pass MUST skip unsealed rows (`hash` NULL +or empty — fail-soft leftovers or pre-migration entries) and MUST return the +nearest PRIOR row with a non-empty `hash` as the `previousHash` source, +falling back to the genesis hash when no sealed predecessor exists. +This mirrors exactly how `verifyChain()` walks the chain (null-hash rows are +skipped and counted as `skippedNullHashes` while the last SEALED hash is +carried forward). Chaining from the immediately-prior row regardless of its +seal state would fall back to genesis whenever that row is unsealed, +producing a PERMANENT false chain break at that link. + +#### Scenario: Unsealed predecessor does not break the chain + +- **GIVEN** row N is an unsealed fail-soft leftover and row N-1 is sealed with hash `h` +- **WHEN** row N+1 is sealed +- **THEN** row N+1's `previousHash` is `h` (the nearest sealed predecessor), not the genesis hash +- **AND** `verifyChain()` over rows N-1..N+1 returns `valid: true` with `skippedNullHashes: 1` + +#### Scenario: Predecessor lookup filters unsealed rows in the query + +- **WHEN** the predecessor hash for a seal pass is read +- **THEN** the lookup query excludes rows with `hash` NULL or empty +- **AND** returns the highest-id remaining row below the sealed row's id diff --git a/openspec/changes/harden-audit-seal-concurrency/tasks.md b/openspec/changes/harden-audit-seal-concurrency/tasks.md new file mode 100644 index 0000000000..a3e740fd07 --- /dev/null +++ b/openspec/changes/harden-audit-seal-concurrency/tasks.md @@ -0,0 +1,22 @@ +# Tasks — harden-audit-seal-concurrency + +## 1. Advisory lock around seal passes (openregister#406) + +- [x] 1.1 Add exclusive advisory lock (`OCP\Lock\ILockingProvider`, well-known key `openregister/audit-seal`) taken by BOTH `sealRow()` and `sealRows()` around the whole critical section (predecessor read + hash writes) — `lib/Service/AuditHashService.php` (`acquireSealLock()` / `releaseSealLock()`, bodies extracted to `sealRowLocked()` / `sealRowsLocked()`). +- [x] 1.2 Bounded, fail-soft acquisition: 3 attempts 50ms apart; on sustained contention log a warning and leave rows unsealed (later seal pass chains them) instead of blocking the write path. Lock released in `finally`, including when the seal pass throws. +- [x] 1.3 Fix `getHashBefore()` to return the nearest PRIOR **SEALED** row's hash (filter `hash IS NULL` / `hash = ''`), mirroring `verifyChain()`'s skip-null walk — kills the permanent false break when the immediately-prior row is an unsealed fail-soft leftover. +- [x] 1.4 Tests — `tests/Unit/Service/AuditHashSealRowsTest.php`: lock acquire/release around `sealRows()` (exact key + exclusive type), lock-unavailable fail-soft for `sealRow()` (3 attempts, warning, zero DB access, no release) and `sealRows()`, release-on-throw, retry-after-transient-conflict, no lock touch for empty input; `testSealChainsFromNearestSealedPredecessorAndVerifies` proves "row N unsealed, row N+1 sealed" chains onto the nearest sealed hash (predecessor query filtered via `isNotNull('hash')` + `neq('hash', '')`) and `verifyChain()` reports `valid: true` with `skippedNullHashes: 1`. +- [x] 1.5 Existing audit-chain suites stay green: AuditHashServiceTest, AuditHashSealRowsTest, AuditHashChainConsistencyTest, AuditTrailMapperBulkInsertTest, AuditTrailMapperBulkTest (constructor wiring updated to the new `ILockingProvider` + `LoggerInterface` dependencies). + +## 2. Remove dead ChunkProcessingHandler (openregister#407) + +- [x] 2.1 Verify zero callers: `grep -rn ChunkProcessingHandler lib/ appinfo/` matches only the class file itself; no DI registration (autowire-only); `SaveObjects` chunks via its own internal `processObjectsChunk()`. +- [x] 2.2 Delete `lib/Service/Object/SaveObjects/ChunkProcessingHandler.php` and `tests/Unit/Service/Object/SaveObjects/ChunkProcessingHandlerTest.php`. +- [x] 2.3 Remove the ChunkProcessingHandler property, container resolution, and its five `processObjectsChunk` test methods from `tests/Service/SaveObjectHandlersIntegrationTest.php`. +- [x] 2.4 Spec references corrected — bulk chunking is `SaveObjects`' internal `processObjectsChunk()`, not a separate handler. `object-lifecycle` REQ-004, `data-import-export` prose, and the `method-decomposition` file list were edited in the canonical specs directly: their `### REQ-NNN:` heading style is not parseable by the openspec delta format (`### Requirement:`), so a MODIFIED delta cannot target them; the edits are implementation-reference corrections, not behaviour changes. + +## 3. Verification + +- [x] 3.1 `composer install`; phpcs/phpmd/phpstan/psalm clean on touched files (php 8.3 container). +- [x] 3.2 Full `tests/Unit/Service/AuditHash*` + `tests/Unit/Db` suites green in the php 8.3 phpunit container. +- [x] 3.3 `openspec validate harden-audit-seal-concurrency --type change --strict` passes. diff --git a/openspec/changes/hybrid-document-search/.openspec.yaml b/openspec/changes/hybrid-document-search/.openspec.yaml new file mode 100644 index 0000000000..dd9a1d92eb --- /dev/null +++ b/openspec/changes/hybrid-document-search/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-06 diff --git a/openspec/changes/hybrid-document-search/design.md b/openspec/changes/hybrid-document-search/design.md new file mode 100644 index 0000000000..69bd898836 --- /dev/null +++ b/openspec/changes/hybrid-document-search/design.md @@ -0,0 +1,332 @@ +## Context + +`openregister_vectors` stores one row per (entity, chunk) with a serialized-PHP- +array `embedding` BLOB (verified: `Version002003000Date20251013000000`, +`embedding` column is `blob`, `notnull`). `VectorSearchHandler::semanticSearch()` +fetches candidate rows via `fetchVectors()`, which defaults `max_vectors` to 500 +and orders `created_at DESC` (verified: `lib/Service/Vectorization/Handlers +/VectorSearchHandler.php` lines 467-470), then `unserialize()`s each embedding +and computes cosine similarity in a PHP loop. Past 500 stored vectors this +returns the 500 newest, not the 500 most similar — a silent relevance +regression that gets worse the more content an install has vectorized. + +`openregister_chunks` (verified: `Version1Date20251116000000`) already carries +`text_content` (TEXT), `indexed` (bool), and `vectorized` (bool, with an index +`chunks_vector_idx`) — `ChunkMapper::countVectorized()` / +`findUnindexed()` already exist, but there is no `findUnvectorized()` and no +background job consumes it: vectorization only happens when +`VectorizationService::vectorizeBatch('file', ...)` is invoked manually. + +`FileSearchController` (verified) has two routes already registered in +`appinfo/routes.php`: `POST /api/search/files/semantic` and +`POST /api/search/files/hybrid`. Both exist; neither needs a new route — the +proposal's "one hybrid route returning `{results,total}`" is a response-shape +fix to `hybridSearch()`, not a new endpoint (see Decision 5). + +`SettingsController` (verified, lines ~460-530) already diagnoses Postgres + +pgvector-extension presence for an admin-facing "database info" panel — this +change adds the schema/index side that panel currently only *describes* +("Recommended: Migrate to PostgreSQL + pgvector") without OpenRegister itself +using it anywhere. + +The 2026-07-05 spike (`SPECTR-NEXTCLOUD-PLAN.md` §3.4) measured a `pg_trgm` GIN +index cutting a rare-term ILIKE seq-scan from 268ms to 1.5ms on a magic table — +direct evidence that "add the missing index" is the fix, not "rewrite the query +logic" (the query logic for keyword search over chunks doesn't exist at all yet; +the vector-search query logic exists but runs unindexed in PHP). + +## Goals / Non-Goals + +**Goals:** +- Vector similarity search on Postgres becomes index-backed (HNSW ANN), not a + full-table PHP cosine loop capped at 500 rows. +- Keyword ranking over chunk text becomes a real, ranked (`ts_rank`) SQL path, + not absent. +- `hybridSearch()`'s RRF fusion combines two real result sets, not one real + + one hard-coded-empty. +- The three or-#277-class bugs (entity_type key mismatch, hard-coded empty + keyword arm, wrong `total`) are fixed as part of touching this exact code. +- Newly extracted chunks become searchable automatically (no manual admin + action required for basic search quality). +- MariaDB/SQLite installs keep working exactly as today (PHP-cosine fallback, + no ranked keyword arm — same as the pre-change baseline on those platforms). + +**Non-Goals:** +- Resurrecting Solr (`SearchBackendHandler` is deliberately database-only per + `remove-solr-and-publishing` — out of scope, not revisited here). +- A general-purpose search UI or nc-vue `CnSearchPage` data-source contract + (plan §4.3(b) — a separate nc-vue change, not this repo). +- Object-property (magic-table) search improvements — that's the sibling spec + `searchable-property-index` (different subsystem: schema properties on + typed object tables, not file/chunk text). +- Multi-model / mixed-dimension pgvector columns — the column is sized for the + currently-configured embedding model's dimension only (see Decision 2). +- Re-ranking or LLM-based result re-ordering — RRF fusion only, as today. + +## Decisions + +> **IMPLEMENTATION AMENDMENT (2026-07-06, live-verified during apply on the +> spectr-spike instance — PostgreSQL 16 + pgvector 0.8.1 + NC 34):** Decisions +> 1 and 7's *storage mechanics* had to change; their intent, guarantees and +> warm-up semantics are implemented unchanged. Adding a `vector`- or +> `tsvector`-typed column to an `oc_`-prefixed table breaks Nextcloud +> permanently: `OC\DB\Migrator::createSchema()` runs Doctrine's +> `introspectSchema()` over ALL prefix-matched tables for EVERY subsequent +> migration of every app (and core upgrades), and Doctrine throws +> `Unknown database type vector requested` on the first unknown column type. +> This was hit live: this change's own second migration failed with exactly +> that error after the first migration added the column. Equivalents shipped: +> +> - **Vectors (Decision 1):** an UNPREFIXED sidecar table +> `openregister_vec_ann` (`vector_id BIGINT PK REFERENCES +> {prefix}openregister_vectors(id) ON DELETE CASCADE, embedding vector(N) +> NOT NULL`) + HNSW cosine index. Unprefixed tables are invisible to the +> `/^{prefix}/` migration filter, so Doctrine never introspects the vector +> type. "Sidecar row absent" is the exact equivalent of the designed +> "`embedding_vector IS NULL`" for the job-only warm-up; dual-write is an +> idempotent upsert; deletes cascade. Skipped (PHP fallback) when +> `dbtableprefix` is empty, since then nothing can be hidden from the filter. +> - **Chunks (Decision 7):** a functional (expression) GIN index +> `USING gin (to_tsvector('simple', text_content))` instead of a STORED +> generated column — zero schema-visible changes, same index-backed ranked +> `ts_rank` query (the query uses the same expression so the planner uses +> the index). Precedent: `Version1Date20260322120000`'s +> `(retention::jsonb) jsonb_path_ops` expression GIN index already lives on +> production installs without breaking migrations. + +### 1. Additive pgvector column, BLOB stays as the storage-of-record fallback + +Add `embedding_vector` (pgvector `vector(N)`, nullable) to `openregister_vectors` +alongside the existing `embedding` BLOB column — do not replace the BLOB. The +BLOB remains the durable, platform-portable storage (works on MariaDB/SQLite); +`embedding_vector` is a Postgres-only accelerated *index path* populated +opportunistically. This mirrors the exact precedent in +`Version1Date20260322120000` (retention-JSON GIN index): the canonical `json` +column stays; a derived Postgres-specific structure is added beside it, guarded +by `str_contains(get_class($platform), 'PostgreSQL')`. + +Migration mechanics: `ISchemaWrapper`/Doctrine DBAL has no native pgvector type, +so the column is added via raw SQL in `postSchemaChange()` (same pattern as the +retention migration's `createPostgreSqlIndex()` / `createMariaDbIndexes()` +split): `ALTER TABLE {prefix}openregister_vectors ADD COLUMN IF NOT EXISTS +embedding_vector vector({dimensions})` guarded behind `CREATE EXTENSION IF NOT +EXISTS vector` (idempotent, already proven safe — `SettingsController` already +queries `pg_extension` for `vector` without issue). Skip entirely (log info, +return) on non-Postgres platforms, matching the retention migration's "Skipping +… unsupported database platform" branch. + +**Alternative considered**: replace the BLOB column outright. Rejected — it +would make MariaDB/SQLite installs (which `SettingsController` already +explicitly supports as a documented tier: "SQLite not recommended for +production… migrate to PostgreSQL") lose vector search entirely instead of +degrading gracefully to the existing PHP path. + +### 2. Fixed embedding dimension per column; mixed-dimension rows stay BLOB-only + +`vector(N)` requires a static `N`. OpenRegister supports multiple embedding +providers/models with different dimensions (verified: +`VectorEmbeddings::checkEmbeddingModelMismatch()` already exists specifically +because historical vectors can carry a different model/dimension than the +currently configured one, and the documented remedy today is "Clear All +Embeddings" + re-vectorize). Design choice: size `embedding_vector` at +migration time from the *currently configured* model's dimension +(`SettingsService::getLLMSettingsOnly()`); rows whose `embedding_dimensions` +column doesn't match are left with `embedding_vector IS NULL` and continue to +be served by the PHP-cosine fallback path until re-vectorized. This is +consistent with the existing model-mismatch handling — it doesn't introduce a +new class of inconsistency, it extends the existing one to a second column. + +**Alternative considered**: multiple `vector(N)` columns (one per known +dimension family — 1536, 3072, etc.). Rejected as premature — the existing +system already treats "embedding model changed" as an event requiring +re-vectorization (`checkEmbeddingModelMismatch` + "Clear All Embeddings"), so a +single active-dimension column matches the system's existing single-active-model +assumption rather than fighting it. + +### 3. SQL KNN as the primary path, PHP-cosine as an explicit fallback (not deleted) + +`VectorSearchHandler::semanticSearch()` gains a Postgres branch: when +`embedding_vector` is populated for the candidate set, run `SELECT … FROM +openregister_vectors WHERE embedding_vector IS NOT NULL {filters} ORDER BY +embedding_vector <=> :queryVector LIMIT :limit` (the `<=>` cosine-distance +operator from pgvector, ordered ascending = most similar first) with an HNSW +index on `embedding_vector` (`CREATE INDEX … USING hnsw (embedding_vector +vector_cosine_ops)`) to make that `ORDER BY … LIMIT` index-backed rather than a +sequential scan. The existing `fetchVectors()` + PHP `cosineSimilarity()` loop +remains verbatim as the fallback (MariaDB/SQLite, or Postgres installs where +the pgvector extension isn't installed, or unmatched-dimension rows) — same +method, same signature, just no longer the only path. Only the fallback's +`fetchVectors()` `orderBy('created_at', 'DESC')` bias is removed (see Decision +4); its `max_vectors` cap stays as an explicit, documented "approximate +fallback" bound (renamed/commented, not silently framed as relevance). + +**Alternative considered**: require pgvector and drop the PHP path entirely. +Rejected — breaks MariaDB/SQLite installs and installs without the extension, +which `SettingsController`'s own diagnostics already treat as a supported (if +suboptimal) tier. + +### 4. Remove the recency bias from the fallback path, keep a bound + +The current `orderBy('created_at', 'DESC')` inside `fetchVectors()` silently +substitutes "newest 500" for "most relevant" once an install exceeds 500 +vectors — this is the or-#277-adjacent bug the spectr deep-dive called out by +name. Fix: on the fallback path, order by primary key (`id ASC`) instead of +`created_at DESC` — a stable, non-biasing tiebreaker — and keep `max_vectors` +as a documented safety cap (default unchanged at 500, now callable via +`$filters['max_vectors']` same as today) so a MariaDB install with tens of +thousands of chunks doesn't `unserialize()` and cosine-score all of them on +every request. This is a pragmatic bound on an inherently O(n) fallback, not a +silent relevance distortion — the difference from today is that it no longer +*pretends* to rank by relevance while actually ranking by recency. + +### 5. Response-shape fix for `hybridSearch()`, no new route + +`FileSearchController::hybridSearch()` currently does +`'total' => count($results)` where `$results` is the *entire* nested return of +`VectorizationService::hybridSearch()` (keys: `results`, `total`, +`search_time_ms`, `source_breakdown`, `weights`) — so `total` is always ~5, +and the outer `results` key nests that whole structure instead of the flat +result list. Fix: destructure the service response and return +`{success, query, results: $serviceResponse['results'], total: +$serviceResponse['total'], search_time_ms, source_breakdown, weights, +search_type: 'hybrid'}` — this *is* the "`POST /api/search/files` hybrid route +returning `{results,total}` shaped for a search page" from the plan; the route +already exists at `/api/search/files/hybrid`, only its response body was wrong. +`semanticSearch()`'s response is already flat (`results`, `total` from +`count($results)` over the real result array) and needs no shape change — +only its filter-key bug (Decision 6). + +### 6. `entityType` → `entity_type` filter-key fix + +`FileSearchController::semanticSearch()` passes `filters: ['entityType' => +'file']`; `VectorSearchHandler::fetchVectors()` reads `$filters['entity_type']` +— the mismatch means the WHERE clause never applies and file-scoped search +silently searches every entity type. Fix: change the controller to pass +`entity_type` (snake_case, matching the handler's contract and the +`openregister_vectors.entity_type` column name it filters on directly). + +### 7. Ranked keyword arm: `tsvector` generated column + GIN + `ts_rank` + +Add `text_search` (Postgres `tsvector`, `GENERATED ALWAYS AS +(to_tsvector('simple', text_content)) STORED`) to `openregister_chunks` + +`CREATE INDEX … USING GIN (text_search)`, added the same way as Decision 1 +(raw SQL in `postSchemaChange()`, Postgres-only, skip elsewhere). New +`ChunkMapper::searchByKeyword(string $query, int $limit, array $filters = +[]): array` builds `SELECT *, ts_rank(text_search, plainto_tsquery('simple', +:q)) AS score FROM openregister_chunks WHERE text_search @@ +plainto_tsquery('simple', :q) ORDER BY score DESC LIMIT :limit` on Postgres; +returns `[]` with a logged warning on other platforms (there is no pre-existing +ranked keyword path on any platform to regress — this is purely additive). +`'simple'` (not `'english'`) is chosen to avoid English-only stemming bias +given OpenRegister's Dutch-government usage context; revisit if per-chunk +`language`/`language_level` (already tracked columns on `Chunk`) should drive a +per-row tsvector-config choice — flagged as an open question, not blocking. + +`FileSearchController::hybridSearch()` calls `ChunkMapper::searchByKeyword()` +and maps its rows into the `{entity_type: 'file', entity_id, score, chunk_text, +chunk_index, metadata}` shape `VectorSearchHandler::reciprocalRankFusion()` +already expects from `$keywordResults` (verified: the RRF implementation reads +`entity_type`, `entity_id`, `score`, `chunk_text`, `metadata` per element — +no changes needed to `reciprocalRankFusion()` itself, only to what's passed in). + +### 8. Auto-vectorization as a native `TimedJob`, not `ScheduledWorkflow`/n8n + +**Declarative-vs-imperative decision (ADR-031)**: this change's background job +is system-maintenance/pipeline infrastructure (embedding-generation batch +processing), not business-domain workflow automation — it has no lifecycle, +aggregation, derived-field, notification, relation, or dashboard-widget shape, +so none of ADR-031's declarative triggers apply. Where the task instructions +ask this to be justified against ADR-031's exception list: this falls under +"scheduled bulk work" — but the exception list's suggested mechanism +(`ScheduledWorkflow` + n8n) is for *business*-process scheduling. The two +directly analogous precedents already in this codebase — +`BlobMigrationJob` (batch-migrates blob-table rows to magic tables) and +`FileTextExtractionJob`/`CronFileTextExtractionJob` (batch-extracts chunk +text) — both use native Nextcloud `OCP\BackgroundJob\TimedJob`/`QueuedJob`, +not n8n, because they are OpenRegister-internal data-pipeline maintenance, the +same category as this new job. `ChunkVectorizationJob` follows the same shape: +`TimedJob`, 5-minute interval (matching `BlobMigrationJob::INTERVAL`), batch +size 50-100, reads `ChunkMapper::findUnvectorized()` (new method, mirrors +`findUnindexed()`), calls `VectorizationService::vectorizeBatch()` / +`generateBatchEmbeddings()` per batch, marks `vectorized = true` on success +per-chunk (partial-batch failures don't block the rest, same tolerance pattern +as `BlobMigrationJob`'s per-group try/catch). + +### 9. `findUnvectorized()` on `ChunkMapper` + +Mirrors the existing `findUnindexed()` exactly (same shape: `?int $limit, +?int $offset`, `orderBy('created_at', 'ASC')` — FIFO processing order is +correct here, unlike the vector-search recency bug in Decision 4, because this +is a work queue, not a relevance ranking) but filters `vectorized = false` +instead of `indexed = false`. + +## Risks / Trade-offs + +- **[Risk] HNSW index build time on large existing installs** → Mitigation: + index creation happens once via `CREATE INDEX` (not `CONCURRENTLY`, to keep + the migration synchronous and match every other index-creation call site in + this codebase — `MagicMapper.php`'s facetable/relation indexes are not + `CONCURRENTLY` either); flagged as an open question below for installs with + very large vector tables where a blocking migration could be undesirable. +- **[Risk] Backfill script cost** → Mitigation: the one-time backfill only + processes rows matching the currently-configured dimension (Decision 2), + batches via the existing `ChunkVectorizationJob`-adjacent batching pattern, + and is idempotent (skips rows where `embedding_vector IS NOT NULL`). +- **[Risk] `to_tsvector('simple', ...)` loses stemming quality vs a + language-aware config** → Mitigation: documented as an explicit trade-off in + Decision 7 and an open question; `'simple'` is the safe default given mixed + Dutch/English content, not a mistake to silently carry forward. +- **[Risk] Removing the `created_at DESC` fallback ordering is a documented + BREAKING change** (proposal.md) → Mitigation: no known external caller of + `VectorSearchHandler::fetchVectors()` outside `VectorSearchHandler` itself + (verified: it's `private`); the observable behavior change is scoped to + `semanticSearch()`/`hybridSearch()` result ordering on the fallback path only. +- **[Risk] `CREATE EXTENSION vector` / `CREATE EXTENSION pg_trgm` requires + appropriate Postgres privileges** → Mitigation: same precondition + `SettingsController` already assumes when it reports "Install pgvector + extension: CREATE EXTENSION vector;" as admin guidance; migration wraps the + statement in a try/catch and logs+skips (matching `MagicMapper`'s existing + `hasPgTrgmExtension()` tolerant-check pattern) rather than failing the whole + migration if the extension can't be created. + +## Migration Plan + +1. Migration A: `openregister_vectors` — `CREATE EXTENSION IF NOT EXISTS + vector` (Postgres only, try/catch+skip), add `embedding_vector + vector({configured_dimension})`, `CREATE INDEX … USING hnsw + (embedding_vector vector_cosine_ops)`. +2. Migration B: `openregister_chunks` — add `text_search tsvector GENERATED + ALWAYS AS (to_tsvector('simple', text_content)) STORED`, `CREATE INDEX … + USING GIN (text_search)` (Postgres only). +3. Deploy `VectorStorageHandler::storeVector()` dual-write + rewritten + `VectorSearchHandler` + `ChunkMapper::searchByKeyword()` + + `FileSearchController` fixes + `ChunkVectorizationJob` together (they are + one coherent code surface — splitting them would leave the new column + unused or the old bugs half-fixed). +4. One-time backfill (either as `postSchemaChange` batch loop bounded by a + row-count safety cap, or as the first few runs of `ChunkVectorizationJob` + naturally catching up given `vectorized` chunks with a stale/absent + `embedding_vector` — implementer's choice, captured as an open question). +5. Rollback: both migrations are additive-only (new nullable column + new + index); rolling back the code to the pre-change version leaves the new + column/index unused but harmless — no destructive rollback migration is + required. + +## Open Questions + +- ~~Backfill placement~~ **DECIDED 2026-07-06 (Ruben): job-only warm-up.** The + migration adds the column/index ONLY; ALL BLOB→pgvector conversion happens + through `ChunkVectorizationJob` warm-up iterations (Decision 8's per-batch + design, selecting rows where `embedding_vector IS NULL`). Zero upgrade-time + impact; capability converges over the first job runs. The task-6.1 + diagnostics panel is the operator's convergence view. +- Should `CREATE INDEX … USING hnsw` use `CONCURRENTLY`? Doctrine + migrations run inside Nextcloud's migration transaction management, and + `CREATE INDEX CONCURRENTLY` cannot run inside a transaction. Provisional + decision: non-concurrent, consistent with every other index-creation call + site in this codebase; document in the migration's admin-facing output that + very large existing vector tables may see a longer `occ upgrade` on first + run. Flagged as `DEFERRED_QUESTIONS`. +- Per-chunk `language`/`language_level`-aware `tsvector` config instead of a + single `'simple'` config for all chunks — deferred as a follow-up, not + blocking this change (documented in Decision 7). diff --git a/openspec/changes/hybrid-document-search/proposal.md b/openspec/changes/hybrid-document-search/proposal.md new file mode 100644 index 0000000000..bac3d42f4f --- /dev/null +++ b/openspec/changes/hybrid-document-search/proposal.md @@ -0,0 +1,113 @@ +--- +kind: code +depends_on: [] +chain: + - hybrid-document-search + - searchable-property-index +--- + +## Why + +File search in OpenRegister is broken in three concrete, file-verified ways +(spectr re-platform FTS deep-dive, 2026-07-05): `VectorSearchHandler::semanticSearch` +fetches at most 500 vectors ordered `created_at DESC` and scores them with a PHP +cosine loop — past 500 chunks, "semantic search" silently degrades into "search the +newest 500 chunks," not the most relevant ones. The hybrid-search keyword arm is +never populated by any caller (`FileSearchController::hybridSearch` hard-codes +`keywordResults: []`), so Reciprocal Rank Fusion always degenerates to pure-vector +ranking regardless of the caller's requested weights, and the endpoint's `total` +field counts the wrong array (the outer response shape, not the results list). +`FileSearchController::semanticSearch` also passes `filters: ['entityType' => 'file']` +into a handler that reads `$filters['entity_type']` — the file-only scope is silently +ignored, so semantic file search actually searches every vectorized entity type. + +Chunk extraction (`openregister_chunks`) already runs automatically after file +upload, but vectorization is a manual batch action with no auto-trigger — chunks +accumulate unvectorized until an admin remembers to run it, so search quality +silently decays over time on every install. + +Solr was removed from OpenRegister (`SearchBackendHandler` is database-only); the +2026-07-05 scale spike proved the concrete fix for the ranking side is available +today: a `pg_trgm` GIN index cut a rare-term ILIKE seq-scan from 268ms to 1.5ms +(~180x). The same category of fix — an actual index instead of an unindexed +in-application scan — applies to vector similarity (pgvector + HNSW instead of a +PHP loop over up to 500 unserialized BLOBs) and to keyword ranking over chunk text +(`tsvector`/`ts_rank` instead of no ranked keyword path at all). + +## What Changes + +- Add a `pgvector` column + HNSW ANN index to `openregister_vectors` (PostgreSQL + only; the existing serialized-BLOB `embedding` column stays as the MariaDB/no- + extension fallback storage — this is additive, not a replacement). +- Dual-write the new column from `VectorStorageHandler::storeVector()` going + forward (Postgres + matching configured embedding dimension only), plus a + one-time backfill pass over existing BLOB rows for the currently-configured + model/dimension. +- Rewrite `VectorSearchHandler::semanticSearch()` to run a SQL `ORDER BY + embedding_vector <=> :query LIMIT :n` KNN query on Postgres when the pgvector + column is populated for the request's filters; keep the existing PHP + cosine-similarity loop as the fallback for MariaDB/SQLite or rows without a + populated pgvector value. Remove the recency-biased `created_at DESC` ordering + from the fallback path's candidate fetch — it silently substituted "newest" for + "most relevant" and is not needed once the primary path is index-backed. + **BREAKING**: `VectorSearchHandler::fetchVectors()`'s default `max_vectors` cap + and ordering change; any caller relying on the old recency bias must switch to + the new relevance-ranked result set. +- Add a ranked keyword-search method to `ChunkMapper` over `openregister_chunks + .text_content` using a `tsvector` generated column + GIN index and `ts_rank` + (PostgreSQL only; returns `[]` with a logged warning on other platforms — no + ranked keyword arm existed before this change on any platform, so this is + additive). +- Wire `FileSearchController::hybridSearch()` to call the new keyword-search + method and pass real results into `VectorizationService::hybridSearch()`'s RRF + fusion, replacing the hard-coded `keywordResults: []`. +- Fix `FileSearchController::semanticSearch()`'s `entityType` → `entity_type` + filter-key mismatch so file-scoped search actually scopes to files. +- Fix `FileSearchController::hybridSearch()`'s `total` (it currently counts the + outer response array's keys, not the result list) and flatten the endpoint's + JSON shape to `{results, total, ...}` matching what a search-page UI consumes + directly, instead of nesting the inner service response under `results`. +- Add a recurring background job that vectorizes chunks where `vectorized = + false` in batches (via the existing `generateBatchEmbeddings()` path), so new + extracted text is searchable without a manual admin action. + +## Capabilities + +### New Capabilities +- `file-search`: The `FileSearchController` HTTP contract for semantic and + hybrid search scoped to file chunks — request/response shape, entity-type + scoping, and the background auto-vectorization job that keeps the index fresh. + +### Modified Capabilities +- `vector-embeddings`: `VectorSearchHandler::semanticSearch()` gains a + Postgres/pgvector KNN path (PHP-cosine becomes the fallback, not the only + path); `hybridSearch()`'s keyword arm becomes real (ranked `tsvector`/`ts_rank` + results) instead of always-empty; the 500-vector recency-biased cap is removed + from the primary path. + +## Impact + +- **Affected code**: `lib/Db/ChunkMapper.php`, `lib/Db/Chunk.php` (no new + columns needed — `vectorized` already exists), `lib/Service/Vectorization + /Handlers/VectorSearchHandler.php`, `lib/Service/Vectorization/Handlers + /VectorStorageHandler.php`, `lib/Controller/FileSearchController.php`, a new + `lib/BackgroundJob/ChunkVectorizationJob.php`, two new migrations + (`openregister_vectors` pgvector column + index, `openregister_chunks` + tsvector column + index). +- **Database**: additive schema changes only; MariaDB/SQLite installs are + unaffected (new columns/indexes are Postgres-only, guarded the same way the + existing retention-JSON GIN index migration guards its Postgres path). +- **Dependent apps**: none directly consume `FileSearchController` today + (verified: no cross-app callers found); the `vector-embeddings` capability is + consumed internally by OpenRegister's own object/file vectorization strategies, + which are unaffected by the ranking-path change (only the ranking mechanics + change, not the public method signatures). +- **Chain**: this is spec 1 of 2. Spec 2, `searchable-property-index` (schema + `searchable` property flag → `pg_trgm` GIN index on OpenRegister object magic + tables, mirroring the existing `facetable` → btree pattern), is a distinct + subsystem (object-property search, not file/chunk search) split out to keep + each spec's reviewer surface and task count tight per ADR-032. No functional + dependency exists between them; `searchable-property-index` declares + `depends_on: [hybrid-document-search]` purely for sequencing discipline within + the same initiative (see that spec's proposal for the rationale, flagged as a + deferred question for confirmation). diff --git a/openspec/changes/hybrid-document-search/specs/file-search/spec.md b/openspec/changes/hybrid-document-search/specs/file-search/spec.md new file mode 100644 index 0000000000..4d033173ca --- /dev/null +++ b/openspec/changes/hybrid-document-search/specs/file-search/spec.md @@ -0,0 +1,73 @@ +## ADDED Requirements + +### Requirement: FileSearchController MUST scope semantic search to file entities and MUST NOT silently ignore the scope filter + +`FileSearchController::semanticSearch()` (route `POST /api/search/files/semantic`) MUST pass `filters: ['entity_type' => 'file']` to `VectorizationService::semanticSearch()` — using the `entity_type` (snake_case) key that `VectorSearchHandler::fetchVectors()` actually reads. A request MUST NOT return results from non-file entity types (e.g. `object`) when this endpoint is called. + +#### Scenario: Semantic file search excludes object-type vectors +- **GIVEN** `openregister_vectors` holds vectors for both `entity_type = 'object'` and `entity_type = 'file'`, and the query text is more similar to a stored `object` vector than to any `file` vector +- **WHEN** `POST /api/search/files/semantic` is called with that query +- **THEN** the `filters` passed to `VectorSearchHandler::fetchVectors()` MUST include `entity_type => 'file'` +- **AND** the returned `results` MUST only contain entries with `entity_type = 'file'` + +#### Scenario: Missing query parameter returns 400 +- **GIVEN** a request to `POST /api/search/files/semantic` with no `query` parameter +- **WHEN** the controller handles the request +- **THEN** the response MUST be a 400 JSON response `{success: false, message: 'Query parameter is required'}` + +### Requirement: FileSearchController MUST return a correctly-shaped, correctly-counted hybrid search response + +`FileSearchController::hybridSearch()` (route `POST /api/search/files/hybrid`) MUST call `ChunkMapper::searchByKeyword()` (scoped to `source_type = 'file'`) to obtain ranked keyword results, then call `VectorizationService::hybridSearch(query, keywordResults: , limit, weights)`. The controller MUST destructure the service's response and return a flat JSON body: `{success: true, query, results: , total: , search_time_ms, source_breakdown, weights, search_type: 'hybrid'}`. The endpoint MUST NOT nest the service's full response object inside the `results` key, and `total` MUST NOT be computed from `count()` over the service's outer response array. + +#### Scenario: Hybrid search response total matches the actual result count +- **GIVEN** a hybrid search query that produces 7 fused results +- **WHEN** `POST /api/search/files/hybrid` is called +- **THEN** the response `total` MUST equal `7` +- **AND** the response `results` MUST be a flat array of 7 result entries (not a nested object containing `results`/`total`/`weights`/etc.) + +#### Scenario: Hybrid search keyword arm is populated from real chunk text matches +- **GIVEN** `openregister_chunks` contains a file chunk whose `text_content` matches the query term but whose embedding is not the top vector match +- **WHEN** `POST /api/search/files/hybrid` is called with `keyword_weight` > 0 +- **THEN** that chunk's entity MUST appear in the fused results (contributed via the keyword arm) +- **AND** the response `source_breakdown` MUST report it under `keyword_only` or `both`, never silently dropped + +#### Scenario: Missing query parameter returns 400 +- **GIVEN** a request to `POST /api/search/files/hybrid` with no `query` parameter +- **WHEN** the controller handles the request +- **THEN** the response MUST be a 400 JSON response `{success: false, message: 'Query parameter is required'}` + +### Requirement: The system MUST provide a ranked keyword-search path over chunk text, backed by a Postgres GIN index + +`ChunkMapper::searchByKeyword(string $query, int $limit, array $filters = []): array` MUST, on PostgreSQL, query `WHERE to_tsvector('simple', text_content) @@ plainto_tsquery('simple', :query)`, ranked by `ts_rank(to_tsvector('simple', text_content), plainto_tsquery('simple', :query)) DESC`, honouring an optional `source_type` filter, and limited to `$limit` rows — the expression form matching the functional GIN index `idx_or_chunks_text_search_gin` (a STORED `tsvector` generated column is not viable: an unknown column type on a prefix-matched table breaks Doctrine's schema introspection for every subsequent Nextcloud migration — implementation amendment 2026-07-06, see design.md). Each returned row MUST be shaped as `['entity_type' => 'file', 'entity_id' => , 'score' => , 'chunk_text' => , 'chunk_index', 'metadata' => []]` so it is directly consumable by `VectorSearchHandler::reciprocalRankFusion()`'s `$keywordResults` parameter. On any non-PostgreSQL platform, or when the query fails, the method MUST return `[]` and log a warning rather than throw. + +#### Scenario: Keyword search ranks by ts_rank relevance +- **GIVEN** PostgreSQL is the active platform and three chunks contain the query term with differing term frequency/density +- **WHEN** `ChunkMapper::searchByKeyword('quarterly report', 10)` is invoked +- **THEN** the three matching chunks MUST be returned ordered by `ts_rank` descending +- **AND** each entry MUST carry `entity_type: 'file'`, `entity_id`, `score`, `chunk_text`, `chunk_index` + +#### Scenario: Non-PostgreSQL platform returns empty result, not an error +- **GIVEN** the active platform is MariaDB +- **WHEN** `ChunkMapper::searchByKeyword('quarterly report', 10)` is invoked +- **THEN** the call MUST return `[]` +- **AND** a warning MUST be logged noting the keyword-search path is unavailable on this platform +- **AND** no exception MUST propagate to the caller + +### Requirement: Chunks MUST be automatically vectorized in the background as they are extracted + +A recurring background job MUST scan `openregister_chunks` WHERE `vectorized = false` (via `ChunkMapper::findUnvectorized()`) in bounded batches, generate embeddings via `VectorizationService`'s existing batch-embedding path, store them via `VectorStorageHandler::storeVector()`, and mark each successfully-processed chunk `vectorized = true`. A per-chunk failure MUST NOT abort the batch — the job MUST continue processing the remaining chunks in that run and pick up the failed chunk again on its next scheduled run. + +#### Scenario: Unvectorized chunks become searchable without manual action +- **GIVEN** a file was uploaded and text-extracted (its chunks exist in `openregister_chunks` with `vectorized = false`) +- **AND** no admin has manually triggered batch vectorization +- **WHEN** the background job's next scheduled run completes +- **THEN** those chunks MUST have `vectorized = true` +- **AND** corresponding rows MUST exist in `openregister_vectors` +- **AND** a subsequent semantic search MUST be able to match content from that file + +#### Scenario: A single failed embedding does not block the rest of the batch +- **GIVEN** a batch of 50 unvectorized chunks where the embedding provider call fails for one chunk +- **WHEN** the background job processes the batch +- **THEN** the 49 other chunks MUST be successfully vectorized and marked `vectorized = true` +- **AND** the failing chunk MUST remain `vectorized = false` for retry on the next scheduled run +- **AND** the failure MUST be logged, not thrown diff --git a/openspec/changes/hybrid-document-search/specs/vector-embeddings/spec.md b/openspec/changes/hybrid-document-search/specs/vector-embeddings/spec.md new file mode 100644 index 0000000000..81e19543b0 --- /dev/null +++ b/openspec/changes/hybrid-document-search/specs/vector-embeddings/spec.md @@ -0,0 +1,59 @@ +## MODIFIED Requirements + +### Requirement: The system MUST execute vector queries via semantic KNN/cosine similarity and hybrid Reciprocal-Rank-Fusion search + +`VectorEmbeddings::semanticSearch(string $query, int $limit = 10, array $filters = [], ?string $provider = null)` MUST first generate a query embedding for `$query` via `generateEmbedding()` (vector-embeddings REQ-001) and delegate to `VectorSearchHandler::semanticSearch(array $queryEmbedding, int $limit, array $filters)`. PostgreSQL is the sole vector storage/search backend (Solr was removed — `SearchBackendHandler` is database-only); there is no `$backend` parameter or Solr routing. + +`VectorSearchHandler::semanticSearch` MUST prefer a SQL K-nearest-neighbour path on PostgreSQL: when the pgvector ANN fast path is populated for the filtered candidate set — implemented as the unprefixed sidecar table `openregister_vec_ann` (`vector_id -> embedding vector(N)`, cascade-deleted with the main row) rather than an in-table `embedding_vector` column, because a `vector`-typed column on a prefix-matched table breaks Doctrine's schema introspection for every subsequent Nextcloud migration (implementation amendment 2026-07-06, see design.md) — the call MUST execute `ORDER BY embedding <=> :queryVector LIMIT :limit` (cosine-distance operator, ascending = most similar first) against the HNSW index on the sidecar, joined to `openregister_vectors` for entity data, honouring `$filters['entity_type']` / `$filters['entity_id']` as WHERE predicates. When the fast path is unavailable (non-Postgres platform, extension not installed, sidecar absent, or the row's stored dimension doesn't match the currently-configured embedding dimension), the call MUST fall back to fetching candidate vectors from `openregister_vectors` (honouring the `$filters` predicate, ordered by `id ASC` — not `created_at DESC` — with a bounded `max_vectors` safety cap), `unserialize()` each stored embedding BLOB, compute `cosineSimilarity()` against the query embedding, sort by similarity descending, and return the top `$limit`. A vector row whose deserialised embedding is not an array MUST be skipped (not fatal) on the fallback path. When neither path finds any candidates the call MUST return `[]`. Any thrown error MUST be re-wrapped as `Semantic search failed: {message}`. + +Each result entry MUST carry `vector_id`, `entity_type`, `entity_id`, `similarity`, `chunk_index`, `total_chunks`, `chunk_text`, `metadata` (JSON-decoded from the row, `[]` when absent), `model`, and `dimensions`, regardless of which path (KNN or PHP-fallback) produced it. + +`VectorEmbeddings::hybridSearch(string $query, array $keywordFilters = [], int $limit = 20, array $weights = ['keyword' => 0.5, 'vector' => 0.5], ?string $provider = null)` MUST generate the query embedding and delegate to `VectorSearchHandler::hybridSearch(array $queryEmbedding, array $keywordResults, int $limit, array $weights)`, where `$keywordResults` MUST be the caller-supplied, already-executed keyword search results (e.g. `ChunkMapper::searchByKeyword()` output) — the handler itself does not execute keyword search; it only fuses results. + +`VectorSearchHandler::hybridSearch` MUST normalise the supplied `keyword` / `vector` weights to sum to 1 (when their sum is `> 0`), run the vector leg via `semanticSearch` with `limit * 2` candidates ONLY when `vectorWeight > 0`, fuse the vector and keyword result sets via Reciprocal Rank Fusion (`reciprocalRankFusion()`), and return the top `$limit` fused results. A failure in the vector leg MUST be logged and tolerated (the keyword leg still contributes) — it MUST NOT abort hybrid search. The response MUST include `results`, `total` (the count of the fused result list, not of the response object's own keys), `search_time_ms`, a `source_breakdown` of `vector_only` / `keyword_only` / `both` counts, and the normalised `weights`. + +#### Scenario: PostgreSQL KNN search ranks by pgvector cosine distance and is index-backed +- **GIVEN** the platform is PostgreSQL, the `pgvector` extension is installed, and `openregister_vectors` holds 5,000 rows with `openregister_vec_ann` sidecar rows populated at the currently-configured dimension +- **WHEN** `VectorEmbeddings::semanticSearch('find me', 10)` runs +- **THEN** the query MUST first be embedded via `generateEmbedding('find me')` +- **AND** the search MUST execute `ORDER BY embedding <=> :queryVector LIMIT 10` against the sidecar's HNSW index, not a PHP loop over all 5,000 rows +- **AND** the result MUST be the 10 nearest entries, each carrying `vector_id`, `entity_type`, `entity_id`, `similarity`, `chunk_text`, `model`, and `dimensions` + +#### Scenario: Fallback PHP-cosine search ranks by similarity, not recency, and caps at limit +- **GIVEN** the platform is MariaDB (no pgvector fast path exists) and `openregister_vectors` holds 50 candidate rows for the filter +- **WHEN** `VectorEmbeddings::semanticSearch('find me', 10)` runs +- **THEN** candidate rows MUST be fetched ordered by `id ASC` (not `created_at DESC`) +- **AND** cosine similarity MUST be computed against each deserialised stored embedding +- **AND** the result MUST be the 10 highest-similarity entries sorted descending, each carrying `vector_id`, `entity_type`, `entity_id`, `similarity`, `chunk_text`, `model`, and `dimensions` + +#### Scenario: Unparseable stored embedding is skipped, not fatal +- **GIVEN** one of the fetched vector rows on the PHP-fallback path deserialises to a non-array value +- **WHEN** the fallback semantic-search loop processes it +- **THEN** that row MUST be skipped with a warning log +- **AND** the remaining rows MUST still be scored and returned + +#### Scenario: No candidates returns empty result on either path +- **GIVEN** `openregister_vectors` has no rows matching `$filters` (neither `embedding_vector`-populated nor BLOB-only) +- **WHEN** `VectorSearchHandler::semanticSearch(...)` runs +- **THEN** the call MUST return `[]` +- **AND** no cosine-similarity computation or KNN query MUST be attempted + +#### Scenario: Hybrid search fuses real keyword results, not a hard-coded empty array +- **GIVEN** `$keywordResults` is a non-empty, already-ranked array (e.g. from `ChunkMapper::searchByKeyword()`) and `weights = ['keyword' => 0.5, 'vector' => 0.5]` +- **WHEN** `VectorSearchHandler::hybridSearch(queryEmbedding, $keywordResults, ...)` runs +- **THEN** both the vector and keyword result sets MUST be fused via Reciprocal Rank Fusion +- **AND** the response's `source_breakdown` MUST report non-zero `keyword_only` and/or `both` counts when keyword-matched entities exist +- **AND** `total` MUST equal `count($finalResults)` — the fused result list length, not the response object's own key count + +#### Scenario: Hybrid search tolerates a failing vector leg +- **GIVEN** `weights = ['keyword' => 0.5, 'vector' => 0.5]` and the vector `semanticSearch` call throws +- **WHEN** `VectorSearchHandler::hybridSearch(...)` runs +- **THEN** the failure MUST be logged and swallowed (no exception propagates) +- **AND** the keyword results MUST still be fused and returned via Reciprocal Rank Fusion +- **AND** the response MUST include `results`, `total`, `search_time_ms`, `source_breakdown`, and the normalised `weights` + +#### Scenario: Hybrid weights are normalised to sum to one +- **GIVEN** `weights = ['keyword' => 3, 'vector' => 1]` +- **WHEN** `hybridSearch(...)` normalises them +- **THEN** the effective weights MUST be `['keyword' => 0.75, 'vector' => 0.25]` +- **AND** the returned `weights` field MUST reflect the normalised values diff --git a/openspec/changes/hybrid-document-search/tasks.md b/openspec/changes/hybrid-document-search/tasks.md new file mode 100644 index 0000000000..64f3f5a671 --- /dev/null +++ b/openspec/changes/hybrid-document-search/tasks.md @@ -0,0 +1,39 @@ +## 1. Database migrations + +- [x] 1.1 Add a migration for `openregister_vectors`: `CREATE EXTENSION IF NOT EXISTS vector` (try/catch + skip on failure), add nullable `embedding_vector vector({configured_dimension})` column, `CREATE INDEX ... USING hnsw (embedding_vector vector_cosine_ops)`. Guard the whole migration behind a PostgreSQL platform check (mirror `Version1Date20260322120000`'s `str_contains(get_class($platform), 'PostgreSQL')` pattern) and no-op with a log line on other platforms. *(AMENDED 2026-07-06, live-verified: an in-table `vector`-typed column breaks Doctrine introspectSchema() for every subsequent NC migration — shipped as the unprefixed `openregister_vec_ann` sidecar table + HNSW index with cascade FK; identical guard/extension/dimension semantics. See design.md amendment.)* +- [x] 1.2 Add a migration for `openregister_chunks`: add `text_search tsvector GENERATED ALWAYS AS (to_tsvector('simple', text_content)) STORED`, `CREATE INDEX ... USING GIN (text_search)`. Same PostgreSQL-only guard as 1.1. *(AMENDED 2026-07-06: a `tsvector` column has the same Doctrine-introspection problem — shipped as a functional GIN index `USING gin (to_tsvector('simple', text_content))`, zero schema-visible changes, same 'simple' config. See design.md amendment.)* + +## 2. Vector storage & backfill + +- [x] 2.1 Update `VectorStorageHandler::storeVector()` to also write `embedding_vector` (Postgres + dimension-matched rows only) alongside the existing BLOB write — additive, does not replace or alter the BLOB path. *(AMENDED: dual-write is an idempotent upsert into the `openregister_vec_ann` sidecar instead of an in-table column — same conditions, same tolerance.)* +- [x] 2.2 Backfill via job-only warm-up (DECIDED 2026-07-06): the migration adds column/index only; extend the task-5.2 background job's selection to rows where `embedding_vector IS NULL` (idempotent, dimension-matched) so existing BLOB rows convert over warm-up runs with zero upgrade-time impact. *(AMENDED: "embedding_vector IS NULL" is implemented as "no sidecar row" — `LEFT JOIN openregister_vec_ann ... WHERE a.vector_id IS NULL`; identical idempotence and zero-upgrade-impact semantics.)* + +## 3. Semantic & hybrid search rewrite + +- [x] 3.1 Rewrite `VectorSearchHandler::semanticSearch()`: on PostgreSQL with `embedding_vector` populated for the filtered candidate set, run `ORDER BY embedding_vector <=> :queryVector LIMIT :limit` (index-backed KNN); otherwise fall back to the existing `fetchVectors()` + PHP `cosineSimilarity()` loop unchanged in shape. +- [x] 3.2 Update `VectorSearchHandler::fetchVectors()`'s fallback path: replace `orderBy('created_at', 'DESC')` with `orderBy('id', 'ASC')`; keep the `max_vectors` safety cap (documented as an approximate-fallback bound, not a relevance mechanism). +- [x] 3.3 Add `ChunkMapper::searchByKeyword(string $query, int $limit, array $filters = []): array` — `ts_rank`-ranked keyword search over the `text_search` column on PostgreSQL (optional `source_type` filter), returning rows shaped for `reciprocalRankFusion()`'s `$keywordResults` input; return `[]` with a logged warning on non-PostgreSQL platforms. +- [x] 3.4 Rename `VectorSearchHandler::hybridSearch()` / `VectorEmbeddings::hybridSearch()` parameters from `solrResults`/`solr` weight key to `keywordResults`/`keyword` (the Solr backend and its routing no longer exist in this code; align naming with actual behaviour) and update the `source_breakdown` keys from `solr_only` to `keyword_only`. *(Ground truth 2026-07-06: the rename had already landed on development — parameters, weight keys and `keyword_only` breakdown verified in place; this change removed the last stale "Solr" docblock mentions in VectorStorageHandler/VectorStatsHandler/FileSearchController.)* + +## 4. FileSearchController fixes + +- [x] 4.1 Fix `FileSearchController::semanticSearch()`: change the filter key from `entityType` to `entity_type` so file-scoped search actually scopes to `entity_type = 'file'`. +- [x] 4.2 Fix `FileSearchController::hybridSearch()`: call `ChunkMapper::searchByKeyword()` (scoped to `source_type = 'file'`) and pass its output as `keywordResults` into `VectorizationService::hybridSearch()`; destructure the service response into a flat `{success, query, results, total, search_time_ms, source_breakdown, weights, search_type}` body instead of nesting the whole service response under `results` with a wrong `total`. + +## 5. Auto-vectorization background job + +- [x] 5.1 Add `ChunkMapper::findUnvectorized(?int $limit = null, ?int $offset = null): array` mirroring the existing `findUnindexed()` shape, filtering `vectorized = false`, ordered `created_at ASC` (FIFO work queue). +- [x] 5.2 Add `ChunkVectorizationJob extends TimedJob` (5-minute interval, batch size 50-100, mirroring `BlobMigrationJob`'s shape): read `ChunkMapper::findUnvectorized()`, generate embeddings via the existing `generateBatchEmbeddings()` path, store via `VectorStorageHandler::storeVector()`, mark each successfully-processed chunk `vectorized = true`. A single chunk's embedding failure must not abort the batch (log + continue; retried on the next scheduled run). +- [x] 5.3 Register `ChunkVectorizationJob` in the app's background-job registration (verify and follow the existing registration call site used for `BlobMigrationJob`/`FileTextExtractionJob`). + +## 6. Diagnostics + +- [x] 6.1 Extend `SettingsController`'s existing database-info diagnostics (the panel that already reports pgvector-extension presence) to also report whether `embedding_vector`/`text_search` columns and their indexes exist, and the current backfill/vectorization progress (`ChunkMapper::countVectorized()` vs `countAll()`). + +## 7. Tests + +- [x] 7.1 Unit tests for `VectorSearchHandler`: KNN-path query construction and result shape on Postgres with `embedding_vector` populated; PHP-fallback path preserves existing scoring behaviour with `id ASC` ordering (no recency bias); filters (`entity_type`, `entity_id`) applied correctly on both paths; empty-candidate-set returns `[]` without computation. +- [x] 7.2 Unit tests for `ChunkMapper::searchByKeyword()`: ranked-by-`ts_rank` results on Postgres; `[]` + logged warning on a non-Postgres platform; `source_type` filter honoured. +- [x] 7.3 Unit/functional tests for `FileSearchController`: `semanticSearch()` passes `entity_type => 'file'` (regression test for the fixed bug); `hybridSearch()` response `total` matches the actual fused-result count and `results` is flat, not nested; both endpoints return 400 on a missing `query` parameter. +- [x] 7.4 Unit tests for `ChunkVectorizationJob`: batch processing marks `vectorized = true` on success; a single chunk's embedding failure doesn't abort the batch and leaves that chunk `vectorized = false` for retry. +- [x] 7.5 Migration tests: verify the pgvector column/HNSW index and the tsvector column/GIN index are created on a PostgreSQL test database and gracefully skipped (no error, informative log) on a non-PostgreSQL test database. diff --git a/openspec/changes/i18n-api-language-negotiation/tasks.md b/openspec/changes/i18n-api-language-negotiation/tasks.md deleted file mode 100644 index ee13df80fe..0000000000 --- a/openspec/changes/i18n-api-language-negotiation/tasks.md +++ /dev/null @@ -1,78 +0,0 @@ -# Tasks: i18n API Language Negotiation - -## Phase 1 — Request-side query parameters - -- [ ] Update `lib/Middleware/LanguageMiddleware::beforeController` to - check for `?_lang=` and `?language=` query parameters before - the `Accept-Language` header. Resolution order: - 1. `?_lang=` (canonical) - 2. `?language=` (alias) - 3. `Accept-Language` (existing) - 4. Default (existing — register first language, or `nl`) -- [ ] Add a `LanguageService::setRequestedLanguageSource(string - $source)` enum-style flag (`'query' | 'header' | 'default'`) - so consumers can introspect WHY a particular language was - chosen. Used by the `X-Source-Language` setter in the companion - change for response diagnostics. -- [ ] Validate that the resolved language is a syntactically valid - BCP-47 tag (basic regex). Invalid input MUST log a warning AND - fall through to the next priority level (do NOT 400 — clients - that mistype a tag should still get a usable response). - -## Phase 2 — Write-side target-language header - -- [ ] Update `lib/Middleware/LanguageMiddleware::beforeController` to - read `X-Translation-Target-Language` request header on POST / - PUT / PATCH. Stash on `LanguageService::setTargetLanguage()`. -- [ ] Update `lib/Service/Object/TranslationHandler::normalizeTranslationsForSave` - to consume `LanguageService::getTargetLanguage()`: - - If header is set AND the body has a scalar (non-language-keyed) - translatable property → wrap under the target language. - - If header is set AND the body has a full language-keyed object - for that property → reject with `400 Bad Request` (conflict). - - If header is unset → preserve current behaviour (wrap under - register default). -- [ ] Add `lib/Exception/TranslationTargetConflictException.php` for - the conflict case; controller catches and returns `400` with a - structured error body. - -## Phase 3 — Spec + tests - -- [ ] Write `specs/i18n-api-language-negotiation/spec.md` with one - Requirement per public surface (query precedence, header - precedence, write-side header, conflict rejection, BCP-47 - validation). -- [ ] Update `openregister/openspec/specs/register-i18n/spec.md` to - reflect the new request-side contract; cross-reference this - change. -- [ ] Add `tests/Unit/Middleware/LanguageMiddlewareTest.php` covering: - `?_lang=` overrides Accept-Language, `?language=` alias, - conflict (both set) → query wins, invalid `?_lang=` falls - through to next priority, default fallback when nothing set. -- [ ] Add `tests/Unit/Service/Object/TranslationHandlerTargetLanguageTest.php` - covering: scalar body + header → wrap under target, full - language object + header → 400 conflict, scalar body without - header → wrap under register default (legacy). -- [ ] Add Newman collection - `tests/integration/openregister-i18n-api-language-negotiation.postman_collection.json` - hitting: GET with `?_lang=`, GET with `?language=`, GET with - both query + Accept-Language (query wins), POST with - `X-Translation-Target-Language`, conflict POST returning 400. -- [ ] Wire the Newman collection into - `tests/newman/run-all.sh::DOMAIN_ORDER` after the - `i18n-source-of-truth` collection (ordering dependency on the - companion change shipping first; gracefully skips if companion - hasn't shipped). - -## Phase 4 — Documentation - -- [ ] Update `docs/i18n.md` with the request-side contract, the - header table, and the request-precedence ordering. -- [ ] Add a `docs/i18n.md#client-snippets` section showing canonical - curl + axios + Postman invocations for: read in a language, - write to a target language, edit a translation without - touching the source. -- [ ] Cross-reference this change from - `hydra/openspec/architecture/adr-025-i18n-source-of-truth.md`'s - "Implementation reference" section once shipped (alongside the - companion). diff --git a/openspec/changes/i18n-source-of-truth/specs/i18n-source-of-truth/spec.md b/openspec/changes/i18n-source-of-truth/specs/i18n-source-of-truth/spec.md deleted file mode 100644 index 73c0268f93..0000000000 --- a/openspec/changes/i18n-source-of-truth/specs/i18n-source-of-truth/spec.md +++ /dev/null @@ -1,152 +0,0 @@ -i18n-source-of-truth ---- -status: draft ---- -# i18n Source of Truth - -## Purpose - -Implement the source-of-truth half of hydra ADR-025: schemas declare -which language is canonical for a translatable property, the -`openregister_translations` sidecar table tracks the source-language -of every projected translation, edits to source values automatically -flip derived translations to `outdated`, and render responses -optionally expose source-vs-translation metadata. The companion -change `i18n-api-language-negotiation` covers the request/response -contract. - -## ADDED Requirements - -### Requirement: Schemas MUST be able to declare a `sourceLanguage` per translatable property - -Schemas MUST accept `sourceLanguage: ` on any property where `translatable: true`. The validator MUST reject `sourceLanguage` on properties without `translatable: true`. If a translatable property omits `sourceLanguage`, OR MUST treat the register's `defaultLanguage` (first element of `Register.languages`, falling back to `nl`) as the source language for that property. - -#### Scenario: Schema declares sourceLanguage on a translatable property -- GIVEN a schema body `{"properties": {"title": {"type": "string", "translatable": true, "sourceLanguage": "nl"}}}` -- WHEN the schema is saved -- THEN the persisted schema MUST retain `sourceLanguage: "nl"` on the `title` property - -#### Scenario: Schema rejects sourceLanguage on a non-translatable property -- WHEN a schema body `{"properties": {"code": {"type": "string", "sourceLanguage": "nl"}}}` is POSTed -- THEN OR MUST return `400 Bad Request` -- AND the response body MUST include `error.invalidProperty: 'code'` and `error.reason: 'sourceLanguage requires translatable: true'` - -#### Scenario: Translatable property without sourceLanguage falls back to register default -- GIVEN a register with `defaultLanguage = "nl"` -- AND a schema with `{"title": {"translatable": true}}` (no `sourceLanguage`) -- WHEN an object is created with `title: {nl: "Hallo", en: "Hello"}` -- THEN the projected Translation rows MUST have `source_language = "nl"` for both rows - -### Requirement: Objects MAY override `sourceLanguage` per property via `_translationMeta` - -Objects MAY include `_translationMeta..sourceLanguage = ""` in their body. When present, this overrides the schema default for that single object's property. When absent, the schema/register fallback chain applies. - -#### Scenario: Object overrides sourceLanguage for a single property -- GIVEN a schema declaring `title.sourceLanguage = "nl"` -- WHEN an object is POSTed with `{"title": {"nl": "x", "en": "y"}, "_translationMeta": {"title": {"sourceLanguage": "en"}}}` -- THEN the projected Translation rows MUST have `source_language = "en"` -- AND `_translationMeta.title.sourceLanguage = "en"` MUST be persisted on the object body - -### Requirement: The `openregister_translations` table MUST include a `source_language` column - -The migration MUST add `source_language VARCHAR(16) NOT NULL DEFAULT ''` to `openregister_translations`. A back-fill MUST set every existing row's `source_language` to the register's `default_language` (or `'nl'` when missing). After back-fill, a follow-up migration MUST drop the default value so new rows are forced to set the column explicitly. - -#### Scenario: Existing translation rows are back-filled -- GIVEN `openregister_translations` contains 100 rows from before this change, each with empty `source_language` -- AND every row's parent register has `default_language = "nl"` -- WHEN the migration runs -- THEN all 100 rows MUST have `source_language = "nl"` after the back-fill -- AND `php occ openregister:translations:backfill-source-language` re-run MUST be a no-op (returns "0 rows updated") - -### Requirement: `TranslationProjectionService` MUST populate `source_language` on every projected row - -The projector MUST resolve `source_language` for each property using the lookup chain: `_translationMeta..sourceLanguage` (object) → schema `properties..sourceLanguage` → `Register.defaultLanguage` → `'nl'`. Every projected Translation row MUST have a non-empty `source_language` after the change ships. - -#### Scenario: Projection respects the resolution chain -- GIVEN a register `defaultLanguage = "nl"` -- AND a schema with `title.sourceLanguage = "fr"` -- AND an object body `{"title": {"fr": "Bonjour", "en": "Hello"}, "_translationMeta": {"title": {"sourceLanguage": "en"}}}` -- WHEN `TranslationProjectionService::project()` runs -- THEN both projected rows for `title` MUST have `source_language = "en"` (object-level override wins) - -### Requirement: `TranslationStatusService` MUST flip derived rows to `outdated` on source-value change - -The system MUST expose `TranslationStatusService::markDerivedTranslationsOutdated(string $objectUuid, string $property, string $sourceLanguage): int` that updates every Translation row WHERE `object_uuid = $objectUuid AND property = $property AND language != $sourceLanguage` AND `status IN ('approved', 'human_reviewed', 'machine_translated')` to `status = 'outdated'`. The method MUST return the count of rows updated. A row already in `outdated` or `draft` status MUST NOT be re-flipped. - -#### Scenario: Source value change flips approved derived rows to outdated -- GIVEN an object with `title.sourceLanguage = "nl"` and an approved English translation -- WHEN `markDerivedTranslationsOutdated('obj-uuid', 'title', 'nl')` is called -- THEN the English translation row MUST have `status = "outdated"` -- AND the Dutch source row MUST be unchanged -- AND the method MUST return `1` (one row flipped) - -#### Scenario: Already-outdated rows are not re-flipped -- GIVEN an English translation row already in `status = "outdated"` -- WHEN `markDerivedTranslationsOutdated('obj-uuid', 'title', 'nl')` is called -- THEN the row MUST remain `outdated` -- AND the method MUST NOT include this row in the returned count - -### Requirement: `SaveObject` MUST trigger the outdated flip on source-value change - -When persisting an object update, `SaveObject` MUST detect changes to a translatable property's source-language value (compare old vs new for the resolved source language) and MUST call `markDerivedTranslationsOutdated($uuid, $property, $sourceLang)` for each such change. Edits to non-source-language values MUST NOT trigger the flip. - -#### Scenario: Editing the Dutch source flips English translation to outdated -- GIVEN an object with `title = {"nl": "Hallo", "en": "Hello"}`, `sourceLanguage = "nl"`, English row `status = "approved"` -- WHEN the object is PATCHed with `title.nl = "Welkom"` -- THEN the English translation row MUST have `status = "outdated"` after persist -- AND a `TranslationStatusChanged` event MUST have been emitted - -#### Scenario: Editing the English translation does NOT flip status -- GIVEN the same starting state -- WHEN the object is PATCHed with `title.en = "Welcome"` -- THEN the English row MUST stay `status = "approved"` (or be the row being edited) -- AND no rows MUST have been flipped to outdated - -### Requirement: `TranslationController` MUST expose source-language query filters - -The translation search endpoint MUST accept three new query parameters: `?sourceLanguage=` (filter to rows whose source language matches), `?isOutOfDate=true` (filter to `status = "outdated"`), `?compareToSource=true` (return both source and translation values per matched row, side-by-side). - -#### Scenario: Filter translations by source language -- GIVEN 5 Translation rows: 3 with `source_language = "nl"`, 2 with `source_language = "en"` -- WHEN `GET /api/translations/search?sourceLanguage=nl` is called -- THEN the response MUST contain exactly 3 rows -- AND every row's `source_language` MUST equal `"nl"` - -#### Scenario: Filter translations to outdated only -- GIVEN 10 rows, 3 of which have `status = "outdated"` -- WHEN `GET /api/translations/search?isOutOfDate=true` is called -- THEN the response MUST contain exactly 3 rows -- AND every row's `status` MUST equal `"outdated"` - -#### Scenario: Compare to source returns side-by-side values -- GIVEN an object with `title.nl = "Welkom"` and `title.en = "Hello"` (out of date) -- WHEN `GET /api/translations/search?compareToSource=true&objectUuid=...` is called -- THEN each returned row MUST include both `value` (the row's value, e.g. "Hello") and `sourceValue` (the source-language value, e.g. "Welkom") - -### Requirement: `RenderObject` MUST attach `_meta.languageMeta` when `?_translationMeta=true` is passed - -When the request includes `?_translationMeta=true`, `RenderObject` MUST add `_meta.languageMeta.` to the response body for every translatable property that was rendered. The envelope MUST contain: `served` (the language actually returned), `sourceLanguage` (the canonical source for the property), `isSource` (boolean — true iff `served === sourceLanguage`), `status` (the translation row's status, or `"source"` for the source-language row). - -#### Scenario: Render with translation metadata -- GIVEN an object with `title = {"nl": "Welkom", "en": "Hello"}`, `sourceLanguage = "nl"` -- WHEN `GET /api/objects/{r}/{s}/{id}?_lang=en&_translationMeta=true` is called -- THEN the response body MUST include `_meta.languageMeta.title = {served: "en", sourceLanguage: "nl", isSource: false, status: "approved"}` - -#### Scenario: Render without `_translationMeta` omits the envelope -- GIVEN the same object -- WHEN `GET /api/objects/{r}/{s}/{id}?_lang=en` is called (no `_translationMeta`) -- THEN the response body MUST NOT contain `_meta.languageMeta` - -### Requirement: Responses MUST set `X-Source-Language` header - -For any endpoint returning object content, OR MUST set the `X-Source-Language` response header to the canonical source language for the object's primary translatable property (or, when an object has multiple translatable properties with different source languages, the most common one across the object). The header is informational; clients use it for at-a-glance "is this the original?" detection. - -#### Scenario: Single-source object exposes source language -- GIVEN an object whose translatable properties all declare `sourceLanguage = "nl"` -- WHEN any endpoint returning that object is called -- THEN the response MUST include header `X-Source-Language: nl` - -#### Scenario: Mixed-source object exposes the dominant language -- GIVEN an object with 2 translatable properties: `title.sourceLanguage = "nl"`, `description.sourceLanguage = "nl"`, `notes.sourceLanguage = "en"` -- WHEN any endpoint returning that object is called -- THEN the response MUST include header `X-Source-Language: nl` (2 of 3) diff --git a/openspec/changes/i18n-source-of-truth/tasks.md b/openspec/changes/i18n-source-of-truth/tasks.md deleted file mode 100644 index b04893c2af..0000000000 --- a/openspec/changes/i18n-source-of-truth/tasks.md +++ /dev/null @@ -1,82 +0,0 @@ -# Tasks: i18n Source of Truth - -## Phase 1 — Schema + entity + migration - -- [ ] Add `sourceLanguage: string` to the JSON-Schema validator for - property definitions in `lib/Db/Schema.php` (translatable - properties may declare it; non-translatable MUST NOT). -- [ ] Add migration `lib/Migration/Version1Date20260520120000.php` — - `ALTER TABLE openregister_translations ADD COLUMN source_language - VARCHAR(16) NOT NULL DEFAULT '';` then a back-fill UPDATE that - sets `source_language = (SELECT default_language FROM - openregister_register WHERE id = …)` for every row. -- [ ] Update `lib/Db/Translation.php` entity — add `sourceLanguage` - column property, getter/setter via QBMapper. -- [ ] Update `lib/Db/TranslationMapper.php` to include - `source_language` in find/insert/update queries. -- [ ] Add a back-fill `php occ openregister:translations:backfill-source-language` - idempotent command (re-runs are no-ops once every row has a - non-empty `source_language`). - -## Phase 2 — Projection + status service - -- [ ] Update `lib/Service/TranslationProjectionService.php` to populate - `source_language` from: object-level `_translationMeta..sourceLanguage` - → schema property `sourceLanguage` → register `defaultLanguage`. -- [ ] Add `lib/Service/TranslationStatusService::markDerivedTranslationsOutdated(string $objectUuid, string $property, string $sourceLanguage): int` - that flips every non-source-language Translation row to status - `outdated`. Returns count of rows flipped. -- [ ] Wire the source-change trigger in - `lib/Service/Object/SaveObject.php`: when a translatable - property's source-language value changes (compare old vs new), - call `markDerivedTranslationsOutdated` for that object/property. - Include the trigger in the existing event-emission path so - consumers can subscribe. - -## Phase 3 — Controller + render - -- [ ] Extend `lib/Controller/TranslationController::search` to honour - `?sourceLanguage=`, `?isOutOfDate=true`, - `?compareToSource=true` query filters. -- [ ] Update `lib/Service/Object/RenderObject.php` to attach - `_meta.languageMeta. = { served, sourceLanguage, - isSource, status }` when `?_translationMeta=true` is requested. -- [ ] Set `X-Source-Language` response header in - `lib/Middleware/LanguageMiddleware::afterController()` based on - the resolved source language for the response. - -## Phase 4 — Spec + tests - -- [ ] Write `specs/i18n-source-of-truth/spec.md` with one Requirement - per public surface (schema property, DB column, projection, - status service, controller filters, render metadata, response - header). -- [ ] Update `openregister/openspec/specs/register-i18n/spec.md` to - remove the "Not yet implemented" line about source-of-truth and - automatic outdated status; cross-reference this change. -- [ ] Add `tests/Unit/Service/TranslationProjectionServiceSourceLanguageTest.php` - covering: object-level override, schema-default inheritance, - register-default fallback. -- [ ] Add `tests/Unit/Service/TranslationStatusServiceOutdatedTest.php` - covering: source-change → derived languages flipped to outdated; - non-source-change does NOT flip; missing `sourceLanguage` falls - back to register default. -- [ ] Add `tests/Unit/Service/Object/RenderObjectLanguageMetaTest.php` - covering: `_meta.languageMeta` envelope present when - `?_translationMeta=true`, absent otherwise. -- [ ] Add Newman collection - `tests/integration/openregister-i18n-source-of-truth.postman_collection.json` - hitting: object create with `sourceLanguage: nl`, source value - edit → outdated flip on derived row, controller filter - `?isOutOfDate=true` returns the flipped row, render with - `?_translationMeta=true` returns the envelope. -- [ ] Wire the new Newman collection into - `tests/newman/run-all.sh::DOMAIN_ORDER` after `relations`. - -## Phase 5 — Documentation - -- [ ] Update `docs/i18n.md` (or create) describing the source-of-truth - model and the `sourceLanguage` schema-property contract. -- [ ] Cross-reference this change from - `hydra/openspec/architecture/adr-025-i18n-source-of-truth.md`'s - "Implementation reference" section once shipped. diff --git a/openspec/changes/integration-activity/hydra.json b/openspec/changes/integration-activity/hydra.json deleted file mode 100644 index 932f4d03a1..0000000000 --- a/openspec/changes/integration-activity/hydra.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema_version": 2, - "spec_slug": "integration-activity", - "app": "openregister", - "repo": "https://github.com/ConductionNL/openregister", - "depends_on": [ - "pluggable-integration-registry" - ], - "issue": "https://github.com/ConductionNL/openregister/issues/1318", - "pipeline": {}, - "cycles": [] -} diff --git a/openspec/changes/integration-activity/specs/generic-integrations/spec.md b/openspec/changes/integration-activity/specs/generic-integrations/spec.md deleted file mode 100644 index 850b26b79a..0000000000 --- a/openspec/changes/integration-activity/specs/generic-integrations/spec.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -status: proposed ---- - -# Integration: Activity - -## Purpose - -Surface NC Activity events relevant to an OR object through a query-time integration (no link table). - -**Standards**: NC Activity API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Activity Provider Registration - -`ActivityProvider` registered with id='activity', group='workflow', requiredApp='activity', storage='query-time' (new storage strategy value). - -### Requirement: Query-Time Storage Strategy - -The provider SHALL implement `list()` by querying NC Activity filtered by object + linked entities. Mutation methods SHALL throw `NotImplementedException`. - -#### Scenario: Mutation attempt returns 501 - -- **WHEN** `POST /api/objects/{register}/{schema}/{id}/activity` is called -- **THEN** the system MUST return HTTP 501 Not Implemented -- **AND** an explanatory message MUST reference NC Activity as the source of truth - -### Requirement: Blended Feed - -Tab SHALL show a unified feed of NC Activity events + OR cross-integration events (files linked, notes added, deck cards moved, etc.) filtered to the object's scope. - -### Requirement: Filter Chips - -Tab SHALL provide event-type filter chips with persistence of the user's last selection. - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`); the `detail-page` surface mirrors the tab. - -### Requirement: Reference-Property (Niche) - -`referenceType: 'activity'` SHALL render a single-event chip. Use cases are rare — activity events aren't typically referenced by schemas — but the contract is preserved for completeness. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; NC Activity's filtering governs per-user visibility. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying activity event in NC Activity is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Activity app disabled mid-session - -- **GIVEN** the user had `CnActivityTab` open when an admin disabled NC Activity -- **WHEN** the next poll fetches new events -- **THEN** the tab MUST render a "Activity unavailable" state (no crash) -- **AND** the integration MUST disappear from the registry on the next request diff --git a/openspec/changes/integration-activity/specs/integration-activity/spec.md b/openspec/changes/integration-activity/specs/integration-activity/spec.md deleted file mode 100644 index 1e5e3bad12..0000000000 --- a/openspec/changes/integration-activity/specs/integration-activity/spec.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -status: proposed ---- - -# Integration: Activity - -## Purpose - -Surface NC Activity events relevant to an OR object through a query-time integration (no link table). - -**Standards**: NC Activity API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Activity Provider Registration - -`ActivityProvider` registered with id='activity', group='workflow', requiredApp='activity', storage='query-time' (new storage strategy value). - -### Requirement: Query-Time Storage Strategy - -The provider SHALL implement `list()` by querying NC Activity filtered by object + linked entities. Mutation methods SHALL throw `NotImplementedException`. - -#### Scenario: Mutation attempt returns 501 - -- **WHEN** `POST /api/objects/{register}/{schema}/{id}/activity` is called -- **THEN** the system MUST return HTTP 501 Not Implemented -- **AND** an explanatory message MUST reference NC Activity as the source of truth - -### Requirement: Blended Feed - -Tab SHALL show a unified feed of NC Activity events + OR cross-integration events (files linked, notes added, deck cards moved, etc.) filtered to the object's scope. - -### Requirement: Filter Chips - -Tab SHALL provide event-type filter chips with persistence of the user's last selection. - -### Requirement: Widget Surfaces - -Standard four surfaces; detail-page surface mirrors tab. - -### Requirement: Reference-Property (Niche) - -`referenceType: 'activity'` SHALL render a single-event chip. Use cases are rare — activity events aren't typically referenced by schemas — but the contract is preserved for completeness. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; NC Activity's filtering governs per-user visibility. diff --git a/openspec/changes/integration-activity/tasks.md b/openspec/changes/integration-activity/tasks.md deleted file mode 100644 index 71a3c80a62..0000000000 --- a/openspec/changes/integration-activity/tasks.md +++ /dev/null @@ -1,39 +0,0 @@ -# Tasks: Integration — Activity - -## Umbrella coordination - -- [ ] Open a tiny PR against the umbrella's docs/enum to add `'query-time'` as a recognised storage strategy - -## Backend - -- [ ] `ActivityFeedService` — query NC Activity filtered by object + linked entities; merge with OR cross-integration events -- [ ] `ActivityController` — list endpoint only (no mutations) -- [x] `ActivityProvider` — id='activity', label='Activity', icon='Timeline', group='workflow', requiredApp='activity', storage='query-time'; mutation methods throw NotImplemented -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnActivityTab.vue` — feed with event-type filter chips, saved filter prefs, infinite scroll -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnActivityCard.vue`: - - `user-dashboard`: "N new today" across user's objects - - `app-dashboard`: scoped - - `detail-page`: feed (same layout as tab, smaller height) - - `single-entity`: single event chip with actor + verb + target -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/activity.js` — register with `referenceType: 'activity'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: activity related to an object appears in tab; filter chips narrow view; "new today" count correct on dashboard -- [ ] Hide test diff --git a/openspec/changes/integration-analytics/specs/generic-integrations/spec.md b/openspec/changes/integration-analytics/specs/generic-integrations/spec.md deleted file mode 100644 index 751f35aaf2..0000000000 --- a/openspec/changes/integration-analytics/specs/generic-integrations/spec.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -status: proposed ---- - -# Integration: Analytics - -## Purpose - -Link NC Analytics reports to OR objects/schemas with embedded chart rendering via apexcharts. - -**Standards**: NC Analytics API, apexcharts, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Analytics Provider Registration - -`AnalyticsProvider` registered with id='analytics', group='workflow', requiredApp='analytics', storage='link-table'. - -### Requirement: Embedded Chart Rendering via Shared Library - -Charts SHALL render via apexcharts (the existing shared dep via `@conduction/nextcloud-vue`) consuming Analytics' chart config. Analytics chart logic SHALL NOT be re-implemented. - -### Requirement: Differential Refresh Rates - -Dashboard surfaces SHALL auto-refresh every 5 minutes. Detail-page and single-entity surfaces SHALL refresh only on user action. - -#### Scenario: Dashboard chart refreshes automatically - -- **GIVEN** a linked Analytics report displayed on `user-dashboard` -- **WHEN** 5 minutes pass since last fetch -- **THEN** the chart data MUST be re-fetched without user interaction - -#### Scenario: Detail-page chart does not auto-refresh - -- **GIVEN** a linked Analytics report displayed on `detail-page` -- **WHEN** 5 minutes pass -- **THEN** the chart data MUST NOT be re-fetched unless user clicks refresh or re-enters the route - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`); `single-entity` includes a sparkline. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'analytics'` SHALL render report-title chip + sparkline. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; Analytics ACLs govern. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying report in NC Analytics is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Analytics report returns no data - -- **GIVEN** a linked Analytics report whose dataset has zero rows -- **WHEN** `CnAnalyticsCard` renders -- **THEN** an empty-state chart MUST be displayed with "No data" labeling -- **AND** the refresh action MUST remain available - -#### Scenario: Analytics app version mismatch - -- **GIVEN** NC Analytics returns a chart config version OR does not recognise -- **WHEN** `CnAnalyticsCard` attempts to render -- **THEN** it MUST fall back to a link-out "Open in Analytics" affordance (no broken chart) diff --git a/openspec/changes/integration-analytics/specs/integration-analytics/spec.md b/openspec/changes/integration-analytics/specs/integration-analytics/spec.md deleted file mode 100644 index 6970c1d831..0000000000 --- a/openspec/changes/integration-analytics/specs/integration-analytics/spec.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -status: proposed ---- - -# Integration: Analytics - -## Purpose - -Link NC Analytics reports to OR objects/schemas with embedded chart rendering via apexcharts. - -**Standards**: NC Analytics API, apexcharts, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Analytics Provider Registration - -`AnalyticsProvider` registered with id='analytics', group='workflow', requiredApp='analytics', storage='link-table'. - -### Requirement: Embedded Chart Rendering via Shared Library - -Charts SHALL render via apexcharts (the existing shared dep via `@conduction/nextcloud-vue`) consuming Analytics' chart config. Analytics chart logic SHALL NOT be re-implemented. - -### Requirement: Differential Refresh Rates - -Dashboard surfaces SHALL auto-refresh every 5 minutes. Detail-page and single-entity surfaces SHALL refresh only on user action. - -#### Scenario: Dashboard chart refreshes automatically - -- **GIVEN** a linked Analytics report displayed on `user-dashboard` -- **WHEN** 5 minutes pass since last fetch -- **THEN** the chart data MUST be re-fetched without user interaction - -#### Scenario: Detail-page chart does not auto-refresh - -- **GIVEN** a linked Analytics report displayed on `detail-page` -- **WHEN** 5 minutes pass -- **THEN** the chart data MUST NOT be re-fetched unless user clicks refresh or re-enters the route - -### Requirement: Widget Surfaces - -Standard four; single-entity includes sparkline. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'analytics'` SHALL render report-title chip + sparkline. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; Analytics ACLs govern. diff --git a/openspec/changes/integration-analytics/tasks.md b/openspec/changes/integration-analytics/tasks.md deleted file mode 100644 index e8638be9fd..0000000000 --- a/openspec/changes/integration-analytics/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -# Tasks: Integration — Analytics - -## Backend - -- [ ] `AnalyticsLink` entity + mapper + migration (schema/object → report id) -- [ ] `AnalyticsReportService` — fetch report config + latest data -- [ ] `AnalyticsController` -- [x] `AnalyticsProvider` — id='analytics', label='Analytics', icon='ChartBar', group='workflow', requiredApp='analytics', storage='link-table' -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnAnalyticsTab.vue` — linked reports with inline chart previews, link-existing, unlink -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnAnalyticsCard.vue`: - - `user-dashboard`: top KPI from report - - `app-dashboard`: scoped, full chart - - `detail-page`: full chart with refresh button - - `single-entity`: report-title chip + sparkline -- [ ] Dashboard 5-min auto-refresh, on-demand elsewhere -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/analytics.js` — register with `referenceType: 'analytics'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: link an Analytics report, verify chart embeds in tab and widget -- [ ] Refresh test: dashboard chart updates within 5 min after data change in Analytics -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-bookmarks/design.md b/openspec/changes/integration-bookmarks/design.md deleted file mode 100644 index 910db76ea6..0000000000 --- a/openspec/changes/integration-bookmarks/design.md +++ /dev/null @@ -1,41 +0,0 @@ -# Design: Integration — Bookmarks - -> Umbrella decisions apply -> -> **Cross-repo note**: file paths under `nextcloud-vue/src/...` or bare component names (`CnXxxTab`, `CnXxxCard`) are **expected locations** in the `@conduction/nextcloud-vue` shared library, not binding spec. The frontend implementation PR lands in that separate repo and MAY choose different paths. - -## Approach - -Simple link-table provider; tab = list + add. The "add URL" flow delegates scraping (title, description, favicon) to Bookmarks app's existing logic. - -## Architecture Decisions - -### AD-1: Delegate URL scraping to Bookmarks app - -**Decision**: "Add URL" calls Bookmarks' own create-bookmark endpoint, then links the result. No duplicate scraping logic in OR. - -**Why**: Bookmarks app already handles URL metadata extraction robustly. Duplicating is churn. - -### AD-2: Reuse Bookmarks' tag system in tab display - -**Decision**: Linked bookmarks show their Bookmarks-side tags as filter chips at top of tab. - -**Why**: Power users of Bookmarks categorise heavily; those categories are already meaningful. Respecting them avoids duplicate tagging effort. - -## Files Affected - -### Backend (new) -- `BookmarkService`, `BookmarksController`, `BookmarkLink` entity + mapper + migration, `BookmarksProvider`, unit tests - -### Backend (modified) -- `Application.php`, `routes.php` - -### Frontend (new) -- `CnBookmarksTab/*`, `CnBookmarksCard/*`, `src/integrations/builtin/bookmarks.js`, barrels + tests - -## Risks - -| Risk | Mitigation | -|---|---| -| URLs requiring auth — scraping returns login page title | Tab offers manual title override | -| Dead links accumulate | Optional "check link" action surfaces HTTP status; detection is manual, not automatic | diff --git a/openspec/changes/integration-bookmarks/hydra.json b/openspec/changes/integration-bookmarks/hydra.json deleted file mode 100644 index ad5067685d..0000000000 --- a/openspec/changes/integration-bookmarks/hydra.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema_version": 2, - "spec_slug": "integration-bookmarks", - "app": "openregister", - "repo": "https://github.com/ConductionNL/openregister", - "depends_on": [ - "pluggable-integration-registry" - ], - "issue": "https://github.com/ConductionNL/openregister/issues/1315", - "pipeline": {}, - "cycles": [] -} diff --git a/openspec/changes/integration-bookmarks/specs/generic-integrations/spec.md b/openspec/changes/integration-bookmarks/specs/generic-integrations/spec.md deleted file mode 100644 index 43450913b0..0000000000 --- a/openspec/changes/integration-bookmarks/specs/generic-integrations/spec.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -status: proposed ---- - -# Integration: Bookmarks - -## Purpose - -Link NC Bookmarks (URLs) to OR objects through the registry with tag-aware display. - -**Standards**: NC Bookmarks API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Bookmarks Provider Registration - -`BookmarksProvider` registered with id='bookmarks', group='docs', requiredApp='bookmarks', storage='link-table'. - -### Requirement: Add URL Flow Delegates Scraping - -"Add URL" MUST call the NC Bookmarks create endpoint to extract title/favicon; OR MUST NOT re-implement scraping. - -### Requirement: Tag-Aware Display - -Linked bookmarks' Bookmarks-side tags SHALL be shown as filter chips in the tab. - -#### Scenario: Tag filter narrows the list - -- **GIVEN** 10 linked bookmarks with 3 distinct Bookmarks-side tags -- **WHEN** user clicks the "legal" tag chip -- **THEN** only bookmarks carrying that tag MUST be shown - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`). - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'bookmarks'` SHALL render favicon chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; Bookmarks' own ACLs apply. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying bookmark in NC Bookmarks is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Bookmark URL returns HTTP error - -- **GIVEN** a linked bookmark whose URL returns 4xx/5xx at check time -- **WHEN** the user clicks "check link" from the tab -- **THEN** the bookmark row MUST display the returned status code and date checked -- **AND** the link itself MUST remain (dead-link cleanup is manual, not automatic) diff --git a/openspec/changes/integration-bookmarks/specs/integration-bookmarks/spec.md b/openspec/changes/integration-bookmarks/specs/integration-bookmarks/spec.md deleted file mode 100644 index 1d2c46388d..0000000000 --- a/openspec/changes/integration-bookmarks/specs/integration-bookmarks/spec.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -status: proposed ---- - -# Integration: Bookmarks - -## Purpose - -Link NC Bookmarks (URLs) to OR objects through the registry with tag-aware display. - -**Standards**: NC Bookmarks API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Bookmarks Provider Registration - -`BookmarksProvider` registered with id='bookmarks', group='docs', requiredApp='bookmarks', storage='link-table'. - -### Requirement: Add URL Flow Delegates Scraping - -"Add URL" MUST call the NC Bookmarks create endpoint to extract title/favicon; OR MUST NOT re-implement scraping. - -### Requirement: Tag-Aware Display - -Linked bookmarks' Bookmarks-side tags SHALL be shown as filter chips in the tab. - -#### Scenario: Tag filter narrows the list - -- **GIVEN** 10 linked bookmarks with 3 distinct Bookmarks-side tags -- **WHEN** user clicks the "legal" tag chip -- **THEN** only bookmarks carrying that tag MUST be shown - -### Requirement: Widget Surfaces - -Four surfaces, standard contract. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'bookmarks'` SHALL render favicon chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; Bookmarks' own ACLs apply. diff --git a/openspec/changes/integration-bookmarks/tasks.md b/openspec/changes/integration-bookmarks/tasks.md deleted file mode 100644 index d7f2d96cc3..0000000000 --- a/openspec/changes/integration-bookmarks/tasks.md +++ /dev/null @@ -1,35 +0,0 @@ -# Tasks: Integration — Bookmarks - -## Backend - -- [ ] `BookmarkLink` entity + mapper + migration -- [ ] `BookmarkService` wrapping Bookmarks REST API -- [ ] `BookmarksController` -- [x] `BookmarksProvider` — id='bookmarks', label='Bookmarks', icon='Bookmark', group='docs', requiredApp='bookmarks', storage='link-table' -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnBookmarksTab.vue` — list with favicon/title/tag chips, link-existing + add-URL (delegates scrape), unlink -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnBookmarksCard.vue`: - - `user-dashboard`: recent bookmarks - - `app-dashboard`: scoped - - `detail-page`: full list with favicon grid - - `single-entity`: favicon chip + title -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/bookmarks.js` — register with `referenceType: 'bookmarks'` - -## Quality - -- [ ] Parity gate passes; nl+en; strict checks; ESLint clean - -## Acceptance verification - -- [ ] E2E: add URL to object, verify scrape + link + Bookmarks app entry; unlink; hide test; reference-property test diff --git a/openspec/changes/integration-brp-haalcentraal/proposal.md b/openspec/changes/integration-brp-haalcentraal/proposal.md new file mode 100644 index 0000000000..04f2cad1dc --- /dev/null +++ b/openspec/changes/integration-brp-haalcentraal/proposal.md @@ -0,0 +1,89 @@ +--- +kind: code +status: proposed +--- + +## Why + +pipelinq holds a bespoke BRP person-lookup HTTP client — +`OCA\Pipelinq\Service\HaalCentraalClient` — that talks to the RvIG +HaalCentraal Personen v2.0 API. Unlike the KvK / OpenCorporates clients, it +carries two government-grade transport capabilities: OAuth2 client_credentials +token acquisition (cached, bearer-injected) and mutual TLS with a PKIoverheid +client certificate. It also normalises the HAL+JSON person object into a +domain shape and masks BSNs for logging. + +The investigation question for this change was: **can OR's canonical transport +(`OCA\OpenConnector\Service\CallService`) actually perform BRP's OAuth2 + +mTLS?** It can: + +- **OAuth2 client_credentials** — `AuthenticationService::fetchOAuthTokens` + (`grant_type=client_credentials`, body or basic_auth client auth) acquires + the token; the bearer is injected per-call via the `{{ oauthToken(source) }}` + Twig placeholder rendered into the source's `Authorization` header. +- **mutual TLS (PKIoverheid)** — `CallService::getCertificate` writes the + source's `configuration.cert` / `configuration.ssl_key` PEM to temp files for + Guzzle's TLS handshake and cleans them up afterwards. + +So BRP is **not** an app-specific transport that OR's generic leaf shouldn't +own — it is exactly the external-leaf shape OR already runs for KvK / xWiki, +just with an OAuth+mTLS source instead of an apikey source. Per ADR-022 the +connection + OAuth credentials + PKIoverheid certificate should live once in +OpenConnector, and OR should expose a thin lookup endpoint pipelinq re-points +to. Today OR has no BRP leaf, so there is nothing to re-point to. + +What stays app-specific (and correctly so): BSN elfproef validation, BSN +masking for logs, and the HaalCentraal→domain field normalisation +(`naam` / `geboorte` / `verblijfplaats`, geslacht-code mapping). This leaf +round-trips the raw HAL+JSON person object; the consuming app keeps the +privacy + mapping logic. + +## What Changes + +- **`BrpPersoonProvider`** (`storage='external'`, + `getOpenConnectorSource()='brp-haalcentraal'`) — a stateless, read-only + person-lookup leaf mirroring `KvkProvider`'s OpenConnector wiring. `list()` + (registry read-path) treats the `_search` filter as a BSN; `lookupByBsn()` + POSTs a `RaadpleegMetBurgerservicenummer` query to the HaalCentraal + `/personen` endpoint and returns the raw person objects. Every call routes + through `ExternalIntegrationRouter` → OpenConnector `CallService`, which + applies the OAuth2 bearer + mTLS client cert from the source. The provider + never holds an HTTP client, credentials, or a certificate. `authRequirements()` + advertises `supports: [oauth2_client_credentials, mtls]`. Degrades + null-safely to `{ unavailable, cause }` (AD-23) — never a fatal. +- **`PersonLookupController` + route** (read-only, `@NoAdminRequired`): + `GET /api/integrations/brp/person?bsn=` → `{ results, total }` on success, + `503 { error, details: { cause } }` when the source is missing/down. The BSN + travels in the request body to the upstream (never the path → no SSRF, no BSN + in upstream access logs) and is never logged by OR. +- **Unit tests** — `BrpPersoonProviderTest` (metadata, auth shape, POST body + + BSN, `personen` / `_embedded.personen` envelope unwrap, the 4-state degraded + contract, health delegation). The router is mocked; no real BSN is used. + +The paired OpenConnector change (`seed-brp-haalcentraal-source`) seeds the +dormant `brp-haalcentraal` source carrying the production HaalCentraal base + +token URLs, the `{{ oauthToken(source) }}` header, and empty OAuth credential + +cert/key placeholders. No secret or certificate is committed in either change. + +## Capabilities + +### Added Capabilities +- `integration-person-lookup`: a new read-only BRP person-lookup surface + (lookup by BSN) backed by an external OpenConnector-routed leaf with OAuth2 + + mTLS configured on the source. + +## Impact + +- **Code:** `lib/Service/Integration/Providers/BrpPersoonProvider.php`, + `lib/Controller/PersonLookupController.php`, `appinfo/routes.php` (one route), + `tests/Unit/Service/Integration/Providers/BrpPersoonProviderTest.php`. +- **Behaviour:** `GET /api/integrations/brp/person?bsn=` resolves the + `brp-haalcentraal` source and returns the raw HaalCentraal person object, or + a 503-with-cause when unconfigured/down. With the paired dormant source + seeded (no credentials/cert), the endpoint degrades to + `upstream-service-down` rather than `openconnector-source-missing`. +- **Consumers:** pipelinq (future) re-points `HaalCentraalClient` at OR's + `/api/integrations/brp/person` endpoint, keeping its BSN elfproef + + masking + field normalisation locally. +- **Secrets / privacy:** none committed. The BSN is never logged by OR and + travels in the request body only. diff --git a/openspec/changes/integration-brp-haalcentraal/specs/integration-person-lookup/spec.md b/openspec/changes/integration-brp-haalcentraal/specs/integration-person-lookup/spec.md new file mode 100644 index 0000000000..bc749b655d --- /dev/null +++ b/openspec/changes/integration-brp-haalcentraal/specs/integration-person-lookup/spec.md @@ -0,0 +1,79 @@ +# integration-person-lookup + +## ADDED Requirements + +### Requirement: BRP HaalCentraal Person Lookup + +OpenRegister SHALL expose a read-only BRP person-lookup endpoint backed by a +`BrpPersoonProvider` leaf (`storage='external'`, +`getOpenConnectorSource()='brp-haalcentraal'`). The HaalCentraal base URL, the +OAuth2 client_credentials secret, and the PKIoverheid mutual-TLS client +certificate SHALL be resolved exclusively from the OpenConnector +`brp-haalcentraal` source via `ExternalIntegrationRouter` → `CallService` — the +caller never supplies a URL, token, or certificate, and the provider never +holds an HTTP client. The endpoint SHALL be `@NoAdminRequired` (any +authenticated user) and read-only. + +`GET /api/integrations/brp/person?bsn=` SHALL look up a single person by +Burgerservicenummer and return `{ results, total }` of raw HaalCentraal person +objects (0 or 1 entries). The lookup SHALL issue a POST to the HaalCentraal +`/personen` endpoint with a `RaadpleegMetBurgerservicenummer` body carrying the +BSN in `burgerservicenummer` and an explicit `fields` list. The BSN SHALL +travel in the request body only (never the URL path) and SHALL never be logged +by OpenRegister. The endpoint SHALL round-trip the raw HAL+JSON person object; +the BSN elfproef validation, BSN masking, and the BRP→domain field mapping are +the consuming app's responsibility. + +When the OpenConnector `brp-haalcentraal` source is missing/unconfigured, or +the upstream HaalCentraal API is unreachable, the endpoint SHALL fail closed — +returning `503 { error, details: { cause } }` (cause one of +`openconnector-down`, `openconnector-source-missing`, `provider-auth`, +`upstream-service-down`) — and SHALL never raise a fatal. The provider method +SHALL return `{ unavailable, cause, results: [], total: 0 }` in that case. + +The provider's `authRequirements()` SHALL report +`{ type: 'external', configuredVia: 'openconnector', source: 'brp-haalcentraal', +supports: ['oauth2_client_credentials', 'mtls'] }` so the admin UI surfaces +that both transports are configured on the OpenConnector source. + +#### Scenario: BRP lookup by BSN returns the person + +- GIVEN the OpenConnector `brp-haalcentraal` source is configured (OAuth2 + + mTLS) and enabled +- WHEN a client calls `GET /api/integrations/brp/person?bsn=999993653` +- THEN the response is `200 { results, total }` carrying the raw HaalCentraal + person object for that BSN +- @e2e exclude Backend integration endpoint — verified by PHPUnit (router mocked) + a live OpenConnector source, not a browser flow. + +#### Scenario: BRP lookup forwards a RaadpleegMetBurgerservicenummer body + +- GIVEN the `brp-haalcentraal` source is configured +- WHEN the provider looks up a BSN +- THEN it routes a POST to `personen` whose body has + `type='RaadpleegMetBurgerservicenummer'`, the BSN in + `burgerservicenummer[0]`, and a non-empty `fields` list +- @e2e exclude Backend request-shape assertion — verified by PHPUnit (router mocked), not a browser flow. + +#### Scenario: BRP lookup degrades when the source is missing + +- GIVEN no OpenConnector `brp-haalcentraal` source exists (or OpenConnector is disabled) +- WHEN a client calls `GET /api/integrations/brp/person?bsn=999993653` +- THEN the response is `503 { error, details: { cause } }` with cause + `openconnector-source-missing` (or `openconnector-down`), and no fatal is raised +- @e2e exclude Backend degraded path — verified by PHPUnit + a live source-missing probe, not a browser flow. + +#### Scenario: BRP lookup degrades when HaalCentraal is unreachable + +- GIVEN the `brp-haalcentraal` source is seeded but dormant/unauthenticated (no + OAuth credentials / cert) +- WHEN a client calls `GET /api/integrations/brp/person?bsn=999993653` +- THEN the response is `503 { error, details: { cause } }` with cause + `upstream-service-down` (or `provider-auth`), and no fatal is raised +- @e2e exclude Backend degraded path — verified by PHPUnit + a live dormant-source probe, not a browser flow. + +#### Scenario: BRP lookup never logs the BSN + +- GIVEN any BRP lookup (success or degraded) +- WHEN the provider or controller writes a log line +- THEN the log line contains neither the raw BSN nor the request/response body +- @e2e exclude Backend privacy invariant — verified by code review + PHPUnit (logger mocked, asserted not to receive the BSN), not a browser flow. diff --git a/openspec/changes/integration-brp-haalcentraal/tasks.md b/openspec/changes/integration-brp-haalcentraal/tasks.md new file mode 100644 index 0000000000..9ca26f32eb --- /dev/null +++ b/openspec/changes/integration-brp-haalcentraal/tasks.md @@ -0,0 +1,42 @@ +# Tasks — integration-brp-haalcentraal + +## 1. BrpPersoonProvider leaf + +- [x] 1.1 Add `lib/Service/Integration/Providers/BrpPersoonProvider.php` + extending `AbstractIntegrationProvider`: `storage='external'`, + `SOURCE_ID='brp-haalcentraal'`, `getRequiredApp()='openconnector'`, + metadata (id/label/icon/group), `authRequirements()` advertising + `supports: [oauth2_client_credentials, mtls]`. +- [x] 1.2 Implement `lookupByBsn()` — POST `personen` with a + `RaadpleegMetBurgerservicenummer` body (BSN + explicit `fields`) via + `ExternalIntegrationRouter::call`; unwrap `personen` / + `_embedded.personen`; return `{ results, total }`. +- [x] 1.3 Implement `list()` (registry read-path) treating `_search` as the BSN. +- [x] 1.4 Degrade null-safely to `{ unavailable, cause }` on + `ProviderUnavailableException` + any Throwable (AD-23); never log the BSN. +- [x] 1.5 `health()` defers to `ExternalIntegrationRouter::probe`. + +## 2. Endpoint + +- [x] 2.1 Add `lib/Controller/PersonLookupController.php` with + `brpPerson()` (`@NoAdminRequired @NoCSRFRequired`): reads `bsn`, + 400 when empty, relays the provider's degraded descriptor as + `503 { error, details: { cause } }`, success as `200 { results, total }`. +- [x] 2.2 Register the route + `GET /api/integrations/brp/person` → `personLookup#brpPerson`. + +## 3. Tests + +- [x] 3.1 Add `BrpPersoonProviderTest` — metadata, auth shape, POST body + BSN, + `personen` + `_embedded.personen` unwrap, empty-BSN short-circuit, the + 4-state degraded contract (source-missing / upstream-down / Throwable), + health delegation. Router mocked; no real BSN. + +## 4. Verify + +- [x] 4.1 `composer test` (the new BrpPersoonProviderTest) is green. +- [x] 4.2 PHPCS + the enforced gate are clean on the changed files. +- [x] 4.3 Live (service layer, dormant source): the BRP lookup endpoint resolves + the seeded `brp-haalcentraal` source and degrades to + `upstream-service-down` (non-fatal) rather than + `openconnector-source-missing`. diff --git a/openspec/changes/integration-calendar/proposal.md b/openspec/changes/integration-calendar/proposal.md deleted file mode 100644 index e67581f2c2..0000000000 --- a/openspec/changes/integration-calendar/proposal.md +++ /dev/null @@ -1,66 +0,0 @@ -# Integration: Calendar (Meetings) - -## Problem - -OpenRegister can already link CalDAV VEVENT entries to objects via `CalendarEventService` and `CalendarEventsController` (253 LOC, shipped in `nextcloud-entity-relations`). The backend stores RFC 9253 LINK properties on the VEVENT and offers full CRUD over `/api/objects/{register}/{schema}/{id}/calendar`. - -**The UI is missing entirely.** Users cannot see, add, or manage meetings linked to objects. Consuming apps (Procest, Pipelinq, Decidesk, ZaakAfhandelApp) have no path to surface meetings on case detail pages or dashboards. - -## Context - -- **Backend already shipped:** [lib/Service/CalendarEventService.php](openregister/lib/Service/CalendarEventService.php), [lib/Controller/CalendarEventsController.php](openregister/lib/Controller/CalendarEventsController.php) — link/create/unlink VEVENT, attendee handling, calendar selection -- **Required NC app:** `calendar` (and CalDAV backend, always present in NC core) -- **Storage strategy:** `link-table` augmented by CalDAV custom properties (X-OPENREGISTER-*) for reverse discovery -- **Depends on:** `pluggable-integration-registry` (umbrella) — the registry contract, parity gate, surface model -- **Cross-app demand:** Procest (case meetings), Pipelinq (kanban deadlines), Decidesk (council meetings), ZaakAfhandelApp (zaak afspraken), MyDash (personal agenda overview) - -## Proposed Solution - -Ship the calendar integration's full vertical slice through the umbrella's contract: - -1. **`CalendarProvider`** (PHP) — `IntegrationProvider` implementation wrapping the existing `CalendarEventService`. id=`calendar`, group=`comms`, requiredApp=`calendar`, storage=`link-table`, no extra permission, no auth requirements. -2. **`CnCalendarTab`** (Vue) — sidebar tab listing linked VEVENTs with date/time, attendees, location. Inline create form (date picker, summary, attendees from contacts integration if present). -3. **`CnCalendarCard`** (Vue) — widget rendering across all four surfaces: - - `user-dashboard`: upcoming meetings across all the user's objects - - `app-dashboard`: meetings for objects in this app's scope - - `detail-page`: this object's meetings as a card with "Add meeting" CTA - - `single-entity`: a single VEVENT rendered as a chip (date + summary + status) -4. **Frontend registration** — `src/integrations/builtin/calendar.js` (or app-side if not built-in) calls `OCA.OpenRegister.integrations.register({ id: 'calendar', ... })`. -5. **Backend registration** — `CalendarProvider` is DI-tagged in `Application::register()`. -6. **Spec delta** — new requirement under `generic-integrations` capability or new `integration-calendar` capability spec. -7. **Tests** — provider unit test, component tests across surfaces, integration test asserting end-to-end flow (register → schema accepts `calendar` in linkedTypes → UI renders). - -## Scope - -### In scope - -- `lib/Service/Integration/Providers/CalendarProvider.php` (wraps existing `CalendarEventService`) -- `nextcloud-vue/src/components/CnCalendarTab/CnCalendarTab.vue` -- `nextcloud-vue/src/components/CnCalendarCard/CnCalendarCard.vue` (handles all 4 surfaces internally) -- `nextcloud-vue/src/integrations/builtin/calendar.js` registration -- DI tag registration in `lib/AppInfo/Application.php` -- Unit test for `CalendarProvider` -- Component tests for `CnCalendarTab` + `CnCalendarCard` (one per surface) -- nl + en translations for all new strings -- Spec delta: new `integration-calendar` capability spec -- Update developer guide to reference calendar as a worked link-table example - -### Out of scope - -- Modifications to the existing `CalendarEventService` or `CalendarEventsController` — they are wrapped, not changed -- Additional VEVENT features beyond what the existing controller exposes (e.g., recurring events UI, free/busy view) — separate future changes if needed -- Calendar selection UI for choosing which calendar a new VEVENT lands in — defaults to the user's first writable calendar (existing service behaviour) -- Cross-meeting analytics / KPIs on dashboards — apps may build these on top, but not in this leaf - -## Acceptance criteria - -- [ ] A user with the Calendar app installed sees a "Meetings" tab in `CnObjectSidebar` for any object whose schema has `calendar` in `linkedTypes` -- [ ] The tab lists all linked VEVENTs with date, summary, attendees -- [ ] User can create a new meeting from the tab (creates the VEVENT in their calendar + the link) -- [ ] User can unlink a meeting (removes the link, leaves the VEVENT in the calendar) -- [ ] `CnCalendarCard` renders correctly on all four surfaces — verified by component tests -- [ ] Registry parity gate passes — `calendar` integration has both `tab` and `widget` registered -- [ ] OCS capabilities response includes `calendar` in the `integrations` block when the NC Calendar app is installed -- [ ] When the NC Calendar app is uninstalled, the integration is hidden everywhere (sidebar, dashboards, capabilities) -- [ ] A schema property with `referenceType: 'calendar'` renders the `single-entity` widget surface for the referenced VEVENT id -- [ ] All new strings translated to nl + en diff --git a/openspec/changes/integration-calendar/specs/generic-integrations/spec.md b/openspec/changes/integration-calendar/specs/generic-integrations/spec.md deleted file mode 100644 index adb07370c7..0000000000 --- a/openspec/changes/integration-calendar/specs/generic-integrations/spec.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -status: proposed ---- - -# Integration: Calendar - -## Purpose - -Surface Nextcloud Calendar VEVENT entries linked to OpenRegister objects through the standard integration registry contract. Users see meetings on object detail pages, dashboards, and inline next to schema reference properties; they can link, create, and unlink meetings without leaving the OR object context. - -**Standards**: RFC 5545 (iCalendar), RFC 9253 (LINK property), Nextcloud CalDAV, ADR-019 (Integration Registry) -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [nextcloud-entity-relations](../../../../specs/nextcloud-entity-relations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Calendar Provider Registration - -The system SHALL ship a `CalendarProvider` implementing `IntegrationProvider`. The provider SHALL be registered as a DI-tagged service and SHALL appear in `IntegrationRegistry::list()` with id `calendar` whenever the Nextcloud Calendar app is installed and enabled. - -#### Scenario: Provider appears when Calendar app is installed - -- **GIVEN** the NC Calendar app is installed and enabled -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the result MUST include a provider with `id='calendar'`, `group='comms'`, `requiredApp='calendar'`, `storageStrategy='link-table'` - -#### Scenario: Provider hidden when Calendar app is missing - -- **GIVEN** the NC Calendar app is not installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the result MUST NOT include the calendar provider -- **AND** `GET /api/integrations/calendar` MUST return HTTP 404 -- **AND** the OCS capabilities `integrations` block MUST NOT include `calendar` - ---- - -### Requirement: Sidebar Tab - -The system SHALL render a `CnCalendarTab` component inside `CnObjectSidebar` when (a) the calendar provider is enabled in the registry AND (b) the object's schema has `calendar` in `linkedTypes` AND (c) the rendering component does not exclude `calendar`. - -#### Scenario: Tab appears for schemas with calendar in linkedTypes - -- **GIVEN** an object whose schema has `configuration.linkedTypes: ["calendar", "files"]` -- **AND** the user has read access to the object -- **WHEN** `CnObjectSidebar` renders for the object -- **THEN** a tab labeled "Meetings" MUST appear with the Calendar icon -- **AND** the tab MUST contain the linked VEVENTs ordered by date ascending - -#### Scenario: Tab hidden for schemas without calendar in linkedTypes - -- **GIVEN** an object whose schema has `configuration.linkedTypes: ["files", "notes"]` (calendar absent) -- **WHEN** `CnObjectSidebar` renders for the object -- **THEN** no Meetings tab MUST appear - -#### Scenario: Inline meeting creation - -- **GIVEN** the Meetings tab is visible -- **WHEN** the user fills the inline form (date, time, summary, attendees) and clicks "Add" -- **THEN** the system MUST POST to `/api/objects/{register}/{schema}/{id}/calendar` with the form data -- **AND** the new VEVENT MUST be created in the user's first writable calendar -- **AND** the X-OPENREGISTER-* properties MUST link the VEVENT to the object -- **AND** the new meeting MUST appear in the tab list without a page reload - -#### Scenario: Unlink does not delete the VEVENT - -- **GIVEN** a linked meeting in the tab -- **WHEN** the user clicks "Unlink" -- **THEN** the system MUST DELETE the link -- **AND** the VEVENT MUST remain in the user's NC Calendar -- **AND** the meeting MUST disappear from the tab list - ---- - -### Requirement: Widget Across Surfaces - -The system SHALL render `CnCalendarCard` for every surface defined in the integration registry. The component SHALL receive `surface` as a prop and render appropriately for each. - -#### Scenario: User dashboard surface renders upcoming meetings - -- **GIVEN** the user has 12 upcoming meetings linked across various objects -- **WHEN** `CnCalendarCard` renders with `surface='user-dashboard'` -- **THEN** the next 5 upcoming meetings MUST be displayed -- **AND** each MUST link to its object's detail page - -#### Scenario: Detail page surface includes create CTA - -- **GIVEN** an object with linked meetings -- **WHEN** `CnCalendarCard` renders with `surface='detail-page'` and the object's id -- **THEN** the meeting list MUST be rendered -- **AND** an "Add meeting" CTA MUST be visible - -#### Scenario: Single-entity surface renders a chip - -- **GIVEN** a VEVENT id `event-uuid-123` -- **WHEN** `CnCalendarCard` renders with `surface='single-entity'` and `entityId='event-uuid-123'` -- **THEN** the component MUST fetch the single VEVENT and render it as a compact chip with date, summary, and status icon - ---- - -### Requirement: Reference-Property Auto-Rendering - -When a schema property declares `referenceType: 'calendar'`, frontend form/detail components SHALL render `CnCalendarCard` at `surface='single-entity'` for the referenced VEVENT id. - -#### Scenario: Detail grid renders calendar reference inline - -- **GIVEN** a schema with property `nextMeeting: { type: 'string', referenceType: 'calendar' }` -- **AND** an object with `nextMeeting: 'event-uuid-456'` -- **WHEN** `CnDetailGrid` renders the object -- **THEN** the `nextMeeting` row MUST contain `CnCalendarCard` with `entityId='event-uuid-456'` and `surface='single-entity'` -- **AND** raw VEVENT id MUST NOT be visible to the user - ---- - -### Requirement: Permission Inheritance - -The provider SHALL declare `requiresPermission(): null`. Access to the calendar integration SHALL be governed by (a) read access to the underlying object AND (b) NC Calendar's own access controls on the linked VEVENTs. - -#### Scenario: User without object read access does not see meetings - -- **GIVEN** a user with no read permission on object `abc-123` -- **WHEN** the user tries `GET /api/objects/{register}/{schema}/abc-123/calendar` -- **THEN** the system MUST return HTTP 403 (object permission denied, before integration check) - -#### Scenario: User with object access but restricted calendar still works - -- **GIVEN** a user with object read access AND a restricted view of NC Calendar (limited to one calendar) -- **WHEN** the user lists the object's meetings -- **THEN** only meetings on calendars the user can see MUST be returned (NC Calendar enforces this transitively) - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying VEVENT in Nextcloud Calendar is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Concurrent unlink vs delete race - -- **GIVEN** a linked VEVENT -- **AND** user A clicks "Unlink" in OR while user B deletes the VEVENT directly in NC Calendar at the same time -- **WHEN** both operations resolve -- **THEN** the link record MUST be removed (unlink succeeds) -- **AND** no exception MUST be surfaced to user A for the missing VEVENT diff --git a/openspec/changes/integration-calendar/specs/integration-calendar/spec.md b/openspec/changes/integration-calendar/specs/integration-calendar/spec.md deleted file mode 100644 index b4d007c550..0000000000 --- a/openspec/changes/integration-calendar/specs/integration-calendar/spec.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -status: proposed ---- - -# Integration: Calendar - -## Purpose - -Surface Nextcloud Calendar VEVENT entries linked to OpenRegister objects through the standard integration registry contract. Users see meetings on object detail pages, dashboards, and inline next to schema reference properties; they can link, create, and unlink meetings without leaving the OR object context. - -**Standards**: RFC 5545 (iCalendar), RFC 9253 (LINK property), Nextcloud CalDAV, ADR-019 (Integration Registry) -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [nextcloud-entity-relations](../../../../specs/nextcloud-entity-relations/spec.md) - ---- - -## Requirements - -### Requirement: Calendar Provider Registration - -The system SHALL ship a `CalendarProvider` implementing `IntegrationProvider`. The provider SHALL be registered as a DI-tagged service and SHALL appear in `IntegrationRegistry::list()` with id `calendar` whenever the Nextcloud Calendar app is installed and enabled. - -#### Scenario: Provider appears when Calendar app is installed - -- **GIVEN** the NC Calendar app is installed and enabled -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the result MUST include a provider with `id='calendar'`, `group='comms'`, `requiredApp='calendar'`, `storageStrategy='link-table'` - -#### Scenario: Provider hidden when Calendar app is missing - -- **GIVEN** the NC Calendar app is not installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the result MUST NOT include the calendar provider -- **AND** `GET /api/integrations/calendar` MUST return HTTP 404 -- **AND** the OCS capabilities `integrations` block MUST NOT include `calendar` - ---- - -### Requirement: Sidebar Tab - -The system SHALL render a `CnCalendarTab` component inside `CnObjectSidebar` when (a) the calendar provider is enabled in the registry AND (b) the object's schema has `calendar` in `linkedTypes` AND (c) the rendering component does not exclude `calendar`. - -#### Scenario: Tab appears for schemas with calendar in linkedTypes - -- **GIVEN** an object whose schema has `configuration.linkedTypes: ["calendar", "files"]` -- **AND** the user has read access to the object -- **WHEN** `CnObjectSidebar` renders for the object -- **THEN** a tab labeled "Meetings" MUST appear with the Calendar icon -- **AND** the tab MUST contain the linked VEVENTs ordered by date ascending - -#### Scenario: Tab hidden for schemas without calendar in linkedTypes - -- **GIVEN** an object whose schema has `configuration.linkedTypes: ["files", "notes"]` (calendar absent) -- **WHEN** `CnObjectSidebar` renders for the object -- **THEN** no Meetings tab MUST appear - -#### Scenario: Inline meeting creation - -- **GIVEN** the Meetings tab is visible -- **WHEN** the user fills the inline form (date, time, summary, attendees) and clicks "Add" -- **THEN** the system MUST POST to `/api/objects/{register}/{schema}/{id}/calendar` with the form data -- **AND** the new VEVENT MUST be created in the user's first writable calendar -- **AND** the X-OPENREGISTER-* properties MUST link the VEVENT to the object -- **AND** the new meeting MUST appear in the tab list without a page reload - -#### Scenario: Unlink does not delete the VEVENT - -- **GIVEN** a linked meeting in the tab -- **WHEN** the user clicks "Unlink" -- **THEN** the system MUST DELETE the link -- **AND** the VEVENT MUST remain in the user's NC Calendar -- **AND** the meeting MUST disappear from the tab list - ---- - -### Requirement: Widget Across Surfaces - -The system SHALL render `CnCalendarCard` for every surface defined in the integration registry. The component SHALL receive `surface` as a prop and render appropriately for each. - -#### Scenario: User dashboard surface renders upcoming meetings - -- **GIVEN** the user has 12 upcoming meetings linked across various objects -- **WHEN** `CnCalendarCard` renders with `surface='user-dashboard'` -- **THEN** the next 5 upcoming meetings MUST be displayed -- **AND** each MUST link to its object's detail page - -#### Scenario: Detail page surface includes create CTA - -- **GIVEN** an object with linked meetings -- **WHEN** `CnCalendarCard` renders with `surface='detail-page'` and the object's id -- **THEN** the meeting list MUST be rendered -- **AND** an "Add meeting" CTA MUST be visible - -#### Scenario: Single-entity surface renders a chip - -- **GIVEN** a VEVENT id `event-uuid-123` -- **WHEN** `CnCalendarCard` renders with `surface='single-entity'` and `entityId='event-uuid-123'` -- **THEN** the component MUST fetch the single VEVENT and render it as a compact chip with date, summary, and status icon - ---- - -### Requirement: Reference-Property Auto-Rendering - -When a schema property declares `referenceType: 'calendar'`, frontend form/detail components SHALL render `CnCalendarCard` at `surface='single-entity'` for the referenced VEVENT id. - -#### Scenario: Detail grid renders calendar reference inline - -- **GIVEN** a schema with property `nextMeeting: { type: 'string', referenceType: 'calendar' }` -- **AND** an object with `nextMeeting: 'event-uuid-456'` -- **WHEN** `CnDetailGrid` renders the object -- **THEN** the `nextMeeting` row MUST contain `CnCalendarCard` with `entityId='event-uuid-456'` and `surface='single-entity'` -- **AND** raw VEVENT id MUST NOT be visible to the user - ---- - -### Requirement: Permission Inheritance - -The provider SHALL declare `requiresPermission(): null`. Access to the calendar integration SHALL be governed by (a) read access to the underlying object AND (b) NC Calendar's own access controls on the linked VEVENTs. - -#### Scenario: User without object read access does not see meetings - -- **GIVEN** a user with no read permission on object `abc-123` -- **WHEN** the user tries `GET /api/objects/{register}/{schema}/abc-123/calendar` -- **THEN** the system MUST return HTTP 403 (object permission denied, before integration check) - -#### Scenario: User with object access but restricted calendar still works - -- **GIVEN** a user with object read access AND a restricted view of NC Calendar (limited to one calendar) -- **WHEN** the user lists the object's meetings -- **THEN** only meetings on calendars the user can see MUST be returned (NC Calendar enforces this transitively) diff --git a/openspec/changes/integration-calendar/tasks.md b/openspec/changes/integration-calendar/tasks.md deleted file mode 100644 index cca1056801..0000000000 --- a/openspec/changes/integration-calendar/tasks.md +++ /dev/null @@ -1,59 +0,0 @@ -# Tasks: Integration — Calendar - -> **ADR-028 task-cap waiver**: this leaf has 26 tasks (cap is 15). The work is a single integration vertical slice (provider + sub-resource controller + tab + 4-surface widget + tests + nl/en). Splitting it would force interleaved depends_on chains that ship slower than one cohesive leaf. Hydra builders SHOULD batch this leaf across multiple turns. - -## Backend - -- [x] Create `lib/Service/Integration/Providers/CalendarProvider.php` extending `AbstractIntegrationProvider` - - getId() returns 'calendar' - - getLabel() returns translatable 'Meetings' - - getIcon() returns 'Calendar' - - getGroup() returns 'comms' - - getRequiredApp() returns 'calendar' - - getStorageStrategy() returns 'link-table' - - isEnabled() checks NC Calendar app availability - - list/get/create/update/delete delegate to CalendarEventService - - health() returns CalDAV status + auth (always 'none') -- [x] Register `CalendarProvider` as DI-tagged `IntegrationProvider` in `lib/AppInfo/Application.php` -- [ ] Unit test `tests/Unit/Service/Integration/Providers/CalendarProviderTest.php` — covers contract methods + delegation + isEnabled when app is missing - -## Frontend — Tab - -- [ ] Create `nextcloud-vue/src/components/CnCalendarTab/CnCalendarTab.vue` - - Lists linked VEVENTs ordered by date ascending - - Inline create form: date picker, time picker, summary, attendees (uses contacts integration if present, falls back to email input) - - Per-meeting actions: open in NC Calendar, unlink - - Empty state: "No meetings linked yet" -- [ ] Create `nextcloud-vue/src/components/CnCalendarTab/index.js` barrel -- [ ] Component test `nextcloud-vue/tests/components/CnCalendarTab.test.js` - -## Frontend — Widget - -- [ ] Create `nextcloud-vue/src/components/CnCalendarCard/CnCalendarCard.vue` - - Branches on `surface` prop: - - `user-dashboard`: next 5 upcoming meetings across all linked objects - - `app-dashboard`: meetings for objects in current app scope - - `detail-page`: this object's meetings + "Add meeting" CTA - - `single-entity`: chip with date + summary + status icon, accepts `entityId` prop -- [ ] Create `nextcloud-vue/src/components/CnCalendarCard/index.js` barrel -- [ ] Surface-specific component tests `nextcloud-vue/tests/components/CnCalendarCard.test.js` — one describe block per surface - -## Frontend — Registration - -- [ ] Create `nextcloud-vue/src/integrations/builtin/calendar.js` calling `OCA.OpenRegister.integrations.register({ id: 'calendar', label: t(...), icon: 'Calendar', group: 'comms', requiredApp: 'calendar', tab: CnCalendarTab, widget: CnCalendarCard, referenceType: 'calendar', defaultSize: { w: 4, h: 3 } })` -- [ ] Wire calendar.js into the registry's boot sequence in `nextcloud-vue/src/integrations/registry.js` -- [ ] Add exports to `nextcloud-vue/src/components/index.js` and `nextcloud-vue/src/index.js` - -## Quality - -- [ ] Parity gate passes locally (`scripts/check-integration-parity.sh`) -- [ ] All new strings translated to nl + en (l10n updates in both repos) -- [ ] PHPCS / PHPMD / PHPStan / Psalm strict pass on backend changes -- [ ] ESLint clean on frontend changes - -## Acceptance verification - -- [ ] End-to-end: install NC Calendar, log in, open an object whose schema has `linkedTypes: ["calendar"]`, see Meetings tab, create a meeting, verify it appears in NC Calendar app, unlink it, verify the VEVENT remains in NC Calendar -- [ ] Hide test: uninstall NC Calendar, verify Meetings tab disappears from sidebar and `calendar` is removed from OCS capabilities -- [ ] Reference-property test: schema with `nextMeeting: { type: 'string', referenceType: 'calendar' }` renders `single-entity` widget for the referenced VEVENT -- [ ] Backwards-compat: app using `` correctly hides the new tab diff --git a/openspec/changes/integration-collectives/design.md b/openspec/changes/integration-collectives/design.md deleted file mode 100644 index a9738cbb9b..0000000000 --- a/openspec/changes/integration-collectives/design.md +++ /dev/null @@ -1,46 +0,0 @@ -# Design: Integration — Collectives - -> Umbrella decisions apply -> -> **Cross-repo note**: file paths under `nextcloud-vue/src/...` or bare component names (`CnXxxTab`, `CnXxxCard`) are **expected locations** in the `@conduction/nextcloud-vue` shared library, not binding spec. The frontend implementation PR lands in that separate repo and MAY choose different paths. - -## Approach - -`CollectivesPageService` wraps Collectives REST API. Links store collective-id + page-id. Tab renders markdown client-side (via Collectives' own renderer where possible, or a standard markdown renderer with sanitization). - -## Architecture Decisions - -### AD-1: Link-only integration — no page creation - -**Decision**: Users link existing pages only; cannot create Collectives pages from OR. - -**Why**: Collectives pages have structural requirements (parent page, collective scope) that OR shouldn't abstract over. Linking existing is the common case. - -**Trade-off**: Small friction for "I want to document this case" workflow; user creates page in Collectives, then links. Acceptable. - -### AD-2: Detail-page surface renders inline page content - -**Decision**: Unlike most integrations where detail-page is a list, for Collectives the detail-page surface defaults to the most recently linked page's content inline (with tabs to switch if multiple are linked). - -**Why**: A procedure document or policy page is context the handler needs while working on the object. Inline beats list-then-click. - -**Trade-off**: Long pages consume detail-page real estate. User can collapse. - -## Files Affected - -### Backend (new) -- `CollectivesPageService`, `CollectivesController`, `CollectiveLink` entity + mapper + migration, `CollectivesProvider`, unit tests - -### Backend (modified) -- `Application.php`, `routes.php` - -### Frontend (new) -- `CnCollectivesTab/*`, `CnCollectivesCard/*`, `src/integrations/builtin/collectives.js`, barrels + tests - -## Risks - -| Risk | Mitigation | -|---|---| -| Markdown rendering differences vs Collectives native | Use a safe default renderer; acknowledge minor divergence (e.g., plugin syntax) | -| Page permissions vary per user | Collectives RBAC enforced transitively; inaccessible pages show "No access" placeholder | -| Long pages slow down detail-page surface | Collapsible with "Read more" link to Collectives | diff --git a/openspec/changes/integration-collectives/hydra.json b/openspec/changes/integration-collectives/hydra.json deleted file mode 100644 index b8a0b51df4..0000000000 --- a/openspec/changes/integration-collectives/hydra.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema_version": 2, - "spec_slug": "integration-collectives", - "app": "openregister", - "repo": "https://github.com/ConductionNL/openregister", - "depends_on": [ - "pluggable-integration-registry" - ], - "issue": "https://github.com/ConductionNL/openregister/issues/1317", - "pipeline": {}, - "cycles": [] -} diff --git a/openspec/changes/integration-collectives/specs/generic-integrations/spec.md b/openspec/changes/integration-collectives/specs/generic-integrations/spec.md deleted file mode 100644 index cf955d656c..0000000000 --- a/openspec/changes/integration-collectives/specs/generic-integrations/spec.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -status: proposed ---- - -# Integration: Collectives - -## Purpose - -Link NC Collectives (team wikis) pages to OR objects. Native alternative to the XWiki external integration. - -**Standards**: NC Collectives API, CommonMark/Markdown, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Collectives Provider Registration - -`CollectivesProvider` registered with id='collectives', group='docs', requiredApp='collectives', storage='link-table'. - -### Requirement: Link-Only (No Create) - -Integration SHALL support linking existing pages; page creation lives in Collectives. - -### Requirement: Markdown Preview in Tab - -Tab SHALL render page content via markdown (safe subset) with collapsible overflow. - -### Requirement: Detail-Page Surface Renders Inline Content - -Unlike other integrations, `CnCollectivesCard` at `surface='detail-page'` SHALL render the most-recent linked page's content inline. - -#### Scenario: One linked page renders in detail-page surface - -- **GIVEN** an object with one linked Collectives page -- **WHEN** `CnCollectivesCard` renders with `surface='detail-page'` -- **THEN** the page's markdown content MUST be rendered inline -- **AND** a "Read more" link MUST point to the page in Collectives - -#### Scenario: Multiple linked pages show tabs - -- **GIVEN** an object with 3 linked pages -- **WHEN** `CnCollectivesCard` renders with `surface='detail-page'` -- **THEN** 3 tabs MUST appear — one per page -- **AND** the most-recently-linked page MUST be selected by default - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'collectives'` SHALL render a page-title chip at single-entity surface. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; Collectives ACLs apply. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying page in NC Collectives is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Linked page's collective access revoked - -- **GIVEN** a user whose access to a collective was revoked after a page was linked -- **WHEN** `CnCollectivesTab` renders for that user -- **THEN** the inaccessible page MUST render a "No access to this page" placeholder -- **AND** the link record MUST NOT be removed (another user may still have access) diff --git a/openspec/changes/integration-collectives/specs/integration-collectives/spec.md b/openspec/changes/integration-collectives/specs/integration-collectives/spec.md deleted file mode 100644 index 37658fb7ae..0000000000 --- a/openspec/changes/integration-collectives/specs/integration-collectives/spec.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -status: proposed ---- - -# Integration: Collectives - -## Purpose - -Link NC Collectives (team wikis) pages to OR objects. Native alternative to the XWiki external integration. - -**Standards**: NC Collectives API, CommonMark/Markdown, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Collectives Provider Registration - -`CollectivesProvider` registered with id='collectives', group='docs', requiredApp='collectives', storage='link-table'. - -### Requirement: Link-Only (No Create) - -Integration SHALL support linking existing pages; page creation lives in Collectives. - -### Requirement: Markdown Preview in Tab - -Tab SHALL render page content via markdown (safe subset) with collapsible overflow. - -### Requirement: Detail-Page Surface Renders Inline Content - -Unlike other integrations, `CnCollectivesCard` at `surface='detail-page'` SHALL render the most-recent linked page's content inline. - -#### Scenario: One linked page renders in detail-page surface - -- **GIVEN** an object with one linked Collectives page -- **WHEN** `CnCollectivesCard` renders with `surface='detail-page'` -- **THEN** the page's markdown content MUST be rendered inline -- **AND** a "Read more" link MUST point to the page in Collectives - -#### Scenario: Multiple linked pages show tabs - -- **GIVEN** an object with 3 linked pages -- **WHEN** `CnCollectivesCard` renders with `surface='detail-page'` -- **THEN** 3 tabs MUST appear — one per page -- **AND** the most-recently-linked page MUST be selected by default - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'collectives'` SHALL render a page-title chip at single-entity surface. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; Collectives ACLs apply. diff --git a/openspec/changes/integration-collectives/tasks.md b/openspec/changes/integration-collectives/tasks.md deleted file mode 100644 index 7c5e6384be..0000000000 --- a/openspec/changes/integration-collectives/tasks.md +++ /dev/null @@ -1,36 +0,0 @@ -# Tasks: Integration — Collectives - -## Backend - -- [ ] `CollectiveLink` entity + mapper + migration -- [ ] `CollectivesPageService` wrapping Collectives REST API -- [ ] `CollectivesController` -- [x] `CollectivesProvider` — id='collectives', label='Knowledge', icon='BookOpenPageVariant', group='docs', requiredApp='collectives', storage='link-table' -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnCollectivesTab.vue` — list with markdown preview, link-existing (collective → page picker), unlink, "Open in Collectives" -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnCollectivesCard.vue`: - - `user-dashboard`: recent linked pages - - `app-dashboard`: scoped - - `detail-page`: inline page content (most recent) with multi-page tabs if >1 - - `single-entity`: page-title chip -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/collectives.js` — register with `referenceType: 'collectives'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: link an existing Collectives page, verify markdown renders in tab; detail-page inline render -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-contacts/specs/generic-integrations/spec.md b/openspec/changes/integration-contacts/specs/generic-integrations/spec.md deleted file mode 100644 index 685fb747ae..0000000000 --- a/openspec/changes/integration-contacts/specs/generic-integrations/spec.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -status: proposed ---- - -# Integration: Contacts - -## Purpose - -Surface NC Contacts linked to OR objects through the registry with first-class role support. The `single-entity` widget surface is the canonical person chip across all Conduction apps. - -**Standards**: RFC 6350 (vCard), CardDAV, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [nextcloud-entity-relations](../../../../specs/nextcloud-entity-relations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Contacts Provider Registration - -`ContactsProvider` registered with id='contacts', group='core', requiredApp='contacts', storage='link-table'. - -#### Scenario: Present when Contacts installed - -- **GIVEN** Contacts app installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the provider MUST be included - ---- - -### Requirement: Role-Grouped Tab Display - -The tab SHALL group linked contacts by role with section headers. - -#### Scenario: Tab groups by role - -- **GIVEN** an object with 3 contacts: Jan (role=applicant), Piet (role=handler), Klaas (no role) -- **WHEN** `CnContactsTab` renders -- **THEN** three sections MUST appear: "Applicants" (Jan), "Handlers" (Piet), "Other" (Klaas) - -#### Scenario: Link-with-role workflow - -- **WHEN** the user links an existing contact and selects role "advisor" -- **THEN** the link record MUST store `role='advisor'` -- **AND** the contact MUST appear under the "Advisors" section - ---- - -### Requirement: Reverse Lookup - -Clicking a contact SHALL open a flyout listing all OR objects (across all schemas the user can read) that have this contact linked. - -#### Scenario: Reverse lookup returns cross-schema results - -- **GIVEN** Jan is linked to object `case-1` (schema `case`) as applicant AND object `permit-7` (schema `permit`) as holder -- **WHEN** the user clicks Jan in the tab and opens reverse lookup -- **THEN** both objects MUST be listed with their schema + role annotations - ---- - -### Requirement: Canonical Person Chip on Single-Entity Surface - -`CnContactCard` at `surface='single-entity'` SHALL be the preferred rendering for schema properties of type person reference across all Conduction apps. - -#### Scenario: Single-entity chip shows avatar + name + hover details - -- **GIVEN** `entityId` pointing to a vCard -- **WHEN** `CnContactCard` renders with `surface='single-entity'` -- **THEN** the chip MUST include avatar (or initials fallback), display name, and hover-expanded details (email, phone) - ---- - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'contacts'` SHALL render the canonical person chip. - -#### Scenario: Person-reference property renders via widget - -- **GIVEN** schema `{assignedHandler: { type: 'string', referenceType: 'contacts' }}` and object `{assignedHandler: 'vcard-uuid-jan'}` -- **WHEN** `CnDetailGrid` renders the object -- **THEN** the row MUST contain `CnContactCard` with `entityId='vcard-uuid-jan'` and `surface='single-entity'` - ---- - -### Requirement: Permission Inheritance - -`ContactsProvider::requiresPermission()` SHALL return `null`. Access inherits from object RBAC + Contacts address book permissions. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying contact in NC Contacts is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Linked contact deleted from address book - -- **GIVEN** a link pointing to a vCard that was deleted from NC Contacts -- **WHEN** `CnContactsTab` renders -- **THEN** the role section MUST show a "Contact unavailable" placeholder for the orphaned link -- **AND** an admin-visible action MUST offer to clean up the stale link diff --git a/openspec/changes/integration-contacts/specs/integration-contacts/spec.md b/openspec/changes/integration-contacts/specs/integration-contacts/spec.md deleted file mode 100644 index 4b0fa979cb..0000000000 --- a/openspec/changes/integration-contacts/specs/integration-contacts/spec.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -status: proposed ---- - -# Integration: Contacts - -## Purpose - -Surface NC Contacts linked to OR objects through the registry with first-class role support. The `single-entity` widget surface is the canonical person chip across all Conduction apps. - -**Standards**: RFC 6350 (vCard), CardDAV, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [nextcloud-entity-relations](../../../../specs/nextcloud-entity-relations/spec.md) - ---- - -## Requirements - -### Requirement: Contacts Provider Registration - -`ContactsProvider` registered with id='contacts', group='core', requiredApp='contacts', storage='link-table'. - -#### Scenario: Present when Contacts installed - -- **GIVEN** Contacts app installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the provider MUST be included - ---- - -### Requirement: Role-Grouped Tab Display - -The tab SHALL group linked contacts by role with section headers. - -#### Scenario: Tab groups by role - -- **GIVEN** an object with 3 contacts: Jan (role=applicant), Piet (role=handler), Klaas (no role) -- **WHEN** `CnContactsTab` renders -- **THEN** three sections MUST appear: "Applicants" (Jan), "Handlers" (Piet), "Other" (Klaas) - -#### Scenario: Link-with-role workflow - -- **WHEN** the user links an existing contact and selects role "advisor" -- **THEN** the link record MUST store `role='advisor'` -- **AND** the contact MUST appear under the "Advisors" section - ---- - -### Requirement: Reverse Lookup - -Clicking a contact SHALL open a flyout listing all OR objects (across all schemas the user can read) that have this contact linked. - -#### Scenario: Reverse lookup returns cross-schema results - -- **GIVEN** Jan is linked to object `case-1` (schema `case`) as applicant AND object `permit-7` (schema `permit`) as holder -- **WHEN** the user clicks Jan in the tab and opens reverse lookup -- **THEN** both objects MUST be listed with their schema + role annotations - ---- - -### Requirement: Canonical Person Chip on Single-Entity Surface - -`CnContactCard` at `surface='single-entity'` SHALL be the preferred rendering for schema properties of type person reference across all Conduction apps. - -#### Scenario: Single-entity chip shows avatar + name + hover details - -- **GIVEN** `entityId` pointing to a vCard -- **WHEN** `CnContactCard` renders with `surface='single-entity'` -- **THEN** the chip MUST include avatar (or initials fallback), display name, and hover-expanded details (email, phone) - ---- - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'contacts'` SHALL render the canonical person chip. - -#### Scenario: Person-reference property renders via widget - -- **GIVEN** schema `{assignedHandler: { type: 'string', referenceType: 'contacts' }}` and object `{assignedHandler: 'vcard-uuid-jan'}` -- **WHEN** `CnDetailGrid` renders the object -- **THEN** the row MUST contain `CnContactCard` with `entityId='vcard-uuid-jan'` and `surface='single-entity'` - ---- - -### Requirement: Permission Inheritance - -`ContactsProvider::requiresPermission()` SHALL return `null`. Access inherits from object RBAC + Contacts address book permissions. diff --git a/openspec/changes/integration-contacts/tasks.md b/openspec/changes/integration-contacts/tasks.md deleted file mode 100644 index 500d6e2546..0000000000 --- a/openspec/changes/integration-contacts/tasks.md +++ /dev/null @@ -1,43 +0,0 @@ -# Tasks: Integration — Contacts - -> **ADR-028 task-cap waiver**: this leaf has 23 tasks (cap is 15). The work is a single integration vertical slice (provider + sub-resource controller + tab + 4-surface widget + tests + nl/en). Splitting it would force interleaved depends_on chains that ship slower than one cohesive leaf. Hydra builders SHOULD batch this leaf across multiple turns. - -## Backend - -- [x] Create `lib/Service/Integration/Providers/ContactsProvider.php` — id='contacts', label='Contacts', icon='AccountMultiple', group='core', requiredApp='contacts', storage='link-table' -- [ ] DI-tag in `Application.php` -- [ ] Unit test - -## Frontend — Tab - -- [ ] `CnContactsTab.vue` — role-grouped list (applicants, handlers, advisors, other); link-existing + create-new; reverse-lookup flyout -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnContactCard.vue`: - - `user-dashboard`: contacts linked across user's objects by most recent - - `app-dashboard`: scoped to app - - `detail-page`: full list with role-grouped sections - - `single-entity`: canonical person chip (avatar + name + role context + hover details) -- [ ] Shared vCard cache (reactive, keyed by uuid) for single-entity perf -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/contacts.js` — register with `referenceType: 'contacts'` -- [ ] Wire + barrels - -## Quality - -- [ ] Parity gate passes -- [ ] nl + en translations -- [ ] PHPCS/PHPMD/PHPStan/Psalm strict pass -- [ ] ESLint clean - -## Acceptance verification - -- [ ] E2E: install Contacts, link a person as "applicant", verify role-grouping in tab; reverse lookup returns linked objects -- [ ] Widget perf: detail grid with 20 person-reference properties shows ≤1 Mail API fetch per unique contact -- [ ] Hide test -- [ ] Reference-property test diff --git a/openspec/changes/integration-cospend/specs/generic-integrations/spec.md b/openspec/changes/integration-cospend/specs/generic-integrations/spec.md deleted file mode 100644 index ec4afeaa2c..0000000000 --- a/openspec/changes/integration-cospend/specs/generic-integrations/spec.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -status: proposed ---- - -# Integration: Cospend - -## Purpose - -Link NC Cospend projects/bills to OR objects through the registry for case cost tracking. - -**Standards**: NC Cospend API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Cospend Provider Registration - -`CospendProvider` registered with id='cospend', group='workflow', requiredApp='cospend', storage='link-table'. - -### Requirement: Project or Bill Link Types - -Link rows SHALL have a `link_type` of `project` or `bill`, not both hybrid. - -### Requirement: Same-Currency Aggregation Only - -Totals SHALL aggregate only bills in the same currency. Mixed-currency sets SHALL render per-currency totals side by side. - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`); the `single-entity` rendering is an amount chip. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'cospend'` SHALL render amount chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; Cospend ACLs apply. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying bill or project in NC Cospend is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Multiple currencies in the same link set - -- **GIVEN** an object linked to bills in EUR, USD, and GBP -- **WHEN** `CnCospendCard` renders with `surface='detail-page'` -- **THEN** three side-by-side totals MUST be rendered, one per currency -- **AND** no cross-currency aggregation MUST be attempted diff --git a/openspec/changes/integration-cospend/specs/integration-cospend/spec.md b/openspec/changes/integration-cospend/specs/integration-cospend/spec.md deleted file mode 100644 index b69995756e..0000000000 --- a/openspec/changes/integration-cospend/specs/integration-cospend/spec.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -status: proposed ---- - -# Integration: Cospend - -## Purpose - -Link NC Cospend projects/bills to OR objects through the registry for case cost tracking. - -**Standards**: NC Cospend API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Cospend Provider Registration - -`CospendProvider` registered with id='cospend', group='workflow', requiredApp='cospend', storage='link-table'. - -### Requirement: Project or Bill Link Types - -Link rows SHALL have a `link_type` of `project` or `bill`, not both hybrid. - -### Requirement: Same-Currency Aggregation Only - -Totals SHALL aggregate only bills in the same currency. Mixed-currency sets SHALL render per-currency totals side by side. - -### Requirement: Widget Surfaces - -Standard four; single-entity is amount chip. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'cospend'` SHALL render amount chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; Cospend ACLs apply. diff --git a/openspec/changes/integration-cospend/tasks.md b/openspec/changes/integration-cospend/tasks.md deleted file mode 100644 index 5af2d0dd4b..0000000000 --- a/openspec/changes/integration-cospend/tasks.md +++ /dev/null @@ -1,37 +0,0 @@ -# Tasks: Integration — Cospend - -## Backend - -- [ ] `CospendLink` entity + mapper + migration (with `link_type` = project|bill) -- [ ] `CospendService` wrapping Cospend REST API -- [ ] `CospendController` -- [x] `CospendProvider` — id='cospend', label='Costs', icon='CurrencyEur', group='workflow', requiredApp='cospend', storage='link-table' -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnCospendTab.vue` — linked projects/bills with totals, link/unlink, click-through to Cospend -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnCospendCard.vue`: - - `user-dashboard`: total spent across user's objects - - `app-dashboard`: scoped - - `detail-page`: total + per-bill list - - `single-entity`: amount chip -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/cospend.js` — register with `referenceType: 'cospend'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: link a Cospend project, verify total displays; unlink -- [ ] Currency test: linked bills in multiple currencies render separately -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-deck/design.md b/openspec/changes/integration-deck/design.md deleted file mode 100644 index 77ed6c7670..0000000000 --- a/openspec/changes/integration-deck/design.md +++ /dev/null @@ -1,60 +0,0 @@ -# Design: Integration — Deck - -> Umbrella decisions apply -> -> **Cross-repo note**: file paths under `nextcloud-vue/src/...` or bare component names (`CnXxxTab`, `CnXxxCard`) are **expected locations** in the `@conduction/nextcloud-vue` shared library, not binding spec. The frontend implementation PR lands in that separate repo and MAY choose different paths. - -## Approach - -`DeckProvider` wraps `DeckCardService`. Tab supports both create-new and link-existing (unlike email which is link-only). Widget on `detail-page` renders a compact stack-aware card. - -## Architecture Decisions - -### AD-1: Default board selection is sticky per schema - -**Decision**: The first time a user creates a Deck card from an object of schema X, the chosen board+stack is saved as schema-level default. Subsequent create-card affordances pre-select it. - -**Why**: In real workflows, all objects of a schema (e.g., "case") go to the same board. Asking every time is friction. Schema-level stickiness is the right granularity — not user (personal bias) or app (too broad). - -**Trade-off**: Cross-user stickiness means a second user's first create uses the first user's choice. Acceptable — the choice is board+stack, which is a shared resource. - -### AD-2: Mini-kanban on detail-page surface - -**Decision**: `CnDeckCard` at `surface='detail-page'` shows a three-column compact kanban (stacks that contain the linked card, or the two most-active stacks on the card's board). The linked card is highlighted in its current stack. - -**Why**: Seeing "where is this card in the workflow?" is the most valuable Deck information for a case handler. The mini-kanban gives context in one glance without leaving the object. - -**Trade-off**: Slightly more complex than a flat list. Payoff is high — this is the kind of visualisation that makes Deck useful for case work. - -## Files Affected - -### New files — Backend - -| File | Purpose | -|---|---| -| `lib/Service/Integration/Providers/DeckProvider.php` | Wraps `DeckCardService` | -| `tests/Unit/Service/Integration/Providers/DeckProviderTest.php` | Unit test | - -### Modified — Backend - -| File | Change | -|---|---| -| `lib/AppInfo/Application.php` | DI-tag `DeckProvider` | -| `lib/Service/SettingsService.php` | Persist schema-level default board+stack (new key `deck.defaultBoard.{schemaSlug}`) | - -### New files — Frontend - -| File | Purpose | -|---|---| -| `CnDeckTab/CnDeckTab.vue` | List + create-new + link-existing | -| `CnDeckCard/CnDeckCard.vue` | 4-surface widget with mini-kanban on detail-page | -| `src/integrations/builtin/deck.js` | Registration | -| Barrels + tests | - -## Risks - -| Risk | Mitigation | -|---|---| -| User has no boards | Service returns empty list; tab shows "Create a board in Nextcloud Deck first" with link | -| Deck API internal classes unstable across NC versions | Existing `DeckCardService` already absorbs this; wrapper doesn't re-introduce coupling | -| Mini-kanban perf on boards with 500+ cards | Server-side stack-scoped fetch, limit to visible-range cards | diff --git a/openspec/changes/integration-deck/hydra.json b/openspec/changes/integration-deck/hydra.json deleted file mode 100644 index aeac18480e..0000000000 --- a/openspec/changes/integration-deck/hydra.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema_version": 2, - "spec_slug": "integration-deck", - "app": "openregister", - "repo": "https://github.com/ConductionNL/openregister", - "depends_on": [ - "pluggable-integration-registry" - ], - "issue": "https://github.com/ConductionNL/openregister/issues/1310", - "pipeline": {}, - "cycles": [] -} diff --git a/openspec/changes/integration-deck/specs/generic-integrations/spec.md b/openspec/changes/integration-deck/specs/generic-integrations/spec.md deleted file mode 100644 index 2ccb6b3bb6..0000000000 --- a/openspec/changes/integration-deck/specs/generic-integrations/spec.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -status: proposed ---- - -# Integration: Deck - -## Purpose - -Surface NC Deck cards linked to OR objects through the registry. Supports create-new and link-existing; includes a mini-kanban widget for detail pages. - -**Standards**: NC Deck (internal service classes, per AD-5 of `nextcloud-entity-relations`), ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [nextcloud-entity-relations](../../../../specs/nextcloud-entity-relations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Deck Provider Registration - -`DeckProvider` registered with id='deck', group='workflow', requiredApp='deck', storage='link-table'. - -#### Scenario: Present when Deck installed - -- **GIVEN** Deck app installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** provider MUST be included - -#### Scenario: Hidden when Deck missing - -- **GIVEN** Deck app not installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** provider MUST NOT be included - ---- - -### Requirement: Sidebar Tab — Create and Link - -Tab SHALL support both creating new cards and linking existing cards. Creating SHALL use sticky schema-level default board+stack. - -#### Scenario: Create new card uses sticky default - -- **GIVEN** a user previously created a card on board B stack S for schema `case` -- **WHEN** the user opens the create form on another `case` object -- **THEN** board B and stack S MUST be pre-selected in the form - -#### Scenario: Link existing card - -- **WHEN** the user picks an existing card via the board+stack+card picker -- **THEN** a link record MUST be created in `openregister_deck_links` -- **AND** the card MUST appear in the tab list - -#### Scenario: Unlink preserves the card in Deck - -- **WHEN** the user unlinks a card -- **THEN** the link MUST be removed -- **AND** the Deck card MUST remain unchanged - ---- - -### Requirement: Mini-Kanban on Detail-Page Surface - -`CnDeckCard` at `surface='detail-page'` SHALL render a compact kanban view of the linked card's board with the card highlighted in its current stack. - -#### Scenario: Mini-kanban highlights current stack - -- **GIVEN** an object with one linked Deck card currently in stack "In Progress" -- **WHEN** `CnDeckCard` renders with `surface='detail-page'` -- **THEN** a kanban view MUST show the card's stacks -- **AND** the linked card MUST be visually highlighted in "In Progress" - ---- - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'deck'` SHALL render `CnDeckCard` at `surface='single-entity'`. - ---- - -### Requirement: Permission Inheritance - -`DeckProvider::requiresPermission()` SHALL return `null`. Deck ACLs govern per-board access transitively. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying Deck card in NC Deck is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Linked card archived in Deck - -- **GIVEN** a linked card that was archived in Deck (not deleted) -- **WHEN** `CnDeckCard` renders -- **THEN** the card MUST display with a visible "Archived" badge -- **AND** link operations (unlink, reassign board) MUST remain available diff --git a/openspec/changes/integration-deck/specs/integration-deck/spec.md b/openspec/changes/integration-deck/specs/integration-deck/spec.md deleted file mode 100644 index 33e3fda78c..0000000000 --- a/openspec/changes/integration-deck/specs/integration-deck/spec.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -status: proposed ---- - -# Integration: Deck - -## Purpose - -Surface NC Deck cards linked to OR objects through the registry. Supports create-new and link-existing; includes a mini-kanban widget for detail pages. - -**Standards**: NC Deck (internal service classes, per AD-5 of `nextcloud-entity-relations`), ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [nextcloud-entity-relations](../../../../specs/nextcloud-entity-relations/spec.md) - ---- - -## Requirements - -### Requirement: Deck Provider Registration - -`DeckProvider` registered with id='deck', group='workflow', requiredApp='deck', storage='link-table'. - -#### Scenario: Present when Deck installed - -- **GIVEN** Deck app installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** provider MUST be included - -#### Scenario: Hidden when Deck missing - -- **GIVEN** Deck app not installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** provider MUST NOT be included - ---- - -### Requirement: Sidebar Tab — Create and Link - -Tab SHALL support both creating new cards and linking existing cards. Creating SHALL use sticky schema-level default board+stack. - -#### Scenario: Create new card uses sticky default - -- **GIVEN** a user previously created a card on board B stack S for schema `case` -- **WHEN** the user opens the create form on another `case` object -- **THEN** board B and stack S MUST be pre-selected in the form - -#### Scenario: Link existing card - -- **WHEN** the user picks an existing card via the board+stack+card picker -- **THEN** a link record MUST be created in `openregister_deck_links` -- **AND** the card MUST appear in the tab list - -#### Scenario: Unlink preserves the card in Deck - -- **WHEN** the user unlinks a card -- **THEN** the link MUST be removed -- **AND** the Deck card MUST remain unchanged - ---- - -### Requirement: Mini-Kanban on Detail-Page Surface - -`CnDeckCard` at `surface='detail-page'` SHALL render a compact kanban view of the linked card's board with the card highlighted in its current stack. - -#### Scenario: Mini-kanban highlights current stack - -- **GIVEN** an object with one linked Deck card currently in stack "In Progress" -- **WHEN** `CnDeckCard` renders with `surface='detail-page'` -- **THEN** a kanban view MUST show the card's stacks -- **AND** the linked card MUST be visually highlighted in "In Progress" - ---- - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'deck'` SHALL render `CnDeckCard` at `surface='single-entity'`. - ---- - -### Requirement: Permission Inheritance - -`DeckProvider::requiresPermission()` SHALL return `null`. Deck ACLs govern per-board access transitively. diff --git a/openspec/changes/integration-deck/tasks.md b/openspec/changes/integration-deck/tasks.md deleted file mode 100644 index a912112698..0000000000 --- a/openspec/changes/integration-deck/tasks.md +++ /dev/null @@ -1,43 +0,0 @@ -# Tasks: Integration — Deck - -> **ADR-028 task-cap waiver**: this leaf has 25 tasks (cap is 15). The work is a single integration vertical slice (provider + sub-resource controller + tab + 4-surface widget + tests + nl/en). Splitting it would force interleaved depends_on chains that ship slower than one cohesive leaf. Hydra builders SHOULD batch this leaf across multiple turns. - -## Backend - -- [x] Create `lib/Service/Integration/Providers/DeckProvider.php` — id='deck', label='Cards', icon='ViewColumn', group='workflow', requiredApp='deck', storage='link-table' -- [ ] DI-tag in `Application.php` -- [ ] Extend `SettingsService` to persist schema-level default board+stack -- [ ] Unit test - -## Frontend — Tab - -- [ ] `CnDeckTab.vue` — list linked cards, "Create new card" inline form (board+stack with sticky default), "Link existing card" picker, unlink -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnDeckCard.vue`: - - `user-dashboard`: cards assigned to current user - - `app-dashboard`: cards on objects in this app's scope - - `detail-page`: mini-kanban with linked card highlighted - - `single-entity`: chip with card title + stack name + assignees -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/deck.js` — register with `referenceType: 'deck'` -- [ ] Wire + barrels - -## Quality - -- [ ] Parity gate passes -- [ ] nl + en translations -- [ ] PHPCS/PHPMD/PHPStan/Psalm strict pass -- [ ] ESLint clean - -## Acceptance verification - -- [ ] E2E: install Deck, create card from OR object, verify in Deck app; link existing; unlink -- [ ] Sticky default: second create on same schema pre-selects previous board+stack -- [ ] Hide test: uninstall Deck → integration hidden -- [ ] Reference-property `referenceType: 'deck'` renders single-entity widget diff --git a/openspec/changes/integration-email/design.md b/openspec/changes/integration-email/design.md deleted file mode 100644 index 6c251fb30e..0000000000 --- a/openspec/changes/integration-email/design.md +++ /dev/null @@ -1,58 +0,0 @@ -# Design: Integration — Email - -> Umbrella decisions apply -> -> **Cross-repo note**: file paths under `nextcloud-vue/src/...` or bare component names (`CnXxxTab`, `CnXxxCard`) are **expected locations** in the `@conduction/nextcloud-vue` shared library, not binding spec. The frontend implementation PR lands in that separate repo and MAY choose different paths. Leaf-specific choices only. - -## Approach - -Thin `EmailProvider` wrapping `EmailService`. Tab focused on list + link (no compose). Widget surfaces reuse one `CnEmailCard` component with internal `surface` branching. - -## Architecture Decisions - -### AD-1: Tab offers "Link existing", not "Send new" - -**Decision**: The tab surfaces a picker (account → folder → message) for linking. No compose form. - -**Why**: Mail app owns SMTP (AD-2 of `nextcloud-entity-relations`). Adding compose duplicates logic and diverges from Mail's UX. n8n handles automated email. - -**Trade-off**: Users wanting to compose-and-link in one step open Mail, send, then return and link the sent message. Acceptable friction for a reference-only integration. - -### AD-2: Cached subject/sender/date rendered without Mail API calls - -**Decision**: `CnEmailCard` at dashboard surfaces uses the cached columns (`subject`, `sender`, `date`) from `openregister_email_links` — no per-render Mail API call. Detail-page and single-entity surfaces fetch the full message on demand for body preview. - -**Why**: Dashboards render many cards; O(N) Mail API calls would be slow. The cached fields support the summary rendering; only expanded views need the live fetch. - -**Trade-off**: Subject/sender can go stale if the message is modified in Mail. Rare in practice (messages are immutable in IMAP); refresh-on-link-time is sufficient. - -## Files Affected - -### New files — Backend - -| File | Purpose | -|---|---| -| `lib/Service/Integration/Providers/EmailProvider.php` | Wraps `EmailService` | -| `tests/Unit/Service/Integration/Providers/EmailProviderTest.php` | Unit test | - -### Modified files — Backend - -| File | Change | -|---|---| -| `lib/AppInfo/Application.php` | DI-tag `EmailProvider` | - -### New files — Frontend - -| File | Purpose | -|---|---| -| `nextcloud-vue/src/components/CnEmailTab/CnEmailTab.vue` | List + Link-existing picker | -| `nextcloud-vue/src/components/CnEmailCard/CnEmailCard.vue` | 4-surface widget | -| `nextcloud-vue/src/integrations/builtin/email.js` | Registration | -| Plus barrels + tests | - -## Risks - -| Risk | Mitigation | -|---|---| -| Mail account with thousands of messages makes the picker slow | Server-side pagination; search-as-you-type on subject/sender before list fetch | -| Mail app API schema drift | Adapter layer in `EmailService` already absorbs this; provider sits above adapter | diff --git a/openspec/changes/integration-email/hydra.json b/openspec/changes/integration-email/hydra.json deleted file mode 100644 index 30f3d43187..0000000000 --- a/openspec/changes/integration-email/hydra.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema_version": 2, - "spec_slug": "integration-email", - "app": "openregister", - "repo": "https://github.com/ConductionNL/openregister", - "depends_on": [ - "pluggable-integration-registry" - ], - "issue": "https://github.com/ConductionNL/openregister/issues/1309", - "pipeline": {}, - "cycles": [] -} diff --git a/openspec/changes/integration-email/specs/generic-integrations/spec.md b/openspec/changes/integration-email/specs/generic-integrations/spec.md deleted file mode 100644 index 7f69ea789c..0000000000 --- a/openspec/changes/integration-email/specs/generic-integrations/spec.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -status: proposed ---- - -# Integration: Email - -## Purpose - -Surface Nextcloud Mail messages linked to OR objects through the registry. Link-only integration — Mail owns send/compose. - -**Standards**: Nextcloud Mail API, ADR-019 (Integration Registry) -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [nextcloud-entity-relations](../../../../specs/nextcloud-entity-relations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Email Provider Registration - -The system SHALL ship `EmailProvider` registered as DI-tagged `IntegrationProvider` with id `email`, group `comms`, requiredApp `mail`, storage `link-table`. - -#### Scenario: Provider present when Mail is installed - -- **GIVEN** NC Mail app installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the result MUST include provider with `id='email'`, `group='comms'`, `storageStrategy='link-table'` - -#### Scenario: Provider hidden when Mail is missing - -- **GIVEN** NC Mail app not installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the result MUST NOT include the email provider - ---- - -### Requirement: Sidebar Tab — List and Link - -The tab SHALL list linked emails by date descending and SHALL provide a "Link existing email" affordance. The tab SHALL NOT provide compose/send. - -#### Scenario: Link an existing email via picker - -- **GIVEN** the tab is open and the user has Mail accounts configured -- **WHEN** the user clicks "Link existing email", selects an account and folder, and picks a message -- **THEN** the system MUST POST to `/api/objects/{register}/{schema}/{id}/email` with `{mailAccountId, mailMessageId}` -- **AND** cached subject/sender/date MUST be populated from the Mail API at link time -- **AND** the email MUST appear in the tab list without reload - -#### Scenario: Unlink preserves the email - -- **WHEN** the user unlinks a message -- **THEN** the link record MUST be deleted -- **AND** the Mail message MUST NOT be touched - ---- - -### Requirement: Widget Across Surfaces - -`CnEmailCard` SHALL render on all four surfaces. Dashboard surfaces MUST use cached columns (no per-render Mail API call); detail/single-entity surfaces MAY fetch live. - -#### Scenario: Dashboard surface uses cached fields - -- **GIVEN** 50 linked emails on user dashboard -- **WHEN** `CnEmailCard` renders with `surface='user-dashboard'` -- **THEN** NO Mail API calls MUST be made — only link-table data used - -#### Scenario: Single-entity surface renders chip - -- **GIVEN** `entityId=` -- **WHEN** `CnEmailCard` renders with `surface='single-entity'` -- **THEN** a chip with subject + sender + date MUST be rendered - ---- - -### Requirement: Reference-Property Auto-Rendering - -Schema property `referenceType: 'email'` SHALL render `CnEmailCard` at `surface='single-entity'`. - -#### Scenario: Detail grid renders email reference inline - -- **GIVEN** schema `{originalRequest: { type: 'string', referenceType: 'email' }}` and object with `originalRequest: 'msg-id-42'` -- **WHEN** `CnDetailGrid` renders -- **THEN** the `originalRequest` row MUST contain `CnEmailCard` with `entityId='msg-id-42'` - ---- - -### Requirement: Permission Inheritance - -`EmailProvider::requiresPermission()` SHALL return `null`. Access inherits from object RBAC + Mail app access (user sees only emails in accounts they control). - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying email in Nextcloud Mail is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Linked message deleted from Mail - -- **GIVEN** a link to a Mail message that was moved to trash and purged -- **WHEN** `GET /api/objects/.../email/{linkId}` is called -- **THEN** the response MUST be HTTP 404 per the umbrella Error-Handling Contract (`ProviderNotFoundException`) -- **AND** the list endpoint MUST filter the orphaned record out rather than 404-ing the whole list diff --git a/openspec/changes/integration-email/specs/integration-email/spec.md b/openspec/changes/integration-email/specs/integration-email/spec.md deleted file mode 100644 index 60acc06f21..0000000000 --- a/openspec/changes/integration-email/specs/integration-email/spec.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -status: proposed ---- - -# Integration: Email - -## Purpose - -Surface Nextcloud Mail messages linked to OR objects through the registry. Link-only integration — Mail owns send/compose. - -**Standards**: Nextcloud Mail API, ADR-019 (Integration Registry) -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [nextcloud-entity-relations](../../../../specs/nextcloud-entity-relations/spec.md) - ---- - -## Requirements - -### Requirement: Email Provider Registration - -The system SHALL ship `EmailProvider` registered as DI-tagged `IntegrationProvider` with id `email`, group `comms`, requiredApp `mail`, storage `link-table`. - -#### Scenario: Provider present when Mail is installed - -- **GIVEN** NC Mail app installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the result MUST include provider with `id='email'`, `group='comms'`, `storageStrategy='link-table'` - -#### Scenario: Provider hidden when Mail is missing - -- **GIVEN** NC Mail app not installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the result MUST NOT include the email provider - ---- - -### Requirement: Sidebar Tab — List and Link - -The tab SHALL list linked emails by date descending and SHALL provide a "Link existing email" affordance. The tab SHALL NOT provide compose/send. - -#### Scenario: Link an existing email via picker - -- **GIVEN** the tab is open and the user has Mail accounts configured -- **WHEN** the user clicks "Link existing email", selects an account and folder, and picks a message -- **THEN** the system MUST POST to `/api/objects/{register}/{schema}/{id}/email` with `{mailAccountId, mailMessageId}` -- **AND** cached subject/sender/date MUST be populated from the Mail API at link time -- **AND** the email MUST appear in the tab list without reload - -#### Scenario: Unlink preserves the email - -- **WHEN** the user unlinks a message -- **THEN** the link record MUST be deleted -- **AND** the Mail message MUST NOT be touched - ---- - -### Requirement: Widget Across Surfaces - -`CnEmailCard` SHALL render on all four surfaces. Dashboard surfaces MUST use cached columns (no per-render Mail API call); detail/single-entity surfaces MAY fetch live. - -#### Scenario: Dashboard surface uses cached fields - -- **GIVEN** 50 linked emails on user dashboard -- **WHEN** `CnEmailCard` renders with `surface='user-dashboard'` -- **THEN** NO Mail API calls MUST be made — only link-table data used - -#### Scenario: Single-entity surface renders chip - -- **GIVEN** `entityId=` -- **WHEN** `CnEmailCard` renders with `surface='single-entity'` -- **THEN** a chip with subject + sender + date MUST be rendered - ---- - -### Requirement: Reference-Property Auto-Rendering - -Schema property `referenceType: 'email'` SHALL render `CnEmailCard` at `surface='single-entity'`. - -#### Scenario: Detail grid renders email reference inline - -- **GIVEN** schema `{originalRequest: { type: 'string', referenceType: 'email' }}` and object with `originalRequest: 'msg-id-42'` -- **WHEN** `CnDetailGrid` renders -- **THEN** the `originalRequest` row MUST contain `CnEmailCard` with `entityId='msg-id-42'` - ---- - -### Requirement: Permission Inheritance - -`EmailProvider::requiresPermission()` SHALL return `null`. Access inherits from object RBAC + Mail app access (user sees only emails in accounts they control). diff --git a/openspec/changes/integration-email/tasks.md b/openspec/changes/integration-email/tasks.md deleted file mode 100644 index 4304cc742b..0000000000 --- a/openspec/changes/integration-email/tasks.md +++ /dev/null @@ -1,39 +0,0 @@ -# Tasks: Integration — Email - -## Backend - -- [x] Create `lib/Service/Integration/Providers/EmailProvider.php` — id='email', label='Emails', icon='Email', group='comms', requiredApp='mail', storage='link-table', delegates to `EmailService` -- [ ] DI-tag `EmailProvider` in `lib/AppInfo/Application.php` -- [ ] Unit test `tests/Unit/Service/Integration/Providers/EmailProviderTest.php` - -## Frontend — Tab - -- [ ] Create `CnEmailTab/CnEmailTab.vue` — list ordered by date desc; "Link existing email" picker (account → folder → message with subject/sender search); unlink action -- [ ] Barrel + component tests - -## Frontend — Widget - -- [ ] Create `CnEmailCard/CnEmailCard.vue` branching on `surface`: - - `user-dashboard`: latest 5 linked emails across all objects - - `app-dashboard`: scoped to current app's objects - - `detail-page`: full list + "Link existing" CTA - - `single-entity`: chip with subject + sender + date -- [ ] Barrel + surface-specific tests - -## Registration - -- [ ] `src/integrations/builtin/email.js` — register with `referenceType: 'email'` -- [ ] Wire into registry boot + barrels - -## Quality - -- [ ] Parity gate passes -- [ ] nl + en translations for all new strings -- [ ] PHPCS/PHPMD/PHPStan/Psalm strict pass -- [ ] ESLint clean - -## Acceptance verification - -- [ ] E2E: install Mail, link a message to an object, verify it shows in the tab, unlink, verify message is preserved in Mail -- [ ] Hide test: uninstall Mail, verify integration hidden from registry + UI + capabilities -- [ ] Reference-property test: schema with `relatedEmail: { referenceType: 'email' }` renders single-entity widget diff --git a/openspec/changes/integration-flow/design.md b/openspec/changes/integration-flow/design.md deleted file mode 100644 index 94aa788342..0000000000 --- a/openspec/changes/integration-flow/design.md +++ /dev/null @@ -1,44 +0,0 @@ -# Design: Integration — Flow - -> Umbrella decisions apply -> -> **Cross-repo note**: file paths under `nextcloud-vue/src/...` or bare component names (`CnXxxTab`, `CnXxxCard`) are **expected locations** in the `@conduction/nextcloud-vue` shared library, not binding spec. The frontend implementation PR lands in that separate repo and MAY choose different paths. - -## Approach - -`FlowService` wraps `workflowengine` Manager. Rules are stored in NC Flow's own tables; OR's link table maps rules to schemas/objects for discovery. Fire events are read from NC's event log. - -## Architecture Decisions - -### AD-1: Scoping via schema, not per-object - -**Decision**: Default scope is schema (rule X applies to all objects of schema Y). Per-object rules are discouraged but supported. - -**Why**: Flow rules are declarative and static. Most cases are "every incoming zaak triggers notification X" (schema-scoped). Per-object flow rules are edge-case and tend to be mis-designed as data-driven logic. - -### AD-2: Coexistence with OR workflow engine - -**Decision**: Tab has two sections: "NC Flow rules" (from workflowengine) and "OR workflow rules" (from OR's own engine). Unified display, separate configurations. - -**Why**: Both engines exist; pretending one owns the space would confuse users. Unified display clarifies the distinction. - -**Trade-off**: Admins must understand both systems. Mitigated by clear labelling. - -## Files Affected - -### Backend (new) -- `FlowService`, `FlowController`, `FlowLink` entity + mapper + migration, `FlowProvider`, unit tests - -### Backend (modified) -- `Application.php`, `routes.php` - -### Frontend (new) -- `CnFlowTab/*`, `CnFlowCard/*`, `src/integrations/builtin/flow.js`, barrels + tests - -## Risks - -| Risk | Mitigation | -|---|---| -| NC Flow event log isn't reliable for "recent events" at scale | Fallback to "last 24h" window; acknowledge limitation | -| Users confuse NC Flow with OR workflow engine | Clear labelling; link documentation explaining when to use which | -| Flow rules fire unexpectedly | OR doesn't change firing logic; this is observation-only | diff --git a/openspec/changes/integration-flow/hydra.json b/openspec/changes/integration-flow/hydra.json deleted file mode 100644 index e97c4a256e..0000000000 --- a/openspec/changes/integration-flow/hydra.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema_version": 2, - "spec_slug": "integration-flow", - "app": "openregister", - "repo": "https://github.com/ConductionNL/openregister", - "depends_on": [ - "pluggable-integration-registry" - ], - "issue": "https://github.com/ConductionNL/openregister/issues/1320", - "pipeline": {}, - "cycles": [] -} diff --git a/openspec/changes/integration-flow/specs/generic-integrations/spec.md b/openspec/changes/integration-flow/specs/generic-integrations/spec.md deleted file mode 100644 index 7e2a269a48..0000000000 --- a/openspec/changes/integration-flow/specs/generic-integrations/spec.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -status: proposed ---- - -# Integration: Flow - -## Purpose - -Link NC Flow (workflowengine) rules to schemas/objects and surface recent fire events through the registry. - -**Standards**: NC workflowengine, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Flow Provider Registration - -`FlowProvider` registered with id='flow', group='workflow', requiredApp='workflowengine', storage='link-table'. - -### Requirement: Schema-Scoped Linking (Default) - -Default link scope SHALL be schema (all objects of the schema trigger the linked rule). Per-object linking SHALL be supported but discouraged in UI. - -### Requirement: Coexistence with OR Workflow Engine - -Tab SHALL show two clearly-labelled sections: "NC Flow rules" and "OR workflow rules". - -### Requirement: Recent Events Panel - -Tab SHALL display recent fire events for linked rules within a configurable window (default 7 days). - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`); the `detail-page` rendering shows linked rules + recent events. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'flow'` SHALL render rule chip. - -### Requirement: Admin-Gated Permission Semantics - -`FlowProvider::requiresPermission()` SHALL return the literal string `'admin'`. In OR's `AuthorizationService` mapping, `'admin'` resolves to **"the current user is a member of the Nextcloud admin group"** (i.e. `IGroupManager::isAdmin($userId) === true`). It is NOT an OR-internal role string and NOT a per-object permission. - -The Flow integration is hidden by **two independent gates**: - -1. **App gate** (`isEnabled()` / `getRequiredApp()`): hides the integration if `workflowengine` is disabled at the NC instance level. When `workflowengine` is disabled, the integration is filtered out at stage 1 of the visibility filter for all users — admins included. -2. **Permission gate** (`requiresPermission(): 'admin'`): when `workflowengine` is enabled, the integration is filtered out at stage 1 for non-admin users. Admins see it. - -The two gates are independent — disabling `workflowengine` hides the tab even from admins; enabling it exposes the tab to admins only. - -#### Scenario: workflowengine disabled — tab hidden from everyone - -- **GIVEN** the NC `workflowengine` app is disabled instance-wide -- **WHEN** `CnObjectSidebar` renders for any user (admin or not) -- **THEN** no Flow tab MUST appear -- **AND** `/api/integrations/flow` MUST return HTTP 404 (integration not registered, distinct from 403) - -#### Scenario: workflowengine enabled, non-admin user — tab hidden via permission gate - -- **GIVEN** `workflowengine` is enabled -- **AND** the current user is not in the NC admin group -- **WHEN** `CnObjectSidebar` renders -- **THEN** no Flow tab MUST appear -- **AND** `/api/integrations/flow` MUST return HTTP 403 for the user - -#### Scenario: workflowengine enabled, admin user — tab visible - -- **GIVEN** `workflowengine` is enabled -- **AND** the current user is a member of the NC admin group -- **WHEN** `CnObjectSidebar` renders for an object whose schema lists `flow` in `linkedTypes` -- **THEN** the Flow tab MUST appear - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying flow rule in NC Flow (workflowengine) is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Flow rule deleted while linked - -- **GIVEN** a flow rule link whose underlying rule was deleted in NC Flow admin -- **WHEN** `CnFlowTab` renders -- **THEN** the row MUST render a "Rule deleted" placeholder with the former rule name (from cache) -- **AND** the "recent events" panel MUST continue to show historical fires from the NC event log diff --git a/openspec/changes/integration-flow/specs/integration-flow/spec.md b/openspec/changes/integration-flow/specs/integration-flow/spec.md deleted file mode 100644 index 9a546051f7..0000000000 --- a/openspec/changes/integration-flow/specs/integration-flow/spec.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -status: proposed ---- - -# Integration: Flow - -## Purpose - -Link NC Flow (workflowengine) rules to schemas/objects and surface recent fire events through the registry. - -**Standards**: NC workflowengine, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Flow Provider Registration - -`FlowProvider` registered with id='flow', group='workflow', requiredApp='workflowengine', storage='link-table'. - -### Requirement: Schema-Scoped Linking (Default) - -Default link scope SHALL be schema (all objects of the schema trigger the linked rule). Per-object linking SHALL be supported but discouraged in UI. - -### Requirement: Coexistence with OR Workflow Engine - -Tab SHALL show two clearly-labelled sections: "NC Flow rules" and "OR workflow rules". - -### Requirement: Recent Events Panel - -Tab SHALL display recent fire events for linked rules within a configurable window (default 7 days). - -### Requirement: Widget Surfaces - -Standard four; detail-page shows linked rules + recent events. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'flow'` SHALL render rule chip. - -### Requirement: Permission Inheritance - -`FlowProvider::requiresPermission()` SHALL return `'admin'` — only admins see flow rules (NC Flow admin-gated). - -#### Scenario: Non-admin user does not see Flow tab - -- **GIVEN** a non-admin user viewing an object -- **WHEN** `CnObjectSidebar` renders -- **THEN** no Flow tab MUST appear -- **AND** `/api/integrations/flow` MUST return HTTP 403 for the user diff --git a/openspec/changes/integration-flow/tasks.md b/openspec/changes/integration-flow/tasks.md deleted file mode 100644 index 2255352e2a..0000000000 --- a/openspec/changes/integration-flow/tasks.md +++ /dev/null @@ -1,36 +0,0 @@ -# Tasks: Integration — Flow - -## Backend - -- [ ] `FlowLink` entity + mapper + migration (schema/object → flow rule id) -- [ ] `FlowService` — read NC Flow rules via workflowengine Manager, read fire events, CRUD on links -- [ ] `FlowController` sub-resource endpoints -- [x] `FlowProvider` — id='flow', label='Automation', icon='RobotOutline', group='workflow', requiredApp='workflowengine', storage='link-table' -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnFlowTab.vue` — two sections (NC Flow + OR workflow rules), recent-events panel, link/unlink, "Open in NC settings" link-out -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnFlowCard.vue`: - - `user-dashboard`: recent fires affecting user's objects - - `app-dashboard`: scoped - - `detail-page`: linked rules + recent events panel - - `single-entity`: rule name + last-fire chip -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/flow.js` — register with `referenceType: 'flow'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: link a flow rule to a schema, verify tab display; recent-events panel populates after fire -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-forms/specs/generic-integrations/spec.md b/openspec/changes/integration-forms/specs/generic-integrations/spec.md deleted file mode 100644 index 87b18fa57d..0000000000 --- a/openspec/changes/integration-forms/specs/generic-integrations/spec.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -status: proposed ---- - -# Integration: Forms - -## Purpose - -Link NC Forms responses and form-mappings to OR objects. Supports ad-hoc response linking and form-mapping for auto-linking future responses. - -**Standards**: NC Forms API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Forms Provider Registration - -`FormsProvider` registered with id='forms', group='workflow', requiredApp='forms', storage='link-table'. - -### Requirement: Two Link Modes - -The integration SHALL support (a) linking individual responses and (b) form-mapping for auto-linking future responses. - -#### Scenario: Individual response link - -- **WHEN** user picks an existing response and clicks "Link" -- **THEN** `openregister_form_links` row type `response_link` MUST be created -- **AND** the response MUST appear in the tab - -#### Scenario: Form-mapping auto-link - -- **GIVEN** a form-mapping exists for form F on schema S with object-selector O -- **WHEN** a user submits a response to form F -- **THEN** a post-submit hook MUST resolve the object via selector O -- **AND** link the response to that object automatically - -### Requirement: Read-Only Response View - -Response rendering SHALL be read-only. Editing delegates to NC Forms. - -### Requirement: Widget Across Surfaces - -`CnFormsCard` renders on all four surfaces with appropriate density per surface. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'forms'` SHALL render `CnFormsCard` at `surface='single-entity'`. - -### Requirement: Permission Inheritance - -`FormsProvider::requiresPermission()` SHALL return `null`. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying form or response in NC Forms is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Form deleted but response links exist - -- **GIVEN** a form was deleted from NC Forms after responses were linked to OR objects -- **WHEN** `CnFormsTab` renders for an affected object -- **THEN** linked response rows MUST render with "Form deleted" context rather than empty labels -- **AND** a reconciliation action MUST be available to admins diff --git a/openspec/changes/integration-forms/specs/integration-forms/spec.md b/openspec/changes/integration-forms/specs/integration-forms/spec.md deleted file mode 100644 index 2d59544218..0000000000 --- a/openspec/changes/integration-forms/specs/integration-forms/spec.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -status: proposed ---- - -# Integration: Forms - -## Purpose - -Link NC Forms responses and form-mappings to OR objects. Supports ad-hoc response linking and form-mapping for auto-linking future responses. - -**Standards**: NC Forms API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Forms Provider Registration - -`FormsProvider` registered with id='forms', group='workflow', requiredApp='forms', storage='link-table'. - -### Requirement: Two Link Modes - -The integration SHALL support (a) linking individual responses and (b) form-mapping for auto-linking future responses. - -#### Scenario: Individual response link - -- **WHEN** user picks an existing response and clicks "Link" -- **THEN** `openregister_form_links` row type `response_link` MUST be created -- **AND** the response MUST appear in the tab - -#### Scenario: Form-mapping auto-link - -- **GIVEN** a form-mapping exists for form F on schema S with object-selector O -- **WHEN** a user submits a response to form F -- **THEN** a post-submit hook MUST resolve the object via selector O -- **AND** link the response to that object automatically - -### Requirement: Read-Only Response View - -Response rendering SHALL be read-only. Editing delegates to NC Forms. - -### Requirement: Widget Across Surfaces - -`CnFormsCard` renders on all four surfaces with appropriate density per surface. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'forms'` SHALL render `CnFormsCard` at `surface='single-entity'`. - -### Requirement: Permission Inheritance - -`FormsProvider::requiresPermission()` SHALL return `null`. diff --git a/openspec/changes/integration-forms/tasks.md b/openspec/changes/integration-forms/tasks.md deleted file mode 100644 index 3f903f82c8..0000000000 --- a/openspec/changes/integration-forms/tasks.md +++ /dev/null @@ -1,40 +0,0 @@ -# Tasks: Integration — Forms - -## Backend - -- [ ] Create `FormLink` entity + `FormLinkMapper` + migration for `openregister_form_links` -- [ ] Create `FormResponseService` wrapping Forms REST API (list responses, link/unlink, form-mapping management) -- [ ] Create `FormResponsesController` with sub-resource endpoints -- [x] Create `FormsProvider` — id='forms', label='Forms', icon='ClipboardText', group='workflow', requiredApp='forms', storage='link-table' -- [ ] DI-tag in `Application.php` -- [ ] Add routes to `appinfo/routes.php` -- [ ] Unit tests for service + provider - -## Frontend — Tab - -- [ ] `CnFormsTab.vue` — linked responses list, "Link response" and "Map form for future responses" affordances, read-only response viewer -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnFormsCard.vue`: - - `user-dashboard`: recent response count - - `app-dashboard`: scoped - - `detail-page`: responses list with inline question/answer preview - - `single-entity`: chip with form name + submitted-at -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/forms.js` — register with `referenceType: 'forms'` -- [ ] Wire + barrels - -## Quality - -- [ ] Parity gate passes; nl+en translations; PHPCS/PHPMD/PHPStan/Psalm strict; ESLint clean - -## Acceptance verification - -- [ ] E2E: install Forms, link a response, verify display -- [ ] Form-mapping: configure a form mapping, submit a response, verify auto-link -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-kvk-opencorporates/proposal.md b/openspec/changes/integration-kvk-opencorporates/proposal.md new file mode 100644 index 0000000000..73f9b96d22 --- /dev/null +++ b/openspec/changes/integration-kvk-opencorporates/proposal.md @@ -0,0 +1,75 @@ +--- +kind: code +status: proposed +--- + +## Why + +pipelinq holds two bespoke company-lookup HTTP clients — +`OCA\Pipelinq\Service\KvkApiClient` (KvK Handelsregister Zoeken API) and +`OCA\Pipelinq\Service\OpenCorporatesApiClient` (OpenCorporates v0.4) — each +carrying its own base URL (admin-tunable app-config) and API key. Both are +generic REST/JSON GET integrations (KvK: an `apikey` request header; +OpenCorporates: an `api_token` query parameter) with no mTLS/PKI and no +app-specific transport logic — exactly the shape OR's external-leaf model +already owns for xWiki / OpenProject. + +Per ADR-022 (apps consume OR abstractions), the connection + credentials +should live once in OpenConnector, and OR should expose a thin lookup +endpoint pipelinq re-points to — instead of every consuming app +re-implementing the base-URL + key plumbing. Today OR has no KvK / +OpenCorporates leaf, so there is nothing to re-point to. + +## What Changes + +- **`KvkProvider`** (`storage='external'`, `getOpenConnectorSource()='kvk'`) + — a stateless, read-only company-lookup leaf mirroring `XwikiProvider`'s + OpenConnector wiring. `list()` (registry read-path) free-text-searches via + the KvK `/zoeken` surface; two public methods add `lookupByKvkNumber()` + (GET a company by KvK number) and `searchCompanies()` (free-text + KvK + criteria). Every call routes through `ExternalIntegrationRouter` → the + OpenConnector `kvk` source; failures degrade to `{ unavailable, cause }`. +- **`OpenCorporatesProvider`** (`storage='external'`, + `getOpenConnectorSource()='opencorporates'`) — the same shape; `list()` + + `searchCompanies()` hit the OpenCorporates `/companies/search` surface + (optional `jurisdiction`). +- **`CompanyLookupController`** + routes exposing the read-only endpoints a + consuming app calls: + - `GET /api/integrations/kvk/company?kvkNumber=` — KvK lookup by number, + - `GET /api/integrations/kvk/search?q=` — KvK free-text search, + - `GET /api/integrations/opencorporates/search?q=` — OpenCorporates search. + All `@NoAdminRequired`, read-only; a degraded provider result is relayed as + `503 { error, details: { cause } }` so consumers render the 4-state banner. +- **DI + registry**: both providers are registered as services and added to + the builtin integration-provider boot list, exactly where `XwikiProvider` + is registered. +- The leaves round-trip the **raw upstream JSON** rows. The Dutch→prospect + (KvK) and company→prospect (OpenCorporates) field mapping stays in the + consuming app (pipelinq `KvkResultMapper` / `OpenCorporatesResultMapper`) — + this leaf owns the connection, not the domain shape. + +The matching OpenConnector change (`seed-kvk-opencorporates-sources`) seeds +the dormant `kvk` + `opencorporates` sources these providers resolve. + +## Capabilities + +### Added Capabilities +- `integration-company-lookup`: OR exposes read-only KvK + OpenCorporates + company-lookup endpoints, backed by external OpenConnector-routed leaves, + that degrade null-safely on a missing/down source. + +## Impact + +- **Code:** `lib/Service/Integration/Providers/KvkProvider.php`, + `lib/Service/Integration/Providers/OpenCorporatesProvider.php`, + `lib/Controller/CompanyLookupController.php`, `appinfo/routes.php`, + `lib/AppInfo/Application.php` (DI + boot list), unit tests. +- **Behaviour:** with the dormant sources seeded (no key), the endpoints + return `503 upstream-service-down` rather than `source-missing`; with a key + + enabled source they return live KvK / OpenCorporates JSON. +- **Consumers:** pipelinq (future) re-points `KvkApiClient` → + `GET /api/integrations/kvk/{company,search}` and + `OpenCorporatesApiClient` → `GET /api/integrations/opencorporates/search`, + dropping `pipelinq.kvk.api_base_url` / `pipelinq.opencorporates.api_base_url` + and the per-app API keys (now on the OpenConnector sources). +- **Secrets:** none in OR — credentials live on the OpenConnector sources. diff --git a/openspec/changes/integration-kvk-opencorporates/specs/integration-company-lookup/spec.md b/openspec/changes/integration-kvk-opencorporates/specs/integration-company-lookup/spec.md new file mode 100644 index 0000000000..cdbe3a93b2 --- /dev/null +++ b/openspec/changes/integration-kvk-opencorporates/specs/integration-company-lookup/spec.md @@ -0,0 +1,91 @@ +# integration-company-lookup + +## ADDED Requirements + +### Requirement: KvK Company Lookup + +OpenRegister SHALL expose read-only KvK Handelsregister company-lookup +endpoints backed by a `KvkProvider` leaf (`storage='external'`, +`getOpenConnectorSource()='kvk'`). The KvK base URL + API key SHALL be +resolved exclusively from the OpenConnector `kvk` source via +`ExternalIntegrationRouter` — the caller never supplies a URL or key. The +endpoints SHALL be `@NoAdminRequired` (any authenticated user) and read-only. + +`GET /api/integrations/kvk/company?kvkNumber=` SHALL look up a single company +by its KvK number and return `{ results, total }` of raw KvK rows. +`GET /api/integrations/kvk/search?q=` SHALL free-text-search the KvK +Handelsregister (optionally scoped by `plaats` / `type` / `sbiHoofdActiviteit`) +and return `{ results, total, limit, page }`. `limit` SHALL be clamped to +1..100. The endpoints SHALL round-trip the raw KvK JSON rows; the +Dutch→prospect field mapping is the consuming app's responsibility. + +When the OpenConnector `kvk` source is missing/unconfigured, or the upstream +KvK API is unreachable, the endpoints SHALL fail closed — returning +`503 { error, details: { cause } }` (cause one of `openconnector-down`, +`openconnector-source-missing`, `provider-auth`, `upstream-service-down`) — +and SHALL never raise a fatal. The provider methods SHALL return +`{ unavailable, cause, results: [], total: 0 }` in that case. + +#### Scenario: KvK lookup by number returns the company + +- GIVEN the OpenConnector `kvk` source is configured and enabled +- WHEN a client calls `GET /api/integrations/kvk/company?kvkNumber=69599084` +- THEN the response is `200 { results, total }` carrying the raw KvK rows for + that number +- @e2e exclude Backend integration endpoint — verified by PHPUnit (router mocked) + a live OpenConnector source, not a browser flow. + +#### Scenario: KvK lookup degrades when the source is missing + +- GIVEN no OpenConnector `kvk` source exists (or OpenConnector is disabled) +- WHEN a client calls `GET /api/integrations/kvk/company?kvkNumber=69599084` +- THEN the response is `503 { error, details: { cause } }` with cause + `openconnector-source-missing` (or `openconnector-down`), and no fatal is raised +- @e2e exclude Backend degraded path — verified by PHPUnit + a live source-missing probe, not a browser flow. + +#### Scenario: KvK lookup degrades when KvK is unreachable + +- GIVEN the `kvk` source is seeded but dormant/unauthenticated (no API key) +- WHEN a client calls `GET /api/integrations/kvk/search?q=Acme` +- THEN the response is `503 { error, details: { cause } }` with cause + `upstream-service-down` (or `provider-auth`), and no fatal is raised +- @e2e exclude Backend degraded path — verified against the real-but-unauthed KvK API, not a browser flow. + +### Requirement: OpenCorporates Company Search + +OpenRegister SHALL expose a read-only OpenCorporates company-search endpoint +backed by an `OpenCorporatesProvider` leaf (`storage='external'`, +`getOpenConnectorSource()='opencorporates'`). The OpenCorporates base URL + +API token SHALL be resolved exclusively from the OpenConnector +`opencorporates` source via `ExternalIntegrationRouter`. The endpoint SHALL be +`@NoAdminRequired` and read-only. + +`GET /api/integrations/opencorporates/search?q=` SHALL free-text-search the +OpenCorporates register (optionally scoped by `jurisdiction`) and return +`{ results, total, limit, page }` of raw company rows (the +`results.companies[].company` envelope unwrapped to a flat list). `limit` SHALL +be clamped to 1..100. The company→prospect field mapping is the consuming +app's responsibility. + +When the OpenConnector `opencorporates` source is missing/unconfigured, or the +upstream OpenCorporates API is unreachable, the endpoint SHALL fail closed — +returning `503 { error, details: { cause } }` (cause one of `openconnector-down`, +`openconnector-source-missing`, `provider-auth`, `upstream-service-down`) — and +SHALL never raise a fatal. + +#### Scenario: OpenCorporates search returns matching companies + +- GIVEN the OpenConnector `opencorporates` source is configured and enabled +- WHEN a client calls `GET /api/integrations/opencorporates/search?q=Acme&jurisdiction=nl` +- THEN the response is `200 { results, total, limit, page }` carrying the raw + OpenCorporates company rows +- @e2e exclude Backend integration endpoint — verified by PHPUnit (router mocked) + a live source, not a browser flow. + +#### Scenario: OpenCorporates search degrades on a missing/down source + +- GIVEN no OpenConnector `opencorporates` source exists, or it is seeded but + dormant/unauthenticated +- WHEN a client calls `GET /api/integrations/opencorporates/search?q=Acme` +- THEN the response is `503 { error, details: { cause } }` (cause + `openconnector-source-missing` when absent, `upstream-service-down` / + `provider-auth` when dormant), and no fatal is raised +- @e2e exclude Backend degraded path — verified by PHPUnit + a live probe, not a browser flow. diff --git a/openspec/changes/integration-kvk-opencorporates/tasks.md b/openspec/changes/integration-kvk-opencorporates/tasks.md new file mode 100644 index 0000000000..2dac254f20 --- /dev/null +++ b/openspec/changes/integration-kvk-opencorporates/tasks.md @@ -0,0 +1,38 @@ +# Tasks — integration-kvk-opencorporates + +## 1. Providers + +- [x] 1.1 Add `KvkProvider` (`storage='external'`, `SOURCE_ID='kvk'`, + group `external`, requiredApp `openconnector`, auth external/apikey) + mirroring `XwikiProvider`'s OpenConnector wiring. +- [x] 1.2 `KvkProvider::list()` free-text-searches via the KvK `/zoeken` + surface (object-independent: register/schema/objectId ignored, + `_search` carries the query); unwrap the `resultaten` envelope. +- [x] 1.3 `KvkProvider::lookupByKvkNumber()` (GET by KvK number) + + `searchCompanies()` (free-text + KvK criteria), both degrading + null-safe to `{ unavailable, cause }`. +- [x] 1.4 Add `OpenCorporatesProvider` (`SOURCE_ID='opencorporates'`) with + `list()` + `searchCompanies()` (optional jurisdiction) hitting + `/companies/search`; unwrap `results.companies[].company`. + +## 2. Controller + routes + +- [x] 2.1 Add `CompanyLookupController` (`@NoAdminRequired`, read-only) with + `kvkCompany()`, `kvkSearch()`, `openCorporatesSearch()`. +- [x] 2.2 Relay a degraded provider result as `503 { error, details: { cause } }`. +- [x] 2.3 Routes: `GET /api/integrations/kvk/company`, + `GET /api/integrations/kvk/search`, + `GET /api/integrations/opencorporates/search`. + +## 3. Wiring + +- [x] 3.1 Register both providers as DI services next to `XwikiProvider`. +- [x] 3.2 Add both to the builtin integration-provider boot list. + +## 4. Tests + verify + +- [x] 4.1 Unit tests: metadata, auth, isEnabled, router delegation + + envelope unwrap, and the 4-state degraded contract for both providers. +- [x] 4.2 Live: with the dormant `kvk`/`opencorporates` sources seeded, + confirm the endpoints return `503 upstream-service-down` (not + `source-missing`); without the sources, `source-missing`. diff --git a/openspec/changes/integration-maps/specs/generic-integrations/spec.md b/openspec/changes/integration-maps/specs/generic-integrations/spec.md deleted file mode 100644 index 32b32646d6..0000000000 --- a/openspec/changes/integration-maps/specs/generic-integrations/spec.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -status: proposed ---- - -# Integration: Maps - -## Purpose - -Link geolocations to OR objects through the registry with cached lat/lon for performant rendering. - -**Standards**: NC Maps API, Leaflet, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Maps Provider Registration - -`MapsProvider` registered with id='maps', group='docs', requiredApp='maps', storage='link-table'. - -### Requirement: Cached lat/lon - -Link table SHALL include `lat`, `lon`, `address`, `address_source`. Rendering SHALL NOT call geocoding. - -#### Scenario: Rendering uses cached fields - -- **GIVEN** 20 linked locations on a dashboard -- **WHEN** `CnMapCard` renders -- **THEN** NO geocoding API calls MUST be made - -### Requirement: Two Add Flows - -Users SHALL be able to add a location by (a) entering an address (geocoded) or (b) clicking on the embedded map. - -### Requirement: Single-Entity is Address Chip - -`surface='single-entity'` SHALL render an address chip (not an inline map). Click expands to a map popover. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'maps'` SHALL render the address chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; object + Maps ACLs apply. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying location in NC Maps is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Geocoding service unavailable during add - -- **GIVEN** user adds a location by address and the Nominatim service is unavailable -- **WHEN** geocoding fails -- **THEN** the UI MUST offer "Place on map" as a fallback (user clicks the map) -- **AND** the link record MUST persist with `address_source='click-placed'` and the entered address text diff --git a/openspec/changes/integration-maps/specs/integration-maps/spec.md b/openspec/changes/integration-maps/specs/integration-maps/spec.md deleted file mode 100644 index 3165b0a664..0000000000 --- a/openspec/changes/integration-maps/specs/integration-maps/spec.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -status: proposed ---- - -# Integration: Maps - -## Purpose - -Link geolocations to OR objects through the registry with cached lat/lon for performant rendering. - -**Standards**: NC Maps API, Leaflet, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Maps Provider Registration - -`MapsProvider` registered with id='maps', group='docs', requiredApp='maps', storage='link-table'. - -### Requirement: Cached lat/lon - -Link table SHALL include `lat`, `lon`, `address`, `address_source`. Rendering SHALL NOT call geocoding. - -#### Scenario: Rendering uses cached fields - -- **GIVEN** 20 linked locations on a dashboard -- **WHEN** `CnMapCard` renders -- **THEN** NO geocoding API calls MUST be made - -### Requirement: Two Add Flows - -Users SHALL be able to add a location by (a) entering an address (geocoded) or (b) clicking on the embedded map. - -### Requirement: Single-Entity is Address Chip - -`surface='single-entity'` SHALL render an address chip (not an inline map). Click expands to a map popover. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'maps'` SHALL render the address chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; object + Maps ACLs apply. diff --git a/openspec/changes/integration-maps/tasks.md b/openspec/changes/integration-maps/tasks.md deleted file mode 100644 index 10e3f779f9..0000000000 --- a/openspec/changes/integration-maps/tasks.md +++ /dev/null @@ -1,36 +0,0 @@ -# Tasks: Integration — Maps - -## Backend - -- [ ] `MapLink` entity + mapper (with `lat`/`lon`/`address`/`address_source` columns) + migration -- [ ] `MapLocationService` — geocode (via Maps), reverse-geocode, CRUD -- [ ] `MapsController` with sub-resource endpoints -- [x] `MapsProvider` — id='maps', label='Location', icon='MapMarker', group='docs', requiredApp='maps', storage='link-table' -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnMapTab.vue` — address-list + embedded Leaflet map; add-location flows (by address, by map click); unlink -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnMapCard.vue`: - - `user-dashboard`: address list - - `app-dashboard`: scoped - - `detail-page`: mini-map with pins - - `single-entity`: address chip, click-expands to mini-map popover -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/maps.js` — register with `referenceType: 'maps'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: add address to object (geocoded), verify pin on mini-map; add via map click; unlink -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-openproject/specs/generic-integrations/spec.md b/openspec/changes/integration-openproject/specs/generic-integrations/spec.md deleted file mode 100644 index 11efe3604a..0000000000 --- a/openspec/changes/integration-openproject/specs/generic-integrations/spec.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -status: proposed ---- - -# Integration: OpenProject - -## Purpose - -Link OpenProject work packages to OR objects through the registry via OpenConnector external routing. First external-service integration proving the umbrella's `storage='external'` path. - -**Standards**: OpenProject REST API v3, OAuth 2.0, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [integration-xwiki](../../../integration-xwiki/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: OpenProject Provider Registration - -`OpenProjectProvider` registered with id='openproject', group='external', requiredApp=null, storage='external', `getOpenConnectorSource()='openproject'`. - -#### Scenario: Provider present when OpenConnector source exists - -- **GIVEN** OpenConnector has an `openproject` source with valid OAuth2 credentials -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the provider MUST be included - -#### Scenario: Provider hidden when source missing - -- **GIVEN** no OpenConnector source named `openproject` -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the provider MUST NOT be included - -### Requirement: Auth Requirements Declaration - -`authRequirements()` SHALL return `{type: 'oauth2', configSchema: {...}}` with the OpenProject-specific fields. - -### Requirement: External Routing - -All CRUD SHALL route through `ExternalIntegrationRouter` to OpenConnector. No local link table SHALL store WP metadata beyond request-scope cache. - -#### Scenario: List linked WPs routes through OpenConnector - -- **WHEN** `GET /api/objects/{register}/{schema}/{id}/openproject` is called -- **THEN** `ExternalIntegrationRouter` MUST resolve the `openproject` OpenConnector source -- **AND** MUST invoke OpenConnector's list operation with object context -- **AND** the response MUST be returned to the caller unchanged - -### Requirement: Auth Expiry Surfaces Clearly - -When OpenConnector reports `authStatus: 'expired'`, the tab SHALL display an explicit "Reconnect" banner, not silently 401. - -#### Scenario: Expired token surfaces banner - -- **GIVEN** OAuth token expired -- **WHEN** the tab loads -- **THEN** a banner with "Authorisation expired — reconnect" MUST be shown -- **AND** clicking MUST link to OpenConnector's credential management for the source - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`); the dashboard surfaces show open WPs assigned to the user, and `single-entity` is a WP chip with status badge. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'openproject'` SHALL render WP chip. - -### Requirement: OCS Capabilities Includes Auth Status - -Capabilities response SHALL include `authStatus` in the `openproject` integrations entry. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; OpenProject's own ACLs govern per-WP visibility transitively. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying work package in OpenProject (via OpenConnector) is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: OpenConnector source returns rate-limit error - -- **GIVEN** a burst of list requests to OpenProject exceeds its rate limit -- **WHEN** OpenConnector surfaces a 429 -- **THEN** the integration MUST render a "Rate-limited, retrying…" state -- **AND** MUST back off and retry per OpenConnector's retry policy (not reimplemented in OR) diff --git a/openspec/changes/integration-openproject/specs/integration-openproject/spec.md b/openspec/changes/integration-openproject/specs/integration-openproject/spec.md deleted file mode 100644 index c46294ffb1..0000000000 --- a/openspec/changes/integration-openproject/specs/integration-openproject/spec.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -status: proposed ---- - -# Integration: OpenProject - -## Purpose - -Link OpenProject work packages to OR objects through the registry via OpenConnector external routing. First external-service integration proving the umbrella's `storage='external'` path. - -**Standards**: OpenProject REST API v3, OAuth 2.0, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [integration-xwiki](../../../integration-xwiki/specs/integration-xwiki/spec.md) - ---- - -## Requirements - -### Requirement: OpenProject Provider Registration - -`OpenProjectProvider` registered with id='openproject', group='external', requiredApp=null, storage='external', `getOpenConnectorSource()='openproject'`. - -#### Scenario: Provider present when OpenConnector source exists - -- **GIVEN** OpenConnector has an `openproject` source with valid OAuth2 credentials -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the provider MUST be included - -#### Scenario: Provider hidden when source missing - -- **GIVEN** no OpenConnector source named `openproject` -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** the provider MUST NOT be included - -### Requirement: Auth Requirements Declaration - -`authRequirements()` SHALL return `{type: 'oauth2', configSchema: {...}}` with the OpenProject-specific fields. - -### Requirement: External Routing - -All CRUD SHALL route through `ExternalIntegrationRouter` to OpenConnector. No local link table SHALL store WP metadata beyond request-scope cache. - -#### Scenario: List linked WPs routes through OpenConnector - -- **WHEN** `GET /api/objects/{register}/{schema}/{id}/openproject` is called -- **THEN** `ExternalIntegrationRouter` MUST resolve the `openproject` OpenConnector source -- **AND** MUST invoke OpenConnector's list operation with object context -- **AND** the response MUST be returned to the caller unchanged - -### Requirement: Auth Expiry Surfaces Clearly - -When OpenConnector reports `authStatus: 'expired'`, the tab SHALL display an explicit "Reconnect" banner, not silently 401. - -#### Scenario: Expired token surfaces banner - -- **GIVEN** OAuth token expired -- **WHEN** the tab loads -- **THEN** a banner with "Authorisation expired — reconnect" MUST be shown -- **AND** clicking MUST link to OpenConnector's credential management for the source - -### Requirement: Widget Surfaces - -Standard four; dashboard shows open WPs assigned to user; single-entity is WP chip with status badge. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'openproject'` SHALL render WP chip. - -### Requirement: OCS Capabilities Includes Auth Status - -Capabilities response SHALL include `authStatus` in the `openproject` integrations entry. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; OpenProject's own ACLs govern per-WP visibility transitively. diff --git a/openspec/changes/integration-openproject/tasks.md b/openspec/changes/integration-openproject/tasks.md deleted file mode 100644 index 03168e0fd5..0000000000 --- a/openspec/changes/integration-openproject/tasks.md +++ /dev/null @@ -1,47 +0,0 @@ -# Tasks: Integration — OpenProject - -> **ADR-028 task-cap waiver**: this leaf has 24 tasks (cap is 15). The work is a single external-integration vertical slice (provider + OpenConnector source template + tab + 4-surface widget + tests + nl/en). Splitting it would force interleaved depends_on chains that ship slower than one cohesive leaf. Hydra builders SHOULD batch this leaf across multiple turns. - -## Umbrella coordination - -- [ ] Add `getOpenConnectorSource(): ?string` to umbrella `IntegrationProvider` interface (tiny umbrella PR) -- [x] `AbstractIntegrationProvider` default returns `null` - -## Backend - -- [x] Create `lib/Service/Integration/Providers/OpenProjectProvider.php` — id='openproject', label='Projects', icon='Briefcase', group='external', requiredApp=null, storage='external', `getOpenConnectorSource()` returns 'openproject' -- [x] Declare auth requirements `{type: 'oauth2', configSchema: {url, client_id, client_secret, scope}}` -- [x] Delegate CRUD to `ExternalIntegrationRouter` (from umbrella) -- [ ] Ship OpenConnector source config template `config/openconnector-sources/openproject.yaml` -- [ ] DI-tag -- [ ] Unit tests (OpenConnector client mocked) - -## Frontend — Tab - -- [ ] `CnOpenProjectTab.vue` — linked WP list with status/assignee/progress badges, link-by-id, link-by-URL, unlink, auth-expired banner -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnOpenProjectCard.vue`: - - `user-dashboard`: open WPs assigned to user across linked objects - - `app-dashboard`: scoped - - `detail-page`: full WP list with status - - `single-entity`: WP chip with status badge -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/openproject.js` — register with `referenceType: 'openproject'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: configure OpenConnector openproject source, link a WP, verify status in tab -- [ ] Auth test: revoke OAuth, verify tab shows expired state with reconnect link -- [ ] Hide test: delete OpenConnector source, verify integration hidden -- [ ] Reference-property test: schema `{wp: { type: 'string', referenceType: 'openproject' }}` renders chip -- [ ] OCS capabilities includes `openproject` with `authStatus` when source present diff --git a/openspec/changes/integration-photos/specs/generic-integrations/spec.md b/openspec/changes/integration-photos/specs/generic-integrations/spec.md deleted file mode 100644 index bb9a05e30c..0000000000 --- a/openspec/changes/integration-photos/specs/generic-integrations/spec.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -status: proposed ---- - -# Integration: Photos - -## Purpose - -Surface image attachments on OR objects with photo-specific UX (grid, lightbox, EXIF) as a filtered view of the Files integration. - -**Standards**: EXIF 2.3, NC Photos API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Photos Provider Registration - -`PhotosProvider` registered with id='photos', group='docs', requiredApp='photos', storage='link-table'. - -### Requirement: Photos is Filtered Files View - -Photos SHALL share the `openregister_file_links` table with Files; filtering is by MIME type at query time. - -#### Scenario: Photo visible in both tabs - -- **GIVEN** an object with a linked JPEG file -- **WHEN** user opens both Files tab and Photos tab -- **THEN** the same file MUST appear in both - -### Requirement: Lazy EXIF - -EXIF SHALL be extracted on first photo view per file and cached in the link row. - -### Requirement: Optional GPS Stripping - -Admin setting SHALL allow stripping GPS data from EXIF at link time. Default OFF (opt-in). - -#### Scenario: GPS strip setting removes GPS data - -- **GIVEN** GPS-strip setting enabled -- **WHEN** a photo is linked to an object -- **THEN** the stored `exif_metadata` MUST NOT contain GPS coordinates -- **AND** the original file MUST NOT be modified - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`) with grid/strip appropriate to each. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'photos'` SHALL render thumbnail chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; file permissions apply. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying photo in NC Photos is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: HEIC format without preview support - -- **GIVEN** a linked photo in HEIC format and the server has no HEIC preview provider -- **WHEN** `CnPhotosTab` grid renders -- **THEN** a format-unsupported placeholder MUST be shown with the filename -- **AND** download MUST still work diff --git a/openspec/changes/integration-photos/specs/integration-photos/spec.md b/openspec/changes/integration-photos/specs/integration-photos/spec.md deleted file mode 100644 index 6e2f61a9c5..0000000000 --- a/openspec/changes/integration-photos/specs/integration-photos/spec.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -status: proposed ---- - -# Integration: Photos - -## Purpose - -Surface image attachments on OR objects with photo-specific UX (grid, lightbox, EXIF) as a filtered view of the Files integration. - -**Standards**: EXIF 2.3, NC Photos API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Photos Provider Registration - -`PhotosProvider` registered with id='photos', group='docs', requiredApp='photos', storage='link-table'. - -### Requirement: Photos is Filtered Files View - -Photos SHALL share the `openregister_file_links` table with Files; filtering is by MIME type at query time. - -#### Scenario: Photo visible in both tabs - -- **GIVEN** an object with a linked JPEG file -- **WHEN** user opens both Files tab and Photos tab -- **THEN** the same file MUST appear in both - -### Requirement: Lazy EXIF - -EXIF SHALL be extracted on first photo view per file and cached in the link row. - -### Requirement: Optional GPS Stripping - -Admin setting SHALL allow stripping GPS data from EXIF at link time. Default OFF (opt-in). - -#### Scenario: GPS strip setting removes GPS data - -- **GIVEN** GPS-strip setting enabled -- **WHEN** a photo is linked to an object -- **THEN** the stored `exif_metadata` MUST NOT contain GPS coordinates -- **AND** the original file MUST NOT be modified - -### Requirement: Widget Surfaces - -Standard four surfaces with grid/strip appropriate to each. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'photos'` SHALL render thumbnail chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; file permissions apply. diff --git a/openspec/changes/integration-photos/tasks.md b/openspec/changes/integration-photos/tasks.md deleted file mode 100644 index d17ae721df..0000000000 --- a/openspec/changes/integration-photos/tasks.md +++ /dev/null @@ -1,38 +0,0 @@ -# Tasks: Integration — Photos - -## Backend - -- [ ] Migration: add `exif_metadata` JSON column to `openregister_file_links` -- [ ] `PhotoService` — filter to images, lazy EXIF extraction -- [ ] `PhotosController` — sub-resource endpoints (list, get with EXIF, link, unlink) -- [x] `PhotosProvider` — id='photos', label='Photos', icon='Image', group='docs', requiredApp='photos', storage='link-table' -- [ ] Admin-setting: strip GPS from EXIF on link (default off) -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnPhotosTab.vue` — thumbnail grid, lightbox with EXIF, upload-and-link, unlink -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnPhotosCard.vue`: - - `user-dashboard`: recent photos across user's objects - - `app-dashboard`: scoped - - `detail-page`: horizontal photo strip (scrollable) - - `single-entity`: thumbnail chip with filename -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/photos.js` — register with `referenceType: 'photos'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: upload photo to object, verify grid thumbnail, lightbox, EXIF display -- [ ] GPS strip setting: toggle on, link a geotagged photo, verify GPS removed -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-polls/specs/generic-integrations/spec.md b/openspec/changes/integration-polls/specs/generic-integrations/spec.md deleted file mode 100644 index a12aeab22b..0000000000 --- a/openspec/changes/integration-polls/specs/generic-integrations/spec.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -status: proposed ---- - -# Integration: Polls - -## Purpose - -Link NC Polls to OR objects through the registry. Surfaces poll status, tally, and user's own vote. - -**Standards**: NC Polls API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Polls Provider Registration - -`PollsProvider` registered with id='polls', group='workflow', requiredApp='polls', storage='link-table'. - -### Requirement: Poll Lifecycle Display - -Tab SHALL show each poll's status (draft/open/closed), vote tally, and the current user's own vote. - -#### Scenario: Closed poll shows final tally - -- **GIVEN** a linked poll with status=closed and tally {yes:7, no:3, abstain:2} -- **WHEN** `CnPollsTab` renders -- **THEN** the row MUST show "Closed • 7 yes / 3 no / 2 abstain" - -#### Scenario: User's own vote highlighted - -- **GIVEN** the current user voted "yes" on a linked poll -- **WHEN** the tab renders -- **THEN** the user's vote MUST be visually highlighted (e.g., bold or badge) - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, `CnPollsCard` SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`); the `detail-page` rendering includes a mini bar-chart tally. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'polls'` SHALL render `CnPollsCard` at `surface='single-entity'`. - -### Requirement: Permission Inheritance - -`PollsProvider::requiresPermission()` SHALL return `null`; Polls' own ACLs apply. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying poll in NC Polls is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Closed poll remains accessible after closure - -- **GIVEN** a linked poll has been closed in NC Polls (status=closed) -- **WHEN** `CnPollsTab` renders -- **THEN** the poll MUST still be shown with final tally and `status='closed'` -- **AND** vote affordances MUST NOT be rendered - -#### Scenario: Deleted poll filters out of list - -- **GIVEN** a linked poll was deleted from NC Polls -- **WHEN** `CnPollsTab` renders -- **THEN** the orphaned link MUST be filtered out with a log warning for admin reconciliation diff --git a/openspec/changes/integration-polls/specs/integration-polls/spec.md b/openspec/changes/integration-polls/specs/integration-polls/spec.md deleted file mode 100644 index 6b84844160..0000000000 --- a/openspec/changes/integration-polls/specs/integration-polls/spec.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -status: proposed ---- - -# Integration: Polls - -## Purpose - -Link NC Polls to OR objects through the registry. Surfaces poll status, tally, and user's own vote. - -**Standards**: NC Polls API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Polls Provider Registration - -`PollsProvider` registered with id='polls', group='workflow', requiredApp='polls', storage='link-table'. - -### Requirement: Poll Lifecycle Display - -Tab SHALL show each poll's status (draft/open/closed), vote tally, and the current user's own vote. - -#### Scenario: Closed poll shows final tally - -- **GIVEN** a linked poll with status=closed and tally {yes:7, no:3, abstain:2} -- **WHEN** `CnPollsTab` renders -- **THEN** the row MUST show "Closed • 7 yes / 3 no / 2 abstain" - -#### Scenario: User's own vote highlighted - -- **GIVEN** the current user voted "yes" on a linked poll -- **WHEN** the tab renders -- **THEN** the user's vote MUST be visually highlighted (e.g., bold or badge) - -### Requirement: Widget Surfaces - -`CnPollsCard` SHALL render on all four surfaces; detail-page includes mini bar-chart tally. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'polls'` SHALL render `CnPollsCard` at `surface='single-entity'`. - -### Requirement: Permission Inheritance - -`PollsProvider::requiresPermission()` SHALL return `null`; Polls' own ACLs apply. diff --git a/openspec/changes/integration-polls/tasks.md b/openspec/changes/integration-polls/tasks.md deleted file mode 100644 index 853fe5eae6..0000000000 --- a/openspec/changes/integration-polls/tasks.md +++ /dev/null @@ -1,37 +0,0 @@ -# Tasks: Integration — Polls - -## Backend - -- [ ] `PollLink` entity + mapper + migration -- [ ] `PollService` wrapping Polls REST API -- [ ] `PollsController` -- [x] `PollsProvider` — id='polls', label='Polls', icon='Poll', group='workflow', requiredApp='polls', storage='link-table' -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnPollsTab.vue` — linked polls with status/tally/user-vote; link-existing + create-new -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnPollsCard.vue`: - - `user-dashboard`: user's open polls across linked objects - - `app-dashboard`: scoped - - `detail-page`: tally + vote status - - `single-entity`: chip with poll title + status -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/polls.js` — register with `referenceType: 'polls'` -- [ ] Wire + barrels - -## Quality - -- [ ] Parity gate passes; nl+en translations; PHPCS/PHPMD/PHPStan/Psalm strict; ESLint clean - -## Acceptance verification - -- [ ] E2E: install Polls, create poll from object, vote, verify in Polls app -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-talk/specs/generic-integrations/spec.md b/openspec/changes/integration-talk/specs/generic-integrations/spec.md deleted file mode 100644 index e8fa903ebf..0000000000 --- a/openspec/changes/integration-talk/specs/generic-integrations/spec.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -status: proposed ---- - -# Integration: Talk - -## Purpose - -Surface NC Talk conversations and chat messages linked to OR objects through the registry. A single `talk` integration routes both Chat and Conversation controllers internally. - -**Standards**: NC Talk (Spreed) API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Talk Provider Registration - -`TalkProvider` registered with id='talk', group='comms', requiredApp='spreed', storage='link-table'. A SINGLE provider routes both chat and conversation concerns. - -#### Scenario: Present when Spreed installed - -- **GIVEN** Spreed app installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** exactly ONE provider with id='talk' MUST be included -- **AND** no separate `talk-chat` or `talk-rooms` providers MUST exist - ---- - -### Requirement: Chat-First Tab - -Tab SHALL default to the most recent conversation with a visible compose box. Conversation list SHALL be accessible via sub-tab or expand affordance. - -#### Scenario: Object with prior conversation opens to chat - -- **GIVEN** an object with one or more linked conversations -- **WHEN** `CnTalkTab` renders -- **THEN** the most recent conversation MUST be displayed -- **AND** a compose box MUST be visible for sending messages - -#### Scenario: Object without conversation shows empty state - -- **WHEN** `CnTalkTab` renders for an object with zero linked conversations -- **THEN** an empty state with "Start conversation" CTA MUST be shown - ---- - -### Requirement: Unread Count on Dashboard Surfaces - -Widget on `user-dashboard` / `app-dashboard` SHALL display unread-message count as the headline metric. - -#### Scenario: Unread count rendered - -- **GIVEN** the user has 7 unread messages across 3 conversations on their linked objects -- **WHEN** `CnTalkCard` renders with `surface='user-dashboard'` -- **THEN** the headline MUST show "7 unread messages across 3 conversations" - -#### Scenario: Clicking headline opens detail - -- **WHEN** the user clicks the unread headline -- **THEN** the view MUST expand to show per-conversation unread breakdowns - ---- - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'talk'` SHALL render `CnTalkCard` at `surface='single-entity'` showing conversation name + unread indicator. - ---- - -### Requirement: Permission Inheritance - -`TalkProvider::requiresPermission()` SHALL return `null`. Talk's own room ACLs govern visibility transitively. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying conversation in NC Talk is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Conversation deleted in Talk - -- **GIVEN** a linked Talk conversation that the user or an admin deleted in the Talk app -- **WHEN** `CnTalkTab` renders -- **THEN** the conversation MUST be filtered out of the listing -- **AND** the empty state MUST appear if it was the last linked conversation diff --git a/openspec/changes/integration-talk/specs/integration-talk/spec.md b/openspec/changes/integration-talk/specs/integration-talk/spec.md deleted file mode 100644 index 219ce9fade..0000000000 --- a/openspec/changes/integration-talk/specs/integration-talk/spec.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -status: proposed ---- - -# Integration: Talk - -## Purpose - -Surface NC Talk conversations and chat messages linked to OR objects through the registry. A single `talk` integration routes both Chat and Conversation controllers internally. - -**Standards**: NC Talk (Spreed) API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Talk Provider Registration - -`TalkProvider` registered with id='talk', group='comms', requiredApp='spreed', storage='link-table'. A SINGLE provider routes both chat and conversation concerns. - -#### Scenario: Present when Spreed installed - -- **GIVEN** Spreed app installed -- **WHEN** `IntegrationRegistry::getEnabled()` is called -- **THEN** exactly ONE provider with id='talk' MUST be included -- **AND** no separate `talk-chat` or `talk-rooms` providers MUST exist - ---- - -### Requirement: Chat-First Tab - -Tab SHALL default to the most recent conversation with a visible compose box. Conversation list SHALL be accessible via sub-tab or expand affordance. - -#### Scenario: Object with prior conversation opens to chat - -- **GIVEN** an object with one or more linked conversations -- **WHEN** `CnTalkTab` renders -- **THEN** the most recent conversation MUST be displayed -- **AND** a compose box MUST be visible for sending messages - -#### Scenario: Object without conversation shows empty state - -- **WHEN** `CnTalkTab` renders for an object with zero linked conversations -- **THEN** an empty state with "Start conversation" CTA MUST be shown - ---- - -### Requirement: Unread Count on Dashboard Surfaces - -Widget on `user-dashboard` / `app-dashboard` SHALL display unread-message count as the headline metric. - -#### Scenario: Unread count rendered - -- **GIVEN** the user has 7 unread messages across 3 conversations on their linked objects -- **WHEN** `CnTalkCard` renders with `surface='user-dashboard'` -- **THEN** the headline MUST show "7 unread messages across 3 conversations" - -#### Scenario: Clicking headline opens detail - -- **WHEN** the user clicks the unread headline -- **THEN** the view MUST expand to show per-conversation unread breakdowns - ---- - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'talk'` SHALL render `CnTalkCard` at `surface='single-entity'` showing conversation name + unread indicator. - ---- - -### Requirement: Permission Inheritance - -`TalkProvider::requiresPermission()` SHALL return `null`. Talk's own room ACLs govern visibility transitively. diff --git a/openspec/changes/integration-talk/tasks.md b/openspec/changes/integration-talk/tasks.md deleted file mode 100644 index 7c369dabdf..0000000000 --- a/openspec/changes/integration-talk/tasks.md +++ /dev/null @@ -1,43 +0,0 @@ -# Tasks: Integration — Talk - -> **ADR-028 task-cap waiver**: this leaf has 24 tasks (cap is 15). The work is a single integration vertical slice (provider + sub-resource controller + tab + 4-surface widget + tests + nl/en). Splitting it would force interleaved depends_on chains that ship slower than one cohesive leaf. Hydra builders SHOULD batch this leaf across multiple turns. - -## Backend - -- [x] Create `lib/Service/Integration/Providers/TalkProvider.php` — id='talk', label='Chat', icon='ChatOutline', group='comms', requiredApp='spreed', storage='link-table'; injects Chat + Conversation services -- [ ] DI-tag in `Application.php` -- [ ] Unit test covering both service-delegation paths - -## Frontend — Tab - -- [ ] `CnTalkTab.vue` — active conversation view with compose box; collapsible conversation list; "Start conversation" CTA; "Open in Talk" link-out -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnTalkCard.vue`: - - `user-dashboard`: "N unread across M conversations" headline - - `app-dashboard`: scoped to app objects - - `detail-page`: most recent conversation inline, unread badge - - `single-entity`: chip with conversation name + unread indicator -- [ ] 30s polling on tab open for new messages -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/talk.js` — register with `referenceType: 'talk'` -- [ ] Wire + barrels - -## Quality - -- [ ] Parity gate passes -- [ ] nl + en translations -- [ ] PHPCS/PHPMD/PHPStan/Psalm strict pass -- [ ] ESLint clean - -## Acceptance verification - -- [ ] E2E: install Spreed, start a conversation on an object, send messages, verify in Talk app -- [ ] Unread badge appears/clears correctly across surfaces -- [ ] Hide test: disable Spreed → integration hidden -- [ ] Reference-property test diff --git a/openspec/changes/integration-time-tracker/specs/generic-integrations/spec.md b/openspec/changes/integration-time-tracker/specs/generic-integrations/spec.md deleted file mode 100644 index fd9aa4982f..0000000000 --- a/openspec/changes/integration-time-tracker/specs/generic-integrations/spec.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -status: proposed ---- - -# Integration: Time Tracker - -## Purpose - -Link time entries to OR objects through the registry with configurable NC time-tracking backend. - -**Standards**: NC Time Manager (and compatible time-tracking apps), ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## ADDED Requirements - -### Requirement: Time Provider Registration - -`TimeProvider` registered with id='time-tracker', group='workflow', requiredApp=(configurable), storage='link-table'. - -### Requirement: Configurable Backend - -Admin setting `time-tracker.backend` SHALL select which NC time-tracking app provides the underlying storage (default `timemanager`). - -### Requirement: Denormalized Object Total - -Link table SHALL store per-object hour total updated on entry write. Dashboard rendering SHALL use this total rather than aggregating entries. - -#### Scenario: Dashboard total fetched as single row - -- **GIVEN** an object with 120 time entries totalling 47h 30m -- **WHEN** `CnTimeCard` renders with `surface='user-dashboard'` -- **THEN** the card MUST fetch ONE row with the total -- **AND** MUST NOT aggregate individual entries at render time - -### Requirement: Reconcile Command - -`occ openregister:time:reconcile` SHALL recalculate totals from source entries. - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`); the `detail-page` rendering shows a per-user/week breakdown. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'time-tracker'` SHALL render hours chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; backend app ACLs govern. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying time entry in the configured Time backend is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: Reconcile repairs drift - -- **GIVEN** the per-object total in the link table drifts from the sum of individual entries -- **WHEN** `occ openregister:time:reconcile` runs -- **THEN** the total MUST be recalculated from the backend truth -- **AND** each correction MUST be audit-logged diff --git a/openspec/changes/integration-time-tracker/specs/integration-time-tracker/spec.md b/openspec/changes/integration-time-tracker/specs/integration-time-tracker/spec.md deleted file mode 100644 index 0ce66d5904..0000000000 --- a/openspec/changes/integration-time-tracker/specs/integration-time-tracker/spec.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -status: proposed ---- - -# Integration: Time Tracker - -## Purpose - -Link time entries to OR objects through the registry with configurable NC time-tracking backend. - -**Standards**: NC Time Manager (and compatible time-tracking apps), ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md) - ---- - -## Requirements - -### Requirement: Time Provider Registration - -`TimeProvider` registered with id='time-tracker', group='workflow', requiredApp=(configurable), storage='link-table'. - -### Requirement: Configurable Backend - -Admin setting `time-tracker.backend` SHALL select which NC time-tracking app provides the underlying storage (default `timemanager`). - -### Requirement: Denormalized Object Total - -Link table SHALL store per-object hour total updated on entry write. Dashboard rendering SHALL use this total rather than aggregating entries. - -#### Scenario: Dashboard total fetched as single row - -- **GIVEN** an object with 120 time entries totalling 47h 30m -- **WHEN** `CnTimeCard` renders with `surface='user-dashboard'` -- **THEN** the card MUST fetch ONE row with the total -- **AND** MUST NOT aggregate individual entries at render time - -### Requirement: Reconcile Command - -`occ openregister:time:reconcile` SHALL recalculate totals from source entries. - -### Requirement: Widget Surfaces - -Standard four; detail-page shows per-user/week breakdown. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'time-tracker'` SHALL render hours chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; backend app ACLs govern. diff --git a/openspec/changes/integration-time-tracker/tasks.md b/openspec/changes/integration-time-tracker/tasks.md deleted file mode 100644 index 5ee7f16281..0000000000 --- a/openspec/changes/integration-time-tracker/tasks.md +++ /dev/null @@ -1,39 +0,0 @@ -# Tasks: Integration — Time Tracker - -## Backend - -- [ ] `TimeLink` entity + mapper + migration (entry linked to object + denormalized object total) -- [ ] `TimeEntryService` with per-backend adapter (default: timemanager) -- [ ] `TimeController` with sub-resource endpoints -- [x] `TimeProvider` — id='time-tracker', label='Time', icon='Clock', group='workflow', requiredApp=(configurable, default 'timemanager'), storage='link-table' -- [ ] Admin setting `time-tracker.backend` -- [ ] `occ openregister:time:reconcile` command for total recalculation -- [ ] DI-tag, routes, unit tests - -## Frontend — Tab - -- [ ] `CnTimeTab.vue` — quick-log form (duration + desc), entry list grouped by user/date, object total -- [ ] Barrel + tests - -## Frontend — Widget - -- [ ] `CnTimeCard.vue`: - - `user-dashboard`: user's hours today across objects - - `app-dashboard`: scoped - - `detail-page`: object total + per-user/week breakdown - - `single-entity`: hours chip (e.g., "4h 30m") -- [ ] Barrel + surface tests - -## Registration - -- [ ] `src/integrations/builtin/time-tracker.js` — register with `referenceType: 'time-tracker'` - -## Quality - -- [ ] Parity gate; nl+en; strict; ESLint - -## Acceptance verification - -- [ ] E2E: log time via tab; verify entry in Time Manager app; totals update on dashboard -- [ ] Reconcile: seed drift, run command, verify total corrected -- [ ] Hide test; reference-property test diff --git a/openspec/changes/integration-xwiki-query-search/proposal.md b/openspec/changes/integration-xwiki-query-search/proposal.md new file mode 100644 index 0000000000..425f45b6bb --- /dev/null +++ b/openspec/changes/integration-xwiki-query-search/proposal.md @@ -0,0 +1,59 @@ +--- +kind: code +status: proposed +--- + +## Why + +The xWiki integration leaf is object-linked: every public path +(`XwikiLinksController` `index`/`link`/`createAndLink`/`destroy`, and the +`available` picker) is scoped to an OR object, or framed as a "pick a page to +link" browse. A consuming app that just wants to **search the knowledge base by +free text and render the hits** — e.g. pipelinq's xWiki dashboard widget — has +no clean, object-independent OR endpoint to call. Today pipelinq works around +this with a bespoke `OCA\Pipelinq\Service\XWikiService` that holds its own +`xwiki_direct_url`, talks XML to xWiki directly, and duplicates the base-URL +resolution OR already owns via the OpenConnector `xwiki` source. + +`XwikiLinkService::getAvailablePages($search)` already resolves the base URL +through the OpenConnector source and free-text-searches pages — but it is +shaped as a link-picker (`{ results, total }`, no pagination) and routed at +`/api/integrations/xwiki/available`, semantically "pages I can link". It is the +right plumbing behind the wrong door. + +## What Changes + +- **Add a query-first search surface** to `XwikiLinkService`: + `searchPages(?string $query, int $limit = 25, int $offset = 0)` — reuses the + existing `XwikiProvider` browse path (base URL resolved from the OpenConnector + `xwiki` source) but returns a paginated `{ results, total, limit, offset }` + envelope and degrades null-safe to `{ unavailable, cause, … }` when the source + is unconfigured or the upstream is down (AD-23). Object-independent: empty OR + context, free-text query, `_limit`/`_page` paging passed to the source. +- **Expose it over HTTP** as `GET /api/integrations/xwiki/search?q=&limit=&offset=` + on `XwikiLinksController::search()` (`@NoAdminRequired`, `@NoCSRFRequired`), + mapping the unconfigured/down state to `503 { error, details.cause }` like the + `available` picker. +- **No change** to the existing object-linked CRUD, the picker, the provider, or + the router — this is a thin additional read-only door onto the plumbing that + already exists. + +## Capabilities + +### Modified Capabilities +- `integration-xwiki`: gains a requirement for an object-independent, paginated, + free-text page-search surface resolved through the OpenConnector `xwiki` + source, exposed at `GET /api/integrations/xwiki/search`. + +## Impact + +- **Code:** `lib/Service/XwikiLinkService.php` (`searchPages()` + a small + `paginate()` helper), `lib/Controller/XwikiLinksController.php` (`search()`), + `appinfo/routes.php` (one app-global GET route). +- **Tests:** `tests/Unit/Service/XwikiLinkServiceTest.php` — search returns a + paginated envelope; degrades to `{ unavailable, cause }` (with limit/offset) + when OpenConnector is absent. +- **Consumers:** pipelinq's xWiki dashboard widget re-points `XWikiService` at + this endpoint and drops `xwiki_direct_url` (future, separate pipelinq change). +- **Security:** read-only; resolves base URL only from the OpenConnector source + (no caller-supplied URL); fails closed to empty/503, never fatal. diff --git a/openspec/changes/integration-xwiki-query-search/specs/integration-xwiki/spec.md b/openspec/changes/integration-xwiki-query-search/specs/integration-xwiki/spec.md new file mode 100644 index 0000000000..7c15b95eba --- /dev/null +++ b/openspec/changes/integration-xwiki-query-search/specs/integration-xwiki/spec.md @@ -0,0 +1,50 @@ +# integration-xwiki + +## ADDED Requirements + +### Requirement: Object-Independent Free-Text Page Search + +OpenRegister SHALL expose an object-independent, paginated, free-text search of +the remote xWiki knowledge base at `GET /api/integrations/xwiki/search`, backed +by `XwikiLinkService::searchPages(?string $query, int $limit, int $offset)`. The +xWiki base URL SHALL be resolved exclusively from the OpenConnector `xwiki` +source via `XwikiProvider` + `ExternalIntegrationRouter` — the caller never +supplies an xWiki URL. The endpoint SHALL be `@NoAdminRequired` (any +authenticated user) and read-only. + +On success it SHALL return `{ results, total, limit, offset }` where `results` +are normalised page rows (`{ id, title, space, url, breadcrumb, … }`). `limit` +SHALL be clamped to 1..100 (default 25) and `offset` to >= 0 (default 0). The +query MAY be passed as `q` or `search`; an empty query lists available pages. + +When the OpenConnector `xwiki` source is missing/unconfigured, or the upstream +xWiki is unreachable, the endpoint SHALL fail closed — returning +`503 { error, details: { cause } }` (cause one of `openconnector-down`, +`openconnector-source-missing`, `provider-auth`, `upstream-service-down`) — and +SHALL never raise a fatal. The service method SHALL return +`{ unavailable, cause, results: [], total: 0, limit, offset }` in that case. + +#### Scenario: free-text search returns paginated hits + +- **GIVEN** a configured, enabled OpenConnector `xwiki` source pointing at a + reachable xWiki +- **WHEN** `GET /api/integrations/xwiki/search?q=passport&limit=10` is called by + an authenticated user +- **THEN** the response is `200 { results, total, limit: 10, offset: 0 }` with + normalised page rows matching the query +- @e2e exclude Backend API endpoint resolved through OpenConnector; verified by PHPUnit + Newman, not a browser flow. + +#### Scenario: unconfigured source fails closed + +- **GIVEN** no OpenConnector `xwiki` source exists (or OpenConnector is disabled) +- **WHEN** `GET /api/integrations/xwiki/search?q=anything` is called +- **THEN** the response is `503 { error, details: { cause } }` with a cause of + `openconnector-down` or `openconnector-source-missing`, and no fatal/500 +- @e2e exclude Backend degradation path; verified by PHPUnit + Newman, not a browser flow. + +#### Scenario: limit and offset are clamped + +- **GIVEN** the search endpoint +- **WHEN** it is called with `limit=9999` and `offset=-5` +- **THEN** the resolved `limit` is 100 and `offset` is 0 in the response envelope +- @e2e exclude Backend input-clamping; verified by PHPUnit, not a browser flow. diff --git a/openspec/changes/integration-xwiki-query-search/tasks.md b/openspec/changes/integration-xwiki-query-search/tasks.md new file mode 100644 index 0000000000..942085e8df --- /dev/null +++ b/openspec/changes/integration-xwiki-query-search/tasks.md @@ -0,0 +1,32 @@ +# Tasks — integration-xwiki-query-search + +## 1. Service + +- [x] 1.1 Add `XwikiLinkService::searchPages(?string $query, int $limit = 25, int $offset = 0)` + — reuse the `XwikiProvider` browse path (empty OR context, `_search` / + `_limit` / `_page` filters), return `{ results, total, limit, offset }`. +- [x] 1.2 Clamp `limit` to 1..100 and `offset` to >= 0. +- [x] 1.3 Degrade null-safe to `{ unavailable, cause, results: [], total: 0, limit, offset }` + (via `paginate()` helper) when OpenConnector / the source / the upstream + is unavailable. + +## 2. Controller + route + +- [x] 2.1 Add `XwikiLinksController::search()` (`@NoAdminRequired`, + `@NoCSRFRequired`) reading `q`/`search`, `limit`, `offset`; map the + unconfigured/down descriptor to `503 { error, details.cause }`. +- [x] 2.2 Register `GET /api/integrations/xwiki/search` in `appinfo/routes.php` + (app-global, before the object-scoped `/xwiki` routes). + +## 3. Tests + quality + +- [x] 3.1 Unit: `searchPages` returns a paginated envelope on success. +- [x] 3.2 Unit: `searchPages` degrades to `{ unavailable, cause, limit, offset }` + when OpenConnector is absent. +- [x] 3.3 Unit: limit/offset clamping. +- [x] 3.4 phpcs/lint clean on all touched files (fix what we touch). + +## 4. Verify + +- [x] 4.1 Live: `GET /api/integrations/xwiki/search?q=test` responds (non-fatal: + empty/503 against the dormant placeholder source). diff --git a/openspec/changes/integration-xwiki/specs/generic-integrations/spec.md b/openspec/changes/integration-xwiki/specs/generic-integrations/spec.md deleted file mode 100644 index 73517891d1..0000000000 --- a/openspec/changes/integration-xwiki/specs/generic-integrations/spec.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -status: proposed ---- - -# Integration: XWiki - -## Purpose - -Link XWiki pages to OR objects through external routing. Complements `integration-collectives` (NC-native wiki) with external-platform support. - -**Standards**: XWiki REST API, ADR-019 -**Cross-references**: [generic-integrations](../../../pluggable-integration-registry/specs/generic-integrations/spec.md), [integration-openproject](../../../integration-openproject/specs/integration-openproject/spec.md), [integration-collectives](../../../integration-collectives/specs/integration-collectives/spec.md) - ---- - -## ADDED Requirements - -### Requirement: XWiki Provider Registration - -`XwikiProvider` registered with id='xwiki', group='external', requiredApp=null, storage='external', `getOpenConnectorSource()='xwiki'`. - -### Requirement: External Routing via OpenConnector - -All CRUD SHALL route through `ExternalIntegrationRouter`. Auth MAY be Basic or OAuth2 depending on the OpenConnector source config. - -### Requirement: Flexible Link Input - -Link form SHALL accept either full XWiki URL or direct space.page path. URL parsing SHALL extract canonical reference. - -#### Scenario: Paste URL resolves to canonical reference - -- **WHEN** user pastes `https://wiki.example.gov/xwiki/bin/view/Dept/Policy/Privacy` -- **THEN** the system MUST parse to space=`Dept.Policy`, page=`Privacy` -- **AND** the link MUST be stored with canonical reference - -### Requirement: Breadcrumb in Tab - -Tab rows SHALL display full breadcrumb (wiki / space hierarchy / page title), not just title. - -### Requirement: Text-Only Preview on Detail-Page - -`CnXwikiCard` at `surface='detail-page'` SHALL render a text preview (first 500 chars of rendered content, macros stripped). Full rendering lives in XWiki. - -#### Scenario: Macros not executed in preview - -- **GIVEN** a linked page containing XWiki macros (velocity, script) -- **WHEN** preview renders -- **THEN** macro output MUST be stripped to plain text -- **AND** no macro execution MUST occur in the NC context - -### Requirement: Auth Expiry Surfacing - -Same as OpenProject — explicit banner + reconnect link. - -### Requirement: Widget Surfaces - -Per umbrella AD-6/AD-18, the widget SHALL render on all four surfaces (`user-dashboard`, `app-dashboard`, `detail-page`, `single-entity`); `single-entity` is a page-title + breadcrumb chip. - -### Requirement: Reference-Property Auto-Rendering - -`referenceType: 'xwiki'` SHALL render page chip. - -### Requirement: Permission Inheritance - -`requiresPermission() === null`; XWiki's own ACLs govern transitively via OpenConnector. - ---- - -### Requirement: Graceful Degradation - -The provider SHALL conform to the umbrella's Error-Handling Contract. When an underlying wiki page in XWiki (via OpenConnector) is missing, inaccessible, or the backing service is down, the provider SHALL surface the documented exception types rather than leaking generic errors. - -#### Scenario: XWiki page moved to a new space - -- **GIVEN** a linked page that was moved from `Dept.Policy.Privacy` to `Legal.Privacy` in XWiki -- **WHEN** `CnXwikiTab` renders -- **THEN** the row MUST show the new breadcrumb (XWiki returns redirect to new path) -- **AND** the link record's canonical reference SHOULD be updated on next fetch - -#### Scenario: XWiki macro output sanitisation - -- **GIVEN** a page containing an XWiki `{{velocity}}` macro -- **WHEN** `CnXwikiCard` renders with `surface='detail-page'` -- **THEN** the macro MUST be stripped (not executed) in the preview text -- **AND** the preview MUST NOT include `"` +- **THEN** the rendered DOM SHALL NOT contain any `"` +- **WHEN** the component renders the description box +- **THEN** the DOM SHALL contain the text `<script>alert(1)</script>` (or equivalent encoding) +- **AND** no `script` element SHALL be present in the rendered tree +- **AND** no JavaScript from the input SHALL execute + +#### Scenario: Plain text passes through visibly unchanged + +- **GIVEN** a `SettingsSection` with `detailedDescription = "Quotas are enforced per organisation."` +- **WHEN** the component renders +- **THEN** the description box SHALL display "Quotas are enforced per organisation." verbatim +- **AND** no characters SHALL be added or removed beyond the entity-escape pass + diff --git a/openspec/specs/spec-governance/spec.md b/openspec/specs/spec-governance/spec.md new file mode 100644 index 0000000000..91e33a0ee8 --- /dev/null +++ b/openspec/specs/spec-governance/spec.md @@ -0,0 +1,25 @@ +# spec-governance Specification + +## Purpose +TBD - created by archiving change sync-specs-and-archive-completed. Update Purpose after archive. +## Requirements +### Requirement: Every implemented capability has a canonical spec + +Every implemented capability SHALL have a canonical spec under `openspec/specs/`, +and completed changes SHALL be archived once their code is merged. A capability +whose code is live but whose requirements exist only under `openspec/changes/` +(un-synced) or whose canonical spec undercounts the implementation is a +documentation defect to reconcile. + +#### Scenario: Implemented capability is discoverable in specs + +- **WHEN** a reviewer looks up a shipped capability (e.g. the credential broker) +- **THEN** its canonical requirements are present under `openspec/specs/` +- **AND** not only under an un-archived `openspec/changes/` folder + +#### Scenario: Completed changes are archived + +- **WHEN** a change's code is merged to the development branch +- **THEN** the change is archived rather than left indefinitely under + `openspec/changes/` + diff --git a/openspec/specs/tables-virtual-register/spec.md b/openspec/specs/tables-virtual-register/spec.md new file mode 100644 index 0000000000..4479ab985e --- /dev/null +++ b/openspec/specs/tables-virtual-register/spec.md @@ -0,0 +1,194 @@ +--- +status: done +--- + +# tables-virtual-register Specification + +## Purpose + +Expose Nextcloud Tables tables as read-only **virtual registers**: each Tables table (or Tables View) becomes an auto-seeded Schema under a `tables` Register, and rows are served live through the existing object-source-provider seam (`x-openregister-object-source`, ADR-049 mechanism) — no copy or sync. Tables column types are mapped to JSON Schema (text/number/datetime/selection/usergroup; relation columns resolve to the referenced virtual object's deterministic UUIDv5), RBAC is delegated to Tables via the acting user (denied == absent, anti-oracle parity), the provider fails closed when the Tables app is missing/disabled, and writes are rejected as a read-only projection. + +**OpenSpec changes**: [tables-object-source-provider](../../changes/archive/2026-07-09-tables-object-source-provider/) _(archived 2026-07-09)_ + +## Requirements + +### Requirement: Tables object-source provider +The system SHALL provide a read-only `ObjectSourceProvider` with `getId()` +returning `tables` that serves a schema's objects live from a single Nextcloud +Tables table (or Tables View), returning non-persisted `ObjectEntity` instances +and never writing to OpenRegister storage. It SHALL integrate via the Tables +internal services (`OCA\Tables\Service\{RowService, ColumnService, TableService, +PermissionsService}`) guarded by `class_exists`, since Tables exposes no stable +public API. + +#### Scenario: Bound schema returns live rows +- **GIVEN** a schema declaring `x-openregister-object-source` with `provider: tables` and `config.tableId` naming a table the acting user may read +- **WHEN** `GET /api/objects/{register}/{schema}` is requested +- **THEN** each Tables row is returned as a virtual object with cells mapped to schema properties by column +- **AND** no row is written to any OpenRegister magic table. +- `@e2e exclude` Requires the Tables app installed in the dev env (not present); covered by `TablesObjectSourceProviderTest` with Tables services mocked, and live-verified once Tables is installed. + +#### Scenario: Single object find by row id +- **GIVEN** a schema bound to a Tables table holding row `` +- **WHEN** `find(register, schema, '')` is called for the acting user +- **THEN** it returns one `ObjectEntity` for that row with `@self.id`/`uuid` set from the row id. +- `@e2e exclude` Backend provider contract; unit-tested with Tables `RowService::find` mocked. + +#### Scenario: Count reflects the bound table +- **GIVEN** a schema bound to a Tables table with N rows the acting user may read +- **WHEN** `count(register, schema, query)` is called +- **THEN** it returns N via `RowService::getRowsCount` (or `getViewRowsCount` for a View binding). +- `@e2e exclude` Backend provider contract; unit-tested. + +### Requirement: RBAC parity and fail-closed absence +Tables reads SHALL pass the acting `$userId` to every Tables service call so +Tables enforces ownership/shares/contexts. A denied or absent table/row SHALL +yield a uniform not-found — `null` from `find`, omission from `findAll`, and not +counted — without distinguishing "denied" from "absent". + +#### Scenario: Permission denied is indistinguishable from absent +- **GIVEN** a Tables row the acting user is not authorized to read +- **WHEN** `find` is called for it +- **THEN** it returns `null` (the read path translates this to a 404 identical to a non-existent id). +- `@e2e exclude` Backend authorization/anti-oracle; unit-tested with Tables returning null for the user. + +#### Scenario: Deleted bound table degrades cleanly +- **GIVEN** a schema whose bound `config.tableId` no longer exists +- **WHEN** its objects are read +- **THEN** an empty result is returned and a warning is logged (no 500). +- `@e2e exclude` Backend degradation; unit-tested. + +### Requirement: Provider disabled when Tables app absent +The provider SHALL report `isEnabled()` false when the Tables app is not +installed or not enabled for the acting user. Every reference to a Tables service +MUST be guarded by `class_exists` so no fatal occurs, and reads of a bound schema +SHALL degrade to an empty result plus a logged warning rather than erroring or +reading the database. + +#### Scenario: Tables app disabled → fail-closed empty +- **GIVEN** the Tables app is not enabled on the instance +- **WHEN** `isEnabled()` is checked and a bound schema is read +- **THEN** `isEnabled()` returns false and the read returns empty with a logged warning (no 500, no DB fallback). +- `@e2e exclude` Backend capability gating; unit-tested with `IAppManager` reporting Tables absent. + +### Requirement: Writes to a Tables-bound schema are rejected +The system SHALL reject create/update/delete on a schema declaring +`x-openregister-object-source` with `provider: tables`, throwing a +read-only-projection error before any persistence (the existing object-source +write-guard), keeping Tables authoritative. + +#### Scenario: Saving a Tables-sourced object is rejected +- **GIVEN** a schema bound to the `tables` provider +- **WHEN** `ObjectService::saveObject()` is called for that schema +- **THEN** it throws a clear read-only-projection error and writes nothing. +- `@e2e exclude` Backend write-guard (inherited from object-source-providers); unit-tested. + +### Requirement: Column-type to JSON-schema mapping +The provider SHALL resolve each Tables cell's numeric `columnId` to its property +name via `ColumnService` and map Tables column types onto schema property types: +text/line/long/rich → string; text/link → string with `format: uri`; number → +number or integer with `minimum`/`maximum` from the column bounds (progress +0-100, stars 0-5); datetime → string with `format` date-time/date/time; +selection → string enum; selection/check → boolean; selection/multi → array of +enum; usergroup → array of `{ id, type }`; relation → the referenced virtual +object's UUID (see the relation requirement). Mandatory columns SHALL be +reflected in the schema `required` list. + +#### Scenario: Column types map to correct property types +- **GIVEN** a table with a stars-number column, a date-only datetime column, a single-select column, and a mandatory text column +- **WHEN** a row is projected to a virtual object and the schema is generated +- **THEN** the stars value is an integer 0-5, the date is a `format: date` string, the selection is one of its enum values, and the mandatory column's property appears in `required`. +- `@e2e exclude` Backend mapping logic; unit-tested per column type. + +#### Scenario: Column drift and columnId collision handled on read +- **GIVEN** a bound table where one column was dropped and two columns slug to the same property name +- **WHEN** a row is read +- **THEN** the dropped column's cell is skipped and logged, and the collision is disambiguated by `columnId` and logged, so the projection stays deterministic without a 500. +- `@e2e exclude` Backend resilience; unit-tested. + +### Requirement: Pagination, filtering, and optional View binding +The provider SHALL push `limit`/`offset` natively to `RowService` +(`findAllByTable` / `findAllByView`), apply any other query filters/sort +provider-side in PHP, and log a warning whenever it caps a fetch it cannot push +down (no silent truncation). A `config.viewId` SHALL bind a Tables View (with the +View's server-side filters/sort) instead of a raw `config.tableId`. + +#### Scenario: limit/offset paginate the bound table +- **GIVEN** a schema bound to a Tables table with 50 readable rows +- **WHEN** `findAll` is called with `limit: 10, offset: 20` +- **THEN** rows 21-30 are returned via `RowService::findAllByTable(tableId, userId, 10, 20)`. +- `@e2e exclude` Backend pagination; unit-tested. + +#### Scenario: viewId binds a Tables View +- **GIVEN** a schema declaring `config.viewId` instead of `config.tableId` +- **WHEN** its objects are read +- **THEN** rows are read via `findAllByView`/`getViewRowsCount` and the View's own server-side filters/sort apply. +- `@e2e exclude` Backend View binding; unit-tested. + +### Requirement: Relation columns map to the referenced virtual object's UUID +A `relation` cell SHALL be mapped to the UUID of the referenced virtual object, +derived deterministically (UUIDv5 over a fixed OpenRegister namespace with name +`tables::`) from the relation column's target table and the +referenced rowId — no per-row lookup. Every virtual object's own uuid SHALL be +derived the same way so relation links and object uuids always agree, enabling +OR-level deep-linking across virtual schemas. When the referenced table's schema +is missing, the cell SHALL fall back to the raw integer rowId with a logged +warning. `find()` SHALL accept both the raw numeric rowId and the derived UUID. + +#### Scenario: Relation cell deep-links to the referenced virtual object +- **GIVEN** a row whose relation cell references row `` in a target table with an auto-seeded schema +- **WHEN** the row is projected to a virtual object +- **THEN** the relation property holds the UUID derived from the target table + `` +- **AND** finding that UUID in the target table's schema returns the referenced virtual object. +- `@e2e exclude` Backend relation derivation; unit-tested (derivation + round-trip with Tables services mocked). + +#### Scenario: Missing target schema falls back to raw rowId +- **GIVEN** a relation cell whose target table has no seeded schema +- **WHEN** the row is projected +- **THEN** the relation property holds the raw integer rowId and a warning is logged (no 500, no fabricated link). +- `@e2e exclude` Backend fallback; unit-tested. + +### Requirement: Auto-seeded virtual schemas for all Tables tables +The system SHALL auto-create one virtual schema per Nextcloud Tables table under +the `tables` virtual register, with a deterministic idempotent slug +(`nc--t`), via (a) a Repair step on install/upgrade, (b) an +`occ openregister:tables:sync` command, and (c) a `TableDeletedEvent` listener +that retires the schema of a deleted table. Seeded schemas SHALL be +instance-global, with per-user visibility enforced at read time by Tables RBAC +(a user without access gets empty/404 — anti-oracle parity). Because Tables +emits no table-created or column-changed event, tables created after the last +sync SHALL NOT be expected to appear until the next occ sync or upgrade repair; +column drift SHALL be handled on-read. The system SHALL also add a `tables` row +to `NcEntitySemanticMap` gated on `requiredApp = tables` so the seeded schemas +participate in ADR-048 semantic resolution and the app-enabled gate. + +#### Scenario: Sync seeds a schema per table +- **GIVEN** the Tables app holds two tables and no seeded schemas exist +- **WHEN** `occ openregister:tables:sync` (or the upgrade Repair step) runs +- **THEN** two virtual schemas exist under the `tables` register with deterministic slugs, each bound to its table via `x-openregister-object-source` +- **AND** re-running the sync changes nothing (idempotent). +- `@e2e exclude` Requires the Tables app in the dev env; unit-tested with `TableService` mocked. + +#### Scenario: Deleted table retires its schema +- **GIVEN** a seeded schema bound to a Tables table +- **WHEN** the table is deleted and `TableDeletedEvent` fires +- **THEN** the bound virtual schema is removed/retired. +- `@e2e exclude` Backend event listener; unit-tested with a synthetic event. + +#### Scenario: New table appears only after the next sync +- **GIVEN** a Tables table created after the last sync +- **WHEN** no sync has run since +- **THEN** no schema exists for it yet, and running `occ openregister:tables:sync` seeds it. +- `@e2e exclude` Backend sync semantics (no table-created event in Tables); unit-tested. + +#### Scenario: Instance-global schema, per-user read-time visibility +- **GIVEN** an auto-seeded schema for a table shared with user A but not user B +- **WHEN** both users list its objects +- **THEN** user A gets the rows and user B gets an empty result / 404 on find, indistinguishable from a table with no rows. +- `@e2e exclude` Read-time RBAC gate; unit-tested with Tables returning per-user results. + +#### Scenario: Tables binding participates in the app-enabled gate +- **GIVEN** the `tables` semantic-map row with `requiredApp: tables` +- **WHEN** the Tables app is uninstalled +- **THEN** the ADR-048 app-enabled gate degrades the Tables-bound virtual schemas (no objects served), consistent with the other app-gated rows. +- `@e2e exclude` Backend semantic-map gating; unit-tested. diff --git a/openspec/specs/tenant-isolation-audit/spec.md b/openspec/specs/tenant-isolation-audit/spec.md index b348df25b3..5da700eb59 100644 --- a/openspec/specs/tenant-isolation-audit/spec.md +++ b/openspec/specs/tenant-isolation-audit/spec.md @@ -1,3 +1,7 @@ +--- +status: done +--- + # Tenant Isolation Audit ## Purpose diff --git a/openspec/specs/tenant-lifecycle/spec.md b/openspec/specs/tenant-lifecycle/spec.md index 3b4f8fbbe5..3a825bace1 100644 --- a/openspec/specs/tenant-lifecycle/spec.md +++ b/openspec/specs/tenant-lifecycle/spec.md @@ -1,4 +1,5 @@ --- +status: done retrofit_extensions: - REQ-005 --- @@ -11,9 +12,7 @@ retrofit_extensions: Define the provisioning, suspension, and deprovisioning workflow for tenant organisations in a SaaS multi-tenant OpenRegister deployment. Each tenant maps to an Organisation entity with a lifecycle state machine that governs API access, data retention, and administrative operations. **Source**: SaaS deployment requirements; BIO/ISO 27001 tenant management; 67% of government tenders require demonstrable tenant isolation with controlled provisioning. - ## Requirements - ### Requirement: Organisation entities MUST have a lifecycle status field with defined state transitions The Organisation entity MUST include a `status` field representing the tenant lifecycle state. Valid states are: `provisioning`, `active`, `suspended`, `deprovisioning`, `archived`. State transitions MUST follow the defined state machine and MUST be enforced at the service layer. @@ -116,3 +115,101 @@ The `TenantLifecycleService` MUST expose utility methods for validating OTAP (De - **GIVEN** an unknown environment string (e.g. `"staging"`) is passed as source or target - **WHEN** `isValidPromotionOrder("staging", "production")` is called - **THEN** the method MUST return `false` + +### Requirement: Organisation and multitenancy configuration API +The system SHALL expose an admin-gated API for reading and writing organisation settings +and multitenancy configuration that govern tenant isolation. `ConfigurationSettingsController` +provides `getOrganisationSettings`/`updateOrganisationSettings` (delegating to +`SettingsService::getOrganisationSettingsOnly()` / `updateOrganisationSettingsOnly()`) and +`getMultitenancySettings`/`updateMultitenancySettings` (delegating to +`getMultitenancySettingsOnly()` / `updateMultitenancySettingsOnly()`). All four return HTTP +500 with an `error` field on service failure. + +#### Scenario: Read multitenancy settings +- **WHEN** `getMultitenancySettings` is called +- **THEN** it MUST return the multitenancy settings from `SettingsService::getMultitenancySettingsOnly()` + +#### Scenario: Update organisation settings +- **GIVEN** an admin posts updated organisation defaults +- **WHEN** `updateOrganisationSettings` runs +- **THEN** it MUST persist them via `SettingsService::updateOrganisationSettingsOnly()` and return the result + +### Requirement: OrganisationService MUST resolve per-user organisation membership and active context + +`OrganisationService` MUST provide the runtime membership-resolution layer that sits +beneath the tenant provisioning state machine. The system MUST always guarantee a user +has at least one organisation: a user with no memberships MUST be auto-added to the +default organisation, which MUST itself be auto-created on first demand. Every user MUST +have an active organisation; when none is explicitly set the system MUST auto-select the +oldest organisation the user belongs to, falling back to the default organisation. Joining +and leaving MUST be guarded — a user MUST NOT be able to leave their last organisation, and +setting an active organisation the user does not belong to MUST be rejected. + +#### Scenario: User with no organisations is added to the default +- **WHEN** `getUserOrganisations()` is called for a user with no memberships +- **THEN** the default organisation MUST be ensured (created if absent) and the user added to it +- **AND** the returned list MUST contain that default organisation + +#### Scenario: Active organisation auto-selects the oldest membership +- **GIVEN** a user with two organisations and no explicitly-set active organisation +- **WHEN** `getActiveOrganisation()` resolves from the database +- **THEN** the oldest organisation (by created date) MUST be selected and persisted as active +- **AND** when the user belongs to none, the default organisation MUST be used as the fallback + +#### Scenario: Setting an active organisation requires membership +- **WHEN** `setActiveOrganisation($uuid)` is called +- **THEN** an organisation the user does not belong to MUST raise an exception +- **AND** a non-existent organisation UUID MUST raise an exception +- **AND** on success the choice MUST be persisted to user config so it survives across sessions + +#### Scenario: Leaving the last organisation is forbidden +- **WHEN** `leaveOrganisation($uuid)` would remove the user's only remaining organisation +- **THEN** an exception MUST be raised preventing the user from being orphaned +- **AND** when the left organisation was the active one, the active selection MUST be cleared and re-resolved + +#### Scenario: Admin users bypass membership for access checks +- **WHEN** `hasAccessToOrganisation($uuid)` is called for a user in the `admin` group +- **THEN** access MUST be granted regardless of explicit membership +- **AND** for non-admin users access MUST require explicit membership + +#### Scenario: Active-organisation resolution includes the parent chain +- **WHEN** `getUserActiveOrganisations()` is called with a hierarchical organisation active +- **THEN** the result MUST contain the active organisation UUID followed by every ancestor UUID resolved via the parent chain +- **AND** this list is what multi-tenancy query filtering uses to grant children visibility of parent resources + +#### Scenario: Creating an organisation recovers from a slug collision +- **WHEN** `createOrganisation($name, ...)` produces a slug that already exists +- **THEN** the unique-constraint violation MUST be caught and the existing organisation with that slug returned instead of crashing +- **AND** when a specific UUID was requested it MUST be reconciled onto the existing entity +- **AND** a newly-created organisation MUST seed the `admin` group into its RBAC authorization and add all admin-group users as members + +### Requirement: Active and default organisation lookups MUST be cached with bounded staleness + +`OrganisationService` MUST cache organisation lookups to keep RBAC-path resolution cheap. +The active organisation MUST be cached in the session (per user) and the default +organisation in static (cross-instance) memory, both with a 15-minute TTL. Cached +organisations MUST be stored as plain arrays and reconstructed into `Organisation` entities +on read to avoid serialization issues. The cache MUST self-invalidate when the underlying +membership becomes stale: a cached active organisation the user no longer belongs to, or +that no longer exists, MUST be cleared and re-resolved. + +#### Scenario: Active organisation is served from session cache within TTL +- **GIVEN** an active organisation cached less than 15 minutes ago +- **WHEN** `getActiveOrganisation()` is called +- **THEN** the organisation MUST be reconstructed from the cached array without a database query + +#### Scenario: Stale active-organisation membership is invalidated +- **GIVEN** a persisted active organisation UUID the user no longer belongs to +- **WHEN** the active organisation is resolved from the database +- **THEN** the stale user-config value and session cache MUST be cleared +- **AND** resolution MUST continue to the oldest-membership / default fallback + +#### Scenario: Switching the active organisation clears and re-primes the cache +- **WHEN** `setActiveOrganisation($uuid)` succeeds +- **THEN** the prior session caches MUST be removed and the new active organisation cached immediately + +#### Scenario: Cache reconstruction round-trips DateTime fields +- **GIVEN** a cached organisation whose `created`/`updated` were stored as ISO strings +- **WHEN** `reconstructOrganisationFromCache()` runs +- **THEN** the string timestamps MUST be parsed back into `DateTime` instances on the rebuilt entity + diff --git a/openspec/specs/tenant-quotas/spec.md b/openspec/specs/tenant-quotas/spec.md index 18a5f90360..1e5531804d 100644 --- a/openspec/specs/tenant-quotas/spec.md +++ b/openspec/specs/tenant-quotas/spec.md @@ -1,4 +1,5 @@ --- +status: done retrofit_extensions: - REQ-001 --- @@ -89,6 +90,8 @@ APCu-based counters MUST be flushed to the `openregister_tenant_usage` database ### REQ-001: The middleware SHALL convert tenant status and quota exceptions raised during the request lifecycle into deterministic JSON error responses +The middleware SHALL convert tenant status and quota exceptions raised during the request lifecycle into deterministic JSON error responses. + > Added by retrofit-2026-05-24-2b-command-repair-middleware (archived). `OCA\OpenRegister\Middleware\TenantQuotaMiddleware::afterException(controller, methodName, \Exception $exception)` is the Nextcloud controller-middleware exception hook. When the request pipeline throws — including from `beforeController()` (status / quota checks) and from controller bodies — the framework invokes `afterException`. The middleware translates two exception families into 4xx responses and re-throws everything else. diff --git a/openspec/specs/text-extraction-eml/spec.md b/openspec/specs/text-extraction-eml/spec.md new file mode 100644 index 0000000000..f7c43eac0a --- /dev/null +++ b/openspec/specs/text-extraction-eml/spec.md @@ -0,0 +1,252 @@ +--- +status: done +--- + +# text-extraction-eml Specification + +## Purpose +Extracts text from `message/rfc822` (EML) email files, producing both a flat plain-text output — ordered headers, body, and recursively-extracted attachment text — and a structured `EmlStructure` value object exposing parsed headers, plain/HTML bodies, and per-attachment metadata. Nested EML is recursed to depth 3, RFC 2047 encoded-words are decoded, non-UTF-8 bodies are transcoded best-effort, and the extraction never logs PII while failing gracefully on malformed input. + +## Requirements +### Requirement: `TextExtractionService` MUST handle `message/rfc822` MIME inputs + +The MIME branch in `extractSourceText` (or equivalent cascade method) MUST recognise `message/rfc822` and delegate to a private `extractEml(File $file): ?string` method. The method MUST return populated plain text (not null) for valid EML inputs. + +#### Scenario: EML input produces populated extracted text + +- **GIVEN** an EML file (mime `message/rfc822`) with headers and a `text/plain` body +- **WHEN** `TextExtractionService::extractFile($fileId)` is called +- **THEN** the persisted extracted-text for the file is non-empty +- **AND** the text contains at minimum the email's body content + +#### Scenario: Pre-change EML inputs that previously returned null now return populated text + +- **GIVEN** an EML file that had been processed before this change landed (resulting in null / empty extracted text) +- **WHEN** the file is re-extracted via `forceReExtract: true` +- **THEN** the persisted extracted-text is now populated per the v1 EML extraction shape + +### Requirement: The flat plain-text output MUST include headers, body, and recursively-extracted attachment text + +The flat output (returned by `extractEml`) MUST be ordered: + +1. Header block: `From:`, `To:`, `Cc:` (when present), `Subject:`, `Date:` (when present). One header per line. After the block, a blank line. +2. Body content: `text/plain` if present; otherwise `text/html` stripped to text. +3. For each attachment, in multipart-document order: + - If the attachment's MIME is recursively extractable (`application/pdf`, the Word DOCX/DOC MIMEs, text MIMEs, or `message/rfc822` for nested EML), the method MUST call `TextExtractionService` for that attachment and inline the result under a marker line `--- Attachment: ---`. + - Otherwise, the marker line MUST be `--- Attachment: (, not extractable) ---` with no body following. + +#### Scenario: Header block precedes body + +- **GIVEN** an EML with `From: alice@example`, `To: bob@example`, `Subject: Hi`, `Date: 2026-04-12T11:00:00Z`, and a body "Hello Bob" +- **WHEN** `extractEml` runs +- **THEN** the output begins with `From: alice@example` (and the other headers in order) +- **AND** a blank line separates the header block from the body +- **AND** the body content "Hello Bob" follows + +#### Scenario: HTML-only body falls back to stripped text + +- **GIVEN** an EML whose body has only a `text/html` part (no `text/plain`) +- **WHEN** `extractEml` runs +- **THEN** the body section contains the HTML stripped to text (no `

`, `` tags; entities decoded; whitespace collapsed) + +#### Scenario: multipart/alternative — `text/plain` is preferred when both parts are present + +- **GIVEN** an EML with a `multipart/alternative` body containing both a `text/plain` part ("Hello Bob") and a `text/html` part (`

Hello Bob

`) +- **WHEN** `extractEml` runs +- **THEN** the body section MUST contain the `text/plain` content "Hello Bob" +- **AND** the body section MUST NOT contain raw HTML tags (`

`, ``) +- **AND** the HTML part MUST NOT be concatenated to the plain part — only one is emitted + +#### Scenario: Recursive attachment extraction for PDF attachment + +- **GIVEN** an EML with a PDF attachment named `report.pdf` +- **WHEN** `extractEml` runs +- **THEN** the output contains a marker line `--- Attachment: report.pdf ---` +- **AND** below the marker, the extracted text from the PDF is inlined + +#### Scenario: Non-extractable attachment listed by name + MIME + +- **GIVEN** an EML with a PNG attachment named `image.png` +- **WHEN** `extractEml` runs +- **THEN** the output contains exactly one marker line `--- Attachment: image.png (image/png, not extractable) ---` +- **AND** no body content follows the marker + +### Requirement: A new public method `parseEmlStructured()` MUST return an `EmlStructure` value object + +The method MUST be public and accept an `\OCP\Files\File`. It MUST return an immutable `EmlStructure` value object with: + +- `headers` — associative array including `from` (string), `to` (string array), `cc` (string array), `subject` (string), `date` (DateTimeImmutable or null), `messageId` (string or null), and any other parsed headers the implementation chooses to expose. +- `body` — value object with `plainText` (string or null) AND `html` (string or null). Both populated when each part exists; either or both can be null. +- `attachments` — array of `EmlAttachment` value objects in multipart order. + +#### Scenario: Structured parse returns populated headers + +- **GIVEN** an EML with full standard headers +- **WHEN** `parseEmlStructured($file)` is called +- **THEN** `result.headers['from']` is the parsed From line (RFC 2047-decoded if encoded) +- **AND** `result.headers['to']` is an array of recipients +- **AND** `result.headers['date']` is a `DateTimeImmutable` matching the Date header + +#### Scenario: Both plainText and html bodies are exposed when present + +- **GIVEN** a multipart EML with both `text/plain` and `text/html` parts +- **WHEN** `parseEmlStructured` runs +- **THEN** `result.body->plainText` contains the plain part +- **AND** `result.body->html` contains the HTML part +- **AND** both are populated (consumer chooses) + +#### Scenario: Single-body EMLs populate only the present part + +- **GIVEN** an EML with only a `text/html` part +- **WHEN** `parseEmlStructured` runs +- **THEN** `result.body->plainText` is null +- **AND** `result.body->html` contains the HTML + +#### Scenario: Date header malformed — date field is null + +- **GIVEN** an EML whose Date header is unparseable +- **WHEN** `parseEmlStructured` runs +- **THEN** `result.headers['date']` is null +- **AND** the rest of the structure is populated normally +- **AND** no exception is thrown + +### Requirement: Each `EmlAttachment` MUST carry filename, MIME type, raw bytes, and inline / contentId metadata + +Each entry in `EmlStructure.attachments[]` MUST be an `EmlAttachment` with: + +- `filename` (string) — from the Content-Disposition `filename` parameter; if absent, from the Content-Type `name` parameter; if both are absent, generated as `attachment-` where `` is the 1-indexed position of the attachment in the multipart-document order. The generated form MUST always be a non-empty string so consumers can render it as a label without special-casing empty names. +- `mimeType` (string) — from Content-Type. +- `content` (string) — MUST be the decoded binary content of the attachment (NOT re-encoded or kept as the MIME-transport base64 string). `zbateson/mail-mime-parser` returns decoded bytes from `$part->getContent()` by default; implementations MUST use that path, not `$part->getRawContent()`. Consumers (e.g. DocuDesk's `eml-pdf-assembly` building PDF/A-3 file attachments or `data:` URIs) can rely on `content` being raw bytes ready to embed, and MUST NOT base64-decode it again. +- `isInline` (bool) — true if the attachment has Content-Disposition: inline. +- `contentId` (string or null) — for HTML body references via `cid:` URLs. +- `nestedEml` (`EmlStructure` or null) — populated if `mimeType === 'message/rfc822'` and the depth limit is not exceeded. + +#### Scenario: Standard PDF attachment + +- **GIVEN** an EML with a single PDF attachment +- **WHEN** structured-parse runs +- **THEN** `result.attachments[0]` has `filename: ""`, `mimeType: "application/pdf"`, populated `content` bytes, `isInline: false`, `contentId: null`, `nestedEml: null` + +#### Scenario: Inline image attachment + +- **GIVEN** an EML where the HTML body references an inline image via `` +- **AND** the EML carries the inline image as an attachment with Content-ID `` +- **WHEN** structured-parse runs +- **THEN** the matching attachment has `isInline: true`, `contentId: "image1@example"` + +#### Scenario: Nested EML attachment + +- **GIVEN** an EML that contains another EML as an attachment (forwarded message) +- **WHEN** structured-parse runs +- **THEN** the nested attachment has `mimeType: "message/rfc822"`, `nestedEml` populated with its own `EmlStructure` + +#### Scenario: Attachment with neither Content-Disposition filename nor Content-Type name + +- **GIVEN** an EML whose second attachment has no `filename` parameter on either Content-Disposition or Content-Type +- **WHEN** structured-parse runs +- **THEN** `result.attachments[1].filename` MUST be `attachment-2` (1-indexed position fallback) +- **AND** the value MUST NOT be empty or null + +### Requirement: Recursive nesting of `message/rfc822` MUST be capped at depth 3 + +The structured parse MUST follow `message/rfc822` attachments recursively, but only up to depth 3. **Depth is measured as the number of recursive `parseEmlStructured` calls made from the root**: the root EML is depth 0, the first nested EML is depth 1, etc. The limit of 3 therefore permits three nested calls — once a parse at depth 3 has completed, any further `message/rfc822` attachments inside it MUST have `nestedEml: null`. The flat path MUST follow the same limit — at the same depth boundary, EML attachments are listed by name only without inline extraction. + +#### Scenario: Depth-3 EML chain is fully recursed + +- **GIVEN** EML A (depth 0) containing EML B (depth 1) containing EML C (depth 2) containing EML D (depth 3) +- **WHEN** `parseEmlStructured(A)` runs +- **THEN** A, B, C, and D are all parsed (`nestedEml` populated on A.attachments[B], B.attachments[C], and C.attachments[D]) +- **AND** any `message/rfc822` attachment inside D (which would be at depth 4) has `nestedEml: null` + +#### Scenario: Depth-4+ chain stops at the limit + +- **GIVEN** an EML chain longer than depth 3 +- **WHEN** the parse runs +- **THEN** at the depth-3 level, the next-level EML attachment carries `nestedEml: null` even though `mimeType: "message/rfc822"` +- **AND** no exception is thrown +- **AND** a debug-level log entry notes the depth-cap activation + +### Requirement: Header block in flat output MUST decode RFC 2047 encoded-words + +When a header value uses RFC 2047 encoded-word form (`=?utf-8?B??=` or `=?utf-8?Q??=`), the flat output MUST contain the decoded plain UTF-8 string. Encoded-word artefacts MUST NOT leak into the output. + +#### Scenario: Encoded-word From header is decoded + +- **GIVEN** an EML whose From header is `From: =?utf-8?B?QnVyZ2VtZWVzdGVyIERlIFZyaWVz?= ` +- **WHEN** `extractEml` runs +- **THEN** the From line in the output is `From: Burgemeester De Vries ` (decoded) + +### Requirement: Malformed input MUST NOT throw from `extractEml`; `parseEmlStructured` MUST throw a typed exception + +`extractEml` MUST follow the existing extraction-failure pattern — return null on irrecoverable parse error and log the error. `parseEmlStructured` MUST throw a typed `EmlParseException` on irrecoverable malformed input (it MUST NOT return null, an empty `EmlStructure`, or a partially-populated one in this case); consumers rely on exception propagation to drive their fallback paths (e.g. DocuDesk's `eml-pdf-assembly` falls back to flat `extractEml` text when `parseEmlStructured` throws — silently returning a partial result would skip that fallback and produce a corrupt PDF). Both paths MUST handle minor malformations (missing optional headers, unusual encoding, extra whitespace) gracefully without raising errors. + +#### Scenario: Catastrophically broken EML returns null from extractEml + +- **GIVEN** a file that's labelled `message/rfc822` but contains binary garbage +- **WHEN** `TextExtractionService::extractFile` runs +- **THEN** the extracted-text is null (or the existing failure marker) +- **AND** an error is logged with details +- **AND** no exception propagates to the caller + +#### Scenario: Catastrophically broken EML throws from parseEmlStructured + +- **GIVEN** the same broken file +- **WHEN** `parseEmlStructured($file)` is called directly +- **THEN** `EmlParseException` is thrown +- **AND** the exception's message identifies the parse failure point + +### Requirement: `extractEml` and `parseEmlStructured` MUST NOT log PII (ADR-005) + +EML headers and bodies contain PII by definition: email addresses, display names, subject lines, and body text. Per ADR-005 ("NO PII in logs, error responses, or debug output"), both methods MUST NOT include any of the following in log lines, exception messages that are subsequently logged, error responses, or debug output: + +- email addresses (From, To, Cc, Bcc, Reply-To headers) +- display names +- Subject header content +- body content (plain-text or HTML) +- attachment filenames (which can themselves carry PII, e.g. `paspoort-de_vries.pdf`) + +Permitted log content is restricted to **structural** failure information: the Nextcloud file ID, the MIME type, the parser exception class name, and the depth at which a recursion-limit was hit. When the underlying `zbateson/mail-mime-parser` raises an exception whose message embeds PII (e.g. a header excerpt), implementations MUST sanitize the message — replace addresses / names / subjects with `` — before logging or rethrowing. + +#### Scenario: Parser failure log contains no PII + +- **GIVEN** an EML with `From: alice@example.com` and `Subject: Confidential — case 123` +- **AND** the parser fails (zbateson exception including the From and Subject in its message) +- **WHEN** `extractEml` runs and the failure is logged +- **THEN** the log entry MUST NOT contain `alice@example.com`, "Confidential", or "case 123" +- **AND** the log entry MUST contain at most: the file ID, the MIME type `message/rfc822`, the exception class name, and any sanitised structural detail + +#### Scenario: `EmlParseException` message is sanitised before logging + +- **GIVEN** `parseEmlStructured` is called on a malformed EML and the underlying parser raises an exception whose `getMessage()` embeds a header value +- **WHEN** the consumer catches the exception and the implementation logs it +- **THEN** the logged message MUST have addresses, names, and subject content replaced with `` +- **AND** the exception class name and the file ID MUST remain in the log line + +### Requirement: Non-UTF-8 body parts MUST be transcoded to UTF-8 on a best-effort basis + +When a body part declares a character set other than UTF-8 (e.g. `Content-Type: text/plain; charset=ISO-8859-1` or `charset=Windows-1252` — common in legacy Dutch government archives), both `extractEml` and `parseEmlStructured` MUST attempt to transcode the bytes to UTF-8 before exposing them in the flat output or the `EmlStructure.body` fields. The recommended implementation uses `mb_detect_encoding` (with a candidate list including the declared charset) followed by `mb_convert_encoding` to UTF-8. + +If transcoding fails (an undetectable or unsupported source charset), the raw bytes MAY be included as-is and an implementation-internal log entry MUST note the transcode failure (subject to the no-PII Requirement above — only the failure reason and charset name, not the body content). + +#### Scenario: ISO-8859-1 body is transcoded to UTF-8 + +- **GIVEN** an EML with `Content-Type: text/plain; charset=ISO-8859-1` and a body containing the bytes for "Café" in ISO-8859-1 encoding (`C` `a` `f` `0xE9`) +- **WHEN** `extractEml` runs +- **THEN** the body section in the flat output contains the UTF-8 string `Café` (bytes `C` `a` `f` `0xC3 0xA9`) + +### Requirement: The change MUST NOT modify behaviour for non-EML files + +All existing MIME branches in `TextExtractionService` (PDF, Word, spreadsheet, plain text) MUST behave identically before and after this change. The new EML branch is additive and orthogonal. + +#### Scenario: PDF extraction unchanged + +- **GIVEN** a PDF file +- **WHEN** `extractFile` runs after this change is applied +- **THEN** the extracted text is identical to what the pre-change code produced for the same file + +#### Scenario: Word document extraction unchanged + +- **GIVEN** a DOCX file +- **WHEN** `extractFile` runs after this change is applied +- **THEN** the extracted text is identical to pre-change behaviour + diff --git a/openspec/specs/text-extraction-word/spec.md b/openspec/specs/text-extraction-word/spec.md new file mode 100644 index 0000000000..1e023b6616 --- /dev/null +++ b/openspec/specs/text-extraction-word/spec.md @@ -0,0 +1,178 @@ +--- +status: done +--- + +# text-extraction-word Specification + +## Purpose +Extracts complete text from Word-family documents (DOCX, legacy DOC, ODT) by recursively walking every element — including table cells and nested tables, section headers and footers, and footnotes and endnotes — and selecting the correct PhpWord reader from the input MIME or extension. The recursive walker is depth-guarded against pathological nesting, reader or load failures return null gracefully rather than aborting, and failure logs never contain document content. + +@e2e exclude Backend PHP text-extraction service (PhpWord element walker for DOCX/DOC/ODT tables, headers, footers, foot/endnotes, reader selection, failure logging). Pure server-side document processing with no user-facing OpenRegister UI surface — exercised via PHPUnit fixture-document units. Covered by PHPUnit. + +## Requirements +### Requirement: Word extraction MUST capture table cell text including nested tables + +The recursive element walker in `extractWord()` MUST descend PhpWord `Table` elements via `getRows()` → each `Row::getCells()` → each `Cell::getElements()`, and MUST recurse each cell element. Because a cell element may itself be a `Table`, **nested tables** MUST be captured by the same path. Table cell text MUST appear in the flat output. (Prior behaviour dropped all table content because a `Table` element does not expose `getElements()`.) + +#### Scenario: Single-level table cell text is extracted + +- **GIVEN** a DOCX document whose body contains a table with cells "Aanvrager", "BSN", "J. de Vries", "123456782" +- **WHEN** `extractWord` runs +- **THEN** the flat output contains the text of every cell ("Aanvrager", "BSN", "J. de Vries", "123456782") + +#### Scenario: Nested table cell text is extracted + +- **GIVEN** a DOCX document containing a table whose cell contains another (nested) table with cell text "Detail-A" +- **WHEN** `extractWord` runs +- **THEN** the flat output contains "Detail-A" + +#### Scenario: TextRun and list-item text inside cells is extracted + +- **GIVEN** a DOCX document with a table cell containing a `TextRun` ("samengesteld blok") and a list item ("punt 1") +- **WHEN** `extractWord` runs +- **THEN** the flat output contains both "samengesteld blok" and "punt 1" + +### Requirement: Word extraction MUST capture section header and footer text + +For each section, `extractWord()` MUST walk `$section->getHeaders()` and `$section->getFooters()` (each header/footer exposes `getElements()`) using the same recursive walker as the body, so header and footer text — including text inside tables in headers/footers — appears in the flat output. + +#### Scenario: Header text is extracted + +- **GIVEN** a DOCX document with a section header containing "Gemeente Voorbeeld — Concept" +- **WHEN** `extractWord` runs +- **THEN** the flat output contains "Gemeente Voorbeeld — Concept" + +#### Scenario: Footer text is extracted + +- **GIVEN** a DOCX document with a section footer containing "Pagina 1 van 3" +- **WHEN** `extractWord` runs +- **THEN** the flat output contains "Pagina 1 van 3" + +### Requirement: Word extraction MUST capture footnote and endnote text + +`extractWord()` MUST capture footnote and endnote text via BOTH paths: inline `Footnote`/`Endnote` elements walked within text runs, AND an unconditional iteration of the document-level notes collection. The text content of footnotes and endnotes MUST appear in the flat output. + +#### Scenario: Footnote text is extracted + +- **GIVEN** a DOCX document with a footnote whose text is "Conform artikel 5.1 Woo" +- **WHEN** `extractWord` runs +- **THEN** the flat output contains "Conform artikel 5.1 Woo" + +#### Scenario: Endnote text is extracted + +- **GIVEN** a DOCX document with an endnote whose text is "Zie bijlage II" +- **WHEN** `extractWord` runs +- **THEN** the flat output contains "Zie bijlage II" + +### Requirement: The recursive walker MUST be depth-guarded + +The recursive element walker MUST accept a depth parameter and MUST stop descending once a fixed maximum depth (`MAX_WORD_DEPTH`, far above realistic document nesting) is exceeded, to defend against pathological or malicious nesting. When the cap is hit, the walker MUST stop descending the over-deep branch and MUST emit a debug-level log noting the cap; it MUST NOT raise an error or abort the overall extraction. + +#### Scenario: Pathologically deep nesting does not crash extraction + +- **GIVEN** a DOCX document whose containers are nested far beyond the depth cap +- **WHEN** `extractWord` runs +- **THEN** extraction completes without an unhandled error +- **AND** a debug-level log entry notes the depth-cap activation +- **AND** text above the cap is still present in the output + +### Requirement: The reader MUST be selected from the input MIME / extension + +`extractWord()` MUST select the PhpWord reader explicitly rather than relying on the `Word2007` default. The mapping MUST be: + +- `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (`.docx`) → `Word2007` +- `application/msword` (`.doc`) → `MsDoc` +- `application/vnd.oasis.opendocument.text` (`.odt`) → `ODText` + +When the MIME is generic or ambiguous, the reader MAY be selected from the file extension; the final fallback MUST be `Word2007`. The selected reader name MUST be passed to `IOFactory::load()`. + +#### Scenario: DOCX selects the Word2007 reader + +- **GIVEN** an input with MIME `application/vnd.openxmlformats-officedocument.wordprocessingml.document` +- **WHEN** `extractWord` selects the reader +- **THEN** the `Word2007` reader is used + +#### Scenario: DOC selects the MsDoc reader + +- **GIVEN** an input with MIME `application/msword` +- **WHEN** `extractWord` selects the reader +- **THEN** the `MsDoc` reader is used + +#### Scenario: ODT selects the ODText reader + +- **GIVEN** an input with MIME `application/vnd.oasis.opendocument.text` +- **WHEN** `extractWord` selects the reader +- **THEN** the `ODText` reader is used + +### Requirement: ODT and DOC MUST be routed through the Word branch + +`isWordDocument()` MUST recognise `application/vnd.oasis.opendocument.text` in addition to the existing `application/vnd.openxmlformats-officedocument.wordprocessingml.document` and `application/msword`, so that ODT inputs dispatch to `extractWord()` in `performTextExtraction()`. `application/msword` MUST continue to route to `extractWord()` and MUST now actually load (via the `MsDoc` reader). + +#### Scenario: ODT MIME is recognised as a Word document + +- **WHEN** `isWordDocument('application/vnd.oasis.opendocument.text')` is evaluated +- **THEN** it returns true + +#### Scenario: ODT input is dispatched to Word extraction and produces text + +- **GIVEN** an ODT file (mime `application/vnd.oasis.opendocument.text`) with a paragraph "Hallo wereld" +- **WHEN** `TextExtractionService::extractFile($fileId)` is called +- **THEN** the persisted extracted-text contains "Hallo wereld" + +#### Scenario: DOC input loads via MsDoc instead of failing on the Word2007 default + +- **GIVEN** a legacy `.doc` file (mime `application/msword`) that the `MsDoc` reader can parse, containing the text "Oud document" +- **WHEN** `TextExtractionService::extractFile($fileId)` is called +- **THEN** the persisted extracted-text contains "Oud document" + +### Requirement: Reader / load failure MUST return null gracefully, not throw fatally + +When `IOFactory::load()` raises an exception (notably the binary `MsDoc` reader's known limitations on documents it cannot parse) or the walk produces no text, `extractWord()` MUST log the failure and return null. It MUST NOT propagate a fatal error for an unsupported/limited per-document input; the surrounding pipeline treats null as "unsupported/empty" and continues. The existing "PhpWord library not installed" guard (class missing) MAY still throw, because that is a deployment error rather than a per-document failure. + +#### Scenario: Unparseable DOC returns null without aborting + +- **GIVEN** a `.doc` file the `MsDoc` reader cannot parse +- **WHEN** `extractWord` runs +- **THEN** the method returns null +- **AND** the failure is logged +- **AND** no exception propagates to abort the extraction run + +#### Scenario: Empty document returns null + +- **GIVEN** a valid but text-empty Word document +- **WHEN** `extractWord` runs +- **THEN** the method returns null + +### Requirement: Failure logs MUST NOT contain document content (ADR-005) + +Per ADR-005 ("NO PII in logs, error responses, or debug output"), log lines and exception messages emitted by `extractWord()` and its helpers MUST NOT include extracted document text, table cell values, header/footer content, or note content. Permitted log content is restricted to structural information: the Nextcloud file ID, the MIME type, the selected reader name, the exception class name, and depth-cap structural detail. + +#### Scenario: Reader-failure log contains no document content + +- **GIVEN** a Word document with body text "Vertrouwelijk: J. de Vries, BSN 123456782" that fails to parse +- **WHEN** `extractWord` logs the failure +- **THEN** the log entry MUST NOT contain "Vertrouwelijk", "J. de Vries", or "123456782" +- **AND** the log entry MAY contain the file ID, the MIME type, the reader name, and the exception class name + +### Requirement: The change MUST NOT reduce or alter DOCX output content nor affect non-Word files + +DOCX extraction MUST return a strict superset of its prior content — all text the prior 2-level traversal produced MUST still be present, with table, header/footer, and note text added. All other MIME branches in `TextExtractionService` (PDF, spreadsheet, plain text, EML) MUST behave identically before and after this change; the Word changes are additive and orthogonal. + +#### Scenario: DOCX body paragraph text is still extracted + +- **GIVEN** a DOCX document with a plain body paragraph "Onderwerp: Besluit" +- **WHEN** `extractWord` runs after this change is applied +- **THEN** the flat output still contains "Onderwerp: Besluit" + +#### Scenario: PDF extraction unchanged + +- **GIVEN** a PDF file +- **WHEN** `extractFile` runs after this change is applied +- **THEN** the extracted text is identical to what the pre-change code produced for the same file + +#### Scenario: Spreadsheet extraction unchanged + +- **GIVEN** an XLSX file +- **WHEN** `extractFile` runs after this change is applied +- **THEN** the extracted text is identical to pre-change behaviour + diff --git a/openspec/specs/text-extraction/spec.md b/openspec/specs/text-extraction/spec.md index e4e581e878..02d7ae2755 100644 --- a/openspec/specs/text-extraction/spec.md +++ b/openspec/specs/text-extraction/spec.md @@ -1,3 +1,7 @@ +--- +status: done +--- + # text-extraction Specification ## Purpose @@ -117,3 +121,22 @@ The system MUST extract a file or object into stored chunks, MUST skip re-extrac - **WHEN** `extractPendingFiles($limit)` runs - **THEN** at most `$limit` files MUST be processed and a per-run summary returned +### Requirement: Per-format text extraction is delegated to dedicated handlers + +Text extraction SHALL delegate per-format parsing to dedicated single- +responsibility handlers rather than implementing every format inline in one +service. The orchestrating service coordinates; each format (PDF, Word, +spreadsheet, EML) and the chunking algorithm live in their own class, mirroring +the handler decomposition already used under `lib/Service/File/`. + +#### Scenario: Extraction routes to a format handler + +- **WHEN** a file of a supported type is extracted +- **THEN** the orchestrating service dispatches to the handler for that format +- **AND** the extraction output is unchanged from the pre-refactor behaviour + +#### Scenario: Behaviour is preserved across the refactor + +- **WHEN** the same file is extracted before and after the handler split +- **THEN** the extracted text and chunk boundaries are identical + diff --git a/openspec/specs/tmlo-auto-populate/spec.md b/openspec/specs/tmlo-auto-populate/spec.md index 940281393d..6bbc245069 100644 --- a/openspec/specs/tmlo-auto-populate/spec.md +++ b/openspec/specs/tmlo-auto-populate/spec.md @@ -1,3 +1,7 @@ +--- +status: done +--- + # tmlo-auto-populate Specification ## Purpose diff --git a/openspec/specs/tmlo-export/spec.md b/openspec/specs/tmlo-export/spec.md index c25d43b27d..b22eaa28e8 100644 --- a/openspec/specs/tmlo-export/spec.md +++ b/openspec/specs/tmlo-export/spec.md @@ -1,3 +1,7 @@ +--- +status: done +--- + # tmlo-export Specification ## Purpose diff --git a/openspec/specs/tmlo-metadata-schema/spec.md b/openspec/specs/tmlo-metadata-schema/spec.md index 20c6abdbd6..dbd0621384 100644 --- a/openspec/specs/tmlo-metadata-schema/spec.md +++ b/openspec/specs/tmlo-metadata-schema/spec.md @@ -1,3 +1,7 @@ +--- +status: done +--- + # tmlo-metadata-schema Specification ## Purpose diff --git a/openspec/specs/tmlo-metadata/spec.md b/openspec/specs/tmlo-metadata/spec.md index 9e4352bff2..fc23f14c52 100644 --- a/openspec/specs/tmlo-metadata/spec.md +++ b/openspec/specs/tmlo-metadata/spec.md @@ -1,5 +1,5 @@ --- -status: implemented +status: done retrofit: true --- diff --git a/openspec/specs/tmlo-query-api/spec.md b/openspec/specs/tmlo-query-api/spec.md index 10ae6c1781..fe21adf89d 100644 --- a/openspec/specs/tmlo-query-api/spec.md +++ b/openspec/specs/tmlo-query-api/spec.md @@ -1,3 +1,7 @@ +--- +status: done +--- + # tmlo-query-api Specification ## Purpose diff --git a/openspec/specs/tmlo-register-toggle/spec.md b/openspec/specs/tmlo-register-toggle/spec.md new file mode 100644 index 0000000000..fb22c08685 --- /dev/null +++ b/openspec/specs/tmlo-register-toggle/spec.md @@ -0,0 +1,45 @@ +--- +status: done +--- +# tmlo-register-toggle Specification + +## Purpose +Controls whether objects in a register carry TMLO archival metadata via a `tmloEnabled` flag in the register's configuration. When enabled, new and updated objects receive TMLO metadata fields seeded from the schema's configured `tmloDefaults` (classification, archival nomination, retention period, destruction category), with `archiefstatus` defaulting to `actief`. +## Requirements +### Requirement: Register-level TMLO toggle + +The system SHALL support a `tmloEnabled` boolean in the Register entity's `configuration` JSON field. When `tmloEnabled` is true, all objects created or updated in that register SHALL carry TMLO metadata fields. + +#### Scenario: Enable TMLO on a register + +- **WHEN** a register is updated with `configuration.tmloEnabled = true` +- **THEN** the register's configuration SHALL persist tmloEnabled=true +- **THEN** new objects in that register SHALL receive TMLO default metadata + +#### Scenario: Disable TMLO on a register + +- **WHEN** a register is updated with `configuration.tmloEnabled = false` +- **THEN** the register's configuration SHALL persist tmloEnabled=false +- **THEN** new objects in that register SHALL NOT receive TMLO default metadata + +### Requirement: Schema-level TMLO defaults + +The system SHALL support TMLO default values in the Schema entity's `configuration` JSON field under a `tmloDefaults` key. These defaults SHALL be applied to new objects when their register has TMLO enabled. + +Supported defaults: +- `classificatie` -- Default classification code +- `archiefnominatie` -- Default archival nomination (`blijvend_bewaren` or `vernietigen`) +- `bewaarTermijn` -- Default retention period (ISO-8601 duration) +- `vernietigingsCategorie` -- Default destruction category + +#### Scenario: Schema with TMLO defaults applied to new object + +- **WHEN** a new object is created in a TMLO-enabled register with a schema that has tmloDefaults configured +- **THEN** the object's `tmlo` field SHALL be populated with the schema's default values +- **THEN** the `archiefstatus` SHALL default to `actief` + +#### Scenario: Schema without TMLO defaults in TMLO-enabled register + +- **WHEN** a new object is created in a TMLO-enabled register with a schema that has no tmloDefaults +- **THEN** the object's `tmlo` field SHALL be an object with all sub-fields set to null except `archiefstatus` which SHALL be `actief` + diff --git a/openspec/specs/tmlo-validation/spec.md b/openspec/specs/tmlo-validation/spec.md new file mode 100644 index 0000000000..42e0ecd497 --- /dev/null +++ b/openspec/specs/tmlo-validation/spec.md @@ -0,0 +1,58 @@ +--- +status: done +--- +# tmlo-validation Specification + +## Purpose +Validates TMLO archival metadata on objects, enforcing the allowed archival-status transition path (actief → semi_statisch → overgebracht or vernietigd) with the required fields and `archiefnominatie` value for each step. It also validates individual TMLO field values against their allowed enums and formats (archiefnominatie, archiefstatus, ISO-8601 retention durations and action dates), rejecting violations with a 422 error. +## Requirements +### Requirement: TMLO status transition validation + +The system SHALL validate archival status transitions to ensure required fields are present before allowing a status change. The valid transitions and their requirements are: + +- `actief` -> `semi_statisch`: No additional requirements +- `semi_statisch` -> `overgebracht`: Requires `archiefactiedatum`, `classificatie`, `archiefnominatie` to be set. `archiefnominatie` MUST be `blijvend_bewaren`. +- `semi_statisch` -> `vernietigd`: Requires `archiefactiedatum`, `classificatie`, `archiefnominatie`, `vernietigingsCategorie` to be set. `archiefnominatie` MUST be `vernietigen`. +- `actief` -> `overgebracht`: NOT allowed (must go through `semi_statisch` first) +- `actief` -> `vernietigd`: NOT allowed (must go through `semi_statisch` first) +- Any status -> `actief`: NOT allowed (cannot revert to active) + +#### Scenario: Valid transition from actief to semi_statisch + +- **WHEN** an object's archiefstatus is changed from `actief` to `semi_statisch` +- **THEN** the transition SHALL be accepted without additional validation + +#### Scenario: Transition to overgebracht with missing fields + +- **WHEN** an object's archiefstatus is changed from `semi_statisch` to `overgebracht` but `classificatie` is null +- **THEN** the system SHALL reject the update with a 422 error listing the missing required fields + +#### Scenario: Transition to vernietigd with wrong archiefnominatie + +- **WHEN** an object's archiefstatus is changed to `vernietigd` but `archiefnominatie` is `blijvend_bewaren` +- **THEN** the system SHALL reject the update with a 422 error indicating archiefnominatie must be `vernietigen` + +#### Scenario: Invalid direct transition from actief to overgebracht + +- **WHEN** an object's archiefstatus is changed directly from `actief` to `overgebracht` +- **THEN** the system SHALL reject the update with a 422 error indicating the transition is not allowed + +### Requirement: TMLO field value validation + +The system SHALL validate TMLO field values to ensure they conform to allowed values: + +- `archiefnominatie` MUST be one of: `blijvend_bewaren`, `vernietigen` +- `archiefstatus` MUST be one of: `actief`, `semi_statisch`, `overgebracht`, `vernietigd` +- `bewaarTermijn` MUST be a valid ISO-8601 duration string (e.g., `P7Y`, `P5Y6M`) +- `archiefactiedatum` MUST be a valid ISO-8601 date string + +#### Scenario: Invalid archiefnominatie value rejected + +- **WHEN** an object is saved with `tmlo.archiefnominatie = "invalid_value"` +- **THEN** the system SHALL reject the save with a 422 error listing valid values + +#### Scenario: Valid ISO-8601 duration accepted + +- **WHEN** an object is saved with `tmlo.bewaarTermijn = "P10Y"` +- **THEN** the value SHALL be accepted and stored + diff --git a/openspec/specs/unified-search-provider/spec.md b/openspec/specs/unified-search-provider/spec.md new file mode 100644 index 0000000000..1a7a803840 --- /dev/null +++ b/openspec/specs/unified-search-provider/spec.md @@ -0,0 +1,215 @@ +--- +status: in-progress +--- + +# unified-search-provider Specification + +## OpenSpec changes + +- `unified-search-index` (in-progress) — fixes cross-schema unified search + over the magic tables (correct per-schema register resolution + bounded + batched UNION fan-out with PHP merge/sort/paginate); no secondary index, + no Solr. + +## Purpose + +OpenRegister provides Nextcloud unified search (top-bar magnifier) over +register objects for the entire fleet, centrally, via +`lib/Search/ObjectsProvider.php` (id `openregister_objects`). Leaf apps do +NOT register their own `OCP\Search\IProvider`; they participate by claiming +(register, schema) pairs through the deep-link registry, which supplies +result URLs, icons, and display names. The provider enforces OR RBAC, tenant +isolation, the published predicate, and schema-level search exposure in one +place, and returns labeled, excerpted, paginated results. + +@e2e exclude The unified-search surface is rendered entirely by Nextcloud's own top-bar search chrome (no OpenRegister Vue component); the provider is a server-side OCP\Search\IProvider. Its observable behaviour is covered by PHPUnit (tests/Unit/Search/ObjectsProviderTest.php) and by Newman against the OCS search endpoint (tests/integration/openregister-unified-search.postman_collection.json), per the Playwright-UI-only / Newman-for-API rule. + +## Requirements +### Requirement: OpenRegister MUST be the single fleet-wide unified search provider for register objects + +OpenRegister MUST register exactly one `OCP\Search\IProvider` +(`ObjectsProvider`, id `openregister_objects`) covering objects in all +registers. Consuming apps MUST NOT register their own search provider +for objects stored in OpenRegister; they declare themselves via the +deep-link registry instead. + +#### Scenario: One provider serves results for objects owned by multiple apps +- GIVEN pipelinq objects (register `pipelinq`) and procest objects (register `case-management`) exist +- WHEN a user searches a term matching one object of each app in Nextcloud unified search +- THEN both results appear under the single `openregister_objects` provider section +- AND neither pipelinq nor procest contributes its own provider section + +#### Scenario: Standalone install keeps working +- GIVEN OpenRegister is installed without any consuming apps +- WHEN a user searches in unified search +- THEN the provider returns matching objects with OpenRegister's own labeling and object-view URLs + +### Requirement: Search results MUST respect OR RBAC, tenant isolation, and the published predicate + +The provider MUST delegate to +`ObjectService::searchObjectsPaginated(query, _rbac: true, +_multitenancy: true)` and MUST NOT apply a weaker (or duplicate) +access filter of its own. The result set MUST contain only objects the +searching user may read: objects granted via RBAC scopes, plus objects +readable through the published predicate (`@self.published` set and in +the past, `@self.depublished` unset or in the future). Soft-deleted +objects MUST never be returned. + +#### Scenario: User only sees objects they may read +- GIVEN user `alice` has an RBAC read grant on schema `client` but not on schema `salary` +- AND a `salary` object and a `client` object both match the term `Jansen` +- WHEN `alice` searches for `Jansen` +- THEN the `client` object is in the results +- AND the `salary` object is NOT in the results + +#### Scenario: Published objects are findable without an explicit grant +- GIVEN user `bob` has no RBAC grant on schema `publication` +- AND a `publication` object matching `subsidieregeling` has `@self.published` in the past and no `depublished` +- WHEN `bob` searches for `subsidieregeling` +- THEN the published object IS in the results + +#### Scenario: Unpublished and depublished objects are hidden from ungranted users +- GIVEN user `bob` has no RBAC grant on schema `publication` +- AND one matching object has `@self.published` unset and another has `@self.depublished` in the past +- WHEN `bob` searches for the matching term +- THEN neither object is in the results + +#### Scenario: Tenant isolation in search results +- GIVEN organisations `gemeente-a` and `gemeente-b` each have objects matching `kerkstraat` +- WHEN a user whose active organisation is `gemeente-a` searches for `kerkstraat` +- THEN only `gemeente-a` objects are returned + +#### Scenario: Soft-deleted objects never appear +- GIVEN an object matching the term has been soft-deleted +- WHEN any user searches for the term +- THEN the deleted object is NOT in the results + +### Requirement: Schemas MUST control their unified-search exposure via the `searchable` flag + +The provider MUST honour the existing `Schema.searchable` boolean +(default `true`). Objects of a schema with `searchable = false` MUST +NOT appear in unified search results, and the exclusion MUST be applied +inside the search query (not by post-filtering a result page). An +explicit `schema` custom filter targeting a non-searchable schema MUST +return an empty result set. + +#### Scenario: Opted-out schema disappears from search +- GIVEN schema `internal-note` has `searchable = false` +- AND an `internal-note` object matches the term `reorganisatie` +- WHEN a user with full RBAC grants searches for `reorganisatie` +- THEN no `internal-note` object is in the results + +#### Scenario: Default schemas remain searchable +- GIVEN a newly created schema with no explicit `searchable` value +- WHEN its objects match a search term +- THEN they appear in unified search (default is `true`) + +#### Scenario: Explicit schema filter cannot bypass opt-out +- GIVEN schema `internal-note` (id 17) has `searchable = false` +- WHEN a search is performed with the custom filter `schema=17` +- THEN the provider returns an empty result set + +### Requirement: Results MUST be labeled per owning app and register + +Each `SearchResultEntry` MUST identify its owner: for a (register, +schema) pair claimed in the deep-link registry, the entry icon MUST be +the registered app icon (rendered rounded) and the subline MUST start +with `{App display name} · {Register title} · {Schema title}`. For +unclaimed pairs the entry keeps the OpenRegister icon and the subline +starts with `Open Register · {Register title} · {Schema title}`. + +#### Scenario: Claimed result carries the owning app's label and icon +- GIVEN pipelinq registered `pipelinq::client` with `displayName: 'Pipelinq'` +- WHEN a search returns a client object +- THEN the entry icon is pipelinq's registered icon (rounded) +- AND the subline starts with `Pipelinq · ` + +#### Scenario: Display name defaults to the app id +- GIVEN procest registered `case-management::case` without a `displayName` +- WHEN a search returns a case object +- THEN the subline starts with `procest · ` + +#### Scenario: Unclaimed result keeps the OpenRegister label +- GIVEN no app has claimed `case-management::audit-log` +- WHEN a search returns an `audit-log` object +- THEN the entry icon is `icon-openregister` +- AND the subline starts with `Open Register · ` + +#### Scenario: Mixed result page labels every entry by its own owner +- GIVEN one matching object each from `pipelinq::client` (claimed), `case-management::case` (claimed), and `case-management::audit-log` (unclaimed) +- WHEN a single search returns all three +- THEN each entry carries its own owner's label and icon independently + +### Requirement: Result URLs MUST deep-link to the owning app's object detail route + +The provider MUST resolve each entry URL through +`DeepLinkRegistryService::resolveUrl()` using the object's `@self` +data; when no registration exists for the (register, schema) pair, the +URL MUST fall back to +`IURLGenerator::linkToRoute('openregister.objects.show', ...)`. Apps +declare their detail-route pattern exclusively via the existing +boot-time `DeepLinkRegistrationEvent`; this capability introduces no +second declaration mechanism. + +#### Scenario: Claimed result links into the owning app +- GIVEN pipelinq registered `urlTemplate: '/apps/pipelinq/#/clients/{uuid}'` for `pipelinq::client` +- WHEN a search returns the client object with UUID `abc-123` +- THEN the entry URL is `/apps/pipelinq/#/clients/abc-123` + +#### Scenario: Unclaimed result links to OpenRegister's object view +- GIVEN no registration exists for the matched object's (register, schema) pair +- WHEN the entry is built +- THEN its URL is the `openregister.objects.show` route for that register, schema, and UUID + +### Requirement: Result sublines MUST contain an excerpt of the matched content + +When the search was term-driven, the subline MUST end with an excerpt: +up to ±60 characters of context around the first case-insensitive +occurrence of the term across the object's top-level scalar string +values (in schema property order, `@self` excluded), ellipsised, with +the matched substring included verbatim. When no string field contains +the term (numeric/relational match) or the search is filter-only, the +excerpt MUST fall back to the object's `summary`, then a truncated +`description`, then be omitted. Excerpts MUST be derived from the +rendered object the user is allowed to read, so row/field-level +security applies to excerpt content. + +#### Scenario: Excerpt around the matched term +- GIVEN an object whose `notes` field contains `…afspraak met mevrouw Jansen over de vergunning…` +- WHEN a user searches for `Jansen` +- THEN the entry subline ends with an ellipsised fragment containing the verbatim substring `Jansen` + +#### Scenario: Fallback when the match is not in a string field +- GIVEN an object matched on a numeric field and having `summary = 'Kapvergunning eik Kerkstraat'` +- WHEN the entry is built +- THEN the subline excerpt is `Kapvergunning eik Kerkstraat` + +#### Scenario: Field-level security redacts excerpt sources +- GIVEN a field hidden from the searching user by field-level security contains the matched term +- WHEN the entry is built +- THEN the excerpt MUST NOT reveal the hidden field's content +- AND the excerpt falls back per the summary/description chain + +### Requirement: The provider MUST paginate results with a cursor + +The provider MUST read the query limit (capped at 25 per page) and the +cursor (integer offset serialised as string) from `ISearchQuery`, pass +them as `_limit`/`_offset`, and return `SearchResult::paginated()` with +the next-offset cursor when a full page was returned, or +`SearchResult::complete()` when the page was short or empty. + +#### Scenario: Full first page returns a cursor +- GIVEN 60 objects match the term +- WHEN the first search request (no cursor) is handled +- THEN 25 entries are returned with a paginated result carrying cursor `25` + +#### Scenario: Requesting the next page continues without duplicates +- GIVEN the previous page returned cursor `25` +- WHEN the search is repeated with cursor `25` +- THEN entries 26–50 are returned and no UUID from the first page reappears + +#### Scenario: Short page completes the result +- GIVEN 10 objects match the term +- WHEN the first page is handled +- THEN all 10 entries are returned as a complete (non-paginated) result + diff --git a/openspec/specs/unit-test-coverage/spec.md b/openspec/specs/unit-test-coverage/spec.md index 8461307b22..0798748c2b 100644 --- a/openspec/specs/unit-test-coverage/spec.md +++ b/openspec/specs/unit-test-coverage/spec.md @@ -6,6 +6,8 @@ status: active @e2e exclude test infrastructure spec — no UI surface, covered by PHPUnit +## Purpose + Achieve 100% unit test code coverage for all PHP source files in `lib/` (excluding `Migration/` and `AppInfo/Application.php`). Tests SHALL exercise every code path — not just the happy flow, but all branches, error paths, edge cases, and boundary conditions. ## Current State @@ -15,6 +17,17 @@ Achieve 100% unit test code coverage for all PHP source files in `lib/` (excludi - Coverage threshold is set at 75% (`composer coverage:check`) - Phase 2 (write ~136 new test files for ~330 untested source files) is planned +## Requirements + +### Requirement: Achieve 100% unit test coverage of all in-scope source files + +All PHP source files in `lib/` (excluding `Migration/` and `AppInfo/Application.php`) SHALL reach 100% unit test coverage, with tests exercising every branch, error path, edge case, and boundary condition. + +#### Scenario: Full code-path coverage for an in-scope source file +- **WHEN** the unit test suite runs against any in-scope source file +- **THEN** the system MUST report 100% line and branch coverage for that file +- **AND** the suite MUST cover happy-flow, error, edge-case, and boundary conditions + ## Testing Standards All unit tests SHALL follow the conventions established in the existing codebase. diff --git a/openspec/specs/urn-resource-addressing/spec.md b/openspec/specs/urn-resource-addressing/spec.md index 7f803a4b3e..8cdb26c27c 100644 --- a/openspec/specs/urn-resource-addressing/spec.md +++ b/openspec/specs/urn-resource-addressing/spec.md @@ -1,5 +1,5 @@ --- -status: implemented +status: done --- # URN Resource Addressing @@ -12,9 +12,7 @@ Implement bidirectional URN-URL mapping for system-independent resource identifi **Source**: Gap identified in cross-platform analysis; part of Dutch government standards ecosystem (VNG Common Ground, NL GOV API Design Rules). **Cross-references**: deep-link-registry (URL template resolution for consuming apps), referential-integrity (URN-based cross-references in `$ref` properties), data-sync-harvesting (URN stability across federated sync sources). - ## Requirements - ### Requirement: Objects MUST have auto-generated URN identifiers following RFC 8141 syntax Every register object MUST have an auto-generated URN following the pattern `urn:{organisation}:{system}:{component}:{resource}:{uuid}` where each segment maps to register and schema metadata. The URN MUST conform to RFC 8141 (Uniform Resource Names) syntax rules: the NID (Namespace Identifier) is the organisation slug, and the NSS (Namespace Specific String) encodes the system, component (register slug), resource (schema slug), and object UUID. Characters in each segment MUST be limited to RFC 8141 allowed characters: unreserved characters (A-Z, a-z, 0-9, `-`, `.`, `_`, `~`) and percent-encoded characters. The URN MUST be generated at object creation time and stored persistently on the `ObjectEntity`. @@ -515,6 +513,114 @@ The URN resolution endpoint availability, configured URN namespace, and supporte - **THEN** `federationSupported` MUST be `false` - **AND** the `federated=true` query parameter on the resolve endpoint MUST return HTTP 501 Not Implemented +### Requirement: The URN resolver MUST expose resolve, lookup, and bulk operations over a fixed URN grammar +`UrnController` MUST expose three `@NoAdminRequired` / `@NoCSRFRequired` endpoints backed by +`UrnService`: `GET /api/urn/resolve?urn=...` (URN → URL), `GET /api/urn/lookup?url=...` +(URL → URN), and `POST /api/urn/bulk` with body `{ "urns": [...] }` (batch URN → URL). The URN +grammar is `urn:::::` where `` is the +`UrnService::DEFAULT_NID` constant (`nl-or`). + +#### Scenario: Resolve a URN to its canonical URL +- **GIVEN** a syntactically valid URN whose register/schema exist on this instance +- **WHEN** `GET /api/urn/resolve?urn=` is called +- **THEN** the response MUST return HTTP 200 with `{ urn, url, instance, register, schema, uuid }` where `url` is produced by `UrnService::resolveUrl()` and the segment fields come from `UrnService::parse()` + +#### Scenario: Missing urn parameter +- **GIVEN** no `urn` query parameter (null or empty) +- **WHEN** the resolve endpoint is called +- **THEN** the response MUST return HTTP 400 with `{ "error": "urn parameter is required" }` + +#### Scenario: Malformed URN +- **GIVEN** a `urn` value that `UrnService::parse()` cannot parse +- **WHEN** the resolve endpoint is called +- **THEN** the response MUST return HTTP 400 with an error describing the expected `urn:::::` shape + +#### Scenario: Parsable URN that does not resolve on this instance +- **GIVEN** a well-formed URN whose register/schema/object is not present on this instance +- **WHEN** the resolve endpoint is called +- **THEN** the response MUST return HTTP 404 with `{ "error": "URN does not resolve on this instance", "urn": ..., "parts": ... }` + +#### Scenario: Reverse lookup URL to URN +- **GIVEN** an OpenRegister object URL +- **WHEN** `GET /api/urn/lookup?url=` is called +- **THEN** the response MUST return HTTP 200 with `{ url, urn }` where `urn` is produced by `UrnService::urnFromUrl()` +- **AND** a missing `url` parameter MUST return HTTP 400 with `{ "error": "url parameter is required" }` +- **AND** a URL that is not an OpenRegister object reference MUST return HTTP 404 with `{ "error": "URL is not an OpenRegister object reference", "url": ... }` + +#### Scenario: Bulk resolution returns a urn→url map +- **GIVEN** a POST body `{ "urns": ["urn:nl-or:...", ...] }` +- **WHEN** `POST /api/urn/bulk` is called +- **THEN** the response MUST return HTTP 200 with `{ "count": , "resolved": { : , ... } }` from `UrnService::resolveBulk()` +- **AND** a missing/empty/non-array `urns` MUST return HTTP 400 with `{ "error": "urns array is required" }` + +#### Scenario: Bulk resolution enforces a hard input cap +- **GIVEN** a POST body with more than 1000 URNs +- **WHEN** `POST /api/urn/bulk` is called +- **THEN** the response MUST return HTTP 422 with `{ "error": "Too many URNs (max 1000 per request)", "count": }` +- **AND** this cap MUST NOT be relaxed without an upstream per-user rate limit, because each URN triggers a parse → register-find → schema-find → object-find chain reachable by any authenticated user + +### Requirement: URNs MUST be constructed on demand in the shipped `urn:nl-or:{instance}:{register}:{schema}:{uuid}` shape + +`UrnService` MUST construct RFC 8141 URNs using a fixed informal NID `nl-or` (per RFC 8141 §5.1) followed by four colon-separated segments: an instance slug, the register slug, the schema slug, and the object UUID. URNs MUST be built on demand and MUST NOT be persisted on the object or auto-generated at object-creation time. + +- `build(registerSlug, schemaSlug, uuid)` MUST emit `urn:nl-or:{instance}:{register}:{schema}:{uuid}` with the register slug, schema slug, and uuid lower-cased (URN comparison is case-insensitive per RFC 8141 §3; emitting one canonical case avoids cache mismatches). +- `buildForObject(ObjectEntity)` MUST return `null` when the object lacks a UUID, or when the register or schema reference cannot be resolved to a slug; otherwise it MUST resolve the register/schema slugs and delegate to `build()`. +- `getInstanceSlug()` MUST resolve the instance slug in order: (1) the `openregister.urn_instance` app-config value when non-empty, (2) the sanitised host portion of the absolute base URL, (3) the literal `localhost` as a final fallback. Sanitisation MUST lower-case and replace runs of non-alphanumeric characters with single hyphens, trimming leading/trailing hyphens. + +#### Scenario: Build a URN from explicit parts +- **GIVEN** register slug `decidesk`, schema slug `meeting`, and uuid `1C1C970F-D50C-4943-8128-78999E240EEC` +- **WHEN** `build()` is called +- **THEN** the result MUST be `urn:nl-or:{instance}:decidesk:meeting:1c1c970f-d50c-4943-8128-78999e240eec` with all three trailing segments lower-cased +- **AND** the `{instance}` segment MUST equal the value returned by `getInstanceSlug()` + +#### Scenario: Build for an object with incomplete identity returns null +- **GIVEN** an `ObjectEntity` with a null or empty UUID, OR whose register/schema reference does not resolve to a slug +- **WHEN** `buildForObject()` is called +- **THEN** the method MUST return `null` rather than emitting a partial URN + +#### Scenario: Instance slug honours config override then host then localhost +- **GIVEN** `openregister.urn_instance` is set to `My Stable ID` +- **WHEN** `getInstanceSlug()` is called +- **THEN** it MUST return the sanitised slug `my-stable-id` +- **AND** when the config value is empty it MUST fall back to the sanitised host of the base URL, and to `localhost` when no host can be derived + +### Requirement: The shipped resolver MUST map URN↔URL in-memory for the local instance only + +`UrnService` MUST provide in-memory, on-demand resolution between URNs and OpenRegister API URLs for the local instance. Cross-instance / federated resolution is explicitly out of scope of the shipped service and MUST return `null`. There is no `/api/urn/*` HTTP endpoint and no external `UrnMapping` table in the shipped surface. + +- `parse(urn)` MUST return `['instance','register','schema','uuid']` only when the string matches the anchored RFC 8141 URN regex AND its NID equals `nl-or` AND the NSS has at least four colon-separated parts; otherwise it MUST return `null`. +- `resolveUrl(urn)` MUST return `null` when the URN does not parse, when its instance segment differs from the local `getInstanceSlug()`, or when the register/schema cannot be looked up; otherwise it MUST return the absolute API URL `/apps/openregister/api/objects/{register-slug}/{schema-slug}/{uuid}` (each path segment `rawurlencode`d) via `IURLGenerator`. +- `urnFromUrl(url)` MUST recognise the three URL shapes the Smart Picker `ObjectReferenceProvider` accepts — hash-routed (`#/registers/{id}/schemas/{id}/objects/{uuid}`), API (`/api/objects/{ref}/{ref}/{uuid}`), and direct (`/objects/{ref}/{ref}/{uuid}`) — resolve numeric ids or slugs to canonical slugs, and return the constructed URN, or `null` when the URL does not match or the register/schema does not resolve. +- `resolveBulk(urns)` MUST return a `{urn => url|null}` map preserving input order, skipping non-string / empty entries, by delegating each entry to `resolveUrl()`. The shipped method imposes no per-request URN cap. + +#### Scenario: Parse rejects a foreign NID +- **GIVEN** a string `urn:isbn:0451450523` +- **WHEN** `parse()` is called +- **THEN** it MUST return `null` because the NID is not `nl-or` +- **AND** a well-formed `urn:nl-or:{instance}:{register}:{schema}:{uuid}` MUST parse into its four named parts + +#### Scenario: Resolve a local URN to its API URL +- **GIVEN** a URN whose instance segment equals the local instance slug and whose register/schema resolve +- **WHEN** `resolveUrl()` is called +- **THEN** the result MUST be the absolute `/apps/openregister/api/objects/{register}/{schema}/{uuid}` URL with `rawurlencode`d segments + +#### Scenario: Cross-instance or unresolvable URN returns null +- **GIVEN** a URN whose instance segment differs from the local instance slug, OR whose register/schema cannot be found +- **WHEN** `resolveUrl()` is called +- **THEN** it MUST return `null` (federation is out of scope for the shipped service) + +#### Scenario: Reverse-resolve a Smart Picker URL +- **GIVEN** an OpenRegister object URL in the hash-routed, API, or direct shape +- **WHEN** `urnFromUrl()` is called and the register/schema resolve +- **THEN** the method MUST return the canonical `urn:nl-or:{instance}:{register-slug}:{schema-slug}:{uuid}` URN +- **AND** a non-OpenRegister URL MUST yield `null` + +#### Scenario: Bulk resolve preserves order and maps misses to null +- **GIVEN** a list mixing resolvable and unresolvable URNs plus a non-string entry +- **WHEN** `resolveBulk()` is called +- **THEN** the result MUST be a `{urn => url|null}` map where resolvable URNs map to their URL and unresolvable ones map to `null` +- **AND** the non-string / empty entry MUST be skipped (absent from the map) + ## Current Implementation Status **Not implemented.** No URN support exists in the codebase: diff --git a/openspec/specs/vector-embeddings/spec.md b/openspec/specs/vector-embeddings/spec.md index 96ff6e5fca..e70e39a67e 100644 --- a/openspec/specs/vector-embeddings/spec.md +++ b/openspec/specs/vector-embeddings/spec.md @@ -1,9 +1,16 @@ +--- +status: in-progress +--- + # vector-embeddings Specification ## Purpose @e2e exclude backend vector/KNN search service — covered by PHPUnit TBD - created by archiving change retrofit-2026-05-25-bw-svc-mid3. Update Purpose after archive. + +**OpenSpec changes** +- `hybrid-document-search` (in progress) — pgvector/HNSW KNN path replacing the PHP-cosine-only 500-row recency-biased fallback; real `tsvector`/`ts_rank` keyword arm wired into hybrid-search RRF fusion (was hard-coded empty); `solr`/`solrResults` naming retired (Solr backend already removed from this code; the spec text hadn't caught up). ## Requirements ### Requirement: The system MUST execute vector queries via semantic KNN/cosine similarity and hybrid Reciprocal-Rank-Fusion search @@ -49,3 +56,293 @@ Each result entry MUST carry `vector_id`, `entity_type`, `entity_id`, `similarit - **THEN** the effective weights MUST be `['solr' => 0.75, 'vector' => 0.25]` - **AND** the returned `weights` field MUST reflect the normalised values +### Requirement: The system MUST generate text embeddings through a pluggable multi-provider backend (OpenAI / Fireworks AI / Ollama) + +The `VectorizationService::generateEmbedding(string $text, ?string $provider = null)` entrypoint MUST resolve the active embedding configuration from `SettingsService::getLLMSettingsOnly()` when `$provider` is `null`, otherwise honour the explicit override. The configured provider MUST be one of `openai`, `fireworks`, or `ollama`; any other provider name MUST throw `Unsupported embedding provider: {provider}` from `EmbeddingGeneratorHandler::getGenerator()`. The handler MUST cache generator instances keyed by `"{provider}_{model}"` and reuse them across calls. + +Each provider has a distinct construction path: OpenAI MUST route through one of `OpenAIADA002EmbeddingGenerator`, `OpenAI3SmallEmbeddingGenerator`, or `OpenAI3LargeEmbeddingGenerator` based on the model name; unknown OpenAI models MUST throw `Unsupported OpenAI model: {model}`. Fireworks MUST be wrapped in an anonymous LLPhant-compatible adapter that posts directly to `{base_url}/embeddings` with `Authorization: Bearer {api_key}` and returns `data[0].embedding`. Ollama MUST instantiate `OllamaEmbeddingGenerator` with `url = rtrim(base_url, '/') + '/api/'`. + +`generateEmbedding()` MUST return `['embedding' => array, 'model' => string, 'dimensions' => int]`. The dimensions value MUST come from `EmbeddingGeneratorHandler::getDefaultDimensions()` which maps `text-embedding-ada-002` and `text-embedding-3-small` to 1536, `text-embedding-3-large` to 3072, `ollama-default` to 384, and any unknown model to a fallback of 1536. + +Batch generation (`generateBatchEmbeddings(array $texts)`) MUST iterate texts serially (no provider-side batching is attempted) and MUST tolerate per-text failures: a failed call MUST yield `['embedding' => null, 'dimensions' => 0, 'error' => string]` for that index without aborting the batch. The aggregate result MUST be returned in input order. + +The admin-facing test affordance (`testEmbedding(string $provider, array $config, string $testText = 'Test.')`) MUST validate the supplied config (`provider` and `model` required; `api_key` required for `openai` and `fireworks`), generate one embedding, and return `['success' => bool, 'message' => string, 'data' => ['provider', 'model', 'vectorLength', 'sampleValues' => first 5 floats, 'testText']]`. On failure the response MUST be `['success' => false, 'error' => string, 'message' => "Failed to generate embedding: ..."]` — the call MUST NOT throw. + +#### Scenario: Default provider resolved from settings on simple generate call +- **GIVEN** `SettingsService::getLLMSettingsOnly()` returns `embeddingProvider: "openai"` and `openaiConfig: {model: "text-embedding-3-small", apiKey: "sk-..."}` +- **WHEN** `VectorizationService::generateEmbedding('hello')` is invoked with no provider override +- **THEN** `EmbeddingGeneratorHandler::createOpenAIGenerator('text-embedding-3-small', ...)` MUST be invoked +- **AND** the cached generator MUST be reused on the next call with the same provider/model pair +- **AND** the response MUST be `['embedding' => array, 'model' => 'text-embedding-3-small', 'dimensions' => 1536]` + +#### Scenario: Unsupported provider throws on getGenerator +- **GIVEN** a caller invokes `generateEmbedding('hello', 'cohere')` +- **WHEN** `EmbeddingGeneratorHandler::getGenerator(['provider' => 'cohere', ...])` runs +- **THEN** an `Exception` with message `Unsupported embedding provider: cohere` MUST be thrown +- **AND** no entry MUST be written to `$generatorCache` + +#### Scenario: Custom-config test rejects missing API key for OpenAI / Fireworks +- **GIVEN** the admin LLM settings page POSTs `{provider: "openai", model: "text-embedding-3-large", apiKey: ""}` to the test endpoint +- **WHEN** `VectorEmbeddings::generateEmbeddingWithCustomConfig(...)` validates the payload +- **THEN** an `Exception` with message `API key is required for openai` MUST be thrown +- **AND** the wrapping `testEmbedding()` MUST catch it and return `['success' => false, 'error' => 'API key is required for openai', 'message' => 'Failed to generate embedding: ...']` + +#### Scenario: Batch generation tolerates per-text failure +- **GIVEN** three texts `['ok-1', 'ok-2', 'ok-3']` are passed to `generateBatchEmbeddings` +- **AND** the second call to `EmbeddingGeneratorInterface::embedText` throws an HTTP-500 error +- **WHEN** the batch loop completes +- **THEN** the result MUST be an array of length 3 in input order +- **AND** index 1 MUST be `['embedding' => null, 'model' => string, 'dimensions' => 0, 'error' => string]` +- **AND** indices 0 and 2 MUST be successful `['embedding' => array, 'model' => string, 'dimensions' => int]` entries + +#### Scenario: Fireworks adapter posts directly without LLPhant client wrapping +- **GIVEN** the configured provider is `fireworks` with `model: "thenlper/gte-base"` and `base_url: "https://api.fireworks.ai/inference/v1"` +- **WHEN** `EmbeddingGeneratorHandler::createFireworksGenerator(...)` is invoked +- **THEN** an anonymous class implementing `EmbeddingGeneratorInterface` MUST be returned +- **AND** calling `embedText('hello')` on that instance MUST issue a `POST https://api.fireworks.ai/inference/v1/embeddings` with `Authorization: Bearer {api_key}` and body `{model: "thenlper/gte-base", input: "hello"}` +- **AND** the embedding length for `thenlper/gte-base` MUST be reported as 768 by `getEmbeddingLength()` + +### Requirement: The system MUST route vector storage to a configured backend (Postgres BLOB or Solr atomic update) with UTF-8 sanitisation + +`VectorEmbeddings::storeVector(...)` MUST resolve the backend by reading `llmSettings.vectorConfig.backend` from `SettingsService::getLLMSettingsOnly()`, defaulting to `'php'` on missing/error. The backend value MUST then be passed to `VectorStorageHandler::storeVector(..., string $backend)` which MUST dispatch to either `storeVectorInSolr` (when backend === `'solr'`) or `storeVectorInDatabase` (default). + +**Database path** (`storeVectorInDatabase`) MUST insert a row into `openregister_vectors` with: `entity_type`, `entity_id`, `chunk_index`, `total_chunks`, `chunk_text` (after sanitisation), `embedding` (PHP-`serialize()`'d to a BLOB via `PDO::PARAM_LOB`), `embedding_model`, `embedding_dimensions`, `metadata` (JSON-encoded when non-empty), and `created_at` / `updated_at` set to `date('Y-m-d H:i:s')`. The autoincrement `id` MUST be returned. Sanitisation (`sanitizeText`) MUST: (1) `mb_convert_encoding($text, 'UTF-8', 'UTF-8')`, (2) strip `\x00-\x08\x0B\x0C\x0E-\x1F\x7F` control characters, (3) `iconv('UTF-8', 'UTF-8//IGNORE', $text)`, (4) collapse whitespace via `preg_replace('/\s+/u', ' ', $text)`, (5) `trim()`. + +**Solr path** (`storeVectorInSolr`) MUST resolve the target collection by entity type: `file` / `files` MUST use `settings.solr.fileCollection`; anything else MUST use `settings.solr.objectCollection` (or fall back to `settings.solr.collection`). A missing collection MUST throw `Solr collection not configured for entity type: {entityType}`. The Solr vector field name MUST come from `settings.llm.vectorConfig.solrField`, defaulting to `'_embedding_'`. The document ID MUST be `entity_id` for objects and `"{entity_id}_chunk_{chunk_index}"` for files. The Solr atomic update payload MUST be `[{id, {vectorField}: {set: embedding}, _embedding_model_: {set: model}, _embedding_dim_: {set: dimensions}, self_updated: {set: gmdate('Y-m-d\TH:i:s\Z')}}]`. The backend instance MUST be an instance of `SolrBackend` (cast from `IndexService::getBackend()`) and MUST report `isAvailable() === true`; otherwise the call MUST throw. On Solr response with `responseHeader.status !== 0` (or missing), the call MUST throw `Solr atomic update failed: {responseJson}`. The Solr path MUST return the string document ID; `VectorStorageHandler::storeVector` then MUST return `crc32(documentId)` as the int facade value. + +Any failure in either path MUST be re-wrapped as `Vector storage failed: {message}` (or `Solr vector storage failed: ...`) before being thrown. + +#### Scenario: Default backend is PHP (database) when settings unavailable +- **GIVEN** `SettingsService::getLLMSettingsOnly()` throws an exception +- **WHEN** `VectorEmbeddings::getVectorSearchBackend()` is invoked +- **THEN** a warning MUST be logged `Failed to get vector search backend, defaulting to PHP` +- **AND** the resolved backend MUST be `'php'` +- **AND** `VectorStorageHandler::storeVectorInDatabase(...)` MUST be the path taken + +#### Scenario: Database insert serialises embedding as a PHP BLOB +- **GIVEN** an embedding `[0.1, 0.2, 0.3]` for object `'abc-uuid'` with model `text-embedding-3-small` +- **WHEN** `storeVectorInDatabase(...)` runs +- **THEN** the inserted row's `embedding` column MUST contain `serialize([0.1, 0.2, 0.3])` bound as `PDO::PARAM_LOB` +- **AND** the inserted row's `embedding_dimensions` MUST equal 3 +- **AND** the function MUST return the autoincrement `id` of the new row + +#### Scenario: Solr file-type document ID embeds chunk index +- **GIVEN** a file vector with `entity_id = 'file-uuid'`, `chunk_index = 2`, `entityType = 'file'` +- **WHEN** `storeVectorInSolr(...)` builds the atomic-update document +- **THEN** the document `id` MUST equal `"file-uuid_chunk_2"` +- **AND** the payload MUST include `_embedding_dim_: {set: dimensions}` and `self_updated: {set: gmdate('Y-m-d\TH:i:s\Z')}` +- **AND** `VectorStorageHandler::storeVector` MUST return `crc32("file-uuid_chunk_2")` to the caller + +#### Scenario: Solr unavailable triggers re-wrap +- **GIVEN** backend is `'solr'` and `IndexService::getBackend()->isAvailable()` returns `false` +- **WHEN** `storeVectorInSolr(...)` is invoked +- **THEN** an `Exception` with message `Solr vector storage failed: Solr service is not available` MUST be thrown +- **AND** the original message MUST be wrapped in the outer `storeVector` catch as `Vector storage failed: Solr vector storage failed: Solr service is not available` + +#### Scenario: sanitizeText strips control chars and normalises whitespace +- **GIVEN** input `"hello\x00world\t\t\nfoo"` +- **WHEN** `VectorStorageHandler::sanitizeText(...)` is invoked +- **THEN** the output MUST equal `"helloworld foo"` (NUL byte removed, tab+newline collapsed to single space, trimmed) + +### Requirement: The system MUST orchestrate batch vectorisation through a pluggable strategy interface with per-entity error capture + +`VectorizationService` MUST expose `registerStrategy(string $entityType, VectorizationStrategyInterface $strategy)` to bind a strategy to an entity-type identifier (e.g. `'object'`, `'file'`). `vectorizeBatch(string $entityType, array $options = [])` MUST resolve the strategy via `getStrategy()`, MUST throw `No vectorization strategy registered for entity type: {entityType}` when none is registered, and MUST invoke `strategy->fetchEntities($options)` to obtain the work list. + +When `fetchEntities` returns `[]`, the call MUST short-circuit with `['success' => true, 'message' => 'No entities found to vectorize', 'entity_type', 'total_entities' => 0, 'total_items' => 0, 'vectorized' => 0, 'failed' => 0]`. Otherwise the orchestrator MUST iterate entities, calling `vectorizeEntity()` for each, and MUST accumulate per-entity errors into a top-level `errors` array without aborting the batch on a single failure. + +`vectorizeEntity()` MUST first call `strategy->extractVectorizationItems($entity)`. When items is `[]`, the entity MUST be skipped (returning zero counts). For non-empty items, the orchestrator MUST select serial vs parallel mode based on `options.mode` (default `'serial'`) and `options.batch_size` (default 50): parallel MUST batch into chunks of `batch_size` and call `VectorEmbeddings::generateBatchEmbeddings()`, while serial MUST call `generateEmbedding()` per item. + +After embedding generation, successful vectors MUST be passed to `storeVector()` which MUST delegate to `strategy->prepareVectorMetadata($entity, $item)` to assemble the storage payload (`entity_type`, `entity_id`, `chunk_index`, `total_chunks`, `chunk_text`, `additional_metadata`) before calling `VectorEmbeddings::storeVector(...)`. Per-item failures (null embedding or thrown exception) MUST be recorded in the `errors` array as `['entity_id' => ..., 'item_index' => ..., 'error' => ...]` and incremented in the `failed` counter. + +The aggregate response MUST be `['success' => true, 'message' => "Batch vectorization completed: {V} vectorized, {F} failed", 'entity_type', 'total_entities' => count(entities), 'total_items', 'vectorized', 'failed', 'errors']`. Top-level orchestration failures (e.g. strategy fetch throws) MUST be re-thrown after error logging — they MUST NOT be swallowed. + +`VectorizationStrategyInterface` MUST define the four-method contract: `fetchEntities(array $options): array`, `extractVectorizationItems(mixed $entity): array`, `prepareVectorMetadata(mixed $entity, array $item): array`, `getEntityIdentifier(mixed $entity): string|int`. The metadata return MUST carry `entity_type`, `entity_id`, optional `chunk_index` (default 0), optional `total_chunks` (default 1), optional `chunk_text`, optional `additional_metadata`. + +#### Scenario: Unregistered entity type throws on vectorizeBatch +- **GIVEN** no strategy has been registered for `'image'` +- **WHEN** `VectorizationService::vectorizeBatch('image', [])` is invoked +- **THEN** an `Exception` with message `No vectorization strategy registered for entity type: image` MUST be thrown +- **AND** the exception MUST be re-thrown after error logging — the orchestrator MUST NOT return a normal result + +#### Scenario: Empty fetch returns success with zero counts +- **GIVEN** a registered strategy whose `fetchEntities([])` returns `[]` +- **WHEN** `vectorizeBatch('object', [])` is invoked +- **THEN** the response MUST be `['success' => true, 'message' => 'No entities found to vectorize', 'entity_type' => 'object', 'total_entities' => 0, 'total_items' => 0, 'vectorized' => 0, 'failed' => 0]` +- **AND** no calls to `strategy->extractVectorizationItems` MUST be made + +#### Scenario: Per-entity failure does not abort the batch +- **GIVEN** three entities are returned by `fetchEntities` +- **AND** `vectorizeEntity` for the second entity throws (strategy `extractVectorizationItems` raises) +- **WHEN** the orchestrator loop completes +- **THEN** the result MUST report `total_entities: 3` +- **AND** the `errors` array MUST contain exactly one entry for the failing entity: `['entity_id' => , 'error' => ]` +- **AND** entities 1 and 3 MUST have contributed normally to `vectorized` / `failed` / `total_items` + +#### Scenario: Parallel mode batches via generateBatchEmbeddings; serial mode loops generateEmbedding +- **GIVEN** an entity that yields 10 items, with `options = ['mode' => 'parallel', 'batch_size' => 4]` +- **WHEN** `vectorizeEntity()` processes the items +- **THEN** `VectorEmbeddings::generateBatchEmbeddings()` MUST be invoked exactly three times (chunks of 4, 4, 2) +- **AND** `VectorEmbeddings::generateEmbedding()` MUST NOT be called on this path +- **AND** with `options = ['mode' => 'serial']` the inverse MUST hold (10 calls to `generateEmbedding`, 0 to `generateBatchEmbeddings`) + +#### Scenario: Per-item null embedding records error and increments failed +- **GIVEN** parallel mode returns an embedding result where index 2 has `embedding => null` +- **WHEN** the orchestrator inspects the result +- **THEN** `failed` MUST be incremented for that item +- **AND** an entry `['entity_id', 'item_index' => 2, 'error' => ]` MUST be appended to `errors` +- **AND** `storeVector` MUST NOT be called for that item + +### Requirement: The system MUST vectorise OpenRegister objects via a strategy that JSON-serialises content and folds register/schema/uuid metadata + +`ObjectVectorizationStrategy` MUST implement `VectorizationStrategyInterface`. `fetchEntities($options)` MUST call `ObjectService::searchObjects(query: ['_limit' => $options['batch_size'] ?? 25, '_source' => 'database'], _rbac: false, _multitenancy: false, ids: null, uses: null, views: $options['views'] ?? null)` and MUST return the resulting list as an array (or `[]` when the return is not an array). + +`extractVectorizationItems($entity)` MUST always produce exactly one item per object: `[['text' => json_encode($objectData, JSON_PRETTY_PRINT), 'index' => 0]]`. `$objectData` MUST come from `$entity->jsonSerialize()` (or `$entity` itself when already an array). `SettingsService::getObjectSettingsOnly()` MUST be consulted for serialisation config (`includeMetadata`, `includeRelations`, `maxNestingDepth`), but the current implementation MUST emit those values in debug logs and then ignore them (the actual encoding is unconditional `json_encode(..., JSON_PRETTY_PRINT)`). + +`prepareVectorMetadata($entity, $item)` MUST extract a title using the fallback chain: (1) `objectData['title']`, (2) `['name']`, (3) `['_name']`, (4) `['summary']`, (5) `extractFirstStringField()` — the first non-`_`/non-`@`-prefixed key whose value is a string of length 1–99 and whose key is not in the skip set `[id, uuid, description, Beschrijving, beschrijving, content, text]`, (6) terminal fallback `"Object #{$objectId}"`. Description MUST be extracted via the chain: `description`, `_description`, `Beschrijving`, `beschrijving`, `summary`, `_summary`, default `""`. + +Register / schema / UUID / URI MUST be folded from multiple possible locations into the metadata: `register` from `_register` → `register` → `@self.register`; `schema` from `_schema` → `schema` → `@self.schema`; `uuid` from `uuid` → `_uuid` → `@self.id`; `uri` from `uri` → `_uri` → `@self.uri`. The returned metadata MUST be: + +``` +['entity_type' => 'object', + 'entity_id' => (string) $objectId, // 'unknown' when id is missing + 'chunk_index' => 0, + 'total_chunks' => 1, + 'chunk_text' => substr($item['text'], 0, 500), + 'additional_metadata' => [ + 'object_id', 'object_title', 'title', 'name', 'description', + 'register', 'register_id', 'schema', 'schema_id', 'uuid', 'uri' + ]] +``` + +`title` and `object_title` and `name` MUST all carry the same resolved title value. `register` and `register_id` MUST carry the same value (likewise `schema` / `schema_id`). The `chunk_text` field MUST be truncated to 500 chars of the serialised JSON. + +`getEntityIdentifier($entity)` MUST return `objectData['id']` when set, else the string `'unknown'`. + +#### Scenario: Single item produced per object regardless of size +- **GIVEN** an `ObjectEntity` whose `jsonSerialize()` returns a 50 KB object +- **WHEN** `extractVectorizationItems($entity)` is invoked +- **THEN** the result MUST be an array of length exactly 1 +- **AND** that item MUST be `['text' => , 'index' => 0]` +- **AND** the JSON MUST include all top-level keys present in the source object + +#### Scenario: Title fallback chain reaches `extractFirstStringField` when standard keys missing +- **GIVEN** an object `{label: "Resident #42", description: "Some long text", other: 12345}` +- **WHEN** `prepareVectorMetadata` extracts the title +- **THEN** the title MUST be `"Resident #42"` (matched via `extractFirstStringField` because `title`, `name`, `_name`, and `summary` are all missing, and `label` is the first eligible non-skip key) +- **AND** the title MUST NOT be `12345` (integer is rejected) and MUST NOT be `"Some long text"` (description is in the skip set) + +#### Scenario: Register/schema folded from multiple legacy locations +- **GIVEN** an object whose top-level lacks `register`/`schema` but has `@self: {register: "9", schema: "12", id: "abc-uuid"}` +- **WHEN** `prepareVectorMetadata` runs +- **THEN** the returned `additional_metadata` MUST have `register: "9"`, `register_id: "9"`, `schema: "12"`, `schema_id: "12"`, `uuid: "abc-uuid"` +- **AND** the same values MUST be present even if the object instead used `_register: "9"`, `_schema: "12"`, `_uuid: "abc-uuid"` + +#### Scenario: Missing id falls back to entity_id 'unknown' +- **GIVEN** an object whose `jsonSerialize()` returns no `id` key +- **WHEN** `getEntityIdentifier($entity)` is invoked +- **THEN** the result MUST be the string `'unknown'` +- **AND** the metadata `entity_id` MUST also be `'unknown'` +- **AND** the metadata `object_title` MUST be `"Object #unknown"` + +#### Scenario: chunk_text is truncated to 500 chars +- **GIVEN** a serialised object JSON of length 12 000 chars +- **WHEN** `prepareVectorMetadata` populates `chunk_text` +- **THEN** the resulting `chunk_text` MUST be exactly the first 500 chars of `$item['text']` + +### Requirement: The system MUST expose vector statistics and surface embedding-model rotation needs + +`VectorizationService::getVectorStats()` MUST resolve the configured backend (same logic as REQ-002), then delegate to `VectorStatsHandler::getStats(string $backend)`. On `'solr'` the handler MUST query the configured object/file collections via `_embedding_:* facet=true facet.field=_embedding_model_` and aggregate counts; on any other backend it MUST run three queryBuilder calls on `openregister_vectors` (total via `COUNT(*)`, breakdown via `GROUP BY entity_type`, breakdown via `GROUP BY embedding_model`). The returned shape MUST be `['total_vectors', 'by_type' => array, 'by_model' => array, 'object_vectors' => int, 'file_vectors' => int]`. Solr responses MUST additionally include `'source' => 'solr' | 'solr_error' | 'solr_unavailable'`. On any exception the handler MUST return `['total_vectors' => 0, 'by_type' => [], 'by_model' => []]` rather than throw. + +`VectorEmbeddings::checkEmbeddingModelMismatch()` MUST compare the currently-configured embedding model (from `SettingsService::getLLMSettingsOnly()`, mapped by provider: `openaiConfig.model`, `ollamaConfig.model`, `fireworksConfig.embeddingModel`) against the distinct `embedding_model` values found in `openregister_vectors`. The response MUST be one of: + +- `['has_vectors' => false, 'mismatch' => false, 'message' => 'No embedding model configured']` — when no current model is configured. +- `['has_vectors' => false, 'mismatch' => false, 'current_model' => string, 'message' => 'No vectors exist yet']` — when the table is empty. +- `['has_vectors' => true, 'mismatch' => bool, 'current_model', 'existing_models' => array, 'total_vectors' => int, 'null_model_count' => int, 'mismatched_models' => array, 'message' => string]` — when vectors exist. `mismatch` MUST be `true` if any stored model differs from current OR if `null_model_count > 0`. + +The user-facing `message` MUST be one of three strings depending on state: `Multiple embedding models detected. Consider re-embedding all vectors with a single model.` (any drift), `{N} vectors have no model information.` (only NULL models), `All vectors use the same embedding model.` (no drift). On exception the response MUST be `['has_vectors' => false, 'mismatch' => false, 'error' => string]`. + +`VectorEmbeddings::clearAllEmbeddings()` MUST count the rows in `openregister_vectors`, return `['success' => true, 'deleted' => 0, 'message' => 'No vectors to delete']` when empty, otherwise `DELETE FROM openregister_vectors` and return `['success' => true, 'deleted' => int, 'message' => "Deleted {N} vectors successfully"]`. On exception the response MUST be `['success' => false, 'error' => string, 'message' => 'Failed to clear embeddings: ...']` — the call MUST NOT throw. + +#### Scenario: Database stats return total + by-type + by-model breakdown +- **GIVEN** `openregister_vectors` contains 100 rows: 70 with `entity_type='object'`, 30 with `entity_type='file'`, split across models `text-embedding-3-small` (60) and `nomic-embed-text` (40) +- **WHEN** `VectorizationService::getVectorStats()` is invoked with backend = `'php'` +- **THEN** the response MUST be `['total_vectors' => 100, 'by_type' => ['object' => 70, 'file' => 30], 'by_model' => ['text-embedding-3-small' => 60, 'nomic-embed-text' => 40], 'object_vectors' => 70, 'file_vectors' => 30]` +- **AND** no `'source'` key MUST be present (that key is Solr-only) + +#### Scenario: Solr stats include `source` discriminator +- **GIVEN** backend = `'solr'` and `IndexService::getBackend()->isAvailable() === false` +- **WHEN** `getVectorStats()` is invoked +- **THEN** the response MUST include `'source' => 'solr_unavailable'` and all numeric fields zeroed +- **AND** on a successful Solr call the response MUST include `'source' => 'solr'` +- **AND** on a thrown error inside Solr query path the response MUST include `'source' => 'solr_error'` + +#### Scenario: Model mismatch flagged when stored vectors use a different model than current settings +- **GIVEN** current `openaiConfig.model` is `text-embedding-3-large` and the vectors table holds 50 rows with `embedding_model = text-embedding-ada-002` +- **WHEN** `checkEmbeddingModelMismatch()` is invoked +- **THEN** the response MUST be `['has_vectors' => true, 'mismatch' => true, 'current_model' => 'text-embedding-3-large', 'existing_models' => ['text-embedding-ada-002'], 'total_vectors' => 50, 'null_model_count' => 0, 'mismatched_models' => ['text-embedding-ada-002'], 'message' => 'Multiple embedding models detected. Consider re-embedding all vectors with a single model.']` + +#### Scenario: NULL embedding_model counted separately and surfaces re-embed need +- **GIVEN** the vectors table holds 30 rows with `embedding_model = text-embedding-3-large` (matching current settings) and 5 rows with `embedding_model IS NULL` +- **WHEN** `checkEmbeddingModelMismatch()` is invoked +- **THEN** the response MUST include `mismatch: true` and `null_model_count: 5` and `mismatched_models: []` +- **AND** the message MUST be `5 vectors have no model information.` + +#### Scenario: clearAllEmbeddings on empty table is a no-op +- **GIVEN** `openregister_vectors` is empty +- **WHEN** `clearAllEmbeddings()` is invoked +- **THEN** the response MUST be `['success' => true, 'deleted' => 0, 'message' => 'No vectors to delete']` +- **AND** no `DELETE` statement MUST be issued + +#### Scenario: clearAllEmbeddings deletes all rows and returns count +- **GIVEN** the table holds 123 rows +- **WHEN** `clearAllEmbeddings()` is invoked +- **THEN** the table MUST be emptied via `DELETE FROM openregister_vectors` +- **AND** the response MUST be `['success' => true, 'deleted' => 123, 'message' => 'Deleted 123 vectors successfully']` + +### Requirement: Embedding requests are time-bounded + +Every outbound embedding HTTP request SHALL set a connect timeout and a read +timeout. A slow or hung provider SHALL fail the affected chunk within the timeout +and SHALL NOT block the rest of the batch or the cron run indefinitely. + +#### Scenario: Hung provider does not stall the batch + +- **WHEN** the embedding provider does not respond +- **THEN** the request fails within the configured timeout +- **AND** the chunk is marked failed and the batch continues + +### Requirement: Batch embedding uses one request per batch + +When embedding multiple texts, OpenRegister SHALL send them as a single provider +request where the provider supports array input, rather than one HTTP request per +text. + +#### Scenario: N texts embed in one request + +- **WHEN** a batch of N texts is embedded and the provider supports array input +- **THEN** a single embeddings request is issued for the batch + +### Requirement: Unchanged content is not re-embedded + +Vectorization SHALL skip chunks whose content hash is unchanged and already +embedded. A batch parameter of `0` SHALL mean a sane default page size, not an +uncapped full-table load. + +#### Scenario: Re-running on unchanged corpus does no work + +- **WHEN** vectorization runs again over an unchanged corpus +- **THEN** no embedding requests are made +- **AND** changing one chunk re-embeds only that chunk + +### Requirement: Batch vectorization runs asynchronously + +Batch vectorization SHALL run in a background job. The triggering endpoint SHALL +enqueue the work and return promptly with a status handle, not process the batch +synchronously in the request. + +#### Scenario: Batch endpoint returns promptly + +- **WHEN** a client triggers batch vectorization +- **THEN** the endpoint enqueues a job and returns without waiting for all + embeddings to complete + diff --git a/openspec/specs/verwerkingsregister-api/spec.md b/openspec/specs/verwerkingsregister-api/spec.md index 99102b8d2b..2d13649ead 100644 --- a/openspec/specs/verwerkingsregister-api/spec.md +++ b/openspec/specs/verwerkingsregister-api/spec.md @@ -1,3 +1,7 @@ +--- +status: done +--- + # verwerkingsregister-api Specification --- @@ -8,9 +12,7 @@ status: implemented @e2e exclude GDPR verwerkingsregister REST API — covered by Newman GDPR Art 30 processing register API for querying processing activities, generating data subject access reports (inzageverzoek), and exporting the verwerkingsregister. Enables compliance auditing for Dutch government organisations. - ## Requirements - ### Requirement: The system MUST provide a verwerkingsregister (processing register) API A dedicated API endpoint MUST return an overview of all processing activities recorded in the audit trail, grouped by processing activity ID. @@ -67,3 +69,52 @@ An API endpoint MUST allow exporting audit trail entries in JSON or CSV format f #### Scenario: Export defaults to JSON - **WHEN** a GET request is made to `/api/audit-trails/export` without a `format` parameter - **THEN** the system MUST default to JSON format + +### Requirement: The system MUST provide a CRUD REST surface over the dedicated verwerkingsactiviteiten catalog + +Beyond the audit-trail-derived read views, the system MUST expose `VerwerkingsactiviteitenController` as a REST CRUD surface over the dedicated `oc_openregister_verwerkingsactiviteiten` table (distinct from the audit-trail aggregation). `index` MUST list activities with optional `status` and `organisation` query filters, returning `{count, results}`. `show` MUST resolve a path identifier that may be a numeric id, a uuid, or a short readable code, returning HTTP 404 when nothing matches. `create` and `update` MUST hydrate the string fields (`code`, `naam`, `beschrijving`, `doelbinding`, `rechtsgrond`, `bewaartermijn`, `technischeMaatregelen`, `organisatorischeMaatregelen`, `organisationId`, `status`) and array fields (`categorieenBetrokkenen`, `categorieenPersoonsgegevens`, `ontvangers`, `doorgifteBuitenEu`, `verwerkingsverantwoordelijke`, `contactgegevensFg`) from the payload, returning HTTP 201 on create and HTTP 422 on `InvalidArgumentException`. `destroy` MUST NOT hard-delete — it MUST set `status = 'archived'` and persist, returning HTTP 204, because audit-trail rows reference activities by uuid as a soft foreign key. Create, update, and destroy MUST be restricted to members of the Nextcloud `admin` group (HTTP 403 otherwise); list, show, and the verantwoording report MUST be available to any authenticated user. + +#### Scenario: List with status filter +- **WHEN** `GET /api/avg/verwerkingsactiviteiten?status=actief` is requested +- **THEN** `index` MUST return `{count, results}` containing only activities with `status = actief` +- **AND** each result MUST be the activity's `jsonSerialize()` form + +#### Scenario: Resolve by id, uuid, or code +- **GIVEN** an activity exists with a uuid and a readable code +- **WHEN** `show` is called with the numeric id, the uuid, or the code +- **THEN** each form MUST resolve to the same activity +- **AND** an unmatched identifier MUST return HTTP 404 with `{error, identifier}` + +#### Scenario: Writes are admin-gated +- **GIVEN** a non-admin authenticated user +- **WHEN** they call `create`, `update`, or `destroy` +- **THEN** the response MUST be HTTP 403 with `{error}` before any persistence +- **AND** an admin performing `create` with valid fields MUST receive HTTP 201 with the persisted activity + +#### Scenario: Delete soft-archives instead of removing +- **GIVEN** an existing activity referenced by audit-trail rows +- **WHEN** an admin calls `destroy` +- **THEN** the activity's `status` MUST be set to `archived` and persisted +- **AND** the row MUST remain resolvable by uuid +- **AND** the response MUST be HTTP 204 + +### Requirement: The system MUST provide an Art 30 §4 accountability report aggregating audit events per processing activity + +`VerwerkingsactiviteitenController::verantwoording` MUST return a verantwoordingsdocument suitable for AP supervisory review: every verwerkingsactiviteit joined with the count of audit-trail rows attributed to it via `processing_activity_id`, broken down per `action`. The aggregation MUST query `openregister_audit_trails` grouped by `processing_activity_id` and `action`, scoped to the activities' uuids. The response MUST be `{count, activities}` where each activity entry is its serialized form plus an `activity` block `{totalEvents, byAction}`. Activities with no audit rows MUST report `{totalEvents: 0, byAction: []}`. A query failure during aggregation MUST degrade to empty counts rather than failing the whole report. + +#### Scenario: Report aggregates audit counts per action +- **GIVEN** activity `A` (uuid `u1`) has 3 `create`, 2 `update`, and 5 `read` audit rows +- **WHEN** `verantwoording` is called +- **THEN** the entry for `A` MUST include `activity.byAction = {create: 3, update: 2, read: 5}` +- **AND** `activity.totalEvents` MUST equal 10 + +#### Scenario: Activity with no audit rows +- **GIVEN** activity `B` has no audit-trail rows referencing it +- **WHEN** `verantwoording` is called +- **THEN** the entry for `B` MUST include `activity = {totalEvents: 0, byAction: []}` + +#### Scenario: Aggregation failure degrades gracefully +- **GIVEN** the audit-trail aggregation query throws +- **WHEN** `verantwoording` runs +- **THEN** every activity MUST report `{totalEvents: 0, byAction: []}` and the report MUST still return HTTP 200 + diff --git a/openspec/specs/webhook-payload-mapping/spec.md b/openspec/specs/webhook-payload-mapping/spec.md index 0deeffe199..254a6c5715 100644 --- a/openspec/specs/webhook-payload-mapping/spec.md +++ b/openspec/specs/webhook-payload-mapping/spec.md @@ -1,5 +1,5 @@ --- -status: implemented +status: done --- # Webhook Payload Mapping @@ -537,6 +537,80 @@ enrichment). The engine MUST support the following semantics: - **WHEN** `executeMapping()` runs - **THEN** the mapping MUST be applied to each element and the results returned keyed as the input +### Requirement: Webhook delivery logs MUST be listable per-webhook and globally +`WebhooksController` MUST expose two delivery-log listing endpoints in addition to the +already-specified `logStats` endpoint: `GET /api/webhooks/{id}/logs` (per-webhook) and +`GET /api/webhooks/logs` (all webhooks). Both are `@NoAdminRequired` / `@NoCSRFRequired`, accept +`limit` (default 50) and `offset` (default 0) query parameters, and return `{ results, total }`. +The per-webhook endpoint MUST validate webhook existence first and the global endpoint MUST +support optional `webhook_id` and `success` filtering. + +#### Scenario: List logs for a specific webhook +- **GIVEN** webhook ID `7` exists with delivery history +- **WHEN** `GET /api/webhooks/7/logs?limit=50&offset=0` is called +- **THEN** the response MUST return HTTP 200 with `{ "results": [...WebhookLog...], "total": }` +- **AND** logs MUST be fetched via `WebhookLogMapper::findByWebhook(webhookId, limit, offset)` + +#### Scenario: Logs for a non-existent webhook +- **GIVEN** no webhook exists with ID `999` +- **WHEN** `GET /api/webhooks/999/logs` is called +- **THEN** the response MUST return HTTP 404 with `{ "error": "Webhook not found" }` + +#### Scenario: Log retrieval failure +- **GIVEN** the log mapper throws a non-`DoesNotExistException` +- **WHEN** the logs endpoint is called +- **THEN** the error MUST be logged and the response MUST return HTTP 500 with `{ "error": "Failed to retrieve webhook logs" }` + +#### Scenario: List all logs with default total +- **WHEN** `GET /api/webhooks/logs` is called with no filters +- **THEN** the response MUST return paginated logs from `WebhookLogMapper::findAll(limit, offset)` +- **AND** `total` MUST be the count of all logs (unpaginated) + +#### Scenario: Filter all logs by webhook_id +- **GIVEN** `GET /api/webhooks/logs?webhook_id=7` +- **WHEN** `webhook_id` is non-empty and not `"0"` +- **THEN** logs MUST be scoped to webhook `7` via `findByWebhook()` and `total` MUST reflect that webhook's full count + +#### Scenario: Filter all logs by success status +- **GIVEN** `GET /api/webhooks/logs?success=false` +- **WHEN** `success` is one of `true`/`1`/`false`/`0` +- **THEN** the returned logs MUST be filtered to entries whose `getSuccess()` matches the boolean +- **AND** `total` MUST be recomputed against the success-filtered full set + +### Requirement: Webhook transport internals MUST configure delivery and normalize event identifiers + +The webhook delivery layer MUST initialize an HTTP client tolerant of webhook endpoints, resolve dot-notation filter keys against the payload, and normalize event identifiers between the dotted event-type form and the fully qualified event class form. These helpers underpin filtering, the standard payload, and request interception. + +#### Scenario: HTTP client is configured for webhook delivery + +- **GIVEN** the webhook service is constructed +- **WHEN** `initializeHttpClient()` runs +- **THEN** it MUST build a Guzzle client with `timeout: 30`, `connect_timeout: 10`, `verify: false` (self-signed endpoints allowed), `allow_redirects: true`, and `http_errors: false` so 4xx/5xx responses are handled manually rather than thrown + +#### Scenario: Dot-notation filter key resolution + +- **GIVEN** a payload and a filter key such as `object.status` +- **WHEN** `getNestedValue()` traverses the payload +- **THEN** it MUST descend each dot-separated segment and return the nested value, or `null` when any segment is absent + +#### Scenario: Short event name derivation + +- **GIVEN** a fully qualified event class such as `OCA\OpenRegister\Event\ObjectCreatedEvent` +- **WHEN** `getShortEventName()` runs +- **THEN** it MUST return the trailing class segment `ObjectCreatedEvent`, which is the value enriched onto mapping input as `event` + +#### Scenario: Event-type to event-class mapping + +- **GIVEN** a dotted interception event type such as `object.creating` +- **WHEN** `eventTypeToEventClass()` runs +- **THEN** it MUST return `OCA\OpenRegister\Event\ObjectCreatingEvent`, capitalizing the entity and action segments and defaulting the action to `created` when absent + +#### Scenario: CloudEvent datacontenttype derives from the request + +- **GIVEN** an intercepted HTTP request being formatted as a CloudEvent +- **WHEN** `CloudEventFormatter::getContentTypeHeader()` runs +- **THEN** it MUST return the request's `Content-Type` header when present, otherwise default to `application/json` + ## Current Implementation Status **Fully implemented.** All core requirements are in place: diff --git a/openspec/specs/workflow-engine-abstraction/spec.md b/openspec/specs/workflow-engine-abstraction/spec.md index 006a182e83..0fd1a08f28 100644 --- a/openspec/specs/workflow-engine-abstraction/spec.md +++ b/openspec/specs/workflow-engine-abstraction/spec.md @@ -1,9 +1,8 @@ --- -status: implemented +status: done --- # Workflow Engine Abstraction - # Workflow Engine Abstraction ## Purpose @@ -16,9 +15,7 @@ Provides an engine-agnostic interface for OpenRegister to interact with workflow OpenRegister needs to trigger external workflow engines for validation, enrichment, notifications, and automation. Currently n8n runs as a Nextcloud ExApp (FastAPI proxy to n8n at :5678) and Windmill exists as a separate ExApp. Rather than coupling to either engine, OpenRegister defines a shared interface (`WorkflowEngineInterface`) with per-engine adapters (`N8nAdapter`, `WindmillAdapter`). The `WorkflowEngineRegistry` service manages engine configurations, resolves the correct adapter for each request, encrypts credentials via `ICrypto`, and supports auto-discovery of installed ExApps via `IAppManager`. Multiple engines can be active simultaneously. Each individual hook on a schema specifies which engine it uses, so a single schema can have hooks targeting different engines (e.g., hook 1 uses n8n for validation, hook 2 uses Windmill for enrichment). - ## Requirements - ### Requirement: Engine Interface Definition Each engine adapter MUST implement the `WorkflowEngineInterface` PHP interface, providing a unified contract for workflow lifecycle management and execution. The interface MUST define methods for deploying, updating, retrieving, deleting, activating, deactivating, and executing workflows, as well as listing workflows, obtaining webhook URLs, and performing health checks. All adapters MUST accept configuration via a `configure(string $baseUrl, array $authConfig)` method that sets the engine connection parameters before any API calls. @@ -517,6 +514,107 @@ Workflows deployed through the import pipeline MUST be tracked via the `Deployed - **WHEN** `DeployedWorkflowMapper::findBySchema("organisation")` is called - **THEN** all three workflows MUST be returned for export purposes +### Requirement: Lifecycle Transition HTTP Surface + +The system MUST provide a sugar HTTP entry point over the lifecycle transition +engine so apps adopting the `x-openregister-lifecycle` annotation do not write a +per-schema action endpoint. `TransitionController::transition()` MUST read the +transition `action` from the request body (HTTP 400 when missing or empty), +delegate to `TransitionEngine::transition()`, and map failures to distinct +statuses: HTTP 403 when the caller lacks `update` permission +(`NotAuthorizedException`), HTTP 422 when a hook rejects the save +(`HookStoppedException`) or the transition is refused/invalid (`RuntimeException`), +and the serialized object on success. `availableActions()` MUST return the list +of actions allowed from the object's current state, mapping `NotAuthorizedException` +to HTTP 403 and a missing object/schema to HTTP 404. + +#### Scenario: Apply a named transition from the request body +- **GIVEN** an object that supports a `publish` transition and a caller with `update` permission +- **WHEN** a request is sent to the transition endpoint with body `{"action": "publish"}` +- **THEN** `TransitionEngine::transition()` MUST be invoked with that action and object id +- **AND** the response MUST be the transitioned object's `jsonSerialize()` output + +#### Scenario: Missing action is rejected +- **GIVEN** a transition request with no `action` field +- **WHEN** `transition()` validates the body +- **THEN** the response MUST be HTTP 400 with `error: 'Missing required field "action".'` + +#### Scenario: Transition error mapping +- **GIVEN** a transition request +- **WHEN** the engine throws +- **THEN** `NotAuthorizedException` MUST map to HTTP 403, `HookStoppedException` and `RuntimeException` MUST map to HTTP 422 + +#### Scenario: List available actions from current state +- **GIVEN** an object in a known lifecycle state +- **WHEN** `availableActions()` runs +- **THEN** the response MUST be `{"actions": [...]}` for the actions allowed from that state +- **AND** a missing object MUST map to HTTP 404, an unauthorized caller to HTTP 403 + +### Requirement: Storage Migration HTTP Surface + +The system MUST expose a storage-migration HTTP surface for moving object data +between blob storage and magic tables. `MigrationController::migrate()` MUST +require `register` and `schema` parameters (HTTP 400 when missing), validate +`direction` against `to-magic`/`to-blob` (HTTP 400 otherwise), accept optional +`batchSize` (default 100) and `dryRun` (boolean) parameters, resolve the +register/schema via `MigrationService::resolveRegisterAndSchema()`, and run the +matching migration returning the migration report. `status()` MUST return the +storage status for a register/schema pair. Errors MUST return HTTP 500 with an +`error` message. + +#### Scenario: Trigger a migration to magic tables +- **GIVEN** a request with `register`, `schema`, and `direction=to-magic` +- **WHEN** `migrate()` runs +- **THEN** `MigrationService::migrateToMagicTable()` MUST be invoked with the resolved register/schema, `batchSize`, and `dryRun` +- **AND** the migration report MUST be returned + +#### Scenario: Invalid direction is rejected +- **GIVEN** a migration request with `direction` other than `to-magic` or `to-blob` +- **WHEN** `migrate()` validates the direction +- **THEN** the response MUST be HTTP 400 with `error: 'direction must be "to-magic" or "to-blob"'` + +#### Scenario: Missing register or schema is rejected +- **GIVEN** a migration request missing `register` or `schema` +- **WHEN** `migrate()` validates the parameters +- **THEN** the response MUST be HTTP 400 with `error: 'register and schema parameters are required'` + +#### Scenario: Report storage status for a pair +- **GIVEN** a request for a register/schema pair +- **WHEN** `status()` runs +- **THEN** the response MUST be the `MigrationService::getStorageStatus()` result for the resolved pair + +### Requirement: Magic-Table Sync HTTP Surface + +The system MUST expose a magic-table synchronisation HTTP surface that updates a +schema-backed table structure to match its schema without dropping or recreating +the table. `TablesController::sync()` MUST accept a register and schema reference +(numeric id or slug), resolve both (HTTP 404 when either is not found), invoke +`MagicMapper::syncTableForRegisterSchema()`, and return a result reporting the +columns added, removed, de-required, re-required, and unchanged plus metadata and +property counts. `syncAll()` MUST iterate every register/schema pair, accumulate +per-pair success results and per-pair errors without aborting on a single +failure, and return a summary with `synced`, `errors`, `totalSynced`, and +`totalErrors`. On unexpected failure both endpoints MUST return HTTP 500 with an +`error` message. + +#### Scenario: Sync the magic table for one register/schema pair +- **GIVEN** a request for an existing register and schema +- **WHEN** `sync()` runs +- **THEN** `MagicMapper::syncTableForRegisterSchema()` MUST be invoked +- **AND** the response MUST include column add/remove/de-require/re-require/unchanged statistics and the resolved table name + +#### Scenario: Unknown register or schema returns 404 +- **GIVEN** a sync request whose register or schema cannot be resolved +- **WHEN** the controller resolves the references +- **THEN** the response MUST be HTTP 404 with the appropriate `error` + +#### Scenario: Sync all pairs tolerates per-pair failures +- **GIVEN** several register/schema pairs where one sync throws +- **WHEN** `syncAll()` runs +- **THEN** the failing pair MUST be recorded in `errors` while the others succeed +- **AND** the response MUST report `totalSynced` and `totalErrors` +- **AND** `success` MUST be true only when `errors` is empty + ## Non-Requirements - This spec does NOT define how workflows are triggered by object lifecycle events (see Schema Hooks spec) - This spec does NOT define the import format for bundling workflows with schemas (see Workflow-in-Import spec) diff --git a/openspec/specs/workflow-in-import/spec.md b/openspec/specs/workflow-in-import/spec.md index aceee01149..c973dadce0 100644 --- a/openspec/specs/workflow-in-import/spec.md +++ b/openspec/specs/workflow-in-import/spec.md @@ -1,5 +1,5 @@ --- -status: implemented +status: done --- # Workflow in Import diff --git a/openspec/specs/workflow-integration/spec.md b/openspec/specs/workflow-integration/spec.md index a49e93479a..e112add5c4 100644 --- a/openspec/specs/workflow-integration/spec.md +++ b/openspec/specs/workflow-integration/spec.md @@ -1,5 +1,5 @@ --- -status: implemented +status: done --- # Workflow Integration diff --git a/openspec/specs/workflow-operations/spec.md b/openspec/specs/workflow-operations/spec.md index a8e895daf1..41a7bf3191 100644 --- a/openspec/specs/workflow-operations/spec.md +++ b/openspec/specs/workflow-operations/spec.md @@ -1,3 +1,7 @@ +--- +status: done +--- + # workflow-operations Specification ## Purpose diff --git a/openspec/specs/zoeken-filteren/spec.md b/openspec/specs/zoeken-filteren/spec.md index 001fabc9c5..657a5dfcd2 100644 --- a/openspec/specs/zoeken-filteren/spec.md +++ b/openspec/specs/zoeken-filteren/spec.md @@ -1,5 +1,5 @@ --- -status: implemented +status: in-progress retrofit_extensions: - search-trail-analytics-dashboard --- @@ -12,6 +12,9 @@ retrofit_extensions: Provide a comprehensive, backend-agnostic search and filtering system for register objects that supports full-text search with relevance ranking, field-level filtering with comparison operators, faceted drill-down navigation, multi-field sorting, cursor and offset pagination, and saved search trails. The system MUST transparently operate against PostgreSQL (with optional pg_trgm fuzzy matching), Apache Solr, or Elasticsearch as interchangeable backends, while exposing a single unified API surface through `ObjectService.searchObjectsPaginated()` and `SearchBackendInterface`. **Tender demand**: 78% of analyzed government tenders require advanced search and filtering capabilities, including full-text search, faceted navigation, and multi-criteria filtering across structured data. + +**OpenSpec changes** +- `searchable-property-index` (in progress) — backs the existing `_name` fuzzy/full-text search with a `pg_trgm` GIN index (was an unindexed `similarity()`/`ILIKE` scan); adds an opt-in `searchable: true` schema-property flag (mirroring `facetable`) for indexed search on other string properties. ## Requirements ### Requirement: Full-text search across object properties The system MUST support free-text search across all string-typed properties of register objects. The `_search` query parameter MUST trigger a case-insensitive search that matches against every string column in the schema's dynamic table, plus the metadata fields `_name`, `_description`, and `_summary`. Search MUST be performed using SQL `ILIKE` patterns in the database backend and native query parsing in Solr/Elasticsearch. @@ -549,6 +552,142 @@ The search-trail sidebar (`SearchTrailSideBar.vue`) MUST render a read-only anal - **THEN** it MUST return `75` - **AND** when the total is `0` it MUST return `0` without dividing by zero +### Requirement: AggregationRunner MUST dispatch by configured search backend + +When the runner executes a named aggregation, it MUST consult `SchemaIndexService::getBackend($schema)` and dispatch to the backend-native `aggregate()` implementation when available. The dispatch order MUST be: + +1. Solr — when the schema is Solr-indexed and the metric is in Solr's facet/stats vocabulary. +2. Elasticsearch — when the schema is ES-indexed. +3. Postgres — when neither index is configured (uses the magic table directly). +4. PHP runner — fallback when any backend rejects the input shape. + +#### Scenario: Solr-indexed schema uses facet path +- GIVEN an `ActionItem` schema with `searchable: true` and a Solr collection synced +- AND a `byStatus` aggregation declared with `metric: count, groupBy: { field: "taskStatus" }` +- WHEN the controller calls `GET /api/objects/aggregations/decidesk/action-item/byStatus` +- THEN the response carries `backend: "solr"` +- AND the value matches what the PHP runner would compute + +### Requirement: Postgres backend MUST translate operator filters to SQL + +When the runner uses the Postgres backend, it MUST translate `in`/`gte`/`lte`/`gt`/`lt`/`ne` operators to SQL clauses (`IN (...)`, `>= ?`, `<= ?`, `> ?`, `< ?`, `<> ?`) and bind concrete values for placeholder strings (`$now`, `$startOfMonth`, etc.) at query time. v1 only supported equality filters and fell back to the PHP runner for everything else. + +#### Scenario: Range filter with placeholder binding +- GIVEN an aggregation declared with `filters: { createdAt: { gte: "$startOfMonth" } }` +- WHEN the runner executes against the Postgres backend +- THEN the SQL emitted carries `created_at >= ?` with `$startOfMonth` bound to the current month's first-day timestamp at query time +- AND the result matches what the PHP runner would compute over the same dataset + +### Requirement: AggregationRunner MUST cache results for 60s + +The runner MUST consult `AggregationCache` before computing. Cache key: `agg:{registerSlug}:{schemaSlug}:{name}:{filtersHash}:{rbacScopeHash}`. TTL: 60 seconds. The cache MUST be evicted for the affected `(register, schema)` on any `ObjectCreatedEvent`/`ObjectUpdatedEvent`/`ObjectDeletedEvent`/`ObjectTransitionedEvent`. + +#### Scenario: cache hit returns within 5 ms +- GIVEN a previous call to `byStatus` populated the cache +- WHEN a second call arrives within 60 s +- THEN the response carries `X-OR-Cache: hit` +- AND total request time is under 5 ms (no backend roundtrip) + +### Requirement: Response MUST carry backend attribution + +Every aggregation response MUST include a `backend` field with one of `"postgres"`, `"solr"`, `"elasticsearch"`, or `"php-fallback"`. Apps and operators use this to debug slow queries. + +#### Scenario: Backend attribution surfaced on every response +- GIVEN any aggregation request +- WHEN the response is rendered +- THEN the JSON body MUST include a top-level `backend` field +- AND the value MUST be one of `"postgres"`, `"solr"`, `"elasticsearch"`, `"php-fallback"` +- AND a fallback path (e.g. unsupported metric on Solr) MUST attribute the actual backend that produced the result, not the originally-targeted one + +### Requirement: Search-backend selection and reindex administration +The system SHALL expose an admin-gated API for selecting the active search backend and for +reindexing collections. `SettingsController` provides `getSearchBackend` (current backend +config), `updateSearchBackend` (sets the active backend and signals `reload_required: +true`), `testSetupHandler` (runs Solr setup via `SetupHandler` when Solr is enabled), and +`reindexSpecificCollection` (reindexes a named collection with validated `batchSize` +1–5000 and `maxObjects` >= 0). `SolrSettingsController` provides `getSolrSettings`, +`updateSolrSettings`, `getSolrInfo`, and `getSolrDashboardStats` for the Solr settings and +status surface. + +#### Scenario: Update search backend signals reload +- **GIVEN** an admin posts `{backend: "solr"}` +- **WHEN** `updateSearchBackend` runs +- **THEN** it MUST persist the backend via `SettingsService::updateSearchBackendConfig()` and return `reload_required: true` +- **AND** an empty backend param MUST produce HTTP 400 + +#### Scenario: Reindex validates batch size +- **GIVEN** an admin requests a reindex with `batchSize=10000` +- **WHEN** `reindexSpecificCollection` runs +- **THEN** it MUST reject with HTTP 400 because batch size must be between 1 and 5000 + +#### Scenario: Setup test refuses when Solr disabled +- **GIVEN** Solr is disabled in settings +- **WHEN** `testSetupHandler` runs +- **THEN** it MUST return HTTP 400 with a "SOLR is disabled" message + +### Requirement: Solr collection, configset, and field administration +The system SHALL expose an admin-gated API for managing Solr collections, configsets, and +schema fields. `SolrController` provides `listCollections`, `listConfigSets`, +`createCollection`, `createConfigSet`, `deleteConfigSet`, and `copyCollection`. +`SolrManagementController` provides `getSolrFields`, `createMissingSolrFields`, +`fixMismatchedSolrFields`, `deleteSolrField`, `deleteSpecificSolrCollection`, and +`updateSolrCollectionAssignments`. `SolrOperationsController` provides `setupSolr`, +`testSolrConnection`, `warmupSolrIndex`, `inspectSolrIndex`, `getSolrMemoryPrediction`, +and `manageSolr`. `ConfigurationSettingsController` provides `getObjectCollectionFields` +and `createMissingObjectFields` to inspect and mirror the object collection's field set. + +#### Scenario: Create a Solr collection +- **WHEN** `SolrController::createCollection` runs with a collection name +- **THEN** it MUST create the collection and report the outcome + +#### Scenario: Synchronize missing object-collection fields +- **GIVEN** the object collection is configured +- **WHEN** `ConfigurationSettingsController::createMissingObjectFields` runs +- **THEN** it MUST mirror schemas into the collection via `IndexService::mirrorSchemas(force: true)` and report success +- **AND** an unconfigured object collection MUST produce HTTP 400 + +#### Scenario: Inspect and warm the Solr index +- **WHEN** `SolrOperationsController::warmupSolrIndex` then `inspectSolrIndex` run +- **THEN** warmup MUST populate the index and inspection MUST report current index state + +### Requirement: Saved views MUST be managed through an access-controlled CRUD lifecycle with a single default per user + +`ViewService` MUST manage saved `View` definitions (the `{registers, schemas, filters, searchTerms}` query objects consumed by `SearchQueryHandler.applyViewsToQuery()`) with owner/public access control and a single-default-view-per-user invariant. + +- `find(id, owner)` MUST load the view by id and grant access only when the caller is the owner OR the view is public; otherwise it MUST throw `DoesNotExistException("View not found or access denied")` (denial is indistinguishable from not-found, so a private view's existence is not leaked). +- `findAll(owner)` MUST return the views the user owns or that are public, delegating to `ViewMapper::findAll(owner:)`. +- `create(name, description, owner, isPublic, isDefault, query)` MUST persist a new `View`; when `isDefault` is `true` it MUST first clear any existing default for that owner so at most one default view exists per user; `favoredBy` MUST be initialised to an empty list. +- `update(id, name, description, owner, isPublic, isDefault, query, favoredBy?)` MUST resolve the view via the access-controlled `find()`, and when it is being newly promoted to default (`isDefault` true and previously false) MUST clear the owner's existing default first; `favoredBy` MUST be updated only when explicitly provided (non-null). +- `delete(id, owner)` MUST resolve the view via the access-controlled `find()` before deleting, so a caller cannot delete a view they cannot access. +- The single-default invariant MUST be enforced by `clearDefaultForUser(owner)`, which unsets `isDefault` on every default view owned by that user. +- All write operations MUST log and re-throw on failure (the persisted state is the mapper's; the service does not swallow errors). + +#### Scenario: Access control on find hides other users' private views +- **GIVEN** a private view owned by `alice` +- **WHEN** `find(id, owner: "bob")` is called and the view is not public +- **THEN** the method MUST throw `DoesNotExistException("View not found or access denied")` +- **AND** when `bob` is the owner OR the view is public, the view MUST be returned + +#### Scenario: Creating a default view clears the previous default +- **GIVEN** user `alice` already has a default view +- **WHEN** `create(..., isDefault: true, ...)` is called for `alice` +- **THEN** `clearDefaultForUser("alice")` MUST run first so the prior default is unset +- **AND** exactly one of `alice`'s views MUST have `isDefault = true` afterwards +- **AND** the new view's `favoredBy` MUST be an empty list + +#### Scenario: Update only clears the default when newly promoting +- **GIVEN** a view that is currently not the default +- **WHEN** `update(..., isDefault: true, ...)` promotes it to default +- **THEN** the owner's existing default MUST be cleared first +- **AND** when the view was already default (`isDefault` unchanged) no extra clear MUST occur +- **AND** `favoredBy` MUST be changed only when a non-null `favoredBy` argument is supplied + +#### Scenario: Delete is access-controlled +- **GIVEN** a view the caller does not own and that is not public +- **WHEN** `delete(id, owner)` is called +- **THEN** the access-controlled `find()` MUST throw before any deletion occurs +- **AND** an owned-or-public view MUST be deleted via `ViewMapper::delete()` + ## Current Implementation Status **Substantially implemented.** The search and filtering system is mature with comprehensive SQL-based and Solr-based backends. diff --git a/package-lock.json b/package-lock.json index d18291cbcc..85ddc5aa09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,33161 +1,33859 @@ { - "name": "openregister", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "openregister", - "version": "1.0.0", - "license": "EUPL-1.2", - "dependencies": { - "@codemirror/lang-json": "^6.0.1", - "@conduction/nextcloud-vue": "^1.0.0-beta.103", - "@fortawesome/fontawesome-svg-core": "^6.5.2", - "@fortawesome/free-solid-svg-icons": "^6.5.2", - "@nextcloud/axios": "~2.5.1", - "@nextcloud/dialogs": "^6.1.1", - "@nextcloud/initial-state": "^2.2.0", - "@nextcloud/l10n": "^3.2.0", - "@nextcloud/router": "^3.0.0", - "@nextcloud/vue": "^8.39.0", - "@vueuse/core": "^10.7.2", - "apexcharts": "^4.0.0", - "axios": "^1.7.3", - "bootstrap": "^5.3.2", - "bootstrap-vue": "^2.23.1", - "css-loader": "^6.8.1", - "marked": "^16.4.0", - "pinia": "^2.1.7", - "remark-cli": "^11.0.0", - "remark-lint-list-item-indent": "^3.1.1", - "remark-preset-lint-consistent": "^5.1.1", - "remark-preset-lint-recommended": "^6.1.2", - "style-loader": "^3.3.3", - "vue": "^2.7.16", - "vue-apexcharts": "^1.7.0", - "vue-codemirror6": "^1.1.5", - "vue-draggable-plus": "^0.2.6", - "vue-frag": "^1.4.3", - "vue-loader": "^15.11.1", - "vue-loading-overlay": "^6.0.3", - "vue-material-design-icons": "^5.2.0", - "vue-template-compiler": "^2.7.16", - "zod": "^3.22.4" - }, - "devDependencies": { - "@babel/core": "^7.23.9", - "@babel/eslint-parser": "^7.28.6", - "@babel/plugin-transform-typescript": "^7.26.8", - "@babel/preset-env": "^7.23.9", - "@babel/preset-typescript": "^7.26.0", - "@babel/traverse": "^7.23.9", - "@cyclonedx/cyclonedx-npm": "^4.2.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.39.1", - "@nextcloud/browserslist-config": "^2.3.0", - "@nextcloud/eslint-config": "^8.4.1", - "@nextcloud/eslint-plugin": "^2.2.1", - "@nextcloud/stylelint-config": "^2.4.0", - "@nextcloud/webpack-vue-config": "^5.5.0", - "@pinia/testing": "^0.1.3", - "@playwright/test": "^1.49.0", - "@stoplight/spectral-cli": "^6.15.0", - "@types/jest": "^29.5.12", - "@types/node": "^20.17.23", - "@vue/eslint-config-typescript": "^13.0.0", - "@vue/test-utils": "^2.4.4", - "@vue/vue2-jest": "^29.2.6", - "babel-jest": "^29.7.0", - "babel-loader": "^9.1.3", - "eslint": "^8.56.0", - "eslint-config-standard": "^17.1.0", - "eslint-import-resolver-alias": "^1.1.2", - "eslint-import-resolver-exports": "^1.0.0-beta.5", - "eslint-import-resolver-typescript": "^3.10.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsdoc": "^46.10.1", - "eslint-plugin-n": "^16.6.2", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-vue": "^9.21.1", - "eslint-webpack-plugin": "^4.0.1", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-transform-stub": "^2.0.0", - "postcss": "^8.4.31", - "postcss-html": "^1.8.1", - "stylelint": "^15.11.0", - "stylelint-config-recommended-scss": "^13.1.0", - "stylelint-config-recommended-vue": "^1.6.1", - "stylelint-webpack-plugin": "^4.1.1", - "ts-jest": "^29.1.2", - "ts-loader": "^9.5.1", - "typescript": "^5.8.2", - "vue-router": "^3.6.5" - }, - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - }, - "peerDependencies": { - "vue": "^2.6 || ^3.0", - "vue-frag": "^1.0.0" - }, - "peerDependenciesMeta": { - "vue-frag": { - "optional": true - } - } - }, - "node_modules/@asyncapi/specs": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.9.0.tgz", - "integrity": "sha512-gatFEH2hfJXWmv3vogIjBZfiIbPRC/ISn9UEHZZLZDdMBO0USxt3AFgCC9AY1P+eNE7zjXddXCIT7gz32XOK4g==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.11" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz", - "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz", - "integrity": "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "debug": "^4.4.3", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.11" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.8.tgz", - "integrity": "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", - "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.28.6", - "@babel/plugin-syntax-import-attributes": "^7.28.6", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.29.0", - "@babel/plugin-transform-async-to-generator": "^7.28.6", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.6", - "@babel/plugin-transform-class-properties": "^7.28.6", - "@babel/plugin-transform-class-static-block": "^7.28.6", - "@babel/plugin-transform-classes": "^7.28.6", - "@babel/plugin-transform-computed-properties": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.28.6", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.6", - "@babel/plugin-transform-exponentiation-operator": "^7.28.6", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.28.6", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", - "@babel/plugin-transform-numeric-separator": "^7.28.6", - "@babel/plugin-transform-object-rest-spread": "^7.28.6", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.28.6", - "@babel/plugin-transform-optional-chaining": "^7.28.6", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.28.6", - "@babel/plugin-transform-private-property-in-object": "^7.28.6", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.29.0", - "@babel/plugin-transform-regexp-modifiers": "^7.28.6", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.28.6", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.28.6", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.15", - "babel-plugin-polyfill-corejs3": "^0.14.0", - "babel-plugin-polyfill-regenerator": "^0.6.6", - "core-js-compat": "^3.48.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", - "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.4", - "@babel/parser": "^7.25.4", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.4", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "node_modules/@buttercup/fetch": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", - "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", - "license": "MIT", - "optionalDependencies": { - "node-fetch": "^3.3.0" - } - }, - "node_modules/@buttercup/fetch/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "optional": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.18.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.2.tgz", - "integrity": "sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - }, - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", - "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.6.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-css": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", - "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.0.2", - "@lezer/css": "^1.1.7" - } - }, - "node_modules/@codemirror/lang-html": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", - "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/lang-css": "^6.0.0", - "@codemirror/lang-javascript": "^6.0.0", - "@codemirror/language": "^6.4.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0", - "@lezer/css": "^1.1.0", - "@lezer/html": "^1.3.12" - } - }, - "node_modules/@codemirror/lang-javascript": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", - "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.6.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0", - "@lezer/javascript": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-json": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", - "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/json": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-xml": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", - "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.4.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/xml": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.10.8", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz", - "integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.8.4", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz", - "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.35.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", - "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.37.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", - "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", - "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.6.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@conduction/nextcloud-vue": { - "version": "1.0.0-beta.103", - "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-1.0.0-beta.103.tgz", - "integrity": "sha512-oi4E1+loZNtr2CqViYU+7W5d6TQgKJEMrhUoF3VU40Hs7GUUxOTKHIZYfg7Z6yw+J0vSXkw3GCULPFKBYGyItA==", - "license": "EUPL-1.2", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/lang-html": "^6.4.11", - "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-xml": "^6.1.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@microsoft/fetch-event-source": "^2.0.1", - "@nextcloud/dialogs": "^6.4.2", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/notify_push": "^1.0.0", - "@types/react": "^18.0.0", - "@uiw/codemirror-theme-github": "^4.25.8", - "@vueuse/core": "^10.0.0", - "ajv": "^8.20.0", - "ajv-formats": "^3.0.1", - "apexcharts": "^4.7.0", - "codemirror": "^6.0.0", - "dompurify": "^3.0.0", - "gridstack": "^10.3.1", - "leaflet": "^1.9.0", - "leaflet.markercluster": "^1.5.3", - "linkifyjs": "^4.3.3", - "lodash": "^4.17.21", - "marked": "^15.0.0", - "style-mod": "^4.0.0", - "vue-apexcharts": "^1.7.0", - "vue-codemirror6": "^1.4.3", - "vue-color": "^2.8.2", - "vue-template-compiler": "^2.7.16" - }, - "bin": { - "manifest-migrate": "src/cli/manifest-migrate.js" - }, - "peerDependencies": { - "@nextcloud/auth": "^2.0.0 || ^3.0.0", - "@nextcloud/axios": "^2.0.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^2.0.0 || ^3.0.0", - "@nextcloud/router": "^2.0.0 || ^3.0.0", - "@nextcloud/vue": "^8.0.0", - "pinia": "^2.0.0", - "vue": "^2.7.0", - "vue-frag": "^1.4.3", - "vue-material-design-icons": "^5.0.0" - } - }, - "node_modules/@conduction/nextcloud-vue/node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "license": "MIT" - }, - "node_modules/@conduction/nextcloud-vue/node_modules/@vueuse/core": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", - "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.3.0", - "@vueuse/shared": "14.3.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@conduction/nextcloud-vue/node_modules/@vueuse/metadata": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", - "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@conduction/nextcloud-vue/node_modules/@vueuse/shared": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", - "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@conduction/nextcloud-vue/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@conduction/nextcloud-vue/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@conduction/nextcloud-vue/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/@conduction/nextcloud-vue/node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", - "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^2.4.1" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", - "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - } - }, - "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", - "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", - "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.13" - } - }, - "node_modules/@cyclonedx/cyclonedx-npm": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-npm/-/cyclonedx-npm-4.2.1.tgz", - "integrity": "sha512-SOA/96sf0wsgUYCRtFkLFm6WoFhG+q1BxdC84hPSn9J3xWlH1e7OnTPJT+WNUzTqzX1nSm5JhjRX4krozu2X+g==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" - } - ], - "license": "Apache-2.0", - "dependencies": { - "@cyclonedx/cyclonedx-library": "^10.0.0", - "commander": "^14.0.0", - "normalize-package-data": "^7.0.0 || ^8.0.0", - "packageurl-js": "^2.0.1", - "spdx-expression-parse": "^3.0.1 || ^4.0.0", - "xmlbuilder2": "^3.0.2 || ^4.0.3" - }, - "bin": { - "cyclonedx-npm": "bin/cyclonedx-npm-cli.js" - }, - "engines": { - "node": ">=20.18.0", - "npm": ">=9" - }, - "optionalDependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "ajv-formats-draft2019": "^1.6.1", - "libxmljs2": "^0.35||^0.37" - } - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/@cyclonedx/cyclonedx-library": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-library/-/cyclonedx-library-10.0.0.tgz", - "integrity": "sha512-xDXf2eqzeFHdjamj6oBV3duRSfrlmsJ5+2z9tXp7q5qxJP5Awmjf4ABSutS4qkVHHj7JzKFL/EM0V0Nihc7zPg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" - } - ], - "license": "Apache-2.0", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "ajv-formats-draft2019": "^1.6.1", - "libxmljs2": "^0.35||^0.37", - "packageurl-js": "*", - "spdx-expression-parse": "*", - "xmlbuilder2": "^3.0.2||^4.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - }, - "ajv-formats": { - "optional": true - }, - "ajv-formats-draft2019": { - "optional": true - }, - "libxmljs2": { - "optional": true - }, - "packageurl-js": { - "optional": true - }, - "spdx-expression-parse": { - "optional": true - }, - "xmlbuilder2": { - "optional": true - } - } - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/hosted-git-info": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", - "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/normalize-package-data": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", - "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^9.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@cyclonedx/cyclonedx-npm/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@es-joy/jsdoccomment": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", - "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "comment-parser": "1.4.1", - "esquery": "^1.5.0", - "jsdoc-type-pratt-parser": "~4.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@file-type/xml": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@file-type/xml/-/xml-0.4.4.tgz", - "integrity": "sha512-NhCyXoHlVZ8TqM476hyzwGJ24+D5IPSaZhmrPj7qXnEVb3q6jrFzA3mM9TBpknKSI9EuQeGTKRg2DXGUwvBBoQ==", - "license": "MIT", - "dependencies": { - "sax": "^1.4.1", - "strtok3": "^10.3.4" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", - "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", - "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", - "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/console/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/console/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/core/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/core/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/reporters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@jest/reporters/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@jest/reporters/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "dev": true, - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "dev": true, - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/ternary": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", - "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", - "dev": true, - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" - }, - "node_modules/@lezer/css": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.3.tgz", - "integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.0" - } - }, - "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/html": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", - "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/javascript": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", - "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.1.3", - "@lezer/lr": "^1.3.0" - } - }, - "node_modules/@lezer/json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", - "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/xml": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", - "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@linusborg/vue-simple-portal": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@linusborg/vue-simple-portal/-/vue-simple-portal-0.1.5.tgz", - "integrity": "sha512-dq+oubEVW4UabBoQxmH97GiDa+F6sTomw4KcXFHnXEpw69rdkXFCxo1WzwuvWjoLiUVYJTyN1dtlUvTa50VcXg==", - "dependencies": { - "nanoid": "^3.1.20" - }, - "peerDependencies": { - "vue": "^2.6.6" - } - }, - "node_modules/@mapbox/hast-util-table-cell-style": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz", - "integrity": "sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==", - "dependencies": { - "unist-util-visit": "^1.4.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" - }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-visit": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", - "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", - "dependencies": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-visit-parents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", - "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", - "dependencies": { - "unist-util-is": "^3.0.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" - }, - "node_modules/@mdi/js": { - "version": "7.4.47", - "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", - "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", - "license": "Apache-2.0" - }, - "node_modules/@microsoft/fetch-event-source": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", - "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", - "license": "MIT" - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@nextcloud/auth": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.6.0.tgz", - "integrity": "sha512-VkT87+9UqpPi7O36bVEE4/MxWF8d90VQcuMlvKltsZyLSLkEGrPXgowtD75Y54k60/8SR6mXbeqBwapi8dDUbA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/router": "^3.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/axios": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-2.5.2.tgz", - "integrity": "sha512-8frJb77jNMbz00TjsSqs1PymY0nIEbNM4mVmwen2tXY7wNgRai6uXilIlXKOYB9jR/F/HKRj6B4vUwVwZbhdbw==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.1", - "@nextcloud/router": "^3.0.1", - "axios": "^1.12.2" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/browser-storage": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@nextcloud/browser-storage/-/browser-storage-0.5.0.tgz", - "integrity": "sha512-usYr4GlJQlK3hgZURvklqWb9ivi7sgsSuFqXrs7s4hl1LTS4enzPrnkQumm6nRsQruf0ITS+OBsK+oELEbvYPA==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^24 || ^22 || ^20" - } - }, - "node_modules/@nextcloud/browserslist-config": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/browserslist-config/-/browserslist-config-2.3.0.tgz", - "integrity": "sha512-1Tpkof2e9Q0UicHWahQnXXrubJoqyiaqsH9G52v3cjGeVeH3BCfa1FOa41eBwBSFe2/Jxj/wCH2YVLgIXpWbBg==", - "dev": true, - "license": "GPL-3.0-or-later", - "engines": { - "node": "^16.0.0", - "npm": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@nextcloud/capabilities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@nextcloud/capabilities/-/capabilities-1.2.1.tgz", - "integrity": "sha512-snZ0/910zzwN6PDsIlx2Uvktr1S5x0ClhDUnfPlCj7ntNvECzuVHNY5wzby22LIkc+9ZjaDKtCwuCt2ye+9p/Q==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/capabilities/node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", - "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/dialogs": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.4.2.tgz", - "integrity": "sha512-xj6fyUdb56StWt1DWeDHYnqK0Ck7EWQLUEyWtXHnneknoOV3x/Y0HPRL5L2PTJaDuVQB8TcVW53JjR38JG9W6Q==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@mdi/js": "^7.4.47", - "@nextcloud/auth": "^2.5.3", - "@nextcloud/axios": "^2.5.2", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/files": "^3.12.2", - "@nextcloud/initial-state": "^3.0.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "@types/toastify-js": "^1.12.4", - "@vueuse/core": "^11.3.0", - "cancelable-promise": "^4.3.1", - "p-queue": "^9.0.1", - "toastify-js": "^1.12.0", - "vue-frag": "^1.4.3", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "peerDependencies": { - "@nextcloud/vue": "^8.24.0", - "vue": "^2.7.16" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", - "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "license": "MIT" - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/core": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", - "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.3.0", - "@vueuse/shared": "14.3.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/metadata": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", - "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@nextcloud/dialogs/node_modules/@vueuse/shared": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", - "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@nextcloud/eslint-config": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@nextcloud/eslint-config/-/eslint-config-8.4.1.tgz", - "integrity": "sha512-ilrPxOnfVkB4dAddtkhbJmbYK9FwEVZ5oIJ2ipiE97rQz82TUZxmfEHE1tr87FbIvz0drIcREgGil3zuNWHjrg==", - "dev": true, - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.24.5", - "@babel/eslint-parser": "^7.16.5", - "@nextcloud/eslint-plugin": "^2.2.1", - "@vue/eslint-config-typescript": "^13.0.0", - "eslint": "^8.27.0", - "eslint-config-standard": "^17.1.0", - "eslint-import-resolver-exports": "^1.0.0-beta.5", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jsdoc": "^46.2.6", - "eslint-plugin-n": "^16.0.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-vue": "^9.7.0", - "typescript": "^5.0.2" - } - }, - "node_modules/@nextcloud/eslint-plugin": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@nextcloud/eslint-plugin/-/eslint-plugin-2.2.1.tgz", - "integrity": "sha512-RX+0FxpL1h2EzjNLeW0VSGTkbyWIq7WgV7QAjtyUmDbSGwf1ds9Zy5OcRkgXRHRIu/W0gB0DhS2iz9qXHphCzA==", - "dev": true, - "license": "ISC", - "dependencies": { - "fast-xml-parser": "^4.2.5", - "requireindex": "^1.2.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/@nextcloud/eslint-plugin/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@nextcloud/event-bus": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-3.3.3.tgz", - "integrity": "sha512-zIfvKmUGkXpVzRKoXrcO9hkoiKDm65fqNxy/XIbIxrQhZByPq3gDkjBpnu3V5Gs8JdYwa73R8DjzV9oH8HYhIg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@types/semver": "^7.7.0", - "semver": "^7.7.2" - }, - "engines": { - "node": "^20 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/event-bus/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@nextcloud/files": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", - "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.3", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.0.0", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "cancelable-promise": "^4.3.1", - "is-svg": "^6.1.0", - "typescript-event-target": "^1.1.1", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/initial-state": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.2.0.tgz", - "integrity": "sha512-cDW98L5KGGgpS8pzd+05304/p80cyu8U2xSDQGa+kGPTpUFmCbv2qnO5WrwwGTauyjYijCal2bmw82VddSH+Pg==", - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - } - }, - "node_modules/@nextcloud/l10n": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-3.4.1.tgz", - "integrity": "sha512-aTFinTcKiK2gEXwLgutXekpZZ8/v/4QiC8C3QCLH5m0o+WtxsBC+fqV142ebC/rfDnzCLhY4ZtswSu8bFbZocg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/router": "^3.0.1", - "@nextcloud/typings": "^1.9.1", - "@types/escape-html": "^1.0.4", - "dompurify": "^3.2.6", - "escape-html": "^1.0.3" - }, - "engines": { - "node": "^20 || ^22 || ^24" - } - }, - "node_modules/@nextcloud/logger": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@nextcloud/logger/-/logger-3.0.3.tgz", - "integrity": "sha512-TcbVRL4/O5ffI1RXFmQAFD3gwwT15AAdr1770x+RNqVvfBdoGVyhzOwCIyA5Vfc3fA1iJXFa+rE6buJZSoqlcw==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/auth": "^2.5.3" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/notify_push": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.4.0.tgz", - "integrity": "sha512-07UDgz1xLG9XABP8+mwQ2CsNWZu6lKzz0ErUA2HfE1ZfxXKiwVpo60t30y34UExGB9+Ok1nFaYU8fyJHncz9aQ==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@nextcloud/axios": "^2.6.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/event-bus": "^3.3.3" - } - }, - "node_modules/@nextcloud/paths": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.1.0.tgz", - "integrity": "sha512-vtFYA/kthaUDzu6KejTOL1OwnOy7/yynq5zdB/UBpYacAWjUX5Ddh4OMWx3rEavkBJ9/QGhrFryNJLjNfe8OQA==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/router": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.1.0.tgz", - "integrity": "sha512-e4dkIaxRSwdZJlZFpn9x03QgBn/Sa2hN1hp/BA7+AbzykmSAlKuWfdmX8j/8ewrLpQwYmZR23IZO9XwpJXq2Uw==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/typings": "^1.10.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/sharing": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", - "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.0" - } - }, - "node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", - "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/stylelint-config": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/stylelint-config/-/stylelint-config-2.4.0.tgz", - "integrity": "sha512-S/q/offcs9pwnkjSrnfvsONryCOe6e1lfK2sszN6ZtkYyXvaqi8EbQuuhaGlxCstn9oXwbXfAI6O3Y8lGrjdFg==", - "dev": true, - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - }, - "peerDependencies": { - "stylelint": "^15.6.0", - "stylelint-config-recommended-scss": "^13.1.0", - "stylelint-config-recommended-vue": "^1.1.0" - } - }, - "node_modules/@nextcloud/typings": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.10.0.tgz", - "integrity": "sha512-SMC42rDjOH3SspPTLMZRv76ZliHpj2JJkF8pGLP8l1QrVTZxE47Qz5qeKmbj2VL+dRv2e/NgixlAFmzVnxkhqg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@types/jquery": "3.5.16" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vue": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-8.39.0.tgz", - "integrity": "sha512-TJgrFeVr82CN8ng4y+IxMBb7mKlww7Fot22z33+Q0zKgWvi5EoQ0vYescA3Drl8cIRSGktUdFm3xO2gAqvnwoA==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@floating-ui/dom": "^1.7.6", - "@linusborg/vue-simple-portal": "^0.1.5", - "@nextcloud/auth": "^2.6.0", - "@nextcloud/axios": "^2.5.2", - "@nextcloud/browser-storage": "^0.5.0", - "@nextcloud/capabilities": "^1.2.1", - "@nextcloud/event-bus": "^3.3.3", - "@nextcloud/initial-state": "^2.2.0", - "@nextcloud/l10n": "^3.4.1", - "@nextcloud/logger": "^3.0.3", - "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.4.0", - "@nextcloud/vue-select": "^3.26.0", - "@vueuse/components": "^11.0.0", - "@vueuse/core": "^11.0.0", - "blurhash": "^2.0.5", - "clone": "^2.1.2", - "debounce": "^2.2.0", - "dompurify": "^3.4.2", - "emoji-mart-vue-fast": "^15.0.5", - "escape-html": "^1.0.3", - "floating-vue": "^1.0.0-beta.19", - "focus-trap": "^7.8.0", - "linkify-string": "^4.3.2", - "md5": "^2.3.0", - "mdast-util-to-string": "^4.0.0", - "p-queue": "^8.1.1", - "rehype-external-links": "^3.0.0", - "rehype-highlight": "^7.0.2", - "rehype-react": "^7.1.2", - "remark-breaks": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "remark-unlink-protocols": "^1.0.0", - "splitpanes": "^2.4.1", - "string-length": "^5.0.1", - "striptags": "^3.2.0", - "tabbable": "^6.4.0", - "tributejs": "^5.1.3", - "unified": "^11.0.1", - "unist-builder": "^4.0.0", - "unist-util-visit-parents": "^6.0.2", - "vue": "^2.7.16", - "vue-color": "^2.8.1", - "vue-frag": "^1.4.3", - "vue-router": "^3.6.5", - "vue2-datepicker": "^3.11.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vue-select": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue-select/-/vue-select-3.26.0.tgz", - "integrity": "sha512-UvJExrxzx5pP3lv7j6zrv2yj6B1dXph7sh3lLNPnbJPjPoH/yg58mHNFBcPJrRYMbpy2t3hlC6F7s33KCTr9FA==", - "license": "MIT", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "peerDependencies": { - "vue": "2.x" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", - "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", - "license": "GPL-3.0-or-later", - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.2 || ^4.0.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", - "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", - "license": "GPL-3.0-or-later", - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/vue/node_modules/@vueuse/core": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz", - "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "11.3.0", - "@vueuse/shared": "11.3.0", - "vue-demi": ">=0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@nextcloud/vue/node_modules/@vueuse/metadata": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz", - "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@nextcloud/vue/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/@nextcloud/vue/node_modules/p-queue": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", - "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^6.1.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nextcloud/vue/node_modules/p-timeout": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", - "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nextcloud/webpack-vue-config": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@nextcloud/webpack-vue-config/-/webpack-vue-config-5.5.1.tgz", - "integrity": "sha512-2Q3kPnsvkKeENG3rruTjAE1oz/9w0u5QMKO8oh9arL2TIO2aswkDKBtkmu6q7Po3tE+3B2f+ggHq1PNkhtwu6w==", - "dev": true, - "license": "AGPL-3.0-or-later", - "engines": { - "node": "^16.0.0", - "npm": "^7.0.0 || ^8.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.19.6", - "babel-loader": "^9.0.0", - "css-loader": "^6.7.1", - "node-polyfill-webpack-plugin": "2.0.1", - "sass": "^1.55.0", - "sass-loader": "^13.1.0", - "style-loader": "^3.3.1", - "vue": "^2.7.13", - "vue-loader": "^15.10.0", - "vue-template-compiler": "^2.7.13", - "webpack": "^5.74.0", - "webpack-cli": "^5.0.1", - "webpack-dev-server": "^4.11.1" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@npmcli/config": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-6.4.1.tgz", - "integrity": "sha512-uSz+elSGzjCMANWa5IlbGczLYPkNI/LeR+cHrgaTqTrTSh9RHhOFA4daD2eRUz6lMtOW+Fnsb+qv7V2Zz8ML0g==", - "license": "ISC", - "dependencies": { - "@npmcli/map-workspaces": "^3.0.2", - "ci-info": "^4.0.0", - "ini": "^4.1.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", - "read-package-json-fast": "^3.0.2", - "semver": "^7.3.5", - "walk-up-path": "^3.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/config/node_modules/ci-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", - "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@npmcli/config/node_modules/ini": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/config/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/map-workspaces": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.6.tgz", - "integrity": "sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==", - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^2.0.0", - "glob": "^10.2.2", - "minimatch": "^9.0.0", - "read-package-json-fast": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/map-workspaces/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/name-from-folder": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", - "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@nuxt/opencollective": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.3.3.tgz", - "integrity": "sha512-6IKCd+gP0HliixqZT/p8nW3tucD6Sv/u/eR2A9X4rxT/6hXlMzA4GZQzq4d2qnBAwSwGpmKyzkyTjNjrhaA25A==", - "dependencies": { - "chalk": "^4.1.0", - "consola": "^2.15.0", - "node-fetch": "^2.6.7" - }, - "bin": { - "opencollective": "bin/opencollective.js" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/@nuxt/opencollective/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@nuxt/opencollective/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@nuxt/opencollective/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@nuxt/opencollective/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@nuxt/opencollective/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@nuxt/opencollective/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true - }, - "node_modules/@oozcitak/dom": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", - "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oozcitak/infra": "^2.0.2", - "@oozcitak/url": "^3.0.0", - "@oozcitak/util": "^10.0.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@oozcitak/infra": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", - "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oozcitak/util": "^10.0.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@oozcitak/url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", - "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oozcitak/infra": "^2.0.2", - "@oozcitak/util": "^10.0.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@oozcitak/util": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", - "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", - "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", - "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", - "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", - "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", - "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", - "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", - "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", - "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", - "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", - "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@pinia/testing": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.5.tgz", - "integrity": "sha512-AcGzuotkzhRoF00htuxLfIPBBHVE6HjjB3YC5Y3os8vRgKu6ipknK5GBQq9+pduwYQhZ+BcCZDC9TyLAUlUpoQ==", - "dev": true, - "dependencies": { - "vue-demi": "^0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "pinia": ">=2.2.1" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@playwright/test": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", - "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.60.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-22.0.2.tgz", - "integrity": "sha512-//NdP6iIwPbMTcazYsiBMbJW7gfmpHom33u1beiIoHDEM0Q9clvtQB1T0efvMqHeKsGohiHo97BCPCkBXdscwg==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "commondir": "^1.0.1", - "estree-walker": "^2.0.1", - "glob": "^7.1.6", - "is-reference": "^1.2.1", - "magic-string": "^0.25.7", - "resolve": "^1.17.0" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "rollup": "^2.68.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dev": true, - "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@stoplight/json": { - "version": "3.21.7", - "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", - "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", - "dev": true, - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.3", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^13.6.0", - "jsonc-parser": "~2.2.1", - "lodash": "^4.17.21", - "safe-stable-stringify": "^1.1" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json-ref-readers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", - "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", - "dev": true, - "dependencies": { - "node-fetch": "^2.6.0", - "tslib": "^1.14.1" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/@stoplight/json-ref-resolver": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", - "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", - "dev": true, - "dependencies": { - "@stoplight/json": "^3.21.0", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^12.3.0 || ^13.0.0", - "@types/urijs": "^1.19.19", - "dependency-graph": "~0.11.0", - "fast-memoize": "^2.5.2", - "immer": "^9.0.6", - "lodash": "^4.17.21", - "tslib": "^2.6.0", - "urijs": "^1.19.11" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/ordered-object-literal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", - "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/path": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", - "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/spectral-cli": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.15.0.tgz", - "integrity": "sha512-FVeQIuqQQnnLfa8vy+oatTKUve7uU+3SaaAfdjpX/B+uB1NcfkKRJYhKT9wMEehDRaMPL5AKIRYMCFerdEbIpw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "~3.21.0", - "@stoplight/path": "1.3.2", - "@stoplight/spectral-core": "^1.19.5", - "@stoplight/spectral-formatters": "^1.4.1", - "@stoplight/spectral-parsers": "^1.0.4", - "@stoplight/spectral-ref-resolver": "^1.0.4", - "@stoplight/spectral-ruleset-bundler": "^1.6.0", - "@stoplight/spectral-ruleset-migrator": "^1.11.0", - "@stoplight/spectral-rulesets": ">=1", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "^13.6.0", - "chalk": "4.1.2", - "fast-glob": "~3.2.12", - "hpagent": "~1.2.0", - "lodash": "~4.17.21", - "pony-cause": "^1.1.1", - "stacktracey": "^2.1.8", - "tslib": "^2.8.1", - "yargs": "~17.7.2" - }, - "bin": { - "spectral": "dist/index.js" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@stoplight/spectral-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@stoplight/spectral-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@stoplight/spectral-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@stoplight/spectral-cli/node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@stoplight/spectral-cli/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@stoplight/spectral-cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/spectral-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/spectral-core": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.21.0.tgz", - "integrity": "sha512-oj4e/FrDLUhBRocIW+lRMKlJ/q/rDZw61HkLbTFsdMd+f/FTkli2xHNB1YC6n1mrMKjjvy7XlUuFkC7XxtgbWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "~3.21.0", - "@stoplight/path": "1.3.2", - "@stoplight/spectral-parsers": "^1.0.0", - "@stoplight/spectral-ref-resolver": "^1.0.4", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "~13.6.0", - "@types/es-aggregate-error": "^1.0.2", - "@types/json-schema": "^7.0.11", - "ajv": "^8.17.1", - "ajv-errors": "~3.0.0", - "ajv-formats": "~2.1.1", - "es-aggregate-error": "^1.0.7", - "jsonpath-plus": "^10.3.0", - "lodash": "~4.17.23", - "lodash.topath": "^4.5.2", - "minimatch": "3.1.2", - "nimma": "0.2.3", - "pony-cause": "^1.1.1", - "simple-eval": "1.0.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", - "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "dev": true, - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/@stoplight/spectral-formats": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", - "integrity": "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw==", - "dev": true, - "dependencies": { - "@stoplight/json": "^3.17.0", - "@stoplight/spectral-core": "^1.19.2", - "@types/json-schema": "^7.0.7", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-formatters": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-formatters/-/spectral-formatters-1.5.0.tgz", - "integrity": "sha512-lR7s41Z00Mf8TdXBBZQ3oi2uR8wqAtR6NO0KA8Ltk4FSpmAy0i6CKUmJG9hZQjanTnGmwpQkT/WP66p1GY3iXA==", - "dev": true, - "dependencies": { - "@stoplight/path": "^1.3.2", - "@stoplight/spectral-core": "^1.19.4", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "^13.15.0", - "@types/markdown-escape": "^1.1.3", - "chalk": "4.1.2", - "cliui": "7.0.4", - "lodash": "^4.17.21", - "markdown-escape": "^2.0.0", - "node-sarif-builder": "^2.0.3", - "strip-ansi": "6.0", - "text-table": "^0.2.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-formatters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@stoplight/spectral-formatters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@stoplight/spectral-formatters/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/@stoplight/spectral-formatters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@stoplight/spectral-formatters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@stoplight/spectral-formatters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/spectral-formatters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/spectral-functions": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz", - "integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==", - "dev": true, - "dependencies": { - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "^3.17.1", - "@stoplight/spectral-core": "^1.19.4", - "@stoplight/spectral-formats": "^1.8.1", - "@stoplight/spectral-runtime": "^1.1.2", - "ajv": "^8.17.1", - "ajv-draft-04": "~1.0.0", - "ajv-errors": "~3.0.0", - "ajv-formats": "~2.1.1", - "lodash": "~4.17.21", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "dev": true, - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/@stoplight/spectral-parsers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", - "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", - "dev": true, - "dependencies": { - "@stoplight/json": "~3.21.0", - "@stoplight/types": "^14.1.1", - "@stoplight/yaml": "~4.3.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", - "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/spectral-ref-resolver": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", - "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", - "dev": true, - "dependencies": { - "@stoplight/json-ref-readers": "1.2.2", - "@stoplight/json-ref-resolver": "~3.1.6", - "@stoplight/spectral-runtime": "^1.1.2", - "dependency-graph": "0.11.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-ruleset-bundler": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-bundler/-/spectral-ruleset-bundler-1.6.3.tgz", - "integrity": "sha512-AQFRO6OCKg8SZJUupnr3+OzI1LrMieDTEUHsYgmaRpNiDRPvzImE3bzM1KyQg99q58kTQyZ8kpr7sG8Lp94RRA==", - "dev": true, - "dependencies": { - "@rollup/plugin-commonjs": "~22.0.2", - "@stoplight/path": "1.3.2", - "@stoplight/spectral-core": ">=1", - "@stoplight/spectral-formats": "^1.8.1", - "@stoplight/spectral-functions": ">=1", - "@stoplight/spectral-parsers": ">=1", - "@stoplight/spectral-ref-resolver": "^1.0.4", - "@stoplight/spectral-ruleset-migrator": "^1.9.6", - "@stoplight/spectral-rulesets": ">=1", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "^13.6.0", - "@types/node": "*", - "pony-cause": "1.1.1", - "rollup": "~2.79.2", - "tslib": "^2.8.1", - "validate-npm-package-name": "3.0.0" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-ruleset-migrator": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-migrator/-/spectral-ruleset-migrator-1.11.3.tgz", - "integrity": "sha512-+9Y1zFxYmSsneT5FPkgS1IlRQs0VgtdMT77f5xf6vzje9ezyhfs7oXwbZOCSZjEJew8iVZBKQtiOFndcBrdtqg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "~3.21.0", - "@stoplight/ordered-object-literal": "~1.0.4", - "@stoplight/path": "1.3.2", - "@stoplight/spectral-functions": "^1.9.1", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "^13.6.0", - "@stoplight/yaml": "~4.2.3", - "@types/node": "*", - "ajv": "^8.17.1", - "ast-types": "0.14.2", - "astring": "^1.9.0", - "reserved": "0.1.2", - "tslib": "^2.8.1", - "validate-npm-package-name": "3.0.0" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.2.3.tgz", - "integrity": "sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw==", - "dev": true, - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.1", - "@stoplight/types": "^13.0.0", - "@stoplight/yaml-ast-parser": "0.0.48", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=10.8" - } - }, - "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml-ast-parser": { - "version": "0.0.48", - "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz", - "integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==", - "dev": true - }, - "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/@stoplight/spectral-rulesets": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.0.tgz", - "integrity": "sha512-l2EY2jiKKLsvnPfGy+pXC0LeGsbJzcQP5G/AojHgf+cwN//VYxW1Wvv4WKFx/CLmLxc42mJYF2juwWofjWYNIQ==", - "dev": true, - "dependencies": { - "@asyncapi/specs": "^6.8.0", - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "^3.17.0", - "@stoplight/spectral-core": "^1.19.4", - "@stoplight/spectral-formats": "^1.8.1", - "@stoplight/spectral-functions": "^1.9.1", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "^13.6.0", - "@types/json-schema": "^7.0.7", - "ajv": "^8.17.1", - "ajv-formats": "~2.1.1", - "json-schema-traverse": "^1.0.0", - "leven": "3.1.0", - "lodash": "~4.17.21", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/@stoplight/spectral-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.4.tgz", - "integrity": "sha512-YHbhX3dqW0do6DhiPSgSGQzr6yQLlWybhKwWx0cqxjMwxej3TqLv3BXMfIUYFKKUqIwH4Q2mV8rrMM8qD2N0rQ==", - "dev": true, - "dependencies": { - "@stoplight/json": "^3.20.1", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^13.6.0", - "abort-controller": "^3.0.0", - "lodash": "^4.17.21", - "node-fetch": "^2.7.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/types": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", - "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", - "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", - "dev": true, - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.5", - "@stoplight/types": "^14.1.1", - "@stoplight/yaml-ast-parser": "0.0.50", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=10.8" - } - }, - "node_modules/@stoplight/yaml-ast-parser": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", - "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", - "dev": true - }, - "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", - "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@svgdotjs/svg.draggable.js": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", - "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", - "license": "MIT", - "peerDependencies": { - "@svgdotjs/svg.js": "^3.2.4" - } - }, - "node_modules/@svgdotjs/svg.filter.js": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz", - "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==", - "license": "MIT", - "dependencies": { - "@svgdotjs/svg.js": "^3.2.4" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/@svgdotjs/svg.js": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz", - "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Fuzzyma" - } - }, - "node_modules/@svgdotjs/svg.resize.js": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz", - "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", - "license": "MIT", - "engines": { - "node": ">= 14.18" - }, - "peerDependencies": { - "@svgdotjs/svg.js": "^3.2.4", - "@svgdotjs/svg.select.js": "^4.0.1" - } - }, - "node_modules/@svgdotjs/svg.select.js": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz", - "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", - "license": "MIT", - "engines": { - "node": ">= 14.18" - }, - "peerDependencies": { - "@svgdotjs/svg.js": "^3.2.4" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/concat-stream": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-2.0.3.tgz", - "integrity": "sha512-3qe4oQAPNwVNwK4C9c8u+VJqv9kez+2MR4qJpoPFfXtgxxif1QbFusvXzK0/Wra2VX07smostI2VMmJNSpZjuQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/es-aggregate-error": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", - "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/escape-html": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", - "integrity": "sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==", - "license": "MIT" - }, - "node_modules/@types/eslint": { - "version": "8.56.11", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", - "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/is-empty": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/is-empty/-/is-empty-1.2.3.tgz", - "integrity": "sha512-4J1l5d79hoIvsrKh5VUKVRA1aIdsOb10Hu5j3J2VfP/msDnfTdGPmNp2E1Wg+vs97Bktzo+MZePFFXSGoykYJw==", - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", - "dev": true, - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/jquery": { - "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", - "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", - "dependencies": { - "@types/sizzle": "*" - } - }, - "node_modules/@types/jsdom": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "node_modules/@types/markdown-escape": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/markdown-escape/-/markdown-escape-1.1.3.tgz", - "integrity": "sha512-JIc1+s3y5ujKnt/+N+wq6s/QdL2qZ11fP79MijrVXsAAnzSxCbT2j/3prHRouJdZ2yFLN3vkP0HytfnoCczjOw==", - "dev": true - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" - }, - "node_modules/@types/node": { - "version": "20.17.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.23.tgz", - "integrity": "sha512-8PCGZ1ZJbEZuYNTMqywO+Sj4vSKjSjT6Ua+6RFOYlEvIvKQABPtrNkoVSLSKDb4obYcMhspVKmsw8Cm10NFRUg==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/react": { - "version": "18.3.29", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", - "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/sarif": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", - "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/sizzle": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", - "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/sortablejs": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", - "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "node_modules/@types/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", - "dev": true - }, - "node_modules/@types/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", - "dev": true - }, - "node_modules/@types/supports-color": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", - "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", - "license": "MIT" - }, - "node_modules/@types/text-table": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@types/text-table/-/text-table-0.2.5.tgz", - "integrity": "sha512-hcZhlNvMkQG/k1vcZ6yHOl6WAYftQ2MLfTHcYRZ2xYZFD8tGVnE3qFV0lj1smQeDSR7/yY0PyuUalauf33bJeA==", - "license": "MIT" - }, - "node_modules/@types/toastify-js": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.12.4.tgz", - "integrity": "sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==", - "license": "MIT" - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" - }, - "node_modules/@types/urijs": { - "version": "1.19.25", - "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", - "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", - "dev": true - }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@uiw/codemirror-theme-github": { - "version": "4.25.9", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.9.tgz", - "integrity": "sha512-AGpTamNiySKNzq3Jc7QjpwgQRVaHUaBtmOKiUDghYSfEGjsc5uW4NUW70sSU3BnkGv+lCTUnF3175KM24BWZbw==", - "license": "MIT", - "dependencies": { - "@uiw/codemirror-themes": "4.25.9" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - } - }, - "node_modules/@uiw/codemirror-themes": { - "version": "4.25.9", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.9.tgz", - "integrity": "sha512-DAHKb/L9ELwjY4nCf/MP/mIllHOn4GQe7RR4x8AMJuNeh9nGRRoo1uPxrxMmUL/bKqe6kDmDbIZ2AlhlqyIJuw==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/language": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", - "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", - "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", - "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", - "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", - "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", - "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", - "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", - "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", - "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", - "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-loong64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", - "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", - "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", - "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", - "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", - "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", - "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", - "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-openharmony-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", - "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", - "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vue/compiler-sfc": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", - "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", - "dependencies": { - "@babel/parser": "^7.23.5", - "postcss": "^8.4.14", - "source-map": "^0.6.1" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@vue/component-compiler-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", - "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", - "dependencies": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.36", - "postcss-selector-parser": "^6.0.2", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.9.0" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" - }, - "node_modules/@vue/devtools-api": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", - "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" - }, - "node_modules/@vue/eslint-config-typescript": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", - "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "^7.1.1", - "@typescript-eslint/parser": "^7.1.1", - "vue-eslint-parser": "^9.3.1" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "peerDependencies": { - "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.0.0", - "typescript": ">=4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/test-utils": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", - "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", - "dev": true, - "dependencies": { - "js-beautify": "^1.14.9", - "vue-component-type-helpers": "^2.0.0" - } - }, - "node_modules/@vue/vue2-jest": { - "version": "29.2.6", - "resolved": "https://registry.npmjs.org/@vue/vue2-jest/-/vue2-jest-29.2.6.tgz", - "integrity": "sha512-nPu9IvnEkP0AEpo9ETOAk50uqyBa0QMJ9GnPYkC7EukFN1z29QKjyucICayMt8KuHJ9oYBca2TDMH40HowY9mQ==", - "dev": true, - "dependencies": { - "@babel/plugin-transform-modules-commonjs": "^7.2.0", - "@vue/component-compiler-utils": "^3.1.0", - "chalk": "^2.1.0", - "css-tree": "^2.0.1", - "source-map": "0.5.6", - "tsconfig": "^7.0.0" - }, - "engines": { - "node": ">10" - }, - "peerDependencies": { - "@babel/core": "7.x", - "babel-jest": "29.x", - "jest": "29.x", - "typescript": ">= 4.3", - "vue": "^2.x", - "vue-template-compiler": "^2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vueuse/components": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-11.3.0.tgz", - "integrity": "sha512-sqaGtWPgobXvZmv3atcjW8YW0ypecFuB286OEKFXaPrLsA5b2Y+xAvHvq5V7d+VJRKt705gCK3BNBjxu3g1PdQ==", - "license": "MIT", - "dependencies": { - "@vueuse/core": "11.3.0", - "@vueuse/shared": "11.3.0", - "vue-demi": ">=0.14.10" - } - }, - "node_modules/@vueuse/components/node_modules/@vueuse/core": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz", - "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==", - "license": "MIT", - "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "11.3.0", - "@vueuse/shared": "11.3.0", - "vue-demi": ">=0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/components/node_modules/@vueuse/metadata": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz", - "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/core": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", - "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", - "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.11.1", - "@vueuse/shared": "10.11.1", - "vue-demi": ">=0.14.8" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/core/node_modules/@vueuse/shared": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", - "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", - "dependencies": { - "vue-demi": ">=0.14.8" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/metadata": { - "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", - "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/shared": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz", - "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==", - "license": "MIT", - "dependencies": { - "vue-demi": ">=0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@yr/monotone-cubic-spline": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", - "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", - "license": "MIT" - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true - }, - "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", - "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "dev": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats-draft2019": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ajv-formats-draft2019/-/ajv-formats-draft2019-1.6.1.tgz", - "integrity": "sha512-JQPvavpkWDvIsBp2Z33UkYCtXCSpW4HD3tAZ+oL4iEFOk9obQZffx0yANwECt6vzr6ET+7HN5czRyqXbnq/u0Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "punycode": "^2.1.1", - "schemes": "^1.4.0", - "smtp-address-parser": "^1.0.3", - "uri-js": "^4.4.1" - }, - "peerDependencies": { - "ajv": "*" - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "peer": true, - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/apexcharts": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.7.0.tgz", - "integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==", - "license": "MIT", - "dependencies": { - "@svgdotjs/svg.draggable.js": "^3.0.4", - "@svgdotjs/svg.filter.js": "^3.0.8", - "@svgdotjs/svg.js": "^3.2.4", - "@svgdotjs/svg.resize.js": "^2.0.2", - "@svgdotjs/svg.select.js": "^4.0.1", - "@yr/monotone-cubic-spline": "^1.0.3" - } - }, - "node_modules/are-docs-informative": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", - "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, - "dependencies": { - "printable-characters": "^1.0.42" - } - }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/ast-types": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", - "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "dev": true, - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/babel-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/babel-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-jest/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", - "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", - "dev": true, - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz", - "integrity": "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.7", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.1.tgz", - "integrity": "sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.7", - "core-js-compat": "^3.48.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz", - "integrity": "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.7" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base-64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", - "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/blurhash": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", - "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", - "license": "MIT" - }, - "node_modules/bn.js": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", - "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/bonjour-service": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.0.tgz", - "integrity": "sha512-fGQtj1qdR9vIKjFiWPQd52qIqwjaYqhcI40JEiDuvlZ86E7ZBPBwY9fPgHy9r2rYGIjiRfctNPYz6OQU73ww2w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/bootstrap-vue": { - "version": "2.23.1", - "resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.23.1.tgz", - "integrity": "sha512-SEWkG4LzmMuWjQdSYmAQk1G/oOKm37dtNfjB5kxq0YafnL2W6qUAmeDTcIZVbPiQd2OQlIkWOMPBRGySk/zGsg==", - "hasInstallScript": true, - "dependencies": { - "@nuxt/opencollective": "^0.3.2", - "bootstrap": "^4.6.1", - "popper.js": "^1.16.1", - "portal-vue": "^2.1.7", - "vue-functional-data-merge": "^3.1.0" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", - "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bn.js": "^5.2.1", - "randombytes": "^2.1.0", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.6.tgz", - "integrity": "sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "bn.js": "^5.2.3", - "browserify-rsa": "^4.1.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.6.1", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.9", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/browserify-sign/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/builtins": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", - "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/builtins/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz", - "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", - "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.3.0", - "map-obj": "^4.1.0", - "quick-lru": "^5.1.1", - "type-fest": "^1.2.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cancelable-promise": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/cancelable-promise/-/cancelable-promise-4.3.1.tgz", - "integrity": "sha512-A/8PwLk/T7IJDfUdQ68NR24QHa8rIlnN/stiJEBo6dmVUkD4K14LswG0w3VwdeK/o7qOwRUR1k2MhK5Rpy2m7A==", - "license": "MIT" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001778", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", - "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/char-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", - "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/cipher-base": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", - "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", - "dev": true - }, - "node_modules/clamp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", - "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "dev": true - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/comment-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", - "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true, - "peer": true - }, - "node_modules/consolidate": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", - "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", - "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", - "dependencies": { - "bluebird": "^3.1.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/core-js": { - "version": "3.37.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.0.tgz", - "integrity": "sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", - "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/create-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/create-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/create-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "engines": { - "node": "*" - } - }, - "node_modules/crypto-browserify": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", - "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "browserify-cipher": "^1.0.1", - "browserify-sign": "^4.2.3", - "create-ecdh": "^4.0.4", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "diffie-hellman": "^5.0.3", - "hash-base": "~3.0.4", - "inherits": "^2.0.4", - "pbkdf2": "^3.1.2", - "public-encrypt": "^4.0.3", - "randombytes": "^2.1.0", - "randomfill": "^1.0.4" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/css-functions-list": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.2.tgz", - "integrity": "sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==", - "dev": true, - "engines": { - "node": ">=12 || >=16" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/css-loader/node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/css-loader/node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/css-loader/node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/css-loader/node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "dev": true, - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/date-format-parse": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/date-format-parse/-/date-format-parse-0.2.7.tgz", - "integrity": "sha512-/+lyMUKoRogMuTeOVii6lUwjbVlesN9YRYLzZT/g3TEZ3uD9QnpjResujeEqUW+OSNbT7T1+SYdyEkTcRv+KDQ==" - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==" - }, - "node_modules/debounce": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", - "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", - "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true - }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dependency-graph": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/des.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/diff": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", - "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domain-browser": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz", - "integrity": "sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==", - "dev": true, - "license": "Artistic-2.0", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/dompurify": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", - "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/editorconfig": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", - "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@one-ini/wasm": "0.1.1", - "commander": "^10.0.0", - "minimatch": "^9.0.1", - "semver": "^7.5.3" - }, - "bin": { - "editorconfig": "bin/editorconfig" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", - "license": "ISC" - }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-mart-vue-fast": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/emoji-mart-vue-fast/-/emoji-mart-vue-fast-15.0.5.tgz", - "integrity": "sha512-wnxLor8ggpqshoOPwIc33MdOC3A1XFeDLgUwYLPtNPL8VeAtXJAVrnFq1CN5PeCYAFoLo4IufHQZ9CfHD4IZiw==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.18.6", - "core-js": "^3.23.5" - }, - "peerDependencies": { - "vue": ">2.0.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", - "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/envinfo": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", - "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-aggregate-error": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", - "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "globalthis": "^1.0.4", - "has-property-descriptors": "^1.0.2", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "license": "MIT", - "peer": true - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-compat-utils/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0" - } - }, - "node_modules/eslint-import-resolver-alias": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", - "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - }, - "peerDependencies": { - "eslint-plugin-import": ">=1.4.0" - } - }, - "node_modules/eslint-import-resolver-exports": { - "version": "1.0.0-beta.5", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-exports/-/eslint-import-resolver-exports-1.0.0-beta.5.tgz", - "integrity": "sha512-o6t0w7muUpXr7MkUVzD5igQoDfAQvTmcPp8HEAJdNF8eOuAO+yn6I/TTyMxz9ecCwzX7e02vzlkHURoScUuidg==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve.exports": "^2.0.0" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", - "dev": true, - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es-x": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", - "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/ota-meshi", - "https://opencollective.com/eslint" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.11.0", - "eslint-compat-utils": "^0.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-jsdoc": { - "version": "46.10.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.10.1.tgz", - "integrity": "sha512-x8wxIpv00Y50NyweDUpa+58ffgSAI5sqe+zcZh33xphD0AVh+1kqr1ombaTRb7Fhpove1zfUuujlX9DWWBP5ag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@es-joy/jsdoccomment": "~0.41.0", - "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.1", - "debug": "^4.3.4", - "escape-string-regexp": "^4.0.0", - "esquery": "^1.5.0", - "is-builtin-module": "^3.2.1", - "semver": "^7.5.4", - "spdx-expression-parse": "^4.0.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-n": { - "version": "16.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", - "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "builtins": "^5.0.1", - "eslint-plugin-es-x": "^7.5.0", - "get-tsconfig": "^4.7.0", - "globals": "^13.24.0", - "ignore": "^5.2.4", - "is-builtin-module": "^3.2.1", - "is-core-module": "^2.12.1", - "minimatch": "^3.1.2", - "resolve": "^1.22.2", - "semver": "^7.5.3" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-n/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", - "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-vue": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz", - "integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "globals": "^13.24.0", - "natural-compare": "^1.4.0", - "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.0", - "vue-eslint-parser": "^9.4.3", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-vue/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-vue/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-vue/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-webpack-plugin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.2.0.tgz", - "integrity": "sha512-rsfpFQ01AWQbqtjgPRr2usVRxhWDuG0YDYcG8DJOteD3EFnpeuYuOwk0PQiN7PRBTqS6ElNdtPZPggj8If9WnA==", - "dev": true, - "dependencies": { - "@types/eslint": "^8.56.10", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^8.0.0 || ^9.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/eslint/node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0", - "optional": true - }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-memoize": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", - "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", - "dev": true - }, - "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" - }, - "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.1.3" - } - }, - "node_modules/fast-xml-parser": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", - "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fault": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", - "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", - "license": "MIT", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz", - "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "dev": true, - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/floating-vue": { - "version": "1.0.0-beta.19", - "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-1.0.0-beta.19.tgz", - "integrity": "sha512-OcM7z5Ua4XAykqolmvPj3l1s+KqUKj6Xz2t66eqjgaWfNBjtuifmxO5+4rRXakIch/Crt8IH+vKdKcR3jOUaoQ==", - "dependencies": { - "@floating-ui/dom": "^0.1.10", - "vue-resize": "^1.0.0" - }, - "peerDependencies": { - "vue": "^2.6.10" - } - }, - "node_modules/floating-vue/node_modules/@floating-ui/core": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.3.1.tgz", - "integrity": "sha512-ensKY7Ub59u16qsVIFEo2hwTCqZ/r9oZZFh51ivcLGHfUwTn8l1Xzng8RJUe91H/UP8PeqeBronAGx0qmzwk2g==" - }, - "node_modules/floating-vue/node_modules/@floating-ui/dom": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.1.10.tgz", - "integrity": "sha512-4kAVoogvQm2N0XE0G6APQJuCNuErjOfPW8Ux7DFxh8+AfugWflwVJ5LDlHOwrwut7z/30NUvdtHzQ3zSip4EzQ==", - "dependencies": { - "@floating-ui/core": "^0.3.0" - } - }, - "node_modules/focus-trap": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", - "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", - "license": "MIT", - "dependencies": { - "tabbable": "^6.4.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs-monkey": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", - "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", - "dev": true, - "license": "Unlicense", - "peer": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, - "dependencies": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - } - }, - "node_modules/get-source/node_modules/data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true - }, - "node_modules/get-source/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true - }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/globjoin": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", - "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", - "dev": true - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/gridstack": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/gridstack/-/gridstack-10.3.1.tgz", - "integrity": "sha512-Ra82k/88gdeiu3ZP40COS4bI4sGhNQlZAaAQ6szfPfr68zVpsXxiyLKr5zYcTpKX4jjcwyNsNNdcV1tDJc71fA==", - "funding": [ - { - "type": "paypal", - "url": "https://www.paypal.me/alaind831" - }, - { - "type": "venmo", - "url": "https://www.venmo.com/adumesny" - } - ], - "license": "MIT" - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==" - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-to-hyperscript": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-10.0.3.tgz", - "integrity": "sha512-NuBoUStp4fRwmvlfbidlEiRSTk0gSHm+97q4Xn9CJ10HO+Py7nlTuDi6RhM1qLOureukGrCXLG7AAxaGqqyslQ==", - "dependencies": { - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.1", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-to-hyperscript/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-text": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/hot-patcher": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz", - "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==", - "license": "MIT" - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/hpagent": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", - "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT", - "peer": true - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/immutable": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", - "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-meta-resolve": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz", - "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-absolute-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", - "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-bun-module/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-empty": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-empty/-/is-empty-1.2.0.tgz", - "integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==", - "license": "MIT" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-svg": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-6.1.0.tgz", - "integrity": "sha512-i7YPdvYuSCYcaLQrKwt8cvKTlwHcdA6Hp8N9SO3Q5jIzo8x6kH3N47W0BvPP7NdxVBmIHx7X9DK36czYYW7lHg==", - "license": "MIT", - "dependencies": { - "@file-type/xml": "^0.4.3" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-circus/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-circus/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-config/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-config/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-each/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-each/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-matcher-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-message-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-message-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-resolve/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-resolve/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-resolve/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-resolve/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runner/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-runner/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-runner/node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runtime/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-runtime/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-snapshot/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-transform-stub": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz", - "integrity": "sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==", - "dev": true - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-validate/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-validate/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-watcher/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-watcher/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-watcher/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher/node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-beautify": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", - "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", - "dev": true, - "dependencies": { - "config-chain": "^1.1.13", - "editorconfig": "^1.0.4", - "glob": "^10.3.3", - "js-cookie": "^3.0.5", - "nopt": "^7.2.0" - }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-beautify/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdoc-type-pratt-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", - "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "dev": true, - "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "dev": true, - "engines": { - "node": ">= 10.16.0" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", - "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", - "dev": true - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/jsonpath-plus": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", - "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", - "dev": true, - "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/known-css-properties": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", - "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", - "dev": true - }, - "node_modules/launch-editor": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", - "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, - "node_modules/layerr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz", - "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==", - "license": "MIT" - }, - "node_modules/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" - }, - "node_modules/leaflet.markercluster": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", - "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", - "license": "MIT", - "peerDependencies": { - "leaflet": "^1.3.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/libxmljs2": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.37.0.tgz", - "integrity": "sha512-Xb78V8GZouoZFrq8cCwx7+G3WYOcJG0xb3YUbweSyE4z2EIrQCZMr3Ye/dHn4mESs6YxUMeQeUZm5IXg+iLHog==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bindings": "~1.5.0", - "nan": "~2.22.2", - "node-gyp": "^11.2.0", - "prebuild-install": "^7.1.3" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/linkify-string": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/linkify-string/-/linkify-string-4.3.2.tgz", - "integrity": "sha512-JqBuQpSa+CSj2tskIII70SKOjPfjXwDFyjRRNFTrlg76gp2nap36xeRj/cWaXxukqBNrxM+L07XyKRsUtH/DpQ==", - "license": "MIT", - "peerDependencies": { - "linkifyjs": "^4.0.0" - } - }, - "node_modules/linkifyjs": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", - "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", - "license": "MIT" - }, - "node_modules/load-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/load-plugin/-/load-plugin-5.1.0.tgz", - "integrity": "sha512-Lg1CZa1CFj2CbNaxijTL6PCbzd4qGTlZov+iH2p5Xwy/ApcZJh+i6jMN2cYePouTfjJfrNu3nXFdEw8LvbjPFQ==", - "license": "MIT", - "dependencies": { - "@npmcli/config": "^6.0.0", - "import-meta-resolve": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loader-runner": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", - "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" - }, - "node_modules/lodash.topath": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", - "dev": true - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lowlight": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", - "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.0.0", - "highlight.js": "~11.11.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-escape": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-escape/-/markdown-escape-2.0.0.tgz", - "integrity": "sha512-Trz4v0+XWlwy68LJIyw3bLbsJiC8XAbRCKF9DbEtZjyndKOGVx6n+wNB0VfoRmY2LKboQLeniap3xrb6LGSJ8A==", - "dev": true - }, - "node_modules/marked": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.0.tgz", - "integrity": "sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/material-colors": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mathml-tag-names": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", - "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/mdast-comment-marker": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-comment-marker/-/mdast-comment-marker-2.1.2.tgz", - "integrity": "sha512-HED3ezseRVkBzZ0uK4q6RJMdufr/2p3VfVZstE3H1N9K8bwtspztWo6Xd7rEatuGNoCXaBna8oEqMwUn0Ve1bw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-mdx-expression": "^1.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-comment-marker/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-comment-marker/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/mdast-squeeze-paragraphs": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-6.0.0.tgz", - "integrity": "sha512-6NDbJPTg0M0Ye+TlYwX1KJ1LFbp515P2immRJyJQhc9Na9cetHzSoHNYIQcXpANEAP1sm9yd/CTZU2uHqR5A+w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", - "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-heading-style": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-heading-style/-/mdast-util-heading-style-2.0.1.tgz", - "integrity": "sha512-0L5rthU4xKDVbw+UQ7D8Y8xOEsX4JXZvemWoEAsL+WAaeSH+TvVVwFnTb3G/OrjyP4VYQULoNWU+PdZfkmNu4A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-heading-style/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-heading-style/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/mdast-util-mdx-expression": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz", - "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-mdx-expression/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-newline-to-break": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", - "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-find-and-replace": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "dev": true, - "license": "Unlicense", - "peer": true, - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/meow": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", - "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimist": "^1.2.2", - "camelcase-keys": "^7.0.0", - "decamelize": "^5.0.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.2", - "read-pkg-up": "^8.0.0", - "redent": "^4.0.0", - "trim-newlines": "^4.0.2", - "type-fest": "^1.2.2", - "yargs-parser": "^20.2.9" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/merge-source-map/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", - "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", - "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/moo": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", - "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/nan": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", - "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/nearley": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", - "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6" - }, - "bin": { - "nearley-railroad": "bin/nearley-railroad.js", - "nearley-test": "bin/nearley-test.js", - "nearley-unparse": "bin/nearley-unparse.js", - "nearleyc": "bin/nearleyc.js" - }, - "funding": { - "type": "individual", - "url": "https://nearley.js.org/#give-to-nearley" - } - }, - "node_modules/nearley/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT", - "peer": true - }, - "node_modules/nested-property": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", - "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==", - "license": "MIT" - }, - "node_modules/nimma": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", - "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", - "dev": true, - "dependencies": { - "@jsep-plugin/regex": "^1.0.1", - "@jsep-plugin/ternary": "^1.0.2", - "astring": "^1.8.1", - "jsep": "^1.2.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - }, - "optionalDependencies": { - "jsonpath-plus": "^6.0.1 || ^10.1.0", - "lodash.topath": "^4.5.2" - } - }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/node-forge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "peer": true, - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "tinyglobby": "^0.2.12", - "which": "^5.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node_modules/node-polyfill-webpack-plugin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz", - "integrity": "sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "assert": "^2.0.0", - "browserify-zlib": "^0.2.0", - "buffer": "^6.0.3", - "console-browserify": "^1.2.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.12.0", - "domain-browser": "^4.22.0", - "events": "^3.3.0", - "filter-obj": "^2.0.2", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "^1.0.1", - "process": "^0.11.10", - "punycode": "^2.1.1", - "querystring-es3": "^0.2.1", - "readable-stream": "^4.0.0", - "stream-browserify": "^3.0.0", - "stream-http": "^3.2.0", - "string_decoder": "^1.3.0", - "timers-browserify": "^2.0.12", - "tty-browserify": "^0.0.1", - "type-fest": "^2.14.0", - "url": "^0.11.0", - "util": "^0.12.4", - "vm-browserify": "^1.1.2" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "webpack": ">=5" - } - }, - "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "peer": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "license": "MIT" - }, - "node_modules/node-sarif-builder": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-2.0.3.tgz", - "integrity": "sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==", - "dev": true, - "dependencies": { - "@types/sarif": "^2.1.4", - "fs-extra": "^10.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", - "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", - "dev": true - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", - "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" - }, - "node_modules/packageurl-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-2.0.1.tgz", - "integrity": "sha512-N5ixXjzTy4QDQH0Q9YFjqIWd6zH6936Djpl2m9QNFmDv5Fum8q8BjkpAcHNMzOFE0IwQrFhJWex3AN6kS0OSwg==", - "dev": true, - "license": "MIT" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)", - "peer": true - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", - "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "pbkdf2": "^3.1.5", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-posix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", - "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", - "license": "ISC" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", - "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "ripemd160": "^2.0.3", - "safe-buffer": "^5.2.1", - "sha.js": "^2.4.12", - "to-buffer": "^1.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pinia": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.2.tgz", - "integrity": "sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==", - "dependencies": { - "@vue/devtools-api": "^6.6.3", - "vue-demi": "^0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "@vue/composition-api": "^1.4.0", - "typescript": ">=4.4.4", - "vue": "^2.6.14 || ^3.3.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "dev": true, - "dependencies": { - "find-up": "^6.3.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/pkg-dir/node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/playwright": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", - "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.60.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", - "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/pony-cause": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", - "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/portal-vue": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-2.1.7.tgz", - "integrity": "sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g==", - "peerDependencies": { - "vue": "^2.5.18" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-html": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.8.1.tgz", - "integrity": "sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "htmlparser2": "^8.0.0", - "js-tokens": "^9.0.0", - "postcss": "^8.5.0", - "postcss-safe-parser": "^6.0.0" - }, - "engines": { - "node": "^12 || >=14" - } - }, - "node_modules/postcss-html/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-resolve-nested-selector": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", - "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", - "dev": true - }, - "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.3.3" - } - }, - "node_modules/postcss-scss": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-scss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.4.29" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "optional": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true - }, - "node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ] - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", - "dev": true, - "license": "CC0-1.0", - "optional": true - }, - "node_modules/randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", - "license": "MIT", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-pkg": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", - "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", - "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^5.0.0", - "read-pkg": "^6.0.0", - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/rehype-external-links": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", - "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", - "dependencies": { - "@types/hast": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-is-element": "^3.0.0", - "is-absolute-url": "^4.0.0", - "space-separated-tokens": "^2.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-highlight": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", - "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-text": "^4.0.0", - "lowlight": "^3.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-7.2.0.tgz", - "integrity": "sha512-MHYyCHka+3TtzBMKtcuvVOBAbI1HrfoYA+XH9m7/rlrQQATCPwtJnPdkxKKcIGF8vc9mxqQja9r9f+FHItQeWg==", - "dependencies": { - "@mapbox/hast-util-table-cell-style": "^0.2.0", - "@types/hast": "^2.0.0", - "hast-to-hyperscript": "^10.0.0", - "hast-util-whitespace": "^2.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=17" - } - }, - "node_modules/rehype-react/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/rehype-react/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" - }, - "node_modules/rehype-react/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/rehype-react/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rehype-react/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.3.tgz", - "integrity": "sha512-bfmJW1dmR2LvaMJuAnE88pZP9DktIFYXazkTfOIKZzi3Knk9lT0roItIA24ydOucI3bV/g/tXBA6hzqq3FV9Ew==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "remark-parse": "^10.0.0", - "remark-stringify": "^10.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-breaks": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", - "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-newline-to-break": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-cli": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-cli/-/remark-cli-11.0.0.tgz", - "integrity": "sha512-8JEWwArXquRq1/In4Ftz7gSG9Scwb1ijT2/dEuBETW9omqhmMRxcfjZ3iKqrak3BnCJeZSXCdWEmPhFKC8+RUQ==", - "license": "MIT", - "dependencies": { - "remark": "^14.0.0", - "unified-args": "^10.0.0" - }, - "bin": { - "remark": "cli.js" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/remark-lint/-/remark-lint-9.1.2.tgz", - "integrity": "sha512-m9e/aPlh7tsvfJfj8tPxrQzD6oEdb9Foko+Ya/6OwUP9EoGMfehv1Qtv26W1DoH58Wn8rT8CD+KuprTWscMmIA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "remark-message-control": "^7.0.0", - "unified": "^10.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-blockquote-indentation/-/remark-lint-blockquote-indentation-3.1.2.tgz", - "integrity": "sha512-5DOrFsZd5dXqA4p/VZvWSrqIWNFbBXjX7IV/FkVkxlNhNF/0FMf/4v8x1I2W3mzaZ7yDsWS/egpZnmligq1ckQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "pluralize": "^8.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-blockquote-indentation/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-checkbox-character-style/-/remark-lint-checkbox-character-style-4.1.2.tgz", - "integrity": "sha512-5ITz+1cCuJ3Jv/Q7rKgDEucCOnIgjWDnSHPJA1tb4TI/D316h+ALbDhZIpP8gyfAm6sBAh3Pwz9XZJN2uJB5UQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-checkbox-character-style/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-code-block-style/-/remark-lint-code-block-style-3.1.2.tgz", - "integrity": "sha512-3wsWmzzdyEsB9sOzBOf46TSkwwVKXN2JpTEQb6feN0Tl6Vg75F7T9MHqMz7aqk/56bOXSxUzdpXDscGBhziLRA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-code-block-style/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-code-block-style/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-emphasis-marker/-/remark-lint-emphasis-marker-3.1.2.tgz", - "integrity": "sha512-hPZ8vxZrIfxmLA5B66bA8y3PdHjcCQuaLsySIqi5PM2DkpN6a7zAP3v1znyRSaYJ1ANVWcu00/0bNzuUjflGCA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-emphasis-marker/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-emphasis-marker/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-fenced-code-marker/-/remark-lint-fenced-code-marker-3.1.2.tgz", - "integrity": "sha512-6XNqjOuhT+0c7Q/22aCsMz61ne9g8HRpYF79EXQPdbzYa+PcfPXMiQKStONY3PfC8OE2/3WXI2zcs8w9x+8+VQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-fenced-code-marker/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-final-newline": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-final-newline/-/remark-lint-final-newline-2.1.2.tgz", - "integrity": "sha512-K0FdPGPyEB94PwNgopwVJFE8oRWi7IhY2ycXFVAMReI51el7EHB8F1gX14tB6p6zyGy6mUh69bCVU9mMTNeOUg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-final-newline/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-final-newline/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-final-newline/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-final-newline/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-final-newline/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-final-newline/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-final-newline/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-final-newline/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-hard-break-spaces/-/remark-lint-hard-break-spaces-3.1.2.tgz", - "integrity": "sha512-HaW0xsl3TI7VFAqGWWcZtPqyz0NWu19KKjSO7OGFTUJU4S9YiRnhIxmSFM0ZLSsVAynE+dhzVKa8U7dOpWDcOg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-hard-break-spaces/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-heading-style": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-heading-style/-/remark-lint-heading-style-3.1.2.tgz", - "integrity": "sha512-0RkcRPV/H2bPFgeInzBkK1cWUwtFTm83I+Db/Z5tDY02GzKOosHLvxtJyj/1391/opAH1LYbHtHWffir99IUgw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-heading-style": "^2.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-heading-style/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-heading-style/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-heading-style/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-heading-style/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-heading-style/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-heading-style/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-heading-style/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-heading-style/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-heading-style/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-heading-style/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-heading-style/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-link-title-style/-/remark-lint-link-title-style-3.1.2.tgz", - "integrity": "sha512-if4MahYJVvQUWlrXDF8GSv4b9VtLSgMSDHeikQp1/hGYlihLl9uGw3nlL5Lf9DqTN0qaT6RPbXOjuuzHlk38sg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0", - "vfile-location": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-link-title-style/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-link-title-style/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-bullet-indent": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-list-item-bullet-indent/-/remark-lint-list-item-bullet-indent-4.1.2.tgz", - "integrity": "sha512-WgU5nooqIEm6f35opcbHKBzWrdFJA3XcyTfB3nv/v0KX43/h6qFGmmMJ5kEiaFExuQp3dZSdatWuY0YZ9YRbUg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "pluralize": "^8.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-bullet-indent/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-list-item-content-indent/-/remark-lint-list-item-content-indent-3.1.2.tgz", - "integrity": "sha512-TB0pmrWiRaQW80Y/PILFQTnHDghRxXNzMwyawlP+DBF9gNom3pEBmb4ZlGQlN0aa3r8VWeIKdv1ylHrfXE0vqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "pluralize": "^8.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-content-indent/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-list-item-indent/-/remark-lint-list-item-indent-3.1.2.tgz", - "integrity": "sha512-tkrra1pxZVE4OVJGfN435u/v0ljruXU+dHzWiKDYeifquD4aWhJxvSApu7+FbE098D/4usVXgMxwFkNhrpZcSQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "pluralize": "^8.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-list-item-indent/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-list-item-indent/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-no-blockquote-without-marker/-/remark-lint-no-blockquote-without-marker-5.1.2.tgz", - "integrity": "sha512-QPbqsrt7EfpSWqTkZJ9tepabPIhBDlNqZkuxxMQYD0OQ2N+tHDUq3zE1JxI5ts1V9o/mWApgySocqGd3jlcKmQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0", - "vfile-location": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-blockquote-without-marker/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-no-duplicate-definitions/-/remark-lint-no-duplicate-definitions-3.1.2.tgz", - "integrity": "sha512-vi0nXA7p+pjQOorZOkr9E+QDhG74JAdbzqglWPrWWNI3z2rUYWYHTNSyWJbwEXaIIcev1ZAw8SCAOis5MNm+pA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-stringify-position": "^3.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-duplicate-definitions/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-no-heading-content-indent/-/remark-lint-no-heading-content-indent-4.1.2.tgz", - "integrity": "sha512-TTxFsm1f4ZHFxZQCuz7j0QK4RvP6oArTiwazKLr16yaZe1608ypogMek4A30j2xX8WuO9+2uBzLXCY5OBo5x5Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-heading-style": "^2.0.0", - "pluralize": "^8.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-heading-content-indent/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-no-inline-padding/-/remark-lint-no-inline-padding-4.1.2.tgz", - "integrity": "sha512-dGyhWsiqCZS3Slob0EVBUfsFBbdpMIBCvb56LlCgaHbnLsnNYx8PpF/wA5CgsN8BXIbXfRpyPB5cIJwIq5taYg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-inline-padding/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-inline-padding/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-no-literal-urls/-/remark-lint-no-literal-urls-3.1.2.tgz", - "integrity": "sha512-4tV9JGLKxAMFSuWDMOqLozkFJ3HyRvhzgrPrxASoziaml23m7UXAozk5dkIrFny1cN2oG988Z8tORxX2FL1Ilw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-literal-urls/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-literal-urls/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-no-shortcut-reference-image/-/remark-lint-no-shortcut-reference-image-3.1.2.tgz", - "integrity": "sha512-NX4XJFPyDeJJ77pmETxRj4oM/zayf7Lmn/O87HgExBkQIPz2NYbDeKD8QEyliLaV/oKA2rQufpzuFw55xa1Tww==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-image/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-no-shortcut-reference-link/-/remark-lint-no-shortcut-reference-link-3.1.2.tgz", - "integrity": "sha512-/9iPN7FLKaaIzw4tLWKu7Rx0wAP7E2EuzIeentQlkY0rO/mMHipmT3IlgiebsAInKagzTY6TNFoG1rq2VnaCcA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-shortcut-reference-link/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/remark-lint-no-undefined-references/-/remark-lint-no-undefined-references-4.2.1.tgz", - "integrity": "sha512-HdNg5b2KiuNplcuVvRtsrUiROw557kAG1CiZYB7jQrrVWFgd86lKTa3bDiywe+87dGrGmHd3qQ28eZYTuHz2Nw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0", - "vfile-location": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-undefined-references/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/remark-lint-no-undefined-references/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-undefined-references/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-unused-definitions": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-no-unused-definitions/-/remark-lint-no-unused-definitions-3.1.2.tgz", - "integrity": "sha512-bOcaJAnjKxT3kASFquUA3fO9xem9wZhVqt8TbqjA84+G4n40qjaLXDs/4vq73aMsSde73K0f3j1u0pMe7et8yQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-no-unused-definitions/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-ordered-list-marker-style/-/remark-lint-ordered-list-marker-style-3.1.2.tgz", - "integrity": "sha512-62iVE/YQsA0Azaqt8yAJWPplWLS47kDLjXeC2PlRIAzCqbNt9qH3HId8vZ15QTSrp8rHmJwrCMdcqV6AZUi7gQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-ordered-list-marker-style/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-rule-style/-/remark-lint-rule-style-3.1.2.tgz", - "integrity": "sha512-0CsX2XcX9pIhAP5N7Y8mhYXp3/Ld+NvxXY1p0LHAq0NZu17UsZLuegvx/s25uFbQs08DcmSqyKnepU9qGGqmTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-rule-style/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-rule-style/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-rule-style/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-rule-style/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-rule-style/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/remark-lint-strong-marker/-/remark-lint-strong-marker-3.1.2.tgz", - "integrity": "sha512-U/g4wngmiI0Q6WBRQG6pZxnDS33Wt/0QYA3+KNFBDykoi1vXsDEorIqy3dEag9z6XHwcMvFDsff6VRUhaOJWQg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-strong-marker/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-strong-marker/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/remark-lint-table-cell-padding/-/remark-lint-table-cell-padding-4.1.3.tgz", - "integrity": "sha512-N9xtnS6MG/H3srAMjqqaF26A7socr87pIgt64dr5rxoSbDRWRPChGQ8y7wKyV8VeyRNF37e3E5KB3bQVqjSYaQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unified": "^10.0.0", - "unified-lint-rule": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint-table-cell-padding/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint-table-cell-padding/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-lint/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-lint/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-lint/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-lint/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-lint/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-message-control": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/remark-message-control/-/remark-message-control-7.1.1.tgz", - "integrity": "sha512-xKRWl1NTBOKed0oEtCd8BUfH5m4s8WXxFFSoo7uUwx6GW/qdCy4zov5LfPyw7emantDmhfWn5PdIZgcbVcWMDQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-comment-marker": "^2.0.0", - "unified": "^10.0.0", - "unified-message-control": "^4.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-message-control/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-message-control/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-message-control/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-message-control/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-message-control/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-message-control/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-message-control/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-message-control/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-consistent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/remark-preset-lint-consistent/-/remark-preset-lint-consistent-5.1.2.tgz", - "integrity": "sha512-RQrWBFmyIkKfXtp9P1Fui7UbGSfXth9nuvRJUVnO0vfevBJe02iyMZWPokXSwkDOI/cM539wj0i3vrQupz+v5A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "remark-lint": "^9.0.0", - "remark-lint-blockquote-indentation": "^3.0.0", - "remark-lint-checkbox-character-style": "^4.0.0", - "remark-lint-code-block-style": "^3.0.0", - "remark-lint-emphasis-marker": "^3.0.0", - "remark-lint-fenced-code-marker": "^3.0.0", - "remark-lint-heading-style": "^3.0.0", - "remark-lint-link-title-style": "^3.0.0", - "remark-lint-list-item-content-indent": "^3.0.0", - "remark-lint-ordered-list-marker-style": "^3.0.0", - "remark-lint-rule-style": "^3.0.0", - "remark-lint-strong-marker": "^3.0.0", - "remark-lint-table-cell-padding": "^4.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-consistent/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-preset-lint-consistent/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-preset-lint-consistent/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-preset-lint-consistent/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-preset-lint-consistent/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-consistent/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-consistent/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-consistent/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-recommended": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/remark-preset-lint-recommended/-/remark-preset-lint-recommended-6.1.3.tgz", - "integrity": "sha512-DGjbeP2TsFmQeJflUiIvJWAOs1PxJt7SG3WQyMxOppkRr/up+mxWVkuv+6AUuaR0EsuaaFGz7WmZM5TrSSFWJw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "remark-lint": "^9.0.0", - "remark-lint-final-newline": "^2.0.0", - "remark-lint-hard-break-spaces": "^3.0.0", - "remark-lint-list-item-bullet-indent": "^4.0.0", - "remark-lint-list-item-indent": "^3.0.0", - "remark-lint-no-blockquote-without-marker": "^5.0.0", - "remark-lint-no-duplicate-definitions": "^3.0.0", - "remark-lint-no-heading-content-indent": "^4.0.0", - "remark-lint-no-inline-padding": "^4.0.0", - "remark-lint-no-literal-urls": "^3.0.0", - "remark-lint-no-shortcut-reference-image": "^3.0.0", - "remark-lint-no-shortcut-reference-link": "^3.0.0", - "remark-lint-no-undefined-references": "^4.0.0", - "remark-lint-no-unused-definitions": "^3.0.0", - "remark-lint-ordered-list-marker-style": "^3.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-recommended/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-preset-lint-recommended/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark-preset-lint-recommended/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark-preset-lint-recommended/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark-preset-lint-recommended/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-recommended/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-recommended/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-preset-lint-recommended/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", - "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-unlink-protocols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/remark-unlink-protocols/-/remark-unlink-protocols-1.0.0.tgz", - "integrity": "sha512-5j/F28jhFmxeyz8nuJYYIWdR4nNpKWZ8A+tVwnK/0pq7Rjue33CINEYSckSq2PZvedhKUwbn08qyiuGoPLBung==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-squeeze-paragraphs": "^6.0.0", - "unist-util-visit": "^5.0.0" - } - }, - "node_modules/remark/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/remark/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/remark/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remark/node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/remark/node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/remark/node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/remark/node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/remark/node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/remark/node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/remark/node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/remark/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/remark/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/remark/node_modules/remark-parse": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", - "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/remark-stringify": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.3.tgz", - "integrity": "sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.5" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, - "node_modules/reserved": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/reserved/-/reserved-0.1.2.tgz", - "integrity": "sha512-/qO54MWj5L8WCBP9/UNe2iefJc+L9yETbH32xO/ft/EYPOTCR5k+azvDUgdCOKwZH8hXwPd0b8XBL78Nn2U69g==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ripemd160": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", - "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "hash-base": "^3.1.2", - "inherits": "^2.0.4" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ripemd160/node_modules/hash-base": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", - "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ripemd160/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/ripemd160/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/ripemd160/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/ripemd160/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", - "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", - "dev": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true - }, - "node_modules/sass": { - "version": "1.100.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", - "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "chokidar": "^5.0.0", - "immutable": "^5.1.5", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=20.19.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/sass-loader": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", - "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/sass/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sax": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", - "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/schemes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/schemes/-/schemes-1.4.0.tgz", - "integrity": "sha512-ImFy9FbCsQlVgnE3TCWmLPCFnVzx0lHL/l+umHplDqAKd0dzFpnS6lFZIpagBlYhKwzVmlV36ec0Y1XTu8JBAQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "extend": "^3.0.0" - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/serve-index": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", - "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "~1.3.8", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.8.0", - "mime-types": "~2.1.35", - "parseurl": "~1.3.3" - }, - "engines": { - "node": ">= 0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "dev": true, - "license": "(MIT AND BSD-3-Clause)", - "peer": true, - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" - }, - "bin": { - "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/simple-eval": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", - "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", - "dev": true, - "dependencies": { - "jsep": "^1.3.6" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/smtp-address-parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.1.0.tgz", - "integrity": "sha512-Gz11jbNU0plrReU9Sj7fmshSBxxJ9ShdD2q4ktHIHo/rpTH6lFyQoYHYKINPJtPe8aHFnsbtW46Ls0tCCBsIZg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "nearley": "^2.20.1" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", - "dev": true - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/spdy-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/splitpanes": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-2.4.1.tgz", - "integrity": "sha512-kpEo1WuMXuc6QfdQdO2V/fl/trONlkUKp+pputsLTiW9RMtwEvjb4/aYGm2m3+KAzjmb+zLwr4A4SYZu74+pgQ==" - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, - "dependencies": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-browserify/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - } - }, - "node_modules/stream-http/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", - "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", - "dependencies": { - "char-regex": "^2.0.0", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", - "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/striptags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz", - "integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==" - }, - "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" - }, - "node_modules/style-search": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", - "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", - "dev": true - }, - "node_modules/style-to-object": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", - "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", - "dependencies": { - "inline-style-parser": "0.1.1" - } - }, - "node_modules/stylelint": { - "version": "15.11.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.11.0.tgz", - "integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-parser-algorithms": "^2.3.1", - "@csstools/css-tokenizer": "^2.2.0", - "@csstools/media-query-list-parser": "^2.1.4", - "@csstools/selector-specificity": "^3.0.0", - "balanced-match": "^2.0.0", - "colord": "^2.9.3", - "cosmiconfig": "^8.2.0", - "css-functions-list": "^3.2.1", - "css-tree": "^2.3.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.1", - "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^7.0.0", - "global-modules": "^2.0.0", - "globby": "^11.1.0", - "globjoin": "^0.1.4", - "html-tags": "^3.3.1", - "ignore": "^5.2.4", - "import-lazy": "^4.0.0", - "imurmurhash": "^0.1.4", - "is-plain-object": "^5.0.0", - "known-css-properties": "^0.29.0", - "mathml-tag-names": "^2.1.3", - "meow": "^10.1.5", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.28", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.0.13", - "postcss-value-parser": "^4.2.0", - "resolve-from": "^5.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "style-search": "^0.1.0", - "supports-hyperlinks": "^3.0.0", - "svg-tags": "^1.0.0", - "table": "^6.8.1", - "write-file-atomic": "^5.0.1" - }, - "bin": { - "stylelint": "bin/stylelint.mjs" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/stylelint" - } - }, - "node_modules/stylelint-config-html": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stylelint-config-html/-/stylelint-config-html-1.1.0.tgz", - "integrity": "sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12 || >=14" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "postcss-html": "^1.0.0", - "stylelint": ">=14.0.0" - } - }, - "node_modules/stylelint-config-recommended": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz", - "integrity": "sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "stylelint": "^15.10.0" - } - }, - "node_modules/stylelint-config-recommended-scss": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-13.1.0.tgz", - "integrity": "sha512-8L5nDfd+YH6AOoBGKmhH8pLWF1dpfY816JtGMePcBqqSsLU+Ysawx44fQSlMOJ2xTfI9yTGpup5JU77c17w1Ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-scss": "^4.0.9", - "stylelint-config-recommended": "^13.0.0", - "stylelint-scss": "^5.3.0" - }, - "peerDependencies": { - "postcss": "^8.3.3", - "stylelint": "^15.10.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - } - } - }, - "node_modules/stylelint-config-recommended-vue": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended-vue/-/stylelint-config-recommended-vue-1.6.1.tgz", - "integrity": "sha512-lLW7hTIMBiTfjenGuDq2kyHA6fBWd/+Df7MO4/AWOxiFeXP9clbpKgg27kHfwA3H7UNMGC7aeP3mNlZB5LMmEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5", - "stylelint-config-html": ">=1.0.0", - "stylelint-config-recommended": ">=6.0.0" - }, - "engines": { - "node": "^12 || >=14" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "postcss-html": "^1.0.0", - "stylelint": ">=14.0.0" - } - }, - "node_modules/stylelint-config-recommended-vue/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stylelint-scss": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-5.3.2.tgz", - "integrity": "sha512-4LzLaayFhFyneJwLo0IUa8knuIvj+zF0vBFueQs4e3tEaAMIQX8q5th8ziKkgOavr6y/y9yoBe+RXN/edwLzsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "known-css-properties": "^0.29.0", - "postcss-media-query-parser": "^0.2.3", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-selector-parser": "^6.0.13", - "postcss-value-parser": "^4.2.0" - }, - "peerDependencies": { - "stylelint": "^14.5.1 || ^15.0.0" - } - }, - "node_modules/stylelint-webpack-plugin": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/stylelint-webpack-plugin/-/stylelint-webpack-plugin-4.1.1.tgz", - "integrity": "sha512-yOyd2AfrxfawxKDememazGVJX2vMq9o11E6HvBu4+SKvgK3ZulkjpYdI1muBTxItwoxH2UmfIZzQM+/M5V3kTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "globby": "^11.1.0", - "jest-worker": "^29.5.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "stylelint": "^13.0.0 || ^14.0.0 || ^15.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/stylelint/node_modules/balanced-match": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", - "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", - "dev": true - }, - "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-7.0.2.tgz", - "integrity": "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.2.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/stylelint/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/stylelint/node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-hyperlinks": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", - "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=14.18" - }, - "funding": { - "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" - } - }, - "node_modules/supports-hyperlinks/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", - "dev": true - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", - "license": "MIT" - }, - "node_modules/table": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", - "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "optional": true, - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/terser": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", - "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@minify-html/node": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "@swc/css": { - "optional": true - }, - "@swc/html": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "cssnano": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "html-minifier-terser": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "postcss": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "setimmediate": "^1.0.4" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-buffer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", - "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/to-vfile": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-7.2.4.tgz", - "integrity": "sha512-2eQ+rJ2qGbyw3senPI0qjuM7aut8IYXK6AEoOWb+fJx/mQYzviTckm1wDjq91QYHAPBTYzmdJXxMFA6Mk14mdw==", - "license": "MIT", - "dependencies": { - "is-buffer": "^2.0.0", - "vfile": "^5.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/to-vfile/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/to-vfile/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-vfile/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/to-vfile/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/to-vfile/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/toastify-js": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", - "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/tributejs": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/tributejs/-/tributejs-5.1.3.tgz", - "integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==" - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trim-newlines": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", - "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", - "dev": true, - "dependencies": { - "bs-logger": "^0.2.6", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.6.3", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/ts-loader": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", - "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ts-loader/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/ts-loader/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/ts-loader/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", - "dev": true, - "dependencies": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/tsconfig/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/tsconfig/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, - "node_modules/tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-event-target": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/typescript-event-target/-/typescript-event-target-1.1.2.tgz", - "integrity": "sha512-TvkrTUpv7gCPlcnSoEwUVUBwsdheKm+HF5u2tPAKubkIGMfovdSizCTaZRY/NhR8+Ijy8iZZUapbVQAsNrkFrw==", - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-args": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/unified-args/-/unified-args-10.0.0.tgz", - "integrity": "sha512-PqsqxwkXpGSLiMkbjNnKU33Ffm6gso6rAvz1TlBGzMBx3gpx7ewIhViBX8HEWmy0v7pebA5PM6RkRWWaYmtfYw==", - "license": "MIT", - "dependencies": { - "@types/text-table": "^0.2.0", - "camelcase": "^7.0.0", - "chalk": "^5.0.0", - "chokidar": "^3.0.0", - "fault": "^2.0.0", - "json5": "^2.0.0", - "minimist": "^1.0.0", - "text-table": "^0.2.0", - "unified-engine": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-args/node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified-args/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/unified-engine": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/unified-engine/-/unified-engine-10.1.0.tgz", - "integrity": "sha512-5+JDIs4hqKfHnJcVCxTid1yBoI/++FfF/1PFdSMpaftZZZY+qg2JFruRbf7PaIwa9KgLotXQV3gSjtY0IdcFGQ==", - "license": "MIT", - "dependencies": { - "@types/concat-stream": "^2.0.0", - "@types/debug": "^4.0.0", - "@types/is-empty": "^1.0.0", - "@types/node": "^18.0.0", - "@types/unist": "^2.0.0", - "concat-stream": "^2.0.0", - "debug": "^4.0.0", - "fault": "^2.0.0", - "glob": "^8.0.0", - "ignore": "^5.0.0", - "is-buffer": "^2.0.0", - "is-empty": "^1.0.0", - "is-plain-obj": "^4.0.0", - "load-plugin": "^5.0.0", - "parse-json": "^6.0.0", - "to-vfile": "^7.0.0", - "trough": "^2.0.0", - "unist-util-inspect": "^7.0.0", - "vfile-message": "^3.0.0", - "vfile-reporter": "^7.0.0", - "vfile-statistics": "^2.0.0", - "yaml": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-engine/node_modules/@types/node": { - "version": "18.19.79", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.79.tgz", - "integrity": "sha512-90K8Oayimbctc5zTPHPfZloc/lGVs7f3phUAAMcTgEPtg8kKquGZDERC8K4vkBYkQQh48msiYUslYtxTWvqcAg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/unified-engine/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/unified-engine/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/unified-engine/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unified-engine/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified-engine/node_modules/lines-and-columns": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", - "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/unified-engine/node_modules/parse-json": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-6.0.2.tgz", - "integrity": "sha512-SA5aMiaIjXkAiBrW/yPgLgQAQg42f7K3ACO+2l/zOvtQBwX58DMUsFJXelW2fx3yMBmWOVkR6j1MGsdSbCA4UA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "error-ex": "^1.3.2", - "json-parse-even-better-errors": "^2.3.1", - "lines-and-columns": "^2.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified-engine/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/unified-engine/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-engine/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-lint-rule": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.2.tgz", - "integrity": "sha512-JWudPtRN7TLFHVLEVZ+Rm8FUb6kCAtHxEXFgBGDxRSdNMnGyTU5zyYvduHSF/liExlFB3vdFvsAHnNVE/UjAwA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "trough": "^2.0.0", - "unified": "^10.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-lint-rule/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/unified-lint-rule/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unified-lint-rule/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified-lint-rule/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-lint-rule/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-lint-rule/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-lint-rule/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-message-control": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unified-message-control/-/unified-message-control-4.0.0.tgz", - "integrity": "sha512-1b92N+VkPHftOsvXNOtkJm4wHlr+UDmTBF2dUzepn40oy9NxanJ9xS1RwUBTjXJwqr2K0kMbEyv1Krdsho7+Iw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit": "^3.0.0", - "vfile": "^5.0.0", - "vfile-location": "^4.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-message-control/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/unified-message-control/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unified-message-control/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-message-control/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-message-control/node_modules/unist-util-visit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz", - "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-message-control/node_modules/unist-util-visit-parents": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz", - "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-message-control/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified-message-control/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unist-builder": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", - "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-generated": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", - "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-inspect": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/unist-util-inspect/-/unist-util-inspect-7.0.2.tgz", - "integrity": "sha512-Op0XnmHUl6C2zo/yJCwhXQSm/SmW22eDZdWP2qdf4WpGrgO1ZxFodq+5zFyeRGasFjJotAnLgfuD1jkcKqiH1Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-inspect/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unrs-resolver": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", - "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.4" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.12.2", - "@unrs/resolver-binding-android-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-x64": "1.12.2", - "@unrs/resolver-binding-freebsd-x64": "1.12.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", - "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-musl": "1.12.2", - "@unrs/resolver-binding-openharmony-arm64": "1.12.2", - "@unrs/resolver-binding-wasm32-wasi": "1.12.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/urijs": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", - "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", - "dev": true - }, - "node_modules/url": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.12.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/url-join": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", - "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/uvu/node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/validate-npm-package-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", - "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", - "dev": true, - "dependencies": { - "builtins": "^1.0.3" - } - }, - "node_modules/validate-npm-package-name/node_modules/builtins": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", - "integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==", - "dev": true - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vfile": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", - "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", - "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/vfile-location/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/vfile-location/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-7.0.5.tgz", - "integrity": "sha512-NdWWXkv6gcd7AZMvDomlQbK3MqFWL1RlGzMn++/O2TI+68+nqxCPTvLugdOtfSzXmjh+xUyhp07HhlrbJjT+mw==", - "license": "MIT", - "dependencies": { - "@types/supports-color": "^8.0.0", - "string-width": "^5.0.0", - "supports-color": "^9.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile": "^5.0.0", - "vfile-message": "^3.0.0", - "vfile-sort": "^3.0.0", - "vfile-statistics": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/vfile-reporter/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/vfile-reporter/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/vfile-reporter/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/vfile-reporter/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vfile-reporter/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/vfile-reporter/node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/vfile-reporter/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-reporter/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-sort": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-3.0.1.tgz", - "integrity": "sha512-1os1733XY6y0D5x0ugqSeaVJm9lYgj0j5qdcZQFyxlZOSy1jYarL77lLyb5gK4Wqr1d5OxmuyflSO3zKyFnTFw==", - "license": "MIT", - "dependencies": { - "vfile": "^5.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-sort/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/vfile-sort/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/vfile-sort/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-sort/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-sort/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-statistics": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-2.0.1.tgz", - "integrity": "sha512-W6dkECZmP32EG/l+dp2jCLdYzmnDBIw6jwiLZSER81oR5AHRcVqL+k3Z+pfH1R73le6ayDkJRMk0sutj1bMVeg==", - "license": "MIT", - "dependencies": { - "vfile": "^5.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-statistics/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/vfile-statistics/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/vfile-statistics/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-statistics/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-statistics/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/vue": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", - "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", - "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", - "dependencies": { - "@vue/compiler-sfc": "2.7.16", - "csstype": "^3.1.0" - } - }, - "node_modules/vue-apexcharts": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vue-apexcharts/-/vue-apexcharts-1.7.0.tgz", - "integrity": "sha512-QMpvBllJ1XvFsK4dwcbyxKalVpHfJnoqsNWszY55HJk/Sn7WP1f5YUv4JIzugqu4GTQB6gLcCVwwPDQFtwr0oQ==", - "license": "MIT", - "peerDependencies": { - "apexcharts": ">=4.0.0", - "vue": "^2.5.17" - } - }, - "node_modules/vue-codemirror6": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.5.1.tgz", - "integrity": "sha512-Ey1uQ5ypB2UregJWhWwjRo/YvElzVUlQWBeCHntoLM361bb6iTMS5GTgni+IHdl5D7rEpRRCpAvYRZQidxe5Ag==", - "license": "MIT", - "dependencies": { - "vue-demi": "latest" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0", - "pnpm": ">=10.3.0" - }, - "peerDependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "codemirror": "^6.0.0", - "style-mod": "^4.0.0", - "vue": "^2.7.14 || ^3.3.4" - } - }, - "node_modules/vue-color": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.8.2.tgz", - "integrity": "sha512-1qmsxl5GiIjx/jApBbTGr2r4bN/7WRKUTl3tc53vkXb9Ua0rZmiqsdq6VdG1e7dVNTLJahdsRGWcjeU2+98+NA==", - "license": "MIT", - "dependencies": { - "clamp": "^1.0.1", - "lodash.throttle": "^4.0.0", - "material-colors": "^1.0.0", - "tinycolor2": "^1.1.2" - } - }, - "node_modules/vue-component-type-helpers": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.29.tgz", - "integrity": "sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==", - "dev": true - }, - "node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, - "node_modules/vue-draggable-plus": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/vue-draggable-plus/-/vue-draggable-plus-0.2.7.tgz", - "integrity": "sha512-asNQ4OxTQtN91+m45lh8QEwscXhVveaLvPAzaRXHD8JIk1+SOg7NWPQ9EmUTeNhSP3SGkJ2Agvp5WRDt7vXM8w==", - "license": "MIT", - "peerDependencies": { - "@types/sortablejs": "^1.15.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, - "node_modules/vue-eslint-parser": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", - "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/vue-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/vue-eslint-parser/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/vue-eslint-parser/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vue-frag": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/vue-frag/-/vue-frag-1.4.3.tgz", - "integrity": "sha512-pQZj03f/j9LRhzz9vKaXTCXUHVYHuAXicshFv76VFqwz4MG3bcb+sPZMAbd0wmw7THjkrTPuoM0EG9TbG8CgMQ==", - "funding": { - "url": "https://github.com/privatenumber/vue-frag?sponsor=1" - }, - "peerDependencies": { - "vue": "^2.6.0" - } - }, - "node_modules/vue-functional-data-merge": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz", - "integrity": "sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA==" - }, - "node_modules/vue-hot-reload-api": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", - "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==" - }, - "node_modules/vue-loader": { - "version": "15.11.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.11.1.tgz", - "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==", - "dependencies": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - }, - "peerDependencies": { - "css-loader": "*", - "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "cache-loader": { - "optional": true - }, - "prettier": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/vue-loading-overlay": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/vue-loading-overlay/-/vue-loading-overlay-6.0.6.tgz", - "integrity": "sha512-ZPrWawjCoNKGbCG9z4nePgbs/K9KXPa1j1oAJXP6T8FQho3NO+/chhjx4MLYFzfpwr+xkiQ8SNrV1kUG1bZPAw==", - "license": "MIT", - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/vue-material-design-icons": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.3.0.tgz", - "integrity": "sha512-wnbRh+48RwX/Gt+iqwCSdWpm0hPBwwv9F7MSouUzZ2PsphYVMJB9KkG9iGs+tgBiT57ZiurFEK07Y/rFKx+Ekg==" - }, - "node_modules/vue-resize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz", - "integrity": "sha512-z5M7lJs0QluJnaoMFTIeGx6dIkYxOwHThlZDeQnWZBizKblb99GSejPnK37ZbNE/rVwDcYcHY+Io+AxdpY952w==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "vue": "^2.6.0" - } - }, - "node_modules/vue-router": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", - "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==", - "license": "MIT" - }, - "node_modules/vue-style-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", - "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", - "dependencies": { - "hash-sum": "^1.0.2", - "loader-utils": "^1.0.2" - } - }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/vue-template-es2015-compiler": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", - "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==" - }, - "node_modules/vue2-datepicker": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-3.11.1.tgz", - "integrity": "sha512-6PU/+pnp2mgZAfnSXmbdwj9516XsEvTiw61Q5SNrvvdy8W/FCxk1GAe9UZn/m9YfS5A47yK6XkcjMHbp7aFApA==", - "dependencies": { - "date-format-parse": "^0.2.7" - }, - "peerDependencies": { - "vue": "^2.5.0" - } - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" - }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dev": true, - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/walk-up-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", - "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", - "license": "ISC" - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "license": "MIT", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webdav": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.9.0.tgz", - "integrity": "sha512-OMJ6wtK1WvCO++aOLoQgE96S8KT4e5aaClWHmHXfFU369r4eyELN569B7EqT4OOUb99mmO58GkyuiCv/Ag6J0Q==", - "license": "MIT", - "dependencies": { - "@buttercup/fetch": "^0.2.1", - "base-64": "^1.0.0", - "byte-length": "^1.0.2", - "entities": "^6.0.1", - "fast-xml-parser": "^5.3.4", - "hot-patcher": "^2.0.1", - "layerr": "^3.0.0", - "md5": "^2.3.0", - "minimatch": "^9.0.5", - "nested-property": "^4.0.0", - "node-fetch": "^3.3.2", - "path-posix": "^1.0.0", - "url-join": "^5.0.0", - "url-parse": "^1.5.10" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/webdav/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/webdav/node_modules/fast-xml-parser": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", - "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/webdav/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/webdav/node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/webpack": { - "version": "5.107.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.1.tgz", - "integrity": "sha512-mvdIWxj/H6QsfgDdH9djne3a5dYcmEmtsXGESkypaGN5jXjF/b+9KDlmTDQ2TKlFUeA2fI9Y65kihD30JOdB+Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.21.4", - "es-module-lexer": "^2.1.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "loader-runner": "^4.3.2", - "mime-db": "^1.54.0", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.5.0", - "watchpack": "^2.5.1", - "webpack-sources": "^3.4.1" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", - "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.4", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", - "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/xmlbuilder2": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", - "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oozcitak/dom": "^2.0.2", - "@oozcitak/infra": "^2.0.2", - "@oozcitak/util": "^10.0.0", - "js-yaml": "^4.1.1" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/xmlbuilder2/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/xmlbuilder2/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } + "name": "openregister", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openregister", + "version": "1.0.0", + "license": "EUPL-1.2", + "dependencies": { + "@codemirror/lang-json": "^6.0.1", + "@conduction/nextcloud-vue": "^1.0.0-beta.213", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@nextcloud/axios": "~2.5.1", + "@nextcloud/dialogs": "^6.1.1", + "@nextcloud/initial-state": "^2.2.0", + "@nextcloud/l10n": "^3.2.0", + "@nextcloud/router": "^3.0.0", + "@nextcloud/vue": "^8.39.0", + "@vueuse/core": "^10.7.2", + "apexcharts": "^4.0.0", + "axios": "^1.7.3", + "bootstrap": "^5.3.2", + "bootstrap-vue": "^2.23.1", + "css-loader": "^6.8.1", + "dexie": "^4.0.8", + "dompurify": "^3.4.0", + "marked": "^16.4.0", + "pinia": "^2.1.7", + "remark-cli": "^11.0.0", + "remark-lint-list-item-indent": "^3.1.1", + "remark-preset-lint-consistent": "^5.1.1", + "remark-preset-lint-recommended": "^6.1.2", + "style-loader": "^3.3.3", + "vue": "^2.7.16", + "vue-apexcharts": "^1.7.0", + "vue-codemirror6": "^1.1.5", + "vue-draggable-plus": "^0.2.6", + "vue-frag": "^1.0.0", + "vue-loader": "^15.11.1", + "vue-loading-overlay": "^6.0.3", + "vue-material-design-icons": "^5.2.0", + "vue-template-compiler": "^2.7.16", + "zod": "^3.22.4" + }, + "devDependencies": { + "@babel/core": "^7.23.9", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-env": "^7.23.9", + "@babel/preset-typescript": "^7.26.0", + "@babel/traverse": "^7.23.9", + "@cyclonedx/cyclonedx-npm": "^4.2.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.39.1", + "@nextcloud/browserslist-config": "^2.3.0", + "@nextcloud/eslint-config": "^8.4.1", + "@nextcloud/eslint-plugin": "^2.2.1", + "@nextcloud/stylelint-config": "^2.4.0", + "@nextcloud/webpack-vue-config": "^5.5.0", + "@pinia/testing": "^0.1.3", + "@playwright/test": "^1.49.0", + "@stoplight/spectral-cli": "^6.15.0", + "@types/jest": "^29.5.12", + "@types/node": "^20.17.23", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/test-utils": "^2.4.4", + "@vue/vue2-jest": "^29.2.6", + "babel-jest": "^29.7.0", + "babel-loader": "^9.1.3", + "esbuild": "^0.28.0", + "eslint": "^8.56.0", + "eslint-config-standard": "^17.1.0", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-import-resolver-exports": "^1.0.0-beta.5", + "eslint-import-resolver-typescript": "^3.10.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsdoc": "^46.10.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-vue": "^9.21.1", + "eslint-webpack-plugin": "^4.0.1", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-transform-stub": "^2.0.0", + "postcss": "^8.4.31", + "postcss-html": "^1.8.1", + "stylelint": "^15.11.0", + "stylelint-config-recommended-scss": "^13.1.0", + "stylelint-config-recommended-vue": "^1.6.1", + "stylelint-webpack-plugin": "^4.1.1", + "terser-webpack-plugin": "^5.6.0", + "ts-jest": "^29.1.2", + "ts-loader": "^9.5.1", + "typescript": "^5.8.2", + "vue-router": "^3.6.5" + }, + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + }, + "peerDependencies": { + "vue": "^2.6 || ^3.0", + "vue-frag": "^1.0.0" + }, + "peerDependenciesMeta": { + "vue-frag": { + "optional": true + } + } + }, + "node_modules/@asyncapi/specs": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.9.0.tgz", + "integrity": "sha512-gatFEH2hfJXWmv3vogIjBZfiIbPRC/ISn9UEHZZLZDdMBO0USxt3AFgCC9AY1P+eNE7zjXddXCIT7gz32XOK4g==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz", + "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz", + "integrity": "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.8.tgz", + "integrity": "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", + "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", + "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-typescript": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", + "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.4", + "@babel/parser": "^7.25.4", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.4", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@buttercup/fetch": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", + "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", + "license": "MIT", + "optionalDependencies": { + "node-fetch": "^3.3.0" + } + }, + "node_modules/@buttercup/fetch/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.2.tgz", + "integrity": "sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.8", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz", + "integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.4", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz", + "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", + "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@conduction/nextcloud-vue": { + "version": "1.0.0-beta.213", + "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-1.0.0-beta.213.tgz", + "integrity": "sha512-L3E7kT3AvVb8HvxRWc2nSA6aYXbuMOj9f7HiGiJQOJ9om6ImCrdreMqACB4pEbHGy49FeRnmX5aHI1qt8o1SaQ==", + "license": "EUPL-1.2", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@microsoft/fetch-event-source": "^2.0.1", + "@nextcloud/dialogs": "^6.4.2", + "@nextcloud/event-bus": "^3.3.3", + "@nextcloud/files": "^3.12.2", + "@nextcloud/notify_push": "^1.4.0", + "@nextcloud/password-confirmation": "^5.3.1", + "@toast-ui/editor": "^3.2.2", + "@toast-ui/vue-editor": "^3.2.3", + "@types/react": "^18.0.0", + "@uiw/codemirror-theme-github": "^4.25.8", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "apexcharts": "^4.7.0", + "codemirror": "^6.0.0", + "dompurify": "^3.0.0", + "gridstack": "^10.3.1", + "leaflet": "^1.9.0", + "leaflet.markercluster": "^1.5.3", + "linkifyjs": "^4.3.3", + "lodash": "^4.17.21", + "marked": "^12.0.0", + "style-mod": "^4.0.0", + "vue-apexcharts": "^1.7.0", + "vue-codemirror6": "^1.4.3", + "vue-color": "^2.8.2", + "vue-template-compiler": "^2.7.16", + "vuedraggable": "^2.24.3" + }, + "bin": { + "manifest-migrate": "src/cli/manifest-migrate.js" + }, + "optionalDependencies": { + "@mdi/js": "^7.4.47" + }, + "peerDependencies": { + "@nextcloud/auth": "^2.0.0 || ^3.0.0", + "@nextcloud/axios": "^2.0.0", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/initial-state": "^2.2.0", + "@nextcloud/l10n": "^2.0.0 || ^3.0.0", + "@nextcloud/router": "^2.0.0 || ^3.0.0", + "@nextcloud/vue": "^8.0.0", + "@vueuse/core": "^10.0.0", + "axe-core": "^4.10.0", + "dexie": "^4.0.8", + "dompurify": "^3.0.0", + "marked": "^12.0.0", + "pinia": "^2.0.0", + "vue": "^2.7.0", + "vue-frag": "^1.4.3", + "vue-material-design-icons": "^5.0.0" + }, + "peerDependenciesMeta": { + "axe-core": { + "optional": true + }, + "dexie": { + "optional": true + }, + "dompurify": { + "optional": true + }, + "marked": { + "optional": true + } + } + }, + "node_modules/@conduction/nextcloud-vue/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@conduction/nextcloud-vue/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@conduction/nextcloud-vue/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@conduction/nextcloud-vue/node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", + "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.13" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-npm/-/cyclonedx-npm-4.2.1.tgz", + "integrity": "sha512-SOA/96sf0wsgUYCRtFkLFm6WoFhG+q1BxdC84hPSn9J3xWlH1e7OnTPJT+WNUzTqzX1nSm5JhjRX4krozu2X+g==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@cyclonedx/cyclonedx-library": "^10.0.0", + "commander": "^14.0.0", + "normalize-package-data": "^7.0.0 || ^8.0.0", + "packageurl-js": "^2.0.1", + "spdx-expression-parse": "^3.0.1 || ^4.0.0", + "xmlbuilder2": "^3.0.2 || ^4.0.3" + }, + "bin": { + "cyclonedx-npm": "bin/cyclonedx-npm-cli.js" + }, + "engines": { + "node": ">=20.18.0", + "npm": ">=9" + }, + "optionalDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "ajv-formats-draft2019": "^1.6.1", + "libxmljs2": "^0.35||^0.37" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/@cyclonedx/cyclonedx-library": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-library/-/cyclonedx-library-10.0.0.tgz", + "integrity": "sha512-xDXf2eqzeFHdjamj6oBV3duRSfrlmsJ5+2z9tXp7q5qxJP5Awmjf4ABSutS4qkVHHj7JzKFL/EM0V0Nihc7zPg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" + } + ], + "license": "Apache-2.0", + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "ajv-formats-draft2019": "^1.6.1", + "libxmljs2": "^0.35||^0.37", + "packageurl-js": "*", + "spdx-expression-parse": "*", + "xmlbuilder2": "^3.0.2||^4.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "ajv-formats-draft2019": { + "optional": true + }, + "libxmljs2": { + "optional": true + }, + "packageurl-js": { + "optional": true + }, + "spdx-expression-parse": { + "optional": true + }, + "xmlbuilder2": { + "optional": true + } + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@cyclonedx/cyclonedx-npm/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@file-type/xml": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@file-type/xml/-/xml-0.4.4.tgz", + "integrity": "sha512-NhCyXoHlVZ8TqM476hyzwGJ24+D5IPSaZhmrPj7qXnEVb3q6jrFzA3mM9TBpknKSI9EuQeGTKRg2DXGUwvBBoQ==", + "license": "MIT", + "dependencies": { + "sax": "^1.4.1", + "strtok3": "^10.3.4" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "dev": true, + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "dev": true, + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/ternary": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", + "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", + "dev": true, + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/css": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.3.tgz", + "integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", + "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@linusborg/vue-simple-portal": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@linusborg/vue-simple-portal/-/vue-simple-portal-0.1.5.tgz", + "integrity": "sha512-dq+oubEVW4UabBoQxmH97GiDa+F6sTomw4KcXFHnXEpw69rdkXFCxo1WzwuvWjoLiUVYJTyN1dtlUvTa50VcXg==", + "dependencies": { + "nanoid": "^3.1.20" + }, + "peerDependencies": { + "vue": "^2.6.6" + } + }, + "node_modules/@mapbox/hast-util-table-cell-style": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz", + "integrity": "sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==", + "dependencies": { + "unist-util-visit": "^1.4.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + }, + "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "dependencies": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "dependencies": { + "unist-util-is": "^3.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" + }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", + "license": "Apache-2.0" + }, + "node_modules/@microsoft/fetch-event-source": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", + "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nextcloud/auth": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.6.0.tgz", + "integrity": "sha512-VkT87+9UqpPi7O36bVEE4/MxWF8d90VQcuMlvKltsZyLSLkEGrPXgowtD75Y54k60/8SR6mXbeqBwapi8dDUbA==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/event-bus": "^3.3.3", + "@nextcloud/router": "^3.1.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/axios": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-2.5.2.tgz", + "integrity": "sha512-8frJb77jNMbz00TjsSqs1PymY0nIEbNM4mVmwen2tXY7wNgRai6uXilIlXKOYB9jR/F/HKRj6B4vUwVwZbhdbw==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/auth": "^2.5.1", + "@nextcloud/router": "^3.0.1", + "axios": "^1.12.2" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/browser-storage": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@nextcloud/browser-storage/-/browser-storage-0.5.0.tgz", + "integrity": "sha512-usYr4GlJQlK3hgZURvklqWb9ivi7sgsSuFqXrs7s4hl1LTS4enzPrnkQumm6nRsQruf0ITS+OBsK+oELEbvYPA==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^24 || ^22 || ^20" + } + }, + "node_modules/@nextcloud/browserslist-config": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/browserslist-config/-/browserslist-config-2.3.0.tgz", + "integrity": "sha512-1Tpkof2e9Q0UicHWahQnXXrubJoqyiaqsH9G52v3cjGeVeH3BCfa1FOa41eBwBSFe2/Jxj/wCH2YVLgIXpWbBg==", + "dev": true, + "license": "GPL-3.0-or-later", + "engines": { + "node": "^16.0.0", + "npm": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@nextcloud/capabilities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@nextcloud/capabilities/-/capabilities-1.2.1.tgz", + "integrity": "sha512-snZ0/910zzwN6PDsIlx2Uvktr1S5x0ClhDUnfPlCj7ntNvECzuVHNY5wzby22LIkc+9ZjaDKtCwuCt2ye+9p/Q==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/initial-state": "^3.0.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/capabilities/node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", + "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/dialogs": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.4.2.tgz", + "integrity": "sha512-xj6fyUdb56StWt1DWeDHYnqK0Ck7EWQLUEyWtXHnneknoOV3x/Y0HPRL5L2PTJaDuVQB8TcVW53JjR38JG9W6Q==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@mdi/js": "^7.4.47", + "@nextcloud/auth": "^2.5.3", + "@nextcloud/axios": "^2.5.2", + "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/event-bus": "^3.3.3", + "@nextcloud/files": "^3.12.2", + "@nextcloud/initial-state": "^3.0.0", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "@types/toastify-js": "^1.12.4", + "@vueuse/core": "^11.3.0", + "cancelable-promise": "^4.3.1", + "p-queue": "^9.0.1", + "toastify-js": "^1.12.0", + "vue-frag": "^1.4.3", + "webdav": "^5.8.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + }, + "peerDependencies": { + "@nextcloud/vue": "^8.24.0", + "vue": "^2.7.16" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", + "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@nextcloud/dialogs/node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@nextcloud/eslint-config": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@nextcloud/eslint-config/-/eslint-config-8.4.1.tgz", + "integrity": "sha512-ilrPxOnfVkB4dAddtkhbJmbYK9FwEVZ5oIJ2ipiE97rQz82TUZxmfEHE1tr87FbIvz0drIcREgGil3zuNWHjrg==", + "dev": true, + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.24.5", + "@babel/eslint-parser": "^7.16.5", + "@nextcloud/eslint-plugin": "^2.2.1", + "@vue/eslint-config-typescript": "^13.0.0", + "eslint": "^8.27.0", + "eslint-config-standard": "^17.1.0", + "eslint-import-resolver-exports": "^1.0.0-beta.5", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsdoc": "^46.2.6", + "eslint-plugin-n": "^16.0.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-vue": "^9.7.0", + "typescript": "^5.0.2" + } + }, + "node_modules/@nextcloud/eslint-plugin": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@nextcloud/eslint-plugin/-/eslint-plugin-2.2.1.tgz", + "integrity": "sha512-RX+0FxpL1h2EzjNLeW0VSGTkbyWIq7WgV7QAjtyUmDbSGwf1ds9Zy5OcRkgXRHRIu/W0gB0DhS2iz9qXHphCzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "fast-xml-parser": "^4.2.5", + "requireindex": "^1.2.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/@nextcloud/eslint-plugin/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nextcloud/event-bus": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-3.3.3.tgz", + "integrity": "sha512-zIfvKmUGkXpVzRKoXrcO9hkoiKDm65fqNxy/XIbIxrQhZByPq3gDkjBpnu3V5Gs8JdYwa73R8DjzV9oH8HYhIg==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@types/semver": "^7.7.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^20 || ^22 || ^24" + } + }, + "node_modules/@nextcloud/event-bus/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nextcloud/files": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", + "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@nextcloud/auth": "^2.5.3", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "cancelable-promise": "^4.3.1", + "is-svg": "^6.1.0", + "typescript-event-target": "^1.1.1", + "webdav": "^5.8.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/initial-state": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.2.0.tgz", + "integrity": "sha512-cDW98L5KGGgpS8pzd+05304/p80cyu8U2xSDQGa+kGPTpUFmCbv2qnO5WrwwGTauyjYijCal2bmw82VddSH+Pg==", + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + } + }, + "node_modules/@nextcloud/l10n": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-3.4.1.tgz", + "integrity": "sha512-aTFinTcKiK2gEXwLgutXekpZZ8/v/4QiC8C3QCLH5m0o+WtxsBC+fqV142ebC/rfDnzCLhY4ZtswSu8bFbZocg==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/router": "^3.0.1", + "@nextcloud/typings": "^1.9.1", + "@types/escape-html": "^1.0.4", + "dompurify": "^3.2.6", + "escape-html": "^1.0.3" + }, + "engines": { + "node": "^20 || ^22 || ^24" + } + }, + "node_modules/@nextcloud/logger": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nextcloud/logger/-/logger-3.0.3.tgz", + "integrity": "sha512-TcbVRL4/O5ffI1RXFmQAFD3gwwT15AAdr1770x+RNqVvfBdoGVyhzOwCIyA5Vfc3fA1iJXFa+rE6buJZSoqlcw==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/auth": "^2.5.3" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/notify_push": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@nextcloud/notify_push/-/notify_push-1.4.0.tgz", + "integrity": "sha512-07UDgz1xLG9XABP8+mwQ2CsNWZu6lKzz0ErUA2HfE1ZfxXKiwVpo60t30y34UExGB9+Ok1nFaYU8fyJHncz9aQ==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@nextcloud/axios": "^2.6.0", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/event-bus": "^3.3.3" + } + }, + "node_modules/@nextcloud/password-confirmation": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@nextcloud/password-confirmation/-/password-confirmation-5.3.2.tgz", + "integrity": "sha512-VtRBguZF8LvNn3I/kJ37upG8welNf7s1gnKwF3pQhC+F4n/Hgx6dp+LhM4fncHF7XH56i4LNZno+0IbvOkOQ8Q==", + "license": "MIT", + "dependencies": { + "@nextcloud/auth": "^2.5.3", + "@nextcloud/axios": "^2.5.2", + "@nextcloud/dialogs": "^6.4.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/router": "^3.0.1" + }, + "engines": { + "node": "^20.0.0", + "npm": "^10.5.1" + }, + "peerDependencies": { + "@nextcloud/vue": "^8.23.0", + "vue": "^2.7.16" + } + }, + "node_modules/@nextcloud/paths": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.1.0.tgz", + "integrity": "sha512-vtFYA/kthaUDzu6KejTOL1OwnOy7/yynq5zdB/UBpYacAWjUX5Ddh4OMWx3rEavkBJ9/QGhrFryNJLjNfe8OQA==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/router": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.1.0.tgz", + "integrity": "sha512-e4dkIaxRSwdZJlZFpn9x03QgBn/Sa2hN1hp/BA7+AbzykmSAlKuWfdmX8j/8ewrLpQwYmZR23IZO9XwpJXq2Uw==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/typings": "^1.10.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/sharing": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", + "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/initial-state": "^3.0.0", + "is-svg": "^6.1.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + }, + "optionalDependencies": { + "@nextcloud/files": "^3.12.0" + } + }, + "node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", + "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/stylelint-config": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@nextcloud/stylelint-config/-/stylelint-config-2.4.0.tgz", + "integrity": "sha512-S/q/offcs9pwnkjSrnfvsONryCOe6e1lfK2sszN6ZtkYyXvaqi8EbQuuhaGlxCstn9oXwbXfAI6O3Y8lGrjdFg==", + "dev": true, + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + }, + "peerDependencies": { + "stylelint": "^15.6.0", + "stylelint-config-recommended-scss": "^13.1.0", + "stylelint-config-recommended-vue": "^1.1.0" + } + }, + "node_modules/@nextcloud/typings": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.10.0.tgz", + "integrity": "sha512-SMC42rDjOH3SspPTLMZRv76ZliHpj2JJkF8pGLP8l1QrVTZxE47Qz5qeKmbj2VL+dRv2e/NgixlAFmzVnxkhqg==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@types/jquery": "3.5.16" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/vue": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-8.39.0.tgz", + "integrity": "sha512-TJgrFeVr82CN8ng4y+IxMBb7mKlww7Fot22z33+Q0zKgWvi5EoQ0vYescA3Drl8cIRSGktUdFm3xO2gAqvnwoA==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@floating-ui/dom": "^1.7.6", + "@linusborg/vue-simple-portal": "^0.1.5", + "@nextcloud/auth": "^2.6.0", + "@nextcloud/axios": "^2.5.2", + "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/event-bus": "^3.3.3", + "@nextcloud/initial-state": "^2.2.0", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.4.0", + "@nextcloud/vue-select": "^3.26.0", + "@vueuse/components": "^11.0.0", + "@vueuse/core": "^11.0.0", + "blurhash": "^2.0.5", + "clone": "^2.1.2", + "debounce": "^2.2.0", + "dompurify": "^3.4.2", + "emoji-mart-vue-fast": "^15.0.5", + "escape-html": "^1.0.3", + "floating-vue": "^1.0.0-beta.19", + "focus-trap": "^7.8.0", + "linkify-string": "^4.3.2", + "md5": "^2.3.0", + "mdast-util-to-string": "^4.0.0", + "p-queue": "^8.1.1", + "rehype-external-links": "^3.0.0", + "rehype-highlight": "^7.0.2", + "rehype-react": "^7.1.2", + "remark-breaks": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "remark-unlink-protocols": "^1.0.0", + "splitpanes": "^2.4.1", + "string-length": "^5.0.1", + "striptags": "^3.2.0", + "tabbable": "^6.4.0", + "tributejs": "^5.1.3", + "unified": "^11.0.1", + "unist-builder": "^4.0.0", + "unist-util-visit-parents": "^6.0.2", + "vue": "^2.7.16", + "vue-color": "^2.8.1", + "vue-frag": "^1.4.3", + "vue-router": "^3.6.5", + "vue2-datepicker": "^3.11.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/vue-select": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@nextcloud/vue-select/-/vue-select-3.26.0.tgz", + "integrity": "sha512-UvJExrxzx5pP3lv7j6zrv2yj6B1dXph7sh3lLNPnbJPjPoH/yg58mHNFBcPJrRYMbpy2t3hlC6F7s33KCTr9FA==", + "license": "MIT", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + }, + "peerDependencies": { + "vue": "2.x" + } + }, + "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.4.0.tgz", + "integrity": "sha512-1hUNyc7uJdBpnimOnEshJjEtAPAjzDYVl6qmWqF5ZxoN9wOvbExw0QjX3xFIbHbX2dmvbRNLBj0RzLzipmZyeg==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/initial-state": "^3.0.0", + "is-svg": "^6.1.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + }, + "optionalDependencies": { + "@nextcloud/files": "^3.12.2 || ^4.0.0" + } + }, + "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", + "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/vue/node_modules/@vueuse/core": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz", + "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "11.3.0", + "@vueuse/shared": "11.3.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nextcloud/vue/node_modules/@vueuse/metadata": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz", + "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nextcloud/vue/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/@nextcloud/vue/node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nextcloud/vue/node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nextcloud/webpack-vue-config": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@nextcloud/webpack-vue-config/-/webpack-vue-config-5.5.1.tgz", + "integrity": "sha512-2Q3kPnsvkKeENG3rruTjAE1oz/9w0u5QMKO8oh9arL2TIO2aswkDKBtkmu6q7Po3tE+3B2f+ggHq1PNkhtwu6w==", + "dev": true, + "license": "AGPL-3.0-or-later", + "engines": { + "node": "^16.0.0", + "npm": "^7.0.0 || ^8.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.19.6", + "babel-loader": "^9.0.0", + "css-loader": "^6.7.1", + "node-polyfill-webpack-plugin": "2.0.1", + "sass": "^1.55.0", + "sass-loader": "^13.1.0", + "style-loader": "^3.3.1", + "vue": "^2.7.13", + "vue-loader": "^15.10.0", + "vue-template-compiler": "^2.7.13", + "webpack": "^5.74.0", + "webpack-cli": "^5.0.1", + "webpack-dev-server": "^4.11.1" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@npmcli/config": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-6.4.1.tgz", + "integrity": "sha512-uSz+elSGzjCMANWa5IlbGczLYPkNI/LeR+cHrgaTqTrTSh9RHhOFA4daD2eRUz6lMtOW+Fnsb+qv7V2Zz8ML0g==", + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^3.0.2", + "ci-info": "^4.0.0", + "ini": "^4.1.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/config/node_modules/ci-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@npmcli/config/node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/config/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/map-workspaces": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.6.tgz", + "integrity": "sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==", + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", + "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.3.3.tgz", + "integrity": "sha512-6IKCd+gP0HliixqZT/p8nW3tucD6Sv/u/eR2A9X4rxT/6hXlMzA4GZQzq4d2qnBAwSwGpmKyzkyTjNjrhaA25A==", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.7" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@nuxt/opencollective/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@nuxt/opencollective/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@nuxt/opencollective/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@nuxt/opencollective/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@nuxt/opencollective/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nuxt/opencollective/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@oozcitak/dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@pinia/testing": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.5.tgz", + "integrity": "sha512-AcGzuotkzhRoF00htuxLfIPBBHVE6HjjB3YC5Y3os8vRgKu6ipknK5GBQq9+pduwYQhZ+BcCZDC9TyLAUlUpoQ==", + "dev": true, + "dependencies": { + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "pinia": ">=2.2.1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-22.0.2.tgz", + "integrity": "sha512-//NdP6iIwPbMTcazYsiBMbJW7gfmpHom33u1beiIoHDEM0Q9clvtQB1T0efvMqHeKsGohiHo97BCPCkBXdscwg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@stoplight/json": { + "version": "3.21.7", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", + "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", + "dev": true, + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.3", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "jsonc-parser": "~2.2.1", + "lodash": "^4.17.21", + "safe-stable-stringify": "^1.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", + "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.0", + "tslib": "^1.14.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@stoplight/json-ref-resolver": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", + "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", + "dev": true, + "dependencies": { + "@stoplight/json": "^3.21.0", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^12.3.0 || ^13.0.0", + "@types/urijs": "^1.19.19", + "dependency-graph": "~0.11.0", + "fast-memoize": "^2.5.2", + "immer": "^9.0.6", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "urijs": "^1.19.11" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/ordered-object-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", + "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/path": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", + "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-cli": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.15.0.tgz", + "integrity": "sha512-FVeQIuqQQnnLfa8vy+oatTKUve7uU+3SaaAfdjpX/B+uB1NcfkKRJYhKT9wMEehDRaMPL5AKIRYMCFerdEbIpw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "~3.21.0", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-core": "^1.19.5", + "@stoplight/spectral-formatters": "^1.4.1", + "@stoplight/spectral-parsers": "^1.0.4", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@stoplight/spectral-ruleset-bundler": "^1.6.0", + "@stoplight/spectral-ruleset-migrator": "^1.11.0", + "@stoplight/spectral-rulesets": ">=1", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "^13.6.0", + "chalk": "4.1.2", + "fast-glob": "~3.2.12", + "hpagent": "~1.2.0", + "lodash": "~4.17.21", + "pony-cause": "^1.1.1", + "stacktracey": "^2.1.8", + "tslib": "^2.8.1", + "yargs": "~17.7.2" + }, + "bin": { + "spectral": "dist/index.js" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@stoplight/spectral-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stoplight/spectral-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@stoplight/spectral-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@stoplight/spectral-cli/node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@stoplight/spectral-cli/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@stoplight/spectral-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-core": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.21.0.tgz", + "integrity": "sha512-oj4e/FrDLUhBRocIW+lRMKlJ/q/rDZw61HkLbTFsdMd+f/FTkli2xHNB1YC6n1mrMKjjvy7XlUuFkC7XxtgbWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "~3.21.0", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-parsers": "^1.0.0", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "~13.6.0", + "@types/es-aggregate-error": "^1.0.2", + "@types/json-schema": "^7.0.11", + "ajv": "^8.17.1", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "es-aggregate-error": "^1.0.7", + "jsonpath-plus": "^10.3.0", + "lodash": "~4.17.23", + "lodash.topath": "^4.5.2", + "minimatch": "3.1.2", + "nimma": "0.2.3", + "pony-cause": "^1.1.1", + "simple-eval": "1.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "dev": true, + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "dev": true, + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@stoplight/spectral-formats": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", + "integrity": "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw==", + "dev": true, + "dependencies": { + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.2", + "@types/json-schema": "^7.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-formatters": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-formatters/-/spectral-formatters-1.5.0.tgz", + "integrity": "sha512-lR7s41Z00Mf8TdXBBZQ3oi2uR8wqAtR6NO0KA8Ltk4FSpmAy0i6CKUmJG9hZQjanTnGmwpQkT/WP66p1GY3iXA==", + "dev": true, + "dependencies": { + "@stoplight/path": "^1.3.2", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "^13.15.0", + "@types/markdown-escape": "^1.1.3", + "chalk": "4.1.2", + "cliui": "7.0.4", + "lodash": "^4.17.21", + "markdown-escape": "^2.0.0", + "node-sarif-builder": "^2.0.3", + "strip-ansi": "6.0", + "text-table": "^0.2.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-formatters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@stoplight/spectral-formatters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stoplight/spectral-formatters/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@stoplight/spectral-formatters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@stoplight/spectral-formatters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@stoplight/spectral-formatters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-formatters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-functions": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz", + "integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==", + "dev": true, + "dependencies": { + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.1", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-runtime": "^1.1.2", + "ajv": "^8.17.1", + "ajv-draft-04": "~1.0.0", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "dev": true, + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "dev": true, + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@stoplight/spectral-parsers": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", + "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", + "dev": true, + "dependencies": { + "@stoplight/json": "~3.21.0", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml": "~4.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-ref-resolver": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", + "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", + "dev": true, + "dependencies": { + "@stoplight/json-ref-readers": "1.2.2", + "@stoplight/json-ref-resolver": "~3.1.6", + "@stoplight/spectral-runtime": "^1.1.2", + "dependency-graph": "0.11.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-ruleset-bundler": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-bundler/-/spectral-ruleset-bundler-1.6.3.tgz", + "integrity": "sha512-AQFRO6OCKg8SZJUupnr3+OzI1LrMieDTEUHsYgmaRpNiDRPvzImE3bzM1KyQg99q58kTQyZ8kpr7sG8Lp94RRA==", + "dev": true, + "dependencies": { + "@rollup/plugin-commonjs": "~22.0.2", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-core": ">=1", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-functions": ">=1", + "@stoplight/spectral-parsers": ">=1", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@stoplight/spectral-ruleset-migrator": "^1.9.6", + "@stoplight/spectral-rulesets": ">=1", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "^13.6.0", + "@types/node": "*", + "pony-cause": "1.1.1", + "rollup": "~2.79.2", + "tslib": "^2.8.1", + "validate-npm-package-name": "3.0.0" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-ruleset-migrator": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-migrator/-/spectral-ruleset-migrator-1.11.3.tgz", + "integrity": "sha512-+9Y1zFxYmSsneT5FPkgS1IlRQs0VgtdMT77f5xf6vzje9ezyhfs7oXwbZOCSZjEJew8iVZBKQtiOFndcBrdtqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "~3.21.0", + "@stoplight/ordered-object-literal": "~1.0.4", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-functions": "^1.9.1", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "^13.6.0", + "@stoplight/yaml": "~4.2.3", + "@types/node": "*", + "ajv": "^8.17.1", + "ast-types": "0.14.2", + "astring": "^1.9.0", + "reserved": "0.1.2", + "tslib": "^2.8.1", + "validate-npm-package-name": "3.0.0" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.2.3.tgz", + "integrity": "sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw==", + "dev": true, + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.1", + "@stoplight/types": "^13.0.0", + "@stoplight/yaml-ast-parser": "0.0.48", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=10.8" + } + }, + "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml-ast-parser": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz", + "integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==", + "dev": true + }, + "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@stoplight/spectral-rulesets": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.0.tgz", + "integrity": "sha512-l2EY2jiKKLsvnPfGy+pXC0LeGsbJzcQP5G/AojHgf+cwN//VYxW1Wvv4WKFx/CLmLxc42mJYF2juwWofjWYNIQ==", + "dev": true, + "dependencies": { + "@asyncapi/specs": "^6.8.0", + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.19.4", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-functions": "^1.9.1", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "^13.6.0", + "@types/json-schema": "^7.0.7", + "ajv": "^8.17.1", + "ajv-formats": "~2.1.1", + "json-schema-traverse": "^1.0.0", + "leven": "3.1.0", + "lodash": "~4.17.21", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "dev": true, + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@stoplight/spectral-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.4.tgz", + "integrity": "sha512-YHbhX3dqW0do6DhiPSgSGQzr6yQLlWybhKwWx0cqxjMwxej3TqLv3BXMfIUYFKKUqIwH4Q2mV8rrMM8qD2N0rQ==", + "dev": true, + "dependencies": { + "@stoplight/json": "^3.20.1", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "abort-controller": "^3.0.0", + "lodash": "^4.17.21", + "node-fetch": "^2.7.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/types": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", + "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", + "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", + "dev": true, + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.5", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml-ast-parser": "0.0.50", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=10.8" + } + }, + "node_modules/@stoplight/yaml-ast-parser": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", + "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", + "dev": true + }, + "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@svgdotjs/svg.draggable.js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", + "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", + "license": "MIT", + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4" + } + }, + "node_modules/@svgdotjs/svg.filter.js": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz", + "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.js": "^3.2.4" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@svgdotjs/svg.js": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz", + "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/@svgdotjs/svg.resize.js": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz", + "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", + "license": "MIT", + "engines": { + "node": ">= 14.18" + }, + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4", + "@svgdotjs/svg.select.js": "^4.0.1" + } + }, + "node_modules/@svgdotjs/svg.select.js": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz", + "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", + "license": "MIT", + "engines": { + "node": ">= 14.18" + }, + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4" + } + }, + "node_modules/@toast-ui/editor": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@toast-ui/editor/-/editor-3.2.2.tgz", + "integrity": "sha512-ASX7LFjN2ZYQJrwmkUajPs7DRr9FsM1+RQ82CfTO0Y5ZXorBk1VZS4C2Dpxinx9kl55V4F8/A2h2QF4QMDtRbA==", + "license": "MIT", + "dependencies": { + "dompurify": "^2.3.3", + "prosemirror-commands": "^1.1.9", + "prosemirror-history": "^1.1.3", + "prosemirror-inputrules": "^1.1.3", + "prosemirror-keymap": "^1.1.4", + "prosemirror-model": "^1.14.1", + "prosemirror-state": "^1.3.4", + "prosemirror-view": "^1.18.7" + } + }, + "node_modules/@toast-ui/vue-editor": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@toast-ui/vue-editor/-/vue-editor-3.2.3.tgz", + "integrity": "sha512-IjoV5tBh/yesIuqRqmOQx1+F0oeeAbIeBA7edMTawIXHQXBeJ1qzGHLTY5NWrUQ6BBtV8CDBeedjnVsJ+mHjKQ==", + "license": "MIT", + "dependencies": { + "@toast-ui/editor": "^3.2.2" + }, + "peerDependencies": { + "vue": "^2.5.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/concat-stream": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-2.0.3.tgz", + "integrity": "sha512-3qe4oQAPNwVNwK4C9c8u+VJqv9kez+2MR4qJpoPFfXtgxxif1QbFusvXzK0/Wra2VX07smostI2VMmJNSpZjuQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/es-aggregate-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", + "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/escape-html": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", + "integrity": "sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "8.56.11", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", + "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.2.tgz", + "integrity": "sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/is-empty": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/is-empty/-/is-empty-1.2.3.tgz", + "integrity": "sha512-4J1l5d79hoIvsrKh5VUKVRA1aIdsOb10Hu5j3J2VfP/msDnfTdGPmNp2E1Wg+vs97Bktzo+MZePFFXSGoykYJw==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jquery": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", + "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/markdown-escape": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/markdown-escape/-/markdown-escape-1.1.3.tgz", + "integrity": "sha512-JIc1+s3y5ujKnt/+N+wq6s/QdL2qZ11fP79MijrVXsAAnzSxCbT2j/3prHRouJdZ2yFLN3vkP0HytfnoCczjOw==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/node": { + "version": "20.17.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.23.tgz", + "integrity": "sha512-8PCGZ1ZJbEZuYNTMqywO+Sj4vSKjSjT6Ua+6RFOYlEvIvKQABPtrNkoVSLSKDb4obYcMhspVKmsw8Cm10NFRUg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/react": { + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sortablejs": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "node_modules/@types/supports-color": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", + "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "license": "MIT" + }, + "node_modules/@types/text-table": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@types/text-table/-/text-table-0.2.5.tgz", + "integrity": "sha512-hcZhlNvMkQG/k1vcZ6yHOl6WAYftQ2MLfTHcYRZ2xYZFD8tGVnE3qFV0lj1smQeDSR7/yY0PyuUalauf33bJeA==", + "license": "MIT" + }, + "node_modules/@types/toastify-js": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.12.4.tgz", + "integrity": "sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==", + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@types/urijs": { + "version": "1.19.25", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", + "integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@uiw/codemirror-theme-github": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.9.tgz", + "integrity": "sha512-AGpTamNiySKNzq3Jc7QjpwgQRVaHUaBtmOKiUDghYSfEGjsc5uW4NUW70sSU3BnkGv+lCTUnF3175KM24BWZbw==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.9" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.9.tgz", + "integrity": "sha512-DAHKb/L9ELwjY4nCf/MP/mIllHOn4GQe7RR4x8AMJuNeh9nGRRoo1uPxrxMmUL/bKqe6kDmDbIZ2AlhlqyIJuw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vue/compiler-sfc": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "dependencies": { + "@babel/parser": "^7.23.5", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/component-compiler-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", + "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", + "dependencies": { + "consolidate": "^0.15.1", + "hash-sum": "^1.0.2", + "lru-cache": "^4.1.2", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.36", + "postcss-selector-parser": "^6.0.2", + "source-map": "~0.6.1", + "vue-template-es2015-compiler": "^1.9.0" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", + "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "peerDependencies": { + "eslint": "^8.56.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": ">=4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/@vue/vue2-jest": { + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/@vue/vue2-jest/-/vue2-jest-29.2.6.tgz", + "integrity": "sha512-nPu9IvnEkP0AEpo9ETOAk50uqyBa0QMJ9GnPYkC7EukFN1z29QKjyucICayMt8KuHJ9oYBca2TDMH40HowY9mQ==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "@vue/component-compiler-utils": "^3.1.0", + "chalk": "^2.1.0", + "css-tree": "^2.0.1", + "source-map": "0.5.6", + "tsconfig": "^7.0.0" + }, + "engines": { + "node": ">10" + }, + "peerDependencies": { + "@babel/core": "7.x", + "babel-jest": "29.x", + "jest": "29.x", + "typescript": ">= 4.3", + "vue": "^2.x", + "vue-template-compiler": "^2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vueuse/components": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-11.3.0.tgz", + "integrity": "sha512-sqaGtWPgobXvZmv3atcjW8YW0ypecFuB286OEKFXaPrLsA5b2Y+xAvHvq5V7d+VJRKt705gCK3BNBjxu3g1PdQ==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "11.3.0", + "@vueuse/shared": "11.3.0", + "vue-demi": ">=0.14.10" + } + }, + "node_modules/@vueuse/components/node_modules/@vueuse/core": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz", + "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "11.3.0", + "@vueuse/shared": "11.3.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/components/node_modules/@vueuse/metadata": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz", + "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz", + "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "license": "MIT" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats-draft2019": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ajv-formats-draft2019/-/ajv-formats-draft2019-1.6.1.tgz", + "integrity": "sha512-JQPvavpkWDvIsBp2Z33UkYCtXCSpW4HD3tAZ+oL4iEFOk9obQZffx0yANwECt6vzr6ET+7HN5czRyqXbnq/u0Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.1.1", + "schemes": "^1.4.0", + "smtp-address-parser": "^1.0.3", + "uri-js": "^4.4.1" + }, + "peerDependencies": { + "ajv": "*" + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "peer": true, + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apexcharts": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.7.0.tgz", + "integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.draggable.js": "^3.0.4", + "@svgdotjs/svg.filter.js": "^3.0.8", + "@svgdotjs/svg.js": "^3.2.4", + "@svgdotjs/svg.resize.js": "^2.0.2", + "@svgdotjs/svg.select.js": "^4.0.1", + "@yr/monotone-cubic-spline": "^1.0.3" + } + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "dev": true, + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz", + "integrity": "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.7", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.1.tgz", + "integrity": "sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.7", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz", + "integrity": "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.7" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "node_modules/blurhash": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", + "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/bonjour-service": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.3.tgz", + "integrity": "sha512-2Kd5UYlFUVgAKMTyuBLl6w49wqfOnbxHqmuH0oCl/n7TfAikR0zoowNOP5BU4dfXmm+Vr9JyEN370auSMx+CNg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-vue": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.23.1.tgz", + "integrity": "sha512-SEWkG4LzmMuWjQdSYmAQk1G/oOKm37dtNfjB5kxq0YafnL2W6qUAmeDTcIZVbPiQd2OQlIkWOMPBRGySk/zGsg==", + "hasInstallScript": true, + "dependencies": { + "@nuxt/opencollective": "^0.3.2", + "bootstrap": "^4.6.1", + "popper.js": "^1.16.1", + "portal-vue": "^2.1.7", + "vue-functional-data-merge": "^3.1.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.6.tgz", + "integrity": "sha512-sd+Q65fjlWCYWtZKXiKfrUc8d+4jtp/8f0W2NkwzLtoW4bI6UDnWusLWIurHnmurW0XShIRxpwiOX4EoPtXUAg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "bn.js": "^5.2.3", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/builtins/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz", + "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cancelable-promise": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cancelable-promise/-/cancelable-promise-4.3.1.tgz", + "integrity": "sha512-A/8PwLk/T7IJDfUdQ68NR24QHa8rIlnN/stiJEBo6dmVUkD4K14LswG0w3VwdeK/o7qOwRUR1k2MhK5Rpy2m7A==", + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/char-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", + "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/clamp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", + "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true, + "peer": true + }, + "node_modules/consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", + "dependencies": { + "bluebird": "^3.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/core-js": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.0.tgz", + "integrity": "sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/css-functions-list": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.2.tgz", + "integrity": "sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==", + "dev": true, + "engines": { + "node": ">=12 || >=16" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-format-parse": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/date-format-parse/-/date-format-parse-0.2.7.tgz", + "integrity": "sha512-/+lyMUKoRogMuTeOVii6lUwjbVlesN9YRYLzZT/g3TEZ3uD9QnpjResujeEqUW+OSNbT7T1+SYdyEkTcRv+KDQ==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==" + }, + "node_modules/debounce": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dexie": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.4.tgz", + "integrity": "sha512-jIwsYI8Os2hgnqc6O49YwFDKGc5v5QjGx0wPVp543ip1F53VFAKMLthV2pQosQcVTv3eAskTWYspOx195PM0FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domain-browser": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz", + "integrity": "sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==", + "dev": true, + "license": "Artistic-2.0", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", + "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "license": "ISC" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-mart-vue-fast": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/emoji-mart-vue-fast/-/emoji-mart-vue-fast-15.0.5.tgz", + "integrity": "sha512-wnxLor8ggpqshoOPwIc33MdOC3A1XFeDLgUwYLPtNPL8VeAtXJAVrnFq1CN5PeCYAFoLo4IufHQZ9CfHD4IZiw==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.18.6", + "core-js": "^3.23.5" + }, + "peerDependencies": { + "vue": ">2.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz", + "integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-aggregate-error": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", + "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "globalthis": "^1.0.4", + "has-property-descriptors": "^1.0.2", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT", + "peer": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-compat-utils/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-import-resolver-alias": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", + "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "eslint-plugin-import": ">=1.4.0" + } + }, + "node_modules/eslint-import-resolver-exports": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-exports/-/eslint-import-resolver-exports-1.0.0-beta.5.tgz", + "integrity": "sha512-o6t0w7muUpXr7MkUVzD5igQoDfAQvTmcPp8HEAJdNF8eOuAO+yn6I/TTyMxz9ecCwzX7e02vzlkHURoScUuidg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve.exports": "^2.0.0" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "46.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.10.1.tgz", + "integrity": "sha512-x8wxIpv00Y50NyweDUpa+58ffgSAI5sqe+zcZh33xphD0AVh+1kqr1ombaTRb7Fhpove1zfUuujlX9DWWBP5ag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.41.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-n": { + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", + "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "builtins": "^5.0.1", + "eslint-plugin-es-x": "^7.5.0", + "get-tsconfig": "^4.7.0", + "globals": "^13.24.0", + "ignore": "^5.2.4", + "is-builtin-module": "^3.2.1", + "is-core-module": "^2.12.1", + "minimatch": "^3.1.2", + "resolve": "^1.22.2", + "semver": "^7.5.3" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-n/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", + "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz", + "integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-rsfpFQ01AWQbqtjgPRr2usVRxhWDuG0YDYcG8DJOteD3EFnpeuYuOwk0PQiN7PRBTqS6ElNdtPZPggj8If9WnA==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/eslint/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" + }, + "node_modules/fast-xml-builder": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", + "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz", + "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/floating-vue": { + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-1.0.0-beta.19.tgz", + "integrity": "sha512-OcM7z5Ua4XAykqolmvPj3l1s+KqUKj6Xz2t66eqjgaWfNBjtuifmxO5+4rRXakIch/Crt8IH+vKdKcR3jOUaoQ==", + "dependencies": { + "@floating-ui/dom": "^0.1.10", + "vue-resize": "^1.0.0" + }, + "peerDependencies": { + "vue": "^2.6.10" + } + }, + "node_modules/floating-vue/node_modules/@floating-ui/core": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.3.1.tgz", + "integrity": "sha512-ensKY7Ub59u16qsVIFEo2hwTCqZ/r9oZZFh51ivcLGHfUwTn8l1Xzng8RJUe91H/UP8PeqeBronAGx0qmzwk2g==" + }, + "node_modules/floating-vue/node_modules/@floating-ui/dom": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.1.10.tgz", + "integrity": "sha512-4kAVoogvQm2N0XE0G6APQJuCNuErjOfPW8Ux7DFxh8+AfugWflwVJ5LDlHOwrwut7z/30NUvdtHzQ3zSip4EzQ==", + "dependencies": { + "@floating-ui/core": "^0.3.0" + } + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense", + "peer": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/get-source/node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true + }, + "node_modules/get-source/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gridstack": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/gridstack/-/gridstack-10.3.1.tgz", + "integrity": "sha512-Ra82k/88gdeiu3ZP40COS4bI4sGhNQlZAaAQ6szfPfr68zVpsXxiyLKr5zYcTpKX4jjcwyNsNNdcV1tDJc71fA==", + "funding": [ + { + "type": "paypal", + "url": "https://www.paypal.me/alaind831" + }, + { + "type": "venmo", + "url": "https://www.venmo.com/adumesny" + } + ], + "license": "MIT" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==" + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-to-hyperscript": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-10.0.3.tgz", + "integrity": "sha512-NuBoUStp4fRwmvlfbidlEiRSTk0gSHm+97q4Xn9CJ10HO+Py7nlTuDi6RhM1qLOureukGrCXLG7AAxaGqqyslQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.1", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-to-hyperscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/hot-patcher": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz", + "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==", + "license": "MIT" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-meta-resolve": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz", + "integrity": "sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-empty": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-empty/-/is-empty-1.2.0.tgz", + "integrity": "sha512-F2FnH/otLNJv0J6wc73A5Xo7oHLNnqplYqZhUu01tD54DIPvxIRSTSLkrUB/M0nHO4vo1O9PDfN4KoTxCzLh/w==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-svg": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-6.1.0.tgz", + "integrity": "sha512-i7YPdvYuSCYcaLQrKwt8cvKTlwHcdA6Hp8N9SO3Q5jIzo8x6kH3N47W0BvPP7NdxVBmIHx7X9DK36czYYW7lHg==", + "license": "MIT", + "dependencies": { + "@file-type/xml": "^0.4.3" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-transform-stub": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz", + "integrity": "sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "dev": true, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", + "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/jsonpath-plus": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", + "dev": true, + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", + "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", + "dev": true + }, + "node_modules/launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, + "node_modules/layerr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz", + "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==", + "license": "MIT" + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "license": "MIT", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libxmljs2": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.37.0.tgz", + "integrity": "sha512-Xb78V8GZouoZFrq8cCwx7+G3WYOcJG0xb3YUbweSyE4z2EIrQCZMr3Ye/dHn4mESs6YxUMeQeUZm5IXg+iLHog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "~1.5.0", + "nan": "~2.22.2", + "node-gyp": "^11.2.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/linkify-string": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkify-string/-/linkify-string-4.3.2.tgz", + "integrity": "sha512-JqBuQpSa+CSj2tskIII70SKOjPfjXwDFyjRRNFTrlg76gp2nap36xeRj/cWaXxukqBNrxM+L07XyKRsUtH/DpQ==", + "license": "MIT", + "peerDependencies": { + "linkifyjs": "^4.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, + "node_modules/load-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/load-plugin/-/load-plugin-5.1.0.tgz", + "integrity": "sha512-Lg1CZa1CFj2CbNaxijTL6PCbzd4qGTlZov+iH2p5Xwy/ApcZJh+i6jMN2cYePouTfjJfrNu3nXFdEw8LvbjPFQ==", + "license": "MIT", + "dependencies": { + "@npmcli/config": "^6.0.0", + "import-meta-resolve": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-escape": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-escape/-/markdown-escape-2.0.0.tgz", + "integrity": "sha512-Trz4v0+XWlwy68LJIyw3bLbsJiC8XAbRCKF9DbEtZjyndKOGVx6n+wNB0VfoRmY2LKboQLeniap3xrb6LGSJ8A==", + "dev": true + }, + "node_modules/marked": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.0.tgz", + "integrity": "sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/mdast-comment-marker": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-comment-marker/-/mdast-comment-marker-2.1.2.tgz", + "integrity": "sha512-HED3ezseRVkBzZ0uK4q6RJMdufr/2p3VfVZstE3H1N9K8bwtspztWo6Xd7rEatuGNoCXaBna8oEqMwUn0Ve1bw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-mdx-expression": "^1.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-comment-marker/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-comment-marker/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/mdast-squeeze-paragraphs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-6.0.0.tgz", + "integrity": "sha512-6NDbJPTg0M0Ye+TlYwX1KJ1LFbp515P2immRJyJQhc9Na9cetHzSoHNYIQcXpANEAP1sm9yd/CTZU2uHqR5A+w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-heading-style": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-heading-style/-/mdast-util-heading-style-2.0.1.tgz", + "integrity": "sha512-0L5rthU4xKDVbw+UQ7D8Y8xOEsX4JXZvemWoEAsL+WAaeSH+TvVVwFnTb3G/OrjyP4VYQULoNWU+PdZfkmNu4A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-heading-style/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-util-heading-style/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/mdast-util-mdx-expression": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz", + "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-mdx-expression/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-newline-to-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "peer": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/meow": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", + "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.2", + "camelcase-keys": "^7.0.0", + "decamelize": "^5.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.2", + "read-pkg-up": "^8.0.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^1.2.2", + "yargs-parser": "^20.2.9" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/merge-source-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/moo": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", + "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT", + "peer": true + }, + "node_modules/nested-property": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", + "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==", + "license": "MIT" + }, + "node_modules/nimma": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", + "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", + "dev": true, + "dependencies": { + "@jsep-plugin/regex": "^1.0.1", + "@jsep-plugin/ternary": "^1.0.2", + "astring": "^1.8.1", + "jsep": "^1.2.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + }, + "optionalDependencies": { + "jsonpath-plus": "^6.0.1 || ^10.1.0", + "lodash.topath": "^4.5.2" + } + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "peer": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-polyfill-webpack-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz", + "integrity": "sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "assert": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "console-browserify": "^1.2.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.12.0", + "domain-browser": "^4.22.0", + "events": "^3.3.0", + "filter-obj": "^2.0.2", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "punycode": "^2.1.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^4.0.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.3.0", + "timers-browserify": "^2.0.12", + "tty-browserify": "^0.0.1", + "type-fest": "^2.14.0", + "url": "^0.11.0", + "util": "^0.12.4", + "vm-browserify": "^1.1.2" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "webpack": ">=5" + } + }, + "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "license": "MIT" + }, + "node_modules/node-sarif-builder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-2.0.3.tgz", + "integrity": "sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==", + "dev": true, + "dependencies": { + "@types/sarif": "^2.1.4", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "dev": true + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", + "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "node_modules/packageurl-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-2.0.1.tgz", + "integrity": "sha512-N5ixXjzTy4QDQH0Q9YFjqIWd6zH6936Djpl2m9QNFmDv5Fum8q8BjkpAcHNMzOFE0IwQrFhJWex3AN6kS0OSwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)", + "peer": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", + "license": "ISC" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.6.tgz", + "integrity": "sha512-BT6eelPB1EyGHo8pC0o9Bl6k6SYVhKO1jEbd3lcTrtr7XHdjP8BW1YpfCV3G9Kwkxgattk+S5q2/RvuttCsS1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.2.tgz", + "integrity": "sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pony-cause": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", + "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/portal-vue": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-2.1.7.tgz", + "integrity": "sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g==", + "peerDependencies": { + "vue": "^2.5.18" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-html": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.8.1.tgz", + "integrity": "sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^8.0.0", + "js-tokens": "^9.0.0", + "postcss": "^8.5.0", + "postcss-safe-parser": "^6.0.0" + }, + "engines": { + "node": "^12 || >=14" + } + }, + "node_modules/postcss-html/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.10", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.10.tgz", + "integrity": "sha512-9n6rH4DbJU1eH4SxLt6Y0HhJIo6cZsb7DJ/30uob1hOKPeO6TAaMWI2tc7kwR92BjfPOU2fFHWbZLovLi3XQfA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.0.tgz", + "integrity": "sha512-N54DF3OXNWDuP81G1kbfCys8ZzIjuL1VnvJ2mk5STSu/fNxWIcX/EutQLA3s9KR/2wVhgDi4hzBB/1fINVxk0A==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.8", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true, + "license": "CC0-1.0", + "optional": true + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-pkg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", + "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", + "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0", + "read-pkg": "^6.0.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-highlight": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-react": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-7.2.0.tgz", + "integrity": "sha512-MHYyCHka+3TtzBMKtcuvVOBAbI1HrfoYA+XH9m7/rlrQQATCPwtJnPdkxKKcIGF8vc9mxqQja9r9f+FHItQeWg==", + "dependencies": { + "@mapbox/hast-util-table-cell-style": "^0.2.0", + "@types/hast": "^2.0.0", + "hast-to-hyperscript": "^10.0.0", + "hast-util-whitespace": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=17" + } + }, + "node_modules/rehype-react/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/rehype-react/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/rehype-react/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/rehype-react/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rehype-react/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-react/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-react/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-react/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.3.tgz", + "integrity": "sha512-bfmJW1dmR2LvaMJuAnE88pZP9DktIFYXazkTfOIKZzi3Knk9lT0roItIA24ydOucI3bV/g/tXBA6hzqq3FV9Ew==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "remark-parse": "^10.0.0", + "remark-stringify": "^10.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-breaks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-newline-to-break": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-cli": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-cli/-/remark-cli-11.0.0.tgz", + "integrity": "sha512-8JEWwArXquRq1/In4Ftz7gSG9Scwb1ijT2/dEuBETW9omqhmMRxcfjZ3iKqrak3BnCJeZSXCdWEmPhFKC8+RUQ==", + "license": "MIT", + "dependencies": { + "remark": "^14.0.0", + "unified-args": "^10.0.0" + }, + "bin": { + "remark": "cli.js" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/remark-lint/-/remark-lint-9.1.2.tgz", + "integrity": "sha512-m9e/aPlh7tsvfJfj8tPxrQzD6oEdb9Foko+Ya/6OwUP9EoGMfehv1Qtv26W1DoH58Wn8rT8CD+KuprTWscMmIA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "remark-message-control": "^7.0.0", + "unified": "^10.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-blockquote-indentation/-/remark-lint-blockquote-indentation-3.1.2.tgz", + "integrity": "sha512-5DOrFsZd5dXqA4p/VZvWSrqIWNFbBXjX7IV/FkVkxlNhNF/0FMf/4v8x1I2W3mzaZ7yDsWS/egpZnmligq1ckQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "pluralize": "^8.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-blockquote-indentation/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-checkbox-character-style/-/remark-lint-checkbox-character-style-4.1.2.tgz", + "integrity": "sha512-5ITz+1cCuJ3Jv/Q7rKgDEucCOnIgjWDnSHPJA1tb4TI/D316h+ALbDhZIpP8gyfAm6sBAh3Pwz9XZJN2uJB5UQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-checkbox-character-style/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-code-block-style/-/remark-lint-code-block-style-3.1.2.tgz", + "integrity": "sha512-3wsWmzzdyEsB9sOzBOf46TSkwwVKXN2JpTEQb6feN0Tl6Vg75F7T9MHqMz7aqk/56bOXSxUzdpXDscGBhziLRA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-code-block-style/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-code-block-style/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-emphasis-marker/-/remark-lint-emphasis-marker-3.1.2.tgz", + "integrity": "sha512-hPZ8vxZrIfxmLA5B66bA8y3PdHjcCQuaLsySIqi5PM2DkpN6a7zAP3v1znyRSaYJ1ANVWcu00/0bNzuUjflGCA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-emphasis-marker/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-emphasis-marker/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-fenced-code-marker/-/remark-lint-fenced-code-marker-3.1.2.tgz", + "integrity": "sha512-6XNqjOuhT+0c7Q/22aCsMz61ne9g8HRpYF79EXQPdbzYa+PcfPXMiQKStONY3PfC8OE2/3WXI2zcs8w9x+8+VQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-fenced-code-marker/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-final-newline": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-final-newline/-/remark-lint-final-newline-2.1.2.tgz", + "integrity": "sha512-K0FdPGPyEB94PwNgopwVJFE8oRWi7IhY2ycXFVAMReI51el7EHB8F1gX14tB6p6zyGy6mUh69bCVU9mMTNeOUg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-final-newline/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-final-newline/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-final-newline/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-final-newline/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-final-newline/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-final-newline/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-final-newline/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-final-newline/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-hard-break-spaces/-/remark-lint-hard-break-spaces-3.1.2.tgz", + "integrity": "sha512-HaW0xsl3TI7VFAqGWWcZtPqyz0NWu19KKjSO7OGFTUJU4S9YiRnhIxmSFM0ZLSsVAynE+dhzVKa8U7dOpWDcOg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-hard-break-spaces/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-style": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-heading-style/-/remark-lint-heading-style-3.1.2.tgz", + "integrity": "sha512-0RkcRPV/H2bPFgeInzBkK1cWUwtFTm83I+Db/Z5tDY02GzKOosHLvxtJyj/1391/opAH1LYbHtHWffir99IUgw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-heading-style": "^2.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-style/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-heading-style/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-heading-style/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-heading-style/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-heading-style/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-style/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-style/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-style/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-style/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-style/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-heading-style/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-link-title-style/-/remark-lint-link-title-style-3.1.2.tgz", + "integrity": "sha512-if4MahYJVvQUWlrXDF8GSv4b9VtLSgMSDHeikQp1/hGYlihLl9uGw3nlL5Lf9DqTN0qaT6RPbXOjuuzHlk38sg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile-location": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-link-title-style/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-link-title-style/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-bullet-indent": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-list-item-bullet-indent/-/remark-lint-list-item-bullet-indent-4.1.2.tgz", + "integrity": "sha512-WgU5nooqIEm6f35opcbHKBzWrdFJA3XcyTfB3nv/v0KX43/h6qFGmmMJ5kEiaFExuQp3dZSdatWuY0YZ9YRbUg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "pluralize": "^8.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-bullet-indent/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-list-item-content-indent/-/remark-lint-list-item-content-indent-3.1.2.tgz", + "integrity": "sha512-TB0pmrWiRaQW80Y/PILFQTnHDghRxXNzMwyawlP+DBF9gNom3pEBmb4ZlGQlN0aa3r8VWeIKdv1ylHrfXE0vqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "pluralize": "^8.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-content-indent/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-list-item-indent/-/remark-lint-list-item-indent-3.1.2.tgz", + "integrity": "sha512-tkrra1pxZVE4OVJGfN435u/v0ljruXU+dHzWiKDYeifquD4aWhJxvSApu7+FbE098D/4usVXgMxwFkNhrpZcSQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "pluralize": "^8.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-list-item-indent/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-list-item-indent/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-blockquote-without-marker/-/remark-lint-no-blockquote-without-marker-5.1.2.tgz", + "integrity": "sha512-QPbqsrt7EfpSWqTkZJ9tepabPIhBDlNqZkuxxMQYD0OQ2N+tHDUq3zE1JxI5ts1V9o/mWApgySocqGd3jlcKmQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile-location": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-blockquote-without-marker/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-duplicate-definitions/-/remark-lint-no-duplicate-definitions-3.1.2.tgz", + "integrity": "sha512-vi0nXA7p+pjQOorZOkr9E+QDhG74JAdbzqglWPrWWNI3z2rUYWYHTNSyWJbwEXaIIcev1ZAw8SCAOis5MNm+pA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-stringify-position": "^3.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-duplicate-definitions/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-heading-content-indent/-/remark-lint-no-heading-content-indent-4.1.2.tgz", + "integrity": "sha512-TTxFsm1f4ZHFxZQCuz7j0QK4RvP6oArTiwazKLr16yaZe1608ypogMek4A30j2xX8WuO9+2uBzLXCY5OBo5x5Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-heading-style": "^2.0.0", + "pluralize": "^8.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-heading-content-indent/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-inline-padding/-/remark-lint-no-inline-padding-4.1.2.tgz", + "integrity": "sha512-dGyhWsiqCZS3Slob0EVBUfsFBbdpMIBCvb56LlCgaHbnLsnNYx8PpF/wA5CgsN8BXIbXfRpyPB5cIJwIq5taYg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-inline-padding/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-inline-padding/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-literal-urls/-/remark-lint-no-literal-urls-3.1.2.tgz", + "integrity": "sha512-4tV9JGLKxAMFSuWDMOqLozkFJ3HyRvhzgrPrxASoziaml23m7UXAozk5dkIrFny1cN2oG988Z8tORxX2FL1Ilw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-literal-urls/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-literal-urls/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-shortcut-reference-image/-/remark-lint-no-shortcut-reference-image-3.1.2.tgz", + "integrity": "sha512-NX4XJFPyDeJJ77pmETxRj4oM/zayf7Lmn/O87HgExBkQIPz2NYbDeKD8QEyliLaV/oKA2rQufpzuFw55xa1Tww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-image/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-shortcut-reference-link/-/remark-lint-no-shortcut-reference-link-3.1.2.tgz", + "integrity": "sha512-/9iPN7FLKaaIzw4tLWKu7Rx0wAP7E2EuzIeentQlkY0rO/mMHipmT3IlgiebsAInKagzTY6TNFoG1rq2VnaCcA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-shortcut-reference-link/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/remark-lint-no-undefined-references/-/remark-lint-no-undefined-references-4.2.1.tgz", + "integrity": "sha512-HdNg5b2KiuNplcuVvRtsrUiROw557kAG1CiZYB7jQrrVWFgd86lKTa3bDiywe+87dGrGmHd3qQ28eZYTuHz2Nw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile-location": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-undefined-references/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark-lint-no-undefined-references/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-undefined-references/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-no-unused-definitions/-/remark-lint-no-unused-definitions-3.1.2.tgz", + "integrity": "sha512-bOcaJAnjKxT3kASFquUA3fO9xem9wZhVqt8TbqjA84+G4n40qjaLXDs/4vq73aMsSde73K0f3j1u0pMe7et8yQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-no-unused-definitions/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-ordered-list-marker-style/-/remark-lint-ordered-list-marker-style-3.1.2.tgz", + "integrity": "sha512-62iVE/YQsA0Azaqt8yAJWPplWLS47kDLjXeC2PlRIAzCqbNt9qH3HId8vZ15QTSrp8rHmJwrCMdcqV6AZUi7gQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-ordered-list-marker-style/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-rule-style/-/remark-lint-rule-style-3.1.2.tgz", + "integrity": "sha512-0CsX2XcX9pIhAP5N7Y8mhYXp3/Ld+NvxXY1p0LHAq0NZu17UsZLuegvx/s25uFbQs08DcmSqyKnepU9qGGqmTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-rule-style/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-rule-style/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-rule-style/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-rule-style/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-rule-style/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/remark-lint-strong-marker/-/remark-lint-strong-marker-3.1.2.tgz", + "integrity": "sha512-U/g4wngmiI0Q6WBRQG6pZxnDS33Wt/0QYA3+KNFBDykoi1vXsDEorIqy3dEag9z6XHwcMvFDsff6VRUhaOJWQg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-strong-marker/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-strong-marker/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/remark-lint-table-cell-padding/-/remark-lint-table-cell-padding-4.1.3.tgz", + "integrity": "sha512-N9xtnS6MG/H3srAMjqqaF26A7socr87pIgt64dr5rxoSbDRWRPChGQ8y7wKyV8VeyRNF37e3E5KB3bQVqjSYaQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unified": "^10.0.0", + "unified-lint-rule": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint-table-cell-padding/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint-table-cell-padding/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-lint/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-lint/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-lint/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-lint/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-lint/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-message-control": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/remark-message-control/-/remark-message-control-7.1.1.tgz", + "integrity": "sha512-xKRWl1NTBOKed0oEtCd8BUfH5m4s8WXxFFSoo7uUwx6GW/qdCy4zov5LfPyw7emantDmhfWn5PdIZgcbVcWMDQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-comment-marker": "^2.0.0", + "unified": "^10.0.0", + "unified-message-control": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-message-control/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-message-control/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-message-control/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-message-control/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-message-control/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-message-control/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-message-control/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-message-control/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-consistent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/remark-preset-lint-consistent/-/remark-preset-lint-consistent-5.1.2.tgz", + "integrity": "sha512-RQrWBFmyIkKfXtp9P1Fui7UbGSfXth9nuvRJUVnO0vfevBJe02iyMZWPokXSwkDOI/cM539wj0i3vrQupz+v5A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "remark-lint": "^9.0.0", + "remark-lint-blockquote-indentation": "^3.0.0", + "remark-lint-checkbox-character-style": "^4.0.0", + "remark-lint-code-block-style": "^3.0.0", + "remark-lint-emphasis-marker": "^3.0.0", + "remark-lint-fenced-code-marker": "^3.0.0", + "remark-lint-heading-style": "^3.0.0", + "remark-lint-link-title-style": "^3.0.0", + "remark-lint-list-item-content-indent": "^3.0.0", + "remark-lint-ordered-list-marker-style": "^3.0.0", + "remark-lint-rule-style": "^3.0.0", + "remark-lint-strong-marker": "^3.0.0", + "remark-lint-table-cell-padding": "^4.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-consistent/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-preset-lint-consistent/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-preset-lint-consistent/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-preset-lint-consistent/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-preset-lint-consistent/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-consistent/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-consistent/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-consistent/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-recommended": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/remark-preset-lint-recommended/-/remark-preset-lint-recommended-6.1.3.tgz", + "integrity": "sha512-DGjbeP2TsFmQeJflUiIvJWAOs1PxJt7SG3WQyMxOppkRr/up+mxWVkuv+6AUuaR0EsuaaFGz7WmZM5TrSSFWJw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "remark-lint": "^9.0.0", + "remark-lint-final-newline": "^2.0.0", + "remark-lint-hard-break-spaces": "^3.0.0", + "remark-lint-list-item-bullet-indent": "^4.0.0", + "remark-lint-list-item-indent": "^3.0.0", + "remark-lint-no-blockquote-without-marker": "^5.0.0", + "remark-lint-no-duplicate-definitions": "^3.0.0", + "remark-lint-no-heading-content-indent": "^4.0.0", + "remark-lint-no-inline-padding": "^4.0.0", + "remark-lint-no-literal-urls": "^3.0.0", + "remark-lint-no-shortcut-reference-image": "^3.0.0", + "remark-lint-no-shortcut-reference-link": "^3.0.0", + "remark-lint-no-undefined-references": "^4.0.0", + "remark-lint-no-unused-definitions": "^3.0.0", + "remark-lint-ordered-list-marker-style": "^3.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-recommended/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark-preset-lint-recommended/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-preset-lint-recommended/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark-preset-lint-recommended/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark-preset-lint-recommended/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-recommended/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-recommended/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-preset-lint-recommended/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-unlink-protocols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/remark-unlink-protocols/-/remark-unlink-protocols-1.0.0.tgz", + "integrity": "sha512-5j/F28jhFmxeyz8nuJYYIWdR4nNpKWZ8A+tVwnK/0pq7Rjue33CINEYSckSq2PZvedhKUwbn08qyiuGoPLBung==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-squeeze-paragraphs": "^6.0.0", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/remark/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/remark/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/remark/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/remark/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/remark/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/remark/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/remark/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/remark/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark/node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/remark-stringify": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-10.0.3.tgz", + "integrity": "sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/reserved": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reserved/-/reserved-0.1.2.tgz", + "integrity": "sha512-/qO54MWj5L8WCBP9/UNe2iefJc+L9yETbH32xO/ft/EYPOTCR5k+azvDUgdCOKwZH8hXwPd0b8XBL78Nn2U69g==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ripemd160/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ripemd160/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ripemd160/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", + "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/sass": { + "version": "1.101.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", + "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", + "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/schemes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/schemes/-/schemes-1.4.0.tgz", + "integrity": "sha512-ImFy9FbCsQlVgnE3TCWmLPCFnVzx0lHL/l+umHplDqAKd0dzFpnS6lFZIpagBlYhKwzVmlV36ec0Y1XTu8JBAQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "extend": "^3.0.0" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-eval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", + "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", + "dev": true, + "dependencies": { + "jsep": "^1.3.6" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/smtp-address-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.1.0.tgz", + "integrity": "sha512-Gz11jbNU0plrReU9Sj7fmshSBxxJ9ShdD2q4ktHIHo/rpTH6lFyQoYHYKINPJtPe8aHFnsbtW46Ls0tCCBsIZg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "nearley": "^2.20.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sortablejs": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz", + "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/splitpanes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-2.4.1.tgz", + "integrity": "sha512-kpEo1WuMXuc6QfdQdO2V/fl/trONlkUKp+pputsLTiW9RMtwEvjb4/aYGm2m3+KAzjmb+zLwr4A4SYZu74+pgQ==" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stacktracey": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "dev": true, + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "dependencies": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/striptags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz", + "integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==" + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true + }, + "node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/stylelint": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.11.0.tgz", + "integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^2.3.1", + "@csstools/css-tokenizer": "^2.2.0", + "@csstools/media-query-list-parser": "^2.1.4", + "@csstools/selector-specificity": "^3.0.0", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^8.2.0", + "css-functions-list": "^3.2.1", + "css-tree": "^2.3.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.1", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^7.0.0", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^5.2.4", + "import-lazy": "^4.0.0", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.29.0", + "mathml-tag-names": "^2.1.3", + "meow": "^10.1.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.28", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.0.13", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "style-search": "^0.1.0", + "supports-hyperlinks": "^3.0.0", + "svg-tags": "^1.0.0", + "table": "^6.8.1", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + } + }, + "node_modules/stylelint-config-html": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-html/-/stylelint-config-html-1.1.0.tgz", + "integrity": "sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12 || >=14" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "postcss-html": "^1.0.0", + "stylelint": ">=14.0.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz", + "integrity": "sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "stylelint": "^15.10.0" + } + }, + "node_modules/stylelint-config-recommended-scss": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-13.1.0.tgz", + "integrity": "sha512-8L5nDfd+YH6AOoBGKmhH8pLWF1dpfY816JtGMePcBqqSsLU+Ysawx44fQSlMOJ2xTfI9yTGpup5JU77c17w1Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-scss": "^4.0.9", + "stylelint-config-recommended": "^13.0.0", + "stylelint-scss": "^5.3.0" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^15.10.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + } + } + }, + "node_modules/stylelint-config-recommended-vue": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-vue/-/stylelint-config-recommended-vue-1.6.1.tgz", + "integrity": "sha512-lLW7hTIMBiTfjenGuDq2kyHA6fBWd/+Df7MO4/AWOxiFeXP9clbpKgg27kHfwA3H7UNMGC7aeP3mNlZB5LMmEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5", + "stylelint-config-html": ">=1.0.0", + "stylelint-config-recommended": ">=6.0.0" + }, + "engines": { + "node": "^12 || >=14" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "postcss-html": "^1.0.0", + "stylelint": ">=14.0.0" + } + }, + "node_modules/stylelint-config-recommended-vue/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stylelint-scss": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-5.3.2.tgz", + "integrity": "sha512-4LzLaayFhFyneJwLo0IUa8knuIvj+zF0vBFueQs4e3tEaAMIQX8q5th8ziKkgOavr6y/y9yoBe+RXN/edwLzsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "known-css-properties": "^0.29.0", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-selector-parser": "^6.0.13", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "stylelint": "^14.5.1 || ^15.0.0" + } + }, + "node_modules/stylelint-webpack-plugin": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/stylelint-webpack-plugin/-/stylelint-webpack-plugin-4.1.1.tgz", + "integrity": "sha512-yOyd2AfrxfawxKDememazGVJX2vMq9o11E6HvBu4+SKvgK3ZulkjpYdI1muBTxItwoxH2UmfIZzQM+/M5V3kTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^11.1.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "stylelint": "^13.0.0 || ^14.0.0 || ^15.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-7.0.2.tgz", + "integrity": "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stylelint/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/table": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", + "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-vfile": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-7.2.4.tgz", + "integrity": "sha512-2eQ+rJ2qGbyw3senPI0qjuM7aut8IYXK6AEoOWb+fJx/mQYzviTckm1wDjq91QYHAPBTYzmdJXxMFA6Mk14mdw==", + "license": "MIT", + "dependencies": { + "is-buffer": "^2.0.0", + "vfile": "^5.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/to-vfile/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/to-vfile/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-vfile/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/to-vfile/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/to-vfile/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tributejs": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/tributejs/-/tributejs-5.1.3.tgz", + "integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trim-newlines": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", + "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-event-target": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/typescript-event-target/-/typescript-event-target-1.1.2.tgz", + "integrity": "sha512-TvkrTUpv7gCPlcnSoEwUVUBwsdheKm+HF5u2tPAKubkIGMfovdSizCTaZRY/NhR8+Ijy8iZZUapbVQAsNrkFrw==", + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-args": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/unified-args/-/unified-args-10.0.0.tgz", + "integrity": "sha512-PqsqxwkXpGSLiMkbjNnKU33Ffm6gso6rAvz1TlBGzMBx3gpx7ewIhViBX8HEWmy0v7pebA5PM6RkRWWaYmtfYw==", + "license": "MIT", + "dependencies": { + "@types/text-table": "^0.2.0", + "camelcase": "^7.0.0", + "chalk": "^5.0.0", + "chokidar": "^3.0.0", + "fault": "^2.0.0", + "json5": "^2.0.0", + "minimist": "^1.0.0", + "text-table": "^0.2.0", + "unified-engine": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-args/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified-args/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/unified-engine": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/unified-engine/-/unified-engine-10.1.0.tgz", + "integrity": "sha512-5+JDIs4hqKfHnJcVCxTid1yBoI/++FfF/1PFdSMpaftZZZY+qg2JFruRbf7PaIwa9KgLotXQV3gSjtY0IdcFGQ==", + "license": "MIT", + "dependencies": { + "@types/concat-stream": "^2.0.0", + "@types/debug": "^4.0.0", + "@types/is-empty": "^1.0.0", + "@types/node": "^18.0.0", + "@types/unist": "^2.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.0.0", + "fault": "^2.0.0", + "glob": "^8.0.0", + "ignore": "^5.0.0", + "is-buffer": "^2.0.0", + "is-empty": "^1.0.0", + "is-plain-obj": "^4.0.0", + "load-plugin": "^5.0.0", + "parse-json": "^6.0.0", + "to-vfile": "^7.0.0", + "trough": "^2.0.0", + "unist-util-inspect": "^7.0.0", + "vfile-message": "^3.0.0", + "vfile-reporter": "^7.0.0", + "vfile-statistics": "^2.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-engine/node_modules/@types/node": { + "version": "18.19.79", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.79.tgz", + "integrity": "sha512-90K8Oayimbctc5zTPHPfZloc/lGVs7f3phUAAMcTgEPtg8kKquGZDERC8K4vkBYkQQh48msiYUslYtxTWvqcAg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/unified-engine/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/unified-engine/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/unified-engine/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified-engine/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified-engine/node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/unified-engine/node_modules/parse-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-6.0.2.tgz", + "integrity": "sha512-SA5aMiaIjXkAiBrW/yPgLgQAQg42f7K3ACO+2l/zOvtQBwX58DMUsFJXelW2fx3yMBmWOVkR6j1MGsdSbCA4UA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^2.3.1", + "lines-and-columns": "^2.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified-engine/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unified-engine/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-engine/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-lint-rule": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unified-lint-rule/-/unified-lint-rule-2.1.2.tgz", + "integrity": "sha512-JWudPtRN7TLFHVLEVZ+Rm8FUb6kCAtHxEXFgBGDxRSdNMnGyTU5zyYvduHSF/liExlFB3vdFvsAHnNVE/UjAwA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "trough": "^2.0.0", + "unified": "^10.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-lint-rule/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/unified-lint-rule/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified-lint-rule/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified-lint-rule/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-lint-rule/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-lint-rule/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-lint-rule/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unified-message-control/-/unified-message-control-4.0.0.tgz", + "integrity": "sha512-1b92N+VkPHftOsvXNOtkJm4wHlr+UDmTBF2dUzepn40oy9NxanJ9xS1RwUBTjXJwqr2K0kMbEyv1Krdsho7+Iw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit": "^3.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/unified-message-control/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified-message-control/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control/node_modules/unist-util-visit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz", + "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control/node_modules/unist-util-visit-parents": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz", + "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified-message-control/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unist-builder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", + "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-inspect": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/unist-util-inspect/-/unist-util-inspect-7.0.2.tgz", + "integrity": "sha512-Op0XnmHUl6C2zo/yJCwhXQSm/SmW22eDZdWP2qdf4WpGrgO1ZxFodq+5zFyeRGasFjJotAnLgfuD1jkcKqiH1Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-inspect/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "dev": true + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uvu/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", + "dev": true, + "dependencies": { + "builtins": "^1.0.3" + } + }, + "node_modules/validate-npm-package-name/node_modules/builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==", + "dev": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", + "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/vfile-location/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/vfile-location/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-reporter": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/vfile-reporter/-/vfile-reporter-7.0.5.tgz", + "integrity": "sha512-NdWWXkv6gcd7AZMvDomlQbK3MqFWL1RlGzMn++/O2TI+68+nqxCPTvLugdOtfSzXmjh+xUyhp07HhlrbJjT+mw==", + "license": "MIT", + "dependencies": { + "@types/supports-color": "^8.0.0", + "string-width": "^5.0.0", + "supports-color": "^9.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile": "^5.0.0", + "vfile-message": "^3.0.0", + "vfile-sort": "^3.0.0", + "vfile-statistics": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-reporter/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/vfile-reporter/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/vfile-reporter/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/vfile-reporter/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/vfile-reporter/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vfile-reporter/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/vfile-reporter/node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/vfile-reporter/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-reporter/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-reporter/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-sort": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vfile-sort/-/vfile-sort-3.0.1.tgz", + "integrity": "sha512-1os1733XY6y0D5x0ugqSeaVJm9lYgj0j5qdcZQFyxlZOSy1jYarL77lLyb5gK4Wqr1d5OxmuyflSO3zKyFnTFw==", + "license": "MIT", + "dependencies": { + "vfile": "^5.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-sort/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/vfile-sort/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/vfile-sort/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-sort/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-sort/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-statistics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vfile-statistics/-/vfile-statistics-2.0.1.tgz", + "integrity": "sha512-W6dkECZmP32EG/l+dp2jCLdYzmnDBIw6jwiLZSER81oR5AHRcVqL+k3Z+pfH1R73le6ayDkJRMk0sutj1bMVeg==", + "license": "MIT", + "dependencies": { + "vfile": "^5.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-statistics/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/vfile-statistics/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/vfile-statistics/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-statistics/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-statistics/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/vue": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "dependencies": { + "@vue/compiler-sfc": "2.7.16", + "csstype": "^3.1.0" + } + }, + "node_modules/vue-apexcharts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vue-apexcharts/-/vue-apexcharts-1.7.0.tgz", + "integrity": "sha512-QMpvBllJ1XvFsK4dwcbyxKalVpHfJnoqsNWszY55HJk/Sn7WP1f5YUv4JIzugqu4GTQB6gLcCVwwPDQFtwr0oQ==", + "license": "MIT", + "peerDependencies": { + "apexcharts": ">=4.0.0", + "vue": "^2.5.17" + } + }, + "node_modules/vue-codemirror6": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.5.1.tgz", + "integrity": "sha512-Ey1uQ5ypB2UregJWhWwjRo/YvElzVUlQWBeCHntoLM361bb6iTMS5GTgni+IHdl5D7rEpRRCpAvYRZQidxe5Ag==", + "license": "MIT", + "dependencies": { + "vue-demi": "latest" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0", + "pnpm": ">=10.3.0" + }, + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "codemirror": "^6.0.0", + "style-mod": "^4.0.0", + "vue": "^2.7.14 || ^3.3.4" + } + }, + "node_modules/vue-color": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.8.2.tgz", + "integrity": "sha512-1qmsxl5GiIjx/jApBbTGr2r4bN/7WRKUTl3tc53vkXb9Ua0rZmiqsdq6VdG1e7dVNTLJahdsRGWcjeU2+98+NA==", + "license": "MIT", + "dependencies": { + "clamp": "^1.0.1", + "lodash.throttle": "^4.0.0", + "material-colors": "^1.0.0", + "tinycolor2": "^1.1.2" + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.29.tgz", + "integrity": "sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==", + "dev": true + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-draggable-plus": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/vue-draggable-plus/-/vue-draggable-plus-0.2.7.tgz", + "integrity": "sha512-asNQ4OxTQtN91+m45lh8QEwscXhVveaLvPAzaRXHD8JIk1+SOg7NWPQ9EmUTeNhSP3SGkJ2Agvp5WRDt7vXM8w==", + "license": "MIT", + "peerDependencies": { + "@types/sortablejs": "^1.15.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-frag": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/vue-frag/-/vue-frag-1.4.3.tgz", + "integrity": "sha512-pQZj03f/j9LRhzz9vKaXTCXUHVYHuAXicshFv76VFqwz4MG3bcb+sPZMAbd0wmw7THjkrTPuoM0EG9TbG8CgMQ==", + "funding": { + "url": "https://github.com/privatenumber/vue-frag?sponsor=1" + }, + "peerDependencies": { + "vue": "^2.6.0" + } + }, + "node_modules/vue-functional-data-merge": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz", + "integrity": "sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA==" + }, + "node_modules/vue-hot-reload-api": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", + "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==" + }, + "node_modules/vue-loader": { + "version": "15.11.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.11.1.tgz", + "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==", + "dependencies": { + "@vue/component-compiler-utils": "^3.1.0", + "hash-sum": "^1.0.2", + "loader-utils": "^1.1.0", + "vue-hot-reload-api": "^2.3.0", + "vue-style-loader": "^4.1.0" + }, + "peerDependencies": { + "css-loader": "*", + "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "cache-loader": { + "optional": true + }, + "prettier": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/vue-loading-overlay": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/vue-loading-overlay/-/vue-loading-overlay-6.0.6.tgz", + "integrity": "sha512-ZPrWawjCoNKGbCG9z4nePgbs/K9KXPa1j1oAJXP6T8FQho3NO+/chhjx4MLYFzfpwr+xkiQ8SNrV1kUG1bZPAw==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-material-design-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.3.0.tgz", + "integrity": "sha512-wnbRh+48RwX/Gt+iqwCSdWpm0hPBwwv9F7MSouUzZ2PsphYVMJB9KkG9iGs+tgBiT57ZiurFEK07Y/rFKx+Ekg==" + }, + "node_modules/vue-resize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz", + "integrity": "sha512-z5M7lJs0QluJnaoMFTIeGx6dIkYxOwHThlZDeQnWZBizKblb99GSejPnK37ZbNE/rVwDcYcHY+Io+AxdpY952w==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "vue": "^2.6.0" + } + }, + "node_modules/vue-router": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", + "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==", + "license": "MIT" + }, + "node_modules/vue-style-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", + "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", + "dependencies": { + "hash-sum": "^1.0.2", + "loader-utils": "^1.0.2" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-template-es2015-compiler": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", + "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==" + }, + "node_modules/vue2-datepicker": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-3.11.1.tgz", + "integrity": "sha512-6PU/+pnp2mgZAfnSXmbdwj9516XsEvTiw61Q5SNrvvdy8W/FCxk1GAe9UZn/m9YfS5A47yK6XkcjMHbp7aFApA==", + "dependencies": { + "date-format-parse": "^0.2.7" + }, + "peerDependencies": { + "vue": "^2.5.0" + } + }, + "node_modules/vuedraggable": { + "version": "2.24.3", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz", + "integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==", + "license": "MIT", + "dependencies": { + "sortablejs": "1.10.2" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walk-up-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", + "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", + "license": "ISC" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webdav": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.9.0.tgz", + "integrity": "sha512-OMJ6wtK1WvCO++aOLoQgE96S8KT4e5aaClWHmHXfFU369r4eyELN569B7EqT4OOUb99mmO58GkyuiCv/Ag6J0Q==", + "license": "MIT", + "dependencies": { + "@buttercup/fetch": "^0.2.1", + "base-64": "^1.0.0", + "byte-length": "^1.0.2", + "entities": "^6.0.1", + "fast-xml-parser": "^5.3.4", + "hot-patcher": "^2.0.1", + "layerr": "^3.0.0", + "md5": "^2.3.0", + "minimatch": "^9.0.5", + "nested-property": "^4.0.0", + "node-fetch": "^3.3.2", + "path-posix": "^1.0.0", + "url-join": "^5.0.0", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webdav/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/webdav/node_modules/fast-xml-parser": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/webdav/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/webdav/node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.108.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.4.tgz", + "integrity": "sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.2", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlbuilder2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/xmlbuilder2/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/xmlbuilder2/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } } diff --git a/package.json b/package.json index 3c2d45ca19..b1961b14b9 100644 --- a/package.json +++ b/package.json @@ -1,159 +1,170 @@ { - "name": "openregister", - "version": "1.0.0", - "license": "EUPL-1.2", - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - }, - "scripts": { - "build": "webpack --config webpack.config.js --progress --mode production", - "dev": "NODE_ENV=development webpack --config webpack.config.js --progress", - "watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch", - "lint": "eslint src", - "lint-fix": "npm run lint -- --fix", - "test": "jest --silent", - "test-coverage": "jest --silent --coverage", - "stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css", - "validate-oas": "spectral lint oas-*.json --fail-severity error", - "download-oas": "scripts/download-oas.sh", - "test:e2e": "playwright test", - "test:e2e:docs": "playwright test --project docs-capture", - "test:e2e:integrations": "playwright test tests/e2e/integration-mount.spec.ts tests/e2e/integration-registry.spec.ts --project chromium", - "test:e2e:install": "playwright install chromium", - "check:json-strict": "node tests/validate-json-strict.js", - "check:manifest": "node tests/validate-manifest.js", - "check:register": "node tests/validate-register.js", - "check:specs": "npm run check:json-strict && npm run check:manifest && npm run check:register" - }, - "browserslist": [ - "extends @nextcloud/browserslist-config" - ], - "sideEffects": true, - "peerDependencies": { - "vue": "^2.6 || ^3.0", - "vue-frag": "^1.0.0" - }, - "peerDependenciesMeta": { - "vue-frag": { - "optional": true - } - }, - "dependencies": { - "@codemirror/lang-json": "^6.0.1", - "@conduction/nextcloud-vue": "^1.0.0-beta.103", - "@fortawesome/fontawesome-svg-core": "^6.5.2", - "@fortawesome/free-solid-svg-icons": "^6.5.2", - "@nextcloud/axios": "~2.5.1", - "@nextcloud/dialogs": "^6.1.1", - "@nextcloud/initial-state": "^2.2.0", - "@nextcloud/l10n": "^3.2.0", - "@nextcloud/router": "^3.0.0", - "@nextcloud/vue": "^8.39.0", - "@vueuse/core": "^10.7.2", - "apexcharts": "^4.0.0", - "axios": "^1.7.3", - "bootstrap": "^5.3.2", - "bootstrap-vue": "^2.23.1", - "css-loader": "^6.8.1", - "marked": "^16.4.0", - "pinia": "^2.1.7", - "remark-cli": "^11.0.0", - "remark-lint-list-item-indent": "^3.1.1", - "remark-preset-lint-consistent": "^5.1.1", - "remark-preset-lint-recommended": "^6.1.2", - "style-loader": "^3.3.3", - "vue": "^2.7.16", - "vue-apexcharts": "^1.7.0", - "vue-codemirror6": "^1.1.5", - "vue-draggable-plus": "^0.2.6", - "vue-frag": "^1.4.3", - "vue-loader": "^15.11.1", - "vue-loading-overlay": "^6.0.3", - "vue-material-design-icons": "^5.2.0", - "vue-template-compiler": "^2.7.16", - "zod": "^3.22.4" - }, - "devDependencies": { - "@babel/core": "^7.23.9", - "@babel/eslint-parser": "^7.28.6", - "@babel/plugin-transform-typescript": "^7.26.8", - "@babel/preset-env": "^7.23.9", - "@babel/preset-typescript": "^7.26.0", - "@babel/traverse": "^7.23.9", - "@cyclonedx/cyclonedx-npm": "^4.2.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.39.1", - "@nextcloud/browserslist-config": "^2.3.0", - "@nextcloud/eslint-config": "^8.4.1", - "@nextcloud/eslint-plugin": "^2.2.1", - "@nextcloud/stylelint-config": "^2.4.0", - "@nextcloud/webpack-vue-config": "^5.5.0", - "@pinia/testing": "^0.1.3", - "@playwright/test": "^1.49.0", - "@stoplight/spectral-cli": "^6.15.0", - "@types/jest": "^29.5.12", - "@types/node": "^20.17.23", - "@vue/eslint-config-typescript": "^13.0.0", - "@vue/test-utils": "^2.4.4", - "@vue/vue2-jest": "^29.2.6", - "babel-jest": "^29.7.0", - "babel-loader": "^9.1.3", - "eslint": "^8.56.0", - "eslint-config-standard": "^17.1.0", - "eslint-import-resolver-alias": "^1.1.2", - "eslint-import-resolver-exports": "^1.0.0-beta.5", - "eslint-import-resolver-typescript": "^3.10.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsdoc": "^46.10.1", - "eslint-plugin-n": "^16.6.2", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-vue": "^9.21.1", - "eslint-webpack-plugin": "^4.0.1", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-transform-stub": "^2.0.0", - "postcss": "^8.4.31", - "postcss-html": "^1.8.1", - "stylelint": "^15.11.0", - "stylelint-config-recommended-scss": "^13.1.0", - "stylelint-config-recommended-vue": "^1.6.1", - "stylelint-webpack-plugin": "^4.1.1", - "ts-jest": "^29.1.2", - "ts-loader": "^9.5.1", - "typescript": "^5.8.2", - "vue-router": "^3.6.5" - }, - "overrides": { - "vue": "^2.7.16", - "@nextcloud/vue": "^8.39.0", - "@nextcloud/axios": "~2.5.1", - "stylelint": "^15.11.0", - "eslint": "^8.56.0", - "postcss": "^8.4.31", - "json5": "^2.2.3", - "@babel/traverse": "^7.23.9", - "babel-jest": "^29.7.0", - "vue-template-compiler": "^2.7.16", - "@vue/test-utils": "^2.4.4", - "bootstrap": "^5.3.2", - "vue-demi": "^0.14.6", - "minimatch": ">=3.1.5", - "rollup": ">=2.80.0", - "dompurify": ">=3.4.0", - "follow-redirects": ">=1.16.0", - "lodash": ">=4.18.1", - "@conduction/nextcloud-vue": { - "@vueuse/core": "^14.0.0" - } - }, - "resolutions": { - "postcss": "^8.4.31", - "json5": "^2.2.3", - "@babel/traverse": "^7.23.9", - "vue-template-compiler": "^2.7.16", - "bootstrap": "^5.3.2", - "vue-demi": "^0.14.6" - } + "name": "openregister", + "version": "1.0.0", + "license": "EUPL-1.2", + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + }, + "scripts": { + "prebuild": "node scripts/build-features-manifest.js", + "build": "webpack --config webpack.config.js --progress --mode production", + "dev": "NODE_ENV=development webpack --config webpack.config.js --progress", + "watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch", + "lint": "eslint src", + "lint-fix": "npm run lint -- --fix", + "test": "jest --silent", + "test-coverage": "jest --silent --coverage", + "test:coverage-summary": "jest --silent --coverage --coverageReporters=json-summary --coverageReporters=text-summary --coverageDirectory=coverage-frontend", + "test:coverage-ratchet": "npm run test:coverage-summary && bash tests/coverage-ratchet.sh vitest coverage-frontend/coverage-summary.json", + "stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css", + "validate-oas": "spectral lint oas-*.json --fail-severity error", + "download-oas": "scripts/download-oas.sh", + "test:e2e": "playwright test", + "test:e2e:docs": "playwright test --project docs-capture", + "test:e2e:integrations": "playwright test tests/e2e/integration-mount.spec.ts tests/e2e/integration-registry.spec.ts --project chromium", + "test:e2e:install": "playwright install chromium", + "test:l10n": "node tests/l10n/check-l10n.js", + "test:l10n:write": "node tests/l10n/check-l10n.js --write", + "check:json-strict": "node tests/validate-json-strict.js", + "check:manifest": "node tests/validate-manifest.js", + "check:register": "node tests/validate-register.js", + "check:specs": "npm run check:json-strict && npm run check:manifest && npm run check:register", + "manifest:build": "node scripts/build-features-manifest.js", + "manifest:check": "node scripts/build-features-manifest.js --check" + }, + "browserslist": [ + "extends @nextcloud/browserslist-config" + ], + "sideEffects": true, + "peerDependencies": { + "vue": "^2.6 || ^3.0", + "vue-frag": "^1.0.0" + }, + "peerDependenciesMeta": { + "vue-frag": { + "optional": true + } + }, + "dependencies": { + "@codemirror/lang-json": "^6.0.1", + "@conduction/nextcloud-vue": "^1.0.0-beta.213", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@nextcloud/axios": "~2.5.1", + "@nextcloud/dialogs": "^6.1.1", + "@nextcloud/initial-state": "^2.2.0", + "@nextcloud/l10n": "^3.2.0", + "@nextcloud/router": "^3.0.0", + "@nextcloud/vue": "^8.39.0", + "@vueuse/core": "^10.7.2", + "apexcharts": "^4.0.0", + "axios": "^1.7.3", + "bootstrap": "^5.3.2", + "bootstrap-vue": "^2.23.1", + "css-loader": "^6.8.1", + "dexie": "^4.0.8", + "dompurify": "^3.4.0", + "marked": "^16.4.0", + "pinia": "^2.1.7", + "remark-cli": "^11.0.0", + "remark-lint-list-item-indent": "^3.1.1", + "remark-preset-lint-consistent": "^5.1.1", + "remark-preset-lint-recommended": "^6.1.2", + "style-loader": "^3.3.3", + "vue": "^2.6 || ^3.0", + "vue-apexcharts": "^1.7.0", + "vue-codemirror6": "^1.1.5", + "vue-draggable-plus": "^0.2.6", + "vue-frag": "^1.0.0", + "vue-loader": "^15.11.1", + "vue-loading-overlay": "^6.0.3", + "vue-material-design-icons": "^5.2.0", + "vue-template-compiler": "^2.7.16", + "zod": "^3.22.4" + }, + "devDependencies": { + "@babel/core": "^7.23.9", + "@babel/eslint-parser": "^7.28.6", + "@babel/plugin-transform-typescript": "^7.26.8", + "@babel/preset-env": "^7.23.9", + "@babel/preset-typescript": "^7.26.0", + "@babel/traverse": "^7.23.9", + "@cyclonedx/cyclonedx-npm": "^4.2.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.39.1", + "@nextcloud/browserslist-config": "^2.3.0", + "@nextcloud/eslint-config": "^8.4.1", + "@nextcloud/eslint-plugin": "^2.2.1", + "@nextcloud/stylelint-config": "^2.4.0", + "@nextcloud/webpack-vue-config": "^5.5.0", + "@pinia/testing": "^0.1.3", + "@playwright/test": "^1.49.0", + "@stoplight/spectral-cli": "^6.15.0", + "@types/jest": "^29.5.12", + "@types/node": "^20.17.23", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/test-utils": "^2.4.4", + "@vue/vue2-jest": "^29.2.6", + "babel-jest": "^29.7.0", + "babel-loader": "^9.1.3", + "esbuild": "^0.28.0", + "eslint": "^8.56.0", + "eslint-config-standard": "^17.1.0", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-import-resolver-exports": "^1.0.0-beta.5", + "eslint-import-resolver-typescript": "^3.10.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsdoc": "^46.10.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-vue": "^9.21.1", + "eslint-webpack-plugin": "^4.0.1", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-transform-stub": "^2.0.0", + "postcss": "^8.4.31", + "postcss-html": "^1.8.1", + "stylelint": "^15.11.0", + "stylelint-config-recommended-scss": "^13.1.0", + "stylelint-config-recommended-vue": "^1.6.1", + "stylelint-webpack-plugin": "^4.1.1", + "terser-webpack-plugin": "^5.6.0", + "ts-jest": "^29.1.2", + "ts-loader": "^9.5.1", + "typescript": "^5.8.2", + "vue-router": "^3.6.5" + }, + "overrides": { + "vue": "^2.7.16", + "@nextcloud/vue": "^8.39.0", + "@nextcloud/axios": "~2.5.1", + "stylelint": "^15.11.0", + "eslint": "^8.56.0", + "postcss": "^8.4.31", + "json5": "^2.2.3", + "@babel/traverse": "^7.23.9", + "babel-jest": "^29.7.0", + "vue-template-compiler": "^2.7.16", + "@vue/test-utils": "^2.4.4", + "bootstrap": "^5.3.2", + "vue-demi": "^0.14.6", + "minimatch": ">=3.1.5", + "rollup": ">=2.80.0", + "dompurify": "$dompurify", + "follow-redirects": ">=1.16.0", + "lodash": ">=4.18.1", + "@conduction/nextcloud-vue": { + "@vueuse/core": "^10.0.0" + } + }, + "resolutions": { + "postcss": "^8.4.31", + "json5": "^2.2.3", + "@babel/traverse": "^7.23.9", + "vue-template-compiler": "^2.7.16", + "bootstrap": "^5.3.2", + "vue-demi": "^0.14.6" + } } diff --git a/patches/llphant-ollama-usage-capture.patch b/patches/llphant-ollama-usage-capture.patch new file mode 100644 index 0000000000..dd7392b4b2 --- /dev/null +++ b/patches/llphant-ollama-usage-capture.patch @@ -0,0 +1,31 @@ +--- a/src/Chat/OllamaChat.php ++++ b/src/Chat/OllamaChat.php +@@ -43,6 +43,14 @@ + /** @var CalledFunction[] */ + public array $functionsCalled = []; + ++ /** ++ * Token/latency usage from the last generateChat round-trip, accumulated across ++ * tool-call recursions. Read by OpenRegister to record per-run cost (run-analytics). ++ * ++ * @var array{promptTokens:int, completionTokens:int, totalDurationMs:int} ++ */ ++ public array $lastUsage = ['promptTokens' => 0, 'completionTokens' => 0, 'totalDurationMs' => 0]; ++ + public function __construct(protected OllamaConfig $config, ?LoggerInterface $logger = null) + { + if (! isset($config->model)) { +@@ -174,6 +182,13 @@ + $contents = $response->getBody()->getContents(); + $json = Utility::decodeJson($contents); + ++ // Accumulate token/latency usage across the tool-call recursion so OpenRegister can ++ // record per-run cost (run-analytics). Ollama returns prompt_eval_count/eval_count ++ // (tokens) and total_duration (ns). Fields absent on some models are treated as 0. ++ $this->lastUsage['promptTokens'] += (int) ($json['prompt_eval_count'] ?? 0); ++ $this->lastUsage['completionTokens'] += (int) ($json['eval_count'] ?? 0); ++ $this->lastUsage['totalDurationMs'] += (int) (((int) ($json['total_duration'] ?? 0)) / 1000000); ++ + $message = $json['message']; + + /** @var Message[] $toolsOutput */ diff --git a/patches/phpspreadsheet-zipstream3-prefer.patch b/patches/phpspreadsheet-zipstream3-prefer.patch new file mode 100644 index 0000000000..fc733177fb --- /dev/null +++ b/patches/phpspreadsheet-zipstream3-prefer.patch @@ -0,0 +1,23 @@ +--- a/src/PhpSpreadsheet/Writer/ZipStream0.php ++++ b/src/PhpSpreadsheet/Writer/ZipStream0.php +@@ -10,8 +10,19 @@ class ZipStream0 + /** + * @param resource $fileHandle + */ + public static function newZipStream($fileHandle): ZipStream + { +- return class_exists(Archive::class) ? ZipStream2::newZipStream($fileHandle) : ZipStream3::newZipStream($fileHandle); ++ // Prefer the v3 shim whenever zipstream-php v3 is installed (its ++ // OperationMode enum exists). On Nextcloud the autoloader is shared ++ // across apps, so a co-installed app bundling zipstream-php v2 (e.g. ++ // Forms) can leak ZipStream\Option\Archive onto the classpath even ++ // though OpenRegister ships v3. The original class_exists(Archive) ++ // check then picks the v2 shim and calls v3's ZipStream(operationMode: ++ // null), throwing a TypeError and 500ing every .xlsx export. ++ if (enum_exists(\ZipStream\OperationMode::class)) { ++ return ZipStream3::newZipStream($fileHandle); ++ } ++ ++ return class_exists(Archive::class) ? ZipStream2::newZipStream($fileHandle) : ZipStream3::newZipStream($fileHandle); + } + } diff --git a/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php b/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php index 4e21f8d353..8a7aa47824 100644 --- a/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php +++ b/phpcs-custom-sniffs/CustomSniffs/Sniffs/Functions/NamedParametersSniff.php @@ -247,7 +247,7 @@ private function isClassFromOurNamespace(File $phpcsFile, int $classNamePtr): bo /** - * Get the app namespace prefix (e.g., "OCA\MyDash") from the file's namespace declaration. + * Get the app namespace prefix (e.g., "OCA\LaunchPad") from the file's namespace declaration. * * @param File $phpcsFile The file being scanned. * diff --git a/phpcs.xml b/phpcs.xml index ee4197926c..34217c6033 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -9,7 +9,7 @@ */vendor-bin/* */node_modules/* composer-setup.php - lib/Resources/template/* diff --git a/phpmd.baseline.xml b/phpmd.baseline.xml new file mode 100644 index 0000000000..490912f13a --- /dev/null +++ b/phpmd.baseline.xml @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4721dfff3b..7d5e1f84c9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,168 @@ parameters: ignoreErrors: + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:discover\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{error\\?\\: string, total_count\\?\\: mixed, results\\?\\: array\\{0\\?\\: mixed\\}, page\\?\\: int, per_page\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ConfigurationController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:calculate\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{status\\: 'error'\\|'success', message\\?\\: string, timestamp\\: string, scope\\?\\: array\\{register\\: array\\{id\\: int, title\\: string\\|null\\}\\|null, schema\\: array\\{id\\: int, title\\: string\\|null\\}\\|null\\}, results\\?\\: array\\{objects\\: array, logs\\: array, total\\: array\\{processed\\: mixed, failed\\: mixed\\}\\}, summary\\?\\: array\\{total_processed\\: mixed, total_failed\\: mixed, success_rate\\: float\\}\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:getAuditTrailActionChart\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, labels\\?\\: array\\, series\\?\\: array\\, name\\: string\\}\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:getAuditTrailActionDistribution\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, actions\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:getAuditTrailStatistics\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, total\\?\\: int, creates\\?\\: int, updates\\?\\: int, deletes\\?\\: int, reads\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:getMostActiveObjects\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, objects\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:getObjectsByRegisterChart\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, labels\\?\\: array, series\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:getObjectsBySchemaChart\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, labels\\?\\: array, series\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:getObjectsBySizeChart\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, labels\\?\\: array\\ 1 MB'\\>, series\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DashboardController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, registers\\?\\: array\\, source\\?\\: string\\|null, \\.\\.\\.\\}\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/DashboardController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FileExtractionController\\:\\:cleanup\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: 'Cleanup failed', message\\: string, data\\?\\: array\\{deleted\\: 0, reasons\\: array\\}\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/FileExtractionController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FileExtractionController\\:\\:discover\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: 'File discovery…', message\\: string, data\\?\\: array\\{discovered\\: int\\<0, max\\>, failed\\: int\\<0, max\\>, total\\: int\\<0, max\\>, error\\?\\: string\\}\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/FileExtractionController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FileExtractionController\\:\\:extractAll\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: 'Batch extraction…', message\\: string, data\\?\\: array\\{processed\\: int\\<0, max\\>, failed\\: int\\<0, max\\>, total\\: int\\<0, max\\>\\}\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/FileExtractionController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FileExtractionController\\:\\:retryFailed\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: 'Retry failed', message\\: string, data\\?\\: array\\{retried\\: int\\<0, max\\>, failed\\: int\\<0, max\\>, total\\: int\\<0, max\\>\\}\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/FileExtractionController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404, array\\, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ObjectsController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:objects\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ObjectsController.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Repair/SeedDirectoryVirtualSchemas.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Repair/SeedDirectoryVirtualSchemas.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Repair/SeedDirectoryVirtualSchemas.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Repair/SeedDirectoryVirtualSchemas.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Repair/SeedAppVirtualSchemas.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Repair/SeedAppVirtualSchemas.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Repair/SeedAppVirtualSchemas.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Repair/SeedAppVirtualSchemas.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 3 + path: lib/Service/ObjectSource/TablesSchemaSyncService.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 3 + path: lib/Service/ObjectSource/TablesSchemaSyncService.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 2 + path: lib/Service/ObjectSource/TablesSchemaSyncService.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 2 + path: lib/Service/ObjectSource/TablesSchemaSyncService.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 2 + path: lib/Search/ObjectsProvider.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 2 + path: lib/Search/ObjectsProvider.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Search/ObjectsProvider.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Search/ObjectsProvider.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/AppHost/Observability/Source/ObjectMetricSource.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/AppHost/Observability/Source/ObjectMetricSource.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/AppHost/Observability/Source/ObjectMetricSource.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/AppHost/Observability/Source/ObjectMetricSource.php + - message: "#^Parameter \\$container of class OCA\\\\OpenRegister\\\\Service\\\\Object\\\\CacheHandler constructor expects OCP\\\\AppFramework\\\\IAppContainer\\|null, Psr\\\\Container\\\\ContainerInterface given\\.$#" count: 1 @@ -131,44 +294,69 @@ parameters: path: lib/Command/BackfillSystemOwnerCommand.php - - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\Register\\:\\:getName\\(\\)\\.$#" + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" count: 1 - path: lib/Command/MigrateStorageCommand.php + path: lib/Command/EncryptFieldCommand.php - - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\Schema\\:\\:getName\\(\\)\\.$#" + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" count: 1 - path: lib/Command/MigrateStorageCommand.php + path: lib/Command/EncryptFieldCommand.php - - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + message: "#^Strict comparison using \\=\\=\\= between int and null will always evaluate to false\\.$#" count: 1 - path: lib/Command/RematerialiseCalculationsCommand.php + path: lib/Command/BackfillTranslationSourceLanguageCommand.php + + - + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" + count: 1 + path: lib/Command/BackfillTranslationSourceLanguageCommand.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Command/BackfillTranslationSourceLanguageCommand.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Command/BackfillTranslationSourceLanguageCommand.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Command/DedupeRegistersCommand.php - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" count: 1 - path: lib/Command/RematerialiseCalculationsCommand.php + path: lib/Command/DedupeRegistersCommand.php - - message: "#^Property OCA\\\\OpenRegister\\\\Command\\\\SolrDebugCommand\\:\\:\\$clientService is never read, only written\\.$#" + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" count: 1 - path: lib/Command/SolrDebugCommand.php + path: lib/Command/DedupeRegistersCommand.php - - message: "#^Property OCA\\\\OpenRegister\\\\Command\\\\SolrDebugCommand\\:\\:\\$logger is never read, only written\\.$#" + message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\Register\\:\\:getName\\(\\)\\.$#" count: 1 - path: lib/Command/SolrDebugCommand.php + path: lib/Command/MigrateStorageCommand.php - - message: "#^Offset 'error' on \\*NEVER\\* on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\Schema\\:\\:getName\\(\\)\\.$#" count: 1 - path: lib/Command/SolrManagementCommand.php + path: lib/Command/MigrateStorageCommand.php - - message: "#^Offset 'success' does not exist on array\\{results\\: mixed, total\\: mixed, start\\: mixed\\}\\.$#" - count: 4 - path: lib/Command/SolrManagementCommand.php + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Command/RematerialiseCalculationsCommand.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Command/RematerialiseCalculationsCommand.php - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AgentsController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: 'Failed to retrieve…', results\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\\\}, array\\{\\}\\>\\.$#" @@ -220,6 +408,41 @@ parameters: count: 1 path: lib/Controller/AggregationController.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AnonymisationBackendController\\:\\:getBackendState\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|403, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{entityRecognitionEnabled\\: bool, activeMethod\\: string, effectiveMethod\\: string, backends\\: array\\\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/AnonymisationBackendController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AnonymisationBackendController\\:\\:getBackendState\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|403, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\>\\.$#" + count: 1 + path: lib/Controller/AnonymisationBackendController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AnonymisationBackendController\\:\\:requireAdmin\\(\\) never returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\> so it can be removed from the return type\\.$#" + count: 1 + path: lib/Controller/AnonymisationBackendController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AnonymisationBackendController\\:\\:requireAdmin\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\>\\|null but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/AnonymisationBackendController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AnonymisationBackendController\\:\\:testConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|403, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{reachable\\: bool, latencyMs\\: int\\|null, error\\: string\\|null, probedAt\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/AnonymisationBackendController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AnonymisationBackendController\\:\\:testConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|403, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/AnonymisationBackendController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AnonymisationBackendController\\:\\:testConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|403, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\>\\.$#" + count: 1 + path: lib/Controller/AnonymisationBackendController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ApplicationsController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400, array\\{error\\?\\: 'Failed to delete…', message\\?\\: 'Application deleted…'\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{message\\: string\\}, array\\{\\}\\>\\.$#" count: 1 @@ -270,11 +493,6 @@ parameters: count: 5 path: lib/Controller/ArchivalController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AuditTrailController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: int\\<0, max\\>, page\\: int\\|null, pages\\: float, limit\\: int, offset\\: int\\|null\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: int, page\\: mixed, pages\\: float, limit\\: mixed, offset\\: mixed\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/AuditTrailController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AuditTrailController\\:\\:objects\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, results\\?\\: array\\, total\\?\\: int\\<0, max\\>, page\\?\\: int\\|null, pages\\?\\: float, limit\\?\\: int, offset\\?\\: int\\|null\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: int, page\\: mixed, pages\\: float, limit\\: mixed, offset\\: mixed\\}, array\\{\\}\\>\\.$#" count: 1 @@ -291,27 +509,27 @@ parameters: path: lib/Controller/AuditTrailController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AuditTrailController\\:\\:show\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: 'Audit trail not…'\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\|bool\\|float\\|int\\|JsonSerializable\\|stdClass\\|string\\|null, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\BulkController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404\\|500, array\\{error\\?\\: string, success\\?\\: true, message\\?\\: 'Bulk save operation…', saved_count\\?\\: mixed, saved_objects\\?\\: array\\, requested_count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, saved_count\\: \\(array\\|float\\|int\\), saved_objects\\: array\\, requested_count\\: int\\}, array\\{\\}\\>\\.$#" count: 1 - path: lib/Controller/AuditTrailController.php + path: lib/Controller/BulkController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\AuditTrailController\\:\\:show\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: 'Audit trail not…'\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\BulkController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404\\|500, array\\{error\\?\\: string, success\\?\\: true, message\\?\\: 'Bulk save operation…', saved_count\\?\\: mixed, saved_objects\\?\\: array\\, requested_count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 - path: lib/Controller/AuditTrailController.php + path: lib/Controller/BulkController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\BulkController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404\\|500, array\\{error\\?\\: string, success\\?\\: true, message\\?\\: 'Bulk save operation…', saved_count\\?\\: mixed, saved_objects\\?\\: array\\, requested_count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, saved_count\\: \\(array\\|float\\|int\\), saved_objects\\: array\\, requested_count\\: int\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\BulkController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404\\|500, array\\{error\\?\\: string, success\\?\\: true, message\\?\\: 'Bulk save operation…', saved_count\\?\\: mixed, saved_objects\\?\\: array\\, requested_count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/BulkController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\BulkController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404\\|500, array\\{error\\?\\: string, success\\?\\: true, message\\?\\: 'Bulk save operation…', saved_count\\?\\: mixed, saved_objects\\?\\: array\\, requested_count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\BulkController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404\\|500, array\\{error\\?\\: string, success\\?\\: true, message\\?\\: 'Bulk save operation…', saved_count\\?\\: mixed, saved_objects\\?\\: array\\, requested_count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 2 path: lib/Controller/BulkController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\BulkController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404\\|500, array\\{error\\?\\: string, success\\?\\: true, message\\?\\: 'Bulk save operation…', saved_count\\?\\: mixed, saved_objects\\?\\: array\\, requested_count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\BulkController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404\\|500, array\\{error\\?\\: string, success\\?\\: true, message\\?\\: 'Bulk save operation…', saved_count\\?\\: mixed, saved_objects\\?\\: array\\, requested_count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{error\\: string, error_code\\: string, detail\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/BulkController.php @@ -365,21 +583,6 @@ parameters: count: 1 path: lib/Controller/ChatStreamController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404\\|500, array\\{error\\?\\: 'Configuration not…'\\|'Failed to delete…', success\\?\\: true\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/ConfigurationController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404\\|500, array\\{error\\?\\: 'Configuration not…'\\|'Failed to delete…', success\\?\\: true\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/ConfigurationController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404\\|500, array\\{error\\?\\: 'Configuration not…'\\|'Failed to delete…', success\\?\\: true\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/ConfigurationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:discover\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{error\\?\\: string, total_count\\?\\: mixed, results\\?\\: array\\{0\\?\\: mixed\\}, page\\?\\: int, per_page\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{total_count\\: mixed, results\\: array\\, page\\: int, per_page\\: int\\}, array\\{\\}\\>\\.$#" count: 1 @@ -391,12 +594,12 @@ parameters: path: lib/Controller/ConfigurationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:discover\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{error\\?\\: string, total_count\\?\\: mixed, results\\?\\: array\\{0\\?\\: mixed\\}, page\\?\\: int, per_page\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404\\|500, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ConfigurationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404\\|500, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404\\|500, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ConfigurationController.php @@ -406,22 +609,22 @@ parameters: path: lib/Controller/ConfigurationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404\\|500, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\<'Failed to fetch…'\\|OCA\\\\OpenRegister\\\\Db\\\\Configuration\\>, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ConfigurationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\<'Failed to fetch…'\\|OCA\\\\OpenRegister\\\\Db\\\\Configuration\\>, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\<'Failed to fetch…'\\|OCA\\\\OpenRegister\\\\Db\\\\Configuration\\>, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ConfigurationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\<'Failed to fetch…'\\|OCA\\\\OpenRegister\\\\Db\\\\Configuration\\>, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:show\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404\\|500, array\\{error\\: 'Configuration not…'\\|'Failed to fetch…'\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Configuration, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Configuration, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ConfigurationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:show\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404\\|500, array\\{error\\: 'Configuration not…'\\|'Failed to fetch…'\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Configuration, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Configuration, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationController\\:\\:show\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404\\|500, array\\{error\\: 'Configuration not…'\\|'Failed to fetch…'\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Configuration, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ConfigurationController.php @@ -485,6 +688,11 @@ parameters: count: 1 path: lib/Controller/ConfigurationsController.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationsController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Configuration, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ConfigurationsController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationsController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<204, null, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<204, null, array\\{\\}\\>\\.$#" count: 1 @@ -495,6 +703,11 @@ parameters: count: 1 path: lib/Controller/ConfigurationsController.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationsController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<204, null, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ConfigurationsController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationsController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/json', array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/json', array\\{\\}\\>\\.$#" count: 1 @@ -530,6 +743,11 @@ parameters: count: 1 path: lib/Controller/ConfigurationsController.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationsController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Configuration, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ConfigurationsController.php + - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\ConfigurationsController\\:\\:\\$uploadService is never read, only written\\.$#" count: 1 @@ -565,6 +783,26 @@ parameters: count: 1 path: lib/Controller/ConfigurationsController.php + - + message: "#^Parameter \\#1 \\$data of class OCP\\\\AppFramework\\\\Http\\\\DataResponse constructor expects T of OCP\\\\AppFramework\\\\Http\\\\DataResponseType, array given\\.$#" + count: 1 + path: lib/Controller/ContextsController.php + + - + message: "#^Parameter \\#1 \\$data of class OCP\\\\AppFramework\\\\Http\\\\DataResponse constructor expects T of OCP\\\\AppFramework\\\\Http\\\\DataResponseType, array\\ given\\.$#" + count: 1 + path: lib/Controller/ContextsController.php + + - + message: "#^Parameter \\#1 \\$data of class OCP\\\\AppFramework\\\\Http\\\\DataResponse constructor expects T of OCP\\\\AppFramework\\\\Http\\\\DataResponseType, array\\ given\\.$#" + count: 3 + path: lib/Controller/ContextsController.php + + - + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" + count: 1 + path: lib/Controller/ContextsController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ConversationController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201\\|500, array\\{error\\?\\: 'Failed to create…', message\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, title\\?\\: string\\|null, userId\\?\\: string\\|null, organisation\\?\\: string\\|null, agentId\\?\\: int\\|null, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, array\\{id\\: int, uuid\\: string\\|null, title\\: string\\|null, userId\\: string\\|null, organisation\\: string\\|null, agentId\\: int\\|null, metadata\\: array\\|null, deletedAt\\: string\\|null, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" count: 1 @@ -681,7 +919,7 @@ parameters: path: lib/Controller/DashboardController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DeletedController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, results\\?\\: array\\, total\\?\\: int, page\\?\\: int, pages\\?\\: 1\\|float, limit\\?\\: int\\|null, offset\\?\\: int\\|null\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: mixed, page\\: mixed, pages\\: float\\|int, limit\\: mixed, offset\\: mixed\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\DeletedController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: string, results\\?\\: array\\, total\\?\\: int, page\\?\\: int, pages\\?\\: 1\\|float, limit\\?\\: int\\|null, offset\\?\\: int\\|null\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: int, page\\: mixed, pages\\: float\\|int, limit\\: mixed, offset\\: mixed\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/DeletedController.php @@ -695,11 +933,6 @@ parameters: count: 1 path: lib/Controller/DeletedController.php - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\DeletedController\\:\\:\\$schemaMapper is never read, only written\\.$#" - count: 1 - path: lib/Controller/DeletedController.php - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\EmailsController\\:\\:\\$userSession is never read, only written\\.$#" count: 1 @@ -731,12 +964,17 @@ parameters: path: lib/Controller/EndpointsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\EndpointsController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Endpoint, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\EndpointsController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Endpoint, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/EndpointsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\EndpointsController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: 'Failed to list…', results\\?\\: array\\, total\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: int\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\EndpointsController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Endpoint, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/EndpointsController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\EndpointsController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: 'Failed to list…', results\\?\\: array\\, total\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: int\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/EndpointsController.php @@ -770,6 +1008,11 @@ parameters: count: 1 path: lib/Controller/EndpointsController.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\EndpointsController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Endpoint, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/EndpointsController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\EndpointsController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Endpoint, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 @@ -875,6 +1118,11 @@ parameters: count: 3 path: lib/Controller/FilesController.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/FilesController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 2 @@ -891,40 +1139,35 @@ parameters: path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:createMultipart\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, 0\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:createMultipart\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, 0\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:delete\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, success\\?\\: bool\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:createMultipart\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, 0\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:delete\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, success\\?\\: bool\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:delete\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, success\\?\\: bool\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:delete\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, success\\?\\: bool\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:delete\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, success\\?\\: bool\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:delete\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, success\\?\\: bool\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:delete\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, success\\?\\: bool\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/FilesController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 2 - path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:page\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\TemplateResponse\\<200, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\TemplateResponse\\<200, array\\{\\}\\>\\.$#" count: 1 @@ -936,30 +1179,25 @@ parameters: path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:resolveParentObjectForFile\\(\\) never returns OCA\\\\OpenRegister\\\\Db\\\\ObjectEntity so it can be removed from the return type\\.$#" count: 1 path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 2 + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 4 path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/FilesController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 4 - path: lib/Controller/FilesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\FilesController\\:\\:save\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: mixed, labels\\?\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 2 @@ -1025,6 +1263,21 @@ parameters: count: 1 path: lib/Controller/MappingsController.php + - + message: "#^Dead catch \\- OCA\\\\OpenRegister\\\\Exception\\\\NotAuthorizedException is never thrown in the try block\\.$#" + count: 3 + path: lib/Controller/MergeController.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Merge/MergeService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Controller/SurvivorshipController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\NamesController\\:\\:stats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: 'Failed to retrieve…', message\\?\\: string, cache_statistics\\?\\: array\\{hits\\: int, misses\\: int, preloads\\: int, query_hits\\: int, query_misses\\: int, name_hits\\: int, name_misses\\: int, name_warmups\\: int, \\.\\.\\.\\}, performance_metrics\\?\\: array\\{name_cache_enabled\\: true, distributed_cache_available\\: true, warmup_available\\: true\\}\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{cache_statistics\\: array\\{hits\\: int, misses\\: int, preloads\\: int, query_hits\\: int, query_misses\\: int, name_hits\\: int, name_misses\\: int, name_warmups\\: int, \\.\\.\\.\\}, performance_metrics\\: array\\{name_cache_enabled\\: bool, distributed_cache_available\\: bool, warmup_available\\: bool\\}\\}, array\\{\\}\\>\\.$#" count: 1 @@ -1080,6 +1333,11 @@ parameters: count: 1 path: lib/Controller/ObjectsController.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201\\|401\\|403\\|404, array\\{@self\\?\\: mixed, message\\?\\: mixed, error\\?\\: mixed\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, string, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<401, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ObjectsController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201\\|401\\|403\\|404, array\\{@self\\?\\: mixed, message\\?\\: mixed, error\\?\\: mixed\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, string, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 @@ -1096,17 +1354,22 @@ parameters: path: lib/Controller/ObjectsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201\\|401\\|403\\|404, array\\{@self\\?\\: mixed, message\\?\\: mixed, error\\?\\: mixed\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, string, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<401, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/pdf'\\|'application/vnd…'\\|'text/csv', array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse but returns OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/json', array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ObjectsController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/pdf'\\|'application/vnd…'\\|'text/csv', array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse but returns OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/pdf', array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ObjectsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/vnd…'\\|'text/csv', array\\> but returns OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/vnd…', array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/pdf'\\|'application/vnd…'\\|'text/csv', array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse but returns OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/vnd…', array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ObjectsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/vnd…'\\|'text/csv', array\\> but returns OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'text/csv', array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:export\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'application/pdf'\\|'application/vnd…'\\|'text/csv', array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse but returns OCP\\\\AppFramework\\\\Http\\\\DataDownloadResponse\\<200, 'text/csv', array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/ObjectsController.php @@ -1155,11 +1418,6 @@ parameters: count: 1 path: lib/Controller/ObjectsController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:unlock\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{message\\: 'Object unlocked…', locked\\: false, uuid\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{message\\: string, locked\\: bool, uuid\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/ObjectsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:used\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array, total\\: 0, limit\\: mixed, offset\\: mixed, message\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\{\\}\\>\\.$#" count: 1 @@ -1231,17 +1489,22 @@ parameters: path: lib/Controller/ObjectsController.php - - message: "#^Result of && is always false\\.$#" - count: 2 + message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:\\$importService is never read, only written\\.$#" + count: 1 path: lib/Controller/ObjectsController.php - - message: "#^Result of && is always true\\.$#" + message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:\\$jsonLdContextService is never read, only written\\.$#" count: 1 path: lib/Controller/ObjectsController.php - - message: "#^Strict comparison using \\=\\=\\= between array and null will always evaluate to false\\.$#" + message: "#^Result of && is always false\\.$#" + count: 2 + path: lib/Controller/ObjectsController.php + + - + message: "#^Result of && is always true\\.$#" count: 1 path: lib/Controller/ObjectsController.php @@ -1310,16 +1573,6 @@ parameters: count: 1 path: lib/Controller/OrganisationController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\OrganisationController\\:\\:join\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400, array\\{error\\?\\: string, message\\?\\: 'Successfully joined…'\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{message\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/OrganisationController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\OrganisationController\\:\\:join\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400, array\\{error\\?\\: string, message\\?\\: 'Successfully joined…'\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/OrganisationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\OrganisationController\\:\\:search\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{error\\?\\: 'Search failed', organisations\\?\\: array\\, limit\\?\\: int\\<1, 100\\>, offset\\?\\: int\\<0, max\\>, count\\?\\: int\\<0, max\\>\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{organisations\\: array\\, limit\\: int, offset\\: int, count\\: int\\}, array\\{\\}\\>\\.$#" count: 1 @@ -1360,6 +1613,11 @@ parameters: count: 1 path: lib/Controller/OrganisationController.php + - + message: "#^Right side of \\|\\| is always true\\.$#" + count: 1 + path: lib/Controller/ProcessingLogController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Register, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<100\\|101\\|102\\|200\\|201\\|202\\|203\\|204\\|205\\|206\\|207\\|208\\|226\\|300\\|301\\|302\\|303\\|304\\|305\\|306\\|307\\|400\\|401\\|402\\|403\\|404\\|405\\|406\\|407\\|408\\|409\\|410\\|411\\|412\\|413\\|414\\|415\\|416\\|417\\|418\\|422\\|423\\|424\\|426\\|428\\|429\\|431\\|500\\|501\\|502\\|503\\|504\\|505\\|506\\|507\\|508\\|509\\|510\\|511, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 2 @@ -1380,21 +1638,6 @@ parameters: count: 1 path: lib/Controller/RegistersController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, version\\?\\: string\\|null, description\\?\\: string\\|null, schemas\\?\\: array\\, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{id\\: int, uuid\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, version\\: string\\|null, description\\: string\\|null, schemas\\: array\\, source\\: string\\|null, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/RegistersController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, version\\?\\: string\\|null, description\\?\\: string\\|null, schemas\\?\\: array\\, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/RegistersController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, version\\?\\: string\\|null, description\\?\\: string\\|null, schemas\\?\\: array\\, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/RegistersController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|403\\|404\\|409\\|500, array\\{error\\?\\: string, objectCount\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{\\}, array\\{\\}\\>\\.$#" count: 1 @@ -1420,21 +1663,6 @@ parameters: count: 1 path: lib/Controller/RegistersController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, version\\?\\: string\\|null, description\\?\\: string\\|null, schemas\\?\\: array\\, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{id\\: int, uuid\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, version\\: string\\|null, description\\: string\\|null, schemas\\: array\\, source\\: string\\|null, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/RegistersController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, version\\?\\: string\\|null, description\\?\\: string\\|null, schemas\\?\\: array\\, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/RegistersController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, version\\?\\: string\\|null, description\\?\\: string\\|null, schemas\\?\\: array\\, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/RegistersController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:stats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|404\\|500, array\\{error\\?\\: string, register\\?\\: array\\{id\\: int, uuid\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, version\\: string\\|null, description\\: string\\|null, schemas\\: array\\, source\\: string\\|null, \\.\\.\\.\\}, message\\?\\: 'Stats calculation…'\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{register\\: array\\{id\\: int, uuid\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, version\\: string\\|null, description\\: string\\|null, schemas\\: array\\, source\\: string\\|null, \\.\\.\\.\\}, message\\: string\\}, array\\{\\}\\>\\.$#" count: 1 @@ -1470,21 +1698,11 @@ parameters: count: 1 path: lib/Controller/RegistersController.php - - - message: "#^Offset 'id' on array\\{id\\: int, uuid\\: string\\|null, uri\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, description\\: string\\|null, version\\: string\\|null, summary\\: string\\|null, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/RegistersController.php - - message: "#^Offset 'objects' on array\\{registers\\: array\\, schemas\\: array\\, objects\\: array\\, endpoints\\: array, sources\\: array, mappings\\: array, jobs\\: array, synchronizations\\: array, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 path: lib/Controller/RegistersController.php - - - message: "#^Offset 'schemas' on array\\{id\\: int, uuid\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, version\\: string\\|null, description\\: string\\|null, schemas\\: array\\, source\\: string\\|null, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/RegistersController.php - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\RegistersController\\:\\:\\$appManager is never read, only written\\.$#" count: 1 @@ -1497,7 +1715,7 @@ parameters: - message: "#^Result of && is always true\\.$#" - count: 2 + count: 1 path: lib/Controller/RegistersController.php - @@ -1531,47 +1749,47 @@ parameters: path: lib/Controller/ScheduledWorkflowController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<100\\|101\\|102\\|200\\|201\\|202\\|203\\|204\\|205\\|206\\|207\\|208\\|226\\|300\\|301\\|302\\|303\\|304\\|305\\|306\\|307\\|400\\|401\\|402\\|403\\|404\\|405\\|406\\|407\\|408\\|409\\|410\\|411\\|412\\|413\\|414\\|415\\|416\\|417\\|418\\|422\\|423\\|424\\|426\\|428\\|429\\|431\\|500\\|501\\|502\\|503\\|504\\|505\\|506\\|507\\|508\\|509\\|510\\|511, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 2 - path: lib/Controller/SchemasController.php + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" + count: 1 + path: lib/Controller/SchemaImportController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\{\\}\\>\\.$#" + message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\SchemaMigrationController\\:\\:\\$logger is never read, only written\\.$#" count: 1 - path: lib/Controller/SchemasController.php + path: lib/Controller/SchemaMigrationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" count: 1 - path: lib/Controller/SchemasController.php + path: lib/Controller/SchemaMigrationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" count: 1 - path: lib/Controller/SchemasController.php + path: lib/Controller/SchemaMigrationController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<409, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<100\\|101\\|102\\|200\\|201\\|202\\|203\\|204\\|205\\|206\\|207\\|208\\|226\\|300\\|301\\|302\\|303\\|304\\|305\\|306\\|307\\|400\\|401\\|402\\|403\\|404\\|405\\|406\\|407\\|408\\|409\\|410\\|411\\|412\\|413\\|414\\|415\\|416\\|417\\|418\\|422\\|423\\|424\\|426\\|428\\|429\\|431\\|500\\|501\\|502\\|503\\|504\\|505\\|506\\|507\\|508\\|509\\|510\\|511, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 2 path: lib/Controller/SchemasController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SchemasController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, uri\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, description\\?\\: string\\|null, version\\?\\: string\\|null, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{id\\: int, uuid\\: string\\|null, uri\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, description\\: string\\|null, version\\: string\\|null, summary\\: string\\|null, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SchemasController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, uri\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, description\\?\\: string\\|null, version\\?\\: string\\|null, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SchemasController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:depublish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, uri\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, description\\?\\: string\\|null, version\\?\\: string\\|null, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<201, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<409, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SchemasController.php @@ -1595,31 +1813,11 @@ parameters: count: 1 path: lib/Controller/SchemasController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|409\\|500, array\\{error\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SchemasController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SchemasController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, uri\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, description\\?\\: string\\|null, version\\?\\: string\\|null, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{id\\: int, uuid\\: string\\|null, uri\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, description\\: string\\|null, version\\: string\\|null, summary\\: string\\|null, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SchemasController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, uri\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, description\\?\\: string\\|null, version\\?\\: string\\|null, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SchemasController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:publish\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|404, array\\{error\\?\\: string, id\\?\\: int, uuid\\?\\: string\\|null, uri\\?\\: string\\|null, slug\\?\\: string\\|null, title\\?\\: string\\|null, description\\?\\: string\\|null, version\\?\\: string\\|null, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<404, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SchemasController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|404\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<100\\|101\\|102\\|200\\|201\\|202\\|203\\|204\\|205\\|206\\|207\\|208\\|226\\|300\\|301\\|302\\|303\\|304\\|305\\|306\\|307\\|400\\|401\\|402\\|403\\|404\\|405\\|406\\|407\\|408\\|409\\|410\\|411\\|412\\|413\\|414\\|415\\|416\\|417\\|418\\|422\\|423\\|424\\|426\\|428\\|429\\|431\\|500\\|501\\|502\\|503\\|504\\|505\\|506\\|507\\|508\\|509\\|510\\|511, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 2 @@ -1646,12 +1844,12 @@ parameters: path: lib/Controller/SchemasController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|404\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<409, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|404\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<409, array\\, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SchemasController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|404\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SchemasController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400\\|403\\|404\\|409\\|500, array\\{error\\: string\\}, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Schema, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<409, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SchemasController.php @@ -1692,7 +1890,7 @@ parameters: - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 6 + count: 4 path: lib/Controller/SchemasController.php - @@ -1746,37 +1944,7 @@ parameters: path: lib/Controller/ScopesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SearchController\\:\\:search\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: mixed, facets\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\{\\}, total\\: mixed, facets\\: array\\{\\}\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SearchController.php - - - - message: "#^Offset 'facets' on array\\{results\\: mixed, total\\: mixed, start\\: mixed\\} on left side of \\?\\? does not exist\\.$#" - count: 1 - path: lib/Controller/SearchController.php - - - - message: "#^Offset 'id' on \\*NEVER\\* on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/SearchController.php - - - - message: "#^Offset 'name' on \\*NEVER\\* on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/SearchController.php - - - - message: "#^Offset 'objects' on array\\{results\\: mixed, total\\: mixed, start\\: mixed\\} on left side of \\?\\? does not exist\\.$#" - count: 1 - path: lib/Controller/SearchController.php - - - - message: "#^Offset 'url' on \\*NEVER\\* on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/SearchController.php - - - - message: "#^Offset 'uuid' on \\*NEVER\\* on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SearchController\\:\\:search\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: int, facets\\: array\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\, total\\: mixed, facets\\: array\\{\\}\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SearchController.php @@ -1830,11 +1998,6 @@ parameters: count: 2 path: lib/Controller/Settings/CacheSettingsController.php - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\CacheSettingsController\\:\\:\\$indexService is never read, only written\\.$#" - count: 1 - path: lib/Controller/Settings/CacheSettingsController.php - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\ConfigurationSettingsController\\:\\:\\$logger is never read, only written\\.$#" count: 1 @@ -1846,22 +2009,12 @@ parameters: path: lib/Controller/Settings/EdepotSettingsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:getFileExtractionStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: true, totalFiles\\: mixed, processedFiles\\: mixed, pendingFiles\\: mixed, untrackedFiles\\: mixed, totalChunks\\: mixed, extractedTextStorageMB\\: string, totalFilesStorageMB\\: string, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, totalFiles\\: int, processedFiles\\: int, pendingFiles\\: int, untrackedFiles\\: int, totalChunks\\: int, extractedTextStorageMB\\: string, totalFilesStorageMB\\: string, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/FileSettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:getFileExtractionStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: true, totalFiles\\: mixed, processedFiles\\: mixed, pendingFiles\\: mixed, untrackedFiles\\: mixed, totalChunks\\: mixed, extractedTextStorageMB\\: string, totalFilesStorageMB\\: string, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, totalFiles\\: mixed, processedFiles\\: mixed, pendingFiles\\: mixed, untrackedFiles\\: float\\|int, totalChunks\\: mixed, extractedTextStorageMB\\: string, totalFilesStorageMB\\: string, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/FileSettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:getFileIndexStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: false, message\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\|bool\\|float\\|int\\|JsonSerializable\\|stdClass\\|string\\|null, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:getFileExtractionStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: true, totalFiles\\: mixed, processedFiles\\: mixed, pendingFiles\\: mixed, untrackedFiles\\: mixed, extractedTextStorageMB\\: string, totalFilesStorageMB\\: string, completed\\: mixed, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, totalFiles\\: int, processedFiles\\: int, pendingFiles\\: int, untrackedFiles\\: int, extractedTextStorageMB\\: string, totalFilesStorageMB\\: string, completed\\: int, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/Settings/FileSettingsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:getFileIndexStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: false, message\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, message\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:getFileExtractionStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: true, totalFiles\\: mixed, processedFiles\\: mixed, pendingFiles\\: mixed, untrackedFiles\\: mixed, extractedTextStorageMB\\: string, totalFilesStorageMB\\: string, completed\\: mixed, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, totalFiles\\: mixed, processedFiles\\: mixed, pendingFiles\\: mixed, untrackedFiles\\: float\\|int, extractedTextStorageMB\\: string, totalFilesStorageMB\\: string, completed\\: mixed, \\.\\.\\.\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/Settings/FileSettingsController.php @@ -1875,21 +2028,6 @@ parameters: count: 1 path: lib/Controller/Settings/FileSettingsController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:indexFile\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|422\\|500, array\\{success\\: bool, message\\: mixed, file_id\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, file_id\\: int\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/FileSettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:indexFile\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|422\\|500, array\\{success\\: bool, message\\: mixed, file_id\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: bool, message\\: mixed, file_id\\: int\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/FileSettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:indexFile\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|422\\|500, array\\{success\\: bool, message\\: mixed, file_id\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, message\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/FileSettingsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:testDolphinConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'Dolphin connection…'\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\?\\: string, error\\?\\: string\\}, array\\{\\}\\>\\.$#" count: 1 @@ -1906,17 +2044,17 @@ parameters: path: lib/Controller/Settings/FileSettingsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:testOpenAnonymiserConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\?\\: string, error\\?\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:testOpenAnonymiserConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, error\\: mixed\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/Settings/FileSettingsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:testOpenAnonymiserConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:testOpenAnonymiserConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/Settings/FileSettingsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:testOpenAnonymiserConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:testOpenAnonymiserConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/Settings/FileSettingsController.php @@ -1935,6 +2073,11 @@ parameters: count: 1 path: lib/Controller/Settings/FileSettingsController.php + - + message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\FileSettingsController\\:\\:\\$logger is never read, only written\\.$#" + count: 1 + path: lib/Controller/Settings/FileSettingsController.php + - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\LlmSettingsController\\:\\:clearAllEmbeddings\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: string, deleted\\?\\: int\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, error\\?\\: string, message\\: string, deleted\\?\\: int\\}, array\\{\\}\\>\\.$#" count: 1 @@ -2005,161 +2148,6 @@ parameters: count: 1 path: lib/Controller/Settings/N8nSettingsController.php - - - message: "#^Access to an undefined property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:\\$userId\\.$#" - count: 4 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:fixMismatchedSolrFields\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|422, array\\{success\\: bool, message\\: string, details\\?\\: array\\{error\\: mixed\\}, fixed\\?\\: array, errors\\?\\: array\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, fixed\\: array\\{\\}, errors\\: array\\{\\}\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:fixMismatchedSolrFields\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|422, array\\{success\\: bool, message\\: string, details\\?\\: array\\{error\\: mixed\\}, fixed\\?\\: array, errors\\?\\: array\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\|bool\\|float\\|int\\|JsonSerializable\\|stdClass\\|string\\|null, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:fixMismatchedSolrFields\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|422, array\\{success\\: bool, message\\: string, details\\?\\: array\\{error\\: mixed\\}, fixed\\?\\: array, errors\\?\\: array\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: bool, message\\: string, details\\: array\\{error\\: mixed\\}\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:fixMismatchedSolrFields\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|422, array\\{success\\: bool, message\\: string, details\\?\\: array\\{error\\: mixed\\}, fixed\\?\\: array, errors\\?\\: array\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: bool, message\\: string, details\\: array\\{error\\: string\\}\\}, array\\{\\}\\>\\.$#" - count: 2 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:updateSolrCollectionAssignments\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, trace\\?\\: string, message\\?\\: 'Collection…', objectCollection\\?\\: mixed, fileCollection\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, objectCollection\\: mixed, fileCollection\\: mixed, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:updateSolrCollectionAssignments\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, trace\\?\\: string, message\\?\\: 'Collection…', objectCollection\\?\\: mixed, fileCollection\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string, trace\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:\\$db is never read, only written\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:\\$indexService is never read, only written\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrManagementController\\:\\:\\$logger is never read, only written\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" - count: 1 - path: lib/Controller/Settings/SolrManagementController.php - - - - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Service\\\\Index\\\\SetupHandler\\:\\:getInfrastructureCreated\\(\\)\\.$#" - count: 2 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Service\\\\Index\\\\SetupHandler\\:\\:getLastErrorDetails\\(\\)\\.$#" - count: 2 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Service\\\\Index\\\\SetupHandler\\:\\:getSetupProgress\\(\\)\\.$#" - count: 3 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:manageSolr\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{error\\?\\: mixed, success\\?\\: mixed, operation\\?\\: 'clear'\\|'commit'\\|'optimize', message\\?\\: string, timestamp\\?\\: string, error_details\\?\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: mixed, operation\\: string, error\\: mixed, error_details\\: mixed, message\\: string, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:manageSolr\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{error\\?\\: mixed, success\\?\\: mixed, operation\\?\\: 'clear'\\|'commit'\\|'optimize', message\\?\\: string, timestamp\\?\\: string, error_details\\?\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: mixed, operation\\: string, message\\: string, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 2 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:manageSolr\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{error\\?\\: mixed, success\\?\\: mixed, operation\\?\\: 'clear'\\|'commit'\\|'optimize', message\\?\\: string, timestamp\\?\\: string, error_details\\?\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{success\\: bool, message\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:manageSolr\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{error\\?\\: mixed, success\\?\\: mixed, operation\\?\\: 'clear'\\|'commit'\\|'optimize', message\\?\\: string, timestamp\\?\\: string, error_details\\?\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:testSolrConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: false, message\\: string, details\\: array\\{exception\\: string\\}\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\|bool\\|float\\|int\\|JsonSerializable\\|stdClass\\|string\\|null, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:testSolrConnection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: false, message\\: string, details\\: array\\{exception\\: string\\}\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: bool, message\\: string, details\\: array\\{exception\\: string\\}\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Offset 'message' on array\\{type\\: int, message\\: string, file\\: string, line\\: int\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:\\$db is never read, only written\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:\\$indexService is never read, only written\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrOperationsController\\:\\:\\$logger is never read, only written\\.$#" - count: 1 - path: lib/Controller/Settings/SolrOperationsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrSettingsController\\:\\:getSolrDashboardStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\|bool\\|float\\|int\\|JsonSerializable\\|stdClass\\|string\\|null, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrSettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrSettingsController\\:\\:getSolrDashboardStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrSettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrSettingsController\\:\\:getSolrFacetConfiguration\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrSettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrSettingsController\\:\\:getSolrFacetConfiguration\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/Settings/SolrSettingsController.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\SolrSettingsController\\:\\:\\$indexService is never read, only written\\.$#" - count: 1 - path: lib/Controller/Settings/SolrSettingsController.php - - message: "#^Property OCA\\\\OpenRegister\\\\Controller\\\\Settings\\\\VectorSettingsController\\:\\:\\$logger is never read, only written\\.$#" count: 1 @@ -2190,26 +2178,6 @@ parameters: count: 1 path: lib/Controller/SettingsController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SettingsController\\:\\:reindexSpecificCollection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|422, array\\{success\\: bool, message\\: mixed, collection\\: string, stats\\?\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, stats\\: mixed, collection\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SettingsController\\:\\:reindexSpecificCollection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|422, array\\{success\\: bool, message\\: mixed, collection\\: string, stats\\?\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{success\\: bool, message\\: string, collection\\: string\\}, array\\{\\}\\>\\.$#" - count: 2 - path: lib/Controller/SettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SettingsController\\:\\:reindexSpecificCollection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|422, array\\{success\\: bool, message\\: mixed, collection\\: string, stats\\?\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: bool, message\\: mixed, collection\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SettingsController\\:\\:reindexSpecificCollection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|422, array\\{success\\: bool, message\\: mixed, collection\\: string, stats\\?\\: mixed\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: bool, message\\: string, collection\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SettingsController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SettingsController\\:\\:semanticSearch\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, trace\\?\\: string, query\\?\\: string, results\\?\\: array\\\\>, total\\?\\: int\\<0, max\\>, limit\\?\\: int, filters\\?\\: array, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, query\\: string, results\\: array\\\\>, total\\: int, limit\\: int, filters\\: array, timestamp\\: string\\}, array\\{\\}\\>\\.$#" count: 1 @@ -2225,233 +2193,43 @@ parameters: count: 1 path: lib/Controller/SettingsController.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SettingsController\\:\\:testSchemaMapping\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: false, error\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\|bool\\|float\\|int\\|JsonSerializable\\|stdClass\\|string\\|null, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SettingsController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SettingsController\\:\\:testSchemaMapping\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: false, error\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<422, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SettingsController.php - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" count: 1 path: lib/Controller/ShareLinksController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:bulkVectorizeObjects\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: mixed, error\\?\\: mixed, message\\?\\: mixed, total\\?\\: mixed, successful\\?\\: mixed, failed\\?\\: mixed, results\\?\\: mixed, timestamp\\?\\: string, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, total\\: int, successful\\: int, failed\\: int, results\\: array\\{\\}, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:bulkVectorizeObjects\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: mixed, error\\?\\: mixed, message\\?\\: mixed, total\\?\\: mixed, successful\\?\\: mixed, failed\\?\\: mixed, results\\?\\: mixed, timestamp\\?\\: string, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, non\\-empty\\-array, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Source, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:bulkVectorizeObjects\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: mixed, error\\?\\: mixed, message\\?\\: mixed, total\\?\\: mixed, successful\\?\\: mixed, failed\\?\\: mixed, results\\?\\: mixed, timestamp\\?\\: string, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 2 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:bulkVectorizeObjects\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: mixed, error\\?\\: mixed, message\\?\\: mixed, total\\?\\: mixed, successful\\?\\: mixed, failed\\?\\: mixed, results\\?\\: mixed, timestamp\\?\\: string, \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:copyCollection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'Collection copied…', source\\?\\: string, target\\?\\: string, result\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, source\\: string, target\\: string, result\\: mixed, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:copyCollection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'Collection copied…', source\\?\\: string, target\\?\\: string, result\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:createCollection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'Collection created…', collection\\?\\: string, result\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, collection\\: string, result\\: mixed, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:createCollection\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'Collection created…', collection\\?\\: string, result\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:createConfigSet\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'ConfigSet created…', configSet\\?\\: string, result\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, configSet\\: string, result\\: mixed, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:createConfigSet\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'ConfigSet created…', configSet\\?\\: string, result\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:deleteConfigSet\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'ConfigSet deleted…', configSet\\?\\: string, result\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, configSet\\: string, result\\: mixed, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:deleteConfigSet\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'ConfigSet deleted…', configSet\\?\\: string, result\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:getVectorStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, stats\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, stats\\: mixed, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:getVectorStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, stats\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:getVectorizationStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, stats\\?\\: array\\{total_objects\\: mixed, vectorized_objects\\: mixed, progress_percentage\\: 0\\|float, remaining_objects\\: mixed, vector_breakdown\\: mixed\\}, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, stats\\: array\\{total_objects\\: mixed, vectorized_objects\\: mixed, progress_percentage\\: float\\|int, remaining_objects\\: \\(float\\|int\\), vector_breakdown\\: mixed\\}, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:getVectorizationStats\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, stats\\?\\: array\\{total_objects\\: mixed, vectorized_objects\\: mixed, progress_percentage\\: 0\\|float, remaining_objects\\: mixed, vector_breakdown\\: mixed\\}, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:hybridSearch\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: mixed, error\\?\\: mixed, query\\?\\: mixed, search_type\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, non\\-empty\\-array, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:hybridSearch\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: mixed, error\\?\\: mixed, query\\?\\: mixed, search_type\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 3 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:hybridSearch\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: mixed, error\\?\\: mixed, query\\?\\: mixed, search_type\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string, query\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:listCollections\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, collections\\?\\: mixed, total\\?\\: int\\<0, max\\>, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, collections\\: mixed, total\\: int, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:listCollections\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, collections\\?\\: mixed, total\\?\\: int\\<0, max\\>, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:listConfigSets\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, configSets\\?\\: mixed, total\\?\\: int\\<0, max\\>, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, configSets\\: mixed, total\\: int, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:listConfigSets\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: bool, error\\?\\: string, configSets\\?\\: mixed, total\\?\\: int\\<0, max\\>, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:semanticSearch\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, query\\?\\: string\\|null, results\\?\\: mixed, total\\?\\: int\\<0, max\\>, limit\\?\\: int\\<1, 100\\>, filters\\?\\: array, search_type\\?\\: 'semantic', \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, query\\: string, results\\: mixed, total\\: int, limit\\: int, filters\\: array, search_type\\: string, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:semanticSearch\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, query\\?\\: string\\|null, results\\?\\: mixed, total\\?\\: int\\<0, max\\>, limit\\?\\: int\\<1, 100\\>, filters\\?\\: array, search_type\\?\\: 'semantic', \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 2 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:semanticSearch\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, query\\?\\: string\\|null, results\\?\\: mixed, total\\?\\: int\\<0, max\\>, limit\\?\\: int\\<1, 100\\>, filters\\?\\: array, search_type\\?\\: 'semantic', \\.\\.\\.\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string, query\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:testVectorEmbedding\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'Embedding generated…', metadata\\?\\: array\\{provider\\: mixed, model\\: mixed, dimensions\\: int\\<0, max\\>, textLength\\: int\\<1, max\\>, duration_ms\\: float, firstValues\\: array\\}, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{success\\: bool, message\\: string, metadata\\: array\\{provider\\: mixed, model\\: mixed, dimensions\\: int, textLength\\: int, duration_ms\\: float, firstValues\\: array\\}, timestamp\\: string\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:testVectorEmbedding\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'Embedding generated…', metadata\\?\\: array\\{provider\\: mixed, model\\: mixed, dimensions\\: int\\<0, max\\>, textLength\\: int\\<1, max\\>, duration_ms\\: float, firstValues\\: array\\}, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 4 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:testVectorEmbedding\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400\\|500, array\\{success\\: bool, error\\?\\: string, message\\?\\: 'Embedding generated…', metadata\\?\\: array\\{provider\\: mixed, model\\: mixed, dimensions\\: int\\<0, max\\>, textLength\\: int\\<1, max\\>, duration_ms\\: float, firstValues\\: array\\}, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string\\}, array\\{\\}\\>\\.$#" - count: 2 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:vectorizeObject\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: mixed, error\\?\\: mixed, object_id\\?\\: mixed, message\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, non\\-empty\\-array, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SolrController\\:\\:vectorizeObject\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|500, array\\{success\\: mixed, error\\?\\: mixed, object_id\\?\\: mixed, message\\?\\: mixed, timestamp\\?\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<500, array\\{success\\: bool, error\\: string, object_id\\: int\\}, array\\{\\}\\>\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Variable \\$collectionName on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Variable \\$limit on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Variable \\$name on left side of \\?\\? always exists and is not nullable\\.$#" - count: 2 - path: lib/Controller/SolrController.php - - - - message: "#^Variable \\$objectId on left side of \\?\\? always exists and is not nullable\\.$#" - count: 2 - path: lib/Controller/SolrController.php - - - - message: "#^Variable \\$offset on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Controller/SolrController.php - - - - message: "#^Variable \\$query on left side of \\?\\? always exists and is not nullable\\.$#" - count: 4 - path: lib/Controller/SolrController.php + path: lib/Controller/SourcesController.php - - message: "#^Variable \\$sourceCollection on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Source, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 - path: lib/Controller/SolrController.php + path: lib/Controller/SourcesController.php - - message: "#^Variable \\$targetCollection on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{\\}, array\\{\\}\\>\\.$#" count: 1 - path: lib/Controller/SolrController.php + path: lib/Controller/SourcesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:create\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Source, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Source, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SourcesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:destroy\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\\\>\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SourcesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:index\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\\\}, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Source, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SourcesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Source, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Source, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\SourcesController\\:\\:update\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, OCA\\\\OpenRegister\\\\Db\\\\Source, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<403, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/SourcesController.php @@ -2481,7 +2259,12 @@ parameters: path: lib/Controller/TablesController.php - - message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\TagsController\\:\\:getAllTags\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\TagsController\\:\\:getAllTags\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<401, array\\{error\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/TagsController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\TagsController\\:\\:getAllTags\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\, array\\>\\|OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<401, array\\{error\\: string\\}, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<401, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" count: 1 path: lib/Controller/TagsController.php @@ -2690,6 +2473,11 @@ parameters: count: 1 path: lib/Db/AgentMapper.php + - + message: "#^Parameter \\$entity of method OCP\\\\AppFramework\\\\Db\\\\QBMapper\\\\:\\:insert\\(\\) expects OCA\\\\OpenRegister\\\\Db\\\\AnonymisationLog, OCP\\\\AppFramework\\\\Db\\\\Entity given\\.$#" + count: 1 + path: lib/Db/AnonymisationLogMapper.php + - message: "#^Variable \\$groups on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 @@ -2750,11 +2538,6 @@ parameters: count: 1 path: lib/Db/AuditTrailMapper.php - - - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\AuditTrail\\:\\:setSession\\(\\)\\.$#" - count: 1 - path: lib/Db/AuditTrailMapper.php - - message: "#^Call to an undefined method OCP\\\\AppFramework\\\\Db\\\\Entity\\:\\:setSize\\(\\)\\.$#" count: 1 @@ -3041,7 +2824,7 @@ parameters: path: lib/Db/MagicMapper.php - - message: "#^Method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\) invoked with 5 parameters, 1 required\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\) invoked with 4 parameters, 1 required\\.$#" count: 1 path: lib/Db/MagicMapper.php @@ -3060,11 +2843,6 @@ parameters: count: 1 path: lib/Db/MagicMapper.php - - - message: "#^Offset 'gemmaType' on array in isset\\(\\) always exists and is not nullable\\.$#" - count: 1 - path: lib/Db/MagicMapper.php - - message: "#^Offset 'handling' on array\\{\\} on left side of \\?\\? does not exist\\.$#" count: 1 @@ -3107,22 +2885,22 @@ parameters: - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" - count: 13 + count: 15 path: lib/Db/MagicMapper.php - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 15 + count: 16 path: lib/Db/MagicMapper.php - message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" - count: 11 + count: 13 path: lib/Db/MagicMapper.php - message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 13 + count: 14 path: lib/Db/MagicMapper.php - @@ -3136,14 +2914,19 @@ parameters: path: lib/Db/MagicMapper/MagicBulkHandler.php - - message: "#^Property OCA\\\\OpenRegister\\\\Db\\\\MagicMapper\\\\MagicFacetHandler\\:\\:\\$uuidLabelCache is never read, only written\\.$#" + message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" count: 1 - path: lib/Db/MagicMapper/MagicFacetHandler.php + path: lib/Db/MagicMapper/MagicBulkHandler.php - - message: "#^Property OCA\\\\OpenRegister\\\\Db\\\\MagicMapper\\\\MagicRbacHandler\\:\\:\\$appConfig is never read, only written\\.$#" + message: "#^Variable \\$filteredChunk in empty\\(\\) always exists and is not falsy\\.$#" count: 1 - path: lib/Db/MagicMapper/MagicRbacHandler.php + path: lib/Db/MagicMapper/MagicBulkHandler.php + + - + message: "#^Property OCA\\\\OpenRegister\\\\Db\\\\MagicMapper\\\\MagicFacetHandler\\:\\:\\$uuidLabelCache is never read, only written\\.$#" + count: 1 + path: lib/Db/MagicMapper/MagicFacetHandler.php - message: "#^Property OCA\\\\OpenRegister\\\\Db\\\\MagicMapper\\\\MagicRbacHandler\\:\\:\\$userManager is never read, only written\\.$#" @@ -3330,11 +3113,6 @@ parameters: count: 2 path: lib/Db/OrganisationMapper.php - - - message: "#^Strict comparison using \\=\\=\\= between bool\\|float\\|int\\|string and null will always evaluate to false\\.$#" - count: 1 - path: lib/Db/RealtimeEventMapper.php - - message: "#^Call to an undefined static method OCP\\\\AppFramework\\\\Db\\\\Entity\\:\\:setSchemas\\(\\)\\.$#" count: 1 @@ -3371,7 +3149,17 @@ parameters: path: lib/Db/RegisterMapper.php - - message: "#^Method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\) invoked with 5 parameters, 1 required\\.$#" + message: "#^Offset 2 on array\\{string, numeric\\-string, ''\\|numeric\\-string, ''\\|numeric\\-string, string\\} on left side of \\?\\? always exists and is not nullable\\.$#" + count: 1 + path: lib/Db/RegisterMapper.php + + - + message: "#^Offset 3 on array\\{string, numeric\\-string, ''\\|numeric\\-string, ''\\|numeric\\-string, string\\} on left side of \\?\\? always exists and is not nullable\\.$#" + count: 1 + path: lib/Db/RegisterMapper.php + + - + message: "#^Offset 4 on array\\{string, numeric\\-string, ''\\|numeric\\-string, ''\\|numeric\\-string, string\\} on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 path: lib/Db/RegisterMapper.php @@ -3391,7 +3179,7 @@ parameters: path: lib/Db/RegisterMapper.php - - message: "#^Unknown parameter \\$_extend in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + message: "#^Unknown parameter \\$_extend in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" count: 1 path: lib/Db/RegisterMapper.php @@ -3400,14 +3188,19 @@ parameters: count: 3 path: lib/Db/RegisterMapper.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Db/RegisterMapper.php + - message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" count: 2 path: lib/Db/RegisterMapper.php - - message: "#^Unknown parameter \\$published in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" - count: 2 + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 path: lib/Db/RegisterMapper.php - @@ -3425,6 +3218,11 @@ parameters: count: 1 path: lib/Db/Schema.php + - + message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\SchemaChangelog\\:\\:hydrate\\(\\)\\.$#" + count: 1 + path: lib/Db/SchemaChangelogMapper.php + - message: "#^Call to an undefined method OCP\\\\AppFramework\\\\Db\\\\Entity\\:\\:getOrganisation\\(\\)\\.$#" count: 1 @@ -3496,9 +3294,14 @@ parameters: path: lib/Db/SchemaMapper.php - - message: "#^Unknown parameter \\$published in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\SchemaRunEntry\\:\\:hydrate\\(\\)\\.$#" count: 1 - path: lib/Db/SchemaMapper.php + path: lib/Db/SchemaRunEntryMapper.php + + - + message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\SchemaRun\\:\\:hydrate\\(\\)\\.$#" + count: 1 + path: lib/Db/SchemaRunMapper.php - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\SearchTrail\\:\\:setExecutionType\\(\\)\\.$#" @@ -3585,6 +3388,21 @@ parameters: count: 1 path: lib/Db/SourceMapper.php + - + message: "#^Parameter \\$entity of method OCP\\\\AppFramework\\\\Db\\\\QBMapper\\\\:\\:insert\\(\\) expects OCA\\\\OpenRegister\\\\Db\\\\FederatedShare, OCP\\\\AppFramework\\\\Db\\\\Entity given\\.$#" + count: 1 + path: lib/Db/FederatedShareMapper.php + + - + message: "#^Parameter \\$entity of method OCP\\\\AppFramework\\\\Db\\\\QBMapper\\\\:\\:update\\(\\) expects OCA\\\\OpenRegister\\\\Db\\\\FederatedShare, OCP\\\\AppFramework\\\\Db\\\\Entity given\\.$#" + count: 1 + path: lib/Db/FederatedShareMapper.php + + - + message: "#^Strict comparison using \\=\\=\\= between non\\-empty\\-string and null will always evaluate to false\\.$#" + count: 1 + path: lib/Db/TranslationMapper.php + - message: "#^Call to an undefined method OCP\\\\AppFramework\\\\Db\\\\Entity\\:\\:getOrganisation\\(\\)\\.$#" count: 1 @@ -3675,16 +3493,6 @@ parameters: count: 1 path: lib/Db/WebhookMapper.php - - - message: "#^Property OCA\\\\OpenRegister\\\\Event\\\\ConfigurationUpdatedEvent\\:\\:\\$newConfiguration is never read, only written\\.$#" - count: 1 - path: lib/Event/ConfigurationUpdatedEvent.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Event\\\\ConfigurationUpdatedEvent\\:\\:\\$oldConfiguration is never read, only written\\.$#" - count: 1 - path: lib/Event/ConfigurationUpdatedEvent.php - - message: "#^Property OCA\\\\OpenRegister\\\\Event\\\\ViewUpdatedEvent\\:\\:\\$newView is never read, only written\\.$#" count: 1 @@ -3735,6 +3543,11 @@ parameters: count: 1 path: lib/Listener/MailAppScriptListener.php + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 2 + path: lib/Listener/NotificationDedupeAnnotationSyncListener.php + - message: "#^Property OCA\\\\OpenRegister\\\\Listener\\\\NotifyPushListener\\:\\:\\$appManager is never read, only written\\.$#" count: 1 @@ -3765,6 +3578,41 @@ parameters: count: 1 path: lib/Listener/ObjectChangeListener.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Listener/QualityScoreOnSaveListener.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Listener/SurvivorshipRecomputeListener.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Listener/SourceRecordChangeListener.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Survivorship/SourceRecordResolver.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Survivorship/SourceRecordResolver.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Listener/SourceRecordChangeListener.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Listener/SourceRecordChangeListener.php + - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" count: 1 @@ -3865,6 +3713,11 @@ parameters: count: 1 path: lib/Service/ActionService.php + - + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Aggregation\\\\AggregationRunner\\:\\:\\$logger is never read, only written\\.$#" + count: 1 + path: lib/Service/Aggregation/AggregationRunner.php + - message: "#^Strict comparison using \\=\\=\\= between int and null will always evaluate to false\\.$#" count: 1 @@ -4025,6 +3878,16 @@ parameters: count: 2 path: lib/Service/CalendarLinkService.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Db\\\\CaseToken\\:\\:getToken\\(\\) invoked with 0 parameters, 1 required\\.$#" + count: 2 + path: lib/Service/CaseTokenService.php + + - + message: "#^Result of method OCA\\\\OpenRegister\\\\Db\\\\CaseToken\\:\\:getToken\\(\\) \\(void\\) is used\\.$#" + count: 2 + path: lib/Service/CaseTokenService.php + - message: "#^Access to an undefined property LLPhant\\\\OpenAIConfig\\:\\:\\$temperature\\.$#" count: 2 @@ -4090,6 +3953,11 @@ parameters: count: 1 path: lib/Service/ChatService.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Service\\\\Configuration\\\\ExportHandler\\:\\:exportSchema\\(\\) should return array\\{uri\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, description\\: string\\|null, version\\: string\\|null, summary\\: string\\|null, icon\\: string\\|null, required\\: array, \\.\\.\\.\\} but returns array\\{uri\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, description\\: string\\|null, version\\: string\\|null, summary\\: string\\|null, icon\\: string\\|null, required\\: array, \\.\\.\\.\\}\\.$#" + count: 1 + path: lib/Service/Configuration/ExportHandler.php + - message: "#^Constant OCA\\\\OpenRegister\\\\Service\\\\Configuration\\\\GitHubHandler\\:\\:SEARCH_RATE_LIMIT is unused\\.$#" count: 1 @@ -4190,6 +4058,11 @@ parameters: count: 1 path: lib/Service/Configuration/ImportHandler.php + - + message: "#^Offset 'id' on array\\{@self\\: array\\{id\\: null, slug\\: string\\|null, name\\: string\\|null, description\\: int\\|string, summary\\: string\\|null, image\\: string\\|null, uri\\: string\\|null, version\\: string\\|null, \\.\\.\\.\\}\\} on left side of \\?\\? does not exist\\.$#" + count: 1 + path: lib/Service/Configuration/ImportHandler.php + - message: "#^Offset 'id' on array\\{@self\\: array\\{id\\: string\\|null, slug\\: string\\|null, name\\: string\\|null, description\\: int\\|string, summary\\: string\\|null, image\\: string\\|null, uri\\: string\\|null, version\\: null, \\.\\.\\.\\}\\}\\|array\\{@self\\: array\\{id\\: string\\|null, slug\\: string\\|null, name\\: string\\|null, description\\: int\\|string, summary\\: string\\|null, image\\: string\\|null, uri\\: string\\|null, version\\: string, \\.\\.\\.\\}\\} on left side of \\?\\? does not exist\\.$#" count: 1 @@ -4236,12 +4109,7 @@ parameters: path: lib/Service/Configuration/ImportHandler.php - - message: "#^Strict comparison using \\=\\=\\= between non\\-empty\\-string and false will always evaluate to false\\.$#" - count: 1 - path: lib/Service/Configuration/ImportHandler.php - - - - message: "#^Unknown parameter \\$_extend in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" count: 1 path: lib/Service/Configuration/ImportHandler.php @@ -4252,7 +4120,7 @@ parameters: - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" - count: 6 + count: 7 path: lib/Service/Configuration/ImportHandler.php - @@ -4262,12 +4130,12 @@ parameters: - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 5 + count: 6 path: lib/Service/Configuration/ImportHandler.php - message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" - count: 4 + count: 5 path: lib/Service/Configuration/ImportHandler.php - @@ -4277,7 +4145,7 @@ parameters: - message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 2 + count: 3 path: lib/Service/Configuration/ImportHandler.php - @@ -4285,16 +4153,6 @@ parameters: count: 1 path: lib/Service/Configuration/ImportHandler.php - - - message: "#^Unknown parameter \\$published in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" - count: 1 - path: lib/Service/Configuration/ImportHandler.php - - - - message: "#^Unknown parameter \\$published in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 1 - path: lib/Service/Configuration/ImportHandler.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 @@ -4315,6 +4173,11 @@ parameters: count: 1 path: lib/Service/Configuration/ImportHandler.php + - + message: "#^Variable \\$objectSlug on left side of \\?\\? always exists and is not nullable\\.$#" + count: 1 + path: lib/Service/Configuration/ImportHandler.php + - message: "#^Variable \\$version on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 @@ -4555,11 +4418,6 @@ parameters: count: 1 path: lib/Service/EmailLinkService.php - - - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\EmailLinkMapper\\:\\:find\\(\\)\\.$#" - count: 1 - path: lib/Service/EmailService.php - - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" count: 1 @@ -4680,6 +4538,11 @@ parameters: count: 1 path: lib/Service/ExportService.php + - + message: "#^Variable \\$output on left side of \\?\\? always exists and is not nullable\\.$#" + count: 1 + path: lib/Service/ExportService.php + - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\File\\\\CreateFileHandler\\:\\:\\$rootFolder is never read, only written\\.$#" count: 1 @@ -4700,11 +4563,6 @@ parameters: count: 1 path: lib/Service/File/DocumentProcessingHandler.php - - - message: "#^Call to an undefined method OCP\\\\Files\\\\Node\\:\\:getContent\\(\\)\\.$#" - count: 1 - path: lib/Service/File/DocumentProcessingHandler.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\File\\\\DocumentProcessingHandler\\:\\:\\$fileService is never read, only written\\.$#" count: 1 @@ -4870,6 +4728,11 @@ parameters: count: 3 path: lib/Service/File/FolderManagementHandler.php + - + message: "#^PHPDoc tag @return with type void is incompatible with native type array\\.$#" + count: 1 + path: lib/Service/File/Pdf/PdfTextReplacer.php + - message: "#^Cannot call method getCategory\\(\\) on array\\\\.$#" count: 1 @@ -4956,14 +4819,24 @@ parameters: path: lib/Service/FileService.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\FileService\\:\\:\\$userSession is never read, only written\\.$#" + message: "#^Call to an undefined method OCP\\\\IDBConnection\\:\\:getInner\\(\\)\\.$#" count: 1 - path: lib/Service/FileService.php + path: lib/Service/FileSidebarService.php - - message: "#^Call to an undefined method OCP\\\\IDBConnection\\:\\:getInner\\(\\)\\.$#" + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" count: 1 - path: lib/Service/FileSidebarService.php + path: lib/Service/Flow/FlowActionService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Flow/FlowActionService.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Flow/FlowActionService.php - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\FlowLinkService\\:\\:\\$container is never read, only written\\.$#" @@ -5056,164 +4929,64 @@ parameters: path: lib/Service/ImportService.php - - message: "#^Offset 'id' does not exist on array\\{_text\\: string\\|false\\}\\.$#" - count: 2 - path: lib/Service/Index/Backends/Elasticsearch/ElasticsearchDocumentIndexer.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Index\\\\Backends\\\\Elasticsearch\\\\ElasticsearchHttpClient\\:\\:\\$settingsService is never read, only written\\.$#" - count: 1 - path: lib/Service/Index/Backends/Elasticsearch/ElasticsearchHttpClient.php - - - - message: "#^Method OCA\\\\OpenRegister\\\\Service\\\\Index\\\\Backends\\\\ElasticsearchBackend\\:\\:searchObjectsPaginated\\(\\) should return array\\{total\\: mixed, results\\: array, page\\: 1, limit\\: mixed\\} but returns array\\{total\\: mixed, results\\: array\\, page\\: 1, limit\\: mixed\\}\\.$#" - count: 1 - path: lib/Service/Index/Backends/ElasticsearchBackend.php - - - - message: "#^Offset 'fq' on array\\{q\\: mixed, start\\: int, rows\\: int, sort\\?\\: string, fl\\?\\: mixed\\} on left side of \\?\\? does not exist\\.$#" - count: 1 - path: lib/Service/Index/Backends/Solr/SolrQueryExecutor.php - - - - message: "#^Offset 'success' on array\\{success\\: bool, message\\: string, collection\\?\\: string\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Service/Index/Backends/SolrBackend.php - - - - message: "#^Call to an undefined method OCA\\\\OpenRegister\\\\Db\\\\Schema\\:\\:getSearchable\\(\\)\\.$#" - count: 2 - path: lib/Service/Index/BulkIndexer.php - - - - message: "#^Offset 'skipped_non…' on array\\{skipped_non_searchable\\: int\\<0, max\\>\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Service/Index/BulkIndexer.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Index\\\\BulkIndexer\\:\\:\\$db is never read, only written\\.$#" - count: 1 - path: lib/Service/Index/BulkIndexer.php - - - - message: "#^Variable \\$batchCount on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Service/Index/BulkIndexer.php - - - - message: "#^Variable \\$totalIndexed on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Service/Index/BulkIndexer.php - - - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 4 - path: lib/Service/Index/DocumentBuilder.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Index\\\\ObjectHandler\\:\\:\\$registerMapper is never read, only written\\.$#" - count: 1 - path: lib/Service/Index/ObjectHandler.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Index\\\\ObjectHandler\\:\\:\\$schemaMapper is never read, only written\\.$#" - count: 1 - path: lib/Service/Index/ObjectHandler.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Index\\\\SchemaHandler\\:\\:\\$config is never read, only written\\.$#" - count: 1 - path: lib/Service/Index/SchemaHandler.php - - - - message: "#^Offset 'cache_refresh' on array\\{success\\: true, operations\\: array\\{configset_list_refresh\\: array\\{name\\: 'configset_list…', description\\: 'List ConfigSets API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}, cluster_status_sync\\: array\\{name\\: 'cluster_status_sync', description\\: 'Cluster Status API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}\\}, successful_operations\\: 0\\|1\\|2, total_operations\\: 2, cluster_sync\\: 'failed'\\|'triggered', cache_refresh\\: 'failed'\\|'triggered', error\\: 'All propagation…'\\|null, summary\\: array\\{configset_list_refresh\\: 'failed'\\|'success', cluster_status_sync\\: 'failed'\\|'success'\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Service/Index/SetupHandler.php - - - - message: "#^Offset 'cluster_sync' on array\\{success\\: true, operations\\: array\\{configset_list_refresh\\: array\\{name\\: 'configset_list…', description\\: 'List ConfigSets API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}, cluster_status_sync\\: array\\{name\\: 'cluster_status_sync', description\\: 'Cluster Status API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}\\}, successful_operations\\: 0\\|1\\|2, total_operations\\: 2, cluster_sync\\: 'failed'\\|'triggered', cache_refresh\\: 'failed'\\|'triggered', error\\: 'All propagation…'\\|null, summary\\: array\\{configset_list_refresh\\: 'failed'\\|'success', cluster_status_sync\\: 'failed'\\|'success'\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: lib/Service/Index/SetupHandler.php - - - - message: "#^Offset 'host' on array\\{endpoint\\: mixed, collection\\: mixed, username\\: mixed, password\\: mixed, timeout\\: mixed\\} on left side of \\?\\? does not exist\\.$#" - count: 1 - path: lib/Service/Index/SetupHandler.php - - - - message: "#^Offset 'operations' on array\\{success\\: false, operations\\: array\\{configset_list_refresh\\: array\\{name\\: 'configset_list…', description\\: 'List ConfigSets API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}, cluster_status_sync\\: array\\{name\\: 'cluster_status_sync', description\\: 'Cluster Status API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}\\}, successful_operations\\: 0\\|1\\|2, total_operations\\: 2, cluster_sync\\: 'failed'\\|'triggered', cache_refresh\\: 'failed'\\|'triggered', error\\: 'All propagation…'\\|null, summary\\: array\\{configset_list_refresh\\: 'failed'\\|'success', cluster_status_sync\\: 'failed'\\|'success'\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 2 - path: lib/Service/Index/SetupHandler.php - - - - message: "#^Offset 'operations' on array\\{success\\: true, operations\\: array\\{configset_list_refresh\\: array\\{name\\: 'configset_list…', description\\: 'List ConfigSets API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}, cluster_status_sync\\: array\\{name\\: 'cluster_status_sync', description\\: 'Cluster Status API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}\\}, successful_operations\\: 0\\|1\\|2, total_operations\\: 2, cluster_sync\\: 'failed'\\|'triggered', cache_refresh\\: 'failed'\\|'triggered', error\\: 'All propagation…'\\|null, summary\\: array\\{configset_list_refresh\\: 'failed'\\|'success', cluster_status_sync\\: 'failed'\\|'success'\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Unknown parameter \\$filters in call to method OCA\\\\OpenRegister\\\\Db\\\\AuditTrailMapper\\:\\:findAll\\(\\)\\.$#" count: 2 - path: lib/Service/Index/SetupHandler.php - - - - message: "#^Offset 'path' on array\\{endpoint\\: mixed, collection\\: mixed, username\\: mixed, password\\: mixed, timeout\\: mixed\\} on left side of \\?\\? does not exist\\.$#" - count: 1 - path: lib/Service/Index/SetupHandler.php + path: lib/Service/Integration/BuiltinProviders/AuditTrailProvider.php - - message: "#^Offset 'port' on array\\{endpoint\\: mixed, collection\\: mixed, username\\: mixed, password\\: mixed, timeout\\: mixed\\} on left side of \\?\\? does not exist\\.$#" + message: "#^Call to function array_is_list\\(\\) with array\\ will always evaluate to false\\.$#" count: 1 - path: lib/Service/Index/SetupHandler.php + path: lib/Service/Integration/Providers/BrpPersoonProvider.php - - message: "#^Offset 'scheme' on array\\{endpoint\\: mixed, collection\\: mixed, username\\: mixed, password\\: mixed, timeout\\: mixed\\} on left side of \\?\\? does not exist\\.$#" + message: "#^Offset 'body' on array\\{body\\: array\\, meta\\: array\\{status\\: int, durationMs\\: int, correlationId\\: string\\|null, headers\\: array\\\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 - path: lib/Service/Index/SetupHandler.php + path: lib/Service/Integration/Providers/BrpPersoonProvider.php - - message: "#^Offset 'successful…' on array\\{success\\: false, operations\\: array\\{configset_list_refresh\\: array\\{name\\: 'configset_list…', description\\: 'List ConfigSets API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}, cluster_status_sync\\: array\\{name\\: 'cluster_status_sync', description\\: 'Cluster Status API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}\\}, successful_operations\\: 0\\|1\\|2, total_operations\\: 2, cluster_sync\\: 'failed'\\|'triggered', cache_refresh\\: 'failed'\\|'triggered', error\\: 'All propagation…'\\|null, summary\\: array\\{configset_list_refresh\\: 'failed'\\|'success', cluster_status_sync\\: 'failed'\\|'success'\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Offset 'meta' on array\\{body\\: array\\, meta\\: array\\{status\\: int, durationMs\\: int, correlationId\\: string\\|null, headers\\: array\\\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 - path: lib/Service/Index/SetupHandler.php + path: lib/Service/Integration/Providers/BrpPersoonProvider.php - - message: "#^Offset 'successful…' on array\\{success\\: true, operations\\: array\\{configset_list_refresh\\: array\\{name\\: 'configset_list…', description\\: 'List ConfigSets API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}, cluster_status_sync\\: array\\{name\\: 'cluster_status_sync', description\\: 'Cluster Status API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}\\}, successful_operations\\: 0\\|1\\|2, total_operations\\: 2, cluster_sync\\: 'failed'\\|'triggered', cache_refresh\\: 'failed'\\|'triggered', error\\: 'All propagation…'\\|null, summary\\: array\\{configset_list_refresh\\: 'failed'\\|'success', cluster_status_sync\\: 'failed'\\|'success'\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" count: 1 - path: lib/Service/Index/SetupHandler.php + path: lib/Service/Integration/Providers/BrpPersoonProvider.php - - message: "#^Offset 'total_operations' on array\\{success\\: false, operations\\: array\\{configset_list_refresh\\: array\\{name\\: 'configset_list…', description\\: 'List ConfigSets API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}, cluster_status_sync\\: array\\{name\\: 'cluster_status_sync', description\\: 'Cluster Status API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}\\}, successful_operations\\: 0\\|1\\|2, total_operations\\: 2, cluster_sync\\: 'failed'\\|'triggered', cache_refresh\\: 'failed'\\|'triggered', error\\: 'All propagation…'\\|null, summary\\: array\\{configset_list_refresh\\: 'failed'\\|'success', cluster_status_sync\\: 'failed'\\|'success'\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Offset 'results' on array\\{results\\: array, total\\: int\\} on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 - path: lib/Service/Index/SetupHandler.php + path: lib/Service/Integration/Providers/ContactsProvider.php - - message: "#^Offset 'total_operations' on array\\{success\\: true, operations\\: array\\{configset_list_refresh\\: array\\{name\\: 'configset_list…', description\\: 'List ConfigSets API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}, cluster_status_sync\\: array\\{name\\: 'cluster_status_sync', description\\: 'Cluster Status API…', url\\: string\\|null, status\\: 'failed'\\|'success', http_status\\: int\\|null, response_size\\: int\\<0, max\\>, error\\: string\\|null\\}\\}, successful_operations\\: 0\\|1\\|2, total_operations\\: 2, cluster_sync\\: 'failed'\\|'triggered', cache_refresh\\: 'failed'\\|'triggered', error\\: 'All propagation…'\\|null, summary\\: array\\{configset_list_refresh\\: 'failed'\\|'success', cluster_status_sync\\: 'failed'\\|'success'\\}\\} on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Integration\\\\Providers\\\\DeckProvider\\:\\:\\$appManager is never read, only written\\.$#" count: 1 - path: lib/Service/Index/SetupHandler.php + path: lib/Service/Integration/Providers/DeckProvider.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Index\\\\WarmupHandler\\:\\:\\$bulkIndexer is never read, only written\\.$#" + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Integration\\\\Providers\\\\EmailProvider\\:\\:\\$appManager is never read, only written\\.$#" count: 1 - path: lib/Service/Index/WarmupHandler.php + path: lib/Service/Integration/Providers/EmailProvider.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Index\\\\WarmupHandler\\:\\:\\$db is never read, only written\\.$#" + message: "#^Call to function array_is_list\\(\\) with array\\ will always evaluate to false\\.$#" count: 1 - path: lib/Service/Index/WarmupHandler.php - - - - message: "#^Unknown parameter \\$filters in call to method OCA\\\\OpenRegister\\\\Db\\\\AuditTrailMapper\\:\\:findAll\\(\\)\\.$#" - count: 2 - path: lib/Service/Integration/BuiltinProviders/AuditTrailProvider.php + path: lib/Service/Integration/Providers/KvkProvider.php - - message: "#^Offset 'results' on array\\{results\\: array, total\\: int\\} on left side of \\?\\? always exists and is not nullable\\.$#" + message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" count: 1 - path: lib/Service/Integration/Providers/ContactsProvider.php + path: lib/Service/Integration/Providers/KvkProvider.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Integration\\\\Providers\\\\DeckProvider\\:\\:\\$appManager is never read, only written\\.$#" + message: "#^Call to function array_is_list\\(\\) with array\\ will always evaluate to false\\.$#" count: 1 - path: lib/Service/Integration/Providers/DeckProvider.php + path: lib/Service/Integration/Providers/OpenCorporatesProvider.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Integration\\\\Providers\\\\EmailProvider\\:\\:\\$appManager is never read, only written\\.$#" + message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" count: 1 - path: lib/Service/Integration/Providers/EmailProvider.php + path: lib/Service/Integration/Providers/OpenCorporatesProvider.php - message: "#^Call to function array_is_list\\(\\) with array\\ will always evaluate to false\\.$#" @@ -5245,6 +5018,36 @@ parameters: count: 1 path: lib/Service/Integration/Providers/SharesProvider.php + - + message: "#^Unknown parameter \\$label in call to method stdClass\\:\\:mint\\(\\)\\.$#" + count: 1 + path: lib/Service/Integration/Providers/SharesProvider.php + + - + message: "#^Unknown parameter \\$objectUuid in call to method stdClass\\:\\:mint\\(\\)\\.$#" + count: 1 + path: lib/Service/Integration/Providers/SharesProvider.php + + - + message: "#^Unknown parameter \\$registerId in call to method stdClass\\:\\:mint\\(\\)\\.$#" + count: 1 + path: lib/Service/Integration/Providers/SharesProvider.php + + - + message: "#^Unknown parameter \\$schemaId in call to method stdClass\\:\\:mint\\(\\)\\.$#" + count: 1 + path: lib/Service/Integration/Providers/SharesProvider.php + + - + message: "#^Unknown parameter \\$ttlSeconds in call to method stdClass\\:\\:mint\\(\\)\\.$#" + count: 1 + path: lib/Service/Integration/Providers/SharesProvider.php + + - + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" + count: 1 + path: lib/Service/Integration/Providers/TimeProvider.php + - message: "#^Call to function array_is_list\\(\\) with array\\ will always evaluate to false\\.$#" count: 1 @@ -5260,6 +5063,11 @@ parameters: count: 1 path: lib/Service/Integration/Providers/XwikiProvider.php + - + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" + count: 1 + path: lib/Service/JsonLd/JsonLdContextService.php + - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" count: 1 @@ -5360,6 +5168,11 @@ parameters: count: 2 path: lib/Service/Mcp/McpToolsService.php + - + message: "#^Offset 'name' on array\\{id\\: string, name\\: string, description\\: string, inputSchema\\: array\\} on left side of \\?\\? always exists and is not nullable\\.$#" + count: 1 + path: lib/Service/Mcp/McpToolsService.php + - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 @@ -5405,11 +5218,21 @@ parameters: count: 2 path: lib/Service/Notification/AnnotationNotificationDispatcher.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Notification/AnnotationNotificationDispatcher.php + - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" count: 1 path: lib/Service/Notification/AnnotationNotificationDispatcher.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Notification/AnnotationNotificationDispatcher.php + - message: "#^Class OCA\\\\OpenRegister\\\\Service\\\\NotificationService has an uninitialized readonly property \\$groupManager\\. Assign it in the constructor\\.$#" count: 1 @@ -5495,21 +5318,11 @@ parameters: count: 1 path: lib/Service/Object/AuditHandler.php - - - message: "#^Method OCA\\\\OpenRegister\\\\Service\\\\Object\\\\CacheHandler\\:\\:extractDynamicFieldsFromObject\\(\\) is unused\\.$#" - count: 1 - path: lib/Service/Object/CacheHandler.php - - message: "#^Offset 'object' on array\\{object\\: OCA\\\\OpenRegister\\\\Db\\\\ObjectEntity, register\\: OCA\\\\OpenRegister\\\\Db\\\\Register\\|null, schema\\: OCA\\\\OpenRegister\\\\Db\\\\Schema\\|null\\} on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 path: lib/Service/Object/CacheHandler.php - - - message: "#^Parameter \\$dateString of method OCA\\\\OpenRegister\\\\Service\\\\Object\\\\CacheHandler\\:\\:formatDateForSolr\\(\\) expects string, mixed given\\.$#" - count: 1 - path: lib/Service/Object/CacheHandler.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Object\\\\CacheHandler\\:\\:\\$nameDistributedCache \\(OCP\\\\IMemcache\\|null\\) does not accept OCP\\\\ICache\\.$#" count: 1 @@ -5521,7 +5334,7 @@ parameters: path: lib/Service/Object/CacheHandler.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Object\\\\CacheHandler\\:\\:\\$userSession is never read, only written\\.$#" + message: "#^Using nullsafe method call on non\\-nullable type OCP\\\\IUserSession\\. Use \\-\\> instead\\.$#" count: 1 path: lib/Service/Object/CacheHandler.php @@ -5722,17 +5535,17 @@ parameters: - message: "#^Offset 'etag' on array\\{fileid\\: int, storage\\: int, path\\: string, path_hash\\: string, parent\\: int, name\\: string, mimetype\\: string, mimepart\\: string, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 2 + count: 1 path: lib/Service/Object/RenderObject.php - message: "#^Offset 'mimetype' on array\\{fileid\\: int, storage\\: int, path\\: string, path_hash\\: string, parent\\: int, name\\: string, mimetype\\: string, mimepart\\: string, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 2 + count: 1 path: lib/Service/Object/RenderObject.php - message: "#^Offset 'mtime' on array\\{fileid\\: int, storage\\: int, path\\: string, path_hash\\: string, parent\\: int, name\\: string, mimetype\\: string, mimepart\\: string, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" - count: 2 + count: 1 path: lib/Service/Object/RenderObject.php - @@ -5745,6 +5558,11 @@ parameters: count: 1 path: lib/Service/Object/RenderObject.php + - + message: "#^Strict comparison using \\=\\=\\= between int\\<1, max\\> and 0 will always evaluate to false\\.$#" + count: 1 + path: lib/Service/Object/RenderObject.php + - message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#" count: 1 @@ -5770,26 +5588,6 @@ parameters: count: 1 path: lib/Service/Object/RenderObject.php - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Object\\\\RevertHandler\\:\\:\\$auditTrailMapper is never written, only read\\.$#" - count: 1 - path: lib/Service/Object/RevertHandler.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Object\\\\RevertHandler\\:\\:\\$container is never written, only read\\.$#" - count: 1 - path: lib/Service/Object/RevertHandler.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Object\\\\RevertHandler\\:\\:\\$eventDispatcher is never written, only read\\.$#" - count: 1 - path: lib/Service/Object/RevertHandler.php - - - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Object\\\\RevertHandler\\:\\:\\$objectEntityMapper is never written, only read\\.$#" - count: 1 - path: lib/Service/Object/RevertHandler.php - - message: "#^Dead catch \\- Exception is never thrown in the try block\\.$#" count: 1 @@ -5825,16 +5623,6 @@ parameters: count: 2 path: lib/Service/Object/SaveObject.php - - - message: "#^Unknown parameter \\$published in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" - count: 1 - path: lib/Service/Object/SaveObject.php - - - - message: "#^Unknown parameter \\$published in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 2 - path: lib/Service/Object/SaveObject.php - - message: "#^Using nullsafe method call on non\\-nullable type OCA\\\\OpenRegister\\\\Db\\\\Register\\. Use \\-\\> instead\\.$#" count: 4 @@ -6070,6 +5858,11 @@ parameters: count: 1 path: lib/Service/Object/ValidationHandler.php + - + message: "#^Method OCA\\\\OpenRegister\\\\Service\\\\ObjectService\\:\\:ensureObjectFolder\\(\\) never returns int so it can be removed from the return type\\.$#" + count: 1 + path: lib/Service/ObjectService.php + - message: "#^PHPDoc tag @throws with type OCP\\\\AppFramework\\\\Http\\\\ContentSecurityPolicy is not subtype of Throwable$#" count: 1 @@ -6150,9 +5943,14 @@ parameters: count: 4 path: lib/Service/ObjectService.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Service/ObjectService.php + - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 4 + count: 5 path: lib/Service/ObjectService.php - @@ -6165,16 +5963,6 @@ parameters: count: 4 path: lib/Service/ObjectService.php - - - message: "#^Unknown parameter \\$published in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" - count: 3 - path: lib/Service/ObjectService.php - - - - message: "#^Unknown parameter \\$published in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" - count: 3 - path: lib/Service/ObjectService.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 @@ -6206,22 +5994,52 @@ parameters: path: lib/Service/PollLinkService.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\RegisterService\\:\\:\\$schemaMapper is never read, only written\\.$#" + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" count: 1 - path: lib/Service/RegisterService.php + path: lib/Service/ProcessingLogService.php - - message: "#^Result of \\|\\| is always true\\.$#" + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" count: 1 - path: lib/Service/RegisterService.php + path: lib/Service/ProcessingLogService.php - - message: "#^Unknown parameter \\$_extend in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" count: 1 - path: lib/Service/RegisterService.php + path: lib/Service/ProcessingLogService.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/ProcessingLogService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Quality/DuplicateDetectionService.php + + - + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" + count: 1 + path: lib/Service/Quality/QualityStatisticsService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Quality/QualityStatisticsService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/RegisterResolverService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/RegisterResolverService.php - - message: "#^Unknown parameter \\$_extend in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" + message: "#^Result of \\|\\| is always true\\.$#" count: 1 path: lib/Service/RegisterService.php @@ -6235,6 +6053,11 @@ parameters: count: 1 path: lib/Service/RegisterService.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/RegisterService.php + - message: "#^Unknown parameter \\$filters in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" count: 1 @@ -6290,6 +6113,21 @@ parameters: count: 1 path: lib/Service/RetentionService.php + - + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" + count: 1 + path: lib/Service/Schema/SchemaMigrationPlanner.php + + - + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Schema\\\\SchemaMigrationService\\:\\:\\$logger is never read, only written\\.$#" + count: 1 + path: lib/Service/Schema/SchemaMigrationService.php + + - + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Schema\\\\SchemaRevalidationService\\:\\:\\$logger is never read, only written\\.$#" + count: 1 + path: lib/Service/Schema/SchemaRevalidationService.php + - message: "#^Regex pattern is invalid\\: Unknown modifier '\\\\' in pattern\\: /\\^\\[\\^\\<\\>\\:\"/\\\\\\|\\?\\*\\]\\+\\\\\\.\\[a\\-zA\\-Z0\\-9\\]\\+\\$/$#" count: 1 @@ -6391,14 +6229,29 @@ parameters: path: lib/Service/SearchTrailService.php - - message: "#^Dead catch \\- Exception is never thrown in the try block\\.$#" + message: "#^Offset 'id' on array\\{id\\: int, uuid\\: string\\|null, uri\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, description\\: string\\|null, version\\: string\\|null, summary\\: string\\|null, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" count: 1 - path: lib/Service/Settings/ObjectRetentionHandler.php + path: lib/Service/Serializer/RegisterSerializer.php - - message: "#^Comparison operation \"\\>\" between \\(array\\|float\\|int\\) and 0 results in an error\\.$#" - count: 2 - path: lib/Service/Settings/SolrSettingsHandler.php + message: "#^Offset 'schemas' on array\\{id\\: int, uuid\\: string\\|null, slug\\: string\\|null, title\\: string\\|null, version\\: string\\|null, description\\: string\\|null, schemas\\: array\\, source\\: string\\|null, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" + count: 1 + path: lib/Service/Serializer/RegisterSerializer.php + + - + message: "#^Offset 'total' on array\\{total\\: int\\} in isset\\(\\) always exists and is not nullable\\.$#" + count: 1 + path: lib/Service/Serializer/RegisterSerializer.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/Serializer/RegisterSerializer.php + + - + message: "#^Dead catch \\- Exception is never thrown in the try block\\.$#" + count: 1 + path: lib/Service/Settings/ObjectRetentionHandler.php - message: "#^Method OCA\\\\OpenRegister\\\\Service\\\\Settings\\\\ValidationOperationsHandler\\:\\:getValidateHandler\\(\\) is unused\\.$#" @@ -6431,7 +6284,7 @@ parameters: path: lib/Service/SettingsService.php - - message: "#^Method OCA\\\\OpenRegister\\\\Service\\\\SettingsService\\:\\:getStats\\(\\) should return array\\{timestamp\\: int, date\\: string, warnings\\: array\\{objectsWithoutOwner\\: int, objectsWithoutOrganisation\\: int, auditTrailsWithoutExpiry\\: int, searchTrailsWithoutExpiry\\: int, expiredAuditTrails\\: int, expiredSearchTrails\\: int, expiredObjects\\: int\\}, totals\\: array\\{totalObjects\\: int, totalAuditTrails\\: int, totalSearchTrails\\: int, totalConfigurations\\: int, totalOrganisations\\: int, totalRegisters\\: int, totalSchemas\\: int, totalSources\\: int, \\.\\.\\.\\}, solr\\?\\: array\\, cache\\?\\: array\\, system\\: array\\{php_version\\: string, memory_limit\\: string, max_execution_time\\: string\\}\\} but returns array\\{error\\: 'Failed to retrieve…', message\\: string\\}\\.$#" + message: "#^Method OCA\\\\OpenRegister\\\\Service\\\\SettingsService\\:\\:getStats\\(\\) should return array\\{timestamp\\: int, date\\: string, warnings\\: array\\{objectsWithoutOwner\\: int, objectsWithoutOrganisation\\: int, auditTrailsWithoutExpiry\\: int, searchTrailsWithoutExpiry\\: int, expiredAuditTrails\\: int, expiredSearchTrails\\: int, expiredObjects\\: int\\}, totals\\: array\\{totalObjects\\: int, totalAuditTrails\\: int, totalSearchTrails\\: int, totalConfigurations\\: int, totalOrganisations\\: int, totalRegisters\\: int, totalSchemas\\: int, totalSources\\: int, \\.\\.\\.\\}, cache\\?\\: array\\, system\\: array\\{php_version\\: string, memory_limit\\: string, max_execution_time\\: string\\}\\} but returns array\\{error\\: 'Failed to retrieve…', message\\: string\\}\\.$#" count: 1 path: lib/Service/SettingsService.php @@ -6475,6 +6328,11 @@ parameters: count: 1 path: lib/Service/SettingsService.php + - + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\SettingsService\\:\\:\\$setupHandler is never read, only written\\.$#" + count: 1 + path: lib/Service/SettingsService.php + - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\SettingsService\\:\\:\\$userManager is never read, only written\\.$#" count: 1 @@ -6500,6 +6358,11 @@ parameters: count: 1 path: lib/Service/ShareLinkService.php + - + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\Sync\\\\RestApiSourceFetcher\\:\\:\\$logger is never read, only written\\.$#" + count: 1 + path: lib/Service/Sync/RestApiSourceFetcher.php + - message: "#^Strict comparison using \\=\\=\\= between DateTime and null will always evaluate to false\\.$#" count: 1 @@ -6581,29 +6444,24 @@ parameters: path: lib/Service/TextExtractionService.php - - message: "#^PHPDoc tag @param for parameter \\$emlParser with type OCA\\\\OpenRegister\\\\Service\\\\EmlParser is not subtype of native type OCA\\\\OpenRegister\\\\Service\\\\TextExtraction\\\\EmlParser\\.$#" + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\TextExtractionService\\:\\:\\$entityMapper is never read, only written\\.$#" count: 1 path: lib/Service/TextExtractionService.php - - message: "#^PHPDoc type for property OCA\\\\OpenRegister\\\\Service\\\\TextExtractionService\\:\\:\\$emlParser with type OCA\\\\OpenRegister\\\\Service\\\\EmlParser is not subtype of native type OCA\\\\OpenRegister\\\\Service\\\\TextExtraction\\\\EmlParser\\.$#" + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\TextExtractionService\\:\\:\\$entityRelationMapper is never read, only written\\.$#" count: 1 path: lib/Service/TextExtractionService.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\TextExtractionService\\:\\:\\$entityMapper is never read, only written\\.$#" + message: "#^Strict comparison using \\=\\=\\= between non\\-empty\\-string and null will always evaluate to false\\.$#" count: 1 path: lib/Service/TextExtractionService.php - - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\TextExtractionService\\:\\:\\$entityRelationMapper is never read, only written\\.$#" + message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\TimeEntryService\\:\\:\\$logger is never read, only written\\.$#" count: 1 - path: lib/Service/TextExtractionService.php - - - - message: "#^Strict comparison using \\=\\=\\= between non\\-empty\\-string and null will always evaluate to false\\.$#" - count: 2 - path: lib/Service/TextExtractionService.php + path: lib/Service/TimeEntryService.php - message: "#^Property OCA\\\\OpenRegister\\\\Service\\\\TmloService\\:\\:\\$registerMapper is never read, only written\\.$#" @@ -6625,11 +6483,21 @@ parameters: count: 1 path: lib/Service/TranslationProjectionService.php + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/TranslationProjectionService.php + - message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" count: 1 path: lib/Service/TranslationProjectionService.php + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 1 + path: lib/Service/TranslationProjectionService.php + - message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" count: 1 @@ -6750,6 +6618,11 @@ parameters: count: 1 path: lib/Service/XwikiLinkService.php + - + message: "#^Strict comparison using \\=\\=\\= between true and false will always evaluate to false\\.$#" + count: 1 + path: lib/Service/ZaaktypeAuthorizationService.php + - message: "#^Property OCA\\\\OpenRegister\\\\Settings\\\\IntegrationsAdminSettings\\:\\:\\$l10n is never read, only written\\.$#" count: 1 @@ -6794,3 +6667,73 @@ parameters: message: "#^Unknown parameter \\$filters in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" count: 1 path: lib/Tool/SchemaTool.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:counts\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400, array\\, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: array\\{\\}\\}, array\\{\\}\\>\\.$#" + count: 2 + path: lib/Controller/ObjectsController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:counts\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400, array\\, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200, array\\{results\\: non\\-empty\\-array\\\\}, array\\{\\}\\>\\.$#" + count: 1 + path: lib/Controller/ObjectsController.php + + - + message: "#^Method OCA\\\\OpenRegister\\\\Controller\\\\ObjectsController\\:\\:counts\\(\\) should return OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<200\\|400, array\\, array\\> but returns OCP\\\\AppFramework\\\\Http\\\\JSONResponse\\<400, array\\{error\\: string\\}, array\\{\\}\\>\\.$#" + count: 2 + path: lib/Controller/ObjectsController.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Service/Calculation/TemporalCalculationSweepService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Service/Calculation/TemporalCalculationSweepService.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Service/Calculation/TemporalCalculationSweepService.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:findAll\\(\\)\\.$#" + count: 1 + path: lib/Service/Calculation/TemporalCalculationSweepService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 3 + path: lib/Service/ScheduledReportService.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\RegisterMapper\\:\\:find\\(\\)\\.$#" + count: 2 + path: lib/Service/ScheduledReportService.php + + - + message: "#^Unknown parameter \\$_multitenancy in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 3 + path: lib/Service/ScheduledReportService.php + + - + message: "#^Unknown parameter \\$_rbac in call to method OCA\\\\OpenRegister\\\\Db\\\\SchemaMapper\\:\\:find\\(\\)\\.$#" + count: 2 + path: lib/Service/ScheduledReportService.php + + - + message: "#^Call to an undefined method OCP\\\\Files\\\\Node\\:\\:get\\(\\)\\.$#" + count: 1 + path: lib/Service/ScheduledReportService.php + + - + message: "#^Call to an undefined method OCP\\\\Files\\\\Node\\:\\:newFile\\(\\)\\.$#" + count: 1 + path: lib/Service/ScheduledReportService.php + + - + message: "#^Call to an undefined method OCP\\\\Files\\\\Node\\:\\:nodeExists\\(\\)\\.$#" + count: 1 + path: lib/Service/ScheduledReportService.php diff --git a/phpstan.neon b/phpstan.neon index 4153beabd2..67d1eea61a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,7 +13,7 @@ parameters: excludePaths: - vendor - vendor-bin - # Apps that ship a verbatim template snapshot under lib/Resources/template/ (e.g. openbuilt). + # Apps that ship a verbatim template snapshot under lib/Resources/template/ (e.g. openbuild). - lib/Resources/template scanDirectories: - vendor/nextcloud/ocp @@ -55,6 +55,18 @@ parameters: - '#unknown class Doctrine\\DBAL\\#' - '#Doctrine\\DBAL\\[a-zA-Z\\]+.*not found#' - '#on an unknown class Doctrine\\DBAL\\#' + # ConductionNL/sapp fork uses lowercase pseudo-classes `obj` / + # `value` / `pdfentry` / `buffer` / `bytes` etc. in `@return` + # PHPDocs (upstream typo — fictitious classes that PHPStan can't + # resolve). The actual runtime types are PDFObject / PDFValue + # subclasses / string / Buffer respectively. Cleanup goes into + # an upstream PR; until then ignore PHPStan's "unknown class + # ddn\sapp\" diagnostic chain. + - '#on an unknown class ddn\\sapp\\(obj|value|pdfentry|buffer|bytes|str)\b#' + - '#unknown class ddn\\sapp\\(obj|value|pdfentry|buffer|bytes|str)\b#' + - '#should return [^ ]+ but returns ddn\\sapp\\(obj|value|pdfentry|buffer|bytes|str)\b#' + - '#Parameter \$pdf_object of method ddn\\sapp\\PDFDoc::add_object\(\) expects ddn\\sapp\\PDFObject, ddn\\sapp\\obj given#' + - '#Strict comparison using === between ddn\\sapp\\(obj|value|pdfentry) and false will always evaluate to false#' # Dynamic HTTP status codes from business rule validation results # (matches both `Parameter $statusCode` and `Parameter #N $statusCode` forms emitted by phpstan) - '#Parameter (\#\d+ )?\$statusCode of class OCP\\AppFramework\\Http\\JSONResponse constructor expects#' diff --git a/phpunit-eml.xml b/phpunit-eml.xml new file mode 100644 index 0000000000..27ef5732e4 --- /dev/null +++ b/phpunit-eml.xml @@ -0,0 +1,17 @@ + + + + + + tests/Unit/Service/TextExtraction/EmlParserTest.php + + + diff --git a/phpunit-integration-providers.xml b/phpunit-integration-providers.xml new file mode 100644 index 0000000000..7fa2387970 --- /dev/null +++ b/phpunit-integration-providers.xml @@ -0,0 +1,30 @@ + + + + + + tests/Unit/Service/Integration + + + + + + lib/Service/Integration/ + + + + + + + diff --git a/phpunit-standalone.xml b/phpunit-standalone.xml new file mode 100644 index 0000000000..e6b940ee0e --- /dev/null +++ b/phpunit-standalone.xml @@ -0,0 +1,17 @@ + + + + + tests/Unit + + + + + + diff --git a/phpunit-unit-local.xml b/phpunit-unit-local.xml new file mode 100644 index 0000000000..7a322bbb61 --- /dev/null +++ b/phpunit-unit-local.xml @@ -0,0 +1,33 @@ + + + + + + tests/Unit + + + + + + lib/ + + + lib/Migration/ + lib/AppInfo/Application.php + + + + + + + diff --git a/phpunit-unit.xml b/phpunit-unit.xml index 6f02d81977..dc8a03b261 100644 --- a/phpunit-unit.xml +++ b/phpunit-unit.xml @@ -36,6 +36,10 @@ + + diff --git a/playwright.config.ts b/playwright.config.ts index b11e7a18c3..3ff47de38d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -60,7 +60,18 @@ export default defineConfig({ // PR pipelines don't reshoot screenshots on every push. { name: 'chromium', - testIgnore: ['**/docs-screenshots.spec.ts'], + // NOTE: a project-level testIgnore REPLACES the top-level + // testIgnore for this project (Playwright does not merge them), + // so the api-direct exclusion must be repeated here. These + // specs are API/contract assertions covered by the Newman suite + // (tests/integration/*.postman_collection.json), not UI tests — + // gate-19: API-direct → Newman. + testIgnore: [ + '**/docs-screenshots.spec.ts', + '**/api-direct/**', + // Visual specs run only under the opt-in `visual` project. + '**/visual/**', + ], use: { ...devices['Desktop Chrome'] }, }, // Documentation capture project (ADR-030 / journeydoc). Opt-in: @@ -76,11 +87,34 @@ export default defineConfig({ }, timeout: 90_000, }, + // Visual-regression project (GAP-5). Opt-in / non-gating: + // npx playwright test --project visual + // npx playwright test --project visual --update-snapshots (rebaseline) + // Fixed viewport + authenticated session => deterministic shots. + // Baselines live in tests/e2e/visual/*-snapshots/ and ARE committed. + // PLATFORM CAVEAT: PNG baselines are host-font/GPU specific, so a CI + // Linux runner will not byte-match a dev-container baseline; the visual + // project must regenerate its baselines in-CI on first run before it + // can gate. See tests/e2e/visual/_visual-helpers.ts. + { + name: 'visual', + testMatch: /visual\/.*\.visual\.spec\.ts$/, + use: { + ...devices['Desktop Chrome'], + viewport: { width: 1280, height: 800 }, + storageState: path.resolve(__dirname, 'tests/e2e/.auth/admin.json'), + }, + timeout: 90_000, + }, ], testIgnore: [ '**/node_modules/**', '**/custom_apps/**', '**/.claude/**', + // API-direct specs are API/contract assertions (Newman-equivalent), + // not real UI-driving Playwright tests. They live here for reference + // but are excluded from the UI test run (gate-19: API-direct → Newman). + '**/api-direct/**', ], }) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 9d131b05df..77e3a14293 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,12 @@ + + + + + + + @@ -37,11 +44,6 @@ - - - - - @@ -72,50 +74,30 @@ - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - + + + csrfManager]]> + nonceManager]]> + + + - - - - - - - - - - - - + + + + - - - - + + + appDataFactory]]> + + @@ -131,6 +113,11 @@ + + + logger]]> + + @@ -141,25 +128,126 @@ - - - - + + + + + db->createSchema()]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id) === true]]> + + + + + getOrganisation() === null && isset($this->appConfig) === true]]> + userSession) === false]]> + appConfig) === false]]> + appConfig) === false]]> + appConfig) === false]]> + appConfig) === false]]> + groupManager) === false]]> + groupManager) === false]]> + groupManager) === false]]> + organisationMapper) === true]]> + organisationMapper) === true]]> + organisationMapper) === true]]> + userSession) === false]]> + userSession) === false]]> + userSession) === false]]> + userSession) === false]]> + + + + + + + + + + + + + + + id]]> + + + + + - - - - + + + id]]> + + + + + + + - - - - + + + + + + + + + + + + + + @@ -196,6 +284,72 @@ + + + + + + + + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + + + + + + + + + {'X-OPENREGISTER-OBJECT'}]]> + {'X-OPENREGISTER-REGISTER'}]]> + {'X-OPENREGISTER-SCHEMA'}]]> + + + + + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + + + + + + getToken()]]> + getToken()]]> + + + + + temperature]]> + temperature]]> + + + apiKey]]> + + + + + organizationId]]> + temperature]]> + temperature]]> + + + + + cardDavBackend]]> @@ -208,6 +362,7 @@ cardDavBackend]]> cardDavBackend]]> cardDavBackend]]> + cardDavBackend]]> @@ -229,6 +384,11 @@ + + + + + @@ -243,25 +403,50 @@ - - - - + + + + + + + + + + + + - - - - + + + relationCache]]> + + - - - - + + + + + + + + + oas['paths'][$pathName][$method]]]> + + + + + stats]]> + + + + + + @@ -276,10 +461,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + calDavBackend]]> + + + + + COMPLETED]]> + DESCRIPTION]]> + DTSTAMP]]> + DUE]]> + PRIORITY]]> + STATUS]]> + SUMMARY]]> + + + DUE]]> + {'X-OPENREGISTER-DATA'}]]> + + + + + + + diff --git a/resources/solr/configset/lang/contractions_ca.txt b/resources/solr/configset/lang/contractions_ca.txt deleted file mode 100644 index 307a85f913..0000000000 --- a/resources/solr/configset/lang/contractions_ca.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Set of Catalan contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -d -l -m -n -s -t diff --git a/resources/solr/configset/lang/contractions_fr.txt b/resources/solr/configset/lang/contractions_fr.txt deleted file mode 100644 index f1bba51b23..0000000000 --- a/resources/solr/configset/lang/contractions_fr.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Set of French contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -l -m -t -qu -n -s -j -d -c -jusqu -quoiqu -lorsqu -puisqu diff --git a/resources/solr/configset/lang/contractions_ga.txt b/resources/solr/configset/lang/contractions_ga.txt deleted file mode 100644 index 9ebe7fa349..0000000000 --- a/resources/solr/configset/lang/contractions_ga.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Set of Irish contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -d -m -b diff --git a/resources/solr/configset/lang/contractions_it.txt b/resources/solr/configset/lang/contractions_it.txt deleted file mode 100644 index cac0409537..0000000000 --- a/resources/solr/configset/lang/contractions_it.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Set of Italian contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -c -l -all -dall -dell -nell -sull -coll -pell -gl -agl -dagl -degl -negl -sugl -un -m -t -s -v -d diff --git a/resources/solr/configset/lang/hyphenations_ga.txt b/resources/solr/configset/lang/hyphenations_ga.txt deleted file mode 100644 index 4d2642cc5a..0000000000 --- a/resources/solr/configset/lang/hyphenations_ga.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Set of Irish hyphenations for StopFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -h -n -t diff --git a/resources/solr/configset/lang/stemdict_nl.txt b/resources/solr/configset/lang/stemdict_nl.txt deleted file mode 100644 index 441072971d..0000000000 --- a/resources/solr/configset/lang/stemdict_nl.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Set of overrides for the dutch stemmer -# TODO: load this as a resource from the analyzer and sync it in build.xml -fiets fiets -bromfiets bromfiets -ei eier -kind kinder diff --git a/resources/solr/configset/lang/stoptags_ja.txt b/resources/solr/configset/lang/stoptags_ja.txt deleted file mode 100644 index 71b750845e..0000000000 --- a/resources/solr/configset/lang/stoptags_ja.txt +++ /dev/null @@ -1,420 +0,0 @@ -# -# This file defines a Japanese stoptag set for JapanesePartOfSpeechStopFilter. -# -# Any token with a part-of-speech tag that exactly matches those defined in this -# file are removed from the token stream. -# -# Set your own stoptags by uncommenting the lines below. Note that comments are -# not allowed on the same line as a stoptag. See LUCENE-3745 for frequency lists, -# etc. that can be useful for building you own stoptag set. -# -# The entire possible tagset is provided below for convenience. -# -##### -# noun: unclassified nouns -#名詞 -# -# noun-common: Common nouns or nouns where the sub-classification is undefined -#名詞-一般 -# -# noun-proper: Proper nouns where the sub-classification is undefined -#名詞-固有名詞 -# -# noun-proper-misc: miscellaneous proper nouns -#名詞-固有名詞-一般 -# -# noun-proper-person: Personal names where the sub-classification is undefined -#名詞-固有名詞-人名 -# -# noun-proper-person-misc: names that cannot be divided into surname and -# given name; foreign names; names where the surname or given name is unknown. -# e.g. お市の方 -#名詞-固有名詞-人名-一般 -# -# noun-proper-person-surname: Mainly Japanese surnames. -# e.g. 山田 -#名詞-固有名詞-人名-姓 -# -# noun-proper-person-given_name: Mainly Japanese given names. -# e.g. 太郎 -#名詞-固有名詞-人名-名 -# -# noun-proper-organization: Names representing organizations. -# e.g. 通産省, NHK -#名詞-固有名詞-組織 -# -# noun-proper-place: Place names where the sub-classification is undefined -#名詞-固有名詞-地域 -# -# noun-proper-place-misc: Place names excluding countries. -# e.g. アジア, バルセロナ, 京都 -#名詞-固有名詞-地域-一般 -# -# noun-proper-place-country: Country names. -# e.g. 日本, オーストラリア -#名詞-固有名詞-地域-国 -# -# noun-pronoun: Pronouns where the sub-classification is undefined -#名詞-代名詞 -# -# noun-pronoun-misc: miscellaneous pronouns: -# e.g. それ, ここ, あいつ, あなた, あちこち, いくつ, どこか, なに, みなさん, みんな, わたくし, われわれ -#名詞-代名詞-一般 -# -# noun-pronoun-contraction: Spoken language contraction made by combining a -# pronoun and the particle 'wa'. -# e.g. ありゃ, こりゃ, こりゃあ, そりゃ, そりゃあ -#名詞-代名詞-縮約 -# -# noun-adverbial: Temporal nouns such as names of days or months that behave -# like adverbs. Nouns that represent amount or ratios and can be used adverbially, -# e.g. 金曜, 一月, 午後, 少量 -#名詞-副詞可能 -# -# noun-verbal: Nouns that take arguments with case and can appear followed by -# 'suru' and related verbs (する, できる, なさる, くださる) -# e.g. インプット, 愛着, 悪化, 悪戦苦闘, 一安心, 下取り -#名詞-サ変接続 -# -# noun-adjective-base: The base form of adjectives, words that appear before な ("na") -# e.g. 健康, 安易, 駄目, だめ -#名詞-形容動詞語幹 -# -# noun-numeric: Arabic numbers, Chinese numerals, and counters like 何 (回), 数. -# e.g. 0, 1, 2, 何, 数, 幾 -#名詞-数 -# -# noun-affix: noun affixes where the sub-classification is undefined -#名詞-非自立 -# -# noun-affix-misc: Of adnominalizers, the case-marker の ("no"), and words that -# attach to the base form of inflectional words, words that cannot be classified -# into any of the other categories below. This category includes indefinite nouns. -# e.g. あかつき, 暁, かい, 甲斐, 気, きらい, 嫌い, くせ, 癖, こと, 事, ごと, 毎, しだい, 次第, -# 順, せい, 所為, ついで, 序で, つもり, 積もり, 点, どころ, の, はず, 筈, はずみ, 弾み, -# 拍子, ふう, ふり, 振り, ほう, 方, 旨, もの, 物, 者, ゆえ, 故, ゆえん, 所以, わけ, 訳, -# わり, 割り, 割, ん-口語/, もん-口語/ -#名詞-非自立-一般 -# -# noun-affix-adverbial: noun affixes that that can behave as adverbs. -# e.g. あいだ, 間, あげく, 挙げ句, あと, 後, 余り, 以外, 以降, 以後, 以上, 以前, 一方, うえ, -# 上, うち, 内, おり, 折り, かぎり, 限り, きり, っきり, 結果, ころ, 頃, さい, 際, 最中, さなか, -# 最中, じたい, 自体, たび, 度, ため, 為, つど, 都度, とおり, 通り, とき, 時, ところ, 所, -# とたん, 途端, なか, 中, のち, 後, ばあい, 場合, 日, ぶん, 分, ほか, 他, まえ, 前, まま, -# 儘, 侭, みぎり, 矢先 -#名詞-非自立-副詞可能 -# -# noun-affix-aux: noun affixes treated as 助動詞 ("auxiliary verb") in school grammars -# with the stem よう(だ) ("you(da)"). -# e.g. よう, やう, 様 (よう) -#名詞-非自立-助動詞語幹 -# -# noun-affix-adjective-base: noun affixes that can connect to the indeclinable -# connection form な (aux "da"). -# e.g. みたい, ふう -#名詞-非自立-形容動詞語幹 -# -# noun-special: special nouns where the sub-classification is undefined. -#名詞-特殊 -# -# noun-special-aux: The そうだ ("souda") stem form that is used for reporting news, is -# treated as 助動詞 ("auxiliary verb") in school grammars, and attach to the base -# form of inflectional words. -# e.g. そう -#名詞-特殊-助動詞語幹 -# -# noun-suffix: noun suffixes where the sub-classification is undefined. -#名詞-接尾 -# -# noun-suffix-misc: Of the nouns or stem forms of other parts of speech that connect -# to ガル or タイ and can combine into compound nouns, words that cannot be classified into -# any of the other categories below. In general, this category is more inclusive than -# 接尾語 ("suffix") and is usually the last element in a compound noun. -# e.g. おき, かた, 方, 甲斐 (がい), がかり, ぎみ, 気味, ぐるみ, (~した) さ, 次第, 済 (ず) み, -# よう, (でき)っこ, 感, 観, 性, 学, 類, 面, 用 -#名詞-接尾-一般 -# -# noun-suffix-person: Suffixes that form nouns and attach to person names more often -# than other nouns. -# e.g. 君, 様, 著 -#名詞-接尾-人名 -# -# noun-suffix-place: Suffixes that form nouns and attach to place names more often -# than other nouns. -# e.g. 町, 市, 県 -#名詞-接尾-地域 -# -# noun-suffix-verbal: Of the suffixes that attach to nouns and form nouns, those that -# can appear before スル ("suru"). -# e.g. 化, 視, 分け, 入り, 落ち, 買い -#名詞-接尾-サ変接続 -# -# noun-suffix-aux: The stem form of そうだ (様態) that is used to indicate conditions, -# is treated as 助動詞 ("auxiliary verb") in school grammars, and attach to the -# conjunctive form of inflectional words. -# e.g. そう -#名詞-接尾-助動詞語幹 -# -# noun-suffix-adjective-base: Suffixes that attach to other nouns or the conjunctive -# form of inflectional words and appear before the copula だ ("da"). -# e.g. 的, げ, がち -#名詞-接尾-形容動詞語幹 -# -# noun-suffix-adverbial: Suffixes that attach to other nouns and can behave as adverbs. -# e.g. 後 (ご), 以後, 以降, 以前, 前後, 中, 末, 上, 時 (じ) -#名詞-接尾-副詞可能 -# -# noun-suffix-classifier: Suffixes that attach to numbers and form nouns. This category -# is more inclusive than 助数詞 ("classifier") and includes common nouns that attach -# to numbers. -# e.g. 個, つ, 本, 冊, パーセント, cm, kg, カ月, か国, 区画, 時間, 時半 -#名詞-接尾-助数詞 -# -# noun-suffix-special: Special suffixes that mainly attach to inflecting words. -# e.g. (楽し) さ, (考え) 方 -#名詞-接尾-特殊 -# -# noun-suffix-conjunctive: Nouns that behave like conjunctions and join two words -# together. -# e.g. (日本) 対 (アメリカ), 対 (アメリカ), (3) 対 (5), (女優) 兼 (主婦) -#名詞-接続詞的 -# -# noun-verbal_aux: Nouns that attach to the conjunctive particle て ("te") and are -# semantically verb-like. -# e.g. ごらん, ご覧, 御覧, 頂戴 -#名詞-動詞非自立的 -# -# noun-quotation: text that cannot be segmented into words, proverbs, Chinese poetry, -# dialects, English, etc. Currently, the only entry for 名詞 引用文字列 ("noun quotation") -# is いわく ("iwaku"). -#名詞-引用文字列 -# -# noun-nai_adjective: Words that appear before the auxiliary verb ない ("nai") and -# behave like an adjective. -# e.g. 申し訳, 仕方, とんでも, 違い -#名詞-ナイ形容詞語幹 -# -##### -# prefix: unclassified prefixes -#接頭詞 -# -# prefix-nominal: Prefixes that attach to nouns (including adjective stem forms) -# excluding numerical expressions. -# e.g. お (水), 某 (氏), 同 (社), 故 (~氏), 高 (品質), お (見事), ご (立派) -#接頭詞-名詞接続 -# -# prefix-verbal: Prefixes that attach to the imperative form of a verb or a verb -# in conjunctive form followed by なる/なさる/くださる. -# e.g. お (読みなさい), お (座り) -#接頭詞-動詞接続 -# -# prefix-adjectival: Prefixes that attach to adjectives. -# e.g. お (寒いですねえ), バカ (でかい) -#接頭詞-形容詞接続 -# -# prefix-numerical: Prefixes that attach to numerical expressions. -# e.g. 約, およそ, 毎時 -#接頭詞-数接続 -# -##### -# verb: unclassified verbs -#動詞 -# -# verb-main: -#動詞-自立 -# -# verb-auxiliary: -#動詞-非自立 -# -# verb-suffix: -#動詞-接尾 -# -##### -# adjective: unclassified adjectives -#形容詞 -# -# adjective-main: -#形容詞-自立 -# -# adjective-auxiliary: -#形容詞-非自立 -# -# adjective-suffix: -#形容詞-接尾 -# -##### -# adverb: unclassified adverbs -#副詞 -# -# adverb-misc: Words that can be segmented into one unit and where adnominal -# modification is not possible. -# e.g. あいかわらず, 多分 -#副詞-一般 -# -# adverb-particle_conjunction: Adverbs that can be followed by の, は, に, -# な, する, だ, etc. -# e.g. こんなに, そんなに, あんなに, なにか, なんでも -#副詞-助詞類接続 -# -##### -# adnominal: Words that only have noun-modifying forms. -# e.g. この, その, あの, どの, いわゆる, なんらかの, 何らかの, いろんな, こういう, そういう, ああいう, -# どういう, こんな, そんな, あんな, どんな, 大きな, 小さな, おかしな, ほんの, たいした, -# 「(, も) さる (ことながら)」, 微々たる, 堂々たる, 単なる, いかなる, 我が」「同じ, 亡き -#連体詞 -# -##### -# conjunction: Conjunctions that can occur independently. -# e.g. が, けれども, そして, じゃあ, それどころか -接続詞 -# -##### -# particle: unclassified particles. -助詞 -# -# particle-case: case particles where the subclassification is undefined. -助詞-格助詞 -# -# particle-case-misc: Case particles. -# e.g. から, が, で, と, に, へ, より, を, の, にて -助詞-格助詞-一般 -# -# particle-case-quote: the "to" that appears after nouns, a person’s speech, -# quotation marks, expressions of decisions from a meeting, reasons, judgements, -# conjectures, etc. -# e.g. ( だ) と (述べた.), ( である) と (して執行猶予...) -助詞-格助詞-引用 -# -# particle-case-compound: Compounds of particles and verbs that mainly behave -# like case particles. -# e.g. という, といった, とかいう, として, とともに, と共に, でもって, にあたって, に当たって, に当って, -# にあたり, に当たり, に当り, に当たる, にあたる, において, に於いて,に於て, における, に於ける, -# にかけ, にかけて, にかんし, に関し, にかんして, に関して, にかんする, に関する, に際し, -# に際して, にしたがい, に従い, に従う, にしたがって, に従って, にたいし, に対し, にたいして, -# に対して, にたいする, に対する, について, につき, につけ, につけて, につれ, につれて, にとって, -# にとり, にまつわる, によって, に依って, に因って, により, に依り, に因り, による, に依る, に因る, -# にわたって, にわたる, をもって, を以って, を通じ, を通じて, を通して, をめぐって, をめぐり, をめぐる, -# って-口語/, ちゅう-関西弁「という」/, (何) ていう (人)-口語/, っていう-口語/, といふ, とかいふ -助詞-格助詞-連語 -# -# particle-conjunctive: -# e.g. から, からには, が, けれど, けれども, けど, し, つつ, て, で, と, ところが, どころか, とも, ども, -# ながら, なり, ので, のに, ば, ものの, や ( した), やいなや, (ころん) じゃ(いけない)-口語/, -# (行っ) ちゃ(いけない)-口語/, (言っ) たって (しかたがない)-口語/, (それがなく)ったって (平気)-口語/ -助詞-接続助詞 -# -# particle-dependency: -# e.g. こそ, さえ, しか, すら, は, も, ぞ -助詞-係助詞 -# -# particle-adverbial: -# e.g. がてら, かも, くらい, 位, ぐらい, しも, (学校) じゃ(これが流行っている)-口語/, -# (それ)じゃあ (よくない)-口語/, ずつ, (私) なぞ, など, (私) なり (に), (先生) なんか (大嫌い)-口語/, -# (私) なんぞ, (先生) なんて (大嫌い)-口語/, のみ, だけ, (私) だって-口語/, だに, -# (彼)ったら-口語/, (お茶) でも (いかが), 等 (とう), (今後) とも, ばかり, ばっか-口語/, ばっかり-口語/, -# ほど, 程, まで, 迄, (誰) も (が)([助詞-格助詞] および [助詞-係助詞] の前に位置する「も」) -助詞-副助詞 -# -# particle-interjective: particles with interjective grammatical roles. -# e.g. (松島) や -助詞-間投助詞 -# -# particle-coordinate: -# e.g. と, たり, だの, だり, とか, なり, や, やら -助詞-並立助詞 -# -# particle-final: -# e.g. かい, かしら, さ, ぜ, (だ)っけ-口語/, (とまってる) で-方言/, な, ナ, なあ-口語/, ぞ, ね, ネ, -# ねぇ-口語/, ねえ-口語/, ねん-方言/, の, のう-口語/, や, よ, ヨ, よぉ-口語/, わ, わい-口語/ -助詞-終助詞 -# -# particle-adverbial/conjunctive/final: The particle "ka" when unknown whether it is -# adverbial, conjunctive, or sentence final. For example: -# (a) 「A か B か」. Ex:「(国内で運用する) か,(海外で運用する) か (.)」 -# (b) Inside an adverb phrase. Ex:「(幸いという) か (, 死者はいなかった.)」 -# 「(祈りが届いたせい) か (, 試験に合格した.)」 -# (c) 「かのように」. Ex:「(何もなかった) か (のように振る舞った.)」 -# e.g. か -助詞-副助詞/並立助詞/終助詞 -# -# particle-adnominalizer: The "no" that attaches to nouns and modifies -# non-inflectional words. -助詞-連体化 -# -# particle-adnominalizer: The "ni" and "to" that appear following nouns and adverbs -# that are giongo, giseigo, or gitaigo. -# e.g. に, と -助詞-副詞化 -# -# particle-special: A particle that does not fit into one of the above classifications. -# This includes particles that are used in Tanka, Haiku, and other poetry. -# e.g. かな, けむ, ( しただろう) に, (あんた) にゃ(わからん), (俺) ん (家) -助詞-特殊 -# -##### -# auxiliary-verb: -助動詞 -# -##### -# interjection: Greetings and other exclamations. -# e.g. おはよう, おはようございます, こんにちは, こんばんは, ありがとう, どうもありがとう, ありがとうございます, -# いただきます, ごちそうさま, さよなら, さようなら, はい, いいえ, ごめん, ごめんなさい -#感動詞 -# -##### -# symbol: unclassified Symbols. -記号 -# -# symbol-misc: A general symbol not in one of the categories below. -# e.g. [○◎@$〒→+] -記号-一般 -# -# symbol-comma: Commas -# e.g. [,、] -記号-読点 -# -# symbol-period: Periods and full stops. -# e.g. [..。] -記号-句点 -# -# symbol-space: Full-width whitespace. -記号-空白 -# -# symbol-open_bracket: -# e.g. [({‘“『【] -記号-括弧開 -# -# symbol-close_bracket: -# e.g. [)}’”』」】] -記号-括弧閉 -# -# symbol-alphabetic: -#記号-アルファベット -# -##### -# other: unclassified other -#その他 -# -# other-interjection: Words that are hard to classify as noun-suffixes or -# sentence-final particles. -# e.g. (だ)ァ -その他-間投 -# -##### -# filler: Aizuchi that occurs during a conversation or sounds inserted as filler. -# e.g. あの, うんと, えと -フィラー -# -##### -# non-verbal: non-verbal sound. -非言語音 -# -##### -# fragment: -#語断片 -# -##### -# unknown: unknown part of speech. -#未知語 -# -##### End of file diff --git a/resources/solr/configset/lang/stopwords_ar.txt b/resources/solr/configset/lang/stopwords_ar.txt deleted file mode 100644 index 046829db6a..0000000000 --- a/resources/solr/configset/lang/stopwords_ar.txt +++ /dev/null @@ -1,125 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -# Cleaned on October 11, 2009 (not normalized, so use before normalization) -# This means that when modifying this list, you might need to add some -# redundant entries, for example containing forms with both أ and ا -من -ومن -منها -منه -في -وفي -فيها -فيه -و -ف -ثم -او -أو -ب -بها -به -ا -أ -اى -اي -أي -أى -لا -ولا -الا -ألا -إلا -لكن -ما -وما -كما -فما -عن -مع -اذا -إذا -ان -أن -إن -انها -أنها -إنها -انه -أنه -إنه -بان -بأن -فان -فأن -وان -وأن -وإن -التى -التي -الذى -الذي -الذين -الى -الي -إلى -إلي -على -عليها -عليه -اما -أما -إما -ايضا -أيضا -كل -وكل -لم -ولم -لن -ولن -هى -هي -هو -وهى -وهي -وهو -فهى -فهي -فهو -انت -أنت -لك -لها -له -هذه -هذا -تلك -ذلك -هناك -كانت -كان -يكون -تكون -وكانت -وكان -غير -بعض -قد -نحو -بين -بينما -منذ -ضمن -حيث -الان -الآن -خلال -بعد -قبل -حتى -عند -عندما -لدى -جميع diff --git a/resources/solr/configset/lang/stopwords_bg.txt b/resources/solr/configset/lang/stopwords_bg.txt deleted file mode 100644 index 1ae4ba2ae3..0000000000 --- a/resources/solr/configset/lang/stopwords_bg.txt +++ /dev/null @@ -1,193 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -а -аз -ако -ала -бе -без -беше -би -бил -била -били -било -близо -бъдат -бъде -бяха -в -вас -ваш -ваша -вероятно -вече -взема -ви -вие -винаги -все -всеки -всички -всичко -всяка -във -въпреки -върху -г -ги -главно -го -д -да -дали -до -докато -докога -дори -досега -доста -е -едва -един -ето -за -зад -заедно -заради -засега -затова -защо -защото -и -из -или -им -има -имат -иска -й -каза -как -каква -какво -както -какъв -като -кога -когато -което -които -кой -който -колко -която -къде -където -към -ли -м -ме -между -мен -ми -мнозина -мога -могат -може -моля -момента -му -н -на -над -назад -най -направи -напред -например -нас -не -него -нея -ни -ние -никой -нито -но -някои -някой -няма -обаче -около -освен -особено -от -отгоре -отново -още -пак -по -повече -повечето -под -поне -поради -после -почти -прави -пред -преди -през -при -пък -първо -с -са -само -се -сега -си -скоро -след -сме -според -сред -срещу -сте -съм -със -също -т -тази -така -такива -такъв -там -твой -те -тези -ти -тн -то -това -тогава -този -той -толкова -точно -трябва -тук -тъй -тя -тях -у -харесва -ч -че -често -чрез -ще -щом -я diff --git a/resources/solr/configset/lang/stopwords_ca.txt b/resources/solr/configset/lang/stopwords_ca.txt deleted file mode 100644 index 3da65deafe..0000000000 --- a/resources/solr/configset/lang/stopwords_ca.txt +++ /dev/null @@ -1,220 +0,0 @@ -# Catalan stopwords from http://github.com/vcl/cue.language (Apache 2 Licensed) -a -abans -ací -ah -així -això -al -als -aleshores -algun -alguna -algunes -alguns -alhora -allà -allí -allò -altra -altre -altres -amb -ambdós -ambdues -apa -aquell -aquella -aquelles -aquells -aquest -aquesta -aquestes -aquests -aquí -baix -cada -cadascú -cadascuna -cadascunes -cadascuns -com -contra -d'un -d'una -d'unes -d'uns -dalt -de -del -dels -des -després -dins -dintre -donat -doncs -durant -e -eh -el -els -em -en -encara -ens -entre -érem -eren -éreu -es -és -esta -està -estàvem -estaven -estàveu -esteu -et -etc -ets -fins -fora -gairebé -ha -han -has -havia -he -hem -heu -hi -ho -i -igual -iguals -ja -l'hi -la -les -li -li'n -llavors -m'he -ma -mal -malgrat -mateix -mateixa -mateixes -mateixos -me -mentre -més -meu -meus -meva -meves -molt -molta -moltes -molts -mon -mons -n'he -n'hi -ne -ni -no -nogensmenys -només -nosaltres -nostra -nostre -nostres -o -oh -oi -on -pas -pel -pels -per -però -perquè -poc -poca -pocs -poques -potser -propi -qual -quals -quan -quant -que -què -quelcom -qui -quin -quina -quines -quins -s'ha -s'han -sa -semblant -semblants -ses -seu -seus -seva -seva -seves -si -sobre -sobretot -sóc -solament -sols -son -són -sons -sota -sou -t'ha -t'han -t'he -ta -tal -també -tampoc -tan -tant -tanta -tantes -teu -teus -teva -teves -ton -tons -tot -tota -totes -tots -un -una -unes -uns -us -va -vaig -vam -van -vas -veu -vosaltres -vostra -vostre -vostres diff --git a/resources/solr/configset/lang/stopwords_cz.txt b/resources/solr/configset/lang/stopwords_cz.txt deleted file mode 100644 index 53c6097dac..0000000000 --- a/resources/solr/configset/lang/stopwords_cz.txt +++ /dev/null @@ -1,172 +0,0 @@ -a -s -k -o -i -u -v -z -dnes -cz -tímto -budeš -budem -byli -jseš -můj -svým -ta -tomto -tohle -tuto -tyto -jej -zda -proč -máte -tato -kam -tohoto -kdo -kteří -mi -nám -tom -tomuto -mít -nic -proto -kterou -byla -toho -protože -asi -ho -naši -napište -re -což -tím -takže -svých -její -svými -jste -aj -tu -tedy -teto -bylo -kde -ke -pravé -ji -nad -nejsou -či -pod -téma -mezi -přes -ty -pak -vám -ani -když -však -neg -jsem -tento -článku -články -aby -jsme -před -pta -jejich -byl -ještě -až -bez -také -pouze -první -vaše -která -nás -nový -tipy -pokud -může -strana -jeho -své -jiné -zprávy -nové -není -vás -jen -podle -zde -už -být -více -bude -již -než -který -by -které -co -nebo -ten -tak -má -při -od -po -jsou -jak -další -ale -si -se -ve -to -jako -za -zpět -ze -do -pro -je -na -atd -atp -jakmile -přičemž -já -on -ona -ono -oni -ony -my -vy -jí -ji -mě -mne -jemu -tomu -těm -těmu -němu -němuž -jehož -jíž -jelikož -jež -jakož -načež diff --git a/resources/solr/configset/lang/stopwords_da.txt b/resources/solr/configset/lang/stopwords_da.txt deleted file mode 100644 index 6e90e8f1aa..0000000000 --- a/resources/solr/configset/lang/stopwords_da.txt +++ /dev/null @@ -1,110 +0,0 @@ - | From https://snowballstem.org/algorithms/danish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Danish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - -og | and -i | in -jeg | I -det | that (dem. pronoun)/it (pers. pronoun) -at | that (in front of a sentence)/to (with infinitive) -en | a/an -den | it (pers. pronoun)/that (dem. pronoun) -til | to/at/for/until/against/by/of/into, more -er | present tense of "to be" -som | who, as -på | on/upon/in/on/at/to/after/of/with/for, on -de | they -med | with/by/in, along -han | he -af | of/by/from/off/for/in/with/on, off -for | at/for/to/from/by/of/ago, in front/before, because -ikke | not -der | who/which, there/those -var | past tense of "to be" -mig | me/myself -sig | oneself/himself/herself/itself/themselves -men | but -et | a/an/one, one (number), someone/somebody/one -har | present tense of "to have" -om | round/about/for/in/a, about/around/down, if -vi | we -min | my -havde | past tense of "to have" -ham | him -hun | she -nu | now -over | over/above/across/by/beyond/past/on/about, over/past -da | then, when/as/since -fra | from/off/since, off, since -du | you -ud | out -sin | his/her/its/one's -dem | them -os | us/ourselves -op | up -man | you/one -hans | his -hvor | where -eller | or -hvad | what -skal | must/shall etc. -selv | myself/yourself/herself/ourselves etc., even -her | here -alle | all/everyone/everybody etc. -vil | will (verb) -blev | past tense of "to stay/to remain/to get/to become" -kunne | could -ind | in -når | when -være | present tense of "to be" -dog | however/yet/after all -noget | something -ville | would -jo | you know/you see (adv), yes -deres | their/theirs -efter | after/behind/according to/for/by/from, later/afterwards -ned | down -skulle | should -denne | this -end | than -dette | this -mit | my/mine -også | also -under | under/beneath/below/during, below/underneath -have | have -dig | you -anden | other -hende | her -mine | my -alt | everything -meget | much/very, plenty of -sit | his, her, its, one's -sine | his, her, its, one's -vor | our -mod | against -disse | these -hvis | if -din | your/yours -nogle | some -hos | by/at -blive | be/become -mange | many -ad | by/through -bliver | present tense of "to be/to become" -hendes | her/hers -været | be -thi | for (conj) -jer | you -sådan | such, like this/like that diff --git a/resources/solr/configset/lang/stopwords_de.txt b/resources/solr/configset/lang/stopwords_de.txt deleted file mode 100644 index 804bbbdb01..0000000000 --- a/resources/solr/configset/lang/stopwords_de.txt +++ /dev/null @@ -1,294 +0,0 @@ - | From https://snowballstem.org/algorithms/german/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A German stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | The number of forms in this list is reduced significantly by passing it - | through the German stemmer. - - -aber | but - -alle | all -allem -allen -aller -alles - -als | than, as -also | so -am | an + dem -an | at - -ander | other -andere -anderem -anderen -anderer -anderes -anderm -andern -anderr -anders - -auch | also -auf | on -aus | out of -bei | by -bin | am -bis | until -bist | art -da | there -damit | with it -dann | then - -der | the -den -des -dem -die -das - -daß | that - -derselbe | the same -derselben -denselben -desselben -demselben -dieselbe -dieselben -dasselbe - -dazu | to that - -dein | thy -deine -deinem -deinen -deiner -deines - -denn | because - -derer | of those -dessen | of him - -dich | thee -dir | to thee -du | thou - -dies | this -diese -diesem -diesen -dieser -dieses - - -doch | (several meanings) -dort | (over) there - - -durch | through - -ein | a -eine -einem -einen -einer -eines - -einig | some -einige -einigem -einigen -einiger -einiges - -einmal | once - -er | he -ihn | him -ihm | to him - -es | it -etwas | something - -euer | your -eure -eurem -euren -eurer -eures - -für | for -gegen | towards -gewesen | p.p. of sein -hab | have -habe | have -haben | have -hat | has -hatte | had -hatten | had -hier | here -hin | there -hinter | behind - -ich | I -mich | me -mir | to me - - -ihr | you, to her -ihre -ihrem -ihren -ihrer -ihres -euch | to you - -im | in + dem -in | in -indem | while -ins | in + das -ist | is - -jede | each, every -jedem -jeden -jeder -jedes - -jene | that -jenem -jenen -jener -jenes - -jetzt | now -kann | can - -kein | no -keine -keinem -keinen -keiner -keines - -können | can -könnte | could -machen | do -man | one - -manche | some, many a -manchem -manchen -mancher -manches - -mein | my -meine -meinem -meinen -meiner -meines - -mit | with -muss | must -musste | had to -nach | to(wards) -nicht | not -nichts | nothing -noch | still, yet -nun | now -nur | only -ob | whether -oder | or -ohne | without -sehr | very - -sein | his -seine -seinem -seinen -seiner -seines - -selbst | self -sich | herself - -sie | they, she -ihnen | to them - -sind | are -so | so - -solche | such -solchem -solchen -solcher -solches - -soll | shall -sollte | should -sondern | but -sonst | else -über | over -um | about, around -und | and - -uns | us -unse -unsem -unsen -unser -unses - -unter | under -viel | much -vom | von + dem -von | from -vor | before -während | while -war | was -waren | were -warst | wast -was | what -weg | away, off -weil | because -weiter | further - -welche | which -welchem -welchen -welcher -welches - -wenn | when -werde | will -werden | will -wie | how -wieder | again -will | want -wir | we -wird | will -wirst | willst -wo | where -wollen | want -wollte | wanted -würde | would -würden | would -zu | to -zum | zu + dem -zur | zu + der -zwar | indeed -zwischen | between - diff --git a/resources/solr/configset/lang/stopwords_el.txt b/resources/solr/configset/lang/stopwords_el.txt deleted file mode 100644 index 232681f5bd..0000000000 --- a/resources/solr/configset/lang/stopwords_el.txt +++ /dev/null @@ -1,78 +0,0 @@ -# Lucene Greek Stopwords list -# Note: by default this file is used after GreekLowerCaseFilter, -# so when modifying this file use 'σ' instead of 'ς' -ο -η -το -οι -τα -του -τησ -των -τον -την -και -κι -κ -ειμαι -εισαι -ειναι -ειμαστε -ειστε -στο -στον -στη -στην -μα -αλλα -απο -για -προσ -με -σε -ωσ -παρα -αντι -κατα -μετα -θα -να -δε -δεν -μη -μην -επι -ενω -εαν -αν -τοτε -που -πωσ -ποιοσ -ποια -ποιο -ποιοι -ποιεσ -ποιων -ποιουσ -αυτοσ -αυτη -αυτο -αυτοι -αυτων -αυτουσ -αυτεσ -αυτα -εκεινοσ -εκεινη -εκεινο -εκεινοι -εκεινεσ -εκεινα -εκεινων -εκεινουσ -οπωσ -ομωσ -ισωσ -οσο -οτι diff --git a/resources/solr/configset/lang/stopwords_en.txt b/resources/solr/configset/lang/stopwords_en.txt deleted file mode 100644 index 2c164c0b2a..0000000000 --- a/resources/solr/configset/lang/stopwords_en.txt +++ /dev/null @@ -1,54 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# a couple of test stopwords to test that the words are really being -# configured from this file: -stopworda -stopwordb - -# Standard english stop words taken from Lucene's StopAnalyzer -a -an -and -are -as -at -be -but -by -for -if -in -into -is -it -no -not -of -on -or -such -that -the -their -then -there -these -they -this -to -was -will -with diff --git a/resources/solr/configset/lang/stopwords_es.txt b/resources/solr/configset/lang/stopwords_es.txt deleted file mode 100644 index 48bd65ef86..0000000000 --- a/resources/solr/configset/lang/stopwords_es.txt +++ /dev/null @@ -1,356 +0,0 @@ - | From https://snowballstem.org/algorithms/spanish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Spanish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - - | The following is a ranked list (commonest to rarest) of stopwords - | deriving from a large sample of text. - - | Extra words have been added at the end. - -de | from, of -la | the, her -que | who, that -el | the -en | in -y | and -a | to -los | the, them -del | de + el -se | himself, from him etc -las | the, them -por | for, by, etc -un | a -para | for -con | with -no | no -una | a -su | his, her -al | a + el - | es from SER -lo | him -como | how -más | more -pero | pero -sus | su plural -le | to him, her -ya | already -o | or - | fue from SER -este | this - | ha from HABER -sí | himself etc -porque | because -esta | this - | son from SER -entre | between - | está from ESTAR -cuando | when -muy | very -sin | without -sobre | on - | ser from SER - | tiene from TENER -también | also -me | me -hasta | until -hay | there is/are -donde | where - | han from HABER -quien | whom, that - | están from ESTAR - | estado from ESTAR -desde | from -todo | all -nos | us -durante | during - | estados from ESTAR -todos | all -uno | a -les | to them -ni | nor -contra | against -otros | other - | fueron from SER -ese | that -eso | that - | había from HABER -ante | before -ellos | they -e | and (variant of y) -esto | this -mí | me -antes | before -algunos | some -qué | what? -unos | a -yo | I -otro | other -otras | other -otra | other -él | he -tanto | so much, many -esa | that -estos | these -mucho | much, many -quienes | who -nada | nothing -muchos | many -cual | who - | sea from SER -poco | few -ella | she -estar | to be - | haber from HABER -estas | these - | estaba from ESTAR - | estamos from ESTAR -algunas | some -algo | something -nosotros | we - - | other forms - -mi | me -mis | mi plural -tú | thou -te | thee -ti | thee -tu | thy -tus | tu plural -ellas | they -nosotras | we -vosotros | you -vosotras | you -os | you -mío | mine -mía | -míos | -mías | -tuyo | thine -tuya | -tuyos | -tuyas | -suyo | his, hers, theirs -suya | -suyos | -suyas | -nuestro | ours -nuestra | -nuestros | -nuestras | -vuestro | yours -vuestra | -vuestros | -vuestras | -esos | those -esas | those - - | forms of estar, to be (not including the infinitive): -estoy -estás -está -estamos -estáis -están -esté -estés -estemos -estéis -estén -estaré -estarás -estará -estaremos -estaréis -estarán -estaría -estarías -estaríamos -estaríais -estarían -estaba -estabas -estábamos -estabais -estaban -estuve -estuviste -estuvo -estuvimos -estuvisteis -estuvieron -estuviera -estuvieras -estuviéramos -estuvierais -estuvieran -estuviese -estuvieses -estuviésemos -estuvieseis -estuviesen -estando -estado -estada -estados -estadas -estad - - | forms of haber, to have (not including the infinitive): -he -has -ha -hemos -habéis -han -haya -hayas -hayamos -hayáis -hayan -habré -habrás -habrá -habremos -habréis -habrán -habría -habrías -habríamos -habríais -habrían -había -habías -habíamos -habíais -habían -hube -hubiste -hubo -hubimos -hubisteis -hubieron -hubiera -hubieras -hubiéramos -hubierais -hubieran -hubiese -hubieses -hubiésemos -hubieseis -hubiesen -habiendo -habido -habida -habidos -habidas - - | forms of ser, to be (not including the infinitive): -soy -eres -es -somos -sois -son -sea -seas -seamos -seáis -sean -seré -serás -será -seremos -seréis -serán -sería -serías -seríamos -seríais -serían -era -eras -éramos -erais -eran -fui -fuiste -fue -fuimos -fuisteis -fueron -fuera -fueras -fuéramos -fuerais -fueran -fuese -fueses -fuésemos -fueseis -fuesen -siendo -sido - | sed also means 'thirst' - - | forms of tener, to have (not including the infinitive): -tengo -tienes -tiene -tenemos -tenéis -tienen -tenga -tengas -tengamos -tengáis -tengan -tendré -tendrás -tendrá -tendremos -tendréis -tendrán -tendría -tendrías -tendríamos -tendríais -tendrían -tenía -tenías -teníamos -teníais -tenían -tuve -tuviste -tuvo -tuvimos -tuvisteis -tuvieron -tuviera -tuvieras -tuviéramos -tuvierais -tuvieran -tuviese -tuvieses -tuviésemos -tuvieseis -tuviesen -teniendo -tenido -tenida -tenidos -tenidas -tened - diff --git a/resources/solr/configset/lang/stopwords_et.txt b/resources/solr/configset/lang/stopwords_et.txt deleted file mode 100644 index 1b06a134b9..0000000000 --- a/resources/solr/configset/lang/stopwords_et.txt +++ /dev/null @@ -1,1603 +0,0 @@ -# Estonian stopwords list -all -alla -allapoole -allpool -alt -altpoolt -eel -eespool -enne -hommikupoole -hoolimata -ilma -kaudu -keset -kesk -kohe -koos -kuhupoole -kuni -kuspool -kustpoolt -kõige -käsikäes -lappi -ligi -läbi -mööda -paitsi -peale -pealepoole -pealpool -pealt -pealtpoolt -piki -pikku -piku -pikuti -põiki -pärast -päri -risti -sealpool -sealtpoolt -seespool -seltsis -siiapoole -siinpool -siitpoolt -sinnapoole -sissepoole -taga -tagantpoolt -tagapidi -tagapool -taha -tahapoole -teispool -teispoole -tänu -tükkis -vaatamata -vastu -väljapoole -väljaspool -väljastpoolt -õhtupoole -ühes -ühestükis -ühestükkis -ülalpool -ülaltpoolt -üle -ülespoole -ülevalpool -ülevaltpoolt -ümber -ümbert -aegu -aegus -alguks -algul -algule -algult -alguni -all -alla -alt -alul -alutsi -arvel -asemel -asemele -eel -eeli -ees -eesotsas -eest -eestotsast -esitsi -ette -etteotsa -haaval -heaks -hoolimata -hulgas -hulgast -hulka -jalgu -jalus -jalust -jaoks -jooksul -juurde -juures -juurest -jälil -jälile -järel -järele -järelt -järgi -kaasas -kallal -kallale -kallalt -kamul -kannul -kannule -kannult -kaudu -kaupa -keskel -keskele -keskelt -keskis -keskpaiku -kestel -kestes -kilda -killas -killast -kimpu -kimpus -kiuste -kohal -kohale -kohalt -kohaselt -kohe -kohta -koos -korral -kukil -kukile -kukilt -kulul -kõrva -kõrval -kõrvale -kõrvalt -kõrvas -kõrvast -käekõrval -käekõrvale -käekõrvalt -käes -käest -kätte -külge -küljes -küljest -küüsi -küüsis -küüsist -ligi -ligidal -ligidale -ligidalt -aegu -aegus -alguks -algul -algule -algult -alguni -all -alla -alt -alul -alutsi -arvel -asemel -asemele -eel -eeli -ees -eesotsas -eest -eestotsast -esitsi -ette -etteotsa -haaval -heaks -hoolimata -hulgas -hulgast -hulka -jalgu -jalus -jalust -jaoks -jooksul -juurde -juures -juurest -jälil -jälile -järel -järele -järelt -järgi -kaasas -kallal -kallale -kallalt -kamul -kannul -kannule -kannult -kaudu -kaupa -keskel -keskele -keskelt -keskis -keskpaiku -kestel -kestes -kilda -killas -killast -kimpu -kimpus -kiuste -kohal -kohale -kohalt -kohaselt -kohe -kohta -koos -korral -kukil -kukile -kukilt -kulul -kõrva -kõrval -kõrvale -kõrvalt -kõrvas -kõrvast -käekõrval -käekõrvale -käekõrvalt -käes -käest -kätte -külge -küljes -küljest -küüsi -küüsis -küüsist -ligi -ligidal -ligidale -ligidalt -lool -läbi -lähedal -lähedale -lähedalt -man -mant -manu -meelest -mööda -nahas -nahka -nahkas -najal -najale -najalt -nõjal -nõjale -otsa -otsas -otsast -paigale -paigu -paiku -peal -peale -pealt -perra -perrä -pidi -pihta -piki -pikku -pool -poole -poolest -poolt -puhul -puksiiris -pähe -päralt -päras -pärast -päri -ringi -ringis -risust -saadetusel -saadik -saatel -saati -seas -seast -sees -seest -sekka -seljataga -seltsi -seltsis -seltsist -sisse -slepis -suhtes -šlepis -taga -tagant -tagantotsast -tagaotsas -tagaselja -tagasi -tagast -tagutsi -taha -tahaotsa -takka -tarvis -tasa -tuuri -tuuris -tõttu -tükkis -uhal -vaatamata -vahel -vahele -vahelt -vahepeal -vahepeale -vahepealt -vahetsi -varal -varale -varul -vastas -vastast -vastu -veerde -veeres -viisi -võidu -võrd -võrdki -võrra -võrragi -väel -väele -vältel -väärt -väärtki -äärde -ääre -ääres -äärest -ühes -üle -ümber -ümbert -a -abil -aina -ainult -alalt -alates -alati -alles -b -c -d -e -eales -ealeski -edasi -edaspidi -eelkõige -eemal -ei -eks -end -enda -enese -ennem -esialgu -f -g -h -hoopis -i -iganes -igatahes -igati -iial -iialgi -ikka -ikkagi -ilmaski -iseenda -iseenese -iseenesest -isegi -j -jah -ju -juba -juhul -just -järelikult -k -ka -kah -kas -kasvõi -keda -kestahes -kogu -koguni -kohati -kokku -kuhu -kuhugi -kuidagi -kuidas -kunagi -kus -kusagil -kusjuures -kuskil -kust -kõigepealt -küll -l -liiga -lisaks -m -miks -mil -millal -millalgi -mispärast -mistahes -mistõttu -mitte -muide -muidu -muidugi -muist -mujal -mujale -mujalt -mõlemad -mõnda -mõne -mõnikord -n -nii -niikaua -niimoodi -niipaljuke -niisama -niisiis -niivõrd -nõnda -nüüd -o -omaette -omakorda -omavahel -ometi -p -palju -paljuke -palju-palju -peaaegu -peagi -peamiselt -pigem -pisut -praegu -päris -r -rohkem -s -samas -samuti -seal -sealt -sedakorda -sedapuhku -seega -seejuures -seejärel -seekord -seepärast -seetõttu -sellepärast -seni -sestap -siia -siiani -siin -siinkohal -siis -siiski -siit -sinna -suht -š -z -ž -t -teel -teineteise -tõesti -täiesti -u -umbes -v -w -veel -veelgi -vist -võibolla -võib-olla -väga -vähemalt -välja -väljas -väljast -õ -ä -ära -ö -ü -ühtlasi -üksi -ükskõik -ülal -ülale -ülalt -üles -ülesse -üleval -ülevalt -ülimalt -üsna -x -y -aga -ega -ehk -ehkki -elik -ellik -enge -ennegu -ent -et -ja -justkui -kui -kuid -kuigi -kuivõrd -kuna -kuni -kut -mistab -muudkui -nagu -nigu -ning -olgugi -otsekui -otsenagu -selmet -sest -sestab -vaid -või -aa -adaa -adjöö -ae -ah -ahaa -ahah -ah-ah-ah -ah-haa -ahoi -ai -aidaa -aidu-raidu -aih -aijeh -aituma -aitäh -aitüma -ammuu -amps -ampsti -aptsih -ass -at -ata -at-at-at -atsih -atsihh -auh -bai-bai -bingo -braavo -brr -ee -eeh -eh -ehee -eheh -eh-eh-hee -eh-eh-ee -ehei -ehh -ehhee -einoh -ena -ennäe -ennäh -fuh -fui -fuih -haa -hah -hahaa -hah-hah-hah -halleluuja -hallo -halloo -hass -hee -heh -he-he-hee -hei -heldeke(ne) -heureka -hihii -hip-hip-hurraa -hmh -hmjah -hoh-hoh-hoo -hohoo -hoi -hollallaa -hoo -hoplaa -hopp -hops -hopsassaa -hopsti -hosianna -huh -huidii -huist -hurjah -hurjeh -hurjoh -hurjuh -hurraa -huu -hõhõh -hõi -hõissa -hõissassa -hõk -hõkk -häh -hä-hä-hää -hüvasti -ih-ah-haa -ih-ih-hii -ii-ha-ha -issake -issakene -isver -jaa-ah -ja-ah -jaah -janäe -jeeh -jeerum -jeever -jessas -jestas -juhhei -jumalaga -jumalime -jumaluke -jumalukene -jutas -kaaps -kaapsti -kaasike -kae -kalps -kalpsti -kannäe -kanäe -kappadi -kaps -kapsti -karkõmm -karkäuh -karkääks -karkääksti -karmauh -karmauhti -karnaps -karnapsti -karniuhti -karpartsaki -karpauh -karpauhti -karplauh -karplauhti -karprauh -karprauhti -karsumdi -karsumm -kartsumdi -kartsumm -karviuh -karviuhti -kaske -kassa -kauh -kauhti -keh -keksti -kepsti -khe -khm -kih -kiiks -kiiksti -kiis -kiiss -kikerii -kikerikii -kili -kilk -kilk-kõlk -kilks -kilks-kolks -kilks-kõlks -kill -killadi -killadi|-kolladi -killadi-kõlladi -killa-kolla -killa-kõlla -kill-kõll -kimps-komps -kipp -kips-kõps -kiriküüt -kirra-kõrra -kirr-kõrr -kirts -klaps -klapsti -klirdi -klirr -klonks -klops -klopsti -kluk -klu-kluu -klõks -klõksti -klõmdi -klõmm -klõmpsti -klõnks -klõnksti -klõps -klõpsti -kläu -kohva-kohva -kok -koks -koksti -kolaki -kolk -kolks -kolksti -koll -kolladi -komp -komps -kompsti -kop -kopp -koppadi -kops -kopsti -kossu -kotsu -kraa -kraak -kraaks -kraaps -kraapsti -krahh -kraks -kraksti -kraps -krapsti -krauh -krauhti -kriiks -kriiksti -kriips -kriips-kraaps -kripa-krõpa -krips-kraps -kriuh -kriuks -kriuksti -kromps -kronk -kronks -krooks -kruu -krõks -krõksti -krõpa -krõps -krõpsti -krõuh -kräu -kräuh -kräuhti -kräuks -kss -kukeleegu -kukku -kuku -kulu -kurluu -kurnäu -kuss -kussu -kõks -kõksti -kõldi -kõlks -kõlksti -kõll -kõmaki -kõmdi -kõmm -kõmps -kõpp -kõps -kõpsadi -kõpsat -kõpsti -kõrr -kõrra-kõrra -kõss -kõtt -kõõksti -kärr -kärts -kärtsti -käuks -käuksti -kääga -kääks -kääksti -köh -köki-möki -köksti -laks -laksti -lampsti -larts -lartsti -lats -latsti -leelo -legoo -lehva -liiri-lõõri -lika-lõka -likat-lõkat -limpsti -lips -lipsti -lirts -lirtsaki -lirtsti -lonksti -lops -lopsti -lorts -lortsti -luks -lups -lupsti -lurts -lurtsti -lõks -lõksti -lõmps -lõmpsti -lõnks -lõnksti -lärts -lärtsti -läts -lätsti -lörts -lörtsti -lötsti -lööps -lööpsti -marss -mats -matsti -mauh -mauhti -mh -mhh -mhmh -miau -mjaa -mkm -m-mh -mnjaa -mnjah -moens -mulks -mulksti -mull-mull -mull-mull-mull -muu -muuh -mõh -mõmm -mäh -mäts -mäu -mää -möh -möh-öh-ää -möö -müh-müh -mühüh -müks -müksti -müraki -mürr -mürts -mürtsaki -mürtsti -mütaku -müta-mäta -müta-müta -müt-müt -müt-müt-müt -müts -mütsti -mütt -naa -naah -nah -naks -naksti -nanuu -naps -napsti -nilpsti -nipsti -nirr -niuh -niuh-näuh -niuhti -noh -noksti -nolpsti -nonoh -nonoo -nonäh -noo -nooh -nooks -norr -nurr -nuuts -nõh -nõhh -nõka-nõka -nõks -nõksat-nõksat -nõks-nõks -nõksti -nõõ -nõõh -näeh -näh -nälpsti -nämm-nämm -näpsti -näts -nätsti -näu -näuh -näuhti -näuks -näuksti -nääh -nääks -nühkat-nühkat -oeh -oh -ohh -ohhh -oh-hoi -oh-hoo -ohoh -oh-oh-oo -oh-oh-hoo -ohoi -ohoo -oi -oih -oijee -oijeh -oo -ooh -oo-oh -oo-ohh -oot -ossa -ot -paa -pah -pahh -pakaa -pamm -pantsti -pardon -pardonks -parlartsti -parts -partsti -partsumdi -partsumm -pastoi -pats -patst -patsti -pau -pauh -pauhti -pele -pfui -phuh -phuuh -phäh -phähh -piiks -piip -piiri-pääri -pimm -pimm-pamm -pimm-pomm -pimm-põmm -piraki -piuks -piu-pau -plaks -plaksti -plarts -plartsti -plats -platsti -plauh -plauhh -plauhti -pliks -pliks-plaks -plinn -pliraki -plirts -plirtsti -pliu -pliuh -ploks -plotsti -plumps -plumpsti -plõks -plõksti -plõmdi -plõmm -plõnn -plärr -plärts -plärtsat -plärtsti -pläu -pläuh -plää -plörtsat -pomm -popp -pops -popsti -ports -pot -pots -potsti -pott -praks -praksti -prants -prantsaki -prantsti -prassai -prauh -prauhh -prauhti -priks -priuh -priuhh -priuh-prauh -proosit -proost -prr -prrr -prõks -prõksti -prõmdi -prõmm -prõntsti -prääk -prääks -pst -psst -ptrr -ptruu -ptüi -puh -puhh -puksti -pumm -pumps -pup-pup-pup -purts -puuh -põks -põksti -põmdi -põmm -põmmadi -põnks -põnn -põnnadi -põnt -põnts -põntsti -põraki -põrr -põrra-põrra -päh -pähh -päntsti -pää -pöörd -püh -raks -raksti -raps -rapsti -ratataa -rauh -riips -riipsti -riks -riks-raks -rips-raps -rivitult -robaki -rops -ropsaki -ropsti -ruik -räntsti -räts -röh -röhh -sah -sahh -sahkat -saps -sapsti -sauh -sauhti -servus -sihkadi-sahkadi -sihka-sahka -sihkat-sahkat -silks -silk-solk -sips -sipsti -sirr -sirr-sorr -sirts -sirtsti -siu -siuh -siuh-sauh -siuh-säuh -siuhti -siuks -siuts -skool -so -soh -solks -solksti -solpsti -soo -sooh -so-oh -soo-oh -sopp -sops -sopsti -sorr -sorts -sortsti -so-soo -soss -soss-soss -ss -sss -sst -stopp -suhkat-sahkat -sulk -sulks -sulksti -sull -sulla-sulla -sulpa-sulpa -sulps -sulpsti -sumaki -sumdi -summ -summat-summat -sups -supsaku -supsti -surts -surtsti -suss -susti -suts -sutsti -säh -sähke -särts -särtsti -säu -säuh -säuhti -taevake -taevakene -takk -tere -terekest -tibi-tibi -tikk-takk -tiks -tilk -tilks -till -tilla-talla -till-tall -tilulii -tinn -tip -tip-tap -tirr -tirtsti -tiu -tjaa -tjah -tohhoh -tohhoo -tohoh -tohoo -tok -tokk -toks -toksti -tonks -tonksti -tota -totsti -tot-tot -tprr -tpruu -trah -trahh -trallallaa -trill -trillallaa -trr -trrr -tsah -tsahh -tsilk -tsilk-tsolk -tsirr -tsiuh -tskae -tsolk -tss -tst -tsst -tsuhh -tsuk -tsumm -tsurr -tsäuh -tšao -tšš -tššš -tuk -tuks -turts -turtsti -tutki -tutkit -tutu-lutu -tutulutu -tuut -tuutu-luutu -tõks -tötsti -tümps -uh -uhh -uh-huu -uhtsa -uhtsaa -uhuh -uhuu -ui -uih -uih-aih -uijah -uijeh -uist -uit -uka -upsti -uraa -urjah -urjeh -urjoh -urjuh -urr -urraa -ust -utu -uu -uuh -vaak -vaat -vae -vaeh -vai -vat -vau -vhüüt -vidiit -viiks -vilks -vilksti -vinki-vinki -virdi -virr -viu -viudi -viuh -viuhti -voeh -voh -vohh -volks -volksti -vooh -vops -vopsti -vot -vuh -vuhti -vuih -vulks -vulksti -vull -vulpsti -vups -vupsaki -vupsaku -vupsti -vurdi -vurr -vurra-vurra -vurts -vurtsti -vutt -võe -võeh -või -võih -võrr -võts -võtt -vääks -õe -õits -õk -õkk -õrr -õss -õuh -äh -ähh -ähhähhää -äh-hää -äh-äh-hää -äiu -äiu-ää -äss -ää -ääh -äähh -öh -öhh -ök -üh -eelmine -eikeegi -eimiski -emb-kumb -enam -enim -iga -igasugune -igaüks -ise -isesugune -järgmine -keegi -kes -kumb -kumbki -kõik -meiesugune -meietaoline -midagi -mihuke -mihukene -milletaoline -milline -mina -minake -mingi -mingisugune -minusugune -minutaoline -mis -miski -miskisugune -missugune -misuke -mitmes -mitmesugune -mitu -mitu-mitu -mitu-setu -muu -mõlema -mõnesugune -mõni -mõningane -mõningas -mäherdune -määrane -naasugune -need -nemad -nendesugune -nendetaoline -nihuke -nihukene -niimitu -niisamasugune -niisugune -nisuke -nisukene -oma -omaenese -omasugune -omataoline -pool -praegune -sama -samasugune -samataoline -see -seesama -seesamane -seesamune -seesinane -seesugune -selline -sihuke -sihukene -sina -sinusugune -sinutaoline -siuke -siukene -säherdune -säärane -taoline -teiesugune -teine -teistsugune -tema -temake -temakene -temasugune -temataoline -too -toosama -toosamane -üks -üksteise -hakkama -minema -olema -pidama -saama -tegema -tulema -võima diff --git a/resources/solr/configset/lang/stopwords_eu.txt b/resources/solr/configset/lang/stopwords_eu.txt deleted file mode 100644 index 25f1db9346..0000000000 --- a/resources/solr/configset/lang/stopwords_eu.txt +++ /dev/null @@ -1,99 +0,0 @@ -# example set of basque stopwords -al -anitz -arabera -asko -baina -bat -batean -batek -bati -batzuei -batzuek -batzuetan -batzuk -bera -beraiek -berau -berauek -bere -berori -beroriek -beste -bezala -da -dago -dira -ditu -du -dute -edo -egin -ere -eta -eurak -ez -gainera -gu -gutxi -guzti -haiei -haiek -haietan -hainbeste -hala -han -handik -hango -hara -hari -hark -hartan -hau -hauei -hauek -hauetan -hemen -hemendik -hemengo -hi -hona -honek -honela -honetan -honi -hor -hori -horiei -horiek -horietan -horko -horra -horrek -horrela -horretan -horri -hortik -hura -izan -ni -noiz -nola -non -nondik -nongo -nor -nora -ze -zein -zen -zenbait -zenbat -zer -zergatik -ziren -zituen -zu -zuek -zuen -zuten diff --git a/resources/solr/configset/lang/stopwords_fa.txt b/resources/solr/configset/lang/stopwords_fa.txt deleted file mode 100644 index 723641c6da..0000000000 --- a/resources/solr/configset/lang/stopwords_fa.txt +++ /dev/null @@ -1,313 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -# Note: by default this file is used after normalization, so when adding entries -# to this file, use the arabic 'ي' instead of 'ی' -انان -نداشته -سراسر -خياه -ايشان -وي -تاكنون -بيشتري -دوم -پس -ناشي -وگو -يا -داشتند -سپس -هنگام -هرگز -پنج -نشان -امسال -ديگر -گروهي -شدند -چطور -ده -و -دو -نخستين -ولي -چرا -چه -وسط -ه -كدام -قابل -يك -رفت -هفت -همچنين -در -هزار -بله -بلي -شايد -اما -شناسي -گرفته -دهد -داشته -دانست -داشتن -خواهيم -ميليارد -وقتيكه -امد -خواهد -جز -اورده -شده -بلكه -خدمات -شدن -برخي -نبود -بسياري -جلوگيري -حق -كردند -نوعي -بعري -نكرده -نظير -نبايد -بوده -بودن -داد -اورد -هست -جايي -شود -دنبال -داده -بايد -سابق -هيچ -همان -انجا -كمتر -كجاست -گردد -كسي -تر -مردم -تان -دادن -بودند -سري -جدا -ندارند -مگر -يكديگر -دارد -دهند -بنابراين -هنگامي -سمت -جا -انچه -خود -دادند -زياد -دارند -اثر -بدون -بهترين -بيشتر -البته -به -براساس -بيرون -كرد -بعضي -گرفت -توي -اي -ميليون -او -جريان -تول -بر -مانند -برابر -باشيم -مدتي -گويند -اكنون -تا -تنها -جديد -چند -بي -نشده -كردن -كردم -گويد -كرده -كنيم -نمي -نزد -روي -قصد -فقط -بالاي -ديگران -اين -ديروز -توسط -سوم -ايم -دانند -سوي -استفاده -شما -كنار -داريم -ساخته -طور -امده -رفته -نخست -بيست -نزديك -طي -كنيد -از -انها -تمامي -داشت -يكي -طريق -اش -چيست -روب -نمايد -گفت -چندين -چيزي -تواند -ام -ايا -با -ان -ايد -ترين -اينكه -ديگري -راه -هايي -بروز -همچنان -پاعين -كس -حدود -مختلف -مقابل -چيز -گيرد -ندارد -ضد -همچون -سازي -شان -مورد -باره -مرسي -خويش -برخوردار -چون -خارج -شش -هنوز -تحت -ضمن -هستيم -گفته -فكر -بسيار -پيش -براي -روزهاي -انكه -نخواهد -بالا -كل -وقتي -كي -چنين -كه -گيري -نيست -است -كجا -كند -نيز -يابد -بندي -حتي -توانند -عقب -خواست -كنند -بين -تمام -همه -ما -باشند -مثل -شد -اري -باشد -اره -طبق -بعد -اگر -صورت -غير -جاي -بيش -ريزي -اند -زيرا -چگونه -بار -لطفا -مي -درباره -من -ديده -همين -گذاري -برداري -علت -گذاشته -هم -فوق -نه -ها -شوند -اباد -همواره -هر -اول -خواهند -چهار -نام -امروز -مان -هاي -قبل -كنم -سعي -تازه -را -هستند -زير -جلوي -عنوان -بود diff --git a/resources/solr/configset/lang/stopwords_fi.txt b/resources/solr/configset/lang/stopwords_fi.txt deleted file mode 100644 index c9ee2f16dc..0000000000 --- a/resources/solr/configset/lang/stopwords_fi.txt +++ /dev/null @@ -1,96 +0,0 @@ - | From https://snowballstem.org/algorithms/finnish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - -| forms of BE - -olla -olen -olet -on -olemme -olette -ovat -ole | negative form - -oli -olisi -olisit -olisin -olisimme -olisitte -olisivat -olit -olin -olimme -olitte -olivat -ollut -olleet - -en | negation -et -ei -emme -ette -eivät - -|Nom Gen Acc Part Iness Elat Illat Adess Ablat Allat Ess Trans -minä minun minut minua minussa minusta minuun minulla minulta minulle | I -sinä sinun sinut sinua sinussa sinusta sinuun sinulla sinulta sinulle | you -hän hänen hänet häntä hänessä hänestä häneen hänellä häneltä hänelle | he she -me meidän meidät meitä meissä meistä meihin meillä meiltä meille | we -te teidän teidät teitä teissä teistä teihin teillä teiltä teille | you -he heidän heidät heitä heissä heistä heihin heillä heiltä heille | they - -tämä tämän tätä tässä tästä tähän tällä tältä tälle tänä täksi | this -tuo tuon tuota tuossa tuosta tuohon tuolla tuolta tuolle tuona tuoksi | that -se sen sitä siinä siitä siihen sillä siltä sille sinä siksi | it -nämä näiden näitä näissä näistä näihin näillä näiltä näille näinä näiksi | these -nuo noiden noita noissa noista noihin noilla noilta noille noina noiksi | those -ne niiden niitä niissä niistä niihin niillä niiltä niille niinä niiksi | they - -kuka kenen kenet ketä kenessä kenestä keneen kenellä keneltä kenelle kenenä keneksi| who -ketkä keiden ketkä keitä keissä keistä keihin keillä keiltä keille keinä keiksi | (pl) -mikä minkä minkä mitä missä mistä mihin millä miltä mille minä miksi | which what -mitkä | (pl) - -joka jonka jota jossa josta johon jolla jolta jolle jona joksi | who which -jotka joiden joita joissa joista joihin joilla joilta joille joina joiksi | (pl) - -| conjunctions - -että | that -ja | and -jos | if -koska | because -kuin | than -mutta | but -niin | so -sekä | and -sillä | for -tai | or -vaan | but -vai | or -vaikka | although - - -| prepositions - -kanssa | with -mukaan | according to -noin | about -poikki | across -yli | over, across - -| other - -kun | when -nyt | now -itse | self - diff --git a/resources/solr/configset/lang/stopwords_fr.txt b/resources/solr/configset/lang/stopwords_fr.txt deleted file mode 100644 index 658ae9c91a..0000000000 --- a/resources/solr/configset/lang/stopwords_fr.txt +++ /dev/null @@ -1,186 +0,0 @@ - | From https://snowballstem.org/algorithms/french/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A French stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - -au | a + le -aux | a + les -avec | with -ce | this -ces | these -dans | with -de | of -des | de + les -du | de + le -elle | she -en | `of them' etc -et | and -eux | them -il | he -je | I -la | the -le | the -leur | their -lui | him -ma | my (fem) -mais | but -me | me -même | same; as in moi-même (myself) etc -mes | me (pl) -moi | me -mon | my (masc) -ne | not -nos | our (pl) -notre | our -nous | we -on | one -ou | where -par | by -pas | not -pour | for -qu | que before vowel -que | that -qui | who -sa | his, her (fem) -se | oneself -ses | his (pl) - | son | his, her (masc). Omitted because it is homonym of "sound" -sur | on -ta | thy (fem) -te | thee -tes | thy (pl) -toi | thee -ton | thy (masc) -tu | thou -un | a -une | a -vos | your (pl) -votre | your -vous | you - - | single letter forms - -c | c' -d | d' -j | j' -l | l' -à | to, at -m | m' -n | n' -s | s' -t | t' -y | there - - | forms of être (not including the infinitive): - | été - Omitted because it is homonym of "summer" -étée -étées - | étés - Omitted because it is homonym of "summers" -étant -suis -es - | est - Omitted because it is homonym of "east" - | sommes - Omitted because it is homonym of "sums" -êtes -sont -serai -seras -sera -serons -serez -seront -serais -serait -serions -seriez -seraient -étais -était -étions -étiez -étaient -fus -fut -fûmes -fûtes -furent -sois -soit -soyons -soyez -soient -fusse -fusses - | fût - Omitted because it is homonym of "tap", like in "beer on tap" -fussions -fussiez -fussent - - | forms of avoir (not including the infinitive): -ayant -eu -eue -eues -eus -ai - | as - Omitted because it is homonym of "ace" -avons -avez -ont -aurai - | auras - Omitted because it is also the name of a kind of wind - | aura - Omitted because it is also the name of a kind of wind and homonym of "aura" -aurons -aurez -auront -aurais -aurait -aurions -auriez -auraient -avais -avait - | avions - Omitted because it is homonym of "planes" -aviez -avaient -eut -eûmes -eûtes -eurent -aie -aies -ait -ayons -ayez -aient -eusse -eusses -eût -eussions -eussiez -eussent - - | Later additions (from Jean-Christophe Deschamps) -ceci | this -cela | that (added 11 Apr 2012. Omission reported by Adrien Grand) -celà | that (incorrect, though common) -cet | this -cette | this -ici | here -ils | they -les | the (pl) -leurs | their (pl) -quel | which -quels | which -quelle | which -quelles | which -sans | without -soi | oneself - diff --git a/resources/solr/configset/lang/stopwords_ga.txt b/resources/solr/configset/lang/stopwords_ga.txt deleted file mode 100644 index 9ff88d747e..0000000000 --- a/resources/solr/configset/lang/stopwords_ga.txt +++ /dev/null @@ -1,110 +0,0 @@ - -a -ach -ag -agus -an -aon -ar -arna -as -b' -ba -beirt -bhúr -caoga -ceathair -ceathrar -chomh -chtó -chuig -chun -cois -céad -cúig -cúigear -d' -daichead -dar -de -deich -deichniúr -den -dhá -do -don -dtí -dá -dár -dó -faoi -faoin -faoina -faoinár -fara -fiche -gach -gan -go -gur -haon -hocht -i -iad -idir -in -ina -ins -inár -is -le -leis -lena -lenár -m' -mar -mo -mé -na -nach -naoi -naonúr -ná -ní -níor -nó -nócha -ocht -ochtar -os -roimh -sa -seacht -seachtar -seachtó -seasca -seisear -siad -sibh -sinn -sna -sé -sí -tar -thar -thú -triúr -trí -trína -trínár -tríocha -tú -um -ár -é -éis -í -ó -ón -óna -ónár diff --git a/resources/solr/configset/lang/stopwords_gl.txt b/resources/solr/configset/lang/stopwords_gl.txt deleted file mode 100644 index d8760b12c1..0000000000 --- a/resources/solr/configset/lang/stopwords_gl.txt +++ /dev/null @@ -1,161 +0,0 @@ -# galican stopwords -a -aínda -alí -aquel -aquela -aquelas -aqueles -aquilo -aquí -ao -aos -as -así -á -ben -cando -che -co -coa -comigo -con -connosco -contigo -convosco -coas -cos -cun -cuns -cunha -cunhas -da -dalgunha -dalgunhas -dalgún -dalgúns -das -de -del -dela -delas -deles -desde -deste -do -dos -dun -duns -dunha -dunhas -e -el -ela -elas -eles -en -era -eran -esa -esas -ese -eses -esta -estar -estaba -está -están -este -estes -estiven -estou -eu -é -facer -foi -foron -fun -había -hai -iso -isto -la -las -lle -lles -lo -los -mais -me -meu -meus -min -miña -miñas -moi -na -nas -neste -nin -no -non -nos -nosa -nosas -noso -nosos -nós -nun -nunha -nuns -nunhas -o -os -ou -ó -ós -para -pero -pode -pois -pola -polas -polo -polos -por -que -se -senón -ser -seu -seus -sexa -sido -sobre -súa -súas -tamén -tan -te -ten -teñen -teño -ter -teu -teus -ti -tido -tiña -tiven -túa -túas -un -unha -unhas -uns -vos -vosa -vosas -voso -vosos -vós diff --git a/resources/solr/configset/lang/stopwords_hi.txt b/resources/solr/configset/lang/stopwords_hi.txt deleted file mode 100644 index 86286bb083..0000000000 --- a/resources/solr/configset/lang/stopwords_hi.txt +++ /dev/null @@ -1,235 +0,0 @@ -# Also see http://www.opensource.org/licenses/bsd-license.html -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# This file was created by Jacques Savoy and is distributed under the BSD license. -# Note: by default this file also contains forms normalized by HindiNormalizer -# for spelling variation (see section below), such that it can be used whether or -# not you enable that feature. When adding additional entries to this list, -# please add the normalized form as well. -अंदर -अत -अपना -अपनी -अपने -अभी -आदि -आप -इत्यादि -इन -इनका -इन्हीं -इन्हें -इन्हों -इस -इसका -इसकी -इसके -इसमें -इसी -इसे -उन -उनका -उनकी -उनके -उनको -उन्हीं -उन्हें -उन्हों -उस -उसके -उसी -उसे -एक -एवं -एस -ऐसे -और -कई -कर -करता -करते -करना -करने -करें -कहते -कहा -का -काफ़ी -कि -कितना -किन्हें -किन्हों -किया -किर -किस -किसी -किसे -की -कुछ -कुल -के -को -कोई -कौन -कौनसा -गया -घर -जब -जहाँ -जा -जितना -जिन -जिन्हें -जिन्हों -जिस -जिसे -जीधर -जैसा -जैसे -जो -तक -तब -तरह -तिन -तिन्हें -तिन्हों -तिस -तिसे -तो -था -थी -थे -दबारा -दिया -दुसरा -दूसरे -दो -द्वारा -न -नहीं -ना -निहायत -नीचे -ने -पर -पर -पहले -पूरा -पे -फिर -बनी -बही -बहुत -बाद -बाला -बिलकुल -भी -भीतर -मगर -मानो -मे -में -यदि -यह -यहाँ -यही -या -यिह -ये -रखें -रहा -रहे -ऱ्वासा -लिए -लिये -लेकिन -व -वर्ग -वह -वह -वहाँ -वहीं -वाले -वुह -वे -वग़ैरह -संग -सकता -सकते -सबसे -सभी -साथ -साबुत -साभ -सारा -से -सो -ही -हुआ -हुई -हुए -है -हैं -हो -होता -होती -होते -होना -होने -# additional normalized forms of the above -अपनि -जेसे -होति -सभि -तिंहों -इंहों -दवारा -इसि -किंहें -थि -उंहों -ओर -जिंहें -वहिं -अभि -बनि -हि -उंहिं -उंहें -हें -वगेरह -एसे -रवासा -कोन -निचे -काफि -उसि -पुरा -भितर -हे -बहि -वहां -कोइ -यहां -जिंहों -तिंहें -किसि -कइ -यहि -इंहिं -जिधर -इंहें -अदि -इतयादि -हुइ -कोनसा -इसकि -दुसरे -जहां -अप -किंहों -उनकि -भि -वरग -हुअ -जेसा -नहिं diff --git a/resources/solr/configset/lang/stopwords_hu.txt b/resources/solr/configset/lang/stopwords_hu.txt deleted file mode 100644 index 3fa279eac9..0000000000 --- a/resources/solr/configset/lang/stopwords_hu.txt +++ /dev/null @@ -1,211 +0,0 @@ - | From https://snowballstem.org/algorithms/hungarian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - -| Hungarian stop word list -| prepared by Anna Tordai - -a -ahogy -ahol -aki -akik -akkor -alatt -által -általában -amely -amelyek -amelyekben -amelyeket -amelyet -amelynek -ami -amit -amolyan -amíg -amikor -át -abban -ahhoz -annak -arra -arról -az -azok -azon -azt -azzal -azért -aztán -azután -azonban -bár -be -belül -benne -cikk -cikkek -cikkeket -csak -de -e -eddig -egész -egy -egyes -egyetlen -egyéb -egyik -egyre -ekkor -el -elég -ellen -elő -először -előtt -első -én -éppen -ebben -ehhez -emilyen -ennek -erre -ez -ezt -ezek -ezen -ezzel -ezért -és -fel -felé -hanem -hiszen -hogy -hogyan -igen -így -illetve -ill. -ill -ilyen -ilyenkor -ison -ismét -itt -jó -jól -jobban -kell -kellett -keresztül -keressünk -ki -kívül -között -közül -legalább -lehet -lehetett -legyen -lenne -lenni -lesz -lett -maga -magát -majd -majd -már -más -másik -meg -még -mellett -mert -mely -melyek -mi -mit -míg -miért -milyen -mikor -minden -mindent -mindenki -mindig -mint -mintha -mivel -most -nagy -nagyobb -nagyon -ne -néha -nekem -neki -nem -néhány -nélkül -nincs -olyan -ott -össze -ő -ők -őket -pedig -persze -rá -s -saját -sem -semmi -sok -sokat -sokkal -számára -szemben -szerint -szinte -talán -tehát -teljes -tovább -továbbá -több -úgy -ugyanis -új -újabb -újra -után -utána -utolsó -vagy -vagyis -valaki -valami -valamint -való -vagyok -van -vannak -volt -voltam -voltak -voltunk -vissza -vele -viszont -volna diff --git a/resources/solr/configset/lang/stopwords_hy.txt b/resources/solr/configset/lang/stopwords_hy.txt deleted file mode 100644 index 60c1c50fbc..0000000000 --- a/resources/solr/configset/lang/stopwords_hy.txt +++ /dev/null @@ -1,46 +0,0 @@ -# example set of Armenian stopwords. -այդ -այլ -այն -այս -դու -դուք -եմ -են -ենք -ես -եք -է -էի -էին -էինք -էիր -էիք -էր -ըստ -թ -ի -ին -իսկ -իր -կամ -համար -հետ -հետո -մենք -մեջ -մի -ն -նա -նաև -նրա -նրանք -որ -որը -որոնք -որպես -ու -ում -պիտի -վրա -և diff --git a/resources/solr/configset/lang/stopwords_id.txt b/resources/solr/configset/lang/stopwords_id.txt deleted file mode 100644 index 4617f83a5c..0000000000 --- a/resources/solr/configset/lang/stopwords_id.txt +++ /dev/null @@ -1,359 +0,0 @@ -# from appendix D of: A Study of Stemming Effects on Information -# Retrieval in Bahasa Indonesia -ada -adanya -adalah -adapun -agak -agaknya -agar -akan -akankah -akhirnya -aku -akulah -amat -amatlah -anda -andalah -antar -diantaranya -antara -antaranya -diantara -apa -apaan -mengapa -apabila -apakah -apalagi -apatah -atau -ataukah -ataupun -bagai -bagaikan -sebagai -sebagainya -bagaimana -bagaimanapun -sebagaimana -bagaimanakah -bagi -bahkan -bahwa -bahwasanya -sebaliknya -banyak -sebanyak -beberapa -seberapa -begini -beginian -beginikah -beginilah -sebegini -begitu -begitukah -begitulah -begitupun -sebegitu -belum -belumlah -sebelum -sebelumnya -sebenarnya -berapa -berapakah -berapalah -berapapun -betulkah -sebetulnya -biasa -biasanya -bila -bilakah -bisa -bisakah -sebisanya -boleh -bolehkah -bolehlah -buat -bukan -bukankah -bukanlah -bukannya -cuma -percuma -dahulu -dalam -dan -dapat -dari -daripada -dekat -demi -demikian -demikianlah -sedemikian -dengan -depan -di -dia -dialah -dini -diri -dirinya -terdiri -dong -dulu -enggak -enggaknya -entah -entahlah -terhadap -terhadapnya -hal -hampir -hanya -hanyalah -harus -haruslah -harusnya -seharusnya -hendak -hendaklah -hendaknya -hingga -sehingga -ia -ialah -ibarat -ingin -inginkah -inginkan -ini -inikah -inilah -itu -itukah -itulah -jangan -jangankan -janganlah -jika -jikalau -juga -justru -kala -kalau -kalaulah -kalaupun -kalian -kami -kamilah -kamu -kamulah -kan -kapan -kapankah -kapanpun -dikarenakan -karena -karenanya -ke -kecil -kemudian -kenapa -kepada -kepadanya -ketika -seketika -khususnya -kini -kinilah -kiranya -sekiranya -kita -kitalah -kok -lagi -lagian -selagi -lah -lain -lainnya -melainkan -selaku -lalu -melalui -terlalu -lama -lamanya -selama -selama -selamanya -lebih -terlebih -bermacam -macam -semacam -maka -makanya -makin -malah -malahan -mampu -mampukah -mana -manakala -manalagi -masih -masihkah -semasih -masing -mau -maupun -semaunya -memang -mereka -merekalah -meski -meskipun -semula -mungkin -mungkinkah -nah -namun -nanti -nantinya -nyaris -oleh -olehnya -seorang -seseorang -pada -padanya -padahal -paling -sepanjang -pantas -sepantasnya -sepantasnyalah -para -pasti -pastilah -per -pernah -pula -pun -merupakan -rupanya -serupa -saat -saatnya -sesaat -saja -sajalah -saling -bersama -sama -sesama -sambil -sampai -sana -sangat -sangatlah -saya -sayalah -se -sebab -sebabnya -sebuah -tersebut -tersebutlah -sedang -sedangkan -sedikit -sedikitnya -segala -segalanya -segera -sesegera -sejak -sejenak -sekali -sekalian -sekalipun -sesekali -sekaligus -sekarang -sekarang -sekitar -sekitarnya -sela -selain -selalu -seluruh -seluruhnya -semakin -sementara -sempat -semua -semuanya -sendiri -sendirinya -seolah -seperti -sepertinya -sering -seringnya -serta -siapa -siapakah -siapapun -disini -disinilah -sini -sinilah -sesuatu -sesuatunya -suatu -sesudah -sesudahnya -sudah -sudahkah -sudahlah -supaya -tadi -tadinya -tak -tanpa -setelah -telah -tentang -tentu -tentulah -tentunya -tertentu -seterusnya -tapi -tetapi -setiap -tiap -setidaknya -tidak -tidakkah -tidaklah -toh -waduh -wah -wahai -sewaktu -walau -walaupun -wong -yaitu -yakni -yang diff --git a/resources/solr/configset/lang/stopwords_it.txt b/resources/solr/configset/lang/stopwords_it.txt deleted file mode 100644 index c74160e28c..0000000000 --- a/resources/solr/configset/lang/stopwords_it.txt +++ /dev/null @@ -1,303 +0,0 @@ - | From https://snowballstem.org/algorithms/italian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | An Italian stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - -ad | a (to) before vowel -al | a + il -allo | a + lo -ai | a + i -agli | a + gli -all | a + l' -agl | a + gl' -alla | a + la -alle | a + le -con | with -col | con + il -coi | con + i (forms collo, cogli etc are now very rare) -da | from -dal | da + il -dallo | da + lo -dai | da + i -dagli | da + gli -dall | da + l' -dagl | da + gll' -dalla | da + la -dalle | da + le -di | of -del | di + il -dello | di + lo -dei | di + i -degli | di + gli -dell | di + l' -degl | di + gl' -della | di + la -delle | di + le -in | in -nel | in + el -nello | in + lo -nei | in + i -negli | in + gli -nell | in + l' -negl | in + gl' -nella | in + la -nelle | in + le -su | on -sul | su + il -sullo | su + lo -sui | su + i -sugli | su + gli -sull | su + l' -sugl | su + gl' -sulla | su + la -sulle | su + le -per | through, by -tra | among -contro | against -io | I -tu | thou -lui | he -lei | she -noi | we -voi | you -loro | they -mio | my -mia | -miei | -mie | -tuo | -tua | -tuoi | thy -tue | -suo | -sua | -suoi | his, her -sue | -nostro | our -nostra | -nostri | -nostre | -vostro | your -vostra | -vostri | -vostre | -mi | me -ti | thee -ci | us, there -vi | you, there -lo | him, the -la | her, the -li | them -le | them, the -gli | to him, the -ne | from there etc -il | the -un | a -uno | a -una | a -ma | but -ed | and -se | if -perché | why, because -anche | also -come | how -dov | where (as dov') -dove | where -che | who, that -chi | who -cui | whom -non | not -più | more -quale | who, that -quanto | how much -quanti | -quanta | -quante | -quello | that -quelli | -quella | -quelle | -questo | this -questi | -questa | -queste | -si | yes -tutto | all -tutti | all - - | single letter forms: - -a | at -c | as c' for ce or ci -e | and -i | the -l | as l' -o | or - - | forms of avere, to have (not including the infinitive): - -ho -hai -ha -abbiamo -avete -hanno -abbia -abbiate -abbiano -avrò -avrai -avrà -avremo -avrete -avranno -avrei -avresti -avrebbe -avremmo -avreste -avrebbero -avevo -avevi -aveva -avevamo -avevate -avevano -ebbi -avesti -ebbe -avemmo -aveste -ebbero -avessi -avesse -avessimo -avessero -avendo -avuto -avuta -avuti -avute - - | forms of essere, to be (not including the infinitive): -sono -sei -è -siamo -siete -sia -siate -siano -sarò -sarai -sarà -saremo -sarete -saranno -sarei -saresti -sarebbe -saremmo -sareste -sarebbero -ero -eri -era -eravamo -eravate -erano -fui -fosti -fu -fummo -foste -furono -fossi -fosse -fossimo -fossero -essendo - - | forms of fare, to do (not including the infinitive, fa, fat-): -faccio -fai -facciamo -fanno -faccia -facciate -facciano -farò -farai -farà -faremo -farete -faranno -farei -faresti -farebbe -faremmo -fareste -farebbero -facevo -facevi -faceva -facevamo -facevate -facevano -feci -facesti -fece -facemmo -faceste -fecero -facessi -facesse -facessimo -facessero -facendo - - | forms of stare, to be (not including the infinitive): -sto -stai -sta -stiamo -stanno -stia -stiate -stiano -starò -starai -starà -staremo -starete -staranno -starei -staresti -starebbe -staremmo -stareste -starebbero -stavo -stavi -stava -stavamo -stavate -stavano -stetti -stesti -stette -stemmo -steste -stettero -stessi -stesse -stessimo -stessero -stando diff --git a/resources/solr/configset/lang/stopwords_ja.txt b/resources/solr/configset/lang/stopwords_ja.txt deleted file mode 100644 index d4321be6b1..0000000000 --- a/resources/solr/configset/lang/stopwords_ja.txt +++ /dev/null @@ -1,127 +0,0 @@ -# -# This file defines a stopword set for Japanese. -# -# This set is made up of hand-picked frequent terms from segmented Japanese Wikipedia. -# Punctuation characters and frequent kanji have mostly been left out. See LUCENE-3745 -# for frequency lists, etc. that can be useful for making your own set (if desired) -# -# Note that there is an overlap between these stopwords and the terms stopped when used -# in combination with the JapanesePartOfSpeechStopFilter. When editing this file, note -# that comments are not allowed on the same line as stopwords. -# -# Also note that stopping is done in a case-insensitive manner. Change your StopFilter -# configuration if you need case-sensitive stopping. Lastly, note that stopping is done -# using the same character width as the entries in this file. Since this StopFilter is -# normally done after a CJKWidthFilter in your chain, you would usually want your romaji -# entries to be in half-width and your kana entries to be in full-width. -# -の -に -は -を -た -が -で -て -と -し -れ -さ -ある -いる -も -する -から -な -こと -として -い -や -れる -など -なっ -ない -この -ため -その -あっ -よう -また -もの -という -あり -まで -られ -なる -へ -か -だ -これ -によって -により -おり -より -による -ず -なり -られる -において -ば -なかっ -なく -しかし -について -せ -だっ -その後 -できる -それ -う -ので -なお -のみ -でき -き -つ -における -および -いう -さらに -でも -ら -たり -その他 -に関する -たち -ます -ん -なら -に対して -特に -せる -及び -これら -とき -では -にて -ほか -ながら -うち -そして -とともに -ただし -かつて -それぞれ -または -お -ほど -ものの -に対する -ほとんど -と共に -といった -です -とも -ところ -ここ -##### End of file diff --git a/resources/solr/configset/lang/stopwords_lv.txt b/resources/solr/configset/lang/stopwords_lv.txt deleted file mode 100644 index e21a23c06c..0000000000 --- a/resources/solr/configset/lang/stopwords_lv.txt +++ /dev/null @@ -1,172 +0,0 @@ -# Set of Latvian stopwords from A Stemming Algorithm for Latvian, Karlis Kreslins -# the original list of over 800 forms was refined: -# pronouns, adverbs, interjections were removed -# -# prepositions -aiz -ap -ar -apakš -ārpus -augšpus -bez -caur -dēļ -gar -iekš -iz -kopš -labad -lejpus -līdz -no -otrpus -pa -par -pār -pēc -pie -pirms -pret -priekš -starp -šaipus -uz -viņpus -virs -virspus -zem -apakšpus -# Conjunctions -un -bet -jo -ja -ka -lai -tomēr -tikko -turpretī -arī -kaut -gan -tādēļ -tā -ne -tikvien -vien -kā -ir -te -vai -kamēr -# Particles -ar -diezin -droši -diemžēl -nebūt -ik -it -taču -nu -pat -tiklab -iekšpus -nedz -tik -nevis -turpretim -jeb -iekam -iekām -iekāms -kolīdz -līdzko -tiklīdz -jebšu -tālab -tāpēc -nekā -itin -jā -jau -jel -nē -nezin -tad -tikai -vis -tak -iekams -vien -# modal verbs -būt -biju -biji -bija -bijām -bijāt -esmu -esi -esam -esat -būšu -būsi -būs -būsim -būsiet -tikt -tiku -tiki -tika -tikām -tikāt -tieku -tiec -tiek -tiekam -tiekat -tikšu -tiks -tiksim -tiksiet -tapt -tapi -tapāt -topat -tapšu -tapsi -taps -tapsim -tapsiet -kļūt -kļuvu -kļuvi -kļuva -kļuvām -kļuvāt -kļūstu -kļūsti -kļūst -kļūstam -kļūstat -kļūšu -kļūsi -kļūs -kļūsim -kļūsiet -# verbs -varēt -varēju -varējām -varēšu -varēsim -var -varēji -varējāt -varēsi -varēsiet -varat -varēja -varēs diff --git a/resources/solr/configset/lang/stopwords_nl.txt b/resources/solr/configset/lang/stopwords_nl.txt deleted file mode 100644 index 48c5515123..0000000000 --- a/resources/solr/configset/lang/stopwords_nl.txt +++ /dev/null @@ -1,121 +0,0 @@ - | From https://snowballstem.org/algorithms/dutch/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - - | A Dutch stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large sample of Dutch text. - - | Dutch stop words frequently exhibit homonym clashes. These are indicated - | clearly below. - -de | the -en | and -van | of, from -ik | I, the ego -te | (1) chez, at etc, (2) to, (3) too -dat | that, which -die | that, those, who, which -in | in, inside -een | a, an, one -hij | he -het | the, it -niet | not, nothing, naught -zijn | (1) to be, being, (2) his, one's, its -is | is -was | (1) was, past tense of all persons sing. of 'zijn' (to be) (2) wax, (3) the washing, (4) rise of river -op | on, upon, at, in, up, used up -aan | on, upon, to (as dative) -met | with, by -als | like, such as, when -voor | (1) before, in front of, (2) furrow -had | had, past tense all persons sing. of 'hebben' (have) -er | there -maar | but, only -om | round, about, for etc -hem | him -dan | then -zou | should/would, past tense all persons sing. of 'zullen' -of | or, whether, if -wat | what, something, anything -mijn | possessive and noun 'mine' -men | people, 'one' -dit | this -zo | so, thus, in this way -door | through by -over | over, across -ze | she, her, they, them -zich | oneself -bij | (1) a bee, (2) by, near, at -ook | also, too -tot | till, until -je | you -mij | me -uit | out of, from -der | Old Dutch form of 'van der' still found in surnames -daar | (1) there, (2) because -haar | (1) her, their, them, (2) hair -naar | (1) unpleasant, unwell etc, (2) towards, (3) as -heb | present first person sing. of 'to have' -hoe | how, why -heeft | present third person sing. of 'to have' -hebben | 'to have' and various parts thereof -deze | this -u | you -want | (1) for, (2) mitten, (3) rigging -nog | yet, still -zal | 'shall', first and third person sing. of verb 'zullen' (will) -me | me -zij | she, they -nu | now -ge | 'thou', still used in Belgium and south Netherlands -geen | none -omdat | because -iets | something, somewhat -worden | to become, grow, get -toch | yet, still -al | all, every, each -waren | (1) 'were' (2) to wander, (3) wares, (3) -veel | much, many -meer | (1) more, (2) lake -doen | to do, to make -toen | then, when -moet | noun 'spot/mote' and present form of 'to must' -ben | (1) am, (2) 'are' in interrogative second person singular of 'to be' -zonder | without -kan | noun 'can' and present form of 'to be able' -hun | their, them -dus | so, consequently -alles | all, everything, anything -onder | under, beneath -ja | yes, of course -eens | once, one day -hier | here -wie | who -werd | imperfect third person sing. of 'become' -altijd | always -doch | yet, but etc -wordt | present third person sing. of 'become' -wezen | (1) to be, (2) 'been' as in 'been fishing', (3) orphans -kunnen | to be able -ons | us/our -zelf | self -tegen | against, towards, at -na | after, near -reeds | already -wil | (1) present tense of 'want', (2) 'will', noun, (3) fender -kon | could; past tense of 'to be able' -niets | nothing -uw | your -iemand | somebody -geweest | been; past participle of 'be' -andere | other - diff --git a/resources/solr/configset/lang/stopwords_no.txt b/resources/solr/configset/lang/stopwords_no.txt deleted file mode 100644 index f427609484..0000000000 --- a/resources/solr/configset/lang/stopwords_no.txt +++ /dev/null @@ -1,190 +0,0 @@ - | From https://snowballstem.org/algorithms/norwegian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Norwegian stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This stop word list is for the dominant bokmål dialect. Words unique - | to nynorsk are marked *. - - | Revised by Jan Bruusgaard , Jan 2005 - -og | and -i | in -jeg | I -det | it/this/that -at | to (w. inf.) -en | a/an -et | a/an -den | it/this/that -til | to -er | is/am/are -som | who/which/that -på | on -de | they / you(formal) -med | with -han | he -av | of -ikke | not -ikkje | not * -der | there -så | so -var | was/were -meg | me -seg | you -men | but -ett | one -har | have -om | about -vi | we -min | my -mitt | my -ha | have -hadde | had -hun | she -nå | now -over | over -da | when/as -ved | by/know -fra | from -du | you -ut | out -sin | your -dem | them -oss | us -opp | up -man | you/one -kan | can -hans | his -hvor | where -eller | or -hva | what -skal | shall/must -selv | self (reflective) -sjøl | self (reflective) -her | here -alle | all -vil | will -bli | become -ble | became -blei | became * -blitt | have become -kunne | could -inn | in -når | when -være | be -kom | come -noen | some -noe | some -ville | would -dere | you -deres | their/theirs -kun | only/just -ja | yes -etter | after -ned | down -skulle | should -denne | this -for | for/because -deg | you -si | hers/his -sine | hers/his -sitt | hers/his -mot | against -å | to -meget | much -hvorfor | why -dette | this -disse | these/those -uten | without -hvordan | how -ingen | none -din | your -ditt | your -blir | become -samme | same -hvilken | which -hvilke | which (plural) -sånn | such a -inni | inside/within -mellom | between -vår | our -hver | each -hvem | who -vors | us/ours -hvis | whose -både | both -bare | only/just -enn | than -fordi | as/because -før | before -mange | many -også | also -slik | just -vært | been -båe | both * -begge | both -siden | since -dykk | your * -dykkar | yours * -dei | they * -deira | them * -deires | theirs * -deim | them * -di | your (fem.) * -då | as/when * -eg | I * -ein | a/an * -eit | a/an * -eitt | a/an * -elles | or * -honom | he * -hjå | at * -ho | she * -hoe | she * -henne | her -hennar | her/hers -hennes | hers -hoss | how * -hossen | how * -ingi | noone * -inkje | noone * -korleis | how * -korso | how * -kva | what/which * -kvar | where * -kvarhelst | where * -kven | who/whom * -kvi | why * -kvifor | why * -me | we * -medan | while * -mi | my * -mine | my * -mykje | much * -no | now * -nokon | some (masc./neut.) * -noka | some (fem.) * -nokor | some * -noko | some * -nokre | some * -sia | since * -sidan | since * -so | so * -somt | some * -somme | some * -um | about* -upp | up * -vere | be * -vore | was * -verte | become * -vort | become * -varte | became * -vart | became * - diff --git a/resources/solr/configset/lang/stopwords_pt.txt b/resources/solr/configset/lang/stopwords_pt.txt deleted file mode 100644 index d03d7f234d..0000000000 --- a/resources/solr/configset/lang/stopwords_pt.txt +++ /dev/null @@ -1,253 +0,0 @@ - | From https://snowballstem.org/algorithms/portuguese/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Portuguese stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - - | The following is a ranked list (commonest to rarest) of stopwords - | deriving from a large sample of text. - - | Extra words have been added at the end. - -de | of, from -a | the; to, at; her -o | the; him -que | who, that -e | and -do | de + o -da | de + a -em | in -um | a -para | for - | é from SER -com | with -não | not, no -uma | a -os | the; them -no | em + o -se | himself etc -na | em + a -por | for -mais | more -as | the; them -dos | de + os -como | as, like -mas | but - | foi from SER -ao | a + o -ele | he -das | de + as - | tem from TER -à | a + a -seu | his -sua | her -ou | or - | ser from SER -quando | when -muito | much - | há from HAV -nos | em + os; us -já | already, now - | está from EST -eu | I -também | also -só | only, just -pelo | per + o -pela | per + a -até | up to -isso | that -ela | he -entre | between - | era from SER -depois | after -sem | without -mesmo | same -aos | a + os - | ter from TER -seus | his -quem | whom -nas | em + as -me | me -esse | that -eles | they - | estão from EST -você | you - | tinha from TER - | foram from SER -essa | that -num | em + um -nem | nor -suas | her -meu | my -às | a + as -minha | my - | têm from TER -numa | em + uma -pelos | per + os -elas | they - | havia from HAV - | seja from SER -qual | which - | será from SER -nós | we - | tenho from TER -lhe | to him, her -deles | of them -essas | those -esses | those -pelas | per + as -este | this - | fosse from SER -dele | of him - - | other words. There are many contractions such as naquele = em+aquele, - | mo = me+o, but they are rare. - | Indefinite article plural forms are also rare. - -tu | thou -te | thee -vocês | you (plural) -vos | you -lhes | to them -meus | my -minhas -teu | thy -tua -teus -tuas -nosso | our -nossa -nossos -nossas - -dela | of her -delas | of them - -esta | this -estes | these -estas | these -aquele | that -aquela | that -aqueles | those -aquelas | those -isto | this -aquilo | that - - | forms of estar, to be (not including the infinitive): -estou -está -estamos -estão -estive -esteve -estivemos -estiveram -estava -estávamos -estavam -estivera -estivéramos -esteja -estejamos -estejam -estivesse -estivéssemos -estivessem -estiver -estivermos -estiverem - - | forms of haver, to have (not including the infinitive): -hei -há -havemos -hão -houve -houvemos -houveram -houvera -houvéramos -haja -hajamos -hajam -houvesse -houvéssemos -houvessem -houver -houvermos -houverem -houverei -houverá -houveremos -houverão -houveria -houveríamos -houveriam - - | forms of ser, to be (not including the infinitive): -sou -somos -são -era -éramos -eram -fui -foi -fomos -foram -fora -fôramos -seja -sejamos -sejam -fosse -fôssemos -fossem -for -formos -forem -serei -será -seremos -serão -seria -seríamos -seriam - - | forms of ter, to have (not including the infinitive): -tenho -tem -temos -tém -tinha -tínhamos -tinham -tive -teve -tivemos -tiveram -tivera -tivéramos -tenha -tenhamos -tenham -tivesse -tivéssemos -tivessem -tiver -tivermos -tiverem -terei -terá -teremos -terão -teria -teríamos -teriam diff --git a/resources/solr/configset/lang/stopwords_ro.txt b/resources/solr/configset/lang/stopwords_ro.txt deleted file mode 100644 index 4fdee90a5b..0000000000 --- a/resources/solr/configset/lang/stopwords_ro.txt +++ /dev/null @@ -1,233 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -acea -aceasta -această -aceea -acei -aceia -acel -acela -acele -acelea -acest -acesta -aceste -acestea -aceşti -aceştia -acolo -acum -ai -aia -aibă -aici -al -ăla -ale -alea -ălea -altceva -altcineva -am -ar -are -aş -aşadar -asemenea -asta -ăsta -astăzi -astea -ăstea -ăştia -asupra -aţi -au -avea -avem -aveţi -azi -bine -bucur -bună -ca -că -căci -când -care -cărei -căror -cărui -cât -câte -câţi -către -câtva -ce -cel -ceva -chiar -cînd -cine -cineva -cît -cîte -cîţi -cîtva -contra -cu -cum -cumva -curând -curînd -da -dă -dacă -dar -datorită -de -deci -deja -deoarece -departe -deşi -din -dinaintea -dintr -dintre -drept -după -ea -ei -el -ele -eram -este -eşti -eu -face -fără -fi -fie -fiecare -fii -fim -fiţi -iar -ieri -îi -îl -îmi -împotriva -în -înainte -înaintea -încât -încît -încotro -între -întrucât -întrucît -îţi -la -lângă -le -li -lîngă -lor -lui -mă -mâine -mea -mei -mele -mereu -meu -mi -mine -mult -multă -mulţi -ne -nicăieri -nici -nimeni -nişte -noastră -noastre -noi -noştri -nostru -nu -ori -oricând -oricare -oricât -orice -oricînd -oricine -oricît -oricum -oriunde -până -pe -pentru -peste -pînă -poate -pot -prea -prima -primul -prin -printr -sa -să -săi -sale -sau -său -se -şi -sînt -sîntem -sînteţi -spre -sub -sunt -suntem -sunteţi -ta -tăi -tale -tău -te -ţi -ţie -tine -toată -toate -tot -toţi -totuşi -tu -un -una -unde -undeva -unei -unele -uneori -unor -vă -vi -voastră -voastre -voi -voştri -vostru -vouă -vreo -vreun diff --git a/resources/solr/configset/lang/stopwords_ru.txt b/resources/solr/configset/lang/stopwords_ru.txt deleted file mode 100644 index 65512d49db..0000000000 --- a/resources/solr/configset/lang/stopwords_ru.txt +++ /dev/null @@ -1,244 +0,0 @@ - | From https://snowballstem.org/algorithms/russian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - - | a russian stop word list. comments begin with vertical bar. each stop - | word is at the start of a line. - - | this is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - | letter `ё' is translated to `е'. - -и | and -в | in/into -во | alternative form -не | not -что | what/that -он | he -на | on/onto -я | i -с | from -со | alternative form -как | how -а | milder form of `no' (but) -то | conjunction and form of `that' -все | all -она | she -так | so, thus -его | him -но | but -да | yes/and -ты | thou -к | towards, by -у | around, chez -же | intensifier particle -вы | you -за | beyond, behind -бы | conditional/subj. particle -по | up to, along -только | only -ее | her -мне | to me -было | it was -вот | here is/are, particle -от | away from -меня | me -еще | still, yet, more -нет | no, there isnt/arent -о | about -из | out of -ему | to him -теперь | now -когда | when -даже | even -ну | so, well -вдруг | suddenly -ли | interrogative particle -если | if -уже | already, but homonym of `narrower' -или | or -ни | neither -быть | to be -был | he was -него | prepositional form of его -до | up to -вас | you accusative -нибудь | indef. suffix preceded by hyphen -опять | again -уж | already, but homonym of `adder' -вам | to you -сказал | he said -ведь | particle `after all' -там | there -потом | then -себя | oneself -ничего | nothing -ей | to her -может | usually with `быть' as `maybe' -они | they -тут | here -где | where -есть | there is/are -надо | got to, must -ней | prepositional form of ей -для | for -мы | we -тебя | thee -их | them, their -чем | than -была | she was -сам | self -чтоб | in order to -без | without -будто | as if -человек | man, person, one -чего | genitive form of `what' -раз | once -тоже | also -себе | to oneself -под | beneath -жизнь | life -будет | will be -ж | short form of intensifer particle `же' -тогда | then -кто | who -этот | this -говорил | was saying -того | genitive form of `that' -потому | for that reason -этого | genitive form of `this' -какой | which -совсем | altogether -ним | prepositional form of `его', `они' -здесь | here -этом | prepositional form of `этот' -один | one -почти | almost -мой | my -тем | instrumental/dative plural of `тот', `то' -чтобы | full form of `in order that' -нее | her (acc.) -кажется | it seems -сейчас | now -были | they were -куда | where to -зачем | why -сказать | to say -всех | all (acc., gen. preposn. plural) -никогда | never -сегодня | today -можно | possible, one can -при | by -наконец | finally -два | two -об | alternative form of `о', about -другой | another -хоть | even -после | after -над | above -больше | more -тот | that one (masc.) -через | across, in -эти | these -нас | us -про | about -всего | in all, only, of all -них | prepositional form of `они' (they) -какая | which, feminine -много | lots -разве | interrogative particle -сказала | she said -три | three -эту | this, acc. fem. sing. -моя | my, feminine -впрочем | moreover, besides -хорошо | good -свою | ones own, acc. fem. sing. -этой | oblique form of `эта', fem. `this' -перед | in front of -иногда | sometimes -лучше | better -чуть | a little -том | preposn. form of `that one' -нельзя | one must not -такой | such a one -им | to them -более | more -всегда | always -конечно | of course -всю | acc. fem. sing of `all' -между | between - - - | b: some paradigms - | - | personal pronouns - | - | я меня мне мной [мною] - | ты тебя тебе тобой [тобою] - | он его ему им [него, нему, ним] - | она ее эи ею [нее, нэи, нею] - | оно его ему им [него, нему, ним] - | - | мы нас нам нами - | вы вас вам вами - | они их им ими [них, ним, ними] - | - | себя себе собой [собою] - | - | demonstrative pronouns: этот (this), тот (that) - | - | этот эта это эти - | этого эты это эти - | этого этой этого этих - | этому этой этому этим - | этим этой этим [этою] этими - | этом этой этом этих - | - | тот та то те - | того ту то те - | того той того тех - | тому той тому тем - | тем той тем [тою] теми - | том той том тех - | - | determinative pronouns - | - | (a) весь (all) - | - | весь вся все все - | всего всю все все - | всего всей всего всех - | всему всей всему всем - | всем всей всем [всею] всеми - | всем всей всем всех - | - | (b) сам (himself etc) - | - | сам сама само сами - | самого саму само самих - | самого самой самого самих - | самому самой самому самим - | самим самой самим [самою] самими - | самом самой самом самих - | - | stems of verbs `to be', `to have', `to do' and modal - | - | быть бы буд быв есть суть - | име - | дел - | мог мож мочь - | уме - | хоч хот - | долж - | можн - | нужн - | нельзя - diff --git a/resources/solr/configset/lang/stopwords_sv.txt b/resources/solr/configset/lang/stopwords_sv.txt deleted file mode 100644 index d1d0d10088..0000000000 --- a/resources/solr/configset/lang/stopwords_sv.txt +++ /dev/null @@ -1,133 +0,0 @@ - | From https://snowballstem.org/algorithms/swedish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Swedish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - | Swedish stop words occasionally exhibit homonym clashes. For example - | så = so, but also seed. These are indicated clearly below. - -och | and -det | it, this/that -att | to (with infinitive) -i | in, at -en | a -jag | I -hon | she -som | who, that -han | he -på | on -den | it, this/that -med | with -var | where, each -sig | him(self) etc -för | for -så | so (also: seed) -till | to -är | is -men | but -ett | a -om | if; around, about -hade | had -de | they, these/those -av | of -icke | not, no -mig | me -du | you -henne | her -då | then, when -sin | his -nu | now -har | have -inte | inte någon = no one -hans | his -honom | him -skulle | 'sake' -hennes | her -där | there -min | my -man | one (pronoun) -ej | nor -vid | at, by, on (also: vast) -kunde | could -något | some etc -från | from, off -ut | out -när | when -efter | after, behind -upp | up -vi | we -dem | them -vara | be -vad | what -över | over -än | than -dig | you -kan | can -sina | his -här | here -ha | have -mot | towards -alla | all -under | under (also: wonder) -någon | some etc -eller | or (else) -allt | all -mycket | much -sedan | since -ju | why -denna | this/that -själv | myself, yourself etc -detta | this/that -åt | to -utan | without -varit | was -hur | how -ingen | no -mitt | my -ni | you -bli | to be, become -blev | from bli -oss | us -din | thy -dessa | these/those -några | some etc -deras | their -blir | from bli -mina | my -samma | (the) same -vilken | who, that -er | you, your -sådan | such a -vår | our -blivit | from bli -dess | its -inom | within -mellan | between -sådant | such a -varför | why -varje | each -vilka | who, that -ditt | thy -vem | who -vilket | who, that -sitt | his -sådana | such a -vart | each -dina | thy -vars | whose -vårt | our -våra | our -ert | your -era | your -vilkas | whose - diff --git a/resources/solr/configset/lang/stopwords_th.txt b/resources/solr/configset/lang/stopwords_th.txt deleted file mode 100644 index 07f0fabe69..0000000000 --- a/resources/solr/configset/lang/stopwords_th.txt +++ /dev/null @@ -1,119 +0,0 @@ -# Thai stopwords from: -# "Opinion Detection in Thai Political News Columns -# Based on Subjectivity Analysis" -# Khampol Sukhum, Supot Nitsuwat, and Choochart Haruechaiyasak -ไว้ -ไม่ -ไป -ได้ -ให้ -ใน -โดย -แห่ง -แล้ว -และ -แรก -แบบ -แต่ -เอง -เห็น -เลย -เริ่ม -เรา -เมื่อ -เพื่อ -เพราะ -เป็นการ -เป็น -เปิดเผย -เปิด -เนื่องจาก -เดียวกัน -เดียว -เช่น -เฉพาะ -เคย -เข้า -เขา -อีก -อาจ -อะไร -ออก -อย่าง -อยู่ -อยาก -หาก -หลาย -หลังจาก -หลัง -หรือ -หนึ่ง -ส่วน -ส่ง -สุด -สําหรับ -ว่า -วัน -ลง -ร่วม -ราย -รับ -ระหว่าง -รวม -ยัง -มี -มาก -มา -พร้อม -พบ -ผ่าน -ผล -บาง -น่า -นี้ -นํา -นั้น -นัก -นอกจาก -ทุก -ที่สุด -ที่ -ทําให้ -ทํา -ทาง -ทั้งนี้ -ทั้ง -ถ้า -ถูก -ถึง -ต้อง -ต่างๆ -ต่าง -ต่อ -ตาม -ตั้งแต่ -ตั้ง -ด้าน -ด้วย -ดัง -ซึ่ง -ช่วง -จึง -จาก -จัด -จะ -คือ -ความ -ครั้ง -คง -ขึ้น -ของ -ขอ -ขณะ -ก่อน -ก็ -การ -กับ -กัน -กว่า -กล่าว diff --git a/resources/solr/configset/lang/stopwords_tr.txt b/resources/solr/configset/lang/stopwords_tr.txt deleted file mode 100644 index 84d9408d4e..0000000000 --- a/resources/solr/configset/lang/stopwords_tr.txt +++ /dev/null @@ -1,212 +0,0 @@ -# Turkish stopwords from LUCENE-559 -# merged with the list from "Information Retrieval on Turkish Texts" -# (http://www.users.muohio.edu/canf/papers/JASIST2008offPrint.pdf) -acaba -altmış -altı -ama -ancak -arada -aslında -ayrıca -bana -bazı -belki -ben -benden -beni -benim -beri -beş -bile -bin -bir -birçok -biri -birkaç -birkez -birşey -birşeyi -biz -bize -bizden -bizi -bizim -böyle -böylece -bu -buna -bunda -bundan -bunlar -bunları -bunların -bunu -bunun -burada -çok -çünkü -da -daha -dahi -de -defa -değil -diğer -diye -doksan -dokuz -dolayı -dolayısıyla -dört -edecek -eden -ederek -edilecek -ediliyor -edilmesi -ediyor -eğer -elli -en -etmesi -etti -ettiği -ettiğini -gibi -göre -halen -hangi -hatta -hem -henüz -hep -hepsi -her -herhangi -herkesin -hiç -hiçbir -için -iki -ile -ilgili -ise -işte -itibaren -itibariyle -kadar -karşın -katrilyon -kendi -kendilerine -kendini -kendisi -kendisine -kendisini -kez -ki -kim -kimden -kime -kimi -kimse -kırk -milyar -milyon -mu -mü -mı -nasıl -ne -neden -nedenle -nerde -nerede -nereye -niye -niçin -o -olan -olarak -oldu -olduğu -olduğunu -olduklarını -olmadı -olmadığı -olmak -olması -olmayan -olmaz -olsa -olsun -olup -olur -olursa -oluyor -on -ona -ondan -onlar -onlardan -onları -onların -onu -onun -otuz -oysa -öyle -pek -rağmen -sadece -sanki -sekiz -seksen -sen -senden -seni -senin -siz -sizden -sizi -sizin -şey -şeyden -şeyi -şeyler -şöyle -şu -şuna -şunda -şundan -şunları -şunu -tarafından -trilyon -tüm -üç -üzere -var -vardı -ve -veya -ya -yani -yapacak -yapılan -yapılması -yapıyor -yapmak -yaptı -yaptığı -yaptığını -yaptıkları -yedi -yerine -yetmiş -yine -yirmi -yoksa -yüz -zaten diff --git a/resources/solr/configset/lang/userdict_ja.txt b/resources/solr/configset/lang/userdict_ja.txt deleted file mode 100644 index 6f0368e4d8..0000000000 --- a/resources/solr/configset/lang/userdict_ja.txt +++ /dev/null @@ -1,29 +0,0 @@ -# -# This is a sample user dictionary for Kuromoji (JapaneseTokenizer) -# -# Add entries to this file in order to override the statistical model in terms -# of segmentation, readings and part-of-speech tags. Notice that entries do -# not have weights since they are always used when found. This is by-design -# in order to maximize ease-of-use. -# -# Entries are defined using the following CSV format: -# , ... , ... , -# -# Notice that a single half-width space separates tokens and readings, and -# that the number tokens and readings must match exactly. -# -# Also notice that multiple entries with the same is undefined. -# -# Whitespace only lines are ignored. Comments are not allowed on entry lines. -# - -# Custom segmentation for kanji compounds -日本経済新聞,日本 経済 新聞,ニホン ケイザイ シンブン,カスタム名詞 -関西国際空港,関西 国際 空港,カンサイ コクサイ クウコウ,カスタム名詞 - -# Custom segmentation for compound katakana -トートバッグ,トート バッグ,トート バッグ,かずカナ名詞 -ショルダーバッグ,ショルダー バッグ,ショルダー バッグ,かずカナ名詞 - -# Custom reading for former sumo wrestler -朝青龍,朝青龍,アサショウリュウ,カスタム人名 diff --git a/resources/solr/configset/managed-schema.xml b/resources/solr/configset/managed-schema.xml deleted file mode 100644 index f31768bd84..0000000000 --- a/resources/solr/configset/managed-schema.xml +++ /dev/null @@ -1,1078 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - id - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/solr/configset/protwords.txt b/resources/solr/configset/protwords.txt deleted file mode 100644 index 1dfc0abecb..0000000000 --- a/resources/solr/configset/protwords.txt +++ /dev/null @@ -1,21 +0,0 @@ -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -#----------------------------------------------------------------------- -# Use a protected word file to protect against the stemmer reducing two -# unrelated words to the same base word. - -# Some non-words that normally won't be encountered, -# just to test that they won't be stemmed. -dontstems -zwhacky - diff --git a/resources/solr/configset/solrconfig.xml b/resources/solr/configset/solrconfig.xml deleted file mode 100644 index 9bef924011..0000000000 --- a/resources/solr/configset/solrconfig.xml +++ /dev/null @@ -1,1026 +0,0 @@ - - - - - - - - - 9.12 - - - ${solr.data.dir:} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ${solr.lock.type:native} - - - - - - - - - - - - - - - - - - - - - ${solr.ulog.dir:} - - - - - ${solr.autoCommit.maxTime:15000} - false - - - - - - ${solr.autoSoftCommit.maxTime:3000} - - - - - - - - - - - - - - ${solr.max.booleanClauses:1024} - - - ${solr.query.minPrefixLength:-1} - - - - - - - - - - - - - - - - - - - - - - - - true - - - - - - 20 - - - 200 - - - - - - - - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - explicit - 10 - - - - - - - explicit - json - true - - - - - - - _text_ - - - - - - - text_general - - - - - - default - _text_ - solr.DirectSolrSpellChecker - - internal - - 0.5 - - 2 - - 1 - - 5 - - 4 - - 0.01 - - - - - - - - - - - - default - on - true - 10 - 5 - 5 - true - true - 10 - 5 - - - spellcheck - - - - - - - - - - - - 100 - - - - - - - - 70 - - 0.5 - - [-\w ,/\n\"']{20,200} - - - - - - - ]]> - ]]> - - - - - - - - - - - - - - - - - - - - - - - - ,, - ,, - ,, - ,, - ,]]> - ]]> - - - - - - 10 - .,!? - - - - - - - WORD - - - en - US - - - - - - - - - - - - [^\w-\.] - _ - - - 1000 - true - - - - - - - yyyy-MM-dd['T'[HH:mm[:ss[.SSS]][z - yyyy-MM-dd['T'[HH:mm[:ss[,SSS]][z - yyyy-MM-dd HH:mm[:ss[.SSS]][z - yyyy-MM-dd HH:mm[:ss[,SSS]][z - [EEE, ]dd MMM yyyy HH:mm[:ss] z - EEEE, dd-MMM-yy HH:mm:ss z - EEE MMM ppd HH:mm:ss [z ]yyyy - - - - - java.lang.String - text_general - - *_str - 256 - - - true - - - java.lang.Boolean - booleans - - - java.util.Date - pdates - - - java.lang.Long - java.lang.Integer - plongs - - - java.lang.Number - pdoubles - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/solr/configset/stopwords.txt b/resources/solr/configset/stopwords.txt deleted file mode 100644 index ae1e83eeb3..0000000000 --- a/resources/solr/configset/stopwords.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/resources/solr/configset/synonyms.txt b/resources/solr/configset/synonyms.txt deleted file mode 100644 index eab4ee8753..0000000000 --- a/resources/solr/configset/synonyms.txt +++ /dev/null @@ -1,29 +0,0 @@ -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -#----------------------------------------------------------------------- -#some test synonym mappings unlikely to appear in real input text -aaafoo => aaabar -bbbfoo => bbbfoo bbbbar -cccfoo => cccbar cccbaz -fooaaa,baraaa,bazaaa - -# Some synonym groups specific to this example -GB,gib,gigabyte,gigabytes -MB,mib,megabyte,megabytes -Television, Televisions, TV, TVs -#notice we use "gib" instead of "GiB" so any WordDelimiterGraphFilter coming -#after us won't split it into two words. - -# Synonym mappings can be used for spelling correction too -pixima => pixma - diff --git a/resources/solr/configset/zknode.data b/resources/solr/configset/zknode.data deleted file mode 100644 index af36cc87b2..0000000000 --- a/resources/solr/configset/zknode.data +++ /dev/null @@ -1 +0,0 @@ -{"trusted":true} \ No newline at end of file diff --git a/resources/solr/default_configset/lang/contractions_ca.txt b/resources/solr/default_configset/lang/contractions_ca.txt deleted file mode 100644 index 307a85f913..0000000000 --- a/resources/solr/default_configset/lang/contractions_ca.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Set of Catalan contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -d -l -m -n -s -t diff --git a/resources/solr/default_configset/lang/contractions_fr.txt b/resources/solr/default_configset/lang/contractions_fr.txt deleted file mode 100644 index f1bba51b23..0000000000 --- a/resources/solr/default_configset/lang/contractions_fr.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Set of French contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -l -m -t -qu -n -s -j -d -c -jusqu -quoiqu -lorsqu -puisqu diff --git a/resources/solr/default_configset/lang/contractions_ga.txt b/resources/solr/default_configset/lang/contractions_ga.txt deleted file mode 100644 index 9ebe7fa349..0000000000 --- a/resources/solr/default_configset/lang/contractions_ga.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Set of Irish contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -d -m -b diff --git a/resources/solr/default_configset/lang/contractions_it.txt b/resources/solr/default_configset/lang/contractions_it.txt deleted file mode 100644 index cac0409537..0000000000 --- a/resources/solr/default_configset/lang/contractions_it.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Set of Italian contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -c -l -all -dall -dell -nell -sull -coll -pell -gl -agl -dagl -degl -negl -sugl -un -m -t -s -v -d diff --git a/resources/solr/default_configset/lang/hyphenations_ga.txt b/resources/solr/default_configset/lang/hyphenations_ga.txt deleted file mode 100644 index 4d2642cc5a..0000000000 --- a/resources/solr/default_configset/lang/hyphenations_ga.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Set of Irish hyphenations for StopFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -h -n -t diff --git a/resources/solr/default_configset/lang/stemdict_nl.txt b/resources/solr/default_configset/lang/stemdict_nl.txt deleted file mode 100644 index 441072971d..0000000000 --- a/resources/solr/default_configset/lang/stemdict_nl.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Set of overrides for the dutch stemmer -# TODO: load this as a resource from the analyzer and sync it in build.xml -fiets fiets -bromfiets bromfiets -ei eier -kind kinder diff --git a/resources/solr/default_configset/lang/stoptags_ja.txt b/resources/solr/default_configset/lang/stoptags_ja.txt deleted file mode 100644 index 71b750845e..0000000000 --- a/resources/solr/default_configset/lang/stoptags_ja.txt +++ /dev/null @@ -1,420 +0,0 @@ -# -# This file defines a Japanese stoptag set for JapanesePartOfSpeechStopFilter. -# -# Any token with a part-of-speech tag that exactly matches those defined in this -# file are removed from the token stream. -# -# Set your own stoptags by uncommenting the lines below. Note that comments are -# not allowed on the same line as a stoptag. See LUCENE-3745 for frequency lists, -# etc. that can be useful for building you own stoptag set. -# -# The entire possible tagset is provided below for convenience. -# -##### -# noun: unclassified nouns -#名詞 -# -# noun-common: Common nouns or nouns where the sub-classification is undefined -#名詞-一般 -# -# noun-proper: Proper nouns where the sub-classification is undefined -#名詞-固有名詞 -# -# noun-proper-misc: miscellaneous proper nouns -#名詞-固有名詞-一般 -# -# noun-proper-person: Personal names where the sub-classification is undefined -#名詞-固有名詞-人名 -# -# noun-proper-person-misc: names that cannot be divided into surname and -# given name; foreign names; names where the surname or given name is unknown. -# e.g. お市の方 -#名詞-固有名詞-人名-一般 -# -# noun-proper-person-surname: Mainly Japanese surnames. -# e.g. 山田 -#名詞-固有名詞-人名-姓 -# -# noun-proper-person-given_name: Mainly Japanese given names. -# e.g. 太郎 -#名詞-固有名詞-人名-名 -# -# noun-proper-organization: Names representing organizations. -# e.g. 通産省, NHK -#名詞-固有名詞-組織 -# -# noun-proper-place: Place names where the sub-classification is undefined -#名詞-固有名詞-地域 -# -# noun-proper-place-misc: Place names excluding countries. -# e.g. アジア, バルセロナ, 京都 -#名詞-固有名詞-地域-一般 -# -# noun-proper-place-country: Country names. -# e.g. 日本, オーストラリア -#名詞-固有名詞-地域-国 -# -# noun-pronoun: Pronouns where the sub-classification is undefined -#名詞-代名詞 -# -# noun-pronoun-misc: miscellaneous pronouns: -# e.g. それ, ここ, あいつ, あなた, あちこち, いくつ, どこか, なに, みなさん, みんな, わたくし, われわれ -#名詞-代名詞-一般 -# -# noun-pronoun-contraction: Spoken language contraction made by combining a -# pronoun and the particle 'wa'. -# e.g. ありゃ, こりゃ, こりゃあ, そりゃ, そりゃあ -#名詞-代名詞-縮約 -# -# noun-adverbial: Temporal nouns such as names of days or months that behave -# like adverbs. Nouns that represent amount or ratios and can be used adverbially, -# e.g. 金曜, 一月, 午後, 少量 -#名詞-副詞可能 -# -# noun-verbal: Nouns that take arguments with case and can appear followed by -# 'suru' and related verbs (する, できる, なさる, くださる) -# e.g. インプット, 愛着, 悪化, 悪戦苦闘, 一安心, 下取り -#名詞-サ変接続 -# -# noun-adjective-base: The base form of adjectives, words that appear before な ("na") -# e.g. 健康, 安易, 駄目, だめ -#名詞-形容動詞語幹 -# -# noun-numeric: Arabic numbers, Chinese numerals, and counters like 何 (回), 数. -# e.g. 0, 1, 2, 何, 数, 幾 -#名詞-数 -# -# noun-affix: noun affixes where the sub-classification is undefined -#名詞-非自立 -# -# noun-affix-misc: Of adnominalizers, the case-marker の ("no"), and words that -# attach to the base form of inflectional words, words that cannot be classified -# into any of the other categories below. This category includes indefinite nouns. -# e.g. あかつき, 暁, かい, 甲斐, 気, きらい, 嫌い, くせ, 癖, こと, 事, ごと, 毎, しだい, 次第, -# 順, せい, 所為, ついで, 序で, つもり, 積もり, 点, どころ, の, はず, 筈, はずみ, 弾み, -# 拍子, ふう, ふり, 振り, ほう, 方, 旨, もの, 物, 者, ゆえ, 故, ゆえん, 所以, わけ, 訳, -# わり, 割り, 割, ん-口語/, もん-口語/ -#名詞-非自立-一般 -# -# noun-affix-adverbial: noun affixes that that can behave as adverbs. -# e.g. あいだ, 間, あげく, 挙げ句, あと, 後, 余り, 以外, 以降, 以後, 以上, 以前, 一方, うえ, -# 上, うち, 内, おり, 折り, かぎり, 限り, きり, っきり, 結果, ころ, 頃, さい, 際, 最中, さなか, -# 最中, じたい, 自体, たび, 度, ため, 為, つど, 都度, とおり, 通り, とき, 時, ところ, 所, -# とたん, 途端, なか, 中, のち, 後, ばあい, 場合, 日, ぶん, 分, ほか, 他, まえ, 前, まま, -# 儘, 侭, みぎり, 矢先 -#名詞-非自立-副詞可能 -# -# noun-affix-aux: noun affixes treated as 助動詞 ("auxiliary verb") in school grammars -# with the stem よう(だ) ("you(da)"). -# e.g. よう, やう, 様 (よう) -#名詞-非自立-助動詞語幹 -# -# noun-affix-adjective-base: noun affixes that can connect to the indeclinable -# connection form な (aux "da"). -# e.g. みたい, ふう -#名詞-非自立-形容動詞語幹 -# -# noun-special: special nouns where the sub-classification is undefined. -#名詞-特殊 -# -# noun-special-aux: The そうだ ("souda") stem form that is used for reporting news, is -# treated as 助動詞 ("auxiliary verb") in school grammars, and attach to the base -# form of inflectional words. -# e.g. そう -#名詞-特殊-助動詞語幹 -# -# noun-suffix: noun suffixes where the sub-classification is undefined. -#名詞-接尾 -# -# noun-suffix-misc: Of the nouns or stem forms of other parts of speech that connect -# to ガル or タイ and can combine into compound nouns, words that cannot be classified into -# any of the other categories below. In general, this category is more inclusive than -# 接尾語 ("suffix") and is usually the last element in a compound noun. -# e.g. おき, かた, 方, 甲斐 (がい), がかり, ぎみ, 気味, ぐるみ, (~した) さ, 次第, 済 (ず) み, -# よう, (でき)っこ, 感, 観, 性, 学, 類, 面, 用 -#名詞-接尾-一般 -# -# noun-suffix-person: Suffixes that form nouns and attach to person names more often -# than other nouns. -# e.g. 君, 様, 著 -#名詞-接尾-人名 -# -# noun-suffix-place: Suffixes that form nouns and attach to place names more often -# than other nouns. -# e.g. 町, 市, 県 -#名詞-接尾-地域 -# -# noun-suffix-verbal: Of the suffixes that attach to nouns and form nouns, those that -# can appear before スル ("suru"). -# e.g. 化, 視, 分け, 入り, 落ち, 買い -#名詞-接尾-サ変接続 -# -# noun-suffix-aux: The stem form of そうだ (様態) that is used to indicate conditions, -# is treated as 助動詞 ("auxiliary verb") in school grammars, and attach to the -# conjunctive form of inflectional words. -# e.g. そう -#名詞-接尾-助動詞語幹 -# -# noun-suffix-adjective-base: Suffixes that attach to other nouns or the conjunctive -# form of inflectional words and appear before the copula だ ("da"). -# e.g. 的, げ, がち -#名詞-接尾-形容動詞語幹 -# -# noun-suffix-adverbial: Suffixes that attach to other nouns and can behave as adverbs. -# e.g. 後 (ご), 以後, 以降, 以前, 前後, 中, 末, 上, 時 (じ) -#名詞-接尾-副詞可能 -# -# noun-suffix-classifier: Suffixes that attach to numbers and form nouns. This category -# is more inclusive than 助数詞 ("classifier") and includes common nouns that attach -# to numbers. -# e.g. 個, つ, 本, 冊, パーセント, cm, kg, カ月, か国, 区画, 時間, 時半 -#名詞-接尾-助数詞 -# -# noun-suffix-special: Special suffixes that mainly attach to inflecting words. -# e.g. (楽し) さ, (考え) 方 -#名詞-接尾-特殊 -# -# noun-suffix-conjunctive: Nouns that behave like conjunctions and join two words -# together. -# e.g. (日本) 対 (アメリカ), 対 (アメリカ), (3) 対 (5), (女優) 兼 (主婦) -#名詞-接続詞的 -# -# noun-verbal_aux: Nouns that attach to the conjunctive particle て ("te") and are -# semantically verb-like. -# e.g. ごらん, ご覧, 御覧, 頂戴 -#名詞-動詞非自立的 -# -# noun-quotation: text that cannot be segmented into words, proverbs, Chinese poetry, -# dialects, English, etc. Currently, the only entry for 名詞 引用文字列 ("noun quotation") -# is いわく ("iwaku"). -#名詞-引用文字列 -# -# noun-nai_adjective: Words that appear before the auxiliary verb ない ("nai") and -# behave like an adjective. -# e.g. 申し訳, 仕方, とんでも, 違い -#名詞-ナイ形容詞語幹 -# -##### -# prefix: unclassified prefixes -#接頭詞 -# -# prefix-nominal: Prefixes that attach to nouns (including adjective stem forms) -# excluding numerical expressions. -# e.g. お (水), 某 (氏), 同 (社), 故 (~氏), 高 (品質), お (見事), ご (立派) -#接頭詞-名詞接続 -# -# prefix-verbal: Prefixes that attach to the imperative form of a verb or a verb -# in conjunctive form followed by なる/なさる/くださる. -# e.g. お (読みなさい), お (座り) -#接頭詞-動詞接続 -# -# prefix-adjectival: Prefixes that attach to adjectives. -# e.g. お (寒いですねえ), バカ (でかい) -#接頭詞-形容詞接続 -# -# prefix-numerical: Prefixes that attach to numerical expressions. -# e.g. 約, およそ, 毎時 -#接頭詞-数接続 -# -##### -# verb: unclassified verbs -#動詞 -# -# verb-main: -#動詞-自立 -# -# verb-auxiliary: -#動詞-非自立 -# -# verb-suffix: -#動詞-接尾 -# -##### -# adjective: unclassified adjectives -#形容詞 -# -# adjective-main: -#形容詞-自立 -# -# adjective-auxiliary: -#形容詞-非自立 -# -# adjective-suffix: -#形容詞-接尾 -# -##### -# adverb: unclassified adverbs -#副詞 -# -# adverb-misc: Words that can be segmented into one unit and where adnominal -# modification is not possible. -# e.g. あいかわらず, 多分 -#副詞-一般 -# -# adverb-particle_conjunction: Adverbs that can be followed by の, は, に, -# な, する, だ, etc. -# e.g. こんなに, そんなに, あんなに, なにか, なんでも -#副詞-助詞類接続 -# -##### -# adnominal: Words that only have noun-modifying forms. -# e.g. この, その, あの, どの, いわゆる, なんらかの, 何らかの, いろんな, こういう, そういう, ああいう, -# どういう, こんな, そんな, あんな, どんな, 大きな, 小さな, おかしな, ほんの, たいした, -# 「(, も) さる (ことながら)」, 微々たる, 堂々たる, 単なる, いかなる, 我が」「同じ, 亡き -#連体詞 -# -##### -# conjunction: Conjunctions that can occur independently. -# e.g. が, けれども, そして, じゃあ, それどころか -接続詞 -# -##### -# particle: unclassified particles. -助詞 -# -# particle-case: case particles where the subclassification is undefined. -助詞-格助詞 -# -# particle-case-misc: Case particles. -# e.g. から, が, で, と, に, へ, より, を, の, にて -助詞-格助詞-一般 -# -# particle-case-quote: the "to" that appears after nouns, a person’s speech, -# quotation marks, expressions of decisions from a meeting, reasons, judgements, -# conjectures, etc. -# e.g. ( だ) と (述べた.), ( である) と (して執行猶予...) -助詞-格助詞-引用 -# -# particle-case-compound: Compounds of particles and verbs that mainly behave -# like case particles. -# e.g. という, といった, とかいう, として, とともに, と共に, でもって, にあたって, に当たって, に当って, -# にあたり, に当たり, に当り, に当たる, にあたる, において, に於いて,に於て, における, に於ける, -# にかけ, にかけて, にかんし, に関し, にかんして, に関して, にかんする, に関する, に際し, -# に際して, にしたがい, に従い, に従う, にしたがって, に従って, にたいし, に対し, にたいして, -# に対して, にたいする, に対する, について, につき, につけ, につけて, につれ, につれて, にとって, -# にとり, にまつわる, によって, に依って, に因って, により, に依り, に因り, による, に依る, に因る, -# にわたって, にわたる, をもって, を以って, を通じ, を通じて, を通して, をめぐって, をめぐり, をめぐる, -# って-口語/, ちゅう-関西弁「という」/, (何) ていう (人)-口語/, っていう-口語/, といふ, とかいふ -助詞-格助詞-連語 -# -# particle-conjunctive: -# e.g. から, からには, が, けれど, けれども, けど, し, つつ, て, で, と, ところが, どころか, とも, ども, -# ながら, なり, ので, のに, ば, ものの, や ( した), やいなや, (ころん) じゃ(いけない)-口語/, -# (行っ) ちゃ(いけない)-口語/, (言っ) たって (しかたがない)-口語/, (それがなく)ったって (平気)-口語/ -助詞-接続助詞 -# -# particle-dependency: -# e.g. こそ, さえ, しか, すら, は, も, ぞ -助詞-係助詞 -# -# particle-adverbial: -# e.g. がてら, かも, くらい, 位, ぐらい, しも, (学校) じゃ(これが流行っている)-口語/, -# (それ)じゃあ (よくない)-口語/, ずつ, (私) なぞ, など, (私) なり (に), (先生) なんか (大嫌い)-口語/, -# (私) なんぞ, (先生) なんて (大嫌い)-口語/, のみ, だけ, (私) だって-口語/, だに, -# (彼)ったら-口語/, (お茶) でも (いかが), 等 (とう), (今後) とも, ばかり, ばっか-口語/, ばっかり-口語/, -# ほど, 程, まで, 迄, (誰) も (が)([助詞-格助詞] および [助詞-係助詞] の前に位置する「も」) -助詞-副助詞 -# -# particle-interjective: particles with interjective grammatical roles. -# e.g. (松島) や -助詞-間投助詞 -# -# particle-coordinate: -# e.g. と, たり, だの, だり, とか, なり, や, やら -助詞-並立助詞 -# -# particle-final: -# e.g. かい, かしら, さ, ぜ, (だ)っけ-口語/, (とまってる) で-方言/, な, ナ, なあ-口語/, ぞ, ね, ネ, -# ねぇ-口語/, ねえ-口語/, ねん-方言/, の, のう-口語/, や, よ, ヨ, よぉ-口語/, わ, わい-口語/ -助詞-終助詞 -# -# particle-adverbial/conjunctive/final: The particle "ka" when unknown whether it is -# adverbial, conjunctive, or sentence final. For example: -# (a) 「A か B か」. Ex:「(国内で運用する) か,(海外で運用する) か (.)」 -# (b) Inside an adverb phrase. Ex:「(幸いという) か (, 死者はいなかった.)」 -# 「(祈りが届いたせい) か (, 試験に合格した.)」 -# (c) 「かのように」. Ex:「(何もなかった) か (のように振る舞った.)」 -# e.g. か -助詞-副助詞/並立助詞/終助詞 -# -# particle-adnominalizer: The "no" that attaches to nouns and modifies -# non-inflectional words. -助詞-連体化 -# -# particle-adnominalizer: The "ni" and "to" that appear following nouns and adverbs -# that are giongo, giseigo, or gitaigo. -# e.g. に, と -助詞-副詞化 -# -# particle-special: A particle that does not fit into one of the above classifications. -# This includes particles that are used in Tanka, Haiku, and other poetry. -# e.g. かな, けむ, ( しただろう) に, (あんた) にゃ(わからん), (俺) ん (家) -助詞-特殊 -# -##### -# auxiliary-verb: -助動詞 -# -##### -# interjection: Greetings and other exclamations. -# e.g. おはよう, おはようございます, こんにちは, こんばんは, ありがとう, どうもありがとう, ありがとうございます, -# いただきます, ごちそうさま, さよなら, さようなら, はい, いいえ, ごめん, ごめんなさい -#感動詞 -# -##### -# symbol: unclassified Symbols. -記号 -# -# symbol-misc: A general symbol not in one of the categories below. -# e.g. [○◎@$〒→+] -記号-一般 -# -# symbol-comma: Commas -# e.g. [,、] -記号-読点 -# -# symbol-period: Periods and full stops. -# e.g. [..。] -記号-句点 -# -# symbol-space: Full-width whitespace. -記号-空白 -# -# symbol-open_bracket: -# e.g. [({‘“『【] -記号-括弧開 -# -# symbol-close_bracket: -# e.g. [)}’”』」】] -記号-括弧閉 -# -# symbol-alphabetic: -#記号-アルファベット -# -##### -# other: unclassified other -#その他 -# -# other-interjection: Words that are hard to classify as noun-suffixes or -# sentence-final particles. -# e.g. (だ)ァ -その他-間投 -# -##### -# filler: Aizuchi that occurs during a conversation or sounds inserted as filler. -# e.g. あの, うんと, えと -フィラー -# -##### -# non-verbal: non-verbal sound. -非言語音 -# -##### -# fragment: -#語断片 -# -##### -# unknown: unknown part of speech. -#未知語 -# -##### End of file diff --git a/resources/solr/default_configset/lang/stopwords_ar.txt b/resources/solr/default_configset/lang/stopwords_ar.txt deleted file mode 100644 index 046829db6a..0000000000 --- a/resources/solr/default_configset/lang/stopwords_ar.txt +++ /dev/null @@ -1,125 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -# Cleaned on October 11, 2009 (not normalized, so use before normalization) -# This means that when modifying this list, you might need to add some -# redundant entries, for example containing forms with both أ and ا -من -ومن -منها -منه -في -وفي -فيها -فيه -و -ف -ثم -او -أو -ب -بها -به -ا -أ -اى -اي -أي -أى -لا -ولا -الا -ألا -إلا -لكن -ما -وما -كما -فما -عن -مع -اذا -إذا -ان -أن -إن -انها -أنها -إنها -انه -أنه -إنه -بان -بأن -فان -فأن -وان -وأن -وإن -التى -التي -الذى -الذي -الذين -الى -الي -إلى -إلي -على -عليها -عليه -اما -أما -إما -ايضا -أيضا -كل -وكل -لم -ولم -لن -ولن -هى -هي -هو -وهى -وهي -وهو -فهى -فهي -فهو -انت -أنت -لك -لها -له -هذه -هذا -تلك -ذلك -هناك -كانت -كان -يكون -تكون -وكانت -وكان -غير -بعض -قد -نحو -بين -بينما -منذ -ضمن -حيث -الان -الآن -خلال -بعد -قبل -حتى -عند -عندما -لدى -جميع diff --git a/resources/solr/default_configset/lang/stopwords_bg.txt b/resources/solr/default_configset/lang/stopwords_bg.txt deleted file mode 100644 index 1ae4ba2ae3..0000000000 --- a/resources/solr/default_configset/lang/stopwords_bg.txt +++ /dev/null @@ -1,193 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -а -аз -ако -ала -бе -без -беше -би -бил -била -били -било -близо -бъдат -бъде -бяха -в -вас -ваш -ваша -вероятно -вече -взема -ви -вие -винаги -все -всеки -всички -всичко -всяка -във -въпреки -върху -г -ги -главно -го -д -да -дали -до -докато -докога -дори -досега -доста -е -едва -един -ето -за -зад -заедно -заради -засега -затова -защо -защото -и -из -или -им -има -имат -иска -й -каза -как -каква -какво -както -какъв -като -кога -когато -което -които -кой -който -колко -която -къде -където -към -ли -м -ме -между -мен -ми -мнозина -мога -могат -може -моля -момента -му -н -на -над -назад -най -направи -напред -например -нас -не -него -нея -ни -ние -никой -нито -но -някои -някой -няма -обаче -около -освен -особено -от -отгоре -отново -още -пак -по -повече -повечето -под -поне -поради -после -почти -прави -пред -преди -през -при -пък -първо -с -са -само -се -сега -си -скоро -след -сме -според -сред -срещу -сте -съм -със -също -т -тази -така -такива -такъв -там -твой -те -тези -ти -тн -то -това -тогава -този -той -толкова -точно -трябва -тук -тъй -тя -тях -у -харесва -ч -че -често -чрез -ще -щом -я diff --git a/resources/solr/default_configset/lang/stopwords_ca.txt b/resources/solr/default_configset/lang/stopwords_ca.txt deleted file mode 100644 index 3da65deafe..0000000000 --- a/resources/solr/default_configset/lang/stopwords_ca.txt +++ /dev/null @@ -1,220 +0,0 @@ -# Catalan stopwords from http://github.com/vcl/cue.language (Apache 2 Licensed) -a -abans -ací -ah -així -això -al -als -aleshores -algun -alguna -algunes -alguns -alhora -allà -allí -allò -altra -altre -altres -amb -ambdós -ambdues -apa -aquell -aquella -aquelles -aquells -aquest -aquesta -aquestes -aquests -aquí -baix -cada -cadascú -cadascuna -cadascunes -cadascuns -com -contra -d'un -d'una -d'unes -d'uns -dalt -de -del -dels -des -després -dins -dintre -donat -doncs -durant -e -eh -el -els -em -en -encara -ens -entre -érem -eren -éreu -es -és -esta -està -estàvem -estaven -estàveu -esteu -et -etc -ets -fins -fora -gairebé -ha -han -has -havia -he -hem -heu -hi -ho -i -igual -iguals -ja -l'hi -la -les -li -li'n -llavors -m'he -ma -mal -malgrat -mateix -mateixa -mateixes -mateixos -me -mentre -més -meu -meus -meva -meves -molt -molta -moltes -molts -mon -mons -n'he -n'hi -ne -ni -no -nogensmenys -només -nosaltres -nostra -nostre -nostres -o -oh -oi -on -pas -pel -pels -per -però -perquè -poc -poca -pocs -poques -potser -propi -qual -quals -quan -quant -que -què -quelcom -qui -quin -quina -quines -quins -s'ha -s'han -sa -semblant -semblants -ses -seu -seus -seva -seva -seves -si -sobre -sobretot -sóc -solament -sols -son -són -sons -sota -sou -t'ha -t'han -t'he -ta -tal -també -tampoc -tan -tant -tanta -tantes -teu -teus -teva -teves -ton -tons -tot -tota -totes -tots -un -una -unes -uns -us -va -vaig -vam -van -vas -veu -vosaltres -vostra -vostre -vostres diff --git a/resources/solr/default_configset/lang/stopwords_cz.txt b/resources/solr/default_configset/lang/stopwords_cz.txt deleted file mode 100644 index 53c6097dac..0000000000 --- a/resources/solr/default_configset/lang/stopwords_cz.txt +++ /dev/null @@ -1,172 +0,0 @@ -a -s -k -o -i -u -v -z -dnes -cz -tímto -budeš -budem -byli -jseš -můj -svým -ta -tomto -tohle -tuto -tyto -jej -zda -proč -máte -tato -kam -tohoto -kdo -kteří -mi -nám -tom -tomuto -mít -nic -proto -kterou -byla -toho -protože -asi -ho -naši -napište -re -což -tím -takže -svých -její -svými -jste -aj -tu -tedy -teto -bylo -kde -ke -pravé -ji -nad -nejsou -či -pod -téma -mezi -přes -ty -pak -vám -ani -když -však -neg -jsem -tento -článku -články -aby -jsme -před -pta -jejich -byl -ještě -až -bez -také -pouze -první -vaše -která -nás -nový -tipy -pokud -může -strana -jeho -své -jiné -zprávy -nové -není -vás -jen -podle -zde -už -být -více -bude -již -než -který -by -které -co -nebo -ten -tak -má -při -od -po -jsou -jak -další -ale -si -se -ve -to -jako -za -zpět -ze -do -pro -je -na -atd -atp -jakmile -přičemž -já -on -ona -ono -oni -ony -my -vy -jí -ji -mě -mne -jemu -tomu -těm -těmu -němu -němuž -jehož -jíž -jelikož -jež -jakož -načež diff --git a/resources/solr/default_configset/lang/stopwords_da.txt b/resources/solr/default_configset/lang/stopwords_da.txt deleted file mode 100644 index 6e90e8f1aa..0000000000 --- a/resources/solr/default_configset/lang/stopwords_da.txt +++ /dev/null @@ -1,110 +0,0 @@ - | From https://snowballstem.org/algorithms/danish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Danish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - -og | and -i | in -jeg | I -det | that (dem. pronoun)/it (pers. pronoun) -at | that (in front of a sentence)/to (with infinitive) -en | a/an -den | it (pers. pronoun)/that (dem. pronoun) -til | to/at/for/until/against/by/of/into, more -er | present tense of "to be" -som | who, as -på | on/upon/in/on/at/to/after/of/with/for, on -de | they -med | with/by/in, along -han | he -af | of/by/from/off/for/in/with/on, off -for | at/for/to/from/by/of/ago, in front/before, because -ikke | not -der | who/which, there/those -var | past tense of "to be" -mig | me/myself -sig | oneself/himself/herself/itself/themselves -men | but -et | a/an/one, one (number), someone/somebody/one -har | present tense of "to have" -om | round/about/for/in/a, about/around/down, if -vi | we -min | my -havde | past tense of "to have" -ham | him -hun | she -nu | now -over | over/above/across/by/beyond/past/on/about, over/past -da | then, when/as/since -fra | from/off/since, off, since -du | you -ud | out -sin | his/her/its/one's -dem | them -os | us/ourselves -op | up -man | you/one -hans | his -hvor | where -eller | or -hvad | what -skal | must/shall etc. -selv | myself/yourself/herself/ourselves etc., even -her | here -alle | all/everyone/everybody etc. -vil | will (verb) -blev | past tense of "to stay/to remain/to get/to become" -kunne | could -ind | in -når | when -være | present tense of "to be" -dog | however/yet/after all -noget | something -ville | would -jo | you know/you see (adv), yes -deres | their/theirs -efter | after/behind/according to/for/by/from, later/afterwards -ned | down -skulle | should -denne | this -end | than -dette | this -mit | my/mine -også | also -under | under/beneath/below/during, below/underneath -have | have -dig | you -anden | other -hende | her -mine | my -alt | everything -meget | much/very, plenty of -sit | his, her, its, one's -sine | his, her, its, one's -vor | our -mod | against -disse | these -hvis | if -din | your/yours -nogle | some -hos | by/at -blive | be/become -mange | many -ad | by/through -bliver | present tense of "to be/to become" -hendes | her/hers -været | be -thi | for (conj) -jer | you -sådan | such, like this/like that diff --git a/resources/solr/default_configset/lang/stopwords_de.txt b/resources/solr/default_configset/lang/stopwords_de.txt deleted file mode 100644 index 804bbbdb01..0000000000 --- a/resources/solr/default_configset/lang/stopwords_de.txt +++ /dev/null @@ -1,294 +0,0 @@ - | From https://snowballstem.org/algorithms/german/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A German stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | The number of forms in this list is reduced significantly by passing it - | through the German stemmer. - - -aber | but - -alle | all -allem -allen -aller -alles - -als | than, as -also | so -am | an + dem -an | at - -ander | other -andere -anderem -anderen -anderer -anderes -anderm -andern -anderr -anders - -auch | also -auf | on -aus | out of -bei | by -bin | am -bis | until -bist | art -da | there -damit | with it -dann | then - -der | the -den -des -dem -die -das - -daß | that - -derselbe | the same -derselben -denselben -desselben -demselben -dieselbe -dieselben -dasselbe - -dazu | to that - -dein | thy -deine -deinem -deinen -deiner -deines - -denn | because - -derer | of those -dessen | of him - -dich | thee -dir | to thee -du | thou - -dies | this -diese -diesem -diesen -dieser -dieses - - -doch | (several meanings) -dort | (over) there - - -durch | through - -ein | a -eine -einem -einen -einer -eines - -einig | some -einige -einigem -einigen -einiger -einiges - -einmal | once - -er | he -ihn | him -ihm | to him - -es | it -etwas | something - -euer | your -eure -eurem -euren -eurer -eures - -für | for -gegen | towards -gewesen | p.p. of sein -hab | have -habe | have -haben | have -hat | has -hatte | had -hatten | had -hier | here -hin | there -hinter | behind - -ich | I -mich | me -mir | to me - - -ihr | you, to her -ihre -ihrem -ihren -ihrer -ihres -euch | to you - -im | in + dem -in | in -indem | while -ins | in + das -ist | is - -jede | each, every -jedem -jeden -jeder -jedes - -jene | that -jenem -jenen -jener -jenes - -jetzt | now -kann | can - -kein | no -keine -keinem -keinen -keiner -keines - -können | can -könnte | could -machen | do -man | one - -manche | some, many a -manchem -manchen -mancher -manches - -mein | my -meine -meinem -meinen -meiner -meines - -mit | with -muss | must -musste | had to -nach | to(wards) -nicht | not -nichts | nothing -noch | still, yet -nun | now -nur | only -ob | whether -oder | or -ohne | without -sehr | very - -sein | his -seine -seinem -seinen -seiner -seines - -selbst | self -sich | herself - -sie | they, she -ihnen | to them - -sind | are -so | so - -solche | such -solchem -solchen -solcher -solches - -soll | shall -sollte | should -sondern | but -sonst | else -über | over -um | about, around -und | and - -uns | us -unse -unsem -unsen -unser -unses - -unter | under -viel | much -vom | von + dem -von | from -vor | before -während | while -war | was -waren | were -warst | wast -was | what -weg | away, off -weil | because -weiter | further - -welche | which -welchem -welchen -welcher -welches - -wenn | when -werde | will -werden | will -wie | how -wieder | again -will | want -wir | we -wird | will -wirst | willst -wo | where -wollen | want -wollte | wanted -würde | would -würden | would -zu | to -zum | zu + dem -zur | zu + der -zwar | indeed -zwischen | between - diff --git a/resources/solr/default_configset/lang/stopwords_el.txt b/resources/solr/default_configset/lang/stopwords_el.txt deleted file mode 100644 index 232681f5bd..0000000000 --- a/resources/solr/default_configset/lang/stopwords_el.txt +++ /dev/null @@ -1,78 +0,0 @@ -# Lucene Greek Stopwords list -# Note: by default this file is used after GreekLowerCaseFilter, -# so when modifying this file use 'σ' instead of 'ς' -ο -η -το -οι -τα -του -τησ -των -τον -την -και -κι -κ -ειμαι -εισαι -ειναι -ειμαστε -ειστε -στο -στον -στη -στην -μα -αλλα -απο -για -προσ -με -σε -ωσ -παρα -αντι -κατα -μετα -θα -να -δε -δεν -μη -μην -επι -ενω -εαν -αν -τοτε -που -πωσ -ποιοσ -ποια -ποιο -ποιοι -ποιεσ -ποιων -ποιουσ -αυτοσ -αυτη -αυτο -αυτοι -αυτων -αυτουσ -αυτεσ -αυτα -εκεινοσ -εκεινη -εκεινο -εκεινοι -εκεινεσ -εκεινα -εκεινων -εκεινουσ -οπωσ -ομωσ -ισωσ -οσο -οτι diff --git a/resources/solr/default_configset/lang/stopwords_en.txt b/resources/solr/default_configset/lang/stopwords_en.txt deleted file mode 100644 index 2c164c0b2a..0000000000 --- a/resources/solr/default_configset/lang/stopwords_en.txt +++ /dev/null @@ -1,54 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# a couple of test stopwords to test that the words are really being -# configured from this file: -stopworda -stopwordb - -# Standard english stop words taken from Lucene's StopAnalyzer -a -an -and -are -as -at -be -but -by -for -if -in -into -is -it -no -not -of -on -or -such -that -the -their -then -there -these -they -this -to -was -will -with diff --git a/resources/solr/default_configset/lang/stopwords_es.txt b/resources/solr/default_configset/lang/stopwords_es.txt deleted file mode 100644 index 48bd65ef86..0000000000 --- a/resources/solr/default_configset/lang/stopwords_es.txt +++ /dev/null @@ -1,356 +0,0 @@ - | From https://snowballstem.org/algorithms/spanish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Spanish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - - | The following is a ranked list (commonest to rarest) of stopwords - | deriving from a large sample of text. - - | Extra words have been added at the end. - -de | from, of -la | the, her -que | who, that -el | the -en | in -y | and -a | to -los | the, them -del | de + el -se | himself, from him etc -las | the, them -por | for, by, etc -un | a -para | for -con | with -no | no -una | a -su | his, her -al | a + el - | es from SER -lo | him -como | how -más | more -pero | pero -sus | su plural -le | to him, her -ya | already -o | or - | fue from SER -este | this - | ha from HABER -sí | himself etc -porque | because -esta | this - | son from SER -entre | between - | está from ESTAR -cuando | when -muy | very -sin | without -sobre | on - | ser from SER - | tiene from TENER -también | also -me | me -hasta | until -hay | there is/are -donde | where - | han from HABER -quien | whom, that - | están from ESTAR - | estado from ESTAR -desde | from -todo | all -nos | us -durante | during - | estados from ESTAR -todos | all -uno | a -les | to them -ni | nor -contra | against -otros | other - | fueron from SER -ese | that -eso | that - | había from HABER -ante | before -ellos | they -e | and (variant of y) -esto | this -mí | me -antes | before -algunos | some -qué | what? -unos | a -yo | I -otro | other -otras | other -otra | other -él | he -tanto | so much, many -esa | that -estos | these -mucho | much, many -quienes | who -nada | nothing -muchos | many -cual | who - | sea from SER -poco | few -ella | she -estar | to be - | haber from HABER -estas | these - | estaba from ESTAR - | estamos from ESTAR -algunas | some -algo | something -nosotros | we - - | other forms - -mi | me -mis | mi plural -tú | thou -te | thee -ti | thee -tu | thy -tus | tu plural -ellas | they -nosotras | we -vosotros | you -vosotras | you -os | you -mío | mine -mía | -míos | -mías | -tuyo | thine -tuya | -tuyos | -tuyas | -suyo | his, hers, theirs -suya | -suyos | -suyas | -nuestro | ours -nuestra | -nuestros | -nuestras | -vuestro | yours -vuestra | -vuestros | -vuestras | -esos | those -esas | those - - | forms of estar, to be (not including the infinitive): -estoy -estás -está -estamos -estáis -están -esté -estés -estemos -estéis -estén -estaré -estarás -estará -estaremos -estaréis -estarán -estaría -estarías -estaríamos -estaríais -estarían -estaba -estabas -estábamos -estabais -estaban -estuve -estuviste -estuvo -estuvimos -estuvisteis -estuvieron -estuviera -estuvieras -estuviéramos -estuvierais -estuvieran -estuviese -estuvieses -estuviésemos -estuvieseis -estuviesen -estando -estado -estada -estados -estadas -estad - - | forms of haber, to have (not including the infinitive): -he -has -ha -hemos -habéis -han -haya -hayas -hayamos -hayáis -hayan -habré -habrás -habrá -habremos -habréis -habrán -habría -habrías -habríamos -habríais -habrían -había -habías -habíamos -habíais -habían -hube -hubiste -hubo -hubimos -hubisteis -hubieron -hubiera -hubieras -hubiéramos -hubierais -hubieran -hubiese -hubieses -hubiésemos -hubieseis -hubiesen -habiendo -habido -habida -habidos -habidas - - | forms of ser, to be (not including the infinitive): -soy -eres -es -somos -sois -son -sea -seas -seamos -seáis -sean -seré -serás -será -seremos -seréis -serán -sería -serías -seríamos -seríais -serían -era -eras -éramos -erais -eran -fui -fuiste -fue -fuimos -fuisteis -fueron -fuera -fueras -fuéramos -fuerais -fueran -fuese -fueses -fuésemos -fueseis -fuesen -siendo -sido - | sed also means 'thirst' - - | forms of tener, to have (not including the infinitive): -tengo -tienes -tiene -tenemos -tenéis -tienen -tenga -tengas -tengamos -tengáis -tengan -tendré -tendrás -tendrá -tendremos -tendréis -tendrán -tendría -tendrías -tendríamos -tendríais -tendrían -tenía -tenías -teníamos -teníais -tenían -tuve -tuviste -tuvo -tuvimos -tuvisteis -tuvieron -tuviera -tuvieras -tuviéramos -tuvierais -tuvieran -tuviese -tuvieses -tuviésemos -tuvieseis -tuviesen -teniendo -tenido -tenida -tenidos -tenidas -tened - diff --git a/resources/solr/default_configset/lang/stopwords_et.txt b/resources/solr/default_configset/lang/stopwords_et.txt deleted file mode 100644 index 1b06a134b9..0000000000 --- a/resources/solr/default_configset/lang/stopwords_et.txt +++ /dev/null @@ -1,1603 +0,0 @@ -# Estonian stopwords list -all -alla -allapoole -allpool -alt -altpoolt -eel -eespool -enne -hommikupoole -hoolimata -ilma -kaudu -keset -kesk -kohe -koos -kuhupoole -kuni -kuspool -kustpoolt -kõige -käsikäes -lappi -ligi -läbi -mööda -paitsi -peale -pealepoole -pealpool -pealt -pealtpoolt -piki -pikku -piku -pikuti -põiki -pärast -päri -risti -sealpool -sealtpoolt -seespool -seltsis -siiapoole -siinpool -siitpoolt -sinnapoole -sissepoole -taga -tagantpoolt -tagapidi -tagapool -taha -tahapoole -teispool -teispoole -tänu -tükkis -vaatamata -vastu -väljapoole -väljaspool -väljastpoolt -õhtupoole -ühes -ühestükis -ühestükkis -ülalpool -ülaltpoolt -üle -ülespoole -ülevalpool -ülevaltpoolt -ümber -ümbert -aegu -aegus -alguks -algul -algule -algult -alguni -all -alla -alt -alul -alutsi -arvel -asemel -asemele -eel -eeli -ees -eesotsas -eest -eestotsast -esitsi -ette -etteotsa -haaval -heaks -hoolimata -hulgas -hulgast -hulka -jalgu -jalus -jalust -jaoks -jooksul -juurde -juures -juurest -jälil -jälile -järel -järele -järelt -järgi -kaasas -kallal -kallale -kallalt -kamul -kannul -kannule -kannult -kaudu -kaupa -keskel -keskele -keskelt -keskis -keskpaiku -kestel -kestes -kilda -killas -killast -kimpu -kimpus -kiuste -kohal -kohale -kohalt -kohaselt -kohe -kohta -koos -korral -kukil -kukile -kukilt -kulul -kõrva -kõrval -kõrvale -kõrvalt -kõrvas -kõrvast -käekõrval -käekõrvale -käekõrvalt -käes -käest -kätte -külge -küljes -küljest -küüsi -küüsis -küüsist -ligi -ligidal -ligidale -ligidalt -aegu -aegus -alguks -algul -algule -algult -alguni -all -alla -alt -alul -alutsi -arvel -asemel -asemele -eel -eeli -ees -eesotsas -eest -eestotsast -esitsi -ette -etteotsa -haaval -heaks -hoolimata -hulgas -hulgast -hulka -jalgu -jalus -jalust -jaoks -jooksul -juurde -juures -juurest -jälil -jälile -järel -järele -järelt -järgi -kaasas -kallal -kallale -kallalt -kamul -kannul -kannule -kannult -kaudu -kaupa -keskel -keskele -keskelt -keskis -keskpaiku -kestel -kestes -kilda -killas -killast -kimpu -kimpus -kiuste -kohal -kohale -kohalt -kohaselt -kohe -kohta -koos -korral -kukil -kukile -kukilt -kulul -kõrva -kõrval -kõrvale -kõrvalt -kõrvas -kõrvast -käekõrval -käekõrvale -käekõrvalt -käes -käest -kätte -külge -küljes -küljest -küüsi -küüsis -küüsist -ligi -ligidal -ligidale -ligidalt -lool -läbi -lähedal -lähedale -lähedalt -man -mant -manu -meelest -mööda -nahas -nahka -nahkas -najal -najale -najalt -nõjal -nõjale -otsa -otsas -otsast -paigale -paigu -paiku -peal -peale -pealt -perra -perrä -pidi -pihta -piki -pikku -pool -poole -poolest -poolt -puhul -puksiiris -pähe -päralt -päras -pärast -päri -ringi -ringis -risust -saadetusel -saadik -saatel -saati -seas -seast -sees -seest -sekka -seljataga -seltsi -seltsis -seltsist -sisse -slepis -suhtes -šlepis -taga -tagant -tagantotsast -tagaotsas -tagaselja -tagasi -tagast -tagutsi -taha -tahaotsa -takka -tarvis -tasa -tuuri -tuuris -tõttu -tükkis -uhal -vaatamata -vahel -vahele -vahelt -vahepeal -vahepeale -vahepealt -vahetsi -varal -varale -varul -vastas -vastast -vastu -veerde -veeres -viisi -võidu -võrd -võrdki -võrra -võrragi -väel -väele -vältel -väärt -väärtki -äärde -ääre -ääres -äärest -ühes -üle -ümber -ümbert -a -abil -aina -ainult -alalt -alates -alati -alles -b -c -d -e -eales -ealeski -edasi -edaspidi -eelkõige -eemal -ei -eks -end -enda -enese -ennem -esialgu -f -g -h -hoopis -i -iganes -igatahes -igati -iial -iialgi -ikka -ikkagi -ilmaski -iseenda -iseenese -iseenesest -isegi -j -jah -ju -juba -juhul -just -järelikult -k -ka -kah -kas -kasvõi -keda -kestahes -kogu -koguni -kohati -kokku -kuhu -kuhugi -kuidagi -kuidas -kunagi -kus -kusagil -kusjuures -kuskil -kust -kõigepealt -küll -l -liiga -lisaks -m -miks -mil -millal -millalgi -mispärast -mistahes -mistõttu -mitte -muide -muidu -muidugi -muist -mujal -mujale -mujalt -mõlemad -mõnda -mõne -mõnikord -n -nii -niikaua -niimoodi -niipaljuke -niisama -niisiis -niivõrd -nõnda -nüüd -o -omaette -omakorda -omavahel -ometi -p -palju -paljuke -palju-palju -peaaegu -peagi -peamiselt -pigem -pisut -praegu -päris -r -rohkem -s -samas -samuti -seal -sealt -sedakorda -sedapuhku -seega -seejuures -seejärel -seekord -seepärast -seetõttu -sellepärast -seni -sestap -siia -siiani -siin -siinkohal -siis -siiski -siit -sinna -suht -š -z -ž -t -teel -teineteise -tõesti -täiesti -u -umbes -v -w -veel -veelgi -vist -võibolla -võib-olla -väga -vähemalt -välja -väljas -väljast -õ -ä -ära -ö -ü -ühtlasi -üksi -ükskõik -ülal -ülale -ülalt -üles -ülesse -üleval -ülevalt -ülimalt -üsna -x -y -aga -ega -ehk -ehkki -elik -ellik -enge -ennegu -ent -et -ja -justkui -kui -kuid -kuigi -kuivõrd -kuna -kuni -kut -mistab -muudkui -nagu -nigu -ning -olgugi -otsekui -otsenagu -selmet -sest -sestab -vaid -või -aa -adaa -adjöö -ae -ah -ahaa -ahah -ah-ah-ah -ah-haa -ahoi -ai -aidaa -aidu-raidu -aih -aijeh -aituma -aitäh -aitüma -ammuu -amps -ampsti -aptsih -ass -at -ata -at-at-at -atsih -atsihh -auh -bai-bai -bingo -braavo -brr -ee -eeh -eh -ehee -eheh -eh-eh-hee -eh-eh-ee -ehei -ehh -ehhee -einoh -ena -ennäe -ennäh -fuh -fui -fuih -haa -hah -hahaa -hah-hah-hah -halleluuja -hallo -halloo -hass -hee -heh -he-he-hee -hei -heldeke(ne) -heureka -hihii -hip-hip-hurraa -hmh -hmjah -hoh-hoh-hoo -hohoo -hoi -hollallaa -hoo -hoplaa -hopp -hops -hopsassaa -hopsti -hosianna -huh -huidii -huist -hurjah -hurjeh -hurjoh -hurjuh -hurraa -huu -hõhõh -hõi -hõissa -hõissassa -hõk -hõkk -häh -hä-hä-hää -hüvasti -ih-ah-haa -ih-ih-hii -ii-ha-ha -issake -issakene -isver -jaa-ah -ja-ah -jaah -janäe -jeeh -jeerum -jeever -jessas -jestas -juhhei -jumalaga -jumalime -jumaluke -jumalukene -jutas -kaaps -kaapsti -kaasike -kae -kalps -kalpsti -kannäe -kanäe -kappadi -kaps -kapsti -karkõmm -karkäuh -karkääks -karkääksti -karmauh -karmauhti -karnaps -karnapsti -karniuhti -karpartsaki -karpauh -karpauhti -karplauh -karplauhti -karprauh -karprauhti -karsumdi -karsumm -kartsumdi -kartsumm -karviuh -karviuhti -kaske -kassa -kauh -kauhti -keh -keksti -kepsti -khe -khm -kih -kiiks -kiiksti -kiis -kiiss -kikerii -kikerikii -kili -kilk -kilk-kõlk -kilks -kilks-kolks -kilks-kõlks -kill -killadi -killadi|-kolladi -killadi-kõlladi -killa-kolla -killa-kõlla -kill-kõll -kimps-komps -kipp -kips-kõps -kiriküüt -kirra-kõrra -kirr-kõrr -kirts -klaps -klapsti -klirdi -klirr -klonks -klops -klopsti -kluk -klu-kluu -klõks -klõksti -klõmdi -klõmm -klõmpsti -klõnks -klõnksti -klõps -klõpsti -kläu -kohva-kohva -kok -koks -koksti -kolaki -kolk -kolks -kolksti -koll -kolladi -komp -komps -kompsti -kop -kopp -koppadi -kops -kopsti -kossu -kotsu -kraa -kraak -kraaks -kraaps -kraapsti -krahh -kraks -kraksti -kraps -krapsti -krauh -krauhti -kriiks -kriiksti -kriips -kriips-kraaps -kripa-krõpa -krips-kraps -kriuh -kriuks -kriuksti -kromps -kronk -kronks -krooks -kruu -krõks -krõksti -krõpa -krõps -krõpsti -krõuh -kräu -kräuh -kräuhti -kräuks -kss -kukeleegu -kukku -kuku -kulu -kurluu -kurnäu -kuss -kussu -kõks -kõksti -kõldi -kõlks -kõlksti -kõll -kõmaki -kõmdi -kõmm -kõmps -kõpp -kõps -kõpsadi -kõpsat -kõpsti -kõrr -kõrra-kõrra -kõss -kõtt -kõõksti -kärr -kärts -kärtsti -käuks -käuksti -kääga -kääks -kääksti -köh -köki-möki -köksti -laks -laksti -lampsti -larts -lartsti -lats -latsti -leelo -legoo -lehva -liiri-lõõri -lika-lõka -likat-lõkat -limpsti -lips -lipsti -lirts -lirtsaki -lirtsti -lonksti -lops -lopsti -lorts -lortsti -luks -lups -lupsti -lurts -lurtsti -lõks -lõksti -lõmps -lõmpsti -lõnks -lõnksti -lärts -lärtsti -läts -lätsti -lörts -lörtsti -lötsti -lööps -lööpsti -marss -mats -matsti -mauh -mauhti -mh -mhh -mhmh -miau -mjaa -mkm -m-mh -mnjaa -mnjah -moens -mulks -mulksti -mull-mull -mull-mull-mull -muu -muuh -mõh -mõmm -mäh -mäts -mäu -mää -möh -möh-öh-ää -möö -müh-müh -mühüh -müks -müksti -müraki -mürr -mürts -mürtsaki -mürtsti -mütaku -müta-mäta -müta-müta -müt-müt -müt-müt-müt -müts -mütsti -mütt -naa -naah -nah -naks -naksti -nanuu -naps -napsti -nilpsti -nipsti -nirr -niuh -niuh-näuh -niuhti -noh -noksti -nolpsti -nonoh -nonoo -nonäh -noo -nooh -nooks -norr -nurr -nuuts -nõh -nõhh -nõka-nõka -nõks -nõksat-nõksat -nõks-nõks -nõksti -nõõ -nõõh -näeh -näh -nälpsti -nämm-nämm -näpsti -näts -nätsti -näu -näuh -näuhti -näuks -näuksti -nääh -nääks -nühkat-nühkat -oeh -oh -ohh -ohhh -oh-hoi -oh-hoo -ohoh -oh-oh-oo -oh-oh-hoo -ohoi -ohoo -oi -oih -oijee -oijeh -oo -ooh -oo-oh -oo-ohh -oot -ossa -ot -paa -pah -pahh -pakaa -pamm -pantsti -pardon -pardonks -parlartsti -parts -partsti -partsumdi -partsumm -pastoi -pats -patst -patsti -pau -pauh -pauhti -pele -pfui -phuh -phuuh -phäh -phähh -piiks -piip -piiri-pääri -pimm -pimm-pamm -pimm-pomm -pimm-põmm -piraki -piuks -piu-pau -plaks -plaksti -plarts -plartsti -plats -platsti -plauh -plauhh -plauhti -pliks -pliks-plaks -plinn -pliraki -plirts -plirtsti -pliu -pliuh -ploks -plotsti -plumps -plumpsti -plõks -plõksti -plõmdi -plõmm -plõnn -plärr -plärts -plärtsat -plärtsti -pläu -pläuh -plää -plörtsat -pomm -popp -pops -popsti -ports -pot -pots -potsti -pott -praks -praksti -prants -prantsaki -prantsti -prassai -prauh -prauhh -prauhti -priks -priuh -priuhh -priuh-prauh -proosit -proost -prr -prrr -prõks -prõksti -prõmdi -prõmm -prõntsti -prääk -prääks -pst -psst -ptrr -ptruu -ptüi -puh -puhh -puksti -pumm -pumps -pup-pup-pup -purts -puuh -põks -põksti -põmdi -põmm -põmmadi -põnks -põnn -põnnadi -põnt -põnts -põntsti -põraki -põrr -põrra-põrra -päh -pähh -päntsti -pää -pöörd -püh -raks -raksti -raps -rapsti -ratataa -rauh -riips -riipsti -riks -riks-raks -rips-raps -rivitult -robaki -rops -ropsaki -ropsti -ruik -räntsti -räts -röh -röhh -sah -sahh -sahkat -saps -sapsti -sauh -sauhti -servus -sihkadi-sahkadi -sihka-sahka -sihkat-sahkat -silks -silk-solk -sips -sipsti -sirr -sirr-sorr -sirts -sirtsti -siu -siuh -siuh-sauh -siuh-säuh -siuhti -siuks -siuts -skool -so -soh -solks -solksti -solpsti -soo -sooh -so-oh -soo-oh -sopp -sops -sopsti -sorr -sorts -sortsti -so-soo -soss -soss-soss -ss -sss -sst -stopp -suhkat-sahkat -sulk -sulks -sulksti -sull -sulla-sulla -sulpa-sulpa -sulps -sulpsti -sumaki -sumdi -summ -summat-summat -sups -supsaku -supsti -surts -surtsti -suss -susti -suts -sutsti -säh -sähke -särts -särtsti -säu -säuh -säuhti -taevake -taevakene -takk -tere -terekest -tibi-tibi -tikk-takk -tiks -tilk -tilks -till -tilla-talla -till-tall -tilulii -tinn -tip -tip-tap -tirr -tirtsti -tiu -tjaa -tjah -tohhoh -tohhoo -tohoh -tohoo -tok -tokk -toks -toksti -tonks -tonksti -tota -totsti -tot-tot -tprr -tpruu -trah -trahh -trallallaa -trill -trillallaa -trr -trrr -tsah -tsahh -tsilk -tsilk-tsolk -tsirr -tsiuh -tskae -tsolk -tss -tst -tsst -tsuhh -tsuk -tsumm -tsurr -tsäuh -tšao -tšš -tššš -tuk -tuks -turts -turtsti -tutki -tutkit -tutu-lutu -tutulutu -tuut -tuutu-luutu -tõks -tötsti -tümps -uh -uhh -uh-huu -uhtsa -uhtsaa -uhuh -uhuu -ui -uih -uih-aih -uijah -uijeh -uist -uit -uka -upsti -uraa -urjah -urjeh -urjoh -urjuh -urr -urraa -ust -utu -uu -uuh -vaak -vaat -vae -vaeh -vai -vat -vau -vhüüt -vidiit -viiks -vilks -vilksti -vinki-vinki -virdi -virr -viu -viudi -viuh -viuhti -voeh -voh -vohh -volks -volksti -vooh -vops -vopsti -vot -vuh -vuhti -vuih -vulks -vulksti -vull -vulpsti -vups -vupsaki -vupsaku -vupsti -vurdi -vurr -vurra-vurra -vurts -vurtsti -vutt -võe -võeh -või -võih -võrr -võts -võtt -vääks -õe -õits -õk -õkk -õrr -õss -õuh -äh -ähh -ähhähhää -äh-hää -äh-äh-hää -äiu -äiu-ää -äss -ää -ääh -äähh -öh -öhh -ök -üh -eelmine -eikeegi -eimiski -emb-kumb -enam -enim -iga -igasugune -igaüks -ise -isesugune -järgmine -keegi -kes -kumb -kumbki -kõik -meiesugune -meietaoline -midagi -mihuke -mihukene -milletaoline -milline -mina -minake -mingi -mingisugune -minusugune -minutaoline -mis -miski -miskisugune -missugune -misuke -mitmes -mitmesugune -mitu -mitu-mitu -mitu-setu -muu -mõlema -mõnesugune -mõni -mõningane -mõningas -mäherdune -määrane -naasugune -need -nemad -nendesugune -nendetaoline -nihuke -nihukene -niimitu -niisamasugune -niisugune -nisuke -nisukene -oma -omaenese -omasugune -omataoline -pool -praegune -sama -samasugune -samataoline -see -seesama -seesamane -seesamune -seesinane -seesugune -selline -sihuke -sihukene -sina -sinusugune -sinutaoline -siuke -siukene -säherdune -säärane -taoline -teiesugune -teine -teistsugune -tema -temake -temakene -temasugune -temataoline -too -toosama -toosamane -üks -üksteise -hakkama -minema -olema -pidama -saama -tegema -tulema -võima diff --git a/resources/solr/default_configset/lang/stopwords_eu.txt b/resources/solr/default_configset/lang/stopwords_eu.txt deleted file mode 100644 index 25f1db9346..0000000000 --- a/resources/solr/default_configset/lang/stopwords_eu.txt +++ /dev/null @@ -1,99 +0,0 @@ -# example set of basque stopwords -al -anitz -arabera -asko -baina -bat -batean -batek -bati -batzuei -batzuek -batzuetan -batzuk -bera -beraiek -berau -berauek -bere -berori -beroriek -beste -bezala -da -dago -dira -ditu -du -dute -edo -egin -ere -eta -eurak -ez -gainera -gu -gutxi -guzti -haiei -haiek -haietan -hainbeste -hala -han -handik -hango -hara -hari -hark -hartan -hau -hauei -hauek -hauetan -hemen -hemendik -hemengo -hi -hona -honek -honela -honetan -honi -hor -hori -horiei -horiek -horietan -horko -horra -horrek -horrela -horretan -horri -hortik -hura -izan -ni -noiz -nola -non -nondik -nongo -nor -nora -ze -zein -zen -zenbait -zenbat -zer -zergatik -ziren -zituen -zu -zuek -zuen -zuten diff --git a/resources/solr/default_configset/lang/stopwords_fa.txt b/resources/solr/default_configset/lang/stopwords_fa.txt deleted file mode 100644 index 723641c6da..0000000000 --- a/resources/solr/default_configset/lang/stopwords_fa.txt +++ /dev/null @@ -1,313 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -# Note: by default this file is used after normalization, so when adding entries -# to this file, use the arabic 'ي' instead of 'ی' -انان -نداشته -سراسر -خياه -ايشان -وي -تاكنون -بيشتري -دوم -پس -ناشي -وگو -يا -داشتند -سپس -هنگام -هرگز -پنج -نشان -امسال -ديگر -گروهي -شدند -چطور -ده -و -دو -نخستين -ولي -چرا -چه -وسط -ه -كدام -قابل -يك -رفت -هفت -همچنين -در -هزار -بله -بلي -شايد -اما -شناسي -گرفته -دهد -داشته -دانست -داشتن -خواهيم -ميليارد -وقتيكه -امد -خواهد -جز -اورده -شده -بلكه -خدمات -شدن -برخي -نبود -بسياري -جلوگيري -حق -كردند -نوعي -بعري -نكرده -نظير -نبايد -بوده -بودن -داد -اورد -هست -جايي -شود -دنبال -داده -بايد -سابق -هيچ -همان -انجا -كمتر -كجاست -گردد -كسي -تر -مردم -تان -دادن -بودند -سري -جدا -ندارند -مگر -يكديگر -دارد -دهند -بنابراين -هنگامي -سمت -جا -انچه -خود -دادند -زياد -دارند -اثر -بدون -بهترين -بيشتر -البته -به -براساس -بيرون -كرد -بعضي -گرفت -توي -اي -ميليون -او -جريان -تول -بر -مانند -برابر -باشيم -مدتي -گويند -اكنون -تا -تنها -جديد -چند -بي -نشده -كردن -كردم -گويد -كرده -كنيم -نمي -نزد -روي -قصد -فقط -بالاي -ديگران -اين -ديروز -توسط -سوم -ايم -دانند -سوي -استفاده -شما -كنار -داريم -ساخته -طور -امده -رفته -نخست -بيست -نزديك -طي -كنيد -از -انها -تمامي -داشت -يكي -طريق -اش -چيست -روب -نمايد -گفت -چندين -چيزي -تواند -ام -ايا -با -ان -ايد -ترين -اينكه -ديگري -راه -هايي -بروز -همچنان -پاعين -كس -حدود -مختلف -مقابل -چيز -گيرد -ندارد -ضد -همچون -سازي -شان -مورد -باره -مرسي -خويش -برخوردار -چون -خارج -شش -هنوز -تحت -ضمن -هستيم -گفته -فكر -بسيار -پيش -براي -روزهاي -انكه -نخواهد -بالا -كل -وقتي -كي -چنين -كه -گيري -نيست -است -كجا -كند -نيز -يابد -بندي -حتي -توانند -عقب -خواست -كنند -بين -تمام -همه -ما -باشند -مثل -شد -اري -باشد -اره -طبق -بعد -اگر -صورت -غير -جاي -بيش -ريزي -اند -زيرا -چگونه -بار -لطفا -مي -درباره -من -ديده -همين -گذاري -برداري -علت -گذاشته -هم -فوق -نه -ها -شوند -اباد -همواره -هر -اول -خواهند -چهار -نام -امروز -مان -هاي -قبل -كنم -سعي -تازه -را -هستند -زير -جلوي -عنوان -بود diff --git a/resources/solr/default_configset/lang/stopwords_fi.txt b/resources/solr/default_configset/lang/stopwords_fi.txt deleted file mode 100644 index c9ee2f16dc..0000000000 --- a/resources/solr/default_configset/lang/stopwords_fi.txt +++ /dev/null @@ -1,96 +0,0 @@ - | From https://snowballstem.org/algorithms/finnish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - -| forms of BE - -olla -olen -olet -on -olemme -olette -ovat -ole | negative form - -oli -olisi -olisit -olisin -olisimme -olisitte -olisivat -olit -olin -olimme -olitte -olivat -ollut -olleet - -en | negation -et -ei -emme -ette -eivät - -|Nom Gen Acc Part Iness Elat Illat Adess Ablat Allat Ess Trans -minä minun minut minua minussa minusta minuun minulla minulta minulle | I -sinä sinun sinut sinua sinussa sinusta sinuun sinulla sinulta sinulle | you -hän hänen hänet häntä hänessä hänestä häneen hänellä häneltä hänelle | he she -me meidän meidät meitä meissä meistä meihin meillä meiltä meille | we -te teidän teidät teitä teissä teistä teihin teillä teiltä teille | you -he heidän heidät heitä heissä heistä heihin heillä heiltä heille | they - -tämä tämän tätä tässä tästä tähän tällä tältä tälle tänä täksi | this -tuo tuon tuota tuossa tuosta tuohon tuolla tuolta tuolle tuona tuoksi | that -se sen sitä siinä siitä siihen sillä siltä sille sinä siksi | it -nämä näiden näitä näissä näistä näihin näillä näiltä näille näinä näiksi | these -nuo noiden noita noissa noista noihin noilla noilta noille noina noiksi | those -ne niiden niitä niissä niistä niihin niillä niiltä niille niinä niiksi | they - -kuka kenen kenet ketä kenessä kenestä keneen kenellä keneltä kenelle kenenä keneksi| who -ketkä keiden ketkä keitä keissä keistä keihin keillä keiltä keille keinä keiksi | (pl) -mikä minkä minkä mitä missä mistä mihin millä miltä mille minä miksi | which what -mitkä | (pl) - -joka jonka jota jossa josta johon jolla jolta jolle jona joksi | who which -jotka joiden joita joissa joista joihin joilla joilta joille joina joiksi | (pl) - -| conjunctions - -että | that -ja | and -jos | if -koska | because -kuin | than -mutta | but -niin | so -sekä | and -sillä | for -tai | or -vaan | but -vai | or -vaikka | although - - -| prepositions - -kanssa | with -mukaan | according to -noin | about -poikki | across -yli | over, across - -| other - -kun | when -nyt | now -itse | self - diff --git a/resources/solr/default_configset/lang/stopwords_fr.txt b/resources/solr/default_configset/lang/stopwords_fr.txt deleted file mode 100644 index 658ae9c91a..0000000000 --- a/resources/solr/default_configset/lang/stopwords_fr.txt +++ /dev/null @@ -1,186 +0,0 @@ - | From https://snowballstem.org/algorithms/french/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A French stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - -au | a + le -aux | a + les -avec | with -ce | this -ces | these -dans | with -de | of -des | de + les -du | de + le -elle | she -en | `of them' etc -et | and -eux | them -il | he -je | I -la | the -le | the -leur | their -lui | him -ma | my (fem) -mais | but -me | me -même | same; as in moi-même (myself) etc -mes | me (pl) -moi | me -mon | my (masc) -ne | not -nos | our (pl) -notre | our -nous | we -on | one -ou | where -par | by -pas | not -pour | for -qu | que before vowel -que | that -qui | who -sa | his, her (fem) -se | oneself -ses | his (pl) - | son | his, her (masc). Omitted because it is homonym of "sound" -sur | on -ta | thy (fem) -te | thee -tes | thy (pl) -toi | thee -ton | thy (masc) -tu | thou -un | a -une | a -vos | your (pl) -votre | your -vous | you - - | single letter forms - -c | c' -d | d' -j | j' -l | l' -à | to, at -m | m' -n | n' -s | s' -t | t' -y | there - - | forms of être (not including the infinitive): - | été - Omitted because it is homonym of "summer" -étée -étées - | étés - Omitted because it is homonym of "summers" -étant -suis -es - | est - Omitted because it is homonym of "east" - | sommes - Omitted because it is homonym of "sums" -êtes -sont -serai -seras -sera -serons -serez -seront -serais -serait -serions -seriez -seraient -étais -était -étions -étiez -étaient -fus -fut -fûmes -fûtes -furent -sois -soit -soyons -soyez -soient -fusse -fusses - | fût - Omitted because it is homonym of "tap", like in "beer on tap" -fussions -fussiez -fussent - - | forms of avoir (not including the infinitive): -ayant -eu -eue -eues -eus -ai - | as - Omitted because it is homonym of "ace" -avons -avez -ont -aurai - | auras - Omitted because it is also the name of a kind of wind - | aura - Omitted because it is also the name of a kind of wind and homonym of "aura" -aurons -aurez -auront -aurais -aurait -aurions -auriez -auraient -avais -avait - | avions - Omitted because it is homonym of "planes" -aviez -avaient -eut -eûmes -eûtes -eurent -aie -aies -ait -ayons -ayez -aient -eusse -eusses -eût -eussions -eussiez -eussent - - | Later additions (from Jean-Christophe Deschamps) -ceci | this -cela | that (added 11 Apr 2012. Omission reported by Adrien Grand) -celà | that (incorrect, though common) -cet | this -cette | this -ici | here -ils | they -les | the (pl) -leurs | their (pl) -quel | which -quels | which -quelle | which -quelles | which -sans | without -soi | oneself - diff --git a/resources/solr/default_configset/lang/stopwords_ga.txt b/resources/solr/default_configset/lang/stopwords_ga.txt deleted file mode 100644 index 9ff88d747e..0000000000 --- a/resources/solr/default_configset/lang/stopwords_ga.txt +++ /dev/null @@ -1,110 +0,0 @@ - -a -ach -ag -agus -an -aon -ar -arna -as -b' -ba -beirt -bhúr -caoga -ceathair -ceathrar -chomh -chtó -chuig -chun -cois -céad -cúig -cúigear -d' -daichead -dar -de -deich -deichniúr -den -dhá -do -don -dtí -dá -dár -dó -faoi -faoin -faoina -faoinár -fara -fiche -gach -gan -go -gur -haon -hocht -i -iad -idir -in -ina -ins -inár -is -le -leis -lena -lenár -m' -mar -mo -mé -na -nach -naoi -naonúr -ná -ní -níor -nó -nócha -ocht -ochtar -os -roimh -sa -seacht -seachtar -seachtó -seasca -seisear -siad -sibh -sinn -sna -sé -sí -tar -thar -thú -triúr -trí -trína -trínár -tríocha -tú -um -ár -é -éis -í -ó -ón -óna -ónár diff --git a/resources/solr/default_configset/lang/stopwords_gl.txt b/resources/solr/default_configset/lang/stopwords_gl.txt deleted file mode 100644 index d8760b12c1..0000000000 --- a/resources/solr/default_configset/lang/stopwords_gl.txt +++ /dev/null @@ -1,161 +0,0 @@ -# galican stopwords -a -aínda -alí -aquel -aquela -aquelas -aqueles -aquilo -aquí -ao -aos -as -así -á -ben -cando -che -co -coa -comigo -con -connosco -contigo -convosco -coas -cos -cun -cuns -cunha -cunhas -da -dalgunha -dalgunhas -dalgún -dalgúns -das -de -del -dela -delas -deles -desde -deste -do -dos -dun -duns -dunha -dunhas -e -el -ela -elas -eles -en -era -eran -esa -esas -ese -eses -esta -estar -estaba -está -están -este -estes -estiven -estou -eu -é -facer -foi -foron -fun -había -hai -iso -isto -la -las -lle -lles -lo -los -mais -me -meu -meus -min -miña -miñas -moi -na -nas -neste -nin -no -non -nos -nosa -nosas -noso -nosos -nós -nun -nunha -nuns -nunhas -o -os -ou -ó -ós -para -pero -pode -pois -pola -polas -polo -polos -por -que -se -senón -ser -seu -seus -sexa -sido -sobre -súa -súas -tamén -tan -te -ten -teñen -teño -ter -teu -teus -ti -tido -tiña -tiven -túa -túas -un -unha -unhas -uns -vos -vosa -vosas -voso -vosos -vós diff --git a/resources/solr/default_configset/lang/stopwords_hi.txt b/resources/solr/default_configset/lang/stopwords_hi.txt deleted file mode 100644 index 86286bb083..0000000000 --- a/resources/solr/default_configset/lang/stopwords_hi.txt +++ /dev/null @@ -1,235 +0,0 @@ -# Also see http://www.opensource.org/licenses/bsd-license.html -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# This file was created by Jacques Savoy and is distributed under the BSD license. -# Note: by default this file also contains forms normalized by HindiNormalizer -# for spelling variation (see section below), such that it can be used whether or -# not you enable that feature. When adding additional entries to this list, -# please add the normalized form as well. -अंदर -अत -अपना -अपनी -अपने -अभी -आदि -आप -इत्यादि -इन -इनका -इन्हीं -इन्हें -इन्हों -इस -इसका -इसकी -इसके -इसमें -इसी -इसे -उन -उनका -उनकी -उनके -उनको -उन्हीं -उन्हें -उन्हों -उस -उसके -उसी -उसे -एक -एवं -एस -ऐसे -और -कई -कर -करता -करते -करना -करने -करें -कहते -कहा -का -काफ़ी -कि -कितना -किन्हें -किन्हों -किया -किर -किस -किसी -किसे -की -कुछ -कुल -के -को -कोई -कौन -कौनसा -गया -घर -जब -जहाँ -जा -जितना -जिन -जिन्हें -जिन्हों -जिस -जिसे -जीधर -जैसा -जैसे -जो -तक -तब -तरह -तिन -तिन्हें -तिन्हों -तिस -तिसे -तो -था -थी -थे -दबारा -दिया -दुसरा -दूसरे -दो -द्वारा -न -नहीं -ना -निहायत -नीचे -ने -पर -पर -पहले -पूरा -पे -फिर -बनी -बही -बहुत -बाद -बाला -बिलकुल -भी -भीतर -मगर -मानो -मे -में -यदि -यह -यहाँ -यही -या -यिह -ये -रखें -रहा -रहे -ऱ्वासा -लिए -लिये -लेकिन -व -वर्ग -वह -वह -वहाँ -वहीं -वाले -वुह -वे -वग़ैरह -संग -सकता -सकते -सबसे -सभी -साथ -साबुत -साभ -सारा -से -सो -ही -हुआ -हुई -हुए -है -हैं -हो -होता -होती -होते -होना -होने -# additional normalized forms of the above -अपनि -जेसे -होति -सभि -तिंहों -इंहों -दवारा -इसि -किंहें -थि -उंहों -ओर -जिंहें -वहिं -अभि -बनि -हि -उंहिं -उंहें -हें -वगेरह -एसे -रवासा -कोन -निचे -काफि -उसि -पुरा -भितर -हे -बहि -वहां -कोइ -यहां -जिंहों -तिंहें -किसि -कइ -यहि -इंहिं -जिधर -इंहें -अदि -इतयादि -हुइ -कोनसा -इसकि -दुसरे -जहां -अप -किंहों -उनकि -भि -वरग -हुअ -जेसा -नहिं diff --git a/resources/solr/default_configset/lang/stopwords_hu.txt b/resources/solr/default_configset/lang/stopwords_hu.txt deleted file mode 100644 index 3fa279eac9..0000000000 --- a/resources/solr/default_configset/lang/stopwords_hu.txt +++ /dev/null @@ -1,211 +0,0 @@ - | From https://snowballstem.org/algorithms/hungarian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - -| Hungarian stop word list -| prepared by Anna Tordai - -a -ahogy -ahol -aki -akik -akkor -alatt -által -általában -amely -amelyek -amelyekben -amelyeket -amelyet -amelynek -ami -amit -amolyan -amíg -amikor -át -abban -ahhoz -annak -arra -arról -az -azok -azon -azt -azzal -azért -aztán -azután -azonban -bár -be -belül -benne -cikk -cikkek -cikkeket -csak -de -e -eddig -egész -egy -egyes -egyetlen -egyéb -egyik -egyre -ekkor -el -elég -ellen -elő -először -előtt -első -én -éppen -ebben -ehhez -emilyen -ennek -erre -ez -ezt -ezek -ezen -ezzel -ezért -és -fel -felé -hanem -hiszen -hogy -hogyan -igen -így -illetve -ill. -ill -ilyen -ilyenkor -ison -ismét -itt -jó -jól -jobban -kell -kellett -keresztül -keressünk -ki -kívül -között -közül -legalább -lehet -lehetett -legyen -lenne -lenni -lesz -lett -maga -magát -majd -majd -már -más -másik -meg -még -mellett -mert -mely -melyek -mi -mit -míg -miért -milyen -mikor -minden -mindent -mindenki -mindig -mint -mintha -mivel -most -nagy -nagyobb -nagyon -ne -néha -nekem -neki -nem -néhány -nélkül -nincs -olyan -ott -össze -ő -ők -őket -pedig -persze -rá -s -saját -sem -semmi -sok -sokat -sokkal -számára -szemben -szerint -szinte -talán -tehát -teljes -tovább -továbbá -több -úgy -ugyanis -új -újabb -újra -után -utána -utolsó -vagy -vagyis -valaki -valami -valamint -való -vagyok -van -vannak -volt -voltam -voltak -voltunk -vissza -vele -viszont -volna diff --git a/resources/solr/default_configset/lang/stopwords_hy.txt b/resources/solr/default_configset/lang/stopwords_hy.txt deleted file mode 100644 index 60c1c50fbc..0000000000 --- a/resources/solr/default_configset/lang/stopwords_hy.txt +++ /dev/null @@ -1,46 +0,0 @@ -# example set of Armenian stopwords. -այդ -այլ -այն -այս -դու -դուք -եմ -են -ենք -ես -եք -է -էի -էին -էինք -էիր -էիք -էր -ըստ -թ -ի -ին -իսկ -իր -կամ -համար -հետ -հետո -մենք -մեջ -մի -ն -նա -նաև -նրա -նրանք -որ -որը -որոնք -որպես -ու -ում -պիտի -վրա -և diff --git a/resources/solr/default_configset/lang/stopwords_id.txt b/resources/solr/default_configset/lang/stopwords_id.txt deleted file mode 100644 index 4617f83a5c..0000000000 --- a/resources/solr/default_configset/lang/stopwords_id.txt +++ /dev/null @@ -1,359 +0,0 @@ -# from appendix D of: A Study of Stemming Effects on Information -# Retrieval in Bahasa Indonesia -ada -adanya -adalah -adapun -agak -agaknya -agar -akan -akankah -akhirnya -aku -akulah -amat -amatlah -anda -andalah -antar -diantaranya -antara -antaranya -diantara -apa -apaan -mengapa -apabila -apakah -apalagi -apatah -atau -ataukah -ataupun -bagai -bagaikan -sebagai -sebagainya -bagaimana -bagaimanapun -sebagaimana -bagaimanakah -bagi -bahkan -bahwa -bahwasanya -sebaliknya -banyak -sebanyak -beberapa -seberapa -begini -beginian -beginikah -beginilah -sebegini -begitu -begitukah -begitulah -begitupun -sebegitu -belum -belumlah -sebelum -sebelumnya -sebenarnya -berapa -berapakah -berapalah -berapapun -betulkah -sebetulnya -biasa -biasanya -bila -bilakah -bisa -bisakah -sebisanya -boleh -bolehkah -bolehlah -buat -bukan -bukankah -bukanlah -bukannya -cuma -percuma -dahulu -dalam -dan -dapat -dari -daripada -dekat -demi -demikian -demikianlah -sedemikian -dengan -depan -di -dia -dialah -dini -diri -dirinya -terdiri -dong -dulu -enggak -enggaknya -entah -entahlah -terhadap -terhadapnya -hal -hampir -hanya -hanyalah -harus -haruslah -harusnya -seharusnya -hendak -hendaklah -hendaknya -hingga -sehingga -ia -ialah -ibarat -ingin -inginkah -inginkan -ini -inikah -inilah -itu -itukah -itulah -jangan -jangankan -janganlah -jika -jikalau -juga -justru -kala -kalau -kalaulah -kalaupun -kalian -kami -kamilah -kamu -kamulah -kan -kapan -kapankah -kapanpun -dikarenakan -karena -karenanya -ke -kecil -kemudian -kenapa -kepada -kepadanya -ketika -seketika -khususnya -kini -kinilah -kiranya -sekiranya -kita -kitalah -kok -lagi -lagian -selagi -lah -lain -lainnya -melainkan -selaku -lalu -melalui -terlalu -lama -lamanya -selama -selama -selamanya -lebih -terlebih -bermacam -macam -semacam -maka -makanya -makin -malah -malahan -mampu -mampukah -mana -manakala -manalagi -masih -masihkah -semasih -masing -mau -maupun -semaunya -memang -mereka -merekalah -meski -meskipun -semula -mungkin -mungkinkah -nah -namun -nanti -nantinya -nyaris -oleh -olehnya -seorang -seseorang -pada -padanya -padahal -paling -sepanjang -pantas -sepantasnya -sepantasnyalah -para -pasti -pastilah -per -pernah -pula -pun -merupakan -rupanya -serupa -saat -saatnya -sesaat -saja -sajalah -saling -bersama -sama -sesama -sambil -sampai -sana -sangat -sangatlah -saya -sayalah -se -sebab -sebabnya -sebuah -tersebut -tersebutlah -sedang -sedangkan -sedikit -sedikitnya -segala -segalanya -segera -sesegera -sejak -sejenak -sekali -sekalian -sekalipun -sesekali -sekaligus -sekarang -sekarang -sekitar -sekitarnya -sela -selain -selalu -seluruh -seluruhnya -semakin -sementara -sempat -semua -semuanya -sendiri -sendirinya -seolah -seperti -sepertinya -sering -seringnya -serta -siapa -siapakah -siapapun -disini -disinilah -sini -sinilah -sesuatu -sesuatunya -suatu -sesudah -sesudahnya -sudah -sudahkah -sudahlah -supaya -tadi -tadinya -tak -tanpa -setelah -telah -tentang -tentu -tentulah -tentunya -tertentu -seterusnya -tapi -tetapi -setiap -tiap -setidaknya -tidak -tidakkah -tidaklah -toh -waduh -wah -wahai -sewaktu -walau -walaupun -wong -yaitu -yakni -yang diff --git a/resources/solr/default_configset/lang/stopwords_it.txt b/resources/solr/default_configset/lang/stopwords_it.txt deleted file mode 100644 index c74160e28c..0000000000 --- a/resources/solr/default_configset/lang/stopwords_it.txt +++ /dev/null @@ -1,303 +0,0 @@ - | From https://snowballstem.org/algorithms/italian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | An Italian stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - -ad | a (to) before vowel -al | a + il -allo | a + lo -ai | a + i -agli | a + gli -all | a + l' -agl | a + gl' -alla | a + la -alle | a + le -con | with -col | con + il -coi | con + i (forms collo, cogli etc are now very rare) -da | from -dal | da + il -dallo | da + lo -dai | da + i -dagli | da + gli -dall | da + l' -dagl | da + gll' -dalla | da + la -dalle | da + le -di | of -del | di + il -dello | di + lo -dei | di + i -degli | di + gli -dell | di + l' -degl | di + gl' -della | di + la -delle | di + le -in | in -nel | in + el -nello | in + lo -nei | in + i -negli | in + gli -nell | in + l' -negl | in + gl' -nella | in + la -nelle | in + le -su | on -sul | su + il -sullo | su + lo -sui | su + i -sugli | su + gli -sull | su + l' -sugl | su + gl' -sulla | su + la -sulle | su + le -per | through, by -tra | among -contro | against -io | I -tu | thou -lui | he -lei | she -noi | we -voi | you -loro | they -mio | my -mia | -miei | -mie | -tuo | -tua | -tuoi | thy -tue | -suo | -sua | -suoi | his, her -sue | -nostro | our -nostra | -nostri | -nostre | -vostro | your -vostra | -vostri | -vostre | -mi | me -ti | thee -ci | us, there -vi | you, there -lo | him, the -la | her, the -li | them -le | them, the -gli | to him, the -ne | from there etc -il | the -un | a -uno | a -una | a -ma | but -ed | and -se | if -perché | why, because -anche | also -come | how -dov | where (as dov') -dove | where -che | who, that -chi | who -cui | whom -non | not -più | more -quale | who, that -quanto | how much -quanti | -quanta | -quante | -quello | that -quelli | -quella | -quelle | -questo | this -questi | -questa | -queste | -si | yes -tutto | all -tutti | all - - | single letter forms: - -a | at -c | as c' for ce or ci -e | and -i | the -l | as l' -o | or - - | forms of avere, to have (not including the infinitive): - -ho -hai -ha -abbiamo -avete -hanno -abbia -abbiate -abbiano -avrò -avrai -avrà -avremo -avrete -avranno -avrei -avresti -avrebbe -avremmo -avreste -avrebbero -avevo -avevi -aveva -avevamo -avevate -avevano -ebbi -avesti -ebbe -avemmo -aveste -ebbero -avessi -avesse -avessimo -avessero -avendo -avuto -avuta -avuti -avute - - | forms of essere, to be (not including the infinitive): -sono -sei -è -siamo -siete -sia -siate -siano -sarò -sarai -sarà -saremo -sarete -saranno -sarei -saresti -sarebbe -saremmo -sareste -sarebbero -ero -eri -era -eravamo -eravate -erano -fui -fosti -fu -fummo -foste -furono -fossi -fosse -fossimo -fossero -essendo - - | forms of fare, to do (not including the infinitive, fa, fat-): -faccio -fai -facciamo -fanno -faccia -facciate -facciano -farò -farai -farà -faremo -farete -faranno -farei -faresti -farebbe -faremmo -fareste -farebbero -facevo -facevi -faceva -facevamo -facevate -facevano -feci -facesti -fece -facemmo -faceste -fecero -facessi -facesse -facessimo -facessero -facendo - - | forms of stare, to be (not including the infinitive): -sto -stai -sta -stiamo -stanno -stia -stiate -stiano -starò -starai -starà -staremo -starete -staranno -starei -staresti -starebbe -staremmo -stareste -starebbero -stavo -stavi -stava -stavamo -stavate -stavano -stetti -stesti -stette -stemmo -steste -stettero -stessi -stesse -stessimo -stessero -stando diff --git a/resources/solr/default_configset/lang/stopwords_ja.txt b/resources/solr/default_configset/lang/stopwords_ja.txt deleted file mode 100644 index d4321be6b1..0000000000 --- a/resources/solr/default_configset/lang/stopwords_ja.txt +++ /dev/null @@ -1,127 +0,0 @@ -# -# This file defines a stopword set for Japanese. -# -# This set is made up of hand-picked frequent terms from segmented Japanese Wikipedia. -# Punctuation characters and frequent kanji have mostly been left out. See LUCENE-3745 -# for frequency lists, etc. that can be useful for making your own set (if desired) -# -# Note that there is an overlap between these stopwords and the terms stopped when used -# in combination with the JapanesePartOfSpeechStopFilter. When editing this file, note -# that comments are not allowed on the same line as stopwords. -# -# Also note that stopping is done in a case-insensitive manner. Change your StopFilter -# configuration if you need case-sensitive stopping. Lastly, note that stopping is done -# using the same character width as the entries in this file. Since this StopFilter is -# normally done after a CJKWidthFilter in your chain, you would usually want your romaji -# entries to be in half-width and your kana entries to be in full-width. -# -の -に -は -を -た -が -で -て -と -し -れ -さ -ある -いる -も -する -から -な -こと -として -い -や -れる -など -なっ -ない -この -ため -その -あっ -よう -また -もの -という -あり -まで -られ -なる -へ -か -だ -これ -によって -により -おり -より -による -ず -なり -られる -において -ば -なかっ -なく -しかし -について -せ -だっ -その後 -できる -それ -う -ので -なお -のみ -でき -き -つ -における -および -いう -さらに -でも -ら -たり -その他 -に関する -たち -ます -ん -なら -に対して -特に -せる -及び -これら -とき -では -にて -ほか -ながら -うち -そして -とともに -ただし -かつて -それぞれ -または -お -ほど -ものの -に対する -ほとんど -と共に -といった -です -とも -ところ -ここ -##### End of file diff --git a/resources/solr/default_configset/lang/stopwords_lv.txt b/resources/solr/default_configset/lang/stopwords_lv.txt deleted file mode 100644 index e21a23c06c..0000000000 --- a/resources/solr/default_configset/lang/stopwords_lv.txt +++ /dev/null @@ -1,172 +0,0 @@ -# Set of Latvian stopwords from A Stemming Algorithm for Latvian, Karlis Kreslins -# the original list of over 800 forms was refined: -# pronouns, adverbs, interjections were removed -# -# prepositions -aiz -ap -ar -apakš -ārpus -augšpus -bez -caur -dēļ -gar -iekš -iz -kopš -labad -lejpus -līdz -no -otrpus -pa -par -pār -pēc -pie -pirms -pret -priekš -starp -šaipus -uz -viņpus -virs -virspus -zem -apakšpus -# Conjunctions -un -bet -jo -ja -ka -lai -tomēr -tikko -turpretī -arī -kaut -gan -tādēļ -tā -ne -tikvien -vien -kā -ir -te -vai -kamēr -# Particles -ar -diezin -droši -diemžēl -nebūt -ik -it -taču -nu -pat -tiklab -iekšpus -nedz -tik -nevis -turpretim -jeb -iekam -iekām -iekāms -kolīdz -līdzko -tiklīdz -jebšu -tālab -tāpēc -nekā -itin -jā -jau -jel -nē -nezin -tad -tikai -vis -tak -iekams -vien -# modal verbs -būt -biju -biji -bija -bijām -bijāt -esmu -esi -esam -esat -būšu -būsi -būs -būsim -būsiet -tikt -tiku -tiki -tika -tikām -tikāt -tieku -tiec -tiek -tiekam -tiekat -tikšu -tiks -tiksim -tiksiet -tapt -tapi -tapāt -topat -tapšu -tapsi -taps -tapsim -tapsiet -kļūt -kļuvu -kļuvi -kļuva -kļuvām -kļuvāt -kļūstu -kļūsti -kļūst -kļūstam -kļūstat -kļūšu -kļūsi -kļūs -kļūsim -kļūsiet -# verbs -varēt -varēju -varējām -varēšu -varēsim -var -varēji -varējāt -varēsi -varēsiet -varat -varēja -varēs diff --git a/resources/solr/default_configset/lang/stopwords_nl.txt b/resources/solr/default_configset/lang/stopwords_nl.txt deleted file mode 100644 index 48c5515123..0000000000 --- a/resources/solr/default_configset/lang/stopwords_nl.txt +++ /dev/null @@ -1,121 +0,0 @@ - | From https://snowballstem.org/algorithms/dutch/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - - | A Dutch stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large sample of Dutch text. - - | Dutch stop words frequently exhibit homonym clashes. These are indicated - | clearly below. - -de | the -en | and -van | of, from -ik | I, the ego -te | (1) chez, at etc, (2) to, (3) too -dat | that, which -die | that, those, who, which -in | in, inside -een | a, an, one -hij | he -het | the, it -niet | not, nothing, naught -zijn | (1) to be, being, (2) his, one's, its -is | is -was | (1) was, past tense of all persons sing. of 'zijn' (to be) (2) wax, (3) the washing, (4) rise of river -op | on, upon, at, in, up, used up -aan | on, upon, to (as dative) -met | with, by -als | like, such as, when -voor | (1) before, in front of, (2) furrow -had | had, past tense all persons sing. of 'hebben' (have) -er | there -maar | but, only -om | round, about, for etc -hem | him -dan | then -zou | should/would, past tense all persons sing. of 'zullen' -of | or, whether, if -wat | what, something, anything -mijn | possessive and noun 'mine' -men | people, 'one' -dit | this -zo | so, thus, in this way -door | through by -over | over, across -ze | she, her, they, them -zich | oneself -bij | (1) a bee, (2) by, near, at -ook | also, too -tot | till, until -je | you -mij | me -uit | out of, from -der | Old Dutch form of 'van der' still found in surnames -daar | (1) there, (2) because -haar | (1) her, their, them, (2) hair -naar | (1) unpleasant, unwell etc, (2) towards, (3) as -heb | present first person sing. of 'to have' -hoe | how, why -heeft | present third person sing. of 'to have' -hebben | 'to have' and various parts thereof -deze | this -u | you -want | (1) for, (2) mitten, (3) rigging -nog | yet, still -zal | 'shall', first and third person sing. of verb 'zullen' (will) -me | me -zij | she, they -nu | now -ge | 'thou', still used in Belgium and south Netherlands -geen | none -omdat | because -iets | something, somewhat -worden | to become, grow, get -toch | yet, still -al | all, every, each -waren | (1) 'were' (2) to wander, (3) wares, (3) -veel | much, many -meer | (1) more, (2) lake -doen | to do, to make -toen | then, when -moet | noun 'spot/mote' and present form of 'to must' -ben | (1) am, (2) 'are' in interrogative second person singular of 'to be' -zonder | without -kan | noun 'can' and present form of 'to be able' -hun | their, them -dus | so, consequently -alles | all, everything, anything -onder | under, beneath -ja | yes, of course -eens | once, one day -hier | here -wie | who -werd | imperfect third person sing. of 'become' -altijd | always -doch | yet, but etc -wordt | present third person sing. of 'become' -wezen | (1) to be, (2) 'been' as in 'been fishing', (3) orphans -kunnen | to be able -ons | us/our -zelf | self -tegen | against, towards, at -na | after, near -reeds | already -wil | (1) present tense of 'want', (2) 'will', noun, (3) fender -kon | could; past tense of 'to be able' -niets | nothing -uw | your -iemand | somebody -geweest | been; past participle of 'be' -andere | other - diff --git a/resources/solr/default_configset/lang/stopwords_no.txt b/resources/solr/default_configset/lang/stopwords_no.txt deleted file mode 100644 index f427609484..0000000000 --- a/resources/solr/default_configset/lang/stopwords_no.txt +++ /dev/null @@ -1,190 +0,0 @@ - | From https://snowballstem.org/algorithms/norwegian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Norwegian stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This stop word list is for the dominant bokmål dialect. Words unique - | to nynorsk are marked *. - - | Revised by Jan Bruusgaard , Jan 2005 - -og | and -i | in -jeg | I -det | it/this/that -at | to (w. inf.) -en | a/an -et | a/an -den | it/this/that -til | to -er | is/am/are -som | who/which/that -på | on -de | they / you(formal) -med | with -han | he -av | of -ikke | not -ikkje | not * -der | there -så | so -var | was/were -meg | me -seg | you -men | but -ett | one -har | have -om | about -vi | we -min | my -mitt | my -ha | have -hadde | had -hun | she -nå | now -over | over -da | when/as -ved | by/know -fra | from -du | you -ut | out -sin | your -dem | them -oss | us -opp | up -man | you/one -kan | can -hans | his -hvor | where -eller | or -hva | what -skal | shall/must -selv | self (reflective) -sjøl | self (reflective) -her | here -alle | all -vil | will -bli | become -ble | became -blei | became * -blitt | have become -kunne | could -inn | in -når | when -være | be -kom | come -noen | some -noe | some -ville | would -dere | you -deres | their/theirs -kun | only/just -ja | yes -etter | after -ned | down -skulle | should -denne | this -for | for/because -deg | you -si | hers/his -sine | hers/his -sitt | hers/his -mot | against -å | to -meget | much -hvorfor | why -dette | this -disse | these/those -uten | without -hvordan | how -ingen | none -din | your -ditt | your -blir | become -samme | same -hvilken | which -hvilke | which (plural) -sånn | such a -inni | inside/within -mellom | between -vår | our -hver | each -hvem | who -vors | us/ours -hvis | whose -både | both -bare | only/just -enn | than -fordi | as/because -før | before -mange | many -også | also -slik | just -vært | been -båe | both * -begge | both -siden | since -dykk | your * -dykkar | yours * -dei | they * -deira | them * -deires | theirs * -deim | them * -di | your (fem.) * -då | as/when * -eg | I * -ein | a/an * -eit | a/an * -eitt | a/an * -elles | or * -honom | he * -hjå | at * -ho | she * -hoe | she * -henne | her -hennar | her/hers -hennes | hers -hoss | how * -hossen | how * -ingi | noone * -inkje | noone * -korleis | how * -korso | how * -kva | what/which * -kvar | where * -kvarhelst | where * -kven | who/whom * -kvi | why * -kvifor | why * -me | we * -medan | while * -mi | my * -mine | my * -mykje | much * -no | now * -nokon | some (masc./neut.) * -noka | some (fem.) * -nokor | some * -noko | some * -nokre | some * -sia | since * -sidan | since * -so | so * -somt | some * -somme | some * -um | about* -upp | up * -vere | be * -vore | was * -verte | become * -vort | become * -varte | became * -vart | became * - diff --git a/resources/solr/default_configset/lang/stopwords_pt.txt b/resources/solr/default_configset/lang/stopwords_pt.txt deleted file mode 100644 index d03d7f234d..0000000000 --- a/resources/solr/default_configset/lang/stopwords_pt.txt +++ /dev/null @@ -1,253 +0,0 @@ - | From https://snowballstem.org/algorithms/portuguese/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Portuguese stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - - | The following is a ranked list (commonest to rarest) of stopwords - | deriving from a large sample of text. - - | Extra words have been added at the end. - -de | of, from -a | the; to, at; her -o | the; him -que | who, that -e | and -do | de + o -da | de + a -em | in -um | a -para | for - | é from SER -com | with -não | not, no -uma | a -os | the; them -no | em + o -se | himself etc -na | em + a -por | for -mais | more -as | the; them -dos | de + os -como | as, like -mas | but - | foi from SER -ao | a + o -ele | he -das | de + as - | tem from TER -à | a + a -seu | his -sua | her -ou | or - | ser from SER -quando | when -muito | much - | há from HAV -nos | em + os; us -já | already, now - | está from EST -eu | I -também | also -só | only, just -pelo | per + o -pela | per + a -até | up to -isso | that -ela | he -entre | between - | era from SER -depois | after -sem | without -mesmo | same -aos | a + os - | ter from TER -seus | his -quem | whom -nas | em + as -me | me -esse | that -eles | they - | estão from EST -você | you - | tinha from TER - | foram from SER -essa | that -num | em + um -nem | nor -suas | her -meu | my -às | a + as -minha | my - | têm from TER -numa | em + uma -pelos | per + os -elas | they - | havia from HAV - | seja from SER -qual | which - | será from SER -nós | we - | tenho from TER -lhe | to him, her -deles | of them -essas | those -esses | those -pelas | per + as -este | this - | fosse from SER -dele | of him - - | other words. There are many contractions such as naquele = em+aquele, - | mo = me+o, but they are rare. - | Indefinite article plural forms are also rare. - -tu | thou -te | thee -vocês | you (plural) -vos | you -lhes | to them -meus | my -minhas -teu | thy -tua -teus -tuas -nosso | our -nossa -nossos -nossas - -dela | of her -delas | of them - -esta | this -estes | these -estas | these -aquele | that -aquela | that -aqueles | those -aquelas | those -isto | this -aquilo | that - - | forms of estar, to be (not including the infinitive): -estou -está -estamos -estão -estive -esteve -estivemos -estiveram -estava -estávamos -estavam -estivera -estivéramos -esteja -estejamos -estejam -estivesse -estivéssemos -estivessem -estiver -estivermos -estiverem - - | forms of haver, to have (not including the infinitive): -hei -há -havemos -hão -houve -houvemos -houveram -houvera -houvéramos -haja -hajamos -hajam -houvesse -houvéssemos -houvessem -houver -houvermos -houverem -houverei -houverá -houveremos -houverão -houveria -houveríamos -houveriam - - | forms of ser, to be (not including the infinitive): -sou -somos -são -era -éramos -eram -fui -foi -fomos -foram -fora -fôramos -seja -sejamos -sejam -fosse -fôssemos -fossem -for -formos -forem -serei -será -seremos -serão -seria -seríamos -seriam - - | forms of ter, to have (not including the infinitive): -tenho -tem -temos -tém -tinha -tínhamos -tinham -tive -teve -tivemos -tiveram -tivera -tivéramos -tenha -tenhamos -tenham -tivesse -tivéssemos -tivessem -tiver -tivermos -tiverem -terei -terá -teremos -terão -teria -teríamos -teriam diff --git a/resources/solr/default_configset/lang/stopwords_ro.txt b/resources/solr/default_configset/lang/stopwords_ro.txt deleted file mode 100644 index 4fdee90a5b..0000000000 --- a/resources/solr/default_configset/lang/stopwords_ro.txt +++ /dev/null @@ -1,233 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -acea -aceasta -această -aceea -acei -aceia -acel -acela -acele -acelea -acest -acesta -aceste -acestea -aceşti -aceştia -acolo -acum -ai -aia -aibă -aici -al -ăla -ale -alea -ălea -altceva -altcineva -am -ar -are -aş -aşadar -asemenea -asta -ăsta -astăzi -astea -ăstea -ăştia -asupra -aţi -au -avea -avem -aveţi -azi -bine -bucur -bună -ca -că -căci -când -care -cărei -căror -cărui -cât -câte -câţi -către -câtva -ce -cel -ceva -chiar -cînd -cine -cineva -cît -cîte -cîţi -cîtva -contra -cu -cum -cumva -curând -curînd -da -dă -dacă -dar -datorită -de -deci -deja -deoarece -departe -deşi -din -dinaintea -dintr -dintre -drept -după -ea -ei -el -ele -eram -este -eşti -eu -face -fără -fi -fie -fiecare -fii -fim -fiţi -iar -ieri -îi -îl -îmi -împotriva -în -înainte -înaintea -încât -încît -încotro -între -întrucât -întrucît -îţi -la -lângă -le -li -lîngă -lor -lui -mă -mâine -mea -mei -mele -mereu -meu -mi -mine -mult -multă -mulţi -ne -nicăieri -nici -nimeni -nişte -noastră -noastre -noi -noştri -nostru -nu -ori -oricând -oricare -oricât -orice -oricînd -oricine -oricît -oricum -oriunde -până -pe -pentru -peste -pînă -poate -pot -prea -prima -primul -prin -printr -sa -să -săi -sale -sau -său -se -şi -sînt -sîntem -sînteţi -spre -sub -sunt -suntem -sunteţi -ta -tăi -tale -tău -te -ţi -ţie -tine -toată -toate -tot -toţi -totuşi -tu -un -una -unde -undeva -unei -unele -uneori -unor -vă -vi -voastră -voastre -voi -voştri -vostru -vouă -vreo -vreun diff --git a/resources/solr/default_configset/lang/stopwords_ru.txt b/resources/solr/default_configset/lang/stopwords_ru.txt deleted file mode 100644 index 65512d49db..0000000000 --- a/resources/solr/default_configset/lang/stopwords_ru.txt +++ /dev/null @@ -1,244 +0,0 @@ - | From https://snowballstem.org/algorithms/russian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - - | a russian stop word list. comments begin with vertical bar. each stop - | word is at the start of a line. - - | this is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - | letter `ё' is translated to `е'. - -и | and -в | in/into -во | alternative form -не | not -что | what/that -он | he -на | on/onto -я | i -с | from -со | alternative form -как | how -а | milder form of `no' (but) -то | conjunction and form of `that' -все | all -она | she -так | so, thus -его | him -но | but -да | yes/and -ты | thou -к | towards, by -у | around, chez -же | intensifier particle -вы | you -за | beyond, behind -бы | conditional/subj. particle -по | up to, along -только | only -ее | her -мне | to me -было | it was -вот | here is/are, particle -от | away from -меня | me -еще | still, yet, more -нет | no, there isnt/arent -о | about -из | out of -ему | to him -теперь | now -когда | when -даже | even -ну | so, well -вдруг | suddenly -ли | interrogative particle -если | if -уже | already, but homonym of `narrower' -или | or -ни | neither -быть | to be -был | he was -него | prepositional form of его -до | up to -вас | you accusative -нибудь | indef. suffix preceded by hyphen -опять | again -уж | already, but homonym of `adder' -вам | to you -сказал | he said -ведь | particle `after all' -там | there -потом | then -себя | oneself -ничего | nothing -ей | to her -может | usually with `быть' as `maybe' -они | they -тут | here -где | where -есть | there is/are -надо | got to, must -ней | prepositional form of ей -для | for -мы | we -тебя | thee -их | them, their -чем | than -была | she was -сам | self -чтоб | in order to -без | without -будто | as if -человек | man, person, one -чего | genitive form of `what' -раз | once -тоже | also -себе | to oneself -под | beneath -жизнь | life -будет | will be -ж | short form of intensifer particle `же' -тогда | then -кто | who -этот | this -говорил | was saying -того | genitive form of `that' -потому | for that reason -этого | genitive form of `this' -какой | which -совсем | altogether -ним | prepositional form of `его', `они' -здесь | here -этом | prepositional form of `этот' -один | one -почти | almost -мой | my -тем | instrumental/dative plural of `тот', `то' -чтобы | full form of `in order that' -нее | her (acc.) -кажется | it seems -сейчас | now -были | they were -куда | where to -зачем | why -сказать | to say -всех | all (acc., gen. preposn. plural) -никогда | never -сегодня | today -можно | possible, one can -при | by -наконец | finally -два | two -об | alternative form of `о', about -другой | another -хоть | even -после | after -над | above -больше | more -тот | that one (masc.) -через | across, in -эти | these -нас | us -про | about -всего | in all, only, of all -них | prepositional form of `они' (they) -какая | which, feminine -много | lots -разве | interrogative particle -сказала | she said -три | three -эту | this, acc. fem. sing. -моя | my, feminine -впрочем | moreover, besides -хорошо | good -свою | ones own, acc. fem. sing. -этой | oblique form of `эта', fem. `this' -перед | in front of -иногда | sometimes -лучше | better -чуть | a little -том | preposn. form of `that one' -нельзя | one must not -такой | such a one -им | to them -более | more -всегда | always -конечно | of course -всю | acc. fem. sing of `all' -между | between - - - | b: some paradigms - | - | personal pronouns - | - | я меня мне мной [мною] - | ты тебя тебе тобой [тобою] - | он его ему им [него, нему, ним] - | она ее эи ею [нее, нэи, нею] - | оно его ему им [него, нему, ним] - | - | мы нас нам нами - | вы вас вам вами - | они их им ими [них, ним, ними] - | - | себя себе собой [собою] - | - | demonstrative pronouns: этот (this), тот (that) - | - | этот эта это эти - | этого эты это эти - | этого этой этого этих - | этому этой этому этим - | этим этой этим [этою] этими - | этом этой этом этих - | - | тот та то те - | того ту то те - | того той того тех - | тому той тому тем - | тем той тем [тою] теми - | том той том тех - | - | determinative pronouns - | - | (a) весь (all) - | - | весь вся все все - | всего всю все все - | всего всей всего всех - | всему всей всему всем - | всем всей всем [всею] всеми - | всем всей всем всех - | - | (b) сам (himself etc) - | - | сам сама само сами - | самого саму само самих - | самого самой самого самих - | самому самой самому самим - | самим самой самим [самою] самими - | самом самой самом самих - | - | stems of verbs `to be', `to have', `to do' and modal - | - | быть бы буд быв есть суть - | име - | дел - | мог мож мочь - | уме - | хоч хот - | долж - | можн - | нужн - | нельзя - diff --git a/resources/solr/default_configset/lang/stopwords_sv.txt b/resources/solr/default_configset/lang/stopwords_sv.txt deleted file mode 100644 index d1d0d10088..0000000000 --- a/resources/solr/default_configset/lang/stopwords_sv.txt +++ /dev/null @@ -1,133 +0,0 @@ - | From https://snowballstem.org/algorithms/swedish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Swedish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - | Swedish stop words occasionally exhibit homonym clashes. For example - | så = so, but also seed. These are indicated clearly below. - -och | and -det | it, this/that -att | to (with infinitive) -i | in, at -en | a -jag | I -hon | she -som | who, that -han | he -på | on -den | it, this/that -med | with -var | where, each -sig | him(self) etc -för | for -så | so (also: seed) -till | to -är | is -men | but -ett | a -om | if; around, about -hade | had -de | they, these/those -av | of -icke | not, no -mig | me -du | you -henne | her -då | then, when -sin | his -nu | now -har | have -inte | inte någon = no one -hans | his -honom | him -skulle | 'sake' -hennes | her -där | there -min | my -man | one (pronoun) -ej | nor -vid | at, by, on (also: vast) -kunde | could -något | some etc -från | from, off -ut | out -när | when -efter | after, behind -upp | up -vi | we -dem | them -vara | be -vad | what -över | over -än | than -dig | you -kan | can -sina | his -här | here -ha | have -mot | towards -alla | all -under | under (also: wonder) -någon | some etc -eller | or (else) -allt | all -mycket | much -sedan | since -ju | why -denna | this/that -själv | myself, yourself etc -detta | this/that -åt | to -utan | without -varit | was -hur | how -ingen | no -mitt | my -ni | you -bli | to be, become -blev | from bli -oss | us -din | thy -dessa | these/those -några | some etc -deras | their -blir | from bli -mina | my -samma | (the) same -vilken | who, that -er | you, your -sådan | such a -vår | our -blivit | from bli -dess | its -inom | within -mellan | between -sådant | such a -varför | why -varje | each -vilka | who, that -ditt | thy -vem | who -vilket | who, that -sitt | his -sådana | such a -vart | each -dina | thy -vars | whose -vårt | our -våra | our -ert | your -era | your -vilkas | whose - diff --git a/resources/solr/default_configset/lang/stopwords_th.txt b/resources/solr/default_configset/lang/stopwords_th.txt deleted file mode 100644 index 07f0fabe69..0000000000 --- a/resources/solr/default_configset/lang/stopwords_th.txt +++ /dev/null @@ -1,119 +0,0 @@ -# Thai stopwords from: -# "Opinion Detection in Thai Political News Columns -# Based on Subjectivity Analysis" -# Khampol Sukhum, Supot Nitsuwat, and Choochart Haruechaiyasak -ไว้ -ไม่ -ไป -ได้ -ให้ -ใน -โดย -แห่ง -แล้ว -และ -แรก -แบบ -แต่ -เอง -เห็น -เลย -เริ่ม -เรา -เมื่อ -เพื่อ -เพราะ -เป็นการ -เป็น -เปิดเผย -เปิด -เนื่องจาก -เดียวกัน -เดียว -เช่น -เฉพาะ -เคย -เข้า -เขา -อีก -อาจ -อะไร -ออก -อย่าง -อยู่ -อยาก -หาก -หลาย -หลังจาก -หลัง -หรือ -หนึ่ง -ส่วน -ส่ง -สุด -สําหรับ -ว่า -วัน -ลง -ร่วม -ราย -รับ -ระหว่าง -รวม -ยัง -มี -มาก -มา -พร้อม -พบ -ผ่าน -ผล -บาง -น่า -นี้ -นํา -นั้น -นัก -นอกจาก -ทุก -ที่สุด -ที่ -ทําให้ -ทํา -ทาง -ทั้งนี้ -ทั้ง -ถ้า -ถูก -ถึง -ต้อง -ต่างๆ -ต่าง -ต่อ -ตาม -ตั้งแต่ -ตั้ง -ด้าน -ด้วย -ดัง -ซึ่ง -ช่วง -จึง -จาก -จัด -จะ -คือ -ความ -ครั้ง -คง -ขึ้น -ของ -ขอ -ขณะ -ก่อน -ก็ -การ -กับ -กัน -กว่า -กล่าว diff --git a/resources/solr/default_configset/lang/stopwords_tr.txt b/resources/solr/default_configset/lang/stopwords_tr.txt deleted file mode 100644 index 84d9408d4e..0000000000 --- a/resources/solr/default_configset/lang/stopwords_tr.txt +++ /dev/null @@ -1,212 +0,0 @@ -# Turkish stopwords from LUCENE-559 -# merged with the list from "Information Retrieval on Turkish Texts" -# (http://www.users.muohio.edu/canf/papers/JASIST2008offPrint.pdf) -acaba -altmış -altı -ama -ancak -arada -aslında -ayrıca -bana -bazı -belki -ben -benden -beni -benim -beri -beş -bile -bin -bir -birçok -biri -birkaç -birkez -birşey -birşeyi -biz -bize -bizden -bizi -bizim -böyle -böylece -bu -buna -bunda -bundan -bunlar -bunları -bunların -bunu -bunun -burada -çok -çünkü -da -daha -dahi -de -defa -değil -diğer -diye -doksan -dokuz -dolayı -dolayısıyla -dört -edecek -eden -ederek -edilecek -ediliyor -edilmesi -ediyor -eğer -elli -en -etmesi -etti -ettiği -ettiğini -gibi -göre -halen -hangi -hatta -hem -henüz -hep -hepsi -her -herhangi -herkesin -hiç -hiçbir -için -iki -ile -ilgili -ise -işte -itibaren -itibariyle -kadar -karşın -katrilyon -kendi -kendilerine -kendini -kendisi -kendisine -kendisini -kez -ki -kim -kimden -kime -kimi -kimse -kırk -milyar -milyon -mu -mü -mı -nasıl -ne -neden -nedenle -nerde -nerede -nereye -niye -niçin -o -olan -olarak -oldu -olduğu -olduğunu -olduklarını -olmadı -olmadığı -olmak -olması -olmayan -olmaz -olsa -olsun -olup -olur -olursa -oluyor -on -ona -ondan -onlar -onlardan -onları -onların -onu -onun -otuz -oysa -öyle -pek -rağmen -sadece -sanki -sekiz -seksen -sen -senden -seni -senin -siz -sizden -sizi -sizin -şey -şeyden -şeyi -şeyler -şöyle -şu -şuna -şunda -şundan -şunları -şunu -tarafından -trilyon -tüm -üç -üzere -var -vardı -ve -veya -ya -yani -yapacak -yapılan -yapılması -yapıyor -yapmak -yaptı -yaptığı -yaptığını -yaptıkları -yedi -yerine -yetmiş -yine -yirmi -yoksa -yüz -zaten diff --git a/resources/solr/default_configset/lang/userdict_ja.txt b/resources/solr/default_configset/lang/userdict_ja.txt deleted file mode 100644 index 6f0368e4d8..0000000000 --- a/resources/solr/default_configset/lang/userdict_ja.txt +++ /dev/null @@ -1,29 +0,0 @@ -# -# This is a sample user dictionary for Kuromoji (JapaneseTokenizer) -# -# Add entries to this file in order to override the statistical model in terms -# of segmentation, readings and part-of-speech tags. Notice that entries do -# not have weights since they are always used when found. This is by-design -# in order to maximize ease-of-use. -# -# Entries are defined using the following CSV format: -# , ... , ... , -# -# Notice that a single half-width space separates tokens and readings, and -# that the number tokens and readings must match exactly. -# -# Also notice that multiple entries with the same is undefined. -# -# Whitespace only lines are ignored. Comments are not allowed on entry lines. -# - -# Custom segmentation for kanji compounds -日本経済新聞,日本 経済 新聞,ニホン ケイザイ シンブン,カスタム名詞 -関西国際空港,関西 国際 空港,カンサイ コクサイ クウコウ,カスタム名詞 - -# Custom segmentation for compound katakana -トートバッグ,トート バッグ,トート バッグ,かずカナ名詞 -ショルダーバッグ,ショルダー バッグ,ショルダー バッグ,かずカナ名詞 - -# Custom reading for former sumo wrestler -朝青龍,朝青龍,アサショウリュウ,カスタム人名 diff --git a/resources/solr/default_configset/managed-schema.xml b/resources/solr/default_configset/managed-schema.xml deleted file mode 100644 index 0b953e7061..0000000000 --- a/resources/solr/default_configset/managed-schema.xml +++ /dev/null @@ -1,1075 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - id - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/solr/default_configset/protwords.txt b/resources/solr/default_configset/protwords.txt deleted file mode 100644 index 1dfc0abecb..0000000000 --- a/resources/solr/default_configset/protwords.txt +++ /dev/null @@ -1,21 +0,0 @@ -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -#----------------------------------------------------------------------- -# Use a protected word file to protect against the stemmer reducing two -# unrelated words to the same base word. - -# Some non-words that normally won't be encountered, -# just to test that they won't be stemmed. -dontstems -zwhacky - diff --git a/resources/solr/default_configset/solrconfig.xml b/resources/solr/default_configset/solrconfig.xml deleted file mode 100644 index 9bef924011..0000000000 --- a/resources/solr/default_configset/solrconfig.xml +++ /dev/null @@ -1,1026 +0,0 @@ - - - - - - - - - 9.12 - - - ${solr.data.dir:} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ${solr.lock.type:native} - - - - - - - - - - - - - - - - - - - - - ${solr.ulog.dir:} - - - - - ${solr.autoCommit.maxTime:15000} - false - - - - - - ${solr.autoSoftCommit.maxTime:3000} - - - - - - - - - - - - - - ${solr.max.booleanClauses:1024} - - - ${solr.query.minPrefixLength:-1} - - - - - - - - - - - - - - - - - - - - - - - - true - - - - - - 20 - - - 200 - - - - - - - - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - explicit - 10 - - - - - - - explicit - json - true - - - - - - - _text_ - - - - - - - text_general - - - - - - default - _text_ - solr.DirectSolrSpellChecker - - internal - - 0.5 - - 2 - - 1 - - 5 - - 4 - - 0.01 - - - - - - - - - - - - default - on - true - 10 - 5 - 5 - true - true - 10 - 5 - - - spellcheck - - - - - - - - - - - - 100 - - - - - - - - 70 - - 0.5 - - [-\w ,/\n\"']{20,200} - - - - - - - ]]> - ]]> - - - - - - - - - - - - - - - - - - - - - - - - ,, - ,, - ,, - ,, - ,]]> - ]]> - - - - - - 10 - .,!? - - - - - - - WORD - - - en - US - - - - - - - - - - - - [^\w-\.] - _ - - - 1000 - true - - - - - - - yyyy-MM-dd['T'[HH:mm[:ss[.SSS]][z - yyyy-MM-dd['T'[HH:mm[:ss[,SSS]][z - yyyy-MM-dd HH:mm[:ss[.SSS]][z - yyyy-MM-dd HH:mm[:ss[,SSS]][z - [EEE, ]dd MMM yyyy HH:mm[:ss] z - EEEE, dd-MMM-yy HH:mm:ss z - EEE MMM ppd HH:mm:ss [z ]yyyy - - - - - java.lang.String - text_general - - *_str - 256 - - - true - - - java.lang.Boolean - booleans - - - java.util.Date - pdates - - - java.lang.Long - java.lang.Integer - plongs - - - java.lang.Number - pdoubles - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/solr/default_configset/stopwords.txt b/resources/solr/default_configset/stopwords.txt deleted file mode 100644 index ae1e83eeb3..0000000000 --- a/resources/solr/default_configset/stopwords.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/resources/solr/default_configset/synonyms.txt b/resources/solr/default_configset/synonyms.txt deleted file mode 100644 index eab4ee8753..0000000000 --- a/resources/solr/default_configset/synonyms.txt +++ /dev/null @@ -1,29 +0,0 @@ -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -#----------------------------------------------------------------------- -#some test synonym mappings unlikely to appear in real input text -aaafoo => aaabar -bbbfoo => bbbfoo bbbbar -cccfoo => cccbar cccbaz -fooaaa,baraaa,bazaaa - -# Some synonym groups specific to this example -GB,gib,gigabyte,gigabytes -MB,mib,megabyte,megabytes -Television, Televisions, TV, TVs -#notice we use "gib" instead of "GiB" so any WordDelimiterGraphFilter coming -#after us won't split it into two words. - -# Synonym mappings can be used for spelling correction too -pixima => pixma - diff --git a/resources/solr/lang/contractions_ca.txt b/resources/solr/lang/contractions_ca.txt deleted file mode 100644 index 307a85f913..0000000000 --- a/resources/solr/lang/contractions_ca.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Set of Catalan contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -d -l -m -n -s -t diff --git a/resources/solr/lang/contractions_fr.txt b/resources/solr/lang/contractions_fr.txt deleted file mode 100644 index f1bba51b23..0000000000 --- a/resources/solr/lang/contractions_fr.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Set of French contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -l -m -t -qu -n -s -j -d -c -jusqu -quoiqu -lorsqu -puisqu diff --git a/resources/solr/lang/contractions_ga.txt b/resources/solr/lang/contractions_ga.txt deleted file mode 100644 index 9ebe7fa349..0000000000 --- a/resources/solr/lang/contractions_ga.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Set of Irish contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -d -m -b diff --git a/resources/solr/lang/contractions_it.txt b/resources/solr/lang/contractions_it.txt deleted file mode 100644 index cac0409537..0000000000 --- a/resources/solr/lang/contractions_it.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Set of Italian contractions for ElisionFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -c -l -all -dall -dell -nell -sull -coll -pell -gl -agl -dagl -degl -negl -sugl -un -m -t -s -v -d diff --git a/resources/solr/lang/hyphenations_ga.txt b/resources/solr/lang/hyphenations_ga.txt deleted file mode 100644 index 4d2642cc5a..0000000000 --- a/resources/solr/lang/hyphenations_ga.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Set of Irish hyphenations for StopFilter -# TODO: load this as a resource from the analyzer and sync it in build.xml -h -n -t diff --git a/resources/solr/lang/stemdict_nl.txt b/resources/solr/lang/stemdict_nl.txt deleted file mode 100644 index 441072971d..0000000000 --- a/resources/solr/lang/stemdict_nl.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Set of overrides for the dutch stemmer -# TODO: load this as a resource from the analyzer and sync it in build.xml -fiets fiets -bromfiets bromfiets -ei eier -kind kinder diff --git a/resources/solr/lang/stoptags_ja.txt b/resources/solr/lang/stoptags_ja.txt deleted file mode 100644 index 71b750845e..0000000000 --- a/resources/solr/lang/stoptags_ja.txt +++ /dev/null @@ -1,420 +0,0 @@ -# -# This file defines a Japanese stoptag set for JapanesePartOfSpeechStopFilter. -# -# Any token with a part-of-speech tag that exactly matches those defined in this -# file are removed from the token stream. -# -# Set your own stoptags by uncommenting the lines below. Note that comments are -# not allowed on the same line as a stoptag. See LUCENE-3745 for frequency lists, -# etc. that can be useful for building you own stoptag set. -# -# The entire possible tagset is provided below for convenience. -# -##### -# noun: unclassified nouns -#名詞 -# -# noun-common: Common nouns or nouns where the sub-classification is undefined -#名詞-一般 -# -# noun-proper: Proper nouns where the sub-classification is undefined -#名詞-固有名詞 -# -# noun-proper-misc: miscellaneous proper nouns -#名詞-固有名詞-一般 -# -# noun-proper-person: Personal names where the sub-classification is undefined -#名詞-固有名詞-人名 -# -# noun-proper-person-misc: names that cannot be divided into surname and -# given name; foreign names; names where the surname or given name is unknown. -# e.g. お市の方 -#名詞-固有名詞-人名-一般 -# -# noun-proper-person-surname: Mainly Japanese surnames. -# e.g. 山田 -#名詞-固有名詞-人名-姓 -# -# noun-proper-person-given_name: Mainly Japanese given names. -# e.g. 太郎 -#名詞-固有名詞-人名-名 -# -# noun-proper-organization: Names representing organizations. -# e.g. 通産省, NHK -#名詞-固有名詞-組織 -# -# noun-proper-place: Place names where the sub-classification is undefined -#名詞-固有名詞-地域 -# -# noun-proper-place-misc: Place names excluding countries. -# e.g. アジア, バルセロナ, 京都 -#名詞-固有名詞-地域-一般 -# -# noun-proper-place-country: Country names. -# e.g. 日本, オーストラリア -#名詞-固有名詞-地域-国 -# -# noun-pronoun: Pronouns where the sub-classification is undefined -#名詞-代名詞 -# -# noun-pronoun-misc: miscellaneous pronouns: -# e.g. それ, ここ, あいつ, あなた, あちこち, いくつ, どこか, なに, みなさん, みんな, わたくし, われわれ -#名詞-代名詞-一般 -# -# noun-pronoun-contraction: Spoken language contraction made by combining a -# pronoun and the particle 'wa'. -# e.g. ありゃ, こりゃ, こりゃあ, そりゃ, そりゃあ -#名詞-代名詞-縮約 -# -# noun-adverbial: Temporal nouns such as names of days or months that behave -# like adverbs. Nouns that represent amount or ratios and can be used adverbially, -# e.g. 金曜, 一月, 午後, 少量 -#名詞-副詞可能 -# -# noun-verbal: Nouns that take arguments with case and can appear followed by -# 'suru' and related verbs (する, できる, なさる, くださる) -# e.g. インプット, 愛着, 悪化, 悪戦苦闘, 一安心, 下取り -#名詞-サ変接続 -# -# noun-adjective-base: The base form of adjectives, words that appear before な ("na") -# e.g. 健康, 安易, 駄目, だめ -#名詞-形容動詞語幹 -# -# noun-numeric: Arabic numbers, Chinese numerals, and counters like 何 (回), 数. -# e.g. 0, 1, 2, 何, 数, 幾 -#名詞-数 -# -# noun-affix: noun affixes where the sub-classification is undefined -#名詞-非自立 -# -# noun-affix-misc: Of adnominalizers, the case-marker の ("no"), and words that -# attach to the base form of inflectional words, words that cannot be classified -# into any of the other categories below. This category includes indefinite nouns. -# e.g. あかつき, 暁, かい, 甲斐, 気, きらい, 嫌い, くせ, 癖, こと, 事, ごと, 毎, しだい, 次第, -# 順, せい, 所為, ついで, 序で, つもり, 積もり, 点, どころ, の, はず, 筈, はずみ, 弾み, -# 拍子, ふう, ふり, 振り, ほう, 方, 旨, もの, 物, 者, ゆえ, 故, ゆえん, 所以, わけ, 訳, -# わり, 割り, 割, ん-口語/, もん-口語/ -#名詞-非自立-一般 -# -# noun-affix-adverbial: noun affixes that that can behave as adverbs. -# e.g. あいだ, 間, あげく, 挙げ句, あと, 後, 余り, 以外, 以降, 以後, 以上, 以前, 一方, うえ, -# 上, うち, 内, おり, 折り, かぎり, 限り, きり, っきり, 結果, ころ, 頃, さい, 際, 最中, さなか, -# 最中, じたい, 自体, たび, 度, ため, 為, つど, 都度, とおり, 通り, とき, 時, ところ, 所, -# とたん, 途端, なか, 中, のち, 後, ばあい, 場合, 日, ぶん, 分, ほか, 他, まえ, 前, まま, -# 儘, 侭, みぎり, 矢先 -#名詞-非自立-副詞可能 -# -# noun-affix-aux: noun affixes treated as 助動詞 ("auxiliary verb") in school grammars -# with the stem よう(だ) ("you(da)"). -# e.g. よう, やう, 様 (よう) -#名詞-非自立-助動詞語幹 -# -# noun-affix-adjective-base: noun affixes that can connect to the indeclinable -# connection form な (aux "da"). -# e.g. みたい, ふう -#名詞-非自立-形容動詞語幹 -# -# noun-special: special nouns where the sub-classification is undefined. -#名詞-特殊 -# -# noun-special-aux: The そうだ ("souda") stem form that is used for reporting news, is -# treated as 助動詞 ("auxiliary verb") in school grammars, and attach to the base -# form of inflectional words. -# e.g. そう -#名詞-特殊-助動詞語幹 -# -# noun-suffix: noun suffixes where the sub-classification is undefined. -#名詞-接尾 -# -# noun-suffix-misc: Of the nouns or stem forms of other parts of speech that connect -# to ガル or タイ and can combine into compound nouns, words that cannot be classified into -# any of the other categories below. In general, this category is more inclusive than -# 接尾語 ("suffix") and is usually the last element in a compound noun. -# e.g. おき, かた, 方, 甲斐 (がい), がかり, ぎみ, 気味, ぐるみ, (~した) さ, 次第, 済 (ず) み, -# よう, (でき)っこ, 感, 観, 性, 学, 類, 面, 用 -#名詞-接尾-一般 -# -# noun-suffix-person: Suffixes that form nouns and attach to person names more often -# than other nouns. -# e.g. 君, 様, 著 -#名詞-接尾-人名 -# -# noun-suffix-place: Suffixes that form nouns and attach to place names more often -# than other nouns. -# e.g. 町, 市, 県 -#名詞-接尾-地域 -# -# noun-suffix-verbal: Of the suffixes that attach to nouns and form nouns, those that -# can appear before スル ("suru"). -# e.g. 化, 視, 分け, 入り, 落ち, 買い -#名詞-接尾-サ変接続 -# -# noun-suffix-aux: The stem form of そうだ (様態) that is used to indicate conditions, -# is treated as 助動詞 ("auxiliary verb") in school grammars, and attach to the -# conjunctive form of inflectional words. -# e.g. そう -#名詞-接尾-助動詞語幹 -# -# noun-suffix-adjective-base: Suffixes that attach to other nouns or the conjunctive -# form of inflectional words and appear before the copula だ ("da"). -# e.g. 的, げ, がち -#名詞-接尾-形容動詞語幹 -# -# noun-suffix-adverbial: Suffixes that attach to other nouns and can behave as adverbs. -# e.g. 後 (ご), 以後, 以降, 以前, 前後, 中, 末, 上, 時 (じ) -#名詞-接尾-副詞可能 -# -# noun-suffix-classifier: Suffixes that attach to numbers and form nouns. This category -# is more inclusive than 助数詞 ("classifier") and includes common nouns that attach -# to numbers. -# e.g. 個, つ, 本, 冊, パーセント, cm, kg, カ月, か国, 区画, 時間, 時半 -#名詞-接尾-助数詞 -# -# noun-suffix-special: Special suffixes that mainly attach to inflecting words. -# e.g. (楽し) さ, (考え) 方 -#名詞-接尾-特殊 -# -# noun-suffix-conjunctive: Nouns that behave like conjunctions and join two words -# together. -# e.g. (日本) 対 (アメリカ), 対 (アメリカ), (3) 対 (5), (女優) 兼 (主婦) -#名詞-接続詞的 -# -# noun-verbal_aux: Nouns that attach to the conjunctive particle て ("te") and are -# semantically verb-like. -# e.g. ごらん, ご覧, 御覧, 頂戴 -#名詞-動詞非自立的 -# -# noun-quotation: text that cannot be segmented into words, proverbs, Chinese poetry, -# dialects, English, etc. Currently, the only entry for 名詞 引用文字列 ("noun quotation") -# is いわく ("iwaku"). -#名詞-引用文字列 -# -# noun-nai_adjective: Words that appear before the auxiliary verb ない ("nai") and -# behave like an adjective. -# e.g. 申し訳, 仕方, とんでも, 違い -#名詞-ナイ形容詞語幹 -# -##### -# prefix: unclassified prefixes -#接頭詞 -# -# prefix-nominal: Prefixes that attach to nouns (including adjective stem forms) -# excluding numerical expressions. -# e.g. お (水), 某 (氏), 同 (社), 故 (~氏), 高 (品質), お (見事), ご (立派) -#接頭詞-名詞接続 -# -# prefix-verbal: Prefixes that attach to the imperative form of a verb or a verb -# in conjunctive form followed by なる/なさる/くださる. -# e.g. お (読みなさい), お (座り) -#接頭詞-動詞接続 -# -# prefix-adjectival: Prefixes that attach to adjectives. -# e.g. お (寒いですねえ), バカ (でかい) -#接頭詞-形容詞接続 -# -# prefix-numerical: Prefixes that attach to numerical expressions. -# e.g. 約, およそ, 毎時 -#接頭詞-数接続 -# -##### -# verb: unclassified verbs -#動詞 -# -# verb-main: -#動詞-自立 -# -# verb-auxiliary: -#動詞-非自立 -# -# verb-suffix: -#動詞-接尾 -# -##### -# adjective: unclassified adjectives -#形容詞 -# -# adjective-main: -#形容詞-自立 -# -# adjective-auxiliary: -#形容詞-非自立 -# -# adjective-suffix: -#形容詞-接尾 -# -##### -# adverb: unclassified adverbs -#副詞 -# -# adverb-misc: Words that can be segmented into one unit and where adnominal -# modification is not possible. -# e.g. あいかわらず, 多分 -#副詞-一般 -# -# adverb-particle_conjunction: Adverbs that can be followed by の, は, に, -# な, する, だ, etc. -# e.g. こんなに, そんなに, あんなに, なにか, なんでも -#副詞-助詞類接続 -# -##### -# adnominal: Words that only have noun-modifying forms. -# e.g. この, その, あの, どの, いわゆる, なんらかの, 何らかの, いろんな, こういう, そういう, ああいう, -# どういう, こんな, そんな, あんな, どんな, 大きな, 小さな, おかしな, ほんの, たいした, -# 「(, も) さる (ことながら)」, 微々たる, 堂々たる, 単なる, いかなる, 我が」「同じ, 亡き -#連体詞 -# -##### -# conjunction: Conjunctions that can occur independently. -# e.g. が, けれども, そして, じゃあ, それどころか -接続詞 -# -##### -# particle: unclassified particles. -助詞 -# -# particle-case: case particles where the subclassification is undefined. -助詞-格助詞 -# -# particle-case-misc: Case particles. -# e.g. から, が, で, と, に, へ, より, を, の, にて -助詞-格助詞-一般 -# -# particle-case-quote: the "to" that appears after nouns, a person’s speech, -# quotation marks, expressions of decisions from a meeting, reasons, judgements, -# conjectures, etc. -# e.g. ( だ) と (述べた.), ( である) と (して執行猶予...) -助詞-格助詞-引用 -# -# particle-case-compound: Compounds of particles and verbs that mainly behave -# like case particles. -# e.g. という, といった, とかいう, として, とともに, と共に, でもって, にあたって, に当たって, に当って, -# にあたり, に当たり, に当り, に当たる, にあたる, において, に於いて,に於て, における, に於ける, -# にかけ, にかけて, にかんし, に関し, にかんして, に関して, にかんする, に関する, に際し, -# に際して, にしたがい, に従い, に従う, にしたがって, に従って, にたいし, に対し, にたいして, -# に対して, にたいする, に対する, について, につき, につけ, につけて, につれ, につれて, にとって, -# にとり, にまつわる, によって, に依って, に因って, により, に依り, に因り, による, に依る, に因る, -# にわたって, にわたる, をもって, を以って, を通じ, を通じて, を通して, をめぐって, をめぐり, をめぐる, -# って-口語/, ちゅう-関西弁「という」/, (何) ていう (人)-口語/, っていう-口語/, といふ, とかいふ -助詞-格助詞-連語 -# -# particle-conjunctive: -# e.g. から, からには, が, けれど, けれども, けど, し, つつ, て, で, と, ところが, どころか, とも, ども, -# ながら, なり, ので, のに, ば, ものの, や ( した), やいなや, (ころん) じゃ(いけない)-口語/, -# (行っ) ちゃ(いけない)-口語/, (言っ) たって (しかたがない)-口語/, (それがなく)ったって (平気)-口語/ -助詞-接続助詞 -# -# particle-dependency: -# e.g. こそ, さえ, しか, すら, は, も, ぞ -助詞-係助詞 -# -# particle-adverbial: -# e.g. がてら, かも, くらい, 位, ぐらい, しも, (学校) じゃ(これが流行っている)-口語/, -# (それ)じゃあ (よくない)-口語/, ずつ, (私) なぞ, など, (私) なり (に), (先生) なんか (大嫌い)-口語/, -# (私) なんぞ, (先生) なんて (大嫌い)-口語/, のみ, だけ, (私) だって-口語/, だに, -# (彼)ったら-口語/, (お茶) でも (いかが), 等 (とう), (今後) とも, ばかり, ばっか-口語/, ばっかり-口語/, -# ほど, 程, まで, 迄, (誰) も (が)([助詞-格助詞] および [助詞-係助詞] の前に位置する「も」) -助詞-副助詞 -# -# particle-interjective: particles with interjective grammatical roles. -# e.g. (松島) や -助詞-間投助詞 -# -# particle-coordinate: -# e.g. と, たり, だの, だり, とか, なり, や, やら -助詞-並立助詞 -# -# particle-final: -# e.g. かい, かしら, さ, ぜ, (だ)っけ-口語/, (とまってる) で-方言/, な, ナ, なあ-口語/, ぞ, ね, ネ, -# ねぇ-口語/, ねえ-口語/, ねん-方言/, の, のう-口語/, や, よ, ヨ, よぉ-口語/, わ, わい-口語/ -助詞-終助詞 -# -# particle-adverbial/conjunctive/final: The particle "ka" when unknown whether it is -# adverbial, conjunctive, or sentence final. For example: -# (a) 「A か B か」. Ex:「(国内で運用する) か,(海外で運用する) か (.)」 -# (b) Inside an adverb phrase. Ex:「(幸いという) か (, 死者はいなかった.)」 -# 「(祈りが届いたせい) か (, 試験に合格した.)」 -# (c) 「かのように」. Ex:「(何もなかった) か (のように振る舞った.)」 -# e.g. か -助詞-副助詞/並立助詞/終助詞 -# -# particle-adnominalizer: The "no" that attaches to nouns and modifies -# non-inflectional words. -助詞-連体化 -# -# particle-adnominalizer: The "ni" and "to" that appear following nouns and adverbs -# that are giongo, giseigo, or gitaigo. -# e.g. に, と -助詞-副詞化 -# -# particle-special: A particle that does not fit into one of the above classifications. -# This includes particles that are used in Tanka, Haiku, and other poetry. -# e.g. かな, けむ, ( しただろう) に, (あんた) にゃ(わからん), (俺) ん (家) -助詞-特殊 -# -##### -# auxiliary-verb: -助動詞 -# -##### -# interjection: Greetings and other exclamations. -# e.g. おはよう, おはようございます, こんにちは, こんばんは, ありがとう, どうもありがとう, ありがとうございます, -# いただきます, ごちそうさま, さよなら, さようなら, はい, いいえ, ごめん, ごめんなさい -#感動詞 -# -##### -# symbol: unclassified Symbols. -記号 -# -# symbol-misc: A general symbol not in one of the categories below. -# e.g. [○◎@$〒→+] -記号-一般 -# -# symbol-comma: Commas -# e.g. [,、] -記号-読点 -# -# symbol-period: Periods and full stops. -# e.g. [..。] -記号-句点 -# -# symbol-space: Full-width whitespace. -記号-空白 -# -# symbol-open_bracket: -# e.g. [({‘“『【] -記号-括弧開 -# -# symbol-close_bracket: -# e.g. [)}’”』」】] -記号-括弧閉 -# -# symbol-alphabetic: -#記号-アルファベット -# -##### -# other: unclassified other -#その他 -# -# other-interjection: Words that are hard to classify as noun-suffixes or -# sentence-final particles. -# e.g. (だ)ァ -その他-間投 -# -##### -# filler: Aizuchi that occurs during a conversation or sounds inserted as filler. -# e.g. あの, うんと, えと -フィラー -# -##### -# non-verbal: non-verbal sound. -非言語音 -# -##### -# fragment: -#語断片 -# -##### -# unknown: unknown part of speech. -#未知語 -# -##### End of file diff --git a/resources/solr/lang/stopwords_ar.txt b/resources/solr/lang/stopwords_ar.txt deleted file mode 100644 index 046829db6a..0000000000 --- a/resources/solr/lang/stopwords_ar.txt +++ /dev/null @@ -1,125 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -# Cleaned on October 11, 2009 (not normalized, so use before normalization) -# This means that when modifying this list, you might need to add some -# redundant entries, for example containing forms with both أ and ا -من -ومن -منها -منه -في -وفي -فيها -فيه -و -ف -ثم -او -أو -ب -بها -به -ا -أ -اى -اي -أي -أى -لا -ولا -الا -ألا -إلا -لكن -ما -وما -كما -فما -عن -مع -اذا -إذا -ان -أن -إن -انها -أنها -إنها -انه -أنه -إنه -بان -بأن -فان -فأن -وان -وأن -وإن -التى -التي -الذى -الذي -الذين -الى -الي -إلى -إلي -على -عليها -عليه -اما -أما -إما -ايضا -أيضا -كل -وكل -لم -ولم -لن -ولن -هى -هي -هو -وهى -وهي -وهو -فهى -فهي -فهو -انت -أنت -لك -لها -له -هذه -هذا -تلك -ذلك -هناك -كانت -كان -يكون -تكون -وكانت -وكان -غير -بعض -قد -نحو -بين -بينما -منذ -ضمن -حيث -الان -الآن -خلال -بعد -قبل -حتى -عند -عندما -لدى -جميع diff --git a/resources/solr/lang/stopwords_bg.txt b/resources/solr/lang/stopwords_bg.txt deleted file mode 100644 index 1ae4ba2ae3..0000000000 --- a/resources/solr/lang/stopwords_bg.txt +++ /dev/null @@ -1,193 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -а -аз -ако -ала -бе -без -беше -би -бил -била -били -било -близо -бъдат -бъде -бяха -в -вас -ваш -ваша -вероятно -вече -взема -ви -вие -винаги -все -всеки -всички -всичко -всяка -във -въпреки -върху -г -ги -главно -го -д -да -дали -до -докато -докога -дори -досега -доста -е -едва -един -ето -за -зад -заедно -заради -засега -затова -защо -защото -и -из -или -им -има -имат -иска -й -каза -как -каква -какво -както -какъв -като -кога -когато -което -които -кой -който -колко -която -къде -където -към -ли -м -ме -между -мен -ми -мнозина -мога -могат -може -моля -момента -му -н -на -над -назад -най -направи -напред -например -нас -не -него -нея -ни -ние -никой -нито -но -някои -някой -няма -обаче -около -освен -особено -от -отгоре -отново -още -пак -по -повече -повечето -под -поне -поради -после -почти -прави -пред -преди -през -при -пък -първо -с -са -само -се -сега -си -скоро -след -сме -според -сред -срещу -сте -съм -със -също -т -тази -така -такива -такъв -там -твой -те -тези -ти -тн -то -това -тогава -този -той -толкова -точно -трябва -тук -тъй -тя -тях -у -харесва -ч -че -често -чрез -ще -щом -я diff --git a/resources/solr/lang/stopwords_ca.txt b/resources/solr/lang/stopwords_ca.txt deleted file mode 100644 index 3da65deafe..0000000000 --- a/resources/solr/lang/stopwords_ca.txt +++ /dev/null @@ -1,220 +0,0 @@ -# Catalan stopwords from http://github.com/vcl/cue.language (Apache 2 Licensed) -a -abans -ací -ah -així -això -al -als -aleshores -algun -alguna -algunes -alguns -alhora -allà -allí -allò -altra -altre -altres -amb -ambdós -ambdues -apa -aquell -aquella -aquelles -aquells -aquest -aquesta -aquestes -aquests -aquí -baix -cada -cadascú -cadascuna -cadascunes -cadascuns -com -contra -d'un -d'una -d'unes -d'uns -dalt -de -del -dels -des -després -dins -dintre -donat -doncs -durant -e -eh -el -els -em -en -encara -ens -entre -érem -eren -éreu -es -és -esta -està -estàvem -estaven -estàveu -esteu -et -etc -ets -fins -fora -gairebé -ha -han -has -havia -he -hem -heu -hi -ho -i -igual -iguals -ja -l'hi -la -les -li -li'n -llavors -m'he -ma -mal -malgrat -mateix -mateixa -mateixes -mateixos -me -mentre -més -meu -meus -meva -meves -molt -molta -moltes -molts -mon -mons -n'he -n'hi -ne -ni -no -nogensmenys -només -nosaltres -nostra -nostre -nostres -o -oh -oi -on -pas -pel -pels -per -però -perquè -poc -poca -pocs -poques -potser -propi -qual -quals -quan -quant -que -què -quelcom -qui -quin -quina -quines -quins -s'ha -s'han -sa -semblant -semblants -ses -seu -seus -seva -seva -seves -si -sobre -sobretot -sóc -solament -sols -son -són -sons -sota -sou -t'ha -t'han -t'he -ta -tal -també -tampoc -tan -tant -tanta -tantes -teu -teus -teva -teves -ton -tons -tot -tota -totes -tots -un -una -unes -uns -us -va -vaig -vam -van -vas -veu -vosaltres -vostra -vostre -vostres diff --git a/resources/solr/lang/stopwords_cz.txt b/resources/solr/lang/stopwords_cz.txt deleted file mode 100644 index 53c6097dac..0000000000 --- a/resources/solr/lang/stopwords_cz.txt +++ /dev/null @@ -1,172 +0,0 @@ -a -s -k -o -i -u -v -z -dnes -cz -tímto -budeš -budem -byli -jseš -můj -svým -ta -tomto -tohle -tuto -tyto -jej -zda -proč -máte -tato -kam -tohoto -kdo -kteří -mi -nám -tom -tomuto -mít -nic -proto -kterou -byla -toho -protože -asi -ho -naši -napište -re -což -tím -takže -svých -její -svými -jste -aj -tu -tedy -teto -bylo -kde -ke -pravé -ji -nad -nejsou -či -pod -téma -mezi -přes -ty -pak -vám -ani -když -však -neg -jsem -tento -článku -články -aby -jsme -před -pta -jejich -byl -ještě -až -bez -také -pouze -první -vaše -která -nás -nový -tipy -pokud -může -strana -jeho -své -jiné -zprávy -nové -není -vás -jen -podle -zde -už -být -více -bude -již -než -který -by -které -co -nebo -ten -tak -má -při -od -po -jsou -jak -další -ale -si -se -ve -to -jako -za -zpět -ze -do -pro -je -na -atd -atp -jakmile -přičemž -já -on -ona -ono -oni -ony -my -vy -jí -ji -mě -mne -jemu -tomu -těm -těmu -němu -němuž -jehož -jíž -jelikož -jež -jakož -načež diff --git a/resources/solr/lang/stopwords_da.txt b/resources/solr/lang/stopwords_da.txt deleted file mode 100644 index 6e90e8f1aa..0000000000 --- a/resources/solr/lang/stopwords_da.txt +++ /dev/null @@ -1,110 +0,0 @@ - | From https://snowballstem.org/algorithms/danish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Danish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - -og | and -i | in -jeg | I -det | that (dem. pronoun)/it (pers. pronoun) -at | that (in front of a sentence)/to (with infinitive) -en | a/an -den | it (pers. pronoun)/that (dem. pronoun) -til | to/at/for/until/against/by/of/into, more -er | present tense of "to be" -som | who, as -på | on/upon/in/on/at/to/after/of/with/for, on -de | they -med | with/by/in, along -han | he -af | of/by/from/off/for/in/with/on, off -for | at/for/to/from/by/of/ago, in front/before, because -ikke | not -der | who/which, there/those -var | past tense of "to be" -mig | me/myself -sig | oneself/himself/herself/itself/themselves -men | but -et | a/an/one, one (number), someone/somebody/one -har | present tense of "to have" -om | round/about/for/in/a, about/around/down, if -vi | we -min | my -havde | past tense of "to have" -ham | him -hun | she -nu | now -over | over/above/across/by/beyond/past/on/about, over/past -da | then, when/as/since -fra | from/off/since, off, since -du | you -ud | out -sin | his/her/its/one's -dem | them -os | us/ourselves -op | up -man | you/one -hans | his -hvor | where -eller | or -hvad | what -skal | must/shall etc. -selv | myself/yourself/herself/ourselves etc., even -her | here -alle | all/everyone/everybody etc. -vil | will (verb) -blev | past tense of "to stay/to remain/to get/to become" -kunne | could -ind | in -når | when -være | present tense of "to be" -dog | however/yet/after all -noget | something -ville | would -jo | you know/you see (adv), yes -deres | their/theirs -efter | after/behind/according to/for/by/from, later/afterwards -ned | down -skulle | should -denne | this -end | than -dette | this -mit | my/mine -også | also -under | under/beneath/below/during, below/underneath -have | have -dig | you -anden | other -hende | her -mine | my -alt | everything -meget | much/very, plenty of -sit | his, her, its, one's -sine | his, her, its, one's -vor | our -mod | against -disse | these -hvis | if -din | your/yours -nogle | some -hos | by/at -blive | be/become -mange | many -ad | by/through -bliver | present tense of "to be/to become" -hendes | her/hers -været | be -thi | for (conj) -jer | you -sådan | such, like this/like that diff --git a/resources/solr/lang/stopwords_de.txt b/resources/solr/lang/stopwords_de.txt deleted file mode 100644 index 804bbbdb01..0000000000 --- a/resources/solr/lang/stopwords_de.txt +++ /dev/null @@ -1,294 +0,0 @@ - | From https://snowballstem.org/algorithms/german/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A German stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | The number of forms in this list is reduced significantly by passing it - | through the German stemmer. - - -aber | but - -alle | all -allem -allen -aller -alles - -als | than, as -also | so -am | an + dem -an | at - -ander | other -andere -anderem -anderen -anderer -anderes -anderm -andern -anderr -anders - -auch | also -auf | on -aus | out of -bei | by -bin | am -bis | until -bist | art -da | there -damit | with it -dann | then - -der | the -den -des -dem -die -das - -daß | that - -derselbe | the same -derselben -denselben -desselben -demselben -dieselbe -dieselben -dasselbe - -dazu | to that - -dein | thy -deine -deinem -deinen -deiner -deines - -denn | because - -derer | of those -dessen | of him - -dich | thee -dir | to thee -du | thou - -dies | this -diese -diesem -diesen -dieser -dieses - - -doch | (several meanings) -dort | (over) there - - -durch | through - -ein | a -eine -einem -einen -einer -eines - -einig | some -einige -einigem -einigen -einiger -einiges - -einmal | once - -er | he -ihn | him -ihm | to him - -es | it -etwas | something - -euer | your -eure -eurem -euren -eurer -eures - -für | for -gegen | towards -gewesen | p.p. of sein -hab | have -habe | have -haben | have -hat | has -hatte | had -hatten | had -hier | here -hin | there -hinter | behind - -ich | I -mich | me -mir | to me - - -ihr | you, to her -ihre -ihrem -ihren -ihrer -ihres -euch | to you - -im | in + dem -in | in -indem | while -ins | in + das -ist | is - -jede | each, every -jedem -jeden -jeder -jedes - -jene | that -jenem -jenen -jener -jenes - -jetzt | now -kann | can - -kein | no -keine -keinem -keinen -keiner -keines - -können | can -könnte | could -machen | do -man | one - -manche | some, many a -manchem -manchen -mancher -manches - -mein | my -meine -meinem -meinen -meiner -meines - -mit | with -muss | must -musste | had to -nach | to(wards) -nicht | not -nichts | nothing -noch | still, yet -nun | now -nur | only -ob | whether -oder | or -ohne | without -sehr | very - -sein | his -seine -seinem -seinen -seiner -seines - -selbst | self -sich | herself - -sie | they, she -ihnen | to them - -sind | are -so | so - -solche | such -solchem -solchen -solcher -solches - -soll | shall -sollte | should -sondern | but -sonst | else -über | over -um | about, around -und | and - -uns | us -unse -unsem -unsen -unser -unses - -unter | under -viel | much -vom | von + dem -von | from -vor | before -während | while -war | was -waren | were -warst | wast -was | what -weg | away, off -weil | because -weiter | further - -welche | which -welchem -welchen -welcher -welches - -wenn | when -werde | will -werden | will -wie | how -wieder | again -will | want -wir | we -wird | will -wirst | willst -wo | where -wollen | want -wollte | wanted -würde | would -würden | would -zu | to -zum | zu + dem -zur | zu + der -zwar | indeed -zwischen | between - diff --git a/resources/solr/lang/stopwords_el.txt b/resources/solr/lang/stopwords_el.txt deleted file mode 100644 index 232681f5bd..0000000000 --- a/resources/solr/lang/stopwords_el.txt +++ /dev/null @@ -1,78 +0,0 @@ -# Lucene Greek Stopwords list -# Note: by default this file is used after GreekLowerCaseFilter, -# so when modifying this file use 'σ' instead of 'ς' -ο -η -το -οι -τα -του -τησ -των -τον -την -και -κι -κ -ειμαι -εισαι -ειναι -ειμαστε -ειστε -στο -στον -στη -στην -μα -αλλα -απο -για -προσ -με -σε -ωσ -παρα -αντι -κατα -μετα -θα -να -δε -δεν -μη -μην -επι -ενω -εαν -αν -τοτε -που -πωσ -ποιοσ -ποια -ποιο -ποιοι -ποιεσ -ποιων -ποιουσ -αυτοσ -αυτη -αυτο -αυτοι -αυτων -αυτουσ -αυτεσ -αυτα -εκεινοσ -εκεινη -εκεινο -εκεινοι -εκεινεσ -εκεινα -εκεινων -εκεινουσ -οπωσ -ομωσ -ισωσ -οσο -οτι diff --git a/resources/solr/lang/stopwords_en.txt b/resources/solr/lang/stopwords_en.txt deleted file mode 100644 index 2c164c0b2a..0000000000 --- a/resources/solr/lang/stopwords_en.txt +++ /dev/null @@ -1,54 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# a couple of test stopwords to test that the words are really being -# configured from this file: -stopworda -stopwordb - -# Standard english stop words taken from Lucene's StopAnalyzer -a -an -and -are -as -at -be -but -by -for -if -in -into -is -it -no -not -of -on -or -such -that -the -their -then -there -these -they -this -to -was -will -with diff --git a/resources/solr/lang/stopwords_es.txt b/resources/solr/lang/stopwords_es.txt deleted file mode 100644 index 48bd65ef86..0000000000 --- a/resources/solr/lang/stopwords_es.txt +++ /dev/null @@ -1,356 +0,0 @@ - | From https://snowballstem.org/algorithms/spanish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Spanish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - - | The following is a ranked list (commonest to rarest) of stopwords - | deriving from a large sample of text. - - | Extra words have been added at the end. - -de | from, of -la | the, her -que | who, that -el | the -en | in -y | and -a | to -los | the, them -del | de + el -se | himself, from him etc -las | the, them -por | for, by, etc -un | a -para | for -con | with -no | no -una | a -su | his, her -al | a + el - | es from SER -lo | him -como | how -más | more -pero | pero -sus | su plural -le | to him, her -ya | already -o | or - | fue from SER -este | this - | ha from HABER -sí | himself etc -porque | because -esta | this - | son from SER -entre | between - | está from ESTAR -cuando | when -muy | very -sin | without -sobre | on - | ser from SER - | tiene from TENER -también | also -me | me -hasta | until -hay | there is/are -donde | where - | han from HABER -quien | whom, that - | están from ESTAR - | estado from ESTAR -desde | from -todo | all -nos | us -durante | during - | estados from ESTAR -todos | all -uno | a -les | to them -ni | nor -contra | against -otros | other - | fueron from SER -ese | that -eso | that - | había from HABER -ante | before -ellos | they -e | and (variant of y) -esto | this -mí | me -antes | before -algunos | some -qué | what? -unos | a -yo | I -otro | other -otras | other -otra | other -él | he -tanto | so much, many -esa | that -estos | these -mucho | much, many -quienes | who -nada | nothing -muchos | many -cual | who - | sea from SER -poco | few -ella | she -estar | to be - | haber from HABER -estas | these - | estaba from ESTAR - | estamos from ESTAR -algunas | some -algo | something -nosotros | we - - | other forms - -mi | me -mis | mi plural -tú | thou -te | thee -ti | thee -tu | thy -tus | tu plural -ellas | they -nosotras | we -vosotros | you -vosotras | you -os | you -mío | mine -mía | -míos | -mías | -tuyo | thine -tuya | -tuyos | -tuyas | -suyo | his, hers, theirs -suya | -suyos | -suyas | -nuestro | ours -nuestra | -nuestros | -nuestras | -vuestro | yours -vuestra | -vuestros | -vuestras | -esos | those -esas | those - - | forms of estar, to be (not including the infinitive): -estoy -estás -está -estamos -estáis -están -esté -estés -estemos -estéis -estén -estaré -estarás -estará -estaremos -estaréis -estarán -estaría -estarías -estaríamos -estaríais -estarían -estaba -estabas -estábamos -estabais -estaban -estuve -estuviste -estuvo -estuvimos -estuvisteis -estuvieron -estuviera -estuvieras -estuviéramos -estuvierais -estuvieran -estuviese -estuvieses -estuviésemos -estuvieseis -estuviesen -estando -estado -estada -estados -estadas -estad - - | forms of haber, to have (not including the infinitive): -he -has -ha -hemos -habéis -han -haya -hayas -hayamos -hayáis -hayan -habré -habrás -habrá -habremos -habréis -habrán -habría -habrías -habríamos -habríais -habrían -había -habías -habíamos -habíais -habían -hube -hubiste -hubo -hubimos -hubisteis -hubieron -hubiera -hubieras -hubiéramos -hubierais -hubieran -hubiese -hubieses -hubiésemos -hubieseis -hubiesen -habiendo -habido -habida -habidos -habidas - - | forms of ser, to be (not including the infinitive): -soy -eres -es -somos -sois -son -sea -seas -seamos -seáis -sean -seré -serás -será -seremos -seréis -serán -sería -serías -seríamos -seríais -serían -era -eras -éramos -erais -eran -fui -fuiste -fue -fuimos -fuisteis -fueron -fuera -fueras -fuéramos -fuerais -fueran -fuese -fueses -fuésemos -fueseis -fuesen -siendo -sido - | sed also means 'thirst' - - | forms of tener, to have (not including the infinitive): -tengo -tienes -tiene -tenemos -tenéis -tienen -tenga -tengas -tengamos -tengáis -tengan -tendré -tendrás -tendrá -tendremos -tendréis -tendrán -tendría -tendrías -tendríamos -tendríais -tendrían -tenía -tenías -teníamos -teníais -tenían -tuve -tuviste -tuvo -tuvimos -tuvisteis -tuvieron -tuviera -tuvieras -tuviéramos -tuvierais -tuvieran -tuviese -tuvieses -tuviésemos -tuvieseis -tuviesen -teniendo -tenido -tenida -tenidos -tenidas -tened - diff --git a/resources/solr/lang/stopwords_et.txt b/resources/solr/lang/stopwords_et.txt deleted file mode 100644 index 1b06a134b9..0000000000 --- a/resources/solr/lang/stopwords_et.txt +++ /dev/null @@ -1,1603 +0,0 @@ -# Estonian stopwords list -all -alla -allapoole -allpool -alt -altpoolt -eel -eespool -enne -hommikupoole -hoolimata -ilma -kaudu -keset -kesk -kohe -koos -kuhupoole -kuni -kuspool -kustpoolt -kõige -käsikäes -lappi -ligi -läbi -mööda -paitsi -peale -pealepoole -pealpool -pealt -pealtpoolt -piki -pikku -piku -pikuti -põiki -pärast -päri -risti -sealpool -sealtpoolt -seespool -seltsis -siiapoole -siinpool -siitpoolt -sinnapoole -sissepoole -taga -tagantpoolt -tagapidi -tagapool -taha -tahapoole -teispool -teispoole -tänu -tükkis -vaatamata -vastu -väljapoole -väljaspool -väljastpoolt -õhtupoole -ühes -ühestükis -ühestükkis -ülalpool -ülaltpoolt -üle -ülespoole -ülevalpool -ülevaltpoolt -ümber -ümbert -aegu -aegus -alguks -algul -algule -algult -alguni -all -alla -alt -alul -alutsi -arvel -asemel -asemele -eel -eeli -ees -eesotsas -eest -eestotsast -esitsi -ette -etteotsa -haaval -heaks -hoolimata -hulgas -hulgast -hulka -jalgu -jalus -jalust -jaoks -jooksul -juurde -juures -juurest -jälil -jälile -järel -järele -järelt -järgi -kaasas -kallal -kallale -kallalt -kamul -kannul -kannule -kannult -kaudu -kaupa -keskel -keskele -keskelt -keskis -keskpaiku -kestel -kestes -kilda -killas -killast -kimpu -kimpus -kiuste -kohal -kohale -kohalt -kohaselt -kohe -kohta -koos -korral -kukil -kukile -kukilt -kulul -kõrva -kõrval -kõrvale -kõrvalt -kõrvas -kõrvast -käekõrval -käekõrvale -käekõrvalt -käes -käest -kätte -külge -küljes -küljest -küüsi -küüsis -küüsist -ligi -ligidal -ligidale -ligidalt -aegu -aegus -alguks -algul -algule -algult -alguni -all -alla -alt -alul -alutsi -arvel -asemel -asemele -eel -eeli -ees -eesotsas -eest -eestotsast -esitsi -ette -etteotsa -haaval -heaks -hoolimata -hulgas -hulgast -hulka -jalgu -jalus -jalust -jaoks -jooksul -juurde -juures -juurest -jälil -jälile -järel -järele -järelt -järgi -kaasas -kallal -kallale -kallalt -kamul -kannul -kannule -kannult -kaudu -kaupa -keskel -keskele -keskelt -keskis -keskpaiku -kestel -kestes -kilda -killas -killast -kimpu -kimpus -kiuste -kohal -kohale -kohalt -kohaselt -kohe -kohta -koos -korral -kukil -kukile -kukilt -kulul -kõrva -kõrval -kõrvale -kõrvalt -kõrvas -kõrvast -käekõrval -käekõrvale -käekõrvalt -käes -käest -kätte -külge -küljes -küljest -küüsi -küüsis -küüsist -ligi -ligidal -ligidale -ligidalt -lool -läbi -lähedal -lähedale -lähedalt -man -mant -manu -meelest -mööda -nahas -nahka -nahkas -najal -najale -najalt -nõjal -nõjale -otsa -otsas -otsast -paigale -paigu -paiku -peal -peale -pealt -perra -perrä -pidi -pihta -piki -pikku -pool -poole -poolest -poolt -puhul -puksiiris -pähe -päralt -päras -pärast -päri -ringi -ringis -risust -saadetusel -saadik -saatel -saati -seas -seast -sees -seest -sekka -seljataga -seltsi -seltsis -seltsist -sisse -slepis -suhtes -šlepis -taga -tagant -tagantotsast -tagaotsas -tagaselja -tagasi -tagast -tagutsi -taha -tahaotsa -takka -tarvis -tasa -tuuri -tuuris -tõttu -tükkis -uhal -vaatamata -vahel -vahele -vahelt -vahepeal -vahepeale -vahepealt -vahetsi -varal -varale -varul -vastas -vastast -vastu -veerde -veeres -viisi -võidu -võrd -võrdki -võrra -võrragi -väel -väele -vältel -väärt -väärtki -äärde -ääre -ääres -äärest -ühes -üle -ümber -ümbert -a -abil -aina -ainult -alalt -alates -alati -alles -b -c -d -e -eales -ealeski -edasi -edaspidi -eelkõige -eemal -ei -eks -end -enda -enese -ennem -esialgu -f -g -h -hoopis -i -iganes -igatahes -igati -iial -iialgi -ikka -ikkagi -ilmaski -iseenda -iseenese -iseenesest -isegi -j -jah -ju -juba -juhul -just -järelikult -k -ka -kah -kas -kasvõi -keda -kestahes -kogu -koguni -kohati -kokku -kuhu -kuhugi -kuidagi -kuidas -kunagi -kus -kusagil -kusjuures -kuskil -kust -kõigepealt -küll -l -liiga -lisaks -m -miks -mil -millal -millalgi -mispärast -mistahes -mistõttu -mitte -muide -muidu -muidugi -muist -mujal -mujale -mujalt -mõlemad -mõnda -mõne -mõnikord -n -nii -niikaua -niimoodi -niipaljuke -niisama -niisiis -niivõrd -nõnda -nüüd -o -omaette -omakorda -omavahel -ometi -p -palju -paljuke -palju-palju -peaaegu -peagi -peamiselt -pigem -pisut -praegu -päris -r -rohkem -s -samas -samuti -seal -sealt -sedakorda -sedapuhku -seega -seejuures -seejärel -seekord -seepärast -seetõttu -sellepärast -seni -sestap -siia -siiani -siin -siinkohal -siis -siiski -siit -sinna -suht -š -z -ž -t -teel -teineteise -tõesti -täiesti -u -umbes -v -w -veel -veelgi -vist -võibolla -võib-olla -väga -vähemalt -välja -väljas -väljast -õ -ä -ära -ö -ü -ühtlasi -üksi -ükskõik -ülal -ülale -ülalt -üles -ülesse -üleval -ülevalt -ülimalt -üsna -x -y -aga -ega -ehk -ehkki -elik -ellik -enge -ennegu -ent -et -ja -justkui -kui -kuid -kuigi -kuivõrd -kuna -kuni -kut -mistab -muudkui -nagu -nigu -ning -olgugi -otsekui -otsenagu -selmet -sest -sestab -vaid -või -aa -adaa -adjöö -ae -ah -ahaa -ahah -ah-ah-ah -ah-haa -ahoi -ai -aidaa -aidu-raidu -aih -aijeh -aituma -aitäh -aitüma -ammuu -amps -ampsti -aptsih -ass -at -ata -at-at-at -atsih -atsihh -auh -bai-bai -bingo -braavo -brr -ee -eeh -eh -ehee -eheh -eh-eh-hee -eh-eh-ee -ehei -ehh -ehhee -einoh -ena -ennäe -ennäh -fuh -fui -fuih -haa -hah -hahaa -hah-hah-hah -halleluuja -hallo -halloo -hass -hee -heh -he-he-hee -hei -heldeke(ne) -heureka -hihii -hip-hip-hurraa -hmh -hmjah -hoh-hoh-hoo -hohoo -hoi -hollallaa -hoo -hoplaa -hopp -hops -hopsassaa -hopsti -hosianna -huh -huidii -huist -hurjah -hurjeh -hurjoh -hurjuh -hurraa -huu -hõhõh -hõi -hõissa -hõissassa -hõk -hõkk -häh -hä-hä-hää -hüvasti -ih-ah-haa -ih-ih-hii -ii-ha-ha -issake -issakene -isver -jaa-ah -ja-ah -jaah -janäe -jeeh -jeerum -jeever -jessas -jestas -juhhei -jumalaga -jumalime -jumaluke -jumalukene -jutas -kaaps -kaapsti -kaasike -kae -kalps -kalpsti -kannäe -kanäe -kappadi -kaps -kapsti -karkõmm -karkäuh -karkääks -karkääksti -karmauh -karmauhti -karnaps -karnapsti -karniuhti -karpartsaki -karpauh -karpauhti -karplauh -karplauhti -karprauh -karprauhti -karsumdi -karsumm -kartsumdi -kartsumm -karviuh -karviuhti -kaske -kassa -kauh -kauhti -keh -keksti -kepsti -khe -khm -kih -kiiks -kiiksti -kiis -kiiss -kikerii -kikerikii -kili -kilk -kilk-kõlk -kilks -kilks-kolks -kilks-kõlks -kill -killadi -killadi|-kolladi -killadi-kõlladi -killa-kolla -killa-kõlla -kill-kõll -kimps-komps -kipp -kips-kõps -kiriküüt -kirra-kõrra -kirr-kõrr -kirts -klaps -klapsti -klirdi -klirr -klonks -klops -klopsti -kluk -klu-kluu -klõks -klõksti -klõmdi -klõmm -klõmpsti -klõnks -klõnksti -klõps -klõpsti -kläu -kohva-kohva -kok -koks -koksti -kolaki -kolk -kolks -kolksti -koll -kolladi -komp -komps -kompsti -kop -kopp -koppadi -kops -kopsti -kossu -kotsu -kraa -kraak -kraaks -kraaps -kraapsti -krahh -kraks -kraksti -kraps -krapsti -krauh -krauhti -kriiks -kriiksti -kriips -kriips-kraaps -kripa-krõpa -krips-kraps -kriuh -kriuks -kriuksti -kromps -kronk -kronks -krooks -kruu -krõks -krõksti -krõpa -krõps -krõpsti -krõuh -kräu -kräuh -kräuhti -kräuks -kss -kukeleegu -kukku -kuku -kulu -kurluu -kurnäu -kuss -kussu -kõks -kõksti -kõldi -kõlks -kõlksti -kõll -kõmaki -kõmdi -kõmm -kõmps -kõpp -kõps -kõpsadi -kõpsat -kõpsti -kõrr -kõrra-kõrra -kõss -kõtt -kõõksti -kärr -kärts -kärtsti -käuks -käuksti -kääga -kääks -kääksti -köh -köki-möki -köksti -laks -laksti -lampsti -larts -lartsti -lats -latsti -leelo -legoo -lehva -liiri-lõõri -lika-lõka -likat-lõkat -limpsti -lips -lipsti -lirts -lirtsaki -lirtsti -lonksti -lops -lopsti -lorts -lortsti -luks -lups -lupsti -lurts -lurtsti -lõks -lõksti -lõmps -lõmpsti -lõnks -lõnksti -lärts -lärtsti -läts -lätsti -lörts -lörtsti -lötsti -lööps -lööpsti -marss -mats -matsti -mauh -mauhti -mh -mhh -mhmh -miau -mjaa -mkm -m-mh -mnjaa -mnjah -moens -mulks -mulksti -mull-mull -mull-mull-mull -muu -muuh -mõh -mõmm -mäh -mäts -mäu -mää -möh -möh-öh-ää -möö -müh-müh -mühüh -müks -müksti -müraki -mürr -mürts -mürtsaki -mürtsti -mütaku -müta-mäta -müta-müta -müt-müt -müt-müt-müt -müts -mütsti -mütt -naa -naah -nah -naks -naksti -nanuu -naps -napsti -nilpsti -nipsti -nirr -niuh -niuh-näuh -niuhti -noh -noksti -nolpsti -nonoh -nonoo -nonäh -noo -nooh -nooks -norr -nurr -nuuts -nõh -nõhh -nõka-nõka -nõks -nõksat-nõksat -nõks-nõks -nõksti -nõõ -nõõh -näeh -näh -nälpsti -nämm-nämm -näpsti -näts -nätsti -näu -näuh -näuhti -näuks -näuksti -nääh -nääks -nühkat-nühkat -oeh -oh -ohh -ohhh -oh-hoi -oh-hoo -ohoh -oh-oh-oo -oh-oh-hoo -ohoi -ohoo -oi -oih -oijee -oijeh -oo -ooh -oo-oh -oo-ohh -oot -ossa -ot -paa -pah -pahh -pakaa -pamm -pantsti -pardon -pardonks -parlartsti -parts -partsti -partsumdi -partsumm -pastoi -pats -patst -patsti -pau -pauh -pauhti -pele -pfui -phuh -phuuh -phäh -phähh -piiks -piip -piiri-pääri -pimm -pimm-pamm -pimm-pomm -pimm-põmm -piraki -piuks -piu-pau -plaks -plaksti -plarts -plartsti -plats -platsti -plauh -plauhh -plauhti -pliks -pliks-plaks -plinn -pliraki -plirts -plirtsti -pliu -pliuh -ploks -plotsti -plumps -plumpsti -plõks -plõksti -plõmdi -plõmm -plõnn -plärr -plärts -plärtsat -plärtsti -pläu -pläuh -plää -plörtsat -pomm -popp -pops -popsti -ports -pot -pots -potsti -pott -praks -praksti -prants -prantsaki -prantsti -prassai -prauh -prauhh -prauhti -priks -priuh -priuhh -priuh-prauh -proosit -proost -prr -prrr -prõks -prõksti -prõmdi -prõmm -prõntsti -prääk -prääks -pst -psst -ptrr -ptruu -ptüi -puh -puhh -puksti -pumm -pumps -pup-pup-pup -purts -puuh -põks -põksti -põmdi -põmm -põmmadi -põnks -põnn -põnnadi -põnt -põnts -põntsti -põraki -põrr -põrra-põrra -päh -pähh -päntsti -pää -pöörd -püh -raks -raksti -raps -rapsti -ratataa -rauh -riips -riipsti -riks -riks-raks -rips-raps -rivitult -robaki -rops -ropsaki -ropsti -ruik -räntsti -räts -röh -röhh -sah -sahh -sahkat -saps -sapsti -sauh -sauhti -servus -sihkadi-sahkadi -sihka-sahka -sihkat-sahkat -silks -silk-solk -sips -sipsti -sirr -sirr-sorr -sirts -sirtsti -siu -siuh -siuh-sauh -siuh-säuh -siuhti -siuks -siuts -skool -so -soh -solks -solksti -solpsti -soo -sooh -so-oh -soo-oh -sopp -sops -sopsti -sorr -sorts -sortsti -so-soo -soss -soss-soss -ss -sss -sst -stopp -suhkat-sahkat -sulk -sulks -sulksti -sull -sulla-sulla -sulpa-sulpa -sulps -sulpsti -sumaki -sumdi -summ -summat-summat -sups -supsaku -supsti -surts -surtsti -suss -susti -suts -sutsti -säh -sähke -särts -särtsti -säu -säuh -säuhti -taevake -taevakene -takk -tere -terekest -tibi-tibi -tikk-takk -tiks -tilk -tilks -till -tilla-talla -till-tall -tilulii -tinn -tip -tip-tap -tirr -tirtsti -tiu -tjaa -tjah -tohhoh -tohhoo -tohoh -tohoo -tok -tokk -toks -toksti -tonks -tonksti -tota -totsti -tot-tot -tprr -tpruu -trah -trahh -trallallaa -trill -trillallaa -trr -trrr -tsah -tsahh -tsilk -tsilk-tsolk -tsirr -tsiuh -tskae -tsolk -tss -tst -tsst -tsuhh -tsuk -tsumm -tsurr -tsäuh -tšao -tšš -tššš -tuk -tuks -turts -turtsti -tutki -tutkit -tutu-lutu -tutulutu -tuut -tuutu-luutu -tõks -tötsti -tümps -uh -uhh -uh-huu -uhtsa -uhtsaa -uhuh -uhuu -ui -uih -uih-aih -uijah -uijeh -uist -uit -uka -upsti -uraa -urjah -urjeh -urjoh -urjuh -urr -urraa -ust -utu -uu -uuh -vaak -vaat -vae -vaeh -vai -vat -vau -vhüüt -vidiit -viiks -vilks -vilksti -vinki-vinki -virdi -virr -viu -viudi -viuh -viuhti -voeh -voh -vohh -volks -volksti -vooh -vops -vopsti -vot -vuh -vuhti -vuih -vulks -vulksti -vull -vulpsti -vups -vupsaki -vupsaku -vupsti -vurdi -vurr -vurra-vurra -vurts -vurtsti -vutt -võe -võeh -või -võih -võrr -võts -võtt -vääks -õe -õits -õk -õkk -õrr -õss -õuh -äh -ähh -ähhähhää -äh-hää -äh-äh-hää -äiu -äiu-ää -äss -ää -ääh -äähh -öh -öhh -ök -üh -eelmine -eikeegi -eimiski -emb-kumb -enam -enim -iga -igasugune -igaüks -ise -isesugune -järgmine -keegi -kes -kumb -kumbki -kõik -meiesugune -meietaoline -midagi -mihuke -mihukene -milletaoline -milline -mina -minake -mingi -mingisugune -minusugune -minutaoline -mis -miski -miskisugune -missugune -misuke -mitmes -mitmesugune -mitu -mitu-mitu -mitu-setu -muu -mõlema -mõnesugune -mõni -mõningane -mõningas -mäherdune -määrane -naasugune -need -nemad -nendesugune -nendetaoline -nihuke -nihukene -niimitu -niisamasugune -niisugune -nisuke -nisukene -oma -omaenese -omasugune -omataoline -pool -praegune -sama -samasugune -samataoline -see -seesama -seesamane -seesamune -seesinane -seesugune -selline -sihuke -sihukene -sina -sinusugune -sinutaoline -siuke -siukene -säherdune -säärane -taoline -teiesugune -teine -teistsugune -tema -temake -temakene -temasugune -temataoline -too -toosama -toosamane -üks -üksteise -hakkama -minema -olema -pidama -saama -tegema -tulema -võima diff --git a/resources/solr/lang/stopwords_eu.txt b/resources/solr/lang/stopwords_eu.txt deleted file mode 100644 index 25f1db9346..0000000000 --- a/resources/solr/lang/stopwords_eu.txt +++ /dev/null @@ -1,99 +0,0 @@ -# example set of basque stopwords -al -anitz -arabera -asko -baina -bat -batean -batek -bati -batzuei -batzuek -batzuetan -batzuk -bera -beraiek -berau -berauek -bere -berori -beroriek -beste -bezala -da -dago -dira -ditu -du -dute -edo -egin -ere -eta -eurak -ez -gainera -gu -gutxi -guzti -haiei -haiek -haietan -hainbeste -hala -han -handik -hango -hara -hari -hark -hartan -hau -hauei -hauek -hauetan -hemen -hemendik -hemengo -hi -hona -honek -honela -honetan -honi -hor -hori -horiei -horiek -horietan -horko -horra -horrek -horrela -horretan -horri -hortik -hura -izan -ni -noiz -nola -non -nondik -nongo -nor -nora -ze -zein -zen -zenbait -zenbat -zer -zergatik -ziren -zituen -zu -zuek -zuen -zuten diff --git a/resources/solr/lang/stopwords_fa.txt b/resources/solr/lang/stopwords_fa.txt deleted file mode 100644 index 723641c6da..0000000000 --- a/resources/solr/lang/stopwords_fa.txt +++ /dev/null @@ -1,313 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -# Note: by default this file is used after normalization, so when adding entries -# to this file, use the arabic 'ي' instead of 'ی' -انان -نداشته -سراسر -خياه -ايشان -وي -تاكنون -بيشتري -دوم -پس -ناشي -وگو -يا -داشتند -سپس -هنگام -هرگز -پنج -نشان -امسال -ديگر -گروهي -شدند -چطور -ده -و -دو -نخستين -ولي -چرا -چه -وسط -ه -كدام -قابل -يك -رفت -هفت -همچنين -در -هزار -بله -بلي -شايد -اما -شناسي -گرفته -دهد -داشته -دانست -داشتن -خواهيم -ميليارد -وقتيكه -امد -خواهد -جز -اورده -شده -بلكه -خدمات -شدن -برخي -نبود -بسياري -جلوگيري -حق -كردند -نوعي -بعري -نكرده -نظير -نبايد -بوده -بودن -داد -اورد -هست -جايي -شود -دنبال -داده -بايد -سابق -هيچ -همان -انجا -كمتر -كجاست -گردد -كسي -تر -مردم -تان -دادن -بودند -سري -جدا -ندارند -مگر -يكديگر -دارد -دهند -بنابراين -هنگامي -سمت -جا -انچه -خود -دادند -زياد -دارند -اثر -بدون -بهترين -بيشتر -البته -به -براساس -بيرون -كرد -بعضي -گرفت -توي -اي -ميليون -او -جريان -تول -بر -مانند -برابر -باشيم -مدتي -گويند -اكنون -تا -تنها -جديد -چند -بي -نشده -كردن -كردم -گويد -كرده -كنيم -نمي -نزد -روي -قصد -فقط -بالاي -ديگران -اين -ديروز -توسط -سوم -ايم -دانند -سوي -استفاده -شما -كنار -داريم -ساخته -طور -امده -رفته -نخست -بيست -نزديك -طي -كنيد -از -انها -تمامي -داشت -يكي -طريق -اش -چيست -روب -نمايد -گفت -چندين -چيزي -تواند -ام -ايا -با -ان -ايد -ترين -اينكه -ديگري -راه -هايي -بروز -همچنان -پاعين -كس -حدود -مختلف -مقابل -چيز -گيرد -ندارد -ضد -همچون -سازي -شان -مورد -باره -مرسي -خويش -برخوردار -چون -خارج -شش -هنوز -تحت -ضمن -هستيم -گفته -فكر -بسيار -پيش -براي -روزهاي -انكه -نخواهد -بالا -كل -وقتي -كي -چنين -كه -گيري -نيست -است -كجا -كند -نيز -يابد -بندي -حتي -توانند -عقب -خواست -كنند -بين -تمام -همه -ما -باشند -مثل -شد -اري -باشد -اره -طبق -بعد -اگر -صورت -غير -جاي -بيش -ريزي -اند -زيرا -چگونه -بار -لطفا -مي -درباره -من -ديده -همين -گذاري -برداري -علت -گذاشته -هم -فوق -نه -ها -شوند -اباد -همواره -هر -اول -خواهند -چهار -نام -امروز -مان -هاي -قبل -كنم -سعي -تازه -را -هستند -زير -جلوي -عنوان -بود diff --git a/resources/solr/lang/stopwords_fi.txt b/resources/solr/lang/stopwords_fi.txt deleted file mode 100644 index c9ee2f16dc..0000000000 --- a/resources/solr/lang/stopwords_fi.txt +++ /dev/null @@ -1,96 +0,0 @@ - | From https://snowballstem.org/algorithms/finnish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - -| forms of BE - -olla -olen -olet -on -olemme -olette -ovat -ole | negative form - -oli -olisi -olisit -olisin -olisimme -olisitte -olisivat -olit -olin -olimme -olitte -olivat -ollut -olleet - -en | negation -et -ei -emme -ette -eivät - -|Nom Gen Acc Part Iness Elat Illat Adess Ablat Allat Ess Trans -minä minun minut minua minussa minusta minuun minulla minulta minulle | I -sinä sinun sinut sinua sinussa sinusta sinuun sinulla sinulta sinulle | you -hän hänen hänet häntä hänessä hänestä häneen hänellä häneltä hänelle | he she -me meidän meidät meitä meissä meistä meihin meillä meiltä meille | we -te teidän teidät teitä teissä teistä teihin teillä teiltä teille | you -he heidän heidät heitä heissä heistä heihin heillä heiltä heille | they - -tämä tämän tätä tässä tästä tähän tällä tältä tälle tänä täksi | this -tuo tuon tuota tuossa tuosta tuohon tuolla tuolta tuolle tuona tuoksi | that -se sen sitä siinä siitä siihen sillä siltä sille sinä siksi | it -nämä näiden näitä näissä näistä näihin näillä näiltä näille näinä näiksi | these -nuo noiden noita noissa noista noihin noilla noilta noille noina noiksi | those -ne niiden niitä niissä niistä niihin niillä niiltä niille niinä niiksi | they - -kuka kenen kenet ketä kenessä kenestä keneen kenellä keneltä kenelle kenenä keneksi| who -ketkä keiden ketkä keitä keissä keistä keihin keillä keiltä keille keinä keiksi | (pl) -mikä minkä minkä mitä missä mistä mihin millä miltä mille minä miksi | which what -mitkä | (pl) - -joka jonka jota jossa josta johon jolla jolta jolle jona joksi | who which -jotka joiden joita joissa joista joihin joilla joilta joille joina joiksi | (pl) - -| conjunctions - -että | that -ja | and -jos | if -koska | because -kuin | than -mutta | but -niin | so -sekä | and -sillä | for -tai | or -vaan | but -vai | or -vaikka | although - - -| prepositions - -kanssa | with -mukaan | according to -noin | about -poikki | across -yli | over, across - -| other - -kun | when -nyt | now -itse | self - diff --git a/resources/solr/lang/stopwords_fr.txt b/resources/solr/lang/stopwords_fr.txt deleted file mode 100644 index 658ae9c91a..0000000000 --- a/resources/solr/lang/stopwords_fr.txt +++ /dev/null @@ -1,186 +0,0 @@ - | From https://snowballstem.org/algorithms/french/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A French stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - -au | a + le -aux | a + les -avec | with -ce | this -ces | these -dans | with -de | of -des | de + les -du | de + le -elle | she -en | `of them' etc -et | and -eux | them -il | he -je | I -la | the -le | the -leur | their -lui | him -ma | my (fem) -mais | but -me | me -même | same; as in moi-même (myself) etc -mes | me (pl) -moi | me -mon | my (masc) -ne | not -nos | our (pl) -notre | our -nous | we -on | one -ou | where -par | by -pas | not -pour | for -qu | que before vowel -que | that -qui | who -sa | his, her (fem) -se | oneself -ses | his (pl) - | son | his, her (masc). Omitted because it is homonym of "sound" -sur | on -ta | thy (fem) -te | thee -tes | thy (pl) -toi | thee -ton | thy (masc) -tu | thou -un | a -une | a -vos | your (pl) -votre | your -vous | you - - | single letter forms - -c | c' -d | d' -j | j' -l | l' -à | to, at -m | m' -n | n' -s | s' -t | t' -y | there - - | forms of être (not including the infinitive): - | été - Omitted because it is homonym of "summer" -étée -étées - | étés - Omitted because it is homonym of "summers" -étant -suis -es - | est - Omitted because it is homonym of "east" - | sommes - Omitted because it is homonym of "sums" -êtes -sont -serai -seras -sera -serons -serez -seront -serais -serait -serions -seriez -seraient -étais -était -étions -étiez -étaient -fus -fut -fûmes -fûtes -furent -sois -soit -soyons -soyez -soient -fusse -fusses - | fût - Omitted because it is homonym of "tap", like in "beer on tap" -fussions -fussiez -fussent - - | forms of avoir (not including the infinitive): -ayant -eu -eue -eues -eus -ai - | as - Omitted because it is homonym of "ace" -avons -avez -ont -aurai - | auras - Omitted because it is also the name of a kind of wind - | aura - Omitted because it is also the name of a kind of wind and homonym of "aura" -aurons -aurez -auront -aurais -aurait -aurions -auriez -auraient -avais -avait - | avions - Omitted because it is homonym of "planes" -aviez -avaient -eut -eûmes -eûtes -eurent -aie -aies -ait -ayons -ayez -aient -eusse -eusses -eût -eussions -eussiez -eussent - - | Later additions (from Jean-Christophe Deschamps) -ceci | this -cela | that (added 11 Apr 2012. Omission reported by Adrien Grand) -celà | that (incorrect, though common) -cet | this -cette | this -ici | here -ils | they -les | the (pl) -leurs | their (pl) -quel | which -quels | which -quelle | which -quelles | which -sans | without -soi | oneself - diff --git a/resources/solr/lang/stopwords_ga.txt b/resources/solr/lang/stopwords_ga.txt deleted file mode 100644 index 9ff88d747e..0000000000 --- a/resources/solr/lang/stopwords_ga.txt +++ /dev/null @@ -1,110 +0,0 @@ - -a -ach -ag -agus -an -aon -ar -arna -as -b' -ba -beirt -bhúr -caoga -ceathair -ceathrar -chomh -chtó -chuig -chun -cois -céad -cúig -cúigear -d' -daichead -dar -de -deich -deichniúr -den -dhá -do -don -dtí -dá -dár -dó -faoi -faoin -faoina -faoinár -fara -fiche -gach -gan -go -gur -haon -hocht -i -iad -idir -in -ina -ins -inár -is -le -leis -lena -lenár -m' -mar -mo -mé -na -nach -naoi -naonúr -ná -ní -níor -nó -nócha -ocht -ochtar -os -roimh -sa -seacht -seachtar -seachtó -seasca -seisear -siad -sibh -sinn -sna -sé -sí -tar -thar -thú -triúr -trí -trína -trínár -tríocha -tú -um -ár -é -éis -í -ó -ón -óna -ónár diff --git a/resources/solr/lang/stopwords_gl.txt b/resources/solr/lang/stopwords_gl.txt deleted file mode 100644 index d8760b12c1..0000000000 --- a/resources/solr/lang/stopwords_gl.txt +++ /dev/null @@ -1,161 +0,0 @@ -# galican stopwords -a -aínda -alí -aquel -aquela -aquelas -aqueles -aquilo -aquí -ao -aos -as -así -á -ben -cando -che -co -coa -comigo -con -connosco -contigo -convosco -coas -cos -cun -cuns -cunha -cunhas -da -dalgunha -dalgunhas -dalgún -dalgúns -das -de -del -dela -delas -deles -desde -deste -do -dos -dun -duns -dunha -dunhas -e -el -ela -elas -eles -en -era -eran -esa -esas -ese -eses -esta -estar -estaba -está -están -este -estes -estiven -estou -eu -é -facer -foi -foron -fun -había -hai -iso -isto -la -las -lle -lles -lo -los -mais -me -meu -meus -min -miña -miñas -moi -na -nas -neste -nin -no -non -nos -nosa -nosas -noso -nosos -nós -nun -nunha -nuns -nunhas -o -os -ou -ó -ós -para -pero -pode -pois -pola -polas -polo -polos -por -que -se -senón -ser -seu -seus -sexa -sido -sobre -súa -súas -tamén -tan -te -ten -teñen -teño -ter -teu -teus -ti -tido -tiña -tiven -túa -túas -un -unha -unhas -uns -vos -vosa -vosas -voso -vosos -vós diff --git a/resources/solr/lang/stopwords_hi.txt b/resources/solr/lang/stopwords_hi.txt deleted file mode 100644 index 86286bb083..0000000000 --- a/resources/solr/lang/stopwords_hi.txt +++ /dev/null @@ -1,235 +0,0 @@ -# Also see http://www.opensource.org/licenses/bsd-license.html -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# This file was created by Jacques Savoy and is distributed under the BSD license. -# Note: by default this file also contains forms normalized by HindiNormalizer -# for spelling variation (see section below), such that it can be used whether or -# not you enable that feature. When adding additional entries to this list, -# please add the normalized form as well. -अंदर -अत -अपना -अपनी -अपने -अभी -आदि -आप -इत्यादि -इन -इनका -इन्हीं -इन्हें -इन्हों -इस -इसका -इसकी -इसके -इसमें -इसी -इसे -उन -उनका -उनकी -उनके -उनको -उन्हीं -उन्हें -उन्हों -उस -उसके -उसी -उसे -एक -एवं -एस -ऐसे -और -कई -कर -करता -करते -करना -करने -करें -कहते -कहा -का -काफ़ी -कि -कितना -किन्हें -किन्हों -किया -किर -किस -किसी -किसे -की -कुछ -कुल -के -को -कोई -कौन -कौनसा -गया -घर -जब -जहाँ -जा -जितना -जिन -जिन्हें -जिन्हों -जिस -जिसे -जीधर -जैसा -जैसे -जो -तक -तब -तरह -तिन -तिन्हें -तिन्हों -तिस -तिसे -तो -था -थी -थे -दबारा -दिया -दुसरा -दूसरे -दो -द्वारा -न -नहीं -ना -निहायत -नीचे -ने -पर -पर -पहले -पूरा -पे -फिर -बनी -बही -बहुत -बाद -बाला -बिलकुल -भी -भीतर -मगर -मानो -मे -में -यदि -यह -यहाँ -यही -या -यिह -ये -रखें -रहा -रहे -ऱ्वासा -लिए -लिये -लेकिन -व -वर्ग -वह -वह -वहाँ -वहीं -वाले -वुह -वे -वग़ैरह -संग -सकता -सकते -सबसे -सभी -साथ -साबुत -साभ -सारा -से -सो -ही -हुआ -हुई -हुए -है -हैं -हो -होता -होती -होते -होना -होने -# additional normalized forms of the above -अपनि -जेसे -होति -सभि -तिंहों -इंहों -दवारा -इसि -किंहें -थि -उंहों -ओर -जिंहें -वहिं -अभि -बनि -हि -उंहिं -उंहें -हें -वगेरह -एसे -रवासा -कोन -निचे -काफि -उसि -पुरा -भितर -हे -बहि -वहां -कोइ -यहां -जिंहों -तिंहें -किसि -कइ -यहि -इंहिं -जिधर -इंहें -अदि -इतयादि -हुइ -कोनसा -इसकि -दुसरे -जहां -अप -किंहों -उनकि -भि -वरग -हुअ -जेसा -नहिं diff --git a/resources/solr/lang/stopwords_hu.txt b/resources/solr/lang/stopwords_hu.txt deleted file mode 100644 index 3fa279eac9..0000000000 --- a/resources/solr/lang/stopwords_hu.txt +++ /dev/null @@ -1,211 +0,0 @@ - | From https://snowballstem.org/algorithms/hungarian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - -| Hungarian stop word list -| prepared by Anna Tordai - -a -ahogy -ahol -aki -akik -akkor -alatt -által -általában -amely -amelyek -amelyekben -amelyeket -amelyet -amelynek -ami -amit -amolyan -amíg -amikor -át -abban -ahhoz -annak -arra -arról -az -azok -azon -azt -azzal -azért -aztán -azután -azonban -bár -be -belül -benne -cikk -cikkek -cikkeket -csak -de -e -eddig -egész -egy -egyes -egyetlen -egyéb -egyik -egyre -ekkor -el -elég -ellen -elő -először -előtt -első -én -éppen -ebben -ehhez -emilyen -ennek -erre -ez -ezt -ezek -ezen -ezzel -ezért -és -fel -felé -hanem -hiszen -hogy -hogyan -igen -így -illetve -ill. -ill -ilyen -ilyenkor -ison -ismét -itt -jó -jól -jobban -kell -kellett -keresztül -keressünk -ki -kívül -között -közül -legalább -lehet -lehetett -legyen -lenne -lenni -lesz -lett -maga -magát -majd -majd -már -más -másik -meg -még -mellett -mert -mely -melyek -mi -mit -míg -miért -milyen -mikor -minden -mindent -mindenki -mindig -mint -mintha -mivel -most -nagy -nagyobb -nagyon -ne -néha -nekem -neki -nem -néhány -nélkül -nincs -olyan -ott -össze -ő -ők -őket -pedig -persze -rá -s -saját -sem -semmi -sok -sokat -sokkal -számára -szemben -szerint -szinte -talán -tehát -teljes -tovább -továbbá -több -úgy -ugyanis -új -újabb -újra -után -utána -utolsó -vagy -vagyis -valaki -valami -valamint -való -vagyok -van -vannak -volt -voltam -voltak -voltunk -vissza -vele -viszont -volna diff --git a/resources/solr/lang/stopwords_hy.txt b/resources/solr/lang/stopwords_hy.txt deleted file mode 100644 index 60c1c50fbc..0000000000 --- a/resources/solr/lang/stopwords_hy.txt +++ /dev/null @@ -1,46 +0,0 @@ -# example set of Armenian stopwords. -այդ -այլ -այն -այս -դու -դուք -եմ -են -ենք -ես -եք -է -էի -էին -էինք -էիր -էիք -էր -ըստ -թ -ի -ին -իսկ -իր -կամ -համար -հետ -հետո -մենք -մեջ -մի -ն -նա -նաև -նրա -նրանք -որ -որը -որոնք -որպես -ու -ում -պիտի -վրա -և diff --git a/resources/solr/lang/stopwords_id.txt b/resources/solr/lang/stopwords_id.txt deleted file mode 100644 index 4617f83a5c..0000000000 --- a/resources/solr/lang/stopwords_id.txt +++ /dev/null @@ -1,359 +0,0 @@ -# from appendix D of: A Study of Stemming Effects on Information -# Retrieval in Bahasa Indonesia -ada -adanya -adalah -adapun -agak -agaknya -agar -akan -akankah -akhirnya -aku -akulah -amat -amatlah -anda -andalah -antar -diantaranya -antara -antaranya -diantara -apa -apaan -mengapa -apabila -apakah -apalagi -apatah -atau -ataukah -ataupun -bagai -bagaikan -sebagai -sebagainya -bagaimana -bagaimanapun -sebagaimana -bagaimanakah -bagi -bahkan -bahwa -bahwasanya -sebaliknya -banyak -sebanyak -beberapa -seberapa -begini -beginian -beginikah -beginilah -sebegini -begitu -begitukah -begitulah -begitupun -sebegitu -belum -belumlah -sebelum -sebelumnya -sebenarnya -berapa -berapakah -berapalah -berapapun -betulkah -sebetulnya -biasa -biasanya -bila -bilakah -bisa -bisakah -sebisanya -boleh -bolehkah -bolehlah -buat -bukan -bukankah -bukanlah -bukannya -cuma -percuma -dahulu -dalam -dan -dapat -dari -daripada -dekat -demi -demikian -demikianlah -sedemikian -dengan -depan -di -dia -dialah -dini -diri -dirinya -terdiri -dong -dulu -enggak -enggaknya -entah -entahlah -terhadap -terhadapnya -hal -hampir -hanya -hanyalah -harus -haruslah -harusnya -seharusnya -hendak -hendaklah -hendaknya -hingga -sehingga -ia -ialah -ibarat -ingin -inginkah -inginkan -ini -inikah -inilah -itu -itukah -itulah -jangan -jangankan -janganlah -jika -jikalau -juga -justru -kala -kalau -kalaulah -kalaupun -kalian -kami -kamilah -kamu -kamulah -kan -kapan -kapankah -kapanpun -dikarenakan -karena -karenanya -ke -kecil -kemudian -kenapa -kepada -kepadanya -ketika -seketika -khususnya -kini -kinilah -kiranya -sekiranya -kita -kitalah -kok -lagi -lagian -selagi -lah -lain -lainnya -melainkan -selaku -lalu -melalui -terlalu -lama -lamanya -selama -selama -selamanya -lebih -terlebih -bermacam -macam -semacam -maka -makanya -makin -malah -malahan -mampu -mampukah -mana -manakala -manalagi -masih -masihkah -semasih -masing -mau -maupun -semaunya -memang -mereka -merekalah -meski -meskipun -semula -mungkin -mungkinkah -nah -namun -nanti -nantinya -nyaris -oleh -olehnya -seorang -seseorang -pada -padanya -padahal -paling -sepanjang -pantas -sepantasnya -sepantasnyalah -para -pasti -pastilah -per -pernah -pula -pun -merupakan -rupanya -serupa -saat -saatnya -sesaat -saja -sajalah -saling -bersama -sama -sesama -sambil -sampai -sana -sangat -sangatlah -saya -sayalah -se -sebab -sebabnya -sebuah -tersebut -tersebutlah -sedang -sedangkan -sedikit -sedikitnya -segala -segalanya -segera -sesegera -sejak -sejenak -sekali -sekalian -sekalipun -sesekali -sekaligus -sekarang -sekarang -sekitar -sekitarnya -sela -selain -selalu -seluruh -seluruhnya -semakin -sementara -sempat -semua -semuanya -sendiri -sendirinya -seolah -seperti -sepertinya -sering -seringnya -serta -siapa -siapakah -siapapun -disini -disinilah -sini -sinilah -sesuatu -sesuatunya -suatu -sesudah -sesudahnya -sudah -sudahkah -sudahlah -supaya -tadi -tadinya -tak -tanpa -setelah -telah -tentang -tentu -tentulah -tentunya -tertentu -seterusnya -tapi -tetapi -setiap -tiap -setidaknya -tidak -tidakkah -tidaklah -toh -waduh -wah -wahai -sewaktu -walau -walaupun -wong -yaitu -yakni -yang diff --git a/resources/solr/lang/stopwords_it.txt b/resources/solr/lang/stopwords_it.txt deleted file mode 100644 index c74160e28c..0000000000 --- a/resources/solr/lang/stopwords_it.txt +++ /dev/null @@ -1,303 +0,0 @@ - | From https://snowballstem.org/algorithms/italian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | An Italian stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - -ad | a (to) before vowel -al | a + il -allo | a + lo -ai | a + i -agli | a + gli -all | a + l' -agl | a + gl' -alla | a + la -alle | a + le -con | with -col | con + il -coi | con + i (forms collo, cogli etc are now very rare) -da | from -dal | da + il -dallo | da + lo -dai | da + i -dagli | da + gli -dall | da + l' -dagl | da + gll' -dalla | da + la -dalle | da + le -di | of -del | di + il -dello | di + lo -dei | di + i -degli | di + gli -dell | di + l' -degl | di + gl' -della | di + la -delle | di + le -in | in -nel | in + el -nello | in + lo -nei | in + i -negli | in + gli -nell | in + l' -negl | in + gl' -nella | in + la -nelle | in + le -su | on -sul | su + il -sullo | su + lo -sui | su + i -sugli | su + gli -sull | su + l' -sugl | su + gl' -sulla | su + la -sulle | su + le -per | through, by -tra | among -contro | against -io | I -tu | thou -lui | he -lei | she -noi | we -voi | you -loro | they -mio | my -mia | -miei | -mie | -tuo | -tua | -tuoi | thy -tue | -suo | -sua | -suoi | his, her -sue | -nostro | our -nostra | -nostri | -nostre | -vostro | your -vostra | -vostri | -vostre | -mi | me -ti | thee -ci | us, there -vi | you, there -lo | him, the -la | her, the -li | them -le | them, the -gli | to him, the -ne | from there etc -il | the -un | a -uno | a -una | a -ma | but -ed | and -se | if -perché | why, because -anche | also -come | how -dov | where (as dov') -dove | where -che | who, that -chi | who -cui | whom -non | not -più | more -quale | who, that -quanto | how much -quanti | -quanta | -quante | -quello | that -quelli | -quella | -quelle | -questo | this -questi | -questa | -queste | -si | yes -tutto | all -tutti | all - - | single letter forms: - -a | at -c | as c' for ce or ci -e | and -i | the -l | as l' -o | or - - | forms of avere, to have (not including the infinitive): - -ho -hai -ha -abbiamo -avete -hanno -abbia -abbiate -abbiano -avrò -avrai -avrà -avremo -avrete -avranno -avrei -avresti -avrebbe -avremmo -avreste -avrebbero -avevo -avevi -aveva -avevamo -avevate -avevano -ebbi -avesti -ebbe -avemmo -aveste -ebbero -avessi -avesse -avessimo -avessero -avendo -avuto -avuta -avuti -avute - - | forms of essere, to be (not including the infinitive): -sono -sei -è -siamo -siete -sia -siate -siano -sarò -sarai -sarà -saremo -sarete -saranno -sarei -saresti -sarebbe -saremmo -sareste -sarebbero -ero -eri -era -eravamo -eravate -erano -fui -fosti -fu -fummo -foste -furono -fossi -fosse -fossimo -fossero -essendo - - | forms of fare, to do (not including the infinitive, fa, fat-): -faccio -fai -facciamo -fanno -faccia -facciate -facciano -farò -farai -farà -faremo -farete -faranno -farei -faresti -farebbe -faremmo -fareste -farebbero -facevo -facevi -faceva -facevamo -facevate -facevano -feci -facesti -fece -facemmo -faceste -fecero -facessi -facesse -facessimo -facessero -facendo - - | forms of stare, to be (not including the infinitive): -sto -stai -sta -stiamo -stanno -stia -stiate -stiano -starò -starai -starà -staremo -starete -staranno -starei -staresti -starebbe -staremmo -stareste -starebbero -stavo -stavi -stava -stavamo -stavate -stavano -stetti -stesti -stette -stemmo -steste -stettero -stessi -stesse -stessimo -stessero -stando diff --git a/resources/solr/lang/stopwords_ja.txt b/resources/solr/lang/stopwords_ja.txt deleted file mode 100644 index d4321be6b1..0000000000 --- a/resources/solr/lang/stopwords_ja.txt +++ /dev/null @@ -1,127 +0,0 @@ -# -# This file defines a stopword set for Japanese. -# -# This set is made up of hand-picked frequent terms from segmented Japanese Wikipedia. -# Punctuation characters and frequent kanji have mostly been left out. See LUCENE-3745 -# for frequency lists, etc. that can be useful for making your own set (if desired) -# -# Note that there is an overlap between these stopwords and the terms stopped when used -# in combination with the JapanesePartOfSpeechStopFilter. When editing this file, note -# that comments are not allowed on the same line as stopwords. -# -# Also note that stopping is done in a case-insensitive manner. Change your StopFilter -# configuration if you need case-sensitive stopping. Lastly, note that stopping is done -# using the same character width as the entries in this file. Since this StopFilter is -# normally done after a CJKWidthFilter in your chain, you would usually want your romaji -# entries to be in half-width and your kana entries to be in full-width. -# -の -に -は -を -た -が -で -て -と -し -れ -さ -ある -いる -も -する -から -な -こと -として -い -や -れる -など -なっ -ない -この -ため -その -あっ -よう -また -もの -という -あり -まで -られ -なる -へ -か -だ -これ -によって -により -おり -より -による -ず -なり -られる -において -ば -なかっ -なく -しかし -について -せ -だっ -その後 -できる -それ -う -ので -なお -のみ -でき -き -つ -における -および -いう -さらに -でも -ら -たり -その他 -に関する -たち -ます -ん -なら -に対して -特に -せる -及び -これら -とき -では -にて -ほか -ながら -うち -そして -とともに -ただし -かつて -それぞれ -または -お -ほど -ものの -に対する -ほとんど -と共に -といった -です -とも -ところ -ここ -##### End of file diff --git a/resources/solr/lang/stopwords_lv.txt b/resources/solr/lang/stopwords_lv.txt deleted file mode 100644 index e21a23c06c..0000000000 --- a/resources/solr/lang/stopwords_lv.txt +++ /dev/null @@ -1,172 +0,0 @@ -# Set of Latvian stopwords from A Stemming Algorithm for Latvian, Karlis Kreslins -# the original list of over 800 forms was refined: -# pronouns, adverbs, interjections were removed -# -# prepositions -aiz -ap -ar -apakš -ārpus -augšpus -bez -caur -dēļ -gar -iekš -iz -kopš -labad -lejpus -līdz -no -otrpus -pa -par -pār -pēc -pie -pirms -pret -priekš -starp -šaipus -uz -viņpus -virs -virspus -zem -apakšpus -# Conjunctions -un -bet -jo -ja -ka -lai -tomēr -tikko -turpretī -arī -kaut -gan -tādēļ -tā -ne -tikvien -vien -kā -ir -te -vai -kamēr -# Particles -ar -diezin -droši -diemžēl -nebūt -ik -it -taču -nu -pat -tiklab -iekšpus -nedz -tik -nevis -turpretim -jeb -iekam -iekām -iekāms -kolīdz -līdzko -tiklīdz -jebšu -tālab -tāpēc -nekā -itin -jā -jau -jel -nē -nezin -tad -tikai -vis -tak -iekams -vien -# modal verbs -būt -biju -biji -bija -bijām -bijāt -esmu -esi -esam -esat -būšu -būsi -būs -būsim -būsiet -tikt -tiku -tiki -tika -tikām -tikāt -tieku -tiec -tiek -tiekam -tiekat -tikšu -tiks -tiksim -tiksiet -tapt -tapi -tapāt -topat -tapšu -tapsi -taps -tapsim -tapsiet -kļūt -kļuvu -kļuvi -kļuva -kļuvām -kļuvāt -kļūstu -kļūsti -kļūst -kļūstam -kļūstat -kļūšu -kļūsi -kļūs -kļūsim -kļūsiet -# verbs -varēt -varēju -varējām -varēšu -varēsim -var -varēji -varējāt -varēsi -varēsiet -varat -varēja -varēs diff --git a/resources/solr/lang/stopwords_nl.txt b/resources/solr/lang/stopwords_nl.txt deleted file mode 100644 index 48c5515123..0000000000 --- a/resources/solr/lang/stopwords_nl.txt +++ /dev/null @@ -1,121 +0,0 @@ - | From https://snowballstem.org/algorithms/dutch/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - - | A Dutch stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large sample of Dutch text. - - | Dutch stop words frequently exhibit homonym clashes. These are indicated - | clearly below. - -de | the -en | and -van | of, from -ik | I, the ego -te | (1) chez, at etc, (2) to, (3) too -dat | that, which -die | that, those, who, which -in | in, inside -een | a, an, one -hij | he -het | the, it -niet | not, nothing, naught -zijn | (1) to be, being, (2) his, one's, its -is | is -was | (1) was, past tense of all persons sing. of 'zijn' (to be) (2) wax, (3) the washing, (4) rise of river -op | on, upon, at, in, up, used up -aan | on, upon, to (as dative) -met | with, by -als | like, such as, when -voor | (1) before, in front of, (2) furrow -had | had, past tense all persons sing. of 'hebben' (have) -er | there -maar | but, only -om | round, about, for etc -hem | him -dan | then -zou | should/would, past tense all persons sing. of 'zullen' -of | or, whether, if -wat | what, something, anything -mijn | possessive and noun 'mine' -men | people, 'one' -dit | this -zo | so, thus, in this way -door | through by -over | over, across -ze | she, her, they, them -zich | oneself -bij | (1) a bee, (2) by, near, at -ook | also, too -tot | till, until -je | you -mij | me -uit | out of, from -der | Old Dutch form of 'van der' still found in surnames -daar | (1) there, (2) because -haar | (1) her, their, them, (2) hair -naar | (1) unpleasant, unwell etc, (2) towards, (3) as -heb | present first person sing. of 'to have' -hoe | how, why -heeft | present third person sing. of 'to have' -hebben | 'to have' and various parts thereof -deze | this -u | you -want | (1) for, (2) mitten, (3) rigging -nog | yet, still -zal | 'shall', first and third person sing. of verb 'zullen' (will) -me | me -zij | she, they -nu | now -ge | 'thou', still used in Belgium and south Netherlands -geen | none -omdat | because -iets | something, somewhat -worden | to become, grow, get -toch | yet, still -al | all, every, each -waren | (1) 'were' (2) to wander, (3) wares, (3) -veel | much, many -meer | (1) more, (2) lake -doen | to do, to make -toen | then, when -moet | noun 'spot/mote' and present form of 'to must' -ben | (1) am, (2) 'are' in interrogative second person singular of 'to be' -zonder | without -kan | noun 'can' and present form of 'to be able' -hun | their, them -dus | so, consequently -alles | all, everything, anything -onder | under, beneath -ja | yes, of course -eens | once, one day -hier | here -wie | who -werd | imperfect third person sing. of 'become' -altijd | always -doch | yet, but etc -wordt | present third person sing. of 'become' -wezen | (1) to be, (2) 'been' as in 'been fishing', (3) orphans -kunnen | to be able -ons | us/our -zelf | self -tegen | against, towards, at -na | after, near -reeds | already -wil | (1) present tense of 'want', (2) 'will', noun, (3) fender -kon | could; past tense of 'to be able' -niets | nothing -uw | your -iemand | somebody -geweest | been; past participle of 'be' -andere | other - diff --git a/resources/solr/lang/stopwords_no.txt b/resources/solr/lang/stopwords_no.txt deleted file mode 100644 index f427609484..0000000000 --- a/resources/solr/lang/stopwords_no.txt +++ /dev/null @@ -1,190 +0,0 @@ - | From https://snowballstem.org/algorithms/norwegian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Norwegian stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This stop word list is for the dominant bokmål dialect. Words unique - | to nynorsk are marked *. - - | Revised by Jan Bruusgaard , Jan 2005 - -og | and -i | in -jeg | I -det | it/this/that -at | to (w. inf.) -en | a/an -et | a/an -den | it/this/that -til | to -er | is/am/are -som | who/which/that -på | on -de | they / you(formal) -med | with -han | he -av | of -ikke | not -ikkje | not * -der | there -så | so -var | was/were -meg | me -seg | you -men | but -ett | one -har | have -om | about -vi | we -min | my -mitt | my -ha | have -hadde | had -hun | she -nå | now -over | over -da | when/as -ved | by/know -fra | from -du | you -ut | out -sin | your -dem | them -oss | us -opp | up -man | you/one -kan | can -hans | his -hvor | where -eller | or -hva | what -skal | shall/must -selv | self (reflective) -sjøl | self (reflective) -her | here -alle | all -vil | will -bli | become -ble | became -blei | became * -blitt | have become -kunne | could -inn | in -når | when -være | be -kom | come -noen | some -noe | some -ville | would -dere | you -deres | their/theirs -kun | only/just -ja | yes -etter | after -ned | down -skulle | should -denne | this -for | for/because -deg | you -si | hers/his -sine | hers/his -sitt | hers/his -mot | against -å | to -meget | much -hvorfor | why -dette | this -disse | these/those -uten | without -hvordan | how -ingen | none -din | your -ditt | your -blir | become -samme | same -hvilken | which -hvilke | which (plural) -sånn | such a -inni | inside/within -mellom | between -vår | our -hver | each -hvem | who -vors | us/ours -hvis | whose -både | both -bare | only/just -enn | than -fordi | as/because -før | before -mange | many -også | also -slik | just -vært | been -båe | both * -begge | both -siden | since -dykk | your * -dykkar | yours * -dei | they * -deira | them * -deires | theirs * -deim | them * -di | your (fem.) * -då | as/when * -eg | I * -ein | a/an * -eit | a/an * -eitt | a/an * -elles | or * -honom | he * -hjå | at * -ho | she * -hoe | she * -henne | her -hennar | her/hers -hennes | hers -hoss | how * -hossen | how * -ingi | noone * -inkje | noone * -korleis | how * -korso | how * -kva | what/which * -kvar | where * -kvarhelst | where * -kven | who/whom * -kvi | why * -kvifor | why * -me | we * -medan | while * -mi | my * -mine | my * -mykje | much * -no | now * -nokon | some (masc./neut.) * -noka | some (fem.) * -nokor | some * -noko | some * -nokre | some * -sia | since * -sidan | since * -so | so * -somt | some * -somme | some * -um | about* -upp | up * -vere | be * -vore | was * -verte | become * -vort | become * -varte | became * -vart | became * - diff --git a/resources/solr/lang/stopwords_pt.txt b/resources/solr/lang/stopwords_pt.txt deleted file mode 100644 index d03d7f234d..0000000000 --- a/resources/solr/lang/stopwords_pt.txt +++ /dev/null @@ -1,253 +0,0 @@ - | From https://snowballstem.org/algorithms/portuguese/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Portuguese stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - - | The following is a ranked list (commonest to rarest) of stopwords - | deriving from a large sample of text. - - | Extra words have been added at the end. - -de | of, from -a | the; to, at; her -o | the; him -que | who, that -e | and -do | de + o -da | de + a -em | in -um | a -para | for - | é from SER -com | with -não | not, no -uma | a -os | the; them -no | em + o -se | himself etc -na | em + a -por | for -mais | more -as | the; them -dos | de + os -como | as, like -mas | but - | foi from SER -ao | a + o -ele | he -das | de + as - | tem from TER -à | a + a -seu | his -sua | her -ou | or - | ser from SER -quando | when -muito | much - | há from HAV -nos | em + os; us -já | already, now - | está from EST -eu | I -também | also -só | only, just -pelo | per + o -pela | per + a -até | up to -isso | that -ela | he -entre | between - | era from SER -depois | after -sem | without -mesmo | same -aos | a + os - | ter from TER -seus | his -quem | whom -nas | em + as -me | me -esse | that -eles | they - | estão from EST -você | you - | tinha from TER - | foram from SER -essa | that -num | em + um -nem | nor -suas | her -meu | my -às | a + as -minha | my - | têm from TER -numa | em + uma -pelos | per + os -elas | they - | havia from HAV - | seja from SER -qual | which - | será from SER -nós | we - | tenho from TER -lhe | to him, her -deles | of them -essas | those -esses | those -pelas | per + as -este | this - | fosse from SER -dele | of him - - | other words. There are many contractions such as naquele = em+aquele, - | mo = me+o, but they are rare. - | Indefinite article plural forms are also rare. - -tu | thou -te | thee -vocês | you (plural) -vos | you -lhes | to them -meus | my -minhas -teu | thy -tua -teus -tuas -nosso | our -nossa -nossos -nossas - -dela | of her -delas | of them - -esta | this -estes | these -estas | these -aquele | that -aquela | that -aqueles | those -aquelas | those -isto | this -aquilo | that - - | forms of estar, to be (not including the infinitive): -estou -está -estamos -estão -estive -esteve -estivemos -estiveram -estava -estávamos -estavam -estivera -estivéramos -esteja -estejamos -estejam -estivesse -estivéssemos -estivessem -estiver -estivermos -estiverem - - | forms of haver, to have (not including the infinitive): -hei -há -havemos -hão -houve -houvemos -houveram -houvera -houvéramos -haja -hajamos -hajam -houvesse -houvéssemos -houvessem -houver -houvermos -houverem -houverei -houverá -houveremos -houverão -houveria -houveríamos -houveriam - - | forms of ser, to be (not including the infinitive): -sou -somos -são -era -éramos -eram -fui -foi -fomos -foram -fora -fôramos -seja -sejamos -sejam -fosse -fôssemos -fossem -for -formos -forem -serei -será -seremos -serão -seria -seríamos -seriam - - | forms of ter, to have (not including the infinitive): -tenho -tem -temos -tém -tinha -tínhamos -tinham -tive -teve -tivemos -tiveram -tivera -tivéramos -tenha -tenhamos -tenham -tivesse -tivéssemos -tivessem -tiver -tivermos -tiverem -terei -terá -teremos -terão -teria -teríamos -teriam diff --git a/resources/solr/lang/stopwords_ro.txt b/resources/solr/lang/stopwords_ro.txt deleted file mode 100644 index 4fdee90a5b..0000000000 --- a/resources/solr/lang/stopwords_ro.txt +++ /dev/null @@ -1,233 +0,0 @@ -# This file was created by Jacques Savoy and is distributed under the BSD license. -# See http://members.unine.ch/jacques.savoy/clef/index.html. -# Also see http://www.opensource.org/licenses/bsd-license.html -acea -aceasta -această -aceea -acei -aceia -acel -acela -acele -acelea -acest -acesta -aceste -acestea -aceşti -aceştia -acolo -acum -ai -aia -aibă -aici -al -ăla -ale -alea -ălea -altceva -altcineva -am -ar -are -aş -aşadar -asemenea -asta -ăsta -astăzi -astea -ăstea -ăştia -asupra -aţi -au -avea -avem -aveţi -azi -bine -bucur -bună -ca -că -căci -când -care -cărei -căror -cărui -cât -câte -câţi -către -câtva -ce -cel -ceva -chiar -cînd -cine -cineva -cît -cîte -cîţi -cîtva -contra -cu -cum -cumva -curând -curînd -da -dă -dacă -dar -datorită -de -deci -deja -deoarece -departe -deşi -din -dinaintea -dintr -dintre -drept -după -ea -ei -el -ele -eram -este -eşti -eu -face -fără -fi -fie -fiecare -fii -fim -fiţi -iar -ieri -îi -îl -îmi -împotriva -în -înainte -înaintea -încât -încît -încotro -între -întrucât -întrucît -îţi -la -lângă -le -li -lîngă -lor -lui -mă -mâine -mea -mei -mele -mereu -meu -mi -mine -mult -multă -mulţi -ne -nicăieri -nici -nimeni -nişte -noastră -noastre -noi -noştri -nostru -nu -ori -oricând -oricare -oricât -orice -oricînd -oricine -oricît -oricum -oriunde -până -pe -pentru -peste -pînă -poate -pot -prea -prima -primul -prin -printr -sa -să -săi -sale -sau -său -se -şi -sînt -sîntem -sînteţi -spre -sub -sunt -suntem -sunteţi -ta -tăi -tale -tău -te -ţi -ţie -tine -toată -toate -tot -toţi -totuşi -tu -un -una -unde -undeva -unei -unele -uneori -unor -vă -vi -voastră -voastre -voi -voştri -vostru -vouă -vreo -vreun diff --git a/resources/solr/lang/stopwords_ru.txt b/resources/solr/lang/stopwords_ru.txt deleted file mode 100644 index 65512d49db..0000000000 --- a/resources/solr/lang/stopwords_ru.txt +++ /dev/null @@ -1,244 +0,0 @@ - | From https://snowballstem.org/algorithms/russian/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - - | a russian stop word list. comments begin with vertical bar. each stop - | word is at the start of a line. - - | this is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - | letter `ё' is translated to `е'. - -и | and -в | in/into -во | alternative form -не | not -что | what/that -он | he -на | on/onto -я | i -с | from -со | alternative form -как | how -а | milder form of `no' (but) -то | conjunction and form of `that' -все | all -она | she -так | so, thus -его | him -но | but -да | yes/and -ты | thou -к | towards, by -у | around, chez -же | intensifier particle -вы | you -за | beyond, behind -бы | conditional/subj. particle -по | up to, along -только | only -ее | her -мне | to me -было | it was -вот | here is/are, particle -от | away from -меня | me -еще | still, yet, more -нет | no, there isnt/arent -о | about -из | out of -ему | to him -теперь | now -когда | when -даже | even -ну | so, well -вдруг | suddenly -ли | interrogative particle -если | if -уже | already, but homonym of `narrower' -или | or -ни | neither -быть | to be -был | he was -него | prepositional form of его -до | up to -вас | you accusative -нибудь | indef. suffix preceded by hyphen -опять | again -уж | already, but homonym of `adder' -вам | to you -сказал | he said -ведь | particle `after all' -там | there -потом | then -себя | oneself -ничего | nothing -ей | to her -может | usually with `быть' as `maybe' -они | they -тут | here -где | where -есть | there is/are -надо | got to, must -ней | prepositional form of ей -для | for -мы | we -тебя | thee -их | them, their -чем | than -была | she was -сам | self -чтоб | in order to -без | without -будто | as if -человек | man, person, one -чего | genitive form of `what' -раз | once -тоже | also -себе | to oneself -под | beneath -жизнь | life -будет | will be -ж | short form of intensifer particle `же' -тогда | then -кто | who -этот | this -говорил | was saying -того | genitive form of `that' -потому | for that reason -этого | genitive form of `this' -какой | which -совсем | altogether -ним | prepositional form of `его', `они' -здесь | here -этом | prepositional form of `этот' -один | one -почти | almost -мой | my -тем | instrumental/dative plural of `тот', `то' -чтобы | full form of `in order that' -нее | her (acc.) -кажется | it seems -сейчас | now -были | they were -куда | where to -зачем | why -сказать | to say -всех | all (acc., gen. preposn. plural) -никогда | never -сегодня | today -можно | possible, one can -при | by -наконец | finally -два | two -об | alternative form of `о', about -другой | another -хоть | even -после | after -над | above -больше | more -тот | that one (masc.) -через | across, in -эти | these -нас | us -про | about -всего | in all, only, of all -них | prepositional form of `они' (they) -какая | which, feminine -много | lots -разве | interrogative particle -сказала | she said -три | three -эту | this, acc. fem. sing. -моя | my, feminine -впрочем | moreover, besides -хорошо | good -свою | ones own, acc. fem. sing. -этой | oblique form of `эта', fem. `this' -перед | in front of -иногда | sometimes -лучше | better -чуть | a little -том | preposn. form of `that one' -нельзя | one must not -такой | such a one -им | to them -более | more -всегда | always -конечно | of course -всю | acc. fem. sing of `all' -между | between - - - | b: some paradigms - | - | personal pronouns - | - | я меня мне мной [мною] - | ты тебя тебе тобой [тобою] - | он его ему им [него, нему, ним] - | она ее эи ею [нее, нэи, нею] - | оно его ему им [него, нему, ним] - | - | мы нас нам нами - | вы вас вам вами - | они их им ими [них, ним, ними] - | - | себя себе собой [собою] - | - | demonstrative pronouns: этот (this), тот (that) - | - | этот эта это эти - | этого эты это эти - | этого этой этого этих - | этому этой этому этим - | этим этой этим [этою] этими - | этом этой этом этих - | - | тот та то те - | того ту то те - | того той того тех - | тому той тому тем - | тем той тем [тою] теми - | том той том тех - | - | determinative pronouns - | - | (a) весь (all) - | - | весь вся все все - | всего всю все все - | всего всей всего всех - | всему всей всему всем - | всем всей всем [всею] всеми - | всем всей всем всех - | - | (b) сам (himself etc) - | - | сам сама само сами - | самого саму само самих - | самого самой самого самих - | самому самой самому самим - | самим самой самим [самою] самими - | самом самой самом самих - | - | stems of verbs `to be', `to have', `to do' and modal - | - | быть бы буд быв есть суть - | име - | дел - | мог мож мочь - | уме - | хоч хот - | долж - | можн - | нужн - | нельзя - diff --git a/resources/solr/lang/stopwords_sv.txt b/resources/solr/lang/stopwords_sv.txt deleted file mode 100644 index d1d0d10088..0000000000 --- a/resources/solr/lang/stopwords_sv.txt +++ /dev/null @@ -1,133 +0,0 @@ - | From https://snowballstem.org/algorithms/swedish/stop.txt - | This file is distributed under the BSD License. - | See https://snowballstem.org/license.html - | Also see https://opensource.org/licenses/bsd-license.html - | - Encoding was converted to UTF-8. - | - This notice was added. - | - | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" - - | A Swedish stop word list. Comments begin with vertical bar. Each stop - | word is at the start of a line. - - | This is a ranked list (commonest to rarest) of stopwords derived from - | a large text sample. - - | Swedish stop words occasionally exhibit homonym clashes. For example - | så = so, but also seed. These are indicated clearly below. - -och | and -det | it, this/that -att | to (with infinitive) -i | in, at -en | a -jag | I -hon | she -som | who, that -han | he -på | on -den | it, this/that -med | with -var | where, each -sig | him(self) etc -för | for -så | so (also: seed) -till | to -är | is -men | but -ett | a -om | if; around, about -hade | had -de | they, these/those -av | of -icke | not, no -mig | me -du | you -henne | her -då | then, when -sin | his -nu | now -har | have -inte | inte någon = no one -hans | his -honom | him -skulle | 'sake' -hennes | her -där | there -min | my -man | one (pronoun) -ej | nor -vid | at, by, on (also: vast) -kunde | could -något | some etc -från | from, off -ut | out -när | when -efter | after, behind -upp | up -vi | we -dem | them -vara | be -vad | what -över | over -än | than -dig | you -kan | can -sina | his -här | here -ha | have -mot | towards -alla | all -under | under (also: wonder) -någon | some etc -eller | or (else) -allt | all -mycket | much -sedan | since -ju | why -denna | this/that -själv | myself, yourself etc -detta | this/that -åt | to -utan | without -varit | was -hur | how -ingen | no -mitt | my -ni | you -bli | to be, become -blev | from bli -oss | us -din | thy -dessa | these/those -några | some etc -deras | their -blir | from bli -mina | my -samma | (the) same -vilken | who, that -er | you, your -sådan | such a -vår | our -blivit | from bli -dess | its -inom | within -mellan | between -sådant | such a -varför | why -varje | each -vilka | who, that -ditt | thy -vem | who -vilket | who, that -sitt | his -sådana | such a -vart | each -dina | thy -vars | whose -vårt | our -våra | our -ert | your -era | your -vilkas | whose - diff --git a/resources/solr/lang/stopwords_th.txt b/resources/solr/lang/stopwords_th.txt deleted file mode 100644 index 07f0fabe69..0000000000 --- a/resources/solr/lang/stopwords_th.txt +++ /dev/null @@ -1,119 +0,0 @@ -# Thai stopwords from: -# "Opinion Detection in Thai Political News Columns -# Based on Subjectivity Analysis" -# Khampol Sukhum, Supot Nitsuwat, and Choochart Haruechaiyasak -ไว้ -ไม่ -ไป -ได้ -ให้ -ใน -โดย -แห่ง -แล้ว -และ -แรก -แบบ -แต่ -เอง -เห็น -เลย -เริ่ม -เรา -เมื่อ -เพื่อ -เพราะ -เป็นการ -เป็น -เปิดเผย -เปิด -เนื่องจาก -เดียวกัน -เดียว -เช่น -เฉพาะ -เคย -เข้า -เขา -อีก -อาจ -อะไร -ออก -อย่าง -อยู่ -อยาก -หาก -หลาย -หลังจาก -หลัง -หรือ -หนึ่ง -ส่วน -ส่ง -สุด -สําหรับ -ว่า -วัน -ลง -ร่วม -ราย -รับ -ระหว่าง -รวม -ยัง -มี -มาก -มา -พร้อม -พบ -ผ่าน -ผล -บาง -น่า -นี้ -นํา -นั้น -นัก -นอกจาก -ทุก -ที่สุด -ที่ -ทําให้ -ทํา -ทาง -ทั้งนี้ -ทั้ง -ถ้า -ถูก -ถึง -ต้อง -ต่างๆ -ต่าง -ต่อ -ตาม -ตั้งแต่ -ตั้ง -ด้าน -ด้วย -ดัง -ซึ่ง -ช่วง -จึง -จาก -จัด -จะ -คือ -ความ -ครั้ง -คง -ขึ้น -ของ -ขอ -ขณะ -ก่อน -ก็ -การ -กับ -กัน -กว่า -กล่าว diff --git a/resources/solr/lang/stopwords_tr.txt b/resources/solr/lang/stopwords_tr.txt deleted file mode 100644 index 84d9408d4e..0000000000 --- a/resources/solr/lang/stopwords_tr.txt +++ /dev/null @@ -1,212 +0,0 @@ -# Turkish stopwords from LUCENE-559 -# merged with the list from "Information Retrieval on Turkish Texts" -# (http://www.users.muohio.edu/canf/papers/JASIST2008offPrint.pdf) -acaba -altmış -altı -ama -ancak -arada -aslında -ayrıca -bana -bazı -belki -ben -benden -beni -benim -beri -beş -bile -bin -bir -birçok -biri -birkaç -birkez -birşey -birşeyi -biz -bize -bizden -bizi -bizim -böyle -böylece -bu -buna -bunda -bundan -bunlar -bunları -bunların -bunu -bunun -burada -çok -çünkü -da -daha -dahi -de -defa -değil -diğer -diye -doksan -dokuz -dolayı -dolayısıyla -dört -edecek -eden -ederek -edilecek -ediliyor -edilmesi -ediyor -eğer -elli -en -etmesi -etti -ettiği -ettiğini -gibi -göre -halen -hangi -hatta -hem -henüz -hep -hepsi -her -herhangi -herkesin -hiç -hiçbir -için -iki -ile -ilgili -ise -işte -itibaren -itibariyle -kadar -karşın -katrilyon -kendi -kendilerine -kendini -kendisi -kendisine -kendisini -kez -ki -kim -kimden -kime -kimi -kimse -kırk -milyar -milyon -mu -mü -mı -nasıl -ne -neden -nedenle -nerde -nerede -nereye -niye -niçin -o -olan -olarak -oldu -olduğu -olduğunu -olduklarını -olmadı -olmadığı -olmak -olması -olmayan -olmaz -olsa -olsun -olup -olur -olursa -oluyor -on -ona -ondan -onlar -onlardan -onları -onların -onu -onun -otuz -oysa -öyle -pek -rağmen -sadece -sanki -sekiz -seksen -sen -senden -seni -senin -siz -sizden -sizi -sizin -şey -şeyden -şeyi -şeyler -şöyle -şu -şuna -şunda -şundan -şunları -şunu -tarafından -trilyon -tüm -üç -üzere -var -vardı -ve -veya -ya -yani -yapacak -yapılan -yapılması -yapıyor -yapmak -yaptı -yaptığı -yaptığını -yaptıkları -yedi -yerine -yetmiş -yine -yirmi -yoksa -yüz -zaten diff --git a/resources/solr/lang/userdict_ja.txt b/resources/solr/lang/userdict_ja.txt deleted file mode 100644 index 6f0368e4d8..0000000000 --- a/resources/solr/lang/userdict_ja.txt +++ /dev/null @@ -1,29 +0,0 @@ -# -# This is a sample user dictionary for Kuromoji (JapaneseTokenizer) -# -# Add entries to this file in order to override the statistical model in terms -# of segmentation, readings and part-of-speech tags. Notice that entries do -# not have weights since they are always used when found. This is by-design -# in order to maximize ease-of-use. -# -# Entries are defined using the following CSV format: -# , ... , ... , -# -# Notice that a single half-width space separates tokens and readings, and -# that the number tokens and readings must match exactly. -# -# Also notice that multiple entries with the same is undefined. -# -# Whitespace only lines are ignored. Comments are not allowed on entry lines. -# - -# Custom segmentation for kanji compounds -日本経済新聞,日本 経済 新聞,ニホン ケイザイ シンブン,カスタム名詞 -関西国際空港,関西 国際 空港,カンサイ コクサイ クウコウ,カスタム名詞 - -# Custom segmentation for compound katakana -トートバッグ,トート バッグ,トート バッグ,かずカナ名詞 -ショルダーバッグ,ショルダー バッグ,ショルダー バッグ,かずカナ名詞 - -# Custom reading for former sumo wrestler -朝青龍,朝青龍,アサショウリュウ,カスタム人名 diff --git a/resources/solr/managed-schema.xml b/resources/solr/managed-schema.xml deleted file mode 100644 index 0b953e7061..0000000000 --- a/resources/solr/managed-schema.xml +++ /dev/null @@ -1,1075 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - id - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/solr/minimal-configset/managed-schema.xml b/resources/solr/minimal-configset/managed-schema.xml deleted file mode 100644 index aef3edaf23..0000000000 --- a/resources/solr/minimal-configset/managed-schema.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - id - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/solr/minimal-configset/protwords.txt b/resources/solr/minimal-configset/protwords.txt deleted file mode 100644 index 88965604e8..0000000000 --- a/resources/solr/minimal-configset/protwords.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Protected words that should not be stemmed -# One word per line diff --git a/resources/solr/minimal-configset/solrconfig.xml b/resources/solr/minimal-configset/solrconfig.xml deleted file mode 100644 index 816c7dc2d8..0000000000 --- a/resources/solr/minimal-configset/solrconfig.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - 9.0.0 - - - ${solr.data.dir:} - - - - ${solr.lock.type:native} - - - - - - ${solr.ulog.dir:} - ${solr.ulog.numVersionBuckets:65536} - - - ${solr.autoCommit.maxTime:15000} - false - - - ${solr.autoSoftCommit.maxTime:1000} - - - - - - - explicit - 10 - - - - - - - - solrpingquery - - - all - - - - - - - id - - - - [^\w-\.] - _ - - - - - - - yyyy-MM-dd'T'HH:mm:ss.SSSZ - yyyy-MM-dd'T'HH:mm:ss,SSSZ - yyyy-MM-dd'T'HH:mm:ss.SSS - yyyy-MM-dd'T'HH:mm:ss,SSS - yyyy-MM-dd'T'HH:mm:ssZ - yyyy-MM-dd'T'HH:mm:ss - yyyy-MM-dd HH:mm:ss.SSSZ - yyyy-MM-dd HH:mm:ss,SSSZ - yyyy-MM-dd HH:mm:ss.SSS - yyyy-MM-dd HH:mm:ss,SSS - yyyy-MM-dd HH:mm:ssZ - yyyy-MM-dd HH:mm:ss - yyyy-MM-dd - - - - strings - - java.lang.String - strings - - - java.lang.Boolean - booleans - - - java.util.Date - pdates - - - java.lang.Long - plongs - - - java.lang.Integer - pints - - - java.lang.Float - pfloats - - - java.lang.Double - pdoubles - - - - - - - diff --git a/resources/solr/minimal-configset/stopwords.txt b/resources/solr/minimal-configset/stopwords.txt deleted file mode 100644 index 6c313336b3..0000000000 --- a/resources/solr/minimal-configset/stopwords.txt +++ /dev/null @@ -1,34 +0,0 @@ -# Standard English stopwords -a -an -and -are -as -at -be -but -by -for -if -in -into -is -it -no -not -of -on -or -such -that -the -their -then -there -these -they -this -to -was -will -with diff --git a/resources/solr/minimal-configset/synonyms.txt b/resources/solr/minimal-configset/synonyms.txt deleted file mode 100644 index a0e9d4b58d..0000000000 --- a/resources/solr/minimal-configset/synonyms.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Example synonyms for OpenRegister -# Format: word1,word2,word3 -# or: word1 => word2,word3 - -# Basic synonyms -app,application,software -doc,document,file -org,organization,organisation -user,person,individual diff --git a/resources/solr/openregister-clean-configset.zip b/resources/solr/openregister-clean-configset.zip deleted file mode 100644 index 3f14cbadc2..0000000000 Binary files a/resources/solr/openregister-clean-configset.zip and /dev/null differ diff --git a/resources/solr/openregister-configset-fixed.zip b/resources/solr/openregister-configset-fixed.zip deleted file mode 100644 index c38810f8c6..0000000000 Binary files a/resources/solr/openregister-configset-fixed.zip and /dev/null differ diff --git a/resources/solr/openregister-configset.zip b/resources/solr/openregister-configset.zip deleted file mode 100644 index 3f14cbadc2..0000000000 Binary files a/resources/solr/openregister-configset.zip and /dev/null differ diff --git a/resources/solr/openregister-minimal-configset.zip b/resources/solr/openregister-minimal-configset.zip deleted file mode 100644 index 8c5fe917cd..0000000000 Binary files a/resources/solr/openregister-minimal-configset.zip and /dev/null differ diff --git a/resources/solr/protwords.txt b/resources/solr/protwords.txt deleted file mode 100644 index 1dfc0abecb..0000000000 --- a/resources/solr/protwords.txt +++ /dev/null @@ -1,21 +0,0 @@ -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -#----------------------------------------------------------------------- -# Use a protected word file to protect against the stemmer reducing two -# unrelated words to the same base word. - -# Some non-words that normally won't be encountered, -# just to test that they won't be stemmed. -dontstems -zwhacky - diff --git a/resources/solr/solrconfig.xml b/resources/solr/solrconfig.xml deleted file mode 100644 index 9bef924011..0000000000 --- a/resources/solr/solrconfig.xml +++ /dev/null @@ -1,1026 +0,0 @@ - - - - - - - - - 9.12 - - - ${solr.data.dir:} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ${solr.lock.type:native} - - - - - - - - - - - - - - - - - - - - - ${solr.ulog.dir:} - - - - - ${solr.autoCommit.maxTime:15000} - false - - - - - - ${solr.autoSoftCommit.maxTime:3000} - - - - - - - - - - - - - - ${solr.max.booleanClauses:1024} - - - ${solr.query.minPrefixLength:-1} - - - - - - - - - - - - - - - - - - - - - - - - true - - - - - - 20 - - - 200 - - - - - - - - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - explicit - 10 - - - - - - - explicit - json - true - - - - - - - _text_ - - - - - - - text_general - - - - - - default - _text_ - solr.DirectSolrSpellChecker - - internal - - 0.5 - - 2 - - 1 - - 5 - - 4 - - 0.01 - - - - - - - - - - - - default - on - true - 10 - 5 - 5 - true - true - 10 - 5 - - - spellcheck - - - - - - - - - - - - 100 - - - - - - - - 70 - - 0.5 - - [-\w ,/\n\"']{20,200} - - - - - - - ]]> - ]]> - - - - - - - - - - - - - - - - - - - - - - - - ,, - ,, - ,, - ,, - ,]]> - ]]> - - - - - - 10 - .,!? - - - - - - - WORD - - - en - US - - - - - - - - - - - - [^\w-\.] - _ - - - 1000 - true - - - - - - - yyyy-MM-dd['T'[HH:mm[:ss[.SSS]][z - yyyy-MM-dd['T'[HH:mm[:ss[,SSS]][z - yyyy-MM-dd HH:mm[:ss[.SSS]][z - yyyy-MM-dd HH:mm[:ss[,SSS]][z - [EEE, ]dd MMM yyyy HH:mm[:ss] z - EEEE, dd-MMM-yy HH:mm:ss z - EEE MMM ppd HH:mm:ss [z ]yyyy - - - - - java.lang.String - text_general - - *_str - 256 - - - true - - - java.lang.Boolean - booleans - - - java.util.Date - pdates - - - java.lang.Long - java.lang.Integer - plongs - - - java.lang.Number - pdoubles - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/solr/stopwords.txt b/resources/solr/stopwords.txt deleted file mode 100644 index ae1e83eeb3..0000000000 --- a/resources/solr/stopwords.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/resources/solr/synonyms.txt b/resources/solr/synonyms.txt deleted file mode 100644 index eab4ee8753..0000000000 --- a/resources/solr/synonyms.txt +++ /dev/null @@ -1,29 +0,0 @@ -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -#----------------------------------------------------------------------- -#some test synonym mappings unlikely to appear in real input text -aaafoo => aaabar -bbbfoo => bbbfoo bbbbar -cccfoo => cccbar cccbaz -fooaaa,baraaa,bazaaa - -# Some synonym groups specific to this example -GB,gib,gigabyte,gigabytes -MB,mib,megabyte,megabytes -Television, Televisions, TV, TVs -#notice we use "gib" instead of "GiB" so any WordDelimiterGraphFilter coming -#after us won't split it into two words. - -# Synonym mappings can be used for spelling correction too -pixima => pixma - diff --git a/scratchpad/authz-fail-closed-and-vocabulary-drift.md b/scratchpad/authz-fail-closed-and-vocabulary-drift.md new file mode 100644 index 0000000000..caa771db5a --- /dev/null +++ b/scratchpad/authz-fail-closed-and-vocabulary-drift.md @@ -0,0 +1,110 @@ +# authz-fail-closed-and-vocabulary-drift — report + +Tracked on openregister#439. PRs: **#441** (apply, merged), **#442** (archive, merged). +Archived to `openspec/changes/archive/2026-07-16-authz-fail-closed-and-vocabulary-drift/` (`status: done`). + +## Verify verdicts (all three CONFIRMED against HEAD) + +| # | Finding | Verdict | Evidence | +|---|---------|---------|----------| +| 1 | `getRegisterAuthorization()` fail-open, unlogged, cached | **CONFIRMED — and wider than reported** | `catch (\Throwable) { $this->cachedRegisterAuth[$id] = null; return null; }`, no logger call | +| 2 | `x-openregister-seed` in vocabulary, zero engines | **CONFIRMED** | only hit in `lib/` is the vocabulary entry itself (`Schema.php:2105`) | +| 3 | `x-openregister-processing` read but not in vocabulary | **CONFIRMED** | `ProcessingLogService::ANNOTATION_KEY`; vocabulary had only the *different* `…-processing-activity` | + +### Where the audit was imprecise +- **Finding 1 understated blast radius.** The audit said "a sibling resolver logs on the same shape; + this one doesn't" — implying the sibling was fine. It wasn't: `getRegisterForSchema()` logged a + **warning** and still returned `null` → open. **Logging a fail-open does not make it fail closed.** + Fixed too. The fail-open also reached `MagicRbacHandler` (`empty($auth)` → `bypass => true`), + so RBAC **SQL filtering** was dropped, not just the PHP verdict. 4 resolvers/callers fixed, not 1. +- **Finding 2's MDM claim root-caused.** The 6 trust rules sat at + `components.schemas.trustConfiguration.x-openregister-seed.objects`; `ImportHandler` only reads + `components.objects` / top-level `objects`. The importer existed (`ImportTrustConfigurationRegister` + runs fine) — the **seed location** was the phantom. Relocated; all 6 objects + every field preserved + (verified semantically, not by eyeballing the diff). + +## Fail-closed test proof (denied + logged + not cached) + +`tests/Unit/Service/Object/PermissionHandlerFailClosedTest.php` — 3 tests, **all fail on pre-fix code** +(reverted `PermissionHandler.php` to `origin/development` and re-ran): + +``` +1) testUnresolvableAuthorizationDeniesEveryAction + Action "read" must be DENIED when authorization cannot be resolved +2) testUnresolvableAuthorizationIsLogged + Authorization resolution failure MUST be logged, not swallowed +3) testResolutionFailureIsNotCachedAsAnAnswer +FAILURES! Tests: 3, Failures: 3 +``` +Post-fix: `OK — Tests: 3, Assertions: 9`. The non-caching test uses a mapper that throws once then +recovers, and asserts the lookup is **retried** — a cached failure would replay a transient error as a +permanent (open) verdict. + +## Seed decision: REMOVE the key (+ relocate OR's own seeds) + +Fleet scan (read-only, `git ls-files`): + +| App | Key | Count | Content | +|-----|-----|-------|---------| +| scholiq | `x-openregister-seed` | 22 | **all empty arrays** — no data at risk | +| openregister | `x-openregister-seed` | 1 | the 6 MDM trust rules — **relocated** | +| decidesk | `x-openregister-seed**s**` (plural) | 21 | a *different* key, never in the vocabulary — already dead | + +Why remove, not implement: OR **already has** a seed engine (`ImportHandler` → `components.objects`); +a second dialect is the drift this change ends. Nothing real breaks (only OR's own held data, migrated). +Removal makes future declarations **fail loudly** via the dropped-key warning (#396 anti-phantom). + +## Processing round-trip proof +`testProcessingAnnotationSurvivesRoundTripAndReachesItsEngine` asserts the key survives +`setConfiguration()` **keyed by `ProcessingLogService::ANNOTATION_KEY` itself** — i.e. the value the +**engine** reads is the value written (`logReads: true`), not merely "not dropped". + +## Root-cause insight: "not dropped" ≠ "consumed" +The contract only required a key to **round-trip**, never that anything **reads** it. +OR's own `testSeedAnnotationSurvivesRoundTrip()` was **green the entire time the 6 trust rules were +never planted**. A round-trip test on an annotation proves storage, never behaviour. Those tests are +inverted here to assert the phantom is rejected. + +## Why gate `unsafe-auth-resolver` missed it — a PATH gap, not a logic gap +The gate globs **non-recursively**: `for f in lib/Service/*.php lib/Controller/*.php`. +`PermissionHandler.php` is at `lib/Service/**Object/**PermissionHandler.php` — one level deeper, never opened. +Measured on this repo: the glob scans **227 of 1264** `lib/` PHP files — **82% unscanned**, including +`lib/Service/Object/`, `lib/Db/` and `lib/Db/MagicMapper/`. +The **detection logic would have caught this verbatim** (`getRegisterAuthorization` matches its +`[Aa]uthori[sz]ation` regex; the catch block held a bare `return null`). +**Blind spot to fix in hydra: `find lib -name '*.php'`.** Today the deeper a security-critical class +sits, the less likely it is checked — exactly backwards. Same idiom likely under-scans other gates. + +## Baseline + delta (real output, no fabrication) +Container: `or-phpunit-83-full:local` (php 8.3 + zip/bcmath/soap/xsl/intl/gd), fresh composer install, +`phpunit-unit-local.xml`, `memory_limit=2G`. (`phpunit-unit.xml` cannot run outside an NC root — fatals at bootstrap.) + +``` +BASELINE (pristine origin/development @ 04e59be30): + Tests: 14727, Assertions: 32254, Errors: 54, Failures: 18, Warnings: 8, Skipped: 22 +THIS BRANCH (merged with development): + Tests: 14735, Assertions: 32309, Errors: 54, Failures: 18, Warnings: 8, Skipped: 22 +``` ++8 tests. Errors and failures **identical**. Verified by test-**NAME** diff (`comm -13`), not counts — +**zero new failure names**. + +One regression surfaced mid-run and was fixed at root, not silenced: +`PermissionHandlerCustomScopeTest` used an unconfigured `ContainerInterface` mock (returning `null` for +`RegisterMapper`), so it **depended on the fail-open** to reach the listener path. Wired the mock +realistically. Its 2 other failures are pre-existing in baseline. + +## Gates: 37/39 pass +Both failures are **pre-existing and untouched by this diff** (proven against `origin/development`): +- **gate-46 spec-anchor-existence** — 25 `@spec` tags pointing at evaporated `openspec/changes/retrofit-*` + dirs. Baseline has **5056** fleet-wide. All of this change's anchors resolve (synced the canonical spec). +- **gate-52 orphaned-write-capability** — `clearPermissionCache` / `clearInheritFromPublicCache`, both + present verbatim on `development`; my diff doesn't touch them. + +PHPCS: **0 errors**. Psalm: **no errors**. + +## Remaining / follow-ups for #439 +1. **hydra**: make the gate globs recursive — `unsafe-auth-resolver` (and peers using the idiom) scan 18% of `lib/`. +2. **decidesk**: 21 `x-openregister-seeds` (plural) declarations are inert — migrate to `components.objects`. +3. **scholiq**: 22 now-dropped empty `x-openregister-seed` declarations — remove (harmless, but noisy warnings). +4. **openregister**: 25 `@spec` tags → evaporated change dirs in `PermissionHandler.php` (5056 fleet-wide). +5. **openregister**: gate-52's 2 orphaned cache-clear methods — wire or remove. diff --git a/scratchpad/baseline.txt b/scratchpad/baseline.txt new file mode 100644 index 0000000000..a33a367eba --- /dev/null +++ b/scratchpad/baseline.txt @@ -0,0 +1,3 @@ +BASELINE — clean origin/development (f6d0b339a), phpunit-unit-local.xml, php:8.3 + zip/bcmath/soap/xsl/intl/gd, memory_limit=2G +Tests: 14717, Assertions: 32218, Errors: 54, Failures: 18, Warnings: 8, Deprecations: 23, PHPUnit Deprecations: 6, Skipped: 22. +NOTE: phpunit-unit.xml (the CI config) cannot run outside an NC root — fatals at bootstrap. phpunit-unit-local.xml is the standalone equivalent. diff --git a/scripts/build-features-manifest.js b/scripts/build-features-manifest.js new file mode 100755 index 0000000000..5f00b438be --- /dev/null +++ b/scripts/build-features-manifest.js @@ -0,0 +1,222 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +/** + * scripts/build-features-manifest.js + * + * Local manifest builder for the `add-features-roadmap-menu` pilot. Mirrors + * the contract documented for the (yet-to-be-published) `@conduction/openspec-manifest` + * package so the pilot is exercised end-to-end from this repo: + * + * - discovers `openspec/specs//spec.md` (falls back to `specs//spec.md`) + * - reads YAML frontmatter; includes only `status: implemented` or `status: reviewed` + * - extracts first H1 → `title`; first paragraph under `## Purpose` → `summary` + * - resolves `docsUrl`: explicit frontmatter wins (https-only); otherwise + * `https://///blob//openspec/specs//spec.md` + * derived from `git config remote.origin.url` + the SCM default-branch ref + * - sorts alphabetically by title (locale-aware, case-insensitive) + * - emits `docs/features.json` with shape + * `{ schemaVersion: 1, generatedAt: , features: [...] }` + * + * Once `@conduction/openspec-manifest` ships, this script is to be replaced + * with the `openspec-manifest build` CLI invocation. The output shape is + * identical so the swap is transparent to downstream consumers. + * + * Usage: + * node scripts/build-features-manifest.js [--cwd ] [--check] + * + * --check Build + diff against the committed `docs/features.json`. Non-zero + * exit if drift is detected (for the CI `--exit-code` step). + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +function parseArgs(argv) { + const args = { cwd: process.cwd(), check: false }; + for (let i = 2; i < argv.length; i += 1) { + if (argv[i] === '--cwd' && argv[i + 1]) { + args.cwd = path.resolve(argv[i + 1]); + i += 1; + } else if (argv[i] === '--check') { + args.check = true; + } + } + return args; +} + +function findSpecDir(cwd) { + const candidates = [ + path.join(cwd, 'openspec', 'specs'), + path.join(cwd, 'specs'), + ]; + const present = candidates.filter((c) => fs.existsSync(c)); + if (present.length === 0) { + return null; + } + if (present.length === 2) { + console.warn( + `[build-features-manifest] both ./openspec/specs and ./specs exist; preferring ./openspec/specs`, + ); + } + return present[0]; +} + +function parseFrontmatter(content) { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); + if (!match) { + return { meta: null, body: content }; + } + const meta = {}; + for (const line of match[1].split(/\r?\n/)) { + const kv = line.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.*)$/); + if (kv) { + meta[kv[1]] = kv[2].trim().replace(/^['"]|['"]$/g, ''); + } + } + return { meta, body: match[2] }; +} + +function extractTitle(body) { + const m = body.match(/^\s*#\s+(.+?)\s*$/m); + return m ? m[1].trim() : null; +} + +function extractSummary(body) { + const idx = body.search(/^##\s+Purpose\s*$/m); + if (idx === -1) return null; + const after = body.slice(idx).replace(/^##\s+Purpose\s*$/m, ''); + const paragraphs = after.split(/\r?\n\s*\r?\n/); + for (const p of paragraphs) { + const trimmed = p.trim(); + if (!trimmed) continue; + if (trimmed.startsWith('@e2e') || trimmed.startsWith('@spec')) continue; + if (trimmed.startsWith('**Source**')) continue; + if (/^#/.test(trimmed)) break; + return trimmed.replace(/\s+/g, ' '); + } + return null; +} + +function resolveGitRemote(cwd) { + try { + const url = execSync('git config --get remote.origin.url', { + cwd, stdio: ['ignore', 'pipe', 'ignore'], + }).toString().trim(); + const sshMatch = url.match(/^git@([^:]+):(.+?)(?:\.git)?$/); + const httpsMatch = url.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/); + const m = sshMatch || httpsMatch; + if (!m) return null; + const [, host, repoPath] = m; + const parts = repoPath.split('/'); + if (parts.length < 2) return null; + const owner = parts[parts.length - 2]; + const repo = parts[parts.length - 1]; + return { host, owner, repo }; + } catch (_) { + return null; + } +} + +function resolveDefaultBranch(cwd) { + try { + const out = execSync('git symbolic-ref refs/remotes/origin/HEAD', { + cwd, stdio: ['ignore', 'pipe', 'ignore'], + }).toString().trim(); + const m = out.match(/refs\/remotes\/origin\/(.+)$/); + return m ? m[1] : null; + } catch (_) { + return null; + } +} + +function buildDocsUrl({ frontmatterUrl, slug, host, owner, repo, branch }) { + if (frontmatterUrl) { + try { + const u = new URL(frontmatterUrl); + if (u.protocol !== 'https:' || !u.hostname) { + console.warn( + `[build-features-manifest] spec ${slug}: docsUrl frontmatter rejected (not https or empty host) — falling back`, + ); + } else { + return frontmatterUrl; + } + } catch (_) { + console.warn( + `[build-features-manifest] spec ${slug}: docsUrl frontmatter is not a valid URL — falling back`, + ); + } + } + if (!host || !owner || !repo || !branch) { + return null; + } + return `https://${host}/${owner}/${repo}/blob/${branch}/openspec/specs/${slug}/spec.md`; +} + +function build(cwd) { + const specsDir = findSpecDir(cwd); + if (!specsDir) { + return { schemaVersion: 1, generatedAt: new Date().toISOString(), features: [] }; + } + const remote = resolveGitRemote(cwd); + const branch = resolveDefaultBranch(cwd); + const entries = fs.readdirSync(specsDir, { withFileTypes: true }) + .filter((e) => e.isDirectory()); + const features = []; + for (const entry of entries) { + const slug = entry.name; + const specPath = path.join(specsDir, slug, 'spec.md'); + if (!fs.existsSync(specPath)) continue; + const raw = fs.readFileSync(specPath, 'utf8'); + const { meta, body } = parseFrontmatter(raw); + if (!meta || !meta.status) { + console.warn(`[build-features-manifest] spec ${slug}: missing frontmatter or status — skipping`); + continue; + } + if (meta.status !== 'implemented' && meta.status !== 'reviewed') { + continue; + } + const title = extractTitle(body); + const summary = extractSummary(body); + if (!title || !summary) { + console.warn(`[build-features-manifest] spec ${slug}: missing title or purpose — skipping`); + continue; + } + const docsUrl = buildDocsUrl({ + frontmatterUrl: meta.docsUrl, slug, + host: remote?.host, owner: remote?.owner, repo: remote?.repo, branch, + }); + features.push({ slug, title, summary, ...(docsUrl ? { docsUrl } : {}) }); + } + features.sort((a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: 'base' })); + return { schemaVersion: 1, generatedAt: new Date().toISOString(), features }; +} + +function main() { + const args = parseArgs(process.argv); + const manifest = build(args.cwd); + const outPath = path.join(args.cwd, 'docs', 'features.json'); + const serialized = JSON.stringify(manifest, null, 2) + '\n'; + if (args.check) { + const existing = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : ''; + const existingNoTimestamp = existing.replace(/"generatedAt"\s*:\s*"[^"]+"/, '"generatedAt":""'); + const newNoTimestamp = serialized.replace(/"generatedAt"\s*:\s*"[^"]+"/, '"generatedAt":""'); + if (existingNoTimestamp === newNoTimestamp) { + console.log('[build-features-manifest] docs/features.json is up-to-date.'); + process.exit(0); + } + console.error('[build-features-manifest] DRIFT detected — run `node scripts/build-features-manifest.js` and commit the result.'); + process.exit(1); + } + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, serialized); + console.log(`[build-features-manifest] wrote ${manifest.features.length} features to ${path.relative(args.cwd, outPath)}`); +} + +if (require.main === module) { + main(); +} + +module.exports = { build, parseFrontmatter, extractTitle, extractSummary, buildDocsUrl }; diff --git a/src/App.vue b/src/App.vue index 4b7e1cd643..21e36228cf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,7 @@ Edit - - - Publish - - - - Depublish -